summaryrefslogtreecommitdiffstats
path: root/src/lib
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-28 09:51:24 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-28 09:51:24 +0000
commitf7548d6d28c313cf80e6f3ef89aed16a19815df1 (patch)
treea3f6f2a3f247293bee59ecd28e8cd8ceb6ca064a /src/lib
parentInitial commit. (diff)
downloaddovecot-f7548d6d28c313cf80e6f3ef89aed16a19815df1.tar.xz
dovecot-f7548d6d28c313cf80e6f3ef89aed16a19815df1.zip
Adding upstream version 1:2.3.19.1+dfsg1.upstream/1%2.3.19.1+dfsg1upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to '')
-rw-r--r--src/lib-auth/Makefile.am46
-rw-r--r--src/lib-auth/Makefile.in877
-rw-r--r--src/lib-auth/auth-client-connection.c552
-rw-r--r--src/lib-auth/auth-client-interface.h43
-rw-r--r--src/lib-auth/auth-client-private.h88
-rw-r--r--src/lib-auth/auth-client-request.c359
-rw-r--r--src/lib-auth/auth-client.c112
-rw-r--r--src/lib-auth/auth-client.h124
-rw-r--r--src/lib-auth/auth-master.c1030
-rw-r--r--src/lib-auth/auth-master.h76
-rw-r--r--src/lib-auth/test-auth-master.c74
-rw-r--r--src/lib-charset/Makefile.am37
-rw-r--r--src/lib-charset/Makefile.in862
-rw-r--r--src/lib-charset/charset-iconv.c147
-rw-r--r--src/lib-charset/charset-utf8-only.c51
-rw-r--r--src/lib-charset/charset-utf8-private.h21
-rw-r--r--src/lib-charset/charset-utf8.c99
-rw-r--r--src/lib-charset/charset-utf8.h53
-rw-r--r--src/lib-charset/test-charset.c231
-rw-r--r--src/lib-compression/Makefile.am61
-rw-r--r--src/lib-compression/Makefile.in976
-rw-r--r--src/lib-compression/bench-compression.c168
-rw-r--r--src/lib-compression/compression.c250
-rw-r--r--src/lib-compression/compression.h55
-rw-r--r--src/lib-compression/iostream-lz4.h30
-rw-r--r--src/lib-compression/iostream-zstd-private.h35
-rw-r--r--src/lib-compression/istream-bzlib.c231
-rw-r--r--src/lib-compression/istream-decompress.c258
-rw-r--r--src/lib-compression/istream-lz4.c281
-rw-r--r--src/lib-compression/istream-lzma.c264
-rw-r--r--src/lib-compression/istream-zlib.c431
-rw-r--r--src/lib-compression/istream-zlib.h11
-rw-r--r--src/lib-compression/istream-zstd.c268
-rw-r--r--src/lib-compression/ostream-bzlib.c307
-rw-r--r--src/lib-compression/ostream-lz4.c249
-rw-r--r--src/lib-compression/ostream-zlib.c392
-rw-r--r--src/lib-compression/ostream-zlib.h26
-rw-r--r--src/lib-compression/ostream-zstd.c243
-rw-r--r--src/lib-compression/test-compression.c1122
-rw-r--r--src/lib-dcrypt/Makefile.am75
-rw-r--r--src/lib-dcrypt/Makefile.in1142
-rw-r--r--src/lib-dcrypt/dcrypt-iostream.h16
-rw-r--r--src/lib-dcrypt/dcrypt-openssl.c3807
-rw-r--r--src/lib-dcrypt/dcrypt-private.h211
-rw-r--r--src/lib-dcrypt/dcrypt.c660
-rw-r--r--src/lib-dcrypt/dcrypt.h409
-rw-r--r--src/lib-dcrypt/istream-decrypt.c1146
-rw-r--r--src/lib-dcrypt/istream-decrypt.h48
-rw-r--r--src/lib-dcrypt/ostream-encrypt.c800
-rw-r--r--src/lib-dcrypt/ostream-encrypt.h30
-rw-r--r--src/lib-dcrypt/sample-v1.asc48
-rw-r--r--src/lib-dcrypt/sample-v1_short.asc4
-rw-r--r--src/lib-dcrypt/sample-v2.asc287
-rw-r--r--src/lib-dcrypt/test-crypto.c1314
-rw-r--r--src/lib-dcrypt/test-stream.c639
-rw-r--r--src/lib-dict-backend/Makefile.am116
-rw-r--r--src/lib-dict-backend/Makefile.in1016
-rw-r--r--src/lib-dict-backend/dict-cdb.c266
-rw-r--r--src/lib-dict-backend/dict-ldap-settings.c313
-rw-r--r--src/lib-dict-backend/dict-ldap-settings.h36
-rw-r--r--src/lib-dict-backend/dict-ldap.c500
-rw-r--r--src/lib-dict-backend/dict-sql-private.h12
-rw-r--r--src/lib-dict-backend/dict-sql-settings.c345
-rw-r--r--src/lib-dict-backend/dict-sql-settings.h47
-rw-r--r--src/lib-dict-backend/dict-sql.c1564
-rw-r--r--src/lib-dict-backend/dict-sql.h7
-rw-r--r--src/lib-dict-backend/dict.conf49
-rw-r--r--src/lib-dict-backend/test-dict-sql.c314
-rw-r--r--src/lib-dict-extra/Makefile.am35
-rw-r--r--src/lib-dict-extra/Makefile.in796
-rw-r--r--src/lib-dict-extra/dict-fs.c330
-rw-r--r--src/lib-dict-extra/dict-register.c30
-rw-r--r--src/lib-dict-extra/test-dict-fs.c106
-rw-r--r--src/lib-dict/Makefile.am73
-rw-r--r--src/lib-dict/Makefile.in970
-rw-r--r--src/lib-dict/dict-client.c1492
-rw-r--r--src/lib-dict/dict-client.h47
-rw-r--r--src/lib-dict/dict-fail.c134
-rw-r--r--src/lib-dict/dict-file.c709
-rw-r--r--src/lib-dict/dict-iter-lua.c193
-rw-r--r--src/lib-dict/dict-lua-private.h9
-rw-r--r--src/lib-dict/dict-lua.c117
-rw-r--r--src/lib-dict/dict-lua.h18
-rw-r--r--src/lib-dict/dict-memcached-ascii.c685
-rw-r--r--src/lib-dict/dict-memcached.c373
-rw-r--r--src/lib-dict/dict-private.h123
-rw-r--r--src/lib-dict/dict-redis.c831
-rw-r--r--src/lib-dict/dict-transaction-memory.c59
-rw-r--r--src/lib-dict/dict-transaction-memory.h38
-rw-r--r--src/lib-dict/dict-txn-lua.c262
-rw-r--r--src/lib-dict/dict.c759
-rw-r--r--src/lib-dict/dict.h200
-rw-r--r--src/lib-dict/test-dict-client.c106
-rw-r--r--src/lib-dict/test-dict.c46
-rw-r--r--src/lib-dns/Makefile.am36
-rw-r--r--src/lib-dns/Makefile.in874
-rw-r--r--src/lib-dns/dns-lookup.c438
-rw-r--r--src/lib-dns/dns-lookup.h90
-rw-r--r--src/lib-dns/dns-util.c85
-rw-r--r--src/lib-dns/dns-util.h36
-rw-r--r--src/lib-dns/test-dns-util.c124
-rw-r--r--src/lib-dovecot/Makefile.am11
-rw-r--r--src/lib-dovecot/Makefile.in705
-rw-r--r--src/lib-fs/Makefile.am70
-rw-r--r--src/lib-fs/Makefile.in955
-rw-r--r--src/lib-fs/fs-api-private.h213
-rw-r--r--src/lib-fs/fs-api.c1407
-rw-r--r--src/lib-fs/fs-api.h397
-rw-r--r--src/lib-fs/fs-dict.c372
-rw-r--r--src/lib-fs/fs-metawrap.c526
-rw-r--r--src/lib-fs/fs-posix.c1028
-rw-r--r--src/lib-fs/fs-randomfail.c555
-rw-r--r--src/lib-fs/fs-sis-common.c59
-rw-r--r--src/lib-fs/fs-sis-common.h14
-rw-r--r--src/lib-fs/fs-sis-queue.c210
-rw-r--r--src/lib-fs/fs-sis.c369
-rw-r--r--src/lib-fs/fs-test-async.c103
-rw-r--r--src/lib-fs/fs-test.c443
-rw-r--r--src/lib-fs/fs-test.h45
-rw-r--r--src/lib-fs/fs-wrapper.c172
-rw-r--r--src/lib-fs/fs-wrapper.h40
-rw-r--r--src/lib-fs/istream-fs-file.c61
-rw-r--r--src/lib-fs/istream-fs-file.h13
-rw-r--r--src/lib-fs/istream-fs-stats.c47
-rw-r--r--src/lib-fs/istream-fs-stats.h9
-rw-r--r--src/lib-fs/istream-metawrap.c152
-rw-r--r--src/lib-fs/istream-metawrap.h14
-rw-r--r--src/lib-fs/ostream-cmp.c95
-rw-r--r--src/lib-fs/ostream-cmp.h15
-rw-r--r--src/lib-fs/ostream-metawrap.c71
-rw-r--r--src/lib-fs/ostream-metawrap.h8
-rw-r--r--src/lib-fs/test-fs-metawrap.c103
-rw-r--r--src/lib-fs/test-fs-posix.c144
-rw-r--r--src/lib-fts/Makefile.am156
-rw-r--r--src/lib-fts/Makefile.in1139
-rw-r--r--src/lib-fts/PropList.txt1579
-rw-r--r--src/lib-fts/WordBreakProperty.txt1298
-rw-r--r--src/lib-fts/fts-common.h48
-rw-r--r--src/lib-fts/fts-filter-common.c20
-rw-r--r--src/lib-fts/fts-filter-common.h6
-rw-r--r--src/lib-fts/fts-filter-contractions.c86
-rw-r--r--src/lib-fts/fts-filter-english-possessive.c47
-rw-r--r--src/lib-fts/fts-filter-lowercase.c71
-rw-r--r--src/lib-fts/fts-filter-normalizer-icu.c145
-rw-r--r--src/lib-fts/fts-filter-private.h35
-rw-r--r--src/lib-fts/fts-filter-stemmer-snowball.c129
-rw-r--r--src/lib-fts/fts-filter-stopwords.c127
-rw-r--r--src/lib-fts/fts-filter.c140
-rw-r--r--src/lib-fts/fts-filter.h71
-rw-r--r--src/lib-fts/fts-icu.c204
-rw-r--r--src/lib-fts/fts-icu.h28
-rw-r--r--src/lib-fts/fts-language.c368
-rw-r--r--src/lib-fts/fts-language.h72
-rw-r--r--src/lib-fts/fts-library.c21
-rw-r--r--src/lib-fts/fts-library.h7
-rw-r--r--src/lib-fts/fts-tokenizer-address.c412
-rw-r--r--src/lib-fts/fts-tokenizer-common.c35
-rw-r--r--src/lib-fts/fts-tokenizer-common.h9
-rw-r--r--src/lib-fts/fts-tokenizer-generic-private.h57
-rw-r--r--src/lib-fts/fts-tokenizer-generic.c906
-rw-r--r--src/lib-fts/fts-tokenizer-private.h52
-rw-r--r--src/lib-fts/fts-tokenizer.c260
-rw-r--r--src/lib-fts/fts-tokenizer.h87
-rw-r--r--src/lib-fts/stopwords/stopwords_da.txt109
-rw-r--r--src/lib-fts/stopwords/stopwords_de.txt293
-rw-r--r--src/lib-fts/stopwords/stopwords_en.txt54
-rw-r--r--src/lib-fts/stopwords/stopwords_es.txt355
-rw-r--r--src/lib-fts/stopwords/stopwords_fi.txt260
-rw-r--r--src/lib-fts/stopwords/stopwords_fr.txt184
-rw-r--r--src/lib-fts/stopwords/stopwords_it.txt302
-rw-r--r--src/lib-fts/stopwords/stopwords_malformed.txt2866
-rw-r--r--src/lib-fts/stopwords/stopwords_nl.txt118
-rw-r--r--src/lib-fts/stopwords/stopwords_no.txt192
-rw-r--r--src/lib-fts/stopwords/stopwords_pt.txt252
-rw-r--r--src/lib-fts/stopwords/stopwords_ro.txt233
-rw-r--r--src/lib-fts/stopwords/stopwords_ru.txt242
-rw-r--r--src/lib-fts/stopwords/stopwords_sv.txt131
-rw-r--r--src/lib-fts/stopwords/stopwords_tr.txt529
-rw-r--r--src/lib-fts/test-fts-filter.c1025
-rw-r--r--src/lib-fts/test-fts-icu.c202
-rw-r--r--src/lib-fts/test-fts-language.c323
-rw-r--r--src/lib-fts/test-fts-tokenizer.c690
-rw-r--r--src/lib-fts/udhr_fra.txt217
-rw-r--r--src/lib-fts/word-boundary-data.c3951
-rw-r--r--src/lib-fts/word-break-data.c58
-rw-r--r--src/lib-fts/word-properties.pl34
-rw-r--r--src/lib-http/Makefile.am211
-rw-r--r--src/lib-http/Makefile.in1310
-rw-r--r--src/lib-http/http-auth.c476
-rw-r--r--src/lib-http/http-auth.h79
-rw-r--r--src/lib-http/http-client-connection.c1954
-rw-r--r--src/lib-http/http-client-host.c500
-rw-r--r--src/lib-http/http-client-peer.c1383
-rw-r--r--src/lib-http/http-client-private.h718
-rw-r--r--src/lib-http/http-client-queue.c1065
-rw-r--r--src/lib-http/http-client-request.c1875
-rw-r--r--src/lib-http/http-client.c740
-rw-r--r--src/lib-http/http-client.h496
-rw-r--r--src/lib-http/http-common.h7
-rw-r--r--src/lib-http/http-date.c487
-rw-r--r--src/lib-http/http-date.h17
-rw-r--r--src/lib-http/http-header-parser.c367
-rw-r--r--src/lib-http/http-header-parser.h24
-rw-r--r--src/lib-http/http-header.c98
-rw-r--r--src/lib-http/http-header.h45
-rw-r--r--src/lib-http/http-message-parser.c658
-rw-r--r--src/lib-http/http-message-parser.h77
-rw-r--r--src/lib-http/http-parser.c208
-rw-r--r--src/lib-http/http-parser.h63
-rw-r--r--src/lib-http/http-request-parser.c635
-rw-r--r--src/lib-http/http-request-parser.h43
-rw-r--r--src/lib-http/http-request.c32
-rw-r--r--src/lib-http/http-request.h84
-rw-r--r--src/lib-http/http-response-parser.c422
-rw-r--r--src/lib-http/http-response-parser.h26
-rw-r--r--src/lib-http/http-response.c46
-rw-r--r--src/lib-http/http-response.h87
-rw-r--r--src/lib-http/http-server-connection.c1180
-rw-r--r--src/lib-http/http-server-ostream.c328
-rw-r--r--src/lib-http/http-server-private.h357
-rw-r--r--src/lib-http/http-server-request.c1006
-rw-r--r--src/lib-http/http-server-resource.c276
-rw-r--r--src/lib-http/http-server-response.c801
-rw-r--r--src/lib-http/http-server.c132
-rw-r--r--src/lib-http/http-server.h427
-rw-r--r--src/lib-http/http-transfer-chunked.c749
-rw-r--r--src/lib-http/http-transfer.h26
-rw-r--r--src/lib-http/http-url.c678
-rw-r--r--src/lib-http/http-url.h108
-rw-r--r--src/lib-http/test-http-auth.c274
-rw-r--r--src/lib-http/test-http-client-errors.c3944
-rw-r--r--src/lib-http/test-http-client-request.c95
-rw-r--r--src/lib-http/test-http-client.c472
-rw-r--r--src/lib-http/test-http-date.c223
-rw-r--r--src/lib-http/test-http-header-parser.c381
-rw-r--r--src/lib-http/test-http-payload.c2445
-rw-r--r--src/lib-http/test-http-request-parser.c701
-rw-r--r--src/lib-http/test-http-response-parser.c406
-rw-r--r--src/lib-http/test-http-server-errors.c1042
-rw-r--r--src/lib-http/test-http-server.c245
-rw-r--r--src/lib-http/test-http-transfer.c347
-rw-r--r--src/lib-http/test-http-url.c950
-rw-r--r--src/lib-imap-client/Makefile.am53
-rw-r--r--src/lib-imap-client/Makefile.in880
-rw-r--r--src/lib-imap-client/imapc-client-private.h63
-rw-r--r--src/lib-imap-client/imapc-client.c584
-rw-r--r--src/lib-imap-client/imapc-client.h250
-rw-r--r--src/lib-imap-client/imapc-connection.c2513
-rw-r--r--src/lib-imap-client/imapc-connection.h63
-rw-r--r--src/lib-imap-client/imapc-msgmap.c89
-rw-r--r--src/lib-imap-client/imapc-msgmap.h18
-rw-r--r--src/lib-imap-client/test-imapc-client.c901
-rw-r--r--src/lib-imap-storage/Makefile.am24
-rw-r--r--src/lib-imap-storage/Makefile.in825
-rw-r--r--src/lib-imap-storage/imap-metadata.c314
-rw-r--r--src/lib-imap-storage/imap-metadata.h61
-rw-r--r--src/lib-imap-storage/imap-msgpart-url.c287
-rw-r--r--src/lib-imap-storage/imap-msgpart-url.h50
-rw-r--r--src/lib-imap-storage/imap-msgpart.c860
-rw-r--r--src/lib-imap-storage/imap-msgpart.h68
-rw-r--r--src/lib-imap-urlauth/Makefile.am29
-rw-r--r--src/lib-imap-urlauth/Makefile.in835
-rw-r--r--src/lib-imap-urlauth/imap-urlauth-backend.c174
-rw-r--r--src/lib-imap-urlauth/imap-urlauth-backend.h16
-rw-r--r--src/lib-imap-urlauth/imap-urlauth-connection.c1027
-rw-r--r--src/lib-imap-urlauth/imap-urlauth-connection.h42
-rw-r--r--src/lib-imap-urlauth/imap-urlauth-fetch.c530
-rw-r--r--src/lib-imap-urlauth/imap-urlauth-fetch.h56
-rw-r--r--src/lib-imap-urlauth/imap-urlauth-private.h20
-rw-r--r--src/lib-imap-urlauth/imap-urlauth.c486
-rw-r--r--src/lib-imap-urlauth/imap-urlauth.h55
-rw-r--r--src/lib-imap/Makefile.am120
-rw-r--r--src/lib-imap/Makefile.in1196
-rw-r--r--src/lib-imap/fuzz-imap-bodystructure.c52
-rw-r--r--src/lib-imap/fuzz-imap-utf7.c15
-rw-r--r--src/lib-imap/imap-arg.c137
-rw-r--r--src/lib-imap/imap-arg.h108
-rw-r--r--src/lib-imap/imap-base-subject.c248
-rw-r--r--src/lib-imap/imap-base-subject.h13
-rw-r--r--src/lib-imap/imap-bodystructure.c956
-rw-r--r--src/lib-imap/imap-bodystructure.h39
-rw-r--r--src/lib-imap/imap-date.c247
-rw-r--r--src/lib-imap/imap-date.h25
-rw-r--r--src/lib-imap/imap-envelope.c248
-rw-r--r--src/lib-imap/imap-envelope.h20
-rw-r--r--src/lib-imap/imap-id.c173
-rw-r--r--src/lib-imap/imap-id.h19
-rw-r--r--src/lib-imap/imap-keepalive.c47
-rw-r--r--src/lib-imap/imap-keepalive.h24
-rw-r--r--src/lib-imap/imap-match.c382
-rw-r--r--src/lib-imap/imap-match.h42
-rw-r--r--src/lib-imap/imap-parser.c1023
-rw-r--r--src/lib-imap/imap-parser.h117
-rw-r--r--src/lib-imap/imap-quote.c239
-rw-r--r--src/lib-imap/imap-quote.h21
-rw-r--r--src/lib-imap/imap-resp-code.h28
-rw-r--r--src/lib-imap/imap-seqset.c105
-rw-r--r--src/lib-imap/imap-seqset.h15
-rw-r--r--src/lib-imap/imap-url.c1009
-rw-r--r--src/lib-imap/imap-url.h71
-rw-r--r--src/lib-imap/imap-utf7.c380
-rw-r--r--src/lib-imap/imap-utf7.h28
-rw-r--r--src/lib-imap/imap-util.c202
-rw-r--r--src/lib-imap/imap-util.h29
-rw-r--r--src/lib-imap/test-imap-bodystructure.c733
-rw-r--r--src/lib-imap/test-imap-envelope.c205
-rw-r--r--src/lib-imap/test-imap-match.c127
-rw-r--r--src/lib-imap/test-imap-parser.c157
-rw-r--r--src/lib-imap/test-imap-quote.c171
-rw-r--r--src/lib-imap/test-imap-url.c1029
-rw-r--r--src/lib-imap/test-imap-utf7.c216
-rw-r--r--src/lib-imap/test-imap-util.c79
-rw-r--r--src/lib-index/Makefile.am150
-rw-r--r--src/lib-index/Makefile.in1285
-rw-r--r--src/lib-index/mail-cache-decisions.c238
-rw-r--r--src/lib-index/mail-cache-fields.c660
-rw-r--r--src/lib-index/mail-cache-lookup.c694
-rw-r--r--src/lib-index/mail-cache-private.h421
-rw-r--r--src/lib-index/mail-cache-purge.c707
-rw-r--r--src/lib-index/mail-cache-sync-update.c68
-rw-r--r--src/lib-index/mail-cache-transaction.c929
-rw-r--r--src/lib-index/mail-cache.c1005
-rw-r--r--src/lib-index/mail-cache.h193
-rw-r--r--src/lib-index/mail-index-alloc-cache.c315
-rw-r--r--src/lib-index/mail-index-alloc-cache.h20
-rw-r--r--src/lib-index/mail-index-dummy-view.c47
-rw-r--r--src/lib-index/mail-index-fsck.c495
-rw-r--r--src/lib-index/mail-index-lock.c63
-rw-r--r--src/lib-index/mail-index-map-hdr.c359
-rw-r--r--src/lib-index/mail-index-map-read.c519
-rw-r--r--src/lib-index/mail-index-map.c595
-rw-r--r--src/lib-index/mail-index-modseq.c733
-rw-r--r--src/lib-index/mail-index-modseq.h66
-rw-r--r--src/lib-index/mail-index-private.h437
-rw-r--r--src/lib-index/mail-index-strmap.c1259
-rw-r--r--src/lib-index/mail-index-strmap.h81
-rw-r--r--src/lib-index/mail-index-sync-ext.c735
-rw-r--r--src/lib-index/mail-index-sync-keywords.c347
-rw-r--r--src/lib-index/mail-index-sync-private.h104
-rw-r--r--src/lib-index/mail-index-sync-update.c1087
-rw-r--r--src/lib-index/mail-index-sync.c1062
-rw-r--r--src/lib-index/mail-index-transaction-export.c533
-rw-r--r--src/lib-index/mail-index-transaction-finish.c350
-rw-r--r--src/lib-index/mail-index-transaction-private.h165
-rw-r--r--src/lib-index/mail-index-transaction-sort-appends.c184
-rw-r--r--src/lib-index/mail-index-transaction-update.c1367
-rw-r--r--src/lib-index/mail-index-transaction-view.c534
-rw-r--r--src/lib-index/mail-index-transaction.c360
-rw-r--r--src/lib-index/mail-index-util.c138
-rw-r--r--src/lib-index/mail-index-util.h22
-rw-r--r--src/lib-index/mail-index-view-private.h120
-rw-r--r--src/lib-index/mail-index-view-sync.c1045
-rw-r--r--src/lib-index/mail-index-view.c651
-rw-r--r--src/lib-index/mail-index-write.c215
-rw-r--r--src/lib-index/mail-index.c1110
-rw-r--r--src/lib-index/mail-index.h817
-rw-r--r--src/lib-index/mail-transaction-log-append.c256
-rw-r--r--src/lib-index/mail-transaction-log-file.c1685
-rw-r--r--src/lib-index/mail-transaction-log-modseq.c298
-rw-r--r--src/lib-index/mail-transaction-log-private.h199
-rw-r--r--src/lib-index/mail-transaction-log-view-private.h33
-rw-r--r--src/lib-index/mail-transaction-log-view.c909
-rw-r--r--src/lib-index/mail-transaction-log.c664
-rw-r--r--src/lib-index/mail-transaction-log.h494
-rw-r--r--src/lib-index/mailbox-log.c292
-rw-r--r--src/lib-index/mailbox-log.h44
-rw-r--r--src/lib-index/test-mail-cache-common.c166
-rw-r--r--src/lib-index/test-mail-cache-fields.c112
-rw-r--r--src/lib-index/test-mail-cache-purge.c1076
-rw-r--r--src/lib-index/test-mail-cache.c764
-rw-r--r--src/lib-index/test-mail-cache.h32
-rw-r--r--src/lib-index/test-mail-index-map.c57
-rw-r--r--src/lib-index/test-mail-index-modseq.c77
-rw-r--r--src/lib-index/test-mail-index-sync-ext.c86
-rw-r--r--src/lib-index/test-mail-index-transaction-finish.c297
-rw-r--r--src/lib-index/test-mail-index-transaction-update.c683
-rw-r--r--src/lib-index/test-mail-index-write.c151
-rw-r--r--src/lib-index/test-mail-index.c169
-rw-r--r--src/lib-index/test-mail-index.h51
-rw-r--r--src/lib-index/test-mail-transaction-log-append.c176
-rw-r--r--src/lib-index/test-mail-transaction-log-file.c418
-rw-r--r--src/lib-index/test-mail-transaction-log-view.c268
-rw-r--r--src/lib-lda/Makefile.am35
-rw-r--r--src/lib-lda/Makefile.in878
-rw-r--r--src/lib-lda/lda-settings.c80
-rw-r--r--src/lib-lda/lda-settings.h21
-rw-r--r--src/lib-lda/mail-deliver.c810
-rw-r--r--src/lib-lda/mail-deliver.h185
-rw-r--r--src/lib-lda/mail-send.c216
-rw-r--r--src/lib-lda/mail-send.h11
-rw-r--r--src/lib-ldap/Makefile.am42
-rw-r--r--src/lib-ldap/Makefile.in890
-rw-r--r--src/lib-ldap/ldap-client.c74
-rw-r--r--src/lib-ldap/ldap-client.h102
-rw-r--r--src/lib-ldap/ldap-compare.c121
-rw-r--r--src/lib-ldap/ldap-connection-pool.c113
-rw-r--r--src/lib-ldap/ldap-connection-pool.h27
-rw-r--r--src/lib-ldap/ldap-connection.c714
-rw-r--r--src/lib-ldap/ldap-entry.c72
-rw-r--r--src/lib-ldap/ldap-iterator.c29
-rw-r--r--src/lib-ldap/ldap-private.h129
-rw-r--r--src/lib-ldap/ldap-search.c169
-rw-r--r--src/lib-lua/Makefile.am68
-rw-r--r--src/lib-lua/Makefile.in971
-rw-r--r--src/lib-lua/dlua-compat.c157
-rw-r--r--src/lib-lua/dlua-compat.h69
-rw-r--r--src/lib-lua/dlua-dovecot-http.c519
-rw-r--r--src/lib-lua/dlua-dovecot.c681
-rw-r--r--src/lib-lua/dlua-error.c13
-rw-r--r--src/lib-lua/dlua-pushstring.c26
-rw-r--r--src/lib-lua/dlua-resume.c208
-rw-r--r--src/lib-lua/dlua-script-private.h264
-rw-r--r--src/lib-lua/dlua-script.c453
-rw-r--r--src/lib-lua/dlua-script.h28
-rw-r--r--src/lib-lua/dlua-table.c301
-rw-r--r--src/lib-lua/dlua-thread.c276
-rw-r--r--src/lib-lua/dlua-wrapper.h186
-rw-r--r--src/lib-lua/test-dict-lua.c99
-rw-r--r--src/lib-lua/test-lua.c473
-rw-r--r--src/lib-mail/Makefile.am262
-rw-r--r--src/lib-mail/Makefile.in1629
-rw-r--r--src/lib-mail/fuzz-message-parser.c28
-rw-r--r--src/lib-mail/html-entities.h253
-rw-r--r--src/lib-mail/istream-attachment-connector.c149
-rw-r--r--src/lib-mail/istream-attachment-connector.h28
-rw-r--r--src/lib-mail/istream-attachment-extractor.c740
-rw-r--r--src/lib-mail/istream-attachment-extractor.h62
-rw-r--r--src/lib-mail/istream-binary-converter.c309
-rw-r--r--src/lib-mail/istream-binary-converter.h6
-rw-r--r--src/lib-mail/istream-dot.c236
-rw-r--r--src/lib-mail/istream-dot.h9
-rw-r--r--src/lib-mail/istream-header-filter.c762
-rw-r--r--src/lib-mail/istream-header-filter.h52
-rw-r--r--src/lib-mail/istream-nonuls.c79
-rw-r--r--src/lib-mail/istream-nonuls.h7
-rw-r--r--src/lib-mail/istream-qp-decoder.c140
-rw-r--r--src/lib-mail/istream-qp-encoder.c127
-rw-r--r--src/lib-mail/istream-qp.h12
-rw-r--r--src/lib-mail/mail-html2text.c354
-rw-r--r--src/lib-mail/mail-html2text.h22
-rw-r--r--src/lib-mail/mail-types.h25
-rw-r--r--src/lib-mail/mail-user-hash.c48
-rw-r--r--src/lib-mail/mail-user-hash.h10
-rw-r--r--src/lib-mail/mbox-from.c301
-rw-r--r--src/lib-mail/mbox-from.h12
-rw-r--r--src/lib-mail/message-address.c672
-rw-r--r--src/lib-mail/message-address.h61
-rw-r--r--src/lib-mail/message-binary-part.c44
-rw-r--r--src/lib-mail/message-binary-part.h29
-rw-r--r--src/lib-mail/message-date.c283
-rw-r--r--src/lib-mail/message-date.h12
-rw-r--r--src/lib-mail/message-decoder.c390
-rw-r--r--src/lib-mail/message-decoder.h54
-rw-r--r--src/lib-mail/message-header-decode.c188
-rw-r--r--src/lib-mail/message-header-decode.h22
-rw-r--r--src/lib-mail/message-header-encode.c406
-rw-r--r--src/lib-mail/message-header-encode.h27
-rw-r--r--src/lib-mail/message-header-hash.c72
-rw-r--r--src/lib-mail/message-header-hash.h18
-rw-r--r--src/lib-mail/message-header-parser.c474
-rw-r--r--src/lib-mail/message-header-parser.h85
-rw-r--r--src/lib-mail/message-id.c129
-rw-r--r--src/lib-mail/message-id.h8
-rw-r--r--src/lib-mail/message-parser-from-parts.c365
-rw-r--r--src/lib-mail/message-parser-private.h62
-rw-r--r--src/lib-mail/message-parser.c907
-rw-r--r--src/lib-mail/message-parser.h112
-rw-r--r--src/lib-mail/message-part-data.c594
-rw-r--r--src/lib-mail/message-part-data.h101
-rw-r--r--src/lib-mail/message-part-serialize.c269
-rw-r--r--src/lib-mail/message-part-serialize.h16
-rw-r--r--src/lib-mail/message-part.c103
-rw-r--r--src/lib-mail/message-part.h67
-rw-r--r--src/lib-mail/message-search.c246
-rw-r--r--src/lib-mail/message-search.h40
-rw-r--r--src/lib-mail/message-size.c174
-rw-r--r--src/lib-mail/message-size.h28
-rw-r--r--src/lib-mail/message-snippet.c207
-rw-r--r--src/lib-mail/message-snippet.h14
-rw-r--r--src/lib-mail/ostream-dot.c235
-rw-r--r--src/lib-mail/ostream-dot.h15
-rw-r--r--src/lib-mail/qp-decoder.c285
-rw-r--r--src/lib-mail/qp-decoder.h19
-rw-r--r--src/lib-mail/qp-encoder.c162
-rw-r--r--src/lib-mail/qp-encoder.h25
-rw-r--r--src/lib-mail/quoted-printable.c49
-rw-r--r--src/lib-mail/quoted-printable.h8
-rw-r--r--src/lib-mail/rfc2231-parser.c179
-rw-r--r--src/lib-mail/rfc2231-parser.h13
-rw-r--r--src/lib-mail/rfc822-parser.c522
-rw-r--r--src/lib-mail/rfc822-parser.h71
-rw-r--r--src/lib-mail/test-istream-attachment.c486
-rw-r--r--src/lib-mail/test-istream-binary-converter.c215
-rw-r--r--src/lib-mail/test-istream-dot.c230
-rw-r--r--src/lib-mail/test-istream-header-filter.c701
-rw-r--r--src/lib-mail/test-istream-qp-decoder.c197
-rw-r--r--src/lib-mail/test-istream-qp-encoder.c160
-rw-r--r--src/lib-mail/test-mail-html2text.c120
-rw-r--r--src/lib-mail/test-mail-user-hash.c185
-rw-r--r--src/lib-mail/test-mbox-from.c104
-rw-r--r--src/lib-mail/test-message-address.c532
-rw-r--r--src/lib-mail/test-message-date.c64
-rw-r--r--src/lib-mail/test-message-decoder.c513
-rw-r--r--src/lib-mail/test-message-header-decode.c216
-rw-r--r--src/lib-mail/test-message-header-encode.c295
-rw-r--r--src/lib-mail/test-message-header-hash.c111
-rw-r--r--src/lib-mail/test-message-header-parser.c479
-rw-r--r--src/lib-mail/test-message-id.c46
-rw-r--r--src/lib-mail/test-message-parser.c1398
-rw-r--r--src/lib-mail/test-message-part-serialize.c266
-rw-r--r--src/lib-mail/test-message-part.c117
-rw-r--r--src/lib-mail/test-message-search.c521
-rw-r--r--src/lib-mail/test-message-size.c159
-rw-r--r--src/lib-mail/test-message-snippet.c329
-rw-r--r--src/lib-mail/test-ostream-dot.c103
-rw-r--r--src/lib-mail/test-qp-decoder.c188
-rw-r--r--src/lib-mail/test-qp-encoder.c206
-rw-r--r--src/lib-mail/test-quoted-printable.c52
-rw-r--r--src/lib-mail/test-rfc2231-parser.c51
-rw-r--r--src/lib-mail/test-rfc822-parser.c445
-rw-r--r--src/lib-master/Makefile.am83
-rw-r--r--src/lib-master/Makefile.in967
-rw-r--r--src/lib-master/anvil-client.c275
-rw-r--r--src/lib-master/anvil-client.h37
-rw-r--r--src/lib-master/ipc-client.c220
-rw-r--r--src/lib-master/ipc-client.h24
-rw-r--r--src/lib-master/ipc-server.c202
-rw-r--r--src/lib-master/ipc-server.h20
-rw-r--r--src/lib-master/master-auth.c286
-rw-r--r--src/lib-master/master-auth.h112
-rw-r--r--src/lib-master/master-instance.c369
-rw-r--r--src/lib-master/master-instance.h42
-rw-r--r--src/lib-master/master-interface.h117
-rw-r--r--src/lib-master/master-login-auth.c642
-rw-r--r--src/lib-master/master-login-auth.h28
-rw-r--r--src/lib-master/master-login.c564
-rw-r--r--src/lib-master/master-login.h50
-rw-r--r--src/lib-master/master-service-haproxy.c689
-rw-r--r--src/lib-master/master-service-private.h109
-rw-r--r--src/lib-master/master-service-settings-cache.c410
-rw-r--r--src/lib-master/master-service-settings-cache.h16
-rw-r--r--src/lib-master/master-service-settings.c816
-rw-r--r--src/lib-master/master-service-settings.h115
-rw-r--r--src/lib-master/master-service-ssl-settings.c275
-rw-r--r--src/lib-master/master-service-ssl-settings.h65
-rw-r--r--src/lib-master/master-service-ssl.c105
-rw-r--r--src/lib-master/master-service-ssl.h16
-rw-r--r--src/lib-master/master-service.c1548
-rw-r--r--src/lib-master/master-service.h262
-rw-r--r--src/lib-master/service-settings.h82
-rw-r--r--src/lib-master/stats-client.c373
-rw-r--r--src/lib-master/stats-client.h8
-rw-r--r--src/lib-master/syslog-util.c65
-rw-r--r--src/lib-master/syslog-util.h14
-rw-r--r--src/lib-master/test-event-stats.c784
-rw-r--r--src/lib-master/test-master-service-settings-cache.c125
-rw-r--r--src/lib-oauth2/Makefile.am71
-rw-r--r--src/lib-oauth2/Makefile.in919
-rw-r--r--src/lib-oauth2/oauth2-jwt.c489
-rw-r--r--src/lib-oauth2/oauth2-key-cache.c158
-rw-r--r--src/lib-oauth2/oauth2-private.h50
-rw-r--r--src/lib-oauth2/oauth2-request.c363
-rw-r--r--src/lib-oauth2/oauth2.c41
-rw-r--r--src/lib-oauth2/oauth2.h149
-rw-r--r--src/lib-oauth2/test-oauth2-json.c104
-rw-r--r--src/lib-oauth2/test-oauth2-jwt.c919
-rw-r--r--src/lib-old-stats/Makefile.am18
-rw-r--r--src/lib-old-stats/Makefile.in820
-rw-r--r--src/lib-old-stats/stats-connection.c119
-rw-r--r--src/lib-old-stats/stats-connection.h11
-rw-r--r--src/lib-old-stats/stats-parser.c178
-rw-r--r--src/lib-old-stats/stats-parser.h29
-rw-r--r--src/lib-old-stats/stats.c229
-rw-r--r--src/lib-old-stats/stats.h71
-rw-r--r--src/lib-otp/Makefile.am17
-rw-r--r--src/lib-otp/Makefile.in771
-rw-r--r--src/lib-otp/otp-dictionary.c589
-rw-r--r--src/lib-otp/otp-dictionary.h6
-rw-r--r--src/lib-otp/otp-hash.c165
-rw-r--r--src/lib-otp/otp-hash.h26
-rw-r--r--src/lib-otp/otp-parity.c29
-rw-r--r--src/lib-otp/otp-parity.h16
-rw-r--r--src/lib-otp/otp-parse.c253
-rw-r--r--src/lib-otp/otp-parse.h16
-rw-r--r--src/lib-otp/otp.h22
-rw-r--r--src/lib-program-client/Makefile.am55
-rw-r--r--src/lib-program-client/Makefile.in912
-rw-r--r--src/lib-program-client/program-client-local.c556
-rw-r--r--src/lib-program-client/program-client-private.h85
-rw-r--r--src/lib-program-client/program-client-remote.c702
-rw-r--r--src/lib-program-client/program-client.c745
-rw-r--r--src/lib-program-client/program-client.h101
-rw-r--r--src/lib-program-client/test-program-client-local.c289
-rw-r--r--src/lib-program-client/test-program-client-net.c545
-rw-r--r--src/lib-program-client/test-program-client-unix.c441
-rw-r--r--src/lib-sasl/Makefile.am40
-rw-r--r--src/lib-sasl/Makefile.in870
-rw-r--r--src/lib-sasl/dsasl-client-private.h45
-rw-r--r--src/lib-sasl/dsasl-client.c152
-rw-r--r--src/lib-sasl/dsasl-client.h50
-rw-r--r--src/lib-sasl/mech-external.c59
-rw-r--r--src/lib-sasl/mech-login.c75
-rw-r--r--src/lib-sasl/mech-oauthbearer.c201
-rw-r--r--src/lib-sasl/mech-plain.c70
-rw-r--r--src/lib-sasl/test-sasl-client.c419
-rw-r--r--src/lib-settings/Makefile.am40
-rw-r--r--src/lib-settings/Makefile.in870
-rw-r--r--src/lib-settings/settings-parser.c2226
-rw-r--r--src/lib-settings/settings-parser.h281
-rw-r--r--src/lib-settings/settings.c434
-rw-r--r--src/lib-settings/settings.h73
-rw-r--r--src/lib-settings/test-settings-parser.c340
-rw-r--r--src/lib-settings/test-settings.c165
-rw-r--r--src/lib-smtp/Makefile.am192
-rw-r--r--src/lib-smtp/Makefile.in1356
-rw-r--r--src/lib-smtp/fuzz-smtp-server.c102
-rw-r--r--src/lib-smtp/smtp-address.c958
-rw-r--r--src/lib-smtp/smtp-address.h214
-rw-r--r--src/lib-smtp/smtp-client-command.c1580
-rw-r--r--src/lib-smtp/smtp-client-command.h274
-rw-r--r--src/lib-smtp/smtp-client-connection.c2522
-rw-r--r--src/lib-smtp/smtp-client-connection.h93
-rw-r--r--src/lib-smtp/smtp-client-private.h340
-rw-r--r--src/lib-smtp/smtp-client-transaction.c1656
-rw-r--r--src/lib-smtp/smtp-client-transaction.h259
-rw-r--r--src/lib-smtp/smtp-client.c142
-rw-r--r--src/lib-smtp/smtp-client.h128
-rw-r--r--src/lib-smtp/smtp-command-parser.c613
-rw-r--r--src/lib-smtp/smtp-command-parser.h51
-rw-r--r--src/lib-smtp/smtp-command.h38
-rw-r--r--src/lib-smtp/smtp-common.c91
-rw-r--r--src/lib-smtp/smtp-common.h119
-rw-r--r--src/lib-smtp/smtp-params.c1375
-rw-r--r--src/lib-smtp/smtp-params.h233
-rw-r--r--src/lib-smtp/smtp-parser.c587
-rw-r--r--src/lib-smtp/smtp-parser.h89
-rw-r--r--src/lib-smtp/smtp-reply-parser.c657
-rw-r--r--src/lib-smtp/smtp-reply-parser.h25
-rw-r--r--src/lib-smtp/smtp-reply.c186
-rw-r--r--src/lib-smtp/smtp-reply.h82
-rw-r--r--src/lib-smtp/smtp-server-cmd-auth.c242
-rw-r--r--src/lib-smtp/smtp-server-cmd-data.c679
-rw-r--r--src/lib-smtp/smtp-server-cmd-helo.c196
-rw-r--r--src/lib-smtp/smtp-server-cmd-mail.c216
-rw-r--r--src/lib-smtp/smtp-server-cmd-noop.c52
-rw-r--r--src/lib-smtp/smtp-server-cmd-quit.c42
-rw-r--r--src/lib-smtp/smtp-server-cmd-rcpt.c243
-rw-r--r--src/lib-smtp/smtp-server-cmd-rset.c70
-rw-r--r--src/lib-smtp/smtp-server-cmd-starttls.c179
-rw-r--r--src/lib-smtp/smtp-server-cmd-vrfy.c73
-rw-r--r--src/lib-smtp/smtp-server-cmd-xclient.c234
-rw-r--r--src/lib-smtp/smtp-server-command.c901
-rw-r--r--src/lib-smtp/smtp-server-connection.c1648
-rw-r--r--src/lib-smtp/smtp-server-private.h404
-rw-r--r--src/lib-smtp/smtp-server-recipient.c361
-rw-r--r--src/lib-smtp/smtp-server-reply.c876
-rw-r--r--src/lib-smtp/smtp-server-transaction.c361
-rw-r--r--src/lib-smtp/smtp-server.c153
-rw-r--r--src/lib-smtp/smtp-server.h802
-rw-r--r--src/lib-smtp/smtp-submit-settings.c69
-rw-r--r--src/lib-smtp/smtp-submit-settings.h17
-rw-r--r--src/lib-smtp/smtp-submit.c505
-rw-r--r--src/lib-smtp/smtp-submit.h73
-rw-r--r--src/lib-smtp/smtp-syntax.c327
-rw-r--r--src/lib-smtp/smtp-syntax.h49
-rwxr-xr-xsrc/lib-smtp/test-bin/sendmail-exit-1.sh4
-rwxr-xr-xsrc/lib-smtp/test-bin/sendmail-success.sh4
-rw-r--r--src/lib-smtp/test-smtp-address.c1382
-rw-r--r--src/lib-smtp/test-smtp-client-errors.c4374
-rw-r--r--src/lib-smtp/test-smtp-command-parser.c643
-rw-r--r--src/lib-smtp/test-smtp-params.c894
-rw-r--r--src/lib-smtp/test-smtp-payload.c1148
-rw-r--r--src/lib-smtp/test-smtp-reply.c279
-rw-r--r--src/lib-smtp/test-smtp-server-errors.c3883
-rw-r--r--src/lib-smtp/test-smtp-submit.c2199
-rw-r--r--src/lib-smtp/test-smtp-syntax.c150
-rw-r--r--src/lib-sql/Makefile.am144
-rw-r--r--src/lib-sql/Makefile.in1159
-rw-r--r--src/lib-sql/driver-cassandra.c2579
-rw-r--r--src/lib-sql/driver-mysql.c835
-rw-r--r--src/lib-sql/driver-pgsql.c1344
-rw-r--r--src/lib-sql/driver-sqlite.c555
-rw-r--r--src/lib-sql/driver-sqlpool.c934
-rw-r--r--src/lib-sql/driver-test.c514
-rw-r--r--src/lib-sql/driver-test.h28
-rw-r--r--src/lib-sql/sql-api-private.h251
-rw-r--r--src/lib-sql/sql-api.c833
-rw-r--r--src/lib-sql/sql-api.h249
-rw-r--r--src/lib-sql/sql-db-cache.c156
-rw-r--r--src/lib-sql/sql-db-cache.h13
-rw-r--r--src/lib-ssl-iostream/Makefile.am61
-rw-r--r--src/lib-ssl-iostream/Makefile.in970
-rw-r--r--src/lib-ssl-iostream/dovecot-openssl-common.c132
-rw-r--r--src/lib-ssl-iostream/dovecot-openssl-common.h16
-rw-r--r--src/lib-ssl-iostream/iostream-openssl-common.c343
-rw-r--r--src/lib-ssl-iostream/iostream-openssl-context.c755
-rw-r--r--src/lib-ssl-iostream/iostream-openssl.c946
-rw-r--r--src/lib-ssl-iostream/iostream-openssl.h129
-rw-r--r--src/lib-ssl-iostream/iostream-ssl-context-cache.c129
-rw-r--r--src/lib-ssl-iostream/iostream-ssl-private.h64
-rw-r--r--src/lib-ssl-iostream/iostream-ssl-test.c158
-rw-r--r--src/lib-ssl-iostream/iostream-ssl-test.h9
-rw-r--r--src/lib-ssl-iostream/iostream-ssl.c351
-rw-r--r--src/lib-ssl-iostream/iostream-ssl.h175
-rw-r--r--src/lib-ssl-iostream/istream-openssl.c130
-rw-r--r--src/lib-ssl-iostream/ostream-openssl.c339
-rw-r--r--src/lib-ssl-iostream/test-iostream-ssl.c559
-rw-r--r--src/lib-storage/Makefile.am210
-rw-r--r--src/lib-storage/Makefile.in1509
-rw-r--r--src/lib-storage/fail-mail-storage.c61
-rw-r--r--src/lib-storage/fail-mail-storage.h19
-rw-r--r--src/lib-storage/fail-mail.c271
-rw-r--r--src/lib-storage/fail-mailbox.c344
-rw-r--r--src/lib-storage/index/Makefile.am58
-rw-r--r--src/lib-storage/index/Makefile.in1061
-rw-r--r--src/lib-storage/index/dbox-common/Makefile.am29
-rw-r--r--src/lib-storage/index/dbox-common/Makefile.in843
-rw-r--r--src/lib-storage/index/dbox-common/dbox-attachment.c77
-rw-r--r--src/lib-storage/index/dbox-common/dbox-attachment.h16
-rw-r--r--src/lib-storage/index/dbox-common/dbox-file-fix.c519
-rw-r--r--src/lib-storage/index/dbox-common/dbox-file.c796
-rw-r--r--src/lib-storage/index/dbox-common/dbox-file.h218
-rw-r--r--src/lib-storage/index/dbox-common/dbox-mail.c318
-rw-r--r--src/lib-storage/index/dbox-common/dbox-mail.h34
-rw-r--r--src/lib-storage/index/dbox-common/dbox-save.c226
-rw-r--r--src/lib-storage/index/dbox-common/dbox-save.h41
-rw-r--r--src/lib-storage/index/dbox-common/dbox-storage.c416
-rw-r--r--src/lib-storage/index/dbox-common/dbox-storage.h85
-rw-r--r--src/lib-storage/index/dbox-multi/Makefile.am36
-rw-r--r--src/lib-storage/index/dbox-multi/Makefile.in866
-rw-r--r--src/lib-storage/index/dbox-multi/mdbox-deleted-storage.c319
-rw-r--r--src/lib-storage/index/dbox-multi/mdbox-file.c349
-rw-r--r--src/lib-storage/index/dbox-multi/mdbox-file.h29
-rw-r--r--src/lib-storage/index/dbox-multi/mdbox-mail.c265
-rw-r--r--src/lib-storage/index/dbox-multi/mdbox-map-private.h64
-rw-r--r--src/lib-storage/index/dbox-multi/mdbox-map.c1502
-rw-r--r--src/lib-storage/index/dbox-multi/mdbox-map.h144
-rw-r--r--src/lib-storage/index/dbox-multi/mdbox-purge.c690
-rw-r--r--src/lib-storage/index/dbox-multi/mdbox-save.c491
-rw-r--r--src/lib-storage/index/dbox-multi/mdbox-settings.c43
-rw-r--r--src/lib-storage/index/dbox-multi/mdbox-settings.h12
-rw-r--r--src/lib-storage/index/dbox-multi/mdbox-storage-rebuild.c1005
-rw-r--r--src/lib-storage/index/dbox-multi/mdbox-storage-rebuild.h10
-rw-r--r--src/lib-storage/index/dbox-multi/mdbox-storage.c530
-rw-r--r--src/lib-storage/index/dbox-multi/mdbox-storage.h118
-rw-r--r--src/lib-storage/index/dbox-multi/mdbox-sync.c377
-rw-r--r--src/lib-storage/index/dbox-multi/mdbox-sync.h37
-rw-r--r--src/lib-storage/index/dbox-single/Makefile.am30
-rw-r--r--src/lib-storage/index/dbox-single/Makefile.in848
-rw-r--r--src/lib-storage/index/dbox-single/sdbox-copy.c185
-rw-r--r--src/lib-storage/index/dbox-single/sdbox-file.c447
-rw-r--r--src/lib-storage/index/dbox-single/sdbox-file.h43
-rw-r--r--src/lib-storage/index/dbox-single/sdbox-mail.c182
-rw-r--r--src/lib-storage/index/dbox-single/sdbox-save.c359
-rw-r--r--src/lib-storage/index/dbox-single/sdbox-storage.c534
-rw-r--r--src/lib-storage/index/dbox-single/sdbox-storage.h69
-rw-r--r--src/lib-storage/index/dbox-single/sdbox-sync-rebuild.c218
-rw-r--r--src/lib-storage/index/dbox-single/sdbox-sync.c326
-rw-r--r--src/lib-storage/index/dbox-single/sdbox-sync.h38
-rw-r--r--src/lib-storage/index/imapc/Makefile.am36
-rw-r--r--src/lib-storage/index/imapc/Makefile.in861
-rw-r--r--src/lib-storage/index/imapc/imapc-list.c1011
-rw-r--r--src/lib-storage/index/imapc/imapc-list.h41
-rw-r--r--src/lib-storage/index/imapc/imapc-mail-fetch.c911
-rw-r--r--src/lib-storage/index/imapc/imapc-mail.c675
-rw-r--r--src/lib-storage/index/imapc/imapc-mail.h51
-rw-r--r--src/lib-storage/index/imapc/imapc-mailbox.c994
-rw-r--r--src/lib-storage/index/imapc/imapc-save.c829
-rw-r--r--src/lib-storage/index/imapc/imapc-search.c330
-rw-r--r--src/lib-storage/index/imapc/imapc-search.h18
-rw-r--r--src/lib-storage/index/imapc/imapc-settings.c173
-rw-r--r--src/lib-storage/index/imapc/imapc-settings.h63
-rw-r--r--src/lib-storage/index/imapc/imapc-storage.c1344
-rw-r--r--src/lib-storage/index/imapc/imapc-storage.h268
-rw-r--r--src/lib-storage/index/imapc/imapc-sync.c702
-rw-r--r--src/lib-storage/index/imapc/imapc-sync.h39
-rw-r--r--src/lib-storage/index/index-attachment.c446
-rw-r--r--src/lib-storage/index/index-attachment.h52
-rw-r--r--src/lib-storage/index/index-attribute.c333
-rw-r--r--src/lib-storage/index/index-mail-binary.c598
-rw-r--r--src/lib-storage/index/index-mail-headers.c990
-rw-r--r--src/lib-storage/index/index-mail.c2619
-rw-r--r--src/lib-storage/index/index-mail.h292
-rw-r--r--src/lib-storage/index/index-mailbox-size.c502
-rw-r--r--src/lib-storage/index/index-mailbox-size.h20
-rw-r--r--src/lib-storage/index/index-pop3-uidl.c104
-rw-r--r--src/lib-storage/index/index-pop3-uidl.h16
-rw-r--r--src/lib-storage/index/index-rebuild.c257
-rw-r--r--src/lib-storage/index/index-rebuild.h32
-rw-r--r--src/lib-storage/index/index-search-mime.c624
-rw-r--r--src/lib-storage/index/index-search-private.h45
-rw-r--r--src/lib-storage/index/index-search-result.c194
-rw-r--r--src/lib-storage/index/index-search-result.h11
-rw-r--r--src/lib-storage/index/index-search.c1922
-rw-r--r--src/lib-storage/index/index-sort-private.h35
-rw-r--r--src/lib-storage/index/index-sort-string.c944
-rw-r--r--src/lib-storage/index/index-sort.c738
-rw-r--r--src/lib-storage/index/index-sort.h18
-rw-r--r--src/lib-storage/index/index-status.c344
-rw-r--r--src/lib-storage/index/index-storage.c1291
-rw-r--r--src/lib-storage/index/index-storage.h193
-rw-r--r--src/lib-storage/index/index-sync-changes.c200
-rw-r--r--src/lib-storage/index/index-sync-changes.h28
-rw-r--r--src/lib-storage/index/index-sync-private.h38
-rw-r--r--src/lib-storage/index/index-sync-pvt.c345
-rw-r--r--src/lib-storage/index/index-sync-search.c97
-rw-r--r--src/lib-storage/index/index-sync.c560
-rw-r--r--src/lib-storage/index/index-thread-finish.c682
-rw-r--r--src/lib-storage/index/index-thread-links.c242
-rw-r--r--src/lib-storage/index/index-thread-private.h82
-rw-r--r--src/lib-storage/index/index-thread.c670
-rw-r--r--src/lib-storage/index/index-transaction.c227
-rw-r--r--src/lib-storage/index/istream-mail.c173
-rw-r--r--src/lib-storage/index/istream-mail.h7
-rw-r--r--src/lib-storage/index/maildir/Makefile.am36
-rw-r--r--src/lib-storage/index/maildir/Makefile.in875
-rw-r--r--src/lib-storage/index/maildir/maildir-copy.c149
-rw-r--r--src/lib-storage/index/maildir/maildir-filename-flags.c185
-rw-r--r--src/lib-storage/index/maildir/maildir-filename-flags.h13
-rw-r--r--src/lib-storage/index/maildir/maildir-filename.c143
-rw-r--r--src/lib-storage/index/maildir/maildir-filename.h14
-rw-r--r--src/lib-storage/index/maildir/maildir-keywords.c499
-rw-r--r--src/lib-storage/index/maildir/maildir-keywords.h36
-rw-r--r--src/lib-storage/index/maildir/maildir-mail.c809
-rw-r--r--src/lib-storage/index/maildir/maildir-save.c1084
-rw-r--r--src/lib-storage/index/maildir/maildir-settings.c46
-rw-r--r--src/lib-storage/index/maildir/maildir-settings.h13
-rw-r--r--src/lib-storage/index/maildir/maildir-storage.c749
-rw-r--r--src/lib-storage/index/maildir/maildir-storage.h148
-rw-r--r--src/lib-storage/index/maildir/maildir-sync-index.c810
-rw-r--r--src/lib-storage/index/maildir/maildir-sync.c1132
-rw-r--r--src/lib-storage/index/maildir/maildir-sync.h59
-rw-r--r--src/lib-storage/index/maildir/maildir-uidlist.c2151
-rw-r--r--src/lib-storage/index/maildir/maildir-uidlist.h161
-rw-r--r--src/lib-storage/index/maildir/maildir-util.c323
-rw-r--r--src/lib-storage/index/mbox/Makefile.am39
-rw-r--r--src/lib-storage/index/mbox/Makefile.in884
-rw-r--r--src/lib-storage/index/mbox/istream-raw-mbox.c821
-rw-r--r--src/lib-storage/index/mbox/istream-raw-mbox.h56
-rw-r--r--src/lib-storage/index/mbox/mbox-file.c207
-rw-r--r--src/lib-storage/index/mbox/mbox-file.h16
-rw-r--r--src/lib-storage/index/mbox/mbox-lock.c900
-rw-r--r--src/lib-storage/index/mbox/mbox-lock.h15
-rw-r--r--src/lib-storage/index/mbox/mbox-mail.c439
-rw-r--r--src/lib-storage/index/mbox/mbox-md5-all.c39
-rw-r--r--src/lib-storage/index/mbox/mbox-md5-apop3d.c119
-rw-r--r--src/lib-storage/index/mbox/mbox-md5.h17
-rw-r--r--src/lib-storage/index/mbox/mbox-save.c833
-rw-r--r--src/lib-storage/index/mbox/mbox-settings.c55
-rw-r--r--src/lib-storage/index/mbox/mbox-settings.h18
-rw-r--r--src/lib-storage/index/mbox/mbox-storage.c911
-rw-r--r--src/lib-storage/index/mbox/mbox-storage.h115
-rw-r--r--src/lib-storage/index/mbox/mbox-sync-list-index.c109
-rw-r--r--src/lib-storage/index/mbox/mbox-sync-parse.c616
-rw-r--r--src/lib-storage/index/mbox/mbox-sync-private.h192
-rw-r--r--src/lib-storage/index/mbox/mbox-sync-rewrite.c615
-rw-r--r--src/lib-storage/index/mbox/mbox-sync-update.c466
-rw-r--r--src/lib-storage/index/mbox/mbox-sync.c2066
-rw-r--r--src/lib-storage/index/pop3c/Makefile.am28
-rw-r--r--src/lib-storage/index/pop3c/Makefile.in837
-rw-r--r--src/lib-storage/index/pop3c/pop3c-client.c902
-rw-r--r--src/lib-storage/index/pop3c/pop3c-client.h86
-rw-r--r--src/lib-storage/index/pop3c/pop3c-mail.c304
-rw-r--r--src/lib-storage/index/pop3c/pop3c-settings.c116
-rw-r--r--src/lib-storage/index/pop3c/pop3c-settings.h33
-rw-r--r--src/lib-storage/index/pop3c/pop3c-storage.c368
-rw-r--r--src/lib-storage/index/pop3c/pop3c-storage.h51
-rw-r--r--src/lib-storage/index/pop3c/pop3c-sync.c361
-rw-r--r--src/lib-storage/index/pop3c/pop3c-sync.h14
-rw-r--r--src/lib-storage/index/raw/Makefile.am21
-rw-r--r--src/lib-storage/index/raw/Makefile.in822
-rw-r--r--src/lib-storage/index/raw/raw-mail.c152
-rw-r--r--src/lib-storage/index/raw/raw-storage.c269
-rw-r--r--src/lib-storage/index/raw/raw-storage.h41
-rw-r--r--src/lib-storage/index/raw/raw-sync.c67
-rw-r--r--src/lib-storage/index/raw/raw-sync.h9
-rw-r--r--src/lib-storage/index/shared/Makefile.am19
-rw-r--r--src/lib-storage/index/shared/Makefile.in817
-rw-r--r--src/lib-storage/index/shared/shared-list.c310
-rw-r--r--src/lib-storage/index/shared/shared-storage.c379
-rw-r--r--src/lib-storage/index/shared/shared-storage.h22
-rw-r--r--src/lib-storage/list/Makefile.am45
-rw-r--r--src/lib-storage/list/Makefile.in916
-rw-r--r--src/lib-storage/list/mail-storage-list-index-rebuild.c510
-rw-r--r--src/lib-storage/list/mailbox-list-delete.c489
-rw-r--r--src/lib-storage/list/mailbox-list-delete.h86
-rw-r--r--src/lib-storage/list/mailbox-list-fs-flags.c243
-rw-r--r--src/lib-storage/list/mailbox-list-fs-iter.c833
-rw-r--r--src/lib-storage/list/mailbox-list-fs.c558
-rw-r--r--src/lib-storage/list/mailbox-list-fs.h28
-rw-r--r--src/lib-storage/list/mailbox-list-index-backend.c979
-rw-r--r--src/lib-storage/list/mailbox-list-index-iter.c266
-rw-r--r--src/lib-storage/list/mailbox-list-index-notify.c967
-rw-r--r--src/lib-storage/list/mailbox-list-index-status.c862
-rw-r--r--src/lib-storage/list/mailbox-list-index-storage.h21
-rw-r--r--src/lib-storage/list/mailbox-list-index-sync.c521
-rw-r--r--src/lib-storage/list/mailbox-list-index-sync.h35
-rw-r--r--src/lib-storage/list/mailbox-list-index.c1168
-rw-r--r--src/lib-storage/list/mailbox-list-index.h240
-rw-r--r--src/lib-storage/list/mailbox-list-iter-private.h42
-rw-r--r--src/lib-storage/list/mailbox-list-iter.c1168
-rw-r--r--src/lib-storage/list/mailbox-list-maildir-iter.c524
-rw-r--r--src/lib-storage/list/mailbox-list-maildir.c546
-rw-r--r--src/lib-storage/list/mailbox-list-maildir.h29
-rw-r--r--src/lib-storage/list/mailbox-list-none.c178
-rw-r--r--src/lib-storage/list/mailbox-list-notify-tree.c131
-rw-r--r--src/lib-storage/list/mailbox-list-notify-tree.h27
-rw-r--r--src/lib-storage/list/mailbox-list-subscriptions.c318
-rw-r--r--src/lib-storage/list/mailbox-list-subscriptions.h33
-rw-r--r--src/lib-storage/list/subscription-file.c380
-rw-r--r--src/lib-storage/list/subscription-file.h25
-rw-r--r--src/lib-storage/mail-autoexpunge.c267
-rw-r--r--src/lib-storage/mail-autoexpunge.h8
-rw-r--r--src/lib-storage/mail-copy.c126
-rw-r--r--src/lib-storage/mail-copy.h20
-rw-r--r--src/lib-storage/mail-duplicate.c767
-rw-r--r--src/lib-storage/mail-duplicate.h51
-rw-r--r--src/lib-storage/mail-error.c34
-rw-r--r--src/lib-storage/mail-error.h70
-rw-r--r--src/lib-storage/mail-lua.c143
-rw-r--r--src/lib-storage/mail-namespace.c866
-rw-r--r--src/lib-storage/mail-namespace.h221
-rw-r--r--src/lib-storage/mail-search-args-cmdline.c106
-rw-r--r--src/lib-storage/mail-search-args-imap.c326
-rw-r--r--src/lib-storage/mail-search-args-simplify.c798
-rw-r--r--src/lib-storage/mail-search-build.c250
-rw-r--r--src/lib-storage/mail-search-build.h59
-rw-r--r--src/lib-storage/mail-search-mime-build.c173
-rw-r--r--src/lib-storage/mail-search-mime-build.h44
-rw-r--r--src/lib-storage/mail-search-mime-register.c547
-rw-r--r--src/lib-storage/mail-search-mime-register.h30
-rw-r--r--src/lib-storage/mail-search-mime.c609
-rw-r--r--src/lib-storage/mail-search-mime.h146
-rw-r--r--src/lib-storage/mail-search-parser-cmdline.c98
-rw-r--r--src/lib-storage/mail-search-parser-imap.c122
-rw-r--r--src/lib-storage/mail-search-parser-private.h22
-rw-r--r--src/lib-storage/mail-search-parser.c49
-rw-r--r--src/lib-storage/mail-search-parser.h34
-rw-r--r--src/lib-storage/mail-search-register-human.c232
-rw-r--r--src/lib-storage/mail-search-register-imap.c638
-rw-r--r--src/lib-storage/mail-search-register.c89
-rw-r--r--src/lib-storage/mail-search-register.h46
-rw-r--r--src/lib-storage/mail-search.c806
-rw-r--r--src/lib-storage/mail-search.h272
-rw-r--r--src/lib-storage/mail-storage-hooks.c291
-rw-r--r--src/lib-storage/mail-storage-hooks.h53
-rw-r--r--src/lib-storage/mail-storage-lua-private.h31
-rw-r--r--src/lib-storage/mail-storage-lua.c91
-rw-r--r--src/lib-storage/mail-storage-lua.h17
-rw-r--r--src/lib-storage/mail-storage-private.h913
-rw-r--r--src/lib-storage/mail-storage-register.c31
-rw-r--r--src/lib-storage/mail-storage-service.c1807
-rw-r--r--src/lib-storage/mail-storage-service.h187
-rw-r--r--src/lib-storage/mail-storage-settings.c809
-rw-r--r--src/lib-storage/mail-storage-settings.h175
-rw-r--r--src/lib-storage/mail-storage.c3288
-rw-r--r--src/lib-storage/mail-storage.h1027
-rw-r--r--src/lib-storage/mail-thread.c37
-rw-r--r--src/lib-storage/mail-thread.h54
-rw-r--r--src/lib-storage/mail-user-lua.c408
-rw-r--r--src/lib-storage/mail-user.c848
-rw-r--r--src/lib-storage/mail-user.h259
-rw-r--r--src/lib-storage/mail.c694
-rw-r--r--src/lib-storage/mailbox-attribute-internal.c149
-rw-r--r--src/lib-storage/mailbox-attribute-internal.h14
-rw-r--r--src/lib-storage/mailbox-attribute-lua.c163
-rw-r--r--src/lib-storage/mailbox-attribute-private.h15
-rw-r--r--src/lib-storage/mailbox-attribute.c582
-rw-r--r--src/lib-storage/mailbox-attribute.h316
-rw-r--r--src/lib-storage/mailbox-get.c239
-rw-r--r--src/lib-storage/mailbox-guid-cache.c120
-rw-r--r--src/lib-storage/mailbox-guid-cache.h8
-rw-r--r--src/lib-storage/mailbox-header.c113
-rw-r--r--src/lib-storage/mailbox-keywords.c148
-rw-r--r--src/lib-storage/mailbox-list-iter.h89
-rw-r--r--src/lib-storage/mailbox-list-notify.c42
-rw-r--r--src/lib-storage/mailbox-list-notify.h67
-rw-r--r--src/lib-storage/mailbox-list-private.h249
-rw-r--r--src/lib-storage/mailbox-list-register.c26
-rw-r--r--src/lib-storage/mailbox-list.c2164
-rw-r--r--src/lib-storage/mailbox-list.h339
-rw-r--r--src/lib-storage/mailbox-lua.c366
-rw-r--r--src/lib-storage/mailbox-match-plugin.c83
-rw-r--r--src/lib-storage/mailbox-match-plugin.h16
-rw-r--r--src/lib-storage/mailbox-recent-flags.c106
-rw-r--r--src/lib-storage/mailbox-recent-flags.h19
-rw-r--r--src/lib-storage/mailbox-search-result-private.h41
-rw-r--r--src/lib-storage/mailbox-search-result.c200
-rw-r--r--src/lib-storage/mailbox-tree.c345
-rw-r--r--src/lib-storage/mailbox-tree.h45
-rw-r--r--src/lib-storage/mailbox-uidvalidity.c249
-rw-r--r--src/lib-storage/mailbox-uidvalidity.h8
-rw-r--r--src/lib-storage/mailbox-watch.c152
-rw-r--r--src/lib-storage/mailbox-watch.h11
-rw-r--r--src/lib-storage/test-mail-search-args-imap.c179
-rw-r--r--src/lib-storage/test-mail-search-args-simplify.c324
-rw-r--r--src/lib-storage/test-mail-storage-common.c118
-rw-r--r--src/lib-storage/test-mail-storage-common.h30
-rw-r--r--src/lib-storage/test-mail-storage.c652
-rw-r--r--src/lib-storage/test-mail.c506
-rw-r--r--src/lib-storage/test-mailbox-get.c162
-rw-r--r--src/lib-storage/test-mailbox-list.c512
-rw-r--r--src/lib-test/Makefile.am20
-rw-r--r--src/lib-test/Makefile.in829
-rw-r--r--src/lib-test/fuzzer.c87
-rw-r--r--src/lib-test/fuzzer.h37
-rw-r--r--src/lib-test/test-common.c465
-rw-r--r--src/lib-test/test-common.h165
-rw-r--r--src/lib-test/test-istream.c166
-rw-r--r--src/lib-test/test-ostream.c194
-rw-r--r--src/lib-test/test-subprocess.c392
-rw-r--r--src/lib-test/test-subprocess.h41
-rw-r--r--src/lib/Makefile.am481
-rw-r--r--src/lib/Makefile.in3709
-rw-r--r--src/lib/UnicodeData.txt30592
-rw-r--r--src/lib/aqueue.c124
-rw-r--r--src/lib/aqueue.h41
-rw-r--r--src/lib/array-decl.h26
-rw-r--r--src/lib/array.c166
-rw-r--r--src/lib/array.h420
-rw-r--r--src/lib/askpass.c72
-rw-r--r--src/lib/askpass.h7
-rw-r--r--src/lib/backtrace-string.c156
-rw-r--r--src/lib/backtrace-string.h8
-rw-r--r--src/lib/base32.c324
-rw-r--r--src/lib/base32.h47
-rw-r--r--src/lib/base64.c968
-rw-r--r--src/lib/base64.h403
-rw-r--r--src/lib/bits.c36
-rw-r--r--src/lib/bits.h184
-rw-r--r--src/lib/bsearch-insert-pos.c48
-rw-r--r--src/lib/bsearch-insert-pos.h52
-rw-r--r--src/lib/buffer-istream.c49
-rw-r--r--src/lib/buffer.c495
-rw-r--r--src/lib/buffer.h199
-rw-r--r--src/lib/byteorder.h268
-rw-r--r--src/lib/child-wait.c139
-rw-r--r--src/lib/child-wait.h34
-rw-r--r--src/lib/compat.c268
-rw-r--r--src/lib/compat.h323
-rw-r--r--src/lib/connection.c952
-rw-r--r--src/lib/connection.h259
-rw-r--r--src/lib/cpu-limit.c200
-rw-r--r--src/lib/cpu-limit.h67
-rw-r--r--src/lib/crc32.c91
-rw-r--r--src/lib/crc32.h10
-rw-r--r--src/lib/data-stack.c777
-rw-r--r--src/lib/data-stack.h165
-rw-r--r--src/lib/eacces-error.c310
-rw-r--r--src/lib/eacces-error.h14
-rw-r--r--src/lib/env-util.c140
-rw-r--r--src/lib/env-util.h30
-rw-r--r--src/lib/event-filter-lexer.c2305
-rw-r--r--src/lib/event-filter-lexer.l149
-rw-r--r--src/lib/event-filter-parser.c1730
-rw-r--r--src/lib/event-filter-parser.h91
-rw-r--r--src/lib/event-filter-parser.y194
-rw-r--r--src/lib/event-filter-private.h121
-rw-r--r--src/lib/event-filter.c855
-rw-r--r--src/lib/event-filter.h64
-rw-r--r--src/lib/event-log.c461
-rw-r--r--src/lib/event-log.h151
-rw-r--r--src/lib/execv-const.c33
-rw-r--r--src/lib/execv-const.h9
-rw-r--r--src/lib/failures-private.h26
-rw-r--r--src/lib/failures.c998
-rw-r--r--src/lib/failures.h157
-rw-r--r--src/lib/fd-util.c166
-rw-r--r--src/lib/fd-util.h26
-rw-r--r--src/lib/fdatasync-path.c31
-rw-r--r--src/lib/fdatasync-path.h7
-rw-r--r--src/lib/fdpass.c212
-rw-r--r--src/lib/fdpass.h15
-rw-r--r--src/lib/file-cache.c336
-rw-r--r--src/lib/file-cache.h37
-rw-r--r--src/lib/file-copy.c125
-rw-r--r--src/lib/file-copy.h12
-rw-r--r--src/lib/file-create-locked.c193
-rw-r--r--src/lib/file-create-locked.h47
-rw-r--r--src/lib/file-dotlock.c925
-rw-r--r--src/lib/file-dotlock.h94
-rw-r--r--src/lib/file-lock.c530
-rw-r--r--src/lib/file-lock.h87
-rw-r--r--src/lib/file-set-size.c108
-rw-r--r--src/lib/file-set-size.h13
-rw-r--r--src/lib/fsync-mode.h14
-rw-r--r--src/lib/guid.c174
-rw-r--r--src/lib/guid.h52
-rw-r--r--src/lib/hash-decl.h20
-rw-r--r--src/lib/hash-format.c245
-rw-r--r--src/lib/hash-format.h23
-rw-r--r--src/lib/hash-method.c98
-rw-r--r--src/lib/hash-method.h55
-rw-r--r--src/lib/hash.c593
-rw-r--r--src/lib/hash.h175
-rw-r--r--src/lib/hash2.c242
-rw-r--r--src/lib/hash2.h56
-rw-r--r--src/lib/hex-binary.c85
-rw-r--r--src/lib/hex-binary.h15
-rw-r--r--src/lib/hex-dec.c38
-rw-r--r--src/lib/hex-dec.h12
-rw-r--r--src/lib/hmac-cram-md5.c65
-rw-r--r--src/lib/hmac-cram-md5.h14
-rw-r--r--src/lib/hmac.c152
-rw-r--r--src/lib/hmac.h65
-rw-r--r--src/lib/home-expand.c72
-rw-r--r--src/lib/home-expand.h12
-rw-r--r--src/lib/hook-build.c121
-rw-r--r--src/lib/hook-build.h19
-rw-r--r--src/lib/hostpid.c69
-rw-r--r--src/lib/hostpid.h21
-rw-r--r--src/lib/imem.c78
-rw-r--r--src/lib/imem.h45
-rw-r--r--src/lib/ioloop-epoll.c230
-rw-r--r--src/lib/ioloop-iolist.c56
-rw-r--r--src/lib/ioloop-iolist.h19
-rw-r--r--src/lib/ioloop-kqueue.c175
-rw-r--r--src/lib/ioloop-notify-fd.c55
-rw-r--r--src/lib/ioloop-notify-fd.h28
-rw-r--r--src/lib/ioloop-notify-inotify.c237
-rw-r--r--src/lib/ioloop-notify-kqueue.c223
-rw-r--r--src/lib/ioloop-notify-none.c33
-rw-r--r--src/lib/ioloop-poll.c221
-rw-r--r--src/lib/ioloop-private.h129
-rw-r--r--src/lib/ioloop-select.c148
-rw-r--r--src/lib/ioloop.c1389
-rw-r--r--src/lib/ioloop.h323
-rw-r--r--src/lib/iostream-private.h51
-rw-r--r--src/lib/iostream-proxy.c173
-rw-r--r--src/lib/iostream-proxy.h87
-rw-r--r--src/lib/iostream-pump.c251
-rw-r--r--src/lib/iostream-pump.h69
-rw-r--r--src/lib/iostream-rawlog-private.h25
-rw-r--r--src/lib/iostream-rawlog.c298
-rw-r--r--src/lib/iostream-rawlog.h28
-rw-r--r--src/lib/iostream-temp.c404
-rw-r--r--src/lib/iostream-temp.h31
-rw-r--r--src/lib/iostream.c154
-rw-r--r--src/lib/iostream.h10
-rw-r--r--src/lib/ipwd.c103
-rw-r--r--src/lib/ipwd.h23
-rw-r--r--src/lib/iso8601-date.c309
-rw-r--r--src/lib/iso8601-date.h21
-rw-r--r--src/lib/istream-base64-decoder.c171
-rw-r--r--src/lib/istream-base64-encoder.c226
-rw-r--r--src/lib/istream-base64.h16
-rw-r--r--src/lib/istream-callback.c118
-rw-r--r--src/lib/istream-callback.h39
-rw-r--r--src/lib/istream-chain.c349
-rw-r--r--src/lib/istream-chain.h21
-rw-r--r--src/lib/istream-concat.c391
-rw-r--r--src/lib/istream-concat.h7
-rw-r--r--src/lib/istream-crlf.c208
-rw-r--r--src/lib/istream-crlf.h9
-rw-r--r--src/lib/istream-data.c62
-rw-r--r--src/lib/istream-failure-at.c97
-rw-r--r--src/lib/istream-failure-at.h11
-rw-r--r--src/lib/istream-file-private.h23
-rw-r--r--src/lib/istream-file.c282
-rw-r--r--src/lib/istream-hash.c87
-rw-r--r--src/lib/istream-hash.h12
-rw-r--r--src/lib/istream-jsonstr.c217
-rw-r--r--src/lib/istream-jsonstr.h7
-rw-r--r--src/lib/istream-limit.c144
-rw-r--r--src/lib/istream-multiplex.c298
-rw-r--r--src/lib/istream-multiplex.h8
-rw-r--r--src/lib/istream-private.h140
-rw-r--r--src/lib/istream-rawlog.c119
-rw-r--r--src/lib/istream-rawlog.h14
-rw-r--r--src/lib/istream-seekable.c558
-rw-r--r--src/lib/istream-seekable.h28
-rw-r--r--src/lib/istream-sized.c232
-rw-r--r--src/lib/istream-sized.h41
-rw-r--r--src/lib/istream-tee.c258
-rw-r--r--src/lib/istream-tee.h17
-rw-r--r--src/lib/istream-timeout.c150
-rw-r--r--src/lib/istream-timeout.h9
-rw-r--r--src/lib/istream-try.c165
-rw-r--r--src/lib/istream-try.h25
-rw-r--r--src/lib/istream-unix.c113
-rw-r--r--src/lib/istream-unix.h15
-rw-r--r--src/lib/istream.c1316
-rw-r--r--src/lib/istream.h255
-rw-r--r--src/lib/json-parser.c850
-rw-r--r--src/lib/json-parser.h61
-rw-r--r--src/lib/json-tree.c173
-rw-r--r--src/lib/json-tree.h62
-rw-r--r--src/lib/lib-event-private.h114
-rw-r--r--src/lib/lib-event.c1764
-rw-r--r--src/lib/lib-event.h432
-rw-r--r--src/lib/lib-signals.c702
-rw-r--r--src/lib/lib-signals.h72
-rw-r--r--src/lib/lib.c206
-rw-r--r--src/lib/lib.h115
-rw-r--r--src/lib/llist.h81
-rw-r--r--src/lib/log-throttle.c78
-rw-r--r--src/lib/log-throttle.h32
-rw-r--r--src/lib/macros.h302
-rw-r--r--src/lib/malloc-overflow.h54
-rw-r--r--src/lib/md4.c300
-rw-r--r--src/lib/md4.h33
-rw-r--r--src/lib/md5.c314
-rw-r--r--src/lib/md5.h33
-rw-r--r--src/lib/memarea.c93
-rw-r--r--src/lib/memarea.h31
-rw-r--r--src/lib/mempool-allocfree.c330
-rw-r--r--src/lib/mempool-alloconly.c546
-rw-r--r--src/lib/mempool-datastack.c190
-rw-r--r--src/lib/mempool-system.c163
-rw-r--r--src/lib/mempool-unsafe-datastack.c135
-rw-r--r--src/lib/mempool.c25
-rw-r--r--src/lib/mempool.h179
-rw-r--r--src/lib/mkdir-parents.c177
-rw-r--r--src/lib/mkdir-parents.h33
-rw-r--r--src/lib/mmap-anon.c183
-rw-r--r--src/lib/mmap-util.c63
-rw-r--r--src/lib/mmap-util.h42
-rw-r--r--src/lib/module-context.h116
-rw-r--r--src/lib/module-dir.c697
-rw-r--r--src/lib/module-dir.h77
-rw-r--r--src/lib/mountpoint.c336
-rw-r--r--src/lib/mountpoint.h23
-rw-r--r--src/lib/net.c1237
-rw-r--r--src/lib/net.h199
-rw-r--r--src/lib/nfs-workarounds.c406
-rw-r--r--src/lib/nfs-workarounds.h40
-rw-r--r--src/lib/numpack.c57
-rw-r--r--src/lib/numpack.h11
-rw-r--r--src/lib/ostream-buffer.c88
-rw-r--r--src/lib/ostream-failure-at.c123
-rw-r--r--src/lib/ostream-failure-at.h10
-rw-r--r--src/lib/ostream-file-private.h45
-rw-r--r--src/lib/ostream-file.c1154
-rw-r--r--src/lib/ostream-hash.c56
-rw-r--r--src/lib/ostream-hash.h12
-rw-r--r--src/lib/ostream-multiplex.c367
-rw-r--r--src/lib/ostream-multiplex.h8
-rw-r--r--src/lib/ostream-null.c33
-rw-r--r--src/lib/ostream-null.h7
-rw-r--r--src/lib/ostream-private.h73
-rw-r--r--src/lib/ostream-rawlog.c88
-rw-r--r--src/lib/ostream-rawlog.h14
-rw-r--r--src/lib/ostream-unix.c95
-rw-r--r--src/lib/ostream-unix.h10
-rw-r--r--src/lib/ostream-wrapper.c1259
-rw-r--r--src/lib/ostream-wrapper.h165
-rw-r--r--src/lib/ostream.c804
-rw-r--r--src/lib/ostream.h260
-rw-r--r--src/lib/path-util.c398
-rw-r--r--src/lib/path-util.h69
-rw-r--r--src/lib/pkcs5.c97
-rw-r--r--src/lib/pkcs5.h35
-rw-r--r--src/lib/primes.c48
-rw-r--r--src/lib/primes.h8
-rw-r--r--src/lib/printf-format-fix.c192
-rw-r--r--src/lib/printf-format-fix.h15
-rw-r--r--src/lib/priorityq.c171
-rw-r--r--src/lib/priorityq.h39
-rw-r--r--src/lib/process-stat.c267
-rw-r--r--src/lib/process-stat.h26
-rw-r--r--src/lib/process-title.c187
-rw-r--r--src/lib/process-title.h17
-rw-r--r--src/lib/rand.c101
-rw-r--r--src/lib/randgen.c222
-rw-r--r--src/lib/randgen.h17
-rw-r--r--src/lib/read-full.c40
-rw-r--r--src/lib/read-full.h9
-rw-r--r--src/lib/restrict-access.c531
-rw-r--r--src/lib/restrict-access.h90
-rw-r--r--src/lib/restrict-process-size.c113
-rw-r--r--src/lib/restrict-process-size.h25
-rw-r--r--src/lib/safe-memset.c17
-rw-r--r--src/lib/safe-memset.h8
-rw-r--r--src/lib/safe-mkdir.c78
-rw-r--r--src/lib/safe-mkdir.h10
-rw-r--r--src/lib/safe-mkstemp.c106
-rw-r--r--src/lib/safe-mkstemp.h15
-rw-r--r--src/lib/sendfile-util.c137
-rw-r--r--src/lib/sendfile-util.h16
-rw-r--r--src/lib/seq-range-array.c569
-rw-r--r--src/lib/seq-range-array.h82
-rw-r--r--src/lib/seq-set-builder.c113
-rw-r--r--src/lib/seq-set-builder.h15
-rw-r--r--src/lib/sha-common.h12
-rw-r--r--src/lib/sha1.c288
-rw-r--r--src/lib/sha1.h84
-rw-r--r--src/lib/sha2.c647
-rw-r--r--src/lib/sha2.h89
-rw-r--r--src/lib/sha3.c335
-rw-r--r--src/lib/sha3.h75
-rw-r--r--src/lib/sleep.c74
-rw-r--r--src/lib/sleep.h30
-rw-r--r--src/lib/sort.c17
-rw-r--r--src/lib/sort.h33
-rw-r--r--src/lib/stats-dist.c183
-rw-r--r--src/lib/stats-dist.h40
-rw-r--r--src/lib/str-find.c183
-rw-r--r--src/lib/str-find.h20
-rw-r--r--src/lib/str-sanitize.c165
-rw-r--r--src/lib/str-sanitize.h22
-rw-r--r--src/lib/str-table.c78
-rw-r--r--src/lib/str-table.h19
-rw-r--r--src/lib/str.c158
-rw-r--r--src/lib/str.h100
-rw-r--r--src/lib/strescape.c358
-rw-r--r--src/lib/strescape.h43
-rw-r--r--src/lib/strfuncs.c945
-rw-r--r--src/lib/strfuncs.h170
-rw-r--r--src/lib/strnum.c464
-rw-r--r--src/lib/strnum.h192
-rw-r--r--src/lib/test-aqueue.c73
-rw-r--r--src/lib/test-array.c441
-rw-r--r--src/lib/test-backtrace.c57
-rw-r--r--src/lib/test-base32.c189
-rw-r--r--src/lib/test-base64.c1317
-rw-r--r--src/lib/test-bits.c281
-rw-r--r--src/lib/test-bsearch-insert-pos.c46
-rw-r--r--src/lib/test-buffer-istream.c101
-rw-r--r--src/lib/test-buffer.c367
-rw-r--r--src/lib/test-byteorder.c251
-rw-r--r--src/lib/test-connection.c744
-rw-r--r--src/lib/test-cpu-limit.c145
-rw-r--r--src/lib/test-crc32.c14
-rw-r--r--src/lib/test-data-stack.c455
-rw-r--r--src/lib/test-env-util.c92
-rw-r--r--src/lib/test-event-category-register.c320
-rw-r--r--src/lib/test-event-filter-expr.c250
-rw-r--r--src/lib/test-event-filter-merge.c67
-rw-r--r--src/lib/test-event-filter-parser.c543
-rw-r--r--src/lib/test-event-filter.c598
-rw-r--r--src/lib/test-event-flatten.c391
-rw-r--r--src/lib/test-event-log.c2528
-rw-r--r--src/lib/test-failures.c176
-rw-r--r--src/lib/test-fd-util.c24
-rw-r--r--src/lib/test-file-cache.c293
-rw-r--r--src/lib/test-file-create-locked.c142
-rw-r--r--src/lib/test-guid.c259
-rw-r--r--src/lib/test-hash-format.c54
-rw-r--r--src/lib/test-hash-method.c460
-rw-r--r--src/lib/test-hash.c47
-rw-r--r--src/lib/test-hex-binary.c61
-rw-r--r--src/lib/test-hmac.c302
-rw-r--r--src/lib/test-imem.c61
-rw-r--r--src/lib/test-ioloop.c404
-rw-r--r--src/lib/test-iostream-proxy.c113
-rw-r--r--src/lib/test-iostream-pump.c325
-rw-r--r--src/lib/test-iostream-temp.c150
-rw-r--r--src/lib/test-iso8601-date.c147
-rw-r--r--src/lib/test-istream-base64-decoder.c337
-rw-r--r--src/lib/test-istream-base64-encoder.c222
-rw-r--r--src/lib/test-istream-chain.c199
-rw-r--r--src/lib/test-istream-concat.c254
-rw-r--r--src/lib/test-istream-crlf.c115
-rw-r--r--src/lib/test-istream-failure-at.c49
-rw-r--r--src/lib/test-istream-jsonstr.c139
-rw-r--r--src/lib/test-istream-multiplex.c372
-rw-r--r--src/lib/test-istream-seekable.c290
-rw-r--r--src/lib/test-istream-sized.c111
-rw-r--r--src/lib/test-istream-tee.c139
-rw-r--r--src/lib/test-istream-try.c195
-rw-r--r--src/lib/test-istream-unix.c187
-rw-r--r--src/lib/test-istream.c381
-rw-r--r--src/lib/test-json-parser.c436
-rw-r--r--src/lib/test-json-tree.c113
-rw-r--r--src/lib/test-lib-event.c96
-rw-r--r--src/lib/test-lib-signals.c245
-rw-r--r--src/lib/test-lib.c28
-rw-r--r--src/lib/test-lib.h13
-rw-r--r--src/lib/test-lib.inc112
-rw-r--r--src/lib/test-llist.c138
-rw-r--r--src/lib/test-log-throttle.c57
-rw-r--r--src/lib/test-macros.c63
-rw-r--r--src/lib/test-malloc-overflow.c132
-rw-r--r--src/lib/test-memarea.c42
-rw-r--r--src/lib/test-mempool-allocfree.c135
-rw-r--r--src/lib/test-mempool-alloconly.c94
-rw-r--r--src/lib/test-mempool.c117
-rw-r--r--src/lib/test-multiplex.c167
-rw-r--r--src/lib/test-net.c167
-rw-r--r--src/lib/test-numpack.c64
-rw-r--r--src/lib/test-ostream-buffer.c108
-rw-r--r--src/lib/test-ostream-failure-at.c52
-rw-r--r--src/lib/test-ostream-file.c189
-rw-r--r--src/lib/test-ostream-multiplex.c405
-rw-r--r--src/lib/test-path-util.c278
-rw-r--r--src/lib/test-pkcs5.c49
-rw-r--r--src/lib/test-primes.c24
-rw-r--r--src/lib/test-printf-format-fix.c142
-rw-r--r--src/lib/test-priorityq.c106
-rw-r--r--src/lib/test-random.c67
-rw-r--r--src/lib/test-seq-range-array.c419
-rw-r--r--src/lib/test-seq-set-builder.c132
-rw-r--r--src/lib/test-stats-dist.c132
-rw-r--r--src/lib/test-str-find.c86
-rw-r--r--src/lib/test-str-sanitize.c127
-rw-r--r--src/lib/test-str-table.c33
-rw-r--r--src/lib/test-str.c182
-rw-r--r--src/lib/test-strescape.c197
-rw-r--r--src/lib/test-strfuncs.c640
-rw-r--r--src/lib/test-strnum.c398
-rw-r--r--src/lib/test-time-util.c406
-rw-r--r--src/lib/test-unichar.c185
-rw-r--r--src/lib/test-uri.c807
-rw-r--r--src/lib/test-utc-mktime.c61
-rw-r--r--src/lib/test-var-expand.c474
-rw-r--r--src/lib/test-wildcard-match.c48
-rw-r--r--src/lib/time-util.c169
-rw-r--r--src/lib/time-util.h108
-rw-r--r--src/lib/unichar.c447
-rw-r--r--src/lib/unichar.h145
-rw-r--r--src/lib/unicodemap.c2474
-rwxr-xr-xsrc/lib/unicodemap.pl162
-rw-r--r--src/lib/unix-socket-create.c36
-rw-r--r--src/lib/unix-socket-create.h7
-rw-r--r--src/lib/unlink-directory.c283
-rw-r--r--src/lib/unlink-directory.h21
-rw-r--r--src/lib/unlink-old-files.c73
-rw-r--r--src/lib/unlink-old-files.h9
-rw-r--r--src/lib/uri-util.c1332
-rw-r--r--src/lib/uri-util.h298
-rw-r--r--src/lib/utc-mktime.c79
-rw-r--r--src/lib/utc-mktime.h11
-rw-r--r--src/lib/utc-offset.c38
-rw-r--r--src/lib/utc-offset.h9
-rw-r--r--src/lib/var-expand-if.c269
-rw-r--r--src/lib/var-expand-private.h56
-rw-r--r--src/lib/var-expand.c833
-rw-r--r--src/lib/var-expand.h60
-rw-r--r--src/lib/wildcard-match.c105
-rw-r--r--src/lib/wildcard-match.h15
-rw-r--r--src/lib/write-full.c54
-rw-r--r--src/lib/write-full.h10
1432 files changed, 492273 insertions, 0 deletions
diff --git a/src/lib-auth/Makefile.am b/src/lib-auth/Makefile.am
new file mode 100644
index 0000000..4aff5f8
--- /dev/null
+++ b/src/lib-auth/Makefile.am
@@ -0,0 +1,46 @@
+noinst_LTLIBRARIES = libauth.la
+
+AM_CPPFLAGS = \
+ -I$(top_srcdir)/src/lib \
+ -I$(top_srcdir)/src/lib-master \
+ -I$(top_srcdir)/src/lib-test
+
+libauth_la_SOURCES = \
+ auth-client.c \
+ auth-client-request.c \
+ auth-client-connection.c \
+ auth-master.c
+
+headers = \
+ auth-client.h \
+ auth-client-interface.h \
+ auth-client-private.h \
+ auth-master.h
+
+pkginc_libdir=$(pkgincludedir)
+pkginc_lib_HEADERS = $(headers)
+
+test_programs = \
+ test-auth-master
+
+noinst_PROGRAMS = $(test_programs)
+
+test_libs = \
+ $(noinst_LTLIBRARIES) \
+ ../lib-test/libtest.la \
+ ../lib/liblib.la \
+ $(MODULE_LIBS)
+
+test_deps = \
+ $(noinst_LTLIBRARIES) \
+ ../lib-test/libtest.la \
+ ../lib/liblib.la
+
+test_auth_master_SOURCES = test-auth-master.c
+test_auth_master_LDADD = $(test_libs)
+test_auth_master_DEPENDENCIES = $(test_deps)
+
+check-local:
+ for bin in $(test_programs); do \
+ if ! $(RUN_TEST) ./$$bin; then exit 1; fi; \
+ done
diff --git a/src/lib-auth/Makefile.in b/src/lib-auth/Makefile.in
new file mode 100644
index 0000000..d3653d8
--- /dev/null
+++ b/src/lib-auth/Makefile.in
@@ -0,0 +1,877 @@
+# Makefile.in generated by automake 1.16.1 from Makefile.am.
+# @configure_input@
+
+# Copyright (C) 1994-2018 Free Software Foundation, Inc.
+
+# This Makefile.in is free software; the Free Software Foundation
+# gives unlimited permission to copy and/or distribute it,
+# with or without modifications, as long as this notice is preserved.
+
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY, to the extent permitted by law; without
+# even the implied warranty of MERCHANTABILITY or FITNESS FOR A
+# PARTICULAR PURPOSE.
+
+@SET_MAKE@
+
+
+
+VPATH = @srcdir@
+am__is_gnu_make = { \
+ if test -z '$(MAKELEVEL)'; then \
+ false; \
+ elif test -n '$(MAKE_HOST)'; then \
+ true; \
+ elif test -n '$(MAKE_VERSION)' && test -n '$(CURDIR)'; then \
+ true; \
+ else \
+ false; \
+ fi; \
+}
+am__make_running_with_option = \
+ case $${target_option-} in \
+ ?) ;; \
+ *) echo "am__make_running_with_option: internal error: invalid" \
+ "target option '$${target_option-}' specified" >&2; \
+ exit 1;; \
+ esac; \
+ has_opt=no; \
+ sane_makeflags=$$MAKEFLAGS; \
+ if $(am__is_gnu_make); then \
+ sane_makeflags=$$MFLAGS; \
+ else \
+ case $$MAKEFLAGS in \
+ *\\[\ \ ]*) \
+ bs=\\; \
+ sane_makeflags=`printf '%s\n' "$$MAKEFLAGS" \
+ | sed "s/$$bs$$bs[$$bs $$bs ]*//g"`;; \
+ esac; \
+ fi; \
+ skip_next=no; \
+ strip_trailopt () \
+ { \
+ flg=`printf '%s\n' "$$flg" | sed "s/$$1.*$$//"`; \
+ }; \
+ for flg in $$sane_makeflags; do \
+ test $$skip_next = yes && { skip_next=no; continue; }; \
+ case $$flg in \
+ *=*|--*) continue;; \
+ -*I) strip_trailopt 'I'; skip_next=yes;; \
+ -*I?*) strip_trailopt 'I';; \
+ -*O) strip_trailopt 'O'; skip_next=yes;; \
+ -*O?*) strip_trailopt 'O';; \
+ -*l) strip_trailopt 'l'; skip_next=yes;; \
+ -*l?*) strip_trailopt 'l';; \
+ -[dEDm]) skip_next=yes;; \
+ -[JT]) skip_next=yes;; \
+ esac; \
+ case $$flg in \
+ *$$target_option*) has_opt=yes; break;; \
+ esac; \
+ done; \
+ test $$has_opt = yes
+am__make_dryrun = (target_option=n; $(am__make_running_with_option))
+am__make_keepgoing = (target_option=k; $(am__make_running_with_option))
+pkgdatadir = $(datadir)/@PACKAGE@
+pkgincludedir = $(includedir)/@PACKAGE@
+pkglibdir = $(libdir)/@PACKAGE@
+pkglibexecdir = $(libexecdir)/@PACKAGE@
+am__cd = CDPATH="$${ZSH_VERSION+.}$(PATH_SEPARATOR)" && cd
+install_sh_DATA = $(install_sh) -c -m 644
+install_sh_PROGRAM = $(install_sh) -c
+install_sh_SCRIPT = $(install_sh) -c
+INSTALL_HEADER = $(INSTALL_DATA)
+transform = $(program_transform_name)
+NORMAL_INSTALL = :
+PRE_INSTALL = :
+POST_INSTALL = :
+NORMAL_UNINSTALL = :
+PRE_UNINSTALL = :
+POST_UNINSTALL = :
+build_triplet = @build@
+host_triplet = @host@
+noinst_PROGRAMS = $(am__EXEEXT_1)
+subdir = src/lib-auth
+ACLOCAL_M4 = $(top_srcdir)/aclocal.m4
+am__aclocal_m4_deps = $(top_srcdir)/m4/ac_checktype2.m4 \
+ $(top_srcdir)/m4/ac_typeof.m4 $(top_srcdir)/m4/arc4random.m4 \
+ $(top_srcdir)/m4/blockdev.m4 $(top_srcdir)/m4/c99_vsnprintf.m4 \
+ $(top_srcdir)/m4/clock_gettime.m4 $(top_srcdir)/m4/crypt.m4 \
+ $(top_srcdir)/m4/crypt_xpg6.m4 $(top_srcdir)/m4/dbqlk.m4 \
+ $(top_srcdir)/m4/dirent_dtype.m4 $(top_srcdir)/m4/dovecot.m4 \
+ $(top_srcdir)/m4/fd_passing.m4 $(top_srcdir)/m4/fdatasync.m4 \
+ $(top_srcdir)/m4/flexible_array_member.m4 \
+ $(top_srcdir)/m4/glibc.m4 $(top_srcdir)/m4/gmtime_max.m4 \
+ $(top_srcdir)/m4/gmtime_tm_gmtoff.m4 \
+ $(top_srcdir)/m4/ioloop.m4 $(top_srcdir)/m4/iovec.m4 \
+ $(top_srcdir)/m4/ipv6.m4 $(top_srcdir)/m4/libcap.m4 \
+ $(top_srcdir)/m4/libtool.m4 $(top_srcdir)/m4/libwrap.m4 \
+ $(top_srcdir)/m4/linux_mremap.m4 $(top_srcdir)/m4/ltoptions.m4 \
+ $(top_srcdir)/m4/ltsugar.m4 $(top_srcdir)/m4/ltversion.m4 \
+ $(top_srcdir)/m4/lt~obsolete.m4 $(top_srcdir)/m4/mmap_write.m4 \
+ $(top_srcdir)/m4/mntctl.m4 $(top_srcdir)/m4/modules.m4 \
+ $(top_srcdir)/m4/notify.m4 $(top_srcdir)/m4/nsl.m4 \
+ $(top_srcdir)/m4/off_t_max.m4 $(top_srcdir)/m4/pkg.m4 \
+ $(top_srcdir)/m4/pr_set_dumpable.m4 \
+ $(top_srcdir)/m4/q_quotactl.m4 $(top_srcdir)/m4/quota.m4 \
+ $(top_srcdir)/m4/random.m4 $(top_srcdir)/m4/rlimit.m4 \
+ $(top_srcdir)/m4/sendfile.m4 $(top_srcdir)/m4/size_t_signed.m4 \
+ $(top_srcdir)/m4/sockpeercred.m4 $(top_srcdir)/m4/sql.m4 \
+ $(top_srcdir)/m4/ssl.m4 $(top_srcdir)/m4/st_tim.m4 \
+ $(top_srcdir)/m4/static_array.m4 $(top_srcdir)/m4/test_with.m4 \
+ $(top_srcdir)/m4/time_t.m4 $(top_srcdir)/m4/typeof.m4 \
+ $(top_srcdir)/m4/typeof_dev_t.m4 \
+ $(top_srcdir)/m4/uoff_t_max.m4 $(top_srcdir)/m4/vararg.m4 \
+ $(top_srcdir)/m4/want_apparmor.m4 \
+ $(top_srcdir)/m4/want_bsdauth.m4 \
+ $(top_srcdir)/m4/want_bzlib.m4 \
+ $(top_srcdir)/m4/want_cassandra.m4 \
+ $(top_srcdir)/m4/want_cdb.m4 \
+ $(top_srcdir)/m4/want_checkpassword.m4 \
+ $(top_srcdir)/m4/want_clucene.m4 $(top_srcdir)/m4/want_db.m4 \
+ $(top_srcdir)/m4/want_gssapi.m4 $(top_srcdir)/m4/want_icu.m4 \
+ $(top_srcdir)/m4/want_ldap.m4 $(top_srcdir)/m4/want_lua.m4 \
+ $(top_srcdir)/m4/want_lz4.m4 $(top_srcdir)/m4/want_lzma.m4 \
+ $(top_srcdir)/m4/want_mysql.m4 $(top_srcdir)/m4/want_pam.m4 \
+ $(top_srcdir)/m4/want_passwd.m4 $(top_srcdir)/m4/want_pgsql.m4 \
+ $(top_srcdir)/m4/want_prefetch.m4 \
+ $(top_srcdir)/m4/want_shadow.m4 \
+ $(top_srcdir)/m4/want_sodium.m4 $(top_srcdir)/m4/want_solr.m4 \
+ $(top_srcdir)/m4/want_sqlite.m4 \
+ $(top_srcdir)/m4/want_stemmer.m4 \
+ $(top_srcdir)/m4/want_systemd.m4 \
+ $(top_srcdir)/m4/want_textcat.m4 \
+ $(top_srcdir)/m4/want_unwind.m4 $(top_srcdir)/m4/want_zlib.m4 \
+ $(top_srcdir)/m4/want_zstd.m4 $(top_srcdir)/configure.ac
+am__configure_deps = $(am__aclocal_m4_deps) $(CONFIGURE_DEPENDENCIES) \
+ $(ACLOCAL_M4)
+DIST_COMMON = $(srcdir)/Makefile.am $(pkginc_lib_HEADERS) \
+ $(am__DIST_COMMON)
+mkinstalldirs = $(install_sh) -d
+CONFIG_HEADER = $(top_builddir)/config.h
+CONFIG_CLEAN_FILES =
+CONFIG_CLEAN_VPATH_FILES =
+am__EXEEXT_1 = test-auth-master$(EXEEXT)
+PROGRAMS = $(noinst_PROGRAMS)
+LTLIBRARIES = $(noinst_LTLIBRARIES)
+libauth_la_LIBADD =
+am_libauth_la_OBJECTS = auth-client.lo auth-client-request.lo \
+ auth-client-connection.lo auth-master.lo
+libauth_la_OBJECTS = $(am_libauth_la_OBJECTS)
+AM_V_lt = $(am__v_lt_@AM_V@)
+am__v_lt_ = $(am__v_lt_@AM_DEFAULT_V@)
+am__v_lt_0 = --silent
+am__v_lt_1 =
+am_test_auth_master_OBJECTS = test-auth-master.$(OBJEXT)
+test_auth_master_OBJECTS = $(am_test_auth_master_OBJECTS)
+am__DEPENDENCIES_1 =
+am__DEPENDENCIES_2 = $(noinst_LTLIBRARIES) ../lib-test/libtest.la \
+ ../lib/liblib.la $(am__DEPENDENCIES_1)
+AM_V_P = $(am__v_P_@AM_V@)
+am__v_P_ = $(am__v_P_@AM_DEFAULT_V@)
+am__v_P_0 = false
+am__v_P_1 = :
+AM_V_GEN = $(am__v_GEN_@AM_V@)
+am__v_GEN_ = $(am__v_GEN_@AM_DEFAULT_V@)
+am__v_GEN_0 = @echo " GEN " $@;
+am__v_GEN_1 =
+AM_V_at = $(am__v_at_@AM_V@)
+am__v_at_ = $(am__v_at_@AM_DEFAULT_V@)
+am__v_at_0 = @
+am__v_at_1 =
+DEFAULT_INCLUDES = -I.@am__isrc@ -I$(top_builddir)
+depcomp = $(SHELL) $(top_srcdir)/depcomp
+am__maybe_remake_depfiles = depfiles
+am__depfiles_remade = ./$(DEPDIR)/auth-client-connection.Plo \
+ ./$(DEPDIR)/auth-client-request.Plo \
+ ./$(DEPDIR)/auth-client.Plo ./$(DEPDIR)/auth-master.Plo \
+ ./$(DEPDIR)/test-auth-master.Po
+am__mv = mv -f
+COMPILE = $(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) \
+ $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS)
+LTCOMPILE = $(LIBTOOL) $(AM_V_lt) --tag=CC $(AM_LIBTOOLFLAGS) \
+ $(LIBTOOLFLAGS) --mode=compile $(CC) $(DEFS) \
+ $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) $(CPPFLAGS) \
+ $(AM_CFLAGS) $(CFLAGS)
+AM_V_CC = $(am__v_CC_@AM_V@)
+am__v_CC_ = $(am__v_CC_@AM_DEFAULT_V@)
+am__v_CC_0 = @echo " CC " $@;
+am__v_CC_1 =
+CCLD = $(CC)
+LINK = $(LIBTOOL) $(AM_V_lt) --tag=CC $(AM_LIBTOOLFLAGS) \
+ $(LIBTOOLFLAGS) --mode=link $(CCLD) $(AM_CFLAGS) $(CFLAGS) \
+ $(AM_LDFLAGS) $(LDFLAGS) -o $@
+AM_V_CCLD = $(am__v_CCLD_@AM_V@)
+am__v_CCLD_ = $(am__v_CCLD_@AM_DEFAULT_V@)
+am__v_CCLD_0 = @echo " CCLD " $@;
+am__v_CCLD_1 =
+SOURCES = $(libauth_la_SOURCES) $(test_auth_master_SOURCES)
+DIST_SOURCES = $(libauth_la_SOURCES) $(test_auth_master_SOURCES)
+am__can_run_installinfo = \
+ case $$AM_UPDATE_INFO_DIR in \
+ n|no|NO) false;; \
+ *) (install-info --version) >/dev/null 2>&1;; \
+ esac
+am__vpath_adj_setup = srcdirstrip=`echo "$(srcdir)" | sed 's|.|.|g'`;
+am__vpath_adj = case $$p in \
+ $(srcdir)/*) f=`echo "$$p" | sed "s|^$$srcdirstrip/||"`;; \
+ *) f=$$p;; \
+ esac;
+am__strip_dir = f=`echo $$p | sed -e 's|^.*/||'`;
+am__install_max = 40
+am__nobase_strip_setup = \
+ srcdirstrip=`echo "$(srcdir)" | sed 's/[].[^$$\\*|]/\\\\&/g'`
+am__nobase_strip = \
+ for p in $$list; do echo "$$p"; done | sed -e "s|$$srcdirstrip/||"
+am__nobase_list = $(am__nobase_strip_setup); \
+ for p in $$list; do echo "$$p $$p"; done | \
+ sed "s| $$srcdirstrip/| |;"' / .*\//!s/ .*/ ./; s,\( .*\)/[^/]*$$,\1,' | \
+ $(AWK) 'BEGIN { files["."] = "" } { files[$$2] = files[$$2] " " $$1; \
+ if (++n[$$2] == $(am__install_max)) \
+ { print $$2, files[$$2]; n[$$2] = 0; files[$$2] = "" } } \
+ END { for (dir in files) print dir, files[dir] }'
+am__base_list = \
+ sed '$$!N;$$!N;$$!N;$$!N;$$!N;$$!N;$$!N;s/\n/ /g' | \
+ sed '$$!N;$$!N;$$!N;$$!N;s/\n/ /g'
+am__uninstall_files_from_dir = { \
+ test -z "$$files" \
+ || { test ! -d "$$dir" && test ! -f "$$dir" && test ! -r "$$dir"; } \
+ || { echo " ( cd '$$dir' && rm -f" $$files ")"; \
+ $(am__cd) "$$dir" && rm -f $$files; }; \
+ }
+am__installdirs = "$(DESTDIR)$(pkginc_libdir)"
+HEADERS = $(pkginc_lib_HEADERS)
+am__tagged_files = $(HEADERS) $(SOURCES) $(TAGS_FILES) $(LISP)
+# Read a list of newline-separated strings from the standard input,
+# and print each of them once, without duplicates. Input order is
+# *not* preserved.
+am__uniquify_input = $(AWK) '\
+ BEGIN { nonempty = 0; } \
+ { items[$$0] = 1; nonempty = 1; } \
+ END { if (nonempty) { for (i in items) print i; }; } \
+'
+# Make sure the list of sources is unique. This is necessary because,
+# e.g., the same source file might be shared among _SOURCES variables
+# for different programs/libraries.
+am__define_uniq_tagged_files = \
+ list='$(am__tagged_files)'; \
+ unique=`for i in $$list; do \
+ if test -f "$$i"; then echo $$i; else echo $(srcdir)/$$i; fi; \
+ done | $(am__uniquify_input)`
+ETAGS = etags
+CTAGS = ctags
+am__DIST_COMMON = $(srcdir)/Makefile.in $(top_srcdir)/depcomp
+DISTFILES = $(DIST_COMMON) $(DIST_SOURCES) $(TEXINFOS) $(EXTRA_DIST)
+ACLOCAL = @ACLOCAL@
+ACLOCAL_AMFLAGS = @ACLOCAL_AMFLAGS@
+AMTAR = @AMTAR@
+AM_DEFAULT_VERBOSITY = @AM_DEFAULT_VERBOSITY@
+APPARMOR_LIBS = @APPARMOR_LIBS@
+AR = @AR@
+AUTH_CFLAGS = @AUTH_CFLAGS@
+AUTH_LIBS = @AUTH_LIBS@
+AUTOCONF = @AUTOCONF@
+AUTOHEADER = @AUTOHEADER@
+AUTOMAKE = @AUTOMAKE@
+AWK = @AWK@
+BINARY_CFLAGS = @BINARY_CFLAGS@
+BINARY_LDFLAGS = @BINARY_LDFLAGS@
+BISON = @BISON@
+CASSANDRA_CFLAGS = @CASSANDRA_CFLAGS@
+CASSANDRA_LIBS = @CASSANDRA_LIBS@
+CC = @CC@
+CCDEPMODE = @CCDEPMODE@
+CDB_LIBS = @CDB_LIBS@
+CFLAGS = @CFLAGS@
+CLUCENE_CFLAGS = @CLUCENE_CFLAGS@
+CLUCENE_LIBS = @CLUCENE_LIBS@
+COMPRESS_LIBS = @COMPRESS_LIBS@
+CPP = @CPP@
+CPPFLAGS = @CPPFLAGS@
+CRYPT_LIBS = @CRYPT_LIBS@
+CXX = @CXX@
+CXXCPP = @CXXCPP@
+CXXDEPMODE = @CXXDEPMODE@
+CXXFLAGS = @CXXFLAGS@
+CYGPATH_W = @CYGPATH_W@
+DEFS = @DEFS@
+DEPDIR = @DEPDIR@
+DICT_LIBS = @DICT_LIBS@
+DLLIB = @DLLIB@
+DLLTOOL = @DLLTOOL@
+DSYMUTIL = @DSYMUTIL@
+DUMPBIN = @DUMPBIN@
+ECHO_C = @ECHO_C@
+ECHO_N = @ECHO_N@
+ECHO_T = @ECHO_T@
+EGREP = @EGREP@
+EXEEXT = @EXEEXT@
+FGREP = @FGREP@
+FLEX = @FLEX@
+FUZZER_CPPFLAGS = @FUZZER_CPPFLAGS@
+FUZZER_LDFLAGS = @FUZZER_LDFLAGS@
+GREP = @GREP@
+INSTALL = @INSTALL@
+INSTALL_DATA = @INSTALL_DATA@
+INSTALL_PROGRAM = @INSTALL_PROGRAM@
+INSTALL_SCRIPT = @INSTALL_SCRIPT@
+INSTALL_STRIP_PROGRAM = @INSTALL_STRIP_PROGRAM@
+KRB5CONFIG = @KRB5CONFIG@
+KRB5_CFLAGS = @KRB5_CFLAGS@
+KRB5_LIBS = @KRB5_LIBS@
+LD = @LD@
+LDAP_LIBS = @LDAP_LIBS@
+LDFLAGS = @LDFLAGS@
+LD_NO_WHOLE_ARCHIVE = @LD_NO_WHOLE_ARCHIVE@
+LD_WHOLE_ARCHIVE = @LD_WHOLE_ARCHIVE@
+LIBCAP = @LIBCAP@
+LIBDOVECOT = @LIBDOVECOT@
+LIBDOVECOT_COMPRESS = @LIBDOVECOT_COMPRESS@
+LIBDOVECOT_DEPS = @LIBDOVECOT_DEPS@
+LIBDOVECOT_DSYNC = @LIBDOVECOT_DSYNC@
+LIBDOVECOT_LA_LIBS = @LIBDOVECOT_LA_LIBS@
+LIBDOVECOT_LDA = @LIBDOVECOT_LDA@
+LIBDOVECOT_LDAP = @LIBDOVECOT_LDAP@
+LIBDOVECOT_LIBFTS = @LIBDOVECOT_LIBFTS@
+LIBDOVECOT_LIBFTS_DEPS = @LIBDOVECOT_LIBFTS_DEPS@
+LIBDOVECOT_LOGIN = @LIBDOVECOT_LOGIN@
+LIBDOVECOT_LUA = @LIBDOVECOT_LUA@
+LIBDOVECOT_LUA_DEPS = @LIBDOVECOT_LUA_DEPS@
+LIBDOVECOT_SQL = @LIBDOVECOT_SQL@
+LIBDOVECOT_STORAGE = @LIBDOVECOT_STORAGE@
+LIBDOVECOT_STORAGE_DEPS = @LIBDOVECOT_STORAGE_DEPS@
+LIBEXTTEXTCAT_CFLAGS = @LIBEXTTEXTCAT_CFLAGS@
+LIBEXTTEXTCAT_LIBS = @LIBEXTTEXTCAT_LIBS@
+LIBICONV = @LIBICONV@
+LIBICU_CFLAGS = @LIBICU_CFLAGS@
+LIBICU_LIBS = @LIBICU_LIBS@
+LIBOBJS = @LIBOBJS@
+LIBS = @LIBS@
+LIBSODIUM_CFLAGS = @LIBSODIUM_CFLAGS@
+LIBSODIUM_LIBS = @LIBSODIUM_LIBS@
+LIBTIRPC_CFLAGS = @LIBTIRPC_CFLAGS@
+LIBTIRPC_LIBS = @LIBTIRPC_LIBS@
+LIBTOOL = @LIBTOOL@
+LIBUNWIND_CFLAGS = @LIBUNWIND_CFLAGS@
+LIBUNWIND_LIBS = @LIBUNWIND_LIBS@
+LIBWRAP_LIBS = @LIBWRAP_LIBS@
+LINKED_STORAGE_LDADD = @LINKED_STORAGE_LDADD@
+LIPO = @LIPO@
+LN_S = @LN_S@
+LTLIBICONV = @LTLIBICONV@
+LTLIBOBJS = @LTLIBOBJS@
+LT_SYS_LIBRARY_PATH = @LT_SYS_LIBRARY_PATH@
+LUA_CFLAGS = @LUA_CFLAGS@
+LUA_LIBS = @LUA_LIBS@
+MAINT = @MAINT@
+MAKEINFO = @MAKEINFO@
+MANIFEST_TOOL = @MANIFEST_TOOL@
+MKDIR_P = @MKDIR_P@
+MODULE_LIBS = @MODULE_LIBS@
+MODULE_SUFFIX = @MODULE_SUFFIX@
+MYSQL_CFLAGS = @MYSQL_CFLAGS@
+MYSQL_CONFIG = @MYSQL_CONFIG@
+MYSQL_LIBS = @MYSQL_LIBS@
+NM = @NM@
+NMEDIT = @NMEDIT@
+NOPLUGIN_LDFLAGS = @NOPLUGIN_LDFLAGS@
+OBJDUMP = @OBJDUMP@
+OBJEXT = @OBJEXT@
+OTOOL = @OTOOL@
+OTOOL64 = @OTOOL64@
+PACKAGE = @PACKAGE@
+PACKAGE_BUGREPORT = @PACKAGE_BUGREPORT@
+PACKAGE_NAME = @PACKAGE_NAME@
+PACKAGE_STRING = @PACKAGE_STRING@
+PACKAGE_TARNAME = @PACKAGE_TARNAME@
+PACKAGE_URL = @PACKAGE_URL@
+PACKAGE_VERSION = @PACKAGE_VERSION@
+PANDOC = @PANDOC@
+PATH_SEPARATOR = @PATH_SEPARATOR@
+PGSQL_CFLAGS = @PGSQL_CFLAGS@
+PGSQL_LIBS = @PGSQL_LIBS@
+PG_CONFIG = @PG_CONFIG@
+PIE_CFLAGS = @PIE_CFLAGS@
+PIE_LDFLAGS = @PIE_LDFLAGS@
+PKG_CONFIG = @PKG_CONFIG@
+PKG_CONFIG_LIBDIR = @PKG_CONFIG_LIBDIR@
+PKG_CONFIG_PATH = @PKG_CONFIG_PATH@
+QUOTA_LIBS = @QUOTA_LIBS@
+RANLIB = @RANLIB@
+RELRO_LDFLAGS = @RELRO_LDFLAGS@
+RPCGEN = @RPCGEN@
+RUN_TEST = @RUN_TEST@
+SED = @SED@
+SETTING_FILES = @SETTING_FILES@
+SET_MAKE = @SET_MAKE@
+SHELL = @SHELL@
+SQLITE_CFLAGS = @SQLITE_CFLAGS@
+SQLITE_LIBS = @SQLITE_LIBS@
+SQL_CFLAGS = @SQL_CFLAGS@
+SQL_LIBS = @SQL_LIBS@
+SSL_CFLAGS = @SSL_CFLAGS@
+SSL_LIBS = @SSL_LIBS@
+STRIP = @STRIP@
+SYSTEMD_CFLAGS = @SYSTEMD_CFLAGS@
+SYSTEMD_LIBS = @SYSTEMD_LIBS@
+VALGRIND = @VALGRIND@
+VERSION = @VERSION@
+ZSTD_CFLAGS = @ZSTD_CFLAGS@
+ZSTD_LIBS = @ZSTD_LIBS@
+abs_builddir = @abs_builddir@
+abs_srcdir = @abs_srcdir@
+abs_top_builddir = @abs_top_builddir@
+abs_top_srcdir = @abs_top_srcdir@
+ac_ct_AR = @ac_ct_AR@
+ac_ct_CC = @ac_ct_CC@
+ac_ct_CXX = @ac_ct_CXX@
+ac_ct_DUMPBIN = @ac_ct_DUMPBIN@
+am__include = @am__include@
+am__leading_dot = @am__leading_dot@
+am__quote = @am__quote@
+am__tar = @am__tar@
+am__untar = @am__untar@
+bindir = @bindir@
+build = @build@
+build_alias = @build_alias@
+build_cpu = @build_cpu@
+build_os = @build_os@
+build_vendor = @build_vendor@
+builddir = @builddir@
+datadir = @datadir@
+datarootdir = @datarootdir@
+dict_drivers = @dict_drivers@
+docdir = @docdir@
+dvidir = @dvidir@
+exec_prefix = @exec_prefix@
+host = @host@
+host_alias = @host_alias@
+host_cpu = @host_cpu@
+host_os = @host_os@
+host_vendor = @host_vendor@
+htmldir = @htmldir@
+includedir = @includedir@
+infodir = @infodir@
+install_sh = @install_sh@
+libdir = @libdir@
+libexecdir = @libexecdir@
+localedir = @localedir@
+localstatedir = @localstatedir@
+mandir = @mandir@
+mkdir_p = @mkdir_p@
+moduledir = @moduledir@
+oldincludedir = @oldincludedir@
+pdfdir = @pdfdir@
+prefix = @prefix@
+program_transform_name = @program_transform_name@
+psdir = @psdir@
+rundir = @rundir@
+runstatedir = @runstatedir@
+sbindir = @sbindir@
+sharedstatedir = @sharedstatedir@
+sql_drivers = @sql_drivers@
+srcdir = @srcdir@
+ssldir = @ssldir@
+statedir = @statedir@
+sysconfdir = @sysconfdir@
+systemdservicetype = @systemdservicetype@
+systemdsystemunitdir = @systemdsystemunitdir@
+target_alias = @target_alias@
+top_build_prefix = @top_build_prefix@
+top_builddir = @top_builddir@
+top_srcdir = @top_srcdir@
+noinst_LTLIBRARIES = libauth.la
+AM_CPPFLAGS = \
+ -I$(top_srcdir)/src/lib \
+ -I$(top_srcdir)/src/lib-master \
+ -I$(top_srcdir)/src/lib-test
+
+libauth_la_SOURCES = \
+ auth-client.c \
+ auth-client-request.c \
+ auth-client-connection.c \
+ auth-master.c
+
+headers = \
+ auth-client.h \
+ auth-client-interface.h \
+ auth-client-private.h \
+ auth-master.h
+
+pkginc_libdir = $(pkgincludedir)
+pkginc_lib_HEADERS = $(headers)
+test_programs = \
+ test-auth-master
+
+test_libs = \
+ $(noinst_LTLIBRARIES) \
+ ../lib-test/libtest.la \
+ ../lib/liblib.la \
+ $(MODULE_LIBS)
+
+test_deps = \
+ $(noinst_LTLIBRARIES) \
+ ../lib-test/libtest.la \
+ ../lib/liblib.la
+
+test_auth_master_SOURCES = test-auth-master.c
+test_auth_master_LDADD = $(test_libs)
+test_auth_master_DEPENDENCIES = $(test_deps)
+all: all-am
+
+.SUFFIXES:
+.SUFFIXES: .c .lo .o .obj
+$(srcdir)/Makefile.in: @MAINTAINER_MODE_TRUE@ $(srcdir)/Makefile.am $(am__configure_deps)
+ @for dep in $?; do \
+ case '$(am__configure_deps)' in \
+ *$$dep*) \
+ ( cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh ) \
+ && { if test -f $@; then exit 0; else break; fi; }; \
+ exit 1;; \
+ esac; \
+ done; \
+ echo ' cd $(top_srcdir) && $(AUTOMAKE) --foreign src/lib-auth/Makefile'; \
+ $(am__cd) $(top_srcdir) && \
+ $(AUTOMAKE) --foreign src/lib-auth/Makefile
+Makefile: $(srcdir)/Makefile.in $(top_builddir)/config.status
+ @case '$?' in \
+ *config.status*) \
+ cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh;; \
+ *) \
+ echo ' cd $(top_builddir) && $(SHELL) ./config.status $(subdir)/$@ $(am__maybe_remake_depfiles)'; \
+ cd $(top_builddir) && $(SHELL) ./config.status $(subdir)/$@ $(am__maybe_remake_depfiles);; \
+ esac;
+
+$(top_builddir)/config.status: $(top_srcdir)/configure $(CONFIG_STATUS_DEPENDENCIES)
+ cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh
+
+$(top_srcdir)/configure: @MAINTAINER_MODE_TRUE@ $(am__configure_deps)
+ cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh
+$(ACLOCAL_M4): @MAINTAINER_MODE_TRUE@ $(am__aclocal_m4_deps)
+ cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh
+$(am__aclocal_m4_deps):
+
+clean-noinstPROGRAMS:
+ @list='$(noinst_PROGRAMS)'; test -n "$$list" || exit 0; \
+ echo " rm -f" $$list; \
+ rm -f $$list || exit $$?; \
+ test -n "$(EXEEXT)" || exit 0; \
+ list=`for p in $$list; do echo "$$p"; done | sed 's/$(EXEEXT)$$//'`; \
+ echo " rm -f" $$list; \
+ rm -f $$list
+
+clean-noinstLTLIBRARIES:
+ -test -z "$(noinst_LTLIBRARIES)" || rm -f $(noinst_LTLIBRARIES)
+ @list='$(noinst_LTLIBRARIES)'; \
+ locs=`for p in $$list; do echo $$p; done | \
+ sed 's|^[^/]*$$|.|; s|/[^/]*$$||; s|$$|/so_locations|' | \
+ sort -u`; \
+ test -z "$$locs" || { \
+ echo rm -f $${locs}; \
+ rm -f $${locs}; \
+ }
+
+libauth.la: $(libauth_la_OBJECTS) $(libauth_la_DEPENDENCIES) $(EXTRA_libauth_la_DEPENDENCIES)
+ $(AM_V_CCLD)$(LINK) $(libauth_la_OBJECTS) $(libauth_la_LIBADD) $(LIBS)
+
+test-auth-master$(EXEEXT): $(test_auth_master_OBJECTS) $(test_auth_master_DEPENDENCIES) $(EXTRA_test_auth_master_DEPENDENCIES)
+ @rm -f test-auth-master$(EXEEXT)
+ $(AM_V_CCLD)$(LINK) $(test_auth_master_OBJECTS) $(test_auth_master_LDADD) $(LIBS)
+
+mostlyclean-compile:
+ -rm -f *.$(OBJEXT)
+
+distclean-compile:
+ -rm -f *.tab.c
+
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/auth-client-connection.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/auth-client-request.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/auth-client.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/auth-master.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/test-auth-master.Po@am__quote@ # am--include-marker
+
+$(am__depfiles_remade):
+ @$(MKDIR_P) $(@D)
+ @echo '# dummy' >$@-t && $(am__mv) $@-t $@
+
+am--depfiles: $(am__depfiles_remade)
+
+.c.o:
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(COMPILE) -MT $@ -MD -MP -MF $(DEPDIR)/$*.Tpo -c -o $@ $<
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/$*.Tpo $(DEPDIR)/$*.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='$<' object='$@' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(COMPILE) -c -o $@ $<
+
+.c.obj:
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(COMPILE) -MT $@ -MD -MP -MF $(DEPDIR)/$*.Tpo -c -o $@ `$(CYGPATH_W) '$<'`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/$*.Tpo $(DEPDIR)/$*.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='$<' object='$@' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(COMPILE) -c -o $@ `$(CYGPATH_W) '$<'`
+
+.c.lo:
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(LTCOMPILE) -MT $@ -MD -MP -MF $(DEPDIR)/$*.Tpo -c -o $@ $<
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/$*.Tpo $(DEPDIR)/$*.Plo
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='$<' object='$@' libtool=yes @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(LTCOMPILE) -c -o $@ $<
+
+mostlyclean-libtool:
+ -rm -f *.lo
+
+clean-libtool:
+ -rm -rf .libs _libs
+install-pkginc_libHEADERS: $(pkginc_lib_HEADERS)
+ @$(NORMAL_INSTALL)
+ @list='$(pkginc_lib_HEADERS)'; test -n "$(pkginc_libdir)" || list=; \
+ if test -n "$$list"; then \
+ echo " $(MKDIR_P) '$(DESTDIR)$(pkginc_libdir)'"; \
+ $(MKDIR_P) "$(DESTDIR)$(pkginc_libdir)" || exit 1; \
+ fi; \
+ for p in $$list; do \
+ if test -f "$$p"; then d=; else d="$(srcdir)/"; fi; \
+ echo "$$d$$p"; \
+ done | $(am__base_list) | \
+ while read files; do \
+ echo " $(INSTALL_HEADER) $$files '$(DESTDIR)$(pkginc_libdir)'"; \
+ $(INSTALL_HEADER) $$files "$(DESTDIR)$(pkginc_libdir)" || exit $$?; \
+ done
+
+uninstall-pkginc_libHEADERS:
+ @$(NORMAL_UNINSTALL)
+ @list='$(pkginc_lib_HEADERS)'; test -n "$(pkginc_libdir)" || list=; \
+ files=`for p in $$list; do echo $$p; done | sed -e 's|^.*/||'`; \
+ dir='$(DESTDIR)$(pkginc_libdir)'; $(am__uninstall_files_from_dir)
+
+ID: $(am__tagged_files)
+ $(am__define_uniq_tagged_files); mkid -fID $$unique
+tags: tags-am
+TAGS: tags
+
+tags-am: $(TAGS_DEPENDENCIES) $(am__tagged_files)
+ set x; \
+ here=`pwd`; \
+ $(am__define_uniq_tagged_files); \
+ shift; \
+ if test -z "$(ETAGS_ARGS)$$*$$unique"; then :; else \
+ test -n "$$unique" || unique=$$empty_fix; \
+ if test $$# -gt 0; then \
+ $(ETAGS) $(ETAGSFLAGS) $(AM_ETAGSFLAGS) $(ETAGS_ARGS) \
+ "$$@" $$unique; \
+ else \
+ $(ETAGS) $(ETAGSFLAGS) $(AM_ETAGSFLAGS) $(ETAGS_ARGS) \
+ $$unique; \
+ fi; \
+ fi
+ctags: ctags-am
+
+CTAGS: ctags
+ctags-am: $(TAGS_DEPENDENCIES) $(am__tagged_files)
+ $(am__define_uniq_tagged_files); \
+ test -z "$(CTAGS_ARGS)$$unique" \
+ || $(CTAGS) $(CTAGSFLAGS) $(AM_CTAGSFLAGS) $(CTAGS_ARGS) \
+ $$unique
+
+GTAGS:
+ here=`$(am__cd) $(top_builddir) && pwd` \
+ && $(am__cd) $(top_srcdir) \
+ && gtags -i $(GTAGS_ARGS) "$$here"
+cscopelist: cscopelist-am
+
+cscopelist-am: $(am__tagged_files)
+ list='$(am__tagged_files)'; \
+ case "$(srcdir)" in \
+ [\\/]* | ?:[\\/]*) sdir="$(srcdir)" ;; \
+ *) sdir=$(subdir)/$(srcdir) ;; \
+ esac; \
+ for i in $$list; do \
+ if test -f "$$i"; then \
+ echo "$(subdir)/$$i"; \
+ else \
+ echo "$$sdir/$$i"; \
+ fi; \
+ done >> $(top_builddir)/cscope.files
+
+distclean-tags:
+ -rm -f TAGS ID GTAGS GRTAGS GSYMS GPATH tags
+
+distdir: $(BUILT_SOURCES)
+ $(MAKE) $(AM_MAKEFLAGS) distdir-am
+
+distdir-am: $(DISTFILES)
+ @srcdirstrip=`echo "$(srcdir)" | sed 's/[].[^$$\\*]/\\\\&/g'`; \
+ topsrcdirstrip=`echo "$(top_srcdir)" | sed 's/[].[^$$\\*]/\\\\&/g'`; \
+ list='$(DISTFILES)'; \
+ dist_files=`for file in $$list; do echo $$file; done | \
+ sed -e "s|^$$srcdirstrip/||;t" \
+ -e "s|^$$topsrcdirstrip/|$(top_builddir)/|;t"`; \
+ case $$dist_files in \
+ */*) $(MKDIR_P) `echo "$$dist_files" | \
+ sed '/\//!d;s|^|$(distdir)/|;s,/[^/]*$$,,' | \
+ sort -u` ;; \
+ esac; \
+ for file in $$dist_files; do \
+ if test -f $$file || test -d $$file; then d=.; else d=$(srcdir); fi; \
+ if test -d $$d/$$file; then \
+ dir=`echo "/$$file" | sed -e 's,/[^/]*$$,,'`; \
+ if test -d "$(distdir)/$$file"; then \
+ find "$(distdir)/$$file" -type d ! -perm -700 -exec chmod u+rwx {} \;; \
+ fi; \
+ if test -d $(srcdir)/$$file && test $$d != $(srcdir); then \
+ cp -fpR $(srcdir)/$$file "$(distdir)$$dir" || exit 1; \
+ find "$(distdir)/$$file" -type d ! -perm -700 -exec chmod u+rwx {} \;; \
+ fi; \
+ cp -fpR $$d/$$file "$(distdir)$$dir" || exit 1; \
+ else \
+ test -f "$(distdir)/$$file" \
+ || cp -p $$d/$$file "$(distdir)/$$file" \
+ || exit 1; \
+ fi; \
+ done
+check-am: all-am
+ $(MAKE) $(AM_MAKEFLAGS) check-local
+check: check-am
+all-am: Makefile $(PROGRAMS) $(LTLIBRARIES) $(HEADERS)
+installdirs:
+ for dir in "$(DESTDIR)$(pkginc_libdir)"; do \
+ test -z "$$dir" || $(MKDIR_P) "$$dir"; \
+ done
+install: install-am
+install-exec: install-exec-am
+install-data: install-data-am
+uninstall: uninstall-am
+
+install-am: all-am
+ @$(MAKE) $(AM_MAKEFLAGS) install-exec-am install-data-am
+
+installcheck: installcheck-am
+install-strip:
+ if test -z '$(STRIP)'; then \
+ $(MAKE) $(AM_MAKEFLAGS) INSTALL_PROGRAM="$(INSTALL_STRIP_PROGRAM)" \
+ install_sh_PROGRAM="$(INSTALL_STRIP_PROGRAM)" INSTALL_STRIP_FLAG=-s \
+ install; \
+ else \
+ $(MAKE) $(AM_MAKEFLAGS) INSTALL_PROGRAM="$(INSTALL_STRIP_PROGRAM)" \
+ install_sh_PROGRAM="$(INSTALL_STRIP_PROGRAM)" INSTALL_STRIP_FLAG=-s \
+ "INSTALL_PROGRAM_ENV=STRIPPROG='$(STRIP)'" install; \
+ fi
+mostlyclean-generic:
+
+clean-generic:
+
+distclean-generic:
+ -test -z "$(CONFIG_CLEAN_FILES)" || rm -f $(CONFIG_CLEAN_FILES)
+ -test . = "$(srcdir)" || test -z "$(CONFIG_CLEAN_VPATH_FILES)" || rm -f $(CONFIG_CLEAN_VPATH_FILES)
+
+maintainer-clean-generic:
+ @echo "This command is intended for maintainers to use"
+ @echo "it deletes files that may require special tools to rebuild."
+clean: clean-am
+
+clean-am: clean-generic clean-libtool clean-noinstLTLIBRARIES \
+ clean-noinstPROGRAMS mostlyclean-am
+
+distclean: distclean-am
+ -rm -f ./$(DEPDIR)/auth-client-connection.Plo
+ -rm -f ./$(DEPDIR)/auth-client-request.Plo
+ -rm -f ./$(DEPDIR)/auth-client.Plo
+ -rm -f ./$(DEPDIR)/auth-master.Plo
+ -rm -f ./$(DEPDIR)/test-auth-master.Po
+ -rm -f Makefile
+distclean-am: clean-am distclean-compile distclean-generic \
+ distclean-tags
+
+dvi: dvi-am
+
+dvi-am:
+
+html: html-am
+
+html-am:
+
+info: info-am
+
+info-am:
+
+install-data-am: install-pkginc_libHEADERS
+
+install-dvi: install-dvi-am
+
+install-dvi-am:
+
+install-exec-am:
+
+install-html: install-html-am
+
+install-html-am:
+
+install-info: install-info-am
+
+install-info-am:
+
+install-man:
+
+install-pdf: install-pdf-am
+
+install-pdf-am:
+
+install-ps: install-ps-am
+
+install-ps-am:
+
+installcheck-am:
+
+maintainer-clean: maintainer-clean-am
+ -rm -f ./$(DEPDIR)/auth-client-connection.Plo
+ -rm -f ./$(DEPDIR)/auth-client-request.Plo
+ -rm -f ./$(DEPDIR)/auth-client.Plo
+ -rm -f ./$(DEPDIR)/auth-master.Plo
+ -rm -f ./$(DEPDIR)/test-auth-master.Po
+ -rm -f Makefile
+maintainer-clean-am: distclean-am maintainer-clean-generic
+
+mostlyclean: mostlyclean-am
+
+mostlyclean-am: mostlyclean-compile mostlyclean-generic \
+ mostlyclean-libtool
+
+pdf: pdf-am
+
+pdf-am:
+
+ps: ps-am
+
+ps-am:
+
+uninstall-am: uninstall-pkginc_libHEADERS
+
+.MAKE: check-am install-am install-strip
+
+.PHONY: CTAGS GTAGS TAGS all all-am am--depfiles check check-am \
+ check-local clean clean-generic clean-libtool \
+ clean-noinstLTLIBRARIES clean-noinstPROGRAMS cscopelist-am \
+ ctags ctags-am distclean distclean-compile distclean-generic \
+ distclean-libtool distclean-tags distdir dvi dvi-am html \
+ html-am info info-am install install-am install-data \
+ install-data-am install-dvi install-dvi-am install-exec \
+ install-exec-am install-html install-html-am install-info \
+ install-info-am install-man install-pdf install-pdf-am \
+ install-pkginc_libHEADERS install-ps install-ps-am \
+ install-strip installcheck installcheck-am installdirs \
+ maintainer-clean maintainer-clean-generic mostlyclean \
+ mostlyclean-compile mostlyclean-generic mostlyclean-libtool \
+ pdf pdf-am ps ps-am tags tags-am uninstall uninstall-am \
+ uninstall-pkginc_libHEADERS
+
+.PRECIOUS: Makefile
+
+
+check-local:
+ for bin in $(test_programs); do \
+ if ! $(RUN_TEST) ./$$bin; then exit 1; fi; \
+ done
+
+# Tell versions [3.59,3.63) of GNU make to not export all variables.
+# Otherwise a system limit (for SysV at least) may be exceeded.
+.NOEXPORT:
diff --git a/src/lib-auth/auth-client-connection.c b/src/lib-auth/auth-client-connection.c
new file mode 100644
index 0000000..3a1e82c
--- /dev/null
+++ b/src/lib-auth/auth-client-connection.c
@@ -0,0 +1,552 @@
+/* Copyright (c) 2003-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "hash.h"
+#include "hostpid.h"
+#include "ioloop.h"
+#include "istream.h"
+#include "ostream.h"
+#include "net.h"
+#include "strescape.h"
+#include "eacces-error.h"
+#include "auth-client-private.h"
+
+#include <unistd.h>
+
+#define AUTH_SERVER_CONN_MAX_LINE_LENGTH AUTH_CLIENT_MAX_LINE_LENGTH
+#define AUTH_SERVER_RECONNECT_TIMEOUT_SECS 5
+
+static void auth_client_connection_connected(struct connection *_conn,
+ bool success);
+static int
+auth_client_connection_input_line(struct connection *_conn,
+ const char *line);
+static int
+auth_client_connection_handshake_line(struct connection *_conn,
+ const char *line);
+static void auth_client_connection_handshake_ready(struct connection *_conn);
+static void auth_client_connection_destroy(struct connection *_conn);
+static void
+auth_client_connection_reconnect(struct auth_client_connection *conn,
+ const char *disconnect_reason);
+
+static const struct connection_vfuncs auth_client_connection_vfuncs = {
+ .destroy = auth_client_connection_destroy,
+ .handshake_line = auth_client_connection_handshake_line,
+ .handshake_ready = auth_client_connection_handshake_ready,
+ .input_line = auth_client_connection_input_line,
+ .client_connected = auth_client_connection_connected,
+};
+
+static const struct connection_settings auth_client_connection_set = {
+ .dont_send_version = TRUE,
+ .service_name_in = "auth-client",
+ .service_name_out = "auth-client",
+ .major_version = AUTH_CLIENT_PROTOCOL_MAJOR_VERSION,
+ .minor_version = AUTH_CLIENT_PROTOCOL_MINOR_VERSION,
+ .unix_client_connect_msecs = 1000,
+ .input_max_size = AUTH_SERVER_CONN_MAX_LINE_LENGTH,
+ .output_max_size = SIZE_MAX,
+ .client = TRUE,
+};
+
+struct connection_list *
+auth_client_connection_list_init(void)
+{
+ return connection_list_init(&auth_client_connection_set,
+ &auth_client_connection_vfuncs);
+}
+
+static int
+auth_server_input_mech(struct auth_client_connection *conn,
+ const char *const *args)
+{
+ struct auth_mech_desc mech_desc;
+
+ if (args[0] == NULL) {
+ e_error(conn->conn.event,
+ "BUG: Authentication server sent broken MECH line");
+ return -1;
+ }
+
+ i_zero(&mech_desc);
+ mech_desc.name = p_strdup(conn->pool, args[0]);
+
+ if (strcmp(mech_desc.name, "PLAIN") == 0)
+ conn->has_plain_mech = TRUE;
+
+ for (args++; *args != NULL; args++) {
+ if (strcmp(*args, "private") == 0)
+ mech_desc.flags |= MECH_SEC_PRIVATE;
+ else if (strcmp(*args, "anonymous") == 0)
+ mech_desc.flags |= MECH_SEC_ANONYMOUS;
+ else if (strcmp(*args, "plaintext") == 0)
+ mech_desc.flags |= MECH_SEC_PLAINTEXT;
+ else if (strcmp(*args, "dictionary") == 0)
+ mech_desc.flags |= MECH_SEC_DICTIONARY;
+ else if (strcmp(*args, "active") == 0)
+ mech_desc.flags |= MECH_SEC_ACTIVE;
+ else if (strcmp(*args, "forward-secrecy") == 0)
+ mech_desc.flags |= MECH_SEC_FORWARD_SECRECY;
+ else if (strcmp(*args, "mutual-auth") == 0)
+ mech_desc.flags |= MECH_SEC_MUTUAL_AUTH;
+ }
+ array_push_back(&conn->available_auth_mechs, &mech_desc);
+ return 0;
+}
+
+static int
+auth_server_input_spid(struct auth_client_connection *conn,
+ const char *const *args)
+{
+ if (str_to_uint(args[0], &conn->server_pid) < 0) {
+ e_error(conn->conn.event,
+ "BUG: Authentication server sent invalid PID");
+ return -1;
+ }
+ return 0;
+}
+
+static int
+auth_server_input_cuid(struct auth_client_connection *conn,
+ const char *const *args)
+{
+ if (args[0] == NULL ||
+ str_to_uint(args[0], &conn->connect_uid) < 0) {
+ e_error(conn->conn.event,
+ "BUG: Authentication server sent broken CUID line");
+ return -1;
+ }
+ return 0;
+}
+
+static int
+auth_server_input_cookie(struct auth_client_connection *conn,
+ const char *const *args)
+{
+ if (conn->cookie != NULL) {
+ e_error(conn->conn.event,
+ "BUG: Authentication server already sent cookie");
+ return -1;
+ }
+ conn->cookie = p_strdup(conn->pool, args[0]);
+ return 0;
+}
+
+static int auth_server_input_done(struct auth_client_connection *conn)
+{
+ if (array_count(&conn->available_auth_mechs) == 0) {
+ e_error(conn->conn.event,
+ "BUG: Authentication server returned no mechanisms");
+ return -1;
+ }
+ if (conn->cookie == NULL) {
+ e_error(conn->conn.event,
+ "BUG: Authentication server didn't send a cookie");
+ return -1;
+ }
+ return 1;
+}
+
+static int
+auth_client_connection_handshake_line(struct connection *_conn,
+ const char *line)
+{
+ struct auth_client_connection *conn =
+ container_of(_conn, struct auth_client_connection, conn);
+ unsigned int major_version, minor_version;
+ const char *const *args;
+
+ args = t_strsplit_tabescaped(line);
+ if (strcmp(args[0], "VERSION") == 0 &&
+ args[1] != NULL && args[2] != NULL) {
+ if (str_to_uint(args[1], &major_version) < 0 ||
+ str_to_uint(args[2], &minor_version) < 0) {
+ e_error(conn->conn.event,
+ "Auth server sent invalid version line: %s",
+ line);
+ return -1;
+ }
+
+ if (connection_verify_version(_conn, "auth-client",
+ major_version,
+ minor_version) < 0) {
+ return -1;
+ }
+
+ return 0;
+ } else if (strcmp(args[0], "MECH") == 0) {
+ return auth_server_input_mech(conn, args + 1);
+ } else if (strcmp(args[0], "SPID") == 0) {
+ return auth_server_input_spid(conn, args + 1);
+ } else if (strcmp(args[0], "CUID") == 0) {
+ return auth_server_input_cuid(conn, args + 1);
+ } else if (strcmp(args[0], "COOKIE") == 0) {
+ return auth_server_input_cookie(conn, args + 1);
+ } else if (strcmp(args[0], "DONE") == 0) {
+ return auth_server_input_done(conn);
+ }
+
+ e_error(conn->conn.event, "Auth server sent unknown handshake: %s", line);
+ return -1;
+}
+
+static void auth_client_connection_handshake_ready(struct connection *_conn)
+{
+ struct auth_client_connection *conn =
+ container_of(_conn, struct auth_client_connection, conn);
+
+ timeout_remove(&conn->to);
+ if (conn->client->connect_notify_callback != NULL) {
+ conn->client->connect_notify_callback(conn->client, TRUE,
+ conn->client->connect_notify_context);
+ }
+}
+
+static int
+auth_server_lookup_request(struct auth_client_connection *conn,
+ const char *id_arg, bool remove,
+ struct auth_client_request **request_r)
+{
+ struct auth_client_request *request;
+ unsigned int id;
+
+ if (id_arg == NULL || str_to_uint(id_arg, &id) < 0) {
+ e_error(conn->conn.event,
+ "BUG: Authentication server input missing ID");
+ return -1;
+ }
+
+ request = hash_table_lookup(conn->requests, POINTER_CAST(id));
+ if (request == NULL) {
+ e_error(conn->conn.event,
+ "Authentication server sent unknown id %u", id);
+ return 0;
+ }
+ if (remove || auth_client_request_is_aborted(request))
+ hash_table_remove(conn->requests, POINTER_CAST(id));
+
+ *request_r = request;
+ return 1;
+}
+
+static int
+auth_server_input_ok(struct auth_client_connection *conn,
+ const char *const *args)
+{
+ struct auth_client_request *request;
+ int ret;
+
+ if ((ret = auth_server_lookup_request(conn, args[0], TRUE, &request)) <= 0)
+ return ret;
+ auth_client_request_server_input(request, AUTH_REQUEST_STATUS_OK,
+ args + 1);
+ return 0;
+}
+
+static int auth_server_input_cont(struct auth_client_connection *conn,
+ const char *const *args)
+{
+ struct auth_client_request *request;
+ int ret;
+
+ if (str_array_length(args) < 2) {
+ e_error(conn->conn.event,
+ "BUG: Authentication server sent broken CONT line");
+ return -1;
+ }
+
+ if ((ret = auth_server_lookup_request(conn, args[0], FALSE, &request)) <= 0)
+ return ret;
+ auth_client_request_server_input(request, AUTH_REQUEST_STATUS_CONTINUE,
+ args + 1);
+ return 0;
+}
+
+static int auth_server_input_fail(struct auth_client_connection *conn,
+ const char *const *args)
+{
+ struct auth_client_request *request;
+ int ret;
+
+ if ((ret = auth_server_lookup_request(conn, args[0], TRUE, &request)) <= 0)
+ return ret;
+ auth_client_request_server_input(request, AUTH_REQUEST_STATUS_FAIL,
+ args + 1);
+ return 0;
+}
+
+static int
+auth_client_connection_handle_line(struct auth_client_connection *conn,
+ const char *line)
+{
+ const char *const *args;
+
+ e_debug(conn->conn.event, "auth input: %s", line);
+
+ args = t_strsplit_tabescaped(line);
+ if (args[0] == NULL) {
+ e_error(conn->conn.event, "Auth server sent empty line");
+ return -1;
+ }
+ if (strcmp(args[0], "OK") == 0)
+ return auth_server_input_ok(conn, args + 1);
+ else if (strcmp(args[0], "CONT") == 0)
+ return auth_server_input_cont(conn, args + 1);
+ else if (strcmp(args[0], "FAIL") == 0)
+ return auth_server_input_fail(conn, args + 1);
+ else {
+ e_error(conn->conn.event,
+ "Auth server sent unknown response: %s", args[0]);
+ return -1;
+ }
+}
+
+static int
+auth_client_connection_input_line(struct connection *_conn,
+ const char *line)
+{
+ struct auth_client_connection *conn =
+ container_of(_conn, struct auth_client_connection, conn);
+ int ret;
+
+ ret = auth_client_connection_handle_line(conn, line);
+ if (ret < 0) {
+ auth_client_connection_disconnect(conn, t_strdup_printf(
+ "Received broken input: %s", line));
+ return -1;
+ }
+ return 1;
+}
+
+struct auth_client_connection *
+auth_client_connection_init(struct auth_client *client)
+{
+ struct auth_client_connection *conn;
+ pool_t pool;
+
+ pool = pool_alloconly_create("auth server connection", 1024);
+ conn = p_new(pool, struct auth_client_connection, 1);
+ conn->pool = pool;
+
+ conn->client = client;
+
+ conn->conn.event_parent = client->event;
+ connection_init_client_unix(client->clist, &conn->conn,
+ client->auth_socket_path);
+
+ hash_table_create_direct(&conn->requests, pool, 100);
+ i_array_init(&conn->available_auth_mechs, 8);
+ return conn;
+}
+
+static void
+auth_client_connection_remove_requests(struct auth_client_connection *conn,
+ const char *disconnect_reason)
+{
+ static const char *const temp_failure_args[] = { "temp", NULL };
+ struct hash_iterate_context *iter;
+ void *key;
+ struct auth_client_request *request;
+ time_t created, oldest = 0;
+ unsigned int request_count = 0;
+
+ if (hash_table_count(conn->requests) == 0)
+ return;
+
+ iter = hash_table_iterate_init(conn->requests);
+ while (hash_table_iterate(iter, conn->requests, &key, &request)) {
+ if (!auth_client_request_is_aborted(request)) {
+ request_count++;
+ created = auth_client_request_get_create_time(request);
+ if (oldest > created || oldest == 0)
+ oldest = created;
+ }
+
+ auth_client_request_server_input(request,
+ AUTH_REQUEST_STATUS_INTERNAL_FAIL,
+ temp_failure_args);
+ }
+ hash_table_iterate_deinit(&iter);
+ hash_table_clear(conn->requests, FALSE);
+
+ if (request_count > 0) {
+ e_warning(conn->conn.event,
+ "Auth connection closed with %u pending requests "
+ "(max %u secs, pid=%s, %s)", request_count,
+ (unsigned int)(ioloop_time - oldest),
+ my_pid, disconnect_reason);
+ }
+}
+
+void auth_client_connection_disconnect(struct auth_client_connection *conn,
+ const char *reason) ATTR_NULL(2)
+{
+ if (reason == NULL)
+ reason = "Disconnected from auth server, aborting";
+
+ if (conn->connected)
+ connection_disconnect(&conn->conn);
+ conn->connected = FALSE;
+
+ conn->has_plain_mech = FALSE;
+ conn->server_pid = 0;
+ conn->connect_uid = 0;
+ conn->cookie = NULL;
+ array_clear(&conn->available_auth_mechs);
+
+ timeout_remove(&conn->to);
+
+ auth_client_connection_remove_requests(conn, reason);
+
+ if (conn->client->connect_notify_callback != NULL) {
+ conn->client->connect_notify_callback(conn->client, FALSE,
+ conn->client->connect_notify_context);
+ }
+}
+
+static void auth_client_connection_destroy(struct connection *_conn)
+{
+ struct auth_client_connection *conn =
+ container_of(_conn, struct auth_client_connection, conn);
+
+ switch (_conn->disconnect_reason) {
+ case CONNECTION_DISCONNECT_HANDSHAKE_FAILED:
+ auth_client_connection_disconnect(
+ conn, "Handshake with auth service failed");
+ break;
+ case CONNECTION_DISCONNECT_BUFFER_FULL:
+ /* buffer full - can't happen unless auth is buggy */
+ e_error(conn->conn.event,
+ "BUG: Auth server sent us more than %d bytes of data",
+ AUTH_SERVER_CONN_MAX_LINE_LENGTH);
+ auth_client_connection_disconnect(conn, "Buffer full");
+ break;
+ default:
+ /* disconnected */
+ auth_client_connection_reconnect(
+ conn, (conn->conn.input->stream_errno != 0 ?
+ strerror(conn->conn.input->stream_errno) :
+ "EOF"));
+ }
+}
+
+static void auth_server_reconnect_timeout(struct auth_client_connection *conn)
+{
+ (void)auth_client_connection_connect(conn);
+}
+
+static void
+auth_client_connection_reconnect(struct auth_client_connection *conn,
+ const char *disconnect_reason)
+{
+ time_t next_connect;
+
+ auth_client_connection_disconnect(conn, disconnect_reason);
+
+ next_connect = conn->last_connect + AUTH_SERVER_RECONNECT_TIMEOUT_SECS;
+ conn->to = timeout_add(ioloop_time >= next_connect ? 0 :
+ (next_connect - ioloop_time) * 1000,
+ auth_server_reconnect_timeout, conn);
+}
+
+void auth_client_connection_deinit(struct auth_client_connection **_conn)
+{
+ struct auth_client_connection *conn = *_conn;
+
+ *_conn = NULL;
+
+ auth_client_connection_disconnect(conn, "deinitializing");
+ i_assert(hash_table_count(conn->requests) == 0);
+ hash_table_destroy(&conn->requests);
+ timeout_remove(&conn->to);
+ array_free(&conn->available_auth_mechs);
+ connection_deinit(&conn->conn);
+ pool_unref(&conn->pool);
+}
+
+static void auth_client_handshake_timeout(struct auth_client_connection *conn)
+{
+ e_error(conn->conn.event, "Timeout waiting for handshake from auth server. "
+ "my pid=%u, input bytes=%"PRIuUOFF_T,
+ conn->client->client_pid, conn->conn.input->v_offset);
+ auth_client_connection_reconnect(conn, "auth server timeout");
+}
+
+static void
+auth_client_connection_connected(struct connection *_conn, bool success)
+{
+ struct auth_client_connection *conn =
+ container_of(_conn, struct auth_client_connection, conn);
+
+ /* Cannot get here unless connect() was successful */
+ i_assert(success);
+
+ conn->connected = TRUE;
+}
+
+int auth_client_connection_connect(struct auth_client_connection *conn)
+{
+ const char *handshake;
+
+ i_assert(!conn->connected);
+
+ conn->last_connect = ioloop_time;
+ timeout_remove(&conn->to);
+
+ /* max. 1 second wait here. */
+ if (connection_client_connect(&conn->conn) < 0) {
+ if (errno == EACCES) {
+ e_error(conn->conn.event, "%s",
+ eacces_error_get("connect",
+ conn->client->auth_socket_path));
+ } else {
+ e_error(conn->conn.event, "connect(%s) failed: %m",
+ conn->client->auth_socket_path);
+ };
+ return -1;
+ }
+
+ handshake = t_strdup_printf("VERSION\t%u\t%u\nCPID\t%u\n",
+ AUTH_CLIENT_PROTOCOL_MAJOR_VERSION,
+ AUTH_CLIENT_PROTOCOL_MINOR_VERSION,
+ conn->client->client_pid);
+ if (o_stream_send_str(conn->conn.output, handshake) < 0) {
+ e_warning(conn->conn.event,
+ "Error sending handshake to auth server: %s",
+ o_stream_get_error(conn->conn.output));
+ auth_client_connection_disconnect(conn,
+ o_stream_get_error(conn->conn.output));
+ return -1;
+ }
+
+ conn->to = timeout_add(conn->client->connect_timeout_msecs,
+ auth_client_handshake_timeout, conn);
+ return 0;
+}
+
+unsigned int
+auth_client_connection_add_request(struct auth_client_connection *conn,
+ struct auth_client_request *request)
+{
+ unsigned int id;
+
+ i_assert(conn->conn.handshake_received);
+
+ id = ++conn->client->request_id_counter;
+ if (id == 0) {
+ /* wrapped - ID 0 not allowed */
+ id = ++conn->client->request_id_counter;
+ }
+ i_assert(hash_table_lookup(conn->requests, POINTER_CAST(id)) == NULL);
+ hash_table_insert(conn->requests, POINTER_CAST(id), request);
+ return id;
+}
+
+void auth_client_connection_remove_request(struct auth_client_connection *conn,
+ unsigned int id)
+{
+ i_assert(conn->conn.handshake_received);
+ hash_table_remove(conn->requests, POINTER_CAST(id));
+}
diff --git a/src/lib-auth/auth-client-interface.h b/src/lib-auth/auth-client-interface.h
new file mode 100644
index 0000000..2367a00
--- /dev/null
+++ b/src/lib-auth/auth-client-interface.h
@@ -0,0 +1,43 @@
+#ifndef AUTH_CLIENT_INTERFACE_H
+#define AUTH_CLIENT_INTERFACE_H
+
+/* Major version changes are not backwards compatible,
+ minor version numbers can be ignored. */
+#define AUTH_CLIENT_PROTOCOL_MAJOR_VERSION 1
+#define AUTH_CLIENT_PROTOCOL_MINOR_VERSION 2
+
+/* GSSAPI can use quite large packets */
+#define AUTH_CLIENT_MAX_LINE_LENGTH 16384
+
+enum mech_security_flags {
+ /* Don't advertise this as available SASL mechanism (eg. APOP) */
+ MECH_SEC_PRIVATE = 0x0001,
+ /* Anonymous authentication */
+ MECH_SEC_ANONYMOUS = 0x0002,
+ /* Transfers plaintext passwords */
+ MECH_SEC_PLAINTEXT = 0x0004,
+ /* Subject to passive (dictionary) attack */
+ MECH_SEC_DICTIONARY = 0x0008,
+ /* Subject to active (non-dictionary) attack */
+ MECH_SEC_ACTIVE = 0x0010,
+ /* Provides forward secrecy between sessions */
+ MECH_SEC_FORWARD_SECRECY = 0x0020,
+ /* Provides mutual authentication */
+ MECH_SEC_MUTUAL_AUTH = 0x0040,
+ /* Allow NULs in input data */
+ MECH_SEC_ALLOW_NULS = 0x0080,
+};
+
+/* auth failure codes */
+#define AUTH_CLIENT_FAIL_CODE_AUTHZFAILED "authz_fail"
+#define AUTH_CLIENT_FAIL_CODE_TEMPFAIL "temp_fail"
+#define AUTH_CLIENT_FAIL_CODE_USER_DISABLED "user_disabled"
+#define AUTH_CLIENT_FAIL_CODE_PASS_EXPIRED "pass_expired"
+#define AUTH_CLIENT_FAIL_CODE_INVALID_BASE64 "invalid_base64"
+
+/* not actually returned from auth service */
+#define AUTH_CLIENT_FAIL_CODE_MECH_INVALID "auth_mech_invalid"
+#define AUTH_CLIENT_FAIL_CODE_MECH_SSL_REQUIRED "auth_mech_ssl_required"
+#define AUTH_CLIENT_FAIL_CODE_ANONYMOUS_DENIED "anonymous_denied"
+
+#endif
diff --git a/src/lib-auth/auth-client-private.h b/src/lib-auth/auth-client-private.h
new file mode 100644
index 0000000..36b2463
--- /dev/null
+++ b/src/lib-auth/auth-client-private.h
@@ -0,0 +1,88 @@
+#ifndef AUTH_CLIENT_PRIVATE_H
+#define AUTH_CLIENT_PRIVATE_H
+
+#include "connection.h"
+
+#include "auth-client.h"
+
+#define AUTH_CONNECT_TIMEOUT_MSECS (30*1000)
+
+struct auth_client_request {
+ pool_t pool;
+ struct event *event;
+
+ struct auth_client_connection *conn;
+ unsigned int id;
+ time_t created;
+
+ auth_request_callback_t *callback;
+ void *context;
+};
+
+struct auth_client_connection {
+ struct connection conn;
+ pool_t pool;
+
+ struct auth_client *client;
+ time_t last_connect;
+
+ struct timeout *to;
+
+ unsigned int server_pid;
+ unsigned int connect_uid;
+ char *cookie;
+
+ ARRAY(struct auth_mech_desc) available_auth_mechs;
+
+ /* id => request */
+ HASH_TABLE(void *, struct auth_client_request *) requests;
+
+ bool has_plain_mech:1;
+ bool connected:1;
+};
+
+struct auth_client {
+ char *auth_socket_path;
+ unsigned int client_pid;
+ struct event *event;
+
+ struct connection_list *clist;
+ struct auth_client_connection *conn;
+
+ auth_connect_notify_callback_t *connect_notify_callback;
+ void *connect_notify_context;
+
+ unsigned int request_id_counter;
+
+ unsigned int connect_timeout_msecs;
+
+ bool debug:1;
+};
+
+extern struct event_category event_category_auth_client;
+
+bool auth_client_request_is_aborted(struct auth_client_request *request);
+time_t auth_client_request_get_create_time(struct auth_client_request *request);
+
+void auth_client_request_server_input(struct auth_client_request *request,
+ enum auth_request_status status,
+ const char *const *args);
+
+struct connection_list *auth_client_connection_list_init(void);
+
+struct auth_client_connection *
+auth_client_connection_init(struct auth_client *client);
+void auth_client_connection_deinit(struct auth_client_connection **conn);
+
+int auth_client_connection_connect(struct auth_client_connection *conn);
+void auth_client_connection_disconnect(struct auth_client_connection *conn,
+ const char *reason);
+
+/* Queues a new request. Must not be called if connection is not connected. */
+unsigned int
+auth_client_connection_add_request(struct auth_client_connection *conn,
+ struct auth_client_request *request);
+void auth_client_connection_remove_request(struct auth_client_connection *conn,
+ unsigned int id);
+
+#endif
diff --git a/src/lib-auth/auth-client-request.c b/src/lib-auth/auth-client-request.c
new file mode 100644
index 0000000..629b77b
--- /dev/null
+++ b/src/lib-auth/auth-client-request.c
@@ -0,0 +1,359 @@
+/* Copyright (c) 2003-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "str.h"
+#include "strescape.h"
+#include "ostream.h"
+#include "auth-client-private.h"
+
+static void auth_server_send_new_request(struct auth_client_connection *conn,
+ struct auth_client_request *request,
+ const struct auth_request_info *info)
+{
+ string_t *str;
+
+ str = t_str_new(512);
+ str_printfa(str, "AUTH\t%u\t", request->id);
+ str_append_tabescaped(str, info->mech);
+ str_append(str, "\tservice=");
+ str_append_tabescaped(str, info->service);
+
+ event_add_str(request->event, "mechanism", info->mech);
+ event_add_str(request->event, "service", info->service);
+
+ if ((info->flags & AUTH_REQUEST_FLAG_SUPPORT_FINAL_RESP) != 0)
+ str_append(str, "\tfinal-resp-ok");
+ if ((info->flags & AUTH_REQUEST_FLAG_SECURED) != 0) {
+ str_append(str, "\tsecured");
+ if ((info->flags & AUTH_REQUEST_FLAG_TRANSPORT_SECURITY_TLS) != 0) {
+ str_append(str, "=tls");
+ event_add_str(request->event, "transport", "TLS");
+ } else {
+ event_add_str(request->event, "transport", "trusted");
+ }
+ } else {
+ i_assert((info->flags & AUTH_REQUEST_FLAG_TRANSPORT_SECURITY_TLS) == 0);
+ event_add_str(request->event, "transport", "insecure");
+ }
+ if ((info->flags & AUTH_REQUEST_FLAG_NO_PENALTY) != 0)
+ str_append(str, "\tno-penalty");
+ if ((info->flags & AUTH_REQUEST_FLAG_VALID_CLIENT_CERT) != 0)
+ str_append(str, "\tvalid-client-cert");
+ if ((info->flags & AUTH_REQUEST_FLAG_DEBUG) != 0)
+ str_append(str, "\tdebug");
+
+ if (info->session_id != NULL) {
+ str_append(str, "\tsession=");
+ str_append_tabescaped(str, info->session_id);
+ event_add_str(request->event, "session", info->session_id);
+ }
+ if (info->cert_username != NULL) {
+ str_append(str, "\tcert_username=");
+ str_append_tabescaped(str, info->cert_username);
+ event_add_str(request->event, "certificate_user",
+ info->cert_username);
+ }
+ if (info->local_ip.family != 0) {
+ str_printfa(str, "\tlip=%s", net_ip2addr(&info->local_ip));
+ event_add_str(request->event, "local_ip", net_ip2addr(&info->local_ip));
+ }
+ if (info->remote_ip.family != 0) {
+ str_printfa(str, "\trip=%s", net_ip2addr(&info->remote_ip));
+ event_add_str(request->event, "remote_ip", net_ip2addr(&info->remote_ip));
+ }
+ if (info->local_port != 0) {
+ str_printfa(str, "\tlport=%u", info->local_port);
+ event_add_int(request->event, "local_port", info->local_port);
+ }
+ if (info->remote_port != 0) {
+ str_printfa(str, "\trport=%u", info->remote_port);
+ event_add_int(request->event, "remote_port", info->remote_port);
+ }
+ if (info->real_local_ip.family != 0) {
+ event_add_str(request->event, "real_local_ip",
+ net_ip2addr(&info->real_local_ip));
+ }
+ if (info->real_remote_ip.family != 0) {
+ event_add_str(request->event, "real_remote_ip",
+ net_ip2addr(&info->real_remote_ip));
+ }
+ if (info->real_local_port != 0) {
+ event_add_int(request->event, "real_local_port",
+ info->real_local_port);
+ }
+ if (info->real_remote_port != 0) {
+ event_add_int(request->event, "real_remote_port",
+ info->real_remote_port);
+ }
+ /* send the real_* variants only when they differ from the unreal
+ ones */
+ if (info->real_local_ip.family != 0 &&
+ !net_ip_compare(&info->real_local_ip, &info->local_ip)) {
+ str_printfa(str, "\treal_lip=%s",
+ net_ip2addr(&info->real_local_ip));
+ }
+ if (info->real_remote_ip.family != 0 &&
+ !net_ip_compare(&info->real_remote_ip, &info->remote_ip)) {
+ str_printfa(str, "\treal_rip=%s",
+ net_ip2addr(&info->real_remote_ip));
+ }
+ if (info->real_local_port != 0 &&
+ info->real_local_port != info->local_port)
+ str_printfa(str, "\treal_lport=%u", info->real_local_port);
+ if (info->real_remote_port != 0 &&
+ info->real_remote_port != info->remote_port)
+ str_printfa(str, "\treal_rport=%u", info->real_remote_port);
+ if (info->local_name != NULL &&
+ *info->local_name != '\0') {
+ str_append(str, "\tlocal_name=");
+ str_append_tabescaped(str, info->local_name);
+ event_add_str(request->event, "local_name", info->local_name);
+ }
+ if (info->ssl_cipher_bits != 0 && info->ssl_cipher != NULL) {
+ event_add_str(request->event, "tls_cipher", info->ssl_cipher);
+ event_add_int(request->event, "tls_cipher_bits", info->ssl_cipher_bits);
+ if (info->ssl_pfs != NULL) {
+ event_add_str(request->event, "tls_pfs", info->ssl_pfs);
+ }
+ }
+ if (info->ssl_protocol != NULL) {
+ event_add_str(request->event, "tls_protocol", info->ssl_protocol);
+ }
+ if (info->client_id != NULL &&
+ *info->client_id != '\0') {
+ str_append(str, "\tclient_id=");
+ str_append_tabescaped(str, info->client_id);
+ event_add_str(request->event, "client_id", info->client_id);
+ }
+ if (info->forward_fields != NULL &&
+ *info->forward_fields != '\0') {
+ str_append(str, "\tforward_fields=");
+ str_append_tabescaped(str, info->forward_fields);
+ }
+ if (array_is_created(&info->extra_fields)) {
+ const char *const *fieldp;
+ array_foreach(&info->extra_fields, fieldp) {
+ str_append_c(str, '\t');
+ str_append_tabescaped(str, *fieldp);
+ }
+ }
+ if (info->initial_resp_base64 != NULL) {
+ str_append(str, "\tresp=");
+ str_append_tabescaped(str, info->initial_resp_base64);
+ }
+ str_append_c(str, '\n');
+
+ struct event_passthrough *e =
+ event_create_passthrough(request->event)->
+ set_name("auth_client_request_started");
+ e_debug(e->event(), "Started request");
+
+ if (o_stream_send(conn->conn.output, str_data(str), str_len(str)) < 0) {
+ e_error(request->event,
+ "Error sending request to auth server: %m");
+ }
+}
+
+struct auth_client_request *
+auth_client_request_new(struct auth_client *client,
+ const struct auth_request_info *request_info,
+ auth_request_callback_t *callback, void *context)
+{
+ struct auth_client_request *request;
+ pool_t pool;
+
+ pool = pool_alloconly_create("auth client request", 512);
+ request = p_new(pool, struct auth_client_request, 1);
+ request->pool = pool;
+ request->conn = client->conn;
+
+ request->callback = callback;
+ request->context = context;
+
+ request->id =
+ auth_client_connection_add_request(request->conn, request);
+ request->created = ioloop_time;
+
+ request->event = event_create(client->event);
+ event_add_int(request->event, "id", request->id);
+ event_set_append_log_prefix(request->event,
+ t_strdup_printf("request [%u]: ",
+ request->id));
+
+ T_BEGIN {
+ auth_server_send_new_request(request->conn, request, request_info);
+ } T_END;
+ return request;
+}
+
+void auth_client_request_continue(struct auth_client_request *request,
+ const char *data_base64)
+{
+ struct const_iovec iov[3];
+ const char *prefix;
+
+ prefix = t_strdup_printf("CONT\t%u\t", request->id);
+
+ iov[0].iov_base = prefix;
+ iov[0].iov_len = strlen(prefix);
+ iov[1].iov_base = data_base64;
+ iov[1].iov_len = strlen(data_base64);
+ iov[2].iov_base = "\n";
+ iov[2].iov_len = 1;
+
+ struct event_passthrough *e =
+ event_create_passthrough(request->event)->
+ set_name("auth_client_request_continued");
+ e_debug(e->event(), "Continue request");
+
+ if (o_stream_sendv(request->conn->conn.output, iov, 3) < 0) {
+ e_error(request->event,
+ "Error sending continue request to auth server: %m");
+ }
+}
+
+static void ATTR_NULL(3, 4)
+call_callback(struct auth_client_request *request,
+ enum auth_request_status status,
+ const char *data_base64,
+ const char *const *args)
+{
+ auth_request_callback_t *callback = request->callback;
+
+ if (status != AUTH_REQUEST_STATUS_CONTINUE)
+ request->callback = NULL;
+ callback(request, status, data_base64, args, request->context);
+}
+
+static void auth_client_request_free(struct auth_client_request **_request)
+{
+ struct auth_client_request *request = *_request;
+
+ *_request = NULL;
+
+ event_unref(&request->event);
+ pool_unref(&request->pool);
+}
+
+void auth_client_request_abort(struct auth_client_request **_request,
+ const char *reason)
+{
+ struct auth_client_request *request = *_request;
+
+ *_request = NULL;
+
+ struct event_passthrough *e =
+ event_create_passthrough(request->event)->
+ set_name("auth_client_request_finished");
+ e->add_str("error", reason);
+ e_debug(e->event(), "Aborted: %s", reason);
+
+ auth_client_send_cancel(request->conn->client, request->id);
+ call_callback(request, AUTH_REQUEST_STATUS_ABORT, NULL, NULL);
+ /* remove the request */
+ auth_client_connection_remove_request(request->conn, request->id);
+ auth_client_request_free(&request);
+}
+
+unsigned int auth_client_request_get_id(struct auth_client_request *request)
+{
+ return request->id;
+}
+
+unsigned int
+auth_client_request_get_server_pid(struct auth_client_request *request)
+{
+ return request->conn->server_pid;
+}
+
+const char *auth_client_request_get_cookie(struct auth_client_request *request)
+{
+ return request->conn->cookie;
+}
+
+bool auth_client_request_is_aborted(struct auth_client_request *request)
+{
+ return request->callback == NULL;
+}
+
+time_t auth_client_request_get_create_time(struct auth_client_request *request)
+{
+ return request->created;
+}
+
+static void args_parse_user(struct auth_client_request *request, const char *arg)
+{
+ if (str_begins(arg, "user="))
+ event_add_str(request->event, "user", arg + 5);
+ else if (str_begins(arg, "original_user="))
+ event_add_str(request->event, "original_user", arg + 14);
+ else if (str_begins(arg, "auth_user="))
+ event_add_str(request->event, "auth_user", arg + 10);
+}
+
+void auth_client_request_server_input(struct auth_client_request *request,
+ enum auth_request_status status,
+ const char *const *args)
+{
+ const char *const *tmp, *base64_data = NULL;
+ struct event_passthrough *e;
+
+ if (request->callback == NULL) {
+ /* aborted already */
+ return;
+ }
+
+ switch (status) {
+ case AUTH_REQUEST_STATUS_CONTINUE:
+ e = event_create_passthrough(request->event)->
+ set_name("auth_client_request_challenged");
+ break;
+ default:
+ e = event_create_passthrough(request->event)->
+ set_name("auth_client_request_finished");
+ break;
+ }
+
+ for (tmp = args; *tmp != NULL; tmp++) {
+ if (str_begins(*tmp, "resp=")) {
+ base64_data = *tmp + 5;
+ }
+ args_parse_user(request, *tmp);
+ }
+
+ switch (status) {
+ case AUTH_REQUEST_STATUS_OK:
+ e_debug(e->event(), "Finished");
+ break;
+ case AUTH_REQUEST_STATUS_CONTINUE:
+ base64_data = args[0];
+ args = NULL;
+ e_debug(e->event(), "Got challenge");
+ break;
+ case AUTH_REQUEST_STATUS_FAIL:
+ e->add_str("error", "Authentication failed");
+ e_debug(e->event(), "Finished");
+ break;
+ case AUTH_REQUEST_STATUS_INTERNAL_FAIL:
+ e->add_str("error", "Internal failure");
+ e_debug(e->event(), "Finished");
+ break;
+ case AUTH_REQUEST_STATUS_ABORT:
+ i_unreached();
+ }
+
+ call_callback(request, status, base64_data, args);
+ if (status != AUTH_REQUEST_STATUS_CONTINUE)
+ auth_client_request_free(&request);
+}
+
+void auth_client_send_cancel(struct auth_client *client, unsigned int id)
+{
+ const char *str = t_strdup_printf("CANCEL\t%u\n", id);
+
+ if (o_stream_send_str(client->conn->conn.output, str) < 0) {
+ e_error(client->conn->conn.event,
+ "Error sending request to auth server: %m");
+ }
+}
diff --git a/src/lib-auth/auth-client.c b/src/lib-auth/auth-client.c
new file mode 100644
index 0000000..9523282
--- /dev/null
+++ b/src/lib-auth/auth-client.c
@@ -0,0 +1,112 @@
+/* Copyright (c) 2005-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "auth-client-private.h"
+
+struct event_category event_category_auth_client = {
+ .name = "auth-client"
+};
+
+struct auth_client *
+auth_client_init(const char *auth_socket_path, unsigned int client_pid,
+ bool debug)
+{
+ struct auth_client *client;
+
+ client = i_new(struct auth_client, 1);
+ client->client_pid = client_pid;
+ client->auth_socket_path = i_strdup(auth_socket_path);
+ client->debug = debug;
+ client->connect_timeout_msecs = AUTH_CONNECT_TIMEOUT_MSECS;
+ client->clist = auth_client_connection_list_init();
+
+ client->event = event_create(NULL);
+ event_add_category(client->event, &event_category_auth_client);
+ event_set_append_log_prefix(client->event, "auth-client: ");
+ event_set_forced_debug(client->event, client->debug);
+
+ client->conn = auth_client_connection_init(client);
+ return client;
+}
+
+void auth_client_deinit(struct auth_client **_client)
+{
+ struct auth_client *client = *_client;
+
+ *_client = NULL;
+
+ auth_client_connection_deinit(&client->conn);
+ connection_list_deinit(&client->clist);
+ event_unref(&client->event);
+ i_free(client->auth_socket_path);
+ i_free(client);
+}
+
+void auth_client_connect(struct auth_client *client)
+{
+ if (!client->conn->connected)
+ (void)auth_client_connection_connect(client->conn);
+}
+
+void auth_client_disconnect(struct auth_client *client, const char *reason)
+{
+ auth_client_connection_disconnect(client->conn, reason);
+}
+
+bool auth_client_is_connected(struct auth_client *client)
+{
+ /* handshake_received isn't unset immediately after disconnection */
+ return client->conn->conn.handshake_received &&
+ client->conn->connected;
+}
+
+bool auth_client_is_disconnected(struct auth_client *client)
+{
+ return !client->conn->connected;
+}
+
+void auth_client_set_connect_timeout(struct auth_client *client,
+ unsigned int msecs)
+{
+ client->connect_timeout_msecs = msecs;
+}
+
+void auth_client_set_connect_notify(struct auth_client *client,
+ auth_connect_notify_callback_t *callback,
+ void *context)
+{
+ client->connect_notify_callback = callback;
+ client->connect_notify_context = context;
+}
+
+const struct auth_mech_desc *
+auth_client_get_available_mechs(struct auth_client *client,
+ unsigned int *mech_count)
+{
+ i_assert(auth_client_is_connected(client));
+
+ return array_get(&client->conn->available_auth_mechs, mech_count);
+}
+
+const struct auth_mech_desc *
+auth_client_find_mech(struct auth_client *client, const char *name)
+{
+ const struct auth_mech_desc *mech;
+
+ array_foreach(&client->conn->available_auth_mechs, mech) {
+ if (strcasecmp(mech->name, name) == 0)
+ return mech;
+ }
+ return NULL;
+}
+
+void auth_client_get_connect_id(struct auth_client *client,
+ unsigned int *server_pid_r,
+ unsigned int *connect_uid_r)
+{
+ i_assert(auth_client_is_connected(client));
+
+ *server_pid_r = client->conn->server_pid;
+ *connect_uid_r = client->conn->connect_uid;
+}
diff --git a/src/lib-auth/auth-client.h b/src/lib-auth/auth-client.h
new file mode 100644
index 0000000..917f904
--- /dev/null
+++ b/src/lib-auth/auth-client.h
@@ -0,0 +1,124 @@
+#ifndef AUTH_CLIENT_H
+#define AUTH_CLIENT_H
+
+#include "net.h"
+#include "auth-client-interface.h"
+
+struct auth_client;
+struct auth_client_request;
+
+enum auth_request_flags {
+ AUTH_REQUEST_FLAG_SECURED = 0x01,
+ AUTH_REQUEST_FLAG_VALID_CLIENT_CERT = 0x02,
+ /* Skip penalty checks for this request */
+ AUTH_REQUEST_FLAG_NO_PENALTY = 0x04,
+ /* Support final SASL response */
+ AUTH_REQUEST_FLAG_SUPPORT_FINAL_RESP = 0x08,
+ /* Enable auth_debug=yes logging for this request */
+ AUTH_REQUEST_FLAG_DEBUG = 0x10,
+ /* If TLS was used */
+ AUTH_REQUEST_FLAG_TRANSPORT_SECURITY_TLS = 0x20,
+};
+
+enum auth_request_status {
+ AUTH_REQUEST_STATUS_ABORT = -3,
+ AUTH_REQUEST_STATUS_INTERNAL_FAIL = -2,
+ AUTH_REQUEST_STATUS_FAIL = -1,
+ AUTH_REQUEST_STATUS_CONTINUE,
+ AUTH_REQUEST_STATUS_OK
+};
+
+struct auth_mech_desc {
+ char *name;
+ enum mech_security_flags flags;
+};
+
+struct auth_connect_id {
+ unsigned int server_pid;
+ unsigned int connect_uid;
+};
+
+struct auth_request_info {
+ const char *mech;
+ const char *service;
+ const char *session_id;
+ const char *cert_username;
+ const char *local_name;
+ const char *client_id;
+ const char *forward_fields;
+ ARRAY_TYPE(const_string) extra_fields;
+
+ unsigned int ssl_cipher_bits;
+ const char *ssl_cipher;
+ const char *ssl_pfs;
+ const char *ssl_protocol;
+
+ enum auth_request_flags flags;
+
+ struct ip_addr local_ip, remote_ip, real_local_ip, real_remote_ip;
+ in_port_t local_port, remote_port, real_local_port, real_remote_port;
+
+ const char *initial_resp_base64;
+};
+
+typedef void auth_request_callback_t(struct auth_client_request *request,
+ enum auth_request_status status,
+ const char *data_base64,
+ const char *const *args, void *context);
+
+typedef void auth_connect_notify_callback_t(struct auth_client *client,
+ bool connected, void *context);
+
+/* Create new authentication client. */
+struct auth_client *
+auth_client_init(const char *auth_socket_path, unsigned int client_pid,
+ bool debug);
+void auth_client_deinit(struct auth_client **client);
+
+void auth_client_connect(struct auth_client *client);
+void auth_client_disconnect(struct auth_client *client, const char *reason);
+bool auth_client_is_connected(struct auth_client *client);
+bool auth_client_is_disconnected(struct auth_client *client);
+
+void auth_client_set_connect_timeout(struct auth_client *client,
+ unsigned int msecs);
+void auth_client_set_connect_notify(struct auth_client *client,
+ auth_connect_notify_callback_t *callback,
+ void *context) ATTR_NULL(2, 3);
+const struct auth_mech_desc *
+auth_client_get_available_mechs(struct auth_client *client,
+ unsigned int *mech_count);
+const struct auth_mech_desc *
+auth_client_find_mech(struct auth_client *client, const char *name);
+
+/* Return current connection's identifiers. */
+void auth_client_get_connect_id(struct auth_client *client,
+ unsigned int *server_pid_r,
+ unsigned int *connect_uid_r);
+
+/* Create a new authentication request. callback is called whenever something
+ happens for the request. */
+struct auth_client_request *
+auth_client_request_new(struct auth_client *client,
+ const struct auth_request_info *request_info,
+ auth_request_callback_t *callback, void *context)
+ ATTR_NULL(4);
+/* Continue authentication. Call when
+ reply->result == AUTH_CLIENT_REQUEST_CONTINUE */
+void auth_client_request_continue(struct auth_client_request *request,
+ const char *data_base64);
+/* Abort ongoing authentication request. */
+void auth_client_request_abort(struct auth_client_request **request,
+ const char *reason) ATTR_NULL(2);
+/* Return ID of this request. */
+unsigned int auth_client_request_get_id(struct auth_client_request *request);
+/* Return the PID of the server that handled this request. */
+unsigned int
+auth_client_request_get_server_pid(struct auth_client_request *request);
+/* Return cookie of the server that handled this request. */
+const char *auth_client_request_get_cookie(struct auth_client_request *request);
+
+/* Tell auth process to drop specified request from memory */
+void auth_client_send_cancel(struct auth_client *client, unsigned int id);
+
+#endif
diff --git a/src/lib-auth/auth-master.c b/src/lib-auth/auth-master.c
new file mode 100644
index 0000000..57cf8d2
--- /dev/null
+++ b/src/lib-auth/auth-master.c
@@ -0,0 +1,1030 @@
+/* Copyright (c) 2005-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "lib-signals.h"
+#include "array.h"
+#include "ioloop.h"
+#include "eacces-error.h"
+#include "net.h"
+#include "istream.h"
+#include "ostream.h"
+#include "str.h"
+#include "strescape.h"
+#include "connection.h"
+#include "master-interface.h"
+#include "auth-client-private.h"
+#include "auth-master.h"
+
+#include <unistd.h>
+
+#define AUTH_PROTOCOL_MAJOR 1
+#define AUTH_PROTOCOL_MINOR 0
+
+#define AUTH_MASTER_IDLE_SECS 60
+
+#define MAX_INBUF_SIZE 8192
+#define MAX_OUTBUF_SIZE 1024
+
+struct auth_master_connection {
+ struct connection conn;
+ struct connection_list *clist;
+ struct event *event_parent, *event;
+
+ char *auth_socket_path;
+ enum auth_master_flags flags;
+
+ struct ioloop *ioloop, *prev_ioloop;
+ struct timeout *to;
+
+ unsigned int request_counter;
+
+ bool (*reply_callback)(const char *cmd, const char *const *args,
+ void *context);
+ void *reply_context;
+
+ unsigned int timeout_msecs;
+
+ bool connected:1;
+ bool sent_handshake:1;
+ bool aborted:1;
+};
+
+struct auth_master_lookup_ctx {
+ struct auth_master_connection *conn;
+ const char *user;
+ const char *expected_reply;
+ int return_value;
+
+ pool_t pool;
+ const char **fields;
+};
+
+struct auth_master_user_list_ctx {
+ struct auth_master_connection *conn;
+ string_t *username;
+ bool finished;
+ bool failed;
+};
+
+static void auth_master_connected(struct connection *_conn, bool success);
+static int
+auth_master_input_args(struct connection *_conn, const char *const *args);
+static int
+auth_master_handshake_line(struct connection *_conn, const char *line);
+static int auth_master_input_line(struct connection *_conn, const char *line);
+static void auth_master_destroy(struct connection *_conn);
+
+static const struct connection_vfuncs auth_master_vfuncs = {
+ .destroy = auth_master_destroy,
+ .handshake_line = auth_master_handshake_line,
+ .input_args = auth_master_input_args,
+ .input_line = auth_master_input_line,
+ .client_connected = auth_master_connected,
+};
+
+static const struct connection_settings auth_master_set = {
+ .dont_send_version = TRUE,
+ .service_name_in = "auth-master",
+ .service_name_out = "auth-master",
+ .major_version = AUTH_PROTOCOL_MAJOR,
+ .minor_version = AUTH_PROTOCOL_MINOR,
+ .unix_client_connect_msecs = 1000,
+ .input_max_size = MAX_INBUF_SIZE,
+ .output_max_size = MAX_OUTBUF_SIZE,
+ .client = TRUE,
+};
+
+struct auth_master_connection *
+auth_master_init(const char *auth_socket_path, enum auth_master_flags flags)
+{
+ struct auth_master_connection *conn;
+
+ conn = i_new(struct auth_master_connection, 1);
+ conn->auth_socket_path = i_strdup(auth_socket_path);
+ conn->flags = flags;
+ conn->timeout_msecs = 1000*MASTER_AUTH_LOOKUP_TIMEOUT_SECS;
+ conn->clist = connection_list_init(&auth_master_set,
+ &auth_master_vfuncs);
+
+ conn->event_parent = conn->event = event_create(NULL);
+ event_add_category(conn->event_parent, &event_category_auth_client);
+ event_set_append_log_prefix(conn->event_parent, "auth-master: ");
+ event_set_forced_debug(conn->event_parent,
+ HAS_ALL_BITS(flags, AUTH_MASTER_FLAG_DEBUG));
+
+ conn->conn.event_parent = conn->event_parent;
+ connection_init_client_unix(conn->clist, &conn->conn,
+ conn->auth_socket_path);
+
+ return conn;
+}
+
+static void auth_connection_close(struct auth_master_connection *conn)
+{
+ conn->connected = FALSE;
+ connection_disconnect(&conn->conn);
+
+ timeout_remove(&conn->to);
+
+ conn->sent_handshake = FALSE;
+}
+
+void auth_master_deinit(struct auth_master_connection **_conn)
+{
+ struct auth_master_connection *conn = *_conn;
+ struct connection_list *clist = conn->clist;
+
+ *_conn = NULL;
+
+ auth_connection_close(conn);
+ connection_deinit(&conn->conn);
+ connection_list_deinit(&clist);
+ event_unref(&conn->event_parent);
+ i_free(conn->auth_socket_path);
+ i_free(conn);
+}
+
+void auth_master_set_timeout(struct auth_master_connection *conn,
+ unsigned int msecs)
+{
+ conn->timeout_msecs = msecs;
+}
+
+const char *auth_master_get_socket_path(struct auth_master_connection *conn)
+{
+ return conn->auth_socket_path;
+}
+
+static void auth_request_lookup_abort(struct auth_master_connection *conn)
+{
+ io_loop_stop(conn->ioloop);
+ conn->aborted = TRUE;
+}
+
+static void auth_master_destroy(struct connection *_conn)
+{
+ struct auth_master_connection *conn =
+ container_of(_conn, struct auth_master_connection, conn);
+
+ if (conn->connected)
+ connection_disconnect(&conn->conn);
+ conn->connected = FALSE;
+ conn->sent_handshake = FALSE;
+
+ switch (_conn->disconnect_reason) {
+ case CONNECTION_DISCONNECT_HANDSHAKE_FAILED:
+ break;
+ case CONNECTION_DISCONNECT_BUFFER_FULL:
+ e_error(conn->event, "BUG: Received more than %d bytes",
+ MAX_INBUF_SIZE);
+ break;
+ default:
+ if (!conn->aborted)
+ e_error(conn->event, "Disconnected unexpectedly");
+ }
+ auth_request_lookup_abort(conn);
+}
+
+static int
+auth_master_handshake_line(struct connection *_conn, const char *line)
+{
+ struct auth_master_connection *conn =
+ container_of(_conn, struct auth_master_connection, conn);
+ const char *const *tmp;
+ unsigned int major_version, minor_version;
+
+ tmp = t_strsplit_tabescaped(line);
+ if (strcmp(tmp[0], "VERSION") == 0 &&
+ tmp[1] != NULL && tmp[2] != NULL) {
+ if (str_to_uint(tmp[1], &major_version) < 0 ||
+ str_to_uint(tmp[2], &minor_version) < 0) {
+ e_error(conn->event,
+ "Auth server sent invalid version line: %s",
+ line);
+ auth_request_lookup_abort(conn);
+ return -1;
+ }
+
+ if (connection_verify_version(_conn, "auth-master",
+ major_version,
+ minor_version) < 0) {
+ auth_request_lookup_abort(conn);
+ return -1;
+ }
+ } else if (strcmp(tmp[0], "SPID") == 0) {
+ return 1;
+ }
+ return 0;
+}
+
+static int
+parse_reply(struct auth_master_lookup_ctx *ctx, const char *cmd,
+ const char *const *args)
+{
+ struct auth_master_connection *conn = ctx->conn;
+
+ if (strcmp(cmd, ctx->expected_reply) == 0)
+ return 1;
+ if (strcmp(cmd, "NOTFOUND") == 0)
+ return 0;
+ if (strcmp(cmd, "FAIL") == 0) {
+ if (*args == NULL) {
+ e_error(conn->event, "Auth %s lookup failed",
+ ctx->expected_reply);
+ } else {
+ e_debug(conn->event,
+ "Auth %s lookup returned temporary failure: %s",
+ ctx->expected_reply, *args);
+ }
+ return -2;
+ }
+ e_error(conn->event, "Unknown reply: %s", cmd);
+ return -1;
+}
+
+static const char *const *args_hide_passwords(const char *const *args)
+{
+ ARRAY_TYPE(const_string) new_args;
+ const char *p, *p2;
+ unsigned int i;
+
+ /* if there are any keys that contain "pass" string */
+ for (i = 0; args[i] != NULL; i++) {
+ p = strstr(args[i], "pass");
+ if (p != NULL && p < strchr(args[i], '='))
+ break;
+ }
+ if (args[i] == NULL)
+ return args;
+
+ /* there are. replace their values with <hidden> */
+ t_array_init(&new_args, i + 16);
+ array_append(&new_args, args, i);
+ for (; args[i] != NULL; i++) {
+ p = strstr(args[i], "pass");
+ p2 = strchr(args[i], '=');
+ if (p != NULL && p < p2) {
+ p = t_strconcat(t_strdup_until(args[i], p2),
+ "=<hidden>", NULL);
+ array_push_back(&new_args, &p);
+ } else {
+ array_push_back(&new_args, &args[i]);
+ }
+ }
+ array_append_zero(&new_args);
+ return array_front(&new_args);
+}
+
+static bool auth_lookup_reply_callback(const char *cmd, const char *const *args,
+ void *context)
+{
+ struct auth_master_lookup_ctx *ctx = context;
+ unsigned int i, len;
+
+ io_loop_stop(ctx->conn->ioloop);
+
+ ctx->return_value = parse_reply(ctx, cmd, args);
+
+ len = str_array_length(args);
+ i_assert(*args != NULL || len == 0); /* for static analyzer */
+ if (ctx->return_value >= 0) {
+ ctx->fields = p_new(ctx->pool, const char *, len + 1);
+ for (i = 0; i < len; i++)
+ ctx->fields[i] = p_strdup(ctx->pool, args[i]);
+ } else {
+ /* put the reason string into first field */
+ ctx->fields = p_new(ctx->pool, const char *, 2);
+ for (i = 0; i < len; i++) {
+ if (str_begins(args[i], "reason=")) {
+ ctx->fields[0] =
+ p_strdup(ctx->pool, args[i] + 7);
+ break;
+ }
+ }
+ }
+ args = args_hide_passwords(args);
+ e_debug(ctx->conn->event, "auth %s input: %s",
+ ctx->expected_reply, t_strarray_join(args, " "));
+ return TRUE;
+}
+
+static int
+auth_master_input_args(struct connection *_conn, const char *const *args)
+{
+ struct auth_master_connection *conn =
+ container_of(_conn, struct auth_master_connection, conn);
+ const char *const *in_args = args;
+ const char *cmd, *id, *wanted_id;
+
+ cmd = *args; args++;
+ if (*args == NULL)
+ id = "";
+ else {
+ id = *args;
+ args++;
+ }
+
+ wanted_id = dec2str(conn->request_counter);
+ if (strcmp(id, wanted_id) == 0) {
+ return (conn->reply_callback(cmd, args, conn->reply_context) ?
+ 0 : 1);
+ }
+
+ if (strcmp(cmd, "CUID") == 0) {
+ e_error(conn->event, "%s is an auth client socket. "
+ "It should be a master socket.",
+ conn->auth_socket_path);
+ } else {
+ e_error(conn->event, "BUG: Unexpected input: %s",
+ t_strarray_join(in_args, "\t"));
+ }
+ auth_request_lookup_abort(conn);
+ return -1;
+}
+
+static int auth_master_input_line(struct connection *_conn, const char *line)
+{
+ struct auth_master_connection *conn =
+ container_of(_conn, struct auth_master_connection, conn);
+ int ret;
+
+ ret = connection_input_line_default(_conn, line);
+ return (io_loop_is_running(conn->ioloop) ? ret : 0);
+}
+
+static void auth_master_connected(struct connection *_conn, bool success)
+{
+ struct auth_master_connection *conn =
+ container_of(_conn, struct auth_master_connection, conn);
+
+ /* Cannot get here unless connect() was successful */
+ i_assert(success);
+
+ conn->connected = TRUE;
+}
+
+static int auth_master_connect(struct auth_master_connection *conn)
+{
+ i_assert(!conn->connected);
+
+ if (conn->ioloop != NULL)
+ connection_switch_ioloop_to(&conn->conn, conn->ioloop);
+ if (connection_client_connect(&conn->conn) < 0) {
+ if (errno == EACCES) {
+ e_error(conn->event,
+ "%s", eacces_error_get("connect",
+ conn->auth_socket_path));
+ } else {
+ e_error(conn->event, "connect(%s) failed: %m",
+ conn->auth_socket_path);
+ }
+ return -1;
+ }
+
+ connection_input_halt(&conn->conn);
+ return 0;
+}
+
+static void auth_request_timeout(struct auth_master_connection *conn)
+{
+ if (!conn->conn.handshake_received)
+ e_error(conn->event, "Connecting timed out");
+ else
+ e_error(conn->event, "Request timed out");
+ auth_request_lookup_abort(conn);
+}
+
+static void auth_idle_timeout(struct auth_master_connection *conn)
+{
+ auth_connection_close(conn);
+}
+
+static void auth_master_set_io(struct auth_master_connection *conn)
+{
+ if (conn->ioloop != NULL)
+ return;
+
+ timeout_remove(&conn->to);
+
+ conn->prev_ioloop = current_ioloop;
+ conn->ioloop = io_loop_create();
+ connection_switch_ioloop_to(&conn->conn, conn->ioloop);
+ if (conn->connected)
+ connection_input_resume(&conn->conn);
+
+ conn->to = timeout_add_to(conn->ioloop, conn->timeout_msecs,
+ auth_request_timeout, conn);
+}
+
+static void auth_master_unset_io(struct auth_master_connection *conn)
+{
+ if (conn->prev_ioloop != NULL) {
+ io_loop_set_current(conn->prev_ioloop);
+ }
+ if (conn->ioloop != NULL) {
+ io_loop_set_current(conn->ioloop);
+ connection_switch_ioloop_to(&conn->conn, conn->ioloop);
+ connection_input_halt(&conn->conn);
+ timeout_remove(&conn->to);
+ io_loop_destroy(&conn->ioloop);
+ }
+
+ if ((conn->flags & AUTH_MASTER_FLAG_NO_IDLE_TIMEOUT) == 0) {
+ if (conn->prev_ioloop == NULL)
+ auth_connection_close(conn);
+ else {
+ i_assert(conn->to == NULL);
+ conn->to = timeout_add(1000*AUTH_MASTER_IDLE_SECS,
+ auth_idle_timeout, conn);
+ }
+ }
+}
+
+static bool is_valid_string(const char *str)
+{
+ const char *p;
+
+ /* make sure we're not sending any characters that have a special
+ meaning. */
+ for (p = str; *p != '\0'; p++) {
+ if (*p == '\t' || *p == '\n' || *p == '\r')
+ return FALSE;
+ }
+ return TRUE;
+}
+
+static int auth_master_run_cmd_pre(struct auth_master_connection *conn,
+ const char *cmd)
+{
+ auth_master_set_io(conn);
+
+ if (!conn->connected) {
+ if (auth_master_connect(conn) < 0) {
+ auth_master_unset_io(conn);
+ return -1;
+ }
+ i_assert(conn->connected);
+ connection_input_resume(&conn->conn);
+ }
+
+ o_stream_cork(conn->conn.output);
+ if (!conn->sent_handshake) {
+ const struct connection_settings *set = &conn->conn.list->set;
+
+ o_stream_nsend_str(conn->conn.output,
+ t_strdup_printf("VERSION\t%u\t%u\n",
+ set->major_version,
+ set->minor_version));
+ conn->sent_handshake = TRUE;
+ }
+
+ o_stream_nsend_str(conn->conn.output, cmd);
+ o_stream_uncork(conn->conn.output);
+
+ if (o_stream_flush(conn->conn.output) < 0) {
+ e_error(conn->event, "write(auth socket) failed: %s",
+ o_stream_get_error(conn->conn.output));
+ auth_master_unset_io(conn);
+ auth_connection_close(conn);
+ return -1;
+ }
+ return 0;
+}
+
+static int auth_master_run_cmd_post(struct auth_master_connection *conn)
+{
+ auth_master_unset_io(conn);
+ if (conn->aborted) {
+ conn->aborted = FALSE;
+ auth_connection_close(conn);
+ return -1;
+ }
+ return 0;
+}
+
+static int auth_master_run_cmd(struct auth_master_connection *conn,
+ const char *cmd)
+{
+ if (auth_master_run_cmd_pre(conn, cmd) < 0)
+ return -1;
+ io_loop_run(conn->ioloop);
+ return auth_master_run_cmd_post(conn);
+}
+
+static unsigned int
+auth_master_next_request_id(struct auth_master_connection *conn)
+{
+ if (++conn->request_counter == 0) {
+ /* avoid zero */
+ conn->request_counter++;
+ }
+ return conn->request_counter;
+}
+
+void auth_user_info_export(string_t *str, const struct auth_user_info *info)
+{
+ const char *const *fieldp;
+
+ if (info->service != NULL) {
+ str_append(str, "\tservice=");
+ str_append(str, info->service);
+ }
+ if (info->session_id != NULL) {
+ str_append(str, "\tsession=");
+ str_append_tabescaped(str, info->session_id);
+ }
+ if (info->local_name != NULL) {
+ str_append(str, "\tlocal_name=");
+ str_append_tabescaped(str, info->local_name);
+ }
+ if (info->local_ip.family != 0)
+ str_printfa(str, "\tlip=%s", net_ip2addr(&info->local_ip));
+ if (info->local_port != 0)
+ str_printfa(str, "\tlport=%d", info->local_port);
+ if (info->remote_ip.family != 0)
+ str_printfa(str, "\trip=%s", net_ip2addr(&info->remote_ip));
+ if (info->remote_port != 0)
+ str_printfa(str, "\trport=%d", info->remote_port);
+ if (info->real_remote_ip.family != 0 &&
+ !net_ip_compare(&info->real_remote_ip, &info->remote_ip))
+ str_printfa(str, "\treal_rip=%s", net_ip2addr(&info->real_remote_ip));
+ if (info->real_local_ip.family != 0 &&
+ !net_ip_compare(&info->real_local_ip, &info->local_ip))
+ str_printfa(str, "\treal_lip=%s", net_ip2addr(&info->real_local_ip));
+ if (info->real_local_port != 0 &&
+ info->real_local_port != info->local_port)
+ str_printfa(str, "\treal_lport=%d", info->real_local_port);
+ if (info->real_remote_port != 0 &&
+ info->real_remote_port != info->remote_port)
+ str_printfa(str, "\treal_rport=%d", info->real_remote_port);
+ if (info->debug)
+ str_append(str, "\tdebug");
+ if (info->forward_fields != NULL &&
+ *info->forward_fields != '\0') {
+ str_append(str, "\tforward_fields=");
+ str_append_tabescaped(str, info->forward_fields);
+ }
+ if (array_is_created(&info->extra_fields)) {
+ array_foreach(&info->extra_fields, fieldp) {
+ str_append_c(str, '\t');
+ str_append_tabescaped(str, *fieldp);
+ }
+ }
+}
+
+static void
+auth_master_event_create(struct auth_master_connection *conn,
+ const char *prefix)
+{
+ i_assert(conn->event == conn->event_parent);
+ conn->event = event_create(conn->event_parent);
+ event_set_append_log_prefix(conn->event, prefix);
+}
+
+static void
+auth_master_user_event_create(struct auth_master_connection *conn,
+ const char *prefix,
+ const struct auth_user_info *info)
+{
+ auth_master_event_create(conn, prefix);
+
+ if (info != NULL) {
+ if (info->service != NULL)
+ event_add_str(conn->event, "service", info->service);
+ if (info->session_id != NULL)
+ event_add_str(conn->event, "session", info->session_id);
+ if (info->local_name != NULL)
+ event_add_str(conn->event, "local_name", info->local_name);
+ if (info->local_ip.family != 0) {
+ event_add_str(conn->event, "local_ip",
+ net_ip2addr(&info->local_ip));
+ }
+ if (info->local_port != 0) {
+ event_add_int(conn->event, "local_port",
+ info->local_port);
+ }
+ if (info->remote_ip.family != 0) {
+ event_add_str(conn->event, "remote_ip",
+ net_ip2addr(&info->remote_ip));
+ }
+ if (info->remote_port != 0) {
+ event_add_int(conn->event, "remote_port",
+ info->remote_port);
+ }
+ if (info->real_local_ip.family != 0)
+ event_add_str(conn->event, "real_local_ip",
+ net_ip2addr(&info->real_local_ip));
+ if (info->real_remote_ip.family != 0)
+ event_add_str(conn->event, "real_remote_ip",
+ net_ip2addr(&info->real_remote_ip));
+ if (info->real_local_port != 0)
+ event_add_int(conn->event, "real_local_port",
+ info->real_local_port);
+ if (info->real_remote_port != 0)
+ event_add_int(conn->event, "real_remote_port",
+ info->real_remote_port);
+ }
+}
+
+static void
+auth_master_event_finish(struct auth_master_connection *conn)
+{
+ i_assert(conn->event != conn->event_parent);
+ event_unref(&conn->event);
+ conn->event = conn->event_parent;
+}
+
+int auth_master_user_lookup(struct auth_master_connection *conn,
+ const char *user, const struct auth_user_info *info,
+ pool_t pool, const char **username_r,
+ const char *const **fields_r)
+{
+ struct auth_master_lookup_ctx ctx;
+ string_t *str;
+
+ if (!is_valid_string(user) || !is_valid_string(info->service)) {
+ /* non-allowed characters, the user can't exist */
+ *username_r = NULL;
+ *fields_r = NULL;
+ return 0;
+ }
+
+ i_zero(&ctx);
+ ctx.conn = conn;
+ ctx.return_value = -1;
+ ctx.pool = pool;
+ ctx.expected_reply = "USER";
+ ctx.user = user;
+
+ conn->reply_callback = auth_lookup_reply_callback;
+ conn->reply_context = &ctx;
+
+ str = t_str_new(128);
+ str_printfa(str, "USER\t%u\t%s",
+ auth_master_next_request_id(conn), user);
+ auth_user_info_export(str, info);
+ str_append_c(str, '\n');
+
+ auth_master_user_event_create(
+ conn, t_strdup_printf("userdb lookup(%s): ", user), info);
+ event_add_str(conn->event, "user", user);
+
+ struct event_passthrough *e =
+ event_create_passthrough(conn->event)->
+ set_name("auth_client_userdb_lookup_started");
+ e_debug(e->event(), "Started userdb lookup");
+
+ (void)auth_master_run_cmd(conn, str_c(str));
+
+ if (ctx.return_value <= 0 || ctx.fields[0] == NULL) {
+ *username_r = NULL;
+ *fields_r = ctx.fields != NULL ? ctx.fields :
+ p_new(pool, const char *, 1);
+
+ struct event_passthrough *e =
+ event_create_passthrough(conn->event)->
+ set_name("auth_client_userdb_lookup_finished");
+
+ if (ctx.return_value > 0) {
+ e->add_str("error", "Lookup didn't return username");
+ e_error(e->event(), "Userdb lookup failed: "
+ "Lookup didn't return username");
+ ctx.return_value = -2;
+ } else if ((*fields_r)[0] == NULL) {
+ e->add_str("error", "Lookup failed");
+ e_debug(e->event(), "Userdb lookup failed");
+ } else {
+ e->add_str("error", (*fields_r)[0]);
+ e_debug(e->event(), "Userdb lookup failed: %s",
+ (*fields_r)[0]);
+ }
+ } else {
+ *username_r = ctx.fields[0];
+ *fields_r = ctx.fields + 1;
+
+ struct event_passthrough *e =
+ event_create_passthrough(conn->event)->
+ set_name("auth_client_userdb_lookup_finished");
+ e_debug(e->event(), "Finished userdb lookup (username=%s %s)",
+ *username_r, t_strarray_join(*fields_r, " "));
+ }
+ auth_master_event_finish(conn);
+
+ conn->reply_context = NULL;
+ return ctx.return_value;
+}
+
+void auth_user_fields_parse(const char *const *fields, pool_t pool,
+ struct auth_user_reply *reply_r)
+{
+ i_zero(reply_r);
+ reply_r->uid = (uid_t)-1;
+ reply_r->gid = (gid_t)-1;
+ p_array_init(&reply_r->extra_fields, pool, 64);
+
+ for (; *fields != NULL; fields++) {
+ if (str_begins(*fields, "uid=")) {
+ if (str_to_uid(*fields + 4, &reply_r->uid) < 0)
+ i_error("Invalid uid in reply");
+ } else if (str_begins(*fields, "gid=")) {
+ if (str_to_gid(*fields + 4, &reply_r->gid) < 0)
+ i_error("Invalid gid in reply");
+ } else if (str_begins(*fields, "home="))
+ reply_r->home = p_strdup(pool, *fields + 5);
+ else if (str_begins(*fields, "chroot="))
+ reply_r->chroot = p_strdup(pool, *fields + 7);
+ else if (strcmp(*fields, "anonymous") == 0)
+ reply_r->anonymous = TRUE;
+ else {
+ const char *field = p_strdup(pool, *fields);
+ array_push_back(&reply_r->extra_fields, &field);
+ }
+ }
+}
+
+int auth_master_pass_lookup(struct auth_master_connection *conn,
+ const char *user, const struct auth_user_info *info,
+ pool_t pool, const char *const **fields_r)
+{
+ struct auth_master_lookup_ctx ctx;
+ string_t *str;
+
+ if (!is_valid_string(user) || !is_valid_string(info->service)) {
+ /* non-allowed characters, the user can't exist */
+ *fields_r = NULL;
+ return 0;
+ }
+
+ i_zero(&ctx);
+ ctx.conn = conn;
+ ctx.return_value = -1;
+ ctx.pool = pool;
+ ctx.expected_reply = "PASS";
+ ctx.user = user;
+
+ conn->reply_callback = auth_lookup_reply_callback;
+ conn->reply_context = &ctx;
+
+ str = t_str_new(128);
+ str_printfa(str, "PASS\t%u\t%s",
+ auth_master_next_request_id(conn), user);
+ auth_user_info_export(str, info);
+ str_append_c(str, '\n');
+
+ auth_master_user_event_create(
+ conn, t_strdup_printf("passdb lookup(%s): ", user), info);
+ event_add_str(conn->event, "user", user);
+
+ struct event_passthrough *e =
+ event_create_passthrough(conn->event)->
+ set_name("auth_client_passdb_lookup_started");
+ e_debug(e->event(), "Started passdb lookup");
+
+ (void)auth_master_run_cmd(conn, str_c(str));
+
+ *fields_r = ctx.fields != NULL ? ctx.fields :
+ p_new(pool, const char *, 1);
+
+ if (ctx.return_value <= 0) {
+ struct event_passthrough *e =
+ event_create_passthrough(conn->event)->
+ set_name("auth_client_passdb_lookup_finished");
+ if ((*fields_r)[0] == NULL) {
+ e->add_str("error", "Lookup failed");
+ e_debug(e->event(), "Passdb lookup failed");
+ } else {
+ e->add_str("error", (*fields_r)[0]);
+ e_debug(e->event(), "Passdb lookup failed: %s",
+ (*fields_r)[0]);
+ }
+ } else {
+ struct event_passthrough *e =
+ event_create_passthrough(conn->event)->
+ set_name("auth_client_passdb_lookup_finished");
+ e_debug(e->event(), "Finished passdb lookup (%s)",
+ t_strarray_join(*fields_r, " "));
+ }
+ auth_master_event_finish(conn);
+
+ conn->reply_context = NULL;
+ return ctx.return_value;
+}
+
+struct auth_master_cache_ctx {
+ struct auth_master_connection *conn;
+ unsigned int count;
+ bool failed;
+};
+
+static bool
+auth_cache_flush_reply_callback(const char *cmd, const char *const *args,
+ void *context)
+{
+ struct auth_master_cache_ctx *ctx = context;
+
+ if (strcmp(cmd, "OK") != 0)
+ ctx->failed = TRUE;
+ else if (args[0] == NULL || str_to_uint(args[0], &ctx->count) < 0)
+ ctx->failed = TRUE;
+
+ io_loop_stop(ctx->conn->ioloop);
+ return TRUE;
+}
+
+int auth_master_cache_flush(struct auth_master_connection *conn,
+ const char *const *users, unsigned int *count_r)
+{
+ struct auth_master_cache_ctx ctx;
+ string_t *str;
+
+ i_zero(&ctx);
+ ctx.conn = conn;
+
+ conn->reply_callback = auth_cache_flush_reply_callback;
+ conn->reply_context = &ctx;
+
+ str = t_str_new(128);
+ str_printfa(str, "CACHE-FLUSH\t%u", auth_master_next_request_id(conn));
+ if (users != NULL) {
+ for (; *users != NULL; users++) {
+ str_append_c(str, '\t');
+ str_append_tabescaped(str, *users);
+ }
+ }
+ str_append_c(str, '\n');
+
+ auth_master_event_create(conn, "auth cache flush: ");
+
+ struct event_passthrough *e =
+ event_create_passthrough(conn->event)->
+ set_name("auth_client_cache_flush_started");
+ e_debug(e->event(), "Started cache flush");
+
+ (void)auth_master_run_cmd(conn, str_c(str));
+
+ if (ctx.failed) {
+ struct event_passthrough *e =
+ event_create_passthrough(conn->event)->
+ set_name("auth_client_cache_flush_finished");
+ e->add_str("error", "Cache flush failed");
+ e_debug(e->event(), "Cache flush failed");
+ } else {
+ struct event_passthrough *e =
+ event_create_passthrough(conn->event)->
+ set_name("auth_client_cache_flush_finished");
+ e_debug(e->event(), "Finished cache flush");
+ }
+ auth_master_event_finish(conn);
+
+ conn->reply_context = NULL;
+ *count_r = ctx.count;
+ return ctx.failed ? -1 : 0;
+}
+
+static bool
+auth_user_list_reply_callback(const char *cmd, const char *const *args,
+ void *context)
+{
+ struct auth_master_user_list_ctx *ctx = context;
+ struct auth_master_connection *conn = ctx->conn;
+
+ timeout_reset(ctx->conn->to);
+ io_loop_stop(ctx->conn->ioloop);
+
+ if (strcmp(cmd, "DONE") == 0) {
+ if (args[0] != NULL && strcmp(args[0], "fail") == 0) {
+ e_error(conn->event, "User listing returned failure");
+ ctx->failed = TRUE;
+ }
+ ctx->finished = TRUE;
+ } else if (strcmp(cmd, "LIST") == 0 && args[0] != NULL) {
+ /* we'll just read all the users into memory. otherwise we'd
+ have to use a separate connection for listing and there's
+ a higher chance of a failure since the connection could be
+ open to dovecot-auth for a long time. */
+ str_append(ctx->username, args[0]);
+ } else {
+ e_error(conn->event, "User listing returned invalid input");
+ ctx->failed = TRUE;
+ }
+ return FALSE;
+}
+
+struct auth_master_user_list_ctx *
+auth_master_user_list_init(struct auth_master_connection *conn,
+ const char *user_mask,
+ const struct auth_user_info *info)
+{
+ struct auth_master_user_list_ctx *ctx;
+ string_t *str;
+
+ ctx = i_new(struct auth_master_user_list_ctx, 1);
+ ctx->conn = conn;
+ ctx->username = str_new(default_pool, 128);
+
+ conn->reply_callback = auth_user_list_reply_callback;
+ conn->reply_context = ctx;
+
+ str = t_str_new(128);
+ str_printfa(str, "LIST\t%u",
+ auth_master_next_request_id(conn));
+ if (*user_mask != '\0')
+ str_printfa(str, "\tuser=%s", user_mask);
+ if (info != NULL)
+ auth_user_info_export(str, info);
+ str_append_c(str, '\n');
+
+ auth_master_user_event_create(conn, "userdb list: ", info);
+ event_add_str(conn->event," user_mask", user_mask);
+
+ struct event_passthrough *e =
+ event_create_passthrough(conn->event)->
+ set_name("auth_client_userdb_list_started");
+ e_debug(e->event(), "Started listing users (user_mask=%s)", user_mask);
+
+ if (auth_master_run_cmd_pre(conn, str_c(str)) < 0)
+ ctx->failed = TRUE;
+ if (conn->prev_ioloop != NULL)
+ io_loop_set_current(conn->prev_ioloop);
+
+ return ctx;
+}
+
+static const char *
+auth_master_user_do_list_next(struct auth_master_user_list_ctx *ctx)
+{
+ struct auth_master_connection *conn = ctx->conn;
+ const char *line;
+
+ if (!conn->connected)
+ return NULL;
+
+ str_truncate(ctx->username, 0);
+
+ /* try to read already buffered input */
+ line = i_stream_next_line(conn->conn.input);
+ if (line != NULL) {
+ T_BEGIN {
+ conn->conn.v.input_line(&conn->conn, line);
+ } T_END;
+ }
+ if (conn->aborted)
+ ctx->failed = TRUE;
+ if (ctx->finished || ctx->failed)
+ return NULL;
+ if (str_len(ctx->username) > 0)
+ return str_c(ctx->username);
+
+ /* wait for more data */
+ io_loop_set_current(conn->ioloop);
+ i_stream_set_input_pending(conn->conn.input, TRUE);
+ io_loop_run(conn->ioloop);
+ io_loop_set_current(conn->prev_ioloop);
+
+ if (conn->aborted)
+ ctx->failed = TRUE;
+ if (ctx->finished || ctx->failed)
+ return NULL;
+ return str_c(ctx->username);
+}
+
+const char *auth_master_user_list_next(struct auth_master_user_list_ctx *ctx)
+{
+ struct auth_master_connection *conn = ctx->conn;
+ const char *username;
+
+ username = auth_master_user_do_list_next(ctx);
+ if (username == NULL)
+ return NULL;
+
+ e_debug(conn->event, "Returned username: %s", username);
+ return username;
+}
+
+int auth_master_user_list_deinit(struct auth_master_user_list_ctx **_ctx)
+{
+ struct auth_master_user_list_ctx *ctx = *_ctx;
+ struct auth_master_connection *conn = ctx->conn;
+ int ret = ctx->failed ? -1 : 0;
+
+ *_ctx = NULL;
+ auth_master_run_cmd_post(ctx->conn);
+
+ if (ret < 0) {
+ struct event_passthrough *e =
+ event_create_passthrough(conn->event)->
+ set_name("auth_client_userdb_list_finished");
+ e->add_str("error", "Listing users failed");
+ e_debug(e->event(), "Listing users failed");
+ } else {
+ struct event_passthrough *e =
+ event_create_passthrough(conn->event)->
+ set_name("auth_client_userdb_list_finished");
+ e_debug(e->event(), "Finished listing users");
+ }
+ auth_master_event_finish(conn);
+
+ str_free(&ctx->username);
+ i_free(ctx);
+ return ret;
+}
diff --git a/src/lib-auth/auth-master.h b/src/lib-auth/auth-master.h
new file mode 100644
index 0000000..f62985a
--- /dev/null
+++ b/src/lib-auth/auth-master.h
@@ -0,0 +1,76 @@
+#ifndef AUTH_MASTER_H
+#define AUTH_MASTER_H
+
+#include "net.h"
+
+enum auth_master_flags {
+ /* Enable logging debug information */
+ AUTH_MASTER_FLAG_DEBUG = 0x01,
+ /* Don't disconnect from auth socket when idling */
+ AUTH_MASTER_FLAG_NO_IDLE_TIMEOUT = 0x02
+};
+
+struct auth_user_info {
+ const char *service;
+ const char *session_id;
+ const char *local_name;
+ struct ip_addr local_ip, remote_ip, real_local_ip, real_remote_ip;
+ in_port_t local_port, remote_port, real_local_port, real_remote_port;
+ const char *forward_fields;
+ ARRAY_TYPE(const_string) extra_fields;
+ bool debug;
+};
+
+struct auth_user_reply {
+ uid_t uid;
+ gid_t gid;
+ const char *home, *chroot;
+ ARRAY_TYPE(const_string) extra_fields;
+ bool anonymous:1;
+};
+
+struct auth_master_connection *
+auth_master_init(const char *auth_socket_path, enum auth_master_flags flags);
+void auth_master_deinit(struct auth_master_connection **conn);
+
+/* Set timeout for lookups. */
+void auth_master_set_timeout(struct auth_master_connection *conn,
+ unsigned int msecs);
+
+/* Returns the auth_socket_path */
+const char *auth_master_get_socket_path(struct auth_master_connection *conn);
+
+/* Do a USER lookup. Returns -2 = user-specific error, -1 = internal error,
+ 0 = user not found, 1 = ok. When returning -1 and fields[0] isn't NULL, it
+ contains an error message that should be shown to user. */
+int auth_master_user_lookup(struct auth_master_connection *conn,
+ const char *user, const struct auth_user_info *info,
+ pool_t pool, const char **username_r,
+ const char *const **fields_r);
+/* Do a PASS lookup (the actual password isn't returned). */
+int auth_master_pass_lookup(struct auth_master_connection *conn,
+ const char *user, const struct auth_user_info *info,
+ pool_t pool, const char *const **fields_r);
+/* Flush authentication cache for everyone (users=NULL) or only for specified
+ users. Returns number of users flushed from cache. */
+int auth_master_cache_flush(struct auth_master_connection *conn,
+ const char *const *users, unsigned int *count_r);
+
+/* Parse userdb extra fields into auth_user_reply structure. */
+void auth_user_fields_parse(const char *const *fields, pool_t pool,
+ struct auth_user_reply *reply_r);
+
+/* Iterate through all users. If user_mask is non-NULL, it contains a string
+ with wildcards ('*', '?') that the auth server MAY use to limit what users
+ are returned (but it may as well return all users anyway). */
+struct auth_master_user_list_ctx *
+auth_master_user_list_init(struct auth_master_connection *conn,
+ const char *user_mask,
+ const struct auth_user_info *info) ATTR_NULL(3);
+const char *auth_master_user_list_next(struct auth_master_user_list_ctx *ctx);
+/* Returns -1 if anything failed, 0 if ok */
+int auth_master_user_list_deinit(struct auth_master_user_list_ctx **ctx);
+
+/* INTERNAL: */
+void auth_user_info_export(string_t *str, const struct auth_user_info *info);
+#endif
diff --git a/src/lib-auth/test-auth-master.c b/src/lib-auth/test-auth-master.c
new file mode 100644
index 0000000..95c9daf
--- /dev/null
+++ b/src/lib-auth/test-auth-master.c
@@ -0,0 +1,74 @@
+/* Copyright (c) 2019 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "auth-master.h"
+#include "net.h"
+#include "test-common.h"
+#include "str.h"
+
+static void test_auth_user_info_export(void)
+{
+ string_t *str;
+ struct auth_user_info info;
+
+ i_zero(&info);
+
+ test_begin("auth_user_info_export()");
+
+ /* Setup info for auth_user_info_export call where the
+ * resulting auth request string should contain all
+ * real_ variables. */
+ test_assert(net_addr2ip("192.168.1.1", &info.local_ip) == 0);
+ test_assert(net_addr2ip("192.23.42.9", &info.real_local_ip) == 0);
+ test_assert(net_addr2ip("10.42.3.223", &info.remote_ip) == 0);
+ test_assert(net_addr2ip("192.168.1.2", &info.real_remote_ip) == 0);
+ info.local_port = 57035;
+ info.remote_port = 53075;
+ info.real_remote_port = 64385;
+ info.real_local_port = 57391;
+
+ str = t_str_new(128);
+ auth_user_info_export(str, &info);
+
+ test_assert(strstr(str_c(str), "real_rip=192.168.1.2") != NULL);
+ test_assert(strstr(str_c(str), "real_lip=192.23.42.9") != NULL);
+ test_assert(strstr(str_c(str), "rip=10.42.3.223") != NULL);
+ test_assert(strstr(str_c(str), "lip=192.168.1.1") != NULL);
+ test_assert(strstr(str_c(str), "real_rport=64385") != NULL);
+ test_assert(strstr(str_c(str), "rport=53075") != NULL);
+ test_assert(strstr(str_c(str), "real_lport=57391") != NULL);
+ test_assert(strstr(str_c(str), "lport=57035") != NULL);
+
+ /* Setup info for auth_user_info_export call where the
+ * resulting auth request string should not contain any
+ * real_ variables. */
+ test_assert(net_addr2ip("10.42.3.223", &info.real_remote_ip) == 0);
+ test_assert(net_addr2ip("192.168.1.1", &info.real_local_ip) == 0);
+ info.real_remote_port = 53075;
+ info.real_local_port = 57035;
+
+ str_truncate(str, 0);
+ auth_user_info_export(str, &info);
+
+ test_assert(strstr(str_c(str), "rip=10.42.3.223") != NULL);
+ test_assert(strstr(str_c(str), "lip=192.168.1.1") != NULL);
+ test_assert(strstr(str_c(str), "lport=57035") != NULL);
+ test_assert(strstr(str_c(str), "rport=53075") != NULL);
+ /* The following fields should not be part of the string as
+ * they are matching with their non-real counterparts */
+ test_assert(strstr(str_c(str), "real_lport") == NULL);
+ test_assert(strstr(str_c(str), "real_rport") == NULL);
+ test_assert(strstr(str_c(str), "real_rip") == NULL);
+ test_assert(strstr(str_c(str), "real_lip") == NULL);
+
+ test_end();
+}
+
+int main(void)
+{
+ static void (*const test_functions[])(void) = {
+ test_auth_user_info_export,
+ NULL
+ };
+ return test_run(test_functions);
+}
diff --git a/src/lib-charset/Makefile.am b/src/lib-charset/Makefile.am
new file mode 100644
index 0000000..5c41f07
--- /dev/null
+++ b/src/lib-charset/Makefile.am
@@ -0,0 +1,37 @@
+noinst_LTLIBRARIES = libcharset.la
+
+AM_CPPFLAGS = \
+ -I$(top_srcdir)/src/lib \
+ -I$(top_srcdir)/src/lib-test
+
+libcharset_la_LIBADD = $(LTLIBICONV)
+libcharset_la_SOURCES = \
+ charset-iconv.c \
+ charset-utf8.c \
+ charset-utf8-only.c
+
+headers = \
+ charset-utf8.h \
+ charset-utf8-private.h
+
+pkginc_libdir=$(pkgincludedir)
+pkginc_lib_HEADERS = $(headers)
+
+test_programs = \
+ test-charset
+
+noinst_PROGRAMS = $(test_programs)
+
+test_libs = \
+ ../lib-test/libtest.la \
+ ../lib/liblib.la
+test_deps = $(noinst_LTLIBRARIES) $(test_libs)
+
+test_charset_SOURCES = test-charset.c
+test_charset_LDADD = libcharset.la $(test_libs)
+test_charset_DEPENDENCIES = libcharset.la $(test_deps)
+
+check-local:
+ for bin in $(test_programs); do \
+ if ! $(RUN_TEST) ./$$bin; then exit 1; fi; \
+ done
diff --git a/src/lib-charset/Makefile.in b/src/lib-charset/Makefile.in
new file mode 100644
index 0000000..6c18a40
--- /dev/null
+++ b/src/lib-charset/Makefile.in
@@ -0,0 +1,862 @@
+# Makefile.in generated by automake 1.16.1 from Makefile.am.
+# @configure_input@
+
+# Copyright (C) 1994-2018 Free Software Foundation, Inc.
+
+# This Makefile.in is free software; the Free Software Foundation
+# gives unlimited permission to copy and/or distribute it,
+# with or without modifications, as long as this notice is preserved.
+
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY, to the extent permitted by law; without
+# even the implied warranty of MERCHANTABILITY or FITNESS FOR A
+# PARTICULAR PURPOSE.
+
+@SET_MAKE@
+
+
+
+VPATH = @srcdir@
+am__is_gnu_make = { \
+ if test -z '$(MAKELEVEL)'; then \
+ false; \
+ elif test -n '$(MAKE_HOST)'; then \
+ true; \
+ elif test -n '$(MAKE_VERSION)' && test -n '$(CURDIR)'; then \
+ true; \
+ else \
+ false; \
+ fi; \
+}
+am__make_running_with_option = \
+ case $${target_option-} in \
+ ?) ;; \
+ *) echo "am__make_running_with_option: internal error: invalid" \
+ "target option '$${target_option-}' specified" >&2; \
+ exit 1;; \
+ esac; \
+ has_opt=no; \
+ sane_makeflags=$$MAKEFLAGS; \
+ if $(am__is_gnu_make); then \
+ sane_makeflags=$$MFLAGS; \
+ else \
+ case $$MAKEFLAGS in \
+ *\\[\ \ ]*) \
+ bs=\\; \
+ sane_makeflags=`printf '%s\n' "$$MAKEFLAGS" \
+ | sed "s/$$bs$$bs[$$bs $$bs ]*//g"`;; \
+ esac; \
+ fi; \
+ skip_next=no; \
+ strip_trailopt () \
+ { \
+ flg=`printf '%s\n' "$$flg" | sed "s/$$1.*$$//"`; \
+ }; \
+ for flg in $$sane_makeflags; do \
+ test $$skip_next = yes && { skip_next=no; continue; }; \
+ case $$flg in \
+ *=*|--*) continue;; \
+ -*I) strip_trailopt 'I'; skip_next=yes;; \
+ -*I?*) strip_trailopt 'I';; \
+ -*O) strip_trailopt 'O'; skip_next=yes;; \
+ -*O?*) strip_trailopt 'O';; \
+ -*l) strip_trailopt 'l'; skip_next=yes;; \
+ -*l?*) strip_trailopt 'l';; \
+ -[dEDm]) skip_next=yes;; \
+ -[JT]) skip_next=yes;; \
+ esac; \
+ case $$flg in \
+ *$$target_option*) has_opt=yes; break;; \
+ esac; \
+ done; \
+ test $$has_opt = yes
+am__make_dryrun = (target_option=n; $(am__make_running_with_option))
+am__make_keepgoing = (target_option=k; $(am__make_running_with_option))
+pkgdatadir = $(datadir)/@PACKAGE@
+pkgincludedir = $(includedir)/@PACKAGE@
+pkglibdir = $(libdir)/@PACKAGE@
+pkglibexecdir = $(libexecdir)/@PACKAGE@
+am__cd = CDPATH="$${ZSH_VERSION+.}$(PATH_SEPARATOR)" && cd
+install_sh_DATA = $(install_sh) -c -m 644
+install_sh_PROGRAM = $(install_sh) -c
+install_sh_SCRIPT = $(install_sh) -c
+INSTALL_HEADER = $(INSTALL_DATA)
+transform = $(program_transform_name)
+NORMAL_INSTALL = :
+PRE_INSTALL = :
+POST_INSTALL = :
+NORMAL_UNINSTALL = :
+PRE_UNINSTALL = :
+POST_UNINSTALL = :
+build_triplet = @build@
+host_triplet = @host@
+noinst_PROGRAMS = $(am__EXEEXT_1)
+subdir = src/lib-charset
+ACLOCAL_M4 = $(top_srcdir)/aclocal.m4
+am__aclocal_m4_deps = $(top_srcdir)/m4/ac_checktype2.m4 \
+ $(top_srcdir)/m4/ac_typeof.m4 $(top_srcdir)/m4/arc4random.m4 \
+ $(top_srcdir)/m4/blockdev.m4 $(top_srcdir)/m4/c99_vsnprintf.m4 \
+ $(top_srcdir)/m4/clock_gettime.m4 $(top_srcdir)/m4/crypt.m4 \
+ $(top_srcdir)/m4/crypt_xpg6.m4 $(top_srcdir)/m4/dbqlk.m4 \
+ $(top_srcdir)/m4/dirent_dtype.m4 $(top_srcdir)/m4/dovecot.m4 \
+ $(top_srcdir)/m4/fd_passing.m4 $(top_srcdir)/m4/fdatasync.m4 \
+ $(top_srcdir)/m4/flexible_array_member.m4 \
+ $(top_srcdir)/m4/glibc.m4 $(top_srcdir)/m4/gmtime_max.m4 \
+ $(top_srcdir)/m4/gmtime_tm_gmtoff.m4 \
+ $(top_srcdir)/m4/ioloop.m4 $(top_srcdir)/m4/iovec.m4 \
+ $(top_srcdir)/m4/ipv6.m4 $(top_srcdir)/m4/libcap.m4 \
+ $(top_srcdir)/m4/libtool.m4 $(top_srcdir)/m4/libwrap.m4 \
+ $(top_srcdir)/m4/linux_mremap.m4 $(top_srcdir)/m4/ltoptions.m4 \
+ $(top_srcdir)/m4/ltsugar.m4 $(top_srcdir)/m4/ltversion.m4 \
+ $(top_srcdir)/m4/lt~obsolete.m4 $(top_srcdir)/m4/mmap_write.m4 \
+ $(top_srcdir)/m4/mntctl.m4 $(top_srcdir)/m4/modules.m4 \
+ $(top_srcdir)/m4/notify.m4 $(top_srcdir)/m4/nsl.m4 \
+ $(top_srcdir)/m4/off_t_max.m4 $(top_srcdir)/m4/pkg.m4 \
+ $(top_srcdir)/m4/pr_set_dumpable.m4 \
+ $(top_srcdir)/m4/q_quotactl.m4 $(top_srcdir)/m4/quota.m4 \
+ $(top_srcdir)/m4/random.m4 $(top_srcdir)/m4/rlimit.m4 \
+ $(top_srcdir)/m4/sendfile.m4 $(top_srcdir)/m4/size_t_signed.m4 \
+ $(top_srcdir)/m4/sockpeercred.m4 $(top_srcdir)/m4/sql.m4 \
+ $(top_srcdir)/m4/ssl.m4 $(top_srcdir)/m4/st_tim.m4 \
+ $(top_srcdir)/m4/static_array.m4 $(top_srcdir)/m4/test_with.m4 \
+ $(top_srcdir)/m4/time_t.m4 $(top_srcdir)/m4/typeof.m4 \
+ $(top_srcdir)/m4/typeof_dev_t.m4 \
+ $(top_srcdir)/m4/uoff_t_max.m4 $(top_srcdir)/m4/vararg.m4 \
+ $(top_srcdir)/m4/want_apparmor.m4 \
+ $(top_srcdir)/m4/want_bsdauth.m4 \
+ $(top_srcdir)/m4/want_bzlib.m4 \
+ $(top_srcdir)/m4/want_cassandra.m4 \
+ $(top_srcdir)/m4/want_cdb.m4 \
+ $(top_srcdir)/m4/want_checkpassword.m4 \
+ $(top_srcdir)/m4/want_clucene.m4 $(top_srcdir)/m4/want_db.m4 \
+ $(top_srcdir)/m4/want_gssapi.m4 $(top_srcdir)/m4/want_icu.m4 \
+ $(top_srcdir)/m4/want_ldap.m4 $(top_srcdir)/m4/want_lua.m4 \
+ $(top_srcdir)/m4/want_lz4.m4 $(top_srcdir)/m4/want_lzma.m4 \
+ $(top_srcdir)/m4/want_mysql.m4 $(top_srcdir)/m4/want_pam.m4 \
+ $(top_srcdir)/m4/want_passwd.m4 $(top_srcdir)/m4/want_pgsql.m4 \
+ $(top_srcdir)/m4/want_prefetch.m4 \
+ $(top_srcdir)/m4/want_shadow.m4 \
+ $(top_srcdir)/m4/want_sodium.m4 $(top_srcdir)/m4/want_solr.m4 \
+ $(top_srcdir)/m4/want_sqlite.m4 \
+ $(top_srcdir)/m4/want_stemmer.m4 \
+ $(top_srcdir)/m4/want_systemd.m4 \
+ $(top_srcdir)/m4/want_textcat.m4 \
+ $(top_srcdir)/m4/want_unwind.m4 $(top_srcdir)/m4/want_zlib.m4 \
+ $(top_srcdir)/m4/want_zstd.m4 $(top_srcdir)/configure.ac
+am__configure_deps = $(am__aclocal_m4_deps) $(CONFIGURE_DEPENDENCIES) \
+ $(ACLOCAL_M4)
+DIST_COMMON = $(srcdir)/Makefile.am $(pkginc_lib_HEADERS) \
+ $(am__DIST_COMMON)
+mkinstalldirs = $(install_sh) -d
+CONFIG_HEADER = $(top_builddir)/config.h
+CONFIG_CLEAN_FILES =
+CONFIG_CLEAN_VPATH_FILES =
+am__EXEEXT_1 = test-charset$(EXEEXT)
+PROGRAMS = $(noinst_PROGRAMS)
+LTLIBRARIES = $(noinst_LTLIBRARIES)
+am__DEPENDENCIES_1 =
+libcharset_la_DEPENDENCIES = $(am__DEPENDENCIES_1)
+am_libcharset_la_OBJECTS = charset-iconv.lo charset-utf8.lo \
+ charset-utf8-only.lo
+libcharset_la_OBJECTS = $(am_libcharset_la_OBJECTS)
+AM_V_lt = $(am__v_lt_@AM_V@)
+am__v_lt_ = $(am__v_lt_@AM_DEFAULT_V@)
+am__v_lt_0 = --silent
+am__v_lt_1 =
+am_test_charset_OBJECTS = test-charset.$(OBJEXT)
+test_charset_OBJECTS = $(am_test_charset_OBJECTS)
+AM_V_P = $(am__v_P_@AM_V@)
+am__v_P_ = $(am__v_P_@AM_DEFAULT_V@)
+am__v_P_0 = false
+am__v_P_1 = :
+AM_V_GEN = $(am__v_GEN_@AM_V@)
+am__v_GEN_ = $(am__v_GEN_@AM_DEFAULT_V@)
+am__v_GEN_0 = @echo " GEN " $@;
+am__v_GEN_1 =
+AM_V_at = $(am__v_at_@AM_V@)
+am__v_at_ = $(am__v_at_@AM_DEFAULT_V@)
+am__v_at_0 = @
+am__v_at_1 =
+DEFAULT_INCLUDES = -I.@am__isrc@ -I$(top_builddir)
+depcomp = $(SHELL) $(top_srcdir)/depcomp
+am__maybe_remake_depfiles = depfiles
+am__depfiles_remade = ./$(DEPDIR)/charset-iconv.Plo \
+ ./$(DEPDIR)/charset-utf8-only.Plo ./$(DEPDIR)/charset-utf8.Plo \
+ ./$(DEPDIR)/test-charset.Po
+am__mv = mv -f
+COMPILE = $(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) \
+ $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS)
+LTCOMPILE = $(LIBTOOL) $(AM_V_lt) --tag=CC $(AM_LIBTOOLFLAGS) \
+ $(LIBTOOLFLAGS) --mode=compile $(CC) $(DEFS) \
+ $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) $(CPPFLAGS) \
+ $(AM_CFLAGS) $(CFLAGS)
+AM_V_CC = $(am__v_CC_@AM_V@)
+am__v_CC_ = $(am__v_CC_@AM_DEFAULT_V@)
+am__v_CC_0 = @echo " CC " $@;
+am__v_CC_1 =
+CCLD = $(CC)
+LINK = $(LIBTOOL) $(AM_V_lt) --tag=CC $(AM_LIBTOOLFLAGS) \
+ $(LIBTOOLFLAGS) --mode=link $(CCLD) $(AM_CFLAGS) $(CFLAGS) \
+ $(AM_LDFLAGS) $(LDFLAGS) -o $@
+AM_V_CCLD = $(am__v_CCLD_@AM_V@)
+am__v_CCLD_ = $(am__v_CCLD_@AM_DEFAULT_V@)
+am__v_CCLD_0 = @echo " CCLD " $@;
+am__v_CCLD_1 =
+SOURCES = $(libcharset_la_SOURCES) $(test_charset_SOURCES)
+DIST_SOURCES = $(libcharset_la_SOURCES) $(test_charset_SOURCES)
+am__can_run_installinfo = \
+ case $$AM_UPDATE_INFO_DIR in \
+ n|no|NO) false;; \
+ *) (install-info --version) >/dev/null 2>&1;; \
+ esac
+am__vpath_adj_setup = srcdirstrip=`echo "$(srcdir)" | sed 's|.|.|g'`;
+am__vpath_adj = case $$p in \
+ $(srcdir)/*) f=`echo "$$p" | sed "s|^$$srcdirstrip/||"`;; \
+ *) f=$$p;; \
+ esac;
+am__strip_dir = f=`echo $$p | sed -e 's|^.*/||'`;
+am__install_max = 40
+am__nobase_strip_setup = \
+ srcdirstrip=`echo "$(srcdir)" | sed 's/[].[^$$\\*|]/\\\\&/g'`
+am__nobase_strip = \
+ for p in $$list; do echo "$$p"; done | sed -e "s|$$srcdirstrip/||"
+am__nobase_list = $(am__nobase_strip_setup); \
+ for p in $$list; do echo "$$p $$p"; done | \
+ sed "s| $$srcdirstrip/| |;"' / .*\//!s/ .*/ ./; s,\( .*\)/[^/]*$$,\1,' | \
+ $(AWK) 'BEGIN { files["."] = "" } { files[$$2] = files[$$2] " " $$1; \
+ if (++n[$$2] == $(am__install_max)) \
+ { print $$2, files[$$2]; n[$$2] = 0; files[$$2] = "" } } \
+ END { for (dir in files) print dir, files[dir] }'
+am__base_list = \
+ sed '$$!N;$$!N;$$!N;$$!N;$$!N;$$!N;$$!N;s/\n/ /g' | \
+ sed '$$!N;$$!N;$$!N;$$!N;s/\n/ /g'
+am__uninstall_files_from_dir = { \
+ test -z "$$files" \
+ || { test ! -d "$$dir" && test ! -f "$$dir" && test ! -r "$$dir"; } \
+ || { echo " ( cd '$$dir' && rm -f" $$files ")"; \
+ $(am__cd) "$$dir" && rm -f $$files; }; \
+ }
+am__installdirs = "$(DESTDIR)$(pkginc_libdir)"
+HEADERS = $(pkginc_lib_HEADERS)
+am__tagged_files = $(HEADERS) $(SOURCES) $(TAGS_FILES) $(LISP)
+# Read a list of newline-separated strings from the standard input,
+# and print each of them once, without duplicates. Input order is
+# *not* preserved.
+am__uniquify_input = $(AWK) '\
+ BEGIN { nonempty = 0; } \
+ { items[$$0] = 1; nonempty = 1; } \
+ END { if (nonempty) { for (i in items) print i; }; } \
+'
+# Make sure the list of sources is unique. This is necessary because,
+# e.g., the same source file might be shared among _SOURCES variables
+# for different programs/libraries.
+am__define_uniq_tagged_files = \
+ list='$(am__tagged_files)'; \
+ unique=`for i in $$list; do \
+ if test -f "$$i"; then echo $$i; else echo $(srcdir)/$$i; fi; \
+ done | $(am__uniquify_input)`
+ETAGS = etags
+CTAGS = ctags
+am__DIST_COMMON = $(srcdir)/Makefile.in $(top_srcdir)/depcomp
+DISTFILES = $(DIST_COMMON) $(DIST_SOURCES) $(TEXINFOS) $(EXTRA_DIST)
+ACLOCAL = @ACLOCAL@
+ACLOCAL_AMFLAGS = @ACLOCAL_AMFLAGS@
+AMTAR = @AMTAR@
+AM_DEFAULT_VERBOSITY = @AM_DEFAULT_VERBOSITY@
+APPARMOR_LIBS = @APPARMOR_LIBS@
+AR = @AR@
+AUTH_CFLAGS = @AUTH_CFLAGS@
+AUTH_LIBS = @AUTH_LIBS@
+AUTOCONF = @AUTOCONF@
+AUTOHEADER = @AUTOHEADER@
+AUTOMAKE = @AUTOMAKE@
+AWK = @AWK@
+BINARY_CFLAGS = @BINARY_CFLAGS@
+BINARY_LDFLAGS = @BINARY_LDFLAGS@
+BISON = @BISON@
+CASSANDRA_CFLAGS = @CASSANDRA_CFLAGS@
+CASSANDRA_LIBS = @CASSANDRA_LIBS@
+CC = @CC@
+CCDEPMODE = @CCDEPMODE@
+CDB_LIBS = @CDB_LIBS@
+CFLAGS = @CFLAGS@
+CLUCENE_CFLAGS = @CLUCENE_CFLAGS@
+CLUCENE_LIBS = @CLUCENE_LIBS@
+COMPRESS_LIBS = @COMPRESS_LIBS@
+CPP = @CPP@
+CPPFLAGS = @CPPFLAGS@
+CRYPT_LIBS = @CRYPT_LIBS@
+CXX = @CXX@
+CXXCPP = @CXXCPP@
+CXXDEPMODE = @CXXDEPMODE@
+CXXFLAGS = @CXXFLAGS@
+CYGPATH_W = @CYGPATH_W@
+DEFS = @DEFS@
+DEPDIR = @DEPDIR@
+DICT_LIBS = @DICT_LIBS@
+DLLIB = @DLLIB@
+DLLTOOL = @DLLTOOL@
+DSYMUTIL = @DSYMUTIL@
+DUMPBIN = @DUMPBIN@
+ECHO_C = @ECHO_C@
+ECHO_N = @ECHO_N@
+ECHO_T = @ECHO_T@
+EGREP = @EGREP@
+EXEEXT = @EXEEXT@
+FGREP = @FGREP@
+FLEX = @FLEX@
+FUZZER_CPPFLAGS = @FUZZER_CPPFLAGS@
+FUZZER_LDFLAGS = @FUZZER_LDFLAGS@
+GREP = @GREP@
+INSTALL = @INSTALL@
+INSTALL_DATA = @INSTALL_DATA@
+INSTALL_PROGRAM = @INSTALL_PROGRAM@
+INSTALL_SCRIPT = @INSTALL_SCRIPT@
+INSTALL_STRIP_PROGRAM = @INSTALL_STRIP_PROGRAM@
+KRB5CONFIG = @KRB5CONFIG@
+KRB5_CFLAGS = @KRB5_CFLAGS@
+KRB5_LIBS = @KRB5_LIBS@
+LD = @LD@
+LDAP_LIBS = @LDAP_LIBS@
+LDFLAGS = @LDFLAGS@
+LD_NO_WHOLE_ARCHIVE = @LD_NO_WHOLE_ARCHIVE@
+LD_WHOLE_ARCHIVE = @LD_WHOLE_ARCHIVE@
+LIBCAP = @LIBCAP@
+LIBDOVECOT = @LIBDOVECOT@
+LIBDOVECOT_COMPRESS = @LIBDOVECOT_COMPRESS@
+LIBDOVECOT_DEPS = @LIBDOVECOT_DEPS@
+LIBDOVECOT_DSYNC = @LIBDOVECOT_DSYNC@
+LIBDOVECOT_LA_LIBS = @LIBDOVECOT_LA_LIBS@
+LIBDOVECOT_LDA = @LIBDOVECOT_LDA@
+LIBDOVECOT_LDAP = @LIBDOVECOT_LDAP@
+LIBDOVECOT_LIBFTS = @LIBDOVECOT_LIBFTS@
+LIBDOVECOT_LIBFTS_DEPS = @LIBDOVECOT_LIBFTS_DEPS@
+LIBDOVECOT_LOGIN = @LIBDOVECOT_LOGIN@
+LIBDOVECOT_LUA = @LIBDOVECOT_LUA@
+LIBDOVECOT_LUA_DEPS = @LIBDOVECOT_LUA_DEPS@
+LIBDOVECOT_SQL = @LIBDOVECOT_SQL@
+LIBDOVECOT_STORAGE = @LIBDOVECOT_STORAGE@
+LIBDOVECOT_STORAGE_DEPS = @LIBDOVECOT_STORAGE_DEPS@
+LIBEXTTEXTCAT_CFLAGS = @LIBEXTTEXTCAT_CFLAGS@
+LIBEXTTEXTCAT_LIBS = @LIBEXTTEXTCAT_LIBS@
+LIBICONV = @LIBICONV@
+LIBICU_CFLAGS = @LIBICU_CFLAGS@
+LIBICU_LIBS = @LIBICU_LIBS@
+LIBOBJS = @LIBOBJS@
+LIBS = @LIBS@
+LIBSODIUM_CFLAGS = @LIBSODIUM_CFLAGS@
+LIBSODIUM_LIBS = @LIBSODIUM_LIBS@
+LIBTIRPC_CFLAGS = @LIBTIRPC_CFLAGS@
+LIBTIRPC_LIBS = @LIBTIRPC_LIBS@
+LIBTOOL = @LIBTOOL@
+LIBUNWIND_CFLAGS = @LIBUNWIND_CFLAGS@
+LIBUNWIND_LIBS = @LIBUNWIND_LIBS@
+LIBWRAP_LIBS = @LIBWRAP_LIBS@
+LINKED_STORAGE_LDADD = @LINKED_STORAGE_LDADD@
+LIPO = @LIPO@
+LN_S = @LN_S@
+LTLIBICONV = @LTLIBICONV@
+LTLIBOBJS = @LTLIBOBJS@
+LT_SYS_LIBRARY_PATH = @LT_SYS_LIBRARY_PATH@
+LUA_CFLAGS = @LUA_CFLAGS@
+LUA_LIBS = @LUA_LIBS@
+MAINT = @MAINT@
+MAKEINFO = @MAKEINFO@
+MANIFEST_TOOL = @MANIFEST_TOOL@
+MKDIR_P = @MKDIR_P@
+MODULE_LIBS = @MODULE_LIBS@
+MODULE_SUFFIX = @MODULE_SUFFIX@
+MYSQL_CFLAGS = @MYSQL_CFLAGS@
+MYSQL_CONFIG = @MYSQL_CONFIG@
+MYSQL_LIBS = @MYSQL_LIBS@
+NM = @NM@
+NMEDIT = @NMEDIT@
+NOPLUGIN_LDFLAGS = @NOPLUGIN_LDFLAGS@
+OBJDUMP = @OBJDUMP@
+OBJEXT = @OBJEXT@
+OTOOL = @OTOOL@
+OTOOL64 = @OTOOL64@
+PACKAGE = @PACKAGE@
+PACKAGE_BUGREPORT = @PACKAGE_BUGREPORT@
+PACKAGE_NAME = @PACKAGE_NAME@
+PACKAGE_STRING = @PACKAGE_STRING@
+PACKAGE_TARNAME = @PACKAGE_TARNAME@
+PACKAGE_URL = @PACKAGE_URL@
+PACKAGE_VERSION = @PACKAGE_VERSION@
+PANDOC = @PANDOC@
+PATH_SEPARATOR = @PATH_SEPARATOR@
+PGSQL_CFLAGS = @PGSQL_CFLAGS@
+PGSQL_LIBS = @PGSQL_LIBS@
+PG_CONFIG = @PG_CONFIG@
+PIE_CFLAGS = @PIE_CFLAGS@
+PIE_LDFLAGS = @PIE_LDFLAGS@
+PKG_CONFIG = @PKG_CONFIG@
+PKG_CONFIG_LIBDIR = @PKG_CONFIG_LIBDIR@
+PKG_CONFIG_PATH = @PKG_CONFIG_PATH@
+QUOTA_LIBS = @QUOTA_LIBS@
+RANLIB = @RANLIB@
+RELRO_LDFLAGS = @RELRO_LDFLAGS@
+RPCGEN = @RPCGEN@
+RUN_TEST = @RUN_TEST@
+SED = @SED@
+SETTING_FILES = @SETTING_FILES@
+SET_MAKE = @SET_MAKE@
+SHELL = @SHELL@
+SQLITE_CFLAGS = @SQLITE_CFLAGS@
+SQLITE_LIBS = @SQLITE_LIBS@
+SQL_CFLAGS = @SQL_CFLAGS@
+SQL_LIBS = @SQL_LIBS@
+SSL_CFLAGS = @SSL_CFLAGS@
+SSL_LIBS = @SSL_LIBS@
+STRIP = @STRIP@
+SYSTEMD_CFLAGS = @SYSTEMD_CFLAGS@
+SYSTEMD_LIBS = @SYSTEMD_LIBS@
+VALGRIND = @VALGRIND@
+VERSION = @VERSION@
+ZSTD_CFLAGS = @ZSTD_CFLAGS@
+ZSTD_LIBS = @ZSTD_LIBS@
+abs_builddir = @abs_builddir@
+abs_srcdir = @abs_srcdir@
+abs_top_builddir = @abs_top_builddir@
+abs_top_srcdir = @abs_top_srcdir@
+ac_ct_AR = @ac_ct_AR@
+ac_ct_CC = @ac_ct_CC@
+ac_ct_CXX = @ac_ct_CXX@
+ac_ct_DUMPBIN = @ac_ct_DUMPBIN@
+am__include = @am__include@
+am__leading_dot = @am__leading_dot@
+am__quote = @am__quote@
+am__tar = @am__tar@
+am__untar = @am__untar@
+bindir = @bindir@
+build = @build@
+build_alias = @build_alias@
+build_cpu = @build_cpu@
+build_os = @build_os@
+build_vendor = @build_vendor@
+builddir = @builddir@
+datadir = @datadir@
+datarootdir = @datarootdir@
+dict_drivers = @dict_drivers@
+docdir = @docdir@
+dvidir = @dvidir@
+exec_prefix = @exec_prefix@
+host = @host@
+host_alias = @host_alias@
+host_cpu = @host_cpu@
+host_os = @host_os@
+host_vendor = @host_vendor@
+htmldir = @htmldir@
+includedir = @includedir@
+infodir = @infodir@
+install_sh = @install_sh@
+libdir = @libdir@
+libexecdir = @libexecdir@
+localedir = @localedir@
+localstatedir = @localstatedir@
+mandir = @mandir@
+mkdir_p = @mkdir_p@
+moduledir = @moduledir@
+oldincludedir = @oldincludedir@
+pdfdir = @pdfdir@
+prefix = @prefix@
+program_transform_name = @program_transform_name@
+psdir = @psdir@
+rundir = @rundir@
+runstatedir = @runstatedir@
+sbindir = @sbindir@
+sharedstatedir = @sharedstatedir@
+sql_drivers = @sql_drivers@
+srcdir = @srcdir@
+ssldir = @ssldir@
+statedir = @statedir@
+sysconfdir = @sysconfdir@
+systemdservicetype = @systemdservicetype@
+systemdsystemunitdir = @systemdsystemunitdir@
+target_alias = @target_alias@
+top_build_prefix = @top_build_prefix@
+top_builddir = @top_builddir@
+top_srcdir = @top_srcdir@
+noinst_LTLIBRARIES = libcharset.la
+AM_CPPFLAGS = \
+ -I$(top_srcdir)/src/lib \
+ -I$(top_srcdir)/src/lib-test
+
+libcharset_la_LIBADD = $(LTLIBICONV)
+libcharset_la_SOURCES = \
+ charset-iconv.c \
+ charset-utf8.c \
+ charset-utf8-only.c
+
+headers = \
+ charset-utf8.h \
+ charset-utf8-private.h
+
+pkginc_libdir = $(pkgincludedir)
+pkginc_lib_HEADERS = $(headers)
+test_programs = \
+ test-charset
+
+test_libs = \
+ ../lib-test/libtest.la \
+ ../lib/liblib.la
+
+test_deps = $(noinst_LTLIBRARIES) $(test_libs)
+test_charset_SOURCES = test-charset.c
+test_charset_LDADD = libcharset.la $(test_libs)
+test_charset_DEPENDENCIES = libcharset.la $(test_deps)
+all: all-am
+
+.SUFFIXES:
+.SUFFIXES: .c .lo .o .obj
+$(srcdir)/Makefile.in: @MAINTAINER_MODE_TRUE@ $(srcdir)/Makefile.am $(am__configure_deps)
+ @for dep in $?; do \
+ case '$(am__configure_deps)' in \
+ *$$dep*) \
+ ( cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh ) \
+ && { if test -f $@; then exit 0; else break; fi; }; \
+ exit 1;; \
+ esac; \
+ done; \
+ echo ' cd $(top_srcdir) && $(AUTOMAKE) --foreign src/lib-charset/Makefile'; \
+ $(am__cd) $(top_srcdir) && \
+ $(AUTOMAKE) --foreign src/lib-charset/Makefile
+Makefile: $(srcdir)/Makefile.in $(top_builddir)/config.status
+ @case '$?' in \
+ *config.status*) \
+ cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh;; \
+ *) \
+ echo ' cd $(top_builddir) && $(SHELL) ./config.status $(subdir)/$@ $(am__maybe_remake_depfiles)'; \
+ cd $(top_builddir) && $(SHELL) ./config.status $(subdir)/$@ $(am__maybe_remake_depfiles);; \
+ esac;
+
+$(top_builddir)/config.status: $(top_srcdir)/configure $(CONFIG_STATUS_DEPENDENCIES)
+ cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh
+
+$(top_srcdir)/configure: @MAINTAINER_MODE_TRUE@ $(am__configure_deps)
+ cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh
+$(ACLOCAL_M4): @MAINTAINER_MODE_TRUE@ $(am__aclocal_m4_deps)
+ cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh
+$(am__aclocal_m4_deps):
+
+clean-noinstPROGRAMS:
+ @list='$(noinst_PROGRAMS)'; test -n "$$list" || exit 0; \
+ echo " rm -f" $$list; \
+ rm -f $$list || exit $$?; \
+ test -n "$(EXEEXT)" || exit 0; \
+ list=`for p in $$list; do echo "$$p"; done | sed 's/$(EXEEXT)$$//'`; \
+ echo " rm -f" $$list; \
+ rm -f $$list
+
+clean-noinstLTLIBRARIES:
+ -test -z "$(noinst_LTLIBRARIES)" || rm -f $(noinst_LTLIBRARIES)
+ @list='$(noinst_LTLIBRARIES)'; \
+ locs=`for p in $$list; do echo $$p; done | \
+ sed 's|^[^/]*$$|.|; s|/[^/]*$$||; s|$$|/so_locations|' | \
+ sort -u`; \
+ test -z "$$locs" || { \
+ echo rm -f $${locs}; \
+ rm -f $${locs}; \
+ }
+
+libcharset.la: $(libcharset_la_OBJECTS) $(libcharset_la_DEPENDENCIES) $(EXTRA_libcharset_la_DEPENDENCIES)
+ $(AM_V_CCLD)$(LINK) $(libcharset_la_OBJECTS) $(libcharset_la_LIBADD) $(LIBS)
+
+test-charset$(EXEEXT): $(test_charset_OBJECTS) $(test_charset_DEPENDENCIES) $(EXTRA_test_charset_DEPENDENCIES)
+ @rm -f test-charset$(EXEEXT)
+ $(AM_V_CCLD)$(LINK) $(test_charset_OBJECTS) $(test_charset_LDADD) $(LIBS)
+
+mostlyclean-compile:
+ -rm -f *.$(OBJEXT)
+
+distclean-compile:
+ -rm -f *.tab.c
+
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/charset-iconv.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/charset-utf8-only.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/charset-utf8.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/test-charset.Po@am__quote@ # am--include-marker
+
+$(am__depfiles_remade):
+ @$(MKDIR_P) $(@D)
+ @echo '# dummy' >$@-t && $(am__mv) $@-t $@
+
+am--depfiles: $(am__depfiles_remade)
+
+.c.o:
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(COMPILE) -MT $@ -MD -MP -MF $(DEPDIR)/$*.Tpo -c -o $@ $<
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/$*.Tpo $(DEPDIR)/$*.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='$<' object='$@' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(COMPILE) -c -o $@ $<
+
+.c.obj:
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(COMPILE) -MT $@ -MD -MP -MF $(DEPDIR)/$*.Tpo -c -o $@ `$(CYGPATH_W) '$<'`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/$*.Tpo $(DEPDIR)/$*.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='$<' object='$@' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(COMPILE) -c -o $@ `$(CYGPATH_W) '$<'`
+
+.c.lo:
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(LTCOMPILE) -MT $@ -MD -MP -MF $(DEPDIR)/$*.Tpo -c -o $@ $<
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/$*.Tpo $(DEPDIR)/$*.Plo
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='$<' object='$@' libtool=yes @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(LTCOMPILE) -c -o $@ $<
+
+mostlyclean-libtool:
+ -rm -f *.lo
+
+clean-libtool:
+ -rm -rf .libs _libs
+install-pkginc_libHEADERS: $(pkginc_lib_HEADERS)
+ @$(NORMAL_INSTALL)
+ @list='$(pkginc_lib_HEADERS)'; test -n "$(pkginc_libdir)" || list=; \
+ if test -n "$$list"; then \
+ echo " $(MKDIR_P) '$(DESTDIR)$(pkginc_libdir)'"; \
+ $(MKDIR_P) "$(DESTDIR)$(pkginc_libdir)" || exit 1; \
+ fi; \
+ for p in $$list; do \
+ if test -f "$$p"; then d=; else d="$(srcdir)/"; fi; \
+ echo "$$d$$p"; \
+ done | $(am__base_list) | \
+ while read files; do \
+ echo " $(INSTALL_HEADER) $$files '$(DESTDIR)$(pkginc_libdir)'"; \
+ $(INSTALL_HEADER) $$files "$(DESTDIR)$(pkginc_libdir)" || exit $$?; \
+ done
+
+uninstall-pkginc_libHEADERS:
+ @$(NORMAL_UNINSTALL)
+ @list='$(pkginc_lib_HEADERS)'; test -n "$(pkginc_libdir)" || list=; \
+ files=`for p in $$list; do echo $$p; done | sed -e 's|^.*/||'`; \
+ dir='$(DESTDIR)$(pkginc_libdir)'; $(am__uninstall_files_from_dir)
+
+ID: $(am__tagged_files)
+ $(am__define_uniq_tagged_files); mkid -fID $$unique
+tags: tags-am
+TAGS: tags
+
+tags-am: $(TAGS_DEPENDENCIES) $(am__tagged_files)
+ set x; \
+ here=`pwd`; \
+ $(am__define_uniq_tagged_files); \
+ shift; \
+ if test -z "$(ETAGS_ARGS)$$*$$unique"; then :; else \
+ test -n "$$unique" || unique=$$empty_fix; \
+ if test $$# -gt 0; then \
+ $(ETAGS) $(ETAGSFLAGS) $(AM_ETAGSFLAGS) $(ETAGS_ARGS) \
+ "$$@" $$unique; \
+ else \
+ $(ETAGS) $(ETAGSFLAGS) $(AM_ETAGSFLAGS) $(ETAGS_ARGS) \
+ $$unique; \
+ fi; \
+ fi
+ctags: ctags-am
+
+CTAGS: ctags
+ctags-am: $(TAGS_DEPENDENCIES) $(am__tagged_files)
+ $(am__define_uniq_tagged_files); \
+ test -z "$(CTAGS_ARGS)$$unique" \
+ || $(CTAGS) $(CTAGSFLAGS) $(AM_CTAGSFLAGS) $(CTAGS_ARGS) \
+ $$unique
+
+GTAGS:
+ here=`$(am__cd) $(top_builddir) && pwd` \
+ && $(am__cd) $(top_srcdir) \
+ && gtags -i $(GTAGS_ARGS) "$$here"
+cscopelist: cscopelist-am
+
+cscopelist-am: $(am__tagged_files)
+ list='$(am__tagged_files)'; \
+ case "$(srcdir)" in \
+ [\\/]* | ?:[\\/]*) sdir="$(srcdir)" ;; \
+ *) sdir=$(subdir)/$(srcdir) ;; \
+ esac; \
+ for i in $$list; do \
+ if test -f "$$i"; then \
+ echo "$(subdir)/$$i"; \
+ else \
+ echo "$$sdir/$$i"; \
+ fi; \
+ done >> $(top_builddir)/cscope.files
+
+distclean-tags:
+ -rm -f TAGS ID GTAGS GRTAGS GSYMS GPATH tags
+
+distdir: $(BUILT_SOURCES)
+ $(MAKE) $(AM_MAKEFLAGS) distdir-am
+
+distdir-am: $(DISTFILES)
+ @srcdirstrip=`echo "$(srcdir)" | sed 's/[].[^$$\\*]/\\\\&/g'`; \
+ topsrcdirstrip=`echo "$(top_srcdir)" | sed 's/[].[^$$\\*]/\\\\&/g'`; \
+ list='$(DISTFILES)'; \
+ dist_files=`for file in $$list; do echo $$file; done | \
+ sed -e "s|^$$srcdirstrip/||;t" \
+ -e "s|^$$topsrcdirstrip/|$(top_builddir)/|;t"`; \
+ case $$dist_files in \
+ */*) $(MKDIR_P) `echo "$$dist_files" | \
+ sed '/\//!d;s|^|$(distdir)/|;s,/[^/]*$$,,' | \
+ sort -u` ;; \
+ esac; \
+ for file in $$dist_files; do \
+ if test -f $$file || test -d $$file; then d=.; else d=$(srcdir); fi; \
+ if test -d $$d/$$file; then \
+ dir=`echo "/$$file" | sed -e 's,/[^/]*$$,,'`; \
+ if test -d "$(distdir)/$$file"; then \
+ find "$(distdir)/$$file" -type d ! -perm -700 -exec chmod u+rwx {} \;; \
+ fi; \
+ if test -d $(srcdir)/$$file && test $$d != $(srcdir); then \
+ cp -fpR $(srcdir)/$$file "$(distdir)$$dir" || exit 1; \
+ find "$(distdir)/$$file" -type d ! -perm -700 -exec chmod u+rwx {} \;; \
+ fi; \
+ cp -fpR $$d/$$file "$(distdir)$$dir" || exit 1; \
+ else \
+ test -f "$(distdir)/$$file" \
+ || cp -p $$d/$$file "$(distdir)/$$file" \
+ || exit 1; \
+ fi; \
+ done
+check-am: all-am
+ $(MAKE) $(AM_MAKEFLAGS) check-local
+check: check-am
+all-am: Makefile $(PROGRAMS) $(LTLIBRARIES) $(HEADERS)
+installdirs:
+ for dir in "$(DESTDIR)$(pkginc_libdir)"; do \
+ test -z "$$dir" || $(MKDIR_P) "$$dir"; \
+ done
+install: install-am
+install-exec: install-exec-am
+install-data: install-data-am
+uninstall: uninstall-am
+
+install-am: all-am
+ @$(MAKE) $(AM_MAKEFLAGS) install-exec-am install-data-am
+
+installcheck: installcheck-am
+install-strip:
+ if test -z '$(STRIP)'; then \
+ $(MAKE) $(AM_MAKEFLAGS) INSTALL_PROGRAM="$(INSTALL_STRIP_PROGRAM)" \
+ install_sh_PROGRAM="$(INSTALL_STRIP_PROGRAM)" INSTALL_STRIP_FLAG=-s \
+ install; \
+ else \
+ $(MAKE) $(AM_MAKEFLAGS) INSTALL_PROGRAM="$(INSTALL_STRIP_PROGRAM)" \
+ install_sh_PROGRAM="$(INSTALL_STRIP_PROGRAM)" INSTALL_STRIP_FLAG=-s \
+ "INSTALL_PROGRAM_ENV=STRIPPROG='$(STRIP)'" install; \
+ fi
+mostlyclean-generic:
+
+clean-generic:
+
+distclean-generic:
+ -test -z "$(CONFIG_CLEAN_FILES)" || rm -f $(CONFIG_CLEAN_FILES)
+ -test . = "$(srcdir)" || test -z "$(CONFIG_CLEAN_VPATH_FILES)" || rm -f $(CONFIG_CLEAN_VPATH_FILES)
+
+maintainer-clean-generic:
+ @echo "This command is intended for maintainers to use"
+ @echo "it deletes files that may require special tools to rebuild."
+clean: clean-am
+
+clean-am: clean-generic clean-libtool clean-noinstLTLIBRARIES \
+ clean-noinstPROGRAMS mostlyclean-am
+
+distclean: distclean-am
+ -rm -f ./$(DEPDIR)/charset-iconv.Plo
+ -rm -f ./$(DEPDIR)/charset-utf8-only.Plo
+ -rm -f ./$(DEPDIR)/charset-utf8.Plo
+ -rm -f ./$(DEPDIR)/test-charset.Po
+ -rm -f Makefile
+distclean-am: clean-am distclean-compile distclean-generic \
+ distclean-tags
+
+dvi: dvi-am
+
+dvi-am:
+
+html: html-am
+
+html-am:
+
+info: info-am
+
+info-am:
+
+install-data-am: install-pkginc_libHEADERS
+
+install-dvi: install-dvi-am
+
+install-dvi-am:
+
+install-exec-am:
+
+install-html: install-html-am
+
+install-html-am:
+
+install-info: install-info-am
+
+install-info-am:
+
+install-man:
+
+install-pdf: install-pdf-am
+
+install-pdf-am:
+
+install-ps: install-ps-am
+
+install-ps-am:
+
+installcheck-am:
+
+maintainer-clean: maintainer-clean-am
+ -rm -f ./$(DEPDIR)/charset-iconv.Plo
+ -rm -f ./$(DEPDIR)/charset-utf8-only.Plo
+ -rm -f ./$(DEPDIR)/charset-utf8.Plo
+ -rm -f ./$(DEPDIR)/test-charset.Po
+ -rm -f Makefile
+maintainer-clean-am: distclean-am maintainer-clean-generic
+
+mostlyclean: mostlyclean-am
+
+mostlyclean-am: mostlyclean-compile mostlyclean-generic \
+ mostlyclean-libtool
+
+pdf: pdf-am
+
+pdf-am:
+
+ps: ps-am
+
+ps-am:
+
+uninstall-am: uninstall-pkginc_libHEADERS
+
+.MAKE: check-am install-am install-strip
+
+.PHONY: CTAGS GTAGS TAGS all all-am am--depfiles check check-am \
+ check-local clean clean-generic clean-libtool \
+ clean-noinstLTLIBRARIES clean-noinstPROGRAMS cscopelist-am \
+ ctags ctags-am distclean distclean-compile distclean-generic \
+ distclean-libtool distclean-tags distdir dvi dvi-am html \
+ html-am info info-am install install-am install-data \
+ install-data-am install-dvi install-dvi-am install-exec \
+ install-exec-am install-html install-html-am install-info \
+ install-info-am install-man install-pdf install-pdf-am \
+ install-pkginc_libHEADERS install-ps install-ps-am \
+ install-strip installcheck installcheck-am installdirs \
+ maintainer-clean maintainer-clean-generic mostlyclean \
+ mostlyclean-compile mostlyclean-generic mostlyclean-libtool \
+ pdf pdf-am ps ps-am tags tags-am uninstall uninstall-am \
+ uninstall-pkginc_libHEADERS
+
+.PRECIOUS: Makefile
+
+
+check-local:
+ for bin in $(test_programs); do \
+ if ! $(RUN_TEST) ./$$bin; then exit 1; fi; \
+ done
+
+# Tell versions [3.59,3.63) of GNU make to not export all variables.
+# Otherwise a system limit (for SysV at least) may be exceeded.
+.NOEXPORT:
diff --git a/src/lib-charset/charset-iconv.c b/src/lib-charset/charset-iconv.c
new file mode 100644
index 0000000..7b29219
--- /dev/null
+++ b/src/lib-charset/charset-iconv.c
@@ -0,0 +1,147 @@
+/* Copyright (c) 2002-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "buffer.h"
+#include "charset-utf8-private.h"
+
+#ifdef HAVE_ICONV
+
+#include <iconv.h>
+#include <ctype.h>
+
+struct charset_translation {
+ iconv_t cd;
+ normalizer_func_t *normalizer;
+};
+
+static int
+iconv_charset_to_utf8_begin(const char *charset, normalizer_func_t *normalizer,
+ struct charset_translation **t_r)
+{
+ struct charset_translation *t;
+ iconv_t cd;
+
+ if (charset_is_utf8(charset))
+ cd = (iconv_t)-1;
+ else {
+ if (strcmp(charset, "UTF-8//TEST") == 0)
+ charset = "UTF-8";
+ cd = iconv_open("UTF-8", charset);
+ if (cd == (iconv_t)-1)
+ return -1;
+ }
+
+ t = i_new(struct charset_translation, 1);
+ t->cd = cd;
+ t->normalizer = normalizer;
+ *t_r = t;
+ return 0;
+}
+
+static void iconv_charset_to_utf8_end(struct charset_translation *t)
+{
+ if (t->cd != (iconv_t)-1)
+ iconv_close(t->cd);
+ i_free(t);
+}
+
+static void iconv_charset_to_utf8_reset(struct charset_translation *t)
+{
+ if (t->cd != (iconv_t)-1)
+ (void)iconv(t->cd, NULL, NULL, NULL, NULL);
+}
+
+static bool
+charset_to_utf8_try(struct charset_translation *t,
+ const unsigned char *src, size_t *src_size, buffer_t *dest,
+ enum charset_result *result)
+{
+ ICONV_CONST char *ic_srcbuf;
+ char tmpbuf[8192], *ic_destbuf;
+ size_t srcleft, destleft, tmpbuf_used;
+ bool ret = TRUE;
+
+ if (t->cd == (iconv_t)-1) {
+ /* input is already supposed to be UTF-8 */
+ *result = charset_utf8_to_utf8(t->normalizer, src, src_size, dest);
+ return TRUE;
+ }
+ destleft = sizeof(tmpbuf);
+ ic_destbuf = tmpbuf;
+ srcleft = *src_size;
+ ic_srcbuf = (ICONV_CONST char *) src;
+
+ if (iconv(t->cd, &ic_srcbuf, &srcleft,
+ &ic_destbuf, &destleft) != SIZE_MAX) {
+ i_assert(srcleft == 0);
+ *result = CHARSET_RET_OK;
+ } else if (errno == E2BIG) {
+ /* set result just to avoid compiler warning */
+ *result = CHARSET_RET_INCOMPLETE_INPUT;
+ ret = FALSE;
+ } else if (errno == EINVAL) {
+ i_assert(srcleft <= CHARSET_MAX_PENDING_BUF_SIZE);
+ *result = CHARSET_RET_INCOMPLETE_INPUT;
+ } else {
+ /* should be EILSEQ */
+ *result = CHARSET_RET_INVALID_INPUT;
+ ret = FALSE;
+ }
+ *src_size -= srcleft;
+
+ /* we just converted data to UTF-8. it shouldn't be invalid, but
+ Solaris iconv appears to pass invalid data through sometimes
+ (e.g. 8 bit characters with UTF-7) */
+ tmpbuf_used = sizeof(tmpbuf) - destleft;
+ if (charset_utf8_to_utf8(t->normalizer, (void *)tmpbuf,
+ &tmpbuf_used, dest) != CHARSET_RET_OK)
+ *result = CHARSET_RET_INVALID_INPUT;
+ return ret;
+}
+
+static enum charset_result
+iconv_charset_to_utf8(struct charset_translation *t,
+ const unsigned char *src, size_t *src_size,
+ buffer_t *dest)
+{
+ enum charset_result result;
+ size_t pos, size;
+ size_t prev_invalid_pos = SIZE_MAX;
+ bool ret;
+
+ for (pos = 0;;) {
+ i_assert(pos <= *src_size);
+ size = *src_size - pos;
+ ret = charset_to_utf8_try(t, src + pos, &size, dest, &result);
+ pos += size;
+
+ if (ret)
+ break;
+
+ if (result == CHARSET_RET_INVALID_INPUT) {
+ if (prev_invalid_pos != dest->used) {
+ buffer_append(dest, UNICODE_REPLACEMENT_CHAR_UTF8,
+ strlen(UNICODE_REPLACEMENT_CHAR_UTF8));
+ prev_invalid_pos = dest->used;
+ }
+ if (pos < *src_size)
+ pos++;
+ }
+ }
+
+ if (prev_invalid_pos != SIZE_MAX)
+ result = CHARSET_RET_INVALID_INPUT;
+
+ i_assert(*src_size - pos <= CHARSET_MAX_PENDING_BUF_SIZE);
+ *src_size = pos;
+ return result;
+}
+
+const struct charset_utf8_vfuncs charset_iconv = {
+ .to_utf8_begin = iconv_charset_to_utf8_begin,
+ .to_utf8_end = iconv_charset_to_utf8_end,
+ .to_utf8_reset = iconv_charset_to_utf8_reset,
+ .to_utf8 = iconv_charset_to_utf8,
+};
+
+#endif
diff --git a/src/lib-charset/charset-utf8-only.c b/src/lib-charset/charset-utf8-only.c
new file mode 100644
index 0000000..e8ea810
--- /dev/null
+++ b/src/lib-charset/charset-utf8-only.c
@@ -0,0 +1,51 @@
+/* Copyright (c) 2002-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "charset-utf8-private.h"
+
+struct charset_translation {
+ normalizer_func_t *normalizer;
+};
+
+static int
+utf8only_charset_to_utf8_begin(const char *charset,
+ normalizer_func_t *normalizer,
+ struct charset_translation **t_r)
+{
+ struct charset_translation *t;
+
+ if (!charset_is_utf8(charset)) {
+ /* no support for charsets that need translation */
+ return -1;
+ }
+
+ t = i_new(struct charset_translation, 1);
+ t->normalizer = normalizer;
+ *t_r = t;
+ return 0;
+}
+
+static void utf8only_charset_to_utf8_end(struct charset_translation *t)
+{
+ i_free(t);
+}
+
+static void
+utf8only_charset_to_utf8_reset(struct charset_translation *t ATTR_UNUSED)
+{
+}
+
+static enum charset_result
+utf8only_charset_to_utf8(struct charset_translation *t,
+ const unsigned char *src, size_t *src_size,
+ buffer_t *dest)
+{
+ return charset_utf8_to_utf8(t->normalizer, src, src_size, dest);
+}
+
+const struct charset_utf8_vfuncs charset_utf8only = {
+ .to_utf8_begin = utf8only_charset_to_utf8_begin,
+ .to_utf8_end = utf8only_charset_to_utf8_end,
+ .to_utf8_reset = utf8only_charset_to_utf8_reset,
+ .to_utf8 = utf8only_charset_to_utf8,
+};
diff --git a/src/lib-charset/charset-utf8-private.h b/src/lib-charset/charset-utf8-private.h
new file mode 100644
index 0000000..0f5064b
--- /dev/null
+++ b/src/lib-charset/charset-utf8-private.h
@@ -0,0 +1,21 @@
+#ifndef CHARSET_UTF8_PRIVATE_H
+#define CHARSET_UTF8_PRIVATE_H
+
+#include "unichar.h"
+#include "charset-utf8.h"
+
+struct charset_utf8_vfuncs {
+ int (*to_utf8_begin)(const char *charset, normalizer_func_t *normalizer,
+ struct charset_translation **t_r);
+ void (*to_utf8_end)(struct charset_translation *t);
+ void (*to_utf8_reset)(struct charset_translation *t);
+
+ enum charset_result (*to_utf8)(struct charset_translation *t,
+ const unsigned char *src,
+ size_t *src_size, buffer_t *dest);
+};
+
+extern const struct charset_utf8_vfuncs charset_utf8only;
+extern const struct charset_utf8_vfuncs charset_iconv;
+
+#endif
diff --git a/src/lib-charset/charset-utf8.c b/src/lib-charset/charset-utf8.c
new file mode 100644
index 0000000..22038e5
--- /dev/null
+++ b/src/lib-charset/charset-utf8.c
@@ -0,0 +1,99 @@
+/* Copyright (c) 2002-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "buffer.h"
+#include "str.h"
+#include "charset-utf8-private.h"
+
+#include <ctype.h>
+
+#ifdef HAVE_ICONV
+const struct charset_utf8_vfuncs *charset_utf8_vfuncs = &charset_iconv;
+#else
+const struct charset_utf8_vfuncs *charset_utf8_vfuncs = &charset_utf8only;
+#endif
+
+bool charset_is_utf8(const char *charset)
+{
+ return strcasecmp(charset, "us-ascii") == 0 ||
+ strcasecmp(charset, "ascii") == 0 ||
+ strcasecmp(charset, "UTF-8") == 0 ||
+ strcasecmp(charset, "UTF8") == 0;
+}
+
+int charset_to_utf8_str(const char *charset, normalizer_func_t *normalizer,
+ const char *input, string_t *output,
+ enum charset_result *result_r)
+{
+ struct charset_translation *t;
+ size_t len = strlen(input);
+
+ if (charset_to_utf8_begin(charset, normalizer, &t) < 0)
+ return -1;
+
+ *result_r = charset_to_utf8(t, (const unsigned char *)input,
+ &len, output);
+ charset_to_utf8_end(&t);
+ return 0;
+}
+
+struct charset_translation *
+charset_utf8_to_utf8_begin(normalizer_func_t *normalizer)
+{
+ struct charset_translation *trans;
+
+ if (charset_to_utf8_begin("UTF-8", normalizer, &trans) < 0)
+ i_unreached();
+ return trans;
+}
+
+enum charset_result
+charset_utf8_to_utf8(normalizer_func_t *normalizer,
+ const unsigned char *src, size_t *src_size, buffer_t *dest)
+{
+ enum charset_result res = CHARSET_RET_OK;
+ size_t pos;
+
+ uni_utf8_partial_strlen_n(src, *src_size, &pos);
+ if (pos < *src_size) {
+ i_assert(*src_size - pos <= CHARSET_MAX_PENDING_BUF_SIZE);
+ *src_size = pos;
+ res = CHARSET_RET_INCOMPLETE_INPUT;
+ }
+
+ if (normalizer != NULL) {
+ if (normalizer(src, *src_size, dest) < 0)
+ return CHARSET_RET_INVALID_INPUT;
+ } else if (!uni_utf8_get_valid_data(src, *src_size, dest)) {
+ return CHARSET_RET_INVALID_INPUT;
+ } else {
+ buffer_append(dest, src, *src_size);
+ }
+ return res;
+}
+
+int charset_to_utf8_begin(const char *charset, normalizer_func_t *normalizer,
+ struct charset_translation **t_r)
+{
+ return charset_utf8_vfuncs->to_utf8_begin(charset, normalizer, t_r);
+}
+
+void charset_to_utf8_end(struct charset_translation **_t)
+{
+ struct charset_translation *t = *_t;
+
+ *_t = NULL;
+ charset_utf8_vfuncs->to_utf8_end(t);
+}
+
+void charset_to_utf8_reset(struct charset_translation *t)
+{
+ charset_utf8_vfuncs->to_utf8_reset(t);
+}
+
+enum charset_result
+charset_to_utf8(struct charset_translation *t,
+ const unsigned char *src, size_t *src_size, buffer_t *dest)
+{
+ return charset_utf8_vfuncs->to_utf8(t, src, src_size, dest);
+}
diff --git a/src/lib-charset/charset-utf8.h b/src/lib-charset/charset-utf8.h
new file mode 100644
index 0000000..c17ab30
--- /dev/null
+++ b/src/lib-charset/charset-utf8.h
@@ -0,0 +1,53 @@
+#ifndef CHARSET_UTF8_H
+#define CHARSET_UTF8_H
+
+#include "unichar.h"
+
+/* Max number of bytes that iconv can require for a single character.
+ UTF-8 takes max 6 bytes per character. Not sure about others, but I'd think
+ 10 is more than enough for everyone.. */
+#define CHARSET_MAX_PENDING_BUF_SIZE 10
+
+struct charset_translation;
+
+enum charset_result {
+ CHARSET_RET_OK = 1,
+ CHARSET_RET_INCOMPLETE_INPUT = -1,
+ CHARSET_RET_INVALID_INPUT = -2
+};
+
+/* Begin translation to UTF-8. Returns -1 if charset is unknown. */
+int charset_to_utf8_begin(const char *charset, normalizer_func_t *normalizer,
+ struct charset_translation **t_r)
+ ATTR_NULL(2);
+/* Translate UTF-8 to UTF-8 while validating the input. */
+struct charset_translation *
+charset_utf8_to_utf8_begin(normalizer_func_t *normalizer);
+void charset_to_utf8_end(struct charset_translation **t);
+void charset_to_utf8_reset(struct charset_translation *t);
+
+/* Returns TRUE if charset is UTF-8 or ASCII */
+bool charset_is_utf8(const char *charset) ATTR_PURE;
+
+/* Translate src to UTF-8. src_size is updated to contain the number of
+ characters actually translated from src. The src_size should never shrink
+ more than CHARSET_MAX_PENDING_BUF_SIZE bytes.
+
+ If src contains invalid input, UNICODE_REPLACEMENT_CHAR is placed in such
+ positions and the invalid input is skipped over. Return value is also
+ CHARSET_RET_INCOMPLETE_INPUT in that case. */
+enum charset_result
+charset_to_utf8(struct charset_translation *t,
+ const unsigned char *src, size_t *src_size, buffer_t *dest);
+
+/* Translate a single string to UTF8. */
+int charset_to_utf8_str(const char *charset, normalizer_func_t *normalizer,
+ const char *input, string_t *output,
+ enum charset_result *result_r) ATTR_NULL(2);
+
+/* INTERNAL: */
+enum charset_result
+charset_utf8_to_utf8(normalizer_func_t *normalizer,
+ const unsigned char *src, size_t *src_size, buffer_t *dest);
+
+#endif
diff --git a/src/lib-charset/test-charset.c b/src/lib-charset/test-charset.c
new file mode 100644
index 0000000..2f9ba2b
--- /dev/null
+++ b/src/lib-charset/test-charset.c
@@ -0,0 +1,231 @@
+/* Copyright (c) 2015-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "istream.h"
+#include "str.h"
+#include "test-common.h"
+#include "charset-utf8.h"
+
+#include <unistd.h>
+
+static void test_charset_is_utf8(void)
+{
+ test_begin("charset_is_utf8");
+ test_assert(charset_is_utf8("AScII"));
+ test_assert(charset_is_utf8("us-AScII"));
+ test_assert(charset_is_utf8("uTF8"));
+ test_assert(charset_is_utf8("uTF-8"));
+ test_end();
+}
+
+static void test_charset_utf8_common(const char *input_charset)
+{
+ static const struct {
+ const char *input;
+ const char *output;
+ enum charset_result result;
+ } tests[] = {
+ { "p\xC3\xA4\xC3", "p\xC3\xA4", CHARSET_RET_INCOMPLETE_INPUT },
+ { "p\xC3\xA4\xC3""a", "p\xC3\xA4"UNICODE_REPLACEMENT_CHAR_UTF8"a", CHARSET_RET_INVALID_INPUT }
+ };
+ string_t *src, *str = t_str_new(256);
+ enum charset_result result;
+ unsigned int i;
+
+ for (i = 0; i < N_ELEMENTS(tests); i++) {
+ str_truncate(str, 0);
+ test_assert_idx(charset_to_utf8_str(input_charset, NULL,
+ tests[i].input, str, &result) == 0, i);
+ test_assert_idx(strcmp(tests[i].output, str_c(str)) == 0, i);
+ test_assert_idx(result == tests[i].result, i);
+ }
+ /* check that E2BIG handling works. We assume that iconv() is called
+ with 8192 byte buffer (tmpbuf[8192]) */
+ src = str_new(default_pool, 16384);
+ for (i = 0; i < 8190; i++)
+ str_append_c(src, 'a' + i % ('z'-'a'+1));
+ for (i = 0; i < 256; i++) {
+ str_truncate(str, 0);
+ str_append_c(src, 'A' + i % ('Z'-'A'+1));
+ test_assert_idx(charset_to_utf8_str(input_charset, NULL,
+ str_c(src), str, &result) == 0, i);
+ }
+ str_free(&src);
+}
+
+static void test_charset_utf8(void)
+{
+ test_begin("charset utf8");
+ test_charset_utf8_common("UTF-8");
+ test_end();
+}
+
+#ifdef HAVE_ICONV
+static void test_charset_iconv(void)
+{
+ static const struct {
+ const char *charset;
+ const char *input;
+ const char *output;
+ enum charset_result result;
+ } tests[] = {
+ { "ISO-8859-1", "p\xE4\xE4", "p\xC3\xA4\xC3\xA4", CHARSET_RET_OK },
+ { "UTF-7", "+AOQA5AD2AOQA9gDkAPYA5AD2AOQA9gDkAPYA5AD2AOQA9gDkAPYA5AD2AOQA9gDkAPYA5AD2AOQA9gDkAPYA5AD2AOQA9gDk",
+ "\xC3\xA4\xC3\xA4\xC3\xB6\xC3\xA4\xC3\xB6\xC3\xA4\xC3\xB6\xC3\xA4"
+ "\xC3\xB6\xC3\xA4\xC3\xB6\xC3\xA4\xC3\xB6\xC3\xA4\xC3\xB6\xC3\xA4"
+ "\xC3\xB6\xC3\xA4\xC3\xB6\xC3\xA4\xC3\xB6\xC3\xA4\xC3\xB6\xC3\xA4"
+ "\xC3\xB6\xC3\xA4\xC3\xB6\xC3\xA4\xC3\xB6\xC3\xA4\xC3\xB6\xC3\xA4"
+ "\xC3\xB6\xC3\xA4\xC3\xB6\xC3\xA4", CHARSET_RET_OK }
+ };
+ string_t *str = t_str_new(128);
+ struct charset_translation *trans;
+ enum charset_result result;
+ size_t pos, left, limit, len;
+ unsigned int i;
+
+ test_begin("charset iconv");
+ for (i = 0; i < N_ELEMENTS(tests); i++) {
+ str_truncate(str, 0);
+ test_assert_idx(charset_to_utf8_str(tests[i].charset, NULL,
+ tests[i].input, str, &result) == 0, i);
+ test_assert_idx(strcmp(tests[i].output, str_c(str)) == 0, i);
+ test_assert_idx(result == tests[i].result, i);
+
+ str_truncate(str, 0);
+ test_assert_idx(charset_to_utf8_begin(tests[i].charset, NULL, &trans) == 0, i);
+ len = strlen(tests[i].input);
+ for (pos = 0, limit = 1; limit <= len; pos += left, limit++) {
+ left = limit - pos;
+ result = charset_to_utf8(trans, (const void *)(tests[i].input + pos),
+ &left, str);
+ if (result != CHARSET_RET_INCOMPLETE_INPUT &&
+ result != CHARSET_RET_OK)
+ break;
+ }
+ test_assert_idx(strcmp(tests[i].output, str_c(str)) == 0, i);
+ test_assert_idx(result == tests[i].result, i);
+ charset_to_utf8_end(&trans);
+ }
+ /* Use //IGNORE just to force handling to be done by iconv
+ instead of our own UTF-8 routines. */
+ test_charset_utf8_common("UTF-8//TEST");
+ test_end();
+}
+static void test_charset_iconv_crashes(void)
+{
+ static const struct {
+ const char *charset;
+ const char *input;
+ } tests[] = {
+ { "CP932", "\203\334" }
+ };
+ string_t *str = t_str_new(128);
+ enum charset_result result;
+ unsigned int i;
+
+ test_begin("charset iconv crashes");
+ for (i = 0; i < N_ELEMENTS(tests); i++) {
+ str_truncate(str, 0);
+ /* we don't care about checking the result. we only want to
+ verify that there's no crash. */
+ (void)charset_to_utf8_str(tests[i].charset, NULL,
+ tests[i].input, str, &result);
+ }
+ test_end();
+}
+
+static void test_charset_iconv_utf7_state(void)
+{
+ struct charset_translation *trans;
+ string_t *str = t_str_new(32);
+ unsigned char nextbuf[5+CHARSET_MAX_PENDING_BUF_SIZE+1];
+ size_t size;
+
+ test_begin("charset iconv utf7 state");
+ test_assert(charset_to_utf8_begin("UTF-7", NULL, &trans) == 0);
+ size = 2;
+ test_assert(charset_to_utf8(trans, (const void *)"a+", &size, str) == CHARSET_RET_INCOMPLETE_INPUT);
+ test_assert(strcmp(str_c(str), "a") == 0);
+ test_assert(size == 1);
+ memset(nextbuf, '?', sizeof(nextbuf));
+ memcpy(nextbuf, "+AOQ-", 5);
+ size = sizeof(nextbuf);
+ test_assert(charset_to_utf8(trans, nextbuf, &size, str) == CHARSET_RET_OK);
+ test_assert(strcmp(str_c(str), "a\xC3\xA4???????????") == 0);
+ charset_to_utf8_end(&trans);
+ test_end();
+}
+#endif
+
+static int convert(const char *charset, const char *path)
+{
+ struct istream *input;
+ const unsigned char *data;
+ size_t size;
+ struct charset_translation *trans;
+ buffer_t *buf = buffer_create_dynamic(default_pool, IO_BLOCK_SIZE);
+ enum charset_result last_ret = CHARSET_RET_OK;
+ bool seen_invalid_input = FALSE;
+
+ input = path == NULL ? i_stream_create_fd(STDIN_FILENO, IO_BLOCK_SIZE) :
+ i_stream_create_file(path, IO_BLOCK_SIZE);
+
+ if (charset_to_utf8_begin(charset, NULL, &trans) < 0)
+ i_fatal("Failed to initialize charset '%s'", charset);
+
+ size_t need = 1;
+ while (i_stream_read_bytes(input, &data, &size, need) > 0) {
+ last_ret = charset_to_utf8(trans, data, &size, buf);
+ if (size > 0)
+ need = 1;
+ i_stream_skip(input, size);
+ switch (last_ret) {
+ case CHARSET_RET_OK:
+ break;
+ case CHARSET_RET_INCOMPLETE_INPUT:
+ need++;
+ break;
+ case CHARSET_RET_INVALID_INPUT:
+ seen_invalid_input = TRUE;
+ break;
+ }
+ if (write(STDOUT_FILENO, buf->data, buf->used) != (ssize_t)buf->used)
+ i_fatal("write(stdout) failed: %m");
+ buffer_set_used_size(buf, 0);
+ }
+ if (input->stream_errno != 0)
+ i_error("read() failed: %s", i_stream_get_error(input));
+ charset_to_utf8_end(&trans);
+ i_stream_destroy(&input);
+ buffer_free(&buf);
+
+ if (seen_invalid_input) {
+ i_error("Seen invalid input");
+ return 1;
+ }
+ if (last_ret == CHARSET_RET_INCOMPLETE_INPUT) {
+ i_error("Incomplete input");
+ return 2;
+ }
+ return 0;
+}
+
+int main(int argc, char *argv[])
+{
+ static void (*const test_functions[])(void) = {
+ test_charset_is_utf8,
+ test_charset_utf8,
+#ifdef HAVE_ICONV
+ test_charset_iconv,
+ test_charset_iconv_crashes,
+ test_charset_iconv_utf7_state,
+#endif
+ NULL
+ };
+
+ if (argc >= 2) {
+ /* <charset> [<input path>] */
+ return convert(argv[1], argv[2]);
+ }
+ return test_run(test_functions);
+}
diff --git a/src/lib-compression/Makefile.am b/src/lib-compression/Makefile.am
new file mode 100644
index 0000000..f689b0c
--- /dev/null
+++ b/src/lib-compression/Makefile.am
@@ -0,0 +1,61 @@
+noinst_LTLIBRARIES = libcompression.la
+
+AM_CPPFLAGS = \
+ -I$(top_srcdir)/src/lib \
+ -I$(top_srcdir)/src/lib-test \
+ $(ZSTD_CFLAGS)
+
+libcompression_la_SOURCES = \
+ compression.c \
+ istream-decompress.c \
+ istream-lzma.c \
+ istream-lz4.c \
+ istream-zlib.c \
+ istream-bzlib.c \
+ istream-zstd.c \
+ ostream-lz4.c \
+ ostream-zlib.c \
+ ostream-bzlib.c \
+ ostream-zstd.c
+libcompression_la_LIBADD = \
+ $(COMPRESS_LIBS)
+
+pkginc_libdir = $(pkgincludedir)
+pkginc_lib_HEADERS = \
+ compression.h \
+ iostream-lz4.h \
+ istream-zlib.h \
+ ostream-zlib.h
+
+noinst_HEADERS = \
+ iostream-zstd-private.h
+
+pkglib_LTLIBRARIES = libdovecot-compression.la
+libdovecot_compression_la_SOURCES =
+libdovecot_compression_la_LIBADD = libcompression.la ../lib-dovecot/libdovecot.la $(COMPRESS_LIBS)
+libdovecot_compression_la_DEPENDENCIES = libcompression.la ../lib-dovecot/libdovecot.la
+libdovecot_compression_la_LDFLAGS = -export-dynamic
+
+test_programs = \
+ test-compression
+
+noinst_PROGRAMS = $(test_programs) bench-compression
+
+test_libs = \
+ $(noinst_LTLIBRARIES) \
+ ../lib-test/libtest.la \
+ ../lib/liblib.la
+test_deps = $(test_libs)
+
+test_compression_SOURCES = test-compression.c
+test_compression_LDADD = $(test_libs)
+test_compression_DEPENDENCIES = $(test_deps)
+
+bench_compression_SOURCES = bench-compression.c
+bench_compression_LDADD = $(test_libs)
+bench_compression_DEPENDENCIES = $(test_deps)
+
+check-local:
+ for bin in $(test_programs); do \
+ if ! $(RUN_TEST) ./$$bin; then exit 1; fi; \
+ done
diff --git a/src/lib-compression/Makefile.in b/src/lib-compression/Makefile.in
new file mode 100644
index 0000000..7badd65
--- /dev/null
+++ b/src/lib-compression/Makefile.in
@@ -0,0 +1,976 @@
+# Makefile.in generated by automake 1.16.1 from Makefile.am.
+# @configure_input@
+
+# Copyright (C) 1994-2018 Free Software Foundation, Inc.
+
+# This Makefile.in is free software; the Free Software Foundation
+# gives unlimited permission to copy and/or distribute it,
+# with or without modifications, as long as this notice is preserved.
+
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY, to the extent permitted by law; without
+# even the implied warranty of MERCHANTABILITY or FITNESS FOR A
+# PARTICULAR PURPOSE.
+
+@SET_MAKE@
+
+
+
+VPATH = @srcdir@
+am__is_gnu_make = { \
+ if test -z '$(MAKELEVEL)'; then \
+ false; \
+ elif test -n '$(MAKE_HOST)'; then \
+ true; \
+ elif test -n '$(MAKE_VERSION)' && test -n '$(CURDIR)'; then \
+ true; \
+ else \
+ false; \
+ fi; \
+}
+am__make_running_with_option = \
+ case $${target_option-} in \
+ ?) ;; \
+ *) echo "am__make_running_with_option: internal error: invalid" \
+ "target option '$${target_option-}' specified" >&2; \
+ exit 1;; \
+ esac; \
+ has_opt=no; \
+ sane_makeflags=$$MAKEFLAGS; \
+ if $(am__is_gnu_make); then \
+ sane_makeflags=$$MFLAGS; \
+ else \
+ case $$MAKEFLAGS in \
+ *\\[\ \ ]*) \
+ bs=\\; \
+ sane_makeflags=`printf '%s\n' "$$MAKEFLAGS" \
+ | sed "s/$$bs$$bs[$$bs $$bs ]*//g"`;; \
+ esac; \
+ fi; \
+ skip_next=no; \
+ strip_trailopt () \
+ { \
+ flg=`printf '%s\n' "$$flg" | sed "s/$$1.*$$//"`; \
+ }; \
+ for flg in $$sane_makeflags; do \
+ test $$skip_next = yes && { skip_next=no; continue; }; \
+ case $$flg in \
+ *=*|--*) continue;; \
+ -*I) strip_trailopt 'I'; skip_next=yes;; \
+ -*I?*) strip_trailopt 'I';; \
+ -*O) strip_trailopt 'O'; skip_next=yes;; \
+ -*O?*) strip_trailopt 'O';; \
+ -*l) strip_trailopt 'l'; skip_next=yes;; \
+ -*l?*) strip_trailopt 'l';; \
+ -[dEDm]) skip_next=yes;; \
+ -[JT]) skip_next=yes;; \
+ esac; \
+ case $$flg in \
+ *$$target_option*) has_opt=yes; break;; \
+ esac; \
+ done; \
+ test $$has_opt = yes
+am__make_dryrun = (target_option=n; $(am__make_running_with_option))
+am__make_keepgoing = (target_option=k; $(am__make_running_with_option))
+pkgdatadir = $(datadir)/@PACKAGE@
+pkgincludedir = $(includedir)/@PACKAGE@
+pkglibdir = $(libdir)/@PACKAGE@
+pkglibexecdir = $(libexecdir)/@PACKAGE@
+am__cd = CDPATH="$${ZSH_VERSION+.}$(PATH_SEPARATOR)" && cd
+install_sh_DATA = $(install_sh) -c -m 644
+install_sh_PROGRAM = $(install_sh) -c
+install_sh_SCRIPT = $(install_sh) -c
+INSTALL_HEADER = $(INSTALL_DATA)
+transform = $(program_transform_name)
+NORMAL_INSTALL = :
+PRE_INSTALL = :
+POST_INSTALL = :
+NORMAL_UNINSTALL = :
+PRE_UNINSTALL = :
+POST_UNINSTALL = :
+build_triplet = @build@
+host_triplet = @host@
+noinst_PROGRAMS = $(am__EXEEXT_1) bench-compression$(EXEEXT)
+subdir = src/lib-compression
+ACLOCAL_M4 = $(top_srcdir)/aclocal.m4
+am__aclocal_m4_deps = $(top_srcdir)/m4/ac_checktype2.m4 \
+ $(top_srcdir)/m4/ac_typeof.m4 $(top_srcdir)/m4/arc4random.m4 \
+ $(top_srcdir)/m4/blockdev.m4 $(top_srcdir)/m4/c99_vsnprintf.m4 \
+ $(top_srcdir)/m4/clock_gettime.m4 $(top_srcdir)/m4/crypt.m4 \
+ $(top_srcdir)/m4/crypt_xpg6.m4 $(top_srcdir)/m4/dbqlk.m4 \
+ $(top_srcdir)/m4/dirent_dtype.m4 $(top_srcdir)/m4/dovecot.m4 \
+ $(top_srcdir)/m4/fd_passing.m4 $(top_srcdir)/m4/fdatasync.m4 \
+ $(top_srcdir)/m4/flexible_array_member.m4 \
+ $(top_srcdir)/m4/glibc.m4 $(top_srcdir)/m4/gmtime_max.m4 \
+ $(top_srcdir)/m4/gmtime_tm_gmtoff.m4 \
+ $(top_srcdir)/m4/ioloop.m4 $(top_srcdir)/m4/iovec.m4 \
+ $(top_srcdir)/m4/ipv6.m4 $(top_srcdir)/m4/libcap.m4 \
+ $(top_srcdir)/m4/libtool.m4 $(top_srcdir)/m4/libwrap.m4 \
+ $(top_srcdir)/m4/linux_mremap.m4 $(top_srcdir)/m4/ltoptions.m4 \
+ $(top_srcdir)/m4/ltsugar.m4 $(top_srcdir)/m4/ltversion.m4 \
+ $(top_srcdir)/m4/lt~obsolete.m4 $(top_srcdir)/m4/mmap_write.m4 \
+ $(top_srcdir)/m4/mntctl.m4 $(top_srcdir)/m4/modules.m4 \
+ $(top_srcdir)/m4/notify.m4 $(top_srcdir)/m4/nsl.m4 \
+ $(top_srcdir)/m4/off_t_max.m4 $(top_srcdir)/m4/pkg.m4 \
+ $(top_srcdir)/m4/pr_set_dumpable.m4 \
+ $(top_srcdir)/m4/q_quotactl.m4 $(top_srcdir)/m4/quota.m4 \
+ $(top_srcdir)/m4/random.m4 $(top_srcdir)/m4/rlimit.m4 \
+ $(top_srcdir)/m4/sendfile.m4 $(top_srcdir)/m4/size_t_signed.m4 \
+ $(top_srcdir)/m4/sockpeercred.m4 $(top_srcdir)/m4/sql.m4 \
+ $(top_srcdir)/m4/ssl.m4 $(top_srcdir)/m4/st_tim.m4 \
+ $(top_srcdir)/m4/static_array.m4 $(top_srcdir)/m4/test_with.m4 \
+ $(top_srcdir)/m4/time_t.m4 $(top_srcdir)/m4/typeof.m4 \
+ $(top_srcdir)/m4/typeof_dev_t.m4 \
+ $(top_srcdir)/m4/uoff_t_max.m4 $(top_srcdir)/m4/vararg.m4 \
+ $(top_srcdir)/m4/want_apparmor.m4 \
+ $(top_srcdir)/m4/want_bsdauth.m4 \
+ $(top_srcdir)/m4/want_bzlib.m4 \
+ $(top_srcdir)/m4/want_cassandra.m4 \
+ $(top_srcdir)/m4/want_cdb.m4 \
+ $(top_srcdir)/m4/want_checkpassword.m4 \
+ $(top_srcdir)/m4/want_clucene.m4 $(top_srcdir)/m4/want_db.m4 \
+ $(top_srcdir)/m4/want_gssapi.m4 $(top_srcdir)/m4/want_icu.m4 \
+ $(top_srcdir)/m4/want_ldap.m4 $(top_srcdir)/m4/want_lua.m4 \
+ $(top_srcdir)/m4/want_lz4.m4 $(top_srcdir)/m4/want_lzma.m4 \
+ $(top_srcdir)/m4/want_mysql.m4 $(top_srcdir)/m4/want_pam.m4 \
+ $(top_srcdir)/m4/want_passwd.m4 $(top_srcdir)/m4/want_pgsql.m4 \
+ $(top_srcdir)/m4/want_prefetch.m4 \
+ $(top_srcdir)/m4/want_shadow.m4 \
+ $(top_srcdir)/m4/want_sodium.m4 $(top_srcdir)/m4/want_solr.m4 \
+ $(top_srcdir)/m4/want_sqlite.m4 \
+ $(top_srcdir)/m4/want_stemmer.m4 \
+ $(top_srcdir)/m4/want_systemd.m4 \
+ $(top_srcdir)/m4/want_textcat.m4 \
+ $(top_srcdir)/m4/want_unwind.m4 $(top_srcdir)/m4/want_zlib.m4 \
+ $(top_srcdir)/m4/want_zstd.m4 $(top_srcdir)/configure.ac
+am__configure_deps = $(am__aclocal_m4_deps) $(CONFIGURE_DEPENDENCIES) \
+ $(ACLOCAL_M4)
+DIST_COMMON = $(srcdir)/Makefile.am $(noinst_HEADERS) \
+ $(pkginc_lib_HEADERS) $(am__DIST_COMMON)
+mkinstalldirs = $(install_sh) -d
+CONFIG_HEADER = $(top_builddir)/config.h
+CONFIG_CLEAN_FILES =
+CONFIG_CLEAN_VPATH_FILES =
+am__EXEEXT_1 = test-compression$(EXEEXT)
+PROGRAMS = $(noinst_PROGRAMS)
+am__vpath_adj_setup = srcdirstrip=`echo "$(srcdir)" | sed 's|.|.|g'`;
+am__vpath_adj = case $$p in \
+ $(srcdir)/*) f=`echo "$$p" | sed "s|^$$srcdirstrip/||"`;; \
+ *) f=$$p;; \
+ esac;
+am__strip_dir = f=`echo $$p | sed -e 's|^.*/||'`;
+am__install_max = 40
+am__nobase_strip_setup = \
+ srcdirstrip=`echo "$(srcdir)" | sed 's/[].[^$$\\*|]/\\\\&/g'`
+am__nobase_strip = \
+ for p in $$list; do echo "$$p"; done | sed -e "s|$$srcdirstrip/||"
+am__nobase_list = $(am__nobase_strip_setup); \
+ for p in $$list; do echo "$$p $$p"; done | \
+ sed "s| $$srcdirstrip/| |;"' / .*\//!s/ .*/ ./; s,\( .*\)/[^/]*$$,\1,' | \
+ $(AWK) 'BEGIN { files["."] = "" } { files[$$2] = files[$$2] " " $$1; \
+ if (++n[$$2] == $(am__install_max)) \
+ { print $$2, files[$$2]; n[$$2] = 0; files[$$2] = "" } } \
+ END { for (dir in files) print dir, files[dir] }'
+am__base_list = \
+ sed '$$!N;$$!N;$$!N;$$!N;$$!N;$$!N;$$!N;s/\n/ /g' | \
+ sed '$$!N;$$!N;$$!N;$$!N;s/\n/ /g'
+am__uninstall_files_from_dir = { \
+ test -z "$$files" \
+ || { test ! -d "$$dir" && test ! -f "$$dir" && test ! -r "$$dir"; } \
+ || { echo " ( cd '$$dir' && rm -f" $$files ")"; \
+ $(am__cd) "$$dir" && rm -f $$files; }; \
+ }
+am__installdirs = "$(DESTDIR)$(pkglibdir)" \
+ "$(DESTDIR)$(pkginc_libdir)"
+LTLIBRARIES = $(noinst_LTLIBRARIES) $(pkglib_LTLIBRARIES)
+am__DEPENDENCIES_1 =
+libcompression_la_DEPENDENCIES = $(am__DEPENDENCIES_1)
+am_libcompression_la_OBJECTS = compression.lo istream-decompress.lo \
+ istream-lzma.lo istream-lz4.lo istream-zlib.lo \
+ istream-bzlib.lo istream-zstd.lo ostream-lz4.lo \
+ ostream-zlib.lo ostream-bzlib.lo ostream-zstd.lo
+libcompression_la_OBJECTS = $(am_libcompression_la_OBJECTS)
+AM_V_lt = $(am__v_lt_@AM_V@)
+am__v_lt_ = $(am__v_lt_@AM_DEFAULT_V@)
+am__v_lt_0 = --silent
+am__v_lt_1 =
+am_libdovecot_compression_la_OBJECTS =
+libdovecot_compression_la_OBJECTS = \
+ $(am_libdovecot_compression_la_OBJECTS)
+libdovecot_compression_la_LINK = $(LIBTOOL) $(AM_V_lt) --tag=CC \
+ $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=link $(CCLD) \
+ $(AM_CFLAGS) $(CFLAGS) $(libdovecot_compression_la_LDFLAGS) \
+ $(LDFLAGS) -o $@
+am_bench_compression_OBJECTS = bench-compression.$(OBJEXT)
+bench_compression_OBJECTS = $(am_bench_compression_OBJECTS)
+am_test_compression_OBJECTS = test-compression.$(OBJEXT)
+test_compression_OBJECTS = $(am_test_compression_OBJECTS)
+AM_V_P = $(am__v_P_@AM_V@)
+am__v_P_ = $(am__v_P_@AM_DEFAULT_V@)
+am__v_P_0 = false
+am__v_P_1 = :
+AM_V_GEN = $(am__v_GEN_@AM_V@)
+am__v_GEN_ = $(am__v_GEN_@AM_DEFAULT_V@)
+am__v_GEN_0 = @echo " GEN " $@;
+am__v_GEN_1 =
+AM_V_at = $(am__v_at_@AM_V@)
+am__v_at_ = $(am__v_at_@AM_DEFAULT_V@)
+am__v_at_0 = @
+am__v_at_1 =
+DEFAULT_INCLUDES = -I.@am__isrc@ -I$(top_builddir)
+depcomp = $(SHELL) $(top_srcdir)/depcomp
+am__maybe_remake_depfiles = depfiles
+am__depfiles_remade = ./$(DEPDIR)/bench-compression.Po \
+ ./$(DEPDIR)/compression.Plo ./$(DEPDIR)/istream-bzlib.Plo \
+ ./$(DEPDIR)/istream-decompress.Plo ./$(DEPDIR)/istream-lz4.Plo \
+ ./$(DEPDIR)/istream-lzma.Plo ./$(DEPDIR)/istream-zlib.Plo \
+ ./$(DEPDIR)/istream-zstd.Plo ./$(DEPDIR)/ostream-bzlib.Plo \
+ ./$(DEPDIR)/ostream-lz4.Plo ./$(DEPDIR)/ostream-zlib.Plo \
+ ./$(DEPDIR)/ostream-zstd.Plo ./$(DEPDIR)/test-compression.Po
+am__mv = mv -f
+COMPILE = $(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) \
+ $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS)
+LTCOMPILE = $(LIBTOOL) $(AM_V_lt) --tag=CC $(AM_LIBTOOLFLAGS) \
+ $(LIBTOOLFLAGS) --mode=compile $(CC) $(DEFS) \
+ $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) $(CPPFLAGS) \
+ $(AM_CFLAGS) $(CFLAGS)
+AM_V_CC = $(am__v_CC_@AM_V@)
+am__v_CC_ = $(am__v_CC_@AM_DEFAULT_V@)
+am__v_CC_0 = @echo " CC " $@;
+am__v_CC_1 =
+CCLD = $(CC)
+LINK = $(LIBTOOL) $(AM_V_lt) --tag=CC $(AM_LIBTOOLFLAGS) \
+ $(LIBTOOLFLAGS) --mode=link $(CCLD) $(AM_CFLAGS) $(CFLAGS) \
+ $(AM_LDFLAGS) $(LDFLAGS) -o $@
+AM_V_CCLD = $(am__v_CCLD_@AM_V@)
+am__v_CCLD_ = $(am__v_CCLD_@AM_DEFAULT_V@)
+am__v_CCLD_0 = @echo " CCLD " $@;
+am__v_CCLD_1 =
+SOURCES = $(libcompression_la_SOURCES) \
+ $(libdovecot_compression_la_SOURCES) \
+ $(bench_compression_SOURCES) $(test_compression_SOURCES)
+DIST_SOURCES = $(libcompression_la_SOURCES) \
+ $(libdovecot_compression_la_SOURCES) \
+ $(bench_compression_SOURCES) $(test_compression_SOURCES)
+am__can_run_installinfo = \
+ case $$AM_UPDATE_INFO_DIR in \
+ n|no|NO) false;; \
+ *) (install-info --version) >/dev/null 2>&1;; \
+ esac
+HEADERS = $(noinst_HEADERS) $(pkginc_lib_HEADERS)
+am__tagged_files = $(HEADERS) $(SOURCES) $(TAGS_FILES) $(LISP)
+# Read a list of newline-separated strings from the standard input,
+# and print each of them once, without duplicates. Input order is
+# *not* preserved.
+am__uniquify_input = $(AWK) '\
+ BEGIN { nonempty = 0; } \
+ { items[$$0] = 1; nonempty = 1; } \
+ END { if (nonempty) { for (i in items) print i; }; } \
+'
+# Make sure the list of sources is unique. This is necessary because,
+# e.g., the same source file might be shared among _SOURCES variables
+# for different programs/libraries.
+am__define_uniq_tagged_files = \
+ list='$(am__tagged_files)'; \
+ unique=`for i in $$list; do \
+ if test -f "$$i"; then echo $$i; else echo $(srcdir)/$$i; fi; \
+ done | $(am__uniquify_input)`
+ETAGS = etags
+CTAGS = ctags
+am__DIST_COMMON = $(srcdir)/Makefile.in $(top_srcdir)/depcomp
+DISTFILES = $(DIST_COMMON) $(DIST_SOURCES) $(TEXINFOS) $(EXTRA_DIST)
+ACLOCAL = @ACLOCAL@
+ACLOCAL_AMFLAGS = @ACLOCAL_AMFLAGS@
+AMTAR = @AMTAR@
+AM_DEFAULT_VERBOSITY = @AM_DEFAULT_VERBOSITY@
+APPARMOR_LIBS = @APPARMOR_LIBS@
+AR = @AR@
+AUTH_CFLAGS = @AUTH_CFLAGS@
+AUTH_LIBS = @AUTH_LIBS@
+AUTOCONF = @AUTOCONF@
+AUTOHEADER = @AUTOHEADER@
+AUTOMAKE = @AUTOMAKE@
+AWK = @AWK@
+BINARY_CFLAGS = @BINARY_CFLAGS@
+BINARY_LDFLAGS = @BINARY_LDFLAGS@
+BISON = @BISON@
+CASSANDRA_CFLAGS = @CASSANDRA_CFLAGS@
+CASSANDRA_LIBS = @CASSANDRA_LIBS@
+CC = @CC@
+CCDEPMODE = @CCDEPMODE@
+CDB_LIBS = @CDB_LIBS@
+CFLAGS = @CFLAGS@
+CLUCENE_CFLAGS = @CLUCENE_CFLAGS@
+CLUCENE_LIBS = @CLUCENE_LIBS@
+COMPRESS_LIBS = @COMPRESS_LIBS@
+CPP = @CPP@
+CPPFLAGS = @CPPFLAGS@
+CRYPT_LIBS = @CRYPT_LIBS@
+CXX = @CXX@
+CXXCPP = @CXXCPP@
+CXXDEPMODE = @CXXDEPMODE@
+CXXFLAGS = @CXXFLAGS@
+CYGPATH_W = @CYGPATH_W@
+DEFS = @DEFS@
+DEPDIR = @DEPDIR@
+DICT_LIBS = @DICT_LIBS@
+DLLIB = @DLLIB@
+DLLTOOL = @DLLTOOL@
+DSYMUTIL = @DSYMUTIL@
+DUMPBIN = @DUMPBIN@
+ECHO_C = @ECHO_C@
+ECHO_N = @ECHO_N@
+ECHO_T = @ECHO_T@
+EGREP = @EGREP@
+EXEEXT = @EXEEXT@
+FGREP = @FGREP@
+FLEX = @FLEX@
+FUZZER_CPPFLAGS = @FUZZER_CPPFLAGS@
+FUZZER_LDFLAGS = @FUZZER_LDFLAGS@
+GREP = @GREP@
+INSTALL = @INSTALL@
+INSTALL_DATA = @INSTALL_DATA@
+INSTALL_PROGRAM = @INSTALL_PROGRAM@
+INSTALL_SCRIPT = @INSTALL_SCRIPT@
+INSTALL_STRIP_PROGRAM = @INSTALL_STRIP_PROGRAM@
+KRB5CONFIG = @KRB5CONFIG@
+KRB5_CFLAGS = @KRB5_CFLAGS@
+KRB5_LIBS = @KRB5_LIBS@
+LD = @LD@
+LDAP_LIBS = @LDAP_LIBS@
+LDFLAGS = @LDFLAGS@
+LD_NO_WHOLE_ARCHIVE = @LD_NO_WHOLE_ARCHIVE@
+LD_WHOLE_ARCHIVE = @LD_WHOLE_ARCHIVE@
+LIBCAP = @LIBCAP@
+LIBDOVECOT = @LIBDOVECOT@
+LIBDOVECOT_COMPRESS = @LIBDOVECOT_COMPRESS@
+LIBDOVECOT_DEPS = @LIBDOVECOT_DEPS@
+LIBDOVECOT_DSYNC = @LIBDOVECOT_DSYNC@
+LIBDOVECOT_LA_LIBS = @LIBDOVECOT_LA_LIBS@
+LIBDOVECOT_LDA = @LIBDOVECOT_LDA@
+LIBDOVECOT_LDAP = @LIBDOVECOT_LDAP@
+LIBDOVECOT_LIBFTS = @LIBDOVECOT_LIBFTS@
+LIBDOVECOT_LIBFTS_DEPS = @LIBDOVECOT_LIBFTS_DEPS@
+LIBDOVECOT_LOGIN = @LIBDOVECOT_LOGIN@
+LIBDOVECOT_LUA = @LIBDOVECOT_LUA@
+LIBDOVECOT_LUA_DEPS = @LIBDOVECOT_LUA_DEPS@
+LIBDOVECOT_SQL = @LIBDOVECOT_SQL@
+LIBDOVECOT_STORAGE = @LIBDOVECOT_STORAGE@
+LIBDOVECOT_STORAGE_DEPS = @LIBDOVECOT_STORAGE_DEPS@
+LIBEXTTEXTCAT_CFLAGS = @LIBEXTTEXTCAT_CFLAGS@
+LIBEXTTEXTCAT_LIBS = @LIBEXTTEXTCAT_LIBS@
+LIBICONV = @LIBICONV@
+LIBICU_CFLAGS = @LIBICU_CFLAGS@
+LIBICU_LIBS = @LIBICU_LIBS@
+LIBOBJS = @LIBOBJS@
+LIBS = @LIBS@
+LIBSODIUM_CFLAGS = @LIBSODIUM_CFLAGS@
+LIBSODIUM_LIBS = @LIBSODIUM_LIBS@
+LIBTIRPC_CFLAGS = @LIBTIRPC_CFLAGS@
+LIBTIRPC_LIBS = @LIBTIRPC_LIBS@
+LIBTOOL = @LIBTOOL@
+LIBUNWIND_CFLAGS = @LIBUNWIND_CFLAGS@
+LIBUNWIND_LIBS = @LIBUNWIND_LIBS@
+LIBWRAP_LIBS = @LIBWRAP_LIBS@
+LINKED_STORAGE_LDADD = @LINKED_STORAGE_LDADD@
+LIPO = @LIPO@
+LN_S = @LN_S@
+LTLIBICONV = @LTLIBICONV@
+LTLIBOBJS = @LTLIBOBJS@
+LT_SYS_LIBRARY_PATH = @LT_SYS_LIBRARY_PATH@
+LUA_CFLAGS = @LUA_CFLAGS@
+LUA_LIBS = @LUA_LIBS@
+MAINT = @MAINT@
+MAKEINFO = @MAKEINFO@
+MANIFEST_TOOL = @MANIFEST_TOOL@
+MKDIR_P = @MKDIR_P@
+MODULE_LIBS = @MODULE_LIBS@
+MODULE_SUFFIX = @MODULE_SUFFIX@
+MYSQL_CFLAGS = @MYSQL_CFLAGS@
+MYSQL_CONFIG = @MYSQL_CONFIG@
+MYSQL_LIBS = @MYSQL_LIBS@
+NM = @NM@
+NMEDIT = @NMEDIT@
+NOPLUGIN_LDFLAGS = @NOPLUGIN_LDFLAGS@
+OBJDUMP = @OBJDUMP@
+OBJEXT = @OBJEXT@
+OTOOL = @OTOOL@
+OTOOL64 = @OTOOL64@
+PACKAGE = @PACKAGE@
+PACKAGE_BUGREPORT = @PACKAGE_BUGREPORT@
+PACKAGE_NAME = @PACKAGE_NAME@
+PACKAGE_STRING = @PACKAGE_STRING@
+PACKAGE_TARNAME = @PACKAGE_TARNAME@
+PACKAGE_URL = @PACKAGE_URL@
+PACKAGE_VERSION = @PACKAGE_VERSION@
+PANDOC = @PANDOC@
+PATH_SEPARATOR = @PATH_SEPARATOR@
+PGSQL_CFLAGS = @PGSQL_CFLAGS@
+PGSQL_LIBS = @PGSQL_LIBS@
+PG_CONFIG = @PG_CONFIG@
+PIE_CFLAGS = @PIE_CFLAGS@
+PIE_LDFLAGS = @PIE_LDFLAGS@
+PKG_CONFIG = @PKG_CONFIG@
+PKG_CONFIG_LIBDIR = @PKG_CONFIG_LIBDIR@
+PKG_CONFIG_PATH = @PKG_CONFIG_PATH@
+QUOTA_LIBS = @QUOTA_LIBS@
+RANLIB = @RANLIB@
+RELRO_LDFLAGS = @RELRO_LDFLAGS@
+RPCGEN = @RPCGEN@
+RUN_TEST = @RUN_TEST@
+SED = @SED@
+SETTING_FILES = @SETTING_FILES@
+SET_MAKE = @SET_MAKE@
+SHELL = @SHELL@
+SQLITE_CFLAGS = @SQLITE_CFLAGS@
+SQLITE_LIBS = @SQLITE_LIBS@
+SQL_CFLAGS = @SQL_CFLAGS@
+SQL_LIBS = @SQL_LIBS@
+SSL_CFLAGS = @SSL_CFLAGS@
+SSL_LIBS = @SSL_LIBS@
+STRIP = @STRIP@
+SYSTEMD_CFLAGS = @SYSTEMD_CFLAGS@
+SYSTEMD_LIBS = @SYSTEMD_LIBS@
+VALGRIND = @VALGRIND@
+VERSION = @VERSION@
+ZSTD_CFLAGS = @ZSTD_CFLAGS@
+ZSTD_LIBS = @ZSTD_LIBS@
+abs_builddir = @abs_builddir@
+abs_srcdir = @abs_srcdir@
+abs_top_builddir = @abs_top_builddir@
+abs_top_srcdir = @abs_top_srcdir@
+ac_ct_AR = @ac_ct_AR@
+ac_ct_CC = @ac_ct_CC@
+ac_ct_CXX = @ac_ct_CXX@
+ac_ct_DUMPBIN = @ac_ct_DUMPBIN@
+am__include = @am__include@
+am__leading_dot = @am__leading_dot@
+am__quote = @am__quote@
+am__tar = @am__tar@
+am__untar = @am__untar@
+bindir = @bindir@
+build = @build@
+build_alias = @build_alias@
+build_cpu = @build_cpu@
+build_os = @build_os@
+build_vendor = @build_vendor@
+builddir = @builddir@
+datadir = @datadir@
+datarootdir = @datarootdir@
+dict_drivers = @dict_drivers@
+docdir = @docdir@
+dvidir = @dvidir@
+exec_prefix = @exec_prefix@
+host = @host@
+host_alias = @host_alias@
+host_cpu = @host_cpu@
+host_os = @host_os@
+host_vendor = @host_vendor@
+htmldir = @htmldir@
+includedir = @includedir@
+infodir = @infodir@
+install_sh = @install_sh@
+libdir = @libdir@
+libexecdir = @libexecdir@
+localedir = @localedir@
+localstatedir = @localstatedir@
+mandir = @mandir@
+mkdir_p = @mkdir_p@
+moduledir = @moduledir@
+oldincludedir = @oldincludedir@
+pdfdir = @pdfdir@
+prefix = @prefix@
+program_transform_name = @program_transform_name@
+psdir = @psdir@
+rundir = @rundir@
+runstatedir = @runstatedir@
+sbindir = @sbindir@
+sharedstatedir = @sharedstatedir@
+sql_drivers = @sql_drivers@
+srcdir = @srcdir@
+ssldir = @ssldir@
+statedir = @statedir@
+sysconfdir = @sysconfdir@
+systemdservicetype = @systemdservicetype@
+systemdsystemunitdir = @systemdsystemunitdir@
+target_alias = @target_alias@
+top_build_prefix = @top_build_prefix@
+top_builddir = @top_builddir@
+top_srcdir = @top_srcdir@
+noinst_LTLIBRARIES = libcompression.la
+AM_CPPFLAGS = \
+ -I$(top_srcdir)/src/lib \
+ -I$(top_srcdir)/src/lib-test \
+ $(ZSTD_CFLAGS)
+
+libcompression_la_SOURCES = \
+ compression.c \
+ istream-decompress.c \
+ istream-lzma.c \
+ istream-lz4.c \
+ istream-zlib.c \
+ istream-bzlib.c \
+ istream-zstd.c \
+ ostream-lz4.c \
+ ostream-zlib.c \
+ ostream-bzlib.c \
+ ostream-zstd.c
+
+libcompression_la_LIBADD = \
+ $(COMPRESS_LIBS)
+
+pkginc_libdir = $(pkgincludedir)
+pkginc_lib_HEADERS = \
+ compression.h \
+ iostream-lz4.h \
+ istream-zlib.h \
+ ostream-zlib.h
+
+noinst_HEADERS = \
+ iostream-zstd-private.h
+
+pkglib_LTLIBRARIES = libdovecot-compression.la
+libdovecot_compression_la_SOURCES =
+libdovecot_compression_la_LIBADD = libcompression.la ../lib-dovecot/libdovecot.la $(COMPRESS_LIBS)
+libdovecot_compression_la_DEPENDENCIES = libcompression.la ../lib-dovecot/libdovecot.la
+libdovecot_compression_la_LDFLAGS = -export-dynamic
+test_programs = \
+ test-compression
+
+test_libs = \
+ $(noinst_LTLIBRARIES) \
+ ../lib-test/libtest.la \
+ ../lib/liblib.la
+
+test_deps = $(test_libs)
+test_compression_SOURCES = test-compression.c
+test_compression_LDADD = $(test_libs)
+test_compression_DEPENDENCIES = $(test_deps)
+bench_compression_SOURCES = bench-compression.c
+bench_compression_LDADD = $(test_libs)
+bench_compression_DEPENDENCIES = $(test_deps)
+all: all-am
+
+.SUFFIXES:
+.SUFFIXES: .c .lo .o .obj
+$(srcdir)/Makefile.in: @MAINTAINER_MODE_TRUE@ $(srcdir)/Makefile.am $(am__configure_deps)
+ @for dep in $?; do \
+ case '$(am__configure_deps)' in \
+ *$$dep*) \
+ ( cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh ) \
+ && { if test -f $@; then exit 0; else break; fi; }; \
+ exit 1;; \
+ esac; \
+ done; \
+ echo ' cd $(top_srcdir) && $(AUTOMAKE) --foreign src/lib-compression/Makefile'; \
+ $(am__cd) $(top_srcdir) && \
+ $(AUTOMAKE) --foreign src/lib-compression/Makefile
+Makefile: $(srcdir)/Makefile.in $(top_builddir)/config.status
+ @case '$?' in \
+ *config.status*) \
+ cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh;; \
+ *) \
+ echo ' cd $(top_builddir) && $(SHELL) ./config.status $(subdir)/$@ $(am__maybe_remake_depfiles)'; \
+ cd $(top_builddir) && $(SHELL) ./config.status $(subdir)/$@ $(am__maybe_remake_depfiles);; \
+ esac;
+
+$(top_builddir)/config.status: $(top_srcdir)/configure $(CONFIG_STATUS_DEPENDENCIES)
+ cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh
+
+$(top_srcdir)/configure: @MAINTAINER_MODE_TRUE@ $(am__configure_deps)
+ cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh
+$(ACLOCAL_M4): @MAINTAINER_MODE_TRUE@ $(am__aclocal_m4_deps)
+ cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh
+$(am__aclocal_m4_deps):
+
+clean-noinstPROGRAMS:
+ @list='$(noinst_PROGRAMS)'; test -n "$$list" || exit 0; \
+ echo " rm -f" $$list; \
+ rm -f $$list || exit $$?; \
+ test -n "$(EXEEXT)" || exit 0; \
+ list=`for p in $$list; do echo "$$p"; done | sed 's/$(EXEEXT)$$//'`; \
+ echo " rm -f" $$list; \
+ rm -f $$list
+
+clean-noinstLTLIBRARIES:
+ -test -z "$(noinst_LTLIBRARIES)" || rm -f $(noinst_LTLIBRARIES)
+ @list='$(noinst_LTLIBRARIES)'; \
+ locs=`for p in $$list; do echo $$p; done | \
+ sed 's|^[^/]*$$|.|; s|/[^/]*$$||; s|$$|/so_locations|' | \
+ sort -u`; \
+ test -z "$$locs" || { \
+ echo rm -f $${locs}; \
+ rm -f $${locs}; \
+ }
+
+install-pkglibLTLIBRARIES: $(pkglib_LTLIBRARIES)
+ @$(NORMAL_INSTALL)
+ @list='$(pkglib_LTLIBRARIES)'; test -n "$(pkglibdir)" || list=; \
+ list2=; for p in $$list; do \
+ if test -f $$p; then \
+ list2="$$list2 $$p"; \
+ else :; fi; \
+ done; \
+ test -z "$$list2" || { \
+ echo " $(MKDIR_P) '$(DESTDIR)$(pkglibdir)'"; \
+ $(MKDIR_P) "$(DESTDIR)$(pkglibdir)" || exit 1; \
+ echo " $(LIBTOOL) $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=install $(INSTALL) $(INSTALL_STRIP_FLAG) $$list2 '$(DESTDIR)$(pkglibdir)'"; \
+ $(LIBTOOL) $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=install $(INSTALL) $(INSTALL_STRIP_FLAG) $$list2 "$(DESTDIR)$(pkglibdir)"; \
+ }
+
+uninstall-pkglibLTLIBRARIES:
+ @$(NORMAL_UNINSTALL)
+ @list='$(pkglib_LTLIBRARIES)'; test -n "$(pkglibdir)" || list=; \
+ for p in $$list; do \
+ $(am__strip_dir) \
+ echo " $(LIBTOOL) $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=uninstall rm -f '$(DESTDIR)$(pkglibdir)/$$f'"; \
+ $(LIBTOOL) $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=uninstall rm -f "$(DESTDIR)$(pkglibdir)/$$f"; \
+ done
+
+clean-pkglibLTLIBRARIES:
+ -test -z "$(pkglib_LTLIBRARIES)" || rm -f $(pkglib_LTLIBRARIES)
+ @list='$(pkglib_LTLIBRARIES)'; \
+ locs=`for p in $$list; do echo $$p; done | \
+ sed 's|^[^/]*$$|.|; s|/[^/]*$$||; s|$$|/so_locations|' | \
+ sort -u`; \
+ test -z "$$locs" || { \
+ echo rm -f $${locs}; \
+ rm -f $${locs}; \
+ }
+
+libcompression.la: $(libcompression_la_OBJECTS) $(libcompression_la_DEPENDENCIES) $(EXTRA_libcompression_la_DEPENDENCIES)
+ $(AM_V_CCLD)$(LINK) $(libcompression_la_OBJECTS) $(libcompression_la_LIBADD) $(LIBS)
+
+libdovecot-compression.la: $(libdovecot_compression_la_OBJECTS) $(libdovecot_compression_la_DEPENDENCIES) $(EXTRA_libdovecot_compression_la_DEPENDENCIES)
+ $(AM_V_CCLD)$(libdovecot_compression_la_LINK) -rpath $(pkglibdir) $(libdovecot_compression_la_OBJECTS) $(libdovecot_compression_la_LIBADD) $(LIBS)
+
+bench-compression$(EXEEXT): $(bench_compression_OBJECTS) $(bench_compression_DEPENDENCIES) $(EXTRA_bench_compression_DEPENDENCIES)
+ @rm -f bench-compression$(EXEEXT)
+ $(AM_V_CCLD)$(LINK) $(bench_compression_OBJECTS) $(bench_compression_LDADD) $(LIBS)
+
+test-compression$(EXEEXT): $(test_compression_OBJECTS) $(test_compression_DEPENDENCIES) $(EXTRA_test_compression_DEPENDENCIES)
+ @rm -f test-compression$(EXEEXT)
+ $(AM_V_CCLD)$(LINK) $(test_compression_OBJECTS) $(test_compression_LDADD) $(LIBS)
+
+mostlyclean-compile:
+ -rm -f *.$(OBJEXT)
+
+distclean-compile:
+ -rm -f *.tab.c
+
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/bench-compression.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/compression.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/istream-bzlib.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/istream-decompress.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/istream-lz4.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/istream-lzma.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/istream-zlib.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/istream-zstd.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/ostream-bzlib.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/ostream-lz4.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/ostream-zlib.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/ostream-zstd.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/test-compression.Po@am__quote@ # am--include-marker
+
+$(am__depfiles_remade):
+ @$(MKDIR_P) $(@D)
+ @echo '# dummy' >$@-t && $(am__mv) $@-t $@
+
+am--depfiles: $(am__depfiles_remade)
+
+.c.o:
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(COMPILE) -MT $@ -MD -MP -MF $(DEPDIR)/$*.Tpo -c -o $@ $<
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/$*.Tpo $(DEPDIR)/$*.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='$<' object='$@' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(COMPILE) -c -o $@ $<
+
+.c.obj:
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(COMPILE) -MT $@ -MD -MP -MF $(DEPDIR)/$*.Tpo -c -o $@ `$(CYGPATH_W) '$<'`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/$*.Tpo $(DEPDIR)/$*.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='$<' object='$@' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(COMPILE) -c -o $@ `$(CYGPATH_W) '$<'`
+
+.c.lo:
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(LTCOMPILE) -MT $@ -MD -MP -MF $(DEPDIR)/$*.Tpo -c -o $@ $<
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/$*.Tpo $(DEPDIR)/$*.Plo
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='$<' object='$@' libtool=yes @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(LTCOMPILE) -c -o $@ $<
+
+mostlyclean-libtool:
+ -rm -f *.lo
+
+clean-libtool:
+ -rm -rf .libs _libs
+install-pkginc_libHEADERS: $(pkginc_lib_HEADERS)
+ @$(NORMAL_INSTALL)
+ @list='$(pkginc_lib_HEADERS)'; test -n "$(pkginc_libdir)" || list=; \
+ if test -n "$$list"; then \
+ echo " $(MKDIR_P) '$(DESTDIR)$(pkginc_libdir)'"; \
+ $(MKDIR_P) "$(DESTDIR)$(pkginc_libdir)" || exit 1; \
+ fi; \
+ for p in $$list; do \
+ if test -f "$$p"; then d=; else d="$(srcdir)/"; fi; \
+ echo "$$d$$p"; \
+ done | $(am__base_list) | \
+ while read files; do \
+ echo " $(INSTALL_HEADER) $$files '$(DESTDIR)$(pkginc_libdir)'"; \
+ $(INSTALL_HEADER) $$files "$(DESTDIR)$(pkginc_libdir)" || exit $$?; \
+ done
+
+uninstall-pkginc_libHEADERS:
+ @$(NORMAL_UNINSTALL)
+ @list='$(pkginc_lib_HEADERS)'; test -n "$(pkginc_libdir)" || list=; \
+ files=`for p in $$list; do echo $$p; done | sed -e 's|^.*/||'`; \
+ dir='$(DESTDIR)$(pkginc_libdir)'; $(am__uninstall_files_from_dir)
+
+ID: $(am__tagged_files)
+ $(am__define_uniq_tagged_files); mkid -fID $$unique
+tags: tags-am
+TAGS: tags
+
+tags-am: $(TAGS_DEPENDENCIES) $(am__tagged_files)
+ set x; \
+ here=`pwd`; \
+ $(am__define_uniq_tagged_files); \
+ shift; \
+ if test -z "$(ETAGS_ARGS)$$*$$unique"; then :; else \
+ test -n "$$unique" || unique=$$empty_fix; \
+ if test $$# -gt 0; then \
+ $(ETAGS) $(ETAGSFLAGS) $(AM_ETAGSFLAGS) $(ETAGS_ARGS) \
+ "$$@" $$unique; \
+ else \
+ $(ETAGS) $(ETAGSFLAGS) $(AM_ETAGSFLAGS) $(ETAGS_ARGS) \
+ $$unique; \
+ fi; \
+ fi
+ctags: ctags-am
+
+CTAGS: ctags
+ctags-am: $(TAGS_DEPENDENCIES) $(am__tagged_files)
+ $(am__define_uniq_tagged_files); \
+ test -z "$(CTAGS_ARGS)$$unique" \
+ || $(CTAGS) $(CTAGSFLAGS) $(AM_CTAGSFLAGS) $(CTAGS_ARGS) \
+ $$unique
+
+GTAGS:
+ here=`$(am__cd) $(top_builddir) && pwd` \
+ && $(am__cd) $(top_srcdir) \
+ && gtags -i $(GTAGS_ARGS) "$$here"
+cscopelist: cscopelist-am
+
+cscopelist-am: $(am__tagged_files)
+ list='$(am__tagged_files)'; \
+ case "$(srcdir)" in \
+ [\\/]* | ?:[\\/]*) sdir="$(srcdir)" ;; \
+ *) sdir=$(subdir)/$(srcdir) ;; \
+ esac; \
+ for i in $$list; do \
+ if test -f "$$i"; then \
+ echo "$(subdir)/$$i"; \
+ else \
+ echo "$$sdir/$$i"; \
+ fi; \
+ done >> $(top_builddir)/cscope.files
+
+distclean-tags:
+ -rm -f TAGS ID GTAGS GRTAGS GSYMS GPATH tags
+
+distdir: $(BUILT_SOURCES)
+ $(MAKE) $(AM_MAKEFLAGS) distdir-am
+
+distdir-am: $(DISTFILES)
+ @srcdirstrip=`echo "$(srcdir)" | sed 's/[].[^$$\\*]/\\\\&/g'`; \
+ topsrcdirstrip=`echo "$(top_srcdir)" | sed 's/[].[^$$\\*]/\\\\&/g'`; \
+ list='$(DISTFILES)'; \
+ dist_files=`for file in $$list; do echo $$file; done | \
+ sed -e "s|^$$srcdirstrip/||;t" \
+ -e "s|^$$topsrcdirstrip/|$(top_builddir)/|;t"`; \
+ case $$dist_files in \
+ */*) $(MKDIR_P) `echo "$$dist_files" | \
+ sed '/\//!d;s|^|$(distdir)/|;s,/[^/]*$$,,' | \
+ sort -u` ;; \
+ esac; \
+ for file in $$dist_files; do \
+ if test -f $$file || test -d $$file; then d=.; else d=$(srcdir); fi; \
+ if test -d $$d/$$file; then \
+ dir=`echo "/$$file" | sed -e 's,/[^/]*$$,,'`; \
+ if test -d "$(distdir)/$$file"; then \
+ find "$(distdir)/$$file" -type d ! -perm -700 -exec chmod u+rwx {} \;; \
+ fi; \
+ if test -d $(srcdir)/$$file && test $$d != $(srcdir); then \
+ cp -fpR $(srcdir)/$$file "$(distdir)$$dir" || exit 1; \
+ find "$(distdir)/$$file" -type d ! -perm -700 -exec chmod u+rwx {} \;; \
+ fi; \
+ cp -fpR $$d/$$file "$(distdir)$$dir" || exit 1; \
+ else \
+ test -f "$(distdir)/$$file" \
+ || cp -p $$d/$$file "$(distdir)/$$file" \
+ || exit 1; \
+ fi; \
+ done
+check-am: all-am
+ $(MAKE) $(AM_MAKEFLAGS) check-local
+check: check-am
+all-am: Makefile $(PROGRAMS) $(LTLIBRARIES) $(HEADERS)
+installdirs:
+ for dir in "$(DESTDIR)$(pkglibdir)" "$(DESTDIR)$(pkginc_libdir)"; do \
+ test -z "$$dir" || $(MKDIR_P) "$$dir"; \
+ done
+install: install-am
+install-exec: install-exec-am
+install-data: install-data-am
+uninstall: uninstall-am
+
+install-am: all-am
+ @$(MAKE) $(AM_MAKEFLAGS) install-exec-am install-data-am
+
+installcheck: installcheck-am
+install-strip:
+ if test -z '$(STRIP)'; then \
+ $(MAKE) $(AM_MAKEFLAGS) INSTALL_PROGRAM="$(INSTALL_STRIP_PROGRAM)" \
+ install_sh_PROGRAM="$(INSTALL_STRIP_PROGRAM)" INSTALL_STRIP_FLAG=-s \
+ install; \
+ else \
+ $(MAKE) $(AM_MAKEFLAGS) INSTALL_PROGRAM="$(INSTALL_STRIP_PROGRAM)" \
+ install_sh_PROGRAM="$(INSTALL_STRIP_PROGRAM)" INSTALL_STRIP_FLAG=-s \
+ "INSTALL_PROGRAM_ENV=STRIPPROG='$(STRIP)'" install; \
+ fi
+mostlyclean-generic:
+
+clean-generic:
+
+distclean-generic:
+ -test -z "$(CONFIG_CLEAN_FILES)" || rm -f $(CONFIG_CLEAN_FILES)
+ -test . = "$(srcdir)" || test -z "$(CONFIG_CLEAN_VPATH_FILES)" || rm -f $(CONFIG_CLEAN_VPATH_FILES)
+
+maintainer-clean-generic:
+ @echo "This command is intended for maintainers to use"
+ @echo "it deletes files that may require special tools to rebuild."
+clean: clean-am
+
+clean-am: clean-generic clean-libtool clean-noinstLTLIBRARIES \
+ clean-noinstPROGRAMS clean-pkglibLTLIBRARIES mostlyclean-am
+
+distclean: distclean-am
+ -rm -f ./$(DEPDIR)/bench-compression.Po
+ -rm -f ./$(DEPDIR)/compression.Plo
+ -rm -f ./$(DEPDIR)/istream-bzlib.Plo
+ -rm -f ./$(DEPDIR)/istream-decompress.Plo
+ -rm -f ./$(DEPDIR)/istream-lz4.Plo
+ -rm -f ./$(DEPDIR)/istream-lzma.Plo
+ -rm -f ./$(DEPDIR)/istream-zlib.Plo
+ -rm -f ./$(DEPDIR)/istream-zstd.Plo
+ -rm -f ./$(DEPDIR)/ostream-bzlib.Plo
+ -rm -f ./$(DEPDIR)/ostream-lz4.Plo
+ -rm -f ./$(DEPDIR)/ostream-zlib.Plo
+ -rm -f ./$(DEPDIR)/ostream-zstd.Plo
+ -rm -f ./$(DEPDIR)/test-compression.Po
+ -rm -f Makefile
+distclean-am: clean-am distclean-compile distclean-generic \
+ distclean-tags
+
+dvi: dvi-am
+
+dvi-am:
+
+html: html-am
+
+html-am:
+
+info: info-am
+
+info-am:
+
+install-data-am: install-pkginc_libHEADERS
+
+install-dvi: install-dvi-am
+
+install-dvi-am:
+
+install-exec-am: install-pkglibLTLIBRARIES
+
+install-html: install-html-am
+
+install-html-am:
+
+install-info: install-info-am
+
+install-info-am:
+
+install-man:
+
+install-pdf: install-pdf-am
+
+install-pdf-am:
+
+install-ps: install-ps-am
+
+install-ps-am:
+
+installcheck-am:
+
+maintainer-clean: maintainer-clean-am
+ -rm -f ./$(DEPDIR)/bench-compression.Po
+ -rm -f ./$(DEPDIR)/compression.Plo
+ -rm -f ./$(DEPDIR)/istream-bzlib.Plo
+ -rm -f ./$(DEPDIR)/istream-decompress.Plo
+ -rm -f ./$(DEPDIR)/istream-lz4.Plo
+ -rm -f ./$(DEPDIR)/istream-lzma.Plo
+ -rm -f ./$(DEPDIR)/istream-zlib.Plo
+ -rm -f ./$(DEPDIR)/istream-zstd.Plo
+ -rm -f ./$(DEPDIR)/ostream-bzlib.Plo
+ -rm -f ./$(DEPDIR)/ostream-lz4.Plo
+ -rm -f ./$(DEPDIR)/ostream-zlib.Plo
+ -rm -f ./$(DEPDIR)/ostream-zstd.Plo
+ -rm -f ./$(DEPDIR)/test-compression.Po
+ -rm -f Makefile
+maintainer-clean-am: distclean-am maintainer-clean-generic
+
+mostlyclean: mostlyclean-am
+
+mostlyclean-am: mostlyclean-compile mostlyclean-generic \
+ mostlyclean-libtool
+
+pdf: pdf-am
+
+pdf-am:
+
+ps: ps-am
+
+ps-am:
+
+uninstall-am: uninstall-pkginc_libHEADERS uninstall-pkglibLTLIBRARIES
+
+.MAKE: check-am install-am install-strip
+
+.PHONY: CTAGS GTAGS TAGS all all-am am--depfiles check check-am \
+ check-local clean clean-generic clean-libtool \
+ clean-noinstLTLIBRARIES clean-noinstPROGRAMS \
+ clean-pkglibLTLIBRARIES cscopelist-am ctags ctags-am distclean \
+ distclean-compile distclean-generic distclean-libtool \
+ distclean-tags distdir dvi dvi-am html html-am info info-am \
+ install install-am install-data install-data-am install-dvi \
+ install-dvi-am install-exec install-exec-am install-html \
+ install-html-am install-info install-info-am install-man \
+ install-pdf install-pdf-am install-pkginc_libHEADERS \
+ install-pkglibLTLIBRARIES install-ps install-ps-am \
+ install-strip installcheck installcheck-am installdirs \
+ maintainer-clean maintainer-clean-generic mostlyclean \
+ mostlyclean-compile mostlyclean-generic mostlyclean-libtool \
+ pdf pdf-am ps ps-am tags tags-am uninstall uninstall-am \
+ uninstall-pkginc_libHEADERS uninstall-pkglibLTLIBRARIES
+
+.PRECIOUS: Makefile
+
+
+check-local:
+ for bin in $(test_programs); do \
+ if ! $(RUN_TEST) ./$$bin; then exit 1; fi; \
+ done
+
+# Tell versions [3.59,3.63) of GNU make to not export all variables.
+# Otherwise a system limit (for SysV at least) may be exceeded.
+.NOEXPORT:
diff --git a/src/lib-compression/bench-compression.c b/src/lib-compression/bench-compression.c
new file mode 100644
index 0000000..655079d
--- /dev/null
+++ b/src/lib-compression/bench-compression.c
@@ -0,0 +1,168 @@
+/* Copyright (c) 2020 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "buffer.h"
+#include "istream.h"
+#include "ostream.h"
+#include "randgen.h"
+#include "time-util.h"
+#include "strnum.h"
+#include "compression.h"
+
+#include <stdio.h>
+#include <unistd.h>
+#include <time.h>
+
+/**
+ * Generates semi-compressible data in blocks of given size, to mimic emails
+ * remotely and then compresses and decompresses it using each algorithm.
+ * It measures the time spent on this giving some estimate how well the data
+ * compressed and how long it took.
+ */
+
+static void bench_compression_speed(const struct compression_handler *handler,
+ unsigned int level, unsigned long block_count)
+{
+ struct istream *is = i_stream_create_file("decompressed.bin", 1024);
+ struct ostream *os = o_stream_create_file("compressed.bin", 0, 0644, 0);
+ struct ostream *os_compressed = handler->create_ostream(os, level);
+ o_stream_unref(&os);
+
+ const unsigned char *data;
+ uint64_t ts_0, ts_1;
+ size_t siz;
+ double compression_speed, decompression_speed;
+
+ ts_0 = i_nanoseconds();
+
+ while (i_stream_read_more(is, &data, &siz) > 0) {
+ o_stream_nsend(os_compressed, data, siz);
+ i_stream_skip(is, siz);
+ }
+
+ if (is->stream_errno != 0)
+ printf("Error: %s\n", i_stream_get_error(is));
+
+ i_assert(o_stream_finish(os_compressed) == 1);
+ o_stream_unref(&os_compressed);
+ i_stream_unref(&is);
+
+ ts_1 = i_nanoseconds();
+
+ /* check ratio */
+ struct stat st_1, st_2;
+ if (stat("decompressed.bin", &st_1) != 0)
+ i_fatal("stat(decompressed.bin): %m");
+ if (stat("compressed.bin", &st_2) != 0)
+ i_fatal("stat(compressed.bin): %m");
+
+ double ratio = (double)st_2.st_size / (double)st_1.st_size;
+
+ compression_speed = ((double)(ts_1-ts_0))/((double)block_count);
+ compression_speed /= 1000.0L;
+
+ is = i_stream_create_file("compressed.bin", 1024);
+ os = o_stream_create_file("decompressed.bin", 0, 0644, 0);
+ struct istream *is_decompressed = handler->create_istream(is);
+ i_stream_unref(&is);
+
+ ts_0 = i_nanoseconds();
+
+ while (i_stream_read_more(is_decompressed, &data, &siz) > 0) {
+ o_stream_nsend(os, data, siz);
+ i_stream_skip(is_decompressed, siz);
+ }
+
+ if (is_decompressed->stream_errno != 0)
+ printf("Error: %s\n", i_stream_get_error(is_decompressed));
+
+ i_assert(o_stream_finish(os) == 1);
+ o_stream_unref(&os);
+ i_stream_unref(&is_decompressed);
+
+ ts_1 = i_nanoseconds();
+
+ decompression_speed = ((double)(ts_1 - ts_0))/((double)block_count);
+ decompression_speed /= 1000.0L;
+
+ printf("%s\n", handler->name);
+ printf("\tCompression: %0.02lf us/block\n\tSpace Saving: %0.02lf%%\n",
+ compression_speed, (1.0-ratio)*100.0);
+ printf("\tDecompression: %0.02lf us/block\n\n", decompression_speed);
+
+}
+
+static void print_usage(const char *prog)
+{
+ fprintf(stderr, "Usage: %s block_size count level\n", prog);
+ fprintf(stderr, "Runs with 1000 8k blocks using level 6 if nothing given\n");
+ lib_exit(1);
+}
+
+int main(int argc, const char *argv[])
+{
+ unsigned int level = 6;
+ lib_init();
+
+ unsigned long block_size = 8192UL;
+ unsigned long block_count = 1000UL;
+
+ if (argc >= 3) {
+ if (str_to_ulong(argv[1], &block_size) < 0 ||
+ str_to_ulong(argv[2], &block_count) < 0) {
+ fprintf(stderr, "Invalid parameters\n");
+ print_usage(argv[0]);
+ }
+ if (argc == 4 &&
+ str_to_uint(argv[3], &level) < 0) {
+ fprintf(stderr, "Invalid parameters\n");
+ print_usage(argv[0]);
+ }
+ if (argc > 4) {
+ print_usage(argv[0]);
+ }
+ } else if (argc != 1) {
+ print_usage(argv[0]);
+ }
+
+ unsigned char buf[block_size];
+ printf("Input data is %lu blocks of %lu bytes\n\n", block_count, block_size);
+
+ time_t t0 = time(NULL);
+
+ /* create plaintext file */
+ struct ostream *os = o_stream_create_file("decompressed.bin", 0, 0644, 0);
+ for (unsigned long r = 0; r < block_count; r++) {
+ time_t t1 = time(NULL);
+ if (t1 - t0 >= 1) {
+ printf("Building block %8lu / %-8lu\r", r, block_count);
+ fflush(stdout);
+ t0 = t1;
+ }
+ for (size_t i = 0; i < sizeof(buf); i++) {
+ if (i_rand_limit(3) == 0)
+ buf[i] = i_rand_limit(4);
+ else
+ buf[i] = i;
+ }
+ o_stream_nsend(os, buf, sizeof(buf));
+ }
+
+ i_assert(o_stream_finish(os) == 1);
+ o_stream_unref(&os);
+
+ printf("Input data constructed \n");
+
+ for (unsigned int i = 0; compression_handlers[i].name != NULL; i++) T_BEGIN {
+ if (compression_handlers[i].create_istream != NULL &&
+ compression_handlers[i].create_ostream != NULL) {
+ bench_compression_speed(&compression_handlers[i], level,
+ block_count);
+ }
+ } T_END;
+
+ i_unlink("decompressed.bin");
+ i_unlink("compressed.bin");
+
+ lib_deinit();
+}
diff --git a/src/lib-compression/compression.c b/src/lib-compression/compression.c
new file mode 100644
index 0000000..e562e73
--- /dev/null
+++ b/src/lib-compression/compression.c
@@ -0,0 +1,250 @@
+/* Copyright (c) 2010-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "istream.h"
+#include "istream-zlib.h"
+#include "ostream-zlib.h"
+#include "iostream-lz4.h"
+#include "compression.h"
+
+#ifndef HAVE_ZLIB
+# define i_stream_create_gz NULL
+# define o_stream_create_gz NULL
+# define i_stream_create_deflate NULL
+# define o_stream_create_deflate NULL
+# define compression_get_min_level_gz NULL
+# define compression_get_default_level_gz NULL
+# define compression_get_max_level_gz NULL
+#endif
+#ifndef HAVE_BZLIB
+# define i_stream_create_bz2 NULL
+# define o_stream_create_bz2 NULL
+# define compression_get_min_level_bz2 NULL
+# define compression_get_default_level_bz2 NULL
+# define compression_get_max_level_bz2 NULL
+#endif
+#ifndef HAVE_LZMA
+# define i_stream_create_lzma NULL
+#endif
+#ifndef HAVE_LZ4
+# define i_stream_create_lz4 NULL
+# define o_stream_create_lz4 NULL
+# define compression_get_min_level_lz4 NULL
+# define compression_get_default_level_lz4 NULL
+# define compression_get_max_level_lz4 NULL
+#endif
+#ifndef HAVE_ZSTD
+# define i_stream_create_zstd NULL
+# define o_stream_create_zstd NULL
+# define compression_get_min_level_zstd NULL
+# define compression_get_default_level_zstd NULL
+# define compression_get_max_level_zstd NULL
+#endif
+
+static bool is_compressed_zlib(struct istream *input)
+{
+ const unsigned char *data;
+ size_t size;
+
+ /* Peek in to the stream and see if it looks like it's compressed
+ (based on its header). This also means that users can try to exploit
+ security holes in the uncompression library by APPENDing a specially
+ crafted mail. So let's hope zlib is free of holes. */
+ if (i_stream_read_bytes(input, &data, &size, 2) <= 0)
+ return FALSE;
+ i_assert(size >= 2);
+
+ return data[0] == 31 && data[1] == 139;
+}
+
+static bool is_compressed_bzlib(struct istream *input)
+{
+ const unsigned char *data;
+ size_t size;
+
+ if (i_stream_read_bytes(input, &data, &size, 4) <= 0)
+ return FALSE;
+ if (memcmp(data, "BZh", 3) != 0)
+ return FALSE;
+ if (data[3] < '1' || data[3] > '9')
+ return FALSE;
+ /* The above is enough to be considered as the bzlib magic.
+ Normally it's followed by data header beginning with 0x31. However,
+ with empty compressed files it's followed by 0x17. */
+ return TRUE;
+}
+
+static bool is_compressed_xz(struct istream *input)
+{
+ const unsigned char *data;
+ size_t size;
+
+ if (i_stream_read_bytes(input, &data, &size, 6) <= 0)
+ return FALSE;
+ return memcmp(data, "\xfd\x37\x7a\x58\x5a\x00", 6) == 0;
+}
+
+static bool is_compressed_lz4(struct istream *input)
+{
+ const unsigned char *data;
+ size_t size;
+
+ if (i_stream_read_bytes(input, &data, &size, IOSTREAM_LZ4_MAGIC_LEN) <= 0)
+ return FALSE;
+ /* there is no standard LZ4 header, so we've created our own */
+ return memcmp(data, IOSTREAM_LZ4_MAGIC, IOSTREAM_LZ4_MAGIC_LEN) == 0;
+}
+
+#define ZSTD_MAGICNUMBER 0xFD2FB528 /* valid since v0.8.0 */
+static bool is_compressed_zstd(struct istream *input)
+{
+ const unsigned char *data;
+ size_t size = 0;
+
+ if (i_stream_read_bytes(input, &data, &size, sizeof(uint32_t)) <= 0)
+ return FALSE;
+ i_assert(size >= sizeof(uint32_t));
+
+ return le32_to_cpu_unaligned(data) == ZSTD_MAGICNUMBER;
+}
+
+int compression_lookup_handler(const char *name,
+ const struct compression_handler **handler_r)
+{
+ unsigned int i;
+
+ for (i = 0; compression_handlers[i].name != NULL; i++) {
+ if (strcmp(name, compression_handlers[i].name) == 0) {
+ if (compression_handlers[i].create_istream == NULL ||
+ compression_handlers[i].create_ostream == NULL) {
+ /* Handler is known but not compiled in */
+ return 0;
+ }
+ (*handler_r) = &compression_handlers[i];
+ return 1;
+ }
+ }
+ return -1;
+}
+
+const struct compression_handler *
+compression_detect_handler(struct istream *input)
+{
+ unsigned int i;
+
+ for (i = 0; compression_handlers[i].name != NULL; i++) {
+ if (compression_handlers[i].is_compressed != NULL &&
+ compression_handlers[i].is_compressed(input))
+ return &compression_handlers[i];
+ }
+ return NULL;
+}
+
+int compression_lookup_handler_from_ext(const char *path,
+ const struct compression_handler **handler_r)
+{
+ unsigned int i;
+ size_t len, path_len = strlen(path);
+
+ for (i = 0; compression_handlers[i].name != NULL; i++) {
+ if (compression_handlers[i].ext == NULL)
+ continue;
+
+ len = strlen(compression_handlers[i].ext);
+ if (path_len > len &&
+ strcmp(path + path_len - len, compression_handlers[i].ext) == 0) {
+ if (compression_handlers[i].create_istream == NULL ||
+ compression_handlers[i].create_ostream == NULL) {
+ /* Handler is known but not compiled in */
+ return 0;
+ }
+ (*handler_r) = &compression_handlers[i];
+ return 1;
+ }
+ }
+ return -1;
+}
+
+static int compression_get_min_level_unsupported(void)
+{
+ return -1;
+}
+
+static int compression_get_default_level_unsupported(void)
+{
+ return -1;
+}
+
+static int compression_get_max_level_unsupported(void)
+{
+ return -1;
+}
+
+const struct compression_handler compression_handlers[] = {
+ {
+ .name = "gz",
+ .ext = ".gz",
+ .is_compressed = is_compressed_zlib,
+ .create_istream = i_stream_create_gz,
+ .create_ostream = o_stream_create_gz,
+ .get_min_level = compression_get_min_level_gz,
+ .get_default_level = compression_get_default_level_gz,
+ .get_max_level = compression_get_max_level_gz,
+ },
+ {
+ .name = "bz2",
+ .ext = ".bz2",
+ .is_compressed = is_compressed_bzlib,
+ .create_istream = i_stream_create_bz2,
+ .create_ostream = o_stream_create_bz2,
+ .get_min_level = compression_get_min_level_bz2,
+ .get_default_level = compression_get_default_level_bz2,
+ .get_max_level = compression_get_max_level_bz2,
+ },
+ {
+ .name = "deflate",
+ .ext = NULL,
+ .is_compressed = NULL,
+ .create_istream = i_stream_create_deflate,
+ .create_ostream = o_stream_create_deflate,
+ .get_min_level = compression_get_min_level_gz,
+ .get_default_level = compression_get_default_level_gz,
+ .get_max_level = compression_get_max_level_gz,
+ },
+ {
+ .name = "xz",
+ .ext = ".xz",
+ .is_compressed = is_compressed_xz,
+ .create_istream = i_stream_create_lzma,
+ .create_ostream = NULL,
+ .get_min_level = compression_get_min_level_unsupported,
+ .get_default_level = compression_get_default_level_unsupported,
+ .get_max_level = compression_get_max_level_unsupported,
+ },
+ {
+ .name = "lz4",
+ .ext = ".lz4",
+ .is_compressed = is_compressed_lz4,
+ .create_istream = i_stream_create_lz4,
+ .create_ostream = o_stream_create_lz4,
+ .get_min_level = compression_get_min_level_lz4, /* does not actually support any of this */
+ .get_default_level = compression_get_default_level_lz4,
+ .get_max_level = compression_get_max_level_lz4,
+ },
+ {
+ .name = "zstd",
+ .ext = ".zstd",
+ .is_compressed = is_compressed_zstd,
+ .create_istream = i_stream_create_zstd,
+ .create_ostream = o_stream_create_zstd,
+ .get_min_level = compression_get_min_level_zstd,
+ .get_default_level = compression_get_default_level_zstd,
+ .get_max_level = compression_get_max_level_zstd,
+ },
+ {
+ .name = "unsupported",
+ },
+ {
+ .name = NULL,
+ }
+};
diff --git a/src/lib-compression/compression.h b/src/lib-compression/compression.h
new file mode 100644
index 0000000..60d58bf
--- /dev/null
+++ b/src/lib-compression/compression.h
@@ -0,0 +1,55 @@
+#ifndef COMPRESSION_H
+#define COMPRESSION_H
+
+enum istream_decompress_flags {
+ /* If stream isn't detected to be compressed, return it as passthrough
+ istream. */
+ ISTREAM_DECOMPRESS_FLAG_TRY = BIT(0),
+};
+
+/* Compressed input is always detected once at maximum this many bytes have
+ been read. This value must be smaller than a typical istream max buffer
+ size. */
+#define COMPRESSION_HDR_MAX_SIZE 128
+
+struct compression_handler {
+ const char *name;
+ const char *ext;
+ bool (*is_compressed)(struct istream *input);
+ struct istream *(*create_istream)(struct istream *input);
+ struct ostream *(*create_ostream)(struct ostream *output, int level);
+ /* returns minimum level */
+ int (*get_min_level)(void);
+ /* the default can be -1 (e.g. gz), so the return value of this has to
+ be used as-is. */
+ int (*get_default_level)(void);
+ /* returns maximum level */
+ int (*get_max_level)(void);
+};
+
+extern const struct compression_handler compression_handlers[];
+
+/* Returns 1 if compression handler was found and is usable, 0 if support isn't
+ compiled in, -1 if unknown. */
+int compression_lookup_handler(const char *name,
+ const struct compression_handler **handler_r);
+/* Detect handler by looking at the first few bytes of the input stream. */
+const struct compression_handler *
+compression_detect_handler(struct istream *input);
+/* Lookup handler based on filename extension in the path, returns the same
+ * values as compression_lookup_handler. */
+int compression_lookup_handler_from_ext(const char *path,
+ const struct compression_handler **handler_r);
+
+/* Automatically detect the compression format. Note that using tee-istream as
+ one of the parent streams is dangerous here: A decompression istream may
+ have to read a lot of data (e.g. 8 kB isn't enough) before it returns even
+ the first byte as output. If the other tee children aren't read forward,
+ this can cause an infinite loop when i_stream_read() is always returning 0.
+ This is why ISTREAM_DECOMPRESS_FLAG_TRY should be used instead of attempting
+ to implement similar functionality with tee-istream. */
+struct istream *
+i_stream_create_decompress(struct istream *input,
+ enum istream_decompress_flags flags);
+
+#endif
diff --git a/src/lib-compression/iostream-lz4.h b/src/lib-compression/iostream-lz4.h
new file mode 100644
index 0000000..a0897f5
--- /dev/null
+++ b/src/lib-compression/iostream-lz4.h
@@ -0,0 +1,30 @@
+#ifndef IOSTREAM_LZ4_H
+#define IOSTREAM_LZ4_H
+
+/*
+ Dovecot's LZ4 compressed files contain:
+
+ IOSTREAM_LZ4_HEADER
+ n x (4 byte big-endian: compressed chunk length, compressed chunk)
+*/
+
+#define IOSTREAM_LZ4_MAGIC "Dovecot-LZ4\x0d\x2a\x9b\xc5"
+#define IOSTREAM_LZ4_MAGIC_LEN (sizeof(IOSTREAM_LZ4_MAGIC)-1)
+
+struct iostream_lz4_header {
+ unsigned char magic[IOSTREAM_LZ4_MAGIC_LEN];
+ /* OSTREAM_LZ4_CHUNK_SIZE in big-endian */
+ unsigned char max_uncompressed_chunk_size[4];
+};
+
+/* How large chunks we're buffering into memory before compressing them */
+#define OSTREAM_LZ4_CHUNK_SIZE (1024*64)
+/* How large chunks we allow in input data before returning a failure.
+ This must be at least OSTREAM_LZ4_CHUNK_SIZE, but for future compatibility
+ should be somewhat higher (but not too high to avoid wasting memory for
+ corrupted files). */
+#define ISTREAM_LZ4_CHUNK_SIZE (1024*1024)
+
+#define IOSTREAM_LZ4_CHUNK_PREFIX_LEN 4 /* big-endian size of chunk */
+
+#endif
diff --git a/src/lib-compression/iostream-zstd-private.h b/src/lib-compression/iostream-zstd-private.h
new file mode 100644
index 0000000..e430bbc
--- /dev/null
+++ b/src/lib-compression/iostream-zstd-private.h
@@ -0,0 +1,35 @@
+#ifndef IOSTREAM_ZSTD_PRIVATE_H
+#define IOSTREAM_ZSTD_PRIVATE_H 1
+
+/* a horrible hack to fix issues when the installed libzstd is lot
+ newer than what we were compiled against. */
+static inline ZSTD_ErrorCode zstd_version_errcode(ZSTD_ErrorCode err)
+{
+#if ZSTD_VERSION_NUMBER < 10301
+ if (ZSTD_versionNumber() > 10300) {
+ /* reinterpret them */
+ if (err == 10)
+ return ZSTD_error_prefix_unknown;
+ if (err == 32)
+ return ZSTD_error_dictionary_wrong;
+ if (err == 62)
+ return ZSTD_error_init_missing;
+ if (err == 64)
+ return ZSTD_error_memory_allocation;
+ return ZSTD_error_GENERIC;
+ }
+#endif
+ return err;
+}
+
+static inline void zstd_version_check(void)
+{
+ /* error codes were pinned on 1.3.1, so we only care about
+ versions before that. */
+ if (ZSTD_VERSION_NUMBER < 10301 || ZSTD_versionNumber() < 10301)
+ if (ZSTD_versionNumber() / 100 != ZSTD_VERSION_NUMBER / 100)
+ i_warning("zstd: Compiled against %u, but %u installed!",
+ ZSTD_VERSION_NUMBER, ZSTD_versionNumber());
+}
+
+#endif
diff --git a/src/lib-compression/istream-bzlib.c b/src/lib-compression/istream-bzlib.c
new file mode 100644
index 0000000..aae1027
--- /dev/null
+++ b/src/lib-compression/istream-bzlib.c
@@ -0,0 +1,231 @@
+/* Copyright (c) 2010-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+
+#ifdef HAVE_BZLIB
+
+#include "istream-private.h"
+#include "istream-zlib.h"
+#include <bzlib.h>
+
+#define CHUNK_SIZE (1024*64)
+
+struct bzlib_istream {
+ struct istream_private istream;
+
+ bz_stream zs;
+ uoff_t eof_offset;
+ struct stat last_parent_statbuf;
+
+ bool hdr_read:1;
+ bool marked:1;
+ bool zs_closed:1;
+};
+
+static void i_stream_bzlib_close(struct iostream_private *stream,
+ bool close_parent)
+{
+ struct bzlib_istream *zstream = (struct bzlib_istream *)stream;
+
+ if (!zstream->zs_closed) {
+ (void)BZ2_bzDecompressEnd(&zstream->zs);
+ zstream->zs_closed = TRUE;
+ }
+ if (close_parent)
+ i_stream_close(zstream->istream.parent);
+}
+
+static void bzlib_read_error(struct bzlib_istream *zstream, const char *error)
+{
+ io_stream_set_error(&zstream->istream.iostream,
+ "bzlib.read(%s): %s at %"PRIuUOFF_T,
+ i_stream_get_name(&zstream->istream.istream), error,
+ i_stream_get_absolute_offset(&zstream->istream.istream));
+}
+
+static ssize_t i_stream_bzlib_read(struct istream_private *stream)
+{
+ struct bzlib_istream *zstream = (struct bzlib_istream *)stream;
+ const unsigned char *data;
+ uoff_t high_offset;
+ size_t size, out_size;
+ int ret;
+
+ high_offset = stream->istream.v_offset + (stream->pos - stream->skip);
+ if (zstream->eof_offset == high_offset) {
+ stream->istream.eof = TRUE;
+ return -1;
+ }
+
+ if (!zstream->marked) {
+ if (!i_stream_try_alloc(stream, CHUNK_SIZE, &out_size))
+ return -2; /* buffer full */
+ } else {
+ /* try to avoid compressing, so we can quickly seek backwards */
+ if (!i_stream_try_alloc_avoid_compress(stream, CHUNK_SIZE, &out_size))
+ return -2; /* buffer full */
+ }
+
+ if (i_stream_read_more(stream->parent, &data, &size) < 0) {
+ if (stream->parent->stream_errno != 0) {
+ stream->istream.stream_errno =
+ stream->parent->stream_errno;
+ } else {
+ i_assert(stream->parent->eof);
+ bzlib_read_error(zstream, "unexpected EOF");
+ if (!zstream->hdr_read)
+ stream->istream.stream_errno = EINVAL;
+ else
+ stream->istream.stream_errno = EPIPE;
+ }
+ return -1;
+ }
+ if (size == 0) {
+ /* no more input */
+ i_assert(!stream->istream.blocking);
+ return 0;
+ }
+
+ zstream->zs.next_in = (char *)data;
+ zstream->zs.avail_in = size;
+
+ zstream->zs.next_out = (char *)stream->w_buffer + stream->pos;
+ zstream->zs.avail_out = out_size;
+ ret = BZ2_bzDecompress(&zstream->zs);
+ zstream->hdr_read = TRUE;
+
+ out_size -= zstream->zs.avail_out;
+ stream->pos += out_size;
+
+ i_stream_skip(stream->parent, size - zstream->zs.avail_in);
+
+ switch (ret) {
+ case BZ_OK:
+ break;
+ case BZ_PARAM_ERROR:
+ i_unreached();
+ case BZ_DATA_ERROR:
+ bzlib_read_error(zstream, "corrupted data");
+ stream->istream.stream_errno = EINVAL;
+ return -1;
+ case BZ_DATA_ERROR_MAGIC:
+ bzlib_read_error(zstream,
+ "wrong magic in header (not bz2 file?)");
+ stream->istream.stream_errno = EINVAL;
+ return -1;
+ case BZ_MEM_ERROR:
+ i_fatal_status(FATAL_OUTOFMEM, "bzlib.read(%s): Out of memory",
+ i_stream_get_name(&stream->istream));
+ case BZ_STREAM_END:
+ zstream->eof_offset = stream->istream.v_offset +
+ (stream->pos - stream->skip);
+ stream->cached_stream_size = zstream->eof_offset;
+ if (out_size == 0) {
+ stream->istream.eof = TRUE;
+ return -1;
+ }
+ break;
+ default:
+ i_fatal("BZ2_bzDecompress() failed with %d", ret);
+ }
+ if (out_size == 0) {
+ /* read more input */
+ return i_stream_bzlib_read(stream);
+ }
+ return out_size;
+}
+
+static void i_stream_bzlib_init(struct bzlib_istream *zstream)
+{
+ int ret;
+
+ ret = BZ2_bzDecompressInit(&zstream->zs, 0, 0);
+ switch (ret) {
+ case BZ_OK:
+ break;
+ case BZ_MEM_ERROR:
+ i_fatal_status(FATAL_OUTOFMEM, "bzlib: Out of memory");
+ case BZ_CONFIG_ERROR:
+ i_fatal("Wrong bzlib library version (broken compilation)");
+ case BZ_PARAM_ERROR:
+ i_fatal("bzlib: Invalid parameters");
+ default:
+ i_fatal("BZ2_bzDecompressInit() failed with %d", ret);
+ }
+}
+
+static void i_stream_bzlib_reset(struct bzlib_istream *zstream)
+{
+ struct istream_private *stream = &zstream->istream;
+
+ i_stream_seek(stream->parent, stream->parent_start_offset);
+ zstream->eof_offset = UOFF_T_MAX;
+ zstream->zs.next_in = NULL;
+ zstream->zs.avail_in = 0;
+
+ stream->parent_expected_offset = stream->parent_start_offset;
+ stream->skip = stream->pos = 0;
+ stream->istream.v_offset = 0;
+ stream->high_pos = 0;
+
+ (void)BZ2_bzDecompressEnd(&zstream->zs);
+ i_stream_bzlib_init(zstream);
+}
+
+static void
+i_stream_bzlib_seek(struct istream_private *stream, uoff_t v_offset, bool mark)
+{
+ struct bzlib_istream *zstream = (struct bzlib_istream *) stream;
+
+ if (i_stream_nonseekable_try_seek(stream, v_offset))
+ return;
+
+ /* have to seek backwards - reset state and retry */
+ i_stream_bzlib_reset(zstream);
+ if (!i_stream_nonseekable_try_seek(stream, v_offset))
+ i_unreached();
+
+ if (mark)
+ zstream->marked = TRUE;
+}
+
+static void i_stream_bzlib_sync(struct istream_private *stream)
+{
+ struct bzlib_istream *zstream = (struct bzlib_istream *) stream;
+ const struct stat *st;
+
+ if (i_stream_stat(stream->parent, FALSE, &st) == 0) {
+ if (memcmp(&zstream->last_parent_statbuf,
+ st, sizeof(*st)) == 0) {
+ /* a compressed file doesn't change unexpectedly,
+ don't clear our caches unnecessarily */
+ return;
+ }
+ zstream->last_parent_statbuf = *st;
+ }
+ i_stream_bzlib_reset(zstream);
+}
+
+struct istream *i_stream_create_bz2(struct istream *input)
+{
+ struct bzlib_istream *zstream;
+
+ zstream = i_new(struct bzlib_istream, 1);
+ zstream->eof_offset = UOFF_T_MAX;
+
+ i_stream_bzlib_init(zstream);
+
+ zstream->istream.iostream.close = i_stream_bzlib_close;
+ zstream->istream.max_buffer_size = input->real_stream->max_buffer_size;
+ zstream->istream.read = i_stream_bzlib_read;
+ zstream->istream.seek = i_stream_bzlib_seek;
+ zstream->istream.sync = i_stream_bzlib_sync;
+
+ zstream->istream.istream.readable_fd = FALSE;
+ zstream->istream.istream.blocking = input->blocking;
+ zstream->istream.istream.seekable = input->seekable;
+
+ return i_stream_create(&zstream->istream, input,
+ i_stream_get_fd(input), 0);
+}
+#endif
diff --git a/src/lib-compression/istream-decompress.c b/src/lib-compression/istream-decompress.c
new file mode 100644
index 0000000..2021a01
--- /dev/null
+++ b/src/lib-compression/istream-decompress.c
@@ -0,0 +1,258 @@
+/* Copyright (c) 2020 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "istream-private.h"
+#include "compression.h"
+
+struct decompress_istream {
+ struct istream_private istream;
+ struct istream *compressed_input;
+ struct istream *decompressed_input;
+ enum istream_decompress_flags flags;
+};
+
+static void copy_compressed_input_error(struct decompress_istream *zstream)
+{
+ struct istream_private *stream = &zstream->istream;
+
+ stream->istream.stream_errno = zstream->compressed_input->stream_errno;
+ stream->istream.eof = zstream->compressed_input->eof;
+ if (zstream->compressed_input->stream_errno != 0) {
+ io_stream_set_error(&stream->iostream, "%s",
+ i_stream_get_error(&zstream->compressed_input->real_stream->istream));
+ }
+}
+
+static void copy_decompressed_input_error(struct decompress_istream *zstream)
+{
+ struct istream_private *stream = &zstream->istream;
+
+ stream->istream.stream_errno = zstream->decompressed_input->stream_errno;
+ stream->istream.eof = zstream->decompressed_input->eof;
+ if (zstream->decompressed_input->stream_errno != 0) {
+ io_stream_set_error(&stream->iostream, "%s",
+ i_stream_get_error(&zstream->decompressed_input->real_stream->istream));
+ }
+}
+
+static void
+i_stream_decompress_close(struct iostream_private *_stream, bool close_parent)
+{
+ struct istream_private *stream =
+ container_of(_stream, struct istream_private, iostream);
+ struct decompress_istream *zstream =
+ container_of(stream, struct decompress_istream, istream);
+
+ if (zstream->decompressed_input != NULL)
+ i_stream_close(zstream->decompressed_input);
+ if (close_parent)
+ i_stream_close(zstream->compressed_input);
+}
+
+static void
+i_stream_decompress_destroy(struct iostream_private *_stream)
+{
+ struct istream_private *stream =
+ container_of(_stream, struct istream_private, iostream);
+ struct decompress_istream *zstream =
+ container_of(stream, struct decompress_istream, istream);
+
+ i_stream_unref(&zstream->decompressed_input);
+ i_stream_unref(&zstream->compressed_input);
+}
+
+static int
+i_stream_decompress_not_compressed(struct decompress_istream *zstream)
+{
+ if ((zstream->flags & ISTREAM_DECOMPRESS_FLAG_TRY) == 0) {
+ zstream->istream.istream.stream_errno = EINVAL;
+ io_stream_set_error(&zstream->istream.iostream,
+ "Stream isn't compressed");
+ return -1;
+ } else {
+ zstream->decompressed_input = zstream->compressed_input;
+ i_stream_ref(zstream->decompressed_input);
+ return 1;
+ }
+}
+
+static int i_stream_decompress_detect(struct decompress_istream *zstream)
+{
+ const struct compression_handler *handler;
+ ssize_t ret;
+
+ ret = i_stream_read(zstream->compressed_input);
+ handler = compression_detect_handler(zstream->compressed_input);
+ if (handler == NULL) {
+ switch (ret) {
+ case -1:
+ if (zstream->compressed_input->stream_errno != 0) {
+ copy_compressed_input_error(zstream);
+ return -1;
+ }
+ /* fall through */
+ case -2:
+ /* we've read a full buffer or we reached EOF -
+ the stream isn't compressed */
+ return i_stream_decompress_not_compressed(zstream);
+ case 0:
+ return 0;
+ default:
+ if (!zstream->istream.istream.blocking)
+ return 0;
+ return i_stream_decompress_detect(zstream);
+ }
+ }
+ if (handler->create_istream == NULL) {
+ zstream->istream.istream.stream_errno = EINVAL;
+ io_stream_set_error(&zstream->istream.iostream,
+ "Compression handler %s not supported", handler->name);
+ return -1;
+ }
+
+ zstream->decompressed_input =
+ handler->create_istream(zstream->compressed_input);
+ return 1;
+}
+
+static ssize_t i_stream_decompress_read(struct istream_private *stream)
+{
+ struct decompress_istream *zstream =
+ container_of(stream, struct decompress_istream, istream);
+ ssize_t ret;
+ size_t pos;
+
+ if (zstream->decompressed_input == NULL) {
+ if ((ret = i_stream_decompress_detect(zstream)) <= 0)
+ return ret;
+ }
+
+ i_stream_seek(zstream->decompressed_input, stream->istream.v_offset);
+ stream->pos -= stream->skip;
+ stream->skip = 0;
+
+ stream->buffer = i_stream_get_data(zstream->decompressed_input, &pos);
+ if (pos > stream->pos)
+ ret = 0;
+ else do {
+ ret = i_stream_read_memarea(zstream->decompressed_input);
+ copy_decompressed_input_error(zstream);
+ stream->buffer = i_stream_get_data(zstream->decompressed_input,
+ &pos);
+ } while (pos <= stream->pos && ret > 0);
+ if (ret == -2)
+ return -2;
+
+ if (pos <= stream->pos)
+ ret = ret == 0 ? 0 : -1;
+ else
+ ret = (ssize_t)(pos - stream->pos);
+ stream->pos = pos;
+ i_assert(ret != -1 || stream->istream.eof ||
+ stream->istream.stream_errno != 0);
+ return ret;
+}
+
+static void i_stream_decompress_reset(struct istream_private *stream)
+{
+ stream->skip = stream->pos = 0;
+ stream->istream.v_offset = 0;
+ stream->istream.eof = FALSE;
+}
+
+static void
+i_stream_decompress_seek(struct istream_private *stream,
+ uoff_t v_offset, bool mark)
+{
+ struct decompress_istream *zstream =
+ container_of(stream, struct decompress_istream, istream);
+
+ if (zstream->decompressed_input == NULL) {
+ if (!i_stream_nonseekable_try_seek(stream, v_offset))
+ i_panic("seeking backwards before detecting compression format");
+ } else {
+ i_stream_decompress_reset(stream);
+ stream->istream.v_offset = v_offset;
+ if (mark)
+ i_stream_seek_mark(zstream->decompressed_input, v_offset);
+ else
+ i_stream_seek(zstream->decompressed_input, v_offset);
+ copy_decompressed_input_error(zstream);
+ }
+}
+
+static void i_stream_decompress_sync(struct istream_private *stream)
+{
+ struct decompress_istream *zstream =
+ container_of(stream, struct decompress_istream, istream);
+
+ i_stream_decompress_reset(stream);
+ if (zstream->decompressed_input != NULL)
+ i_stream_sync(zstream->decompressed_input);
+}
+
+static int i_stream_decompress_stat(struct istream_private *stream, bool exact)
+{
+ struct decompress_istream *zstream =
+ container_of(stream, struct decompress_istream, istream);
+ const struct stat *st;
+
+ if (!exact) {
+ if (i_stream_stat(zstream->compressed_input, exact, &st) < 0) {
+ copy_compressed_input_error(zstream);
+ return -1;
+ }
+ stream->statbuf = *st;
+ return 0;
+ }
+ if (zstream->decompressed_input == NULL) {
+ (void)i_stream_read(&stream->istream);
+ if (zstream->decompressed_input == NULL) {
+ if (stream->istream.stream_errno == 0) {
+ zstream->istream.istream.stream_errno = EINVAL;
+ io_stream_set_error(&zstream->istream.iostream,
+ "Stream compression couldn't be detected during stat");
+ }
+ return -1;
+ }
+ }
+
+ if (i_stream_stat(zstream->decompressed_input, exact, &st) < 0) {
+ copy_decompressed_input_error(zstream);
+ return -1;
+ }
+ i_stream_decompress_reset(stream);
+ stream->statbuf = *st;
+ return 0;
+}
+
+struct istream *
+i_stream_create_decompress(struct istream *input,
+ enum istream_decompress_flags flags)
+{
+ struct decompress_istream *zstream;
+
+ zstream = i_new(struct decompress_istream, 1);
+ zstream->compressed_input = input;
+ zstream->flags = flags;
+ i_stream_ref(input);
+
+ zstream->istream.iostream.close = i_stream_decompress_close;
+ zstream->istream.iostream.destroy = i_stream_decompress_destroy;
+ zstream->istream.max_buffer_size = input->real_stream->max_buffer_size;
+ zstream->istream.read = i_stream_decompress_read;
+ zstream->istream.seek = i_stream_decompress_seek;
+ zstream->istream.sync = i_stream_decompress_sync;
+ zstream->istream.stat = i_stream_decompress_stat;
+
+ zstream->istream.istream.readable_fd = FALSE;
+ zstream->istream.istream.blocking = input->blocking;
+ zstream->istream.istream.seekable = input->seekable;
+
+ struct istream *ret = i_stream_create(&zstream->istream, NULL,
+ i_stream_get_fd(input), 0);
+ /* input isn't used as our parent istream, so need to copy the stream
+ name to preserve it. */
+ i_stream_set_name(ret, i_stream_get_name(input));
+ return ret;
+}
diff --git a/src/lib-compression/istream-lz4.c b/src/lib-compression/istream-lz4.c
new file mode 100644
index 0000000..24d539a
--- /dev/null
+++ b/src/lib-compression/istream-lz4.c
@@ -0,0 +1,281 @@
+/* Copyright (c) 2013-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+
+#ifdef HAVE_LZ4
+
+#include "buffer.h"
+#include "istream-private.h"
+#include "istream-zlib.h"
+#include "iostream-lz4.h"
+#include <lz4.h>
+
+struct lz4_istream {
+ struct istream_private istream;
+
+ struct stat last_parent_statbuf;
+
+ buffer_t *chunk_buf;
+ uint32_t chunk_size, chunk_left, max_uncompressed_chunk_size;
+
+ bool marked:1;
+ bool header_read:1;
+};
+
+static void i_stream_lz4_close(struct iostream_private *stream,
+ bool close_parent)
+{
+ struct lz4_istream *zstream = (struct lz4_istream *)stream;
+
+ buffer_free(&zstream->chunk_buf);
+ if (close_parent)
+ i_stream_close(zstream->istream.parent);
+}
+
+static void lz4_read_error(struct lz4_istream *zstream, const char *error)
+{
+ io_stream_set_error(&zstream->istream.iostream,
+ "lz4.read(%s): %s at %"PRIuUOFF_T,
+ i_stream_get_name(&zstream->istream.istream), error,
+ i_stream_get_absolute_offset(&zstream->istream.istream));
+}
+
+static int i_stream_lz4_read_header(struct lz4_istream *zstream)
+{
+ const struct iostream_lz4_header *hdr;
+ const unsigned char *data;
+ size_t size;
+ int ret;
+
+ ret = i_stream_read_bytes(zstream->istream.parent, &data,
+ &size, sizeof(*hdr));
+ size = I_MIN(size, sizeof(*hdr));
+ buffer_append(zstream->chunk_buf, data, size);
+ i_stream_skip(zstream->istream.parent, size);
+ if (ret < 0 || (ret == 0 && zstream->istream.istream.eof)) {
+ i_assert(ret != -2);
+ if (zstream->istream.istream.stream_errno == 0) {
+ lz4_read_error(zstream, "missing header (not lz4 file?)");
+ zstream->istream.istream.stream_errno = EINVAL;
+ } else
+ zstream->istream.istream.stream_errno =
+ zstream->istream.parent->stream_errno;
+ return ret;
+ }
+ if (zstream->chunk_buf->used < sizeof(*hdr)) {
+ i_assert(!zstream->istream.istream.blocking);
+ return 0;
+ }
+
+ hdr = zstream->chunk_buf->data;
+ if (ret == 0 || memcmp(hdr->magic, IOSTREAM_LZ4_MAGIC,
+ IOSTREAM_LZ4_MAGIC_LEN) != 0) {
+ lz4_read_error(zstream, "wrong magic in header (not lz4 file?)");
+ zstream->istream.istream.stream_errno = EINVAL;
+ return -1;
+ }
+ zstream->max_uncompressed_chunk_size =
+ be32_to_cpu_unaligned(hdr->max_uncompressed_chunk_size);
+ buffer_set_used_size(zstream->chunk_buf, 0);
+ if (zstream->max_uncompressed_chunk_size > ISTREAM_LZ4_CHUNK_SIZE) {
+ lz4_read_error(zstream, t_strdup_printf(
+ "lz4 max chunk size too large (%u > %u)",
+ zstream->max_uncompressed_chunk_size,
+ ISTREAM_LZ4_CHUNK_SIZE));
+ zstream->istream.istream.stream_errno = EINVAL;
+ return -1;
+ }
+ return 1;
+}
+
+static int i_stream_lz4_read_chunk_header(struct lz4_istream *zstream)
+{
+ int ret;
+ size_t size;
+ const unsigned char *data;
+ struct istream_private *stream = &zstream->istream;
+
+ i_assert(zstream->chunk_buf->used <= IOSTREAM_LZ4_CHUNK_PREFIX_LEN);
+ ret = i_stream_read_more(stream->parent, &data, &size);
+ size = I_MIN(size, IOSTREAM_LZ4_CHUNK_PREFIX_LEN - zstream->chunk_buf->used);
+ buffer_append(zstream->chunk_buf, data, size);
+ i_stream_skip(stream->parent, size);
+ if (ret < 0) {
+ i_assert(ret != -2);
+ stream->istream.stream_errno = stream->parent->stream_errno;
+ if (stream->istream.stream_errno == 0) {
+ stream->istream.eof = TRUE;
+ stream->cached_stream_size =
+ stream->istream.v_offset +
+ stream->pos - stream->skip;
+ }
+ return ret;
+ }
+ i_assert(ret != 0 || !stream->istream.blocking);
+ if (ret == 0)
+ return ret;
+ if (zstream->chunk_buf->used < IOSTREAM_LZ4_CHUNK_PREFIX_LEN)
+ return 0;
+ zstream->chunk_size = zstream->chunk_left =
+ be32_to_cpu_unaligned(zstream->chunk_buf->data);
+ if (zstream->chunk_size == 0 ||
+ zstream->chunk_size > ISTREAM_LZ4_CHUNK_SIZE) {
+ lz4_read_error(zstream, t_strdup_printf(
+ "invalid lz4 chunk size: %u", zstream->chunk_size));
+ stream->istream.stream_errno = EINVAL;
+ return -1;
+ }
+ buffer_set_used_size(zstream->chunk_buf, 0);
+ return 1;
+}
+
+static ssize_t i_stream_lz4_read(struct istream_private *stream)
+{
+ struct lz4_istream *zstream = (struct lz4_istream *)stream;
+ const unsigned char *data;
+ size_t size;
+ int ret;
+
+ /* if we already have max_buffer_size amount of data, fail here */
+ if (stream->pos - stream->skip >= i_stream_get_max_buffer_size(&stream->istream))
+ return -2;
+
+ if (!zstream->header_read) {
+ if ((ret = i_stream_lz4_read_header(zstream)) <= 0) {
+ stream->istream.eof = TRUE;
+ return ret;
+ }
+ zstream->header_read = TRUE;
+ }
+
+ if (zstream->chunk_left == 0) {
+ while ((ret = i_stream_lz4_read_chunk_header(zstream)) == 0) {
+ if (!stream->istream.blocking)
+ return 0;
+ }
+ if (ret < 0)
+ return ret;
+ }
+
+ /* read the whole compressed chunk into memory */
+ while (zstream->chunk_left > 0 &&
+ (ret = i_stream_read_more(zstream->istream.parent, &data, &size)) > 0) {
+ if (size > zstream->chunk_left)
+ size = zstream->chunk_left;
+ buffer_append(zstream->chunk_buf, data, size);
+ i_stream_skip(zstream->istream.parent, size);
+ zstream->chunk_left -= size;
+ }
+ if (zstream->chunk_left > 0) {
+ if (ret == -1 && zstream->istream.parent->stream_errno == 0) {
+ lz4_read_error(zstream, "truncated lz4 chunk");
+ stream->istream.stream_errno = EPIPE;
+ return -1;
+ }
+ zstream->istream.istream.stream_errno =
+ zstream->istream.parent->stream_errno;
+ i_assert(ret != 0 || !stream->istream.blocking);
+ return ret;
+ }
+ /* if we already have max_buffer_size amount of data, fail here */
+ if (stream->pos - stream->skip >= i_stream_get_max_buffer_size(&stream->istream))
+ return -2;
+ if (i_stream_get_data_size(zstream->istream.parent) > 0) {
+ /* Parent stream was only partially consumed. Set the stream's
+ IO as pending to avoid hangs. */
+ i_stream_set_input_pending(&zstream->istream.istream, TRUE);
+ }
+ /* allocate enough space for the old data and the new
+ decompressed chunk. we don't know the original compressed size,
+ so just allocate the max amount of memory. */
+ void *dest = i_stream_alloc(stream, zstream->max_uncompressed_chunk_size);
+ ret = LZ4_decompress_safe(zstream->chunk_buf->data, dest,
+ zstream->chunk_buf->used,
+ zstream->max_uncompressed_chunk_size);
+ i_assert(ret <= (int)zstream->max_uncompressed_chunk_size);
+ if (ret < 0) {
+ lz4_read_error(zstream, "corrupted lz4 chunk");
+ stream->istream.stream_errno = EINVAL;
+ return -1;
+ }
+ i_assert(ret > 0);
+ stream->pos += ret;
+ i_assert(stream->pos <= stream->buffer_size);
+
+ /* we are going to get next chunk after this, so reset here
+ so we can reuse the chunk buf for reading next buffer prefix */
+ if (zstream->chunk_left == 0)
+ buffer_set_used_size(zstream->chunk_buf, 0);
+
+ return ret;
+}
+
+static void i_stream_lz4_reset(struct lz4_istream *zstream)
+{
+ struct istream_private *stream = &zstream->istream;
+
+ i_stream_seek(stream->parent, stream->parent_start_offset);
+ zstream->header_read = FALSE;
+ zstream->chunk_size = zstream->chunk_left = 0;
+
+ stream->parent_expected_offset = stream->parent_start_offset;
+ stream->skip = stream->pos = 0;
+ stream->istream.v_offset = 0;
+ buffer_set_used_size(zstream->chunk_buf, 0);
+}
+
+static void
+i_stream_lz4_seek(struct istream_private *stream, uoff_t v_offset, bool mark)
+{
+ struct lz4_istream *zstream = (struct lz4_istream *) stream;
+
+ if (i_stream_nonseekable_try_seek(stream, v_offset))
+ return;
+
+ /* have to seek backwards - reset state and retry */
+ i_stream_lz4_reset(zstream);
+ if (!i_stream_nonseekable_try_seek(stream, v_offset))
+ i_unreached();
+
+ if (mark)
+ zstream->marked = TRUE;
+}
+
+static void i_stream_lz4_sync(struct istream_private *stream)
+{
+ struct lz4_istream *zstream = (struct lz4_istream *) stream;
+ const struct stat *st;
+
+ if (i_stream_stat(stream->parent, FALSE, &st) == 0) {
+ if (memcmp(&zstream->last_parent_statbuf,
+ st, sizeof(*st)) == 0) {
+ /* a compressed file doesn't change unexpectedly,
+ don't clear our caches unnecessarily */
+ return;
+ }
+ zstream->last_parent_statbuf = *st;
+ }
+ i_stream_lz4_reset(zstream);
+}
+
+struct istream *i_stream_create_lz4(struct istream *input)
+{
+ struct lz4_istream *zstream;
+
+ zstream = i_new(struct lz4_istream, 1);
+
+ zstream->istream.iostream.close = i_stream_lz4_close;
+ zstream->istream.max_buffer_size = input->real_stream->max_buffer_size;
+ zstream->istream.read = i_stream_lz4_read;
+ zstream->istream.seek = i_stream_lz4_seek;
+ zstream->istream.sync = i_stream_lz4_sync;
+
+ zstream->istream.istream.readable_fd = FALSE;
+ zstream->istream.istream.blocking = input->blocking;
+ zstream->istream.istream.seekable = input->seekable;
+ zstream->chunk_buf = buffer_create_dynamic(default_pool, 1024);
+
+ return i_stream_create(&zstream->istream, input,
+ i_stream_get_fd(input), 0);
+}
+#endif
diff --git a/src/lib-compression/istream-lzma.c b/src/lib-compression/istream-lzma.c
new file mode 100644
index 0000000..7b0c2a6
--- /dev/null
+++ b/src/lib-compression/istream-lzma.c
@@ -0,0 +1,264 @@
+/* Copyright (c) 2010-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+
+#ifdef HAVE_LZMA
+
+#include "istream-private.h"
+#include "istream-zlib.h"
+#include <lzma.h>
+
+#define CHUNK_SIZE (1024*64)
+
+#define LZMA_MEMORY_LIMIT (1024*1024*80)
+
+struct lzma_istream {
+ struct istream_private istream;
+
+ lzma_stream strm;
+ uoff_t eof_offset;
+ struct stat last_parent_statbuf;
+
+ bool hdr_read:1;
+ bool marked:1;
+ bool strm_closed:1;
+};
+
+static void i_stream_lzma_close(struct iostream_private *stream,
+ bool close_parent)
+{
+ struct lzma_istream *zstream = (struct lzma_istream *)stream;
+
+ if (!zstream->strm_closed) {
+ lzma_end(&zstream->strm);
+ zstream->strm_closed = TRUE;
+ }
+ if (close_parent)
+ i_stream_close(zstream->istream.parent);
+}
+
+static void lzma_read_error(struct lzma_istream *zstream, const char *error)
+{
+ io_stream_set_error(&zstream->istream.iostream,
+ "lzma.read(%s): %s at %"PRIuUOFF_T,
+ i_stream_get_name(&zstream->istream.istream), error,
+ i_stream_get_absolute_offset(&zstream->istream.istream));
+}
+
+static int lzma_handle_error(struct lzma_istream *zstream, lzma_ret lzma_err)
+{
+ struct istream_private *stream = &zstream->istream;
+ switch (lzma_err) {
+ case LZMA_OK:
+ break;
+ case LZMA_DATA_ERROR:
+ case LZMA_BUF_ERROR:
+ lzma_read_error(zstream, "corrupted data");
+ stream->istream.stream_errno = EINVAL;
+ return -1;
+ case LZMA_FORMAT_ERROR:
+ lzma_read_error(zstream, "wrong magic in header (not xz file?)");
+ stream->istream.stream_errno = EINVAL;
+ return -1;
+ case LZMA_OPTIONS_ERROR:
+ lzma_read_error(zstream, "Unsupported xz options");
+ stream->istream.stream_errno = EIO;
+ return -1;
+ case LZMA_MEM_ERROR:
+ i_fatal_status(FATAL_OUTOFMEM, "lzma.read(%s): Out of memory",
+ i_stream_get_name(&stream->istream));
+ case LZMA_STREAM_END:
+ break;
+ default:
+ lzma_read_error(zstream, t_strdup_printf(
+ "lzma_code() failed with %d", lzma_err));
+ stream->istream.stream_errno = EIO;
+ return -1;
+ }
+ return 0;
+}
+
+static void lzma_stream_end(struct lzma_istream *zstream)
+{
+ zstream->eof_offset = zstream->istream.istream.v_offset +
+ (zstream->istream.pos - zstream->istream.skip);
+ zstream->istream.cached_stream_size = zstream->eof_offset;
+}
+
+static ssize_t i_stream_lzma_read(struct istream_private *stream)
+{
+ struct lzma_istream *zstream = (struct lzma_istream *)stream;
+ const unsigned char *data;
+ uoff_t high_offset;
+ size_t size, out_size;
+ lzma_ret ret;
+
+ high_offset = stream->istream.v_offset + (stream->pos - stream->skip);
+ if (zstream->eof_offset == high_offset) {
+ stream->istream.eof = TRUE;
+ return -1;
+ }
+
+ if (!zstream->marked) {
+ if (!i_stream_try_alloc(stream, CHUNK_SIZE, &out_size))
+ return -2; /* buffer full */
+ } else {
+ /* try to avoid compressing, so we can quickly seek backwards */
+ if (!i_stream_try_alloc_avoid_compress(stream, CHUNK_SIZE, &out_size))
+ return -2; /* buffer full */
+ }
+
+ if (i_stream_read_more(stream->parent, &data, &size) < 0) {
+ if (stream->parent->stream_errno != 0) {
+ stream->istream.stream_errno =
+ stream->parent->stream_errno;
+ } else {
+ i_assert(stream->parent->eof);
+ lzma_stream_end(zstream);
+ ret = lzma_code(&zstream->strm, LZMA_FINISH);
+ if (lzma_handle_error(zstream, ret) < 0)
+ ;
+ else if (!zstream->hdr_read) {
+ lzma_read_error(zstream, "file too small (not xz file?)");
+ stream->istream.stream_errno = EINVAL;
+ } else if (ret != LZMA_STREAM_END) {
+ lzma_read_error(zstream, "unexpected EOF");
+ stream->istream.stream_errno = EPIPE;
+ }
+ stream->istream.eof = TRUE;
+ }
+ return -1;
+ }
+ if (size == 0) {
+ /* no more input */
+ i_assert(!stream->istream.blocking);
+ return 0;
+ }
+
+ zstream->strm.next_in = data;
+ zstream->strm.avail_in = size;
+
+ zstream->strm.next_out = stream->w_buffer + stream->pos;
+ zstream->strm.avail_out = out_size;
+ if (!zstream->hdr_read && size > LZMA_STREAM_HEADER_SIZE)
+ zstream->hdr_read = TRUE;
+ ret = lzma_code(&zstream->strm, LZMA_RUN);
+
+ out_size -= zstream->strm.avail_out;
+ stream->pos += out_size;
+
+ size_t bytes_consumed = size - zstream->strm.avail_in;
+ i_stream_skip(stream->parent, bytes_consumed);
+ if (i_stream_get_data_size(stream->parent) > 0 &&
+ (bytes_consumed > 0 || out_size > 0)) {
+ /* Parent stream was only partially consumed. Set the stream's
+ IO as pending to avoid hangs. */
+ i_stream_set_input_pending(&stream->istream, TRUE);
+ }
+
+ if (lzma_handle_error(zstream, ret) < 0) {
+ return -1;
+ } else if (ret == LZMA_STREAM_END) {
+ lzma_stream_end(zstream);
+ if (out_size == 0) {
+ stream->istream.eof = TRUE;
+ return -1;
+ }
+ }
+ if (out_size == 0) {
+ /* read more input */
+ return i_stream_lzma_read(stream);
+ }
+ return out_size;
+}
+
+static void i_stream_lzma_init(struct lzma_istream *zstream)
+{
+ lzma_ret ret;
+
+ ret = lzma_stream_decoder(&zstream->strm, LZMA_MEMORY_LIMIT,
+ LZMA_CONCATENATED);
+ switch (ret) {
+ case LZMA_OK:
+ break;
+ case LZMA_MEM_ERROR:
+ i_fatal_status(FATAL_OUTOFMEM, "lzma: Out of memory");
+ default:
+ i_fatal("lzma_stream_decoder() failed with ret=%d", ret);
+ }
+}
+
+static void i_stream_lzma_reset(struct lzma_istream *zstream)
+{
+ struct istream_private *stream = &zstream->istream;
+
+ i_stream_seek(stream->parent, stream->parent_start_offset);
+ zstream->eof_offset = UOFF_T_MAX;
+ zstream->strm.next_in = NULL;
+ zstream->strm.avail_in = 0;
+
+ stream->parent_expected_offset = stream->parent_start_offset;
+ stream->skip = stream->pos = 0;
+ stream->istream.v_offset = 0;
+
+ lzma_end(&zstream->strm);
+ i_stream_lzma_init(zstream);
+}
+
+static void
+i_stream_lzma_seek(struct istream_private *stream, uoff_t v_offset, bool mark)
+{
+ struct lzma_istream *zstream = (struct lzma_istream *) stream;
+
+ if (i_stream_nonseekable_try_seek(stream, v_offset))
+ return;
+
+ /* have to seek backwards - reset state and retry */
+ i_stream_lzma_reset(zstream);
+ if (!i_stream_nonseekable_try_seek(stream, v_offset))
+ i_unreached();
+
+ if (mark)
+ zstream->marked = TRUE;
+}
+
+static void i_stream_lzma_sync(struct istream_private *stream)
+{
+ struct lzma_istream *zstream = (struct lzma_istream *) stream;
+ const struct stat *st;
+
+ if (i_stream_stat(stream->parent, FALSE, &st) == 0) {
+ if (memcmp(&zstream->last_parent_statbuf,
+ st, sizeof(*st)) == 0) {
+ /* a compressed file doesn't change unexpectedly,
+ don't clear our caches unnecessarily */
+ return;
+ }
+ zstream->last_parent_statbuf = *st;
+ }
+ i_stream_lzma_reset(zstream);
+}
+
+struct istream *i_stream_create_lzma(struct istream *input)
+{
+ struct lzma_istream *zstream;
+
+ zstream = i_new(struct lzma_istream, 1);
+ zstream->eof_offset = UOFF_T_MAX;
+
+ i_stream_lzma_init(zstream);
+
+ zstream->istream.iostream.close = i_stream_lzma_close;
+ zstream->istream.max_buffer_size = input->real_stream->max_buffer_size;
+ zstream->istream.read = i_stream_lzma_read;
+ zstream->istream.seek = i_stream_lzma_seek;
+ zstream->istream.sync = i_stream_lzma_sync;
+
+ zstream->istream.istream.readable_fd = FALSE;
+ zstream->istream.istream.blocking = input->blocking;
+ zstream->istream.istream.seekable = input->seekable;
+
+ return i_stream_create(&zstream->istream, input,
+ i_stream_get_fd(input), 0);
+}
+#endif
diff --git a/src/lib-compression/istream-zlib.c b/src/lib-compression/istream-zlib.c
new file mode 100644
index 0000000..3a975c3
--- /dev/null
+++ b/src/lib-compression/istream-zlib.c
@@ -0,0 +1,431 @@
+/* Copyright (c) 2010-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+
+#ifdef HAVE_ZLIB
+
+#include "crc32.h"
+#include "istream-private.h"
+#include "istream-zlib.h"
+#include <zlib.h>
+
+#define CHUNK_SIZE (1024*64)
+
+#define GZ_HEADER_MIN_SIZE 10
+#define GZ_TRAILER_SIZE 8
+
+#define GZ_MAGIC1 0x1f
+#define GZ_MAGIC2 0x8b
+#define GZ_FLAG_FHCRC 0x02
+#define GZ_FLAG_FEXTRA 0x04
+#define GZ_FLAG_FNAME 0x08
+#define GZ_FLAG_FCOMMENT 0x10
+
+struct zlib_istream {
+ struct istream_private istream;
+
+ z_stream zs;
+ uoff_t eof_offset;
+ size_t prev_size;
+ uint32_t crc32;
+ struct stat last_parent_statbuf;
+
+ bool gz:1;
+ bool marked:1;
+ bool header_read:1;
+ bool trailer_read:1;
+ bool zs_closed:1;
+ bool starting_concated_output:1;
+};
+
+static void i_stream_zlib_init(struct zlib_istream *zstream);
+
+static void i_stream_zlib_close(struct iostream_private *stream,
+ bool close_parent)
+{
+ struct zlib_istream *zstream = (struct zlib_istream *)stream;
+
+ if (!zstream->zs_closed) {
+ (void)inflateEnd(&zstream->zs);
+ zstream->zs_closed = TRUE;
+ }
+ if (close_parent)
+ i_stream_close(zstream->istream.parent);
+}
+
+static void zlib_read_error(struct zlib_istream *zstream, const char *error)
+{
+ io_stream_set_error(&zstream->istream.iostream,
+ "zlib.read(%s): %s at %"PRIuUOFF_T,
+ i_stream_get_name(&zstream->istream.istream), error,
+ i_stream_get_absolute_offset(&zstream->istream.istream));
+}
+
+static int i_stream_zlib_read_header(struct istream_private *stream)
+{
+ struct zlib_istream *zstream = (struct zlib_istream *)stream;
+ const unsigned char *data;
+ size_t size;
+ unsigned int pos, fextra_size;
+ int ret;
+
+ ret = i_stream_read_bytes(stream->parent, &data, &size,
+ zstream->prev_size + 1);
+ if (size == zstream->prev_size) {
+ stream->istream.stream_errno = stream->parent->stream_errno;
+ if (ret == -1 && stream->istream.stream_errno == 0) {
+ zlib_read_error(zstream, "missing gz header");
+ stream->istream.stream_errno = EINVAL;
+ }
+ if (ret == -2) {
+ zlib_read_error(zstream, "gz header is too large");
+ stream->istream.stream_errno = EINVAL;
+ ret = -1;
+ }
+ return ret;
+ }
+ zstream->prev_size = size;
+
+ if (size < GZ_HEADER_MIN_SIZE)
+ return 0;
+ pos = GZ_HEADER_MIN_SIZE;
+
+ if (data[0] != GZ_MAGIC1 || data[1] != GZ_MAGIC2) {
+ /* missing gzip magic header */
+ zlib_read_error(zstream, "wrong magic in header (not gz file?)");
+ stream->istream.stream_errno = EINVAL;
+ return -1;
+ }
+ if ((data[3] & GZ_FLAG_FEXTRA) != 0) {
+ if (pos + 2 > size)
+ return 0;
+
+ fextra_size = le16_to_cpu_unaligned(&data[pos]);
+ pos += 2;
+ if (pos + fextra_size > size)
+ return 0;
+ pos += fextra_size;
+ }
+ if ((data[3] & GZ_FLAG_FNAME) != 0) {
+ do {
+ if (pos == size)
+ return 0;
+ } while (data[pos++] != '\0');
+ }
+ if ((data[3] & GZ_FLAG_FCOMMENT) != 0) {
+ do {
+ if (pos == size)
+ return 0;
+ } while (data[pos++] != '\0');
+ }
+ if ((data[3] & GZ_FLAG_FHCRC) != 0) {
+ if (pos + 2 > size)
+ return 0;
+ pos += 2;
+ }
+ i_stream_skip(stream->parent, pos);
+ zstream->prev_size = 0;
+ return 1;
+}
+
+static int i_stream_zlib_read_trailer(struct zlib_istream *zstream)
+{
+ struct istream_private *stream = &zstream->istream;
+ const unsigned char *data;
+ size_t size;
+ int ret;
+
+ ret = i_stream_read_bytes(stream->parent, &data, &size,
+ GZ_TRAILER_SIZE);
+ if (size == zstream->prev_size) {
+ stream->istream.stream_errno = stream->parent->stream_errno;
+ if (ret == -1 && stream->istream.stream_errno == 0) {
+ zlib_read_error(zstream, "missing gz trailer");
+ stream->istream.stream_errno = EINVAL;
+ }
+ return ret;
+ }
+ zstream->prev_size = size;
+
+ if (size < GZ_TRAILER_SIZE)
+ return 0;
+
+ if (le32_to_cpu_unaligned(data) != zstream->crc32) {
+ zlib_read_error(zstream, "gz trailer has wrong CRC value");
+ stream->istream.stream_errno = EINVAL;
+ return -1;
+ }
+ i_stream_skip(stream->parent, GZ_TRAILER_SIZE);
+ zstream->prev_size = 0;
+ zstream->trailer_read = TRUE;
+ return 1;
+}
+
+static ssize_t i_stream_zlib_read(struct istream_private *stream)
+{
+ struct zlib_istream *zstream = (struct zlib_istream *)stream;
+ const unsigned char *data;
+ uoff_t high_offset;
+ size_t size, out_size;
+ int ret;
+
+ high_offset = stream->istream.v_offset + (stream->pos - stream->skip);
+ if (zstream->eof_offset == high_offset) {
+ /* zlib library returned EOF. */
+ if (!zstream->gz) {
+ /* deflate - ignore if there's still more data */
+ stream->istream.eof = TRUE;
+ return -1;
+ }
+ /* gz format - read the trailer */
+ if (!zstream->trailer_read) {
+ do {
+ ret = i_stream_zlib_read_trailer(zstream);
+ } while (ret == 0 && stream->istream.blocking);
+ if (ret <= 0)
+ return ret;
+ }
+ /* See if there's another concatenated gz stream. */
+ if (i_stream_read_eof(stream->parent)) {
+ /* EOF or error */
+ stream->istream.stream_errno =
+ stream->parent->stream_errno;
+ stream->istream.eof = TRUE;
+ return -1;
+ }
+ /* Multiple gz streams concatenated together */
+ zstream->starting_concated_output = TRUE;
+ }
+ if (zstream->starting_concated_output) {
+ /* make sure there actually is something in parent stream.
+ we don't want to reset the stream unless we actually see
+ some concated output. */
+ ret = i_stream_read_more(stream->parent, &data, &size);
+ if (ret <= 0) {
+ if (ret == 0)
+ return 0;
+ if (stream->parent->stream_errno != 0) {
+ stream->istream.stream_errno =
+ stream->parent->stream_errno;
+ }
+ stream->istream.eof = TRUE;
+ return -1;
+ }
+
+ /* gzip file with concatenated content */
+ stream->cached_stream_size = UOFF_T_MAX;
+ zstream->eof_offset = UOFF_T_MAX;
+ zstream->header_read = FALSE;
+ zstream->trailer_read = FALSE;
+ zstream->crc32 = 0;
+ zstream->starting_concated_output = FALSE;
+
+ (void)inflateEnd(&zstream->zs);
+ i_stream_zlib_init(zstream);
+ }
+
+ if (!zstream->header_read) {
+ do {
+ ret = i_stream_zlib_read_header(stream);
+ } while (ret == 0 && stream->istream.blocking);
+ if (ret <= 0)
+ return ret;
+ zstream->header_read = TRUE;
+ }
+
+ if (!zstream->marked) {
+ if (!i_stream_try_alloc(stream, CHUNK_SIZE, &out_size))
+ return -2; /* buffer full */
+ } else {
+ /* try to avoid compressing, so we can quickly seek backwards */
+ if (!i_stream_try_alloc_avoid_compress(stream, CHUNK_SIZE, &out_size))
+ return -2; /* buffer full */
+ }
+
+ if (i_stream_read_more(stream->parent, &data, &size) < 0) {
+ if (stream->parent->stream_errno != 0) {
+ stream->istream.stream_errno =
+ stream->parent->stream_errno;
+ } else {
+ i_assert(stream->parent->eof);
+ zlib_read_error(zstream, "unexpected EOF");
+ stream->istream.stream_errno = EPIPE;
+ }
+ return -1;
+ }
+ if (size == 0) {
+ /* no more input */
+ i_assert(!stream->istream.blocking);
+ return 0;
+ }
+
+ zstream->zs.next_in = (void *)data;
+ zstream->zs.avail_in = size;
+
+ zstream->zs.next_out = stream->w_buffer + stream->pos;
+ zstream->zs.avail_out = out_size;
+ ret = inflate(&zstream->zs, Z_SYNC_FLUSH);
+
+ out_size -= zstream->zs.avail_out;
+ zstream->crc32 = crc32_data_more(zstream->crc32,
+ stream->w_buffer + stream->pos,
+ out_size);
+ stream->pos += out_size;
+
+ size_t bytes_consumed = size - zstream->zs.avail_in;
+ i_stream_skip(stream->parent, bytes_consumed);
+ if (i_stream_get_data_size(stream->parent) > 0 &&
+ (bytes_consumed > 0 || out_size > 0)) {
+ /* Parent stream was only partially consumed. Set the stream's
+ IO as pending to avoid hangs. */
+ i_stream_set_input_pending(&stream->istream, TRUE);
+ }
+
+ switch (ret) {
+ case Z_OK:
+ break;
+ case Z_NEED_DICT:
+ zlib_read_error(zstream, "can't read file without dict");
+ stream->istream.stream_errno = EIO;
+ return -1;
+ case Z_DATA_ERROR:
+ zlib_read_error(zstream, "corrupted data");
+ stream->istream.stream_errno = EINVAL;
+ return -1;
+ case Z_MEM_ERROR:
+ i_fatal_status(FATAL_OUTOFMEM, "zlib.read(%s): Out of memory",
+ i_stream_get_name(&stream->istream));
+ case Z_STREAM_END:
+ zstream->eof_offset = stream->istream.v_offset +
+ (stream->pos - stream->skip);
+ stream->cached_stream_size = zstream->eof_offset;
+ zstream->zs.avail_in = 0;
+
+ if (!zstream->trailer_read) {
+ /* try to read and verify the trailer, we might not
+ be called again. */
+ if (i_stream_zlib_read_trailer(zstream) < 0)
+ return -1;
+ }
+ break;
+ default:
+ i_fatal("inflate() failed with %d", ret);
+ }
+ if (out_size == 0) {
+ /* read more input */
+ return i_stream_zlib_read(stream);
+ }
+ return out_size;
+}
+
+static void i_stream_zlib_init(struct zlib_istream *zstream)
+{
+ int ret;
+
+ ret = inflateInit2(&zstream->zs, -15);
+ switch (ret) {
+ case Z_OK:
+ break;
+ case Z_MEM_ERROR:
+ i_fatal_status(FATAL_OUTOFMEM, "zlib: Out of memory");
+ case Z_VERSION_ERROR:
+ i_fatal("Wrong zlib library version (broken compilation)");
+ case Z_STREAM_ERROR:
+ i_fatal("zlib: Invalid parameters");
+ default:
+ i_fatal("inflateInit() failed with %d", ret);
+ }
+ zstream->header_read = !zstream->gz;
+ zstream->trailer_read = !zstream->gz;
+}
+
+static void i_stream_zlib_reset(struct zlib_istream *zstream)
+{
+ struct istream_private *stream = &zstream->istream;
+
+ i_stream_seek(stream->parent, stream->parent_start_offset);
+ zstream->eof_offset = UOFF_T_MAX;
+ zstream->crc32 = 0;
+
+ zstream->zs.next_in = NULL;
+ zstream->zs.avail_in = 0;
+
+ stream->parent_expected_offset = stream->parent_start_offset;
+ stream->skip = stream->pos = 0;
+ stream->istream.v_offset = 0;
+ stream->high_pos = 0;
+ zstream->prev_size = 0;
+
+ (void)inflateEnd(&zstream->zs);
+ i_stream_zlib_init(zstream);
+}
+
+static void
+i_stream_zlib_seek(struct istream_private *stream, uoff_t v_offset, bool mark)
+{
+ struct zlib_istream *zstream = (struct zlib_istream *) stream;
+
+ if (i_stream_nonseekable_try_seek(stream, v_offset))
+ return;
+
+ /* have to seek backwards - reset state and retry */
+ i_stream_zlib_reset(zstream);
+ if (!i_stream_nonseekable_try_seek(stream, v_offset))
+ i_unreached();
+
+ if (mark)
+ zstream->marked = TRUE;
+}
+
+static void i_stream_zlib_sync(struct istream_private *stream)
+{
+ struct zlib_istream *zstream = (struct zlib_istream *) stream;
+ const struct stat *st;
+
+ if (i_stream_stat(stream->parent, FALSE, &st) == 0) {
+ if (memcmp(&zstream->last_parent_statbuf,
+ st, sizeof(*st)) == 0) {
+ /* a compressed file doesn't change unexpectedly,
+ don't clear our caches unnecessarily */
+ return;
+ }
+ zstream->last_parent_statbuf = *st;
+ }
+ i_stream_zlib_reset(zstream);
+}
+
+static struct istream *
+i_stream_create_zlib(struct istream *input, bool gz)
+{
+ struct zlib_istream *zstream;
+
+ zstream = i_new(struct zlib_istream, 1);
+ zstream->eof_offset = UOFF_T_MAX;
+ zstream->gz = gz;
+
+ i_stream_zlib_init(zstream);
+
+ zstream->istream.iostream.close = i_stream_zlib_close;
+ zstream->istream.max_buffer_size = input->real_stream->max_buffer_size;
+ zstream->istream.read = i_stream_zlib_read;
+ zstream->istream.seek = i_stream_zlib_seek;
+ zstream->istream.sync = i_stream_zlib_sync;
+
+ zstream->istream.istream.readable_fd = FALSE;
+ zstream->istream.istream.blocking = input->blocking;
+ zstream->istream.istream.seekable = input->seekable;
+
+ return i_stream_create(&zstream->istream, input,
+ i_stream_get_fd(input), 0);
+}
+
+struct istream *i_stream_create_gz(struct istream *input)
+{
+ return i_stream_create_zlib(input, TRUE);
+}
+
+struct istream *i_stream_create_deflate(struct istream *input)
+{
+ return i_stream_create_zlib(input, FALSE);
+}
+#endif
diff --git a/src/lib-compression/istream-zlib.h b/src/lib-compression/istream-zlib.h
new file mode 100644
index 0000000..041fbf3
--- /dev/null
+++ b/src/lib-compression/istream-zlib.h
@@ -0,0 +1,11 @@
+#ifndef ISTREAM_ZLIB_H
+#define ISTREAM_ZLIB_H
+
+struct istream *i_stream_create_gz(struct istream *input);
+struct istream *i_stream_create_deflate(struct istream *input);
+struct istream *i_stream_create_bz2(struct istream *input);
+struct istream *i_stream_create_lzma(struct istream *input);
+struct istream *i_stream_create_lz4(struct istream *input);
+struct istream *i_stream_create_zstd(struct istream *input);
+
+#endif
diff --git a/src/lib-compression/istream-zstd.c b/src/lib-compression/istream-zstd.c
new file mode 100644
index 0000000..9e059bc
--- /dev/null
+++ b/src/lib-compression/istream-zstd.c
@@ -0,0 +1,268 @@
+/* Copyright (c) 2020 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+
+#ifdef HAVE_ZSTD
+
+#include "buffer.h"
+#include "istream-private.h"
+#include "istream-zlib.h"
+
+#include "zstd.h"
+#include "zstd_errors.h"
+#include "iostream-zstd-private.h"
+
+#ifndef HAVE_ZSTD_GETERRORCODE
+ZSTD_ErrorCode ZSTD_getErrorCode(size_t functionResult)
+{
+ ssize_t errcode = (ssize_t)functionResult;
+ if (errcode < 0)
+ return -errcode;
+ return ZSTD_error_no_error;
+}
+#endif
+
+struct zstd_istream {
+ struct istream_private istream;
+
+ ZSTD_DStream *dstream;
+ ZSTD_inBuffer input;
+ ZSTD_outBuffer output;
+
+ struct stat last_parent_statbuf;
+
+ /* ZSTD input size */
+ size_t input_size;
+
+ /* storage for frames */
+ buffer_t *frame_buffer;
+
+ /* storage for data */
+ buffer_t *data_buffer;
+
+ bool hdr_read:1;
+ bool marked:1;
+ bool zs_closed:1;
+ /* is there data remaining */
+ bool remain:1;
+};
+
+static void i_stream_zstd_init(struct zstd_istream *zstream)
+{
+ zstream->dstream = ZSTD_createDStream();
+ if (zstream->dstream == NULL)
+ i_fatal_status(FATAL_OUTOFMEM, "zstd: Out of memory");
+ ZSTD_initDStream(zstream->dstream);
+ zstream->input_size = ZSTD_DStreamInSize();
+ if (zstream->frame_buffer == NULL)
+ zstream->frame_buffer = buffer_create_dynamic(default_pool, ZSTD_DStreamInSize());
+ else
+ buffer_set_used_size(zstream->frame_buffer, 0);
+ if (zstream->data_buffer == NULL)
+ zstream->data_buffer = buffer_create_dynamic(default_pool, ZSTD_DStreamOutSize());
+ else
+ buffer_set_used_size(zstream->data_buffer, 0);
+ zstream->zs_closed = FALSE;
+}
+
+static void i_stream_zstd_deinit(struct zstd_istream *zstream, bool reuse_buffers)
+{
+ (void)ZSTD_freeDStream(zstream->dstream);
+ zstream->dstream = NULL;
+ if (!reuse_buffers) {
+ buffer_free(&zstream->frame_buffer);
+ buffer_free(&zstream->data_buffer);
+ }
+ zstream->zs_closed = TRUE;
+ i_zero(&zstream->input);
+}
+
+static void i_stream_zstd_close(struct iostream_private *stream,
+ bool close_parent)
+{
+ struct istream_private *_istream =
+ container_of(stream, struct istream_private, iostream);
+ struct zstd_istream *zstream =
+ container_of(_istream, struct zstd_istream, istream);
+ if (!zstream->zs_closed)
+ i_stream_zstd_deinit(zstream, FALSE);
+ buffer_free(&zstream->frame_buffer);
+ if (close_parent)
+ i_stream_close(zstream->istream.parent);
+}
+
+static void i_stream_zstd_read_error(struct zstd_istream *zstream, size_t err)
+{
+ ZSTD_ErrorCode errcode = zstd_version_errcode(ZSTD_getErrorCode(err));
+ const char *error = ZSTD_getErrorName(err);
+ if (errcode == ZSTD_error_memory_allocation)
+ i_fatal_status(FATAL_OUTOFMEM, "zstd.read(%s): Out of memory",
+ i_stream_get_name(&zstream->istream.istream));
+ else if (errcode == ZSTD_error_prefix_unknown ||
+#if HAVE_DECL_ZSTD_ERROR_PARAMETER_UNSUPPORTED == 1
+ errcode == ZSTD_error_parameter_unsupported ||
+#endif
+
+ errcode == ZSTD_error_dictionary_wrong ||
+ errcode == ZSTD_error_init_missing)
+ zstream->istream.istream.stream_errno = EINVAL;
+ else
+ zstream->istream.istream.stream_errno = EIO;
+
+ io_stream_set_error(&zstream->istream.iostream,
+ "zstd.read(%s): %s at %"PRIuUOFF_T,
+ i_stream_get_name(&zstream->istream.istream), error,
+ i_stream_get_absolute_offset(&zstream->istream.istream));
+}
+
+static ssize_t i_stream_zstd_read(struct istream_private *stream)
+{
+ struct zstd_istream *zstream =
+ container_of(stream, struct zstd_istream, istream);
+ const unsigned char *data;
+ size_t size;
+
+ if (stream->istream.eof)
+ return -1;
+
+ for (;;) {
+ if (zstream->data_buffer->used > 0) {
+ if (!i_stream_try_alloc(stream, stream->max_buffer_size, &size))
+ return -2;
+ size = I_MIN(zstream->data_buffer->used, size);
+ memcpy(PTR_OFFSET(stream->w_buffer,stream->pos),
+ zstream->data_buffer->data, size);
+ stream->pos += size;
+ buffer_delete(zstream->data_buffer, 0, size);
+ return size;
+ }
+
+ /* see if we can get more */
+ if (zstream->input.pos == zstream->input.size) {
+ ssize_t ret;
+ buffer_set_used_size(zstream->frame_buffer, 0);
+ /* need to read more */
+ if ((ret = i_stream_read_more(stream->parent, &data, &size)) < 0) {
+ stream->istream.stream_errno =
+ stream->parent->stream_errno;
+ stream->istream.eof = stream->parent->eof;
+ if (stream->istream.stream_errno != 0)
+ ;
+ else if (!zstream->hdr_read)
+ stream->istream.stream_errno = EINVAL;
+ else if (zstream->remain)
+ /* truncated data */
+ stream->istream.stream_errno = EPIPE;
+ return ret;
+ }
+ if (ret == 0)
+ return 0;
+ buffer_append(zstream->frame_buffer, data, size);
+ /* NOTE: All of the parent stream input is skipped
+ over here. This is why there's no need to call
+ i_stream_set_input_pending() here like with other
+ compression istreams. */
+ i_stream_skip(stream->parent, size);
+ zstream->input.src = zstream->frame_buffer->data;
+ zstream->input.size = zstream->frame_buffer->used;
+ zstream->input.pos = 0;
+ }
+
+ i_assert(zstream->input.size > 0);
+ i_assert(zstream->data_buffer->used == 0);
+ zstream->output.dst = buffer_append_space_unsafe(zstream->data_buffer,
+ ZSTD_DStreamOutSize());
+ zstream->output.pos = 0;
+ zstream->output.size = ZSTD_DStreamOutSize();
+
+ size_t zret = ZSTD_decompressStream(zstream->dstream, &zstream->output,
+ &zstream->input);
+ if (ZSTD_isError(zret) != 0) {
+ i_stream_zstd_read_error(zstream, zret);
+ return -1;
+ }
+ /* ZSTD magic number is 4 bytes, but it's only defined after v0.8 */
+ if (!zstream->hdr_read && zstream->input.size > 4)
+ zstream->hdr_read = TRUE;
+ zstream->remain = zret > 0;
+ buffer_set_used_size(zstream->data_buffer, zstream->output.pos);
+ }
+ i_unreached();
+}
+
+static void i_stream_zstd_reset(struct zstd_istream *zstream)
+{
+ struct istream_private *stream = &zstream->istream;
+
+ i_stream_seek(stream->parent, stream->parent_start_offset);
+ stream->parent_expected_offset = stream->parent_start_offset;
+ stream->skip = stream->pos = 0;
+ stream->istream.v_offset = 0;
+ stream->high_pos = 0;
+
+ i_stream_zstd_deinit(zstream, TRUE);
+ i_stream_zstd_init(zstream);
+}
+
+static void
+i_stream_zstd_seek(struct istream_private *stream, uoff_t v_offset, bool mark)
+{
+ struct zstd_istream *zstream =
+ container_of(stream, struct zstd_istream, istream);
+
+ if (i_stream_nonseekable_try_seek(stream, v_offset))
+ return;
+
+ /* have to seek backwards - reset state and retry */
+ i_stream_zstd_reset(zstream);
+ if (!i_stream_nonseekable_try_seek(stream, v_offset))
+ i_unreached();
+
+ if (mark)
+ zstream->marked = TRUE;
+}
+
+static void i_stream_zstd_sync(struct istream_private *stream)
+{
+ struct zstd_istream *zstream =
+ container_of(stream, struct zstd_istream, istream);
+ const struct stat *st;
+
+ if (i_stream_stat(stream->parent, FALSE, &st) == 0) {
+ if (memcmp(&zstream->last_parent_statbuf,
+ st, sizeof(*st)) == 0) {
+ /* a compressed file doesn't change unexpectedly,
+ don't clear our caches unnecessarily */
+ return;
+ }
+ zstream->last_parent_statbuf = *st;
+ }
+ i_stream_zstd_reset(zstream);
+}
+
+struct istream *
+i_stream_create_zstd(struct istream *input)
+{
+ struct zstd_istream *zstream;
+
+ zstd_version_check();
+
+ zstream = i_new(struct zstd_istream, 1);
+
+ i_stream_zstd_init(zstream);
+
+ zstream->istream.iostream.close = i_stream_zstd_close;
+ zstream->istream.max_buffer_size = input->real_stream->max_buffer_size;
+ zstream->istream.read = i_stream_zstd_read;
+ zstream->istream.seek = i_stream_zstd_seek;
+ zstream->istream.sync = i_stream_zstd_sync;
+
+ zstream->istream.istream.readable_fd = FALSE;
+ zstream->istream.istream.blocking = input->blocking;
+ zstream->istream.istream.seekable = input->seekable;
+
+ return i_stream_create(&zstream->istream, input,
+ i_stream_get_fd(input), 0);
+}
+
+#endif
diff --git a/src/lib-compression/ostream-bzlib.c b/src/lib-compression/ostream-bzlib.c
new file mode 100644
index 0000000..4d072b2
--- /dev/null
+++ b/src/lib-compression/ostream-bzlib.c
@@ -0,0 +1,307 @@
+/* Copyright (c) 2010-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+
+#ifdef HAVE_BZLIB
+
+#include "ostream-private.h"
+#include "ostream-zlib.h"
+#include <bzlib.h>
+
+#define CHUNK_SIZE (1024*64)
+
+struct bzlib_ostream {
+ struct ostream_private ostream;
+ bz_stream zs;
+
+ char outbuf[CHUNK_SIZE];
+ unsigned int outbuf_offset, outbuf_used;
+
+ bool flushed:1;
+};
+
+/* in bzlib, level is actually block size. From bzlib manual:
+
+ The block size affects both the compression ratio achieved,
+ and the amount of memory needed for compression and decompression.
+
+ BlockSize 1 through BlockSize 9 specify the block size to be 100,000 bytes
+ through 900,000 bytes respectively. The default is to use the maximum block
+ size.
+
+ Larger block sizes give rapidly diminishing marginal returns.
+ Most of the compression comes from the first two or three hundred k of
+ block size, a fact worth bearing in mind when using bzip2 on small machines.
+ It is also important to appreciate that the decompression memory
+ requirement is set at compression time by the choice of block size.
+
+ * In general, try and use the largest block size memory constraints
+ allow, since that maximises the compression achieved.
+ * Compression and decompression speed are virtually unaffected by block
+ size.
+
+ Another significant point applies to files which fit in a single block -
+ that means most files you'd encounter using a large block size. The
+ amount of real memory touched is proportional to the size of the file,
+ since the file is smaller than a block. For example, compressing a file
+ 20,000 bytes long with the flag BlockSize 9 will cause the compressor to
+ allocate around 7600k of memory, but only touch 400k + 20000 * 8 = 560 kbytes
+ of it. Similarly, the decompressor will allocate 3700k but only
+ touch 100k + 20000 * 4 = 180 kbytes.
+*/
+
+int compression_get_min_level_bz2(void)
+{
+ return 1;
+}
+
+int compression_get_default_level_bz2(void)
+{
+ /* default is maximum level */
+ return 9;
+}
+
+int compression_get_max_level_bz2(void)
+{
+ return 9;
+}
+
+static void o_stream_bzlib_close(struct iostream_private *stream,
+ bool close_parent)
+{
+ struct bzlib_ostream *zstream = (struct bzlib_ostream *)stream;
+
+ (void)BZ2_bzCompressEnd(&zstream->zs);
+ if (close_parent)
+ o_stream_close(zstream->ostream.parent);
+}
+
+static int o_stream_zlib_send_outbuf(struct bzlib_ostream *zstream)
+{
+ ssize_t ret;
+ size_t size;
+
+ if (zstream->outbuf_used == 0)
+ return 1;
+
+ size = zstream->outbuf_used - zstream->outbuf_offset;
+ i_assert(size > 0);
+ ret = o_stream_send(zstream->ostream.parent,
+ zstream->outbuf + zstream->outbuf_offset, size);
+ if (ret < 0) {
+ o_stream_copy_error_from_parent(&zstream->ostream);
+ return -1;
+ }
+ if ((size_t)ret != size) {
+ zstream->outbuf_offset += ret;
+ return 0;
+ }
+ zstream->outbuf_offset = 0;
+ zstream->outbuf_used = 0;
+ return 1;
+}
+
+static ssize_t
+o_stream_bzlib_send_chunk(struct bzlib_ostream *zstream,
+ const void *data, size_t size)
+{
+ bz_stream *zs = &zstream->zs;
+ int ret;
+
+ i_assert(zstream->outbuf_used == 0);
+
+ zs->next_in = (void *)data;
+ zs->avail_in = size;
+ while (zs->avail_in > 0) {
+ if (zs->avail_out == 0) {
+ /* previous block was compressed. send it and start
+ compression for a new block. */
+ zs->next_out = zstream->outbuf;
+ zs->avail_out = sizeof(zstream->outbuf);
+
+ zstream->outbuf_used = sizeof(zstream->outbuf);
+ if ((ret = o_stream_zlib_send_outbuf(zstream)) < 0)
+ return -1;
+ if (ret == 0) {
+ /* parent stream's buffer full */
+ break;
+ }
+ }
+
+ switch ((ret = BZ2_bzCompress(zs, BZ_RUN))) {
+ case BZ_RUN_OK:
+ break;
+ case BZ_MEM_ERROR:
+ i_fatal_status(FATAL_OUTOFMEM, "bzip2.write(%s): Out of memory",
+ o_stream_get_name(&zstream->ostream.ostream));
+ default:
+ i_fatal("BZ2_bzCompress() failed with %d", ret);
+ }
+ }
+ size -= zs->avail_in;
+
+ zstream->flushed = FALSE;
+ return size;
+}
+
+static int o_stream_bzlib_send_flush(struct bzlib_ostream *zstream, bool final)
+{
+ bz_stream *zs = &zstream->zs;
+ size_t len;
+ bool done = FALSE;
+ int ret;
+
+ i_assert(zs->avail_in == 0);
+
+ if (zstream->flushed) {
+ i_assert(zstream->outbuf_used == 0);
+ return 1;
+ }
+
+ if ((ret = o_stream_flush_parent_if_needed(&zstream->ostream)) <= 0)
+ return ret;
+ if ((ret = o_stream_zlib_send_outbuf(zstream)) <= 0)
+ return ret;
+
+ /* do not attempt to finish the stream early */
+ if (!final)
+ return 1;
+
+ i_assert(zstream->outbuf_used == 0);
+ do {
+ len = sizeof(zstream->outbuf) - zs->avail_out;
+ if (len != 0) {
+ zs->next_out = zstream->outbuf;
+ zs->avail_out = sizeof(zstream->outbuf);
+
+ zstream->outbuf_used = len;
+ if ((ret = o_stream_zlib_send_outbuf(zstream)) <= 0)
+ return ret;
+ if (done)
+ break;
+ }
+
+ ret = BZ2_bzCompress(zs, BZ_FINISH);
+ switch (ret) {
+ case BZ_RUN_OK:
+ case BZ_FLUSH_OK:
+ case BZ_STREAM_END:
+ done = TRUE;
+ break;
+ case BZ_FINISH_OK:
+ break;
+ case BZ_MEM_ERROR:
+ i_fatal_status(FATAL_OUTOFMEM, "bzip2.write(%s): Out of memory",
+ o_stream_get_name(&zstream->ostream.ostream));
+ default:
+ i_fatal("BZ2_bzCompress() failed with %d", ret);
+ }
+ } while (zs->avail_out != sizeof(zstream->outbuf));
+
+ if (final)
+ zstream->flushed = TRUE;
+ i_assert(zstream->outbuf_used == 0);
+ return 1;
+}
+
+static int o_stream_bzlib_flush(struct ostream_private *stream)
+{
+ struct bzlib_ostream *zstream = (struct bzlib_ostream *)stream;
+ int ret;
+ if ((ret = o_stream_bzlib_send_flush(zstream, stream->finished)) < 0)
+ return -1;
+ else if (ret > 0)
+ return o_stream_flush_parent(stream);
+ return ret;
+}
+
+static size_t
+o_stream_bzlib_get_buffer_used_size(const struct ostream_private *stream)
+{
+ const struct bzlib_ostream *zstream =
+ (const struct bzlib_ostream *)stream;
+
+ /* outbuf has already compressed data that we're trying to send to the
+ parent stream. We're not including bzlib's internal compression
+ buffer size. */
+ return (zstream->outbuf_used - zstream->outbuf_offset) +
+ o_stream_get_buffer_used_size(stream->parent);
+}
+
+static size_t
+o_stream_bzlib_get_buffer_avail_size(const struct ostream_private *stream)
+{
+ /* FIXME: not correct - this is counting compressed size, which may be
+ too larger than uncompressed size in some situations. Fixing would
+ require some kind of additional buffering. */
+ return o_stream_get_buffer_avail_size(stream->parent);
+}
+
+static ssize_t
+o_stream_bzlib_sendv(struct ostream_private *stream,
+ const struct const_iovec *iov, unsigned int iov_count)
+{
+ struct bzlib_ostream *zstream = (struct bzlib_ostream *)stream;
+ ssize_t ret, bytes = 0;
+ unsigned int i;
+
+ if ((ret = o_stream_zlib_send_outbuf(zstream)) <= 0) {
+ /* error / we still couldn't flush existing data to
+ parent stream. */
+ return ret;
+ }
+
+ for (i = 0; i < iov_count; i++) {
+ ret = o_stream_bzlib_send_chunk(zstream, iov[i].iov_base,
+ iov[i].iov_len);
+ if (ret < 0)
+ return -1;
+ bytes += ret;
+ if ((size_t)ret != iov[i].iov_len)
+ break;
+ }
+ stream->ostream.offset += bytes;
+
+ /* avail_in!=0 check is used to detect errors. if it's non-zero here
+ it simply means we didn't send all the data */
+ zstream->zs.avail_in = 0;
+ return bytes;
+}
+
+struct ostream *o_stream_create_bz2(struct ostream *output, int level)
+{
+ struct bzlib_ostream *zstream;
+ int ret;
+
+ i_assert(level >= 1 && level <= 9);
+
+ zstream = i_new(struct bzlib_ostream, 1);
+ zstream->ostream.sendv = o_stream_bzlib_sendv;
+ zstream->ostream.flush = o_stream_bzlib_flush;
+ zstream->ostream.get_buffer_used_size =
+ o_stream_bzlib_get_buffer_used_size;
+ zstream->ostream.get_buffer_avail_size =
+ o_stream_bzlib_get_buffer_avail_size;
+ zstream->ostream.iostream.close = o_stream_bzlib_close;
+
+ ret = BZ2_bzCompressInit(&zstream->zs, level, 0, 0);
+ switch (ret) {
+ case BZ_OK:
+ break;
+ case BZ_MEM_ERROR:
+ i_fatal_status(FATAL_OUTOFMEM,
+ "bzlib: Out of memory");
+ case BZ_CONFIG_ERROR:
+ i_fatal("Wrong bzlib library version (broken compilation)");
+ case BZ_PARAM_ERROR:
+ i_fatal("bzlib: Invalid parameters");
+ default:
+ i_fatal("BZ2_bzCompressInit() failed with %d", ret);
+ }
+
+ zstream->zs.next_out = zstream->outbuf;
+ zstream->zs.avail_out = sizeof(zstream->outbuf);
+ return o_stream_create(&zstream->ostream, output,
+ o_stream_get_fd(output));
+}
+#endif
diff --git a/src/lib-compression/ostream-lz4.c b/src/lib-compression/ostream-lz4.c
new file mode 100644
index 0000000..360f475
--- /dev/null
+++ b/src/lib-compression/ostream-lz4.c
@@ -0,0 +1,249 @@
+/* Copyright (c) 2013-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+
+#ifdef HAVE_LZ4
+
+#include "ostream-private.h"
+#include "ostream-zlib.h"
+#include "iostream-lz4.h"
+#include <lz4.h>
+
+#define CHUNK_SIZE OSTREAM_LZ4_CHUNK_SIZE
+
+struct lz4_ostream {
+ struct ostream_private ostream;
+
+ unsigned char compressbuf[CHUNK_SIZE];
+ unsigned int compressbuf_offset;
+
+ /* chunk size, followed by compressed data */
+ unsigned char outbuf[IOSTREAM_LZ4_CHUNK_PREFIX_LEN +
+ LZ4_COMPRESSBOUND(CHUNK_SIZE)];
+ unsigned int outbuf_offset, outbuf_used;
+};
+
+/* There is no actual compression level in LZ4, so for legacy
+ reasons we allow 1-9 to avoid breaking anyone's config. */
+int compression_get_min_level_lz4(void)
+{
+ return 1;
+}
+
+int compression_get_default_level_lz4(void)
+{
+ return 1;
+}
+
+int compression_get_max_level_lz4(void)
+{
+ return 9;
+}
+
+static void o_stream_lz4_close(struct iostream_private *stream,
+ bool close_parent)
+{
+ struct lz4_ostream *zstream = (struct lz4_ostream *)stream;
+
+ if (close_parent)
+ o_stream_close(zstream->ostream.parent);
+}
+
+static int o_stream_lz4_send_outbuf(struct lz4_ostream *zstream)
+{
+ ssize_t ret;
+ size_t size;
+
+ if (zstream->outbuf_used == 0)
+ return 1;
+
+ size = zstream->outbuf_used - zstream->outbuf_offset;
+ i_assert(size > 0);
+ ret = o_stream_send(zstream->ostream.parent,
+ zstream->outbuf + zstream->outbuf_offset, size);
+ if (ret < 0) {
+ o_stream_copy_error_from_parent(&zstream->ostream);
+ return -1;
+ }
+ if ((size_t)ret != size) {
+ zstream->outbuf_offset += ret;
+ return 0;
+ }
+ zstream->outbuf_offset = 0;
+ zstream->outbuf_used = 0;
+ return 1;
+}
+
+static int o_stream_lz4_compress(struct lz4_ostream *zstream)
+{
+ uint32_t chunk_size;
+ int ret;
+
+ if (zstream->compressbuf_offset == 0)
+ return 1;
+ if ((ret = o_stream_lz4_send_outbuf(zstream)) <= 0)
+ return ret;
+
+ i_assert(zstream->outbuf_offset == 0);
+ i_assert(zstream->outbuf_used == 0);
+
+#if defined(HAVE_LZ4_COMPRESS_DEFAULT)
+ int max_dest_size = LZ4_compressBound(zstream->compressbuf_offset);
+ i_assert(max_dest_size >= 0);
+ if (max_dest_size == 0) {
+ io_stream_set_error(&zstream->ostream.iostream,
+ "lz4-compress: input size %u too large (> %u)",
+ zstream->compressbuf_offset, LZ4_MAX_INPUT_SIZE);
+ zstream->ostream.ostream.stream_errno = EINVAL;
+ return -1;
+ }
+ ret = LZ4_compress_default((void *)zstream->compressbuf,
+ (void *)(zstream->outbuf +
+ IOSTREAM_LZ4_CHUNK_PREFIX_LEN),
+ zstream->compressbuf_offset,
+ max_dest_size);
+#else
+ ret = LZ4_compress((void *)zstream->compressbuf,
+ (void *)(zstream->outbuf +
+ IOSTREAM_LZ4_CHUNK_PREFIX_LEN),
+ zstream->compressbuf_offset);
+#endif /* defined(HAVE_LZ4_COMPRESS_DEFAULT) */
+ i_assert(ret > 0 && (unsigned int)ret <= sizeof(zstream->outbuf) -
+ IOSTREAM_LZ4_CHUNK_PREFIX_LEN);
+ zstream->outbuf_used = IOSTREAM_LZ4_CHUNK_PREFIX_LEN + ret;
+ chunk_size = zstream->outbuf_used - IOSTREAM_LZ4_CHUNK_PREFIX_LEN;
+ zstream->outbuf[0] = (chunk_size & 0xff000000) >> 24;
+ zstream->outbuf[1] = (chunk_size & 0x00ff0000) >> 16;
+ zstream->outbuf[2] = (chunk_size & 0x0000ff00) >> 8;
+ zstream->outbuf[3] = (chunk_size & 0x000000ff);
+ zstream->compressbuf_offset = 0;
+ return 1;
+}
+
+static ssize_t
+o_stream_lz4_send_chunk(struct lz4_ostream *zstream,
+ const void *data, size_t size)
+{
+ size_t max_size;
+ ssize_t added_bytes = 0;
+ int ret;
+
+ i_assert(zstream->outbuf_used == 0);
+
+ do {
+ max_size = I_MIN(size, sizeof(zstream->compressbuf) -
+ zstream->compressbuf_offset);
+ memcpy(zstream->compressbuf + zstream->compressbuf_offset,
+ data, max_size);
+ zstream->compressbuf_offset += max_size;
+
+ data = CONST_PTR_OFFSET(data, max_size);
+ size -= max_size;
+ added_bytes += max_size;
+
+ if (zstream->compressbuf_offset == sizeof(zstream->compressbuf)) {
+ ret = o_stream_lz4_compress(zstream);
+ if (ret <= 0)
+ return added_bytes != 0 ? added_bytes : ret;
+ }
+ } while (size > 0);
+
+ return added_bytes;
+}
+
+static int o_stream_lz4_flush(struct ostream_private *stream)
+{
+ struct lz4_ostream *zstream = (struct lz4_ostream *)stream;
+
+ if (o_stream_lz4_compress(zstream) < 0)
+ return -1;
+ if (o_stream_lz4_send_outbuf(zstream) < 0)
+ return -1;
+
+ return o_stream_flush_parent(stream);
+}
+
+static size_t
+o_stream_lz4_get_buffer_used_size(const struct ostream_private *stream)
+{
+ const struct lz4_ostream *zstream =
+ (const struct lz4_ostream *)stream;
+
+ /* outbuf has already compressed data that we're trying to send to the
+ parent stream. compressbuf isn't included in the return value,
+ because it needs to be filled up or flushed. */
+ return (zstream->outbuf_used - zstream->outbuf_offset) +
+ o_stream_get_buffer_used_size(stream->parent);
+}
+
+static size_t
+o_stream_lz4_get_buffer_avail_size(const struct ostream_private *stream)
+{
+ const struct lz4_ostream *zstream =
+ (const struct lz4_ostream *)stream;
+
+ /* We're only guaranteed to accept data to compressbuf. The parent
+ stream might have space, but since compressed data gets written
+ there it's not really known how much we can actually write there. */
+ return sizeof(zstream->compressbuf) - zstream->compressbuf_offset;
+}
+
+static ssize_t
+o_stream_lz4_sendv(struct ostream_private *stream,
+ const struct const_iovec *iov, unsigned int iov_count)
+{
+ struct lz4_ostream *zstream = (struct lz4_ostream *)stream;
+ ssize_t ret, bytes = 0;
+ unsigned int i;
+
+ if ((ret = o_stream_lz4_send_outbuf(zstream)) <= 0) {
+ /* error / we still couldn't flush existing data to
+ parent stream. */
+ return ret;
+ }
+
+ for (i = 0; i < iov_count; i++) {
+ ret = o_stream_lz4_send_chunk(zstream, iov[i].iov_base,
+ iov[i].iov_len);
+ if (ret < 0)
+ return -1;
+ bytes += ret;
+ if ((size_t)ret != iov[i].iov_len)
+ break;
+ }
+ stream->ostream.offset += bytes;
+ return bytes;
+}
+
+struct ostream *o_stream_create_lz4(struct ostream *output, int level)
+{
+ struct iostream_lz4_header *hdr;
+ struct lz4_ostream *zstream;
+
+ i_assert(level >= 1 && level <= 9);
+
+ zstream = i_new(struct lz4_ostream, 1);
+ zstream->ostream.sendv = o_stream_lz4_sendv;
+ zstream->ostream.flush = o_stream_lz4_flush;
+ zstream->ostream.get_buffer_used_size =
+ o_stream_lz4_get_buffer_used_size;
+ zstream->ostream.get_buffer_avail_size =
+ o_stream_lz4_get_buffer_avail_size;
+ zstream->ostream.iostream.close = o_stream_lz4_close;
+
+ i_assert(sizeof(zstream->outbuf) >= sizeof(*hdr));
+ hdr = (void *)zstream->outbuf;
+ memcpy(hdr->magic, IOSTREAM_LZ4_MAGIC, sizeof(hdr->magic));
+ hdr->max_uncompressed_chunk_size[0] =
+ (OSTREAM_LZ4_CHUNK_SIZE & 0xff000000) >> 24;
+ hdr->max_uncompressed_chunk_size[1] =
+ (OSTREAM_LZ4_CHUNK_SIZE & 0x00ff0000) >> 16;
+ hdr->max_uncompressed_chunk_size[2] =
+ (OSTREAM_LZ4_CHUNK_SIZE & 0x0000ff00) >> 8;
+ hdr->max_uncompressed_chunk_size[3] =
+ (OSTREAM_LZ4_CHUNK_SIZE & 0x000000ff);
+ zstream->outbuf_used = sizeof(*hdr);
+ return o_stream_create(&zstream->ostream, output,
+ o_stream_get_fd(output));
+}
+#endif
diff --git a/src/lib-compression/ostream-zlib.c b/src/lib-compression/ostream-zlib.c
new file mode 100644
index 0000000..7267917
--- /dev/null
+++ b/src/lib-compression/ostream-zlib.c
@@ -0,0 +1,392 @@
+/* Copyright (c) 2010-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+
+#ifdef HAVE_ZLIB
+
+#include "crc32.h"
+#include "ostream-private.h"
+#include "ostream-zlib.h"
+#include <zlib.h>
+
+#define CHUNK_SIZE (1024*32)
+#define ZLIB_OS_CODE 0x03 /* Unix */
+
+struct zlib_ostream {
+ struct ostream_private ostream;
+ z_stream zs;
+
+ unsigned char gz_header[10];
+ unsigned char outbuf[CHUNK_SIZE];
+ unsigned int outbuf_offset, outbuf_used;
+ unsigned int header_bytes_left;
+
+ uint32_t crc, bytes32;
+
+ bool gz:1;
+ bool flushed:1;
+};
+
+int compression_get_min_level_gz(void)
+{
+ return Z_NO_COMPRESSION;
+}
+
+int compression_get_default_level_gz(void)
+{
+ return Z_DEFAULT_COMPRESSION;
+}
+
+int compression_get_max_level_gz(void)
+{
+ return Z_BEST_COMPRESSION;
+}
+
+static void o_stream_zlib_close(struct iostream_private *stream,
+ bool close_parent)
+{
+ struct zlib_ostream *zstream = (struct zlib_ostream *)stream;
+
+ i_assert(zstream->ostream.finished ||
+ zstream->ostream.ostream.stream_errno != 0 ||
+ zstream->ostream.error_handling_disabled);
+ (void)deflateEnd(&zstream->zs);
+ if (close_parent)
+ o_stream_close(zstream->ostream.parent);
+}
+
+static int o_stream_zlib_send_gz_header(struct zlib_ostream *zstream)
+{
+ size_t header_send_offset =
+ sizeof(zstream->gz_header) - zstream->header_bytes_left;
+ ssize_t ret;
+
+ i_assert(zstream->header_bytes_left <= sizeof(zstream->gz_header));
+ ret = o_stream_send(zstream->ostream.parent,
+ zstream->gz_header + header_send_offset,
+ zstream->header_bytes_left);
+ if (ret < 0) {
+ o_stream_copy_error_from_parent(&zstream->ostream);
+ return -1;
+ }
+ i_assert((size_t)ret <= zstream->header_bytes_left);
+ zstream->header_bytes_left -= ret;
+ return zstream->header_bytes_left == 0 ? 1 : 0;
+}
+
+static int o_stream_zlib_lsb_uint32(struct ostream *output, uint32_t num)
+{
+ unsigned char buf[sizeof(uint32_t)];
+ unsigned int i;
+
+ for (i = 0; i < sizeof(buf); i++) {
+ buf[i] = num & 0xff;
+ num >>= 8;
+ }
+ if (o_stream_send(output, buf, sizeof(buf)) != sizeof(buf))
+ return -1;
+ return 0;
+}
+
+static int o_stream_zlib_send_gz_trailer(struct zlib_ostream *zstream)
+{
+ struct ostream *output = zstream->ostream.parent;
+
+ if (!zstream->gz)
+ return 0;
+
+ if (o_stream_zlib_lsb_uint32(output, zstream->crc) < 0 ||
+ o_stream_zlib_lsb_uint32(output, zstream->bytes32) < 0) {
+ o_stream_copy_error_from_parent(&zstream->ostream);
+ return -1;
+ }
+ return 0;
+}
+
+static int o_stream_zlib_send_outbuf(struct zlib_ostream *zstream)
+{
+ ssize_t ret;
+ size_t size;
+
+ if (zstream->outbuf_used == 0)
+ return 1;
+
+ size = zstream->outbuf_used - zstream->outbuf_offset;
+ i_assert(size > 0);
+ ret = o_stream_send(zstream->ostream.parent,
+ zstream->outbuf + zstream->outbuf_offset, size);
+ if (ret < 0) {
+ o_stream_copy_error_from_parent(&zstream->ostream);
+ return -1;
+ }
+ if ((size_t)ret != size) {
+ zstream->outbuf_offset += ret;
+ return 0;
+ }
+ zstream->outbuf_offset = 0;
+ zstream->outbuf_used = 0;
+ return 1;
+}
+
+static ssize_t
+o_stream_zlib_send_chunk(struct zlib_ostream *zstream,
+ const void *data, size_t size)
+{
+ z_stream *zs = &zstream->zs;
+ int ret, flush;
+
+ i_assert(zstream->outbuf_used == 0);
+
+ flush = zstream->ostream.corked || zstream->gz ?
+ Z_NO_FLUSH : Z_SYNC_FLUSH;
+
+ if (zstream->header_bytes_left > 0) {
+ if ((ret = o_stream_zlib_send_gz_header(zstream)) <= 0)
+ return ret;
+ }
+
+ zs->next_in = (void *)data;
+ zs->avail_in = size;
+ while (zs->avail_in > 0) {
+ if (zs->avail_out == 0) {
+ /* previous block was compressed. send it and start
+ compression for a new block. */
+ zs->next_out = zstream->outbuf;
+ zs->avail_out = sizeof(zstream->outbuf);
+
+ zstream->outbuf_used = sizeof(zstream->outbuf);
+ if ((ret = o_stream_zlib_send_outbuf(zstream)) < 0)
+ return -1;
+ if (ret == 0) {
+ /* parent stream's buffer full */
+ break;
+ }
+ }
+
+ ret = deflate(zs, flush);
+ switch (ret) {
+ case Z_OK:
+ case Z_BUF_ERROR:
+ break;
+ case Z_MEM_ERROR:
+ i_fatal_status(FATAL_OUTOFMEM, "zlib: Out of memory");
+ case Z_STREAM_ERROR:
+ i_assert(zstream->gz);
+ i_panic("zlib.write(%s) failed: Can't write more data to .gz after flushing",
+ o_stream_get_name(&zstream->ostream.ostream));
+ default:
+ i_panic("zlib.write(%s) failed with unexpected code %d",
+ o_stream_get_name(&zstream->ostream.ostream), ret);
+ }
+ }
+ size -= zs->avail_in;
+
+ zstream->crc = crc32_data_more(zstream->crc, data, size);
+ zstream->bytes32 += size;
+ zstream->flushed = FALSE;
+ return size;
+}
+
+static int
+o_stream_zlib_send_flush(struct zlib_ostream *zstream, bool final)
+{
+ z_stream *zs = &zstream->zs;
+ size_t len;
+ bool done = FALSE;
+ int ret, flush;
+
+ i_assert(zs->avail_in == 0);
+
+ if (zstream->flushed) {
+ i_assert(zstream->outbuf_used == 0);
+ return 1;
+ }
+
+ if ((ret = o_stream_flush_parent_if_needed(&zstream->ostream)) <= 0)
+ return ret;
+ if (zstream->header_bytes_left > 0) {
+ if ((ret = o_stream_zlib_send_gz_header(zstream)) <= 0)
+ return ret;
+ }
+
+ if ((ret = o_stream_zlib_send_outbuf(zstream)) <= 0)
+ return ret;
+
+ flush = final ? Z_FINISH :
+ (!zstream->gz ? Z_SYNC_FLUSH : Z_NO_FLUSH);
+
+ i_assert(zstream->outbuf_used == 0);
+ do {
+ len = sizeof(zstream->outbuf) - zs->avail_out;
+ if (len != 0) {
+ zs->next_out = zstream->outbuf;
+ zs->avail_out = sizeof(zstream->outbuf);
+
+ zstream->outbuf_used = len;
+ if ((ret = o_stream_zlib_send_outbuf(zstream)) <= 0)
+ return ret;
+ if (done)
+ break;
+ }
+
+ switch (deflate(zs, flush)) {
+ case Z_OK:
+ case Z_BUF_ERROR:
+ break;
+ case Z_STREAM_END:
+ done = TRUE;
+ break;
+ case Z_MEM_ERROR:
+ i_fatal_status(FATAL_OUTOFMEM, "zlib: Out of memory");
+ default:
+ i_unreached();
+ }
+ } while (zs->avail_out != sizeof(zstream->outbuf));
+
+ if (final) {
+ if (o_stream_zlib_send_gz_trailer(zstream) < 0)
+ return -1;
+ }
+ if (final)
+ zstream->flushed = TRUE;
+ i_assert(zstream->outbuf_used == 0);
+ return 1;
+}
+
+static int o_stream_zlib_flush(struct ostream_private *stream)
+{
+ struct zlib_ostream *zstream = (struct zlib_ostream *)stream;
+ int ret;
+ if ((ret = o_stream_zlib_send_flush(zstream, stream->finished)) < 0)
+ return -1;
+ else if (ret > 0)
+ return o_stream_flush_parent(stream);
+ return ret;
+}
+
+static size_t
+o_stream_zlib_get_buffer_used_size(const struct ostream_private *stream)
+{
+ const struct zlib_ostream *zstream =
+ (const struct zlib_ostream *)stream;
+
+ /* outbuf has already compressed data that we're trying to send to the
+ parent stream. We're not including zlib's internal compression
+ buffer size. */
+ return (zstream->outbuf_used - zstream->outbuf_offset) +
+ o_stream_get_buffer_used_size(stream->parent);
+}
+
+static size_t
+o_stream_zlib_get_buffer_avail_size(const struct ostream_private *stream)
+{
+ /* FIXME: not correct - this is counting compressed size, which may be
+ too larger than uncompressed size in some situations. Fixing would
+ require some kind of additional buffering. */
+ return o_stream_get_buffer_avail_size(stream->parent);
+}
+
+static ssize_t
+o_stream_zlib_sendv(struct ostream_private *stream,
+ const struct const_iovec *iov, unsigned int iov_count)
+{
+ struct zlib_ostream *zstream = (struct zlib_ostream *)stream;
+ ssize_t ret, bytes = 0;
+ unsigned int i;
+
+ if ((ret = o_stream_zlib_send_outbuf(zstream)) <= 0) {
+ /* error / we still couldn't flush existing data to
+ parent stream. */
+ return ret;
+ }
+
+ for (i = 0; i < iov_count; i++) {
+ ret = o_stream_zlib_send_chunk(zstream, iov[i].iov_base,
+ iov[i].iov_len);
+ if (ret < 0)
+ return -1;
+ bytes += ret;
+ if ((size_t)ret != iov[i].iov_len)
+ break;
+ }
+ stream->ostream.offset += bytes;
+
+ if (!zstream->ostream.corked && i == iov_count) {
+ if (o_stream_zlib_send_flush(zstream, FALSE) < 0)
+ return -1;
+ }
+ /* avail_in!=0 check is used to detect errors. if it's non-zero here
+ it simply means we didn't send all the data */
+ zstream->zs.avail_in = 0;
+ return bytes;
+}
+
+static void o_stream_zlib_init_gz_header(struct zlib_ostream *zstream,
+ int level, int strategy)
+{
+ unsigned char *hdr = zstream->gz_header;
+
+ hdr[0] = 0x1f;
+ hdr[1] = 0x8b;
+ hdr[2] = Z_DEFLATED;
+ hdr[8] = level == 9 ? 2 :
+ (strategy >= Z_HUFFMAN_ONLY ||
+ (level != Z_DEFAULT_COMPRESSION && level < 2) ? 4 : 0);
+ hdr[9] = ZLIB_OS_CODE;
+ i_assert(sizeof(zstream->gz_header) == 10);
+}
+
+static struct ostream *
+o_stream_create_zlib(struct ostream *output, int level, bool gz)
+{
+ const int strategy = Z_DEFAULT_STRATEGY;
+ struct zlib_ostream *zstream;
+ int ret;
+
+ /* accepted range is 0..9 and -1 is default compression */
+ i_assert(level >= -1 && level <= 9);
+
+ zstream = i_new(struct zlib_ostream, 1);
+ zstream->ostream.sendv = o_stream_zlib_sendv;
+ zstream->ostream.flush = o_stream_zlib_flush;
+ zstream->ostream.get_buffer_used_size =
+ o_stream_zlib_get_buffer_used_size;
+ zstream->ostream.get_buffer_avail_size =
+ o_stream_zlib_get_buffer_avail_size;
+ zstream->ostream.iostream.close = o_stream_zlib_close;
+ zstream->crc = 0;
+ zstream->gz = gz;
+ if (gz)
+ zstream->header_bytes_left = sizeof(zstream->gz_header);
+
+ o_stream_zlib_init_gz_header(zstream, level, strategy);
+ ret = deflateInit2(&zstream->zs, level, Z_DEFLATED, -15, 8, strategy);
+ switch (ret) {
+ case Z_OK:
+ break;
+ case Z_MEM_ERROR:
+ i_fatal_status(FATAL_OUTOFMEM, "deflateInit(): Out of memory");
+ case Z_VERSION_ERROR:
+ i_fatal("Wrong zlib library version (broken compilation)");
+ case Z_STREAM_ERROR:
+ i_fatal("Invalid compression level %d", level);
+ default:
+ i_fatal("deflateInit() failed with %d", ret);
+ }
+
+ zstream->zs.next_out = zstream->outbuf;
+ zstream->zs.avail_out = sizeof(zstream->outbuf);
+ return o_stream_create(&zstream->ostream, output,
+ o_stream_get_fd(output));
+}
+
+struct ostream *o_stream_create_gz(struct ostream *output, int level)
+{
+ return o_stream_create_zlib(output, level, TRUE);
+}
+
+struct ostream *o_stream_create_deflate(struct ostream *output, int level)
+{
+ return o_stream_create_zlib(output, level, FALSE);
+}
+#endif
diff --git a/src/lib-compression/ostream-zlib.h b/src/lib-compression/ostream-zlib.h
new file mode 100644
index 0000000..ac038b5
--- /dev/null
+++ b/src/lib-compression/ostream-zlib.h
@@ -0,0 +1,26 @@
+#ifndef OSTREAM_ZLIB_H
+#define OSTREAM_ZLIB_H
+
+struct ostream *o_stream_create_gz(struct ostream *output, int level);
+struct ostream *o_stream_create_deflate(struct ostream *output, int level);
+struct ostream *o_stream_create_bz2(struct ostream *output, int level);
+struct ostream *o_stream_create_lz4(struct ostream *output, int level);
+struct ostream *o_stream_create_zstd(struct ostream *output, int level);
+
+int compression_get_min_level_gz(void);
+int compression_get_default_level_gz(void);
+int compression_get_max_level_gz(void);
+
+int compression_get_min_level_bz2(void);
+int compression_get_default_level_bz2(void);
+int compression_get_max_level_bz2(void);
+
+int compression_get_min_level_lz4(void);
+int compression_get_default_level_lz4(void);
+int compression_get_max_level_lz4(void);
+
+int compression_get_min_level_zstd(void);
+int compression_get_default_level_zstd(void);
+int compression_get_max_level_zstd(void);
+
+#endif
diff --git a/src/lib-compression/ostream-zstd.c b/src/lib-compression/ostream-zstd.c
new file mode 100644
index 0000000..e40380b
--- /dev/null
+++ b/src/lib-compression/ostream-zstd.c
@@ -0,0 +1,243 @@
+/* Copyright (c) 2020 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+
+#ifdef HAVE_ZSTD
+
+#include "ostream.h"
+#include "ostream-private.h"
+#include "ostream-zlib.h"
+
+#include "zstd.h"
+#include "zstd_errors.h"
+#include "iostream-zstd-private.h"
+
+struct zstd_ostream {
+ struct ostream_private ostream;
+
+ ZSTD_CStream *cstream;
+ ZSTD_outBuffer output;
+
+ unsigned char *outbuf;
+
+ bool flushed:1;
+ bool closed:1;
+ bool finished:1;
+};
+
+int compression_get_min_level_zstd(void)
+{
+#if HAVE_DECL_ZSTD_MINCLEVEL == 1
+ return ZSTD_minCLevel();
+#else
+ return 1;
+#endif
+}
+
+int compression_get_default_level_zstd(void)
+{
+#ifdef ZSTD_CLEVEL_DEFAULT
+ return ZSTD_CLEVEL_DEFAULT;
+#else
+ /* Documentation says 3 is default */
+ return 3;
+#endif
+}
+
+int compression_get_max_level_zstd(void)
+{
+ return ZSTD_maxCLevel();
+}
+
+static void o_stream_zstd_write_error(struct zstd_ostream *zstream, size_t err)
+{
+ ZSTD_ErrorCode errcode = zstd_version_errcode(ZSTD_getErrorCode(err));
+ const char *error = ZSTD_getErrorName(err);
+ if (errcode == ZSTD_error_memory_allocation)
+ i_fatal_status(FATAL_OUTOFMEM, "zstd.write(%s): Out of memory",
+ o_stream_get_name(&zstream->ostream.ostream));
+ else if (errcode == ZSTD_error_prefix_unknown ||
+#if HAVE_DECL_ZSTD_ERROR_PARAMETER_UNSUPPORTED == 1
+ errcode == ZSTD_error_parameter_unsupported ||
+#endif
+ errcode == ZSTD_error_dictionary_wrong ||
+ errcode == ZSTD_error_init_missing)
+ zstream->ostream.ostream.stream_errno = EINVAL;
+ else
+ zstream->ostream.ostream.stream_errno = EIO;
+
+ io_stream_set_error(&zstream->ostream.iostream,
+ "zstd.write(%s): %s at %"PRIuUOFF_T,
+ o_stream_get_name(&zstream->ostream.ostream), error,
+ zstream->ostream.ostream.offset);
+}
+
+static ssize_t o_stream_zstd_send_outbuf(struct zstd_ostream *zstream)
+{
+ ssize_t ret;
+ /* nothing to send */
+ if (zstream->output.pos == 0)
+ return 1;
+ ret = o_stream_send(zstream->ostream.parent, zstream->output.dst,
+ zstream->output.pos);
+ if (ret < 0) {
+ o_stream_copy_error_from_parent(&zstream->ostream);
+ return -1;
+ } else {
+ memmove(zstream->outbuf, zstream->outbuf+ret, zstream->output.pos-ret);
+ zstream->output.pos -= ret;
+ }
+ if (zstream->output.pos > 0)
+ return 0;
+ return 1;
+}
+
+static ssize_t
+o_stream_zstd_sendv(struct ostream_private *stream,
+ const struct const_iovec *iov, unsigned int iov_count)
+{
+ struct zstd_ostream *zstream =
+ container_of(stream, struct zstd_ostream, ostream);
+ ssize_t total = 0;
+ size_t ret;
+
+ for (unsigned int i = 0; i < iov_count; i++) {
+ /* does it actually fit there */
+ ZSTD_inBuffer input = {
+ .src = iov[i].iov_base,
+ .pos = 0,
+ .size = iov[i].iov_len
+ };
+ bool flush_attempted = FALSE;
+ for (;;) {
+ size_t prev_pos = input.pos;
+ ret = ZSTD_compressStream(zstream->cstream, &zstream->output,
+ &input);
+ if (ZSTD_isError(ret) != 0) {
+ o_stream_zstd_write_error(zstream, ret);
+ return -1;
+ }
+ size_t new_input_size = input.pos - prev_pos;
+ if (new_input_size == 0 && flush_attempted) {
+ /* non-blocking output buffer full */
+ return total;
+ }
+ stream->ostream.offset += new_input_size;
+ total += new_input_size;
+ if (input.pos == input.size)
+ break;
+ /* output buffer full. try to flush it. */
+ if (o_stream_zstd_send_outbuf(zstream) < 0)
+ return -1;
+ flush_attempted = TRUE;
+ }
+ }
+ if (o_stream_zstd_send_outbuf(zstream) < 0)
+ return -1;
+ return total;
+}
+
+static int o_stream_zstd_send_flush(struct zstd_ostream *zstream, bool final)
+{
+ int ret;
+
+ if (zstream->flushed) {
+ i_assert(zstream->output.pos == 0);
+ return 1;
+ }
+
+ if ((ret = o_stream_flush_parent_if_needed(&zstream->ostream)) <= 0)
+ return ret;
+
+ if (zstream->output.pos == 0)
+ ZSTD_flushStream(zstream->cstream, &zstream->output);
+
+ if ((ret = o_stream_zstd_send_outbuf(zstream)) <= 0)
+ return ret;
+
+ if (!final)
+ return 1;
+
+ if (!zstream->finished) {
+ ret = ZSTD_endStream(zstream->cstream, &zstream->output);
+ if (ZSTD_isError(ret) != 0) {
+ o_stream_zstd_write_error(zstream, ret);
+ return -1;
+ }
+ zstream->finished = TRUE;
+ }
+
+ if ((ret = o_stream_zstd_send_outbuf(zstream)) <= 0)
+ return ret;
+
+ if (final)
+ zstream->flushed = TRUE;
+ i_assert(zstream->output.pos == 0);
+ return 1;
+}
+
+static int o_stream_zstd_flush(struct ostream_private *stream)
+{
+ struct zstd_ostream *zstream =
+ container_of(stream, struct zstd_ostream, ostream);
+
+ int ret;
+ if ((ret = o_stream_zstd_send_flush(zstream, stream->finished)) < 0)
+ return -1;
+ else if (ret > 0)
+ return o_stream_flush_parent(stream);
+ return ret;
+}
+
+static void o_stream_zstd_close(struct iostream_private *stream,
+ bool close_parent)
+{
+ struct ostream_private *_ostream =
+ container_of(stream, struct ostream_private, iostream);
+ struct zstd_ostream *zstream =
+ container_of(_ostream, struct zstd_ostream, ostream);
+
+ i_assert(zstream->ostream.finished ||
+ zstream->ostream.ostream.stream_errno != 0 ||
+ zstream->ostream.error_handling_disabled);
+ if (zstream->cstream != NULL) {
+ ZSTD_freeCStream(zstream->cstream);
+ zstream->cstream = NULL;
+ }
+ i_free(zstream->outbuf);
+ i_zero(&zstream->output);
+ if (close_parent)
+ o_stream_close(zstream->ostream.parent);
+}
+
+struct ostream *
+o_stream_create_zstd(struct ostream *output, int level)
+{
+ struct zstd_ostream *zstream;
+ size_t ret;
+
+ i_assert(level >= compression_get_min_level_zstd() &&
+ level <= compression_get_max_level_zstd());
+
+ zstd_version_check();
+
+ zstream = i_new(struct zstd_ostream, 1);
+ zstream->ostream.sendv = o_stream_zstd_sendv;
+ zstream->ostream.flush = o_stream_zstd_flush;
+ zstream->ostream.iostream.close = o_stream_zstd_close;
+ zstream->cstream = ZSTD_createCStream();
+ if (zstream->cstream == NULL)
+ i_fatal_status(FATAL_OUTOFMEM, "zstd: Out of memory");
+ ret = ZSTD_initCStream(zstream->cstream, level);
+ if (ZSTD_isError(ret) != 0)
+ o_stream_zstd_write_error(zstream, ret);
+ else {
+ zstream->outbuf = i_malloc(ZSTD_CStreamOutSize());
+ zstream->output.dst = zstream->outbuf;
+ zstream->output.size = ZSTD_CStreamOutSize();
+ }
+ return o_stream_create(&zstream->ostream, output,
+ o_stream_get_fd(output));
+}
+
+#endif
diff --git a/src/lib-compression/test-compression.c b/src/lib-compression/test-compression.c
new file mode 100644
index 0000000..f690c7b
--- /dev/null
+++ b/src/lib-compression/test-compression.c
@@ -0,0 +1,1122 @@
+/* Copyright (c) 2014-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "buffer.h"
+#include "istream.h"
+#include "iostream-temp.h"
+#include "ostream.h"
+#include "sha1.h"
+#include "randgen.h"
+#include "test-common.h"
+#include "compression.h"
+#include "iostream-lz4.h"
+
+#include "hex-binary.h"
+
+#include <unistd.h>
+#include <fcntl.h>
+
+static void test_compression_handler_detect(const struct compression_handler *handler)
+{
+ const unsigned char test_data[] = {'h','e','l','l','o',' ',
+ 'w','o','r','l','d','\n'};
+ const unsigned char *data;
+ size_t size;
+ buffer_t *buffer;
+ struct ostream *test_output;
+ struct ostream *output;
+
+ struct istream *test_input;
+ struct istream *input;
+
+ /* write some amount of data */
+ test_begin(t_strdup_printf("compression handler %s (detect)", handler->name));
+
+ buffer = buffer_create_dynamic(default_pool, 1024);
+
+ test_output = test_ostream_create(buffer);
+ output = handler->create_ostream(test_output, 1);
+ o_stream_unref(&test_output);
+
+ /* write data at once */
+ test_assert(o_stream_send(output, test_data, sizeof(test_data)) == sizeof(test_data));
+ test_assert(o_stream_finish(output) == 1);
+ o_stream_unref(&output);
+
+ test_input = test_istream_create_data(buffer->data, buffer->used);
+ handler = compression_detect_handler(test_input);
+ i_stream_seek(test_input, 0);
+ test_assert(handler != NULL);
+ if (handler != NULL) {
+ input = handler->create_istream(test_input);
+ i_stream_unref(&test_input);
+
+ test_assert(i_stream_read_more(input, &data, &size) > 0);
+ test_assert(size == sizeof(test_data) &&
+ memcmp(data, test_data, size) == 0);
+
+ i_stream_unref(&input);
+ } else {
+ i_stream_unref(&test_input);
+ }
+
+ buffer_free(&buffer);
+ test_end();
+}
+
+static void
+test_compression_handler_short(const struct compression_handler *handler,
+ bool autodetect)
+{
+ const unsigned char *data;
+ size_t len, size;
+ buffer_t *test_data;
+ buffer_t *buffer;
+ struct ostream *test_output;
+ struct ostream *output;
+
+ struct istream *test_input;
+ struct istream *input;
+
+ /* write some amount of data */
+ test_begin(t_strdup_printf("compression handler %s (small, autodetect=%s)",
+ handler->name, autodetect ? "yes" : "no"));
+ len = i_rand_minmax(1, 1024);
+ test_data = buffer_create_dynamic(default_pool, len);
+ random_fill(buffer_append_space_unsafe(test_data, len), len);
+ buffer_set_used_size(test_data, len);
+ buffer_append(test_data, "hello. world.\n", 14);
+
+ buffer = buffer_create_dynamic(default_pool, 1024);
+ test_output = test_ostream_create(buffer);
+ output = handler->create_ostream(test_output, 1);
+ o_stream_unref(&test_output);
+
+ /* write data at once */
+ test_assert(o_stream_send(output, test_data->data, test_data->used) == (ssize_t)test_data->used);
+ test_assert(o_stream_finish(output) == 1);
+ o_stream_unref(&output);
+
+ /* read data at once */
+ test_input = test_istream_create_data(buffer->data, buffer->used);
+ input = !autodetect ? handler->create_istream(test_input) :
+ i_stream_create_decompress(test_input, 0);
+ i_stream_unref(&test_input);
+
+ test_assert(i_stream_read_more(input, &data, &size) > 0);
+ test_assert(size == test_data->used &&
+ memcmp(data, test_data->data, size) ==0);
+
+ i_stream_unref(&input);
+
+ buffer_free(&buffer);
+ buffer_free(&test_data);
+
+ test_end();
+}
+
+static void
+test_compression_handler_empty(const struct compression_handler *handler,
+ bool autodetect)
+{
+ buffer_t *buffer;
+ struct ostream *test_output;
+ struct ostream *output;
+
+ struct istream *test_input;
+ struct istream *input;
+
+ /* create stream and finish it without writing anything */
+ test_begin(t_strdup_printf("compression handler %s (empty, autodetect=%s)",
+ handler->name, autodetect ? "yes" : "no"));
+ buffer = buffer_create_dynamic(default_pool, 128);
+ test_output = test_ostream_create(buffer);
+ output = handler->create_ostream(test_output, 1);
+ o_stream_unref(&test_output);
+ test_assert(o_stream_finish(output) == 1);
+ o_stream_unref(&output);
+
+ /* read the input */
+ test_input = test_istream_create_data(buffer->data, buffer->used);
+ input = !autodetect ? handler->create_istream(test_input) :
+ i_stream_create_decompress(test_input, 0);
+ i_stream_unref(&test_input);
+
+ test_assert(i_stream_read(input) == -1);
+ test_assert(i_stream_get_data_size(input) == 0);
+ i_stream_unref(&input);
+
+ buffer_free(&buffer);
+
+ test_end();
+}
+
+static void
+test_compression_handler_seek(const struct compression_handler *handler,
+ bool autodetect)
+{
+ const unsigned char *data,*ptr;
+ size_t len, size, pos;
+ buffer_t *test_data;
+ buffer_t *buffer;
+ struct ostream *test_output;
+ struct ostream *output;
+
+ struct istream *test_input;
+ struct istream *input;
+
+ /* write some amount of data */
+ test_begin(t_strdup_printf("compression handler %s (seek, autodetect=%s)",
+ handler->name, autodetect ? "yes" : "no"));
+ len = i_rand_minmax(1024, 2048);
+ test_data = buffer_create_dynamic(default_pool, len);
+ random_fill(buffer_append_space_unsafe(test_data, len), len);
+ buffer_set_used_size(test_data, len);
+ buffer_append(test_data, "hello. world.\n", 14);
+
+ buffer = buffer_create_dynamic(default_pool, 1024);
+ test_output = test_ostream_create(buffer);
+ output = handler->create_ostream(test_output, 1);
+ o_stream_unref(&test_output);
+
+ /* write data at once */
+ test_assert(o_stream_send(output, test_data->data, test_data->used) == (ssize_t)test_data->used);
+ test_assert(o_stream_finish(output) == 1);
+ o_stream_unref(&output);
+
+ test_input = test_istream_create_data(buffer->data, buffer->used);
+ input = !autodetect ? handler->create_istream(test_input) :
+ i_stream_create_decompress(test_input, 0);
+ i_stream_unref(&test_input);
+
+ /* seek forward */
+ i_stream_seek(input, test_data->used - 14); /* should read 'hello. world.\n' */
+
+ test_assert(i_stream_read_more(input, &data, &size) > 0);
+ test_assert(size >= 14 && memcmp(data, "hello. world.\n", 14) == 0);
+ i_stream_skip(input, size);
+
+ ptr = test_data->data;
+
+ /* seek to random positions and see that we get correct data */
+ for (unsigned int i = 0; i < 1000; i++) {
+ pos = i_rand_limit(test_data->used);
+ i_stream_seek(input, pos);
+ size = 0;
+ test_assert_idx(i_stream_read_more(input, &data, &size) > 0, i);
+ test_assert_idx(size > 0 && memcmp(data,ptr+pos,size) == 0, i);
+ }
+
+ i_stream_unref(&input);
+
+ buffer_free(&buffer);
+ buffer_free(&test_data);
+
+ test_end();
+}
+
+static void
+test_compression_handler_reset(const struct compression_handler *handler,
+ bool autodetect)
+{
+ const unsigned char *data;
+ size_t len, size;
+ buffer_t *test_data;
+ buffer_t *buffer;
+ struct ostream *test_output;
+ struct ostream *output;
+
+ struct istream *test_input;
+ struct istream *input;
+
+ /* write some amount of data */
+ test_begin(t_strdup_printf("compression handler %s (reset, autodetect=%s)",
+ handler->name, autodetect ? "yes" : "no"));
+ len = i_rand_minmax(1024, 2048);
+ test_data = buffer_create_dynamic(default_pool, len);
+ random_fill(buffer_append_space_unsafe(test_data, len), len);
+ buffer_set_used_size(test_data, len);
+ buffer_append(test_data, "hello. world.\n", 14);
+
+ buffer = buffer_create_dynamic(default_pool, 1024);
+ test_output = test_ostream_create(buffer);
+ output = handler->create_ostream(test_output, 1);
+ o_stream_unref(&test_output);
+
+ /* write data at once */
+ test_assert(o_stream_send(output, test_data->data, test_data->used) == (ssize_t)test_data->used);
+ test_assert(o_stream_finish(output) == 1);
+ o_stream_unref(&output);
+
+ test_input = test_istream_create_data(buffer->data, buffer->used);
+ input = !autodetect ? handler->create_istream(test_input) :
+ i_stream_create_decompress(test_input, 0);
+ i_stream_unref(&test_input);
+
+ /* seek forward */
+ i_stream_seek(input, test_data->used - 14); /* should read 'hello. world.\n' */
+
+ test_assert(i_stream_read_more(input, &data, &size) > 0);
+ test_assert(size >= 14 && memcmp(data, "hello. world.\n", 14) == 0);
+ i_stream_skip(input, size);
+
+ /* reset */
+ i_stream_sync(input);
+
+ /* see that we still get data, at start */
+ size = 0;
+ test_assert(i_stream_read_more(input, &data, &size) > 0);
+ test_assert(size > 0 && memcmp(data, test_data->data, size) == 0);
+
+ i_stream_unref(&input);
+
+ buffer_free(&buffer);
+ buffer_free(&test_data);
+
+ test_end();
+}
+
+static void
+test_compression_handler(const struct compression_handler *handler,
+ bool autodetect)
+{
+ const char *path = "test-compression.tmp";
+ struct istream *file_input, *input;
+ struct ostream *file_output, *output;
+ unsigned char buf[IO_BLOCK_SIZE];
+ const unsigned char *data;
+ size_t size;
+ uoff_t stream_size;
+ struct sha1_ctxt sha1;
+ unsigned char output_sha1[SHA1_RESULTLEN], input_sha1[SHA1_RESULTLEN];
+ unsigned int i;
+ int fd;
+ ssize_t ret;
+
+ test_begin(t_strdup_printf("compression handler %s (autodetect=%s)",
+ handler->name, autodetect ? "yes" : "no"));
+
+ /* write compressed data */
+ fd = open(path, O_TRUNC | O_CREAT | O_RDWR, 0600);
+ if (fd == -1)
+ i_fatal("creat(%s) failed: %m", path);
+ file_output = o_stream_create_fd_file(fd, 0, FALSE);
+ output = handler->create_ostream(file_output, 1);
+ sha1_init(&sha1);
+
+ /* 1) write lots of easily compressible data */
+ memset(buf, 0, sizeof(buf));
+ for (i = 0; i < 1024*1024*4 / sizeof(buf); i++) {
+ sha1_loop(&sha1, buf, sizeof(buf));
+ test_assert(o_stream_send(output, buf, sizeof(buf)) == sizeof(buf));
+ }
+
+ /* 2) write uncompressible data */
+ for (i = 0; i < 1024*128 / sizeof(buf); i++) {
+ random_fill(buf, sizeof(buf));
+ sha1_loop(&sha1, buf, sizeof(buf));
+ test_assert(o_stream_send(output, buf, sizeof(buf)) == sizeof(buf));
+ }
+ /* make sure the input size isn't multiple of something simple */
+ random_fill(buf, sizeof(buf));
+ sha1_loop(&sha1, buf, sizeof(buf) - 5);
+ test_assert(o_stream_send(output, buf, sizeof(buf) - 5) == sizeof(buf) - 5);
+
+ /* 3) write semi-compressible data */
+ for (i = 0; i < sizeof(buf); i++) {
+ if (i_rand_limit(3) == 0)
+ buf[i] = i_rand_limit(4);
+ else
+ buf[i] = i & UCHAR_MAX;
+ }
+ for (i = 0; i < 1024*128 / sizeof(buf); i++) {
+ sha1_loop(&sha1, buf, sizeof(buf));
+ test_assert(o_stream_send(output, buf, sizeof(buf)) == sizeof(buf));
+ }
+
+ test_assert(o_stream_finish(output) > 0);
+ uoff_t uncompressed_size = output->offset;
+ o_stream_destroy(&output);
+ uoff_t compressed_size = file_output->offset;
+ o_stream_destroy(&file_output);
+ sha1_result(&sha1, output_sha1);
+
+ /* read and uncompress the data */
+ file_input = i_stream_create_fd(fd, IO_BLOCK_SIZE);
+ input = !autodetect ? handler->create_istream(file_input) :
+ i_stream_create_decompress(file_input, 0);
+
+ test_assert(i_stream_get_size(input, FALSE, &stream_size) == 1);
+ test_assert(stream_size == compressed_size);
+
+ test_assert(i_stream_get_size(input, TRUE, &stream_size) == 1);
+ test_assert(stream_size == uncompressed_size);
+
+ sha1_init(&sha1);
+ for (bool seeked = FALSE;;) {
+ sha1_init(&sha1);
+ while ((ret = i_stream_read_more(input, &data, &size)) > 0) {
+ sha1_loop(&sha1, data, size);
+ i_stream_skip(input, size);
+ }
+ test_assert(ret == -1);
+ test_assert(input->stream_errno == 0);
+ sha1_result(&sha1, input_sha1);
+ test_assert(memcmp(input_sha1, output_sha1, sizeof(input_sha1)) == 0);
+ if (seeked)
+ break;
+ seeked = TRUE;
+ i_stream_seek(input, 1);
+ (void)i_stream_read(input);
+ i_stream_seek(input, 0);
+ }
+ i_stream_destroy(&input);
+ i_stream_destroy(&file_input);
+
+ i_unlink(path);
+ i_close_fd(&fd);
+
+ test_end();
+}
+
+static void
+test_compression_handler_partial_parent_write(const struct compression_handler *handler,
+ bool autodetect)
+{
+ test_begin(t_strdup_printf("compression handler %s (partial parent writes, autodetect=%s)",
+ handler->name, autodetect ? "yes" : "no"));
+
+ int ret;
+ buffer_t *buffer = t_buffer_create(64);
+ buffer_t *compressed_data = t_buffer_create(256);
+ struct ostream *os = test_ostream_create_nonblocking(buffer, 64);
+ struct ostream *os_compressed = handler->create_ostream(os, 9);
+ o_stream_unref(&os);
+
+ unsigned char input_buffer[64];
+ /* create unlikely compressible data */
+ random_fill(input_buffer, sizeof(input_buffer));
+
+ for (unsigned int i = 0; i < 10; i++) {
+ /* write it to stream */
+ test_assert_idx(o_stream_send(os_compressed, input_buffer, sizeof(input_buffer)) == sizeof(input_buffer), i);
+
+ while ((ret = o_stream_flush(os_compressed)) == 0) {
+ /* flush buffer */
+ if (buffer->used > 0)
+ buffer_append(compressed_data, buffer->data, buffer->used);
+ buffer_set_used_size(buffer, 0);
+ }
+ if (buffer->used > 0)
+ buffer_append(compressed_data, buffer->data, buffer->used);
+ buffer_set_used_size(buffer, 0);
+ test_assert_idx(ret == 1, i);
+ }
+ test_assert(o_stream_finish(os_compressed) == 1);
+ o_stream_unref(&os_compressed);
+ if (buffer->used > 0)
+ buffer_append(compressed_data, buffer->data, buffer->used);
+
+ struct istream *is = test_istream_create_data(compressed_data->data, compressed_data->used);
+ struct istream *is_decompressed =
+ !autodetect ? handler->create_istream(is) :
+ i_stream_create_decompress(is, 0);
+ i_stream_unref(&is);
+
+ const unsigned char *data;
+ size_t siz;
+ buffer_t *decompressed_data = t_buffer_create(sizeof(input_buffer)*10);
+
+ while(i_stream_read_more(is_decompressed, &data, &siz) > 0) {
+ buffer_append(decompressed_data, data, siz);
+ i_stream_skip(is_decompressed, siz);
+ }
+ test_assert(decompressed_data->used == sizeof(input_buffer)*10);
+ for(siz = 0; siz < decompressed_data->used; siz+=sizeof(input_buffer)) {
+ test_assert(decompressed_data->used - siz >= sizeof(input_buffer) &&
+ memcmp(CONST_PTR_OFFSET(decompressed_data->data, siz),
+ input_buffer, sizeof(input_buffer)) == 0);
+ }
+
+ i_stream_unref(&is_decompressed);
+
+ test_end();
+}
+
+static void
+test_compression_handler_random_io(const struct compression_handler *handler,
+ bool autodetect)
+{
+ unsigned char in_buf[8192];
+ size_t in_buf_size;
+ buffer_t *enc_buf, *dec_buf;
+ unsigned int i, j;
+ int ret;
+
+ enc_buf = buffer_create_dynamic(default_pool, sizeof(in_buf));
+ dec_buf = buffer_create_dynamic(default_pool, sizeof(in_buf));
+
+ test_begin(t_strdup_printf("compression handler %s (random I/O, autodetect=%s)",
+ handler->name, autodetect ? "yes" : "no"));
+
+ for (i = 0; !test_has_failed() && i < 300; i++) {
+ struct istream *input1, *input2;
+ struct ostream *output1, *output2;
+ struct istream *top_input;
+ const unsigned char *data;
+ size_t size, in_pos, out_pos;
+
+ /* Initialize test data (semi-compressible) */
+ in_buf_size = i_rand_limit(sizeof(in_buf));
+ for (j = 0; j < in_buf_size; j++) {
+ if (i_rand_limit(3) == 0)
+ in_buf[j] = i_rand_limit(256);
+ else
+ in_buf[j] = (unsigned char)j;
+ }
+
+ /* Reset encode output buffer */
+ buffer_set_used_size(enc_buf, 0);
+
+ /* Create input stream for test data */
+ input1 = test_istream_create_data(in_buf, in_buf_size);
+ i_stream_set_name(input1, "[data]");
+
+ /* Create output stream for compressed data */
+ output1 = test_ostream_create_nonblocking(enc_buf,
+ i_rand_minmax(1, 512));
+
+ /* Create compressor output stream */
+ output2 = handler->create_ostream(output1, i_rand_minmax(1, 6));
+
+ /* Compress the data incrementally */
+ in_pos = out_pos = 0;
+ ret = 0;
+ test_istream_set_size(input1, in_pos);
+ while (ret == 0) {
+ enum ostream_send_istream_result res;
+
+ res = o_stream_send_istream(output2, input1);
+ switch(res) {
+ case OSTREAM_SEND_ISTREAM_RESULT_ERROR_INPUT:
+ case OSTREAM_SEND_ISTREAM_RESULT_ERROR_OUTPUT:
+ ret = -1;
+ break;
+ case OSTREAM_SEND_ISTREAM_RESULT_WAIT_OUTPUT:
+ out_pos += i_rand_limit(512);
+ test_ostream_set_max_output_size(
+ output1, out_pos);
+ break;
+ case OSTREAM_SEND_ISTREAM_RESULT_WAIT_INPUT:
+ in_pos += i_rand_limit(512);
+ if (in_pos > in_buf_size)
+ in_pos = in_buf_size;
+ test_istream_set_size(input1, in_pos);
+ break;
+ case OSTREAM_SEND_ISTREAM_RESULT_FINISHED:
+ /* finish it */
+ ret = o_stream_finish(output2);
+ break;
+ }
+ }
+
+ /* Clean up */
+ i_stream_unref(&input1);
+ o_stream_unref(&output1);
+ o_stream_unref(&output2);
+
+ /* Reset decode output buffer */
+ buffer_set_used_size(dec_buf, 0);
+
+ /* Create input stream for compressed data */
+ input1 = i_stream_create_from_buffer(enc_buf);
+ i_stream_set_name(input1, "[compressed-data]");
+
+ /* Create decompressor stream */
+ input2 = !autodetect ? handler->create_istream(input1) :
+ i_stream_create_decompress(input1, 0);
+ i_stream_set_name(input2, "[decompressor]");
+
+ /* Assign random buffer sizes */
+ i_stream_set_max_buffer_size(input2, i_rand_minmax(1, 512));
+
+ /* Read the outer stream in full with random increments. */
+ top_input = input2;
+ while ((ret = i_stream_read_more(
+ top_input, &data, &size)) > 0) {
+ size_t ch = i_rand_limit(512);
+
+ size = I_MIN(size, ch);
+ buffer_append(dec_buf, data, size);
+ i_stream_skip(top_input, size);
+ }
+ if (ret < 0 && top_input->stream_errno == 0) {
+ data = i_stream_get_data(top_input, &size);
+ if (size > 0) {
+ buffer_append(dec_buf, data, size);
+ i_stream_skip(top_input, size);
+ }
+ }
+
+ /* Assert stream status */
+ test_assert_idx(ret < 0 && top_input->stream_errno == 0, i);
+ /* Assert input/output equality */
+ test_assert_idx(dec_buf->used == in_buf_size &&
+ memcmp(in_buf, dec_buf->data, in_buf_size) == 0,
+ i);
+
+ if (top_input->stream_errno != 0) {
+ i_error("%s: %s", i_stream_get_name(input1),
+ i_stream_get_error(input1));
+ i_error("%s: %s", i_stream_get_name(input2),
+ i_stream_get_error(input2));
+ }
+
+ if (test_has_failed()) {
+ i_info("Test parameters: size=%zu",
+ in_buf_size);
+ }
+
+ /* Clean up */
+ i_stream_unref(&input1);
+ i_stream_unref(&input2);
+ }
+ test_end();
+
+ buffer_free(&enc_buf);
+ buffer_free(&dec_buf);
+}
+
+static void
+test_compression_handler_large_random_io(const struct compression_handler *handler,
+ bool autodetect)
+{
+#define RANDOMNESS_SIZE (1024*1024)
+ unsigned char *randomness;
+ struct istream *input, *dec_input;
+ struct ostream *temp_output, *output;
+ const unsigned char *data;
+ size_t size;
+ int ret;
+
+ test_begin(t_strdup_printf("compression handler %s (large random io, autodetect=%s)",
+ handler->name, autodetect ? "yes" : "no"));
+ randomness = i_malloc(RANDOMNESS_SIZE);
+ random_fill(randomness, RANDOMNESS_SIZE);
+
+ /* write 1 MB of randomness to buffer */
+ input = i_stream_create_from_data(randomness, RANDOMNESS_SIZE);
+
+ temp_output = iostream_temp_create(".temp.", 0);
+ output = handler->create_ostream(temp_output, i_rand_minmax(1, 6));
+
+ switch (o_stream_send_istream(output, input)) {
+ case OSTREAM_SEND_ISTREAM_RESULT_ERROR_INPUT:
+ case OSTREAM_SEND_ISTREAM_RESULT_ERROR_OUTPUT:
+ test_assert(FALSE);
+ break;
+ case OSTREAM_SEND_ISTREAM_RESULT_WAIT_OUTPUT:
+ case OSTREAM_SEND_ISTREAM_RESULT_WAIT_INPUT:
+ i_unreached();
+ case OSTREAM_SEND_ISTREAM_RESULT_FINISHED:
+ test_assert(o_stream_finish(output) == 1);
+ break;
+ }
+ test_assert(output->offset == RANDOMNESS_SIZE);
+ test_assert(output->stream_errno == 0);
+ i_stream_unref(&input);
+
+ input = iostream_temp_finish(&temp_output, SIZE_MAX);
+ o_stream_unref(&output);
+
+ /* verify that reading the input works */
+
+ dec_input = !autodetect ? handler->create_istream(input) :
+ i_stream_create_decompress(input, 0);
+
+ while ((ret = i_stream_read_more(dec_input, &data, &size)) > 0) {
+ test_assert(memcmp(data, randomness + dec_input->v_offset, size) == 0);
+ i_stream_skip(dec_input, size);
+ }
+ test_assert(ret == -1);
+ test_assert(dec_input->v_offset == RANDOMNESS_SIZE);
+ test_assert(dec_input->stream_errno == 0);
+
+ i_stream_unref(&dec_input);
+ i_stream_unref(&input);
+ i_free(randomness);
+ test_end();
+}
+
+static void
+test_compression_handler_errors(const struct compression_handler *handler,
+ bool autodetect)
+{
+ test_begin(t_strdup_printf("compression handler %s (errors, autodetect=%s)",
+ handler->name, autodetect ? "yes" : "no"));
+
+ /* test that zero stream reading errors out */
+ struct istream *is = test_istream_create("");
+ struct istream *input =
+ !autodetect ? handler->create_istream(is) :
+ i_stream_create_decompress(is, 0);
+ i_stream_unref(&is);
+ test_assert(i_stream_read(input) == -1 && input->eof);
+ i_stream_unref(&input);
+
+ /* test that garbage isn't considered valid */
+ is = test_istream_create("dedededededededededededededede"
+ "dedededeededdedededededededede"
+ "dedededededededededededededede");
+ is->blocking = TRUE;
+ input = !autodetect ? handler->create_istream(is) :
+ i_stream_create_decompress(is, 0);
+ i_stream_unref(&is);
+ test_assert(i_stream_read(input) == -1 && input->eof);
+ i_stream_unref(&input);
+
+ /* test that truncated data is not considered valid */
+ buffer_t *odata = buffer_create_dynamic(pool_datastack_create(), 65535);
+ unsigned char buf[IO_BLOCK_SIZE];
+ struct ostream *os = test_ostream_create(odata);
+ struct ostream *output = handler->create_ostream(os, 1);
+ o_stream_unref(&os);
+
+ for (unsigned int i = 0; i < 10; i++) {
+ random_fill(buf, sizeof(buf));
+ test_assert(o_stream_send(output, buf, sizeof(buf)) == sizeof(buf));
+ }
+
+ test_assert(o_stream_finish(output) == 1);
+ o_stream_unref(&output);
+
+ /* truncate buffer */
+ is = test_istream_create_data(odata->data, odata->used - sizeof(buf)*2 - 1);
+ input = !autodetect ? handler->create_istream(is) :
+ i_stream_create_decompress(is, 0);
+ i_stream_unref(&is);
+
+ const unsigned char *data ATTR_UNUSED;
+ size_t size;
+ while (i_stream_read_more(input, &data, &size) > 0)
+ i_stream_skip(input, size);
+
+ test_assert(input->stream_errno == EPIPE);
+ i_stream_unref(&input);
+
+ /* Cannot do the next check if we don't know if it's compressed. */
+ if (handler->is_compressed != NULL) {
+ /* test incrementally reading up to 32 bytes of plaintext data
+ that should not match any handlers' header */
+ for (size_t i = 0; i < 32; i++) {
+ is = test_istream_create_data("dededededededededededededededede", i);
+ input = !autodetect ? handler->create_istream(is) :
+ i_stream_create_decompress(is, 0);
+ i_stream_unref(&is);
+ while (i_stream_read_more(input, &data, &size) >= 0) {
+ test_assert_idx(size == 0, i);
+ i_stream_skip(input, size);
+ }
+ test_assert_idx(input->stream_errno == EINVAL, i);
+ i_stream_unref(&input);
+ }
+ }
+
+ test_end();
+}
+
+static void test_compression_int(bool autodetect)
+{
+ unsigned int i;
+
+ for (i = 0; compression_handlers[i].name != NULL; i++) {
+ if (compression_handlers[i].create_istream != NULL &&
+ compression_handlers[i].create_ostream != NULL &&
+ (!autodetect ||
+ compression_handlers[i].is_compressed != NULL)) T_BEGIN {
+ if (compression_handlers[i].is_compressed != NULL &&
+ !autodetect)
+ test_compression_handler_detect(&compression_handlers[i]);
+ test_compression_handler_short(&compression_handlers[i], autodetect);
+ test_compression_handler_empty(&compression_handlers[i], autodetect);
+ test_compression_handler(&compression_handlers[i], autodetect);
+ test_compression_handler_seek(&compression_handlers[i], autodetect);
+ test_compression_handler_reset(&compression_handlers[i], autodetect);
+ test_compression_handler_partial_parent_write(&compression_handlers[i], autodetect);
+ test_compression_handler_random_io(&compression_handlers[i], autodetect);
+ test_compression_handler_large_random_io(&compression_handlers[i], autodetect);
+ test_compression_handler_errors(&compression_handlers[i], autodetect);
+ } T_END;
+ }
+}
+
+static void test_compression(void)
+{
+ test_compression_int(FALSE);
+ test_compression_int(TRUE);
+}
+
+static void test_istream_decompression_try(void)
+{
+ const char *tests[] = {
+ "",
+ "1",
+ "12",
+ "12345678901234567890123456789012345678901234567890",
+ };
+ struct istream *is, *input;
+ const unsigned char *data;
+ size_t size;
+
+ test_begin("istream-decompression try");
+
+ for (unsigned int i = 0; i < N_ELEMENTS(tests); i++) {
+ size_t test_len = strlen(tests[i]);
+ is = i_stream_create_from_data(tests[i], test_len);
+ input = i_stream_create_decompress(is, ISTREAM_DECOMPRESS_FLAG_TRY);
+ i_stream_unref(&is);
+
+ ssize_t ret = i_stream_read(input);
+ test_assert_idx((test_len == 0 && ret == -1) ||
+ (test_len > 0 && ret == (ssize_t)test_len), i);
+ data = i_stream_get_data(input, &size);
+ test_assert_idx(size == test_len &&
+ memcmp(data, tests[i], size) == 0, i);
+
+ i_stream_skip(input, size);
+ test_assert_idx(i_stream_read(input) == -1, i);
+ test_assert_idx(input->stream_errno == 0, i);
+ i_stream_unref(&input);
+ }
+ test_end();
+}
+
+static void test_gz(const char *str1, const char *str2, bool autodetect)
+{
+ const struct compression_handler *gz;
+ struct ostream *buf_output, *output;
+ struct istream *test_input, *input;
+ buffer_t *buf = t_buffer_create(512);
+
+ if (compression_lookup_handler("gz", &gz) <= 0 )
+ return; /* not compiled in or unkown*/
+
+ /* write concated output */
+ buf_output = o_stream_create_buffer(buf);
+ o_stream_set_finish_via_child(buf_output, FALSE);
+
+ output = gz->create_ostream(buf_output, 6);
+ o_stream_nsend_str(output, str1);
+ test_assert(o_stream_finish(output) > 0);
+ o_stream_destroy(&output);
+
+ if (str2[0] != '\0') {
+ output = gz->create_ostream(buf_output, 6);
+ o_stream_nsend_str(output, "world");
+ test_assert(o_stream_finish(output) > 0);
+ o_stream_destroy(&output);
+ }
+
+ o_stream_destroy(&buf_output);
+
+ /* read concated input */
+ const unsigned char *data;
+ size_t size;
+ test_input = test_istream_create_data(buf->data, buf->used);
+ test_istream_set_allow_eof(test_input, FALSE);
+ input = !autodetect ? gz->create_istream(test_input) :
+ i_stream_create_decompress(test_input, 0);
+ for (size_t i = 0; i <= buf->used; i++) {
+ test_istream_set_size(test_input, i);
+ test_assert(i_stream_read(input) >= 0);
+ }
+ test_istream_set_allow_eof(test_input, TRUE);
+ test_assert(i_stream_read(input) == -1);
+ test_assert(input->stream_errno == 0);
+
+ data = i_stream_get_data(input, &size);
+ test_assert(size == strlen(str1)+strlen(str2) &&
+ memcmp(data, str1, strlen(str1)) == 0 &&
+ memcmp(data+strlen(str1), str2, strlen(str2)) == 0);
+ i_stream_unref(&input);
+ i_stream_unref(&test_input);
+}
+
+static void test_gz_concat(void)
+{
+ test_begin("gz concat (autodetect=no)");
+ test_gz("hello", "world", FALSE);
+ test_end();
+
+ test_begin("gz concat (autodetect=yes)");
+ test_gz("hello", "world", TRUE);
+ test_end();
+}
+
+static void test_gz_no_concat(void)
+{
+ test_begin("gz no concat (autodetect=no)");
+ test_gz("hello", "", FALSE);
+ test_end();
+
+ test_begin("gz no concat (autodetect=yes)");
+ test_gz("hello", "", TRUE);
+ test_end();
+}
+
+static void test_gz_header_int(bool autodetect)
+{
+ const struct compression_handler *gz;
+ const char *input_strings[] = {
+ "\x1F\x8B",
+ "\x1F\x8B\x01\x02"/* GZ_FLAG_FHCRC */"\xFF\xFF\x01\x01\x01\x01",
+ "\x1F\x8B\x01\x04"/* GZ_FLAG_FEXTRA */"\xFF\xFF\x01\x01\x01\x01",
+ "\x1F\x8B\x01\x08"/* GZ_FLAG_FNAME */"\x01\x01\x01\x01\x01\x01",
+ "\x1F\x8B\x01\x10"/* GZ_FLAG_FCOMMENT */"\x01\x01\x01\x01\x01\x01",
+ "\x1F\x8B\x01\x0C"/* GZ_FLAG_FEXTRA | GZ_FLAG_FNAME */"\xFF\xFF\x01\x01\x01\x01",
+ };
+ struct istream *file_input, *input;
+ if (compression_lookup_handler("gz", &gz) <= 0 )
+ return; /* not compiled in or unkown*/
+
+ test_begin(t_strdup_printf(
+ "gz header (autodetect=%s)", autodetect ? "yes" : "no"));
+ for (unsigned int i = 0; i < N_ELEMENTS(input_strings); i++) {
+ file_input = test_istream_create_data(input_strings[i],
+ strlen(input_strings[i]));
+ file_input->blocking = TRUE;
+ input = !autodetect ? gz->create_istream(file_input) :
+ i_stream_create_decompress(file_input, 0);
+ test_assert_idx(i_stream_read(input) == -1, i);
+ test_assert_idx(input->stream_errno == EINVAL, i);
+ i_stream_unref(&input);
+ i_stream_unref(&file_input);
+ }
+ test_end();
+}
+
+static void test_gz_header(void)
+{
+ test_gz_header_int(FALSE);
+ test_gz_header_int(TRUE);
+}
+
+static void test_gz_large_header_int(bool autodetect)
+{
+ const struct compression_handler *gz;
+ static const unsigned char gz_input[] = {
+ 0x1f, 0x8b, 0x08, 0x08,
+ 'a','a','a','a','a','a','a','a','a','a','a',
+ 0
+ };
+ struct istream *file_input, *input;
+ size_t i;
+
+ if (compression_lookup_handler("gz", &gz) <= 0 )
+ return; /* not compiled in or unkown*/
+
+ test_begin(t_strdup_printf(
+ "gz large header (autodetect=%s)", autodetect ? "yes" : "no"));
+
+ /* max buffer size smaller than gz header */
+ for (i = 1; i < sizeof(gz_input); i++) {
+ file_input = test_istream_create_data(gz_input, sizeof(gz_input));
+ test_istream_set_size(file_input, i);
+ test_istream_set_max_buffer_size(file_input, i);
+
+ input = !autodetect ? gz->create_istream(file_input) :
+ i_stream_create_decompress(file_input, 0);
+ test_assert_idx(i_stream_read(input) == 0, i);
+ test_assert_idx(i_stream_read(input) == -1 &&
+ input->stream_errno == EINVAL, i);
+ i_stream_unref(&input);
+ i_stream_unref(&file_input);
+ }
+
+ /* max buffer size is exactly the gz header */
+ file_input = test_istream_create_data(gz_input, sizeof(gz_input));
+ input = !autodetect ? gz->create_istream(file_input) :
+ i_stream_create_decompress(file_input, 0);
+ test_istream_set_size(file_input, i);
+ test_istream_set_allow_eof(file_input, FALSE);
+ test_istream_set_max_buffer_size(file_input, i);
+ test_assert(i_stream_read(input) == 0);
+ i_stream_unref(&input);
+ i_stream_unref(&file_input);
+
+ test_end();
+}
+
+static void test_gz_large_header(void)
+{
+ test_gz_large_header_int(FALSE);
+ test_gz_large_header_int(TRUE);
+}
+
+static void test_lz4_small_header(void)
+{
+ const struct compression_handler *lz4;
+ struct iostream_lz4_header lz4_input;
+ struct istream *file_input, *input;
+
+ if (compression_lookup_handler("lz4", &lz4) <= 0)
+ return; /* not compiled in or unkown */
+
+ test_begin("lz4 small header");
+
+ memcpy(lz4_input.magic, IOSTREAM_LZ4_MAGIC, IOSTREAM_LZ4_MAGIC_LEN);
+ lz4_input.max_uncompressed_chunk_size[0] = 0;
+ lz4_input.max_uncompressed_chunk_size[1] = 1;
+ lz4_input.max_uncompressed_chunk_size[2] = 0;
+ lz4_input.max_uncompressed_chunk_size[3] = 0;
+
+ /* truncated header */
+ file_input = i_stream_create_from_data(&lz4_input, sizeof(lz4_input)-1);
+ input = lz4->create_istream(file_input);
+ test_assert(i_stream_read(input) == -1 &&
+ input->stream_errno == EINVAL);
+ i_stream_unref(&input);
+ i_stream_unref(&file_input);
+
+ /* partial initial header read */
+ file_input = test_istream_create_data(&lz4_input, sizeof(lz4_input));
+ file_input->blocking = TRUE;
+
+ test_istream_set_max_buffer_size(file_input, sizeof(lz4_input)-1);
+ (void)i_stream_read(file_input);
+ test_istream_set_max_buffer_size(file_input, sizeof(lz4_input));
+
+ input = lz4->create_istream(file_input);
+ test_assert(i_stream_read(input) == -1 &&
+ input->stream_errno == 0);
+ i_stream_unref(&input);
+ i_stream_unref(&file_input);
+
+ test_end();
+}
+
+static void test_uncompress_file(const char *path)
+{
+ const struct compression_handler *handler;
+ struct istream *input, *file_input;
+ const unsigned char *data;
+ size_t size;
+ int ret;
+
+ ret = compression_lookup_handler_from_ext(path, &handler);
+ if (ret < 0)
+ i_fatal("Can't detect compression algorithm from path %s", path);
+ else if (ret == 0)
+ i_fatal("Support not compiled in for %s", handler->name);
+
+ file_input = i_stream_create_file(path, IO_BLOCK_SIZE);
+ input = handler->create_istream(file_input);
+ while (i_stream_read_more(input, &data, &size) > 0) {
+ if (write(STDOUT_FILENO, data, size) < 0)
+ break;
+ i_stream_skip(input, size);
+ }
+ i_stream_destroy(&input);
+}
+
+static void test_compress_file(const char *in_path, const char *out_path)
+{
+ const struct compression_handler *handler;
+ struct istream *input, *file_input;
+ struct ostream *output, *file_output;
+ int fd_in, fd_out;
+ struct sha1_ctxt sha1;
+ unsigned char output_sha1[SHA1_RESULTLEN], input_sha1[SHA1_RESULTLEN];
+ const unsigned char *data;
+ size_t size;
+ int ret;
+
+ ret = compression_lookup_handler_from_ext(out_path, &handler);
+ if (ret < 0)
+ i_fatal("Can't detect compression algorithm from path %s", out_path);
+ if (ret == 0)
+ i_fatal("Support not compiled in for %s", handler->name);
+
+ /* write the compressed output file */
+ fd_in = open(in_path, O_RDONLY);
+ if (fd_in == -1)
+ i_fatal("open(%s) failed: %m", in_path);
+ fd_out = open(out_path, O_TRUNC | O_CREAT | O_RDWR, 0600);
+ if (fd_out == -1)
+ i_fatal("creat(%s) failed: %m", out_path);
+
+ sha1_init(&sha1);
+ file_output = o_stream_create_fd_file(fd_out, 0, FALSE);
+ output = handler->create_ostream(file_output, 1);
+ input = i_stream_create_fd_autoclose(&fd_in, IO_BLOCK_SIZE);
+ while (i_stream_read_more(input, &data, &size) > 0) {
+ sha1_loop(&sha1, data, size);
+ o_stream_nsend(output, data, size);
+ i_stream_skip(input, size);
+ }
+ if (o_stream_finish(output) < 0) {
+ i_fatal("write(%s) failed: %s",
+ out_path, o_stream_get_error(output));
+ }
+ i_stream_destroy(&input);
+ o_stream_destroy(&output);
+ o_stream_destroy(&file_output);
+ sha1_result(&sha1, output_sha1);
+
+ /* verify that we can read the compressed file */
+ sha1_init(&sha1);
+ file_input = i_stream_create_fd(fd_out, IO_BLOCK_SIZE);
+ input = handler->create_istream(file_input);
+ while (i_stream_read_more(input, &data, &size) > 0) {
+ sha1_loop(&sha1, data, size);
+ i_stream_skip(input, size);
+ }
+ i_stream_destroy(&input);
+ i_stream_destroy(&file_input);
+ sha1_result(&sha1, input_sha1);
+
+ if (memcmp(input_sha1, output_sha1, sizeof(input_sha1)) != 0)
+ i_fatal("Decompression couldn't get the original input");
+ i_close_fd(&fd_out);
+}
+
+static void test_compression_ext(void)
+{
+ const struct compression_handler *handler;
+ test_begin("compression handler by extension");
+
+ for (unsigned int i = 0; compression_handlers[i].name != NULL; i++) {
+ if (compression_handlers[i].ext != NULL) {
+ const char *path =
+ t_strconcat("file", compression_handlers[i].ext, NULL);
+ int ret = compression_lookup_handler_from_ext(path, &handler);
+ test_assert(ret == 0 || handler == &compression_handlers[i]);
+ }
+ }
+
+ test_end();
+}
+
+int main(int argc, char *argv[])
+{
+ static void (*const test_functions[])(void) = {
+ test_compression,
+ test_istream_decompression_try,
+ test_gz_concat,
+ test_gz_no_concat,
+ test_gz_header,
+ test_gz_large_header,
+ test_lz4_small_header,
+ test_compression_ext,
+ NULL
+ };
+ if (argc == 2) {
+ test_uncompress_file(argv[1]);
+ return 0;
+ }
+ if (argc == 3) {
+ test_compress_file(argv[1], argv[2]);
+ return 0;
+ }
+ return test_run(test_functions);
+}
diff --git a/src/lib-dcrypt/Makefile.am b/src/lib-dcrypt/Makefile.am
new file mode 100644
index 0000000..e9e5116
--- /dev/null
+++ b/src/lib-dcrypt/Makefile.am
@@ -0,0 +1,75 @@
+noinst_LTLIBRARIES = libdcrypt.la
+pkglib_LTLIBRARIES =
+
+NOPLUGIN_LDFLAGS=
+
+AM_CPPFLAGS = \
+ -I$(top_srcdir)/src/lib \
+ -I$(top_srcdir)/src/lib-test \
+ -I$(top_srcdir)/src/lib-ssl-iostream \
+ -DDCRYPT_MODULE_DIR=\"$(pkglibdir)\"
+
+libdcrypt_la_SOURCES = \
+ dcrypt.c \
+ istream-decrypt.c \
+ ostream-encrypt.c
+
+libdcrypt_la_CFLAGS = $(AM_CPPFLAGS) \
+ -DDCRYPT_MODULE_DIR=\"$(pkglibdir)\"
+
+if BUILD_DCRYPT_OPENSSL
+pkglib_LTLIBRARIES += libdcrypt_openssl.la
+libdcrypt_openssl_la_SOURCES = dcrypt-openssl.c
+libdcrypt_openssl_la_LDFLAGS = -module -avoid-version ../lib-ssl-iostream/libssl_iostream_openssl.la
+libdcrypt_openssl_la_LIBADD = $(SSL_LIBS)
+libdcrypt_openssl_la_DEPENDENCIES = ../lib-ssl-iostream/libssl_iostream_openssl.la
+libdcrypt_openssl_la_CFLAGS = $(AM_CPPFLAGS) \
+ $(SSL_CFLAGS)
+endif
+
+headers = \
+ dcrypt.h \
+ dcrypt-iostream.h \
+ dcrypt-private.h \
+ ostream-encrypt.h \
+ istream-decrypt.h
+
+pkginc_libdir=$(pkgincludedir)
+pkginc_lib_HEADERS = $(headers)
+
+EXTRA_DIST = \
+ sample-v1.asc \
+ sample-v1_short.asc \
+ sample-v2.asc
+
+test_programs = test-crypto test-stream
+noinst_PROGRAMS = $(test_programs)
+
+check-local:
+ for bin in $(test_programs); do \
+ if ! $(RUN_TEST) ./$$bin; then exit 1; fi; \
+ done
+
+LIBDOVECOT_TEST_DEPS = \
+ ../lib-ssl-iostream/libssl_iostream.la \
+ ../lib-test/libtest.la \
+ ../lib/liblib.la
+LIBDOVECOT_TEST = \
+ $(LIBDOVECOT_TEST_DEPS) \
+ $(MODULE_LIBS)
+
+test_crypto_LDADD = $(LIBDOVECOT_TEST)
+test_crypto_DEPENDENCIES = $(LIBDOVECOT_TEST_DEPS)
+if HAVE_WHOLE_ARCHIVE
+test_crypto_LDFLAGS = -Wl,$(LD_WHOLE_ARCHIVE),../lib-ssl-iostream/.libs/libssl_iostream.a,$(LD_NO_WHOLE_ARCHIVE)
+endif
+test_crypto_CFLAGS = $(AM_CPPFLAGS) -DDCRYPT_SRC_DIR=\"$(top_srcdir)/src/lib-dcrypt\"
+test_crypto_SOURCES = $(libdcrypt_la_SOURCES) test-crypto.c
+
+test_stream_LDADD = $(LIBDOVECOT_TEST)
+test_stream_DEPENDENCIES = $(LIBDOVECOT_TEST_DEPS)
+if HAVE_WHOLE_ARCHIVE
+test_stream_LDFLAGS = -Wl,$(LD_WHOLE_ARCHIVE),../lib-ssl-iostream/.libs/libssl_iostream.a,$(LD_NO_WHOLE_ARCHIVE)
+endif
+test_stream_CFLAGS = $(AM_CPPFLAGS) -DDCRYPT_SRC_DIR=\"$(top_srcdir)/src/lib-dcrypt\"
+test_stream_SOURCES = $(libdcrypt_la_SOURCES) test-stream.c
diff --git a/src/lib-dcrypt/Makefile.in b/src/lib-dcrypt/Makefile.in
new file mode 100644
index 0000000..801b966
--- /dev/null
+++ b/src/lib-dcrypt/Makefile.in
@@ -0,0 +1,1142 @@
+# Makefile.in generated by automake 1.16.1 from Makefile.am.
+# @configure_input@
+
+# Copyright (C) 1994-2018 Free Software Foundation, Inc.
+
+# This Makefile.in is free software; the Free Software Foundation
+# gives unlimited permission to copy and/or distribute it,
+# with or without modifications, as long as this notice is preserved.
+
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY, to the extent permitted by law; without
+# even the implied warranty of MERCHANTABILITY or FITNESS FOR A
+# PARTICULAR PURPOSE.
+
+@SET_MAKE@
+
+
+
+VPATH = @srcdir@
+am__is_gnu_make = { \
+ if test -z '$(MAKELEVEL)'; then \
+ false; \
+ elif test -n '$(MAKE_HOST)'; then \
+ true; \
+ elif test -n '$(MAKE_VERSION)' && test -n '$(CURDIR)'; then \
+ true; \
+ else \
+ false; \
+ fi; \
+}
+am__make_running_with_option = \
+ case $${target_option-} in \
+ ?) ;; \
+ *) echo "am__make_running_with_option: internal error: invalid" \
+ "target option '$${target_option-}' specified" >&2; \
+ exit 1;; \
+ esac; \
+ has_opt=no; \
+ sane_makeflags=$$MAKEFLAGS; \
+ if $(am__is_gnu_make); then \
+ sane_makeflags=$$MFLAGS; \
+ else \
+ case $$MAKEFLAGS in \
+ *\\[\ \ ]*) \
+ bs=\\; \
+ sane_makeflags=`printf '%s\n' "$$MAKEFLAGS" \
+ | sed "s/$$bs$$bs[$$bs $$bs ]*//g"`;; \
+ esac; \
+ fi; \
+ skip_next=no; \
+ strip_trailopt () \
+ { \
+ flg=`printf '%s\n' "$$flg" | sed "s/$$1.*$$//"`; \
+ }; \
+ for flg in $$sane_makeflags; do \
+ test $$skip_next = yes && { skip_next=no; continue; }; \
+ case $$flg in \
+ *=*|--*) continue;; \
+ -*I) strip_trailopt 'I'; skip_next=yes;; \
+ -*I?*) strip_trailopt 'I';; \
+ -*O) strip_trailopt 'O'; skip_next=yes;; \
+ -*O?*) strip_trailopt 'O';; \
+ -*l) strip_trailopt 'l'; skip_next=yes;; \
+ -*l?*) strip_trailopt 'l';; \
+ -[dEDm]) skip_next=yes;; \
+ -[JT]) skip_next=yes;; \
+ esac; \
+ case $$flg in \
+ *$$target_option*) has_opt=yes; break;; \
+ esac; \
+ done; \
+ test $$has_opt = yes
+am__make_dryrun = (target_option=n; $(am__make_running_with_option))
+am__make_keepgoing = (target_option=k; $(am__make_running_with_option))
+pkgdatadir = $(datadir)/@PACKAGE@
+pkgincludedir = $(includedir)/@PACKAGE@
+pkglibdir = $(libdir)/@PACKAGE@
+pkglibexecdir = $(libexecdir)/@PACKAGE@
+am__cd = CDPATH="$${ZSH_VERSION+.}$(PATH_SEPARATOR)" && cd
+install_sh_DATA = $(install_sh) -c -m 644
+install_sh_PROGRAM = $(install_sh) -c
+install_sh_SCRIPT = $(install_sh) -c
+INSTALL_HEADER = $(INSTALL_DATA)
+transform = $(program_transform_name)
+NORMAL_INSTALL = :
+PRE_INSTALL = :
+POST_INSTALL = :
+NORMAL_UNINSTALL = :
+PRE_UNINSTALL = :
+POST_UNINSTALL = :
+build_triplet = @build@
+host_triplet = @host@
+@BUILD_DCRYPT_OPENSSL_TRUE@am__append_1 = libdcrypt_openssl.la
+noinst_PROGRAMS = $(am__EXEEXT_1)
+subdir = src/lib-dcrypt
+ACLOCAL_M4 = $(top_srcdir)/aclocal.m4
+am__aclocal_m4_deps = $(top_srcdir)/m4/ac_checktype2.m4 \
+ $(top_srcdir)/m4/ac_typeof.m4 $(top_srcdir)/m4/arc4random.m4 \
+ $(top_srcdir)/m4/blockdev.m4 $(top_srcdir)/m4/c99_vsnprintf.m4 \
+ $(top_srcdir)/m4/clock_gettime.m4 $(top_srcdir)/m4/crypt.m4 \
+ $(top_srcdir)/m4/crypt_xpg6.m4 $(top_srcdir)/m4/dbqlk.m4 \
+ $(top_srcdir)/m4/dirent_dtype.m4 $(top_srcdir)/m4/dovecot.m4 \
+ $(top_srcdir)/m4/fd_passing.m4 $(top_srcdir)/m4/fdatasync.m4 \
+ $(top_srcdir)/m4/flexible_array_member.m4 \
+ $(top_srcdir)/m4/glibc.m4 $(top_srcdir)/m4/gmtime_max.m4 \
+ $(top_srcdir)/m4/gmtime_tm_gmtoff.m4 \
+ $(top_srcdir)/m4/ioloop.m4 $(top_srcdir)/m4/iovec.m4 \
+ $(top_srcdir)/m4/ipv6.m4 $(top_srcdir)/m4/libcap.m4 \
+ $(top_srcdir)/m4/libtool.m4 $(top_srcdir)/m4/libwrap.m4 \
+ $(top_srcdir)/m4/linux_mremap.m4 $(top_srcdir)/m4/ltoptions.m4 \
+ $(top_srcdir)/m4/ltsugar.m4 $(top_srcdir)/m4/ltversion.m4 \
+ $(top_srcdir)/m4/lt~obsolete.m4 $(top_srcdir)/m4/mmap_write.m4 \
+ $(top_srcdir)/m4/mntctl.m4 $(top_srcdir)/m4/modules.m4 \
+ $(top_srcdir)/m4/notify.m4 $(top_srcdir)/m4/nsl.m4 \
+ $(top_srcdir)/m4/off_t_max.m4 $(top_srcdir)/m4/pkg.m4 \
+ $(top_srcdir)/m4/pr_set_dumpable.m4 \
+ $(top_srcdir)/m4/q_quotactl.m4 $(top_srcdir)/m4/quota.m4 \
+ $(top_srcdir)/m4/random.m4 $(top_srcdir)/m4/rlimit.m4 \
+ $(top_srcdir)/m4/sendfile.m4 $(top_srcdir)/m4/size_t_signed.m4 \
+ $(top_srcdir)/m4/sockpeercred.m4 $(top_srcdir)/m4/sql.m4 \
+ $(top_srcdir)/m4/ssl.m4 $(top_srcdir)/m4/st_tim.m4 \
+ $(top_srcdir)/m4/static_array.m4 $(top_srcdir)/m4/test_with.m4 \
+ $(top_srcdir)/m4/time_t.m4 $(top_srcdir)/m4/typeof.m4 \
+ $(top_srcdir)/m4/typeof_dev_t.m4 \
+ $(top_srcdir)/m4/uoff_t_max.m4 $(top_srcdir)/m4/vararg.m4 \
+ $(top_srcdir)/m4/want_apparmor.m4 \
+ $(top_srcdir)/m4/want_bsdauth.m4 \
+ $(top_srcdir)/m4/want_bzlib.m4 \
+ $(top_srcdir)/m4/want_cassandra.m4 \
+ $(top_srcdir)/m4/want_cdb.m4 \
+ $(top_srcdir)/m4/want_checkpassword.m4 \
+ $(top_srcdir)/m4/want_clucene.m4 $(top_srcdir)/m4/want_db.m4 \
+ $(top_srcdir)/m4/want_gssapi.m4 $(top_srcdir)/m4/want_icu.m4 \
+ $(top_srcdir)/m4/want_ldap.m4 $(top_srcdir)/m4/want_lua.m4 \
+ $(top_srcdir)/m4/want_lz4.m4 $(top_srcdir)/m4/want_lzma.m4 \
+ $(top_srcdir)/m4/want_mysql.m4 $(top_srcdir)/m4/want_pam.m4 \
+ $(top_srcdir)/m4/want_passwd.m4 $(top_srcdir)/m4/want_pgsql.m4 \
+ $(top_srcdir)/m4/want_prefetch.m4 \
+ $(top_srcdir)/m4/want_shadow.m4 \
+ $(top_srcdir)/m4/want_sodium.m4 $(top_srcdir)/m4/want_solr.m4 \
+ $(top_srcdir)/m4/want_sqlite.m4 \
+ $(top_srcdir)/m4/want_stemmer.m4 \
+ $(top_srcdir)/m4/want_systemd.m4 \
+ $(top_srcdir)/m4/want_textcat.m4 \
+ $(top_srcdir)/m4/want_unwind.m4 $(top_srcdir)/m4/want_zlib.m4 \
+ $(top_srcdir)/m4/want_zstd.m4 $(top_srcdir)/configure.ac
+am__configure_deps = $(am__aclocal_m4_deps) $(CONFIGURE_DEPENDENCIES) \
+ $(ACLOCAL_M4)
+DIST_COMMON = $(srcdir)/Makefile.am $(pkginc_lib_HEADERS) \
+ $(am__DIST_COMMON)
+mkinstalldirs = $(install_sh) -d
+CONFIG_HEADER = $(top_builddir)/config.h
+CONFIG_CLEAN_FILES =
+CONFIG_CLEAN_VPATH_FILES =
+am__EXEEXT_1 = test-crypto$(EXEEXT) test-stream$(EXEEXT)
+PROGRAMS = $(noinst_PROGRAMS)
+am__vpath_adj_setup = srcdirstrip=`echo "$(srcdir)" | sed 's|.|.|g'`;
+am__vpath_adj = case $$p in \
+ $(srcdir)/*) f=`echo "$$p" | sed "s|^$$srcdirstrip/||"`;; \
+ *) f=$$p;; \
+ esac;
+am__strip_dir = f=`echo $$p | sed -e 's|^.*/||'`;
+am__install_max = 40
+am__nobase_strip_setup = \
+ srcdirstrip=`echo "$(srcdir)" | sed 's/[].[^$$\\*|]/\\\\&/g'`
+am__nobase_strip = \
+ for p in $$list; do echo "$$p"; done | sed -e "s|$$srcdirstrip/||"
+am__nobase_list = $(am__nobase_strip_setup); \
+ for p in $$list; do echo "$$p $$p"; done | \
+ sed "s| $$srcdirstrip/| |;"' / .*\//!s/ .*/ ./; s,\( .*\)/[^/]*$$,\1,' | \
+ $(AWK) 'BEGIN { files["."] = "" } { files[$$2] = files[$$2] " " $$1; \
+ if (++n[$$2] == $(am__install_max)) \
+ { print $$2, files[$$2]; n[$$2] = 0; files[$$2] = "" } } \
+ END { for (dir in files) print dir, files[dir] }'
+am__base_list = \
+ sed '$$!N;$$!N;$$!N;$$!N;$$!N;$$!N;$$!N;s/\n/ /g' | \
+ sed '$$!N;$$!N;$$!N;$$!N;s/\n/ /g'
+am__uninstall_files_from_dir = { \
+ test -z "$$files" \
+ || { test ! -d "$$dir" && test ! -f "$$dir" && test ! -r "$$dir"; } \
+ || { echo " ( cd '$$dir' && rm -f" $$files ")"; \
+ $(am__cd) "$$dir" && rm -f $$files; }; \
+ }
+am__installdirs = "$(DESTDIR)$(pkglibdir)" \
+ "$(DESTDIR)$(pkginc_libdir)"
+LTLIBRARIES = $(noinst_LTLIBRARIES) $(pkglib_LTLIBRARIES)
+libdcrypt_la_LIBADD =
+am_libdcrypt_la_OBJECTS = libdcrypt_la-dcrypt.lo \
+ libdcrypt_la-istream-decrypt.lo \
+ libdcrypt_la-ostream-encrypt.lo
+libdcrypt_la_OBJECTS = $(am_libdcrypt_la_OBJECTS)
+AM_V_lt = $(am__v_lt_@AM_V@)
+am__v_lt_ = $(am__v_lt_@AM_DEFAULT_V@)
+am__v_lt_0 = --silent
+am__v_lt_1 =
+libdcrypt_la_LINK = $(LIBTOOL) $(AM_V_lt) --tag=CC $(AM_LIBTOOLFLAGS) \
+ $(LIBTOOLFLAGS) --mode=link $(CCLD) $(libdcrypt_la_CFLAGS) \
+ $(CFLAGS) $(AM_LDFLAGS) $(LDFLAGS) -o $@
+am__DEPENDENCIES_1 =
+am__libdcrypt_openssl_la_SOURCES_DIST = dcrypt-openssl.c
+@BUILD_DCRYPT_OPENSSL_TRUE@am_libdcrypt_openssl_la_OBJECTS = libdcrypt_openssl_la-dcrypt-openssl.lo
+libdcrypt_openssl_la_OBJECTS = $(am_libdcrypt_openssl_la_OBJECTS)
+libdcrypt_openssl_la_LINK = $(LIBTOOL) $(AM_V_lt) --tag=CC \
+ $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=link $(CCLD) \
+ $(libdcrypt_openssl_la_CFLAGS) $(CFLAGS) \
+ $(libdcrypt_openssl_la_LDFLAGS) $(LDFLAGS) -o $@
+@BUILD_DCRYPT_OPENSSL_TRUE@am_libdcrypt_openssl_la_rpath = -rpath \
+@BUILD_DCRYPT_OPENSSL_TRUE@ $(pkglibdir)
+am__objects_1 = test_crypto-dcrypt.$(OBJEXT) \
+ test_crypto-istream-decrypt.$(OBJEXT) \
+ test_crypto-ostream-encrypt.$(OBJEXT)
+am_test_crypto_OBJECTS = $(am__objects_1) \
+ test_crypto-test-crypto.$(OBJEXT)
+test_crypto_OBJECTS = $(am_test_crypto_OBJECTS)
+am__DEPENDENCIES_2 = $(LIBDOVECOT_TEST_DEPS) $(am__DEPENDENCIES_1)
+test_crypto_LINK = $(LIBTOOL) $(AM_V_lt) --tag=CC $(AM_LIBTOOLFLAGS) \
+ $(LIBTOOLFLAGS) --mode=link $(CCLD) $(test_crypto_CFLAGS) \
+ $(CFLAGS) $(test_crypto_LDFLAGS) $(LDFLAGS) -o $@
+am__objects_2 = test_stream-dcrypt.$(OBJEXT) \
+ test_stream-istream-decrypt.$(OBJEXT) \
+ test_stream-ostream-encrypt.$(OBJEXT)
+am_test_stream_OBJECTS = $(am__objects_2) \
+ test_stream-test-stream.$(OBJEXT)
+test_stream_OBJECTS = $(am_test_stream_OBJECTS)
+test_stream_LINK = $(LIBTOOL) $(AM_V_lt) --tag=CC $(AM_LIBTOOLFLAGS) \
+ $(LIBTOOLFLAGS) --mode=link $(CCLD) $(test_stream_CFLAGS) \
+ $(CFLAGS) $(test_stream_LDFLAGS) $(LDFLAGS) -o $@
+AM_V_P = $(am__v_P_@AM_V@)
+am__v_P_ = $(am__v_P_@AM_DEFAULT_V@)
+am__v_P_0 = false
+am__v_P_1 = :
+AM_V_GEN = $(am__v_GEN_@AM_V@)
+am__v_GEN_ = $(am__v_GEN_@AM_DEFAULT_V@)
+am__v_GEN_0 = @echo " GEN " $@;
+am__v_GEN_1 =
+AM_V_at = $(am__v_at_@AM_V@)
+am__v_at_ = $(am__v_at_@AM_DEFAULT_V@)
+am__v_at_0 = @
+am__v_at_1 =
+DEFAULT_INCLUDES = -I.@am__isrc@ -I$(top_builddir)
+depcomp = $(SHELL) $(top_srcdir)/depcomp
+am__maybe_remake_depfiles = depfiles
+am__depfiles_remade = ./$(DEPDIR)/libdcrypt_la-dcrypt.Plo \
+ ./$(DEPDIR)/libdcrypt_la-istream-decrypt.Plo \
+ ./$(DEPDIR)/libdcrypt_la-ostream-encrypt.Plo \
+ ./$(DEPDIR)/libdcrypt_openssl_la-dcrypt-openssl.Plo \
+ ./$(DEPDIR)/test_crypto-dcrypt.Po \
+ ./$(DEPDIR)/test_crypto-istream-decrypt.Po \
+ ./$(DEPDIR)/test_crypto-ostream-encrypt.Po \
+ ./$(DEPDIR)/test_crypto-test-crypto.Po \
+ ./$(DEPDIR)/test_stream-dcrypt.Po \
+ ./$(DEPDIR)/test_stream-istream-decrypt.Po \
+ ./$(DEPDIR)/test_stream-ostream-encrypt.Po \
+ ./$(DEPDIR)/test_stream-test-stream.Po
+am__mv = mv -f
+COMPILE = $(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) \
+ $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS)
+LTCOMPILE = $(LIBTOOL) $(AM_V_lt) --tag=CC $(AM_LIBTOOLFLAGS) \
+ $(LIBTOOLFLAGS) --mode=compile $(CC) $(DEFS) \
+ $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) $(CPPFLAGS) \
+ $(AM_CFLAGS) $(CFLAGS)
+AM_V_CC = $(am__v_CC_@AM_V@)
+am__v_CC_ = $(am__v_CC_@AM_DEFAULT_V@)
+am__v_CC_0 = @echo " CC " $@;
+am__v_CC_1 =
+CCLD = $(CC)
+LINK = $(LIBTOOL) $(AM_V_lt) --tag=CC $(AM_LIBTOOLFLAGS) \
+ $(LIBTOOLFLAGS) --mode=link $(CCLD) $(AM_CFLAGS) $(CFLAGS) \
+ $(AM_LDFLAGS) $(LDFLAGS) -o $@
+AM_V_CCLD = $(am__v_CCLD_@AM_V@)
+am__v_CCLD_ = $(am__v_CCLD_@AM_DEFAULT_V@)
+am__v_CCLD_0 = @echo " CCLD " $@;
+am__v_CCLD_1 =
+SOURCES = $(libdcrypt_la_SOURCES) $(libdcrypt_openssl_la_SOURCES) \
+ $(test_crypto_SOURCES) $(test_stream_SOURCES)
+DIST_SOURCES = $(libdcrypt_la_SOURCES) \
+ $(am__libdcrypt_openssl_la_SOURCES_DIST) \
+ $(test_crypto_SOURCES) $(test_stream_SOURCES)
+am__can_run_installinfo = \
+ case $$AM_UPDATE_INFO_DIR in \
+ n|no|NO) false;; \
+ *) (install-info --version) >/dev/null 2>&1;; \
+ esac
+HEADERS = $(pkginc_lib_HEADERS)
+am__tagged_files = $(HEADERS) $(SOURCES) $(TAGS_FILES) $(LISP)
+# Read a list of newline-separated strings from the standard input,
+# and print each of them once, without duplicates. Input order is
+# *not* preserved.
+am__uniquify_input = $(AWK) '\
+ BEGIN { nonempty = 0; } \
+ { items[$$0] = 1; nonempty = 1; } \
+ END { if (nonempty) { for (i in items) print i; }; } \
+'
+# Make sure the list of sources is unique. This is necessary because,
+# e.g., the same source file might be shared among _SOURCES variables
+# for different programs/libraries.
+am__define_uniq_tagged_files = \
+ list='$(am__tagged_files)'; \
+ unique=`for i in $$list; do \
+ if test -f "$$i"; then echo $$i; else echo $(srcdir)/$$i; fi; \
+ done | $(am__uniquify_input)`
+ETAGS = etags
+CTAGS = ctags
+am__DIST_COMMON = $(srcdir)/Makefile.in $(top_srcdir)/depcomp
+DISTFILES = $(DIST_COMMON) $(DIST_SOURCES) $(TEXINFOS) $(EXTRA_DIST)
+ACLOCAL = @ACLOCAL@
+ACLOCAL_AMFLAGS = @ACLOCAL_AMFLAGS@
+AMTAR = @AMTAR@
+AM_DEFAULT_VERBOSITY = @AM_DEFAULT_VERBOSITY@
+APPARMOR_LIBS = @APPARMOR_LIBS@
+AR = @AR@
+AUTH_CFLAGS = @AUTH_CFLAGS@
+AUTH_LIBS = @AUTH_LIBS@
+AUTOCONF = @AUTOCONF@
+AUTOHEADER = @AUTOHEADER@
+AUTOMAKE = @AUTOMAKE@
+AWK = @AWK@
+BINARY_CFLAGS = @BINARY_CFLAGS@
+BINARY_LDFLAGS = @BINARY_LDFLAGS@
+BISON = @BISON@
+CASSANDRA_CFLAGS = @CASSANDRA_CFLAGS@
+CASSANDRA_LIBS = @CASSANDRA_LIBS@
+CC = @CC@
+CCDEPMODE = @CCDEPMODE@
+CDB_LIBS = @CDB_LIBS@
+CFLAGS = @CFLAGS@
+CLUCENE_CFLAGS = @CLUCENE_CFLAGS@
+CLUCENE_LIBS = @CLUCENE_LIBS@
+COMPRESS_LIBS = @COMPRESS_LIBS@
+CPP = @CPP@
+CPPFLAGS = @CPPFLAGS@
+CRYPT_LIBS = @CRYPT_LIBS@
+CXX = @CXX@
+CXXCPP = @CXXCPP@
+CXXDEPMODE = @CXXDEPMODE@
+CXXFLAGS = @CXXFLAGS@
+CYGPATH_W = @CYGPATH_W@
+DEFS = @DEFS@
+DEPDIR = @DEPDIR@
+DICT_LIBS = @DICT_LIBS@
+DLLIB = @DLLIB@
+DLLTOOL = @DLLTOOL@
+DSYMUTIL = @DSYMUTIL@
+DUMPBIN = @DUMPBIN@
+ECHO_C = @ECHO_C@
+ECHO_N = @ECHO_N@
+ECHO_T = @ECHO_T@
+EGREP = @EGREP@
+EXEEXT = @EXEEXT@
+FGREP = @FGREP@
+FLEX = @FLEX@
+FUZZER_CPPFLAGS = @FUZZER_CPPFLAGS@
+FUZZER_LDFLAGS = @FUZZER_LDFLAGS@
+GREP = @GREP@
+INSTALL = @INSTALL@
+INSTALL_DATA = @INSTALL_DATA@
+INSTALL_PROGRAM = @INSTALL_PROGRAM@
+INSTALL_SCRIPT = @INSTALL_SCRIPT@
+INSTALL_STRIP_PROGRAM = @INSTALL_STRIP_PROGRAM@
+KRB5CONFIG = @KRB5CONFIG@
+KRB5_CFLAGS = @KRB5_CFLAGS@
+KRB5_LIBS = @KRB5_LIBS@
+LD = @LD@
+LDAP_LIBS = @LDAP_LIBS@
+LDFLAGS = @LDFLAGS@
+LD_NO_WHOLE_ARCHIVE = @LD_NO_WHOLE_ARCHIVE@
+LD_WHOLE_ARCHIVE = @LD_WHOLE_ARCHIVE@
+LIBCAP = @LIBCAP@
+LIBDOVECOT = @LIBDOVECOT@
+LIBDOVECOT_COMPRESS = @LIBDOVECOT_COMPRESS@
+LIBDOVECOT_DEPS = @LIBDOVECOT_DEPS@
+LIBDOVECOT_DSYNC = @LIBDOVECOT_DSYNC@
+LIBDOVECOT_LA_LIBS = @LIBDOVECOT_LA_LIBS@
+LIBDOVECOT_LDA = @LIBDOVECOT_LDA@
+LIBDOVECOT_LDAP = @LIBDOVECOT_LDAP@
+LIBDOVECOT_LIBFTS = @LIBDOVECOT_LIBFTS@
+LIBDOVECOT_LIBFTS_DEPS = @LIBDOVECOT_LIBFTS_DEPS@
+LIBDOVECOT_LOGIN = @LIBDOVECOT_LOGIN@
+LIBDOVECOT_LUA = @LIBDOVECOT_LUA@
+LIBDOVECOT_LUA_DEPS = @LIBDOVECOT_LUA_DEPS@
+LIBDOVECOT_SQL = @LIBDOVECOT_SQL@
+LIBDOVECOT_STORAGE = @LIBDOVECOT_STORAGE@
+LIBDOVECOT_STORAGE_DEPS = @LIBDOVECOT_STORAGE_DEPS@
+LIBEXTTEXTCAT_CFLAGS = @LIBEXTTEXTCAT_CFLAGS@
+LIBEXTTEXTCAT_LIBS = @LIBEXTTEXTCAT_LIBS@
+LIBICONV = @LIBICONV@
+LIBICU_CFLAGS = @LIBICU_CFLAGS@
+LIBICU_LIBS = @LIBICU_LIBS@
+LIBOBJS = @LIBOBJS@
+LIBS = @LIBS@
+LIBSODIUM_CFLAGS = @LIBSODIUM_CFLAGS@
+LIBSODIUM_LIBS = @LIBSODIUM_LIBS@
+LIBTIRPC_CFLAGS = @LIBTIRPC_CFLAGS@
+LIBTIRPC_LIBS = @LIBTIRPC_LIBS@
+LIBTOOL = @LIBTOOL@
+LIBUNWIND_CFLAGS = @LIBUNWIND_CFLAGS@
+LIBUNWIND_LIBS = @LIBUNWIND_LIBS@
+LIBWRAP_LIBS = @LIBWRAP_LIBS@
+LINKED_STORAGE_LDADD = @LINKED_STORAGE_LDADD@
+LIPO = @LIPO@
+LN_S = @LN_S@
+LTLIBICONV = @LTLIBICONV@
+LTLIBOBJS = @LTLIBOBJS@
+LT_SYS_LIBRARY_PATH = @LT_SYS_LIBRARY_PATH@
+LUA_CFLAGS = @LUA_CFLAGS@
+LUA_LIBS = @LUA_LIBS@
+MAINT = @MAINT@
+MAKEINFO = @MAKEINFO@
+MANIFEST_TOOL = @MANIFEST_TOOL@
+MKDIR_P = @MKDIR_P@
+MODULE_LIBS = @MODULE_LIBS@
+MODULE_SUFFIX = @MODULE_SUFFIX@
+MYSQL_CFLAGS = @MYSQL_CFLAGS@
+MYSQL_CONFIG = @MYSQL_CONFIG@
+MYSQL_LIBS = @MYSQL_LIBS@
+NM = @NM@
+NMEDIT = @NMEDIT@
+NOPLUGIN_LDFLAGS =
+OBJDUMP = @OBJDUMP@
+OBJEXT = @OBJEXT@
+OTOOL = @OTOOL@
+OTOOL64 = @OTOOL64@
+PACKAGE = @PACKAGE@
+PACKAGE_BUGREPORT = @PACKAGE_BUGREPORT@
+PACKAGE_NAME = @PACKAGE_NAME@
+PACKAGE_STRING = @PACKAGE_STRING@
+PACKAGE_TARNAME = @PACKAGE_TARNAME@
+PACKAGE_URL = @PACKAGE_URL@
+PACKAGE_VERSION = @PACKAGE_VERSION@
+PANDOC = @PANDOC@
+PATH_SEPARATOR = @PATH_SEPARATOR@
+PGSQL_CFLAGS = @PGSQL_CFLAGS@
+PGSQL_LIBS = @PGSQL_LIBS@
+PG_CONFIG = @PG_CONFIG@
+PIE_CFLAGS = @PIE_CFLAGS@
+PIE_LDFLAGS = @PIE_LDFLAGS@
+PKG_CONFIG = @PKG_CONFIG@
+PKG_CONFIG_LIBDIR = @PKG_CONFIG_LIBDIR@
+PKG_CONFIG_PATH = @PKG_CONFIG_PATH@
+QUOTA_LIBS = @QUOTA_LIBS@
+RANLIB = @RANLIB@
+RELRO_LDFLAGS = @RELRO_LDFLAGS@
+RPCGEN = @RPCGEN@
+RUN_TEST = @RUN_TEST@
+SED = @SED@
+SETTING_FILES = @SETTING_FILES@
+SET_MAKE = @SET_MAKE@
+SHELL = @SHELL@
+SQLITE_CFLAGS = @SQLITE_CFLAGS@
+SQLITE_LIBS = @SQLITE_LIBS@
+SQL_CFLAGS = @SQL_CFLAGS@
+SQL_LIBS = @SQL_LIBS@
+SSL_CFLAGS = @SSL_CFLAGS@
+SSL_LIBS = @SSL_LIBS@
+STRIP = @STRIP@
+SYSTEMD_CFLAGS = @SYSTEMD_CFLAGS@
+SYSTEMD_LIBS = @SYSTEMD_LIBS@
+VALGRIND = @VALGRIND@
+VERSION = @VERSION@
+ZSTD_CFLAGS = @ZSTD_CFLAGS@
+ZSTD_LIBS = @ZSTD_LIBS@
+abs_builddir = @abs_builddir@
+abs_srcdir = @abs_srcdir@
+abs_top_builddir = @abs_top_builddir@
+abs_top_srcdir = @abs_top_srcdir@
+ac_ct_AR = @ac_ct_AR@
+ac_ct_CC = @ac_ct_CC@
+ac_ct_CXX = @ac_ct_CXX@
+ac_ct_DUMPBIN = @ac_ct_DUMPBIN@
+am__include = @am__include@
+am__leading_dot = @am__leading_dot@
+am__quote = @am__quote@
+am__tar = @am__tar@
+am__untar = @am__untar@
+bindir = @bindir@
+build = @build@
+build_alias = @build_alias@
+build_cpu = @build_cpu@
+build_os = @build_os@
+build_vendor = @build_vendor@
+builddir = @builddir@
+datadir = @datadir@
+datarootdir = @datarootdir@
+dict_drivers = @dict_drivers@
+docdir = @docdir@
+dvidir = @dvidir@
+exec_prefix = @exec_prefix@
+host = @host@
+host_alias = @host_alias@
+host_cpu = @host_cpu@
+host_os = @host_os@
+host_vendor = @host_vendor@
+htmldir = @htmldir@
+includedir = @includedir@
+infodir = @infodir@
+install_sh = @install_sh@
+libdir = @libdir@
+libexecdir = @libexecdir@
+localedir = @localedir@
+localstatedir = @localstatedir@
+mandir = @mandir@
+mkdir_p = @mkdir_p@
+moduledir = @moduledir@
+oldincludedir = @oldincludedir@
+pdfdir = @pdfdir@
+prefix = @prefix@
+program_transform_name = @program_transform_name@
+psdir = @psdir@
+rundir = @rundir@
+runstatedir = @runstatedir@
+sbindir = @sbindir@
+sharedstatedir = @sharedstatedir@
+sql_drivers = @sql_drivers@
+srcdir = @srcdir@
+ssldir = @ssldir@
+statedir = @statedir@
+sysconfdir = @sysconfdir@
+systemdservicetype = @systemdservicetype@
+systemdsystemunitdir = @systemdsystemunitdir@
+target_alias = @target_alias@
+top_build_prefix = @top_build_prefix@
+top_builddir = @top_builddir@
+top_srcdir = @top_srcdir@
+noinst_LTLIBRARIES = libdcrypt.la
+pkglib_LTLIBRARIES = $(am__append_1)
+AM_CPPFLAGS = \
+ -I$(top_srcdir)/src/lib \
+ -I$(top_srcdir)/src/lib-test \
+ -I$(top_srcdir)/src/lib-ssl-iostream \
+ -DDCRYPT_MODULE_DIR=\"$(pkglibdir)\"
+
+libdcrypt_la_SOURCES = \
+ dcrypt.c \
+ istream-decrypt.c \
+ ostream-encrypt.c
+
+libdcrypt_la_CFLAGS = $(AM_CPPFLAGS) \
+ -DDCRYPT_MODULE_DIR=\"$(pkglibdir)\"
+
+@BUILD_DCRYPT_OPENSSL_TRUE@libdcrypt_openssl_la_SOURCES = dcrypt-openssl.c
+@BUILD_DCRYPT_OPENSSL_TRUE@libdcrypt_openssl_la_LDFLAGS = -module -avoid-version ../lib-ssl-iostream/libssl_iostream_openssl.la
+@BUILD_DCRYPT_OPENSSL_TRUE@libdcrypt_openssl_la_LIBADD = $(SSL_LIBS)
+@BUILD_DCRYPT_OPENSSL_TRUE@libdcrypt_openssl_la_DEPENDENCIES = ../lib-ssl-iostream/libssl_iostream_openssl.la
+@BUILD_DCRYPT_OPENSSL_TRUE@libdcrypt_openssl_la_CFLAGS = $(AM_CPPFLAGS) \
+@BUILD_DCRYPT_OPENSSL_TRUE@ $(SSL_CFLAGS)
+
+headers = \
+ dcrypt.h \
+ dcrypt-iostream.h \
+ dcrypt-private.h \
+ ostream-encrypt.h \
+ istream-decrypt.h
+
+pkginc_libdir = $(pkgincludedir)
+pkginc_lib_HEADERS = $(headers)
+EXTRA_DIST = \
+ sample-v1.asc \
+ sample-v1_short.asc \
+ sample-v2.asc
+
+test_programs = test-crypto test-stream
+LIBDOVECOT_TEST_DEPS = \
+ ../lib-ssl-iostream/libssl_iostream.la \
+ ../lib-test/libtest.la \
+ ../lib/liblib.la
+
+LIBDOVECOT_TEST = \
+ $(LIBDOVECOT_TEST_DEPS) \
+ $(MODULE_LIBS)
+
+test_crypto_LDADD = $(LIBDOVECOT_TEST)
+test_crypto_DEPENDENCIES = $(LIBDOVECOT_TEST_DEPS)
+@HAVE_WHOLE_ARCHIVE_TRUE@test_crypto_LDFLAGS = -Wl,$(LD_WHOLE_ARCHIVE),../lib-ssl-iostream/.libs/libssl_iostream.a,$(LD_NO_WHOLE_ARCHIVE)
+test_crypto_CFLAGS = $(AM_CPPFLAGS) -DDCRYPT_SRC_DIR=\"$(top_srcdir)/src/lib-dcrypt\"
+test_crypto_SOURCES = $(libdcrypt_la_SOURCES) test-crypto.c
+test_stream_LDADD = $(LIBDOVECOT_TEST)
+test_stream_DEPENDENCIES = $(LIBDOVECOT_TEST_DEPS)
+@HAVE_WHOLE_ARCHIVE_TRUE@test_stream_LDFLAGS = -Wl,$(LD_WHOLE_ARCHIVE),../lib-ssl-iostream/.libs/libssl_iostream.a,$(LD_NO_WHOLE_ARCHIVE)
+test_stream_CFLAGS = $(AM_CPPFLAGS) -DDCRYPT_SRC_DIR=\"$(top_srcdir)/src/lib-dcrypt\"
+test_stream_SOURCES = $(libdcrypt_la_SOURCES) test-stream.c
+all: all-am
+
+.SUFFIXES:
+.SUFFIXES: .c .lo .o .obj
+$(srcdir)/Makefile.in: @MAINTAINER_MODE_TRUE@ $(srcdir)/Makefile.am $(am__configure_deps)
+ @for dep in $?; do \
+ case '$(am__configure_deps)' in \
+ *$$dep*) \
+ ( cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh ) \
+ && { if test -f $@; then exit 0; else break; fi; }; \
+ exit 1;; \
+ esac; \
+ done; \
+ echo ' cd $(top_srcdir) && $(AUTOMAKE) --foreign src/lib-dcrypt/Makefile'; \
+ $(am__cd) $(top_srcdir) && \
+ $(AUTOMAKE) --foreign src/lib-dcrypt/Makefile
+Makefile: $(srcdir)/Makefile.in $(top_builddir)/config.status
+ @case '$?' in \
+ *config.status*) \
+ cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh;; \
+ *) \
+ echo ' cd $(top_builddir) && $(SHELL) ./config.status $(subdir)/$@ $(am__maybe_remake_depfiles)'; \
+ cd $(top_builddir) && $(SHELL) ./config.status $(subdir)/$@ $(am__maybe_remake_depfiles);; \
+ esac;
+
+$(top_builddir)/config.status: $(top_srcdir)/configure $(CONFIG_STATUS_DEPENDENCIES)
+ cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh
+
+$(top_srcdir)/configure: @MAINTAINER_MODE_TRUE@ $(am__configure_deps)
+ cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh
+$(ACLOCAL_M4): @MAINTAINER_MODE_TRUE@ $(am__aclocal_m4_deps)
+ cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh
+$(am__aclocal_m4_deps):
+
+clean-noinstPROGRAMS:
+ @list='$(noinst_PROGRAMS)'; test -n "$$list" || exit 0; \
+ echo " rm -f" $$list; \
+ rm -f $$list || exit $$?; \
+ test -n "$(EXEEXT)" || exit 0; \
+ list=`for p in $$list; do echo "$$p"; done | sed 's/$(EXEEXT)$$//'`; \
+ echo " rm -f" $$list; \
+ rm -f $$list
+
+clean-noinstLTLIBRARIES:
+ -test -z "$(noinst_LTLIBRARIES)" || rm -f $(noinst_LTLIBRARIES)
+ @list='$(noinst_LTLIBRARIES)'; \
+ locs=`for p in $$list; do echo $$p; done | \
+ sed 's|^[^/]*$$|.|; s|/[^/]*$$||; s|$$|/so_locations|' | \
+ sort -u`; \
+ test -z "$$locs" || { \
+ echo rm -f $${locs}; \
+ rm -f $${locs}; \
+ }
+
+install-pkglibLTLIBRARIES: $(pkglib_LTLIBRARIES)
+ @$(NORMAL_INSTALL)
+ @list='$(pkglib_LTLIBRARIES)'; test -n "$(pkglibdir)" || list=; \
+ list2=; for p in $$list; do \
+ if test -f $$p; then \
+ list2="$$list2 $$p"; \
+ else :; fi; \
+ done; \
+ test -z "$$list2" || { \
+ echo " $(MKDIR_P) '$(DESTDIR)$(pkglibdir)'"; \
+ $(MKDIR_P) "$(DESTDIR)$(pkglibdir)" || exit 1; \
+ echo " $(LIBTOOL) $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=install $(INSTALL) $(INSTALL_STRIP_FLAG) $$list2 '$(DESTDIR)$(pkglibdir)'"; \
+ $(LIBTOOL) $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=install $(INSTALL) $(INSTALL_STRIP_FLAG) $$list2 "$(DESTDIR)$(pkglibdir)"; \
+ }
+
+uninstall-pkglibLTLIBRARIES:
+ @$(NORMAL_UNINSTALL)
+ @list='$(pkglib_LTLIBRARIES)'; test -n "$(pkglibdir)" || list=; \
+ for p in $$list; do \
+ $(am__strip_dir) \
+ echo " $(LIBTOOL) $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=uninstall rm -f '$(DESTDIR)$(pkglibdir)/$$f'"; \
+ $(LIBTOOL) $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=uninstall rm -f "$(DESTDIR)$(pkglibdir)/$$f"; \
+ done
+
+clean-pkglibLTLIBRARIES:
+ -test -z "$(pkglib_LTLIBRARIES)" || rm -f $(pkglib_LTLIBRARIES)
+ @list='$(pkglib_LTLIBRARIES)'; \
+ locs=`for p in $$list; do echo $$p; done | \
+ sed 's|^[^/]*$$|.|; s|/[^/]*$$||; s|$$|/so_locations|' | \
+ sort -u`; \
+ test -z "$$locs" || { \
+ echo rm -f $${locs}; \
+ rm -f $${locs}; \
+ }
+
+libdcrypt.la: $(libdcrypt_la_OBJECTS) $(libdcrypt_la_DEPENDENCIES) $(EXTRA_libdcrypt_la_DEPENDENCIES)
+ $(AM_V_CCLD)$(libdcrypt_la_LINK) $(libdcrypt_la_OBJECTS) $(libdcrypt_la_LIBADD) $(LIBS)
+
+libdcrypt_openssl.la: $(libdcrypt_openssl_la_OBJECTS) $(libdcrypt_openssl_la_DEPENDENCIES) $(EXTRA_libdcrypt_openssl_la_DEPENDENCIES)
+ $(AM_V_CCLD)$(libdcrypt_openssl_la_LINK) $(am_libdcrypt_openssl_la_rpath) $(libdcrypt_openssl_la_OBJECTS) $(libdcrypt_openssl_la_LIBADD) $(LIBS)
+
+test-crypto$(EXEEXT): $(test_crypto_OBJECTS) $(test_crypto_DEPENDENCIES) $(EXTRA_test_crypto_DEPENDENCIES)
+ @rm -f test-crypto$(EXEEXT)
+ $(AM_V_CCLD)$(test_crypto_LINK) $(test_crypto_OBJECTS) $(test_crypto_LDADD) $(LIBS)
+
+test-stream$(EXEEXT): $(test_stream_OBJECTS) $(test_stream_DEPENDENCIES) $(EXTRA_test_stream_DEPENDENCIES)
+ @rm -f test-stream$(EXEEXT)
+ $(AM_V_CCLD)$(test_stream_LINK) $(test_stream_OBJECTS) $(test_stream_LDADD) $(LIBS)
+
+mostlyclean-compile:
+ -rm -f *.$(OBJEXT)
+
+distclean-compile:
+ -rm -f *.tab.c
+
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/libdcrypt_la-dcrypt.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/libdcrypt_la-istream-decrypt.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/libdcrypt_la-ostream-encrypt.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/libdcrypt_openssl_la-dcrypt-openssl.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/test_crypto-dcrypt.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/test_crypto-istream-decrypt.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/test_crypto-ostream-encrypt.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/test_crypto-test-crypto.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/test_stream-dcrypt.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/test_stream-istream-decrypt.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/test_stream-ostream-encrypt.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/test_stream-test-stream.Po@am__quote@ # am--include-marker
+
+$(am__depfiles_remade):
+ @$(MKDIR_P) $(@D)
+ @echo '# dummy' >$@-t && $(am__mv) $@-t $@
+
+am--depfiles: $(am__depfiles_remade)
+
+.c.o:
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(COMPILE) -MT $@ -MD -MP -MF $(DEPDIR)/$*.Tpo -c -o $@ $<
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/$*.Tpo $(DEPDIR)/$*.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='$<' object='$@' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(COMPILE) -c -o $@ $<
+
+.c.obj:
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(COMPILE) -MT $@ -MD -MP -MF $(DEPDIR)/$*.Tpo -c -o $@ `$(CYGPATH_W) '$<'`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/$*.Tpo $(DEPDIR)/$*.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='$<' object='$@' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(COMPILE) -c -o $@ `$(CYGPATH_W) '$<'`
+
+.c.lo:
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(LTCOMPILE) -MT $@ -MD -MP -MF $(DEPDIR)/$*.Tpo -c -o $@ $<
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/$*.Tpo $(DEPDIR)/$*.Plo
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='$<' object='$@' libtool=yes @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(LTCOMPILE) -c -o $@ $<
+
+libdcrypt_la-dcrypt.lo: dcrypt.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(LIBTOOL) $(AM_V_lt) --tag=CC $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=compile $(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) $(CPPFLAGS) $(libdcrypt_la_CFLAGS) $(CFLAGS) -MT libdcrypt_la-dcrypt.lo -MD -MP -MF $(DEPDIR)/libdcrypt_la-dcrypt.Tpo -c -o libdcrypt_la-dcrypt.lo `test -f 'dcrypt.c' || echo '$(srcdir)/'`dcrypt.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/libdcrypt_la-dcrypt.Tpo $(DEPDIR)/libdcrypt_la-dcrypt.Plo
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='dcrypt.c' object='libdcrypt_la-dcrypt.lo' libtool=yes @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(LIBTOOL) $(AM_V_lt) --tag=CC $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=compile $(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) $(CPPFLAGS) $(libdcrypt_la_CFLAGS) $(CFLAGS) -c -o libdcrypt_la-dcrypt.lo `test -f 'dcrypt.c' || echo '$(srcdir)/'`dcrypt.c
+
+libdcrypt_la-istream-decrypt.lo: istream-decrypt.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(LIBTOOL) $(AM_V_lt) --tag=CC $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=compile $(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) $(CPPFLAGS) $(libdcrypt_la_CFLAGS) $(CFLAGS) -MT libdcrypt_la-istream-decrypt.lo -MD -MP -MF $(DEPDIR)/libdcrypt_la-istream-decrypt.Tpo -c -o libdcrypt_la-istream-decrypt.lo `test -f 'istream-decrypt.c' || echo '$(srcdir)/'`istream-decrypt.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/libdcrypt_la-istream-decrypt.Tpo $(DEPDIR)/libdcrypt_la-istream-decrypt.Plo
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='istream-decrypt.c' object='libdcrypt_la-istream-decrypt.lo' libtool=yes @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(LIBTOOL) $(AM_V_lt) --tag=CC $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=compile $(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) $(CPPFLAGS) $(libdcrypt_la_CFLAGS) $(CFLAGS) -c -o libdcrypt_la-istream-decrypt.lo `test -f 'istream-decrypt.c' || echo '$(srcdir)/'`istream-decrypt.c
+
+libdcrypt_la-ostream-encrypt.lo: ostream-encrypt.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(LIBTOOL) $(AM_V_lt) --tag=CC $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=compile $(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) $(CPPFLAGS) $(libdcrypt_la_CFLAGS) $(CFLAGS) -MT libdcrypt_la-ostream-encrypt.lo -MD -MP -MF $(DEPDIR)/libdcrypt_la-ostream-encrypt.Tpo -c -o libdcrypt_la-ostream-encrypt.lo `test -f 'ostream-encrypt.c' || echo '$(srcdir)/'`ostream-encrypt.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/libdcrypt_la-ostream-encrypt.Tpo $(DEPDIR)/libdcrypt_la-ostream-encrypt.Plo
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='ostream-encrypt.c' object='libdcrypt_la-ostream-encrypt.lo' libtool=yes @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(LIBTOOL) $(AM_V_lt) --tag=CC $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=compile $(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) $(CPPFLAGS) $(libdcrypt_la_CFLAGS) $(CFLAGS) -c -o libdcrypt_la-ostream-encrypt.lo `test -f 'ostream-encrypt.c' || echo '$(srcdir)/'`ostream-encrypt.c
+
+libdcrypt_openssl_la-dcrypt-openssl.lo: dcrypt-openssl.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(LIBTOOL) $(AM_V_lt) --tag=CC $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=compile $(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) $(CPPFLAGS) $(libdcrypt_openssl_la_CFLAGS) $(CFLAGS) -MT libdcrypt_openssl_la-dcrypt-openssl.lo -MD -MP -MF $(DEPDIR)/libdcrypt_openssl_la-dcrypt-openssl.Tpo -c -o libdcrypt_openssl_la-dcrypt-openssl.lo `test -f 'dcrypt-openssl.c' || echo '$(srcdir)/'`dcrypt-openssl.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/libdcrypt_openssl_la-dcrypt-openssl.Tpo $(DEPDIR)/libdcrypt_openssl_la-dcrypt-openssl.Plo
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='dcrypt-openssl.c' object='libdcrypt_openssl_la-dcrypt-openssl.lo' libtool=yes @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(LIBTOOL) $(AM_V_lt) --tag=CC $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=compile $(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) $(CPPFLAGS) $(libdcrypt_openssl_la_CFLAGS) $(CFLAGS) -c -o libdcrypt_openssl_la-dcrypt-openssl.lo `test -f 'dcrypt-openssl.c' || echo '$(srcdir)/'`dcrypt-openssl.c
+
+test_crypto-dcrypt.o: dcrypt.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) $(CPPFLAGS) $(test_crypto_CFLAGS) $(CFLAGS) -MT test_crypto-dcrypt.o -MD -MP -MF $(DEPDIR)/test_crypto-dcrypt.Tpo -c -o test_crypto-dcrypt.o `test -f 'dcrypt.c' || echo '$(srcdir)/'`dcrypt.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/test_crypto-dcrypt.Tpo $(DEPDIR)/test_crypto-dcrypt.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='dcrypt.c' object='test_crypto-dcrypt.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) $(CPPFLAGS) $(test_crypto_CFLAGS) $(CFLAGS) -c -o test_crypto-dcrypt.o `test -f 'dcrypt.c' || echo '$(srcdir)/'`dcrypt.c
+
+test_crypto-dcrypt.obj: dcrypt.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) $(CPPFLAGS) $(test_crypto_CFLAGS) $(CFLAGS) -MT test_crypto-dcrypt.obj -MD -MP -MF $(DEPDIR)/test_crypto-dcrypt.Tpo -c -o test_crypto-dcrypt.obj `if test -f 'dcrypt.c'; then $(CYGPATH_W) 'dcrypt.c'; else $(CYGPATH_W) '$(srcdir)/dcrypt.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/test_crypto-dcrypt.Tpo $(DEPDIR)/test_crypto-dcrypt.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='dcrypt.c' object='test_crypto-dcrypt.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) $(CPPFLAGS) $(test_crypto_CFLAGS) $(CFLAGS) -c -o test_crypto-dcrypt.obj `if test -f 'dcrypt.c'; then $(CYGPATH_W) 'dcrypt.c'; else $(CYGPATH_W) '$(srcdir)/dcrypt.c'; fi`
+
+test_crypto-istream-decrypt.o: istream-decrypt.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) $(CPPFLAGS) $(test_crypto_CFLAGS) $(CFLAGS) -MT test_crypto-istream-decrypt.o -MD -MP -MF $(DEPDIR)/test_crypto-istream-decrypt.Tpo -c -o test_crypto-istream-decrypt.o `test -f 'istream-decrypt.c' || echo '$(srcdir)/'`istream-decrypt.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/test_crypto-istream-decrypt.Tpo $(DEPDIR)/test_crypto-istream-decrypt.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='istream-decrypt.c' object='test_crypto-istream-decrypt.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) $(CPPFLAGS) $(test_crypto_CFLAGS) $(CFLAGS) -c -o test_crypto-istream-decrypt.o `test -f 'istream-decrypt.c' || echo '$(srcdir)/'`istream-decrypt.c
+
+test_crypto-istream-decrypt.obj: istream-decrypt.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) $(CPPFLAGS) $(test_crypto_CFLAGS) $(CFLAGS) -MT test_crypto-istream-decrypt.obj -MD -MP -MF $(DEPDIR)/test_crypto-istream-decrypt.Tpo -c -o test_crypto-istream-decrypt.obj `if test -f 'istream-decrypt.c'; then $(CYGPATH_W) 'istream-decrypt.c'; else $(CYGPATH_W) '$(srcdir)/istream-decrypt.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/test_crypto-istream-decrypt.Tpo $(DEPDIR)/test_crypto-istream-decrypt.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='istream-decrypt.c' object='test_crypto-istream-decrypt.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) $(CPPFLAGS) $(test_crypto_CFLAGS) $(CFLAGS) -c -o test_crypto-istream-decrypt.obj `if test -f 'istream-decrypt.c'; then $(CYGPATH_W) 'istream-decrypt.c'; else $(CYGPATH_W) '$(srcdir)/istream-decrypt.c'; fi`
+
+test_crypto-ostream-encrypt.o: ostream-encrypt.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) $(CPPFLAGS) $(test_crypto_CFLAGS) $(CFLAGS) -MT test_crypto-ostream-encrypt.o -MD -MP -MF $(DEPDIR)/test_crypto-ostream-encrypt.Tpo -c -o test_crypto-ostream-encrypt.o `test -f 'ostream-encrypt.c' || echo '$(srcdir)/'`ostream-encrypt.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/test_crypto-ostream-encrypt.Tpo $(DEPDIR)/test_crypto-ostream-encrypt.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='ostream-encrypt.c' object='test_crypto-ostream-encrypt.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) $(CPPFLAGS) $(test_crypto_CFLAGS) $(CFLAGS) -c -o test_crypto-ostream-encrypt.o `test -f 'ostream-encrypt.c' || echo '$(srcdir)/'`ostream-encrypt.c
+
+test_crypto-ostream-encrypt.obj: ostream-encrypt.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) $(CPPFLAGS) $(test_crypto_CFLAGS) $(CFLAGS) -MT test_crypto-ostream-encrypt.obj -MD -MP -MF $(DEPDIR)/test_crypto-ostream-encrypt.Tpo -c -o test_crypto-ostream-encrypt.obj `if test -f 'ostream-encrypt.c'; then $(CYGPATH_W) 'ostream-encrypt.c'; else $(CYGPATH_W) '$(srcdir)/ostream-encrypt.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/test_crypto-ostream-encrypt.Tpo $(DEPDIR)/test_crypto-ostream-encrypt.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='ostream-encrypt.c' object='test_crypto-ostream-encrypt.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) $(CPPFLAGS) $(test_crypto_CFLAGS) $(CFLAGS) -c -o test_crypto-ostream-encrypt.obj `if test -f 'ostream-encrypt.c'; then $(CYGPATH_W) 'ostream-encrypt.c'; else $(CYGPATH_W) '$(srcdir)/ostream-encrypt.c'; fi`
+
+test_crypto-test-crypto.o: test-crypto.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) $(CPPFLAGS) $(test_crypto_CFLAGS) $(CFLAGS) -MT test_crypto-test-crypto.o -MD -MP -MF $(DEPDIR)/test_crypto-test-crypto.Tpo -c -o test_crypto-test-crypto.o `test -f 'test-crypto.c' || echo '$(srcdir)/'`test-crypto.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/test_crypto-test-crypto.Tpo $(DEPDIR)/test_crypto-test-crypto.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='test-crypto.c' object='test_crypto-test-crypto.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) $(CPPFLAGS) $(test_crypto_CFLAGS) $(CFLAGS) -c -o test_crypto-test-crypto.o `test -f 'test-crypto.c' || echo '$(srcdir)/'`test-crypto.c
+
+test_crypto-test-crypto.obj: test-crypto.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) $(CPPFLAGS) $(test_crypto_CFLAGS) $(CFLAGS) -MT test_crypto-test-crypto.obj -MD -MP -MF $(DEPDIR)/test_crypto-test-crypto.Tpo -c -o test_crypto-test-crypto.obj `if test -f 'test-crypto.c'; then $(CYGPATH_W) 'test-crypto.c'; else $(CYGPATH_W) '$(srcdir)/test-crypto.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/test_crypto-test-crypto.Tpo $(DEPDIR)/test_crypto-test-crypto.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='test-crypto.c' object='test_crypto-test-crypto.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) $(CPPFLAGS) $(test_crypto_CFLAGS) $(CFLAGS) -c -o test_crypto-test-crypto.obj `if test -f 'test-crypto.c'; then $(CYGPATH_W) 'test-crypto.c'; else $(CYGPATH_W) '$(srcdir)/test-crypto.c'; fi`
+
+test_stream-dcrypt.o: dcrypt.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) $(CPPFLAGS) $(test_stream_CFLAGS) $(CFLAGS) -MT test_stream-dcrypt.o -MD -MP -MF $(DEPDIR)/test_stream-dcrypt.Tpo -c -o test_stream-dcrypt.o `test -f 'dcrypt.c' || echo '$(srcdir)/'`dcrypt.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/test_stream-dcrypt.Tpo $(DEPDIR)/test_stream-dcrypt.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='dcrypt.c' object='test_stream-dcrypt.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) $(CPPFLAGS) $(test_stream_CFLAGS) $(CFLAGS) -c -o test_stream-dcrypt.o `test -f 'dcrypt.c' || echo '$(srcdir)/'`dcrypt.c
+
+test_stream-dcrypt.obj: dcrypt.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) $(CPPFLAGS) $(test_stream_CFLAGS) $(CFLAGS) -MT test_stream-dcrypt.obj -MD -MP -MF $(DEPDIR)/test_stream-dcrypt.Tpo -c -o test_stream-dcrypt.obj `if test -f 'dcrypt.c'; then $(CYGPATH_W) 'dcrypt.c'; else $(CYGPATH_W) '$(srcdir)/dcrypt.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/test_stream-dcrypt.Tpo $(DEPDIR)/test_stream-dcrypt.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='dcrypt.c' object='test_stream-dcrypt.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) $(CPPFLAGS) $(test_stream_CFLAGS) $(CFLAGS) -c -o test_stream-dcrypt.obj `if test -f 'dcrypt.c'; then $(CYGPATH_W) 'dcrypt.c'; else $(CYGPATH_W) '$(srcdir)/dcrypt.c'; fi`
+
+test_stream-istream-decrypt.o: istream-decrypt.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) $(CPPFLAGS) $(test_stream_CFLAGS) $(CFLAGS) -MT test_stream-istream-decrypt.o -MD -MP -MF $(DEPDIR)/test_stream-istream-decrypt.Tpo -c -o test_stream-istream-decrypt.o `test -f 'istream-decrypt.c' || echo '$(srcdir)/'`istream-decrypt.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/test_stream-istream-decrypt.Tpo $(DEPDIR)/test_stream-istream-decrypt.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='istream-decrypt.c' object='test_stream-istream-decrypt.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) $(CPPFLAGS) $(test_stream_CFLAGS) $(CFLAGS) -c -o test_stream-istream-decrypt.o `test -f 'istream-decrypt.c' || echo '$(srcdir)/'`istream-decrypt.c
+
+test_stream-istream-decrypt.obj: istream-decrypt.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) $(CPPFLAGS) $(test_stream_CFLAGS) $(CFLAGS) -MT test_stream-istream-decrypt.obj -MD -MP -MF $(DEPDIR)/test_stream-istream-decrypt.Tpo -c -o test_stream-istream-decrypt.obj `if test -f 'istream-decrypt.c'; then $(CYGPATH_W) 'istream-decrypt.c'; else $(CYGPATH_W) '$(srcdir)/istream-decrypt.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/test_stream-istream-decrypt.Tpo $(DEPDIR)/test_stream-istream-decrypt.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='istream-decrypt.c' object='test_stream-istream-decrypt.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) $(CPPFLAGS) $(test_stream_CFLAGS) $(CFLAGS) -c -o test_stream-istream-decrypt.obj `if test -f 'istream-decrypt.c'; then $(CYGPATH_W) 'istream-decrypt.c'; else $(CYGPATH_W) '$(srcdir)/istream-decrypt.c'; fi`
+
+test_stream-ostream-encrypt.o: ostream-encrypt.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) $(CPPFLAGS) $(test_stream_CFLAGS) $(CFLAGS) -MT test_stream-ostream-encrypt.o -MD -MP -MF $(DEPDIR)/test_stream-ostream-encrypt.Tpo -c -o test_stream-ostream-encrypt.o `test -f 'ostream-encrypt.c' || echo '$(srcdir)/'`ostream-encrypt.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/test_stream-ostream-encrypt.Tpo $(DEPDIR)/test_stream-ostream-encrypt.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='ostream-encrypt.c' object='test_stream-ostream-encrypt.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) $(CPPFLAGS) $(test_stream_CFLAGS) $(CFLAGS) -c -o test_stream-ostream-encrypt.o `test -f 'ostream-encrypt.c' || echo '$(srcdir)/'`ostream-encrypt.c
+
+test_stream-ostream-encrypt.obj: ostream-encrypt.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) $(CPPFLAGS) $(test_stream_CFLAGS) $(CFLAGS) -MT test_stream-ostream-encrypt.obj -MD -MP -MF $(DEPDIR)/test_stream-ostream-encrypt.Tpo -c -o test_stream-ostream-encrypt.obj `if test -f 'ostream-encrypt.c'; then $(CYGPATH_W) 'ostream-encrypt.c'; else $(CYGPATH_W) '$(srcdir)/ostream-encrypt.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/test_stream-ostream-encrypt.Tpo $(DEPDIR)/test_stream-ostream-encrypt.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='ostream-encrypt.c' object='test_stream-ostream-encrypt.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) $(CPPFLAGS) $(test_stream_CFLAGS) $(CFLAGS) -c -o test_stream-ostream-encrypt.obj `if test -f 'ostream-encrypt.c'; then $(CYGPATH_W) 'ostream-encrypt.c'; else $(CYGPATH_W) '$(srcdir)/ostream-encrypt.c'; fi`
+
+test_stream-test-stream.o: test-stream.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) $(CPPFLAGS) $(test_stream_CFLAGS) $(CFLAGS) -MT test_stream-test-stream.o -MD -MP -MF $(DEPDIR)/test_stream-test-stream.Tpo -c -o test_stream-test-stream.o `test -f 'test-stream.c' || echo '$(srcdir)/'`test-stream.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/test_stream-test-stream.Tpo $(DEPDIR)/test_stream-test-stream.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='test-stream.c' object='test_stream-test-stream.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) $(CPPFLAGS) $(test_stream_CFLAGS) $(CFLAGS) -c -o test_stream-test-stream.o `test -f 'test-stream.c' || echo '$(srcdir)/'`test-stream.c
+
+test_stream-test-stream.obj: test-stream.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) $(CPPFLAGS) $(test_stream_CFLAGS) $(CFLAGS) -MT test_stream-test-stream.obj -MD -MP -MF $(DEPDIR)/test_stream-test-stream.Tpo -c -o test_stream-test-stream.obj `if test -f 'test-stream.c'; then $(CYGPATH_W) 'test-stream.c'; else $(CYGPATH_W) '$(srcdir)/test-stream.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/test_stream-test-stream.Tpo $(DEPDIR)/test_stream-test-stream.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='test-stream.c' object='test_stream-test-stream.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) $(CPPFLAGS) $(test_stream_CFLAGS) $(CFLAGS) -c -o test_stream-test-stream.obj `if test -f 'test-stream.c'; then $(CYGPATH_W) 'test-stream.c'; else $(CYGPATH_W) '$(srcdir)/test-stream.c'; fi`
+
+mostlyclean-libtool:
+ -rm -f *.lo
+
+clean-libtool:
+ -rm -rf .libs _libs
+install-pkginc_libHEADERS: $(pkginc_lib_HEADERS)
+ @$(NORMAL_INSTALL)
+ @list='$(pkginc_lib_HEADERS)'; test -n "$(pkginc_libdir)" || list=; \
+ if test -n "$$list"; then \
+ echo " $(MKDIR_P) '$(DESTDIR)$(pkginc_libdir)'"; \
+ $(MKDIR_P) "$(DESTDIR)$(pkginc_libdir)" || exit 1; \
+ fi; \
+ for p in $$list; do \
+ if test -f "$$p"; then d=; else d="$(srcdir)/"; fi; \
+ echo "$$d$$p"; \
+ done | $(am__base_list) | \
+ while read files; do \
+ echo " $(INSTALL_HEADER) $$files '$(DESTDIR)$(pkginc_libdir)'"; \
+ $(INSTALL_HEADER) $$files "$(DESTDIR)$(pkginc_libdir)" || exit $$?; \
+ done
+
+uninstall-pkginc_libHEADERS:
+ @$(NORMAL_UNINSTALL)
+ @list='$(pkginc_lib_HEADERS)'; test -n "$(pkginc_libdir)" || list=; \
+ files=`for p in $$list; do echo $$p; done | sed -e 's|^.*/||'`; \
+ dir='$(DESTDIR)$(pkginc_libdir)'; $(am__uninstall_files_from_dir)
+
+ID: $(am__tagged_files)
+ $(am__define_uniq_tagged_files); mkid -fID $$unique
+tags: tags-am
+TAGS: tags
+
+tags-am: $(TAGS_DEPENDENCIES) $(am__tagged_files)
+ set x; \
+ here=`pwd`; \
+ $(am__define_uniq_tagged_files); \
+ shift; \
+ if test -z "$(ETAGS_ARGS)$$*$$unique"; then :; else \
+ test -n "$$unique" || unique=$$empty_fix; \
+ if test $$# -gt 0; then \
+ $(ETAGS) $(ETAGSFLAGS) $(AM_ETAGSFLAGS) $(ETAGS_ARGS) \
+ "$$@" $$unique; \
+ else \
+ $(ETAGS) $(ETAGSFLAGS) $(AM_ETAGSFLAGS) $(ETAGS_ARGS) \
+ $$unique; \
+ fi; \
+ fi
+ctags: ctags-am
+
+CTAGS: ctags
+ctags-am: $(TAGS_DEPENDENCIES) $(am__tagged_files)
+ $(am__define_uniq_tagged_files); \
+ test -z "$(CTAGS_ARGS)$$unique" \
+ || $(CTAGS) $(CTAGSFLAGS) $(AM_CTAGSFLAGS) $(CTAGS_ARGS) \
+ $$unique
+
+GTAGS:
+ here=`$(am__cd) $(top_builddir) && pwd` \
+ && $(am__cd) $(top_srcdir) \
+ && gtags -i $(GTAGS_ARGS) "$$here"
+cscopelist: cscopelist-am
+
+cscopelist-am: $(am__tagged_files)
+ list='$(am__tagged_files)'; \
+ case "$(srcdir)" in \
+ [\\/]* | ?:[\\/]*) sdir="$(srcdir)" ;; \
+ *) sdir=$(subdir)/$(srcdir) ;; \
+ esac; \
+ for i in $$list; do \
+ if test -f "$$i"; then \
+ echo "$(subdir)/$$i"; \
+ else \
+ echo "$$sdir/$$i"; \
+ fi; \
+ done >> $(top_builddir)/cscope.files
+
+distclean-tags:
+ -rm -f TAGS ID GTAGS GRTAGS GSYMS GPATH tags
+
+distdir: $(BUILT_SOURCES)
+ $(MAKE) $(AM_MAKEFLAGS) distdir-am
+
+distdir-am: $(DISTFILES)
+ @srcdirstrip=`echo "$(srcdir)" | sed 's/[].[^$$\\*]/\\\\&/g'`; \
+ topsrcdirstrip=`echo "$(top_srcdir)" | sed 's/[].[^$$\\*]/\\\\&/g'`; \
+ list='$(DISTFILES)'; \
+ dist_files=`for file in $$list; do echo $$file; done | \
+ sed -e "s|^$$srcdirstrip/||;t" \
+ -e "s|^$$topsrcdirstrip/|$(top_builddir)/|;t"`; \
+ case $$dist_files in \
+ */*) $(MKDIR_P) `echo "$$dist_files" | \
+ sed '/\//!d;s|^|$(distdir)/|;s,/[^/]*$$,,' | \
+ sort -u` ;; \
+ esac; \
+ for file in $$dist_files; do \
+ if test -f $$file || test -d $$file; then d=.; else d=$(srcdir); fi; \
+ if test -d $$d/$$file; then \
+ dir=`echo "/$$file" | sed -e 's,/[^/]*$$,,'`; \
+ if test -d "$(distdir)/$$file"; then \
+ find "$(distdir)/$$file" -type d ! -perm -700 -exec chmod u+rwx {} \;; \
+ fi; \
+ if test -d $(srcdir)/$$file && test $$d != $(srcdir); then \
+ cp -fpR $(srcdir)/$$file "$(distdir)$$dir" || exit 1; \
+ find "$(distdir)/$$file" -type d ! -perm -700 -exec chmod u+rwx {} \;; \
+ fi; \
+ cp -fpR $$d/$$file "$(distdir)$$dir" || exit 1; \
+ else \
+ test -f "$(distdir)/$$file" \
+ || cp -p $$d/$$file "$(distdir)/$$file" \
+ || exit 1; \
+ fi; \
+ done
+check-am: all-am
+ $(MAKE) $(AM_MAKEFLAGS) check-local
+check: check-am
+all-am: Makefile $(PROGRAMS) $(LTLIBRARIES) $(HEADERS)
+installdirs:
+ for dir in "$(DESTDIR)$(pkglibdir)" "$(DESTDIR)$(pkginc_libdir)"; do \
+ test -z "$$dir" || $(MKDIR_P) "$$dir"; \
+ done
+install: install-am
+install-exec: install-exec-am
+install-data: install-data-am
+uninstall: uninstall-am
+
+install-am: all-am
+ @$(MAKE) $(AM_MAKEFLAGS) install-exec-am install-data-am
+
+installcheck: installcheck-am
+install-strip:
+ if test -z '$(STRIP)'; then \
+ $(MAKE) $(AM_MAKEFLAGS) INSTALL_PROGRAM="$(INSTALL_STRIP_PROGRAM)" \
+ install_sh_PROGRAM="$(INSTALL_STRIP_PROGRAM)" INSTALL_STRIP_FLAG=-s \
+ install; \
+ else \
+ $(MAKE) $(AM_MAKEFLAGS) INSTALL_PROGRAM="$(INSTALL_STRIP_PROGRAM)" \
+ install_sh_PROGRAM="$(INSTALL_STRIP_PROGRAM)" INSTALL_STRIP_FLAG=-s \
+ "INSTALL_PROGRAM_ENV=STRIPPROG='$(STRIP)'" install; \
+ fi
+mostlyclean-generic:
+
+clean-generic:
+
+distclean-generic:
+ -test -z "$(CONFIG_CLEAN_FILES)" || rm -f $(CONFIG_CLEAN_FILES)
+ -test . = "$(srcdir)" || test -z "$(CONFIG_CLEAN_VPATH_FILES)" || rm -f $(CONFIG_CLEAN_VPATH_FILES)
+
+maintainer-clean-generic:
+ @echo "This command is intended for maintainers to use"
+ @echo "it deletes files that may require special tools to rebuild."
+clean: clean-am
+
+clean-am: clean-generic clean-libtool clean-noinstLTLIBRARIES \
+ clean-noinstPROGRAMS clean-pkglibLTLIBRARIES mostlyclean-am
+
+distclean: distclean-am
+ -rm -f ./$(DEPDIR)/libdcrypt_la-dcrypt.Plo
+ -rm -f ./$(DEPDIR)/libdcrypt_la-istream-decrypt.Plo
+ -rm -f ./$(DEPDIR)/libdcrypt_la-ostream-encrypt.Plo
+ -rm -f ./$(DEPDIR)/libdcrypt_openssl_la-dcrypt-openssl.Plo
+ -rm -f ./$(DEPDIR)/test_crypto-dcrypt.Po
+ -rm -f ./$(DEPDIR)/test_crypto-istream-decrypt.Po
+ -rm -f ./$(DEPDIR)/test_crypto-ostream-encrypt.Po
+ -rm -f ./$(DEPDIR)/test_crypto-test-crypto.Po
+ -rm -f ./$(DEPDIR)/test_stream-dcrypt.Po
+ -rm -f ./$(DEPDIR)/test_stream-istream-decrypt.Po
+ -rm -f ./$(DEPDIR)/test_stream-ostream-encrypt.Po
+ -rm -f ./$(DEPDIR)/test_stream-test-stream.Po
+ -rm -f Makefile
+distclean-am: clean-am distclean-compile distclean-generic \
+ distclean-tags
+
+dvi: dvi-am
+
+dvi-am:
+
+html: html-am
+
+html-am:
+
+info: info-am
+
+info-am:
+
+install-data-am: install-pkginc_libHEADERS
+
+install-dvi: install-dvi-am
+
+install-dvi-am:
+
+install-exec-am: install-pkglibLTLIBRARIES
+
+install-html: install-html-am
+
+install-html-am:
+
+install-info: install-info-am
+
+install-info-am:
+
+install-man:
+
+install-pdf: install-pdf-am
+
+install-pdf-am:
+
+install-ps: install-ps-am
+
+install-ps-am:
+
+installcheck-am:
+
+maintainer-clean: maintainer-clean-am
+ -rm -f ./$(DEPDIR)/libdcrypt_la-dcrypt.Plo
+ -rm -f ./$(DEPDIR)/libdcrypt_la-istream-decrypt.Plo
+ -rm -f ./$(DEPDIR)/libdcrypt_la-ostream-encrypt.Plo
+ -rm -f ./$(DEPDIR)/libdcrypt_openssl_la-dcrypt-openssl.Plo
+ -rm -f ./$(DEPDIR)/test_crypto-dcrypt.Po
+ -rm -f ./$(DEPDIR)/test_crypto-istream-decrypt.Po
+ -rm -f ./$(DEPDIR)/test_crypto-ostream-encrypt.Po
+ -rm -f ./$(DEPDIR)/test_crypto-test-crypto.Po
+ -rm -f ./$(DEPDIR)/test_stream-dcrypt.Po
+ -rm -f ./$(DEPDIR)/test_stream-istream-decrypt.Po
+ -rm -f ./$(DEPDIR)/test_stream-ostream-encrypt.Po
+ -rm -f ./$(DEPDIR)/test_stream-test-stream.Po
+ -rm -f Makefile
+maintainer-clean-am: distclean-am maintainer-clean-generic
+
+mostlyclean: mostlyclean-am
+
+mostlyclean-am: mostlyclean-compile mostlyclean-generic \
+ mostlyclean-libtool
+
+pdf: pdf-am
+
+pdf-am:
+
+ps: ps-am
+
+ps-am:
+
+uninstall-am: uninstall-pkginc_libHEADERS uninstall-pkglibLTLIBRARIES
+
+.MAKE: check-am install-am install-strip
+
+.PHONY: CTAGS GTAGS TAGS all all-am am--depfiles check check-am \
+ check-local clean clean-generic clean-libtool \
+ clean-noinstLTLIBRARIES clean-noinstPROGRAMS \
+ clean-pkglibLTLIBRARIES cscopelist-am ctags ctags-am distclean \
+ distclean-compile distclean-generic distclean-libtool \
+ distclean-tags distdir dvi dvi-am html html-am info info-am \
+ install install-am install-data install-data-am install-dvi \
+ install-dvi-am install-exec install-exec-am install-html \
+ install-html-am install-info install-info-am install-man \
+ install-pdf install-pdf-am install-pkginc_libHEADERS \
+ install-pkglibLTLIBRARIES install-ps install-ps-am \
+ install-strip installcheck installcheck-am installdirs \
+ maintainer-clean maintainer-clean-generic mostlyclean \
+ mostlyclean-compile mostlyclean-generic mostlyclean-libtool \
+ pdf pdf-am ps ps-am tags tags-am uninstall uninstall-am \
+ uninstall-pkginc_libHEADERS uninstall-pkglibLTLIBRARIES
+
+.PRECIOUS: Makefile
+
+
+check-local:
+ for bin in $(test_programs); do \
+ if ! $(RUN_TEST) ./$$bin; then exit 1; fi; \
+ done
+
+# Tell versions [3.59,3.63) of GNU make to not export all variables.
+# Otherwise a system limit (for SysV at least) may be exceeded.
+.NOEXPORT:
diff --git a/src/lib-dcrypt/dcrypt-iostream.h b/src/lib-dcrypt/dcrypt-iostream.h
new file mode 100644
index 0000000..ec78978
--- /dev/null
+++ b/src/lib-dcrypt/dcrypt-iostream.h
@@ -0,0 +1,16 @@
+#ifndef DCRYPT_IOSTREAM_H
+#define DCRYPT_IOSTREAM_H 1
+
+static const unsigned char IOSTREAM_CRYPT_MAGIC[] =
+ {'C','R','Y','P','T','E','D','\x03','\x07'};
+#define IOSTREAM_CRYPT_VERSION 2
+#define IOSTREAM_TAG_SIZE 16
+
+enum io_stream_encrypt_flags {
+ IO_STREAM_ENC_INTEGRITY_HMAC = 0x1,
+ IO_STREAM_ENC_INTEGRITY_AEAD = 0x2,
+ IO_STREAM_ENC_INTEGRITY_NONE = 0x4,
+ IO_STREAM_ENC_VERSION_1 = 0x8,
+};
+
+#endif
diff --git a/src/lib-dcrypt/dcrypt-openssl.c b/src/lib-dcrypt/dcrypt-openssl.c
new file mode 100644
index 0000000..1cbe352
--- /dev/null
+++ b/src/lib-dcrypt/dcrypt-openssl.c
@@ -0,0 +1,3807 @@
+/* Copyright (c) 2016-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "buffer.h"
+#include "base64.h"
+#include "str.h"
+#include "hex-binary.h"
+#include "safe-memset.h"
+#include "randgen.h"
+#include "array.h"
+#include "module-dir.h"
+#include "istream.h"
+#include "json-tree.h"
+#include "dovecot-openssl-common.h"
+#include <openssl/evp.h>
+#include <openssl/sha.h>
+#include <openssl/err.h>
+#include <openssl/rsa.h>
+#include <openssl/ec.h>
+#include <openssl/bio.h>
+#include <openssl/pem.h>
+#include <openssl/x509.h>
+#include <openssl/engine.h>
+#include <openssl/hmac.h>
+#include <openssl/objects.h>
+#include <openssl/bn.h>
+#include "dcrypt.h"
+#include "dcrypt-private.h"
+
+/**
+
+ key format documentation:
+ =========================
+
+ v1 key
+ ------
+ algo id = openssl NID
+ enctype = 0 = none, 1 = ecdhe, 2 = password
+ key id = sha256(hex encoded public point)
+
+ public key
+ ----------
+ 1<tab>algo id<tab>public point
+
+ private key
+ -----------
+ - enctype none
+ 1<tab>algo id<tab>0<tab>private point<tab>key id
+
+ - enctype ecdh (algorithm AES-256-CTR, key = SHA256(shared secret), IV = \0\0\0...)
+ 1<tab>algo id<tab>1<tab>private point<tab>ephemeral public key<tab>encryption key id<tab>key id
+
+ - enctype password (algorithm AES-256-CTR, key = PBKDF2(SHA1, 16, password, salt), IV = \0\0\0...)
+ 1<tab>algo id<tab>2<tab>private point<tab>salt<tab>key id
+
+ v2 key
+ ------
+ algo oid = ASN1 OID of key algorithm (RSA or EC curve)
+ enctype = 0 = none, 1 = ecdhe, 2 = password
+ key id = SHA256(i2d_PUBKEY)
+
+ public key
+ ----------
+ 2<tab>HEX(i2d_PUBKEY)<tab>key id
+
+ - enctype none
+ 2<tab>key algo oid<tab>0<tab>(RSA = i2d_PrivateKey, EC=Private Point)<tab>key id
+
+ - enctype ecdh, key,iv = PBKDF2(hash algo, rounds, shared secret, salt)
+ 2<tab>key algo oid<tab>1<tab>symmetric algo name<tab>salt<tab>hash algo<tab>rounds<tab>E(RSA = i2d_PrivateKey, EC=Private Point)<tab>ephemeral public key<tab>encryption key id<tab>key id
+
+ - enctype password, key,iv = PBKDF2(hash algo, rounds, password, salt)
+ 2<tab>key algo oid<tab>1<tab>symmetric algo name<tab>salt<tab>hash algo<tab>rounds<tab>E(RSA = i2d_PrivateKey, EC=Private Point)<tab>key id
+**/
+
+#ifndef HAVE_EVP_PKEY_get0
+#define EVP_PKEY_get0_EC_KEY(x) x->pkey.ec
+#define EVP_PKEY_get0_RSA(x) x->pkey.rsa
+#endif
+
+#ifndef HAVE_OBJ_LENGTH
+#define OBJ_length(o) ((o)->length)
+#endif
+
+#ifndef HAVE_EVP_MD_CTX_NEW
+# define EVP_MD_CTX_new() EVP_MD_CTX_create()
+# define EVP_MD_CTX_free(ctx) EVP_MD_CTX_destroy(ctx)
+#endif
+
+#ifndef HAVE_HMAC_CTX_NEW
+# define HMAC_Init_ex(ctx, key, key_len, md, impl) \
+ HMAC_Init_ex(&(ctx), key, key_len, md, impl)
+# define HMAC_Update(ctx, data, len) HMAC_Update(&(ctx), data, len)
+# define HMAC_Final(ctx, md, len) HMAC_Final(&(ctx), md, len)
+# define HMAC_CTX_free(ctx) HMAC_cleanup(&(ctx))
+#else
+# define HMAC_CTX_free(ctx) \
+ STMT_START { HMAC_CTX_free(ctx); (ctx) = NULL; } STMT_END
+#endif
+
+/* Not always present */
+#ifndef HAVE_BN_SECURE_NEW
+# define BN_secure_new BN_new
+#endif
+
+/* openssl manual says this is OK */
+#define OID_TEXT_MAX_LEN 80
+
+#define t_base64url_decode_str(x) t_base64url_decode_str(BASE64_DECODE_FLAG_IGNORE_PADDING, (x))
+
+struct dcrypt_context_symmetric {
+ pool_t pool;
+ const EVP_CIPHER *cipher;
+ EVP_CIPHER_CTX *ctx;
+ unsigned char *key;
+ unsigned char *iv;
+ unsigned char *aad;
+ size_t aad_len;
+ unsigned char *tag;
+ size_t tag_len;
+ int padding;
+ int mode;
+};
+
+struct dcrypt_context_hmac {
+ pool_t pool;
+ const EVP_MD *md;
+#ifdef HAVE_HMAC_CTX_NEW
+ HMAC_CTX *ctx;
+#else
+ HMAC_CTX ctx;
+#endif
+ unsigned char *key;
+ size_t klen;
+};
+
+struct dcrypt_public_key {
+ EVP_PKEY *key;
+ unsigned int ref;
+ enum dcrypt_key_usage usage;
+ char *key_id;
+};
+
+struct dcrypt_private_key {
+ EVP_PKEY *key;
+ unsigned int ref;
+ enum dcrypt_key_usage usage;
+ char *key_id;
+};
+
+#define DCRYPT_SET_ERROR(error) STMT_START { if (error_r != NULL) *error_r = (error); } STMT_END
+
+static bool
+dcrypt_openssl_public_key_id(struct dcrypt_public_key *key,
+ const char *algorithm, buffer_t *result,
+ const char **error_r);
+static bool
+dcrypt_openssl_public_key_id_old(struct dcrypt_public_key *key,
+ buffer_t *result, const char **error_r);
+static bool
+dcrypt_openssl_private_key_id(struct dcrypt_private_key *key,
+ const char *algorithm, buffer_t *result,
+ const char **error_r);
+static bool
+dcrypt_openssl_private_key_id_old(struct dcrypt_private_key *key,
+ buffer_t *result, const char **error_r);
+static void
+dcrypt_openssl_private_to_public_key(struct dcrypt_private_key *priv_key,
+ struct dcrypt_public_key **pub_key_r);
+static void
+dcrypt_openssl_unref_private_key(struct dcrypt_private_key **key);
+static void
+dcrypt_openssl_unref_public_key(struct dcrypt_public_key **key);
+static bool
+dcrypt_openssl_rsa_decrypt(struct dcrypt_private_key *key,
+ const unsigned char *data, size_t data_len,
+ buffer_t *result, enum dcrypt_padding padding,
+ const char **error_r);
+static bool
+dcrypt_openssl_key_string_get_info(const char *key_data,
+ enum dcrypt_key_format *format_r, enum dcrypt_key_version *version_r,
+ enum dcrypt_key_kind *kind_r,
+ enum dcrypt_key_encryption_type *encryption_type_r,
+ const char **encryption_key_hash_r, const char **key_hash_r,
+ const char **error_r);
+
+#ifndef HAVE_EC_GROUP_order_bits
+static int EC_GROUP_order_bits(const EC_GROUP *grp)
+{
+ int bits;
+ BIGNUM *bn = BN_new();
+ (void)EC_GROUP_get_order(grp, bn, NULL);
+ bits = BN_num_bits(bn);
+ BN_free(bn);
+ return bits;
+}
+#endif
+
+static bool dcrypt_openssl_error(const char **error_r)
+{
+ unsigned long ec;
+
+ if (error_r == NULL) {
+ /* caller is not really interested */
+ return FALSE;
+ }
+
+ ec = ERR_get_error();
+ DCRYPT_SET_ERROR(t_strdup_printf("%s", ERR_error_string(ec, NULL)));
+ return FALSE;
+}
+
+static int
+dcrypt_openssl_padding_mode(enum dcrypt_padding padding,
+ bool sig, const char **error_r)
+{
+ switch (padding) {
+ case DCRYPT_PADDING_DEFAULT:
+ if (sig) return RSA_PKCS1_PSS_PADDING;
+ else return RSA_PKCS1_OAEP_PADDING;
+ case DCRYPT_PADDING_RSA_PKCS1_OAEP:
+ return RSA_PKCS1_OAEP_PADDING;
+ case DCRYPT_PADDING_RSA_PKCS1_PSS:
+ return RSA_PKCS1_PSS_PADDING;
+ case DCRYPT_PADDING_RSA_PKCS1:
+ return RSA_PKCS1_PADDING;
+ case DCRYPT_PADDING_RSA_NO:
+ return RSA_NO_PADDING;
+ default:
+ DCRYPT_SET_ERROR("Unsupported padding mode");
+ return -1;
+ }
+ i_unreached();
+}
+
+static bool dcrypt_openssl_initialize(const struct dcrypt_settings *set,
+ const char **error_r)
+{
+ if (set->crypto_device != NULL && set->crypto_device[0] != '\0') {
+ if (dovecot_openssl_common_global_set_engine(
+ set->crypto_device, error_r) <= 0)
+ return FALSE;
+ }
+ return TRUE;
+}
+
+/* legacy function for old formats that generates
+ hex encoded point from EC public key
+ */
+static char *ec_key_get_pub_point_hex(const EC_KEY *key)
+{
+ const EC_POINT *p;
+ const EC_GROUP *g;
+
+ p = EC_KEY_get0_public_key(key);
+ g = EC_KEY_get0_group(key);
+ return EC_POINT_point2hex(g, p, POINT_CONVERSION_COMPRESSED, NULL);
+}
+
+static bool
+dcrypt_openssl_ctx_sym_create(const char *algorithm, enum dcrypt_sym_mode mode,
+ struct dcrypt_context_symmetric **ctx_r,
+ const char **error_r)
+{
+ struct dcrypt_context_symmetric *ctx;
+ pool_t pool;
+ const EVP_CIPHER *cipher;
+
+ cipher = EVP_get_cipherbyname(algorithm);
+ if (cipher == NULL) {
+ DCRYPT_SET_ERROR(t_strdup_printf("Invalid cipher %s",
+ algorithm));
+ return FALSE;
+ }
+
+ /* allocate context */
+ pool = pool_alloconly_create("dcrypt openssl", 1024);
+ ctx = p_new(pool, struct dcrypt_context_symmetric, 1);
+ ctx->pool = pool;
+ ctx->cipher = cipher;
+ ctx->padding = 1;
+ ctx->mode = (mode == DCRYPT_MODE_ENCRYPT ? 1 : 0);
+ *ctx_r = ctx;
+ return TRUE;
+}
+
+static void
+dcrypt_openssl_ctx_sym_destroy(struct dcrypt_context_symmetric **ctx)
+{
+ pool_t pool = (*ctx)->pool;
+
+ if ((*ctx)->ctx != NULL)
+ EVP_CIPHER_CTX_free((*ctx)->ctx);
+ pool_unref(&pool);
+ *ctx = NULL;
+}
+
+static void
+dcrypt_openssl_ctx_sym_set_key(struct dcrypt_context_symmetric *ctx,
+ const unsigned char *key, size_t key_len)
+{
+ if (ctx->key != NULL)
+ p_free(ctx->pool, ctx->key);
+ ctx->key = p_malloc(ctx->pool, EVP_CIPHER_key_length(ctx->cipher));
+ memcpy(ctx->key, key, I_MIN(key_len,
+ (size_t)EVP_CIPHER_key_length(ctx->cipher)));
+}
+
+static void
+dcrypt_openssl_ctx_sym_set_iv(struct dcrypt_context_symmetric *ctx,
+ const unsigned char *iv, size_t iv_len)
+{
+ if(ctx->iv != NULL)
+ p_free(ctx->pool, ctx->iv);
+
+ ctx->iv = p_malloc(ctx->pool, EVP_CIPHER_iv_length(ctx->cipher));
+ memcpy(ctx->iv, iv, I_MIN(iv_len,
+ (size_t)EVP_CIPHER_iv_length(ctx->cipher)));
+}
+
+static void
+dcrypt_openssl_ctx_sym_set_key_iv_random(struct dcrypt_context_symmetric *ctx)
+{
+ if(ctx->key != NULL)
+ p_free(ctx->pool, ctx->key);
+ if(ctx->iv != NULL)
+ p_free(ctx->pool, ctx->iv);
+
+ ctx->key = p_malloc(ctx->pool, EVP_CIPHER_key_length(ctx->cipher));
+ random_fill(ctx->key, EVP_CIPHER_key_length(ctx->cipher));
+ ctx->iv = p_malloc(ctx->pool, EVP_CIPHER_iv_length(ctx->cipher));
+ random_fill(ctx->iv, EVP_CIPHER_iv_length(ctx->cipher));
+}
+
+static void
+dcrypt_openssl_ctx_sym_set_padding(struct dcrypt_context_symmetric *ctx,
+ bool padding)
+{
+ ctx->padding = (padding?1:0);
+}
+
+static bool
+dcrypt_openssl_ctx_sym_get_key(struct dcrypt_context_symmetric *ctx,
+ buffer_t *key)
+{
+ if(ctx->key == NULL)
+ return FALSE;
+
+ buffer_append(key, ctx->key, EVP_CIPHER_key_length(ctx->cipher));
+ return TRUE;
+}
+
+static bool
+dcrypt_openssl_ctx_sym_get_iv(struct dcrypt_context_symmetric *ctx,
+ buffer_t *iv)
+{
+ if(ctx->iv == NULL)
+ return FALSE;
+
+ buffer_append(iv, ctx->iv, EVP_CIPHER_iv_length(ctx->cipher));
+ return TRUE;
+}
+
+static void
+dcrypt_openssl_ctx_sym_set_aad(struct dcrypt_context_symmetric *ctx,
+ const unsigned char *aad, size_t aad_len)
+{
+ if (ctx->aad != NULL)
+ p_free(ctx->pool, ctx->aad);
+
+ /* allow empty aad */
+ ctx->aad = p_malloc(ctx->pool, I_MAX(1,aad_len));
+ memcpy(ctx->aad, aad, aad_len);
+ ctx->aad_len = aad_len;
+}
+
+static bool
+dcrypt_openssl_ctx_sym_get_aad(struct dcrypt_context_symmetric *ctx,
+ buffer_t *aad)
+{
+ if (ctx->aad == NULL)
+ return FALSE;
+
+ buffer_append(aad, ctx->aad, ctx->aad_len);
+ return TRUE;
+}
+
+static void
+dcrypt_openssl_ctx_sym_set_tag(struct dcrypt_context_symmetric *ctx,
+ const unsigned char *tag, size_t tag_len)
+{
+ if (ctx->tag != NULL)
+ p_free(ctx->pool, ctx->tag);
+
+ /* unlike aad, tag cannot be empty */
+ ctx->tag = p_malloc(ctx->pool, tag_len);
+ memcpy(ctx->tag, tag, tag_len);
+ ctx->tag_len = tag_len;
+}
+
+static bool
+dcrypt_openssl_ctx_sym_get_tag(struct dcrypt_context_symmetric *ctx,
+ buffer_t *tag)
+{
+ if (ctx->tag == NULL)
+ return FALSE;
+
+ buffer_append(tag, ctx->tag, ctx->tag_len);
+ return TRUE;
+}
+
+static unsigned int
+dcrypt_openssl_ctx_sym_get_key_length(struct dcrypt_context_symmetric *ctx)
+{
+ return EVP_CIPHER_key_length(ctx->cipher);
+}
+
+static unsigned int
+dcrypt_openssl_ctx_sym_get_iv_length(struct dcrypt_context_symmetric *ctx)
+{
+ return EVP_CIPHER_iv_length(ctx->cipher);
+}
+
+static unsigned int
+dcrypt_openssl_ctx_sym_get_block_size(struct dcrypt_context_symmetric *ctx)
+{
+ return EVP_CIPHER_block_size(ctx->cipher);
+}
+
+static bool
+dcrypt_openssl_ctx_sym_init(struct dcrypt_context_symmetric *ctx,
+ const char **error_r)
+{
+ int ec;
+ int len;
+
+ i_assert(ctx->key != NULL);
+ i_assert(ctx->iv != NULL);
+ i_assert(ctx->ctx == NULL);
+
+ if((ctx->ctx = EVP_CIPHER_CTX_new()) == NULL)
+ return dcrypt_openssl_error(error_r);
+
+ ec = EVP_CipherInit_ex(ctx->ctx, ctx->cipher, NULL,
+ ctx->key, ctx->iv, ctx->mode);
+ if (ec != 1)
+ return dcrypt_openssl_error(error_r);
+
+ EVP_CIPHER_CTX_set_padding(ctx->ctx, ctx->padding);
+ len = 0;
+ if (ctx->aad != NULL) {
+ ec = EVP_CipherUpdate(ctx->ctx, NULL, &len,
+ ctx->aad, ctx->aad_len);
+ }
+ if (ec != 1)
+ return dcrypt_openssl_error(error_r);
+ return TRUE;
+}
+
+static bool
+dcrypt_openssl_ctx_sym_update(struct dcrypt_context_symmetric *ctx,
+ const unsigned char *data, size_t data_len,
+ buffer_t *result, const char **error_r)
+{
+ const size_t block_size = (size_t)EVP_CIPHER_block_size(ctx->cipher);
+ size_t buf_used = result->used;
+ unsigned char *buf;
+ int outl;
+
+ i_assert(ctx->ctx != NULL);
+
+ /* From `man 3 evp_cipherupdate`:
+
+ EVP_EncryptUpdate() encrypts inl bytes from the buffer in and writes
+ the encrypted version to out. This function can be called multiple
+ times to encrypt successive blocks of data. The amount of data
+ written depends on the block alignment of the encrypted data: as a
+ result the amount of data written may be anything from zero bytes to
+ (inl + cipher_block_size - 1) so out should contain sufficient room.
+ The actual number of bytes written is placed in outl.
+ */
+
+ buf = buffer_append_space_unsafe(result, data_len + block_size);
+ outl = 0;
+ if (EVP_CipherUpdate
+ (ctx->ctx, buf, &outl, data, data_len) != 1)
+ return dcrypt_openssl_error(error_r);
+ buffer_set_used_size(result, buf_used + outl);
+ return TRUE;
+}
+
+static bool
+dcrypt_openssl_ctx_sym_final(struct dcrypt_context_symmetric *ctx,
+ buffer_t *result, const char **error_r)
+{
+ const size_t block_size = (size_t)EVP_CIPHER_block_size(ctx->cipher);
+ size_t buf_used = result->used;
+ unsigned char *buf;
+ int outl;
+ int ec;
+
+ i_assert(ctx->ctx != NULL);
+
+ /* From `man 3 evp_cipherupdate`:
+
+ If padding is enabled (the default) then EVP_EncryptFinal_ex()
+ encrypts the "final" data, that is any data that remains in a partial
+ block. It uses standard block padding (aka PKCS padding). The
+ encrypted final data is written to out which should have sufficient
+ space for one cipher block. The number of bytes written is placed in
+ outl. After this function is called the encryption operation is
+ finished and no further calls to EVP_EncryptUpdate() should be made.
+ */
+
+ buf = buffer_append_space_unsafe(result, block_size);
+ outl = 0;
+
+ /* when **DECRYPTING** set expected tag */
+ if (ctx->mode == 0 && ctx->tag != NULL) {
+ ec = EVP_CIPHER_CTX_ctrl(ctx->ctx, EVP_CTRL_GCM_SET_TAG,
+ ctx->tag_len, ctx->tag);
+ } else {
+ ec = 1;
+ }
+
+ if (ec == 1)
+ ec = EVP_CipherFinal_ex(ctx->ctx, buf, &outl);
+
+ if (ec == 1) {
+ buffer_set_used_size(result, buf_used + outl);
+ /* when **ENCRYPTING** recover tag */
+ if (ctx->mode == 1 && ctx->aad != NULL) {
+ /* tag should be NULL here */
+ i_assert(ctx->tag == NULL);
+ /* openssl claims taglen is always 16, go figure .. */
+ ctx->tag = p_malloc(ctx->pool, EVP_GCM_TLS_TAG_LEN);
+ ec = EVP_CIPHER_CTX_ctrl(ctx->ctx, EVP_CTRL_GCM_GET_TAG,
+ EVP_GCM_TLS_TAG_LEN, ctx->tag);
+ ctx->tag_len = EVP_GCM_TLS_TAG_LEN;
+ }
+ }
+
+ if (ec == 0)
+ DCRYPT_SET_ERROR("data authentication failed");
+ else if (ec < 0)
+ dcrypt_openssl_error(error_r);
+
+ EVP_CIPHER_CTX_free(ctx->ctx);
+ ctx->ctx = NULL;
+
+ return (ec == 1);
+}
+
+static bool
+dcrypt_openssl_ctx_hmac_create(const char *algorithm,
+ struct dcrypt_context_hmac **ctx_r,
+ const char **error_r)
+{
+ struct dcrypt_context_hmac *ctx;
+ pool_t pool;
+ const EVP_MD *md;
+
+ md = EVP_get_digestbyname(algorithm);
+ if(md == NULL) {
+ DCRYPT_SET_ERROR(t_strdup_printf("Invalid digest %s",
+ algorithm));
+ return FALSE;
+ }
+
+ /* allocate context */
+ pool = pool_alloconly_create("dcrypt openssl", 1024);
+ ctx = p_new(pool, struct dcrypt_context_hmac, 1);
+ ctx->pool = pool;
+ ctx->md = md;
+ *ctx_r = ctx;
+ return TRUE;
+}
+
+static void
+dcrypt_openssl_ctx_hmac_destroy(struct dcrypt_context_hmac **ctx)
+{
+ pool_t pool = (*ctx)->pool;
+ HMAC_CTX_free((*ctx)->ctx);
+ pool_unref(&pool);
+ *ctx = NULL;
+}
+
+static void
+dcrypt_openssl_ctx_hmac_set_key(struct dcrypt_context_hmac *ctx,
+ const unsigned char *key, size_t key_len)
+{
+ if (ctx->key != NULL)
+ p_free(ctx->pool, ctx->key);
+
+ ctx->klen = I_MIN(key_len, HMAC_MAX_MD_CBLOCK);
+ ctx->key = p_malloc(ctx->pool, ctx->klen);
+ memcpy(ctx->key, key, ctx->klen);
+}
+
+static bool
+dcrypt_openssl_ctx_hmac_get_key(struct dcrypt_context_hmac *ctx, buffer_t *key)
+{
+ if (ctx->key == NULL)
+ return FALSE;
+ buffer_append(key, ctx->key, ctx->klen);
+ return TRUE;
+}
+
+static void
+dcrypt_openssl_ctx_hmac_set_key_random(struct dcrypt_context_hmac *ctx)
+{
+ ctx->klen = HMAC_MAX_MD_CBLOCK;
+ ctx->key = p_malloc(ctx->pool, ctx->klen);
+ random_fill(ctx->key, ctx->klen);
+}
+
+static unsigned int
+dcrypt_openssl_ctx_hmac_get_digest_length(struct dcrypt_context_hmac *ctx)
+{
+ return EVP_MD_size(ctx->md);
+}
+
+static bool
+dcrypt_openssl_ctx_hmac_init(struct dcrypt_context_hmac *ctx,
+ const char **error_r)
+{
+ int ec;
+
+ i_assert(ctx->md != NULL);
+#ifdef HAVE_HMAC_CTX_NEW
+ ctx->ctx = HMAC_CTX_new();
+ if (ctx->ctx == NULL)
+ return dcrypt_openssl_error(error_r);
+#endif
+ ec = HMAC_Init_ex(ctx->ctx, ctx->key, ctx->klen, ctx->md, NULL);
+ if (ec != 1)
+ return dcrypt_openssl_error(error_r);
+ return TRUE;
+}
+
+static bool
+dcrypt_openssl_ctx_hmac_update(struct dcrypt_context_hmac *ctx,
+ const unsigned char *data, size_t data_len,
+ const char **error_r)
+{
+ int ec;
+
+ ec = HMAC_Update(ctx->ctx, data, data_len);
+ if (ec != 1)
+ return dcrypt_openssl_error(error_r);
+ return TRUE;
+}
+
+static bool
+dcrypt_openssl_ctx_hmac_final(struct dcrypt_context_hmac *ctx, buffer_t *result,
+ const char **error_r)
+{
+ int ec;
+ unsigned char buf[HMAC_MAX_MD_CBLOCK];
+ unsigned int outl;
+
+ ec = HMAC_Final(ctx->ctx, buf, &outl);
+ HMAC_CTX_free(ctx->ctx);
+ if (ec == 1)
+ buffer_append(result, buf, outl);
+ else
+ return dcrypt_openssl_error(error_r);
+ return TRUE;
+}
+
+static bool
+dcrypt_openssl_generate_ec_key(int nid, EVP_PKEY **key, const char **error_r)
+{
+ EVP_PKEY_CTX *pctx;
+ EVP_PKEY_CTX *ctx;
+ EVP_PKEY *params = NULL;
+
+ /* generate parameters for EC */
+ pctx = EVP_PKEY_CTX_new_id(EVP_PKEY_EC, NULL);
+ if (pctx == NULL ||
+ EVP_PKEY_paramgen_init(pctx) < 1 ||
+ EVP_PKEY_CTX_set_ec_paramgen_curve_nid(pctx, nid) < 1 ||
+ EVP_PKEY_paramgen(pctx, &params) < 1)
+ {
+ dcrypt_openssl_error(error_r);
+ EVP_PKEY_CTX_free(pctx);
+ return FALSE;
+ }
+
+ /* generate key from parameters */
+ ctx = EVP_PKEY_CTX_new(params, NULL);
+ if (ctx == NULL ||
+ EVP_PKEY_keygen_init(ctx) < 1 ||
+ EVP_PKEY_keygen(ctx, key) < 1)
+ {
+ dcrypt_openssl_error(error_r);
+ EVP_PKEY_free(params);
+ EVP_PKEY_CTX_free(pctx);
+ EVP_PKEY_CTX_free(ctx);
+ return FALSE;
+ }
+
+ EVP_PKEY_free(params);
+ EVP_PKEY_CTX_free(pctx);
+ EVP_PKEY_CTX_free(ctx);
+ EC_KEY_set_asn1_flag(EVP_PKEY_get0_EC_KEY((*key)),
+ OPENSSL_EC_NAMED_CURVE);
+ return TRUE;
+}
+
+static bool
+dcrypt_openssl_generate_rsa_key(int bits, EVP_PKEY **key, const char **error_r)
+{
+ i_assert(bits >= 256);
+ int ec = 0;
+
+ EVP_PKEY_CTX *ctx;
+ ctx = EVP_PKEY_CTX_new_id(EVP_PKEY_RSA, NULL);
+ if (ctx == NULL ||
+ EVP_PKEY_keygen_init(ctx) < 1 ||
+ EVP_PKEY_CTX_set_rsa_keygen_bits(ctx, bits) < 1 ||
+ EVP_PKEY_keygen(ctx, key) < 1) {
+ dcrypt_openssl_error(error_r);
+ ec = -1;
+ }
+
+ EVP_PKEY_CTX_free(ctx);
+ return ec == 0;
+}
+
+static bool
+dcrypt_openssl_ecdh_derive_secret(struct dcrypt_private_key *priv_key,
+ struct dcrypt_public_key *pub_key,
+ buffer_t *shared_secret,
+ const char **error_r)
+{
+ /* initialize */
+ EVP_PKEY_CTX *pctx = EVP_PKEY_CTX_new(priv_key->key, NULL);
+ if (pctx == NULL ||
+ EVP_PKEY_derive_init(pctx) != 1 ||
+ EVP_PKEY_derive_set_peer(pctx, pub_key->key) != 1) {
+ EVP_PKEY_CTX_free(pctx);
+ return dcrypt_openssl_error(error_r);
+ }
+
+ /* derive */
+ size_t len;
+ if (EVP_PKEY_derive(pctx, NULL, &len) != 1) {
+ EVP_PKEY_CTX_free(pctx);
+ return dcrypt_openssl_error(error_r);
+ }
+ unsigned char buf[len];
+ if (EVP_PKEY_derive(pctx, buf, &len) != 1) {
+ EVP_PKEY_CTX_free(pctx);
+ return dcrypt_openssl_error(error_r);
+ }
+
+ EVP_PKEY_CTX_free(pctx);
+ buffer_append(shared_secret, buf, len);
+
+ return TRUE;
+}
+
+static bool
+dcrypt_openssl_ecdh_derive_secret_local(struct dcrypt_private_key *local_key,
+ buffer_t *R, buffer_t *S,
+ const char **error_r)
+{
+ bool ret;
+ i_assert(local_key != NULL && local_key->key != NULL);
+
+ EVP_PKEY *local = local_key->key;
+ BN_CTX *bn_ctx = BN_CTX_new();
+ if (bn_ctx == NULL)
+ return dcrypt_openssl_error(error_r);
+
+ const EC_GROUP *grp = EC_KEY_get0_group(EVP_PKEY_get0_EC_KEY(local));
+ EC_POINT *pub = EC_POINT_new(grp);
+
+ /* convert ephemeral key data EC point */
+ if (pub == NULL ||
+ EC_POINT_oct2point(grp, pub, R->data, R->used, bn_ctx) != 1)
+ {
+ EC_POINT_free(pub);
+ BN_CTX_free(bn_ctx);
+ return dcrypt_openssl_error(error_r);
+ }
+ EC_KEY *ec_key = EC_KEY_new();
+
+ /* convert point to public key */
+ int ec = 0;
+ if (ec_key == NULL ||
+ EC_KEY_set_group(ec_key, grp) != 1 ||
+ EC_KEY_set_public_key(ec_key, pub) != 1)
+ ec = -1;
+ else
+ EC_POINT_free(pub);
+ BN_CTX_free(bn_ctx);
+
+ /* make sure it looks like a valid key */
+ if (ec == -1 || EC_KEY_check_key(ec_key) != 1) {
+ EC_KEY_free(ec_key);
+ return dcrypt_openssl_error(error_r);
+ }
+
+ EVP_PKEY *peer = EVP_PKEY_new();
+ if (peer == NULL) {
+ EC_KEY_free(ec_key);
+ return dcrypt_openssl_error(error_r);
+ }
+ EVP_PKEY_set1_EC_KEY(peer, ec_key);
+ EC_KEY_free(ec_key);
+
+ struct dcrypt_public_key pub_key;
+ i_zero(&pub_key);
+ pub_key.key = peer;
+
+ ret = dcrypt_openssl_ecdh_derive_secret(local_key, &pub_key, S, error_r);
+
+ EVP_PKEY_free(peer);
+ return ret;
+}
+
+static bool
+dcrypt_openssl_ecdh_derive_secret_peer(struct dcrypt_public_key *peer_key,
+ buffer_t *R, buffer_t *S,
+ const char **error_r)
+{
+ i_assert(peer_key != NULL && peer_key->key != NULL);
+ bool ret;
+
+ /* ensure peer_key is EC key */
+ EVP_PKEY *local = NULL;
+ EVP_PKEY *peer = peer_key->key;
+ if (EVP_PKEY_base_id(peer) != EVP_PKEY_EC) {
+ DCRYPT_SET_ERROR("Only ECC key can be used");
+ return FALSE;
+ }
+
+ /* generate another key from same group */
+ int nid = EC_GROUP_get_curve_name(
+ EC_KEY_get0_group(EVP_PKEY_get0_EC_KEY(peer)));
+ if (!dcrypt_openssl_generate_ec_key(nid, &local, error_r))
+ return FALSE;
+
+ struct dcrypt_private_key priv_key;
+ i_zero(&priv_key);
+ priv_key.key = local;
+
+ if (!(ret = dcrypt_openssl_ecdh_derive_secret(&priv_key, peer_key, S,
+ error_r))) {
+ EVP_PKEY_free(local);
+ return FALSE;
+ }
+
+ /* get ephemeral key (=R) */
+ BN_CTX *bn_ctx = BN_CTX_new();
+ const EC_POINT *pub = EC_KEY_get0_public_key(EVP_PKEY_get0_EC_KEY(local));
+ const EC_GROUP *grp = EC_KEY_get0_group(EVP_PKEY_get0_EC_KEY(local));
+ size_t len = EC_POINT_point2oct(grp, pub, POINT_CONVERSION_UNCOMPRESSED,
+ NULL, 0, bn_ctx);
+ unsigned char R_buf[len];
+ EC_POINT_point2oct(grp, pub, POINT_CONVERSION_UNCOMPRESSED,
+ R_buf, len, bn_ctx);
+ BN_CTX_free(bn_ctx);
+ buffer_append(R, R_buf, len);
+ EVP_PKEY_free(local);
+
+ return ret;
+}
+
+static bool
+dcrypt_openssl_pbkdf2(const unsigned char *password, size_t password_len,
+ const unsigned char *salt, size_t salt_len,
+ const char *hash, unsigned int rounds,
+ buffer_t *result, unsigned int result_len,
+ const char **error_r)
+{
+ int ret;
+ i_assert(rounds > 0);
+ i_assert(result_len > 0);
+ i_assert(result != NULL);
+ /* determine MD */
+ const EVP_MD* md = EVP_get_digestbyname(hash);
+ if (md == NULL) {
+ DCRYPT_SET_ERROR(t_strdup_printf("Invalid digest %s", hash));
+ return FALSE;
+ }
+
+ unsigned char buffer[result_len];
+ if ((ret = PKCS5_PBKDF2_HMAC((const char*)password, password_len,
+ salt, salt_len, rounds,
+ md, result_len, buffer)) == 1) {
+ buffer_append(result, buffer, result_len);
+ }
+ if (ret != 1)
+ return dcrypt_openssl_error(error_r);
+ return TRUE;
+}
+
+static bool
+dcrypt_openssl_generate_keypair(struct dcrypt_keypair *pair_r,
+ enum dcrypt_key_type kind, unsigned int bits,
+ const char *curve, const char **error_r)
+{
+ EVP_PKEY *pkey = NULL;
+
+ i_assert(pair_r != NULL);
+ i_zero(pair_r);
+ if (kind == DCRYPT_KEY_RSA) {
+ if (dcrypt_openssl_generate_rsa_key(bits, &pkey, error_r)) {
+ pair_r->priv = i_new(struct dcrypt_private_key, 1);
+ pair_r->priv->key = pkey;
+ pair_r->priv->ref++;
+ pair_r->pub = NULL;
+ dcrypt_openssl_private_to_public_key(pair_r->priv,
+ &pair_r->pub);
+ return TRUE;
+ } else {
+ return dcrypt_openssl_error(error_r);
+ }
+ } else if (kind == DCRYPT_KEY_EC) {
+ int nid = OBJ_sn2nid(curve);
+ if (nid == NID_undef) {
+ DCRYPT_SET_ERROR(t_strdup_printf("Unknown EC curve %s",
+ curve));
+ return FALSE;
+ }
+ if (dcrypt_openssl_generate_ec_key(nid, &pkey, error_r)) {
+ pair_r->priv = i_new(struct dcrypt_private_key, 1);
+ pair_r->priv->key = pkey;
+ pair_r->priv->ref++;
+ pair_r->pub = NULL;
+ dcrypt_openssl_private_to_public_key(pair_r->priv,
+ &pair_r->pub);
+ return TRUE;
+ } else {
+ return dcrypt_openssl_error(error_r);
+ }
+ }
+ DCRYPT_SET_ERROR("Key type not supported in this build");
+ return FALSE;
+}
+
+static bool
+dcrypt_openssl_decrypt_point_v1(buffer_t *data, buffer_t *key, BIGNUM **point_r,
+ const char **error_r)
+{
+ struct dcrypt_context_symmetric *dctx;
+ buffer_t *tmp = t_buffer_create(64);
+
+ if (!dcrypt_openssl_ctx_sym_create("aes-256-ctr", DCRYPT_MODE_DECRYPT,
+ &dctx, error_r)) {
+ return FALSE;
+ }
+
+ /* v1 KEYS have all-zero IV - have to use it ourselves too */
+ dcrypt_openssl_ctx_sym_set_iv(dctx, (const unsigned char*)
+ "\x0\x0\x0\x0\x0\x0\x0\x0\x0\x0\x0\x0\x0\x0\x0\x0", 16);
+ dcrypt_openssl_ctx_sym_set_key(dctx, key->data, key->used);
+
+ if (!dcrypt_openssl_ctx_sym_init(dctx, error_r) ||
+ !dcrypt_openssl_ctx_sym_update(dctx, data->data, data->used,
+ tmp, error_r) ||
+ !dcrypt_openssl_ctx_sym_final(dctx, tmp, error_r)) {
+ dcrypt_openssl_ctx_sym_destroy(&dctx);
+ return FALSE;
+ }
+
+ dcrypt_openssl_ctx_sym_destroy(&dctx);
+
+ *point_r = BN_bin2bn(tmp->data, tmp->used, NULL);
+ safe_memset(buffer_get_modifiable_data(tmp, NULL), 0,tmp->used);
+ buffer_set_used_size(key, 0);
+
+ if (*point_r == NULL)
+ return dcrypt_openssl_error(error_r);
+ return TRUE;
+}
+
+static bool
+dcrypt_openssl_decrypt_point_ec_v1(struct dcrypt_private_key *dec_key,
+ const char *data_hex,
+ const char *peer_key_hex, BIGNUM **point_r,
+ const char **error_r)
+{
+ buffer_t *peer_key, *data, key, *secret;
+ bool res;
+
+ data = t_buffer_create(128);
+ peer_key = t_buffer_create(64);
+
+ hex_to_binary(data_hex, data);
+ hex_to_binary(peer_key_hex, peer_key);
+
+ secret = t_buffer_create(64);
+
+ if (!dcrypt_openssl_ecdh_derive_secret_local(dec_key, peer_key,
+ secret, error_r))
+ return FALSE;
+
+ /* run it thru SHA256 once */
+ unsigned char digest[SHA256_DIGEST_LENGTH];
+ SHA256(secret->data, secret->used, digest);
+ safe_memset(buffer_get_modifiable_data(secret, NULL), 0, secret->used);
+ buffer_set_used_size(secret, 0);
+ buffer_create_from_const_data(&key, digest, SHA256_DIGEST_LENGTH);
+
+ /* then use this as key */
+ res = dcrypt_openssl_decrypt_point_v1(data, &key, point_r, error_r);
+ memset(digest, 0, sizeof(digest));
+ safe_memset(digest, 0, SHA256_DIGEST_LENGTH);
+
+ return res;
+}
+
+static bool
+dcrypt_openssl_decrypt_point_password_v1(const char *data_hex,
+ const char *password_hex,
+ const char *salt_hex, BIGNUM **point_r,
+ const char **error_r)
+{
+ buffer_t *salt, *data, *password, *key;
+
+ data = t_buffer_create(128);
+ salt = t_buffer_create(16);
+ password = t_buffer_create(32);
+ key = t_buffer_create(32);
+
+ hex_to_binary(data_hex, data);
+ hex_to_binary(salt_hex, salt);
+ hex_to_binary(password_hex, password);
+
+ /* aes-256-ctr uses 32 byte key, and v1 uses all-zero IV */
+ if (!dcrypt_openssl_pbkdf2(password->data, password->used,
+ salt->data, salt->used,
+ "sha256", 16, key, 32, error_r))
+ return FALSE;
+
+ return dcrypt_openssl_decrypt_point_v1(data, key, point_r, error_r);
+}
+
+static bool
+dcrypt_openssl_load_private_key_dovecot_v1(struct dcrypt_private_key **key_r,
+ int len, const char **input,
+ const char *password,
+ struct dcrypt_private_key *dec_key,
+ const char **error_r)
+{
+ int nid, ec, enctype;
+ BIGNUM *point = NULL;
+
+ if (str_to_int(input[1], &nid) != 0) {
+ DCRYPT_SET_ERROR("Corrupted data");
+ return FALSE;
+ }
+
+ if (str_to_int(input[2], &enctype) != 0) {
+ DCRYPT_SET_ERROR("Corrupted data");
+ return FALSE;
+ }
+
+ /* decode and optionally decipher private key value */
+ if (enctype == DCRYPT_DOVECOT_KEY_ENCRYPT_NONE) {
+ point = BN_secure_new();
+ if (point == NULL || BN_hex2bn(&point, input[3]) < 1) {
+ BN_free(point);
+ return dcrypt_openssl_error(error_r);
+ }
+ } else if (enctype == DCRYPT_DOVECOT_KEY_ENCRYPT_PASSWORD) {
+ /* by password */
+ if (password == NULL) {
+ DCRYPT_SET_ERROR("password missing");
+ return FALSE;
+ }
+ const char *enc_priv_pt = input[3];
+ const char *salt = input[4];
+ if (!dcrypt_openssl_decrypt_point_password_v1(
+ enc_priv_pt, password, salt, &point, error_r)) {
+ return FALSE;
+ }
+ } else if (enctype == DCRYPT_DOVECOT_KEY_ENCRYPT_PK) {
+ /* by key */
+ if (dec_key == NULL) {
+ DCRYPT_SET_ERROR("decrypt key missing");
+ return FALSE;
+ }
+ const char *enc_priv_pt = input[3];
+ const char *peer_key = input[4];
+ if (!dcrypt_openssl_decrypt_point_ec_v1(
+ dec_key, enc_priv_pt, peer_key, &point, error_r)) {
+ return FALSE;
+ }
+ } else {
+ DCRYPT_SET_ERROR("Invalid key data");
+ return FALSE;
+ }
+
+ EC_KEY *eckey = EC_KEY_new_by_curve_name(nid);
+ if (eckey == NULL) return dcrypt_openssl_error(error_r);
+
+ /* assign private key */
+ BN_CTX *bnctx = BN_CTX_new();
+ if (bnctx == NULL) {
+ EC_KEY_free(eckey);
+ return dcrypt_openssl_error(error_r);
+ }
+ EC_KEY_set_private_key(eckey, point);
+ EC_KEY_precompute_mult(eckey, bnctx);
+ EC_KEY_set_asn1_flag(eckey, OPENSSL_EC_NAMED_CURVE);
+ EC_POINT *pub = EC_POINT_new(EC_KEY_get0_group(eckey));
+ if (pub == NULL) {
+ EC_KEY_free(eckey);
+ BN_CTX_free(bnctx);
+ return dcrypt_openssl_error(error_r);
+ }
+ /* calculate public key */
+ ec = EC_POINT_mul(EC_KEY_get0_group(eckey), pub, point,
+ NULL, NULL, bnctx);
+ EC_KEY_set_public_key(eckey, pub);
+ BN_free(point);
+ EC_POINT_free(pub);
+ BN_CTX_free(bnctx);
+
+ /* make sure it looks OK and is correct */
+ if (ec == 1 && EC_KEY_check_key(eckey) == 1) {
+ unsigned char digest[SHA256_DIGEST_LENGTH];
+ /* validate that the key was loaded correctly */
+ char *id = ec_key_get_pub_point_hex(eckey);
+ if (id == NULL) {
+ EC_KEY_free(eckey);
+ return dcrypt_openssl_error(error_r);
+ }
+ SHA256((unsigned char*)id, strlen(id), digest);
+ OPENSSL_free(id);
+ const char *digest_hex =
+ binary_to_hex(digest, SHA256_DIGEST_LENGTH);
+ if (strcmp(digest_hex, input[len-1]) != 0) {
+ DCRYPT_SET_ERROR("Key id mismatch after load");
+ EC_KEY_free(eckey);
+ return FALSE;
+ }
+ EVP_PKEY *key = EVP_PKEY_new();
+ if (key == NULL) {
+ EC_KEY_free(eckey);
+ return dcrypt_openssl_error(error_r);
+ }
+ EVP_PKEY_set1_EC_KEY(key, eckey);
+ EC_KEY_free(eckey);
+ *key_r = i_new(struct dcrypt_private_key, 1);
+ (*key_r)->key = key;
+ (*key_r)->ref++;
+ return TRUE;
+ }
+
+ EC_KEY_free(eckey);
+
+ return dcrypt_openssl_error(error_r);
+}
+
+/* encrypt/decrypt private keys */
+static bool
+dcrypt_openssl_cipher_key_dovecot_v2(const char *cipher,
+ enum dcrypt_sym_mode mode,
+ buffer_t *input, buffer_t *secret,
+ buffer_t *salt, const char *digalgo,
+ unsigned int rounds, buffer_t *result_r,
+ const char **error_r)
+{
+ struct dcrypt_context_symmetric *dctx;
+ bool res;
+
+ if (!dcrypt_openssl_ctx_sym_create(cipher, mode, &dctx, error_r)) {
+ return FALSE;
+ }
+
+ /* generate encryption key/iv based on secret/salt */
+ buffer_t *key_data = t_buffer_create(128);
+ res = dcrypt_openssl_pbkdf2(secret->data, secret->used,
+ salt->data, salt->used, digalgo, rounds, key_data,
+ dcrypt_openssl_ctx_sym_get_key_length(dctx) +
+ dcrypt_openssl_ctx_sym_get_iv_length(dctx),
+ error_r);
+
+ if (!res) {
+ dcrypt_openssl_ctx_sym_destroy(&dctx);
+ return FALSE;
+ }
+
+ buffer_t *tmp = t_buffer_create(128);
+ const unsigned char *kd = buffer_free_without_data(&key_data);
+
+ /* perform ciphering */
+ dcrypt_openssl_ctx_sym_set_key(dctx, kd,
+ dcrypt_openssl_ctx_sym_get_key_length(dctx));
+ dcrypt_openssl_ctx_sym_set_iv(dctx,
+ kd + dcrypt_openssl_ctx_sym_get_key_length(dctx),
+ dcrypt_openssl_ctx_sym_get_iv_length(dctx));
+
+ if (!dcrypt_openssl_ctx_sym_init(dctx, error_r) ||
+ !dcrypt_openssl_ctx_sym_update(dctx, input->data,
+ input->used, tmp, error_r) ||
+ !dcrypt_openssl_ctx_sym_final(dctx, tmp, error_r)) {
+ res = FALSE;
+ } else {
+ /* provide result if succeeded */
+ buffer_append_buf(result_r, tmp, 0, SIZE_MAX);
+ res = TRUE;
+ }
+ /* and ensure no data leaks */
+ safe_memset(buffer_get_modifiable_data(tmp, NULL), 0, tmp->used);
+
+ dcrypt_openssl_ctx_sym_destroy(&dctx);
+ return res;
+}
+
+static bool
+dcrypt_openssl_load_private_key_dovecot_v2(struct dcrypt_private_key **key_r,
+ int len, const char **input,
+ const char *password,
+ struct dcrypt_private_key *dec_key,
+ const char **error_r)
+{
+ int enctype;
+ buffer_t *key_data = t_buffer_create(256);
+
+ /* check for encryption type */
+ if (str_to_int(input[2], &enctype) != 0) {
+ DCRYPT_SET_ERROR("Corrupted data");
+ return FALSE;
+ }
+
+ if (enctype < 0 || enctype > 2) {
+ DCRYPT_SET_ERROR("Corrupted data");
+ return FALSE;
+ }
+
+ /* match encryption type to field counts */
+ if ((enctype == DCRYPT_DOVECOT_KEY_ENCRYPT_NONE && len != 5) ||
+ (enctype == DCRYPT_DOVECOT_KEY_ENCRYPT_PASSWORD && len != 9) ||
+ (enctype == DCRYPT_DOVECOT_KEY_ENCRYPT_PK && len != 11)) {
+ DCRYPT_SET_ERROR("Corrupted data");
+ return FALSE;
+ }
+
+ /* get key type */
+ int nid = OBJ_txt2nid(input[1]);
+
+ if (nid == NID_undef)
+ return dcrypt_openssl_error(error_r);
+
+ /* decode and possibly decipher private key value */
+ if (enctype == DCRYPT_DOVECOT_KEY_ENCRYPT_NONE) {
+ if (hex_to_binary(input[3], key_data) != 0) {
+ DCRYPT_SET_ERROR("Corrupted data");
+ }
+ } else if (enctype == DCRYPT_DOVECOT_KEY_ENCRYPT_PK) {
+ if (dec_key == NULL) {
+ DCRYPT_SET_ERROR("decrypt key missing");
+ return FALSE;
+ }
+ unsigned int rounds;
+ struct dcrypt_public_key *pubkey = NULL;
+ if (str_to_uint(input[6], &rounds) != 0) {
+ DCRYPT_SET_ERROR("Corrupted data");
+ return FALSE;
+ }
+
+ buffer_t *data = t_buffer_create(128);
+
+ /* check that we have correct decryption key */
+ dcrypt_openssl_private_to_public_key(dec_key, &pubkey);
+ if (!dcrypt_openssl_public_key_id(pubkey, "sha256",
+ data, error_r)) {
+ dcrypt_openssl_unref_public_key(&pubkey);
+ return FALSE;
+ }
+
+ dcrypt_openssl_unref_public_key(&pubkey);
+
+ if (strcmp(binary_to_hex(data->data, data->used),
+ input[9]) != 0) {
+ DCRYPT_SET_ERROR("No private key available");
+ return FALSE;
+ }
+
+
+ buffer_t *salt, *peer_key, *secret;
+ salt = t_buffer_create(strlen(input[4])/2);
+ peer_key = t_buffer_create(strlen(input[8])/2);
+ secret = t_buffer_create(128);
+
+ buffer_set_used_size(data, 0);
+ hex_to_binary(input[4], salt);
+ hex_to_binary(input[8], peer_key);
+ hex_to_binary(input[7], data);
+
+ /* get us secret value to use for key/iv generation */
+ if (EVP_PKEY_base_id((EVP_PKEY*)dec_key) == EVP_PKEY_RSA) {
+ if (!dcrypt_openssl_rsa_decrypt(dec_key,
+ peer_key->data, peer_key->used, secret,
+ DCRYPT_PADDING_RSA_PKCS1_OAEP, error_r))
+ return FALSE;
+ } else {
+ /* perform ECDH */
+ if (!dcrypt_openssl_ecdh_derive_secret_local(
+ dec_key, peer_key, secret, error_r))
+ return FALSE;
+ }
+ /* decrypt key */
+ if (!dcrypt_openssl_cipher_key_dovecot_v2(input[3],
+ DCRYPT_MODE_DECRYPT, data, secret, salt,
+ input[5], rounds, key_data, error_r)) {
+ return FALSE;
+ }
+ } else if (enctype == DCRYPT_DOVECOT_KEY_ENCRYPT_PASSWORD) {
+ if (password == NULL) {
+ DCRYPT_SET_ERROR("password missing");
+ return FALSE;
+ }
+ unsigned int rounds;
+ if (str_to_uint(input[6], &rounds) != 0) {
+ DCRYPT_SET_ERROR("Corrupted data");
+ return FALSE;
+ }
+
+ buffer_t *salt, secret, *data;
+ salt = t_buffer_create(strlen(input[4])/2);
+ buffer_create_from_const_data(&secret, password, strlen(password));
+ data = t_buffer_create(strlen(input[7])/2);
+ if (hex_to_binary(input[4], salt) != 0 ||
+ hex_to_binary(input[7], data) != 0) {
+ DCRYPT_SET_ERROR("Corrupted data");
+ return FALSE;
+ }
+
+ if (!dcrypt_openssl_cipher_key_dovecot_v2(input[3],
+ DCRYPT_MODE_DECRYPT, data, &secret, salt,
+ input[5], rounds, key_data, error_r)) {
+ return FALSE;
+ }
+ }
+
+ /* decode actual key */
+ if (EVP_PKEY_type(nid) == EVP_PKEY_RSA) {
+ RSA *rsa = RSA_new();
+ const unsigned char *ptr = buffer_get_data(key_data, NULL);
+ if (rsa == NULL ||
+ d2i_RSAPrivateKey(&rsa, &ptr, key_data->used) == NULL ||
+ RSA_check_key(rsa) != 1) {
+ safe_memset(buffer_get_modifiable_data(key_data, NULL),
+ 0, key_data->used);
+ RSA_free(rsa);
+ return dcrypt_openssl_error(error_r);
+ }
+ safe_memset(buffer_get_modifiable_data(key_data, NULL),
+ 0, key_data->used);
+ buffer_set_used_size(key_data, 0);
+ EVP_PKEY *pkey = EVP_PKEY_new();
+ if (pkey == NULL) {
+ RSA_free(rsa);
+ return dcrypt_openssl_error(error_r);
+ }
+ EVP_PKEY_set1_RSA(pkey, rsa);
+ RSA_free(rsa);
+ *key_r = i_new(struct dcrypt_private_key, 1);
+ (*key_r)->key = pkey;
+ (*key_r)->ref++;
+ } else {
+ int ec;
+ BIGNUM *point = BN_secure_new();
+ if (point == NULL ||
+ BN_mpi2bn(key_data->data, key_data->used, point) == NULL) {
+ safe_memset(buffer_get_modifiable_data(key_data, NULL),
+ 0, key_data->used);
+ BN_free(point);
+ return dcrypt_openssl_error(error_r);
+ }
+ EC_KEY *eckey = EC_KEY_new_by_curve_name(nid);
+ safe_memset(buffer_get_modifiable_data(key_data, NULL),
+ 0, key_data->used);
+ buffer_set_used_size(key_data, 0);
+ BN_CTX *bnctx = BN_CTX_new();
+ if (eckey == NULL || bnctx == NULL) {
+ BN_free(point);
+ EC_KEY_free(eckey);
+ BN_CTX_free(bnctx);
+ return dcrypt_openssl_error(error_r);
+ }
+ EC_KEY_set_private_key(eckey, point);
+ EC_KEY_precompute_mult(eckey, bnctx);
+ EC_KEY_set_asn1_flag(eckey, OPENSSL_EC_NAMED_CURVE);
+ EC_POINT *pub = EC_POINT_new(EC_KEY_get0_group(eckey));
+ if (pub == NULL)
+ ec = -1;
+ else {
+ /* calculate public key */
+ ec = EC_POINT_mul(EC_KEY_get0_group(eckey), pub, point,
+ NULL, NULL, bnctx);
+ EC_KEY_set_public_key(eckey, pub);
+ EC_POINT_free(pub);
+ }
+ BN_free(point);
+ BN_CTX_free(bnctx);
+ /* make sure the EC key is valid */
+ EVP_PKEY *key = EVP_PKEY_new();
+ if (ec == 1 && key != NULL && EC_KEY_check_key(eckey) == 1) {
+ EVP_PKEY_set1_EC_KEY(key, eckey);
+ EC_KEY_free(eckey);
+ *key_r = i_new(struct dcrypt_private_key, 1);
+ (*key_r)->key = key;
+ (*key_r)->ref++;
+ } else {
+ EVP_PKEY_free(key);
+ EC_KEY_free(eckey);
+ return dcrypt_openssl_error(error_r);
+ }
+ }
+
+ /* finally compare key to key id */
+ dcrypt_openssl_private_key_id(*key_r, "sha256", key_data, NULL);
+
+ if (strcmp(binary_to_hex(key_data->data, key_data->used),
+ input[len-1]) != 0) {
+ dcrypt_openssl_unref_private_key(key_r);
+ DCRYPT_SET_ERROR("Key id mismatch after load");
+ return FALSE;
+ }
+
+ return TRUE;
+}
+
+/* JWK Parameter names defined at https://www.iana.org/assignments/jose/jose.xhtml */
+
+static const struct jwk_to_ssl_map_entry {
+ const char *jwk_curve;
+ int nid;
+} jwk_to_ssl_curves[] =
+{
+ /* See https://tools.ietf.org/search/rfc8422#appendix-A */
+ { .jwk_curve = "P-256", .nid = NID_X9_62_prime256v1 },
+ { .jwk_curve = "P-384", .nid = NID_secp384r1 },
+ { .jwk_curve = "P-521", .nid = NID_secp521r1 },
+ { .jwk_curve = NULL, .nid = 0 }
+};
+
+static const char *key_usage_to_jwk_use(enum dcrypt_key_usage usage)
+{
+ switch(usage) {
+ case DCRYPT_KEY_USAGE_NONE:
+ return NULL;
+ case DCRYPT_KEY_USAGE_ENCRYPT:
+ return "enc";
+ case DCRYPT_KEY_USAGE_SIGN:
+ return "sig";
+ };
+ i_unreached();
+}
+
+static enum dcrypt_key_usage jwk_use_to_key_usage(const char *use)
+{
+ if (strcmp(use, "enc") == 0)
+ return DCRYPT_KEY_USAGE_ENCRYPT;
+ if (strcmp(use, "sig") == 0)
+ return DCRYPT_KEY_USAGE_SIGN;
+ return DCRYPT_KEY_USAGE_NONE;
+}
+
+static int jwk_curve_to_nid(const char *curve)
+{
+ /* use static mapping table to get correct input for OpenSSL */
+ const struct jwk_to_ssl_map_entry *entry = jwk_to_ssl_curves;
+ for (;entry->jwk_curve != NULL;entry++)
+ if (strcmp(curve, entry->jwk_curve) == 0)
+ return entry->nid;
+ return 0;
+}
+
+static const char *nid_to_jwk_curve(int nid)
+{
+ const struct jwk_to_ssl_map_entry *entry = jwk_to_ssl_curves;
+ for (;entry->jwk_curve != NULL;entry++)
+ if (nid == entry->nid)
+ return entry->jwk_curve;
+ return NULL;
+}
+
+/* Loads both public and private key */
+static bool load_jwk_ec_key(EVP_PKEY **key_r, bool want_private_key,
+ const struct json_tree_node *root,
+ const char *password ATTR_UNUSED,
+ struct dcrypt_private_key *dec_key ATTR_UNUSED,
+ const char **error_r)
+{
+ i_assert(password == NULL && dec_key == NULL);
+ const char *crv, *x, *y, *d;
+ const struct json_tree_node *node;
+
+ if ((node = json_tree_find_key(root, "crv")) == NULL ||
+ (crv = json_tree_get_value_str(node)) == NULL) {
+ DCRYPT_SET_ERROR("Missing crv parameter");
+ return FALSE;
+ }
+
+ if ((node = json_tree_find_key(root, "x")) == NULL ||
+ (x = json_tree_get_value_str(node)) == NULL) {
+ DCRYPT_SET_ERROR("Missing x parameter");
+ return FALSE;
+ }
+
+ if ((node = json_tree_find_key(root, "y")) == NULL ||
+ (y = json_tree_get_value_str(node)) == NULL) {
+ DCRYPT_SET_ERROR("Missing y parameter");
+ return FALSE;
+ }
+
+ if ((node = json_tree_find_key(root, "d")) == NULL ||
+ (d = json_tree_get_value_str(node)) == NULL) {
+ if (want_private_key) {
+ DCRYPT_SET_ERROR("Missing d parameter");
+ return FALSE;
+ }
+ }
+
+ /* base64 decode x and y */
+ buffer_t *bx = t_base64url_decode_str(x);
+ buffer_t *by = t_base64url_decode_str(y);
+
+ /* determine NID */
+ int nid = jwk_curve_to_nid(crv);
+ if (nid == 0) {
+ DCRYPT_SET_ERROR(t_strdup_printf("Unsupported curve: %s", crv));
+ return FALSE;
+ }
+ /* create key */
+ EC_KEY *ec_key = EC_KEY_new_by_curve_name(nid);
+ if (ec_key == NULL) {
+ DCRYPT_SET_ERROR("Cannot allocate memory");
+ return FALSE;
+ }
+
+ BIGNUM *px = BN_new();
+ BIGNUM *py = BN_new();
+
+ if (BN_bin2bn(bx->data, bx->used, px) == NULL ||
+ BN_bin2bn(by->data, by->used, py) == NULL) {
+ EC_KEY_free(ec_key);
+ BN_free(px);
+ BN_free(py);
+ return dcrypt_openssl_error(error_r);
+ }
+
+ int ret = EC_KEY_set_public_key_affine_coordinates(ec_key, px, py);
+ BN_free(px);
+ BN_free(py);
+
+ if (ret != 1) {
+ EC_KEY_free(ec_key);
+ return dcrypt_openssl_error(error_r);
+ }
+
+ /* FIXME: Support decryption */
+ if (want_private_key) {
+ buffer_t *bd = t_base64url_decode_str(d);
+ BIGNUM *pd = BN_secure_new();
+ if (BN_bin2bn(bd->data, bd->used, pd) == NULL) {
+ EC_KEY_free(ec_key);
+ return dcrypt_openssl_error(error_r);
+ }
+ ret = EC_KEY_set_private_key(ec_key, pd);
+ BN_free(pd);
+ if (ret != 1) {
+ EC_KEY_free(ec_key);
+ return dcrypt_openssl_error(error_r);
+ }
+ }
+
+ if (EC_KEY_check_key(ec_key) != 1) {
+ EC_KEY_free(ec_key);
+ return dcrypt_openssl_error(error_r);
+ }
+
+ EC_KEY_precompute_mult(ec_key, NULL);
+ EC_KEY_set_asn1_flag(ec_key, OPENSSL_EC_NAMED_CURVE);
+
+ /* return as EVP_PKEY */
+ EVP_PKEY *pkey = EVP_PKEY_new();
+ EVP_PKEY_set1_EC_KEY(pkey, ec_key);
+ EC_KEY_free(ec_key);
+ *key_r = pkey;
+
+ return TRUE;
+}
+
+/* RSA helpers */
+#if !defined(HAVE_RSA_SET0_KEY)
+static int RSA_set0_key(RSA *r, BIGNUM *n, BIGNUM *e, BIGNUM *d)
+{
+ if (n == NULL || e == NULL) {
+ RSAerr(0, ERR_R_PASSED_NULL_PARAMETER);
+ return 0;
+ }
+ BN_free(r->n);
+ r->n = n;
+ BN_free(r->e);
+ r->e = e;
+ BN_free(r->d);
+ r->d = d;
+ return 1;
+}
+#endif
+#if !defined(HAVE_RSA_SET0_FACTORS)
+static int RSA_set0_factors(RSA *r, BIGNUM *p, BIGNUM *q)
+{
+ if (p == NULL || q == NULL) {
+ RSAerr(0, ERR_R_PASSED_NULL_PARAMETER);
+ return 0;
+ }
+ BN_free(r->p);
+ r->p = p;
+ BN_free(r->q);
+ r->q = q;
+ return 1;
+}
+#endif
+#if !defined(HAVE_RSA_SET0_CRT_PARAMS)
+static int RSA_set0_crt_params(RSA *r, BIGNUM *dmp1, BIGNUM *dmq1, BIGNUM *iqmp)
+{
+ if (dmp1 == NULL || dmq1 == NULL || iqmp == NULL) {
+ RSAerr(0, ERR_R_PASSED_NULL_PARAMETER);
+ return 0;
+ }
+ BN_free(r->dmp1);
+ r->dmp1 = dmp1;
+ BN_free(r->dmq1);
+ r->dmq1 = dmq1;
+ BN_free(r->iqmp);
+ r->iqmp = iqmp;
+ return 1;
+}
+#endif
+
+/* Loads both public and private key */
+static bool load_jwk_rsa_key(EVP_PKEY **key_r, bool want_private_key,
+ const struct json_tree_node *root,
+ const char *password ATTR_UNUSED,
+ struct dcrypt_private_key *dec_key ATTR_UNUSED,
+ const char **error_r)
+{
+ const char *n, *e, *d = NULL, *p = NULL, *q = NULL, *dp = NULL;
+ const char *dq = NULL, *qi = NULL;
+ const struct json_tree_node *node;
+
+ /* n and e must be present */
+ if ((node = json_tree_find_key(root, "n")) == NULL ||
+ (n = json_tree_get_value_str(node)) == NULL) {
+ DCRYPT_SET_ERROR("Missing n parameter");
+ return FALSE;
+ }
+
+ if ((node = json_tree_find_key(root, "e")) == NULL ||
+ (e = json_tree_get_value_str(node)) == NULL) {
+ DCRYPT_SET_ERROR("Missing e parameter");
+ return FALSE;
+ }
+
+ if (want_private_key) {
+ if ((node = json_tree_find_key(root, "d")) == NULL ||
+ (d = json_tree_get_value_str(node)) == NULL) {
+ DCRYPT_SET_ERROR("Missing d parameter");
+ return FALSE;
+ }
+
+ if ((node = json_tree_find_key(root, "p")) == NULL ||
+ (p = json_tree_get_value_str(node)) == NULL) {
+ DCRYPT_SET_ERROR("Missing p parameter");
+ return FALSE;
+ }
+
+ if ((node = json_tree_find_key(root, "q")) == NULL ||
+ (q = json_tree_get_value_str(node)) == NULL) {
+ DCRYPT_SET_ERROR("Missing q parameter");
+ return FALSE;
+ }
+
+ if ((node = json_tree_find_key(root, "dp")) == NULL ||
+ (dp = json_tree_get_value_str(node)) == NULL) {
+ DCRYPT_SET_ERROR("Missing dp parameter");
+ return FALSE;
+ }
+
+ if ((node = json_tree_find_key(root, "dq")) == NULL ||
+ (dq = json_tree_get_value_str(node)) == NULL) {
+ DCRYPT_SET_ERROR("Missing dq parameter");
+ return FALSE;
+ }
+
+ if ((node = json_tree_find_key(root, "qi")) == NULL ||
+ (qi = json_tree_get_value_str(node)) == NULL) {
+ DCRYPT_SET_ERROR("Missing qi parameter");
+ return FALSE;
+ }
+ }
+
+ /* convert into BIGNUMs */
+ BIGNUM *pn, *pe, *pd, *pp, *pq, *pdp, *pdq, *pqi;
+ buffer_t *bn = t_base64url_decode_str(n);
+ buffer_t *be = t_base64url_decode_str(e);
+ if (want_private_key) {
+ pd = BN_secure_new();
+ buffer_t *bd = t_base64url_decode_str(d);
+ if (BN_bin2bn(bd->data, bd->used, pd) == NULL) {
+ BN_free(pd);
+ return dcrypt_openssl_error(error_r);
+ }
+ } else {
+ pd = NULL;
+ }
+
+ pn = BN_new();
+ pe = BN_new();
+
+ if (BN_bin2bn(bn->data, bn->used, pn) == NULL ||
+ BN_bin2bn(be->data, be->used, pe) == NULL) {
+ if (pd != NULL)
+ BN_free(pd);
+ BN_free(pn);
+ BN_free(pe);
+ return dcrypt_openssl_error(error_r);
+ }
+
+ RSA *rsa_key = RSA_new();
+ if (rsa_key == NULL) {
+ if (pd != NULL)
+ BN_free(pd);
+ BN_free(pn);
+ BN_free(pe);
+ return dcrypt_openssl_error(error_r);
+ }
+
+ if (RSA_set0_key(rsa_key, pn, pe, pd) != 1) {
+ if (pd != NULL)
+ BN_free(pd);
+ BN_free(pn);
+ BN_free(pe);
+ RSA_free(rsa_key);
+ return dcrypt_openssl_error(error_r);
+ }
+
+ if (want_private_key) {
+ pp = BN_secure_new();
+ pq = BN_secure_new();
+ pdp = BN_secure_new();
+ pdq = BN_secure_new();
+ pqi = BN_secure_new();
+
+ buffer_t *bp = t_base64url_decode_str(p);
+ buffer_t *bq = t_base64url_decode_str(q);
+ buffer_t *bdp = t_base64url_decode_str(dp);
+ buffer_t *bdq = t_base64url_decode_str(dq);
+ buffer_t *bqi = t_base64url_decode_str(qi);
+
+ if (BN_bin2bn(bp->data, bp->used, pp) == NULL ||
+ BN_bin2bn(bq->data, bq->used, pq) == NULL ||
+ BN_bin2bn(bdp->data, bdp->used, pdp) == NULL ||
+ BN_bin2bn(bdq->data, bdq->used, pdq) == NULL ||
+ BN_bin2bn(bqi->data, bqi->used, pqi) == NULL ||
+ RSA_set0_factors(rsa_key, pp, pq) != 1) {
+ RSA_free(rsa_key);
+ BN_free(pp);
+ BN_free(pq);
+ BN_free(pdp);
+ BN_free(pdq);
+ BN_free(pqi);
+ return dcrypt_openssl_error(error_r);
+ } else if (RSA_set0_crt_params(rsa_key, pdp, pdq, pqi) != 1) {
+ RSA_free(rsa_key);
+ BN_free(pdp);
+ BN_free(pdq);
+ BN_free(pqi);
+ return dcrypt_openssl_error(error_r);
+ }
+ }
+
+ /* return as EVP_PKEY */
+ EVP_PKEY *pkey = EVP_PKEY_new();
+ EVP_PKEY_set1_RSA(pkey, rsa_key);
+ RSA_free(rsa_key);
+ *key_r = pkey;
+
+ return TRUE;
+}
+
+
+static bool
+dcrypt_openssl_load_private_key_jwk(struct dcrypt_private_key **key_r,
+ const char *data, const char *password,
+ struct dcrypt_private_key *dec_key,
+ const char **error_r)
+{
+ const char *kty;
+ const char *error;
+ const struct json_tree_node *root, *node;
+ struct json_tree *key_tree;
+ EVP_PKEY *pkey;
+ bool ret;
+
+ if (parse_jwk_key(data, &key_tree, &error) != 0) {
+ DCRYPT_SET_ERROR(t_strdup_printf("Cannot load JWK private key: %s",
+ error));
+ return FALSE;
+ }
+
+ root = json_tree_root(key_tree);
+
+ /* check key type */
+ if ((node = json_tree_find_key(root, "kty")) == NULL) {
+ DCRYPT_SET_ERROR("Cannot load JWK private key: no kty parameter");
+ json_tree_deinit(&key_tree);
+ return FALSE;
+ }
+
+ kty = json_tree_get_value_str(node);
+
+ if (null_strcmp(kty, "EC") == 0) {
+ ret = load_jwk_ec_key(&pkey, TRUE, root, password, dec_key, &error);
+ } else if (strcmp(kty, "RSA") == 0) {
+ ret = load_jwk_rsa_key(&pkey, TRUE, root, password, dec_key, &error);
+ } else {
+ error = "Unsupported key type";
+ ret = FALSE;
+ }
+
+ i_assert(ret || error != NULL);
+
+ if (!ret)
+ DCRYPT_SET_ERROR(t_strdup_printf("Cannot load JWK private key: %s", error));
+ else if (ret) {
+ *key_r = i_new(struct dcrypt_private_key, 1);
+ (*key_r)->key = pkey;
+ (*key_r)->ref++;
+ /* check if kid is present */
+ if ((node = json_tree_find_key(root, "kid")) != NULL)
+ (*key_r)->key_id = i_strdup_empty(json_tree_get_value_str(node));
+ /* check if use is present */
+ if ((node = json_tree_find_key(root, "use")) != NULL)
+ (*key_r)->usage = jwk_use_to_key_usage(json_tree_get_value_str(node));
+ }
+
+ json_tree_deinit(&key_tree);
+
+ return ret;
+}
+
+static bool
+dcrypt_openssl_load_public_key_jwk(struct dcrypt_public_key **key_r,
+ const char *data, const char **error_r)
+{
+ const char *kty;
+ const char *error;
+ const struct json_tree_node *root, *node;
+ struct json_tree *key_tree;
+ EVP_PKEY *pkey;
+ bool ret;
+
+ if (parse_jwk_key(data, &key_tree, &error) != 0) {
+ DCRYPT_SET_ERROR(t_strdup_printf("Cannot load JWK public key: %s",
+ error));
+ return FALSE;
+ }
+
+ root = json_tree_root(key_tree);
+
+ /* check key type */
+ if ((node = json_tree_find_key(root, "kty")) == NULL) {
+ DCRYPT_SET_ERROR("Cannot load JWK public key: no kty parameter");
+ json_tree_deinit(&key_tree);
+ return FALSE;
+ }
+
+ kty = json_tree_get_value_str(node);
+
+ if (null_strcmp(kty, "EC") == 0) {
+ ret = load_jwk_ec_key(&pkey, FALSE, root, NULL, NULL, &error);
+ } else if (strcmp(kty, "RSA") == 0) {
+ ret = load_jwk_rsa_key(&pkey, FALSE, root, NULL, NULL, &error);
+ } else {
+ error = "Unsupported key type";
+ ret = FALSE;
+ }
+
+ i_assert(ret || error != NULL);
+
+ if (!ret)
+ DCRYPT_SET_ERROR(t_strdup_printf("Cannot load JWK public key: %s", error));
+ else if (ret) {
+ *key_r = i_new(struct dcrypt_public_key, 1);
+ (*key_r)->key = pkey;
+ (*key_r)->ref++;
+ /* check if kid is present */
+ if ((node = json_tree_find_key(root, "kid")) != NULL)
+ (*key_r)->key_id = i_strdup_empty(json_tree_get_value_str(node));
+ /* check if use is present */
+ if ((node = json_tree_find_key(root, "use")) != NULL)
+ (*key_r)->usage = jwk_use_to_key_usage(json_tree_get_value_str(node));
+ }
+
+ json_tree_deinit(&key_tree);
+
+ return ret;
+}
+
+
+static int bn2base64url(const BIGNUM *bn, string_t *dest)
+{
+ int len = BN_num_bytes(bn);
+ unsigned char *data = t_malloc_no0(len);
+ if (BN_bn2bin(bn, data) != len)
+ return -1;
+ base64url_encode(BASE64_ENCODE_FLAG_NO_PADDING, SIZE_MAX, data, len, dest);
+ return 0;
+}
+
+/* FIXME: Add encryption support */
+/* FIXME: Add support for 'algo' field */
+static bool store_jwk_ec_key(EVP_PKEY *pkey, bool is_private_key,
+ enum dcrypt_key_usage usage,
+ const char *key_id,
+ const char *cipher ATTR_UNUSED,
+ const char *password ATTR_UNUSED,
+ struct dcrypt_public_key *enc_key ATTR_UNUSED,
+ string_t *dest, const char **error_r)
+{
+ i_assert(cipher == NULL && password == NULL && enc_key == NULL);
+ string_t *temp = t_str_new(256);
+ const EC_KEY *ec_key = EVP_PKEY_get0_EC_KEY(pkey);
+ i_assert(ec_key != NULL);
+
+ int nid = EC_GROUP_get_curve_name(EC_KEY_get0_group(ec_key));
+ const EC_POINT *public_point = EC_KEY_get0_public_key(ec_key);
+ BIGNUM *x, *y;
+
+ x = BN_new();
+ y = BN_new();
+ if (EC_POINT_get_affine_coordinates_GFp(EC_KEY_get0_group(ec_key), public_point,
+ x, y, NULL) != 1) {
+ BN_free(x);
+ BN_free(y);
+ return dcrypt_openssl_error(error_r);
+ }
+
+ const char *curve = nid_to_jwk_curve(nid);
+ const char *use = key_usage_to_jwk_use(usage);
+
+ str_printfa(temp, "{\"kty\":\"EC\",\"crv\":\"%s\"", curve);
+ str_append(temp, ",\"x\":\"");
+ bn2base64url(x, temp);
+ str_append(temp, "\",\"y\":\"");
+ bn2base64url(y, temp);
+
+ if (use != NULL) {
+ str_append(temp, "\",\"use\":\"");
+ json_append_escaped(temp, use);
+ }
+ if (key_id != NULL) {
+ str_append(temp, "\",\"kid\":\"");
+ json_append_escaped(temp, key_id);
+ }
+ BN_free(x);
+ BN_free(y);
+
+ if (is_private_key) {
+ const BIGNUM *d = EC_KEY_get0_private_key(ec_key);
+ if (d == NULL) {
+ DCRYPT_SET_ERROR("No private key available");
+ return FALSE;
+ }
+ str_append(temp, "\",\"d\":\"");
+ bn2base64url(d, temp);
+ }
+ str_append(temp, "\"}");
+ str_append_str(dest, temp);
+ return TRUE;
+}
+
+/* FIXME: Add RSA support */
+
+static bool store_jwk_key(EVP_PKEY *pkey, bool is_private_key,
+ enum dcrypt_key_usage usage,
+ const char *key_id,
+ const char *cipher,
+ const char *password,
+ struct dcrypt_public_key *enc_key,
+ string_t *dest, const char **error_r)
+{
+ i_assert(cipher == NULL && password == NULL && enc_key == NULL);
+ if (EVP_PKEY_base_id(pkey) == EVP_PKEY_EC) {
+ return store_jwk_ec_key(pkey, is_private_key, usage, key_id,
+ cipher, password, enc_key, dest, error_r);
+ }
+ DCRYPT_SET_ERROR("Unsupported key type");
+ return FALSE;
+}
+
+static bool
+dcrypt_openssl_load_private_key_dovecot(struct dcrypt_private_key **key_r,
+ const char *data, const char *password,
+ struct dcrypt_private_key *key,
+ enum dcrypt_key_version version,
+ const char **error_r)
+{
+ const char **input = t_strsplit(data, ":\t");
+ size_t len = str_array_length(input);
+
+ switch (version) {
+ case DCRYPT_KEY_VERSION_1:
+ return dcrypt_openssl_load_private_key_dovecot_v1(
+ key_r, len, input, password, key, error_r);
+ case DCRYPT_KEY_VERSION_2:
+ return dcrypt_openssl_load_private_key_dovecot_v2(
+ key_r, len, input, password, key, error_r);
+ case DCRYPT_KEY_VERSION_NA:
+ i_unreached();
+ }
+ return FALSE;
+}
+
+static bool
+dcrypt_openssl_load_public_key_dovecot_v1(struct dcrypt_public_key **key_r,
+ int len, const char **input,
+ const char **error_r)
+{
+ int nid;
+ if (str_to_int(input[1], &nid) != 0) {
+ DCRYPT_SET_ERROR("Corrupted data");
+ return FALSE;
+ }
+
+ EC_KEY *eckey = EC_KEY_new_by_curve_name(nid);
+ if (eckey == NULL) {
+ dcrypt_openssl_error(error_r);
+ return FALSE;
+ }
+
+ EC_KEY_set_asn1_flag(eckey, OPENSSL_EC_NAMED_CURVE);
+ BN_CTX *bnctx = BN_CTX_new();
+
+ EC_POINT *point = EC_POINT_new(EC_KEY_get0_group(eckey));
+ if (bnctx == NULL || point == NULL ||
+ EC_POINT_hex2point(EC_KEY_get0_group(eckey),
+ input[2], point, bnctx) == NULL) {
+ BN_CTX_free(bnctx);
+ EC_KEY_free(eckey);
+ EC_POINT_free(point);
+ dcrypt_openssl_error(error_r);
+ return FALSE;
+ }
+ BN_CTX_free(bnctx);
+
+ EC_KEY_set_public_key(eckey, point);
+ EC_KEY_set_asn1_flag(eckey, OPENSSL_EC_NAMED_CURVE);
+
+ EC_POINT_free(point);
+
+ if (EC_KEY_check_key(eckey) == 1) {
+ EVP_PKEY *key = EVP_PKEY_new();
+ EVP_PKEY_set1_EC_KEY(key, eckey);
+ EC_KEY_free(eckey);
+ /* make sure digest matches */
+ buffer_t *dgst = t_buffer_create(32);
+ struct dcrypt_public_key tmp;
+ i_zero(&tmp);
+ tmp.key = key;
+ dcrypt_openssl_public_key_id_old(&tmp, dgst, NULL);
+ if (strcmp(binary_to_hex(dgst->data, dgst->used),
+ input[len-1]) != 0) {
+ DCRYPT_SET_ERROR("Key id mismatch after load");
+ EVP_PKEY_free(key);
+ return FALSE;
+ }
+ *key_r = i_new(struct dcrypt_public_key, 1);
+ (*key_r)->key = key;
+ (*key_r)->ref++;
+ return TRUE;
+ }
+
+ dcrypt_openssl_error(error_r);
+ return FALSE;
+}
+
+static bool
+dcrypt_openssl_load_public_key_dovecot_v2(struct dcrypt_public_key **key_r,
+ int len, const char **input,
+ const char **error_r)
+{
+ buffer_t tmp;
+ size_t keylen = strlen(input[1])/2;
+ unsigned char keybuf[keylen];
+ const unsigned char *ptr;
+ buffer_create_from_data(&tmp, keybuf, keylen);
+ hex_to_binary(input[1], &tmp);
+ ptr = keybuf;
+
+ EVP_PKEY *pkey = EVP_PKEY_new();
+ if (pkey == NULL || d2i_PUBKEY(&pkey, &ptr, keylen)==NULL) {
+ EVP_PKEY_free(pkey);
+ dcrypt_openssl_error(error_r);
+ return FALSE;
+ }
+
+ /* make sure digest matches */
+ buffer_t *dgst = t_buffer_create(32);
+ struct dcrypt_public_key tmpkey;
+ i_zero(&tmpkey);
+ tmpkey.key = pkey;
+ dcrypt_openssl_public_key_id(&tmpkey, "sha256", dgst, NULL);
+ if (strcmp(binary_to_hex(dgst->data, dgst->used), input[len-1]) != 0) {
+ DCRYPT_SET_ERROR("Key id mismatch after load");
+ EVP_PKEY_free(pkey);
+ return FALSE;
+ }
+
+ *key_r = i_new(struct dcrypt_public_key, 1);
+ (*key_r)->key = pkey;
+ (*key_r)->ref++;
+ return TRUE;
+}
+
+static bool
+dcrypt_openssl_load_public_key_dovecot(struct dcrypt_public_key **key_r,
+ const char *data,
+ enum dcrypt_key_version version,
+ const char **error_r)
+{
+ const char **input = t_strsplit(data, ":\t");
+ size_t len = str_array_length(input);
+
+ switch (version) {
+ case DCRYPT_KEY_VERSION_1:
+ return dcrypt_openssl_load_public_key_dovecot_v1(
+ key_r, len, input, error_r);
+ break;
+ case DCRYPT_KEY_VERSION_2:
+ return dcrypt_openssl_load_public_key_dovecot_v2(
+ key_r, len, input, error_r);
+ break;
+ case DCRYPT_KEY_VERSION_NA:
+ i_unreached();
+ }
+ return FALSE;
+}
+
+static bool
+dcrypt_openssl_encrypt_private_key_dovecot(buffer_t *key, int enctype,
+ const char *cipher,
+ const char *password,
+ struct dcrypt_public_key *enc_key,
+ buffer_t *destination,
+ const char **error_r)
+{
+ bool res;
+ unsigned char *ptr;
+
+ unsigned char salt[8];
+ buffer_t *peer_key = t_buffer_create(128);
+ buffer_t *secret = t_buffer_create(128);
+ cipher = t_str_lcase(cipher);
+
+ str_append(destination, cipher);
+ str_append_c(destination, ':');
+ random_fill(salt, sizeof(salt));
+ binary_to_hex_append(destination, salt, sizeof(salt));
+ buffer_t saltbuf;
+ buffer_create_from_const_data(&saltbuf, salt, sizeof(salt));
+
+ /* so we don't have to make new version if we ever upgrade these */
+ str_append(destination, t_strdup_printf(":%s:%d:",
+ DCRYPT_DOVECOT_KEY_ENCRYPT_HASH,
+ DCRYPT_DOVECOT_KEY_ENCRYPT_ROUNDS));
+
+ if (enctype == DCRYPT_DOVECOT_KEY_ENCRYPT_PK) {
+ if (EVP_PKEY_base_id(enc_key->key) == EVP_PKEY_RSA) {
+ size_t used = buffer_get_used_size(secret);
+ /* peer key, in this case, is encrypted secret,
+ which is 16 bytes of data */
+ ptr = buffer_append_space_unsafe(secret, 16);
+ random_fill(ptr, 16);
+ buffer_set_used_size(secret, used+16);
+ if (!dcrypt_rsa_encrypt(enc_key, secret->data,
+ secret->used, peer_key,
+ DCRYPT_PADDING_RSA_PKCS1_OAEP,
+ error_r)) {
+ return FALSE;
+ }
+ } else if (EVP_PKEY_base_id(enc_key->key) == EVP_PKEY_EC) {
+ /* generate secret by ECDHE */
+ if (!dcrypt_openssl_ecdh_derive_secret_peer(
+ enc_key, peer_key, secret, error_r)) {
+ return FALSE;
+ }
+ } else {
+ /* Loading the key should have failed */
+ i_unreached();
+ }
+ /* add encryption key id, reuse peer_key buffer */
+ } else if (enctype == DCRYPT_DOVECOT_KEY_ENCRYPT_PASSWORD) {
+ str_append(secret, password);
+ }
+
+ /* encrypt key using secret and salt */
+ buffer_t *tmp = t_buffer_create(128);
+ res = dcrypt_openssl_cipher_key_dovecot_v2(cipher,
+ DCRYPT_MODE_ENCRYPT, key, secret, &saltbuf,
+ DCRYPT_DOVECOT_KEY_ENCRYPT_HASH,
+ DCRYPT_DOVECOT_KEY_ENCRYPT_ROUNDS, tmp, error_r);
+ safe_memset(buffer_get_modifiable_data(secret, NULL), 0, secret->used);
+ binary_to_hex_append(destination, tmp->data, tmp->used);
+
+ /* some additional fields or private key version */
+ if (enctype == DCRYPT_DOVECOT_KEY_ENCRYPT_PK) {
+ str_append_c(destination, ':');
+
+ /* for RSA, this is the actual encrypted secret */
+ binary_to_hex_append(destination,
+ peer_key->data, peer_key->used);
+ str_append_c(destination, ':');
+
+ buffer_set_used_size(peer_key, 0);
+ if (!dcrypt_openssl_public_key_id(enc_key, "sha256",
+ peer_key, error_r))
+ return FALSE;
+ binary_to_hex_append(destination,
+ peer_key->data, peer_key->used);
+ }
+ return res;
+}
+
+static bool
+dcrypt_openssl_store_private_key_dovecot(struct dcrypt_private_key *key,
+ const char *cipher,
+ buffer_t *destination,
+ const char *password,
+ struct dcrypt_public_key *enc_key,
+ const char **error_r)
+{
+ size_t dest_used = buffer_get_used_size(destination);
+ const char *cipher2 = NULL;
+ EVP_PKEY *pkey = key->key;
+ char objtxt[OID_TEXT_MAX_LEN];
+ ASN1_OBJECT *obj;
+
+ if (EVP_PKEY_base_id(pkey) == EVP_PKEY_EC) {
+ /* because otherwise we get wrong nid */
+ obj = OBJ_nid2obj(EC_GROUP_get_curve_name(
+ EC_KEY_get0_group(EVP_PKEY_get0_EC_KEY(pkey))));
+ EC_KEY_set_conv_form(EVP_PKEY_get0_EC_KEY(pkey),
+ POINT_CONVERSION_COMPRESSED);
+
+ } else {
+ obj = OBJ_nid2obj(EVP_PKEY_id(pkey));
+ }
+
+ int enctype = DCRYPT_KEY_ENCRYPTION_TYPE_NONE;
+ int len = OBJ_obj2txt(objtxt, sizeof(objtxt), obj, 1);
+ if (len < 1)
+ return dcrypt_openssl_error(error_r);
+ if (len > (int)sizeof(objtxt)) {
+ DCRYPT_SET_ERROR("Object identifier too long");
+ return FALSE;
+ }
+
+ buffer_t *buf = t_buffer_create(256);
+
+ /* convert key to private key value */
+ if (EVP_PKEY_base_id(pkey) == EVP_PKEY_RSA) {
+ unsigned char *ptr;
+ RSA *rsa = EVP_PKEY_get0_RSA(pkey);
+ int len = i2d_RSAPrivateKey(rsa, &ptr);
+ if (len < 1)
+ return dcrypt_openssl_error(error_r);
+ buffer_append(buf, ptr, len);
+ } else if (EVP_PKEY_base_id(pkey) == EVP_PKEY_EC) {
+ unsigned char *ptr;
+ EC_KEY *eckey = EVP_PKEY_get0_EC_KEY(pkey);
+ const BIGNUM *pk = EC_KEY_get0_private_key(eckey);
+ /* serialize to MPI which is portable */
+ int len = BN_bn2mpi(pk, NULL);
+ ptr = buffer_append_space_unsafe(buf, len);
+ BN_bn2mpi(pk, ptr);
+ } else {
+ /* Loading the key should have failed */
+ i_unreached();
+ }
+
+ /* see if we want ECDH based or password based encryption */
+ if (cipher != NULL && strncasecmp(cipher, "ecdh-", 5) == 0) {
+ i_assert(enc_key != NULL);
+ i_assert(password == NULL);
+ enctype = DCRYPT_DOVECOT_KEY_ENCRYPT_PK;
+ cipher2 = cipher+5;
+ } else if (cipher != NULL) {
+ i_assert(enc_key == NULL);
+ i_assert(password != NULL);
+ enctype = DCRYPT_DOVECOT_KEY_ENCRYPT_PASSWORD;
+ cipher2 = cipher;
+ } else if (enctype == DCRYPT_KEY_ENCRYPTION_TYPE_NONE) {
+ i_assert(enc_key == NULL && password == NULL);
+ }
+
+ /* put in OID and encryption type */
+ str_append(destination, t_strdup_printf("2:%s:%d:",
+ objtxt, enctype));
+
+ /* perform encryption if desired */
+ if (enctype != DCRYPT_KEY_ENCRYPTION_TYPE_NONE) {
+ if (!dcrypt_openssl_encrypt_private_key_dovecot(buf,
+ enctype, cipher2, password, enc_key, destination,
+ error_r)) {
+ buffer_set_used_size(destination, dest_used);
+ return FALSE;
+ }
+ } else {
+ binary_to_hex_append(destination, buf->data, buf->used);
+ }
+
+ /* append public key id */
+ str_append_c(destination, ':');
+ buffer_set_used_size(buf, 0);
+ bool res = dcrypt_openssl_private_key_id(key, "sha256", buf, error_r);
+ binary_to_hex_append(destination, buf->data, buf->used);
+
+ if (!res) {
+ /* well, that didn't end well */
+ buffer_set_used_size(destination, dest_used);
+ return FALSE;
+ }
+ return TRUE;
+}
+
+static bool
+dcrypt_openssl_store_public_key_dovecot(struct dcrypt_public_key *key,
+ buffer_t *destination,
+ const char **error_r)
+{
+ EVP_PKEY *pubkey = key->key;
+ unsigned char *tmp = NULL;
+ size_t dest_used = buffer_get_used_size(destination);
+
+ if (EVP_PKEY_base_id(pubkey) == EVP_PKEY_EC)
+ EC_KEY_set_conv_form(EVP_PKEY_get0_EC_KEY(pubkey),
+ POINT_CONVERSION_COMPRESSED);
+ int rv = i2d_PUBKEY(pubkey, &tmp);
+
+ if (tmp == NULL)
+ return dcrypt_openssl_error(error_r);
+
+ /* then store it */
+ str_append_c(destination, '2');
+ str_append_c(destination, ':');
+ binary_to_hex_append(destination, tmp, rv);
+ OPENSSL_free(tmp);
+
+ /* append public key ID */
+ str_append_c(destination, ':');
+
+ buffer_t *buf = t_buffer_create(32);
+ bool res = dcrypt_openssl_public_key_id(key, "sha256", buf, error_r);
+
+ if (!res) {
+ buffer_set_used_size(destination, dest_used);
+ return FALSE;
+ }
+
+ str_append(destination, binary_to_hex(buf->data, buf->used));
+ return TRUE;
+}
+
+static bool
+dcrypt_openssl_load_private_key(struct dcrypt_private_key **key_r,
+ const char *data, const char *password,
+ struct dcrypt_private_key *dec_key,
+ const char **error_r)
+{
+ i_assert(key_r != NULL);
+
+ enum dcrypt_key_format format;
+ enum dcrypt_key_version version;
+ enum dcrypt_key_kind kind;
+ if (!dcrypt_openssl_key_string_get_info(data, &format, &version,
+ &kind, NULL, NULL, NULL, error_r)) {
+ return FALSE;
+ }
+ if (kind != DCRYPT_KEY_KIND_PRIVATE) {
+ DCRYPT_SET_ERROR("key is not private");
+ return FALSE;
+ }
+
+ if (format == DCRYPT_FORMAT_JWK)
+ return dcrypt_openssl_load_private_key_jwk(key_r, data, password,
+ dec_key, error_r);
+
+ if (format == DCRYPT_FORMAT_DOVECOT)
+ return dcrypt_openssl_load_private_key_dovecot(key_r, data,
+ password, dec_key, version, error_r);
+
+ EVP_PKEY *key = NULL, *key2;
+
+ BIO *key_in = BIO_new_mem_buf((void*)data, strlen(data));
+
+ key = EVP_PKEY_new();
+
+ key2 = PEM_read_bio_PrivateKey(key_in, &key, NULL, (void*)password);
+
+ BIO_vfree(key_in);
+
+ if (key2 == NULL) {
+ EVP_PKEY_free(key);
+ return dcrypt_openssl_error(error_r);
+ }
+
+ if (EVP_PKEY_base_id(key) == EVP_PKEY_EC) {
+ EC_KEY_set_asn1_flag(EVP_PKEY_get0_EC_KEY(key),
+ OPENSSL_EC_NAMED_CURVE);
+ }
+
+ *key_r = i_new(struct dcrypt_private_key, 1);
+ (*key_r)->key = key;
+ (*key_r)->ref++;
+
+ return TRUE;
+}
+
+static bool
+dcrypt_openssl_load_public_key(struct dcrypt_public_key **key_r,
+ const char *data, const char **error_r)
+{
+ enum dcrypt_key_format format;
+ enum dcrypt_key_version version;
+ enum dcrypt_key_kind kind;
+ i_assert(key_r != NULL);
+
+ if (!dcrypt_openssl_key_string_get_info(data, &format, &version,
+ &kind, NULL, NULL, NULL,
+ error_r)) {
+ return FALSE;
+ }
+ /* JWK private keys can be loaded as public */
+ if (kind != DCRYPT_KEY_KIND_PUBLIC && format != DCRYPT_FORMAT_JWK) {
+ DCRYPT_SET_ERROR("key is not public");
+ return FALSE;
+ }
+
+ if (format == DCRYPT_FORMAT_JWK)
+ return dcrypt_openssl_load_public_key_jwk(key_r, data, error_r);
+
+ if (format == DCRYPT_FORMAT_DOVECOT)
+ return dcrypt_openssl_load_public_key_dovecot(key_r, data,
+ version, error_r);
+
+ EVP_PKEY *key = NULL;
+ BIO *key_in = BIO_new_mem_buf((void*)data, strlen(data));
+ if (key_in == NULL)
+ return dcrypt_openssl_error(error_r);
+
+ key = PEM_read_bio_PUBKEY(key_in, &key, NULL, NULL);
+ if (BIO_reset(key_in) <= 0)
+ i_unreached();
+ if (key == NULL) { /* ec keys are bother */
+ /* read the header */
+ char buf[27]; /* begin public key */
+ if (BIO_gets(key_in, buf, sizeof(buf)) != 1) {
+ BIO_vfree(key_in);
+ return dcrypt_openssl_error(error_r);
+ }
+ if (strcmp(buf, "-----BEGIN PUBLIC KEY-----") != 0) {
+ DCRYPT_SET_ERROR("Missing public key header");
+ return FALSE;
+ }
+ BIO *b64 = BIO_new(BIO_f_base64());
+ if (b64 == NULL) {
+ BIO_vfree(key_in);
+ return dcrypt_openssl_error(error_r);
+ }
+ EC_KEY *eckey = d2i_EC_PUBKEY_bio(b64, NULL);
+ if (eckey != NULL) {
+ EC_KEY_set_asn1_flag(eckey, OPENSSL_EC_NAMED_CURVE);
+ key = EVP_PKEY_new();
+ if (key != NULL)
+ EVP_PKEY_set1_EC_KEY(key, eckey);
+ EC_KEY_free(eckey);
+ }
+ }
+
+ BIO_vfree(key_in);
+
+ if (key == NULL)
+ return dcrypt_openssl_error(error_r);
+
+ *key_r = i_new(struct dcrypt_public_key, 1);
+ (*key_r)->key = key;
+ (*key_r)->ref++;
+
+ return TRUE;
+}
+
+static bool
+dcrypt_openssl_store_private_key(struct dcrypt_private_key *key,
+ enum dcrypt_key_format format,
+ const char *cipher, buffer_t *destination,
+ const char *password,
+ struct dcrypt_public_key *enc_key,
+ const char **error_r)
+{
+ i_assert(key != NULL && key->key != NULL);
+
+ int ec;
+ if (format == DCRYPT_FORMAT_DOVECOT) {
+ bool ret;
+ ret = dcrypt_openssl_store_private_key_dovecot(
+ key, cipher, destination, password, enc_key, error_r);
+ return ret;
+ }
+
+ EVP_PKEY *pkey = key->key;
+
+ if (format == DCRYPT_FORMAT_JWK) {
+ bool ret;
+ ret = store_jwk_key(pkey, TRUE, key->usage, key->key_id,
+ cipher, password, enc_key,
+ destination, error_r);
+ return ret;
+ }
+
+ if (EVP_PKEY_base_id(pkey) == EVP_PKEY_EC)
+ EC_KEY_set_conv_form(EVP_PKEY_get0_EC_KEY(pkey),
+ POINT_CONVERSION_UNCOMPRESSED);
+
+ BIO *key_out = BIO_new(BIO_s_mem());
+ if (key_out == NULL)
+ return dcrypt_openssl_error(error_r);
+
+ const EVP_CIPHER *algo = NULL;
+ if (cipher != NULL) {
+ algo = EVP_get_cipherbyname(cipher);
+ if (algo == NULL) {
+ DCRYPT_SET_ERROR(t_strdup_printf("Invalid cipher %s",
+ cipher));
+ return FALSE;
+ }
+ }
+
+ ec = PEM_write_bio_PrivateKey(key_out, pkey, algo,
+ NULL, 0, NULL, (void*)password);
+
+ if (BIO_flush(key_out) <= 0)
+ ec = -1;
+
+ if (ec != 1) {
+ BIO_vfree(key_out);
+ return dcrypt_openssl_error(error_r);
+ }
+
+ long bs;
+ char *buf;
+ bs = BIO_get_mem_data(key_out, &buf);
+ buffer_append(destination, buf, bs);
+ BIO_vfree(key_out);
+
+ return TRUE;
+}
+
+static bool
+dcrypt_openssl_store_public_key(struct dcrypt_public_key *key,
+ enum dcrypt_key_format format,
+ buffer_t *destination, const char **error_r)
+{
+ int ec;
+
+ i_assert(key != NULL && key->key != NULL);
+
+ if (format == DCRYPT_FORMAT_DOVECOT) {
+ return dcrypt_openssl_store_public_key_dovecot(key, destination,
+ error_r);
+ }
+
+ EVP_PKEY *pkey = key->key;
+
+ if (format == DCRYPT_FORMAT_JWK) {
+ bool ret;
+ ret = store_jwk_key(pkey, FALSE, key->usage, key->key_id,
+ NULL, NULL, NULL,
+ destination, error_r);
+ return ret;
+ }
+
+ if (EVP_PKEY_base_id(pkey) == EVP_PKEY_EC)
+ EC_KEY_set_conv_form(EVP_PKEY_get0_EC_KEY(pkey),
+ POINT_CONVERSION_UNCOMPRESSED);
+
+ BIO *key_out = BIO_new(BIO_s_mem());
+ if (key_out == NULL)
+ return dcrypt_openssl_error(error_r);
+
+ BIO *b64;
+ if (EVP_PKEY_base_id(pkey) == EVP_PKEY_RSA)
+ ec = PEM_write_bio_PUBKEY(key_out, pkey);
+ else if ((b64 = BIO_new(BIO_f_base64())) == NULL)
+ ec = -1;
+ else {
+ (void)BIO_puts(key_out, "-----BEGIN PUBLIC KEY-----\n");
+ (void)BIO_push(b64, key_out);
+ ec = i2d_EC_PUBKEY_bio(b64, EVP_PKEY_get0_EC_KEY(pkey));
+ if (BIO_flush(b64) <= 0)
+ ec = -1;
+ (void)BIO_pop(b64);
+ BIO_vfree(b64);
+ if (BIO_puts(key_out, "-----END PUBLIC KEY-----") <= 0)
+ ec = -1;
+ }
+
+ if (ec != 1) {
+ BIO_vfree(key_out);
+ return dcrypt_openssl_error(error_r);
+ }
+
+ long bs;
+ char *buf;
+ bs = BIO_get_mem_data(key_out, &buf);
+ buffer_append(destination, buf, bs);
+ BIO_vfree(key_out);
+
+ return TRUE;
+}
+
+static void
+dcrypt_openssl_private_to_public_key(struct dcrypt_private_key *priv_key,
+ struct dcrypt_public_key **pub_key_r)
+{
+ i_assert(priv_key != NULL && pub_key_r != NULL);
+
+ EVP_PKEY *pkey = priv_key->key;
+ EVP_PKEY *pk;
+
+ pk = EVP_PKEY_new();
+ i_assert(pk != NULL); /* we shouldn't get malloc() failures */
+
+ if (EVP_PKEY_base_id(pkey) == EVP_PKEY_RSA)
+ {
+ RSA *rsa = RSAPublicKey_dup(EVP_PKEY_get0_RSA(pkey));
+ EVP_PKEY_set1_RSA(pk, rsa);
+ RSA_free(rsa);
+ } else if (EVP_PKEY_base_id(pkey) == EVP_PKEY_EC) {
+ EC_KEY* eck = EVP_PKEY_get1_EC_KEY(pkey);
+ EC_KEY_set_asn1_flag(eck, OPENSSL_EC_NAMED_CURVE);
+ EVP_PKEY_set1_EC_KEY(pk, eck);
+ EC_KEY_free(eck);
+ } else {
+ /* Loading the key should have failed */
+ i_unreached();
+ }
+
+ *pub_key_r = i_new(struct dcrypt_public_key, 1);
+ (*pub_key_r)->key = pk;
+ (*pub_key_r)->ref++;
+}
+
+static bool
+dcrypt_openssl_key_string_get_info(
+ const char *key_data, enum dcrypt_key_format *format_r,
+ enum dcrypt_key_version *version_r, enum dcrypt_key_kind *kind_r,
+ enum dcrypt_key_encryption_type *encryption_type_r,
+ const char **encryption_key_hash_r, const char **key_hash_r,
+ const char **error_r)
+{
+ enum dcrypt_key_format format = DCRYPT_FORMAT_PEM;
+ enum dcrypt_key_version version = DCRYPT_KEY_VERSION_NA;
+ enum dcrypt_key_encryption_type encryption_type =
+ DCRYPT_KEY_ENCRYPTION_TYPE_NONE;
+ enum dcrypt_key_kind kind = DCRYPT_KEY_KIND_PUBLIC;
+ char *encryption_key_hash = NULL;
+ char *key_hash = NULL;
+
+ i_assert(key_data != NULL);
+
+ /* is it PEM key */
+ if (str_begins(key_data, "-----BEGIN ")) {
+ format = DCRYPT_FORMAT_PEM;
+ version = DCRYPT_KEY_VERSION_NA;
+ key_data += 11;
+ if (str_begins(key_data, "RSA ")) {
+ DCRYPT_SET_ERROR("RSA private key format not supported, convert it to PKEY format with openssl pkey");
+ return FALSE;
+ }
+ if (str_begins(key_data, "ENCRYPTED ")) {
+ encryption_type = DCRYPT_KEY_ENCRYPTION_TYPE_PASSWORD;
+ key_data += 10;
+ }
+ if (str_begins(key_data, "PRIVATE KEY-----"))
+ kind = DCRYPT_KEY_KIND_PRIVATE;
+ else if (str_begins(key_data, "PUBLIC KEY-----"))
+ kind = DCRYPT_KEY_KIND_PUBLIC;
+ else {
+ DCRYPT_SET_ERROR("Unknown/invalid PEM key type");
+ return FALSE;
+ }
+ } else if (*key_data == '{') {
+ /* possibly a JWK key */
+ format = DCRYPT_FORMAT_JWK;
+ version = DCRYPT_KEY_VERSION_NA;
+ struct json_tree *tree;
+ const struct json_tree_node *root, *node;
+ const char *value, *error;
+ if (parse_jwk_key(key_data, &tree, &error) != 0) {
+ DCRYPT_SET_ERROR("Unknown/invalid key data");
+ return FALSE;
+ }
+
+ /* determine key type */
+ root = json_tree_root(tree);
+ if ((node = json_tree_find_key(root, "kty")) == NULL ||
+ (value = json_tree_get_value_str(node)) == NULL) {
+ json_tree_deinit(&tree);
+ DCRYPT_SET_ERROR("Invalid JWK key: Missing kty parameter");
+ return FALSE;
+ } else if (strcmp(value, "RSA") == 0) {
+ if (json_tree_find_key(root, "d") != NULL)
+ kind = DCRYPT_KEY_KIND_PRIVATE;
+ else
+ kind = DCRYPT_KEY_KIND_PUBLIC;
+ } else if (strcmp(value, "EC") == 0) {
+ if (json_tree_find_key(root, "d") != NULL)
+ kind = DCRYPT_KEY_KIND_PRIVATE;
+ else
+ kind = DCRYPT_KEY_KIND_PUBLIC;
+ } else {
+ json_tree_deinit(&tree);
+ DCRYPT_SET_ERROR("Unsupported JWK key type");
+ return FALSE;
+ }
+ json_tree_deinit(&tree);
+ } else {
+ if (str_begins(key_data, "1:")) {
+ DCRYPT_SET_ERROR("Dovecot v1 key format uses tab to separate fields");
+ return FALSE;
+ } else if (str_begins(key_data, "2\t")) {
+ DCRYPT_SET_ERROR("Dovecot v2 key format uses colon to separate fields");
+ return FALSE;
+ }
+ const char **fields = t_strsplit(key_data, ":\t");
+ int nfields = str_array_length(fields);
+
+ if (nfields < 2) {
+ DCRYPT_SET_ERROR("Unknown key format");
+ return FALSE;
+ }
+
+ format = DCRYPT_FORMAT_DOVECOT;
+
+ /* field 1 - version */
+ if (strcmp(fields[0], "1") == 0) {
+ version = DCRYPT_KEY_VERSION_1;
+ if (nfields == 4) {
+ kind = DCRYPT_KEY_KIND_PUBLIC;
+ } else if (nfields == 5 && strcmp(fields[2],"0") == 0) {
+ kind = DCRYPT_KEY_KIND_PRIVATE;
+ encryption_type = DCRYPT_KEY_ENCRYPTION_TYPE_NONE;
+ } else if (nfields == 6 && strcmp(fields[2],"2") == 0) {
+ kind = DCRYPT_KEY_KIND_PRIVATE;
+ encryption_type = DCRYPT_KEY_ENCRYPTION_TYPE_PASSWORD;
+ } else if (nfields == 7 && strcmp(fields[2],"1") == 0) {
+ kind = DCRYPT_KEY_KIND_PRIVATE;
+ encryption_type = DCRYPT_KEY_ENCRYPTION_TYPE_KEY;
+ if (encryption_key_hash_r != NULL)
+ encryption_key_hash = i_strdup(fields[nfields-2]);
+ } else {
+ DCRYPT_SET_ERROR("Invalid dovecot v1 encoding");
+ return FALSE;
+ }
+ } else if (strcmp(fields[0], "2") == 0) {
+ version = DCRYPT_KEY_VERSION_2;
+ if (nfields == 3) {
+ kind = DCRYPT_KEY_KIND_PUBLIC;
+ } else if (nfields == 5 && strcmp(fields[2],"0") == 0) {
+ kind = DCRYPT_KEY_KIND_PRIVATE;
+ encryption_type = DCRYPT_KEY_ENCRYPTION_TYPE_NONE;
+ } else if (nfields == 9 && strcmp(fields[2],"2") == 0) {
+ kind = DCRYPT_KEY_KIND_PRIVATE;
+ encryption_type = DCRYPT_KEY_ENCRYPTION_TYPE_PASSWORD;
+ } else if (nfields == 11 && strcmp(fields[2],"1") == 0) {
+ kind = DCRYPT_KEY_KIND_PRIVATE;
+ encryption_type = DCRYPT_KEY_ENCRYPTION_TYPE_KEY;
+ if (encryption_key_hash_r != NULL)
+ encryption_key_hash = i_strdup(fields[nfields-2]);
+ } else {
+ DCRYPT_SET_ERROR("Invalid dovecot v2 encoding");
+ return FALSE;
+ }
+ } else {
+ DCRYPT_SET_ERROR("Invalid dovecot key version");
+ return FALSE;
+ }
+
+ /* last field is always key hash */
+ if (key_hash_r != NULL)
+ key_hash = i_strdup(fields[nfields-1]);
+ }
+
+ if (format_r != NULL) *format_r = format;
+ if (version_r != NULL) *version_r = version;
+ if (encryption_type_r != NULL) *encryption_type_r = encryption_type;
+ if (encryption_key_hash_r != NULL) {
+ *encryption_key_hash_r = t_strdup(encryption_key_hash);
+ i_free(encryption_key_hash);
+ }
+ if (kind_r != NULL) *kind_r = kind;
+ if (key_hash_r != NULL) {
+ *key_hash_r = t_strdup(key_hash);
+ i_free(key_hash);
+ }
+ return TRUE;
+}
+
+static void dcrypt_openssl_ref_public_key(struct dcrypt_public_key *key)
+{
+ i_assert(key != NULL && key->ref > 0);
+ key->ref++;
+}
+
+static void dcrypt_openssl_ref_private_key(struct dcrypt_private_key *key)
+{
+ i_assert(key != NULL && key->ref > 0);
+ key->ref++;
+}
+
+static void dcrypt_openssl_unref_public_key(struct dcrypt_public_key **key)
+{
+ i_assert(key != NULL);
+ struct dcrypt_public_key *_key = *key;
+ if (_key == NULL)
+ return;
+ i_assert(_key->ref > 0);
+ *key = NULL;
+ if (--_key->ref > 0) return;
+ EVP_PKEY_free(_key->key);
+ i_free(_key->key_id);
+ i_free(_key);
+}
+
+static void dcrypt_openssl_unref_private_key(struct dcrypt_private_key **key)
+{
+ i_assert(key != NULL);
+ struct dcrypt_private_key *_key = *key;
+ if (_key == NULL)
+ return;
+ i_assert(_key->ref > 0);
+ *key = NULL;
+ if (--_key->ref > 0) return;
+ EVP_PKEY_free(_key->key);
+ i_free(_key->key_id);
+ i_free(_key);
+}
+
+static void dcrypt_openssl_unref_keypair(struct dcrypt_keypair *keypair)
+{
+ i_assert(keypair != NULL);
+ dcrypt_openssl_unref_public_key(&keypair->pub);
+ dcrypt_openssl_unref_private_key(&keypair->priv);
+}
+
+static bool
+dcrypt_openssl_rsa_encrypt(struct dcrypt_public_key *key,
+ const unsigned char *data, size_t data_len,
+ buffer_t *result, enum dcrypt_padding padding,
+ const char **error_r)
+{
+ i_assert(key != NULL && key->key != NULL);
+ int ec, pad = dcrypt_openssl_padding_mode(padding, FALSE, error_r);
+ if (pad == -1)
+ return FALSE;
+ EVP_PKEY_CTX *ctx = EVP_PKEY_CTX_new(key->key, NULL);
+ size_t outl = EVP_PKEY_size(key->key);
+ unsigned char buf[outl];
+
+ if (ctx == NULL ||
+ EVP_PKEY_encrypt_init(ctx) < 1 ||
+ EVP_PKEY_CTX_set_rsa_padding(ctx, pad) < 1 ||
+ EVP_PKEY_encrypt(ctx, buf, &outl, data, data_len) < 1) {
+ dcrypt_openssl_error(error_r);
+ ec = -1;
+ } else {
+ buffer_append(result, buf, outl);
+ ec = 0;
+ }
+
+ EVP_PKEY_CTX_free(ctx);
+
+ return ec == 0;
+}
+
+static bool
+dcrypt_openssl_rsa_decrypt(struct dcrypt_private_key *key,
+ const unsigned char *data, size_t data_len,
+ buffer_t *result, enum dcrypt_padding padding,
+ const char **error_r)
+{
+ i_assert(key != NULL && key->key != NULL);
+ int ec, pad = dcrypt_openssl_padding_mode(padding, FALSE, error_r);
+ if (pad == -1)
+ return FALSE;
+ EVP_PKEY_CTX *ctx = EVP_PKEY_CTX_new(key->key, NULL);
+ size_t outl = EVP_PKEY_size(key->key);
+ unsigned char buf[outl];
+
+ if (ctx == NULL ||
+ EVP_PKEY_decrypt_init(ctx) < 1 ||
+ EVP_PKEY_CTX_set_rsa_padding(ctx, pad) < 1 ||
+ EVP_PKEY_decrypt(ctx, buf, &outl, data, data_len) < 1) {
+ dcrypt_openssl_error(error_r);
+ ec = -1;
+ } else {
+ buffer_append(result, buf, outl);
+ ec = 0;
+ }
+
+ EVP_PKEY_CTX_free(ctx);
+
+ return ec == 0;
+}
+
+static const char *
+dcrypt_openssl_oid2name(const unsigned char *oid, size_t oid_len,
+ const char **error_r)
+{
+ const char *name;
+ i_assert(oid != NULL);
+ ASN1_OBJECT *obj = d2i_ASN1_OBJECT(NULL, &oid, oid_len);
+ if (obj == NULL) {
+ dcrypt_openssl_error(error_r);
+ return NULL;
+ }
+ name = OBJ_nid2sn(OBJ_obj2nid(obj));
+ ASN1_OBJECT_free(obj);
+ return name;
+}
+
+static bool
+dcrypt_openssl_name2oid(const char *name, buffer_t *oid, const char **error_r)
+{
+ i_assert(name != NULL);
+ ASN1_OBJECT *obj = OBJ_txt2obj(name, 0);
+ if (obj == NULL)
+ return dcrypt_openssl_error(error_r);
+
+ size_t len = OBJ_length(obj);
+ if (len == 0)
+ {
+ DCRYPT_SET_ERROR("Object has no OID assigned");
+ return FALSE;
+ }
+ len = i2d_ASN1_OBJECT(obj, NULL);
+ unsigned char *bufptr = buffer_append_space_unsafe(oid, len);
+ i2d_ASN1_OBJECT(obj, &bufptr);
+ ASN1_OBJECT_free(obj);
+ if (bufptr != NULL) {
+ return TRUE;
+ }
+ return dcrypt_openssl_error(error_r);
+}
+
+static enum dcrypt_key_type
+dcrypt_openssl_private_key_type(struct dcrypt_private_key *key)
+{
+ i_assert(key != NULL && key->key != NULL);
+ EVP_PKEY *priv = key->key;
+ if (EVP_PKEY_base_id(priv) == EVP_PKEY_RSA) return DCRYPT_KEY_RSA;
+ else if (EVP_PKEY_base_id(priv) == EVP_PKEY_EC) return DCRYPT_KEY_EC;
+ else i_unreached();
+}
+
+static enum dcrypt_key_type
+dcrypt_openssl_public_key_type(struct dcrypt_public_key *key)
+{
+ i_assert(key != NULL && key->key != NULL);
+ EVP_PKEY *pub = key->key;
+ if (EVP_PKEY_base_id(pub) == EVP_PKEY_RSA) return DCRYPT_KEY_RSA;
+ else if (EVP_PKEY_base_id(pub) == EVP_PKEY_EC) return DCRYPT_KEY_EC;
+ else i_unreached();
+}
+
+/** this is the v1 old legacy way of doing key id's **/
+static bool
+dcrypt_openssl_public_key_id_old(struct dcrypt_public_key *key,
+ buffer_t *result, const char **error_r)
+{
+ unsigned char buf[SHA256_DIGEST_LENGTH];
+ i_assert(key != NULL && key->key != NULL);
+ EVP_PKEY *pub = key->key;
+
+ if (EVP_PKEY_base_id(pub) != EVP_PKEY_EC) {
+ DCRYPT_SET_ERROR("Only EC key supported");
+ return FALSE;
+ }
+
+ char *pub_pt_hex = ec_key_get_pub_point_hex(EVP_PKEY_get0_EC_KEY(pub));
+ if (pub_pt_hex == NULL)
+ return dcrypt_openssl_error(error_r);
+ /* digest this */
+ SHA256((const unsigned char*)pub_pt_hex, strlen(pub_pt_hex), buf);
+ buffer_append(result, buf, SHA256_DIGEST_LENGTH);
+ OPENSSL_free(pub_pt_hex);
+ return TRUE;
+}
+
+static bool
+dcrypt_openssl_private_key_id_old(struct dcrypt_private_key *key,
+ buffer_t *result, const char **error_r)
+{
+ unsigned char buf[SHA256_DIGEST_LENGTH];
+ i_assert(key != NULL && key->key != NULL);
+ EVP_PKEY *priv = key->key;
+
+ if (EVP_PKEY_base_id(priv) != EVP_PKEY_EC) {
+ DCRYPT_SET_ERROR("Only EC key supported");
+ return FALSE;
+ }
+
+ char *pub_pt_hex = ec_key_get_pub_point_hex(EVP_PKEY_get0_EC_KEY(priv));
+ if (pub_pt_hex == NULL)
+ return dcrypt_openssl_error(error_r);
+ /* digest this */
+ SHA256((const unsigned char*)pub_pt_hex, strlen(pub_pt_hex), buf);
+ buffer_append(result, buf, SHA256_DIGEST_LENGTH);
+ OPENSSL_free(pub_pt_hex);
+ return TRUE;
+}
+
+/** this is the new which uses H(der formatted public key) **/
+static bool
+dcrypt_openssl_public_key_id_evp(EVP_PKEY *key,
+ const EVP_MD *md, buffer_t *result,
+ const char **error_r)
+{
+ bool res = FALSE;
+ unsigned char buf[EVP_MD_size(md)], *ptr;
+
+ if (EVP_PKEY_base_id(key) == EVP_PKEY_EC) {
+ EC_KEY_set_conv_form(EVP_PKEY_get0_EC_KEY(key),
+ POINT_CONVERSION_COMPRESSED);
+ }
+ BIO *b = BIO_new(BIO_s_mem());
+ if (b == NULL || i2d_PUBKEY_bio(b, key) < 1) {
+ BIO_vfree(b);
+ return dcrypt_openssl_error(error_r);
+ }
+ long len = BIO_get_mem_data(b, &ptr);
+ unsigned int hlen = sizeof(buf);
+ /* then hash it */
+ EVP_MD_CTX *ctx = EVP_MD_CTX_new();
+ if (ctx == NULL ||
+ EVP_DigestInit_ex(ctx, md, NULL) < 1 ||
+ EVP_DigestUpdate(ctx, (const unsigned char*)ptr, len) < 1 ||
+ EVP_DigestFinal_ex(ctx, buf, &hlen) < 1) {
+ res = dcrypt_openssl_error(error_r);
+ } else {
+ buffer_append(result, buf, hlen);
+ res = TRUE;
+ }
+ EVP_MD_CTX_free(ctx);
+ BIO_vfree(b);
+
+ return res;
+}
+
+static bool
+dcrypt_openssl_public_key_id(struct dcrypt_public_key *key,
+ const char *algorithm, buffer_t *result,
+ const char **error_r)
+{
+ const EVP_MD *md = EVP_get_digestbyname(algorithm);
+ i_assert(key != NULL && key->key != NULL);
+ EVP_PKEY *pub = key->key;
+
+ if (md == NULL) {
+ DCRYPT_SET_ERROR(t_strdup_printf("Unknown cipher %s", algorithm));
+ return FALSE;
+ }
+
+ return dcrypt_openssl_public_key_id_evp(pub, md, result, error_r);
+}
+
+static bool
+dcrypt_openssl_private_key_id(struct dcrypt_private_key *key,
+ const char *algorithm, buffer_t *result,
+ const char **error_r)
+{
+ const EVP_MD *md = EVP_get_digestbyname(algorithm);
+ i_assert(key != NULL && key->key != NULL);
+ EVP_PKEY *priv = key->key;
+
+ if (md == NULL) {
+ DCRYPT_SET_ERROR(t_strdup_printf("Unknown cipher %s", algorithm));
+ return FALSE;
+ }
+
+ return dcrypt_openssl_public_key_id_evp(priv, md, result, error_r);
+}
+
+static bool
+dcrypt_openssl_digest(const char *algorithm, const void *data, size_t data_len,
+ buffer_t *digest_r, const char **error_r)
+{
+ bool ret;
+ EVP_MD_CTX *mdctx;
+ const EVP_MD *md = EVP_get_digestbyname(algorithm);
+ if (md == NULL)
+ return dcrypt_openssl_error(error_r);
+ unsigned int md_size = EVP_MD_size(md);
+ if ((mdctx = EVP_MD_CTX_create()) == NULL)
+ return dcrypt_openssl_error(error_r);
+ unsigned char *buf = buffer_append_space_unsafe(digest_r, md_size);
+ if (EVP_DigestInit_ex(mdctx, EVP_sha256(), NULL) != 1 ||
+ EVP_DigestUpdate(mdctx, data, data_len) != 1 ||
+ EVP_DigestFinal_ex(mdctx, buf, &md_size) != 1) {
+ ret = dcrypt_openssl_error(error_r);
+ } else {
+ ret = TRUE;
+ }
+ EVP_MD_CTX_free(mdctx);
+ return ret;
+}
+
+#ifndef HAVE_ECDSA_SIG_GET0
+static void ECDSA_SIG_get0(const ECDSA_SIG *sig, const BIGNUM **pr, const BIGNUM **ps)
+{
+ i_assert(sig != NULL);
+ *pr = sig->r;
+ *ps = sig->s;
+}
+#endif
+#ifndef HAVE_ECDSA_SIG_SET0
+static int ECDSA_SIG_set0(ECDSA_SIG *sig, BIGNUM *r, BIGNUM *s)
+{
+ if (sig == NULL || r == NULL || s == NULL) {
+ ECDSAerr(0, ERR_R_PASSED_NULL_PARAMETER);
+ return 0;
+ }
+
+ BN_free(sig->r);
+ sig->r = r;
+ BN_free(sig->s);
+ sig->s = s;
+
+ return 1;
+}
+#endif
+
+static bool
+dcrypt_openssl_sign_ecdsa(struct dcrypt_private_key *key, const char *algorithm,
+ const void *data, size_t data_len, buffer_t *signature_r,
+ const char **error_r)
+{
+ EVP_PKEY *pkey = key->key;
+ EC_KEY *ec_key = EVP_PKEY_get0_EC_KEY(pkey);
+ bool ret;
+ int rs_len = EC_GROUP_order_bits(EC_KEY_get0_group(ec_key)) / 8;
+
+ /* digest data */
+ buffer_t *digest = t_buffer_create(64);
+ if (!dcrypt_openssl_digest(algorithm, data, data_len, digest, error_r))
+ return FALSE;
+
+ /* sign data */
+ ECDSA_SIG *ec_sig;
+ if ((ec_sig = ECDSA_do_sign(digest->data, digest->used, ec_key)) == NULL)
+ return dcrypt_openssl_error(error_r);
+
+ /* export signature */
+ const BIGNUM *r;
+ const BIGNUM *s;
+
+ ECDSA_SIG_get0(ec_sig, &r, &s);
+
+ int r_len = BN_num_bytes(r);
+ i_assert(rs_len >= r_len);
+
+ /* write r */
+ unsigned char *buf = buffer_append_space_unsafe(signature_r, rs_len);
+ if (BN_bn2bin(r, buf + (rs_len - r_len)) != r_len) {
+ ret = dcrypt_openssl_error(error_r);
+ } else {
+ buf = buffer_append_space_unsafe(signature_r, rs_len);
+ int s_len = BN_num_bytes(s);
+ i_assert(rs_len >= s_len);
+ if (BN_bn2bin(s, buf + (rs_len - s_len)) != s_len) {
+ ret = dcrypt_openssl_error(error_r);
+ } else {
+ ret = TRUE;
+ }
+ }
+
+ ECDSA_SIG_free(ec_sig);
+
+ return ret;
+}
+
+static bool
+dcrypt_openssl_sign(struct dcrypt_private_key *key, const char *algorithm,
+ enum dcrypt_signature_format format,
+ const void *data, size_t data_len, buffer_t *signature_r,
+ enum dcrypt_padding padding, const char **error_r)
+{
+ switch (format) {
+ case DCRYPT_SIGNATURE_FORMAT_DSS:
+ break;
+ case DCRYPT_SIGNATURE_FORMAT_X962:
+ if (EVP_PKEY_base_id(key->key) == EVP_PKEY_RSA) {
+ DCRYPT_SET_ERROR("Format does not support RSA");
+ return FALSE;
+ }
+ return dcrypt_openssl_sign_ecdsa(key, algorithm,
+ data, data_len, signature_r, error_r);
+ default:
+ i_unreached();
+ }
+
+ EVP_PKEY_CTX *pctx = NULL;
+ EVP_MD_CTX *dctx;
+ bool ret;
+ const EVP_MD *md = EVP_get_digestbyname(algorithm);
+ size_t siglen;
+ int pad = dcrypt_openssl_padding_mode(padding, TRUE, error_r);
+
+ if (pad == -1)
+ return FALSE;
+
+ if (md == NULL) {
+ DCRYPT_SET_ERROR(t_strdup_printf("Unknown digest %s", algorithm));
+ return FALSE;
+ }
+
+ dctx = EVP_MD_CTX_create();
+
+ /* NB! Padding is set only on RSA signatures
+ ECDSA signatures use whatever is default */
+ if (EVP_DigestSignInit(dctx, &pctx, md, NULL, key->key) != 1 ||
+ (EVP_PKEY_base_id(key->key) == EVP_PKEY_RSA &&
+ EVP_PKEY_CTX_set_rsa_padding(pctx, pad) != 1) ||
+ EVP_DigestSignUpdate(dctx, data, data_len) != 1 ||
+ EVP_DigestSignFinal(dctx, NULL, &siglen) != 1) {
+ ret = dcrypt_openssl_error(error_r);
+ } else {
+ i_assert(siglen > 0);
+ /* @UNSAFE */
+ unsigned char *buf =
+ buffer_append_space_unsafe(signature_r, siglen);
+ if (EVP_DigestSignFinal(dctx, buf, &siglen) != 1) {
+ ret = dcrypt_openssl_error(error_r);
+ } else {
+ buffer_set_used_size(signature_r, siglen);
+ ret = TRUE;
+ }
+ }
+
+ EVP_MD_CTX_destroy(dctx);
+
+ return ret;
+}
+
+static bool
+dcrypt_openssl_verify_ecdsa(struct dcrypt_public_key *key, const char *algorithm,
+ const void *data, size_t data_len,
+ const unsigned char *signature, size_t signature_len,
+ bool *valid_r, const char **error_r)
+{
+ if ((signature_len % 2) != 0) {
+ DCRYPT_SET_ERROR("Truncated signature");
+ return FALSE;
+ }
+
+ EVP_PKEY *pkey = key->key;
+ EC_KEY *ec_key = EVP_PKEY_get0_EC_KEY(pkey);
+ int ec;
+
+ /* digest data */
+ buffer_t *digest = t_buffer_create(64);
+ if (!dcrypt_openssl_digest(algorithm, data, data_len, digest, error_r))
+ return FALSE;
+
+ BIGNUM *r = BN_new();
+ BIGNUM *s = BN_new();
+ /* attempt to decode BIGNUMs */
+ if (BN_bin2bn(signature, signature_len / 2, r) == NULL) {
+ BN_free(r);
+ BN_free(s);
+ return dcrypt_openssl_error(error_r);
+ }
+ /* then next */
+ if (BN_bin2bn(CONST_PTR_OFFSET(signature, signature_len / 2),
+ signature_len / 2, s) == NULL) {
+ BN_free(r);
+ BN_free(s);
+ return dcrypt_openssl_error(error_r);
+ }
+
+ /* reconstruct signature */
+ ECDSA_SIG *ec_sig = ECDSA_SIG_new();
+ ECDSA_SIG_set0(ec_sig, r, s);
+
+ /* verify it */
+ ec = ECDSA_do_verify(digest->data, digest->used, ec_sig, ec_key);
+ ECDSA_SIG_free(ec_sig);
+
+ if (ec == 1) {
+ *valid_r = TRUE;
+ } else if (ec == 0) {
+ *valid_r = FALSE;
+ } else {
+ return dcrypt_openssl_error(error_r);
+ }
+ return TRUE;
+}
+
+static bool
+dcrypt_openssl_verify(struct dcrypt_public_key *key, const char *algorithm,
+ enum dcrypt_signature_format format,
+ const void *data, size_t data_len,
+ const unsigned char *signature, size_t signature_len,
+ bool *valid_r, enum dcrypt_padding padding,
+ const char **error_r)
+{
+ switch (format) {
+ case DCRYPT_SIGNATURE_FORMAT_DSS:
+ break;
+ case DCRYPT_SIGNATURE_FORMAT_X962:
+ if (EVP_PKEY_base_id(key->key) == EVP_PKEY_RSA) {
+ DCRYPT_SET_ERROR("Format does not support RSA");
+ return FALSE;
+ }
+ return dcrypt_openssl_verify_ecdsa(key, algorithm,
+ data, data_len, signature, signature_len,
+ valid_r, error_r);
+ default:
+ i_unreached();
+ }
+
+ EVP_PKEY_CTX *pctx = NULL;
+ EVP_MD_CTX *dctx;
+ bool ret;
+ const EVP_MD *md = EVP_get_digestbyname(algorithm);
+ int rc, pad = dcrypt_openssl_padding_mode(padding, TRUE, error_r);
+
+ if (pad == -1)
+ return FALSE;
+
+ if (md == NULL) {
+ DCRYPT_SET_ERROR(t_strdup_printf("Unknown digest %s", algorithm));
+ return FALSE;
+ }
+
+ dctx = EVP_MD_CTX_create();
+
+ /* NB! Padding is set only on RSA signatures
+ ECDSA signatures use whatever is default */
+ if (EVP_DigestVerifyInit(dctx, &pctx, md, NULL, key->key) != 1 ||
+ (EVP_PKEY_base_id(key->key) == EVP_PKEY_RSA &&
+ EVP_PKEY_CTX_set_rsa_padding(pctx, pad) != 1) ||
+ EVP_DigestVerifyUpdate(dctx, data, data_len) != 1 ||
+ (rc = EVP_DigestVerifyFinal(dctx, signature, signature_len)) < 0) {
+ ret = dcrypt_openssl_error(error_r);
+ } else {
+ /* return code 1 means valid signature, otherwise invalid */
+ *valid_r = (rc == 1);
+ ret = TRUE;
+ }
+
+ EVP_MD_CTX_destroy(dctx);
+
+ return ret;
+}
+
+static bool
+dcrypt_openssl_key_store_private_raw(struct dcrypt_private_key *key,
+ pool_t pool,
+ enum dcrypt_key_type *type_r,
+ ARRAY_TYPE(dcrypt_raw_key) *keys_r,
+ const char **error_r)
+{
+ i_assert(key != NULL && key->key != NULL);
+ i_assert(array_is_created(keys_r));
+ EVP_PKEY *priv = key->key;
+ ARRAY_TYPE(dcrypt_raw_key) keys;
+ t_array_init(&keys, 2);
+
+ if (EVP_PKEY_base_id(priv) == EVP_PKEY_RSA) {
+ DCRYPT_SET_ERROR("Not implemented");
+ return FALSE;
+ } else if (EVP_PKEY_base_id(priv) == EVP_PKEY_EC) {
+ /* store OID */
+ EC_KEY *key = EVP_PKEY_get0_EC_KEY(priv);
+ EC_KEY_set_conv_form(key, POINT_CONVERSION_UNCOMPRESSED);
+ int nid = EC_GROUP_get_curve_name(EC_KEY_get0_group(key));
+ ASN1_OBJECT *obj = OBJ_nid2obj(nid);
+ int len = OBJ_length(obj);
+ if (len == 0) {
+ DCRYPT_SET_ERROR("Object has no OID assigned");
+ return FALSE;
+ }
+ len = i2d_ASN1_OBJECT(obj, NULL);
+ unsigned char *bufptr = p_malloc(pool, len);
+ struct dcrypt_raw_key *item = array_append_space(&keys);
+ item->parameter = bufptr;
+ item->len = i2d_ASN1_OBJECT(obj, &bufptr);
+ ASN1_OBJECT_free(obj);
+ /* store private key */
+ const BIGNUM *b = EC_KEY_get0_private_key(key);
+ len = BN_num_bytes(b);
+ item = array_append_space(&keys);
+ bufptr = p_malloc(pool, len);
+ if (BN_bn2bin(b, bufptr) < len)
+ return dcrypt_openssl_error(error_r);
+ item->parameter = bufptr;
+ item->len = len;
+ *type_r = DCRYPT_KEY_EC;
+ } else {
+ DCRYPT_SET_ERROR("Key type unsupported");
+ return FALSE;
+ }
+
+ array_append_array(keys_r, &keys);
+ return TRUE;
+}
+
+static bool
+dcrypt_openssl_key_store_public_raw(struct dcrypt_public_key *key,
+ pool_t pool,
+ enum dcrypt_key_type *type_r,
+ ARRAY_TYPE(dcrypt_raw_key) *keys_r,
+ const char **error_r)
+{
+ i_assert(key != NULL && key->key != NULL);
+ EVP_PKEY *pub = key->key;
+ ARRAY_TYPE(dcrypt_raw_key) keys;
+ t_array_init(&keys, 2);
+
+ if (EVP_PKEY_base_id(pub) == EVP_PKEY_RSA) {
+ DCRYPT_SET_ERROR("Not implemented");
+ return FALSE;
+ } else if (EVP_PKEY_base_id(pub) == EVP_PKEY_EC) {
+ /* store OID */
+ EC_KEY *key = EVP_PKEY_get0_EC_KEY(pub);
+ EC_KEY_set_conv_form(key, POINT_CONVERSION_UNCOMPRESSED);
+ int nid = EC_GROUP_get_curve_name(EC_KEY_get0_group(key));
+ ASN1_OBJECT *obj = OBJ_nid2obj(nid);
+ int len = OBJ_length(obj);
+ if (len == 0) {
+ DCRYPT_SET_ERROR("Object has no OID assigned");
+ return FALSE;
+ }
+ len = i2d_ASN1_OBJECT(obj, NULL);
+ unsigned char *bufptr = p_malloc(pool, len);
+ struct dcrypt_raw_key *item = array_append_space(&keys);
+ item->parameter = bufptr;
+ item->len = i2d_ASN1_OBJECT(obj, &bufptr);
+ ASN1_OBJECT_free(obj);
+
+ /* store public key */
+ const EC_POINT *point = EC_KEY_get0_public_key(key);
+ len = EC_POINT_point2oct(EC_KEY_get0_group(key), point,
+ POINT_CONVERSION_UNCOMPRESSED,
+ NULL, 0, NULL);
+ bufptr = p_malloc(pool, len);
+ item = array_append_space(&keys);
+ item->parameter = bufptr;
+ item->len = len;
+ if (EC_POINT_point2oct(EC_KEY_get0_group(key), point,
+ POINT_CONVERSION_UNCOMPRESSED,
+ bufptr, len, NULL) < (unsigned int)len)
+ return dcrypt_openssl_error(error_r);
+ *type_r = DCRYPT_KEY_EC;
+ } else {
+ DCRYPT_SET_ERROR("Key type unsupported");
+ return FALSE;
+ }
+
+ array_append_array(keys_r, &keys);
+
+ return TRUE;
+}
+
+static bool
+dcrypt_openssl_key_load_private_raw(struct dcrypt_private_key **key_r,
+ enum dcrypt_key_type type,
+ const ARRAY_TYPE(dcrypt_raw_key) *keys,
+ const char **error_r)
+{
+ int ec;
+ i_assert(keys != NULL && array_is_created(keys) && array_count(keys) > 1);
+ const struct dcrypt_raw_key *item;
+
+ if (type == DCRYPT_KEY_RSA) {
+ DCRYPT_SET_ERROR("Not implemented");
+ return FALSE;
+ } else if (type == DCRYPT_KEY_EC) {
+ /* get curve */
+ if (array_count(keys) < 2) {
+ DCRYPT_SET_ERROR("Invalid parameters");
+ return FALSE;
+ }
+ item = array_idx(keys, 0);
+ const unsigned char *oid = item->parameter;
+ ASN1_OBJECT *obj = d2i_ASN1_OBJECT(NULL, &oid, item->len);
+ if (obj == NULL)
+ return dcrypt_openssl_error(error_r);
+ int nid = OBJ_obj2nid(obj);
+ ASN1_OBJECT_free(obj);
+
+ /* load private point */
+ item = array_idx(keys, 1);
+ BIGNUM *bn = BN_secure_new();
+ if (BN_bin2bn(item->parameter, item->len, bn) == NULL) {
+ BN_free(bn);
+ return dcrypt_openssl_error(error_r);
+ }
+
+ /* setup a key */
+ EC_KEY *key = EC_KEY_new_by_curve_name(nid);
+ ec = EC_KEY_set_private_key(key, bn);
+ BN_free(bn);
+
+ if (ec != 1) {
+ EC_KEY_free(key);
+ return dcrypt_openssl_error(error_r);
+ }
+
+ /* calculate & assign public key */
+ EC_POINT *pub = EC_POINT_new(EC_KEY_get0_group(key));
+ if (pub == NULL) {
+ EC_KEY_free(key);
+ return dcrypt_openssl_error(error_r);
+ }
+ /* calculate public key */
+ ec = EC_POINT_mul(EC_KEY_get0_group(key), pub,
+ EC_KEY_get0_private_key(key),
+ NULL, NULL, NULL);
+ if (ec == 1)
+ ec = EC_KEY_set_public_key(key, pub);
+ EC_POINT_free(pub);
+
+ /* check the key */
+ if (ec != 1 || EC_KEY_check_key(key) != 1) {
+ EC_KEY_free(key);
+ return dcrypt_openssl_error(error_r);
+ }
+ EC_KEY_set_asn1_flag(key, OPENSSL_EC_NAMED_CURVE);
+
+ EVP_PKEY *pkey = EVP_PKEY_new();
+ EVP_PKEY_set1_EC_KEY(pkey, key);
+ EC_KEY_free(key);
+ *key_r = i_new(struct dcrypt_private_key, 1);
+ (*key_r)->key = pkey;
+ (*key_r)->ref++;
+ return TRUE;
+ } else {
+ DCRYPT_SET_ERROR("Key type unsupported");
+ }
+
+ return FALSE;
+}
+
+static bool
+dcrypt_openssl_key_load_public_raw(struct dcrypt_public_key **key_r,
+ enum dcrypt_key_type type,
+ const ARRAY_TYPE(dcrypt_raw_key) *keys,
+ const char **error_r)
+{
+ int ec;
+ i_assert(keys != NULL && array_is_created(keys) && array_count(keys) > 1);
+ const struct dcrypt_raw_key *item;
+
+ if (type == DCRYPT_KEY_RSA) {
+ DCRYPT_SET_ERROR("Not implemented");
+ return FALSE;
+ } else if (type == DCRYPT_KEY_EC) {
+ /* get curve */
+ if (array_count(keys) < 2) {
+ DCRYPT_SET_ERROR("Invalid parameters");
+ return FALSE;
+ }
+ item = array_idx(keys, 0);
+ const unsigned char *oid = item->parameter;
+ ASN1_OBJECT *obj = d2i_ASN1_OBJECT(NULL, &oid, item->len);
+ if (obj == NULL) {
+ dcrypt_openssl_error(error_r);
+ return FALSE;
+ }
+ int nid = OBJ_obj2nid(obj);
+ ASN1_OBJECT_free(obj);
+
+ /* set group */
+ EC_GROUP *group = EC_GROUP_new_by_curve_name(nid);
+ if (group == NULL) {
+ dcrypt_openssl_error(error_r);
+ return FALSE;
+ }
+
+ /* load point */
+ item = array_idx(keys, 1);
+ EC_POINT *point = EC_POINT_new(group);
+ if (EC_POINT_oct2point(group, point, item->parameter,
+ item->len, NULL) != 1) {
+ EC_POINT_free(point);
+ EC_GROUP_free(group);
+ return dcrypt_openssl_error(error_r);
+ }
+
+ EC_KEY *key = EC_KEY_new();
+ ec = EC_KEY_set_group(key, group);
+ if (ec == 1)
+ ec = EC_KEY_set_public_key(key, point);
+ EC_POINT_free(point);
+ EC_GROUP_free(group);
+
+ if (ec != 1 || EC_KEY_check_key(key) != 1) {
+ EC_KEY_free(key);
+ return dcrypt_openssl_error(error_r);
+ }
+
+ EC_KEY_precompute_mult(key, NULL);
+ EC_KEY_set_asn1_flag(key, OPENSSL_EC_NAMED_CURVE);
+ EVP_PKEY *pkey = EVP_PKEY_new();
+ EVP_PKEY_set1_EC_KEY(pkey, key);
+ EC_KEY_free(key);
+ *key_r = i_new(struct dcrypt_public_key, 1);
+ (*key_r)->key = pkey;
+ (*key_r)->ref++;
+ return TRUE;
+ } else {
+ DCRYPT_SET_ERROR("Key type unsupported");
+ }
+
+ return FALSE;
+}
+
+static bool
+dcrypt_openssl_key_get_curve_public(struct dcrypt_public_key *key,
+ const char **curve_r, const char **error_r)
+{
+ EVP_PKEY *pkey = key->key;
+ char objtxt[OID_TEXT_MAX_LEN];
+
+ if (EVP_PKEY_base_id(pkey) != EVP_PKEY_EC) {
+ DCRYPT_SET_ERROR("Unsupported key type");
+ return FALSE;
+ }
+
+ ASN1_OBJECT *obj = OBJ_nid2obj(EC_GROUP_get_curve_name(
+ EC_KEY_get0_group(EVP_PKEY_get0_EC_KEY(pkey))));
+
+ int len = OBJ_obj2txt(objtxt, sizeof(objtxt), obj, 1);
+ ASN1_OBJECT_free(obj);
+
+ if (len < 1) {
+ return dcrypt_openssl_error(error_r);
+ } else if ((unsigned int)len > sizeof(objtxt)) {
+ DCRYPT_SET_ERROR("Object name too long");
+ return FALSE;
+ }
+
+ *curve_r = t_strndup(objtxt, len);
+ return TRUE;
+}
+
+static const char *
+dcrypt_openssl_key_get_id_public(struct dcrypt_public_key *key)
+{
+ return key->key_id;
+}
+
+static const char *
+dcrypt_openssl_key_get_id_private(struct dcrypt_private_key *key)
+{
+ return key->key_id;
+}
+
+static void
+dcrypt_openssl_key_set_id_public(struct dcrypt_public_key *key, const char *id)
+{
+ i_free(key->key_id);
+ key->key_id = i_strdup_empty(id);
+}
+
+static void
+dcrypt_openssl_key_set_id_private(struct dcrypt_private_key *key, const char *id)
+{
+ i_free(key->key_id);
+ key->key_id = i_strdup_empty(id);
+}
+
+static enum dcrypt_key_usage
+dcrypt_openssl_key_get_usage_public(struct dcrypt_public_key *key)
+{
+ return key->usage;
+}
+
+static enum dcrypt_key_usage
+dcrypt_openssl_key_get_usage_private(struct dcrypt_private_key *key)
+{
+ return key->usage;
+}
+
+static void
+dcrypt_openssl_key_set_usage_public(struct dcrypt_public_key *key,
+ enum dcrypt_key_usage usage)
+{
+ key->usage = usage;
+}
+
+static void
+dcrypt_openssl_key_set_usage_private(struct dcrypt_private_key *key,
+ enum dcrypt_key_usage usage)
+{
+ key->usage = usage;
+}
+
+
+static struct dcrypt_vfs dcrypt_openssl_vfs = {
+ .initialize = dcrypt_openssl_initialize,
+ .ctx_sym_create = dcrypt_openssl_ctx_sym_create,
+ .ctx_sym_destroy = dcrypt_openssl_ctx_sym_destroy,
+ .ctx_sym_set_key = dcrypt_openssl_ctx_sym_set_key,
+ .ctx_sym_set_iv = dcrypt_openssl_ctx_sym_set_iv,
+ .ctx_sym_set_key_iv_random = dcrypt_openssl_ctx_sym_set_key_iv_random,
+ .ctx_sym_set_padding = dcrypt_openssl_ctx_sym_set_padding,
+ .ctx_sym_get_key = dcrypt_openssl_ctx_sym_get_key,
+ .ctx_sym_get_iv = dcrypt_openssl_ctx_sym_get_iv,
+ .ctx_sym_set_aad = dcrypt_openssl_ctx_sym_set_aad,
+ .ctx_sym_get_aad = dcrypt_openssl_ctx_sym_get_aad,
+ .ctx_sym_set_tag = dcrypt_openssl_ctx_sym_set_tag,
+ .ctx_sym_get_tag = dcrypt_openssl_ctx_sym_get_tag,
+ .ctx_sym_get_key_length = dcrypt_openssl_ctx_sym_get_key_length,
+ .ctx_sym_get_iv_length = dcrypt_openssl_ctx_sym_get_iv_length,
+ .ctx_sym_get_block_size = dcrypt_openssl_ctx_sym_get_block_size,
+ .ctx_sym_init = dcrypt_openssl_ctx_sym_init,
+ .ctx_sym_update = dcrypt_openssl_ctx_sym_update,
+ .ctx_sym_final = dcrypt_openssl_ctx_sym_final,
+ .ctx_hmac_create = dcrypt_openssl_ctx_hmac_create,
+ .ctx_hmac_destroy = dcrypt_openssl_ctx_hmac_destroy,
+ .ctx_hmac_set_key = dcrypt_openssl_ctx_hmac_set_key,
+ .ctx_hmac_set_key_random = dcrypt_openssl_ctx_hmac_set_key_random,
+ .ctx_hmac_get_digest_length = dcrypt_openssl_ctx_hmac_get_digest_length,
+ .ctx_hmac_get_key = dcrypt_openssl_ctx_hmac_get_key,
+ .ctx_hmac_init = dcrypt_openssl_ctx_hmac_init,
+ .ctx_hmac_update = dcrypt_openssl_ctx_hmac_update,
+ .ctx_hmac_final = dcrypt_openssl_ctx_hmac_final,
+ .ecdh_derive_secret_local = dcrypt_openssl_ecdh_derive_secret_local,
+ .ecdh_derive_secret_peer = dcrypt_openssl_ecdh_derive_secret_peer,
+ .pbkdf2 = dcrypt_openssl_pbkdf2,
+ .generate_keypair = dcrypt_openssl_generate_keypair,
+ .load_private_key = dcrypt_openssl_load_private_key,
+ .load_public_key = dcrypt_openssl_load_public_key,
+ .store_private_key = dcrypt_openssl_store_private_key,
+ .store_public_key = dcrypt_openssl_store_public_key,
+ .private_to_public_key = dcrypt_openssl_private_to_public_key,
+ .key_string_get_info = dcrypt_openssl_key_string_get_info,
+ .unref_keypair = dcrypt_openssl_unref_keypair,
+ .unref_public_key = dcrypt_openssl_unref_public_key,
+ .unref_private_key = dcrypt_openssl_unref_private_key,
+ .ref_public_key = dcrypt_openssl_ref_public_key,
+ .ref_private_key = dcrypt_openssl_ref_private_key,
+ .rsa_encrypt = dcrypt_openssl_rsa_encrypt,
+ .rsa_decrypt = dcrypt_openssl_rsa_decrypt,
+ .oid2name = dcrypt_openssl_oid2name,
+ .name2oid = dcrypt_openssl_name2oid,
+ .private_key_type = dcrypt_openssl_private_key_type,
+ .public_key_type = dcrypt_openssl_public_key_type,
+ .public_key_id = dcrypt_openssl_public_key_id,
+ .public_key_id_old = dcrypt_openssl_public_key_id_old,
+ .private_key_id = dcrypt_openssl_private_key_id,
+ .private_key_id_old = dcrypt_openssl_private_key_id_old,
+ .key_store_private_raw = dcrypt_openssl_key_store_private_raw,
+ .key_store_public_raw = dcrypt_openssl_key_store_public_raw,
+ .key_load_private_raw = dcrypt_openssl_key_load_private_raw,
+ .key_load_public_raw = dcrypt_openssl_key_load_public_raw,
+ .key_get_curve_public = dcrypt_openssl_key_get_curve_public,
+ .key_get_id_public = dcrypt_openssl_key_get_id_public,
+ .key_get_id_private = dcrypt_openssl_key_get_id_private,
+ .key_set_id_public = dcrypt_openssl_key_set_id_public,
+ .key_set_id_private = dcrypt_openssl_key_set_id_private,
+ .key_get_usage_public = dcrypt_openssl_key_get_usage_public,
+ .key_get_usage_private = dcrypt_openssl_key_get_usage_private,
+ .key_set_usage_public = dcrypt_openssl_key_set_usage_public,
+ .key_set_usage_private = dcrypt_openssl_key_set_usage_private,
+ .sign = dcrypt_openssl_sign,
+ .verify = dcrypt_openssl_verify,
+ .ecdh_derive_secret = dcrypt_openssl_ecdh_derive_secret,
+};
+
+void dcrypt_openssl_init(struct module *module ATTR_UNUSED)
+{
+ dovecot_openssl_common_global_ref();
+ dcrypt_set_vfs(&dcrypt_openssl_vfs);
+}
+
+void dcrypt_openssl_deinit(void)
+{
+ dovecot_openssl_common_global_unref();
+}
diff --git a/src/lib-dcrypt/dcrypt-private.h b/src/lib-dcrypt/dcrypt-private.h
new file mode 100644
index 0000000..13da99e
--- /dev/null
+++ b/src/lib-dcrypt/dcrypt-private.h
@@ -0,0 +1,211 @@
+#ifndef DCRYPT_PRIVATE_H
+#define DCRYPT_PRIVATE_H
+
+#define DCRYPT_DOVECOT_KEY_ENCRYPT_HASH "sha256"
+#define DCRYPT_DOVECOT_KEY_ENCRYPT_ROUNDS 2048
+
+#define DCRYPT_DOVECOT_KEY_ENCRYPT_NONE 0
+#define DCRYPT_DOVECOT_KEY_ENCRYPT_PK 1
+#define DCRYPT_DOVECOT_KEY_ENCRYPT_PASSWORD 2
+
+struct dcrypt_vfs {
+ bool (*initialize)(const struct dcrypt_settings *set,
+ const char **error_r);
+
+ bool (*ctx_sym_create)(const char *algorithm,
+ enum dcrypt_sym_mode mode,
+ struct dcrypt_context_symmetric **ctx_r,
+ const char **error_r);
+ void (*ctx_sym_destroy)(struct dcrypt_context_symmetric **ctx);
+
+ void (*ctx_sym_set_key)(struct dcrypt_context_symmetric *ctx,
+ const unsigned char *key, size_t key_len);
+ void (*ctx_sym_set_iv)(struct dcrypt_context_symmetric *ctx,
+ const unsigned char *iv, size_t iv_len);
+ void (*ctx_sym_set_key_iv_random)(struct dcrypt_context_symmetric *ctx);
+
+ void (*ctx_sym_set_padding)(struct dcrypt_context_symmetric *ctx,
+ bool padding);
+
+ bool (*ctx_sym_get_key)(struct dcrypt_context_symmetric *ctx,
+ buffer_t *key);
+ bool (*ctx_sym_get_iv)(struct dcrypt_context_symmetric *ctx,
+ buffer_t *iv);
+
+ void (*ctx_sym_set_aad)(struct dcrypt_context_symmetric *ctx,
+ const unsigned char *aad, size_t aad_len);
+ bool (*ctx_sym_get_aad)(struct dcrypt_context_symmetric *ctx,
+ buffer_t *aad);
+ void (*ctx_sym_set_tag)(struct dcrypt_context_symmetric *ctx,
+ const unsigned char *tag, size_t tag_len);
+ bool (*ctx_sym_get_tag)(struct dcrypt_context_symmetric *ctx,
+ buffer_t *tag);
+
+ unsigned int (*ctx_sym_get_key_length)(
+ struct dcrypt_context_symmetric *ctx);
+ unsigned int (*ctx_sym_get_iv_length)(
+ struct dcrypt_context_symmetric *ctx);
+ unsigned int (*ctx_sym_get_block_size)(
+ struct dcrypt_context_symmetric *ctx);
+
+ bool (*ctx_sym_init)(struct dcrypt_context_symmetric *ctx,
+ const char **error_r);
+ bool (*ctx_sym_update)(struct dcrypt_context_symmetric *ctx,
+ const unsigned char *data, size_t data_len,
+ buffer_t *result, const char **error_r);
+ bool (*ctx_sym_final)(struct dcrypt_context_symmetric *ctx,
+ buffer_t *result, const char **error_r);
+
+ bool (*ctx_hmac_create)(const char *algorithm,
+ struct dcrypt_context_hmac **ctx_r,
+ const char **error_r);
+ void (*ctx_hmac_destroy)(struct dcrypt_context_hmac **ctx);
+
+ void (*ctx_hmac_set_key)(struct dcrypt_context_hmac *ctx,
+ const unsigned char *key, size_t key_len);
+ bool (*ctx_hmac_get_key)(struct dcrypt_context_hmac *ctx,
+ buffer_t *key);
+ unsigned int (*ctx_hmac_get_digest_length)(
+ struct dcrypt_context_hmac *ctx);
+ void (*ctx_hmac_set_key_random)(struct dcrypt_context_hmac *ctx);
+
+ bool (*ctx_hmac_init)(struct dcrypt_context_hmac *ctx,
+ const char **error_r);
+ bool (*ctx_hmac_update)(struct dcrypt_context_hmac *ctx,
+ const unsigned char *data, size_t data_len,
+ const char **error_r);
+ bool (*ctx_hmac_final)(struct dcrypt_context_hmac *ctx,
+ buffer_t *result, const char **error_r);
+
+ bool (*ecdh_derive_secret_local)(struct dcrypt_private_key *local_key,
+ buffer_t *R, buffer_t *S,
+ const char **error_r);
+ bool (*ecdh_derive_secret_peer)(struct dcrypt_public_key *peer_key,
+ buffer_t *R, buffer_t *S,
+ const char **error_r);
+ bool (*pbkdf2)(const unsigned char *password, size_t password_len,
+ const unsigned char *salt, size_t salt_len,
+ const char *hash, unsigned int rounds,
+ buffer_t *result, unsigned int result_len,
+ const char **error_r);
+
+ bool (*generate_keypair)(struct dcrypt_keypair *pair_r,
+ enum dcrypt_key_type kind, unsigned int bits,
+ const char *curve, const char **error_r);
+
+ bool (*load_private_key)(struct dcrypt_private_key **key_r,
+ const char *data, const char *password,
+ struct dcrypt_private_key *dec_key,
+ const char **error_r);
+ bool (*load_public_key)(struct dcrypt_public_key **key_r,
+ const char *data, const char **error_r);
+
+ bool (*store_private_key)(struct dcrypt_private_key *key,
+ enum dcrypt_key_format format,
+ const char *cipher, buffer_t *destination,
+ const char *password,
+ struct dcrypt_public_key *enc_key,
+ const char **error_r);
+ bool (*store_public_key)(struct dcrypt_public_key *key,
+ enum dcrypt_key_format format,
+ buffer_t *destination, const char **error_r);
+
+ void (*private_to_public_key)(struct dcrypt_private_key *priv_key,
+ struct dcrypt_public_key **pub_key_r);
+
+ bool (*key_string_get_info)(
+ const char *key_data, enum dcrypt_key_format *format_r,
+ enum dcrypt_key_version *version_r,
+ enum dcrypt_key_kind *kind_r,
+ enum dcrypt_key_encryption_type *encryption_type_r,
+ const char **encryption_key_hash_r, const char **key_hash_r,
+ const char **error_r);
+
+ void (*unref_keypair)(struct dcrypt_keypair *keypair);
+ void (*unref_public_key)(struct dcrypt_public_key **key);
+ void (*unref_private_key)(struct dcrypt_private_key **key);
+ void (*ref_public_key)(struct dcrypt_public_key *key);
+ void (*ref_private_key)(struct dcrypt_private_key *key);
+
+ bool (*rsa_encrypt)(struct dcrypt_public_key *key,
+ const unsigned char *data, size_t data_len,
+ buffer_t *result, enum dcrypt_padding padding,
+ const char **error_r);
+ bool (*rsa_decrypt)(struct dcrypt_private_key *key,
+ const unsigned char *data, size_t data_len,
+ buffer_t *result, enum dcrypt_padding padding,
+ const char **error_r);
+
+ const char *(*oid2name)(const unsigned char *oid,
+ size_t oid_len, const char **error_r);
+ bool (*name2oid)(const char *name, buffer_t *oid,
+ const char **error_r);
+
+ enum dcrypt_key_type (*private_key_type)(struct dcrypt_private_key *key);
+ enum dcrypt_key_type (*public_key_type)(struct dcrypt_public_key *key);
+ bool (*public_key_id)(struct dcrypt_public_key *key,
+ const char *algorithm, buffer_t *result,
+ const char **error_r);
+ bool (*public_key_id_old)(struct dcrypt_public_key *key,
+ buffer_t *result, const char **error_r);
+ bool (*private_key_id)(struct dcrypt_private_key *key,
+ const char *algorithm, buffer_t *result,
+ const char **error_r);
+ bool (*private_key_id_old)(struct dcrypt_private_key *key,
+ buffer_t *result, const char **error_r);
+ bool (*key_store_private_raw)(struct dcrypt_private_key *key,
+ pool_t pool,
+ enum dcrypt_key_type *key_type_r,
+ ARRAY_TYPE(dcrypt_raw_key) *keys_r,
+ const char **error_r);
+ bool (*key_store_public_raw)(struct dcrypt_public_key *key,
+ pool_t pool,
+ enum dcrypt_key_type *key_type_r,
+ ARRAY_TYPE(dcrypt_raw_key) *keys_r,
+ const char **error_r);
+ bool (*key_load_private_raw)(struct dcrypt_private_key **key_r,
+ enum dcrypt_key_type key_type,
+ const ARRAY_TYPE(dcrypt_raw_key) *keys,
+ const char **error_r);
+ bool (*key_load_public_raw)(struct dcrypt_public_key **key_r,
+ enum dcrypt_key_type key_type,
+ const ARRAY_TYPE(dcrypt_raw_key) *keys,
+ const char **error_r);
+ bool (*key_get_curve_public)(struct dcrypt_public_key *key,
+ const char **curve_r, const char **error_r);
+ const char *(*key_get_id_public)(struct dcrypt_public_key *key);
+ const char *(*key_get_id_private)(struct dcrypt_private_key *key);
+ void (*key_set_id_public)(struct dcrypt_public_key *key, const char *id);
+ void (*key_set_id_private)(struct dcrypt_private_key *key, const char *id);
+ enum dcrypt_key_usage (*key_get_usage_public)(struct dcrypt_public_key *key);
+ enum dcrypt_key_usage (*key_get_usage_private)(struct dcrypt_private_key *key);
+ void (*key_set_usage_public)(struct dcrypt_public_key *key,
+ enum dcrypt_key_usage usage);
+ void (*key_set_usage_private)(struct dcrypt_private_key *key,
+ enum dcrypt_key_usage usage);
+ bool (*sign)(struct dcrypt_private_key *key, const char *algorithm,
+ enum dcrypt_signature_format format,
+ const void *data, size_t data_len, buffer_t *signature_r,
+ enum dcrypt_padding padding, const char **error_r);
+ bool (*verify)(struct dcrypt_public_key *key, const char *algorithm,
+ enum dcrypt_signature_format format,
+ const void *data, size_t data_len,
+ const unsigned char *signature, size_t signature_len,
+ bool *valid_r, enum dcrypt_padding padding,
+ const char **error_r);
+ bool (*ecdh_derive_secret)(struct dcrypt_private_key *priv_key,
+ struct dcrypt_public_key *pub_key,
+ buffer_t *shared_secret, const char **error_r);
+};
+
+void dcrypt_set_vfs(struct dcrypt_vfs *vfs);
+
+void dcrypt_openssl_init(struct module *module ATTR_UNUSED);
+void dcrypt_gnutls_init(struct module *module ATTR_UNUSED);
+void dcrypt_openssl_deinit(void);
+void dcrypt_gnutls_deinit(void);
+
+int parse_jwk_key(const char *key_data, struct json_tree **tree_r,
+ const char **error_r);
+
+#endif
diff --git a/src/lib-dcrypt/dcrypt.c b/src/lib-dcrypt/dcrypt.c
new file mode 100644
index 0000000..9916754
--- /dev/null
+++ b/src/lib-dcrypt/dcrypt.c
@@ -0,0 +1,660 @@
+/* Copyright (c) 2016-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "module-dir.h"
+#include "dcrypt.h"
+#include "istream.h"
+#include "json-tree.h"
+#include "dcrypt-private.h"
+
+static struct module *dcrypt_module = NULL;
+static struct dcrypt_vfs *dcrypt_vfs = NULL;
+static const struct dcrypt_settings dcrypt_default_set = {
+ .module_dir = DCRYPT_MODULE_DIR,
+};
+
+bool dcrypt_initialize(const char *backend, const struct dcrypt_settings *set,
+ const char **error_r)
+{
+ struct module_dir_load_settings mod_set;
+ const char *error;
+
+ if (dcrypt_vfs != NULL) {
+ return TRUE;
+ }
+ if (backend == NULL) backend = "openssl"; /* default for now */
+ if (set == NULL)
+ set = &dcrypt_default_set;
+
+ const char *implementation = t_strconcat("dcrypt_",backend,NULL);
+
+ i_zero(&mod_set);
+ mod_set.abi_version = DOVECOT_ABI_VERSION;
+ mod_set.require_init_funcs = TRUE;
+ if (module_dir_try_load_missing(&dcrypt_module, set->module_dir,
+ implementation, &mod_set, &error) < 0) {
+ if (error_r != NULL)
+ *error_r = error;
+ return FALSE;
+ }
+ module_dir_init(dcrypt_module);
+ i_assert(dcrypt_vfs != NULL);
+ if (dcrypt_vfs->initialize != NULL) {
+ if (!dcrypt_vfs->initialize(set, error_r)) {
+ dcrypt_deinitialize();
+ return FALSE;
+ }
+ }
+ /* Destroy SSL module after(most of) the others. Especially lib-fs
+ backends may still want to access SSL module in their own
+ atexit-callbacks. */
+ lib_atexit_priority(dcrypt_deinitialize, LIB_ATEXIT_PRIORITY_LOW);
+ return TRUE;
+}
+
+void dcrypt_deinitialize(void)
+{
+ module_dir_unload(&dcrypt_module);
+ dcrypt_vfs = NULL;
+}
+
+void dcrypt_set_vfs(struct dcrypt_vfs *vfs)
+{
+ dcrypt_vfs = vfs;
+}
+
+bool dcrypt_is_initialized(void)
+{
+ return dcrypt_vfs != NULL;
+}
+
+bool dcrypt_ctx_sym_create(const char *algorithm, enum dcrypt_sym_mode mode,
+ struct dcrypt_context_symmetric **ctx_r,
+ const char **error_r)
+{
+ i_assert(dcrypt_vfs != NULL);
+ return dcrypt_vfs->ctx_sym_create(algorithm, mode, ctx_r, error_r);
+}
+
+void dcrypt_ctx_sym_destroy(struct dcrypt_context_symmetric **ctx)
+{
+ i_assert(dcrypt_vfs != NULL);
+ dcrypt_vfs->ctx_sym_destroy(ctx);
+}
+
+void dcrypt_ctx_sym_set_key(struct dcrypt_context_symmetric *ctx,
+ const unsigned char *key, size_t key_len)
+{
+ i_assert(dcrypt_vfs != NULL);
+ dcrypt_vfs->ctx_sym_set_key(ctx, key, key_len);
+}
+
+void dcrypt_ctx_sym_set_iv(struct dcrypt_context_symmetric *ctx,
+ const unsigned char *iv, size_t iv_len)
+{
+ i_assert(dcrypt_vfs != NULL);
+ dcrypt_vfs->ctx_sym_set_iv(ctx, iv, iv_len);
+}
+
+void dcrypt_ctx_sym_set_key_iv_random(struct dcrypt_context_symmetric *ctx)
+{
+ i_assert(dcrypt_vfs != NULL);
+ dcrypt_vfs->ctx_sym_set_key_iv_random(ctx);
+}
+
+bool dcrypt_ctx_sym_get_key(struct dcrypt_context_symmetric *ctx, buffer_t *key)
+{
+ i_assert(dcrypt_vfs != NULL);
+ return dcrypt_vfs->ctx_sym_get_key(ctx, key);
+}
+bool dcrypt_ctx_sym_get_iv(struct dcrypt_context_symmetric *ctx, buffer_t *iv)
+{
+ i_assert(dcrypt_vfs != NULL);
+ return dcrypt_vfs->ctx_sym_get_iv(ctx, iv);
+}
+
+unsigned int dcrypt_ctx_sym_get_key_length(struct dcrypt_context_symmetric *ctx)
+{
+ i_assert(dcrypt_vfs != NULL);
+ return dcrypt_vfs->ctx_sym_get_key_length(ctx);
+}
+
+unsigned int dcrypt_ctx_sym_get_iv_length(struct dcrypt_context_symmetric *ctx)
+{
+ i_assert(dcrypt_vfs != NULL);
+ return dcrypt_vfs->ctx_sym_get_iv_length(ctx);
+}
+
+void dcrypt_ctx_sym_set_aad(struct dcrypt_context_symmetric *ctx,
+ const unsigned char *aad, size_t aad_len)
+{
+ i_assert(dcrypt_vfs != NULL);
+ dcrypt_vfs->ctx_sym_set_aad(ctx, aad, aad_len);
+}
+
+bool dcrypt_ctx_sym_get_aad(struct dcrypt_context_symmetric *ctx, buffer_t *aad)
+{
+ i_assert(dcrypt_vfs != NULL);
+ return dcrypt_vfs->ctx_sym_get_aad(ctx, aad);
+}
+
+void dcrypt_ctx_sym_set_tag(struct dcrypt_context_symmetric *ctx,
+ const unsigned char *tag, size_t tag_len)
+{
+ i_assert(dcrypt_vfs != NULL);
+ dcrypt_vfs->ctx_sym_set_tag(ctx, tag, tag_len);
+}
+
+bool dcrypt_ctx_sym_get_tag(struct dcrypt_context_symmetric *ctx, buffer_t *tag)
+{
+ i_assert(dcrypt_vfs != NULL);
+ return dcrypt_vfs->ctx_sym_get_tag(ctx, tag);
+}
+
+unsigned int dcrypt_ctx_sym_get_block_size(struct dcrypt_context_symmetric *ctx)
+{
+ i_assert(dcrypt_vfs != NULL);
+ return dcrypt_vfs->ctx_sym_get_block_size(ctx);
+}
+
+bool dcrypt_ctx_sym_init(struct dcrypt_context_symmetric *ctx,
+ const char **error_r)
+{
+ i_assert(dcrypt_vfs != NULL);
+ return dcrypt_vfs->ctx_sym_init(ctx, error_r);
+}
+
+bool dcrypt_ctx_sym_update(struct dcrypt_context_symmetric *ctx,
+ const unsigned char *data,
+ size_t data_len, buffer_t *result,
+ const char **error_r)
+{
+ i_assert(dcrypt_vfs != NULL);
+ return dcrypt_vfs->ctx_sym_update(ctx, data, data_len, result, error_r);
+}
+
+bool dcrypt_ctx_sym_final(struct dcrypt_context_symmetric *ctx,
+ buffer_t *result, const char **error_r)
+{
+ i_assert(dcrypt_vfs != NULL);
+ return dcrypt_vfs->ctx_sym_final(ctx, result, error_r);
+}
+
+void dcrypt_ctx_sym_set_padding(struct dcrypt_context_symmetric *ctx,
+ bool padding)
+{
+ i_assert(dcrypt_vfs != NULL);
+ dcrypt_vfs->ctx_sym_set_padding(ctx, padding);
+}
+
+bool dcrypt_ctx_hmac_create(const char *algorithm,
+ struct dcrypt_context_hmac **ctx_r,
+ const char **error_r)
+{
+ i_assert(dcrypt_vfs != NULL);
+ return dcrypt_vfs->ctx_hmac_create(algorithm, ctx_r, error_r);
+}
+
+void dcrypt_ctx_hmac_destroy(struct dcrypt_context_hmac **ctx)
+{
+ i_assert(dcrypt_vfs != NULL);
+ dcrypt_vfs->ctx_hmac_destroy(ctx);
+}
+
+void dcrypt_ctx_hmac_set_key(struct dcrypt_context_hmac *ctx,
+ const unsigned char *key, size_t key_len)
+{
+ i_assert(dcrypt_vfs != NULL);
+ dcrypt_vfs->ctx_hmac_set_key(ctx, key, key_len);
+}
+
+bool dcrypt_ctx_hmac_get_key(struct dcrypt_context_hmac *ctx, buffer_t *key)
+{
+ i_assert(dcrypt_vfs != NULL);
+ return dcrypt_vfs->ctx_hmac_get_key(ctx, key);
+}
+
+void dcrypt_ctx_hmac_set_key_random(struct dcrypt_context_hmac *ctx)
+{
+ i_assert(dcrypt_vfs != NULL);
+ dcrypt_vfs->ctx_hmac_set_key_random(ctx);
+}
+
+unsigned int dcrypt_ctx_hmac_get_digest_length(struct dcrypt_context_hmac *ctx)
+{
+ i_assert(dcrypt_vfs != NULL);
+ return dcrypt_vfs->ctx_hmac_get_digest_length(ctx);
+}
+
+bool dcrypt_ctx_hmac_init(struct dcrypt_context_hmac *ctx, const char **error_r)
+{
+ i_assert(dcrypt_vfs != NULL);
+ return dcrypt_vfs->ctx_hmac_init(ctx, error_r);
+}
+
+bool dcrypt_ctx_hmac_update(struct dcrypt_context_hmac *ctx,
+ const unsigned char *data, size_t data_len,
+ const char **error_r)
+{
+ i_assert(dcrypt_vfs != NULL);
+ return dcrypt_vfs->ctx_hmac_update(ctx, data, data_len, error_r);
+}
+
+bool dcrypt_ctx_hmac_final(struct dcrypt_context_hmac *ctx, buffer_t *result,
+ const char **error_r)
+{
+ i_assert(dcrypt_vfs != NULL);
+ return dcrypt_vfs->ctx_hmac_final(ctx, result, error_r);
+}
+
+bool dcrypt_ecdh_derive_secret(struct dcrypt_private_key *local_key,
+ struct dcrypt_public_key *pub_key,
+ buffer_t *shared_secret,
+ const char **error_r)
+{
+ i_assert(dcrypt_vfs != NULL);
+ if (dcrypt_vfs->ecdh_derive_secret == NULL) {
+ *error_r = "Not implemented";
+ return FALSE;
+ }
+ return dcrypt_vfs->ecdh_derive_secret(local_key, pub_key, shared_secret,
+ error_r);
+}
+
+bool dcrypt_ecdh_derive_secret_local(struct dcrypt_private_key *local_key,
+ buffer_t *R, buffer_t *S,
+ const char **error_r)
+{
+ i_assert(dcrypt_vfs != NULL);
+ return dcrypt_vfs->ecdh_derive_secret_local(local_key, R, S, error_r);
+}
+bool dcrypt_ecdh_derive_secret_peer(struct dcrypt_public_key *peer_key,
+ buffer_t *R, buffer_t *S,
+ const char **error_r)
+{
+ i_assert(dcrypt_vfs != NULL);
+ return dcrypt_vfs->ecdh_derive_secret_peer(peer_key, R, S, error_r);
+}
+
+bool dcrypt_pbkdf2(const unsigned char *password, size_t password_len,
+ const unsigned char *salt, size_t salt_len,
+ const char *hash, unsigned int rounds, buffer_t *result,
+ unsigned int result_len, const char **error_r)
+{
+ i_assert(dcrypt_vfs != NULL);
+ return dcrypt_vfs->pbkdf2(password, password_len, salt, salt_len,
+ hash, rounds, result, result_len, error_r);
+}
+
+bool dcrypt_keypair_generate(struct dcrypt_keypair *pair_r,
+ enum dcrypt_key_type kind, unsigned int bits,
+ const char *curve, const char **error_r)
+{
+ i_assert(dcrypt_vfs != NULL);
+ i_zero(pair_r);
+ return dcrypt_vfs->generate_keypair(pair_r, kind, bits, curve, error_r);
+}
+
+bool dcrypt_key_load_private(struct dcrypt_private_key **key_r,
+ const char *data, const char *password,
+ struct dcrypt_private_key *dec_key,
+ const char **error_r)
+{
+ i_assert(dcrypt_vfs != NULL);
+ return dcrypt_vfs->load_private_key(key_r, data, password,
+ dec_key, error_r);
+}
+
+bool dcrypt_key_load_public(struct dcrypt_public_key **key_r,
+ const char *data, const char **error_r)
+{
+ i_assert(dcrypt_vfs != NULL);
+ return dcrypt_vfs->load_public_key(key_r, data, error_r);
+}
+
+bool dcrypt_key_store_private(struct dcrypt_private_key *key,
+ enum dcrypt_key_format format,
+ const char *cipher, buffer_t *destination,
+ const char *password,
+ struct dcrypt_public_key *enc_key,
+ const char **error_r)
+{
+ i_assert(dcrypt_vfs != NULL);
+ return dcrypt_vfs->store_private_key(key, format, cipher,
+ destination, password, enc_key,
+ error_r);
+}
+bool dcrypt_key_store_public(struct dcrypt_public_key *key,
+ enum dcrypt_key_format format,
+ buffer_t *destination, const char **error_r)
+{
+ i_assert(dcrypt_vfs != NULL);
+ return dcrypt_vfs->store_public_key(key, format, destination, error_r);
+}
+
+void dcrypt_key_convert_private_to_public(struct dcrypt_private_key *priv_key,
+ struct dcrypt_public_key **pub_key_r)
+{
+ i_assert(dcrypt_vfs != NULL);
+ dcrypt_vfs->private_to_public_key(priv_key, pub_key_r);
+}
+
+bool dcrypt_key_string_get_info(const char *key_data,
+ enum dcrypt_key_format *format_r,
+ enum dcrypt_key_version *version_r,
+ enum dcrypt_key_kind *kind_r,
+ enum dcrypt_key_encryption_type *encryption_type_r,
+ const char **encryption_key_hash_r,
+ const char **key_hash_r, const char **error_r)
+{
+ i_assert(dcrypt_vfs != NULL);
+ return dcrypt_vfs->
+ key_string_get_info(key_data, format_r, version_r, kind_r,
+ encryption_type_r, encryption_key_hash_r,
+ key_hash_r, error_r);
+}
+
+enum dcrypt_key_type dcrypt_key_type_private(struct dcrypt_private_key *key)
+{
+ i_assert(dcrypt_vfs != NULL);
+ return dcrypt_vfs->private_key_type(key);
+}
+
+enum dcrypt_key_type dcrypt_key_type_public(struct dcrypt_public_key *key)
+{
+ i_assert(dcrypt_vfs != NULL);
+ return dcrypt_vfs->public_key_type(key);
+}
+
+bool dcrypt_key_id_public(struct dcrypt_public_key *key, const char *algorithm,
+ buffer_t *result, const char **error_r)
+{
+ i_assert(dcrypt_vfs != NULL);
+ return dcrypt_vfs->public_key_id(key, algorithm, result, error_r);
+}
+
+bool dcrypt_key_id_public_old(struct dcrypt_public_key *key, buffer_t *result,
+ const char **error_r)
+{
+ i_assert(dcrypt_vfs != NULL);
+ return dcrypt_vfs->public_key_id_old(key, result, error_r);
+}
+
+bool dcrypt_key_id_private(struct dcrypt_private_key *key,
+ const char *algorithm, buffer_t *result,
+ const char **error_r)
+{
+ i_assert(dcrypt_vfs != NULL);
+ return dcrypt_vfs->private_key_id(key, algorithm, result, error_r);
+}
+
+bool dcrypt_key_id_private_old(struct dcrypt_private_key *key, buffer_t *result,
+ const char **error_r)
+{
+ i_assert(dcrypt_vfs != NULL);
+ return dcrypt_vfs->private_key_id_old(key, result, error_r);
+}
+
+void dcrypt_keypair_unref(struct dcrypt_keypair *keypair)
+{
+ i_assert(dcrypt_vfs != NULL);
+ dcrypt_vfs->unref_keypair(keypair);
+}
+
+void dcrypt_key_ref_public(struct dcrypt_public_key *key)
+{
+ i_assert(dcrypt_vfs != NULL);
+ dcrypt_vfs->ref_public_key(key);
+}
+
+void dcrypt_key_ref_private(struct dcrypt_private_key *key)
+{
+ i_assert(dcrypt_vfs != NULL);
+ dcrypt_vfs->ref_private_key(key);
+}
+
+void dcrypt_key_unref_public(struct dcrypt_public_key **key)
+{
+ i_assert(dcrypt_vfs != NULL);
+ dcrypt_vfs->unref_public_key(key);
+}
+
+void dcrypt_key_unref_private(struct dcrypt_private_key **key)
+{
+ i_assert(dcrypt_vfs != NULL);
+ dcrypt_vfs->unref_private_key(key);
+}
+
+bool dcrypt_rsa_encrypt(struct dcrypt_public_key *key,
+ const unsigned char *data, size_t data_len,
+ buffer_t *result, enum dcrypt_padding padding,
+ const char **error_r)
+{
+ i_assert(dcrypt_vfs != NULL);
+ return dcrypt_vfs->rsa_encrypt(key, data, data_len, result,
+ padding, error_r);
+}
+
+bool dcrypt_rsa_decrypt(struct dcrypt_private_key *key,
+ const unsigned char *data, size_t data_len,
+ buffer_t *result, enum dcrypt_padding padding,
+ const char **error_r)
+{
+ i_assert(dcrypt_vfs != NULL);
+ return dcrypt_vfs->rsa_decrypt(key, data, data_len, result,
+ padding, error_r);
+}
+
+const char *dcrypt_oid2name(const unsigned char *oid, size_t oid_len,
+ const char **error_r)
+{
+ i_assert(dcrypt_vfs != NULL);
+ return dcrypt_vfs->oid2name(oid, oid_len, error_r);
+}
+
+bool dcrypt_name2oid(const char *name, buffer_t *oid, const char **error_r)
+{
+ i_assert(dcrypt_vfs != NULL);
+ return dcrypt_vfs->name2oid(name, oid, error_r);
+}
+
+bool dcrypt_key_store_private_raw(struct dcrypt_private_key *key,
+ pool_t pool,
+ enum dcrypt_key_type *key_type_r,
+ ARRAY_TYPE(dcrypt_raw_key) *keys_r,
+ const char **error_r)
+{
+ i_assert(dcrypt_vfs != NULL);
+ if (dcrypt_vfs->key_store_private_raw == NULL) {
+ *error_r = "Not implemented";
+ return FALSE;
+ }
+ return dcrypt_vfs->key_store_private_raw(key, pool, key_type_r, keys_r,
+ error_r);
+}
+
+bool dcrypt_key_store_public_raw(struct dcrypt_public_key *key,
+ pool_t pool,
+ enum dcrypt_key_type *key_type_r,
+ ARRAY_TYPE(dcrypt_raw_key) *keys_r,
+ const char **error_r)
+{
+ i_assert(dcrypt_vfs != NULL);
+ if (dcrypt_vfs->key_store_public_raw == NULL) {
+ *error_r = "Not implemented";
+ return FALSE;
+ }
+ return dcrypt_vfs->key_store_public_raw(key, pool, key_type_r, keys_r,
+ error_r);
+}
+
+bool dcrypt_key_load_private_raw(struct dcrypt_private_key **key_r,
+ enum dcrypt_key_type key_type,
+ const ARRAY_TYPE(dcrypt_raw_key) *keys,
+ const char **error_r)
+{
+ i_assert(dcrypt_vfs != NULL);
+ if (dcrypt_vfs->key_load_private_raw == NULL) {
+ *error_r = "Not implemented";
+ return FALSE;
+ }
+ return dcrypt_vfs->key_load_private_raw(key_r, key_type, keys,
+ error_r);
+}
+
+bool dcrypt_key_load_public_raw(struct dcrypt_public_key **key_r,
+ enum dcrypt_key_type key_type,
+ const ARRAY_TYPE(dcrypt_raw_key) *keys,
+ const char **error_r)
+{
+ i_assert(dcrypt_vfs != NULL);
+ if (dcrypt_vfs->key_load_public_raw == NULL) {
+ *error_r = "Not implemented";
+ return FALSE;
+ }
+ return dcrypt_vfs->key_load_public_raw(key_r, key_type, keys,
+ error_r);
+}
+
+bool dcrypt_key_get_curve_public(struct dcrypt_public_key *key,
+ const char **curve_r, const char **error_r)
+{
+ i_assert(dcrypt_vfs != NULL);
+ if (dcrypt_vfs->key_get_curve_public == NULL) {
+ *error_r = "Not implemented";
+ return FALSE;
+ }
+ return dcrypt_vfs->key_get_curve_public(key, curve_r, error_r);
+}
+
+const char *dcrypt_key_get_id_public(struct dcrypt_public_key *key)
+{
+ i_assert(dcrypt_vfs != NULL);
+ if (dcrypt_vfs->key_get_id_public == NULL)
+ return NULL;
+ return dcrypt_vfs->key_get_id_public(key);
+}
+
+const char *dcrypt_key_get_id_private(struct dcrypt_private_key *key)
+{
+ i_assert(dcrypt_vfs != NULL);
+ if (dcrypt_vfs->key_get_id_private == NULL)
+ return NULL;
+ return dcrypt_vfs->key_get_id_private(key);
+}
+
+void dcrypt_key_set_id_public(struct dcrypt_public_key *key, const char *id)
+{
+ i_assert(dcrypt_vfs != NULL);
+ if (dcrypt_vfs->key_set_id_public == NULL)
+ return;
+ dcrypt_vfs->key_set_id_public(key, id);
+}
+
+void dcrypt_key_set_id_private(struct dcrypt_private_key *key, const char *id)
+{
+ i_assert(dcrypt_vfs != NULL);
+ if (dcrypt_vfs->key_set_id_private == NULL)
+ return;
+ dcrypt_vfs->key_set_id_private(key, id);
+}
+
+enum dcrypt_key_usage dcrypt_key_get_usage_public(struct dcrypt_public_key *key)
+{
+ i_assert(dcrypt_vfs != NULL);
+ if (dcrypt_vfs->key_get_usage_public == NULL)
+ return DCRYPT_KEY_USAGE_NONE;
+ return dcrypt_vfs->key_get_usage_public(key);
+}
+
+enum dcrypt_key_usage dcrypt_key_get_usage_private(struct dcrypt_private_key *key)
+{
+ i_assert(dcrypt_vfs != NULL);
+ if (dcrypt_vfs->key_get_usage_private == NULL)
+ return DCRYPT_KEY_USAGE_NONE;
+ return dcrypt_vfs->key_get_usage_private(key);
+}
+
+void dcrypt_key_set_usage_public(struct dcrypt_public_key *key,
+ enum dcrypt_key_usage usage)
+{
+ i_assert(dcrypt_vfs != NULL);
+ if (dcrypt_vfs->key_set_usage_public == NULL)
+ return;
+ dcrypt_vfs->key_set_usage_public(key, usage);
+}
+
+void dcrypt_key_set_usage_private(struct dcrypt_private_key *key,
+ enum dcrypt_key_usage usage)
+{
+ i_assert(dcrypt_vfs != NULL);
+ if (dcrypt_vfs->key_set_usage_private == NULL)
+ return;
+ dcrypt_vfs->key_set_usage_private(key, usage);
+}
+
+bool dcrypt_sign(struct dcrypt_private_key *key, const char *algorithm,
+ enum dcrypt_signature_format format,
+ const void *data, size_t data_len, buffer_t *signature_r,
+ enum dcrypt_padding padding, const char **error_r)
+{
+ i_assert(dcrypt_vfs != NULL);
+
+ if (dcrypt_vfs->sign == NULL) {
+ *error_r = "Not implemented";
+ return FALSE;
+ }
+
+ return dcrypt_vfs->sign(key, algorithm, format, data, data_len,
+ signature_r, padding, error_r);
+}
+
+bool dcrypt_verify(struct dcrypt_public_key *key, const char *algorithm,
+ enum dcrypt_signature_format format,
+ const void *data, size_t data_len,
+ const unsigned char *signature, size_t signature_len,
+ bool *valid_r, enum dcrypt_padding padding,
+ const char **error_r)
+{
+ i_assert(dcrypt_vfs != NULL);
+
+ if (dcrypt_vfs->verify == NULL) {
+ *error_r = "Not implemented";
+ return FALSE;
+ }
+
+ return dcrypt_vfs->verify(key, algorithm, format, data, data_len,
+ signature, signature_len,
+ valid_r, padding, error_r);
+}
+
+int parse_jwk_key(const char *key_data, struct json_tree **tree_r,
+ const char **error_r)
+{
+ struct istream *is = i_stream_create_from_data(key_data, strlen(key_data));
+ struct json_parser *parser = json_parser_init(is);
+ struct json_tree *tree = json_tree_init();
+ const char *error;
+ enum json_type type;
+ const char *value;
+ int ret;
+
+ i_stream_unref(&is);
+
+ while ((ret = json_parse_next(parser, &type, &value)) == 1)
+ json_tree_append(tree, type, value);
+
+ i_assert(ret == -1);
+
+ if (json_parser_deinit(&parser, &error) != 0) {
+ json_tree_deinit(&tree);
+ *error_r = error;
+ if (error == NULL)
+ *error_r = "Truncated JSON";
+ return -1;
+ }
+
+ *tree_r = tree;
+
+ return 0;
+}
diff --git a/src/lib-dcrypt/dcrypt.h b/src/lib-dcrypt/dcrypt.h
new file mode 100644
index 0000000..0e8a9aa
--- /dev/null
+++ b/src/lib-dcrypt/dcrypt.h
@@ -0,0 +1,409 @@
+#ifndef DCRYPT_H
+#define DCRYPT_H 1
+#include "array.h"
+
+struct dcrypt_context_symmetric;
+struct dcrypt_context_hmac;
+struct dcrypt_public_key;
+struct dcrypt_private_key;
+
+struct dcrypt_keypair {
+ struct dcrypt_public_key *pub;
+ struct dcrypt_private_key *priv;
+};
+
+enum dcrypt_sym_mode {
+ DCRYPT_MODE_ENCRYPT,
+ DCRYPT_MODE_DECRYPT
+};
+
+enum dcrypt_key_type {
+ DCRYPT_KEY_RSA = 0x1,
+ DCRYPT_KEY_EC = 0x2
+};
+
+/**
+ * dovecot key format:
+ * version version-specific data
+ * v1: version tab nid tab raw ec private key (in hex)
+ * v2: version colon algorithm oid colon private-or-public-key-only (in hex)
+ */
+enum dcrypt_key_format {
+ DCRYPT_FORMAT_PEM,
+ DCRYPT_FORMAT_DOVECOT,
+ DCRYPT_FORMAT_JWK, /* JSON Web Key (JWK) [RFC7517] */
+};
+
+enum dcrypt_key_encryption_type {
+ DCRYPT_KEY_ENCRYPTION_TYPE_NONE,
+ DCRYPT_KEY_ENCRYPTION_TYPE_KEY,
+ DCRYPT_KEY_ENCRYPTION_TYPE_PASSWORD
+};
+
+enum dcrypt_key_version {
+ DCRYPT_KEY_VERSION_1,
+ DCRYPT_KEY_VERSION_2,
+ DCRYPT_KEY_VERSION_NA /* not applicable, PEM key */
+};
+
+enum dcrypt_key_kind {
+ DCRYPT_KEY_KIND_PUBLIC,
+ DCRYPT_KEY_KIND_PRIVATE
+};
+
+enum dcrypt_key_usage {
+ DCRYPT_KEY_USAGE_NONE,
+ DCRYPT_KEY_USAGE_ENCRYPT,
+ DCRYPT_KEY_USAGE_SIGN,
+};
+
+enum dcrypt_signature_format {
+ DCRYPT_SIGNATURE_FORMAT_DSS,
+ DCRYPT_SIGNATURE_FORMAT_X962,
+};
+
+/* this parameter makes sense with RSA only
+ default for RSA means either PSS (sign/verify)
+ or OAEP (encrypt/decrypt).
+ for ECDSA default can be used.
+*/
+enum dcrypt_padding {
+ DCRYPT_PADDING_DEFAULT,
+ DCRYPT_PADDING_RSA_PKCS1_PSS,
+ DCRYPT_PADDING_RSA_PKCS1_OAEP,
+ DCRYPT_PADDING_RSA_PKCS1, /* for compatibility use only */
+ DCRYPT_PADDING_RSA_NO,
+};
+
+struct dcrypt_settings {
+ /* OpenSSL engine to use */
+ const char *crypto_device;
+ /* Look for backends in this directory */
+ const char *module_dir;
+};
+
+struct dcrypt_raw_key {
+ const void *parameter;
+ size_t len;
+};
+
+ARRAY_DEFINE_TYPE(dcrypt_raw_key, struct dcrypt_raw_key);
+
+/**
+ * load and initialize dcrypt backend, use either openssl or gnutls
+ */
+bool dcrypt_initialize(const char *backend, const struct dcrypt_settings *set,
+ const char **error_r);
+
+/**
+ * Returns TRUE if dcrypt has been initialized.
+ */
+bool dcrypt_is_initialized(void);
+
+/**
+ * deinitialize dcrypt.
+ *
+ * NOTE: Do not call this function if you are going to use dcrypt later on.
+ * Deinitializing the library using this will not allow it to be reinitialized
+ * when using OpenSSL.
+ */
+void dcrypt_deinitialize(void);
+
+/**
+ * create symmetric context
+ */
+bool dcrypt_ctx_sym_create(const char *algorithm, enum dcrypt_sym_mode mode,
+ struct dcrypt_context_symmetric **ctx_r,
+ const char **error_r);
+
+/**
+ * destroy symmetric context and free memory
+ */
+void dcrypt_ctx_sym_destroy(struct dcrypt_context_symmetric **ctx);
+
+/**
+ * key and IV manipulation functions
+ */
+void dcrypt_ctx_sym_set_key(struct dcrypt_context_symmetric *ctx,
+ const unsigned char *key, size_t key_len);
+void dcrypt_ctx_sym_set_iv(struct dcrypt_context_symmetric *ctx,
+ const unsigned char *iv, size_t iv_len);
+void dcrypt_ctx_sym_set_key_iv_random(struct dcrypt_context_symmetric *ctx);
+bool dcrypt_ctx_sym_get_key(struct dcrypt_context_symmetric *ctx, buffer_t *key);
+bool dcrypt_ctx_sym_get_iv(struct dcrypt_context_symmetric *ctx, buffer_t *iv);
+
+/**
+ * turn padding on/off (default: on)
+ */
+void dcrypt_ctx_sym_set_padding(struct dcrypt_context_symmetric *ctx,
+ bool padding);
+
+/**
+ * authentication data manipulation (use with GCM only)
+ */
+void dcrypt_ctx_sym_set_aad(struct dcrypt_context_symmetric *ctx,
+ const unsigned char *aad, size_t aad_len);
+bool dcrypt_ctx_sym_get_aad(struct dcrypt_context_symmetric *ctx,
+ buffer_t *aad);
+/**
+ * result tag from aead (use with GCM only)
+ */
+void dcrypt_ctx_sym_set_tag(struct dcrypt_context_symmetric *ctx,
+ const unsigned char *tag, size_t tag_len);
+bool dcrypt_ctx_sym_get_tag(struct dcrypt_context_symmetric *ctx,
+ buffer_t *tag);
+
+/* get various lengths */
+unsigned int dcrypt_ctx_sym_get_key_length(struct dcrypt_context_symmetric *ctx);
+unsigned int dcrypt_ctx_sym_get_iv_length(struct dcrypt_context_symmetric *ctx);
+unsigned int dcrypt_ctx_sym_get_block_size(struct dcrypt_context_symmetric *ctx);
+
+/**
+ * initialize crypto
+ */
+bool dcrypt_ctx_sym_init(struct dcrypt_context_symmetric *ctx,
+ const char **error_r);
+
+/**
+ * update with data
+ */
+bool dcrypt_ctx_sym_update(struct dcrypt_context_symmetric *ctx,
+ const unsigned char *data, size_t data_len,
+ buffer_t *result, const char **error_r);
+
+/**
+ * perform final step (may or may not emit data)
+ */
+bool dcrypt_ctx_sym_final(struct dcrypt_context_symmetric *ctx,
+ buffer_t *result, const char **error_r);
+
+/**
+ * create HMAC context, algorithm is digest algorithm
+ */
+bool dcrypt_ctx_hmac_create(const char *algorithm,
+ struct dcrypt_context_hmac **ctx_r,
+ const char **error_r);
+/**
+ * destroy HMAC context and free memory
+ */
+void dcrypt_ctx_hmac_destroy(struct dcrypt_context_hmac **ctx);
+
+/**
+ * hmac key manipulation
+ */
+void dcrypt_ctx_hmac_set_key(struct dcrypt_context_hmac *ctx,
+ const unsigned char *key, size_t key_len);
+bool dcrypt_ctx_hmac_get_key(struct dcrypt_context_hmac *ctx, buffer_t *key);
+void dcrypt_ctx_hmac_set_key_random(struct dcrypt_context_hmac *ctx);
+
+/**
+ * get digest length for HMAC
+ */
+unsigned int dcrypt_ctx_hmac_get_digest_length(struct dcrypt_context_hmac *ctx);
+
+/**
+ * initialize hmac
+ */
+bool dcrypt_ctx_hmac_init(struct dcrypt_context_hmac *ctx,
+ const char **error_r);
+
+/**
+ * update hmac context with data
+ */
+bool dcrypt_ctx_hmac_update(struct dcrypt_context_hmac *ctx,
+ const unsigned char *data, size_t data_len,
+ const char **error_r);
+
+/**
+ * perform final rounds and retrieve result
+ */
+bool dcrypt_ctx_hmac_final(struct dcrypt_context_hmac *ctx, buffer_t *result,
+ const char **error_r);
+
+/**
+ * Elliptic Curve based Diffie-Heffman shared secret derivation */
+bool dcrypt_ecdh_derive_secret(struct dcrypt_private_key *priv_key,
+ struct dcrypt_public_key *pub_key,
+ buffer_t *shared_secret,
+ const char **error_r);
+/**
+ * Helpers for DCRYPT file format */
+bool dcrypt_ecdh_derive_secret_local(struct dcrypt_private_key *local_key,
+ buffer_t *R, buffer_t *S,
+ const char **error_r);
+bool dcrypt_ecdh_derive_secret_peer(struct dcrypt_public_key *peer_key,
+ buffer_t *R, buffer_t *S,
+ const char **error_r);
+
+/** Signature functions
+ algorithm is name of digest algorithm to use, such as SHA256.
+
+ both RSA and EC keys are supported.
+*/
+
+/* returns false on error, true on success */
+bool dcrypt_sign(struct dcrypt_private_key *key, const char *algorithm,
+ enum dcrypt_signature_format format,
+ const void *data, size_t data_len, buffer_t *signature_r,
+ enum dcrypt_padding padding, const char **error_r);
+
+/* check valid_r for signature validity
+ false return means it wasn't able to verify it for other reasons */
+bool dcrypt_verify(struct dcrypt_public_key *key, const char *algorithm,
+ enum dcrypt_signature_format format,
+ const void *data, size_t data_len,
+ const unsigned char *signature, size_t signature_len,
+ bool *valid_r, enum dcrypt_padding padding,
+ const char **error_r);
+
+/**
+ * generate cryptographic data from password and salt. Use 1000-10000 for rounds.
+ */
+bool dcrypt_pbkdf2(const unsigned char *password, size_t password_len,
+ const unsigned char *salt, size_t salt_len,
+ const char *hash, unsigned int rounds,
+ buffer_t *result, unsigned int result_len,
+ const char **error_r);
+
+bool dcrypt_keypair_generate(struct dcrypt_keypair *pair_r,
+ enum dcrypt_key_type kind, unsigned int bits,
+ const char *curve, const char **error_r);
+
+/**
+ * load loads key structure from external format.
+ * store stores key structure into external format.
+ *
+ * you can provide either PASSWORD or ENC_KEY, not both.
+ */
+bool dcrypt_key_load_private(struct dcrypt_private_key **key_r,
+ const char *data, const char *password,
+ struct dcrypt_private_key *dec_key,
+ const char **error_r);
+
+bool dcrypt_key_load_public(struct dcrypt_public_key **key_r,
+ const char *data, const char **error_r);
+
+/**
+ * When encrypting with public key, the cipher parameter here must begin with
+ * ecdh-, for example ecdh-aes-256-ctr. An example of a valid cipher for
+ * encrypting with password would be aes-256-ctr.
+ */
+bool dcrypt_key_store_private(struct dcrypt_private_key *key,
+ enum dcrypt_key_format format, const char *cipher,
+ buffer_t *destination, const char *password,
+ struct dcrypt_public_key *enc_key,
+ const char **error_r);
+
+bool dcrypt_key_store_public(struct dcrypt_public_key *key,
+ enum dcrypt_key_format format,
+ buffer_t *destination, const char **error_r);
+
+void dcrypt_key_convert_private_to_public(struct dcrypt_private_key *priv_key,
+ struct dcrypt_public_key **pub_key_r);
+
+void dcrypt_keypair_unref(struct dcrypt_keypair *keypair);
+void dcrypt_key_ref_public(struct dcrypt_public_key *key);
+void dcrypt_key_ref_private(struct dcrypt_private_key *key);
+void dcrypt_key_unref_public(struct dcrypt_public_key **key);
+void dcrypt_key_unref_private(struct dcrypt_private_key **key);
+
+enum dcrypt_key_type dcrypt_key_type_private(struct dcrypt_private_key *key);
+enum dcrypt_key_type dcrypt_key_type_public(struct dcrypt_public_key *key);
+/* return digest of key */
+bool dcrypt_key_id_public(struct dcrypt_public_key *key, const char *algorithm,
+ buffer_t *result, const char **error_r);
+/* return SHA1 sum of key */
+bool dcrypt_key_id_public_old(struct dcrypt_public_key *key, buffer_t *result,
+ const char **error_r);
+/* return digest of key */
+bool dcrypt_key_id_private(struct dcrypt_private_key *key,
+ const char *algorithm, buffer_t *result,
+ const char **error_r);
+/* return SHA1 sum of key */
+bool dcrypt_key_id_private_old(struct dcrypt_private_key *key,
+ buffer_t *result, const char **error_r);
+
+/* return raw private key:
+ Only ECC supported currently
+
+ returns OID bytes and private key in bigendian bytes
+*/
+bool dcrypt_key_store_private_raw(struct dcrypt_private_key *key,
+ pool_t pool,
+ enum dcrypt_key_type *key_type_r,
+ ARRAY_TYPE(dcrypt_raw_key) *keys_r,
+ const char **error_r);
+
+/* return raw public key
+ Only ECC supported currently
+
+ returns OID bytes and public key in compressed form (z||x)
+*/
+bool dcrypt_key_store_public_raw(struct dcrypt_public_key *key,
+ pool_t pool,
+ enum dcrypt_key_type *key_type_r,
+ ARRAY_TYPE(dcrypt_raw_key) *keys_r,
+ const char **error_r);
+
+/* load raw private key:
+ Only ECC supported currently
+
+ expects OID bytes and private key in bigendian bytes
+*/
+bool dcrypt_key_load_private_raw(struct dcrypt_private_key **key_r,
+ enum dcrypt_key_type key_type,
+ const ARRAY_TYPE(dcrypt_raw_key) *keys,
+ const char **error_r);
+
+/* load raw public key
+ Only ECC supported currently
+
+ expects OID bytes and public key bytes.
+*/
+bool dcrypt_key_load_public_raw(struct dcrypt_public_key **key_r,
+ enum dcrypt_key_type key_type,
+ const ARRAY_TYPE(dcrypt_raw_key) *keys,
+ const char **error_r);
+
+/* for ECC only - return textual name or OID of used curve */
+bool dcrypt_key_get_curve_public(struct dcrypt_public_key *key,
+ const char **curve_r, const char **error_r);
+
+bool dcrypt_key_string_get_info(const char *key_data,
+ enum dcrypt_key_format *format_r,
+ enum dcrypt_key_version *version_r,
+ enum dcrypt_key_kind *kind_r,
+ enum dcrypt_key_encryption_type *encryption_type_r,
+ const char **encryption_key_hash_r,
+ const char **key_hash_r, const char **error_r);
+
+/* Get/Set key identifier, this is optional opaque string identifying the key. */
+const char *dcrypt_key_get_id_public(struct dcrypt_public_key *key);
+const char *dcrypt_key_get_id_private(struct dcrypt_private_key *key);
+void dcrypt_key_set_id_public(struct dcrypt_public_key *key, const char *id);
+void dcrypt_key_set_id_private(struct dcrypt_private_key *key, const char *id);
+
+/* Get/Set key usage, optional. Defaults to NONE */
+enum dcrypt_key_usage dcrypt_key_get_usage_public(struct dcrypt_public_key *key);
+enum dcrypt_key_usage dcrypt_key_get_usage_private(struct dcrypt_private_key *key);
+void dcrypt_key_set_usage_public(struct dcrypt_public_key *key,
+ enum dcrypt_key_usage usage);
+void dcrypt_key_set_usage_private(struct dcrypt_private_key *key,
+ enum dcrypt_key_usage usage);
+
+/* RSA stuff */
+bool dcrypt_rsa_encrypt(struct dcrypt_public_key *key,
+ const unsigned char *data, size_t data_len,
+ buffer_t *result, enum dcrypt_padding padding,
+ const char **error_r);
+bool dcrypt_rsa_decrypt(struct dcrypt_private_key *key,
+ const unsigned char *data, size_t data_len,
+ buffer_t *result, enum dcrypt_padding padding,
+ const char **error_r);
+
+/* OID stuff */
+const char *dcrypt_oid2name(const unsigned char *oid, size_t oid_len,
+ const char **error_r);
+bool dcrypt_name2oid(const char *name, buffer_t *oid, const char **error_r);
+
+#endif
diff --git a/src/lib-dcrypt/istream-decrypt.c b/src/lib-dcrypt/istream-decrypt.c
new file mode 100644
index 0000000..be550fb
--- /dev/null
+++ b/src/lib-dcrypt/istream-decrypt.c
@@ -0,0 +1,1146 @@
+/* Copyright (c) 2016-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "buffer.h"
+#include "randgen.h"
+#include "safe-memset.h"
+#include "hash-method.h"
+#include "sha2.h"
+#include "memarea.h"
+#include "dcrypt.h"
+#include "istream.h"
+#include "istream-decrypt.h"
+#include "istream-private.h"
+#include "dcrypt-iostream.h"
+
+#include "hex-binary.h"
+
+#include <arpa/inet.h>
+
+#define ISTREAM_DECRYPT_READ_FIRST 15
+
+struct decrypt_istream_snapshot {
+ struct istream_snapshot snapshot;
+ struct decrypt_istream *dstream;
+ buffer_t *buf;
+};
+
+struct decrypt_istream {
+ struct istream_private istream;
+ buffer_t *buf;
+ bool symmetric;
+
+ i_stream_decrypt_get_key_callback_t *key_callback;
+ void *key_context;
+
+ struct dcrypt_private_key *priv_key;
+ bool initialized;
+ bool finalized;
+ bool use_mac;
+ bool snapshot_pending;
+
+ uoff_t ftr, pos;
+ enum io_stream_encrypt_flags flags;
+
+ /* original iv, in case seeking is done, future feature */
+ unsigned char *iv;
+
+ struct dcrypt_context_symmetric *ctx_sym;
+ struct dcrypt_context_hmac *ctx_mac;
+
+ enum decrypt_istream_format format;
+};
+
+static void i_stream_decrypt_reset(struct decrypt_istream *dstream)
+{
+ dstream->finalized = FALSE;
+ dstream->use_mac = FALSE;
+
+ dstream->ftr = 0;
+ dstream->pos = 0;
+ dstream->flags = 0;
+
+ if (!dstream->symmetric) {
+ dstream->initialized = FALSE;
+ if (dstream->ctx_sym != NULL)
+ dcrypt_ctx_sym_destroy(&dstream->ctx_sym);
+ if (dstream->ctx_mac != NULL)
+ dcrypt_ctx_hmac_destroy(&dstream->ctx_mac);
+ }
+ i_free(dstream->iv);
+ dstream->format = DECRYPT_FORMAT_V1;
+}
+
+enum decrypt_istream_format
+i_stream_encrypt_get_format(const struct istream *input)
+{
+ return ((const struct decrypt_istream*)input->real_stream)->format;
+}
+
+enum io_stream_encrypt_flags
+i_stream_encrypt_get_flags(const struct istream *input)
+{
+ return ((const struct decrypt_istream*)input->real_stream)->flags;
+}
+
+static ssize_t
+i_stream_decrypt_read_header_v1(struct decrypt_istream *stream,
+ const unsigned char *data, size_t mlen)
+{
+ const char *error = NULL;
+ size_t keydata_len = 0;
+ uint16_t len;
+ int ec, i = 0;
+
+ const unsigned char *digest_pos = NULL, *key_digest_pos = NULL,
+ *key_ct_pos = NULL;
+ size_t digest_len = 0, key_ct_len = 0, key_digest_size = 0;
+
+ buffer_t ephemeral_key;
+ buffer_t *secret = t_buffer_create(256);
+ buffer_t *key = t_buffer_create(256);
+
+ if (mlen < 2)
+ return 0;
+ keydata_len = be16_to_cpu_unaligned(data);
+ if (mlen-2 < keydata_len) {
+ /* try to read more */
+ return 0;
+ }
+
+ data+=2;
+ mlen-=2;
+
+ while (i < 4 && mlen > 2) {
+ memcpy(&len, data, 2);
+ len = ntohs(len);
+ if (len == 0 || len > mlen-2)
+ break;
+ data += 2;
+ mlen -= 2;
+
+ switch(i++) {
+ case 0:
+ buffer_create_from_const_data(&ephemeral_key,
+ data, len);
+ break;
+ case 1:
+ /* public key id */
+ digest_pos = data;
+ digest_len = len;
+ break;
+ case 2:
+ /* encryption key digest */
+ key_digest_pos = data;
+ key_digest_size = len;
+ break;
+ case 3:
+ /* encrypted key data */
+ key_ct_pos = data;
+ key_ct_len = len;
+ break;
+ }
+ data += len;
+ mlen -= len;
+ }
+
+ if (i < 4) {
+ io_stream_set_error(&stream->istream.iostream,
+ "Invalid or corrupted header");
+ /* was it consumed? */
+ stream->istream.istream.stream_errno =
+ mlen > 2 ? EINVAL : EPIPE;
+ return -1;
+ }
+
+ /* we don't have a private key */
+ if (stream->priv_key == NULL) {
+ /* see if we can get one */
+ if (stream->key_callback != NULL) {
+ const char *key_id =
+ binary_to_hex(digest_pos, digest_len);
+ int ret = stream->key_callback(key_id,
+ &stream->priv_key, &error, stream->key_context);
+ if (ret < 0) {
+ io_stream_set_error(&stream->istream.iostream,
+ "Private key not available: %s",
+ error);
+ return -1;
+ }
+ if (ret == 0) {
+ io_stream_set_error(&stream->istream.iostream,
+ "Private key not available");
+ return -1;
+ }
+ } else {
+ io_stream_set_error(&stream->istream.iostream,
+ "Private key not available");
+ return -1;
+ }
+ }
+
+ buffer_t *check = t_buffer_create(32);
+
+ if (!dcrypt_key_id_private_old(stream->priv_key, check, &error)) {
+ io_stream_set_error(&stream->istream.iostream,
+ "Cannot get public key hash: %s", error);
+ return -1;
+ } else {
+ if (memcmp(digest_pos, check->data,
+ I_MIN(digest_len,check->used)) != 0) {
+ io_stream_set_error(&stream->istream.iostream,
+ "Private key not available");
+ return -1;
+ }
+ }
+
+ /* derive shared secret */
+ if (!dcrypt_ecdh_derive_secret_local(stream->priv_key,
+ &ephemeral_key, secret, &error)) {
+ io_stream_set_error(&stream->istream.iostream,
+ "Cannot perform ECDH: %s", error);
+ return -1;
+ }
+
+ /* run it thru SHA256 once */
+ const struct hash_method *hash = &hash_method_sha256;
+ unsigned char hctx[hash->context_size];
+ unsigned char hres[hash->digest_size];
+ hash->init(hctx);
+ hash->loop(hctx, secret->data, secret->used);
+ hash->result(hctx, hres);
+ safe_memset(buffer_get_modifiable_data(secret, 0), 0, secret->used);
+
+ /* NB! The old code was broken and used this kind of IV - it is not
+ correct, but we need to stay compatible with old data */
+
+ /* use it to decrypt the actual encryption key */
+ struct dcrypt_context_symmetric *dctx;
+ if (!dcrypt_ctx_sym_create("aes-256-ctr", DCRYPT_MODE_DECRYPT,
+ &dctx, &error)) {
+ io_stream_set_error(&stream->istream.iostream,
+ "Key decryption error: %s", error);
+ return -1;
+ }
+
+ ec = 0;
+ dcrypt_ctx_sym_set_iv(dctx, (const unsigned char*)
+ "\x0\x0\x0\x0\x0\x0\x0\x0\x0\x0\x0\x0\x0\x0\x0\x0", 16);
+ dcrypt_ctx_sym_set_key(dctx, hres, hash->digest_size);
+ if (!dcrypt_ctx_sym_init(dctx, &error) ||
+ !dcrypt_ctx_sym_update(dctx, key_ct_pos, key_ct_len, key, &error) ||
+ !dcrypt_ctx_sym_final(dctx, key, &error)) {
+ io_stream_set_error(&stream->istream.iostream,
+ "Key decryption error: %s", error);
+ ec = -1;
+ }
+ dcrypt_ctx_sym_destroy(&dctx);
+
+ if (ec != 0) {
+ io_stream_set_error(&stream->istream.iostream,
+ "Key decryption error: %s", error);
+ return -1;
+ }
+
+ /* see if we got the correct key */
+ hash->init(hctx);
+ hash->loop(hctx, key->data, key->used);
+ hash->result(hctx, hres);
+
+ if (key_digest_size != sizeof(hres)) {
+ io_stream_set_error(&stream->istream.iostream,
+ "Key decryption error: "
+ "invalid digest length");
+ return -1;
+ }
+ if (memcmp(hres, key_digest_pos, sizeof(hres)) != 0) {
+ io_stream_set_error(&stream->istream.iostream,
+ "Key decryption error: "
+ "decrypted key is invalid");
+ return -1;
+ }
+
+ /* prime context with key */
+ if (!dcrypt_ctx_sym_create("aes-256-ctr", DCRYPT_MODE_DECRYPT,
+ &stream->ctx_sym, &error)) {
+ io_stream_set_error(&stream->istream.iostream,
+ "Decryption context create error: %s",
+ error);
+ return -1;
+ }
+
+ /* Again, old code used this IV, so we have to use it too */
+ dcrypt_ctx_sym_set_iv(stream->ctx_sym, (const unsigned char*)
+ "\x0\x0\x0\x0\x0\x0\x0\x0\x0\x0\x0\x0\x0\x0\x0\x0", 16);
+ dcrypt_ctx_sym_set_key(stream->ctx_sym, key->data, key->used);
+
+ safe_memset(buffer_get_modifiable_data(key, 0), 0, key->used);
+
+ if (!dcrypt_ctx_sym_init(stream->ctx_sym, &error)) {
+ io_stream_set_error(&stream->istream.iostream,
+ "Decryption init error: %s", error);
+ return -1;
+ }
+
+ stream->use_mac = FALSE;
+ stream->initialized = TRUE;
+ /* now we are ready to decrypt stream */
+
+ return sizeof(IOSTREAM_CRYPT_MAGIC) + 1 + 2 + keydata_len;
+}
+
+static bool
+get_msb32(const unsigned char **_data, const unsigned char *end,
+ uint32_t *num_r)
+{
+ const unsigned char *data = *_data;
+ if (end-data < 4)
+ return FALSE;
+ *num_r = be32_to_cpu_unaligned(data);
+ *_data += 4;
+ return TRUE;
+}
+
+static bool
+i_stream_decrypt_der(const unsigned char **_data, const unsigned char *end,
+ const char **str_r)
+{
+ const unsigned char *data = *_data;
+ unsigned int len;
+
+ if (end-data < 2)
+ return FALSE;
+ /* get us DER encoded length */
+ if ((data[1] & 0x80) != 0) {
+ /* two byte length */
+ if (end-data < 3)
+ return FALSE;
+ len = ((data[1] & 0x7f) << 8) + data[2] + 3;
+ } else {
+ len = data[1] + 2;
+ }
+ if ((size_t)(end-data) < len)
+ return FALSE;
+ *str_r = dcrypt_oid2name(data, len, NULL);
+ *_data += len;
+ return TRUE;
+}
+
+static ssize_t
+i_stream_decrypt_key(struct decrypt_istream *stream, const char *malg,
+ unsigned int rounds, const unsigned char *data,
+ const unsigned char *end, buffer_t *key, size_t key_len)
+{
+ const char *error;
+ enum dcrypt_key_type ktype;
+ int keys;
+ bool have_key = FALSE;
+ unsigned char dgst[32];
+ uint32_t val;
+ buffer_t buf;
+
+ if (data == end)
+ return 0;
+
+ keys = *data++;
+
+ /* if we have a key, prefab the digest */
+ if (stream->priv_key != NULL) {
+ buffer_create_from_data(&buf, dgst, sizeof(dgst));
+ if (!dcrypt_key_id_private(stream->priv_key, "sha256", &buf,
+ &error)) {
+ io_stream_set_error(&stream->istream.iostream,
+ "Decryption error: "
+ "dcrypt_key_id_private failed: %s",
+ error);
+ return -1;
+ }
+ } else if (stream->key_callback == NULL) {
+ io_stream_set_error(&stream->istream.iostream,
+ "Decryption error: "
+ "no private key available");
+ return -1;
+ }
+
+ /* for each key */
+ for(;keys>0;keys--) {
+ if ((size_t)(end-data) < 1 + (ssize_t)sizeof(dgst))
+ return 0;
+ ktype = *data++;
+
+ if (stream->priv_key != NULL) {
+ /* see if key matches to the one we have */
+ if (memcmp(dgst, data, sizeof(dgst)) == 0) {
+ have_key = TRUE;
+ break;
+ }
+ } else if (stream->key_callback != NULL) {
+ const char *hexdgst = /* digest length */
+ binary_to_hex(data, sizeof(dgst));
+ if (stream->priv_key != NULL)
+ dcrypt_key_unref_private(&stream->priv_key);
+ /* hope you going to give us right key.. */
+ int ret = stream->key_callback(hexdgst,
+ &stream->priv_key, &error, stream->key_context);
+ if (ret < 0) {
+ io_stream_set_error(&stream->istream.iostream,
+ "Private key not available: "
+ "%s", error);
+ return -1;
+ }
+ if (ret > 0) {
+ have_key = TRUE;
+ break;
+ }
+ }
+ data += sizeof(dgst);
+
+ /* wasn't correct key, skip over some data */
+ if (!get_msb32(&data, end, &val) ||
+ !get_msb32(&data, end, &val))
+ return 0;
+ }
+
+ /* didn't find matching key */
+ if (!have_key) {
+ io_stream_set_error(&stream->istream.iostream,
+ "Decryption error: "
+ "no private key available");
+ return -1;
+ }
+
+ data += sizeof(dgst);
+
+ const unsigned char *ephemeral_key;
+ uint32_t ep_key_len;
+ const unsigned char *encrypted_key;
+ uint32_t eklen;
+ const unsigned char *ekhash;
+ uint32_t ekhash_len;
+
+ /* read ephemeral key (can be missing for RSA) */
+ if (!get_msb32(&data, end, &ep_key_len) ||
+ (size_t)(end-data) < ep_key_len)
+ return 0;
+ ephemeral_key = data;
+ data += ep_key_len;
+
+ /* read encrypted key */
+ if (!get_msb32(&data, end, &eklen) || (size_t)(end-data) < eklen)
+ return 0;
+ encrypted_key = data;
+ data += eklen;
+
+ /* read key data hash */
+ if (!get_msb32(&data, end, &ekhash_len) ||
+ (size_t)(end-data) < ekhash_len)
+ return 0;
+ ekhash = data;
+ data += ekhash_len;
+
+ /* decrypt the seed */
+ if (ktype == DCRYPT_KEY_RSA) {
+ if (!dcrypt_rsa_decrypt(stream->priv_key, encrypted_key, eklen,
+ key, DCRYPT_PADDING_RSA_PKCS1_OAEP,
+ &error)) {
+ io_stream_set_error(&stream->istream.iostream,
+ "key decryption error: %s", error);
+ return -1;
+ }
+ } else if (ktype == DCRYPT_KEY_EC) {
+ /* perform ECDHE */
+ buffer_t *temp_key = t_buffer_create(256);
+ buffer_t *secret = t_buffer_create(256);
+ buffer_t peer_key;
+ buffer_create_from_const_data(&peer_key,
+ ephemeral_key, ep_key_len);
+ if (!dcrypt_ecdh_derive_secret_local(stream->priv_key,
+ &peer_key, secret, &error)) {
+ io_stream_set_error(&stream->istream.iostream,
+ "Key decryption error: corrupted header");
+ return -1;
+ }
+
+ /* use shared secret and peer key to generate decryption key,
+ AES-256-CBC has 32 byte key and 16 byte IV */
+ if (!dcrypt_pbkdf2(secret->data, secret->used,
+ peer_key.data, peer_key.used,
+ malg, rounds, temp_key, 32+16, &error)) {
+ safe_memset(buffer_get_modifiable_data(secret, 0),
+ 0, secret->used);
+ io_stream_set_error(&stream->istream.iostream,
+ "Key decryption error: %s", error);
+ return -1;
+ }
+
+ safe_memset(buffer_get_modifiable_data(secret, 0),
+ 0, secret->used);
+ if (temp_key->used != 32+16) {
+ safe_memset(buffer_get_modifiable_data(temp_key, 0),
+ 0, temp_key->used);
+ io_stream_set_error(&stream->istream.iostream,
+ "Cannot perform key decryption: "
+ "invalid temporary key");
+ return -1;
+ }
+ struct dcrypt_context_symmetric *dctx;
+ if (!dcrypt_ctx_sym_create("AES-256-CBC", DCRYPT_MODE_DECRYPT,
+ &dctx, &error)) {
+ safe_memset(buffer_get_modifiable_data(temp_key, 0),
+ 0, temp_key->used);
+ io_stream_set_error(&stream->istream.iostream,
+ "Key decryption error: %s", error);
+ return -1;
+ }
+ const unsigned char *ptr = temp_key->data;
+
+ /* we use ephemeral_key for IV */
+ dcrypt_ctx_sym_set_key(dctx, ptr, 32);
+ dcrypt_ctx_sym_set_iv(dctx, ptr+32, 16);
+ safe_memset(buffer_get_modifiable_data(temp_key, 0),
+ 0, temp_key->used);
+
+ int ec = 0;
+ if (!dcrypt_ctx_sym_init(dctx, &error) ||
+ !dcrypt_ctx_sym_update(dctx, encrypted_key, eklen,
+ key, &error) ||
+ !dcrypt_ctx_sym_final(dctx, key, &error)) {
+ io_stream_set_error(&stream->istream.iostream,
+ "Cannot perform key decryption: %s",
+ error);
+ ec = -1;
+ }
+
+ if (key->used != key_len) {
+ io_stream_set_error(&stream->istream.iostream,
+ "Cannot perform key decryption: "
+ "invalid key length");
+ ec = -1;
+ }
+
+ dcrypt_ctx_sym_destroy(&dctx);
+ if (ec != 0) return ec;
+ } else {
+ io_stream_set_error(&stream->istream.iostream,
+ "Decryption error: "
+ "unsupported key type 0x%02x", ktype);
+ return -1;
+ }
+
+ /* make sure we were able to decrypt the encrypted key correctly */
+ const struct hash_method *hash = hash_method_lookup(t_str_lcase(malg));
+ if (hash == NULL) {
+ safe_memset(buffer_get_modifiable_data(key, 0), 0, key->used);
+ io_stream_set_error(&stream->istream.iostream,
+ "Decryption error: "
+ "unsupported hash algorithm: %s", malg);
+ return -1;
+ }
+ unsigned char hctx[hash->context_size];
+ unsigned char hres[hash->digest_size];
+ hash->init(hctx);
+ hash->loop(hctx, key->data, key->used);
+ hash->result(hctx, hres);
+
+ for(int i = 1; i < 2049; i++) {
+ uint32_t i_msb = cpu32_to_be(i);
+
+ hash->init(hctx);
+ hash->loop(hctx, hres, sizeof(hres));
+ hash->loop(hctx, &i_msb, sizeof(i_msb));
+ hash->result(hctx, hres);
+ }
+
+ /* do the comparison */
+ if (memcmp(ekhash, hres, I_MIN(ekhash_len, sizeof(hres))) != 0) {
+ safe_memset(buffer_get_modifiable_data(key, 0), 0, key->used);
+ io_stream_set_error(&stream->istream.iostream,
+ "Decryption error: "
+ "corrupted header ekhash");
+ return -1;
+ }
+ return 1;
+}
+
+static int
+i_stream_decrypt_header_contents(struct decrypt_istream *stream,
+ const unsigned char *data, size_t size)
+{
+ const unsigned char *end = data + size;
+ bool failed = FALSE;
+
+ /* read cipher OID */
+ const char *calg;
+ if (!i_stream_decrypt_der(&data, end, &calg))
+ return 0;
+ if (calg == NULL ||
+ !dcrypt_ctx_sym_create(calg, DCRYPT_MODE_DECRYPT,
+ &stream->ctx_sym, NULL)) {
+ io_stream_set_error(&stream->istream.iostream,
+ "Decryption error: "
+ "unsupported/invalid cipher: %s", calg);
+ return -1;
+ }
+
+ /* read MAC oid (MAC is used for PBKDF2 and key data digest, too) */
+ const char *malg;
+ if (!i_stream_decrypt_der(&data, end, &malg))
+ return 0;
+ if (malg == NULL || !dcrypt_ctx_hmac_create(malg, &stream->ctx_mac, NULL)) {
+ io_stream_set_error(&stream->istream.iostream,
+ "Decryption error: "
+ "unsupported/invalid MAC algorithm: %s",
+ malg);
+ return -1;
+ }
+
+ /* read rounds (for PBKDF2) */
+ uint32_t rounds;
+ if (!get_msb32(&data, end, &rounds))
+ return 0;
+ /* read key data length */
+ uint32_t kdlen;
+ if (!get_msb32(&data, end, &kdlen))
+ return 0;
+
+ size_t tagsize;
+
+ if ((stream->flags & IO_STREAM_ENC_INTEGRITY_HMAC) ==
+ IO_STREAM_ENC_INTEGRITY_HMAC) {
+ tagsize = IOSTREAM_TAG_SIZE;
+ } else if ((stream->flags & IO_STREAM_ENC_INTEGRITY_AEAD) ==
+ IO_STREAM_ENC_INTEGRITY_AEAD) {
+ tagsize = IOSTREAM_TAG_SIZE;
+ } else {
+ tagsize = 0;
+ }
+
+ /* how much key data we should be getting */
+ size_t kl = dcrypt_ctx_sym_get_key_length(stream->ctx_sym) +
+ dcrypt_ctx_sym_get_iv_length(stream->ctx_sym) + tagsize;
+ buffer_t *keydata = t_buffer_create(kl);
+
+ /* try to decrypt the keydata with a private key */
+ int ret;
+ if ((ret = i_stream_decrypt_key(stream, malg, rounds, data,
+ end, keydata, kl)) <= 0)
+ return ret;
+
+ /* oh, it worked! */
+ const unsigned char *ptr = keydata->data;
+ if (keydata->used != kl) {
+ /* but returned wrong amount of data */
+ io_stream_set_error(&stream->istream.iostream,
+ "Key decryption error: "
+ "Key data length mismatch");
+ return -1;
+ }
+
+ /* prime contexts */
+ dcrypt_ctx_sym_set_key(stream->ctx_sym, ptr,
+ dcrypt_ctx_sym_get_key_length(stream->ctx_sym));
+ ptr += dcrypt_ctx_sym_get_key_length(stream->ctx_sym);
+ dcrypt_ctx_sym_set_iv(stream->ctx_sym, ptr,
+ dcrypt_ctx_sym_get_iv_length(stream->ctx_sym));
+ stream->iv = i_malloc(dcrypt_ctx_sym_get_iv_length(stream->ctx_sym));
+ memcpy(stream->iv, ptr, dcrypt_ctx_sym_get_iv_length(stream->ctx_sym));
+ ptr += dcrypt_ctx_sym_get_iv_length(stream->ctx_sym);
+
+ /* based on the chosen MAC, initialize HMAC or AEAD */
+ if ((stream->flags & IO_STREAM_ENC_INTEGRITY_HMAC) ==
+ IO_STREAM_ENC_INTEGRITY_HMAC) {
+ const char *error;
+ dcrypt_ctx_hmac_set_key(stream->ctx_mac, ptr, tagsize);
+ if (!dcrypt_ctx_hmac_init(stream->ctx_mac, &error)) {
+ io_stream_set_error(&stream->istream.iostream,
+ "MAC error: %s", error);
+ stream->istream.istream.stream_errno = EINVAL;
+ failed = TRUE;
+ }
+ stream->ftr = dcrypt_ctx_hmac_get_digest_length(stream->ctx_mac);
+ stream->use_mac = TRUE;
+ } else if ((stream->flags & IO_STREAM_ENC_INTEGRITY_AEAD) ==
+ IO_STREAM_ENC_INTEGRITY_AEAD) {
+ dcrypt_ctx_sym_set_aad(stream->ctx_sym, ptr, tagsize);
+ stream->ftr = tagsize;
+ stream->use_mac = TRUE;
+ } else {
+ stream->use_mac = FALSE;
+ }
+ /* destroy private key data */
+ safe_memset(buffer_get_modifiable_data(keydata, 0), 0, keydata->used);
+ buffer_set_used_size(keydata, 0);
+ return failed ? -1 : 1;
+}
+
+static ssize_t
+i_stream_decrypt_read_header(struct decrypt_istream *stream,
+ const unsigned char *data, size_t mlen)
+{
+ const char *error;
+ const unsigned char *end = data + mlen;
+
+ /* check magic */
+ if (mlen < sizeof(IOSTREAM_CRYPT_MAGIC))
+ return 0;
+ if (memcmp(data, IOSTREAM_CRYPT_MAGIC, sizeof(IOSTREAM_CRYPT_MAGIC)) != 0) {
+ io_stream_set_error(&stream->istream.iostream,
+ "Stream is not encrypted (invalid magic)");
+ stream->istream.istream.stream_errno = EINVAL;
+ return -1;
+ }
+ data += sizeof(IOSTREAM_CRYPT_MAGIC);
+
+ if (data >= end)
+ return 0; /* read more? */
+
+ /* check version */
+ if (*data == '\x01') {
+ stream->format = DECRYPT_FORMAT_V1;
+ return i_stream_decrypt_read_header_v1(stream, data+1,
+ end - (data+1));
+ } else if (*data != '\x02') {
+ io_stream_set_error(&stream->istream.iostream,
+ "Unsupported encrypted data 0x%02x", *data);
+ return -1;
+ }
+
+ stream->format = DECRYPT_FORMAT_V2;
+
+ data++;
+
+ /* read flags */
+ uint32_t flags;
+ if (!get_msb32(&data, end, &flags))
+ return 0;
+ stream->flags = flags;
+
+ /* get the total length of header */
+ uint32_t hdr_len;
+ if (!get_msb32(&data, end, &hdr_len))
+ return 0;
+ /* do not forget stream format */
+ if ((size_t)(end-data)+1 < hdr_len)
+ return 0;
+
+ int ret;
+ if ((ret = i_stream_decrypt_header_contents(stream, data, hdr_len)) < 0)
+ return -1;
+ else if (ret == 0) {
+ io_stream_set_error(&stream->istream.iostream,
+ "Decryption error: truncate header length");
+ stream->istream.istream.stream_errno = EPIPE;
+ return -1;
+ }
+ stream->initialized = TRUE;
+
+ /* if it all went well, try to initialize decryption context */
+ if (!dcrypt_ctx_sym_init(stream->ctx_sym, &error)) {
+ io_stream_set_error(&stream->istream.iostream,
+ "Decryption init error: %s", error);
+ return -1;
+ }
+ return hdr_len;
+}
+
+static void
+i_stream_decrypt_realloc_buf_if_needed(struct decrypt_istream *dstream)
+{
+ if (!dstream->snapshot_pending)
+ return;
+
+ /* buf exists in a snapshot. Leave it be and create a copy of it
+ that we modify. */
+ buffer_t *old_buf = dstream->buf;
+ dstream->buf = buffer_create_dynamic(default_pool,
+ I_MAX(512, old_buf->used));
+ buffer_append(dstream->buf, old_buf->data, old_buf->used);
+ dstream->snapshot_pending = FALSE;
+
+ dstream->istream.buffer = dstream->buf->data;
+}
+
+static ssize_t
+i_stream_decrypt_read(struct istream_private *stream)
+{
+ struct decrypt_istream *dstream =
+ (struct decrypt_istream *)stream;
+ const unsigned char *data;
+ size_t size, decrypt_size;
+ const char *error = NULL;
+ int ret;
+ bool check_mac = FALSE;
+
+ /* not if it's broken */
+ if (stream->istream.stream_errno != 0)
+ return -1;
+
+ i_stream_decrypt_realloc_buf_if_needed(dstream);
+ for (;;) {
+ /* remove skipped data from buffer */
+ if (stream->skip > 0) {
+ i_assert(stream->skip <= dstream->buf->used);
+ buffer_delete(dstream->buf, 0, stream->skip);
+ stream->pos -= stream->skip;
+ stream->skip = 0;
+ }
+
+ stream->buffer = dstream->buf->data;
+
+ i_assert(stream->pos <= dstream->buf->used);
+ if (stream->pos >= dstream->istream.max_buffer_size) {
+ /* stream buffer still at maximum */
+ return -2;
+ }
+
+ /* if something is already decrypted, return as much of it as
+ we can */
+ if (dstream->initialized && dstream->buf->used > 0) {
+ size_t new_pos, bytes;
+
+ /* only return up to max_buffer_size bytes, even when
+ buffer actually has more, as not to confuse the
+ caller */
+ if (dstream->buf->used <=
+ dstream->istream.max_buffer_size) {
+ new_pos = dstream->buf->used;
+ if (dstream->finalized)
+ stream->istream.eof = TRUE;
+ } else {
+ new_pos = dstream->istream.max_buffer_size;
+ }
+
+ bytes = new_pos - stream->pos;
+ stream->pos = new_pos;
+ if (bytes > 0)
+ return (ssize_t)bytes;
+ }
+ if (dstream->finalized) {
+ /* all data decrypted */
+ stream->istream.eof = TRUE;
+ return -1;
+ }
+
+ /* need to read more input */
+ ret = i_stream_read_memarea(stream->parent);
+ if (ret == 0)
+ return ret;
+
+ data = i_stream_get_data(stream->parent, &size);
+
+ if (ret == -1 &&
+ (size == 0 || stream->parent->stream_errno != 0)) {
+ stream->istream.stream_errno =
+ stream->parent->stream_errno;
+
+ /* file was empty */
+ if (!dstream->initialized &&
+ size == 0 && stream->parent->eof) {
+ stream->istream.eof = TRUE;
+ return -1;
+ }
+
+ if (stream->istream.stream_errno != 0)
+ return -1;
+
+ if (!dstream->initialized) {
+ io_stream_set_error(&stream->iostream,
+ "Decryption error: %s",
+ "Input truncated in decryption header");
+ stream->istream.stream_errno = EPIPE;
+ return -1;
+ }
+
+ /* final block */
+ if (dcrypt_ctx_sym_final(dstream->ctx_sym,
+ dstream->buf, &error)) {
+ dstream->finalized = TRUE;
+ continue;
+ }
+ io_stream_set_error(&stream->iostream,
+ "MAC error: %s", error);
+ stream->istream.stream_errno = EINVAL;
+ return -1;
+ }
+
+ if (!dstream->initialized) {
+ ssize_t hret;
+
+ if ((hret=i_stream_decrypt_read_header(
+ dstream, data, size)) <= 0) {
+ if (hret < 0) {
+ if (stream->istream.stream_errno == 0)
+ /* assume temporary failure */
+ stream->istream.stream_errno = EIO;
+ return -1;
+ }
+
+ if (hret == 0 && stream->parent->eof) {
+ /* not encrypted by us */
+ stream->istream.stream_errno = EPIPE;
+ io_stream_set_error(&stream->iostream,
+ "Truncated header");
+ return -1;
+ }
+ }
+
+ if (hret == 0) {
+ /* see if we can get more data */
+ if (ret == -2) {
+ stream->istream.stream_errno = EINVAL;
+ io_stream_set_error(&stream->iostream,
+ "Header too large "
+ "(more than %zu bytes)",
+ size);
+ return -1;
+ }
+ continue;
+ } else {
+ /* clean up buffer */
+ safe_memset(buffer_get_modifiable_data(dstream->buf, 0),
+ 0, dstream->buf->used);
+ buffer_set_used_size(dstream->buf, 0);
+ i_stream_skip(stream->parent, hret);
+ }
+
+ data = i_stream_get_data(stream->parent, &size);
+ }
+ decrypt_size = size;
+
+ if (dstream->use_mac) {
+ if (stream->parent->eof) {
+ if (decrypt_size < dstream->ftr) {
+ io_stream_set_error(&stream->iostream,
+ "Decryption error: "
+ "footer is longer than data");
+ stream->istream.stream_errno = EINVAL;
+ return -1;
+ }
+ check_mac = TRUE;
+ } else {
+ /* ignore footer's length of data until we
+ reach EOF */
+ size -= dstream->ftr;
+ }
+ decrypt_size -= dstream->ftr;
+ if ((dstream->flags & IO_STREAM_ENC_INTEGRITY_HMAC) ==
+ IO_STREAM_ENC_INTEGRITY_HMAC) {
+ if (!dcrypt_ctx_hmac_update(dstream->ctx_mac,
+ data, decrypt_size, &error)) {
+ io_stream_set_error(&stream->iostream,
+ "MAC error: %s", error);
+ stream->istream.stream_errno = EINVAL;
+ return -1;
+ }
+ }
+ }
+
+ if (check_mac) {
+ if ((dstream->flags & IO_STREAM_ENC_INTEGRITY_HMAC) ==
+ IO_STREAM_ENC_INTEGRITY_HMAC) {
+ unsigned char dgst[dcrypt_ctx_hmac_get_digest_length(dstream->ctx_mac)];
+ buffer_t db;
+ buffer_create_from_data(&db, dgst, sizeof(dgst));
+ if (!dcrypt_ctx_hmac_final(dstream->ctx_mac, &db, &error)) {
+ io_stream_set_error(&stream->iostream,
+ "Cannot verify MAC: %s", error);
+ stream->istream.stream_errno = EINVAL;
+ return -1;
+ }
+ if (memcmp(dgst, data + decrypt_size,
+ dcrypt_ctx_hmac_get_digest_length(dstream->ctx_mac)) != 0) {
+ io_stream_set_error(&stream->iostream,
+ "Cannot verify MAC: mismatch");
+ stream->istream.stream_errno = EINVAL;
+ return -1;
+ }
+ } else if ((dstream->flags & IO_STREAM_ENC_INTEGRITY_AEAD) ==
+ IO_STREAM_ENC_INTEGRITY_AEAD) {
+ dcrypt_ctx_sym_set_tag(dstream->ctx_sym,
+ data + decrypt_size,
+ dstream->ftr);
+ }
+ }
+
+ if (!dcrypt_ctx_sym_update(dstream->ctx_sym,
+ data, decrypt_size, dstream->buf, &error)) {
+ io_stream_set_error(&stream->iostream,
+ "Decryption error: %s", error);
+ stream->istream.stream_errno = EINVAL;
+ return -1;
+ }
+ i_stream_skip(stream->parent, size);
+ }
+}
+
+static void
+i_stream_decrypt_seek(struct istream_private *stream, uoff_t v_offset,
+ bool mark ATTR_UNUSED)
+{
+ struct decrypt_istream *dstream =
+ (struct decrypt_istream *)stream;
+
+ i_stream_decrypt_realloc_buf_if_needed(dstream);
+
+ if (i_stream_nonseekable_try_seek(stream, v_offset))
+ return;
+
+ /* have to seek backwards - reset crypt state and retry */
+ i_stream_decrypt_reset(dstream);
+ if (!i_stream_nonseekable_try_seek(stream, v_offset))
+ i_unreached();
+}
+
+static void i_stream_decrypt_close(struct iostream_private *stream,
+ bool close_parent)
+{
+ struct decrypt_istream *dstream =
+ (struct decrypt_istream *)stream;
+
+ if (close_parent)
+ i_stream_close(dstream->istream.parent);
+}
+
+static void
+i_stream_decrypt_snapshot_free(struct istream_snapshot *_snapshot)
+{
+ struct decrypt_istream_snapshot *snapshot =
+ container_of(_snapshot, struct decrypt_istream_snapshot,
+ snapshot);
+
+ if (snapshot->dstream->buf != snapshot->buf)
+ buffer_free(&snapshot->buf);
+ else {
+ i_assert(snapshot->dstream->snapshot_pending);
+ snapshot->dstream->snapshot_pending = FALSE;
+ }
+ i_free(snapshot);
+}
+
+static struct istream_snapshot *
+i_stream_decrypt_snapshot(struct istream_private *stream,
+ struct istream_snapshot *prev_snapshot)
+{
+ struct decrypt_istream *dstream =
+ (struct decrypt_istream *)stream;
+ struct decrypt_istream_snapshot *snapshot;
+
+ if (stream->buffer != dstream->buf->data) {
+ /* reading body */
+ return i_stream_default_snapshot(stream, prev_snapshot);
+ }
+
+ /* snapshot the header buffer */
+ snapshot = i_new(struct decrypt_istream_snapshot, 1);
+ snapshot->dstream = dstream;
+ snapshot->buf = dstream->buf;
+ snapshot->snapshot.free = i_stream_decrypt_snapshot_free;
+ snapshot->snapshot.prev_snapshot = prev_snapshot;
+ dstream->snapshot_pending = TRUE;
+ return &snapshot->snapshot;
+}
+
+static void i_stream_decrypt_destroy(struct iostream_private *stream)
+{
+ struct decrypt_istream *dstream =
+ (struct decrypt_istream *)stream;
+
+ if (!dstream->snapshot_pending)
+ buffer_free(&dstream->buf);
+ else {
+ /* Clear buf to make sure i_stream_decrypt_snapshot_free()
+ frees it. */
+ dstream->buf = NULL;
+ }
+
+ if (dstream->iv != NULL)
+ i_free_and_null(dstream->iv);
+ if (dstream->ctx_sym != NULL)
+ dcrypt_ctx_sym_destroy(&dstream->ctx_sym);
+ if (dstream->ctx_mac != NULL)
+ dcrypt_ctx_hmac_destroy(&dstream->ctx_mac);
+ if (dstream->priv_key != NULL)
+ dcrypt_key_unref_private(&dstream->priv_key);
+
+ i_stream_unref(&dstream->istream.parent);
+}
+
+static struct decrypt_istream *
+i_stream_create_decrypt_common(struct istream *input)
+{
+ struct decrypt_istream *dstream;
+
+ i_assert(input->real_stream->max_buffer_size > 0);
+
+ dstream = i_new(struct decrypt_istream, 1);
+ dstream->istream.max_buffer_size = input->real_stream->max_buffer_size;
+ dstream->istream.read = i_stream_decrypt_read;
+ dstream->istream.snapshot = i_stream_decrypt_snapshot;
+ if (input->seekable)
+ dstream->istream.seek = i_stream_decrypt_seek;
+ dstream->istream.iostream.close = i_stream_decrypt_close;
+ dstream->istream.iostream.destroy = i_stream_decrypt_destroy;
+
+ dstream->istream.istream.readable_fd = FALSE;
+ dstream->istream.istream.blocking = input->blocking;
+ dstream->istream.istream.seekable = input->seekable;
+
+ dstream->buf = buffer_create_dynamic(default_pool, 512);
+
+ (void)i_stream_create(&dstream->istream, input,
+ i_stream_get_fd(input), 0);
+ return dstream;
+}
+
+struct istream *
+i_stream_create_decrypt(struct istream *input,
+ struct dcrypt_private_key *priv_key)
+{
+ struct decrypt_istream *dstream;
+
+ dstream = i_stream_create_decrypt_common(input);
+ dcrypt_key_ref_private(priv_key);
+ dstream->priv_key = priv_key;
+ return &dstream->istream.istream;
+}
+
+struct istream *
+i_stream_create_sym_decrypt(struct istream *input,
+ struct dcrypt_context_symmetric *ctx)
+{
+ const char *error;
+ int ec;
+ struct decrypt_istream *dstream;
+ dstream = i_stream_create_decrypt_common(input);
+ dstream->use_mac = FALSE;
+ dstream->initialized = TRUE;
+ dstream->symmetric = TRUE;
+
+ if (!dcrypt_ctx_sym_init(ctx, &error)) ec = -1;
+ else ec = 0;
+
+ dstream->ctx_sym = ctx;
+
+ if (ec != 0) {
+ io_stream_set_error(&dstream->istream.iostream,
+ "Cannot initialize decryption: %s", error);
+ dstream->istream.istream.stream_errno = EIO;
+ };
+
+ return &dstream->istream.istream;
+}
+
+struct istream *
+i_stream_create_decrypt_callback(struct istream *input,
+ i_stream_decrypt_get_key_callback_t *callback,
+ void *context)
+{
+ struct decrypt_istream *dstream;
+
+ i_assert(callback != NULL);
+
+ dstream = i_stream_create_decrypt_common(input);
+ dstream->key_callback = callback;
+ dstream->key_context = context;
+ return &dstream->istream.istream;
+}
diff --git a/src/lib-dcrypt/istream-decrypt.h b/src/lib-dcrypt/istream-decrypt.h
new file mode 100644
index 0000000..d531c00
--- /dev/null
+++ b/src/lib-dcrypt/istream-decrypt.h
@@ -0,0 +1,48 @@
+#ifndef ISTREAM_DECRYPT_H
+#define ISTREAM_DECRYPT_H
+
+struct dcrypt_private_key;
+struct dcrypt_context_symmetric;
+
+enum decrypt_istream_format {
+ DECRYPT_FORMAT_V1,
+ DECRYPT_FORMAT_V2
+};
+
+/* Look for a private key for a specified public key digest and set it to
+ priv_key_r. Returns 1 if ok, 0 if key doesn't exist, -1 on internal error.
+
+ Note that the private key will be unreferenced when the istream is
+ destroyed. If the callback is returning a persistent key, it must reference
+ the key first. (This is required, because otherwise a key newly created by
+ the callback couldn't be automatically freed.) */
+typedef int
+i_stream_decrypt_get_key_callback_t(const char *pubkey_digest,
+ struct dcrypt_private_key **priv_key_r,
+ const char **error_r, void *context);
+
+struct istream *
+i_stream_create_decrypt(struct istream *input,
+ struct dcrypt_private_key *priv_key);
+
+/* create stream for reading plain encrypted data with no header or MAC.
+ do not call dcrypt_ctx_sym_init
+ */
+struct istream *
+i_stream_create_sym_decrypt(struct istream *input,
+ struct dcrypt_context_symmetric *ctx);
+
+
+/* Decrypt the istream. When a private key is needed, the callback will be
+ called. This allows using multiple private keys for different mails. */
+struct istream *
+i_stream_create_decrypt_callback(struct istream *input,
+ i_stream_decrypt_get_key_callback_t *callback,
+ void *context);
+
+enum decrypt_istream_format
+i_stream_encrypt_get_format(const struct istream *input);
+enum io_stream_encrypt_flags
+i_stream_encrypt_get_flags(const struct istream *input);
+
+#endif
diff --git a/src/lib-dcrypt/ostream-encrypt.c b/src/lib-dcrypt/ostream-encrypt.c
new file mode 100644
index 0000000..c6e67f5
--- /dev/null
+++ b/src/lib-dcrypt/ostream-encrypt.c
@@ -0,0 +1,800 @@
+/* Copyright (c) 2016-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "buffer.h"
+#include "randgen.h"
+#include "dcrypt-iostream.h"
+#include "ostream-encrypt.h"
+#include "ostream-private.h"
+#include "hash-method.h"
+#include "sha2.h"
+#include "safe-memset.h"
+#include "dcrypt.h"
+
+#include <arpa/inet.h>
+
+/* file struct dcrypt_public_key syntax
+ * magic (14 bytes)
+ * version (1 bytes)
+ * flags (4 bytes)
+ * size of header (4 bytes)
+ * sha1 of key id (20 bytes)
+ * cipher oid
+ * mac oid
+ * rounds (4 bytes)
+ * key data size (4 bytes)
+ * key data
+ * cipher data
+ * mac data (mac specific bytes)
+ */
+
+#define IO_STREAM_ENCRYPT_SEED_SIZE 32
+#define IO_STREAM_ENCRYPT_ROUNDS 2048
+
+struct encrypt_ostream {
+ struct ostream_private ostream;
+
+ struct dcrypt_context_symmetric *ctx_sym;
+ struct dcrypt_context_hmac *ctx_mac;
+
+ enum io_stream_encrypt_flags flags;
+ struct dcrypt_public_key *pub;
+
+ unsigned char *key_data;
+ size_t key_data_len;
+
+ buffer_t *cipher_oid;
+ buffer_t *mac_oid;
+ size_t block_size;
+
+ bool finalized;
+ bool failed;
+ bool prefix_written;
+};
+
+static int
+o_stream_encrypt_send(struct encrypt_ostream *stream,
+ const unsigned char *data, size_t size)
+{
+ ssize_t ec;
+
+ ec = o_stream_send(stream->ostream.parent, data, size);
+ if (ec == (ssize_t)size)
+ return 0;
+ else if (ec < 0) {
+ o_stream_copy_error_from_parent(&stream->ostream);
+ return -1;
+ } else {
+ io_stream_set_error(&stream->ostream.iostream,
+ "ostream-encrypt: "
+ "Unexpectedly short write to parent stream");
+ stream->ostream.ostream.stream_errno = EINVAL;
+ return -1;
+ }
+}
+
+static int
+o_stream_encrypt_send_header_v1(struct encrypt_ostream *stream)
+{
+ unsigned char c;
+ unsigned short s;
+
+ i_assert(!stream->prefix_written);
+ stream->prefix_written = TRUE;
+
+ buffer_t *values = t_buffer_create(256);
+ buffer_append(values, IOSTREAM_CRYPT_MAGIC,
+ sizeof(IOSTREAM_CRYPT_MAGIC));
+ /* version */
+ c = 1;
+ buffer_append(values, &c, 1);
+ /* key data length */
+ s = htons(stream->key_data_len);
+ buffer_append(values, &s, 2);
+ /* then write key data */
+ buffer_append(values, stream->key_data, stream->key_data_len);
+ i_free_and_null(stream->key_data);
+
+ /* then send it to stream */
+ return o_stream_encrypt_send(stream, values->data, values->used);
+}
+
+static int
+o_stream_encrypt_send_header_v2(struct encrypt_ostream *stream)
+{
+ unsigned char c;
+ unsigned int i;
+
+ i_assert(!stream->prefix_written);
+ stream->prefix_written = TRUE;
+
+ buffer_t *values = t_buffer_create(256);
+ buffer_append(values, IOSTREAM_CRYPT_MAGIC,
+ sizeof(IOSTREAM_CRYPT_MAGIC));
+ c = 2;
+ buffer_append(values, &c, 1);
+ i = cpu32_to_be(stream->flags);
+ buffer_append(values, &i, 4);
+ /* store total length of header
+ 9 = version + flags + length
+ 8 = rounds + key data length
+ */
+ i = cpu32_to_be(sizeof(IOSTREAM_CRYPT_MAGIC) + 9 +
+ stream->cipher_oid->used + stream->mac_oid->used +
+ 8 + stream->key_data_len);
+ buffer_append(values, &i, 4);
+
+ buffer_append_buf(values, stream->cipher_oid, 0, SIZE_MAX);
+ buffer_append_buf(values, stream->mac_oid, 0, SIZE_MAX);
+ i = cpu32_to_be(IO_STREAM_ENCRYPT_ROUNDS);
+ buffer_append(values, &i, 4);
+ i = cpu32_to_be(stream->key_data_len);
+ buffer_append(values, &i, 4);
+ buffer_append(values, stream->key_data, stream->key_data_len);
+ i_free_and_null(stream->key_data);
+
+ return o_stream_encrypt_send(stream, values->data, values->used);
+}
+
+static int
+o_stream_encrypt_keydata_create_v1(struct encrypt_ostream *stream)
+{
+ buffer_t *encrypted_key, *ephemeral_key, *secret, *res, buf;
+ const char *error = NULL;
+ const struct hash_method *hash = &hash_method_sha256;
+
+ /* various temporary buffers */
+ unsigned char seed[IO_STREAM_ENCRYPT_SEED_SIZE];
+ unsigned char pkhash[hash->digest_size];
+ unsigned char ekhash[hash->digest_size];
+ unsigned char hres[hash->digest_size];
+
+ unsigned char hctx[hash->context_size];
+
+ /* hash the public key first */
+ buffer_create_from_data(&buf, pkhash, sizeof(pkhash));
+ if (!dcrypt_key_id_public_old(stream->pub, &buf, &error)) {
+ io_stream_set_error(&stream->ostream.iostream,
+ "Key hash failed: %s", error);
+ return -1;
+ }
+
+ /* hash the key base */
+ hash->init(hctx);
+ hash->loop(hctx, seed, sizeof(seed));
+ hash->result(hctx, ekhash);
+
+ ephemeral_key = t_buffer_create(256);
+ encrypted_key = t_buffer_create(256);
+ secret = t_buffer_create(256);
+
+ if (!dcrypt_ecdh_derive_secret_peer(stream->pub, ephemeral_key,
+ secret, &error)) {
+ io_stream_set_error(&stream->ostream.iostream,
+ "Cannot perform ECDH: %s", error);
+ return -1;
+ }
+
+ /* hash the secret data */
+ hash->init(hctx);
+ hash->loop(hctx, secret->data, secret->used);
+ hash->result(hctx, hres);
+ safe_memset(buffer_get_modifiable_data(secret, 0), 0, secret->used);
+
+ /* use it to encrypt the actual encryption key */
+ struct dcrypt_context_symmetric *dctx;
+ if (!dcrypt_ctx_sym_create("aes-256-ctr", DCRYPT_MODE_ENCRYPT,
+ &dctx, &error)) {
+ io_stream_set_error(&stream->ostream.iostream,
+ "Key encryption error: %s", error);
+ return -1;
+ }
+
+ random_fill(seed, sizeof(seed));
+ hash->init(hctx);
+ hash->loop(hctx, seed, sizeof(seed));
+ hash->result(hctx, ekhash);
+
+ int ec = 0;
+
+ /* NB! The old code was broken and used this kind of IV - it is not
+ correct, but we need to stay compatible with old data */
+ dcrypt_ctx_sym_set_iv(dctx, (const unsigned char*)
+ "\x00\x00\x00\x00\x00\x00\x00\x00"
+ "\x00\x00\x00\x00\x00\x00\x00\x00", 16);
+ dcrypt_ctx_sym_set_key(dctx, hres, sizeof(hres));
+
+ if (!dcrypt_ctx_sym_init(dctx, &error) ||
+ !dcrypt_ctx_sym_update(dctx, seed, sizeof(seed),
+ encrypted_key, &error) ||
+ !dcrypt_ctx_sym_final(dctx, encrypted_key, &error)) {
+ ec = -1;
+ }
+ dcrypt_ctx_sym_destroy(&dctx);
+
+ if (ec != 0) {
+ safe_memset(seed, 0, sizeof(seed));
+ io_stream_set_error(&stream->ostream.iostream,
+ "Key encryption error: %s", error);
+ return -1;
+ }
+
+ /* same as above */
+ dcrypt_ctx_sym_set_iv(stream->ctx_sym, (const unsigned char*)
+ "\x00\x00\x00\x00\x00\x00\x00\x00"
+ "\x00\x00\x00\x00\x00\x00\x00\x00", 16);
+ dcrypt_ctx_sym_set_key(stream->ctx_sym, seed, sizeof(seed));
+ safe_memset(seed, 0, sizeof(seed));
+
+ if (!dcrypt_ctx_sym_init(stream->ctx_sym, &error)) {
+ io_stream_set_error(&stream->ostream.iostream,
+ "Encryption init error: %s", error);
+ return -1;
+ }
+
+ res = buffer_create_dynamic(default_pool, 256);
+
+ /* ephemeral key */
+ unsigned short s;
+ s = htons(ephemeral_key->used);
+ buffer_append(res, &s, 2);
+ buffer_append(res, ephemeral_key->data, ephemeral_key->used);
+ /* public key hash */
+ s = htons(sizeof(pkhash));
+ buffer_append(res, &s, 2);
+ buffer_append(res, pkhash, sizeof(pkhash));
+ /* encrypted key hash */
+ s = htons(sizeof(ekhash));
+ buffer_append(res, &s, 2);
+ buffer_append(res, ekhash, sizeof(ekhash));
+ /* encrypted key */
+ s = htons(encrypted_key->used);
+ buffer_append(res, &s, 2);
+ buffer_append(res, encrypted_key->data, encrypted_key->used);
+
+ stream->key_data_len = res->used;
+ stream->key_data = buffer_free_without_data(&res);
+
+ return 0;
+}
+
+static int
+o_stream_encrypt_key_for_pubkey_v2(struct encrypt_ostream *stream,
+ const char *malg, const unsigned char *key,
+ size_t key_len,
+ struct dcrypt_public_key *pubkey,
+ buffer_t *res)
+{
+ enum dcrypt_key_type ktype;
+ const char *error;
+ buffer_t *encrypted_key, *ephemeral_key, *temp_key;
+
+ ephemeral_key = t_buffer_create(256);
+ encrypted_key = t_buffer_create(256);
+ temp_key = t_buffer_create(48);
+
+ ktype = dcrypt_key_type_public(pubkey);
+
+ if (ktype == DCRYPT_KEY_RSA) {
+ /* encrypt key as R (as we don't need DH with RSA)*/
+ if (!dcrypt_rsa_encrypt(pubkey, key, key_len, encrypted_key,
+ DCRYPT_PADDING_RSA_PKCS1_OAEP,
+ &error)) {
+ io_stream_set_error(&stream->ostream.iostream,
+ "Cannot encrypt key data: %s",
+ error);
+ return -1;
+ }
+ } else if (ktype == DCRYPT_KEY_EC) {
+ /* R = our ephemeral public key */
+ buffer_t *secret = t_buffer_create(256);
+
+ /* derive ephemeral key and shared secret */
+ if (!dcrypt_ecdh_derive_secret_peer(pubkey, ephemeral_key,
+ secret, &error)) {
+ io_stream_set_error(&stream->ostream.iostream,
+ "Cannot perform ECDH: %s", error);
+ return -1;
+ }
+
+ /* use shared secret and ephemeral key to generate encryption
+ key/iv */
+ if (!dcrypt_pbkdf2(secret->data, secret->used,
+ ephemeral_key->data, ephemeral_key->used,
+ malg, IO_STREAM_ENCRYPT_ROUNDS, temp_key,
+ 48, &error)) {
+ safe_memset(buffer_get_modifiable_data(secret, 0),
+ 0, secret->used);
+ io_stream_set_error(&stream->ostream.iostream,
+ "Cannot perform key encryption: %s",
+ error);
+ }
+ safe_memset(buffer_get_modifiable_data(secret, 0),
+ 0, secret->used);
+
+ /* encrypt key with shared secret */
+ struct dcrypt_context_symmetric *dctx;
+ if (!dcrypt_ctx_sym_create("AES-256-CBC", DCRYPT_MODE_ENCRYPT,
+ &dctx, &error)) {
+ safe_memset(buffer_get_modifiable_data(temp_key, 0),
+ 0, temp_key->used);
+ io_stream_set_error(&stream->ostream.iostream,
+ "Cannot perform key encryption: %s",
+ error);
+ return -1;
+ }
+
+ const unsigned char *ptr = temp_key->data;
+ i_assert(temp_key->used == 48);
+
+ dcrypt_ctx_sym_set_key(dctx, ptr, 32);
+ dcrypt_ctx_sym_set_iv(dctx, ptr+32, 16);
+ safe_memset(buffer_get_modifiable_data(temp_key, 0),
+ 0, temp_key->used);
+
+ int ec = 0;
+ if (!dcrypt_ctx_sym_init(dctx, &error) ||
+ !dcrypt_ctx_sym_update(dctx, key, key_len,
+ encrypted_key, &error) ||
+ !dcrypt_ctx_sym_final(dctx, encrypted_key, &error)) {
+ io_stream_set_error(&stream->ostream.iostream,
+ "Cannot perform key encryption: %s",
+ error);
+ ec = -1;
+ }
+
+ dcrypt_ctx_sym_destroy(&dctx);
+ if (ec != 0) return ec;
+ } else {
+ io_stream_set_error(&stream->ostream.iostream,
+ "Unsupported key type");
+ return -1;
+ }
+
+ /* store key type */
+ char kt = ktype;
+ buffer_append(res, &kt, 1);
+ /* store hash of public key as ID */
+ dcrypt_key_id_public(stream->pub, "sha256", res, NULL);
+ /* store ephemeral key (if present) */
+ unsigned int val = cpu32_to_be(ephemeral_key->used);
+ buffer_append(res, &val, 4);
+ buffer_append_buf(res, ephemeral_key, 0, SIZE_MAX);
+ /* store encrypted key */
+ val = cpu32_to_be(encrypted_key->used);
+ buffer_append(res, &val, 4);
+ buffer_append_buf(res, encrypted_key, 0, SIZE_MAX);
+
+ return 0;
+}
+
+static int
+o_stream_encrypt_keydata_create_v2(struct encrypt_ostream *stream,
+ const char *malg)
+{
+ const struct hash_method *hash = hash_method_lookup(malg);
+ const char *error;
+ size_t tagsize;
+ const unsigned char *ptr;
+ size_t kl;
+ unsigned int val;
+
+ buffer_t *keydata, *res;
+
+ if (hash == NULL) {
+ io_stream_set_error(&stream->ostream.iostream,
+ "Encryption init error: "
+ "Hash algorithm '%s' not supported", malg);
+ return -1;
+ }
+
+ /* key data length for internal use */
+ if ((stream->flags & IO_STREAM_ENC_INTEGRITY_HMAC) ==
+ IO_STREAM_ENC_INTEGRITY_HMAC) {
+ tagsize = IOSTREAM_TAG_SIZE;
+ } else if ((stream->flags & IO_STREAM_ENC_INTEGRITY_AEAD) ==
+ IO_STREAM_ENC_INTEGRITY_AEAD) {
+ tagsize = IOSTREAM_TAG_SIZE;
+ } else {
+ /* do not include MAC */
+ tagsize = 0;
+ }
+
+ /* generate keydata length of random data for key/iv/mac */
+ kl = dcrypt_ctx_sym_get_key_length(stream->ctx_sym) +
+ dcrypt_ctx_sym_get_iv_length(stream->ctx_sym) + tagsize;
+ keydata = t_buffer_create(kl);
+ random_fill(buffer_append_space_unsafe(keydata, kl), kl);
+ buffer_set_used_size(keydata, kl);
+ ptr = keydata->data;
+
+ res = buffer_create_dynamic(default_pool, 256);
+
+ /* store number of public key(s) */
+ buffer_append(res, "\1", 1); /* one key for now */
+
+ /* we can do multiple keys at this point, but do it only once now */
+ if (o_stream_encrypt_key_for_pubkey_v2(stream, malg, ptr, kl,
+ stream->pub, res) != 0) {
+ buffer_free(&res);
+ return -1;
+ }
+
+ /* create hash of the key data */
+ unsigned char hctx[hash->context_size];
+ unsigned char hres[hash->digest_size];
+ hash->init(hctx);
+ hash->loop(hctx, ptr, kl);
+ hash->result(hctx, hres);
+
+ for(int i = 1; i < 2049; i++) {
+ uint32_t i_msb = cpu32_to_be(i);
+
+ hash->init(hctx);
+ hash->loop(hctx, hres, sizeof(hres));
+ hash->loop(hctx, &i_msb, sizeof(i_msb));
+ hash->result(hctx, hres);
+ }
+
+ /* store key data hash */
+ val = cpu32_to_be(sizeof(hres));
+ buffer_append(res, &val, 4);
+ buffer_append(res, hres, sizeof(hres));
+
+ /* pick up key data that goes into stream */
+ stream->key_data_len = res->used;
+ stream->key_data = buffer_free_without_data(&res);
+
+ /* prime contexts */
+ dcrypt_ctx_sym_set_key(stream->ctx_sym, ptr,
+ dcrypt_ctx_sym_get_key_length(stream->ctx_sym));
+ ptr += dcrypt_ctx_sym_get_key_length(stream->ctx_sym);
+ dcrypt_ctx_sym_set_iv(stream->ctx_sym, ptr,
+ dcrypt_ctx_sym_get_iv_length(stream->ctx_sym));
+ ptr += dcrypt_ctx_sym_get_iv_length(stream->ctx_sym);
+
+ if ((stream->flags & IO_STREAM_ENC_INTEGRITY_HMAC) ==
+ IO_STREAM_ENC_INTEGRITY_HMAC) {
+ dcrypt_ctx_hmac_set_key(stream->ctx_mac, ptr, tagsize);
+ dcrypt_ctx_hmac_init(stream->ctx_mac, &error);
+ } else if ((stream->flags & IO_STREAM_ENC_INTEGRITY_AEAD) ==
+ IO_STREAM_ENC_INTEGRITY_AEAD) {
+ dcrypt_ctx_sym_set_aad(stream->ctx_sym, ptr, tagsize);
+ }
+
+ /* clear out private key data */
+ safe_memset(buffer_get_modifiable_data(keydata, 0), 0, keydata->used);
+
+ if (!dcrypt_ctx_sym_init(stream->ctx_sym, &error)) {
+ io_stream_set_error(&stream->ostream.iostream,
+ "Encryption init error: %s", error);
+ return -1;
+ }
+ return 0;
+}
+
+static ssize_t
+o_stream_encrypt_sendv(struct ostream_private *stream,
+ const struct const_iovec *iov, unsigned int iov_count)
+{
+ struct encrypt_ostream *estream = (struct encrypt_ostream *)stream;
+ const char *error;
+ ssize_t ec,total = 0;
+
+ /* not if finalized */
+ i_assert(!estream->finalized);
+
+ /* write prefix */
+ if (!estream->prefix_written) {
+ T_BEGIN {
+ if ((estream->flags & IO_STREAM_ENC_VERSION_1) ==
+ IO_STREAM_ENC_VERSION_1)
+ ec = o_stream_encrypt_send_header_v1(estream);
+ else
+ ec = o_stream_encrypt_send_header_v2(estream);
+ } T_END;
+ if (ec < 0) {
+ return -1;
+ }
+ }
+
+ /* buffer for encrypted data */
+ unsigned char ciphertext[IO_BLOCK_SIZE];
+ buffer_t buf;
+ buffer_create_from_data(&buf, ciphertext, sizeof(ciphertext));
+
+ /* encrypt & send all blocks of data at max ciphertext buffer's
+ length */
+ for(unsigned int i = 0; i < iov_count; i++) {
+ size_t bl, off = 0, len = iov[i].iov_len;
+ const unsigned char *ptr = iov[i].iov_base;
+ while(len > 0) {
+ buffer_set_used_size(&buf, 0);
+ /* update can emite twice the size of input */
+ bl = I_MIN(sizeof(ciphertext)/2, len);
+
+ if (!dcrypt_ctx_sym_update(estream->ctx_sym, ptr + off,
+ bl, &buf, &error)) {
+ io_stream_set_error(&stream->iostream,
+ "Encryption failure: %s",
+ error);
+ return -1;
+ }
+ if ((estream->flags & IO_STREAM_ENC_INTEGRITY_HMAC) ==
+ IO_STREAM_ENC_INTEGRITY_HMAC) {
+ /* update mac */
+ if (!dcrypt_ctx_hmac_update(estream->ctx_mac,
+ buf.data, buf.used, &error)) {
+ io_stream_set_error(&stream->iostream,
+ "MAC failure: %s", error);
+ return -1;
+ }
+ }
+
+ /* hopefully upstream can accommodate */
+ if (o_stream_encrypt_send(estream, buf.data, buf.used) < 0) {
+ return -1;
+ }
+
+ len -= bl;
+ off += bl;
+ total += bl;
+ }
+ }
+
+ stream->ostream.offset += total;
+ return total;
+}
+
+static int
+o_stream_encrypt_finalize(struct ostream_private *stream)
+{
+ const char *error;
+ struct encrypt_ostream *estream = (struct encrypt_ostream *)stream;
+
+ if (estream->finalized) {
+ /* we've already flushed the encrypted output. */
+ return 0;
+ }
+ estream->finalized = TRUE;
+
+ /* if nothing was written, we are done */
+ if (!estream->prefix_written) return 0;
+
+ /* acquire last block */
+ buffer_t *buf = t_buffer_create(
+ dcrypt_ctx_sym_get_block_size(estream->ctx_sym));
+ if (!dcrypt_ctx_sym_final(estream->ctx_sym, buf, &error)) {
+ io_stream_set_error(&estream->ostream.iostream,
+ "Encryption failure: %s", error);
+ return -1;
+ }
+ /* sometimes final does not emit anything */
+ if (buf->used > 0) {
+ /* update mac */
+ if (((estream->flags & IO_STREAM_ENC_INTEGRITY_HMAC) ==
+ IO_STREAM_ENC_INTEGRITY_HMAC)) {
+ if (!dcrypt_ctx_hmac_update(estream->ctx_mac, buf->data,
+ buf->used, &error)) {
+ io_stream_set_error(&estream->ostream.iostream,
+ "MAC failure: %s", error);
+ return -1;
+ }
+ }
+ if (o_stream_encrypt_send(estream, buf->data, buf->used) < 0) {
+ return -1;
+ }
+ }
+
+ /* write last mac bytes */
+ buffer_set_used_size(buf, 0);
+ if ((estream->flags & IO_STREAM_ENC_INTEGRITY_HMAC) ==
+ IO_STREAM_ENC_INTEGRITY_HMAC) {
+ if (!dcrypt_ctx_hmac_final(estream->ctx_mac, buf, &error)) {
+ io_stream_set_error(&estream->ostream.iostream,
+ "MAC failure: %s", error);
+ return -1;
+ }
+ } else if ((estream->flags & IO_STREAM_ENC_INTEGRITY_AEAD) ==
+ IO_STREAM_ENC_INTEGRITY_AEAD) {
+ dcrypt_ctx_sym_get_tag(estream->ctx_sym, buf);
+ i_assert(buf->used > 0);
+ }
+ if (buf->used > 0 &&
+ o_stream_encrypt_send(estream, buf->data, buf->used) < 0) {
+ return -1;
+ }
+
+ return 0;
+}
+
+static int
+o_stream_encrypt_flush(struct ostream_private *stream)
+{
+ struct encrypt_ostream *estream = (struct encrypt_ostream *)stream;
+
+ if (stream->finished && estream->ctx_sym != NULL &&
+ !estream->finalized) {
+ if (o_stream_encrypt_finalize(&estream->ostream) < 0)
+ return -1;
+ }
+
+ return o_stream_flush_parent(stream);
+}
+
+static void
+o_stream_encrypt_close(struct iostream_private *stream,
+ bool close_parent)
+{
+ struct encrypt_ostream *estream = (struct encrypt_ostream *)stream;
+
+ i_assert(estream->finalized || estream->ctx_sym == NULL ||
+ estream->ostream.ostream.stream_errno != 0);
+ if (close_parent)
+ o_stream_close(estream->ostream.parent);
+}
+
+static void
+o_stream_encrypt_destroy(struct iostream_private *stream)
+{
+ struct encrypt_ostream *estream = (struct encrypt_ostream *)stream;
+
+ /* release resources */
+ if (estream->ctx_sym != NULL)
+ dcrypt_ctx_sym_destroy(&estream->ctx_sym);
+ if (estream->ctx_mac != NULL)
+ dcrypt_ctx_hmac_destroy(&estream->ctx_mac);
+ if (estream->key_data != NULL)
+ i_free(estream->key_data);
+ if (estream->cipher_oid != NULL)
+ buffer_free(&estream->cipher_oid);
+ if (estream->mac_oid != NULL)
+ buffer_free(&estream->mac_oid);
+ if (estream->pub != NULL)
+ dcrypt_key_unref_public(&estream->pub);
+ o_stream_unref(&estream->ostream.parent);
+}
+
+static int
+o_stream_encrypt_init(struct encrypt_ostream *estream, const char *algorithm)
+{
+ const char *error;
+ char *calg, *malg;
+
+ if ((estream->flags & IO_STREAM_ENC_VERSION_1) ==
+ IO_STREAM_ENC_VERSION_1) {
+ if (!dcrypt_ctx_sym_create("AES-256-CTR", DCRYPT_MODE_ENCRYPT,
+ &estream->ctx_sym, &error)) {
+ io_stream_set_error(&estream->ostream.iostream,
+ "Cannot create ostream-encrypt: %s",
+ error);
+ return -1;
+ }
+ /* disable MAC */
+ estream->flags |= IO_STREAM_ENC_INTEGRITY_NONE;
+ /* then do keying */
+ return o_stream_encrypt_keydata_create_v1(estream);
+ } else {
+ calg = t_strdup_noconst(algorithm);
+ malg = strrchr(calg, '-');
+
+ if (malg == NULL) {
+ io_stream_set_error(&estream->ostream.iostream,
+ "Invalid algorithm "
+ "(must be cipher-mac)");
+ return -1;
+ }
+ (*malg++) = '\0';
+
+ if (!dcrypt_ctx_sym_create(calg, DCRYPT_MODE_ENCRYPT,
+ &estream->ctx_sym, &error)) {
+ io_stream_set_error(&estream->ostream.iostream,
+ "Cannot create ostream-encrypt: %s",
+ error);
+ return -1;
+ }
+
+ /* create cipher and mac context, take note of OIDs */
+ estream->cipher_oid = buffer_create_dynamic(default_pool, 12);
+ estream->block_size =
+ dcrypt_ctx_sym_get_block_size(estream->ctx_sym);
+ if (!dcrypt_name2oid(calg, estream->cipher_oid, &error)) {
+ io_stream_set_error(&estream->ostream.iostream,
+ "Cannot create ostream-encrypt: %s",
+ error);
+ return -1;
+ }
+
+ /* mac context is optional */
+ if ((estream->flags & IO_STREAM_ENC_INTEGRITY_HMAC) ==
+ IO_STREAM_ENC_INTEGRITY_HMAC) {
+ if (!dcrypt_ctx_hmac_create(malg, &estream->ctx_mac,
+ &error)) {
+ io_stream_set_error(&estream->ostream.iostream,
+ "Cannot create ostream-encrypt: %s",
+ error);
+ return -1;
+ }
+ }
+
+ estream->mac_oid = buffer_create_dynamic(default_pool, 12);
+ if (!dcrypt_name2oid(malg, estream->mac_oid, &error)) {
+ io_stream_set_error(&estream->ostream.iostream,
+ "Cannot create ostream-encrypt: %s", error);
+ return -1;
+ }
+
+ /* MAC algorithm is used for PBKDF2 and keydata hashing */
+ return o_stream_encrypt_keydata_create_v2(estream, malg);
+ }
+}
+
+static struct encrypt_ostream *
+o_stream_create_encrypt_common(enum io_stream_encrypt_flags flags)
+{
+ struct encrypt_ostream *estream;
+
+ estream = i_new(struct encrypt_ostream, 1);
+ estream->ostream.sendv = o_stream_encrypt_sendv;
+ estream->ostream.flush = o_stream_encrypt_flush;
+ estream->ostream.iostream.close = o_stream_encrypt_close;
+ estream->ostream.iostream.destroy = o_stream_encrypt_destroy;
+
+ estream->flags = flags;
+
+ return estream;
+}
+
+struct ostream *
+o_stream_create_encrypt(struct ostream *output, const char *algorithm,
+ struct dcrypt_public_key *box_pub, enum io_stream_encrypt_flags flags)
+{
+ struct encrypt_ostream *estream = o_stream_create_encrypt_common(flags);
+ int ec;
+
+ dcrypt_key_ref_public(box_pub);
+ estream->pub = box_pub;
+
+ T_BEGIN {
+ ec = o_stream_encrypt_init(estream, algorithm);
+ } T_END;
+
+ struct ostream *os = o_stream_create(&estream->ostream, output,
+ o_stream_get_fd(output));
+
+ if (ec != 0) {
+ os->stream_errno = EINVAL;
+ }
+
+ return os;
+}
+
+struct ostream *
+o_stream_create_sym_encrypt(struct ostream *output,
+ struct dcrypt_context_symmetric *ctx)
+{
+ struct encrypt_ostream *estream =
+ o_stream_create_encrypt_common(IO_STREAM_ENC_INTEGRITY_NONE);
+ const char *error;
+ int ec;
+
+ estream->prefix_written = TRUE;
+
+ if (!dcrypt_ctx_sym_init(estream->ctx_sym, &error))
+ ec = -1;
+ else
+ ec = 0;
+
+ estream->ctx_sym = ctx;
+
+ struct ostream *os = o_stream_create(&estream->ostream, output,
+ o_stream_get_fd(output));
+ if (ec != 0) {
+ io_stream_set_error(&estream->ostream.iostream,
+ "Could not initialize stream: %s",
+ error);
+ os->stream_errno = EINVAL;
+ }
+
+ return os;
+}
diff --git a/src/lib-dcrypt/ostream-encrypt.h b/src/lib-dcrypt/ostream-encrypt.h
new file mode 100644
index 0000000..0b28547
--- /dev/null
+++ b/src/lib-dcrypt/ostream-encrypt.h
@@ -0,0 +1,30 @@
+#ifndef OSTREAM_ENCRYPT_H
+#define OSTREAM_ENCRYPT_H
+
+struct dcrypt_public_key;
+struct dcrypt_context_symmetric;
+
+/**
+ * algorithm is in form AES-256-CBC-SHA1, recommended
+ * AES-256-GCM-SHA256
+ *
+ * Algorithms (both crypto and digest) *MUST* have OID to use it.
+ *
+ */
+
+struct ostream *
+o_stream_create_encrypt(struct ostream *output, const char *algorithm,
+ struct dcrypt_public_key *box_pub,
+ enum io_stream_encrypt_flags flags);
+
+/* create context for performing encryption with
+ preset crypto context. do not call ctx_sym_init.
+
+ no header or mac is written, just plain crypto
+ data.
+ */
+struct ostream *
+o_stream_create_sym_encrypt(struct ostream *output,
+ struct dcrypt_context_symmetric *ctx);
+
+#endif
diff --git a/src/lib-dcrypt/sample-v1.asc b/src/lib-dcrypt/sample-v1.asc
new file mode 100644
index 0000000..c69fb50
--- /dev/null
+++ b/src/lib-dcrypt/sample-v1.asc
@@ -0,0 +1,48 @@
+Q1JZUFRFRAMHAQCtAEMCAMyKuGO/j3TPoXzRJ39THa1oGDChmueqRVFlR3qnIWcd
+Mt15zp+juTJzwfxKDNsgdIfFleIbuuo1AX1TgimaVfb8ACB8mhA56i5P7XPoHdP/
+w/oi6kooNSk5rd57+OqFiwD6TwAgWi/IHZ3tFmaohetUkFowgcrYwMh9HR9iOXg6
+QdIDnqMAIIrC9OcLyuMzUp18LpVKZLg6QaJEsjrepBatgkqRDgBKAAD7tnI+Rjjg
+rNZ5UHYvjA1xsrEhfbyx8X6Vb+em++p+aE+I92pBrqV/XIeR1er1oNX3nxZwEnL4
+UwhavZOMw7Qna0o4bop4PfK65HqnFTmgaiNDBMdE/CFaxSRlI0PLc0jhqxoEYU/d
+0hrtPcpQMq0sCxBbHqcnDF1xAK2hAZU12BH+JoV4bI1k1MMn1xAcXxiVdtSO5NE8
+E5fyaMfvTq2zIZtqQY09arrd0DaQ8o/L2dIV6jQVNZLxbRFWVayoWloT/YVSlHhi
+w5YHJetffXO02mllj3Mxr5aIfCmpZbrcWMfrF88ksI6HyQOrTHKS+Y95+VFLbxy4
++BWFGKV4zUGUwrfEDOpxIAZbBHsABjV82NS2TEZltu/ki3EhlwC8Hy+oyqn+LZN5
+LCmbt/maMI0EJU6cNCUCQM8Rq2Xv7xP/DrC3A0y7gj3pT44dY0dMj76gBApKO4Qw
+rLcBgY9qycXHtUwvtg/QZJrb2n2AB7h0+B3LgVm8P12l1KAFS2ykugBWUVJSuUjK
+5fjTk4EDIaCm0rhs2hNty9OtBkBuQBolkzxHtqp9/+QhIWJEtNtQEdnwN8DtdlNH
+/p69sZvkDhmyqSuByCodwVhkPZf5d/oGVUvE73btlAJcl8NZMXDqgKHfT4U5OF6Y
+eSXUIz9oiM2Wy1CVqrA2JdFGQ4Hcbf76IP462+gGefOd62atFfvjGGIb+Okyrmab
+jNxn/7wtUw42MXIzoAk+GQsOgo77rH075mILWqp0OuAyRTKmsZTP3FaDb4SxQk3K
+pw3N7HiNIiDW1wd5BLJE73qxKr+JC8GLs/s+JpfVb+lxzXAKXpEZTGFd/zphF1Kf
+J+aBCF+UB5Lq+QYoHfKSJzRb24PScAVs1VrV8nJlQvxMZW6Rd4ofNcsMjY+b1pBx
+bv74+oACEXC/F1jpp3tMTTLOLN8/hNq0J1mE3uOYRAaNK1jaQQAoa4rNxeY/2vBw
+mod8/Erc9M0M4B3VHVfRJ4F8MAx3b8if3oiicLu0OJ16snGHM7BGp2pOEPRVAg1Q
++70zDctuSV6HwPYSAXaIw2LPrbsQmq1Lq/JivHYRwjUDBFAaXkPv2WFEfUsXEtmw
+UzUPLgWxZM8MsPDEI510SzRd3OlBUjuS96+/9n/Qv1D5DBnrDH9gUwUiPr9V+rMM
+kmNaAQpTKpCNpevCH/F18rQnUi/PJKY37q8pF/lO0OW7mzS+CxCvFr+aWxZ2kV84
+GwUrUuSdHa1Z/0BY/UTZn1BAi6bOudiWHH5loJBldreSJ2K2/iwMpLUUEuQ+TJJ8
+BvsWeOLMqEpDIxuuYERCUqHO8EmtvsSPKcIeS7ZZpvkRlRIWCuKgnaHSymT87Eq1
+SJskTkAWd1iGvQI78tM8KwT2KDmf7Qs3RUCiSynT/H1OmVQqwVn/5c9wFjV+0PRA
+KFZZDEMvwy5tYpJ1Y0nYuUUMOlA8l11rpKqAIcRj0V256uXoj5cBnTsGZAOFDBqx
+pBLmGFgz1havj8RsgqtJfCkkh+Y8l9xszAC98/FGYOtmKpP4sXXM3LASkhi2FU4C
+OH/4tUsoh3tMCendx36s1UmliE94BiNuJMUAqSh9cLCnW/Uiw0bzqV8GOJLDk/A3
+XMWrERuA2Jn7qQ9e9qmYarc8r6JjWUuxECZ04d12wNCcDF7hWxFYLbwTk/ZUIM5D
+1ZsdOPUQDf7gjEW4gQoOK7pQWifJa56ZSSFurLoL5ae+3dPwUu8HNQLvwmx4paSe
+01shQd36MXcRZ7BRVp0GqNrviuAtXacSxx1GIO9rh7RtyGGs1grsQ07P6Evpk0k/
+1WY48cE+xWU5SH4JwxMZ3vbDguMY/cnp2VhuzZguJ4iIFKg5RMVShrSkZQcWwH7e
+7JVu7hOe3bWp1KeVG41IsOFpo0Jfpegtgf4r1hYih02Q54UNIFf5G3IRsbC1pjtj
+ALYteCLe9oa+7lIAVDWgmq/NqWLsi4dtlz97TG8XApIFZ6Prr7KG8N+RFTmouXYT
+QH6FuF5XvJ0TqIgIkdSzbuaNmN47E6PQoDAuRJ4X9ahYpF1xC4ecnAaI+rlaparF
+Z1OOtwYVQ1Hthc7wp+205aK6ujZyU6a9MrGXGZklRQdigCd+EOs0kWy8t+bcmk6K
+9aXEJyGMBtcgQDGWZZJer5w8aUKj6SDp9y7X1kyAuhh8DFvNKMgR3vs4wnvsjr1W
+cYxH8RvAVXj76Xjvvgeg3jcJbcr0mPwB0SrcpQ+88mF64x+nrKsXmz9E8wBgOMY6
+4qXXb27YUehAGN81jTZRN8lShl20fnPZKd5U1sqIhevXNUrTVjxVNff0fHRnOOb9
+sVD3vdQmfbXvB3nyQLKQ+Wcw/WPqa0But5KEXFjnaXPORcEWEpYvWOgGPegmrTgh
+P4ZVscRdxozvd0sKvpLNMd/EiNLBaYft+4Yo38WZYkZzNJT7LhW41pbQyK1cmZiU
+COisSP2rrus6iKCNwaGPMZJNCCdQN862DSTaa0IZSEeSRpfBfA2UevSsfX0uTgyb
+B4Az1u4QJv9fQp8oAnnpH7YHhW43V/YTVuwLLF7pqSl2J2gNcLFfGuJVftRp6xJg
+mkfXOxoJ0y5CQU7pwQZ/F6WxuWBX1m7MNE/OfI9l9ODj0uAgKn1e+DzH1VXZi8ls
+lenQuepkt7zqXG5dRRhW+FSlJy1oo9oPcf2bZhAYMty3mh9F4Ils3SSCE6T6cU0A
+yYCNrq8iKyq5V1jdC9haN5NmF9yNiIxYWdbigcBTzR5+AZuNe7aJSXu0qvQJR9s0
+d/l7J6LsH25I/zsIV/0OHcrvMUEf2WzU3Q==
diff --git a/src/lib-dcrypt/sample-v1_short.asc b/src/lib-dcrypt/sample-v1_short.asc
new file mode 100644
index 0000000..4bc4c06
--- /dev/null
+++ b/src/lib-dcrypt/sample-v1_short.asc
@@ -0,0 +1,4 @@
+Q1JZUFRFRAMHAQCtAEMDAA+5INzRNr5OAicv3XI4jh//ufjZN9yYr7mElHKNGY+D
+D2ziqHhPKVra6JzBzZvfySntDnDvdLAomafpDVlESMlkACB8mhA56i5P7XPoHdP/
+w/oi6kooNSk5rd57+OqFiwD6TwAgu4C6eZWV+Spojlk8eOAw784ySgMHK8gDrXhA
+Jwg34GcAIPX4RmqXh+7QTAQWtGNHQvjqSygBxSUYkQ3KfPLMymKvAACMLy6/
diff --git a/src/lib-dcrypt/sample-v2.asc b/src/lib-dcrypt/sample-v2.asc
new file mode 100644
index 0000000..f9b2be3
--- /dev/null
+++ b/src/lib-dcrypt/sample-v2.asc
@@ -0,0 +1,287 @@
+Q1JZUFRFRAMHAgAAAAIAAADfBglghkgBZQMEAS4GCWCGSAFlAwQCAQAACAAAAACv
+AQKrE9JRl23tq1RrZzVOdniCF0DdU0t0nChX9mv2K7rd/QAAACEDwvjlcRkV+Zrl
+JQIYgGVPawwFMOUC/ezeeEhcxKeS7MEAAABA2/Y3MaAjsrRPX6pwn2K3Cd8fUZcz
+S27nzQpUedF2Snt5neblmGVSSwJjZCbnmK91SHmb6TMxflQUntMvjfna0AAAACCD
+ICFyviOkFxwR4KhIlPV5v5CIINPw5PYmg7m1JcJoAHQSyJAiN7SnaDrV/DZws6zu
+/Pe6tI6yjY8bIZyVw9sOIhL1UHR8z+RVsn6wwuHqHJhl32isMG2u3I4Q7pY8o/or
+xcfJZj/HimYKmBc/1Wg6RSLQLIMEg496hFVHBJ4RC+rifhlf88j4s2Txyn+fO9al
+oqa4Z2hMuW/ad1NlieWNr54/6xbeZbpP17KayiYQPz9CFChAD+L2Rxg3DeJARWBY
+ywaPmy/NaoNU4+Cllbc6zfI0uvZySe+cYOYnKBi7G3ZuENdmHkSA/JWX1BjGnRdc
+TRG1vIdit8rL1MGBc3zvKqT10h+ZOOedCEVBi6KXOZAe9FM9DK80CnHjOr9BGNtq
+mM9ag1sWDAMPvlCK/1+/0A/CyBebjYqoeVF09Q/TrPbNSzdB/AF+wlS/uS+W3ope
+Ny5+vHz3iO6pKF0gnepFfyk0Eav+5efnOS8Mr3FhQCWIBm7jBCpmyIUlIZ+oE+D3
+2qmNyjMpaYB6WcXplQXbK4yTL1kty5BNP++92yuOA22CTZgIy+k0ZK1N6yScnzG4
+Bya1KBlbBGOT92/HYqfm4YZ8hjJ/aCBK36UfDnsTIOHx/dm4jUCaGo7MOAmOfTxJ
+mSnzNyXclbo2+MNK3YFLarkcT9XI848ojnAWOpZ1x/062oXIILH3Q/PSYXeRlzhQ
+yWNCXU3b0liyo7vl+l/jRSYlhPGvSO5ZUoDJD/KHk6xrZI68pZ0oogm8ywSgMlpQ
+VYjTTdaUAUEcueC/n46WTITSvIEf4RfdXjE/r7MStZfd0ypofNJDo9H/xQRWctzq
+Go9Ta2jtmOTFcy1JAuUbYGe6SyG7MlOAEprLnwl8gji4uXCb2yD4pYTmDbRwcvbi
+Xldcbzi7XIA3cbeDWJRiBsUh5OkbKKgK9UpbYLuZyya4H6mohVfC/bDFPlSezbzX
+z3WGNwNRY+mDIPCDXP4qgomiQ0SkK8gmPZY+q8YNePgB3CryNmea4Bw5waAJ6DHY
+66D9jL37IFjG/mOXqwIinwDvxdoWI8SjwqwFzUsjRnUe9zkIBAV3+KKU7pkDrLe2
+1K9ZvDhrVvHgYPOOkQ0UxEvkb2QamLlAV+1Py68kKKouVcwF3KTvq1bJRMlfLEFp
+f2d/agN92yMac1jCC42hUDYNZrGMBTtco+ZqOJJ5xZouYDTiLP9lgHfO9sHZ+9pk
+Nf39SlnDfsYeRhOTAlGCYVEkEGDxWU+5CTZRHDTZYXU26a0Pyd82dE47QHqGikqk
+sFIqtIaxuJEXDva5gmtBgDf9u1xSUe4r6CXtymT+zOjp2Zj9+lu9ggB+hpymgeRL
+CjzTk2lr6cx9n63k0U8QFRTz5N2yNvubD4mv6RkqTY8xf+q2WDD/aX61uImw/ltC
+EtYJfxKHRHAeXJDlSN9DLZ3+lhfVmEyw6MdeeQH2hpvAKTZeXcPuMB2PRwB8+vkK
+LgFNneRn1Tuztirs9sPtnErgaDEyNGF/o8xrDAQ1mnzKR9tWAPtXKh119snDps4N
+wwiYGkT3pAsxDoh4LiJWOyks4PRKX+1Ly1BGCPoRX3/t/QaPmN61vXSHnwUQVCJ8
+kaxyqOl43eQbKCdw/+EfyPyyp7I7w4IShUe7g4RtHbxltNcNvQTyztxdkYDWiFQ0
+C5aDmaqtaYFB/lqOFKJNC+SOCrleSqUYnJj/PWf+jZuZ11zjI0XAlXVY1mbxjEG8
+RqlU4HAtcUa78AACc+vBJ6HD3KydpKB+R1kBCw9VTWiNx5o2TmS0uw3uimzBrkWE
+VM2QntF438DJl0d5fGWeG89KqOFM10sDyNDQ+tjserDyaVKHBuErDC/C2BI7YFA2
+63mfRTn4Yt7HltFch3MRFo9x3iRLrkqRuww2kCvZTauC9YxqZgeuD5XYDrRwbx2A
+WkF8bL55jnODO/RWxOthSv5z2OTWwqCCw8/SE6IAIRdGnIH5nu6uoK90NfxsN/iG
+c6VjqY8HXY1mCQwdTFKmY1gsGLVgyKPbugF6LMOjEvy6mN3GiJvd+Ncs5/5QPVeh
+gakujGRmkwQ4PMGn56mU7MvKoJ1jq4GPV3DDjSgTwxxJn0GUKihTnslR+lI9mJXD
++riKQAzT6POjfvWnQ9bfhVmPrGU/rPAAzi02jUXwXCXPfDUBqiD9yixGCGF2gHeW
+rXFqQvDYU0DuetOePprlC+IfqgMJHTM7Eepqi0Kx+vygH4wnu/JJKcSmCte/z5lH
+iVM3c4SZVMnBWhfhN+TWnuFoPuUGVgisLePoi1NsAzbsh5GbvImLgVVNbSAZmWCc
+nbnh1VXU0MNF5Ovyx6XAWUeHnhosIuLP/zQR/ne0QgOYUxE61dJ5iRnE1oA2hrJm
+UeISJ6QKzJtf690EtqnSq8qzjOLsOlvNgf6O030vc9Wgj2PqSK93Ai5kNb5r2iyX
+0y/JIJaEPN5mjS4VVZDgcQoo6tRQWF9+iGYgPK3vGqeAOLrorw7UG0FsvkNqm2US
+Kqi6/65jEdmp975vuRwa4o0zCfuWc9OZ8UZTkREmouc3fEkvaHoOKJm/O68E6aHU
+WlpgYvLWLWychCWMqr4tOBAarTz+UZGI6wtORYTQWXPbfl4YtmJ7fxuykNtkLC1/
+Jw+MHZ1WmA1csvalfYvcY/rvNDlkdpM7iPeKLpAMspyWueXiYl5cCEJ7R4eexs+p
+71juVPhv1UvBtR6vcxjd9PryFCeMpJDvLUwtNq2taRXTFFa4pz3+zl7zWA5gqIag
+HSd0khr00aF8fUQhTfSVUg6qYo/FOAeinJMfSsyXnJ/R7WOtZmi7UkuC63zaA4nr
+HXXtrefd434tHUuWQP+76Fwmp62tlPFlfiMqNRipIRUi2JLgRXws7A7DH9msSvAz
+IHD9lxR7KECQWkJUhGhTOWCTUeNeVdyeAZdLD+bGkicvaQlFfyaqxm0XCqdJEviU
+PngsGLaqgEqfMGUkOObd4W4MlRw6X+77pkQf/l1FktSm4Ixo7qMxDz0En7irYvlj
+BDCsVyig0MM853I8cYoMHkd33HAnMITS2LkFfjlwUFh91rZlBQawFt5JWAbx0UaR
+/dJOHkxEhyX6yW5+PWMxazPyiVLAqxErsOspJXlUI77zucII5REDHnRL5qo0+9x8
+sR4a9AyUch+oOgFoHMTKeNEMoFtVfNmpeyc3TCZmPQFNFb4KT+jrpQuH8ohREdGh
+/73Np9ufGBAeGqOhkIWNGD+VTZqFATANkbRJWzGSKoG53W4XFrYhXEx0/MmHy3Uo
+a9pvN/nspChjMncoe0x9ZwO4S/Zxr0JWuikFzvUfEqVck3R99Bcd5dD67qHx8Wih
+s7m/Q3RxLS+dU/1fPa2SA/tVGC0OciYmr7vDZArsUscpYehtMqbpgaQPDJI07x64
+ilQiNg2ZtFmTh8VHQIQyf8zdE9jtFhKJwrT/M/MN1st5v0dfBbohz7Q7TGDct9Fu
+4+JwdpeZK+dm0yxNT+gEhQOAuK5+rBX5wsr1kZ5nohg0nrGAQ3Kl2z+QV4u1Xnr+
+d4W7TBpsy/ugq4ZV9/T/p3DiObPl/kjjjipsJCHN5wpRGr7YeDWUbKRq/GNqKNrW
+6CpyyYnMFK6PfP85u0poDXlW2DX72/s+bf0pOJIHjRl3BoKhhQpQbQADb/3aV0Ov
+/3/RKN7zN8Z2ctmOmbCeFpp/Mren56kM/TjnTZs0WLwesShcAY0JNCQ2QcZRFHkr
+8SQjlgVPOVVdP1835X2muQgnbyoQ7VNAplA3xsVlvtmOQRD10LOAMF/8s+EF0yZB
+keieB84j7eHZ9u2763pCMNAokhQ9FcGskEOqM/iNmTtiS0xL4NFfRlKXb7zZcpVT
+bqLg1mi5c2PLLJls2dw5gfgz6TNcnBSaDn2zhnFll4kuZqxhz5i/2RfPQgicMlV7
+/3MDEhTzm3qVhf+S380qatNN94JK0QAn37oKZiK+GTHs69L82YbT9ILMKNXipT9t
+iViUHzZnYrffayW2+vLWFfjkvkJDrawxcVZ52RuXq0S5qjWmVrkK2prv97PTpNOx
+H1W4ap+AFQqGgQkoQjqZXTKEr2DIIhg4Ntt/kqeEMLUdS26lKFBSx81JOZivBmby
+NXIFpNw6FG0RdjAoTt17+ka2hQqk1tya2zIbL+ppQBwJblLx6tEKTfD11QDunsMS
+8mM1K6zw1Js2BYiMwpwlN5knRXPzy4oC2iWG5dan2IxW2Fd82zIMLPbeCe6PkwMV
+FEhaR5l8GYrpYHIgK2JaZm183sOAsav4RDsmc8bio/crrGnSu1B/apMuf79xqXvx
+ygwRi4t0iborisRsVJveG7+IVf9I5ZHT+WqeEhkhh+lJ4QlRN0JfxuX/cy8I8Pd1
+0oHztq+Eyx551a4zgrVl3nDuAZDlByMZlXZCQwwAGMIOCmBNp07WmZ58XZOfQ6Dy
+rQiHY2gYYI6wGwy/aiiy7BzR0/JGTfs2o1EfHwuI+95D8gyjbDX9VlngNmbpfisK
+9mOx2cLBc2aOIu4mYFmpnL7G5gUOz6M2G8MDX0J/NraMcvFSlCT6KyuMzmBnSIHW
+mmI1k0xKZ+rPxmad8arpZ37KXsKQaRmhYo0WgDSABGnW5lE/9ZJxOj92fgywFLB0
+a58uyxF8M/XagJAE6teU4A63xxkiWNj2ZCG9/tndCrzAIe8rMqNbWD15x7tw5LlD
+GZI+DWVstLyNvaXOhF+Nt+oMwEtMVqxZCO4/1I1zBIS9ginTaw8lE76oWGkwbnP7
+5LZb4+BJqPyvaIrHijZR3l/qbK9r0cWdOHjNjKYFPEouz2vuYd4Mh5+wlHcoixw4
+SpQ3ZQMBJCfanZXAhcLUYsSp0fmXPmmdPnQ7U6cuqlcurJhQt0Ks8LCKS/cytezK
+89q1XTrJ75etdwGfuCMRCInU+lqIvzxZiOHrn8Nxcg48HOvgfo6r9dnmwa//7Lgz
+jfGJgI222B6/KXeTPyqDhbJP0/gc09tqsmTYMKw/w9wdu3ehbu1lSqALx76O9SgB
+cl+HDfHNgTt5Yd3BpGbEgG2G+Iin2v1DPXTu/cMYynXmb5X78CPcU9iVc7JbHFVa
+kepWqFdqRrUuZ2u92N7fm2DgqcBcTex5VRLnN+pAT8VZM8nJIRGgUl0YXY1C5ilN
+lcQm0jHI0StOrtr6TcFmw6Mru3v+xygWy185js+Y9KZaglmuF7iTsmBfAYfWW8hj
+8Rx9jL8Gi54uxel9sL0y4lSA3veX8eo7k3euvGKKE0eSuHYq0XAKAMJgW4DeRmQU
+iilg6h1AEdbTDxAlkwwr0bKMcZtsidLX6dxIe+zZj6Hck224ms29Sbid/RLFGqxb
+h97dTz+YwK3dyC5LhD/2txVCEPhBZW3x5sBwbORzllI7WcqB+i/ILlqJwAJ5bd8q
+x0NP6+ZiepeTSUpeZAL/Z3N49bIO1QNDItvjYxQAixpliMoDlQZPPzPRWtZB/jkV
+BeUjeiAzD5gcQ8h1EsaRtoESEI/QX/e/kfU0YtW+jYrt0l3GS9rs5IBzod6Z/Igh
+9wSZ8JIjnUhEwlibTTdr5xk+K07u6LlhFs3Ho8lzz9bS+YxqOfp/htfe3Mt2MWhl
+MCCMl1mLVGUyK63znvWeki6TATrqnqokxST1BMKA0rSRxRpzJIbMcAkn5cFLbyZt
+nP962O8+C8qgTvH+SIB/st+9YtqpluN/gOIzb5tcpI2bwo3N7nZos/uJwYOybh8Z
+CQgDJSJqBatkldu0ZG0i226k/APO5MkyKQflyxaOvIL/f79i3tSYDIECPKNf1Txx
+oXkL21hHPbflnpbbw1Bd8Qc9Cjr2Ku8H4Gy7xSE1k+KNkJQv4VZ7nZX/35X/hAS9
+Wxk/BtVh8jJQ6BOsB/6nS1i9FIrQOAl1XmksvRzXBRdxUnRSiqa6ctGPJF2/Pvcz
+IkceEsdrFLrG9twoLP8XEYlJ06F/aLCROhIGGuy5MtMuF8kSWvDu6m5bZVARm2V0
+VvtcuEb7OlOwKA/vsC771zEGqnojpIYuRywLu7W1fS0xbwWMFzrAHHadgRlW+MAA
+DaKbPtXMPjyx5euHkCk0Fx+gkqK7N7MJQzakbcrhojKIR/1lZdi/lq16hc0f4TJV
+OUZETZL1UqY2q/RU8kdDySa3sLc8RqqKB7mwNswQQL4lMX4PlF2FgbdOKCJusjFT
+3BEA0/7YKwfV5x3h/OU/dUPSGykpb7zcZBVmJcWIzPy08xTevNb7L/Efgamfrn2q
+InzZVdp9pa5FhajpbZ+bCAQ2xnM56ZBV7o4Fw7YaXVP+yJ6EdJyFyfalHmWcIfFQ
+M/CtTNK2Z3y+01+YxrQJ9EcK+v6+kjyDMsWTfJgWgCyn2dzQ5STip0Dd33wXj8+h
+2xep/nQ8ez1O3AUrUNi196dtmlEk5XjyIbGnCKvEvqu6z3GjSy+Rdf12B25h+XBm
+FdW00pdTkscmhm8gNbvVQBR0B/9GfNCE55zRPOrZPo/2GduZwr8ZfhgBDP/945LZ
+Vij8eiOftznUt9vxsxpCOu+JUA1vrbspnA1R4qw9+GLgotW6Jkh3tuHa543lfDmm
+BNFNssANVkwXWkqlSUNBEudfW+om9lQbb7hxxI2t8azNldYOt6vcRU1ZzGTj7BW3
+usUXzcywoAyEXoPYK++64BUPIYcOhyps/OMuyFbu0zANrLsSwUjB58iYNPGSo09G
+VFCrhS+q5l7KEeyfUu07DB5rw+DNDiB/mhOvtysjbo49nBb+ccFtfP7FtlFuv/In
+itPR5p46km49q1RfnrrHFw3dy0fHWpw744ivLAVUhcLB0FZpwQ0rxe5Hi3VvWIiL
+ZEiZHHuw1os1sG+TirapFDjOefnYRDZSEJQTNjGHI8Js3jzgWZX41SU2bnf7eaNp
+nM5o3O/ZwdN6K92J4jvRD5W0UfmCWabQoLkJ/pkWb9lc7CipkkUUaZdXDwKk7vBd
+SMuvttpQJv4iCUkJNpCiRyYIfw/hUgGOHNpSZGuUQRtk3HzrEo6wdsQf2WY1tZfD
+4q9607pzDGWzRFR0KDPKfrjk7/Q/2hpm2JGMHmg8dd4V5JQXljqyuffA/c8yrlPj
+KFQHEFrg9CI2oA3UUNgpDaKxLr/ucuA9bjMI5APs37Eyk5jO5fDvh5XKDTXZUzaW
+9Nt9FMH+FAHXIGTf3URvYJZ39hUS+Gngv1BDPNpkH4XtBytlLDeETaxyEjPIMkJZ
+eT0v+g8jVpHk85qJTxbuqZG13u610ZDmyPoPRAqfdFsNAJub8OpJLj4YzRl36G4g
+un5TG5TE1v0T2fRkWW8fZxNiHzblirx45dXF+xaB+LfElQWr/V+AFaWiKfy3KpP8
+G83FWASAMlnK5RQf+OVYeIAihKGoPGygbMsfpn6E2BUXK+E8Hz6sQMfFxqllGPgk
+/f87pzQAOpAbPgR+/XjGD3YGlA3zAFMwl3rCBo4TtJ9wH35vEJmeaRHx5YL1uvVA
+S+/krsYMKQ1hjQKDi0Hlyx66sl1rIYCSytjSS9Ae6lb2rPSqspRyShpJL1S+Q5al
+3urZKhrurGiJmKmZrpZomjnKr6l045Fs6XR/nmaLXBf0j3y5LJ5OsWZ6Rwzfajk7
+n4pdaotHgs1LdQdm06bHUIeFwgUBno5LqeC+DXZhbV5I+b1I78H8BUXDPv88dPiL
+66wGx+Ha6M8sm335ydnJL02cDFOJGywdKc1XilXPQP/c6EYUrYVbDQXebtgDac78
+aw+8vMFy0DeKtlKfEBtm44eG3BNQI+jnZbg6TQtkp3noD5OYFjjWPLtbKBPgYU8b
+JvTOE/MTblHC34Kn5rlanArdLgMH31FLbr1o18gRQNws6Jyxv3+eFIA6umvSXiFk
+PNNMDFqTvJBJtlMhhhH1kVItZvzol8bJDd7ed4ZQie7fH3ukt1E3iZMW3fRgqomq
+ZDb4GOWAKfpKrWwH+c6JXPO5+1URwjpTmrZj7wPubtJMMFDbtoTBNJFAaOMwlSMb
+z94AP8kd2gr/pREl7Ih+vJoyruVNJybKjZenidQlAjbhNn6hBIUSGDyC4mKLhnD6
+EA1DTxYjZupajP68DC+S/bAO7k9veQ2IWTdbFqkjCC6pUkxFp6DuM+ADZdbR7Llm
+nWUjb5CLLwNuuE+CMFFYUrK8BdnC1uaqb6fQiEo42u9lAkX4pRIB6TVnTtLa25ub
+CUWpSWpu6bEP1nIf9YuNPIxZt8YAA97HX3fSKY6PcRp7QDP9Gu2kRenZRfeHpoYz
+bkruh6jWXIMhtZiWO4+qpFW2AQBro8KSrnNxjpruxy/DxbbexJt7LNv8ZnlaIKCc
+S5Bv9WHfxUk6tJ03+CqxDqsRHy3QkzwnzXLnNnRUiNKi/fID1YrbSbPvnYJwDWi9
+Dc2eDOdWJck71nnJSWP6nRcskNxbNnNnJ4MNXKmbGlJ1G+2F5zpqnO8O9VbMb5S9
+9vE8dW2LFQ/7QzCqakL4X1oKzjEyZ/LOaYgH/KtSwEZWpeI/v6w41AVOTRgvkQYY
+kP//kDFPr1WepWqZ6dkq0JrpudTkddFoIJmRvxRFYFkBwd7KthKOzI3kZlB6gQss
+/wO04e8Wv/iGoNJJN6SxapJpe7Hhx7GljesOTSM0Vd6t7NgELWUtwZ0SGpoMZAup
+HIKT1tAt1XsorC51jzksaEYCxUK/Ceb/B3y9xYpD+97BwZsHheAG3yq05FItHKZB
+njzqBM8fvKR4GyGRN2HGcybet+FEDyg1Tl3kpjgbqagqCmqwZdGuo6XwW65NpODg
+TIIqaXudEOCo6VUcA68S0ZfIF7KaBzAinQy8Qw4m7WlzQa9R7peterNTeMDj9fBk
+6daJNJ//AUwcDSUa1pfL4OlkjmquE1JYal2crA++XuTzRKIQA6HmYt2QEZjqNyG1
+S0j1xhFgebNEmwyqRnwrhnWIZNJpltZE9iGfeEXi0QtEZn4/A2fNaDF1R/WIUVfA
+M0tdSZz9rL+Q6UxsfFdlTszcTN7P0z8XM1IO9q5HMgZ7FgT/FozDhbYW6cNN8Haa
++wYL3foW8CV5NWxUbYD+OZejHQTpbqTkYGvumSjPWtlqUhTWJ1NZRa5Y6tH2vwou
+Lh3IA43DSYVvhDcZeVwi3C9HMDRPkr0ZkLQYfpL4jqeE30Wodg0JWbihDnNscCLq
+Cl2BLU/o9PydlgRkq6vWNXtRIBoA3yaMytFu4+sHIrhGkxXwpmnP9bOgjSeNqmJe
+V07r3FTso7cnHqOQUHbRmsogEMALUegTpDUE0+mWyrdmKRvkbhm2Rj0MLrhpZjqv
+E2RngcJ3pWVX3Ph7oVPfdRWwMlL7lAgSTY2LHwVdUjKPL93wX+mY+ZnMWNgw619A
+ix/bNX68Ufeqhp0mUVrJg2gdhlYiRLDocaVZdoB3/M1ENLbbAT9NyQGp5uP2wCZE
+deE3ttI8CNRq4HQ+BzHo/bQiflG+GwcX+3rKn9m0JA6Jx3FlsMh+a+mKhILoj/VO
+sy/nzz9Mzf0nh0SFkwv+mH9qTc+paX33zV4TQeyR/zRDs7/vGHR5VQsKArHkUY9c
+D/c1HjXlbIlX5MHGRv126XdEVFd0FAxdZBRw/R+21MG3FZ9QMvslWcQxYiOovXmP
+DXyiNA10jzFZwGPSVfoh2FlfcIHnNUdaa/2oY0vVmUcSpU4CRS/cTC1+9X6CHLNv
+Nok1Hcy7MowgPEmqpIIOTI5vyXDr9PtKU9z65fvYEhj+dLlBvK1QCLl1TBPNg8PU
+MJLFcQlO9Owwe1GbWCoqJ4+3Z5O41jIckkjkrASGuwxiihVNuAMrmIocPlmsdOIW
+DikNtSNX+gLb3BPEjVrt7aZkotdvzqD/ZPZdcTN/g5/LG01ShUWBhMjMnw4eW+eF
+AZy8xMymZFeD3BrGEY7UNwpsYlYDJq00FRwz6pRw21oZkQwSxjLUnwi9M7pNJ00K
+cddZHf8XDUiTRvYxVvC+piVb5bPzilIHHhbG3RdVU4RPjeQdfvE4zffD81n81az1
+mioDUSdGcPJM/fyBFnIkQmfF12m4bheGQuoXWr801LpBmy+x0W89tSWycvPY5gTi
+6Ua/novyfnTrYTWaDG0I+tX/3HWS+FXDj/HSCMv3kalxYrAOBDQoYTasdJlARVwU
+aGeDcgel9yCmRd9PlPGUX1HQ/IA2fLVkkZYV8Lo0K3x2EYJ1cmInm+c91CztpIb3
+2Nlejn1lg0CnvR4sVZRh+TYe65eMw4sFWWIJDt6Ad2fxdP/SBCJa9rUbbWV/K9xc
+zxU/GdO5uWOiGdYm7Zf/8gPBRWozxw0zi1DrZNGrRCkPNY95MYHSTD0GsFy29ySu
+ROy5KEaXXxNzrqWhG90sQu43A31OermYR2G0ERbl7gSFoOhI7WusYwW2Me19tySp
+9O5rS1gJieFJkpAKO2mVMGw1p1b6U4snyWYW4+gVfVanYhJ1RLof6rzgKhazMfIs
+HRkwh4+Snjup+3Uu29+R3p/nHJ9/cgMAKgT64Ll/VZrf+krym2DirZr3EbnAb4HW
+r60Hm8cR64WjTBmSXSgy17CWPPtTuEqwqU1TLNDMuSeLT6EQujEcHqYwdjLju3JQ
+Fcfz205gLjy+MsfuiiZznyTIANaSBPvyC3ExJ5Kagvq2pq/W3rURNV79vJinjOeu
+qa/EPiRIddNSMF2yJ/7VFTsHvJaz03y4AGth/FQxQSDk5F94B0iGEGeRKbSOjaKq
+wucfsur3OHmeDGmAdRrB24grkSP3Pnj+AVj9aWQxlmWGLBCnuGK3iqy2jw7+2ekA
+PLSBTN/YuilrOF3Cgv1bwgYH4FIS389LMnYyzovDlrRFWjchzSmaGZqD0QKXyjmd
+evpyQsUBBlvZiRHjRgjhlML/J88hFCtFehLuxuzqw9JSY8Bhauevw7rwKhrw4ZHB
+2GxF5IXrRuar9U3ofI+4vg+THR0WnHpFIjNpH8zamKK+vD8IkBy6v4RuNCtLzvM0
+ulvJmYe+Ep5E2ZP5Y7pTkSiHUAtlH8PKqTwOu51W3GdQPDNR7q31BSpEJS4T/fTl
+CtyiYXd+onkhzutkyBeOSYBtH8GpLzYPc6tiontupT0trC6bJzhm6Hj7JtEhVx4u
+80PRd7ZnhuLIQ8K5Vc3uDXjoJ7G5xCr7pOk8rQsXAzuVTn3djUZDj94AOEgXXo3M
+0TdfgH/ZOsF1nCkxQ41cVqAyG3801mx7vxkKfeqA9Is4lIHVMLt7JuT9BYVxI2Wd
+O3f2oxGaYjJdA+GYxM2Xug+9F9ZfmsU+6/urCvvw8mw8oW27gEN+iMrwVrB057Fz
+s5O2mWG1xuFdoiOjLaGaKOYoi621w4mRJjZuTLv75QdsTOlXdcTCvqsTZEcU0xdJ
+6DR2Gcz8OlA0jEbFc78W8746gjATcevEqtVr+XZEYlTGR5sJ/CRNtD44FCTewIrl
+/7Z30d8SKw3aTm0JFgBV3Ki2Pgct0Yz3JSpMd/ELMtPD9zNcLrWj+iKQ9ztEvRlg
+eMl+7LgtadAjQcJDXxIrceKvahh9auGI1kqO4f2o0klw5iGXXWMid1dTH6GqtFrh
+JME0R9oCwUIrcFoXPFq+9uvRPUCJQzS/wa+BUsenEF2i1Bos6gtxfI8EoglUpLPq
+IhwM/cAuuGJdPDd2762dBNMZNtcwI5/AfxLFv/dJ//E562YpSvcnis6hw+DJphGx
+3NJ9EIeNIuv5K58mGXDaBgh07pk8flKtk5OAX9kT1MXzCYhjkI7V+m2O9OHoV1BN
+ylktKz7p6a4TjpFfci5dWUkcSqq5g0W28I5155iFe1LI+P7OU1Be2dpldSkl/OjJ
+Z5YKQg2hhj4Ik4fbahD+/nXCUWzoJzI1T/Fszx2h20OsnRrCDwucSz4MpyXd7afK
+DxOjRQJrK9NqZOXiGy2DFzvpsQrqvu1LXcRGDFI8FX69th5ea0nyInIrcwRcJcqV
+V4mgrTMMgaXjAbfH3OLARNnwwCv+CMlxk+Y+zmlOqbMgwJmfgG+H1TVDOnbuYNPA
+o7P7oYOKNuCwi6sXl5I6CUray0WNSxW/ySz7J86krQD8KVUsCOlyUI+tJ1WLO7yN
+zes699oPUNEXg2rxv60F7k7/FQzwvoNrTqpiMU95nrc4D1WIWtZNqHRhgcEF5g6L
+lZWVJsdj6SfA+yRzZ9KZ4CMcJt9bGg06qXVfIFbhlycM/83ickP0fC4uQkrkdu2S
+QwXkfsWFE/PNMAnUoW1ctgP6VD2Gha2gXXNzgD9WZOM6b30ZjrIxr518q9xDVWfX
+uYNjlUVZEhwL1yC8RSslS+AvMjFmFiHphQbtk8YO4/hq26uBtK5TTNKNhH/9Ep7e
+iQ3veRS/XsIpI3FaS9ZHJTdKA+ZiZpTgUW8HGWLVCswIP1IFfprbrpcsGpEWz9wm
+vVKtwVxh1i58Y6DqZv18lWgdsxJ+26cZOJT8aPjRBha+Wx/eqY60e4KHN1BYd4Rd
+1mcGM5XPQht4d9LjpuoaolgZwWgqH0SpUpEgD+nbq85BQk4oBXO332JF/RofErJs
+mqUncAoo/VJx/FsqZVJo4XDo3SOueOKYIyrL26MJQ9H864L2gxq9hZmrzYW2iv9n
+l5w9t+f70hsk69i2Se9Sz32WudbHDcnR7SaGvCKHKogAvd/7jryVA/ZgcLlpgMcZ
+GB2CjurQZuBe4OXvwmJxeKwB1d98in2sM5t8LhwKEvi8JDvi5Xy+z84ZmWsULT+H
+qmQeszsimFbeWPRi7LT2rm+/AwUNUY0XoAVSPiludjR+f4ld3CZydPs5ccXm4tGy
+JcYffvDtyIyYMCQjBM/zPi+qKTwyeIEwLZcfOwXeDpKCdtSOxBNfKJgupEWPoj0c
+T+1C+AomgQNLd5kfBc3/Y5v3hHH5eAMiieejHt7yYYQcTbok5OfJEsl/ZsG0JBLO
+TKTKiUGLVgYekiSkpYZaxe8Yof22ZUDLYUszfuMt/Kul0EdXxkFT3EYWQSe+7db4
+lj2+AAI9ExtqkaOLn/JVcoCDRvIB2iRhCA+u01eluYoQaRww5/qEWU31BsSC2cKe
+BimZyOX2pMULVZP9g0RSdj68J6415F2Q9nma0GdagvQKDDqWqqWwYhi/nZvVQVRd
+rtOIaKvjRXw2I699eKTmLOvzY9s0Sm2jMeCb+QZEAnnUlWgraez9PHy83eV4PBw3
+9gyQdM2zipIWzf9j9hYRyfSt4myxSWKctgo8r7C/9DJYbRFVeG7zixnkm6yu5kmC
++fTxuaybximy8znEW16Uz9toAOQBhYEuS48P2/whExCPUgCqns8OeCm272Evf+Ki
+ybgAOkZfRRXbU6YNqC3gM4sZMk2u0RBLj6nz+Quw/BMddiIJoIQjF5JJIL+yZEWd
+FDTlbmxKHVadU+4F5g2Z7XRw5Nh4lOY0iov2n5Q4S8f5ISyJUm+UsYFptHqKo73Q
+CGIGu6G1uvr4IZMUAN9T6M34oOV94q3pateKV0NbqpkAlt7iOSXgi4ueOTHnPKPN
+/4dp9YsbxQhYgVUJbWhxf+x/Z7iwgn2JPEEfkONrOmgepvR9APrcHCIv31rzMS3S
+YnVfaAGnqqrHBYsNHiraBeUKNZLgefTtQBxfuZkrtmqvrG6FeS9AX7l5IVYLq+jq
++IEY3S9vsr26hABtCKgszVxtVXnQrCitYaN4F+ssfvmLuHPn2xHXkeMGrf1728mc
+6pDHVR8CVi8u+3Og606F5xfcp2qkfuzCoS2AyvRg5uSUbRJdmkfA/WVerjOUwmPb
+ImWzb84L83McI8qMGrjSi1MNfYiWQFB9Qqs7gOm2ixdiYNqqS6X/7DPrXtdWoiLF
+M511CGQTPWdIr18snnupLdLzdX+iFzmD9uatc41hhZNyyTpqr8OmYHEwQq2WdyPK
+r2AWXk8nJBzBNNqIZURz0zousFA8sgCk7mma0CDNj+cM1v03ekXrxM+GAP1RUBqL
+Em7VrPpSkJtMpWV+LuaDhHyh9XesVedOHICSdazc0qe4ZeLdDDJ9F3kCOqslOFLs
+A9zFFt99hfWyR62iKVT/fcDWLO0Jof7lTF+z/G1GzWyMm8jAeR5mEeJVi8BpQqSN
+7yI1SdHr5qFrJe9Bbd5wS5pQctxtg8zItYRdXOy9OAo4W7OrGOSYhXAHheSE1yIC
+yLgXl0yex4fl89UEOUUhK2Sk2H0VcoZBwY0gG8Ynt9h8VQqe5MQhMG/bJDHck+5E
+i9gpSN/J7WvFCWVdnFbgthdRih+mLJ9/NIeHy3FJFnQ47OxHevEmsYTIBqjj6AS/
+Y+W9L7QKOCiAl7s6e8sNBL6HLR3gtIxQ4McSfe5fmmbzvRSRyF6NVHf2FI3qrPjq
+LFdwZUwCaMWLJzeUcxe2Vdvl69mfK/sAM8Cm1cElQ2YAWAAta4xyTYbz+M3DskeP
+rkLTJE4aakesoHYhFQ54Uz9JDLd0BzaQhYB2KpQfF+d6pVeGbOlRKbz49GRJ+Cc6
+FYt1c4KZa3Vrb6XBqCirknJbd4S8jCIoc7DdSzaob5ahwULUrKOvghbEvADeIvd0
+b6sjeSbH8RDt51pelrSLAoFe4B8AbPiMshNKVBmuStZRUQ3/sEnuLkKsgDHDp98Y
+H8CJrticAQ+qJd972qikimSminlUFkXwSrG9Zg7IOmm3WdU+3UaftiSMft5lIHhb
+fimhrDyXDkiGjHb7D7piPansmoyvnDngdhOTcrLXFXQfnlvrfa2j99v9+l+JfBwN
+waJ/c9xcfLyAbb9nkrVUjIS69OdZPhGVf87y5Ny32EhsqM5IE8EbpG1E22YvtQqq
+nuqeY4eLT/ngJDIL6zD3XVzhxUrko/QrHu71rccW+XX8NnA7lEfZRjtxkzPwqSiE
+iajmTrcCQJTDqyPxSTUpnESPQUegVfgF25yghBkWbdWKCf3t3LZxozV1IcSRu445
+nU8wKXkmKYnmFQDAe6VzPipmkctgpW4zaHddP7Lm0qhiWkWlB1seRkd7bAG1uQaZ
+GKYpPZ8g50VEQhNP/lqU5FQ3q0bRibcIdkjXNnwFlPWBFa2LfTlXDmX/dE6HLbUB
+x+2l4KB/EIUfQPwtleC/7oscZoZbOSUi0pBfda2CXUDo5afB9xwtUaFV1Ncl6u6B
+ThPW4SFB6oaExfUCSlQ8dLYtqrluF5p/u9mTCdXNavxpz39bRsPk0zzWIdJ7e6Fg
+T/Slk1+pHqShrMUYacjon3jkVi3dhzMUwZBIuVfnA0ZaUwNEPrW4nj9HgqFAlSOT
+HWwrVrRaKIubV7v7ra9rCb+8kjQOwxDO2uma+CarAf6IvL+Qn1OhWa+Gw0lE6N6g
+zF6RXe+zoOk+wAjI9HR0LDYP2Wx76E9N0CUMvJS6gNS5Xyan17rzRWd2CJ4KrEGK
+ZSXbqtDX6HzZ9C0blC7d8IYhVq2Jog8AzljkTHb+Uy8PbWnLKtWIARgCa0IwPray
+v5BPDuXyXo3hZOVMDQvZ2oVXk4XRPuf8GkmWqlNSY5klyat+GxJWxZSAfQkEC2EJ
+JgVGHqoPOlJc0AZg/gYqweP3OzNqqS+4EAFMByw4VAAJOArzwRP98r26mO10iucQ
+8jCQ+zGg1r49naP/orOAwPf0ovGe+nsy5ZAaz323mVVkJXcNFZFjMl7ZLg5RrmyS
+IKQmSET2KqA8EOJGb6G00wqGf2gp36bQktgI4byi2Oj/bKr3TLLLrB8AD8KxaPiv
+U4AHX1EM450LL6/lwoA2SbMy96Cg8yqT6heIDU/oP+mKlTYi9gxK+MkDy8TrU2vN
+ZC8o8Hrux5w3zIc6pzODQ62x35Uk6bRNGaAEDav74JK2sX6SXlsBLy2Ke0rOwfFk
+t9+/GoMcytmojQVn5TLSnwNRDRD/2rNGIPFvbG4XbmiQgoreyWTUp2EZXYp8Abkv
+bk9r23Mbx39NS+qL46l0vH0XGKFP5yXsGrON3h5Rdr2ASjKP8uJ4ztL+MRaVe7ly
+hM3t8FRAxct8R38glEBXYxEj6tmQVwRb7Y9sgNvA1HbiysqLRBrIRgl6QCYKwd30
+ov/rWUgv5McPh6y8XXwiOermElFYCa2y3cR4rY4ERGRkV+HndqAMNSgvw5i8Y0Yj
+f1ysnL5Uiip9Olgd7q/syvAD1X/N+JpytgWkmN0fTQD9vL2wwBTsHODwrooatW/h
+aG2sKTjuFW/wJ73DY6eJ0DfJfDo3lo85jbrVMc75jCupTsfD7yPRhcdyIAOe8sRc
+DvjjOR33Wz4CxYoV9feVSbVuyucS7tDSGYWvorYViNPrcKRNVFSIUI+nfySWuhsF
+SetqFoWFZhQRTAJ4m7KqfWfaq2lDiB9LCwBCmqdMjAAn8Pwj1WQWJeQ55FbdWgCh
+e+OY2cc9G7YpAnYeDvrPFL25xiaGkZIGZZsBfGZgUBkSp0gEJIhsoP+ZGB/7xR4D
+pwMEpo13quAB5A0M/MQ1PEKwlw5T+wlBd3ndM99VYDAvBGoKGA2IHJgz5MXQ9kbV
+idc+3ECGAVKnIa/4kUs/pEMkX8nuU8lkrzi3RJ34iIoeOc0KoGkYRZsJ1woSAlRT
+JNtbwSlEvihbMgLYf9ChwvGkIeQYqujcnWv+eb3IShRxNqZqfso/Y1ng3JFsmb4m
+Xq2wimE8gsv1LPXhxZU36z+b3uyLSjwQgfdcpZ8OxTd1BQzFVP5NcLgYlcoPBWbM
+BY8Sw6J+2goXbVgkMPhqyvKPOfrs7ozTKZ5KEpiKhNnuOLEJaBWolUk0AFgGCseA
+O0kYu2cz0gq9iLH1rV5q324DitsTAcDaSnODuQ6A0VarVvJse60gsPNcIfMYseCQ
+FY4OEjlth+dGYt55ULEzOieaVan1U6rm0uczBo7usbLSDYpKkqiONxZchZaiL6b8
+ltLfTAiQWo8aWloR1Q5enVkQhN0KexhIa2bdCpuCfdtDUAOteNka+6nspU7FH3YU
+BF/GdP+nLLnI/jYj9VqcLbN+XflvgakXHLzn57Ik3jtFsZYpfREL3XjOb0ZodIFm
+lAExZIy1bawdHSRahf0esxpGtswZrHlfyP8LwpAdazIQyYqxD2+R7oD1fsKXC7uG
+hvYairIOJOZ3mmEtIG0HkYNxTH8d1EuJHtXXcFWRboCnMqL5KaqQUdXRdisPLeJp
+P0dMiGVJAL8dN4A9BBz1q73e0HI/Y+OzNymNL2lwQUWKPrO2glgP9Cr/7NOOsbYU
+LWeeVz+jqItS8Kf6HXsiN3KVUsAwPk3g1I/IG+Z7ADHOiwayntk3lufWWROVo0Io
+5A92tAlAXxwFJjkWAAPbCvjxJF98Z5KKt+8cP1VIaX25qHxsGfmX1FFgiCUDN0XI
+KyWpeuacpI8Qnyj9G3U3gqWhBAYkNne1EP4TwFA8Q9E8fKV9NgSA73kTMdqSnXyh
+48OoSW7nWA9D188korO46qM8k54q2NTmoiEz5Q27U0t8/gnGzJ+h3jJjWI7kbUTr
+nUgJaChsce/xgCqI24c6y/Ypnjc2Is6l2ja8siIXhJehFS653J3TX6D3FwZZdw4r
+XID568OB5pb8xWiwoahLPsuHdclN7sZY6eb09TDXLK7hixezX0ntwJY9VVQUzWvP
+c8oUUuS4/F89+fcwNJl+1Yd5dEj8Z158ImSd8i3CxX8/ErnmJeaP5En+e/PJ+fYu
+nATo1VWUlyhD6lpjBbMs1HGELvFj/8TlBT/n5frmMvOx7TfkcCtYrA/EAuvV5DeO
+LE0iFX2UHVV4WaGLIXLE+woDMvsSckdtBsbxcSdYl+1tsAy0ok/OLdjYssseWj2e
+KJDL6XEC5IjTiO41UcwpJN0W5TU4aUcGLBbr8fO1wUkPrthezj2bGYlHeRIKbONb
+joQ1SXYqXzeokgJo1iEsS0xWJ+6TKSBgnn8QMLD/hY1SkcuadTdowUG2RTubhQts
+RfH4YgAHeiEXItKiCKLvmmuK3FFplRN9mtf4f+SJgS9I2tMfqokgMAUyVRdJ7NO5
+k0gW2JRPg+qL3PXY5JQvsIfPATxSBHldSnYE+iLctZ+0hQRan0b93oQPpdED4xHR
+PqfcEOB0y1sfJfU1gYLU8+PgCcQdQhMGegMu8gM3cU00fa5nd7GCTtzD8ZtMAyR1
+HW0MjzxbzleQuks41t5N4xyZBbC/nYU2dtkFgkOQ8OQVT/YOUlQwOWrWM4LozvXq
+g9p6Jfx+zW6C8i+uSRrniGNbD3PddWT1U/Myn8nbyvSvXjysL7hPxuHyjZmHmhad
+b9NmVcROFHOKXlLWVMbimXXZTbeqm9ouduKVTmWfZSq86YaIeIfQMJW7iER7
diff --git a/src/lib-dcrypt/test-crypto.c b/src/lib-dcrypt/test-crypto.c
new file mode 100644
index 0000000..ce43791
--- /dev/null
+++ b/src/lib-dcrypt/test-crypto.c
@@ -0,0 +1,1314 @@
+/* Copyright (c) 2016-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "buffer.h"
+#include "str.h"
+#include "dcrypt.h"
+#include "dcrypt-iostream.h"
+#include "ostream.h"
+#include "ostream-encrypt.h"
+#include "istream.h"
+#include "iostream-temp.h"
+#include "randgen.h"
+#include "test-common.h"
+#include "hex-binary.h"
+#include "json-parser.h"
+#include <fcntl.h>
+#include <sys/stat.h>
+#include <stdio.h>
+
+static void test_cipher_test_vectors(void)
+{
+ static const struct {
+ const char *key;
+ const char *iv;
+ const char *pt;
+ const char *ct;
+ } vectors[] = {
+ {
+ "2b7e151628aed2a6abf7158809cf4f3c",
+ "000102030405060708090a0b0c0d0e0f",
+ "6bc1bee22e409f96e93d7e117393172a",
+ "7649abac8119b246cee98e9b12e9197d"
+ }, {
+ "2b7e151628aed2a6abf7158809cf4f3c",
+ "7649ABAC8119B246CEE98E9B12E9197D",
+ "ae2d8a571e03ac9c9eb76fac45af8e51",
+ "5086cb9b507219ee95db113a917678b2"
+ }
+ };
+
+
+ test_begin("test_cipher_test_vectors");
+
+ buffer_t *key,*iv,*pt,*ct,*res_enc,*res_dec;
+
+ key = t_buffer_create(16);
+ iv = t_buffer_create(16);
+ pt = t_buffer_create(16);
+ ct = t_buffer_create(16);
+
+ res_enc = t_buffer_create(32);
+ res_dec = t_buffer_create(32);
+
+ for(size_t i = 0; i < N_ELEMENTS(vectors); i++) {
+ struct dcrypt_context_symmetric *ctx;
+
+ buffer_set_used_size(key, 0);
+ buffer_set_used_size(iv, 0);
+ buffer_set_used_size(pt, 0);
+ buffer_set_used_size(ct, 0);
+ buffer_set_used_size(res_enc, 0);
+ buffer_set_used_size(res_dec, 0);
+
+ hex_to_binary(vectors[i].key, key);
+ hex_to_binary(vectors[i].iv, iv);
+ hex_to_binary(vectors[i].pt, pt);
+ hex_to_binary(vectors[i].ct, ct);
+
+ if (!dcrypt_ctx_sym_create("AES-128-CBC", DCRYPT_MODE_ENCRYPT,
+ &ctx, NULL)) {
+ test_assert_failed("dcrypt_ctx_sym_create",
+ __FILE__, __LINE__-1);
+ continue;
+ }
+
+ dcrypt_ctx_sym_set_padding(ctx, FALSE);
+
+ dcrypt_ctx_sym_set_key(ctx, key->data, key->used);
+ dcrypt_ctx_sym_set_iv(ctx, iv->data, iv->used);
+
+ test_assert_idx(dcrypt_ctx_sym_init(ctx, NULL), i);
+
+ test_assert_idx(dcrypt_ctx_sym_update(ctx,
+ pt->data, pt->used, res_enc, NULL), i);
+ test_assert_idx(dcrypt_ctx_sym_final(ctx, res_enc, NULL), i);
+
+ test_assert_idx(buffer_cmp(ct, res_enc), i);
+
+ dcrypt_ctx_sym_destroy(&ctx);
+
+ if (!dcrypt_ctx_sym_create("AES-128-CBC", DCRYPT_MODE_DECRYPT,
+ &ctx, NULL)) {
+ test_assert_failed("dcrypt_ctx_sym_create",
+ __FILE__, __LINE__-1);
+ continue;
+ }
+
+ dcrypt_ctx_sym_set_padding(ctx, FALSE);
+
+ dcrypt_ctx_sym_set_key(ctx, key->data, key->used);
+ dcrypt_ctx_sym_set_iv(ctx, iv->data, iv->used);
+
+ test_assert_idx(dcrypt_ctx_sym_init(ctx, NULL), i);
+ test_assert_idx(dcrypt_ctx_sym_update(ctx,
+ res_enc->data, res_enc->used, res_dec, NULL), i);
+ test_assert_idx(dcrypt_ctx_sym_final(ctx, res_dec, NULL), i);
+
+ test_assert_idx(buffer_cmp(pt, res_dec), i);
+
+ dcrypt_ctx_sym_destroy(&ctx);
+ }
+
+ test_end();
+}
+
+static void test_cipher_aead_test_vectors(void)
+{
+ struct dcrypt_context_symmetric *ctx;
+ const char *error = NULL;
+
+ test_begin("test_cipher_aead_test_vectors");
+
+ if (!dcrypt_ctx_sym_create("aes-128-gcm", DCRYPT_MODE_ENCRYPT,
+ &ctx, &error)) {
+ test_assert_failed("dcrypt_ctx_sym_create",
+ __FILE__, __LINE__-1);
+ return;
+ }
+
+ buffer_t *key, *iv, *aad, *pt, *ct, *tag, *tag_res, *res;
+
+ key = t_buffer_create(16);
+ iv = t_buffer_create(16);
+ aad = t_buffer_create(16);
+ pt = t_buffer_create(16);
+ ct = t_buffer_create(16);
+ tag = t_buffer_create(16);
+ res = t_buffer_create(16);
+ tag_res = t_buffer_create(16);
+
+ hex_to_binary("feffe9928665731c6d6a8f9467308308", key);
+ hex_to_binary("cafebabefacedbaddecaf888", iv);
+ hex_to_binary("d9313225f88406e5a55909c5aff5269a"
+ "86a7a9531534f7da2e4c303d8a318a72"
+ "1c3c0c95956809532fcf0e2449a6b525"
+ "b16aedf5aa0de657ba637b391aafd255", pt);
+ hex_to_binary("42831ec2217774244b7221b784d0d49c"
+ "e3aa212f2c02a4e035c17e2329aca12e"
+ "21d514b25466931c7d8f6a5aac84aa05"
+ "1ba30b396a0aac973d58e091473f5985", ct);
+ hex_to_binary("4d5c2af327cd64a62cf35abd2ba6fab4", tag);
+
+ dcrypt_ctx_sym_set_key(ctx, key->data, key->used);
+ dcrypt_ctx_sym_set_iv(ctx, iv->data, iv->used);
+ dcrypt_ctx_sym_set_aad(ctx, aad->data, aad->used);
+ test_assert(dcrypt_ctx_sym_init(ctx, &error));
+ test_assert(dcrypt_ctx_sym_update(ctx, pt->data, pt->used, res, &error));
+ test_assert(dcrypt_ctx_sym_final(ctx, res, &error));
+ test_assert(dcrypt_ctx_sym_get_tag(ctx, tag_res));
+
+ test_assert(buffer_cmp(ct, res) == TRUE);
+ test_assert(buffer_cmp(tag, tag_res) == TRUE);
+
+ dcrypt_ctx_sym_destroy(&ctx);
+
+ if (!dcrypt_ctx_sym_create("aes-128-gcm", DCRYPT_MODE_DECRYPT,
+ &ctx, &error)) {
+ test_assert_failed("dcrypt_ctx_sym_create",
+ __FILE__, __LINE__-1);
+ } else {
+
+ buffer_set_used_size(res, 0);
+
+ dcrypt_ctx_sym_set_key(ctx, key->data, key->used);
+ dcrypt_ctx_sym_set_iv(ctx, iv->data, iv->used);
+ dcrypt_ctx_sym_set_aad(ctx, aad->data, aad->used);
+ dcrypt_ctx_sym_set_tag(ctx, tag->data, tag->used);
+ test_assert(dcrypt_ctx_sym_init(ctx, &error));
+ test_assert(dcrypt_ctx_sym_update(ctx,
+ ct->data, ct->used, res, &error));
+ test_assert(dcrypt_ctx_sym_final(ctx, res, &error));
+
+ test_assert(buffer_cmp(pt, res) == TRUE);
+
+ dcrypt_ctx_sym_destroy(&ctx);
+ }
+
+ test_end();
+}
+
+static void test_hmac_test_vectors(void)
+{
+ test_begin("test_hmac_test_vectors");
+
+ buffer_t *pt, *ct, *key, *res;
+ pt = t_buffer_create(50);
+ key = t_buffer_create(20);
+ ct = t_buffer_create(32);
+ res = t_buffer_create(32);
+
+ hex_to_binary("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", key);
+ hex_to_binary("dddddddddddddddddddddddddddddddddddddddddddddddddd"
+ "dddddddddddddddddddddddddddddddddddddddddddddddddd", pt);
+ hex_to_binary("773ea91e36800e46854db8ebd09181a7"
+ "2959098b3ef8c122d9635514ced565fe", res);
+
+ struct dcrypt_context_hmac *hctx;
+ if (!dcrypt_ctx_hmac_create("sha256", &hctx, NULL)) {
+ test_assert_failed("dcrypt_ctx_hmac_create",
+ __FILE__, __LINE__-1);
+ } else {
+ dcrypt_ctx_hmac_set_key(hctx, key->data, key->used);
+ test_assert(dcrypt_ctx_hmac_init(hctx, NULL));
+ test_assert(dcrypt_ctx_hmac_update(hctx,
+ pt->data, pt->used, NULL));
+ test_assert(dcrypt_ctx_hmac_final(hctx, ct, NULL));
+ test_assert(buffer_cmp(ct, res));
+ dcrypt_ctx_hmac_destroy(&hctx);
+ }
+
+ test_end();
+}
+
+static void test_load_v1_keys(void)
+{
+ test_begin("test_load_v1_keys");
+
+ const char *error = NULL;
+ const char *data1 =
+ "1\t716\t1\t0567e6bf9579813ae967314423b0fceb14bda24"
+ "749303923de9a9bb9370e0026f995901a57e63113eeb2baf0c"
+ "940e978d00686cbb52bd5014bc318563375876255\t0300E46"
+ "DA2125427BE968EB3B649910CDC4C405E5FFDE18D433A97CAB"
+ "FEE28CEEFAE9EE356C792004FFB80981D67E741B8CC036A342"
+ "35A8D2E1F98D1658CFC963D07EB\td0cfaca5d335f9edc41c8"
+ "4bb47465184cb0e2ec3931bebfcea4dd433615e77a0\t7c9a1"
+ "039ea2e4fed73e81dd3ffc3fa22ea4a28352939adde7bf8ea8"
+ "58b00fa4f";
+
+ enum dcrypt_key_format format;
+ enum dcrypt_key_version version;
+ enum dcrypt_key_kind kind;
+ enum dcrypt_key_encryption_type encryption_type;
+ const char *encryption_key_hash = NULL;
+ const char *key_hash = NULL;
+
+ bool ret = dcrypt_key_string_get_info(data1, &format, &version,
+ &kind, &encryption_type, &encryption_key_hash,
+ &key_hash, &error);
+
+ test_assert(ret == TRUE);
+ test_assert(error == NULL);
+ test_assert(format == DCRYPT_FORMAT_DOVECOT);
+ test_assert(version == DCRYPT_KEY_VERSION_1);
+ test_assert(kind == DCRYPT_KEY_KIND_PRIVATE);
+ test_assert(encryption_type == DCRYPT_KEY_ENCRYPTION_TYPE_KEY);
+ test_assert(strcmp(encryption_key_hash,
+ "d0cfaca5d335f9edc41c84bb47465184"
+ "cb0e2ec3931bebfcea4dd433615e77a0") == 0);
+ test_assert(strcmp(key_hash,
+ "7c9a1039ea2e4fed73e81dd3ffc3fa22"
+ "ea4a28352939adde7bf8ea858b00fa4f") == 0);
+
+ const char* data2 =
+ "1\t716\t0301EB00973C4EFC8FCECA4EA33E941F50B561199A"
+ "5159BCB6C2EED9DD1D62D65E38A254979D89E28F0C28883E71"
+ "EE2AD264CD16B863FA094A8F6F69A56B62E8918040\t7c9a10"
+ "39ea2e4fed73e81dd3ffc3fa22ea4a28352939adde7bf8ea85"
+ "8b00fa4f";
+
+ error = NULL;
+ encryption_key_hash = NULL;
+ key_hash = NULL;
+
+ ret = dcrypt_key_string_get_info(data2, &format, &version,
+ &kind, &encryption_type, &encryption_key_hash,
+ &key_hash, &error);
+
+ test_assert(ret == TRUE);
+ test_assert(error == NULL);
+ test_assert(format == DCRYPT_FORMAT_DOVECOT);
+ test_assert(version == DCRYPT_KEY_VERSION_1);
+ test_assert(kind == DCRYPT_KEY_KIND_PUBLIC);
+ test_assert(encryption_type == DCRYPT_KEY_ENCRYPTION_TYPE_NONE);
+ test_assert(encryption_key_hash == NULL);
+ test_assert(strcmp(key_hash,
+ "7c9a1039ea2e4fed73e81dd3ffc3fa22"
+ "ea4a28352939adde7bf8ea858b00fa4f") == 0);
+
+ /* This is the key that should be able to decrypt key1 */
+ const char *data3 =
+ "1\t716\t0\t048FD04FD3612B22D32790C592CF21CEF417EFD"
+ "2EA34AE5F688FA5B51BED29E05A308B68DA78E16E90B47A11E"
+ "133BD9A208A2894FD01B0BEE865CE339EA3FB17AC\td0cfaca"
+ "5d335f9edc41c84bb47465184cb0e2ec3931bebfcea4dd4336"
+ "15e77a0";
+
+ error = NULL;
+ encryption_key_hash = NULL;
+ key_hash = NULL;
+
+ ret = dcrypt_key_string_get_info(data3, &format, &version,
+ &kind, &encryption_type, &encryption_key_hash,
+ &key_hash, &error);
+ test_assert(ret == TRUE);
+ test_assert(error == NULL);
+ test_assert(format == DCRYPT_FORMAT_DOVECOT);
+ test_assert(version == DCRYPT_KEY_VERSION_1);
+ test_assert(kind == DCRYPT_KEY_KIND_PRIVATE);
+ test_assert(encryption_type == DCRYPT_KEY_ENCRYPTION_TYPE_NONE);
+ test_assert(encryption_key_hash == NULL);
+ test_assert(strcmp(key_hash,
+ "d0cfaca5d335f9edc41c84bb47465184"
+ "cb0e2ec3931bebfcea4dd433615e77a0") == 0);
+
+ /* key3's key_hash should and does match key1's encryption_key_hash */
+ struct dcrypt_private_key *pkey = NULL;
+ struct dcrypt_private_key *pkey2 = NULL;
+ pkey = NULL;
+ error = NULL;
+
+ ret = dcrypt_key_load_private(&pkey2, data3, NULL, NULL, &error);
+ test_assert(ret == TRUE);
+ test_assert(error == NULL);
+
+ ret = dcrypt_key_load_private(&pkey, data1, NULL, pkey2, &error);
+ test_assert(ret == TRUE);
+ test_assert(error == NULL);
+
+ dcrypt_key_unref_private(&pkey2);
+ dcrypt_key_unref_private(&pkey);
+
+ test_end();
+}
+
+static void test_load_v1_key(void)
+{
+ test_begin("test_load_v1_key");
+
+ buffer_t *key_1 = t_buffer_create(128);
+
+ struct dcrypt_private_key *pkey = NULL, *pkey2 = NULL;
+ const char *error = NULL;
+
+ test_assert(dcrypt_key_load_private(&pkey,
+ "1\t716\t0\t048FD04FD3612B22D32790C592CF21CEF417EFD"
+ "2EA34AE5F688FA5B51BED29E05A308B68DA78E16E90B47A11E"
+ "133BD9A208A2894FD01B0BEE865CE339EA3FB17AC\td0cfaca"
+ "5d335f9edc41c84bb47465184cb0e2ec3931bebfcea4dd4336"
+ "15e77a0", NULL, NULL, &error));
+ if (pkey != NULL) {
+ buffer_set_used_size(key_1, 0);
+ /* check that key_id matches */
+ struct dcrypt_public_key *pubkey = NULL;
+ dcrypt_key_convert_private_to_public(pkey, &pubkey);
+ test_assert(dcrypt_key_store_public(pubkey,
+ DCRYPT_FORMAT_DOVECOT, key_1, NULL));
+ buffer_set_used_size(key_1, 0);
+ dcrypt_key_id_public(pubkey, "sha256", key_1, &error);
+ test_assert(strcmp("792caad4d38c9eb2134a0cbc844eae38"
+ "6116de096a0ccafc98479825fc99b6a1",
+ binary_to_hex(key_1->data, key_1->used))
+ == 0);
+
+ dcrypt_key_unref_public(&pubkey);
+ pkey2 = NULL;
+
+ test_assert(dcrypt_key_load_private(&pkey2,
+ "1\t716\t1\t0567e6bf9579813ae967314423b0fceb14"
+ "bda24749303923de9a9bb9370e0026f995901a57e6311"
+ "3eeb2baf0c940e978d00686cbb52bd5014bc318563375"
+ "876255\t0300E46DA2125427BE968EB3B649910CDC4C4"
+ "05E5FFDE18D433A97CABFEE28CEEFAE9EE356C792004F"
+ "FB80981D67E741B8CC036A34235A8D2E1F98D1658CFC9"
+ "63D07EB\td0cfaca5d335f9edc41c84bb47465184cb0e"
+ "2ec3931bebfcea4dd433615e77a0\t7c9a1039ea2e4fe"
+ "d73e81dd3ffc3fa22ea4a28352939adde7bf8ea858b00"
+ "fa4f", NULL, pkey, &error));
+ if (pkey2 != NULL) {
+ buffer_set_used_size(key_1, 0);
+ /* check that key_id matches */
+ struct dcrypt_public_key *pubkey = NULL;
+ dcrypt_key_convert_private_to_public(pkey2, &pubkey);
+ test_assert(dcrypt_key_store_public(pubkey,
+ DCRYPT_FORMAT_DOVECOT, key_1, NULL));
+ buffer_set_used_size(key_1, 0);
+ test_assert(dcrypt_key_id_public_old(pubkey,
+ key_1, &error));
+ test_assert(strcmp(
+ "7c9a1039ea2e4fed73e81dd3ffc3fa22"
+ "ea4a28352939adde7bf8ea858b00fa4f",
+ binary_to_hex(key_1->data, key_1->used)) == 0);
+
+ dcrypt_key_unref_public(&pubkey);
+ dcrypt_key_unref_private(&pkey2);
+ }
+ dcrypt_key_unref_private(&pkey);
+ }
+
+ test_end();
+}
+
+static void test_load_v1_public_key(void)
+{
+ test_begin("test_load_v1_public_key");
+
+ const char* data1 =
+ "1\t716\t030131D8A5FD5167947A0AE9CB112ADED652665463"
+ "5AA5887051EE2364414B60FF32EBA8FA0BBE9485DBDE8794BB"
+ "BCB44BBFC0D662A4287A848BA570D4E5E45A11FE0F\td0cfac"
+ "a5d335f9edc41c84bb47465184cb0e2ec3931bebfcea4dd433"
+ "615e77a0";
+
+ const char* error = NULL;
+ const char* key_hash = NULL;
+ const char* encryption_key_hash = NULL;
+
+ enum dcrypt_key_format format;
+ enum dcrypt_key_version version;
+ enum dcrypt_key_kind kind;
+ enum dcrypt_key_encryption_type encryption_type;
+
+ bool ret = dcrypt_key_string_get_info(data1, &format, &version,
+ &kind, &encryption_type, &encryption_key_hash,
+ &key_hash, &error);
+
+ test_assert(ret == TRUE);
+ test_assert(error == NULL);
+ test_assert(format == DCRYPT_FORMAT_DOVECOT);
+ test_assert(version == DCRYPT_KEY_VERSION_1);
+ test_assert(kind == DCRYPT_KEY_KIND_PUBLIC);
+ test_assert(encryption_type == DCRYPT_KEY_ENCRYPTION_TYPE_NONE);
+ test_assert(key_hash != NULL &&
+ strcmp(key_hash, "d0cfaca5d335f9edc41c84bb47465184"
+ "cb0e2ec3931bebfcea4dd433615e77a0") == 0);
+ test_assert(encryption_key_hash == NULL);
+
+ struct dcrypt_public_key *pub_key = NULL;
+ ret = dcrypt_key_load_public(&pub_key, data1, &error);
+ test_assert(ret == TRUE);
+ test_assert(error == NULL);
+
+ test_assert(dcrypt_key_type_public(pub_key) == DCRYPT_KEY_EC);
+
+ dcrypt_key_unref_public(&pub_key);
+ test_assert(pub_key == NULL);
+
+ test_end();
+}
+
+static void test_load_v2_key(void)
+{
+ const char *keys[] = {
+ "-----BEGIN PRIVATE KEY-----\n"
+ "MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgtu"
+ "QJA+uboZWVwgHc\n"
+ "DciyVdrovAPwlMqshDK3s78IDDuhRANCAAQm0VEdzLB9PtD0HA"
+ "8JK1zifWnj8M00\n"
+ "FQzedfp9SQsWyA8dzs5/NFR5MTe6Xbh/ndKEs1zZH3vZ4FlNri"
+ "lZc0st\n"
+ "-----END PRIVATE KEY-----\n",
+ "2:1.2.840.10045.3.1.7:0:0000002100b6e40903eb9ba195"
+ "95c201dc0dc8b255dae8bc03f094caac8432b7b3bf080c3b:a"
+ "b13d251976dedab546b67354e7678821740dd534b749c2857f"
+ "66bf62bbaddfd",
+ "2:1.2.840.10045.3.1.7:2:aes-256-ctr:483bd74fd3d917"
+ "63:sha256:2048:d44ae35d3af7a2febcb15cde0c3693e7ed9"
+ "8595665ed655a97fa918d346d5c661a6e2339f4:ab13d25197"
+ "6dedab546b67354e7678821740dd534b749c2857f66bf62bba"
+ "ddfd",
+ "2:1.2.840.10045.3.1.7:1:aes-256-ctr:2574c10be28a4c"
+ "09:sha256:2048:a750ec9dea91999f108f943485a20f273f4"
+ "0f75c37fc9bcccdedda514c8243e550d69ce1bd:02237a199d"
+ "7d945aa6492275a02881071eceec5749caf2485da8c64fb601"
+ "229098:ab13d251976dedab546b67354e7678821740dd534b7"
+ "49c2857f66bf62bbaddfd:ab13d251976dedab546b67354e76"
+ "78821740dd534b749c2857f66bf62bbaddfd"
+ };
+
+ test_begin("test_load_v2_key");
+ const char *error = NULL;
+ buffer_t *tmp = buffer_create_dynamic(default_pool, 256);
+
+ struct dcrypt_private_key *priv,*priv2;
+
+ test_assert_idx(dcrypt_key_load_private(&priv2,
+ keys[0], NULL, NULL, &error), 0);
+ test_assert_idx(dcrypt_key_store_private(priv2,
+ DCRYPT_FORMAT_PEM, NULL, tmp, NULL, NULL, &error), 0);
+ test_assert_idx(strcmp(str_c(tmp), keys[0])==0, 0);
+ buffer_set_used_size(tmp, 0);
+
+ test_assert_idx(dcrypt_key_load_private(&priv,
+ keys[1], NULL, NULL, &error), 1);
+ test_assert_idx(dcrypt_key_store_private(priv,
+ DCRYPT_FORMAT_DOVECOT, NULL, tmp, NULL, NULL, &error), 1);
+ test_assert_idx(strcmp(str_c(tmp), keys[1])==0, 1);
+ buffer_set_used_size(tmp, 0);
+ dcrypt_key_unref_private(&priv);
+
+ test_assert_idx(dcrypt_key_load_private(&priv,
+ keys[2], "This Is Sparta", NULL, &error), 2);
+ test_assert_idx(dcrypt_key_store_private(priv,
+ DCRYPT_FORMAT_DOVECOT, "aes-256-ctr", tmp,
+ "This Is Sparta", NULL, &error), 2);
+ buffer_set_used_size(tmp, 0);
+ dcrypt_key_unref_private(&priv);
+
+ struct dcrypt_public_key *pub = NULL;
+ dcrypt_key_convert_private_to_public(priv2, &pub);
+ test_assert_idx(dcrypt_key_load_private(&priv,
+ keys[3], NULL, priv2, &error), 3);
+ test_assert_idx(dcrypt_key_store_private(priv,
+ DCRYPT_FORMAT_DOVECOT, "ecdh-aes-256-ctr", tmp,
+ NULL, pub, &error), 3);
+ buffer_set_used_size(tmp, 0);
+ dcrypt_key_unref_private(&priv2);
+ dcrypt_key_unref_private(&priv);
+ dcrypt_key_unref_public(&pub);
+
+ buffer_free(&tmp);
+
+ if (error != NULL) error = NULL;
+
+ test_end();
+}
+
+static void test_load_v2_public_key(void)
+{
+ struct dcrypt_public_key *pub = NULL;
+ const char *error;
+
+ test_begin("test_load_v2_public_key");
+ const char *key =
+ "2:3058301006072a8648ce3d020106052b810400230344000"
+ "301c50954e734dd8b410a607764a7057065a45510da52f2c6"
+ "e28e0cb353b9c389fa8cb786943ae991fce9befed78fb162f"
+ "bbc615415f06af06c8cc80c37f4e94ff6c7:185a721254278"
+ "2e239111f9c19d126ad55b18ddaf4883d66afe8d9627c3607"
+ "d8";
+
+ test_assert(dcrypt_key_load_public(&pub, key, &error));
+
+ buffer_t *tmp = buffer_create_dynamic(default_pool, 256);
+
+ if (pub != NULL) {
+ test_assert(dcrypt_key_store_public(pub,
+ DCRYPT_FORMAT_DOVECOT, tmp, &error));
+ test_assert(strcmp(key, str_c(tmp))==0);
+ buffer_free(&tmp);
+ dcrypt_key_unref_public(&pub);
+ }
+
+ test_end();
+}
+
+static void test_get_info_v2_key(void)
+{
+ test_begin("test_get_info_v2_key");
+
+ const char *key =
+ "2:305e301006072a8648ce3d020106052b81040026034a0002"
+ "03fcc90034fa03d6fb79a0fc8b3b43c3398f68e76029307360"
+ "cdcb9e27bb7e84b3c19dfb7244763bc4d442d216f09b7b7945"
+ "ed9d182f3156550e9ee30b237a0217dbf79d28975f31:86706"
+ "b69d1f640011a65d26a42f2ba20a619173644e1cc7475eb1d9"
+ "0966e84dc";
+ enum dcrypt_key_format format;
+ enum dcrypt_key_version version = DCRYPT_KEY_VERSION_NA;
+ enum dcrypt_key_kind kind;
+ enum dcrypt_key_encryption_type encryption_type;
+ const char *encryption_key_hash = NULL;
+ const char *key_hash = NULL;
+ const char *error = NULL;
+
+ test_assert(dcrypt_key_string_get_info(key, &format, &version,
+ &kind, &encryption_type, &encryption_key_hash,
+ &key_hash, &error));
+ test_assert(error == NULL);
+ test_assert(format == DCRYPT_FORMAT_DOVECOT);
+ test_assert(version == DCRYPT_KEY_VERSION_2);
+
+ test_assert(kind == DCRYPT_KEY_KIND_PUBLIC);
+ test_assert(encryption_type == DCRYPT_KEY_ENCRYPTION_TYPE_NONE);
+ test_assert(encryption_key_hash == NULL);
+ test_assert(key_hash != NULL && strcmp(key_hash,
+ "86706b69d1f640011a65d26a42f2ba20"
+ "a619173644e1cc7475eb1d90966e84dc") == 0);
+
+ test_end();
+}
+
+static void test_gen_and_get_info_rsa_pem(void)
+{
+ test_begin("test_gen_and_get_info_rsa_pem");
+
+ const char *error = NULL;
+ bool ret = FALSE;
+ struct dcrypt_keypair pair;
+ string_t* buf = str_new(default_pool, 4096);
+
+ ret = dcrypt_keypair_generate(&pair, DCRYPT_KEY_RSA, 1024, NULL, NULL);
+ test_assert(ret == TRUE);
+
+ /* test public key */
+ enum dcrypt_key_format format;
+ enum dcrypt_key_version version;
+ enum dcrypt_key_kind kind;
+ enum dcrypt_key_encryption_type encryption_type;
+ const char *encryption_key_hash;
+ const char *key_hash;
+
+ ret = dcrypt_key_store_public(pair.pub, DCRYPT_FORMAT_PEM, buf,
+ &error);
+ test_assert(ret == TRUE);
+
+ ret = dcrypt_key_string_get_info(str_c(buf), &format, &version,
+ &kind, &encryption_type, &encryption_key_hash,
+ &key_hash, &error);
+ test_assert(ret == TRUE);
+ test_assert(format == DCRYPT_FORMAT_PEM);
+ test_assert(version == DCRYPT_KEY_VERSION_NA);
+
+ test_assert(kind == DCRYPT_KEY_KIND_PUBLIC);
+ test_assert(encryption_type == DCRYPT_KEY_ENCRYPTION_TYPE_NONE);
+ test_assert(encryption_key_hash == NULL);
+ test_assert(key_hash == NULL);
+
+ /* test private key */
+ buffer_set_used_size(buf, 0);
+ ret = dcrypt_key_store_private(pair.priv, DCRYPT_FORMAT_PEM, NULL,
+ buf, NULL, NULL, &error);
+
+ test_assert(ret == TRUE);
+
+ ret = dcrypt_key_string_get_info(str_c(buf), &format, &version,
+ &kind, &encryption_type, &encryption_key_hash,
+ &key_hash, &error);
+
+ test_assert(ret == TRUE);
+ test_assert(format == DCRYPT_FORMAT_PEM);
+ test_assert(version == DCRYPT_KEY_VERSION_NA);
+
+ test_assert(kind == DCRYPT_KEY_KIND_PRIVATE);
+
+ test_assert(encryption_type == DCRYPT_KEY_ENCRYPTION_TYPE_NONE);
+ test_assert(encryption_key_hash == NULL);
+ test_assert(key_hash == NULL);
+
+ dcrypt_keypair_unref(&pair);
+ buffer_free(&buf);
+
+ test_end();
+}
+
+static void test_get_info_rsa_private_key(void)
+{
+ test_begin("test_get_info_rsa_private_key");
+
+ const char *key = "-----BEGIN RSA PRIVATE KEY-----\n"
+"MIICXQIBAAKBgQC89q02I9NezBLQ+otn5XLYE7S+GsKUz59ogr45DA/6MI9jey0W\n"
+"56SeWQ1FJD1vDhAx/TRBMfOmhcIPsBjc5sakYOawPdoiqLjOIlO+iHwnbbmLuMsq\n"
+"ue09vgvZsKjuTr2F5DOFQY43Bq/Nd+4bjHJItdOM58+xwA2I/8vDbtI8jwIDAQAB\n"
+"AoGBAJCUrTMfdjqyKjN7f+6ewKBTc5eBIiB6O53ba3B6qj7jqNKVDIrZ8jq2KFEe\n"
+"yWKPgBS/h5vafHKNJU6bjmp2qMUJPB7PTA876eDo0cq9PplUqihiTlXJFwNQYtF+\n"
+"o27To5t25+5qdSAj657+lQfFT9Xn9fzYHDmotURxH10FgFkBAkEA+7Ny6lBTeb3W\n"
+"LnP0UPfPzQLilEr8u81PLWe69RGtsEaMQHGpHOl4e+bvvVYbG1cgxwxI1m01uR9r\n"
+"qpD3qLUdrQJBAMAw6UvN8R+opYTZzwqK7Nliil2QZMPmXM04SV1iFq26NM60w2Fm\n"
+"HqOOh0EbpSWsFtIgxJFWoZOtrguxqCJuUqsCQF3EoXf3StHczhDqM8eCOpD2lTCH\n"
+"qxXPy8JvlW+9EUbNUWykq0rRE4idJQ0VKe4KjHR6+Buh/dSkhvi5Hvpj1tUCQHRv\n"
+"LWeXZLVhXqWVrzEb6VHpuRnmGKX2MdLCfu/sNQEbBlMUgCnJzFYaSybOsMaZ81lq\n"
+"MKw8Z7coSYEcKFhzrfECQQD7l+4Bhy8Zuz6VoGGIZwIhxkJrImBFmaUwx8N6jg20\n"
+"sgDRYwCoGkGd7B8uIHZLJoWzSSutHiu5i5PYUy5VT1yT\n"
+"-----END RSA PRIVATE KEY-----\n";
+
+ const char *error = NULL;
+
+ test_assert(!dcrypt_key_string_get_info(key, NULL, NULL,
+ NULL, NULL, NULL, NULL, &error));
+ test_assert(error != NULL && strstr(error, "pkey") != NULL);
+
+ test_end();
+}
+
+static void test_get_info_invalid_keys(void)
+{
+ test_begin("test_get_info_invalid_keys");
+
+ const char *key =
+ "1:716:030131D8A5FD5167947A0AE9CB112ADED6526654635A"
+ "A5887051EE2364414B60FF32EBA8FA0BBE9485DBDE8794BBBC"
+ "B44BBFC0D662A4287A848BA570D4E5E45A11FE0F:d0cfaca5d"
+ "335f9edc41c84bb47465184cb0e2ec3931bebfcea4dd433615"
+ "e77a0";
+ const char *error = NULL;
+
+ test_assert(dcrypt_key_string_get_info(key, NULL, NULL,
+ NULL, NULL, NULL, NULL, &error) == FALSE);
+ test_assert(error != NULL && strstr(error, "tab") != NULL);
+
+ key =
+ "2\t305e301006072a8648ce3d020106052b81040026034a000"
+ "203fcc90034fa03d6fb79a0fc8b3b43c3398f68e7602930736"
+ "0cdcb9e27bb7e84b3c19dfb7244763bc4d442d216f09b7b794"
+ "5ed9d182f3156550e9ee30b237a0217dbf79d28975f31\t867"
+ "06b69d1f640011a65d26a42f2ba20a619173644e1cc7475eb1"
+ "d90966e84dc";
+ error = NULL;
+
+ test_assert(dcrypt_key_string_get_info(key, NULL, NULL,
+ NULL, NULL, NULL, NULL, &error) == FALSE);
+ test_assert(error != NULL && strstr(error, "colon") != NULL);
+
+ key = "2";
+ error = NULL;
+
+ test_assert(dcrypt_key_string_get_info(key, NULL, NULL,
+ NULL, NULL, NULL, NULL, &error) == FALSE);
+ test_assert(error != NULL && strstr(error, "Unknown") != NULL);
+
+ test_end();
+}
+
+static void test_get_info_key_encrypted(void)
+{
+ test_begin("test_get_info_key_encrypted");
+
+ struct dcrypt_keypair p1, p2;
+ const char *error = NULL;
+ bool ret = dcrypt_keypair_generate(&p1,
+ DCRYPT_KEY_EC, 0, "secp521r1", &error);
+ test_assert(ret == TRUE);
+ ret = dcrypt_keypair_generate(&p2,
+ DCRYPT_KEY_EC, 0, "secp521r1", &error);
+ test_assert(ret == TRUE);
+
+ string_t* buf = t_str_new(4096);
+
+ buffer_set_used_size(buf, 0);
+ ret = dcrypt_key_store_private(p1.priv,
+ DCRYPT_FORMAT_DOVECOT, "ecdh-aes-256-ctr", buf,
+ NULL, p2.pub, &error);
+ test_assert(ret == TRUE);
+
+ enum dcrypt_key_format format;
+ enum dcrypt_key_version version;
+ enum dcrypt_key_kind kind;
+ enum dcrypt_key_encryption_type enc_type;
+ const char *enc_hash;
+ const char *key_hash;
+
+ ret = dcrypt_key_string_get_info(str_c(buf), &format, &version,
+ &kind, &enc_type, &enc_hash, &key_hash, &error);
+ test_assert(ret == TRUE);
+ test_assert(format == DCRYPT_FORMAT_DOVECOT);
+ test_assert(version == DCRYPT_KEY_VERSION_2);
+ test_assert(kind == DCRYPT_KEY_KIND_PRIVATE);
+ test_assert(enc_type == DCRYPT_KEY_ENCRYPTION_TYPE_KEY);
+ test_assert(enc_hash != NULL);
+ test_assert(key_hash != NULL);
+
+ dcrypt_keypair_unref(&p1);
+ dcrypt_keypair_unref(&p2);
+
+ test_end();
+}
+
+static void test_get_info_pw_encrypted(void)
+{
+ test_begin("test_get_info_pw_encrypted");
+
+ struct dcrypt_keypair p1;
+ i_zero(&p1);
+ const char *error;
+ bool ret = dcrypt_keypair_generate(&p1,
+ DCRYPT_KEY_EC, 0, "secp521r1", &error);
+ test_assert(ret == TRUE);
+
+ string_t* buf = t_str_new(4096);
+ ret = dcrypt_key_store_private(p1.priv,
+ DCRYPT_FORMAT_DOVECOT, "aes-256-ctr", buf, "pw", NULL, &error);
+ test_assert(ret == TRUE);
+
+ enum dcrypt_key_format format;
+ enum dcrypt_key_version version;
+ enum dcrypt_key_kind kind;
+ enum dcrypt_key_encryption_type enc_type;
+ const char *enc_hash;
+ const char *key_hash;
+
+ ret = dcrypt_key_string_get_info(str_c(buf), &format, &version,
+ &kind, &enc_type, &enc_hash, &key_hash, &error);
+ test_assert(ret == TRUE);
+ test_assert(format == DCRYPT_FORMAT_DOVECOT);
+ test_assert(version == DCRYPT_KEY_VERSION_2);
+ test_assert(kind == DCRYPT_KEY_KIND_PRIVATE);
+ test_assert(enc_type == DCRYPT_KEY_ENCRYPTION_TYPE_PASSWORD);
+ test_assert(enc_hash == NULL);
+ test_assert(key_hash != NULL);
+
+ dcrypt_keypair_unref(&p1);
+
+ test_end();
+}
+
+static void test_password_change(void)
+{
+ test_begin("test_password_change");
+
+ const char *pw1 = "first password";
+ struct dcrypt_keypair orig;
+ const char *error = NULL;
+
+ bool ret = dcrypt_keypair_generate(&orig,
+ DCRYPT_KEY_EC, 0, "secp521r1", &error);
+ test_assert(ret == TRUE);
+
+ string_t *buf = t_str_new(4096);
+ ret = dcrypt_key_store_private(orig.priv,
+ DCRYPT_FORMAT_DOVECOT, "aes-256-ctr", buf, pw1, NULL, &error);
+ test_assert(ret == TRUE);
+
+ /* load the pw-encrypted key */
+ struct dcrypt_private_key *k1_priv = NULL;
+ ret = dcrypt_key_load_private(&k1_priv, str_c(buf), pw1, NULL, &error);
+ test_assert(ret == TRUE);
+
+ /* encrypt a key with the pw-encrypted key k1 */
+ struct dcrypt_keypair k2;
+ ret = dcrypt_keypair_generate(&k2,
+ DCRYPT_KEY_EC, 0, "secp521r1", &error);
+ test_assert(ret == TRUE);
+
+ string_t *buf2 = t_str_new(4096);
+ struct dcrypt_public_key *k1_pub = NULL;
+ dcrypt_key_convert_private_to_public(k1_priv, &k1_pub);
+ ret = dcrypt_key_store_private(k2.priv,
+ DCRYPT_FORMAT_DOVECOT, "ecdh-aes-256-ctr", buf2,
+ NULL, k1_pub, &error);
+ test_assert(ret == TRUE);
+
+ /* change the password */
+ const char *pw2 = "second password";
+ string_t *buf3 = t_str_new(4096);
+
+ /* encrypt k1 with pw2 */
+ ret = dcrypt_key_store_private(k1_priv,
+ DCRYPT_FORMAT_DOVECOT, "aes-256-ctr", buf3, pw2, NULL, &error);
+ test_assert(ret == TRUE);
+
+ /* load the pw2 encrypted key */
+ struct dcrypt_private_key *k2_priv = NULL;
+ ret = dcrypt_key_load_private(&k2_priv, str_c(buf3), pw2, NULL, &error);
+ test_assert(ret == TRUE);
+
+ /* load the key that was encrypted with pw1 using the pw2 encrypted key */
+ struct dcrypt_private_key *k3_priv = NULL;
+ ret = dcrypt_key_load_private(&k3_priv,
+ str_c(buf2), NULL, k2_priv, &error);
+ test_assert(ret == TRUE);
+
+ dcrypt_key_unref_private(&k1_priv);
+ dcrypt_key_unref_public(&k1_pub);
+ dcrypt_key_unref_private(&k2_priv);
+ dcrypt_key_unref_private(&k3_priv);
+ dcrypt_keypair_unref(&orig);
+ dcrypt_keypair_unref(&k2);
+
+ test_end();
+}
+
+static void test_load_invalid_keys(void)
+{
+ test_begin("test_load_invalid_keys");
+
+ const char *error = NULL;
+ const char *key =
+ "1:716:0301EB00973C4EFC8FCECA4EA33E941F50B561199A51"
+ "59BCB6C2EED9DD1D62D65E38A254979D89E28F0C28883E71EE"
+ "2AD264CD16B863FA094A8F6F69A56B62E8918040:7c9a1039e"
+ "a2e4fed73e81dd3ffc3fa22ea4a28352939adde7bf8ea858b0"
+ "0fa4f";
+ struct dcrypt_public_key *pub_key = NULL;
+
+ bool ret = dcrypt_key_load_public(&pub_key, key, &error);
+ test_assert(ret == FALSE);
+ test_assert(error != NULL);
+
+ error = NULL;
+ key =
+ "2:305e301006072a8648ce3d020106052b81040026034a0002"
+ "03fcc90034fa03d6fb79a0fc8b3b43c3398f68e76029307360"
+ "cdcb9e27bb7e84b3c19dfb7244763bc4d442d216f09b7b7945"
+ "ed9d182f3156550e9ee30b237a0217dbf79d28975f31:86706"
+ "b69d1f640011a65d26a42f2ba20a619173644e1cc7475eb1d9"
+ "0966e84dc";
+ struct dcrypt_private_key *priv_key = NULL;
+
+ ret = dcrypt_key_load_private(&priv_key, key, NULL, NULL, &error);
+ test_assert(ret == FALSE);
+ test_assert(error != NULL);
+
+ test_end();
+}
+
+static void test_raw_keys(void)
+{
+
+ test_begin("test_raw_keys");
+
+ ARRAY_TYPE(dcrypt_raw_key) priv_key;
+ ARRAY_TYPE(dcrypt_raw_key) pub_key;
+ pool_t pool = pool_datastack_create();
+
+ enum dcrypt_key_type t;
+
+ p_array_init(&priv_key, pool, 2);
+ p_array_init(&pub_key, pool, 2);
+
+ /* generate ECC key */
+ struct dcrypt_keypair pair;
+ i_assert(dcrypt_keypair_generate(&pair, DCRYPT_KEY_EC, 0, "prime256v1", NULL));
+
+ /* store it */
+ test_assert(dcrypt_key_store_private_raw(pair.priv, pool, &t, &priv_key,
+ NULL));
+ test_assert(dcrypt_key_store_public_raw(pair.pub, pool, &t, &pub_key,
+ NULL));
+ dcrypt_keypair_unref(&pair);
+
+ /* load it */
+ test_assert(dcrypt_key_load_private_raw(&pair.priv, t, &priv_key,
+ NULL));
+ test_assert(dcrypt_key_load_public_raw(&pair.pub, t, &pub_key,
+ NULL));
+
+ dcrypt_keypair_unref(&pair);
+
+ /* test load known raw private key */
+ const char *curve = "prime256v1";
+ const unsigned char priv_key_data[] = {
+ 0x16, 0x9e, 0x62, 0x36, 0xaf, 0x9c, 0xae, 0x0e, 0x71, 0xda,
+ 0xf2, 0x63, 0xe2, 0xe0, 0x5d, 0xf1, 0xd5, 0x35, 0x8c, 0x2b,
+ 0x68, 0xf0, 0x2a, 0x69, 0xc4, 0x5d, 0x3d, 0x1c, 0xde, 0xa1,
+ 0x9b, 0xd3
+ };
+
+ /* create buffers */
+ struct dcrypt_raw_key *item;
+ ARRAY_TYPE(dcrypt_raw_key) static_key;
+ t_array_init(&static_key, 2);
+
+ /* Add OID */
+ buffer_t *buf = t_buffer_create(32);
+ test_assert(dcrypt_name2oid(curve, buf, NULL));
+ item = array_append_space(&static_key);
+ item->parameter = buf->data;
+ item->len = buf->used;
+
+ /* Add key data */
+ item = array_append_space(&static_key);
+ item->parameter = priv_key_data;
+ item->len = sizeof(priv_key_data);
+
+ /* Try load it */
+ test_assert(dcrypt_key_load_private_raw(&pair.priv, t,
+ &static_key, NULL));
+
+ /* See what we got */
+ buf = t_buffer_create(128);
+ test_assert(dcrypt_key_store_private(pair.priv, DCRYPT_FORMAT_DOVECOT,
+ NULL, buf, NULL, NULL, NULL));
+ test_assert_strcmp(str_c(buf),
+ "2:1.2.840.10045.3.1.7:0:00000020169e6236af9cae0e71d"
+ "af263e2e05df1d5358c2b68f02a69c45d3d1cdea19bd3:21d11"
+ "6b7b3e5c52e81f0437a10b0116cfafc467fb1b96e48926d0216"
+ "68fc1bea");
+
+ /* try to load public key, too */
+ const unsigned char pub_key_data[] = {
+ 0x04, 0xe8, 0x7c, 0x6d, 0xa0, 0x29, 0xfe, 0x5d, 0x16, 0x1a,
+ 0xd6, 0x6a, 0xc6, 0x1c, 0x78, 0x8a, 0x36, 0x0f, 0xfb, 0x64,
+ 0xe7, 0x7f, 0x58, 0x13, 0xb3, 0x80, 0x1f, 0x99, 0x45, 0xee,
+ 0xa9, 0x4a, 0xe2, 0xde, 0xf3, 0x88, 0xc6, 0x37, 0x72, 0x7f,
+ 0xbe, 0x97, 0x02, 0x94, 0xb2, 0x21, 0x60, 0xa4, 0x98, 0x4e,
+ 0xfb, 0x46, 0x19, 0x61, 0x4c, 0xc5, 0xe1, 0x9f, 0xe9, 0xb2,
+ 0xd2, 0x4d, 0xae, 0x83, 0x4b
+ };
+
+ array_clear(&static_key);
+
+ /* Add OID */
+ buf = t_buffer_create(32);
+ test_assert(dcrypt_name2oid(curve, buf, NULL));
+ item = array_append_space(&static_key);
+ item->parameter = buf->data;
+ item->len = buf->used;
+
+ /* Add key data */
+ item = array_append_space(&static_key);
+ item->parameter = pub_key_data;
+ item->len = sizeof(pub_key_data);
+
+ /* See what we got */
+ test_assert(dcrypt_key_load_public_raw(&pair.pub, t,
+ &static_key, NULL));
+ buf = t_buffer_create(128);
+ test_assert(dcrypt_key_store_public(pair.pub, DCRYPT_FORMAT_DOVECOT,
+ buf, NULL));
+ test_assert_strcmp(str_c(buf),
+ "2:3039301306072a8648ce3d020106082a8648ce3d03010703220003e87c6d"
+ "a029fe5d161ad66ac61c788a360ffb64e77f5813b3801f9945eea94ae2:21d"
+ "116b7b3e5c52e81f0437a10b0116cfafc467fb1b96e48926d021668fc1bea");
+ dcrypt_keypair_unref(&pair);
+
+ test_end();
+}
+
+static void test_sign_verify_rsa(void)
+{
+ const char *error = NULL;
+ bool valid;
+ struct dcrypt_private_key *priv_key = NULL;
+ struct dcrypt_public_key *pub_key = NULL;
+
+ buffer_t *signature =
+ buffer_create_dynamic(pool_datastack_create(), 128);
+ const char *data = "signed data";
+
+ test_begin("sign and verify (rsa)");
+ const char *key = "-----BEGIN PRIVATE KEY-----\n"
+"MIICdwIBADANBgkqhkiG9w0BAQEFAASCAmEwggJdAgEAAoGBALz2rTYj017MEtD6\n"
+"i2flctgTtL4awpTPn2iCvjkMD/owj2N7LRbnpJ5ZDUUkPW8OEDH9NEEx86aFwg+w\n"
+"GNzmxqRg5rA92iKouM4iU76IfCdtuYu4yyq57T2+C9mwqO5OvYXkM4VBjjcGr813\n"
+"7huMcki104znz7HADYj/y8Nu0jyPAgMBAAECgYEAkJStMx92OrIqM3t/7p7AoFNz\n"
+"l4EiIHo7ndtrcHqqPuOo0pUMitnyOrYoUR7JYo+AFL+Hm9p8co0lTpuOanaoxQk8\n"
+"Hs9MDzvp4OjRyr0+mVSqKGJOVckXA1Bi0X6jbtOjm3bn7mp1ICPrnv6VB8VP1ef1\n"
+"/NgcOai1RHEfXQWAWQECQQD7s3LqUFN5vdYuc/RQ98/NAuKUSvy7zU8tZ7r1Ea2w\n"
+"RoxAcakc6Xh75u+9VhsbVyDHDEjWbTW5H2uqkPeotR2tAkEAwDDpS83xH6ilhNnP\n"
+"Cors2WKKXZBkw+ZczThJXWIWrbo0zrTDYWYeo46HQRulJawW0iDEkVahk62uC7Go\n"
+"Im5SqwJAXcShd/dK0dzOEOozx4I6kPaVMIerFc/Lwm+Vb70RRs1RbKSrStETiJ0l\n"
+"DRUp7gqMdHr4G6H91KSG+Lke+mPW1QJAdG8tZ5dktWFepZWvMRvpUem5GeYYpfYx\n"
+"0sJ+7+w1ARsGUxSAKcnMVhpLJs6wxpnzWWowrDxntyhJgRwoWHOt8QJBAPuX7gGH\n"
+"Lxm7PpWgYYhnAiHGQmsiYEWZpTDHw3qODbSyANFjAKgaQZ3sHy4gdksmhbNJK60e\n"
+"K7mLk9hTLlVPXJM=\n"
+"-----END PRIVATE KEY-----";
+
+ test_assert(dcrypt_key_load_private(&priv_key,
+ key, NULL, NULL, &error));
+ if (priv_key == NULL)
+ i_fatal("%s", error);
+ dcrypt_key_convert_private_to_public(priv_key, &pub_key);
+ test_assert(dcrypt_sign(priv_key, "sha256", DCRYPT_SIGNATURE_FORMAT_DSS,
+ data, strlen(data), signature, 0, &error));
+ /* verify signature */
+ test_assert(dcrypt_verify(pub_key, "sha256", DCRYPT_SIGNATURE_FORMAT_DSS,
+ data, strlen(data),
+ signature->data, signature->used, &valid, 0, &error) && valid);
+
+ dcrypt_key_unref_public(&pub_key);
+ dcrypt_key_unref_private(&priv_key);
+
+ test_end();
+}
+
+static void test_sign_verify_ecdsa(void)
+{
+ const char *error = NULL;
+ bool valid;
+ struct dcrypt_private_key *priv_key = NULL;
+ struct dcrypt_public_key *pub_key = NULL;
+
+ buffer_t *signature =
+ buffer_create_dynamic(pool_datastack_create(), 128);
+ const char *data = "signed data";
+
+ test_begin("sign and verify (ecdsa)");
+ const char *key = "-----BEGIN PRIVATE KEY-----\n"
+"MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgZ4AMMyJ9XDl5lKM2\n"
+"vusbT1OQ6VzBWBkB3/4syovaKtyhRANCAAQHTR+6L2qMh5fdcMZF+Y1rctBsq8Oy\n"
+"7jZ4uV+MiuaoGNQ5sTxlcv6ETX/XrEDq4S/DUhFKzQ6u9VXYZImvRCT1\n"
+"-----END PRIVATE KEY-----";
+
+ test_assert(dcrypt_key_load_private(&priv_key,
+ key, NULL, NULL, &error));
+ if (priv_key == NULL)
+ i_fatal("%s", error);
+ dcrypt_key_convert_private_to_public(priv_key, &pub_key);
+ test_assert(dcrypt_sign(priv_key, "sha256", DCRYPT_SIGNATURE_FORMAT_DSS,
+ data, strlen(data), signature, 0, &error));
+ /* verify signature */
+ test_assert(dcrypt_verify(pub_key, "sha256", DCRYPT_SIGNATURE_FORMAT_DSS,
+ data, strlen(data), signature->data,
+ signature->used, &valid, 0, &error) && valid);
+
+ dcrypt_key_unref_public(&pub_key);
+ dcrypt_key_unref_private(&priv_key);
+
+ test_end();
+}
+
+static void test_static_verify_ecdsa(void)
+{
+ test_begin("static verify (ecdsa)");
+ const char *input = "hello, world";
+ const char *priv_key_pem =
+ "-----BEGIN PRIVATE KEY-----\n"
+ "MGcCAQAwEwYHKoZIzj0CAQYIKoZIzj0DAQcETTBLAgEBBCC25AkD65uhlZXCAdwN\n"
+ "yLJV2ui8A/CUyqyEMrezvwgMO6EkAyIAAybRUR3MsH0+0PQcDwkrXOJ9aePwzTQV\n"
+ "DN51+n1JCxbI\n"
+ "-----END PRIVATE KEY-----";
+ const char *pub_key_pem =
+ "-----BEGIN PUBLIC KEY-----\n"
+ "MDkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDIgADJtFRHcywfT7Q9BwPCStc4n1p4/DN\n"
+ "NBUM3nX6fUkLFsg=\n"
+ "-----END PUBLIC KEY-----";
+
+ const unsigned char sig[] = {
+ 0x30,0x45,0x02,0x20,0x2c,0x76,0x20,0x5e,0xfc,0xa6,0x9e,0x16,
+ 0x44,0xb3,0xbc,0xbf,0xcc,0x43,0xc1,0x08,0x76,0x4a,0xe8,0x60,
+ 0xc5,0x9b,0x99,0x20,0x5b,0x44,0x33,0x5c,0x38,0x84,0x63,0xcb,
+ 0x02,0x21,0x00,0xa3,0x67,0xed,0x57,0xbf,0x59,0x46,0xb7,0x0c,
+ 0x7b,0xec,0x4f,0x78,0x14,0xec,0xfa,0x8d,0xa2,0x85,0x48,0xea,
+ 0xe1,0xaf,0x9e,0xbf,0x04,0xac,0x0e,0x41,0xfe,0x84,0x0e
+ };
+
+ struct dcrypt_keypair pair;
+ bool valid;
+ const char *error;
+
+ i_zero(&pair);
+ /* static key test */
+ test_assert(dcrypt_key_load_public(&pair.pub, pub_key_pem, NULL));
+ test_assert(dcrypt_key_load_private(&pair.priv, priv_key_pem, NULL, NULL, NULL));
+ /* validate signature */
+ test_assert(dcrypt_verify(pair.pub, "sha256", DCRYPT_SIGNATURE_FORMAT_DSS,
+ input, strlen(input),
+ sig, sizeof(sig), &valid, 0, &error) &&
+ valid == TRUE);
+
+ dcrypt_keypair_unref(&pair);
+
+ test_end();
+}
+
+static void test_jwk_keys(void)
+{
+ /* Make sure this matches what comes out from store private */
+ const char *jwk_key_json = "{\"kty\":\"EC\","
+ "\"crv\":\"P-256\","
+ "\"x\":\"Kp0Y4-Wpt-D9t_2XenFIj0LmvaZByLG69yOisek4aMI\","
+ "\"y\":\"wjEPB5BhH5SRPw1cCN5grWrLCphrW19fCFR8p7c9O5o\","
+ "\"use\":\"sig\","
+ "\"kid\":\"123\","
+ "\"d\":\"Po2z9rs86J2Qb_xWprr4idsWNPlgKf3G8-mftnE2ync\"}";
+ /* Acquired using another tool */
+ const char *pem_key =
+ "-----BEGIN PUBLIC KEY-----\n"
+ "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEKp0Y4+Wpt+D9t/2XenFIj0LmvaZB\n"
+ "yLG69yOisek4aMLCMQ8HkGEflJE/DVwI3mCtassKmGtbX18IVHyntz07mg==\n"
+ "-----END PUBLIC KEY-----";
+ test_begin("test_jwk_keys");
+ struct dcrypt_keypair pair;
+ buffer_t *pem = t_buffer_create(256);
+ i_zero(&pair);
+
+ test_assert(dcrypt_key_load_public(&pair.pub, jwk_key_json, NULL));
+ test_assert(dcrypt_key_load_private(&pair.priv, jwk_key_json, NULL, NULL, NULL));
+
+ /* test accessors */
+ test_assert_strcmp(dcrypt_key_get_id_public(pair.pub), "123");
+ test_assert(dcrypt_key_get_usage_public(pair.pub) == DCRYPT_KEY_USAGE_SIGN);
+
+ /* make sure we got the right key */
+ test_assert(dcrypt_key_store_public(pair.pub, DCRYPT_FORMAT_PEM, pem, NULL));
+ test_assert_strcmp(str_c(pem), pem_key);
+
+ str_truncate(pem, 0);
+ test_assert(dcrypt_key_store_private(pair.priv, DCRYPT_FORMAT_JWK, NULL, pem, NULL, NULL, NULL));
+ test_assert_strcmp(str_c(pem), jwk_key_json);
+
+ dcrypt_keypair_unref(&pair);
+
+ test_end();
+}
+
+static void test_static_verify_rsa(void)
+{
+ const char *error = NULL;
+ bool valid;
+ struct dcrypt_public_key *pub_key = NULL;
+
+ test_begin("static verify (rsa)");
+ const char *data = "test signature input\n";
+ const unsigned char sig[] = {
+ 0x6f,0x1b,0xfb,0xdd,0xdb,0xb1,0xcd,0x6f,0xf1,0x1b,
+ 0xb8,0xad,0x71,0x75,0x6c,0x87,0x22,0x11,0xe4,0xc3,
+ 0xe7,0xca,0x15,0x04,0xda,0x98,0xab,0x07,0x27,0xcc,
+ 0x5a,0x4d,0xab,0xac,0x37,0x7a,0xff,0xd2,0xdf,0x37,
+ 0x58,0x37,0x53,0x46,0xd5,0x6d,0x9d,0x73,0x83,0x90,
+ 0xea,0x5e,0x2c,0xc7,0x51,0x9e,0xc4,0xda,0xc5,0x7d,
+ 0xa5,0xcd,0xb7,0xd7,0x41,0x23,0x6d,0xb9,0x6d,0xe0,
+ 0x99,0xa1,0x63,0x6b,0x60,0x5f,0x15,0x5b,0xda,0x21,
+ 0x17,0x4c,0x37,0x68,0x67,0x7f,0x8e,0x02,0x93,0xd2,
+ 0x86,0xdd,0xe5,0xa7,0xc3,0xd9,0x93,0x8b,0x0c,0x56,
+ 0x1d,0x5c,0x60,0x63,0x3e,0x8b,0xbe,0x1f,0xb2,0xe7,
+ 0x7f,0xe5,0x66,0x6f,0xcd,0x2b,0x0c,0x02,0x2a,0x12,
+ 0x96,0x86,0x66,0x00,0xff,0x12,0x8a,0x79
+ };
+ const char *key = "-----BEGIN PUBLIC KEY-----\n"
+"MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC89q02I9NezBLQ+otn5XLYE7S+\n"
+"GsKUz59ogr45DA/6MI9jey0W56SeWQ1FJD1vDhAx/TRBMfOmhcIPsBjc5sakYOaw\n"
+"PdoiqLjOIlO+iHwnbbmLuMsque09vgvZsKjuTr2F5DOFQY43Bq/Nd+4bjHJItdOM\n"
+"58+xwA2I/8vDbtI8jwIDAQAB\n"
+"-----END PUBLIC KEY-----";
+
+ test_assert(dcrypt_key_load_public(&pub_key, key, &error));
+ if (pub_key == NULL)
+ i_fatal("%s", error);
+ test_assert(dcrypt_verify(pub_key, "sha256", DCRYPT_SIGNATURE_FORMAT_DSS,
+ data, strlen(data),
+ sig, sizeof(sig), &valid, DCRYPT_PADDING_RSA_PKCS1, &error) &&
+ valid);
+ dcrypt_key_unref_public(&pub_key);
+
+ test_end();
+}
+
+/* Sample values from RFC8292 */
+static void test_static_verify_ecdsa_x962(void)
+{
+ const char *error = NULL;
+ bool valid;
+ struct dcrypt_public_key *pub_key = NULL;
+
+ test_begin("static verify (ecdsa x9.62)");
+ const char *data =
+ "eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NiJ9.eyJhdWQiOiJodHRwczovL3B1c"
+ "2guZXhhbXBsZS5uZXQiLCJleHAiOjE0NTM1MjM3NjgsInN1YiI6Im1haWx0bzp"
+ "wdXNoQGV4YW1wbGUuY29tIn0";
+ const unsigned char sig[] = {
+ 0x8b,0x70,0x98,0x6f,0xbb,0x78,0xc5,0xfc,0x42,0x0e,0xab,
+ 0xa9,0xb4,0x53,0x9e,0xa4,0x2f,0x46,0x02,0xef,0xc7,0x2c,
+ 0x69,0x0c,0x94,0xcb,0x82,0x19,0x22,0xb6,0xae,0x98,0x94,
+ 0x7e,0x72,0xbd,0xa2,0x31,0x70,0x0d,0x76,0xf5,0x26,0xb1,
+ 0x2b,0xb6,0x6c,0xac,0x6b,0x33,0x63,0x8e,0xf5,0xb6,0x2f,
+ 0xd3,0xa4,0x49,0x21,0xf3,0xbe,0x80,0xf5,0xa0
+ };
+ const char *key =
+"-----BEGIN PUBLIC KEY-----\n"
+"MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEDUfHPKLVFQzVvnCPGyfucbECzPDa\n"
+"7rWbXriLcysAjEcXpgrmHhINiJz51G5T9EI8J8Dlqr2iNLCTljYSYKUE+w==\n"
+"-----END PUBLIC KEY-----";
+
+ test_assert(dcrypt_key_load_public(&pub_key, key, &error));
+ if (pub_key == NULL)
+ i_fatal("%s", error);
+ test_assert(dcrypt_verify(pub_key, "sha256", DCRYPT_SIGNATURE_FORMAT_X962,
+ data, strlen(data),
+ sig, sizeof(sig), &valid, DCRYPT_PADDING_RSA_PKCS1, &error) &&
+ valid);
+ dcrypt_key_unref_public(&pub_key);
+
+ test_end();
+}
+
+
+int main(void)
+{
+ struct dcrypt_settings set = {
+ .module_dir = ".libs"
+ };
+ const char *error;
+
+ if (!dcrypt_initialize(NULL, &set, &error)) {
+ i_error("No functional dcrypt backend found - "
+ "skipping tests: %s", error);
+ return 0;
+ }
+
+ static void (*const test_functions[])(void) = {
+ test_cipher_test_vectors,
+ test_cipher_aead_test_vectors,
+ test_hmac_test_vectors,
+ test_load_v1_keys,
+ test_load_v1_key,
+ test_load_v1_public_key,
+ test_load_v2_key,
+ test_load_v2_public_key,
+ test_get_info_v2_key,
+ test_gen_and_get_info_rsa_pem,
+ test_get_info_rsa_private_key,
+ test_get_info_invalid_keys,
+ test_get_info_key_encrypted,
+ test_get_info_pw_encrypted,
+ test_password_change,
+ test_load_invalid_keys,
+ test_raw_keys,
+ test_jwk_keys,
+ test_sign_verify_rsa,
+ test_sign_verify_ecdsa,
+ test_static_verify_ecdsa,
+ test_static_verify_rsa,
+ test_static_verify_ecdsa_x962,
+ NULL
+ };
+
+ int ret = test_run(test_functions);
+
+ dcrypt_deinitialize();
+
+ return ret;
+}
diff --git a/src/lib-dcrypt/test-stream.c b/src/lib-dcrypt/test-stream.c
new file mode 100644
index 0000000..8c1a985
--- /dev/null
+++ b/src/lib-dcrypt/test-stream.c
@@ -0,0 +1,639 @@
+/* Copyright (c) 2016-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "buffer.h"
+#include "str.h"
+#include "dcrypt.h"
+#include "dcrypt-iostream.h"
+#include "ostream.h"
+#include "ostream-encrypt.h"
+#include "istream.h"
+#include "istream-decrypt.h"
+#include "istream-hash.h"
+#include "istream-base64.h"
+#include "randgen.h"
+#include "hash-method.h"
+#include "test-common.h"
+#include "hex-binary.h"
+
+#include <fcntl.h>
+#include <sys/stat.h>
+#include <stdio.h>
+
+static const char key_v1_priv[] =
+ "-----BEGIN PRIVATE KEY-----\n"
+ "MIGpAgEAMBAGByqGSM49AgEGBSuBBAAjBIGRMIGOAgEBBEGz2V2VMi/5s+Z+GJh7\n"
+ "4WfqZjZUpqqm+NJWojm6BbrZMY+9ZComlTGVcUZ007acFxV93oMmrfmtRUb5ynrb\n"
+ "MRFskKFGA0QAAwHrAJc8TvyPzspOoz6UH1C1YRmaUVm8tsLu2d0dYtZeOKJUl52J\n"
+ "4o8MKIg+ce4q0mTNFrhj+glKj29ppWti6JGAQA==\n"
+ "-----END PRIVATE KEY-----";
+
+static const char key_v1_pub[] =
+ "-----BEGIN PUBLIC KEY-----\n"
+ "MFgwEAYHKoZIzj0CAQYFK4EEACMDRAADAesAlzxO/I/Oyk6jPpQfULVhGZpRWby2\n"
+ "wu7Z3R1i1l44olSXnYnijwwoiD5x7irSZM0WuGP6CUqPb2mla2LokYBA\n"
+ "-----END PUBLIC KEY-----";
+
+static const char key_v2_priv[] =
+ "-----BEGIN PRIVATE KEY-----\n"
+ "MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgtuQJA+uboZWVwgHc\n"
+ "DciyVdrovAPwlMqshDK3s78IDDuhRANCAAQm0VEdzLB9PtD0HA8JK1zifWnj8M00\n"
+ "FQzedfp9SQsWyA8dzs5/NFR5MTe6Xbh/ndKEs1zZH3vZ4FlNrilZc0st\n"
+ "-----END PRIVATE KEY-----";
+
+static const char key_v2_pub[] =
+ "-----BEGIN PUBLIC KEY-----\n"
+ "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEJtFRHcywfT7Q9BwPCStc4n1p4/DN\n"
+ "NBUM3nX6fUkLFsgPHc7OfzRUeTE3ul24f53ShLNc2R972eBZTa4pWXNLLQ==\n"
+ "-----END PUBLIC KEY-----";
+
+static const char test_sample_v1_hash[] =
+ "1d7cc2cc1f1983f76241cc42389911e88590ad58cf9d54cafeb5b198d3723dd1";
+static const char test_sample_v1_short_hash[] =
+ "b5bb9d8014a0f9b1d61e21e796d78dccdf1352f23cd32812f4850b878ae4944c";
+static const char test_sample_v2_hash[] =
+ "2e31218656dd34db65b321688bf418dee4ee785e99eb9c21e0d29b4af27a863e";
+
+static struct dcrypt_keypair test_v1_kp;
+static struct dcrypt_keypair test_v2_kp;
+
+static void test_static_v1_input(void)
+{
+ ssize_t siz;
+ const struct hash_method *hash = hash_method_lookup("sha256");
+ unsigned char hash_ctx[hash->context_size];
+ unsigned char hash_dgst[hash->digest_size];
+ hash->init(hash_ctx);
+
+ test_begin("test_static_v1_input");
+
+ struct istream *is_1 =
+ i_stream_create_file(DCRYPT_SRC_DIR"/sample-v1.asc",
+ IO_BLOCK_SIZE);
+ struct istream *is_2 = i_stream_create_base64_decoder(is_1);
+ i_stream_unref(&is_1);
+ struct istream *is_3 = i_stream_create_decrypt(is_2, test_v1_kp.priv);
+ i_stream_unref(&is_2);
+ struct istream *is_4 = i_stream_create_hash(is_3, hash, hash_ctx);
+ i_stream_unref(&is_3);
+
+ while((siz = i_stream_read(is_4))>0) { i_stream_skip(is_4, siz); }
+
+ if (is_4->stream_errno != 0)
+ i_debug("error: %s", i_stream_get_error(is_4));
+
+ test_assert(is_4->stream_errno == 0);
+
+ i_stream_unref(&is_4);
+
+ hash->result(hash_ctx, hash_dgst);
+
+ test_assert(strcmp(test_sample_v1_hash,
+ binary_to_hex(hash_dgst, sizeof(hash_dgst))) == 0);
+
+ test_end();
+}
+
+static void test_static_v1_input_short(void)
+{
+ ssize_t siz;
+ const struct hash_method *hash = hash_method_lookup("sha256");
+ unsigned char hash_ctx[hash->context_size];
+ unsigned char hash_dgst[hash->digest_size];
+ hash->init(hash_ctx);
+
+ test_begin("test_static_v1_input_short");
+
+ struct istream *is_1 =
+ i_stream_create_file(DCRYPT_SRC_DIR"/sample-v1_short.asc",
+ IO_BLOCK_SIZE);
+ struct istream *is_2 = i_stream_create_base64_decoder(is_1);
+ i_stream_unref(&is_1);
+ struct istream *is_3 = i_stream_create_decrypt(is_2, test_v1_kp.priv);
+ i_stream_unref(&is_2);
+ struct istream *is_4 = i_stream_create_hash(is_3, hash, hash_ctx);
+ i_stream_unref(&is_3);
+
+ while((siz = i_stream_read(is_4))>0) { i_stream_skip(is_4, siz); }
+
+ if (is_4->stream_errno != 0)
+ i_debug("error: %s", i_stream_get_error(is_4));
+
+ test_assert(is_4->stream_errno == 0);
+
+ i_stream_unref(&is_4);
+
+ hash->result(hash_ctx, hash_dgst);
+
+ test_assert(strcmp(test_sample_v1_short_hash,
+ binary_to_hex(hash_dgst, sizeof(hash_dgst))) == 0);
+
+ test_end();
+}
+
+static void test_static_v2_input(void)
+{
+ test_begin("test_static_v2_input");
+
+ ssize_t amt;
+ const struct hash_method *hash = hash_method_lookup("sha256");
+ unsigned char hash_ctx[hash->context_size];
+ unsigned char hash_dgst[hash->digest_size];
+ hash->init(hash_ctx);
+
+ struct istream *is_1 =
+ i_stream_create_file(DCRYPT_SRC_DIR"/sample-v2.asc",
+ IO_BLOCK_SIZE);
+ struct istream *is_2 = i_stream_create_base64_decoder(is_1);
+ i_stream_unref(&is_1);
+ struct istream *is_3 = i_stream_create_decrypt(is_2, test_v2_kp.priv);
+ i_stream_unref(&is_2);
+ struct istream *is_4 = i_stream_create_hash(is_3, hash, hash_ctx);
+ i_stream_unref(&is_3);
+
+ while((amt = i_stream_read(is_4))>0) { i_stream_skip(is_4, amt); }
+
+ if (is_4->stream_errno != 0)
+ i_debug("error: %s", i_stream_get_error(is_4));
+
+ test_assert(is_4->stream_errno == 0);
+
+ i_stream_unref(&is_4);
+
+ hash->result(hash_ctx, hash_dgst);
+
+ test_assert(strcmp(test_sample_v2_hash,
+ binary_to_hex(hash_dgst, sizeof(hash_dgst))) == 0);
+
+ test_end();
+
+/** this code is left here to show how the sample file is created
+ struct istream *is =
+ i_stream_create_file("../lib-fts/udhr_fra.txt", 8192);
+ struct istream *is_2 = i_stream_create_hash(is, hash, hash_ctx);
+ int fd = open("sample-v2.bin", O_CREAT|O_TRUNC|O_WRONLY, S_IRWXU);
+ struct ostream *os = o_stream_create_fd_file(fd, 0, TRUE);
+ struct ostream *os_2 = o_stream_create_encrypt(os,
+ "aes-256-gcm-sha256", test_v2_kp.pub,
+ IO_STREAM_ENC_INTEGRITY_AEAD);
+ const unsigned char *ptr;
+ size_t siz;
+
+ while(i_stream_read_data(is_2, &ptr, &siz, 0)>0) {
+ o_stream_nsend(os_2, ptr, siz);
+ i_stream_skip(is_2, siz);
+ }
+
+ i_assert(o_stream_finish(os_2) > 0);
+
+ o_stream_close(os_2);
+ i_stream_close(is_2);
+
+ hash->result(hash_ctx, hash_dgst);
+ printf("%s\n", binary_to_hex(hash_dgst, sizeof(hash_dgst)));
+*/
+}
+
+static void test_write_read_v1(void)
+{
+ test_begin("test_write_read_v1");
+ unsigned char payload[IO_BLOCK_SIZE];
+ const unsigned char *ptr;
+ size_t pos = 0, siz;
+ random_fill(payload, IO_BLOCK_SIZE);
+
+ buffer_t *buf = buffer_create_dynamic(default_pool, sizeof(payload));
+ struct ostream *os = o_stream_create_buffer(buf);
+ struct ostream *os_2 = o_stream_create_encrypt(os,
+ "<unused>", test_v2_kp.pub, IO_STREAM_ENC_VERSION_1);
+ o_stream_nsend(os_2, payload, sizeof(payload));
+
+ if (os_2->stream_errno != 0)
+ i_debug("error: %s", o_stream_get_error(os_2));
+
+ test_assert(os_2->stream_errno == 0);
+ test_assert(o_stream_finish(os_2) > 0);
+ test_assert(os_2->stream_errno == 0);
+
+ o_stream_unref(&os);
+ o_stream_unref(&os_2);
+
+ struct istream *is = test_istream_create_data(buf->data, buf->used);
+ struct istream *is_2 = i_stream_create_decrypt(is, test_v2_kp.priv);
+
+ size_t offset = 0;
+ test_istream_set_allow_eof(is, FALSE);
+ test_istream_set_size(is, 0);
+ while(i_stream_read_data(is_2, &ptr, &siz, 0)>=0) {
+ if (offset == buf->used)
+ test_istream_set_allow_eof(is, TRUE);
+ else
+ test_istream_set_size(is, ++offset);
+
+ test_assert_idx(pos + siz <= sizeof(payload), pos);
+ if (pos + siz > sizeof(payload))
+ break;
+ test_assert_idx(siz == 0 ||
+ memcmp(ptr, payload + pos, siz) == 0, pos);
+ i_stream_skip(is_2, siz); pos += siz;
+ }
+
+ test_assert(is_2->stream_errno == 0);
+
+ i_stream_unref(&is);
+ i_stream_unref(&is_2);
+ buffer_free(&buf);
+
+ test_end();
+}
+
+static void test_write_read_v1_short(void)
+{
+ test_begin("test_write_read_v1_short");
+ unsigned char payload[1];
+ const unsigned char *ptr;
+ size_t pos = 0, siz;
+ random_fill(payload, 1);
+
+ buffer_t *buf = buffer_create_dynamic(default_pool, 64);
+ struct ostream *os = o_stream_create_buffer(buf);
+ struct ostream *os_2 = o_stream_create_encrypt(os,
+ "<unused>", test_v2_kp.pub, IO_STREAM_ENC_VERSION_1);
+ o_stream_nsend(os_2, payload, sizeof(payload));
+
+ if (os_2->stream_errno != 0)
+ i_debug("error: %s", o_stream_get_error(os_2));
+
+ test_assert(os_2->stream_errno == 0);
+ test_assert(o_stream_finish(os_2) > 0);
+ test_assert(os_2->stream_errno == 0);
+
+ o_stream_unref(&os);
+ o_stream_unref(&os_2);
+
+ struct istream *is = test_istream_create_data(buf->data, buf->used);
+ struct istream *is_2 = i_stream_create_decrypt(is, test_v2_kp.priv);
+
+ size_t offset = 0;
+ test_istream_set_allow_eof(is, FALSE);
+ test_istream_set_size(is, 0);
+ while(i_stream_read_data(is_2, &ptr, &siz, 0)>=0) {
+ if (offset == buf->used)
+ test_istream_set_allow_eof(is, TRUE);
+ else
+ test_istream_set_size(is, ++offset);
+
+ test_assert_idx(pos + siz <= sizeof(payload), pos);
+ if (siz > sizeof(payload) || pos + siz > sizeof(payload))
+ break;
+ test_assert_idx(siz == 0 ||
+ memcmp(ptr, payload + pos, siz) == 0, pos);
+ i_stream_skip(is_2, siz); pos += siz;
+ }
+
+ test_assert(is_2->stream_errno == 0);
+
+ i_stream_unref(&is);
+ i_stream_unref(&is_2);
+ buffer_free(&buf);
+
+ test_end();
+}
+
+static void test_write_read_v1_empty(void)
+{
+ const unsigned char *ptr;
+ size_t siz;
+ test_begin("test_write_read_v1_empty");
+ buffer_t *buf = buffer_create_dynamic(default_pool, 64);
+ struct ostream *os = o_stream_create_buffer(buf);
+ struct ostream *os_2 = o_stream_create_encrypt(os,
+ "<unused>", test_v1_kp.pub, IO_STREAM_ENC_VERSION_1);
+ test_assert(o_stream_finish(os_2) > 0);
+ if (os_2->stream_errno != 0)
+ i_debug("error: %s", o_stream_get_error(os_2));
+
+ o_stream_unref(&os);
+ o_stream_unref(&os_2);
+ /* this should've been enough */
+
+ struct istream *is = test_istream_create_data(buf->data, buf->used);
+ struct istream *is_2 = i_stream_create_decrypt(is, test_v1_kp.priv);
+
+ /* read should not fail */
+ test_istream_set_allow_eof(is, FALSE);
+ test_istream_set_size(is, 0);
+ size_t offset = 0;
+ ssize_t ret;
+ while ((ret = i_stream_read_data(is_2, &ptr, &siz, 0)) >= 0) {
+ test_assert(ret == 0);
+ if (offset == buf->used)
+ test_istream_set_allow_eof(is, TRUE);
+ else
+ test_istream_set_size(is, ++offset);
+ };
+
+ test_assert(is_2->stream_errno == 0);
+ if (is_2->stream_errno != 0)
+ i_debug("error: %s", i_stream_get_error(is_2));
+ i_stream_unref(&is);
+ i_stream_unref(&is_2);
+ buffer_free(&buf);
+ test_end();
+}
+
+static void test_write_read_v2(void)
+{
+ test_begin("test_write_read_v2");
+ unsigned char payload[IO_BLOCK_SIZE*10];
+ const unsigned char *ptr;
+ size_t pos = 0, siz;
+ random_fill(payload, IO_BLOCK_SIZE*10);
+
+ buffer_t *buf = buffer_create_dynamic(default_pool, sizeof(payload));
+ struct ostream *os = o_stream_create_buffer(buf);
+ struct ostream *os_2 = o_stream_create_encrypt(os,
+ "aes-256-gcm-sha256", test_v1_kp.pub,
+ IO_STREAM_ENC_INTEGRITY_AEAD);
+ o_stream_nsend(os_2, payload, sizeof(payload));
+ test_assert(o_stream_finish(os_2) > 0);
+ if (os_2->stream_errno != 0)
+ i_debug("error: %s", o_stream_get_error(os_2));
+
+ o_stream_unref(&os);
+ o_stream_unref(&os_2);
+
+ struct istream *is = test_istream_create_data(buf->data, buf->used);
+ /* test regression where read fails due to incorrect behaviour
+ when buffer is full before going to decrypt code */
+ i_stream_set_max_buffer_size(is, 8192);
+ test_assert(i_stream_read(is) > 0);
+ struct istream *is_2 = i_stream_create_decrypt(is, test_v1_kp.priv);
+
+ size_t offset = 0;
+ test_istream_set_size(is, 0);
+ test_istream_set_allow_eof(is, FALSE);
+ while(i_stream_read_data(is_2, &ptr, &siz, 0)>=0) {
+ if (offset == buf->used)
+ test_istream_set_allow_eof(is, TRUE);
+ else
+ test_istream_set_size(is, ++offset);
+
+ test_assert_idx(pos + siz <= sizeof(payload), pos);
+ if (pos + siz > sizeof(payload)) break;
+ test_assert_idx(siz == 0 ||
+ memcmp(ptr, payload + pos, siz) == 0, pos);
+ i_stream_skip(is_2, siz); pos += siz;
+ }
+
+ test_assert(is_2->stream_errno == 0);
+ if (is_2->stream_errno != 0)
+ i_debug("error: %s", i_stream_get_error(is_2));
+
+ /* test seeking */
+ for (size_t i = sizeof(payload)-100; i > 100; i -= 100) {
+ i_stream_seek(is_2, i);
+ test_assert_idx(i_stream_read_data(is_2, &ptr, &siz, 0) == 1, i);
+ test_assert_idx(memcmp(ptr, payload + i, siz) == 0, i);
+ }
+ i_stream_seek(is_2, 0);
+ test_assert(i_stream_read_data(is_2, &ptr, &siz, 0) == 1);
+ test_assert(memcmp(ptr, payload, siz) == 0);
+
+ i_stream_unref(&is);
+ i_stream_unref(&is_2);
+ buffer_free(&buf);
+
+ test_end();
+}
+
+static void test_write_read_v2_short(void)
+{
+ test_begin("test_write_read_v2_short");
+ unsigned char payload[1];
+ const unsigned char *ptr;
+ size_t pos = 0, siz;
+ random_fill(payload, 1);
+
+ buffer_t *buf = buffer_create_dynamic(default_pool, 64);
+ struct ostream *os = o_stream_create_buffer(buf);
+ struct ostream *os_2 = o_stream_create_encrypt(os,
+ "aes-256-gcm-sha256", test_v1_kp.pub,
+ IO_STREAM_ENC_INTEGRITY_AEAD);
+ o_stream_nsend(os_2, payload, sizeof(payload));
+ test_assert(o_stream_finish(os_2) > 0);
+ if (os_2->stream_errno != 0)
+ i_debug("error: %s", o_stream_get_error(os_2));
+
+ o_stream_unref(&os);
+ o_stream_unref(&os_2);
+
+ struct istream *is = test_istream_create_data(buf->data, buf->used);
+ struct istream *is_2 = i_stream_create_decrypt(is, test_v1_kp.priv);
+
+ size_t offset = 0;
+ test_istream_set_allow_eof(is, FALSE);
+ test_istream_set_size(is, 0);
+ while(i_stream_read_data(is_2, &ptr, &siz, 0)>=0) {
+ if (offset == buf->used)
+ test_istream_set_allow_eof(is, TRUE);
+ test_istream_set_size(is, ++offset);
+
+ test_assert_idx(pos + siz <= sizeof(payload), pos);
+ if (siz > sizeof(payload) || pos + siz > sizeof(payload))
+ break;
+ test_assert_idx(siz == 0 ||
+ memcmp(ptr, payload + pos, siz) == 0, pos);
+ i_stream_skip(is_2, siz); pos += siz;
+ }
+
+ test_assert(is_2->stream_errno == 0);
+ if (is_2->stream_errno != 0)
+ i_debug("error: %s", i_stream_get_error(is_2));
+
+ i_stream_unref(&is);
+ i_stream_unref(&is_2);
+ buffer_free(&buf);
+
+ test_end();
+}
+
+static void test_write_read_v2_empty(void)
+{
+ const unsigned char *ptr;
+ size_t siz;
+ test_begin("test_write_read_v2_empty");
+ buffer_t *buf = buffer_create_dynamic(default_pool, 64);
+ struct ostream *os = o_stream_create_buffer(buf);
+ struct ostream *os_2 = o_stream_create_encrypt(os,
+ "aes-256-gcm-sha256", test_v1_kp.pub,
+ IO_STREAM_ENC_INTEGRITY_AEAD);
+ test_assert(o_stream_finish(os_2) > 0);
+ if (os_2->stream_errno != 0)
+ i_debug("error: %s", o_stream_get_error(os_2));
+
+ o_stream_unref(&os);
+ o_stream_unref(&os_2);
+ /* this should've been enough */
+
+ struct istream *is = test_istream_create_data(buf->data, buf->used);
+ struct istream *is_2 = i_stream_create_decrypt(is, test_v1_kp.priv);
+
+ /* read should not fail */
+ size_t offset = 0;
+ test_istream_set_allow_eof(is, FALSE);
+ test_istream_set_size(is, 0);
+ ssize_t ret;
+ while ((ret = i_stream_read_data(is_2, &ptr, &siz, 0)) >= 0) {
+ test_assert(ret == 0);
+ if (offset == buf->used)
+ test_istream_set_allow_eof(is, TRUE);
+ test_istream_set_size(is, ++offset);
+ };
+
+ test_assert(is_2->stream_errno == 0);
+ if (is_2->stream_errno != 0)
+ i_debug("error: %s", i_stream_get_error(is_2));
+ i_stream_unref(&is);
+ i_stream_unref(&is_2);
+ buffer_free(&buf);
+ test_end();
+}
+
+static int
+no_op_cb(const char *digest ATTR_UNUSED,
+ struct dcrypt_private_key **priv_key_r ATTR_UNUSED,
+ const char **error_r ATTR_UNUSED,
+ void *context ATTR_UNUSED)
+{
+ return 0;
+}
+
+static void test_read_0_to_400_byte_garbage(void)
+{
+ test_begin("test_read_0_to_100_byte_garbage");
+
+ char data[512];
+ memset(data, 0, sizeof(data));
+
+ for (size_t s = 0; s <= 400; ++s) {
+ struct istream *is = test_istream_create_data(data, s);
+ struct istream *ds = i_stream_create_decrypt_callback(is,
+ no_op_cb, NULL);
+ test_istream_set_size(is, 0);
+ test_istream_set_allow_eof(is, FALSE);
+ ssize_t siz = 0;
+ for (size_t offset = 0; offset <= s && siz == 0; offset++) {
+ if (offset == s)
+ test_istream_set_allow_eof(is, TRUE);
+ test_istream_set_size(is, offset);
+ siz = i_stream_read(ds);
+ }
+ test_assert_idx(siz < 0, s);
+ i_stream_unref(&ds);
+ i_stream_unref(&is);
+ }
+
+ test_end();
+}
+
+static void test_read_large_header(void)
+{
+ test_begin("test_read_large_header");
+
+ struct istream *is =
+ test_istream_create_data(IOSTREAM_CRYPT_MAGIC,
+ sizeof(IOSTREAM_CRYPT_MAGIC));
+ struct istream *ds =
+ i_stream_create_decrypt_callback(is, no_op_cb, NULL);
+ test_istream_set_allow_eof(is, FALSE);
+ test_istream_set_max_buffer_size(is, sizeof(IOSTREAM_CRYPT_MAGIC));
+
+ test_assert(i_stream_read(ds) == -1);
+ i_stream_unref(&ds);
+ i_stream_unref(&is);
+
+ test_end();
+}
+
+static void test_read_increment(void)
+{
+ test_begin("test_read_increment");
+
+ ssize_t amt, total, i;
+
+ struct istream *is_1 = i_stream_create_file(
+ DCRYPT_SRC_DIR"/sample-v2.asc", IO_BLOCK_SIZE);
+ struct istream *is_2 = i_stream_create_base64_decoder(is_1);
+ i_stream_unref(&is_1);
+ struct istream *is_3 = i_stream_create_decrypt(is_2, test_v2_kp.priv);
+ i_stream_unref(&is_2);
+ total = 0;
+ i = 500;
+
+ i_stream_set_max_buffer_size(is_3, i);
+ while((amt = i_stream_read(is_3)) > 0) {
+ total += amt;
+ i_stream_set_max_buffer_size(is_3, ++i);
+ }
+
+ /* make sure snapshotting works: */
+ i_stream_skip(is_3, 1);
+ test_assert(i_stream_read(is_3) == -1);
+
+ test_assert(total == 13534);
+ test_assert(is_3->stream_errno == 0);
+ test_assert(is_3->eof);
+
+ i_stream_unref(&is_3);
+
+ test_end();
+}
+
+static void test_free_keys()
+{
+ dcrypt_key_unref_private(&test_v1_kp.priv);
+ dcrypt_key_unref_public(&test_v1_kp.pub);
+ dcrypt_key_unref_private(&test_v2_kp.priv);
+ dcrypt_key_unref_public(&test_v2_kp.pub);
+}
+
+int main(void)
+{
+ struct dcrypt_settings set = {
+ .module_dir = ".libs"
+ };
+ const char *error;
+
+ if (!dcrypt_initialize(NULL, &set, &error)) {
+ i_error("No functional dcrypt backend found - "
+ "skipping tests: %s", error);
+ return 0;
+ }
+
+ test_assert(dcrypt_key_load_private(&test_v1_kp.priv, key_v1_priv,
+ NULL, NULL, NULL));
+ test_assert(dcrypt_key_load_public(&test_v1_kp.pub, key_v1_pub, NULL));
+ test_assert(dcrypt_key_load_private(&test_v2_kp.priv, key_v2_priv,
+ NULL, NULL, NULL));
+ test_assert(dcrypt_key_load_public(&test_v2_kp.pub, key_v2_pub, NULL));
+
+ static void (*const test_functions[])(void) = {
+ test_static_v1_input,
+ test_static_v1_input_short,
+ test_static_v2_input,
+ test_read_increment,
+ test_write_read_v1,
+ test_write_read_v1_short,
+ test_write_read_v1_empty,
+ test_write_read_v2,
+ test_write_read_v2_short,
+ test_write_read_v2_empty,
+ test_free_keys,
+ test_read_0_to_400_byte_garbage,
+ test_read_large_header,
+ NULL
+ };
+
+ return test_run(test_functions);
+}
diff --git a/src/lib-dict-backend/Makefile.am b/src/lib-dict-backend/Makefile.am
new file mode 100644
index 0000000..80c5050
--- /dev/null
+++ b/src/lib-dict-backend/Makefile.am
@@ -0,0 +1,116 @@
+noinst_LTLIBRARIES = libdict_backend.la
+
+module_dictdir = $(moduledir)/dict
+dict_drivers = @dict_drivers@
+
+AM_CPPFLAGS = \
+ -I$(top_srcdir)/src/lib \
+ -I$(top_srcdir)/src/lib-test \
+ -I$(top_srcdir)/src/lib-dict \
+ -I$(top_srcdir)/src/lib-ldap \
+ -I$(top_srcdir)/src/lib-sql \
+ -I$(top_srcdir)/src/lib-settings \
+ $(SQL_CFLAGS)
+
+NOPLUGIN_LDFLAGS =
+
+ldap_sources = \
+ dict-ldap.c \
+ dict-ldap-settings.c
+
+libdict_backend_la_SOURCES = \
+ dict-cdb.c \
+ dict-sql.c \
+ dict-sql-settings.c \
+ $(ldap_sources)
+libdict_backend_la_LIBADD =
+
+nodist_libdict_backend_la_SOURCES = \
+ dict-drivers-register.c
+
+noinst_HEADERS = \
+ dict-ldap-settings.h \
+ dict-sql.h \
+ dict-sql-private.h \
+ dict-sql-settings.h
+
+if LDAP_PLUGIN
+LIBDICT_LDAP = libdict_ldap.la
+libdict_ldap_la_DEPENDENCIES = $(LIBDOVECOT_LDAP) $(LIBDOVECOT_DEPS)
+libdict_ldap_la_LDFLAGS = -module -avoid-version
+libdict_ldap_la_LIBADD = $(LIBDOVECOT_LDAP) $(LIBDOVECOT)
+libdict_ldap_la_CPPFLAGS = $(AM_CPPFLAGS) -DPLUGIN_BUILD
+libdict_ldap_la_SOURCES = $(ldap_sources)
+else
+if HAVE_LDAP
+libdict_backend_la_LIBADD += $(LIBDOVECOT_LDAP)
+dict_drivers += ldap
+endif
+endif
+
+module_dict_LTLIBRARIES = \
+ $(LIBDICT_LDAP)
+
+EXTRA_DIST = dict.conf
+
+dict-drivers-register.c: Makefile $(top_builddir)/config.h
+ rm -f $@
+ echo '/* this file automatically generated by Makefile */' >$@
+ echo '#include "lib.h"' >>$@
+ echo '#include "dict.h"' >>$@
+ echo '#include "ldap-client.h"' >>$@
+ echo '#include "dict-sql.h"' >>$@
+ for i in $(dict_drivers) null; do \
+ if [ "$${i}" != "null" ]; then \
+ echo "extern struct dict dict_driver_$${i};" >>$@ ; \
+ fi; \
+ done
+ echo 'void dict_drivers_register_all(void) {' >>$@
+ echo 'dict_drivers_register_builtin();' >>$@
+ echo 'dict_sql_register();' >>$@
+ for i in $(dict_drivers) null; do \
+ if [ "$${i}" != "null" ]; then \
+ echo "dict_driver_register(&dict_driver_$${i});" >>$@ ; \
+ fi; \
+ done
+ echo '}' >>$@
+ echo 'void dict_drivers_unregister_all(void) {' >>$@
+ echo '#ifdef BUILTIN_LDAP' >>$@
+ echo 'ldap_clients_cleanup();' >>$@
+ echo '#endif' >>$@
+ echo 'dict_drivers_unregister_builtin();' >>$@
+ echo 'dict_sql_unregister();' >>$@
+ for i in $(dict_drivers) null; do \
+ if [ "$${i}" != "null" ]; then \
+ echo "dict_driver_unregister(&dict_driver_$${i});" >>$@ ; \
+ fi; \
+ done
+ echo '}' >>$@
+
+distclean-generic:
+ rm -f Makefile dict-drivers-register.c
+
+test_programs = \
+ test-dict-sql
+
+noinst_PROGRAMS = $(test_programs)
+
+test_dict_sql_CFLAGS = $(AM_CPPFLAGS) -DDICT_SRC_DIR=\"$(top_srcdir)/src/lib-dict-backend\"
+test_dict_sql_SOURCES = \
+ test-dict-sql.c
+test_dict_sql_LDADD = \
+ $(noinst_LTLIBRARIES) \
+ $(DICT_LIBS) \
+ ../lib-sql/libdriver_test.la \
+ ../lib-sql/libsql.la \
+ ../lib-dovecot/libdovecot.la
+test_dict_sql_DEPENDENCIES = \
+ $(noinst_LTLIBRARIES) \
+ ../lib-sql/libdriver_test.la \
+ ../lib-sql/libsql.la \
+ ../lib-dovecot/libdovecot.la
+
+check-local:
+ for bin in $(test_programs) $(check_PROGRAMS); do \
+ if ! $(RUN_TEST) ./$$bin; then exit 1; fi; \
+ done
diff --git a/src/lib-dict-backend/Makefile.in b/src/lib-dict-backend/Makefile.in
new file mode 100644
index 0000000..e5162e9
--- /dev/null
+++ b/src/lib-dict-backend/Makefile.in
@@ -0,0 +1,1016 @@
+# Makefile.in generated by automake 1.16.1 from Makefile.am.
+# @configure_input@
+
+# Copyright (C) 1994-2018 Free Software Foundation, Inc.
+
+# This Makefile.in is free software; the Free Software Foundation
+# gives unlimited permission to copy and/or distribute it,
+# with or without modifications, as long as this notice is preserved.
+
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY, to the extent permitted by law; without
+# even the implied warranty of MERCHANTABILITY or FITNESS FOR A
+# PARTICULAR PURPOSE.
+
+@SET_MAKE@
+
+
+
+VPATH = @srcdir@
+am__is_gnu_make = { \
+ if test -z '$(MAKELEVEL)'; then \
+ false; \
+ elif test -n '$(MAKE_HOST)'; then \
+ true; \
+ elif test -n '$(MAKE_VERSION)' && test -n '$(CURDIR)'; then \
+ true; \
+ else \
+ false; \
+ fi; \
+}
+am__make_running_with_option = \
+ case $${target_option-} in \
+ ?) ;; \
+ *) echo "am__make_running_with_option: internal error: invalid" \
+ "target option '$${target_option-}' specified" >&2; \
+ exit 1;; \
+ esac; \
+ has_opt=no; \
+ sane_makeflags=$$MAKEFLAGS; \
+ if $(am__is_gnu_make); then \
+ sane_makeflags=$$MFLAGS; \
+ else \
+ case $$MAKEFLAGS in \
+ *\\[\ \ ]*) \
+ bs=\\; \
+ sane_makeflags=`printf '%s\n' "$$MAKEFLAGS" \
+ | sed "s/$$bs$$bs[$$bs $$bs ]*//g"`;; \
+ esac; \
+ fi; \
+ skip_next=no; \
+ strip_trailopt () \
+ { \
+ flg=`printf '%s\n' "$$flg" | sed "s/$$1.*$$//"`; \
+ }; \
+ for flg in $$sane_makeflags; do \
+ test $$skip_next = yes && { skip_next=no; continue; }; \
+ case $$flg in \
+ *=*|--*) continue;; \
+ -*I) strip_trailopt 'I'; skip_next=yes;; \
+ -*I?*) strip_trailopt 'I';; \
+ -*O) strip_trailopt 'O'; skip_next=yes;; \
+ -*O?*) strip_trailopt 'O';; \
+ -*l) strip_trailopt 'l'; skip_next=yes;; \
+ -*l?*) strip_trailopt 'l';; \
+ -[dEDm]) skip_next=yes;; \
+ -[JT]) skip_next=yes;; \
+ esac; \
+ case $$flg in \
+ *$$target_option*) has_opt=yes; break;; \
+ esac; \
+ done; \
+ test $$has_opt = yes
+am__make_dryrun = (target_option=n; $(am__make_running_with_option))
+am__make_keepgoing = (target_option=k; $(am__make_running_with_option))
+pkgdatadir = $(datadir)/@PACKAGE@
+pkgincludedir = $(includedir)/@PACKAGE@
+pkglibdir = $(libdir)/@PACKAGE@
+pkglibexecdir = $(libexecdir)/@PACKAGE@
+am__cd = CDPATH="$${ZSH_VERSION+.}$(PATH_SEPARATOR)" && cd
+install_sh_DATA = $(install_sh) -c -m 644
+install_sh_PROGRAM = $(install_sh) -c
+install_sh_SCRIPT = $(install_sh) -c
+INSTALL_HEADER = $(INSTALL_DATA)
+transform = $(program_transform_name)
+NORMAL_INSTALL = :
+PRE_INSTALL = :
+POST_INSTALL = :
+NORMAL_UNINSTALL = :
+PRE_UNINSTALL = :
+POST_UNINSTALL = :
+build_triplet = @build@
+host_triplet = @host@
+@HAVE_LDAP_TRUE@@LDAP_PLUGIN_FALSE@am__append_1 = $(LIBDOVECOT_LDAP)
+@HAVE_LDAP_TRUE@@LDAP_PLUGIN_FALSE@am__append_2 = ldap
+noinst_PROGRAMS = $(am__EXEEXT_1)
+subdir = src/lib-dict-backend
+ACLOCAL_M4 = $(top_srcdir)/aclocal.m4
+am__aclocal_m4_deps = $(top_srcdir)/m4/ac_checktype2.m4 \
+ $(top_srcdir)/m4/ac_typeof.m4 $(top_srcdir)/m4/arc4random.m4 \
+ $(top_srcdir)/m4/blockdev.m4 $(top_srcdir)/m4/c99_vsnprintf.m4 \
+ $(top_srcdir)/m4/clock_gettime.m4 $(top_srcdir)/m4/crypt.m4 \
+ $(top_srcdir)/m4/crypt_xpg6.m4 $(top_srcdir)/m4/dbqlk.m4 \
+ $(top_srcdir)/m4/dirent_dtype.m4 $(top_srcdir)/m4/dovecot.m4 \
+ $(top_srcdir)/m4/fd_passing.m4 $(top_srcdir)/m4/fdatasync.m4 \
+ $(top_srcdir)/m4/flexible_array_member.m4 \
+ $(top_srcdir)/m4/glibc.m4 $(top_srcdir)/m4/gmtime_max.m4 \
+ $(top_srcdir)/m4/gmtime_tm_gmtoff.m4 \
+ $(top_srcdir)/m4/ioloop.m4 $(top_srcdir)/m4/iovec.m4 \
+ $(top_srcdir)/m4/ipv6.m4 $(top_srcdir)/m4/libcap.m4 \
+ $(top_srcdir)/m4/libtool.m4 $(top_srcdir)/m4/libwrap.m4 \
+ $(top_srcdir)/m4/linux_mremap.m4 $(top_srcdir)/m4/ltoptions.m4 \
+ $(top_srcdir)/m4/ltsugar.m4 $(top_srcdir)/m4/ltversion.m4 \
+ $(top_srcdir)/m4/lt~obsolete.m4 $(top_srcdir)/m4/mmap_write.m4 \
+ $(top_srcdir)/m4/mntctl.m4 $(top_srcdir)/m4/modules.m4 \
+ $(top_srcdir)/m4/notify.m4 $(top_srcdir)/m4/nsl.m4 \
+ $(top_srcdir)/m4/off_t_max.m4 $(top_srcdir)/m4/pkg.m4 \
+ $(top_srcdir)/m4/pr_set_dumpable.m4 \
+ $(top_srcdir)/m4/q_quotactl.m4 $(top_srcdir)/m4/quota.m4 \
+ $(top_srcdir)/m4/random.m4 $(top_srcdir)/m4/rlimit.m4 \
+ $(top_srcdir)/m4/sendfile.m4 $(top_srcdir)/m4/size_t_signed.m4 \
+ $(top_srcdir)/m4/sockpeercred.m4 $(top_srcdir)/m4/sql.m4 \
+ $(top_srcdir)/m4/ssl.m4 $(top_srcdir)/m4/st_tim.m4 \
+ $(top_srcdir)/m4/static_array.m4 $(top_srcdir)/m4/test_with.m4 \
+ $(top_srcdir)/m4/time_t.m4 $(top_srcdir)/m4/typeof.m4 \
+ $(top_srcdir)/m4/typeof_dev_t.m4 \
+ $(top_srcdir)/m4/uoff_t_max.m4 $(top_srcdir)/m4/vararg.m4 \
+ $(top_srcdir)/m4/want_apparmor.m4 \
+ $(top_srcdir)/m4/want_bsdauth.m4 \
+ $(top_srcdir)/m4/want_bzlib.m4 \
+ $(top_srcdir)/m4/want_cassandra.m4 \
+ $(top_srcdir)/m4/want_cdb.m4 \
+ $(top_srcdir)/m4/want_checkpassword.m4 \
+ $(top_srcdir)/m4/want_clucene.m4 $(top_srcdir)/m4/want_db.m4 \
+ $(top_srcdir)/m4/want_gssapi.m4 $(top_srcdir)/m4/want_icu.m4 \
+ $(top_srcdir)/m4/want_ldap.m4 $(top_srcdir)/m4/want_lua.m4 \
+ $(top_srcdir)/m4/want_lz4.m4 $(top_srcdir)/m4/want_lzma.m4 \
+ $(top_srcdir)/m4/want_mysql.m4 $(top_srcdir)/m4/want_pam.m4 \
+ $(top_srcdir)/m4/want_passwd.m4 $(top_srcdir)/m4/want_pgsql.m4 \
+ $(top_srcdir)/m4/want_prefetch.m4 \
+ $(top_srcdir)/m4/want_shadow.m4 \
+ $(top_srcdir)/m4/want_sodium.m4 $(top_srcdir)/m4/want_solr.m4 \
+ $(top_srcdir)/m4/want_sqlite.m4 \
+ $(top_srcdir)/m4/want_stemmer.m4 \
+ $(top_srcdir)/m4/want_systemd.m4 \
+ $(top_srcdir)/m4/want_textcat.m4 \
+ $(top_srcdir)/m4/want_unwind.m4 $(top_srcdir)/m4/want_zlib.m4 \
+ $(top_srcdir)/m4/want_zstd.m4 $(top_srcdir)/configure.ac
+am__configure_deps = $(am__aclocal_m4_deps) $(CONFIGURE_DEPENDENCIES) \
+ $(ACLOCAL_M4)
+DIST_COMMON = $(srcdir)/Makefile.am $(noinst_HEADERS) \
+ $(am__DIST_COMMON)
+mkinstalldirs = $(install_sh) -d
+CONFIG_HEADER = $(top_builddir)/config.h
+CONFIG_CLEAN_FILES =
+CONFIG_CLEAN_VPATH_FILES =
+am__EXEEXT_1 = test-dict-sql$(EXEEXT)
+PROGRAMS = $(noinst_PROGRAMS)
+am__vpath_adj_setup = srcdirstrip=`echo "$(srcdir)" | sed 's|.|.|g'`;
+am__vpath_adj = case $$p in \
+ $(srcdir)/*) f=`echo "$$p" | sed "s|^$$srcdirstrip/||"`;; \
+ *) f=$$p;; \
+ esac;
+am__strip_dir = f=`echo $$p | sed -e 's|^.*/||'`;
+am__install_max = 40
+am__nobase_strip_setup = \
+ srcdirstrip=`echo "$(srcdir)" | sed 's/[].[^$$\\*|]/\\\\&/g'`
+am__nobase_strip = \
+ for p in $$list; do echo "$$p"; done | sed -e "s|$$srcdirstrip/||"
+am__nobase_list = $(am__nobase_strip_setup); \
+ for p in $$list; do echo "$$p $$p"; done | \
+ sed "s| $$srcdirstrip/| |;"' / .*\//!s/ .*/ ./; s,\( .*\)/[^/]*$$,\1,' | \
+ $(AWK) 'BEGIN { files["."] = "" } { files[$$2] = files[$$2] " " $$1; \
+ if (++n[$$2] == $(am__install_max)) \
+ { print $$2, files[$$2]; n[$$2] = 0; files[$$2] = "" } } \
+ END { for (dir in files) print dir, files[dir] }'
+am__base_list = \
+ sed '$$!N;$$!N;$$!N;$$!N;$$!N;$$!N;$$!N;s/\n/ /g' | \
+ sed '$$!N;$$!N;$$!N;$$!N;s/\n/ /g'
+am__uninstall_files_from_dir = { \
+ test -z "$$files" \
+ || { test ! -d "$$dir" && test ! -f "$$dir" && test ! -r "$$dir"; } \
+ || { echo " ( cd '$$dir' && rm -f" $$files ")"; \
+ $(am__cd) "$$dir" && rm -f $$files; }; \
+ }
+am__installdirs = "$(DESTDIR)$(module_dictdir)"
+LTLIBRARIES = $(module_dict_LTLIBRARIES) $(noinst_LTLIBRARIES)
+am__DEPENDENCIES_1 =
+@HAVE_LDAP_TRUE@@LDAP_PLUGIN_FALSE@am__DEPENDENCIES_2 = \
+@HAVE_LDAP_TRUE@@LDAP_PLUGIN_FALSE@ $(am__DEPENDENCIES_1)
+libdict_backend_la_DEPENDENCIES = $(am__DEPENDENCIES_2)
+am__objects_1 = dict-ldap.lo dict-ldap-settings.lo
+am_libdict_backend_la_OBJECTS = dict-cdb.lo dict-sql.lo \
+ dict-sql-settings.lo $(am__objects_1)
+nodist_libdict_backend_la_OBJECTS = dict-drivers-register.lo
+libdict_backend_la_OBJECTS = $(am_libdict_backend_la_OBJECTS) \
+ $(nodist_libdict_backend_la_OBJECTS)
+AM_V_lt = $(am__v_lt_@AM_V@)
+am__v_lt_ = $(am__v_lt_@AM_DEFAULT_V@)
+am__v_lt_0 = --silent
+am__v_lt_1 =
+am__libdict_ldap_la_SOURCES_DIST = dict-ldap.c dict-ldap-settings.c
+am__objects_2 = libdict_ldap_la-dict-ldap.lo \
+ libdict_ldap_la-dict-ldap-settings.lo
+@LDAP_PLUGIN_TRUE@am_libdict_ldap_la_OBJECTS = $(am__objects_2)
+libdict_ldap_la_OBJECTS = $(am_libdict_ldap_la_OBJECTS)
+libdict_ldap_la_LINK = $(LIBTOOL) $(AM_V_lt) --tag=CC \
+ $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=link $(CCLD) \
+ $(AM_CFLAGS) $(CFLAGS) $(libdict_ldap_la_LDFLAGS) $(LDFLAGS) \
+ -o $@
+@LDAP_PLUGIN_TRUE@am_libdict_ldap_la_rpath = -rpath $(module_dictdir)
+am_test_dict_sql_OBJECTS = test_dict_sql-test-dict-sql.$(OBJEXT)
+test_dict_sql_OBJECTS = $(am_test_dict_sql_OBJECTS)
+test_dict_sql_LINK = $(LIBTOOL) $(AM_V_lt) --tag=CC $(AM_LIBTOOLFLAGS) \
+ $(LIBTOOLFLAGS) --mode=link $(CCLD) $(test_dict_sql_CFLAGS) \
+ $(CFLAGS) $(AM_LDFLAGS) $(LDFLAGS) -o $@
+AM_V_P = $(am__v_P_@AM_V@)
+am__v_P_ = $(am__v_P_@AM_DEFAULT_V@)
+am__v_P_0 = false
+am__v_P_1 = :
+AM_V_GEN = $(am__v_GEN_@AM_V@)
+am__v_GEN_ = $(am__v_GEN_@AM_DEFAULT_V@)
+am__v_GEN_0 = @echo " GEN " $@;
+am__v_GEN_1 =
+AM_V_at = $(am__v_at_@AM_V@)
+am__v_at_ = $(am__v_at_@AM_DEFAULT_V@)
+am__v_at_0 = @
+am__v_at_1 =
+DEFAULT_INCLUDES = -I.@am__isrc@ -I$(top_builddir)
+depcomp = $(SHELL) $(top_srcdir)/depcomp
+am__maybe_remake_depfiles = depfiles
+am__depfiles_remade = ./$(DEPDIR)/dict-cdb.Plo \
+ ./$(DEPDIR)/dict-drivers-register.Plo \
+ ./$(DEPDIR)/dict-ldap-settings.Plo ./$(DEPDIR)/dict-ldap.Plo \
+ ./$(DEPDIR)/dict-sql-settings.Plo ./$(DEPDIR)/dict-sql.Plo \
+ ./$(DEPDIR)/libdict_ldap_la-dict-ldap-settings.Plo \
+ ./$(DEPDIR)/libdict_ldap_la-dict-ldap.Plo \
+ ./$(DEPDIR)/test_dict_sql-test-dict-sql.Po
+am__mv = mv -f
+COMPILE = $(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) \
+ $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS)
+LTCOMPILE = $(LIBTOOL) $(AM_V_lt) --tag=CC $(AM_LIBTOOLFLAGS) \
+ $(LIBTOOLFLAGS) --mode=compile $(CC) $(DEFS) \
+ $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) $(CPPFLAGS) \
+ $(AM_CFLAGS) $(CFLAGS)
+AM_V_CC = $(am__v_CC_@AM_V@)
+am__v_CC_ = $(am__v_CC_@AM_DEFAULT_V@)
+am__v_CC_0 = @echo " CC " $@;
+am__v_CC_1 =
+CCLD = $(CC)
+LINK = $(LIBTOOL) $(AM_V_lt) --tag=CC $(AM_LIBTOOLFLAGS) \
+ $(LIBTOOLFLAGS) --mode=link $(CCLD) $(AM_CFLAGS) $(CFLAGS) \
+ $(AM_LDFLAGS) $(LDFLAGS) -o $@
+AM_V_CCLD = $(am__v_CCLD_@AM_V@)
+am__v_CCLD_ = $(am__v_CCLD_@AM_DEFAULT_V@)
+am__v_CCLD_0 = @echo " CCLD " $@;
+am__v_CCLD_1 =
+SOURCES = $(libdict_backend_la_SOURCES) \
+ $(nodist_libdict_backend_la_SOURCES) \
+ $(libdict_ldap_la_SOURCES) $(test_dict_sql_SOURCES)
+DIST_SOURCES = $(libdict_backend_la_SOURCES) \
+ $(am__libdict_ldap_la_SOURCES_DIST) $(test_dict_sql_SOURCES)
+am__can_run_installinfo = \
+ case $$AM_UPDATE_INFO_DIR in \
+ n|no|NO) false;; \
+ *) (install-info --version) >/dev/null 2>&1;; \
+ esac
+HEADERS = $(noinst_HEADERS)
+am__tagged_files = $(HEADERS) $(SOURCES) $(TAGS_FILES) $(LISP)
+# Read a list of newline-separated strings from the standard input,
+# and print each of them once, without duplicates. Input order is
+# *not* preserved.
+am__uniquify_input = $(AWK) '\
+ BEGIN { nonempty = 0; } \
+ { items[$$0] = 1; nonempty = 1; } \
+ END { if (nonempty) { for (i in items) print i; }; } \
+'
+# Make sure the list of sources is unique. This is necessary because,
+# e.g., the same source file might be shared among _SOURCES variables
+# for different programs/libraries.
+am__define_uniq_tagged_files = \
+ list='$(am__tagged_files)'; \
+ unique=`for i in $$list; do \
+ if test -f "$$i"; then echo $$i; else echo $(srcdir)/$$i; fi; \
+ done | $(am__uniquify_input)`
+ETAGS = etags
+CTAGS = ctags
+am__DIST_COMMON = $(srcdir)/Makefile.in $(top_srcdir)/depcomp
+DISTFILES = $(DIST_COMMON) $(DIST_SOURCES) $(TEXINFOS) $(EXTRA_DIST)
+ACLOCAL = @ACLOCAL@
+ACLOCAL_AMFLAGS = @ACLOCAL_AMFLAGS@
+AMTAR = @AMTAR@
+AM_DEFAULT_VERBOSITY = @AM_DEFAULT_VERBOSITY@
+APPARMOR_LIBS = @APPARMOR_LIBS@
+AR = @AR@
+AUTH_CFLAGS = @AUTH_CFLAGS@
+AUTH_LIBS = @AUTH_LIBS@
+AUTOCONF = @AUTOCONF@
+AUTOHEADER = @AUTOHEADER@
+AUTOMAKE = @AUTOMAKE@
+AWK = @AWK@
+BINARY_CFLAGS = @BINARY_CFLAGS@
+BINARY_LDFLAGS = @BINARY_LDFLAGS@
+BISON = @BISON@
+CASSANDRA_CFLAGS = @CASSANDRA_CFLAGS@
+CASSANDRA_LIBS = @CASSANDRA_LIBS@
+CC = @CC@
+CCDEPMODE = @CCDEPMODE@
+CDB_LIBS = @CDB_LIBS@
+CFLAGS = @CFLAGS@
+CLUCENE_CFLAGS = @CLUCENE_CFLAGS@
+CLUCENE_LIBS = @CLUCENE_LIBS@
+COMPRESS_LIBS = @COMPRESS_LIBS@
+CPP = @CPP@
+CPPFLAGS = @CPPFLAGS@
+CRYPT_LIBS = @CRYPT_LIBS@
+CXX = @CXX@
+CXXCPP = @CXXCPP@
+CXXDEPMODE = @CXXDEPMODE@
+CXXFLAGS = @CXXFLAGS@
+CYGPATH_W = @CYGPATH_W@
+DEFS = @DEFS@
+DEPDIR = @DEPDIR@
+DICT_LIBS = @DICT_LIBS@
+DLLIB = @DLLIB@
+DLLTOOL = @DLLTOOL@
+DSYMUTIL = @DSYMUTIL@
+DUMPBIN = @DUMPBIN@
+ECHO_C = @ECHO_C@
+ECHO_N = @ECHO_N@
+ECHO_T = @ECHO_T@
+EGREP = @EGREP@
+EXEEXT = @EXEEXT@
+FGREP = @FGREP@
+FLEX = @FLEX@
+FUZZER_CPPFLAGS = @FUZZER_CPPFLAGS@
+FUZZER_LDFLAGS = @FUZZER_LDFLAGS@
+GREP = @GREP@
+INSTALL = @INSTALL@
+INSTALL_DATA = @INSTALL_DATA@
+INSTALL_PROGRAM = @INSTALL_PROGRAM@
+INSTALL_SCRIPT = @INSTALL_SCRIPT@
+INSTALL_STRIP_PROGRAM = @INSTALL_STRIP_PROGRAM@
+KRB5CONFIG = @KRB5CONFIG@
+KRB5_CFLAGS = @KRB5_CFLAGS@
+KRB5_LIBS = @KRB5_LIBS@
+LD = @LD@
+LDAP_LIBS = @LDAP_LIBS@
+LDFLAGS = @LDFLAGS@
+LD_NO_WHOLE_ARCHIVE = @LD_NO_WHOLE_ARCHIVE@
+LD_WHOLE_ARCHIVE = @LD_WHOLE_ARCHIVE@
+LIBCAP = @LIBCAP@
+LIBDOVECOT = @LIBDOVECOT@
+LIBDOVECOT_COMPRESS = @LIBDOVECOT_COMPRESS@
+LIBDOVECOT_DEPS = @LIBDOVECOT_DEPS@
+LIBDOVECOT_DSYNC = @LIBDOVECOT_DSYNC@
+LIBDOVECOT_LA_LIBS = @LIBDOVECOT_LA_LIBS@
+LIBDOVECOT_LDA = @LIBDOVECOT_LDA@
+LIBDOVECOT_LDAP = @LIBDOVECOT_LDAP@
+LIBDOVECOT_LIBFTS = @LIBDOVECOT_LIBFTS@
+LIBDOVECOT_LIBFTS_DEPS = @LIBDOVECOT_LIBFTS_DEPS@
+LIBDOVECOT_LOGIN = @LIBDOVECOT_LOGIN@
+LIBDOVECOT_LUA = @LIBDOVECOT_LUA@
+LIBDOVECOT_LUA_DEPS = @LIBDOVECOT_LUA_DEPS@
+LIBDOVECOT_SQL = @LIBDOVECOT_SQL@
+LIBDOVECOT_STORAGE = @LIBDOVECOT_STORAGE@
+LIBDOVECOT_STORAGE_DEPS = @LIBDOVECOT_STORAGE_DEPS@
+LIBEXTTEXTCAT_CFLAGS = @LIBEXTTEXTCAT_CFLAGS@
+LIBEXTTEXTCAT_LIBS = @LIBEXTTEXTCAT_LIBS@
+LIBICONV = @LIBICONV@
+LIBICU_CFLAGS = @LIBICU_CFLAGS@
+LIBICU_LIBS = @LIBICU_LIBS@
+LIBOBJS = @LIBOBJS@
+LIBS = @LIBS@
+LIBSODIUM_CFLAGS = @LIBSODIUM_CFLAGS@
+LIBSODIUM_LIBS = @LIBSODIUM_LIBS@
+LIBTIRPC_CFLAGS = @LIBTIRPC_CFLAGS@
+LIBTIRPC_LIBS = @LIBTIRPC_LIBS@
+LIBTOOL = @LIBTOOL@
+LIBUNWIND_CFLAGS = @LIBUNWIND_CFLAGS@
+LIBUNWIND_LIBS = @LIBUNWIND_LIBS@
+LIBWRAP_LIBS = @LIBWRAP_LIBS@
+LINKED_STORAGE_LDADD = @LINKED_STORAGE_LDADD@
+LIPO = @LIPO@
+LN_S = @LN_S@
+LTLIBICONV = @LTLIBICONV@
+LTLIBOBJS = @LTLIBOBJS@
+LT_SYS_LIBRARY_PATH = @LT_SYS_LIBRARY_PATH@
+LUA_CFLAGS = @LUA_CFLAGS@
+LUA_LIBS = @LUA_LIBS@
+MAINT = @MAINT@
+MAKEINFO = @MAKEINFO@
+MANIFEST_TOOL = @MANIFEST_TOOL@
+MKDIR_P = @MKDIR_P@
+MODULE_LIBS = @MODULE_LIBS@
+MODULE_SUFFIX = @MODULE_SUFFIX@
+MYSQL_CFLAGS = @MYSQL_CFLAGS@
+MYSQL_CONFIG = @MYSQL_CONFIG@
+MYSQL_LIBS = @MYSQL_LIBS@
+NM = @NM@
+NMEDIT = @NMEDIT@
+NOPLUGIN_LDFLAGS =
+OBJDUMP = @OBJDUMP@
+OBJEXT = @OBJEXT@
+OTOOL = @OTOOL@
+OTOOL64 = @OTOOL64@
+PACKAGE = @PACKAGE@
+PACKAGE_BUGREPORT = @PACKAGE_BUGREPORT@
+PACKAGE_NAME = @PACKAGE_NAME@
+PACKAGE_STRING = @PACKAGE_STRING@
+PACKAGE_TARNAME = @PACKAGE_TARNAME@
+PACKAGE_URL = @PACKAGE_URL@
+PACKAGE_VERSION = @PACKAGE_VERSION@
+PANDOC = @PANDOC@
+PATH_SEPARATOR = @PATH_SEPARATOR@
+PGSQL_CFLAGS = @PGSQL_CFLAGS@
+PGSQL_LIBS = @PGSQL_LIBS@
+PG_CONFIG = @PG_CONFIG@
+PIE_CFLAGS = @PIE_CFLAGS@
+PIE_LDFLAGS = @PIE_LDFLAGS@
+PKG_CONFIG = @PKG_CONFIG@
+PKG_CONFIG_LIBDIR = @PKG_CONFIG_LIBDIR@
+PKG_CONFIG_PATH = @PKG_CONFIG_PATH@
+QUOTA_LIBS = @QUOTA_LIBS@
+RANLIB = @RANLIB@
+RELRO_LDFLAGS = @RELRO_LDFLAGS@
+RPCGEN = @RPCGEN@
+RUN_TEST = @RUN_TEST@
+SED = @SED@
+SETTING_FILES = @SETTING_FILES@
+SET_MAKE = @SET_MAKE@
+SHELL = @SHELL@
+SQLITE_CFLAGS = @SQLITE_CFLAGS@
+SQLITE_LIBS = @SQLITE_LIBS@
+SQL_CFLAGS = @SQL_CFLAGS@
+SQL_LIBS = @SQL_LIBS@
+SSL_CFLAGS = @SSL_CFLAGS@
+SSL_LIBS = @SSL_LIBS@
+STRIP = @STRIP@
+SYSTEMD_CFLAGS = @SYSTEMD_CFLAGS@
+SYSTEMD_LIBS = @SYSTEMD_LIBS@
+VALGRIND = @VALGRIND@
+VERSION = @VERSION@
+ZSTD_CFLAGS = @ZSTD_CFLAGS@
+ZSTD_LIBS = @ZSTD_LIBS@
+abs_builddir = @abs_builddir@
+abs_srcdir = @abs_srcdir@
+abs_top_builddir = @abs_top_builddir@
+abs_top_srcdir = @abs_top_srcdir@
+ac_ct_AR = @ac_ct_AR@
+ac_ct_CC = @ac_ct_CC@
+ac_ct_CXX = @ac_ct_CXX@
+ac_ct_DUMPBIN = @ac_ct_DUMPBIN@
+am__include = @am__include@
+am__leading_dot = @am__leading_dot@
+am__quote = @am__quote@
+am__tar = @am__tar@
+am__untar = @am__untar@
+bindir = @bindir@
+build = @build@
+build_alias = @build_alias@
+build_cpu = @build_cpu@
+build_os = @build_os@
+build_vendor = @build_vendor@
+builddir = @builddir@
+datadir = @datadir@
+datarootdir = @datarootdir@
+dict_drivers = @dict_drivers@ $(am__append_2)
+docdir = @docdir@
+dvidir = @dvidir@
+exec_prefix = @exec_prefix@
+host = @host@
+host_alias = @host_alias@
+host_cpu = @host_cpu@
+host_os = @host_os@
+host_vendor = @host_vendor@
+htmldir = @htmldir@
+includedir = @includedir@
+infodir = @infodir@
+install_sh = @install_sh@
+libdir = @libdir@
+libexecdir = @libexecdir@
+localedir = @localedir@
+localstatedir = @localstatedir@
+mandir = @mandir@
+mkdir_p = @mkdir_p@
+moduledir = @moduledir@
+oldincludedir = @oldincludedir@
+pdfdir = @pdfdir@
+prefix = @prefix@
+program_transform_name = @program_transform_name@
+psdir = @psdir@
+rundir = @rundir@
+runstatedir = @runstatedir@
+sbindir = @sbindir@
+sharedstatedir = @sharedstatedir@
+sql_drivers = @sql_drivers@
+srcdir = @srcdir@
+ssldir = @ssldir@
+statedir = @statedir@
+sysconfdir = @sysconfdir@
+systemdservicetype = @systemdservicetype@
+systemdsystemunitdir = @systemdsystemunitdir@
+target_alias = @target_alias@
+top_build_prefix = @top_build_prefix@
+top_builddir = @top_builddir@
+top_srcdir = @top_srcdir@
+noinst_LTLIBRARIES = libdict_backend.la
+module_dictdir = $(moduledir)/dict
+AM_CPPFLAGS = \
+ -I$(top_srcdir)/src/lib \
+ -I$(top_srcdir)/src/lib-test \
+ -I$(top_srcdir)/src/lib-dict \
+ -I$(top_srcdir)/src/lib-ldap \
+ -I$(top_srcdir)/src/lib-sql \
+ -I$(top_srcdir)/src/lib-settings \
+ $(SQL_CFLAGS)
+
+ldap_sources = \
+ dict-ldap.c \
+ dict-ldap-settings.c
+
+libdict_backend_la_SOURCES = \
+ dict-cdb.c \
+ dict-sql.c \
+ dict-sql-settings.c \
+ $(ldap_sources)
+
+libdict_backend_la_LIBADD = $(am__append_1)
+nodist_libdict_backend_la_SOURCES = \
+ dict-drivers-register.c
+
+noinst_HEADERS = \
+ dict-ldap-settings.h \
+ dict-sql.h \
+ dict-sql-private.h \
+ dict-sql-settings.h
+
+@LDAP_PLUGIN_TRUE@LIBDICT_LDAP = libdict_ldap.la
+@LDAP_PLUGIN_TRUE@libdict_ldap_la_DEPENDENCIES = $(LIBDOVECOT_LDAP) $(LIBDOVECOT_DEPS)
+@LDAP_PLUGIN_TRUE@libdict_ldap_la_LDFLAGS = -module -avoid-version
+@LDAP_PLUGIN_TRUE@libdict_ldap_la_LIBADD = $(LIBDOVECOT_LDAP) $(LIBDOVECOT)
+@LDAP_PLUGIN_TRUE@libdict_ldap_la_CPPFLAGS = $(AM_CPPFLAGS) -DPLUGIN_BUILD
+@LDAP_PLUGIN_TRUE@libdict_ldap_la_SOURCES = $(ldap_sources)
+module_dict_LTLIBRARIES = \
+ $(LIBDICT_LDAP)
+
+EXTRA_DIST = dict.conf
+test_programs = \
+ test-dict-sql
+
+test_dict_sql_CFLAGS = $(AM_CPPFLAGS) -DDICT_SRC_DIR=\"$(top_srcdir)/src/lib-dict-backend\"
+test_dict_sql_SOURCES = \
+ test-dict-sql.c
+
+test_dict_sql_LDADD = \
+ $(noinst_LTLIBRARIES) \
+ $(DICT_LIBS) \
+ ../lib-sql/libdriver_test.la \
+ ../lib-sql/libsql.la \
+ ../lib-dovecot/libdovecot.la
+
+test_dict_sql_DEPENDENCIES = \
+ $(noinst_LTLIBRARIES) \
+ ../lib-sql/libdriver_test.la \
+ ../lib-sql/libsql.la \
+ ../lib-dovecot/libdovecot.la
+
+all: all-am
+
+.SUFFIXES:
+.SUFFIXES: .c .lo .o .obj
+$(srcdir)/Makefile.in: @MAINTAINER_MODE_TRUE@ $(srcdir)/Makefile.am $(am__configure_deps)
+ @for dep in $?; do \
+ case '$(am__configure_deps)' in \
+ *$$dep*) \
+ ( cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh ) \
+ && { if test -f $@; then exit 0; else break; fi; }; \
+ exit 1;; \
+ esac; \
+ done; \
+ echo ' cd $(top_srcdir) && $(AUTOMAKE) --foreign src/lib-dict-backend/Makefile'; \
+ $(am__cd) $(top_srcdir) && \
+ $(AUTOMAKE) --foreign src/lib-dict-backend/Makefile
+Makefile: $(srcdir)/Makefile.in $(top_builddir)/config.status
+ @case '$?' in \
+ *config.status*) \
+ cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh;; \
+ *) \
+ echo ' cd $(top_builddir) && $(SHELL) ./config.status $(subdir)/$@ $(am__maybe_remake_depfiles)'; \
+ cd $(top_builddir) && $(SHELL) ./config.status $(subdir)/$@ $(am__maybe_remake_depfiles);; \
+ esac;
+
+$(top_builddir)/config.status: $(top_srcdir)/configure $(CONFIG_STATUS_DEPENDENCIES)
+ cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh
+
+$(top_srcdir)/configure: @MAINTAINER_MODE_TRUE@ $(am__configure_deps)
+ cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh
+$(ACLOCAL_M4): @MAINTAINER_MODE_TRUE@ $(am__aclocal_m4_deps)
+ cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh
+$(am__aclocal_m4_deps):
+
+clean-noinstPROGRAMS:
+ @list='$(noinst_PROGRAMS)'; test -n "$$list" || exit 0; \
+ echo " rm -f" $$list; \
+ rm -f $$list || exit $$?; \
+ test -n "$(EXEEXT)" || exit 0; \
+ list=`for p in $$list; do echo "$$p"; done | sed 's/$(EXEEXT)$$//'`; \
+ echo " rm -f" $$list; \
+ rm -f $$list
+
+install-module_dictLTLIBRARIES: $(module_dict_LTLIBRARIES)
+ @$(NORMAL_INSTALL)
+ @list='$(module_dict_LTLIBRARIES)'; test -n "$(module_dictdir)" || list=; \
+ list2=; for p in $$list; do \
+ if test -f $$p; then \
+ list2="$$list2 $$p"; \
+ else :; fi; \
+ done; \
+ test -z "$$list2" || { \
+ echo " $(MKDIR_P) '$(DESTDIR)$(module_dictdir)'"; \
+ $(MKDIR_P) "$(DESTDIR)$(module_dictdir)" || exit 1; \
+ echo " $(LIBTOOL) $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=install $(INSTALL) $(INSTALL_STRIP_FLAG) $$list2 '$(DESTDIR)$(module_dictdir)'"; \
+ $(LIBTOOL) $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=install $(INSTALL) $(INSTALL_STRIP_FLAG) $$list2 "$(DESTDIR)$(module_dictdir)"; \
+ }
+
+uninstall-module_dictLTLIBRARIES:
+ @$(NORMAL_UNINSTALL)
+ @list='$(module_dict_LTLIBRARIES)'; test -n "$(module_dictdir)" || list=; \
+ for p in $$list; do \
+ $(am__strip_dir) \
+ echo " $(LIBTOOL) $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=uninstall rm -f '$(DESTDIR)$(module_dictdir)/$$f'"; \
+ $(LIBTOOL) $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=uninstall rm -f "$(DESTDIR)$(module_dictdir)/$$f"; \
+ done
+
+clean-module_dictLTLIBRARIES:
+ -test -z "$(module_dict_LTLIBRARIES)" || rm -f $(module_dict_LTLIBRARIES)
+ @list='$(module_dict_LTLIBRARIES)'; \
+ locs=`for p in $$list; do echo $$p; done | \
+ sed 's|^[^/]*$$|.|; s|/[^/]*$$||; s|$$|/so_locations|' | \
+ sort -u`; \
+ test -z "$$locs" || { \
+ echo rm -f $${locs}; \
+ rm -f $${locs}; \
+ }
+
+clean-noinstLTLIBRARIES:
+ -test -z "$(noinst_LTLIBRARIES)" || rm -f $(noinst_LTLIBRARIES)
+ @list='$(noinst_LTLIBRARIES)'; \
+ locs=`for p in $$list; do echo $$p; done | \
+ sed 's|^[^/]*$$|.|; s|/[^/]*$$||; s|$$|/so_locations|' | \
+ sort -u`; \
+ test -z "$$locs" || { \
+ echo rm -f $${locs}; \
+ rm -f $${locs}; \
+ }
+
+libdict_backend.la: $(libdict_backend_la_OBJECTS) $(libdict_backend_la_DEPENDENCIES) $(EXTRA_libdict_backend_la_DEPENDENCIES)
+ $(AM_V_CCLD)$(LINK) $(libdict_backend_la_OBJECTS) $(libdict_backend_la_LIBADD) $(LIBS)
+
+libdict_ldap.la: $(libdict_ldap_la_OBJECTS) $(libdict_ldap_la_DEPENDENCIES) $(EXTRA_libdict_ldap_la_DEPENDENCIES)
+ $(AM_V_CCLD)$(libdict_ldap_la_LINK) $(am_libdict_ldap_la_rpath) $(libdict_ldap_la_OBJECTS) $(libdict_ldap_la_LIBADD) $(LIBS)
+
+test-dict-sql$(EXEEXT): $(test_dict_sql_OBJECTS) $(test_dict_sql_DEPENDENCIES) $(EXTRA_test_dict_sql_DEPENDENCIES)
+ @rm -f test-dict-sql$(EXEEXT)
+ $(AM_V_CCLD)$(test_dict_sql_LINK) $(test_dict_sql_OBJECTS) $(test_dict_sql_LDADD) $(LIBS)
+
+mostlyclean-compile:
+ -rm -f *.$(OBJEXT)
+
+distclean-compile:
+ -rm -f *.tab.c
+
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/dict-cdb.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/dict-drivers-register.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/dict-ldap-settings.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/dict-ldap.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/dict-sql-settings.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/dict-sql.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/libdict_ldap_la-dict-ldap-settings.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/libdict_ldap_la-dict-ldap.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/test_dict_sql-test-dict-sql.Po@am__quote@ # am--include-marker
+
+$(am__depfiles_remade):
+ @$(MKDIR_P) $(@D)
+ @echo '# dummy' >$@-t && $(am__mv) $@-t $@
+
+am--depfiles: $(am__depfiles_remade)
+
+.c.o:
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(COMPILE) -MT $@ -MD -MP -MF $(DEPDIR)/$*.Tpo -c -o $@ $<
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/$*.Tpo $(DEPDIR)/$*.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='$<' object='$@' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(COMPILE) -c -o $@ $<
+
+.c.obj:
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(COMPILE) -MT $@ -MD -MP -MF $(DEPDIR)/$*.Tpo -c -o $@ `$(CYGPATH_W) '$<'`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/$*.Tpo $(DEPDIR)/$*.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='$<' object='$@' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(COMPILE) -c -o $@ `$(CYGPATH_W) '$<'`
+
+.c.lo:
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(LTCOMPILE) -MT $@ -MD -MP -MF $(DEPDIR)/$*.Tpo -c -o $@ $<
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/$*.Tpo $(DEPDIR)/$*.Plo
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='$<' object='$@' libtool=yes @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(LTCOMPILE) -c -o $@ $<
+
+libdict_ldap_la-dict-ldap.lo: dict-ldap.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(LIBTOOL) $(AM_V_lt) --tag=CC $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=compile $(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdict_ldap_la_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT libdict_ldap_la-dict-ldap.lo -MD -MP -MF $(DEPDIR)/libdict_ldap_la-dict-ldap.Tpo -c -o libdict_ldap_la-dict-ldap.lo `test -f 'dict-ldap.c' || echo '$(srcdir)/'`dict-ldap.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/libdict_ldap_la-dict-ldap.Tpo $(DEPDIR)/libdict_ldap_la-dict-ldap.Plo
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='dict-ldap.c' object='libdict_ldap_la-dict-ldap.lo' libtool=yes @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(LIBTOOL) $(AM_V_lt) --tag=CC $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=compile $(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdict_ldap_la_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o libdict_ldap_la-dict-ldap.lo `test -f 'dict-ldap.c' || echo '$(srcdir)/'`dict-ldap.c
+
+libdict_ldap_la-dict-ldap-settings.lo: dict-ldap-settings.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(LIBTOOL) $(AM_V_lt) --tag=CC $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=compile $(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdict_ldap_la_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT libdict_ldap_la-dict-ldap-settings.lo -MD -MP -MF $(DEPDIR)/libdict_ldap_la-dict-ldap-settings.Tpo -c -o libdict_ldap_la-dict-ldap-settings.lo `test -f 'dict-ldap-settings.c' || echo '$(srcdir)/'`dict-ldap-settings.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/libdict_ldap_la-dict-ldap-settings.Tpo $(DEPDIR)/libdict_ldap_la-dict-ldap-settings.Plo
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='dict-ldap-settings.c' object='libdict_ldap_la-dict-ldap-settings.lo' libtool=yes @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(LIBTOOL) $(AM_V_lt) --tag=CC $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=compile $(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdict_ldap_la_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o libdict_ldap_la-dict-ldap-settings.lo `test -f 'dict-ldap-settings.c' || echo '$(srcdir)/'`dict-ldap-settings.c
+
+test_dict_sql-test-dict-sql.o: test-dict-sql.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) $(CPPFLAGS) $(test_dict_sql_CFLAGS) $(CFLAGS) -MT test_dict_sql-test-dict-sql.o -MD -MP -MF $(DEPDIR)/test_dict_sql-test-dict-sql.Tpo -c -o test_dict_sql-test-dict-sql.o `test -f 'test-dict-sql.c' || echo '$(srcdir)/'`test-dict-sql.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/test_dict_sql-test-dict-sql.Tpo $(DEPDIR)/test_dict_sql-test-dict-sql.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='test-dict-sql.c' object='test_dict_sql-test-dict-sql.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) $(CPPFLAGS) $(test_dict_sql_CFLAGS) $(CFLAGS) -c -o test_dict_sql-test-dict-sql.o `test -f 'test-dict-sql.c' || echo '$(srcdir)/'`test-dict-sql.c
+
+test_dict_sql-test-dict-sql.obj: test-dict-sql.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) $(CPPFLAGS) $(test_dict_sql_CFLAGS) $(CFLAGS) -MT test_dict_sql-test-dict-sql.obj -MD -MP -MF $(DEPDIR)/test_dict_sql-test-dict-sql.Tpo -c -o test_dict_sql-test-dict-sql.obj `if test -f 'test-dict-sql.c'; then $(CYGPATH_W) 'test-dict-sql.c'; else $(CYGPATH_W) '$(srcdir)/test-dict-sql.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/test_dict_sql-test-dict-sql.Tpo $(DEPDIR)/test_dict_sql-test-dict-sql.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='test-dict-sql.c' object='test_dict_sql-test-dict-sql.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) $(CPPFLAGS) $(test_dict_sql_CFLAGS) $(CFLAGS) -c -o test_dict_sql-test-dict-sql.obj `if test -f 'test-dict-sql.c'; then $(CYGPATH_W) 'test-dict-sql.c'; else $(CYGPATH_W) '$(srcdir)/test-dict-sql.c'; fi`
+
+mostlyclean-libtool:
+ -rm -f *.lo
+
+clean-libtool:
+ -rm -rf .libs _libs
+
+ID: $(am__tagged_files)
+ $(am__define_uniq_tagged_files); mkid -fID $$unique
+tags: tags-am
+TAGS: tags
+
+tags-am: $(TAGS_DEPENDENCIES) $(am__tagged_files)
+ set x; \
+ here=`pwd`; \
+ $(am__define_uniq_tagged_files); \
+ shift; \
+ if test -z "$(ETAGS_ARGS)$$*$$unique"; then :; else \
+ test -n "$$unique" || unique=$$empty_fix; \
+ if test $$# -gt 0; then \
+ $(ETAGS) $(ETAGSFLAGS) $(AM_ETAGSFLAGS) $(ETAGS_ARGS) \
+ "$$@" $$unique; \
+ else \
+ $(ETAGS) $(ETAGSFLAGS) $(AM_ETAGSFLAGS) $(ETAGS_ARGS) \
+ $$unique; \
+ fi; \
+ fi
+ctags: ctags-am
+
+CTAGS: ctags
+ctags-am: $(TAGS_DEPENDENCIES) $(am__tagged_files)
+ $(am__define_uniq_tagged_files); \
+ test -z "$(CTAGS_ARGS)$$unique" \
+ || $(CTAGS) $(CTAGSFLAGS) $(AM_CTAGSFLAGS) $(CTAGS_ARGS) \
+ $$unique
+
+GTAGS:
+ here=`$(am__cd) $(top_builddir) && pwd` \
+ && $(am__cd) $(top_srcdir) \
+ && gtags -i $(GTAGS_ARGS) "$$here"
+cscopelist: cscopelist-am
+
+cscopelist-am: $(am__tagged_files)
+ list='$(am__tagged_files)'; \
+ case "$(srcdir)" in \
+ [\\/]* | ?:[\\/]*) sdir="$(srcdir)" ;; \
+ *) sdir=$(subdir)/$(srcdir) ;; \
+ esac; \
+ for i in $$list; do \
+ if test -f "$$i"; then \
+ echo "$(subdir)/$$i"; \
+ else \
+ echo "$$sdir/$$i"; \
+ fi; \
+ done >> $(top_builddir)/cscope.files
+
+distclean-tags:
+ -rm -f TAGS ID GTAGS GRTAGS GSYMS GPATH tags
+
+distdir: $(BUILT_SOURCES)
+ $(MAKE) $(AM_MAKEFLAGS) distdir-am
+
+distdir-am: $(DISTFILES)
+ @srcdirstrip=`echo "$(srcdir)" | sed 's/[].[^$$\\*]/\\\\&/g'`; \
+ topsrcdirstrip=`echo "$(top_srcdir)" | sed 's/[].[^$$\\*]/\\\\&/g'`; \
+ list='$(DISTFILES)'; \
+ dist_files=`for file in $$list; do echo $$file; done | \
+ sed -e "s|^$$srcdirstrip/||;t" \
+ -e "s|^$$topsrcdirstrip/|$(top_builddir)/|;t"`; \
+ case $$dist_files in \
+ */*) $(MKDIR_P) `echo "$$dist_files" | \
+ sed '/\//!d;s|^|$(distdir)/|;s,/[^/]*$$,,' | \
+ sort -u` ;; \
+ esac; \
+ for file in $$dist_files; do \
+ if test -f $$file || test -d $$file; then d=.; else d=$(srcdir); fi; \
+ if test -d $$d/$$file; then \
+ dir=`echo "/$$file" | sed -e 's,/[^/]*$$,,'`; \
+ if test -d "$(distdir)/$$file"; then \
+ find "$(distdir)/$$file" -type d ! -perm -700 -exec chmod u+rwx {} \;; \
+ fi; \
+ if test -d $(srcdir)/$$file && test $$d != $(srcdir); then \
+ cp -fpR $(srcdir)/$$file "$(distdir)$$dir" || exit 1; \
+ find "$(distdir)/$$file" -type d ! -perm -700 -exec chmod u+rwx {} \;; \
+ fi; \
+ cp -fpR $$d/$$file "$(distdir)$$dir" || exit 1; \
+ else \
+ test -f "$(distdir)/$$file" \
+ || cp -p $$d/$$file "$(distdir)/$$file" \
+ || exit 1; \
+ fi; \
+ done
+check-am: all-am
+ $(MAKE) $(AM_MAKEFLAGS) check-local
+check: check-am
+all-am: Makefile $(PROGRAMS) $(LTLIBRARIES) $(HEADERS)
+installdirs:
+ for dir in "$(DESTDIR)$(module_dictdir)"; do \
+ test -z "$$dir" || $(MKDIR_P) "$$dir"; \
+ done
+install: install-am
+install-exec: install-exec-am
+install-data: install-data-am
+uninstall: uninstall-am
+
+install-am: all-am
+ @$(MAKE) $(AM_MAKEFLAGS) install-exec-am install-data-am
+
+installcheck: installcheck-am
+install-strip:
+ if test -z '$(STRIP)'; then \
+ $(MAKE) $(AM_MAKEFLAGS) INSTALL_PROGRAM="$(INSTALL_STRIP_PROGRAM)" \
+ install_sh_PROGRAM="$(INSTALL_STRIP_PROGRAM)" INSTALL_STRIP_FLAG=-s \
+ install; \
+ else \
+ $(MAKE) $(AM_MAKEFLAGS) INSTALL_PROGRAM="$(INSTALL_STRIP_PROGRAM)" \
+ install_sh_PROGRAM="$(INSTALL_STRIP_PROGRAM)" INSTALL_STRIP_FLAG=-s \
+ "INSTALL_PROGRAM_ENV=STRIPPROG='$(STRIP)'" install; \
+ fi
+mostlyclean-generic:
+
+clean-generic:
+
+maintainer-clean-generic:
+ @echo "This command is intended for maintainers to use"
+ @echo "it deletes files that may require special tools to rebuild."
+clean: clean-am
+
+clean-am: clean-generic clean-libtool clean-module_dictLTLIBRARIES \
+ clean-noinstLTLIBRARIES clean-noinstPROGRAMS mostlyclean-am
+
+distclean: distclean-am
+ -rm -f ./$(DEPDIR)/dict-cdb.Plo
+ -rm -f ./$(DEPDIR)/dict-drivers-register.Plo
+ -rm -f ./$(DEPDIR)/dict-ldap-settings.Plo
+ -rm -f ./$(DEPDIR)/dict-ldap.Plo
+ -rm -f ./$(DEPDIR)/dict-sql-settings.Plo
+ -rm -f ./$(DEPDIR)/dict-sql.Plo
+ -rm -f ./$(DEPDIR)/libdict_ldap_la-dict-ldap-settings.Plo
+ -rm -f ./$(DEPDIR)/libdict_ldap_la-dict-ldap.Plo
+ -rm -f ./$(DEPDIR)/test_dict_sql-test-dict-sql.Po
+ -rm -f Makefile
+distclean-am: clean-am distclean-compile distclean-generic \
+ distclean-tags
+
+dvi: dvi-am
+
+dvi-am:
+
+html: html-am
+
+html-am:
+
+info: info-am
+
+info-am:
+
+install-data-am: install-module_dictLTLIBRARIES
+
+install-dvi: install-dvi-am
+
+install-dvi-am:
+
+install-exec-am:
+
+install-html: install-html-am
+
+install-html-am:
+
+install-info: install-info-am
+
+install-info-am:
+
+install-man:
+
+install-pdf: install-pdf-am
+
+install-pdf-am:
+
+install-ps: install-ps-am
+
+install-ps-am:
+
+installcheck-am:
+
+maintainer-clean: maintainer-clean-am
+ -rm -f ./$(DEPDIR)/dict-cdb.Plo
+ -rm -f ./$(DEPDIR)/dict-drivers-register.Plo
+ -rm -f ./$(DEPDIR)/dict-ldap-settings.Plo
+ -rm -f ./$(DEPDIR)/dict-ldap.Plo
+ -rm -f ./$(DEPDIR)/dict-sql-settings.Plo
+ -rm -f ./$(DEPDIR)/dict-sql.Plo
+ -rm -f ./$(DEPDIR)/libdict_ldap_la-dict-ldap-settings.Plo
+ -rm -f ./$(DEPDIR)/libdict_ldap_la-dict-ldap.Plo
+ -rm -f ./$(DEPDIR)/test_dict_sql-test-dict-sql.Po
+ -rm -f Makefile
+maintainer-clean-am: distclean-am maintainer-clean-generic
+
+mostlyclean: mostlyclean-am
+
+mostlyclean-am: mostlyclean-compile mostlyclean-generic \
+ mostlyclean-libtool
+
+pdf: pdf-am
+
+pdf-am:
+
+ps: ps-am
+
+ps-am:
+
+uninstall-am: uninstall-module_dictLTLIBRARIES
+
+.MAKE: check-am install-am install-strip
+
+.PHONY: CTAGS GTAGS TAGS all all-am am--depfiles check check-am \
+ check-local clean clean-generic clean-libtool \
+ clean-module_dictLTLIBRARIES clean-noinstLTLIBRARIES \
+ clean-noinstPROGRAMS cscopelist-am ctags ctags-am distclean \
+ distclean-compile distclean-generic distclean-libtool \
+ distclean-tags distdir dvi dvi-am html html-am info info-am \
+ install install-am install-data install-data-am install-dvi \
+ install-dvi-am install-exec install-exec-am install-html \
+ install-html-am install-info install-info-am install-man \
+ install-module_dictLTLIBRARIES install-pdf install-pdf-am \
+ install-ps install-ps-am install-strip installcheck \
+ installcheck-am installdirs maintainer-clean \
+ maintainer-clean-generic mostlyclean mostlyclean-compile \
+ mostlyclean-generic mostlyclean-libtool pdf pdf-am ps ps-am \
+ tags tags-am uninstall uninstall-am \
+ uninstall-module_dictLTLIBRARIES
+
+.PRECIOUS: Makefile
+
+
+dict-drivers-register.c: Makefile $(top_builddir)/config.h
+ rm -f $@
+ echo '/* this file automatically generated by Makefile */' >$@
+ echo '#include "lib.h"' >>$@
+ echo '#include "dict.h"' >>$@
+ echo '#include "ldap-client.h"' >>$@
+ echo '#include "dict-sql.h"' >>$@
+ for i in $(dict_drivers) null; do \
+ if [ "$${i}" != "null" ]; then \
+ echo "extern struct dict dict_driver_$${i};" >>$@ ; \
+ fi; \
+ done
+ echo 'void dict_drivers_register_all(void) {' >>$@
+ echo 'dict_drivers_register_builtin();' >>$@
+ echo 'dict_sql_register();' >>$@
+ for i in $(dict_drivers) null; do \
+ if [ "$${i}" != "null" ]; then \
+ echo "dict_driver_register(&dict_driver_$${i});" >>$@ ; \
+ fi; \
+ done
+ echo '}' >>$@
+ echo 'void dict_drivers_unregister_all(void) {' >>$@
+ echo '#ifdef BUILTIN_LDAP' >>$@
+ echo 'ldap_clients_cleanup();' >>$@
+ echo '#endif' >>$@
+ echo 'dict_drivers_unregister_builtin();' >>$@
+ echo 'dict_sql_unregister();' >>$@
+ for i in $(dict_drivers) null; do \
+ if [ "$${i}" != "null" ]; then \
+ echo "dict_driver_unregister(&dict_driver_$${i});" >>$@ ; \
+ fi; \
+ done
+ echo '}' >>$@
+
+distclean-generic:
+ rm -f Makefile dict-drivers-register.c
+
+check-local:
+ for bin in $(test_programs) $(check_PROGRAMS); do \
+ if ! $(RUN_TEST) ./$$bin; then exit 1; fi; \
+ done
+
+# Tell versions [3.59,3.63) of GNU make to not export all variables.
+# Otherwise a system limit (for SysV at least) may be exceeded.
+.NOEXPORT:
diff --git a/src/lib-dict-backend/dict-cdb.c b/src/lib-dict-backend/dict-cdb.c
new file mode 100644
index 0000000..c6fad30
--- /dev/null
+++ b/src/lib-dict-backend/dict-cdb.c
@@ -0,0 +1,266 @@
+/* Copyright (c) 2013-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "buffer.h"
+
+#ifdef BUILD_CDB
+#include "dict-private.h"
+
+#include <string.h>
+#include <cdb.h>
+#include <unistd.h>
+#include <fcntl.h>
+
+#define CDB_WITH_NULL 1
+#define CDB_WITHOUT_NULL 2
+
+struct cdb_dict {
+ struct dict dict;
+ struct cdb cdb;
+ char *path;
+ int fd, flag;
+};
+
+struct cdb_dict_iterate_context {
+ struct dict_iterate_context ctx;
+
+ enum dict_iterate_flags flags;
+ buffer_t *buffer;
+ const char *values[2];
+ char *path;
+ unsigned cptr;
+ char *error;
+};
+
+static void cdb_dict_deinit(struct dict *_dict);
+
+static int
+cdb_dict_init(struct dict *driver, const char *uri,
+ const struct dict_settings *set ATTR_UNUSED,
+ struct dict **dict_r, const char **error_r)
+{
+ struct cdb_dict *dict;
+
+ dict = i_new(struct cdb_dict, 1);
+ dict->dict = *driver;
+ dict->path = i_strdup(uri);
+ dict->flag = CDB_WITH_NULL | CDB_WITHOUT_NULL;
+
+ /* initialize cdb to 0 (unallocated) */
+ i_zero(&dict->cdb);
+
+ dict->fd = open(dict->path, O_RDONLY);
+ if (dict->fd == -1) {
+ *error_r = t_strdup_printf("open(%s) failed: %m", dict->path);
+ cdb_dict_deinit(&dict->dict);
+ return -1;
+ }
+
+#ifdef TINYCDB_VERSION
+ if (cdb_init(&dict->cdb, dict->fd) < 0) {
+ *error_r = t_strdup_printf("cdb_init(%s) failed: %m", dict->path);
+ cdb_dict_deinit(&dict->dict);
+ return -1;
+ }
+#else
+ cdb_init(&dict->cdb, dict->fd);
+#endif
+
+ *dict_r = &dict->dict;
+ return 0;
+}
+
+static void cdb_dict_deinit(struct dict *_dict)
+{
+ struct cdb_dict *dict = (struct cdb_dict *)_dict;
+
+ /* we can safely deinit unallocated cdb */
+ cdb_free(&dict->cdb);
+
+ i_close_fd_path(&dict->fd, dict->path);
+
+ i_free(dict->path);
+ i_free(dict);
+}
+
+static int
+cdb_dict_lookup(struct dict *_dict,
+ const struct dict_op_settings *set ATTR_UNUSED,
+ pool_t pool,
+ const char *key, const char **value_r,
+ const char **error_r)
+{
+ struct cdb_dict *dict = (struct cdb_dict *)_dict;
+ unsigned datalen;
+ int ret = 0;
+ char *data;
+
+ /* keys and values may be null terminated... */
+ if ((dict->flag & CDB_WITH_NULL) != 0) {
+ ret = cdb_find(&dict->cdb, key, (unsigned)strlen(key)+1);
+ if (ret > 0)
+ dict->flag &= ENUM_NEGATE(CDB_WITHOUT_NULL);
+ }
+
+ /* ...or not */
+ if (ret == 0 && (dict->flag & CDB_WITHOUT_NULL) != 0) {
+ ret = cdb_find(&dict->cdb, key, (unsigned)strlen(key));
+ if (ret > 0)
+ dict->flag &= ENUM_NEGATE(CDB_WITH_NULL);
+ }
+
+ if (ret <= 0) {
+ *value_r = NULL;
+ /* something bad with db */
+ if (ret < 0) {
+ *error_r = t_strdup_printf("cdb_find(%s) failed: %m", dict->path);
+ return -1;
+ }
+ /* found nothing */
+ return 0;
+ }
+
+ datalen = cdb_datalen(&dict->cdb);
+ data = p_malloc(pool, datalen + 1);
+ if (cdb_read(&dict->cdb, data, datalen, cdb_datapos(&dict->cdb)) < 0) {
+ *error_r = t_strdup_printf("cdb_read(%s) failed: %m", dict->path);
+ return -1;
+ }
+ *value_r = data;
+ return 1;
+}
+
+static struct dict_iterate_context *
+cdb_dict_iterate_init(struct dict *_dict,
+ const struct dict_op_settings *set ATTR_UNUSED,
+ const char *path, enum dict_iterate_flags flags)
+{
+ struct cdb_dict_iterate_context *ctx =
+ i_new(struct cdb_dict_iterate_context, 1);
+ struct cdb_dict *dict = (struct cdb_dict *)_dict;
+
+ ctx->ctx.dict = &dict->dict;
+ ctx->path = i_strdup(path);
+ ctx->flags = flags;
+ ctx->buffer = buffer_create_dynamic(default_pool, 256);
+
+ cdb_seqinit(&ctx->cptr, &dict->cdb);
+
+ return &ctx->ctx;
+}
+
+static bool
+cdb_dict_next(struct cdb_dict_iterate_context *ctx, const char **key_r)
+{
+ struct cdb_dict *dict = (struct cdb_dict *)ctx->ctx.dict;
+ char *data;
+ unsigned datalen;
+ int ret;
+
+ if ((ret = cdb_seqnext(&ctx->cptr, &dict->cdb)) < 1) {
+ if (ret < 0)
+ ctx->error = i_strdup_printf("cdb_seqnext(%s) failed: %m",
+ dict->path);
+ return FALSE;
+ }
+
+ buffer_set_used_size(ctx->buffer, 0);
+
+ datalen = cdb_keylen(&dict->cdb);
+ data = buffer_append_space_unsafe(ctx->buffer, datalen + 1);
+
+ if (cdb_read(&dict->cdb, data, datalen, cdb_keypos(&dict->cdb)) < 0) {
+ ctx->error = i_strdup_printf("cdb_read(%s) failed: %m",
+ dict->path);
+ return FALSE;
+ }
+
+ data[datalen] = '\0';
+ *key_r = data;
+
+ return TRUE;
+}
+
+static bool cdb_dict_iterate(struct dict_iterate_context *_ctx,
+ const char **key_r, const char *const **values_r)
+{
+ struct cdb_dict_iterate_context *ctx =
+ (struct cdb_dict_iterate_context *)_ctx;
+ struct cdb_dict *dict = (struct cdb_dict *)_ctx->dict;
+ const char *key;
+ bool match = FALSE;
+ char *data;
+ unsigned datalen;
+
+ if (ctx->error != NULL)
+ return FALSE;
+
+ while(!match && cdb_dict_next(ctx, &key)) {
+ if (((ctx->flags & DICT_ITERATE_FLAG_EXACT_KEY) != 0 &&
+ strcmp(key, ctx->path) == 0) ||
+ ((ctx->flags & DICT_ITERATE_FLAG_RECURSE) != 0 &&
+ str_begins(key, ctx->path)) ||
+ ((ctx->flags & DICT_ITERATE_FLAG_RECURSE) == 0 &&
+ str_begins(key, ctx->path) &&
+ strchr(key + strlen(ctx->path), '/') == NULL)) {
+ match = TRUE;
+ break;
+ }
+ }
+
+ if (!match)
+ return FALSE;
+
+ *key_r = key;
+
+ if ((ctx->flags & DICT_ITERATE_FLAG_NO_VALUE) != 0)
+ return TRUE;
+
+ datalen = cdb_datalen(&dict->cdb);
+ data = buffer_append_space_unsafe(ctx->buffer, datalen + 1);
+
+ if (cdb_read(&dict->cdb, data, datalen, cdb_datapos(&dict->cdb)) < 0) {
+ ctx->error = i_strdup_printf("cdb_read(%s) failed: %m",
+ dict->path);
+ return FALSE;
+ }
+
+ data[datalen] = '\0';
+ ctx->values[0] = data;
+ *values_r = ctx->values;
+
+ return TRUE;
+}
+
+static int cdb_dict_iterate_deinit(struct dict_iterate_context *_ctx,
+ const char **error_r)
+{
+ int ret = 0;
+ struct cdb_dict_iterate_context *ctx =
+ (struct cdb_dict_iterate_context *)_ctx;
+ if (ctx->error != NULL) {
+ *error_r = t_strdup(ctx->error);
+ ret = -1;
+ }
+
+ buffer_free(&ctx->buffer);
+ i_free(ctx->error);
+ i_free(ctx->path);
+ i_free(ctx);
+
+ return ret;
+}
+
+
+struct dict dict_driver_cdb = {
+ .name = "cdb",
+ {
+ .init = cdb_dict_init,
+ .deinit = cdb_dict_deinit,
+ .lookup = cdb_dict_lookup,
+ .iterate_init = cdb_dict_iterate_init,
+ .iterate = cdb_dict_iterate,
+ .iterate_deinit = cdb_dict_iterate_deinit,
+ }
+};
+#endif
diff --git a/src/lib-dict-backend/dict-ldap-settings.c b/src/lib-dict-backend/dict-ldap-settings.c
new file mode 100644
index 0000000..01205eb
--- /dev/null
+++ b/src/lib-dict-backend/dict-ldap-settings.c
@@ -0,0 +1,313 @@
+/* Copyright (c) 2008-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+
+#if defined(BUILTIN_LDAP) || defined(PLUGIN_BUILD)
+
+#include "array.h"
+#include "str.h"
+#include "settings.h"
+#include "dict-ldap-settings.h"
+
+#include <ctype.h>
+
+static const char *dict_ldap_commonName = "cn";
+static const char *dict_ldap_empty_filter = "";
+
+enum section_type {
+ SECTION_ROOT = 0,
+ SECTION_MAP,
+ SECTION_FIELDS
+};
+
+struct dict_ldap_map_attribute {
+ const char *name;
+ const char *variable;
+};
+
+struct setting_parser_ctx {
+ pool_t pool;
+ struct dict_ldap_settings *set;
+ enum section_type type;
+
+ struct dict_ldap_map cur_map;
+ ARRAY(struct dict_ldap_map_attribute) cur_attributes;
+};
+
+#undef DEF_STR
+#undef DEF_BOOL
+#undef DEF_UINT
+
+#define DEF_STR(name) DEF_STRUCT_STR(name, dict_ldap_map)
+#define DEF_BOOL(name) DEF_STRUCT_BOOL(name, dict_ldap_map)
+#define DEF_UINT(name) DEF_STRUCT_UINT(name ,dict_ldap_map)
+
+static const struct setting_def dict_ldap_map_setting_defs[] = {
+ DEF_STR(pattern),
+ DEF_STR(filter),
+ DEF_STR(filter_iter),
+ DEF_STR(username_attribute),
+ DEF_STR(value_attribute),
+ DEF_STR(base_dn),
+ DEF_STR(scope),
+ { 0, NULL, 0 }
+};
+
+static const char *pattern_read_name(const char **pattern)
+{
+ const char *p = *pattern, *name;
+
+ if (*p == '{') {
+ /* ${name} */
+ name = ++p;
+ p = strchr(p, '}');
+ if (p == NULL) {
+ /* error, but allow anyway */
+ *pattern += strlen(*pattern);
+ return "";
+ }
+ *pattern = p + 1;
+ } else {
+ /* $name - ends at the first non-alnum_ character */
+ name = p;
+ for (; *p != '\0'; p++) {
+ if (!i_isalnum(*p) && *p != '_')
+ break;
+ }
+ *pattern = p;
+ }
+ name = t_strdup_until(name, p);
+ return name;
+}
+
+static const char *dict_ldap_attributes_map(struct setting_parser_ctx *ctx)
+{
+ struct dict_ldap_map_attribute *attributes;
+ string_t *pattern;
+ const char *p, *name;
+ unsigned int i, count;
+
+ /* go through the variables in the pattern, replace them with plain
+ '$' character and add its ldap attribute */
+ pattern = t_str_new(strlen(ctx->cur_map.pattern) + 1);
+ attributes = array_get_modifiable(&ctx->cur_attributes, &count);
+
+ p_array_init(&ctx->cur_map.ldap_attributes, ctx->pool, count);
+ for (p = ctx->cur_map.pattern; *p != '\0';) {
+ if (*p != '$') {
+ str_append_c(pattern, *p);
+ p++;
+ continue;
+ }
+ p++;
+ str_append_c(pattern, '$');
+
+ name = pattern_read_name(&p);
+ for (i = 0; i < count; i++) {
+ if (attributes[i].variable != NULL &&
+ strcmp(attributes[i].variable, name) == 0)
+ break;
+ }
+ if (i == count) {
+ return t_strconcat("Missing LDAP attribute for variable: ",
+ name, NULL);
+ }
+
+ /* mark this attribute as used */
+ attributes[i].variable = NULL;
+ array_push_back(&ctx->cur_map.ldap_attributes,
+ &attributes[i].name);
+ }
+
+ /* make sure there aren't any unused attributes */
+ for (i = 0; i < count; i++) {
+ if (attributes[i].variable != NULL) {
+ return t_strconcat("Unused variable: ",
+ attributes[i].variable, NULL);
+ }
+ }
+
+ if (ctx->set->max_attribute_count < count)
+ ctx->set->max_attribute_count = count;
+ ctx->cur_map.pattern = p_strdup(ctx->pool, str_c(pattern));
+ return NULL;
+}
+
+static const char *dict_ldap_map_finish(struct setting_parser_ctx *ctx)
+{
+ if (ctx->cur_map.pattern == NULL)
+ return "Missing setting: pattern";
+ if (ctx->cur_map.filter == NULL)
+ ctx->cur_map.filter = dict_ldap_empty_filter;
+ if (*ctx->cur_map.filter != '\0') {
+ const char *ptr = ctx->cur_map.filter;
+ if (*ptr != '(')
+ return "Filter must start with (";
+ while(*ptr != '\0') ptr++;
+ ptr--;
+ if (*ptr != ')')
+ return "Filter must end with )";
+ }
+ if (ctx->cur_map.value_attribute == NULL)
+ return "Missing setting: value_attribute";
+
+ if (ctx->cur_map.username_attribute == NULL) {
+ /* default to commonName */
+ ctx->cur_map.username_attribute = dict_ldap_commonName;
+ }
+ if (ctx->cur_map.scope == NULL) {
+ ctx->cur_map.scope_val = 2; /* subtree */
+ } else {
+ if (strcasecmp(ctx->cur_map.scope, "one") == 0) ctx->cur_map.scope_val = 1;
+ else if (strcasecmp(ctx->cur_map.scope, "base") == 0) ctx->cur_map.scope_val = 0;
+ else if (strcasecmp(ctx->cur_map.scope, "subtree") == 0) ctx->cur_map.scope_val = 2;
+ else return "Scope must be one, base or subtree";
+ }
+ if (!array_is_created(&ctx->cur_map.ldap_attributes)) {
+ /* no attributes besides value. allocate the array anyway. */
+ p_array_init(&ctx->cur_map.ldap_attributes, ctx->pool, 1);
+ if (strchr(ctx->cur_map.pattern, '$') != NULL)
+ return "Missing attributes for pattern variables";
+ }
+ array_push_back(&ctx->set->maps, &ctx->cur_map);
+ i_zero(&ctx->cur_map);
+ return NULL;
+}
+
+static const char *
+parse_setting(const char *key, const char *value,
+ struct setting_parser_ctx *ctx)
+{
+ struct dict_ldap_map_attribute *attribute;
+
+ switch (ctx->type) {
+ case SECTION_ROOT:
+ if (strcmp(key, "uri") == 0) {
+ ctx->set->uri = p_strdup(ctx->pool, value);
+ return NULL;
+ }
+ if (strcmp(key, "bind_dn") == 0) {
+ ctx->set->bind_dn = p_strdup(ctx->pool, value);
+ return NULL;
+ }
+ if (strcmp(key, "password") == 0) {
+ ctx->set->password = p_strdup(ctx->pool, value);
+ return NULL;
+ }
+ if (strcmp(key, "timeout") == 0) {
+ if (str_to_uint(value, &ctx->set->timeout) != 0) {
+ return "Invalid timeout value";
+ }
+ return NULL;
+ }
+ if (strcmp(key, "max_idle_time") == 0) {
+ if (str_to_uint(value, &ctx->set->max_idle_time) != 0) {
+ return "Invalid max_idle_time value";
+ }
+ return NULL;
+ }
+ if (strcmp(key, "debug") == 0) {
+ if (str_to_uint(value, &ctx->set->debug) != 0) {
+ return "invalid debug value";
+ }
+ return NULL;
+ }
+ if (strcmp(key, "tls") == 0) {
+ if (strcasecmp(value, "yes") == 0) {
+ ctx->set->require_ssl = TRUE;
+ ctx->set->start_tls = TRUE;
+ } else if (strcasecmp(value, "no") == 0) {
+ ctx->set->require_ssl = FALSE;
+ ctx->set->start_tls = FALSE;
+ } else if (strcasecmp(value, "try") == 0) {
+ ctx->set->require_ssl = FALSE;
+ ctx->set->start_tls = TRUE;
+ } else {
+ return "tls must be yes, try or no";
+ }
+ return NULL;
+ }
+ break;
+ case SECTION_MAP:
+ return parse_setting_from_defs(ctx->pool,
+ dict_ldap_map_setting_defs,
+ &ctx->cur_map, key, value);
+ case SECTION_FIELDS:
+ if (*value != '$') {
+ return t_strconcat("Value is missing '$' for attribute: ",
+ key, NULL);
+ }
+ attribute = array_append_space(&ctx->cur_attributes);
+ attribute->name = p_strdup(ctx->pool, key);
+ attribute->variable = p_strdup(ctx->pool, value + 1);
+ return NULL;
+ }
+ return t_strconcat("Unknown setting: ", key, NULL);
+}
+
+static bool
+parse_section(const char *type, const char *name ATTR_UNUSED,
+ struct setting_parser_ctx *ctx, const char **error_r)
+{
+ switch (ctx->type) {
+ case SECTION_ROOT:
+ if (type == NULL)
+ return FALSE;
+ if (strcmp(type, "map") == 0) {
+ array_clear(&ctx->cur_attributes);
+ ctx->type = SECTION_MAP;
+ return TRUE;
+ }
+ break;
+ case SECTION_MAP:
+ if (type == NULL) {
+ ctx->type = SECTION_ROOT;
+ *error_r = dict_ldap_map_finish(ctx);
+ return FALSE;
+ }
+ if (strcmp(type, "fields") == 0) {
+ ctx->type = SECTION_FIELDS;
+ return TRUE;
+ }
+ break;
+ case SECTION_FIELDS:
+ if (type == NULL) {
+ ctx->type = SECTION_MAP;
+ *error_r = dict_ldap_attributes_map(ctx);
+ return FALSE;
+ }
+ break;
+ }
+ *error_r = t_strconcat("Unknown section: ", type, NULL);
+ return FALSE;
+}
+
+struct dict_ldap_settings *
+dict_ldap_settings_read(pool_t pool, const char *path, const char **error_r)
+{
+ struct setting_parser_ctx ctx;
+
+ i_zero(&ctx);
+ ctx.pool = pool;
+ ctx.set = p_new(pool, struct dict_ldap_settings, 1);
+ t_array_init(&ctx.cur_attributes, 16);
+ p_array_init(&ctx.set->maps, pool, 8);
+
+ ctx.set->timeout = 30; /* default timeout */
+ ctx.set->require_ssl = FALSE; /* try to start SSL */
+ ctx.set->start_tls = TRUE;
+
+ if (!settings_read(path, NULL, parse_setting, parse_section,
+ &ctx, error_r))
+ return NULL;
+
+ if (ctx.set->uri == NULL) {
+ *error_r = t_strdup_printf("Error in configuration file %s: "
+ "Missing ldap uri", path);
+ return NULL;
+ }
+
+ return ctx.set;
+}
+
+#endif
diff --git a/src/lib-dict-backend/dict-ldap-settings.h b/src/lib-dict-backend/dict-ldap-settings.h
new file mode 100644
index 0000000..0919ca9
--- /dev/null
+++ b/src/lib-dict-backend/dict-ldap-settings.h
@@ -0,0 +1,36 @@
+#ifndef DICT_LDAP_SETTINGS_H
+#define DICT_LDAP_SETTINGS_H
+
+struct dict_ldap_map {
+ /* pattern is in simplified form: all variables are stored as simple
+ '$' character. fields array is sorted by the variable index. */
+ const char *pattern;
+ const char *filter;
+ const char *filter_iter;
+ const char *username_attribute;
+ const char *value_attribute;
+ const char *base_dn;
+ const char *scope;
+ int scope_val;
+ unsigned int timeout;
+
+ ARRAY_TYPE(const_string) ldap_attributes;
+};
+
+struct dict_ldap_settings {
+ const char *uri;
+ const char *bind_dn;
+ const char *password;
+ unsigned int timeout;
+ unsigned int max_idle_time;
+ unsigned int debug;
+ unsigned int max_attribute_count;
+ bool require_ssl;
+ bool start_tls;
+ ARRAY(struct dict_ldap_map) maps;
+};
+
+struct dict_ldap_settings *
+dict_ldap_settings_read(pool_t pool, const char *path, const char **error_r);
+
+#endif
diff --git a/src/lib-dict-backend/dict-ldap.c b/src/lib-dict-backend/dict-ldap.c
new file mode 100644
index 0000000..433871b
--- /dev/null
+++ b/src/lib-dict-backend/dict-ldap.c
@@ -0,0 +1,500 @@
+/* Copyright (c) 2016-2018 Dovecot authors, see the included COPYING memcached */
+
+#include "lib.h"
+
+#if defined(BUILTIN_LDAP) || defined(PLUGIN_BUILD)
+
+#include "array.h"
+#include "module-dir.h"
+#include "str.h"
+#include "istream.h"
+#include "ostream.h"
+#include "var-expand.h"
+#include "connection.h"
+#include "llist.h"
+#include "ldap-client.h"
+#include "dict.h"
+#include "dict-private.h"
+#include "dict-ldap-settings.h"
+
+static const char *LDAP_ESCAPE_CHARS = "*,\\#+<>;\"()= ";
+
+struct ldap_dict;
+
+struct dict_ldap_op {
+ struct ldap_dict *dict;
+ const struct dict_ldap_map *map;
+ pool_t pool;
+ unsigned long txid;
+ struct dict_lookup_result res;
+ dict_lookup_callback_t *callback;
+ void *callback_ctx;
+};
+
+struct ldap_dict {
+ struct dict dict;
+ struct dict_ldap_settings *set;
+
+ const char *uri;
+ const char *base_dn;
+ enum ldap_scope scope;
+
+ pool_t pool;
+
+ struct ldap_client *client;
+
+ unsigned long last_txid;
+ unsigned int pending;
+
+ struct ldap_dict *prev,*next;
+};
+
+static
+void ldap_dict_lookup_async(struct dict *dict,
+ const struct dict_op_settings *set, const char *key,
+ dict_lookup_callback_t *callback, void *context);
+
+
+static bool
+dict_ldap_map_match(const struct dict_ldap_map *map, const char *path,
+ ARRAY_TYPE(const_string) *values, size_t *pat_len_r,
+ size_t *path_len_r, bool partial_ok, bool recurse)
+{
+ const char *path_start = path;
+ const char *pat, *attribute, *p;
+ size_t len;
+
+ array_clear(values);
+ pat = map->pattern;
+ while (*pat != '\0' && *path != '\0') {
+ if (*pat == '$') {
+ /* variable */
+ pat++;
+ if (*pat == '\0') {
+ /* pattern ended with this variable,
+ it'll match the rest of the path */
+ len = strlen(path);
+ if (partial_ok) {
+ /* iterating - the last field never
+ matches fully. if there's a trailing
+ '/', drop it. */
+ pat--;
+ if (path[len-1] == '/') {
+ attribute = t_strndup(path, len-1);
+ array_push_back(values,
+ &attribute);
+ } else {
+ array_push_back(values, &path);
+ }
+ } else {
+ array_push_back(values, &path);
+ path += len;
+ }
+ *path_len_r = path - path_start;
+ *pat_len_r = pat - map->pattern;
+ return TRUE;
+ }
+ /* pattern matches until the next '/' in path */
+ p = strchr(path, '/');
+ if (p != NULL) {
+ attribute = t_strdup_until(path, p);
+ array_push_back(values, &attribute);
+ path = p;
+ } else {
+ /* no '/' anymore, but it'll still match a
+ partial */
+ array_push_back(values, &path);
+ path += strlen(path);
+ pat++;
+ }
+ } else if (*pat == *path) {
+ pat++;
+ path++;
+ } else {
+ return FALSE;
+ }
+ }
+
+ *path_len_r = path - path_start;
+ *pat_len_r = pat - map->pattern;
+
+ if (*pat == '\0')
+ return *path == '\0';
+ else if (!partial_ok)
+ return FALSE;
+ else {
+ /* partial matches must end with '/'. */
+ if (pat != map->pattern && pat[-1] != '/')
+ return FALSE;
+ /* if we're not recursing, there should be only one $variable
+ left. */
+ if (recurse)
+ return TRUE;
+ return pat[0] == '$' && strchr(pat, '/') == NULL;
+ }
+}
+
+static const struct dict_ldap_map *
+ldap_dict_find_map(struct ldap_dict *dict, const char *path,
+ ARRAY_TYPE(const_string) *values)
+{
+ const struct dict_ldap_map *maps;
+ unsigned int i, count;
+ size_t len;
+
+ t_array_init(values, dict->set->max_attribute_count);
+ maps = array_get(&dict->set->maps, &count);
+ for (i = 0; i < count; i++) {
+ if (dict_ldap_map_match(&maps[i], path, values,
+ &len, &len, FALSE, FALSE))
+ return &maps[i];
+ }
+ return NULL;
+}
+
+static
+int dict_ldap_connect(struct ldap_dict *dict, const char **error_r)
+{
+ struct ldap_client_settings set;
+ i_zero(&set);
+ set.uri = dict->set->uri;
+ set.bind_dn = dict->set->bind_dn;
+ set.password = dict->set->password;
+ set.timeout_secs = dict->set->timeout;
+ set.max_idle_time_secs = dict->set->max_idle_time;
+ set.debug = dict->set->debug;
+ set.require_ssl = dict->set->require_ssl;
+ set.start_tls = dict->set->start_tls;
+ return ldap_client_init(&set, &dict->client, error_r);
+}
+
+#define IS_LDAP_ESCAPED_CHAR(c) \
+ ((((unsigned char)(c)) & 0x80) != 0 || strchr(LDAP_ESCAPE_CHARS, (c)) != NULL)
+
+static const char *ldap_escape(const char *str)
+{
+ string_t *ret = NULL;
+
+ for (const char *p = str; *p != '\0'; p++) {
+ if (IS_LDAP_ESCAPED_CHAR(*p)) {
+ if (ret == NULL) {
+ ret = t_str_new((size_t) (p - str) + 64);
+ str_append_data(ret, str, (size_t) (p - str));
+ }
+ str_printfa(ret, "\\%02X", (unsigned char)*p);
+ } else if (ret != NULL)
+ str_append_c(ret, *p);
+ }
+
+ return ret == NULL ? str : str_c(ret);
+}
+
+static bool
+ldap_dict_build_query(const struct dict_op_settings *set,
+ const struct dict_ldap_map *map,
+ ARRAY_TYPE(const_string) *values, bool priv,
+ string_t *query_r, const char **error_r)
+{
+ const char *template, *error;
+ ARRAY(struct var_expand_table) exp;
+ struct var_expand_table entry;
+
+ t_array_init(&exp, 8);
+ entry.key = '\0';
+ entry.value = ldap_escape(set->username);
+ entry.long_key = "username";
+ array_push_back(&exp, &entry);
+
+ if (priv) {
+ template = t_strdup_printf("(&(%s=%s)%s)", map->username_attribute, "%{username}", map->filter);
+ } else {
+ template = map->filter;
+ }
+
+ for(size_t i = 0; i < array_count(values) && i < array_count(&map->ldap_attributes); i++) {
+ struct var_expand_table entry;
+ const char *value = array_idx_elem(values, i);
+ const char *long_key = array_idx_elem(&map->ldap_attributes, i);
+
+ entry.value = ldap_escape(value);
+ entry.long_key = long_key;
+ array_push_back(&exp, &entry);
+ }
+
+ array_append_zero(&exp);
+
+ if (var_expand(query_r, template, array_front(&exp), &error) <= 0) {
+ *error_r = t_strdup_printf("Failed to expand %s: %s", template, error);
+ return FALSE;
+ }
+ return TRUE;
+}
+
+static
+int ldap_dict_init(struct dict *dict_driver, const char *uri,
+ const struct dict_settings *set ATTR_UNUSED,
+ struct dict **dict_r, const char **error_r)
+{
+ pool_t pool = pool_alloconly_create("ldap dict", 2048);
+ struct ldap_dict *dict = p_new(pool, struct ldap_dict, 1);
+ dict->pool = pool;
+ dict->dict = *dict_driver;
+ dict->uri = p_strdup(pool, uri);
+ dict->set = dict_ldap_settings_read(pool, uri, error_r);
+
+ if (dict->set == NULL) {
+ pool_unref(&pool);
+ return -1;
+ }
+
+ if (dict_ldap_connect(dict, error_r) < 0) {
+ pool_unref(&pool);
+ return -1;
+ }
+
+ *dict_r = (struct dict*)dict;
+ *error_r = NULL;
+ return 0;
+}
+
+static
+void ldap_dict_deinit(struct dict *dict)
+{
+ struct ldap_dict *ctx = (struct ldap_dict *)dict;
+
+ ldap_client_deinit(&ctx->client);
+ pool_unref(&ctx->pool);
+}
+
+static void ldap_dict_wait(struct dict *dict)
+{
+ struct ldap_dict *ctx = (struct ldap_dict *)dict;
+
+ i_assert(ctx->dict.ioloop == NULL);
+
+ ctx->dict.prev_ioloop = current_ioloop;
+ ctx->dict.ioloop = io_loop_create();
+ dict_switch_ioloop(dict);
+
+ do {
+ io_loop_run(current_ioloop);
+ } while (ctx->pending > 0);
+
+ io_loop_set_current(ctx->dict.prev_ioloop);
+ dict_switch_ioloop(dict);
+ io_loop_set_current(ctx->dict.ioloop);
+ io_loop_destroy(&ctx->dict.ioloop);
+ ctx->dict.prev_ioloop = NULL;
+}
+
+static bool ldap_dict_switch_ioloop(struct dict *dict)
+{
+ struct ldap_dict *ctx = (struct ldap_dict *)dict;
+
+ ldap_client_switch_ioloop(ctx->client);
+ return ctx->pending > 0;
+}
+
+static
+void ldap_dict_lookup_done(const struct dict_lookup_result *result, void *ctx)
+{
+ struct dict_lookup_result *res = ctx;
+ res->ret = result->ret;
+ res->value = t_strdup(result->value);
+ res->error = t_strdup(result->error);
+}
+
+static void
+ldap_dict_lookup_callback(struct ldap_result *result, struct dict_ldap_op *op)
+{
+ pool_t pool = op->pool;
+ struct ldap_search_iterator *iter;
+ const struct ldap_entry *entry;
+
+ op->dict->pending--;
+
+ if (ldap_result_has_failed(result)) {
+ op->res.ret = -1;
+ op->res.error = ldap_result_get_error(result);
+ } else {
+ iter = ldap_search_iterator_init(result);
+ entry = ldap_search_iterator_next(iter);
+ if (entry != NULL) {
+ if (op->dict->set->debug > 0)
+ i_debug("ldap_dict_lookup_callback got dn %s", ldap_entry_dn(entry));
+ /* try extract value */
+ const char *const *values = ldap_entry_get_attribute(entry, op->map->value_attribute);
+ if (values != NULL) {
+ const char **new_values;
+
+ if (op->dict->set->debug > 0)
+ i_debug("ldap_dict_lookup_callback got attribute %s", op->map->value_attribute);
+ op->res.ret = 1;
+ new_values = p_new(op->pool, const char *, 2);
+ new_values[0] = p_strdup(op->pool, values[0]);
+ op->res.values = new_values;
+ op->res.value = op->res.values[0];
+ } else {
+ if (op->dict->set->debug > 0)
+ i_debug("ldap_dict_lookup_callback dit not get attribute %s", op->map->value_attribute);
+ op->res.value = NULL;
+ }
+ }
+ ldap_search_iterator_deinit(&iter);
+ }
+ if (op->dict->dict.prev_ioloop != NULL)
+ io_loop_set_current(op->dict->dict.prev_ioloop);
+ op->callback(&op->res, op->callback_ctx);
+ if (op->dict->dict.prev_ioloop != NULL) {
+ io_loop_set_current(op->dict->dict.ioloop);
+ io_loop_stop(op->dict->dict.ioloop);
+ }
+ pool_unref(&pool);
+}
+
+static int
+ldap_dict_lookup(struct dict *dict, const struct dict_op_settings *set,
+ pool_t pool, const char *key,
+ const char **value_r, const char **error_r)
+{
+ struct dict_lookup_result res;
+
+ ldap_dict_lookup_async(dict, set, key, ldap_dict_lookup_done, &res);
+
+ ldap_dict_wait(dict);
+ if (res.ret < 0) {
+ *error_r = res.error;
+ return -1;
+ }
+ if (res.ret > 0)
+ *value_r = p_strdup(pool, res.value);
+ return res.ret;
+}
+
+/*
+static
+struct dict_iterate_context *ldap_dict_iterate_init(struct dict *dict,
+ const char *const *paths,
+ enum dict_iterate_flags flags)
+{
+ return NULL;
+}
+
+static
+bool ldap_dict_iterate(struct dict_iterate_context *ctx,
+ const char **key_r, const char **value_r)
+{
+ return FALSE;
+}
+
+static
+int ldap_dict_iterate_deinit(struct dict_iterate_context *ctx)
+{
+ return -1;
+}
+
+static
+struct dict_transaction_context ldap_dict_transaction_init(struct dict *dict);
+
+static
+int ldap_dict_transaction_commit(struct dict_transaction_context *ctx,
+ bool async,
+ dict_transaction_commit_callback_t *callback,
+ void *context);
+static
+void ldap_dict_transaction_rollback(struct dict_transaction_context *ctx);
+
+static
+void ldap_dict_set(struct dict_transaction_context *ctx,
+ const char *key, const char *value);
+static
+void ldap_dict_unset(struct dict_transaction_context *ctx,
+ const char *key);
+static
+void ldap_dict_atomic_inc(struct dict_transaction_context *ctx,
+ const char *key, long long diff);
+*/
+
+static
+void ldap_dict_lookup_async(struct dict *dict,
+ const struct dict_op_settings *set,
+ const char *key,
+ dict_lookup_callback_t *callback, void *context)
+{
+ struct ldap_search_input input;
+ struct ldap_dict *ctx = (struct ldap_dict*)dict;
+ struct dict_ldap_op *op;
+ const char *error;
+
+ pool_t oppool = pool_alloconly_create("ldap dict lookup", 64);
+ string_t *query = str_new(oppool, 64);
+ op = p_new(oppool, struct dict_ldap_op, 1);
+ op->pool = oppool;
+ op->dict = ctx;
+ op->callback = callback;
+ op->callback_ctx = context;
+ op->txid = ctx->last_txid++;
+
+ /* key needs to be transformed into something else */
+ ARRAY_TYPE(const_string) values;
+ const char *attributes[2] = {0, 0};
+ t_array_init(&values, 8);
+ const struct dict_ldap_map *map = ldap_dict_find_map(ctx, key, &values);
+
+ if (map != NULL) {
+ op->map = map;
+ attributes[0] = map->value_attribute;
+ /* build lookup */
+ i_zero(&input);
+ input.base_dn = map->base_dn;
+ input.scope = map->scope_val;
+ if (!ldap_dict_build_query(set, map, &values, strncmp(key, DICT_PATH_PRIVATE, strlen(DICT_PATH_PRIVATE))==0, query, &error)) {
+ op->res.error = error;
+ callback(&op->res, context);
+ pool_unref(&oppool);
+ return;
+ }
+ input.filter = str_c(query);
+ input.attributes = attributes;
+ input.timeout_secs = ctx->set->timeout;
+ ctx->pending++;
+ ldap_search_start(ctx->client, &input, ldap_dict_lookup_callback, op);
+ } else {
+ op->res.error = "no such key";
+ callback(&op->res, context);
+ pool_unref(&oppool);
+ }
+}
+
+struct dict dict_driver_ldap = {
+ .name = "ldap",
+ {
+ .init = ldap_dict_init,
+ .deinit = ldap_dict_deinit,
+ .wait = ldap_dict_wait,
+ .lookup = ldap_dict_lookup,
+ .lookup_async = ldap_dict_lookup_async,
+ .switch_ioloop = ldap_dict_switch_ioloop,
+ }
+};
+
+#ifndef BUILTIN_LDAP
+/* Building a plugin */
+void dict_ldap_init(struct module *module ATTR_UNUSED);
+void dict_ldap_deinit(void);
+
+void dict_ldap_init(struct module *module ATTR_UNUSED)
+{
+ dict_driver_register(&dict_driver_ldap);
+}
+
+void dict_ldap_deinit(void)
+{
+ ldap_clients_cleanup();
+ dict_driver_unregister(&dict_driver_ldap);
+}
+
+const char *dict_ldap_plugin_dependencies[] = { NULL };
+#endif
+
+#endif
diff --git a/src/lib-dict-backend/dict-sql-private.h b/src/lib-dict-backend/dict-sql-private.h
new file mode 100644
index 0000000..11e9dfc
--- /dev/null
+++ b/src/lib-dict-backend/dict-sql-private.h
@@ -0,0 +1,12 @@
+#ifndef DICT_SQL_PRIVATE_H
+#define DICT_SQL_PRIVATE_H 1
+
+struct sql_dict {
+ struct dict dict;
+
+ pool_t pool;
+ struct sql_db *db;
+ const struct dict_sql_settings *set;
+};
+
+#endif
diff --git a/src/lib-dict-backend/dict-sql-settings.c b/src/lib-dict-backend/dict-sql-settings.c
new file mode 100644
index 0000000..0d44923
--- /dev/null
+++ b/src/lib-dict-backend/dict-sql-settings.c
@@ -0,0 +1,345 @@
+/* Copyright (c) 2008-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "str.h"
+#include "hash.h"
+#include "settings.h"
+#include "dict-sql-settings.h"
+
+#include <ctype.h>
+
+enum section_type {
+ SECTION_ROOT = 0,
+ SECTION_MAP,
+ SECTION_FIELDS
+};
+
+struct dict_sql_map_field {
+ struct dict_sql_field sql_field;
+ const char *variable;
+};
+
+struct setting_parser_ctx {
+ pool_t pool;
+ struct dict_sql_settings *set;
+ enum section_type type;
+
+ struct dict_sql_map cur_map;
+ ARRAY(struct dict_sql_map_field) cur_fields;
+};
+
+#define DEF_STR(name) DEF_STRUCT_STR(name, dict_sql_map)
+#define DEF_BOOL(name) DEF_STRUCT_BOOL(name, dict_sql_map)
+
+static const struct setting_def dict_sql_map_setting_defs[] = {
+ DEF_STR(pattern),
+ DEF_STR(table),
+ DEF_STR(username_field),
+ DEF_STR(value_field),
+ DEF_STR(value_type),
+ DEF_BOOL(value_hexblob),
+
+ { 0, NULL, 0 }
+};
+
+struct dict_sql_settings_cache {
+ pool_t pool;
+ const char *path;
+ struct dict_sql_settings *set;
+};
+
+static HASH_TABLE(const char *, struct dict_sql_settings_cache *) dict_sql_settings_cache;
+
+static const char *pattern_read_name(const char **pattern)
+{
+ const char *p = *pattern, *name;
+
+ if (*p == '{') {
+ /* ${name} */
+ name = ++p;
+ p = strchr(p, '}');
+ if (p == NULL) {
+ /* error, but allow anyway */
+ *pattern += strlen(*pattern);
+ return "";
+ }
+ *pattern = p + 1;
+ } else {
+ /* $name - ends at the first non-alnum_ character */
+ name = p;
+ for (; *p != '\0'; p++) {
+ if (!i_isalnum(*p) && *p != '_')
+ break;
+ }
+ *pattern = p;
+ }
+ name = t_strdup_until(name, p);
+ return name;
+}
+
+static const char *dict_sql_fields_map(struct setting_parser_ctx *ctx)
+{
+ struct dict_sql_map_field *fields;
+ string_t *pattern;
+ const char *p, *name;
+ unsigned int i, count;
+
+ /* go through the variables in the pattern, replace them with plain
+ '$' character and add its sql field */
+ pattern = t_str_new(strlen(ctx->cur_map.pattern) + 1);
+ fields = array_get_modifiable(&ctx->cur_fields, &count);
+
+ p_array_init(&ctx->cur_map.pattern_fields, ctx->pool, count);
+ for (p = ctx->cur_map.pattern; *p != '\0';) {
+ if (*p != '$') {
+ str_append_c(pattern, *p);
+ p++;
+ continue;
+ }
+ p++;
+ str_append_c(pattern, '$');
+
+ name = pattern_read_name(&p);
+ for (i = 0; i < count; i++) {
+ if (fields[i].variable != NULL &&
+ strcmp(fields[i].variable, name) == 0)
+ break;
+ }
+ if (i == count) {
+ return t_strconcat("Missing SQL field for variable: ",
+ name, NULL);
+ }
+
+ /* mark this field as used */
+ fields[i].variable = NULL;
+ array_push_back(&ctx->cur_map.pattern_fields,
+ &fields[i].sql_field);
+ }
+
+ /* make sure there aren't any unused fields */
+ for (i = 0; i < count; i++) {
+ if (fields[i].variable != NULL) {
+ return t_strconcat("Unused variable: ",
+ fields[i].variable, NULL);
+ }
+ }
+
+ if (ctx->set->max_pattern_fields_count < count)
+ ctx->set->max_pattern_fields_count = count;
+ ctx->cur_map.pattern = p_strdup(ctx->pool, str_c(pattern));
+ return NULL;
+}
+
+static bool
+dict_sql_value_type_parse(const char *value_type, enum dict_sql_type *type_r)
+{
+ if (strcmp(value_type, "string") == 0)
+ *type_r = DICT_SQL_TYPE_STRING;
+ else if (strcmp(value_type, "hexblob") == 0)
+ *type_r = DICT_SQL_TYPE_HEXBLOB;
+ else if (strcmp(value_type, "int") == 0)
+ *type_r = DICT_SQL_TYPE_INT;
+ else if (strcmp(value_type, "uint") == 0)
+ *type_r = DICT_SQL_TYPE_UINT;
+ else
+ return FALSE;
+ return TRUE;
+}
+
+static const char *dict_sql_map_finish(struct setting_parser_ctx *ctx)
+{
+ unsigned int i;
+
+ if (ctx->cur_map.pattern == NULL)
+ return "Missing setting: pattern";
+ if (ctx->cur_map.table == NULL)
+ return "Missing setting: table";
+ if (ctx->cur_map.value_field == NULL)
+ return "Missing setting: value_field";
+
+ ctx->cur_map.value_fields = (const char *const *)
+ p_strsplit_spaces(ctx->pool, ctx->cur_map.value_field, ",");
+ ctx->cur_map.values_count = str_array_length(ctx->cur_map.value_fields);
+
+ enum dict_sql_type *value_types =
+ p_new(ctx->pool, enum dict_sql_type, ctx->cur_map.values_count);
+ if (ctx->cur_map.value_type != NULL) {
+ const char *const *types =
+ t_strsplit_spaces(ctx->cur_map.value_type, ",");
+ if (str_array_length(types) != ctx->cur_map.values_count)
+ return "Number of fields in value_fields doesn't match value_type";
+ for (i = 0; i < ctx->cur_map.values_count; i++) {
+ if (!dict_sql_value_type_parse(types[i], &value_types[i]))
+ return "Invalid value in value_type";
+ }
+ } else {
+ for (i = 0; i < ctx->cur_map.values_count; i++) {
+ value_types[i] = ctx->cur_map.value_hexblob ?
+ DICT_SQL_TYPE_HEXBLOB : DICT_SQL_TYPE_STRING;
+ }
+ }
+ ctx->cur_map.value_types = value_types;
+
+ if (ctx->cur_map.username_field == NULL) {
+ /* not all queries require this */
+ ctx->cur_map.username_field = "'username_field not set'";
+ }
+
+ if (!array_is_created(&ctx->cur_map.pattern_fields)) {
+ /* no fields besides value. allocate the array anyway. */
+ p_array_init(&ctx->cur_map.pattern_fields, ctx->pool, 1);
+ if (strchr(ctx->cur_map.pattern, '$') != NULL)
+ return "Missing fields for pattern variables";
+ }
+ array_push_back(&ctx->set->maps, &ctx->cur_map);
+ i_zero(&ctx->cur_map);
+ return NULL;
+}
+
+static const char *
+parse_setting(const char *key, const char *value,
+ struct setting_parser_ctx *ctx)
+{
+ struct dict_sql_map_field *field;
+ size_t value_len;
+
+ switch (ctx->type) {
+ case SECTION_ROOT:
+ if (strcmp(key, "connect") == 0) {
+ ctx->set->connect = p_strdup(ctx->pool, value);
+ return NULL;
+ }
+ break;
+ case SECTION_MAP:
+ return parse_setting_from_defs(ctx->pool,
+ dict_sql_map_setting_defs,
+ &ctx->cur_map, key, value);
+ case SECTION_FIELDS:
+ if (*value != '$') {
+ return t_strconcat("Value is missing '$' for field: ",
+ key, NULL);
+ }
+ field = array_append_space(&ctx->cur_fields);
+ field->sql_field.name = p_strdup(ctx->pool, key);
+ value_len = strlen(value);
+ if (str_begins(value, "${hexblob:") &&
+ value[value_len-1] == '}') {
+ field->variable = p_strndup(ctx->pool, value + 10,
+ value_len-10-1);
+ field->sql_field.value_type = DICT_SQL_TYPE_HEXBLOB;
+ } else if (str_begins(value, "${int:") &&
+ value[value_len-1] == '}') {
+ field->variable = p_strndup(ctx->pool, value + 6,
+ value_len-6-1);
+ field->sql_field.value_type = DICT_SQL_TYPE_INT;
+ } else if (str_begins(value, "${uint:") &&
+ value[value_len-1] == '}') {
+ field->variable = p_strndup(ctx->pool, value + 7,
+ value_len-7-1);
+ field->sql_field.value_type = DICT_SQL_TYPE_UINT;
+ } else {
+ field->variable = p_strdup(ctx->pool, value + 1);
+ }
+ return NULL;
+ }
+ return t_strconcat("Unknown setting: ", key, NULL);
+}
+
+static bool
+parse_section(const char *type, const char *name ATTR_UNUSED,
+ struct setting_parser_ctx *ctx, const char **error_r)
+{
+ switch (ctx->type) {
+ case SECTION_ROOT:
+ if (type == NULL)
+ return FALSE;
+ if (strcmp(type, "map") == 0) {
+ array_clear(&ctx->cur_fields);
+ ctx->type = SECTION_MAP;
+ return TRUE;
+ }
+ break;
+ case SECTION_MAP:
+ if (type == NULL) {
+ ctx->type = SECTION_ROOT;
+ *error_r = dict_sql_map_finish(ctx);
+ return FALSE;
+ }
+ if (strcmp(type, "fields") == 0) {
+ ctx->type = SECTION_FIELDS;
+ return TRUE;
+ }
+ break;
+ case SECTION_FIELDS:
+ if (type == NULL) {
+ ctx->type = SECTION_MAP;
+ *error_r = dict_sql_fields_map(ctx);
+ return FALSE;
+ }
+ break;
+ }
+ *error_r = t_strconcat("Unknown section: ", type, NULL);
+ return FALSE;
+}
+
+struct dict_sql_settings *
+dict_sql_settings_read(const char *path, const char **error_r)
+{
+ struct setting_parser_ctx ctx;
+ struct dict_sql_settings_cache *cache;
+ pool_t pool;
+
+ if (!hash_table_is_created(dict_sql_settings_cache)) {
+ hash_table_create(&dict_sql_settings_cache, default_pool, 0,
+ str_hash, strcmp);
+ }
+
+ cache = hash_table_lookup(dict_sql_settings_cache, path);
+ if (cache != NULL)
+ return cache->set;
+
+ i_zero(&ctx);
+ pool = pool_alloconly_create("dict sql settings", 1024);
+ ctx.pool = pool;
+ ctx.set = p_new(pool, struct dict_sql_settings, 1);
+ t_array_init(&ctx.cur_fields, 16);
+ p_array_init(&ctx.set->maps, pool, 8);
+
+ if (!settings_read(path, NULL, parse_setting, parse_section,
+ &ctx, error_r)) {
+ pool_unref(&pool);
+ return NULL;
+ }
+
+ if (ctx.set->connect == NULL) {
+ *error_r = t_strdup_printf("Error in configuration file %s: "
+ "Missing connect setting", path);
+ pool_unref(&pool);
+ return NULL;
+ }
+
+ cache = p_new(pool, struct dict_sql_settings_cache, 1);
+ cache->pool = pool;
+ cache->path = p_strdup(pool, path);
+ cache->set = ctx.set;
+
+ hash_table_insert(dict_sql_settings_cache, cache->path, cache);
+ return ctx.set;
+}
+
+void dict_sql_settings_deinit(void)
+{
+ struct hash_iterate_context *iter;
+ struct dict_sql_settings_cache *cache;
+ const char *key;
+
+ if (!hash_table_is_created(dict_sql_settings_cache))
+ return;
+
+ iter = hash_table_iterate_init(dict_sql_settings_cache);
+ while (hash_table_iterate(iter, dict_sql_settings_cache, &key, &cache))
+ pool_unref(&cache->pool);
+ hash_table_iterate_deinit(&iter);
+ hash_table_destroy(&dict_sql_settings_cache);
+}
diff --git a/src/lib-dict-backend/dict-sql-settings.h b/src/lib-dict-backend/dict-sql-settings.h
new file mode 100644
index 0000000..c9ee037
--- /dev/null
+++ b/src/lib-dict-backend/dict-sql-settings.h
@@ -0,0 +1,47 @@
+#ifndef DICT_SQL_SETTINGS_H
+#define DICT_SQL_SETTINGS_H
+
+enum dict_sql_type {
+ DICT_SQL_TYPE_STRING = 0,
+ DICT_SQL_TYPE_INT,
+ DICT_SQL_TYPE_UINT,
+ DICT_SQL_TYPE_HEXBLOB
+};
+
+struct dict_sql_field {
+ const char *name;
+ enum dict_sql_type value_type;
+};
+
+struct dict_sql_map {
+ /* pattern is in simplified form: all variables are stored as simple
+ '$' character. fields array is sorted by the variable index. */
+ const char *pattern;
+ const char *table;
+ const char *username_field;
+ const char *value_field;
+ const char *value_type;
+ bool value_hexblob;
+
+ /* SQL field names, one for each $ variable in the pattern */
+ ARRAY(struct dict_sql_field) pattern_fields;
+
+ /* generated: */
+ unsigned int values_count;
+ const char *const *value_fields;
+ const enum dict_sql_type *value_types;
+};
+
+struct dict_sql_settings {
+ const char *connect;
+
+ unsigned int max_pattern_fields_count;
+ ARRAY(struct dict_sql_map) maps;
+};
+
+struct dict_sql_settings *
+dict_sql_settings_read(const char *path, const char **error_r);
+
+void dict_sql_settings_deinit(void);
+
+#endif
diff --git a/src/lib-dict-backend/dict-sql.c b/src/lib-dict-backend/dict-sql.c
new file mode 100644
index 0000000..c44330b
--- /dev/null
+++ b/src/lib-dict-backend/dict-sql.c
@@ -0,0 +1,1564 @@
+/* Copyright (c) 2005-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "istream.h"
+#include "hex-binary.h"
+#include "hash.h"
+#include "str.h"
+#include "sql-api-private.h"
+#include "sql-db-cache.h"
+#include "dict-private.h"
+#include "dict-sql-settings.h"
+#include "dict-sql.h"
+#include "dict-sql-private.h"
+
+#include <unistd.h>
+#include <fcntl.h>
+
+#define DICT_SQL_MAX_UNUSED_CONNECTIONS 10
+
+enum sql_recurse_type {
+ SQL_DICT_RECURSE_NONE,
+ SQL_DICT_RECURSE_ONE,
+ SQL_DICT_RECURSE_FULL
+};
+
+struct sql_dict_param {
+ enum dict_sql_type value_type;
+
+ const char *value_str;
+ int64_t value_int64;
+ const void *value_binary;
+ size_t value_binary_size;
+};
+ARRAY_DEFINE_TYPE(sql_dict_param, struct sql_dict_param);
+
+struct sql_dict_iterate_context {
+ struct dict_iterate_context ctx;
+ pool_t pool;
+
+ enum dict_iterate_flags flags;
+ const char *path;
+
+ struct sql_result *result;
+ string_t *key;
+ const struct dict_sql_map *map;
+ size_t key_prefix_len, pattern_prefix_len;
+ unsigned int sql_fields_start_idx, next_map_idx;
+ bool destroyed;
+ bool synchronous_result;
+ bool iter_query_sent;
+ bool allow_null_map; /* allow next map to be NULL */
+ const char *error;
+};
+
+struct sql_dict_inc_row {
+ struct sql_dict_inc_row *prev;
+ unsigned int rows;
+};
+
+struct sql_dict_prev {
+ const struct dict_sql_map *map;
+ char *key;
+ union {
+ char *str;
+ long long diff;
+ } value;
+};
+
+struct sql_dict_transaction_context {
+ struct dict_transaction_context ctx;
+
+ struct sql_transaction_context *sql_ctx;
+
+ pool_t inc_row_pool;
+ struct sql_dict_inc_row *inc_row;
+
+ ARRAY(struct sql_dict_prev) prev_inc;
+ ARRAY(struct sql_dict_prev) prev_set;
+
+ dict_transaction_commit_callback_t *async_callback;
+ void *async_context;
+
+ char *error;
+};
+
+static struct sql_db_cache *dict_sql_db_cache;
+
+static void sql_dict_prev_inc_flush(struct sql_dict_transaction_context *ctx);
+static void sql_dict_prev_set_flush(struct sql_dict_transaction_context *ctx);
+static void sql_dict_prev_inc_free(struct sql_dict_transaction_context *ctx);
+static void sql_dict_prev_set_free(struct sql_dict_transaction_context *ctx);
+
+static int
+sql_dict_init(struct dict *driver, const char *uri,
+ const struct dict_settings *set,
+ struct dict **dict_r, const char **error_r)
+{
+ struct sql_settings sql_set;
+ struct sql_dict *dict;
+ pool_t pool;
+
+ pool = pool_alloconly_create("sql dict", 2048);
+ dict = p_new(pool, struct sql_dict, 1);
+ dict->pool = pool;
+ dict->dict = *driver;
+ dict->set = dict_sql_settings_read(uri, error_r);
+ if (dict->set == NULL) {
+ pool_unref(&pool);
+ return -1;
+ }
+ i_zero(&sql_set);
+ sql_set.driver = driver->name;
+ sql_set.connect_string = dict->set->connect;
+ sql_set.event_parent = set->event_parent;
+
+ if (sql_db_cache_new(dict_sql_db_cache, &sql_set, &dict->db, error_r) < 0) {
+ pool_unref(&pool);
+ return -1;
+ }
+
+ *dict_r = &dict->dict;
+ return 0;
+}
+
+static void sql_dict_deinit(struct dict *_dict)
+{
+ struct sql_dict *dict = (struct sql_dict *)_dict;
+
+ sql_unref(&dict->db);
+ pool_unref(&dict->pool);
+}
+
+static void sql_dict_wait(struct dict *_dict)
+{
+ struct sql_dict *dict = (struct sql_dict *)_dict;
+ sql_wait(dict->db);
+}
+
+/* Try to match path to map->pattern. For example pattern="shared/x/$/$/y"
+ and path="shared/x/1/2/y", this is match and pattern_values=[1, 2]. */
+static bool
+dict_sql_map_match(const struct dict_sql_map *map, const char *path,
+ ARRAY_TYPE(const_string) *pattern_values, size_t *pat_len_r,
+ size_t *path_len_r, bool partial_ok, bool recurse)
+{
+ const char *path_start = path;
+ const char *pat, *field, *p;
+ size_t len;
+
+ array_clear(pattern_values);
+ pat = map->pattern;
+ while (*pat != '\0' && *path != '\0') {
+ if (*pat == '$') {
+ /* variable */
+ pat++;
+ if (*pat == '\0') {
+ /* pattern ended with this variable,
+ it'll match the rest of the path */
+ len = strlen(path);
+ if (partial_ok) {
+ /* iterating - the last field never
+ matches fully. if there's a trailing
+ '/', drop it. */
+ pat--;
+ if (path[len-1] == '/') {
+ field = t_strndup(path, len-1);
+ array_push_back(pattern_values,
+ &field);
+ } else {
+ array_push_back(pattern_values,
+ &path);
+ }
+ } else {
+ array_push_back(pattern_values, &path);
+ path += len;
+ }
+ *path_len_r = path - path_start;
+ *pat_len_r = pat - map->pattern;
+ return TRUE;
+ }
+ /* pattern matches until the next '/' in path */
+ p = strchr(path, '/');
+ if (p != NULL) {
+ field = t_strdup_until(path, p);
+ array_push_back(pattern_values, &field);
+ path = p;
+ } else {
+ /* no '/' anymore, but it'll still match a
+ partial */
+ array_push_back(pattern_values, &path);
+ path += strlen(path);
+ pat++;
+ }
+ } else if (*pat == *path) {
+ pat++;
+ path++;
+ } else {
+ return FALSE;
+ }
+ }
+
+ *path_len_r = path - path_start;
+ *pat_len_r = pat - map->pattern;
+
+ if (*pat == '\0')
+ return *path == '\0';
+ else if (!partial_ok)
+ return FALSE;
+ else {
+ /* partial matches must end with '/'. */
+ if (pat != map->pattern && pat[-1] != '/')
+ return FALSE;
+ /* if we're not recursing, there should be only one $variable
+ left. */
+ if (recurse)
+ return TRUE;
+ return pat[0] == '$' && strchr(pat, '/') == NULL;
+ }
+}
+
+static const struct dict_sql_map *
+sql_dict_find_map(struct sql_dict *dict, const char *path,
+ ARRAY_TYPE(const_string) *pattern_values)
+{
+ const struct dict_sql_map *maps;
+ unsigned int i, count;
+ size_t len;
+
+ t_array_init(pattern_values, dict->set->max_pattern_fields_count);
+ maps = array_get(&dict->set->maps, &count);
+ for (i = 0; i < count; i++) {
+ if (dict_sql_map_match(&maps[i], path, pattern_values,
+ &len, &len, FALSE, FALSE))
+ return &maps[i];
+ }
+ return NULL;
+}
+
+static void
+sql_dict_statement_bind(struct sql_statement *stmt, unsigned int column_idx,
+ const struct sql_dict_param *param)
+{
+ switch (param->value_type) {
+ case DICT_SQL_TYPE_STRING:
+ sql_statement_bind_str(stmt, column_idx, param->value_str);
+ break;
+ case DICT_SQL_TYPE_INT:
+ case DICT_SQL_TYPE_UINT:
+ sql_statement_bind_int64(stmt, column_idx, param->value_int64);
+ break;
+ case DICT_SQL_TYPE_HEXBLOB:
+ sql_statement_bind_binary(stmt, column_idx, param->value_binary,
+ param->value_binary_size);
+ break;
+ }
+}
+
+static struct sql_statement *
+sql_dict_statement_init(struct sql_dict *dict, const char *query,
+ const ARRAY_TYPE(sql_dict_param) *params)
+{
+ struct sql_statement *stmt;
+ struct sql_prepared_statement *prep_stmt;
+ const struct sql_dict_param *param;
+
+ if ((sql_get_flags(dict->db) & SQL_DB_FLAG_PREP_STATEMENTS) != 0) {
+ prep_stmt = sql_prepared_statement_init(dict->db, query);
+ stmt = sql_statement_init_prepared(prep_stmt);
+ sql_prepared_statement_unref(&prep_stmt);
+ } else {
+ /* Prepared statements not supported by the backend.
+ Just use regular statements to avoid wasting memory. */
+ stmt = sql_statement_init(dict->db, query);
+ }
+
+ array_foreach(params, param) {
+ sql_dict_statement_bind(stmt, array_foreach_idx(params, param),
+ param);
+ }
+ return stmt;
+}
+
+static int
+sql_dict_value_get(const struct dict_sql_map *map,
+ enum dict_sql_type value_type, const char *field_name,
+ const char *value, const char *value_suffix,
+ ARRAY_TYPE(sql_dict_param) *params, const char **error_r)
+{
+ struct sql_dict_param *param;
+ buffer_t *buf;
+
+ param = array_append_space(params);
+ param->value_type = value_type;
+
+ switch (value_type) {
+ case DICT_SQL_TYPE_STRING:
+ if (value_suffix[0] != '\0')
+ value = t_strconcat(value, value_suffix, NULL);
+ param->value_str = value;
+ return 0;
+ case DICT_SQL_TYPE_INT:
+ if (value_suffix[0] != '\0' ||
+ str_to_int64(value, &param->value_int64) < 0) {
+ *error_r = t_strdup_printf(
+ "%s field's value isn't 64bit signed integer: %s%s (in pattern: %s)",
+ field_name, value, value_suffix, map->pattern);
+ return -1;
+ }
+ return 0;
+ case DICT_SQL_TYPE_UINT:
+ if (value_suffix[0] != '\0' || value[0] == '-' ||
+ str_to_int64(value, &param->value_int64) < 0) {
+ *error_r = t_strdup_printf(
+ "%s field's value isn't 64bit unsigned integer: %s%s (in pattern: %s)",
+ field_name, value, value_suffix, map->pattern);
+ return -1;
+ }
+ return 0;
+ case DICT_SQL_TYPE_HEXBLOB:
+ break;
+ }
+
+ buf = t_buffer_create(strlen(value)/2);
+ if (hex_to_binary(value, buf) < 0) {
+ /* we shouldn't get untrusted input here. it's also a bit
+ annoying to handle this error. */
+ *error_r = t_strdup_printf("%s field's value isn't hexblob: %s (in pattern: %s)",
+ field_name, value, map->pattern);
+ return -1;
+ }
+ str_append(buf, value_suffix);
+ param->value_binary = buf->data;
+ param->value_binary_size = buf->used;
+ return 0;
+}
+
+static int
+sql_dict_field_get_value(const struct dict_sql_map *map,
+ const struct dict_sql_field *field,
+ const char *value, const char *value_suffix,
+ ARRAY_TYPE(sql_dict_param) *params,
+ const char **error_r)
+{
+ return sql_dict_value_get(map, field->value_type, field->name,
+ value, value_suffix, params, error_r);
+}
+
+static int
+sql_dict_where_build(const char *username, const struct dict_sql_map *map,
+ const ARRAY_TYPE(const_string) *values_arr,
+ bool add_username, enum sql_recurse_type recurse_type,
+ string_t *query, ARRAY_TYPE(sql_dict_param) *params,
+ const char **error_r)
+{
+ const struct dict_sql_field *pattern_fields;
+ const char *const *pattern_values;
+ unsigned int i, count, count2, exact_count;
+
+ pattern_fields = array_get(&map->pattern_fields, &count);
+ pattern_values = array_get(values_arr, &count2);
+ /* if we came here from iteration code there may be fewer
+ pattern_values */
+ i_assert(count2 <= count);
+
+ if (count2 == 0 && !add_username) {
+ /* we want everything */
+ return 0;
+ }
+
+ str_append(query, " WHERE");
+ exact_count = count == count2 && recurse_type != SQL_DICT_RECURSE_NONE ?
+ count2-1 : count2;
+ if (exact_count != array_count(values_arr)) {
+ *error_r = t_strdup_printf("Key continues past the matched pattern %s", map->pattern);
+ return -1;
+ }
+
+ for (i = 0; i < exact_count; i++) {
+ if (i > 0)
+ str_append(query, " AND");
+ str_printfa(query, " %s = ?", pattern_fields[i].name);
+ if (sql_dict_field_get_value(map, &pattern_fields[i],
+ pattern_values[i], "",
+ params, error_r) < 0)
+ return -1;
+ }
+ switch (recurse_type) {
+ case SQL_DICT_RECURSE_NONE:
+ break;
+ case SQL_DICT_RECURSE_ONE:
+ if (i > 0)
+ str_append(query, " AND");
+ if (i < count2) {
+ str_printfa(query, " %s LIKE ?", pattern_fields[i].name);
+ if (sql_dict_field_get_value(map, &pattern_fields[i],
+ pattern_values[i], "/%",
+ params, error_r) < 0)
+ return -1;
+ str_printfa(query, " AND %s NOT LIKE ?", pattern_fields[i].name);
+ if (sql_dict_field_get_value(map, &pattern_fields[i],
+ pattern_values[i], "/%/%",
+ params, error_r) < 0)
+ return -1;
+ } else {
+ str_printfa(query, " %s LIKE '%%' AND "
+ "%s NOT LIKE '%%/%%'",
+ pattern_fields[i].name,
+ pattern_fields[i].name);
+ }
+ break;
+ case SQL_DICT_RECURSE_FULL:
+ if (i < count2) {
+ if (i > 0)
+ str_append(query, " AND");
+ str_printfa(query, " %s LIKE ",
+ pattern_fields[i].name);
+ if (sql_dict_field_get_value(map, &pattern_fields[i],
+ pattern_values[i], "/%",
+ params, error_r) < 0)
+ return -1;
+ }
+ break;
+ }
+ if (add_username) {
+ struct sql_dict_param *param = array_append_space(params);
+ if (count2 > 0)
+ str_append(query, " AND");
+ str_printfa(query, " %s = ?", map->username_field);
+ param->value_type = DICT_SQL_TYPE_STRING;
+ param->value_str = t_strdup(username);
+ }
+ return 0;
+}
+
+static int
+sql_lookup_get_query(struct sql_dict *dict,
+ const struct dict_op_settings *set,
+ const char *key,
+ const struct dict_sql_map **map_r,
+ struct sql_statement **stmt_r,
+ const char **error_r)
+{
+ const struct dict_sql_map *map;
+ ARRAY_TYPE(const_string) pattern_values;
+ const char *error;
+
+ map = *map_r = sql_dict_find_map(dict, key, &pattern_values);
+ if (map == NULL) {
+ *error_r = t_strdup_printf(
+ "sql dict lookup: Invalid/unmapped key: %s", key);
+ return -1;
+ }
+
+ string_t *query = t_str_new(256);
+ ARRAY_TYPE(sql_dict_param) params;
+ t_array_init(&params, 4);
+ str_printfa(query, "SELECT %s FROM %s",
+ map->value_field, map->table);
+ if (sql_dict_where_build(set->username, map, &pattern_values,
+ key[0] == DICT_PATH_PRIVATE[0],
+ SQL_DICT_RECURSE_NONE, query,
+ &params, &error) < 0) {
+ *error_r = t_strdup_printf(
+ "sql dict lookup: Failed to lookup key %s: %s", key, error);
+ return -1;
+ }
+ *stmt_r = sql_dict_statement_init(dict, str_c(query), &params);
+ return 0;
+}
+
+static const char *
+sql_dict_result_unescape(enum dict_sql_type type, pool_t pool,
+ struct sql_result *result, unsigned int result_idx)
+{
+ const unsigned char *data;
+ size_t size;
+ const char *value;
+ string_t *str;
+
+ switch (type) {
+ case DICT_SQL_TYPE_STRING:
+ case DICT_SQL_TYPE_INT:
+ case DICT_SQL_TYPE_UINT:
+ value = sql_result_get_field_value(result, result_idx);
+ return value == NULL ? "" : p_strdup(pool, value);
+ case DICT_SQL_TYPE_HEXBLOB:
+ break;
+ }
+
+ data = sql_result_get_field_value_binary(result, result_idx, &size);
+ str = str_new(pool, size*2 + 1);
+ binary_to_hex_append(str, data, size);
+ return str_c(str);
+}
+
+static const char *
+sql_dict_result_unescape_value(const struct dict_sql_map *map, pool_t pool,
+ struct sql_result *result)
+{
+ return sql_dict_result_unescape(map->value_types[0], pool, result, 0);
+}
+
+static const char *const *
+sql_dict_result_unescape_values(const struct dict_sql_map *map, pool_t pool,
+ struct sql_result *result)
+{
+ const char **values;
+ unsigned int i;
+
+ values = p_new(pool, const char *, map->values_count + 1);
+ for (i = 0; i < map->values_count; i++) {
+ values[i] = sql_dict_result_unescape(map->value_types[i],
+ pool, result, i);
+ }
+ return values;
+}
+
+static const char *
+sql_dict_result_unescape_field(const struct dict_sql_map *map, pool_t pool,
+ struct sql_result *result, unsigned int result_idx,
+ unsigned int sql_field_idx)
+{
+ const struct dict_sql_field *sql_field;
+
+ sql_field = array_idx(&map->pattern_fields, sql_field_idx);
+ return sql_dict_result_unescape(sql_field->value_type, pool,
+ result, result_idx);
+}
+
+static int sql_dict_lookup(struct dict *_dict, const struct dict_op_settings *set,
+ pool_t pool, const char *key,
+ const char **value_r, const char **error_r)
+{
+ struct sql_dict *dict = (struct sql_dict *)_dict;
+ const struct dict_sql_map *map;
+ struct sql_statement *stmt;
+ struct sql_result *result = NULL;
+ int ret;
+
+ *value_r = NULL;
+
+ if (sql_lookup_get_query(dict, set, key, &map, &stmt, error_r) < 0)
+ return -1;
+
+ result = sql_statement_query_s(&stmt);
+ ret = sql_result_next_row(result);
+ if (ret < 0) {
+ *error_r = t_strdup_printf("dict sql lookup failed: %s",
+ sql_result_get_error(result));
+ } else if (ret > 0) {
+ *value_r = sql_dict_result_unescape_value(map, pool, result);
+ }
+
+ sql_result_unref(result);
+ return ret;
+}
+
+struct sql_dict_lookup_context {
+ const struct dict_sql_map *map;
+ dict_lookup_callback_t *callback;
+ void *context;
+};
+
+static void
+sql_dict_lookup_async_callback(struct sql_result *sql_result,
+ struct sql_dict_lookup_context *ctx)
+{
+ struct dict_lookup_result result;
+
+ i_zero(&result);
+ result.ret = sql_result_next_row(sql_result);
+ if (result.ret < 0)
+ result.error = sql_result_get_error(sql_result);
+ else if (result.ret > 0) {
+ result.values = sql_dict_result_unescape_values(ctx->map,
+ pool_datastack_create(), sql_result);
+ result.value = result.values[0];
+ if (result.value == NULL) {
+ /* NULL value returned. we'll treat this as
+ "not found", which is probably what is usually
+ wanted. */
+ result.ret = 0;
+ }
+ }
+ ctx->callback(&result, ctx->context);
+
+ i_free(ctx);
+}
+
+static void
+sql_dict_lookup_async(struct dict *_dict,
+ const struct dict_op_settings *set,
+ const char *key,
+ dict_lookup_callback_t *callback, void *context)
+{
+ struct sql_dict *dict = (struct sql_dict *)_dict;
+ const struct dict_sql_map *map;
+ struct sql_dict_lookup_context *ctx;
+ struct sql_statement *stmt;
+ const char *error;
+
+ if (sql_lookup_get_query(dict, set, key, &map, &stmt, &error) < 0) {
+ struct dict_lookup_result result;
+
+ i_zero(&result);
+ result.ret = -1;
+ result.error = error;
+ callback(&result, context);
+ } else {
+ ctx = i_new(struct sql_dict_lookup_context, 1);
+ ctx->callback = callback;
+ ctx->context = context;
+ ctx->map = map;
+ sql_statement_query(&stmt, sql_dict_lookup_async_callback, ctx);
+ }
+}
+
+static const struct dict_sql_map *
+sql_dict_iterate_find_next_map(struct sql_dict_iterate_context *ctx,
+ ARRAY_TYPE(const_string) *pattern_values)
+{
+ struct sql_dict *dict = (struct sql_dict *)ctx->ctx.dict;
+ const struct dict_sql_map *maps;
+ unsigned int i, count;
+ size_t pat_len, path_len;
+ bool recurse = (ctx->flags & DICT_ITERATE_FLAG_RECURSE) != 0;
+
+ t_array_init(pattern_values, dict->set->max_pattern_fields_count);
+ maps = array_get(&dict->set->maps, &count);
+ for (i = ctx->next_map_idx; i < count; i++) {
+ if (dict_sql_map_match(&maps[i], ctx->path,
+ pattern_values, &pat_len, &path_len,
+ TRUE, recurse) &&
+ (recurse ||
+ array_count(pattern_values)+1 >= array_count(&maps[i].pattern_fields))) {
+ ctx->key_prefix_len = path_len;
+ ctx->pattern_prefix_len = pat_len;
+ ctx->next_map_idx = i + 1;
+
+ str_truncate(ctx->key, 0);
+ str_append(ctx->key, ctx->path);
+ return &maps[i];
+ }
+ }
+ return NULL;
+}
+
+static int
+sql_dict_iterate_build_next_query(struct sql_dict_iterate_context *ctx,
+ struct sql_statement **stmt_r,
+ const char **error_r)
+{
+ struct sql_dict *dict = (struct sql_dict *)ctx->ctx.dict;
+ const struct dict_op_settings_private *set = &ctx->ctx.set;
+ const struct dict_sql_map *map;
+ ARRAY_TYPE(const_string) pattern_values;
+ const struct dict_sql_field *pattern_fields;
+ enum sql_recurse_type recurse_type;
+ unsigned int i, count;
+
+ map = sql_dict_iterate_find_next_map(ctx, &pattern_values);
+ /* NULL map is allowed if we have already done some lookups */
+ if (map == NULL) {
+ if (!ctx->allow_null_map) {
+ *error_r = "Invalid/unmapped path";
+ return -1;
+ }
+ return 0;
+ }
+
+ if (ctx->result != NULL) {
+ sql_result_unref(ctx->result);
+ ctx->result = NULL;
+ }
+
+ string_t *query = t_str_new(256);
+ str_append(query, "SELECT ");
+ if ((ctx->flags & DICT_ITERATE_FLAG_NO_VALUE) == 0)
+ str_printfa(query, "%s,", map->value_field);
+
+ /* get all missing fields */
+ pattern_fields = array_get(&map->pattern_fields, &count);
+ i = array_count(&pattern_values);
+ if (i == count) {
+ /* we always want to know the last field since we're
+ iterating its children */
+ i_assert(i > 0);
+ i--;
+ }
+ ctx->sql_fields_start_idx = i;
+ for (; i < count; i++)
+ str_printfa(query, "%s,", pattern_fields[i].name);
+ str_truncate(query, str_len(query)-1);
+
+ str_printfa(query, " FROM %s", map->table);
+
+ if ((ctx->flags & DICT_ITERATE_FLAG_RECURSE) != 0)
+ recurse_type = SQL_DICT_RECURSE_FULL;
+ else if ((ctx->flags & DICT_ITERATE_FLAG_EXACT_KEY) != 0)
+ recurse_type = SQL_DICT_RECURSE_NONE;
+ else
+ recurse_type = SQL_DICT_RECURSE_ONE;
+
+ ARRAY_TYPE(sql_dict_param) params;
+ t_array_init(&params, 4);
+ bool add_username = (ctx->path[0] == DICT_PATH_PRIVATE[0]);
+ if (sql_dict_where_build(set->username, map, &pattern_values, add_username,
+ recurse_type, query, &params, error_r) < 0)
+ return -1;
+
+ if ((ctx->flags & DICT_ITERATE_FLAG_SORT_BY_KEY) != 0) {
+ str_append(query, " ORDER BY ");
+ for (i = 0; i < count; i++) {
+ str_printfa(query, "%s", pattern_fields[i].name);
+ if (i < count-1)
+ str_append_c(query, ',');
+ }
+ } else if ((ctx->flags & DICT_ITERATE_FLAG_SORT_BY_VALUE) != 0)
+ str_printfa(query, " ORDER BY %s", map->value_field);
+
+ if (ctx->ctx.max_rows > 0) {
+ i_assert(ctx->ctx.row_count < ctx->ctx.max_rows);
+ str_printfa(query, " LIMIT %"PRIu64,
+ ctx->ctx.max_rows - ctx->ctx.row_count);
+ }
+
+ *stmt_r = sql_dict_statement_init(dict, str_c(query), &params);
+ ctx->map = map;
+ return 1;
+}
+
+static void sql_dict_iterate_callback(struct sql_result *result,
+ struct sql_dict_iterate_context *ctx)
+{
+ if (!ctx->destroyed) {
+ sql_result_ref(result);
+ ctx->result = result;
+ if (ctx->ctx.async_callback != NULL && !ctx->synchronous_result)
+ ctx->ctx.async_callback(ctx->ctx.async_context);
+ }
+
+ pool_t pool_copy = ctx->pool;
+ pool_unref(&pool_copy);
+}
+
+static int sql_dict_iterate_next_query(struct sql_dict_iterate_context *ctx)
+{
+ struct sql_statement *stmt;
+ const char *error;
+ int ret;
+
+ ret = sql_dict_iterate_build_next_query(ctx, &stmt, &error);
+ if (ret <= 0) {
+ /* this is expected error */
+ if (ret == 0)
+ return ret;
+ /* failed */
+ ctx->error = p_strdup_printf(ctx->pool,
+ "sql dict iterate failed for %s: %s",
+ ctx->path, error);
+ return -1;
+ }
+
+ if ((ctx->flags & DICT_ITERATE_FLAG_ASYNC) == 0) {
+ ctx->result = sql_statement_query_s(&stmt);
+ } else {
+ i_assert(ctx->result == NULL);
+ ctx->synchronous_result = TRUE;
+ pool_ref(ctx->pool);
+ sql_statement_query(&stmt, sql_dict_iterate_callback, ctx);
+ ctx->synchronous_result = FALSE;
+ }
+ return ret;
+}
+
+static struct dict_iterate_context *
+sql_dict_iterate_init(struct dict *_dict,
+ const struct dict_op_settings *set ATTR_UNUSED,
+ const char *path, enum dict_iterate_flags flags)
+{
+ struct sql_dict_iterate_context *ctx;
+ pool_t pool;
+
+ pool = pool_alloconly_create("sql dict iterate", 512);
+ ctx = p_new(pool, struct sql_dict_iterate_context, 1);
+ ctx->ctx.dict = _dict;
+ ctx->pool = pool;
+ ctx->flags = flags;
+
+ ctx->path = p_strdup(pool, path);
+
+ ctx->key = str_new(pool, 256);
+ return &ctx->ctx;
+}
+
+static bool sql_dict_iterate(struct dict_iterate_context *_ctx,
+ const char **key_r, const char *const **values_r)
+{
+ struct sql_dict_iterate_context *ctx =
+ (struct sql_dict_iterate_context *)_ctx;
+ const char *p, *value;
+ unsigned int i, sql_field_i, count;
+ int ret;
+
+ _ctx->has_more = FALSE;
+ if (ctx->error != NULL)
+ return FALSE;
+ if (!ctx->iter_query_sent) {
+ ctx->iter_query_sent = TRUE;
+ if (sql_dict_iterate_next_query(ctx) <= 0)
+ return FALSE;
+ }
+
+ if (ctx->result == NULL) {
+ /* wait for async lookup to finish */
+ i_assert((ctx->flags & DICT_ITERATE_FLAG_ASYNC) != 0);
+ _ctx->has_more = TRUE;
+ return FALSE;
+ }
+
+ ret = sql_result_next_row(ctx->result);
+ while (ret == SQL_RESULT_NEXT_MORE) {
+ if ((ctx->flags & DICT_ITERATE_FLAG_ASYNC) == 0)
+ sql_result_more_s(&ctx->result);
+ else {
+ /* get more results asynchronously */
+ ctx->synchronous_result = TRUE;
+ pool_ref(ctx->pool);
+ sql_result_more(&ctx->result, sql_dict_iterate_callback, ctx);
+ ctx->synchronous_result = FALSE;
+ if (ctx->result == NULL) {
+ _ctx->has_more = TRUE;
+ return FALSE;
+ }
+ }
+ ret = sql_result_next_row(ctx->result);
+ }
+ if (ret == 0) {
+ /* see if there are more results in the next map.
+ don't do it if we're looking for an exact match, since we
+ already should have handled it. */
+ if ((ctx->flags & DICT_ITERATE_FLAG_EXACT_KEY) != 0)
+ return FALSE;
+ ctx->iter_query_sent = FALSE;
+ /* we have gotten *SOME* results, so can allow
+ unmapped next key now. */
+ ctx->allow_null_map = TRUE;
+ return sql_dict_iterate(_ctx, key_r, values_r);
+ }
+ if (ret < 0) {
+ ctx->error = p_strdup_printf(ctx->pool,
+ "dict sql iterate failed: %s",
+ sql_result_get_error(ctx->result));
+ return FALSE;
+ }
+
+ /* convert fetched row to dict key */
+ str_truncate(ctx->key, ctx->key_prefix_len);
+ if (ctx->key_prefix_len > 0 &&
+ str_c(ctx->key)[ctx->key_prefix_len-1] != '/')
+ str_append_c(ctx->key, '/');
+
+ count = sql_result_get_fields_count(ctx->result);
+ i = (ctx->flags & DICT_ITERATE_FLAG_NO_VALUE) != 0 ? 0 :
+ ctx->map->values_count;
+ sql_field_i = ctx->sql_fields_start_idx;
+ for (p = ctx->map->pattern + ctx->pattern_prefix_len; *p != '\0'; p++) {
+ if (*p != '$')
+ str_append_c(ctx->key, *p);
+ else {
+ i_assert(i < count);
+ value = sql_dict_result_unescape_field(ctx->map,
+ pool_datastack_create(), ctx->result, i, sql_field_i);
+ if (value != NULL)
+ str_append(ctx->key, value);
+ i++; sql_field_i++;
+ }
+ }
+
+ *key_r = str_c(ctx->key);
+ if ((ctx->flags & DICT_ITERATE_FLAG_NO_VALUE) == 0) {
+ *values_r = sql_dict_result_unescape_values(ctx->map,
+ pool_datastack_create(), ctx->result);
+ }
+ return TRUE;
+}
+
+static int sql_dict_iterate_deinit(struct dict_iterate_context *_ctx,
+ const char **error_r)
+{
+ struct sql_dict_iterate_context *ctx =
+ (struct sql_dict_iterate_context *)_ctx;
+ int ret = ctx->error != NULL ? -1 : 0;
+
+ *error_r = t_strdup(ctx->error);
+ if (ctx->result != NULL)
+ sql_result_unref(ctx->result);
+ ctx->destroyed = TRUE;
+
+ pool_t pool_copy = ctx->pool;
+ pool_unref(&pool_copy);
+ return ret;
+}
+
+static struct dict_transaction_context *
+sql_dict_transaction_init(struct dict *_dict)
+{
+ struct sql_dict *dict = (struct sql_dict *)_dict;
+ struct sql_dict_transaction_context *ctx;
+
+ ctx = i_new(struct sql_dict_transaction_context, 1);
+ ctx->ctx.dict = _dict;
+ ctx->sql_ctx = sql_transaction_begin(dict->db);
+
+ return &ctx->ctx;
+}
+
+static void sql_dict_transaction_free(struct sql_dict_transaction_context *ctx)
+{
+ if (array_is_created(&ctx->prev_inc))
+ sql_dict_prev_inc_free(ctx);
+ if (array_is_created(&ctx->prev_set))
+ sql_dict_prev_set_free(ctx);
+
+ pool_unref(&ctx->inc_row_pool);
+ i_free(ctx->error);
+ i_free(ctx);
+}
+
+static bool
+sql_dict_transaction_has_nonexistent(struct sql_dict_transaction_context *ctx)
+{
+ struct sql_dict_inc_row *inc_row;
+
+ for (inc_row = ctx->inc_row; inc_row != NULL; inc_row = inc_row->prev) {
+ i_assert(inc_row->rows != UINT_MAX);
+ if (inc_row->rows == 0)
+ return TRUE;
+ }
+ return FALSE;
+}
+
+static void
+sql_dict_transaction_commit_callback(const struct sql_commit_result *sql_result,
+ struct sql_dict_transaction_context *ctx)
+{
+ struct dict_commit_result result;
+
+ i_zero(&result);
+ if (sql_result->error == NULL)
+ result.ret = sql_dict_transaction_has_nonexistent(ctx) ?
+ DICT_COMMIT_RET_NOTFOUND : DICT_COMMIT_RET_OK;
+ else {
+ result.error = t_strdup_printf("sql dict: commit failed: %s",
+ sql_result->error);
+ switch (sql_result->error_type) {
+ case SQL_RESULT_ERROR_TYPE_UNKNOWN:
+ default:
+ result.ret = DICT_COMMIT_RET_FAILED;
+ break;
+ case SQL_RESULT_ERROR_TYPE_WRITE_UNCERTAIN:
+ result.ret = DICT_COMMIT_RET_WRITE_UNCERTAIN;
+ break;
+ }
+ }
+
+ if (ctx->async_callback != NULL)
+ ctx->async_callback(&result, ctx->async_context);
+ else if (result.ret < 0)
+ i_error("%s", result.error);
+ sql_dict_transaction_free(ctx);
+}
+
+static void
+sql_dict_transaction_commit(struct dict_transaction_context *_ctx, bool async,
+ dict_transaction_commit_callback_t *callback,
+ void *context)
+{
+ struct sql_dict_transaction_context *ctx =
+ (struct sql_dict_transaction_context *)_ctx;
+ const char *error;
+ struct dict_commit_result result;
+
+ /* flush any pending set/inc */
+ if (array_is_created(&ctx->prev_inc))
+ sql_dict_prev_inc_flush(ctx);
+ if (array_is_created(&ctx->prev_set))
+ sql_dict_prev_set_flush(ctx);
+
+ /* note that the above calls might still set ctx->error */
+ i_zero(&result);
+ result.ret = DICT_COMMIT_RET_FAILED;
+ result.error = t_strdup(ctx->error);
+
+ if (ctx->error != NULL) {
+ sql_transaction_rollback(&ctx->sql_ctx);
+ } else if (!_ctx->changed) {
+ /* nothing changed, no need to commit */
+ sql_transaction_rollback(&ctx->sql_ctx);
+ result.ret = DICT_COMMIT_RET_OK;
+ } else if (async) {
+ ctx->async_callback = callback;
+ ctx->async_context = context;
+ sql_transaction_commit(&ctx->sql_ctx,
+ sql_dict_transaction_commit_callback, ctx);
+ return;
+ } else if (sql_transaction_commit_s(&ctx->sql_ctx, &error) < 0) {
+ result.error = t_strdup_printf(
+ "sql dict: commit failed: %s", error);
+ } else {
+ if (sql_dict_transaction_has_nonexistent(ctx))
+ result.ret = DICT_COMMIT_RET_NOTFOUND;
+ else
+ result.ret = DICT_COMMIT_RET_OK;
+ }
+ sql_dict_transaction_free(ctx);
+
+ callback(&result, context);
+}
+
+static void sql_dict_transaction_rollback(struct dict_transaction_context *_ctx)
+{
+ struct sql_dict_transaction_context *ctx =
+ (struct sql_dict_transaction_context *)_ctx;
+
+ sql_transaction_rollback(&ctx->sql_ctx);
+ sql_dict_transaction_free(ctx);
+}
+
+static struct sql_statement *
+sql_dict_transaction_stmt_init(struct sql_dict_transaction_context *ctx,
+ const char *query,
+ const ARRAY_TYPE(sql_dict_param) *params)
+{
+ struct sql_dict *dict = (struct sql_dict *)ctx->ctx.dict;
+ struct sql_statement *stmt =
+ sql_dict_statement_init(dict, query, params);
+
+ if (ctx->ctx.timestamp.tv_sec != 0)
+ sql_statement_set_timestamp(stmt, &ctx->ctx.timestamp);
+ return stmt;
+}
+
+struct dict_sql_build_query_field {
+ const struct dict_sql_map *map;
+ const char *value;
+};
+
+struct dict_sql_build_query {
+ struct sql_dict *dict;
+
+ ARRAY(struct dict_sql_build_query_field) fields;
+ const ARRAY_TYPE(const_string) *pattern_values;
+ bool add_username;
+};
+
+static int sql_dict_set_query(struct sql_dict_transaction_context *ctx,
+ const struct dict_sql_build_query *build,
+ struct sql_statement **stmt_r,
+ const char **error_r)
+{
+ struct sql_dict *dict = build->dict;
+ const struct dict_sql_build_query_field *fields;
+ const struct dict_sql_field *pattern_fields;
+ ARRAY_TYPE(sql_dict_param) params;
+ const char *const *pattern_values;
+ unsigned int i, field_count, count, count2;
+ string_t *prefix, *suffix;
+
+ fields = array_get(&build->fields, &field_count);
+ i_assert(field_count > 0);
+
+ t_array_init(&params, 4);
+ prefix = t_str_new(64);
+ suffix = t_str_new(256);
+ /* SQL table is guaranteed to be the same for all fields.
+ Build all the SQL field names into prefix and '?' placeholders for
+ each value into the suffix. The actual field values will be added
+ into params[]. */
+ str_printfa(prefix, "INSERT INTO %s", fields[0].map->table);
+ str_append(prefix, " (");
+ str_append(suffix, ") VALUES (");
+ for (i = 0; i < field_count; i++) {
+ if (i > 0) {
+ str_append_c(prefix, ',');
+ str_append_c(suffix, ',');
+ }
+ str_append(prefix, t_strcut(fields[i].map->value_field, ','));
+
+ enum dict_sql_type value_type =
+ fields[i].map->value_types[0];
+ str_append_c(suffix, '?');
+ if (sql_dict_value_get(fields[i].map,
+ value_type, "value", fields[i].value,
+ "", &params, error_r) < 0)
+ return -1;
+ }
+ if (build->add_username) {
+ struct sql_dict_param *param = array_append_space(&params);
+ str_printfa(prefix, ",%s", fields[0].map->username_field);
+ str_append(suffix, ",?");
+ param->value_type = DICT_SQL_TYPE_STRING;
+ param->value_str = ctx->ctx.set.username;
+ }
+
+ /* add the variable fields that were parsed from the path */
+ pattern_fields = array_get(&fields[0].map->pattern_fields, &count);
+ pattern_values = array_get(build->pattern_values, &count2);
+ i_assert(count == count2);
+ for (i = 0; i < count; i++) {
+ str_printfa(prefix, ",%s", pattern_fields[i].name);
+ str_append(suffix, ",?");
+ if (sql_dict_field_get_value(fields[0].map, &pattern_fields[i],
+ pattern_values[i], "",
+ &params, error_r) < 0)
+ return -1;
+ }
+
+ str_append_str(prefix, suffix);
+ str_append_c(prefix, ')');
+
+ enum sql_db_flags flags = sql_get_flags(dict->db);
+ if ((flags & SQL_DB_FLAG_ON_DUPLICATE_KEY) != 0)
+ str_append(prefix, " ON DUPLICATE KEY UPDATE ");
+ else if ((flags & SQL_DB_FLAG_ON_CONFLICT_DO) != 0) {
+ str_append(prefix, " ON CONFLICT (");
+ for (i = 0; i < count; i++) {
+ if (i > 0)
+ str_append_c(prefix, ',');
+ str_append(prefix, pattern_fields[i].name);
+ }
+ if (build->add_username) {
+ if (count > 0)
+ str_append_c(prefix, ',');
+ str_append(prefix, fields[0].map->username_field);
+ }
+ str_append(prefix, ") DO UPDATE SET ");
+ } else {
+ *stmt_r = sql_dict_transaction_stmt_init(ctx, str_c(prefix), &params);
+ return 0;
+ }
+
+ /* If the row already exists, UPDATE it instead. The pattern_values
+ don't need to be updated here, because they are expected to be part
+ of the row's primary key. */
+ for (i = 0; i < field_count; i++) {
+ const char *first_value_field =
+ t_strcut(fields[i].map->value_field, ',');
+ if (i > 0)
+ str_append_c(prefix, ',');
+ str_append(prefix, first_value_field);
+ str_append_c(prefix, '=');
+
+ enum dict_sql_type value_type =
+ fields[i].map->value_types[0];
+ str_append_c(prefix, '?');
+ if (sql_dict_value_get(fields[i].map,
+ value_type, "value", fields[i].value,
+ "", &params, error_r) < 0)
+ return -1;
+ }
+ *stmt_r = sql_dict_transaction_stmt_init(ctx, str_c(prefix), &params);
+ return 0;
+}
+
+static int
+sql_dict_update_query(const struct dict_sql_build_query *build,
+ const struct dict_op_settings_private *set,
+ const char **query_r, ARRAY_TYPE(sql_dict_param) *params,
+ const char **error_r)
+{
+ const struct dict_sql_build_query_field *fields;
+ unsigned int i, field_count;
+ string_t *query;
+
+ fields = array_get(&build->fields, &field_count);
+ i_assert(field_count > 0);
+
+ query = t_str_new(64);
+ str_printfa(query, "UPDATE %s SET ", fields[0].map->table);
+ for (i = 0; i < field_count; i++) {
+ const char *first_value_field =
+ t_strcut(fields[i].map->value_field, ',');
+ if (i > 0)
+ str_append_c(query, ',');
+ str_printfa(query, "%s=%s+?", first_value_field,
+ first_value_field);
+ }
+
+ if (sql_dict_where_build(set->username, fields[0].map, build->pattern_values,
+ build->add_username, SQL_DICT_RECURSE_NONE,
+ query, params, error_r) < 0)
+ return -1;
+ *query_r = str_c(query);
+ return 0;
+}
+
+static void sql_dict_prev_set_free(struct sql_dict_transaction_context *ctx)
+{
+ struct sql_dict_prev *prev_set;
+
+ array_foreach_modifiable(&ctx->prev_set, prev_set) {
+ i_free(prev_set->value.str);
+ i_free(prev_set->key);
+ }
+ array_free(&ctx->prev_set);
+}
+
+static void sql_dict_prev_set_flush(struct sql_dict_transaction_context *ctx)
+{
+ struct sql_dict *dict = (struct sql_dict *)ctx->ctx.dict;
+ const struct sql_dict_prev *prev_sets;
+ unsigned int count;
+ struct sql_statement *stmt;
+ ARRAY_TYPE(const_string) pattern_values;
+ struct dict_sql_build_query build;
+ struct dict_sql_build_query_field *field;
+ const char *error;
+
+ i_assert(array_is_created(&ctx->prev_set));
+
+ if (ctx->error != NULL) {
+ sql_dict_prev_set_free(ctx);
+ return;
+ }
+
+ prev_sets = array_get(&ctx->prev_set, &count);
+ i_assert(count > 0);
+
+ /* Get the variable values from the dict path. We already verified that
+ these are all exactly the same for everything in prev_sets. */
+ if (sql_dict_find_map(dict, prev_sets[0].key, &pattern_values) == NULL)
+ i_unreached(); /* this was already checked */
+
+ i_zero(&build);
+ build.dict = dict;
+ build.pattern_values = &pattern_values;
+ build.add_username = (prev_sets[0].key[0] == DICT_PATH_PRIVATE[0]);
+
+ /* build.fields[] is used to get the map { value_field } for the
+ SQL field names, as well as the values for them.
+
+ Example: INSERT INTO ... (build.fields[0].map->value_field,
+ ...[1], ...) VALUES (build.fields[0].value, ...[1], ...) */
+ t_array_init(&build.fields, count);
+ for (unsigned int i = 0; i < count; i++) {
+ i_assert(build.add_username ==
+ (prev_sets[i].key[0] == DICT_PATH_PRIVATE[0]));
+ field = array_append_space(&build.fields);
+ field->map = prev_sets[i].map;
+ field->value = prev_sets[i].value.str;
+ }
+
+ if (sql_dict_set_query(ctx, &build, &stmt, &error) < 0) {
+ ctx->error = i_strdup_printf(
+ "dict-sql: Failed to set %u fields (first %s): %s",
+ count, prev_sets[0].key, error);
+ } else {
+ sql_update_stmt(ctx->sql_ctx, &stmt);
+ }
+ sql_dict_prev_set_free(ctx);
+}
+
+static void sql_dict_unset(struct dict_transaction_context *_ctx,
+ const char *key)
+{
+ struct sql_dict_transaction_context *ctx =
+ (struct sql_dict_transaction_context *)_ctx;
+ struct sql_dict *dict = (struct sql_dict *)_ctx->dict;
+ const struct dict_op_settings_private *set = &_ctx->set;
+ const struct dict_sql_map *map;
+ ARRAY_TYPE(const_string) pattern_values;
+ string_t *query = t_str_new(256);
+ ARRAY_TYPE(sql_dict_param) params;
+ const char *error;
+
+ if (ctx->error != NULL)
+ return;
+
+ /* In theory we could unset one of the previous set/incs in this
+ same transaction, so flush them first. */
+ if (array_is_created(&ctx->prev_inc))
+ sql_dict_prev_inc_flush(ctx);
+ if (array_is_created(&ctx->prev_set))
+ sql_dict_prev_set_flush(ctx);
+
+ map = sql_dict_find_map(dict, key, &pattern_values);
+ if (map == NULL) {
+ ctx->error = i_strdup_printf("dict-sql: Invalid/unmapped key: %s", key);
+ return;
+ }
+
+ str_printfa(query, "DELETE FROM %s", map->table);
+ t_array_init(&params, 4);
+ if (sql_dict_where_build(set->username, map, &pattern_values,
+ key[0] == DICT_PATH_PRIVATE[0],
+ SQL_DICT_RECURSE_NONE, query,
+ &params, &error) < 0) {
+ ctx->error = i_strdup_printf(
+ "dict-sql: Failed to delete %s: %s", key, error);
+ } else {
+ struct sql_statement *stmt =
+ sql_dict_transaction_stmt_init(ctx, str_c(query), &params);
+ sql_update_stmt(ctx->sql_ctx, &stmt);
+ }
+}
+
+static unsigned int *
+sql_dict_next_inc_row(struct sql_dict_transaction_context *ctx)
+{
+ struct sql_dict_inc_row *row;
+
+ if (ctx->inc_row_pool == NULL) {
+ ctx->inc_row_pool =
+ pool_alloconly_create("sql dict inc rows", 128);
+ }
+ row = p_new(ctx->inc_row_pool, struct sql_dict_inc_row, 1);
+ row->prev = ctx->inc_row;
+ row->rows = UINT_MAX;
+ ctx->inc_row = row;
+ return &row->rows;
+}
+
+static void sql_dict_prev_inc_free(struct sql_dict_transaction_context *ctx)
+{
+ struct sql_dict_prev *prev_inc;
+
+ array_foreach_modifiable(&ctx->prev_inc, prev_inc)
+ i_free(prev_inc->key);
+ array_free(&ctx->prev_inc);
+}
+
+static void sql_dict_prev_inc_flush(struct sql_dict_transaction_context *ctx)
+{
+ struct sql_dict *dict = (struct sql_dict *)ctx->ctx.dict;
+ const struct dict_op_settings_private *set = &ctx->ctx.set;
+ const struct sql_dict_prev *prev_incs;
+ unsigned int count;
+ ARRAY_TYPE(const_string) pattern_values;
+ struct dict_sql_build_query build;
+ struct dict_sql_build_query_field *field;
+ ARRAY_TYPE(sql_dict_param) params;
+ struct sql_dict_param *param;
+ const char *query, *error;
+
+ i_assert(array_is_created(&ctx->prev_inc));
+
+ if (ctx->error != NULL) {
+ sql_dict_prev_inc_free(ctx);
+ return;
+ }
+
+ prev_incs = array_get(&ctx->prev_inc, &count);
+ i_assert(count > 0);
+
+ /* Get the variable values from the dict path. We already verified that
+ these are all exactly the same for everything in prev_incs. */
+ if (sql_dict_find_map(dict, prev_incs[0].key, &pattern_values) == NULL)
+ i_unreached(); /* this was already checked */
+
+ i_zero(&build);
+ build.dict = dict;
+ build.pattern_values = &pattern_values;
+ build.add_username = (prev_incs[0].key[0] == DICT_PATH_PRIVATE[0]);
+
+ /* build.fields[] is an array of maps, which are used to get the
+ map { value_field } for the SQL field names.
+
+ params[] specifies the list of values to use for each field.
+
+ Example: UPDATE .. SET build.fields[0].map->value_field =
+ ...->value_field + params[0]->value_int64, ...[1]... */
+ t_array_init(&build.fields, count);
+ t_array_init(&params, count);
+ for (unsigned int i = 0; i < count; i++) {
+ i_assert(build.add_username ==
+ (prev_incs[i].key[0] == DICT_PATH_PRIVATE[0]));
+ field = array_append_space(&build.fields);
+ field->map = prev_incs[i].map;
+ field->value = NULL; /* unused */
+
+ param = array_append_space(&params);
+ param->value_type = DICT_SQL_TYPE_INT;
+ param->value_int64 = prev_incs[i].value.diff;
+ }
+
+ if (sql_dict_update_query(&build, set, &query, &params, &error) < 0) {
+ ctx->error = i_strdup_printf(
+ "dict-sql: Failed to increase %u fields (first %s): %s",
+ count, prev_incs[0].key, error);
+ } else {
+ struct sql_statement *stmt =
+ sql_dict_transaction_stmt_init(ctx, query, &params);
+ sql_update_stmt_get_rows(ctx->sql_ctx, &stmt,
+ sql_dict_next_inc_row(ctx));
+ }
+ sql_dict_prev_inc_free(ctx);
+}
+
+static bool
+sql_dict_maps_are_mergeable(struct sql_dict *dict,
+ const struct sql_dict_prev *prev1,
+ const struct dict_sql_map *map2,
+ const char *map2_key,
+ const ARRAY_TYPE(const_string) *map2_pattern_values)
+{
+ const struct dict_sql_map *map3;
+ ARRAY_TYPE(const_string) map1_pattern_values;
+
+ /* sql table names must equal */
+ if (strcmp(prev1->map->table, map2->table) != 0)
+ return FALSE;
+ /* private vs shared prefix must equal */
+ if (prev1->key[0] != map2_key[0])
+ return FALSE;
+ if (prev1->key[0] == DICT_PATH_PRIVATE[0]) {
+ /* for private keys, username must equal */
+ if (strcmp(prev1->map->username_field, map2->username_field) != 0)
+ return FALSE;
+ }
+
+ /* variable values in the paths must equal exactly */
+ map3 = sql_dict_find_map(dict, prev1->key, &map1_pattern_values);
+ i_assert(map3 == prev1->map);
+
+ return array_equal_fn(&map1_pattern_values, map2_pattern_values,
+ i_strcmp_p);
+}
+
+static void sql_dict_set(struct dict_transaction_context *_ctx,
+ const char *key, const char *value)
+{
+ struct sql_dict_transaction_context *ctx =
+ (struct sql_dict_transaction_context *)_ctx;
+ struct sql_dict *dict = (struct sql_dict *)_ctx->dict;
+ const struct dict_sql_map *map;
+ ARRAY_TYPE(const_string) pattern_values;
+
+ if (ctx->error != NULL)
+ return;
+
+ /* In theory we could set the previous inc in this same transaction,
+ so flush it first. */
+ if (array_is_created(&ctx->prev_inc))
+ sql_dict_prev_inc_flush(ctx);
+
+ map = sql_dict_find_map(dict, key, &pattern_values);
+ if (map == NULL) {
+ ctx->error = i_strdup_printf(
+ "sql dict set: Invalid/unmapped key: %s", key);
+ return;
+ }
+
+ if (array_is_created(&ctx->prev_set) &&
+ !sql_dict_maps_are_mergeable(dict, array_front(&ctx->prev_set),
+ map, key, &pattern_values)) {
+ /* couldn't merge to the previous set - flush it */
+ sql_dict_prev_set_flush(ctx);
+ }
+
+ if (!array_is_created(&ctx->prev_set))
+ i_array_init(&ctx->prev_set, 4);
+ /* Either this is the first set, or this can be merged with the
+ previous set. */
+ struct sql_dict_prev *prev_set = array_append_space(&ctx->prev_set);
+ prev_set->map = map;
+ prev_set->key = i_strdup(key);
+ prev_set->value.str = i_strdup(value);
+}
+
+static void sql_dict_atomic_inc(struct dict_transaction_context *_ctx,
+ const char *key, long long diff)
+{
+ struct sql_dict_transaction_context *ctx =
+ (struct sql_dict_transaction_context *)_ctx;
+ struct sql_dict *dict = (struct sql_dict *)_ctx->dict;
+ const struct dict_sql_map *map;
+ ARRAY_TYPE(const_string) pattern_values;
+
+ if (ctx->error != NULL)
+ return;
+
+ /* In theory we could inc the previous set in this same transaction,
+ so flush it first. */
+ if (array_is_created(&ctx->prev_set))
+ sql_dict_prev_set_flush(ctx);
+
+ map = sql_dict_find_map(dict, key, &pattern_values);
+ if (map == NULL) {
+ ctx->error = i_strdup_printf(
+ "sql dict atomic inc: Invalid/unmapped key: %s", key);
+ return;
+ }
+
+ if (array_is_created(&ctx->prev_inc) &&
+ !sql_dict_maps_are_mergeable(dict, array_front(&ctx->prev_inc),
+ map, key, &pattern_values)) {
+ /* couldn't merge to the previous inc - flush it */
+ sql_dict_prev_inc_flush(ctx);
+ }
+
+ if (!array_is_created(&ctx->prev_inc))
+ i_array_init(&ctx->prev_inc, 4);
+ /* Either this is the first inc, or this can be merged with the
+ previous inc. */
+ struct sql_dict_prev *prev_inc = array_append_space(&ctx->prev_inc);
+ prev_inc->map = map;
+ prev_inc->key = i_strdup(key);
+ prev_inc->value.diff = diff;
+}
+
+static struct dict sql_dict = {
+ .name = "sql",
+
+ {
+ .init = sql_dict_init,
+ .deinit = sql_dict_deinit,
+ .wait = sql_dict_wait,
+ .lookup = sql_dict_lookup,
+ .iterate_init = sql_dict_iterate_init,
+ .iterate = sql_dict_iterate,
+ .iterate_deinit = sql_dict_iterate_deinit,
+ .transaction_init = sql_dict_transaction_init,
+ .transaction_commit = sql_dict_transaction_commit,
+ .transaction_rollback = sql_dict_transaction_rollback,
+ .set = sql_dict_set,
+ .unset = sql_dict_unset,
+ .atomic_inc = sql_dict_atomic_inc,
+ .lookup_async = sql_dict_lookup_async,
+ }
+};
+
+static struct dict *dict_sql_drivers;
+
+void dict_sql_register(void)
+{
+ const struct sql_db *const *drivers;
+ unsigned int i, count;
+
+ dict_sql_db_cache = sql_db_cache_init(DICT_SQL_MAX_UNUSED_CONNECTIONS);
+
+ /* @UNSAFE */
+ drivers = array_get(&sql_drivers, &count);
+ dict_sql_drivers = i_new(struct dict, count + 1);
+
+ for (i = 0; i < count; i++) {
+ dict_sql_drivers[i] = sql_dict;
+ dict_sql_drivers[i].name = drivers[i]->name;
+
+ dict_driver_register(&dict_sql_drivers[i]);
+ }
+}
+
+void dict_sql_unregister(void)
+{
+ int i;
+
+ for (i = 0; dict_sql_drivers[i].name != NULL; i++)
+ dict_driver_unregister(&dict_sql_drivers[i]);
+ i_free(dict_sql_drivers);
+ sql_db_cache_deinit(&dict_sql_db_cache);
+ dict_sql_settings_deinit();
+}
diff --git a/src/lib-dict-backend/dict-sql.h b/src/lib-dict-backend/dict-sql.h
new file mode 100644
index 0000000..418a650
--- /dev/null
+++ b/src/lib-dict-backend/dict-sql.h
@@ -0,0 +1,7 @@
+#ifndef DICT_SQL_H
+#define DICT_SQL_H
+
+void dict_sql_register(void);
+void dict_sql_unregister(void);
+
+#endif
diff --git a/src/lib-dict-backend/dict.conf b/src/lib-dict-backend/dict.conf
new file mode 100644
index 0000000..04dd63a
--- /dev/null
+++ b/src/lib-dict-backend/dict.conf
@@ -0,0 +1,49 @@
+connect = host=localhost
+
+map {
+ pattern = shared/dictmap/$key1/$key2
+ table = table
+ value_field = value
+ value_type = string
+
+ fields {
+ a = $key1
+ b = $key2
+ }
+}
+
+map {
+ pattern = shared/counters/$class/$name
+ table = counters
+ value_field = value
+ value_type = uint
+
+ fields {
+ class = $class
+ name = $name
+ }
+}
+
+map {
+ pattern = priv/quota/bytes
+ table = quota
+ username_field = username
+ value_field = bytes
+ value_type = uint
+}
+
+map {
+ pattern = priv/quota/count
+ table = quota
+ username_field = username
+ value_field = count
+ value_type = uint
+}
+
+map {
+ pattern = priv/quota/folders
+ table = quota
+ username_field = username
+ value_field = folders
+ value_type = uint
+}
diff --git a/src/lib-dict-backend/test-dict-sql.c b/src/lib-dict-backend/test-dict-sql.c
new file mode 100644
index 0000000..4de45eb
--- /dev/null
+++ b/src/lib-dict-backend/test-dict-sql.c
@@ -0,0 +1,314 @@
+/* Copyright (c) 2017-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "test-lib.h"
+#include "sql-api.h"
+#include "dict.h"
+#include "dict-private.h"
+#include "dict-sql.h"
+#include "dict-sql-private.h"
+#include "driver-test.h"
+
+struct dict_op_settings dict_op_settings = {
+ .username = "testuser",
+};
+
+static void test_setup(struct dict **dict_r)
+{
+ const char *error = NULL;
+ struct dict_settings set = {
+ .base_dir = "."
+ };
+ struct dict *dict = NULL;
+
+ if (dict_init("mysql:" DICT_SRC_DIR "/dict.conf", &set, &dict, &error) < 0)
+ i_fatal("cannot initialize dict: %s", error);
+
+ *dict_r = dict;
+}
+
+static void test_teardown(struct dict **_dict)
+{
+ struct dict *dict = *_dict;
+ *_dict = NULL;
+ if (dict != NULL) {
+ dict_deinit(&dict);
+ }
+}
+
+static void test_set_expected(struct dict *_dict,
+ const struct test_driver_result *result)
+{
+ struct sql_dict *dict =
+ (struct sql_dict *)_dict;
+
+ sql_driver_test_add_expected_result(dict->db, result);
+}
+
+static void test_lookup_one(void)
+{
+ const char *value = NULL, *error = NULL;
+ struct test_driver_result_set rset = {
+ .rows = 1,
+ .cols = 1,
+ .col_names = (const char *[]){"value", NULL},
+ .row_data = (const char **[]){(const char*[]){"one", NULL}},
+ };
+ struct test_driver_result res = {
+ .nqueries = 1,
+ .queries = (const char *[]){"SELECT value FROM table WHERE a = 'hello' AND b = 'world'", NULL},
+ .result = &rset,
+ };
+ const struct dict_op_settings set = {
+ .username = "testuser",
+ };
+ struct dict *dict;
+ pool_t pool = pool_datastack_create();
+
+ test_begin("dict lookup one");
+ test_setup(&dict);
+
+ test_set_expected(dict, &res);
+
+ test_assert(dict_lookup(dict, &set, pool, "shared/dictmap/hello/world", &value, &error) == 1);
+ test_assert_strcmp(value, "one");
+ if (error != NULL)
+ i_error("dict_lookup failed: %s", error);
+ test_teardown(&dict);
+ test_end();
+}
+
+static void test_atomic_inc(void)
+{
+ const char *error;
+ struct test_driver_result res = {
+ .nqueries = 3,
+ .queries = (const char *[]){
+ "UPDATE counters SET value=value+128 WHERE class = 'global' AND name = 'counter'",
+ "UPDATE quota SET bytes=bytes+128,count=count+1 WHERE username = 'testuser'",
+ "UPDATE quota SET bytes=bytes+128,count=count+1,folders=folders+123 WHERE username = 'testuser'",
+ NULL},
+ .result = NULL,
+ };
+ struct dict_op_settings set = {
+ .username = "testuser",
+ };
+ struct dict *dict;
+
+ test_begin("dict atomic inc");
+ test_setup(&dict);
+
+ test_set_expected(dict, &res);
+
+ /* 1 field */
+ struct dict_transaction_context *ctx = dict_transaction_begin(dict, &set);
+ dict_atomic_inc(ctx, "shared/counters/global/counter", 128);
+ test_assert(dict_transaction_commit(&ctx, &error) == 0);
+ if (error != NULL)
+ i_error("dict_transaction_commit failed: %s", error);
+ error = NULL;
+
+ /* 2 fields */
+ ctx = dict_transaction_begin(dict, &set);
+ dict_atomic_inc(ctx, "priv/quota/bytes", 128);
+ dict_atomic_inc(ctx, "priv/quota/count", 1);
+ test_assert(dict_transaction_commit(&ctx, &error) == 0);
+ if (error != NULL)
+ i_error("dict_transaction_commit failed: %s", error);
+ error = NULL;
+
+ /* 3 fields */
+ ctx = dict_transaction_begin(dict, &set);
+ dict_atomic_inc(ctx, "priv/quota/bytes", 128);
+ dict_atomic_inc(ctx, "priv/quota/count", 1);
+ dict_atomic_inc(ctx, "priv/quota/folders", 123);
+ test_assert(dict_transaction_commit(&ctx, &error) == 0);
+ if (error != NULL)
+ i_error("dict_transaction_commit failed: %s", error);
+ test_teardown(&dict);
+ test_end();
+}
+
+static void test_set(void)
+{
+ const char *error;
+ struct test_driver_result res = {
+ .affected_rows = 1,
+ .nqueries = 3,
+ .queries = (const char *[]){
+ "INSERT INTO counters (value,class,name) VALUES (128,'global','counter') ON DUPLICATE KEY UPDATE value=128",
+ "INSERT INTO quota (bytes,count,username) VALUES (128,1,'testuser') ON DUPLICATE KEY UPDATE bytes=128,count=1",
+ "INSERT INTO quota (bytes,count,folders,username) VALUES (128,1,123,'testuser') ON DUPLICATE KEY UPDATE bytes=128,count=1,folders=123",
+ NULL},
+ .result = NULL,
+ };
+ struct dict *dict;
+
+ test_begin("dict set");
+ test_setup(&dict);
+
+ test_set_expected(dict, &res);
+
+ /* 1 field */
+ struct dict_transaction_context *ctx = dict_transaction_begin(dict, &dict_op_settings);
+ dict_set(ctx, "shared/counters/global/counter", "128");
+ test_assert(dict_transaction_commit(&ctx, &error) == 1);
+ if (error != NULL)
+ i_error("dict_transaction_commit failed: %s", error);
+ error = NULL;
+
+ /* 2 fields */
+ ctx = dict_transaction_begin(dict, &dict_op_settings);
+ dict_set(ctx, "priv/quota/bytes", "128");
+ dict_set(ctx, "priv/quota/count", "1");
+ test_assert(dict_transaction_commit(&ctx, &error) == 1);
+ if (error != NULL)
+ i_error("dict_transaction_commit failed: %s", error);
+ error = NULL;
+
+ /* 3 fields */
+ ctx = dict_transaction_begin(dict, &dict_op_settings);
+ dict_set(ctx, "priv/quota/bytes", "128");
+ dict_set(ctx, "priv/quota/count", "1");
+ dict_set(ctx, "priv/quota/folders", "123");
+ test_assert(dict_transaction_commit(&ctx, &error) == 1);
+ if (error != NULL)
+ i_error("dict_transaction_commit failed: %s", error);
+ test_teardown(&dict);
+ test_end();
+}
+
+static void test_unset(void)
+{
+ const char *error;
+ struct test_driver_result res = {
+ .affected_rows = 1,
+ .nqueries = 3,
+ .queries = (const char *[]){
+ "DELETE FROM counters WHERE class = 'global' AND name = 'counter'",
+ "DELETE FROM quota WHERE username = 'testuser'",
+ "DELETE FROM quota WHERE username = 'testuser'",
+ NULL},
+ .result = NULL,
+ };
+ struct dict *dict;
+
+ test_begin("dict unset");
+ test_setup(&dict);
+
+ test_set_expected(dict, &res);
+
+ struct dict_transaction_context *ctx = dict_transaction_begin(dict, &dict_op_settings);
+ dict_unset(ctx, "shared/counters/global/counter");
+ test_assert(dict_transaction_commit(&ctx, &error) == 1);
+ if (error != NULL)
+ i_error("dict_transaction_commit failed: %s", error);
+ error = NULL;
+ ctx = dict_transaction_begin(dict, &dict_op_settings);
+ dict_unset(ctx, "priv/quota/bytes");
+ dict_unset(ctx, "priv/quota/count");
+ test_assert(dict_transaction_commit(&ctx, &error) == 1);
+ if (error != NULL)
+ i_error("dict_transaction_commit failed: %s", error);
+ test_teardown(&dict);
+ test_end();
+}
+
+static void test_iterate(void)
+{
+ const char *key = NULL, *value = NULL, *error;
+ struct test_driver_result_set rset = {
+ .rows = 5,
+ .cols = 2,
+ .col_names = (const char *[]){"value", "name", NULL},
+ .row_data = (const char **[]){
+ (const char*[]){"one", "counter", NULL},
+ (const char*[]){"two", "counter", NULL},
+ (const char*[]){"three", "counter", NULL},
+ (const char*[]){"four", "counter", NULL},
+ (const char*[]){"five", "counter", NULL},
+ },
+ };
+ struct test_driver_result res = {
+ .nqueries = 1,
+ .queries = (const char *[]){
+ "SELECT value,name FROM counters WHERE class = 'global' AND name = 'counter'",
+ NULL},
+ .result = &rset,
+ };
+ struct dict *dict;
+
+ test_begin("dict iterate");
+ test_setup(&dict);
+
+ test_set_expected(dict, &res);
+
+ struct dict_iterate_context *iter =
+ dict_iterate_init(dict, &dict_op_settings, "shared/counters/global/counter",
+ DICT_ITERATE_FLAG_EXACT_KEY);
+
+ size_t idx = 0;
+ while(dict_iterate(iter, &key, &value)) {
+ i_assert(idx < rset.rows);
+ test_assert_strcmp_idx(key, "shared/counters/global/counter", idx);
+ test_assert_strcmp_idx(value, rset.row_data[idx][0], idx);
+ idx++;
+ }
+
+ test_assert(idx == rset.rows);
+ test_assert(dict_iterate_deinit(&iter, &error) == 0);
+ if (error != NULL)
+ i_error("dict_iterate_deinit failed: %s", error);
+ error = NULL;
+
+ res.queries = (const char*[]){
+ "SELECT value,name FROM counters WHERE class = 'global' AND name LIKE '%' AND name NOT LIKE '%/%'",
+ NULL
+ };
+
+ res.cur = 0;
+ res.result->cur = 0;
+
+ test_set_expected(dict, &res);
+
+ iter = dict_iterate_init(dict, &dict_op_settings, "shared/counters/global/", 0);
+
+ idx = 0;
+
+ while(dict_iterate(iter, &key, &value)) {
+ i_assert(idx < rset.rows);
+ test_assert_strcmp_idx(key, "shared/counters/global/counter", idx);
+ test_assert_strcmp_idx(value, rset.row_data[idx][0], idx);
+ idx++;
+ }
+
+ test_assert(idx == rset.rows);
+ test_assert(dict_iterate_deinit(&iter, &error) == 0);
+ if (error != NULL)
+ i_error("dict_iterate_deinit failed: %s", error);
+ test_teardown(&dict);
+ test_end();
+}
+
+int main(void) {
+ sql_drivers_init();
+ sql_driver_test_register();
+ dict_sql_register();
+
+ static void (*const test_functions[])(void) = {
+ test_lookup_one,
+ test_atomic_inc,
+ test_set,
+ test_unset,
+ test_iterate,
+ NULL
+ };
+
+ int ret = test_run(test_functions);
+
+ dict_sql_unregister();
+ sql_driver_test_unregister();
+ sql_drivers_deinit();
+
+ return ret;
+}
diff --git a/src/lib-dict-extra/Makefile.am b/src/lib-dict-extra/Makefile.am
new file mode 100644
index 0000000..ce4ec4f
--- /dev/null
+++ b/src/lib-dict-extra/Makefile.am
@@ -0,0 +1,35 @@
+noinst_LTLIBRARIES = libdict_extra.la
+
+dict_drivers = @dict_drivers@
+
+AM_CPPFLAGS = \
+ -I$(top_srcdir)/src/lib \
+ -I$(top_srcdir)/src/lib-test \
+ -I$(top_srcdir)/src/lib-dict \
+ -I$(top_srcdir)/src/lib-fs \
+ -I$(top_srcdir)/src/lib-settings
+
+libdict_extra_la_SOURCES = \
+ dict-fs.c \
+ dict-register.c
+
+NOPLUGIN_LDFLAGS =
+
+test_programs = \
+ test-dict-fs
+
+noinst_PROGRAMS = $(test_programs)
+
+test_libs = \
+ ../lib-test/libtest.la \
+ ../lib-dict/libdict.la \
+ ../lib/liblib.la
+
+test_dict_fs_SOURCES = test-dict-fs.c
+test_dict_fs_LDADD = $(noinst_LTLIBRARIES) ../lib-fs/libfs.la $(test_libs) $(MODULE_LIBS)
+test_dict_fs_DEPENDENCIES = $(noinst_LTLIBRARIES) ../lib-fs/libfs.la $(test_libs)
+
+check-local:
+ for bin in $(test_programs) $(check_PROGRAMS); do \
+ if ! $(RUN_TEST) ./$$bin; then exit 1; fi; \
+ done
diff --git a/src/lib-dict-extra/Makefile.in b/src/lib-dict-extra/Makefile.in
new file mode 100644
index 0000000..39d3036
--- /dev/null
+++ b/src/lib-dict-extra/Makefile.in
@@ -0,0 +1,796 @@
+# Makefile.in generated by automake 1.16.1 from Makefile.am.
+# @configure_input@
+
+# Copyright (C) 1994-2018 Free Software Foundation, Inc.
+
+# This Makefile.in is free software; the Free Software Foundation
+# gives unlimited permission to copy and/or distribute it,
+# with or without modifications, as long as this notice is preserved.
+
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY, to the extent permitted by law; without
+# even the implied warranty of MERCHANTABILITY or FITNESS FOR A
+# PARTICULAR PURPOSE.
+
+@SET_MAKE@
+
+
+VPATH = @srcdir@
+am__is_gnu_make = { \
+ if test -z '$(MAKELEVEL)'; then \
+ false; \
+ elif test -n '$(MAKE_HOST)'; then \
+ true; \
+ elif test -n '$(MAKE_VERSION)' && test -n '$(CURDIR)'; then \
+ true; \
+ else \
+ false; \
+ fi; \
+}
+am__make_running_with_option = \
+ case $${target_option-} in \
+ ?) ;; \
+ *) echo "am__make_running_with_option: internal error: invalid" \
+ "target option '$${target_option-}' specified" >&2; \
+ exit 1;; \
+ esac; \
+ has_opt=no; \
+ sane_makeflags=$$MAKEFLAGS; \
+ if $(am__is_gnu_make); then \
+ sane_makeflags=$$MFLAGS; \
+ else \
+ case $$MAKEFLAGS in \
+ *\\[\ \ ]*) \
+ bs=\\; \
+ sane_makeflags=`printf '%s\n' "$$MAKEFLAGS" \
+ | sed "s/$$bs$$bs[$$bs $$bs ]*//g"`;; \
+ esac; \
+ fi; \
+ skip_next=no; \
+ strip_trailopt () \
+ { \
+ flg=`printf '%s\n' "$$flg" | sed "s/$$1.*$$//"`; \
+ }; \
+ for flg in $$sane_makeflags; do \
+ test $$skip_next = yes && { skip_next=no; continue; }; \
+ case $$flg in \
+ *=*|--*) continue;; \
+ -*I) strip_trailopt 'I'; skip_next=yes;; \
+ -*I?*) strip_trailopt 'I';; \
+ -*O) strip_trailopt 'O'; skip_next=yes;; \
+ -*O?*) strip_trailopt 'O';; \
+ -*l) strip_trailopt 'l'; skip_next=yes;; \
+ -*l?*) strip_trailopt 'l';; \
+ -[dEDm]) skip_next=yes;; \
+ -[JT]) skip_next=yes;; \
+ esac; \
+ case $$flg in \
+ *$$target_option*) has_opt=yes; break;; \
+ esac; \
+ done; \
+ test $$has_opt = yes
+am__make_dryrun = (target_option=n; $(am__make_running_with_option))
+am__make_keepgoing = (target_option=k; $(am__make_running_with_option))
+pkgdatadir = $(datadir)/@PACKAGE@
+pkgincludedir = $(includedir)/@PACKAGE@
+pkglibdir = $(libdir)/@PACKAGE@
+pkglibexecdir = $(libexecdir)/@PACKAGE@
+am__cd = CDPATH="$${ZSH_VERSION+.}$(PATH_SEPARATOR)" && cd
+install_sh_DATA = $(install_sh) -c -m 644
+install_sh_PROGRAM = $(install_sh) -c
+install_sh_SCRIPT = $(install_sh) -c
+INSTALL_HEADER = $(INSTALL_DATA)
+transform = $(program_transform_name)
+NORMAL_INSTALL = :
+PRE_INSTALL = :
+POST_INSTALL = :
+NORMAL_UNINSTALL = :
+PRE_UNINSTALL = :
+POST_UNINSTALL = :
+build_triplet = @build@
+host_triplet = @host@
+noinst_PROGRAMS = $(am__EXEEXT_1)
+subdir = src/lib-dict-extra
+ACLOCAL_M4 = $(top_srcdir)/aclocal.m4
+am__aclocal_m4_deps = $(top_srcdir)/m4/ac_checktype2.m4 \
+ $(top_srcdir)/m4/ac_typeof.m4 $(top_srcdir)/m4/arc4random.m4 \
+ $(top_srcdir)/m4/blockdev.m4 $(top_srcdir)/m4/c99_vsnprintf.m4 \
+ $(top_srcdir)/m4/clock_gettime.m4 $(top_srcdir)/m4/crypt.m4 \
+ $(top_srcdir)/m4/crypt_xpg6.m4 $(top_srcdir)/m4/dbqlk.m4 \
+ $(top_srcdir)/m4/dirent_dtype.m4 $(top_srcdir)/m4/dovecot.m4 \
+ $(top_srcdir)/m4/fd_passing.m4 $(top_srcdir)/m4/fdatasync.m4 \
+ $(top_srcdir)/m4/flexible_array_member.m4 \
+ $(top_srcdir)/m4/glibc.m4 $(top_srcdir)/m4/gmtime_max.m4 \
+ $(top_srcdir)/m4/gmtime_tm_gmtoff.m4 \
+ $(top_srcdir)/m4/ioloop.m4 $(top_srcdir)/m4/iovec.m4 \
+ $(top_srcdir)/m4/ipv6.m4 $(top_srcdir)/m4/libcap.m4 \
+ $(top_srcdir)/m4/libtool.m4 $(top_srcdir)/m4/libwrap.m4 \
+ $(top_srcdir)/m4/linux_mremap.m4 $(top_srcdir)/m4/ltoptions.m4 \
+ $(top_srcdir)/m4/ltsugar.m4 $(top_srcdir)/m4/ltversion.m4 \
+ $(top_srcdir)/m4/lt~obsolete.m4 $(top_srcdir)/m4/mmap_write.m4 \
+ $(top_srcdir)/m4/mntctl.m4 $(top_srcdir)/m4/modules.m4 \
+ $(top_srcdir)/m4/notify.m4 $(top_srcdir)/m4/nsl.m4 \
+ $(top_srcdir)/m4/off_t_max.m4 $(top_srcdir)/m4/pkg.m4 \
+ $(top_srcdir)/m4/pr_set_dumpable.m4 \
+ $(top_srcdir)/m4/q_quotactl.m4 $(top_srcdir)/m4/quota.m4 \
+ $(top_srcdir)/m4/random.m4 $(top_srcdir)/m4/rlimit.m4 \
+ $(top_srcdir)/m4/sendfile.m4 $(top_srcdir)/m4/size_t_signed.m4 \
+ $(top_srcdir)/m4/sockpeercred.m4 $(top_srcdir)/m4/sql.m4 \
+ $(top_srcdir)/m4/ssl.m4 $(top_srcdir)/m4/st_tim.m4 \
+ $(top_srcdir)/m4/static_array.m4 $(top_srcdir)/m4/test_with.m4 \
+ $(top_srcdir)/m4/time_t.m4 $(top_srcdir)/m4/typeof.m4 \
+ $(top_srcdir)/m4/typeof_dev_t.m4 \
+ $(top_srcdir)/m4/uoff_t_max.m4 $(top_srcdir)/m4/vararg.m4 \
+ $(top_srcdir)/m4/want_apparmor.m4 \
+ $(top_srcdir)/m4/want_bsdauth.m4 \
+ $(top_srcdir)/m4/want_bzlib.m4 \
+ $(top_srcdir)/m4/want_cassandra.m4 \
+ $(top_srcdir)/m4/want_cdb.m4 \
+ $(top_srcdir)/m4/want_checkpassword.m4 \
+ $(top_srcdir)/m4/want_clucene.m4 $(top_srcdir)/m4/want_db.m4 \
+ $(top_srcdir)/m4/want_gssapi.m4 $(top_srcdir)/m4/want_icu.m4 \
+ $(top_srcdir)/m4/want_ldap.m4 $(top_srcdir)/m4/want_lua.m4 \
+ $(top_srcdir)/m4/want_lz4.m4 $(top_srcdir)/m4/want_lzma.m4 \
+ $(top_srcdir)/m4/want_mysql.m4 $(top_srcdir)/m4/want_pam.m4 \
+ $(top_srcdir)/m4/want_passwd.m4 $(top_srcdir)/m4/want_pgsql.m4 \
+ $(top_srcdir)/m4/want_prefetch.m4 \
+ $(top_srcdir)/m4/want_shadow.m4 \
+ $(top_srcdir)/m4/want_sodium.m4 $(top_srcdir)/m4/want_solr.m4 \
+ $(top_srcdir)/m4/want_sqlite.m4 \
+ $(top_srcdir)/m4/want_stemmer.m4 \
+ $(top_srcdir)/m4/want_systemd.m4 \
+ $(top_srcdir)/m4/want_textcat.m4 \
+ $(top_srcdir)/m4/want_unwind.m4 $(top_srcdir)/m4/want_zlib.m4 \
+ $(top_srcdir)/m4/want_zstd.m4 $(top_srcdir)/configure.ac
+am__configure_deps = $(am__aclocal_m4_deps) $(CONFIGURE_DEPENDENCIES) \
+ $(ACLOCAL_M4)
+DIST_COMMON = $(srcdir)/Makefile.am $(am__DIST_COMMON)
+mkinstalldirs = $(install_sh) -d
+CONFIG_HEADER = $(top_builddir)/config.h
+CONFIG_CLEAN_FILES =
+CONFIG_CLEAN_VPATH_FILES =
+am__EXEEXT_1 = test-dict-fs$(EXEEXT)
+PROGRAMS = $(noinst_PROGRAMS)
+LTLIBRARIES = $(noinst_LTLIBRARIES)
+libdict_extra_la_LIBADD =
+am_libdict_extra_la_OBJECTS = dict-fs.lo dict-register.lo
+libdict_extra_la_OBJECTS = $(am_libdict_extra_la_OBJECTS)
+AM_V_lt = $(am__v_lt_@AM_V@)
+am__v_lt_ = $(am__v_lt_@AM_DEFAULT_V@)
+am__v_lt_0 = --silent
+am__v_lt_1 =
+am_test_dict_fs_OBJECTS = test-dict-fs.$(OBJEXT)
+test_dict_fs_OBJECTS = $(am_test_dict_fs_OBJECTS)
+am__DEPENDENCIES_1 =
+AM_V_P = $(am__v_P_@AM_V@)
+am__v_P_ = $(am__v_P_@AM_DEFAULT_V@)
+am__v_P_0 = false
+am__v_P_1 = :
+AM_V_GEN = $(am__v_GEN_@AM_V@)
+am__v_GEN_ = $(am__v_GEN_@AM_DEFAULT_V@)
+am__v_GEN_0 = @echo " GEN " $@;
+am__v_GEN_1 =
+AM_V_at = $(am__v_at_@AM_V@)
+am__v_at_ = $(am__v_at_@AM_DEFAULT_V@)
+am__v_at_0 = @
+am__v_at_1 =
+DEFAULT_INCLUDES = -I.@am__isrc@ -I$(top_builddir)
+depcomp = $(SHELL) $(top_srcdir)/depcomp
+am__maybe_remake_depfiles = depfiles
+am__depfiles_remade = ./$(DEPDIR)/dict-fs.Plo \
+ ./$(DEPDIR)/dict-register.Plo ./$(DEPDIR)/test-dict-fs.Po
+am__mv = mv -f
+COMPILE = $(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) \
+ $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS)
+LTCOMPILE = $(LIBTOOL) $(AM_V_lt) --tag=CC $(AM_LIBTOOLFLAGS) \
+ $(LIBTOOLFLAGS) --mode=compile $(CC) $(DEFS) \
+ $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) $(CPPFLAGS) \
+ $(AM_CFLAGS) $(CFLAGS)
+AM_V_CC = $(am__v_CC_@AM_V@)
+am__v_CC_ = $(am__v_CC_@AM_DEFAULT_V@)
+am__v_CC_0 = @echo " CC " $@;
+am__v_CC_1 =
+CCLD = $(CC)
+LINK = $(LIBTOOL) $(AM_V_lt) --tag=CC $(AM_LIBTOOLFLAGS) \
+ $(LIBTOOLFLAGS) --mode=link $(CCLD) $(AM_CFLAGS) $(CFLAGS) \
+ $(AM_LDFLAGS) $(LDFLAGS) -o $@
+AM_V_CCLD = $(am__v_CCLD_@AM_V@)
+am__v_CCLD_ = $(am__v_CCLD_@AM_DEFAULT_V@)
+am__v_CCLD_0 = @echo " CCLD " $@;
+am__v_CCLD_1 =
+SOURCES = $(libdict_extra_la_SOURCES) $(test_dict_fs_SOURCES)
+DIST_SOURCES = $(libdict_extra_la_SOURCES) $(test_dict_fs_SOURCES)
+am__can_run_installinfo = \
+ case $$AM_UPDATE_INFO_DIR in \
+ n|no|NO) false;; \
+ *) (install-info --version) >/dev/null 2>&1;; \
+ esac
+am__tagged_files = $(HEADERS) $(SOURCES) $(TAGS_FILES) $(LISP)
+# Read a list of newline-separated strings from the standard input,
+# and print each of them once, without duplicates. Input order is
+# *not* preserved.
+am__uniquify_input = $(AWK) '\
+ BEGIN { nonempty = 0; } \
+ { items[$$0] = 1; nonempty = 1; } \
+ END { if (nonempty) { for (i in items) print i; }; } \
+'
+# Make sure the list of sources is unique. This is necessary because,
+# e.g., the same source file might be shared among _SOURCES variables
+# for different programs/libraries.
+am__define_uniq_tagged_files = \
+ list='$(am__tagged_files)'; \
+ unique=`for i in $$list; do \
+ if test -f "$$i"; then echo $$i; else echo $(srcdir)/$$i; fi; \
+ done | $(am__uniquify_input)`
+ETAGS = etags
+CTAGS = ctags
+am__DIST_COMMON = $(srcdir)/Makefile.in $(top_srcdir)/depcomp
+DISTFILES = $(DIST_COMMON) $(DIST_SOURCES) $(TEXINFOS) $(EXTRA_DIST)
+ACLOCAL = @ACLOCAL@
+ACLOCAL_AMFLAGS = @ACLOCAL_AMFLAGS@
+AMTAR = @AMTAR@
+AM_DEFAULT_VERBOSITY = @AM_DEFAULT_VERBOSITY@
+APPARMOR_LIBS = @APPARMOR_LIBS@
+AR = @AR@
+AUTH_CFLAGS = @AUTH_CFLAGS@
+AUTH_LIBS = @AUTH_LIBS@
+AUTOCONF = @AUTOCONF@
+AUTOHEADER = @AUTOHEADER@
+AUTOMAKE = @AUTOMAKE@
+AWK = @AWK@
+BINARY_CFLAGS = @BINARY_CFLAGS@
+BINARY_LDFLAGS = @BINARY_LDFLAGS@
+BISON = @BISON@
+CASSANDRA_CFLAGS = @CASSANDRA_CFLAGS@
+CASSANDRA_LIBS = @CASSANDRA_LIBS@
+CC = @CC@
+CCDEPMODE = @CCDEPMODE@
+CDB_LIBS = @CDB_LIBS@
+CFLAGS = @CFLAGS@
+CLUCENE_CFLAGS = @CLUCENE_CFLAGS@
+CLUCENE_LIBS = @CLUCENE_LIBS@
+COMPRESS_LIBS = @COMPRESS_LIBS@
+CPP = @CPP@
+CPPFLAGS = @CPPFLAGS@
+CRYPT_LIBS = @CRYPT_LIBS@
+CXX = @CXX@
+CXXCPP = @CXXCPP@
+CXXDEPMODE = @CXXDEPMODE@
+CXXFLAGS = @CXXFLAGS@
+CYGPATH_W = @CYGPATH_W@
+DEFS = @DEFS@
+DEPDIR = @DEPDIR@
+DICT_LIBS = @DICT_LIBS@
+DLLIB = @DLLIB@
+DLLTOOL = @DLLTOOL@
+DSYMUTIL = @DSYMUTIL@
+DUMPBIN = @DUMPBIN@
+ECHO_C = @ECHO_C@
+ECHO_N = @ECHO_N@
+ECHO_T = @ECHO_T@
+EGREP = @EGREP@
+EXEEXT = @EXEEXT@
+FGREP = @FGREP@
+FLEX = @FLEX@
+FUZZER_CPPFLAGS = @FUZZER_CPPFLAGS@
+FUZZER_LDFLAGS = @FUZZER_LDFLAGS@
+GREP = @GREP@
+INSTALL = @INSTALL@
+INSTALL_DATA = @INSTALL_DATA@
+INSTALL_PROGRAM = @INSTALL_PROGRAM@
+INSTALL_SCRIPT = @INSTALL_SCRIPT@
+INSTALL_STRIP_PROGRAM = @INSTALL_STRIP_PROGRAM@
+KRB5CONFIG = @KRB5CONFIG@
+KRB5_CFLAGS = @KRB5_CFLAGS@
+KRB5_LIBS = @KRB5_LIBS@
+LD = @LD@
+LDAP_LIBS = @LDAP_LIBS@
+LDFLAGS = @LDFLAGS@
+LD_NO_WHOLE_ARCHIVE = @LD_NO_WHOLE_ARCHIVE@
+LD_WHOLE_ARCHIVE = @LD_WHOLE_ARCHIVE@
+LIBCAP = @LIBCAP@
+LIBDOVECOT = @LIBDOVECOT@
+LIBDOVECOT_COMPRESS = @LIBDOVECOT_COMPRESS@
+LIBDOVECOT_DEPS = @LIBDOVECOT_DEPS@
+LIBDOVECOT_DSYNC = @LIBDOVECOT_DSYNC@
+LIBDOVECOT_LA_LIBS = @LIBDOVECOT_LA_LIBS@
+LIBDOVECOT_LDA = @LIBDOVECOT_LDA@
+LIBDOVECOT_LDAP = @LIBDOVECOT_LDAP@
+LIBDOVECOT_LIBFTS = @LIBDOVECOT_LIBFTS@
+LIBDOVECOT_LIBFTS_DEPS = @LIBDOVECOT_LIBFTS_DEPS@
+LIBDOVECOT_LOGIN = @LIBDOVECOT_LOGIN@
+LIBDOVECOT_LUA = @LIBDOVECOT_LUA@
+LIBDOVECOT_LUA_DEPS = @LIBDOVECOT_LUA_DEPS@
+LIBDOVECOT_SQL = @LIBDOVECOT_SQL@
+LIBDOVECOT_STORAGE = @LIBDOVECOT_STORAGE@
+LIBDOVECOT_STORAGE_DEPS = @LIBDOVECOT_STORAGE_DEPS@
+LIBEXTTEXTCAT_CFLAGS = @LIBEXTTEXTCAT_CFLAGS@
+LIBEXTTEXTCAT_LIBS = @LIBEXTTEXTCAT_LIBS@
+LIBICONV = @LIBICONV@
+LIBICU_CFLAGS = @LIBICU_CFLAGS@
+LIBICU_LIBS = @LIBICU_LIBS@
+LIBOBJS = @LIBOBJS@
+LIBS = @LIBS@
+LIBSODIUM_CFLAGS = @LIBSODIUM_CFLAGS@
+LIBSODIUM_LIBS = @LIBSODIUM_LIBS@
+LIBTIRPC_CFLAGS = @LIBTIRPC_CFLAGS@
+LIBTIRPC_LIBS = @LIBTIRPC_LIBS@
+LIBTOOL = @LIBTOOL@
+LIBUNWIND_CFLAGS = @LIBUNWIND_CFLAGS@
+LIBUNWIND_LIBS = @LIBUNWIND_LIBS@
+LIBWRAP_LIBS = @LIBWRAP_LIBS@
+LINKED_STORAGE_LDADD = @LINKED_STORAGE_LDADD@
+LIPO = @LIPO@
+LN_S = @LN_S@
+LTLIBICONV = @LTLIBICONV@
+LTLIBOBJS = @LTLIBOBJS@
+LT_SYS_LIBRARY_PATH = @LT_SYS_LIBRARY_PATH@
+LUA_CFLAGS = @LUA_CFLAGS@
+LUA_LIBS = @LUA_LIBS@
+MAINT = @MAINT@
+MAKEINFO = @MAKEINFO@
+MANIFEST_TOOL = @MANIFEST_TOOL@
+MKDIR_P = @MKDIR_P@
+MODULE_LIBS = @MODULE_LIBS@
+MODULE_SUFFIX = @MODULE_SUFFIX@
+MYSQL_CFLAGS = @MYSQL_CFLAGS@
+MYSQL_CONFIG = @MYSQL_CONFIG@
+MYSQL_LIBS = @MYSQL_LIBS@
+NM = @NM@
+NMEDIT = @NMEDIT@
+NOPLUGIN_LDFLAGS =
+OBJDUMP = @OBJDUMP@
+OBJEXT = @OBJEXT@
+OTOOL = @OTOOL@
+OTOOL64 = @OTOOL64@
+PACKAGE = @PACKAGE@
+PACKAGE_BUGREPORT = @PACKAGE_BUGREPORT@
+PACKAGE_NAME = @PACKAGE_NAME@
+PACKAGE_STRING = @PACKAGE_STRING@
+PACKAGE_TARNAME = @PACKAGE_TARNAME@
+PACKAGE_URL = @PACKAGE_URL@
+PACKAGE_VERSION = @PACKAGE_VERSION@
+PANDOC = @PANDOC@
+PATH_SEPARATOR = @PATH_SEPARATOR@
+PGSQL_CFLAGS = @PGSQL_CFLAGS@
+PGSQL_LIBS = @PGSQL_LIBS@
+PG_CONFIG = @PG_CONFIG@
+PIE_CFLAGS = @PIE_CFLAGS@
+PIE_LDFLAGS = @PIE_LDFLAGS@
+PKG_CONFIG = @PKG_CONFIG@
+PKG_CONFIG_LIBDIR = @PKG_CONFIG_LIBDIR@
+PKG_CONFIG_PATH = @PKG_CONFIG_PATH@
+QUOTA_LIBS = @QUOTA_LIBS@
+RANLIB = @RANLIB@
+RELRO_LDFLAGS = @RELRO_LDFLAGS@
+RPCGEN = @RPCGEN@
+RUN_TEST = @RUN_TEST@
+SED = @SED@
+SETTING_FILES = @SETTING_FILES@
+SET_MAKE = @SET_MAKE@
+SHELL = @SHELL@
+SQLITE_CFLAGS = @SQLITE_CFLAGS@
+SQLITE_LIBS = @SQLITE_LIBS@
+SQL_CFLAGS = @SQL_CFLAGS@
+SQL_LIBS = @SQL_LIBS@
+SSL_CFLAGS = @SSL_CFLAGS@
+SSL_LIBS = @SSL_LIBS@
+STRIP = @STRIP@
+SYSTEMD_CFLAGS = @SYSTEMD_CFLAGS@
+SYSTEMD_LIBS = @SYSTEMD_LIBS@
+VALGRIND = @VALGRIND@
+VERSION = @VERSION@
+ZSTD_CFLAGS = @ZSTD_CFLAGS@
+ZSTD_LIBS = @ZSTD_LIBS@
+abs_builddir = @abs_builddir@
+abs_srcdir = @abs_srcdir@
+abs_top_builddir = @abs_top_builddir@
+abs_top_srcdir = @abs_top_srcdir@
+ac_ct_AR = @ac_ct_AR@
+ac_ct_CC = @ac_ct_CC@
+ac_ct_CXX = @ac_ct_CXX@
+ac_ct_DUMPBIN = @ac_ct_DUMPBIN@
+am__include = @am__include@
+am__leading_dot = @am__leading_dot@
+am__quote = @am__quote@
+am__tar = @am__tar@
+am__untar = @am__untar@
+bindir = @bindir@
+build = @build@
+build_alias = @build_alias@
+build_cpu = @build_cpu@
+build_os = @build_os@
+build_vendor = @build_vendor@
+builddir = @builddir@
+datadir = @datadir@
+datarootdir = @datarootdir@
+dict_drivers = @dict_drivers@
+docdir = @docdir@
+dvidir = @dvidir@
+exec_prefix = @exec_prefix@
+host = @host@
+host_alias = @host_alias@
+host_cpu = @host_cpu@
+host_os = @host_os@
+host_vendor = @host_vendor@
+htmldir = @htmldir@
+includedir = @includedir@
+infodir = @infodir@
+install_sh = @install_sh@
+libdir = @libdir@
+libexecdir = @libexecdir@
+localedir = @localedir@
+localstatedir = @localstatedir@
+mandir = @mandir@
+mkdir_p = @mkdir_p@
+moduledir = @moduledir@
+oldincludedir = @oldincludedir@
+pdfdir = @pdfdir@
+prefix = @prefix@
+program_transform_name = @program_transform_name@
+psdir = @psdir@
+rundir = @rundir@
+runstatedir = @runstatedir@
+sbindir = @sbindir@
+sharedstatedir = @sharedstatedir@
+sql_drivers = @sql_drivers@
+srcdir = @srcdir@
+ssldir = @ssldir@
+statedir = @statedir@
+sysconfdir = @sysconfdir@
+systemdservicetype = @systemdservicetype@
+systemdsystemunitdir = @systemdsystemunitdir@
+target_alias = @target_alias@
+top_build_prefix = @top_build_prefix@
+top_builddir = @top_builddir@
+top_srcdir = @top_srcdir@
+noinst_LTLIBRARIES = libdict_extra.la
+AM_CPPFLAGS = \
+ -I$(top_srcdir)/src/lib \
+ -I$(top_srcdir)/src/lib-test \
+ -I$(top_srcdir)/src/lib-dict \
+ -I$(top_srcdir)/src/lib-fs \
+ -I$(top_srcdir)/src/lib-settings
+
+libdict_extra_la_SOURCES = \
+ dict-fs.c \
+ dict-register.c
+
+test_programs = \
+ test-dict-fs
+
+test_libs = \
+ ../lib-test/libtest.la \
+ ../lib-dict/libdict.la \
+ ../lib/liblib.la
+
+test_dict_fs_SOURCES = test-dict-fs.c
+test_dict_fs_LDADD = $(noinst_LTLIBRARIES) ../lib-fs/libfs.la $(test_libs) $(MODULE_LIBS)
+test_dict_fs_DEPENDENCIES = $(noinst_LTLIBRARIES) ../lib-fs/libfs.la $(test_libs)
+all: all-am
+
+.SUFFIXES:
+.SUFFIXES: .c .lo .o .obj
+$(srcdir)/Makefile.in: @MAINTAINER_MODE_TRUE@ $(srcdir)/Makefile.am $(am__configure_deps)
+ @for dep in $?; do \
+ case '$(am__configure_deps)' in \
+ *$$dep*) \
+ ( cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh ) \
+ && { if test -f $@; then exit 0; else break; fi; }; \
+ exit 1;; \
+ esac; \
+ done; \
+ echo ' cd $(top_srcdir) && $(AUTOMAKE) --foreign src/lib-dict-extra/Makefile'; \
+ $(am__cd) $(top_srcdir) && \
+ $(AUTOMAKE) --foreign src/lib-dict-extra/Makefile
+Makefile: $(srcdir)/Makefile.in $(top_builddir)/config.status
+ @case '$?' in \
+ *config.status*) \
+ cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh;; \
+ *) \
+ echo ' cd $(top_builddir) && $(SHELL) ./config.status $(subdir)/$@ $(am__maybe_remake_depfiles)'; \
+ cd $(top_builddir) && $(SHELL) ./config.status $(subdir)/$@ $(am__maybe_remake_depfiles);; \
+ esac;
+
+$(top_builddir)/config.status: $(top_srcdir)/configure $(CONFIG_STATUS_DEPENDENCIES)
+ cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh
+
+$(top_srcdir)/configure: @MAINTAINER_MODE_TRUE@ $(am__configure_deps)
+ cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh
+$(ACLOCAL_M4): @MAINTAINER_MODE_TRUE@ $(am__aclocal_m4_deps)
+ cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh
+$(am__aclocal_m4_deps):
+
+clean-noinstPROGRAMS:
+ @list='$(noinst_PROGRAMS)'; test -n "$$list" || exit 0; \
+ echo " rm -f" $$list; \
+ rm -f $$list || exit $$?; \
+ test -n "$(EXEEXT)" || exit 0; \
+ list=`for p in $$list; do echo "$$p"; done | sed 's/$(EXEEXT)$$//'`; \
+ echo " rm -f" $$list; \
+ rm -f $$list
+
+clean-noinstLTLIBRARIES:
+ -test -z "$(noinst_LTLIBRARIES)" || rm -f $(noinst_LTLIBRARIES)
+ @list='$(noinst_LTLIBRARIES)'; \
+ locs=`for p in $$list; do echo $$p; done | \
+ sed 's|^[^/]*$$|.|; s|/[^/]*$$||; s|$$|/so_locations|' | \
+ sort -u`; \
+ test -z "$$locs" || { \
+ echo rm -f $${locs}; \
+ rm -f $${locs}; \
+ }
+
+libdict_extra.la: $(libdict_extra_la_OBJECTS) $(libdict_extra_la_DEPENDENCIES) $(EXTRA_libdict_extra_la_DEPENDENCIES)
+ $(AM_V_CCLD)$(LINK) $(libdict_extra_la_OBJECTS) $(libdict_extra_la_LIBADD) $(LIBS)
+
+test-dict-fs$(EXEEXT): $(test_dict_fs_OBJECTS) $(test_dict_fs_DEPENDENCIES) $(EXTRA_test_dict_fs_DEPENDENCIES)
+ @rm -f test-dict-fs$(EXEEXT)
+ $(AM_V_CCLD)$(LINK) $(test_dict_fs_OBJECTS) $(test_dict_fs_LDADD) $(LIBS)
+
+mostlyclean-compile:
+ -rm -f *.$(OBJEXT)
+
+distclean-compile:
+ -rm -f *.tab.c
+
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/dict-fs.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/dict-register.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/test-dict-fs.Po@am__quote@ # am--include-marker
+
+$(am__depfiles_remade):
+ @$(MKDIR_P) $(@D)
+ @echo '# dummy' >$@-t && $(am__mv) $@-t $@
+
+am--depfiles: $(am__depfiles_remade)
+
+.c.o:
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(COMPILE) -MT $@ -MD -MP -MF $(DEPDIR)/$*.Tpo -c -o $@ $<
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/$*.Tpo $(DEPDIR)/$*.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='$<' object='$@' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(COMPILE) -c -o $@ $<
+
+.c.obj:
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(COMPILE) -MT $@ -MD -MP -MF $(DEPDIR)/$*.Tpo -c -o $@ `$(CYGPATH_W) '$<'`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/$*.Tpo $(DEPDIR)/$*.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='$<' object='$@' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(COMPILE) -c -o $@ `$(CYGPATH_W) '$<'`
+
+.c.lo:
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(LTCOMPILE) -MT $@ -MD -MP -MF $(DEPDIR)/$*.Tpo -c -o $@ $<
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/$*.Tpo $(DEPDIR)/$*.Plo
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='$<' object='$@' libtool=yes @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(LTCOMPILE) -c -o $@ $<
+
+mostlyclean-libtool:
+ -rm -f *.lo
+
+clean-libtool:
+ -rm -rf .libs _libs
+
+ID: $(am__tagged_files)
+ $(am__define_uniq_tagged_files); mkid -fID $$unique
+tags: tags-am
+TAGS: tags
+
+tags-am: $(TAGS_DEPENDENCIES) $(am__tagged_files)
+ set x; \
+ here=`pwd`; \
+ $(am__define_uniq_tagged_files); \
+ shift; \
+ if test -z "$(ETAGS_ARGS)$$*$$unique"; then :; else \
+ test -n "$$unique" || unique=$$empty_fix; \
+ if test $$# -gt 0; then \
+ $(ETAGS) $(ETAGSFLAGS) $(AM_ETAGSFLAGS) $(ETAGS_ARGS) \
+ "$$@" $$unique; \
+ else \
+ $(ETAGS) $(ETAGSFLAGS) $(AM_ETAGSFLAGS) $(ETAGS_ARGS) \
+ $$unique; \
+ fi; \
+ fi
+ctags: ctags-am
+
+CTAGS: ctags
+ctags-am: $(TAGS_DEPENDENCIES) $(am__tagged_files)
+ $(am__define_uniq_tagged_files); \
+ test -z "$(CTAGS_ARGS)$$unique" \
+ || $(CTAGS) $(CTAGSFLAGS) $(AM_CTAGSFLAGS) $(CTAGS_ARGS) \
+ $$unique
+
+GTAGS:
+ here=`$(am__cd) $(top_builddir) && pwd` \
+ && $(am__cd) $(top_srcdir) \
+ && gtags -i $(GTAGS_ARGS) "$$here"
+cscopelist: cscopelist-am
+
+cscopelist-am: $(am__tagged_files)
+ list='$(am__tagged_files)'; \
+ case "$(srcdir)" in \
+ [\\/]* | ?:[\\/]*) sdir="$(srcdir)" ;; \
+ *) sdir=$(subdir)/$(srcdir) ;; \
+ esac; \
+ for i in $$list; do \
+ if test -f "$$i"; then \
+ echo "$(subdir)/$$i"; \
+ else \
+ echo "$$sdir/$$i"; \
+ fi; \
+ done >> $(top_builddir)/cscope.files
+
+distclean-tags:
+ -rm -f TAGS ID GTAGS GRTAGS GSYMS GPATH tags
+
+distdir: $(BUILT_SOURCES)
+ $(MAKE) $(AM_MAKEFLAGS) distdir-am
+
+distdir-am: $(DISTFILES)
+ @srcdirstrip=`echo "$(srcdir)" | sed 's/[].[^$$\\*]/\\\\&/g'`; \
+ topsrcdirstrip=`echo "$(top_srcdir)" | sed 's/[].[^$$\\*]/\\\\&/g'`; \
+ list='$(DISTFILES)'; \
+ dist_files=`for file in $$list; do echo $$file; done | \
+ sed -e "s|^$$srcdirstrip/||;t" \
+ -e "s|^$$topsrcdirstrip/|$(top_builddir)/|;t"`; \
+ case $$dist_files in \
+ */*) $(MKDIR_P) `echo "$$dist_files" | \
+ sed '/\//!d;s|^|$(distdir)/|;s,/[^/]*$$,,' | \
+ sort -u` ;; \
+ esac; \
+ for file in $$dist_files; do \
+ if test -f $$file || test -d $$file; then d=.; else d=$(srcdir); fi; \
+ if test -d $$d/$$file; then \
+ dir=`echo "/$$file" | sed -e 's,/[^/]*$$,,'`; \
+ if test -d "$(distdir)/$$file"; then \
+ find "$(distdir)/$$file" -type d ! -perm -700 -exec chmod u+rwx {} \;; \
+ fi; \
+ if test -d $(srcdir)/$$file && test $$d != $(srcdir); then \
+ cp -fpR $(srcdir)/$$file "$(distdir)$$dir" || exit 1; \
+ find "$(distdir)/$$file" -type d ! -perm -700 -exec chmod u+rwx {} \;; \
+ fi; \
+ cp -fpR $$d/$$file "$(distdir)$$dir" || exit 1; \
+ else \
+ test -f "$(distdir)/$$file" \
+ || cp -p $$d/$$file "$(distdir)/$$file" \
+ || exit 1; \
+ fi; \
+ done
+check-am: all-am
+ $(MAKE) $(AM_MAKEFLAGS) check-local
+check: check-am
+all-am: Makefile $(PROGRAMS) $(LTLIBRARIES)
+installdirs:
+install: install-am
+install-exec: install-exec-am
+install-data: install-data-am
+uninstall: uninstall-am
+
+install-am: all-am
+ @$(MAKE) $(AM_MAKEFLAGS) install-exec-am install-data-am
+
+installcheck: installcheck-am
+install-strip:
+ if test -z '$(STRIP)'; then \
+ $(MAKE) $(AM_MAKEFLAGS) INSTALL_PROGRAM="$(INSTALL_STRIP_PROGRAM)" \
+ install_sh_PROGRAM="$(INSTALL_STRIP_PROGRAM)" INSTALL_STRIP_FLAG=-s \
+ install; \
+ else \
+ $(MAKE) $(AM_MAKEFLAGS) INSTALL_PROGRAM="$(INSTALL_STRIP_PROGRAM)" \
+ install_sh_PROGRAM="$(INSTALL_STRIP_PROGRAM)" INSTALL_STRIP_FLAG=-s \
+ "INSTALL_PROGRAM_ENV=STRIPPROG='$(STRIP)'" install; \
+ fi
+mostlyclean-generic:
+
+clean-generic:
+
+distclean-generic:
+ -test -z "$(CONFIG_CLEAN_FILES)" || rm -f $(CONFIG_CLEAN_FILES)
+ -test . = "$(srcdir)" || test -z "$(CONFIG_CLEAN_VPATH_FILES)" || rm -f $(CONFIG_CLEAN_VPATH_FILES)
+
+maintainer-clean-generic:
+ @echo "This command is intended for maintainers to use"
+ @echo "it deletes files that may require special tools to rebuild."
+clean: clean-am
+
+clean-am: clean-generic clean-libtool clean-noinstLTLIBRARIES \
+ clean-noinstPROGRAMS mostlyclean-am
+
+distclean: distclean-am
+ -rm -f ./$(DEPDIR)/dict-fs.Plo
+ -rm -f ./$(DEPDIR)/dict-register.Plo
+ -rm -f ./$(DEPDIR)/test-dict-fs.Po
+ -rm -f Makefile
+distclean-am: clean-am distclean-compile distclean-generic \
+ distclean-tags
+
+dvi: dvi-am
+
+dvi-am:
+
+html: html-am
+
+html-am:
+
+info: info-am
+
+info-am:
+
+install-data-am:
+
+install-dvi: install-dvi-am
+
+install-dvi-am:
+
+install-exec-am:
+
+install-html: install-html-am
+
+install-html-am:
+
+install-info: install-info-am
+
+install-info-am:
+
+install-man:
+
+install-pdf: install-pdf-am
+
+install-pdf-am:
+
+install-ps: install-ps-am
+
+install-ps-am:
+
+installcheck-am:
+
+maintainer-clean: maintainer-clean-am
+ -rm -f ./$(DEPDIR)/dict-fs.Plo
+ -rm -f ./$(DEPDIR)/dict-register.Plo
+ -rm -f ./$(DEPDIR)/test-dict-fs.Po
+ -rm -f Makefile
+maintainer-clean-am: distclean-am maintainer-clean-generic
+
+mostlyclean: mostlyclean-am
+
+mostlyclean-am: mostlyclean-compile mostlyclean-generic \
+ mostlyclean-libtool
+
+pdf: pdf-am
+
+pdf-am:
+
+ps: ps-am
+
+ps-am:
+
+uninstall-am:
+
+.MAKE: check-am install-am install-strip
+
+.PHONY: CTAGS GTAGS TAGS all all-am am--depfiles check check-am \
+ check-local clean clean-generic clean-libtool \
+ clean-noinstLTLIBRARIES clean-noinstPROGRAMS cscopelist-am \
+ ctags ctags-am distclean distclean-compile distclean-generic \
+ distclean-libtool distclean-tags distdir dvi dvi-am html \
+ html-am info info-am install install-am install-data \
+ install-data-am install-dvi install-dvi-am install-exec \
+ install-exec-am install-html install-html-am install-info \
+ install-info-am install-man install-pdf install-pdf-am \
+ install-ps install-ps-am install-strip installcheck \
+ installcheck-am installdirs maintainer-clean \
+ maintainer-clean-generic mostlyclean mostlyclean-compile \
+ mostlyclean-generic mostlyclean-libtool pdf pdf-am ps ps-am \
+ tags tags-am uninstall uninstall-am
+
+.PRECIOUS: Makefile
+
+
+check-local:
+ for bin in $(test_programs) $(check_PROGRAMS); do \
+ if ! $(RUN_TEST) ./$$bin; then exit 1; fi; \
+ done
+
+# Tell versions [3.59,3.63) of GNU make to not export all variables.
+# Otherwise a system limit (for SysV at least) may be exceeded.
+.NOEXPORT:
diff --git a/src/lib-dict-extra/dict-fs.c b/src/lib-dict-extra/dict-fs.c
new file mode 100644
index 0000000..4c173e3
--- /dev/null
+++ b/src/lib-dict-extra/dict-fs.c
@@ -0,0 +1,330 @@
+/* Copyright (c) 2013-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "fs-api.h"
+#include "istream.h"
+#include "str.h"
+#include "dict-transaction-memory.h"
+#include "dict-private.h"
+
+struct fs_dict {
+ struct dict dict;
+ struct fs *fs;
+};
+
+struct fs_dict_iterate_context {
+ struct dict_iterate_context ctx;
+ char *path;
+ enum dict_iterate_flags flags;
+ pool_t value_pool;
+ struct fs_iter *fs_iter;
+ const char *values[2];
+ char *error;
+};
+
+static int
+fs_dict_init(struct dict *driver, const char *uri,
+ const struct dict_settings *set,
+ struct dict **dict_r, const char **error_r)
+{
+ struct fs_settings fs_set;
+ struct fs *fs;
+ struct fs_dict *dict;
+ const char *p, *fs_driver, *fs_args;
+
+ p = strchr(uri, ':');
+ if (p == NULL) {
+ fs_driver = uri;
+ fs_args = "";
+ } else {
+ fs_driver = t_strdup_until(uri, p);
+ fs_args = p+1;
+ }
+
+ i_zero(&fs_set);
+ fs_set.base_dir = set->base_dir;
+ if (fs_init(fs_driver, fs_args, &fs_set, &fs, error_r) < 0)
+ return -1;
+
+ dict = i_new(struct fs_dict, 1);
+ dict->dict = *driver;
+ dict->fs = fs;
+
+ *dict_r = &dict->dict;
+ return 0;
+}
+
+static void fs_dict_deinit(struct dict *_dict)
+{
+ struct fs_dict *dict = (struct fs_dict *)_dict;
+
+ fs_deinit(&dict->fs);
+ i_free(dict);
+}
+
+/* Remove unsafe paths */
+static const char *fs_dict_escape_key(const char *key)
+{
+ const char *ptr;
+ string_t *new_key = NULL;
+ /* we take the slow path always if we see potential
+ need for escaping */
+ while ((ptr = strstr(key, "/.")) != NULL) {
+ /* move to the first dot */
+ const char *ptr2 = ptr + 1;
+ /* find position of non-dot */
+ while (*ptr2 == '.') ptr2++;
+ if (new_key == NULL)
+ new_key = t_str_new(strlen(key));
+ str_append_data(new_key, key, ptr - key);
+ /* if ptr2 is / or end of string, escape */
+ if (*ptr2 == '/' || *ptr2 == '\0')
+ str_append(new_key, "/...");
+ else
+ str_append(new_key, "/.");
+ key = ptr + 2;
+ }
+ if (new_key == NULL)
+ return key;
+ str_append(new_key, key);
+ return str_c(new_key);
+}
+
+static const char *fs_dict_get_full_key(const char *username, const char *key)
+{
+ key = fs_dict_escape_key(key);
+ if (str_begins(key, DICT_PATH_SHARED))
+ return key + strlen(DICT_PATH_SHARED);
+ else if (str_begins(key, DICT_PATH_PRIVATE)) {
+ return t_strdup_printf("%s/%s", username,
+ key + strlen(DICT_PATH_PRIVATE));
+ } else {
+ i_unreached();
+ }
+}
+
+static int fs_dict_lookup(struct dict *_dict, const struct dict_op_settings *set,
+ pool_t pool, const char *key,
+ const char **value_r, const char **error_r)
+{
+ struct fs_dict *dict = (struct fs_dict *)_dict;
+ struct fs_file *file;
+ struct istream *input;
+ const unsigned char *data;
+ size_t size;
+ const char *path;
+ string_t *str;
+ int ret;
+
+ path = fs_dict_get_full_key(set->username, key);
+ file = fs_file_init(dict->fs, path, FS_OPEN_MODE_READONLY);
+ input = fs_read_stream(file, IO_BLOCK_SIZE);
+ (void)i_stream_read(input);
+
+ str = str_new(pool, i_stream_get_data_size(input)+1);
+ while ((ret = i_stream_read_more(input, &data, &size)) > 0) {
+ str_append_data(str, data, size);
+ i_stream_skip(input, size);
+ }
+ i_assert(ret == -1);
+
+ if (input->stream_errno == 0) {
+ *value_r = str_c(str);
+ ret = 1;
+ } else {
+ *value_r = NULL;
+ if (input->stream_errno == ENOENT)
+ ret = 0;
+ else {
+ *error_r = t_strdup_printf("read(%s) failed: %s",
+ path, i_stream_get_error(input));
+ }
+ }
+
+ i_stream_unref(&input);
+ fs_file_deinit(&file);
+ return ret;
+}
+
+static struct dict_iterate_context *
+fs_dict_iterate_init(struct dict *_dict, const struct dict_op_settings *set,
+ const char *path, enum dict_iterate_flags flags)
+{
+ struct fs_dict *dict = (struct fs_dict *)_dict;
+ struct fs_dict_iterate_context *iter;
+
+ /* these flags are not supported for now */
+ i_assert((flags & DICT_ITERATE_FLAG_RECURSE) == 0);
+ i_assert((flags & DICT_ITERATE_FLAG_EXACT_KEY) == 0);
+ i_assert((flags & (DICT_ITERATE_FLAG_SORT_BY_KEY |
+ DICT_ITERATE_FLAG_SORT_BY_VALUE)) == 0);
+
+ iter = i_new(struct fs_dict_iterate_context, 1);
+ iter->ctx.dict = _dict;
+ iter->path = i_strdup(path);
+ iter->flags = flags;
+ iter->value_pool = pool_alloconly_create("iterate value pool", 128);
+ iter->fs_iter = fs_iter_init(dict->fs,
+ fs_dict_get_full_key(set->username, path), 0);
+ return &iter->ctx;
+}
+
+static bool fs_dict_iterate(struct dict_iterate_context *ctx,
+ const char **key_r, const char *const **values_r)
+{
+ struct fs_dict_iterate_context *iter =
+ (struct fs_dict_iterate_context *)ctx;
+ struct fs_dict *dict = (struct fs_dict *)ctx->dict;
+ const char *path, *error;
+ int ret;
+
+ if (iter->error != NULL)
+ return FALSE;
+
+ *key_r = fs_iter_next(iter->fs_iter);
+ if (*key_r == NULL) {
+ if (fs_iter_deinit(&iter->fs_iter, &error) < 0) {
+ iter->error = i_strdup(error);
+ return FALSE;
+ }
+ if (iter->path == NULL)
+ return FALSE;
+ path = fs_dict_get_full_key(ctx->set.username, iter->path);
+ iter->fs_iter = fs_iter_init(dict->fs, path, 0);
+ return fs_dict_iterate(ctx, key_r, values_r);
+ }
+ path = t_strconcat(iter->path, *key_r, NULL);
+ if ((iter->flags & DICT_ITERATE_FLAG_NO_VALUE) != 0) {
+ iter->values[0] = NULL;
+ *key_r = path;
+ return TRUE;
+ }
+ p_clear(iter->value_pool);
+ struct dict_op_settings set = {
+ .username = ctx->set.username,
+ };
+ ret = fs_dict_lookup(ctx->dict, &set, iter->value_pool, path,
+ &iter->values[0], &error);
+ if (ret < 0) {
+ /* I/O error */
+ iter->error = i_strdup(error);
+ return FALSE;
+ } else if (ret == 0) {
+ /* file was just deleted, just skip to next one */
+ return fs_dict_iterate(ctx, key_r, values_r);
+ }
+ *key_r = path;
+ *values_r = iter->values;
+ return TRUE;
+}
+
+static int fs_dict_iterate_deinit(struct dict_iterate_context *ctx,
+ const char **error_r)
+{
+ struct fs_dict_iterate_context *iter =
+ (struct fs_dict_iterate_context *)ctx;
+ const char *error;
+ int ret;
+
+ if (fs_iter_deinit(&iter->fs_iter, &error) < 0 && iter->error == NULL)
+ iter->error = i_strdup(error);
+
+ ret = iter->error != NULL ? -1 : 0;
+ *error_r = t_strdup(iter->error);
+
+ pool_unref(&iter->value_pool);
+ i_free(iter->path);
+ i_free(iter->error);
+ i_free(iter);
+ return ret;
+}
+
+static struct dict_transaction_context *
+fs_dict_transaction_init(struct dict *_dict)
+{
+ struct dict_transaction_memory_context *ctx;
+ pool_t pool;
+
+ pool = pool_alloconly_create("file dict transaction", 2048);
+ ctx = p_new(pool, struct dict_transaction_memory_context, 1);
+ dict_transaction_memory_init(ctx, _dict, pool);
+ return &ctx->ctx;
+}
+
+static int fs_dict_write_changes(struct dict_transaction_memory_context *ctx,
+ const char **error_r)
+{
+ struct fs_dict *dict = (struct fs_dict *)ctx->ctx.dict;
+ struct fs_file *file;
+ const struct dict_transaction_memory_change *change;
+ const char *key;
+ int ret = 0;
+
+ array_foreach(&ctx->changes, change) {
+ key = fs_dict_get_full_key(ctx->ctx.set.username, change->key);
+ switch (change->type) {
+ case DICT_CHANGE_TYPE_SET:
+ file = fs_file_init(dict->fs, key,
+ FS_OPEN_MODE_REPLACE);
+ if (fs_write(file, change->value.str, strlen(change->value.str)) < 0) {
+ *error_r = t_strdup_printf(
+ "fs_write(%s) failed: %s", key,
+ fs_file_last_error(file));
+ ret = -1;
+ }
+ fs_file_deinit(&file);
+ break;
+ case DICT_CHANGE_TYPE_UNSET:
+ file = fs_file_init(dict->fs, key, FS_OPEN_MODE_READONLY);
+ if (fs_delete(file) < 0) {
+ *error_r = t_strdup_printf(
+ "fs_delete(%s) failed: %s", key,
+ fs_file_last_error(file));
+ ret = -1;
+ }
+ fs_file_deinit(&file);
+ break;
+ case DICT_CHANGE_TYPE_INC:
+ i_unreached();
+ }
+ if (ret < 0)
+ return -1;
+ }
+ return 0;
+}
+
+static void
+fs_dict_transaction_commit(struct dict_transaction_context *_ctx,
+ bool async ATTR_UNUSED,
+ dict_transaction_commit_callback_t *callback,
+ void *context)
+{
+ struct dict_transaction_memory_context *ctx =
+ (struct dict_transaction_memory_context *)_ctx;
+ struct dict_commit_result result = { .ret = 1 };
+
+
+ if (fs_dict_write_changes(ctx, &result.error) < 0)
+ result.ret = -1;
+ pool_unref(&ctx->pool);
+
+ callback(&result, context);
+}
+
+struct dict dict_driver_fs = {
+ .name = "fs",
+ {
+ .init = fs_dict_init,
+ .deinit = fs_dict_deinit,
+ .lookup = fs_dict_lookup,
+ .iterate_init = fs_dict_iterate_init,
+ .iterate = fs_dict_iterate,
+ .iterate_deinit = fs_dict_iterate_deinit,
+ .transaction_init = fs_dict_transaction_init,
+ .transaction_commit = fs_dict_transaction_commit,
+ .transaction_rollback = dict_transaction_memory_rollback,
+ .set = dict_transaction_memory_set,
+ .unset = dict_transaction_memory_unset,
+ }
+};
diff --git a/src/lib-dict-extra/dict-register.c b/src/lib-dict-extra/dict-register.c
new file mode 100644
index 0000000..96d2bef
--- /dev/null
+++ b/src/lib-dict-extra/dict-register.c
@@ -0,0 +1,30 @@
+/* Copyright (c) 2013-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "dict-private.h"
+
+static int refcount = 0;
+
+void dict_drivers_register_builtin(void)
+{
+ if (refcount++ > 0)
+ return;
+ dict_driver_register(&dict_driver_client);
+ dict_driver_register(&dict_driver_file);
+ dict_driver_register(&dict_driver_fs);
+ dict_driver_register(&dict_driver_memcached);
+ dict_driver_register(&dict_driver_memcached_ascii);
+ dict_driver_register(&dict_driver_redis);
+}
+
+void dict_drivers_unregister_builtin(void)
+{
+ if (--refcount > 0)
+ return;
+ dict_driver_unregister(&dict_driver_client);
+ dict_driver_unregister(&dict_driver_file);
+ dict_driver_unregister(&dict_driver_fs);
+ dict_driver_unregister(&dict_driver_memcached);
+ dict_driver_unregister(&dict_driver_memcached_ascii);
+ dict_driver_unregister(&dict_driver_redis);
+}
diff --git a/src/lib-dict-extra/test-dict-fs.c b/src/lib-dict-extra/test-dict-fs.c
new file mode 100644
index 0000000..a7b9905
--- /dev/null
+++ b/src/lib-dict-extra/test-dict-fs.c
@@ -0,0 +1,106 @@
+#include <errno.h>
+#include <sys/stat.h>
+#include "lib.h"
+#include "unlink-directory.h"
+#include "test-common.h"
+#include "dict-private.h"
+
+static void test_dict_set_get(struct dict *dict, const char *key,
+ const char *value)
+{
+ const char *got_value, *error;
+ struct dict_op_settings set = {
+ .username = "testuser",
+ };
+ struct dict_transaction_context *t = dict_transaction_begin(dict, &set);
+ dict_set(t, key, value);
+ if (dict_transaction_commit(&t, &error) < 0)
+ i_fatal("dict_transaction_commit(%s) failed: %s", key, error);
+ if (dict_lookup(dict, &set, pool_datastack_create(), key, &got_value,
+ &error) < 0)
+ i_fatal("dict_lookup(%s) failed: %s", key, error);
+ test_assert_strcmp(got_value, value);
+}
+
+static bool test_file_exists(const char *path)
+{
+ struct stat st;
+ if (stat(path, &st) < 0) {
+ if (ENOTFOUND(errno)) return FALSE;
+ i_fatal("stat(%s) failed: %m", path);
+ }
+ return TRUE;
+}
+
+static void test_dict_fs_set_get(void)
+{
+ test_begin("dict-fs get/set");
+ const char *error;
+ struct dict *dict;
+ struct dict_settings set = {
+ .base_dir = ".",
+ };
+ if (dict_init("fs:posix:prefix=.test-dict/", &set, &dict, &error) < 0)
+ i_fatal("dict_init() failed: %s", error);
+
+ /* shared paths */
+ struct {
+ const char *key;
+ const char *path;
+ } test_cases[] = {
+ { "shared/./key", ".test-dict/.../key" },
+ { "shared/../key", ".test-dict/..../key" },
+ { "shared/.../key", ".test-dict/...../key" },
+ { "shared/..../key", ".test-dict/....../key" },
+ { "shared/...../key", ".test-dict/......./key" },
+ { "shared/key/.", ".test-dict/key/..." },
+ { "shared/key/..", ".test-dict/key/...." },
+ { "shared/key/...", ".test-dict/key/....." },
+ { "shared/key/....", ".test-dict/key/......" },
+ { "shared/key/.....", ".test-dict/key/......." },
+ { "shared/key/.key", ".test-dict/key/.key" },
+ { "shared/key/..key", ".test-dict/key/..key" },
+ { "shared/key/...key", ".test-dict/key/...key" },
+ { "shared/.key/key", ".test-dict/.key/key" },
+ { "shared/..key/key", ".test-dict/..key/key" },
+ { "shared/...key/key", ".test-dict/...key/key" },
+ };
+ for (size_t i = 0; i < N_ELEMENTS(test_cases); i++) {
+ test_dict_set_get(dict, test_cases[i].key, "1");
+ test_assert(test_file_exists(test_cases[i].path));
+ }
+
+ /* per user paths */
+ test_dict_set_get(dict, "priv/value", "priv1");
+ test_assert(test_file_exists(".test-dict/testuser/value"));
+ test_dict_set_get(dict, "priv/path/with/value", "priv2");
+ test_assert(test_file_exists(".test-dict/testuser/path/with/value"));
+
+ /* check that dots work correctly */
+ test_dict_set_get(dict, "shared/../test-dict-fs.c", "3");
+ test_assert(test_file_exists(".test-dict/..../test-dict-fs.c"));
+ test_dict_set_get(dict, "shared/./test", "4");
+ test_assert(test_file_exists(".test-dict/.../test"));
+ test_dict_set_get(dict, "shared/.test", "5");
+ test_assert(test_file_exists(".test-dict/.test"));
+ test_dict_set_get(dict, "shared/..test", "6");
+ test_assert(test_file_exists(".test-dict/..test"));
+ dict_deinit(&dict);
+
+ if (unlink_directory(".test-dict", UNLINK_DIRECTORY_FLAG_RMDIR, &error) < 0)
+ i_fatal("unlink_directory(.test_dict) failed: %s", error);
+ test_end();
+}
+
+int main(void)
+{
+ static void (*const test_functions[])(void) = {
+ test_dict_fs_set_get,
+ NULL
+ };
+ int ret;
+ dict_driver_register(&dict_driver_fs);
+ ret = test_run(test_functions);
+ dict_driver_unregister(&dict_driver_fs);
+ return ret;
+}
diff --git a/src/lib-dict/Makefile.am b/src/lib-dict/Makefile.am
new file mode 100644
index 0000000..97e5eb6
--- /dev/null
+++ b/src/lib-dict/Makefile.am
@@ -0,0 +1,73 @@
+noinst_LTLIBRARIES = \
+ libdict.la
+
+AM_CPPFLAGS = \
+ -I$(top_srcdir)/src/lib \
+ -I$(top_srcdir)/src/lib-test \
+ -I$(top_srcdir)/src/lib-settings
+
+base_sources = \
+ dict.c \
+ dict-client.c \
+ dict-file.c \
+ dict-memcached.c \
+ dict-memcached-ascii.c \
+ dict-redis.c \
+ dict-fail.c \
+ dict-transaction-memory.c
+
+libdict_la_SOURCES = \
+ $(base_sources)
+
+headers = \
+ dict.h \
+ dict-client.h \
+ dict-private.h \
+ dict-transaction-memory.h
+
+# Internally, the dict methods yield via lua_yieldk() as implemented in Lua
+# 5.3 and newer.
+if DLUA_WITH_YIELDS
+noinst_LTLIBRARIES += libdict_lua.la
+
+libdict_lua_la_SOURCES = \
+ dict-lua.c \
+ dict-iter-lua.c \
+ dict-txn-lua.c
+libdict_lua_la_CPPFLAGS = \
+ $(AM_CPPFLAGS) \
+ $(LUA_CFLAGS) \
+ -I$(top_srcdir)/src/lib-lua
+libdict_lua_la_LIBADD =
+libdict_lua_la_DEPENDENCIES = \
+ libdict.la
+
+headers += \
+ dict-lua.h \
+ dict-lua-private.h
+endif
+
+pkginc_libdir=$(pkgincludedir)
+pkginc_lib_HEADERS = $(headers)
+
+test_programs = \
+ test-dict
+
+noinst_PROGRAMS = $(test_programs) test-dict-client
+
+test_libs = \
+ ../lib-test/libtest.la \
+ ../lib/liblib.la
+
+test_dict_SOURCES = test-dict.c
+test_dict_LDADD = libdict.la $(test_libs)
+test_dict_DEPENDENCIES = $(noinst_LTLIBRARIES) $(test_libs)
+
+test_dict_client_SOURCES = test-dict-client.c
+test_dict_client_LDADD = $(noinst_LTLIBRARIES) ../lib/liblib.la
+test_dict_client_DEPENDENCIES = $(noinst_LTLIBRARIES) $(test_libs)
+
+check-local:
+ for bin in $(test_programs) $(check_PROGRAMS); do \
+ if ! $(RUN_TEST) ./$$bin; then exit 1; fi; \
+ done
diff --git a/src/lib-dict/Makefile.in b/src/lib-dict/Makefile.in
new file mode 100644
index 0000000..aa8f17e
--- /dev/null
+++ b/src/lib-dict/Makefile.in
@@ -0,0 +1,970 @@
+# Makefile.in generated by automake 1.16.1 from Makefile.am.
+# @configure_input@
+
+# Copyright (C) 1994-2018 Free Software Foundation, Inc.
+
+# This Makefile.in is free software; the Free Software Foundation
+# gives unlimited permission to copy and/or distribute it,
+# with or without modifications, as long as this notice is preserved.
+
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY, to the extent permitted by law; without
+# even the implied warranty of MERCHANTABILITY or FITNESS FOR A
+# PARTICULAR PURPOSE.
+
+@SET_MAKE@
+
+
+
+VPATH = @srcdir@
+am__is_gnu_make = { \
+ if test -z '$(MAKELEVEL)'; then \
+ false; \
+ elif test -n '$(MAKE_HOST)'; then \
+ true; \
+ elif test -n '$(MAKE_VERSION)' && test -n '$(CURDIR)'; then \
+ true; \
+ else \
+ false; \
+ fi; \
+}
+am__make_running_with_option = \
+ case $${target_option-} in \
+ ?) ;; \
+ *) echo "am__make_running_with_option: internal error: invalid" \
+ "target option '$${target_option-}' specified" >&2; \
+ exit 1;; \
+ esac; \
+ has_opt=no; \
+ sane_makeflags=$$MAKEFLAGS; \
+ if $(am__is_gnu_make); then \
+ sane_makeflags=$$MFLAGS; \
+ else \
+ case $$MAKEFLAGS in \
+ *\\[\ \ ]*) \
+ bs=\\; \
+ sane_makeflags=`printf '%s\n' "$$MAKEFLAGS" \
+ | sed "s/$$bs$$bs[$$bs $$bs ]*//g"`;; \
+ esac; \
+ fi; \
+ skip_next=no; \
+ strip_trailopt () \
+ { \
+ flg=`printf '%s\n' "$$flg" | sed "s/$$1.*$$//"`; \
+ }; \
+ for flg in $$sane_makeflags; do \
+ test $$skip_next = yes && { skip_next=no; continue; }; \
+ case $$flg in \
+ *=*|--*) continue;; \
+ -*I) strip_trailopt 'I'; skip_next=yes;; \
+ -*I?*) strip_trailopt 'I';; \
+ -*O) strip_trailopt 'O'; skip_next=yes;; \
+ -*O?*) strip_trailopt 'O';; \
+ -*l) strip_trailopt 'l'; skip_next=yes;; \
+ -*l?*) strip_trailopt 'l';; \
+ -[dEDm]) skip_next=yes;; \
+ -[JT]) skip_next=yes;; \
+ esac; \
+ case $$flg in \
+ *$$target_option*) has_opt=yes; break;; \
+ esac; \
+ done; \
+ test $$has_opt = yes
+am__make_dryrun = (target_option=n; $(am__make_running_with_option))
+am__make_keepgoing = (target_option=k; $(am__make_running_with_option))
+pkgdatadir = $(datadir)/@PACKAGE@
+pkgincludedir = $(includedir)/@PACKAGE@
+pkglibdir = $(libdir)/@PACKAGE@
+pkglibexecdir = $(libexecdir)/@PACKAGE@
+am__cd = CDPATH="$${ZSH_VERSION+.}$(PATH_SEPARATOR)" && cd
+install_sh_DATA = $(install_sh) -c -m 644
+install_sh_PROGRAM = $(install_sh) -c
+install_sh_SCRIPT = $(install_sh) -c
+INSTALL_HEADER = $(INSTALL_DATA)
+transform = $(program_transform_name)
+NORMAL_INSTALL = :
+PRE_INSTALL = :
+POST_INSTALL = :
+NORMAL_UNINSTALL = :
+PRE_UNINSTALL = :
+POST_UNINSTALL = :
+build_triplet = @build@
+host_triplet = @host@
+
+# Internally, the dict methods yield via lua_yieldk() as implemented in Lua
+# 5.3 and newer.
+@DLUA_WITH_YIELDS_TRUE@am__append_1 = libdict_lua.la
+@DLUA_WITH_YIELDS_FALSE@libdict_lua_la_DEPENDENCIES =
+@DLUA_WITH_YIELDS_TRUE@am__append_2 = \
+@DLUA_WITH_YIELDS_TRUE@ dict-lua.h \
+@DLUA_WITH_YIELDS_TRUE@ dict-lua-private.h
+
+noinst_PROGRAMS = $(am__EXEEXT_1) test-dict-client$(EXEEXT)
+subdir = src/lib-dict
+ACLOCAL_M4 = $(top_srcdir)/aclocal.m4
+am__aclocal_m4_deps = $(top_srcdir)/m4/ac_checktype2.m4 \
+ $(top_srcdir)/m4/ac_typeof.m4 $(top_srcdir)/m4/arc4random.m4 \
+ $(top_srcdir)/m4/blockdev.m4 $(top_srcdir)/m4/c99_vsnprintf.m4 \
+ $(top_srcdir)/m4/clock_gettime.m4 $(top_srcdir)/m4/crypt.m4 \
+ $(top_srcdir)/m4/crypt_xpg6.m4 $(top_srcdir)/m4/dbqlk.m4 \
+ $(top_srcdir)/m4/dirent_dtype.m4 $(top_srcdir)/m4/dovecot.m4 \
+ $(top_srcdir)/m4/fd_passing.m4 $(top_srcdir)/m4/fdatasync.m4 \
+ $(top_srcdir)/m4/flexible_array_member.m4 \
+ $(top_srcdir)/m4/glibc.m4 $(top_srcdir)/m4/gmtime_max.m4 \
+ $(top_srcdir)/m4/gmtime_tm_gmtoff.m4 \
+ $(top_srcdir)/m4/ioloop.m4 $(top_srcdir)/m4/iovec.m4 \
+ $(top_srcdir)/m4/ipv6.m4 $(top_srcdir)/m4/libcap.m4 \
+ $(top_srcdir)/m4/libtool.m4 $(top_srcdir)/m4/libwrap.m4 \
+ $(top_srcdir)/m4/linux_mremap.m4 $(top_srcdir)/m4/ltoptions.m4 \
+ $(top_srcdir)/m4/ltsugar.m4 $(top_srcdir)/m4/ltversion.m4 \
+ $(top_srcdir)/m4/lt~obsolete.m4 $(top_srcdir)/m4/mmap_write.m4 \
+ $(top_srcdir)/m4/mntctl.m4 $(top_srcdir)/m4/modules.m4 \
+ $(top_srcdir)/m4/notify.m4 $(top_srcdir)/m4/nsl.m4 \
+ $(top_srcdir)/m4/off_t_max.m4 $(top_srcdir)/m4/pkg.m4 \
+ $(top_srcdir)/m4/pr_set_dumpable.m4 \
+ $(top_srcdir)/m4/q_quotactl.m4 $(top_srcdir)/m4/quota.m4 \
+ $(top_srcdir)/m4/random.m4 $(top_srcdir)/m4/rlimit.m4 \
+ $(top_srcdir)/m4/sendfile.m4 $(top_srcdir)/m4/size_t_signed.m4 \
+ $(top_srcdir)/m4/sockpeercred.m4 $(top_srcdir)/m4/sql.m4 \
+ $(top_srcdir)/m4/ssl.m4 $(top_srcdir)/m4/st_tim.m4 \
+ $(top_srcdir)/m4/static_array.m4 $(top_srcdir)/m4/test_with.m4 \
+ $(top_srcdir)/m4/time_t.m4 $(top_srcdir)/m4/typeof.m4 \
+ $(top_srcdir)/m4/typeof_dev_t.m4 \
+ $(top_srcdir)/m4/uoff_t_max.m4 $(top_srcdir)/m4/vararg.m4 \
+ $(top_srcdir)/m4/want_apparmor.m4 \
+ $(top_srcdir)/m4/want_bsdauth.m4 \
+ $(top_srcdir)/m4/want_bzlib.m4 \
+ $(top_srcdir)/m4/want_cassandra.m4 \
+ $(top_srcdir)/m4/want_cdb.m4 \
+ $(top_srcdir)/m4/want_checkpassword.m4 \
+ $(top_srcdir)/m4/want_clucene.m4 $(top_srcdir)/m4/want_db.m4 \
+ $(top_srcdir)/m4/want_gssapi.m4 $(top_srcdir)/m4/want_icu.m4 \
+ $(top_srcdir)/m4/want_ldap.m4 $(top_srcdir)/m4/want_lua.m4 \
+ $(top_srcdir)/m4/want_lz4.m4 $(top_srcdir)/m4/want_lzma.m4 \
+ $(top_srcdir)/m4/want_mysql.m4 $(top_srcdir)/m4/want_pam.m4 \
+ $(top_srcdir)/m4/want_passwd.m4 $(top_srcdir)/m4/want_pgsql.m4 \
+ $(top_srcdir)/m4/want_prefetch.m4 \
+ $(top_srcdir)/m4/want_shadow.m4 \
+ $(top_srcdir)/m4/want_sodium.m4 $(top_srcdir)/m4/want_solr.m4 \
+ $(top_srcdir)/m4/want_sqlite.m4 \
+ $(top_srcdir)/m4/want_stemmer.m4 \
+ $(top_srcdir)/m4/want_systemd.m4 \
+ $(top_srcdir)/m4/want_textcat.m4 \
+ $(top_srcdir)/m4/want_unwind.m4 $(top_srcdir)/m4/want_zlib.m4 \
+ $(top_srcdir)/m4/want_zstd.m4 $(top_srcdir)/configure.ac
+am__configure_deps = $(am__aclocal_m4_deps) $(CONFIGURE_DEPENDENCIES) \
+ $(ACLOCAL_M4)
+DIST_COMMON = $(srcdir)/Makefile.am $(am__pkginc_lib_HEADERS_DIST) \
+ $(am__DIST_COMMON)
+mkinstalldirs = $(install_sh) -d
+CONFIG_HEADER = $(top_builddir)/config.h
+CONFIG_CLEAN_FILES =
+CONFIG_CLEAN_VPATH_FILES =
+am__EXEEXT_1 = test-dict$(EXEEXT)
+PROGRAMS = $(noinst_PROGRAMS)
+LTLIBRARIES = $(noinst_LTLIBRARIES)
+libdict_la_LIBADD =
+am__objects_1 = dict.lo dict-client.lo dict-file.lo dict-memcached.lo \
+ dict-memcached-ascii.lo dict-redis.lo dict-fail.lo \
+ dict-transaction-memory.lo
+am_libdict_la_OBJECTS = $(am__objects_1)
+libdict_la_OBJECTS = $(am_libdict_la_OBJECTS)
+AM_V_lt = $(am__v_lt_@AM_V@)
+am__v_lt_ = $(am__v_lt_@AM_DEFAULT_V@)
+am__v_lt_0 = --silent
+am__v_lt_1 =
+am__libdict_lua_la_SOURCES_DIST = dict-lua.c dict-iter-lua.c \
+ dict-txn-lua.c
+@DLUA_WITH_YIELDS_TRUE@am_libdict_lua_la_OBJECTS = \
+@DLUA_WITH_YIELDS_TRUE@ libdict_lua_la-dict-lua.lo \
+@DLUA_WITH_YIELDS_TRUE@ libdict_lua_la-dict-iter-lua.lo \
+@DLUA_WITH_YIELDS_TRUE@ libdict_lua_la-dict-txn-lua.lo
+libdict_lua_la_OBJECTS = $(am_libdict_lua_la_OBJECTS)
+@DLUA_WITH_YIELDS_TRUE@am_libdict_lua_la_rpath =
+am_test_dict_OBJECTS = test-dict.$(OBJEXT)
+test_dict_OBJECTS = $(am_test_dict_OBJECTS)
+am_test_dict_client_OBJECTS = test-dict-client.$(OBJEXT)
+test_dict_client_OBJECTS = $(am_test_dict_client_OBJECTS)
+AM_V_P = $(am__v_P_@AM_V@)
+am__v_P_ = $(am__v_P_@AM_DEFAULT_V@)
+am__v_P_0 = false
+am__v_P_1 = :
+AM_V_GEN = $(am__v_GEN_@AM_V@)
+am__v_GEN_ = $(am__v_GEN_@AM_DEFAULT_V@)
+am__v_GEN_0 = @echo " GEN " $@;
+am__v_GEN_1 =
+AM_V_at = $(am__v_at_@AM_V@)
+am__v_at_ = $(am__v_at_@AM_DEFAULT_V@)
+am__v_at_0 = @
+am__v_at_1 =
+DEFAULT_INCLUDES = -I.@am__isrc@ -I$(top_builddir)
+depcomp = $(SHELL) $(top_srcdir)/depcomp
+am__maybe_remake_depfiles = depfiles
+am__depfiles_remade = ./$(DEPDIR)/dict-client.Plo \
+ ./$(DEPDIR)/dict-fail.Plo ./$(DEPDIR)/dict-file.Plo \
+ ./$(DEPDIR)/dict-memcached-ascii.Plo \
+ ./$(DEPDIR)/dict-memcached.Plo ./$(DEPDIR)/dict-redis.Plo \
+ ./$(DEPDIR)/dict-transaction-memory.Plo ./$(DEPDIR)/dict.Plo \
+ ./$(DEPDIR)/libdict_lua_la-dict-iter-lua.Plo \
+ ./$(DEPDIR)/libdict_lua_la-dict-lua.Plo \
+ ./$(DEPDIR)/libdict_lua_la-dict-txn-lua.Plo \
+ ./$(DEPDIR)/test-dict-client.Po ./$(DEPDIR)/test-dict.Po
+am__mv = mv -f
+COMPILE = $(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) \
+ $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS)
+LTCOMPILE = $(LIBTOOL) $(AM_V_lt) --tag=CC $(AM_LIBTOOLFLAGS) \
+ $(LIBTOOLFLAGS) --mode=compile $(CC) $(DEFS) \
+ $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) $(CPPFLAGS) \
+ $(AM_CFLAGS) $(CFLAGS)
+AM_V_CC = $(am__v_CC_@AM_V@)
+am__v_CC_ = $(am__v_CC_@AM_DEFAULT_V@)
+am__v_CC_0 = @echo " CC " $@;
+am__v_CC_1 =
+CCLD = $(CC)
+LINK = $(LIBTOOL) $(AM_V_lt) --tag=CC $(AM_LIBTOOLFLAGS) \
+ $(LIBTOOLFLAGS) --mode=link $(CCLD) $(AM_CFLAGS) $(CFLAGS) \
+ $(AM_LDFLAGS) $(LDFLAGS) -o $@
+AM_V_CCLD = $(am__v_CCLD_@AM_V@)
+am__v_CCLD_ = $(am__v_CCLD_@AM_DEFAULT_V@)
+am__v_CCLD_0 = @echo " CCLD " $@;
+am__v_CCLD_1 =
+SOURCES = $(libdict_la_SOURCES) $(libdict_lua_la_SOURCES) \
+ $(test_dict_SOURCES) $(test_dict_client_SOURCES)
+DIST_SOURCES = $(libdict_la_SOURCES) \
+ $(am__libdict_lua_la_SOURCES_DIST) $(test_dict_SOURCES) \
+ $(test_dict_client_SOURCES)
+am__can_run_installinfo = \
+ case $$AM_UPDATE_INFO_DIR in \
+ n|no|NO) false;; \
+ *) (install-info --version) >/dev/null 2>&1;; \
+ esac
+am__pkginc_lib_HEADERS_DIST = dict.h dict-client.h dict-private.h \
+ dict-transaction-memory.h dict-lua.h dict-lua-private.h
+am__vpath_adj_setup = srcdirstrip=`echo "$(srcdir)" | sed 's|.|.|g'`;
+am__vpath_adj = case $$p in \
+ $(srcdir)/*) f=`echo "$$p" | sed "s|^$$srcdirstrip/||"`;; \
+ *) f=$$p;; \
+ esac;
+am__strip_dir = f=`echo $$p | sed -e 's|^.*/||'`;
+am__install_max = 40
+am__nobase_strip_setup = \
+ srcdirstrip=`echo "$(srcdir)" | sed 's/[].[^$$\\*|]/\\\\&/g'`
+am__nobase_strip = \
+ for p in $$list; do echo "$$p"; done | sed -e "s|$$srcdirstrip/||"
+am__nobase_list = $(am__nobase_strip_setup); \
+ for p in $$list; do echo "$$p $$p"; done | \
+ sed "s| $$srcdirstrip/| |;"' / .*\//!s/ .*/ ./; s,\( .*\)/[^/]*$$,\1,' | \
+ $(AWK) 'BEGIN { files["."] = "" } { files[$$2] = files[$$2] " " $$1; \
+ if (++n[$$2] == $(am__install_max)) \
+ { print $$2, files[$$2]; n[$$2] = 0; files[$$2] = "" } } \
+ END { for (dir in files) print dir, files[dir] }'
+am__base_list = \
+ sed '$$!N;$$!N;$$!N;$$!N;$$!N;$$!N;$$!N;s/\n/ /g' | \
+ sed '$$!N;$$!N;$$!N;$$!N;s/\n/ /g'
+am__uninstall_files_from_dir = { \
+ test -z "$$files" \
+ || { test ! -d "$$dir" && test ! -f "$$dir" && test ! -r "$$dir"; } \
+ || { echo " ( cd '$$dir' && rm -f" $$files ")"; \
+ $(am__cd) "$$dir" && rm -f $$files; }; \
+ }
+am__installdirs = "$(DESTDIR)$(pkginc_libdir)"
+HEADERS = $(pkginc_lib_HEADERS)
+am__tagged_files = $(HEADERS) $(SOURCES) $(TAGS_FILES) $(LISP)
+# Read a list of newline-separated strings from the standard input,
+# and print each of them once, without duplicates. Input order is
+# *not* preserved.
+am__uniquify_input = $(AWK) '\
+ BEGIN { nonempty = 0; } \
+ { items[$$0] = 1; nonempty = 1; } \
+ END { if (nonempty) { for (i in items) print i; }; } \
+'
+# Make sure the list of sources is unique. This is necessary because,
+# e.g., the same source file might be shared among _SOURCES variables
+# for different programs/libraries.
+am__define_uniq_tagged_files = \
+ list='$(am__tagged_files)'; \
+ unique=`for i in $$list; do \
+ if test -f "$$i"; then echo $$i; else echo $(srcdir)/$$i; fi; \
+ done | $(am__uniquify_input)`
+ETAGS = etags
+CTAGS = ctags
+am__DIST_COMMON = $(srcdir)/Makefile.in $(top_srcdir)/depcomp
+DISTFILES = $(DIST_COMMON) $(DIST_SOURCES) $(TEXINFOS) $(EXTRA_DIST)
+ACLOCAL = @ACLOCAL@
+ACLOCAL_AMFLAGS = @ACLOCAL_AMFLAGS@
+AMTAR = @AMTAR@
+AM_DEFAULT_VERBOSITY = @AM_DEFAULT_VERBOSITY@
+APPARMOR_LIBS = @APPARMOR_LIBS@
+AR = @AR@
+AUTH_CFLAGS = @AUTH_CFLAGS@
+AUTH_LIBS = @AUTH_LIBS@
+AUTOCONF = @AUTOCONF@
+AUTOHEADER = @AUTOHEADER@
+AUTOMAKE = @AUTOMAKE@
+AWK = @AWK@
+BINARY_CFLAGS = @BINARY_CFLAGS@
+BINARY_LDFLAGS = @BINARY_LDFLAGS@
+BISON = @BISON@
+CASSANDRA_CFLAGS = @CASSANDRA_CFLAGS@
+CASSANDRA_LIBS = @CASSANDRA_LIBS@
+CC = @CC@
+CCDEPMODE = @CCDEPMODE@
+CDB_LIBS = @CDB_LIBS@
+CFLAGS = @CFLAGS@
+CLUCENE_CFLAGS = @CLUCENE_CFLAGS@
+CLUCENE_LIBS = @CLUCENE_LIBS@
+COMPRESS_LIBS = @COMPRESS_LIBS@
+CPP = @CPP@
+CPPFLAGS = @CPPFLAGS@
+CRYPT_LIBS = @CRYPT_LIBS@
+CXX = @CXX@
+CXXCPP = @CXXCPP@
+CXXDEPMODE = @CXXDEPMODE@
+CXXFLAGS = @CXXFLAGS@
+CYGPATH_W = @CYGPATH_W@
+DEFS = @DEFS@
+DEPDIR = @DEPDIR@
+DICT_LIBS = @DICT_LIBS@
+DLLIB = @DLLIB@
+DLLTOOL = @DLLTOOL@
+DSYMUTIL = @DSYMUTIL@
+DUMPBIN = @DUMPBIN@
+ECHO_C = @ECHO_C@
+ECHO_N = @ECHO_N@
+ECHO_T = @ECHO_T@
+EGREP = @EGREP@
+EXEEXT = @EXEEXT@
+FGREP = @FGREP@
+FLEX = @FLEX@
+FUZZER_CPPFLAGS = @FUZZER_CPPFLAGS@
+FUZZER_LDFLAGS = @FUZZER_LDFLAGS@
+GREP = @GREP@
+INSTALL = @INSTALL@
+INSTALL_DATA = @INSTALL_DATA@
+INSTALL_PROGRAM = @INSTALL_PROGRAM@
+INSTALL_SCRIPT = @INSTALL_SCRIPT@
+INSTALL_STRIP_PROGRAM = @INSTALL_STRIP_PROGRAM@
+KRB5CONFIG = @KRB5CONFIG@
+KRB5_CFLAGS = @KRB5_CFLAGS@
+KRB5_LIBS = @KRB5_LIBS@
+LD = @LD@
+LDAP_LIBS = @LDAP_LIBS@
+LDFLAGS = @LDFLAGS@
+LD_NO_WHOLE_ARCHIVE = @LD_NO_WHOLE_ARCHIVE@
+LD_WHOLE_ARCHIVE = @LD_WHOLE_ARCHIVE@
+LIBCAP = @LIBCAP@
+LIBDOVECOT = @LIBDOVECOT@
+LIBDOVECOT_COMPRESS = @LIBDOVECOT_COMPRESS@
+LIBDOVECOT_DEPS = @LIBDOVECOT_DEPS@
+LIBDOVECOT_DSYNC = @LIBDOVECOT_DSYNC@
+LIBDOVECOT_LA_LIBS = @LIBDOVECOT_LA_LIBS@
+LIBDOVECOT_LDA = @LIBDOVECOT_LDA@
+LIBDOVECOT_LDAP = @LIBDOVECOT_LDAP@
+LIBDOVECOT_LIBFTS = @LIBDOVECOT_LIBFTS@
+LIBDOVECOT_LIBFTS_DEPS = @LIBDOVECOT_LIBFTS_DEPS@
+LIBDOVECOT_LOGIN = @LIBDOVECOT_LOGIN@
+LIBDOVECOT_LUA = @LIBDOVECOT_LUA@
+LIBDOVECOT_LUA_DEPS = @LIBDOVECOT_LUA_DEPS@
+LIBDOVECOT_SQL = @LIBDOVECOT_SQL@
+LIBDOVECOT_STORAGE = @LIBDOVECOT_STORAGE@
+LIBDOVECOT_STORAGE_DEPS = @LIBDOVECOT_STORAGE_DEPS@
+LIBEXTTEXTCAT_CFLAGS = @LIBEXTTEXTCAT_CFLAGS@
+LIBEXTTEXTCAT_LIBS = @LIBEXTTEXTCAT_LIBS@
+LIBICONV = @LIBICONV@
+LIBICU_CFLAGS = @LIBICU_CFLAGS@
+LIBICU_LIBS = @LIBICU_LIBS@
+LIBOBJS = @LIBOBJS@
+LIBS = @LIBS@
+LIBSODIUM_CFLAGS = @LIBSODIUM_CFLAGS@
+LIBSODIUM_LIBS = @LIBSODIUM_LIBS@
+LIBTIRPC_CFLAGS = @LIBTIRPC_CFLAGS@
+LIBTIRPC_LIBS = @LIBTIRPC_LIBS@
+LIBTOOL = @LIBTOOL@
+LIBUNWIND_CFLAGS = @LIBUNWIND_CFLAGS@
+LIBUNWIND_LIBS = @LIBUNWIND_LIBS@
+LIBWRAP_LIBS = @LIBWRAP_LIBS@
+LINKED_STORAGE_LDADD = @LINKED_STORAGE_LDADD@
+LIPO = @LIPO@
+LN_S = @LN_S@
+LTLIBICONV = @LTLIBICONV@
+LTLIBOBJS = @LTLIBOBJS@
+LT_SYS_LIBRARY_PATH = @LT_SYS_LIBRARY_PATH@
+LUA_CFLAGS = @LUA_CFLAGS@
+LUA_LIBS = @LUA_LIBS@
+MAINT = @MAINT@
+MAKEINFO = @MAKEINFO@
+MANIFEST_TOOL = @MANIFEST_TOOL@
+MKDIR_P = @MKDIR_P@
+MODULE_LIBS = @MODULE_LIBS@
+MODULE_SUFFIX = @MODULE_SUFFIX@
+MYSQL_CFLAGS = @MYSQL_CFLAGS@
+MYSQL_CONFIG = @MYSQL_CONFIG@
+MYSQL_LIBS = @MYSQL_LIBS@
+NM = @NM@
+NMEDIT = @NMEDIT@
+NOPLUGIN_LDFLAGS = @NOPLUGIN_LDFLAGS@
+OBJDUMP = @OBJDUMP@
+OBJEXT = @OBJEXT@
+OTOOL = @OTOOL@
+OTOOL64 = @OTOOL64@
+PACKAGE = @PACKAGE@
+PACKAGE_BUGREPORT = @PACKAGE_BUGREPORT@
+PACKAGE_NAME = @PACKAGE_NAME@
+PACKAGE_STRING = @PACKAGE_STRING@
+PACKAGE_TARNAME = @PACKAGE_TARNAME@
+PACKAGE_URL = @PACKAGE_URL@
+PACKAGE_VERSION = @PACKAGE_VERSION@
+PANDOC = @PANDOC@
+PATH_SEPARATOR = @PATH_SEPARATOR@
+PGSQL_CFLAGS = @PGSQL_CFLAGS@
+PGSQL_LIBS = @PGSQL_LIBS@
+PG_CONFIG = @PG_CONFIG@
+PIE_CFLAGS = @PIE_CFLAGS@
+PIE_LDFLAGS = @PIE_LDFLAGS@
+PKG_CONFIG = @PKG_CONFIG@
+PKG_CONFIG_LIBDIR = @PKG_CONFIG_LIBDIR@
+PKG_CONFIG_PATH = @PKG_CONFIG_PATH@
+QUOTA_LIBS = @QUOTA_LIBS@
+RANLIB = @RANLIB@
+RELRO_LDFLAGS = @RELRO_LDFLAGS@
+RPCGEN = @RPCGEN@
+RUN_TEST = @RUN_TEST@
+SED = @SED@
+SETTING_FILES = @SETTING_FILES@
+SET_MAKE = @SET_MAKE@
+SHELL = @SHELL@
+SQLITE_CFLAGS = @SQLITE_CFLAGS@
+SQLITE_LIBS = @SQLITE_LIBS@
+SQL_CFLAGS = @SQL_CFLAGS@
+SQL_LIBS = @SQL_LIBS@
+SSL_CFLAGS = @SSL_CFLAGS@
+SSL_LIBS = @SSL_LIBS@
+STRIP = @STRIP@
+SYSTEMD_CFLAGS = @SYSTEMD_CFLAGS@
+SYSTEMD_LIBS = @SYSTEMD_LIBS@
+VALGRIND = @VALGRIND@
+VERSION = @VERSION@
+ZSTD_CFLAGS = @ZSTD_CFLAGS@
+ZSTD_LIBS = @ZSTD_LIBS@
+abs_builddir = @abs_builddir@
+abs_srcdir = @abs_srcdir@
+abs_top_builddir = @abs_top_builddir@
+abs_top_srcdir = @abs_top_srcdir@
+ac_ct_AR = @ac_ct_AR@
+ac_ct_CC = @ac_ct_CC@
+ac_ct_CXX = @ac_ct_CXX@
+ac_ct_DUMPBIN = @ac_ct_DUMPBIN@
+am__include = @am__include@
+am__leading_dot = @am__leading_dot@
+am__quote = @am__quote@
+am__tar = @am__tar@
+am__untar = @am__untar@
+bindir = @bindir@
+build = @build@
+build_alias = @build_alias@
+build_cpu = @build_cpu@
+build_os = @build_os@
+build_vendor = @build_vendor@
+builddir = @builddir@
+datadir = @datadir@
+datarootdir = @datarootdir@
+dict_drivers = @dict_drivers@
+docdir = @docdir@
+dvidir = @dvidir@
+exec_prefix = @exec_prefix@
+host = @host@
+host_alias = @host_alias@
+host_cpu = @host_cpu@
+host_os = @host_os@
+host_vendor = @host_vendor@
+htmldir = @htmldir@
+includedir = @includedir@
+infodir = @infodir@
+install_sh = @install_sh@
+libdir = @libdir@
+libexecdir = @libexecdir@
+localedir = @localedir@
+localstatedir = @localstatedir@
+mandir = @mandir@
+mkdir_p = @mkdir_p@
+moduledir = @moduledir@
+oldincludedir = @oldincludedir@
+pdfdir = @pdfdir@
+prefix = @prefix@
+program_transform_name = @program_transform_name@
+psdir = @psdir@
+rundir = @rundir@
+runstatedir = @runstatedir@
+sbindir = @sbindir@
+sharedstatedir = @sharedstatedir@
+sql_drivers = @sql_drivers@
+srcdir = @srcdir@
+ssldir = @ssldir@
+statedir = @statedir@
+sysconfdir = @sysconfdir@
+systemdservicetype = @systemdservicetype@
+systemdsystemunitdir = @systemdsystemunitdir@
+target_alias = @target_alias@
+top_build_prefix = @top_build_prefix@
+top_builddir = @top_builddir@
+top_srcdir = @top_srcdir@
+noinst_LTLIBRARIES = libdict.la $(am__append_1)
+AM_CPPFLAGS = \
+ -I$(top_srcdir)/src/lib \
+ -I$(top_srcdir)/src/lib-test \
+ -I$(top_srcdir)/src/lib-settings
+
+base_sources = \
+ dict.c \
+ dict-client.c \
+ dict-file.c \
+ dict-memcached.c \
+ dict-memcached-ascii.c \
+ dict-redis.c \
+ dict-fail.c \
+ dict-transaction-memory.c
+
+libdict_la_SOURCES = \
+ $(base_sources)
+
+headers = dict.h dict-client.h dict-private.h \
+ dict-transaction-memory.h $(am__append_2)
+@DLUA_WITH_YIELDS_TRUE@libdict_lua_la_SOURCES = \
+@DLUA_WITH_YIELDS_TRUE@ dict-lua.c \
+@DLUA_WITH_YIELDS_TRUE@ dict-iter-lua.c \
+@DLUA_WITH_YIELDS_TRUE@ dict-txn-lua.c
+
+@DLUA_WITH_YIELDS_TRUE@libdict_lua_la_CPPFLAGS = \
+@DLUA_WITH_YIELDS_TRUE@ $(AM_CPPFLAGS) \
+@DLUA_WITH_YIELDS_TRUE@ $(LUA_CFLAGS) \
+@DLUA_WITH_YIELDS_TRUE@ -I$(top_srcdir)/src/lib-lua
+
+@DLUA_WITH_YIELDS_TRUE@libdict_lua_la_LIBADD =
+@DLUA_WITH_YIELDS_TRUE@libdict_lua_la_DEPENDENCIES = \
+@DLUA_WITH_YIELDS_TRUE@ libdict.la
+
+pkginc_libdir = $(pkgincludedir)
+pkginc_lib_HEADERS = $(headers)
+test_programs = \
+ test-dict
+
+test_libs = \
+ ../lib-test/libtest.la \
+ ../lib/liblib.la
+
+test_dict_SOURCES = test-dict.c
+test_dict_LDADD = libdict.la $(test_libs)
+test_dict_DEPENDENCIES = $(noinst_LTLIBRARIES) $(test_libs)
+test_dict_client_SOURCES = test-dict-client.c
+test_dict_client_LDADD = $(noinst_LTLIBRARIES) ../lib/liblib.la
+test_dict_client_DEPENDENCIES = $(noinst_LTLIBRARIES) $(test_libs)
+all: all-am
+
+.SUFFIXES:
+.SUFFIXES: .c .lo .o .obj
+$(srcdir)/Makefile.in: @MAINTAINER_MODE_TRUE@ $(srcdir)/Makefile.am $(am__configure_deps)
+ @for dep in $?; do \
+ case '$(am__configure_deps)' in \
+ *$$dep*) \
+ ( cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh ) \
+ && { if test -f $@; then exit 0; else break; fi; }; \
+ exit 1;; \
+ esac; \
+ done; \
+ echo ' cd $(top_srcdir) && $(AUTOMAKE) --foreign src/lib-dict/Makefile'; \
+ $(am__cd) $(top_srcdir) && \
+ $(AUTOMAKE) --foreign src/lib-dict/Makefile
+Makefile: $(srcdir)/Makefile.in $(top_builddir)/config.status
+ @case '$?' in \
+ *config.status*) \
+ cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh;; \
+ *) \
+ echo ' cd $(top_builddir) && $(SHELL) ./config.status $(subdir)/$@ $(am__maybe_remake_depfiles)'; \
+ cd $(top_builddir) && $(SHELL) ./config.status $(subdir)/$@ $(am__maybe_remake_depfiles);; \
+ esac;
+
+$(top_builddir)/config.status: $(top_srcdir)/configure $(CONFIG_STATUS_DEPENDENCIES)
+ cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh
+
+$(top_srcdir)/configure: @MAINTAINER_MODE_TRUE@ $(am__configure_deps)
+ cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh
+$(ACLOCAL_M4): @MAINTAINER_MODE_TRUE@ $(am__aclocal_m4_deps)
+ cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh
+$(am__aclocal_m4_deps):
+
+clean-noinstPROGRAMS:
+ @list='$(noinst_PROGRAMS)'; test -n "$$list" || exit 0; \
+ echo " rm -f" $$list; \
+ rm -f $$list || exit $$?; \
+ test -n "$(EXEEXT)" || exit 0; \
+ list=`for p in $$list; do echo "$$p"; done | sed 's/$(EXEEXT)$$//'`; \
+ echo " rm -f" $$list; \
+ rm -f $$list
+
+clean-noinstLTLIBRARIES:
+ -test -z "$(noinst_LTLIBRARIES)" || rm -f $(noinst_LTLIBRARIES)
+ @list='$(noinst_LTLIBRARIES)'; \
+ locs=`for p in $$list; do echo $$p; done | \
+ sed 's|^[^/]*$$|.|; s|/[^/]*$$||; s|$$|/so_locations|' | \
+ sort -u`; \
+ test -z "$$locs" || { \
+ echo rm -f $${locs}; \
+ rm -f $${locs}; \
+ }
+
+libdict.la: $(libdict_la_OBJECTS) $(libdict_la_DEPENDENCIES) $(EXTRA_libdict_la_DEPENDENCIES)
+ $(AM_V_CCLD)$(LINK) $(libdict_la_OBJECTS) $(libdict_la_LIBADD) $(LIBS)
+
+libdict_lua.la: $(libdict_lua_la_OBJECTS) $(libdict_lua_la_DEPENDENCIES) $(EXTRA_libdict_lua_la_DEPENDENCIES)
+ $(AM_V_CCLD)$(LINK) $(am_libdict_lua_la_rpath) $(libdict_lua_la_OBJECTS) $(libdict_lua_la_LIBADD) $(LIBS)
+
+test-dict$(EXEEXT): $(test_dict_OBJECTS) $(test_dict_DEPENDENCIES) $(EXTRA_test_dict_DEPENDENCIES)
+ @rm -f test-dict$(EXEEXT)
+ $(AM_V_CCLD)$(LINK) $(test_dict_OBJECTS) $(test_dict_LDADD) $(LIBS)
+
+test-dict-client$(EXEEXT): $(test_dict_client_OBJECTS) $(test_dict_client_DEPENDENCIES) $(EXTRA_test_dict_client_DEPENDENCIES)
+ @rm -f test-dict-client$(EXEEXT)
+ $(AM_V_CCLD)$(LINK) $(test_dict_client_OBJECTS) $(test_dict_client_LDADD) $(LIBS)
+
+mostlyclean-compile:
+ -rm -f *.$(OBJEXT)
+
+distclean-compile:
+ -rm -f *.tab.c
+
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/dict-client.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/dict-fail.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/dict-file.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/dict-memcached-ascii.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/dict-memcached.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/dict-redis.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/dict-transaction-memory.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/dict.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/libdict_lua_la-dict-iter-lua.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/libdict_lua_la-dict-lua.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/libdict_lua_la-dict-txn-lua.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/test-dict-client.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/test-dict.Po@am__quote@ # am--include-marker
+
+$(am__depfiles_remade):
+ @$(MKDIR_P) $(@D)
+ @echo '# dummy' >$@-t && $(am__mv) $@-t $@
+
+am--depfiles: $(am__depfiles_remade)
+
+.c.o:
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(COMPILE) -MT $@ -MD -MP -MF $(DEPDIR)/$*.Tpo -c -o $@ $<
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/$*.Tpo $(DEPDIR)/$*.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='$<' object='$@' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(COMPILE) -c -o $@ $<
+
+.c.obj:
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(COMPILE) -MT $@ -MD -MP -MF $(DEPDIR)/$*.Tpo -c -o $@ `$(CYGPATH_W) '$<'`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/$*.Tpo $(DEPDIR)/$*.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='$<' object='$@' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(COMPILE) -c -o $@ `$(CYGPATH_W) '$<'`
+
+.c.lo:
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(LTCOMPILE) -MT $@ -MD -MP -MF $(DEPDIR)/$*.Tpo -c -o $@ $<
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/$*.Tpo $(DEPDIR)/$*.Plo
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='$<' object='$@' libtool=yes @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(LTCOMPILE) -c -o $@ $<
+
+libdict_lua_la-dict-lua.lo: dict-lua.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(LIBTOOL) $(AM_V_lt) --tag=CC $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=compile $(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdict_lua_la_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT libdict_lua_la-dict-lua.lo -MD -MP -MF $(DEPDIR)/libdict_lua_la-dict-lua.Tpo -c -o libdict_lua_la-dict-lua.lo `test -f 'dict-lua.c' || echo '$(srcdir)/'`dict-lua.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/libdict_lua_la-dict-lua.Tpo $(DEPDIR)/libdict_lua_la-dict-lua.Plo
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='dict-lua.c' object='libdict_lua_la-dict-lua.lo' libtool=yes @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(LIBTOOL) $(AM_V_lt) --tag=CC $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=compile $(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdict_lua_la_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o libdict_lua_la-dict-lua.lo `test -f 'dict-lua.c' || echo '$(srcdir)/'`dict-lua.c
+
+libdict_lua_la-dict-iter-lua.lo: dict-iter-lua.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(LIBTOOL) $(AM_V_lt) --tag=CC $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=compile $(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdict_lua_la_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT libdict_lua_la-dict-iter-lua.lo -MD -MP -MF $(DEPDIR)/libdict_lua_la-dict-iter-lua.Tpo -c -o libdict_lua_la-dict-iter-lua.lo `test -f 'dict-iter-lua.c' || echo '$(srcdir)/'`dict-iter-lua.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/libdict_lua_la-dict-iter-lua.Tpo $(DEPDIR)/libdict_lua_la-dict-iter-lua.Plo
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='dict-iter-lua.c' object='libdict_lua_la-dict-iter-lua.lo' libtool=yes @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(LIBTOOL) $(AM_V_lt) --tag=CC $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=compile $(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdict_lua_la_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o libdict_lua_la-dict-iter-lua.lo `test -f 'dict-iter-lua.c' || echo '$(srcdir)/'`dict-iter-lua.c
+
+libdict_lua_la-dict-txn-lua.lo: dict-txn-lua.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(LIBTOOL) $(AM_V_lt) --tag=CC $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=compile $(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdict_lua_la_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT libdict_lua_la-dict-txn-lua.lo -MD -MP -MF $(DEPDIR)/libdict_lua_la-dict-txn-lua.Tpo -c -o libdict_lua_la-dict-txn-lua.lo `test -f 'dict-txn-lua.c' || echo '$(srcdir)/'`dict-txn-lua.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/libdict_lua_la-dict-txn-lua.Tpo $(DEPDIR)/libdict_lua_la-dict-txn-lua.Plo
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='dict-txn-lua.c' object='libdict_lua_la-dict-txn-lua.lo' libtool=yes @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(LIBTOOL) $(AM_V_lt) --tag=CC $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=compile $(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdict_lua_la_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o libdict_lua_la-dict-txn-lua.lo `test -f 'dict-txn-lua.c' || echo '$(srcdir)/'`dict-txn-lua.c
+
+mostlyclean-libtool:
+ -rm -f *.lo
+
+clean-libtool:
+ -rm -rf .libs _libs
+install-pkginc_libHEADERS: $(pkginc_lib_HEADERS)
+ @$(NORMAL_INSTALL)
+ @list='$(pkginc_lib_HEADERS)'; test -n "$(pkginc_libdir)" || list=; \
+ if test -n "$$list"; then \
+ echo " $(MKDIR_P) '$(DESTDIR)$(pkginc_libdir)'"; \
+ $(MKDIR_P) "$(DESTDIR)$(pkginc_libdir)" || exit 1; \
+ fi; \
+ for p in $$list; do \
+ if test -f "$$p"; then d=; else d="$(srcdir)/"; fi; \
+ echo "$$d$$p"; \
+ done | $(am__base_list) | \
+ while read files; do \
+ echo " $(INSTALL_HEADER) $$files '$(DESTDIR)$(pkginc_libdir)'"; \
+ $(INSTALL_HEADER) $$files "$(DESTDIR)$(pkginc_libdir)" || exit $$?; \
+ done
+
+uninstall-pkginc_libHEADERS:
+ @$(NORMAL_UNINSTALL)
+ @list='$(pkginc_lib_HEADERS)'; test -n "$(pkginc_libdir)" || list=; \
+ files=`for p in $$list; do echo $$p; done | sed -e 's|^.*/||'`; \
+ dir='$(DESTDIR)$(pkginc_libdir)'; $(am__uninstall_files_from_dir)
+
+ID: $(am__tagged_files)
+ $(am__define_uniq_tagged_files); mkid -fID $$unique
+tags: tags-am
+TAGS: tags
+
+tags-am: $(TAGS_DEPENDENCIES) $(am__tagged_files)
+ set x; \
+ here=`pwd`; \
+ $(am__define_uniq_tagged_files); \
+ shift; \
+ if test -z "$(ETAGS_ARGS)$$*$$unique"; then :; else \
+ test -n "$$unique" || unique=$$empty_fix; \
+ if test $$# -gt 0; then \
+ $(ETAGS) $(ETAGSFLAGS) $(AM_ETAGSFLAGS) $(ETAGS_ARGS) \
+ "$$@" $$unique; \
+ else \
+ $(ETAGS) $(ETAGSFLAGS) $(AM_ETAGSFLAGS) $(ETAGS_ARGS) \
+ $$unique; \
+ fi; \
+ fi
+ctags: ctags-am
+
+CTAGS: ctags
+ctags-am: $(TAGS_DEPENDENCIES) $(am__tagged_files)
+ $(am__define_uniq_tagged_files); \
+ test -z "$(CTAGS_ARGS)$$unique" \
+ || $(CTAGS) $(CTAGSFLAGS) $(AM_CTAGSFLAGS) $(CTAGS_ARGS) \
+ $$unique
+
+GTAGS:
+ here=`$(am__cd) $(top_builddir) && pwd` \
+ && $(am__cd) $(top_srcdir) \
+ && gtags -i $(GTAGS_ARGS) "$$here"
+cscopelist: cscopelist-am
+
+cscopelist-am: $(am__tagged_files)
+ list='$(am__tagged_files)'; \
+ case "$(srcdir)" in \
+ [\\/]* | ?:[\\/]*) sdir="$(srcdir)" ;; \
+ *) sdir=$(subdir)/$(srcdir) ;; \
+ esac; \
+ for i in $$list; do \
+ if test -f "$$i"; then \
+ echo "$(subdir)/$$i"; \
+ else \
+ echo "$$sdir/$$i"; \
+ fi; \
+ done >> $(top_builddir)/cscope.files
+
+distclean-tags:
+ -rm -f TAGS ID GTAGS GRTAGS GSYMS GPATH tags
+
+distdir: $(BUILT_SOURCES)
+ $(MAKE) $(AM_MAKEFLAGS) distdir-am
+
+distdir-am: $(DISTFILES)
+ @srcdirstrip=`echo "$(srcdir)" | sed 's/[].[^$$\\*]/\\\\&/g'`; \
+ topsrcdirstrip=`echo "$(top_srcdir)" | sed 's/[].[^$$\\*]/\\\\&/g'`; \
+ list='$(DISTFILES)'; \
+ dist_files=`for file in $$list; do echo $$file; done | \
+ sed -e "s|^$$srcdirstrip/||;t" \
+ -e "s|^$$topsrcdirstrip/|$(top_builddir)/|;t"`; \
+ case $$dist_files in \
+ */*) $(MKDIR_P) `echo "$$dist_files" | \
+ sed '/\//!d;s|^|$(distdir)/|;s,/[^/]*$$,,' | \
+ sort -u` ;; \
+ esac; \
+ for file in $$dist_files; do \
+ if test -f $$file || test -d $$file; then d=.; else d=$(srcdir); fi; \
+ if test -d $$d/$$file; then \
+ dir=`echo "/$$file" | sed -e 's,/[^/]*$$,,'`; \
+ if test -d "$(distdir)/$$file"; then \
+ find "$(distdir)/$$file" -type d ! -perm -700 -exec chmod u+rwx {} \;; \
+ fi; \
+ if test -d $(srcdir)/$$file && test $$d != $(srcdir); then \
+ cp -fpR $(srcdir)/$$file "$(distdir)$$dir" || exit 1; \
+ find "$(distdir)/$$file" -type d ! -perm -700 -exec chmod u+rwx {} \;; \
+ fi; \
+ cp -fpR $$d/$$file "$(distdir)$$dir" || exit 1; \
+ else \
+ test -f "$(distdir)/$$file" \
+ || cp -p $$d/$$file "$(distdir)/$$file" \
+ || exit 1; \
+ fi; \
+ done
+check-am: all-am
+ $(MAKE) $(AM_MAKEFLAGS) check-local
+check: check-am
+all-am: Makefile $(PROGRAMS) $(LTLIBRARIES) $(HEADERS)
+installdirs:
+ for dir in "$(DESTDIR)$(pkginc_libdir)"; do \
+ test -z "$$dir" || $(MKDIR_P) "$$dir"; \
+ done
+install: install-am
+install-exec: install-exec-am
+install-data: install-data-am
+uninstall: uninstall-am
+
+install-am: all-am
+ @$(MAKE) $(AM_MAKEFLAGS) install-exec-am install-data-am
+
+installcheck: installcheck-am
+install-strip:
+ if test -z '$(STRIP)'; then \
+ $(MAKE) $(AM_MAKEFLAGS) INSTALL_PROGRAM="$(INSTALL_STRIP_PROGRAM)" \
+ install_sh_PROGRAM="$(INSTALL_STRIP_PROGRAM)" INSTALL_STRIP_FLAG=-s \
+ install; \
+ else \
+ $(MAKE) $(AM_MAKEFLAGS) INSTALL_PROGRAM="$(INSTALL_STRIP_PROGRAM)" \
+ install_sh_PROGRAM="$(INSTALL_STRIP_PROGRAM)" INSTALL_STRIP_FLAG=-s \
+ "INSTALL_PROGRAM_ENV=STRIPPROG='$(STRIP)'" install; \
+ fi
+mostlyclean-generic:
+
+clean-generic:
+
+distclean-generic:
+ -test -z "$(CONFIG_CLEAN_FILES)" || rm -f $(CONFIG_CLEAN_FILES)
+ -test . = "$(srcdir)" || test -z "$(CONFIG_CLEAN_VPATH_FILES)" || rm -f $(CONFIG_CLEAN_VPATH_FILES)
+
+maintainer-clean-generic:
+ @echo "This command is intended for maintainers to use"
+ @echo "it deletes files that may require special tools to rebuild."
+clean: clean-am
+
+clean-am: clean-generic clean-libtool clean-noinstLTLIBRARIES \
+ clean-noinstPROGRAMS mostlyclean-am
+
+distclean: distclean-am
+ -rm -f ./$(DEPDIR)/dict-client.Plo
+ -rm -f ./$(DEPDIR)/dict-fail.Plo
+ -rm -f ./$(DEPDIR)/dict-file.Plo
+ -rm -f ./$(DEPDIR)/dict-memcached-ascii.Plo
+ -rm -f ./$(DEPDIR)/dict-memcached.Plo
+ -rm -f ./$(DEPDIR)/dict-redis.Plo
+ -rm -f ./$(DEPDIR)/dict-transaction-memory.Plo
+ -rm -f ./$(DEPDIR)/dict.Plo
+ -rm -f ./$(DEPDIR)/libdict_lua_la-dict-iter-lua.Plo
+ -rm -f ./$(DEPDIR)/libdict_lua_la-dict-lua.Plo
+ -rm -f ./$(DEPDIR)/libdict_lua_la-dict-txn-lua.Plo
+ -rm -f ./$(DEPDIR)/test-dict-client.Po
+ -rm -f ./$(DEPDIR)/test-dict.Po
+ -rm -f Makefile
+distclean-am: clean-am distclean-compile distclean-generic \
+ distclean-tags
+
+dvi: dvi-am
+
+dvi-am:
+
+html: html-am
+
+html-am:
+
+info: info-am
+
+info-am:
+
+install-data-am: install-pkginc_libHEADERS
+
+install-dvi: install-dvi-am
+
+install-dvi-am:
+
+install-exec-am:
+
+install-html: install-html-am
+
+install-html-am:
+
+install-info: install-info-am
+
+install-info-am:
+
+install-man:
+
+install-pdf: install-pdf-am
+
+install-pdf-am:
+
+install-ps: install-ps-am
+
+install-ps-am:
+
+installcheck-am:
+
+maintainer-clean: maintainer-clean-am
+ -rm -f ./$(DEPDIR)/dict-client.Plo
+ -rm -f ./$(DEPDIR)/dict-fail.Plo
+ -rm -f ./$(DEPDIR)/dict-file.Plo
+ -rm -f ./$(DEPDIR)/dict-memcached-ascii.Plo
+ -rm -f ./$(DEPDIR)/dict-memcached.Plo
+ -rm -f ./$(DEPDIR)/dict-redis.Plo
+ -rm -f ./$(DEPDIR)/dict-transaction-memory.Plo
+ -rm -f ./$(DEPDIR)/dict.Plo
+ -rm -f ./$(DEPDIR)/libdict_lua_la-dict-iter-lua.Plo
+ -rm -f ./$(DEPDIR)/libdict_lua_la-dict-lua.Plo
+ -rm -f ./$(DEPDIR)/libdict_lua_la-dict-txn-lua.Plo
+ -rm -f ./$(DEPDIR)/test-dict-client.Po
+ -rm -f ./$(DEPDIR)/test-dict.Po
+ -rm -f Makefile
+maintainer-clean-am: distclean-am maintainer-clean-generic
+
+mostlyclean: mostlyclean-am
+
+mostlyclean-am: mostlyclean-compile mostlyclean-generic \
+ mostlyclean-libtool
+
+pdf: pdf-am
+
+pdf-am:
+
+ps: ps-am
+
+ps-am:
+
+uninstall-am: uninstall-pkginc_libHEADERS
+
+.MAKE: check-am install-am install-strip
+
+.PHONY: CTAGS GTAGS TAGS all all-am am--depfiles check check-am \
+ check-local clean clean-generic clean-libtool \
+ clean-noinstLTLIBRARIES clean-noinstPROGRAMS cscopelist-am \
+ ctags ctags-am distclean distclean-compile distclean-generic \
+ distclean-libtool distclean-tags distdir dvi dvi-am html \
+ html-am info info-am install install-am install-data \
+ install-data-am install-dvi install-dvi-am install-exec \
+ install-exec-am install-html install-html-am install-info \
+ install-info-am install-man install-pdf install-pdf-am \
+ install-pkginc_libHEADERS install-ps install-ps-am \
+ install-strip installcheck installcheck-am installdirs \
+ maintainer-clean maintainer-clean-generic mostlyclean \
+ mostlyclean-compile mostlyclean-generic mostlyclean-libtool \
+ pdf pdf-am ps ps-am tags tags-am uninstall uninstall-am \
+ uninstall-pkginc_libHEADERS
+
+.PRECIOUS: Makefile
+
+
+check-local:
+ for bin in $(test_programs) $(check_PROGRAMS); do \
+ if ! $(RUN_TEST) ./$$bin; then exit 1; fi; \
+ done
+
+# Tell versions [3.59,3.63) of GNU make to not export all variables.
+# Otherwise a system limit (for SysV at least) may be exceeded.
+.NOEXPORT:
diff --git a/src/lib-dict/dict-client.c b/src/lib-dict/dict-client.c
new file mode 100644
index 0000000..32b2da1
--- /dev/null
+++ b/src/lib-dict/dict-client.c
@@ -0,0 +1,1492 @@
+/* Copyright (c) 2005-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "llist.h"
+#include "str.h"
+#include "strescape.h"
+#include "file-lock.h"
+#include "time-util.h"
+#include "connection.h"
+#include "ostream.h"
+#include "eacces-error.h"
+#include "dict-private.h"
+#include "dict-client.h"
+
+#include <unistd.h>
+#include <fcntl.h>
+
+/* Disconnect from dict server after this many milliseconds of idling after
+ sending a command. Because dict server does blocking dict accesses, it can
+ handle only one client at a time. This is why the default timeout is zero,
+ so that there won't be many dict processes just doing nothing. Zero means
+ that the socket is disconnected immediately after returning to ioloop. */
+#define DICT_CLIENT_DEFAULT_TIMEOUT_MSECS 0
+
+/* Abort dict lookup after this many seconds. */
+#define DICT_CLIENT_REQUEST_TIMEOUT_MSECS 30000
+/* When dict lookup timeout is reached, wait a bit longer if the last dict
+ ioloop wait was shorter than this. */
+#define DICT_CLIENT_REQUEST_TIMEOUT_MIN_LAST_IOLOOP_WAIT_MSECS 1000
+/* Log a warning if dict lookup takes longer than this many milliseconds. */
+#define DICT_CLIENT_DEFAULT_WARN_SLOW_MSECS 5000
+
+struct client_dict_cmd {
+ int refcount;
+ struct client_dict *dict;
+ struct timeval start_time;
+ char *query;
+ unsigned int async_id;
+ struct timeval async_id_received_time;
+
+ uint64_t start_global_ioloop_usecs;
+ uint64_t start_dict_ioloop_usecs;
+ uint64_t start_lock_usecs;
+
+ bool reconnected;
+ bool retry_errors;
+ bool no_replies;
+ bool unfinished;
+ bool background;
+
+ void (*callback)(struct client_dict_cmd *cmd,
+ enum dict_protocol_reply reply, const char *value,
+ const char *const *extra_args, const char *error,
+ bool disconnected);
+ struct client_dict_iterate_context *iter;
+ struct client_dict_transaction_context *trans;
+
+ struct {
+ dict_lookup_callback_t *lookup;
+ dict_transaction_commit_callback_t *commit;
+ void *context;
+ } api_callback;
+};
+
+struct dict_client_connection {
+ struct connection conn;
+ struct client_dict *dict;
+};
+
+struct client_dict {
+ struct dict dict;
+ struct dict_client_connection conn;
+
+ char *uri;
+ enum dict_data_type value_type;
+ unsigned warn_slow_msecs;
+
+ time_t last_failed_connect;
+ char *last_connect_error;
+
+ struct io_wait_timer *wait_timer;
+ uint64_t last_timer_switch_usecs;
+ struct timeout *to_requests;
+ struct timeout *to_idle;
+ unsigned int idle_msecs;
+
+ ARRAY(struct client_dict_cmd *) cmds;
+ struct client_dict_transaction_context *transactions;
+
+ unsigned int transaction_id_counter;
+};
+
+struct client_dict_iter_result {
+ const char *key, *const *values;
+};
+
+struct client_dict_iterate_context {
+ struct dict_iterate_context ctx;
+ char *error;
+ char *path;
+ enum dict_iterate_flags flags;
+ int refcount;
+
+ pool_t results_pool;
+ ARRAY(struct client_dict_iter_result) results;
+ unsigned int result_idx;
+
+ bool cmd_sent;
+ bool seen_results;
+ bool finished;
+ bool deinit;
+};
+
+struct client_dict_transaction_context {
+ struct dict_transaction_context ctx;
+ struct client_dict_transaction_context *prev, *next;
+
+ char *first_query;
+ char *error;
+
+ unsigned int id;
+ unsigned int query_count;
+
+ bool sent_begin:1;
+};
+
+static struct connection_list *dict_connections;
+
+static int client_dict_connect(struct client_dict *dict, const char **error_r);
+static int client_dict_reconnect(struct client_dict *dict, const char *reason,
+ const char **error_r);
+static void client_dict_disconnect(struct client_dict *dict, const char *reason);
+static const char *dict_wait_warnings(const struct client_dict_cmd *cmd);
+
+static struct client_dict_cmd *
+client_dict_cmd_init(struct client_dict *dict, const char *query)
+{
+ struct client_dict_cmd *cmd;
+
+ io_loop_time_refresh();
+
+ cmd = i_new(struct client_dict_cmd, 1);
+ cmd->refcount = 1;
+ cmd->dict = dict;
+ cmd->query = i_strdup(query);
+ cmd->start_time = ioloop_timeval;
+ cmd->start_global_ioloop_usecs = ioloop_global_wait_usecs;
+ cmd->start_dict_ioloop_usecs = io_wait_timer_get_usecs(dict->wait_timer);
+ cmd->start_lock_usecs = file_lock_wait_get_total_usecs();
+ return cmd;
+}
+
+static void client_dict_cmd_ref(struct client_dict_cmd *cmd)
+{
+ i_assert(cmd->refcount > 0);
+ cmd->refcount++;
+}
+
+static bool client_dict_cmd_unref(struct client_dict_cmd *cmd)
+{
+ i_assert(cmd->refcount > 0);
+ if (--cmd->refcount > 0)
+ return TRUE;
+
+ i_assert(cmd->trans == NULL);
+
+ i_free(cmd->query);
+ i_free(cmd);
+ return FALSE;
+}
+
+static bool
+dict_cmd_callback_line(struct client_dict_cmd *cmd, const char *const *args)
+{
+ const char *value = args[0];
+ enum dict_protocol_reply reply;
+
+ if (value == NULL) {
+ /* "" is a valid iteration reply */
+ reply = DICT_PROTOCOL_REPLY_ITER_FINISHED;
+ } else {
+ reply = value[0];
+ value++;
+ args++;
+ }
+
+ cmd->unfinished = FALSE;
+ cmd->callback(cmd, reply, value, args, NULL, FALSE);
+ return !cmd->unfinished;
+}
+
+static void
+dict_cmd_callback_error(struct client_dict_cmd *cmd, const char *error,
+ bool disconnected)
+{
+ const char *null_arg = NULL;
+
+ cmd->unfinished = FALSE;
+ if (cmd->callback != NULL) {
+ cmd->callback(cmd, DICT_PROTOCOL_REPLY_ERROR,
+ "", &null_arg, error, disconnected);
+ }
+ i_assert(!cmd->unfinished);
+}
+
+static struct client_dict_cmd *
+client_dict_cmd_first_nonbg(struct client_dict *dict)
+{
+ struct client_dict_cmd *const *cmds;
+ unsigned int i, count;
+
+ cmds = array_get(&dict->cmds, &count);
+ for (i = 0; i < count; i++) {
+ if (!cmds[i]->background)
+ return cmds[i];
+ }
+ return NULL;
+}
+
+static void client_dict_input_timeout(struct client_dict *dict)
+{
+ struct client_dict_cmd *cmd;
+ const char *error;
+ uint64_t msecs_in_last_dict_ioloop_wait;
+ int cmd_diff;
+
+ /* find the first non-background command. there must be at least one. */
+ cmd = client_dict_cmd_first_nonbg(dict);
+ i_assert(cmd != NULL);
+
+ cmd_diff = timeval_diff_msecs(&ioloop_timeval, &cmd->start_time);
+ if (cmd_diff < DICT_CLIENT_REQUEST_TIMEOUT_MSECS) {
+ /* need to re-create this timeout. the currently-oldest
+ command was added when another command was still
+ running with an older timeout. */
+ timeout_remove(&dict->to_requests);
+ dict->to_requests =
+ timeout_add(DICT_CLIENT_REQUEST_TIMEOUT_MSECS - cmd_diff,
+ client_dict_input_timeout, dict);
+ return;
+ }
+
+ /* If we've gotten here because all the time was spent in other ioloops
+ or locks, make sure there's a bit of time waiting for the dict
+ ioloop as well. There's a good chance that the reply can be read. */
+ msecs_in_last_dict_ioloop_wait =
+ (io_wait_timer_get_usecs(dict->wait_timer) -
+ dict->last_timer_switch_usecs + 999) / 1000;
+ if (msecs_in_last_dict_ioloop_wait < DICT_CLIENT_REQUEST_TIMEOUT_MIN_LAST_IOLOOP_WAIT_MSECS) {
+ timeout_remove(&dict->to_requests);
+ dict->to_requests =
+ timeout_add(DICT_CLIENT_REQUEST_TIMEOUT_MIN_LAST_IOLOOP_WAIT_MSECS -
+ msecs_in_last_dict_ioloop_wait,
+ client_dict_input_timeout, dict);
+ return;
+ }
+
+ (void)client_dict_reconnect(dict, t_strdup_printf(
+ "Dict server timeout: %s "
+ "(%u commands pending, oldest sent %u.%03u secs ago: %s, %s)",
+ connection_input_timeout_reason(&dict->conn.conn),
+ array_count(&dict->cmds),
+ cmd_diff/1000, cmd_diff%1000, cmd->query,
+ dict_wait_warnings(cmd)), &error);
+}
+
+static int
+client_dict_cmd_query_send(struct client_dict *dict, const char *query)
+{
+ struct const_iovec iov[2];
+ ssize_t ret;
+
+ iov[0].iov_base = query;
+ iov[0].iov_len = strlen(query);
+ iov[1].iov_base = "\n";
+ iov[1].iov_len = 1;
+ ret = o_stream_sendv(dict->conn.conn.output, iov, 2);
+ if (ret < 0)
+ return -1;
+ i_assert((size_t)ret == iov[0].iov_len + 1);
+ return 0;
+}
+
+static bool
+client_dict_cmd_send(struct client_dict *dict, struct client_dict_cmd **_cmd,
+ const char **error_r)
+{
+ struct client_dict_cmd *cmd = *_cmd;
+ const char *error = NULL;
+ bool retry = cmd->retry_errors;
+ int ret;
+
+ *_cmd = NULL;
+
+ /* we're no longer idling. even with no_replies=TRUE we're going to
+ wait for COMMIT/ROLLBACK. */
+ timeout_remove(&dict->to_idle);
+
+ if (client_dict_connect(dict, &error) < 0) {
+ retry = FALSE;
+ ret = -1;
+ } else {
+ ret = client_dict_cmd_query_send(dict, cmd->query);
+ if (ret < 0) {
+ error = t_strdup_printf("write(%s) failed: %s", dict->conn.conn.name,
+ o_stream_get_error(dict->conn.conn.output));
+ }
+ }
+ if (ret < 0 && retry) {
+ /* Reconnect and try again. */
+ if (client_dict_reconnect(dict, error, &error) < 0)
+ ;
+ else if (client_dict_cmd_query_send(dict, cmd->query) < 0) {
+ error = t_strdup_printf("write(%s) failed: %s", dict->conn.conn.name,
+ o_stream_get_error(dict->conn.conn.output));
+ } else {
+ ret = 0;
+ }
+ }
+
+ if (cmd->no_replies) {
+ /* just send and forget */
+ client_dict_cmd_unref(cmd);
+ return TRUE;
+ } else if (ret < 0) {
+ i_assert(error != NULL);
+ /* we didn't successfully send this command to dict */
+ dict_cmd_callback_error(cmd, error, cmd->reconnected);
+ client_dict_cmd_unref(cmd);
+ if (error_r != NULL)
+ *error_r = error;
+ return FALSE;
+ } else {
+ if (dict->to_requests == NULL && !cmd->background) {
+ dict->to_requests =
+ timeout_add(DICT_CLIENT_REQUEST_TIMEOUT_MSECS,
+ client_dict_input_timeout, dict);
+ }
+ array_push_back(&dict->cmds, &cmd);
+ return TRUE;
+ }
+}
+
+static bool
+client_dict_transaction_send_begin(struct client_dict_transaction_context *ctx,
+ const struct dict_op_settings_private *set)
+{
+ struct client_dict *dict = (struct client_dict *)ctx->ctx.dict;
+ struct client_dict_cmd *cmd;
+ const char *query, *error;
+
+ i_assert(ctx->error == NULL);
+
+ ctx->sent_begin = TRUE;
+
+ /* transactions commands don't have replies. only COMMIT has. */
+ query = t_strdup_printf("%c%u\t%s", DICT_PROTOCOL_CMD_BEGIN,
+ ctx->id,
+ set->username == NULL ? "" : str_tabescape(set->username));
+ cmd = client_dict_cmd_init(dict, query);
+ cmd->no_replies = TRUE;
+ cmd->retry_errors = TRUE;
+ if (!client_dict_cmd_send(dict, &cmd, &error)) {
+ ctx->error = i_strdup(error);
+ return FALSE;
+ }
+ return TRUE;
+}
+
+static void
+client_dict_send_transaction_query(struct client_dict_transaction_context *ctx,
+ const char *query)
+{
+ struct client_dict *dict = (struct client_dict *)ctx->ctx.dict;
+ const struct dict_op_settings_private *set = &ctx->ctx.set;
+ struct client_dict_cmd *cmd;
+ const char *error;
+
+ if (ctx->error != NULL)
+ return;
+
+ if (!ctx->sent_begin) {
+ if (!client_dict_transaction_send_begin(ctx, set))
+ return;
+ }
+
+ ctx->query_count++;
+ if (ctx->first_query == NULL)
+ ctx->first_query = i_strdup(query);
+
+ cmd = client_dict_cmd_init(dict, query);
+ cmd->no_replies = TRUE;
+ if (!client_dict_cmd_send(dict, &cmd, &error))
+ ctx->error = i_strdup(error);
+}
+
+static bool client_dict_is_finished(struct client_dict *dict)
+{
+ return dict->transactions == NULL && array_count(&dict->cmds) == 0;
+}
+
+static void client_dict_timeout(struct client_dict *dict)
+{
+ if (client_dict_is_finished(dict))
+ client_dict_disconnect(dict, "Idle disconnection");
+ else
+ timeout_remove(&dict->to_idle);
+}
+
+static bool client_dict_have_nonbackground_cmds(struct client_dict *dict)
+{
+ struct client_dict_cmd *cmd;
+
+ array_foreach_elem(&dict->cmds, cmd) {
+ if (!cmd->background)
+ return TRUE;
+ }
+ return FALSE;
+}
+
+static void client_dict_add_timeout(struct client_dict *dict)
+{
+ if (dict->to_idle != NULL) {
+ if (dict->idle_msecs > 0)
+ timeout_reset(dict->to_idle);
+ } else if (client_dict_is_finished(dict)) {
+ dict->to_idle = timeout_add(dict->idle_msecs,
+ client_dict_timeout, dict);
+ timeout_remove(&dict->to_requests);
+ } else if (dict->transactions == NULL &&
+ !client_dict_have_nonbackground_cmds(dict)) {
+ /* we had non-background commands, but now we're back to
+ having only background commands. remove timeouts. */
+ timeout_remove(&dict->to_requests);
+ }
+}
+
+static void client_dict_cmd_backgrounded(struct client_dict *dict)
+{
+ if (dict->to_requests == NULL)
+ return;
+
+ if (!client_dict_have_nonbackground_cmds(dict)) {
+ /* we only have background-commands.
+ remove the request timeout. */
+ timeout_remove(&dict->to_requests);
+ }
+}
+
+static int
+dict_conn_assign_next_async_id(struct dict_client_connection *conn,
+ const char *line)
+{
+ struct client_dict_cmd *const *cmds;
+ unsigned int i, count, async_id;
+
+ i_assert(line[0] == DICT_PROTOCOL_REPLY_ASYNC_ID);
+
+ if (str_to_uint(line+1, &async_id) < 0 || async_id == 0) {
+ e_error(conn->conn.event, "Received invalid async-id line: %s",
+ line);
+ return -1;
+ }
+ cmds = array_get(&conn->dict->cmds, &count);
+ for (i = 0; i < count; i++) {
+ if (cmds[i]->async_id == 0) {
+ cmds[i]->async_id = async_id;
+ cmds[i]->async_id_received_time = ioloop_timeval;
+ return 0;
+ }
+ }
+ e_error(conn->conn.event, "Received async-id line, but all %u "
+ "commands already have it: %s",
+ count, line);
+ return -1;
+}
+
+static int dict_conn_find_async_id(struct dict_client_connection *conn,
+ const char *async_arg,
+ const char *line, unsigned int *idx_r)
+{
+ struct client_dict_cmd *const *cmds;
+ unsigned int i, count, async_id;
+
+ i_assert(async_arg[0] == DICT_PROTOCOL_REPLY_ASYNC_REPLY);
+
+ if (str_to_uint(async_arg+1, &async_id) < 0 || async_id == 0) {
+ e_error(conn->conn.event, "Received invalid async-reply line: %s",
+ line);
+ return -1;
+ }
+
+ cmds = array_get(&conn->dict->cmds, &count);
+ for (i = 0; i < count; i++) {
+ if (cmds[i]->async_id == async_id) {
+ *idx_r = i;
+ return 0;
+ }
+ }
+ e_error(conn->conn.event, "Received reply for nonexistent async-id %u: %s",
+ async_id, line);
+ return -1;
+}
+
+static int dict_conn_input_line(struct connection *_conn, const char *line)
+{
+ struct dict_client_connection *conn =
+ (struct dict_client_connection *)_conn;
+ struct client_dict *dict = conn->dict;
+ struct client_dict_cmd *const *cmds;
+ const char *const *args;
+ unsigned int i, count;
+ bool finished;
+
+ if (dict->to_requests != NULL)
+ timeout_reset(dict->to_requests);
+
+ if (line[0] == DICT_PROTOCOL_REPLY_ASYNC_ID)
+ return dict_conn_assign_next_async_id(conn, line) < 0 ? -1 : 1;
+
+ cmds = array_get(&conn->dict->cmds, &count);
+ if (count == 0) {
+ e_error(conn->conn.event, "Received reply without pending commands: %s",
+ line);
+ return -1;
+ }
+
+ args = t_strsplit_tabescaped(line);
+ if (args[0] != NULL && args[0][0] == DICT_PROTOCOL_REPLY_ASYNC_REPLY) {
+ if (dict_conn_find_async_id(conn, args[0], line, &i) < 0)
+ return -1;
+ args++;
+ } else {
+ i = 0;
+ }
+ i_assert(!cmds[i]->no_replies);
+
+ client_dict_cmd_ref(cmds[i]);
+ finished = dict_cmd_callback_line(cmds[i], args);
+ if (!client_dict_cmd_unref(cmds[i])) {
+ /* disconnected during command handling */
+ return -1;
+ }
+ if (!finished) {
+ /* more lines needed for this command */
+ return 1;
+ }
+ client_dict_cmd_unref(cmds[i]);
+ array_delete(&dict->cmds, i, 1);
+
+ client_dict_add_timeout(dict);
+ return 1;
+}
+
+static int client_dict_connect(struct client_dict *dict, const char **error_r)
+{
+ const char *query, *error;
+
+ if (dict->conn.conn.fd_in != -1)
+ return 0;
+ if (dict->last_failed_connect == ioloop_time) {
+ /* Try again later */
+ *error_r = dict->last_connect_error;
+ return -1;
+ }
+
+ if (connection_client_connect(&dict->conn.conn) < 0) {
+ dict->last_failed_connect = ioloop_time;
+ if (errno == EACCES) {
+ error = eacces_error_get("net_connect_unix",
+ dict->conn.conn.name);
+ } else {
+ error = t_strdup_printf(
+ "net_connect_unix(%s) failed: %m", dict->conn.conn.name);
+ }
+ i_free(dict->last_connect_error);
+ dict->last_connect_error = i_strdup(error);
+ *error_r = error;
+ return -1;
+ }
+
+ query = t_strdup_printf("%c%u\t%u\t%d\t%s\t%s\n",
+ DICT_PROTOCOL_CMD_HELLO,
+ DICT_CLIENT_PROTOCOL_MAJOR_VERSION,
+ DICT_CLIENT_PROTOCOL_MINOR_VERSION,
+ dict->value_type,
+ "",
+ str_tabescape(dict->uri));
+ o_stream_nsend_str(dict->conn.conn.output, query);
+ client_dict_add_timeout(dict);
+ return 0;
+}
+
+static void
+client_dict_abort_commands(struct client_dict *dict, const char *reason)
+{
+ ARRAY(struct client_dict_cmd *) cmds_copy;
+ struct client_dict_cmd *cmd;
+
+ /* abort all commands */
+ t_array_init(&cmds_copy, array_count(&dict->cmds));
+ array_append_array(&cmds_copy, &dict->cmds);
+ array_clear(&dict->cmds);
+
+ array_foreach_elem(&cmds_copy, cmd) {
+ dict_cmd_callback_error(cmd, reason, TRUE);
+ client_dict_cmd_unref(cmd);
+ }
+}
+
+static void client_dict_disconnect(struct client_dict *dict, const char *reason)
+{
+ struct client_dict_transaction_context *ctx, *next;
+
+ client_dict_abort_commands(dict, reason);
+
+ /* all transactions that have sent BEGIN are no longer valid */
+ for (ctx = dict->transactions; ctx != NULL; ctx = next) {
+ next = ctx->next;
+ if (ctx->sent_begin && ctx->error == NULL)
+ ctx->error = i_strdup(reason);
+ }
+
+ timeout_remove(&dict->to_idle);
+ timeout_remove(&dict->to_requests);
+ connection_disconnect(&dict->conn.conn);
+}
+
+static int client_dict_reconnect(struct client_dict *dict, const char *reason,
+ const char **error_r)
+{
+ ARRAY(struct client_dict_cmd *) retry_cmds;
+ struct client_dict_cmd *cmd;
+ const char *error;
+ int ret;
+
+ t_array_init(&retry_cmds, array_count(&dict->cmds));
+ for (unsigned int i = 0; i < array_count(&dict->cmds); ) {
+ cmd = array_idx_elem(&dict->cmds, i);
+ if (!cmd->retry_errors) {
+ i++;
+ } else if (cmd->iter != NULL &&
+ cmd->iter->seen_results) {
+ /* don't retry iteration that already returned
+ something to the caller. otherwise we'd return
+ duplicates. */
+ i++;
+ } else {
+ array_push_back(&retry_cmds, &cmd);
+ array_delete(&dict->cmds, i, 1);
+ }
+ }
+ client_dict_disconnect(dict, reason);
+ if (client_dict_connect(dict, error_r) < 0) {
+ reason = t_strdup_printf("%s - reconnect failed: %s",
+ reason, *error_r);
+ array_foreach_elem(&retry_cmds, cmd) {
+ dict_cmd_callback_error(cmd, reason, TRUE);
+ client_dict_cmd_unref(cmd);
+ }
+ return -1;
+ }
+ if (array_count(&retry_cmds) == 0)
+ return 0;
+ e_warning(dict->conn.conn.event, "%s - reconnected", reason);
+
+ ret = 0; error = "";
+ array_foreach_elem(&retry_cmds, cmd) {
+ cmd->reconnected = TRUE;
+ cmd->async_id = 0;
+ /* if it fails again, don't retry anymore */
+ cmd->retry_errors = FALSE;
+ if (ret < 0) {
+ dict_cmd_callback_error(cmd, error, TRUE);
+ client_dict_cmd_unref(cmd);
+ } else if (!client_dict_cmd_send(dict, &cmd, &error))
+ ret = -1;
+ }
+ return ret;
+}
+
+static void dict_conn_destroy(struct connection *_conn)
+{
+ struct dict_client_connection *conn =
+ (struct dict_client_connection *)_conn;
+
+ client_dict_disconnect(conn->dict, connection_disconnect_reason(_conn));
+}
+
+static const struct connection_settings dict_conn_set = {
+ .input_max_size = SIZE_MAX,
+ .output_max_size = SIZE_MAX,
+ .unix_client_connect_msecs = 1000,
+ .client = TRUE
+};
+
+static const struct connection_vfuncs dict_conn_vfuncs = {
+ .destroy = dict_conn_destroy,
+ .input_line = dict_conn_input_line
+};
+
+static int
+client_dict_init(struct dict *driver, const char *uri,
+ const struct dict_settings *set,
+ struct dict **dict_r, const char **error_r)
+{
+ struct ioloop *old_ioloop = current_ioloop;
+ struct client_dict *dict;
+ const char *p, *dest_uri, *path;
+ unsigned int idle_msecs = DICT_CLIENT_DEFAULT_TIMEOUT_MSECS;
+ unsigned int warn_slow_msecs = DICT_CLIENT_DEFAULT_WARN_SLOW_MSECS;
+
+ /* uri = [idle_msecs=<n>:] [warn_slow_msecs=<n>:] [<path>] ":" <uri> */
+ for (;;) {
+ if (str_begins(uri, "idle_msecs=")) {
+ p = strchr(uri+11, ':');
+ if (p == NULL) {
+ *error_r = t_strdup_printf("Invalid URI: %s", uri);
+ return -1;
+ }
+ if (str_to_uint(t_strdup_until(uri+11, p), &idle_msecs) < 0) {
+ *error_r = "Invalid idle_msecs";
+ return -1;
+ }
+ uri = p+1;
+ } else if (str_begins(uri, "warn_slow_msecs=")) {
+ p = strchr(uri+11, ':');
+ if (p == NULL) {
+ *error_r = t_strdup_printf("Invalid URI: %s", uri);
+ return -1;
+ }
+ if (str_to_uint(t_strdup_until(uri+16, p), &warn_slow_msecs) < 0) {
+ *error_r = "Invalid warn_slow_msecs";
+ return -1;
+ }
+ uri = p+1;
+ } else {
+ break;
+ }
+ }
+ dest_uri = strchr(uri, ':');
+ if (dest_uri == NULL) {
+ *error_r = t_strdup_printf("Invalid URI: %s", uri);
+ return -1;
+ }
+
+ if (dict_connections == NULL) {
+ dict_connections = connection_list_init(&dict_conn_set,
+ &dict_conn_vfuncs);
+ }
+
+ dict = i_new(struct client_dict, 1);
+ dict->dict = *driver;
+ dict->conn.dict = dict;
+ dict->conn.conn.event_parent = set->event_parent;
+ dict->idle_msecs = idle_msecs;
+ dict->warn_slow_msecs = warn_slow_msecs;
+ i_array_init(&dict->cmds, 32);
+
+ if (uri[0] == ':') {
+ /* default path */
+ path = t_strconcat(set->base_dir,
+ "/"DEFAULT_DICT_SERVER_SOCKET_FNAME, NULL);
+ } else if (uri[0] == '/') {
+ /* absolute path */
+ path = t_strdup_until(uri, dest_uri);
+ } else {
+ /* relative path to base_dir */
+ path = t_strconcat(set->base_dir, "/",
+ t_strdup_until(uri, dest_uri), NULL);
+ }
+ connection_init_client_unix(dict_connections, &dict->conn.conn, path);
+ dict->uri = i_strdup(dest_uri + 1);
+
+ dict->dict.ioloop = io_loop_create();
+ dict->wait_timer = io_wait_timer_add();
+ io_loop_set_current(old_ioloop);
+ *dict_r = &dict->dict;
+ return 0;
+}
+
+static void client_dict_deinit(struct dict *_dict)
+{
+ struct client_dict *dict = (struct client_dict *)_dict;
+ struct ioloop *old_ioloop = current_ioloop;
+
+ client_dict_disconnect(dict, "Deinit");
+ connection_deinit(&dict->conn.conn);
+ io_wait_timer_remove(&dict->wait_timer);
+
+ i_assert(dict->transactions == NULL);
+ i_assert(array_count(&dict->cmds) == 0);
+
+ io_loop_set_current(dict->dict.ioloop);
+ io_loop_destroy(&dict->dict.ioloop);
+ io_loop_set_current(old_ioloop);
+
+ array_free(&dict->cmds);
+ i_free(dict->last_connect_error);
+ i_free(dict->uri);
+ i_free(dict);
+
+ if (dict_connections->connections == NULL)
+ connection_list_deinit(&dict_connections);
+}
+
+static void client_dict_wait(struct dict *_dict)
+{
+ struct client_dict *dict = (struct client_dict *)_dict;
+
+ if (array_count(&dict->cmds) == 0)
+ return;
+
+ i_assert(io_loop_is_empty(dict->dict.ioloop));
+ dict->dict.prev_ioloop = current_ioloop;
+ io_loop_set_current(dict->dict.ioloop);
+ dict_switch_ioloop(_dict);
+ while (array_count(&dict->cmds) > 0)
+ io_loop_run(dict->dict.ioloop);
+
+ io_loop_set_current(dict->dict.prev_ioloop);
+ dict->dict.prev_ioloop = NULL;
+
+ dict_switch_ioloop(_dict);
+ i_assert(io_loop_is_empty(dict->dict.ioloop));
+}
+
+static bool client_dict_switch_ioloop(struct dict *_dict)
+{
+ struct client_dict *dict = (struct client_dict *)_dict;
+
+ dict->last_timer_switch_usecs =
+ io_wait_timer_get_usecs(dict->wait_timer);
+ dict->wait_timer = io_wait_timer_move(&dict->wait_timer);
+ if (dict->to_idle != NULL)
+ dict->to_idle = io_loop_move_timeout(&dict->to_idle);
+ if (dict->to_requests != NULL)
+ dict->to_requests = io_loop_move_timeout(&dict->to_requests);
+ connection_switch_ioloop(&dict->conn.conn);
+ return array_count(&dict->cmds) > 0;
+}
+
+static const char *dict_wait_warnings(const struct client_dict_cmd *cmd)
+{
+ int global_ioloop_msecs = (ioloop_global_wait_usecs -
+ cmd->start_global_ioloop_usecs + 999) / 1000;
+ int dict_ioloop_msecs = (io_wait_timer_get_usecs(cmd->dict->wait_timer) -
+ cmd->start_dict_ioloop_usecs + 999) / 1000;
+ int other_ioloop_msecs = global_ioloop_msecs - dict_ioloop_msecs;
+ int lock_msecs = (file_lock_wait_get_total_usecs() -
+ cmd->start_lock_usecs + 999) / 1000;
+
+ return t_strdup_printf(
+ "%d.%03d in dict wait, %d.%03d in other ioloops, %d.%03d in locks",
+ dict_ioloop_msecs/1000, dict_ioloop_msecs%1000,
+ other_ioloop_msecs/1000, other_ioloop_msecs%1000,
+ lock_msecs/1000, lock_msecs%1000);
+}
+
+static const char *
+dict_warnings_sec(const struct client_dict_cmd *cmd, int msecs,
+ const char *const *extra_args)
+{
+ string_t *str = t_str_new(64);
+ struct timeval tv_start, tv_end;
+ unsigned int tv_start_usec, tv_end_usec;
+
+ str_printfa(str, "%d.%03d secs (%s", msecs/1000, msecs%1000,
+ dict_wait_warnings(cmd));
+ if (cmd->reconnected) {
+ int reconnected_msecs =
+ timeval_diff_msecs(&ioloop_timeval,
+ &cmd->dict->conn.conn.connect_started);
+ str_printfa(str, ", reconnected %u.%03u secs ago",
+ reconnected_msecs/1000, reconnected_msecs%1000);
+ }
+ if (cmd->async_id != 0) {
+ int async_reply_msecs =
+ timeval_diff_msecs(&ioloop_timeval, &cmd->async_id_received_time);
+ str_printfa(str, ", async-id reply %u.%03u secs ago",
+ async_reply_msecs/1000, async_reply_msecs%1000);
+ }
+ if (extra_args != NULL &&
+ str_array_length(extra_args) >= 4 &&
+ str_to_time(extra_args[0], &tv_start.tv_sec) == 0 &&
+ str_to_uint(extra_args[1], &tv_start_usec) == 0 &&
+ str_to_time(extra_args[2], &tv_end.tv_sec) == 0 &&
+ str_to_uint(extra_args[3], &tv_end_usec) == 0) {
+ tv_start.tv_usec = tv_start_usec;
+ tv_end.tv_usec = tv_end_usec;
+
+ int server_msecs_since_start =
+ timeval_diff_msecs(&ioloop_timeval, &tv_start);
+ int server_msecs = timeval_diff_msecs(&tv_end, &tv_start);
+ str_printfa(str, ", started on dict-server %u.%03d secs ago, "
+ "took %u.%03d secs",
+ server_msecs_since_start/1000,
+ server_msecs_since_start%1000,
+ server_msecs/1000, server_msecs%1000);
+ }
+ str_append_c(str, ')');
+ return str_c(str);
+}
+
+static void
+client_dict_lookup_async_callback(struct client_dict_cmd *cmd,
+ enum dict_protocol_reply reply,
+ const char *value,
+ const char *const *extra_args,
+ const char *error,
+ bool disconnected ATTR_UNUSED)
+{
+ struct client_dict *dict = cmd->dict;
+ struct dict_lookup_result result;
+ const char *const values[] = { value, NULL };
+
+ i_zero(&result);
+ if (error != NULL) {
+ result.ret = -1;
+ result.error = error;
+ } else switch (reply) {
+ case DICT_PROTOCOL_REPLY_OK:
+ result.value = value;
+ result.values = values;
+ result.ret = 1;
+ break;
+ case DICT_PROTOCOL_REPLY_MULTI_OK:
+ result.values = t_strsplit_tabescaped(value);
+ result.value = result.values[0];
+ result.ret = 1;
+ break;
+ case DICT_PROTOCOL_REPLY_NOTFOUND:
+ result.ret = 0;
+ break;
+ case DICT_PROTOCOL_REPLY_FAIL:
+ result.error = value[0] == '\0' ? "dict-server returned failure" :
+ t_strdup_printf("dict-server returned failure: %s",
+ value);
+ result.ret = -1;
+ break;
+ default:
+ result.error = t_strdup_printf(
+ "dict-client: Invalid lookup '%s' reply: %c%s",
+ cmd->query, reply, value);
+ client_dict_disconnect(dict, result.error);
+ result.ret = -1;
+ break;
+ }
+
+ int diff = timeval_diff_msecs(&ioloop_timeval, &cmd->start_time);
+ if (result.error != NULL) {
+ /* include timing info always in error messages */
+ result.error = t_strdup_printf("%s (reply took %s)",
+ result.error, dict_warnings_sec(cmd, diff, extra_args));
+ } else if (!cmd->background &&
+ diff >= (int)dict->warn_slow_msecs) {
+ e_warning(dict->conn.conn.event, "dict lookup took %s: %s",
+ dict_warnings_sec(cmd, diff, extra_args),
+ cmd->query);
+ }
+
+ dict_pre_api_callback(&dict->dict);
+ cmd->api_callback.lookup(&result, cmd->api_callback.context);
+ dict_post_api_callback(&dict->dict);
+}
+
+static void
+client_dict_lookup_async(struct dict *_dict, const struct dict_op_settings *set,
+ const char *key, dict_lookup_callback_t *callback,
+ void *context)
+{
+ struct client_dict *dict = (struct client_dict *)_dict;
+ struct client_dict_cmd *cmd;
+ const char *query;
+
+ query = t_strdup_printf("%c%s\t%s", DICT_PROTOCOL_CMD_LOOKUP,
+ str_tabescape(key),
+ set->username == NULL ? "" : str_tabescape(set->username));
+ cmd = client_dict_cmd_init(dict, query);
+ cmd->callback = client_dict_lookup_async_callback;
+ cmd->api_callback.lookup = callback;
+ cmd->api_callback.context = context;
+ cmd->retry_errors = TRUE;
+
+ client_dict_cmd_send(dict, &cmd, NULL);
+}
+
+struct client_dict_sync_lookup {
+ char *error;
+ char *value;
+ int ret;
+};
+
+static void client_dict_lookup_callback(const struct dict_lookup_result *result,
+ struct client_dict_sync_lookup *lookup)
+{
+ lookup->ret = result->ret;
+ if (result->ret == -1)
+ lookup->error = i_strdup(result->error);
+ else if (result->ret == 1)
+ lookup->value = i_strdup(result->value);
+}
+
+static int client_dict_lookup(struct dict *_dict,
+ const struct dict_op_settings *set,
+ pool_t pool, const char *key,
+ const char **value_r, const char **error_r)
+{
+ struct client_dict_sync_lookup lookup;
+
+ i_zero(&lookup);
+ lookup.ret = -2;
+
+ dict_lookup_async(_dict, set, key, client_dict_lookup_callback, &lookup);
+ if (lookup.ret == -2)
+ client_dict_wait(_dict);
+
+ switch (lookup.ret) {
+ case -1:
+ *error_r = t_strdup(lookup.error);
+ i_free(lookup.error);
+ return -1;
+ case 0:
+ i_assert(lookup.value == NULL);
+ *value_r = NULL;
+ return 0;
+ case 1:
+ *value_r = p_strdup(pool, lookup.value);
+ i_free(lookup.value);
+ return 1;
+ }
+ i_unreached();
+}
+
+static void client_dict_iterate_unref(struct client_dict_iterate_context *ctx)
+{
+ i_assert(ctx->refcount > 0);
+ if (--ctx->refcount > 0)
+ return;
+ i_free(ctx->error);
+ i_free(ctx);
+}
+
+static void
+client_dict_iter_api_callback(struct client_dict_iterate_context *ctx,
+ struct client_dict_cmd *cmd,
+ const char *const *extra_args)
+{
+ struct client_dict *dict = cmd->dict;
+
+ if (ctx->deinit) {
+ /* Iterator was already deinitialized. Stop if we're in
+ client_dict_wait(). */
+ dict_post_api_callback(&dict->dict);
+ return;
+ }
+ if (ctx->finished) {
+ int diff = timeval_diff_msecs(&ioloop_timeval, &cmd->start_time);
+ if (ctx->error != NULL) {
+ /* include timing info always in error messages */
+ char *new_error = i_strdup_printf("%s (reply took %s)",
+ ctx->error, dict_warnings_sec(cmd, diff, extra_args));
+ i_free(ctx->error);
+ ctx->error = new_error;
+ } else if (!cmd->background &&
+ diff >= (int)dict->warn_slow_msecs) {
+ e_warning(dict->conn.conn.event, "dict iteration took %s: %s",
+ dict_warnings_sec(cmd, diff, extra_args),
+ cmd->query);
+ }
+ }
+ if (ctx->ctx.async_callback != NULL) {
+ dict_pre_api_callback(&dict->dict);
+ ctx->ctx.async_callback(ctx->ctx.async_context);
+ dict_post_api_callback(&dict->dict);
+ } else {
+ /* synchronous lookup */
+ io_loop_stop(dict->dict.ioloop);
+ }
+}
+
+static void
+client_dict_iter_async_callback(struct client_dict_cmd *cmd,
+ enum dict_protocol_reply reply,
+ const char *value,
+ const char *const *extra_args,
+ const char *error,
+ bool disconnected ATTR_UNUSED)
+{
+ struct client_dict_iterate_context *ctx = cmd->iter;
+ struct client_dict *dict = cmd->dict;
+ struct client_dict_iter_result *result;
+ const char *iter_key = NULL, *const *iter_values = NULL;
+
+ if (ctx->deinit) {
+ cmd->background = TRUE;
+ client_dict_cmd_backgrounded(dict);
+ }
+
+ if (error != NULL) {
+ /* failed */
+ } else switch (reply) {
+ case DICT_PROTOCOL_REPLY_ITER_FINISHED:
+ /* end of iteration */
+ i_assert(!ctx->finished);
+ ctx->finished = TRUE;
+ client_dict_iter_api_callback(ctx, cmd, extra_args);
+ client_dict_iterate_unref(ctx);
+ return;
+ case DICT_PROTOCOL_REPLY_OK:
+ /* key \t value */
+ iter_key = value;
+ iter_values = extra_args;
+ extra_args++;
+ break;
+ case DICT_PROTOCOL_REPLY_FAIL:
+ error = t_strdup_printf("dict-server returned failure: %s", value);
+ break;
+ default:
+ break;
+ }
+ if ((iter_values == NULL || iter_values[0] == NULL) && error == NULL) {
+ /* broken protocol */
+ error = t_strdup_printf("dict client (%s) sent broken iterate reply: %c%s",
+ dict->conn.conn.name, reply, value);
+ client_dict_disconnect(dict, error);
+ }
+
+ if (error != NULL) {
+ if (ctx->error == NULL)
+ ctx->error = i_strdup(error);
+ i_assert(!ctx->finished);
+ ctx->finished = TRUE;
+ client_dict_iter_api_callback(ctx, cmd, extra_args);
+ client_dict_iterate_unref(ctx);
+ return;
+ }
+ cmd->unfinished = TRUE;
+
+ if (ctx->deinit) {
+ /* iterator was already deinitialized */
+ return;
+ }
+
+ result = array_append_space(&ctx->results);
+ result->key = p_strdup(ctx->results_pool, iter_key);
+ result->values = p_strarray_dup(ctx->results_pool, iter_values);
+
+ client_dict_iter_api_callback(ctx, cmd, NULL);
+}
+
+static struct dict_iterate_context *
+client_dict_iterate_init(struct dict *_dict,
+ const struct dict_op_settings *set ATTR_UNUSED,
+ const char *path, enum dict_iterate_flags flags)
+{
+ struct client_dict_iterate_context *ctx;
+
+ ctx = i_new(struct client_dict_iterate_context, 1);
+ ctx->ctx.dict = _dict;
+ ctx->results_pool = pool_alloconly_create("client dict iteration", 512);
+ ctx->flags = flags;
+ ctx->path = i_strdup(path);
+ ctx->refcount = 1;
+ i_array_init(&ctx->results, 64);
+ return &ctx->ctx;
+}
+
+static void
+client_dict_iterate_cmd_send(struct client_dict_iterate_context *ctx)
+{
+ struct client_dict *dict = (struct client_dict *)ctx->ctx.dict;
+ const struct dict_op_settings_private *set = &ctx->ctx.set;
+ struct client_dict_cmd *cmd;
+ string_t *query = t_str_new(256);
+
+ /* we can't do this query in _iterate_init(), because
+ _set_limit() hasn't been called yet at that point. */
+ str_printfa(query, "%c%d\t%"PRIu64, DICT_PROTOCOL_CMD_ITERATE,
+ ctx->flags, ctx->ctx.max_rows);
+ str_append_c(query, '\t');
+ str_append_tabescaped(query, ctx->path);
+ str_append_c(query, '\t');
+ str_append_tabescaped(query, set->username == NULL ? "" : set->username);
+
+ cmd = client_dict_cmd_init(dict, str_c(query));
+ cmd->iter = ctx;
+ cmd->callback = client_dict_iter_async_callback;
+ cmd->retry_errors = TRUE;
+
+ ctx->refcount++;
+ client_dict_cmd_send(dict, &cmd, NULL);
+}
+
+static bool client_dict_iterate(struct dict_iterate_context *_ctx,
+ const char **key_r, const char *const **values_r)
+{
+ struct client_dict_iterate_context *ctx =
+ (struct client_dict_iterate_context *)_ctx;
+ const struct client_dict_iter_result *results;
+ unsigned int count;
+
+ if (ctx->error != NULL) {
+ ctx->ctx.has_more = FALSE;
+ return FALSE;
+ }
+
+ results = array_get(&ctx->results, &count);
+ if (ctx->result_idx < count) {
+ *key_r = results[ctx->result_idx].key;
+ *values_r = results[ctx->result_idx].values;
+ ctx->ctx.has_more = TRUE;
+ ctx->result_idx++;
+ ctx->seen_results = TRUE;
+ return TRUE;
+ }
+ if (!ctx->cmd_sent) {
+ ctx->cmd_sent = TRUE;
+ client_dict_iterate_cmd_send(ctx);
+ return client_dict_iterate(_ctx, key_r, values_r);
+ }
+ ctx->ctx.has_more = !ctx->finished;
+ ctx->result_idx = 0;
+ array_clear(&ctx->results);
+ p_clear(ctx->results_pool);
+
+ if ((ctx->flags & DICT_ITERATE_FLAG_ASYNC) == 0 && ctx->ctx.has_more) {
+ client_dict_wait(_ctx->dict);
+ return client_dict_iterate(_ctx, key_r, values_r);
+ }
+ return FALSE;
+}
+
+static int client_dict_iterate_deinit(struct dict_iterate_context *_ctx,
+ const char **error_r)
+{
+ struct client_dict *dict = (struct client_dict *)_ctx->dict;
+ struct client_dict_iterate_context *ctx =
+ (struct client_dict_iterate_context *)_ctx;
+ int ret = ctx->error != NULL ? -1 : 0;
+
+ i_assert(!ctx->deinit);
+ ctx->deinit = TRUE;
+
+ *error_r = t_strdup(ctx->error);
+ array_free(&ctx->results);
+ pool_unref(&ctx->results_pool);
+ i_free(ctx->path);
+ client_dict_iterate_unref(ctx);
+
+ client_dict_add_timeout(dict);
+ return ret;
+}
+
+static struct dict_transaction_context *
+client_dict_transaction_init(struct dict *_dict)
+{
+ struct client_dict *dict = (struct client_dict *)_dict;
+ struct client_dict_transaction_context *ctx;
+
+ ctx = i_new(struct client_dict_transaction_context, 1);
+ ctx->ctx.dict = _dict;
+ ctx->id = ++dict->transaction_id_counter;
+
+ DLLIST_PREPEND(&dict->transactions, ctx);
+ return &ctx->ctx;
+}
+
+static void
+client_dict_transaction_free(struct client_dict_transaction_context **_ctx)
+{
+ struct client_dict_transaction_context *ctx = *_ctx;
+
+ *_ctx = NULL;
+ i_free(ctx->first_query);
+ i_free(ctx->error);
+ i_free(ctx);
+}
+
+static void
+client_dict_transaction_commit_callback(struct client_dict_cmd *cmd,
+ enum dict_protocol_reply reply,
+ const char *value,
+ const char *const *extra_args,
+ const char *error, bool disconnected)
+{
+ struct client_dict *dict = cmd->dict;
+ struct dict_commit_result result = {
+ .ret = DICT_COMMIT_RET_FAILED, .error = NULL
+ };
+
+ i_assert(cmd->trans != NULL);
+
+ if (error != NULL) {
+ /* failed */
+ if (disconnected)
+ result.ret = DICT_COMMIT_RET_WRITE_UNCERTAIN;
+ result.error = error;
+ } else switch (reply) {
+ case DICT_PROTOCOL_REPLY_OK:
+ result.ret = DICT_COMMIT_RET_OK;
+ break;
+ case DICT_PROTOCOL_REPLY_NOTFOUND:
+ result.ret = DICT_COMMIT_RET_NOTFOUND;
+ break;
+ case DICT_PROTOCOL_REPLY_WRITE_UNCERTAIN:
+ result.ret = DICT_COMMIT_RET_WRITE_UNCERTAIN;
+ /* fallthrough */
+ case DICT_PROTOCOL_REPLY_FAIL: {
+ /* value contains the obsolete trans_id */
+ const char *error = extra_args[0];
+
+ result.error = t_strdup_printf("dict-server returned failure: %s",
+ error != NULL ? t_str_tabunescape(error) : "");
+ if (error != NULL)
+ extra_args++;
+ break;
+ }
+ default:
+ result.ret = DICT_COMMIT_RET_FAILED;
+ result.error = t_strdup_printf(
+ "dict-client: Invalid commit reply: %c%s",
+ reply, value);
+ client_dict_disconnect(dict, result.error);
+ break;
+ }
+
+ int diff = timeval_diff_msecs(&ioloop_timeval, &cmd->start_time);
+ if (result.error != NULL) {
+ /* include timing info always in error messages */
+ result.error = t_strdup_printf("%s (reply took %s)",
+ result.error, dict_warnings_sec(cmd, diff, extra_args));
+ } else if (!cmd->background && !cmd->trans->ctx.no_slowness_warning &&
+ diff >= (int)dict->warn_slow_msecs) {
+ e_warning(dict->conn.conn.event, "dict commit took %s: "
+ "%s (%u commands, first: %s)",
+ dict_warnings_sec(cmd, diff, extra_args),
+ cmd->query, cmd->trans->query_count,
+ cmd->trans->first_query);
+ }
+ client_dict_transaction_free(&cmd->trans);
+
+ dict_pre_api_callback(&dict->dict);
+ cmd->api_callback.commit(&result, cmd->api_callback.context);
+ dict_post_api_callback(&dict->dict);
+}
+
+
+static void
+client_dict_transaction_commit(struct dict_transaction_context *_ctx,
+ bool async,
+ dict_transaction_commit_callback_t *callback,
+ void *context)
+{
+ struct client_dict_transaction_context *ctx =
+ (struct client_dict_transaction_context *)_ctx;
+ struct client_dict *dict = (struct client_dict *)_ctx->dict;
+ struct client_dict_cmd *cmd;
+ const char *query;
+
+ DLLIST_REMOVE(&dict->transactions, ctx);
+
+ if (ctx->sent_begin && ctx->error == NULL) {
+ query = t_strdup_printf("%c%u", DICT_PROTOCOL_CMD_COMMIT, ctx->id);
+ cmd = client_dict_cmd_init(dict, query);
+ cmd->trans = ctx;
+
+ cmd->callback = client_dict_transaction_commit_callback;
+ cmd->api_callback.commit = callback;
+ cmd->api_callback.context = context;
+ if (callback == dict_transaction_commit_async_noop_callback)
+ cmd->background = TRUE;
+ if (client_dict_cmd_send(dict, &cmd, NULL)) {
+ if (!async)
+ client_dict_wait(_ctx->dict);
+ }
+ } else if (ctx->error != NULL) {
+ /* already failed */
+ struct dict_commit_result result = {
+ .ret = DICT_COMMIT_RET_FAILED, .error = ctx->error
+ };
+ callback(&result, context);
+ client_dict_transaction_free(&ctx);
+ } else {
+ /* nothing changed */
+ struct dict_commit_result result = {
+ .ret = DICT_COMMIT_RET_OK, .error = NULL
+ };
+ callback(&result, context);
+ client_dict_transaction_free(&ctx);
+ }
+
+ client_dict_add_timeout(dict);
+}
+
+static void
+client_dict_transaction_rollback(struct dict_transaction_context *_ctx)
+{
+ struct client_dict_transaction_context *ctx =
+ (struct client_dict_transaction_context *)_ctx;
+ struct client_dict *dict = (struct client_dict *)_ctx->dict;
+
+ if (ctx->sent_begin) {
+ const char *query;
+
+ query = t_strdup_printf("%c%u", DICT_PROTOCOL_CMD_ROLLBACK,
+ ctx->id);
+ client_dict_send_transaction_query(ctx, query);
+ }
+
+ DLLIST_REMOVE(&dict->transactions, ctx);
+ client_dict_transaction_free(&ctx);
+ client_dict_add_timeout(dict);
+}
+
+static void client_dict_set(struct dict_transaction_context *_ctx,
+ const char *key, const char *value)
+{
+ struct client_dict_transaction_context *ctx =
+ (struct client_dict_transaction_context *)_ctx;
+ const char *query;
+
+ query = t_strdup_printf("%c%u\t%s\t%s",
+ DICT_PROTOCOL_CMD_SET, ctx->id,
+ str_tabescape(key),
+ str_tabescape(value));
+ client_dict_send_transaction_query(ctx, query);
+}
+
+static void client_dict_unset(struct dict_transaction_context *_ctx,
+ const char *key)
+{
+ struct client_dict_transaction_context *ctx =
+ (struct client_dict_transaction_context *)_ctx;
+ const char *query;
+
+ query = t_strdup_printf("%c%u\t%s",
+ DICT_PROTOCOL_CMD_UNSET, ctx->id,
+ str_tabescape(key));
+ client_dict_send_transaction_query(ctx, query);
+}
+
+static void client_dict_atomic_inc(struct dict_transaction_context *_ctx,
+ const char *key, long long diff)
+{
+ struct client_dict_transaction_context *ctx =
+ (struct client_dict_transaction_context *)_ctx;
+ const char *query;
+
+ query = t_strdup_printf("%c%u\t%s\t%lld",
+ DICT_PROTOCOL_CMD_ATOMIC_INC,
+ ctx->id, str_tabescape(key), diff);
+ client_dict_send_transaction_query(ctx, query);
+}
+
+static void client_dict_set_timestamp(struct dict_transaction_context *_ctx,
+ const struct timespec *ts)
+{
+ struct client_dict_transaction_context *ctx =
+ (struct client_dict_transaction_context *)_ctx;
+ const char *query;
+
+ query = t_strdup_printf("%c%u\t%s\t%u",
+ DICT_PROTOCOL_CMD_TIMESTAMP,
+ ctx->id, dec2str(ts->tv_sec),
+ (unsigned int)ts->tv_nsec);
+ client_dict_send_transaction_query(ctx, query);
+}
+
+struct dict dict_driver_client = {
+ .name = "proxy",
+
+ {
+ .init = client_dict_init,
+ .deinit = client_dict_deinit,
+ .wait = client_dict_wait,
+ .lookup = client_dict_lookup,
+ .iterate_init = client_dict_iterate_init,
+ .iterate = client_dict_iterate,
+ .iterate_deinit = client_dict_iterate_deinit,
+ .transaction_init = client_dict_transaction_init,
+ .transaction_commit = client_dict_transaction_commit,
+ .transaction_rollback = client_dict_transaction_rollback,
+ .set = client_dict_set,
+ .unset = client_dict_unset,
+ .atomic_inc = client_dict_atomic_inc,
+ .lookup_async = client_dict_lookup_async,
+ .switch_ioloop = client_dict_switch_ioloop,
+ .set_timestamp = client_dict_set_timestamp,
+ }
+};
diff --git a/src/lib-dict/dict-client.h b/src/lib-dict/dict-client.h
new file mode 100644
index 0000000..5c14f73
--- /dev/null
+++ b/src/lib-dict/dict-client.h
@@ -0,0 +1,47 @@
+#ifndef DICT_CLIENT_H
+#define DICT_CLIENT_H
+
+#include "dict.h"
+
+#define DEFAULT_DICT_SERVER_SOCKET_FNAME "dict"
+
+#define DICT_CLIENT_PROTOCOL_MAJOR_VERSION 3
+#define DICT_CLIENT_PROTOCOL_MINOR_VERSION 2
+
+#define DICT_CLIENT_PROTOCOL_VERSION_MIN_MULTI_OK 2
+
+#define DICT_CLIENT_MAX_LINE_LENGTH (64*1024)
+
+enum dict_protocol_cmd {
+ /* <major-version> <minor-version> <value type> <user> <dict name> */
+ DICT_PROTOCOL_CMD_HELLO = 'H',
+
+ DICT_PROTOCOL_CMD_LOOKUP = 'L', /* <key> */
+ DICT_PROTOCOL_CMD_ITERATE = 'I', /* <flags> <path> */
+
+ DICT_PROTOCOL_CMD_BEGIN = 'B', /* <id> */
+ DICT_PROTOCOL_CMD_COMMIT = 'C', /* <id> */
+ DICT_PROTOCOL_CMD_COMMIT_ASYNC = 'D', /* <id> */
+ DICT_PROTOCOL_CMD_ROLLBACK = 'R', /* <id> */
+
+ DICT_PROTOCOL_CMD_SET = 'S', /* <id> <key> <value> */
+ DICT_PROTOCOL_CMD_UNSET = 'U', /* <id> <key> */
+ DICT_PROTOCOL_CMD_ATOMIC_INC = 'A', /* <id> <key> <diff> */
+ DICT_PROTOCOL_CMD_TIMESTAMP = 'T', /* <id> <secs> <nsecs> */
+};
+
+enum dict_protocol_reply {
+ DICT_PROTOCOL_REPLY_ERROR = -1,
+
+ DICT_PROTOCOL_REPLY_OK = 'O', /* <value> */
+ DICT_PROTOCOL_REPLY_MULTI_OK = 'M', /* protocol v2.2+ */
+ DICT_PROTOCOL_REPLY_NOTFOUND = 'N',
+ DICT_PROTOCOL_REPLY_FAIL = 'F',
+ DICT_PROTOCOL_REPLY_WRITE_UNCERTAIN = 'W',
+ DICT_PROTOCOL_REPLY_ASYNC_COMMIT = 'A',
+ DICT_PROTOCOL_REPLY_ITER_FINISHED = '\0',
+ DICT_PROTOCOL_REPLY_ASYNC_ID = '*',
+ DICT_PROTOCOL_REPLY_ASYNC_REPLY = '+',
+};
+
+#endif
diff --git a/src/lib-dict/dict-fail.c b/src/lib-dict/dict-fail.c
new file mode 100644
index 0000000..101750b
--- /dev/null
+++ b/src/lib-dict/dict-fail.c
@@ -0,0 +1,134 @@
+/* Copyright (c) 2017-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "dict.h"
+#include "dict-private.h"
+
+struct dict_iterate_context dict_iter_unsupported =
+{
+ .dict = &dict_driver_fail,
+};
+
+struct dict_transaction_context dict_transaction_unsupported =
+{
+ .dict = &dict_driver_fail,
+};
+
+static int dict_fail_init(struct dict *dict_driver ATTR_UNUSED,
+ const char *uri ATTR_UNUSED,
+ const struct dict_settings *set ATTR_UNUSED,
+ struct dict **dict_r ATTR_UNUSED, const char **error_r)
+{
+ *error_r = "Unsupported operation (dict does not support this feature)";
+ return -1;
+}
+
+static void dict_fail_deinit(struct dict *dict ATTR_UNUSED)
+{
+}
+
+static void dict_fail_wait(struct dict *dict ATTR_UNUSED)
+{
+}
+
+static int dict_fail_lookup(struct dict *dict ATTR_UNUSED,
+ const struct dict_op_settings *set ATTR_UNUSED,
+ pool_t pool ATTR_UNUSED,
+ const char *key ATTR_UNUSED, const char **value_r ATTR_UNUSED,
+ const char **error_r)
+{
+ *error_r = "Unsupported operation (dict does not support this feature)";
+ return -1;
+}
+
+static struct dict_iterate_context *
+dict_fail_iterate_init(struct dict *dict ATTR_UNUSED,
+ const struct dict_op_settings *set ATTR_UNUSED,
+ const char *path ATTR_UNUSED,
+ enum dict_iterate_flags flags ATTR_UNUSED)
+{
+ return &dict_iter_unsupported;
+}
+
+static bool dict_fail_iterate(struct dict_iterate_context *ctx ATTR_UNUSED,
+ const char **key_r ATTR_UNUSED,
+ const char *const **values_r ATTR_UNUSED)
+{
+ return FALSE;
+}
+
+static int dict_fail_iterate_deinit(struct dict_iterate_context *ctx ATTR_UNUSED,
+ const char **error_r)
+{
+ *error_r = "Unsupported operation (dict does not support this feature)";
+ return -1;
+}
+
+static struct dict_transaction_context *dict_fail_transaction_init(struct dict *dict ATTR_UNUSED)
+{
+ return &dict_transaction_unsupported;
+}
+
+static void dict_fail_transaction_commit(struct dict_transaction_context *ctx ATTR_UNUSED,
+ bool async ATTR_UNUSED,
+ dict_transaction_commit_callback_t *callback,
+ void *context)
+{
+ struct dict_commit_result res = {
+ .ret = DICT_COMMIT_RET_FAILED,
+ .error = "Unsupported operation (dict does not support this feature)"
+ };
+ if (callback != NULL)
+ callback(&res, context);
+}
+
+static void dict_fail_transaction_rollback(struct dict_transaction_context *ctx ATTR_UNUSED)
+{
+}
+
+static void dict_fail_set(struct dict_transaction_context *ctx ATTR_UNUSED,
+ const char *key ATTR_UNUSED, const char *value ATTR_UNUSED)
+{
+}
+
+static void dict_fail_unset(struct dict_transaction_context *ctx ATTR_UNUSED,
+ const char *key ATTR_UNUSED)
+{
+}
+
+static void dict_fail_atomic_inc(struct dict_transaction_context *ctx ATTR_UNUSED,
+ const char *key ATTR_UNUSED, long long diff ATTR_UNUSED)
+{
+}
+
+static bool dict_fail_switch_ioloop(struct dict *dict ATTR_UNUSED)
+{
+ return TRUE;
+}
+
+static void dict_fail_set_timestamp(struct dict_transaction_context *ctx ATTR_UNUSED,
+ const struct timespec *ts ATTR_UNUSED)
+{
+}
+
+struct dict dict_driver_fail = {
+ .name = "fail",
+ .v = {
+ .init = dict_fail_init,
+ .deinit = dict_fail_deinit,
+ .wait = dict_fail_wait,
+ .lookup = dict_fail_lookup,
+ .iterate_init = dict_fail_iterate_init,
+ .iterate = dict_fail_iterate,
+ .iterate_deinit = dict_fail_iterate_deinit,
+ .transaction_init = dict_fail_transaction_init,
+ .transaction_commit = dict_fail_transaction_commit,
+ .transaction_rollback = dict_fail_transaction_rollback,
+ .set = dict_fail_set,
+ .unset = dict_fail_unset,
+ .atomic_inc = dict_fail_atomic_inc,
+ .lookup_async = NULL,
+ .switch_ioloop = dict_fail_switch_ioloop,
+ .set_timestamp = dict_fail_set_timestamp
+ },
+};
diff --git a/src/lib-dict/dict-file.c b/src/lib-dict/dict-file.c
new file mode 100644
index 0000000..c9228a8
--- /dev/null
+++ b/src/lib-dict/dict-file.c
@@ -0,0 +1,709 @@
+/* Copyright (c) 2008-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "hash.h"
+#include "str.h"
+#include "strescape.h"
+#include "home-expand.h"
+#include "mkdir-parents.h"
+#include "eacces-error.h"
+#include "file-lock.h"
+#include "file-dotlock.h"
+#include "nfs-workarounds.h"
+#include "istream.h"
+#include "ostream.h"
+#include "dict-transaction-memory.h"
+#include "dict-private.h"
+
+#include <stdio.h>
+#include <unistd.h>
+#include <fcntl.h>
+#include <sys/stat.h>
+
+struct file_dict {
+ struct dict dict;
+ pool_t hash_pool;
+ enum file_lock_method lock_method;
+
+ char *path;
+ char *home_dir;
+ bool dict_path_checked;
+ HASH_TABLE(char *, char *) hash;
+ int fd;
+
+ bool refreshed;
+};
+
+struct file_dict_iterate_context {
+ struct dict_iterate_context ctx;
+ pool_t pool;
+
+ struct hash_iterate_context *iter;
+ const char *path;
+ size_t path_len;
+
+ enum dict_iterate_flags flags;
+ const char *values[2];
+ const char *error;
+};
+
+static struct dotlock_settings file_dict_dotlock_settings = {
+ .timeout = 60*2,
+ .stale_timeout = 60,
+ .use_io_notify = TRUE
+};
+
+static int
+file_dict_ensure_path_home_dir(struct file_dict *dict, const char *home_dir,
+ const char **error_r)
+{
+ if (null_strcmp(dict->home_dir, home_dir) == 0)
+ return 0;
+
+ if (dict->dict_path_checked) {
+ *error_r = t_strdup_printf("home_dir changed from %s to %s "
+ "(requested dict was: %s)", dict->home_dir,
+ home_dir, dict->path);
+ return -1;
+ }
+
+ char *_p = dict->path;
+ dict->path = i_strdup(home_expand_tilde(dict->path, home_dir));
+ dict->home_dir = i_strdup(home_dir);
+ i_free(_p);
+ dict->dict_path_checked = TRUE;
+ return 0;
+}
+
+static int
+file_dict_init(struct dict *driver, const char *uri,
+ const struct dict_settings *set ATTR_UNUSED,
+ struct dict **dict_r, const char **error_r)
+{
+ struct file_dict *dict;
+ const char *p, *path;
+
+ dict = i_new(struct file_dict, 1);
+ dict->lock_method = FILE_LOCK_METHOD_DOTLOCK;
+
+ p = strchr(uri, ':');
+ if (p == NULL) {
+ /* no parameters */
+ path = uri;
+ } else {
+ path = t_strdup_until(uri, p++);
+ if (strcmp(p, "lock=fcntl") == 0)
+ dict->lock_method = FILE_LOCK_METHOD_FCNTL;
+ else if (strcmp(p, "lock=flock") == 0)
+ dict->lock_method = FILE_LOCK_METHOD_FLOCK;
+ else {
+ *error_r = t_strdup_printf("Invalid parameter: %s", p+1);
+ i_free(dict);
+ return -1;
+ }
+ }
+
+ /* keep the path for now, later in dict operations check if home_dir
+ should be prepended. */
+ dict->path = i_strdup(path);
+
+ dict->dict = *driver;
+ dict->hash_pool = pool_alloconly_create("file dict", 1024);
+ hash_table_create(&dict->hash, dict->hash_pool, 0, str_hash, strcmp);
+ dict->fd = -1;
+ *dict_r = &dict->dict;
+ return 0;
+}
+
+static void file_dict_deinit(struct dict *_dict)
+{
+ struct file_dict *dict = (struct file_dict *)_dict;
+
+ i_close_fd_path(&dict->fd, dict->path);
+ hash_table_destroy(&dict->hash);
+ pool_unref(&dict->hash_pool);
+ i_free(dict->path);
+ i_free(dict->home_dir);
+ i_free(dict);
+}
+
+static bool file_dict_need_refresh(struct file_dict *dict)
+{
+ struct stat st1, st2;
+
+ if (dict->dict.iter_count > 0) {
+ /* Change nothing while there are iterators or they can crash
+ because the hash table content recreated. */
+ return FALSE;
+ }
+
+ if (dict->fd == -1)
+ return TRUE;
+
+ /* Disable NFS flushing for now since it can cause unnecessary
+ problems and there's no easy way for us to know here if
+ mail_nfs_storage=yes. In any case it's pretty much an unsupported
+ setting nowadays. */
+ /*nfs_flush_file_handle_cache(dict->path);*/
+ if (nfs_safe_stat(dict->path, &st1) < 0) {
+ e_error(dict->dict.event, "stat(%s) failed: %m", dict->path);
+ return FALSE;
+ }
+
+ if (fstat(dict->fd, &st2) < 0) {
+ if (errno != ESTALE)
+ e_error(dict->dict.event, "fstat(%s) failed: %m", dict->path);
+ return TRUE;
+ }
+ if (st1.st_ino != st2.st_ino ||
+ !CMP_DEV_T(st1.st_dev, st2.st_dev)) {
+ /* file changed */
+ return TRUE;
+ }
+ return FALSE;
+}
+
+static int file_dict_open_latest(struct file_dict *dict, const char **error_r)
+{
+ int open_type;
+
+ if (!file_dict_need_refresh(dict))
+ return 0;
+
+ i_close_fd_path(&dict->fd, dict->path);
+
+ open_type = dict->lock_method == FILE_LOCK_METHOD_DOTLOCK ?
+ O_RDONLY : O_RDWR;
+ dict->fd = open(dict->path, open_type);
+ if (dict->fd == -1) {
+ if (errno == ENOENT)
+ return 0;
+ if (errno == EACCES)
+ *error_r = eacces_error_get("open", dict->path);
+ else
+ *error_r = t_strdup_printf("open(%s) failed: %m", dict->path);
+ return -1;
+ }
+ dict->refreshed = FALSE;
+ return 1;
+}
+
+static int file_dict_refresh(struct file_dict *dict, const char **error_r)
+{
+ struct istream *input;
+ char *key, *value;
+
+ if (file_dict_open_latest(dict, error_r) < 0)
+ return -1;
+ if (dict->refreshed || dict->dict.iter_count > 0)
+ return 0;
+
+ hash_table_clear(dict->hash, TRUE);
+ p_clear(dict->hash_pool);
+
+ if (dict->fd != -1) {
+ input = i_stream_create_fd(dict->fd, SIZE_MAX);
+
+ while ((key = i_stream_read_next_line(input)) != NULL) {
+ /* strdup() before the second read */
+ key = str_tabunescape(p_strdup(dict->hash_pool, key));
+
+ if ((value = i_stream_read_next_line(input)) == NULL)
+ break;
+
+ value = str_tabunescape(p_strdup(dict->hash_pool, value));
+ hash_table_update(dict->hash, key, value);
+ }
+ i_stream_destroy(&input);
+ }
+ dict->refreshed = TRUE;
+ return 0;
+}
+
+static int file_dict_lookup(struct dict *_dict,
+ const struct dict_op_settings *set,
+ pool_t pool, const char *key,
+ const char **value_r, const char **error_r)
+{
+ struct file_dict *dict = (struct file_dict *)_dict;
+
+ if (file_dict_ensure_path_home_dir(dict, set->home_dir, error_r) < 0)
+ return -1;
+
+ if (file_dict_refresh(dict, error_r) < 0)
+ return -1;
+
+ *value_r = p_strdup(pool, hash_table_lookup(dict->hash, key));
+ return *value_r == NULL ? 0 : 1;
+}
+
+static struct dict_iterate_context *
+file_dict_iterate_init(struct dict *_dict,
+ const struct dict_op_settings *set ATTR_UNUSED,
+ const char *path, enum dict_iterate_flags flags)
+{
+ struct file_dict_iterate_context *ctx;
+ struct file_dict *dict = (struct file_dict *)_dict;
+ const char *error;
+ pool_t pool;
+
+ pool = pool_alloconly_create("file dict iterate", 256);
+ ctx = p_new(pool, struct file_dict_iterate_context, 1);
+ ctx->ctx.dict = _dict;
+ ctx->pool = pool;
+
+ ctx->path = p_strdup(pool, path);
+ ctx->path_len = strlen(path);
+ ctx->flags = flags;
+
+ if (file_dict_ensure_path_home_dir(dict, set->home_dir, &error) < 0 ||
+ file_dict_refresh(dict, &error) < 0)
+ ctx->error = p_strdup(pool, error);
+
+ ctx->iter = hash_table_iterate_init(dict->hash);
+ return &ctx->ctx;
+}
+
+static bool
+file_dict_iterate_key_matches(struct file_dict_iterate_context *ctx,
+ const char *key)
+{
+ if (strncmp(ctx->path, key, ctx->path_len) == 0)
+ return TRUE;
+ return FALSE;
+}
+
+
+static bool file_dict_iterate(struct dict_iterate_context *_ctx,
+ const char **key_r, const char *const **values_r)
+{
+ struct file_dict_iterate_context *ctx =
+ (struct file_dict_iterate_context *)_ctx;
+ char *key, *value;
+
+ while (hash_table_iterate(ctx->iter,
+ ((struct file_dict *)_ctx->dict)->hash,
+ &key, &value)) {
+ if (!file_dict_iterate_key_matches(ctx, key))
+ continue;
+
+ if ((ctx->flags & DICT_ITERATE_FLAG_RECURSE) != 0) {
+ /* match everything */
+ } else if ((ctx->flags & DICT_ITERATE_FLAG_EXACT_KEY) != 0) {
+ if (key[ctx->path_len] != '\0')
+ continue;
+ } else {
+ if (strchr(key + ctx->path_len, '/') != NULL)
+ continue;
+ }
+
+ *key_r = key;
+ ctx->values[0] = value;
+ *values_r = ctx->values;
+ return TRUE;
+ }
+ return FALSE;
+}
+
+static int file_dict_iterate_deinit(struct dict_iterate_context *_ctx,
+ const char **error_r)
+{
+ struct file_dict_iterate_context *ctx =
+ (struct file_dict_iterate_context *)_ctx;
+ int ret = ctx->error != NULL ? -1 : 0;
+
+ *error_r = t_strdup(ctx->error);
+ hash_table_iterate_deinit(&ctx->iter);
+ pool_unref(&ctx->pool);
+ return ret;
+}
+
+static struct dict_transaction_context *
+file_dict_transaction_init(struct dict *_dict)
+{
+ struct dict_transaction_memory_context *ctx;
+ pool_t pool;
+
+ pool = pool_alloconly_create("file dict transaction", 2048);
+ ctx = p_new(pool, struct dict_transaction_memory_context, 1);
+ dict_transaction_memory_init(ctx, _dict, pool);
+ return &ctx->ctx;
+}
+
+static void file_dict_apply_changes(struct dict_transaction_memory_context *ctx,
+ bool *atomic_inc_not_found_r)
+{
+ struct file_dict *dict = (struct file_dict *)ctx->ctx.dict;
+ const char *tmp;
+ char *key, *value, *old_value;
+ char *orig_key, *orig_value;
+ const struct dict_transaction_memory_change *change;
+ size_t new_len;
+ long long diff;
+
+ array_foreach(&ctx->changes, change) {
+ if (hash_table_lookup_full(dict->hash, change->key,
+ &orig_key, &orig_value)) {
+ key = orig_key;
+ old_value = orig_value;
+ } else {
+ key = NULL;
+ old_value = NULL;
+ }
+ value = NULL;
+
+ switch (change->type) {
+ case DICT_CHANGE_TYPE_INC:
+ if (old_value == NULL) {
+ *atomic_inc_not_found_r = TRUE;
+ break;
+ }
+ if (str_to_llong(old_value, &diff) < 0)
+ i_unreached();
+ diff += change->value.diff;
+ tmp = t_strdup_printf("%lld", diff);
+ new_len = strlen(tmp);
+ if (old_value == NULL || new_len > strlen(old_value))
+ value = p_strdup(dict->hash_pool, tmp);
+ else {
+ memcpy(old_value, tmp, new_len + 1);
+ value = old_value;
+ }
+ /* fall through */
+ case DICT_CHANGE_TYPE_SET:
+ if (key == NULL)
+ key = p_strdup(dict->hash_pool, change->key);
+ if (value == NULL) {
+ value = p_strdup(dict->hash_pool,
+ change->value.str);
+ }
+ hash_table_update(dict->hash, key, value);
+ break;
+ case DICT_CHANGE_TYPE_UNSET:
+ if (old_value != NULL)
+ hash_table_remove(dict->hash, key);
+ break;
+ }
+ }
+}
+
+static int
+fd_copy_stat_permissions(const struct stat *src_st,
+ int dest_fd, const char *dest_path,
+ const char **error_r)
+{
+ struct stat dest_st;
+
+ if (fstat(dest_fd, &dest_st) < 0) {
+ *error_r = t_strdup_printf("fstat(%s) failed: %m", dest_path);
+ return -1;
+ }
+
+ if (src_st->st_gid != dest_st.st_gid &&
+ ((src_st->st_mode & 0070) >> 3 != (src_st->st_mode & 0007))) {
+ /* group has different permissions from world.
+ preserve the group. */
+ if (fchown(dest_fd, (uid_t)-1, src_st->st_gid) < 0) {
+ *error_r = t_strdup_printf("fchown(%s, -1, %s) failed: %m",
+ dest_path, dec2str(src_st->st_gid));
+ return -1;
+ }
+ }
+
+ if ((src_st->st_mode & 07777) != (dest_st.st_mode & 07777)) {
+ if (fchmod(dest_fd, src_st->st_mode & 07777) < 0) {
+ *error_r = t_strdup_printf("fchmod(%s, %o) failed: %m",
+ dest_path, (int)(src_st->st_mode & 0777));
+ return -1;
+ }
+ }
+ return 0;
+}
+
+static int fd_copy_permissions(int src_fd, const char *src_path,
+ int dest_fd, const char *dest_path,
+ const char **error_r)
+{
+ struct stat src_st;
+
+ if (fstat(src_fd, &src_st) < 0) {
+ *error_r = t_strdup_printf("fstat(%s) failed: %m", src_path);
+ return -1;
+ }
+ return fd_copy_stat_permissions(&src_st, dest_fd, dest_path, error_r);
+}
+
+static int
+fd_copy_parent_dir_permissions(const char *src_path, int dest_fd,
+ const char *dest_path, const char **error_r)
+{
+ struct stat src_st;
+ const char *src_dir, *p;
+
+ p = strrchr(src_path, '/');
+ if (p == NULL)
+ src_dir = ".";
+ else
+ src_dir = t_strdup_until(src_path, p);
+ if (stat(src_dir, &src_st) < 0) {
+ *error_r = t_strdup_printf("stat(%s) failed: %m", src_dir);
+ return -1;
+ }
+ src_st.st_mode &= 0666;
+ return fd_copy_stat_permissions(&src_st, dest_fd, dest_path, error_r);
+}
+
+static int file_dict_mkdir(struct file_dict *dict, const char **error_r)
+{
+ const char *path, *p, *root;
+ struct stat st;
+ mode_t mode = 0700;
+
+ p = strrchr(dict->path, '/');
+ if (p == NULL)
+ return 0;
+ path = t_strdup_until(dict->path, p);
+
+ if (stat_first_parent(path, &root, &st) < 0) {
+ if (errno == EACCES)
+ *error_r = eacces_error_get("stat", root);
+ else
+ *error_r = t_strdup_printf("stat(%s) failed: %m", root);
+ return -1;
+ }
+ if ((st.st_mode & S_ISGID) != 0) {
+ /* preserve parent's permissions when it has setgid bit */
+ mode = st.st_mode;
+ }
+
+ if (mkdir_parents(path, mode) < 0 && errno != EEXIST) {
+ if (errno == EACCES)
+ *error_r = eacces_error_get("mkdir_parents", path);
+ else
+ *error_r = t_strdup_printf("mkdir_parents(%s) failed: %m", path);
+ return -1;
+ }
+ return 0;
+}
+
+static int
+file_dict_lock(struct file_dict *dict, struct file_lock **lock_r,
+ const char **error_r)
+{
+ int ret;
+ const char *error;
+
+ if (file_dict_open_latest(dict, error_r) < 0)
+ return -1;
+
+ if (dict->fd == -1) {
+ /* quota file doesn't exist yet, we need to create it */
+ dict->fd = open(dict->path, O_CREAT | O_RDWR, 0600);
+ if (dict->fd == -1 && errno == ENOENT) {
+ if (file_dict_mkdir(dict, error_r) < 0)
+ return -1;
+ dict->fd = open(dict->path, O_CREAT | O_RDWR, 0600);
+ }
+ if (dict->fd == -1) {
+ if (errno == EACCES)
+ *error_r = eacces_error_get("creat", dict->path);
+ else {
+ *error_r = t_strdup_printf(
+ "creat(%s) failed: %m", dict->path);
+ }
+ return -1;
+ }
+ if (fd_copy_parent_dir_permissions(dict->path, dict->fd,
+ dict->path, &error) < 0)
+ e_error(dict->dict.event, "%s", error);
+ }
+
+ *lock_r = NULL;
+ struct file_lock_settings lock_set = {
+ .lock_method = dict->lock_method,
+ };
+ do {
+ file_lock_free(lock_r);
+ if (file_wait_lock(dict->fd, dict->path, F_WRLCK, &lock_set,
+ file_dict_dotlock_settings.timeout,
+ lock_r, &error) <= 0) {
+ *error_r = t_strdup_printf(
+ "file_wait_lock(%s) failed: %s",
+ dict->path, error);
+ return -1;
+ }
+ /* check again if we need to reopen the file because it was
+ just replaced */
+ } while ((ret = file_dict_open_latest(dict, error_r)) > 0);
+
+ return ret < 0 ? -1 : 0;
+}
+
+static int
+file_dict_write_changes(struct dict_transaction_memory_context *ctx,
+ bool *atomic_inc_not_found_r, const char **error_r)
+{
+ struct file_dict *dict = (struct file_dict *)ctx->ctx.dict;
+ struct dotlock *dotlock = NULL;
+ struct file_lock *lock = NULL;
+ const char *temp_path = NULL;
+ const char *error;
+ struct hash_iterate_context *iter;
+ struct ostream *output;
+ char *key, *value;
+ string_t *str;
+ int fd = -1;
+
+ *atomic_inc_not_found_r = FALSE;
+
+ if (file_dict_ensure_path_home_dir(dict, ctx->ctx.set.home_dir, error_r) < 0)
+ return -1;
+
+ switch (dict->lock_method) {
+ case FILE_LOCK_METHOD_FCNTL:
+ case FILE_LOCK_METHOD_FLOCK:
+ if (file_dict_lock(dict, &lock, error_r) < 0)
+ return -1;
+ temp_path = t_strdup_printf("%s.tmp", dict->path);
+ fd = creat(temp_path, 0600);
+ if (fd == -1) {
+ *error_r = t_strdup_printf(
+ "dict-file: creat(%s) failed: %m", temp_path);
+ file_unlock(&lock);
+ return -1;
+ }
+ break;
+ case FILE_LOCK_METHOD_DOTLOCK:
+ fd = file_dotlock_open(&file_dict_dotlock_settings, dict->path, 0,
+ &dotlock);
+ if (fd == -1 && errno == ENOENT) {
+ if (file_dict_mkdir(dict, error_r) < 0)
+ return -1;
+ fd = file_dotlock_open(&file_dict_dotlock_settings,
+ dict->path, 0, &dotlock);
+ }
+ if (fd == -1) {
+ *error_r = t_strdup_printf(
+ "dict-file: file_dotlock_open(%s) failed: %m",
+ dict->path);
+ return -1;
+ }
+ temp_path = file_dotlock_get_lock_path(dotlock);
+ break;
+ }
+
+ /* refresh once more now that we're locked */
+ if (file_dict_refresh(dict, error_r) < 0) {
+ if (dotlock != NULL)
+ file_dotlock_delete(&dotlock);
+ else {
+ i_close_fd(&fd);
+ file_unlock(&lock);
+ }
+ return -1;
+ }
+ if (dict->fd != -1) {
+ /* preserve the permissions */
+ if (fd_copy_permissions(dict->fd, dict->path, fd, temp_path, &error) < 0)
+ e_error(ctx->ctx.event, "%s", error);
+ } else {
+ /* get initial permissions from parent directory */
+ if (fd_copy_parent_dir_permissions(dict->path, fd, temp_path, &error) < 0)
+ e_error(ctx->ctx.event, "%s", error);
+ }
+ file_dict_apply_changes(ctx, atomic_inc_not_found_r);
+
+ output = o_stream_create_fd(fd, 0);
+ o_stream_cork(output);
+ iter = hash_table_iterate_init(dict->hash);
+ str = t_str_new(256);
+ while (hash_table_iterate(iter, dict->hash, &key, &value)) {
+ str_truncate(str, 0);
+ str_append_tabescaped(str, key);
+ str_append_c(str, '\n');
+ str_append_tabescaped(str, value);
+ str_append_c(str, '\n');
+ o_stream_nsend(output, str_data(str), str_len(str));
+ }
+ hash_table_iterate_deinit(&iter);
+
+ if (o_stream_finish(output) <= 0) {
+ *error_r = t_strdup_printf("write(%s) failed: %s", temp_path,
+ o_stream_get_error(output));
+ o_stream_destroy(&output);
+ if (dotlock != NULL)
+ file_dotlock_delete(&dotlock);
+ else {
+ i_close_fd(&fd);
+ file_unlock(&lock);
+ }
+ return -1;
+ }
+ o_stream_destroy(&output);
+
+ if (dotlock != NULL) {
+ if (file_dotlock_replace(&dotlock,
+ DOTLOCK_REPLACE_FLAG_DONT_CLOSE_FD) < 0) {
+ *error_r = t_strdup_printf("file_dotlock_replace() failed: %m");
+ i_close_fd(&fd);
+ return -1;
+ }
+ } else {
+ if (rename(temp_path, dict->path) < 0) {
+ *error_r = t_strdup_printf("rename(%s, %s) failed: %m",
+ temp_path, dict->path);
+ file_unlock(&lock);
+ i_close_fd(&fd);
+ return -1;
+ }
+ /* dict->fd is locked, not the new fd. We're closing dict->fd
+ so we can just free the lock struct. */
+ file_lock_free(&lock);
+ }
+
+ i_close_fd(&dict->fd);
+ dict->fd = fd;
+ return 0;
+}
+
+static void
+file_dict_transaction_commit(struct dict_transaction_context *_ctx,
+ bool async ATTR_UNUSED,
+ dict_transaction_commit_callback_t *callback,
+ void *context)
+{
+ struct dict_transaction_memory_context *ctx =
+ (struct dict_transaction_memory_context *)_ctx;
+ struct dict_commit_result result;
+ bool atomic_inc_not_found;
+
+ i_zero(&result);
+ if (file_dict_write_changes(ctx, &atomic_inc_not_found, &result.error) < 0)
+ result.ret = DICT_COMMIT_RET_FAILED;
+ else if (atomic_inc_not_found)
+ result.ret = DICT_COMMIT_RET_NOTFOUND;
+ else
+ result.ret = DICT_COMMIT_RET_OK;
+ pool_unref(&ctx->pool);
+
+ callback(&result, context);
+}
+
+struct dict dict_driver_file = {
+ .name = "file",
+ {
+ .init = file_dict_init,
+ .deinit = file_dict_deinit,
+ .lookup = file_dict_lookup,
+ .iterate_init = file_dict_iterate_init,
+ .iterate = file_dict_iterate,
+ .iterate_deinit = file_dict_iterate_deinit,
+ .transaction_init = file_dict_transaction_init,
+ .transaction_commit = file_dict_transaction_commit,
+ .transaction_rollback = dict_transaction_memory_rollback,
+ .set = dict_transaction_memory_set,
+ .unset = dict_transaction_memory_unset,
+ .atomic_inc = dict_transaction_memory_atomic_inc,
+ }
+};
diff --git a/src/lib-dict/dict-iter-lua.c b/src/lib-dict/dict-iter-lua.c
new file mode 100644
index 0000000..780de03
--- /dev/null
+++ b/src/lib-dict/dict-iter-lua.c
@@ -0,0 +1,193 @@
+/* Copyright (c) 2021 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "dict.h"
+#include "dlua-script-private.h"
+#include "dict-lua-private.h"
+#include "dlua-wrapper.h"
+
+struct lua_dict_iter {
+ pool_t pool;
+ struct dict_iterate_context *iter;
+ ARRAY(int) refs;
+ int error_ref;
+
+ lua_State *L;
+ bool yielded:1;
+};
+
+static void lua_dict_iter_unref(struct lua_dict_iter *iter)
+{
+ const char *error;
+
+ /* deinit iteration if it hasn't been done yet */
+ if (dict_iterate_deinit(&iter->iter, &error) < 0) {
+ e_error(dlua_script_from_state(iter->L)->event,
+ "Dict iteration failed: %s", error);
+ }
+
+ pool_unref(&iter->pool);
+}
+
+DLUA_WRAP_C_DATA(dict_iter, struct lua_dict_iter, lua_dict_iter_unref, NULL);
+
+static int lua_dict_iterate_step(lua_State *L);
+
+/* resume after a yield */
+static int lua_dict_iterate_step_continue(lua_State *L,
+ int status ATTR_UNUSED,
+ lua_KContext ctx ATTR_UNUSED)
+{
+ return lua_dict_iterate_step(L);
+}
+
+static void lua_dict_iterate_more(struct lua_dict_iter *iter);
+
+/*
+ * Iteration step function
+ *
+ * Takes two args (a userdata state, and previous value) and returns the
+ * next value.
+ */
+static int lua_dict_iterate_step(lua_State *L)
+{
+ struct lua_dict_iter *iter;
+ const int *refs;
+ unsigned nrefs;
+
+ DLUA_REQUIRE_ARGS(L, 2);
+
+ iter = xlua_dict_iter_getptr(L, 1, NULL);
+ iter->yielded = FALSE;
+
+ lua_dict_iterate_more(iter);
+
+ if (iter->iter != NULL) {
+ /* iteration didn't end yet - yield */
+ return lua_dict_iterate_step_continue(L,
+ lua_yieldk(L, 0, 0, lua_dict_iterate_step_continue), 0);
+ }
+
+ /* dict iteration ended - return first key-value pair */
+ refs = array_get(&iter->refs, &nrefs);
+ i_assert(nrefs % 2 == 0);
+
+ if (nrefs == 0) {
+ if (iter->error_ref != 0) {
+ /* dict iteration generated an error - raise it now */
+ lua_rawgeti(L, LUA_REGISTRYINDEX, iter->error_ref);
+ luaL_unref(L, LUA_REGISTRYINDEX, iter->error_ref);
+ return lua_error(L);
+ }
+
+ return 0; /* return nil */
+ }
+
+ /* get the key & value from the registry */
+ lua_rawgeti(L, LUA_REGISTRYINDEX, refs[0]);
+ lua_rawgeti(L, LUA_REGISTRYINDEX, refs[1]);
+ luaL_unref(L, LUA_REGISTRYINDEX, refs[0]);
+ luaL_unref(L, LUA_REGISTRYINDEX, refs[1]);
+
+ array_delete(&iter->refs, 0, 2);
+
+ return 2;
+}
+
+static void lua_dict_iterate_more(struct lua_dict_iter *iter)
+{
+ const char *key, *const *values;
+ lua_State *L = iter->L;
+ const char *error;
+
+ if (iter->iter == NULL)
+ return; /* done iterating the dict */
+
+ while (dict_iterate_values(iter->iter, &key, &values)) {
+ int ref;
+
+ /* stash key */
+ lua_pushstring(L, key);
+ ref = luaL_ref(L, LUA_REGISTRYINDEX);
+ array_push_back(&iter->refs, &ref);
+
+ /* stash values */
+ lua_newtable(L);
+ for (unsigned int i = 0; values[i] != NULL; i++) {
+ lua_pushstring(L, values[i]);
+ lua_seti(L, -2, i + 1);
+ }
+ ref = luaL_ref(L, LUA_REGISTRYINDEX);
+ array_push_back(&iter->refs, &ref);
+ }
+
+ if (dict_iterate_has_more(iter->iter))
+ return;
+
+ if (dict_iterate_deinit(&iter->iter, &error) < 0) {
+ lua_pushstring(L, error);
+ iter->error_ref = luaL_ref(L, LUA_REGISTRYINDEX);
+ }
+}
+
+/* dict iter callback */
+static void lua_dict_iterate_callback(struct lua_dict_iter *iter)
+{
+ if (iter->yielded)
+ return;
+ iter->yielded = TRUE;
+ dlua_pcall_yieldable_resume(iter->L, 1);
+}
+
+/*
+ * Iterate a dict at key [-(3|4),+2,e]
+ *
+ * Args:
+ * 1) userdata: sturct dict *dict
+ * 2) string: key
+ * 3) integer: flags
+ * 4*) string: username
+ *
+ * Returns:
+ * Returns a iteration step function and dict iter userdata.
+ * Username will be NULL if not provided in args.
+ */
+int lua_dict_iterate(lua_State *L)
+{
+ enum dict_iterate_flags flags;
+ struct lua_dict_iter *iter;
+ struct dict *dict;
+ const char *path, *username = NULL;
+ pool_t pool;
+
+ DLUA_REQUIRE_ARGS_IN(L, 3, 4);
+
+ dict = dlua_check_dict(L, 1);
+ path = luaL_checkstring(L, 2);
+ flags = luaL_checkinteger(L, 3);
+ if (lua_gettop(L) >= 4)
+ username = luaL_checkstring(L, 4);
+ lua_dict_check_key_prefix(L, path, username);
+
+ struct dict_op_settings set = {
+ .username = username,
+ };
+
+ /* set up iteration */
+ pool = pool_alloconly_create("lua dict iter", 128);
+ iter = p_new(pool, struct lua_dict_iter, 1);
+ iter->pool = pool;
+ iter->iter = dict_iterate_init(dict, &set, path, flags |
+ DICT_ITERATE_FLAG_ASYNC);
+ p_array_init(&iter->refs, iter->pool, 32);
+ iter->L = L;
+
+ dict_iterate_set_async_callback(iter->iter,
+ lua_dict_iterate_callback, iter);
+
+ /* push return values: func, state */
+ lua_pushcfunction(L, lua_dict_iterate_step);
+ xlua_pushdict_iter(L, iter, FALSE);
+ return 2;
+}
diff --git a/src/lib-dict/dict-lua-private.h b/src/lib-dict/dict-lua-private.h
new file mode 100644
index 0000000..f9f9943
--- /dev/null
+++ b/src/lib-dict/dict-lua-private.h
@@ -0,0 +1,9 @@
+#ifndef DICT_LUA_PRIVATE_H
+#define DICT_LUA_PRIVATE_H
+
+#include "dict-lua.h"
+
+int lua_dict_iterate(lua_State *l);
+int lua_dict_transaction_begin(lua_State *l);
+
+#endif
diff --git a/src/lib-dict/dict-lua.c b/src/lib-dict/dict-lua.c
new file mode 100644
index 0000000..d5de534
--- /dev/null
+++ b/src/lib-dict/dict-lua.c
@@ -0,0 +1,117 @@
+/* Copyright (c) 2021 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "dict.h"
+#include "dlua-script-private.h"
+#include "dict-lua-private.h"
+#include "dlua-wrapper.h"
+
+static int lua_dict_lookup(lua_State *);
+
+static luaL_Reg lua_dict_methods[] = {
+ { "lookup", lua_dict_lookup },
+ { "iterate", lua_dict_iterate },
+ { "transaction_begin", lua_dict_transaction_begin },
+ { NULL, NULL },
+};
+
+/* no actual ref counting */
+static void lua_dict_unref(struct dict *dict ATTR_UNUSED)
+{
+}
+
+DLUA_WRAP_C_DATA(dict, struct dict, lua_dict_unref, lua_dict_methods);
+
+static int lua_dict_async_continue(lua_State *L,
+ int status ATTR_UNUSED,
+ lua_KContext ctx ATTR_UNUSED)
+{
+ /*
+ * lua_dict_*_callback() already pushed the result table/nil or error
+ * string. We simply need to return/error out.
+ */
+
+ if (lua_istable(L, -1) || lua_isnil(L, -1))
+ return 1;
+ else
+ return lua_error(L);
+}
+
+static void lua_dict_lookup_callback(const struct dict_lookup_result *result,
+ lua_State *L)
+{
+ if (result->ret < 0) {
+ lua_pushstring(L, result->error);
+ } else if (result->ret == 0) {
+ lua_pushnil(L);
+ } else {
+ unsigned int i;
+
+ lua_newtable(L);
+
+ for (i = 0; i < str_array_length(result->values); i++) {
+ lua_pushstring(L, result->values[i]);
+ lua_seti(L, -2, i + 1);
+ }
+ }
+
+ dlua_pcall_yieldable_resume(L, 1);
+}
+
+void lua_dict_check_key_prefix(lua_State *L, const char *key,
+ const char *username)
+{
+ if (str_begins(key, DICT_PATH_SHARED))
+ ;
+ else if (str_begins(key, DICT_PATH_PRIVATE)) {
+ if (username == NULL || username[0] == '\0')
+ luaL_error(L, DICT_PATH_PRIVATE" dict key prefix requires username");
+ } else {
+ luaL_error(L, "Invalid dict key prefix");
+ }
+}
+
+/*
+ * Lookup a key in dict [-(2|3),+1,e]
+ *
+ * Args:
+ * 1) userdata: struct dict *dict
+ * 2) string: key
+ * 3*) string: username
+ *
+ * Returns:
+ * If key is found, returns a table with values. If key is not found,
+ * returns nil.
+ * Username will be NULL if not provided in args.
+ */
+static int lua_dict_lookup(lua_State *L)
+{
+ struct dict *dict;
+ const char *key, *username = NULL;
+
+ DLUA_REQUIRE_ARGS_IN(L, 2, 3);
+
+ dict = xlua_dict_getptr(L, 1, NULL);
+ key = luaL_checkstring(L, 2);
+ if (lua_gettop(L) >= 3)
+ username = luaL_checkstring(L, 3);
+ lua_dict_check_key_prefix(L, key, username);
+
+ struct dict_op_settings set = {
+ .username = username,
+ };
+ dict_lookup_async(dict, &set, key, lua_dict_lookup_callback, L);
+
+ return lua_dict_async_continue(L,
+ lua_yieldk(L, 0, 0, lua_dict_async_continue), 0);
+}
+
+void dlua_push_dict(lua_State *L, struct dict *dict)
+{
+ xlua_pushdict(L, dict, FALSE);
+}
+
+struct dict *dlua_check_dict(lua_State *L, int idx)
+{
+ return xlua_dict_getptr(L, idx, NULL);
+}
diff --git a/src/lib-dict/dict-lua.h b/src/lib-dict/dict-lua.h
new file mode 100644
index 0000000..bf4255c
--- /dev/null
+++ b/src/lib-dict/dict-lua.h
@@ -0,0 +1,18 @@
+#ifndef DICT_LUA_H
+#define DICT_LUA_H
+
+#ifdef DLUA_WITH_YIELDS
+/*
+ * Internally, the dict methods yield via lua_yieldk() as implemented in Lua
+ * 5.3 and newer.
+ */
+
+void lua_dict_check_key_prefix(lua_State *L, const char *key,
+ const char *username);
+
+void dlua_push_dict(lua_State *L, struct dict *dict);
+struct dict *dlua_check_dict(lua_State *L, int idx);
+
+#endif
+
+#endif
diff --git a/src/lib-dict/dict-memcached-ascii.c b/src/lib-dict/dict-memcached-ascii.c
new file mode 100644
index 0000000..6ae5443
--- /dev/null
+++ b/src/lib-dict/dict-memcached-ascii.c
@@ -0,0 +1,685 @@
+/* Copyright (c) 2008-2018 Dovecot authors, see the included COPYING memcached_ascii */
+
+#include "lib.h"
+#include "array.h"
+#include "str.h"
+#include "istream.h"
+#include "ostream.h"
+#include "connection.h"
+#include "dict-transaction-memory.h"
+#include "dict-private.h"
+
+#define MEMCACHED_DEFAULT_PORT 11211
+#define MEMCACHED_DEFAULT_LOOKUP_TIMEOUT_MSECS (1000*30)
+#define DICT_USERNAME_SEPARATOR '/'
+
+enum memcached_ascii_input_state {
+ /* GET: expecting VALUE or END */
+ MEMCACHED_INPUT_STATE_GET,
+ /* SET: expecting STORED / NOT_STORED */
+ MEMCACHED_INPUT_STATE_STORED,
+ /* DELETE: expecting DELETED */
+ MEMCACHED_INPUT_STATE_DELETED,
+ /* (INCR+ADD)/DECR: expecting number / NOT_FOUND / STORED / NOT_STORED */
+ MEMCACHED_INPUT_STATE_INCRDECR
+};
+
+struct memcached_ascii_connection {
+ struct connection conn;
+ struct memcached_ascii_dict *dict;
+
+ string_t *reply_str;
+ unsigned int reply_bytes_left;
+ bool value_received;
+ bool value_waiting_end;
+};
+
+struct memcached_ascii_dict_reply {
+ unsigned int reply_count;
+ dict_transaction_commit_callback_t *callback;
+ void *context;
+};
+
+struct dict_memcached_ascii_commit_ctx {
+ struct memcached_ascii_dict *dict;
+ struct dict_transaction_memory_context *memctx;
+ string_t *str;
+
+ dict_transaction_commit_callback_t *callback;
+ void *context;
+};
+
+struct memcached_ascii_dict {
+ struct dict dict;
+ struct ip_addr ip;
+ char *key_prefix;
+ in_port_t port;
+ unsigned int timeout_msecs;
+
+ struct timeout *to;
+ struct memcached_ascii_connection conn;
+
+ ARRAY(enum memcached_ascii_input_state) input_states;
+ ARRAY(struct memcached_ascii_dict_reply) replies;
+};
+
+static struct connection_list *memcached_ascii_connections;
+
+static void
+memcached_ascii_callback(struct memcached_ascii_dict *dict,
+ const struct memcached_ascii_dict_reply *reply,
+ const struct dict_commit_result *result)
+{
+ if (reply->callback != NULL) {
+ if (dict->dict.prev_ioloop != NULL) {
+ /* Don't let callback see that we've created our
+ internal ioloop in case it wants to add some ios
+ or timeouts. */
+ current_ioloop = dict->dict.prev_ioloop;
+ }
+ reply->callback(result, reply->context);
+ if (dict->dict.prev_ioloop != NULL)
+ current_ioloop = dict->dict.ioloop;
+ }
+}
+
+static void
+memcached_ascii_disconnected(struct memcached_ascii_connection *conn,
+ const char *reason)
+{
+ const struct dict_commit_result result = {
+ DICT_COMMIT_RET_FAILED, reason
+ };
+ const struct memcached_ascii_dict_reply *reply;
+
+ connection_disconnect(&conn->conn);
+ if (conn->dict->dict.ioloop != NULL)
+ io_loop_stop(conn->dict->dict.ioloop);
+
+ array_foreach(&conn->dict->replies, reply)
+ memcached_ascii_callback(conn->dict, reply, &result);
+ array_clear(&conn->dict->replies);
+ array_clear(&conn->dict->input_states);
+ conn->reply_bytes_left = 0;
+}
+
+static void memcached_ascii_conn_destroy(struct connection *_conn)
+{
+ struct memcached_ascii_connection *conn =
+ (struct memcached_ascii_connection *)_conn;
+
+ memcached_ascii_disconnected(conn, connection_disconnect_reason(_conn));
+}
+
+static bool memcached_ascii_input_value(struct memcached_ascii_connection *conn)
+{
+ const unsigned char *data;
+ size_t size;
+
+ data = i_stream_get_data(conn->conn.input, &size);
+ if (size > conn->reply_bytes_left)
+ size = conn->reply_bytes_left;
+ conn->reply_bytes_left -= size;
+
+ str_append_data(conn->reply_str, data, size);
+ i_stream_skip(conn->conn.input, size);
+ if (conn->reply_bytes_left > 0)
+ return FALSE;
+
+ /* finished. drop the trailing CRLF */
+ str_truncate(conn->reply_str, str_len(conn->reply_str)-2);
+ conn->value_received = TRUE;
+ return TRUE;
+}
+
+static int memcached_ascii_input_reply_read(struct memcached_ascii_dict *dict,
+ const char **error_r)
+{
+ struct memcached_ascii_connection *conn = &dict->conn;
+ const enum memcached_ascii_input_state *states;
+ const char *line, *p;
+ unsigned int count;
+ long long num;
+
+ if (conn->reply_bytes_left > 0) {
+ /* continue reading bulk reply */
+ if (!memcached_ascii_input_value(conn))
+ return 0;
+ conn->value_waiting_end = TRUE;
+ } else if (conn->value_waiting_end) {
+ conn->value_waiting_end = FALSE;
+ } else {
+ str_truncate(conn->reply_str, 0);
+ conn->value_received = FALSE;
+ }
+
+ line = i_stream_next_line(conn->conn.input);
+ if (line == NULL)
+ return 0;
+
+ states = array_get(&dict->input_states, &count);
+ if (count == 0) {
+ *error_r = t_strdup_printf(
+ "memcached_ascii: Unexpected input (expected nothing): %s", line);
+ return -1;
+ }
+ switch (states[0]) {
+ case MEMCACHED_INPUT_STATE_GET:
+ /* VALUE <key> <flags> <bytes>
+ END */
+ if (str_begins(line, "VALUE ")) {
+ p = strrchr(line, ' ');
+ if (str_to_uint(p+1, &conn->reply_bytes_left) < 0)
+ break;
+ conn->reply_bytes_left += 2; /* CRLF */
+ return memcached_ascii_input_reply_read(dict, error_r);
+ } else if (strcmp(line, "END") == 0)
+ return 1;
+ break;
+ case MEMCACHED_INPUT_STATE_STORED:
+ if (strcmp(line, "STORED") != 0 &&
+ strcmp(line, "NOT_STORED") != 0)
+ break;
+ return 1;
+ case MEMCACHED_INPUT_STATE_DELETED:
+ if (strcmp(line, "DELETED") != 0)
+ break;
+ return 1;
+ case MEMCACHED_INPUT_STATE_INCRDECR:
+ if (strcmp(line, "NOT_FOUND") != 0 &&
+ strcmp(line, "STORED") != 0 &&
+ strcmp(line, "NOT_STORED") != 0 &&
+ str_to_llong(line, &num) < 0)
+ break;
+ return 1;
+ }
+ *error_r = t_strdup_printf(
+ "memcached_ascii: Unexpected input (state=%d): %s",
+ states[0], line);
+ return -1;
+}
+
+static int memcached_ascii_input_reply(struct memcached_ascii_dict *dict,
+ const char **error_r)
+{
+ const struct dict_commit_result result = {
+ DICT_COMMIT_RET_OK, NULL
+ };
+ struct memcached_ascii_dict_reply *replies;
+ unsigned int count;
+ int ret;
+
+ if ((ret = memcached_ascii_input_reply_read(dict, error_r)) <= 0)
+ return ret;
+ /* finished a reply */
+ array_pop_front(&dict->input_states);
+
+ replies = array_get_modifiable(&dict->replies, &count);
+ i_assert(count > 0);
+ i_assert(replies[0].reply_count > 0);
+ if (--replies[0].reply_count == 0) {
+ memcached_ascii_callback(dict, &replies[0], &result);
+ array_pop_front(&dict->replies);
+ }
+ return 1;
+}
+
+static void memcached_ascii_conn_input(struct connection *_conn)
+{
+ struct memcached_ascii_connection *conn =
+ (struct memcached_ascii_connection *)_conn;
+ const char *error;
+ int ret;
+
+ switch (i_stream_read(_conn->input)) {
+ case 0:
+ return;
+ case -1:
+ memcached_ascii_disconnected(conn,
+ i_stream_get_disconnect_reason(_conn->input));
+ return;
+ default:
+ break;
+ }
+
+ while ((ret = memcached_ascii_input_reply(conn->dict, &error)) > 0) ;
+ if (ret < 0)
+ memcached_ascii_disconnected(conn, error);
+ io_loop_stop(conn->dict->dict.ioloop);
+}
+
+static int memcached_ascii_input_wait(struct memcached_ascii_dict *dict,
+ const char **error_r)
+{
+ i_assert(io_loop_is_empty(dict->dict.ioloop));
+ dict->dict.prev_ioloop = current_ioloop;
+ io_loop_set_current(dict->dict.ioloop);
+ if (dict->to != NULL)
+ dict->to = io_loop_move_timeout(&dict->to);
+ connection_switch_ioloop(&dict->conn.conn);
+ io_loop_run(dict->dict.ioloop);
+
+ io_loop_set_current(dict->dict.prev_ioloop);
+ dict->dict.prev_ioloop = NULL;
+
+ if (dict->to != NULL)
+ dict->to = io_loop_move_timeout(&dict->to);
+ connection_switch_ioloop(&dict->conn.conn);
+ i_assert(io_loop_is_empty(dict->dict.ioloop));
+
+ if (dict->conn.conn.fd_in == -1) {
+ *error_r = "memcached_ascii: Communication failure";
+ return -1;
+ }
+ return 0;
+}
+
+static void memcached_ascii_input_timeout(struct memcached_ascii_dict *dict)
+{
+ const char *reason = t_strdup_printf(
+ "memcached_ascii: Request timed out in %u.%03u secs",
+ dict->timeout_msecs/1000, dict->timeout_msecs%1000);
+ memcached_ascii_disconnected(&dict->conn, reason);
+}
+
+static int memcached_ascii_wait_replies(struct memcached_ascii_dict *dict,
+ const char **error_r)
+{
+ int ret = 0;
+
+ dict->to = timeout_add(dict->timeout_msecs,
+ memcached_ascii_input_timeout, dict);
+ while (array_count(&dict->input_states) > 0) {
+ i_assert(array_count(&dict->replies) > 0);
+
+ if ((ret = memcached_ascii_input_reply(dict, error_r)) != 0) {
+ if (ret < 0)
+ memcached_ascii_disconnected(&dict->conn, *error_r);
+ break;
+ }
+ ret = memcached_ascii_input_wait(dict, error_r);
+ if (ret != 0)
+ break;
+ }
+
+ timeout_remove(&dict->to);
+ return ret < 0 ? -1 : 0;
+}
+
+static int memcached_ascii_wait(struct memcached_ascii_dict *dict,
+ const char **error_r)
+{
+ int ret;
+
+ i_assert(dict->conn.conn.fd_in != -1);
+
+ if (dict->conn.conn.input == NULL) {
+ /* waiting for connection to finish */
+ dict->to = timeout_add(dict->timeout_msecs,
+ memcached_ascii_input_timeout, dict);
+ ret = memcached_ascii_input_wait(dict, error_r);
+ timeout_remove(&dict->to);
+ if (ret < 0)
+ return -1;
+ }
+ if (memcached_ascii_wait_replies(dict, error_r) < 0)
+ return -1;
+ i_assert(array_count(&dict->input_states) == 0);
+ i_assert(array_count(&dict->replies) == 0);
+ return 0;
+}
+
+static void
+memcached_ascii_conn_connected(struct connection *_conn, bool success)
+{
+ struct memcached_ascii_connection *conn = (struct memcached_ascii_connection *)_conn;
+
+ if (!success) {
+ e_error(conn->conn.event, "connect() failed: %m");
+ }
+ if (conn->dict->dict.ioloop != NULL)
+ io_loop_stop(conn->dict->dict.ioloop);
+}
+
+static const struct connection_settings memcached_ascii_conn_set = {
+ .input_max_size = SIZE_MAX,
+ .output_max_size = SIZE_MAX,
+ .client = TRUE
+};
+
+static const struct connection_vfuncs memcached_ascii_conn_vfuncs = {
+ .destroy = memcached_ascii_conn_destroy,
+ .input = memcached_ascii_conn_input,
+ .client_connected = memcached_ascii_conn_connected
+};
+
+static const char *memcached_ascii_escape_username(const char *username)
+{
+ const char *p;
+ string_t *str = t_str_new(64);
+
+ for (p = username; *p != '\0'; p++) {
+ switch (*p) {
+ case DICT_USERNAME_SEPARATOR:
+ str_append(str, "\\-");
+ break;
+ case '\\':
+ str_append(str, "\\\\");
+ break;
+ default:
+ str_append_c(str, *p);
+ }
+ }
+ return str_c(str);
+}
+
+static int
+memcached_ascii_dict_init(struct dict *driver, const char *uri,
+ const struct dict_settings *set,
+ struct dict **dict_r, const char **error_r)
+{
+ struct memcached_ascii_dict *dict;
+ const char *const *args;
+ struct ioloop *old_ioloop = current_ioloop;
+ int ret = 0;
+
+ if (memcached_ascii_connections == NULL) {
+ memcached_ascii_connections =
+ connection_list_init(&memcached_ascii_conn_set,
+ &memcached_ascii_conn_vfuncs);
+ }
+
+ dict = i_new(struct memcached_ascii_dict, 1);
+ if (net_addr2ip("127.0.0.1", &dict->ip) < 0)
+ i_unreached();
+ dict->port = MEMCACHED_DEFAULT_PORT;
+ dict->timeout_msecs = MEMCACHED_DEFAULT_LOOKUP_TIMEOUT_MSECS;
+ dict->key_prefix = i_strdup("");
+
+ args = t_strsplit(uri, ":");
+ for (; *args != NULL; args++) {
+ if (str_begins(*args, "host=")) {
+ if (net_addr2ip(*args+5, &dict->ip) < 0) {
+ *error_r = t_strdup_printf("Invalid IP: %s",
+ *args+5);
+ ret = -1;
+ }
+ } else if (str_begins(*args, "port=")) {
+ if (net_str2port(*args+5, &dict->port) < 0) {
+ *error_r = t_strdup_printf("Invalid port: %s",
+ *args+5);
+ ret = -1;
+ }
+ } else if (str_begins(*args, "prefix=")) {
+ i_free(dict->key_prefix);
+ dict->key_prefix = i_strdup(*args + 7);
+ } else if (str_begins(*args, "timeout_msecs=")) {
+ if (str_to_uint(*args+14, &dict->timeout_msecs) < 0) {
+ *error_r = t_strdup_printf(
+ "Invalid timeout_msecs: %s", *args+14);
+ ret = -1;
+ }
+ } else {
+ *error_r = t_strdup_printf("Unknown parameter: %s",
+ *args);
+ ret = -1;
+ }
+ }
+ if (ret < 0) {
+ i_free(dict->key_prefix);
+ i_free(dict);
+ return -1;
+ }
+
+ dict->conn.conn.event_parent = set->event_parent;
+ connection_init_client_ip(memcached_ascii_connections, &dict->conn.conn,
+ NULL, &dict->ip, dict->port);
+ event_set_append_log_prefix(dict->conn.conn.event, "memcached: ");
+ dict->dict = *driver;
+ dict->conn.reply_str = str_new(default_pool, 256);
+ dict->conn.dict = dict;
+
+ i_array_init(&dict->input_states, 4);
+ i_array_init(&dict->replies, 4);
+
+ dict->dict.ioloop = io_loop_create();
+ io_loop_set_current(old_ioloop);
+ *dict_r = &dict->dict;
+ return 0;
+}
+
+static void memcached_ascii_dict_deinit(struct dict *_dict)
+{
+ struct memcached_ascii_dict *dict =
+ (struct memcached_ascii_dict *)_dict;
+ struct ioloop *old_ioloop = current_ioloop;
+ const char *error;
+
+ if (array_count(&dict->input_states) > 0) {
+ if (memcached_ascii_wait(dict, &error) < 0)
+ i_error("%s", error);
+ }
+ connection_deinit(&dict->conn.conn);
+
+ io_loop_set_current(dict->dict.ioloop);
+ io_loop_destroy(&dict->dict.ioloop);
+ io_loop_set_current(old_ioloop);
+
+ str_free(&dict->conn.reply_str);
+ array_free(&dict->replies);
+ array_free(&dict->input_states);
+ i_free(dict->key_prefix);
+ i_free(dict);
+
+ if (memcached_ascii_connections->connections == NULL)
+ connection_list_deinit(&memcached_ascii_connections);
+}
+
+static int memcached_ascii_connect(struct memcached_ascii_dict *dict,
+ const char **error_r)
+{
+ if (dict->conn.conn.input != NULL)
+ return 0;
+
+ if (dict->conn.conn.fd_in == -1) {
+ if (connection_client_connect(&dict->conn.conn) < 0) {
+ *error_r = t_strdup_printf(
+ "memcached_ascii: Couldn't connect to %s:%u",
+ net_ip2addr(&dict->ip), dict->port);
+ return -1;
+ }
+ }
+ return memcached_ascii_wait(dict, error_r);
+}
+
+static const char *
+memcached_ascii_dict_get_full_key(struct memcached_ascii_dict *dict,
+ const char *username, const char *key)
+{
+ if (str_begins(key, DICT_PATH_SHARED))
+ key += strlen(DICT_PATH_SHARED);
+ else if (str_begins(key, DICT_PATH_PRIVATE)) {
+ if (strchr(username, DICT_USERNAME_SEPARATOR) == NULL) {
+ key = t_strdup_printf("%s%c%s", username,
+ DICT_USERNAME_SEPARATOR,
+ key + strlen(DICT_PATH_PRIVATE));
+ } else {
+ /* escape the username */
+ key = t_strdup_printf("%s%c%s", memcached_ascii_escape_username(username),
+ DICT_USERNAME_SEPARATOR,
+ key + strlen(DICT_PATH_PRIVATE));
+ }
+ } else {
+ i_unreached();
+ }
+ if (*dict->key_prefix != '\0')
+ key = t_strconcat(dict->key_prefix, key, NULL);
+ return key;
+}
+
+static int
+memcached_ascii_dict_lookup(struct dict *_dict,
+ const struct dict_op_settings *set,
+ pool_t pool, const char *key, const char **value_r,
+ const char **error_r)
+{
+ struct memcached_ascii_dict *dict = (struct memcached_ascii_dict *)_dict;
+ struct memcached_ascii_dict_reply *reply;
+ enum memcached_ascii_input_state state = MEMCACHED_INPUT_STATE_GET;
+
+ if (memcached_ascii_connect(dict, error_r) < 0)
+ return -1;
+
+ key = memcached_ascii_dict_get_full_key(dict, set->username, key);
+ o_stream_nsend_str(dict->conn.conn.output,
+ t_strdup_printf("get %s\r\n", key));
+ array_push_back(&dict->input_states, &state);
+
+ reply = array_append_space(&dict->replies);
+ reply->reply_count = 1;
+
+ if (memcached_ascii_wait(dict, error_r) < 0)
+ return -1;
+
+ *value_r = p_strdup(pool, str_c(dict->conn.reply_str));
+ return dict->conn.value_received ? 1 : 0;
+}
+
+static struct dict_transaction_context *
+memcached_ascii_transaction_init(struct dict *_dict)
+{
+ struct dict_transaction_memory_context *ctx;
+ pool_t pool;
+
+ pool = pool_alloconly_create("file dict transaction", 2048);
+ ctx = p_new(pool, struct dict_transaction_memory_context, 1);
+ dict_transaction_memory_init(ctx, _dict, pool);
+ return &ctx->ctx;
+}
+
+static void
+memcached_send_change(struct dict_memcached_ascii_commit_ctx *ctx,
+ const struct dict_op_settings_private *set,
+ const struct dict_transaction_memory_change *change)
+{
+ enum memcached_ascii_input_state state;
+ const char *key, *value;
+
+ key = memcached_ascii_dict_get_full_key(ctx->dict, set->username,
+ change->key);
+
+ str_truncate(ctx->str, 0);
+ switch (change->type) {
+ case DICT_CHANGE_TYPE_SET:
+ state = MEMCACHED_INPUT_STATE_STORED;
+ str_printfa(ctx->str, "set %s 0 0 %zu\r\n%s\r\n",
+ key, strlen(change->value.str), change->value.str);
+ break;
+ case DICT_CHANGE_TYPE_UNSET:
+ state = MEMCACHED_INPUT_STATE_DELETED;
+ str_printfa(ctx->str, "delete %s\r\n", key);
+ break;
+ case DICT_CHANGE_TYPE_INC:
+ state = MEMCACHED_INPUT_STATE_INCRDECR;
+ if (change->value.diff > 0) {
+ str_printfa(ctx->str, "incr %s %lld\r\n",
+ key, change->value.diff);
+ array_push_back(&ctx->dict->input_states, &state);
+ /* same kludge as with append */
+ value = t_strdup_printf("%lld", change->value.diff);
+ str_printfa(ctx->str, "add %s 0 0 %u\r\n%s\r\n",
+ key, (unsigned int)strlen(value), value);
+ } else {
+ str_printfa(ctx->str, "decr %s %lld\r\n",
+ key, -change->value.diff);
+ }
+ break;
+ }
+ array_push_back(&ctx->dict->input_states, &state);
+ o_stream_nsend(ctx->dict->conn.conn.output,
+ str_data(ctx->str), str_len(ctx->str));
+}
+
+static int
+memcached_ascii_transaction_send(struct dict_memcached_ascii_commit_ctx *ctx,
+ const struct dict_op_settings_private *set,
+ const char **error_r)
+{
+ struct memcached_ascii_dict *dict = ctx->dict;
+ struct memcached_ascii_dict_reply *reply;
+ const struct dict_transaction_memory_change *changes;
+ unsigned int i, count, old_state_count;
+
+ if (memcached_ascii_connect(dict, error_r) < 0)
+ return -1;
+
+ old_state_count = array_count(&dict->input_states);
+ changes = array_get(&ctx->memctx->changes, &count);
+ i_assert(count > 0);
+
+ o_stream_cork(dict->conn.conn.output);
+ for (i = 0; i < count; i++) T_BEGIN {
+ memcached_send_change(ctx, set, &changes[i]);
+ } T_END;
+ o_stream_uncork(dict->conn.conn.output);
+
+ reply = array_append_space(&dict->replies);
+ reply->callback = ctx->callback;
+ reply->context = ctx->context;
+ reply->reply_count = array_count(&dict->input_states) - old_state_count;
+ return 0;
+}
+
+static void
+memcached_ascii_transaction_commit(struct dict_transaction_context *_ctx,
+ bool async,
+ dict_transaction_commit_callback_t *callback,
+ void *context)
+{
+ struct dict_transaction_memory_context *ctx =
+ (struct dict_transaction_memory_context *)_ctx;
+ struct memcached_ascii_dict *dict =
+ (struct memcached_ascii_dict *)_ctx->dict;
+ struct dict_memcached_ascii_commit_ctx commit_ctx;
+ struct dict_commit_result result = { DICT_COMMIT_RET_OK, NULL };
+ const struct dict_op_settings_private *set = &_ctx->set;
+
+ if (_ctx->changed) {
+ i_zero(&commit_ctx);
+ commit_ctx.dict = dict;
+ commit_ctx.memctx = ctx;
+ commit_ctx.callback = callback;
+ commit_ctx.context = context;
+ commit_ctx.str = str_new(default_pool, 128);
+
+ result.ret = memcached_ascii_transaction_send(&commit_ctx, set, &result.error);
+ str_free(&commit_ctx.str);
+
+ if (async && result.ret == 0) {
+ pool_unref(&ctx->pool);
+ return;
+ }
+
+ if (result.ret == 0) {
+ if (memcached_ascii_wait(dict, &result.error) < 0)
+ result.ret = -1;
+ }
+ }
+ callback(&result, context);
+ pool_unref(&ctx->pool);
+}
+
+struct dict dict_driver_memcached_ascii = {
+ .name = "memcached_ascii",
+ {
+ .init = memcached_ascii_dict_init,
+ .deinit = memcached_ascii_dict_deinit,
+ .lookup = memcached_ascii_dict_lookup,
+ .transaction_init = memcached_ascii_transaction_init,
+ .transaction_commit = memcached_ascii_transaction_commit,
+ .transaction_rollback = dict_transaction_memory_rollback,
+ .set = dict_transaction_memory_set,
+ .unset = dict_transaction_memory_unset,
+ .atomic_inc = dict_transaction_memory_atomic_inc,
+ }
+};
diff --git a/src/lib-dict/dict-memcached.c b/src/lib-dict/dict-memcached.c
new file mode 100644
index 0000000..c5b7ce6
--- /dev/null
+++ b/src/lib-dict/dict-memcached.c
@@ -0,0 +1,373 @@
+/* Copyright (c) 2013-2018 Dovecot authors, see the included COPYING memcached */
+
+#include "lib.h"
+#include "array.h"
+#include "str.h"
+#include "istream.h"
+#include "ostream.h"
+#include "connection.h"
+#include "dict-private.h"
+
+#define MEMCACHED_DEFAULT_PORT 11211
+#define MEMCACHED_DEFAULT_LOOKUP_TIMEOUT_MSECS (1000*30)
+
+/* we need only very limited memcached functionality, so just define the binary
+ protocol ourself instead requiring protocol_binary.h */
+#define MEMCACHED_REQUEST_HDR_MAGIC 0x80
+#define MEMCACHED_REPLY_HDR_MAGIC 0x81
+
+#define MEMCACHED_REQUEST_HDR_LENGTH 24
+#define MEMCACHED_REPLY_HDR_LENGTH 24
+
+#define MEMCACHED_CMD_GET 0x00
+
+#define MEMCACHED_DATA_TYPE_RAW 0x00
+
+enum memcached_response {
+ MEMCACHED_RESPONSE_OK = 0x0000,
+ MEMCACHED_RESPONSE_NOTFOUND = 0x0001,
+ MEMCACHED_RESPONSE_INTERNALERROR= 0x0084,
+ MEMCACHED_RESPONSE_BUSY = 0x0085,
+ MEMCACHED_RESPONSE_TEMPFAILURE = 0x0086,
+};
+
+struct memcached_connection {
+ struct connection conn;
+ struct memcached_dict *dict;
+
+ buffer_t *cmd;
+ struct {
+ const unsigned char *value;
+ size_t value_len;
+ uint16_t status; /* enum memcached_response */
+ bool reply_received;
+ } reply;
+};
+
+struct memcached_dict {
+ struct dict dict;
+ struct ip_addr ip;
+ char *key_prefix;
+ in_port_t port;
+ unsigned int timeout_msecs;
+
+ struct memcached_connection conn;
+
+ bool connected;
+};
+
+static struct connection_list *memcached_connections;
+
+static void memcached_conn_destroy(struct connection *_conn)
+{
+ struct memcached_connection *conn = (struct memcached_connection *)_conn;
+
+ conn->dict->connected = FALSE;
+ connection_disconnect(_conn);
+
+ if (conn->dict->dict.ioloop != NULL)
+ io_loop_stop(conn->dict->dict.ioloop);
+}
+
+static int memcached_input_get(struct memcached_connection *conn)
+{
+ const unsigned char *data;
+ size_t size;
+ uint32_t body_len, value_pos;
+ uint16_t key_len, key_pos, status;
+ uint8_t extras_len, data_type;
+
+ data = i_stream_get_data(conn->conn.input, &size);
+ if (size < MEMCACHED_REPLY_HDR_LENGTH)
+ return 0;
+
+ if (data[0] != MEMCACHED_REPLY_HDR_MAGIC) {
+ e_error(conn->conn.event, "Invalid reply magic: %u != %u",
+ data[0], MEMCACHED_REPLY_HDR_MAGIC);
+ return -1;
+ }
+ memcpy(&body_len, data+8, 4); body_len = ntohl(body_len);
+ body_len += MEMCACHED_REPLY_HDR_LENGTH;
+ if (size < body_len) {
+ /* we haven't read the whole response yet */
+ return 0;
+ }
+
+ memcpy(&key_len, data+2, 2); key_len = ntohs(key_len);
+ extras_len = data[4];
+ data_type = data[5];
+ memcpy(&status, data+6, 2); status = ntohs(status);
+ if (data_type != MEMCACHED_DATA_TYPE_RAW) {
+ e_error(conn->conn.event, "Unsupported data type: %u != %u",
+ data[0], MEMCACHED_DATA_TYPE_RAW);
+ return -1;
+ }
+
+ key_pos = MEMCACHED_REPLY_HDR_LENGTH + extras_len;
+ value_pos = key_pos + key_len;
+ if (value_pos > body_len) {
+ e_error(conn->conn.event, "Invalid key/extras lengths");
+ return -1;
+ }
+ conn->reply.value = data + value_pos;
+ conn->reply.value_len = body_len - value_pos;
+ conn->reply.status = status;
+
+ i_stream_skip(conn->conn.input, body_len);
+ conn->reply.reply_received = TRUE;
+
+ if (conn->dict->dict.ioloop != NULL)
+ io_loop_stop(conn->dict->dict.ioloop);
+ return 1;
+}
+
+static void memcached_conn_input(struct connection *_conn)
+{
+ struct memcached_connection *conn = (struct memcached_connection *)_conn;
+
+ switch (i_stream_read(_conn->input)) {
+ case 0:
+ return;
+ case -1:
+ memcached_conn_destroy(_conn);
+ return;
+ default:
+ break;
+ }
+
+ if (memcached_input_get(conn) < 0)
+ memcached_conn_destroy(_conn);
+}
+
+static void memcached_conn_connected(struct connection *_conn, bool success)
+{
+ struct memcached_connection *conn =
+ (struct memcached_connection *)_conn;
+
+ if (!success) {
+ e_error(conn->conn.event, "connect() failed: %m");
+ } else {
+ conn->dict->connected = TRUE;
+ }
+ if (conn->dict->dict.ioloop != NULL)
+ io_loop_stop(conn->dict->dict.ioloop);
+}
+
+static const struct connection_settings memcached_conn_set = {
+ .input_max_size = SIZE_MAX,
+ .output_max_size = SIZE_MAX,
+ .client = TRUE
+};
+
+static const struct connection_vfuncs memcached_conn_vfuncs = {
+ .destroy = memcached_conn_destroy,
+ .input = memcached_conn_input,
+ .client_connected = memcached_conn_connected
+};
+
+static int
+memcached_dict_init(struct dict *driver, const char *uri,
+ const struct dict_settings *set,
+ struct dict **dict_r, const char **error_r)
+{
+ struct memcached_dict *dict;
+ const char *const *args;
+ int ret = 0;
+
+ if (memcached_connections == NULL) {
+ memcached_connections =
+ connection_list_init(&memcached_conn_set,
+ &memcached_conn_vfuncs);
+ }
+
+ dict = i_new(struct memcached_dict, 1);
+ if (net_addr2ip("127.0.0.1", &dict->ip) < 0)
+ i_unreached();
+ dict->port = MEMCACHED_DEFAULT_PORT;
+ dict->timeout_msecs = MEMCACHED_DEFAULT_LOOKUP_TIMEOUT_MSECS;
+ dict->key_prefix = i_strdup("");
+
+ args = t_strsplit(uri, ":");
+ for (; *args != NULL; args++) {
+ if (str_begins(*args, "host=")) {
+ if (net_addr2ip(*args+5, &dict->ip) < 0) {
+ *error_r = t_strdup_printf("Invalid IP: %s",
+ *args+5);
+ ret = -1;
+ }
+ } else if (str_begins(*args, "port=")) {
+ if (net_str2port(*args+5, &dict->port) < 0) {
+ *error_r = t_strdup_printf("Invalid port: %s",
+ *args+5);
+ ret = -1;
+ }
+ } else if (str_begins(*args, "prefix=")) {
+ i_free(dict->key_prefix);
+ dict->key_prefix = i_strdup(*args + 7);
+ } else if (str_begins(*args, "timeout_msecs=")) {
+ if (str_to_uint(*args+14, &dict->timeout_msecs) < 0) {
+ *error_r = t_strdup_printf(
+ "Invalid timeout_msecs: %s", *args+14);
+ ret = -1;
+ }
+ } else {
+ *error_r = t_strdup_printf("Unknown parameter: %s",
+ *args);
+ ret = -1;
+ }
+ }
+ if (ret < 0) {
+ i_free(dict->key_prefix);
+ i_free(dict);
+ return -1;
+ }
+
+ dict->conn.conn.event_parent = set->event_parent;
+
+ connection_init_client_ip(memcached_connections, &dict->conn.conn,
+ NULL, &dict->ip, dict->port);
+ event_set_append_log_prefix(dict->conn.conn.event, "memcached: ");
+ dict->dict = *driver;
+ dict->conn.cmd = buffer_create_dynamic(default_pool, 256);
+ dict->conn.dict = dict;
+ *dict_r = &dict->dict;
+ return 0;
+}
+
+static void memcached_dict_deinit(struct dict *_dict)
+{
+ struct memcached_dict *dict = (struct memcached_dict *)_dict;
+
+ connection_deinit(&dict->conn.conn);
+ buffer_free(&dict->conn.cmd);
+ i_free(dict->key_prefix);
+ i_free(dict);
+
+ if (memcached_connections->connections == NULL)
+ connection_list_deinit(&memcached_connections);
+}
+
+static void memcached_dict_lookup_timeout(struct memcached_dict *dict)
+{
+ e_error(dict->dict.event, "Lookup timed out in %u.%03u secs",
+ dict->timeout_msecs/1000, dict->timeout_msecs%1000);
+ io_loop_stop(dict->dict.ioloop);
+}
+
+static void memcached_add_header(buffer_t *buf, unsigned int key_len)
+{
+ uint32_t body_len = htonl(key_len);
+
+ i_assert(key_len <= 0xffff);
+
+ buffer_append_c(buf, MEMCACHED_REQUEST_HDR_MAGIC);
+ buffer_append_c(buf, MEMCACHED_CMD_GET);
+ buffer_append_c(buf, (key_len >> 8) & 0xff);
+ buffer_append_c(buf, key_len & 0xff);
+ buffer_append_c(buf, 0); /* extras length */
+ buffer_append_c(buf, MEMCACHED_DATA_TYPE_RAW);
+ buffer_append_zero(buf, 2); /* vbucket id - we probably don't care? */
+ buffer_append(buf, &body_len, sizeof(body_len));
+ buffer_append_zero(buf, 4+8); /* opaque + cas */
+ i_assert(buf->used == MEMCACHED_REQUEST_HDR_LENGTH);
+}
+
+static int
+memcached_dict_lookup(struct dict *_dict, const struct dict_op_settings *set ATTR_UNUSED,
+ pool_t pool, const char *key, const char **value_r,
+ const char **error_r)
+{
+ struct memcached_dict *dict = (struct memcached_dict *)_dict;
+ struct ioloop *prev_ioloop = current_ioloop;
+ struct timeout *to;
+ size_t key_len;
+
+ if (str_begins(key, DICT_PATH_SHARED))
+ key += strlen(DICT_PATH_SHARED);
+ else {
+ *error_r = t_strdup_printf("memcached: Only shared keys supported currently");
+ return -1;
+ }
+ if (*dict->key_prefix != '\0')
+ key = t_strconcat(dict->key_prefix, key, NULL);
+ key_len = strlen(key);
+ if (key_len > 0xffff) {
+ *error_r = t_strdup_printf(
+ "memcached: Key is too long (%zu bytes): %s", key_len, key);
+ return -1;
+ }
+
+ i_assert(dict->dict.ioloop == NULL);
+
+ dict->dict.ioloop = io_loop_create();
+ connection_switch_ioloop(&dict->conn.conn);
+
+ if (dict->conn.conn.fd_in == -1 &&
+ connection_client_connect(&dict->conn.conn) < 0) {
+ e_error(dict->conn.conn.event, "Couldn't connect");
+ } else {
+ to = timeout_add(dict->timeout_msecs,
+ memcached_dict_lookup_timeout, dict);
+ if (!dict->connected) {
+ /* wait for connection */
+ io_loop_run(dict->dict.ioloop);
+ }
+
+ if (dict->connected) {
+ buffer_set_used_size(dict->conn.cmd, 0);
+ memcached_add_header(dict->conn.cmd, key_len);
+ buffer_append(dict->conn.cmd, key, key_len);
+
+ o_stream_nsend(dict->conn.conn.output,
+ dict->conn.cmd->data,
+ dict->conn.cmd->used);
+
+ i_zero(&dict->conn.reply);
+ io_loop_run(dict->dict.ioloop);
+ }
+ timeout_remove(&to);
+ }
+
+ io_loop_set_current(prev_ioloop);
+ connection_switch_ioloop(&dict->conn.conn);
+ io_loop_set_current(dict->dict.ioloop);
+ io_loop_destroy(&dict->dict.ioloop);
+
+ if (!dict->conn.reply.reply_received) {
+ /* we failed in some way. make sure we disconnect since the
+ connection state isn't known anymore */
+ memcached_conn_destroy(&dict->conn.conn);
+ *error_r = "Communication failure";
+ return -1;
+ }
+ switch (dict->conn.reply.status) {
+ case MEMCACHED_RESPONSE_OK:
+ *value_r = p_strndup(pool, dict->conn.reply.value,
+ dict->conn.reply.value_len);
+ return 1;
+ case MEMCACHED_RESPONSE_NOTFOUND:
+ return 0;
+ case MEMCACHED_RESPONSE_INTERNALERROR:
+ *error_r = "Lookup failed: Internal error";
+ return -1;
+ case MEMCACHED_RESPONSE_BUSY:
+ *error_r = "Lookup failed: Busy";
+ return -1;
+ case MEMCACHED_RESPONSE_TEMPFAILURE:
+ *error_r = "Lookup failed: Temporary failure";
+ return -1;
+ }
+
+ *error_r = t_strdup_printf("Lookup failed: Error code=%u",
+ dict->conn.reply.status);
+ return -1;
+}
+
+struct dict dict_driver_memcached = {
+ .name = "memcached",
+ {
+ .init = memcached_dict_init,
+ .deinit = memcached_dict_deinit,
+ .lookup = memcached_dict_lookup,
+ }
+};
diff --git a/src/lib-dict/dict-private.h b/src/lib-dict/dict-private.h
new file mode 100644
index 0000000..e5ec5e3
--- /dev/null
+++ b/src/lib-dict/dict-private.h
@@ -0,0 +1,123 @@
+#ifndef DICT_PRIVATE_H
+#define DICT_PRIVATE_H
+
+#include <time.h>
+#include "dict.h"
+
+struct ioloop;
+
+struct dict_vfuncs {
+ int (*init)(struct dict *dict_driver, const char *uri,
+ const struct dict_settings *set,
+ struct dict **dict_r, const char **error_r);
+ void (*deinit)(struct dict *dict);
+ void (*wait)(struct dict *dict);
+
+ int (*lookup)(struct dict *dict, const struct dict_op_settings *set,
+ pool_t pool, const char *key, const char **value_r,
+ const char **error_r);
+
+ struct dict_iterate_context *
+ (*iterate_init)(struct dict *dict,
+ const struct dict_op_settings *set,
+ const char *path,
+ enum dict_iterate_flags flags);
+ bool (*iterate)(struct dict_iterate_context *ctx,
+ const char **key_r, const char *const **values_r);
+ int (*iterate_deinit)(struct dict_iterate_context *ctx,
+ const char **error_r);
+
+ struct dict_transaction_context *(*transaction_init)(struct dict *dict);
+ /* call the callback before returning if non-async commits */
+ void (*transaction_commit)(struct dict_transaction_context *ctx,
+ bool async,
+ dict_transaction_commit_callback_t *callback,
+ void *context);
+ void (*transaction_rollback)(struct dict_transaction_context *ctx);
+
+ void (*set)(struct dict_transaction_context *ctx,
+ const char *key, const char *value);
+ void (*unset)(struct dict_transaction_context *ctx,
+ const char *key);
+ void (*atomic_inc)(struct dict_transaction_context *ctx,
+ const char *key, long long diff);
+
+ void (*lookup_async)(struct dict *dict, const struct dict_op_settings *set,
+ const char *key, dict_lookup_callback_t *callback,
+ void *context);
+ bool (*switch_ioloop)(struct dict *dict);
+ void (*set_timestamp)(struct dict_transaction_context *ctx,
+ const struct timespec *ts);
+};
+
+struct dict_commit_callback_ctx;
+
+struct dict_op_settings_private {
+ char *username;
+ char *home_dir;
+};
+
+struct dict {
+ const char *name;
+
+ struct dict_vfuncs v;
+ unsigned int iter_count;
+ unsigned int transaction_count;
+ struct dict_transaction_context *transactions;
+ int refcount;
+ struct event *event;
+ struct ioloop *ioloop, *prev_ioloop;
+ struct dict_commit_callback_ctx *commits;
+};
+
+struct dict_iterate_context {
+ struct dict *dict;
+ struct event *event;
+ struct dict_op_settings_private set;
+ enum dict_iterate_flags flags;
+
+ dict_iterate_callback_t *async_callback;
+ void *async_context;
+
+ uint64_t row_count, max_rows;
+
+ bool has_more:1;
+};
+
+struct dict_transaction_context {
+ struct dict *dict;
+ struct dict_op_settings_private set;
+ struct dict_transaction_context *prev, *next;
+
+ struct event *event;
+ struct timespec timestamp;
+
+ bool changed:1;
+ bool no_slowness_warning:1;
+};
+
+void dict_transaction_commit_async_noop_callback(
+ const struct dict_commit_result *result, void *context);
+
+extern struct dict dict_driver_client;
+extern struct dict dict_driver_file;
+extern struct dict dict_driver_fs;
+extern struct dict dict_driver_memcached;
+extern struct dict dict_driver_memcached_ascii;
+extern struct dict dict_driver_redis;
+extern struct dict dict_driver_cdb;
+extern struct dict dict_driver_fail;
+
+extern struct dict_iterate_context dict_iter_unsupported;
+extern struct dict_transaction_context dict_transaction_unsupported;
+
+void dict_pre_api_callback(struct dict *dict);
+void dict_post_api_callback(struct dict *dict);
+
+/* Duplicate an object of type dict_op_settings. Used for initializing/freeing
+ iterator and transaction contexts. */
+void dict_op_settings_dup(const struct dict_op_settings *source,
+ struct dict_op_settings_private *dest_r);
+void dict_op_settings_private_free(struct dict_op_settings_private *set);
+
+#endif
diff --git a/src/lib-dict/dict-redis.c b/src/lib-dict/dict-redis.c
new file mode 100644
index 0000000..01ec7b0
--- /dev/null
+++ b/src/lib-dict/dict-redis.c
@@ -0,0 +1,831 @@
+/* Copyright (c) 2008-2018 Dovecot authors, see the included COPYING redis */
+
+#include "lib.h"
+#include "array.h"
+#include "str.h"
+#include "istream.h"
+#include "ostream.h"
+#include "connection.h"
+#include "dict-private.h"
+
+#define REDIS_DEFAULT_PORT 6379
+#define REDIS_DEFAULT_LOOKUP_TIMEOUT_MSECS (1000*30)
+#define DICT_USERNAME_SEPARATOR '/'
+
+enum redis_input_state {
+ /* expecting +OK reply for AUTH */
+ REDIS_INPUT_STATE_AUTH,
+ /* expecting +OK reply for SELECT */
+ REDIS_INPUT_STATE_SELECT,
+ /* expecting $-1 / $<size> followed by GET reply */
+ REDIS_INPUT_STATE_GET,
+ /* expecting +QUEUED */
+ REDIS_INPUT_STATE_MULTI,
+ /* expecting +OK reply for DISCARD */
+ REDIS_INPUT_STATE_DISCARD,
+ /* expecting *<nreplies> */
+ REDIS_INPUT_STATE_EXEC,
+ /* expecting EXEC reply */
+ REDIS_INPUT_STATE_EXEC_REPLY
+};
+
+struct redis_connection {
+ struct connection conn;
+ struct redis_dict *dict;
+
+ string_t *last_reply;
+ unsigned int bytes_left;
+ bool value_not_found;
+ bool value_received;
+};
+
+struct redis_dict_reply {
+ unsigned int reply_count;
+ dict_transaction_commit_callback_t *callback;
+ void *context;
+};
+
+struct redis_dict {
+ struct dict dict;
+ char *password, *key_prefix, *expire_value;
+ unsigned int timeout_msecs, db_id;
+
+ struct redis_connection conn;
+
+ ARRAY(enum redis_input_state) input_states;
+ ARRAY(struct redis_dict_reply) replies;
+
+ bool connected;
+ bool transaction_open;
+ bool db_id_set;
+};
+
+struct redis_dict_transaction_context {
+ struct dict_transaction_context ctx;
+ unsigned int cmd_count;
+ char *error;
+};
+
+static struct connection_list *redis_connections;
+
+static void
+redis_input_state_add(struct redis_dict *dict, enum redis_input_state state)
+{
+ array_push_back(&dict->input_states, &state);
+}
+
+static void redis_input_state_remove(struct redis_dict *dict)
+{
+ array_pop_front(&dict->input_states);
+}
+
+static void redis_reply_callback(struct redis_connection *conn,
+ const struct redis_dict_reply *reply,
+ const struct dict_commit_result *result)
+{
+ if (conn->dict->dict.prev_ioloop != NULL)
+ io_loop_set_current(conn->dict->dict.prev_ioloop);
+ reply->callback(result, reply->context);
+ if (conn->dict->dict.prev_ioloop != NULL)
+ io_loop_set_current(conn->dict->dict.ioloop);
+}
+
+static void
+redis_disconnected(struct redis_connection *conn, const char *reason)
+{
+ const struct dict_commit_result result = {
+ DICT_COMMIT_RET_FAILED, reason
+ };
+ const struct redis_dict_reply *reply;
+
+ conn->dict->db_id_set = FALSE;
+ conn->dict->connected = FALSE;
+ connection_disconnect(&conn->conn);
+
+ array_foreach(&conn->dict->replies, reply)
+ redis_reply_callback(conn, reply, &result);
+ array_clear(&conn->dict->replies);
+ array_clear(&conn->dict->input_states);
+
+ if (conn->dict->dict.ioloop != NULL)
+ io_loop_stop(conn->dict->dict.ioloop);
+}
+
+static void redis_conn_destroy(struct connection *_conn)
+{
+ struct redis_connection *conn = (struct redis_connection *)_conn;
+
+ redis_disconnected(conn, connection_disconnect_reason(_conn));
+}
+
+static void redis_dict_wait_timeout(struct redis_dict *dict)
+{
+ const char *reason = t_strdup_printf(
+ "redis: Commit timed out in %u.%03u secs",
+ dict->timeout_msecs/1000, dict->timeout_msecs%1000);
+ redis_disconnected(&dict->conn, reason);
+}
+
+static void redis_wait(struct redis_dict *dict)
+{
+ struct timeout *to;
+
+ i_assert(dict->dict.ioloop == NULL);
+
+ dict->dict.prev_ioloop = current_ioloop;
+ dict->dict.ioloop = io_loop_create();
+ to = timeout_add(dict->timeout_msecs, redis_dict_wait_timeout, dict);
+ connection_switch_ioloop(&dict->conn.conn);
+
+ do {
+ io_loop_run(dict->dict.ioloop);
+ } while (array_count(&dict->input_states) > 0);
+
+ timeout_remove(&to);
+ io_loop_set_current(dict->dict.prev_ioloop);
+ connection_switch_ioloop(&dict->conn.conn);
+ io_loop_set_current(dict->dict.ioloop);
+ io_loop_destroy(&dict->dict.ioloop);
+ dict->dict.prev_ioloop = NULL;
+}
+
+static int redis_input_get(struct redis_connection *conn, const char **error_r)
+{
+ const unsigned char *data;
+ size_t size;
+ const char *line;
+
+ if (conn->bytes_left == 0) {
+ /* read the size first */
+ line = i_stream_next_line(conn->conn.input);
+ if (line == NULL)
+ return 0;
+ if (strcmp(line, "$-1") == 0) {
+ conn->value_received = TRUE;
+ conn->value_not_found = TRUE;
+ if (conn->dict->dict.ioloop != NULL)
+ io_loop_stop(conn->dict->dict.ioloop);
+ redis_input_state_remove(conn->dict);
+ return 1;
+ }
+ if (line[0] != '$' || str_to_uint(line+1, &conn->bytes_left) < 0) {
+ *error_r = t_strdup_printf(
+ "redis: Unexpected input (wanted $size): %s", line);
+ return -1;
+ }
+ conn->bytes_left += 2; /* include trailing CRLF */
+ }
+
+ data = i_stream_get_data(conn->conn.input, &size);
+ if (size > conn->bytes_left)
+ size = conn->bytes_left;
+ str_append_data(conn->last_reply, data, size);
+
+ conn->bytes_left -= size;
+ i_stream_skip(conn->conn.input, size);
+
+ if (conn->bytes_left > 0)
+ return 0;
+
+ /* reply fully read - drop trailing CRLF */
+ conn->value_received = TRUE;
+ str_truncate(conn->last_reply, str_len(conn->last_reply)-2);
+
+ if (conn->dict->dict.ioloop != NULL)
+ io_loop_stop(conn->dict->dict.ioloop);
+ redis_input_state_remove(conn->dict);
+ return 1;
+}
+
+static int
+redis_conn_input_more(struct redis_connection *conn, const char **error_r)
+{
+ struct redis_dict *dict = conn->dict;
+ struct redis_dict_reply *reply;
+ const enum redis_input_state *states;
+ enum redis_input_state state;
+ unsigned int count, num_replies;
+ const char *line;
+
+ states = array_get(&dict->input_states, &count);
+ if (count == 0) {
+ line = i_stream_next_line(conn->conn.input);
+ if (line == NULL)
+ return 0;
+ *error_r = t_strdup_printf(
+ "redis: Unexpected input (expected nothing): %s", line);
+ return -1;
+ }
+ state = states[0];
+ if (state == REDIS_INPUT_STATE_GET)
+ return redis_input_get(conn, error_r);
+
+ line = i_stream_next_line(conn->conn.input);
+ if (line == NULL)
+ return 0;
+
+ redis_input_state_remove(dict);
+ switch (state) {
+ case REDIS_INPUT_STATE_GET:
+ i_unreached();
+ case REDIS_INPUT_STATE_AUTH:
+ case REDIS_INPUT_STATE_SELECT:
+ case REDIS_INPUT_STATE_MULTI:
+ case REDIS_INPUT_STATE_DISCARD:
+ if (line[0] != '+')
+ break;
+ return 1;
+ case REDIS_INPUT_STATE_EXEC:
+ if (line[0] != '*' || str_to_uint(line+1, &num_replies) < 0)
+ break;
+
+ reply = array_front_modifiable(&dict->replies);
+ i_assert(reply->reply_count > 0);
+ if (reply->reply_count != num_replies) {
+ *error_r = t_strdup_printf(
+ "redis: EXEC expected %u replies, not %u",
+ reply->reply_count, num_replies);
+ return -1;
+ }
+ return 1;
+ case REDIS_INPUT_STATE_EXEC_REPLY:
+ if (*line != '+' && *line != ':')
+ break;
+ /* success, just ignore the actual reply */
+ reply = array_front_modifiable(&dict->replies);
+ i_assert(reply->reply_count > 0);
+ if (--reply->reply_count == 0) {
+ const struct dict_commit_result result = {
+ DICT_COMMIT_RET_OK, NULL
+ };
+ redis_reply_callback(conn, reply, &result);
+ array_pop_front(&dict->replies);
+ /* if we're running in a dict-ioloop, we're handling a
+ synchronous commit and need to stop now */
+ if (array_count(&dict->replies) == 0 &&
+ conn->dict->dict.ioloop != NULL)
+ io_loop_stop(conn->dict->dict.ioloop);
+ }
+ return 1;
+ }
+ str_truncate(dict->conn.last_reply, 0);
+ str_append(dict->conn.last_reply, line);
+ *error_r = t_strdup_printf("redis: Unexpected input (state=%d): %s", state, line);
+ return -1;
+}
+
+static void redis_conn_input(struct connection *_conn)
+{
+ struct redis_connection *conn = (struct redis_connection *)_conn;
+ const char *error = NULL;
+ int ret;
+
+ switch (i_stream_read(_conn->input)) {
+ case 0:
+ return;
+ case -1:
+ redis_disconnected(conn, i_stream_get_error(_conn->input));
+ return;
+ default:
+ break;
+ }
+
+ while ((ret = redis_conn_input_more(conn, &error)) > 0) ;
+ if (ret < 0) {
+ i_assert(error != NULL);
+ redis_disconnected(conn, error);
+ }
+}
+
+static void redis_conn_connected(struct connection *_conn, bool success)
+{
+ struct redis_connection *conn = (struct redis_connection *)_conn;
+
+ if (!success) {
+ e_error(conn->conn.event, "connect() failed: %m");
+ } else {
+ conn->dict->connected = TRUE;
+ }
+ if (conn->dict->dict.ioloop != NULL)
+ io_loop_stop(conn->dict->dict.ioloop);
+}
+
+static const struct connection_settings redis_conn_set = {
+ .input_max_size = SIZE_MAX,
+ .output_max_size = SIZE_MAX,
+ .client = TRUE
+};
+
+static const struct connection_vfuncs redis_conn_vfuncs = {
+ .destroy = redis_conn_destroy,
+ .input = redis_conn_input,
+ .client_connected = redis_conn_connected
+};
+
+static const char *redis_escape_username(const char *username)
+{
+ const char *p;
+ string_t *str = t_str_new(64);
+
+ for (p = username; *p != '\0'; p++) {
+ switch (*p) {
+ case DICT_USERNAME_SEPARATOR:
+ str_append(str, "\\-");
+ break;
+ case '\\':
+ str_append(str, "\\\\");
+ break;
+ default:
+ str_append_c(str, *p);
+ }
+ }
+ return str_c(str);
+}
+
+static int
+redis_dict_init(struct dict *driver, const char *uri,
+ const struct dict_settings *set,
+ struct dict **dict_r, const char **error_r)
+{
+ struct redis_dict *dict;
+ struct ip_addr ip;
+ unsigned int secs;
+ in_port_t port = REDIS_DEFAULT_PORT;
+ const char *const *args, *unix_path = NULL;
+ int ret = 0;
+
+ if (redis_connections == NULL) {
+ redis_connections =
+ connection_list_init(&redis_conn_set,
+ &redis_conn_vfuncs);
+ }
+
+ dict = i_new(struct redis_dict, 1);
+ if (net_addr2ip("127.0.0.1", &ip) < 0)
+ i_unreached();
+ dict->timeout_msecs = REDIS_DEFAULT_LOOKUP_TIMEOUT_MSECS;
+ dict->key_prefix = i_strdup("");
+ dict->password = i_strdup("");
+
+ args = t_strsplit(uri, ":");
+ for (; *args != NULL; args++) {
+ if (str_begins(*args, "path=")) {
+ unix_path = *args + 5;
+ } else if (str_begins(*args, "host=")) {
+ if (net_addr2ip(*args+5, &ip) < 0) {
+ *error_r = t_strdup_printf("Invalid IP: %s",
+ *args+5);
+ ret = -1;
+ }
+ } else if (str_begins(*args, "port=")) {
+ if (net_str2port(*args+5, &port) < 0) {
+ *error_r = t_strdup_printf("Invalid port: %s",
+ *args+5);
+ ret = -1;
+ }
+ } else if (str_begins(*args, "prefix=")) {
+ i_free(dict->key_prefix);
+ dict->key_prefix = i_strdup(*args + 7);
+ } else if (str_begins(*args, "db=")) {
+ if (str_to_uint(*args+3, &dict->db_id) < 0) {
+ *error_r = t_strdup_printf(
+ "Invalid db number: %s", *args+3);
+ ret = -1;
+ }
+ } else if (str_begins(*args, "expire_secs=")) {
+ const char *value = *args + 12;
+
+ if (str_to_uint(value, &secs) < 0 || secs == 0) {
+ *error_r = t_strdup_printf(
+ "Invalid expire_secs: %s", value);
+ ret = -1;
+ }
+ i_free(dict->expire_value);
+ dict->expire_value = i_strdup(value);
+ } else if (str_begins(*args, "timeout_msecs=")) {
+ if (str_to_uint(*args+14, &dict->timeout_msecs) < 0) {
+ *error_r = t_strdup_printf(
+ "Invalid timeout_msecs: %s", *args+14);
+ ret = -1;
+ }
+ } else if (str_begins(*args, "password=")) {
+ i_free(dict->password);
+ dict->password = i_strdup(*args + 9);
+ } else {
+ *error_r = t_strdup_printf("Unknown parameter: %s",
+ *args);
+ ret = -1;
+ }
+ }
+ if (ret < 0) {
+ i_free(dict->password);
+ i_free(dict->key_prefix);
+ i_free(dict);
+ return -1;
+ }
+
+ dict->conn.conn.event_parent = set->event_parent;
+
+ if (unix_path != NULL) {
+ connection_init_client_unix(redis_connections, &dict->conn.conn,
+ unix_path);
+ } else {
+ connection_init_client_ip(redis_connections, &dict->conn.conn,
+ NULL, &ip, port);
+ }
+ event_set_append_log_prefix(dict->conn.conn.event, "redis: ");
+ dict->dict = *driver;
+ dict->conn.last_reply = str_new(default_pool, 256);
+ dict->conn.dict = dict;
+
+ i_array_init(&dict->input_states, 4);
+ i_array_init(&dict->replies, 4);
+
+ *dict_r = &dict->dict;
+ return 0;
+}
+
+static void redis_dict_deinit(struct dict *_dict)
+{
+ struct redis_dict *dict = (struct redis_dict *)_dict;
+
+ if (array_count(&dict->input_states) > 0) {
+ i_assert(dict->connected);
+ redis_wait(dict);
+ }
+ connection_deinit(&dict->conn.conn);
+ str_free(&dict->conn.last_reply);
+ array_free(&dict->replies);
+ array_free(&dict->input_states);
+ i_free(dict->expire_value);
+ i_free(dict->key_prefix);
+ i_free(dict->password);
+ i_free(dict);
+
+ if (redis_connections->connections == NULL)
+ connection_list_deinit(&redis_connections);
+}
+
+static void redis_dict_wait(struct dict *_dict)
+{
+ struct redis_dict *dict = (struct redis_dict *)_dict;
+
+ if (array_count(&dict->input_states) > 0)
+ redis_wait(dict);
+}
+
+static void redis_dict_lookup_timeout(struct redis_dict *dict)
+{
+ const char *reason = t_strdup_printf(
+ "redis: Lookup timed out in %u.%03u secs",
+ dict->timeout_msecs/1000, dict->timeout_msecs%1000);
+ redis_disconnected(&dict->conn, reason);
+}
+
+static const char *
+redis_dict_get_full_key(struct redis_dict *dict, const char *username,
+ const char *key)
+{
+ const char *username_sp = strchr(username, DICT_USERNAME_SEPARATOR);
+
+ if (str_begins(key, DICT_PATH_SHARED))
+ key += strlen(DICT_PATH_SHARED);
+ else if (str_begins(key, DICT_PATH_PRIVATE)) {
+ key = t_strdup_printf("%s%c%s",
+ username_sp == NULL ? username :
+ redis_escape_username(username),
+ DICT_USERNAME_SEPARATOR,
+ key + strlen(DICT_PATH_PRIVATE));
+ } else {
+ i_unreached();
+ }
+ if (*dict->key_prefix != '\0')
+ key = t_strconcat(dict->key_prefix, key, NULL);
+ return key;
+}
+
+static void redis_dict_auth(struct redis_dict *dict)
+{
+ const char *cmd;
+
+ if (*dict->password == '\0')
+ return;
+
+ cmd = t_strdup_printf("*2\r\n$4\r\nAUTH\r\n$%d\r\n%s\r\n",
+ (int)strlen(dict->password), dict->password);
+ o_stream_nsend_str(dict->conn.conn.output, cmd);
+ redis_input_state_add(dict, REDIS_INPUT_STATE_AUTH);
+}
+
+static void redis_dict_select_db(struct redis_dict *dict)
+{
+ const char *cmd, *db_str;
+
+ if (dict->db_id_set)
+ return;
+ dict->db_id_set = TRUE;
+ if (dict->db_id == 0) {
+ /* 0 is the default */
+ return;
+ }
+ db_str = dec2str(dict->db_id);
+ cmd = t_strdup_printf("*2\r\n$6\r\nSELECT\r\n$%d\r\n%s\r\n",
+ (int)strlen(db_str), db_str);
+ o_stream_nsend_str(dict->conn.conn.output, cmd);
+ redis_input_state_add(dict, REDIS_INPUT_STATE_SELECT);
+}
+
+static int redis_dict_lookup(struct dict *_dict,
+ const struct dict_op_settings *set,
+ pool_t pool, const char *key,
+ const char **value_r, const char **error_r)
+{
+ struct redis_dict *dict = (struct redis_dict *)_dict;
+ struct timeout *to;
+ const char *cmd;
+
+ key = redis_dict_get_full_key(dict, set->username, key);
+
+ dict->conn.value_received = FALSE;
+ dict->conn.value_not_found = FALSE;
+
+ i_assert(dict->dict.ioloop == NULL);
+
+ dict->dict.prev_ioloop = current_ioloop;
+ dict->dict.ioloop = io_loop_create();
+ connection_switch_ioloop(&dict->conn.conn);
+
+ if (dict->conn.conn.fd_in == -1 &&
+ connection_client_connect(&dict->conn.conn) < 0) {
+ e_error(dict->conn.conn.event, "Couldn't connect");
+ } else {
+ to = timeout_add(dict->timeout_msecs,
+ redis_dict_lookup_timeout, dict);
+ if (!dict->connected) {
+ /* wait for connection */
+ io_loop_run(dict->dict.ioloop);
+ if (dict->connected)
+ redis_dict_auth(dict);
+ }
+
+ if (dict->connected) {
+ redis_dict_select_db(dict);
+ cmd = t_strdup_printf("*2\r\n$3\r\nGET\r\n$%d\r\n%s\r\n",
+ (int)strlen(key), key);
+ o_stream_nsend_str(dict->conn.conn.output, cmd);
+
+ str_truncate(dict->conn.last_reply, 0);
+ redis_input_state_add(dict, REDIS_INPUT_STATE_GET);
+ do {
+ io_loop_run(dict->dict.ioloop);
+ } while (array_count(&dict->input_states) > 0);
+ }
+ timeout_remove(&to);
+ }
+
+ io_loop_set_current(dict->dict.prev_ioloop);
+ connection_switch_ioloop(&dict->conn.conn);
+ io_loop_set_current(dict->dict.ioloop);
+ io_loop_destroy(&dict->dict.ioloop);
+ dict->dict.prev_ioloop = NULL;
+
+ if (!dict->conn.value_received) {
+ /* we failed in some way. make sure we disconnect since the
+ connection state isn't known anymore */
+ *error_r = t_strdup_printf("redis: Communication failure (last reply: %s)",
+ str_c(dict->conn.last_reply));
+ redis_disconnected(&dict->conn, *error_r);
+ return -1;
+ }
+ if (dict->conn.value_not_found)
+ return 0;
+
+ *value_r = p_strdup(pool, str_c(dict->conn.last_reply));
+ return 1;
+}
+
+static struct dict_transaction_context *
+redis_transaction_init(struct dict *_dict)
+{
+ struct redis_dict *dict = (struct redis_dict *)_dict;
+ struct redis_dict_transaction_context *ctx;
+
+ i_assert(!dict->transaction_open);
+ dict->transaction_open = TRUE;
+
+ ctx = i_new(struct redis_dict_transaction_context, 1);
+ ctx->ctx.dict = _dict;
+
+ if (dict->conn.conn.fd_in == -1 &&
+ connection_client_connect(&dict->conn.conn) < 0) {
+ e_error(dict->conn.conn.event, "Couldn't connect");
+ } else if (!dict->connected) {
+ /* wait for connection */
+ redis_wait(dict);
+ if (dict->connected)
+ redis_dict_auth(dict);
+ }
+ if (dict->connected)
+ redis_dict_select_db(dict);
+ return &ctx->ctx;
+}
+
+static void
+redis_transaction_commit(struct dict_transaction_context *_ctx, bool async,
+ dict_transaction_commit_callback_t *callback,
+ void *context)
+{
+ struct redis_dict_transaction_context *ctx =
+ (struct redis_dict_transaction_context *)_ctx;
+ struct redis_dict *dict = (struct redis_dict *)_ctx->dict;
+ struct redis_dict_reply *reply;
+ unsigned int i;
+ struct dict_commit_result result = { .ret = DICT_COMMIT_RET_OK };
+
+ i_assert(dict->transaction_open);
+ dict->transaction_open = FALSE;
+
+ if (ctx->error != NULL) {
+ /* make sure we're disconnected */
+ redis_disconnected(&dict->conn, ctx->error);
+ result.ret = -1;
+ result.error = ctx->error;
+ } else if (_ctx->changed) {
+ i_assert(ctx->cmd_count > 0);
+
+ o_stream_nsend_str(dict->conn.conn.output,
+ "*1\r\n$4\r\nEXEC\r\n");
+ reply = array_append_space(&dict->replies);
+ reply->callback = callback;
+ reply->context = context;
+ reply->reply_count = ctx->cmd_count;
+ redis_input_state_add(dict, REDIS_INPUT_STATE_EXEC);
+ for (i = 0; i < ctx->cmd_count; i++)
+ redis_input_state_add(dict, REDIS_INPUT_STATE_EXEC_REPLY);
+ if (async) {
+ i_free(ctx);
+ return;
+ }
+ redis_wait(dict);
+ }
+ callback(&result, context);
+ i_free(ctx->error);
+ i_free(ctx);
+}
+
+static void redis_transaction_rollback(struct dict_transaction_context *_ctx)
+{
+ struct redis_dict_transaction_context *ctx =
+ (struct redis_dict_transaction_context *)_ctx;
+ struct redis_dict *dict = (struct redis_dict *)_ctx->dict;
+ struct redis_dict_reply *reply;
+
+ i_assert(dict->transaction_open);
+ dict->transaction_open = FALSE;
+
+ if (ctx->error != NULL) {
+ /* make sure we're disconnected */
+ redis_disconnected(&dict->conn, ctx->error);
+ } else if (_ctx->changed) {
+ o_stream_nsend_str(dict->conn.conn.output,
+ "*1\r\n$7\r\nDISCARD\r\n");
+ reply = array_append_space(&dict->replies);
+ reply->reply_count = 1;
+ redis_input_state_add(dict, REDIS_INPUT_STATE_DISCARD);
+ }
+ i_free(ctx->error);
+ i_free(ctx);
+}
+
+static int redis_check_transaction(struct redis_dict_transaction_context *ctx)
+{
+ struct redis_dict *dict = (struct redis_dict *)ctx->ctx.dict;
+
+ if (ctx->error != NULL)
+ return -1;
+ if (!dict->connected) {
+ ctx->error = i_strdup("Disconnected during transaction");
+ return -1;
+ }
+ if (ctx->ctx.changed)
+ return 0;
+
+ redis_input_state_add(dict, REDIS_INPUT_STATE_MULTI);
+ if (o_stream_send_str(dict->conn.conn.output,
+ "*1\r\n$5\r\nMULTI\r\n") < 0) {
+ ctx->error = i_strdup_printf("write() failed: %s",
+ o_stream_get_error(dict->conn.conn.output));
+ return -1;
+ }
+ return 0;
+}
+
+static void
+redis_append_expire(struct redis_dict_transaction_context *ctx,
+ string_t *cmd, const char *key)
+{
+ struct redis_dict *dict = (struct redis_dict *)ctx->ctx.dict;
+
+ if (dict->expire_value == NULL)
+ return;
+
+ str_printfa(cmd, "*3\r\n$6\r\nEXPIRE\r\n$%u\r\n%s\r\n$%u\r\n%s\r\n",
+ (unsigned int)strlen(key), key,
+ (unsigned int)strlen(dict->expire_value),
+ dict->expire_value);
+ redis_input_state_add(dict, REDIS_INPUT_STATE_MULTI);
+ ctx->cmd_count++;
+}
+
+static void redis_set(struct dict_transaction_context *_ctx,
+ const char *key, const char *value)
+{
+ struct redis_dict_transaction_context *ctx =
+ (struct redis_dict_transaction_context *)_ctx;
+ struct redis_dict *dict = (struct redis_dict *)_ctx->dict;
+ const struct dict_op_settings_private *set = &_ctx->set;
+ string_t *cmd;
+
+ if (redis_check_transaction(ctx) < 0)
+ return;
+
+ key = redis_dict_get_full_key(dict, set->username, key);
+ cmd = t_str_new(128);
+ str_printfa(cmd, "*3\r\n$3\r\nSET\r\n$%u\r\n%s\r\n$%u\r\n%s\r\n",
+ (unsigned int)strlen(key), key,
+ (unsigned int)strlen(value), value);
+ redis_input_state_add(dict, REDIS_INPUT_STATE_MULTI);
+ ctx->cmd_count++;
+ redis_append_expire(ctx, cmd, key);
+ if (o_stream_send(dict->conn.conn.output, str_data(cmd), str_len(cmd)) < 0) {
+ ctx->error = i_strdup_printf("write() failed: %s",
+ o_stream_get_error(dict->conn.conn.output));
+ }
+}
+
+static void redis_unset(struct dict_transaction_context *_ctx,
+ const char *key)
+{
+ struct redis_dict_transaction_context *ctx =
+ (struct redis_dict_transaction_context *)_ctx;
+ struct redis_dict *dict = (struct redis_dict *)_ctx->dict;
+ const struct dict_op_settings_private *set = &_ctx->set;
+ const char *cmd;
+
+ if (redis_check_transaction(ctx) < 0)
+ return;
+
+ key = redis_dict_get_full_key(dict, set->username, key);
+ cmd = t_strdup_printf("*2\r\n$3\r\nDEL\r\n$%u\r\n%s\r\n",
+ (unsigned int)strlen(key), key);
+ if (o_stream_send_str(dict->conn.conn.output, cmd) < 0) {
+ ctx->error = i_strdup_printf("write() failed: %s",
+ o_stream_get_error(dict->conn.conn.output));
+ }
+ redis_input_state_add(dict, REDIS_INPUT_STATE_MULTI);
+ ctx->cmd_count++;
+}
+
+static void redis_atomic_inc(struct dict_transaction_context *_ctx,
+ const char *key, long long diff)
+{
+ struct redis_dict_transaction_context *ctx =
+ (struct redis_dict_transaction_context *)_ctx;
+ struct redis_dict *dict = (struct redis_dict *)_ctx->dict;
+ const struct dict_op_settings_private *set = &_ctx->set;
+ const char *diffstr;
+ string_t *cmd;
+
+ if (redis_check_transaction(ctx) < 0)
+ return;
+
+ key = redis_dict_get_full_key(dict, set->username, key);
+ diffstr = t_strdup_printf("%lld", diff);
+ cmd = t_str_new(128);
+ str_printfa(cmd, "*3\r\n$6\r\nINCRBY\r\n$%u\r\n%s\r\n$%u\r\n%s\r\n",
+ (unsigned int)strlen(key), key,
+ (unsigned int)strlen(diffstr), diffstr);
+ redis_input_state_add(dict, REDIS_INPUT_STATE_MULTI);
+ ctx->cmd_count++;
+ redis_append_expire(ctx, cmd, key);
+ if (o_stream_send(dict->conn.conn.output, str_data(cmd), str_len(cmd)) < 0) {
+ ctx->error = i_strdup_printf("write() failed: %s",
+ o_stream_get_error(dict->conn.conn.output));
+ }
+}
+
+struct dict dict_driver_redis = {
+ .name = "redis",
+ {
+ .init = redis_dict_init,
+ .deinit = redis_dict_deinit,
+ .wait = redis_dict_wait,
+ .lookup = redis_dict_lookup,
+ .transaction_init = redis_transaction_init,
+ .transaction_commit = redis_transaction_commit,
+ .transaction_rollback = redis_transaction_rollback,
+ .set = redis_set,
+ .unset = redis_unset,
+ .atomic_inc = redis_atomic_inc,
+ }
+};
diff --git a/src/lib-dict/dict-transaction-memory.c b/src/lib-dict/dict-transaction-memory.c
new file mode 100644
index 0000000..4793ad3
--- /dev/null
+++ b/src/lib-dict/dict-transaction-memory.c
@@ -0,0 +1,59 @@
+/* Copyright (c) 2005-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "dict-transaction-memory.h"
+
+void dict_transaction_memory_init(struct dict_transaction_memory_context *ctx,
+ struct dict *dict, pool_t pool)
+{
+ ctx->ctx.dict = dict;
+ ctx->pool = pool;
+ p_array_init(&ctx->changes, pool, 32);
+}
+
+void dict_transaction_memory_rollback(struct dict_transaction_context *_ctx)
+{
+ struct dict_transaction_memory_context *ctx =
+ (struct dict_transaction_memory_context *)_ctx;
+
+ pool_unref(&ctx->pool);
+}
+
+void dict_transaction_memory_set(struct dict_transaction_context *_ctx,
+ const char *key, const char *value)
+{
+ struct dict_transaction_memory_context *ctx =
+ (struct dict_transaction_memory_context *)_ctx;
+ struct dict_transaction_memory_change *change;
+
+ change = array_append_space(&ctx->changes);
+ change->type = DICT_CHANGE_TYPE_SET;
+ change->key = p_strdup(ctx->pool, key);
+ change->value.str = p_strdup(ctx->pool, value);
+}
+
+void dict_transaction_memory_unset(struct dict_transaction_context *_ctx,
+ const char *key)
+{
+ struct dict_transaction_memory_context *ctx =
+ (struct dict_transaction_memory_context *)_ctx;
+ struct dict_transaction_memory_change *change;
+
+ change = array_append_space(&ctx->changes);
+ change->type = DICT_CHANGE_TYPE_UNSET;
+ change->key = p_strdup(ctx->pool, key);
+}
+
+void dict_transaction_memory_atomic_inc(struct dict_transaction_context *_ctx,
+ const char *key, long long diff)
+{
+ struct dict_transaction_memory_context *ctx =
+ (struct dict_transaction_memory_context *)_ctx;
+ struct dict_transaction_memory_change *change;
+
+ change = array_append_space(&ctx->changes);
+ change->type = DICT_CHANGE_TYPE_INC;
+ change->key = p_strdup(ctx->pool, key);
+ change->value.diff = diff;
+}
diff --git a/src/lib-dict/dict-transaction-memory.h b/src/lib-dict/dict-transaction-memory.h
new file mode 100644
index 0000000..2164c1e
--- /dev/null
+++ b/src/lib-dict/dict-transaction-memory.h
@@ -0,0 +1,38 @@
+#ifndef DICT_TRANSACTION_MEMORY_H
+#define DICT_TRANSACTION_MEMORY_H
+
+#include "dict-private.h"
+
+enum dict_change_type {
+ DICT_CHANGE_TYPE_SET,
+ DICT_CHANGE_TYPE_UNSET,
+ DICT_CHANGE_TYPE_INC
+};
+
+struct dict_transaction_memory_change {
+ enum dict_change_type type;
+ const char *key;
+ union {
+ const char *str;
+ long long diff;
+ } value;
+};
+
+struct dict_transaction_memory_context {
+ struct dict_transaction_context ctx;
+ pool_t pool;
+ ARRAY(struct dict_transaction_memory_change) changes;
+};
+
+void dict_transaction_memory_init(struct dict_transaction_memory_context *ctx,
+ struct dict *dict, pool_t pool);
+void dict_transaction_memory_rollback(struct dict_transaction_context *ctx);
+
+void dict_transaction_memory_set(struct dict_transaction_context *ctx,
+ const char *key, const char *value);
+void dict_transaction_memory_unset(struct dict_transaction_context *ctx,
+ const char *key);
+void dict_transaction_memory_atomic_inc(struct dict_transaction_context *ctx,
+ const char *key, long long diff);
+
+#endif
diff --git a/src/lib-dict/dict-txn-lua.c b/src/lib-dict/dict-txn-lua.c
new file mode 100644
index 0000000..34a475d
--- /dev/null
+++ b/src/lib-dict/dict-txn-lua.c
@@ -0,0 +1,262 @@
+/* Copyright (c) 2021 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "dict.h"
+#include "dlua-script-private.h"
+#include "dict-lua-private.h"
+#include "dlua-wrapper.h"
+
+struct lua_dict_txn {
+ pool_t pool;
+ struct dict_transaction_context *txn;
+ enum {
+ STATE_OPEN,
+ STATE_COMMITTED,
+ STATE_ABORTED,
+ } state;
+
+ lua_State *L;
+ const char *username;
+};
+
+static int lua_dict_transaction_rollback(lua_State *L);
+static int lua_dict_transaction_commit(lua_State *L);
+static int lua_dict_set(lua_State *L);
+static int lua_dict_unset(lua_State *L);
+static int lua_dict_set_timestamp(lua_State *L);
+
+static luaL_Reg lua_dict_txn_methods[] = {
+ { "rollback", lua_dict_transaction_rollback },
+ { "commit", lua_dict_transaction_commit },
+ { "set", lua_dict_set },
+ { "unset", lua_dict_unset },
+ { "set_timestamp", lua_dict_set_timestamp },
+ { NULL, NULL },
+};
+
+static void sanity_check_txn(lua_State *L, struct lua_dict_txn *txn)
+{
+ switch (txn->state) {
+ case STATE_OPEN:
+ return;
+ case STATE_COMMITTED:
+ luaL_error(L, "dict transaction already committed");
+ return;
+ case STATE_ABORTED:
+ luaL_error(L, "dict transaction already aborted");
+ return;
+ }
+
+ i_unreached();
+}
+
+/* no actual ref counting, but we use it for clean up */
+static void lua_dict_txn_unref(struct lua_dict_txn *txn)
+{
+ /* rollback any transactions that were forgotten about */
+ dict_transaction_rollback(&txn->txn);
+
+ pool_unref(&txn->pool);
+}
+
+DLUA_WRAP_C_DATA(dict_txn, struct lua_dict_txn, lua_dict_txn_unref,
+ lua_dict_txn_methods);
+
+/*
+ * Abort a transaction [-1,+0,e]
+ *
+ * Args:
+ * 1) userdata: struct lua_dict_txn *
+ */
+static int lua_dict_transaction_rollback(lua_State *L)
+{
+ struct lua_dict_txn *txn;
+
+ DLUA_REQUIRE_ARGS(L, 1);
+
+ txn = xlua_dict_txn_getptr(L, 1, NULL);
+ sanity_check_txn(L, txn);
+
+ txn->state = STATE_ABORTED;
+ dict_transaction_rollback(&txn->txn);
+
+ return 0;
+}
+
+static int lua_dict_transaction_commit_continue(lua_State *L,
+ int status ATTR_UNUSED,
+ lua_KContext ctx ATTR_UNUSED)
+{
+ if (!lua_isnil(L, -1))
+ lua_error(L); /* commit failed */
+
+ lua_pop(L, 1); /* pop the nil indicating the lack of error */
+
+ return 0;
+}
+
+static void
+lua_dict_transaction_commit_callback(const struct dict_commit_result *result,
+ struct lua_dict_txn *txn)
+{
+
+ switch (result->ret) {
+ case DICT_COMMIT_RET_OK:
+ /* push a nil to indicate everything is ok */
+ lua_pushnil(txn->L);
+ break;
+ case DICT_COMMIT_RET_NOTFOUND:
+ /* we don't expose dict_atomic_inc(), so this should never happen */
+ i_unreached();
+ case DICT_COMMIT_RET_FAILED:
+ case DICT_COMMIT_RET_WRITE_UNCERTAIN:
+ /* push the error we'll raise when we resume */
+ i_assert(result->error != NULL);
+ lua_pushfstring(txn->L, "dict transaction commit failed: %s",
+ result->error);
+ break;
+ }
+
+ dlua_pcall_yieldable_resume(txn->L, 1);
+}
+
+/*
+ * Commit a transaction [-1,+0,e]
+ *
+ * Args:
+ * 1) userdata: struct lua_dict_txn *
+ */
+static int lua_dict_transaction_commit(lua_State *L)
+{
+ struct lua_dict_txn *txn;
+
+ DLUA_REQUIRE_ARGS(L, 1);
+
+ txn = xlua_dict_txn_getptr(L, 1, NULL);
+ sanity_check_txn(L, txn);
+
+ txn->state = STATE_COMMITTED;
+ dict_transaction_commit_async(&txn->txn,
+ lua_dict_transaction_commit_callback, txn);
+
+ return lua_dict_transaction_commit_continue(L,
+ lua_yieldk(L, 0, 0, lua_dict_transaction_commit_continue), 0);
+}
+
+/*
+ * Set key to value [-3,+0,e]
+ *
+ * Args:
+ * 1) userdata: struct lua_dict_txn *
+ * 2) string: key
+ * 3) string: value
+ */
+static int lua_dict_set(lua_State *L)
+{
+ struct lua_dict_txn *txn;
+ const char *key, *value;
+
+ DLUA_REQUIRE_ARGS(L, 3);
+
+ txn = xlua_dict_txn_getptr(L, 1, NULL);
+ key = luaL_checkstring(L, 2);
+ value = luaL_checkstring(L, 3);
+ lua_dict_check_key_prefix(L, key, txn->username);
+
+ dict_set(txn->txn, key, value);
+
+ return 0;
+}
+
+/*
+ * Unset key [-2,+0,e]
+ *
+ * Args:
+ * 1) userdata: struct lua_dict_txn *
+ * 2) string: key
+ */
+static int lua_dict_unset(lua_State *L)
+{
+ struct lua_dict_txn *txn;
+ const char *key;
+
+ DLUA_REQUIRE_ARGS(L, 2);
+
+ txn = xlua_dict_txn_getptr(L, 1, NULL);
+ key = luaL_checkstring(L, 2);
+ lua_dict_check_key_prefix(L, key, txn->username);
+
+ dict_unset(txn->txn, key);
+
+ return 0;
+}
+
+/*
+ * Start a dict transaction [-(1|2),+1,e]
+ *
+ * Args:
+ * 1) userdata: struct dict *
+ * 2*) string: username
+ *
+ * Returns:
+ * Returns a new transaction object.
+ * Username will be NULL if not provided in args.
+ */
+int lua_dict_transaction_begin(lua_State *L)
+{
+ struct lua_dict_txn *txn;
+ struct dict *dict;
+ const char *username = NULL;
+ pool_t pool;
+
+ DLUA_REQUIRE_ARGS_IN(L, 1, 2);
+
+ dict = dlua_check_dict(L, 1);
+ if (lua_gettop(L) >= 2)
+ username = luaL_checkstring(L, 2);
+
+ pool = pool_alloconly_create("lua dict txn", 128);
+ txn = p_new(pool, struct lua_dict_txn, 1);
+ txn->pool = pool;
+
+ struct dict_op_settings set = {
+ .username = username,
+ };
+ txn->txn = dict_transaction_begin(dict, &set);
+ txn->state = STATE_OPEN;
+ txn->L = L;
+ txn->username = p_strdup(txn->pool, username);
+
+ xlua_pushdict_txn(L, txn, FALSE);
+
+ return 1;
+}
+
+/*
+ * Set timestamp to the transaction [-2,+0,e]
+ *
+ * Args:
+ * 1) userdata: struct lua_dict_txn *
+ * 2) PosixTimespec : { tv_sec, tv_nsec }
+ */
+static int lua_dict_set_timestamp(lua_State *L)
+{
+ struct lua_dict_txn *txn;
+ lua_Number tv_sec, tv_nsec;
+
+ DLUA_REQUIRE_ARGS(L, 2);
+
+ txn = xlua_dict_txn_getptr(L, 1, NULL);
+ if (dlua_table_get_number_by_str(L, 2, "tv_sec", &tv_sec) <= 0)
+ luaL_error(L, "tv_sec missing from table");
+ if (dlua_table_get_number_by_str(L, 2, "tv_nsec", &tv_nsec) <= 0)
+ luaL_error(L, "tv_nsec missing from table");
+
+ struct timespec ts = {
+ .tv_sec = tv_sec,
+ .tv_nsec = tv_nsec
+ };
+ dict_transaction_set_timestamp(txn->txn, &ts);
+ return 0;
+}
diff --git a/src/lib-dict/dict.c b/src/lib-dict/dict.c
new file mode 100644
index 0000000..cdaec09
--- /dev/null
+++ b/src/lib-dict/dict.c
@@ -0,0 +1,759 @@
+/* Copyright (c) 2005-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "guid.h"
+#include "llist.h"
+#include "ioloop.h"
+#include "str.h"
+#include "ioloop.h"
+#include "dict-private.h"
+
+struct dict_commit_callback_ctx {
+ pool_t pool;
+ struct dict_commit_callback_ctx *prev, *next;
+ struct dict *dict;
+ struct event *event;
+ dict_transaction_commit_callback_t *callback;
+ struct dict_op_settings_private set;
+ struct timeout *to;
+ void *context;
+ struct dict_commit_result result;
+ bool delayed_callback:1;
+};
+
+struct dict_lookup_callback_ctx {
+ struct dict *dict;
+ struct event *event;
+ dict_lookup_callback_t *callback;
+ void *context;
+};
+
+static ARRAY(struct dict *) dict_drivers;
+
+static void
+dict_commit_async_timeout(struct dict_commit_callback_ctx *ctx);
+
+static struct event_category event_category_dict = {
+ .name = "dict",
+};
+
+static struct dict *dict_driver_lookup(const char *name)
+{
+ struct dict *dict;
+
+ array_foreach_elem(&dict_drivers, dict) {
+ if (strcmp(dict->name, name) == 0)
+ return dict;
+ }
+ return NULL;
+}
+
+void dict_transaction_commit_async_noop_callback(
+ const struct dict_commit_result *result ATTR_UNUSED,
+ void *context ATTR_UNUSED)
+{
+ /* do nothing */
+}
+
+void dict_driver_register(struct dict *driver)
+{
+ if (!array_is_created(&dict_drivers))
+ i_array_init(&dict_drivers, 8);
+
+ if (dict_driver_lookup(driver->name) != NULL) {
+ i_fatal("dict_driver_register(%s): Already registered",
+ driver->name);
+ }
+ array_push_back(&dict_drivers, &driver);
+}
+
+void dict_driver_unregister(struct dict *driver)
+{
+ struct dict *const *dicts;
+ unsigned int idx = UINT_MAX;
+
+ array_foreach(&dict_drivers, dicts) {
+ if (*dicts == driver) {
+ idx = array_foreach_idx(&dict_drivers, dicts);
+ break;
+ }
+ }
+ i_assert(idx != UINT_MAX);
+ array_delete(&dict_drivers, idx, 1);
+
+ if (array_count(&dict_drivers) == 0)
+ array_free(&dict_drivers);
+}
+
+int dict_init(const char *uri, const struct dict_settings *set,
+ struct dict **dict_r, const char **error_r)
+{
+ struct dict_settings set_dup = *set;
+ struct dict *dict;
+ const char *p, *name, *error;
+
+ p = strchr(uri, ':');
+ if (p == NULL) {
+ *error_r = t_strdup_printf("Dictionary URI is missing ':': %s",
+ uri);
+ return -1;
+ }
+
+ name = t_strdup_until(uri, p);
+ dict = dict_driver_lookup(name);
+ if (dict == NULL) {
+ *error_r = t_strdup_printf("Unknown dict module: %s", name);
+ return -1;
+ }
+ struct event *event = event_create(set->event_parent);
+ event_add_category(event, &event_category_dict);
+ event_add_str(event, "driver", dict->name);
+ event_set_append_log_prefix(event, t_strdup_printf("dict(%s): ",
+ dict->name));
+ set_dup.event_parent = event;
+ if (dict->v.init(dict, p+1, &set_dup, dict_r, &error) < 0) {
+ *error_r = t_strdup_printf("dict %s: %s", name, error);
+ event_unref(&event);
+ return -1;
+ }
+ i_assert(*dict_r != NULL);
+ (*dict_r)->refcount++;
+ (*dict_r)->event = event;
+ e_debug(event_create_passthrough(event)->set_name("dict_created")->event(),
+ "dict created (uri=%s, base_dir=%s)", uri, set->base_dir);
+
+ return 0;
+}
+
+static void dict_ref(struct dict *dict)
+{
+ i_assert(dict->refcount > 0);
+
+ dict->refcount++;
+}
+
+static void dict_unref(struct dict **_dict)
+{
+ struct dict *dict = *_dict;
+ *_dict = NULL;
+ if (dict == NULL)
+ return;
+ struct event *event = dict->event;
+ i_assert(dict->refcount > 0);
+ if (--dict->refcount == 0) {
+ dict->v.deinit(dict);
+ e_debug(event_create_passthrough(event)->
+ set_name("dict_destroyed")->event(), "dict destroyed");
+ event_unref(&event);
+ }
+}
+
+void dict_deinit(struct dict **_dict)
+{
+ struct dict *dict = *_dict;
+
+ *_dict = NULL;
+
+ i_assert(dict->iter_count == 0);
+ i_assert(dict->transaction_count == 0);
+ i_assert(dict->transactions == NULL);
+ i_assert(dict->commits == NULL);
+ dict_unref(&dict);
+}
+
+void dict_wait(struct dict *dict)
+{
+ struct dict_commit_callback_ctx *commit, *next;
+
+ e_debug(dict->event, "Waiting for dict to finish pending operations");
+ if (dict->v.wait != NULL)
+ dict->v.wait(dict);
+ for (commit = dict->commits; commit != NULL; commit = next) {
+ next = commit->next;
+ dict_commit_async_timeout(commit);
+ }
+}
+
+bool dict_switch_ioloop(struct dict *dict)
+{
+ struct dict_commit_callback_ctx *commit;
+ bool ret = FALSE;
+
+ for (commit = dict->commits; commit != NULL; commit = commit->next) {
+ commit->to = io_loop_move_timeout(&commit->to);
+ ret = TRUE;
+ }
+ if (dict->v.switch_ioloop != NULL) {
+ if (dict->v.switch_ioloop(dict))
+ return TRUE;
+ }
+ return ret;
+}
+
+static bool dict_key_prefix_is_valid(const char *key, const char *username)
+{
+ if (str_begins(key, DICT_PATH_SHARED))
+ return TRUE;
+ if (str_begins(key, DICT_PATH_PRIVATE)) {
+ i_assert(username != NULL && username[0] != '\0');
+ return TRUE;
+ }
+ return FALSE;
+
+}
+
+void dict_pre_api_callback(struct dict *dict)
+{
+ if (dict->prev_ioloop != NULL) {
+ /* Don't let callback see that we've created our
+ internal ioloop in case it wants to add some ios
+ or timeouts. */
+ io_loop_set_current(dict->prev_ioloop);
+ }
+}
+
+void dict_post_api_callback(struct dict *dict)
+{
+ if (dict->prev_ioloop != NULL) {
+ io_loop_set_current(dict->ioloop);
+ io_loop_stop(dict->ioloop);
+ }
+}
+
+static void dict_lookup_finished(struct event *event, int ret, const char *error)
+{
+ i_assert(ret >= 0 || error != NULL);
+ const char *key = event_find_field_recursive_str(event, "key");
+ if (ret < 0)
+ event_add_str(event, "error", error);
+ else if (ret == 0)
+ event_add_str(event, "key_not_found", "yes");
+ event_set_name(event, "dict_lookup_finished");
+ e_debug(event, "Lookup finished for '%s': %s", key, ret > 0 ?
+ "found" :
+ "not found");
+}
+
+static void dict_transaction_finished(struct event *event, enum dict_commit_ret ret,
+ bool rollback, const char *error)
+{
+ i_assert(ret > DICT_COMMIT_RET_FAILED || error != NULL);
+ if (ret == DICT_COMMIT_RET_FAILED || ret == DICT_COMMIT_RET_WRITE_UNCERTAIN) {
+ if (ret == DICT_COMMIT_RET_WRITE_UNCERTAIN)
+ event_add_str(event, "write_uncertain", "yes");
+ event_add_str(event, "error", error);
+ } else if (rollback) {
+ event_add_str(event, "rollback", "yes");
+ } else if (ret == 0) {
+ event_add_str(event, "key_not_found", "yes");
+ }
+ event_set_name(event, "dict_transaction_finished");
+ e_debug(event, "Dict transaction finished");
+}
+
+static void
+dict_lookup_callback(const struct dict_lookup_result *result,
+ void *context)
+{
+ struct dict_lookup_callback_ctx *ctx = context;
+
+ dict_pre_api_callback(ctx->dict);
+ ctx->callback(result, ctx->context);
+ dict_post_api_callback(ctx->dict);
+ dict_lookup_finished(ctx->event, result->ret, result->error);
+ event_unref(&ctx->event);
+
+ dict_unref(&ctx->dict);
+ i_free(ctx);
+}
+
+static void
+dict_commit_async_timeout(struct dict_commit_callback_ctx *ctx)
+{
+ DLLIST_REMOVE(&ctx->dict->commits, ctx);
+ timeout_remove(&ctx->to);
+ dict_pre_api_callback(ctx->dict);
+ if (ctx->callback != NULL)
+ ctx->callback(&ctx->result, ctx->context);
+ else if (ctx->result.ret < 0)
+ e_error(ctx->event, "Commit failed: %s", ctx->result.error);
+ dict_post_api_callback(ctx->dict);
+
+ dict_transaction_finished(ctx->event, ctx->result.ret, FALSE, ctx->result.error);
+ dict_op_settings_private_free(&ctx->set);
+ event_unref(&ctx->event);
+ dict_unref(&ctx->dict);
+ pool_unref(&ctx->pool);
+}
+
+static void dict_commit_callback(const struct dict_commit_result *result,
+ void *context)
+{
+ struct dict_commit_callback_ctx *ctx = context;
+
+ i_assert(result->ret >= 0 || result->error != NULL);
+ ctx->result = *result;
+ if (ctx->delayed_callback) {
+ ctx->result.error = p_strdup(ctx->pool, ctx->result.error);
+ ctx->to = timeout_add_short(0, dict_commit_async_timeout, ctx);
+ } else {
+ dict_commit_async_timeout(ctx);
+ }
+}
+
+static struct event *
+dict_event_create(struct dict *dict, const struct dict_op_settings *set)
+{
+ struct event *event = event_create(dict->event);
+ if (set->username != NULL)
+ event_add_str(event, "user", set->username);
+ return event;
+}
+
+int dict_lookup(struct dict *dict, const struct dict_op_settings *set,
+ pool_t pool, const char *key,
+ const char **value_r, const char **error_r)
+{
+ struct event *event = dict_event_create(dict, set);
+ int ret;
+ i_assert(dict_key_prefix_is_valid(key, set->username));
+
+ e_debug(event, "Looking up '%s'", key);
+ event_add_str(event, "key", key);
+ ret = dict->v.lookup(dict, set, pool, key, value_r, error_r);
+ dict_lookup_finished(event, ret, *error_r);
+ event_unref(&event);
+ return ret;
+}
+
+#undef dict_lookup_async
+void dict_lookup_async(struct dict *dict, const struct dict_op_settings *set,
+ const char *key, dict_lookup_callback_t *callback,
+ void *context)
+{
+ i_assert(dict_key_prefix_is_valid(key, set->username));
+ if (dict->v.lookup_async == NULL) {
+ struct dict_lookup_result result;
+
+ i_zero(&result);
+ /* event is going to be sent by dict_lookup */
+ result.ret = dict_lookup(dict, set, pool_datastack_create(),
+ key, &result.value, &result.error);
+ const char *const values[] = { result.value, NULL };
+ result.values = values;
+ callback(&result, context);
+ return;
+ }
+ struct dict_lookup_callback_ctx *lctx =
+ i_new(struct dict_lookup_callback_ctx, 1);
+ lctx->dict = dict;
+ dict_ref(lctx->dict);
+ lctx->callback = callback;
+ lctx->context = context;
+ lctx->event = dict_event_create(dict, set);
+ event_add_str(lctx->event, "key", key);
+ e_debug(lctx->event, "Looking up (async) '%s'", key);
+ dict->v.lookup_async(dict, set, key, dict_lookup_callback, lctx);
+}
+
+struct dict_iterate_context *
+dict_iterate_init(struct dict *dict, const struct dict_op_settings *set,
+ const char *path, enum dict_iterate_flags flags)
+{
+ struct dict_iterate_context *ctx;
+
+ i_assert(path != NULL);
+ i_assert(dict_key_prefix_is_valid(path, set->username));
+
+ if (dict->v.iterate_init == NULL) {
+ /* not supported by backend */
+ ctx = &dict_iter_unsupported;
+ } else {
+ ctx = dict->v.iterate_init(dict, set, path, flags);
+ }
+ /* the dict in context can differ from the dict
+ passed as parameter, e.g. it can be dict-fail when
+ iteration is not supported. */
+ ctx->event = dict_event_create(dict, set);
+ ctx->flags = flags;
+ dict_op_settings_dup(set, &ctx->set);
+
+ event_add_str(ctx->event, "key", path);
+ event_set_name(ctx->event, "dict_iteration_started");
+ e_debug(ctx->event, "Iterating prefix %s", path);
+ ctx->dict->iter_count++;
+ return ctx;
+}
+
+bool dict_iterate(struct dict_iterate_context *ctx,
+ const char **key_r, const char **value_r)
+{
+ const char *const *values;
+
+ if (!dict_iterate_values(ctx, key_r, &values))
+ return FALSE;
+ if ((ctx->flags & DICT_ITERATE_FLAG_NO_VALUE) == 0)
+ *value_r = values[0];
+ return TRUE;
+}
+
+bool dict_iterate_values(struct dict_iterate_context *ctx,
+ const char **key_r, const char *const **values_r)
+{
+
+ if (ctx->max_rows > 0 && ctx->row_count >= ctx->max_rows) {
+ e_debug(ctx->event, "Maximum row count (%"PRIu64") reached",
+ ctx->max_rows);
+ /* row count was limited */
+ ctx->has_more = FALSE;
+ return FALSE;
+ }
+ if (!ctx->dict->v.iterate(ctx, key_r, values_r))
+ return FALSE;
+ if ((ctx->flags & DICT_ITERATE_FLAG_NO_VALUE) != 0) {
+ /* always return value as NULL to be consistent across
+ drivers */
+ *values_r = NULL;
+ } else {
+ i_assert(values_r[0] != NULL);
+ }
+ ctx->row_count++;
+ return TRUE;
+}
+
+#undef dict_iterate_set_async_callback
+void dict_iterate_set_async_callback(struct dict_iterate_context *ctx,
+ dict_iterate_callback_t *callback,
+ void *context)
+{
+ ctx->async_callback = callback;
+ ctx->async_context = context;
+}
+
+void dict_iterate_set_limit(struct dict_iterate_context *ctx,
+ uint64_t max_rows)
+{
+ ctx->max_rows = max_rows;
+}
+
+bool dict_iterate_has_more(struct dict_iterate_context *ctx)
+{
+ return ctx->has_more;
+}
+
+int dict_iterate_deinit(struct dict_iterate_context **_ctx,
+ const char **error_r)
+{
+ struct dict_iterate_context *ctx = *_ctx;
+
+ if (ctx == NULL)
+ return 0;
+
+ struct event *event = ctx->event;
+ int ret;
+ uint64_t rows;
+
+ i_assert(ctx->dict->iter_count > 0);
+ ctx->dict->iter_count--;
+
+ *_ctx = NULL;
+ rows = ctx->row_count;
+ struct dict_op_settings_private set_copy = ctx->set;
+ ret = ctx->dict->v.iterate_deinit(ctx, error_r);
+ dict_op_settings_private_free(&set_copy);
+
+ event_add_int(event, "rows", rows);
+ event_set_name(event, "dict_iteration_finished");
+
+ if (ret < 0) {
+ event_add_str(event, "error", *error_r);
+ e_debug(event, "Iteration finished: %s", *error_r);
+ } else {
+ if (rows == 0)
+ event_add_str(event, "key_not_found", "yes");
+ e_debug(event, "Iteration finished, got %"PRIu64" rows", rows);
+ }
+
+ event_unref(&event);
+ return ret;
+}
+
+struct dict_transaction_context *
+dict_transaction_begin(struct dict *dict, const struct dict_op_settings *set)
+{
+ struct dict_transaction_context *ctx;
+ guid_128_t guid;
+ if (dict->v.transaction_init == NULL)
+ ctx = &dict_transaction_unsupported;
+ else
+ ctx = dict->v.transaction_init(dict);
+ /* the dict in context can differ from the dict
+ passed as parameter, e.g. it can be dict-fail when
+ transactions are not supported. */
+ ctx->dict->transaction_count++;
+ DLLIST_PREPEND(&ctx->dict->transactions, ctx);
+ ctx->event = dict_event_create(dict, set);
+ dict_op_settings_dup(set, &ctx->set);
+ guid_128_generate(guid);
+ event_add_str(ctx->event, "txid", guid_128_to_string(guid));
+ event_set_name(ctx->event, "dict_transaction_started");
+ e_debug(ctx->event, "Starting transaction");
+ return ctx;
+}
+
+void dict_transaction_no_slowness_warning(struct dict_transaction_context *ctx)
+{
+ ctx->no_slowness_warning = TRUE;
+}
+
+void dict_transaction_set_timestamp(struct dict_transaction_context *ctx,
+ const struct timespec *ts)
+{
+ /* These asserts are mainly here to guarantee a possibility in future
+ to change the API to support multiple timestamps within the same
+ transaction, so this call would apply only to the following
+ changes. */
+ i_assert(!ctx->changed);
+ i_assert(ctx->timestamp.tv_sec == 0);
+ i_assert(ts->tv_sec > 0);
+
+ ctx->timestamp = *ts;
+ struct event_passthrough *e = event_create_passthrough(ctx->event)->
+ set_name("dict_set_timestamp");
+
+ e_debug(e->event(), "Setting timestamp on transaction to (%"PRIdTIME_T", %ld)",
+ ts->tv_sec, ts->tv_nsec);
+ if (ctx->dict->v.set_timestamp != NULL)
+ ctx->dict->v.set_timestamp(ctx, ts);
+}
+
+struct dict_commit_sync_result {
+ int ret;
+ char *error;
+};
+
+static void
+dict_transaction_commit_sync_callback(const struct dict_commit_result *result,
+ void *context)
+{
+ struct dict_commit_sync_result *sync_result = context;
+
+ sync_result->ret = result->ret;
+ sync_result->error = i_strdup(result->error);
+}
+
+int dict_transaction_commit(struct dict_transaction_context **_ctx,
+ const char **error_r)
+{
+ pool_t pool = pool_alloconly_create("dict_commit_callback_ctx", 64);
+ struct dict_commit_callback_ctx *cctx =
+ p_new(pool, struct dict_commit_callback_ctx, 1);
+ struct dict_transaction_context *ctx = *_ctx;
+ struct dict_commit_sync_result result;
+
+ *_ctx = NULL;
+ cctx->pool = pool;
+ i_zero(&result);
+ i_assert(ctx->dict->transaction_count > 0);
+ ctx->dict->transaction_count--;
+ DLLIST_REMOVE(&ctx->dict->transactions, ctx);
+ DLLIST_PREPEND(&ctx->dict->commits, cctx);
+ cctx->dict = ctx->dict;
+ dict_ref(cctx->dict);
+ cctx->callback = dict_transaction_commit_sync_callback;
+ cctx->context = &result;
+ cctx->event = ctx->event;
+ cctx->set = ctx->set;
+
+ ctx->dict->v.transaction_commit(ctx, FALSE, dict_commit_callback, cctx);
+ *error_r = t_strdup(result.error);
+ i_free(result.error);
+ return result.ret;
+}
+
+#undef dict_transaction_commit_async
+void dict_transaction_commit_async(struct dict_transaction_context **_ctx,
+ dict_transaction_commit_callback_t *callback,
+ void *context)
+{
+ pool_t pool = pool_alloconly_create("dict_commit_callback_ctx", 64);
+ struct dict_commit_callback_ctx *cctx =
+ p_new(pool, struct dict_commit_callback_ctx, 1);
+ struct dict_transaction_context *ctx = *_ctx;
+
+ *_ctx = NULL;
+ i_assert(ctx->dict->transaction_count > 0);
+ ctx->dict->transaction_count--;
+ DLLIST_REMOVE(&ctx->dict->transactions, ctx);
+ DLLIST_PREPEND(&ctx->dict->commits, cctx);
+ if (callback == NULL)
+ callback = dict_transaction_commit_async_noop_callback;
+ cctx->pool = pool;
+ cctx->dict = ctx->dict;
+ dict_ref(cctx->dict);
+ cctx->callback = callback;
+ cctx->context = context;
+ cctx->event = ctx->event;
+ cctx->set = ctx->set;
+ cctx->delayed_callback = TRUE;
+ ctx->dict->v.transaction_commit(ctx, TRUE, dict_commit_callback, cctx);
+ cctx->delayed_callback = FALSE;
+}
+
+void dict_transaction_commit_async_nocallback(
+ struct dict_transaction_context **ctx)
+{
+ dict_transaction_commit_async(ctx, NULL, NULL);
+}
+
+void dict_transaction_rollback(struct dict_transaction_context **_ctx)
+{
+ struct dict_transaction_context *ctx = *_ctx;
+
+ if (ctx == NULL)
+ return;
+
+ struct event *event = ctx->event;
+
+ *_ctx = NULL;
+ i_assert(ctx->dict->transaction_count > 0);
+ ctx->dict->transaction_count--;
+ DLLIST_REMOVE(&ctx->dict->transactions, ctx);
+ struct dict_op_settings_private set_copy = ctx->set;
+ ctx->dict->v.transaction_rollback(ctx);
+ dict_transaction_finished(event, DICT_COMMIT_RET_OK, TRUE, NULL);
+ dict_op_settings_private_free(&set_copy);
+ event_unref(&event);
+}
+
+void dict_set(struct dict_transaction_context *ctx,
+ const char *key, const char *value)
+{
+ i_assert(dict_key_prefix_is_valid(key, ctx->set.username));
+ struct event_passthrough *e = event_create_passthrough(ctx->event)->
+ set_name("dict_set_key")->
+ add_str("key", key);
+
+ e_debug(e->event(), "Setting '%s' to '%s'", key, value);
+
+ T_BEGIN {
+ ctx->dict->v.set(ctx, key, value);
+ } T_END;
+ ctx->changed = TRUE;
+}
+
+void dict_unset(struct dict_transaction_context *ctx,
+ const char *key)
+{
+ i_assert(dict_key_prefix_is_valid(key, ctx->set.username));
+ struct event_passthrough *e = event_create_passthrough(ctx->event)->
+ set_name("dict_unset_key")->
+ add_str("key", key);
+
+ e_debug(e->event(), "Unsetting '%s'", key);
+
+ T_BEGIN {
+ ctx->dict->v.unset(ctx, key);
+ } T_END;
+ ctx->changed = TRUE;
+}
+
+void dict_atomic_inc(struct dict_transaction_context *ctx,
+ const char *key, long long diff)
+{
+ i_assert(dict_key_prefix_is_valid(key, ctx->set.username));
+ struct event_passthrough *e = event_create_passthrough(ctx->event)->
+ set_name("dict_increment_key")->
+ add_str("key", key);
+
+ e_debug(e->event(), "Incrementing '%s' with %lld", key, diff);
+
+ if (diff != 0) T_BEGIN {
+ ctx->dict->v.atomic_inc(ctx, key, diff);
+ ctx->changed = TRUE;
+ } T_END;
+}
+
+const char *dict_escape_string(const char *str)
+{
+ const char *p;
+ string_t *ret;
+
+ /* see if we need to escape it */
+ for (p = str; *p != '\0'; p++) {
+ if (*p == '/' || *p == '\\')
+ break;
+ }
+
+ if (*p == '\0')
+ return str;
+
+ /* escape */
+ ret = t_str_new((size_t) (p - str) + 128);
+ str_append_data(ret, str, (size_t) (p - str));
+
+ for (; *p != '\0'; p++) {
+ switch (*p) {
+ case '/':
+ str_append_c(ret, '\\');
+ str_append_c(ret, '|');
+ break;
+ case '\\':
+ str_append_c(ret, '\\');
+ str_append_c(ret, '\\');
+ break;
+ default:
+ str_append_c(ret, *p);
+ break;
+ }
+ }
+ return str_c(ret);
+}
+
+const char *dict_unescape_string(const char *str)
+{
+ const char *p;
+ string_t *ret;
+
+ /* see if we need to unescape it */
+ for (p = str; *p != '\0'; p++) {
+ if (*p == '\\')
+ break;
+ }
+
+ if (*p == '\0')
+ return str;
+
+ /* unescape */
+ ret = t_str_new((size_t) (p - str) + strlen(p) + 1);
+ str_append_data(ret, str, (size_t) (p - str));
+
+ for (; *p != '\0'; p++) {
+ if (*p != '\\')
+ str_append_c(ret, *p);
+ else {
+ if (*++p == '|')
+ str_append_c(ret, '/');
+ else if (*p == '\0')
+ break;
+ else
+ str_append_c(ret, *p);
+ }
+ }
+ return str_c(ret);
+}
+
+void dict_op_settings_dup(const struct dict_op_settings *source,
+ struct dict_op_settings_private *dest_r)
+{
+ i_zero(dest_r);
+ dest_r->username = i_strdup(source->username);
+ dest_r->home_dir = i_strdup(source->home_dir);
+}
+
+void dict_op_settings_private_free(struct dict_op_settings_private *set)
+{
+ i_free(set->username);
+ i_free(set->home_dir);
+}
diff --git a/src/lib-dict/dict.h b/src/lib-dict/dict.h
new file mode 100644
index 0000000..8e4b39e
--- /dev/null
+++ b/src/lib-dict/dict.h
@@ -0,0 +1,200 @@
+#ifndef DICT_H
+#define DICT_H
+
+#define DICT_PATH_PRIVATE "priv/"
+#define DICT_PATH_SHARED "shared/"
+
+struct timespec;
+struct dict;
+struct dict_iterate_context;
+
+enum dict_iterate_flags {
+ /* Recurse to all the sub-hierarchies (e.g. iterating "foo/" will
+ return "foo/a", but should it return "foo/a/b"?) */
+ DICT_ITERATE_FLAG_RECURSE = 0x01,
+ /* Sort returned results by key */
+ DICT_ITERATE_FLAG_SORT_BY_KEY = 0x02,
+ /* Sort returned results by value */
+ DICT_ITERATE_FLAG_SORT_BY_VALUE = 0x04,
+ /* Don't return values, only keys */
+ DICT_ITERATE_FLAG_NO_VALUE = 0x08,
+ /* Don't recurse at all. This is basically the same as dict_lookup(),
+ but it'll return all the rows instead of only the first one. */
+ DICT_ITERATE_FLAG_EXACT_KEY = 0x10,
+ /* Perform iteration asynchronously. */
+ DICT_ITERATE_FLAG_ASYNC = 0x20
+};
+
+enum dict_data_type {
+ DICT_DATA_TYPE_STRING = 0,
+ DICT_DATA_TYPE_UINT32,
+ DICT_DATA_TYPE_LAST
+};
+
+struct dict_settings {
+ const char *base_dir;
+ /* set to parent event, if exists */
+ struct event *event_parent;
+};
+
+struct dict_op_settings {
+ const char *username;
+ /* home directory for the user, if known */
+ const char *home_dir;
+};
+
+struct dict_lookup_result {
+ int ret;
+
+ /* First returned value (ret > 0) */
+ const char *value;
+ /* NULL-terminated list of all returned values (ret > 0) */
+ const char *const *values;
+
+ /* Error message for a failed lookup (ret < 0) */
+ const char *error;
+};
+
+enum dict_commit_ret {
+ DICT_COMMIT_RET_OK = 1,
+ DICT_COMMIT_RET_NOTFOUND = 0,
+ DICT_COMMIT_RET_FAILED = -1,
+ /* write may or may not have succeeded (e.g. write timeout or
+ disconnected from server) */
+ DICT_COMMIT_RET_WRITE_UNCERTAIN = -2,
+};
+
+struct dict_commit_result {
+ enum dict_commit_ret ret;
+ const char *error;
+};
+
+typedef void dict_lookup_callback_t(const struct dict_lookup_result *result,
+ void *context);
+typedef void dict_iterate_callback_t(void *context);
+typedef void
+dict_transaction_commit_callback_t(const struct dict_commit_result *result,
+ void *context);
+
+void dict_driver_register(struct dict *driver);
+void dict_driver_unregister(struct dict *driver);
+
+void dict_drivers_register_builtin(void);
+void dict_drivers_unregister_builtin(void);
+
+void dict_drivers_register_all(void);
+void dict_drivers_unregister_all(void);
+
+/* Open dictionary with given URI (type:data).
+ Returns 0 if ok, -1 if URI is invalid. */
+int dict_init(const char *uri, const struct dict_settings *set,
+ struct dict **dict_r, const char **error_r);
+/* Close dictionary. */
+void dict_deinit(struct dict **dict);
+/* Wait for all pending asynchronous operations to finish. */
+void dict_wait(struct dict *dict);
+/* Switch the dict to the current ioloop. This can be used to do dict_wait()
+ among other IO work. Returns TRUE if there is actually some work that can
+ be waited on. */
+bool dict_switch_ioloop(struct dict *dict) ATTR_NOWARN_UNUSED_RESULT;
+
+/* Lookup value for key. Set it to NULL if it's not found.
+ Returns 1 if found, 0 if not found and -1 if lookup failed. */
+int dict_lookup(struct dict *dict, const struct dict_op_settings *set, pool_t pool,
+ const char *key, const char **value_r, const char **error_r);
+void dict_lookup_async(struct dict *dict, const struct dict_op_settings *set,
+ const char *key, dict_lookup_callback_t *callback,
+ void *context);
+#define dict_lookup_async(dict, set, key, callback, context) \
+ dict_lookup_async(dict, set, key, (dict_lookup_callback_t *)(callback), \
+ 1 ? (context) : \
+ CALLBACK_TYPECHECK(callback, \
+ void (*)(const struct dict_lookup_result *, typeof(context))))
+
+/* Iterate through all values in a path. flag indicates how iteration
+ is carried out */
+struct dict_iterate_context *
+dict_iterate_init(struct dict *dict, const struct dict_op_settings *set,
+ const char *path, enum dict_iterate_flags flags);
+/* Set async callback. Note that if dict_iterate_init() already did all the
+ work, this callback may never be called. So after dict_iterate_init() you
+ should call dict_iterate() in any case to see if all the results are
+ already available. */
+void dict_iterate_set_async_callback(struct dict_iterate_context *ctx,
+ dict_iterate_callback_t *callback,
+ void *context);
+#define dict_iterate_set_async_callback(ctx, callback, context) \
+ dict_iterate_set_async_callback(ctx, (dict_iterate_callback_t *)(callback), \
+ 1 ? (context) : \
+ CALLBACK_TYPECHECK(callback, void (*)(typeof(context))))
+/* Limit how many rows will be returned by the iteration (0 = unlimited).
+ This allows backends to optimize the query (e.g. use LIMIT 1 with SQL). */
+void dict_iterate_set_limit(struct dict_iterate_context *ctx,
+ uint64_t max_rows);
+/* If dict_iterate() returns FALSE, the iteration may be finished or if this
+ is an async iteration it may be waiting for more data. If this function
+ returns TRUE, the dict callback is called again with more data. If dict
+ supports multiple values, dict_iterate_values() can be used to return all
+ of them. dict_iterate() returns only the first value and ignores the rest. */
+bool dict_iterate_has_more(struct dict_iterate_context *ctx);
+bool dict_iterate(struct dict_iterate_context *ctx,
+ const char **key_r, const char **value_r);
+bool dict_iterate_values(struct dict_iterate_context *ctx,
+ const char **key_r, const char *const **values_r);
+/* Returns 0 = ok, -1 = iteration failed */
+int dict_iterate_deinit(struct dict_iterate_context **ctx, const char **error_r);
+
+/* Start a new dictionary transaction. */
+struct dict_transaction_context *
+dict_transaction_begin(struct dict *dict, const struct dict_op_settings *set);
+/* Don't log a warning if the transaction commit took a long time.
+ This is needed if there are no guarantees that an asynchronous commit will
+ finish up anytime soon. Mainly useful for transactions which aren't
+ especially important whether they finish or not. */
+void dict_transaction_no_slowness_warning(struct dict_transaction_context *ctx);
+/* Set write timestamp for the entire transaction. This must be set before
+ any changes are done and can't be changed afterwards. Currently only
+ dict-sql with Cassandra backend does anything with this. */
+void dict_transaction_set_timestamp(struct dict_transaction_context *ctx,
+ const struct timespec *ts);
+/* Commit the transaction. Returns 1 if ok, 0 if dict_atomic_inc() was used
+ on a nonexistent key, -1 if failed. */
+int dict_transaction_commit(struct dict_transaction_context **ctx,
+ const char **error_r);
+/* Commit the transaction, but don't wait to see if it finishes successfully.
+ The callback is called when the transaction is finished. If it's not called
+ by the time you want to deinitialize dict, call dict_flush() to wait for the
+ result. */
+void dict_transaction_commit_async(struct dict_transaction_context **ctx,
+ dict_transaction_commit_callback_t *callback,
+ void *context) ATTR_NULL(2, 3);
+#define dict_transaction_commit_async(ctx, callback, context) \
+ dict_transaction_commit_async(ctx, (dict_transaction_commit_callback_t *)(callback), \
+ 1 ? (context) : \
+ CALLBACK_TYPECHECK(callback, \
+ void (*)(const struct dict_commit_result *, typeof(context))))
+/* Same as dict_transaction_commit_async(), but don't call a callback. */
+void dict_transaction_commit_async_nocallback(
+ struct dict_transaction_context **ctx);
+/* Rollback all changes made in transaction. */
+void dict_transaction_rollback(struct dict_transaction_context **ctx);
+
+/* Set key=value in dictionary. */
+void dict_set(struct dict_transaction_context *ctx,
+ const char *key, const char *value);
+/* Unset a record in dictionary, identified by key*/
+void dict_unset(struct dict_transaction_context *ctx,
+ const char *key);
+/* Increase/decrease a numeric value in dictionary. Note that the value is
+ changed when transaction is being committed, so you can't know beforehand
+ what the value will become. The value is updated only if it already exists,
+ otherwise commit() will return 0. */
+void dict_atomic_inc(struct dict_transaction_context *ctx,
+ const char *key, long long diff);
+
+/* Escape/unescape '/' characters in a string, so that it can be safely added
+ into path components in dict keys. */
+const char *dict_escape_string(const char *str);
+const char *dict_unescape_string(const char *str);
+
+#endif
diff --git a/src/lib-dict/test-dict-client.c b/src/lib-dict/test-dict-client.c
new file mode 100644
index 0000000..9b3b80b
--- /dev/null
+++ b/src/lib-dict/test-dict-client.c
@@ -0,0 +1,106 @@
+/* Copyright (c) 2016-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "ioloop.h"
+#include "dict-private.h"
+
+#include <stdio.h>
+
+static int pending = 0;
+
+static void lookup_callback(const struct dict_lookup_result *result,
+ void *context ATTR_UNUSED)
+{
+ if (result->error != NULL)
+ i_error("%s", result->error);
+ /*else if (result->ret == 0)
+ i_info("not found");
+ else
+ i_info("%s", result->value);*/
+ pending--;
+}
+
+static void commit_callback(const struct dict_commit_result *result,
+ void *context ATTR_UNUSED)
+{
+ if (result->ret < 0)
+ i_error("commit %d", result->ret);
+ pending--;
+}
+
+int main(int argc, char *argv[])
+{
+ const char *prefix, *uri;
+ struct dict *dict;
+ struct dict_settings set;
+ struct dict_op_settings opset;
+ struct ioloop *ioloop;
+ const char *error;
+ unsigned int i;
+ char key[1000], value[100];
+
+ lib_init();
+ ioloop = io_loop_create();
+ dict_driver_register(&dict_driver_client);
+
+ if (argc < 3)
+ i_fatal("Usage: <prefix> <uri>");
+ prefix = argv[1];
+ uri = argv[2];
+
+ i_zero(&set);
+ i_zero(&opset);
+ set.base_dir = "/var/run/dovecot";
+ opset.username = "testuser";
+
+ if (dict_init(uri, &set, &dict, &error) < 0)
+ i_fatal("dict_init(%s) failed: %s", argv[1], error);
+
+ for (i = 0;; i++) {
+ i_snprintf(key, sizeof(key), "%s/%02x", prefix,
+ i_rand_limit(0xff));
+ i_snprintf(value, sizeof(value), "%04x", i_rand_limit(0xffff));
+ switch (i_rand_limit(4)) {
+ case 0:
+ pending++;
+ dict_lookup_async(dict, NULL, key, lookup_callback, NULL);
+ break;
+ case 1: {
+ struct dict_transaction_context *trans;
+
+ pending++;
+ trans = dict_transaction_begin(dict, &opset);
+ dict_set(trans, key, value);
+ dict_transaction_commit_async(&trans, commit_callback, NULL);
+ break;
+ }
+ case 2: {
+ struct dict_transaction_context *trans;
+
+ pending++;
+ trans = dict_transaction_begin(dict, &opset);
+ dict_unset(trans, key);
+ dict_transaction_commit_async(&trans, commit_callback, NULL);
+ break;
+ }
+ case 3: {
+ struct dict_iterate_context *iter;
+ const char *k, *v;
+
+ iter = dict_iterate_init(dict, &opset, prefix, DICT_ITERATE_FLAG_EXACT_KEY);
+ while (dict_iterate(iter, &k, &v)) ;
+ if (dict_iterate_deinit(&iter, &error) < 0)
+ i_error("iter failed: %s", error);
+ break;
+ }
+ }
+ while (pending > 100) {
+ dict_wait(dict);
+ printf("%d\n", pending); fflush(stdout);
+ }
+ }
+ dict_deinit(&dict);
+
+ io_loop_destroy(&ioloop);
+ lib_deinit();
+}
diff --git a/src/lib-dict/test-dict.c b/src/lib-dict/test-dict.c
new file mode 100644
index 0000000..04864ff
--- /dev/null
+++ b/src/lib-dict/test-dict.c
@@ -0,0 +1,46 @@
+/* Copyright (c) 2010-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "dict-private.h"
+#include "test-common.h"
+
+struct dict dict_driver_client;
+struct dict dict_driver_file;
+struct dict dict_driver_memcached;
+struct dict dict_driver_memcached_ascii;
+struct dict dict_driver_redis;
+
+static void test_dict_escape(void)
+{
+ static const char *input[] = {
+ "", "",
+ "foo", "foo",
+ "foo\\", "foo\\\\",
+ "foo\\bar", "foo\\\\bar",
+ "\\bar", "\\\\bar",
+ "foo/", "foo\\|",
+ "foo/bar", "foo\\|bar",
+ "/bar", "\\|bar",
+ "////", "\\|\\|\\|\\|",
+ "/", "\\|"
+ };
+ unsigned int i;
+
+ test_begin("dict escape");
+ for (i = 0; i < N_ELEMENTS(input); i += 2) {
+ test_assert(strcmp(dict_escape_string(input[i]), input[i+1]) == 0);
+ test_assert(strcmp(dict_unescape_string(input[i+1]), input[i]) == 0);
+ }
+ test_assert(strcmp(dict_unescape_string("x\\"), "x") == 0);
+ test_assert(strcmp(dict_unescape_string("\\"), "") == 0);
+ test_end();
+}
+
+int main(void)
+{
+ static void (*const test_functions[])(void) = {
+ test_dict_escape,
+ NULL
+ };
+ return test_run(test_functions);
+}
diff --git a/src/lib-dns/Makefile.am b/src/lib-dns/Makefile.am
new file mode 100644
index 0000000..509f7b0
--- /dev/null
+++ b/src/lib-dns/Makefile.am
@@ -0,0 +1,36 @@
+noinst_LTLIBRARIES = libdns.la
+
+AM_CPPFLAGS = \
+ -I$(top_srcdir)/src/lib
+
+libdns_la_SOURCES = \
+ dns-lookup.c \
+ dns-util.c
+
+headers = \
+ dns-lookup.h \
+ dns-util.h
+
+test_programs = \
+ test-dns-util
+
+noinst_PROGRAMS = $(test_programs)
+
+test_libs = \
+ libdns.la \
+ ../lib-test/libtest.la \
+ ../lib/liblib.la
+
+test_dns_util_SOURCE = test-dns-util.c
+test_dns_util_LDADD = $(test_libs)
+test_dns_util_CFLAGS = $(AM_CPPFLAGS) \
+ -I$(top_srcdir)/src/lib-test
+
+check-local:
+ for bin in $(test_programs); do \
+ if ! $(RUN_TEST) ./$$bin; then exit 1; fi; \
+ done
+
+
+pkginc_libdir=$(pkgincludedir)
+pkginc_lib_HEADERS = $(headers)
diff --git a/src/lib-dns/Makefile.in b/src/lib-dns/Makefile.in
new file mode 100644
index 0000000..fe45740
--- /dev/null
+++ b/src/lib-dns/Makefile.in
@@ -0,0 +1,874 @@
+# Makefile.in generated by automake 1.16.1 from Makefile.am.
+# @configure_input@
+
+# Copyright (C) 1994-2018 Free Software Foundation, Inc.
+
+# This Makefile.in is free software; the Free Software Foundation
+# gives unlimited permission to copy and/or distribute it,
+# with or without modifications, as long as this notice is preserved.
+
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY, to the extent permitted by law; without
+# even the implied warranty of MERCHANTABILITY or FITNESS FOR A
+# PARTICULAR PURPOSE.
+
+@SET_MAKE@
+
+
+
+VPATH = @srcdir@
+am__is_gnu_make = { \
+ if test -z '$(MAKELEVEL)'; then \
+ false; \
+ elif test -n '$(MAKE_HOST)'; then \
+ true; \
+ elif test -n '$(MAKE_VERSION)' && test -n '$(CURDIR)'; then \
+ true; \
+ else \
+ false; \
+ fi; \
+}
+am__make_running_with_option = \
+ case $${target_option-} in \
+ ?) ;; \
+ *) echo "am__make_running_with_option: internal error: invalid" \
+ "target option '$${target_option-}' specified" >&2; \
+ exit 1;; \
+ esac; \
+ has_opt=no; \
+ sane_makeflags=$$MAKEFLAGS; \
+ if $(am__is_gnu_make); then \
+ sane_makeflags=$$MFLAGS; \
+ else \
+ case $$MAKEFLAGS in \
+ *\\[\ \ ]*) \
+ bs=\\; \
+ sane_makeflags=`printf '%s\n' "$$MAKEFLAGS" \
+ | sed "s/$$bs$$bs[$$bs $$bs ]*//g"`;; \
+ esac; \
+ fi; \
+ skip_next=no; \
+ strip_trailopt () \
+ { \
+ flg=`printf '%s\n' "$$flg" | sed "s/$$1.*$$//"`; \
+ }; \
+ for flg in $$sane_makeflags; do \
+ test $$skip_next = yes && { skip_next=no; continue; }; \
+ case $$flg in \
+ *=*|--*) continue;; \
+ -*I) strip_trailopt 'I'; skip_next=yes;; \
+ -*I?*) strip_trailopt 'I';; \
+ -*O) strip_trailopt 'O'; skip_next=yes;; \
+ -*O?*) strip_trailopt 'O';; \
+ -*l) strip_trailopt 'l'; skip_next=yes;; \
+ -*l?*) strip_trailopt 'l';; \
+ -[dEDm]) skip_next=yes;; \
+ -[JT]) skip_next=yes;; \
+ esac; \
+ case $$flg in \
+ *$$target_option*) has_opt=yes; break;; \
+ esac; \
+ done; \
+ test $$has_opt = yes
+am__make_dryrun = (target_option=n; $(am__make_running_with_option))
+am__make_keepgoing = (target_option=k; $(am__make_running_with_option))
+pkgdatadir = $(datadir)/@PACKAGE@
+pkgincludedir = $(includedir)/@PACKAGE@
+pkglibdir = $(libdir)/@PACKAGE@
+pkglibexecdir = $(libexecdir)/@PACKAGE@
+am__cd = CDPATH="$${ZSH_VERSION+.}$(PATH_SEPARATOR)" && cd
+install_sh_DATA = $(install_sh) -c -m 644
+install_sh_PROGRAM = $(install_sh) -c
+install_sh_SCRIPT = $(install_sh) -c
+INSTALL_HEADER = $(INSTALL_DATA)
+transform = $(program_transform_name)
+NORMAL_INSTALL = :
+PRE_INSTALL = :
+POST_INSTALL = :
+NORMAL_UNINSTALL = :
+PRE_UNINSTALL = :
+POST_UNINSTALL = :
+build_triplet = @build@
+host_triplet = @host@
+noinst_PROGRAMS = $(am__EXEEXT_1)
+subdir = src/lib-dns
+ACLOCAL_M4 = $(top_srcdir)/aclocal.m4
+am__aclocal_m4_deps = $(top_srcdir)/m4/ac_checktype2.m4 \
+ $(top_srcdir)/m4/ac_typeof.m4 $(top_srcdir)/m4/arc4random.m4 \
+ $(top_srcdir)/m4/blockdev.m4 $(top_srcdir)/m4/c99_vsnprintf.m4 \
+ $(top_srcdir)/m4/clock_gettime.m4 $(top_srcdir)/m4/crypt.m4 \
+ $(top_srcdir)/m4/crypt_xpg6.m4 $(top_srcdir)/m4/dbqlk.m4 \
+ $(top_srcdir)/m4/dirent_dtype.m4 $(top_srcdir)/m4/dovecot.m4 \
+ $(top_srcdir)/m4/fd_passing.m4 $(top_srcdir)/m4/fdatasync.m4 \
+ $(top_srcdir)/m4/flexible_array_member.m4 \
+ $(top_srcdir)/m4/glibc.m4 $(top_srcdir)/m4/gmtime_max.m4 \
+ $(top_srcdir)/m4/gmtime_tm_gmtoff.m4 \
+ $(top_srcdir)/m4/ioloop.m4 $(top_srcdir)/m4/iovec.m4 \
+ $(top_srcdir)/m4/ipv6.m4 $(top_srcdir)/m4/libcap.m4 \
+ $(top_srcdir)/m4/libtool.m4 $(top_srcdir)/m4/libwrap.m4 \
+ $(top_srcdir)/m4/linux_mremap.m4 $(top_srcdir)/m4/ltoptions.m4 \
+ $(top_srcdir)/m4/ltsugar.m4 $(top_srcdir)/m4/ltversion.m4 \
+ $(top_srcdir)/m4/lt~obsolete.m4 $(top_srcdir)/m4/mmap_write.m4 \
+ $(top_srcdir)/m4/mntctl.m4 $(top_srcdir)/m4/modules.m4 \
+ $(top_srcdir)/m4/notify.m4 $(top_srcdir)/m4/nsl.m4 \
+ $(top_srcdir)/m4/off_t_max.m4 $(top_srcdir)/m4/pkg.m4 \
+ $(top_srcdir)/m4/pr_set_dumpable.m4 \
+ $(top_srcdir)/m4/q_quotactl.m4 $(top_srcdir)/m4/quota.m4 \
+ $(top_srcdir)/m4/random.m4 $(top_srcdir)/m4/rlimit.m4 \
+ $(top_srcdir)/m4/sendfile.m4 $(top_srcdir)/m4/size_t_signed.m4 \
+ $(top_srcdir)/m4/sockpeercred.m4 $(top_srcdir)/m4/sql.m4 \
+ $(top_srcdir)/m4/ssl.m4 $(top_srcdir)/m4/st_tim.m4 \
+ $(top_srcdir)/m4/static_array.m4 $(top_srcdir)/m4/test_with.m4 \
+ $(top_srcdir)/m4/time_t.m4 $(top_srcdir)/m4/typeof.m4 \
+ $(top_srcdir)/m4/typeof_dev_t.m4 \
+ $(top_srcdir)/m4/uoff_t_max.m4 $(top_srcdir)/m4/vararg.m4 \
+ $(top_srcdir)/m4/want_apparmor.m4 \
+ $(top_srcdir)/m4/want_bsdauth.m4 \
+ $(top_srcdir)/m4/want_bzlib.m4 \
+ $(top_srcdir)/m4/want_cassandra.m4 \
+ $(top_srcdir)/m4/want_cdb.m4 \
+ $(top_srcdir)/m4/want_checkpassword.m4 \
+ $(top_srcdir)/m4/want_clucene.m4 $(top_srcdir)/m4/want_db.m4 \
+ $(top_srcdir)/m4/want_gssapi.m4 $(top_srcdir)/m4/want_icu.m4 \
+ $(top_srcdir)/m4/want_ldap.m4 $(top_srcdir)/m4/want_lua.m4 \
+ $(top_srcdir)/m4/want_lz4.m4 $(top_srcdir)/m4/want_lzma.m4 \
+ $(top_srcdir)/m4/want_mysql.m4 $(top_srcdir)/m4/want_pam.m4 \
+ $(top_srcdir)/m4/want_passwd.m4 $(top_srcdir)/m4/want_pgsql.m4 \
+ $(top_srcdir)/m4/want_prefetch.m4 \
+ $(top_srcdir)/m4/want_shadow.m4 \
+ $(top_srcdir)/m4/want_sodium.m4 $(top_srcdir)/m4/want_solr.m4 \
+ $(top_srcdir)/m4/want_sqlite.m4 \
+ $(top_srcdir)/m4/want_stemmer.m4 \
+ $(top_srcdir)/m4/want_systemd.m4 \
+ $(top_srcdir)/m4/want_textcat.m4 \
+ $(top_srcdir)/m4/want_unwind.m4 $(top_srcdir)/m4/want_zlib.m4 \
+ $(top_srcdir)/m4/want_zstd.m4 $(top_srcdir)/configure.ac
+am__configure_deps = $(am__aclocal_m4_deps) $(CONFIGURE_DEPENDENCIES) \
+ $(ACLOCAL_M4)
+DIST_COMMON = $(srcdir)/Makefile.am $(pkginc_lib_HEADERS) \
+ $(am__DIST_COMMON)
+mkinstalldirs = $(install_sh) -d
+CONFIG_HEADER = $(top_builddir)/config.h
+CONFIG_CLEAN_FILES =
+CONFIG_CLEAN_VPATH_FILES =
+am__EXEEXT_1 = test-dns-util$(EXEEXT)
+PROGRAMS = $(noinst_PROGRAMS)
+LTLIBRARIES = $(noinst_LTLIBRARIES)
+libdns_la_LIBADD =
+am_libdns_la_OBJECTS = dns-lookup.lo dns-util.lo
+libdns_la_OBJECTS = $(am_libdns_la_OBJECTS)
+AM_V_lt = $(am__v_lt_@AM_V@)
+am__v_lt_ = $(am__v_lt_@AM_DEFAULT_V@)
+am__v_lt_0 = --silent
+am__v_lt_1 =
+test_dns_util_SOURCES = test-dns-util.c
+test_dns_util_OBJECTS = test_dns_util-test-dns-util.$(OBJEXT)
+test_dns_util_DEPENDENCIES = $(test_libs)
+test_dns_util_LINK = $(LIBTOOL) $(AM_V_lt) --tag=CC $(AM_LIBTOOLFLAGS) \
+ $(LIBTOOLFLAGS) --mode=link $(CCLD) $(test_dns_util_CFLAGS) \
+ $(CFLAGS) $(AM_LDFLAGS) $(LDFLAGS) -o $@
+AM_V_P = $(am__v_P_@AM_V@)
+am__v_P_ = $(am__v_P_@AM_DEFAULT_V@)
+am__v_P_0 = false
+am__v_P_1 = :
+AM_V_GEN = $(am__v_GEN_@AM_V@)
+am__v_GEN_ = $(am__v_GEN_@AM_DEFAULT_V@)
+am__v_GEN_0 = @echo " GEN " $@;
+am__v_GEN_1 =
+AM_V_at = $(am__v_at_@AM_V@)
+am__v_at_ = $(am__v_at_@AM_DEFAULT_V@)
+am__v_at_0 = @
+am__v_at_1 =
+DEFAULT_INCLUDES = -I.@am__isrc@ -I$(top_builddir)
+depcomp = $(SHELL) $(top_srcdir)/depcomp
+am__maybe_remake_depfiles = depfiles
+am__depfiles_remade = ./$(DEPDIR)/dns-lookup.Plo \
+ ./$(DEPDIR)/dns-util.Plo \
+ ./$(DEPDIR)/test_dns_util-test-dns-util.Po
+am__mv = mv -f
+COMPILE = $(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) \
+ $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS)
+LTCOMPILE = $(LIBTOOL) $(AM_V_lt) --tag=CC $(AM_LIBTOOLFLAGS) \
+ $(LIBTOOLFLAGS) --mode=compile $(CC) $(DEFS) \
+ $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) $(CPPFLAGS) \
+ $(AM_CFLAGS) $(CFLAGS)
+AM_V_CC = $(am__v_CC_@AM_V@)
+am__v_CC_ = $(am__v_CC_@AM_DEFAULT_V@)
+am__v_CC_0 = @echo " CC " $@;
+am__v_CC_1 =
+CCLD = $(CC)
+LINK = $(LIBTOOL) $(AM_V_lt) --tag=CC $(AM_LIBTOOLFLAGS) \
+ $(LIBTOOLFLAGS) --mode=link $(CCLD) $(AM_CFLAGS) $(CFLAGS) \
+ $(AM_LDFLAGS) $(LDFLAGS) -o $@
+AM_V_CCLD = $(am__v_CCLD_@AM_V@)
+am__v_CCLD_ = $(am__v_CCLD_@AM_DEFAULT_V@)
+am__v_CCLD_0 = @echo " CCLD " $@;
+am__v_CCLD_1 =
+SOURCES = $(libdns_la_SOURCES) test-dns-util.c
+DIST_SOURCES = $(libdns_la_SOURCES) test-dns-util.c
+am__can_run_installinfo = \
+ case $$AM_UPDATE_INFO_DIR in \
+ n|no|NO) false;; \
+ *) (install-info --version) >/dev/null 2>&1;; \
+ esac
+am__vpath_adj_setup = srcdirstrip=`echo "$(srcdir)" | sed 's|.|.|g'`;
+am__vpath_adj = case $$p in \
+ $(srcdir)/*) f=`echo "$$p" | sed "s|^$$srcdirstrip/||"`;; \
+ *) f=$$p;; \
+ esac;
+am__strip_dir = f=`echo $$p | sed -e 's|^.*/||'`;
+am__install_max = 40
+am__nobase_strip_setup = \
+ srcdirstrip=`echo "$(srcdir)" | sed 's/[].[^$$\\*|]/\\\\&/g'`
+am__nobase_strip = \
+ for p in $$list; do echo "$$p"; done | sed -e "s|$$srcdirstrip/||"
+am__nobase_list = $(am__nobase_strip_setup); \
+ for p in $$list; do echo "$$p $$p"; done | \
+ sed "s| $$srcdirstrip/| |;"' / .*\//!s/ .*/ ./; s,\( .*\)/[^/]*$$,\1,' | \
+ $(AWK) 'BEGIN { files["."] = "" } { files[$$2] = files[$$2] " " $$1; \
+ if (++n[$$2] == $(am__install_max)) \
+ { print $$2, files[$$2]; n[$$2] = 0; files[$$2] = "" } } \
+ END { for (dir in files) print dir, files[dir] }'
+am__base_list = \
+ sed '$$!N;$$!N;$$!N;$$!N;$$!N;$$!N;$$!N;s/\n/ /g' | \
+ sed '$$!N;$$!N;$$!N;$$!N;s/\n/ /g'
+am__uninstall_files_from_dir = { \
+ test -z "$$files" \
+ || { test ! -d "$$dir" && test ! -f "$$dir" && test ! -r "$$dir"; } \
+ || { echo " ( cd '$$dir' && rm -f" $$files ")"; \
+ $(am__cd) "$$dir" && rm -f $$files; }; \
+ }
+am__installdirs = "$(DESTDIR)$(pkginc_libdir)"
+HEADERS = $(pkginc_lib_HEADERS)
+am__tagged_files = $(HEADERS) $(SOURCES) $(TAGS_FILES) $(LISP)
+# Read a list of newline-separated strings from the standard input,
+# and print each of them once, without duplicates. Input order is
+# *not* preserved.
+am__uniquify_input = $(AWK) '\
+ BEGIN { nonempty = 0; } \
+ { items[$$0] = 1; nonempty = 1; } \
+ END { if (nonempty) { for (i in items) print i; }; } \
+'
+# Make sure the list of sources is unique. This is necessary because,
+# e.g., the same source file might be shared among _SOURCES variables
+# for different programs/libraries.
+am__define_uniq_tagged_files = \
+ list='$(am__tagged_files)'; \
+ unique=`for i in $$list; do \
+ if test -f "$$i"; then echo $$i; else echo $(srcdir)/$$i; fi; \
+ done | $(am__uniquify_input)`
+ETAGS = etags
+CTAGS = ctags
+am__DIST_COMMON = $(srcdir)/Makefile.in $(top_srcdir)/depcomp
+DISTFILES = $(DIST_COMMON) $(DIST_SOURCES) $(TEXINFOS) $(EXTRA_DIST)
+ACLOCAL = @ACLOCAL@
+ACLOCAL_AMFLAGS = @ACLOCAL_AMFLAGS@
+AMTAR = @AMTAR@
+AM_DEFAULT_VERBOSITY = @AM_DEFAULT_VERBOSITY@
+APPARMOR_LIBS = @APPARMOR_LIBS@
+AR = @AR@
+AUTH_CFLAGS = @AUTH_CFLAGS@
+AUTH_LIBS = @AUTH_LIBS@
+AUTOCONF = @AUTOCONF@
+AUTOHEADER = @AUTOHEADER@
+AUTOMAKE = @AUTOMAKE@
+AWK = @AWK@
+BINARY_CFLAGS = @BINARY_CFLAGS@
+BINARY_LDFLAGS = @BINARY_LDFLAGS@
+BISON = @BISON@
+CASSANDRA_CFLAGS = @CASSANDRA_CFLAGS@
+CASSANDRA_LIBS = @CASSANDRA_LIBS@
+CC = @CC@
+CCDEPMODE = @CCDEPMODE@
+CDB_LIBS = @CDB_LIBS@
+CFLAGS = @CFLAGS@
+CLUCENE_CFLAGS = @CLUCENE_CFLAGS@
+CLUCENE_LIBS = @CLUCENE_LIBS@
+COMPRESS_LIBS = @COMPRESS_LIBS@
+CPP = @CPP@
+CPPFLAGS = @CPPFLAGS@
+CRYPT_LIBS = @CRYPT_LIBS@
+CXX = @CXX@
+CXXCPP = @CXXCPP@
+CXXDEPMODE = @CXXDEPMODE@
+CXXFLAGS = @CXXFLAGS@
+CYGPATH_W = @CYGPATH_W@
+DEFS = @DEFS@
+DEPDIR = @DEPDIR@
+DICT_LIBS = @DICT_LIBS@
+DLLIB = @DLLIB@
+DLLTOOL = @DLLTOOL@
+DSYMUTIL = @DSYMUTIL@
+DUMPBIN = @DUMPBIN@
+ECHO_C = @ECHO_C@
+ECHO_N = @ECHO_N@
+ECHO_T = @ECHO_T@
+EGREP = @EGREP@
+EXEEXT = @EXEEXT@
+FGREP = @FGREP@
+FLEX = @FLEX@
+FUZZER_CPPFLAGS = @FUZZER_CPPFLAGS@
+FUZZER_LDFLAGS = @FUZZER_LDFLAGS@
+GREP = @GREP@
+INSTALL = @INSTALL@
+INSTALL_DATA = @INSTALL_DATA@
+INSTALL_PROGRAM = @INSTALL_PROGRAM@
+INSTALL_SCRIPT = @INSTALL_SCRIPT@
+INSTALL_STRIP_PROGRAM = @INSTALL_STRIP_PROGRAM@
+KRB5CONFIG = @KRB5CONFIG@
+KRB5_CFLAGS = @KRB5_CFLAGS@
+KRB5_LIBS = @KRB5_LIBS@
+LD = @LD@
+LDAP_LIBS = @LDAP_LIBS@
+LDFLAGS = @LDFLAGS@
+LD_NO_WHOLE_ARCHIVE = @LD_NO_WHOLE_ARCHIVE@
+LD_WHOLE_ARCHIVE = @LD_WHOLE_ARCHIVE@
+LIBCAP = @LIBCAP@
+LIBDOVECOT = @LIBDOVECOT@
+LIBDOVECOT_COMPRESS = @LIBDOVECOT_COMPRESS@
+LIBDOVECOT_DEPS = @LIBDOVECOT_DEPS@
+LIBDOVECOT_DSYNC = @LIBDOVECOT_DSYNC@
+LIBDOVECOT_LA_LIBS = @LIBDOVECOT_LA_LIBS@
+LIBDOVECOT_LDA = @LIBDOVECOT_LDA@
+LIBDOVECOT_LDAP = @LIBDOVECOT_LDAP@
+LIBDOVECOT_LIBFTS = @LIBDOVECOT_LIBFTS@
+LIBDOVECOT_LIBFTS_DEPS = @LIBDOVECOT_LIBFTS_DEPS@
+LIBDOVECOT_LOGIN = @LIBDOVECOT_LOGIN@
+LIBDOVECOT_LUA = @LIBDOVECOT_LUA@
+LIBDOVECOT_LUA_DEPS = @LIBDOVECOT_LUA_DEPS@
+LIBDOVECOT_SQL = @LIBDOVECOT_SQL@
+LIBDOVECOT_STORAGE = @LIBDOVECOT_STORAGE@
+LIBDOVECOT_STORAGE_DEPS = @LIBDOVECOT_STORAGE_DEPS@
+LIBEXTTEXTCAT_CFLAGS = @LIBEXTTEXTCAT_CFLAGS@
+LIBEXTTEXTCAT_LIBS = @LIBEXTTEXTCAT_LIBS@
+LIBICONV = @LIBICONV@
+LIBICU_CFLAGS = @LIBICU_CFLAGS@
+LIBICU_LIBS = @LIBICU_LIBS@
+LIBOBJS = @LIBOBJS@
+LIBS = @LIBS@
+LIBSODIUM_CFLAGS = @LIBSODIUM_CFLAGS@
+LIBSODIUM_LIBS = @LIBSODIUM_LIBS@
+LIBTIRPC_CFLAGS = @LIBTIRPC_CFLAGS@
+LIBTIRPC_LIBS = @LIBTIRPC_LIBS@
+LIBTOOL = @LIBTOOL@
+LIBUNWIND_CFLAGS = @LIBUNWIND_CFLAGS@
+LIBUNWIND_LIBS = @LIBUNWIND_LIBS@
+LIBWRAP_LIBS = @LIBWRAP_LIBS@
+LINKED_STORAGE_LDADD = @LINKED_STORAGE_LDADD@
+LIPO = @LIPO@
+LN_S = @LN_S@
+LTLIBICONV = @LTLIBICONV@
+LTLIBOBJS = @LTLIBOBJS@
+LT_SYS_LIBRARY_PATH = @LT_SYS_LIBRARY_PATH@
+LUA_CFLAGS = @LUA_CFLAGS@
+LUA_LIBS = @LUA_LIBS@
+MAINT = @MAINT@
+MAKEINFO = @MAKEINFO@
+MANIFEST_TOOL = @MANIFEST_TOOL@
+MKDIR_P = @MKDIR_P@
+MODULE_LIBS = @MODULE_LIBS@
+MODULE_SUFFIX = @MODULE_SUFFIX@
+MYSQL_CFLAGS = @MYSQL_CFLAGS@
+MYSQL_CONFIG = @MYSQL_CONFIG@
+MYSQL_LIBS = @MYSQL_LIBS@
+NM = @NM@
+NMEDIT = @NMEDIT@
+NOPLUGIN_LDFLAGS = @NOPLUGIN_LDFLAGS@
+OBJDUMP = @OBJDUMP@
+OBJEXT = @OBJEXT@
+OTOOL = @OTOOL@
+OTOOL64 = @OTOOL64@
+PACKAGE = @PACKAGE@
+PACKAGE_BUGREPORT = @PACKAGE_BUGREPORT@
+PACKAGE_NAME = @PACKAGE_NAME@
+PACKAGE_STRING = @PACKAGE_STRING@
+PACKAGE_TARNAME = @PACKAGE_TARNAME@
+PACKAGE_URL = @PACKAGE_URL@
+PACKAGE_VERSION = @PACKAGE_VERSION@
+PANDOC = @PANDOC@
+PATH_SEPARATOR = @PATH_SEPARATOR@
+PGSQL_CFLAGS = @PGSQL_CFLAGS@
+PGSQL_LIBS = @PGSQL_LIBS@
+PG_CONFIG = @PG_CONFIG@
+PIE_CFLAGS = @PIE_CFLAGS@
+PIE_LDFLAGS = @PIE_LDFLAGS@
+PKG_CONFIG = @PKG_CONFIG@
+PKG_CONFIG_LIBDIR = @PKG_CONFIG_LIBDIR@
+PKG_CONFIG_PATH = @PKG_CONFIG_PATH@
+QUOTA_LIBS = @QUOTA_LIBS@
+RANLIB = @RANLIB@
+RELRO_LDFLAGS = @RELRO_LDFLAGS@
+RPCGEN = @RPCGEN@
+RUN_TEST = @RUN_TEST@
+SED = @SED@
+SETTING_FILES = @SETTING_FILES@
+SET_MAKE = @SET_MAKE@
+SHELL = @SHELL@
+SQLITE_CFLAGS = @SQLITE_CFLAGS@
+SQLITE_LIBS = @SQLITE_LIBS@
+SQL_CFLAGS = @SQL_CFLAGS@
+SQL_LIBS = @SQL_LIBS@
+SSL_CFLAGS = @SSL_CFLAGS@
+SSL_LIBS = @SSL_LIBS@
+STRIP = @STRIP@
+SYSTEMD_CFLAGS = @SYSTEMD_CFLAGS@
+SYSTEMD_LIBS = @SYSTEMD_LIBS@
+VALGRIND = @VALGRIND@
+VERSION = @VERSION@
+ZSTD_CFLAGS = @ZSTD_CFLAGS@
+ZSTD_LIBS = @ZSTD_LIBS@
+abs_builddir = @abs_builddir@
+abs_srcdir = @abs_srcdir@
+abs_top_builddir = @abs_top_builddir@
+abs_top_srcdir = @abs_top_srcdir@
+ac_ct_AR = @ac_ct_AR@
+ac_ct_CC = @ac_ct_CC@
+ac_ct_CXX = @ac_ct_CXX@
+ac_ct_DUMPBIN = @ac_ct_DUMPBIN@
+am__include = @am__include@
+am__leading_dot = @am__leading_dot@
+am__quote = @am__quote@
+am__tar = @am__tar@
+am__untar = @am__untar@
+bindir = @bindir@
+build = @build@
+build_alias = @build_alias@
+build_cpu = @build_cpu@
+build_os = @build_os@
+build_vendor = @build_vendor@
+builddir = @builddir@
+datadir = @datadir@
+datarootdir = @datarootdir@
+dict_drivers = @dict_drivers@
+docdir = @docdir@
+dvidir = @dvidir@
+exec_prefix = @exec_prefix@
+host = @host@
+host_alias = @host_alias@
+host_cpu = @host_cpu@
+host_os = @host_os@
+host_vendor = @host_vendor@
+htmldir = @htmldir@
+includedir = @includedir@
+infodir = @infodir@
+install_sh = @install_sh@
+libdir = @libdir@
+libexecdir = @libexecdir@
+localedir = @localedir@
+localstatedir = @localstatedir@
+mandir = @mandir@
+mkdir_p = @mkdir_p@
+moduledir = @moduledir@
+oldincludedir = @oldincludedir@
+pdfdir = @pdfdir@
+prefix = @prefix@
+program_transform_name = @program_transform_name@
+psdir = @psdir@
+rundir = @rundir@
+runstatedir = @runstatedir@
+sbindir = @sbindir@
+sharedstatedir = @sharedstatedir@
+sql_drivers = @sql_drivers@
+srcdir = @srcdir@
+ssldir = @ssldir@
+statedir = @statedir@
+sysconfdir = @sysconfdir@
+systemdservicetype = @systemdservicetype@
+systemdsystemunitdir = @systemdsystemunitdir@
+target_alias = @target_alias@
+top_build_prefix = @top_build_prefix@
+top_builddir = @top_builddir@
+top_srcdir = @top_srcdir@
+noinst_LTLIBRARIES = libdns.la
+AM_CPPFLAGS = \
+ -I$(top_srcdir)/src/lib
+
+libdns_la_SOURCES = \
+ dns-lookup.c \
+ dns-util.c
+
+headers = \
+ dns-lookup.h \
+ dns-util.h
+
+test_programs = \
+ test-dns-util
+
+test_libs = \
+ libdns.la \
+ ../lib-test/libtest.la \
+ ../lib/liblib.la
+
+test_dns_util_SOURCE = test-dns-util.c
+test_dns_util_LDADD = $(test_libs)
+test_dns_util_CFLAGS = $(AM_CPPFLAGS) \
+ -I$(top_srcdir)/src/lib-test
+
+pkginc_libdir = $(pkgincludedir)
+pkginc_lib_HEADERS = $(headers)
+all: all-am
+
+.SUFFIXES:
+.SUFFIXES: .c .lo .o .obj
+$(srcdir)/Makefile.in: @MAINTAINER_MODE_TRUE@ $(srcdir)/Makefile.am $(am__configure_deps)
+ @for dep in $?; do \
+ case '$(am__configure_deps)' in \
+ *$$dep*) \
+ ( cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh ) \
+ && { if test -f $@; then exit 0; else break; fi; }; \
+ exit 1;; \
+ esac; \
+ done; \
+ echo ' cd $(top_srcdir) && $(AUTOMAKE) --foreign src/lib-dns/Makefile'; \
+ $(am__cd) $(top_srcdir) && \
+ $(AUTOMAKE) --foreign src/lib-dns/Makefile
+Makefile: $(srcdir)/Makefile.in $(top_builddir)/config.status
+ @case '$?' in \
+ *config.status*) \
+ cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh;; \
+ *) \
+ echo ' cd $(top_builddir) && $(SHELL) ./config.status $(subdir)/$@ $(am__maybe_remake_depfiles)'; \
+ cd $(top_builddir) && $(SHELL) ./config.status $(subdir)/$@ $(am__maybe_remake_depfiles);; \
+ esac;
+
+$(top_builddir)/config.status: $(top_srcdir)/configure $(CONFIG_STATUS_DEPENDENCIES)
+ cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh
+
+$(top_srcdir)/configure: @MAINTAINER_MODE_TRUE@ $(am__configure_deps)
+ cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh
+$(ACLOCAL_M4): @MAINTAINER_MODE_TRUE@ $(am__aclocal_m4_deps)
+ cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh
+$(am__aclocal_m4_deps):
+
+clean-noinstPROGRAMS:
+ @list='$(noinst_PROGRAMS)'; test -n "$$list" || exit 0; \
+ echo " rm -f" $$list; \
+ rm -f $$list || exit $$?; \
+ test -n "$(EXEEXT)" || exit 0; \
+ list=`for p in $$list; do echo "$$p"; done | sed 's/$(EXEEXT)$$//'`; \
+ echo " rm -f" $$list; \
+ rm -f $$list
+
+clean-noinstLTLIBRARIES:
+ -test -z "$(noinst_LTLIBRARIES)" || rm -f $(noinst_LTLIBRARIES)
+ @list='$(noinst_LTLIBRARIES)'; \
+ locs=`for p in $$list; do echo $$p; done | \
+ sed 's|^[^/]*$$|.|; s|/[^/]*$$||; s|$$|/so_locations|' | \
+ sort -u`; \
+ test -z "$$locs" || { \
+ echo rm -f $${locs}; \
+ rm -f $${locs}; \
+ }
+
+libdns.la: $(libdns_la_OBJECTS) $(libdns_la_DEPENDENCIES) $(EXTRA_libdns_la_DEPENDENCIES)
+ $(AM_V_CCLD)$(LINK) $(libdns_la_OBJECTS) $(libdns_la_LIBADD) $(LIBS)
+
+test-dns-util$(EXEEXT): $(test_dns_util_OBJECTS) $(test_dns_util_DEPENDENCIES) $(EXTRA_test_dns_util_DEPENDENCIES)
+ @rm -f test-dns-util$(EXEEXT)
+ $(AM_V_CCLD)$(test_dns_util_LINK) $(test_dns_util_OBJECTS) $(test_dns_util_LDADD) $(LIBS)
+
+mostlyclean-compile:
+ -rm -f *.$(OBJEXT)
+
+distclean-compile:
+ -rm -f *.tab.c
+
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/dns-lookup.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/dns-util.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/test_dns_util-test-dns-util.Po@am__quote@ # am--include-marker
+
+$(am__depfiles_remade):
+ @$(MKDIR_P) $(@D)
+ @echo '# dummy' >$@-t && $(am__mv) $@-t $@
+
+am--depfiles: $(am__depfiles_remade)
+
+.c.o:
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(COMPILE) -MT $@ -MD -MP -MF $(DEPDIR)/$*.Tpo -c -o $@ $<
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/$*.Tpo $(DEPDIR)/$*.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='$<' object='$@' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(COMPILE) -c -o $@ $<
+
+.c.obj:
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(COMPILE) -MT $@ -MD -MP -MF $(DEPDIR)/$*.Tpo -c -o $@ `$(CYGPATH_W) '$<'`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/$*.Tpo $(DEPDIR)/$*.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='$<' object='$@' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(COMPILE) -c -o $@ `$(CYGPATH_W) '$<'`
+
+.c.lo:
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(LTCOMPILE) -MT $@ -MD -MP -MF $(DEPDIR)/$*.Tpo -c -o $@ $<
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/$*.Tpo $(DEPDIR)/$*.Plo
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='$<' object='$@' libtool=yes @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(LTCOMPILE) -c -o $@ $<
+
+test_dns_util-test-dns-util.o: test-dns-util.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) $(CPPFLAGS) $(test_dns_util_CFLAGS) $(CFLAGS) -MT test_dns_util-test-dns-util.o -MD -MP -MF $(DEPDIR)/test_dns_util-test-dns-util.Tpo -c -o test_dns_util-test-dns-util.o `test -f 'test-dns-util.c' || echo '$(srcdir)/'`test-dns-util.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/test_dns_util-test-dns-util.Tpo $(DEPDIR)/test_dns_util-test-dns-util.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='test-dns-util.c' object='test_dns_util-test-dns-util.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) $(CPPFLAGS) $(test_dns_util_CFLAGS) $(CFLAGS) -c -o test_dns_util-test-dns-util.o `test -f 'test-dns-util.c' || echo '$(srcdir)/'`test-dns-util.c
+
+test_dns_util-test-dns-util.obj: test-dns-util.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) $(CPPFLAGS) $(test_dns_util_CFLAGS) $(CFLAGS) -MT test_dns_util-test-dns-util.obj -MD -MP -MF $(DEPDIR)/test_dns_util-test-dns-util.Tpo -c -o test_dns_util-test-dns-util.obj `if test -f 'test-dns-util.c'; then $(CYGPATH_W) 'test-dns-util.c'; else $(CYGPATH_W) '$(srcdir)/test-dns-util.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/test_dns_util-test-dns-util.Tpo $(DEPDIR)/test_dns_util-test-dns-util.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='test-dns-util.c' object='test_dns_util-test-dns-util.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) $(CPPFLAGS) $(test_dns_util_CFLAGS) $(CFLAGS) -c -o test_dns_util-test-dns-util.obj `if test -f 'test-dns-util.c'; then $(CYGPATH_W) 'test-dns-util.c'; else $(CYGPATH_W) '$(srcdir)/test-dns-util.c'; fi`
+
+mostlyclean-libtool:
+ -rm -f *.lo
+
+clean-libtool:
+ -rm -rf .libs _libs
+install-pkginc_libHEADERS: $(pkginc_lib_HEADERS)
+ @$(NORMAL_INSTALL)
+ @list='$(pkginc_lib_HEADERS)'; test -n "$(pkginc_libdir)" || list=; \
+ if test -n "$$list"; then \
+ echo " $(MKDIR_P) '$(DESTDIR)$(pkginc_libdir)'"; \
+ $(MKDIR_P) "$(DESTDIR)$(pkginc_libdir)" || exit 1; \
+ fi; \
+ for p in $$list; do \
+ if test -f "$$p"; then d=; else d="$(srcdir)/"; fi; \
+ echo "$$d$$p"; \
+ done | $(am__base_list) | \
+ while read files; do \
+ echo " $(INSTALL_HEADER) $$files '$(DESTDIR)$(pkginc_libdir)'"; \
+ $(INSTALL_HEADER) $$files "$(DESTDIR)$(pkginc_libdir)" || exit $$?; \
+ done
+
+uninstall-pkginc_libHEADERS:
+ @$(NORMAL_UNINSTALL)
+ @list='$(pkginc_lib_HEADERS)'; test -n "$(pkginc_libdir)" || list=; \
+ files=`for p in $$list; do echo $$p; done | sed -e 's|^.*/||'`; \
+ dir='$(DESTDIR)$(pkginc_libdir)'; $(am__uninstall_files_from_dir)
+
+ID: $(am__tagged_files)
+ $(am__define_uniq_tagged_files); mkid -fID $$unique
+tags: tags-am
+TAGS: tags
+
+tags-am: $(TAGS_DEPENDENCIES) $(am__tagged_files)
+ set x; \
+ here=`pwd`; \
+ $(am__define_uniq_tagged_files); \
+ shift; \
+ if test -z "$(ETAGS_ARGS)$$*$$unique"; then :; else \
+ test -n "$$unique" || unique=$$empty_fix; \
+ if test $$# -gt 0; then \
+ $(ETAGS) $(ETAGSFLAGS) $(AM_ETAGSFLAGS) $(ETAGS_ARGS) \
+ "$$@" $$unique; \
+ else \
+ $(ETAGS) $(ETAGSFLAGS) $(AM_ETAGSFLAGS) $(ETAGS_ARGS) \
+ $$unique; \
+ fi; \
+ fi
+ctags: ctags-am
+
+CTAGS: ctags
+ctags-am: $(TAGS_DEPENDENCIES) $(am__tagged_files)
+ $(am__define_uniq_tagged_files); \
+ test -z "$(CTAGS_ARGS)$$unique" \
+ || $(CTAGS) $(CTAGSFLAGS) $(AM_CTAGSFLAGS) $(CTAGS_ARGS) \
+ $$unique
+
+GTAGS:
+ here=`$(am__cd) $(top_builddir) && pwd` \
+ && $(am__cd) $(top_srcdir) \
+ && gtags -i $(GTAGS_ARGS) "$$here"
+cscopelist: cscopelist-am
+
+cscopelist-am: $(am__tagged_files)
+ list='$(am__tagged_files)'; \
+ case "$(srcdir)" in \
+ [\\/]* | ?:[\\/]*) sdir="$(srcdir)" ;; \
+ *) sdir=$(subdir)/$(srcdir) ;; \
+ esac; \
+ for i in $$list; do \
+ if test -f "$$i"; then \
+ echo "$(subdir)/$$i"; \
+ else \
+ echo "$$sdir/$$i"; \
+ fi; \
+ done >> $(top_builddir)/cscope.files
+
+distclean-tags:
+ -rm -f TAGS ID GTAGS GRTAGS GSYMS GPATH tags
+
+distdir: $(BUILT_SOURCES)
+ $(MAKE) $(AM_MAKEFLAGS) distdir-am
+
+distdir-am: $(DISTFILES)
+ @srcdirstrip=`echo "$(srcdir)" | sed 's/[].[^$$\\*]/\\\\&/g'`; \
+ topsrcdirstrip=`echo "$(top_srcdir)" | sed 's/[].[^$$\\*]/\\\\&/g'`; \
+ list='$(DISTFILES)'; \
+ dist_files=`for file in $$list; do echo $$file; done | \
+ sed -e "s|^$$srcdirstrip/||;t" \
+ -e "s|^$$topsrcdirstrip/|$(top_builddir)/|;t"`; \
+ case $$dist_files in \
+ */*) $(MKDIR_P) `echo "$$dist_files" | \
+ sed '/\//!d;s|^|$(distdir)/|;s,/[^/]*$$,,' | \
+ sort -u` ;; \
+ esac; \
+ for file in $$dist_files; do \
+ if test -f $$file || test -d $$file; then d=.; else d=$(srcdir); fi; \
+ if test -d $$d/$$file; then \
+ dir=`echo "/$$file" | sed -e 's,/[^/]*$$,,'`; \
+ if test -d "$(distdir)/$$file"; then \
+ find "$(distdir)/$$file" -type d ! -perm -700 -exec chmod u+rwx {} \;; \
+ fi; \
+ if test -d $(srcdir)/$$file && test $$d != $(srcdir); then \
+ cp -fpR $(srcdir)/$$file "$(distdir)$$dir" || exit 1; \
+ find "$(distdir)/$$file" -type d ! -perm -700 -exec chmod u+rwx {} \;; \
+ fi; \
+ cp -fpR $$d/$$file "$(distdir)$$dir" || exit 1; \
+ else \
+ test -f "$(distdir)/$$file" \
+ || cp -p $$d/$$file "$(distdir)/$$file" \
+ || exit 1; \
+ fi; \
+ done
+check-am: all-am
+ $(MAKE) $(AM_MAKEFLAGS) check-local
+check: check-am
+all-am: Makefile $(PROGRAMS) $(LTLIBRARIES) $(HEADERS)
+installdirs:
+ for dir in "$(DESTDIR)$(pkginc_libdir)"; do \
+ test -z "$$dir" || $(MKDIR_P) "$$dir"; \
+ done
+install: install-am
+install-exec: install-exec-am
+install-data: install-data-am
+uninstall: uninstall-am
+
+install-am: all-am
+ @$(MAKE) $(AM_MAKEFLAGS) install-exec-am install-data-am
+
+installcheck: installcheck-am
+install-strip:
+ if test -z '$(STRIP)'; then \
+ $(MAKE) $(AM_MAKEFLAGS) INSTALL_PROGRAM="$(INSTALL_STRIP_PROGRAM)" \
+ install_sh_PROGRAM="$(INSTALL_STRIP_PROGRAM)" INSTALL_STRIP_FLAG=-s \
+ install; \
+ else \
+ $(MAKE) $(AM_MAKEFLAGS) INSTALL_PROGRAM="$(INSTALL_STRIP_PROGRAM)" \
+ install_sh_PROGRAM="$(INSTALL_STRIP_PROGRAM)" INSTALL_STRIP_FLAG=-s \
+ "INSTALL_PROGRAM_ENV=STRIPPROG='$(STRIP)'" install; \
+ fi
+mostlyclean-generic:
+
+clean-generic:
+
+distclean-generic:
+ -test -z "$(CONFIG_CLEAN_FILES)" || rm -f $(CONFIG_CLEAN_FILES)
+ -test . = "$(srcdir)" || test -z "$(CONFIG_CLEAN_VPATH_FILES)" || rm -f $(CONFIG_CLEAN_VPATH_FILES)
+
+maintainer-clean-generic:
+ @echo "This command is intended for maintainers to use"
+ @echo "it deletes files that may require special tools to rebuild."
+clean: clean-am
+
+clean-am: clean-generic clean-libtool clean-noinstLTLIBRARIES \
+ clean-noinstPROGRAMS mostlyclean-am
+
+distclean: distclean-am
+ -rm -f ./$(DEPDIR)/dns-lookup.Plo
+ -rm -f ./$(DEPDIR)/dns-util.Plo
+ -rm -f ./$(DEPDIR)/test_dns_util-test-dns-util.Po
+ -rm -f Makefile
+distclean-am: clean-am distclean-compile distclean-generic \
+ distclean-tags
+
+dvi: dvi-am
+
+dvi-am:
+
+html: html-am
+
+html-am:
+
+info: info-am
+
+info-am:
+
+install-data-am: install-pkginc_libHEADERS
+
+install-dvi: install-dvi-am
+
+install-dvi-am:
+
+install-exec-am:
+
+install-html: install-html-am
+
+install-html-am:
+
+install-info: install-info-am
+
+install-info-am:
+
+install-man:
+
+install-pdf: install-pdf-am
+
+install-pdf-am:
+
+install-ps: install-ps-am
+
+install-ps-am:
+
+installcheck-am:
+
+maintainer-clean: maintainer-clean-am
+ -rm -f ./$(DEPDIR)/dns-lookup.Plo
+ -rm -f ./$(DEPDIR)/dns-util.Plo
+ -rm -f ./$(DEPDIR)/test_dns_util-test-dns-util.Po
+ -rm -f Makefile
+maintainer-clean-am: distclean-am maintainer-clean-generic
+
+mostlyclean: mostlyclean-am
+
+mostlyclean-am: mostlyclean-compile mostlyclean-generic \
+ mostlyclean-libtool
+
+pdf: pdf-am
+
+pdf-am:
+
+ps: ps-am
+
+ps-am:
+
+uninstall-am: uninstall-pkginc_libHEADERS
+
+.MAKE: check-am install-am install-strip
+
+.PHONY: CTAGS GTAGS TAGS all all-am am--depfiles check check-am \
+ check-local clean clean-generic clean-libtool \
+ clean-noinstLTLIBRARIES clean-noinstPROGRAMS cscopelist-am \
+ ctags ctags-am distclean distclean-compile distclean-generic \
+ distclean-libtool distclean-tags distdir dvi dvi-am html \
+ html-am info info-am install install-am install-data \
+ install-data-am install-dvi install-dvi-am install-exec \
+ install-exec-am install-html install-html-am install-info \
+ install-info-am install-man install-pdf install-pdf-am \
+ install-pkginc_libHEADERS install-ps install-ps-am \
+ install-strip installcheck installcheck-am installdirs \
+ maintainer-clean maintainer-clean-generic mostlyclean \
+ mostlyclean-compile mostlyclean-generic mostlyclean-libtool \
+ pdf pdf-am ps ps-am tags tags-am uninstall uninstall-am \
+ uninstall-pkginc_libHEADERS
+
+.PRECIOUS: Makefile
+
+
+check-local:
+ for bin in $(test_programs); do \
+ if ! $(RUN_TEST) ./$$bin; then exit 1; fi; \
+ done
+
+# Tell versions [3.59,3.63) of GNU make to not export all variables.
+# Otherwise a system limit (for SysV at least) may be exceeded.
+.NOEXPORT:
diff --git a/src/lib-dns/dns-lookup.c b/src/lib-dns/dns-lookup.c
new file mode 100644
index 0000000..c37445e
--- /dev/null
+++ b/src/lib-dns/dns-lookup.c
@@ -0,0 +1,438 @@
+/* Copyright (c) 2010-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "ioloop.h"
+#include "str.h"
+#include "array.h"
+#include "ostream.h"
+#include "connection.h"
+#include "lib-event.h"
+#include "llist.h"
+#include "istream.h"
+#include "write-full.h"
+#include "time-util.h"
+#include "dns-lookup.h"
+
+#include <stdio.h>
+#include <unistd.h>
+
+#define MAX_INBUF_SIZE 512
+
+static struct event_category event_category_dns = {
+ .name = "dns"
+};
+
+struct dns_lookup {
+ struct dns_lookup *prev, *next;
+ struct dns_client *client;
+ pool_t pool;
+ bool ptr_lookup;
+
+ struct timeout *to;
+
+ struct timeval start_time;
+ unsigned int warn_msecs;
+
+ struct dns_lookup_result result;
+ struct event *event;
+
+ dns_lookup_callback_t *callback;
+ void *context;
+};
+
+struct dns_client {
+ struct connection conn;
+ struct connection_list *clist;
+ struct dns_lookup *head, *tail;
+ struct timeout *to_idle;
+ struct ioloop *ioloop;
+ char *path;
+
+ unsigned int timeout_msecs;
+ unsigned int idle_timeout_msecs;
+
+ bool connected:1;
+ bool deinit_client_at_free:1;
+};
+
+#undef dns_lookup
+#undef dns_lookup_ptr
+#undef dns_client_lookup
+#undef dns_client_lookup_ptr
+
+static void dns_lookup_free(struct dns_lookup **_lookup);
+
+static void dns_lookup_save_msecs(struct dns_lookup *lookup);
+
+static void dns_lookup_callback(struct dns_lookup *lookup)
+{
+ struct event_passthrough *e =
+ event_create_passthrough(lookup->event)->
+ set_name("dns_request_finished");
+
+ dns_lookup_save_msecs(lookup);
+
+ if (lookup->result.ret != 0) {
+ e->add_int("error_code", lookup->result.ret);
+ e->add_str("error", lookup->result.error);
+ e_debug(e->event(), "Lookup failed after %u msecs: %s",
+ lookup->result.msecs, lookup->result.error);
+ } else {
+ e_debug(e->event(), "Lookup successful after %u msecs",
+ lookup->result.msecs);
+ }
+ lookup->callback(&lookup->result, lookup->context);
+}
+
+static void dns_client_disconnect(struct dns_client *client, const char *error)
+{
+ struct dns_lookup *lookup, *next;
+ struct dns_lookup_result result;
+
+ if (!client->connected)
+ return;
+ timeout_remove(&client->to_idle);
+
+ connection_disconnect(&client->conn);
+ client->connected = FALSE;
+
+ i_zero(&result);
+ result.ret = EAI_FAIL;
+ result.error = error;
+ e_debug(client->conn.event, "Disconnect: %s", error);
+
+ lookup = client->head;
+ client->head = NULL;
+ while (lookup != NULL) {
+ next = lookup->next;
+ dns_lookup_callback(lookup);
+ dns_lookup_free(&lookup);
+ lookup = next;
+ }
+}
+
+static void dns_client_destroy(struct connection *conn)
+{
+ struct dns_client *client = container_of(conn, struct dns_client, conn);
+ client->connected = FALSE;
+ connection_deinit(conn);
+}
+
+static int dns_lookup_input_args(struct dns_lookup *lookup, const char *const *args)
+{
+ struct dns_lookup_result *result = &lookup->result;
+
+ if (str_to_int(args[0], &result->ret) < 0)
+ return -1;
+ if (result->ret != 0) {
+ result->error = args[1];
+ return 1;
+ }
+
+ if (lookup->ptr_lookup) {
+ result->name = p_strdup(lookup->pool, args[1]);
+ return 1;
+ }
+
+ ARRAY(struct ip_addr) ips;
+ p_array_init(&ips, lookup->pool, 2);
+ for(unsigned int i = 1; args[i] != NULL; i++) {
+ struct ip_addr *ip = array_append_space(&ips);
+ if (net_addr2ip(args[i], ip) < 0)
+ return -1;
+ }
+ result->ips = array_get(&ips, &result->ips_count);
+
+ return 1;
+}
+
+static void dns_lookup_save_msecs(struct dns_lookup *lookup)
+{
+ struct timeval now;
+ int diff;
+
+ i_gettimeofday(&now);
+
+ diff = timeval_diff_msecs(&now, &lookup->start_time);
+ if (diff > 0)
+ lookup->result.msecs = diff;
+}
+
+static int dns_client_input_args(struct connection *conn, const char *const *args)
+{
+ struct dns_client *client = container_of(conn, struct dns_client, conn);
+ struct dns_lookup *lookup = client->head;
+ bool retry = FALSE;
+ int ret = 0;
+
+ if (lookup == NULL) {
+ dns_client_disconnect(client, t_strdup_printf(
+ "Unexpected input from %s", conn->name));
+ return -1;
+ }
+
+ if ((ret = dns_lookup_input_args(lookup, args)) == 0) {
+ return 1; /* keep on reading */
+ } else if (ret < 0) {
+ dns_client_disconnect(client, t_strdup_printf(
+ "Invalid input from %s", conn->name));
+ return -1;
+ } else if (ret > 0) {
+ dns_lookup_callback(lookup);
+ retry = !lookup->client->deinit_client_at_free;
+ dns_lookup_free(&lookup);
+ }
+
+ return retry ? 1 : -1;
+}
+
+static void dns_lookup_timeout(struct dns_lookup *lookup)
+{
+ lookup->result.error = "Lookup timed out";
+
+ dns_lookup_callback(lookup);
+ dns_lookup_free(&lookup);
+}
+
+int dns_lookup(const char *host, const struct dns_lookup_settings *set,
+ dns_lookup_callback_t *callback, void *context,
+ struct dns_lookup **lookup_r)
+{
+ struct dns_client *client;
+
+ client = dns_client_init(set);
+ event_add_category(client->conn.event, &event_category_dns);
+ client->deinit_client_at_free = TRUE;
+ return dns_client_lookup(client, host, callback, context, lookup_r);
+}
+
+int dns_lookup_ptr(const struct ip_addr *ip,
+ const struct dns_lookup_settings *set,
+ dns_lookup_callback_t *callback, void *context,
+ struct dns_lookup **lookup_r)
+{
+ struct dns_client *client;
+
+ client = dns_client_init(set);
+ event_add_category(client->conn.event, &event_category_dns);
+ client->deinit_client_at_free = TRUE;
+ return dns_client_lookup_ptr(client, ip, callback, context, lookup_r);
+}
+
+static void dns_client_idle_timeout(struct dns_client *client)
+{
+ i_assert(client->head == NULL);
+
+ /* send QUIT */
+ o_stream_nsend_str(client->conn.output, "QUIT\n");
+ dns_client_disconnect(client, "Idle timeout");
+}
+
+static void dns_lookup_free(struct dns_lookup **_lookup)
+{
+ struct dns_lookup *lookup = *_lookup;
+ struct dns_client *client = lookup->client;
+
+ *_lookup = NULL;
+
+ DLLIST2_REMOVE(&client->head, &client->tail, lookup);
+ timeout_remove(&lookup->to);
+ if (client->deinit_client_at_free)
+ dns_client_deinit(&client);
+ else if (client->head == NULL && client->connected) {
+ client->to_idle = timeout_add_to(client->ioloop,
+ client->idle_timeout_msecs,
+ dns_client_idle_timeout, client);
+ }
+ event_unref(&lookup->event);
+ pool_unref(&lookup->pool);
+}
+
+void dns_lookup_abort(struct dns_lookup **lookup)
+{
+ dns_lookup_free(lookup);
+}
+
+static void dns_lookup_switch_ioloop_real(struct dns_lookup *lookup)
+{
+ if (lookup->to != NULL)
+ lookup->to = io_loop_move_timeout(&lookup->to);
+}
+
+void dns_lookup_switch_ioloop(struct dns_lookup *lookup)
+{
+ /* dns client ioloop switch switches all lookups too */
+ if (lookup->client->deinit_client_at_free)
+ dns_client_switch_ioloop(lookup->client);
+ else
+ dns_lookup_switch_ioloop_real(lookup);
+}
+
+static void dns_client_connected(struct connection *conn, bool success)
+{
+ struct dns_client *client = container_of(conn, struct dns_client, conn);
+ if (!success)
+ return;
+ client->connected = TRUE;
+}
+
+static const struct connection_vfuncs dns_client_vfuncs = {
+ .destroy = dns_client_destroy,
+ .input_args = dns_client_input_args,
+ .client_connected = dns_client_connected,
+};
+
+static const struct connection_settings dns_client_set = {
+ .service_name_in = "dns",
+ .service_name_out = "dns-client",
+ .major_version = 1,
+ .minor_version = 0,
+ .input_max_size = SIZE_MAX,
+ .output_max_size = SIZE_MAX,
+ .client = TRUE,
+};
+
+struct dns_client *dns_client_init(const struct dns_lookup_settings *set)
+{
+ struct dns_client *client;
+
+ client = i_new(struct dns_client, 1);
+ client->timeout_msecs = set->timeout_msecs;
+ client->idle_timeout_msecs = set->idle_timeout_msecs;
+ client->clist = connection_list_init(&dns_client_set, &dns_client_vfuncs);
+ client->ioloop = set->ioloop == NULL ? current_ioloop : set->ioloop;
+ client->path = i_strdup(set->dns_client_socket_path);
+ client->conn.event_parent=set->event_parent;
+ connection_init_client_unix(client->clist, &client->conn, client->path);
+ return client;
+}
+
+void dns_client_deinit(struct dns_client **_client)
+{
+ struct dns_client *client = *_client;
+ struct connection_list *clist = client->clist;
+ *_client = NULL;
+
+ i_assert(client->head == NULL);
+
+ dns_client_disconnect(client, "deinit");
+ connection_list_deinit(&clist);
+ i_free(client->path);
+ i_free(client);
+}
+
+int dns_client_connect(struct dns_client *client, const char **error_r)
+{
+ if (client->connected)
+ return 0;
+ if (client->ioloop != NULL)
+ connection_switch_ioloop_to(&client->conn, client->ioloop);
+ int ret = connection_client_connect(&client->conn);
+ if (ret < 0)
+ *error_r = t_strdup_printf("Failed to connect to %s: %m",
+ client->path);
+ return ret;
+}
+
+static int
+dns_client_send_request(struct dns_client *client, const char *cmd,
+ const char **error_r)
+{
+ int ret;
+
+ if (!client->connected) {
+ if (dns_client_connect(client, error_r) < 0)
+ return -1;
+ }
+
+ if ((ret = o_stream_send(client->conn.output, cmd, strlen(cmd))) < 0) {
+ *error_r = t_strdup_printf("write(%s) failed: %s",
+ client->conn.name,
+ o_stream_get_error(client->conn.output));
+ dns_client_disconnect(client, "Cannot send data");
+ }
+
+ return ret;
+}
+
+static int
+dns_client_lookup_common(struct dns_client *client,
+ const char *cmd, const char *param, bool ptr_lookup,
+ dns_lookup_callback_t *callback, void *context,
+ struct dns_lookup **lookup_r)
+{
+ struct dns_lookup *lookup;
+ int ret;
+
+ i_assert(param != NULL && *param != '\0');
+ cmd = t_strdup_printf("%s\t%s\n", cmd, param);
+
+ pool_t pool = pool_alloconly_create("dns lookup", 512);
+ lookup = p_new(pool, struct dns_lookup, 1);
+ lookup->pool = pool;
+
+ i_gettimeofday(&lookup->start_time);
+
+ lookup->client = client;
+ lookup->callback = callback;
+ lookup->context = context;
+ lookup->ptr_lookup = ptr_lookup;
+ lookup->result.ret = EAI_FAIL;
+ lookup->event = event_create(client->conn.event);
+ event_set_append_log_prefix(lookup->event, t_strconcat("dns(", param, "): ", NULL));
+ struct event_passthrough *e =
+ event_create_passthrough(lookup->event)->
+ set_name("dns_request_started");
+ e_debug(e->event(), "Lookup started");
+
+ if ((ret = dns_client_send_request(client, cmd, &lookup->result.error)) <= 0) {
+ if (ret == 0) {
+ /* retry once */
+ ret = dns_client_send_request(client, cmd,
+ &lookup->result.error);
+ }
+ if (ret <= 0) {
+ dns_lookup_callback(lookup);
+ dns_lookup_free(&lookup);
+ return -1;
+ }
+ }
+
+ if (client->timeout_msecs != 0) {
+ lookup->to = timeout_add_to(client->ioloop,
+ client->timeout_msecs,
+ dns_lookup_timeout, lookup);
+ }
+ timeout_remove(&client->to_idle);
+ DLLIST2_APPEND(&client->head, &client->tail, lookup);
+ *lookup_r = lookup;
+ return 0;
+}
+
+int dns_client_lookup(struct dns_client *client, const char *host,
+ dns_lookup_callback_t *callback, void *context,
+ struct dns_lookup **lookup_r)
+{
+ return dns_client_lookup_common(client, "IP", host, FALSE,
+ callback, context, lookup_r);
+}
+
+int dns_client_lookup_ptr(struct dns_client *client, const struct ip_addr *ip,
+ dns_lookup_callback_t *callback, void *context,
+ struct dns_lookup **lookup_r)
+{
+ return dns_client_lookup_common(client, "NAME", net_ip2addr(ip), TRUE,
+ callback, context, lookup_r);
+}
+
+void dns_client_switch_ioloop(struct dns_client *client)
+{
+ struct dns_lookup *lookup;
+
+ connection_switch_ioloop(&client->conn);
+ client->to_idle = io_loop_move_timeout(&client->to_idle);
+ client->ioloop = current_ioloop;
+
+ for (lookup = client->head; lookup != NULL; lookup = lookup->next)
+ dns_lookup_switch_ioloop_real(lookup);
+}
diff --git a/src/lib-dns/dns-lookup.h b/src/lib-dns/dns-lookup.h
new file mode 100644
index 0000000..38d65f6
--- /dev/null
+++ b/src/lib-dns/dns-lookup.h
@@ -0,0 +1,90 @@
+#ifndef DNS_LOOKUP_H
+#define DNS_LOOKUP_H
+
+#define DNS_CLIENT_SOCKET_NAME "dns-client"
+
+struct dns_lookup;
+
+struct dns_lookup_settings {
+ const char *dns_client_socket_path;
+ unsigned int timeout_msecs;
+ /* the idle_timeout_msecs works only with the dns_client_* API.
+ 0 = disconnect immediately */
+ unsigned int idle_timeout_msecs;
+
+ /* ioloop to run the lookup on (defaults to current_ioloop) */
+ struct ioloop *ioloop;
+ struct event *event_parent;
+};
+
+struct dns_lookup_result {
+ /* all is ok if ret=0, otherwise it contains net_gethosterror()
+ compatible error code. error string is always set if ret != 0. */
+ int ret;
+ const char *error;
+
+ /* how many milliseconds the lookup took. */
+ unsigned int msecs;
+
+ /* for IP lookup: */
+ unsigned int ips_count;
+ const struct ip_addr *ips;
+ /* for PTR lookup: */
+ const char *name;
+};
+
+typedef void dns_lookup_callback_t(const struct dns_lookup_result *result,
+ void *context);
+
+/* Do asynchronous DNS lookup via dns-client UNIX socket. Returns 0 if lookup
+ started, -1 if there was an error communicating with the UNIX socket.
+ When failing with -1, the callback is called before returning from the
+ function. */
+int dns_lookup(const char *host, const struct dns_lookup_settings *set,
+ dns_lookup_callback_t *callback, void *context,
+ struct dns_lookup **lookup_r) ATTR_NULL(4);
+#define dns_lookup(host, set, callback, context, lookup_r) \
+ dns_lookup(host - \
+ CALLBACK_TYPECHECK(callback, void (*)( \
+ const struct dns_lookup_result *, typeof(context))), \
+ set, (dns_lookup_callback_t *)callback, context, lookup_r)
+int dns_lookup_ptr(const struct ip_addr *ip,
+ const struct dns_lookup_settings *set,
+ dns_lookup_callback_t *callback, void *context,
+ struct dns_lookup **lookup_r) ATTR_NULL(4);
+#define dns_lookup_ptr(host, set, callback, context, lookup_r) \
+ dns_lookup_ptr(host - \
+ CALLBACK_TYPECHECK(callback, void (*)( \
+ const struct dns_lookup_result *, typeof(context))), \
+ set, (dns_lookup_callback_t *)callback, context, lookup_r)
+/* Abort the DNS lookup without calling the callback. */
+void dns_lookup_abort(struct dns_lookup **lookup);
+
+void dns_lookup_switch_ioloop(struct dns_lookup *lookup);
+
+/* Alternative API for clients that need to do multiple DNS lookups. */
+struct dns_client *dns_client_init(const struct dns_lookup_settings *set);
+void dns_client_deinit(struct dns_client **client);
+
+/* Connect immediately to the dns-lookup socket. */
+int dns_client_connect(struct dns_client *client, const char **error_r);
+int dns_client_lookup(struct dns_client *client, const char *host,
+ dns_lookup_callback_t *callback, void *context,
+ struct dns_lookup **lookup_r) ATTR_NULL(4);
+#define dns_client_lookup(client, host, callback, context, lookup_r) \
+ dns_client_lookup(client, host - \
+ CALLBACK_TYPECHECK(callback, void (*)( \
+ const struct dns_lookup_result *, typeof(context))), \
+ (dns_lookup_callback_t *)callback, context, lookup_r)
+int dns_client_lookup_ptr(struct dns_client *client, const struct ip_addr *ip,
+ dns_lookup_callback_t *callback, void *context,
+ struct dns_lookup **lookup_r) ATTR_NULL(4);
+#define dns_client_lookup_ptr(client, host, callback, context, lookup_r) \
+ dns_client_lookup_ptr(client, host - \
+ CALLBACK_TYPECHECK(callback, void (*)( \
+ const struct dns_lookup_result *, typeof(context))), \
+ (dns_lookup_callback_t *)callback, context, lookup_r)
+
+void dns_client_switch_ioloop(struct dns_client *client);
+
+#endif
diff --git a/src/lib-dns/dns-util.c b/src/lib-dns/dns-util.c
new file mode 100644
index 0000000..a8469ce
--- /dev/null
+++ b/src/lib-dns/dns-util.c
@@ -0,0 +1,85 @@
+/* Copyright (c) 2010-2018 Dovecot authors, see the included COPYING file */
+#include "lib.h"
+#include "dns-util.h"
+
+/**
+ return first position from b->a of c or a if not found
+ */
+static inline
+const char *strchr_ba(const char *a, const char *b, char c)
+{
+ for(;b>a && *b != c; b--);
+ return b;
+}
+
+int dns_ncompare(const char *a, const char *b, size_t n)
+{
+ if (a == NULL && b == NULL) return 0;
+ if (a == NULL && b != NULL) return 1;
+ if (a != NULL && b == NULL) return -1;
+
+ for(size_t i = 0; i < n &&
+ *a != '\0' &&
+ *b != '\0' &&
+ dns_tolower(*a) == dns_tolower(*b);
+ i++, a++, b++);
+
+ return dns_tolower(*a) - dns_tolower(*b);
+}
+
+int dns_compare(const char *a, const char *b)
+{
+ return dns_ncompare(a, b, SIZE_MAX);
+}
+
+int dns_compare_labels(const char *a, const char *b)
+{
+ if (a == NULL && b == NULL) return 0;
+ if (a == NULL && b != NULL) return 1;
+ if (a != NULL && b == NULL) return -1;
+
+ const char *ptr_a = a + strlen(a);
+ const char *ptr_b = b + strlen(b);
+ const char *label_a = ptr_a, *label_b = ptr_b;
+ int comp = 0;
+
+ while(comp == 0 && ptr_a > a && ptr_b > b) {
+ /* look for start of label, including dot */
+ label_a = strchr_ba(a, ptr_a, '.');
+ label_b = strchr_ba(b, ptr_b, '.');
+ if (ptr_a - label_a != ptr_b - label_b)
+ /* compare labels up to minimum length
+ but include \0 to make sure that we
+ don't consider alpha and alphabet
+ equal */
+ return dns_ncompare(label_a, label_b,
+ I_MIN(ptr_a - label_a,
+ ptr_b - label_b)+1);
+ comp = dns_ncompare(label_a, label_b, ptr_a -label_a);
+ ptr_a = label_a - 1;
+ ptr_b = label_b - 1;
+ }
+
+ return dns_tolower(*label_a) - dns_tolower(*label_b);
+}
+
+int dns_match_wildcard(const char *name, const char *mask)
+{
+ i_assert(name != NULL && mask != NULL);
+
+ for(;*name != '\0' && *mask != '\0'; name++, mask++) {
+ switch(*mask) {
+ case '*':
+ name = strchr(name, '.');
+ if (name == NULL || mask[1] != '.') return -1;
+ mask++;
+ break;
+ case '?':
+ break;
+ default:
+ if (dns_tolower(*name) != dns_tolower(*mask)) return -1;
+ }
+ }
+ if (*mask == '*') mask++;
+ return dns_tolower(*name) == dns_tolower(*mask) ? 0 : -1;
+}
diff --git a/src/lib-dns/dns-util.h b/src/lib-dns/dns-util.h
new file mode 100644
index 0000000..cff52ff
--- /dev/null
+++ b/src/lib-dns/dns-util.h
@@ -0,0 +1,36 @@
+#ifndef DNS_UTIL_H
+#define DNS_UTIL_H 1
+
+static inline char
+dns_tolower(char c)
+{
+ if (c >= 'A' && c <= 'Z')
+ c+='a'-'A';
+ return c;
+}
+
+/**
+ * Will compare names in accordance with RFC4343
+ */
+int dns_compare(const char *a, const char *b) ATTR_PURE;
+int dns_ncompare(const char *a, const char *b, size_t n) ATTR_PURE;
+
+/**
+ * Same as above but done by labels from right to left
+ *
+ * www.example.org and www.example.net would be compared as
+ * org = net (return first difference)
+ * example = example
+ * www = www
+ */
+int dns_compare_labels(const char *a, const char *b) ATTR_PURE;
+
+/**
+ * Will match names in RFC4592 style
+ *
+ * this means *.foo.bar will match name.foo.bar
+ * but *DOES NOT* match something.name.foo.bar
+ */
+int dns_match_wildcard(const char *name, const char *mask) ATTR_PURE;
+
+#endif
diff --git a/src/lib-dns/test-dns-util.c b/src/lib-dns/test-dns-util.c
new file mode 100644
index 0000000..15d5047
--- /dev/null
+++ b/src/lib-dns/test-dns-util.c
@@ -0,0 +1,124 @@
+/* Copyright (c) 2016-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "test-lib.h"
+#include "dns-util.h"
+#include "array.h"
+
+static void test_dns_compare(void)
+{
+ static const struct {
+ const char *a;
+ const char *b;
+ int res;
+ } tests[] =
+ {
+ { NULL, NULL, 0 },
+ { NULL, "", 1 },
+ { "", NULL, -1 },
+ { "", "", 0 },
+ { "a", "a", 0 },
+ { "a", "b", -1 },
+ { "b", "a", 1 },
+ { "A", "A", 0 },
+ { "A", "B", -1 },
+ { "B", "A", 1 },
+ { "A", "a", 0 },
+ { "a", "B", -1 },
+ { "B", "a", 1 },
+ { "test.invalid", "TeSt.InVaLid", 0 },
+ { "alphabet.com", "alpha.com", 52 },
+ { "com.alphabet", "com.alpha", 98 },
+ { "com.com", "com.comcom", -99 },
+ };
+
+ test_begin("test_dns_compare");
+
+ for(size_t i = 0; i < N_ELEMENTS(tests); i++) {
+ test_assert_idx(dns_compare(tests[i].a, tests[i].b) == tests[i].res, i);
+ test_assert_idx(dns_compare_labels(tests[i].a, tests[i].b) == tests[i].res, i);
+ }
+
+ test_end();
+}
+
+static void test_dns_match(void)
+{
+ static const struct {
+ const char *name;
+ const char *mask;
+ int res;
+ } tests[] =
+ {
+ { "", "", 0 },
+ { "", "*", 0 },
+ { "*", "", -1 },
+ { "TeSt.InVaLid", "test.invalid", 0 },
+ { "contoso.com", "test.invalid", -1 },
+ { "test.invalid", "test.unvalid", -1 },
+ { "name.test.invalid", "*.test.invalid", 0 },
+ { "real.name.test.invalid", "*.test.invalid", -1 },
+ { "real.name.test.invalid", "*.*.test.invalid", 0 },
+ { "name.test.invalid", "*name*.test.invalid", -1 },
+ { "name.invalid", "name.*", -1 },
+ };
+
+ test_begin("test_dns_match");
+
+ for(size_t i = 0; i < N_ELEMENTS(tests); i++) {
+ test_assert_idx(dns_match_wildcard(tests[i].name, tests[i].mask) == tests[i].res, i);
+ }
+
+ test_end();
+}
+
+static int
+arr_dns_compare(const char *const *a, const char *const *b)
+{
+ return dns_compare_labels(*a,*b);
+}
+
+static void test_dns_sort(void)
+{
+ const char *input[] = {
+ "test.invalid",
+ "test.com",
+ "test.contoso.com",
+ "test.alphabet.com",
+ "test.xxx",
+ };
+
+ const char *output[] = {
+ "test.alphabet.com",
+ "test.contoso.com",
+ "test.com",
+ "test.invalid",
+ "test.xxx",
+ };
+
+ test_begin("test_dns_sort");
+
+ ARRAY_TYPE(const_string) arr;
+ t_array_init(&arr, 8);
+
+ array_append(&arr, input, N_ELEMENTS(input));
+
+ array_sort(&arr, arr_dns_compare);
+
+ for(size_t i = 0; i < N_ELEMENTS(output); i++) {
+ const char *str = array_idx_elem(&arr, i);
+ test_assert_idx(dns_compare(str, output[i]) == 0, i);
+ }
+
+ test_end();
+}
+
+int main(void) {
+ void (*const test_functions[])(void) = {
+ test_dns_compare,
+ test_dns_match,
+ test_dns_sort,
+ NULL
+ };
+ return test_run(test_functions);
+}
diff --git a/src/lib-dovecot/Makefile.am b/src/lib-dovecot/Makefile.am
new file mode 100644
index 0000000..7856cf3
--- /dev/null
+++ b/src/lib-dovecot/Makefile.am
@@ -0,0 +1,11 @@
+pkglib_LTLIBRARIES = libdovecot.la
+
+libdovecot_la_SOURCES =
+
+libdovecot_la_LIBADD = \
+ $(LIBDOVECOT_LA_LIBS) \
+ $(MODULE_LIBS) \
+ $(RELRO_LDFLAGS)
+
+libdovecot_la_DEPENDENCIES = $(LIBDOVECOT_LA_LIBS)
+libdovecot_la_LDFLAGS = -export-dynamic
diff --git a/src/lib-dovecot/Makefile.in b/src/lib-dovecot/Makefile.in
new file mode 100644
index 0000000..2815a36
--- /dev/null
+++ b/src/lib-dovecot/Makefile.in
@@ -0,0 +1,705 @@
+# Makefile.in generated by automake 1.16.1 from Makefile.am.
+# @configure_input@
+
+# Copyright (C) 1994-2018 Free Software Foundation, Inc.
+
+# This Makefile.in is free software; the Free Software Foundation
+# gives unlimited permission to copy and/or distribute it,
+# with or without modifications, as long as this notice is preserved.
+
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY, to the extent permitted by law; without
+# even the implied warranty of MERCHANTABILITY or FITNESS FOR A
+# PARTICULAR PURPOSE.
+
+@SET_MAKE@
+
+VPATH = @srcdir@
+am__is_gnu_make = { \
+ if test -z '$(MAKELEVEL)'; then \
+ false; \
+ elif test -n '$(MAKE_HOST)'; then \
+ true; \
+ elif test -n '$(MAKE_VERSION)' && test -n '$(CURDIR)'; then \
+ true; \
+ else \
+ false; \
+ fi; \
+}
+am__make_running_with_option = \
+ case $${target_option-} in \
+ ?) ;; \
+ *) echo "am__make_running_with_option: internal error: invalid" \
+ "target option '$${target_option-}' specified" >&2; \
+ exit 1;; \
+ esac; \
+ has_opt=no; \
+ sane_makeflags=$$MAKEFLAGS; \
+ if $(am__is_gnu_make); then \
+ sane_makeflags=$$MFLAGS; \
+ else \
+ case $$MAKEFLAGS in \
+ *\\[\ \ ]*) \
+ bs=\\; \
+ sane_makeflags=`printf '%s\n' "$$MAKEFLAGS" \
+ | sed "s/$$bs$$bs[$$bs $$bs ]*//g"`;; \
+ esac; \
+ fi; \
+ skip_next=no; \
+ strip_trailopt () \
+ { \
+ flg=`printf '%s\n' "$$flg" | sed "s/$$1.*$$//"`; \
+ }; \
+ for flg in $$sane_makeflags; do \
+ test $$skip_next = yes && { skip_next=no; continue; }; \
+ case $$flg in \
+ *=*|--*) continue;; \
+ -*I) strip_trailopt 'I'; skip_next=yes;; \
+ -*I?*) strip_trailopt 'I';; \
+ -*O) strip_trailopt 'O'; skip_next=yes;; \
+ -*O?*) strip_trailopt 'O';; \
+ -*l) strip_trailopt 'l'; skip_next=yes;; \
+ -*l?*) strip_trailopt 'l';; \
+ -[dEDm]) skip_next=yes;; \
+ -[JT]) skip_next=yes;; \
+ esac; \
+ case $$flg in \
+ *$$target_option*) has_opt=yes; break;; \
+ esac; \
+ done; \
+ test $$has_opt = yes
+am__make_dryrun = (target_option=n; $(am__make_running_with_option))
+am__make_keepgoing = (target_option=k; $(am__make_running_with_option))
+pkgdatadir = $(datadir)/@PACKAGE@
+pkgincludedir = $(includedir)/@PACKAGE@
+pkglibdir = $(libdir)/@PACKAGE@
+pkglibexecdir = $(libexecdir)/@PACKAGE@
+am__cd = CDPATH="$${ZSH_VERSION+.}$(PATH_SEPARATOR)" && cd
+install_sh_DATA = $(install_sh) -c -m 644
+install_sh_PROGRAM = $(install_sh) -c
+install_sh_SCRIPT = $(install_sh) -c
+INSTALL_HEADER = $(INSTALL_DATA)
+transform = $(program_transform_name)
+NORMAL_INSTALL = :
+PRE_INSTALL = :
+POST_INSTALL = :
+NORMAL_UNINSTALL = :
+PRE_UNINSTALL = :
+POST_UNINSTALL = :
+build_triplet = @build@
+host_triplet = @host@
+subdir = src/lib-dovecot
+ACLOCAL_M4 = $(top_srcdir)/aclocal.m4
+am__aclocal_m4_deps = $(top_srcdir)/m4/ac_checktype2.m4 \
+ $(top_srcdir)/m4/ac_typeof.m4 $(top_srcdir)/m4/arc4random.m4 \
+ $(top_srcdir)/m4/blockdev.m4 $(top_srcdir)/m4/c99_vsnprintf.m4 \
+ $(top_srcdir)/m4/clock_gettime.m4 $(top_srcdir)/m4/crypt.m4 \
+ $(top_srcdir)/m4/crypt_xpg6.m4 $(top_srcdir)/m4/dbqlk.m4 \
+ $(top_srcdir)/m4/dirent_dtype.m4 $(top_srcdir)/m4/dovecot.m4 \
+ $(top_srcdir)/m4/fd_passing.m4 $(top_srcdir)/m4/fdatasync.m4 \
+ $(top_srcdir)/m4/flexible_array_member.m4 \
+ $(top_srcdir)/m4/glibc.m4 $(top_srcdir)/m4/gmtime_max.m4 \
+ $(top_srcdir)/m4/gmtime_tm_gmtoff.m4 \
+ $(top_srcdir)/m4/ioloop.m4 $(top_srcdir)/m4/iovec.m4 \
+ $(top_srcdir)/m4/ipv6.m4 $(top_srcdir)/m4/libcap.m4 \
+ $(top_srcdir)/m4/libtool.m4 $(top_srcdir)/m4/libwrap.m4 \
+ $(top_srcdir)/m4/linux_mremap.m4 $(top_srcdir)/m4/ltoptions.m4 \
+ $(top_srcdir)/m4/ltsugar.m4 $(top_srcdir)/m4/ltversion.m4 \
+ $(top_srcdir)/m4/lt~obsolete.m4 $(top_srcdir)/m4/mmap_write.m4 \
+ $(top_srcdir)/m4/mntctl.m4 $(top_srcdir)/m4/modules.m4 \
+ $(top_srcdir)/m4/notify.m4 $(top_srcdir)/m4/nsl.m4 \
+ $(top_srcdir)/m4/off_t_max.m4 $(top_srcdir)/m4/pkg.m4 \
+ $(top_srcdir)/m4/pr_set_dumpable.m4 \
+ $(top_srcdir)/m4/q_quotactl.m4 $(top_srcdir)/m4/quota.m4 \
+ $(top_srcdir)/m4/random.m4 $(top_srcdir)/m4/rlimit.m4 \
+ $(top_srcdir)/m4/sendfile.m4 $(top_srcdir)/m4/size_t_signed.m4 \
+ $(top_srcdir)/m4/sockpeercred.m4 $(top_srcdir)/m4/sql.m4 \
+ $(top_srcdir)/m4/ssl.m4 $(top_srcdir)/m4/st_tim.m4 \
+ $(top_srcdir)/m4/static_array.m4 $(top_srcdir)/m4/test_with.m4 \
+ $(top_srcdir)/m4/time_t.m4 $(top_srcdir)/m4/typeof.m4 \
+ $(top_srcdir)/m4/typeof_dev_t.m4 \
+ $(top_srcdir)/m4/uoff_t_max.m4 $(top_srcdir)/m4/vararg.m4 \
+ $(top_srcdir)/m4/want_apparmor.m4 \
+ $(top_srcdir)/m4/want_bsdauth.m4 \
+ $(top_srcdir)/m4/want_bzlib.m4 \
+ $(top_srcdir)/m4/want_cassandra.m4 \
+ $(top_srcdir)/m4/want_cdb.m4 \
+ $(top_srcdir)/m4/want_checkpassword.m4 \
+ $(top_srcdir)/m4/want_clucene.m4 $(top_srcdir)/m4/want_db.m4 \
+ $(top_srcdir)/m4/want_gssapi.m4 $(top_srcdir)/m4/want_icu.m4 \
+ $(top_srcdir)/m4/want_ldap.m4 $(top_srcdir)/m4/want_lua.m4 \
+ $(top_srcdir)/m4/want_lz4.m4 $(top_srcdir)/m4/want_lzma.m4 \
+ $(top_srcdir)/m4/want_mysql.m4 $(top_srcdir)/m4/want_pam.m4 \
+ $(top_srcdir)/m4/want_passwd.m4 $(top_srcdir)/m4/want_pgsql.m4 \
+ $(top_srcdir)/m4/want_prefetch.m4 \
+ $(top_srcdir)/m4/want_shadow.m4 \
+ $(top_srcdir)/m4/want_sodium.m4 $(top_srcdir)/m4/want_solr.m4 \
+ $(top_srcdir)/m4/want_sqlite.m4 \
+ $(top_srcdir)/m4/want_stemmer.m4 \
+ $(top_srcdir)/m4/want_systemd.m4 \
+ $(top_srcdir)/m4/want_textcat.m4 \
+ $(top_srcdir)/m4/want_unwind.m4 $(top_srcdir)/m4/want_zlib.m4 \
+ $(top_srcdir)/m4/want_zstd.m4 $(top_srcdir)/configure.ac
+am__configure_deps = $(am__aclocal_m4_deps) $(CONFIGURE_DEPENDENCIES) \
+ $(ACLOCAL_M4)
+DIST_COMMON = $(srcdir)/Makefile.am $(am__DIST_COMMON)
+mkinstalldirs = $(install_sh) -d
+CONFIG_HEADER = $(top_builddir)/config.h
+CONFIG_CLEAN_FILES =
+CONFIG_CLEAN_VPATH_FILES =
+am__vpath_adj_setup = srcdirstrip=`echo "$(srcdir)" | sed 's|.|.|g'`;
+am__vpath_adj = case $$p in \
+ $(srcdir)/*) f=`echo "$$p" | sed "s|^$$srcdirstrip/||"`;; \
+ *) f=$$p;; \
+ esac;
+am__strip_dir = f=`echo $$p | sed -e 's|^.*/||'`;
+am__install_max = 40
+am__nobase_strip_setup = \
+ srcdirstrip=`echo "$(srcdir)" | sed 's/[].[^$$\\*|]/\\\\&/g'`
+am__nobase_strip = \
+ for p in $$list; do echo "$$p"; done | sed -e "s|$$srcdirstrip/||"
+am__nobase_list = $(am__nobase_strip_setup); \
+ for p in $$list; do echo "$$p $$p"; done | \
+ sed "s| $$srcdirstrip/| |;"' / .*\//!s/ .*/ ./; s,\( .*\)/[^/]*$$,\1,' | \
+ $(AWK) 'BEGIN { files["."] = "" } { files[$$2] = files[$$2] " " $$1; \
+ if (++n[$$2] == $(am__install_max)) \
+ { print $$2, files[$$2]; n[$$2] = 0; files[$$2] = "" } } \
+ END { for (dir in files) print dir, files[dir] }'
+am__base_list = \
+ sed '$$!N;$$!N;$$!N;$$!N;$$!N;$$!N;$$!N;s/\n/ /g' | \
+ sed '$$!N;$$!N;$$!N;$$!N;s/\n/ /g'
+am__uninstall_files_from_dir = { \
+ test -z "$$files" \
+ || { test ! -d "$$dir" && test ! -f "$$dir" && test ! -r "$$dir"; } \
+ || { echo " ( cd '$$dir' && rm -f" $$files ")"; \
+ $(am__cd) "$$dir" && rm -f $$files; }; \
+ }
+am__installdirs = "$(DESTDIR)$(pkglibdir)"
+LTLIBRARIES = $(pkglib_LTLIBRARIES)
+am__DEPENDENCIES_1 =
+am_libdovecot_la_OBJECTS =
+libdovecot_la_OBJECTS = $(am_libdovecot_la_OBJECTS)
+AM_V_lt = $(am__v_lt_@AM_V@)
+am__v_lt_ = $(am__v_lt_@AM_DEFAULT_V@)
+am__v_lt_0 = --silent
+am__v_lt_1 =
+libdovecot_la_LINK = $(LIBTOOL) $(AM_V_lt) --tag=CC $(AM_LIBTOOLFLAGS) \
+ $(LIBTOOLFLAGS) --mode=link $(CCLD) $(AM_CFLAGS) $(CFLAGS) \
+ $(libdovecot_la_LDFLAGS) $(LDFLAGS) -o $@
+AM_V_P = $(am__v_P_@AM_V@)
+am__v_P_ = $(am__v_P_@AM_DEFAULT_V@)
+am__v_P_0 = false
+am__v_P_1 = :
+AM_V_GEN = $(am__v_GEN_@AM_V@)
+am__v_GEN_ = $(am__v_GEN_@AM_DEFAULT_V@)
+am__v_GEN_0 = @echo " GEN " $@;
+am__v_GEN_1 =
+AM_V_at = $(am__v_at_@AM_V@)
+am__v_at_ = $(am__v_at_@AM_DEFAULT_V@)
+am__v_at_0 = @
+am__v_at_1 =
+DEFAULT_INCLUDES = -I.@am__isrc@ -I$(top_builddir)
+COMPILE = $(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) \
+ $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS)
+LTCOMPILE = $(LIBTOOL) $(AM_V_lt) --tag=CC $(AM_LIBTOOLFLAGS) \
+ $(LIBTOOLFLAGS) --mode=compile $(CC) $(DEFS) \
+ $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) $(CPPFLAGS) \
+ $(AM_CFLAGS) $(CFLAGS)
+AM_V_CC = $(am__v_CC_@AM_V@)
+am__v_CC_ = $(am__v_CC_@AM_DEFAULT_V@)
+am__v_CC_0 = @echo " CC " $@;
+am__v_CC_1 =
+CCLD = $(CC)
+LINK = $(LIBTOOL) $(AM_V_lt) --tag=CC $(AM_LIBTOOLFLAGS) \
+ $(LIBTOOLFLAGS) --mode=link $(CCLD) $(AM_CFLAGS) $(CFLAGS) \
+ $(AM_LDFLAGS) $(LDFLAGS) -o $@
+AM_V_CCLD = $(am__v_CCLD_@AM_V@)
+am__v_CCLD_ = $(am__v_CCLD_@AM_DEFAULT_V@)
+am__v_CCLD_0 = @echo " CCLD " $@;
+am__v_CCLD_1 =
+SOURCES = $(libdovecot_la_SOURCES)
+DIST_SOURCES = $(libdovecot_la_SOURCES)
+am__can_run_installinfo = \
+ case $$AM_UPDATE_INFO_DIR in \
+ n|no|NO) false;; \
+ *) (install-info --version) >/dev/null 2>&1;; \
+ esac
+am__tagged_files = $(HEADERS) $(SOURCES) $(TAGS_FILES) $(LISP)
+am__DIST_COMMON = $(srcdir)/Makefile.in
+DISTFILES = $(DIST_COMMON) $(DIST_SOURCES) $(TEXINFOS) $(EXTRA_DIST)
+ACLOCAL = @ACLOCAL@
+ACLOCAL_AMFLAGS = @ACLOCAL_AMFLAGS@
+AMTAR = @AMTAR@
+AM_DEFAULT_VERBOSITY = @AM_DEFAULT_VERBOSITY@
+APPARMOR_LIBS = @APPARMOR_LIBS@
+AR = @AR@
+AUTH_CFLAGS = @AUTH_CFLAGS@
+AUTH_LIBS = @AUTH_LIBS@
+AUTOCONF = @AUTOCONF@
+AUTOHEADER = @AUTOHEADER@
+AUTOMAKE = @AUTOMAKE@
+AWK = @AWK@
+BINARY_CFLAGS = @BINARY_CFLAGS@
+BINARY_LDFLAGS = @BINARY_LDFLAGS@
+BISON = @BISON@
+CASSANDRA_CFLAGS = @CASSANDRA_CFLAGS@
+CASSANDRA_LIBS = @CASSANDRA_LIBS@
+CC = @CC@
+CCDEPMODE = @CCDEPMODE@
+CDB_LIBS = @CDB_LIBS@
+CFLAGS = @CFLAGS@
+CLUCENE_CFLAGS = @CLUCENE_CFLAGS@
+CLUCENE_LIBS = @CLUCENE_LIBS@
+COMPRESS_LIBS = @COMPRESS_LIBS@
+CPP = @CPP@
+CPPFLAGS = @CPPFLAGS@
+CRYPT_LIBS = @CRYPT_LIBS@
+CXX = @CXX@
+CXXCPP = @CXXCPP@
+CXXDEPMODE = @CXXDEPMODE@
+CXXFLAGS = @CXXFLAGS@
+CYGPATH_W = @CYGPATH_W@
+DEFS = @DEFS@
+DEPDIR = @DEPDIR@
+DICT_LIBS = @DICT_LIBS@
+DLLIB = @DLLIB@
+DLLTOOL = @DLLTOOL@
+DSYMUTIL = @DSYMUTIL@
+DUMPBIN = @DUMPBIN@
+ECHO_C = @ECHO_C@
+ECHO_N = @ECHO_N@
+ECHO_T = @ECHO_T@
+EGREP = @EGREP@
+EXEEXT = @EXEEXT@
+FGREP = @FGREP@
+FLEX = @FLEX@
+FUZZER_CPPFLAGS = @FUZZER_CPPFLAGS@
+FUZZER_LDFLAGS = @FUZZER_LDFLAGS@
+GREP = @GREP@
+INSTALL = @INSTALL@
+INSTALL_DATA = @INSTALL_DATA@
+INSTALL_PROGRAM = @INSTALL_PROGRAM@
+INSTALL_SCRIPT = @INSTALL_SCRIPT@
+INSTALL_STRIP_PROGRAM = @INSTALL_STRIP_PROGRAM@
+KRB5CONFIG = @KRB5CONFIG@
+KRB5_CFLAGS = @KRB5_CFLAGS@
+KRB5_LIBS = @KRB5_LIBS@
+LD = @LD@
+LDAP_LIBS = @LDAP_LIBS@
+LDFLAGS = @LDFLAGS@
+LD_NO_WHOLE_ARCHIVE = @LD_NO_WHOLE_ARCHIVE@
+LD_WHOLE_ARCHIVE = @LD_WHOLE_ARCHIVE@
+LIBCAP = @LIBCAP@
+LIBDOVECOT = @LIBDOVECOT@
+LIBDOVECOT_COMPRESS = @LIBDOVECOT_COMPRESS@
+LIBDOVECOT_DEPS = @LIBDOVECOT_DEPS@
+LIBDOVECOT_DSYNC = @LIBDOVECOT_DSYNC@
+LIBDOVECOT_LA_LIBS = @LIBDOVECOT_LA_LIBS@
+LIBDOVECOT_LDA = @LIBDOVECOT_LDA@
+LIBDOVECOT_LDAP = @LIBDOVECOT_LDAP@
+LIBDOVECOT_LIBFTS = @LIBDOVECOT_LIBFTS@
+LIBDOVECOT_LIBFTS_DEPS = @LIBDOVECOT_LIBFTS_DEPS@
+LIBDOVECOT_LOGIN = @LIBDOVECOT_LOGIN@
+LIBDOVECOT_LUA = @LIBDOVECOT_LUA@
+LIBDOVECOT_LUA_DEPS = @LIBDOVECOT_LUA_DEPS@
+LIBDOVECOT_SQL = @LIBDOVECOT_SQL@
+LIBDOVECOT_STORAGE = @LIBDOVECOT_STORAGE@
+LIBDOVECOT_STORAGE_DEPS = @LIBDOVECOT_STORAGE_DEPS@
+LIBEXTTEXTCAT_CFLAGS = @LIBEXTTEXTCAT_CFLAGS@
+LIBEXTTEXTCAT_LIBS = @LIBEXTTEXTCAT_LIBS@
+LIBICONV = @LIBICONV@
+LIBICU_CFLAGS = @LIBICU_CFLAGS@
+LIBICU_LIBS = @LIBICU_LIBS@
+LIBOBJS = @LIBOBJS@
+LIBS = @LIBS@
+LIBSODIUM_CFLAGS = @LIBSODIUM_CFLAGS@
+LIBSODIUM_LIBS = @LIBSODIUM_LIBS@
+LIBTIRPC_CFLAGS = @LIBTIRPC_CFLAGS@
+LIBTIRPC_LIBS = @LIBTIRPC_LIBS@
+LIBTOOL = @LIBTOOL@
+LIBUNWIND_CFLAGS = @LIBUNWIND_CFLAGS@
+LIBUNWIND_LIBS = @LIBUNWIND_LIBS@
+LIBWRAP_LIBS = @LIBWRAP_LIBS@
+LINKED_STORAGE_LDADD = @LINKED_STORAGE_LDADD@
+LIPO = @LIPO@
+LN_S = @LN_S@
+LTLIBICONV = @LTLIBICONV@
+LTLIBOBJS = @LTLIBOBJS@
+LT_SYS_LIBRARY_PATH = @LT_SYS_LIBRARY_PATH@
+LUA_CFLAGS = @LUA_CFLAGS@
+LUA_LIBS = @LUA_LIBS@
+MAINT = @MAINT@
+MAKEINFO = @MAKEINFO@
+MANIFEST_TOOL = @MANIFEST_TOOL@
+MKDIR_P = @MKDIR_P@
+MODULE_LIBS = @MODULE_LIBS@
+MODULE_SUFFIX = @MODULE_SUFFIX@
+MYSQL_CFLAGS = @MYSQL_CFLAGS@
+MYSQL_CONFIG = @MYSQL_CONFIG@
+MYSQL_LIBS = @MYSQL_LIBS@
+NM = @NM@
+NMEDIT = @NMEDIT@
+NOPLUGIN_LDFLAGS = @NOPLUGIN_LDFLAGS@
+OBJDUMP = @OBJDUMP@
+OBJEXT = @OBJEXT@
+OTOOL = @OTOOL@
+OTOOL64 = @OTOOL64@
+PACKAGE = @PACKAGE@
+PACKAGE_BUGREPORT = @PACKAGE_BUGREPORT@
+PACKAGE_NAME = @PACKAGE_NAME@
+PACKAGE_STRING = @PACKAGE_STRING@
+PACKAGE_TARNAME = @PACKAGE_TARNAME@
+PACKAGE_URL = @PACKAGE_URL@
+PACKAGE_VERSION = @PACKAGE_VERSION@
+PANDOC = @PANDOC@
+PATH_SEPARATOR = @PATH_SEPARATOR@
+PGSQL_CFLAGS = @PGSQL_CFLAGS@
+PGSQL_LIBS = @PGSQL_LIBS@
+PG_CONFIG = @PG_CONFIG@
+PIE_CFLAGS = @PIE_CFLAGS@
+PIE_LDFLAGS = @PIE_LDFLAGS@
+PKG_CONFIG = @PKG_CONFIG@
+PKG_CONFIG_LIBDIR = @PKG_CONFIG_LIBDIR@
+PKG_CONFIG_PATH = @PKG_CONFIG_PATH@
+QUOTA_LIBS = @QUOTA_LIBS@
+RANLIB = @RANLIB@
+RELRO_LDFLAGS = @RELRO_LDFLAGS@
+RPCGEN = @RPCGEN@
+RUN_TEST = @RUN_TEST@
+SED = @SED@
+SETTING_FILES = @SETTING_FILES@
+SET_MAKE = @SET_MAKE@
+SHELL = @SHELL@
+SQLITE_CFLAGS = @SQLITE_CFLAGS@
+SQLITE_LIBS = @SQLITE_LIBS@
+SQL_CFLAGS = @SQL_CFLAGS@
+SQL_LIBS = @SQL_LIBS@
+SSL_CFLAGS = @SSL_CFLAGS@
+SSL_LIBS = @SSL_LIBS@
+STRIP = @STRIP@
+SYSTEMD_CFLAGS = @SYSTEMD_CFLAGS@
+SYSTEMD_LIBS = @SYSTEMD_LIBS@
+VALGRIND = @VALGRIND@
+VERSION = @VERSION@
+ZSTD_CFLAGS = @ZSTD_CFLAGS@
+ZSTD_LIBS = @ZSTD_LIBS@
+abs_builddir = @abs_builddir@
+abs_srcdir = @abs_srcdir@
+abs_top_builddir = @abs_top_builddir@
+abs_top_srcdir = @abs_top_srcdir@
+ac_ct_AR = @ac_ct_AR@
+ac_ct_CC = @ac_ct_CC@
+ac_ct_CXX = @ac_ct_CXX@
+ac_ct_DUMPBIN = @ac_ct_DUMPBIN@
+am__include = @am__include@
+am__leading_dot = @am__leading_dot@
+am__quote = @am__quote@
+am__tar = @am__tar@
+am__untar = @am__untar@
+bindir = @bindir@
+build = @build@
+build_alias = @build_alias@
+build_cpu = @build_cpu@
+build_os = @build_os@
+build_vendor = @build_vendor@
+builddir = @builddir@
+datadir = @datadir@
+datarootdir = @datarootdir@
+dict_drivers = @dict_drivers@
+docdir = @docdir@
+dvidir = @dvidir@
+exec_prefix = @exec_prefix@
+host = @host@
+host_alias = @host_alias@
+host_cpu = @host_cpu@
+host_os = @host_os@
+host_vendor = @host_vendor@
+htmldir = @htmldir@
+includedir = @includedir@
+infodir = @infodir@
+install_sh = @install_sh@
+libdir = @libdir@
+libexecdir = @libexecdir@
+localedir = @localedir@
+localstatedir = @localstatedir@
+mandir = @mandir@
+mkdir_p = @mkdir_p@
+moduledir = @moduledir@
+oldincludedir = @oldincludedir@
+pdfdir = @pdfdir@
+prefix = @prefix@
+program_transform_name = @program_transform_name@
+psdir = @psdir@
+rundir = @rundir@
+runstatedir = @runstatedir@
+sbindir = @sbindir@
+sharedstatedir = @sharedstatedir@
+sql_drivers = @sql_drivers@
+srcdir = @srcdir@
+ssldir = @ssldir@
+statedir = @statedir@
+sysconfdir = @sysconfdir@
+systemdservicetype = @systemdservicetype@
+systemdsystemunitdir = @systemdsystemunitdir@
+target_alias = @target_alias@
+top_build_prefix = @top_build_prefix@
+top_builddir = @top_builddir@
+top_srcdir = @top_srcdir@
+pkglib_LTLIBRARIES = libdovecot.la
+libdovecot_la_SOURCES =
+libdovecot_la_LIBADD = \
+ $(LIBDOVECOT_LA_LIBS) \
+ $(MODULE_LIBS) \
+ $(RELRO_LDFLAGS)
+
+libdovecot_la_DEPENDENCIES = $(LIBDOVECOT_LA_LIBS)
+libdovecot_la_LDFLAGS = -export-dynamic
+all: all-am
+
+.SUFFIXES:
+$(srcdir)/Makefile.in: @MAINTAINER_MODE_TRUE@ $(srcdir)/Makefile.am $(am__configure_deps)
+ @for dep in $?; do \
+ case '$(am__configure_deps)' in \
+ *$$dep*) \
+ ( cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh ) \
+ && { if test -f $@; then exit 0; else break; fi; }; \
+ exit 1;; \
+ esac; \
+ done; \
+ echo ' cd $(top_srcdir) && $(AUTOMAKE) --foreign src/lib-dovecot/Makefile'; \
+ $(am__cd) $(top_srcdir) && \
+ $(AUTOMAKE) --foreign src/lib-dovecot/Makefile
+Makefile: $(srcdir)/Makefile.in $(top_builddir)/config.status
+ @case '$?' in \
+ *config.status*) \
+ cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh;; \
+ *) \
+ echo ' cd $(top_builddir) && $(SHELL) ./config.status $(subdir)/$@ $(am__maybe_remake_depfiles)'; \
+ cd $(top_builddir) && $(SHELL) ./config.status $(subdir)/$@ $(am__maybe_remake_depfiles);; \
+ esac;
+
+$(top_builddir)/config.status: $(top_srcdir)/configure $(CONFIG_STATUS_DEPENDENCIES)
+ cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh
+
+$(top_srcdir)/configure: @MAINTAINER_MODE_TRUE@ $(am__configure_deps)
+ cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh
+$(ACLOCAL_M4): @MAINTAINER_MODE_TRUE@ $(am__aclocal_m4_deps)
+ cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh
+$(am__aclocal_m4_deps):
+
+install-pkglibLTLIBRARIES: $(pkglib_LTLIBRARIES)
+ @$(NORMAL_INSTALL)
+ @list='$(pkglib_LTLIBRARIES)'; test -n "$(pkglibdir)" || list=; \
+ list2=; for p in $$list; do \
+ if test -f $$p; then \
+ list2="$$list2 $$p"; \
+ else :; fi; \
+ done; \
+ test -z "$$list2" || { \
+ echo " $(MKDIR_P) '$(DESTDIR)$(pkglibdir)'"; \
+ $(MKDIR_P) "$(DESTDIR)$(pkglibdir)" || exit 1; \
+ echo " $(LIBTOOL) $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=install $(INSTALL) $(INSTALL_STRIP_FLAG) $$list2 '$(DESTDIR)$(pkglibdir)'"; \
+ $(LIBTOOL) $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=install $(INSTALL) $(INSTALL_STRIP_FLAG) $$list2 "$(DESTDIR)$(pkglibdir)"; \
+ }
+
+uninstall-pkglibLTLIBRARIES:
+ @$(NORMAL_UNINSTALL)
+ @list='$(pkglib_LTLIBRARIES)'; test -n "$(pkglibdir)" || list=; \
+ for p in $$list; do \
+ $(am__strip_dir) \
+ echo " $(LIBTOOL) $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=uninstall rm -f '$(DESTDIR)$(pkglibdir)/$$f'"; \
+ $(LIBTOOL) $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=uninstall rm -f "$(DESTDIR)$(pkglibdir)/$$f"; \
+ done
+
+clean-pkglibLTLIBRARIES:
+ -test -z "$(pkglib_LTLIBRARIES)" || rm -f $(pkglib_LTLIBRARIES)
+ @list='$(pkglib_LTLIBRARIES)'; \
+ locs=`for p in $$list; do echo $$p; done | \
+ sed 's|^[^/]*$$|.|; s|/[^/]*$$||; s|$$|/so_locations|' | \
+ sort -u`; \
+ test -z "$$locs" || { \
+ echo rm -f $${locs}; \
+ rm -f $${locs}; \
+ }
+
+libdovecot.la: $(libdovecot_la_OBJECTS) $(libdovecot_la_DEPENDENCIES) $(EXTRA_libdovecot_la_DEPENDENCIES)
+ $(AM_V_CCLD)$(libdovecot_la_LINK) -rpath $(pkglibdir) $(libdovecot_la_OBJECTS) $(libdovecot_la_LIBADD) $(LIBS)
+
+mostlyclean-compile:
+ -rm -f *.$(OBJEXT)
+
+distclean-compile:
+ -rm -f *.tab.c
+
+mostlyclean-libtool:
+ -rm -f *.lo
+
+clean-libtool:
+ -rm -rf .libs _libs
+tags TAGS:
+
+ctags CTAGS:
+
+cscope cscopelist:
+
+
+distdir: $(BUILT_SOURCES)
+ $(MAKE) $(AM_MAKEFLAGS) distdir-am
+
+distdir-am: $(DISTFILES)
+ @srcdirstrip=`echo "$(srcdir)" | sed 's/[].[^$$\\*]/\\\\&/g'`; \
+ topsrcdirstrip=`echo "$(top_srcdir)" | sed 's/[].[^$$\\*]/\\\\&/g'`; \
+ list='$(DISTFILES)'; \
+ dist_files=`for file in $$list; do echo $$file; done | \
+ sed -e "s|^$$srcdirstrip/||;t" \
+ -e "s|^$$topsrcdirstrip/|$(top_builddir)/|;t"`; \
+ case $$dist_files in \
+ */*) $(MKDIR_P) `echo "$$dist_files" | \
+ sed '/\//!d;s|^|$(distdir)/|;s,/[^/]*$$,,' | \
+ sort -u` ;; \
+ esac; \
+ for file in $$dist_files; do \
+ if test -f $$file || test -d $$file; then d=.; else d=$(srcdir); fi; \
+ if test -d $$d/$$file; then \
+ dir=`echo "/$$file" | sed -e 's,/[^/]*$$,,'`; \
+ if test -d "$(distdir)/$$file"; then \
+ find "$(distdir)/$$file" -type d ! -perm -700 -exec chmod u+rwx {} \;; \
+ fi; \
+ if test -d $(srcdir)/$$file && test $$d != $(srcdir); then \
+ cp -fpR $(srcdir)/$$file "$(distdir)$$dir" || exit 1; \
+ find "$(distdir)/$$file" -type d ! -perm -700 -exec chmod u+rwx {} \;; \
+ fi; \
+ cp -fpR $$d/$$file "$(distdir)$$dir" || exit 1; \
+ else \
+ test -f "$(distdir)/$$file" \
+ || cp -p $$d/$$file "$(distdir)/$$file" \
+ || exit 1; \
+ fi; \
+ done
+check-am: all-am
+check: check-am
+all-am: Makefile $(LTLIBRARIES)
+installdirs:
+ for dir in "$(DESTDIR)$(pkglibdir)"; do \
+ test -z "$$dir" || $(MKDIR_P) "$$dir"; \
+ done
+install: install-am
+install-exec: install-exec-am
+install-data: install-data-am
+uninstall: uninstall-am
+
+install-am: all-am
+ @$(MAKE) $(AM_MAKEFLAGS) install-exec-am install-data-am
+
+installcheck: installcheck-am
+install-strip:
+ if test -z '$(STRIP)'; then \
+ $(MAKE) $(AM_MAKEFLAGS) INSTALL_PROGRAM="$(INSTALL_STRIP_PROGRAM)" \
+ install_sh_PROGRAM="$(INSTALL_STRIP_PROGRAM)" INSTALL_STRIP_FLAG=-s \
+ install; \
+ else \
+ $(MAKE) $(AM_MAKEFLAGS) INSTALL_PROGRAM="$(INSTALL_STRIP_PROGRAM)" \
+ install_sh_PROGRAM="$(INSTALL_STRIP_PROGRAM)" INSTALL_STRIP_FLAG=-s \
+ "INSTALL_PROGRAM_ENV=STRIPPROG='$(STRIP)'" install; \
+ fi
+mostlyclean-generic:
+
+clean-generic:
+
+distclean-generic:
+ -test -z "$(CONFIG_CLEAN_FILES)" || rm -f $(CONFIG_CLEAN_FILES)
+ -test . = "$(srcdir)" || test -z "$(CONFIG_CLEAN_VPATH_FILES)" || rm -f $(CONFIG_CLEAN_VPATH_FILES)
+
+maintainer-clean-generic:
+ @echo "This command is intended for maintainers to use"
+ @echo "it deletes files that may require special tools to rebuild."
+clean: clean-am
+
+clean-am: clean-generic clean-libtool clean-pkglibLTLIBRARIES \
+ mostlyclean-am
+
+distclean: distclean-am
+ -rm -f Makefile
+distclean-am: clean-am distclean-compile distclean-generic
+
+dvi: dvi-am
+
+dvi-am:
+
+html: html-am
+
+html-am:
+
+info: info-am
+
+info-am:
+
+install-data-am:
+
+install-dvi: install-dvi-am
+
+install-dvi-am:
+
+install-exec-am: install-pkglibLTLIBRARIES
+
+install-html: install-html-am
+
+install-html-am:
+
+install-info: install-info-am
+
+install-info-am:
+
+install-man:
+
+install-pdf: install-pdf-am
+
+install-pdf-am:
+
+install-ps: install-ps-am
+
+install-ps-am:
+
+installcheck-am:
+
+maintainer-clean: maintainer-clean-am
+ -rm -f Makefile
+maintainer-clean-am: distclean-am maintainer-clean-generic
+
+mostlyclean: mostlyclean-am
+
+mostlyclean-am: mostlyclean-compile mostlyclean-generic \
+ mostlyclean-libtool
+
+pdf: pdf-am
+
+pdf-am:
+
+ps: ps-am
+
+ps-am:
+
+uninstall-am: uninstall-pkglibLTLIBRARIES
+
+.MAKE: install-am install-strip
+
+.PHONY: all all-am check check-am clean clean-generic clean-libtool \
+ clean-pkglibLTLIBRARIES cscopelist-am ctags-am distclean \
+ distclean-compile distclean-generic distclean-libtool distdir \
+ dvi dvi-am html html-am info info-am install install-am \
+ install-data install-data-am install-dvi install-dvi-am \
+ install-exec install-exec-am install-html install-html-am \
+ install-info install-info-am install-man install-pdf \
+ install-pdf-am install-pkglibLTLIBRARIES install-ps \
+ install-ps-am install-strip installcheck installcheck-am \
+ installdirs maintainer-clean maintainer-clean-generic \
+ mostlyclean mostlyclean-compile mostlyclean-generic \
+ mostlyclean-libtool pdf pdf-am ps ps-am tags-am uninstall \
+ uninstall-am uninstall-pkglibLTLIBRARIES
+
+.PRECIOUS: Makefile
+
+
+# Tell versions [3.59,3.63) of GNU make to not export all variables.
+# Otherwise a system limit (for SysV at least) may be exceeded.
+.NOEXPORT:
diff --git a/src/lib-fs/Makefile.am b/src/lib-fs/Makefile.am
new file mode 100644
index 0000000..e5ae056
--- /dev/null
+++ b/src/lib-fs/Makefile.am
@@ -0,0 +1,70 @@
+noinst_LTLIBRARIES = libfs.la
+
+AM_CPPFLAGS = \
+ -I$(top_srcdir)/src/lib \
+ -I$(top_srcdir)/src/lib-test \
+ -I$(top_srcdir)/src/lib-dict \
+ -I$(top_srcdir)/src/lib-ssl-iostream \
+ -DMODULE_DIR=\""$(moduledir)"\"
+
+libfs_la_SOURCES = \
+ fs-api.c \
+ fs-dict.c \
+ fs-metawrap.c \
+ fs-randomfail.c \
+ fs-posix.c \
+ fs-test.c \
+ fs-test-async.c \
+ fs-sis.c \
+ fs-sis-common.c \
+ fs-sis-queue.c \
+ fs-wrapper.c \
+ istream-fs-file.c \
+ istream-fs-stats.c \
+ istream-metawrap.c \
+ ostream-metawrap.c \
+ ostream-cmp.c
+
+headers = \
+ fs-api.h \
+ fs-api-private.h \
+ fs-sis-common.h \
+ fs-wrapper.h \
+ fs-test.h \
+ istream-fs-file.h \
+ istream-fs-stats.h \
+ istream-metawrap.h \
+ ostream-metawrap.h \
+ ostream-cmp.h
+
+pkginc_libdir=$(pkgincludedir)
+pkginc_lib_HEADERS = $(headers)
+
+noinst_PROGRAMS = $(test_programs)
+
+test_programs = \
+ test-fs-metawrap \
+ test-fs-posix
+
+test_deps = \
+ $(noinst_LTLIBRARIES) \
+ ../lib-dict/libdict.la \
+ ../lib-test/libtest.la \
+ ../lib/liblib.la
+
+test_libs = \
+ $(test_deps) \
+ $(MODULE_LIBS)
+
+test_fs_metawrap_SOURCES = test-fs-metawrap.c
+test_fs_metawrap_LDADD = $(test_libs)
+test_fs_metawrap_DEPENDENCIES = $(test_deps)
+
+test_fs_posix_SOURCES = test-fs-posix.c
+test_fs_posix_LDADD = $(test_libs)
+test_fs_posix_DEPENDENCIES = $(test_deps)
+
+check-local:
+ for bin in $(test_programs); do \
+ if ! $(RUN_TEST) ./$$bin; then exit 1; fi; \
+ done
diff --git a/src/lib-fs/Makefile.in b/src/lib-fs/Makefile.in
new file mode 100644
index 0000000..c31cc22
--- /dev/null
+++ b/src/lib-fs/Makefile.in
@@ -0,0 +1,955 @@
+# Makefile.in generated by automake 1.16.1 from Makefile.am.
+# @configure_input@
+
+# Copyright (C) 1994-2018 Free Software Foundation, Inc.
+
+# This Makefile.in is free software; the Free Software Foundation
+# gives unlimited permission to copy and/or distribute it,
+# with or without modifications, as long as this notice is preserved.
+
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY, to the extent permitted by law; without
+# even the implied warranty of MERCHANTABILITY or FITNESS FOR A
+# PARTICULAR PURPOSE.
+
+@SET_MAKE@
+
+
+
+VPATH = @srcdir@
+am__is_gnu_make = { \
+ if test -z '$(MAKELEVEL)'; then \
+ false; \
+ elif test -n '$(MAKE_HOST)'; then \
+ true; \
+ elif test -n '$(MAKE_VERSION)' && test -n '$(CURDIR)'; then \
+ true; \
+ else \
+ false; \
+ fi; \
+}
+am__make_running_with_option = \
+ case $${target_option-} in \
+ ?) ;; \
+ *) echo "am__make_running_with_option: internal error: invalid" \
+ "target option '$${target_option-}' specified" >&2; \
+ exit 1;; \
+ esac; \
+ has_opt=no; \
+ sane_makeflags=$$MAKEFLAGS; \
+ if $(am__is_gnu_make); then \
+ sane_makeflags=$$MFLAGS; \
+ else \
+ case $$MAKEFLAGS in \
+ *\\[\ \ ]*) \
+ bs=\\; \
+ sane_makeflags=`printf '%s\n' "$$MAKEFLAGS" \
+ | sed "s/$$bs$$bs[$$bs $$bs ]*//g"`;; \
+ esac; \
+ fi; \
+ skip_next=no; \
+ strip_trailopt () \
+ { \
+ flg=`printf '%s\n' "$$flg" | sed "s/$$1.*$$//"`; \
+ }; \
+ for flg in $$sane_makeflags; do \
+ test $$skip_next = yes && { skip_next=no; continue; }; \
+ case $$flg in \
+ *=*|--*) continue;; \
+ -*I) strip_trailopt 'I'; skip_next=yes;; \
+ -*I?*) strip_trailopt 'I';; \
+ -*O) strip_trailopt 'O'; skip_next=yes;; \
+ -*O?*) strip_trailopt 'O';; \
+ -*l) strip_trailopt 'l'; skip_next=yes;; \
+ -*l?*) strip_trailopt 'l';; \
+ -[dEDm]) skip_next=yes;; \
+ -[JT]) skip_next=yes;; \
+ esac; \
+ case $$flg in \
+ *$$target_option*) has_opt=yes; break;; \
+ esac; \
+ done; \
+ test $$has_opt = yes
+am__make_dryrun = (target_option=n; $(am__make_running_with_option))
+am__make_keepgoing = (target_option=k; $(am__make_running_with_option))
+pkgdatadir = $(datadir)/@PACKAGE@
+pkgincludedir = $(includedir)/@PACKAGE@
+pkglibdir = $(libdir)/@PACKAGE@
+pkglibexecdir = $(libexecdir)/@PACKAGE@
+am__cd = CDPATH="$${ZSH_VERSION+.}$(PATH_SEPARATOR)" && cd
+install_sh_DATA = $(install_sh) -c -m 644
+install_sh_PROGRAM = $(install_sh) -c
+install_sh_SCRIPT = $(install_sh) -c
+INSTALL_HEADER = $(INSTALL_DATA)
+transform = $(program_transform_name)
+NORMAL_INSTALL = :
+PRE_INSTALL = :
+POST_INSTALL = :
+NORMAL_UNINSTALL = :
+PRE_UNINSTALL = :
+POST_UNINSTALL = :
+build_triplet = @build@
+host_triplet = @host@
+noinst_PROGRAMS = $(am__EXEEXT_1)
+subdir = src/lib-fs
+ACLOCAL_M4 = $(top_srcdir)/aclocal.m4
+am__aclocal_m4_deps = $(top_srcdir)/m4/ac_checktype2.m4 \
+ $(top_srcdir)/m4/ac_typeof.m4 $(top_srcdir)/m4/arc4random.m4 \
+ $(top_srcdir)/m4/blockdev.m4 $(top_srcdir)/m4/c99_vsnprintf.m4 \
+ $(top_srcdir)/m4/clock_gettime.m4 $(top_srcdir)/m4/crypt.m4 \
+ $(top_srcdir)/m4/crypt_xpg6.m4 $(top_srcdir)/m4/dbqlk.m4 \
+ $(top_srcdir)/m4/dirent_dtype.m4 $(top_srcdir)/m4/dovecot.m4 \
+ $(top_srcdir)/m4/fd_passing.m4 $(top_srcdir)/m4/fdatasync.m4 \
+ $(top_srcdir)/m4/flexible_array_member.m4 \
+ $(top_srcdir)/m4/glibc.m4 $(top_srcdir)/m4/gmtime_max.m4 \
+ $(top_srcdir)/m4/gmtime_tm_gmtoff.m4 \
+ $(top_srcdir)/m4/ioloop.m4 $(top_srcdir)/m4/iovec.m4 \
+ $(top_srcdir)/m4/ipv6.m4 $(top_srcdir)/m4/libcap.m4 \
+ $(top_srcdir)/m4/libtool.m4 $(top_srcdir)/m4/libwrap.m4 \
+ $(top_srcdir)/m4/linux_mremap.m4 $(top_srcdir)/m4/ltoptions.m4 \
+ $(top_srcdir)/m4/ltsugar.m4 $(top_srcdir)/m4/ltversion.m4 \
+ $(top_srcdir)/m4/lt~obsolete.m4 $(top_srcdir)/m4/mmap_write.m4 \
+ $(top_srcdir)/m4/mntctl.m4 $(top_srcdir)/m4/modules.m4 \
+ $(top_srcdir)/m4/notify.m4 $(top_srcdir)/m4/nsl.m4 \
+ $(top_srcdir)/m4/off_t_max.m4 $(top_srcdir)/m4/pkg.m4 \
+ $(top_srcdir)/m4/pr_set_dumpable.m4 \
+ $(top_srcdir)/m4/q_quotactl.m4 $(top_srcdir)/m4/quota.m4 \
+ $(top_srcdir)/m4/random.m4 $(top_srcdir)/m4/rlimit.m4 \
+ $(top_srcdir)/m4/sendfile.m4 $(top_srcdir)/m4/size_t_signed.m4 \
+ $(top_srcdir)/m4/sockpeercred.m4 $(top_srcdir)/m4/sql.m4 \
+ $(top_srcdir)/m4/ssl.m4 $(top_srcdir)/m4/st_tim.m4 \
+ $(top_srcdir)/m4/static_array.m4 $(top_srcdir)/m4/test_with.m4 \
+ $(top_srcdir)/m4/time_t.m4 $(top_srcdir)/m4/typeof.m4 \
+ $(top_srcdir)/m4/typeof_dev_t.m4 \
+ $(top_srcdir)/m4/uoff_t_max.m4 $(top_srcdir)/m4/vararg.m4 \
+ $(top_srcdir)/m4/want_apparmor.m4 \
+ $(top_srcdir)/m4/want_bsdauth.m4 \
+ $(top_srcdir)/m4/want_bzlib.m4 \
+ $(top_srcdir)/m4/want_cassandra.m4 \
+ $(top_srcdir)/m4/want_cdb.m4 \
+ $(top_srcdir)/m4/want_checkpassword.m4 \
+ $(top_srcdir)/m4/want_clucene.m4 $(top_srcdir)/m4/want_db.m4 \
+ $(top_srcdir)/m4/want_gssapi.m4 $(top_srcdir)/m4/want_icu.m4 \
+ $(top_srcdir)/m4/want_ldap.m4 $(top_srcdir)/m4/want_lua.m4 \
+ $(top_srcdir)/m4/want_lz4.m4 $(top_srcdir)/m4/want_lzma.m4 \
+ $(top_srcdir)/m4/want_mysql.m4 $(top_srcdir)/m4/want_pam.m4 \
+ $(top_srcdir)/m4/want_passwd.m4 $(top_srcdir)/m4/want_pgsql.m4 \
+ $(top_srcdir)/m4/want_prefetch.m4 \
+ $(top_srcdir)/m4/want_shadow.m4 \
+ $(top_srcdir)/m4/want_sodium.m4 $(top_srcdir)/m4/want_solr.m4 \
+ $(top_srcdir)/m4/want_sqlite.m4 \
+ $(top_srcdir)/m4/want_stemmer.m4 \
+ $(top_srcdir)/m4/want_systemd.m4 \
+ $(top_srcdir)/m4/want_textcat.m4 \
+ $(top_srcdir)/m4/want_unwind.m4 $(top_srcdir)/m4/want_zlib.m4 \
+ $(top_srcdir)/m4/want_zstd.m4 $(top_srcdir)/configure.ac
+am__configure_deps = $(am__aclocal_m4_deps) $(CONFIGURE_DEPENDENCIES) \
+ $(ACLOCAL_M4)
+DIST_COMMON = $(srcdir)/Makefile.am $(pkginc_lib_HEADERS) \
+ $(am__DIST_COMMON)
+mkinstalldirs = $(install_sh) -d
+CONFIG_HEADER = $(top_builddir)/config.h
+CONFIG_CLEAN_FILES =
+CONFIG_CLEAN_VPATH_FILES =
+am__EXEEXT_1 = test-fs-metawrap$(EXEEXT) test-fs-posix$(EXEEXT)
+PROGRAMS = $(noinst_PROGRAMS)
+LTLIBRARIES = $(noinst_LTLIBRARIES)
+libfs_la_LIBADD =
+am_libfs_la_OBJECTS = fs-api.lo fs-dict.lo fs-metawrap.lo \
+ fs-randomfail.lo fs-posix.lo fs-test.lo fs-test-async.lo \
+ fs-sis.lo fs-sis-common.lo fs-sis-queue.lo fs-wrapper.lo \
+ istream-fs-file.lo istream-fs-stats.lo istream-metawrap.lo \
+ ostream-metawrap.lo ostream-cmp.lo
+libfs_la_OBJECTS = $(am_libfs_la_OBJECTS)
+AM_V_lt = $(am__v_lt_@AM_V@)
+am__v_lt_ = $(am__v_lt_@AM_DEFAULT_V@)
+am__v_lt_0 = --silent
+am__v_lt_1 =
+am_test_fs_metawrap_OBJECTS = test-fs-metawrap.$(OBJEXT)
+test_fs_metawrap_OBJECTS = $(am_test_fs_metawrap_OBJECTS)
+am__DEPENDENCIES_1 =
+am__DEPENDENCIES_2 = $(test_deps) $(am__DEPENDENCIES_1)
+am_test_fs_posix_OBJECTS = test-fs-posix.$(OBJEXT)
+test_fs_posix_OBJECTS = $(am_test_fs_posix_OBJECTS)
+AM_V_P = $(am__v_P_@AM_V@)
+am__v_P_ = $(am__v_P_@AM_DEFAULT_V@)
+am__v_P_0 = false
+am__v_P_1 = :
+AM_V_GEN = $(am__v_GEN_@AM_V@)
+am__v_GEN_ = $(am__v_GEN_@AM_DEFAULT_V@)
+am__v_GEN_0 = @echo " GEN " $@;
+am__v_GEN_1 =
+AM_V_at = $(am__v_at_@AM_V@)
+am__v_at_ = $(am__v_at_@AM_DEFAULT_V@)
+am__v_at_0 = @
+am__v_at_1 =
+DEFAULT_INCLUDES = -I.@am__isrc@ -I$(top_builddir)
+depcomp = $(SHELL) $(top_srcdir)/depcomp
+am__maybe_remake_depfiles = depfiles
+am__depfiles_remade = ./$(DEPDIR)/fs-api.Plo ./$(DEPDIR)/fs-dict.Plo \
+ ./$(DEPDIR)/fs-metawrap.Plo ./$(DEPDIR)/fs-posix.Plo \
+ ./$(DEPDIR)/fs-randomfail.Plo ./$(DEPDIR)/fs-sis-common.Plo \
+ ./$(DEPDIR)/fs-sis-queue.Plo ./$(DEPDIR)/fs-sis.Plo \
+ ./$(DEPDIR)/fs-test-async.Plo ./$(DEPDIR)/fs-test.Plo \
+ ./$(DEPDIR)/fs-wrapper.Plo ./$(DEPDIR)/istream-fs-file.Plo \
+ ./$(DEPDIR)/istream-fs-stats.Plo \
+ ./$(DEPDIR)/istream-metawrap.Plo ./$(DEPDIR)/ostream-cmp.Plo \
+ ./$(DEPDIR)/ostream-metawrap.Plo \
+ ./$(DEPDIR)/test-fs-metawrap.Po ./$(DEPDIR)/test-fs-posix.Po
+am__mv = mv -f
+COMPILE = $(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) \
+ $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS)
+LTCOMPILE = $(LIBTOOL) $(AM_V_lt) --tag=CC $(AM_LIBTOOLFLAGS) \
+ $(LIBTOOLFLAGS) --mode=compile $(CC) $(DEFS) \
+ $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) $(CPPFLAGS) \
+ $(AM_CFLAGS) $(CFLAGS)
+AM_V_CC = $(am__v_CC_@AM_V@)
+am__v_CC_ = $(am__v_CC_@AM_DEFAULT_V@)
+am__v_CC_0 = @echo " CC " $@;
+am__v_CC_1 =
+CCLD = $(CC)
+LINK = $(LIBTOOL) $(AM_V_lt) --tag=CC $(AM_LIBTOOLFLAGS) \
+ $(LIBTOOLFLAGS) --mode=link $(CCLD) $(AM_CFLAGS) $(CFLAGS) \
+ $(AM_LDFLAGS) $(LDFLAGS) -o $@
+AM_V_CCLD = $(am__v_CCLD_@AM_V@)
+am__v_CCLD_ = $(am__v_CCLD_@AM_DEFAULT_V@)
+am__v_CCLD_0 = @echo " CCLD " $@;
+am__v_CCLD_1 =
+SOURCES = $(libfs_la_SOURCES) $(test_fs_metawrap_SOURCES) \
+ $(test_fs_posix_SOURCES)
+DIST_SOURCES = $(libfs_la_SOURCES) $(test_fs_metawrap_SOURCES) \
+ $(test_fs_posix_SOURCES)
+am__can_run_installinfo = \
+ case $$AM_UPDATE_INFO_DIR in \
+ n|no|NO) false;; \
+ *) (install-info --version) >/dev/null 2>&1;; \
+ esac
+am__vpath_adj_setup = srcdirstrip=`echo "$(srcdir)" | sed 's|.|.|g'`;
+am__vpath_adj = case $$p in \
+ $(srcdir)/*) f=`echo "$$p" | sed "s|^$$srcdirstrip/||"`;; \
+ *) f=$$p;; \
+ esac;
+am__strip_dir = f=`echo $$p | sed -e 's|^.*/||'`;
+am__install_max = 40
+am__nobase_strip_setup = \
+ srcdirstrip=`echo "$(srcdir)" | sed 's/[].[^$$\\*|]/\\\\&/g'`
+am__nobase_strip = \
+ for p in $$list; do echo "$$p"; done | sed -e "s|$$srcdirstrip/||"
+am__nobase_list = $(am__nobase_strip_setup); \
+ for p in $$list; do echo "$$p $$p"; done | \
+ sed "s| $$srcdirstrip/| |;"' / .*\//!s/ .*/ ./; s,\( .*\)/[^/]*$$,\1,' | \
+ $(AWK) 'BEGIN { files["."] = "" } { files[$$2] = files[$$2] " " $$1; \
+ if (++n[$$2] == $(am__install_max)) \
+ { print $$2, files[$$2]; n[$$2] = 0; files[$$2] = "" } } \
+ END { for (dir in files) print dir, files[dir] }'
+am__base_list = \
+ sed '$$!N;$$!N;$$!N;$$!N;$$!N;$$!N;$$!N;s/\n/ /g' | \
+ sed '$$!N;$$!N;$$!N;$$!N;s/\n/ /g'
+am__uninstall_files_from_dir = { \
+ test -z "$$files" \
+ || { test ! -d "$$dir" && test ! -f "$$dir" && test ! -r "$$dir"; } \
+ || { echo " ( cd '$$dir' && rm -f" $$files ")"; \
+ $(am__cd) "$$dir" && rm -f $$files; }; \
+ }
+am__installdirs = "$(DESTDIR)$(pkginc_libdir)"
+HEADERS = $(pkginc_lib_HEADERS)
+am__tagged_files = $(HEADERS) $(SOURCES) $(TAGS_FILES) $(LISP)
+# Read a list of newline-separated strings from the standard input,
+# and print each of them once, without duplicates. Input order is
+# *not* preserved.
+am__uniquify_input = $(AWK) '\
+ BEGIN { nonempty = 0; } \
+ { items[$$0] = 1; nonempty = 1; } \
+ END { if (nonempty) { for (i in items) print i; }; } \
+'
+# Make sure the list of sources is unique. This is necessary because,
+# e.g., the same source file might be shared among _SOURCES variables
+# for different programs/libraries.
+am__define_uniq_tagged_files = \
+ list='$(am__tagged_files)'; \
+ unique=`for i in $$list; do \
+ if test -f "$$i"; then echo $$i; else echo $(srcdir)/$$i; fi; \
+ done | $(am__uniquify_input)`
+ETAGS = etags
+CTAGS = ctags
+am__DIST_COMMON = $(srcdir)/Makefile.in $(top_srcdir)/depcomp
+DISTFILES = $(DIST_COMMON) $(DIST_SOURCES) $(TEXINFOS) $(EXTRA_DIST)
+ACLOCAL = @ACLOCAL@
+ACLOCAL_AMFLAGS = @ACLOCAL_AMFLAGS@
+AMTAR = @AMTAR@
+AM_DEFAULT_VERBOSITY = @AM_DEFAULT_VERBOSITY@
+APPARMOR_LIBS = @APPARMOR_LIBS@
+AR = @AR@
+AUTH_CFLAGS = @AUTH_CFLAGS@
+AUTH_LIBS = @AUTH_LIBS@
+AUTOCONF = @AUTOCONF@
+AUTOHEADER = @AUTOHEADER@
+AUTOMAKE = @AUTOMAKE@
+AWK = @AWK@
+BINARY_CFLAGS = @BINARY_CFLAGS@
+BINARY_LDFLAGS = @BINARY_LDFLAGS@
+BISON = @BISON@
+CASSANDRA_CFLAGS = @CASSANDRA_CFLAGS@
+CASSANDRA_LIBS = @CASSANDRA_LIBS@
+CC = @CC@
+CCDEPMODE = @CCDEPMODE@
+CDB_LIBS = @CDB_LIBS@
+CFLAGS = @CFLAGS@
+CLUCENE_CFLAGS = @CLUCENE_CFLAGS@
+CLUCENE_LIBS = @CLUCENE_LIBS@
+COMPRESS_LIBS = @COMPRESS_LIBS@
+CPP = @CPP@
+CPPFLAGS = @CPPFLAGS@
+CRYPT_LIBS = @CRYPT_LIBS@
+CXX = @CXX@
+CXXCPP = @CXXCPP@
+CXXDEPMODE = @CXXDEPMODE@
+CXXFLAGS = @CXXFLAGS@
+CYGPATH_W = @CYGPATH_W@
+DEFS = @DEFS@
+DEPDIR = @DEPDIR@
+DICT_LIBS = @DICT_LIBS@
+DLLIB = @DLLIB@
+DLLTOOL = @DLLTOOL@
+DSYMUTIL = @DSYMUTIL@
+DUMPBIN = @DUMPBIN@
+ECHO_C = @ECHO_C@
+ECHO_N = @ECHO_N@
+ECHO_T = @ECHO_T@
+EGREP = @EGREP@
+EXEEXT = @EXEEXT@
+FGREP = @FGREP@
+FLEX = @FLEX@
+FUZZER_CPPFLAGS = @FUZZER_CPPFLAGS@
+FUZZER_LDFLAGS = @FUZZER_LDFLAGS@
+GREP = @GREP@
+INSTALL = @INSTALL@
+INSTALL_DATA = @INSTALL_DATA@
+INSTALL_PROGRAM = @INSTALL_PROGRAM@
+INSTALL_SCRIPT = @INSTALL_SCRIPT@
+INSTALL_STRIP_PROGRAM = @INSTALL_STRIP_PROGRAM@
+KRB5CONFIG = @KRB5CONFIG@
+KRB5_CFLAGS = @KRB5_CFLAGS@
+KRB5_LIBS = @KRB5_LIBS@
+LD = @LD@
+LDAP_LIBS = @LDAP_LIBS@
+LDFLAGS = @LDFLAGS@
+LD_NO_WHOLE_ARCHIVE = @LD_NO_WHOLE_ARCHIVE@
+LD_WHOLE_ARCHIVE = @LD_WHOLE_ARCHIVE@
+LIBCAP = @LIBCAP@
+LIBDOVECOT = @LIBDOVECOT@
+LIBDOVECOT_COMPRESS = @LIBDOVECOT_COMPRESS@
+LIBDOVECOT_DEPS = @LIBDOVECOT_DEPS@
+LIBDOVECOT_DSYNC = @LIBDOVECOT_DSYNC@
+LIBDOVECOT_LA_LIBS = @LIBDOVECOT_LA_LIBS@
+LIBDOVECOT_LDA = @LIBDOVECOT_LDA@
+LIBDOVECOT_LDAP = @LIBDOVECOT_LDAP@
+LIBDOVECOT_LIBFTS = @LIBDOVECOT_LIBFTS@
+LIBDOVECOT_LIBFTS_DEPS = @LIBDOVECOT_LIBFTS_DEPS@
+LIBDOVECOT_LOGIN = @LIBDOVECOT_LOGIN@
+LIBDOVECOT_LUA = @LIBDOVECOT_LUA@
+LIBDOVECOT_LUA_DEPS = @LIBDOVECOT_LUA_DEPS@
+LIBDOVECOT_SQL = @LIBDOVECOT_SQL@
+LIBDOVECOT_STORAGE = @LIBDOVECOT_STORAGE@
+LIBDOVECOT_STORAGE_DEPS = @LIBDOVECOT_STORAGE_DEPS@
+LIBEXTTEXTCAT_CFLAGS = @LIBEXTTEXTCAT_CFLAGS@
+LIBEXTTEXTCAT_LIBS = @LIBEXTTEXTCAT_LIBS@
+LIBICONV = @LIBICONV@
+LIBICU_CFLAGS = @LIBICU_CFLAGS@
+LIBICU_LIBS = @LIBICU_LIBS@
+LIBOBJS = @LIBOBJS@
+LIBS = @LIBS@
+LIBSODIUM_CFLAGS = @LIBSODIUM_CFLAGS@
+LIBSODIUM_LIBS = @LIBSODIUM_LIBS@
+LIBTIRPC_CFLAGS = @LIBTIRPC_CFLAGS@
+LIBTIRPC_LIBS = @LIBTIRPC_LIBS@
+LIBTOOL = @LIBTOOL@
+LIBUNWIND_CFLAGS = @LIBUNWIND_CFLAGS@
+LIBUNWIND_LIBS = @LIBUNWIND_LIBS@
+LIBWRAP_LIBS = @LIBWRAP_LIBS@
+LINKED_STORAGE_LDADD = @LINKED_STORAGE_LDADD@
+LIPO = @LIPO@
+LN_S = @LN_S@
+LTLIBICONV = @LTLIBICONV@
+LTLIBOBJS = @LTLIBOBJS@
+LT_SYS_LIBRARY_PATH = @LT_SYS_LIBRARY_PATH@
+LUA_CFLAGS = @LUA_CFLAGS@
+LUA_LIBS = @LUA_LIBS@
+MAINT = @MAINT@
+MAKEINFO = @MAKEINFO@
+MANIFEST_TOOL = @MANIFEST_TOOL@
+MKDIR_P = @MKDIR_P@
+MODULE_LIBS = @MODULE_LIBS@
+MODULE_SUFFIX = @MODULE_SUFFIX@
+MYSQL_CFLAGS = @MYSQL_CFLAGS@
+MYSQL_CONFIG = @MYSQL_CONFIG@
+MYSQL_LIBS = @MYSQL_LIBS@
+NM = @NM@
+NMEDIT = @NMEDIT@
+NOPLUGIN_LDFLAGS = @NOPLUGIN_LDFLAGS@
+OBJDUMP = @OBJDUMP@
+OBJEXT = @OBJEXT@
+OTOOL = @OTOOL@
+OTOOL64 = @OTOOL64@
+PACKAGE = @PACKAGE@
+PACKAGE_BUGREPORT = @PACKAGE_BUGREPORT@
+PACKAGE_NAME = @PACKAGE_NAME@
+PACKAGE_STRING = @PACKAGE_STRING@
+PACKAGE_TARNAME = @PACKAGE_TARNAME@
+PACKAGE_URL = @PACKAGE_URL@
+PACKAGE_VERSION = @PACKAGE_VERSION@
+PANDOC = @PANDOC@
+PATH_SEPARATOR = @PATH_SEPARATOR@
+PGSQL_CFLAGS = @PGSQL_CFLAGS@
+PGSQL_LIBS = @PGSQL_LIBS@
+PG_CONFIG = @PG_CONFIG@
+PIE_CFLAGS = @PIE_CFLAGS@
+PIE_LDFLAGS = @PIE_LDFLAGS@
+PKG_CONFIG = @PKG_CONFIG@
+PKG_CONFIG_LIBDIR = @PKG_CONFIG_LIBDIR@
+PKG_CONFIG_PATH = @PKG_CONFIG_PATH@
+QUOTA_LIBS = @QUOTA_LIBS@
+RANLIB = @RANLIB@
+RELRO_LDFLAGS = @RELRO_LDFLAGS@
+RPCGEN = @RPCGEN@
+RUN_TEST = @RUN_TEST@
+SED = @SED@
+SETTING_FILES = @SETTING_FILES@
+SET_MAKE = @SET_MAKE@
+SHELL = @SHELL@
+SQLITE_CFLAGS = @SQLITE_CFLAGS@
+SQLITE_LIBS = @SQLITE_LIBS@
+SQL_CFLAGS = @SQL_CFLAGS@
+SQL_LIBS = @SQL_LIBS@
+SSL_CFLAGS = @SSL_CFLAGS@
+SSL_LIBS = @SSL_LIBS@
+STRIP = @STRIP@
+SYSTEMD_CFLAGS = @SYSTEMD_CFLAGS@
+SYSTEMD_LIBS = @SYSTEMD_LIBS@
+VALGRIND = @VALGRIND@
+VERSION = @VERSION@
+ZSTD_CFLAGS = @ZSTD_CFLAGS@
+ZSTD_LIBS = @ZSTD_LIBS@
+abs_builddir = @abs_builddir@
+abs_srcdir = @abs_srcdir@
+abs_top_builddir = @abs_top_builddir@
+abs_top_srcdir = @abs_top_srcdir@
+ac_ct_AR = @ac_ct_AR@
+ac_ct_CC = @ac_ct_CC@
+ac_ct_CXX = @ac_ct_CXX@
+ac_ct_DUMPBIN = @ac_ct_DUMPBIN@
+am__include = @am__include@
+am__leading_dot = @am__leading_dot@
+am__quote = @am__quote@
+am__tar = @am__tar@
+am__untar = @am__untar@
+bindir = @bindir@
+build = @build@
+build_alias = @build_alias@
+build_cpu = @build_cpu@
+build_os = @build_os@
+build_vendor = @build_vendor@
+builddir = @builddir@
+datadir = @datadir@
+datarootdir = @datarootdir@
+dict_drivers = @dict_drivers@
+docdir = @docdir@
+dvidir = @dvidir@
+exec_prefix = @exec_prefix@
+host = @host@
+host_alias = @host_alias@
+host_cpu = @host_cpu@
+host_os = @host_os@
+host_vendor = @host_vendor@
+htmldir = @htmldir@
+includedir = @includedir@
+infodir = @infodir@
+install_sh = @install_sh@
+libdir = @libdir@
+libexecdir = @libexecdir@
+localedir = @localedir@
+localstatedir = @localstatedir@
+mandir = @mandir@
+mkdir_p = @mkdir_p@
+moduledir = @moduledir@
+oldincludedir = @oldincludedir@
+pdfdir = @pdfdir@
+prefix = @prefix@
+program_transform_name = @program_transform_name@
+psdir = @psdir@
+rundir = @rundir@
+runstatedir = @runstatedir@
+sbindir = @sbindir@
+sharedstatedir = @sharedstatedir@
+sql_drivers = @sql_drivers@
+srcdir = @srcdir@
+ssldir = @ssldir@
+statedir = @statedir@
+sysconfdir = @sysconfdir@
+systemdservicetype = @systemdservicetype@
+systemdsystemunitdir = @systemdsystemunitdir@
+target_alias = @target_alias@
+top_build_prefix = @top_build_prefix@
+top_builddir = @top_builddir@
+top_srcdir = @top_srcdir@
+noinst_LTLIBRARIES = libfs.la
+AM_CPPFLAGS = \
+ -I$(top_srcdir)/src/lib \
+ -I$(top_srcdir)/src/lib-test \
+ -I$(top_srcdir)/src/lib-dict \
+ -I$(top_srcdir)/src/lib-ssl-iostream \
+ -DMODULE_DIR=\""$(moduledir)"\"
+
+libfs_la_SOURCES = \
+ fs-api.c \
+ fs-dict.c \
+ fs-metawrap.c \
+ fs-randomfail.c \
+ fs-posix.c \
+ fs-test.c \
+ fs-test-async.c \
+ fs-sis.c \
+ fs-sis-common.c \
+ fs-sis-queue.c \
+ fs-wrapper.c \
+ istream-fs-file.c \
+ istream-fs-stats.c \
+ istream-metawrap.c \
+ ostream-metawrap.c \
+ ostream-cmp.c
+
+headers = \
+ fs-api.h \
+ fs-api-private.h \
+ fs-sis-common.h \
+ fs-wrapper.h \
+ fs-test.h \
+ istream-fs-file.h \
+ istream-fs-stats.h \
+ istream-metawrap.h \
+ ostream-metawrap.h \
+ ostream-cmp.h
+
+pkginc_libdir = $(pkgincludedir)
+pkginc_lib_HEADERS = $(headers)
+test_programs = \
+ test-fs-metawrap \
+ test-fs-posix
+
+test_deps = \
+ $(noinst_LTLIBRARIES) \
+ ../lib-dict/libdict.la \
+ ../lib-test/libtest.la \
+ ../lib/liblib.la
+
+test_libs = \
+ $(test_deps) \
+ $(MODULE_LIBS)
+
+test_fs_metawrap_SOURCES = test-fs-metawrap.c
+test_fs_metawrap_LDADD = $(test_libs)
+test_fs_metawrap_DEPENDENCIES = $(test_deps)
+test_fs_posix_SOURCES = test-fs-posix.c
+test_fs_posix_LDADD = $(test_libs)
+test_fs_posix_DEPENDENCIES = $(test_deps)
+all: all-am
+
+.SUFFIXES:
+.SUFFIXES: .c .lo .o .obj
+$(srcdir)/Makefile.in: @MAINTAINER_MODE_TRUE@ $(srcdir)/Makefile.am $(am__configure_deps)
+ @for dep in $?; do \
+ case '$(am__configure_deps)' in \
+ *$$dep*) \
+ ( cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh ) \
+ && { if test -f $@; then exit 0; else break; fi; }; \
+ exit 1;; \
+ esac; \
+ done; \
+ echo ' cd $(top_srcdir) && $(AUTOMAKE) --foreign src/lib-fs/Makefile'; \
+ $(am__cd) $(top_srcdir) && \
+ $(AUTOMAKE) --foreign src/lib-fs/Makefile
+Makefile: $(srcdir)/Makefile.in $(top_builddir)/config.status
+ @case '$?' in \
+ *config.status*) \
+ cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh;; \
+ *) \
+ echo ' cd $(top_builddir) && $(SHELL) ./config.status $(subdir)/$@ $(am__maybe_remake_depfiles)'; \
+ cd $(top_builddir) && $(SHELL) ./config.status $(subdir)/$@ $(am__maybe_remake_depfiles);; \
+ esac;
+
+$(top_builddir)/config.status: $(top_srcdir)/configure $(CONFIG_STATUS_DEPENDENCIES)
+ cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh
+
+$(top_srcdir)/configure: @MAINTAINER_MODE_TRUE@ $(am__configure_deps)
+ cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh
+$(ACLOCAL_M4): @MAINTAINER_MODE_TRUE@ $(am__aclocal_m4_deps)
+ cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh
+$(am__aclocal_m4_deps):
+
+clean-noinstPROGRAMS:
+ @list='$(noinst_PROGRAMS)'; test -n "$$list" || exit 0; \
+ echo " rm -f" $$list; \
+ rm -f $$list || exit $$?; \
+ test -n "$(EXEEXT)" || exit 0; \
+ list=`for p in $$list; do echo "$$p"; done | sed 's/$(EXEEXT)$$//'`; \
+ echo " rm -f" $$list; \
+ rm -f $$list
+
+clean-noinstLTLIBRARIES:
+ -test -z "$(noinst_LTLIBRARIES)" || rm -f $(noinst_LTLIBRARIES)
+ @list='$(noinst_LTLIBRARIES)'; \
+ locs=`for p in $$list; do echo $$p; done | \
+ sed 's|^[^/]*$$|.|; s|/[^/]*$$||; s|$$|/so_locations|' | \
+ sort -u`; \
+ test -z "$$locs" || { \
+ echo rm -f $${locs}; \
+ rm -f $${locs}; \
+ }
+
+libfs.la: $(libfs_la_OBJECTS) $(libfs_la_DEPENDENCIES) $(EXTRA_libfs_la_DEPENDENCIES)
+ $(AM_V_CCLD)$(LINK) $(libfs_la_OBJECTS) $(libfs_la_LIBADD) $(LIBS)
+
+test-fs-metawrap$(EXEEXT): $(test_fs_metawrap_OBJECTS) $(test_fs_metawrap_DEPENDENCIES) $(EXTRA_test_fs_metawrap_DEPENDENCIES)
+ @rm -f test-fs-metawrap$(EXEEXT)
+ $(AM_V_CCLD)$(LINK) $(test_fs_metawrap_OBJECTS) $(test_fs_metawrap_LDADD) $(LIBS)
+
+test-fs-posix$(EXEEXT): $(test_fs_posix_OBJECTS) $(test_fs_posix_DEPENDENCIES) $(EXTRA_test_fs_posix_DEPENDENCIES)
+ @rm -f test-fs-posix$(EXEEXT)
+ $(AM_V_CCLD)$(LINK) $(test_fs_posix_OBJECTS) $(test_fs_posix_LDADD) $(LIBS)
+
+mostlyclean-compile:
+ -rm -f *.$(OBJEXT)
+
+distclean-compile:
+ -rm -f *.tab.c
+
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/fs-api.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/fs-dict.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/fs-metawrap.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/fs-posix.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/fs-randomfail.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/fs-sis-common.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/fs-sis-queue.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/fs-sis.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/fs-test-async.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/fs-test.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/fs-wrapper.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/istream-fs-file.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/istream-fs-stats.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/istream-metawrap.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/ostream-cmp.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/ostream-metawrap.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/test-fs-metawrap.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/test-fs-posix.Po@am__quote@ # am--include-marker
+
+$(am__depfiles_remade):
+ @$(MKDIR_P) $(@D)
+ @echo '# dummy' >$@-t && $(am__mv) $@-t $@
+
+am--depfiles: $(am__depfiles_remade)
+
+.c.o:
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(COMPILE) -MT $@ -MD -MP -MF $(DEPDIR)/$*.Tpo -c -o $@ $<
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/$*.Tpo $(DEPDIR)/$*.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='$<' object='$@' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(COMPILE) -c -o $@ $<
+
+.c.obj:
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(COMPILE) -MT $@ -MD -MP -MF $(DEPDIR)/$*.Tpo -c -o $@ `$(CYGPATH_W) '$<'`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/$*.Tpo $(DEPDIR)/$*.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='$<' object='$@' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(COMPILE) -c -o $@ `$(CYGPATH_W) '$<'`
+
+.c.lo:
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(LTCOMPILE) -MT $@ -MD -MP -MF $(DEPDIR)/$*.Tpo -c -o $@ $<
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/$*.Tpo $(DEPDIR)/$*.Plo
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='$<' object='$@' libtool=yes @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(LTCOMPILE) -c -o $@ $<
+
+mostlyclean-libtool:
+ -rm -f *.lo
+
+clean-libtool:
+ -rm -rf .libs _libs
+install-pkginc_libHEADERS: $(pkginc_lib_HEADERS)
+ @$(NORMAL_INSTALL)
+ @list='$(pkginc_lib_HEADERS)'; test -n "$(pkginc_libdir)" || list=; \
+ if test -n "$$list"; then \
+ echo " $(MKDIR_P) '$(DESTDIR)$(pkginc_libdir)'"; \
+ $(MKDIR_P) "$(DESTDIR)$(pkginc_libdir)" || exit 1; \
+ fi; \
+ for p in $$list; do \
+ if test -f "$$p"; then d=; else d="$(srcdir)/"; fi; \
+ echo "$$d$$p"; \
+ done | $(am__base_list) | \
+ while read files; do \
+ echo " $(INSTALL_HEADER) $$files '$(DESTDIR)$(pkginc_libdir)'"; \
+ $(INSTALL_HEADER) $$files "$(DESTDIR)$(pkginc_libdir)" || exit $$?; \
+ done
+
+uninstall-pkginc_libHEADERS:
+ @$(NORMAL_UNINSTALL)
+ @list='$(pkginc_lib_HEADERS)'; test -n "$(pkginc_libdir)" || list=; \
+ files=`for p in $$list; do echo $$p; done | sed -e 's|^.*/||'`; \
+ dir='$(DESTDIR)$(pkginc_libdir)'; $(am__uninstall_files_from_dir)
+
+ID: $(am__tagged_files)
+ $(am__define_uniq_tagged_files); mkid -fID $$unique
+tags: tags-am
+TAGS: tags
+
+tags-am: $(TAGS_DEPENDENCIES) $(am__tagged_files)
+ set x; \
+ here=`pwd`; \
+ $(am__define_uniq_tagged_files); \
+ shift; \
+ if test -z "$(ETAGS_ARGS)$$*$$unique"; then :; else \
+ test -n "$$unique" || unique=$$empty_fix; \
+ if test $$# -gt 0; then \
+ $(ETAGS) $(ETAGSFLAGS) $(AM_ETAGSFLAGS) $(ETAGS_ARGS) \
+ "$$@" $$unique; \
+ else \
+ $(ETAGS) $(ETAGSFLAGS) $(AM_ETAGSFLAGS) $(ETAGS_ARGS) \
+ $$unique; \
+ fi; \
+ fi
+ctags: ctags-am
+
+CTAGS: ctags
+ctags-am: $(TAGS_DEPENDENCIES) $(am__tagged_files)
+ $(am__define_uniq_tagged_files); \
+ test -z "$(CTAGS_ARGS)$$unique" \
+ || $(CTAGS) $(CTAGSFLAGS) $(AM_CTAGSFLAGS) $(CTAGS_ARGS) \
+ $$unique
+
+GTAGS:
+ here=`$(am__cd) $(top_builddir) && pwd` \
+ && $(am__cd) $(top_srcdir) \
+ && gtags -i $(GTAGS_ARGS) "$$here"
+cscopelist: cscopelist-am
+
+cscopelist-am: $(am__tagged_files)
+ list='$(am__tagged_files)'; \
+ case "$(srcdir)" in \
+ [\\/]* | ?:[\\/]*) sdir="$(srcdir)" ;; \
+ *) sdir=$(subdir)/$(srcdir) ;; \
+ esac; \
+ for i in $$list; do \
+ if test -f "$$i"; then \
+ echo "$(subdir)/$$i"; \
+ else \
+ echo "$$sdir/$$i"; \
+ fi; \
+ done >> $(top_builddir)/cscope.files
+
+distclean-tags:
+ -rm -f TAGS ID GTAGS GRTAGS GSYMS GPATH tags
+
+distdir: $(BUILT_SOURCES)
+ $(MAKE) $(AM_MAKEFLAGS) distdir-am
+
+distdir-am: $(DISTFILES)
+ @srcdirstrip=`echo "$(srcdir)" | sed 's/[].[^$$\\*]/\\\\&/g'`; \
+ topsrcdirstrip=`echo "$(top_srcdir)" | sed 's/[].[^$$\\*]/\\\\&/g'`; \
+ list='$(DISTFILES)'; \
+ dist_files=`for file in $$list; do echo $$file; done | \
+ sed -e "s|^$$srcdirstrip/||;t" \
+ -e "s|^$$topsrcdirstrip/|$(top_builddir)/|;t"`; \
+ case $$dist_files in \
+ */*) $(MKDIR_P) `echo "$$dist_files" | \
+ sed '/\//!d;s|^|$(distdir)/|;s,/[^/]*$$,,' | \
+ sort -u` ;; \
+ esac; \
+ for file in $$dist_files; do \
+ if test -f $$file || test -d $$file; then d=.; else d=$(srcdir); fi; \
+ if test -d $$d/$$file; then \
+ dir=`echo "/$$file" | sed -e 's,/[^/]*$$,,'`; \
+ if test -d "$(distdir)/$$file"; then \
+ find "$(distdir)/$$file" -type d ! -perm -700 -exec chmod u+rwx {} \;; \
+ fi; \
+ if test -d $(srcdir)/$$file && test $$d != $(srcdir); then \
+ cp -fpR $(srcdir)/$$file "$(distdir)$$dir" || exit 1; \
+ find "$(distdir)/$$file" -type d ! -perm -700 -exec chmod u+rwx {} \;; \
+ fi; \
+ cp -fpR $$d/$$file "$(distdir)$$dir" || exit 1; \
+ else \
+ test -f "$(distdir)/$$file" \
+ || cp -p $$d/$$file "$(distdir)/$$file" \
+ || exit 1; \
+ fi; \
+ done
+check-am: all-am
+ $(MAKE) $(AM_MAKEFLAGS) check-local
+check: check-am
+all-am: Makefile $(PROGRAMS) $(LTLIBRARIES) $(HEADERS)
+installdirs:
+ for dir in "$(DESTDIR)$(pkginc_libdir)"; do \
+ test -z "$$dir" || $(MKDIR_P) "$$dir"; \
+ done
+install: install-am
+install-exec: install-exec-am
+install-data: install-data-am
+uninstall: uninstall-am
+
+install-am: all-am
+ @$(MAKE) $(AM_MAKEFLAGS) install-exec-am install-data-am
+
+installcheck: installcheck-am
+install-strip:
+ if test -z '$(STRIP)'; then \
+ $(MAKE) $(AM_MAKEFLAGS) INSTALL_PROGRAM="$(INSTALL_STRIP_PROGRAM)" \
+ install_sh_PROGRAM="$(INSTALL_STRIP_PROGRAM)" INSTALL_STRIP_FLAG=-s \
+ install; \
+ else \
+ $(MAKE) $(AM_MAKEFLAGS) INSTALL_PROGRAM="$(INSTALL_STRIP_PROGRAM)" \
+ install_sh_PROGRAM="$(INSTALL_STRIP_PROGRAM)" INSTALL_STRIP_FLAG=-s \
+ "INSTALL_PROGRAM_ENV=STRIPPROG='$(STRIP)'" install; \
+ fi
+mostlyclean-generic:
+
+clean-generic:
+
+distclean-generic:
+ -test -z "$(CONFIG_CLEAN_FILES)" || rm -f $(CONFIG_CLEAN_FILES)
+ -test . = "$(srcdir)" || test -z "$(CONFIG_CLEAN_VPATH_FILES)" || rm -f $(CONFIG_CLEAN_VPATH_FILES)
+
+maintainer-clean-generic:
+ @echo "This command is intended for maintainers to use"
+ @echo "it deletes files that may require special tools to rebuild."
+clean: clean-am
+
+clean-am: clean-generic clean-libtool clean-noinstLTLIBRARIES \
+ clean-noinstPROGRAMS mostlyclean-am
+
+distclean: distclean-am
+ -rm -f ./$(DEPDIR)/fs-api.Plo
+ -rm -f ./$(DEPDIR)/fs-dict.Plo
+ -rm -f ./$(DEPDIR)/fs-metawrap.Plo
+ -rm -f ./$(DEPDIR)/fs-posix.Plo
+ -rm -f ./$(DEPDIR)/fs-randomfail.Plo
+ -rm -f ./$(DEPDIR)/fs-sis-common.Plo
+ -rm -f ./$(DEPDIR)/fs-sis-queue.Plo
+ -rm -f ./$(DEPDIR)/fs-sis.Plo
+ -rm -f ./$(DEPDIR)/fs-test-async.Plo
+ -rm -f ./$(DEPDIR)/fs-test.Plo
+ -rm -f ./$(DEPDIR)/fs-wrapper.Plo
+ -rm -f ./$(DEPDIR)/istream-fs-file.Plo
+ -rm -f ./$(DEPDIR)/istream-fs-stats.Plo
+ -rm -f ./$(DEPDIR)/istream-metawrap.Plo
+ -rm -f ./$(DEPDIR)/ostream-cmp.Plo
+ -rm -f ./$(DEPDIR)/ostream-metawrap.Plo
+ -rm -f ./$(DEPDIR)/test-fs-metawrap.Po
+ -rm -f ./$(DEPDIR)/test-fs-posix.Po
+ -rm -f Makefile
+distclean-am: clean-am distclean-compile distclean-generic \
+ distclean-tags
+
+dvi: dvi-am
+
+dvi-am:
+
+html: html-am
+
+html-am:
+
+info: info-am
+
+info-am:
+
+install-data-am: install-pkginc_libHEADERS
+
+install-dvi: install-dvi-am
+
+install-dvi-am:
+
+install-exec-am:
+
+install-html: install-html-am
+
+install-html-am:
+
+install-info: install-info-am
+
+install-info-am:
+
+install-man:
+
+install-pdf: install-pdf-am
+
+install-pdf-am:
+
+install-ps: install-ps-am
+
+install-ps-am:
+
+installcheck-am:
+
+maintainer-clean: maintainer-clean-am
+ -rm -f ./$(DEPDIR)/fs-api.Plo
+ -rm -f ./$(DEPDIR)/fs-dict.Plo
+ -rm -f ./$(DEPDIR)/fs-metawrap.Plo
+ -rm -f ./$(DEPDIR)/fs-posix.Plo
+ -rm -f ./$(DEPDIR)/fs-randomfail.Plo
+ -rm -f ./$(DEPDIR)/fs-sis-common.Plo
+ -rm -f ./$(DEPDIR)/fs-sis-queue.Plo
+ -rm -f ./$(DEPDIR)/fs-sis.Plo
+ -rm -f ./$(DEPDIR)/fs-test-async.Plo
+ -rm -f ./$(DEPDIR)/fs-test.Plo
+ -rm -f ./$(DEPDIR)/fs-wrapper.Plo
+ -rm -f ./$(DEPDIR)/istream-fs-file.Plo
+ -rm -f ./$(DEPDIR)/istream-fs-stats.Plo
+ -rm -f ./$(DEPDIR)/istream-metawrap.Plo
+ -rm -f ./$(DEPDIR)/ostream-cmp.Plo
+ -rm -f ./$(DEPDIR)/ostream-metawrap.Plo
+ -rm -f ./$(DEPDIR)/test-fs-metawrap.Po
+ -rm -f ./$(DEPDIR)/test-fs-posix.Po
+ -rm -f Makefile
+maintainer-clean-am: distclean-am maintainer-clean-generic
+
+mostlyclean: mostlyclean-am
+
+mostlyclean-am: mostlyclean-compile mostlyclean-generic \
+ mostlyclean-libtool
+
+pdf: pdf-am
+
+pdf-am:
+
+ps: ps-am
+
+ps-am:
+
+uninstall-am: uninstall-pkginc_libHEADERS
+
+.MAKE: check-am install-am install-strip
+
+.PHONY: CTAGS GTAGS TAGS all all-am am--depfiles check check-am \
+ check-local clean clean-generic clean-libtool \
+ clean-noinstLTLIBRARIES clean-noinstPROGRAMS cscopelist-am \
+ ctags ctags-am distclean distclean-compile distclean-generic \
+ distclean-libtool distclean-tags distdir dvi dvi-am html \
+ html-am info info-am install install-am install-data \
+ install-data-am install-dvi install-dvi-am install-exec \
+ install-exec-am install-html install-html-am install-info \
+ install-info-am install-man install-pdf install-pdf-am \
+ install-pkginc_libHEADERS install-ps install-ps-am \
+ install-strip installcheck installcheck-am installdirs \
+ maintainer-clean maintainer-clean-generic mostlyclean \
+ mostlyclean-compile mostlyclean-generic mostlyclean-libtool \
+ pdf pdf-am ps ps-am tags tags-am uninstall uninstall-am \
+ uninstall-pkginc_libHEADERS
+
+.PRECIOUS: Makefile
+
+
+check-local:
+ for bin in $(test_programs); do \
+ if ! $(RUN_TEST) ./$$bin; then exit 1; fi; \
+ done
+
+# Tell versions [3.59,3.63) of GNU make to not export all variables.
+# Otherwise a system limit (for SysV at least) may be exceeded.
+.NOEXPORT:
diff --git a/src/lib-fs/fs-api-private.h b/src/lib-fs/fs-api-private.h
new file mode 100644
index 0000000..76eb2c5
--- /dev/null
+++ b/src/lib-fs/fs-api-private.h
@@ -0,0 +1,213 @@
+#ifndef FS_API_PRIVATE_H
+#define FS_API_PRIVATE_H
+
+#include "fs-api.h"
+#include "fs-wrapper.h"
+#include "module-context.h"
+
+#include <sys/time.h>
+
+#define FS_EVENT_FIELD_FS "lib-fs#fs"
+#define FS_EVENT_FIELD_FILE "lib-fs#file"
+#define FS_EVENT_FIELD_ITER "lib-fs#iter"
+
+enum fs_get_metadata_flags {
+ FS_GET_METADATA_FLAG_LOADED_ONLY = BIT(0),
+};
+
+struct fs_api_module_register {
+ unsigned int id;
+};
+
+union fs_api_module_context {
+ struct fs_api_module_register *reg;
+};
+
+extern struct fs_api_module_register fs_api_module_register;
+
+struct fs_vfuncs {
+ struct fs *(*alloc)(void);
+ int (*init)(struct fs *fs, const char *args,
+ const struct fs_settings *set, const char **error_r);
+ void (*deinit)(struct fs *fs);
+ void (*free)(struct fs *fs);
+
+ enum fs_properties (*get_properties)(struct fs *fs);
+
+ struct fs_file *(*file_alloc)(void);
+ void (*file_init)(struct fs_file *file, const char *path,
+ enum fs_open_mode mode, enum fs_open_flags flags);
+ void (*file_deinit)(struct fs_file *file);
+ void (*file_close)(struct fs_file *file);
+ const char *(*get_path)(struct fs_file *file);
+
+ void (*set_async_callback)(struct fs_file *file,
+ fs_file_async_callback_t *callback,
+ void *context);
+ void (*wait_async)(struct fs *fs);
+
+ void (*set_metadata)(struct fs_file *file, const char *key,
+ const char *value);
+ int (*get_metadata)(struct fs_file *file,
+ enum fs_get_metadata_flags flags,
+ const ARRAY_TYPE(fs_metadata) **metadata_r);
+
+ bool (*prefetch)(struct fs_file *file, uoff_t length);
+ ssize_t (*read)(struct fs_file *file, void *buf, size_t size);
+ struct istream *(*read_stream)(struct fs_file *file,
+ size_t max_buffer_size);
+
+ int (*write)(struct fs_file *file, const void *data, size_t size);
+ void (*write_stream)(struct fs_file *file);
+ /* After write_stream_finish() is called once, all the following
+ (async) calls will have success==TRUE. */
+ int (*write_stream_finish)(struct fs_file *file, bool success);
+
+ int (*lock)(struct fs_file *file, unsigned int secs,
+ struct fs_lock **lock_r);
+ void (*unlock)(struct fs_lock *lock);
+
+ int (*exists)(struct fs_file *file);
+ int (*stat)(struct fs_file *file, struct stat *st_r);
+ int (*copy)(struct fs_file *src, struct fs_file *dest);
+ int (*rename)(struct fs_file *src, struct fs_file *dest);
+ int (*delete_file)(struct fs_file *file);
+
+ struct fs_iter *(*iter_alloc)(void);
+ void (*iter_init)(struct fs_iter *iter, const char *path,
+ enum fs_iter_flags flags);
+ const char *(*iter_next)(struct fs_iter *iter);
+ int (*iter_deinit)(struct fs_iter *iter);
+
+ bool (*switch_ioloop)(struct fs *fs);
+ int (*get_nlinks)(struct fs_file *file, nlink_t *nlinks_r);
+};
+
+struct fs {
+ struct fs *parent; /* for wrapper filesystems */
+ const char *name;
+ struct fs_vfuncs v;
+ char *temp_path_prefix;
+ int refcount;
+
+ char *username, *session_id;
+
+ struct fs_settings set;
+
+ /* may be used by fs_wait_async() to do the waiting */
+ struct ioloop *wait_ioloop, *prev_ioloop;
+
+ unsigned int files_open_count;
+ struct fs_file *files;
+ struct fs_iter *iters;
+ struct event *event;
+
+ struct fs_stats stats;
+
+ ARRAY(union fs_api_module_context *) module_contexts;
+};
+
+struct fs_file {
+ /* linked list of all files */
+ struct fs_file *prev, *next;
+
+ struct fs_file *parent; /* for wrapper filesystems */
+ struct fs *fs;
+ struct ostream *output;
+ struct event *event;
+ char *path;
+ char *last_error;
+ enum fs_open_flags flags;
+
+ struct istream *seekable_input;
+ struct istream *pending_read_input;
+
+ const struct hash_method *write_digest_method;
+ void *write_digest;
+
+ pool_t metadata_pool;
+ ARRAY_TYPE(fs_metadata) metadata;
+
+ struct fs_file *copy_src;
+ struct istream *copy_input;
+ struct ostream *copy_output;
+
+ struct timeval timing_start[FS_OP_COUNT];
+
+ bool write_pending:1;
+ bool writing_stream:1;
+ bool metadata_changed:1;
+
+ bool read_or_prefetch_counted:1;
+ bool lookup_metadata_counted:1;
+ bool stat_counted:1;
+ bool copy_counted:1;
+ bool istream_open:1;
+ bool last_error_changed:1;
+};
+
+struct fs_lock {
+ struct fs_file *file;
+};
+
+struct fs_iter {
+ /* linked list of all iters */
+ struct fs_iter *prev, *next;
+
+ struct fs *fs;
+ struct event *event;
+ char *path;
+ enum fs_iter_flags flags;
+ struct timeval start_time;
+ char *last_error;
+
+ bool async_have_more;
+ fs_file_async_callback_t *async_callback;
+ void *async_context;
+};
+
+extern const struct fs fs_class_dict;
+extern const struct fs fs_class_posix;
+extern const struct fs fs_class_randomfail;
+extern const struct fs fs_class_metawrap;
+extern const struct fs fs_class_sis;
+extern const struct fs fs_class_sis_queue;
+extern const struct fs fs_class_test;
+
+void fs_class_register(const struct fs *fs_class);
+
+/* Event must be fs_file or fs_iter events. Set errno from err. */
+void fs_set_error(struct event *event, int err,
+ const char *fmt, ...) ATTR_FORMAT(3, 4);
+/* Like fs_set_error(), but use the existing errno. */
+void fs_set_error_errno(struct event *event, const char *fmt, ...) ATTR_FORMAT(2, 3);
+void fs_file_set_error_async(struct fs_file *file);
+
+ssize_t fs_read_via_stream(struct fs_file *file, void *buf, size_t size);
+int fs_write_via_stream(struct fs_file *file, const void *data, size_t size);
+void fs_metadata_init(struct fs_file *file);
+void fs_metadata_init_or_clear(struct fs_file *file);
+void fs_default_set_metadata(struct fs_file *file,
+ const char *key, const char *value);
+int fs_get_metadata_full(struct fs_file *file,
+ enum fs_get_metadata_flags flags,
+ const ARRAY_TYPE(fs_metadata) **metadata_r);
+const char *fs_metadata_find(const ARRAY_TYPE(fs_metadata) *metadata,
+ const char *key);
+int fs_default_copy(struct fs_file *src, struct fs_file *dest);
+
+void fs_file_timing_end(struct fs_file *file, enum fs_op op);
+
+struct fs_file *
+fs_file_init_parent(struct fs_file *parent, const char *path,
+ enum fs_open_mode mode, enum fs_open_flags flags);
+struct fs_iter *
+fs_iter_init_parent(struct fs_iter *parent,
+ const char *path, enum fs_iter_flags flags);
+void fs_file_free(struct fs_file *file);
+
+/* Same as fs_write_stream_abort_error(), except it closes the *parent* file
+ and error is left untouched */
+void fs_write_stream_abort_parent(struct fs_file *file, struct ostream **output);
+
+#endif
diff --git a/src/lib-fs/fs-api.c b/src/lib-fs/fs-api.c
new file mode 100644
index 0000000..1b47ded
--- /dev/null
+++ b/src/lib-fs/fs-api.c
@@ -0,0 +1,1407 @@
+/* Copyright (c) 2010-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "module-dir.h"
+#include "llist.h"
+#include "str.h"
+#include "hash-method.h"
+#include "istream.h"
+#include "istream-seekable.h"
+#include "ostream.h"
+#include "stats-dist.h"
+#include "time-util.h"
+#include "istream-fs-stats.h"
+#include "fs-api-private.h"
+
+static struct event_category event_category_fs = {
+ .name = "fs"
+};
+
+struct fs_api_module_register fs_api_module_register = { 0 };
+
+static struct module *fs_modules = NULL;
+static ARRAY(const struct fs *) fs_classes;
+
+static void fs_classes_init(void);
+
+static struct event *fs_create_event(struct fs *fs, struct event *parent)
+{
+ struct event *event;
+
+ event = event_create(parent);
+ event_add_category(event, &event_category_fs);
+ event_set_append_log_prefix(event,
+ t_strdup_printf("fs-%s: ", fs->name));
+ return event;
+}
+
+static int
+fs_alloc(const struct fs *fs_class, const char *args,
+ const struct fs_settings *set, struct fs **fs_r, const char **error_r)
+{
+ struct fs *fs;
+ const char *error;
+ int ret;
+
+ fs = fs_class->v.alloc();
+ fs->refcount = 1;
+ fs->set.debug = set->debug;
+ fs->set.enable_timing = set->enable_timing;
+ i_array_init(&fs->module_contexts, 5);
+ fs->event = fs_create_event(fs, set->event_parent);
+ event_set_forced_debug(fs->event, fs->set.debug);
+
+ T_BEGIN {
+ ret = fs_class->v.init(fs, args, set, &error);
+ } T_END_PASS_STR_IF(ret < 0, &error);
+ if (ret < 0) {
+ /* a bit kludgy way to allow data stack frame usage in normal
+ conditions but still be able to return error message from
+ data stack. */
+ *error_r = t_strdup_printf("%s: %s", fs_class->name, error);
+ fs_unref(&fs);
+ return -1;
+ }
+ fs->username = i_strdup(set->username);
+ fs->session_id = i_strdup(set->session_id);
+ *fs_r = fs;
+ return 0;
+}
+
+void fs_class_register(const struct fs *fs_class)
+{
+ if (!array_is_created(&fs_classes))
+ fs_classes_init();
+ array_push_back(&fs_classes, &fs_class);
+}
+
+static void fs_classes_deinit(void)
+{
+ array_free(&fs_classes);
+}
+
+static void fs_classes_init(void)
+{
+ i_array_init(&fs_classes, 8);
+ fs_class_register(&fs_class_dict);
+ fs_class_register(&fs_class_posix);
+ fs_class_register(&fs_class_randomfail);
+ fs_class_register(&fs_class_metawrap);
+ fs_class_register(&fs_class_sis);
+ fs_class_register(&fs_class_sis_queue);
+ fs_class_register(&fs_class_test);
+ lib_atexit(fs_classes_deinit);
+}
+
+static const struct fs *fs_class_find(const char *driver)
+{
+ const struct fs *class;
+
+ if (!array_is_created(&fs_classes))
+ fs_classes_init();
+
+ array_foreach_elem(&fs_classes, class) {
+ if (strcmp(class->name, driver) == 0)
+ return class;
+ }
+ return NULL;
+}
+
+static void fs_class_deinit_modules(void)
+{
+ module_dir_unload(&fs_modules);
+}
+
+static const char *fs_driver_module_name(const char *driver)
+{
+ return t_str_replace(driver, '-', '_');
+}
+
+static void fs_class_try_load_plugin(const char *driver)
+{
+ const char *module_name =
+ t_strdup_printf("fs_%s", fs_driver_module_name(driver));
+ struct module *module;
+ struct module_dir_load_settings mod_set;
+ const struct fs *fs_class;
+
+ i_zero(&mod_set);
+ mod_set.abi_version = DOVECOT_ABI_VERSION;
+ mod_set.ignore_missing = TRUE;
+
+ fs_modules = module_dir_load_missing(fs_modules, MODULE_DIR,
+ module_name, &mod_set);
+ module_dir_init(fs_modules);
+
+ module = module_dir_find(fs_modules, module_name);
+ fs_class = module == NULL ? NULL :
+ module_get_symbol(module, t_strdup_printf(
+ "fs_class_%s", fs_driver_module_name(driver)));
+ if (fs_class != NULL)
+ fs_class_register(fs_class);
+
+ lib_atexit(fs_class_deinit_modules);
+}
+
+int fs_init(const char *driver, const char *args,
+ const struct fs_settings *set,
+ struct fs **fs_r, const char **error_r)
+{
+ const struct fs *fs_class;
+ const char *temp_file_prefix;
+
+ fs_class = fs_class_find(driver);
+ if (fs_class == NULL) {
+ T_BEGIN {
+ fs_class_try_load_plugin(driver);
+ } T_END;
+ fs_class = fs_class_find(driver);
+ }
+ if (fs_class == NULL) {
+ *error_r = t_strdup_printf("Unknown fs driver: %s", driver);
+ return -1;
+ }
+ if (fs_alloc(fs_class, args, set, fs_r, error_r) < 0)
+ return -1;
+ event_set_ptr((*fs_r)->event, FS_EVENT_FIELD_FS, *fs_r);
+
+ temp_file_prefix = set->temp_file_prefix != NULL ?
+ set->temp_file_prefix : ".temp.dovecot";
+ if(set->temp_dir == NULL)
+ (*fs_r)->temp_path_prefix = i_strconcat("/tmp/",
+ temp_file_prefix, NULL);
+ else
+ (*fs_r)->temp_path_prefix = i_strconcat(set->temp_dir, "/",
+ temp_file_prefix, NULL);
+ return 0;
+}
+
+int fs_init_from_string(const char *str, const struct fs_settings *set,
+ struct fs **fs_r, const char **error_r)
+{
+ const char *args = strpbrk(str, " :");
+ if (args == NULL)
+ args = "";
+ else
+ str = t_strdup_until(str, args++);
+ return fs_init(str, args, set, fs_r, error_r);
+}
+
+void fs_deinit(struct fs **fs)
+{
+ fs_unref(fs);
+}
+
+void fs_ref(struct fs *fs)
+{
+ i_assert(fs->refcount > 0);
+
+ fs->refcount++;
+}
+
+void fs_unref(struct fs **_fs)
+{
+ struct fs *fs = *_fs;
+ struct array module_contexts_arr;
+ unsigned int i;
+
+ if (fs == NULL)
+ return;
+
+ module_contexts_arr = fs->module_contexts.arr;
+
+ i_assert(fs->refcount > 0);
+
+ *_fs = NULL;
+
+ if (--fs->refcount > 0)
+ return;
+
+ if (fs->files_open_count > 0) {
+ i_panic("fs-%s: %u files still open (first = %s)",
+ fs->name, fs->files_open_count, fs_file_path(fs->files));
+ }
+ i_assert(fs->files == NULL);
+
+ if (fs->v.deinit != NULL)
+ fs->v.deinit(fs);
+
+ fs_deinit(&fs->parent);
+ event_unref(&fs->event);
+ i_free(fs->username);
+ i_free(fs->session_id);
+ i_free(fs->temp_path_prefix);
+ for (i = 0; i < FS_OP_COUNT; i++) {
+ if (fs->stats.timings[i] != NULL)
+ stats_dist_deinit(&fs->stats.timings[i]);
+ }
+ T_BEGIN {
+ fs->v.free(fs);
+ } T_END;
+ array_free_i(&module_contexts_arr);
+}
+
+struct fs *fs_get_parent(struct fs *fs)
+{
+ return fs->parent;
+}
+
+const char *fs_get_driver(struct fs *fs)
+{
+ return fs->name;
+}
+
+const char *fs_get_root_driver(struct fs *fs)
+{
+ while (fs->parent != NULL)
+ fs = fs->parent;
+ return fs->name;
+}
+
+struct fs_file *fs_file_init(struct fs *fs, const char *path, int mode_flags)
+{
+ return fs_file_init_with_event(fs, fs->event, path, mode_flags);
+}
+
+struct fs_file *fs_file_init_with_event(struct fs *fs, struct event *event,
+ const char *path, int mode_flags)
+{
+ struct fs_file *file;
+
+ i_assert(path != NULL);
+ i_assert((mode_flags & FS_OPEN_FLAG_ASYNC_NOQUEUE) == 0 ||
+ (mode_flags & FS_OPEN_FLAG_ASYNC) != 0);
+
+ T_BEGIN {
+ file = fs->v.file_alloc();
+ file->fs = fs;
+ file->flags = mode_flags & ENUM_NEGATE(FS_OPEN_MODE_MASK);
+ file->event = fs_create_event(fs, event);
+ event_set_ptr(file->event, FS_EVENT_FIELD_FS, fs);
+ event_set_ptr(file->event, FS_EVENT_FIELD_FILE, file);
+ fs->v.file_init(file, path, mode_flags & FS_OPEN_MODE_MASK,
+ mode_flags & ENUM_NEGATE(FS_OPEN_MODE_MASK));
+ } T_END;
+
+ fs->files_open_count++;
+ DLLIST_PREPEND(&fs->files, file);
+
+ fs_set_metadata(file, FS_METADATA_ORIG_PATH, path);
+ return file;
+}
+
+void fs_file_deinit(struct fs_file **_file)
+{
+ struct fs_file *file = *_file;
+
+ if (file == NULL)
+ return;
+
+ i_assert(file->fs->files_open_count > 0);
+
+ *_file = NULL;
+
+ fs_file_close(file);
+
+ DLLIST_REMOVE(&file->fs->files, file);
+ file->fs->files_open_count--;
+ T_BEGIN {
+ file->fs->v.file_deinit(file);
+ } T_END;
+}
+
+void fs_file_free(struct fs_file *file)
+{
+ if (file->last_error_changed) {
+ /* fs_set_error() used without ever accessing it via
+ fs_file_last_error(). Log it to make sure it's not lost.
+ Note that the errors are always set only to the file at
+ the root of the parent hierarchy. */
+ e_error(file->event, "%s (in file %s deinit)",
+ file->last_error, fs_file_path(file));
+ }
+
+ fs_file_deinit(&file->parent);
+ event_unref(&file->event);
+ pool_unref(&file->metadata_pool);
+ i_free(file->last_error);
+}
+
+void fs_file_set_flags(struct fs_file *file,
+ enum fs_open_flags add_flags,
+ enum fs_open_flags remove_flags)
+{
+ file->flags |= add_flags;
+ file->flags &= ENUM_NEGATE(remove_flags);
+
+ if (file->parent != NULL)
+ fs_file_set_flags(file->parent, add_flags, remove_flags);
+}
+
+void fs_file_close(struct fs_file *file)
+{
+ if (file == NULL)
+ return;
+
+ i_assert(!file->writing_stream);
+ i_assert(file->output == NULL);
+
+ if (file->pending_read_input != NULL)
+ i_stream_unref(&file->pending_read_input);
+ if (file->seekable_input != NULL)
+ i_stream_unref(&file->seekable_input);
+
+ if (file->copy_input != NULL) {
+ i_stream_unref(&file->copy_input);
+ fs_write_stream_abort_error(file, &file->copy_output, "fs_file_close(%s)",
+ o_stream_get_name(file->copy_output));
+ }
+ i_free_and_null(file->write_digest);
+ if (file->fs->v.file_close != NULL) T_BEGIN {
+ file->fs->v.file_close(file);
+ } T_END;
+
+ /* check this only after closing, because some of the fs backends keep
+ the istream internally open and don't call the destroy-callback
+ until after file_close() */
+ i_assert(!file->istream_open);
+}
+
+enum fs_properties fs_get_properties(struct fs *fs)
+{
+ return fs->v.get_properties(fs);
+}
+
+void fs_metadata_init(struct fs_file *file)
+{
+ if (file->metadata_pool == NULL) {
+ i_assert(!array_is_created(&file->metadata));
+ file->metadata_pool = pool_alloconly_create("fs metadata", 1024);
+ p_array_init(&file->metadata, file->metadata_pool, 8);
+ }
+}
+
+void fs_metadata_init_or_clear(struct fs_file *file)
+{
+ if (file->metadata_pool == NULL)
+ fs_metadata_init(file);
+ else T_BEGIN {
+ const struct fs_metadata *md;
+ ARRAY_TYPE(fs_metadata) internal_metadata;
+
+ t_array_init(&internal_metadata, 4);
+ array_foreach(&file->metadata, md) {
+ if (strncmp(md->key, FS_METADATA_INTERNAL_PREFIX,
+ strlen(FS_METADATA_INTERNAL_PREFIX)) == 0)
+ array_push_back(&internal_metadata, md);
+ }
+ array_clear(&file->metadata);
+ array_append_array(&file->metadata, &internal_metadata);
+ } T_END;
+}
+
+static struct fs_metadata *
+fs_metadata_find_md(const ARRAY_TYPE(fs_metadata) *metadata,
+ const char *key)
+{
+ struct fs_metadata *md;
+
+ array_foreach_modifiable(metadata, md) {
+ if (strcmp(md->key, key) == 0)
+ return md;
+ }
+ return NULL;
+}
+
+void fs_default_set_metadata(struct fs_file *file,
+ const char *key, const char *value)
+{
+ struct fs_metadata *metadata;
+
+ fs_metadata_init(file);
+ metadata = fs_metadata_find_md(&file->metadata, key);
+ if (metadata == NULL) {
+ metadata = array_append_space(&file->metadata);
+ metadata->key = p_strdup(file->metadata_pool, key);
+ }
+ metadata->value = p_strdup(file->metadata_pool, value);
+}
+
+const char *fs_metadata_find(const ARRAY_TYPE(fs_metadata) *metadata,
+ const char *key)
+{
+ const struct fs_metadata *md;
+
+ if (!array_is_created(metadata))
+ return NULL;
+
+ md = fs_metadata_find_md(metadata, key);
+ return md == NULL ? NULL : md->value;
+}
+
+void fs_set_metadata(struct fs_file *file, const char *key, const char *value)
+{
+ i_assert(key != NULL);
+ i_assert(value != NULL);
+
+ if (file->fs->v.set_metadata != NULL) T_BEGIN {
+ file->fs->v.set_metadata(file, key, value);
+ if (strncmp(key, FS_METADATA_INTERNAL_PREFIX,
+ strlen(FS_METADATA_INTERNAL_PREFIX)) == 0) {
+ /* internal metadata change, which isn't stored. */
+ } else {
+ file->metadata_changed = TRUE;
+ }
+ } T_END;
+}
+
+static void fs_file_timing_start(struct fs_file *file, enum fs_op op)
+{
+ if (!file->fs->set.enable_timing)
+ return;
+ if (file->timing_start[op].tv_sec == 0)
+ i_gettimeofday(&file->timing_start[op]);
+}
+
+static void
+fs_timing_end(struct stats_dist **timing, const struct timeval *start_tv)
+{
+ struct timeval now;
+ long long diff;
+
+ i_gettimeofday(&now);
+
+ diff = timeval_diff_usecs(&now, start_tv);
+ if (diff > 0) {
+ if (*timing == NULL)
+ *timing = stats_dist_init();
+ stats_dist_add(*timing, diff);
+ }
+}
+
+void fs_file_timing_end(struct fs_file *file, enum fs_op op)
+{
+ if (!file->fs->set.enable_timing || file->timing_start[op].tv_sec == 0)
+ return;
+
+ fs_timing_end(&file->fs->stats.timings[op], &file->timing_start[op]);
+ /* don't count this again */
+ file->timing_start[op].tv_sec = 0;
+}
+
+int fs_get_metadata_full(struct fs_file *file,
+ enum fs_get_metadata_flags flags,
+ const ARRAY_TYPE(fs_metadata) **metadata_r)
+{
+ int ret;
+
+ if (file->fs->v.get_metadata == NULL) {
+ if (array_is_created(&file->metadata)) {
+ /* Return internal metadata. */
+ *metadata_r = &file->metadata;
+ return 0;
+ }
+ fs_set_error(file->event, ENOTSUP, "Metadata not supported by backend");
+ return -1;
+ }
+ if (!file->read_or_prefetch_counted &&
+ !file->lookup_metadata_counted) {
+ if ((flags & FS_GET_METADATA_FLAG_LOADED_ONLY) == 0) {
+ file->lookup_metadata_counted = TRUE;
+ file->fs->stats.lookup_metadata_count++;
+ }
+ fs_file_timing_start(file, FS_OP_METADATA);
+ }
+ T_BEGIN {
+ ret = file->fs->v.get_metadata(file, flags, metadata_r);
+ } T_END;
+ if (!(ret < 0 && errno == EAGAIN))
+ fs_file_timing_end(file, FS_OP_METADATA);
+ return ret;
+}
+
+int fs_get_metadata(struct fs_file *file,
+ const ARRAY_TYPE(fs_metadata) **metadata_r)
+{
+ return fs_get_metadata_full(file, 0, metadata_r);
+}
+
+int fs_lookup_metadata(struct fs_file *file, const char *key,
+ const char **value_r)
+{
+ const ARRAY_TYPE(fs_metadata) *metadata;
+
+ if (fs_get_metadata(file, &metadata) < 0)
+ return -1;
+ *value_r = fs_metadata_find(metadata, key);
+ return *value_r != NULL ? 1 : 0;
+}
+
+const char *fs_lookup_loaded_metadata(struct fs_file *file, const char *key)
+{
+ const ARRAY_TYPE(fs_metadata) *metadata;
+
+ if (fs_get_metadata_full(file, FS_GET_METADATA_FLAG_LOADED_ONLY, &metadata) < 0)
+ i_panic("FS_GET_METADATA_FLAG_LOADED_ONLY lookup can't fail");
+ return fs_metadata_find(metadata, key);
+}
+
+const char *fs_file_path(struct fs_file *file)
+{
+ return file->fs->v.get_path == NULL ? file->path :
+ file->fs->v.get_path(file);
+}
+
+struct fs *fs_file_fs(struct fs_file *file)
+{
+ return file->fs;
+}
+
+struct event *fs_file_event(struct fs_file *file)
+{
+ return file->event;
+}
+
+static struct fs_file *fs_file_get_error_file(struct fs_file *file)
+{
+ /* the error is always kept in the parentmost file */
+ while (file->parent != NULL)
+ file = file->parent;
+ return file;
+}
+
+static void ATTR_FORMAT(2, 0)
+fs_set_verror(struct event *event, const char *fmt, va_list args)
+{
+ struct event *fs_event = event;
+ struct fs_file *file;
+ struct fs_iter *iter;
+
+ /* NOTE: the event might be a passthrough event. We must log it exactly
+ once so it gets freed. */
+
+ /* figure out if the error is for a file or iter */
+ while ((file = event_get_ptr(fs_event, FS_EVENT_FIELD_FILE)) == NULL &&
+ (iter = event_get_ptr(fs_event, FS_EVENT_FIELD_ITER)) == NULL) {
+ fs_event = event_get_parent(fs_event);
+ i_assert(fs_event != NULL);
+ }
+
+ char *new_error = i_strdup_vprintf(fmt, args);
+ /* Don't flood the debug log with "Asynchronous operation in progress"
+ messages. They tell nothing useful. */
+ if (errno != EAGAIN)
+ e_debug(event, "%s", new_error);
+ else
+ event_send_abort(event);
+
+ /* free old error after strdup in case args point to the old error */
+ if (file != NULL) {
+ file = fs_file_get_error_file(file);
+ char *old_error = file->last_error;
+
+ if (old_error == NULL) {
+ i_assert(!file->last_error_changed);
+ } else if (strcmp(old_error, new_error) == 0) {
+ /* identical error - ignore */
+ } else if (file->last_error_changed) {
+ /* multiple fs_set_error() calls used without
+ fs_file_last_error() in the middle. */
+ e_error(file->event, "%s (overwriting error for file %s)",
+ old_error, fs_file_path(file));
+ }
+ if (errno == EAGAIN || errno == ENOENT || errno == EEXIST ||
+ errno == ENOTEMPTY) {
+ /* These are (or can be) expected errors - don't log
+ them if they have a missing fs_file_last_error()
+ call */
+ file->last_error_changed = FALSE;
+ } else {
+ file->last_error_changed = TRUE;
+ }
+
+ i_free(file->last_error);
+ file->last_error = new_error;
+ } else {
+ i_assert(iter != NULL);
+ if (iter->last_error != NULL &&
+ strcmp(iter->last_error, new_error) == 0) {
+ /* identical error - ignore */
+ } else if (iter->last_error != NULL) {
+ /* multiple fs_set_error() calls before the iter
+ finishes */
+ e_error(iter->fs->event, "%s (overwriting error for file %s)",
+ iter->last_error, iter->path);
+ }
+ i_free(iter->last_error);
+ iter->last_error = new_error;
+ }
+}
+
+const char *fs_file_last_error(struct fs_file *file)
+{
+ struct fs_file *error_file = fs_file_get_error_file(file);
+
+ error_file->last_error_changed = FALSE;
+ if (error_file->last_error == NULL)
+ return "BUG: Unknown file error";
+ return error_file->last_error;
+}
+
+bool fs_prefetch(struct fs_file *file, uoff_t length)
+{
+ bool ret;
+
+ if (!file->read_or_prefetch_counted) {
+ file->read_or_prefetch_counted = TRUE;
+ file->fs->stats.prefetch_count++;
+ fs_file_timing_start(file, FS_OP_PREFETCH);
+ }
+ T_BEGIN {
+ ret = file->fs->v.prefetch(file, length);
+ } T_END;
+ fs_file_timing_end(file, FS_OP_PREFETCH);
+ return ret;
+}
+
+ssize_t fs_read_via_stream(struct fs_file *file, void *buf, size_t size)
+{
+ const unsigned char *data;
+ size_t data_size;
+ ssize_t ret;
+
+ i_assert(size > 0);
+
+ if (file->pending_read_input == NULL)
+ file->pending_read_input = fs_read_stream(file, size+1);
+ ret = i_stream_read_bytes(file->pending_read_input, &data,
+ &data_size, size);
+ if (ret == 0) {
+ fs_file_set_error_async(file);
+ return -1;
+ }
+ if (ret < 0 && file->pending_read_input->stream_errno != 0) {
+ fs_set_error(file->event,
+ file->pending_read_input->stream_errno,
+ "read(%s) failed: %s",
+ i_stream_get_name(file->pending_read_input),
+ i_stream_get_error(file->pending_read_input));
+ } else {
+ ret = I_MIN(size, data_size);
+ memcpy(buf, data, ret);
+ }
+ i_stream_unref(&file->pending_read_input);
+ return ret;
+}
+
+ssize_t fs_read(struct fs_file *file, void *buf, size_t size)
+{
+ int ret;
+
+ if (!file->read_or_prefetch_counted) {
+ file->read_or_prefetch_counted = TRUE;
+ file->fs->stats.read_count++;
+ fs_file_timing_start(file, FS_OP_READ);
+ }
+
+ if (file->fs->v.read != NULL) {
+ T_BEGIN {
+ ret = file->fs->v.read(file, buf, size);
+ } T_END;
+ if (!(ret < 0 && errno == EAGAIN))
+ fs_file_timing_end(file, FS_OP_READ);
+ return ret;
+ }
+
+ /* backend didn't bother to implement read(), but we can do it with
+ streams. */
+ return fs_read_via_stream(file, buf, size);
+}
+
+static void fs_file_istream_destroyed(struct fs_file *file)
+{
+ i_assert(file->istream_open);
+
+ file->istream_open = FALSE;
+}
+
+struct istream *fs_read_stream(struct fs_file *file, size_t max_buffer_size)
+{
+ struct istream *input, *inputs[2];
+ const unsigned char *data;
+ size_t size;
+ ssize_t ret;
+ bool want_seekable = FALSE;
+
+ if (!file->read_or_prefetch_counted) {
+ file->read_or_prefetch_counted = TRUE;
+ file->fs->stats.read_count++;
+ fs_file_timing_start(file, FS_OP_READ);
+ }
+
+ if (file->seekable_input != NULL) {
+ /* allow multiple open streams, each in a different position */
+ input = i_stream_create_limit(file->seekable_input, UOFF_T_MAX);
+ i_stream_seek(input, 0);
+ return input;
+ }
+ i_assert(!file->istream_open);
+ T_BEGIN {
+ input = file->fs->v.read_stream(file, max_buffer_size);
+ } T_END;
+ if (input->stream_errno != 0) {
+ /* read failed already */
+ fs_file_timing_end(file, FS_OP_READ);
+ return input;
+ }
+ if (file->fs->set.enable_timing) {
+ struct istream *input2 = i_stream_create_fs_stats(input, file);
+
+ i_stream_unref(&input);
+ input = input2;
+ }
+
+ if ((file->flags & FS_OPEN_FLAG_SEEKABLE) != 0)
+ want_seekable = TRUE;
+ else if ((file->flags & FS_OPEN_FLAG_ASYNC) == 0 && !input->blocking)
+ want_seekable = TRUE;
+
+ if (want_seekable && !input->seekable) {
+ /* need to make the stream seekable */
+ inputs[0] = input;
+ inputs[1] = NULL;
+ input = i_stream_create_seekable_path(inputs, max_buffer_size,
+ file->fs->temp_path_prefix);
+ i_stream_set_name(input, i_stream_get_name(inputs[0]));
+ i_stream_unref(&inputs[0]);
+ }
+ file->seekable_input = input;
+ i_stream_ref(file->seekable_input);
+
+ if ((file->flags & FS_OPEN_FLAG_ASYNC) == 0 && !input->blocking) {
+ /* read the whole input stream before returning */
+ while ((ret = i_stream_read_more(input, &data, &size)) >= 0) {
+ i_stream_skip(input, size);
+ if (ret == 0)
+ fs_wait_async(file->fs);
+ }
+ i_stream_seek(input, 0);
+ }
+ file->istream_open = TRUE;
+ i_stream_add_destroy_callback(input, fs_file_istream_destroyed, file);
+ return input;
+}
+
+int fs_write_via_stream(struct fs_file *file, const void *data, size_t size)
+{
+ struct ostream *output;
+ ssize_t ret;
+ int err;
+
+ if (!file->write_pending) {
+ output = fs_write_stream(file);
+ if ((ret = o_stream_send(output, data, size)) < 0) {
+ err = errno;
+ fs_write_stream_abort_error(file, &output, "fs_write(%s) failed: %s",
+ o_stream_get_name(output),
+ o_stream_get_error(output));
+ errno = err;
+ return -1;
+ }
+ i_assert((size_t)ret == size);
+ ret = fs_write_stream_finish(file, &output);
+ } else {
+ ret = fs_write_stream_finish_async(file);
+ }
+ if (ret == 0) {
+ fs_file_set_error_async(file);
+ file->write_pending = TRUE;
+ return -1;
+ }
+ file->write_pending = FALSE;
+ return ret < 0 ? -1 : 0;
+}
+
+int fs_write(struct fs_file *file, const void *data, size_t size)
+{
+ int ret;
+
+ if (file->fs->v.write != NULL) {
+ fs_file_timing_start(file, FS_OP_WRITE);
+ T_BEGIN {
+ ret = file->fs->v.write(file, data, size);
+ } T_END;
+ if (!(ret < 0 && errno == EAGAIN)) {
+ file->fs->stats.write_count++;
+ file->fs->stats.write_bytes += size;
+ fs_file_timing_end(file, FS_OP_WRITE);
+ }
+ return ret;
+ }
+
+ /* backend didn't bother to implement write(), but we can do it with
+ streams. */
+ return fs_write_via_stream(file, data, size);
+}
+
+struct ostream *fs_write_stream(struct fs_file *file)
+{
+ i_assert(!file->writing_stream);
+ i_assert(file->output == NULL);
+
+ file->writing_stream = TRUE;
+ file->fs->stats.write_count++;
+ T_BEGIN {
+ file->fs->v.write_stream(file);
+ } T_END;
+ i_assert(file->output != NULL);
+ o_stream_cork(file->output);
+ return file->output;
+}
+
+static int fs_write_stream_finish_int(struct fs_file *file, bool success)
+{
+ int ret;
+
+ i_assert(file->writing_stream);
+
+ fs_file_timing_start(file, FS_OP_WRITE);
+ T_BEGIN {
+ ret = file->fs->v.write_stream_finish(file, success);
+ } T_END;
+ if (ret != 0) {
+ fs_file_timing_end(file, FS_OP_WRITE);
+ file->metadata_changed = FALSE;
+ } else {
+ /* write didn't finish yet. this shouldn't happen if we
+ indicated a failure. */
+ i_assert(success);
+ }
+ if (ret != 0) {
+ i_assert(file->output == NULL);
+ file->writing_stream = FALSE;
+ }
+ return ret;
+}
+
+int fs_write_stream_finish(struct fs_file *file, struct ostream **output)
+{
+ bool success = TRUE;
+ int ret;
+
+ i_assert(*output == file->output || *output == NULL);
+ i_assert(output != &file->output);
+
+ *output = NULL;
+ if (file->output != NULL) {
+ o_stream_uncork(file->output);
+ if ((ret = o_stream_finish(file->output)) <= 0) {
+ i_assert(ret < 0);
+ fs_set_error(file->event, file->output->stream_errno,
+ "write(%s) failed: %s",
+ o_stream_get_name(file->output),
+ o_stream_get_error(file->output));
+ success = FALSE;
+ }
+ file->fs->stats.write_bytes += file->output->offset;
+ }
+ return fs_write_stream_finish_int(file, success);
+}
+
+int fs_write_stream_finish_async(struct fs_file *file)
+{
+ return fs_write_stream_finish_int(file, TRUE);
+}
+
+static void fs_write_stream_abort(struct fs_file *file, struct ostream **output)
+{
+ int ret;
+
+ i_assert(*output == file->output);
+ i_assert(file->output != NULL);
+ i_assert(output != &file->output);
+
+ *output = NULL;
+ o_stream_abort(file->output);
+ /* make sure we don't have an old error lying around */
+ ret = fs_write_stream_finish_int(file, FALSE);
+ i_assert(ret != 0);
+}
+
+void fs_write_stream_abort_error(struct fs_file *file, struct ostream **output, const char *error_fmt, ...)
+{
+ va_list args;
+ va_start(args, error_fmt);
+ fs_set_verror(file->event, error_fmt, args);
+ /* the error shouldn't be automatically logged if
+ fs_file_last_error() is no longer used */
+ fs_file_get_error_file(file)->last_error_changed = FALSE;
+ fs_write_stream_abort(file, output);
+ va_end(args);
+}
+
+void fs_write_stream_abort_parent(struct fs_file *file, struct ostream **output)
+{
+ i_assert(file->parent != NULL);
+ i_assert(fs_file_last_error(file->parent) != NULL);
+ fs_write_stream_abort(file->parent, output);
+}
+
+void fs_write_set_hash(struct fs_file *file, const struct hash_method *method,
+ const void *digest)
+{
+ file->write_digest_method = method;
+
+ i_free(file->write_digest);
+ file->write_digest = i_malloc(method->digest_size);
+ memcpy(file->write_digest, digest, method->digest_size);
+}
+
+#undef fs_file_set_async_callback
+void fs_file_set_async_callback(struct fs_file *file,
+ fs_file_async_callback_t *callback,
+ void *context)
+{
+ if (file->fs->v.set_async_callback != NULL)
+ file->fs->v.set_async_callback(file, callback, context);
+ else
+ callback(context);
+}
+
+void fs_wait_async(struct fs *fs)
+{
+ /* recursion not allowed */
+ i_assert(fs->prev_ioloop == NULL);
+
+ if (fs->v.wait_async != NULL) T_BEGIN {
+ fs->prev_ioloop = current_ioloop;
+ fs->v.wait_async(fs);
+ i_assert(current_ioloop == fs->prev_ioloop);
+ fs->prev_ioloop = NULL;
+ } T_END;
+}
+
+bool fs_switch_ioloop(struct fs *fs)
+{
+ bool ret = FALSE;
+
+ if (fs->v.switch_ioloop != NULL) {
+ T_BEGIN {
+ ret = fs->v.switch_ioloop(fs);
+ } T_END;
+ } else if (fs->parent != NULL) {
+ ret = fs_switch_ioloop(fs->parent);
+ }
+ return ret;
+}
+
+int fs_lock(struct fs_file *file, unsigned int secs, struct fs_lock **lock_r)
+{
+ int ret;
+
+ T_BEGIN {
+ ret = file->fs->v.lock(file, secs, lock_r);
+ } T_END;
+ return ret;
+}
+
+void fs_unlock(struct fs_lock **_lock)
+{
+ struct fs_lock *lock = *_lock;
+
+ if (lock == NULL)
+ return;
+
+ *_lock = NULL;
+ T_BEGIN {
+ lock->file->fs->v.unlock(lock);
+ } T_END;
+}
+
+int fs_exists(struct fs_file *file)
+{
+ struct stat st;
+ int ret;
+
+ if (file->fs->v.exists == NULL) {
+ /* fallback to stat() */
+ if (fs_stat(file, &st) == 0)
+ return 1;
+ else
+ return errno == ENOENT ? 0 : -1;
+ }
+ fs_file_timing_start(file, FS_OP_EXISTS);
+ T_BEGIN {
+ ret = file->fs->v.exists(file);
+ } T_END;
+ if (!(ret < 0 && errno == EAGAIN)) {
+ file->fs->stats.exists_count++;
+ fs_file_timing_end(file, FS_OP_EXISTS);
+ }
+ return ret;
+}
+
+int fs_stat(struct fs_file *file, struct stat *st_r)
+{
+ int ret;
+
+ if (file->fs->v.stat == NULL) {
+ fs_set_error(file->event, ENOTSUP, "fs_stat() not supported");
+ return -1;
+ }
+
+ if (!file->read_or_prefetch_counted &&
+ !file->lookup_metadata_counted && !file->stat_counted) {
+ file->stat_counted = TRUE;
+ file->fs->stats.stat_count++;
+ fs_file_timing_start(file, FS_OP_STAT);
+ }
+ T_BEGIN {
+ ret = file->fs->v.stat(file, st_r);
+ } T_END;
+ if (!(ret < 0 && errno == EAGAIN))
+ fs_file_timing_end(file, FS_OP_STAT);
+ return ret;
+}
+
+int fs_get_nlinks(struct fs_file *file, nlink_t *nlinks_r)
+{
+ int ret;
+
+ if (file->fs->v.get_nlinks == NULL) {
+ struct stat st;
+
+ if (fs_stat(file, &st) < 0)
+ return -1;
+ *nlinks_r = st.st_nlink;
+ return 0;
+ }
+
+ if (!file->read_or_prefetch_counted &&
+ !file->lookup_metadata_counted && !file->stat_counted) {
+ file->stat_counted = TRUE;
+ file->fs->stats.stat_count++;
+ fs_file_timing_start(file, FS_OP_STAT);
+ }
+ T_BEGIN {
+ ret = file->fs->v.get_nlinks(file, nlinks_r);
+ } T_END;
+ if (!(ret < 0 && errno == EAGAIN))
+ fs_file_timing_end(file, FS_OP_STAT);
+ return ret;
+}
+
+int fs_default_copy(struct fs_file *src, struct fs_file *dest)
+{
+ int tmp_errno;
+ /* we're going to be counting this as read+write, so don't update
+ copy_count */
+ dest->copy_counted = TRUE;
+
+ if (dest->copy_src != NULL) {
+ i_assert(src == NULL || src == dest->copy_src);
+ if (dest->copy_output == NULL) {
+ i_assert(dest->copy_input == NULL);
+ if (fs_write_stream_finish_async(dest) <= 0)
+ return -1;
+ dest->copy_src = NULL;
+ return 0;
+ }
+ } else {
+ dest->copy_src = src;
+ dest->copy_input = fs_read_stream(src, IO_BLOCK_SIZE);
+ dest->copy_output = fs_write_stream(dest);
+ }
+ switch (o_stream_send_istream(dest->copy_output, dest->copy_input)) {
+ case OSTREAM_SEND_ISTREAM_RESULT_FINISHED:
+ break;
+ case OSTREAM_SEND_ISTREAM_RESULT_WAIT_INPUT:
+ case OSTREAM_SEND_ISTREAM_RESULT_WAIT_OUTPUT:
+ fs_file_set_error_async(dest);
+ return -1;
+ case OSTREAM_SEND_ISTREAM_RESULT_ERROR_INPUT:
+ fs_write_stream_abort_error(dest, &dest->copy_output,
+ "read(%s) failed: %s",
+ i_stream_get_name(dest->copy_input),
+ i_stream_get_error(dest->copy_input));
+ errno = dest->copy_input->stream_errno;
+ i_stream_unref(&dest->copy_input);
+ return -1;
+ case OSTREAM_SEND_ISTREAM_RESULT_ERROR_OUTPUT:
+ /* errno might not survive abort error */
+ tmp_errno = dest->copy_output->stream_errno;
+ fs_write_stream_abort_error(dest, &dest->copy_output,
+ "write(%s) failed: %s",
+ o_stream_get_name(dest->copy_output),
+ o_stream_get_error(dest->copy_output));
+ errno = tmp_errno;
+ i_stream_unref(&dest->copy_input);
+ return -1;
+ }
+ i_stream_unref(&dest->copy_input);
+ if (fs_write_stream_finish(dest, &dest->copy_output) <= 0)
+ return -1;
+ dest->copy_src = NULL;
+ return 0;
+}
+
+int fs_copy(struct fs_file *src, struct fs_file *dest)
+{
+ int ret;
+
+ i_assert(src->fs == dest->fs);
+
+ if (src->fs->v.copy == NULL) {
+ fs_set_error(src->event, ENOTSUP, "fs_copy() not supported");
+ return -1;
+ }
+
+ fs_file_timing_start(dest, FS_OP_COPY);
+ T_BEGIN {
+ ret = src->fs->v.copy(src, dest);
+ } T_END;
+ if (!(ret < 0 && errno == EAGAIN)) {
+ fs_file_timing_end(dest, FS_OP_COPY);
+ if (dest->copy_counted)
+ dest->copy_counted = FALSE;
+ else
+ dest->fs->stats.copy_count++;
+ dest->metadata_changed = FALSE;
+ }
+ return ret;
+}
+
+int fs_copy_finish_async(struct fs_file *dest)
+{
+ int ret;
+
+ T_BEGIN {
+ ret = dest->fs->v.copy(NULL, dest);
+ } T_END;
+ if (!(ret < 0 && errno == EAGAIN)) {
+ fs_file_timing_end(dest, FS_OP_COPY);
+ if (dest->copy_counted)
+ dest->copy_counted = FALSE;
+ else
+ dest->fs->stats.copy_count++;
+ dest->metadata_changed = FALSE;
+ }
+ return ret;
+}
+
+int fs_rename(struct fs_file *src, struct fs_file *dest)
+{
+ int ret;
+
+ i_assert(src->fs == dest->fs);
+
+ fs_file_timing_start(dest, FS_OP_RENAME);
+ T_BEGIN {
+ ret = src->fs->v.rename(src, dest);
+ } T_END;
+ if (!(ret < 0 && errno == EAGAIN)) {
+ dest->fs->stats.rename_count++;
+ fs_file_timing_end(dest, FS_OP_RENAME);
+ }
+ return ret;
+}
+
+int fs_delete(struct fs_file *file)
+{
+ int ret;
+
+ i_assert(!file->writing_stream);
+
+ fs_file_timing_start(file, FS_OP_DELETE);
+ T_BEGIN {
+ ret = file->fs->v.delete_file(file);
+ } T_END;
+ if (!(ret < 0 && errno == EAGAIN)) {
+ file->fs->stats.delete_count++;
+ fs_file_timing_end(file, FS_OP_DELETE);
+ }
+ return ret;
+}
+
+struct fs_iter *
+fs_iter_init(struct fs *fs, const char *path, enum fs_iter_flags flags)
+{
+ return fs_iter_init_with_event(fs, fs->event, path, flags);
+}
+
+struct fs_iter *
+fs_iter_init_with_event(struct fs *fs, struct event *event,
+ const char *path, enum fs_iter_flags flags)
+{
+ struct fs_iter *iter;
+ struct timeval now = ioloop_timeval;
+
+ i_assert((flags & FS_ITER_FLAG_OBJECTIDS) == 0 ||
+ (fs_get_properties(fs) & FS_PROPERTY_OBJECTIDS) != 0);
+
+ fs->stats.iter_count++;
+ if (fs->set.enable_timing)
+ i_gettimeofday(&now);
+ if (fs->v.iter_init == NULL) {
+ iter = i_new(struct fs_iter, 1);
+ iter->fs = fs;
+ } else T_BEGIN {
+ iter = fs->v.iter_alloc();
+ iter->fs = fs;
+ iter->flags = flags;
+ iter->path = i_strdup(path);
+ iter->event = fs_create_event(fs, event);
+ event_set_ptr(iter->event, FS_EVENT_FIELD_FS, fs);
+ event_set_ptr(iter->event, FS_EVENT_FIELD_ITER, iter);
+ fs->v.iter_init(iter, path, flags);
+ } T_END;
+ iter->start_time = now;
+ DLLIST_PREPEND(&fs->iters, iter);
+ return iter;
+}
+
+int fs_iter_deinit(struct fs_iter **_iter, const char **error_r)
+{
+ struct fs_iter *iter = *_iter;
+ struct fs *fs;
+ struct event *event;
+ int ret;
+
+ if (iter == NULL)
+ return 0;
+
+ fs = iter->fs;
+ event = iter->event;
+
+ *_iter = NULL;
+ DLLIST_REMOVE(&fs->iters, iter);
+
+ if (fs->v.iter_deinit == NULL) {
+ fs_set_error(event, ENOTSUP, "FS iteration not supported");
+ ret = -1;
+ } else T_BEGIN {
+ ret = iter->fs->v.iter_deinit(iter);
+ } T_END;
+ if (ret < 0)
+ *error_r = t_strdup(iter->last_error);
+ i_free(iter->last_error);
+ i_free(iter->path);
+ i_free(iter);
+ event_unref(&event);
+ return ret;
+}
+
+const char *fs_iter_next(struct fs_iter *iter)
+{
+ const char *ret;
+
+ if (iter->fs->v.iter_next == NULL)
+ return NULL;
+ T_BEGIN {
+ ret = iter->fs->v.iter_next(iter);
+ } T_END;
+ if (iter->start_time.tv_sec != 0 &&
+ (ret != NULL || !fs_iter_have_more(iter))) {
+ /* first result returned - count this as the finish time, since
+ we don't want to count the time caller spends on this
+ iteration. */
+ fs_timing_end(&iter->fs->stats.timings[FS_OP_ITER], &iter->start_time);
+ /* don't count this again */
+ iter->start_time.tv_sec = 0;
+ }
+ return ret;
+}
+
+#undef fs_iter_set_async_callback
+void fs_iter_set_async_callback(struct fs_iter *iter,
+ fs_file_async_callback_t *callback,
+ void *context)
+{
+ iter->async_callback = callback;
+ iter->async_context = context;
+}
+
+bool fs_iter_have_more(struct fs_iter *iter)
+{
+ return iter->async_have_more;
+}
+
+const struct fs_stats *fs_get_stats(struct fs *fs)
+{
+ return &fs->stats;
+}
+
+void fs_set_error(struct event *event, int err, const char *fmt, ...)
+{
+ va_list args;
+
+ i_assert(err != 0);
+
+ errno = err;
+ va_start(args, fmt);
+ fs_set_verror(event, fmt, args);
+ va_end(args);
+}
+
+void fs_set_error_errno(struct event *event, const char *fmt, ...)
+{
+ va_list args;
+
+ i_assert(errno != 0);
+
+ va_start(args, fmt);
+ fs_set_verror(event, fmt, args);
+ va_end(args);
+}
+
+void fs_file_set_error_async(struct fs_file *file)
+{
+ fs_set_error(file->event, EAGAIN, "Asynchronous operation in progress");
+}
+
+static uint64_t
+fs_stats_count_ops(const struct fs_stats *stats, const enum fs_op ops[],
+ unsigned int ops_count)
+{
+ uint64_t ret = 0;
+
+ for (unsigned int i = 0; i < ops_count; i++) {
+ if (stats->timings[ops[i]] != NULL)
+ ret += stats_dist_get_sum(stats->timings[ops[i]]);
+ }
+ return ret;
+}
+
+uint64_t fs_stats_get_read_usecs(const struct fs_stats *stats)
+{
+ const enum fs_op read_ops[] = {
+ FS_OP_METADATA, FS_OP_PREFETCH, FS_OP_READ, FS_OP_EXISTS,
+ FS_OP_STAT, FS_OP_ITER
+ };
+ return fs_stats_count_ops(stats, read_ops, N_ELEMENTS(read_ops));
+}
+
+uint64_t fs_stats_get_write_usecs(const struct fs_stats *stats)
+{
+ const enum fs_op write_ops[] = {
+ FS_OP_WRITE, FS_OP_COPY, FS_OP_DELETE
+ };
+ return fs_stats_count_ops(stats, write_ops, N_ELEMENTS(write_ops));
+}
+
+struct fs_file *
+fs_file_init_parent(struct fs_file *parent, const char *path,
+ enum fs_open_mode mode, enum fs_open_flags flags)
+{
+ return fs_file_init_with_event(parent->fs->parent, parent->event,
+ path, (int)mode | (int)flags);
+}
+
+struct fs_iter *
+fs_iter_init_parent(struct fs_iter *parent,
+ const char *path, enum fs_iter_flags flags)
+{
+ return fs_iter_init_with_event(parent->fs->parent, parent->event,
+ path, flags);
+}
diff --git a/src/lib-fs/fs-api.h b/src/lib-fs/fs-api.h
new file mode 100644
index 0000000..c4918f5
--- /dev/null
+++ b/src/lib-fs/fs-api.h
@@ -0,0 +1,397 @@
+#ifndef FS_API_H
+#define FS_API_H
+
+struct stat;
+struct fs;
+struct fs_file;
+struct fs_lock;
+struct hash_method;
+
+/* Metadata with this prefix shouldn't actually be sent to storage. */
+#define FS_METADATA_INTERNAL_PREFIX ":/X-Dovecot-fs-api-"
+/* fs_write*() may return a hex-encoded object ID after write is finished.
+ This can be later on used to optimize reads by setting it before reading
+ the file. */
+#define FS_METADATA_OBJECTID FS_METADATA_INTERNAL_PREFIX"ObjectID"
+/* Calling this before fs_write_stream_finish() allows renaming the filename.
+ This can be useful if you don't know the final filename before writing it
+ (e.g. filename contains the file size). The given filename must include the
+ full path also. */
+#define FS_METADATA_WRITE_FNAME FS_METADATA_INTERNAL_PREFIX"WriteFilename"
+/* Original path of the file. The path that's eventually visible to a fs
+ backend may be something different, e.g. object ID. This allows the backend
+ to still access the original path. */
+#define FS_METADATA_ORIG_PATH FS_METADATA_INTERNAL_PREFIX"OrigPath"
+
+enum fs_properties {
+ FS_PROPERTY_METADATA = 0x01,
+ FS_PROPERTY_LOCKS = 0x02,
+ FS_PROPERTY_FASTCOPY = 0x04,
+ FS_PROPERTY_RENAME = 0x08,
+ FS_PROPERTY_STAT = 0x10,
+ /* Iteration is possible */
+ FS_PROPERTY_ITER = 0x20,
+ /* Iteration always returns all of the files (instead of possibly
+ slightly out of date view) */
+ FS_PROPERTY_RELIABLEITER= 0x40,
+ /* Backend uses directories, which aren't automatically deleted
+ when its children are deleted. */
+ FS_PROPERTY_DIRECTORIES = 0x80,
+ FS_PROPERTY_WRITE_HASH_MD5 = 0x100,
+ FS_PROPERTY_WRITE_HASH_SHA256 = 0x200,
+ /* fs_copy() will copy the metadata if fs_set_metadata() hasn't
+ been explicitly called. */
+ FS_PROPERTY_COPY_METADATA = 0x400,
+ /* Backend support asynchronous file operations. */
+ FS_PROPERTY_ASYNC = 0x800,
+ /* Backend supports FS_ITER_FLAG_OBJECTIDS. */
+ FS_PROPERTY_OBJECTIDS = 0x1000,
+ /* fs_copy() is fast even when file's metadata is changed */
+ FS_PROPERTY_FASTCOPY_CHANGED_METADATA = 0x2000,
+};
+
+enum fs_open_mode {
+ /* Open only for reading, or fail with ENOENT if it doesn't exist */
+ FS_OPEN_MODE_READONLY,
+ /* Create a new file, fail with EEXIST if it already exists */
+ FS_OPEN_MODE_CREATE,
+ /* Create a new file with a new unique name. The generated name is a
+ 128bit hex-encoded string. The fs_open()'s path parameter specifies
+ only the directory where the file is created to. */
+ FS_OPEN_MODE_CREATE_UNIQUE_128,
+ /* Create or replace a file */
+ FS_OPEN_MODE_REPLACE,
+ /* Append to existing file, fail with ENOENT if it doesn't exist */
+ FS_OPEN_MODE_APPEND
+
+#define FS_OPEN_MODE_MASK 0x0f
+};
+
+enum fs_open_flags {
+ /* File is important and writing must call fsync() or have equivalent
+ behavior. */
+ FS_OPEN_FLAG_FSYNC = 0x10,
+ /* Asynchronous writes: fs_write() will fail with EAGAIN if it needs to
+ be called again (the retries can use size=0). For streams
+ fs_write_stream_finish() may request retrying with 0.
+
+ Asynchronous reads: fs_read() will fail with EAGAIN if it's not
+ finished and fs_read_stream() returns a nonblocking stream. */
+ FS_OPEN_FLAG_ASYNC = 0x20,
+ /* fs_read_stream() must return a seekable input stream */
+ FS_OPEN_FLAG_SEEKABLE = 0x40,
+ /* Backend should handle this file's operations immediately without
+ any additional command queueing. The caller is assumed to be the one
+ doing any rate limiting if needed. This flag can only be used with
+ ASYNC flag, synchronous requests are never queued. */
+ FS_OPEN_FLAG_ASYNC_NOQUEUE = 0x80
+};
+
+enum fs_iter_flags {
+ /* Iterate only directories, not files */
+ FS_ITER_FLAG_DIRS = 0x01,
+ /* Request asynchronous iteration. */
+ FS_ITER_FLAG_ASYNC = 0x02,
+ /* Instead of returning object names, return <objectid>/<object name>.
+ If this isn't supported, the <objectid> is returned empty. The
+ object IDs are always hex-encoded data. This flag can be used only
+ if FS_PROPERTY_OBJECTIDS is enabled. */
+ FS_ITER_FLAG_OBJECTIDS = 0x04,
+ /* Explicitly disable all caching for this iteration (if anything
+ happens to be enabled). This should be used only in situations where
+ the iteration is used to fix something that is broken, e.g. doveadm
+ force-resync. */
+ FS_ITER_FLAG_NOCACHE = 0x08
+};
+
+enum fs_op {
+ FS_OP_WAIT,
+ FS_OP_METADATA,
+ FS_OP_PREFETCH,
+ FS_OP_READ,
+ FS_OP_WRITE,
+ FS_OP_LOCK,
+ FS_OP_EXISTS,
+ FS_OP_STAT,
+ FS_OP_COPY,
+ FS_OP_RENAME,
+ FS_OP_DELETE,
+ FS_OP_ITER,
+
+ FS_OP_COUNT
+};
+
+struct fs_settings {
+ /* Username and session ID are mainly used for debugging/logging,
+ but may also be useful for other purposes if they exist (they
+ may be NULL). */
+ const char *username;
+ const char *session_id;
+
+ /* Dovecot instance's base_dir */
+ const char *base_dir;
+ /* Directory where temporary files can be created at any time
+ (e.g. /tmp or mail_temp_dir) */
+ const char *temp_dir;
+ /* SSL client settings. */
+ const struct ssl_iostream_settings *ssl_client_set;
+
+ /* Automatically try to rmdir() directories up to this path when
+ deleting files. */
+ const char *root_path;
+ /* When creating temporary files, use this prefix
+ (to avoid conflicts with existing files). */
+ const char *temp_file_prefix;
+ /* If the backend needs to do DNS lookups, use this dns_client for
+ them. */
+ struct dns_client *dns_client;
+
+ /* Parent event to use, unless overridden by
+ fs_file_init_with_event() */
+ struct event *event_parent;
+
+ /* Enable debugging */
+ bool debug;
+ /* Enable timing statistics */
+ bool enable_timing;
+};
+
+struct fs_stats {
+ /* Number of fs_prefetch() calls. Counted only if fs_read*() hasn't
+ already been called for the file (which would be pretty pointless
+ to do). */
+ unsigned int prefetch_count;
+ /* Number of fs_read*() calls. Counted only if fs_prefetch() hasn't
+ already been called for the file. */
+ unsigned int read_count;
+ /* Number of fs_lookup_metadata() calls. Counted only if neither
+ fs_read*() nor fs_prefetch() has been called for the file. */
+ unsigned int lookup_metadata_count;
+ /* Number of fs_stat() calls. Counted only if none of the above
+ has been called (because the stat result should be cached). */
+ unsigned int stat_count;
+
+ /* Number of fs_write*() calls. */
+ unsigned int write_count;
+ /* Number of fs_exists() calls, which actually went to the backend
+ instead of being handled by fs_stat() call due to fs_exists() not
+ being implemented. */
+ unsigned int exists_count;
+ /* Number of fs_delete() calls. */
+ unsigned int delete_count;
+ /* Number of fs_copy() calls. If backend doesn't implement copying
+ operation but falls back to regular read+write instead, this count
+ isn't increased but the read+write counters are. */
+ unsigned int copy_count;
+ /* Number of fs_rename() calls. */
+ unsigned int rename_count;
+ /* Number of fs_iter_init() calls. */
+ unsigned int iter_count;
+
+ /* Number of bytes written by fs_write*() calls. */
+ uint64_t write_bytes;
+
+ /* Cumulative sum of usecs spent on calls - set only if
+ fs_settings.enable_timing=TRUE */
+ struct stats_dist *timings[FS_OP_COUNT];
+};
+
+struct fs_metadata {
+ const char *key;
+ const char *value;
+};
+ARRAY_DEFINE_TYPE(fs_metadata, struct fs_metadata);
+
+typedef void fs_file_async_callback_t(void *context);
+
+int fs_init(const char *driver, const char *args,
+ const struct fs_settings *set,
+ struct fs **fs_r, const char **error_r);
+/* helper for fs_init, accepts a filesystem string
+ that can come directly from config */
+int fs_init_from_string(const char *str, const struct fs_settings *set,
+ struct fs **fs_r, const char **error_r);
+/* same as fs_unref() */
+void fs_deinit(struct fs **fs);
+
+void fs_ref(struct fs *fs);
+void fs_unref(struct fs **fs);
+
+/* Returns the parent filesystem (if this is a wrapper fs) or NULL if
+ there's no parent. */
+struct fs *fs_get_parent(struct fs *fs);
+/* Returns the filesystem's driver name. */
+const char *fs_get_driver(struct fs *fs);
+/* Returns the root fs's driver name (bypassing all wrapper fses) */
+const char *fs_get_root_driver(struct fs *fs);
+
+struct fs_file *fs_file_init(struct fs *fs, const char *path, int mode_flags);
+struct fs_file *fs_file_init_with_event(struct fs *fs, struct event *event,
+ const char *path, int mode_flags);
+void fs_file_deinit(struct fs_file **file);
+
+/* Change flags for a file (and its parents). */
+void fs_file_set_flags(struct fs_file *file,
+ enum fs_open_flags add_flags,
+ enum fs_open_flags remove_flags);
+/* If the file has an input streams open, close them. */
+void fs_file_close(struct fs_file *file);
+
+/* Return properties supported by backend. */
+enum fs_properties fs_get_properties(struct fs *fs);
+
+/* Add/replace metadata when saving a file. This makes sense only when the
+ file is being created/replaced. */
+void fs_set_metadata(struct fs_file *file, const char *key, const char *value);
+/* Return file's all metadata. */
+int fs_get_metadata(struct fs_file *file,
+ const ARRAY_TYPE(fs_metadata) **metadata_r);
+/* Wrapper to fs_get_metadata() to lookup a specific key. Returns 1 if value_r
+ is set, 0 if key wasn't found, -1 if error. */
+int fs_lookup_metadata(struct fs_file *file, const char *key,
+ const char **value_r);
+/* Try to find key from the currently set metadata (without refreshing it).
+ This is typically used e.g. after writing or copying a file to find some
+ extra metadata they may have set. It can also be used after
+ fs_get_metadata() or fs_lookup_metadata() to search within the looked up
+ metadata. */
+const char *fs_lookup_loaded_metadata(struct fs_file *file, const char *key);
+
+/* Returns the path given to fs_open(). If file was opened with
+ FS_OPEN_MODE_CREATE_UNIQUE_128 and the write has already finished,
+ return the path including the generated filename. */
+const char *fs_file_path(struct fs_file *file);
+/* Returns the file's fs. */
+struct fs *fs_file_fs(struct fs_file *file);
+/* Returns the file's event. */
+struct event *fs_file_event(struct fs_file *file);
+
+/* Return the error message for the last failed file operation. Each file
+ keeps track of its own errors. For failed copy/rename operations the "dest"
+ file contains the error. */
+const char *fs_file_last_error(struct fs_file *file);
+
+/* Try to asynchronously prefetch file into memory. Returns TRUE if file is
+ already in memory (i.e. caller should handle this file before prefetching
+ more), FALSE if not. The length is a hint of how much the caller expects
+ to read, but it may be more or less (0=whole file). */
+bool fs_prefetch(struct fs_file *file, uoff_t length);
+/* Returns >0 if something was read, -1 if error (errno is set). */
+ssize_t fs_read(struct fs_file *file, void *buf, size_t size);
+/* Returns a stream for reading from file. Multiple streams can be opened,
+ and caller must destroy the streams before closing the file. */
+struct istream *fs_read_stream(struct fs_file *file, size_t max_buffer_size);
+
+/* Returns 0 if ok, -1 if error (errno is set). Note: With CREATE/REPLACE mode
+ files you can call fs_write() only once, the file creation is finished by it.
+ CREATE can return EEXIST here, if the destination file was already created.
+ With APPEND mode each fs_write() atomically appends the given data to
+ file. */
+int fs_write(struct fs_file *file, const void *data, size_t size);
+
+/* Write to file via output stream. The stream will be destroyed by
+ fs_write_stream_finish/abort. The returned ostream is already corked and
+ it doesn't need to be uncorked. */
+struct ostream *fs_write_stream(struct fs_file *file);
+/* Finish writing via stream, calling also o_stream_flush() on the stream and
+ handling any pending errors. The file will be created/replaced/appended only
+ after this call, same as with fs_write(). Anything written to the stream
+ won't be visible earlier. Returns 1 if ok, 0 if async write isn't finished
+ yet (retry calling fs_write_stream_finish_async()), -1 if error */
+int fs_write_stream_finish(struct fs_file *file, struct ostream **output);
+int fs_write_stream_finish_async(struct fs_file *file);
+/* Abort writing via stream. Anything written to the stream is discarded.
+ o_stream_ignore_last_errors() is called on the output stream so the caller
+ doesn't need to do it. This must not be called after
+ fs_write_stream_finish(), i.e. it can't be used to abort a pending async
+ write. */
+void fs_write_stream_abort_error(struct fs_file *file, struct ostream **output, const char *error_fmt, ...) ATTR_FORMAT(3, 4);
+
+/* Set a hash to the following write. The storage can then verify that the
+ input data matches the specified hash, or fail if it doesn't. Typically
+ implemented by Content-MD5 header. */
+void fs_write_set_hash(struct fs_file *file, const struct hash_method *method,
+ const void *digest);
+
+/* Call the specified callback whenever the file can be read/written to.
+ May call the callback immediately. */
+void fs_file_set_async_callback(struct fs_file *file,
+ fs_file_async_callback_t *callback,
+ void *context);
+#define fs_file_set_async_callback(file, callback, context) \
+ fs_file_set_async_callback(file, (fs_file_async_callback_t *)(callback), \
+ 1 ? (context) : \
+ CALLBACK_TYPECHECK(callback, void (*)(typeof(context))))
+/* Wait until some file can be read/written to more before returning.
+ It's an error to call this when there are no pending async operations. */
+void fs_wait_async(struct fs *fs);
+/* Switch the fs to the current ioloop. This can be used to do fs_wait_async()
+ among other IO work. Returns TRUE if there is actually some work that can
+ be waited on. */
+bool fs_switch_ioloop(struct fs *fs) ATTR_NOWARN_UNUSED_RESULT;
+
+/* Returns 1 if file exists, 0 if not, -1 if error occurred. */
+int fs_exists(struct fs_file *file);
+/* Delete a file. Returns 0 if file was actually deleted by us, -1 if error. */
+int fs_delete(struct fs_file *file);
+
+/* Returns 0 if ok, -1 if error occurred (e.g. errno=ENOENT).
+ All fs backends may not support all stat fields. */
+int fs_stat(struct fs_file *file, struct stat *st_r);
+/* Get number of links to the file. This is the same as using fs_stat()'s
+ st_nlinks field, except not all backends support returning it via fs_stat().
+ Returns 0 if ok, -1 if error occurred. */
+int fs_get_nlinks(struct fs_file *file, nlink_t *nlinks_r);
+/* Copy an object with possibly updated metadata. Destination parent
+ directories are created automatically. Returns 0 if ok, -1 if error
+ occurred. The "dest" file contains the error. */
+int fs_copy(struct fs_file *src, struct fs_file *dest);
+/* Try to finish asynchronous fs_copy(). Returns the same as fs_copy(). */
+int fs_copy_finish_async(struct fs_file *dest);
+/* Atomically rename a file. Destination parent directories are created
+ automatically. Returns 0 if ok, -1 if error occurred. The "dest" file
+ contains the error. */
+int fs_rename(struct fs_file *src, struct fs_file *dest);
+
+/* Exclusively lock a file. If file is already locked, wait for it for given
+ number of seconds (0 = fail immediately). Returns 1 if locked, 0 if wait
+ timed out, -1 if error. */
+int fs_lock(struct fs_file *file, unsigned int secs, struct fs_lock **lock_r);
+void fs_unlock(struct fs_lock **lock);
+
+/* Iterate through all files or directories in the given directory.
+ Doesn't recurse to child directories. It's not an error to iterate a
+ nonexistent directory. */
+struct fs_iter *
+fs_iter_init(struct fs *fs, const char *path, enum fs_iter_flags flags);
+struct fs_iter *
+fs_iter_init_with_event(struct fs *fs, struct event *event,
+ const char *path, enum fs_iter_flags flags);
+/* Returns 0 if ok, -1 if iteration failed. */
+int fs_iter_deinit(struct fs_iter **iter, const char **error_r);
+/* Returns the next filename. */
+const char *fs_iter_next(struct fs_iter *iter);
+
+/* For asynchronous iterations: Specify the callback that is called whenever
+ there's more data available for reading. */
+void fs_iter_set_async_callback(struct fs_iter *iter,
+ fs_file_async_callback_t *callback,
+ void *context);
+#define fs_iter_set_async_callback(iter, callback, context) \
+ fs_iter_set_async_callback(iter, (fs_file_async_callback_t *)(callback), \
+ 1 ? (context) : \
+ CALLBACK_TYPECHECK(callback, void (*)(typeof(context))))
+/* For asynchronous iterations: If fs_iter_next() returns NULL, use this
+ function to determine if you should wait for more data or finish up. */
+bool fs_iter_have_more(struct fs_iter *iter);
+
+/* Return the filesystem's fs_stats. Note that each wrapper filesystem keeps
+ track of its own fs_stats calls. You can use fs_get_parent() to get to the
+ filesystem whose stats you want to see. */
+const struct fs_stats *fs_get_stats(struct fs *fs);
+
+/* Helper functions to count number of usecs for read/write operations. */
+uint64_t fs_stats_get_read_usecs(const struct fs_stats *stats);
+uint64_t fs_stats_get_write_usecs(const struct fs_stats *stats);
+
+#endif
diff --git a/src/lib-fs/fs-dict.c b/src/lib-fs/fs-dict.c
new file mode 100644
index 0000000..936e9b6
--- /dev/null
+++ b/src/lib-fs/fs-dict.c
@@ -0,0 +1,372 @@
+/* Copyright (c) 2015-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "buffer.h"
+#include "str.h"
+#include "guid.h"
+#include "hex-binary.h"
+#include "base64.h"
+#include "istream.h"
+#include "ostream.h"
+#include "dict.h"
+#include "fs-api-private.h"
+
+enum fs_dict_value_encoding {
+ FS_DICT_VALUE_ENCODING_RAW,
+ FS_DICT_VALUE_ENCODING_HEX,
+ FS_DICT_VALUE_ENCODING_BASE64
+};
+
+struct dict_fs {
+ struct fs fs;
+ struct dict *dict;
+ char *path_prefix;
+ enum fs_dict_value_encoding encoding;
+};
+
+struct dict_fs_file {
+ struct fs_file file;
+ pool_t pool;
+ const char *key, *value;
+ buffer_t *write_buffer;
+};
+
+struct dict_fs_iter {
+ struct fs_iter iter;
+ struct dict_iterate_context *dict_iter;
+};
+
+static struct fs *fs_dict_alloc(void)
+{
+ struct dict_fs *fs;
+
+ fs = i_new(struct dict_fs, 1);
+ fs->fs = fs_class_dict;
+ return &fs->fs;
+}
+
+static int
+fs_dict_init(struct fs *_fs, const char *args, const struct fs_settings *set,
+ const char **error_r)
+{
+ struct dict_fs *fs = (struct dict_fs *)_fs;
+ struct dict_settings dict_set;
+ const char *p, *encoding_str, *error;
+
+ p = strchr(args, ':');
+ if (p == NULL) {
+ *error_r = "':' missing in args";
+ return -1;
+ }
+ encoding_str = t_strdup_until(args, p++);
+ if (strcmp(encoding_str, "raw") == 0)
+ fs->encoding = FS_DICT_VALUE_ENCODING_RAW;
+ else if (strcmp(encoding_str, "hex") == 0)
+ fs->encoding = FS_DICT_VALUE_ENCODING_HEX;
+ else if (strcmp(encoding_str, "base64") == 0)
+ fs->encoding = FS_DICT_VALUE_ENCODING_BASE64;
+ else {
+ *error_r = t_strdup_printf("Unknown value encoding '%s'",
+ encoding_str);
+ return -1;
+ }
+
+ i_zero(&dict_set);
+ dict_set.base_dir = set->base_dir;
+ dict_set.event_parent = set->event_parent;
+
+ if (dict_init(p, &dict_set, &fs->dict, &error) < 0) {
+ *error_r = t_strdup_printf("dict_init(%s) failed: %s",
+ args, error);
+ return -1;
+ }
+ return 0;
+}
+
+static void fs_dict_free(struct fs *_fs)
+{
+ struct dict_fs *fs = (struct dict_fs *)_fs;
+
+ if (fs->dict != NULL) dict_deinit(&fs->dict);
+ i_free(fs);
+}
+
+static enum fs_properties fs_dict_get_properties(struct fs *fs ATTR_UNUSED)
+{
+ return FS_PROPERTY_ITER | FS_PROPERTY_RELIABLEITER;
+}
+
+static struct fs_file *fs_dict_file_alloc(void)
+{
+ struct dict_fs_file *file;
+ pool_t pool;
+
+ pool = pool_alloconly_create("fs dict file", 128);
+ file = p_new(pool, struct dict_fs_file, 1);
+ file->pool = pool;
+ return &file->file;
+}
+
+static void
+fs_dict_file_init(struct fs_file *_file, const char *path,
+ enum fs_open_mode mode, enum fs_open_flags flags ATTR_UNUSED)
+{
+ struct dict_fs_file *file = (struct dict_fs_file *)_file;
+ struct dict_fs *fs = (struct dict_fs *)_file->fs;
+ guid_128_t guid;
+
+ i_assert(mode != FS_OPEN_MODE_APPEND); /* not supported */
+ i_assert(mode != FS_OPEN_MODE_CREATE); /* not supported */
+
+ if (mode != FS_OPEN_MODE_CREATE_UNIQUE_128)
+ file->file.path = p_strdup(file->pool, path);
+ else {
+ guid_128_generate(guid);
+ file->file.path = p_strdup_printf(file->pool, "%s/%s", path,
+ guid_128_to_string(guid));
+ }
+ file->key = fs->path_prefix == NULL ?
+ p_strdup(file->pool, file->file.path) :
+ p_strconcat(file->pool, fs->path_prefix, file->file.path, NULL);
+}
+
+static void fs_dict_file_deinit(struct fs_file *_file)
+{
+ struct dict_fs_file *file = (struct dict_fs_file *)_file;
+
+ i_assert(_file->output == NULL);
+
+ fs_file_free(_file);
+ pool_unref(&file->pool);
+}
+
+static bool fs_dict_prefetch(struct fs_file *_file ATTR_UNUSED,
+ uoff_t length ATTR_UNUSED)
+{
+ /* once async dict_lookup() is implemented, we want to start it here */
+ return TRUE;
+}
+
+static int fs_dict_lookup(struct dict_fs_file *file)
+{
+ struct dict_fs *fs = (struct dict_fs *)file->file.fs;
+ const char *error;
+ int ret;
+
+ if (file->value != NULL)
+ return 0;
+
+ struct dict_op_settings set = {
+ .username = file->file.fs->username,
+ };
+ ret = dict_lookup(fs->dict, &set, file->pool, file->key, &file->value, &error);
+ if (ret > 0)
+ return 0;
+ else if (ret < 0) {
+ fs_set_error(file->file.event, EIO,
+ "dict_lookup(%s) failed: %s", file->key, error);
+ return -1;
+ } else {
+ fs_set_error(file->file.event, ENOENT,
+ "Dict key %s doesn't exist", file->key);
+ return -1;
+ }
+}
+
+static struct istream *
+fs_dict_read_stream(struct fs_file *_file, size_t max_buffer_size ATTR_UNUSED)
+{
+ struct dict_fs_file *file = (struct dict_fs_file *)_file;
+ struct istream *input;
+
+ if (fs_dict_lookup(file) < 0)
+ input = i_stream_create_error_str(errno, "%s", fs_file_last_error(_file));
+ else
+ input = i_stream_create_from_data(file->value, strlen(file->value));
+ i_stream_set_name(input, file->key);
+ return input;
+}
+
+static void fs_dict_write_stream(struct fs_file *_file)
+{
+ struct dict_fs_file *file = (struct dict_fs_file *)_file;
+
+ i_assert(_file->output == NULL);
+
+ file->write_buffer = buffer_create_dynamic(file->pool, 128);
+ _file->output = o_stream_create_buffer(file->write_buffer);
+ o_stream_set_name(_file->output, file->key);
+}
+
+static void fs_dict_write_rename_if_needed(struct dict_fs_file *file)
+{
+ struct dict_fs *fs = (struct dict_fs *)file->file.fs;
+ const char *new_fname;
+
+ new_fname = fs_metadata_find(&file->file.metadata, FS_METADATA_WRITE_FNAME);
+ if (new_fname == NULL)
+ return;
+
+ file->file.path = p_strdup(file->pool, new_fname);
+ file->key = fs->path_prefix == NULL ? p_strdup(file->pool, new_fname) :
+ p_strconcat(file->pool, fs->path_prefix, new_fname, NULL);
+}
+
+static int fs_dict_write_stream_finish(struct fs_file *_file, bool success)
+{
+ struct dict_fs_file *file = (struct dict_fs_file *)_file;
+ struct dict_fs *fs = (struct dict_fs *)_file->fs;
+ struct dict_transaction_context *trans;
+ const char *error;
+
+ o_stream_destroy(&_file->output);
+ if (!success)
+ return -1;
+
+ struct dict_op_settings set = {
+ .username = _file->fs->username,
+ };
+ fs_dict_write_rename_if_needed(file);
+ trans = dict_transaction_begin(fs->dict, &set);
+ switch (fs->encoding) {
+ case FS_DICT_VALUE_ENCODING_RAW:
+ dict_set(trans, file->key, str_c(file->write_buffer));
+ break;
+ case FS_DICT_VALUE_ENCODING_HEX: {
+ string_t *hex = t_str_new(file->write_buffer->used * 2 + 1);
+ binary_to_hex_append(hex, file->write_buffer->data,
+ file->write_buffer->used);
+ dict_set(trans, file->key, str_c(hex));
+ break;
+ }
+ case FS_DICT_VALUE_ENCODING_BASE64: {
+ const size_t base64_size =
+ MAX_BASE64_ENCODED_SIZE(file->write_buffer->used);
+ string_t *base64 = t_str_new(base64_size);
+ base64_encode(file->write_buffer->data,
+ file->write_buffer->used, base64);
+ dict_set(trans, file->key, str_c(base64));
+ }
+ }
+ if (dict_transaction_commit(&trans, &error) < 0) {
+ fs_set_error(_file->event, EIO,
+ "Dict transaction commit failed: %s", error);
+ return -1;
+ }
+ return 1;
+}
+
+static int fs_dict_stat(struct fs_file *_file, struct stat *st_r)
+{
+ struct dict_fs_file *file = (struct dict_fs_file *)_file;
+
+ i_zero(st_r);
+
+ if (fs_dict_lookup(file) < 0)
+ return -1;
+ st_r->st_size = strlen(file->value);
+ return 0;
+}
+
+static int fs_dict_delete(struct fs_file *_file)
+{
+ struct dict_fs_file *file = (struct dict_fs_file *)_file;
+ struct dict_fs *fs = (struct dict_fs *)_file->fs;
+ struct dict_transaction_context *trans;
+ const char *error;
+
+ struct dict_op_settings set = {
+ .username = fs->fs.username,
+ };
+ trans = dict_transaction_begin(fs->dict, &set);
+ dict_unset(trans, file->key);
+ if (dict_transaction_commit(&trans, &error) < 0) {
+ fs_set_error(_file->event, EIO,
+ "Dict transaction commit failed: %s", error);
+ return -1;
+ }
+ return 0;
+}
+
+static struct fs_iter *fs_dict_iter_alloc(void)
+{
+ struct dict_fs_iter *iter = i_new(struct dict_fs_iter, 1);
+ return &iter->iter;
+}
+
+static void
+fs_dict_iter_init(struct fs_iter *_iter, const char *path,
+ enum fs_iter_flags flags ATTR_UNUSED)
+{
+ struct dict_fs_iter *iter = (struct dict_fs_iter *)_iter;
+ struct dict_fs *fs = (struct dict_fs *)_iter->fs;
+
+ if (fs->path_prefix != NULL)
+ path = t_strconcat(fs->path_prefix, path, NULL);
+
+ struct dict_op_settings set = {
+ .username = iter->iter.fs->username,
+ };
+ iter->dict_iter = dict_iterate_init(fs->dict, &set, path, 0);
+}
+
+static const char *fs_dict_iter_next(struct fs_iter *_iter)
+{
+ struct dict_fs_iter *iter = (struct dict_fs_iter *)_iter;
+ const char *key, *value;
+
+ if (!dict_iterate(iter->dict_iter, &key, &value))
+ return NULL;
+ return key;
+}
+
+static int fs_dict_iter_deinit(struct fs_iter *_iter)
+{
+ struct dict_fs_iter *iter = (struct dict_fs_iter *)_iter;
+ const char *error;
+ int ret;
+
+ ret = dict_iterate_deinit(&iter->dict_iter, &error);
+ if (ret < 0)
+ fs_set_error(_iter->event, EIO,
+ "Dict iteration failed: %s", error);
+ return ret;
+}
+
+const struct fs fs_class_dict = {
+ .name = "dict",
+ .v = {
+ fs_dict_alloc,
+ fs_dict_init,
+ NULL,
+ fs_dict_free,
+ fs_dict_get_properties,
+ fs_dict_file_alloc,
+ fs_dict_file_init,
+ fs_dict_file_deinit,
+ NULL,
+ NULL,
+ NULL, NULL,
+ fs_default_set_metadata,
+ NULL,
+ fs_dict_prefetch,
+ NULL,
+ fs_dict_read_stream,
+ NULL,
+ fs_dict_write_stream,
+ fs_dict_write_stream_finish,
+ NULL,
+ NULL,
+ NULL,
+ fs_dict_stat,
+ fs_default_copy,
+ NULL,
+ fs_dict_delete,
+ fs_dict_iter_alloc,
+ fs_dict_iter_init,
+ fs_dict_iter_next,
+ fs_dict_iter_deinit,
+ NULL,
+ NULL
+ }
+};
diff --git a/src/lib-fs/fs-metawrap.c b/src/lib-fs/fs-metawrap.c
new file mode 100644
index 0000000..22517e7
--- /dev/null
+++ b/src/lib-fs/fs-metawrap.c
@@ -0,0 +1,526 @@
+/* Copyright (c) 2013-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "str.h"
+#include "strescape.h"
+#include "istream.h"
+#include "istream-private.h"
+#include "istream-concat.h"
+#include "istream-metawrap.h"
+#include "ostream.h"
+#include "ostream-metawrap.h"
+#include "iostream-temp.h"
+#include "fs-api-private.h"
+
+struct metawrap_fs {
+ struct fs fs;
+ bool wrap_metadata;
+};
+
+struct metawrap_fs_file {
+ struct fs_file file;
+ struct metawrap_fs *fs;
+ struct fs_file *super_read;
+ enum fs_open_mode open_mode;
+ struct istream *input;
+ bool metadata_read;
+
+ struct ostream *super_output;
+ struct ostream *temp_output;
+ string_t *metadata_header;
+ uoff_t metadata_write_size;
+ bool metadata_changed_since_write;
+};
+
+#define METAWRAP_FS(ptr) container_of((ptr), struct metawrap_fs, fs)
+#define METAWRAP_FILE(ptr) container_of((ptr), struct metawrap_fs_file, file)
+
+static struct fs *fs_metawrap_alloc(void)
+{
+ struct metawrap_fs *fs;
+
+ fs = i_new(struct metawrap_fs, 1);
+ fs->fs = fs_class_metawrap;
+ return &fs->fs;
+}
+
+static int
+fs_metawrap_init(struct fs *_fs, const char *args,
+ const struct fs_settings *set, const char **error_r)
+{
+ struct metawrap_fs *fs = METAWRAP_FS(_fs);
+ const char *parent_name, *parent_args;
+
+ if (*args == '\0') {
+ *error_r = "Parent filesystem not given as parameter";
+ return -1;
+ }
+
+ parent_args = strchr(args, ':');
+ if (parent_args == NULL) {
+ parent_name = args;
+ parent_args = "";
+ } else {
+ parent_name = t_strdup_until(args, parent_args);
+ parent_args++;
+ }
+ if (fs_init(parent_name, parent_args, set, &_fs->parent, error_r) < 0)
+ return -1;
+ if ((fs_get_properties(_fs->parent) & FS_PROPERTY_METADATA) == 0)
+ fs->wrap_metadata = TRUE;
+ return 0;
+}
+
+static void fs_metawrap_free(struct fs *_fs)
+{
+ struct metawrap_fs *fs = METAWRAP_FS(_fs);
+
+ i_free(fs);
+}
+
+static enum fs_properties fs_metawrap_get_properties(struct fs *_fs)
+{
+ const struct metawrap_fs *fs = METAWRAP_FS(_fs);
+ enum fs_properties props;
+
+ props = fs_get_properties(_fs->parent);
+ if (fs->wrap_metadata) {
+ /* we don't have a quick stat() to see the file's size,
+ because of the metadata header */
+ props &= ENUM_NEGATE(FS_PROPERTY_STAT);
+ /* Copying can copy the whole metadata. */
+ props |= FS_PROPERTY_COPY_METADATA;
+ }
+ return props;
+}
+
+static struct fs_file *fs_metawrap_file_alloc(void)
+{
+ struct metawrap_fs_file *file = i_new(struct metawrap_fs_file, 1);
+ return &file->file;
+}
+
+static void
+fs_metawrap_file_init(struct fs_file *_file, const char *path,
+ enum fs_open_mode mode, enum fs_open_flags flags)
+{
+ struct metawrap_fs_file *file = METAWRAP_FILE(_file);
+ struct metawrap_fs *fs = METAWRAP_FS(_file->fs);
+
+ file->file.path = i_strdup(path);
+ file->fs = fs;
+ file->open_mode = mode;
+
+ /* avoid unnecessarily creating two seekable streams */
+ flags &= ENUM_NEGATE(FS_OPEN_FLAG_SEEKABLE);
+
+ file->file.parent = fs_file_init_parent(_file, path, mode, flags);
+ if (file->fs->wrap_metadata && mode == FS_OPEN_MODE_READONLY &&
+ (flags & FS_OPEN_FLAG_ASYNC) == 0) {
+ /* use async stream for parent, so fs_read_stream() won't create
+ another seekable stream needlessly */
+ file->super_read = fs_file_init_parent(_file, path,
+ mode, flags | FS_OPEN_FLAG_ASYNC |
+ FS_OPEN_FLAG_ASYNC_NOQUEUE);
+ } else {
+ file->super_read = file->file.parent;
+ }
+ fs_metadata_init(&file->file);
+}
+
+static void fs_metawrap_file_deinit(struct fs_file *_file)
+{
+ struct metawrap_fs_file *file = METAWRAP_FILE(_file);
+
+ if (file->super_read != _file->parent)
+ fs_file_deinit(&file->super_read);
+ str_free(&file->metadata_header);
+ fs_file_free(_file);
+ i_free(file->file.path);
+ i_free(file);
+}
+
+static void fs_metawrap_file_close(struct fs_file *_file)
+{
+ struct metawrap_fs_file *file = METAWRAP_FILE(_file);
+
+ i_stream_unref(&file->input);
+ fs_file_close(file->super_read);
+ fs_file_close(_file->parent);
+}
+
+static void
+fs_metawrap_set_metadata(struct fs_file *_file, const char *key,
+ const char *value)
+{
+ struct metawrap_fs_file *file = METAWRAP_FILE(_file);
+
+ if (!file->fs->wrap_metadata ||
+ strcmp(key, FS_METADATA_WRITE_FNAME) == 0)
+ fs_set_metadata(_file->parent, key, value);
+ else {
+ fs_default_set_metadata(_file, key, value);
+ file->metadata_changed_since_write = TRUE;
+ }
+}
+
+static int
+fs_metawrap_get_metadata(struct fs_file *_file,
+ enum fs_get_metadata_flags flags,
+ const ARRAY_TYPE(fs_metadata) **metadata_r)
+{
+ struct metawrap_fs_file *file = METAWRAP_FILE(_file);
+ ssize_t ret;
+ char c;
+
+ if (!file->fs->wrap_metadata)
+ return fs_get_metadata_full(_file->parent, flags, metadata_r);
+
+ if (file->metadata_read) {
+ /* we have the metadata */
+ } else if ((flags & FS_GET_METADATA_FLAG_LOADED_ONLY) != 0) {
+ /* use the existing metadata only */
+ } else if (file->input == NULL) {
+ if (fs_read(_file, &c, 1) < 0)
+ return -1;
+ } else {
+ /* use the existing istream to read it */
+ while ((ret = i_stream_read(file->input)) == 0) {
+ if (file->metadata_read)
+ break;
+
+ i_assert(!file->input->blocking);
+ fs_wait_async(_file->fs);
+ }
+ if (ret == -1 && file->input->stream_errno != 0) {
+ fs_set_error(_file->event, file->input->stream_errno,
+ "read(%s) failed: %s",
+ i_stream_get_name(file->input),
+ i_stream_get_error(file->input));
+ return -1;
+ }
+ i_assert(file->metadata_read);
+ }
+ *metadata_r = &_file->metadata;
+ return 0;
+}
+
+static bool fs_metawrap_prefetch(struct fs_file *_file, uoff_t length)
+{
+ struct metawrap_fs_file *file = METAWRAP_FILE(_file);
+
+ if (!file->fs->wrap_metadata)
+ return fs_prefetch(_file->parent, length);
+ else
+ return fs_prefetch(file->super_read, length);
+}
+
+static ssize_t fs_metawrap_read(struct fs_file *_file, void *buf, size_t size)
+{
+ struct metawrap_fs_file *file = METAWRAP_FILE(_file);
+
+ if (!file->fs->wrap_metadata)
+ return fs_read(_file->parent, buf, size);
+ return fs_read_via_stream(_file, buf, size);
+}
+
+static void
+fs_metawrap_callback(const char *key, const char *value, void *context)
+{
+ struct metawrap_fs_file *file = context;
+
+ if (key == NULL) {
+ file->metadata_read = TRUE;
+ return;
+ }
+
+ T_BEGIN {
+ key = str_tabunescape(t_strdup_noconst(key));
+ value = str_tabunescape(t_strdup_noconst(value));
+ fs_default_set_metadata(&file->file, key, value);
+ } T_END;
+}
+
+static struct istream *
+fs_metawrap_read_stream(struct fs_file *_file, size_t max_buffer_size)
+{
+ struct metawrap_fs_file *file = (struct metawrap_fs_file *)_file;
+ struct istream *input;
+
+ if (!file->fs->wrap_metadata)
+ return fs_read_stream(_file->parent, max_buffer_size);
+
+ if (file->input != NULL) {
+ i_stream_ref(file->input);
+ i_stream_seek(file->input, 0);
+ return file->input;
+ }
+
+ input = fs_read_stream(file->super_read, max_buffer_size);
+ file->input = i_stream_create_metawrap(input, fs_metawrap_callback, file);
+ i_stream_unref(&input);
+ i_stream_ref(file->input);
+ return file->input;
+}
+
+static int fs_metawrap_write(struct fs_file *_file, const void *data, size_t size)
+{
+ struct metawrap_fs_file *file = METAWRAP_FILE(_file);
+
+ if (!file->fs->wrap_metadata)
+ return fs_write(_file->parent, data, size);
+ return fs_write_via_stream(_file, data, size);
+}
+
+static void
+fs_metawrap_append_metadata(struct metawrap_fs_file *file, string_t *str)
+{
+ const struct fs_metadata *metadata;
+
+ array_foreach(&file->file.metadata, metadata) {
+ if (str_begins(metadata->key, FS_METADATA_INTERNAL_PREFIX))
+ continue;
+
+ str_append_tabescaped(str, metadata->key);
+ str_append_c(str, ':');
+ str_append_tabescaped(str, metadata->value);
+ str_append_c(str, '\n');
+ }
+ str_append_c(str, '\n');
+}
+
+static void
+fs_metawrap_write_metadata_to(struct metawrap_fs_file *file,
+ struct ostream *output)
+{
+ string_t *str = t_str_new(256);
+ ssize_t ret;
+
+ fs_metawrap_append_metadata(file, str);
+ file->metadata_write_size = str_len(str);
+
+ ret = o_stream_send(output, str_data(str), str_len(str));
+ if (ret < 0)
+ o_stream_close(output);
+ else
+ i_assert((size_t)ret == str_len(str));
+ file->metadata_changed_since_write = FALSE;
+}
+
+static void fs_metawrap_write_metadata(void *context)
+{
+ struct metawrap_fs_file *file = context;
+
+ fs_metawrap_write_metadata_to(file, file->file.output);
+}
+
+static void fs_metawrap_write_stream(struct fs_file *_file)
+{
+ struct metawrap_fs_file *file = (struct metawrap_fs_file *)_file;
+
+ i_assert(_file->output == NULL);
+
+ if (!file->fs->wrap_metadata) {
+ file->super_output = fs_write_stream(_file->parent);
+ _file->output = file->super_output;
+ } else {
+ file->temp_output =
+ iostream_temp_create_named(_file->fs->temp_path_prefix,
+ IOSTREAM_TEMP_FLAG_TRY_FD_DUP,
+ fs_file_path(_file));
+ _file->output = o_stream_create_metawrap(file->temp_output,
+ fs_metawrap_write_metadata, file);
+ }
+}
+
+static struct istream *
+fs_metawrap_create_updated_istream(struct metawrap_fs_file *file,
+ struct istream *input)
+{
+ struct istream *input2, *inputs[3];
+
+ if (file->metadata_header != NULL)
+ str_truncate(file->metadata_header, 0);
+ else
+ file->metadata_header = str_new(default_pool, 1024);
+ fs_metawrap_append_metadata(file, file->metadata_header);
+ inputs[0] = i_stream_create_from_data(str_data(file->metadata_header),
+ str_len(file->metadata_header));
+
+ i_stream_seek(input, file->metadata_write_size);
+ inputs[1] = i_stream_create_limit(input, UOFF_T_MAX);
+ inputs[2] = NULL;
+ input2 = i_stream_create_concat(inputs);
+ i_stream_unref(&inputs[0]);
+ i_stream_unref(&inputs[1]);
+
+ file->metadata_write_size = str_len(file->metadata_header);
+ return input2;
+}
+
+static int fs_metawrap_write_stream_finish(struct fs_file *_file, bool success)
+{
+ struct metawrap_fs_file *file = METAWRAP_FILE(_file);
+ struct istream *input;
+ int ret;
+
+ if (_file->output != NULL) {
+ if (_file->output == file->super_output)
+ _file->output = NULL;
+ else
+ o_stream_unref(&_file->output);
+ }
+ if (!success) {
+ if (file->super_output != NULL) {
+ /* no metawrap */
+ i_assert(file->temp_output == NULL);
+ fs_write_stream_abort_parent(_file, &file->super_output);
+ } else {
+ i_assert(file->temp_output != NULL);
+ o_stream_destroy(&file->temp_output);
+ }
+ return -1;
+ }
+
+ if (file->super_output != NULL) {
+ /* no metawrap */
+ i_assert(file->temp_output == NULL);
+ return fs_write_stream_finish(_file->parent, &file->super_output);
+ }
+ if (file->temp_output == NULL) {
+ /* finishing up */
+ i_assert(file->super_output == NULL);
+ return fs_write_stream_finish_async(_file->parent);
+ }
+ /* finish writing the temporary file */
+ if (file->temp_output->offset == 0) {
+ /* empty file - temp_output is already finished,
+ so we can't write to it. To make sure metadata is still
+ appended/written to file use metadata_changed_since_write */
+ file->metadata_changed_since_write = TRUE;
+ }
+ input = iostream_temp_finish(&file->temp_output, IO_BLOCK_SIZE);
+ if (file->metadata_changed_since_write) {
+ /* we'll need to recreate the metadata. do this by creating a
+ new istream combining the new metadata header and the
+ old body. */
+ struct istream *input2 = input;
+
+ input = fs_metawrap_create_updated_istream(file, input);
+ i_stream_unref(&input2);
+ }
+ file->super_output = fs_write_stream(_file->parent);
+ o_stream_nsend_istream(file->super_output, input);
+ /* because of the "end of metadata" LF, there's always at least
+ 1 byte */
+ i_assert(file->super_output->offset > 0 ||
+ file->super_output->stream_errno != 0);
+ ret = fs_write_stream_finish(_file->parent, &file->super_output);
+ i_stream_unref(&input);
+ return ret;
+}
+
+static int fs_metawrap_stat(struct fs_file *_file, struct stat *st_r)
+{
+ struct metawrap_fs_file *file = METAWRAP_FILE(_file);
+ struct istream *input;
+ uoff_t input_size;
+ ssize_t ret;
+
+ if (!file->fs->wrap_metadata)
+ return fs_stat(_file->parent, st_r);
+
+ if (file->metadata_write_size != 0) {
+ /* fs_stat() after a write. we can do this quickly. */
+ if (fs_stat(_file->parent, st_r) < 0)
+ return -1;
+ if ((uoff_t)st_r->st_size < file->metadata_write_size) {
+ fs_set_error(_file->event, EIO,
+ "Just-written %s shrank unexpectedly "
+ "(%"PRIuUOFF_T" < %"PRIuUOFF_T")",
+ fs_file_path(_file), st_r->st_size,
+ file->metadata_write_size);
+ return -1;
+ }
+ st_r->st_size -= file->metadata_write_size;
+ return 0;
+ }
+
+ if (file->input == NULL)
+ input = fs_read_stream(_file, IO_BLOCK_SIZE);
+ else {
+ input = file->input;
+ i_stream_ref(input);
+ }
+ if ((ret = i_stream_get_size(input, TRUE, &input_size)) < 0) {
+ fs_set_error(_file->event, input->stream_errno,
+ "i_stream_get_size(%s) failed: %s",
+ fs_file_path(_file), i_stream_get_error(input));
+ i_stream_unref(&input);
+ return -1;
+ }
+ i_stream_unref(&input);
+ if (ret == 0) {
+ /* we shouldn't get here */
+ fs_set_error(_file->event, EIO, "i_stream_get_size(%s) returned size as unknown",
+ fs_file_path(_file));
+ return -1;
+ }
+
+ if (fs_stat(_file->parent, st_r) < 0) {
+ i_assert(errno != EAGAIN); /* read should have caught this */
+ return -1;
+ }
+ st_r->st_size = input_size;
+ return 0;
+}
+
+static int fs_metawrap_copy(struct fs_file *_src, struct fs_file *_dest)
+{
+ struct metawrap_fs_file *dest = METAWRAP_FILE(_dest);
+
+ if (!dest->fs->wrap_metadata || !_dest->metadata_changed)
+ return fs_wrapper_copy(_src, _dest);
+ else
+ return fs_default_copy(_src, _dest);
+}
+
+const struct fs fs_class_metawrap = {
+ .name = "metawrap",
+ .v = {
+ fs_metawrap_alloc,
+ fs_metawrap_init,
+ NULL,
+ fs_metawrap_free,
+ fs_metawrap_get_properties,
+ fs_metawrap_file_alloc,
+ fs_metawrap_file_init,
+ fs_metawrap_file_deinit,
+ fs_metawrap_file_close,
+ fs_wrapper_file_get_path,
+ fs_wrapper_set_async_callback,
+ fs_wrapper_wait_async,
+ fs_metawrap_set_metadata,
+ fs_metawrap_get_metadata,
+ fs_metawrap_prefetch,
+ fs_metawrap_read,
+ fs_metawrap_read_stream,
+ fs_metawrap_write,
+ fs_metawrap_write_stream,
+ fs_metawrap_write_stream_finish,
+ fs_wrapper_lock,
+ fs_wrapper_unlock,
+ fs_wrapper_exists,
+ fs_metawrap_stat,
+ fs_metawrap_copy,
+ fs_wrapper_rename,
+ fs_wrapper_delete,
+ fs_wrapper_iter_alloc,
+ fs_wrapper_iter_init,
+ fs_wrapper_iter_next,
+ fs_wrapper_iter_deinit,
+ NULL,
+ fs_wrapper_get_nlinks,
+ }
+};
diff --git a/src/lib-fs/fs-posix.c b/src/lib-fs/fs-posix.c
new file mode 100644
index 0000000..657938e
--- /dev/null
+++ b/src/lib-fs/fs-posix.c
@@ -0,0 +1,1028 @@
+/* Copyright (c) 2010-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "buffer.h"
+#include "str.h"
+#include "guid.h"
+#include "istream.h"
+#include "ostream.h"
+#include "safe-mkstemp.h"
+#include "mkdir-parents.h"
+#include "write-full.h"
+#include "file-lock.h"
+#include "file-dotlock.h"
+#include "time-util.h"
+#include "fs-api-private.h"
+
+#include <stdio.h>
+#include <unistd.h>
+#include <dirent.h>
+#include <fcntl.h>
+#include <sys/stat.h>
+
+#define FS_POSIX_DOTLOCK_STALE_TIMEOUT_SECS (60*10)
+#define MAX_MKDIR_RETRY_COUNT 5
+#define FS_DEFAULT_MODE 0600
+
+enum fs_posix_lock_method {
+ FS_POSIX_LOCK_METHOD_FLOCK,
+ FS_POSIX_LOCK_METHOD_DOTLOCK
+};
+
+struct posix_fs {
+ struct fs fs;
+ char *temp_file_prefix, *root_path, *path_prefix;
+ size_t temp_file_prefix_len;
+ enum fs_posix_lock_method lock_method;
+ mode_t mode;
+ bool mode_auto;
+ bool have_dirs;
+ bool disable_fsync;
+ bool accurate_mtime;
+};
+
+struct posix_fs_file {
+ struct fs_file file;
+ char *temp_path, *full_path;
+ int fd;
+ enum fs_open_mode open_mode;
+ enum fs_open_flags open_flags;
+
+ buffer_t *write_buf;
+
+ bool seek_to_beginning;
+};
+
+struct posix_fs_lock {
+ struct fs_lock lock;
+ struct file_lock *file_lock;
+ struct dotlock *dotlock;
+};
+
+struct posix_fs_iter {
+ struct fs_iter iter;
+ char *path;
+ DIR *dir;
+ int err;
+};
+
+static struct fs *fs_posix_alloc(void)
+{
+ struct posix_fs *fs;
+
+ fs = i_new(struct posix_fs, 1);
+ fs->fs = fs_class_posix;
+ return &fs->fs;
+}
+
+static int
+fs_posix_init(struct fs *_fs, const char *args, const struct fs_settings *set,
+ const char **error_r)
+{
+ struct posix_fs *fs = container_of(_fs, struct posix_fs, fs);
+ const char *const *tmp;
+
+ fs->temp_file_prefix = set->temp_file_prefix != NULL ?
+ i_strdup(set->temp_file_prefix) : i_strdup("temp.dovecot.");
+ fs->temp_file_prefix_len = strlen(fs->temp_file_prefix);
+ fs->root_path = i_strdup(set->root_path);
+ fs->fs.set.temp_file_prefix = fs->temp_file_prefix;
+ fs->fs.set.root_path = fs->root_path;
+
+ fs->lock_method = FS_POSIX_LOCK_METHOD_FLOCK;
+ fs->mode = FS_DEFAULT_MODE;
+
+ tmp = t_strsplit_spaces(args, ":");
+ for (; *tmp != NULL; tmp++) {
+ const char *arg = *tmp;
+
+ if (strcmp(arg, "lock=flock") == 0)
+ fs->lock_method = FS_POSIX_LOCK_METHOD_FLOCK;
+ else if (strcmp(arg, "lock=dotlock") == 0)
+ fs->lock_method = FS_POSIX_LOCK_METHOD_DOTLOCK;
+ else if (str_begins(arg, "prefix=")) {
+ i_free(fs->path_prefix);
+ fs->path_prefix = i_strdup(arg + 7);
+ } else if (strcmp(arg, "mode=auto") == 0) {
+ fs->mode_auto = TRUE;
+ } else if (strcmp(arg, "dirs") == 0) {
+ fs->have_dirs = TRUE;
+ } else if (strcmp(arg, "no-fsync") == 0) {
+ fs->disable_fsync = TRUE;
+ } else if (strcmp(arg, "accurate-mtime") == 0) {
+ fs->accurate_mtime = TRUE;
+ } else if (str_begins(arg, "mode=")) {
+ unsigned int mode;
+ if (str_to_uint_oct(arg+5, &mode) < 0) {
+ *error_r = t_strdup_printf("Invalid mode value: %s", arg+5);
+ return -1;
+ }
+ fs->mode = mode & 0666;
+ if (fs->mode == 0) {
+ *error_r = t_strdup_printf("Invalid mode: %s", arg+5);
+ return -1;
+ }
+ } else {
+ *error_r = t_strdup_printf("Unknown arg '%s'", arg);
+ return -1;
+ }
+ }
+ return 0;
+}
+
+static void fs_posix_free(struct fs *_fs)
+{
+ struct posix_fs *fs = container_of(_fs, struct posix_fs, fs);
+
+ i_free(fs->temp_file_prefix);
+ i_free(fs->root_path);
+ i_free(fs->path_prefix);
+ i_free(fs);
+}
+
+static enum fs_properties fs_posix_get_properties(struct fs *_fs)
+{
+ struct posix_fs *fs = container_of(_fs, struct posix_fs, fs);
+ enum fs_properties props =
+ FS_PROPERTY_LOCKS | FS_PROPERTY_FASTCOPY | FS_PROPERTY_RENAME |
+ FS_PROPERTY_STAT | FS_PROPERTY_ITER | FS_PROPERTY_RELIABLEITER;
+
+ /* FS_PROPERTY_DIRECTORIES is not returned normally because fs_delete()
+ automatically rmdir()s parents. For backwards compatibility
+ (especially with SIS code) we'll do it that way, but optionally with
+ "dirs" parameter enable them. This is especially important to be
+ able to use doveadm fs commands to delete empty directories. */
+ if (fs->have_dirs)
+ props |= FS_PROPERTY_DIRECTORIES;
+ return props;
+}
+
+static int
+fs_posix_get_mode(struct posix_fs_file *file, const char *path, mode_t *mode_r)
+{
+ struct posix_fs *fs = (struct posix_fs *)file->file.fs;
+ struct stat st;
+ const char *p;
+
+ *mode_r = fs->mode;
+
+ /* This function is used to get mode of the parent directory, so path
+ is never the same as file->path. The file is used just to set the
+ errors. */
+ while (stat(path, &st) < 0) {
+ if (errno != ENOENT) {
+ fs_set_error_errno(file->file.event,
+ "stat(%s) failed: %m", path);
+ return -1;
+ }
+ p = strrchr(path, '/');
+ if (p != NULL)
+ path = t_strdup_until(path, p);
+ else if (strcmp(path, ".") != 0)
+ path = ".";
+ else
+ return 0;
+ }
+ if ((st.st_mode & S_ISGID) != 0) {
+ /* setgid set - copy mode from parent */
+ *mode_r = st.st_mode & 0666;
+ }
+ return 0;
+}
+
+static int fs_posix_mkdir_parents(struct posix_fs_file *file, const char *path)
+{
+ const char *dir, *fname;
+ mode_t mode, dir_mode;
+
+ fname = strrchr(path, '/');
+ if (fname == NULL)
+ return 1;
+ dir = t_strdup_until(path, fname);
+
+ if (fs_posix_get_mode(file, dir, &mode) < 0)
+ return -1;
+ dir_mode = mode;
+ if ((dir_mode & 0600) != 0) dir_mode |= 0100;
+ if ((dir_mode & 0060) != 0) dir_mode |= 0010;
+ if ((dir_mode & 0006) != 0) dir_mode |= 0001;
+
+ if (mkdir_parents(dir, dir_mode) == 0)
+ return 0;
+ else if (errno == EEXIST)
+ return 1;
+ else {
+ fs_set_error_errno(file->file.event,
+ "mkdir_parents(%s) failed: %m", dir);
+ return -1;
+ }
+}
+
+static int fs_posix_rmdir_parents(struct posix_fs_file *file, const char *path)
+{
+ struct posix_fs *fs = (struct posix_fs *)file->file.fs;
+ const char *p;
+
+ if (fs->have_dirs)
+ return 0;
+ if (fs->root_path == NULL && fs->path_prefix == NULL)
+ return 0;
+
+ while ((p = strrchr(path, '/')) != NULL) {
+ path = t_strdup_until(path, p);
+ if ((fs->root_path != NULL && strcmp(path, fs->root_path) == 0) ||
+ (fs->path_prefix != NULL && str_begins(fs->path_prefix, path)))
+ break;
+ if (rmdir(path) == 0) {
+ /* success, continue to parent */
+ } else if (errno == ENOTEMPTY || errno == EEXIST) {
+ /* there are other entries in this directory */
+ break;
+ } else if (errno == EBUSY || errno == ENOENT) {
+ /* some other not-unexpected error */
+ break;
+ } else {
+ fs_set_error_errno(file->file.event,
+ "rmdir(%s) failed: %m", path);
+ return -1;
+ }
+ }
+ return 0;
+}
+
+static int fs_posix_create(struct posix_fs_file *file)
+{
+ struct posix_fs *fs = container_of(file->file.fs, struct posix_fs, fs);
+ string_t *str = t_str_new(256);
+ const char *slash;
+ unsigned int try_count = 0;
+ mode_t mode;
+ int fd;
+
+ i_assert(file->temp_path == NULL);
+
+ if ((slash = strrchr(file->full_path, '/')) != NULL) {
+ str_append_data(str, file->full_path, slash - file->full_path);
+ if (fs_posix_get_mode(file, str_c(str), &mode) < 0)
+ return -1;
+ str_append_c(str, '/');
+ } else {
+ if (fs_posix_get_mode(file, ".", &mode) < 0)
+ return -1;
+ }
+ str_append(str, fs->temp_file_prefix);
+
+ fd = safe_mkstemp_hostpid(str, mode, (uid_t)-1, (gid_t)-1);
+ while (fd == -1 && errno == ENOENT &&
+ try_count <= MAX_MKDIR_RETRY_COUNT) {
+ if (fs_posix_mkdir_parents(file, str_c(str)) < 0)
+ return -1;
+ fd = safe_mkstemp_hostpid(str, mode, (uid_t)-1, (gid_t)-1);
+ try_count++;
+ }
+ if (fd == -1) {
+ fs_set_error_errno(file->file.event,
+ "safe_mkstemp(%s) failed: %m", str_c(str));
+ return -1;
+ }
+ file->temp_path = i_strdup(str_c(str));
+ return fd;
+}
+
+static int fs_posix_open(struct posix_fs_file *file)
+{
+ const char *path = file->full_path;
+
+ i_assert(file->fd == -1);
+
+ switch (file->open_mode) {
+ case FS_OPEN_MODE_READONLY:
+ file->fd = open(path, O_RDONLY);
+ if (file->fd == -1)
+ fs_set_error_errno(file->file.event,
+ "open(%s) failed: %m", path);
+ break;
+ case FS_OPEN_MODE_APPEND:
+ file->fd = open(path, O_RDWR | O_APPEND);
+ if (file->fd == -1)
+ fs_set_error_errno(file->file.event,
+ "open(%s) failed: %m", path);
+ break;
+ case FS_OPEN_MODE_CREATE_UNIQUE_128:
+ case FS_OPEN_MODE_CREATE:
+ case FS_OPEN_MODE_REPLACE:
+ T_BEGIN {
+ file->fd = fs_posix_create(file);
+ } T_END;
+ break;
+ }
+ if (file->fd == -1)
+ return -1;
+ return 0;
+}
+
+static struct fs_file *fs_posix_file_alloc(void)
+{
+ struct posix_fs_file *file = i_new(struct posix_fs_file, 1);
+ return &file->file;
+}
+
+static void
+fs_posix_file_init(struct fs_file *_file, const char *path,
+ enum fs_open_mode mode, enum fs_open_flags flags)
+{
+ struct posix_fs_file *file =
+ container_of(_file, struct posix_fs_file, file);
+ struct posix_fs *fs = container_of(_file->fs, struct posix_fs, fs);
+ guid_128_t guid;
+ size_t path_len = strlen(path);
+
+ if (path_len > 0 && path[path_len-1] == '/') {
+ /* deleting "path/" (used e.g. by doveadm fs delete) - strip
+ out the trailing "/" since it doesn't work well with NFS. */
+ path = t_strndup(path, path_len-1);
+ }
+
+ if (mode != FS_OPEN_MODE_CREATE_UNIQUE_128)
+ file->file.path = i_strdup(path);
+ else {
+ guid_128_generate(guid);
+ file->file.path = i_strdup_printf("%s/%s", path,
+ guid_128_to_string(guid));
+ }
+ file->full_path = fs->path_prefix == NULL ? i_strdup(file->file.path) :
+ i_strconcat(fs->path_prefix, file->file.path, NULL);
+ file->open_mode = mode;
+ file->open_flags = flags;
+ file->fd = -1;
+}
+
+static void fs_posix_file_close(struct fs_file *_file)
+{
+ struct posix_fs_file *file =
+ container_of(_file, struct posix_fs_file, file);
+
+ if (file->fd != -1 && file->file.output == NULL) {
+ if (close(file->fd) < 0) {
+ e_error(_file->event, "close(%s) failed: %m",
+ file->full_path);
+ }
+ file->fd = -1;
+ }
+}
+
+static void fs_posix_file_deinit(struct fs_file *_file)
+{
+ struct posix_fs_file *file =
+ container_of(_file, struct posix_fs_file, file);
+
+ i_assert(_file->output == NULL);
+
+ switch (file->open_mode) {
+ case FS_OPEN_MODE_READONLY:
+ case FS_OPEN_MODE_APPEND:
+ break;
+ case FS_OPEN_MODE_CREATE_UNIQUE_128:
+ case FS_OPEN_MODE_CREATE:
+ case FS_OPEN_MODE_REPLACE:
+ if (file->temp_path == NULL)
+ break;
+ /* failed to create/replace this. delete the temp file */
+ if (unlink(file->temp_path) < 0) {
+ e_error(_file->event, "unlink(%s) failed: %m",
+ file->temp_path);
+ }
+ break;
+ }
+
+ fs_file_free(_file);
+ i_free(file->temp_path);
+ i_free(file->full_path);
+ i_free(file->file.path);
+ i_free(file);
+}
+
+static int fs_posix_open_for_read(struct posix_fs_file *file)
+{
+ i_assert(file->file.output == NULL);
+ i_assert(file->temp_path == NULL);
+
+ if (file->fd == -1) {
+ if (fs_posix_open(file) < 0)
+ return -1;
+ }
+ return 0;
+}
+
+static bool fs_posix_prefetch(struct fs_file *_file, uoff_t length ATTR_UNUSED)
+{
+ struct posix_fs_file *file =
+ container_of(_file, struct posix_fs_file, file);
+
+ if (fs_posix_open_for_read(file) < 0)
+ return TRUE;
+
+/* HAVE_POSIX_FADVISE alone isn't enough for CentOS 4.9 */
+#if defined(HAVE_POSIX_FADVISE) && defined(POSIX_FADV_WILLNEED)
+ if (posix_fadvise(file->fd, 0, length, POSIX_FADV_WILLNEED) < 0) {
+ e_error(_file->event, "posix_fadvise(%s) failed: %m", file->full_path);
+ return TRUE;
+ }
+#endif
+ return FALSE;
+}
+
+static ssize_t fs_posix_read(struct fs_file *_file, void *buf, size_t size)
+{
+ struct posix_fs_file *file =
+ container_of(_file, struct posix_fs_file, file);
+ ssize_t ret;
+
+ if (fs_posix_open_for_read(file) < 0)
+ return -1;
+
+ if (file->seek_to_beginning) {
+ file->seek_to_beginning = FALSE;
+ if (lseek(file->fd, 0, SEEK_SET) < 0) {
+ fs_set_error_errno(_file->event,
+ "lseek(%s, 0) failed: %m",
+ file->full_path);
+ return -1;
+ }
+ }
+
+ ret = read(file->fd, buf, size);
+ if (ret < 0)
+ fs_set_error_errno(_file->event, "read(%s) failed: %m",
+ file->full_path);
+ fs_posix_file_close(_file);
+ return ret;
+}
+
+static struct istream *
+fs_posix_read_stream(struct fs_file *_file, size_t max_buffer_size)
+{
+ struct posix_fs_file *file =
+ container_of(_file, struct posix_fs_file, file);
+ struct istream *input;
+ int fd_dup;
+
+ if (fs_posix_open_for_read(file) < 0)
+ input = i_stream_create_error_str(errno, "%s", fs_file_last_error(_file));
+ else if ((fd_dup = dup(file->fd)) == -1)
+ input = i_stream_create_error_str(errno, "dup() failed: %m");
+ else {
+ /* The stream could live even after the fs_file.
+ Don't use file->fd directly, because the fd may still be
+ used for other purposes. It's especially important for files
+ that were just created. */
+ input = i_stream_create_fd_autoclose(&fd_dup, max_buffer_size);
+ }
+ i_stream_set_name(input, file->full_path);
+ return input;
+}
+
+static void fs_posix_write_rename_if_needed(struct posix_fs_file *file)
+{
+ struct posix_fs *fs = container_of(file->file.fs, struct posix_fs, fs);
+ const char *new_fname;
+
+ new_fname = fs_metadata_find(&file->file.metadata, FS_METADATA_WRITE_FNAME);
+ if (new_fname == NULL)
+ return;
+
+ i_free(file->file.path);
+ file->file.path = i_strdup(new_fname);
+
+ i_free(file->full_path);
+ file->full_path = fs->path_prefix == NULL ? i_strdup(file->file.path) :
+ i_strconcat(fs->path_prefix, file->file.path, NULL);
+}
+
+static int fs_posix_write_finish_link(struct posix_fs_file *file)
+{
+ unsigned int try_count = 0;
+ int ret;
+
+ ret = link(file->temp_path, file->full_path);
+ while (ret < 0 && errno == ENOENT &&
+ try_count <= MAX_MKDIR_RETRY_COUNT) {
+ if (fs_posix_mkdir_parents(file, file->full_path) < 0)
+ return -1;
+ ret = link(file->temp_path, file->full_path);
+ try_count++;
+ }
+ if (ret < 0) {
+ fs_set_error_errno(file->file.event, "link(%s, %s) failed: %m",
+ file->temp_path, file->full_path);
+ }
+ return ret;
+}
+
+static int fs_posix_write_finish(struct posix_fs_file *file)
+{
+ struct posix_fs *fs = container_of(file->file.fs, struct posix_fs, fs);
+ unsigned int try_count = 0;
+ int ret, old_errno;
+
+ if ((file->open_flags & FS_OPEN_FLAG_FSYNC) != 0 &&
+ !fs->disable_fsync) {
+ if (fdatasync(file->fd) < 0) {
+ fs_set_error_errno(file->file.event,
+ "fdatasync(%s) failed: %m",
+ file->full_path);
+ return -1;
+ }
+ }
+ if (fs->accurate_mtime) {
+ /* Linux updates the mtime timestamp only on timer interrupts.
+ This isn't anywhere close to being microsecond precision.
+ If requested, use utimes() to explicitly set a more accurate
+ mtime. */
+ struct timeval tv[2];
+ i_gettimeofday(&tv[0]);
+ tv[1] = tv[0];
+ if ((utimes(file->temp_path, tv)) < 0) {
+ fs_set_error_errno(file->file.event,
+ "utimes(%s) failed: %m",
+ file->temp_path);
+ return -1;
+ }
+ }
+
+ fs_posix_write_rename_if_needed(file);
+ switch (file->open_mode) {
+ case FS_OPEN_MODE_CREATE_UNIQUE_128:
+ case FS_OPEN_MODE_CREATE:
+ ret = fs_posix_write_finish_link(file);
+ old_errno = errno;
+ if (unlink(file->temp_path) < 0) {
+ fs_set_error_errno(file->file.event,
+ "unlink(%s) failed: %m",
+ file->temp_path);
+ }
+ errno = old_errno;
+ if (ret < 0) {
+ fs_posix_file_close(&file->file);
+ i_free_and_null(file->temp_path);
+ return -1;
+ }
+ break;
+ case FS_OPEN_MODE_REPLACE:
+ ret = rename(file->temp_path, file->full_path);
+ while (ret < 0 && errno == ENOENT &&
+ try_count <= MAX_MKDIR_RETRY_COUNT) {
+ if (fs_posix_mkdir_parents(file, file->full_path) < 0)
+ return -1;
+ ret = rename(file->temp_path, file->full_path);
+ try_count++;
+ }
+ if (ret < 0) {
+ fs_set_error_errno(file->file.event,
+ "rename(%s, %s) failed: %m",
+ file->temp_path, file->full_path);
+ return -1;
+ }
+ break;
+ default:
+ i_unreached();
+ }
+ i_free_and_null(file->temp_path);
+ file->seek_to_beginning = TRUE;
+ /* allow opening the file after writing to it */
+ file->open_mode = FS_OPEN_MODE_READONLY;
+ return 0;
+}
+
+static int fs_posix_write(struct fs_file *_file, const void *data, size_t size)
+{
+ struct posix_fs_file *file =
+ container_of(_file, struct posix_fs_file, file);
+ ssize_t ret;
+
+ if (file->fd == -1) {
+ if (fs_posix_open(file) < 0)
+ return -1;
+ i_assert(file->fd != -1);
+ }
+
+ if (file->open_mode != FS_OPEN_MODE_APPEND) {
+ if (write_full(file->fd, data, size) < 0) {
+ fs_set_error_errno(_file->event, "write(%s) failed: %m",
+ file->full_path);
+ return -1;
+ }
+ return fs_posix_write_finish(file);
+ }
+
+ /* atomic append - it should either succeed or fail */
+ ret = write(file->fd, data, size);
+ if (ret < 0) {
+ fs_set_error_errno(_file->event, "write(%s) failed: %m",
+ file->full_path);
+ return -1;
+ }
+ if ((size_t)ret != size) {
+ fs_set_error(_file->event, ENOSPC,
+ "write(%s) returned %zu/%zu",
+ file->full_path, (size_t)ret, size);
+ return -1;
+ }
+ return 0;
+}
+
+static void fs_posix_write_stream(struct fs_file *_file)
+{
+ struct posix_fs_file *file =
+ container_of(_file, struct posix_fs_file, file);
+
+ i_assert(_file->output == NULL);
+
+ if (file->open_mode == FS_OPEN_MODE_APPEND) {
+ file->write_buf = buffer_create_dynamic(default_pool, 1024*32);
+ _file->output = o_stream_create_buffer(file->write_buf);
+ } else if (file->fd == -1 && fs_posix_open(file) < 0) {
+ _file->output = o_stream_create_error_str(errno, "%s",
+ fs_file_last_error(_file));
+ } else {
+ i_assert(file->fd != -1);
+ _file->output = o_stream_create_fd_file(file->fd,
+ UOFF_T_MAX, FALSE);
+ }
+ o_stream_set_name(_file->output, file->full_path);
+}
+
+static int fs_posix_write_stream_finish(struct fs_file *_file, bool success)
+{
+ struct posix_fs_file *file =
+ container_of(_file, struct posix_fs_file, file);
+ int ret = success ? 0 : -1;
+
+ o_stream_destroy(&_file->output);
+
+ switch (file->open_mode) {
+ case FS_OPEN_MODE_APPEND:
+ if (ret == 0) {
+ ret = fs_posix_write(_file, file->write_buf->data,
+ file->write_buf->used);
+ }
+ buffer_free(&file->write_buf);
+ break;
+ case FS_OPEN_MODE_CREATE:
+ case FS_OPEN_MODE_CREATE_UNIQUE_128:
+ case FS_OPEN_MODE_REPLACE:
+ if (ret == 0)
+ ret = fs_posix_write_finish(file);
+ break;
+ case FS_OPEN_MODE_READONLY:
+ i_unreached();
+ }
+ return ret < 0 ? -1 : 1;
+}
+
+static int
+fs_posix_lock(struct fs_file *_file, unsigned int secs, struct fs_lock **lock_r)
+{
+ struct posix_fs_file *file =
+ container_of(_file, struct posix_fs_file, file);
+ struct posix_fs *fs = container_of(_file->fs, struct posix_fs, fs);
+ struct dotlock_settings dotlock_set;
+ struct posix_fs_lock fs_lock, *ret_lock;
+ const char *error;
+ int ret = -1;
+
+ i_zero(&fs_lock);
+ fs_lock.lock.file = _file;
+
+ struct file_lock_settings lock_set = {
+ .lock_method = FILE_LOCK_METHOD_FLOCK,
+ };
+ switch (fs->lock_method) {
+ case FS_POSIX_LOCK_METHOD_FLOCK:
+#ifndef HAVE_FLOCK
+ fs_set_error(_file->event, ENOTSUP,
+ "flock() not supported by OS (for file %s)",
+ file->full_path);
+#else
+ if (secs == 0) {
+ ret = file_try_lock(file->fd, file->full_path, F_WRLCK,
+ &lock_set, &fs_lock.file_lock,
+ &error);
+ } else {
+ ret = file_wait_lock(file->fd, file->full_path, F_WRLCK,
+ &lock_set, secs,
+ &fs_lock.file_lock, &error);
+ }
+ if (ret < 0) {
+ fs_set_error_errno(_file->event, "flock(%s) failed: %s",
+ file->full_path, error);
+ }
+#endif
+ break;
+ case FS_POSIX_LOCK_METHOD_DOTLOCK:
+ i_zero(&dotlock_set);
+ dotlock_set.stale_timeout = FS_POSIX_DOTLOCK_STALE_TIMEOUT_SECS;
+ dotlock_set.use_excl_lock = TRUE;
+ dotlock_set.timeout = secs;
+
+ ret = file_dotlock_create(&dotlock_set, file->full_path,
+ secs == 0 ? 0 :
+ DOTLOCK_CREATE_FLAG_NONBLOCK,
+ &fs_lock.dotlock);
+ if (ret < 0) {
+ fs_set_error_errno(_file->event,
+ "file_dotlock_create(%s) failed: %m",
+ file->full_path);
+ }
+ break;
+ }
+ if (ret <= 0)
+ return ret;
+
+ ret_lock = i_new(struct posix_fs_lock, 1);
+ *ret_lock = fs_lock;
+ *lock_r = &ret_lock->lock;
+ return 1;
+}
+
+static void fs_posix_unlock(struct fs_lock *_lock)
+{
+ struct posix_fs_lock *lock =
+ container_of(_lock, struct posix_fs_lock, lock);
+
+ if (lock->file_lock != NULL)
+ file_unlock(&lock->file_lock);
+ if (lock->dotlock != NULL)
+ file_dotlock_delete(&lock->dotlock);
+ i_free(lock);
+}
+
+static int fs_posix_exists(struct fs_file *_file)
+{
+ struct posix_fs_file *file =
+ container_of(_file, struct posix_fs_file, file);
+ struct stat st;
+
+ if (stat(file->full_path, &st) < 0) {
+ if (errno != ENOENT) {
+ fs_set_error_errno(_file->event, "stat(%s) failed: %m",
+ file->full_path);
+ return -1;
+ }
+ return 0;
+ }
+ return 1;
+}
+
+static int fs_posix_stat(struct fs_file *_file, struct stat *st_r)
+{
+ struct posix_fs_file *file =
+ container_of(_file, struct posix_fs_file, file);
+
+ /* in case output != NULL it means that we're still writing to the file
+ and fs_stat() shouldn't stat the unfinished file. this is done by
+ fs-sis after fs_copy(). */
+ if (file->fd != -1 && _file->output == NULL) {
+ if (fstat(file->fd, st_r) < 0) {
+ fs_set_error_errno(_file->event, "fstat(%s) failed: %m",
+ file->full_path);
+ return -1;
+ }
+ } else {
+ if (stat(file->full_path, st_r) < 0) {
+ fs_set_error_errno(_file->event, "stat(%s) failed: %m",
+ file->full_path);
+ return -1;
+ }
+ }
+ return 0;
+}
+
+static int fs_posix_copy(struct fs_file *_src, struct fs_file *_dest)
+{
+ struct posix_fs_file *src =
+ container_of(_src, struct posix_fs_file, file);
+ struct posix_fs_file *dest =
+ container_of(_dest, struct posix_fs_file, file);
+ unsigned int try_count = 0;
+ int ret;
+
+ fs_posix_write_rename_if_needed(dest);
+ ret = link(src->full_path, dest->full_path);
+ if (errno == EEXIST && dest->open_mode == FS_OPEN_MODE_REPLACE) {
+ /* destination file already exists - replace it */
+ i_unlink_if_exists(dest->full_path);
+ ret = link(src->full_path, dest->full_path);
+ }
+ while (ret < 0 && errno == ENOENT &&
+ try_count <= MAX_MKDIR_RETRY_COUNT) {
+ if (fs_posix_mkdir_parents(dest, dest->full_path) < 0)
+ return -1;
+ ret = link(src->full_path, dest->full_path);
+ try_count++;
+ }
+ if (ret < 0) {
+ fs_set_error_errno(_src->event, "link(%s, %s) failed: %m",
+ src->full_path, dest->full_path);
+ return -1;
+ }
+ return 0;
+}
+
+static int fs_posix_rename(struct fs_file *_src, struct fs_file *_dest)
+{
+ struct posix_fs_file *src =
+ container_of(_src, struct posix_fs_file, file);
+ struct posix_fs_file *dest =
+ container_of(_dest, struct posix_fs_file, file);
+ unsigned int try_count = 0;
+ int ret;
+
+ ret = rename(src->full_path, dest->full_path);
+ while (ret < 0 && errno == ENOENT &&
+ try_count <= MAX_MKDIR_RETRY_COUNT) {
+ if (fs_posix_mkdir_parents(dest, dest->full_path) < 0)
+ return -1;
+ ret = rename(src->full_path, dest->full_path);
+ try_count++;
+ }
+ if (ret < 0) {
+ fs_set_error_errno(_src->event, "rename(%s, %s) failed: %m",
+ src->full_path, dest->full_path);
+ return -1;
+ }
+ return 0;
+}
+
+static int fs_posix_delete(struct fs_file *_file)
+{
+ struct posix_fs_file *file =
+ container_of(_file, struct posix_fs_file, file);
+
+ if (unlink(file->full_path) < 0) {
+ if (!UNLINK_EISDIR(errno)) {
+ fs_set_error_errno(_file->event, "unlink(%s) failed: %m",
+ file->full_path);
+ return -1;
+ }
+ /* attempting to delete a directory. convert it to rmdir()
+ automatically. */
+ if (rmdir(file->full_path) < 0) {
+ fs_set_error_errno(_file->event, "rmdir(%s) failed: %m",
+ file->full_path);
+ return -1;
+ }
+ }
+ (void)fs_posix_rmdir_parents(file, file->full_path);
+ return 0;
+}
+
+static struct fs_iter *fs_posix_iter_alloc(void)
+{
+ struct posix_fs_iter *iter = i_new(struct posix_fs_iter, 1);
+ return &iter->iter;
+}
+
+static void
+fs_posix_iter_init(struct fs_iter *_iter, const char *path,
+ enum fs_iter_flags flags ATTR_UNUSED)
+{
+ struct posix_fs_iter *iter =
+ container_of(_iter, struct posix_fs_iter, iter);
+ struct posix_fs *fs = container_of(_iter->fs, struct posix_fs, fs);
+
+ iter->path = fs->path_prefix == NULL ? i_strdup(path) :
+ i_strconcat(fs->path_prefix, path, NULL);
+ if (iter->path[0] == '\0') {
+ i_free(iter->path);
+ iter->path = i_strdup(".");
+ }
+ iter->dir = opendir(iter->path);
+ if (iter->dir == NULL && errno != ENOENT) {
+ iter->err = errno;
+ fs_set_error_errno(_iter->event,
+ "opendir(%s) failed: %m", iter->path);
+ }
+}
+
+static bool fs_posix_iter_want(struct posix_fs_iter *iter, const char *fname)
+{
+ bool ret;
+
+ T_BEGIN {
+ const char *path = t_strdup_printf("%s/%s", iter->path, fname);
+ struct stat st;
+
+ if (stat(path, &st) < 0 &&
+ lstat(path, &st) < 0)
+ ret = FALSE;
+ else if (!S_ISDIR(st.st_mode))
+ ret = (iter->iter.flags & FS_ITER_FLAG_DIRS) == 0;
+ else
+ ret = (iter->iter.flags & FS_ITER_FLAG_DIRS) != 0;
+ } T_END;
+ return ret;
+}
+
+static const char *fs_posix_iter_next(struct fs_iter *_iter)
+{
+ struct posix_fs_iter *iter =
+ container_of(_iter, struct posix_fs_iter, iter);
+ struct posix_fs *fs = container_of(_iter->fs, struct posix_fs, fs);
+ struct dirent *d;
+
+ if (iter->dir == NULL)
+ return NULL;
+
+ errno = 0;
+ for (; (d = readdir(iter->dir)) != NULL; errno = 0) {
+ if (strcmp(d->d_name, ".") == 0 ||
+ strcmp(d->d_name, "..") == 0)
+ continue;
+ if (strncmp(d->d_name, fs->temp_file_prefix,
+ fs->temp_file_prefix_len) == 0)
+ continue;
+#ifdef HAVE_DIRENT_D_TYPE
+ switch (d->d_type) {
+ case DT_UNKNOWN:
+ if (fs_posix_iter_want(iter, d->d_name))
+ return d->d_name;
+ break;
+ case DT_DIR:
+ if ((iter->iter.flags & FS_ITER_FLAG_DIRS) != 0)
+ return d->d_name;
+ break;
+ default:
+ if ((iter->iter.flags & FS_ITER_FLAG_DIRS) == 0)
+ return d->d_name;
+ break;
+ }
+#else
+ if (fs_posix_iter_want(iter, d->d_name))
+ return d->d_name;
+#endif
+ }
+ if (errno != 0) {
+ iter->err = errno;
+ fs_set_error_errno(_iter->event,
+ "readdir(%s) failed: %m", iter->path);
+ }
+ return NULL;
+}
+
+static int fs_posix_iter_deinit(struct fs_iter *_iter)
+{
+ struct posix_fs_iter *iter =
+ container_of(_iter, struct posix_fs_iter, iter);
+ int ret = 0;
+
+ if (iter->dir != NULL && closedir(iter->dir) < 0 && iter->err == 0) {
+ iter->err = errno;
+ fs_set_error_errno(_iter->event,
+ "closedir(%s) failed: %m", iter->path);
+ }
+ if (iter->err != 0) {
+ errno = iter->err;
+ ret = -1;
+ }
+ i_free(iter->path);
+ return ret;
+}
+
+const struct fs fs_class_posix = {
+ .name = "posix",
+ .v = {
+ fs_posix_alloc,
+ fs_posix_init,
+ NULL,
+ fs_posix_free,
+ fs_posix_get_properties,
+ fs_posix_file_alloc,
+ fs_posix_file_init,
+ fs_posix_file_deinit,
+ fs_posix_file_close,
+ NULL,
+ NULL, NULL,
+ fs_default_set_metadata,
+ NULL,
+ fs_posix_prefetch,
+ fs_posix_read,
+ fs_posix_read_stream,
+ fs_posix_write,
+ fs_posix_write_stream,
+ fs_posix_write_stream_finish,
+ fs_posix_lock,
+ fs_posix_unlock,
+ fs_posix_exists,
+ fs_posix_stat,
+ fs_posix_copy,
+ fs_posix_rename,
+ fs_posix_delete,
+ fs_posix_iter_alloc,
+ fs_posix_iter_init,
+ fs_posix_iter_next,
+ fs_posix_iter_deinit,
+ NULL,
+ NULL,
+ }
+};
diff --git a/src/lib-fs/fs-randomfail.c b/src/lib-fs/fs-randomfail.c
new file mode 100644
index 0000000..48b1bf0
--- /dev/null
+++ b/src/lib-fs/fs-randomfail.c
@@ -0,0 +1,555 @@
+/* Copyright (c) 2015-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "istream.h"
+#include "istream-private.h"
+#include "istream-concat.h"
+#include "istream-failure-at.h"
+#include "ostream-failure-at.h"
+#include "ostream.h"
+#include "fs-api-private.h"
+
+
+#define RANDOMFAIL_ERROR "Random failure injection"
+
+static const char *fs_op_names[FS_OP_COUNT] = {
+ "wait", "metadata", "prefetch", "read", "write", "lock", "exists",
+ "stat", "copy", "rename", "delete", "iter"
+};
+
+struct randomfail_fs {
+ struct fs fs;
+ unsigned int op_probability[FS_OP_COUNT];
+ uoff_t range_start[FS_OP_COUNT], range_end[FS_OP_COUNT];
+};
+
+struct randomfail_fs_file {
+ struct fs_file file;
+ struct fs_file *super_read;
+ struct istream *input;
+ bool op_pending[FS_OP_COUNT];
+
+ struct ostream *super_output;
+};
+
+struct randomfail_fs_iter {
+ struct fs_iter iter;
+ struct fs_iter *super;
+ unsigned int fail_pos;
+};
+
+#define RANDOMFAIL_FS(ptr) container_of((ptr), struct randomfail_fs, fs)
+#define RANDOMFAIL_FILE(ptr) container_of((ptr), struct randomfail_fs_file, file)
+#define RANDOMFAIL_ITER(ptr) container_of((ptr), struct randomfail_fs_iter, iter)
+
+static struct fs *fs_randomfail_alloc(void)
+{
+ struct randomfail_fs *fs;
+
+ fs = i_new(struct randomfail_fs, 1);
+ fs->fs = fs_class_randomfail;
+ return &fs->fs;
+}
+
+static bool fs_op_find(const char *str, enum fs_op *op_r)
+{
+ enum fs_op op;
+
+ for (op = 0; op < FS_OP_COUNT; op++) {
+ if (strcmp(fs_op_names[op], str) == 0) {
+ *op_r = op;
+ return TRUE;
+ }
+ }
+ return FALSE;
+}
+
+static int
+fs_randomfail_add_probability(struct randomfail_fs *fs,
+ const char *key, const char *value,
+ const char **error_r)
+{
+ unsigned int num;
+ enum fs_op op;
+ bool invalid_value = FALSE;
+
+ if (str_to_uint(value, &num) < 0 || num > 100)
+ invalid_value = TRUE;
+ if (fs_op_find(key, &op)) {
+ if (invalid_value) {
+ *error_r = "Invalid probability value";
+ return -1;
+ }
+ fs->op_probability[op] = num;
+ return 1;
+ }
+ if (strcmp(key, "all") == 0) {
+ if (invalid_value) {
+ *error_r = "Invalid probability value";
+ return -1;
+ }
+ for (op = 0; op < FS_OP_COUNT; op++)
+ fs->op_probability[op] = num;
+ return 1;
+ }
+ return 0;
+}
+
+static int
+fs_randomfail_add_probability_range(struct randomfail_fs *fs,
+ const char *key, const char *value,
+ const char **error_r)
+{
+ enum fs_op op;
+ const char *p;
+ uoff_t num1, num2;
+
+ if (strcmp(key, "read-range") == 0)
+ op = FS_OP_READ;
+ else if (strcmp(key, "write-range") == 0)
+ op = FS_OP_WRITE;
+ else if (strcmp(key, "iter-range") == 0)
+ op = FS_OP_ITER;
+ else
+ return 0;
+
+ p = strchr(value, '-');
+ if (p == NULL) {
+ if (str_to_uoff(value, &num1) < 0) {
+ *error_r = "Invalid range value";
+ return -1;
+ }
+ num2 = num1;
+ } else if (str_to_uoff(t_strdup_until(value, p), &num1) < 0 ||
+ str_to_uoff(p+1, &num2) < 0 || num1 > num2) {
+ *error_r = "Invalid range values";
+ return -1;
+ }
+ fs->range_start[op] = num1;
+ fs->range_end[op] = num2;
+ return 1;
+}
+
+static int fs_randomfail_parse_params(struct randomfail_fs *fs,
+ const char *params, const char **error_r)
+{
+ const char *const *tmp;
+ int ret;
+
+ for (tmp = t_strsplit_spaces(params, ","); *tmp != NULL; tmp++) {
+ const char *key = *tmp;
+ const char *value = strchr(key, '=');
+
+ if (value == NULL) {
+ *error_r = "Missing '='";
+ return -1;
+ }
+ key = t_strdup_until(key, value++);
+ if ((ret = fs_randomfail_add_probability(fs, key, value, error_r)) != 0) {
+ if (ret < 0)
+ return -1;
+ continue;
+ }
+ if ((ret = fs_randomfail_add_probability_range(fs, key, value, error_r)) != 0) {
+ if (ret < 0)
+ return -1;
+ continue;
+ }
+ *error_r = t_strdup_printf("Unknown key '%s'", key);
+ return -1;
+ }
+ return 0;
+}
+
+static int
+fs_randomfail_init(struct fs *_fs, const char *args,
+ const struct fs_settings *set, const char **error_r)
+{
+ struct randomfail_fs *fs = RANDOMFAIL_FS(_fs);
+ const char *p, *parent_name, *parent_args, *error;
+
+ p = strchr(args, ':');
+ if (p == NULL) {
+ *error_r = "Randomfail parameters missing";
+ return -1;
+ }
+ if (fs_randomfail_parse_params(fs, t_strdup_until(args, p++), &error) < 0) {
+ *error_r = t_strdup_printf(
+ "Invalid randomfail parameters: %s", error);
+ return -1;
+ }
+ args = p;
+
+ if (*args == '\0') {
+ *error_r = "Parent filesystem not given as parameter";
+ return -1;
+ }
+
+ parent_args = strchr(args, ':');
+ if (parent_args == NULL) {
+ parent_name = args;
+ parent_args = "";
+ } else {
+ parent_name = t_strdup_until(args, parent_args);
+ parent_args++;
+ }
+ if (fs_init(parent_name, parent_args, set, &_fs->parent, error_r) < 0)
+ return -1;
+ return 0;
+}
+
+static void fs_randomfail_free(struct fs *_fs)
+{
+ struct randomfail_fs *fs = RANDOMFAIL_FS(_fs);
+
+ i_free(fs);
+}
+
+static enum fs_properties fs_randomfail_get_properties(struct fs *_fs)
+{
+ return fs_get_properties(_fs->parent);
+}
+
+static struct fs_file *fs_randomfail_file_alloc(void)
+{
+ struct randomfail_fs_file *file = i_new(struct randomfail_fs_file, 1);
+ return &file->file;
+}
+
+static void
+fs_randomfail_file_init(struct fs_file *_file, const char *path,
+ enum fs_open_mode mode, enum fs_open_flags flags)
+{
+ struct randomfail_fs_file *file = RANDOMFAIL_FILE(_file);
+
+ file->file.path = i_strdup(path);
+ file->file.parent = fs_file_init_parent(_file, path, mode, flags);
+}
+
+static void fs_randomfail_file_deinit(struct fs_file *_file)
+{
+ struct randomfail_fs_file *file = RANDOMFAIL_FILE(_file);
+
+ fs_file_free(_file);
+ i_free(file->file.path);
+ i_free(file);
+}
+
+static bool fs_random_fail(struct fs *_fs, struct event *event,
+ int divider, enum fs_op op)
+{
+ struct randomfail_fs *fs = RANDOMFAIL_FS(_fs);
+
+ if (fs->op_probability[op] == 0)
+ return FALSE;
+ if ((unsigned int)i_rand_limit(100 * divider) <= fs->op_probability[op]) {
+ fs_set_error(event, EIO, RANDOMFAIL_ERROR);
+ return TRUE;
+ }
+ return FALSE;
+}
+
+static bool
+fs_file_random_fail_begin(struct randomfail_fs_file *file, enum fs_op op)
+{
+ if (!file->op_pending[op]) {
+ if (fs_random_fail(file->file.fs, file->file.event, 2, op))
+ return TRUE;
+ }
+ file->op_pending[op] = TRUE;
+ return FALSE;
+}
+
+static int
+fs_file_random_fail_end(struct randomfail_fs_file *file,
+ int ret, enum fs_op op)
+{
+ if (ret == 0 || errno != EAGAIN) {
+ if (fs_random_fail(file->file.fs, file->file.event, 2, op))
+ return -1;
+ file->op_pending[op] = FALSE;
+ }
+ return ret;
+}
+
+static bool
+fs_random_fail_range(struct fs *_fs, struct event *event,
+ enum fs_op op, uoff_t *offset_r)
+{
+ struct randomfail_fs *fs = RANDOMFAIL_FS(_fs);
+
+ if (!fs_random_fail(_fs, event, 1, op))
+ return FALSE;
+ *offset_r = i_rand_minmax(fs->range_start[op], fs->range_end[op]);
+ return TRUE;
+}
+
+static int
+fs_randomfail_get_metadata(struct fs_file *_file,
+ enum fs_get_metadata_flags flags,
+ const ARRAY_TYPE(fs_metadata) **metadata_r)
+{
+ struct randomfail_fs_file *file = RANDOMFAIL_FILE(_file);
+ int ret;
+
+ if (fs_file_random_fail_begin(file, FS_OP_METADATA))
+ return -1;
+ ret = fs_get_metadata_full(_file->parent, flags, metadata_r);
+ return fs_file_random_fail_end(file, ret, FS_OP_METADATA);
+}
+
+static bool fs_randomfail_prefetch(struct fs_file *_file, uoff_t length)
+{
+ if (fs_random_fail(_file->fs, _file->event, 1, FS_OP_PREFETCH))
+ return TRUE;
+ return fs_prefetch(_file->parent, length);
+}
+
+static ssize_t fs_randomfail_read(struct fs_file *_file, void *buf, size_t size)
+{
+ struct randomfail_fs_file *file = RANDOMFAIL_FILE(_file);
+ int ret;
+
+ if (fs_file_random_fail_begin(file, FS_OP_READ))
+ return -1;
+ ret = fs_read(_file->parent, buf, size);
+ if (fs_file_random_fail_end(file, ret < 0 ? -1 : 0, FS_OP_READ) < 0)
+ return -1;
+ return ret;
+}
+
+static struct istream *
+fs_randomfail_read_stream(struct fs_file *_file, size_t max_buffer_size)
+{
+ struct istream *input, *input2;
+ uoff_t offset;
+
+ input = fs_read_stream(_file->parent, max_buffer_size);
+ if (!fs_random_fail_range(_file->fs, _file->event, FS_OP_READ, &offset))
+ return input;
+ input2 = i_stream_create_failure_at(input, offset, EIO, RANDOMFAIL_ERROR);
+ i_stream_unref(&input);
+ return input2;
+}
+
+static int fs_randomfail_write(struct fs_file *_file, const void *data, size_t size)
+{
+ struct randomfail_fs_file *file = RANDOMFAIL_FILE(_file);
+ int ret;
+
+ if (fs_file_random_fail_begin(file, FS_OP_WRITE))
+ return -1;
+ ret = fs_write(_file->parent, data, size);
+ return fs_file_random_fail_end(file, ret, FS_OP_EXISTS);
+}
+
+static void fs_randomfail_write_stream(struct fs_file *_file)
+{
+ struct randomfail_fs_file *file = RANDOMFAIL_FILE(_file);
+ uoff_t offset;
+
+ i_assert(_file->output == NULL);
+
+ file->super_output = fs_write_stream(_file->parent);
+ if (!fs_random_fail_range(_file->fs, _file->event, FS_OP_WRITE, &offset))
+ _file->output = file->super_output;
+ else {
+ _file->output = o_stream_create_failure_at(file->super_output, offset,
+ RANDOMFAIL_ERROR);
+ }
+}
+
+static int fs_randomfail_write_stream_finish(struct fs_file *_file, bool success)
+{
+ struct randomfail_fs_file *file = RANDOMFAIL_FILE(_file);
+
+ if (_file->output != NULL) {
+ if (_file->output == file->super_output)
+ _file->output = NULL;
+ else
+ o_stream_unref(&_file->output);
+ if (!success) {
+ fs_write_stream_abort_parent(_file, &file->super_output);
+ return -1;
+ }
+ if (fs_random_fail(_file->fs, _file->event, 1, FS_OP_WRITE)) {
+ fs_write_stream_abort_error(_file->parent, &file->super_output, RANDOMFAIL_ERROR);
+ return -1;
+ }
+ }
+ return fs_write_stream_finish(_file->parent, &file->super_output);
+}
+
+static int
+fs_randomfail_lock(struct fs_file *_file, unsigned int secs, struct fs_lock **lock_r)
+{
+ if (fs_random_fail(_file->fs, _file->event, 1, FS_OP_LOCK))
+ return -1;
+ return fs_lock(_file->parent, secs, lock_r);
+}
+
+static void fs_randomfail_unlock(struct fs_lock *_lock ATTR_UNUSED)
+{
+ i_unreached();
+}
+
+static int fs_randomfail_exists(struct fs_file *_file)
+{
+ struct randomfail_fs_file *file = RANDOMFAIL_FILE(_file);
+ int ret;
+
+ if (fs_file_random_fail_begin(file, FS_OP_EXISTS))
+ return -1;
+ ret = fs_exists(_file->parent);
+ return fs_file_random_fail_end(file, ret, FS_OP_EXISTS);
+}
+
+static int fs_randomfail_stat(struct fs_file *_file, struct stat *st_r)
+{
+ struct randomfail_fs_file *file = RANDOMFAIL_FILE(_file);
+ int ret;
+
+ if (fs_file_random_fail_begin(file, FS_OP_STAT))
+ return -1;
+ ret = fs_stat(_file->parent, st_r);
+ return fs_file_random_fail_end(file, ret, FS_OP_STAT);
+}
+
+static int fs_randomfail_get_nlinks(struct fs_file *_file, nlink_t *nlinks_r)
+{
+ struct randomfail_fs_file *file = RANDOMFAIL_FILE(_file);
+ int ret;
+
+ if (fs_file_random_fail_begin(file, FS_OP_STAT))
+ return -1;
+ ret = fs_get_nlinks(_file->parent, nlinks_r);
+ return fs_file_random_fail_end(file, ret, FS_OP_STAT);
+}
+
+static int fs_randomfail_copy(struct fs_file *_src, struct fs_file *_dest)
+{
+ struct randomfail_fs_file *dest = RANDOMFAIL_FILE(_dest);
+ int ret;
+
+ if (fs_file_random_fail_begin(dest, FS_OP_COPY))
+ return -1;
+
+ if (_src != NULL)
+ ret = fs_copy(_src->parent, _dest->parent);
+ else
+ ret = fs_copy_finish_async(_dest->parent);
+ return fs_file_random_fail_end(dest, ret, FS_OP_COPY);
+}
+
+static int fs_randomfail_rename(struct fs_file *_src, struct fs_file *_dest)
+{
+ struct randomfail_fs_file *dest = RANDOMFAIL_FILE(_dest);
+ int ret;
+
+ if (fs_file_random_fail_begin(dest, FS_OP_RENAME))
+ return -1;
+ ret = fs_rename(_src->parent, _dest->parent);
+ return fs_file_random_fail_end(dest, ret, FS_OP_RENAME);
+}
+
+static int fs_randomfail_delete(struct fs_file *_file)
+{
+ struct randomfail_fs_file *file = RANDOMFAIL_FILE(_file);
+ int ret;
+
+ if (fs_file_random_fail_begin(file, FS_OP_DELETE))
+ return -1;
+ ret = fs_delete(_file->parent);
+ return fs_file_random_fail_end(file, ret, FS_OP_DELETE);
+}
+
+static struct fs_iter *fs_randomfail_iter_alloc(void)
+{
+ struct randomfail_fs_iter *iter = i_new(struct randomfail_fs_iter, 1);
+ return &iter->iter;
+}
+
+static void
+fs_randomfail_iter_init(struct fs_iter *_iter, const char *path,
+ enum fs_iter_flags flags)
+{
+ struct randomfail_fs_iter *iter = RANDOMFAIL_ITER(_iter);
+ uoff_t pos;
+
+ iter->super = fs_iter_init_parent(_iter, path, flags);
+ if (fs_random_fail_range(_iter->fs, _iter->event, FS_OP_ITER, &pos))
+ iter->fail_pos = pos + 1;
+}
+
+static const char *fs_randomfail_iter_next(struct fs_iter *_iter)
+{
+ struct randomfail_fs_iter *iter = RANDOMFAIL_ITER(_iter);
+ const char *fname;
+
+ if (iter->fail_pos > 0) {
+ if (iter->fail_pos == 1)
+ return NULL;
+ iter->fail_pos--;
+ }
+
+ iter->super->async_callback = _iter->async_callback;
+ iter->super->async_context = _iter->async_context;
+
+ fname = fs_iter_next(iter->super);
+ _iter->async_have_more = iter->super->async_have_more;
+ return fname;
+}
+
+static int fs_randomfail_iter_deinit(struct fs_iter *_iter)
+{
+ struct randomfail_fs_iter *iter = RANDOMFAIL_ITER(_iter);
+ const char *error;
+ int ret;
+
+ if ((ret = fs_iter_deinit(&iter->super, &error)) < 0)
+ fs_set_error_errno(_iter->event, "%s", error);
+ if (iter->fail_pos == 1) {
+ fs_set_error(_iter->event, EIO, RANDOMFAIL_ERROR);
+ ret = -1;
+ }
+ return ret;
+}
+
+const struct fs fs_class_randomfail = {
+ .name = "randomfail",
+ .v = {
+ fs_randomfail_alloc,
+ fs_randomfail_init,
+ NULL,
+ fs_randomfail_free,
+ fs_randomfail_get_properties,
+ fs_randomfail_file_alloc,
+ fs_randomfail_file_init,
+ fs_randomfail_file_deinit,
+ fs_wrapper_file_close,
+ fs_wrapper_file_get_path,
+ fs_wrapper_set_async_callback,
+ fs_wrapper_wait_async,
+ fs_wrapper_set_metadata,
+ fs_randomfail_get_metadata,
+ fs_randomfail_prefetch,
+ fs_randomfail_read,
+ fs_randomfail_read_stream,
+ fs_randomfail_write,
+ fs_randomfail_write_stream,
+ fs_randomfail_write_stream_finish,
+ fs_randomfail_lock,
+ fs_randomfail_unlock,
+ fs_randomfail_exists,
+ fs_randomfail_stat,
+ fs_randomfail_copy,
+ fs_randomfail_rename,
+ fs_randomfail_delete,
+ fs_randomfail_iter_alloc,
+ fs_randomfail_iter_init,
+ fs_randomfail_iter_next,
+ fs_randomfail_iter_deinit,
+ NULL,
+ fs_randomfail_get_nlinks,
+ }
+};
diff --git a/src/lib-fs/fs-sis-common.c b/src/lib-fs/fs-sis-common.c
new file mode 100644
index 0000000..654c44e
--- /dev/null
+++ b/src/lib-fs/fs-sis-common.c
@@ -0,0 +1,59 @@
+/* Copyright (c) 2010-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "fs-sis-common.h"
+
+#include <sys/stat.h>
+
+int fs_sis_path_parse(struct fs_file *file, const char *path,
+ const char **dir_r, const char **hash_r)
+{
+ const char *fname, *p;
+
+ fname = strrchr(path, '/');
+ if (fname == NULL) {
+ *dir_r = ".";
+ fname = path;
+ } else {
+ *dir_r = t_strdup_until(path, fname);
+ fname++;
+ }
+
+ /* assume filename begins with "<hash>-" */
+ p = strchr(fname, '-');
+ if (p == NULL) {
+ fs_set_error(file->event, EINVAL, "open(%s) failed: "
+ "Filenames must begin with '<hash>-'", path);
+ return -1;
+ }
+ *hash_r = t_strdup_until(fname, p);
+ return 0;
+}
+
+void fs_sis_try_unlink_hash_file(struct fs_file *sis_file,
+ struct fs_file *super_file)
+{
+ struct fs_file *hash_file;
+ struct stat st1, st2;
+ const char *dir, *hash, *hash_path;
+
+ if (fs_sis_path_parse(sis_file, super_file->path, &dir, &hash) == 0 &&
+ fs_stat(super_file, &st1) == 0 && st1.st_nlink == 2) {
+ /* this may be the last link. if hashes/ file is the same,
+ delete it. */
+ hash_path = t_strdup_printf("%s/"HASH_DIR_NAME"/%s", dir, hash);
+ hash_file = fs_file_init_with_event(super_file->fs,
+ super_file->event, hash_path,
+ FS_OPEN_MODE_READONLY);
+ if (fs_stat(hash_file, &st2) == 0 &&
+ st1.st_ino == st2.st_ino &&
+ CMP_DEV_T(st1.st_dev, st2.st_dev)) {
+ if (fs_delete(hash_file) < 0) {
+ e_error(hash_file->event, "%s",
+ fs_file_last_error(hash_file));
+ }
+ }
+ fs_file_deinit(&hash_file);
+ }
+}
+
diff --git a/src/lib-fs/fs-sis-common.h b/src/lib-fs/fs-sis-common.h
new file mode 100644
index 0000000..5156ca6
--- /dev/null
+++ b/src/lib-fs/fs-sis-common.h
@@ -0,0 +1,14 @@
+#ifndef FS_SIS_COMMON_H
+#define FS_SIS_COMMON_H
+
+#include "fs-api-private.h"
+
+#define HASH_DIR_NAME "hashes"
+
+int fs_sis_path_parse(struct fs_file *file, const char *path,
+ const char **dir_r, const char **hash_r);
+void fs_sis_try_unlink_hash_file(struct fs_file *sis_file,
+ struct fs_file *super_file);
+
+#endif
+
diff --git a/src/lib-fs/fs-sis-queue.c b/src/lib-fs/fs-sis-queue.c
new file mode 100644
index 0000000..8ad7d98
--- /dev/null
+++ b/src/lib-fs/fs-sis-queue.c
@@ -0,0 +1,210 @@
+/* Copyright (c) 2010-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "str.h"
+#include "istream.h"
+#include "ostream.h"
+#include "fs-sis-common.h"
+
+#define QUEUE_DIR_NAME "queue"
+
+struct sis_queue_fs {
+ struct fs fs;
+ char *queue_dir;
+};
+
+struct sis_queue_fs_file {
+ struct fs_file file;
+ struct sis_queue_fs *fs;
+};
+
+#define SISQUEUE_FS(ptr) container_of((ptr), struct sis_queue_fs, fs)
+#define SISQUEUE_FILE(ptr) container_of((ptr), struct sis_queue_fs_file, file)
+
+static struct fs *fs_sis_queue_alloc(void)
+{
+ struct sis_queue_fs *fs;
+
+ fs = i_new(struct sis_queue_fs, 1);
+ fs->fs = fs_class_sis_queue;
+ return &fs->fs;
+}
+
+static int
+fs_sis_queue_init(struct fs *_fs, const char *args,
+ const struct fs_settings *set, const char **error_r)
+{
+ struct sis_queue_fs *fs = SISQUEUE_FS(_fs);
+ const char *p, *parent_name, *parent_args;
+
+ /* <queue_dir>:<parent fs>[:<args>] */
+
+ p = strchr(args, ':');
+ if (p == NULL || p[1] == '\0') {
+ *error_r = "Parent filesystem not given as parameter";
+ return -1;
+ }
+
+ fs->queue_dir = i_strdup_until(args, p);
+ parent_name = p + 1;
+
+ parent_args = strchr(parent_name, ':');
+ if (parent_args == NULL)
+ parent_args = "";
+ else
+ parent_name = t_strdup_until(parent_name, parent_args++);
+ if (fs_init(parent_name, parent_args, set, &_fs->parent, error_r) < 0)
+ return -1;
+ return 0;
+}
+
+static void fs_sis_queue_free(struct fs *_fs)
+{
+ struct sis_queue_fs *fs = SISQUEUE_FS(_fs);
+
+ i_free(fs->queue_dir);
+ i_free(fs);
+}
+
+static struct fs_file *fs_sis_queue_file_alloc(void)
+{
+ struct sis_queue_fs_file *file = i_new(struct sis_queue_fs_file, 1);
+ return &file->file;
+}
+
+static void
+fs_sis_queue_file_init(struct fs_file *_file, const char *path,
+ enum fs_open_mode mode, enum fs_open_flags flags)
+{
+ struct sis_queue_fs_file *file = SISQUEUE_FILE(_file);
+ struct sis_queue_fs *fs = SISQUEUE_FS(_file->fs);
+
+ file->file.path = i_strdup(path);
+ file->fs = fs;
+
+ if (mode == FS_OPEN_MODE_APPEND)
+ fs_set_error(_file->event, ENOTSUP, "APPEND mode not supported");
+ else
+ file->file.parent = fs_file_init_parent(_file, path, mode, flags);
+}
+
+static void fs_sis_queue_file_deinit(struct fs_file *_file)
+{
+ struct sis_queue_fs_file *file = SISQUEUE_FILE(_file);
+
+ fs_file_free(_file);
+ i_free(file->file.path);
+ i_free(file);
+}
+
+static void fs_sis_queue_add(struct sis_queue_fs_file *file)
+{
+ struct sis_queue_fs *fs = SISQUEUE_FS(file->file.fs);
+ struct fs_file *queue_file;
+ const char *fname, *path, *queue_path;
+
+ path = fs_file_path(&file->file);
+ fname = strrchr(path, '/');
+ if (fname != NULL)
+ fname++;
+ else
+ fname = path;
+
+ queue_path = t_strdup_printf("%s/%s", fs->queue_dir, fname);
+ queue_file = fs_file_init_parent(&file->file, queue_path, FS_OPEN_MODE_CREATE, 0);
+ if (fs_write(queue_file, "", 0) < 0 && errno != EEXIST)
+ e_error(file->file.event, "%s", fs_file_last_error(queue_file));
+ fs_file_deinit(&queue_file);
+}
+
+static int fs_sis_queue_write(struct fs_file *_file, const void *data, size_t size)
+{
+ struct sis_queue_fs_file *file = SISQUEUE_FILE(_file);
+
+ if (_file->parent == NULL)
+ return -1;
+ if (fs_write(_file->parent, data, size) < 0)
+ return -1;
+ T_BEGIN {
+ fs_sis_queue_add(file);
+ } T_END;
+ return 0;
+}
+
+static void fs_sis_queue_write_stream(struct fs_file *_file)
+{
+ i_assert(_file->output == NULL);
+
+ if (_file->parent == NULL) {
+ _file->output = o_stream_create_error_str(EINVAL, "%s",
+ fs_file_last_error(_file));
+ } else {
+ _file->output = fs_write_stream(_file->parent);
+ }
+ o_stream_set_name(_file->output, _file->path);
+}
+
+static int fs_sis_queue_write_stream_finish(struct fs_file *_file, bool success)
+{
+ struct sis_queue_fs_file *file = SISQUEUE_FILE(_file);
+
+ if (!success) {
+ if (_file->parent != NULL)
+ fs_write_stream_abort_parent(_file, &_file->output);
+ return -1;
+ }
+
+ if (fs_write_stream_finish(_file->parent, &_file->output) < 0)
+ return -1;
+ T_BEGIN {
+ fs_sis_queue_add(file);
+ } T_END;
+ return 1;
+}
+
+static int fs_sis_queue_delete(struct fs_file *_file)
+{
+ T_BEGIN {
+ fs_sis_try_unlink_hash_file(_file, _file->parent);
+ } T_END;
+ return fs_delete(_file->parent);
+}
+
+const struct fs fs_class_sis_queue = {
+ .name = "sis-queue",
+ .v = {
+ fs_sis_queue_alloc,
+ fs_sis_queue_init,
+ NULL,
+ fs_sis_queue_free,
+ fs_wrapper_get_properties,
+ fs_sis_queue_file_alloc,
+ fs_sis_queue_file_init,
+ fs_sis_queue_file_deinit,
+ fs_wrapper_file_close,
+ fs_wrapper_file_get_path,
+ fs_wrapper_set_async_callback,
+ fs_wrapper_wait_async,
+ fs_wrapper_set_metadata,
+ fs_wrapper_get_metadata,
+ fs_wrapper_prefetch,
+ fs_wrapper_read,
+ fs_wrapper_read_stream,
+ fs_sis_queue_write,
+ fs_sis_queue_write_stream,
+ fs_sis_queue_write_stream_finish,
+ fs_wrapper_lock,
+ fs_wrapper_unlock,
+ fs_wrapper_exists,
+ fs_wrapper_stat,
+ fs_wrapper_copy,
+ fs_wrapper_rename,
+ fs_sis_queue_delete,
+ fs_wrapper_iter_alloc,
+ fs_wrapper_iter_init,
+ NULL,
+ NULL,
+ NULL,
+ fs_wrapper_get_nlinks,
+ }
+};
diff --git a/src/lib-fs/fs-sis.c b/src/lib-fs/fs-sis.c
new file mode 100644
index 0000000..6a020bc
--- /dev/null
+++ b/src/lib-fs/fs-sis.c
@@ -0,0 +1,369 @@
+/* Copyright (c) 2010-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "str.h"
+#include "istream.h"
+#include "ostream.h"
+#include "ostream-cmp.h"
+#include "fs-sis-common.h"
+
+#define FS_SIS_REQUIRED_PROPS \
+ (FS_PROPERTY_FASTCOPY | FS_PROPERTY_STAT)
+
+struct sis_fs {
+ struct fs fs;
+};
+
+struct sis_fs_file {
+ struct fs_file file;
+ struct sis_fs *fs;
+ enum fs_open_mode open_mode;
+
+ struct fs_file *hash_file;
+ struct istream *hash_input;
+ struct ostream *fs_output;
+
+ char *hash, *hash_path;
+ bool opened;
+};
+
+#define SIS_FS(ptr) container_of((ptr), struct sis_fs, fs)
+#define SIS_FILE(ptr) container_of((ptr), struct sis_fs_file, file)
+
+static struct fs *fs_sis_alloc(void)
+{
+ struct sis_fs *fs;
+
+ fs = i_new(struct sis_fs, 1);
+ fs->fs = fs_class_sis;
+ return &fs->fs;
+}
+
+static int
+fs_sis_init(struct fs *_fs, const char *args, const struct fs_settings *set,
+ const char **error_r)
+{
+ enum fs_properties props;
+ const char *parent_name, *parent_args;
+
+ if (*args == '\0') {
+ *error_r = "Parent filesystem not given as parameter";
+ return -1;
+ }
+
+ parent_args = strchr(args, ':');
+ if (parent_args == NULL) {
+ parent_name = args;
+ parent_args = "";
+ } else {
+ parent_name = t_strdup_until(args, parent_args);
+ parent_args++;
+ }
+ if (fs_init(parent_name, parent_args, set, &_fs->parent, error_r) < 0)
+ return -1;
+ props = fs_get_properties(_fs->parent);
+ if ((props & FS_SIS_REQUIRED_PROPS) != FS_SIS_REQUIRED_PROPS) {
+ *error_r = t_strdup_printf("%s backend can't be used with SIS",
+ parent_name);
+ return -1;
+ }
+ return 0;
+}
+
+static void fs_sis_free(struct fs *_fs)
+{
+ struct sis_fs *fs = SIS_FS(_fs);
+
+ i_free(fs);
+}
+
+static struct fs_file *fs_sis_file_alloc(void)
+{
+ struct sis_fs_file *file = i_new(struct sis_fs_file, 1);
+ return &file->file;
+}
+
+static void
+fs_sis_file_init(struct fs_file *_file, const char *path,
+ enum fs_open_mode mode, enum fs_open_flags flags)
+{
+ struct sis_fs_file *file = SIS_FILE(_file);
+ struct sis_fs *fs = SIS_FS(_file->fs);
+ const char *dir, *hash;
+
+ file->file.path = i_strdup(path);
+ file->fs = fs;
+ file->open_mode = mode;
+ if (mode == FS_OPEN_MODE_APPEND) {
+ fs_set_error(_file->event, ENOTSUP, "APPEND mode not supported");
+ return;
+ }
+
+ if (fs_sis_path_parse(_file, path, &dir, &hash) < 0)
+ return;
+
+ /* if hashes/<hash> already exists, open it */
+ file->hash_path = i_strdup_printf("%s/"HASH_DIR_NAME"/%s", dir, hash);
+ file->hash_file = fs_file_init_parent(_file, file->hash_path,
+ FS_OPEN_MODE_READONLY, 0);
+
+ file->hash_input = fs_read_stream(file->hash_file, IO_BLOCK_SIZE);
+ if (i_stream_read(file->hash_input) == -1) {
+ /* doesn't exist */
+ if (errno != ENOENT) {
+ e_error(file->file.event, "Couldn't read hash file %s: %m",
+ file->hash_path);
+ }
+ i_stream_destroy(&file->hash_input);
+ }
+
+ file->file.parent = fs_file_init_parent(_file, path, mode, flags);
+}
+
+static void fs_sis_file_deinit(struct fs_file *_file)
+{
+ struct sis_fs_file *file = SIS_FILE(_file);
+
+ fs_file_deinit(&file->hash_file);
+ fs_file_free(_file);
+ i_free(file->hash);
+ i_free(file->hash_path);
+ i_free(file->file.path);
+ i_free(file);
+}
+
+static void fs_sis_file_close(struct fs_file *_file)
+{
+ struct sis_fs_file *file = SIS_FILE(_file);
+
+ i_stream_unref(&file->hash_input);
+ fs_file_close(file->hash_file);
+ fs_file_close(_file->parent);
+}
+
+static bool fs_sis_try_link(struct sis_fs_file *file)
+{
+ const struct stat *st;
+ struct stat st2;
+
+ if (i_stream_stat(file->hash_input, FALSE, &st) < 0)
+ return FALSE;
+
+ /* we can use the existing file */
+ if (fs_copy(file->hash_file, file->file.parent) < 0) {
+ if (errno != ENOENT && errno != EMLINK) {
+ e_error(file->file.event, "%s",
+ fs_file_last_error(file->hash_file));
+ }
+ /* failed to use link(), continue as if it hadn't been equal */
+ return FALSE;
+ }
+ if (fs_stat(file->file.parent, &st2) < 0) {
+ e_error(file->file.event, "%s",
+ fs_file_last_error(file->file.parent));
+ if (fs_delete(file->file.parent) < 0) {
+ e_error(file->file.event, "%s",
+ fs_file_last_error(file->file.parent));
+ }
+ return FALSE;
+ }
+ if (st->st_ino != st2.st_ino) {
+ /* the hashes/ file was already replaced with something else */
+ if (fs_delete(file->file.parent) < 0) {
+ e_error(file->file.event, "%s",
+ fs_file_last_error(file->file.parent));
+ }
+ return FALSE;
+ }
+ return TRUE;
+}
+
+static void fs_sis_replace_hash_file(struct sis_fs_file *file)
+{
+ struct fs *super_fs = file->file.parent->fs;
+ struct fs_file *temp_file;
+ const char *hash_fname;
+ string_t *temp_path;
+ int ret;
+
+ if (file->hash_input == NULL) {
+ /* hash file didn't exist previously. we should be able to
+ create it with link() */
+ if (fs_copy(file->file.parent, file->hash_file) < 0) {
+ if (errno == EEXIST) {
+ /* the file was just created. it's probably
+ a duplicate, but it's too much trouble
+ trying to deduplicate it anymore */
+ } else {
+ e_error(file->file.event, "%s",
+ fs_file_last_error(file->hash_file));
+ }
+ }
+ return;
+ }
+
+ temp_path = t_str_new(256);
+ hash_fname = strrchr(file->hash_path, '/');
+ if (hash_fname == NULL)
+ hash_fname = file->hash_path;
+ else {
+ str_append_data(temp_path, file->hash_path,
+ (hash_fname-file->hash_path) + 1);
+ hash_fname++;
+ }
+ str_printfa(temp_path, "%s%s.tmp",
+ super_fs->set.temp_file_prefix, hash_fname);
+
+ /* replace existing hash file atomically */
+ temp_file = fs_file_init_parent(&file->file, str_c(temp_path),
+ FS_OPEN_MODE_READONLY, 0);
+ ret = fs_copy(file->file.parent, temp_file);
+ if (ret < 0 && errno == EEXIST) {
+ /* either someone's racing us or it's a stale file.
+ try to continue. */
+ if (fs_delete(temp_file) < 0 &&
+ errno != ENOENT)
+ e_error(file->file.event, "%s", fs_file_last_error(temp_file));
+ ret = fs_copy(file->file.parent, temp_file);
+ }
+ if (ret < 0) {
+ e_error(file->file.event, "%s", fs_file_last_error(temp_file));
+ fs_file_deinit(&temp_file);
+ return;
+ }
+
+ if (fs_rename(temp_file, file->hash_file) < 0) {
+ if (errno == ENOENT) {
+ /* apparently someone else just renamed it. ignore. */
+ } else {
+ e_error(file->file.event, "%s",
+ fs_file_last_error(file->hash_file));
+ }
+ (void)fs_delete(temp_file);
+ }
+ fs_file_deinit(&temp_file);
+}
+
+static int fs_sis_write(struct fs_file *_file, const void *data, size_t size)
+{
+ struct sis_fs_file *file = SIS_FILE(_file);
+
+ if (_file->parent == NULL)
+ return -1;
+
+ if (file->hash_input != NULL &&
+ stream_cmp_block(file->hash_input, data, size) &&
+ i_stream_read_eof(file->hash_input)) {
+ /* try to use existing file */
+ if (fs_sis_try_link(file))
+ return 0;
+ }
+
+ if (fs_write(_file->parent, data, size) < 0)
+ return -1;
+ T_BEGIN {
+ fs_sis_replace_hash_file(file);
+ } T_END;
+ return 0;
+}
+
+static void fs_sis_write_stream(struct fs_file *_file)
+{
+ struct sis_fs_file *file = SIS_FILE(_file);
+
+ i_assert(_file->output == NULL);
+
+ if (_file->parent == NULL) {
+ _file->output = o_stream_create_error_str(EINVAL, "%s",
+ fs_file_last_error(_file));
+ } else {
+ file->fs_output = fs_write_stream(_file->parent);
+ if (file->hash_input == NULL) {
+ _file->output = file->fs_output;
+ o_stream_ref(_file->output);
+ } else {
+ /* compare if files are equal */
+ _file->output = o_stream_create_cmp(file->fs_output,
+ file->hash_input);
+ }
+ }
+ o_stream_set_name(_file->output, _file->path);
+}
+
+static int fs_sis_write_stream_finish(struct fs_file *_file, bool success)
+{
+ struct sis_fs_file *file = SIS_FILE(_file);
+
+ if (!success) {
+ if (_file->parent != NULL)
+ fs_write_stream_abort_parent(_file, &file->fs_output);
+ o_stream_unref(&_file->output);
+ return -1;
+ }
+
+ if (file->hash_input != NULL &&
+ o_stream_cmp_equals(_file->output) &&
+ i_stream_read_eof(file->hash_input)) {
+ o_stream_unref(&_file->output);
+ if (fs_sis_try_link(file)) {
+ fs_write_stream_abort_parent(_file, &file->fs_output);
+ return 1;
+ }
+ }
+ if (_file->output != NULL)
+ o_stream_unref(&_file->output);
+
+ if (fs_write_stream_finish(_file->parent, &file->fs_output) < 0)
+ return -1;
+ T_BEGIN {
+ fs_sis_replace_hash_file(file);
+ } T_END;
+ return 1;
+}
+
+static int fs_sis_delete(struct fs_file *_file)
+{
+ T_BEGIN {
+ fs_sis_try_unlink_hash_file(_file, _file->parent);
+ } T_END;
+ return fs_delete(_file->parent);
+}
+
+const struct fs fs_class_sis = {
+ .name = "sis",
+ .v = {
+ fs_sis_alloc,
+ fs_sis_init,
+ NULL,
+ fs_sis_free,
+ fs_wrapper_get_properties,
+ fs_sis_file_alloc,
+ fs_sis_file_init,
+ fs_sis_file_deinit,
+ fs_sis_file_close,
+ fs_wrapper_file_get_path,
+ fs_wrapper_set_async_callback,
+ fs_wrapper_wait_async,
+ fs_wrapper_set_metadata,
+ fs_wrapper_get_metadata,
+ fs_wrapper_prefetch,
+ fs_wrapper_read,
+ fs_wrapper_read_stream,
+ fs_sis_write,
+ fs_sis_write_stream,
+ fs_sis_write_stream_finish,
+ fs_wrapper_lock,
+ fs_wrapper_unlock,
+ fs_wrapper_exists,
+ fs_wrapper_stat,
+ fs_wrapper_copy,
+ fs_wrapper_rename,
+ fs_sis_delete,
+ fs_wrapper_iter_alloc,
+ fs_wrapper_iter_init,
+ NULL,
+ NULL,
+ NULL,
+ fs_wrapper_get_nlinks,
+ }
+};
diff --git a/src/lib-fs/fs-test-async.c b/src/lib-fs/fs-test-async.c
new file mode 100644
index 0000000..a8cd3f5
--- /dev/null
+++ b/src/lib-fs/fs-test-async.c
@@ -0,0 +1,103 @@
+/* Copyright (c) 2016-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "str.h"
+#include "ostream.h"
+#include "fs-test.h"
+#include "test-common.h"
+
+static void test_fs_async_write(const char *test_name, struct fs *fs)
+{
+ struct fs_file *file;
+ struct test_fs_file *test_file;
+ struct ostream *output;
+ unsigned int i;
+ int ret;
+
+ test_begin(t_strdup_printf("%s: async write", test_name));
+ for (i = 0; i < 3; i++) {
+ file = fs_file_init(fs, "foo", FS_OPEN_MODE_REPLACE |
+ FS_OPEN_FLAG_ASYNC);
+ output = fs_write_stream(file);
+
+ o_stream_nsend_str(output, "12345");
+ if (i < 2) {
+ test_assert(fs_write_stream_finish(file, &output) == 0);
+ test_assert(output == NULL);
+ test_assert(fs_write_stream_finish_async(file) == 0);
+ }
+
+ test_file = test_fs_file_get(fs, "foo");
+ test_file->wait_async = FALSE;
+
+ switch (i) {
+ case 0:
+ while ((ret = fs_write_stream_finish_async(file)) == 0)
+ fs_wait_async(fs);
+ test_assert(ret > 0);
+ test_assert(test_file->contents->used > 0);
+ break;
+ case 1:
+ test_file->io_failure = TRUE;
+ test_assert(fs_write_stream_finish_async(file) < 0);
+ test_assert(test_file->contents->used == 0);
+ break;
+ case 2:
+ fs_write_stream_abort_error(file, &output, "test");
+ test_assert(test_file->contents->used == 0);
+ break;
+ }
+ fs_file_deinit(&file);
+ }
+ test_end();
+}
+
+static void test_fs_async_copy(const char *test_name, struct fs *fs)
+{
+ struct fs_file *src, *dest;
+ struct test_fs_file *test_file;
+ int ret;
+
+ test_begin(t_strdup_printf("%s: async copy", test_name));
+
+ src = fs_file_init(fs, "foo", FS_OPEN_MODE_REPLACE);
+ test_assert(fs_write(src, "source", 6) == 0);
+
+ dest = fs_file_init(fs, "bar", FS_OPEN_MODE_REPLACE |
+ FS_OPEN_FLAG_ASYNC);
+
+ test_assert(fs_copy(src, dest) == -1 && errno == EAGAIN);
+
+ test_file = test_fs_file_get(fs, "bar");
+ test_file->wait_async = FALSE;
+
+ while ((ret = fs_copy_finish_async(dest)) < 0 && errno == EAGAIN)
+ fs_wait_async(fs);
+ test_assert(ret == 0);
+ test_assert(test_file->contents->used > 0);
+ fs_file_deinit(&dest);
+
+ fs_file_deinit(&src);
+ test_end();
+}
+
+void test_fs_async(const char *test_name, enum fs_properties properties,
+ const char *driver, const char *args)
+{
+ struct fs_settings fs_set;
+ struct fs *fs;
+ struct test_fs *test_fs;
+ const char *error;
+
+ i_zero(&fs_set);
+ if (fs_init(driver, args, &fs_set, &fs, &error) < 0)
+ i_fatal("fs_init() failed: %s", error);
+
+ test_fs = test_fs_get(fs);
+ test_fs->properties = properties;
+
+ test_fs_async_write(test_name, fs);
+ test_fs_async_copy(test_name, fs);
+
+ fs_deinit(&fs);
+}
diff --git a/src/lib-fs/fs-test.c b/src/lib-fs/fs-test.c
new file mode 100644
index 0000000..d352718
--- /dev/null
+++ b/src/lib-fs/fs-test.c
@@ -0,0 +1,443 @@
+/* Copyright (c) 2016-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "istream.h"
+#include "ostream.h"
+#include "test-common.h"
+#include "fs-test.h"
+
+static struct fs *fs_test_alloc(void)
+{
+ struct test_fs *fs;
+
+ fs = i_new(struct test_fs, 1);
+ fs->fs = fs_class_test;
+ i_array_init(&fs->iter_files, 32);
+ return &fs->fs;
+}
+
+static int
+fs_test_init(struct fs *_fs ATTR_UNUSED, const char *args ATTR_UNUSED,
+ const struct fs_settings *set ATTR_UNUSED,
+ const char **error_r ATTR_UNUSED)
+{
+ return 0;
+}
+
+static void fs_test_free(struct fs *_fs)
+{
+ struct test_fs *fs = (struct test_fs *)_fs;
+
+ array_free(&fs->iter_files);
+ i_free(fs);
+}
+
+static enum fs_properties fs_test_get_properties(struct fs *_fs)
+{
+ struct test_fs *fs = (struct test_fs *)_fs;
+
+ return fs->properties;
+}
+
+static struct fs_file *fs_test_file_alloc(void)
+{
+ struct test_fs_file *file = i_new(struct test_fs_file, 1);
+ return &file->file;
+}
+
+static void
+fs_test_file_init(struct fs_file *_file, const char *path,
+ enum fs_open_mode mode, enum fs_open_flags flags)
+{
+ struct test_fs_file *file = (struct test_fs_file *)_file;
+
+ file->file.path = i_strdup(path);
+ file->file.flags = flags;
+ file->mode = mode;
+ file->contents = buffer_create_dynamic(default_pool, 1024);
+ file->exists = TRUE;
+ file->seekable = TRUE;
+ file->wait_async = (flags & FS_OPEN_FLAG_ASYNC) != 0;
+}
+
+static void fs_test_file_deinit(struct fs_file *_file)
+{
+ struct test_fs_file *file = (struct test_fs_file *)_file;
+
+ fs_file_free(_file);
+ buffer_free(&file->contents);
+ i_free(file->file.path);
+ i_free(file);
+}
+
+static void fs_test_file_close(struct fs_file *_file)
+{
+ struct test_fs_file *file = (struct test_fs_file *)_file;
+
+ file->closed = TRUE;
+}
+
+static const char *fs_test_file_get_path(struct fs_file *_file)
+{
+ return _file->path;
+}
+
+static void
+fs_test_set_async_callback(struct fs_file *_file,
+ fs_file_async_callback_t *callback,
+ void *context)
+{
+ struct test_fs_file *file = (struct test_fs_file *)_file;
+
+ file->async_callback = callback;
+ file->async_context = context;
+}
+
+static void fs_test_wait_async(struct fs *_fs ATTR_UNUSED)
+{
+}
+
+static void
+fs_test_set_metadata(struct fs_file *_file, const char *key,
+ const char *value)
+{
+ if (strcmp(key, FS_METADATA_WRITE_FNAME) == 0) {
+ i_free(_file->path);
+ _file->path = i_strdup(value);
+ } else {
+ fs_default_set_metadata(_file, key, value);
+ }
+}
+
+static int
+fs_test_get_metadata(struct fs_file *_file,
+ enum fs_get_metadata_flags flags,
+ const ARRAY_TYPE(fs_metadata) **metadata_r)
+{
+ struct test_fs_file *file = (struct test_fs_file *)_file;
+
+ if ((flags & FS_GET_METADATA_FLAG_LOADED_ONLY) != 0) {
+ *metadata_r = &_file->metadata;
+ return 0;
+ }
+
+ if (file->wait_async) {
+ fs_file_set_error_async(_file);
+ return -1;
+ }
+ if (file->io_failure) {
+ errno = EIO;
+ return -1;
+ }
+ fs_metadata_init(_file);
+ *metadata_r = &_file->metadata;
+ return 0;
+}
+
+static bool fs_test_prefetch(struct fs_file *_file ATTR_UNUSED,
+ uoff_t length ATTR_UNUSED)
+{
+ struct test_fs_file *file = (struct test_fs_file *)_file;
+
+ file->prefetched = TRUE;
+ return TRUE;
+}
+
+static void fs_test_stream_destroyed(struct test_fs_file *file)
+{
+ i_assert(file->input != NULL);
+ file->input = NULL;
+}
+
+static struct istream *
+fs_test_read_stream(struct fs_file *_file, size_t max_buffer_size ATTR_UNUSED)
+{
+ struct test_fs_file *file = (struct test_fs_file *)_file;
+ struct istream *input;
+
+ i_assert(file->input == NULL);
+
+ if (!file->exists)
+ return i_stream_create_error(ENOENT);
+ if (file->io_failure)
+ return i_stream_create_error(EIO);
+ input = test_istream_create_data(file->contents->data,
+ file->contents->used);
+ i_stream_add_destroy_callback(input, fs_test_stream_destroyed, file);
+ if (!file->seekable)
+ input->seekable = FALSE;
+ file->input = input;
+ return input;
+}
+
+static void fs_test_write_stream(struct fs_file *_file)
+{
+ struct test_fs_file *file = (struct test_fs_file *)_file;
+
+ i_assert(_file->output == NULL);
+
+ buffer_set_used_size(file->contents, 0);
+ _file->output = o_stream_create_buffer(file->contents);
+}
+
+static int fs_test_write_stream_finish(struct fs_file *_file, bool success)
+{
+ struct test_fs_file *file = (struct test_fs_file *)_file;
+
+ o_stream_destroy(&_file->output);
+ if (file->wait_async) {
+ fs_file_set_error_async(_file);
+ return 0;
+ }
+ if (file->io_failure)
+ success = FALSE;
+ if (!success)
+ buffer_set_used_size(file->contents, 0);
+ return success ? 1 : -1;
+}
+
+static int
+fs_test_lock(struct fs_file *_file, unsigned int secs ATTR_UNUSED,
+ struct fs_lock **lock_r)
+{
+ struct test_fs_file *file = (struct test_fs_file *)_file;
+
+ if (file->locked)
+ return 0;
+ file->locked = TRUE;
+ *lock_r = i_new(struct fs_lock, 1);
+ (*lock_r)->file = _file;
+ return 1;
+}
+
+static void fs_test_unlock(struct fs_lock *lock)
+{
+ struct test_fs_file *file = (struct test_fs_file *)lock->file;
+
+ file->locked = FALSE;
+ i_free(lock);
+}
+
+static int fs_test_exists(struct fs_file *_file)
+{
+ struct test_fs_file *file = (struct test_fs_file *)_file;
+
+ if (file->wait_async) {
+ fs_file_set_error_async(_file);
+ return -1;
+ }
+ if (file->io_failure) {
+ errno = EIO;
+ return -1;
+ }
+ return file->exists ? 1 : 0;
+}
+
+static int fs_test_stat(struct fs_file *_file, struct stat *st_r)
+{
+ struct test_fs_file *file = (struct test_fs_file *)_file;
+
+ if (file->wait_async) {
+ fs_file_set_error_async(_file);
+ return -1;
+ }
+ if (file->io_failure) {
+ errno = EIO;
+ return -1;
+ }
+ if (!file->exists) {
+ errno = ENOENT;
+ return -1;
+ }
+ i_zero(st_r);
+ st_r->st_size = file->contents->used;
+ return 0;
+}
+
+static int fs_test_copy(struct fs_file *_src, struct fs_file *_dest)
+{
+ struct test_fs_file *src;
+ struct test_fs_file *dest = (struct test_fs_file *)_dest;
+
+ if (_src != NULL)
+ dest->copy_src = test_fs_file_get(_src->fs, fs_file_path(_src));
+ src = dest->copy_src;
+ if (dest->wait_async) {
+ fs_file_set_error_async(_dest);
+ return -1;
+ }
+ dest->copy_src = NULL;
+
+ if (dest->io_failure) {
+ errno = EIO;
+ return -1;
+ }
+ if (!src->exists) {
+ errno = ENOENT;
+ return -1;
+ }
+ buffer_set_used_size(dest->contents, 0);
+ buffer_append_buf(dest->contents, src->contents, 0, SIZE_MAX);
+ dest->exists = TRUE;
+ return 0;
+}
+
+static int fs_test_rename(struct fs_file *_src, struct fs_file *_dest)
+{
+ struct test_fs_file *src = (struct test_fs_file *)_src;
+ struct test_fs_file *dest = (struct test_fs_file *)_dest;
+
+ if (src->wait_async || dest->wait_async) {
+ fs_file_set_error_async(_dest);
+ return -1;
+ }
+
+ if (fs_test_copy(_src, _dest) < 0)
+ return -1;
+ src->exists = FALSE;
+ return 0;
+}
+
+static int fs_test_delete(struct fs_file *_file)
+{
+ struct test_fs_file *file = (struct test_fs_file *)_file;
+
+ if (file->wait_async) {
+ fs_file_set_error_async(_file);
+ return -1;
+ }
+
+ if (!file->exists) {
+ errno = ENOENT;
+ return -1;
+ }
+ return 0;
+}
+
+static struct fs_iter *fs_test_iter_alloc(void)
+{
+ struct test_fs_iter *iter = i_new(struct test_fs_iter, 1);
+ return &iter->iter;
+}
+
+static void
+fs_test_iter_init(struct fs_iter *_iter, const char *path,
+ enum fs_iter_flags flags ATTR_UNUSED)
+{
+ struct test_fs_iter *iter = (struct test_fs_iter *)_iter;
+ struct test_fs *fs = (struct test_fs *)_iter->fs;
+
+ iter->prefix = i_strdup(path);
+ iter->prefix_len = strlen(iter->prefix);
+ iter->prev_dir = i_strdup("");
+ array_sort(&fs->iter_files, i_strcmp_p);
+}
+
+static const char *fs_test_iter_next(struct fs_iter *_iter)
+{
+ struct test_fs_iter *iter = (struct test_fs_iter *)_iter;
+ struct test_fs *fs = (struct test_fs *)_iter->fs;
+ const char *const *files, *p;
+ unsigned int count;
+ size_t len, prev_dir_len = strlen(iter->prev_dir);
+
+ files = array_get(&fs->iter_files, &count);
+ for (; iter->idx < count; iter->idx++) {
+ const char *fname = files[iter->idx];
+
+ if (strncmp(fname, iter->prefix, iter->prefix_len) != 0)
+ continue;
+ p = strrchr(fname, '/');
+ if ((_iter->flags & FS_ITER_FLAG_DIRS) == 0) {
+ if (p == NULL)
+ return fname;
+ if (p[1] == '\0')
+ continue; /* dir/ */
+ return p+1;
+ }
+
+ if (p == NULL)
+ continue;
+ len = p - fname;
+ if (len == 0)
+ continue;
+ if (len == prev_dir_len &&
+ strncmp(fname, iter->prev_dir, len) == 0)
+ continue;
+ i_free(iter->prev_dir);
+ iter->prev_dir = i_strndup(fname, len);
+ return iter->prev_dir;
+ }
+ return NULL;
+}
+
+static int fs_test_iter_deinit(struct fs_iter *_iter)
+{
+ struct test_fs_iter *iter = (struct test_fs_iter *)_iter;
+ int ret = iter->failed ? -1 : 0;
+
+ i_free(iter->prefix);
+ return ret;
+}
+
+struct test_fs *test_fs_get(struct fs *fs)
+{
+ while (strcmp(fs->name, "test") != 0) {
+ i_assert(fs->parent != NULL);
+ fs = fs->parent;
+ }
+ return (struct test_fs *)fs;
+}
+
+struct test_fs_file *test_fs_file_get(struct fs *fs, const char *path)
+{
+ struct fs_file *file;
+
+ fs = &test_fs_get(fs)->fs;
+
+ for (file = fs->files;; file = file->next) {
+ i_assert(file != NULL);
+ if (strcmp(fs_file_path(file), path) == 0)
+ break;
+ }
+ return (struct test_fs_file *)file;
+}
+
+const struct fs fs_class_test = {
+ .name = "test",
+ .v = {
+ fs_test_alloc,
+ fs_test_init,
+ NULL,
+ fs_test_free,
+ fs_test_get_properties,
+ fs_test_file_alloc,
+ fs_test_file_init,
+ fs_test_file_deinit,
+ fs_test_file_close,
+ fs_test_file_get_path,
+ fs_test_set_async_callback,
+ fs_test_wait_async,
+ fs_test_set_metadata,
+ fs_test_get_metadata,
+ fs_test_prefetch,
+ NULL,
+ fs_test_read_stream,
+ NULL,
+ fs_test_write_stream,
+ fs_test_write_stream_finish,
+ fs_test_lock,
+ fs_test_unlock,
+ fs_test_exists,
+ fs_test_stat,
+ fs_test_copy,
+ fs_test_rename,
+ fs_test_delete,
+ fs_test_iter_alloc,
+ fs_test_iter_init,
+ fs_test_iter_next,
+ fs_test_iter_deinit,
+ NULL,
+ NULL,
+ }
+};
diff --git a/src/lib-fs/fs-test.h b/src/lib-fs/fs-test.h
new file mode 100644
index 0000000..5a2ed14
--- /dev/null
+++ b/src/lib-fs/fs-test.h
@@ -0,0 +1,45 @@
+#ifndef FS_TEST_H
+#define FS_TEST_H
+
+#include "fs-api-private.h"
+
+struct test_fs {
+ struct fs fs;
+ enum fs_properties properties;
+ ARRAY_TYPE(const_string) iter_files;
+};
+
+struct test_fs_file {
+ struct fs_file file;
+ enum fs_open_mode mode;
+
+ fs_file_async_callback_t *async_callback;
+ void *async_context;
+
+ buffer_t *contents;
+ struct istream *input;
+ struct test_fs_file *copy_src;
+
+ bool prefetched;
+ bool locked;
+ bool exists;
+ bool seekable;
+ bool closed;
+ bool io_failure;
+ bool wait_async;
+};
+
+struct test_fs_iter {
+ struct fs_iter iter;
+ char *prefix, *prev_dir;
+ unsigned int prefix_len, idx;
+ bool failed;
+};
+
+struct test_fs *test_fs_get(struct fs *fs);
+struct test_fs_file *test_fs_file_get(struct fs *fs, const char *path);
+
+void test_fs_async(const char *test_name, enum fs_properties properties,
+ const char *driver, const char *args);
+
+#endif
diff --git a/src/lib-fs/fs-wrapper.c b/src/lib-fs/fs-wrapper.c
new file mode 100644
index 0000000..81d746e
--- /dev/null
+++ b/src/lib-fs/fs-wrapper.c
@@ -0,0 +1,172 @@
+/* Copyright (c) 2016-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "fs-api-private.h"
+#include "ostream.h"
+
+struct wrapper_fs_iter {
+ struct fs_iter iter;
+ struct fs_iter *parent;
+};
+
+enum fs_properties fs_wrapper_get_properties(struct fs *fs)
+{
+ return fs_get_properties(fs->parent);
+}
+
+void fs_wrapper_file_close(struct fs_file *file)
+{
+ fs_file_close(file->parent);
+}
+
+const char *fs_wrapper_file_get_path(struct fs_file *file)
+{
+ return fs_file_path(file->parent);
+}
+
+void fs_wrapper_set_async_callback(struct fs_file *file,
+ fs_file_async_callback_t *callback,
+ void *context)
+{
+ fs_file_set_async_callback(file->parent, *callback, context);
+}
+
+void fs_wrapper_wait_async(struct fs *fs)
+{
+ fs_wait_async(fs->parent);
+}
+
+void fs_wrapper_set_metadata(struct fs_file *file, const char *key,
+ const char *value)
+{
+ fs_set_metadata(file->parent, key, value);
+}
+
+int fs_wrapper_get_metadata(struct fs_file *file,
+ enum fs_get_metadata_flags flags,
+ const ARRAY_TYPE(fs_metadata) **metadata_r)
+{
+ return fs_get_metadata_full(file->parent, flags, metadata_r);
+}
+
+bool fs_wrapper_prefetch(struct fs_file *file, uoff_t length)
+{
+ return fs_prefetch(file->parent, length);
+}
+
+ssize_t fs_wrapper_read(struct fs_file *file, void *buf, size_t size)
+{
+ return fs_read(file->parent, buf, size);
+}
+
+struct istream *
+fs_wrapper_read_stream(struct fs_file *file, size_t max_buffer_size)
+{
+ return fs_read_stream(file->parent, max_buffer_size);
+}
+
+int fs_wrapper_write(struct fs_file *file, const void *data, size_t size)
+{
+ return fs_write(file->parent, data, size);
+}
+
+void fs_wrapper_write_stream(struct fs_file *file)
+{
+ i_assert(file->output == NULL);
+
+ file->output = fs_write_stream(file->parent);
+}
+
+int fs_wrapper_write_stream_finish(struct fs_file *file, bool success)
+{
+ if (file->output == NULL)
+ return fs_write_stream_finish_async(file->parent);
+
+ if (!success) {
+ fs_write_stream_abort_parent(file, &file->output);
+ return -1;
+ }
+ return fs_write_stream_finish(file->parent, &file->output);
+}
+
+int fs_wrapper_lock(struct fs_file *file, unsigned int secs,
+ struct fs_lock **lock_r)
+{
+ return fs_lock(file->parent, secs, lock_r);
+}
+
+void fs_wrapper_unlock(struct fs_lock *_lock ATTR_UNUSED)
+{
+ i_unreached();
+}
+
+int fs_wrapper_exists(struct fs_file *file)
+{
+ return fs_exists(file->parent);
+}
+
+int fs_wrapper_stat(struct fs_file *file, struct stat *st_r)
+{
+ return fs_stat(file->parent, st_r);
+}
+
+int fs_wrapper_get_nlinks(struct fs_file *file, nlink_t *nlinks_r)
+{
+ return fs_get_nlinks(file->parent, nlinks_r);
+}
+
+int fs_wrapper_copy(struct fs_file *src, struct fs_file *dest)
+{
+ if (src != NULL)
+ return fs_copy(src->parent, dest->parent);
+ else
+ return fs_copy_finish_async(dest->parent);
+}
+
+int fs_wrapper_rename(struct fs_file *src, struct fs_file *dest)
+{
+ return fs_rename(src->parent, dest->parent);
+}
+
+int fs_wrapper_delete(struct fs_file *file)
+{
+ return fs_delete(file->parent);
+}
+
+struct fs_iter *fs_wrapper_iter_alloc(void)
+{
+ struct wrapper_fs_iter *iter = i_new(struct wrapper_fs_iter, 1);
+ return &iter->iter;
+}
+
+void fs_wrapper_iter_init(struct fs_iter *_iter, const char *path,
+ enum fs_iter_flags flags)
+{
+ struct wrapper_fs_iter *iter = (struct wrapper_fs_iter *)_iter;
+
+ iter->parent = fs_iter_init_parent(_iter, path, flags);
+}
+
+const char *fs_wrapper_iter_next(struct fs_iter *_iter)
+{
+ struct wrapper_fs_iter *iter = (struct wrapper_fs_iter *)_iter;
+ const char *fname;
+
+ iter->parent->async_callback = _iter->async_callback;
+ iter->parent->async_context = _iter->async_context;
+
+ fname = fs_iter_next(iter->parent);
+ _iter->async_have_more = iter->parent->async_have_more;
+ return fname;
+}
+
+int fs_wrapper_iter_deinit(struct fs_iter *_iter)
+{
+ struct wrapper_fs_iter *iter = (struct wrapper_fs_iter *)_iter;
+ const char *error;
+ int ret;
+
+ if ((ret = fs_iter_deinit(&iter->parent, &error)) < 0)
+ fs_set_error_errno(_iter->event, "%s", error);
+ return ret;
+}
diff --git a/src/lib-fs/fs-wrapper.h b/src/lib-fs/fs-wrapper.h
new file mode 100644
index 0000000..ebfd35d
--- /dev/null
+++ b/src/lib-fs/fs-wrapper.h
@@ -0,0 +1,40 @@
+#ifndef FS_WRAPPER_H
+#define FS_WRAPPER_H
+
+enum fs_get_metadata_flags;
+
+enum fs_properties fs_wrapper_get_properties(struct fs *fs);
+void fs_wrapper_file_close(struct fs_file *file);
+const char *fs_wrapper_file_get_path(struct fs_file *file);
+void fs_wrapper_set_async_callback(struct fs_file *file,
+ fs_file_async_callback_t *callback,
+ void *context);
+void fs_wrapper_wait_async(struct fs *fs);
+void fs_wrapper_set_metadata(struct fs_file *file, const char *key,
+ const char *value);
+int fs_wrapper_get_metadata(struct fs_file *file,
+ enum fs_get_metadata_flags flags,
+ const ARRAY_TYPE(fs_metadata) **metadata_r);
+bool fs_wrapper_prefetch(struct fs_file *file, uoff_t length);
+ssize_t fs_wrapper_read(struct fs_file *file, void *buf, size_t size);
+struct istream *
+fs_wrapper_read_stream(struct fs_file *file, size_t max_buffer_size);
+int fs_wrapper_write(struct fs_file *file, const void *data, size_t size);
+void fs_wrapper_write_stream(struct fs_file *file);
+int fs_wrapper_write_stream_finish(struct fs_file *file, bool success);
+int fs_wrapper_lock(struct fs_file *file, unsigned int secs,
+ struct fs_lock **lock_r);
+void fs_wrapper_unlock(struct fs_lock *_lock);
+int fs_wrapper_exists(struct fs_file *file);
+int fs_wrapper_stat(struct fs_file *file, struct stat *st_r);
+int fs_wrapper_get_nlinks(struct fs_file *file, nlink_t *nlinks_r);
+int fs_wrapper_copy(struct fs_file *src, struct fs_file *dest);
+int fs_wrapper_rename(struct fs_file *src, struct fs_file *dest);
+int fs_wrapper_delete(struct fs_file *file);
+struct fs_iter *fs_wrapper_iter_alloc(void);
+void fs_wrapper_iter_init(struct fs_iter *iter, const char *path,
+ enum fs_iter_flags flags);
+const char *fs_wrapper_iter_next(struct fs_iter *iter);
+int fs_wrapper_iter_deinit(struct fs_iter *iter);
+
+#endif
diff --git a/src/lib-fs/istream-fs-file.c b/src/lib-fs/istream-fs-file.c
new file mode 100644
index 0000000..4f61cfe
--- /dev/null
+++ b/src/lib-fs/istream-fs-file.c
@@ -0,0 +1,61 @@
+/* Copyright (c) 2013-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "istream-private.h"
+#include "fs-api-private.h"
+#include "istream-fs-file.h"
+
+struct fs_file_istream {
+ struct istream_private istream;
+ struct fs_file *file;
+};
+
+static void i_stream_fs_file_close(struct iostream_private *stream,
+ bool close_parent ATTR_UNUSED)
+{
+ struct fs_file_istream *fstream = (struct fs_file_istream *)stream;
+
+ i_stream_destroy(&fstream->istream.parent);
+ fs_file_deinit(&fstream->file);
+}
+
+static ssize_t i_stream_fs_file_read(struct istream_private *stream)
+{
+ struct fs_file_istream *fstream = (struct fs_file_istream *)stream;
+ struct istream *input;
+
+ if (fstream->istream.parent == NULL) {
+ input = fs_read_stream(fstream->file,
+ i_stream_get_max_buffer_size(&stream->istream));
+ i_stream_init_parent(stream, input);
+ i_stream_unref(&input);
+ }
+
+ i_stream_seek(stream->parent, stream->parent_start_offset +
+ stream->istream.v_offset);
+ return i_stream_read_copy_from_parent(&stream->istream);
+}
+
+struct istream *
+i_stream_create_fs_file(struct fs_file **file, size_t max_buffer_size)
+{
+ struct fs_file_istream *fstream;
+ struct istream *input;
+
+ fstream = i_new(struct fs_file_istream, 1);
+ fstream->file = *file;
+ fstream->istream.iostream.close = i_stream_fs_file_close;
+ fstream->istream.max_buffer_size = max_buffer_size;
+ fstream->istream.read = i_stream_fs_file_read;
+ fstream->istream.stream_size_passthrough = TRUE;
+
+ fstream->istream.istream.blocking =
+ ((*file)->flags & FS_OPEN_FLAG_ASYNC) == 0;
+ fstream->istream.istream.seekable =
+ ((*file)->flags & FS_OPEN_FLAG_SEEKABLE) != 0;
+
+ input = i_stream_create(&fstream->istream, NULL, -1, 0);
+ i_stream_set_name(input, fs_file_path(*file));
+ *file = NULL;
+ return input;
+}
diff --git a/src/lib-fs/istream-fs-file.h b/src/lib-fs/istream-fs-file.h
new file mode 100644
index 0000000..2821c0e
--- /dev/null
+++ b/src/lib-fs/istream-fs-file.h
@@ -0,0 +1,13 @@
+#ifndef ISTREAM_FS_FILE_H
+#define ISTREAM_FS_FILE_H
+
+struct fs_file;
+
+/* Open the given file only when something is actually tried to be read from
+ the stream. The file is automatically deinitialized when the stream is
+ destroyed (which is why it's also set to NULL so it's not accidentally
+ double-freed). */
+struct istream *
+i_stream_create_fs_file(struct fs_file **file, size_t max_buffer_size);
+
+#endif
diff --git a/src/lib-fs/istream-fs-stats.c b/src/lib-fs/istream-fs-stats.c
new file mode 100644
index 0000000..3f77430
--- /dev/null
+++ b/src/lib-fs/istream-fs-stats.c
@@ -0,0 +1,47 @@
+/* Copyright (c) 2015-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "fs-api-private.h"
+#include "istream-private.h"
+#include "istream-fs-stats.h"
+
+struct fs_stats_istream {
+ struct istream_private istream;
+ struct fs_file *file;
+};
+
+static ssize_t
+i_stream_fs_stats_read(struct istream_private *stream)
+{
+ struct fs_stats_istream *sstream = (struct fs_stats_istream *)stream;
+ ssize_t ret;
+
+ i_stream_seek(stream->parent, stream->parent_start_offset +
+ stream->istream.v_offset);
+
+ ret = i_stream_read_copy_from_parent(&stream->istream);
+ if (ret > 0) {
+ /* count the first returned bytes as the finish time, since
+ we don't want to count the time caller spends on processing
+ this stream. (only the first fs_file_timing_end() call
+ actually does anything - the others are ignored.) */
+ fs_file_timing_end(sstream->file, FS_OP_READ);
+ }
+ return ret;
+}
+
+struct istream *
+i_stream_create_fs_stats(struct istream *input, struct fs_file *file)
+{
+ struct fs_stats_istream *sstream;
+
+ sstream = i_new(struct fs_stats_istream, 1);
+ sstream->file = file;
+ sstream->istream.max_buffer_size = input->real_stream->max_buffer_size;
+ sstream->istream.stream_size_passthrough = TRUE;
+ sstream->istream.read = i_stream_fs_stats_read;
+ sstream->istream.istream.blocking = input->blocking;
+ sstream->istream.istream.seekable = input->seekable;
+ return i_stream_create(&sstream->istream, input,
+ i_stream_get_fd(input), 0);
+}
diff --git a/src/lib-fs/istream-fs-stats.h b/src/lib-fs/istream-fs-stats.h
new file mode 100644
index 0000000..aca74b6
--- /dev/null
+++ b/src/lib-fs/istream-fs-stats.h
@@ -0,0 +1,9 @@
+#ifndef ISTREAM_FS_STATS_H
+#define ISTREAM_FS_STATS_H
+
+struct fs_file;
+
+struct istream *
+i_stream_create_fs_stats(struct istream *input, struct fs_file *file);
+
+#endif
diff --git a/src/lib-fs/istream-metawrap.c b/src/lib-fs/istream-metawrap.c
new file mode 100644
index 0000000..ac2e8fb
--- /dev/null
+++ b/src/lib-fs/istream-metawrap.c
@@ -0,0 +1,152 @@
+/* Copyright (c) 2007-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "istream-private.h"
+#include "istream-metawrap.h"
+
+#define METAWRAP_MAX_METADATA_LINE_LEN 8192
+
+struct metawrap_istream {
+ struct istream_private istream;
+ metawrap_callback_t *callback;
+ void *context;
+
+ uoff_t start_offset, pending_seek;
+ bool in_metadata;
+};
+
+static int metadata_header_read(struct metawrap_istream *mstream)
+{
+ char *line, *p;
+
+ while ((line = i_stream_read_next_line(mstream->istream.parent)) != NULL) {
+ if (*line == '\0') {
+ mstream->callback(NULL, NULL, mstream->context);
+ return 1;
+ }
+ p = strchr(line, ':');
+ if (p == NULL) {
+ io_stream_set_error(&mstream->istream.iostream,
+ "Metadata header line is missing ':' at offset %"PRIuUOFF_T,
+ mstream->istream.istream.v_offset);
+ mstream->istream.istream.stream_errno = EINVAL;
+ return -1;
+ }
+ *p++ = '\0';
+ mstream->callback(line, p, mstream->context);
+ }
+ if (mstream->istream.parent->eof) {
+ if (mstream->istream.parent->stream_errno != 0) {
+ mstream->istream.istream.stream_errno =
+ mstream->istream.parent->stream_errno;
+ } else {
+ io_stream_set_error(&mstream->istream.iostream,
+ "Metadata header is missing ending line at offset %"PRIuUOFF_T,
+ mstream->istream.istream.v_offset);
+ mstream->istream.istream.stream_errno = EPIPE;
+ return -1;
+ }
+ mstream->istream.istream.eof = TRUE;
+ return -1;
+ }
+ i_assert(!mstream->istream.parent->blocking);
+ return 0;
+}
+
+static ssize_t i_stream_metawrap_read(struct istream_private *stream)
+{
+ struct metawrap_istream *mstream = (struct metawrap_istream *)stream;
+ int ret;
+
+ i_stream_seek(stream->parent, mstream->start_offset +
+ stream->istream.v_offset);
+
+ if (mstream->in_metadata) {
+ size_t prev_max_size = i_stream_get_max_buffer_size(stream->parent);
+
+ i_stream_set_max_buffer_size(stream->parent, METAWRAP_MAX_METADATA_LINE_LEN);
+ ret = metadata_header_read(mstream);
+ i_stream_set_max_buffer_size(stream->parent, prev_max_size);
+
+ i_assert(stream->istream.v_offset == 0);
+ mstream->start_offset = stream->parent->v_offset;
+ if (ret <= 0)
+ return ret;
+ /* this stream is kind of silently skipping over the metadata */
+ stream->start_offset += mstream->start_offset;
+ mstream->in_metadata = FALSE;
+ if (mstream->pending_seek != 0) {
+ i_stream_seek(&stream->istream, mstream->pending_seek);
+ return i_stream_read_memarea(&stream->istream);
+ }
+ }
+ /* after metadata header it's all just passthrough */
+ return i_stream_read_copy_from_parent(&stream->istream);
+}
+
+static void
+i_stream_metawrap_seek(struct istream_private *stream,
+ uoff_t v_offset, bool mark ATTR_UNUSED)
+{
+ struct metawrap_istream *mstream = (struct metawrap_istream *)stream;
+
+ if (!mstream->in_metadata) {
+ /* already read through metadata. we can skip directly. */
+ stream->istream.v_offset = v_offset;
+ mstream->pending_seek = 0;
+ } else {
+ /* we need to read through the metadata first */
+ mstream->pending_seek = v_offset;
+ stream->istream.v_offset = 0;
+ }
+ stream->skip = stream->pos = 0;
+}
+
+static int i_stream_metawrap_stat(struct istream_private *stream, bool exact)
+{
+ struct metawrap_istream *mstream = (struct metawrap_istream *)stream;
+ const struct stat *st;
+ int ret;
+
+ if (i_stream_stat(stream->parent, exact, &st) < 0) {
+ stream->istream.stream_errno = stream->parent->stream_errno;
+ return -1;
+ }
+ stream->statbuf = *st;
+
+ if (mstream->in_metadata) {
+ ret = i_stream_read_memarea(&stream->istream);
+ if (ret < 0 && stream->istream.stream_errno != 0)
+ return -1;
+ if (ret == 0) {
+ stream->statbuf.st_size = -1;
+ return 0;
+ }
+ }
+ i_assert((uoff_t)stream->statbuf.st_size >= mstream->start_offset);
+ stream->statbuf.st_size -= mstream->start_offset;
+ return 0;
+}
+
+struct istream *
+i_stream_create_metawrap(struct istream *input,
+ metawrap_callback_t *callback, void *context)
+{
+ struct metawrap_istream *mstream;
+
+ mstream = i_new(struct metawrap_istream, 1);
+ mstream->istream.max_buffer_size = input->real_stream->max_buffer_size;
+
+ mstream->istream.read = i_stream_metawrap_read;
+ mstream->istream.seek = i_stream_metawrap_seek;
+ mstream->istream.stat = input->seekable ? i_stream_metawrap_stat : NULL;
+
+ mstream->istream.istream.readable_fd = input->readable_fd;
+ mstream->istream.istream.blocking = input->blocking;
+ mstream->istream.istream.seekable = input->seekable;
+ mstream->in_metadata = TRUE;
+ mstream->callback = callback;
+ mstream->context = context;
+ return i_stream_create(&mstream->istream, input,
+ i_stream_get_fd(input), 0);
+}
diff --git a/src/lib-fs/istream-metawrap.h b/src/lib-fs/istream-metawrap.h
new file mode 100644
index 0000000..90ca2d3
--- /dev/null
+++ b/src/lib-fs/istream-metawrap.h
@@ -0,0 +1,14 @@
+#ifndef ISTREAM_METAWRAP_H
+#define ISTREAM_METAWRAP_H
+
+typedef void
+metawrap_callback_t(const char *key, const char *value, void *context);
+
+/* Input stream is in format "key:value\nkey2:value2\n...\n\ncontents.
+ The given callback is called for each key/value metadata pair, and the
+ returned stream will skip over the metadata and return only the contents. */
+struct istream *
+i_stream_create_metawrap(struct istream *input,
+ metawrap_callback_t *callback, void *context);
+
+#endif
diff --git a/src/lib-fs/ostream-cmp.c b/src/lib-fs/ostream-cmp.c
new file mode 100644
index 0000000..8599799
--- /dev/null
+++ b/src/lib-fs/ostream-cmp.c
@@ -0,0 +1,95 @@
+/* Copyright (c) 2010-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "istream.h"
+#include "ostream-private.h"
+#include "ostream-cmp.h"
+
+struct cmp_ostream {
+ struct ostream_private ostream;
+
+ struct istream *input;
+ bool equals;
+};
+
+static void o_stream_cmp_close(struct iostream_private *stream,
+ bool close_parent)
+{
+ struct cmp_ostream *cstream = (struct cmp_ostream *)stream;
+
+ if (cstream->input == NULL)
+ return;
+
+ i_stream_unref(&cstream->input);
+ if (close_parent)
+ o_stream_close(cstream->ostream.parent);
+}
+
+bool stream_cmp_block(struct istream *input,
+ const unsigned char *data, size_t size)
+{
+ const unsigned char *indata;
+ size_t insize, max;
+
+ while (size > 0) {
+ (void)i_stream_read_bytes(input, &indata, &insize, size);
+ max = I_MIN(insize, size);
+ if (insize == 0 || memcmp(data, indata, max) != 0)
+ return FALSE;
+ data += max;
+ size -= max;
+ i_stream_skip(input, max);
+ }
+ return TRUE;
+}
+
+static ssize_t
+o_stream_cmp_sendv(struct ostream_private *stream,
+ const struct const_iovec *iov, unsigned int iov_count)
+{
+ struct cmp_ostream *cstream = (struct cmp_ostream *)stream;
+ unsigned int i;
+ ssize_t ret;
+
+ if (cstream->equals) {
+ for (i = 0; i < iov_count; i++) {
+ if (!stream_cmp_block(cstream->input, iov[i].iov_base,
+ iov[i].iov_len)) {
+ cstream->equals = FALSE;
+ break;
+ }
+ }
+ }
+
+ if ((ret = o_stream_sendv(stream->parent, iov, iov_count)) < 0) {
+ o_stream_copy_error_from_parent(stream);
+ return -1;
+ }
+
+ stream->ostream.offset += ret;
+ return ret;
+}
+
+struct ostream *
+o_stream_create_cmp(struct ostream *output, struct istream *input)
+{
+ struct cmp_ostream *cstream;
+
+ cstream = i_new(struct cmp_ostream, 1);
+ cstream->ostream.sendv = o_stream_cmp_sendv;
+ cstream->ostream.iostream.close = o_stream_cmp_close;
+ cstream->input = input;
+ cstream->equals = TRUE;
+ i_stream_ref(input);
+
+ return o_stream_create(&cstream->ostream, output,
+ o_stream_get_fd(output));
+}
+
+bool o_stream_cmp_equals(struct ostream *_output)
+{
+ struct cmp_ostream *cstream =
+ (struct cmp_ostream *)_output->real_stream;
+
+ return cstream->equals;
+}
diff --git a/src/lib-fs/ostream-cmp.h b/src/lib-fs/ostream-cmp.h
new file mode 100644
index 0000000..cd6d567
--- /dev/null
+++ b/src/lib-fs/ostream-cmp.h
@@ -0,0 +1,15 @@
+#ifndef OSTREAM_CMP_H
+#define OSTREAM_CMP_H
+
+/* Compare given input stream to output being written to output stream. */
+struct ostream *
+o_stream_create_cmp(struct ostream *output, struct istream *input);
+/* Returns TRUE if input and output are equal so far. If the caller needs to
+ know if the files are entirely equal, it should check also if input stream
+ is at EOF. */
+bool o_stream_cmp_equals(struct ostream *output);
+
+bool stream_cmp_block(struct istream *input,
+ const unsigned char *data, size_t size);
+
+#endif
diff --git a/src/lib-fs/ostream-metawrap.c b/src/lib-fs/ostream-metawrap.c
new file mode 100644
index 0000000..3359bd3
--- /dev/null
+++ b/src/lib-fs/ostream-metawrap.c
@@ -0,0 +1,71 @@
+/* Copyright (c) 2013-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "istream.h"
+#include "ostream-private.h"
+#include "ostream-metawrap.h"
+
+struct metawrap_ostream {
+ struct ostream_private ostream;
+ void (*write_callback)(void *);
+ void *context;
+};
+
+static void o_stream_metawrap_call_callback(struct metawrap_ostream *mstream)
+{
+ void (*write_callback)(void *) = mstream->write_callback;
+
+ if (write_callback != NULL) {
+ mstream->write_callback = NULL;
+ write_callback(mstream->context);
+ /* metadata headers aren't counted as part of the offset */
+ mstream->ostream.ostream.offset = 0;
+ }
+}
+
+static ssize_t
+o_stream_metawrap_sendv(struct ostream_private *stream,
+ const struct const_iovec *iov, unsigned int iov_count)
+{
+ struct metawrap_ostream *mstream = (struct metawrap_ostream *)stream;
+ ssize_t ret;
+
+ o_stream_metawrap_call_callback(mstream);
+ if ((ret = o_stream_sendv(stream->parent, iov, iov_count)) < 0)
+ o_stream_copy_error_from_parent(stream);
+ else
+ stream->ostream.offset += ret;
+ return ret;
+}
+
+static enum ostream_send_istream_result
+o_stream_metawrap_send_istream(struct ostream_private *_outstream,
+ struct istream *instream)
+{
+ struct metawrap_ostream *outstream =
+ (struct metawrap_ostream *)_outstream;
+ uoff_t orig_instream_offset = instream->v_offset;
+ enum ostream_send_istream_result res;
+
+ o_stream_metawrap_call_callback(outstream);
+ if ((res = o_stream_send_istream(_outstream->parent, instream)) == OSTREAM_SEND_ISTREAM_RESULT_ERROR_OUTPUT)
+ o_stream_copy_error_from_parent(_outstream);
+ _outstream->ostream.offset += instream->v_offset - orig_instream_offset;
+ return res;
+}
+
+struct ostream *
+o_stream_create_metawrap(struct ostream *output,
+ void (*write_callback)(void *), void *context)
+{
+ struct metawrap_ostream *mstream;
+
+ mstream = i_new(struct metawrap_ostream, 1);
+ mstream->ostream.sendv = o_stream_metawrap_sendv;
+ mstream->ostream.send_istream = o_stream_metawrap_send_istream;
+ mstream->write_callback = write_callback;
+ mstream->context = context;
+
+ return o_stream_create(&mstream->ostream, output,
+ o_stream_get_fd(output));
+}
diff --git a/src/lib-fs/ostream-metawrap.h b/src/lib-fs/ostream-metawrap.h
new file mode 100644
index 0000000..09a172c
--- /dev/null
+++ b/src/lib-fs/ostream-metawrap.h
@@ -0,0 +1,8 @@
+#ifndef OSTREAM_METAWRAP_H
+#define OSTREAM_METAWRAP_H
+
+struct ostream *
+o_stream_create_metawrap(struct ostream *output,
+ void (*write_callback)(void *), void *context);
+
+#endif
diff --git a/src/lib-fs/test-fs-metawrap.c b/src/lib-fs/test-fs-metawrap.c
new file mode 100644
index 0000000..161f1c5
--- /dev/null
+++ b/src/lib-fs/test-fs-metawrap.c
@@ -0,0 +1,103 @@
+/* Copyright (c) 2016-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "str.h"
+#include "istream.h"
+#include "ostream.h"
+#include "fs-test.h"
+#include "test-common.h"
+
+static const struct fs_settings fs_set;
+
+static void test_fs_metawrap_stat(void)
+{
+ struct fs *fs;
+ struct fs_file *file;
+ struct test_fs_file *test_file;
+ struct istream *input;
+ struct stat st;
+ const char *error;
+ unsigned int i;
+
+ test_begin("fs metawrap stat");
+
+ if (fs_init("metawrap", "test", &fs_set, &fs, &error) < 0)
+ i_fatal("fs_init() failed: %s", error);
+
+ for (i = 0; i < 2; i++) {
+ file = fs_file_init(fs, "foo", FS_OPEN_MODE_READONLY);
+
+ test_file = test_fs_file_get(fs, "foo");
+ str_append(test_file->contents, "key:value\n\n12345678901234567890");
+
+ if (i == 0) {
+ input = fs_read_stream(file, 2);
+ test_istream_set_max_buffer_size(test_file->input, 2);
+ } else {
+ input = NULL;
+ }
+
+ test_assert_idx(fs_stat(file, &st) == 0 && st.st_size == 20, i);
+
+ i_stream_unref(&input);
+ fs_file_deinit(&file);
+ }
+ fs_deinit(&fs);
+ test_end();
+}
+
+static void test_fs_metawrap_async(void)
+{
+ test_fs_async("metawrap", FS_PROPERTY_METADATA, "metawrap", "test");
+ test_fs_async("metawrap passthrough", 0, "metawrap", "test");
+ test_fs_async("double-metawrap", FS_PROPERTY_METADATA, "metawrap", "metawrap:test");
+}
+
+static void test_fs_metawrap_write_empty(void)
+{
+ struct fs *fs;
+ struct stat st;
+ const char *error;
+
+ test_begin("fs metawrap write empty file");
+ if (fs_init("metawrap", "test", &fs_set, &fs, &error) < 0)
+ i_fatal("fs_init() failed: %s", error);
+ struct fs_file *file = fs_file_init(fs, "foo", FS_OPEN_MODE_REPLACE);
+ struct ostream *output = fs_write_stream(file);
+ test_assert(fs_write_stream_finish(file, &output) > 0);
+ test_assert(fs_stat(file, &st) == 0 && st.st_size == 0);
+ fs_file_deinit(&file);
+ fs_deinit(&fs);
+ test_end();
+}
+
+static void test_fs_metawrap_write_fname_rename(void)
+{
+ struct fs *fs;
+ const char *error;
+
+ test_begin("fs metawrap write fname rename");
+ if (fs_init("metawrap", "test", &fs_set, &fs, &error) < 0)
+ i_fatal("fs_init() failed: %s", error);
+ struct fs_file *file = fs_file_init(fs, "foo", FS_OPEN_MODE_REPLACE);
+ struct ostream *output = fs_write_stream(file);
+ o_stream_nsend_str(output, "test");
+ fs_set_metadata(file, FS_METADATA_WRITE_FNAME, "renamed");
+ test_assert(fs_write_stream_finish(file, &output) > 0);
+ test_assert(strcmp(fs_file_path(file), "renamed") == 0);
+ fs_file_deinit(&file);
+ fs_deinit(&fs);
+ test_end();
+}
+
+int main(void)
+{
+ static void (*const test_functions[])(void) = {
+ test_fs_metawrap_stat,
+ test_fs_metawrap_async,
+ test_fs_metawrap_write_empty,
+ test_fs_metawrap_write_fname_rename,
+ NULL
+ };
+ return test_run(test_functions);
+}
diff --git a/src/lib-fs/test-fs-posix.c b/src/lib-fs/test-fs-posix.c
new file mode 100644
index 0000000..d6fea11
--- /dev/null
+++ b/src/lib-fs/test-fs-posix.c
@@ -0,0 +1,144 @@
+/* Copyright (c) 2016-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "str.h"
+#include "ostream.h"
+#include "fs-api.h"
+#include "safe-mkdir.h"
+#include "safe-mkstemp.h"
+#include "test-common.h"
+#include "unlink-directory.h"
+#include <sys/stat.h>
+#include <unistd.h>
+
+static void test_fs_posix(void)
+{
+ const char testdir[] = ".test-fs-posix";
+ const char *unlink_err;
+
+ if (unlink_directory(testdir, UNLINK_DIRECTORY_FLAG_RMDIR, &unlink_err) < 0) {
+ i_error("Couldn't prepare test directory (%s): %s", testdir, unlink_err);
+ goto error_no_testdir;
+ }
+ if (safe_mkdir(testdir, 0700, (uid_t)-1, (gid_t)-1) != 1) {
+ /* Something just raced us to create this directory, bail. */
+ goto error_no_testdir;
+ }
+
+ int ret;
+ const char *error;
+ struct fs *fs;
+ struct fs_settings fs_set;
+
+ test_begin("test-fs-posix filesystem");
+ i_zero(&fs_set);
+ ret = fs_init("posix", t_strdup_printf("prefix=%s/", testdir), &fs_set, &fs, &error);
+ test_out_quiet("fs_init() failed", ret >= 0);
+ if (ret < 0) {
+ test_end();
+ goto error_no_fs;
+ }
+
+ struct fs *ref = fs;
+ fs_ref(ref);
+ fs_unref(&ref);
+ test_assert(ref == NULL);
+ test_assert(fs != NULL);
+
+ test_assert(fs_get_parent(fs) == NULL);
+ test_assert_strcmp(fs_get_driver(fs), "posix");
+ test_end();
+
+ struct fs_file *file;
+ char buf[10];
+ ssize_t count;
+ test_begin("test-fs-posix bad file read");
+ file = fs_file_init(fs, "fail_1", FS_OPEN_MODE_READONLY);
+ test_assert(fs_exists(file) == 0);
+ count = fs_read(file, buf, 1);
+ test_assert(count == -1 && errno == ENOENT &&
+ strstr(fs_file_last_error(file), "No such file or directory") != NULL);
+ fs_file_deinit(&file);
+ test_end();
+
+ test_begin("test-fs-posix good file write");
+ file = fs_file_init(fs, "good1", FS_OPEN_MODE_REPLACE);
+ test_assert(file != NULL);
+ test_assert(fs_exists(file) == 0); /* file not created until data is written */
+ test_assert(fs_write(file, "X", 1) == 0);
+ test_assert(fs_exists(file) == 1);
+ fs_file_deinit(&file);
+ test_end();
+
+ test_begin("test-fs-posix good file read");
+ file = fs_file_init(fs, "good1", FS_OPEN_MODE_READONLY);
+ test_assert(fs_exists(file) == 1);
+ errno = 0;
+ count = fs_read(file, buf, 2);
+ test_assert(count == 1 && errno == 0);
+ fs_file_deinit(&file);
+ test_end();
+
+ struct fs_iter *iter = fs_iter_init(fs, "/", 0);
+ const char *filename;
+ test_begin("test-fs-posix iterator");
+ filename = fs_iter_next(iter);
+ test_assert_strcmp(filename, "good1");
+ test_assert(fs_iter_next(iter) == NULL);
+ fs_iter_deinit(&iter, &error);
+ test_end();
+
+ struct stat st;
+ test_begin("test-fs-posix file stat and delete");
+ file = fs_file_init(fs, "good1", FS_OPEN_MODE_READONLY);
+ test_assert(fs_stat(file, &st) == 0);
+ test_assert(st.st_size == 1);
+ test_assert(fs_delete(file) == 0);
+ fs_file_deinit(&file);
+ test_end();
+
+ test_begin("test-fs-posix file write fname rename");
+ file = fs_file_init(fs, "subdir/badfname", FS_OPEN_MODE_REPLACE);
+ struct ostream *output = fs_write_stream(file);
+ o_stream_nsend_str(output, "hello");
+ fs_set_metadata(file, FS_METADATA_WRITE_FNAME, "subdir/rename1");
+ test_assert(fs_write_stream_finish(file, &output) == 1);
+ test_assert(strcmp(fs_file_path(file), "subdir/rename1") == 0);
+ fs_file_deinit(&file);
+ file = fs_file_init(fs, "subdir/rename1", FS_OPEN_MODE_READONLY);
+ test_assert(fs_stat(file, &st) == 0);
+ test_assert(st.st_size == 5);
+ fs_file_deinit(&file);
+ test_end();
+
+ test_begin("test-fs-posix file copy fname rename");
+ struct fs_file *src = fs_file_init(fs, "subdir/rename1", FS_OPEN_MODE_READONLY);
+ struct fs_file *dest = fs_file_init(fs, "subdir/badfname", FS_OPEN_MODE_REPLACE);
+ fs_set_metadata(dest, FS_METADATA_WRITE_FNAME, "subdir/rename2");
+ test_assert(fs_copy(src, dest) == 0);
+ test_assert(strcmp(fs_file_path(dest), "subdir/rename2") == 0);
+ fs_file_deinit(&src);
+ fs_file_deinit(&dest);
+ file = fs_file_init(fs, "subdir/rename2", FS_OPEN_MODE_READONLY);
+ test_assert(fs_stat(file, &st) == 0);
+ test_assert(st.st_size == 5);
+ fs_file_deinit(&file);
+ test_end();
+
+ fs_deinit(&fs);
+
+error_no_fs:
+ if (unlink_directory(testdir, UNLINK_DIRECTORY_FLAG_RMDIR, &unlink_err) < 0)
+ i_error("Couldn't clean up test directory (%s): %s", testdir, unlink_err);
+error_no_testdir:
+ return;
+}
+
+int main(void)
+{
+ static void (*const test_functions[])(void) = {
+ test_fs_posix,
+ NULL
+ };
+ return test_run(test_functions);
+}
diff --git a/src/lib-fts/Makefile.am b/src/lib-fts/Makefile.am
new file mode 100644
index 0000000..1b10070
--- /dev/null
+++ b/src/lib-fts/Makefile.am
@@ -0,0 +1,156 @@
+noinst_LTLIBRARIES = libfts.la
+
+# I$(top_srcdir)/src/lib-fts needed to include
+# word-break-data.c and word-boundary-data.c
+# in fts-tokenizer-generic.c
+AM_CPPFLAGS = \
+ -I$(top_srcdir)/src/lib \
+ -I$(top_srcdir)/src/lib-test \
+ -I$(top_srcdir)/src/lib-mail \
+ -I$(top_srcdir)/src/lib-fts \
+ $(LIBEXTTEXTCAT_CFLAGS) \
+ $(LIBICU_CFLAGS) \
+ -DUDHRDIR=\""$(top_srcdir)/src/lib-fts"\" \
+ -DDATADIR=\"$(pkgdatadir)\" \
+ -DTEST_STOPWORDS_DIR=\""$(top_srcdir)/src/lib-fts/stopwords"\"
+
+stopwordsdir = $(datadir)/${PACKAGE_TARNAME}/stopwords
+dist_stopwords_DATA = \
+ stopwords/stopwords_da.txt \
+ stopwords/stopwords_de.txt \
+ stopwords/stopwords_en.txt \
+ stopwords/stopwords_es.txt \
+ stopwords/stopwords_fi.txt \
+ stopwords/stopwords_fr.txt \
+ stopwords/stopwords_it.txt \
+ stopwords/stopwords_nl.txt \
+ stopwords/stopwords_no.txt \
+ stopwords/stopwords_pt.txt \
+ stopwords/stopwords_ro.txt \
+ stopwords/stopwords_ru.txt \
+ stopwords/stopwords_sv.txt \
+ stopwords/stopwords_tr.txt
+
+BUILT_SOURCES = $(srcdir)/word-boundary-data.c \
+ $(srcdir)/word-break-data.c
+
+EXTRA_DIST = \
+ udhr_fra.txt \
+ PropList.txt \
+ word-properties.pl \
+ WordBreakProperty.txt \
+ word-boundary-data.c \
+ word-break-data.c \
+ stopwords/stopwords_malformed.txt
+
+$(srcdir)/WordBreakProperty.txt:
+ test -f $@ || wget -O $@ https://dovecot.org/res/WordBreakProperty.txt
+$(srcdir)/word-boundary-data.c: $(srcdir)/word-properties.pl $(srcdir)/WordBreakProperty.txt
+ perl $(srcdir)/word-properties.pl boundaries $(srcdir)/WordBreakProperty.txt > $@
+
+$(srcdir)/PropList.txt:
+ test -f $@ || wget -O $@ https://dovecot.org/res/PropList.txt
+$(srcdir)/word-break-data.c: $(srcdir)/word-properties.pl $(srcdir)/PropList.txt
+ perl $(srcdir)/word-properties.pl breaks $(srcdir)/PropList.txt > $@
+
+
+if BUILD_FTS_STEMMER
+STEMMER_LIBS = -lstemmer
+endif
+
+if BUILD_FTS_EXTTEXTCAT
+TEXTCAT_LIBS = $(LIBEXTTEXTCAT_LIBS)
+else
+if BUILD_FTS_TEXTCAT
+TEXTCAT_LIBS = -ltextcat
+endif
+endif
+
+if BUILD_LIBICU
+ICU_SOURCES = fts-icu.c
+NORMALIZER_LIBS = $(LIBICU_LIBS)
+ICU_TESTS = test-fts-icu
+endif
+
+libfts_la_LIBADD = \
+ $(STEMMER_LIBS) \
+ $(TEXTCAT_LIBS) \
+ $(NORMALIZER_LIBS)
+
+libfts_la_SOURCES = \
+ fts-filter.c \
+ fts-filter-contractions.c \
+ fts-filter-common.c \
+ fts-filter-english-possessive.c \
+ fts-filter-lowercase.c \
+ fts-filter-normalizer-icu.c \
+ fts-filter-stopwords.c \
+ fts-filter-stemmer-snowball.c \
+ fts-language.c \
+ fts-library.c \
+ fts-tokenizer.c \
+ fts-tokenizer-address.c \
+ fts-tokenizer-common.c \
+ fts-tokenizer-generic.c \
+ $(ICU_SOURCES)
+
+headers = \
+ fts-common.h \
+ fts-filter.h \
+ fts-filter-common.h \
+ fts-filter-private.h \
+ fts-icu.h \
+ fts-language.h \
+ fts-library.h \
+ fts-tokenizer.h \
+ fts-tokenizer-common.h \
+ fts-tokenizer-private.h \
+ fts-tokenizer-generic-private.h
+
+pkginc_libdir=$(pkgincludedir)
+pkginc_lib_HEADERS = $(headers)
+
+deps=../lib-dovecot/libdovecot.la
+
+pkglib_LTLIBRARIES = libdovecot-fts.la
+libdovecot_fts_la_SOURCES =
+libdovecot_fts_la_LIBADD = libfts.la $(deps)
+libdovecot_fts_la_DEPENDENCIES = libfts.la $(deps)
+libdovecot_fts_la_LDFLAGS = -export-dynamic
+
+test_programs = \
+ $(ICU_TESTS) \
+ $(TEST_FTS_LANGUAGE) \
+ test-fts-filter \
+ test-fts-tokenizer
+
+noinst_PROGRAMS = $(test_programs)
+
+test_libs = \
+ ../lib-test/libtest.la \
+ ../lib/liblib.la
+test_deps = $(noinst_LTLIBRARIES) $(test_libs)
+
+test_fts_icu_SOURCES = test-fts-icu.c
+test_fts_icu_LDADD = fts-icu.lo $(LIBICU_LIBS) $(test_libs)
+test_fts_icu_DEPENDENCIES = fts-icu.lo $(test_deps)
+
+test_fts_filter_SOURCES = test-fts-filter.c
+test_fts_filter_LDADD = libfts.la $(test_libs)
+test_fts_filter_DEPENDENCIES = libfts.la $(test_deps)
+
+if BUILD_FTS_EXTTEXTCAT
+TEST_FTS_LANGUAGE = test-fts-language
+test_fts_language_SOURCES = test-fts-language.c
+test_fts_language_LDADD = fts-language.lo $(test_libs) $(TEXTCAT_LIBS)
+test_fts_language_DEPENDENCIES = $(test_deps)
+endif
+
+test_fts_tokenizer_SOURCES = test-fts-tokenizer.c
+test_fts_tokenizer_LDADD = fts-tokenizer.lo fts-tokenizer-generic.lo fts-tokenizer-address.lo fts-tokenizer-common.lo ../lib-mail/libmail.la $(test_libs)
+test_fts_tokenizer_DEPENDENCIES = ../lib-mail/libmail.la $(test_deps)
+
+check-local:
+ for bin in $(test_programs); do \
+ if ! $(RUN_TEST) ./$$bin; then exit 1; fi; \
+ done
diff --git a/src/lib-fts/Makefile.in b/src/lib-fts/Makefile.in
new file mode 100644
index 0000000..697ae2a
--- /dev/null
+++ b/src/lib-fts/Makefile.in
@@ -0,0 +1,1139 @@
+# Makefile.in generated by automake 1.16.1 from Makefile.am.
+# @configure_input@
+
+# Copyright (C) 1994-2018 Free Software Foundation, Inc.
+
+# This Makefile.in is free software; the Free Software Foundation
+# gives unlimited permission to copy and/or distribute it,
+# with or without modifications, as long as this notice is preserved.
+
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY, to the extent permitted by law; without
+# even the implied warranty of MERCHANTABILITY or FITNESS FOR A
+# PARTICULAR PURPOSE.
+
+@SET_MAKE@
+
+
+
+
+VPATH = @srcdir@
+am__is_gnu_make = { \
+ if test -z '$(MAKELEVEL)'; then \
+ false; \
+ elif test -n '$(MAKE_HOST)'; then \
+ true; \
+ elif test -n '$(MAKE_VERSION)' && test -n '$(CURDIR)'; then \
+ true; \
+ else \
+ false; \
+ fi; \
+}
+am__make_running_with_option = \
+ case $${target_option-} in \
+ ?) ;; \
+ *) echo "am__make_running_with_option: internal error: invalid" \
+ "target option '$${target_option-}' specified" >&2; \
+ exit 1;; \
+ esac; \
+ has_opt=no; \
+ sane_makeflags=$$MAKEFLAGS; \
+ if $(am__is_gnu_make); then \
+ sane_makeflags=$$MFLAGS; \
+ else \
+ case $$MAKEFLAGS in \
+ *\\[\ \ ]*) \
+ bs=\\; \
+ sane_makeflags=`printf '%s\n' "$$MAKEFLAGS" \
+ | sed "s/$$bs$$bs[$$bs $$bs ]*//g"`;; \
+ esac; \
+ fi; \
+ skip_next=no; \
+ strip_trailopt () \
+ { \
+ flg=`printf '%s\n' "$$flg" | sed "s/$$1.*$$//"`; \
+ }; \
+ for flg in $$sane_makeflags; do \
+ test $$skip_next = yes && { skip_next=no; continue; }; \
+ case $$flg in \
+ *=*|--*) continue;; \
+ -*I) strip_trailopt 'I'; skip_next=yes;; \
+ -*I?*) strip_trailopt 'I';; \
+ -*O) strip_trailopt 'O'; skip_next=yes;; \
+ -*O?*) strip_trailopt 'O';; \
+ -*l) strip_trailopt 'l'; skip_next=yes;; \
+ -*l?*) strip_trailopt 'l';; \
+ -[dEDm]) skip_next=yes;; \
+ -[JT]) skip_next=yes;; \
+ esac; \
+ case $$flg in \
+ *$$target_option*) has_opt=yes; break;; \
+ esac; \
+ done; \
+ test $$has_opt = yes
+am__make_dryrun = (target_option=n; $(am__make_running_with_option))
+am__make_keepgoing = (target_option=k; $(am__make_running_with_option))
+pkgdatadir = $(datadir)/@PACKAGE@
+pkgincludedir = $(includedir)/@PACKAGE@
+pkglibdir = $(libdir)/@PACKAGE@
+pkglibexecdir = $(libexecdir)/@PACKAGE@
+am__cd = CDPATH="$${ZSH_VERSION+.}$(PATH_SEPARATOR)" && cd
+install_sh_DATA = $(install_sh) -c -m 644
+install_sh_PROGRAM = $(install_sh) -c
+install_sh_SCRIPT = $(install_sh) -c
+INSTALL_HEADER = $(INSTALL_DATA)
+transform = $(program_transform_name)
+NORMAL_INSTALL = :
+PRE_INSTALL = :
+POST_INSTALL = :
+NORMAL_UNINSTALL = :
+PRE_UNINSTALL = :
+POST_UNINSTALL = :
+build_triplet = @build@
+host_triplet = @host@
+noinst_PROGRAMS = $(am__EXEEXT_3)
+subdir = src/lib-fts
+ACLOCAL_M4 = $(top_srcdir)/aclocal.m4
+am__aclocal_m4_deps = $(top_srcdir)/m4/ac_checktype2.m4 \
+ $(top_srcdir)/m4/ac_typeof.m4 $(top_srcdir)/m4/arc4random.m4 \
+ $(top_srcdir)/m4/blockdev.m4 $(top_srcdir)/m4/c99_vsnprintf.m4 \
+ $(top_srcdir)/m4/clock_gettime.m4 $(top_srcdir)/m4/crypt.m4 \
+ $(top_srcdir)/m4/crypt_xpg6.m4 $(top_srcdir)/m4/dbqlk.m4 \
+ $(top_srcdir)/m4/dirent_dtype.m4 $(top_srcdir)/m4/dovecot.m4 \
+ $(top_srcdir)/m4/fd_passing.m4 $(top_srcdir)/m4/fdatasync.m4 \
+ $(top_srcdir)/m4/flexible_array_member.m4 \
+ $(top_srcdir)/m4/glibc.m4 $(top_srcdir)/m4/gmtime_max.m4 \
+ $(top_srcdir)/m4/gmtime_tm_gmtoff.m4 \
+ $(top_srcdir)/m4/ioloop.m4 $(top_srcdir)/m4/iovec.m4 \
+ $(top_srcdir)/m4/ipv6.m4 $(top_srcdir)/m4/libcap.m4 \
+ $(top_srcdir)/m4/libtool.m4 $(top_srcdir)/m4/libwrap.m4 \
+ $(top_srcdir)/m4/linux_mremap.m4 $(top_srcdir)/m4/ltoptions.m4 \
+ $(top_srcdir)/m4/ltsugar.m4 $(top_srcdir)/m4/ltversion.m4 \
+ $(top_srcdir)/m4/lt~obsolete.m4 $(top_srcdir)/m4/mmap_write.m4 \
+ $(top_srcdir)/m4/mntctl.m4 $(top_srcdir)/m4/modules.m4 \
+ $(top_srcdir)/m4/notify.m4 $(top_srcdir)/m4/nsl.m4 \
+ $(top_srcdir)/m4/off_t_max.m4 $(top_srcdir)/m4/pkg.m4 \
+ $(top_srcdir)/m4/pr_set_dumpable.m4 \
+ $(top_srcdir)/m4/q_quotactl.m4 $(top_srcdir)/m4/quota.m4 \
+ $(top_srcdir)/m4/random.m4 $(top_srcdir)/m4/rlimit.m4 \
+ $(top_srcdir)/m4/sendfile.m4 $(top_srcdir)/m4/size_t_signed.m4 \
+ $(top_srcdir)/m4/sockpeercred.m4 $(top_srcdir)/m4/sql.m4 \
+ $(top_srcdir)/m4/ssl.m4 $(top_srcdir)/m4/st_tim.m4 \
+ $(top_srcdir)/m4/static_array.m4 $(top_srcdir)/m4/test_with.m4 \
+ $(top_srcdir)/m4/time_t.m4 $(top_srcdir)/m4/typeof.m4 \
+ $(top_srcdir)/m4/typeof_dev_t.m4 \
+ $(top_srcdir)/m4/uoff_t_max.m4 $(top_srcdir)/m4/vararg.m4 \
+ $(top_srcdir)/m4/want_apparmor.m4 \
+ $(top_srcdir)/m4/want_bsdauth.m4 \
+ $(top_srcdir)/m4/want_bzlib.m4 \
+ $(top_srcdir)/m4/want_cassandra.m4 \
+ $(top_srcdir)/m4/want_cdb.m4 \
+ $(top_srcdir)/m4/want_checkpassword.m4 \
+ $(top_srcdir)/m4/want_clucene.m4 $(top_srcdir)/m4/want_db.m4 \
+ $(top_srcdir)/m4/want_gssapi.m4 $(top_srcdir)/m4/want_icu.m4 \
+ $(top_srcdir)/m4/want_ldap.m4 $(top_srcdir)/m4/want_lua.m4 \
+ $(top_srcdir)/m4/want_lz4.m4 $(top_srcdir)/m4/want_lzma.m4 \
+ $(top_srcdir)/m4/want_mysql.m4 $(top_srcdir)/m4/want_pam.m4 \
+ $(top_srcdir)/m4/want_passwd.m4 $(top_srcdir)/m4/want_pgsql.m4 \
+ $(top_srcdir)/m4/want_prefetch.m4 \
+ $(top_srcdir)/m4/want_shadow.m4 \
+ $(top_srcdir)/m4/want_sodium.m4 $(top_srcdir)/m4/want_solr.m4 \
+ $(top_srcdir)/m4/want_sqlite.m4 \
+ $(top_srcdir)/m4/want_stemmer.m4 \
+ $(top_srcdir)/m4/want_systemd.m4 \
+ $(top_srcdir)/m4/want_textcat.m4 \
+ $(top_srcdir)/m4/want_unwind.m4 $(top_srcdir)/m4/want_zlib.m4 \
+ $(top_srcdir)/m4/want_zstd.m4 $(top_srcdir)/configure.ac
+am__configure_deps = $(am__aclocal_m4_deps) $(CONFIGURE_DEPENDENCIES) \
+ $(ACLOCAL_M4)
+DIST_COMMON = $(srcdir)/Makefile.am $(dist_stopwords_DATA) \
+ $(pkginc_lib_HEADERS) $(am__DIST_COMMON)
+mkinstalldirs = $(install_sh) -d
+CONFIG_HEADER = $(top_builddir)/config.h
+CONFIG_CLEAN_FILES =
+CONFIG_CLEAN_VPATH_FILES =
+@BUILD_LIBICU_TRUE@am__EXEEXT_1 = test-fts-icu$(EXEEXT)
+@BUILD_FTS_EXTTEXTCAT_TRUE@am__EXEEXT_2 = test-fts-language$(EXEEXT)
+am__EXEEXT_3 = $(am__EXEEXT_1) $(am__EXEEXT_2) \
+ test-fts-filter$(EXEEXT) test-fts-tokenizer$(EXEEXT)
+PROGRAMS = $(noinst_PROGRAMS)
+am__vpath_adj_setup = srcdirstrip=`echo "$(srcdir)" | sed 's|.|.|g'`;
+am__vpath_adj = case $$p in \
+ $(srcdir)/*) f=`echo "$$p" | sed "s|^$$srcdirstrip/||"`;; \
+ *) f=$$p;; \
+ esac;
+am__strip_dir = f=`echo $$p | sed -e 's|^.*/||'`;
+am__install_max = 40
+am__nobase_strip_setup = \
+ srcdirstrip=`echo "$(srcdir)" | sed 's/[].[^$$\\*|]/\\\\&/g'`
+am__nobase_strip = \
+ for p in $$list; do echo "$$p"; done | sed -e "s|$$srcdirstrip/||"
+am__nobase_list = $(am__nobase_strip_setup); \
+ for p in $$list; do echo "$$p $$p"; done | \
+ sed "s| $$srcdirstrip/| |;"' / .*\//!s/ .*/ ./; s,\( .*\)/[^/]*$$,\1,' | \
+ $(AWK) 'BEGIN { files["."] = "" } { files[$$2] = files[$$2] " " $$1; \
+ if (++n[$$2] == $(am__install_max)) \
+ { print $$2, files[$$2]; n[$$2] = 0; files[$$2] = "" } } \
+ END { for (dir in files) print dir, files[dir] }'
+am__base_list = \
+ sed '$$!N;$$!N;$$!N;$$!N;$$!N;$$!N;$$!N;s/\n/ /g' | \
+ sed '$$!N;$$!N;$$!N;$$!N;s/\n/ /g'
+am__uninstall_files_from_dir = { \
+ test -z "$$files" \
+ || { test ! -d "$$dir" && test ! -f "$$dir" && test ! -r "$$dir"; } \
+ || { echo " ( cd '$$dir' && rm -f" $$files ")"; \
+ $(am__cd) "$$dir" && rm -f $$files; }; \
+ }
+am__installdirs = "$(DESTDIR)$(pkglibdir)" "$(DESTDIR)$(stopwordsdir)" \
+ "$(DESTDIR)$(pkginc_libdir)"
+LTLIBRARIES = $(noinst_LTLIBRARIES) $(pkglib_LTLIBRARIES)
+am_libdovecot_fts_la_OBJECTS =
+libdovecot_fts_la_OBJECTS = $(am_libdovecot_fts_la_OBJECTS)
+AM_V_lt = $(am__v_lt_@AM_V@)
+am__v_lt_ = $(am__v_lt_@AM_DEFAULT_V@)
+am__v_lt_0 = --silent
+am__v_lt_1 =
+libdovecot_fts_la_LINK = $(LIBTOOL) $(AM_V_lt) --tag=CC \
+ $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=link $(CCLD) \
+ $(AM_CFLAGS) $(CFLAGS) $(libdovecot_fts_la_LDFLAGS) $(LDFLAGS) \
+ -o $@
+am__DEPENDENCIES_1 =
+@BUILD_FTS_EXTTEXTCAT_TRUE@am__DEPENDENCIES_2 = $(am__DEPENDENCIES_1)
+@BUILD_LIBICU_TRUE@am__DEPENDENCIES_3 = $(am__DEPENDENCIES_1)
+libfts_la_DEPENDENCIES = $(am__DEPENDENCIES_1) $(am__DEPENDENCIES_2) \
+ $(am__DEPENDENCIES_3)
+am__libfts_la_SOURCES_DIST = fts-filter.c fts-filter-contractions.c \
+ fts-filter-common.c fts-filter-english-possessive.c \
+ fts-filter-lowercase.c fts-filter-normalizer-icu.c \
+ fts-filter-stopwords.c fts-filter-stemmer-snowball.c \
+ fts-language.c fts-library.c fts-tokenizer.c \
+ fts-tokenizer-address.c fts-tokenizer-common.c \
+ fts-tokenizer-generic.c fts-icu.c
+@BUILD_LIBICU_TRUE@am__objects_1 = fts-icu.lo
+am_libfts_la_OBJECTS = fts-filter.lo fts-filter-contractions.lo \
+ fts-filter-common.lo fts-filter-english-possessive.lo \
+ fts-filter-lowercase.lo fts-filter-normalizer-icu.lo \
+ fts-filter-stopwords.lo fts-filter-stemmer-snowball.lo \
+ fts-language.lo fts-library.lo fts-tokenizer.lo \
+ fts-tokenizer-address.lo fts-tokenizer-common.lo \
+ fts-tokenizer-generic.lo $(am__objects_1)
+libfts_la_OBJECTS = $(am_libfts_la_OBJECTS)
+am_test_fts_filter_OBJECTS = test-fts-filter.$(OBJEXT)
+test_fts_filter_OBJECTS = $(am_test_fts_filter_OBJECTS)
+am_test_fts_icu_OBJECTS = test-fts-icu.$(OBJEXT)
+test_fts_icu_OBJECTS = $(am_test_fts_icu_OBJECTS)
+am__test_fts_language_SOURCES_DIST = test-fts-language.c
+@BUILD_FTS_EXTTEXTCAT_TRUE@am_test_fts_language_OBJECTS = \
+@BUILD_FTS_EXTTEXTCAT_TRUE@ test-fts-language.$(OBJEXT)
+test_fts_language_OBJECTS = $(am_test_fts_language_OBJECTS)
+am_test_fts_tokenizer_OBJECTS = test-fts-tokenizer.$(OBJEXT)
+test_fts_tokenizer_OBJECTS = $(am_test_fts_tokenizer_OBJECTS)
+AM_V_P = $(am__v_P_@AM_V@)
+am__v_P_ = $(am__v_P_@AM_DEFAULT_V@)
+am__v_P_0 = false
+am__v_P_1 = :
+AM_V_GEN = $(am__v_GEN_@AM_V@)
+am__v_GEN_ = $(am__v_GEN_@AM_DEFAULT_V@)
+am__v_GEN_0 = @echo " GEN " $@;
+am__v_GEN_1 =
+AM_V_at = $(am__v_at_@AM_V@)
+am__v_at_ = $(am__v_at_@AM_DEFAULT_V@)
+am__v_at_0 = @
+am__v_at_1 =
+DEFAULT_INCLUDES = -I.@am__isrc@ -I$(top_builddir)
+depcomp = $(SHELL) $(top_srcdir)/depcomp
+am__maybe_remake_depfiles = depfiles
+am__depfiles_remade = ./$(DEPDIR)/fts-filter-common.Plo \
+ ./$(DEPDIR)/fts-filter-contractions.Plo \
+ ./$(DEPDIR)/fts-filter-english-possessive.Plo \
+ ./$(DEPDIR)/fts-filter-lowercase.Plo \
+ ./$(DEPDIR)/fts-filter-normalizer-icu.Plo \
+ ./$(DEPDIR)/fts-filter-stemmer-snowball.Plo \
+ ./$(DEPDIR)/fts-filter-stopwords.Plo \
+ ./$(DEPDIR)/fts-filter.Plo ./$(DEPDIR)/fts-icu.Plo \
+ ./$(DEPDIR)/fts-language.Plo ./$(DEPDIR)/fts-library.Plo \
+ ./$(DEPDIR)/fts-tokenizer-address.Plo \
+ ./$(DEPDIR)/fts-tokenizer-common.Plo \
+ ./$(DEPDIR)/fts-tokenizer-generic.Plo \
+ ./$(DEPDIR)/fts-tokenizer.Plo ./$(DEPDIR)/test-fts-filter.Po \
+ ./$(DEPDIR)/test-fts-icu.Po ./$(DEPDIR)/test-fts-language.Po \
+ ./$(DEPDIR)/test-fts-tokenizer.Po
+am__mv = mv -f
+COMPILE = $(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) \
+ $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS)
+LTCOMPILE = $(LIBTOOL) $(AM_V_lt) --tag=CC $(AM_LIBTOOLFLAGS) \
+ $(LIBTOOLFLAGS) --mode=compile $(CC) $(DEFS) \
+ $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) $(CPPFLAGS) \
+ $(AM_CFLAGS) $(CFLAGS)
+AM_V_CC = $(am__v_CC_@AM_V@)
+am__v_CC_ = $(am__v_CC_@AM_DEFAULT_V@)
+am__v_CC_0 = @echo " CC " $@;
+am__v_CC_1 =
+CCLD = $(CC)
+LINK = $(LIBTOOL) $(AM_V_lt) --tag=CC $(AM_LIBTOOLFLAGS) \
+ $(LIBTOOLFLAGS) --mode=link $(CCLD) $(AM_CFLAGS) $(CFLAGS) \
+ $(AM_LDFLAGS) $(LDFLAGS) -o $@
+AM_V_CCLD = $(am__v_CCLD_@AM_V@)
+am__v_CCLD_ = $(am__v_CCLD_@AM_DEFAULT_V@)
+am__v_CCLD_0 = @echo " CCLD " $@;
+am__v_CCLD_1 =
+SOURCES = $(libdovecot_fts_la_SOURCES) $(libfts_la_SOURCES) \
+ $(test_fts_filter_SOURCES) $(test_fts_icu_SOURCES) \
+ $(test_fts_language_SOURCES) $(test_fts_tokenizer_SOURCES)
+DIST_SOURCES = $(libdovecot_fts_la_SOURCES) \
+ $(am__libfts_la_SOURCES_DIST) $(test_fts_filter_SOURCES) \
+ $(test_fts_icu_SOURCES) $(am__test_fts_language_SOURCES_DIST) \
+ $(test_fts_tokenizer_SOURCES)
+am__can_run_installinfo = \
+ case $$AM_UPDATE_INFO_DIR in \
+ n|no|NO) false;; \
+ *) (install-info --version) >/dev/null 2>&1;; \
+ esac
+DATA = $(dist_stopwords_DATA)
+HEADERS = $(pkginc_lib_HEADERS)
+am__tagged_files = $(HEADERS) $(SOURCES) $(TAGS_FILES) $(LISP)
+# Read a list of newline-separated strings from the standard input,
+# and print each of them once, without duplicates. Input order is
+# *not* preserved.
+am__uniquify_input = $(AWK) '\
+ BEGIN { nonempty = 0; } \
+ { items[$$0] = 1; nonempty = 1; } \
+ END { if (nonempty) { for (i in items) print i; }; } \
+'
+# Make sure the list of sources is unique. This is necessary because,
+# e.g., the same source file might be shared among _SOURCES variables
+# for different programs/libraries.
+am__define_uniq_tagged_files = \
+ list='$(am__tagged_files)'; \
+ unique=`for i in $$list; do \
+ if test -f "$$i"; then echo $$i; else echo $(srcdir)/$$i; fi; \
+ done | $(am__uniquify_input)`
+ETAGS = etags
+CTAGS = ctags
+am__DIST_COMMON = $(srcdir)/Makefile.in $(top_srcdir)/depcomp
+DISTFILES = $(DIST_COMMON) $(DIST_SOURCES) $(TEXINFOS) $(EXTRA_DIST)
+ACLOCAL = @ACLOCAL@
+ACLOCAL_AMFLAGS = @ACLOCAL_AMFLAGS@
+AMTAR = @AMTAR@
+AM_DEFAULT_VERBOSITY = @AM_DEFAULT_VERBOSITY@
+APPARMOR_LIBS = @APPARMOR_LIBS@
+AR = @AR@
+AUTH_CFLAGS = @AUTH_CFLAGS@
+AUTH_LIBS = @AUTH_LIBS@
+AUTOCONF = @AUTOCONF@
+AUTOHEADER = @AUTOHEADER@
+AUTOMAKE = @AUTOMAKE@
+AWK = @AWK@
+BINARY_CFLAGS = @BINARY_CFLAGS@
+BINARY_LDFLAGS = @BINARY_LDFLAGS@
+BISON = @BISON@
+CASSANDRA_CFLAGS = @CASSANDRA_CFLAGS@
+CASSANDRA_LIBS = @CASSANDRA_LIBS@
+CC = @CC@
+CCDEPMODE = @CCDEPMODE@
+CDB_LIBS = @CDB_LIBS@
+CFLAGS = @CFLAGS@
+CLUCENE_CFLAGS = @CLUCENE_CFLAGS@
+CLUCENE_LIBS = @CLUCENE_LIBS@
+COMPRESS_LIBS = @COMPRESS_LIBS@
+CPP = @CPP@
+CPPFLAGS = @CPPFLAGS@
+CRYPT_LIBS = @CRYPT_LIBS@
+CXX = @CXX@
+CXXCPP = @CXXCPP@
+CXXDEPMODE = @CXXDEPMODE@
+CXXFLAGS = @CXXFLAGS@
+CYGPATH_W = @CYGPATH_W@
+DEFS = @DEFS@
+DEPDIR = @DEPDIR@
+DICT_LIBS = @DICT_LIBS@
+DLLIB = @DLLIB@
+DLLTOOL = @DLLTOOL@
+DSYMUTIL = @DSYMUTIL@
+DUMPBIN = @DUMPBIN@
+ECHO_C = @ECHO_C@
+ECHO_N = @ECHO_N@
+ECHO_T = @ECHO_T@
+EGREP = @EGREP@
+EXEEXT = @EXEEXT@
+FGREP = @FGREP@
+FLEX = @FLEX@
+FUZZER_CPPFLAGS = @FUZZER_CPPFLAGS@
+FUZZER_LDFLAGS = @FUZZER_LDFLAGS@
+GREP = @GREP@
+INSTALL = @INSTALL@
+INSTALL_DATA = @INSTALL_DATA@
+INSTALL_PROGRAM = @INSTALL_PROGRAM@
+INSTALL_SCRIPT = @INSTALL_SCRIPT@
+INSTALL_STRIP_PROGRAM = @INSTALL_STRIP_PROGRAM@
+KRB5CONFIG = @KRB5CONFIG@
+KRB5_CFLAGS = @KRB5_CFLAGS@
+KRB5_LIBS = @KRB5_LIBS@
+LD = @LD@
+LDAP_LIBS = @LDAP_LIBS@
+LDFLAGS = @LDFLAGS@
+LD_NO_WHOLE_ARCHIVE = @LD_NO_WHOLE_ARCHIVE@
+LD_WHOLE_ARCHIVE = @LD_WHOLE_ARCHIVE@
+LIBCAP = @LIBCAP@
+LIBDOVECOT = @LIBDOVECOT@
+LIBDOVECOT_COMPRESS = @LIBDOVECOT_COMPRESS@
+LIBDOVECOT_DEPS = @LIBDOVECOT_DEPS@
+LIBDOVECOT_DSYNC = @LIBDOVECOT_DSYNC@
+LIBDOVECOT_LA_LIBS = @LIBDOVECOT_LA_LIBS@
+LIBDOVECOT_LDA = @LIBDOVECOT_LDA@
+LIBDOVECOT_LDAP = @LIBDOVECOT_LDAP@
+LIBDOVECOT_LIBFTS = @LIBDOVECOT_LIBFTS@
+LIBDOVECOT_LIBFTS_DEPS = @LIBDOVECOT_LIBFTS_DEPS@
+LIBDOVECOT_LOGIN = @LIBDOVECOT_LOGIN@
+LIBDOVECOT_LUA = @LIBDOVECOT_LUA@
+LIBDOVECOT_LUA_DEPS = @LIBDOVECOT_LUA_DEPS@
+LIBDOVECOT_SQL = @LIBDOVECOT_SQL@
+LIBDOVECOT_STORAGE = @LIBDOVECOT_STORAGE@
+LIBDOVECOT_STORAGE_DEPS = @LIBDOVECOT_STORAGE_DEPS@
+LIBEXTTEXTCAT_CFLAGS = @LIBEXTTEXTCAT_CFLAGS@
+LIBEXTTEXTCAT_LIBS = @LIBEXTTEXTCAT_LIBS@
+LIBICONV = @LIBICONV@
+LIBICU_CFLAGS = @LIBICU_CFLAGS@
+LIBICU_LIBS = @LIBICU_LIBS@
+LIBOBJS = @LIBOBJS@
+LIBS = @LIBS@
+LIBSODIUM_CFLAGS = @LIBSODIUM_CFLAGS@
+LIBSODIUM_LIBS = @LIBSODIUM_LIBS@
+LIBTIRPC_CFLAGS = @LIBTIRPC_CFLAGS@
+LIBTIRPC_LIBS = @LIBTIRPC_LIBS@
+LIBTOOL = @LIBTOOL@
+LIBUNWIND_CFLAGS = @LIBUNWIND_CFLAGS@
+LIBUNWIND_LIBS = @LIBUNWIND_LIBS@
+LIBWRAP_LIBS = @LIBWRAP_LIBS@
+LINKED_STORAGE_LDADD = @LINKED_STORAGE_LDADD@
+LIPO = @LIPO@
+LN_S = @LN_S@
+LTLIBICONV = @LTLIBICONV@
+LTLIBOBJS = @LTLIBOBJS@
+LT_SYS_LIBRARY_PATH = @LT_SYS_LIBRARY_PATH@
+LUA_CFLAGS = @LUA_CFLAGS@
+LUA_LIBS = @LUA_LIBS@
+MAINT = @MAINT@
+MAKEINFO = @MAKEINFO@
+MANIFEST_TOOL = @MANIFEST_TOOL@
+MKDIR_P = @MKDIR_P@
+MODULE_LIBS = @MODULE_LIBS@
+MODULE_SUFFIX = @MODULE_SUFFIX@
+MYSQL_CFLAGS = @MYSQL_CFLAGS@
+MYSQL_CONFIG = @MYSQL_CONFIG@
+MYSQL_LIBS = @MYSQL_LIBS@
+NM = @NM@
+NMEDIT = @NMEDIT@
+NOPLUGIN_LDFLAGS = @NOPLUGIN_LDFLAGS@
+OBJDUMP = @OBJDUMP@
+OBJEXT = @OBJEXT@
+OTOOL = @OTOOL@
+OTOOL64 = @OTOOL64@
+PACKAGE = @PACKAGE@
+PACKAGE_BUGREPORT = @PACKAGE_BUGREPORT@
+PACKAGE_NAME = @PACKAGE_NAME@
+PACKAGE_STRING = @PACKAGE_STRING@
+PACKAGE_TARNAME = @PACKAGE_TARNAME@
+PACKAGE_URL = @PACKAGE_URL@
+PACKAGE_VERSION = @PACKAGE_VERSION@
+PANDOC = @PANDOC@
+PATH_SEPARATOR = @PATH_SEPARATOR@
+PGSQL_CFLAGS = @PGSQL_CFLAGS@
+PGSQL_LIBS = @PGSQL_LIBS@
+PG_CONFIG = @PG_CONFIG@
+PIE_CFLAGS = @PIE_CFLAGS@
+PIE_LDFLAGS = @PIE_LDFLAGS@
+PKG_CONFIG = @PKG_CONFIG@
+PKG_CONFIG_LIBDIR = @PKG_CONFIG_LIBDIR@
+PKG_CONFIG_PATH = @PKG_CONFIG_PATH@
+QUOTA_LIBS = @QUOTA_LIBS@
+RANLIB = @RANLIB@
+RELRO_LDFLAGS = @RELRO_LDFLAGS@
+RPCGEN = @RPCGEN@
+RUN_TEST = @RUN_TEST@
+SED = @SED@
+SETTING_FILES = @SETTING_FILES@
+SET_MAKE = @SET_MAKE@
+SHELL = @SHELL@
+SQLITE_CFLAGS = @SQLITE_CFLAGS@
+SQLITE_LIBS = @SQLITE_LIBS@
+SQL_CFLAGS = @SQL_CFLAGS@
+SQL_LIBS = @SQL_LIBS@
+SSL_CFLAGS = @SSL_CFLAGS@
+SSL_LIBS = @SSL_LIBS@
+STRIP = @STRIP@
+SYSTEMD_CFLAGS = @SYSTEMD_CFLAGS@
+SYSTEMD_LIBS = @SYSTEMD_LIBS@
+VALGRIND = @VALGRIND@
+VERSION = @VERSION@
+ZSTD_CFLAGS = @ZSTD_CFLAGS@
+ZSTD_LIBS = @ZSTD_LIBS@
+abs_builddir = @abs_builddir@
+abs_srcdir = @abs_srcdir@
+abs_top_builddir = @abs_top_builddir@
+abs_top_srcdir = @abs_top_srcdir@
+ac_ct_AR = @ac_ct_AR@
+ac_ct_CC = @ac_ct_CC@
+ac_ct_CXX = @ac_ct_CXX@
+ac_ct_DUMPBIN = @ac_ct_DUMPBIN@
+am__include = @am__include@
+am__leading_dot = @am__leading_dot@
+am__quote = @am__quote@
+am__tar = @am__tar@
+am__untar = @am__untar@
+bindir = @bindir@
+build = @build@
+build_alias = @build_alias@
+build_cpu = @build_cpu@
+build_os = @build_os@
+build_vendor = @build_vendor@
+builddir = @builddir@
+datadir = @datadir@
+datarootdir = @datarootdir@
+dict_drivers = @dict_drivers@
+docdir = @docdir@
+dvidir = @dvidir@
+exec_prefix = @exec_prefix@
+host = @host@
+host_alias = @host_alias@
+host_cpu = @host_cpu@
+host_os = @host_os@
+host_vendor = @host_vendor@
+htmldir = @htmldir@
+includedir = @includedir@
+infodir = @infodir@
+install_sh = @install_sh@
+libdir = @libdir@
+libexecdir = @libexecdir@
+localedir = @localedir@
+localstatedir = @localstatedir@
+mandir = @mandir@
+mkdir_p = @mkdir_p@
+moduledir = @moduledir@
+oldincludedir = @oldincludedir@
+pdfdir = @pdfdir@
+prefix = @prefix@
+program_transform_name = @program_transform_name@
+psdir = @psdir@
+rundir = @rundir@
+runstatedir = @runstatedir@
+sbindir = @sbindir@
+sharedstatedir = @sharedstatedir@
+sql_drivers = @sql_drivers@
+srcdir = @srcdir@
+ssldir = @ssldir@
+statedir = @statedir@
+sysconfdir = @sysconfdir@
+systemdservicetype = @systemdservicetype@
+systemdsystemunitdir = @systemdsystemunitdir@
+target_alias = @target_alias@
+top_build_prefix = @top_build_prefix@
+top_builddir = @top_builddir@
+top_srcdir = @top_srcdir@
+noinst_LTLIBRARIES = libfts.la
+
+# I$(top_srcdir)/src/lib-fts needed to include
+# word-break-data.c and word-boundary-data.c
+# in fts-tokenizer-generic.c
+AM_CPPFLAGS = \
+ -I$(top_srcdir)/src/lib \
+ -I$(top_srcdir)/src/lib-test \
+ -I$(top_srcdir)/src/lib-mail \
+ -I$(top_srcdir)/src/lib-fts \
+ $(LIBEXTTEXTCAT_CFLAGS) \
+ $(LIBICU_CFLAGS) \
+ -DUDHRDIR=\""$(top_srcdir)/src/lib-fts"\" \
+ -DDATADIR=\"$(pkgdatadir)\" \
+ -DTEST_STOPWORDS_DIR=\""$(top_srcdir)/src/lib-fts/stopwords"\"
+
+stopwordsdir = $(datadir)/${PACKAGE_TARNAME}/stopwords
+dist_stopwords_DATA = \
+ stopwords/stopwords_da.txt \
+ stopwords/stopwords_de.txt \
+ stopwords/stopwords_en.txt \
+ stopwords/stopwords_es.txt \
+ stopwords/stopwords_fi.txt \
+ stopwords/stopwords_fr.txt \
+ stopwords/stopwords_it.txt \
+ stopwords/stopwords_nl.txt \
+ stopwords/stopwords_no.txt \
+ stopwords/stopwords_pt.txt \
+ stopwords/stopwords_ro.txt \
+ stopwords/stopwords_ru.txt \
+ stopwords/stopwords_sv.txt \
+ stopwords/stopwords_tr.txt
+
+BUILT_SOURCES = $(srcdir)/word-boundary-data.c \
+ $(srcdir)/word-break-data.c
+
+EXTRA_DIST = \
+ udhr_fra.txt \
+ PropList.txt \
+ word-properties.pl \
+ WordBreakProperty.txt \
+ word-boundary-data.c \
+ word-break-data.c \
+ stopwords/stopwords_malformed.txt
+
+@BUILD_FTS_STEMMER_TRUE@STEMMER_LIBS = -lstemmer
+@BUILD_FTS_EXTTEXTCAT_FALSE@@BUILD_FTS_TEXTCAT_TRUE@TEXTCAT_LIBS = -ltextcat
+@BUILD_FTS_EXTTEXTCAT_TRUE@TEXTCAT_LIBS = $(LIBEXTTEXTCAT_LIBS)
+@BUILD_LIBICU_TRUE@ICU_SOURCES = fts-icu.c
+@BUILD_LIBICU_TRUE@NORMALIZER_LIBS = $(LIBICU_LIBS)
+@BUILD_LIBICU_TRUE@ICU_TESTS = test-fts-icu
+libfts_la_LIBADD = \
+ $(STEMMER_LIBS) \
+ $(TEXTCAT_LIBS) \
+ $(NORMALIZER_LIBS)
+
+libfts_la_SOURCES = \
+ fts-filter.c \
+ fts-filter-contractions.c \
+ fts-filter-common.c \
+ fts-filter-english-possessive.c \
+ fts-filter-lowercase.c \
+ fts-filter-normalizer-icu.c \
+ fts-filter-stopwords.c \
+ fts-filter-stemmer-snowball.c \
+ fts-language.c \
+ fts-library.c \
+ fts-tokenizer.c \
+ fts-tokenizer-address.c \
+ fts-tokenizer-common.c \
+ fts-tokenizer-generic.c \
+ $(ICU_SOURCES)
+
+headers = \
+ fts-common.h \
+ fts-filter.h \
+ fts-filter-common.h \
+ fts-filter-private.h \
+ fts-icu.h \
+ fts-language.h \
+ fts-library.h \
+ fts-tokenizer.h \
+ fts-tokenizer-common.h \
+ fts-tokenizer-private.h \
+ fts-tokenizer-generic-private.h
+
+pkginc_libdir = $(pkgincludedir)
+pkginc_lib_HEADERS = $(headers)
+deps = ../lib-dovecot/libdovecot.la
+pkglib_LTLIBRARIES = libdovecot-fts.la
+libdovecot_fts_la_SOURCES =
+libdovecot_fts_la_LIBADD = libfts.la $(deps)
+libdovecot_fts_la_DEPENDENCIES = libfts.la $(deps)
+libdovecot_fts_la_LDFLAGS = -export-dynamic
+test_programs = \
+ $(ICU_TESTS) \
+ $(TEST_FTS_LANGUAGE) \
+ test-fts-filter \
+ test-fts-tokenizer
+
+test_libs = \
+ ../lib-test/libtest.la \
+ ../lib/liblib.la
+
+test_deps = $(noinst_LTLIBRARIES) $(test_libs)
+test_fts_icu_SOURCES = test-fts-icu.c
+test_fts_icu_LDADD = fts-icu.lo $(LIBICU_LIBS) $(test_libs)
+test_fts_icu_DEPENDENCIES = fts-icu.lo $(test_deps)
+test_fts_filter_SOURCES = test-fts-filter.c
+test_fts_filter_LDADD = libfts.la $(test_libs)
+test_fts_filter_DEPENDENCIES = libfts.la $(test_deps)
+@BUILD_FTS_EXTTEXTCAT_TRUE@TEST_FTS_LANGUAGE = test-fts-language
+@BUILD_FTS_EXTTEXTCAT_TRUE@test_fts_language_SOURCES = test-fts-language.c
+@BUILD_FTS_EXTTEXTCAT_TRUE@test_fts_language_LDADD = fts-language.lo $(test_libs) $(TEXTCAT_LIBS)
+@BUILD_FTS_EXTTEXTCAT_TRUE@test_fts_language_DEPENDENCIES = $(test_deps)
+test_fts_tokenizer_SOURCES = test-fts-tokenizer.c
+test_fts_tokenizer_LDADD = fts-tokenizer.lo fts-tokenizer-generic.lo fts-tokenizer-address.lo fts-tokenizer-common.lo ../lib-mail/libmail.la $(test_libs)
+test_fts_tokenizer_DEPENDENCIES = ../lib-mail/libmail.la $(test_deps)
+all: $(BUILT_SOURCES)
+ $(MAKE) $(AM_MAKEFLAGS) all-am
+
+.SUFFIXES:
+.SUFFIXES: .c .lo .o .obj
+$(srcdir)/Makefile.in: @MAINTAINER_MODE_TRUE@ $(srcdir)/Makefile.am $(am__configure_deps)
+ @for dep in $?; do \
+ case '$(am__configure_deps)' in \
+ *$$dep*) \
+ ( cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh ) \
+ && { if test -f $@; then exit 0; else break; fi; }; \
+ exit 1;; \
+ esac; \
+ done; \
+ echo ' cd $(top_srcdir) && $(AUTOMAKE) --foreign src/lib-fts/Makefile'; \
+ $(am__cd) $(top_srcdir) && \
+ $(AUTOMAKE) --foreign src/lib-fts/Makefile
+Makefile: $(srcdir)/Makefile.in $(top_builddir)/config.status
+ @case '$?' in \
+ *config.status*) \
+ cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh;; \
+ *) \
+ echo ' cd $(top_builddir) && $(SHELL) ./config.status $(subdir)/$@ $(am__maybe_remake_depfiles)'; \
+ cd $(top_builddir) && $(SHELL) ./config.status $(subdir)/$@ $(am__maybe_remake_depfiles);; \
+ esac;
+
+$(top_builddir)/config.status: $(top_srcdir)/configure $(CONFIG_STATUS_DEPENDENCIES)
+ cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh
+
+$(top_srcdir)/configure: @MAINTAINER_MODE_TRUE@ $(am__configure_deps)
+ cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh
+$(ACLOCAL_M4): @MAINTAINER_MODE_TRUE@ $(am__aclocal_m4_deps)
+ cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh
+$(am__aclocal_m4_deps):
+
+clean-noinstPROGRAMS:
+ @list='$(noinst_PROGRAMS)'; test -n "$$list" || exit 0; \
+ echo " rm -f" $$list; \
+ rm -f $$list || exit $$?; \
+ test -n "$(EXEEXT)" || exit 0; \
+ list=`for p in $$list; do echo "$$p"; done | sed 's/$(EXEEXT)$$//'`; \
+ echo " rm -f" $$list; \
+ rm -f $$list
+
+clean-noinstLTLIBRARIES:
+ -test -z "$(noinst_LTLIBRARIES)" || rm -f $(noinst_LTLIBRARIES)
+ @list='$(noinst_LTLIBRARIES)'; \
+ locs=`for p in $$list; do echo $$p; done | \
+ sed 's|^[^/]*$$|.|; s|/[^/]*$$||; s|$$|/so_locations|' | \
+ sort -u`; \
+ test -z "$$locs" || { \
+ echo rm -f $${locs}; \
+ rm -f $${locs}; \
+ }
+
+install-pkglibLTLIBRARIES: $(pkglib_LTLIBRARIES)
+ @$(NORMAL_INSTALL)
+ @list='$(pkglib_LTLIBRARIES)'; test -n "$(pkglibdir)" || list=; \
+ list2=; for p in $$list; do \
+ if test -f $$p; then \
+ list2="$$list2 $$p"; \
+ else :; fi; \
+ done; \
+ test -z "$$list2" || { \
+ echo " $(MKDIR_P) '$(DESTDIR)$(pkglibdir)'"; \
+ $(MKDIR_P) "$(DESTDIR)$(pkglibdir)" || exit 1; \
+ echo " $(LIBTOOL) $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=install $(INSTALL) $(INSTALL_STRIP_FLAG) $$list2 '$(DESTDIR)$(pkglibdir)'"; \
+ $(LIBTOOL) $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=install $(INSTALL) $(INSTALL_STRIP_FLAG) $$list2 "$(DESTDIR)$(pkglibdir)"; \
+ }
+
+uninstall-pkglibLTLIBRARIES:
+ @$(NORMAL_UNINSTALL)
+ @list='$(pkglib_LTLIBRARIES)'; test -n "$(pkglibdir)" || list=; \
+ for p in $$list; do \
+ $(am__strip_dir) \
+ echo " $(LIBTOOL) $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=uninstall rm -f '$(DESTDIR)$(pkglibdir)/$$f'"; \
+ $(LIBTOOL) $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=uninstall rm -f "$(DESTDIR)$(pkglibdir)/$$f"; \
+ done
+
+clean-pkglibLTLIBRARIES:
+ -test -z "$(pkglib_LTLIBRARIES)" || rm -f $(pkglib_LTLIBRARIES)
+ @list='$(pkglib_LTLIBRARIES)'; \
+ locs=`for p in $$list; do echo $$p; done | \
+ sed 's|^[^/]*$$|.|; s|/[^/]*$$||; s|$$|/so_locations|' | \
+ sort -u`; \
+ test -z "$$locs" || { \
+ echo rm -f $${locs}; \
+ rm -f $${locs}; \
+ }
+
+libdovecot-fts.la: $(libdovecot_fts_la_OBJECTS) $(libdovecot_fts_la_DEPENDENCIES) $(EXTRA_libdovecot_fts_la_DEPENDENCIES)
+ $(AM_V_CCLD)$(libdovecot_fts_la_LINK) -rpath $(pkglibdir) $(libdovecot_fts_la_OBJECTS) $(libdovecot_fts_la_LIBADD) $(LIBS)
+
+libfts.la: $(libfts_la_OBJECTS) $(libfts_la_DEPENDENCIES) $(EXTRA_libfts_la_DEPENDENCIES)
+ $(AM_V_CCLD)$(LINK) $(libfts_la_OBJECTS) $(libfts_la_LIBADD) $(LIBS)
+
+test-fts-filter$(EXEEXT): $(test_fts_filter_OBJECTS) $(test_fts_filter_DEPENDENCIES) $(EXTRA_test_fts_filter_DEPENDENCIES)
+ @rm -f test-fts-filter$(EXEEXT)
+ $(AM_V_CCLD)$(LINK) $(test_fts_filter_OBJECTS) $(test_fts_filter_LDADD) $(LIBS)
+
+test-fts-icu$(EXEEXT): $(test_fts_icu_OBJECTS) $(test_fts_icu_DEPENDENCIES) $(EXTRA_test_fts_icu_DEPENDENCIES)
+ @rm -f test-fts-icu$(EXEEXT)
+ $(AM_V_CCLD)$(LINK) $(test_fts_icu_OBJECTS) $(test_fts_icu_LDADD) $(LIBS)
+
+test-fts-language$(EXEEXT): $(test_fts_language_OBJECTS) $(test_fts_language_DEPENDENCIES) $(EXTRA_test_fts_language_DEPENDENCIES)
+ @rm -f test-fts-language$(EXEEXT)
+ $(AM_V_CCLD)$(LINK) $(test_fts_language_OBJECTS) $(test_fts_language_LDADD) $(LIBS)
+
+test-fts-tokenizer$(EXEEXT): $(test_fts_tokenizer_OBJECTS) $(test_fts_tokenizer_DEPENDENCIES) $(EXTRA_test_fts_tokenizer_DEPENDENCIES)
+ @rm -f test-fts-tokenizer$(EXEEXT)
+ $(AM_V_CCLD)$(LINK) $(test_fts_tokenizer_OBJECTS) $(test_fts_tokenizer_LDADD) $(LIBS)
+
+mostlyclean-compile:
+ -rm -f *.$(OBJEXT)
+
+distclean-compile:
+ -rm -f *.tab.c
+
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/fts-filter-common.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/fts-filter-contractions.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/fts-filter-english-possessive.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/fts-filter-lowercase.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/fts-filter-normalizer-icu.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/fts-filter-stemmer-snowball.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/fts-filter-stopwords.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/fts-filter.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/fts-icu.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/fts-language.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/fts-library.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/fts-tokenizer-address.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/fts-tokenizer-common.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/fts-tokenizer-generic.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/fts-tokenizer.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/test-fts-filter.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/test-fts-icu.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/test-fts-language.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/test-fts-tokenizer.Po@am__quote@ # am--include-marker
+
+$(am__depfiles_remade):
+ @$(MKDIR_P) $(@D)
+ @echo '# dummy' >$@-t && $(am__mv) $@-t $@
+
+am--depfiles: $(am__depfiles_remade)
+
+.c.o:
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(COMPILE) -MT $@ -MD -MP -MF $(DEPDIR)/$*.Tpo -c -o $@ $<
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/$*.Tpo $(DEPDIR)/$*.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='$<' object='$@' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(COMPILE) -c -o $@ $<
+
+.c.obj:
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(COMPILE) -MT $@ -MD -MP -MF $(DEPDIR)/$*.Tpo -c -o $@ `$(CYGPATH_W) '$<'`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/$*.Tpo $(DEPDIR)/$*.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='$<' object='$@' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(COMPILE) -c -o $@ `$(CYGPATH_W) '$<'`
+
+.c.lo:
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(LTCOMPILE) -MT $@ -MD -MP -MF $(DEPDIR)/$*.Tpo -c -o $@ $<
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/$*.Tpo $(DEPDIR)/$*.Plo
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='$<' object='$@' libtool=yes @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(LTCOMPILE) -c -o $@ $<
+
+mostlyclean-libtool:
+ -rm -f *.lo
+
+clean-libtool:
+ -rm -rf .libs _libs
+install-dist_stopwordsDATA: $(dist_stopwords_DATA)
+ @$(NORMAL_INSTALL)
+ @list='$(dist_stopwords_DATA)'; test -n "$(stopwordsdir)" || list=; \
+ if test -n "$$list"; then \
+ echo " $(MKDIR_P) '$(DESTDIR)$(stopwordsdir)'"; \
+ $(MKDIR_P) "$(DESTDIR)$(stopwordsdir)" || exit 1; \
+ fi; \
+ for p in $$list; do \
+ if test -f "$$p"; then d=; else d="$(srcdir)/"; fi; \
+ echo "$$d$$p"; \
+ done | $(am__base_list) | \
+ while read files; do \
+ echo " $(INSTALL_DATA) $$files '$(DESTDIR)$(stopwordsdir)'"; \
+ $(INSTALL_DATA) $$files "$(DESTDIR)$(stopwordsdir)" || exit $$?; \
+ done
+
+uninstall-dist_stopwordsDATA:
+ @$(NORMAL_UNINSTALL)
+ @list='$(dist_stopwords_DATA)'; test -n "$(stopwordsdir)" || list=; \
+ files=`for p in $$list; do echo $$p; done | sed -e 's|^.*/||'`; \
+ dir='$(DESTDIR)$(stopwordsdir)'; $(am__uninstall_files_from_dir)
+install-pkginc_libHEADERS: $(pkginc_lib_HEADERS)
+ @$(NORMAL_INSTALL)
+ @list='$(pkginc_lib_HEADERS)'; test -n "$(pkginc_libdir)" || list=; \
+ if test -n "$$list"; then \
+ echo " $(MKDIR_P) '$(DESTDIR)$(pkginc_libdir)'"; \
+ $(MKDIR_P) "$(DESTDIR)$(pkginc_libdir)" || exit 1; \
+ fi; \
+ for p in $$list; do \
+ if test -f "$$p"; then d=; else d="$(srcdir)/"; fi; \
+ echo "$$d$$p"; \
+ done | $(am__base_list) | \
+ while read files; do \
+ echo " $(INSTALL_HEADER) $$files '$(DESTDIR)$(pkginc_libdir)'"; \
+ $(INSTALL_HEADER) $$files "$(DESTDIR)$(pkginc_libdir)" || exit $$?; \
+ done
+
+uninstall-pkginc_libHEADERS:
+ @$(NORMAL_UNINSTALL)
+ @list='$(pkginc_lib_HEADERS)'; test -n "$(pkginc_libdir)" || list=; \
+ files=`for p in $$list; do echo $$p; done | sed -e 's|^.*/||'`; \
+ dir='$(DESTDIR)$(pkginc_libdir)'; $(am__uninstall_files_from_dir)
+
+ID: $(am__tagged_files)
+ $(am__define_uniq_tagged_files); mkid -fID $$unique
+tags: tags-am
+TAGS: tags
+
+tags-am: $(TAGS_DEPENDENCIES) $(am__tagged_files)
+ set x; \
+ here=`pwd`; \
+ $(am__define_uniq_tagged_files); \
+ shift; \
+ if test -z "$(ETAGS_ARGS)$$*$$unique"; then :; else \
+ test -n "$$unique" || unique=$$empty_fix; \
+ if test $$# -gt 0; then \
+ $(ETAGS) $(ETAGSFLAGS) $(AM_ETAGSFLAGS) $(ETAGS_ARGS) \
+ "$$@" $$unique; \
+ else \
+ $(ETAGS) $(ETAGSFLAGS) $(AM_ETAGSFLAGS) $(ETAGS_ARGS) \
+ $$unique; \
+ fi; \
+ fi
+ctags: ctags-am
+
+CTAGS: ctags
+ctags-am: $(TAGS_DEPENDENCIES) $(am__tagged_files)
+ $(am__define_uniq_tagged_files); \
+ test -z "$(CTAGS_ARGS)$$unique" \
+ || $(CTAGS) $(CTAGSFLAGS) $(AM_CTAGSFLAGS) $(CTAGS_ARGS) \
+ $$unique
+
+GTAGS:
+ here=`$(am__cd) $(top_builddir) && pwd` \
+ && $(am__cd) $(top_srcdir) \
+ && gtags -i $(GTAGS_ARGS) "$$here"
+cscopelist: cscopelist-am
+
+cscopelist-am: $(am__tagged_files)
+ list='$(am__tagged_files)'; \
+ case "$(srcdir)" in \
+ [\\/]* | ?:[\\/]*) sdir="$(srcdir)" ;; \
+ *) sdir=$(subdir)/$(srcdir) ;; \
+ esac; \
+ for i in $$list; do \
+ if test -f "$$i"; then \
+ echo "$(subdir)/$$i"; \
+ else \
+ echo "$$sdir/$$i"; \
+ fi; \
+ done >> $(top_builddir)/cscope.files
+
+distclean-tags:
+ -rm -f TAGS ID GTAGS GRTAGS GSYMS GPATH tags
+
+distdir: $(BUILT_SOURCES)
+ $(MAKE) $(AM_MAKEFLAGS) distdir-am
+
+distdir-am: $(DISTFILES)
+ @srcdirstrip=`echo "$(srcdir)" | sed 's/[].[^$$\\*]/\\\\&/g'`; \
+ topsrcdirstrip=`echo "$(top_srcdir)" | sed 's/[].[^$$\\*]/\\\\&/g'`; \
+ list='$(DISTFILES)'; \
+ dist_files=`for file in $$list; do echo $$file; done | \
+ sed -e "s|^$$srcdirstrip/||;t" \
+ -e "s|^$$topsrcdirstrip/|$(top_builddir)/|;t"`; \
+ case $$dist_files in \
+ */*) $(MKDIR_P) `echo "$$dist_files" | \
+ sed '/\//!d;s|^|$(distdir)/|;s,/[^/]*$$,,' | \
+ sort -u` ;; \
+ esac; \
+ for file in $$dist_files; do \
+ if test -f $$file || test -d $$file; then d=.; else d=$(srcdir); fi; \
+ if test -d $$d/$$file; then \
+ dir=`echo "/$$file" | sed -e 's,/[^/]*$$,,'`; \
+ if test -d "$(distdir)/$$file"; then \
+ find "$(distdir)/$$file" -type d ! -perm -700 -exec chmod u+rwx {} \;; \
+ fi; \
+ if test -d $(srcdir)/$$file && test $$d != $(srcdir); then \
+ cp -fpR $(srcdir)/$$file "$(distdir)$$dir" || exit 1; \
+ find "$(distdir)/$$file" -type d ! -perm -700 -exec chmod u+rwx {} \;; \
+ fi; \
+ cp -fpR $$d/$$file "$(distdir)$$dir" || exit 1; \
+ else \
+ test -f "$(distdir)/$$file" \
+ || cp -p $$d/$$file "$(distdir)/$$file" \
+ || exit 1; \
+ fi; \
+ done
+check-am: all-am
+ $(MAKE) $(AM_MAKEFLAGS) check-local
+check: $(BUILT_SOURCES)
+ $(MAKE) $(AM_MAKEFLAGS) check-am
+all-am: Makefile $(PROGRAMS) $(LTLIBRARIES) $(DATA) $(HEADERS)
+installdirs:
+ for dir in "$(DESTDIR)$(pkglibdir)" "$(DESTDIR)$(stopwordsdir)" "$(DESTDIR)$(pkginc_libdir)"; do \
+ test -z "$$dir" || $(MKDIR_P) "$$dir"; \
+ done
+install: $(BUILT_SOURCES)
+ $(MAKE) $(AM_MAKEFLAGS) install-am
+install-exec: install-exec-am
+install-data: install-data-am
+uninstall: uninstall-am
+
+install-am: all-am
+ @$(MAKE) $(AM_MAKEFLAGS) install-exec-am install-data-am
+
+installcheck: installcheck-am
+install-strip:
+ if test -z '$(STRIP)'; then \
+ $(MAKE) $(AM_MAKEFLAGS) INSTALL_PROGRAM="$(INSTALL_STRIP_PROGRAM)" \
+ install_sh_PROGRAM="$(INSTALL_STRIP_PROGRAM)" INSTALL_STRIP_FLAG=-s \
+ install; \
+ else \
+ $(MAKE) $(AM_MAKEFLAGS) INSTALL_PROGRAM="$(INSTALL_STRIP_PROGRAM)" \
+ install_sh_PROGRAM="$(INSTALL_STRIP_PROGRAM)" INSTALL_STRIP_FLAG=-s \
+ "INSTALL_PROGRAM_ENV=STRIPPROG='$(STRIP)'" install; \
+ fi
+mostlyclean-generic:
+
+clean-generic:
+
+distclean-generic:
+ -test -z "$(CONFIG_CLEAN_FILES)" || rm -f $(CONFIG_CLEAN_FILES)
+ -test . = "$(srcdir)" || test -z "$(CONFIG_CLEAN_VPATH_FILES)" || rm -f $(CONFIG_CLEAN_VPATH_FILES)
+
+maintainer-clean-generic:
+ @echo "This command is intended for maintainers to use"
+ @echo "it deletes files that may require special tools to rebuild."
+ -test -z "$(BUILT_SOURCES)" || rm -f $(BUILT_SOURCES)
+clean: clean-am
+
+clean-am: clean-generic clean-libtool clean-noinstLTLIBRARIES \
+ clean-noinstPROGRAMS clean-pkglibLTLIBRARIES mostlyclean-am
+
+distclean: distclean-am
+ -rm -f ./$(DEPDIR)/fts-filter-common.Plo
+ -rm -f ./$(DEPDIR)/fts-filter-contractions.Plo
+ -rm -f ./$(DEPDIR)/fts-filter-english-possessive.Plo
+ -rm -f ./$(DEPDIR)/fts-filter-lowercase.Plo
+ -rm -f ./$(DEPDIR)/fts-filter-normalizer-icu.Plo
+ -rm -f ./$(DEPDIR)/fts-filter-stemmer-snowball.Plo
+ -rm -f ./$(DEPDIR)/fts-filter-stopwords.Plo
+ -rm -f ./$(DEPDIR)/fts-filter.Plo
+ -rm -f ./$(DEPDIR)/fts-icu.Plo
+ -rm -f ./$(DEPDIR)/fts-language.Plo
+ -rm -f ./$(DEPDIR)/fts-library.Plo
+ -rm -f ./$(DEPDIR)/fts-tokenizer-address.Plo
+ -rm -f ./$(DEPDIR)/fts-tokenizer-common.Plo
+ -rm -f ./$(DEPDIR)/fts-tokenizer-generic.Plo
+ -rm -f ./$(DEPDIR)/fts-tokenizer.Plo
+ -rm -f ./$(DEPDIR)/test-fts-filter.Po
+ -rm -f ./$(DEPDIR)/test-fts-icu.Po
+ -rm -f ./$(DEPDIR)/test-fts-language.Po
+ -rm -f ./$(DEPDIR)/test-fts-tokenizer.Po
+ -rm -f Makefile
+distclean-am: clean-am distclean-compile distclean-generic \
+ distclean-tags
+
+dvi: dvi-am
+
+dvi-am:
+
+html: html-am
+
+html-am:
+
+info: info-am
+
+info-am:
+
+install-data-am: install-dist_stopwordsDATA install-pkginc_libHEADERS
+
+install-dvi: install-dvi-am
+
+install-dvi-am:
+
+install-exec-am: install-pkglibLTLIBRARIES
+
+install-html: install-html-am
+
+install-html-am:
+
+install-info: install-info-am
+
+install-info-am:
+
+install-man:
+
+install-pdf: install-pdf-am
+
+install-pdf-am:
+
+install-ps: install-ps-am
+
+install-ps-am:
+
+installcheck-am:
+
+maintainer-clean: maintainer-clean-am
+ -rm -f ./$(DEPDIR)/fts-filter-common.Plo
+ -rm -f ./$(DEPDIR)/fts-filter-contractions.Plo
+ -rm -f ./$(DEPDIR)/fts-filter-english-possessive.Plo
+ -rm -f ./$(DEPDIR)/fts-filter-lowercase.Plo
+ -rm -f ./$(DEPDIR)/fts-filter-normalizer-icu.Plo
+ -rm -f ./$(DEPDIR)/fts-filter-stemmer-snowball.Plo
+ -rm -f ./$(DEPDIR)/fts-filter-stopwords.Plo
+ -rm -f ./$(DEPDIR)/fts-filter.Plo
+ -rm -f ./$(DEPDIR)/fts-icu.Plo
+ -rm -f ./$(DEPDIR)/fts-language.Plo
+ -rm -f ./$(DEPDIR)/fts-library.Plo
+ -rm -f ./$(DEPDIR)/fts-tokenizer-address.Plo
+ -rm -f ./$(DEPDIR)/fts-tokenizer-common.Plo
+ -rm -f ./$(DEPDIR)/fts-tokenizer-generic.Plo
+ -rm -f ./$(DEPDIR)/fts-tokenizer.Plo
+ -rm -f ./$(DEPDIR)/test-fts-filter.Po
+ -rm -f ./$(DEPDIR)/test-fts-icu.Po
+ -rm -f ./$(DEPDIR)/test-fts-language.Po
+ -rm -f ./$(DEPDIR)/test-fts-tokenizer.Po
+ -rm -f Makefile
+maintainer-clean-am: distclean-am maintainer-clean-generic
+
+mostlyclean: mostlyclean-am
+
+mostlyclean-am: mostlyclean-compile mostlyclean-generic \
+ mostlyclean-libtool
+
+pdf: pdf-am
+
+pdf-am:
+
+ps: ps-am
+
+ps-am:
+
+uninstall-am: uninstall-dist_stopwordsDATA uninstall-pkginc_libHEADERS \
+ uninstall-pkglibLTLIBRARIES
+
+.MAKE: all check check-am install install-am install-strip
+
+.PHONY: CTAGS GTAGS TAGS all all-am am--depfiles check check-am \
+ check-local clean clean-generic clean-libtool \
+ clean-noinstLTLIBRARIES clean-noinstPROGRAMS \
+ clean-pkglibLTLIBRARIES cscopelist-am ctags ctags-am distclean \
+ distclean-compile distclean-generic distclean-libtool \
+ distclean-tags distdir dvi dvi-am html html-am info info-am \
+ install install-am install-data install-data-am \
+ install-dist_stopwordsDATA install-dvi install-dvi-am \
+ install-exec install-exec-am install-html install-html-am \
+ install-info install-info-am install-man install-pdf \
+ install-pdf-am install-pkginc_libHEADERS \
+ install-pkglibLTLIBRARIES install-ps install-ps-am \
+ install-strip installcheck installcheck-am installdirs \
+ maintainer-clean maintainer-clean-generic mostlyclean \
+ mostlyclean-compile mostlyclean-generic mostlyclean-libtool \
+ pdf pdf-am ps ps-am tags tags-am uninstall uninstall-am \
+ uninstall-dist_stopwordsDATA uninstall-pkginc_libHEADERS \
+ uninstall-pkglibLTLIBRARIES
+
+.PRECIOUS: Makefile
+
+
+$(srcdir)/WordBreakProperty.txt:
+ test -f $@ || wget -O $@ https://dovecot.org/res/WordBreakProperty.txt
+$(srcdir)/word-boundary-data.c: $(srcdir)/word-properties.pl $(srcdir)/WordBreakProperty.txt
+ perl $(srcdir)/word-properties.pl boundaries $(srcdir)/WordBreakProperty.txt > $@
+
+$(srcdir)/PropList.txt:
+ test -f $@ || wget -O $@ https://dovecot.org/res/PropList.txt
+$(srcdir)/word-break-data.c: $(srcdir)/word-properties.pl $(srcdir)/PropList.txt
+ perl $(srcdir)/word-properties.pl breaks $(srcdir)/PropList.txt > $@
+
+check-local:
+ for bin in $(test_programs); do \
+ if ! $(RUN_TEST) ./$$bin; then exit 1; fi; \
+ done
+
+# Tell versions [3.59,3.63) of GNU make to not export all variables.
+# Otherwise a system limit (for SysV at least) may be exceeded.
+.NOEXPORT:
diff --git a/src/lib-fts/PropList.txt b/src/lib-fts/PropList.txt
new file mode 100644
index 0000000..a8c0da7
--- /dev/null
+++ b/src/lib-fts/PropList.txt
@@ -0,0 +1,1579 @@
+# PropList-9.0.0.txt
+# Date: 2016-06-01, 10:34:30 GMT
+# © 2016 Unicode®, Inc.
+# Unicode and the Unicode Logo are registered trademarks of Unicode, Inc. in the U.S. and other countries.
+# For terms of use, see http://www.unicode.org/terms_of_use.html
+#
+# Unicode Character Database
+# For documentation, see http://www.unicode.org/reports/tr44/
+
+# ================================================
+
+0009..000D ; White_Space # Cc [5] <control-0009>..<control-000D>
+0020 ; White_Space # Zs SPACE
+0085 ; White_Space # Cc <control-0085>
+00A0 ; White_Space # Zs NO-BREAK SPACE
+1680 ; White_Space # Zs OGHAM SPACE MARK
+2000..200A ; White_Space # Zs [11] EN QUAD..HAIR SPACE
+2028 ; White_Space # Zl LINE SEPARATOR
+2029 ; White_Space # Zp PARAGRAPH SEPARATOR
+202F ; White_Space # Zs NARROW NO-BREAK SPACE
+205F ; White_Space # Zs MEDIUM MATHEMATICAL SPACE
+3000 ; White_Space # Zs IDEOGRAPHIC SPACE
+
+# Total code points: 25
+
+# ================================================
+
+061C ; Bidi_Control # Cf ARABIC LETTER MARK
+200E..200F ; Bidi_Control # Cf [2] LEFT-TO-RIGHT MARK..RIGHT-TO-LEFT MARK
+202A..202E ; Bidi_Control # Cf [5] LEFT-TO-RIGHT EMBEDDING..RIGHT-TO-LEFT OVERRIDE
+2066..2069 ; Bidi_Control # Cf [4] LEFT-TO-RIGHT ISOLATE..POP DIRECTIONAL ISOLATE
+
+# Total code points: 12
+
+# ================================================
+
+200C..200D ; Join_Control # Cf [2] ZERO WIDTH NON-JOINER..ZERO WIDTH JOINER
+
+# Total code points: 2
+
+# ================================================
+
+002D ; Dash # Pd HYPHEN-MINUS
+058A ; Dash # Pd ARMENIAN HYPHEN
+05BE ; Dash # Pd HEBREW PUNCTUATION MAQAF
+1400 ; Dash # Pd CANADIAN SYLLABICS HYPHEN
+1806 ; Dash # Pd MONGOLIAN TODO SOFT HYPHEN
+2010..2015 ; Dash # Pd [6] HYPHEN..HORIZONTAL BAR
+2053 ; Dash # Po SWUNG DASH
+207B ; Dash # Sm SUPERSCRIPT MINUS
+208B ; Dash # Sm SUBSCRIPT MINUS
+2212 ; Dash # Sm MINUS SIGN
+2E17 ; Dash # Pd DOUBLE OBLIQUE HYPHEN
+2E1A ; Dash # Pd HYPHEN WITH DIAERESIS
+2E3A..2E3B ; Dash # Pd [2] TWO-EM DASH..THREE-EM DASH
+2E40 ; Dash # Pd DOUBLE HYPHEN
+301C ; Dash # Pd WAVE DASH
+3030 ; Dash # Pd WAVY DASH
+30A0 ; Dash # Pd KATAKANA-HIRAGANA DOUBLE HYPHEN
+FE31..FE32 ; Dash # Pd [2] PRESENTATION FORM FOR VERTICAL EM DASH..PRESENTATION FORM FOR VERTICAL EN DASH
+FE58 ; Dash # Pd SMALL EM DASH
+FE63 ; Dash # Pd SMALL HYPHEN-MINUS
+FF0D ; Dash # Pd FULLWIDTH HYPHEN-MINUS
+
+# Total code points: 28
+
+# ================================================
+
+002D ; Hyphen # Pd HYPHEN-MINUS
+00AD ; Hyphen # Cf SOFT HYPHEN
+058A ; Hyphen # Pd ARMENIAN HYPHEN
+1806 ; Hyphen # Pd MONGOLIAN TODO SOFT HYPHEN
+2010..2011 ; Hyphen # Pd [2] HYPHEN..NON-BREAKING HYPHEN
+2E17 ; Hyphen # Pd DOUBLE OBLIQUE HYPHEN
+30FB ; Hyphen # Po KATAKANA MIDDLE DOT
+FE63 ; Hyphen # Pd SMALL HYPHEN-MINUS
+FF0D ; Hyphen # Pd FULLWIDTH HYPHEN-MINUS
+FF65 ; Hyphen # Po HALFWIDTH KATAKANA MIDDLE DOT
+
+# Total code points: 11
+
+# ================================================
+
+0022 ; Quotation_Mark # Po QUOTATION MARK
+0027 ; Quotation_Mark # Po APOSTROPHE
+00AB ; Quotation_Mark # Pi LEFT-POINTING DOUBLE ANGLE QUOTATION MARK
+00BB ; Quotation_Mark # Pf RIGHT-POINTING DOUBLE ANGLE QUOTATION MARK
+2018 ; Quotation_Mark # Pi LEFT SINGLE QUOTATION MARK
+2019 ; Quotation_Mark # Pf RIGHT SINGLE QUOTATION MARK
+201A ; Quotation_Mark # Ps SINGLE LOW-9 QUOTATION MARK
+201B..201C ; Quotation_Mark # Pi [2] SINGLE HIGH-REVERSED-9 QUOTATION MARK..LEFT DOUBLE QUOTATION MARK
+201D ; Quotation_Mark # Pf RIGHT DOUBLE QUOTATION MARK
+201E ; Quotation_Mark # Ps DOUBLE LOW-9 QUOTATION MARK
+201F ; Quotation_Mark # Pi DOUBLE HIGH-REVERSED-9 QUOTATION MARK
+2039 ; Quotation_Mark # Pi SINGLE LEFT-POINTING ANGLE QUOTATION MARK
+203A ; Quotation_Mark # Pf SINGLE RIGHT-POINTING ANGLE QUOTATION MARK
+2E42 ; Quotation_Mark # Ps DOUBLE LOW-REVERSED-9 QUOTATION MARK
+300C ; Quotation_Mark # Ps LEFT CORNER BRACKET
+300D ; Quotation_Mark # Pe RIGHT CORNER BRACKET
+300E ; Quotation_Mark # Ps LEFT WHITE CORNER BRACKET
+300F ; Quotation_Mark # Pe RIGHT WHITE CORNER BRACKET
+301D ; Quotation_Mark # Ps REVERSED DOUBLE PRIME QUOTATION MARK
+301E..301F ; Quotation_Mark # Pe [2] DOUBLE PRIME QUOTATION MARK..LOW DOUBLE PRIME QUOTATION MARK
+FE41 ; Quotation_Mark # Ps PRESENTATION FORM FOR VERTICAL LEFT CORNER BRACKET
+FE42 ; Quotation_Mark # Pe PRESENTATION FORM FOR VERTICAL RIGHT CORNER BRACKET
+FE43 ; Quotation_Mark # Ps PRESENTATION FORM FOR VERTICAL LEFT WHITE CORNER BRACKET
+FE44 ; Quotation_Mark # Pe PRESENTATION FORM FOR VERTICAL RIGHT WHITE CORNER BRACKET
+FF02 ; Quotation_Mark # Po FULLWIDTH QUOTATION MARK
+FF07 ; Quotation_Mark # Po FULLWIDTH APOSTROPHE
+FF62 ; Quotation_Mark # Ps HALFWIDTH LEFT CORNER BRACKET
+FF63 ; Quotation_Mark # Pe HALFWIDTH RIGHT CORNER BRACKET
+
+# Total code points: 30
+
+# ================================================
+
+0021 ; Terminal_Punctuation # Po EXCLAMATION MARK
+002C ; Terminal_Punctuation # Po COMMA
+002E ; Terminal_Punctuation # Po FULL STOP
+003A..003B ; Terminal_Punctuation # Po [2] COLON..SEMICOLON
+003F ; Terminal_Punctuation # Po QUESTION MARK
+037E ; Terminal_Punctuation # Po GREEK QUESTION MARK
+0387 ; Terminal_Punctuation # Po GREEK ANO TELEIA
+0589 ; Terminal_Punctuation # Po ARMENIAN FULL STOP
+05C3 ; Terminal_Punctuation # Po HEBREW PUNCTUATION SOF PASUQ
+060C ; Terminal_Punctuation # Po ARABIC COMMA
+061B ; Terminal_Punctuation # Po ARABIC SEMICOLON
+061F ; Terminal_Punctuation # Po ARABIC QUESTION MARK
+06D4 ; Terminal_Punctuation # Po ARABIC FULL STOP
+0700..070A ; Terminal_Punctuation # Po [11] SYRIAC END OF PARAGRAPH..SYRIAC CONTRACTION
+070C ; Terminal_Punctuation # Po SYRIAC HARKLEAN METOBELUS
+07F8..07F9 ; Terminal_Punctuation # Po [2] NKO COMMA..NKO EXCLAMATION MARK
+0830..083E ; Terminal_Punctuation # Po [15] SAMARITAN PUNCTUATION NEQUDAA..SAMARITAN PUNCTUATION ANNAAU
+085E ; Terminal_Punctuation # Po MANDAIC PUNCTUATION
+0964..0965 ; Terminal_Punctuation # Po [2] DEVANAGARI DANDA..DEVANAGARI DOUBLE DANDA
+0E5A..0E5B ; Terminal_Punctuation # Po [2] THAI CHARACTER ANGKHANKHU..THAI CHARACTER KHOMUT
+0F08 ; Terminal_Punctuation # Po TIBETAN MARK SBRUL SHAD
+0F0D..0F12 ; Terminal_Punctuation # Po [6] TIBETAN MARK SHAD..TIBETAN MARK RGYA GRAM SHAD
+104A..104B ; Terminal_Punctuation # Po [2] MYANMAR SIGN LITTLE SECTION..MYANMAR SIGN SECTION
+1361..1368 ; Terminal_Punctuation # Po [8] ETHIOPIC WORDSPACE..ETHIOPIC PARAGRAPH SEPARATOR
+166D..166E ; Terminal_Punctuation # Po [2] CANADIAN SYLLABICS CHI SIGN..CANADIAN SYLLABICS FULL STOP
+16EB..16ED ; Terminal_Punctuation # Po [3] RUNIC SINGLE PUNCTUATION..RUNIC CROSS PUNCTUATION
+1735..1736 ; Terminal_Punctuation # Po [2] PHILIPPINE SINGLE PUNCTUATION..PHILIPPINE DOUBLE PUNCTUATION
+17D4..17D6 ; Terminal_Punctuation # Po [3] KHMER SIGN KHAN..KHMER SIGN CAMNUC PII KUUH
+17DA ; Terminal_Punctuation # Po KHMER SIGN KOOMUUT
+1802..1805 ; Terminal_Punctuation # Po [4] MONGOLIAN COMMA..MONGOLIAN FOUR DOTS
+1808..1809 ; Terminal_Punctuation # Po [2] MONGOLIAN MANCHU COMMA..MONGOLIAN MANCHU FULL STOP
+1944..1945 ; Terminal_Punctuation # Po [2] LIMBU EXCLAMATION MARK..LIMBU QUESTION MARK
+1AA8..1AAB ; Terminal_Punctuation # Po [4] TAI THAM SIGN KAAN..TAI THAM SIGN SATKAANKUU
+1B5A..1B5B ; Terminal_Punctuation # Po [2] BALINESE PANTI..BALINESE PAMADA
+1B5D..1B5F ; Terminal_Punctuation # Po [3] BALINESE CARIK PAMUNGKAH..BALINESE CARIK PAREREN
+1C3B..1C3F ; Terminal_Punctuation # Po [5] LEPCHA PUNCTUATION TA-ROL..LEPCHA PUNCTUATION TSHOOK
+1C7E..1C7F ; Terminal_Punctuation # Po [2] OL CHIKI PUNCTUATION MUCAAD..OL CHIKI PUNCTUATION DOUBLE MUCAAD
+203C..203D ; Terminal_Punctuation # Po [2] DOUBLE EXCLAMATION MARK..INTERROBANG
+2047..2049 ; Terminal_Punctuation # Po [3] DOUBLE QUESTION MARK..EXCLAMATION QUESTION MARK
+2E2E ; Terminal_Punctuation # Po REVERSED QUESTION MARK
+2E3C ; Terminal_Punctuation # Po STENOGRAPHIC FULL STOP
+2E41 ; Terminal_Punctuation # Po REVERSED COMMA
+3001..3002 ; Terminal_Punctuation # Po [2] IDEOGRAPHIC COMMA..IDEOGRAPHIC FULL STOP
+A4FE..A4FF ; Terminal_Punctuation # Po [2] LISU PUNCTUATION COMMA..LISU PUNCTUATION FULL STOP
+A60D..A60F ; Terminal_Punctuation # Po [3] VAI COMMA..VAI QUESTION MARK
+A6F3..A6F7 ; Terminal_Punctuation # Po [5] BAMUM FULL STOP..BAMUM QUESTION MARK
+A876..A877 ; Terminal_Punctuation # Po [2] PHAGS-PA MARK SHAD..PHAGS-PA MARK DOUBLE SHAD
+A8CE..A8CF ; Terminal_Punctuation # Po [2] SAURASHTRA DANDA..SAURASHTRA DOUBLE DANDA
+A92F ; Terminal_Punctuation # Po KAYAH LI SIGN SHYA
+A9C7..A9C9 ; Terminal_Punctuation # Po [3] JAVANESE PADA PANGKAT..JAVANESE PADA LUNGSI
+AA5D..AA5F ; Terminal_Punctuation # Po [3] CHAM PUNCTUATION DANDA..CHAM PUNCTUATION TRIPLE DANDA
+AADF ; Terminal_Punctuation # Po TAI VIET SYMBOL KOI KOI
+AAF0..AAF1 ; Terminal_Punctuation # Po [2] MEETEI MAYEK CHEIKHAN..MEETEI MAYEK AHANG KHUDAM
+ABEB ; Terminal_Punctuation # Po MEETEI MAYEK CHEIKHEI
+FE50..FE52 ; Terminal_Punctuation # Po [3] SMALL COMMA..SMALL FULL STOP
+FE54..FE57 ; Terminal_Punctuation # Po [4] SMALL SEMICOLON..SMALL EXCLAMATION MARK
+FF01 ; Terminal_Punctuation # Po FULLWIDTH EXCLAMATION MARK
+FF0C ; Terminal_Punctuation # Po FULLWIDTH COMMA
+FF0E ; Terminal_Punctuation # Po FULLWIDTH FULL STOP
+FF1A..FF1B ; Terminal_Punctuation # Po [2] FULLWIDTH COLON..FULLWIDTH SEMICOLON
+FF1F ; Terminal_Punctuation # Po FULLWIDTH QUESTION MARK
+FF61 ; Terminal_Punctuation # Po HALFWIDTH IDEOGRAPHIC FULL STOP
+FF64 ; Terminal_Punctuation # Po HALFWIDTH IDEOGRAPHIC COMMA
+1039F ; Terminal_Punctuation # Po UGARITIC WORD DIVIDER
+103D0 ; Terminal_Punctuation # Po OLD PERSIAN WORD DIVIDER
+10857 ; Terminal_Punctuation # Po IMPERIAL ARAMAIC SECTION SIGN
+1091F ; Terminal_Punctuation # Po PHOENICIAN WORD SEPARATOR
+10A56..10A57 ; Terminal_Punctuation # Po [2] KHAROSHTHI PUNCTUATION DANDA..KHAROSHTHI PUNCTUATION DOUBLE DANDA
+10AF0..10AF5 ; Terminal_Punctuation # Po [6] MANICHAEAN PUNCTUATION STAR..MANICHAEAN PUNCTUATION TWO DOTS
+10B3A..10B3F ; Terminal_Punctuation # Po [6] TINY TWO DOTS OVER ONE DOT PUNCTUATION..LARGE ONE RING OVER TWO RINGS PUNCTUATION
+10B99..10B9C ; Terminal_Punctuation # Po [4] PSALTER PAHLAVI SECTION MARK..PSALTER PAHLAVI FOUR DOTS WITH DOT
+11047..1104D ; Terminal_Punctuation # Po [7] BRAHMI DANDA..BRAHMI PUNCTUATION LOTUS
+110BE..110C1 ; Terminal_Punctuation # Po [4] KAITHI SECTION MARK..KAITHI DOUBLE DANDA
+11141..11143 ; Terminal_Punctuation # Po [3] CHAKMA DANDA..CHAKMA QUESTION MARK
+111C5..111C6 ; Terminal_Punctuation # Po [2] SHARADA DANDA..SHARADA DOUBLE DANDA
+111CD ; Terminal_Punctuation # Po SHARADA SUTRA MARK
+111DE..111DF ; Terminal_Punctuation # Po [2] SHARADA SECTION MARK-1..SHARADA SECTION MARK-2
+11238..1123C ; Terminal_Punctuation # Po [5] KHOJKI DANDA..KHOJKI DOUBLE SECTION MARK
+112A9 ; Terminal_Punctuation # Po MULTANI SECTION MARK
+1144B..1144D ; Terminal_Punctuation # Po [3] NEWA DANDA..NEWA COMMA
+1145B ; Terminal_Punctuation # Po NEWA PLACEHOLDER MARK
+115C2..115C5 ; Terminal_Punctuation # Po [4] SIDDHAM DANDA..SIDDHAM SEPARATOR BAR
+115C9..115D7 ; Terminal_Punctuation # Po [15] SIDDHAM END OF TEXT MARK..SIDDHAM SECTION MARK WITH CIRCLES AND FOUR ENCLOSURES
+11641..11642 ; Terminal_Punctuation # Po [2] MODI DANDA..MODI DOUBLE DANDA
+1173C..1173E ; Terminal_Punctuation # Po [3] AHOM SIGN SMALL SECTION..AHOM SIGN RULAI
+11C41..11C43 ; Terminal_Punctuation # Po [3] BHAIKSUKI DANDA..BHAIKSUKI WORD SEPARATOR
+11C71 ; Terminal_Punctuation # Po MARCHEN MARK SHAD
+12470..12474 ; Terminal_Punctuation # Po [5] CUNEIFORM PUNCTUATION SIGN OLD ASSYRIAN WORD DIVIDER..CUNEIFORM PUNCTUATION SIGN DIAGONAL QUADCOLON
+16A6E..16A6F ; Terminal_Punctuation # Po [2] MRO DANDA..MRO DOUBLE DANDA
+16AF5 ; Terminal_Punctuation # Po BASSA VAH FULL STOP
+16B37..16B39 ; Terminal_Punctuation # Po [3] PAHAWH HMONG SIGN VOS THOM..PAHAWH HMONG SIGN CIM CHEEM
+16B44 ; Terminal_Punctuation # Po PAHAWH HMONG SIGN XAUS
+1BC9F ; Terminal_Punctuation # Po DUPLOYAN PUNCTUATION CHINOOK FULL STOP
+1DA87..1DA8A ; Terminal_Punctuation # Po [4] SIGNWRITING COMMA..SIGNWRITING COLON
+
+# Total code points: 246
+
+# ================================================
+
+005E ; Other_Math # Sk CIRCUMFLEX ACCENT
+03D0..03D2 ; Other_Math # L& [3] GREEK BETA SYMBOL..GREEK UPSILON WITH HOOK SYMBOL
+03D5 ; Other_Math # L& GREEK PHI SYMBOL
+03F0..03F1 ; Other_Math # L& [2] GREEK KAPPA SYMBOL..GREEK RHO SYMBOL
+03F4..03F5 ; Other_Math # L& [2] GREEK CAPITAL THETA SYMBOL..GREEK LUNATE EPSILON SYMBOL
+2016 ; Other_Math # Po DOUBLE VERTICAL LINE
+2032..2034 ; Other_Math # Po [3] PRIME..TRIPLE PRIME
+2040 ; Other_Math # Pc CHARACTER TIE
+2061..2064 ; Other_Math # Cf [4] FUNCTION APPLICATION..INVISIBLE PLUS
+207D ; Other_Math # Ps SUPERSCRIPT LEFT PARENTHESIS
+207E ; Other_Math # Pe SUPERSCRIPT RIGHT PARENTHESIS
+208D ; Other_Math # Ps SUBSCRIPT LEFT PARENTHESIS
+208E ; Other_Math # Pe SUBSCRIPT RIGHT PARENTHESIS
+20D0..20DC ; Other_Math # Mn [13] COMBINING LEFT HARPOON ABOVE..COMBINING FOUR DOTS ABOVE
+20E1 ; Other_Math # Mn COMBINING LEFT RIGHT ARROW ABOVE
+20E5..20E6 ; Other_Math # Mn [2] COMBINING REVERSE SOLIDUS OVERLAY..COMBINING DOUBLE VERTICAL STROKE OVERLAY
+20EB..20EF ; Other_Math # Mn [5] COMBINING LONG DOUBLE SOLIDUS OVERLAY..COMBINING RIGHT ARROW BELOW
+2102 ; Other_Math # L& DOUBLE-STRUCK CAPITAL C
+2107 ; Other_Math # L& EULER CONSTANT
+210A..2113 ; Other_Math # L& [10] SCRIPT SMALL G..SCRIPT SMALL L
+2115 ; Other_Math # L& DOUBLE-STRUCK CAPITAL N
+2119..211D ; Other_Math # L& [5] DOUBLE-STRUCK CAPITAL P..DOUBLE-STRUCK CAPITAL R
+2124 ; Other_Math # L& DOUBLE-STRUCK CAPITAL Z
+2128 ; Other_Math # L& BLACK-LETTER CAPITAL Z
+2129 ; Other_Math # So TURNED GREEK SMALL LETTER IOTA
+212C..212D ; Other_Math # L& [2] SCRIPT CAPITAL B..BLACK-LETTER CAPITAL C
+212F..2131 ; Other_Math # L& [3] SCRIPT SMALL E..SCRIPT CAPITAL F
+2133..2134 ; Other_Math # L& [2] SCRIPT CAPITAL M..SCRIPT SMALL O
+2135..2138 ; Other_Math # Lo [4] ALEF SYMBOL..DALET SYMBOL
+213C..213F ; Other_Math # L& [4] DOUBLE-STRUCK SMALL PI..DOUBLE-STRUCK CAPITAL PI
+2145..2149 ; Other_Math # L& [5] DOUBLE-STRUCK ITALIC CAPITAL D..DOUBLE-STRUCK ITALIC SMALL J
+2195..2199 ; Other_Math # So [5] UP DOWN ARROW..SOUTH WEST ARROW
+219C..219F ; Other_Math # So [4] LEFTWARDS WAVE ARROW..UPWARDS TWO HEADED ARROW
+21A1..21A2 ; Other_Math # So [2] DOWNWARDS TWO HEADED ARROW..LEFTWARDS ARROW WITH TAIL
+21A4..21A5 ; Other_Math # So [2] LEFTWARDS ARROW FROM BAR..UPWARDS ARROW FROM BAR
+21A7 ; Other_Math # So DOWNWARDS ARROW FROM BAR
+21A9..21AD ; Other_Math # So [5] LEFTWARDS ARROW WITH HOOK..LEFT RIGHT WAVE ARROW
+21B0..21B1 ; Other_Math # So [2] UPWARDS ARROW WITH TIP LEFTWARDS..UPWARDS ARROW WITH TIP RIGHTWARDS
+21B6..21B7 ; Other_Math # So [2] ANTICLOCKWISE TOP SEMICIRCLE ARROW..CLOCKWISE TOP SEMICIRCLE ARROW
+21BC..21CD ; Other_Math # So [18] LEFTWARDS HARPOON WITH BARB UPWARDS..LEFTWARDS DOUBLE ARROW WITH STROKE
+21D0..21D1 ; Other_Math # So [2] LEFTWARDS DOUBLE ARROW..UPWARDS DOUBLE ARROW
+21D3 ; Other_Math # So DOWNWARDS DOUBLE ARROW
+21D5..21DB ; Other_Math # So [7] UP DOWN DOUBLE ARROW..RIGHTWARDS TRIPLE ARROW
+21DD ; Other_Math # So RIGHTWARDS SQUIGGLE ARROW
+21E4..21E5 ; Other_Math # So [2] LEFTWARDS ARROW TO BAR..RIGHTWARDS ARROW TO BAR
+2308 ; Other_Math # Ps LEFT CEILING
+2309 ; Other_Math # Pe RIGHT CEILING
+230A ; Other_Math # Ps LEFT FLOOR
+230B ; Other_Math # Pe RIGHT FLOOR
+23B4..23B5 ; Other_Math # So [2] TOP SQUARE BRACKET..BOTTOM SQUARE BRACKET
+23B7 ; Other_Math # So RADICAL SYMBOL BOTTOM
+23D0 ; Other_Math # So VERTICAL LINE EXTENSION
+23E2 ; Other_Math # So WHITE TRAPEZIUM
+25A0..25A1 ; Other_Math # So [2] BLACK SQUARE..WHITE SQUARE
+25AE..25B6 ; Other_Math # So [9] BLACK VERTICAL RECTANGLE..BLACK RIGHT-POINTING TRIANGLE
+25BC..25C0 ; Other_Math # So [5] BLACK DOWN-POINTING TRIANGLE..BLACK LEFT-POINTING TRIANGLE
+25C6..25C7 ; Other_Math # So [2] BLACK DIAMOND..WHITE DIAMOND
+25CA..25CB ; Other_Math # So [2] LOZENGE..WHITE CIRCLE
+25CF..25D3 ; Other_Math # So [5] BLACK CIRCLE..CIRCLE WITH UPPER HALF BLACK
+25E2 ; Other_Math # So BLACK LOWER RIGHT TRIANGLE
+25E4 ; Other_Math # So BLACK UPPER LEFT TRIANGLE
+25E7..25EC ; Other_Math # So [6] SQUARE WITH LEFT HALF BLACK..WHITE UP-POINTING TRIANGLE WITH DOT
+2605..2606 ; Other_Math # So [2] BLACK STAR..WHITE STAR
+2640 ; Other_Math # So FEMALE SIGN
+2642 ; Other_Math # So MALE SIGN
+2660..2663 ; Other_Math # So [4] BLACK SPADE SUIT..BLACK CLUB SUIT
+266D..266E ; Other_Math # So [2] MUSIC FLAT SIGN..MUSIC NATURAL SIGN
+27C5 ; Other_Math # Ps LEFT S-SHAPED BAG DELIMITER
+27C6 ; Other_Math # Pe RIGHT S-SHAPED BAG DELIMITER
+27E6 ; Other_Math # Ps MATHEMATICAL LEFT WHITE SQUARE BRACKET
+27E7 ; Other_Math # Pe MATHEMATICAL RIGHT WHITE SQUARE BRACKET
+27E8 ; Other_Math # Ps MATHEMATICAL LEFT ANGLE BRACKET
+27E9 ; Other_Math # Pe MATHEMATICAL RIGHT ANGLE BRACKET
+27EA ; Other_Math # Ps MATHEMATICAL LEFT DOUBLE ANGLE BRACKET
+27EB ; Other_Math # Pe MATHEMATICAL RIGHT DOUBLE ANGLE BRACKET
+27EC ; Other_Math # Ps MATHEMATICAL LEFT WHITE TORTOISE SHELL BRACKET
+27ED ; Other_Math # Pe MATHEMATICAL RIGHT WHITE TORTOISE SHELL BRACKET
+27EE ; Other_Math # Ps MATHEMATICAL LEFT FLATTENED PARENTHESIS
+27EF ; Other_Math # Pe MATHEMATICAL RIGHT FLATTENED PARENTHESIS
+2983 ; Other_Math # Ps LEFT WHITE CURLY BRACKET
+2984 ; Other_Math # Pe RIGHT WHITE CURLY BRACKET
+2985 ; Other_Math # Ps LEFT WHITE PARENTHESIS
+2986 ; Other_Math # Pe RIGHT WHITE PARENTHESIS
+2987 ; Other_Math # Ps Z NOTATION LEFT IMAGE BRACKET
+2988 ; Other_Math # Pe Z NOTATION RIGHT IMAGE BRACKET
+2989 ; Other_Math # Ps Z NOTATION LEFT BINDING BRACKET
+298A ; Other_Math # Pe Z NOTATION RIGHT BINDING BRACKET
+298B ; Other_Math # Ps LEFT SQUARE BRACKET WITH UNDERBAR
+298C ; Other_Math # Pe RIGHT SQUARE BRACKET WITH UNDERBAR
+298D ; Other_Math # Ps LEFT SQUARE BRACKET WITH TICK IN TOP CORNER
+298E ; Other_Math # Pe RIGHT SQUARE BRACKET WITH TICK IN BOTTOM CORNER
+298F ; Other_Math # Ps LEFT SQUARE BRACKET WITH TICK IN BOTTOM CORNER
+2990 ; Other_Math # Pe RIGHT SQUARE BRACKET WITH TICK IN TOP CORNER
+2991 ; Other_Math # Ps LEFT ANGLE BRACKET WITH DOT
+2992 ; Other_Math # Pe RIGHT ANGLE BRACKET WITH DOT
+2993 ; Other_Math # Ps LEFT ARC LESS-THAN BRACKET
+2994 ; Other_Math # Pe RIGHT ARC GREATER-THAN BRACKET
+2995 ; Other_Math # Ps DOUBLE LEFT ARC GREATER-THAN BRACKET
+2996 ; Other_Math # Pe DOUBLE RIGHT ARC LESS-THAN BRACKET
+2997 ; Other_Math # Ps LEFT BLACK TORTOISE SHELL BRACKET
+2998 ; Other_Math # Pe RIGHT BLACK TORTOISE SHELL BRACKET
+29D8 ; Other_Math # Ps LEFT WIGGLY FENCE
+29D9 ; Other_Math # Pe RIGHT WIGGLY FENCE
+29DA ; Other_Math # Ps LEFT DOUBLE WIGGLY FENCE
+29DB ; Other_Math # Pe RIGHT DOUBLE WIGGLY FENCE
+29FC ; Other_Math # Ps LEFT-POINTING CURVED ANGLE BRACKET
+29FD ; Other_Math # Pe RIGHT-POINTING CURVED ANGLE BRACKET
+FE61 ; Other_Math # Po SMALL ASTERISK
+FE63 ; Other_Math # Pd SMALL HYPHEN-MINUS
+FE68 ; Other_Math # Po SMALL REVERSE SOLIDUS
+FF3C ; Other_Math # Po FULLWIDTH REVERSE SOLIDUS
+FF3E ; Other_Math # Sk FULLWIDTH CIRCUMFLEX ACCENT
+1D400..1D454 ; Other_Math # L& [85] MATHEMATICAL BOLD CAPITAL A..MATHEMATICAL ITALIC SMALL G
+1D456..1D49C ; Other_Math # L& [71] MATHEMATICAL ITALIC SMALL I..MATHEMATICAL SCRIPT CAPITAL A
+1D49E..1D49F ; Other_Math # L& [2] MATHEMATICAL SCRIPT CAPITAL C..MATHEMATICAL SCRIPT CAPITAL D
+1D4A2 ; Other_Math # L& MATHEMATICAL SCRIPT CAPITAL G
+1D4A5..1D4A6 ; Other_Math # L& [2] MATHEMATICAL SCRIPT CAPITAL J..MATHEMATICAL SCRIPT CAPITAL K
+1D4A9..1D4AC ; Other_Math # L& [4] MATHEMATICAL SCRIPT CAPITAL N..MATHEMATICAL SCRIPT CAPITAL Q
+1D4AE..1D4B9 ; Other_Math # L& [12] MATHEMATICAL SCRIPT CAPITAL S..MATHEMATICAL SCRIPT SMALL D
+1D4BB ; Other_Math # L& MATHEMATICAL SCRIPT SMALL F
+1D4BD..1D4C3 ; Other_Math # L& [7] MATHEMATICAL SCRIPT SMALL H..MATHEMATICAL SCRIPT SMALL N
+1D4C5..1D505 ; Other_Math # L& [65] MATHEMATICAL SCRIPT SMALL P..MATHEMATICAL FRAKTUR CAPITAL B
+1D507..1D50A ; Other_Math # L& [4] MATHEMATICAL FRAKTUR CAPITAL D..MATHEMATICAL FRAKTUR CAPITAL G
+1D50D..1D514 ; Other_Math # L& [8] MATHEMATICAL FRAKTUR CAPITAL J..MATHEMATICAL FRAKTUR CAPITAL Q
+1D516..1D51C ; Other_Math # L& [7] MATHEMATICAL FRAKTUR CAPITAL S..MATHEMATICAL FRAKTUR CAPITAL Y
+1D51E..1D539 ; Other_Math # L& [28] MATHEMATICAL FRAKTUR SMALL A..MATHEMATICAL DOUBLE-STRUCK CAPITAL B
+1D53B..1D53E ; Other_Math # L& [4] MATHEMATICAL DOUBLE-STRUCK CAPITAL D..MATHEMATICAL DOUBLE-STRUCK CAPITAL G
+1D540..1D544 ; Other_Math # L& [5] MATHEMATICAL DOUBLE-STRUCK CAPITAL I..MATHEMATICAL DOUBLE-STRUCK CAPITAL M
+1D546 ; Other_Math # L& MATHEMATICAL DOUBLE-STRUCK CAPITAL O
+1D54A..1D550 ; Other_Math # L& [7] MATHEMATICAL DOUBLE-STRUCK CAPITAL S..MATHEMATICAL DOUBLE-STRUCK CAPITAL Y
+1D552..1D6A5 ; Other_Math # L& [340] MATHEMATICAL DOUBLE-STRUCK SMALL A..MATHEMATICAL ITALIC SMALL DOTLESS J
+1D6A8..1D6C0 ; Other_Math # L& [25] MATHEMATICAL BOLD CAPITAL ALPHA..MATHEMATICAL BOLD CAPITAL OMEGA
+1D6C2..1D6DA ; Other_Math # L& [25] MATHEMATICAL BOLD SMALL ALPHA..MATHEMATICAL BOLD SMALL OMEGA
+1D6DC..1D6FA ; Other_Math # L& [31] MATHEMATICAL BOLD EPSILON SYMBOL..MATHEMATICAL ITALIC CAPITAL OMEGA
+1D6FC..1D714 ; Other_Math # L& [25] MATHEMATICAL ITALIC SMALL ALPHA..MATHEMATICAL ITALIC SMALL OMEGA
+1D716..1D734 ; Other_Math # L& [31] MATHEMATICAL ITALIC EPSILON SYMBOL..MATHEMATICAL BOLD ITALIC CAPITAL OMEGA
+1D736..1D74E ; Other_Math # L& [25] MATHEMATICAL BOLD ITALIC SMALL ALPHA..MATHEMATICAL BOLD ITALIC SMALL OMEGA
+1D750..1D76E ; Other_Math # L& [31] MATHEMATICAL BOLD ITALIC EPSILON SYMBOL..MATHEMATICAL SANS-SERIF BOLD CAPITAL OMEGA
+1D770..1D788 ; Other_Math # L& [25] MATHEMATICAL SANS-SERIF BOLD SMALL ALPHA..MATHEMATICAL SANS-SERIF BOLD SMALL OMEGA
+1D78A..1D7A8 ; Other_Math # L& [31] MATHEMATICAL SANS-SERIF BOLD EPSILON SYMBOL..MATHEMATICAL SANS-SERIF BOLD ITALIC CAPITAL OMEGA
+1D7AA..1D7C2 ; Other_Math # L& [25] MATHEMATICAL SANS-SERIF BOLD ITALIC SMALL ALPHA..MATHEMATICAL SANS-SERIF BOLD ITALIC SMALL OMEGA
+1D7C4..1D7CB ; Other_Math # L& [8] MATHEMATICAL SANS-SERIF BOLD ITALIC EPSILON SYMBOL..MATHEMATICAL BOLD SMALL DIGAMMA
+1D7CE..1D7FF ; Other_Math # Nd [50] MATHEMATICAL BOLD DIGIT ZERO..MATHEMATICAL MONOSPACE DIGIT NINE
+1EE00..1EE03 ; Other_Math # Lo [4] ARABIC MATHEMATICAL ALEF..ARABIC MATHEMATICAL DAL
+1EE05..1EE1F ; Other_Math # Lo [27] ARABIC MATHEMATICAL WAW..ARABIC MATHEMATICAL DOTLESS QAF
+1EE21..1EE22 ; Other_Math # Lo [2] ARABIC MATHEMATICAL INITIAL BEH..ARABIC MATHEMATICAL INITIAL JEEM
+1EE24 ; Other_Math # Lo ARABIC MATHEMATICAL INITIAL HEH
+1EE27 ; Other_Math # Lo ARABIC MATHEMATICAL INITIAL HAH
+1EE29..1EE32 ; Other_Math # Lo [10] ARABIC MATHEMATICAL INITIAL YEH..ARABIC MATHEMATICAL INITIAL QAF
+1EE34..1EE37 ; Other_Math # Lo [4] ARABIC MATHEMATICAL INITIAL SHEEN..ARABIC MATHEMATICAL INITIAL KHAH
+1EE39 ; Other_Math # Lo ARABIC MATHEMATICAL INITIAL DAD
+1EE3B ; Other_Math # Lo ARABIC MATHEMATICAL INITIAL GHAIN
+1EE42 ; Other_Math # Lo ARABIC MATHEMATICAL TAILED JEEM
+1EE47 ; Other_Math # Lo ARABIC MATHEMATICAL TAILED HAH
+1EE49 ; Other_Math # Lo ARABIC MATHEMATICAL TAILED YEH
+1EE4B ; Other_Math # Lo ARABIC MATHEMATICAL TAILED LAM
+1EE4D..1EE4F ; Other_Math # Lo [3] ARABIC MATHEMATICAL TAILED NOON..ARABIC MATHEMATICAL TAILED AIN
+1EE51..1EE52 ; Other_Math # Lo [2] ARABIC MATHEMATICAL TAILED SAD..ARABIC MATHEMATICAL TAILED QAF
+1EE54 ; Other_Math # Lo ARABIC MATHEMATICAL TAILED SHEEN
+1EE57 ; Other_Math # Lo ARABIC MATHEMATICAL TAILED KHAH
+1EE59 ; Other_Math # Lo ARABIC MATHEMATICAL TAILED DAD
+1EE5B ; Other_Math # Lo ARABIC MATHEMATICAL TAILED GHAIN
+1EE5D ; Other_Math # Lo ARABIC MATHEMATICAL TAILED DOTLESS NOON
+1EE5F ; Other_Math # Lo ARABIC MATHEMATICAL TAILED DOTLESS QAF
+1EE61..1EE62 ; Other_Math # Lo [2] ARABIC MATHEMATICAL STRETCHED BEH..ARABIC MATHEMATICAL STRETCHED JEEM
+1EE64 ; Other_Math # Lo ARABIC MATHEMATICAL STRETCHED HEH
+1EE67..1EE6A ; Other_Math # Lo [4] ARABIC MATHEMATICAL STRETCHED HAH..ARABIC MATHEMATICAL STRETCHED KAF
+1EE6C..1EE72 ; Other_Math # Lo [7] ARABIC MATHEMATICAL STRETCHED MEEM..ARABIC MATHEMATICAL STRETCHED QAF
+1EE74..1EE77 ; Other_Math # Lo [4] ARABIC MATHEMATICAL STRETCHED SHEEN..ARABIC MATHEMATICAL STRETCHED KHAH
+1EE79..1EE7C ; Other_Math # Lo [4] ARABIC MATHEMATICAL STRETCHED DAD..ARABIC MATHEMATICAL STRETCHED DOTLESS BEH
+1EE7E ; Other_Math # Lo ARABIC MATHEMATICAL STRETCHED DOTLESS FEH
+1EE80..1EE89 ; Other_Math # Lo [10] ARABIC MATHEMATICAL LOOPED ALEF..ARABIC MATHEMATICAL LOOPED YEH
+1EE8B..1EE9B ; Other_Math # Lo [17] ARABIC MATHEMATICAL LOOPED LAM..ARABIC MATHEMATICAL LOOPED GHAIN
+1EEA1..1EEA3 ; Other_Math # Lo [3] ARABIC MATHEMATICAL DOUBLE-STRUCK BEH..ARABIC MATHEMATICAL DOUBLE-STRUCK DAL
+1EEA5..1EEA9 ; Other_Math # Lo [5] ARABIC MATHEMATICAL DOUBLE-STRUCK WAW..ARABIC MATHEMATICAL DOUBLE-STRUCK YEH
+1EEAB..1EEBB ; Other_Math # Lo [17] ARABIC MATHEMATICAL DOUBLE-STRUCK LAM..ARABIC MATHEMATICAL DOUBLE-STRUCK GHAIN
+
+# Total code points: 1362
+
+# ================================================
+
+0030..0039 ; Hex_Digit # Nd [10] DIGIT ZERO..DIGIT NINE
+0041..0046 ; Hex_Digit # L& [6] LATIN CAPITAL LETTER A..LATIN CAPITAL LETTER F
+0061..0066 ; Hex_Digit # L& [6] LATIN SMALL LETTER A..LATIN SMALL LETTER F
+FF10..FF19 ; Hex_Digit # Nd [10] FULLWIDTH DIGIT ZERO..FULLWIDTH DIGIT NINE
+FF21..FF26 ; Hex_Digit # L& [6] FULLWIDTH LATIN CAPITAL LETTER A..FULLWIDTH LATIN CAPITAL LETTER F
+FF41..FF46 ; Hex_Digit # L& [6] FULLWIDTH LATIN SMALL LETTER A..FULLWIDTH LATIN SMALL LETTER F
+
+# Total code points: 44
+
+# ================================================
+
+0030..0039 ; ASCII_Hex_Digit # Nd [10] DIGIT ZERO..DIGIT NINE
+0041..0046 ; ASCII_Hex_Digit # L& [6] LATIN CAPITAL LETTER A..LATIN CAPITAL LETTER F
+0061..0066 ; ASCII_Hex_Digit # L& [6] LATIN SMALL LETTER A..LATIN SMALL LETTER F
+
+# Total code points: 22
+
+# ================================================
+
+0345 ; Other_Alphabetic # Mn COMBINING GREEK YPOGEGRAMMENI
+05B0..05BD ; Other_Alphabetic # Mn [14] HEBREW POINT SHEVA..HEBREW POINT METEG
+05BF ; Other_Alphabetic # Mn HEBREW POINT RAFE
+05C1..05C2 ; Other_Alphabetic # Mn [2] HEBREW POINT SHIN DOT..HEBREW POINT SIN DOT
+05C4..05C5 ; Other_Alphabetic # Mn [2] HEBREW MARK UPPER DOT..HEBREW MARK LOWER DOT
+05C7 ; Other_Alphabetic # Mn HEBREW POINT QAMATS QATAN
+0610..061A ; Other_Alphabetic # Mn [11] ARABIC SIGN SALLALLAHOU ALAYHE WASSALLAM..ARABIC SMALL KASRA
+064B..0657 ; Other_Alphabetic # Mn [13] ARABIC FATHATAN..ARABIC INVERTED DAMMA
+0659..065F ; Other_Alphabetic # Mn [7] ARABIC ZWARAKAY..ARABIC WAVY HAMZA BELOW
+0670 ; Other_Alphabetic # Mn ARABIC LETTER SUPERSCRIPT ALEF
+06D6..06DC ; Other_Alphabetic # Mn [7] ARABIC SMALL HIGH LIGATURE SAD WITH LAM WITH ALEF MAKSURA..ARABIC SMALL HIGH SEEN
+06E1..06E4 ; Other_Alphabetic # Mn [4] ARABIC SMALL HIGH DOTLESS HEAD OF KHAH..ARABIC SMALL HIGH MADDA
+06E7..06E8 ; Other_Alphabetic # Mn [2] ARABIC SMALL HIGH YEH..ARABIC SMALL HIGH NOON
+06ED ; Other_Alphabetic # Mn ARABIC SMALL LOW MEEM
+0711 ; Other_Alphabetic # Mn SYRIAC LETTER SUPERSCRIPT ALAPH
+0730..073F ; Other_Alphabetic # Mn [16] SYRIAC PTHAHA ABOVE..SYRIAC RWAHA
+07A6..07B0 ; Other_Alphabetic # Mn [11] THAANA ABAFILI..THAANA SUKUN
+0816..0817 ; Other_Alphabetic # Mn [2] SAMARITAN MARK IN..SAMARITAN MARK IN-ALAF
+081B..0823 ; Other_Alphabetic # Mn [9] SAMARITAN MARK EPENTHETIC YUT..SAMARITAN VOWEL SIGN A
+0825..0827 ; Other_Alphabetic # Mn [3] SAMARITAN VOWEL SIGN SHORT A..SAMARITAN VOWEL SIGN U
+0829..082C ; Other_Alphabetic # Mn [4] SAMARITAN VOWEL SIGN LONG I..SAMARITAN VOWEL SIGN SUKUN
+08D4..08DF ; Other_Alphabetic # Mn [12] ARABIC SMALL HIGH WORD AR-RUB..ARABIC SMALL HIGH WORD WAQFA
+08E3..08E9 ; Other_Alphabetic # Mn [7] ARABIC TURNED DAMMA BELOW..ARABIC CURLY KASRATAN
+08F0..0902 ; Other_Alphabetic # Mn [19] ARABIC OPEN FATHATAN..DEVANAGARI SIGN ANUSVARA
+0903 ; Other_Alphabetic # Mc DEVANAGARI SIGN VISARGA
+093A ; Other_Alphabetic # Mn DEVANAGARI VOWEL SIGN OE
+093B ; Other_Alphabetic # Mc DEVANAGARI VOWEL SIGN OOE
+093E..0940 ; Other_Alphabetic # Mc [3] DEVANAGARI VOWEL SIGN AA..DEVANAGARI VOWEL SIGN II
+0941..0948 ; Other_Alphabetic # Mn [8] DEVANAGARI VOWEL SIGN U..DEVANAGARI VOWEL SIGN AI
+0949..094C ; Other_Alphabetic # Mc [4] DEVANAGARI VOWEL SIGN CANDRA O..DEVANAGARI VOWEL SIGN AU
+094E..094F ; Other_Alphabetic # Mc [2] DEVANAGARI VOWEL SIGN PRISHTHAMATRA E..DEVANAGARI VOWEL SIGN AW
+0955..0957 ; Other_Alphabetic # Mn [3] DEVANAGARI VOWEL SIGN CANDRA LONG E..DEVANAGARI VOWEL SIGN UUE
+0962..0963 ; Other_Alphabetic # Mn [2] DEVANAGARI VOWEL SIGN VOCALIC L..DEVANAGARI VOWEL SIGN VOCALIC LL
+0981 ; Other_Alphabetic # Mn BENGALI SIGN CANDRABINDU
+0982..0983 ; Other_Alphabetic # Mc [2] BENGALI SIGN ANUSVARA..BENGALI SIGN VISARGA
+09BE..09C0 ; Other_Alphabetic # Mc [3] BENGALI VOWEL SIGN AA..BENGALI VOWEL SIGN II
+09C1..09C4 ; Other_Alphabetic # Mn [4] BENGALI VOWEL SIGN U..BENGALI VOWEL SIGN VOCALIC RR
+09C7..09C8 ; Other_Alphabetic # Mc [2] BENGALI VOWEL SIGN E..BENGALI VOWEL SIGN AI
+09CB..09CC ; Other_Alphabetic # Mc [2] BENGALI VOWEL SIGN O..BENGALI VOWEL SIGN AU
+09D7 ; Other_Alphabetic # Mc BENGALI AU LENGTH MARK
+09E2..09E3 ; Other_Alphabetic # Mn [2] BENGALI VOWEL SIGN VOCALIC L..BENGALI VOWEL SIGN VOCALIC LL
+0A01..0A02 ; Other_Alphabetic # Mn [2] GURMUKHI SIGN ADAK BINDI..GURMUKHI SIGN BINDI
+0A03 ; Other_Alphabetic # Mc GURMUKHI SIGN VISARGA
+0A3E..0A40 ; Other_Alphabetic # Mc [3] GURMUKHI VOWEL SIGN AA..GURMUKHI VOWEL SIGN II
+0A41..0A42 ; Other_Alphabetic # Mn [2] GURMUKHI VOWEL SIGN U..GURMUKHI VOWEL SIGN UU
+0A47..0A48 ; Other_Alphabetic # Mn [2] GURMUKHI VOWEL SIGN EE..GURMUKHI VOWEL SIGN AI
+0A4B..0A4C ; Other_Alphabetic # Mn [2] GURMUKHI VOWEL SIGN OO..GURMUKHI VOWEL SIGN AU
+0A51 ; Other_Alphabetic # Mn GURMUKHI SIGN UDAAT
+0A70..0A71 ; Other_Alphabetic # Mn [2] GURMUKHI TIPPI..GURMUKHI ADDAK
+0A75 ; Other_Alphabetic # Mn GURMUKHI SIGN YAKASH
+0A81..0A82 ; Other_Alphabetic # Mn [2] GUJARATI SIGN CANDRABINDU..GUJARATI SIGN ANUSVARA
+0A83 ; Other_Alphabetic # Mc GUJARATI SIGN VISARGA
+0ABE..0AC0 ; Other_Alphabetic # Mc [3] GUJARATI VOWEL SIGN AA..GUJARATI VOWEL SIGN II
+0AC1..0AC5 ; Other_Alphabetic # Mn [5] GUJARATI VOWEL SIGN U..GUJARATI VOWEL SIGN CANDRA E
+0AC7..0AC8 ; Other_Alphabetic # Mn [2] GUJARATI VOWEL SIGN E..GUJARATI VOWEL SIGN AI
+0AC9 ; Other_Alphabetic # Mc GUJARATI VOWEL SIGN CANDRA O
+0ACB..0ACC ; Other_Alphabetic # Mc [2] GUJARATI VOWEL SIGN O..GUJARATI VOWEL SIGN AU
+0AE2..0AE3 ; Other_Alphabetic # Mn [2] GUJARATI VOWEL SIGN VOCALIC L..GUJARATI VOWEL SIGN VOCALIC LL
+0B01 ; Other_Alphabetic # Mn ORIYA SIGN CANDRABINDU
+0B02..0B03 ; Other_Alphabetic # Mc [2] ORIYA SIGN ANUSVARA..ORIYA SIGN VISARGA
+0B3E ; Other_Alphabetic # Mc ORIYA VOWEL SIGN AA
+0B3F ; Other_Alphabetic # Mn ORIYA VOWEL SIGN I
+0B40 ; Other_Alphabetic # Mc ORIYA VOWEL SIGN II
+0B41..0B44 ; Other_Alphabetic # Mn [4] ORIYA VOWEL SIGN U..ORIYA VOWEL SIGN VOCALIC RR
+0B47..0B48 ; Other_Alphabetic # Mc [2] ORIYA VOWEL SIGN E..ORIYA VOWEL SIGN AI
+0B4B..0B4C ; Other_Alphabetic # Mc [2] ORIYA VOWEL SIGN O..ORIYA VOWEL SIGN AU
+0B56 ; Other_Alphabetic # Mn ORIYA AI LENGTH MARK
+0B57 ; Other_Alphabetic # Mc ORIYA AU LENGTH MARK
+0B62..0B63 ; Other_Alphabetic # Mn [2] ORIYA VOWEL SIGN VOCALIC L..ORIYA VOWEL SIGN VOCALIC LL
+0B82 ; Other_Alphabetic # Mn TAMIL SIGN ANUSVARA
+0BBE..0BBF ; Other_Alphabetic # Mc [2] TAMIL VOWEL SIGN AA..TAMIL VOWEL SIGN I
+0BC0 ; Other_Alphabetic # Mn TAMIL VOWEL SIGN II
+0BC1..0BC2 ; Other_Alphabetic # Mc [2] TAMIL VOWEL SIGN U..TAMIL VOWEL SIGN UU
+0BC6..0BC8 ; Other_Alphabetic # Mc [3] TAMIL VOWEL SIGN E..TAMIL VOWEL SIGN AI
+0BCA..0BCC ; Other_Alphabetic # Mc [3] TAMIL VOWEL SIGN O..TAMIL VOWEL SIGN AU
+0BD7 ; Other_Alphabetic # Mc TAMIL AU LENGTH MARK
+0C00 ; Other_Alphabetic # Mn TELUGU SIGN COMBINING CANDRABINDU ABOVE
+0C01..0C03 ; Other_Alphabetic # Mc [3] TELUGU SIGN CANDRABINDU..TELUGU SIGN VISARGA
+0C3E..0C40 ; Other_Alphabetic # Mn [3] TELUGU VOWEL SIGN AA..TELUGU VOWEL SIGN II
+0C41..0C44 ; Other_Alphabetic # Mc [4] TELUGU VOWEL SIGN U..TELUGU VOWEL SIGN VOCALIC RR
+0C46..0C48 ; Other_Alphabetic # Mn [3] TELUGU VOWEL SIGN E..TELUGU VOWEL SIGN AI
+0C4A..0C4C ; Other_Alphabetic # Mn [3] TELUGU VOWEL SIGN O..TELUGU VOWEL SIGN AU
+0C55..0C56 ; Other_Alphabetic # Mn [2] TELUGU LENGTH MARK..TELUGU AI LENGTH MARK
+0C62..0C63 ; Other_Alphabetic # Mn [2] TELUGU VOWEL SIGN VOCALIC L..TELUGU VOWEL SIGN VOCALIC LL
+0C81 ; Other_Alphabetic # Mn KANNADA SIGN CANDRABINDU
+0C82..0C83 ; Other_Alphabetic # Mc [2] KANNADA SIGN ANUSVARA..KANNADA SIGN VISARGA
+0CBE ; Other_Alphabetic # Mc KANNADA VOWEL SIGN AA
+0CBF ; Other_Alphabetic # Mn KANNADA VOWEL SIGN I
+0CC0..0CC4 ; Other_Alphabetic # Mc [5] KANNADA VOWEL SIGN II..KANNADA VOWEL SIGN VOCALIC RR
+0CC6 ; Other_Alphabetic # Mn KANNADA VOWEL SIGN E
+0CC7..0CC8 ; Other_Alphabetic # Mc [2] KANNADA VOWEL SIGN EE..KANNADA VOWEL SIGN AI
+0CCA..0CCB ; Other_Alphabetic # Mc [2] KANNADA VOWEL SIGN O..KANNADA VOWEL SIGN OO
+0CCC ; Other_Alphabetic # Mn KANNADA VOWEL SIGN AU
+0CD5..0CD6 ; Other_Alphabetic # Mc [2] KANNADA LENGTH MARK..KANNADA AI LENGTH MARK
+0CE2..0CE3 ; Other_Alphabetic # Mn [2] KANNADA VOWEL SIGN VOCALIC L..KANNADA VOWEL SIGN VOCALIC LL
+0D01 ; Other_Alphabetic # Mn MALAYALAM SIGN CANDRABINDU
+0D02..0D03 ; Other_Alphabetic # Mc [2] MALAYALAM SIGN ANUSVARA..MALAYALAM SIGN VISARGA
+0D3E..0D40 ; Other_Alphabetic # Mc [3] MALAYALAM VOWEL SIGN AA..MALAYALAM VOWEL SIGN II
+0D41..0D44 ; Other_Alphabetic # Mn [4] MALAYALAM VOWEL SIGN U..MALAYALAM VOWEL SIGN VOCALIC RR
+0D46..0D48 ; Other_Alphabetic # Mc [3] MALAYALAM VOWEL SIGN E..MALAYALAM VOWEL SIGN AI
+0D4A..0D4C ; Other_Alphabetic # Mc [3] MALAYALAM VOWEL SIGN O..MALAYALAM VOWEL SIGN AU
+0D57 ; Other_Alphabetic # Mc MALAYALAM AU LENGTH MARK
+0D62..0D63 ; Other_Alphabetic # Mn [2] MALAYALAM VOWEL SIGN VOCALIC L..MALAYALAM VOWEL SIGN VOCALIC LL
+0D82..0D83 ; Other_Alphabetic # Mc [2] SINHALA SIGN ANUSVARAYA..SINHALA SIGN VISARGAYA
+0DCF..0DD1 ; Other_Alphabetic # Mc [3] SINHALA VOWEL SIGN AELA-PILLA..SINHALA VOWEL SIGN DIGA AEDA-PILLA
+0DD2..0DD4 ; Other_Alphabetic # Mn [3] SINHALA VOWEL SIGN KETTI IS-PILLA..SINHALA VOWEL SIGN KETTI PAA-PILLA
+0DD6 ; Other_Alphabetic # Mn SINHALA VOWEL SIGN DIGA PAA-PILLA
+0DD8..0DDF ; Other_Alphabetic # Mc [8] SINHALA VOWEL SIGN GAETTA-PILLA..SINHALA VOWEL SIGN GAYANUKITTA
+0DF2..0DF3 ; Other_Alphabetic # Mc [2] SINHALA VOWEL SIGN DIGA GAETTA-PILLA..SINHALA VOWEL SIGN DIGA GAYANUKITTA
+0E31 ; Other_Alphabetic # Mn THAI CHARACTER MAI HAN-AKAT
+0E34..0E3A ; Other_Alphabetic # Mn [7] THAI CHARACTER SARA I..THAI CHARACTER PHINTHU
+0E4D ; Other_Alphabetic # Mn THAI CHARACTER NIKHAHIT
+0EB1 ; Other_Alphabetic # Mn LAO VOWEL SIGN MAI KAN
+0EB4..0EB9 ; Other_Alphabetic # Mn [6] LAO VOWEL SIGN I..LAO VOWEL SIGN UU
+0EBB..0EBC ; Other_Alphabetic # Mn [2] LAO VOWEL SIGN MAI KON..LAO SEMIVOWEL SIGN LO
+0ECD ; Other_Alphabetic # Mn LAO NIGGAHITA
+0F71..0F7E ; Other_Alphabetic # Mn [14] TIBETAN VOWEL SIGN AA..TIBETAN SIGN RJES SU NGA RO
+0F7F ; Other_Alphabetic # Mc TIBETAN SIGN RNAM BCAD
+0F80..0F81 ; Other_Alphabetic # Mn [2] TIBETAN VOWEL SIGN REVERSED I..TIBETAN VOWEL SIGN REVERSED II
+0F8D..0F97 ; Other_Alphabetic # Mn [11] TIBETAN SUBJOINED SIGN LCE TSA CAN..TIBETAN SUBJOINED LETTER JA
+0F99..0FBC ; Other_Alphabetic # Mn [36] TIBETAN SUBJOINED LETTER NYA..TIBETAN SUBJOINED LETTER FIXED-FORM RA
+102B..102C ; Other_Alphabetic # Mc [2] MYANMAR VOWEL SIGN TALL AA..MYANMAR VOWEL SIGN AA
+102D..1030 ; Other_Alphabetic # Mn [4] MYANMAR VOWEL SIGN I..MYANMAR VOWEL SIGN UU
+1031 ; Other_Alphabetic # Mc MYANMAR VOWEL SIGN E
+1032..1036 ; Other_Alphabetic # Mn [5] MYANMAR VOWEL SIGN AI..MYANMAR SIGN ANUSVARA
+1038 ; Other_Alphabetic # Mc MYANMAR SIGN VISARGA
+103B..103C ; Other_Alphabetic # Mc [2] MYANMAR CONSONANT SIGN MEDIAL YA..MYANMAR CONSONANT SIGN MEDIAL RA
+103D..103E ; Other_Alphabetic # Mn [2] MYANMAR CONSONANT SIGN MEDIAL WA..MYANMAR CONSONANT SIGN MEDIAL HA
+1056..1057 ; Other_Alphabetic # Mc [2] MYANMAR VOWEL SIGN VOCALIC R..MYANMAR VOWEL SIGN VOCALIC RR
+1058..1059 ; Other_Alphabetic # Mn [2] MYANMAR VOWEL SIGN VOCALIC L..MYANMAR VOWEL SIGN VOCALIC LL
+105E..1060 ; Other_Alphabetic # Mn [3] MYANMAR CONSONANT SIGN MON MEDIAL NA..MYANMAR CONSONANT SIGN MON MEDIAL LA
+1062 ; Other_Alphabetic # Mc MYANMAR VOWEL SIGN SGAW KAREN EU
+1067..1068 ; Other_Alphabetic # Mc [2] MYANMAR VOWEL SIGN WESTERN PWO KAREN EU..MYANMAR VOWEL SIGN WESTERN PWO KAREN UE
+1071..1074 ; Other_Alphabetic # Mn [4] MYANMAR VOWEL SIGN GEBA KAREN I..MYANMAR VOWEL SIGN KAYAH EE
+1082 ; Other_Alphabetic # Mn MYANMAR CONSONANT SIGN SHAN MEDIAL WA
+1083..1084 ; Other_Alphabetic # Mc [2] MYANMAR VOWEL SIGN SHAN AA..MYANMAR VOWEL SIGN SHAN E
+1085..1086 ; Other_Alphabetic # Mn [2] MYANMAR VOWEL SIGN SHAN E ABOVE..MYANMAR VOWEL SIGN SHAN FINAL Y
+109C ; Other_Alphabetic # Mc MYANMAR VOWEL SIGN AITON A
+109D ; Other_Alphabetic # Mn MYANMAR VOWEL SIGN AITON AI
+135F ; Other_Alphabetic # Mn ETHIOPIC COMBINING GEMINATION MARK
+1712..1713 ; Other_Alphabetic # Mn [2] TAGALOG VOWEL SIGN I..TAGALOG VOWEL SIGN U
+1732..1733 ; Other_Alphabetic # Mn [2] HANUNOO VOWEL SIGN I..HANUNOO VOWEL SIGN U
+1752..1753 ; Other_Alphabetic # Mn [2] BUHID VOWEL SIGN I..BUHID VOWEL SIGN U
+1772..1773 ; Other_Alphabetic # Mn [2] TAGBANWA VOWEL SIGN I..TAGBANWA VOWEL SIGN U
+17B6 ; Other_Alphabetic # Mc KHMER VOWEL SIGN AA
+17B7..17BD ; Other_Alphabetic # Mn [7] KHMER VOWEL SIGN I..KHMER VOWEL SIGN UA
+17BE..17C5 ; Other_Alphabetic # Mc [8] KHMER VOWEL SIGN OE..KHMER VOWEL SIGN AU
+17C6 ; Other_Alphabetic # Mn KHMER SIGN NIKAHIT
+17C7..17C8 ; Other_Alphabetic # Mc [2] KHMER SIGN REAHMUK..KHMER SIGN YUUKALEAPINTU
+1885..1886 ; Other_Alphabetic # Mn [2] MONGOLIAN LETTER ALI GALI BALUDA..MONGOLIAN LETTER ALI GALI THREE BALUDA
+18A9 ; Other_Alphabetic # Mn MONGOLIAN LETTER ALI GALI DAGALGA
+1920..1922 ; Other_Alphabetic # Mn [3] LIMBU VOWEL SIGN A..LIMBU VOWEL SIGN U
+1923..1926 ; Other_Alphabetic # Mc [4] LIMBU VOWEL SIGN EE..LIMBU VOWEL SIGN AU
+1927..1928 ; Other_Alphabetic # Mn [2] LIMBU VOWEL SIGN E..LIMBU VOWEL SIGN O
+1929..192B ; Other_Alphabetic # Mc [3] LIMBU SUBJOINED LETTER YA..LIMBU SUBJOINED LETTER WA
+1930..1931 ; Other_Alphabetic # Mc [2] LIMBU SMALL LETTER KA..LIMBU SMALL LETTER NGA
+1932 ; Other_Alphabetic # Mn LIMBU SMALL LETTER ANUSVARA
+1933..1938 ; Other_Alphabetic # Mc [6] LIMBU SMALL LETTER TA..LIMBU SMALL LETTER LA
+1A17..1A18 ; Other_Alphabetic # Mn [2] BUGINESE VOWEL SIGN I..BUGINESE VOWEL SIGN U
+1A19..1A1A ; Other_Alphabetic # Mc [2] BUGINESE VOWEL SIGN E..BUGINESE VOWEL SIGN O
+1A1B ; Other_Alphabetic # Mn BUGINESE VOWEL SIGN AE
+1A55 ; Other_Alphabetic # Mc TAI THAM CONSONANT SIGN MEDIAL RA
+1A56 ; Other_Alphabetic # Mn TAI THAM CONSONANT SIGN MEDIAL LA
+1A57 ; Other_Alphabetic # Mc TAI THAM CONSONANT SIGN LA TANG LAI
+1A58..1A5E ; Other_Alphabetic # Mn [7] TAI THAM SIGN MAI KANG LAI..TAI THAM CONSONANT SIGN SA
+1A61 ; Other_Alphabetic # Mc TAI THAM VOWEL SIGN A
+1A62 ; Other_Alphabetic # Mn TAI THAM VOWEL SIGN MAI SAT
+1A63..1A64 ; Other_Alphabetic # Mc [2] TAI THAM VOWEL SIGN AA..TAI THAM VOWEL SIGN TALL AA
+1A65..1A6C ; Other_Alphabetic # Mn [8] TAI THAM VOWEL SIGN I..TAI THAM VOWEL SIGN OA BELOW
+1A6D..1A72 ; Other_Alphabetic # Mc [6] TAI THAM VOWEL SIGN OY..TAI THAM VOWEL SIGN THAM AI
+1A73..1A74 ; Other_Alphabetic # Mn [2] TAI THAM VOWEL SIGN OA ABOVE..TAI THAM SIGN MAI KANG
+1B00..1B03 ; Other_Alphabetic # Mn [4] BALINESE SIGN ULU RICEM..BALINESE SIGN SURANG
+1B04 ; Other_Alphabetic # Mc BALINESE SIGN BISAH
+1B35 ; Other_Alphabetic # Mc BALINESE VOWEL SIGN TEDUNG
+1B36..1B3A ; Other_Alphabetic # Mn [5] BALINESE VOWEL SIGN ULU..BALINESE VOWEL SIGN RA REPA
+1B3B ; Other_Alphabetic # Mc BALINESE VOWEL SIGN RA REPA TEDUNG
+1B3C ; Other_Alphabetic # Mn BALINESE VOWEL SIGN LA LENGA
+1B3D..1B41 ; Other_Alphabetic # Mc [5] BALINESE VOWEL SIGN LA LENGA TEDUNG..BALINESE VOWEL SIGN TALING REPA TEDUNG
+1B42 ; Other_Alphabetic # Mn BALINESE VOWEL SIGN PEPET
+1B43 ; Other_Alphabetic # Mc BALINESE VOWEL SIGN PEPET TEDUNG
+1B80..1B81 ; Other_Alphabetic # Mn [2] SUNDANESE SIGN PANYECEK..SUNDANESE SIGN PANGLAYAR
+1B82 ; Other_Alphabetic # Mc SUNDANESE SIGN PANGWISAD
+1BA1 ; Other_Alphabetic # Mc SUNDANESE CONSONANT SIGN PAMINGKAL
+1BA2..1BA5 ; Other_Alphabetic # Mn [4] SUNDANESE CONSONANT SIGN PANYAKRA..SUNDANESE VOWEL SIGN PANYUKU
+1BA6..1BA7 ; Other_Alphabetic # Mc [2] SUNDANESE VOWEL SIGN PANAELAENG..SUNDANESE VOWEL SIGN PANOLONG
+1BA8..1BA9 ; Other_Alphabetic # Mn [2] SUNDANESE VOWEL SIGN PAMEPET..SUNDANESE VOWEL SIGN PANEULEUNG
+1BAC..1BAD ; Other_Alphabetic # Mn [2] SUNDANESE CONSONANT SIGN PASANGAN MA..SUNDANESE CONSONANT SIGN PASANGAN WA
+1BE7 ; Other_Alphabetic # Mc BATAK VOWEL SIGN E
+1BE8..1BE9 ; Other_Alphabetic # Mn [2] BATAK VOWEL SIGN PAKPAK E..BATAK VOWEL SIGN EE
+1BEA..1BEC ; Other_Alphabetic # Mc [3] BATAK VOWEL SIGN I..BATAK VOWEL SIGN O
+1BED ; Other_Alphabetic # Mn BATAK VOWEL SIGN KARO O
+1BEE ; Other_Alphabetic # Mc BATAK VOWEL SIGN U
+1BEF..1BF1 ; Other_Alphabetic # Mn [3] BATAK VOWEL SIGN U FOR SIMALUNGUN SA..BATAK CONSONANT SIGN H
+1C24..1C2B ; Other_Alphabetic # Mc [8] LEPCHA SUBJOINED LETTER YA..LEPCHA VOWEL SIGN UU
+1C2C..1C33 ; Other_Alphabetic # Mn [8] LEPCHA VOWEL SIGN E..LEPCHA CONSONANT SIGN T
+1C34..1C35 ; Other_Alphabetic # Mc [2] LEPCHA CONSONANT SIGN NYIN-DO..LEPCHA CONSONANT SIGN KANG
+1CF2..1CF3 ; Other_Alphabetic # Mc [2] VEDIC SIGN ARDHAVISARGA..VEDIC SIGN ROTATED ARDHAVISARGA
+1DE7..1DF4 ; Other_Alphabetic # Mn [14] COMBINING LATIN SMALL LETTER ALPHA..COMBINING LATIN SMALL LETTER U WITH DIAERESIS
+24B6..24E9 ; Other_Alphabetic # So [52] CIRCLED LATIN CAPITAL LETTER A..CIRCLED LATIN SMALL LETTER Z
+2DE0..2DFF ; Other_Alphabetic # Mn [32] COMBINING CYRILLIC LETTER BE..COMBINING CYRILLIC LETTER IOTIFIED BIG YUS
+A674..A67B ; Other_Alphabetic # Mn [8] COMBINING CYRILLIC LETTER UKRAINIAN IE..COMBINING CYRILLIC LETTER OMEGA
+A69E..A69F ; Other_Alphabetic # Mn [2] COMBINING CYRILLIC LETTER EF..COMBINING CYRILLIC LETTER IOTIFIED E
+A823..A824 ; Other_Alphabetic # Mc [2] SYLOTI NAGRI VOWEL SIGN A..SYLOTI NAGRI VOWEL SIGN I
+A825..A826 ; Other_Alphabetic # Mn [2] SYLOTI NAGRI VOWEL SIGN U..SYLOTI NAGRI VOWEL SIGN E
+A827 ; Other_Alphabetic # Mc SYLOTI NAGRI VOWEL SIGN OO
+A880..A881 ; Other_Alphabetic # Mc [2] SAURASHTRA SIGN ANUSVARA..SAURASHTRA SIGN VISARGA
+A8B4..A8C3 ; Other_Alphabetic # Mc [16] SAURASHTRA CONSONANT SIGN HAARU..SAURASHTRA VOWEL SIGN AU
+A8C5 ; Other_Alphabetic # Mn SAURASHTRA SIGN CANDRABINDU
+A926..A92A ; Other_Alphabetic # Mn [5] KAYAH LI VOWEL UE..KAYAH LI VOWEL O
+A947..A951 ; Other_Alphabetic # Mn [11] REJANG VOWEL SIGN I..REJANG CONSONANT SIGN R
+A952 ; Other_Alphabetic # Mc REJANG CONSONANT SIGN H
+A980..A982 ; Other_Alphabetic # Mn [3] JAVANESE SIGN PANYANGGA..JAVANESE SIGN LAYAR
+A983 ; Other_Alphabetic # Mc JAVANESE SIGN WIGNYAN
+A9B4..A9B5 ; Other_Alphabetic # Mc [2] JAVANESE VOWEL SIGN TARUNG..JAVANESE VOWEL SIGN TOLONG
+A9B6..A9B9 ; Other_Alphabetic # Mn [4] JAVANESE VOWEL SIGN WULU..JAVANESE VOWEL SIGN SUKU MENDUT
+A9BA..A9BB ; Other_Alphabetic # Mc [2] JAVANESE VOWEL SIGN TALING..JAVANESE VOWEL SIGN DIRGA MURE
+A9BC ; Other_Alphabetic # Mn JAVANESE VOWEL SIGN PEPET
+A9BD..A9BF ; Other_Alphabetic # Mc [3] JAVANESE CONSONANT SIGN KERET..JAVANESE CONSONANT SIGN CAKRA
+AA29..AA2E ; Other_Alphabetic # Mn [6] CHAM VOWEL SIGN AA..CHAM VOWEL SIGN OE
+AA2F..AA30 ; Other_Alphabetic # Mc [2] CHAM VOWEL SIGN O..CHAM VOWEL SIGN AI
+AA31..AA32 ; Other_Alphabetic # Mn [2] CHAM VOWEL SIGN AU..CHAM VOWEL SIGN UE
+AA33..AA34 ; Other_Alphabetic # Mc [2] CHAM CONSONANT SIGN YA..CHAM CONSONANT SIGN RA
+AA35..AA36 ; Other_Alphabetic # Mn [2] CHAM CONSONANT SIGN LA..CHAM CONSONANT SIGN WA
+AA43 ; Other_Alphabetic # Mn CHAM CONSONANT SIGN FINAL NG
+AA4C ; Other_Alphabetic # Mn CHAM CONSONANT SIGN FINAL M
+AA4D ; Other_Alphabetic # Mc CHAM CONSONANT SIGN FINAL H
+AAB0 ; Other_Alphabetic # Mn TAI VIET MAI KANG
+AAB2..AAB4 ; Other_Alphabetic # Mn [3] TAI VIET VOWEL I..TAI VIET VOWEL U
+AAB7..AAB8 ; Other_Alphabetic # Mn [2] TAI VIET MAI KHIT..TAI VIET VOWEL IA
+AABE ; Other_Alphabetic # Mn TAI VIET VOWEL AM
+AAEB ; Other_Alphabetic # Mc MEETEI MAYEK VOWEL SIGN II
+AAEC..AAED ; Other_Alphabetic # Mn [2] MEETEI MAYEK VOWEL SIGN UU..MEETEI MAYEK VOWEL SIGN AAI
+AAEE..AAEF ; Other_Alphabetic # Mc [2] MEETEI MAYEK VOWEL SIGN AU..MEETEI MAYEK VOWEL SIGN AAU
+AAF5 ; Other_Alphabetic # Mc MEETEI MAYEK VOWEL SIGN VISARGA
+ABE3..ABE4 ; Other_Alphabetic # Mc [2] MEETEI MAYEK VOWEL SIGN ONAP..MEETEI MAYEK VOWEL SIGN INAP
+ABE5 ; Other_Alphabetic # Mn MEETEI MAYEK VOWEL SIGN ANAP
+ABE6..ABE7 ; Other_Alphabetic # Mc [2] MEETEI MAYEK VOWEL SIGN YENAP..MEETEI MAYEK VOWEL SIGN SOUNAP
+ABE8 ; Other_Alphabetic # Mn MEETEI MAYEK VOWEL SIGN UNAP
+ABE9..ABEA ; Other_Alphabetic # Mc [2] MEETEI MAYEK VOWEL SIGN CHEINAP..MEETEI MAYEK VOWEL SIGN NUNG
+FB1E ; Other_Alphabetic # Mn HEBREW POINT JUDEO-SPANISH VARIKA
+10376..1037A ; Other_Alphabetic # Mn [5] COMBINING OLD PERMIC LETTER AN..COMBINING OLD PERMIC LETTER SII
+10A01..10A03 ; Other_Alphabetic # Mn [3] KHAROSHTHI VOWEL SIGN I..KHAROSHTHI VOWEL SIGN VOCALIC R
+10A05..10A06 ; Other_Alphabetic # Mn [2] KHAROSHTHI VOWEL SIGN E..KHAROSHTHI VOWEL SIGN O
+10A0C..10A0F ; Other_Alphabetic # Mn [4] KHAROSHTHI VOWEL LENGTH MARK..KHAROSHTHI SIGN VISARGA
+11000 ; Other_Alphabetic # Mc BRAHMI SIGN CANDRABINDU
+11001 ; Other_Alphabetic # Mn BRAHMI SIGN ANUSVARA
+11002 ; Other_Alphabetic # Mc BRAHMI SIGN VISARGA
+11038..11045 ; Other_Alphabetic # Mn [14] BRAHMI VOWEL SIGN AA..BRAHMI VOWEL SIGN AU
+11082 ; Other_Alphabetic # Mc KAITHI SIGN VISARGA
+110B0..110B2 ; Other_Alphabetic # Mc [3] KAITHI VOWEL SIGN AA..KAITHI VOWEL SIGN II
+110B3..110B6 ; Other_Alphabetic # Mn [4] KAITHI VOWEL SIGN U..KAITHI VOWEL SIGN AI
+110B7..110B8 ; Other_Alphabetic # Mc [2] KAITHI VOWEL SIGN O..KAITHI VOWEL SIGN AU
+11100..11102 ; Other_Alphabetic # Mn [3] CHAKMA SIGN CANDRABINDU..CHAKMA SIGN VISARGA
+11127..1112B ; Other_Alphabetic # Mn [5] CHAKMA VOWEL SIGN A..CHAKMA VOWEL SIGN UU
+1112C ; Other_Alphabetic # Mc CHAKMA VOWEL SIGN E
+1112D..11132 ; Other_Alphabetic # Mn [6] CHAKMA VOWEL SIGN AI..CHAKMA AU MARK
+11180..11181 ; Other_Alphabetic # Mn [2] SHARADA SIGN CANDRABINDU..SHARADA SIGN ANUSVARA
+11182 ; Other_Alphabetic # Mc SHARADA SIGN VISARGA
+111B3..111B5 ; Other_Alphabetic # Mc [3] SHARADA VOWEL SIGN AA..SHARADA VOWEL SIGN II
+111B6..111BE ; Other_Alphabetic # Mn [9] SHARADA VOWEL SIGN U..SHARADA VOWEL SIGN O
+111BF ; Other_Alphabetic # Mc SHARADA VOWEL SIGN AU
+1122C..1122E ; Other_Alphabetic # Mc [3] KHOJKI VOWEL SIGN AA..KHOJKI VOWEL SIGN II
+1122F..11231 ; Other_Alphabetic # Mn [3] KHOJKI VOWEL SIGN U..KHOJKI VOWEL SIGN AI
+11232..11233 ; Other_Alphabetic # Mc [2] KHOJKI VOWEL SIGN O..KHOJKI VOWEL SIGN AU
+11234 ; Other_Alphabetic # Mn KHOJKI SIGN ANUSVARA
+11237 ; Other_Alphabetic # Mn KHOJKI SIGN SHADDA
+1123E ; Other_Alphabetic # Mn KHOJKI SIGN SUKUN
+112DF ; Other_Alphabetic # Mn KHUDAWADI SIGN ANUSVARA
+112E0..112E2 ; Other_Alphabetic # Mc [3] KHUDAWADI VOWEL SIGN AA..KHUDAWADI VOWEL SIGN II
+112E3..112E8 ; Other_Alphabetic # Mn [6] KHUDAWADI VOWEL SIGN U..KHUDAWADI VOWEL SIGN AU
+11300..11301 ; Other_Alphabetic # Mn [2] GRANTHA SIGN COMBINING ANUSVARA ABOVE..GRANTHA SIGN CANDRABINDU
+11302..11303 ; Other_Alphabetic # Mc [2] GRANTHA SIGN ANUSVARA..GRANTHA SIGN VISARGA
+1133E..1133F ; Other_Alphabetic # Mc [2] GRANTHA VOWEL SIGN AA..GRANTHA VOWEL SIGN I
+11340 ; Other_Alphabetic # Mn GRANTHA VOWEL SIGN II
+11341..11344 ; Other_Alphabetic # Mc [4] GRANTHA VOWEL SIGN U..GRANTHA VOWEL SIGN VOCALIC RR
+11347..11348 ; Other_Alphabetic # Mc [2] GRANTHA VOWEL SIGN EE..GRANTHA VOWEL SIGN AI
+1134B..1134C ; Other_Alphabetic # Mc [2] GRANTHA VOWEL SIGN OO..GRANTHA VOWEL SIGN AU
+11357 ; Other_Alphabetic # Mc GRANTHA AU LENGTH MARK
+11362..11363 ; Other_Alphabetic # Mc [2] GRANTHA VOWEL SIGN VOCALIC L..GRANTHA VOWEL SIGN VOCALIC LL
+11435..11437 ; Other_Alphabetic # Mc [3] NEWA VOWEL SIGN AA..NEWA VOWEL SIGN II
+11438..1143F ; Other_Alphabetic # Mn [8] NEWA VOWEL SIGN U..NEWA VOWEL SIGN AI
+11440..11441 ; Other_Alphabetic # Mc [2] NEWA VOWEL SIGN O..NEWA VOWEL SIGN AU
+11443..11444 ; Other_Alphabetic # Mn [2] NEWA SIGN CANDRABINDU..NEWA SIGN ANUSVARA
+11445 ; Other_Alphabetic # Mc NEWA SIGN VISARGA
+114B0..114B2 ; Other_Alphabetic # Mc [3] TIRHUTA VOWEL SIGN AA..TIRHUTA VOWEL SIGN II
+114B3..114B8 ; Other_Alphabetic # Mn [6] TIRHUTA VOWEL SIGN U..TIRHUTA VOWEL SIGN VOCALIC LL
+114B9 ; Other_Alphabetic # Mc TIRHUTA VOWEL SIGN E
+114BA ; Other_Alphabetic # Mn TIRHUTA VOWEL SIGN SHORT E
+114BB..114BE ; Other_Alphabetic # Mc [4] TIRHUTA VOWEL SIGN AI..TIRHUTA VOWEL SIGN AU
+114BF..114C0 ; Other_Alphabetic # Mn [2] TIRHUTA SIGN CANDRABINDU..TIRHUTA SIGN ANUSVARA
+114C1 ; Other_Alphabetic # Mc TIRHUTA SIGN VISARGA
+115AF..115B1 ; Other_Alphabetic # Mc [3] SIDDHAM VOWEL SIGN AA..SIDDHAM VOWEL SIGN II
+115B2..115B5 ; Other_Alphabetic # Mn [4] SIDDHAM VOWEL SIGN U..SIDDHAM VOWEL SIGN VOCALIC RR
+115B8..115BB ; Other_Alphabetic # Mc [4] SIDDHAM VOWEL SIGN E..SIDDHAM VOWEL SIGN AU
+115BC..115BD ; Other_Alphabetic # Mn [2] SIDDHAM SIGN CANDRABINDU..SIDDHAM SIGN ANUSVARA
+115BE ; Other_Alphabetic # Mc SIDDHAM SIGN VISARGA
+115DC..115DD ; Other_Alphabetic # Mn [2] SIDDHAM VOWEL SIGN ALTERNATE U..SIDDHAM VOWEL SIGN ALTERNATE UU
+11630..11632 ; Other_Alphabetic # Mc [3] MODI VOWEL SIGN AA..MODI VOWEL SIGN II
+11633..1163A ; Other_Alphabetic # Mn [8] MODI VOWEL SIGN U..MODI VOWEL SIGN AI
+1163B..1163C ; Other_Alphabetic # Mc [2] MODI VOWEL SIGN O..MODI VOWEL SIGN AU
+1163D ; Other_Alphabetic # Mn MODI SIGN ANUSVARA
+1163E ; Other_Alphabetic # Mc MODI SIGN VISARGA
+11640 ; Other_Alphabetic # Mn MODI SIGN ARDHACANDRA
+116AB ; Other_Alphabetic # Mn TAKRI SIGN ANUSVARA
+116AC ; Other_Alphabetic # Mc TAKRI SIGN VISARGA
+116AD ; Other_Alphabetic # Mn TAKRI VOWEL SIGN AA
+116AE..116AF ; Other_Alphabetic # Mc [2] TAKRI VOWEL SIGN I..TAKRI VOWEL SIGN II
+116B0..116B5 ; Other_Alphabetic # Mn [6] TAKRI VOWEL SIGN U..TAKRI VOWEL SIGN AU
+1171D..1171F ; Other_Alphabetic # Mn [3] AHOM CONSONANT SIGN MEDIAL LA..AHOM CONSONANT SIGN MEDIAL LIGATING RA
+11720..11721 ; Other_Alphabetic # Mc [2] AHOM VOWEL SIGN A..AHOM VOWEL SIGN AA
+11722..11725 ; Other_Alphabetic # Mn [4] AHOM VOWEL SIGN I..AHOM VOWEL SIGN UU
+11726 ; Other_Alphabetic # Mc AHOM VOWEL SIGN E
+11727..1172A ; Other_Alphabetic # Mn [4] AHOM VOWEL SIGN AW..AHOM VOWEL SIGN AM
+11C2F ; Other_Alphabetic # Mc BHAIKSUKI VOWEL SIGN AA
+11C30..11C36 ; Other_Alphabetic # Mn [7] BHAIKSUKI VOWEL SIGN I..BHAIKSUKI VOWEL SIGN VOCALIC L
+11C38..11C3D ; Other_Alphabetic # Mn [6] BHAIKSUKI VOWEL SIGN E..BHAIKSUKI SIGN ANUSVARA
+11C3E ; Other_Alphabetic # Mc BHAIKSUKI SIGN VISARGA
+11C92..11CA7 ; Other_Alphabetic # Mn [22] MARCHEN SUBJOINED LETTER KA..MARCHEN SUBJOINED LETTER ZA
+11CA9 ; Other_Alphabetic # Mc MARCHEN SUBJOINED LETTER YA
+11CAA..11CB0 ; Other_Alphabetic # Mn [7] MARCHEN SUBJOINED LETTER RA..MARCHEN VOWEL SIGN AA
+11CB1 ; Other_Alphabetic # Mc MARCHEN VOWEL SIGN I
+11CB2..11CB3 ; Other_Alphabetic # Mn [2] MARCHEN VOWEL SIGN U..MARCHEN VOWEL SIGN E
+11CB4 ; Other_Alphabetic # Mc MARCHEN VOWEL SIGN O
+11CB5..11CB6 ; Other_Alphabetic # Mn [2] MARCHEN SIGN ANUSVARA..MARCHEN SIGN CANDRABINDU
+16B30..16B36 ; Other_Alphabetic # Mn [7] PAHAWH HMONG MARK CIM TUB..PAHAWH HMONG MARK CIM TAUM
+16F51..16F7E ; Other_Alphabetic # Mc [46] MIAO SIGN ASPIRATION..MIAO VOWEL SIGN NG
+1BC9E ; Other_Alphabetic # Mn DUPLOYAN DOUBLE MARK
+1E000..1E006 ; Other_Alphabetic # Mn [7] COMBINING GLAGOLITIC LETTER AZU..COMBINING GLAGOLITIC LETTER ZHIVETE
+1E008..1E018 ; Other_Alphabetic # Mn [17] COMBINING GLAGOLITIC LETTER ZEMLJA..COMBINING GLAGOLITIC LETTER HERU
+1E01B..1E021 ; Other_Alphabetic # Mn [7] COMBINING GLAGOLITIC LETTER SHTA..COMBINING GLAGOLITIC LETTER YATI
+1E023..1E024 ; Other_Alphabetic # Mn [2] COMBINING GLAGOLITIC LETTER YU..COMBINING GLAGOLITIC LETTER SMALL YUS
+1E026..1E02A ; Other_Alphabetic # Mn [5] COMBINING GLAGOLITIC LETTER YO..COMBINING GLAGOLITIC LETTER FITA
+1E947 ; Other_Alphabetic # Mn ADLAM HAMZA
+1F130..1F149 ; Other_Alphabetic # So [26] SQUARED LATIN CAPITAL LETTER A..SQUARED LATIN CAPITAL LETTER Z
+1F150..1F169 ; Other_Alphabetic # So [26] NEGATIVE CIRCLED LATIN CAPITAL LETTER A..NEGATIVE CIRCLED LATIN CAPITAL LETTER Z
+1F170..1F189 ; Other_Alphabetic # So [26] NEGATIVE SQUARED LATIN CAPITAL LETTER A..NEGATIVE SQUARED LATIN CAPITAL LETTER Z
+
+# Total code points: 1238
+
+# ================================================
+
+3006 ; Ideographic # Lo IDEOGRAPHIC CLOSING MARK
+3007 ; Ideographic # Nl IDEOGRAPHIC NUMBER ZERO
+3021..3029 ; Ideographic # Nl [9] HANGZHOU NUMERAL ONE..HANGZHOU NUMERAL NINE
+3038..303A ; Ideographic # Nl [3] HANGZHOU NUMERAL TEN..HANGZHOU NUMERAL THIRTY
+3400..4DB5 ; Ideographic # Lo [6582] CJK UNIFIED IDEOGRAPH-3400..CJK UNIFIED IDEOGRAPH-4DB5
+4E00..9FD5 ; Ideographic # Lo [20950] CJK UNIFIED IDEOGRAPH-4E00..CJK UNIFIED IDEOGRAPH-9FD5
+F900..FA6D ; Ideographic # Lo [366] CJK COMPATIBILITY IDEOGRAPH-F900..CJK COMPATIBILITY IDEOGRAPH-FA6D
+FA70..FAD9 ; Ideographic # Lo [106] CJK COMPATIBILITY IDEOGRAPH-FA70..CJK COMPATIBILITY IDEOGRAPH-FAD9
+17000..187EC ; Ideographic # Lo [6125] TANGUT IDEOGRAPH-17000..TANGUT IDEOGRAPH-187EC
+18800..18AF2 ; Ideographic # Lo [755] TANGUT COMPONENT-001..TANGUT COMPONENT-755
+20000..2A6D6 ; Ideographic # Lo [42711] CJK UNIFIED IDEOGRAPH-20000..CJK UNIFIED IDEOGRAPH-2A6D6
+2A700..2B734 ; Ideographic # Lo [4149] CJK UNIFIED IDEOGRAPH-2A700..CJK UNIFIED IDEOGRAPH-2B734
+2B740..2B81D ; Ideographic # Lo [222] CJK UNIFIED IDEOGRAPH-2B740..CJK UNIFIED IDEOGRAPH-2B81D
+2B820..2CEA1 ; Ideographic # Lo [5762] CJK UNIFIED IDEOGRAPH-2B820..CJK UNIFIED IDEOGRAPH-2CEA1
+2F800..2FA1D ; Ideographic # Lo [542] CJK COMPATIBILITY IDEOGRAPH-2F800..CJK COMPATIBILITY IDEOGRAPH-2FA1D
+
+# Total code points: 88284
+
+# ================================================
+
+005E ; Diacritic # Sk CIRCUMFLEX ACCENT
+0060 ; Diacritic # Sk GRAVE ACCENT
+00A8 ; Diacritic # Sk DIAERESIS
+00AF ; Diacritic # Sk MACRON
+00B4 ; Diacritic # Sk ACUTE ACCENT
+00B7 ; Diacritic # Po MIDDLE DOT
+00B8 ; Diacritic # Sk CEDILLA
+02B0..02C1 ; Diacritic # Lm [18] MODIFIER LETTER SMALL H..MODIFIER LETTER REVERSED GLOTTAL STOP
+02C2..02C5 ; Diacritic # Sk [4] MODIFIER LETTER LEFT ARROWHEAD..MODIFIER LETTER DOWN ARROWHEAD
+02C6..02D1 ; Diacritic # Lm [12] MODIFIER LETTER CIRCUMFLEX ACCENT..MODIFIER LETTER HALF TRIANGULAR COLON
+02D2..02DF ; Diacritic # Sk [14] MODIFIER LETTER CENTRED RIGHT HALF RING..MODIFIER LETTER CROSS ACCENT
+02E0..02E4 ; Diacritic # Lm [5] MODIFIER LETTER SMALL GAMMA..MODIFIER LETTER SMALL REVERSED GLOTTAL STOP
+02E5..02EB ; Diacritic # Sk [7] MODIFIER LETTER EXTRA-HIGH TONE BAR..MODIFIER LETTER YANG DEPARTING TONE MARK
+02EC ; Diacritic # Lm MODIFIER LETTER VOICING
+02ED ; Diacritic # Sk MODIFIER LETTER UNASPIRATED
+02EE ; Diacritic # Lm MODIFIER LETTER DOUBLE APOSTROPHE
+02EF..02FF ; Diacritic # Sk [17] MODIFIER LETTER LOW DOWN ARROWHEAD..MODIFIER LETTER LOW LEFT ARROW
+0300..034E ; Diacritic # Mn [79] COMBINING GRAVE ACCENT..COMBINING UPWARDS ARROW BELOW
+0350..0357 ; Diacritic # Mn [8] COMBINING RIGHT ARROWHEAD ABOVE..COMBINING RIGHT HALF RING ABOVE
+035D..0362 ; Diacritic # Mn [6] COMBINING DOUBLE BREVE..COMBINING DOUBLE RIGHTWARDS ARROW BELOW
+0374 ; Diacritic # Lm GREEK NUMERAL SIGN
+0375 ; Diacritic # Sk GREEK LOWER NUMERAL SIGN
+037A ; Diacritic # Lm GREEK YPOGEGRAMMENI
+0384..0385 ; Diacritic # Sk [2] GREEK TONOS..GREEK DIALYTIKA TONOS
+0483..0487 ; Diacritic # Mn [5] COMBINING CYRILLIC TITLO..COMBINING CYRILLIC POKRYTIE
+0559 ; Diacritic # Lm ARMENIAN MODIFIER LETTER LEFT HALF RING
+0591..05A1 ; Diacritic # Mn [17] HEBREW ACCENT ETNAHTA..HEBREW ACCENT PAZER
+05A3..05BD ; Diacritic # Mn [27] HEBREW ACCENT MUNAH..HEBREW POINT METEG
+05BF ; Diacritic # Mn HEBREW POINT RAFE
+05C1..05C2 ; Diacritic # Mn [2] HEBREW POINT SHIN DOT..HEBREW POINT SIN DOT
+05C4 ; Diacritic # Mn HEBREW MARK UPPER DOT
+064B..0652 ; Diacritic # Mn [8] ARABIC FATHATAN..ARABIC SUKUN
+0657..0658 ; Diacritic # Mn [2] ARABIC INVERTED DAMMA..ARABIC MARK NOON GHUNNA
+06DF..06E0 ; Diacritic # Mn [2] ARABIC SMALL HIGH ROUNDED ZERO..ARABIC SMALL HIGH UPRIGHT RECTANGULAR ZERO
+06E5..06E6 ; Diacritic # Lm [2] ARABIC SMALL WAW..ARABIC SMALL YEH
+06EA..06EC ; Diacritic # Mn [3] ARABIC EMPTY CENTRE LOW STOP..ARABIC ROUNDED HIGH STOP WITH FILLED CENTRE
+0730..074A ; Diacritic # Mn [27] SYRIAC PTHAHA ABOVE..SYRIAC BARREKH
+07A6..07B0 ; Diacritic # Mn [11] THAANA ABAFILI..THAANA SUKUN
+07EB..07F3 ; Diacritic # Mn [9] NKO COMBINING SHORT HIGH TONE..NKO COMBINING DOUBLE DOT ABOVE
+07F4..07F5 ; Diacritic # Lm [2] NKO HIGH TONE APOSTROPHE..NKO LOW TONE APOSTROPHE
+0818..0819 ; Diacritic # Mn [2] SAMARITAN MARK OCCLUSION..SAMARITAN MARK DAGESH
+08E3..08FE ; Diacritic # Mn [28] ARABIC TURNED DAMMA BELOW..ARABIC DAMMA WITH DOT
+093C ; Diacritic # Mn DEVANAGARI SIGN NUKTA
+094D ; Diacritic # Mn DEVANAGARI SIGN VIRAMA
+0951..0954 ; Diacritic # Mn [4] DEVANAGARI STRESS SIGN UDATTA..DEVANAGARI ACUTE ACCENT
+0971 ; Diacritic # Lm DEVANAGARI SIGN HIGH SPACING DOT
+09BC ; Diacritic # Mn BENGALI SIGN NUKTA
+09CD ; Diacritic # Mn BENGALI SIGN VIRAMA
+0A3C ; Diacritic # Mn GURMUKHI SIGN NUKTA
+0A4D ; Diacritic # Mn GURMUKHI SIGN VIRAMA
+0ABC ; Diacritic # Mn GUJARATI SIGN NUKTA
+0ACD ; Diacritic # Mn GUJARATI SIGN VIRAMA
+0B3C ; Diacritic # Mn ORIYA SIGN NUKTA
+0B4D ; Diacritic # Mn ORIYA SIGN VIRAMA
+0BCD ; Diacritic # Mn TAMIL SIGN VIRAMA
+0C4D ; Diacritic # Mn TELUGU SIGN VIRAMA
+0CBC ; Diacritic # Mn KANNADA SIGN NUKTA
+0CCD ; Diacritic # Mn KANNADA SIGN VIRAMA
+0D4D ; Diacritic # Mn MALAYALAM SIGN VIRAMA
+0DCA ; Diacritic # Mn SINHALA SIGN AL-LAKUNA
+0E47..0E4C ; Diacritic # Mn [6] THAI CHARACTER MAITAIKHU..THAI CHARACTER THANTHAKHAT
+0E4E ; Diacritic # Mn THAI CHARACTER YAMAKKAN
+0EC8..0ECC ; Diacritic # Mn [5] LAO TONE MAI EK..LAO CANCELLATION MARK
+0F18..0F19 ; Diacritic # Mn [2] TIBETAN ASTROLOGICAL SIGN -KHYUD PA..TIBETAN ASTROLOGICAL SIGN SDONG TSHUGS
+0F35 ; Diacritic # Mn TIBETAN MARK NGAS BZUNG NYI ZLA
+0F37 ; Diacritic # Mn TIBETAN MARK NGAS BZUNG SGOR RTAGS
+0F39 ; Diacritic # Mn TIBETAN MARK TSA -PHRU
+0F3E..0F3F ; Diacritic # Mc [2] TIBETAN SIGN YAR TSHES..TIBETAN SIGN MAR TSHES
+0F82..0F84 ; Diacritic # Mn [3] TIBETAN SIGN NYI ZLA NAA DA..TIBETAN MARK HALANTA
+0F86..0F87 ; Diacritic # Mn [2] TIBETAN SIGN LCI RTAGS..TIBETAN SIGN YANG RTAGS
+0FC6 ; Diacritic # Mn TIBETAN SYMBOL PADMA GDAN
+1037 ; Diacritic # Mn MYANMAR SIGN DOT BELOW
+1039..103A ; Diacritic # Mn [2] MYANMAR SIGN VIRAMA..MYANMAR SIGN ASAT
+1087..108C ; Diacritic # Mc [6] MYANMAR SIGN SHAN TONE-2..MYANMAR SIGN SHAN COUNCIL TONE-3
+108D ; Diacritic # Mn MYANMAR SIGN SHAN COUNCIL EMPHATIC TONE
+108F ; Diacritic # Mc MYANMAR SIGN RUMAI PALAUNG TONE-5
+109A..109B ; Diacritic # Mc [2] MYANMAR SIGN KHAMTI TONE-1..MYANMAR SIGN KHAMTI TONE-3
+17C9..17D3 ; Diacritic # Mn [11] KHMER SIGN MUUSIKATOAN..KHMER SIGN BATHAMASAT
+17DD ; Diacritic # Mn KHMER SIGN ATTHACAN
+1939..193B ; Diacritic # Mn [3] LIMBU SIGN MUKPHRENG..LIMBU SIGN SA-I
+1A75..1A7C ; Diacritic # Mn [8] TAI THAM SIGN TONE-1..TAI THAM SIGN KHUEN-LUE KARAN
+1A7F ; Diacritic # Mn TAI THAM COMBINING CRYPTOGRAMMIC DOT
+1AB0..1ABD ; Diacritic # Mn [14] COMBINING DOUBLED CIRCUMFLEX ACCENT..COMBINING PARENTHESES BELOW
+1B34 ; Diacritic # Mn BALINESE SIGN REREKAN
+1B44 ; Diacritic # Mc BALINESE ADEG ADEG
+1B6B..1B73 ; Diacritic # Mn [9] BALINESE MUSICAL SYMBOL COMBINING TEGEH..BALINESE MUSICAL SYMBOL COMBINING GONG
+1BAA ; Diacritic # Mc SUNDANESE SIGN PAMAAEH
+1BAB ; Diacritic # Mn SUNDANESE SIGN VIRAMA
+1C36..1C37 ; Diacritic # Mn [2] LEPCHA SIGN RAN..LEPCHA SIGN NUKTA
+1C78..1C7D ; Diacritic # Lm [6] OL CHIKI MU TTUDDAG..OL CHIKI AHAD
+1CD0..1CD2 ; Diacritic # Mn [3] VEDIC TONE KARSHANA..VEDIC TONE PRENKHA
+1CD3 ; Diacritic # Po VEDIC SIGN NIHSHVASA
+1CD4..1CE0 ; Diacritic # Mn [13] VEDIC SIGN YAJURVEDIC MIDLINE SVARITA..VEDIC TONE RIGVEDIC KASHMIRI INDEPENDENT SVARITA
+1CE1 ; Diacritic # Mc VEDIC TONE ATHARVAVEDIC INDEPENDENT SVARITA
+1CE2..1CE8 ; Diacritic # Mn [7] VEDIC SIGN VISARGA SVARITA..VEDIC SIGN VISARGA ANUDATTA WITH TAIL
+1CED ; Diacritic # Mn VEDIC SIGN TIRYAK
+1CF4 ; Diacritic # Mn VEDIC TONE CANDRA ABOVE
+1CF8..1CF9 ; Diacritic # Mn [2] VEDIC TONE RING ABOVE..VEDIC TONE DOUBLE RING ABOVE
+1D2C..1D6A ; Diacritic # Lm [63] MODIFIER LETTER CAPITAL A..GREEK SUBSCRIPT SMALL LETTER CHI
+1DC4..1DCF ; Diacritic # Mn [12] COMBINING MACRON-ACUTE..COMBINING ZIGZAG BELOW
+1DF5 ; Diacritic # Mn COMBINING UP TACK ABOVE
+1DFD..1DFF ; Diacritic # Mn [3] COMBINING ALMOST EQUAL TO BELOW..COMBINING RIGHT ARROWHEAD AND DOWN ARROWHEAD BELOW
+1FBD ; Diacritic # Sk GREEK KORONIS
+1FBF..1FC1 ; Diacritic # Sk [3] GREEK PSILI..GREEK DIALYTIKA AND PERISPOMENI
+1FCD..1FCF ; Diacritic # Sk [3] GREEK PSILI AND VARIA..GREEK PSILI AND PERISPOMENI
+1FDD..1FDF ; Diacritic # Sk [3] GREEK DASIA AND VARIA..GREEK DASIA AND PERISPOMENI
+1FED..1FEF ; Diacritic # Sk [3] GREEK DIALYTIKA AND VARIA..GREEK VARIA
+1FFD..1FFE ; Diacritic # Sk [2] GREEK OXIA..GREEK DASIA
+2CEF..2CF1 ; Diacritic # Mn [3] COPTIC COMBINING NI ABOVE..COPTIC COMBINING SPIRITUS LENIS
+2E2F ; Diacritic # Lm VERTICAL TILDE
+302A..302D ; Diacritic # Mn [4] IDEOGRAPHIC LEVEL TONE MARK..IDEOGRAPHIC ENTERING TONE MARK
+302E..302F ; Diacritic # Mc [2] HANGUL SINGLE DOT TONE MARK..HANGUL DOUBLE DOT TONE MARK
+3099..309A ; Diacritic # Mn [2] COMBINING KATAKANA-HIRAGANA VOICED SOUND MARK..COMBINING KATAKANA-HIRAGANA SEMI-VOICED SOUND MARK
+309B..309C ; Diacritic # Sk [2] KATAKANA-HIRAGANA VOICED SOUND MARK..KATAKANA-HIRAGANA SEMI-VOICED SOUND MARK
+30FC ; Diacritic # Lm KATAKANA-HIRAGANA PROLONGED SOUND MARK
+A66F ; Diacritic # Mn COMBINING CYRILLIC VZMET
+A67C..A67D ; Diacritic # Mn [2] COMBINING CYRILLIC KAVYKA..COMBINING CYRILLIC PAYEROK
+A67F ; Diacritic # Lm CYRILLIC PAYEROK
+A69C..A69D ; Diacritic # Lm [2] MODIFIER LETTER CYRILLIC HARD SIGN..MODIFIER LETTER CYRILLIC SOFT SIGN
+A6F0..A6F1 ; Diacritic # Mn [2] BAMUM COMBINING MARK KOQNDON..BAMUM COMBINING MARK TUKWENTIS
+A717..A71F ; Diacritic # Lm [9] MODIFIER LETTER DOT VERTICAL BAR..MODIFIER LETTER LOW INVERTED EXCLAMATION MARK
+A720..A721 ; Diacritic # Sk [2] MODIFIER LETTER STRESS AND HIGH TONE..MODIFIER LETTER STRESS AND LOW TONE
+A788 ; Diacritic # Lm MODIFIER LETTER LOW CIRCUMFLEX ACCENT
+A7F8..A7F9 ; Diacritic # Lm [2] MODIFIER LETTER CAPITAL H WITH STROKE..MODIFIER LETTER SMALL LIGATURE OE
+A8C4 ; Diacritic # Mn SAURASHTRA SIGN VIRAMA
+A8E0..A8F1 ; Diacritic # Mn [18] COMBINING DEVANAGARI DIGIT ZERO..COMBINING DEVANAGARI SIGN AVAGRAHA
+A92B..A92D ; Diacritic # Mn [3] KAYAH LI TONE PLOPHU..KAYAH LI TONE CALYA PLOPHU
+A92E ; Diacritic # Po KAYAH LI SIGN CWI
+A953 ; Diacritic # Mc REJANG VIRAMA
+A9B3 ; Diacritic # Mn JAVANESE SIGN CECAK TELU
+A9C0 ; Diacritic # Mc JAVANESE PANGKON
+A9E5 ; Diacritic # Mn MYANMAR SIGN SHAN SAW
+AA7B ; Diacritic # Mc MYANMAR SIGN PAO KAREN TONE
+AA7C ; Diacritic # Mn MYANMAR SIGN TAI LAING TONE-2
+AA7D ; Diacritic # Mc MYANMAR SIGN TAI LAING TONE-5
+AABF ; Diacritic # Mn TAI VIET TONE MAI EK
+AAC0 ; Diacritic # Lo TAI VIET TONE MAI NUENG
+AAC1 ; Diacritic # Mn TAI VIET TONE MAI THO
+AAC2 ; Diacritic # Lo TAI VIET TONE MAI SONG
+AAF6 ; Diacritic # Mn MEETEI MAYEK VIRAMA
+AB5B ; Diacritic # Sk MODIFIER BREVE WITH INVERTED BREVE
+AB5C..AB5F ; Diacritic # Lm [4] MODIFIER LETTER SMALL HENG..MODIFIER LETTER SMALL U WITH LEFT HOOK
+ABEC ; Diacritic # Mc MEETEI MAYEK LUM IYEK
+ABED ; Diacritic # Mn MEETEI MAYEK APUN IYEK
+FB1E ; Diacritic # Mn HEBREW POINT JUDEO-SPANISH VARIKA
+FE20..FE2F ; Diacritic # Mn [16] COMBINING LIGATURE LEFT HALF..COMBINING CYRILLIC TITLO RIGHT HALF
+FF3E ; Diacritic # Sk FULLWIDTH CIRCUMFLEX ACCENT
+FF40 ; Diacritic # Sk FULLWIDTH GRAVE ACCENT
+FF70 ; Diacritic # Lm HALFWIDTH KATAKANA-HIRAGANA PROLONGED SOUND MARK
+FF9E..FF9F ; Diacritic # Lm [2] HALFWIDTH KATAKANA VOICED SOUND MARK..HALFWIDTH KATAKANA SEMI-VOICED SOUND MARK
+FFE3 ; Diacritic # Sk FULLWIDTH MACRON
+102E0 ; Diacritic # Mn COPTIC EPACT THOUSANDS MARK
+10AE5..10AE6 ; Diacritic # Mn [2] MANICHAEAN ABBREVIATION MARK ABOVE..MANICHAEAN ABBREVIATION MARK BELOW
+110B9..110BA ; Diacritic # Mn [2] KAITHI SIGN VIRAMA..KAITHI SIGN NUKTA
+11133..11134 ; Diacritic # Mn [2] CHAKMA VIRAMA..CHAKMA MAAYYAA
+11173 ; Diacritic # Mn MAHAJANI SIGN NUKTA
+111C0 ; Diacritic # Mc SHARADA SIGN VIRAMA
+111CA..111CC ; Diacritic # Mn [3] SHARADA SIGN NUKTA..SHARADA EXTRA SHORT VOWEL MARK
+11235 ; Diacritic # Mc KHOJKI SIGN VIRAMA
+11236 ; Diacritic # Mn KHOJKI SIGN NUKTA
+112E9..112EA ; Diacritic # Mn [2] KHUDAWADI SIGN NUKTA..KHUDAWADI SIGN VIRAMA
+1133C ; Diacritic # Mn GRANTHA SIGN NUKTA
+1134D ; Diacritic # Mc GRANTHA SIGN VIRAMA
+11366..1136C ; Diacritic # Mn [7] COMBINING GRANTHA DIGIT ZERO..COMBINING GRANTHA DIGIT SIX
+11370..11374 ; Diacritic # Mn [5] COMBINING GRANTHA LETTER A..COMBINING GRANTHA LETTER PA
+11442 ; Diacritic # Mn NEWA SIGN VIRAMA
+11446 ; Diacritic # Mn NEWA SIGN NUKTA
+114C2..114C3 ; Diacritic # Mn [2] TIRHUTA SIGN VIRAMA..TIRHUTA SIGN NUKTA
+115BF..115C0 ; Diacritic # Mn [2] SIDDHAM SIGN VIRAMA..SIDDHAM SIGN NUKTA
+1163F ; Diacritic # Mn MODI SIGN VIRAMA
+116B6 ; Diacritic # Mc TAKRI SIGN VIRAMA
+116B7 ; Diacritic # Mn TAKRI SIGN NUKTA
+1172B ; Diacritic # Mn AHOM SIGN KILLER
+11C3F ; Diacritic # Mn BHAIKSUKI SIGN VIRAMA
+16AF0..16AF4 ; Diacritic # Mn [5] BASSA VAH COMBINING HIGH TONE..BASSA VAH COMBINING HIGH-LOW TONE
+16F8F..16F92 ; Diacritic # Mn [4] MIAO TONE RIGHT..MIAO TONE BELOW
+16F93..16F9F ; Diacritic # Lm [13] MIAO LETTER TONE-2..MIAO LETTER REFORMED TONE-8
+1D167..1D169 ; Diacritic # Mn [3] MUSICAL SYMBOL COMBINING TREMOLO-1..MUSICAL SYMBOL COMBINING TREMOLO-3
+1D16D..1D172 ; Diacritic # Mc [6] MUSICAL SYMBOL COMBINING AUGMENTATION DOT..MUSICAL SYMBOL COMBINING FLAG-5
+1D17B..1D182 ; Diacritic # Mn [8] MUSICAL SYMBOL COMBINING ACCENT..MUSICAL SYMBOL COMBINING LOURE
+1D185..1D18B ; Diacritic # Mn [7] MUSICAL SYMBOL COMBINING DOIT..MUSICAL SYMBOL COMBINING TRIPLE TONGUE
+1D1AA..1D1AD ; Diacritic # Mn [4] MUSICAL SYMBOL COMBINING DOWN BOW..MUSICAL SYMBOL COMBINING SNAP PIZZICATO
+1E8D0..1E8D6 ; Diacritic # Mn [7] MENDE KIKAKUI COMBINING NUMBER TEENS..MENDE KIKAKUI COMBINING NUMBER MILLIONS
+1E944..1E946 ; Diacritic # Mn [3] ADLAM ALIF LENGTHENER..ADLAM GEMINATION MARK
+1E948..1E94A ; Diacritic # Mn [3] ADLAM CONSONANT MODIFIER..ADLAM NUKTA
+
+# Total code points: 782
+
+# ================================================
+
+00B7 ; Extender # Po MIDDLE DOT
+02D0..02D1 ; Extender # Lm [2] MODIFIER LETTER TRIANGULAR COLON..MODIFIER LETTER HALF TRIANGULAR COLON
+0640 ; Extender # Lm ARABIC TATWEEL
+07FA ; Extender # Lm NKO LAJANYALAN
+0E46 ; Extender # Lm THAI CHARACTER MAIYAMOK
+0EC6 ; Extender # Lm LAO KO LA
+180A ; Extender # Po MONGOLIAN NIRUGU
+1843 ; Extender # Lm MONGOLIAN LETTER TODO LONG VOWEL SIGN
+1AA7 ; Extender # Lm TAI THAM SIGN MAI YAMOK
+1C36 ; Extender # Mn LEPCHA SIGN RAN
+1C7B ; Extender # Lm OL CHIKI RELAA
+3005 ; Extender # Lm IDEOGRAPHIC ITERATION MARK
+3031..3035 ; Extender # Lm [5] VERTICAL KANA REPEAT MARK..VERTICAL KANA REPEAT MARK LOWER HALF
+309D..309E ; Extender # Lm [2] HIRAGANA ITERATION MARK..HIRAGANA VOICED ITERATION MARK
+30FC..30FE ; Extender # Lm [3] KATAKANA-HIRAGANA PROLONGED SOUND MARK..KATAKANA VOICED ITERATION MARK
+A015 ; Extender # Lm YI SYLLABLE WU
+A60C ; Extender # Lm VAI SYLLABLE LENGTHENER
+A9CF ; Extender # Lm JAVANESE PANGRANGKEP
+A9E6 ; Extender # Lm MYANMAR MODIFIER LETTER SHAN REDUPLICATION
+AA70 ; Extender # Lm MYANMAR MODIFIER LETTER KHAMTI REDUPLICATION
+AADD ; Extender # Lm TAI VIET SYMBOL SAM
+AAF3..AAF4 ; Extender # Lm [2] MEETEI MAYEK SYLLABLE REPETITION MARK..MEETEI MAYEK WORD REPETITION MARK
+FF70 ; Extender # Lm HALFWIDTH KATAKANA-HIRAGANA PROLONGED SOUND MARK
+1135D ; Extender # Lo GRANTHA SIGN PLUTA
+115C6..115C8 ; Extender # Po [3] SIDDHAM REPETITION MARK-1..SIDDHAM REPETITION MARK-3
+16B42..16B43 ; Extender # Lm [2] PAHAWH HMONG SIGN VOS NRUA..PAHAWH HMONG SIGN IB YAM
+16FE0 ; Extender # Lm TANGUT ITERATION MARK
+1E944..1E946 ; Extender # Mn [3] ADLAM ALIF LENGTHENER..ADLAM GEMINATION MARK
+
+# Total code points: 42
+
+# ================================================
+
+00AA ; Other_Lowercase # Lo FEMININE ORDINAL INDICATOR
+00BA ; Other_Lowercase # Lo MASCULINE ORDINAL INDICATOR
+02B0..02B8 ; Other_Lowercase # Lm [9] MODIFIER LETTER SMALL H..MODIFIER LETTER SMALL Y
+02C0..02C1 ; Other_Lowercase # Lm [2] MODIFIER LETTER GLOTTAL STOP..MODIFIER LETTER REVERSED GLOTTAL STOP
+02E0..02E4 ; Other_Lowercase # Lm [5] MODIFIER LETTER SMALL GAMMA..MODIFIER LETTER SMALL REVERSED GLOTTAL STOP
+0345 ; Other_Lowercase # Mn COMBINING GREEK YPOGEGRAMMENI
+037A ; Other_Lowercase # Lm GREEK YPOGEGRAMMENI
+1D2C..1D6A ; Other_Lowercase # Lm [63] MODIFIER LETTER CAPITAL A..GREEK SUBSCRIPT SMALL LETTER CHI
+1D78 ; Other_Lowercase # Lm MODIFIER LETTER CYRILLIC EN
+1D9B..1DBF ; Other_Lowercase # Lm [37] MODIFIER LETTER SMALL TURNED ALPHA..MODIFIER LETTER SMALL THETA
+2071 ; Other_Lowercase # Lm SUPERSCRIPT LATIN SMALL LETTER I
+207F ; Other_Lowercase # Lm SUPERSCRIPT LATIN SMALL LETTER N
+2090..209C ; Other_Lowercase # Lm [13] LATIN SUBSCRIPT SMALL LETTER A..LATIN SUBSCRIPT SMALL LETTER T
+2170..217F ; Other_Lowercase # Nl [16] SMALL ROMAN NUMERAL ONE..SMALL ROMAN NUMERAL ONE THOUSAND
+24D0..24E9 ; Other_Lowercase # So [26] CIRCLED LATIN SMALL LETTER A..CIRCLED LATIN SMALL LETTER Z
+2C7C..2C7D ; Other_Lowercase # Lm [2] LATIN SUBSCRIPT SMALL LETTER J..MODIFIER LETTER CAPITAL V
+A69C..A69D ; Other_Lowercase # Lm [2] MODIFIER LETTER CYRILLIC HARD SIGN..MODIFIER LETTER CYRILLIC SOFT SIGN
+A770 ; Other_Lowercase # Lm MODIFIER LETTER US
+A7F8..A7F9 ; Other_Lowercase # Lm [2] MODIFIER LETTER CAPITAL H WITH STROKE..MODIFIER LETTER SMALL LIGATURE OE
+AB5C..AB5F ; Other_Lowercase # Lm [4] MODIFIER LETTER SMALL HENG..MODIFIER LETTER SMALL U WITH LEFT HOOK
+
+# Total code points: 189
+
+# ================================================
+
+2160..216F ; Other_Uppercase # Nl [16] ROMAN NUMERAL ONE..ROMAN NUMERAL ONE THOUSAND
+24B6..24CF ; Other_Uppercase # So [26] CIRCLED LATIN CAPITAL LETTER A..CIRCLED LATIN CAPITAL LETTER Z
+1F130..1F149 ; Other_Uppercase # So [26] SQUARED LATIN CAPITAL LETTER A..SQUARED LATIN CAPITAL LETTER Z
+1F150..1F169 ; Other_Uppercase # So [26] NEGATIVE CIRCLED LATIN CAPITAL LETTER A..NEGATIVE CIRCLED LATIN CAPITAL LETTER Z
+1F170..1F189 ; Other_Uppercase # So [26] NEGATIVE SQUARED LATIN CAPITAL LETTER A..NEGATIVE SQUARED LATIN CAPITAL LETTER Z
+
+# Total code points: 120
+
+# ================================================
+
+FDD0..FDEF ; Noncharacter_Code_Point # Cn [32] <noncharacter-FDD0>..<noncharacter-FDEF>
+FFFE..FFFF ; Noncharacter_Code_Point # Cn [2] <noncharacter-FFFE>..<noncharacter-FFFF>
+1FFFE..1FFFF ; Noncharacter_Code_Point # Cn [2] <noncharacter-1FFFE>..<noncharacter-1FFFF>
+2FFFE..2FFFF ; Noncharacter_Code_Point # Cn [2] <noncharacter-2FFFE>..<noncharacter-2FFFF>
+3FFFE..3FFFF ; Noncharacter_Code_Point # Cn [2] <noncharacter-3FFFE>..<noncharacter-3FFFF>
+4FFFE..4FFFF ; Noncharacter_Code_Point # Cn [2] <noncharacter-4FFFE>..<noncharacter-4FFFF>
+5FFFE..5FFFF ; Noncharacter_Code_Point # Cn [2] <noncharacter-5FFFE>..<noncharacter-5FFFF>
+6FFFE..6FFFF ; Noncharacter_Code_Point # Cn [2] <noncharacter-6FFFE>..<noncharacter-6FFFF>
+7FFFE..7FFFF ; Noncharacter_Code_Point # Cn [2] <noncharacter-7FFFE>..<noncharacter-7FFFF>
+8FFFE..8FFFF ; Noncharacter_Code_Point # Cn [2] <noncharacter-8FFFE>..<noncharacter-8FFFF>
+9FFFE..9FFFF ; Noncharacter_Code_Point # Cn [2] <noncharacter-9FFFE>..<noncharacter-9FFFF>
+AFFFE..AFFFF ; Noncharacter_Code_Point # Cn [2] <noncharacter-AFFFE>..<noncharacter-AFFFF>
+BFFFE..BFFFF ; Noncharacter_Code_Point # Cn [2] <noncharacter-BFFFE>..<noncharacter-BFFFF>
+CFFFE..CFFFF ; Noncharacter_Code_Point # Cn [2] <noncharacter-CFFFE>..<noncharacter-CFFFF>
+DFFFE..DFFFF ; Noncharacter_Code_Point # Cn [2] <noncharacter-DFFFE>..<noncharacter-DFFFF>
+EFFFE..EFFFF ; Noncharacter_Code_Point # Cn [2] <noncharacter-EFFFE>..<noncharacter-EFFFF>
+FFFFE..FFFFF ; Noncharacter_Code_Point # Cn [2] <noncharacter-FFFFE>..<noncharacter-FFFFF>
+10FFFE..10FFFF; Noncharacter_Code_Point # Cn [2] <noncharacter-10FFFE>..<noncharacter-10FFFF>
+
+# Total code points: 66
+
+# ================================================
+
+09BE ; Other_Grapheme_Extend # Mc BENGALI VOWEL SIGN AA
+09D7 ; Other_Grapheme_Extend # Mc BENGALI AU LENGTH MARK
+0B3E ; Other_Grapheme_Extend # Mc ORIYA VOWEL SIGN AA
+0B57 ; Other_Grapheme_Extend # Mc ORIYA AU LENGTH MARK
+0BBE ; Other_Grapheme_Extend # Mc TAMIL VOWEL SIGN AA
+0BD7 ; Other_Grapheme_Extend # Mc TAMIL AU LENGTH MARK
+0CC2 ; Other_Grapheme_Extend # Mc KANNADA VOWEL SIGN UU
+0CD5..0CD6 ; Other_Grapheme_Extend # Mc [2] KANNADA LENGTH MARK..KANNADA AI LENGTH MARK
+0D3E ; Other_Grapheme_Extend # Mc MALAYALAM VOWEL SIGN AA
+0D57 ; Other_Grapheme_Extend # Mc MALAYALAM AU LENGTH MARK
+0DCF ; Other_Grapheme_Extend # Mc SINHALA VOWEL SIGN AELA-PILLA
+0DDF ; Other_Grapheme_Extend # Mc SINHALA VOWEL SIGN GAYANUKITTA
+200C ; Other_Grapheme_Extend # Cf ZERO WIDTH NON-JOINER
+302E..302F ; Other_Grapheme_Extend # Mc [2] HANGUL SINGLE DOT TONE MARK..HANGUL DOUBLE DOT TONE MARK
+FF9E..FF9F ; Other_Grapheme_Extend # Lm [2] HALFWIDTH KATAKANA VOICED SOUND MARK..HALFWIDTH KATAKANA SEMI-VOICED SOUND MARK
+1133E ; Other_Grapheme_Extend # Mc GRANTHA VOWEL SIGN AA
+11357 ; Other_Grapheme_Extend # Mc GRANTHA AU LENGTH MARK
+114B0 ; Other_Grapheme_Extend # Mc TIRHUTA VOWEL SIGN AA
+114BD ; Other_Grapheme_Extend # Mc TIRHUTA VOWEL SIGN SHORT O
+115AF ; Other_Grapheme_Extend # Mc SIDDHAM VOWEL SIGN AA
+1D165 ; Other_Grapheme_Extend # Mc MUSICAL SYMBOL COMBINING STEM
+1D16E..1D172 ; Other_Grapheme_Extend # Mc [5] MUSICAL SYMBOL COMBINING FLAG-1..MUSICAL SYMBOL COMBINING FLAG-5
+E0020..E007F ; Other_Grapheme_Extend # Cf [96] TAG SPACE..CANCEL TAG
+
+# Total code points: 125
+
+# ================================================
+
+2FF0..2FF1 ; IDS_Binary_Operator # So [2] IDEOGRAPHIC DESCRIPTION CHARACTER LEFT TO RIGHT..IDEOGRAPHIC DESCRIPTION CHARACTER ABOVE TO BELOW
+2FF4..2FFB ; IDS_Binary_Operator # So [8] IDEOGRAPHIC DESCRIPTION CHARACTER FULL SURROUND..IDEOGRAPHIC DESCRIPTION CHARACTER OVERLAID
+
+# Total code points: 10
+
+# ================================================
+
+2FF2..2FF3 ; IDS_Trinary_Operator # So [2] IDEOGRAPHIC DESCRIPTION CHARACTER LEFT TO MIDDLE AND RIGHT..IDEOGRAPHIC DESCRIPTION CHARACTER ABOVE TO MIDDLE AND BELOW
+
+# Total code points: 2
+
+# ================================================
+
+2E80..2E99 ; Radical # So [26] CJK RADICAL REPEAT..CJK RADICAL RAP
+2E9B..2EF3 ; Radical # So [89] CJK RADICAL CHOKE..CJK RADICAL C-SIMPLIFIED TURTLE
+2F00..2FD5 ; Radical # So [214] KANGXI RADICAL ONE..KANGXI RADICAL FLUTE
+
+# Total code points: 329
+
+# ================================================
+
+3400..4DB5 ; Unified_Ideograph # Lo [6582] CJK UNIFIED IDEOGRAPH-3400..CJK UNIFIED IDEOGRAPH-4DB5
+4E00..9FD5 ; Unified_Ideograph # Lo [20950] CJK UNIFIED IDEOGRAPH-4E00..CJK UNIFIED IDEOGRAPH-9FD5
+FA0E..FA0F ; Unified_Ideograph # Lo [2] CJK COMPATIBILITY IDEOGRAPH-FA0E..CJK COMPATIBILITY IDEOGRAPH-FA0F
+FA11 ; Unified_Ideograph # Lo CJK COMPATIBILITY IDEOGRAPH-FA11
+FA13..FA14 ; Unified_Ideograph # Lo [2] CJK COMPATIBILITY IDEOGRAPH-FA13..CJK COMPATIBILITY IDEOGRAPH-FA14
+FA1F ; Unified_Ideograph # Lo CJK COMPATIBILITY IDEOGRAPH-FA1F
+FA21 ; Unified_Ideograph # Lo CJK COMPATIBILITY IDEOGRAPH-FA21
+FA23..FA24 ; Unified_Ideograph # Lo [2] CJK COMPATIBILITY IDEOGRAPH-FA23..CJK COMPATIBILITY IDEOGRAPH-FA24
+FA27..FA29 ; Unified_Ideograph # Lo [3] CJK COMPATIBILITY IDEOGRAPH-FA27..CJK COMPATIBILITY IDEOGRAPH-FA29
+20000..2A6D6 ; Unified_Ideograph # Lo [42711] CJK UNIFIED IDEOGRAPH-20000..CJK UNIFIED IDEOGRAPH-2A6D6
+2A700..2B734 ; Unified_Ideograph # Lo [4149] CJK UNIFIED IDEOGRAPH-2A700..CJK UNIFIED IDEOGRAPH-2B734
+2B740..2B81D ; Unified_Ideograph # Lo [222] CJK UNIFIED IDEOGRAPH-2B740..CJK UNIFIED IDEOGRAPH-2B81D
+2B820..2CEA1 ; Unified_Ideograph # Lo [5762] CJK UNIFIED IDEOGRAPH-2B820..CJK UNIFIED IDEOGRAPH-2CEA1
+
+# Total code points: 80388
+
+# ================================================
+
+034F ; Other_Default_Ignorable_Code_Point # Mn COMBINING GRAPHEME JOINER
+115F..1160 ; Other_Default_Ignorable_Code_Point # Lo [2] HANGUL CHOSEONG FILLER..HANGUL JUNGSEONG FILLER
+17B4..17B5 ; Other_Default_Ignorable_Code_Point # Mn [2] KHMER VOWEL INHERENT AQ..KHMER VOWEL INHERENT AA
+2065 ; Other_Default_Ignorable_Code_Point # Cn <reserved-2065>
+3164 ; Other_Default_Ignorable_Code_Point # Lo HANGUL FILLER
+FFA0 ; Other_Default_Ignorable_Code_Point # Lo HALFWIDTH HANGUL FILLER
+FFF0..FFF8 ; Other_Default_Ignorable_Code_Point # Cn [9] <reserved-FFF0>..<reserved-FFF8>
+E0000 ; Other_Default_Ignorable_Code_Point # Cn <reserved-E0000>
+E0002..E001F ; Other_Default_Ignorable_Code_Point # Cn [30] <reserved-E0002>..<reserved-E001F>
+E0080..E00FF ; Other_Default_Ignorable_Code_Point # Cn [128] <reserved-E0080>..<reserved-E00FF>
+E01F0..E0FFF ; Other_Default_Ignorable_Code_Point # Cn [3600] <reserved-E01F0>..<reserved-E0FFF>
+
+# Total code points: 3776
+
+# ================================================
+
+0149 ; Deprecated # L& LATIN SMALL LETTER N PRECEDED BY APOSTROPHE
+0673 ; Deprecated # Lo ARABIC LETTER ALEF WITH WAVY HAMZA BELOW
+0F77 ; Deprecated # Mn TIBETAN VOWEL SIGN VOCALIC RR
+0F79 ; Deprecated # Mn TIBETAN VOWEL SIGN VOCALIC LL
+17A3..17A4 ; Deprecated # Lo [2] KHMER INDEPENDENT VOWEL QAQ..KHMER INDEPENDENT VOWEL QAA
+206A..206F ; Deprecated # Cf [6] INHIBIT SYMMETRIC SWAPPING..NOMINAL DIGIT SHAPES
+2329 ; Deprecated # Ps LEFT-POINTING ANGLE BRACKET
+232A ; Deprecated # Pe RIGHT-POINTING ANGLE BRACKET
+E0001 ; Deprecated # Cf LANGUAGE TAG
+
+# Total code points: 15
+
+# ================================================
+
+0069..006A ; Soft_Dotted # L& [2] LATIN SMALL LETTER I..LATIN SMALL LETTER J
+012F ; Soft_Dotted # L& LATIN SMALL LETTER I WITH OGONEK
+0249 ; Soft_Dotted # L& LATIN SMALL LETTER J WITH STROKE
+0268 ; Soft_Dotted # L& LATIN SMALL LETTER I WITH STROKE
+029D ; Soft_Dotted # L& LATIN SMALL LETTER J WITH CROSSED-TAIL
+02B2 ; Soft_Dotted # Lm MODIFIER LETTER SMALL J
+03F3 ; Soft_Dotted # L& GREEK LETTER YOT
+0456 ; Soft_Dotted # L& CYRILLIC SMALL LETTER BYELORUSSIAN-UKRAINIAN I
+0458 ; Soft_Dotted # L& CYRILLIC SMALL LETTER JE
+1D62 ; Soft_Dotted # Lm LATIN SUBSCRIPT SMALL LETTER I
+1D96 ; Soft_Dotted # L& LATIN SMALL LETTER I WITH RETROFLEX HOOK
+1DA4 ; Soft_Dotted # Lm MODIFIER LETTER SMALL I WITH STROKE
+1DA8 ; Soft_Dotted # Lm MODIFIER LETTER SMALL J WITH CROSSED-TAIL
+1E2D ; Soft_Dotted # L& LATIN SMALL LETTER I WITH TILDE BELOW
+1ECB ; Soft_Dotted # L& LATIN SMALL LETTER I WITH DOT BELOW
+2071 ; Soft_Dotted # Lm SUPERSCRIPT LATIN SMALL LETTER I
+2148..2149 ; Soft_Dotted # L& [2] DOUBLE-STRUCK ITALIC SMALL I..DOUBLE-STRUCK ITALIC SMALL J
+2C7C ; Soft_Dotted # Lm LATIN SUBSCRIPT SMALL LETTER J
+1D422..1D423 ; Soft_Dotted # L& [2] MATHEMATICAL BOLD SMALL I..MATHEMATICAL BOLD SMALL J
+1D456..1D457 ; Soft_Dotted # L& [2] MATHEMATICAL ITALIC SMALL I..MATHEMATICAL ITALIC SMALL J
+1D48A..1D48B ; Soft_Dotted # L& [2] MATHEMATICAL BOLD ITALIC SMALL I..MATHEMATICAL BOLD ITALIC SMALL J
+1D4BE..1D4BF ; Soft_Dotted # L& [2] MATHEMATICAL SCRIPT SMALL I..MATHEMATICAL SCRIPT SMALL J
+1D4F2..1D4F3 ; Soft_Dotted # L& [2] MATHEMATICAL BOLD SCRIPT SMALL I..MATHEMATICAL BOLD SCRIPT SMALL J
+1D526..1D527 ; Soft_Dotted # L& [2] MATHEMATICAL FRAKTUR SMALL I..MATHEMATICAL FRAKTUR SMALL J
+1D55A..1D55B ; Soft_Dotted # L& [2] MATHEMATICAL DOUBLE-STRUCK SMALL I..MATHEMATICAL DOUBLE-STRUCK SMALL J
+1D58E..1D58F ; Soft_Dotted # L& [2] MATHEMATICAL BOLD FRAKTUR SMALL I..MATHEMATICAL BOLD FRAKTUR SMALL J
+1D5C2..1D5C3 ; Soft_Dotted # L& [2] MATHEMATICAL SANS-SERIF SMALL I..MATHEMATICAL SANS-SERIF SMALL J
+1D5F6..1D5F7 ; Soft_Dotted # L& [2] MATHEMATICAL SANS-SERIF BOLD SMALL I..MATHEMATICAL SANS-SERIF BOLD SMALL J
+1D62A..1D62B ; Soft_Dotted # L& [2] MATHEMATICAL SANS-SERIF ITALIC SMALL I..MATHEMATICAL SANS-SERIF ITALIC SMALL J
+1D65E..1D65F ; Soft_Dotted # L& [2] MATHEMATICAL SANS-SERIF BOLD ITALIC SMALL I..MATHEMATICAL SANS-SERIF BOLD ITALIC SMALL J
+1D692..1D693 ; Soft_Dotted # L& [2] MATHEMATICAL MONOSPACE SMALL I..MATHEMATICAL MONOSPACE SMALL J
+
+# Total code points: 46
+
+# ================================================
+
+0E40..0E44 ; Logical_Order_Exception # Lo [5] THAI CHARACTER SARA E..THAI CHARACTER SARA AI MAIMALAI
+0EC0..0EC4 ; Logical_Order_Exception # Lo [5] LAO VOWEL SIGN E..LAO VOWEL SIGN AI
+19B5..19B7 ; Logical_Order_Exception # Lo [3] NEW TAI LUE VOWEL SIGN E..NEW TAI LUE VOWEL SIGN O
+19BA ; Logical_Order_Exception # Lo NEW TAI LUE VOWEL SIGN AY
+AAB5..AAB6 ; Logical_Order_Exception # Lo [2] TAI VIET VOWEL E..TAI VIET VOWEL O
+AAB9 ; Logical_Order_Exception # Lo TAI VIET VOWEL UEA
+AABB..AABC ; Logical_Order_Exception # Lo [2] TAI VIET VOWEL AUE..TAI VIET VOWEL AY
+
+# Total code points: 19
+
+# ================================================
+
+1885..1886 ; Other_ID_Start # Mn [2] MONGOLIAN LETTER ALI GALI BALUDA..MONGOLIAN LETTER ALI GALI THREE BALUDA
+2118 ; Other_ID_Start # Sm SCRIPT CAPITAL P
+212E ; Other_ID_Start # So ESTIMATED SYMBOL
+309B..309C ; Other_ID_Start # Sk [2] KATAKANA-HIRAGANA VOICED SOUND MARK..KATAKANA-HIRAGANA SEMI-VOICED SOUND MARK
+
+# Total code points: 6
+
+# ================================================
+
+00B7 ; Other_ID_Continue # Po MIDDLE DOT
+0387 ; Other_ID_Continue # Po GREEK ANO TELEIA
+1369..1371 ; Other_ID_Continue # No [9] ETHIOPIC DIGIT ONE..ETHIOPIC DIGIT NINE
+19DA ; Other_ID_Continue # No NEW TAI LUE THAM DIGIT ONE
+
+# Total code points: 12
+
+# ================================================
+
+0021 ; Sentence_Terminal # Po EXCLAMATION MARK
+002E ; Sentence_Terminal # Po FULL STOP
+003F ; Sentence_Terminal # Po QUESTION MARK
+0589 ; Sentence_Terminal # Po ARMENIAN FULL STOP
+061F ; Sentence_Terminal # Po ARABIC QUESTION MARK
+06D4 ; Sentence_Terminal # Po ARABIC FULL STOP
+0700..0702 ; Sentence_Terminal # Po [3] SYRIAC END OF PARAGRAPH..SYRIAC SUBLINEAR FULL STOP
+07F9 ; Sentence_Terminal # Po NKO EXCLAMATION MARK
+0964..0965 ; Sentence_Terminal # Po [2] DEVANAGARI DANDA..DEVANAGARI DOUBLE DANDA
+104A..104B ; Sentence_Terminal # Po [2] MYANMAR SIGN LITTLE SECTION..MYANMAR SIGN SECTION
+1362 ; Sentence_Terminal # Po ETHIOPIC FULL STOP
+1367..1368 ; Sentence_Terminal # Po [2] ETHIOPIC QUESTION MARK..ETHIOPIC PARAGRAPH SEPARATOR
+166E ; Sentence_Terminal # Po CANADIAN SYLLABICS FULL STOP
+1735..1736 ; Sentence_Terminal # Po [2] PHILIPPINE SINGLE PUNCTUATION..PHILIPPINE DOUBLE PUNCTUATION
+1803 ; Sentence_Terminal # Po MONGOLIAN FULL STOP
+1809 ; Sentence_Terminal # Po MONGOLIAN MANCHU FULL STOP
+1944..1945 ; Sentence_Terminal # Po [2] LIMBU EXCLAMATION MARK..LIMBU QUESTION MARK
+1AA8..1AAB ; Sentence_Terminal # Po [4] TAI THAM SIGN KAAN..TAI THAM SIGN SATKAANKUU
+1B5A..1B5B ; Sentence_Terminal # Po [2] BALINESE PANTI..BALINESE PAMADA
+1B5E..1B5F ; Sentence_Terminal # Po [2] BALINESE CARIK SIKI..BALINESE CARIK PAREREN
+1C3B..1C3C ; Sentence_Terminal # Po [2] LEPCHA PUNCTUATION TA-ROL..LEPCHA PUNCTUATION NYET THYOOM TA-ROL
+1C7E..1C7F ; Sentence_Terminal # Po [2] OL CHIKI PUNCTUATION MUCAAD..OL CHIKI PUNCTUATION DOUBLE MUCAAD
+203C..203D ; Sentence_Terminal # Po [2] DOUBLE EXCLAMATION MARK..INTERROBANG
+2047..2049 ; Sentence_Terminal # Po [3] DOUBLE QUESTION MARK..EXCLAMATION QUESTION MARK
+2E2E ; Sentence_Terminal # Po REVERSED QUESTION MARK
+2E3C ; Sentence_Terminal # Po STENOGRAPHIC FULL STOP
+3002 ; Sentence_Terminal # Po IDEOGRAPHIC FULL STOP
+A4FF ; Sentence_Terminal # Po LISU PUNCTUATION FULL STOP
+A60E..A60F ; Sentence_Terminal # Po [2] VAI FULL STOP..VAI QUESTION MARK
+A6F3 ; Sentence_Terminal # Po BAMUM FULL STOP
+A6F7 ; Sentence_Terminal # Po BAMUM QUESTION MARK
+A876..A877 ; Sentence_Terminal # Po [2] PHAGS-PA MARK SHAD..PHAGS-PA MARK DOUBLE SHAD
+A8CE..A8CF ; Sentence_Terminal # Po [2] SAURASHTRA DANDA..SAURASHTRA DOUBLE DANDA
+A92F ; Sentence_Terminal # Po KAYAH LI SIGN SHYA
+A9C8..A9C9 ; Sentence_Terminal # Po [2] JAVANESE PADA LINGSA..JAVANESE PADA LUNGSI
+AA5D..AA5F ; Sentence_Terminal # Po [3] CHAM PUNCTUATION DANDA..CHAM PUNCTUATION TRIPLE DANDA
+AAF0..AAF1 ; Sentence_Terminal # Po [2] MEETEI MAYEK CHEIKHAN..MEETEI MAYEK AHANG KHUDAM
+ABEB ; Sentence_Terminal # Po MEETEI MAYEK CHEIKHEI
+FE52 ; Sentence_Terminal # Po SMALL FULL STOP
+FE56..FE57 ; Sentence_Terminal # Po [2] SMALL QUESTION MARK..SMALL EXCLAMATION MARK
+FF01 ; Sentence_Terminal # Po FULLWIDTH EXCLAMATION MARK
+FF0E ; Sentence_Terminal # Po FULLWIDTH FULL STOP
+FF1F ; Sentence_Terminal # Po FULLWIDTH QUESTION MARK
+FF61 ; Sentence_Terminal # Po HALFWIDTH IDEOGRAPHIC FULL STOP
+10A56..10A57 ; Sentence_Terminal # Po [2] KHAROSHTHI PUNCTUATION DANDA..KHAROSHTHI PUNCTUATION DOUBLE DANDA
+11047..11048 ; Sentence_Terminal # Po [2] BRAHMI DANDA..BRAHMI DOUBLE DANDA
+110BE..110C1 ; Sentence_Terminal # Po [4] KAITHI SECTION MARK..KAITHI DOUBLE DANDA
+11141..11143 ; Sentence_Terminal # Po [3] CHAKMA DANDA..CHAKMA QUESTION MARK
+111C5..111C6 ; Sentence_Terminal # Po [2] SHARADA DANDA..SHARADA DOUBLE DANDA
+111CD ; Sentence_Terminal # Po SHARADA SUTRA MARK
+111DE..111DF ; Sentence_Terminal # Po [2] SHARADA SECTION MARK-1..SHARADA SECTION MARK-2
+11238..11239 ; Sentence_Terminal # Po [2] KHOJKI DANDA..KHOJKI DOUBLE DANDA
+1123B..1123C ; Sentence_Terminal # Po [2] KHOJKI SECTION MARK..KHOJKI DOUBLE SECTION MARK
+112A9 ; Sentence_Terminal # Po MULTANI SECTION MARK
+1144B..1144C ; Sentence_Terminal # Po [2] NEWA DANDA..NEWA DOUBLE DANDA
+115C2..115C3 ; Sentence_Terminal # Po [2] SIDDHAM DANDA..SIDDHAM DOUBLE DANDA
+115C9..115D7 ; Sentence_Terminal # Po [15] SIDDHAM END OF TEXT MARK..SIDDHAM SECTION MARK WITH CIRCLES AND FOUR ENCLOSURES
+11641..11642 ; Sentence_Terminal # Po [2] MODI DANDA..MODI DOUBLE DANDA
+1173C..1173E ; Sentence_Terminal # Po [3] AHOM SIGN SMALL SECTION..AHOM SIGN RULAI
+11C41..11C42 ; Sentence_Terminal # Po [2] BHAIKSUKI DANDA..BHAIKSUKI DOUBLE DANDA
+16A6E..16A6F ; Sentence_Terminal # Po [2] MRO DANDA..MRO DOUBLE DANDA
+16AF5 ; Sentence_Terminal # Po BASSA VAH FULL STOP
+16B37..16B38 ; Sentence_Terminal # Po [2] PAHAWH HMONG SIGN VOS THOM..PAHAWH HMONG SIGN VOS TSHAB CEEB
+16B44 ; Sentence_Terminal # Po PAHAWH HMONG SIGN XAUS
+1BC9F ; Sentence_Terminal # Po DUPLOYAN PUNCTUATION CHINOOK FULL STOP
+1DA88 ; Sentence_Terminal # Po SIGNWRITING FULL STOP
+
+# Total code points: 124
+
+# ================================================
+
+180B..180D ; Variation_Selector # Mn [3] MONGOLIAN FREE VARIATION SELECTOR ONE..MONGOLIAN FREE VARIATION SELECTOR THREE
+FE00..FE0F ; Variation_Selector # Mn [16] VARIATION SELECTOR-1..VARIATION SELECTOR-16
+E0100..E01EF ; Variation_Selector # Mn [240] VARIATION SELECTOR-17..VARIATION SELECTOR-256
+
+# Total code points: 259
+
+# ================================================
+
+0009..000D ; Pattern_White_Space # Cc [5] <control-0009>..<control-000D>
+0020 ; Pattern_White_Space # Zs SPACE
+0085 ; Pattern_White_Space # Cc <control-0085>
+200E..200F ; Pattern_White_Space # Cf [2] LEFT-TO-RIGHT MARK..RIGHT-TO-LEFT MARK
+2028 ; Pattern_White_Space # Zl LINE SEPARATOR
+2029 ; Pattern_White_Space # Zp PARAGRAPH SEPARATOR
+
+# Total code points: 11
+
+# ================================================
+
+0021..0023 ; Pattern_Syntax # Po [3] EXCLAMATION MARK..NUMBER SIGN
+0024 ; Pattern_Syntax # Sc DOLLAR SIGN
+0025..0027 ; Pattern_Syntax # Po [3] PERCENT SIGN..APOSTROPHE
+0028 ; Pattern_Syntax # Ps LEFT PARENTHESIS
+0029 ; Pattern_Syntax # Pe RIGHT PARENTHESIS
+002A ; Pattern_Syntax # Po ASTERISK
+002B ; Pattern_Syntax # Sm PLUS SIGN
+002C ; Pattern_Syntax # Po COMMA
+002D ; Pattern_Syntax # Pd HYPHEN-MINUS
+002E..002F ; Pattern_Syntax # Po [2] FULL STOP..SOLIDUS
+003A..003B ; Pattern_Syntax # Po [2] COLON..SEMICOLON
+003C..003E ; Pattern_Syntax # Sm [3] LESS-THAN SIGN..GREATER-THAN SIGN
+003F..0040 ; Pattern_Syntax # Po [2] QUESTION MARK..COMMERCIAL AT
+005B ; Pattern_Syntax # Ps LEFT SQUARE BRACKET
+005C ; Pattern_Syntax # Po REVERSE SOLIDUS
+005D ; Pattern_Syntax # Pe RIGHT SQUARE BRACKET
+005E ; Pattern_Syntax # Sk CIRCUMFLEX ACCENT
+0060 ; Pattern_Syntax # Sk GRAVE ACCENT
+007B ; Pattern_Syntax # Ps LEFT CURLY BRACKET
+007C ; Pattern_Syntax # Sm VERTICAL LINE
+007D ; Pattern_Syntax # Pe RIGHT CURLY BRACKET
+007E ; Pattern_Syntax # Sm TILDE
+00A1 ; Pattern_Syntax # Po INVERTED EXCLAMATION MARK
+00A2..00A5 ; Pattern_Syntax # Sc [4] CENT SIGN..YEN SIGN
+00A6 ; Pattern_Syntax # So BROKEN BAR
+00A7 ; Pattern_Syntax # Po SECTION SIGN
+00A9 ; Pattern_Syntax # So COPYRIGHT SIGN
+00AB ; Pattern_Syntax # Pi LEFT-POINTING DOUBLE ANGLE QUOTATION MARK
+00AC ; Pattern_Syntax # Sm NOT SIGN
+00AE ; Pattern_Syntax # So REGISTERED SIGN
+00B0 ; Pattern_Syntax # So DEGREE SIGN
+00B1 ; Pattern_Syntax # Sm PLUS-MINUS SIGN
+00B6 ; Pattern_Syntax # Po PILCROW SIGN
+00BB ; Pattern_Syntax # Pf RIGHT-POINTING DOUBLE ANGLE QUOTATION MARK
+00BF ; Pattern_Syntax # Po INVERTED QUESTION MARK
+00D7 ; Pattern_Syntax # Sm MULTIPLICATION SIGN
+00F7 ; Pattern_Syntax # Sm DIVISION SIGN
+2010..2015 ; Pattern_Syntax # Pd [6] HYPHEN..HORIZONTAL BAR
+2016..2017 ; Pattern_Syntax # Po [2] DOUBLE VERTICAL LINE..DOUBLE LOW LINE
+2018 ; Pattern_Syntax # Pi LEFT SINGLE QUOTATION MARK
+2019 ; Pattern_Syntax # Pf RIGHT SINGLE QUOTATION MARK
+201A ; Pattern_Syntax # Ps SINGLE LOW-9 QUOTATION MARK
+201B..201C ; Pattern_Syntax # Pi [2] SINGLE HIGH-REVERSED-9 QUOTATION MARK..LEFT DOUBLE QUOTATION MARK
+201D ; Pattern_Syntax # Pf RIGHT DOUBLE QUOTATION MARK
+201E ; Pattern_Syntax # Ps DOUBLE LOW-9 QUOTATION MARK
+201F ; Pattern_Syntax # Pi DOUBLE HIGH-REVERSED-9 QUOTATION MARK
+2020..2027 ; Pattern_Syntax # Po [8] DAGGER..HYPHENATION POINT
+2030..2038 ; Pattern_Syntax # Po [9] PER MILLE SIGN..CARET
+2039 ; Pattern_Syntax # Pi SINGLE LEFT-POINTING ANGLE QUOTATION MARK
+203A ; Pattern_Syntax # Pf SINGLE RIGHT-POINTING ANGLE QUOTATION MARK
+203B..203E ; Pattern_Syntax # Po [4] REFERENCE MARK..OVERLINE
+2041..2043 ; Pattern_Syntax # Po [3] CARET INSERTION POINT..HYPHEN BULLET
+2044 ; Pattern_Syntax # Sm FRACTION SLASH
+2045 ; Pattern_Syntax # Ps LEFT SQUARE BRACKET WITH QUILL
+2046 ; Pattern_Syntax # Pe RIGHT SQUARE BRACKET WITH QUILL
+2047..2051 ; Pattern_Syntax # Po [11] DOUBLE QUESTION MARK..TWO ASTERISKS ALIGNED VERTICALLY
+2052 ; Pattern_Syntax # Sm COMMERCIAL MINUS SIGN
+2053 ; Pattern_Syntax # Po SWUNG DASH
+2055..205E ; Pattern_Syntax # Po [10] FLOWER PUNCTUATION MARK..VERTICAL FOUR DOTS
+2190..2194 ; Pattern_Syntax # Sm [5] LEFTWARDS ARROW..LEFT RIGHT ARROW
+2195..2199 ; Pattern_Syntax # So [5] UP DOWN ARROW..SOUTH WEST ARROW
+219A..219B ; Pattern_Syntax # Sm [2] LEFTWARDS ARROW WITH STROKE..RIGHTWARDS ARROW WITH STROKE
+219C..219F ; Pattern_Syntax # So [4] LEFTWARDS WAVE ARROW..UPWARDS TWO HEADED ARROW
+21A0 ; Pattern_Syntax # Sm RIGHTWARDS TWO HEADED ARROW
+21A1..21A2 ; Pattern_Syntax # So [2] DOWNWARDS TWO HEADED ARROW..LEFTWARDS ARROW WITH TAIL
+21A3 ; Pattern_Syntax # Sm RIGHTWARDS ARROW WITH TAIL
+21A4..21A5 ; Pattern_Syntax # So [2] LEFTWARDS ARROW FROM BAR..UPWARDS ARROW FROM BAR
+21A6 ; Pattern_Syntax # Sm RIGHTWARDS ARROW FROM BAR
+21A7..21AD ; Pattern_Syntax # So [7] DOWNWARDS ARROW FROM BAR..LEFT RIGHT WAVE ARROW
+21AE ; Pattern_Syntax # Sm LEFT RIGHT ARROW WITH STROKE
+21AF..21CD ; Pattern_Syntax # So [31] DOWNWARDS ZIGZAG ARROW..LEFTWARDS DOUBLE ARROW WITH STROKE
+21CE..21CF ; Pattern_Syntax # Sm [2] LEFT RIGHT DOUBLE ARROW WITH STROKE..RIGHTWARDS DOUBLE ARROW WITH STROKE
+21D0..21D1 ; Pattern_Syntax # So [2] LEFTWARDS DOUBLE ARROW..UPWARDS DOUBLE ARROW
+21D2 ; Pattern_Syntax # Sm RIGHTWARDS DOUBLE ARROW
+21D3 ; Pattern_Syntax # So DOWNWARDS DOUBLE ARROW
+21D4 ; Pattern_Syntax # Sm LEFT RIGHT DOUBLE ARROW
+21D5..21F3 ; Pattern_Syntax # So [31] UP DOWN DOUBLE ARROW..UP DOWN WHITE ARROW
+21F4..22FF ; Pattern_Syntax # Sm [268] RIGHT ARROW WITH SMALL CIRCLE..Z NOTATION BAG MEMBERSHIP
+2300..2307 ; Pattern_Syntax # So [8] DIAMETER SIGN..WAVY LINE
+2308 ; Pattern_Syntax # Ps LEFT CEILING
+2309 ; Pattern_Syntax # Pe RIGHT CEILING
+230A ; Pattern_Syntax # Ps LEFT FLOOR
+230B ; Pattern_Syntax # Pe RIGHT FLOOR
+230C..231F ; Pattern_Syntax # So [20] BOTTOM RIGHT CROP..BOTTOM RIGHT CORNER
+2320..2321 ; Pattern_Syntax # Sm [2] TOP HALF INTEGRAL..BOTTOM HALF INTEGRAL
+2322..2328 ; Pattern_Syntax # So [7] FROWN..KEYBOARD
+2329 ; Pattern_Syntax # Ps LEFT-POINTING ANGLE BRACKET
+232A ; Pattern_Syntax # Pe RIGHT-POINTING ANGLE BRACKET
+232B..237B ; Pattern_Syntax # So [81] ERASE TO THE LEFT..NOT CHECK MARK
+237C ; Pattern_Syntax # Sm RIGHT ANGLE WITH DOWNWARDS ZIGZAG ARROW
+237D..239A ; Pattern_Syntax # So [30] SHOULDERED OPEN BOX..CLEAR SCREEN SYMBOL
+239B..23B3 ; Pattern_Syntax # Sm [25] LEFT PARENTHESIS UPPER HOOK..SUMMATION BOTTOM
+23B4..23DB ; Pattern_Syntax # So [40] TOP SQUARE BRACKET..FUSE
+23DC..23E1 ; Pattern_Syntax # Sm [6] TOP PARENTHESIS..BOTTOM TORTOISE SHELL BRACKET
+23E2..23FE ; Pattern_Syntax # So [29] WHITE TRAPEZIUM..POWER SLEEP SYMBOL
+23FF ; Pattern_Syntax # Cn <reserved-23FF>
+2400..2426 ; Pattern_Syntax # So [39] SYMBOL FOR NULL..SYMBOL FOR SUBSTITUTE FORM TWO
+2427..243F ; Pattern_Syntax # Cn [25] <reserved-2427>..<reserved-243F>
+2440..244A ; Pattern_Syntax # So [11] OCR HOOK..OCR DOUBLE BACKSLASH
+244B..245F ; Pattern_Syntax # Cn [21] <reserved-244B>..<reserved-245F>
+2500..25B6 ; Pattern_Syntax # So [183] BOX DRAWINGS LIGHT HORIZONTAL..BLACK RIGHT-POINTING TRIANGLE
+25B7 ; Pattern_Syntax # Sm WHITE RIGHT-POINTING TRIANGLE
+25B8..25C0 ; Pattern_Syntax # So [9] BLACK RIGHT-POINTING SMALL TRIANGLE..BLACK LEFT-POINTING TRIANGLE
+25C1 ; Pattern_Syntax # Sm WHITE LEFT-POINTING TRIANGLE
+25C2..25F7 ; Pattern_Syntax # So [54] BLACK LEFT-POINTING SMALL TRIANGLE..WHITE CIRCLE WITH UPPER RIGHT QUADRANT
+25F8..25FF ; Pattern_Syntax # Sm [8] UPPER LEFT TRIANGLE..LOWER RIGHT TRIANGLE
+2600..266E ; Pattern_Syntax # So [111] BLACK SUN WITH RAYS..MUSIC NATURAL SIGN
+266F ; Pattern_Syntax # Sm MUSIC SHARP SIGN
+2670..2767 ; Pattern_Syntax # So [248] WEST SYRIAC CROSS..ROTATED FLORAL HEART BULLET
+2768 ; Pattern_Syntax # Ps MEDIUM LEFT PARENTHESIS ORNAMENT
+2769 ; Pattern_Syntax # Pe MEDIUM RIGHT PARENTHESIS ORNAMENT
+276A ; Pattern_Syntax # Ps MEDIUM FLATTENED LEFT PARENTHESIS ORNAMENT
+276B ; Pattern_Syntax # Pe MEDIUM FLATTENED RIGHT PARENTHESIS ORNAMENT
+276C ; Pattern_Syntax # Ps MEDIUM LEFT-POINTING ANGLE BRACKET ORNAMENT
+276D ; Pattern_Syntax # Pe MEDIUM RIGHT-POINTING ANGLE BRACKET ORNAMENT
+276E ; Pattern_Syntax # Ps HEAVY LEFT-POINTING ANGLE QUOTATION MARK ORNAMENT
+276F ; Pattern_Syntax # Pe HEAVY RIGHT-POINTING ANGLE QUOTATION MARK ORNAMENT
+2770 ; Pattern_Syntax # Ps HEAVY LEFT-POINTING ANGLE BRACKET ORNAMENT
+2771 ; Pattern_Syntax # Pe HEAVY RIGHT-POINTING ANGLE BRACKET ORNAMENT
+2772 ; Pattern_Syntax # Ps LIGHT LEFT TORTOISE SHELL BRACKET ORNAMENT
+2773 ; Pattern_Syntax # Pe LIGHT RIGHT TORTOISE SHELL BRACKET ORNAMENT
+2774 ; Pattern_Syntax # Ps MEDIUM LEFT CURLY BRACKET ORNAMENT
+2775 ; Pattern_Syntax # Pe MEDIUM RIGHT CURLY BRACKET ORNAMENT
+2794..27BF ; Pattern_Syntax # So [44] HEAVY WIDE-HEADED RIGHTWARDS ARROW..DOUBLE CURLY LOOP
+27C0..27C4 ; Pattern_Syntax # Sm [5] THREE DIMENSIONAL ANGLE..OPEN SUPERSET
+27C5 ; Pattern_Syntax # Ps LEFT S-SHAPED BAG DELIMITER
+27C6 ; Pattern_Syntax # Pe RIGHT S-SHAPED BAG DELIMITER
+27C7..27E5 ; Pattern_Syntax # Sm [31] OR WITH DOT INSIDE..WHITE SQUARE WITH RIGHTWARDS TICK
+27E6 ; Pattern_Syntax # Ps MATHEMATICAL LEFT WHITE SQUARE BRACKET
+27E7 ; Pattern_Syntax # Pe MATHEMATICAL RIGHT WHITE SQUARE BRACKET
+27E8 ; Pattern_Syntax # Ps MATHEMATICAL LEFT ANGLE BRACKET
+27E9 ; Pattern_Syntax # Pe MATHEMATICAL RIGHT ANGLE BRACKET
+27EA ; Pattern_Syntax # Ps MATHEMATICAL LEFT DOUBLE ANGLE BRACKET
+27EB ; Pattern_Syntax # Pe MATHEMATICAL RIGHT DOUBLE ANGLE BRACKET
+27EC ; Pattern_Syntax # Ps MATHEMATICAL LEFT WHITE TORTOISE SHELL BRACKET
+27ED ; Pattern_Syntax # Pe MATHEMATICAL RIGHT WHITE TORTOISE SHELL BRACKET
+27EE ; Pattern_Syntax # Ps MATHEMATICAL LEFT FLATTENED PARENTHESIS
+27EF ; Pattern_Syntax # Pe MATHEMATICAL RIGHT FLATTENED PARENTHESIS
+27F0..27FF ; Pattern_Syntax # Sm [16] UPWARDS QUADRUPLE ARROW..LONG RIGHTWARDS SQUIGGLE ARROW
+2800..28FF ; Pattern_Syntax # So [256] BRAILLE PATTERN BLANK..BRAILLE PATTERN DOTS-12345678
+2900..2982 ; Pattern_Syntax # Sm [131] RIGHTWARDS TWO-HEADED ARROW WITH VERTICAL STROKE..Z NOTATION TYPE COLON
+2983 ; Pattern_Syntax # Ps LEFT WHITE CURLY BRACKET
+2984 ; Pattern_Syntax # Pe RIGHT WHITE CURLY BRACKET
+2985 ; Pattern_Syntax # Ps LEFT WHITE PARENTHESIS
+2986 ; Pattern_Syntax # Pe RIGHT WHITE PARENTHESIS
+2987 ; Pattern_Syntax # Ps Z NOTATION LEFT IMAGE BRACKET
+2988 ; Pattern_Syntax # Pe Z NOTATION RIGHT IMAGE BRACKET
+2989 ; Pattern_Syntax # Ps Z NOTATION LEFT BINDING BRACKET
+298A ; Pattern_Syntax # Pe Z NOTATION RIGHT BINDING BRACKET
+298B ; Pattern_Syntax # Ps LEFT SQUARE BRACKET WITH UNDERBAR
+298C ; Pattern_Syntax # Pe RIGHT SQUARE BRACKET WITH UNDERBAR
+298D ; Pattern_Syntax # Ps LEFT SQUARE BRACKET WITH TICK IN TOP CORNER
+298E ; Pattern_Syntax # Pe RIGHT SQUARE BRACKET WITH TICK IN BOTTOM CORNER
+298F ; Pattern_Syntax # Ps LEFT SQUARE BRACKET WITH TICK IN BOTTOM CORNER
+2990 ; Pattern_Syntax # Pe RIGHT SQUARE BRACKET WITH TICK IN TOP CORNER
+2991 ; Pattern_Syntax # Ps LEFT ANGLE BRACKET WITH DOT
+2992 ; Pattern_Syntax # Pe RIGHT ANGLE BRACKET WITH DOT
+2993 ; Pattern_Syntax # Ps LEFT ARC LESS-THAN BRACKET
+2994 ; Pattern_Syntax # Pe RIGHT ARC GREATER-THAN BRACKET
+2995 ; Pattern_Syntax # Ps DOUBLE LEFT ARC GREATER-THAN BRACKET
+2996 ; Pattern_Syntax # Pe DOUBLE RIGHT ARC LESS-THAN BRACKET
+2997 ; Pattern_Syntax # Ps LEFT BLACK TORTOISE SHELL BRACKET
+2998 ; Pattern_Syntax # Pe RIGHT BLACK TORTOISE SHELL BRACKET
+2999..29D7 ; Pattern_Syntax # Sm [63] DOTTED FENCE..BLACK HOURGLASS
+29D8 ; Pattern_Syntax # Ps LEFT WIGGLY FENCE
+29D9 ; Pattern_Syntax # Pe RIGHT WIGGLY FENCE
+29DA ; Pattern_Syntax # Ps LEFT DOUBLE WIGGLY FENCE
+29DB ; Pattern_Syntax # Pe RIGHT DOUBLE WIGGLY FENCE
+29DC..29FB ; Pattern_Syntax # Sm [32] INCOMPLETE INFINITY..TRIPLE PLUS
+29FC ; Pattern_Syntax # Ps LEFT-POINTING CURVED ANGLE BRACKET
+29FD ; Pattern_Syntax # Pe RIGHT-POINTING CURVED ANGLE BRACKET
+29FE..2AFF ; Pattern_Syntax # Sm [258] TINY..N-ARY WHITE VERTICAL BAR
+2B00..2B2F ; Pattern_Syntax # So [48] NORTH EAST WHITE ARROW..WHITE VERTICAL ELLIPSE
+2B30..2B44 ; Pattern_Syntax # Sm [21] LEFT ARROW WITH SMALL CIRCLE..RIGHTWARDS ARROW THROUGH SUPERSET
+2B45..2B46 ; Pattern_Syntax # So [2] LEFTWARDS QUADRUPLE ARROW..RIGHTWARDS QUADRUPLE ARROW
+2B47..2B4C ; Pattern_Syntax # Sm [6] REVERSE TILDE OPERATOR ABOVE RIGHTWARDS ARROW..RIGHTWARDS ARROW ABOVE REVERSE TILDE OPERATOR
+2B4D..2B73 ; Pattern_Syntax # So [39] DOWNWARDS TRIANGLE-HEADED ZIGZAG ARROW..DOWNWARDS TRIANGLE-HEADED ARROW TO BAR
+2B74..2B75 ; Pattern_Syntax # Cn [2] <reserved-2B74>..<reserved-2B75>
+2B76..2B95 ; Pattern_Syntax # So [32] NORTH WEST TRIANGLE-HEADED ARROW TO BAR..RIGHTWARDS BLACK ARROW
+2B96..2B97 ; Pattern_Syntax # Cn [2] <reserved-2B96>..<reserved-2B97>
+2B98..2BB9 ; Pattern_Syntax # So [34] THREE-D TOP-LIGHTED LEFTWARDS EQUILATERAL ARROWHEAD..UP ARROWHEAD IN A RECTANGLE BOX
+2BBA..2BBC ; Pattern_Syntax # Cn [3] <reserved-2BBA>..<reserved-2BBC>
+2BBD..2BC8 ; Pattern_Syntax # So [12] BALLOT BOX WITH LIGHT X..BLACK MEDIUM RIGHT-POINTING TRIANGLE CENTRED
+2BC9 ; Pattern_Syntax # Cn <reserved-2BC9>
+2BCA..2BD1 ; Pattern_Syntax # So [8] TOP HALF BLACK CIRCLE..UNCERTAINTY SIGN
+2BD2..2BEB ; Pattern_Syntax # Cn [26] <reserved-2BD2>..<reserved-2BEB>
+2BEC..2BEF ; Pattern_Syntax # So [4] LEFTWARDS TWO-HEADED ARROW WITH TRIANGLE ARROWHEADS..DOWNWARDS TWO-HEADED ARROW WITH TRIANGLE ARROWHEADS
+2BF0..2BFF ; Pattern_Syntax # Cn [16] <reserved-2BF0>..<reserved-2BFF>
+2E00..2E01 ; Pattern_Syntax # Po [2] RIGHT ANGLE SUBSTITUTION MARKER..RIGHT ANGLE DOTTED SUBSTITUTION MARKER
+2E02 ; Pattern_Syntax # Pi LEFT SUBSTITUTION BRACKET
+2E03 ; Pattern_Syntax # Pf RIGHT SUBSTITUTION BRACKET
+2E04 ; Pattern_Syntax # Pi LEFT DOTTED SUBSTITUTION BRACKET
+2E05 ; Pattern_Syntax # Pf RIGHT DOTTED SUBSTITUTION BRACKET
+2E06..2E08 ; Pattern_Syntax # Po [3] RAISED INTERPOLATION MARKER..DOTTED TRANSPOSITION MARKER
+2E09 ; Pattern_Syntax # Pi LEFT TRANSPOSITION BRACKET
+2E0A ; Pattern_Syntax # Pf RIGHT TRANSPOSITION BRACKET
+2E0B ; Pattern_Syntax # Po RAISED SQUARE
+2E0C ; Pattern_Syntax # Pi LEFT RAISED OMISSION BRACKET
+2E0D ; Pattern_Syntax # Pf RIGHT RAISED OMISSION BRACKET
+2E0E..2E16 ; Pattern_Syntax # Po [9] EDITORIAL CORONIS..DOTTED RIGHT-POINTING ANGLE
+2E17 ; Pattern_Syntax # Pd DOUBLE OBLIQUE HYPHEN
+2E18..2E19 ; Pattern_Syntax # Po [2] INVERTED INTERROBANG..PALM BRANCH
+2E1A ; Pattern_Syntax # Pd HYPHEN WITH DIAERESIS
+2E1B ; Pattern_Syntax # Po TILDE WITH RING ABOVE
+2E1C ; Pattern_Syntax # Pi LEFT LOW PARAPHRASE BRACKET
+2E1D ; Pattern_Syntax # Pf RIGHT LOW PARAPHRASE BRACKET
+2E1E..2E1F ; Pattern_Syntax # Po [2] TILDE WITH DOT ABOVE..TILDE WITH DOT BELOW
+2E20 ; Pattern_Syntax # Pi LEFT VERTICAL BAR WITH QUILL
+2E21 ; Pattern_Syntax # Pf RIGHT VERTICAL BAR WITH QUILL
+2E22 ; Pattern_Syntax # Ps TOP LEFT HALF BRACKET
+2E23 ; Pattern_Syntax # Pe TOP RIGHT HALF BRACKET
+2E24 ; Pattern_Syntax # Ps BOTTOM LEFT HALF BRACKET
+2E25 ; Pattern_Syntax # Pe BOTTOM RIGHT HALF BRACKET
+2E26 ; Pattern_Syntax # Ps LEFT SIDEWAYS U BRACKET
+2E27 ; Pattern_Syntax # Pe RIGHT SIDEWAYS U BRACKET
+2E28 ; Pattern_Syntax # Ps LEFT DOUBLE PARENTHESIS
+2E29 ; Pattern_Syntax # Pe RIGHT DOUBLE PARENTHESIS
+2E2A..2E2E ; Pattern_Syntax # Po [5] TWO DOTS OVER ONE DOT PUNCTUATION..REVERSED QUESTION MARK
+2E2F ; Pattern_Syntax # Lm VERTICAL TILDE
+2E30..2E39 ; Pattern_Syntax # Po [10] RING POINT..TOP HALF SECTION SIGN
+2E3A..2E3B ; Pattern_Syntax # Pd [2] TWO-EM DASH..THREE-EM DASH
+2E3C..2E3F ; Pattern_Syntax # Po [4] STENOGRAPHIC FULL STOP..CAPITULUM
+2E40 ; Pattern_Syntax # Pd DOUBLE HYPHEN
+2E41 ; Pattern_Syntax # Po REVERSED COMMA
+2E42 ; Pattern_Syntax # Ps DOUBLE LOW-REVERSED-9 QUOTATION MARK
+2E43..2E44 ; Pattern_Syntax # Po [2] DASH WITH LEFT UPTURN..DOUBLE SUSPENSION MARK
+2E45..2E7F ; Pattern_Syntax # Cn [59] <reserved-2E45>..<reserved-2E7F>
+3001..3003 ; Pattern_Syntax # Po [3] IDEOGRAPHIC COMMA..DITTO MARK
+3008 ; Pattern_Syntax # Ps LEFT ANGLE BRACKET
+3009 ; Pattern_Syntax # Pe RIGHT ANGLE BRACKET
+300A ; Pattern_Syntax # Ps LEFT DOUBLE ANGLE BRACKET
+300B ; Pattern_Syntax # Pe RIGHT DOUBLE ANGLE BRACKET
+300C ; Pattern_Syntax # Ps LEFT CORNER BRACKET
+300D ; Pattern_Syntax # Pe RIGHT CORNER BRACKET
+300E ; Pattern_Syntax # Ps LEFT WHITE CORNER BRACKET
+300F ; Pattern_Syntax # Pe RIGHT WHITE CORNER BRACKET
+3010 ; Pattern_Syntax # Ps LEFT BLACK LENTICULAR BRACKET
+3011 ; Pattern_Syntax # Pe RIGHT BLACK LENTICULAR BRACKET
+3012..3013 ; Pattern_Syntax # So [2] POSTAL MARK..GETA MARK
+3014 ; Pattern_Syntax # Ps LEFT TORTOISE SHELL BRACKET
+3015 ; Pattern_Syntax # Pe RIGHT TORTOISE SHELL BRACKET
+3016 ; Pattern_Syntax # Ps LEFT WHITE LENTICULAR BRACKET
+3017 ; Pattern_Syntax # Pe RIGHT WHITE LENTICULAR BRACKET
+3018 ; Pattern_Syntax # Ps LEFT WHITE TORTOISE SHELL BRACKET
+3019 ; Pattern_Syntax # Pe RIGHT WHITE TORTOISE SHELL BRACKET
+301A ; Pattern_Syntax # Ps LEFT WHITE SQUARE BRACKET
+301B ; Pattern_Syntax # Pe RIGHT WHITE SQUARE BRACKET
+301C ; Pattern_Syntax # Pd WAVE DASH
+301D ; Pattern_Syntax # Ps REVERSED DOUBLE PRIME QUOTATION MARK
+301E..301F ; Pattern_Syntax # Pe [2] DOUBLE PRIME QUOTATION MARK..LOW DOUBLE PRIME QUOTATION MARK
+3020 ; Pattern_Syntax # So POSTAL MARK FACE
+3030 ; Pattern_Syntax # Pd WAVY DASH
+FD3E ; Pattern_Syntax # Pe ORNATE LEFT PARENTHESIS
+FD3F ; Pattern_Syntax # Ps ORNATE RIGHT PARENTHESIS
+FE45..FE46 ; Pattern_Syntax # Po [2] SESAME DOT..WHITE SESAME DOT
+
+# Total code points: 2760
+
+# ================================================
+
+0600..0605 ; Prepended_Concatenation_Mark # Cf [6] ARABIC NUMBER SIGN..ARABIC NUMBER MARK ABOVE
+06DD ; Prepended_Concatenation_Mark # Cf ARABIC END OF AYAH
+070F ; Prepended_Concatenation_Mark # Cf SYRIAC ABBREVIATION MARK
+08E2 ; Prepended_Concatenation_Mark # Cf ARABIC DISPUTED END OF AYAH
+110BD ; Prepended_Concatenation_Mark # Cf KAITHI NUMBER SIGN
+
+# Total code points: 10
+
+# EOF
diff --git a/src/lib-fts/WordBreakProperty.txt b/src/lib-fts/WordBreakProperty.txt
new file mode 100644
index 0000000..6ccba54
--- /dev/null
+++ b/src/lib-fts/WordBreakProperty.txt
@@ -0,0 +1,1298 @@
+# WordBreakProperty-9.0.0.txt
+# Date: 2016-06-01, 10:34:38 GMT
+# © 2016 Unicode®, Inc.
+# Unicode and the Unicode Logo are registered trademarks of Unicode, Inc. in the U.S. and other countries.
+# For terms of use, see http://www.unicode.org/terms_of_use.html
+#
+# Unicode Character Database
+# For documentation, see http://www.unicode.org/reports/tr44/
+
+# ================================================
+
+# Property: Word_Break
+
+# All code points not explicitly listed for Word_Break
+# have the value Other (XX).
+
+# @missing: 0000..10FFFF; Other
+
+# ================================================
+
+0022 ; Double_Quote # Po QUOTATION MARK
+
+# Total code points: 1
+
+# ================================================
+
+0027 ; Single_Quote # Po APOSTROPHE
+
+# Total code points: 1
+
+# ================================================
+
+05D0..05EA ; Hebrew_Letter # Lo [27] HEBREW LETTER ALEF..HEBREW LETTER TAV
+05F0..05F2 ; Hebrew_Letter # Lo [3] HEBREW LIGATURE YIDDISH DOUBLE VAV..HEBREW LIGATURE YIDDISH DOUBLE YOD
+FB1D ; Hebrew_Letter # Lo HEBREW LETTER YOD WITH HIRIQ
+FB1F..FB28 ; Hebrew_Letter # Lo [10] HEBREW LIGATURE YIDDISH YOD YOD PATAH..HEBREW LETTER WIDE TAV
+FB2A..FB36 ; Hebrew_Letter # Lo [13] HEBREW LETTER SHIN WITH SHIN DOT..HEBREW LETTER ZAYIN WITH DAGESH
+FB38..FB3C ; Hebrew_Letter # Lo [5] HEBREW LETTER TET WITH DAGESH..HEBREW LETTER LAMED WITH DAGESH
+FB3E ; Hebrew_Letter # Lo HEBREW LETTER MEM WITH DAGESH
+FB40..FB41 ; Hebrew_Letter # Lo [2] HEBREW LETTER NUN WITH DAGESH..HEBREW LETTER SAMEKH WITH DAGESH
+FB43..FB44 ; Hebrew_Letter # Lo [2] HEBREW LETTER FINAL PE WITH DAGESH..HEBREW LETTER PE WITH DAGESH
+FB46..FB4F ; Hebrew_Letter # Lo [10] HEBREW LETTER TSADI WITH DAGESH..HEBREW LIGATURE ALEF LAMED
+
+# Total code points: 74
+
+# ================================================
+
+000D ; CR # Cc <control-000D>
+
+# Total code points: 1
+
+# ================================================
+
+000A ; LF # Cc <control-000A>
+
+# Total code points: 1
+
+# ================================================
+
+000B..000C ; Newline # Cc [2] <control-000B>..<control-000C>
+0085 ; Newline # Cc <control-0085>
+2028 ; Newline # Zl LINE SEPARATOR
+2029 ; Newline # Zp PARAGRAPH SEPARATOR
+
+# Total code points: 5
+
+# ================================================
+
+0300..036F ; Extend # Mn [112] COMBINING GRAVE ACCENT..COMBINING LATIN SMALL LETTER X
+0483..0487 ; Extend # Mn [5] COMBINING CYRILLIC TITLO..COMBINING CYRILLIC POKRYTIE
+0488..0489 ; Extend # Me [2] COMBINING CYRILLIC HUNDRED THOUSANDS SIGN..COMBINING CYRILLIC MILLIONS SIGN
+0591..05BD ; Extend # Mn [45] HEBREW ACCENT ETNAHTA..HEBREW POINT METEG
+05BF ; Extend # Mn HEBREW POINT RAFE
+05C1..05C2 ; Extend # Mn [2] HEBREW POINT SHIN DOT..HEBREW POINT SIN DOT
+05C4..05C5 ; Extend # Mn [2] HEBREW MARK UPPER DOT..HEBREW MARK LOWER DOT
+05C7 ; Extend # Mn HEBREW POINT QAMATS QATAN
+0610..061A ; Extend # Mn [11] ARABIC SIGN SALLALLAHOU ALAYHE WASSALLAM..ARABIC SMALL KASRA
+064B..065F ; Extend # Mn [21] ARABIC FATHATAN..ARABIC WAVY HAMZA BELOW
+0670 ; Extend # Mn ARABIC LETTER SUPERSCRIPT ALEF
+06D6..06DC ; Extend # Mn [7] ARABIC SMALL HIGH LIGATURE SAD WITH LAM WITH ALEF MAKSURA..ARABIC SMALL HIGH SEEN
+06DF..06E4 ; Extend # Mn [6] ARABIC SMALL HIGH ROUNDED ZERO..ARABIC SMALL HIGH MADDA
+06E7..06E8 ; Extend # Mn [2] ARABIC SMALL HIGH YEH..ARABIC SMALL HIGH NOON
+06EA..06ED ; Extend # Mn [4] ARABIC EMPTY CENTRE LOW STOP..ARABIC SMALL LOW MEEM
+0711 ; Extend # Mn SYRIAC LETTER SUPERSCRIPT ALAPH
+0730..074A ; Extend # Mn [27] SYRIAC PTHAHA ABOVE..SYRIAC BARREKH
+07A6..07B0 ; Extend # Mn [11] THAANA ABAFILI..THAANA SUKUN
+07EB..07F3 ; Extend # Mn [9] NKO COMBINING SHORT HIGH TONE..NKO COMBINING DOUBLE DOT ABOVE
+0816..0819 ; Extend # Mn [4] SAMARITAN MARK IN..SAMARITAN MARK DAGESH
+081B..0823 ; Extend # Mn [9] SAMARITAN MARK EPENTHETIC YUT..SAMARITAN VOWEL SIGN A
+0825..0827 ; Extend # Mn [3] SAMARITAN VOWEL SIGN SHORT A..SAMARITAN VOWEL SIGN U
+0829..082D ; Extend # Mn [5] SAMARITAN VOWEL SIGN LONG I..SAMARITAN MARK NEQUDAA
+0859..085B ; Extend # Mn [3] MANDAIC AFFRICATION MARK..MANDAIC GEMINATION MARK
+08D4..08E1 ; Extend # Mn [14] ARABIC SMALL HIGH WORD AR-RUB..ARABIC SMALL HIGH SIGN SAFHA
+08E3..0902 ; Extend # Mn [32] ARABIC TURNED DAMMA BELOW..DEVANAGARI SIGN ANUSVARA
+0903 ; Extend # Mc DEVANAGARI SIGN VISARGA
+093A ; Extend # Mn DEVANAGARI VOWEL SIGN OE
+093B ; Extend # Mc DEVANAGARI VOWEL SIGN OOE
+093C ; Extend # Mn DEVANAGARI SIGN NUKTA
+093E..0940 ; Extend # Mc [3] DEVANAGARI VOWEL SIGN AA..DEVANAGARI VOWEL SIGN II
+0941..0948 ; Extend # Mn [8] DEVANAGARI VOWEL SIGN U..DEVANAGARI VOWEL SIGN AI
+0949..094C ; Extend # Mc [4] DEVANAGARI VOWEL SIGN CANDRA O..DEVANAGARI VOWEL SIGN AU
+094D ; Extend # Mn DEVANAGARI SIGN VIRAMA
+094E..094F ; Extend # Mc [2] DEVANAGARI VOWEL SIGN PRISHTHAMATRA E..DEVANAGARI VOWEL SIGN AW
+0951..0957 ; Extend # Mn [7] DEVANAGARI STRESS SIGN UDATTA..DEVANAGARI VOWEL SIGN UUE
+0962..0963 ; Extend # Mn [2] DEVANAGARI VOWEL SIGN VOCALIC L..DEVANAGARI VOWEL SIGN VOCALIC LL
+0981 ; Extend # Mn BENGALI SIGN CANDRABINDU
+0982..0983 ; Extend # Mc [2] BENGALI SIGN ANUSVARA..BENGALI SIGN VISARGA
+09BC ; Extend # Mn BENGALI SIGN NUKTA
+09BE..09C0 ; Extend # Mc [3] BENGALI VOWEL SIGN AA..BENGALI VOWEL SIGN II
+09C1..09C4 ; Extend # Mn [4] BENGALI VOWEL SIGN U..BENGALI VOWEL SIGN VOCALIC RR
+09C7..09C8 ; Extend # Mc [2] BENGALI VOWEL SIGN E..BENGALI VOWEL SIGN AI
+09CB..09CC ; Extend # Mc [2] BENGALI VOWEL SIGN O..BENGALI VOWEL SIGN AU
+09CD ; Extend # Mn BENGALI SIGN VIRAMA
+09D7 ; Extend # Mc BENGALI AU LENGTH MARK
+09E2..09E3 ; Extend # Mn [2] BENGALI VOWEL SIGN VOCALIC L..BENGALI VOWEL SIGN VOCALIC LL
+0A01..0A02 ; Extend # Mn [2] GURMUKHI SIGN ADAK BINDI..GURMUKHI SIGN BINDI
+0A03 ; Extend # Mc GURMUKHI SIGN VISARGA
+0A3C ; Extend # Mn GURMUKHI SIGN NUKTA
+0A3E..0A40 ; Extend # Mc [3] GURMUKHI VOWEL SIGN AA..GURMUKHI VOWEL SIGN II
+0A41..0A42 ; Extend # Mn [2] GURMUKHI VOWEL SIGN U..GURMUKHI VOWEL SIGN UU
+0A47..0A48 ; Extend # Mn [2] GURMUKHI VOWEL SIGN EE..GURMUKHI VOWEL SIGN AI
+0A4B..0A4D ; Extend # Mn [3] GURMUKHI VOWEL SIGN OO..GURMUKHI SIGN VIRAMA
+0A51 ; Extend # Mn GURMUKHI SIGN UDAAT
+0A70..0A71 ; Extend # Mn [2] GURMUKHI TIPPI..GURMUKHI ADDAK
+0A75 ; Extend # Mn GURMUKHI SIGN YAKASH
+0A81..0A82 ; Extend # Mn [2] GUJARATI SIGN CANDRABINDU..GUJARATI SIGN ANUSVARA
+0A83 ; Extend # Mc GUJARATI SIGN VISARGA
+0ABC ; Extend # Mn GUJARATI SIGN NUKTA
+0ABE..0AC0 ; Extend # Mc [3] GUJARATI VOWEL SIGN AA..GUJARATI VOWEL SIGN II
+0AC1..0AC5 ; Extend # Mn [5] GUJARATI VOWEL SIGN U..GUJARATI VOWEL SIGN CANDRA E
+0AC7..0AC8 ; Extend # Mn [2] GUJARATI VOWEL SIGN E..GUJARATI VOWEL SIGN AI
+0AC9 ; Extend # Mc GUJARATI VOWEL SIGN CANDRA O
+0ACB..0ACC ; Extend # Mc [2] GUJARATI VOWEL SIGN O..GUJARATI VOWEL SIGN AU
+0ACD ; Extend # Mn GUJARATI SIGN VIRAMA
+0AE2..0AE3 ; Extend # Mn [2] GUJARATI VOWEL SIGN VOCALIC L..GUJARATI VOWEL SIGN VOCALIC LL
+0B01 ; Extend # Mn ORIYA SIGN CANDRABINDU
+0B02..0B03 ; Extend # Mc [2] ORIYA SIGN ANUSVARA..ORIYA SIGN VISARGA
+0B3C ; Extend # Mn ORIYA SIGN NUKTA
+0B3E ; Extend # Mc ORIYA VOWEL SIGN AA
+0B3F ; Extend # Mn ORIYA VOWEL SIGN I
+0B40 ; Extend # Mc ORIYA VOWEL SIGN II
+0B41..0B44 ; Extend # Mn [4] ORIYA VOWEL SIGN U..ORIYA VOWEL SIGN VOCALIC RR
+0B47..0B48 ; Extend # Mc [2] ORIYA VOWEL SIGN E..ORIYA VOWEL SIGN AI
+0B4B..0B4C ; Extend # Mc [2] ORIYA VOWEL SIGN O..ORIYA VOWEL SIGN AU
+0B4D ; Extend # Mn ORIYA SIGN VIRAMA
+0B56 ; Extend # Mn ORIYA AI LENGTH MARK
+0B57 ; Extend # Mc ORIYA AU LENGTH MARK
+0B62..0B63 ; Extend # Mn [2] ORIYA VOWEL SIGN VOCALIC L..ORIYA VOWEL SIGN VOCALIC LL
+0B82 ; Extend # Mn TAMIL SIGN ANUSVARA
+0BBE..0BBF ; Extend # Mc [2] TAMIL VOWEL SIGN AA..TAMIL VOWEL SIGN I
+0BC0 ; Extend # Mn TAMIL VOWEL SIGN II
+0BC1..0BC2 ; Extend # Mc [2] TAMIL VOWEL SIGN U..TAMIL VOWEL SIGN UU
+0BC6..0BC8 ; Extend # Mc [3] TAMIL VOWEL SIGN E..TAMIL VOWEL SIGN AI
+0BCA..0BCC ; Extend # Mc [3] TAMIL VOWEL SIGN O..TAMIL VOWEL SIGN AU
+0BCD ; Extend # Mn TAMIL SIGN VIRAMA
+0BD7 ; Extend # Mc TAMIL AU LENGTH MARK
+0C00 ; Extend # Mn TELUGU SIGN COMBINING CANDRABINDU ABOVE
+0C01..0C03 ; Extend # Mc [3] TELUGU SIGN CANDRABINDU..TELUGU SIGN VISARGA
+0C3E..0C40 ; Extend # Mn [3] TELUGU VOWEL SIGN AA..TELUGU VOWEL SIGN II
+0C41..0C44 ; Extend # Mc [4] TELUGU VOWEL SIGN U..TELUGU VOWEL SIGN VOCALIC RR
+0C46..0C48 ; Extend # Mn [3] TELUGU VOWEL SIGN E..TELUGU VOWEL SIGN AI
+0C4A..0C4D ; Extend # Mn [4] TELUGU VOWEL SIGN O..TELUGU SIGN VIRAMA
+0C55..0C56 ; Extend # Mn [2] TELUGU LENGTH MARK..TELUGU AI LENGTH MARK
+0C62..0C63 ; Extend # Mn [2] TELUGU VOWEL SIGN VOCALIC L..TELUGU VOWEL SIGN VOCALIC LL
+0C81 ; Extend # Mn KANNADA SIGN CANDRABINDU
+0C82..0C83 ; Extend # Mc [2] KANNADA SIGN ANUSVARA..KANNADA SIGN VISARGA
+0CBC ; Extend # Mn KANNADA SIGN NUKTA
+0CBE ; Extend # Mc KANNADA VOWEL SIGN AA
+0CBF ; Extend # Mn KANNADA VOWEL SIGN I
+0CC0..0CC4 ; Extend # Mc [5] KANNADA VOWEL SIGN II..KANNADA VOWEL SIGN VOCALIC RR
+0CC6 ; Extend # Mn KANNADA VOWEL SIGN E
+0CC7..0CC8 ; Extend # Mc [2] KANNADA VOWEL SIGN EE..KANNADA VOWEL SIGN AI
+0CCA..0CCB ; Extend # Mc [2] KANNADA VOWEL SIGN O..KANNADA VOWEL SIGN OO
+0CCC..0CCD ; Extend # Mn [2] KANNADA VOWEL SIGN AU..KANNADA SIGN VIRAMA
+0CD5..0CD6 ; Extend # Mc [2] KANNADA LENGTH MARK..KANNADA AI LENGTH MARK
+0CE2..0CE3 ; Extend # Mn [2] KANNADA VOWEL SIGN VOCALIC L..KANNADA VOWEL SIGN VOCALIC LL
+0D01 ; Extend # Mn MALAYALAM SIGN CANDRABINDU
+0D02..0D03 ; Extend # Mc [2] MALAYALAM SIGN ANUSVARA..MALAYALAM SIGN VISARGA
+0D3E..0D40 ; Extend # Mc [3] MALAYALAM VOWEL SIGN AA..MALAYALAM VOWEL SIGN II
+0D41..0D44 ; Extend # Mn [4] MALAYALAM VOWEL SIGN U..MALAYALAM VOWEL SIGN VOCALIC RR
+0D46..0D48 ; Extend # Mc [3] MALAYALAM VOWEL SIGN E..MALAYALAM VOWEL SIGN AI
+0D4A..0D4C ; Extend # Mc [3] MALAYALAM VOWEL SIGN O..MALAYALAM VOWEL SIGN AU
+0D4D ; Extend # Mn MALAYALAM SIGN VIRAMA
+0D57 ; Extend # Mc MALAYALAM AU LENGTH MARK
+0D62..0D63 ; Extend # Mn [2] MALAYALAM VOWEL SIGN VOCALIC L..MALAYALAM VOWEL SIGN VOCALIC LL
+0D82..0D83 ; Extend # Mc [2] SINHALA SIGN ANUSVARAYA..SINHALA SIGN VISARGAYA
+0DCA ; Extend # Mn SINHALA SIGN AL-LAKUNA
+0DCF..0DD1 ; Extend # Mc [3] SINHALA VOWEL SIGN AELA-PILLA..SINHALA VOWEL SIGN DIGA AEDA-PILLA
+0DD2..0DD4 ; Extend # Mn [3] SINHALA VOWEL SIGN KETTI IS-PILLA..SINHALA VOWEL SIGN KETTI PAA-PILLA
+0DD6 ; Extend # Mn SINHALA VOWEL SIGN DIGA PAA-PILLA
+0DD8..0DDF ; Extend # Mc [8] SINHALA VOWEL SIGN GAETTA-PILLA..SINHALA VOWEL SIGN GAYANUKITTA
+0DF2..0DF3 ; Extend # Mc [2] SINHALA VOWEL SIGN DIGA GAETTA-PILLA..SINHALA VOWEL SIGN DIGA GAYANUKITTA
+0E31 ; Extend # Mn THAI CHARACTER MAI HAN-AKAT
+0E34..0E3A ; Extend # Mn [7] THAI CHARACTER SARA I..THAI CHARACTER PHINTHU
+0E47..0E4E ; Extend # Mn [8] THAI CHARACTER MAITAIKHU..THAI CHARACTER YAMAKKAN
+0EB1 ; Extend # Mn LAO VOWEL SIGN MAI KAN
+0EB4..0EB9 ; Extend # Mn [6] LAO VOWEL SIGN I..LAO VOWEL SIGN UU
+0EBB..0EBC ; Extend # Mn [2] LAO VOWEL SIGN MAI KON..LAO SEMIVOWEL SIGN LO
+0EC8..0ECD ; Extend # Mn [6] LAO TONE MAI EK..LAO NIGGAHITA
+0F18..0F19 ; Extend # Mn [2] TIBETAN ASTROLOGICAL SIGN -KHYUD PA..TIBETAN ASTROLOGICAL SIGN SDONG TSHUGS
+0F35 ; Extend # Mn TIBETAN MARK NGAS BZUNG NYI ZLA
+0F37 ; Extend # Mn TIBETAN MARK NGAS BZUNG SGOR RTAGS
+0F39 ; Extend # Mn TIBETAN MARK TSA -PHRU
+0F3E..0F3F ; Extend # Mc [2] TIBETAN SIGN YAR TSHES..TIBETAN SIGN MAR TSHES
+0F71..0F7E ; Extend # Mn [14] TIBETAN VOWEL SIGN AA..TIBETAN SIGN RJES SU NGA RO
+0F7F ; Extend # Mc TIBETAN SIGN RNAM BCAD
+0F80..0F84 ; Extend # Mn [5] TIBETAN VOWEL SIGN REVERSED I..TIBETAN MARK HALANTA
+0F86..0F87 ; Extend # Mn [2] TIBETAN SIGN LCI RTAGS..TIBETAN SIGN YANG RTAGS
+0F8D..0F97 ; Extend # Mn [11] TIBETAN SUBJOINED SIGN LCE TSA CAN..TIBETAN SUBJOINED LETTER JA
+0F99..0FBC ; Extend # Mn [36] TIBETAN SUBJOINED LETTER NYA..TIBETAN SUBJOINED LETTER FIXED-FORM RA
+0FC6 ; Extend # Mn TIBETAN SYMBOL PADMA GDAN
+102B..102C ; Extend # Mc [2] MYANMAR VOWEL SIGN TALL AA..MYANMAR VOWEL SIGN AA
+102D..1030 ; Extend # Mn [4] MYANMAR VOWEL SIGN I..MYANMAR VOWEL SIGN UU
+1031 ; Extend # Mc MYANMAR VOWEL SIGN E
+1032..1037 ; Extend # Mn [6] MYANMAR VOWEL SIGN AI..MYANMAR SIGN DOT BELOW
+1038 ; Extend # Mc MYANMAR SIGN VISARGA
+1039..103A ; Extend # Mn [2] MYANMAR SIGN VIRAMA..MYANMAR SIGN ASAT
+103B..103C ; Extend # Mc [2] MYANMAR CONSONANT SIGN MEDIAL YA..MYANMAR CONSONANT SIGN MEDIAL RA
+103D..103E ; Extend # Mn [2] MYANMAR CONSONANT SIGN MEDIAL WA..MYANMAR CONSONANT SIGN MEDIAL HA
+1056..1057 ; Extend # Mc [2] MYANMAR VOWEL SIGN VOCALIC R..MYANMAR VOWEL SIGN VOCALIC RR
+1058..1059 ; Extend # Mn [2] MYANMAR VOWEL SIGN VOCALIC L..MYANMAR VOWEL SIGN VOCALIC LL
+105E..1060 ; Extend # Mn [3] MYANMAR CONSONANT SIGN MON MEDIAL NA..MYANMAR CONSONANT SIGN MON MEDIAL LA
+1062..1064 ; Extend # Mc [3] MYANMAR VOWEL SIGN SGAW KAREN EU..MYANMAR TONE MARK SGAW KAREN KE PHO
+1067..106D ; Extend # Mc [7] MYANMAR VOWEL SIGN WESTERN PWO KAREN EU..MYANMAR SIGN WESTERN PWO KAREN TONE-5
+1071..1074 ; Extend # Mn [4] MYANMAR VOWEL SIGN GEBA KAREN I..MYANMAR VOWEL SIGN KAYAH EE
+1082 ; Extend # Mn MYANMAR CONSONANT SIGN SHAN MEDIAL WA
+1083..1084 ; Extend # Mc [2] MYANMAR VOWEL SIGN SHAN AA..MYANMAR VOWEL SIGN SHAN E
+1085..1086 ; Extend # Mn [2] MYANMAR VOWEL SIGN SHAN E ABOVE..MYANMAR VOWEL SIGN SHAN FINAL Y
+1087..108C ; Extend # Mc [6] MYANMAR SIGN SHAN TONE-2..MYANMAR SIGN SHAN COUNCIL TONE-3
+108D ; Extend # Mn MYANMAR SIGN SHAN COUNCIL EMPHATIC TONE
+108F ; Extend # Mc MYANMAR SIGN RUMAI PALAUNG TONE-5
+109A..109C ; Extend # Mc [3] MYANMAR SIGN KHAMTI TONE-1..MYANMAR VOWEL SIGN AITON A
+109D ; Extend # Mn MYANMAR VOWEL SIGN AITON AI
+135D..135F ; Extend # Mn [3] ETHIOPIC COMBINING GEMINATION AND VOWEL LENGTH MARK..ETHIOPIC COMBINING GEMINATION MARK
+1712..1714 ; Extend # Mn [3] TAGALOG VOWEL SIGN I..TAGALOG SIGN VIRAMA
+1732..1734 ; Extend # Mn [3] HANUNOO VOWEL SIGN I..HANUNOO SIGN PAMUDPOD
+1752..1753 ; Extend # Mn [2] BUHID VOWEL SIGN I..BUHID VOWEL SIGN U
+1772..1773 ; Extend # Mn [2] TAGBANWA VOWEL SIGN I..TAGBANWA VOWEL SIGN U
+17B4..17B5 ; Extend # Mn [2] KHMER VOWEL INHERENT AQ..KHMER VOWEL INHERENT AA
+17B6 ; Extend # Mc KHMER VOWEL SIGN AA
+17B7..17BD ; Extend # Mn [7] KHMER VOWEL SIGN I..KHMER VOWEL SIGN UA
+17BE..17C5 ; Extend # Mc [8] KHMER VOWEL SIGN OE..KHMER VOWEL SIGN AU
+17C6 ; Extend # Mn KHMER SIGN NIKAHIT
+17C7..17C8 ; Extend # Mc [2] KHMER SIGN REAHMUK..KHMER SIGN YUUKALEAPINTU
+17C9..17D3 ; Extend # Mn [11] KHMER SIGN MUUSIKATOAN..KHMER SIGN BATHAMASAT
+17DD ; Extend # Mn KHMER SIGN ATTHACAN
+180B..180D ; Extend # Mn [3] MONGOLIAN FREE VARIATION SELECTOR ONE..MONGOLIAN FREE VARIATION SELECTOR THREE
+1885..1886 ; Extend # Mn [2] MONGOLIAN LETTER ALI GALI BALUDA..MONGOLIAN LETTER ALI GALI THREE BALUDA
+18A9 ; Extend # Mn MONGOLIAN LETTER ALI GALI DAGALGA
+1920..1922 ; Extend # Mn [3] LIMBU VOWEL SIGN A..LIMBU VOWEL SIGN U
+1923..1926 ; Extend # Mc [4] LIMBU VOWEL SIGN EE..LIMBU VOWEL SIGN AU
+1927..1928 ; Extend # Mn [2] LIMBU VOWEL SIGN E..LIMBU VOWEL SIGN O
+1929..192B ; Extend # Mc [3] LIMBU SUBJOINED LETTER YA..LIMBU SUBJOINED LETTER WA
+1930..1931 ; Extend # Mc [2] LIMBU SMALL LETTER KA..LIMBU SMALL LETTER NGA
+1932 ; Extend # Mn LIMBU SMALL LETTER ANUSVARA
+1933..1938 ; Extend # Mc [6] LIMBU SMALL LETTER TA..LIMBU SMALL LETTER LA
+1939..193B ; Extend # Mn [3] LIMBU SIGN MUKPHRENG..LIMBU SIGN SA-I
+1A17..1A18 ; Extend # Mn [2] BUGINESE VOWEL SIGN I..BUGINESE VOWEL SIGN U
+1A19..1A1A ; Extend # Mc [2] BUGINESE VOWEL SIGN E..BUGINESE VOWEL SIGN O
+1A1B ; Extend # Mn BUGINESE VOWEL SIGN AE
+1A55 ; Extend # Mc TAI THAM CONSONANT SIGN MEDIAL RA
+1A56 ; Extend # Mn TAI THAM CONSONANT SIGN MEDIAL LA
+1A57 ; Extend # Mc TAI THAM CONSONANT SIGN LA TANG LAI
+1A58..1A5E ; Extend # Mn [7] TAI THAM SIGN MAI KANG LAI..TAI THAM CONSONANT SIGN SA
+1A60 ; Extend # Mn TAI THAM SIGN SAKOT
+1A61 ; Extend # Mc TAI THAM VOWEL SIGN A
+1A62 ; Extend # Mn TAI THAM VOWEL SIGN MAI SAT
+1A63..1A64 ; Extend # Mc [2] TAI THAM VOWEL SIGN AA..TAI THAM VOWEL SIGN TALL AA
+1A65..1A6C ; Extend # Mn [8] TAI THAM VOWEL SIGN I..TAI THAM VOWEL SIGN OA BELOW
+1A6D..1A72 ; Extend # Mc [6] TAI THAM VOWEL SIGN OY..TAI THAM VOWEL SIGN THAM AI
+1A73..1A7C ; Extend # Mn [10] TAI THAM VOWEL SIGN OA ABOVE..TAI THAM SIGN KHUEN-LUE KARAN
+1A7F ; Extend # Mn TAI THAM COMBINING CRYPTOGRAMMIC DOT
+1AB0..1ABD ; Extend # Mn [14] COMBINING DOUBLED CIRCUMFLEX ACCENT..COMBINING PARENTHESES BELOW
+1ABE ; Extend # Me COMBINING PARENTHESES OVERLAY
+1B00..1B03 ; Extend # Mn [4] BALINESE SIGN ULU RICEM..BALINESE SIGN SURANG
+1B04 ; Extend # Mc BALINESE SIGN BISAH
+1B34 ; Extend # Mn BALINESE SIGN REREKAN
+1B35 ; Extend # Mc BALINESE VOWEL SIGN TEDUNG
+1B36..1B3A ; Extend # Mn [5] BALINESE VOWEL SIGN ULU..BALINESE VOWEL SIGN RA REPA
+1B3B ; Extend # Mc BALINESE VOWEL SIGN RA REPA TEDUNG
+1B3C ; Extend # Mn BALINESE VOWEL SIGN LA LENGA
+1B3D..1B41 ; Extend # Mc [5] BALINESE VOWEL SIGN LA LENGA TEDUNG..BALINESE VOWEL SIGN TALING REPA TEDUNG
+1B42 ; Extend # Mn BALINESE VOWEL SIGN PEPET
+1B43..1B44 ; Extend # Mc [2] BALINESE VOWEL SIGN PEPET TEDUNG..BALINESE ADEG ADEG
+1B6B..1B73 ; Extend # Mn [9] BALINESE MUSICAL SYMBOL COMBINING TEGEH..BALINESE MUSICAL SYMBOL COMBINING GONG
+1B80..1B81 ; Extend # Mn [2] SUNDANESE SIGN PANYECEK..SUNDANESE SIGN PANGLAYAR
+1B82 ; Extend # Mc SUNDANESE SIGN PANGWISAD
+1BA1 ; Extend # Mc SUNDANESE CONSONANT SIGN PAMINGKAL
+1BA2..1BA5 ; Extend # Mn [4] SUNDANESE CONSONANT SIGN PANYAKRA..SUNDANESE VOWEL SIGN PANYUKU
+1BA6..1BA7 ; Extend # Mc [2] SUNDANESE VOWEL SIGN PANAELAENG..SUNDANESE VOWEL SIGN PANOLONG
+1BA8..1BA9 ; Extend # Mn [2] SUNDANESE VOWEL SIGN PAMEPET..SUNDANESE VOWEL SIGN PANEULEUNG
+1BAA ; Extend # Mc SUNDANESE SIGN PAMAAEH
+1BAB..1BAD ; Extend # Mn [3] SUNDANESE SIGN VIRAMA..SUNDANESE CONSONANT SIGN PASANGAN WA
+1BE6 ; Extend # Mn BATAK SIGN TOMPI
+1BE7 ; Extend # Mc BATAK VOWEL SIGN E
+1BE8..1BE9 ; Extend # Mn [2] BATAK VOWEL SIGN PAKPAK E..BATAK VOWEL SIGN EE
+1BEA..1BEC ; Extend # Mc [3] BATAK VOWEL SIGN I..BATAK VOWEL SIGN O
+1BED ; Extend # Mn BATAK VOWEL SIGN KARO O
+1BEE ; Extend # Mc BATAK VOWEL SIGN U
+1BEF..1BF1 ; Extend # Mn [3] BATAK VOWEL SIGN U FOR SIMALUNGUN SA..BATAK CONSONANT SIGN H
+1BF2..1BF3 ; Extend # Mc [2] BATAK PANGOLAT..BATAK PANONGONAN
+1C24..1C2B ; Extend # Mc [8] LEPCHA SUBJOINED LETTER YA..LEPCHA VOWEL SIGN UU
+1C2C..1C33 ; Extend # Mn [8] LEPCHA VOWEL SIGN E..LEPCHA CONSONANT SIGN T
+1C34..1C35 ; Extend # Mc [2] LEPCHA CONSONANT SIGN NYIN-DO..LEPCHA CONSONANT SIGN KANG
+1C36..1C37 ; Extend # Mn [2] LEPCHA SIGN RAN..LEPCHA SIGN NUKTA
+1CD0..1CD2 ; Extend # Mn [3] VEDIC TONE KARSHANA..VEDIC TONE PRENKHA
+1CD4..1CE0 ; Extend # Mn [13] VEDIC SIGN YAJURVEDIC MIDLINE SVARITA..VEDIC TONE RIGVEDIC KASHMIRI INDEPENDENT SVARITA
+1CE1 ; Extend # Mc VEDIC TONE ATHARVAVEDIC INDEPENDENT SVARITA
+1CE2..1CE8 ; Extend # Mn [7] VEDIC SIGN VISARGA SVARITA..VEDIC SIGN VISARGA ANUDATTA WITH TAIL
+1CED ; Extend # Mn VEDIC SIGN TIRYAK
+1CF2..1CF3 ; Extend # Mc [2] VEDIC SIGN ARDHAVISARGA..VEDIC SIGN ROTATED ARDHAVISARGA
+1CF4 ; Extend # Mn VEDIC TONE CANDRA ABOVE
+1CF8..1CF9 ; Extend # Mn [2] VEDIC TONE RING ABOVE..VEDIC TONE DOUBLE RING ABOVE
+1DC0..1DF5 ; Extend # Mn [54] COMBINING DOTTED GRAVE ACCENT..COMBINING UP TACK ABOVE
+1DFB..1DFF ; Extend # Mn [5] COMBINING DELETION MARK..COMBINING RIGHT ARROWHEAD AND DOWN ARROWHEAD BELOW
+200C ; Extend # Cf ZERO WIDTH NON-JOINER
+20D0..20DC ; Extend # Mn [13] COMBINING LEFT HARPOON ABOVE..COMBINING FOUR DOTS ABOVE
+20DD..20E0 ; Extend # Me [4] COMBINING ENCLOSING CIRCLE..COMBINING ENCLOSING CIRCLE BACKSLASH
+20E1 ; Extend # Mn COMBINING LEFT RIGHT ARROW ABOVE
+20E2..20E4 ; Extend # Me [3] COMBINING ENCLOSING SCREEN..COMBINING ENCLOSING UPWARD POINTING TRIANGLE
+20E5..20F0 ; Extend # Mn [12] COMBINING REVERSE SOLIDUS OVERLAY..COMBINING ASTERISK ABOVE
+2CEF..2CF1 ; Extend # Mn [3] COPTIC COMBINING NI ABOVE..COPTIC COMBINING SPIRITUS LENIS
+2D7F ; Extend # Mn TIFINAGH CONSONANT JOINER
+2DE0..2DFF ; Extend # Mn [32] COMBINING CYRILLIC LETTER BE..COMBINING CYRILLIC LETTER IOTIFIED BIG YUS
+302A..302D ; Extend # Mn [4] IDEOGRAPHIC LEVEL TONE MARK..IDEOGRAPHIC ENTERING TONE MARK
+302E..302F ; Extend # Mc [2] HANGUL SINGLE DOT TONE MARK..HANGUL DOUBLE DOT TONE MARK
+3099..309A ; Extend # Mn [2] COMBINING KATAKANA-HIRAGANA VOICED SOUND MARK..COMBINING KATAKANA-HIRAGANA SEMI-VOICED SOUND MARK
+A66F ; Extend # Mn COMBINING CYRILLIC VZMET
+A670..A672 ; Extend # Me [3] COMBINING CYRILLIC TEN MILLIONS SIGN..COMBINING CYRILLIC THOUSAND MILLIONS SIGN
+A674..A67D ; Extend # Mn [10] COMBINING CYRILLIC LETTER UKRAINIAN IE..COMBINING CYRILLIC PAYEROK
+A69E..A69F ; Extend # Mn [2] COMBINING CYRILLIC LETTER EF..COMBINING CYRILLIC LETTER IOTIFIED E
+A6F0..A6F1 ; Extend # Mn [2] BAMUM COMBINING MARK KOQNDON..BAMUM COMBINING MARK TUKWENTIS
+A802 ; Extend # Mn SYLOTI NAGRI SIGN DVISVARA
+A806 ; Extend # Mn SYLOTI NAGRI SIGN HASANTA
+A80B ; Extend # Mn SYLOTI NAGRI SIGN ANUSVARA
+A823..A824 ; Extend # Mc [2] SYLOTI NAGRI VOWEL SIGN A..SYLOTI NAGRI VOWEL SIGN I
+A825..A826 ; Extend # Mn [2] SYLOTI NAGRI VOWEL SIGN U..SYLOTI NAGRI VOWEL SIGN E
+A827 ; Extend # Mc SYLOTI NAGRI VOWEL SIGN OO
+A880..A881 ; Extend # Mc [2] SAURASHTRA SIGN ANUSVARA..SAURASHTRA SIGN VISARGA
+A8B4..A8C3 ; Extend # Mc [16] SAURASHTRA CONSONANT SIGN HAARU..SAURASHTRA VOWEL SIGN AU
+A8C4..A8C5 ; Extend # Mn [2] SAURASHTRA SIGN VIRAMA..SAURASHTRA SIGN CANDRABINDU
+A8E0..A8F1 ; Extend # Mn [18] COMBINING DEVANAGARI DIGIT ZERO..COMBINING DEVANAGARI SIGN AVAGRAHA
+A926..A92D ; Extend # Mn [8] KAYAH LI VOWEL UE..KAYAH LI TONE CALYA PLOPHU
+A947..A951 ; Extend # Mn [11] REJANG VOWEL SIGN I..REJANG CONSONANT SIGN R
+A952..A953 ; Extend # Mc [2] REJANG CONSONANT SIGN H..REJANG VIRAMA
+A980..A982 ; Extend # Mn [3] JAVANESE SIGN PANYANGGA..JAVANESE SIGN LAYAR
+A983 ; Extend # Mc JAVANESE SIGN WIGNYAN
+A9B3 ; Extend # Mn JAVANESE SIGN CECAK TELU
+A9B4..A9B5 ; Extend # Mc [2] JAVANESE VOWEL SIGN TARUNG..JAVANESE VOWEL SIGN TOLONG
+A9B6..A9B9 ; Extend # Mn [4] JAVANESE VOWEL SIGN WULU..JAVANESE VOWEL SIGN SUKU MENDUT
+A9BA..A9BB ; Extend # Mc [2] JAVANESE VOWEL SIGN TALING..JAVANESE VOWEL SIGN DIRGA MURE
+A9BC ; Extend # Mn JAVANESE VOWEL SIGN PEPET
+A9BD..A9C0 ; Extend # Mc [4] JAVANESE CONSONANT SIGN KERET..JAVANESE PANGKON
+A9E5 ; Extend # Mn MYANMAR SIGN SHAN SAW
+AA29..AA2E ; Extend # Mn [6] CHAM VOWEL SIGN AA..CHAM VOWEL SIGN OE
+AA2F..AA30 ; Extend # Mc [2] CHAM VOWEL SIGN O..CHAM VOWEL SIGN AI
+AA31..AA32 ; Extend # Mn [2] CHAM VOWEL SIGN AU..CHAM VOWEL SIGN UE
+AA33..AA34 ; Extend # Mc [2] CHAM CONSONANT SIGN YA..CHAM CONSONANT SIGN RA
+AA35..AA36 ; Extend # Mn [2] CHAM CONSONANT SIGN LA..CHAM CONSONANT SIGN WA
+AA43 ; Extend # Mn CHAM CONSONANT SIGN FINAL NG
+AA4C ; Extend # Mn CHAM CONSONANT SIGN FINAL M
+AA4D ; Extend # Mc CHAM CONSONANT SIGN FINAL H
+AA7B ; Extend # Mc MYANMAR SIGN PAO KAREN TONE
+AA7C ; Extend # Mn MYANMAR SIGN TAI LAING TONE-2
+AA7D ; Extend # Mc MYANMAR SIGN TAI LAING TONE-5
+AAB0 ; Extend # Mn TAI VIET MAI KANG
+AAB2..AAB4 ; Extend # Mn [3] TAI VIET VOWEL I..TAI VIET VOWEL U
+AAB7..AAB8 ; Extend # Mn [2] TAI VIET MAI KHIT..TAI VIET VOWEL IA
+AABE..AABF ; Extend # Mn [2] TAI VIET VOWEL AM..TAI VIET TONE MAI EK
+AAC1 ; Extend # Mn TAI VIET TONE MAI THO
+AAEB ; Extend # Mc MEETEI MAYEK VOWEL SIGN II
+AAEC..AAED ; Extend # Mn [2] MEETEI MAYEK VOWEL SIGN UU..MEETEI MAYEK VOWEL SIGN AAI
+AAEE..AAEF ; Extend # Mc [2] MEETEI MAYEK VOWEL SIGN AU..MEETEI MAYEK VOWEL SIGN AAU
+AAF5 ; Extend # Mc MEETEI MAYEK VOWEL SIGN VISARGA
+AAF6 ; Extend # Mn MEETEI MAYEK VIRAMA
+ABE3..ABE4 ; Extend # Mc [2] MEETEI MAYEK VOWEL SIGN ONAP..MEETEI MAYEK VOWEL SIGN INAP
+ABE5 ; Extend # Mn MEETEI MAYEK VOWEL SIGN ANAP
+ABE6..ABE7 ; Extend # Mc [2] MEETEI MAYEK VOWEL SIGN YENAP..MEETEI MAYEK VOWEL SIGN SOUNAP
+ABE8 ; Extend # Mn MEETEI MAYEK VOWEL SIGN UNAP
+ABE9..ABEA ; Extend # Mc [2] MEETEI MAYEK VOWEL SIGN CHEINAP..MEETEI MAYEK VOWEL SIGN NUNG
+ABEC ; Extend # Mc MEETEI MAYEK LUM IYEK
+ABED ; Extend # Mn MEETEI MAYEK APUN IYEK
+FB1E ; Extend # Mn HEBREW POINT JUDEO-SPANISH VARIKA
+FE00..FE0F ; Extend # Mn [16] VARIATION SELECTOR-1..VARIATION SELECTOR-16
+FE20..FE2F ; Extend # Mn [16] COMBINING LIGATURE LEFT HALF..COMBINING CYRILLIC TITLO RIGHT HALF
+FF9E..FF9F ; Extend # Lm [2] HALFWIDTH KATAKANA VOICED SOUND MARK..HALFWIDTH KATAKANA SEMI-VOICED SOUND MARK
+101FD ; Extend # Mn PHAISTOS DISC SIGN COMBINING OBLIQUE STROKE
+102E0 ; Extend # Mn COPTIC EPACT THOUSANDS MARK
+10376..1037A ; Extend # Mn [5] COMBINING OLD PERMIC LETTER AN..COMBINING OLD PERMIC LETTER SII
+10A01..10A03 ; Extend # Mn [3] KHAROSHTHI VOWEL SIGN I..KHAROSHTHI VOWEL SIGN VOCALIC R
+10A05..10A06 ; Extend # Mn [2] KHAROSHTHI VOWEL SIGN E..KHAROSHTHI VOWEL SIGN O
+10A0C..10A0F ; Extend # Mn [4] KHAROSHTHI VOWEL LENGTH MARK..KHAROSHTHI SIGN VISARGA
+10A38..10A3A ; Extend # Mn [3] KHAROSHTHI SIGN BAR ABOVE..KHAROSHTHI SIGN DOT BELOW
+10A3F ; Extend # Mn KHAROSHTHI VIRAMA
+10AE5..10AE6 ; Extend # Mn [2] MANICHAEAN ABBREVIATION MARK ABOVE..MANICHAEAN ABBREVIATION MARK BELOW
+11000 ; Extend # Mc BRAHMI SIGN CANDRABINDU
+11001 ; Extend # Mn BRAHMI SIGN ANUSVARA
+11002 ; Extend # Mc BRAHMI SIGN VISARGA
+11038..11046 ; Extend # Mn [15] BRAHMI VOWEL SIGN AA..BRAHMI VIRAMA
+1107F..11081 ; Extend # Mn [3] BRAHMI NUMBER JOINER..KAITHI SIGN ANUSVARA
+11082 ; Extend # Mc KAITHI SIGN VISARGA
+110B0..110B2 ; Extend # Mc [3] KAITHI VOWEL SIGN AA..KAITHI VOWEL SIGN II
+110B3..110B6 ; Extend # Mn [4] KAITHI VOWEL SIGN U..KAITHI VOWEL SIGN AI
+110B7..110B8 ; Extend # Mc [2] KAITHI VOWEL SIGN O..KAITHI VOWEL SIGN AU
+110B9..110BA ; Extend # Mn [2] KAITHI SIGN VIRAMA..KAITHI SIGN NUKTA
+11100..11102 ; Extend # Mn [3] CHAKMA SIGN CANDRABINDU..CHAKMA SIGN VISARGA
+11127..1112B ; Extend # Mn [5] CHAKMA VOWEL SIGN A..CHAKMA VOWEL SIGN UU
+1112C ; Extend # Mc CHAKMA VOWEL SIGN E
+1112D..11134 ; Extend # Mn [8] CHAKMA VOWEL SIGN AI..CHAKMA MAAYYAA
+11173 ; Extend # Mn MAHAJANI SIGN NUKTA
+11180..11181 ; Extend # Mn [2] SHARADA SIGN CANDRABINDU..SHARADA SIGN ANUSVARA
+11182 ; Extend # Mc SHARADA SIGN VISARGA
+111B3..111B5 ; Extend # Mc [3] SHARADA VOWEL SIGN AA..SHARADA VOWEL SIGN II
+111B6..111BE ; Extend # Mn [9] SHARADA VOWEL SIGN U..SHARADA VOWEL SIGN O
+111BF..111C0 ; Extend # Mc [2] SHARADA VOWEL SIGN AU..SHARADA SIGN VIRAMA
+111CA..111CC ; Extend # Mn [3] SHARADA SIGN NUKTA..SHARADA EXTRA SHORT VOWEL MARK
+1122C..1122E ; Extend # Mc [3] KHOJKI VOWEL SIGN AA..KHOJKI VOWEL SIGN II
+1122F..11231 ; Extend # Mn [3] KHOJKI VOWEL SIGN U..KHOJKI VOWEL SIGN AI
+11232..11233 ; Extend # Mc [2] KHOJKI VOWEL SIGN O..KHOJKI VOWEL SIGN AU
+11234 ; Extend # Mn KHOJKI SIGN ANUSVARA
+11235 ; Extend # Mc KHOJKI SIGN VIRAMA
+11236..11237 ; Extend # Mn [2] KHOJKI SIGN NUKTA..KHOJKI SIGN SHADDA
+1123E ; Extend # Mn KHOJKI SIGN SUKUN
+112DF ; Extend # Mn KHUDAWADI SIGN ANUSVARA
+112E0..112E2 ; Extend # Mc [3] KHUDAWADI VOWEL SIGN AA..KHUDAWADI VOWEL SIGN II
+112E3..112EA ; Extend # Mn [8] KHUDAWADI VOWEL SIGN U..KHUDAWADI SIGN VIRAMA
+11300..11301 ; Extend # Mn [2] GRANTHA SIGN COMBINING ANUSVARA ABOVE..GRANTHA SIGN CANDRABINDU
+11302..11303 ; Extend # Mc [2] GRANTHA SIGN ANUSVARA..GRANTHA SIGN VISARGA
+1133C ; Extend # Mn GRANTHA SIGN NUKTA
+1133E..1133F ; Extend # Mc [2] GRANTHA VOWEL SIGN AA..GRANTHA VOWEL SIGN I
+11340 ; Extend # Mn GRANTHA VOWEL SIGN II
+11341..11344 ; Extend # Mc [4] GRANTHA VOWEL SIGN U..GRANTHA VOWEL SIGN VOCALIC RR
+11347..11348 ; Extend # Mc [2] GRANTHA VOWEL SIGN EE..GRANTHA VOWEL SIGN AI
+1134B..1134D ; Extend # Mc [3] GRANTHA VOWEL SIGN OO..GRANTHA SIGN VIRAMA
+11357 ; Extend # Mc GRANTHA AU LENGTH MARK
+11362..11363 ; Extend # Mc [2] GRANTHA VOWEL SIGN VOCALIC L..GRANTHA VOWEL SIGN VOCALIC LL
+11366..1136C ; Extend # Mn [7] COMBINING GRANTHA DIGIT ZERO..COMBINING GRANTHA DIGIT SIX
+11370..11374 ; Extend # Mn [5] COMBINING GRANTHA LETTER A..COMBINING GRANTHA LETTER PA
+11435..11437 ; Extend # Mc [3] NEWA VOWEL SIGN AA..NEWA VOWEL SIGN II
+11438..1143F ; Extend # Mn [8] NEWA VOWEL SIGN U..NEWA VOWEL SIGN AI
+11440..11441 ; Extend # Mc [2] NEWA VOWEL SIGN O..NEWA VOWEL SIGN AU
+11442..11444 ; Extend # Mn [3] NEWA SIGN VIRAMA..NEWA SIGN ANUSVARA
+11445 ; Extend # Mc NEWA SIGN VISARGA
+11446 ; Extend # Mn NEWA SIGN NUKTA
+114B0..114B2 ; Extend # Mc [3] TIRHUTA VOWEL SIGN AA..TIRHUTA VOWEL SIGN II
+114B3..114B8 ; Extend # Mn [6] TIRHUTA VOWEL SIGN U..TIRHUTA VOWEL SIGN VOCALIC LL
+114B9 ; Extend # Mc TIRHUTA VOWEL SIGN E
+114BA ; Extend # Mn TIRHUTA VOWEL SIGN SHORT E
+114BB..114BE ; Extend # Mc [4] TIRHUTA VOWEL SIGN AI..TIRHUTA VOWEL SIGN AU
+114BF..114C0 ; Extend # Mn [2] TIRHUTA SIGN CANDRABINDU..TIRHUTA SIGN ANUSVARA
+114C1 ; Extend # Mc TIRHUTA SIGN VISARGA
+114C2..114C3 ; Extend # Mn [2] TIRHUTA SIGN VIRAMA..TIRHUTA SIGN NUKTA
+115AF..115B1 ; Extend # Mc [3] SIDDHAM VOWEL SIGN AA..SIDDHAM VOWEL SIGN II
+115B2..115B5 ; Extend # Mn [4] SIDDHAM VOWEL SIGN U..SIDDHAM VOWEL SIGN VOCALIC RR
+115B8..115BB ; Extend # Mc [4] SIDDHAM VOWEL SIGN E..SIDDHAM VOWEL SIGN AU
+115BC..115BD ; Extend # Mn [2] SIDDHAM SIGN CANDRABINDU..SIDDHAM SIGN ANUSVARA
+115BE ; Extend # Mc SIDDHAM SIGN VISARGA
+115BF..115C0 ; Extend # Mn [2] SIDDHAM SIGN VIRAMA..SIDDHAM SIGN NUKTA
+115DC..115DD ; Extend # Mn [2] SIDDHAM VOWEL SIGN ALTERNATE U..SIDDHAM VOWEL SIGN ALTERNATE UU
+11630..11632 ; Extend # Mc [3] MODI VOWEL SIGN AA..MODI VOWEL SIGN II
+11633..1163A ; Extend # Mn [8] MODI VOWEL SIGN U..MODI VOWEL SIGN AI
+1163B..1163C ; Extend # Mc [2] MODI VOWEL SIGN O..MODI VOWEL SIGN AU
+1163D ; Extend # Mn MODI SIGN ANUSVARA
+1163E ; Extend # Mc MODI SIGN VISARGA
+1163F..11640 ; Extend # Mn [2] MODI SIGN VIRAMA..MODI SIGN ARDHACANDRA
+116AB ; Extend # Mn TAKRI SIGN ANUSVARA
+116AC ; Extend # Mc TAKRI SIGN VISARGA
+116AD ; Extend # Mn TAKRI VOWEL SIGN AA
+116AE..116AF ; Extend # Mc [2] TAKRI VOWEL SIGN I..TAKRI VOWEL SIGN II
+116B0..116B5 ; Extend # Mn [6] TAKRI VOWEL SIGN U..TAKRI VOWEL SIGN AU
+116B6 ; Extend # Mc TAKRI SIGN VIRAMA
+116B7 ; Extend # Mn TAKRI SIGN NUKTA
+1171D..1171F ; Extend # Mn [3] AHOM CONSONANT SIGN MEDIAL LA..AHOM CONSONANT SIGN MEDIAL LIGATING RA
+11720..11721 ; Extend # Mc [2] AHOM VOWEL SIGN A..AHOM VOWEL SIGN AA
+11722..11725 ; Extend # Mn [4] AHOM VOWEL SIGN I..AHOM VOWEL SIGN UU
+11726 ; Extend # Mc AHOM VOWEL SIGN E
+11727..1172B ; Extend # Mn [5] AHOM VOWEL SIGN AW..AHOM SIGN KILLER
+11C2F ; Extend # Mc BHAIKSUKI VOWEL SIGN AA
+11C30..11C36 ; Extend # Mn [7] BHAIKSUKI VOWEL SIGN I..BHAIKSUKI VOWEL SIGN VOCALIC L
+11C38..11C3D ; Extend # Mn [6] BHAIKSUKI VOWEL SIGN E..BHAIKSUKI SIGN ANUSVARA
+11C3E ; Extend # Mc BHAIKSUKI SIGN VISARGA
+11C3F ; Extend # Mn BHAIKSUKI SIGN VIRAMA
+11C92..11CA7 ; Extend # Mn [22] MARCHEN SUBJOINED LETTER KA..MARCHEN SUBJOINED LETTER ZA
+11CA9 ; Extend # Mc MARCHEN SUBJOINED LETTER YA
+11CAA..11CB0 ; Extend # Mn [7] MARCHEN SUBJOINED LETTER RA..MARCHEN VOWEL SIGN AA
+11CB1 ; Extend # Mc MARCHEN VOWEL SIGN I
+11CB2..11CB3 ; Extend # Mn [2] MARCHEN VOWEL SIGN U..MARCHEN VOWEL SIGN E
+11CB4 ; Extend # Mc MARCHEN VOWEL SIGN O
+11CB5..11CB6 ; Extend # Mn [2] MARCHEN SIGN ANUSVARA..MARCHEN SIGN CANDRABINDU
+16AF0..16AF4 ; Extend # Mn [5] BASSA VAH COMBINING HIGH TONE..BASSA VAH COMBINING HIGH-LOW TONE
+16B30..16B36 ; Extend # Mn [7] PAHAWH HMONG MARK CIM TUB..PAHAWH HMONG MARK CIM TAUM
+16F51..16F7E ; Extend # Mc [46] MIAO SIGN ASPIRATION..MIAO VOWEL SIGN NG
+16F8F..16F92 ; Extend # Mn [4] MIAO TONE RIGHT..MIAO TONE BELOW
+1BC9D..1BC9E ; Extend # Mn [2] DUPLOYAN THICK LETTER SELECTOR..DUPLOYAN DOUBLE MARK
+1D165..1D166 ; Extend # Mc [2] MUSICAL SYMBOL COMBINING STEM..MUSICAL SYMBOL COMBINING SPRECHGESANG STEM
+1D167..1D169 ; Extend # Mn [3] MUSICAL SYMBOL COMBINING TREMOLO-1..MUSICAL SYMBOL COMBINING TREMOLO-3
+1D16D..1D172 ; Extend # Mc [6] MUSICAL SYMBOL COMBINING AUGMENTATION DOT..MUSICAL SYMBOL COMBINING FLAG-5
+1D17B..1D182 ; Extend # Mn [8] MUSICAL SYMBOL COMBINING ACCENT..MUSICAL SYMBOL COMBINING LOURE
+1D185..1D18B ; Extend # Mn [7] MUSICAL SYMBOL COMBINING DOIT..MUSICAL SYMBOL COMBINING TRIPLE TONGUE
+1D1AA..1D1AD ; Extend # Mn [4] MUSICAL SYMBOL COMBINING DOWN BOW..MUSICAL SYMBOL COMBINING SNAP PIZZICATO
+1D242..1D244 ; Extend # Mn [3] COMBINING GREEK MUSICAL TRISEME..COMBINING GREEK MUSICAL PENTASEME
+1DA00..1DA36 ; Extend # Mn [55] SIGNWRITING HEAD RIM..SIGNWRITING AIR SUCKING IN
+1DA3B..1DA6C ; Extend # Mn [50] SIGNWRITING MOUTH CLOSED NEUTRAL..SIGNWRITING EXCITEMENT
+1DA75 ; Extend # Mn SIGNWRITING UPPER BODY TILTING FROM HIP JOINTS
+1DA84 ; Extend # Mn SIGNWRITING LOCATION HEAD NECK
+1DA9B..1DA9F ; Extend # Mn [5] SIGNWRITING FILL MODIFIER-2..SIGNWRITING FILL MODIFIER-6
+1DAA1..1DAAF ; Extend # Mn [15] SIGNWRITING ROTATION MODIFIER-2..SIGNWRITING ROTATION MODIFIER-16
+1E000..1E006 ; Extend # Mn [7] COMBINING GLAGOLITIC LETTER AZU..COMBINING GLAGOLITIC LETTER ZHIVETE
+1E008..1E018 ; Extend # Mn [17] COMBINING GLAGOLITIC LETTER ZEMLJA..COMBINING GLAGOLITIC LETTER HERU
+1E01B..1E021 ; Extend # Mn [7] COMBINING GLAGOLITIC LETTER SHTA..COMBINING GLAGOLITIC LETTER YATI
+1E023..1E024 ; Extend # Mn [2] COMBINING GLAGOLITIC LETTER YU..COMBINING GLAGOLITIC LETTER SMALL YUS
+1E026..1E02A ; Extend # Mn [5] COMBINING GLAGOLITIC LETTER YO..COMBINING GLAGOLITIC LETTER FITA
+1E8D0..1E8D6 ; Extend # Mn [7] MENDE KIKAKUI COMBINING NUMBER TEENS..MENDE KIKAKUI COMBINING NUMBER MILLIONS
+1E944..1E94A ; Extend # Mn [7] ADLAM ALIF LENGTHENER..ADLAM NUKTA
+E0020..E007F ; Extend # Cf [96] TAG SPACE..CANCEL TAG
+E0100..E01EF ; Extend # Mn [240] VARIATION SELECTOR-17..VARIATION SELECTOR-256
+
+# Total code points: 2196
+
+# ================================================
+
+1F1E6..1F1FF ; Regional_Indicator # So [26] REGIONAL INDICATOR SYMBOL LETTER A..REGIONAL INDICATOR SYMBOL LETTER Z
+
+# Total code points: 26
+
+# ================================================
+
+00AD ; Format # Cf SOFT HYPHEN
+0600..0605 ; Format # Cf [6] ARABIC NUMBER SIGN..ARABIC NUMBER MARK ABOVE
+061C ; Format # Cf ARABIC LETTER MARK
+06DD ; Format # Cf ARABIC END OF AYAH
+070F ; Format # Cf SYRIAC ABBREVIATION MARK
+08E2 ; Format # Cf ARABIC DISPUTED END OF AYAH
+180E ; Format # Cf MONGOLIAN VOWEL SEPARATOR
+200E..200F ; Format # Cf [2] LEFT-TO-RIGHT MARK..RIGHT-TO-LEFT MARK
+202A..202E ; Format # Cf [5] LEFT-TO-RIGHT EMBEDDING..RIGHT-TO-LEFT OVERRIDE
+2060..2064 ; Format # Cf [5] WORD JOINER..INVISIBLE PLUS
+2066..206F ; Format # Cf [10] LEFT-TO-RIGHT ISOLATE..NOMINAL DIGIT SHAPES
+FEFF ; Format # Cf ZERO WIDTH NO-BREAK SPACE
+FFF9..FFFB ; Format # Cf [3] INTERLINEAR ANNOTATION ANCHOR..INTERLINEAR ANNOTATION TERMINATOR
+110BD ; Format # Cf KAITHI NUMBER SIGN
+1BCA0..1BCA3 ; Format # Cf [4] SHORTHAND FORMAT LETTER OVERLAP..SHORTHAND FORMAT UP STEP
+1D173..1D17A ; Format # Cf [8] MUSICAL SYMBOL BEGIN BEAM..MUSICAL SYMBOL END PHRASE
+E0001 ; Format # Cf LANGUAGE TAG
+
+# Total code points: 52
+
+# ================================================
+
+3031..3035 ; Katakana # Lm [5] VERTICAL KANA REPEAT MARK..VERTICAL KANA REPEAT MARK LOWER HALF
+309B..309C ; Katakana # Sk [2] KATAKANA-HIRAGANA VOICED SOUND MARK..KATAKANA-HIRAGANA SEMI-VOICED SOUND MARK
+30A0 ; Katakana # Pd KATAKANA-HIRAGANA DOUBLE HYPHEN
+30A1..30FA ; Katakana # Lo [90] KATAKANA LETTER SMALL A..KATAKANA LETTER VO
+30FC..30FE ; Katakana # Lm [3] KATAKANA-HIRAGANA PROLONGED SOUND MARK..KATAKANA VOICED ITERATION MARK
+30FF ; Katakana # Lo KATAKANA DIGRAPH KOTO
+31F0..31FF ; Katakana # Lo [16] KATAKANA LETTER SMALL KU..KATAKANA LETTER SMALL RO
+32D0..32FE ; Katakana # So [47] CIRCLED KATAKANA A..CIRCLED KATAKANA WO
+3300..3357 ; Katakana # So [88] SQUARE APAATO..SQUARE WATTO
+FF66..FF6F ; Katakana # Lo [10] HALFWIDTH KATAKANA LETTER WO..HALFWIDTH KATAKANA LETTER SMALL TU
+FF70 ; Katakana # Lm HALFWIDTH KATAKANA-HIRAGANA PROLONGED SOUND MARK
+FF71..FF9D ; Katakana # Lo [45] HALFWIDTH KATAKANA LETTER A..HALFWIDTH KATAKANA LETTER N
+1B000 ; Katakana # Lo KATAKANA LETTER ARCHAIC E
+
+# Total code points: 310
+
+# ================================================
+
+0041..005A ; ALetter # L& [26] LATIN CAPITAL LETTER A..LATIN CAPITAL LETTER Z
+0061..007A ; ALetter # L& [26] LATIN SMALL LETTER A..LATIN SMALL LETTER Z
+00AA ; ALetter # Lo FEMININE ORDINAL INDICATOR
+00B5 ; ALetter # L& MICRO SIGN
+00BA ; ALetter # Lo MASCULINE ORDINAL INDICATOR
+00C0..00D6 ; ALetter # L& [23] LATIN CAPITAL LETTER A WITH GRAVE..LATIN CAPITAL LETTER O WITH DIAERESIS
+00D8..00F6 ; ALetter # L& [31] LATIN CAPITAL LETTER O WITH STROKE..LATIN SMALL LETTER O WITH DIAERESIS
+00F8..01BA ; ALetter # L& [195] LATIN SMALL LETTER O WITH STROKE..LATIN SMALL LETTER EZH WITH TAIL
+01BB ; ALetter # Lo LATIN LETTER TWO WITH STROKE
+01BC..01BF ; ALetter # L& [4] LATIN CAPITAL LETTER TONE FIVE..LATIN LETTER WYNN
+01C0..01C3 ; ALetter # Lo [4] LATIN LETTER DENTAL CLICK..LATIN LETTER RETROFLEX CLICK
+01C4..0293 ; ALetter # L& [208] LATIN CAPITAL LETTER DZ WITH CARON..LATIN SMALL LETTER EZH WITH CURL
+0294 ; ALetter # Lo LATIN LETTER GLOTTAL STOP
+0295..02AF ; ALetter # L& [27] LATIN LETTER PHARYNGEAL VOICED FRICATIVE..LATIN SMALL LETTER TURNED H WITH FISHHOOK AND TAIL
+02B0..02C1 ; ALetter # Lm [18] MODIFIER LETTER SMALL H..MODIFIER LETTER REVERSED GLOTTAL STOP
+02C6..02D1 ; ALetter # Lm [12] MODIFIER LETTER CIRCUMFLEX ACCENT..MODIFIER LETTER HALF TRIANGULAR COLON
+02E0..02E4 ; ALetter # Lm [5] MODIFIER LETTER SMALL GAMMA..MODIFIER LETTER SMALL REVERSED GLOTTAL STOP
+02EC ; ALetter # Lm MODIFIER LETTER VOICING
+02EE ; ALetter # Lm MODIFIER LETTER DOUBLE APOSTROPHE
+0370..0373 ; ALetter # L& [4] GREEK CAPITAL LETTER HETA..GREEK SMALL LETTER ARCHAIC SAMPI
+0374 ; ALetter # Lm GREEK NUMERAL SIGN
+0376..0377 ; ALetter # L& [2] GREEK CAPITAL LETTER PAMPHYLIAN DIGAMMA..GREEK SMALL LETTER PAMPHYLIAN DIGAMMA
+037A ; ALetter # Lm GREEK YPOGEGRAMMENI
+037B..037D ; ALetter # L& [3] GREEK SMALL REVERSED LUNATE SIGMA SYMBOL..GREEK SMALL REVERSED DOTTED LUNATE SIGMA SYMBOL
+037F ; ALetter # L& GREEK CAPITAL LETTER YOT
+0386 ; ALetter # L& GREEK CAPITAL LETTER ALPHA WITH TONOS
+0388..038A ; ALetter # L& [3] GREEK CAPITAL LETTER EPSILON WITH TONOS..GREEK CAPITAL LETTER IOTA WITH TONOS
+038C ; ALetter # L& GREEK CAPITAL LETTER OMICRON WITH TONOS
+038E..03A1 ; ALetter # L& [20] GREEK CAPITAL LETTER UPSILON WITH TONOS..GREEK CAPITAL LETTER RHO
+03A3..03F5 ; ALetter # L& [83] GREEK CAPITAL LETTER SIGMA..GREEK LUNATE EPSILON SYMBOL
+03F7..0481 ; ALetter # L& [139] GREEK CAPITAL LETTER SHO..CYRILLIC SMALL LETTER KOPPA
+048A..052F ; ALetter # L& [166] CYRILLIC CAPITAL LETTER SHORT I WITH TAIL..CYRILLIC SMALL LETTER EL WITH DESCENDER
+0531..0556 ; ALetter # L& [38] ARMENIAN CAPITAL LETTER AYB..ARMENIAN CAPITAL LETTER FEH
+0559 ; ALetter # Lm ARMENIAN MODIFIER LETTER LEFT HALF RING
+0561..0587 ; ALetter # L& [39] ARMENIAN SMALL LETTER AYB..ARMENIAN SMALL LIGATURE ECH YIWN
+05F3 ; ALetter # Po HEBREW PUNCTUATION GERESH
+0620..063F ; ALetter # Lo [32] ARABIC LETTER KASHMIRI YEH..ARABIC LETTER FARSI YEH WITH THREE DOTS ABOVE
+0640 ; ALetter # Lm ARABIC TATWEEL
+0641..064A ; ALetter # Lo [10] ARABIC LETTER FEH..ARABIC LETTER YEH
+066E..066F ; ALetter # Lo [2] ARABIC LETTER DOTLESS BEH..ARABIC LETTER DOTLESS QAF
+0671..06D3 ; ALetter # Lo [99] ARABIC LETTER ALEF WASLA..ARABIC LETTER YEH BARREE WITH HAMZA ABOVE
+06D5 ; ALetter # Lo ARABIC LETTER AE
+06E5..06E6 ; ALetter # Lm [2] ARABIC SMALL WAW..ARABIC SMALL YEH
+06EE..06EF ; ALetter # Lo [2] ARABIC LETTER DAL WITH INVERTED V..ARABIC LETTER REH WITH INVERTED V
+06FA..06FC ; ALetter # Lo [3] ARABIC LETTER SHEEN WITH DOT BELOW..ARABIC LETTER GHAIN WITH DOT BELOW
+06FF ; ALetter # Lo ARABIC LETTER HEH WITH INVERTED V
+0710 ; ALetter # Lo SYRIAC LETTER ALAPH
+0712..072F ; ALetter # Lo [30] SYRIAC LETTER BETH..SYRIAC LETTER PERSIAN DHALATH
+074D..07A5 ; ALetter # Lo [89] SYRIAC LETTER SOGDIAN ZHAIN..THAANA LETTER WAAVU
+07B1 ; ALetter # Lo THAANA LETTER NAA
+07CA..07EA ; ALetter # Lo [33] NKO LETTER A..NKO LETTER JONA RA
+07F4..07F5 ; ALetter # Lm [2] NKO HIGH TONE APOSTROPHE..NKO LOW TONE APOSTROPHE
+07FA ; ALetter # Lm NKO LAJANYALAN
+0800..0815 ; ALetter # Lo [22] SAMARITAN LETTER ALAF..SAMARITAN LETTER TAAF
+081A ; ALetter # Lm SAMARITAN MODIFIER LETTER EPENTHETIC YUT
+0824 ; ALetter # Lm SAMARITAN MODIFIER LETTER SHORT A
+0828 ; ALetter # Lm SAMARITAN MODIFIER LETTER I
+0840..0858 ; ALetter # Lo [25] MANDAIC LETTER HALQA..MANDAIC LETTER AIN
+08A0..08B4 ; ALetter # Lo [21] ARABIC LETTER BEH WITH SMALL V BELOW..ARABIC LETTER KAF WITH DOT BELOW
+08B6..08BD ; ALetter # Lo [8] ARABIC LETTER BEH WITH SMALL MEEM ABOVE..ARABIC LETTER AFRICAN NOON
+0904..0939 ; ALetter # Lo [54] DEVANAGARI LETTER SHORT A..DEVANAGARI LETTER HA
+093D ; ALetter # Lo DEVANAGARI SIGN AVAGRAHA
+0950 ; ALetter # Lo DEVANAGARI OM
+0958..0961 ; ALetter # Lo [10] DEVANAGARI LETTER QA..DEVANAGARI LETTER VOCALIC LL
+0971 ; ALetter # Lm DEVANAGARI SIGN HIGH SPACING DOT
+0972..0980 ; ALetter # Lo [15] DEVANAGARI LETTER CANDRA A..BENGALI ANJI
+0985..098C ; ALetter # Lo [8] BENGALI LETTER A..BENGALI LETTER VOCALIC L
+098F..0990 ; ALetter # Lo [2] BENGALI LETTER E..BENGALI LETTER AI
+0993..09A8 ; ALetter # Lo [22] BENGALI LETTER O..BENGALI LETTER NA
+09AA..09B0 ; ALetter # Lo [7] BENGALI LETTER PA..BENGALI LETTER RA
+09B2 ; ALetter # Lo BENGALI LETTER LA
+09B6..09B9 ; ALetter # Lo [4] BENGALI LETTER SHA..BENGALI LETTER HA
+09BD ; ALetter # Lo BENGALI SIGN AVAGRAHA
+09CE ; ALetter # Lo BENGALI LETTER KHANDA TA
+09DC..09DD ; ALetter # Lo [2] BENGALI LETTER RRA..BENGALI LETTER RHA
+09DF..09E1 ; ALetter # Lo [3] BENGALI LETTER YYA..BENGALI LETTER VOCALIC LL
+09F0..09F1 ; ALetter # Lo [2] BENGALI LETTER RA WITH MIDDLE DIAGONAL..BENGALI LETTER RA WITH LOWER DIAGONAL
+0A05..0A0A ; ALetter # Lo [6] GURMUKHI LETTER A..GURMUKHI LETTER UU
+0A0F..0A10 ; ALetter # Lo [2] GURMUKHI LETTER EE..GURMUKHI LETTER AI
+0A13..0A28 ; ALetter # Lo [22] GURMUKHI LETTER OO..GURMUKHI LETTER NA
+0A2A..0A30 ; ALetter # Lo [7] GURMUKHI LETTER PA..GURMUKHI LETTER RA
+0A32..0A33 ; ALetter # Lo [2] GURMUKHI LETTER LA..GURMUKHI LETTER LLA
+0A35..0A36 ; ALetter # Lo [2] GURMUKHI LETTER VA..GURMUKHI LETTER SHA
+0A38..0A39 ; ALetter # Lo [2] GURMUKHI LETTER SA..GURMUKHI LETTER HA
+0A59..0A5C ; ALetter # Lo [4] GURMUKHI LETTER KHHA..GURMUKHI LETTER RRA
+0A5E ; ALetter # Lo GURMUKHI LETTER FA
+0A72..0A74 ; ALetter # Lo [3] GURMUKHI IRI..GURMUKHI EK ONKAR
+0A85..0A8D ; ALetter # Lo [9] GUJARATI LETTER A..GUJARATI VOWEL CANDRA E
+0A8F..0A91 ; ALetter # Lo [3] GUJARATI LETTER E..GUJARATI VOWEL CANDRA O
+0A93..0AA8 ; ALetter # Lo [22] GUJARATI LETTER O..GUJARATI LETTER NA
+0AAA..0AB0 ; ALetter # Lo [7] GUJARATI LETTER PA..GUJARATI LETTER RA
+0AB2..0AB3 ; ALetter # Lo [2] GUJARATI LETTER LA..GUJARATI LETTER LLA
+0AB5..0AB9 ; ALetter # Lo [5] GUJARATI LETTER VA..GUJARATI LETTER HA
+0ABD ; ALetter # Lo GUJARATI SIGN AVAGRAHA
+0AD0 ; ALetter # Lo GUJARATI OM
+0AE0..0AE1 ; ALetter # Lo [2] GUJARATI LETTER VOCALIC RR..GUJARATI LETTER VOCALIC LL
+0AF9 ; ALetter # Lo GUJARATI LETTER ZHA
+0B05..0B0C ; ALetter # Lo [8] ORIYA LETTER A..ORIYA LETTER VOCALIC L
+0B0F..0B10 ; ALetter # Lo [2] ORIYA LETTER E..ORIYA LETTER AI
+0B13..0B28 ; ALetter # Lo [22] ORIYA LETTER O..ORIYA LETTER NA
+0B2A..0B30 ; ALetter # Lo [7] ORIYA LETTER PA..ORIYA LETTER RA
+0B32..0B33 ; ALetter # Lo [2] ORIYA LETTER LA..ORIYA LETTER LLA
+0B35..0B39 ; ALetter # Lo [5] ORIYA LETTER VA..ORIYA LETTER HA
+0B3D ; ALetter # Lo ORIYA SIGN AVAGRAHA
+0B5C..0B5D ; ALetter # Lo [2] ORIYA LETTER RRA..ORIYA LETTER RHA
+0B5F..0B61 ; ALetter # Lo [3] ORIYA LETTER YYA..ORIYA LETTER VOCALIC LL
+0B71 ; ALetter # Lo ORIYA LETTER WA
+0B83 ; ALetter # Lo TAMIL SIGN VISARGA
+0B85..0B8A ; ALetter # Lo [6] TAMIL LETTER A..TAMIL LETTER UU
+0B8E..0B90 ; ALetter # Lo [3] TAMIL LETTER E..TAMIL LETTER AI
+0B92..0B95 ; ALetter # Lo [4] TAMIL LETTER O..TAMIL LETTER KA
+0B99..0B9A ; ALetter # Lo [2] TAMIL LETTER NGA..TAMIL LETTER CA
+0B9C ; ALetter # Lo TAMIL LETTER JA
+0B9E..0B9F ; ALetter # Lo [2] TAMIL LETTER NYA..TAMIL LETTER TTA
+0BA3..0BA4 ; ALetter # Lo [2] TAMIL LETTER NNA..TAMIL LETTER TA
+0BA8..0BAA ; ALetter # Lo [3] TAMIL LETTER NA..TAMIL LETTER PA
+0BAE..0BB9 ; ALetter # Lo [12] TAMIL LETTER MA..TAMIL LETTER HA
+0BD0 ; ALetter # Lo TAMIL OM
+0C05..0C0C ; ALetter # Lo [8] TELUGU LETTER A..TELUGU LETTER VOCALIC L
+0C0E..0C10 ; ALetter # Lo [3] TELUGU LETTER E..TELUGU LETTER AI
+0C12..0C28 ; ALetter # Lo [23] TELUGU LETTER O..TELUGU LETTER NA
+0C2A..0C39 ; ALetter # Lo [16] TELUGU LETTER PA..TELUGU LETTER HA
+0C3D ; ALetter # Lo TELUGU SIGN AVAGRAHA
+0C58..0C5A ; ALetter # Lo [3] TELUGU LETTER TSA..TELUGU LETTER RRRA
+0C60..0C61 ; ALetter # Lo [2] TELUGU LETTER VOCALIC RR..TELUGU LETTER VOCALIC LL
+0C80 ; ALetter # Lo KANNADA SIGN SPACING CANDRABINDU
+0C85..0C8C ; ALetter # Lo [8] KANNADA LETTER A..KANNADA LETTER VOCALIC L
+0C8E..0C90 ; ALetter # Lo [3] KANNADA LETTER E..KANNADA LETTER AI
+0C92..0CA8 ; ALetter # Lo [23] KANNADA LETTER O..KANNADA LETTER NA
+0CAA..0CB3 ; ALetter # Lo [10] KANNADA LETTER PA..KANNADA LETTER LLA
+0CB5..0CB9 ; ALetter # Lo [5] KANNADA LETTER VA..KANNADA LETTER HA
+0CBD ; ALetter # Lo KANNADA SIGN AVAGRAHA
+0CDE ; ALetter # Lo KANNADA LETTER FA
+0CE0..0CE1 ; ALetter # Lo [2] KANNADA LETTER VOCALIC RR..KANNADA LETTER VOCALIC LL
+0CF1..0CF2 ; ALetter # Lo [2] KANNADA SIGN JIHVAMULIYA..KANNADA SIGN UPADHMANIYA
+0D05..0D0C ; ALetter # Lo [8] MALAYALAM LETTER A..MALAYALAM LETTER VOCALIC L
+0D0E..0D10 ; ALetter # Lo [3] MALAYALAM LETTER E..MALAYALAM LETTER AI
+0D12..0D3A ; ALetter # Lo [41] MALAYALAM LETTER O..MALAYALAM LETTER TTTA
+0D3D ; ALetter # Lo MALAYALAM SIGN AVAGRAHA
+0D4E ; ALetter # Lo MALAYALAM LETTER DOT REPH
+0D54..0D56 ; ALetter # Lo [3] MALAYALAM LETTER CHILLU M..MALAYALAM LETTER CHILLU LLL
+0D5F..0D61 ; ALetter # Lo [3] MALAYALAM LETTER ARCHAIC II..MALAYALAM LETTER VOCALIC LL
+0D7A..0D7F ; ALetter # Lo [6] MALAYALAM LETTER CHILLU NN..MALAYALAM LETTER CHILLU K
+0D85..0D96 ; ALetter # Lo [18] SINHALA LETTER AYANNA..SINHALA LETTER AUYANNA
+0D9A..0DB1 ; ALetter # Lo [24] SINHALA LETTER ALPAPRAANA KAYANNA..SINHALA LETTER DANTAJA NAYANNA
+0DB3..0DBB ; ALetter # Lo [9] SINHALA LETTER SANYAKA DAYANNA..SINHALA LETTER RAYANNA
+0DBD ; ALetter # Lo SINHALA LETTER DANTAJA LAYANNA
+0DC0..0DC6 ; ALetter # Lo [7] SINHALA LETTER VAYANNA..SINHALA LETTER FAYANNA
+0F00 ; ALetter # Lo TIBETAN SYLLABLE OM
+0F40..0F47 ; ALetter # Lo [8] TIBETAN LETTER KA..TIBETAN LETTER JA
+0F49..0F6C ; ALetter # Lo [36] TIBETAN LETTER NYA..TIBETAN LETTER RRA
+0F88..0F8C ; ALetter # Lo [5] TIBETAN SIGN LCE TSA CAN..TIBETAN SIGN INVERTED MCHU CAN
+10A0..10C5 ; ALetter # L& [38] GEORGIAN CAPITAL LETTER AN..GEORGIAN CAPITAL LETTER HOE
+10C7 ; ALetter # L& GEORGIAN CAPITAL LETTER YN
+10CD ; ALetter # L& GEORGIAN CAPITAL LETTER AEN
+10D0..10FA ; ALetter # Lo [43] GEORGIAN LETTER AN..GEORGIAN LETTER AIN
+10FC ; ALetter # Lm MODIFIER LETTER GEORGIAN NAR
+10FD..1248 ; ALetter # Lo [332] GEORGIAN LETTER AEN..ETHIOPIC SYLLABLE QWA
+124A..124D ; ALetter # Lo [4] ETHIOPIC SYLLABLE QWI..ETHIOPIC SYLLABLE QWE
+1250..1256 ; ALetter # Lo [7] ETHIOPIC SYLLABLE QHA..ETHIOPIC SYLLABLE QHO
+1258 ; ALetter # Lo ETHIOPIC SYLLABLE QHWA
+125A..125D ; ALetter # Lo [4] ETHIOPIC SYLLABLE QHWI..ETHIOPIC SYLLABLE QHWE
+1260..1288 ; ALetter # Lo [41] ETHIOPIC SYLLABLE BA..ETHIOPIC SYLLABLE XWA
+128A..128D ; ALetter # Lo [4] ETHIOPIC SYLLABLE XWI..ETHIOPIC SYLLABLE XWE
+1290..12B0 ; ALetter # Lo [33] ETHIOPIC SYLLABLE NA..ETHIOPIC SYLLABLE KWA
+12B2..12B5 ; ALetter # Lo [4] ETHIOPIC SYLLABLE KWI..ETHIOPIC SYLLABLE KWE
+12B8..12BE ; ALetter # Lo [7] ETHIOPIC SYLLABLE KXA..ETHIOPIC SYLLABLE KXO
+12C0 ; ALetter # Lo ETHIOPIC SYLLABLE KXWA
+12C2..12C5 ; ALetter # Lo [4] ETHIOPIC SYLLABLE KXWI..ETHIOPIC SYLLABLE KXWE
+12C8..12D6 ; ALetter # Lo [15] ETHIOPIC SYLLABLE WA..ETHIOPIC SYLLABLE PHARYNGEAL O
+12D8..1310 ; ALetter # Lo [57] ETHIOPIC SYLLABLE ZA..ETHIOPIC SYLLABLE GWA
+1312..1315 ; ALetter # Lo [4] ETHIOPIC SYLLABLE GWI..ETHIOPIC SYLLABLE GWE
+1318..135A ; ALetter # Lo [67] ETHIOPIC SYLLABLE GGA..ETHIOPIC SYLLABLE FYA
+1380..138F ; ALetter # Lo [16] ETHIOPIC SYLLABLE SEBATBEIT MWA..ETHIOPIC SYLLABLE PWE
+13A0..13F5 ; ALetter # L& [86] CHEROKEE LETTER A..CHEROKEE LETTER MV
+13F8..13FD ; ALetter # L& [6] CHEROKEE SMALL LETTER YE..CHEROKEE SMALL LETTER MV
+1401..166C ; ALetter # Lo [620] CANADIAN SYLLABICS E..CANADIAN SYLLABICS CARRIER TTSA
+166F..167F ; ALetter # Lo [17] CANADIAN SYLLABICS QAI..CANADIAN SYLLABICS BLACKFOOT W
+1681..169A ; ALetter # Lo [26] OGHAM LETTER BEITH..OGHAM LETTER PEITH
+16A0..16EA ; ALetter # Lo [75] RUNIC LETTER FEHU FEOH FE F..RUNIC LETTER X
+16EE..16F0 ; ALetter # Nl [3] RUNIC ARLAUG SYMBOL..RUNIC BELGTHOR SYMBOL
+16F1..16F8 ; ALetter # Lo [8] RUNIC LETTER K..RUNIC LETTER FRANKS CASKET AESC
+1700..170C ; ALetter # Lo [13] TAGALOG LETTER A..TAGALOG LETTER YA
+170E..1711 ; ALetter # Lo [4] TAGALOG LETTER LA..TAGALOG LETTER HA
+1720..1731 ; ALetter # Lo [18] HANUNOO LETTER A..HANUNOO LETTER HA
+1740..1751 ; ALetter # Lo [18] BUHID LETTER A..BUHID LETTER HA
+1760..176C ; ALetter # Lo [13] TAGBANWA LETTER A..TAGBANWA LETTER YA
+176E..1770 ; ALetter # Lo [3] TAGBANWA LETTER LA..TAGBANWA LETTER SA
+1820..1842 ; ALetter # Lo [35] MONGOLIAN LETTER A..MONGOLIAN LETTER CHI
+1843 ; ALetter # Lm MONGOLIAN LETTER TODO LONG VOWEL SIGN
+1844..1877 ; ALetter # Lo [52] MONGOLIAN LETTER TODO E..MONGOLIAN LETTER MANCHU ZHA
+1880..1884 ; ALetter # Lo [5] MONGOLIAN LETTER ALI GALI ANUSVARA ONE..MONGOLIAN LETTER ALI GALI INVERTED UBADAMA
+1887..18A8 ; ALetter # Lo [34] MONGOLIAN LETTER ALI GALI A..MONGOLIAN LETTER MANCHU ALI GALI BHA
+18AA ; ALetter # Lo MONGOLIAN LETTER MANCHU ALI GALI LHA
+18B0..18F5 ; ALetter # Lo [70] CANADIAN SYLLABICS OY..CANADIAN SYLLABICS CARRIER DENTAL S
+1900..191E ; ALetter # Lo [31] LIMBU VOWEL-CARRIER LETTER..LIMBU LETTER TRA
+1A00..1A16 ; ALetter # Lo [23] BUGINESE LETTER KA..BUGINESE LETTER HA
+1B05..1B33 ; ALetter # Lo [47] BALINESE LETTER AKARA..BALINESE LETTER HA
+1B45..1B4B ; ALetter # Lo [7] BALINESE LETTER KAF SASAK..BALINESE LETTER ASYURA SASAK
+1B83..1BA0 ; ALetter # Lo [30] SUNDANESE LETTER A..SUNDANESE LETTER HA
+1BAE..1BAF ; ALetter # Lo [2] SUNDANESE LETTER KHA..SUNDANESE LETTER SYA
+1BBA..1BE5 ; ALetter # Lo [44] SUNDANESE AVAGRAHA..BATAK LETTER U
+1C00..1C23 ; ALetter # Lo [36] LEPCHA LETTER KA..LEPCHA LETTER A
+1C4D..1C4F ; ALetter # Lo [3] LEPCHA LETTER TTA..LEPCHA LETTER DDA
+1C5A..1C77 ; ALetter # Lo [30] OL CHIKI LETTER LA..OL CHIKI LETTER OH
+1C78..1C7D ; ALetter # Lm [6] OL CHIKI MU TTUDDAG..OL CHIKI AHAD
+1C80..1C88 ; ALetter # L& [9] CYRILLIC SMALL LETTER ROUNDED VE..CYRILLIC SMALL LETTER UNBLENDED UK
+1CE9..1CEC ; ALetter # Lo [4] VEDIC SIGN ANUSVARA ANTARGOMUKHA..VEDIC SIGN ANUSVARA VAMAGOMUKHA WITH TAIL
+1CEE..1CF1 ; ALetter # Lo [4] VEDIC SIGN HEXIFORM LONG ANUSVARA..VEDIC SIGN ANUSVARA UBHAYATO MUKHA
+1CF5..1CF6 ; ALetter # Lo [2] VEDIC SIGN JIHVAMULIYA..VEDIC SIGN UPADHMANIYA
+1D00..1D2B ; ALetter # L& [44] LATIN LETTER SMALL CAPITAL A..CYRILLIC LETTER SMALL CAPITAL EL
+1D2C..1D6A ; ALetter # Lm [63] MODIFIER LETTER CAPITAL A..GREEK SUBSCRIPT SMALL LETTER CHI
+1D6B..1D77 ; ALetter # L& [13] LATIN SMALL LETTER UE..LATIN SMALL LETTER TURNED G
+1D78 ; ALetter # Lm MODIFIER LETTER CYRILLIC EN
+1D79..1D9A ; ALetter # L& [34] LATIN SMALL LETTER INSULAR G..LATIN SMALL LETTER EZH WITH RETROFLEX HOOK
+1D9B..1DBF ; ALetter # Lm [37] MODIFIER LETTER SMALL TURNED ALPHA..MODIFIER LETTER SMALL THETA
+1E00..1F15 ; ALetter # L& [278] LATIN CAPITAL LETTER A WITH RING BELOW..GREEK SMALL LETTER EPSILON WITH DASIA AND OXIA
+1F18..1F1D ; ALetter # L& [6] GREEK CAPITAL LETTER EPSILON WITH PSILI..GREEK CAPITAL LETTER EPSILON WITH DASIA AND OXIA
+1F20..1F45 ; ALetter # L& [38] GREEK SMALL LETTER ETA WITH PSILI..GREEK SMALL LETTER OMICRON WITH DASIA AND OXIA
+1F48..1F4D ; ALetter # L& [6] GREEK CAPITAL LETTER OMICRON WITH PSILI..GREEK CAPITAL LETTER OMICRON WITH DASIA AND OXIA
+1F50..1F57 ; ALetter # L& [8] GREEK SMALL LETTER UPSILON WITH PSILI..GREEK SMALL LETTER UPSILON WITH DASIA AND PERISPOMENI
+1F59 ; ALetter # L& GREEK CAPITAL LETTER UPSILON WITH DASIA
+1F5B ; ALetter # L& GREEK CAPITAL LETTER UPSILON WITH DASIA AND VARIA
+1F5D ; ALetter # L& GREEK CAPITAL LETTER UPSILON WITH DASIA AND OXIA
+1F5F..1F7D ; ALetter # L& [31] GREEK CAPITAL LETTER UPSILON WITH DASIA AND PERISPOMENI..GREEK SMALL LETTER OMEGA WITH OXIA
+1F80..1FB4 ; ALetter # L& [53] GREEK SMALL LETTER ALPHA WITH PSILI AND YPOGEGRAMMENI..GREEK SMALL LETTER ALPHA WITH OXIA AND YPOGEGRAMMENI
+1FB6..1FBC ; ALetter # L& [7] GREEK SMALL LETTER ALPHA WITH PERISPOMENI..GREEK CAPITAL LETTER ALPHA WITH PROSGEGRAMMENI
+1FBE ; ALetter # L& GREEK PROSGEGRAMMENI
+1FC2..1FC4 ; ALetter # L& [3] GREEK SMALL LETTER ETA WITH VARIA AND YPOGEGRAMMENI..GREEK SMALL LETTER ETA WITH OXIA AND YPOGEGRAMMENI
+1FC6..1FCC ; ALetter # L& [7] GREEK SMALL LETTER ETA WITH PERISPOMENI..GREEK CAPITAL LETTER ETA WITH PROSGEGRAMMENI
+1FD0..1FD3 ; ALetter # L& [4] GREEK SMALL LETTER IOTA WITH VRACHY..GREEK SMALL LETTER IOTA WITH DIALYTIKA AND OXIA
+1FD6..1FDB ; ALetter # L& [6] GREEK SMALL LETTER IOTA WITH PERISPOMENI..GREEK CAPITAL LETTER IOTA WITH OXIA
+1FE0..1FEC ; ALetter # L& [13] GREEK SMALL LETTER UPSILON WITH VRACHY..GREEK CAPITAL LETTER RHO WITH DASIA
+1FF2..1FF4 ; ALetter # L& [3] GREEK SMALL LETTER OMEGA WITH VARIA AND YPOGEGRAMMENI..GREEK SMALL LETTER OMEGA WITH OXIA AND YPOGEGRAMMENI
+1FF6..1FFC ; ALetter # L& [7] GREEK SMALL LETTER OMEGA WITH PERISPOMENI..GREEK CAPITAL LETTER OMEGA WITH PROSGEGRAMMENI
+2071 ; ALetter # Lm SUPERSCRIPT LATIN SMALL LETTER I
+207F ; ALetter # Lm SUPERSCRIPT LATIN SMALL LETTER N
+2090..209C ; ALetter # Lm [13] LATIN SUBSCRIPT SMALL LETTER A..LATIN SUBSCRIPT SMALL LETTER T
+2102 ; ALetter # L& DOUBLE-STRUCK CAPITAL C
+2107 ; ALetter # L& EULER CONSTANT
+210A..2113 ; ALetter # L& [10] SCRIPT SMALL G..SCRIPT SMALL L
+2115 ; ALetter # L& DOUBLE-STRUCK CAPITAL N
+2119..211D ; ALetter # L& [5] DOUBLE-STRUCK CAPITAL P..DOUBLE-STRUCK CAPITAL R
+2124 ; ALetter # L& DOUBLE-STRUCK CAPITAL Z
+2126 ; ALetter # L& OHM SIGN
+2128 ; ALetter # L& BLACK-LETTER CAPITAL Z
+212A..212D ; ALetter # L& [4] KELVIN SIGN..BLACK-LETTER CAPITAL C
+212F..2134 ; ALetter # L& [6] SCRIPT SMALL E..SCRIPT SMALL O
+2135..2138 ; ALetter # Lo [4] ALEF SYMBOL..DALET SYMBOL
+2139 ; ALetter # L& INFORMATION SOURCE
+213C..213F ; ALetter # L& [4] DOUBLE-STRUCK SMALL PI..DOUBLE-STRUCK CAPITAL PI
+2145..2149 ; ALetter # L& [5] DOUBLE-STRUCK ITALIC CAPITAL D..DOUBLE-STRUCK ITALIC SMALL J
+214E ; ALetter # L& TURNED SMALL F
+2160..2182 ; ALetter # Nl [35] ROMAN NUMERAL ONE..ROMAN NUMERAL TEN THOUSAND
+2183..2184 ; ALetter # L& [2] ROMAN NUMERAL REVERSED ONE HUNDRED..LATIN SMALL LETTER REVERSED C
+2185..2188 ; ALetter # Nl [4] ROMAN NUMERAL SIX LATE FORM..ROMAN NUMERAL ONE HUNDRED THOUSAND
+24B6..24E9 ; ALetter # So [52] CIRCLED LATIN CAPITAL LETTER A..CIRCLED LATIN SMALL LETTER Z
+2C00..2C2E ; ALetter # L& [47] GLAGOLITIC CAPITAL LETTER AZU..GLAGOLITIC CAPITAL LETTER LATINATE MYSLITE
+2C30..2C5E ; ALetter # L& [47] GLAGOLITIC SMALL LETTER AZU..GLAGOLITIC SMALL LETTER LATINATE MYSLITE
+2C60..2C7B ; ALetter # L& [28] LATIN CAPITAL LETTER L WITH DOUBLE BAR..LATIN LETTER SMALL CAPITAL TURNED E
+2C7C..2C7D ; ALetter # Lm [2] LATIN SUBSCRIPT SMALL LETTER J..MODIFIER LETTER CAPITAL V
+2C7E..2CE4 ; ALetter # L& [103] LATIN CAPITAL LETTER S WITH SWASH TAIL..COPTIC SYMBOL KAI
+2CEB..2CEE ; ALetter # L& [4] COPTIC CAPITAL LETTER CRYPTOGRAMMIC SHEI..COPTIC SMALL LETTER CRYPTOGRAMMIC GANGIA
+2CF2..2CF3 ; ALetter # L& [2] COPTIC CAPITAL LETTER BOHAIRIC KHEI..COPTIC SMALL LETTER BOHAIRIC KHEI
+2D00..2D25 ; ALetter # L& [38] GEORGIAN SMALL LETTER AN..GEORGIAN SMALL LETTER HOE
+2D27 ; ALetter # L& GEORGIAN SMALL LETTER YN
+2D2D ; ALetter # L& GEORGIAN SMALL LETTER AEN
+2D30..2D67 ; ALetter # Lo [56] TIFINAGH LETTER YA..TIFINAGH LETTER YO
+2D6F ; ALetter # Lm TIFINAGH MODIFIER LETTER LABIALIZATION MARK
+2D80..2D96 ; ALetter # Lo [23] ETHIOPIC SYLLABLE LOA..ETHIOPIC SYLLABLE GGWE
+2DA0..2DA6 ; ALetter # Lo [7] ETHIOPIC SYLLABLE SSA..ETHIOPIC SYLLABLE SSO
+2DA8..2DAE ; ALetter # Lo [7] ETHIOPIC SYLLABLE CCA..ETHIOPIC SYLLABLE CCO
+2DB0..2DB6 ; ALetter # Lo [7] ETHIOPIC SYLLABLE ZZA..ETHIOPIC SYLLABLE ZZO
+2DB8..2DBE ; ALetter # Lo [7] ETHIOPIC SYLLABLE CCHA..ETHIOPIC SYLLABLE CCHO
+2DC0..2DC6 ; ALetter # Lo [7] ETHIOPIC SYLLABLE QYA..ETHIOPIC SYLLABLE QYO
+2DC8..2DCE ; ALetter # Lo [7] ETHIOPIC SYLLABLE KYA..ETHIOPIC SYLLABLE KYO
+2DD0..2DD6 ; ALetter # Lo [7] ETHIOPIC SYLLABLE XYA..ETHIOPIC SYLLABLE XYO
+2DD8..2DDE ; ALetter # Lo [7] ETHIOPIC SYLLABLE GYA..ETHIOPIC SYLLABLE GYO
+2E2F ; ALetter # Lm VERTICAL TILDE
+3005 ; ALetter # Lm IDEOGRAPHIC ITERATION MARK
+303B ; ALetter # Lm VERTICAL IDEOGRAPHIC ITERATION MARK
+303C ; ALetter # Lo MASU MARK
+3105..312D ; ALetter # Lo [41] BOPOMOFO LETTER B..BOPOMOFO LETTER IH
+3131..318E ; ALetter # Lo [94] HANGUL LETTER KIYEOK..HANGUL LETTER ARAEAE
+31A0..31BA ; ALetter # Lo [27] BOPOMOFO LETTER BU..BOPOMOFO LETTER ZY
+A000..A014 ; ALetter # Lo [21] YI SYLLABLE IT..YI SYLLABLE E
+A015 ; ALetter # Lm YI SYLLABLE WU
+A016..A48C ; ALetter # Lo [1143] YI SYLLABLE BIT..YI SYLLABLE YYR
+A4D0..A4F7 ; ALetter # Lo [40] LISU LETTER BA..LISU LETTER OE
+A4F8..A4FD ; ALetter # Lm [6] LISU LETTER TONE MYA TI..LISU LETTER TONE MYA JEU
+A500..A60B ; ALetter # Lo [268] VAI SYLLABLE EE..VAI SYLLABLE NG
+A60C ; ALetter # Lm VAI SYLLABLE LENGTHENER
+A610..A61F ; ALetter # Lo [16] VAI SYLLABLE NDOLE FA..VAI SYMBOL JONG
+A62A..A62B ; ALetter # Lo [2] VAI SYLLABLE NDOLE MA..VAI SYLLABLE NDOLE DO
+A640..A66D ; ALetter # L& [46] CYRILLIC CAPITAL LETTER ZEMLYA..CYRILLIC SMALL LETTER DOUBLE MONOCULAR O
+A66E ; ALetter # Lo CYRILLIC LETTER MULTIOCULAR O
+A67F ; ALetter # Lm CYRILLIC PAYEROK
+A680..A69B ; ALetter # L& [28] CYRILLIC CAPITAL LETTER DWE..CYRILLIC SMALL LETTER CROSSED O
+A69C..A69D ; ALetter # Lm [2] MODIFIER LETTER CYRILLIC HARD SIGN..MODIFIER LETTER CYRILLIC SOFT SIGN
+A6A0..A6E5 ; ALetter # Lo [70] BAMUM LETTER A..BAMUM LETTER KI
+A6E6..A6EF ; ALetter # Nl [10] BAMUM LETTER MO..BAMUM LETTER KOGHOM
+A717..A71F ; ALetter # Lm [9] MODIFIER LETTER DOT VERTICAL BAR..MODIFIER LETTER LOW INVERTED EXCLAMATION MARK
+A722..A76F ; ALetter # L& [78] LATIN CAPITAL LETTER EGYPTOLOGICAL ALEF..LATIN SMALL LETTER CON
+A770 ; ALetter # Lm MODIFIER LETTER US
+A771..A787 ; ALetter # L& [23] LATIN SMALL LETTER DUM..LATIN SMALL LETTER INSULAR T
+A788 ; ALetter # Lm MODIFIER LETTER LOW CIRCUMFLEX ACCENT
+A78B..A78E ; ALetter # L& [4] LATIN CAPITAL LETTER SALTILLO..LATIN SMALL LETTER L WITH RETROFLEX HOOK AND BELT
+A78F ; ALetter # Lo LATIN LETTER SINOLOGICAL DOT
+A790..A7AE ; ALetter # L& [31] LATIN CAPITAL LETTER N WITH DESCENDER..LATIN CAPITAL LETTER SMALL CAPITAL I
+A7B0..A7B7 ; ALetter # L& [8] LATIN CAPITAL LETTER TURNED K..LATIN SMALL LETTER OMEGA
+A7F7 ; ALetter # Lo LATIN EPIGRAPHIC LETTER SIDEWAYS I
+A7F8..A7F9 ; ALetter # Lm [2] MODIFIER LETTER CAPITAL H WITH STROKE..MODIFIER LETTER SMALL LIGATURE OE
+A7FA ; ALetter # L& LATIN LETTER SMALL CAPITAL TURNED M
+A7FB..A801 ; ALetter # Lo [7] LATIN EPIGRAPHIC LETTER REVERSED F..SYLOTI NAGRI LETTER I
+A803..A805 ; ALetter # Lo [3] SYLOTI NAGRI LETTER U..SYLOTI NAGRI LETTER O
+A807..A80A ; ALetter # Lo [4] SYLOTI NAGRI LETTER KO..SYLOTI NAGRI LETTER GHO
+A80C..A822 ; ALetter # Lo [23] SYLOTI NAGRI LETTER CO..SYLOTI NAGRI LETTER HO
+A840..A873 ; ALetter # Lo [52] PHAGS-PA LETTER KA..PHAGS-PA LETTER CANDRABINDU
+A882..A8B3 ; ALetter # Lo [50] SAURASHTRA LETTER A..SAURASHTRA LETTER LLA
+A8F2..A8F7 ; ALetter # Lo [6] DEVANAGARI SIGN SPACING CANDRABINDU..DEVANAGARI SIGN CANDRABINDU AVAGRAHA
+A8FB ; ALetter # Lo DEVANAGARI HEADSTROKE
+A8FD ; ALetter # Lo DEVANAGARI JAIN OM
+A90A..A925 ; ALetter # Lo [28] KAYAH LI LETTER KA..KAYAH LI LETTER OO
+A930..A946 ; ALetter # Lo [23] REJANG LETTER KA..REJANG LETTER A
+A960..A97C ; ALetter # Lo [29] HANGUL CHOSEONG TIKEUT-MIEUM..HANGUL CHOSEONG SSANGYEORINHIEUH
+A984..A9B2 ; ALetter # Lo [47] JAVANESE LETTER A..JAVANESE LETTER HA
+A9CF ; ALetter # Lm JAVANESE PANGRANGKEP
+AA00..AA28 ; ALetter # Lo [41] CHAM LETTER A..CHAM LETTER HA
+AA40..AA42 ; ALetter # Lo [3] CHAM LETTER FINAL K..CHAM LETTER FINAL NG
+AA44..AA4B ; ALetter # Lo [8] CHAM LETTER FINAL CH..CHAM LETTER FINAL SS
+AAE0..AAEA ; ALetter # Lo [11] MEETEI MAYEK LETTER E..MEETEI MAYEK LETTER SSA
+AAF2 ; ALetter # Lo MEETEI MAYEK ANJI
+AAF3..AAF4 ; ALetter # Lm [2] MEETEI MAYEK SYLLABLE REPETITION MARK..MEETEI MAYEK WORD REPETITION MARK
+AB01..AB06 ; ALetter # Lo [6] ETHIOPIC SYLLABLE TTHU..ETHIOPIC SYLLABLE TTHO
+AB09..AB0E ; ALetter # Lo [6] ETHIOPIC SYLLABLE DDHU..ETHIOPIC SYLLABLE DDHO
+AB11..AB16 ; ALetter # Lo [6] ETHIOPIC SYLLABLE DZU..ETHIOPIC SYLLABLE DZO
+AB20..AB26 ; ALetter # Lo [7] ETHIOPIC SYLLABLE CCHHA..ETHIOPIC SYLLABLE CCHHO
+AB28..AB2E ; ALetter # Lo [7] ETHIOPIC SYLLABLE BBA..ETHIOPIC SYLLABLE BBO
+AB30..AB5A ; ALetter # L& [43] LATIN SMALL LETTER BARRED ALPHA..LATIN SMALL LETTER Y WITH SHORT RIGHT LEG
+AB5C..AB5F ; ALetter # Lm [4] MODIFIER LETTER SMALL HENG..MODIFIER LETTER SMALL U WITH LEFT HOOK
+AB60..AB65 ; ALetter # L& [6] LATIN SMALL LETTER SAKHA YAT..GREEK LETTER SMALL CAPITAL OMEGA
+AB70..ABBF ; ALetter # L& [80] CHEROKEE SMALL LETTER A..CHEROKEE SMALL LETTER YA
+ABC0..ABE2 ; ALetter # Lo [35] MEETEI MAYEK LETTER KOK..MEETEI MAYEK LETTER I LONSUM
+AC00..D7A3 ; ALetter # Lo [11172] HANGUL SYLLABLE GA..HANGUL SYLLABLE HIH
+D7B0..D7C6 ; ALetter # Lo [23] HANGUL JUNGSEONG O-YEO..HANGUL JUNGSEONG ARAEA-E
+D7CB..D7FB ; ALetter # Lo [49] HANGUL JONGSEONG NIEUN-RIEUL..HANGUL JONGSEONG PHIEUPH-THIEUTH
+FB00..FB06 ; ALetter # L& [7] LATIN SMALL LIGATURE FF..LATIN SMALL LIGATURE ST
+FB13..FB17 ; ALetter # L& [5] ARMENIAN SMALL LIGATURE MEN NOW..ARMENIAN SMALL LIGATURE MEN XEH
+FB50..FBB1 ; ALetter # Lo [98] ARABIC LETTER ALEF WASLA ISOLATED FORM..ARABIC LETTER YEH BARREE WITH HAMZA ABOVE FINAL FORM
+FBD3..FD3D ; ALetter # Lo [363] ARABIC LETTER NG ISOLATED FORM..ARABIC LIGATURE ALEF WITH FATHATAN ISOLATED FORM
+FD50..FD8F ; ALetter # Lo [64] ARABIC LIGATURE TEH WITH JEEM WITH MEEM INITIAL FORM..ARABIC LIGATURE MEEM WITH KHAH WITH MEEM INITIAL FORM
+FD92..FDC7 ; ALetter # Lo [54] ARABIC LIGATURE MEEM WITH JEEM WITH KHAH INITIAL FORM..ARABIC LIGATURE NOON WITH JEEM WITH YEH FINAL FORM
+FDF0..FDFB ; ALetter # Lo [12] ARABIC LIGATURE SALLA USED AS KORANIC STOP SIGN ISOLATED FORM..ARABIC LIGATURE JALLAJALALOUHOU
+FE70..FE74 ; ALetter # Lo [5] ARABIC FATHATAN ISOLATED FORM..ARABIC KASRATAN ISOLATED FORM
+FE76..FEFC ; ALetter # Lo [135] ARABIC FATHA ISOLATED FORM..ARABIC LIGATURE LAM WITH ALEF FINAL FORM
+FF21..FF3A ; ALetter # L& [26] FULLWIDTH LATIN CAPITAL LETTER A..FULLWIDTH LATIN CAPITAL LETTER Z
+FF41..FF5A ; ALetter # L& [26] FULLWIDTH LATIN SMALL LETTER A..FULLWIDTH LATIN SMALL LETTER Z
+FFA0..FFBE ; ALetter # Lo [31] HALFWIDTH HANGUL FILLER..HALFWIDTH HANGUL LETTER HIEUH
+FFC2..FFC7 ; ALetter # Lo [6] HALFWIDTH HANGUL LETTER A..HALFWIDTH HANGUL LETTER E
+FFCA..FFCF ; ALetter # Lo [6] HALFWIDTH HANGUL LETTER YEO..HALFWIDTH HANGUL LETTER OE
+FFD2..FFD7 ; ALetter # Lo [6] HALFWIDTH HANGUL LETTER YO..HALFWIDTH HANGUL LETTER YU
+FFDA..FFDC ; ALetter # Lo [3] HALFWIDTH HANGUL LETTER EU..HALFWIDTH HANGUL LETTER I
+10000..1000B ; ALetter # Lo [12] LINEAR B SYLLABLE B008 A..LINEAR B SYLLABLE B046 JE
+1000D..10026 ; ALetter # Lo [26] LINEAR B SYLLABLE B036 JO..LINEAR B SYLLABLE B032 QO
+10028..1003A ; ALetter # Lo [19] LINEAR B SYLLABLE B060 RA..LINEAR B SYLLABLE B042 WO
+1003C..1003D ; ALetter # Lo [2] LINEAR B SYLLABLE B017 ZA..LINEAR B SYLLABLE B074 ZE
+1003F..1004D ; ALetter # Lo [15] LINEAR B SYLLABLE B020 ZO..LINEAR B SYLLABLE B091 TWO
+10050..1005D ; ALetter # Lo [14] LINEAR B SYMBOL B018..LINEAR B SYMBOL B089
+10080..100FA ; ALetter # Lo [123] LINEAR B IDEOGRAM B100 MAN..LINEAR B IDEOGRAM VESSEL B305
+10140..10174 ; ALetter # Nl [53] GREEK ACROPHONIC ATTIC ONE QUARTER..GREEK ACROPHONIC STRATIAN FIFTY MNAS
+10280..1029C ; ALetter # Lo [29] LYCIAN LETTER A..LYCIAN LETTER X
+102A0..102D0 ; ALetter # Lo [49] CARIAN LETTER A..CARIAN LETTER UUU3
+10300..1031F ; ALetter # Lo [32] OLD ITALIC LETTER A..OLD ITALIC LETTER ESS
+10330..10340 ; ALetter # Lo [17] GOTHIC LETTER AHSA..GOTHIC LETTER PAIRTHRA
+10341 ; ALetter # Nl GOTHIC LETTER NINETY
+10342..10349 ; ALetter # Lo [8] GOTHIC LETTER RAIDA..GOTHIC LETTER OTHAL
+1034A ; ALetter # Nl GOTHIC LETTER NINE HUNDRED
+10350..10375 ; ALetter # Lo [38] OLD PERMIC LETTER AN..OLD PERMIC LETTER IA
+10380..1039D ; ALetter # Lo [30] UGARITIC LETTER ALPA..UGARITIC LETTER SSU
+103A0..103C3 ; ALetter # Lo [36] OLD PERSIAN SIGN A..OLD PERSIAN SIGN HA
+103C8..103CF ; ALetter # Lo [8] OLD PERSIAN SIGN AURAMAZDAA..OLD PERSIAN SIGN BUUMISH
+103D1..103D5 ; ALetter # Nl [5] OLD PERSIAN NUMBER ONE..OLD PERSIAN NUMBER HUNDRED
+10400..1044F ; ALetter # L& [80] DESERET CAPITAL LETTER LONG I..DESERET SMALL LETTER EW
+10450..1049D ; ALetter # Lo [78] SHAVIAN LETTER PEEP..OSMANYA LETTER OO
+104B0..104D3 ; ALetter # L& [36] OSAGE CAPITAL LETTER A..OSAGE CAPITAL LETTER ZHA
+104D8..104FB ; ALetter # L& [36] OSAGE SMALL LETTER A..OSAGE SMALL LETTER ZHA
+10500..10527 ; ALetter # Lo [40] ELBASAN LETTER A..ELBASAN LETTER KHE
+10530..10563 ; ALetter # Lo [52] CAUCASIAN ALBANIAN LETTER ALT..CAUCASIAN ALBANIAN LETTER KIW
+10600..10736 ; ALetter # Lo [311] LINEAR A SIGN AB001..LINEAR A SIGN A664
+10740..10755 ; ALetter # Lo [22] LINEAR A SIGN A701 A..LINEAR A SIGN A732 JE
+10760..10767 ; ALetter # Lo [8] LINEAR A SIGN A800..LINEAR A SIGN A807
+10800..10805 ; ALetter # Lo [6] CYPRIOT SYLLABLE A..CYPRIOT SYLLABLE JA
+10808 ; ALetter # Lo CYPRIOT SYLLABLE JO
+1080A..10835 ; ALetter # Lo [44] CYPRIOT SYLLABLE KA..CYPRIOT SYLLABLE WO
+10837..10838 ; ALetter # Lo [2] CYPRIOT SYLLABLE XA..CYPRIOT SYLLABLE XE
+1083C ; ALetter # Lo CYPRIOT SYLLABLE ZA
+1083F..10855 ; ALetter # Lo [23] CYPRIOT SYLLABLE ZO..IMPERIAL ARAMAIC LETTER TAW
+10860..10876 ; ALetter # Lo [23] PALMYRENE LETTER ALEPH..PALMYRENE LETTER TAW
+10880..1089E ; ALetter # Lo [31] NABATAEAN LETTER FINAL ALEPH..NABATAEAN LETTER TAW
+108E0..108F2 ; ALetter # Lo [19] HATRAN LETTER ALEPH..HATRAN LETTER QOPH
+108F4..108F5 ; ALetter # Lo [2] HATRAN LETTER SHIN..HATRAN LETTER TAW
+10900..10915 ; ALetter # Lo [22] PHOENICIAN LETTER ALF..PHOENICIAN LETTER TAU
+10920..10939 ; ALetter # Lo [26] LYDIAN LETTER A..LYDIAN LETTER C
+10980..109B7 ; ALetter # Lo [56] MEROITIC HIEROGLYPHIC LETTER A..MEROITIC CURSIVE LETTER DA
+109BE..109BF ; ALetter # Lo [2] MEROITIC CURSIVE LOGOGRAM RMT..MEROITIC CURSIVE LOGOGRAM IMN
+10A00 ; ALetter # Lo KHAROSHTHI LETTER A
+10A10..10A13 ; ALetter # Lo [4] KHAROSHTHI LETTER KA..KHAROSHTHI LETTER GHA
+10A15..10A17 ; ALetter # Lo [3] KHAROSHTHI LETTER CA..KHAROSHTHI LETTER JA
+10A19..10A33 ; ALetter # Lo [27] KHAROSHTHI LETTER NYA..KHAROSHTHI LETTER TTTHA
+10A60..10A7C ; ALetter # Lo [29] OLD SOUTH ARABIAN LETTER HE..OLD SOUTH ARABIAN LETTER THETH
+10A80..10A9C ; ALetter # Lo [29] OLD NORTH ARABIAN LETTER HEH..OLD NORTH ARABIAN LETTER ZAH
+10AC0..10AC7 ; ALetter # Lo [8] MANICHAEAN LETTER ALEPH..MANICHAEAN LETTER WAW
+10AC9..10AE4 ; ALetter # Lo [28] MANICHAEAN LETTER ZAYIN..MANICHAEAN LETTER TAW
+10B00..10B35 ; ALetter # Lo [54] AVESTAN LETTER A..AVESTAN LETTER HE
+10B40..10B55 ; ALetter # Lo [22] INSCRIPTIONAL PARTHIAN LETTER ALEPH..INSCRIPTIONAL PARTHIAN LETTER TAW
+10B60..10B72 ; ALetter # Lo [19] INSCRIPTIONAL PAHLAVI LETTER ALEPH..INSCRIPTIONAL PAHLAVI LETTER TAW
+10B80..10B91 ; ALetter # Lo [18] PSALTER PAHLAVI LETTER ALEPH..PSALTER PAHLAVI LETTER TAW
+10C00..10C48 ; ALetter # Lo [73] OLD TURKIC LETTER ORKHON A..OLD TURKIC LETTER ORKHON BASH
+10C80..10CB2 ; ALetter # L& [51] OLD HUNGARIAN CAPITAL LETTER A..OLD HUNGARIAN CAPITAL LETTER US
+10CC0..10CF2 ; ALetter # L& [51] OLD HUNGARIAN SMALL LETTER A..OLD HUNGARIAN SMALL LETTER US
+11003..11037 ; ALetter # Lo [53] BRAHMI SIGN JIHVAMULIYA..BRAHMI LETTER OLD TAMIL NNNA
+11083..110AF ; ALetter # Lo [45] KAITHI LETTER A..KAITHI LETTER HA
+110D0..110E8 ; ALetter # Lo [25] SORA SOMPENG LETTER SAH..SORA SOMPENG LETTER MAE
+11103..11126 ; ALetter # Lo [36] CHAKMA LETTER AA..CHAKMA LETTER HAA
+11150..11172 ; ALetter # Lo [35] MAHAJANI LETTER A..MAHAJANI LETTER RRA
+11176 ; ALetter # Lo MAHAJANI LIGATURE SHRI
+11183..111B2 ; ALetter # Lo [48] SHARADA LETTER A..SHARADA LETTER HA
+111C1..111C4 ; ALetter # Lo [4] SHARADA SIGN AVAGRAHA..SHARADA OM
+111DA ; ALetter # Lo SHARADA EKAM
+111DC ; ALetter # Lo SHARADA HEADSTROKE
+11200..11211 ; ALetter # Lo [18] KHOJKI LETTER A..KHOJKI LETTER JJA
+11213..1122B ; ALetter # Lo [25] KHOJKI LETTER NYA..KHOJKI LETTER LLA
+11280..11286 ; ALetter # Lo [7] MULTANI LETTER A..MULTANI LETTER GA
+11288 ; ALetter # Lo MULTANI LETTER GHA
+1128A..1128D ; ALetter # Lo [4] MULTANI LETTER CA..MULTANI LETTER JJA
+1128F..1129D ; ALetter # Lo [15] MULTANI LETTER NYA..MULTANI LETTER BA
+1129F..112A8 ; ALetter # Lo [10] MULTANI LETTER BHA..MULTANI LETTER RHA
+112B0..112DE ; ALetter # Lo [47] KHUDAWADI LETTER A..KHUDAWADI LETTER HA
+11305..1130C ; ALetter # Lo [8] GRANTHA LETTER A..GRANTHA LETTER VOCALIC L
+1130F..11310 ; ALetter # Lo [2] GRANTHA LETTER EE..GRANTHA LETTER AI
+11313..11328 ; ALetter # Lo [22] GRANTHA LETTER OO..GRANTHA LETTER NA
+1132A..11330 ; ALetter # Lo [7] GRANTHA LETTER PA..GRANTHA LETTER RA
+11332..11333 ; ALetter # Lo [2] GRANTHA LETTER LA..GRANTHA LETTER LLA
+11335..11339 ; ALetter # Lo [5] GRANTHA LETTER VA..GRANTHA LETTER HA
+1133D ; ALetter # Lo GRANTHA SIGN AVAGRAHA
+11350 ; ALetter # Lo GRANTHA OM
+1135D..11361 ; ALetter # Lo [5] GRANTHA SIGN PLUTA..GRANTHA LETTER VOCALIC LL
+11400..11434 ; ALetter # Lo [53] NEWA LETTER A..NEWA LETTER HA
+11447..1144A ; ALetter # Lo [4] NEWA SIGN AVAGRAHA..NEWA SIDDHI
+11480..114AF ; ALetter # Lo [48] TIRHUTA ANJI..TIRHUTA LETTER HA
+114C4..114C5 ; ALetter # Lo [2] TIRHUTA SIGN AVAGRAHA..TIRHUTA GVANG
+114C7 ; ALetter # Lo TIRHUTA OM
+11580..115AE ; ALetter # Lo [47] SIDDHAM LETTER A..SIDDHAM LETTER HA
+115D8..115DB ; ALetter # Lo [4] SIDDHAM LETTER THREE-CIRCLE ALTERNATE I..SIDDHAM LETTER ALTERNATE U
+11600..1162F ; ALetter # Lo [48] MODI LETTER A..MODI LETTER LLA
+11644 ; ALetter # Lo MODI SIGN HUVA
+11680..116AA ; ALetter # Lo [43] TAKRI LETTER A..TAKRI LETTER RRA
+118A0..118DF ; ALetter # L& [64] WARANG CITI CAPITAL LETTER NGAA..WARANG CITI SMALL LETTER VIYO
+118FF ; ALetter # Lo WARANG CITI OM
+11AC0..11AF8 ; ALetter # Lo [57] PAU CIN HAU LETTER PA..PAU CIN HAU GLOTTAL STOP FINAL
+11C00..11C08 ; ALetter # Lo [9] BHAIKSUKI LETTER A..BHAIKSUKI LETTER VOCALIC L
+11C0A..11C2E ; ALetter # Lo [37] BHAIKSUKI LETTER E..BHAIKSUKI LETTER HA
+11C40 ; ALetter # Lo BHAIKSUKI SIGN AVAGRAHA
+11C72..11C8F ; ALetter # Lo [30] MARCHEN LETTER KA..MARCHEN LETTER A
+12000..12399 ; ALetter # Lo [922] CUNEIFORM SIGN A..CUNEIFORM SIGN U U
+12400..1246E ; ALetter # Nl [111] CUNEIFORM NUMERIC SIGN TWO ASH..CUNEIFORM NUMERIC SIGN NINE U VARIANT FORM
+12480..12543 ; ALetter # Lo [196] CUNEIFORM SIGN AB TIMES NUN TENU..CUNEIFORM SIGN ZU5 TIMES THREE DISH TENU
+13000..1342E ; ALetter # Lo [1071] EGYPTIAN HIEROGLYPH A001..EGYPTIAN HIEROGLYPH AA032
+14400..14646 ; ALetter # Lo [583] ANATOLIAN HIEROGLYPH A001..ANATOLIAN HIEROGLYPH A530
+16800..16A38 ; ALetter # Lo [569] BAMUM LETTER PHASE-A NGKUE MFON..BAMUM LETTER PHASE-F VUEQ
+16A40..16A5E ; ALetter # Lo [31] MRO LETTER TA..MRO LETTER TEK
+16AD0..16AED ; ALetter # Lo [30] BASSA VAH LETTER ENNI..BASSA VAH LETTER I
+16B00..16B2F ; ALetter # Lo [48] PAHAWH HMONG VOWEL KEEB..PAHAWH HMONG CONSONANT CAU
+16B40..16B43 ; ALetter # Lm [4] PAHAWH HMONG SIGN VOS SEEV..PAHAWH HMONG SIGN IB YAM
+16B63..16B77 ; ALetter # Lo [21] PAHAWH HMONG SIGN VOS LUB..PAHAWH HMONG SIGN CIM NRES TOS
+16B7D..16B8F ; ALetter # Lo [19] PAHAWH HMONG CLAN SIGN TSHEEJ..PAHAWH HMONG CLAN SIGN VWJ
+16F00..16F44 ; ALetter # Lo [69] MIAO LETTER PA..MIAO LETTER HHA
+16F50 ; ALetter # Lo MIAO LETTER NASALIZATION
+16F93..16F9F ; ALetter # Lm [13] MIAO LETTER TONE-2..MIAO LETTER REFORMED TONE-8
+16FE0 ; ALetter # Lm TANGUT ITERATION MARK
+1BC00..1BC6A ; ALetter # Lo [107] DUPLOYAN LETTER H..DUPLOYAN LETTER VOCALIC M
+1BC70..1BC7C ; ALetter # Lo [13] DUPLOYAN AFFIX LEFT HORIZONTAL SECANT..DUPLOYAN AFFIX ATTACHED TANGENT HOOK
+1BC80..1BC88 ; ALetter # Lo [9] DUPLOYAN AFFIX HIGH ACUTE..DUPLOYAN AFFIX HIGH VERTICAL
+1BC90..1BC99 ; ALetter # Lo [10] DUPLOYAN AFFIX LOW ACUTE..DUPLOYAN AFFIX LOW ARROW
+1D400..1D454 ; ALetter # L& [85] MATHEMATICAL BOLD CAPITAL A..MATHEMATICAL ITALIC SMALL G
+1D456..1D49C ; ALetter # L& [71] MATHEMATICAL ITALIC SMALL I..MATHEMATICAL SCRIPT CAPITAL A
+1D49E..1D49F ; ALetter # L& [2] MATHEMATICAL SCRIPT CAPITAL C..MATHEMATICAL SCRIPT CAPITAL D
+1D4A2 ; ALetter # L& MATHEMATICAL SCRIPT CAPITAL G
+1D4A5..1D4A6 ; ALetter # L& [2] MATHEMATICAL SCRIPT CAPITAL J..MATHEMATICAL SCRIPT CAPITAL K
+1D4A9..1D4AC ; ALetter # L& [4] MATHEMATICAL SCRIPT CAPITAL N..MATHEMATICAL SCRIPT CAPITAL Q
+1D4AE..1D4B9 ; ALetter # L& [12] MATHEMATICAL SCRIPT CAPITAL S..MATHEMATICAL SCRIPT SMALL D
+1D4BB ; ALetter # L& MATHEMATICAL SCRIPT SMALL F
+1D4BD..1D4C3 ; ALetter # L& [7] MATHEMATICAL SCRIPT SMALL H..MATHEMATICAL SCRIPT SMALL N
+1D4C5..1D505 ; ALetter # L& [65] MATHEMATICAL SCRIPT SMALL P..MATHEMATICAL FRAKTUR CAPITAL B
+1D507..1D50A ; ALetter # L& [4] MATHEMATICAL FRAKTUR CAPITAL D..MATHEMATICAL FRAKTUR CAPITAL G
+1D50D..1D514 ; ALetter # L& [8] MATHEMATICAL FRAKTUR CAPITAL J..MATHEMATICAL FRAKTUR CAPITAL Q
+1D516..1D51C ; ALetter # L& [7] MATHEMATICAL FRAKTUR CAPITAL S..MATHEMATICAL FRAKTUR CAPITAL Y
+1D51E..1D539 ; ALetter # L& [28] MATHEMATICAL FRAKTUR SMALL A..MATHEMATICAL DOUBLE-STRUCK CAPITAL B
+1D53B..1D53E ; ALetter # L& [4] MATHEMATICAL DOUBLE-STRUCK CAPITAL D..MATHEMATICAL DOUBLE-STRUCK CAPITAL G
+1D540..1D544 ; ALetter # L& [5] MATHEMATICAL DOUBLE-STRUCK CAPITAL I..MATHEMATICAL DOUBLE-STRUCK CAPITAL M
+1D546 ; ALetter # L& MATHEMATICAL DOUBLE-STRUCK CAPITAL O
+1D54A..1D550 ; ALetter # L& [7] MATHEMATICAL DOUBLE-STRUCK CAPITAL S..MATHEMATICAL DOUBLE-STRUCK CAPITAL Y
+1D552..1D6A5 ; ALetter # L& [340] MATHEMATICAL DOUBLE-STRUCK SMALL A..MATHEMATICAL ITALIC SMALL DOTLESS J
+1D6A8..1D6C0 ; ALetter # L& [25] MATHEMATICAL BOLD CAPITAL ALPHA..MATHEMATICAL BOLD CAPITAL OMEGA
+1D6C2..1D6DA ; ALetter # L& [25] MATHEMATICAL BOLD SMALL ALPHA..MATHEMATICAL BOLD SMALL OMEGA
+1D6DC..1D6FA ; ALetter # L& [31] MATHEMATICAL BOLD EPSILON SYMBOL..MATHEMATICAL ITALIC CAPITAL OMEGA
+1D6FC..1D714 ; ALetter # L& [25] MATHEMATICAL ITALIC SMALL ALPHA..MATHEMATICAL ITALIC SMALL OMEGA
+1D716..1D734 ; ALetter # L& [31] MATHEMATICAL ITALIC EPSILON SYMBOL..MATHEMATICAL BOLD ITALIC CAPITAL OMEGA
+1D736..1D74E ; ALetter # L& [25] MATHEMATICAL BOLD ITALIC SMALL ALPHA..MATHEMATICAL BOLD ITALIC SMALL OMEGA
+1D750..1D76E ; ALetter # L& [31] MATHEMATICAL BOLD ITALIC EPSILON SYMBOL..MATHEMATICAL SANS-SERIF BOLD CAPITAL OMEGA
+1D770..1D788 ; ALetter # L& [25] MATHEMATICAL SANS-SERIF BOLD SMALL ALPHA..MATHEMATICAL SANS-SERIF BOLD SMALL OMEGA
+1D78A..1D7A8 ; ALetter # L& [31] MATHEMATICAL SANS-SERIF BOLD EPSILON SYMBOL..MATHEMATICAL SANS-SERIF BOLD ITALIC CAPITAL OMEGA
+1D7AA..1D7C2 ; ALetter # L& [25] MATHEMATICAL SANS-SERIF BOLD ITALIC SMALL ALPHA..MATHEMATICAL SANS-SERIF BOLD ITALIC SMALL OMEGA
+1D7C4..1D7CB ; ALetter # L& [8] MATHEMATICAL SANS-SERIF BOLD ITALIC EPSILON SYMBOL..MATHEMATICAL BOLD SMALL DIGAMMA
+1E800..1E8C4 ; ALetter # Lo [197] MENDE KIKAKUI SYLLABLE M001 KI..MENDE KIKAKUI SYLLABLE M060 NYON
+1E900..1E943 ; ALetter # L& [68] ADLAM CAPITAL LETTER ALIF..ADLAM SMALL LETTER SHA
+1EE00..1EE03 ; ALetter # Lo [4] ARABIC MATHEMATICAL ALEF..ARABIC MATHEMATICAL DAL
+1EE05..1EE1F ; ALetter # Lo [27] ARABIC MATHEMATICAL WAW..ARABIC MATHEMATICAL DOTLESS QAF
+1EE21..1EE22 ; ALetter # Lo [2] ARABIC MATHEMATICAL INITIAL BEH..ARABIC MATHEMATICAL INITIAL JEEM
+1EE24 ; ALetter # Lo ARABIC MATHEMATICAL INITIAL HEH
+1EE27 ; ALetter # Lo ARABIC MATHEMATICAL INITIAL HAH
+1EE29..1EE32 ; ALetter # Lo [10] ARABIC MATHEMATICAL INITIAL YEH..ARABIC MATHEMATICAL INITIAL QAF
+1EE34..1EE37 ; ALetter # Lo [4] ARABIC MATHEMATICAL INITIAL SHEEN..ARABIC MATHEMATICAL INITIAL KHAH
+1EE39 ; ALetter # Lo ARABIC MATHEMATICAL INITIAL DAD
+1EE3B ; ALetter # Lo ARABIC MATHEMATICAL INITIAL GHAIN
+1EE42 ; ALetter # Lo ARABIC MATHEMATICAL TAILED JEEM
+1EE47 ; ALetter # Lo ARABIC MATHEMATICAL TAILED HAH
+1EE49 ; ALetter # Lo ARABIC MATHEMATICAL TAILED YEH
+1EE4B ; ALetter # Lo ARABIC MATHEMATICAL TAILED LAM
+1EE4D..1EE4F ; ALetter # Lo [3] ARABIC MATHEMATICAL TAILED NOON..ARABIC MATHEMATICAL TAILED AIN
+1EE51..1EE52 ; ALetter # Lo [2] ARABIC MATHEMATICAL TAILED SAD..ARABIC MATHEMATICAL TAILED QAF
+1EE54 ; ALetter # Lo ARABIC MATHEMATICAL TAILED SHEEN
+1EE57 ; ALetter # Lo ARABIC MATHEMATICAL TAILED KHAH
+1EE59 ; ALetter # Lo ARABIC MATHEMATICAL TAILED DAD
+1EE5B ; ALetter # Lo ARABIC MATHEMATICAL TAILED GHAIN
+1EE5D ; ALetter # Lo ARABIC MATHEMATICAL TAILED DOTLESS NOON
+1EE5F ; ALetter # Lo ARABIC MATHEMATICAL TAILED DOTLESS QAF
+1EE61..1EE62 ; ALetter # Lo [2] ARABIC MATHEMATICAL STRETCHED BEH..ARABIC MATHEMATICAL STRETCHED JEEM
+1EE64 ; ALetter # Lo ARABIC MATHEMATICAL STRETCHED HEH
+1EE67..1EE6A ; ALetter # Lo [4] ARABIC MATHEMATICAL STRETCHED HAH..ARABIC MATHEMATICAL STRETCHED KAF
+1EE6C..1EE72 ; ALetter # Lo [7] ARABIC MATHEMATICAL STRETCHED MEEM..ARABIC MATHEMATICAL STRETCHED QAF
+1EE74..1EE77 ; ALetter # Lo [4] ARABIC MATHEMATICAL STRETCHED SHEEN..ARABIC MATHEMATICAL STRETCHED KHAH
+1EE79..1EE7C ; ALetter # Lo [4] ARABIC MATHEMATICAL STRETCHED DAD..ARABIC MATHEMATICAL STRETCHED DOTLESS BEH
+1EE7E ; ALetter # Lo ARABIC MATHEMATICAL STRETCHED DOTLESS FEH
+1EE80..1EE89 ; ALetter # Lo [10] ARABIC MATHEMATICAL LOOPED ALEF..ARABIC MATHEMATICAL LOOPED YEH
+1EE8B..1EE9B ; ALetter # Lo [17] ARABIC MATHEMATICAL LOOPED LAM..ARABIC MATHEMATICAL LOOPED GHAIN
+1EEA1..1EEA3 ; ALetter # Lo [3] ARABIC MATHEMATICAL DOUBLE-STRUCK BEH..ARABIC MATHEMATICAL DOUBLE-STRUCK DAL
+1EEA5..1EEA9 ; ALetter # Lo [5] ARABIC MATHEMATICAL DOUBLE-STRUCK WAW..ARABIC MATHEMATICAL DOUBLE-STRUCK YEH
+1EEAB..1EEBB ; ALetter # Lo [17] ARABIC MATHEMATICAL DOUBLE-STRUCK LAM..ARABIC MATHEMATICAL DOUBLE-STRUCK GHAIN
+1F130..1F149 ; ALetter # So [26] SQUARED LATIN CAPITAL LETTER A..SQUARED LATIN CAPITAL LETTER Z
+1F150..1F169 ; ALetter # So [26] NEGATIVE CIRCLED LATIN CAPITAL LETTER A..NEGATIVE CIRCLED LATIN CAPITAL LETTER Z
+1F170..1F189 ; ALetter # So [26] NEGATIVE SQUARED LATIN CAPITAL LETTER A..NEGATIVE SQUARED LATIN CAPITAL LETTER Z
+
+# Total code points: 27992
+
+# ================================================
+
+003A ; MidLetter # Po COLON
+00B7 ; MidLetter # Po MIDDLE DOT
+02D7 ; MidLetter # Sk MODIFIER LETTER MINUS SIGN
+0387 ; MidLetter # Po GREEK ANO TELEIA
+05F4 ; MidLetter # Po HEBREW PUNCTUATION GERSHAYIM
+2027 ; MidLetter # Po HYPHENATION POINT
+FE13 ; MidLetter # Po PRESENTATION FORM FOR VERTICAL COLON
+FE55 ; MidLetter # Po SMALL COLON
+FF1A ; MidLetter # Po FULLWIDTH COLON
+
+# Total code points: 9
+
+# ================================================
+
+002C ; MidNum # Po COMMA
+003B ; MidNum # Po SEMICOLON
+037E ; MidNum # Po GREEK QUESTION MARK
+0589 ; MidNum # Po ARMENIAN FULL STOP
+060C..060D ; MidNum # Po [2] ARABIC COMMA..ARABIC DATE SEPARATOR
+066C ; MidNum # Po ARABIC THOUSANDS SEPARATOR
+07F8 ; MidNum # Po NKO COMMA
+2044 ; MidNum # Sm FRACTION SLASH
+FE10 ; MidNum # Po PRESENTATION FORM FOR VERTICAL COMMA
+FE14 ; MidNum # Po PRESENTATION FORM FOR VERTICAL SEMICOLON
+FE50 ; MidNum # Po SMALL COMMA
+FE54 ; MidNum # Po SMALL SEMICOLON
+FF0C ; MidNum # Po FULLWIDTH COMMA
+FF1B ; MidNum # Po FULLWIDTH SEMICOLON
+
+# Total code points: 15
+
+# ================================================
+
+002E ; MidNumLet # Po FULL STOP
+2018 ; MidNumLet # Pi LEFT SINGLE QUOTATION MARK
+2019 ; MidNumLet # Pf RIGHT SINGLE QUOTATION MARK
+2024 ; MidNumLet # Po ONE DOT LEADER
+FE52 ; MidNumLet # Po SMALL FULL STOP
+FF07 ; MidNumLet # Po FULLWIDTH APOSTROPHE
+FF0E ; MidNumLet # Po FULLWIDTH FULL STOP
+
+# Total code points: 7
+
+# ================================================
+
+0030..0039 ; Numeric # Nd [10] DIGIT ZERO..DIGIT NINE
+0660..0669 ; Numeric # Nd [10] ARABIC-INDIC DIGIT ZERO..ARABIC-INDIC DIGIT NINE
+066B ; Numeric # Po ARABIC DECIMAL SEPARATOR
+06F0..06F9 ; Numeric # Nd [10] EXTENDED ARABIC-INDIC DIGIT ZERO..EXTENDED ARABIC-INDIC DIGIT NINE
+07C0..07C9 ; Numeric # Nd [10] NKO DIGIT ZERO..NKO DIGIT NINE
+0966..096F ; Numeric # Nd [10] DEVANAGARI DIGIT ZERO..DEVANAGARI DIGIT NINE
+09E6..09EF ; Numeric # Nd [10] BENGALI DIGIT ZERO..BENGALI DIGIT NINE
+0A66..0A6F ; Numeric # Nd [10] GURMUKHI DIGIT ZERO..GURMUKHI DIGIT NINE
+0AE6..0AEF ; Numeric # Nd [10] GUJARATI DIGIT ZERO..GUJARATI DIGIT NINE
+0B66..0B6F ; Numeric # Nd [10] ORIYA DIGIT ZERO..ORIYA DIGIT NINE
+0BE6..0BEF ; Numeric # Nd [10] TAMIL DIGIT ZERO..TAMIL DIGIT NINE
+0C66..0C6F ; Numeric # Nd [10] TELUGU DIGIT ZERO..TELUGU DIGIT NINE
+0CE6..0CEF ; Numeric # Nd [10] KANNADA DIGIT ZERO..KANNADA DIGIT NINE
+0D66..0D6F ; Numeric # Nd [10] MALAYALAM DIGIT ZERO..MALAYALAM DIGIT NINE
+0DE6..0DEF ; Numeric # Nd [10] SINHALA LITH DIGIT ZERO..SINHALA LITH DIGIT NINE
+0E50..0E59 ; Numeric # Nd [10] THAI DIGIT ZERO..THAI DIGIT NINE
+0ED0..0ED9 ; Numeric # Nd [10] LAO DIGIT ZERO..LAO DIGIT NINE
+0F20..0F29 ; Numeric # Nd [10] TIBETAN DIGIT ZERO..TIBETAN DIGIT NINE
+1040..1049 ; Numeric # Nd [10] MYANMAR DIGIT ZERO..MYANMAR DIGIT NINE
+1090..1099 ; Numeric # Nd [10] MYANMAR SHAN DIGIT ZERO..MYANMAR SHAN DIGIT NINE
+17E0..17E9 ; Numeric # Nd [10] KHMER DIGIT ZERO..KHMER DIGIT NINE
+1810..1819 ; Numeric # Nd [10] MONGOLIAN DIGIT ZERO..MONGOLIAN DIGIT NINE
+1946..194F ; Numeric # Nd [10] LIMBU DIGIT ZERO..LIMBU DIGIT NINE
+19D0..19D9 ; Numeric # Nd [10] NEW TAI LUE DIGIT ZERO..NEW TAI LUE DIGIT NINE
+1A80..1A89 ; Numeric # Nd [10] TAI THAM HORA DIGIT ZERO..TAI THAM HORA DIGIT NINE
+1A90..1A99 ; Numeric # Nd [10] TAI THAM THAM DIGIT ZERO..TAI THAM THAM DIGIT NINE
+1B50..1B59 ; Numeric # Nd [10] BALINESE DIGIT ZERO..BALINESE DIGIT NINE
+1BB0..1BB9 ; Numeric # Nd [10] SUNDANESE DIGIT ZERO..SUNDANESE DIGIT NINE
+1C40..1C49 ; Numeric # Nd [10] LEPCHA DIGIT ZERO..LEPCHA DIGIT NINE
+1C50..1C59 ; Numeric # Nd [10] OL CHIKI DIGIT ZERO..OL CHIKI DIGIT NINE
+A620..A629 ; Numeric # Nd [10] VAI DIGIT ZERO..VAI DIGIT NINE
+A8D0..A8D9 ; Numeric # Nd [10] SAURASHTRA DIGIT ZERO..SAURASHTRA DIGIT NINE
+A900..A909 ; Numeric # Nd [10] KAYAH LI DIGIT ZERO..KAYAH LI DIGIT NINE
+A9D0..A9D9 ; Numeric # Nd [10] JAVANESE DIGIT ZERO..JAVANESE DIGIT NINE
+A9F0..A9F9 ; Numeric # Nd [10] MYANMAR TAI LAING DIGIT ZERO..MYANMAR TAI LAING DIGIT NINE
+AA50..AA59 ; Numeric # Nd [10] CHAM DIGIT ZERO..CHAM DIGIT NINE
+ABF0..ABF9 ; Numeric # Nd [10] MEETEI MAYEK DIGIT ZERO..MEETEI MAYEK DIGIT NINE
+104A0..104A9 ; Numeric # Nd [10] OSMANYA DIGIT ZERO..OSMANYA DIGIT NINE
+11066..1106F ; Numeric # Nd [10] BRAHMI DIGIT ZERO..BRAHMI DIGIT NINE
+110F0..110F9 ; Numeric # Nd [10] SORA SOMPENG DIGIT ZERO..SORA SOMPENG DIGIT NINE
+11136..1113F ; Numeric # Nd [10] CHAKMA DIGIT ZERO..CHAKMA DIGIT NINE
+111D0..111D9 ; Numeric # Nd [10] SHARADA DIGIT ZERO..SHARADA DIGIT NINE
+112F0..112F9 ; Numeric # Nd [10] KHUDAWADI DIGIT ZERO..KHUDAWADI DIGIT NINE
+11450..11459 ; Numeric # Nd [10] NEWA DIGIT ZERO..NEWA DIGIT NINE
+114D0..114D9 ; Numeric # Nd [10] TIRHUTA DIGIT ZERO..TIRHUTA DIGIT NINE
+11650..11659 ; Numeric # Nd [10] MODI DIGIT ZERO..MODI DIGIT NINE
+116C0..116C9 ; Numeric # Nd [10] TAKRI DIGIT ZERO..TAKRI DIGIT NINE
+11730..11739 ; Numeric # Nd [10] AHOM DIGIT ZERO..AHOM DIGIT NINE
+118E0..118E9 ; Numeric # Nd [10] WARANG CITI DIGIT ZERO..WARANG CITI DIGIT NINE
+11C50..11C59 ; Numeric # Nd [10] BHAIKSUKI DIGIT ZERO..BHAIKSUKI DIGIT NINE
+16A60..16A69 ; Numeric # Nd [10] MRO DIGIT ZERO..MRO DIGIT NINE
+16B50..16B59 ; Numeric # Nd [10] PAHAWH HMONG DIGIT ZERO..PAHAWH HMONG DIGIT NINE
+1D7CE..1D7FF ; Numeric # Nd [50] MATHEMATICAL BOLD DIGIT ZERO..MATHEMATICAL MONOSPACE DIGIT NINE
+1E950..1E959 ; Numeric # Nd [10] ADLAM DIGIT ZERO..ADLAM DIGIT NINE
+
+# Total code points: 571
+
+# ================================================
+
+005F ; ExtendNumLet # Pc LOW LINE
+202F ; ExtendNumLet # Zs NARROW NO-BREAK SPACE
+203F..2040 ; ExtendNumLet # Pc [2] UNDERTIE..CHARACTER TIE
+2054 ; ExtendNumLet # Pc INVERTED UNDERTIE
+FE33..FE34 ; ExtendNumLet # Pc [2] PRESENTATION FORM FOR VERTICAL LOW LINE..PRESENTATION FORM FOR VERTICAL WAVY LOW LINE
+FE4D..FE4F ; ExtendNumLet # Pc [3] DASHED LOW LINE..WAVY LOW LINE
+FF3F ; ExtendNumLet # Pc FULLWIDTH LOW LINE
+
+# Total code points: 11
+
+# ================================================
+
+261D ; E_Base # So WHITE UP POINTING INDEX
+26F9 ; E_Base # So PERSON WITH BALL
+270A..270D ; E_Base # So [4] RAISED FIST..WRITING HAND
+1F385 ; E_Base # So FATHER CHRISTMAS
+1F3C3..1F3C4 ; E_Base # So [2] RUNNER..SURFER
+1F3CA..1F3CB ; E_Base # So [2] SWIMMER..WEIGHT LIFTER
+1F442..1F443 ; E_Base # So [2] EAR..NOSE
+1F446..1F450 ; E_Base # So [11] WHITE UP POINTING BACKHAND INDEX..OPEN HANDS SIGN
+1F46E ; E_Base # So POLICE OFFICER
+1F470..1F478 ; E_Base # So [9] BRIDE WITH VEIL..PRINCESS
+1F47C ; E_Base # So BABY ANGEL
+1F481..1F483 ; E_Base # So [3] INFORMATION DESK PERSON..DANCER
+1F485..1F487 ; E_Base # So [3] NAIL POLISH..HAIRCUT
+1F4AA ; E_Base # So FLEXED BICEPS
+1F575 ; E_Base # So SLEUTH OR SPY
+1F57A ; E_Base # So MAN DANCING
+1F590 ; E_Base # So RAISED HAND WITH FINGERS SPLAYED
+1F595..1F596 ; E_Base # So [2] REVERSED HAND WITH MIDDLE FINGER EXTENDED..RAISED HAND WITH PART BETWEEN MIDDLE AND RING FINGERS
+1F645..1F647 ; E_Base # So [3] FACE WITH NO GOOD GESTURE..PERSON BOWING DEEPLY
+1F64B..1F64F ; E_Base # So [5] HAPPY PERSON RAISING ONE HAND..PERSON WITH FOLDED HANDS
+1F6A3 ; E_Base # So ROWBOAT
+1F6B4..1F6B6 ; E_Base # So [3] BICYCLIST..PEDESTRIAN
+1F6C0 ; E_Base # So BATH
+1F918..1F91E ; E_Base # So [7] SIGN OF THE HORNS..HAND WITH INDEX AND MIDDLE FINGERS CROSSED
+1F926 ; E_Base # So FACE PALM
+1F930 ; E_Base # So PREGNANT WOMAN
+1F933..1F939 ; E_Base # So [7] SELFIE..JUGGLING
+1F93C..1F93E ; E_Base # So [3] WRESTLERS..HANDBALL
+
+# Total code points: 79
+
+# ================================================
+
+1F3FB..1F3FF ; E_Modifier # Sk [5] EMOJI MODIFIER FITZPATRICK TYPE-1-2..EMOJI MODIFIER FITZPATRICK TYPE-6
+
+# Total code points: 5
+
+# ================================================
+
+200D ; ZWJ # Cf ZERO WIDTH JOINER
+
+# Total code points: 1
+
+# ================================================
+
+2764 ; Glue_After_Zwj # So HEAVY BLACK HEART
+1F48B ; Glue_After_Zwj # So KISS MARK
+1F5E8 ; Glue_After_Zwj # So LEFT SPEECH BUBBLE
+
+# Total code points: 3
+
+# ================================================
+
+1F466..1F469 ; E_Base_GAZ # So [4] BOY..WOMAN
+
+# Total code points: 4
+
+# EOF
diff --git a/src/lib-fts/fts-common.h b/src/lib-fts/fts-common.h
new file mode 100644
index 0000000..1a14463
--- /dev/null
+++ b/src/lib-fts/fts-common.h
@@ -0,0 +1,48 @@
+#ifndef FTS_COMMON_H
+#define FTS_COMMON_H
+
+/* Some might consider 0x02BB an apostrophe also. */
+#define IS_NONASCII_APOSTROPHE(c) \
+ ((c) == 0x2019 || (c) == 0xFF07)
+#define IS_APOSTROPHE(c) \
+ ((c) == 0x0027 || IS_NONASCII_APOSTROPHE(c))
+#define IS_WB5A_APOSTROPHE(c) \
+ ((c) == 0x0027 || (c) == 0x2019)
+#define FTS_PREFIX_SPLAT_CHAR 0x002A /* '*' */
+#define IS_PREFIX_SPLAT(c) \
+ ((c) == FTS_PREFIX_SPLAT_CHAR)
+/* The h letters are included because it is an exception in French.
+ A, E, H, I, O, U, Y, a, e, h, i, o, u, y */
+#define IS_ASCII_VOWEL(c) \
+ ((c) == 0x0041 || (c) == 0x0045 || (c) == 0x0048 || (c) == 0x0049 || \
+ (c) == 0x004F || (c) == 0x0055 || (c) == 0x0059 || (c) == 0x0061 || \
+ (c) == 0x0065 || (c) == 0x0068 || (c) == 0x0069 || (c) == 0x006F || \
+ (c) == 0x0075 || (c) == 0x0079)
+#define IS_NONASCII_VOWEL(c) \
+ /*latin capital letter a with grave, acute and circumflex*/ \
+ ((c) == 0x00C0 || (c) == 0x00C1 || (c) == 0x00C2 || \
+ /* latin capital letter e with grave, acute and circumflex */ \
+ (c) == 0x00C8 || (c) == 0x00C9 || (c) == 0x00CA || \
+ /* latin capital letter i with grave, acute and circumflex */ \
+ (c) == 0x00CC || (c) == 0x00CD || (c) == 0x00CE || \
+ /* latin capital letter o with grave, acute and circumflex */ \
+ (c) == 0x00D2 || (c) == 0x00D3 || (c) == 0x00D4 || \
+ /* latin capital letter u with grave, acute and circumflex */ \
+ (c) == 0x00D9 || (c) == 0x00DA || (c) == 0x00DB || \
+ /* latin capital letter y with acute */ \
+ (c) == 0x00DD || \
+ /* latin small letter a with grave, acute and circumflex */ \
+ (c) == 0x00E0 || (c) == 0x00E1 || (c) == 0x00E2 || \
+ /* latin small letter e with grave, acute and circumflex */ \
+ (c) == 0x00E8 || (c) == 0x00E9 || (c) == 0x00EA || \
+ /* latin small letter i with grave, acute and circumflex */ \
+ (c) == 0x00EC || (c) == 0x00ED || (c) == 0x00EE || \
+ /* latin small letter o with grave, acute and circumflex */ \
+ (c) == 0x00F2 || (c) == 0x00F3 || (c) == 0x00F4 || \
+ /* latin small letter u with grave, acute and circumflex */ \
+ (c) == 0x00F9 || (c) == 0x00FA || (c) == 0x00FB || \
+ /* latin small letter y with acute */ \
+ (c) == 0x00FD )
+#define IS_VOWEL(c) \
+ (IS_ASCII_VOWEL(c) || IS_NONASCII_VOWEL(c))
+#endif
diff --git a/src/lib-fts/fts-filter-common.c b/src/lib-fts/fts-filter-common.c
new file mode 100644
index 0000000..570e3ca
--- /dev/null
+++ b/src/lib-fts/fts-filter-common.c
@@ -0,0 +1,20 @@
+/* Copyright (c) 2016-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "str.h"
+#include "buffer.h"
+#include "unichar.h"
+#include "fts-filter-private.h"
+#include "fts-filter-common.h"
+#include "fts-tokenizer-common.h"
+
+void fts_filter_truncate_token(string_t *token, size_t max_length)
+{
+ if (str_len(token) <= max_length)
+ return;
+
+ size_t len = max_length;
+ fts_tokenizer_delete_trailing_partial_char(token->data, &len);
+ str_truncate(token, len);
+ i_assert(len <= max_length);
+}
diff --git a/src/lib-fts/fts-filter-common.h b/src/lib-fts/fts-filter-common.h
new file mode 100644
index 0000000..7b6552c
--- /dev/null
+++ b/src/lib-fts/fts-filter-common.h
@@ -0,0 +1,6 @@
+#ifndef FTS_FILTER_COMMON_H
+#define FTS_FILTER_COMMON_H
+
+void fts_filter_truncate_token(string_t *token, size_t max_length);
+
+#endif
diff --git a/src/lib-fts/fts-filter-contractions.c b/src/lib-fts/fts-filter-contractions.c
new file mode 100644
index 0000000..11afbb9
--- /dev/null
+++ b/src/lib-fts/fts-filter-contractions.c
@@ -0,0 +1,86 @@
+/* Copyright (c) 2015-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "str.h"
+#include "fts-language.h"
+#include "fts-filter-private.h"
+#include "fts-common.h"
+#include "unichar.h"
+
+static int
+fts_filter_contractions_create(const struct fts_language *lang,
+ const char *const *settings,
+ struct fts_filter **filter_r,
+ const char **error_r)
+{
+ struct fts_filter *filter;
+
+ if (settings[0] != NULL) {
+ *error_r = t_strdup_printf("Unknown setting: %s", settings[0]);
+ return -1;
+ }
+ if (strcmp(lang->name, "fr") != 0) {
+ *error_r = t_strdup_printf("Unsupported language: %s", lang->name);
+ return -1;
+ }
+
+ filter = i_new(struct fts_filter, 1);
+ *filter = *fts_filter_contractions;
+ filter->token = str_new(default_pool, 64);
+ *filter_r = filter;
+ return 0;
+}
+
+static int
+fts_filter_contractions_filter(struct fts_filter *filter ATTR_UNUSED,
+ const char **_token,
+ const char **error_r ATTR_UNUSED)
+{
+ int char_size, pos = 0;
+ unichar_t apostrophe;
+ const char *token = *_token;
+
+ switch (token[pos]) {
+ case 'q':
+ pos++;
+ if (token[pos] == '\0' || token[pos] != 'u')
+ break;
+ /* fall through */
+ case 'c':
+ case 'd':
+ case 'j':
+ case 'l':
+ case 'm':
+ case 'n':
+ case 's':
+ case 't':
+ pos++;
+ if (token[pos] == '\0')
+ break;
+ char_size = uni_utf8_get_char(token + pos, &apostrophe);
+ i_assert(char_size > 0);
+ if (IS_APOSTROPHE(apostrophe)) {
+ pos += char_size;
+ *_token = token + pos;
+ }
+ if (token[pos] == '\0') /* nothing left */
+ return 0;
+ break;
+ default:
+ /* do nothing */
+ break;
+ }
+
+ return 1;
+}
+
+static const struct fts_filter fts_filter_contractions_real = {
+ .class_name = "contractions",
+ .v = {
+ fts_filter_contractions_create,
+ fts_filter_contractions_filter,
+ NULL
+ }
+};
+
+const struct fts_filter *fts_filter_contractions = &fts_filter_contractions_real;
diff --git a/src/lib-fts/fts-filter-english-possessive.c b/src/lib-fts/fts-filter-english-possessive.c
new file mode 100644
index 0000000..783b6d4
--- /dev/null
+++ b/src/lib-fts/fts-filter-english-possessive.c
@@ -0,0 +1,47 @@
+/* Copyright (c) 2015-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "unichar.h"
+#include "fts-common.h"
+#include "fts-filter-private.h"
+
+static unichar_t get_ending_utf8_char(const char *str, size_t *end_pos)
+{
+ unichar_t c;
+
+ while (!UTF8_IS_START_SEQ(str[*end_pos])) {
+ i_assert(*end_pos > 0);
+ *end_pos -= 1;
+ }
+ if (uni_utf8_get_char(str + *end_pos, &c) <= 0)
+ i_unreached();
+ return c;
+}
+
+static int
+fts_filter_english_possessive_filter(struct fts_filter *filter ATTR_UNUSED,
+ const char **token,
+ const char **error_r ATTR_UNUSED)
+{
+ size_t len = strlen(*token);
+ unichar_t c;
+
+ if (len > 1 && ((*token)[len-1] == 's' || (*token)[len-1] == 'S')) {
+ len -= 2;
+ c = get_ending_utf8_char(*token, &len);
+ if (IS_APOSTROPHE(c))
+ *token = t_strndup(*token, len);
+ }
+ return 1;
+}
+
+static const struct fts_filter fts_filter_english_possessive_real = {
+ .class_name = "english-possessive",
+ .v = {
+ NULL,
+ fts_filter_english_possessive_filter,
+ NULL
+ }
+};
+
+const struct fts_filter *fts_filter_english_possessive = &fts_filter_english_possessive_real;
diff --git a/src/lib-fts/fts-filter-lowercase.c b/src/lib-fts/fts-filter-lowercase.c
new file mode 100644
index 0000000..4aa5033
--- /dev/null
+++ b/src/lib-fts/fts-filter-lowercase.c
@@ -0,0 +1,71 @@
+/* Copyright (c) 2015-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "str.h"
+#include "fts-language.h"
+#include "fts-filter-private.h"
+
+#ifdef HAVE_LIBICU
+# include "fts-icu.h"
+# include "fts-filter-common.h"
+#endif
+
+static int
+fts_filter_lowercase_create(const struct fts_language *lang ATTR_UNUSED,
+ const char *const *settings,
+ struct fts_filter **filter_r,
+ const char **error_r)
+{
+ struct fts_filter *filter;
+ unsigned int i, max_length = 250;
+
+ for (i = 0; settings[i] != NULL; i += 2) {
+ const char *key = settings[i], *value = settings[i+1];
+
+ if (strcmp(key, "maxlen") == 0) {
+ if (str_to_uint(value, &max_length) < 0 ||
+ max_length == 0) {
+ *error_r = t_strdup_printf("Invalid lowercase filter maxlen setting: %s", value);
+ return -1;
+ }
+ }
+ else {
+ *error_r = t_strdup_printf("Unknown setting: %s", key);
+ return -1;
+ }
+ }
+ filter = i_new(struct fts_filter, 1);
+ *filter = *fts_filter_lowercase;
+ filter->token = str_new(default_pool, 64);
+ filter->max_length = max_length;
+
+ *filter_r = filter;
+ return 0;
+}
+
+static int
+fts_filter_lowercase_filter(struct fts_filter *filter ATTR_UNUSED,
+ const char **token,
+ const char **error_r ATTR_UNUSED)
+{
+#ifdef HAVE_LIBICU
+ str_truncate(filter->token, 0);
+ fts_icu_lcase(filter->token, *token);
+ fts_filter_truncate_token(filter->token, filter->max_length);
+ *token = str_c(filter->token);
+#else
+ *token = t_str_lcase(*token);
+#endif
+ return 1;
+}
+
+static const struct fts_filter fts_filter_lowercase_real = {
+ .class_name = "lowercase",
+ .v = {
+ fts_filter_lowercase_create,
+ fts_filter_lowercase_filter,
+ NULL
+ }
+};
+
+const struct fts_filter *fts_filter_lowercase = &fts_filter_lowercase_real;
diff --git a/src/lib-fts/fts-filter-normalizer-icu.c b/src/lib-fts/fts-filter-normalizer-icu.c
new file mode 100644
index 0000000..50a5250
--- /dev/null
+++ b/src/lib-fts/fts-filter-normalizer-icu.c
@@ -0,0 +1,145 @@
+/* Copyright (c) 2014-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "str.h"
+#include "unichar.h" /* unicode replacement char */
+#include "fts-filter-common.h"
+#include "fts-filter-private.h"
+#include "fts-language.h"
+
+#ifdef HAVE_LIBICU
+#include "fts-icu.h"
+
+struct fts_filter_normalizer_icu {
+ struct fts_filter filter;
+ pool_t pool;
+ const char *transliterator_id;
+
+ UTransliterator *transliterator;
+ ARRAY_TYPE(icu_utf16) utf16_token, trans_token;
+ string_t *utf8_token;
+};
+
+static void fts_filter_normalizer_icu_destroy(struct fts_filter *filter)
+{
+ struct fts_filter_normalizer_icu *np =
+ (struct fts_filter_normalizer_icu *)filter;
+
+ if (np->transliterator != NULL)
+ utrans_close(np->transliterator);
+ pool_unref(&np->pool);
+}
+
+static int
+fts_filter_normalizer_icu_create(const struct fts_language *lang ATTR_UNUSED,
+ const char *const *settings,
+ struct fts_filter **filter_r,
+ const char **error_r)
+{
+ struct fts_filter_normalizer_icu *np;
+ pool_t pp;
+ unsigned int i, max_length = 250;
+ const char *id = "Any-Lower; NFKD; [: Nonspacing Mark :] Remove; NFC; [\\x20] Remove";
+
+ for (i = 0; settings[i] != NULL; i += 2) {
+ const char *key = settings[i], *value = settings[i+1];
+
+ if (strcmp(key, "id") == 0) {
+ id = value;
+ } else if (strcmp(key, "maxlen") == 0) {
+ if (str_to_uint(value, &max_length) < 0 ||
+ max_length == 0) {
+ *error_r = t_strdup_printf("Invalid icu maxlen setting: %s", value);
+ return -1;
+ }
+ } else {
+ *error_r = t_strdup_printf("Unknown setting: %s", key);
+ return -1;
+ }
+ }
+
+ pp = pool_alloconly_create(MEMPOOL_GROWING"fts_filter_normalizer_icu",
+ sizeof(struct fts_filter_normalizer_icu));
+ np = p_new(pp, struct fts_filter_normalizer_icu, 1);
+ np->pool = pp;
+ np->filter = *fts_filter_normalizer_icu;
+ np->transliterator_id = p_strdup(pp, id);
+ p_array_init(&np->utf16_token, pp, 64);
+ p_array_init(&np->trans_token, pp, 64);
+ np->utf8_token = buffer_create_dynamic(pp, 128);
+ np->filter.max_length = max_length;
+ *filter_r = &np->filter;
+ return 0;
+}
+
+static int
+fts_filter_normalizer_icu_filter(struct fts_filter *filter, const char **token,
+ const char **error_r)
+{
+ struct fts_filter_normalizer_icu *np =
+ (struct fts_filter_normalizer_icu *)filter;
+
+ if (np->transliterator == NULL)
+ if (fts_icu_transliterator_create(np->transliterator_id,
+ &np->transliterator,
+ error_r) < 0)
+ return -1;
+
+ fts_icu_utf8_to_utf16(&np->utf16_token, *token);
+ array_append_zero(&np->utf16_token);
+ array_pop_back(&np->utf16_token);
+ array_clear(&np->trans_token);
+ if (fts_icu_translate(&np->trans_token, array_front(&np->utf16_token),
+ array_count(&np->utf16_token),
+ np->transliterator, error_r) < 0)
+ return -1;
+
+ if (array_count(&np->trans_token) == 0)
+ return 0;
+
+ fts_icu_utf16_to_utf8(np->utf8_token, array_front(&np->trans_token),
+ array_count(&np->trans_token));
+ fts_filter_truncate_token(np->utf8_token, np->filter.max_length);
+ *token = str_c(np->utf8_token);
+ return 1;
+}
+
+#else
+
+static int
+fts_filter_normalizer_icu_create(const struct fts_language *lang ATTR_UNUSED,
+ const char *const *settings ATTR_UNUSED,
+ struct fts_filter **filter_r ATTR_UNUSED,
+ const char **error_r)
+{
+ *error_r = "libicu support not built in";
+ return -1;
+}
+
+static int
+fts_filter_normalizer_icu_filter(struct fts_filter *filter ATTR_UNUSED,
+ const char **token ATTR_UNUSED,
+ const char **error_r ATTR_UNUSED)
+{
+ return -1;
+}
+
+static void
+fts_filter_normalizer_icu_destroy(struct fts_filter *normalizer ATTR_UNUSED)
+{
+}
+
+#endif
+
+static const struct fts_filter fts_filter_normalizer_icu_real = {
+ .class_name = "normalizer-icu",
+ .v = {
+ fts_filter_normalizer_icu_create,
+ fts_filter_normalizer_icu_filter,
+ fts_filter_normalizer_icu_destroy
+ }
+};
+
+const struct fts_filter *fts_filter_normalizer_icu =
+ &fts_filter_normalizer_icu_real;
diff --git a/src/lib-fts/fts-filter-private.h b/src/lib-fts/fts-filter-private.h
new file mode 100644
index 0000000..ea8685b
--- /dev/null
+++ b/src/lib-fts/fts-filter-private.h
@@ -0,0 +1,35 @@
+#ifndef FTS_FILTER_PRIVATE_H
+#define FTS_FILTER_PRIVATE_H
+
+#include "fts-filter.h"
+
+#define FTS_FILTER_CLASSES_NR 6
+
+/*
+ API that stemming providers (classes) must provide: The create()
+ function is called to get an instance of a registered filter class.
+ The filter() function is called with tokens for the specific filter.
+ The destroy function is called to destroy an instance of a filter.
+
+*/
+struct fts_filter_vfuncs {
+ int (*create)(const struct fts_language *lang,
+ const char *const *settings,
+ struct fts_filter **filter_r,
+ const char **error_r);
+ int (*filter)(struct fts_filter *filter, const char **token,
+ const char **error_r);
+
+ void (*destroy)(struct fts_filter *filter);
+};
+
+struct fts_filter {
+ const char *class_name; /* name of the class this is based on */
+ struct fts_filter_vfuncs v;
+ struct fts_filter *parent;
+ string_t *token;
+ size_t max_length;
+ int refcount;
+};
+
+#endif
diff --git a/src/lib-fts/fts-filter-stemmer-snowball.c b/src/lib-fts/fts-filter-stemmer-snowball.c
new file mode 100644
index 0000000..96d91fd
--- /dev/null
+++ b/src/lib-fts/fts-filter-stemmer-snowball.c
@@ -0,0 +1,129 @@
+/* Copyright (c) 2014-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "fts-language.h"
+#include "fts-filter-private.h"
+
+#ifdef HAVE_FTS_STEMMER
+
+#include <libstemmer.h>
+
+struct fts_filter_stemmer_snowball {
+ struct fts_filter filter;
+ pool_t pool;
+ struct fts_language *lang;
+ struct sb_stemmer *stemmer;
+};
+
+static void fts_filter_stemmer_snowball_destroy(struct fts_filter *filter)
+{
+ struct fts_filter_stemmer_snowball *sp =
+ (struct fts_filter_stemmer_snowball *)filter;
+
+ if (sp->stemmer != NULL)
+ sb_stemmer_delete(sp->stemmer);
+ pool_unref(&sp->pool);
+}
+
+static int
+fts_filter_stemmer_snowball_create(const struct fts_language *lang,
+ const char *const *settings,
+ struct fts_filter **filter_r,
+ const char **error_r)
+{
+ struct fts_filter_stemmer_snowball *sp;
+ pool_t pp;
+
+ *filter_r = NULL;
+
+ if (settings[0] != NULL) {
+ *error_r = t_strdup_printf("Unknown setting: %s", settings[0]);
+ return -1;
+ }
+ pp = pool_alloconly_create(MEMPOOL_GROWING"fts_filter_stemmer_snowball",
+ sizeof(struct fts_filter));
+ sp = p_new(pp, struct fts_filter_stemmer_snowball, 1);
+ sp->pool = pp;
+ sp->filter = *fts_filter_stemmer_snowball;
+ sp->lang = p_malloc(sp->pool, sizeof(struct fts_language));
+ sp->lang->name = p_strdup(sp->pool, lang->name);
+ *filter_r = &sp->filter;
+ return 0;
+}
+
+static int
+fts_filter_stemmer_snowball_create_stemmer(struct fts_filter_stemmer_snowball *sp,
+ const char **error_r)
+{
+ sp->stemmer = sb_stemmer_new(sp->lang->name, "UTF_8");
+ if (sp->stemmer == NULL) {
+ *error_r = t_strdup_printf(
+ "Creating a Snowball stemmer for language '%s' failed.",
+ sp->lang->name);
+ fts_filter_stemmer_snowball_destroy(&sp->filter);
+ return -1;
+ }
+ return 0;
+}
+
+static int
+fts_filter_stemmer_snowball_filter(struct fts_filter *filter,
+ const char **token, const char **error_r)
+{
+ struct fts_filter_stemmer_snowball *sp =
+ (struct fts_filter_stemmer_snowball *) filter;
+ const sb_symbol *base;
+
+ if (sp->stemmer == NULL) {
+ if (fts_filter_stemmer_snowball_create_stemmer(sp, error_r) < 0)
+ return -1;
+ }
+
+ base = sb_stemmer_stem(sp->stemmer, (const unsigned char *)*token, strlen(*token));
+ if (base == NULL) {
+ /* the only reason why this could fail is because of
+ out of memory. */
+ i_fatal_status(FATAL_OUTOFMEM,
+ "sb_stemmer_stem(len=%zu) failed: Out of memory",
+ strlen(*token));
+ }
+ *token = t_strndup(base, sb_stemmer_length(sp->stemmer));
+ return 1;
+}
+
+#else
+
+static int
+fts_filter_stemmer_snowball_create(const struct fts_language *lang ATTR_UNUSED,
+ const char *const *settings ATTR_UNUSED,
+ struct fts_filter **filter_r ATTR_UNUSED,
+ const char **error_r)
+{
+ *error_r = "Snowball support not built in";
+ return -1;
+}
+static void
+fts_filter_stemmer_snowball_destroy(struct fts_filter *stemmer ATTR_UNUSED)
+{
+}
+
+static int
+fts_filter_stemmer_snowball_filter(struct fts_filter *filter ATTR_UNUSED,
+ const char **token ATTR_UNUSED,
+ const char **error_r ATTR_UNUSED)
+{
+ return -1;
+}
+
+#endif
+
+static const struct fts_filter fts_filter_stemmer_snowball_real = {
+ .class_name = "snowball",
+ .v = {
+ fts_filter_stemmer_snowball_create,
+ fts_filter_stemmer_snowball_filter,
+ fts_filter_stemmer_snowball_destroy
+ }
+};
+
+const struct fts_filter *fts_filter_stemmer_snowball = &fts_filter_stemmer_snowball_real;
diff --git a/src/lib-fts/fts-filter-stopwords.c b/src/lib-fts/fts-filter-stopwords.c
new file mode 100644
index 0000000..3440cfe
--- /dev/null
+++ b/src/lib-fts/fts-filter-stopwords.c
@@ -0,0 +1,127 @@
+/* Copyright (c) 2014-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "istream.h"
+#include "strfuncs.h"
+#include "hash.h"
+#include "unichar.h"
+#include "fts-language.h"
+#include "fts-filter-private.h"
+
+#define STOPWORDS_FILE_FORMAT "%s/stopwords_%s.txt"
+
+#define STOPWORDS_CUTCHARS "|#\t "
+#define STOPWORDS_DISALLOWED_CHARS "/\\<>.,\":()\t\n\r"
+
+struct fts_filter_stopwords {
+ struct fts_filter filter;
+ struct fts_language *lang;
+ pool_t pool;
+ HASH_TABLE(const char *, const char *) stopwords;
+ const char *stopwords_dir;
+};
+
+static int fts_filter_stopwords_read_list(struct fts_filter_stopwords *filter,
+ const char **error_r)
+{
+ struct istream *input;
+ const char *line, *word, *path;
+ int ret = 0;
+ size_t len;
+
+ path = t_strdup_printf(STOPWORDS_FILE_FORMAT,
+ filter->stopwords_dir, filter->lang->name);
+
+ input = i_stream_create_file(path, IO_BLOCK_SIZE);
+ while ((line = i_stream_read_next_line(input)) != NULL) {
+ len = strcspn(line, STOPWORDS_CUTCHARS);
+ if (len == 0)
+ continue;
+ if (strcspn(line, STOPWORDS_DISALLOWED_CHARS) < len)
+ continue;
+ word = p_strndup(filter->pool, line, len);
+ hash_table_update(filter->stopwords, word, word);
+ }
+
+ if (input->stream_errno != 0) {
+ *error_r = t_strdup_printf("Failed to read stopword list %s: %s",
+ path, i_stream_get_error(input));
+ ret = -1;
+ }
+
+ if (ret == 0 && hash_table_count(filter->stopwords) == 0)
+ i_warning("Stopwords list \"%s\" seems empty. Is the file correctly formatted?", path);
+
+ i_stream_destroy(&input);
+ return ret;
+}
+
+static void fts_filter_stopwords_destroy(struct fts_filter *filter)
+{
+ struct fts_filter_stopwords *sp = (struct fts_filter_stopwords *)filter;
+
+ hash_table_destroy(&sp->stopwords);
+ pool_unref(&sp->pool);
+}
+
+static int
+fts_filter_stopwords_create(const struct fts_language *lang,
+ const char *const *settings,
+ struct fts_filter **filter_r,
+ const char **error_r)
+{
+ struct fts_filter_stopwords *sp;
+ pool_t pp;
+ const char *dir = NULL;
+ unsigned int i;
+
+ for (i = 0; settings[i] != NULL; i += 2) {
+ const char *key = settings[i], *value = settings[i+1];
+
+ if (strcmp(key, "stopwords_dir") == 0) {
+ dir = value;
+ } else {
+ *error_r = t_strdup_printf("Unknown setting: %s", key);
+ return -1;
+ }
+ }
+ pp = pool_alloconly_create(MEMPOOL_GROWING"fts_filter_stopwords",
+ sizeof(struct fts_filter));
+ sp = p_new(pp, struct fts_filter_stopwords, 1);
+ sp->filter = *fts_filter_stopwords;
+ sp->pool = pp;
+ sp->lang = p_malloc(sp->pool, sizeof(struct fts_language));
+ sp->lang->name = p_strdup(sp->pool, lang->name);
+ if (dir != NULL)
+ sp->stopwords_dir = p_strdup(pp, dir);
+ else
+ sp->stopwords_dir = DATADIR"/stopwords";
+ *filter_r = &sp->filter;
+ return 0;
+}
+
+static int
+fts_filter_stopwords_filter(struct fts_filter *filter, const char **token,
+ const char **error_r)
+{
+ struct fts_filter_stopwords *sp =
+ (struct fts_filter_stopwords *) filter;
+
+ if (!hash_table_is_created(sp->stopwords)) {
+ hash_table_create(&sp->stopwords, sp->pool, 0, str_hash, strcmp);
+ if (fts_filter_stopwords_read_list(sp, error_r) < 0)
+ return -1;
+ }
+ return hash_table_lookup(sp->stopwords, *token) == NULL ? 1 : 0;
+}
+
+const struct fts_filter fts_filter_stopwords_real = {
+ .class_name = "stopwords",
+ .v = {
+ fts_filter_stopwords_create,
+ fts_filter_stopwords_filter,
+ fts_filter_stopwords_destroy
+ }
+};
+const struct fts_filter *fts_filter_stopwords = &fts_filter_stopwords_real;
diff --git a/src/lib-fts/fts-filter.c b/src/lib-fts/fts-filter.c
new file mode 100644
index 0000000..89e554a
--- /dev/null
+++ b/src/lib-fts/fts-filter.c
@@ -0,0 +1,140 @@
+/* Copyright (c) 2014-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "str.h"
+#include "fts-language.h"
+#include "fts-filter-private.h"
+
+#ifdef HAVE_LIBICU
+# include "fts-icu.h"
+#endif
+
+static ARRAY(const struct fts_filter *) fts_filter_classes;
+
+void fts_filters_init(void)
+{
+ i_array_init(&fts_filter_classes, FTS_FILTER_CLASSES_NR);
+
+ fts_filter_register(fts_filter_stopwords);
+ fts_filter_register(fts_filter_stemmer_snowball);
+ fts_filter_register(fts_filter_normalizer_icu);
+ fts_filter_register(fts_filter_lowercase);
+ fts_filter_register(fts_filter_english_possessive);
+ fts_filter_register(fts_filter_contractions);
+}
+
+void fts_filters_deinit(void)
+{
+#ifdef HAVE_LIBICU
+ fts_icu_deinit();
+#endif
+ array_free(&fts_filter_classes);
+}
+
+void fts_filter_register(const struct fts_filter *filter_class)
+{
+ i_assert(fts_filter_find(filter_class->class_name) == NULL);
+
+ array_push_back(&fts_filter_classes, &filter_class);
+}
+
+const struct fts_filter *fts_filter_find(const char *name)
+{
+ const struct fts_filter *filter;
+
+ array_foreach_elem(&fts_filter_classes, filter) {
+ if (strcmp(filter->class_name, name) == 0)
+ return filter;
+ }
+ return NULL;
+}
+
+int fts_filter_create(const struct fts_filter *filter_class,
+ struct fts_filter *parent,
+ const struct fts_language *lang,
+ const char *const *settings,
+ struct fts_filter **filter_r,
+ const char **error_r)
+{
+ struct fts_filter *fp;
+ const char *empty_settings = NULL;
+
+ i_assert(settings == NULL || str_array_length(settings) % 2 == 0);
+
+ if (settings == NULL)
+ settings = &empty_settings;
+
+ if (filter_class->v.create != NULL) {
+ if (filter_class->v.create(lang, settings, &fp, error_r) < 0) {
+ *filter_r = NULL;
+ return -1;
+ }
+ } else {
+ /* default implementation */
+ if (settings[0] != NULL) {
+ *error_r = t_strdup_printf("Unknown setting: %s", settings[0]);
+ return -1;
+ }
+ fp = i_new(struct fts_filter, 1);
+ *fp = *filter_class;
+ }
+ fp->refcount = 1;
+ fp->parent = parent;
+ if (parent != NULL) {
+ fts_filter_ref(parent);
+ }
+ *filter_r = fp;
+ return 0;
+}
+void fts_filter_ref(struct fts_filter *fp)
+{
+ i_assert(fp->refcount > 0);
+
+ fp->refcount++;
+}
+
+void fts_filter_unref(struct fts_filter **_fpp)
+{
+ struct fts_filter *fp = *_fpp;
+
+ i_assert(fp->refcount > 0);
+ *_fpp = NULL;
+
+ if (--fp->refcount > 0)
+ return;
+
+ if (fp->parent != NULL)
+ fts_filter_unref(&fp->parent);
+ if (fp->v.destroy != NULL)
+ fp->v.destroy(fp);
+ else {
+ /* default destroy implementation */
+ str_free(&fp->token);
+ i_free(fp);
+ }
+}
+
+int fts_filter_filter(struct fts_filter *filter, const char **token,
+ const char **error_r)
+{
+ int ret = 0;
+
+ i_assert((*token)[0] != '\0');
+
+ /* Recurse to parent. */
+ if (filter->parent != NULL)
+ ret = fts_filter_filter(filter->parent, token, error_r);
+
+ /* Parent returned token or no parent. */
+ if (ret > 0 || filter->parent == NULL)
+ ret = filter->v.filter(filter, token, error_r);
+
+ if (ret <= 0)
+ *token = NULL;
+ else {
+ i_assert(*token != NULL);
+ i_assert((*token)[0] != '\0');
+ }
+ return ret;
+}
diff --git a/src/lib-fts/fts-filter.h b/src/lib-fts/fts-filter.h
new file mode 100644
index 0000000..89060b7
--- /dev/null
+++ b/src/lib-fts/fts-filter.h
@@ -0,0 +1,71 @@
+#ifndef FTS_FILTER_H
+#define FTS_FILTER_H
+
+struct fts_language;
+struct fts_filter;
+/*
+ Settings are given in the form of a const char * const *settings =
+ {"key, "value", "key2", "value2", NULL} array of string pairs.
+ The array has to be NULL terminated.
+*/
+/*
+ Settings: "stopwords_dir", path to the directory containing stopword files.
+ Stopword files are looked up in "<path>"/stopwords_<lang>.txt
+
+ */
+extern const struct fts_filter *fts_filter_stopwords;
+
+/*
+ Settings: "lang", language of the stemmed language.
+ */
+extern const struct fts_filter *fts_filter_stemmer_snowball;
+
+/*
+ Settings: "id", description of the normalizing/translitterating rules
+ to use. See
+ http://userguide.icu-project.org/transforms/general#TOC-Transliterator-Identifiers
+ for syntax. Defaults to "Any-Lower; NFKD; [: Nonspacing Mark :] Remove; NFC"
+
+ "maxlen", maximum length of tokens that ICU normalizer will output.
+ Defaults to 250.
+ */
+extern const struct fts_filter *fts_filter_normalizer_icu;
+
+/* Lowercases the input. Supports UTF8, if libicu is available. */
+extern const struct fts_filter *fts_filter_lowercase;
+
+/* Removes <'s> suffix from words. */
+extern const struct fts_filter *fts_filter_english_possessive;
+
+/* Removes prefixing contractions from words. */
+extern const struct fts_filter *fts_filter_contractions;
+
+/* Register all built-in filters. */
+void fts_filters_init(void);
+void fts_filters_deinit(void);
+
+/* Register a new class explicitly. Built-in classes are automatically
+ registered. */
+void fts_filter_register(const struct fts_filter *filter_class);
+
+/*
+ Filtering workflow, find --> create --> filter --> destroy.
+ */
+const struct fts_filter *fts_filter_find(const char *name);
+int fts_filter_create(const struct fts_filter *filter_class,
+ struct fts_filter *parent,
+ const struct fts_language *lang,
+ const char *const *settings,
+ struct fts_filter **filter_r,
+ const char **error_r);
+void fts_filter_ref(struct fts_filter *filter);
+void fts_filter_unref(struct fts_filter **filter);
+
+/* Returns 1 if token is returned in *token, 0 if token was filtered
+ out (*token is also set to NULL) and -1 on error.
+ Input is also given via *token.
+*/
+int fts_filter_filter(struct fts_filter *filter, const char **token,
+ const char **error_r);
+
+#endif
diff --git a/src/lib-fts/fts-icu.c b/src/lib-fts/fts-icu.c
new file mode 100644
index 0000000..ca652c3
--- /dev/null
+++ b/src/lib-fts/fts-icu.c
@@ -0,0 +1,204 @@
+/* Copyright (c) 2014-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "mempool.h"
+#include "array.h"
+#include "str.h"
+#include "unichar.h"
+#include "fts-icu.h"
+
+#include <unicode/uchar.h>
+#include <unicode/ucasemap.h>
+#include <unicode/uclean.h>
+
+static struct UCaseMap *icu_csm = NULL;
+
+static struct UCaseMap *fts_icu_csm(void)
+{
+ UErrorCode err = U_ZERO_ERROR;
+
+ if (icu_csm != NULL)
+ return icu_csm;
+ icu_csm = ucasemap_open(NULL, U_FOLD_CASE_DEFAULT, &err);
+ if (U_FAILURE(err)) {
+ i_fatal("LibICU ucasemap_open() failed: %s",
+ u_errorName(err));
+ }
+ return icu_csm;
+}
+
+void fts_icu_utf8_to_utf16(ARRAY_TYPE(icu_utf16) *dest_utf16,
+ const char *src_utf8)
+{
+ buffer_t *dest_buf = dest_utf16->arr.buffer;
+ UErrorCode err = U_ZERO_ERROR;
+ size_t src_bytes = strlen(src_utf8);
+ int32_t utf16_len;
+ UChar *dest_data, *retp = NULL;
+ int32_t avail_uchars = 0;
+
+ /* try to encode with the current buffer size */
+ avail_uchars = buffer_get_writable_size(dest_buf) / sizeof(UChar);
+ dest_data = buffer_get_space_unsafe(dest_buf, 0,
+ buffer_get_writable_size(dest_buf));
+ retp = u_strFromUTF8Lenient(dest_data, avail_uchars,
+ &utf16_len, src_utf8, src_bytes, &err);
+ if (err == U_BUFFER_OVERFLOW_ERROR) {
+ /* try again with a larger buffer */
+ dest_data = buffer_get_space_unsafe(dest_buf, 0,
+ utf16_len * sizeof(UChar));
+ err = U_ZERO_ERROR;
+ retp = u_strFromUTF8Lenient(dest_data, utf16_len,
+ &utf16_len, src_utf8,
+ src_bytes, &err);
+ }
+ if (U_FAILURE(err)) {
+ i_panic("LibICU u_strFromUTF8Lenient() failed: %s",
+ u_errorName(err));
+ }
+ buffer_set_used_size(dest_buf, utf16_len * sizeof(UChar));
+ i_assert(retp == dest_data);
+}
+
+void fts_icu_utf16_to_utf8(string_t *dest_utf8, const UChar *src_utf16,
+ unsigned int src_len)
+{
+ int32_t dest_len = 0;
+ int32_t sub_num = 0;
+ char *dest_data, *retp = NULL;
+ UErrorCode err = U_ZERO_ERROR;
+
+ /* try to encode with the current buffer size */
+ dest_data = buffer_get_space_unsafe(dest_utf8, 0,
+ buffer_get_writable_size(dest_utf8));
+ retp = u_strToUTF8WithSub(dest_data, buffer_get_writable_size(dest_utf8),
+ &dest_len, src_utf16, src_len,
+ UNICODE_REPLACEMENT_CHAR, &sub_num, &err);
+ if (err == U_BUFFER_OVERFLOW_ERROR) {
+ /* try again with a larger buffer */
+ dest_data = buffer_get_space_unsafe(dest_utf8, 0, dest_len);
+ err = U_ZERO_ERROR;
+ retp = u_strToUTF8WithSub(dest_data, buffer_get_writable_size(dest_utf8), &dest_len,
+ src_utf16, src_len,
+ UNICODE_REPLACEMENT_CHAR,
+ &sub_num, &err);
+ }
+ if (U_FAILURE(err)) {
+ i_panic("LibICU u_strToUTF8WithSub() failed: %s",
+ u_errorName(err));
+ }
+ buffer_set_used_size(dest_utf8, dest_len);
+ i_assert(retp == dest_data);
+}
+
+int fts_icu_translate(ARRAY_TYPE(icu_utf16) *dest_utf16, const UChar *src_utf16,
+ unsigned int src_len, UTransliterator *transliterator,
+ const char **error_r)
+{
+ buffer_t *dest_buf = dest_utf16->arr.buffer;
+ UErrorCode err = U_ZERO_ERROR;
+ int32_t utf16_len = src_len;
+ UChar *dest_data;
+ int32_t avail_uchars, limit = src_len;
+ size_t dest_pos = dest_buf->used;
+
+ /* translation is done in-place in the buffer. try first with the
+ current buffer size. */
+ array_append(dest_utf16, src_utf16, src_len);
+
+ avail_uchars = (buffer_get_writable_size(dest_buf)-dest_pos) / sizeof(UChar);
+ dest_data = buffer_get_space_unsafe(dest_buf, dest_pos,
+ buffer_get_writable_size(dest_buf) - dest_pos);
+ utrans_transUChars(transliterator, dest_data, &utf16_len,
+ avail_uchars, 0, &limit, &err);
+ if (err == U_BUFFER_OVERFLOW_ERROR) {
+ /* try again with a larger buffer */
+ err = U_ZERO_ERROR;
+ avail_uchars = utf16_len;
+ limit = utf16_len = src_len;
+ buffer_write(dest_buf, dest_pos,
+ src_utf16, src_len*sizeof(UChar));
+ dest_data = buffer_get_space_unsafe(dest_buf, dest_pos,
+ avail_uchars * sizeof(UChar));
+ utrans_transUChars(transliterator, dest_data, &utf16_len,
+ avail_uchars, 0, &limit, &err);
+ i_assert(err != U_BUFFER_OVERFLOW_ERROR);
+ }
+ if (U_FAILURE(err)) {
+ *error_r = t_strdup_printf("LibICU utrans_transUChars() failed: %s",
+ u_errorName(err));
+ buffer_set_used_size(dest_buf, dest_pos);
+ return -1;
+ }
+ buffer_set_used_size(dest_buf, utf16_len * sizeof(UChar));
+ return 0;
+}
+
+void fts_icu_lcase(string_t *dest_utf8, const char *src_utf8)
+{
+ struct UCaseMap *csm = fts_icu_csm();
+ size_t avail_bytes, dest_pos = dest_utf8->used;
+ char *dest_data;
+ int dest_full_len;
+ UErrorCode err = U_ZERO_ERROR;
+
+ avail_bytes = buffer_get_writable_size(dest_utf8) - dest_pos;
+ dest_data = buffer_get_space_unsafe(dest_utf8, dest_pos, avail_bytes);
+
+ /* ucasemap_utf8ToLower() may need to be called multiple times, because
+ the first return value may not be large enough. */
+ for (unsigned int i = 0;; i++) {
+ dest_full_len = ucasemap_utf8ToLower(csm, dest_data, avail_bytes,
+ src_utf8, -1, &err);
+ if (err != U_BUFFER_OVERFLOW_ERROR || i == 2)
+ break;
+
+ err = U_ZERO_ERROR;
+ dest_data = buffer_get_space_unsafe(dest_utf8, dest_pos, dest_full_len);
+ avail_bytes = dest_full_len;
+ }
+ if (U_FAILURE(err)) {
+ i_fatal("LibICU ucasemap_utf8ToLower() failed: %s",
+ u_errorName(err));
+ }
+ buffer_set_used_size(dest_utf8, dest_full_len);
+}
+
+void fts_icu_deinit(void)
+{
+ if (icu_csm != NULL) {
+ ucasemap_close(icu_csm);
+ icu_csm = NULL;
+ }
+ u_cleanup();
+}
+
+int fts_icu_transliterator_create(const char *id,
+ UTransliterator **transliterator_r,
+ const char **error_r)
+{
+ UErrorCode err = U_ZERO_ERROR;
+ UParseError perr;
+ ARRAY_TYPE(icu_utf16) id_utf16;
+ i_zero(&perr);
+
+ t_array_init(&id_utf16, strlen(id));
+ fts_icu_utf8_to_utf16(&id_utf16, id);
+ *transliterator_r = utrans_openU(array_front(&id_utf16),
+ array_count(&id_utf16),
+ UTRANS_FORWARD, NULL, 0, &perr, &err);
+ if (U_FAILURE(err)) {
+ string_t *str = t_str_new(128);
+
+ str_printfa(str, "Failed to open transliterator for id '%s': %s",
+ id, u_errorName(err));
+ if (perr.line >= 1) {
+ /* we have only one line in our ID */
+ str_printfa(str, " (parse error on offset %u)",
+ perr.offset);
+ }
+ *error_r = str_c(str);
+ return -1;
+ }
+ return 0;
+}
diff --git a/src/lib-fts/fts-icu.h b/src/lib-fts/fts-icu.h
new file mode 100644
index 0000000..5b0f3dc
--- /dev/null
+++ b/src/lib-fts/fts-icu.h
@@ -0,0 +1,28 @@
+#ifndef HAVE_FTS_ICU_H
+#define HAVE_FTS_ICU_H
+
+#include <unicode/ustring.h>
+#include <unicode/utrans.h>
+
+ARRAY_DEFINE_TYPE(icu_utf16, UChar);
+
+/* Convert UTF-8 input to UTF-16 output. */
+void fts_icu_utf8_to_utf16(ARRAY_TYPE(icu_utf16) *dest_utf16,
+ const char *src_utf8);
+/* Convert UTF-16 input to UTF-8 output. */
+void fts_icu_utf16_to_utf8(string_t *dest_utf8, const UChar *src_utf16,
+ unsigned int src_len);
+/* Run ICU translation for the string. Returns 0 on success, -1 on error. */
+int fts_icu_translate(ARRAY_TYPE(icu_utf16) *dest_utf16, const UChar *src_utf16,
+ unsigned int src_len, UTransliterator *transliterator,
+ const char **error_r);
+/* Lowercase the given UTF-8 string. */
+void fts_icu_lcase(string_t *dest_utf8, const char *src_utf8);
+
+/* Free all the memory used by ICU functions. */
+void fts_icu_deinit(void);
+
+int fts_icu_transliterator_create(const char *id,
+ UTransliterator **transliterator_r,
+ const char **error_r) ;
+#endif
diff --git a/src/lib-fts/fts-language.c b/src/lib-fts/fts-language.c
new file mode 100644
index 0000000..7874d3a
--- /dev/null
+++ b/src/lib-fts/fts-language.c
@@ -0,0 +1,368 @@
+/* Copyright (c) 2014-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "llist.h"
+#include "fts-language.h"
+
+
+#ifdef HAVE_LIBEXTTEXTCAT_TEXTCAT_H
+# include <libexttextcat/textcat.h>
+#elif defined (HAVE_FTS_EXTTEXTCAT)
+# include <textcat.h>
+#endif
+
+#ifndef TEXTCAT_RESULT_UNKNOWN /* old textcat.h has typos */
+# ifdef TEXTCAT_RESULT_UNKOWN
+# define TEXTCAT_RESULT_UNKNOWN TEXTCAT_RESULT_UNKOWN
+# endif
+#endif
+
+#define DETECT_STR_MAX_LEN 200
+
+struct fts_textcat {
+ int refcount;
+ void *handle;
+ char *config_path, *data_dir, *failed;
+};
+
+struct fts_language_list {
+ pool_t pool;
+ ARRAY_TYPE(fts_language) languages;
+ struct fts_textcat *textcat;
+ const char *textcat_config;
+ const char *textcat_datadir;
+};
+
+pool_t fts_languages_pool;
+ARRAY_TYPE(fts_language) fts_languages;
+#ifdef HAVE_FTS_EXTTEXTCAT
+static struct fts_textcat *fts_textcat_cache = NULL;
+#endif
+
+/* ISO 639-1 alpha 2 codes for languages */
+const struct fts_language fts_languages_builtin [] = {
+ { "da" }, /* Danish */
+ { "de" }, /* German */
+ { "en" }, /* English */
+ { "es" }, /* Spanish */
+ { "fi" }, /* Finnish */
+ { "fr" }, /* French */
+ { "it" }, /* Italian */
+ { "nl" }, /* Dutch */
+ { "no" }, /* Both Bokmal and Nynorsk are detected as Norwegian */
+ { "pt" }, /* Portuguese */
+ { "ro" }, /* Romanian */
+ { "ru" }, /* Russian */
+ { "sv" }, /* Swedish */
+ { "tr" }, /* Turkish */
+};
+
+const struct fts_language fts_language_data = {
+ "data"
+};
+
+#ifdef HAVE_FTS_EXTTEXTCAT
+static void fts_textcat_unref(struct fts_textcat *textcat)
+{
+ i_assert(textcat->refcount > 0);
+ if (--textcat->refcount > 0)
+ return;
+
+ if (textcat == fts_textcat_cache)
+ fts_textcat_cache = NULL;
+
+ i_free(textcat->config_path);
+ i_free(textcat->data_dir);
+ i_free(textcat->failed);
+ if (textcat->handle != NULL)
+ textcat_Done(textcat->handle);
+ i_free(textcat);
+}
+#endif
+
+void fts_languages_init(void)
+{
+ unsigned int i;
+ const struct fts_language *lp;
+
+ fts_languages_pool = pool_alloconly_create("fts_language",
+ sizeof(fts_languages_builtin));
+ p_array_init(&fts_languages, fts_languages_pool,
+ N_ELEMENTS(fts_languages_builtin));
+ for (i = 0; i < N_ELEMENTS(fts_languages_builtin); i++){
+ lp = &fts_languages_builtin[i];
+ array_push_back(&fts_languages, &lp);
+ }
+}
+
+void fts_languages_deinit(void)
+{
+#ifdef HAVE_FTS_EXTTEXTCAT
+ if (fts_textcat_cache != NULL)
+ fts_textcat_unref(fts_textcat_cache);
+#endif
+ pool_unref(&fts_languages_pool);
+}
+
+void fts_language_register(const char *name)
+{
+ struct fts_language *lang;
+
+ if (fts_language_find(name) != NULL)
+ return;
+
+ lang = p_new(fts_languages_pool, struct fts_language, 1);
+ lang->name = p_strdup(fts_languages_pool, name);
+ array_push_back(&fts_languages, (const struct fts_language **)&lang);
+}
+
+const struct fts_language *fts_language_find(const char *name)
+{
+ const struct fts_language *lang;
+
+ array_foreach_elem(&fts_languages, lang) {
+ if (strcmp(lang->name, name) == 0)
+ return lang;
+ }
+ return NULL;
+}
+
+int fts_language_list_init(const char *const *settings,
+ struct fts_language_list **list_r,
+ const char **error_r)
+{
+ struct fts_language_list *lp;
+ pool_t pool;
+ unsigned int i;
+ const char *conf = NULL, *data = NULL;
+
+ for (i = 0; settings[i] != NULL; i += 2) {
+ const char *key = settings[i], *value = settings[i+1];
+
+ if (strcmp(key, "fts_language_config") == 0)
+ conf = value;
+ else if (strcmp(key, "fts_language_data") == 0)
+ data = value;
+ else {
+ *error_r = t_strdup_printf("Unknown setting: %s", key);
+ return -1;
+ }
+ }
+
+ pool = pool_alloconly_create("fts_language_list", 128);
+ lp = p_new(pool, struct fts_language_list, 1);
+ lp->pool = pool;
+ if (conf != NULL)
+ lp->textcat_config = p_strdup(pool, conf);
+ else
+ lp->textcat_config = NULL;
+ if (data != NULL)
+ lp->textcat_datadir = p_strdup(pool, data);
+ else
+ lp->textcat_datadir = NULL;
+ p_array_init(&lp->languages, pool, 32);
+ *list_r = lp;
+ return 0;
+}
+
+void fts_language_list_deinit(struct fts_language_list **list)
+{
+ struct fts_language_list *lp = *list;
+
+ *list = NULL;
+#ifdef HAVE_FTS_EXTTEXTCAT
+ if (lp->textcat != NULL)
+ fts_textcat_unref(lp->textcat);
+#endif
+ pool_unref(&lp->pool);
+}
+
+static const struct fts_language *
+fts_language_list_find(struct fts_language_list *list, const char *name)
+{
+ const struct fts_language *lang;
+
+ array_foreach_elem(&list->languages, lang) {
+ if (strcmp(lang->name, name) == 0)
+ return lang;
+ }
+ return NULL;
+}
+
+void fts_language_list_add(struct fts_language_list *list,
+ const struct fts_language *lang)
+{
+ i_assert(fts_language_list_find(list, lang->name) == NULL);
+ array_push_back(&list->languages, &lang);
+}
+
+bool fts_language_list_add_names(struct fts_language_list *list,
+ const char *names,
+ const char **unknown_name_r)
+{
+ const char *const *langs;
+ const struct fts_language *lang;
+
+ for (langs = t_strsplit_spaces(names, ", "); *langs != NULL; langs++) {
+ lang = fts_language_find(*langs);
+ if (lang == NULL) {
+ /* unknown language */
+ *unknown_name_r = *langs;
+ return FALSE;
+ }
+ if (fts_language_list_find(list, lang->name) == NULL)
+ fts_language_list_add(list, lang);
+ }
+ return TRUE;
+}
+
+const ARRAY_TYPE(fts_language) *
+fts_language_list_get_all(struct fts_language_list *list)
+{
+ return &list->languages;
+}
+
+const struct fts_language *
+fts_language_list_get_first(struct fts_language_list *list)
+{
+ const struct fts_language *const *langp;
+
+ langp = array_front(&list->languages);
+ return *langp;
+}
+
+#ifdef HAVE_FTS_EXTTEXTCAT
+static bool fts_language_match_lists(struct fts_language_list *list,
+ candidate_t *candp, int candp_len,
+ const struct fts_language **lang_r)
+{
+ const char *name;
+
+ for (int i = 0; i < candp_len; i++) {
+ /* name is <lang>-<optional country or characterset>-<encoding>
+ eg, fi--utf8 or pt-PT-utf8 */
+ name = t_strcut(candp[i].name, '-');
+
+ /* For Norwegian we treat both bokmal and nynorsk as "no". */
+ if (strcmp(name, "nb") == 0 || strcmp(name, "nn") == 0)
+ name = "no";
+ if ((*lang_r = fts_language_list_find(list, name)) != NULL)
+ return TRUE;
+ }
+ return FALSE;
+}
+#endif
+
+#ifdef HAVE_FTS_EXTTEXTCAT
+static int fts_language_textcat_init(struct fts_language_list *list,
+ const char **error_r)
+{
+ const char *config_path;
+ const char *data_dir;
+
+ if (list->textcat != NULL) {
+ if (list->textcat->failed != NULL) {
+ *error_r = list->textcat->failed;
+ return -1;
+ }
+ i_assert(list->textcat->handle != NULL);
+ return 0;
+ }
+
+ config_path = list->textcat_config != NULL ? list->textcat_config :
+ TEXTCAT_DATADIR"/fpdb.conf";
+ data_dir = list->textcat_datadir != NULL ? list->textcat_datadir :
+ TEXTCAT_DATADIR"/";
+ if (fts_textcat_cache != NULL) {
+ if (strcmp(fts_textcat_cache->config_path, config_path) == 0 &&
+ strcmp(fts_textcat_cache->data_dir, data_dir) == 0) {
+ list->textcat = fts_textcat_cache;
+ list->textcat->refcount++;
+ return 0;
+ }
+ fts_textcat_unref(fts_textcat_cache);
+ }
+
+ fts_textcat_cache = list->textcat = i_new(struct fts_textcat, 1);
+ fts_textcat_cache->refcount = 2;
+ fts_textcat_cache->config_path = i_strdup(config_path);
+ fts_textcat_cache->data_dir = i_strdup(data_dir);
+ fts_textcat_cache->handle = special_textcat_Init(config_path, data_dir);
+ if (fts_textcat_cache->handle == NULL) {
+ fts_textcat_cache->failed = i_strdup_printf(
+ "special_textcat_Init(%s, %s) failed",
+ config_path, data_dir);
+ *error_r = fts_textcat_cache->failed;
+ return -1;
+ }
+ /* The textcat minimum document size could be set here. It
+ currently defaults to 3. UTF8 is enabled by default. */
+ return 0;
+}
+#endif
+
+static enum fts_language_result
+fts_language_detect_textcat(struct fts_language_list *list ATTR_UNUSED,
+ const unsigned char *text ATTR_UNUSED,
+ size_t size ATTR_UNUSED,
+ const struct fts_language **lang_r ATTR_UNUSED,
+ const char **error_r ATTR_UNUSED)
+{
+#ifdef HAVE_FTS_EXTTEXTCAT
+ candidate_t *candp; /* textcat candidate result array pointer */
+ int cnt;
+ bool match = FALSE;
+
+ if (fts_language_textcat_init(list, error_r) < 0)
+ return FTS_LANGUAGE_RESULT_ERROR;
+
+ candp = textcat_GetClassifyFullOutput(list->textcat->handle);
+ if (candp == NULL)
+ i_fatal_status(FATAL_OUTOFMEM, "textcat_GetCLassifyFullOutput failed: malloc() returned NULL");
+ cnt = textcat_ClassifyFull(list->textcat->handle, (const void *)text,
+ I_MIN(size, DETECT_STR_MAX_LEN), candp);
+ if (cnt > 0) {
+ T_BEGIN {
+ match = fts_language_match_lists(list, candp, cnt, lang_r);
+ } T_END;
+ textcat_ReleaseClassifyFullOutput(list->textcat->handle, candp);
+ if (match)
+ return FTS_LANGUAGE_RESULT_OK;
+ else
+ return FTS_LANGUAGE_RESULT_UNKNOWN;
+ } else {
+ textcat_ReleaseClassifyFullOutput(list->textcat->handle, candp);
+ switch (cnt) {
+ case TEXTCAT_RESULT_SHORT:
+ i_assert(size < DETECT_STR_MAX_LEN);
+ return FTS_LANGUAGE_RESULT_SHORT;
+ case TEXTCAT_RESULT_UNKNOWN:
+ return FTS_LANGUAGE_RESULT_UNKNOWN;
+ default:
+ i_unreached();
+ }
+ }
+#else
+ return FTS_LANGUAGE_RESULT_UNKNOWN;
+#endif
+}
+
+enum fts_language_result
+fts_language_detect(struct fts_language_list *list,
+ const unsigned char *text ATTR_UNUSED,
+ size_t size ATTR_UNUSED,
+ const struct fts_language **lang_r,
+ const char **error_r)
+{
+ i_assert(array_count(&list->languages) > 0);
+
+ /* if there's only a single wanted language, return it always. */
+ if (array_count(&list->languages) == 1) {
+ const struct fts_language *const *langp =
+ array_front(&list->languages);
+ *lang_r = *langp;
+ return FTS_LANGUAGE_RESULT_OK;
+ }
+ return fts_language_detect_textcat(list, text, size, lang_r, error_r);
+}
diff --git a/src/lib-fts/fts-language.h b/src/lib-fts/fts-language.h
new file mode 100644
index 0000000..884998f
--- /dev/null
+++ b/src/lib-fts/fts-language.h
@@ -0,0 +1,72 @@
+#ifndef FTS_LANGUAGE_H
+#define FTS_LANGUAGE_H
+
+struct fts_language_list;
+
+enum fts_language_result {
+ /* Provided sample is too short. */
+ FTS_LANGUAGE_RESULT_SHORT,
+ /* Language is unknown or not in the provided list . */
+ FTS_LANGUAGE_RESULT_UNKNOWN,
+
+ FTS_LANGUAGE_RESULT_OK,
+ /* textcat library initialization failed. */
+ FTS_LANGUAGE_RESULT_ERROR
+};
+
+struct fts_language {
+ /* Two-letter language name lowercased, e.g. "en" */
+ const char *name;
+};
+ARRAY_DEFINE_TYPE(fts_language, const struct fts_language *);
+
+/* Used for raw data that is indexed. This data shouldn't go through any
+ language-specific filters. */
+extern const struct fts_language fts_language_data;
+
+/*
+ Language module API.
+*/
+void fts_languages_init(void);
+void fts_languages_deinit(void);
+/* Add a language to the list of supported languages. */
+void fts_language_register(const char *name);
+/* Find a specified language by name. This finds from the internal list of
+ supported languages. */
+const struct fts_language *fts_language_find(const char *name);
+
+/*
+ Language list API
+*/
+int fts_language_list_init(const char *const *settings,
+ struct fts_language_list **list_r,
+ const char **error_r);
+void fts_language_list_deinit(struct fts_language_list **list);
+
+/* Add a language to the list of wanted languages. */
+void fts_language_list_add(struct fts_language_list *list,
+ const struct fts_language *lang);
+/* Add wanted languages from a space-separated list of language names.
+ Duplicates are ignored. Returns TRUE if ok, FALSE and unknown_name if an
+ unknown language was found from the list. */
+bool fts_language_list_add_names(struct fts_language_list *list,
+ const char *names,
+ const char **unknown_name_r);
+
+/* Return an array of all wanted languages. */
+const ARRAY_TYPE(fts_language) *
+fts_language_list_get_all(struct fts_language_list *list);
+/* Returns the first wanted language (default language). */
+const struct fts_language *
+fts_language_list_get_first(struct fts_language_list *list);
+
+/* If text was detected to be one of the languages in the list,
+ returns FTS_LANGUAGE_RESULT_OK and (a pointer to) the language (in
+ the list). error_r is set for FTS_LANGUAGE_RESULT_ERROR. */
+enum fts_language_result
+fts_language_detect(struct fts_language_list *list,
+ const unsigned char *text, size_t size,
+ const struct fts_language **lang_r,
+ const char **error_r);
+
+#endif
diff --git a/src/lib-fts/fts-library.c b/src/lib-fts/fts-library.c
new file mode 100644
index 0000000..f10634d
--- /dev/null
+++ b/src/lib-fts/fts-library.c
@@ -0,0 +1,21 @@
+/* Copyright (c) 2015-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "fts-language.h"
+#include "fts-tokenizer.h"
+#include "fts-filter.h"
+#include "fts-library.h"
+
+void fts_library_init(void)
+{
+ fts_languages_init();
+ fts_tokenizers_init();
+ fts_filters_init();
+}
+
+void fts_library_deinit(void)
+{
+ fts_languages_deinit();
+ fts_tokenizers_deinit();
+ fts_filters_deinit();
+}
diff --git a/src/lib-fts/fts-library.h b/src/lib-fts/fts-library.h
new file mode 100644
index 0000000..8799b10
--- /dev/null
+++ b/src/lib-fts/fts-library.h
@@ -0,0 +1,7 @@
+#ifndef FTS_LIBRARY_H
+#define FTS_LIBRARY_H
+
+void fts_library_init(void);
+void fts_library_deinit(void);
+
+#endif
diff --git a/src/lib-fts/fts-tokenizer-address.c b/src/lib-fts/fts-tokenizer-address.c
new file mode 100644
index 0000000..1a2fb3d
--- /dev/null
+++ b/src/lib-fts/fts-tokenizer-address.c
@@ -0,0 +1,412 @@
+/* Copyright (c) 2015-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "str.h"
+#include "buffer.h"
+#include "rfc822-parser.h"
+#include "fts-tokenizer-private.h"
+#include "fts-tokenizer-common.h"
+
+#define IS_DTEXT(c) \
+ (rfc822_atext_chars[(int)(unsigned char)(c)] == 2)
+
+#define FTS_DEFAULT_ADDRESS_MAX_LENGTH 254
+
+enum email_address_parser_state {
+ EMAIL_ADDRESS_PARSER_STATE_NONE = 0,
+ EMAIL_ADDRESS_PARSER_STATE_LOCALPART,
+ EMAIL_ADDRESS_PARSER_STATE_DOMAIN,
+ EMAIL_ADDRESS_PARSER_STATE_COMPLETE,
+ EMAIL_ADDRESS_PARSER_STATE_SKIP,
+};
+
+struct email_address_fts_tokenizer {
+ struct fts_tokenizer tokenizer;
+ enum email_address_parser_state state;
+ string_t *last_word;
+ string_t *parent_data; /* Copy of input data between tokens. */
+ unsigned int max_length;
+ bool search;
+};
+
+static int
+fts_tokenizer_email_address_create(const char *const *settings,
+ struct fts_tokenizer **tokenizer_r,
+ const char **error_r)
+{
+ struct email_address_fts_tokenizer *tok;
+ bool search = FALSE;
+ unsigned int max_length = FTS_DEFAULT_ADDRESS_MAX_LENGTH;
+ unsigned int i;
+
+ for (i = 0; settings[i] != NULL; i += 2) {
+ const char *key = settings[i], *value = settings[i+1];
+
+ if (strcmp(key, "search") == 0) {
+ search = TRUE;
+ } else if (strcmp(key, "maxlen") == 0) {
+ if (str_to_uint(value, &max_length) < 0 ||
+ max_length == 0) {
+ *error_r = t_strdup_printf("Invalid maxlen setting: %s", value);
+ return -1;
+ }
+ } else {
+ *error_r = t_strdup_printf("Unknown setting: %s", key);
+ return -1;
+ }
+ }
+
+ tok = i_new(struct email_address_fts_tokenizer, 1);
+ tok->tokenizer = *fts_tokenizer_email_address;
+ tok->last_word = str_new(default_pool, 128);
+ tok->parent_data = str_new(default_pool, 128);
+ tok->max_length = max_length;
+ tok->search = search;
+ *tokenizer_r = &tok->tokenizer;
+ return 0;
+}
+
+static void fts_tokenizer_email_address_destroy(struct fts_tokenizer *_tok)
+{
+ struct email_address_fts_tokenizer *tok =
+ (struct email_address_fts_tokenizer *)_tok;
+
+ str_free(&tok->last_word);
+ str_free(&tok->parent_data);
+ i_free(tok);
+}
+
+static bool
+fts_tokenizer_address_current_token(struct email_address_fts_tokenizer *tok,
+ const char **token_r)
+{
+ const unsigned char *data = tok->last_word->data;
+ size_t len = tok->last_word->used;
+
+ tok->tokenizer.skip_parents = TRUE;
+ tok->state = EMAIL_ADDRESS_PARSER_STATE_NONE;
+ if (str_len(tok->last_word) > tok->max_length) {
+ str_truncate(tok->last_word, tok->max_length);
+ /* As future proofing, delete partial utf8.
+ IS_DTEXT() does not actually allow utf8 addresses
+ yet though. */
+ len = tok->last_word->used;
+ fts_tokenizer_delete_trailing_partial_char(data, &len);
+ i_assert(len <= tok->max_length);
+ }
+
+ if (len > 0)
+ fts_tokenizer_delete_trailing_invalid_char(data, &len);
+ *token_r = len == 0 ? "" :
+ t_strndup(data, len);
+ return len > 0;
+}
+
+static bool
+fts_tokenizer_address_parent_data(struct email_address_fts_tokenizer *tok,
+ const char **token_r)
+{
+ if (tok->tokenizer.parent == NULL || str_len(tok->parent_data) == 0)
+ return FALSE;
+
+ if (tok->search && tok->state >= EMAIL_ADDRESS_PARSER_STATE_DOMAIN) {
+ /* we're searching and we want to find only the full
+ user@domain (not "user" and "domain"). we'll do this by
+ not feeding the last user@domain to parent tokenizer. */
+ size_t parent_prefix_len =
+ str_len(tok->parent_data) - str_len(tok->last_word);
+ i_assert(str_len(tok->parent_data) >= str_len(tok->last_word) &&
+ strcmp(str_c(tok->parent_data) + parent_prefix_len,
+ str_c(tok->last_word)) == 0);
+ str_truncate(tok->parent_data, parent_prefix_len);
+ if (str_len(tok->parent_data) == 0)
+ return FALSE;
+ }
+
+ *token_r = t_strdup(str_c(tok->parent_data));
+ str_truncate(tok->parent_data, 0);
+ return TRUE;
+}
+
+/* Used to rewind past characters that can not be the start of a new localpart.
+ Returns size that can be skipped. */
+static size_t skip_nonlocal_part(const unsigned char *data, size_t size)
+{
+ size_t skip = 0;
+
+ /* Yes, a dot can start an address. De facto before de jure. */
+ while (skip < size && (!IS_ATEXT(data[skip]) && data[skip] != '.'))
+ skip++;
+ return skip;
+}
+
+static bool
+fts_tokenizer_email_address_too_large(struct email_address_fts_tokenizer *tok,
+ size_t pos)
+{
+ if (str_len(tok->last_word) + pos <= tok->max_length)
+ return FALSE;
+
+ /* The token is too large - skip over it.
+
+ Truncate the input that was added so far to the token, so all of it
+ gets sent to the parent tokenizer in
+ fts_tokenizer_address_parent_data(). */
+ str_truncate(tok->last_word, 0);
+ return TRUE;
+}
+
+static enum email_address_parser_state
+fts_tokenizer_email_address_parse_local(struct email_address_fts_tokenizer *tok,
+ const unsigned char *data, size_t size,
+ size_t *skip_r)
+{
+ size_t pos = 0;
+ bool seen_at = FALSE;
+
+ i_assert(size == 0 || data != NULL);
+
+ while (pos < size && (IS_ATEXT(data[pos]) ||
+ data[pos] == '@' || data[pos] == '.')) {
+ if (data[pos] == '@')
+ seen_at = TRUE;
+ pos++;
+ if (seen_at)
+ break;
+ }
+
+ if (fts_tokenizer_email_address_too_large(tok, pos)) {
+ *skip_r = 0;
+ return EMAIL_ADDRESS_PARSER_STATE_SKIP;
+ }
+
+ /* localpart and @ */
+ if (seen_at && (pos > 1 || str_len(tok->last_word) > 0)) {
+ str_append_data(tok->last_word, data, pos);
+ *skip_r = pos;
+ return EMAIL_ADDRESS_PARSER_STATE_DOMAIN;
+ }
+
+ /* localpart, @ not included yet */
+ if (pos > 0 && (IS_ATEXT(data[pos-1]) || data[pos-1] == '.')) {
+ str_append_data(tok->last_word, data, pos);
+ *skip_r = pos;
+ return EMAIL_ADDRESS_PARSER_STATE_LOCALPART;
+ }
+ /* not a localpart. skip past rest of no-good chars. */
+ pos += skip_nonlocal_part(data+pos, size - pos);
+ *skip_r = pos;
+ return EMAIL_ADDRESS_PARSER_STATE_NONE;
+}
+
+static bool domain_is_empty(struct email_address_fts_tokenizer *tok)
+{
+ const char *p, *str = str_c(tok->last_word);
+
+ if ((p = strchr(str, '@')) == NULL)
+ return TRUE;
+ return p[1] == '\0';
+}
+
+static enum email_address_parser_state
+fts_tokenizer_email_address_parse_domain(struct email_address_fts_tokenizer *tok,
+ const unsigned char *data, size_t size,
+ size_t *skip_r)
+{
+ size_t pos = 0;
+
+ while (pos < size && (IS_DTEXT(data[pos]) || data[pos] == '.' || data[pos] == '-'))
+ pos++;
+
+ if (fts_tokenizer_email_address_too_large(tok, pos)) {
+ *skip_r = 0;
+ return EMAIL_ADDRESS_PARSER_STATE_SKIP;
+ }
+
+ /* A complete domain name */
+ if ((pos > 0 && pos < size) || /* non-atext after atext in this data*/
+ (pos < size && !domain_is_empty(tok))) { /* non-atext after previous atext */
+ str_append_data(tok->last_word, data, pos);
+ *skip_r = pos;
+ return EMAIL_ADDRESS_PARSER_STATE_COMPLETE;
+ }
+ if (pos == size) { /* All good, but possibly not complete. */
+ str_append_data(tok->last_word, data, pos);
+ *skip_r = pos;
+ return EMAIL_ADDRESS_PARSER_STATE_DOMAIN;
+ }
+ /* not a domain. skip past no-good chars. */
+ pos += skip_nonlocal_part(data + pos, size - pos);
+ *skip_r = pos;
+ return EMAIL_ADDRESS_PARSER_STATE_NONE;
+}
+
+static bool
+fts_tokenizer_address_skip(const unsigned char *data, size_t size,
+ size_t *skip_r)
+{
+ for (size_t pos = 0; pos < size; pos++) {
+ if (!(IS_ATEXT(data[pos]) || data[pos] == '.' ||
+ data[pos] == '-') || data[pos] == '@') {
+ *skip_r = pos;
+ return TRUE;
+ }
+ }
+ *skip_r = size;
+ return FALSE;
+}
+
+/* Buffer raw data for parent. */
+static void
+fts_tokenizer_address_update_parent(struct email_address_fts_tokenizer *tok,
+ const unsigned char *data, size_t size)
+{
+ if (tok->tokenizer.parent != NULL)
+ str_append_data(tok->parent_data, data, size);
+}
+
+static void fts_tokenizer_email_address_reset(struct fts_tokenizer *_tok)
+{
+ struct email_address_fts_tokenizer *tok =
+ (struct email_address_fts_tokenizer *)_tok;
+
+ tok->state = EMAIL_ADDRESS_PARSER_STATE_NONE;
+ str_truncate(tok->last_word, 0);
+ str_truncate(tok->parent_data, 0);
+}
+
+static int
+fts_tokenizer_email_address_next(struct fts_tokenizer *_tok,
+ const unsigned char *data, size_t size,
+ size_t *skip_r, const char **token_r,
+ const char **error_r ATTR_UNUSED)
+{
+ struct email_address_fts_tokenizer *tok =
+ (struct email_address_fts_tokenizer *)_tok;
+ size_t pos = 0, local_skip;
+ bool finished;
+
+ if (tok->tokenizer.skip_parents == TRUE)
+ tok->tokenizer.skip_parents = FALSE;
+
+ if (tok->state == EMAIL_ADDRESS_PARSER_STATE_COMPLETE) {
+ *skip_r = pos;
+ if (fts_tokenizer_address_current_token(tok, token_r))
+ return 1;
+ }
+
+ /* end of data, output lingering tokens. first the parents data, then
+ possibly our token, if complete enough */
+ if (size == 0) {
+ if (tok->state == EMAIL_ADDRESS_PARSER_STATE_DOMAIN &&
+ domain_is_empty(tok)) {
+ /* user@ without domain - reset state */
+ str_truncate(tok->last_word, 0);
+ tok->state = EMAIL_ADDRESS_PARSER_STATE_NONE;
+ }
+
+ if (fts_tokenizer_address_parent_data(tok, token_r))
+ return 1;
+
+ if (tok->state == EMAIL_ADDRESS_PARSER_STATE_DOMAIN) {
+ if (fts_tokenizer_address_current_token(tok, token_r))
+ return 1;
+ }
+ tok->state = EMAIL_ADDRESS_PARSER_STATE_NONE;
+ }
+
+ /* 1) regular input data OR
+ 2) circle around to return completed address */
+ while(pos < size || tok->state == EMAIL_ADDRESS_PARSER_STATE_COMPLETE) {
+
+ switch (tok->state) {
+ case EMAIL_ADDRESS_PARSER_STATE_NONE:
+ /* no part of address found yet. remove possible
+ earlier data */
+ str_truncate(tok->last_word, 0);
+
+ /* fall through */
+ case EMAIL_ADDRESS_PARSER_STATE_LOCALPART:
+ /* last_word is empty or has the beginnings of a valid
+ local-part, but no '@' found yet. continue parsing
+ the beginning of data to see if it contains a full
+ local-part@ */
+ tok->state =
+ fts_tokenizer_email_address_parse_local(tok,
+ data + pos,
+ size - pos,
+ &local_skip);
+ fts_tokenizer_address_update_parent(tok, data+pos,
+ local_skip);
+ pos += local_skip;
+
+ break;
+ case EMAIL_ADDRESS_PARSER_STATE_DOMAIN:
+ /* last_word has a local-part@ and maybe the beginning
+ of a domain. continue parsing the beginning of data
+ to see if it contains a valid domain. */
+
+ tok->state =
+ fts_tokenizer_email_address_parse_domain(tok,
+ data + pos,
+ size - pos,
+ &local_skip);
+ fts_tokenizer_address_update_parent(tok, data+pos,
+ local_skip);
+ pos += local_skip;
+
+ break;
+ case EMAIL_ADDRESS_PARSER_STATE_COMPLETE:
+ *skip_r = pos;
+ if (fts_tokenizer_address_parent_data(tok, token_r))
+ return 1;
+ if (fts_tokenizer_address_current_token(tok, token_r))
+ return 1;
+ break;
+ case EMAIL_ADDRESS_PARSER_STATE_SKIP:
+ /* The curernt token is too large to determine if it's
+ an email address or not. The address-tokenizer is
+ simply skipping over it, but the input is being
+ passed to the parent tokenizer. */
+ *skip_r = pos;
+ if (fts_tokenizer_address_parent_data(tok, token_r))
+ return 1;
+
+ finished = fts_tokenizer_address_skip(data + pos,
+ size - pos,
+ &local_skip);
+ fts_tokenizer_address_update_parent(tok, data+pos,
+ local_skip);
+ pos += local_skip;
+ if (finished) {
+ *skip_r = pos;
+ if (fts_tokenizer_address_parent_data(tok, token_r)) {
+ tok->state = EMAIL_ADDRESS_PARSER_STATE_NONE;
+ return 1;
+ }
+ tok->state = EMAIL_ADDRESS_PARSER_STATE_NONE;
+ }
+ break;
+ default:
+ i_unreached();
+ }
+
+ }
+ *skip_r = pos;
+ return 0;
+}
+
+static const struct fts_tokenizer_vfuncs email_address_tokenizer_vfuncs = {
+ fts_tokenizer_email_address_create,
+ fts_tokenizer_email_address_destroy,
+ fts_tokenizer_email_address_reset,
+ fts_tokenizer_email_address_next
+};
+
+static const struct fts_tokenizer fts_tokenizer_email_address_real = {
+ .name = "email-address",
+ .v = &email_address_tokenizer_vfuncs,
+ .stream_to_parents = TRUE,
+};
+const struct fts_tokenizer *fts_tokenizer_email_address =
+ &fts_tokenizer_email_address_real;
diff --git a/src/lib-fts/fts-tokenizer-common.c b/src/lib-fts/fts-tokenizer-common.c
new file mode 100644
index 0000000..2763cdf
--- /dev/null
+++ b/src/lib-fts/fts-tokenizer-common.c
@@ -0,0 +1,35 @@
+/* Copyright (c) 2016-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "unichar.h"
+#include "fts-tokenizer-common.h"
+void
+fts_tokenizer_delete_trailing_partial_char(const unsigned char *data,
+ size_t *len)
+{
+ size_t pos;
+ unsigned int char_bytes;
+
+ /* the token is truncated - make sure the last character
+ exists entirely in the token */
+ for (pos = *len-1; pos > 0; pos--) {
+ if (UTF8_IS_START_SEQ(data[pos]))
+ break;
+ }
+ char_bytes = uni_utf8_char_bytes(data[pos]);
+ if (char_bytes != *len-pos) {
+ i_assert(char_bytes > *len-pos);
+ *len = pos;
+ }
+}
+void fts_tokenizer_delete_trailing_invalid_char(const unsigned char *data,
+ size_t *len)
+{
+ size_t pos = *len;
+
+ /* the token may contain '.' in the end - remove all of them. */
+ while (pos > 0 &&
+ (data[pos-1] == '.' || data[pos-1] == '-'))
+ pos--;
+ *len = pos;
+}
diff --git a/src/lib-fts/fts-tokenizer-common.h b/src/lib-fts/fts-tokenizer-common.h
new file mode 100644
index 0000000..b90e543
--- /dev/null
+++ b/src/lib-fts/fts-tokenizer-common.h
@@ -0,0 +1,9 @@
+#ifndef FTS_TOKENIZER_COMMON_H
+#define FTS_TOKENIZER_COMMON_H
+void
+fts_tokenizer_delete_trailing_partial_char(const unsigned char *data,
+ size_t *len);
+void
+fts_tokenizer_delete_trailing_invalid_char(const unsigned char *data,
+ size_t *len);
+#endif
diff --git a/src/lib-fts/fts-tokenizer-generic-private.h b/src/lib-fts/fts-tokenizer-generic-private.h
new file mode 100644
index 0000000..87f4d48
--- /dev/null
+++ b/src/lib-fts/fts-tokenizer-generic-private.h
@@ -0,0 +1,57 @@
+#ifndef FTS_TOKENIZER_GENERIC_PRIVATE_H
+#define FTS_TOKENIZER_GENERIC_PRIVATE_H
+
+extern const struct fts_tokenizer_vfuncs generic_tokenizer_vfuncs_simple;
+extern const struct fts_tokenizer_vfuncs generic_tokenizer_vfuncs_tr29;
+
+/* Word boundary letter type */
+enum letter_type {
+ LETTER_TYPE_NONE = 0,
+ LETTER_TYPE_CR,
+ LETTER_TYPE_LF,
+ LETTER_TYPE_NEWLINE,
+ LETTER_TYPE_EXTEND,
+ LETTER_TYPE_REGIONAL_INDICATOR,
+ LETTER_TYPE_FORMAT,
+ LETTER_TYPE_KATAKANA,
+ LETTER_TYPE_HEBREW_LETTER,
+ LETTER_TYPE_ALETTER,
+ LETTER_TYPE_SINGLE_QUOTE,
+ LETTER_TYPE_DOUBLE_QUOTE,
+ LETTER_TYPE_MIDNUMLET,
+ LETTER_TYPE_MIDLETTER,
+ LETTER_TYPE_MIDNUM,
+ LETTER_TYPE_NUMERIC,
+ LETTER_TYPE_EXTENDNUMLET,
+ LETTER_TYPE_SOT,
+ LETTER_TYPE_EOT,
+ LETTER_TYPE_APOSTROPHE, /* Own modification to TR29 */
+ LETTER_TYPE_PREFIXSPLAT, /* Dovecot '*' for glob-like explicit prefix searching */
+ LETTER_TYPE_OTHER /* WB14 "any" */
+};
+
+enum boundary_algorithm {
+ BOUNDARY_ALGORITHM_NONE = 0,
+ BOUNDARY_ALGORITHM_SIMPLE,
+#define ALGORITHM_SIMPLE_NAME "simple"
+ BOUNDARY_ALGORITHM_TR29
+#define ALGORITHM_TR29_NAME "tr29"
+};
+
+struct generic_fts_tokenizer {
+ struct fts_tokenizer tokenizer;
+ unsigned int max_length;
+ bool prefixsplat; /* for search strings, accept a trailing '*' for explicit prefix */
+ bool wb5a; /* TR29 rule for prefix separation
+ in e.g. French or Italian. */
+ bool seen_wb5a;
+ unichar_t prev_letter;
+ unichar_t letter;
+ enum boundary_algorithm algorithm;
+ enum letter_type prev_type;
+ enum letter_type prev_prev_type;
+ size_t untruncated_length;
+ buffer_t *token;
+};
+
+#endif
diff --git a/src/lib-fts/fts-tokenizer-generic.c b/src/lib-fts/fts-tokenizer-generic.c
new file mode 100644
index 0000000..be72aa3
--- /dev/null
+++ b/src/lib-fts/fts-tokenizer-generic.c
@@ -0,0 +1,906 @@
+/* Copyright (c) 2014-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "base64.h"
+#include "buffer.h"
+#include "str.h"
+#include "unichar.h"
+#include "bsearch-insert-pos.h"
+#include "fts-common.h"
+#include "fts-tokenizer-private.h"
+#include "fts-tokenizer-generic-private.h"
+#include "fts-tokenizer-common.h"
+#include "word-boundary-data.c"
+#include "word-break-data.c"
+
+/* see comments below between is_base64() and skip_base64() */
+#define FTS_SKIP_BASE64_MIN_SEQUENCES 1
+#define FTS_SKIP_BASE64_MIN_CHARS 50
+
+#define FTS_DEFAULT_TOKEN_MAX_LENGTH 30
+#define FTS_WB5A_PREFIX_MAX_LENGTH 3 /* Including apostrophe */
+
+static unsigned char fts_ascii_word_breaks[128] = {
+ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, /* 0-15 */
+ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, /* 16-31 */
+
+ 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, /* 32-47: !"#$%&()*+,-./ */
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, /* 48-63: :;<=>? */
+ 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, /* 64-79: @ */
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 0, /* 80-95: [\]^ */
+ 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, /* 96-111: ` */
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 0 /* 112-127: {|}~ */
+};
+
+static int
+fts_tokenizer_generic_create(const char *const *settings,
+ struct fts_tokenizer **tokenizer_r,
+ const char **error_r)
+{
+ struct generic_fts_tokenizer *tok;
+ unsigned int max_length = FTS_DEFAULT_TOKEN_MAX_LENGTH;
+ enum boundary_algorithm algo = BOUNDARY_ALGORITHM_SIMPLE;
+ bool wb5a = FALSE;
+ bool search = FALSE;
+ bool explicitprefix = FALSE;
+ unsigned int i;
+
+ for (i = 0; settings[i] != NULL; i += 2) {
+ const char *key = settings[i], *value = settings[i+1];
+
+ if (strcmp(key, "maxlen") == 0) {
+ if (str_to_uint(value, &max_length) < 0 ||
+ max_length == 0) {
+ *error_r = t_strdup_printf(
+ "Invalid maxlen setting: %s", value);
+ return -1;
+ }
+ } else if (strcmp(key, "algorithm") == 0) {
+ if (strcmp(value, ALGORITHM_TR29_NAME) == 0)
+ algo = BOUNDARY_ALGORITHM_TR29;
+ else if (strcmp(value, ALGORITHM_SIMPLE_NAME) == 0)
+ ;
+ else {
+ *error_r = t_strdup_printf(
+ "Invalid algorithm: %s", value);
+ return -1;
+ }
+ } else if (strcmp(key, "search") == 0) {
+ /* tokenizing a search string -
+ makes no difference to us */
+ search = TRUE;
+ } else if (strcasecmp(key, "wb5a") == 0) {
+ if (strcasecmp(value, "no") == 0)
+ wb5a = FALSE;
+ else
+ wb5a = TRUE;
+ } else if (strcasecmp(key, "explicitprefix") == 0) {
+ explicitprefix = TRUE;
+ } else {
+ *error_r = t_strdup_printf("Unknown setting: %s", key);
+ return -1;
+ }
+ }
+
+ /* Tokenise normally unless tokenising an explicit prefix query */
+ if (!search)
+ explicitprefix = FALSE;
+
+ if (wb5a && algo != BOUNDARY_ALGORITHM_TR29) {
+ *error_r = "Can not use WB5a for algorithms other than TR29.";
+ return -1;
+ }
+
+ tok = i_new(struct generic_fts_tokenizer, 1);
+ if (algo == BOUNDARY_ALGORITHM_TR29)
+ tok->tokenizer.v = &generic_tokenizer_vfuncs_tr29;
+ else
+ tok->tokenizer.v = &generic_tokenizer_vfuncs_simple;
+ tok->max_length = max_length;
+ tok->algorithm = algo;
+ tok->wb5a = wb5a;
+ tok->prefixsplat = explicitprefix;
+ tok->token = buffer_create_dynamic(default_pool, 64);
+
+ *tokenizer_r = &tok->tokenizer;
+ return 0;
+}
+
+static void
+fts_tokenizer_generic_destroy(struct fts_tokenizer *_tok)
+{
+ struct generic_fts_tokenizer *tok =
+ container_of(_tok, struct generic_fts_tokenizer, tokenizer);
+
+ buffer_free(&tok->token);
+ i_free(tok);
+}
+
+static inline void
+shift_prev_type(struct generic_fts_tokenizer *tok, enum letter_type lt)
+{
+ tok->prev_prev_type = tok->prev_type;
+ tok->prev_type = lt;
+}
+
+static inline void
+add_prev_type(struct generic_fts_tokenizer *tok, enum letter_type lt)
+{
+ if(tok->prev_type != LETTER_TYPE_NONE)
+ tok->prev_prev_type = tok->prev_type;
+ tok->prev_type = lt;
+}
+
+static inline void
+add_letter(struct generic_fts_tokenizer *tok, unichar_t c)
+{
+ if(tok->letter != 0)
+ tok->prev_letter = tok->letter;
+ tok->letter = c;
+}
+
+static bool
+fts_tokenizer_generic_simple_current_token(struct generic_fts_tokenizer *tok,
+ const char **token_r)
+{
+ const unsigned char *data = tok->token->data;
+ size_t len = tok->token->used;
+
+ if (tok->untruncated_length <= tok->max_length) {
+ /* Remove the trailing apostrophe - it was made
+ into U+0027 earlier. There can be only a single such
+ apostrophe, because otherwise the token would have already
+ been split. We also want to remove the trailing apostrophe
+ only if it's the the last character in the nontruncated
+ token - a truncated token may end with apostrophe. */
+ if (len > 0 && data[len-1] == '\'') {
+ len--;
+ i_assert(len > 0 && data[len-1] != '\'');
+ }
+ if (len > 0 && data[len-1] == '*' && !tok->prefixsplat) {
+ len--;
+ i_assert(len > 0 && data[len-1] != '*');
+ }
+ } else {
+ fts_tokenizer_delete_trailing_partial_char(data, &len);
+ }
+ i_assert(len <= tok->max_length);
+
+ *token_r = len == 0 ? "" :
+ t_strndup(tok->token->data, len);
+ buffer_set_used_size(tok->token, 0);
+ tok->untruncated_length = 0;
+ return len > 0;
+}
+
+static bool uint32_find(const uint32_t *data, unsigned int count,
+ uint32_t value, unsigned int *idx_r)
+{
+ BINARY_NUMBER_SEARCH(data, count, value, idx_r);
+}
+
+static bool fts_uni_word_break(unichar_t c)
+{
+ unsigned int idx;
+
+ /* Unicode General Punctuation, including deprecated characters. */
+ if (c >= 0x2000 && c <= 0x206f)
+ return TRUE;
+ /* From word-break-data.c, which is generated from PropList.txt. */
+ if (uint32_find(White_Space, N_ELEMENTS(White_Space), c, &idx))
+ return TRUE;
+ if (uint32_find(Dash, N_ELEMENTS(Dash), c, &idx))
+ return TRUE;
+ if (uint32_find(Quotation_Mark, N_ELEMENTS(Quotation_Mark), c, &idx))
+ return TRUE;
+ if (uint32_find(Terminal_Punctuation, N_ELEMENTS(Terminal_Punctuation), c, &idx))
+ return TRUE;
+ if (uint32_find(STerm, N_ELEMENTS(STerm), c, &idx))
+ return TRUE;
+ if (uint32_find(Pattern_White_Space, N_ELEMENTS(Pattern_White_Space), c, &idx))
+ return TRUE;
+ return FALSE;
+}
+
+enum fts_break_type {
+ FTS_FROM_STOP = 0,
+ FTS_FROM_WORD = 2,
+ FTS_TO_STOP= 0,
+ FTS_TO_WORD = 1,
+#define FROM_TO(f,t) FTS_##f##_TO_##t = FTS_FROM_##f | FTS_TO_##t
+ FROM_TO(STOP,STOP),
+ FROM_TO(STOP,WORD),
+ FROM_TO(WORD,STOP),
+ FROM_TO(WORD,WORD),
+};
+static inline enum fts_break_type
+fts_simple_is_word_break(const struct generic_fts_tokenizer *tok,
+ unichar_t c, bool apostrophe)
+{
+ /* Until we know better, a letter followed by an apostrophe is continuation of the word.
+ However, if we see non-word letters afterwards, we'll reverse that decision. */
+ if (apostrophe)
+ return tok->prev_type == LETTER_TYPE_ALETTER ? FTS_WORD_TO_WORD : FTS_STOP_TO_STOP;
+
+ bool new_breakiness = (c < 0x80) ? (fts_ascii_word_breaks[c] != 0) : fts_uni_word_break(c);
+
+ return (new_breakiness ? FTS_TO_STOP : FTS_TO_WORD)
+ + (tok->prev_type == LETTER_TYPE_ALETTER ||
+ tok->prev_type == LETTER_TYPE_SINGLE_QUOTE
+ ? FTS_FROM_WORD : FTS_FROM_STOP);
+}
+
+static void fts_tokenizer_generic_reset(struct fts_tokenizer *_tok)
+{
+ struct generic_fts_tokenizer *tok =
+ container_of(_tok, struct generic_fts_tokenizer, tokenizer);
+
+ tok->prev_type = LETTER_TYPE_NONE;
+ tok->prev_prev_type = LETTER_TYPE_NONE;
+ tok->untruncated_length = 0;
+ buffer_set_used_size(tok->token, 0);
+}
+
+static void tok_append_truncated(struct generic_fts_tokenizer *tok,
+ const unsigned char *data, size_t size)
+{
+ buffer_append(tok->token, data,
+ I_MIN(size, tok->max_length - tok->token->used));
+ tok->untruncated_length += size;
+}
+
+inline static bool
+is_base64(const unsigned char ch)
+{
+ return base64_scheme.decmap[ch] != 0xff;
+}
+
+/* So far the following rule seems give good results in avoid indexing base64
+ as keywords. It also seems to run well against against base64 embedded
+ headers, like ARC-Seal, DKIM-Signature, X-SG-EID, X-SG-ID, including
+ encoded parts (e.g. =?us-ascii?Q?...?= sequences).
+
+ leader characters : [ \t\r\n=:;?]*
+ matching characters : base64_scheme.decmap[ch] != 0xff
+ trailing characters : none or [ \t\r\n=:;?] (other characters cause
+ the run to be ignored)
+ minimum run length : 50
+ minimum runs count : 1
+
+ i.e. (single or multiple) 50-chars runs of characters in the base64 set
+ - excluded the trailing '=' - are recognized as base64 and ignored
+ in indexing. */
+
+#define allowed_base64_trailers allowed_base64_leaders
+static unsigned char allowed_base64_leaders[] = {
+ ' ', '\t', '\r', '\n', '=', ';', ':', '?'
+};
+
+/* skip_base64() works doing lookahead on the data available in the tokenizer
+ buffer, .i.e. it is not able to see "what will come next" to perform more
+ extensive matches. This implies that a very long base64 sequence, which is
+ split halfway into two different chunks while feeding tokenizer, will be
+ matched separately as the trailing part of first buffer and as the leading
+ part of the second. Each of these two segments must fulfill the match
+ criteria on its own to be discarded. What we pay is we will fail to reject
+ small base64 chunks segments instead of rejecting the whole sequence.
+
+ When skip_base64() is invoked in fts_tokenizer_generic_XX_next(), we know
+ that we are not halfway the collection of a token.
+
+ As (after the previous token) the buffer will contain non-token characters
+ (i.e. token separators of some kind), we try to move forward among those
+ until we find a base64 character. If we don't find one, there's nothing we
+ can skip in the buffer and the skip phase terminates.
+
+ If we found a base64 character, we check that the previous one is in
+ allowed_base64_leaders[]; otherwise, the skip phase terminates.
+
+ Now we try to determine how long the base64 sequence is. If it is too short,
+ the skip phase terminates. It also terminates if there's a character
+ in the buffer after the sequence and this is not in
+ allowed_base64_trailers[].
+
+ At this point we know that we have:
+ - possibly a skipped sequence of non base64 characters ending with an
+ allowed leader character, followed by:
+ - a skipped sequence of base64 characters, possibly followed by an allowed
+ trailed character
+ we advance the start pointer to after the last skipped base64 character,
+ and scan again to see if we can skip further chunks in the same way. */
+
+static size_t
+skip_base64(const unsigned char *data, size_t size)
+{
+ if (data == NULL) {
+ i_assert(size == 0);
+ return 0;
+ }
+
+ const unsigned char *start, *end = data + size;
+ unsigned int matches = 0;
+ for (start = data; start < end; ) {
+ const unsigned char *first;
+ for (first = start; first < end && !is_base64(*first); first++);
+ if (first > start && memchr(allowed_base64_leaders, *(first - 1),
+ N_ELEMENTS(allowed_base64_leaders)) == NULL)
+ break;
+
+ const unsigned char *past;
+ for (past = first; past < end && is_base64(*past); past++);
+ if (past - first < FTS_SKIP_BASE64_MIN_CHARS)
+ break;
+ if (past < end && memchr(allowed_base64_trailers, *past,
+ N_ELEMENTS(allowed_base64_trailers)) == NULL)
+ break;
+ start = past;
+ matches++;
+ }
+ return matches < FTS_SKIP_BASE64_MIN_SEQUENCES ? 0 : start - data;
+}
+
+static int
+fts_tokenizer_generic_simple_next(struct fts_tokenizer *_tok,
+ const unsigned char *data, size_t size,
+ size_t *skip_r, const char **token_r,
+ const char **error_r ATTR_UNUSED)
+{
+ struct generic_fts_tokenizer *tok =
+ container_of(_tok, struct generic_fts_tokenizer, tokenizer);
+ size_t i, start;
+ int char_size;
+ unichar_t c;
+ bool apostrophe;
+ enum fts_break_type break_type;
+
+ start = tok->token->used > 0 ? 0 : skip_base64(data, size);
+ for (i = start; i < size; i += char_size) {
+ char_size = uni_utf8_get_char_n(data + i, size - i, &c);
+ i_assert(char_size > 0);
+
+ apostrophe = IS_APOSTROPHE(c);
+ if ((tok->prefixsplat && IS_PREFIX_SPLAT(c)) &&
+ (tok->prev_type == LETTER_TYPE_ALETTER)) {
+ /* this might be a prefix-mathing query */
+ shift_prev_type(tok, LETTER_TYPE_PREFIXSPLAT);
+ } else if ((break_type = fts_simple_is_word_break(tok, c, apostrophe))
+ != FTS_WORD_TO_WORD) {
+ tok_append_truncated(tok, data + start, i - start);
+ shift_prev_type(tok, (break_type & FTS_TO_WORD) != 0
+ ? LETTER_TYPE_ALETTER : LETTER_TYPE_NONE);
+ if (fts_tokenizer_generic_simple_current_token(tok, token_r)) {
+ *skip_r = i;
+ if (break_type != FTS_STOP_TO_WORD) /* therefore *_TO_STOP */
+ *skip_r += char_size;
+ return 1;
+ }
+ if ((break_type & FTS_TO_WORD) == 0)
+ start = i + char_size;
+ } else if (apostrophe) {
+ /* all apostrophes require special handling */
+ const unsigned char apostrophe_char = '\'';
+
+ tok_append_truncated(tok, data + start, i - start);
+ if (tok->token->used > 0)
+ tok_append_truncated(tok, &apostrophe_char, 1);
+ start = i + char_size;
+ shift_prev_type(tok, LETTER_TYPE_SINGLE_QUOTE);
+ } else {
+ /* Lie slightly about the type. This is anything that
+ we're not skipping or cutting on and are prepared to
+ search for - it's "as good as" a letter. */
+ shift_prev_type(tok, LETTER_TYPE_ALETTER);
+ }
+ }
+ /* word boundary not found yet */
+ if (i > start)
+ tok_append_truncated(tok, data + start, i - start);
+ *skip_r = i;
+
+ /* return the last token */
+ if (size == 0) {
+ shift_prev_type(tok, LETTER_TYPE_NONE);
+ if (fts_tokenizer_generic_simple_current_token(tok, token_r))
+ return 1;
+ }
+
+ return 0;
+}
+
+/* TODO: Arrange array searches roughly in order of likelihood of a match.
+ TODO: Make some array of the arrays, so this can be a foreach loop.
+ TODO: Check for Hangul.
+ TODO: Add Hyphens U+002D HYPHEN-MINUS, U+2010 HYPHEN, possibly also
+ U+058A ( ֊ ) ARMENIAN HYPHEN, and U+30A0 KATAKANA-HIRAGANA DOUBLE
+ HYPHEN.
+ TODO
+*/
+static enum letter_type letter_type(unichar_t c)
+{
+ unsigned int idx;
+
+ if (IS_APOSTROPHE(c))
+ return LETTER_TYPE_APOSTROPHE;
+ if (uint32_find(CR, N_ELEMENTS(CR), c, &idx))
+ return LETTER_TYPE_CR;
+ if (uint32_find(LF, N_ELEMENTS(LF), c, &idx))
+ return LETTER_TYPE_LF;
+ if (uint32_find(Newline, N_ELEMENTS(Newline), c, &idx))
+ return LETTER_TYPE_NEWLINE;
+ if (uint32_find(Extend, N_ELEMENTS(Extend), c, &idx))
+ return LETTER_TYPE_EXTEND;
+ if (uint32_find(Regional_Indicator, N_ELEMENTS(Regional_Indicator), c, &idx))
+ return LETTER_TYPE_REGIONAL_INDICATOR;
+ if (uint32_find(Format, N_ELEMENTS(Format), c, &idx))
+ return LETTER_TYPE_FORMAT;
+ if (uint32_find(Katakana, N_ELEMENTS(Katakana), c, &idx))
+ return LETTER_TYPE_KATAKANA;
+ if (uint32_find(Hebrew_Letter, N_ELEMENTS(Hebrew_Letter), c, &idx))
+ return LETTER_TYPE_HEBREW_LETTER;
+ if (uint32_find(ALetter, N_ELEMENTS(ALetter), c, &idx))
+ return LETTER_TYPE_ALETTER;
+ if (uint32_find(Single_Quote, N_ELEMENTS(Single_Quote), c, &idx))
+ return LETTER_TYPE_SINGLE_QUOTE;
+ if (uint32_find(Double_Quote, N_ELEMENTS(Double_Quote), c, &idx))
+ return LETTER_TYPE_DOUBLE_QUOTE;
+ if (uint32_find(MidNumLet, N_ELEMENTS(MidNumLet), c, &idx))
+ return LETTER_TYPE_MIDNUMLET;
+ if (uint32_find(MidLetter, N_ELEMENTS(MidLetter), c, &idx))
+ return LETTER_TYPE_MIDLETTER;
+ if (uint32_find(MidNum, N_ELEMENTS(MidNum), c, &idx))
+ return LETTER_TYPE_MIDNUM;
+ if (uint32_find(Numeric, N_ELEMENTS(Numeric), c, &idx))
+ return LETTER_TYPE_NUMERIC;
+ if (uint32_find(ExtendNumLet, N_ELEMENTS(ExtendNumLet), c, &idx))
+ return LETTER_TYPE_EXTENDNUMLET;
+ if (IS_PREFIX_SPLAT(c)) /* prioritise appropriately */
+ return LETTER_TYPE_PREFIXSPLAT;
+ return LETTER_TYPE_OTHER;
+}
+
+static bool letter_panic(struct generic_fts_tokenizer *tok ATTR_UNUSED)
+{
+ i_panic("Letter type should not be used.");
+}
+
+/* WB3, WB3a and WB3b, but really different since we try to eat
+ whitespace between words. */
+static bool letter_cr_lf_newline(struct generic_fts_tokenizer *tok ATTR_UNUSED)
+{
+ return TRUE;
+}
+
+static bool letter_extend_format(struct generic_fts_tokenizer *tok ATTR_UNUSED)
+{
+ /* WB4 */
+ return FALSE;
+}
+
+static bool letter_regional_indicator(struct generic_fts_tokenizer *tok)
+{
+ /* WB13c */
+ if (tok->prev_type == LETTER_TYPE_REGIONAL_INDICATOR)
+ return FALSE;
+
+ return TRUE; /* Any / Any */
+}
+
+static bool letter_katakana(struct generic_fts_tokenizer *tok)
+{
+ /* WB13 */
+ if (tok->prev_type == LETTER_TYPE_KATAKANA)
+ return FALSE;
+
+ /* WB13b */
+ if (tok->prev_type == LETTER_TYPE_EXTENDNUMLET)
+ return FALSE;
+
+ return TRUE; /* Any / Any */
+}
+
+static bool letter_hebrew(struct generic_fts_tokenizer *tok)
+{
+ /* WB5 */
+ if (tok->prev_type == LETTER_TYPE_HEBREW_LETTER)
+ return FALSE;
+
+ /* WB7 WB7c, except MidNumLet */
+ if (tok->prev_prev_type == LETTER_TYPE_HEBREW_LETTER &&
+ (tok->prev_type == LETTER_TYPE_SINGLE_QUOTE ||
+ tok->prev_type == LETTER_TYPE_APOSTROPHE ||
+ tok->prev_type == LETTER_TYPE_MIDLETTER ||
+ tok->prev_type == LETTER_TYPE_DOUBLE_QUOTE))
+ return FALSE;
+
+ /* WB10 */
+ if (tok->prev_type == LETTER_TYPE_NUMERIC)
+ return FALSE;
+
+ /* WB13b */
+ if (tok->prev_type == LETTER_TYPE_EXTENDNUMLET)
+ return FALSE;
+
+ return TRUE; /* Any / Any */
+}
+
+static bool letter_aletter(struct generic_fts_tokenizer *tok)
+{
+
+ /* WB5a */
+ if (tok->wb5a && tok->token->used <= FTS_WB5A_PREFIX_MAX_LENGTH)
+ if (IS_WB5A_APOSTROPHE(tok->prev_letter) && IS_VOWEL(tok->letter)) {
+ tok->seen_wb5a = TRUE;
+ return TRUE;
+ }
+
+ /* WB5 */
+ if (tok->prev_type == LETTER_TYPE_ALETTER)
+ return FALSE;
+
+ /* WB7, except MidNumLet */
+ if (tok->prev_prev_type == LETTER_TYPE_ALETTER &&
+ (tok->prev_type == LETTER_TYPE_SINGLE_QUOTE ||
+ tok->prev_type == LETTER_TYPE_APOSTROPHE ||
+ tok->prev_type == LETTER_TYPE_MIDLETTER))
+ return FALSE;
+
+ /* WB10 */
+ if (tok->prev_type == LETTER_TYPE_NUMERIC)
+ return FALSE;
+
+ /* WB13b */
+ if (tok->prev_type == LETTER_TYPE_EXTENDNUMLET)
+ return FALSE;
+
+
+ return TRUE; /* Any / Any */
+}
+
+static bool letter_single_quote(struct generic_fts_tokenizer *tok)
+{
+ /* WB6 */
+ if (tok->prev_type == LETTER_TYPE_ALETTER ||
+ tok->prev_type == LETTER_TYPE_HEBREW_LETTER)
+ return FALSE;
+
+ /* WB12 */
+ if (tok->prev_type == LETTER_TYPE_NUMERIC)
+ return FALSE;
+
+ return TRUE; /* Any / Any */
+}
+
+static bool letter_double_quote(struct generic_fts_tokenizer *tok)
+{
+
+ if (tok->prev_type == LETTER_TYPE_DOUBLE_QUOTE)
+ return FALSE;
+
+ return TRUE; /* Any / Any */
+}
+
+static bool letter_midnumlet(struct generic_fts_tokenizer *tok ATTR_UNUSED)
+{
+
+ /* Break at MidNumLet, non-conformant with WB6/WB7 */
+ return TRUE;
+}
+
+static bool letter_midletter(struct generic_fts_tokenizer *tok)
+{
+ /* WB6 */
+ if (tok->prev_type == LETTER_TYPE_ALETTER ||
+ tok->prev_type == LETTER_TYPE_HEBREW_LETTER)
+ return FALSE;
+
+ return TRUE; /* Any / Any */
+}
+
+static bool letter_midnum(struct generic_fts_tokenizer *tok)
+{
+ /* WB12 */
+ if (tok->prev_type == LETTER_TYPE_NUMERIC)
+ return FALSE;
+
+ return TRUE; /* Any / Any */
+}
+
+static bool letter_numeric(struct generic_fts_tokenizer *tok)
+{
+ /* WB8 */
+ if (tok->prev_type == LETTER_TYPE_NUMERIC)
+ return FALSE;
+
+ /* WB9 */
+ if (tok->prev_type == LETTER_TYPE_ALETTER ||
+ tok->prev_type == LETTER_TYPE_HEBREW_LETTER)
+ return FALSE;
+
+ /* WB11 */
+ if(tok->prev_prev_type == LETTER_TYPE_NUMERIC &&
+ (tok->prev_type == LETTER_TYPE_MIDNUM ||
+ tok->prev_type == LETTER_TYPE_MIDNUMLET ||
+ tok->prev_type == LETTER_TYPE_SINGLE_QUOTE))
+ return FALSE;
+
+ /* WB13b */
+ if (tok->prev_type == LETTER_TYPE_EXTENDNUMLET)
+ return FALSE;
+
+ return TRUE; /* Any / Any */
+}
+
+static bool letter_extendnumlet(struct generic_fts_tokenizer *tok)
+{
+
+ /* WB13a */
+ if (tok->prev_type == LETTER_TYPE_ALETTER ||
+ tok->prev_type == LETTER_TYPE_HEBREW_LETTER ||
+ tok->prev_type == LETTER_TYPE_NUMERIC ||
+ tok->prev_type == LETTER_TYPE_KATAKANA ||
+ tok->prev_type == LETTER_TYPE_EXTENDNUMLET)
+ return FALSE;
+
+ return TRUE; /* Any / Any */
+}
+
+static bool letter_apostrophe(struct generic_fts_tokenizer *tok)
+{
+
+ if (tok->prev_type == LETTER_TYPE_ALETTER ||
+ tok->prev_type == LETTER_TYPE_HEBREW_LETTER)
+ return FALSE;
+
+ return TRUE; /* Any / Any */
+}
+static bool letter_prefixsplat(struct generic_fts_tokenizer *tok ATTR_UNUSED)
+{
+ /* Dovecot explicit-prefix specific */
+ return TRUE; /* Always induces a word break - but with special handling */
+}
+static bool letter_other(struct generic_fts_tokenizer *tok ATTR_UNUSED)
+{
+ return TRUE; /* Any / Any */
+}
+
+/*
+ TODO: Define what to skip between words.
+ TODO: Include double quotation marks? Messes up parsing?
+ TODO: Does this "reverse approach" include too much in "whitespace"?
+ TODO: Possibly use is_word_break()?
+ */
+static bool is_nontoken(enum letter_type lt)
+{
+ if (lt == LETTER_TYPE_REGIONAL_INDICATOR || lt == LETTER_TYPE_KATAKANA ||
+ lt == LETTER_TYPE_HEBREW_LETTER || lt == LETTER_TYPE_ALETTER ||
+ lt == LETTER_TYPE_NUMERIC)
+ return FALSE;
+
+ return TRUE;
+}
+
+/* The way things are done WB6/7 and WB11/12 "false positives" can
+ leave trailing unwanted chars. They are searched for here. This is
+ very kludgy and should be coded into the rules themselves
+ somehow.
+*/
+static bool is_one_past_end(struct generic_fts_tokenizer *tok)
+{
+ /* WB6/7 false positive detected at one past end. */
+ if (tok->prev_type == LETTER_TYPE_MIDLETTER ||
+ tok->prev_type == LETTER_TYPE_MIDNUMLET ||
+ tok->prev_type == LETTER_TYPE_APOSTROPHE ||
+ tok->prev_type == LETTER_TYPE_SINGLE_QUOTE )
+ return TRUE;
+
+ /* WB11/12 false positive detected at one past end. */
+ if (tok->prev_type == LETTER_TYPE_MIDNUM ||
+ tok->prev_type == LETTER_TYPE_MIDNUMLET ||
+ tok->prev_type == LETTER_TYPE_APOSTROPHE ||
+ tok->prev_type == LETTER_TYPE_SINGLE_QUOTE)
+ return TRUE;
+
+ return FALSE;
+}
+
+static void
+fts_tokenizer_generic_tr29_current_token(struct generic_fts_tokenizer *tok,
+ const char **token_r)
+{
+ const unsigned char *data = tok->token->data;
+ size_t len = tok->token->used;
+
+ if (is_one_past_end(tok) &&
+ tok->untruncated_length <= tok->max_length) {
+ /* delete the last character */
+ while (!UTF8_IS_START_SEQ(data[len-1]))
+ len--;
+ i_assert(len > 0);
+ len--;
+ } else if (tok->untruncated_length > tok->max_length) {
+ fts_tokenizer_delete_trailing_partial_char(data, &len);
+ }
+ /* we're skipping all non-token chars at the beginning of the word,
+ so by this point we must have something here - even if we just
+ deleted the last character */
+ i_assert(len > 0);
+ i_assert(len <= tok->max_length);
+
+ tok->prev_prev_type = LETTER_TYPE_NONE;
+ tok->prev_type = LETTER_TYPE_NONE;
+ *token_r = t_strndup(data, len);
+ buffer_set_used_size(tok->token, 0);
+ tok->untruncated_length = 0;
+}
+
+static void wb5a_reinsert(struct generic_fts_tokenizer *tok)
+{
+ string_t *utf8_str = t_str_new(6);
+
+ uni_ucs4_to_utf8_c(tok->letter, utf8_str);
+ buffer_insert(tok->token, 0, str_data(utf8_str), str_len(utf8_str));
+ tok->prev_type = letter_type(tok->letter);
+ tok->letter = 0;
+ tok->prev_letter = 0;
+ tok->seen_wb5a = FALSE;
+}
+
+struct letter_fn {
+ bool (*fn)(struct generic_fts_tokenizer *tok);
+};
+static struct letter_fn letter_fns[] = {
+ {letter_panic}, {letter_cr_lf_newline}, {letter_cr_lf_newline},
+ {letter_cr_lf_newline}, {letter_extend_format},
+ {letter_regional_indicator}, {letter_extend_format},
+ {letter_katakana}, {letter_hebrew}, {letter_aletter},
+ {letter_single_quote}, {letter_double_quote},
+ {letter_midnumlet}, {letter_midletter}, {letter_midnum},
+ {letter_numeric}, {letter_extendnumlet}, {letter_panic},
+ {letter_panic}, {letter_apostrophe}, {letter_prefixsplat},
+ {letter_other}
+};
+
+/*
+ Find word boundaries in input text. Based on Unicode standard annex
+ #29, but tailored for FTS purposes.
+ http://www.unicode.org/reports/tr29/
+
+ Note: The text of tr29 is a living standard, so it keeps
+ changing. In newer specs some characters are combined, like AHLetter
+ (ALetter | Hebrew_Letter) and MidNumLetQ (MidNumLet | Single_Quote).
+
+ Adaptions:
+ * Added optional WB5a as a configurable option. The cut of prefix is
+ max FTS_WB5A_PREFIX chars.
+ * No word boundary at Start-Of-Text or End-of-Text (Wb1 and WB2).
+ * Break just once, not before and after.
+ * Break at MidNumLet, except apostrophes (diverging from WB6/WB7).
+ * Other things also (e.g. is_nontoken(), not really pure tr29. Meant
+ to assist in finding individual words.
+*/
+static bool
+uni_found_word_boundary(struct generic_fts_tokenizer *tok, enum letter_type lt)
+{
+ /* No rule knows what to do with just one char, except the linebreaks
+ we eat away (above) anyway. */
+ if (tok->prev_type != LETTER_TYPE_NONE) {
+ if (letter_fns[lt].fn(tok))
+ return TRUE;
+ }
+
+ if (lt == LETTER_TYPE_EXTEND || lt == LETTER_TYPE_FORMAT) {
+ /* These types are completely ignored. */
+ } else {
+ add_prev_type(tok,lt);
+ }
+ return FALSE;
+}
+
+static int
+fts_tokenizer_generic_tr29_next(struct fts_tokenizer *_tok,
+ const unsigned char *data, size_t size,
+ size_t *skip_r, const char **token_r,
+ const char **error_r ATTR_UNUSED)
+{
+ struct generic_fts_tokenizer *tok =
+ container_of(_tok, struct generic_fts_tokenizer, tokenizer);
+ unichar_t c;
+ size_t i, char_start_i, start_pos;
+ enum letter_type lt;
+ int char_size;
+
+ start_pos = tok->token->used > 0 ? 0 : skip_base64(data, size);
+ for (i = start_pos; i < size; ) {
+ char_start_i = i;
+ char_size = uni_utf8_get_char_n(data + i, size - i, &c);
+ i_assert(char_size > 0);
+ i += char_size;
+ lt = letter_type(c);
+
+ /* The WB5a break is detected only when the "after
+ break" char is inspected. That char needs to be
+ reinserted as the "previous char". */
+ if (tok->seen_wb5a)
+ wb5a_reinsert(tok);
+
+ if (tok->prev_type == LETTER_TYPE_NONE && is_nontoken(lt)) {
+ /* Skip non-token chars at the beginning of token */
+ i_assert(tok->token->used == 0);
+ start_pos = i;
+ continue;
+ }
+
+ if (tok->wb5a && tok->token->used <= FTS_WB5A_PREFIX_MAX_LENGTH)
+ add_letter(tok, c);
+
+ if (uni_found_word_boundary(tok, lt)) {
+ i_assert(char_start_i >= start_pos && size >= start_pos);
+ tok_append_truncated(tok, data + start_pos,
+ char_start_i - start_pos);
+ if (lt == LETTER_TYPE_PREFIXSPLAT && tok->prefixsplat) {
+ const unsigned char prefix_char = FTS_PREFIX_SPLAT_CHAR;
+ tok_append_truncated(tok, &prefix_char, 1);
+ }
+ *skip_r = i;
+ fts_tokenizer_generic_tr29_current_token(tok, token_r);
+ return 1;
+ } else if (lt == LETTER_TYPE_APOSTROPHE ||
+ lt == LETTER_TYPE_SINGLE_QUOTE) {
+ /* all apostrophes require special handling */
+ const unsigned char apostrophe_char = '\'';
+ tok_append_truncated(tok, data + start_pos,
+ char_start_i - start_pos);
+ tok_append_truncated(tok, &apostrophe_char, 1);
+ start_pos = i;
+ }
+ }
+ i_assert(i >= start_pos && size >= start_pos);
+ if (i > start_pos)
+ tok_append_truncated(tok, data + start_pos, i - start_pos);
+ *skip_r = i;
+
+ if (size == 0 && tok->token->used > 0) {
+ /* return the last token */
+ *skip_r = 0;
+ fts_tokenizer_generic_tr29_current_token(tok, token_r);
+ return 1;
+ }
+ return 0;
+}
+
+static int
+fts_tokenizer_generic_next(struct fts_tokenizer *_tok ATTR_UNUSED,
+ const unsigned char *data ATTR_UNUSED,
+ size_t size ATTR_UNUSED,
+ size_t *skip_r ATTR_UNUSED,
+ const char **token_r ATTR_UNUSED,
+ const char **error_r ATTR_UNUSED)
+{
+ i_unreached();
+}
+
+static const struct fts_tokenizer_vfuncs generic_tokenizer_vfuncs = {
+ fts_tokenizer_generic_create,
+ fts_tokenizer_generic_destroy,
+ fts_tokenizer_generic_reset,
+ fts_tokenizer_generic_next
+};
+
+static const struct fts_tokenizer fts_tokenizer_generic_real = {
+ .name = "generic",
+ .v = &generic_tokenizer_vfuncs
+};
+const struct fts_tokenizer *fts_tokenizer_generic = &fts_tokenizer_generic_real;
+
+const struct fts_tokenizer_vfuncs generic_tokenizer_vfuncs_simple = {
+ fts_tokenizer_generic_create,
+ fts_tokenizer_generic_destroy,
+ fts_tokenizer_generic_reset,
+ fts_tokenizer_generic_simple_next
+};
+const struct fts_tokenizer_vfuncs generic_tokenizer_vfuncs_tr29 = {
+ fts_tokenizer_generic_create,
+ fts_tokenizer_generic_destroy,
+ fts_tokenizer_generic_reset,
+ fts_tokenizer_generic_tr29_next
+};
diff --git a/src/lib-fts/fts-tokenizer-private.h b/src/lib-fts/fts-tokenizer-private.h
new file mode 100644
index 0000000..b7615b1
--- /dev/null
+++ b/src/lib-fts/fts-tokenizer-private.h
@@ -0,0 +1,52 @@
+#ifndef FTS_TOKENIZER_PRIVATE_H
+#define FTS_TOKENIZER_PRIVATE_H
+
+#include "fts-tokenizer.h"
+
+#define FTS_TOKENIZER_CLASSES_NR 2
+
+struct fts_tokenizer_vfuncs {
+ int (*create)(const char *const *settings,
+ struct fts_tokenizer **tokenizer_r, const char **error_r);
+ void (*destroy)(struct fts_tokenizer *tok);
+
+ void (*reset)(struct fts_tokenizer *tok);
+ int (*next)(struct fts_tokenizer *tok, const unsigned char *data,
+ size_t size, size_t *skip_r, const char **token_r,
+ const char **error_r);
+};
+
+enum fts_tokenizer_parent_state {
+ FTS_TOKENIZER_PARENT_STATE_ADD_DATA = 0,
+ FTS_TOKENIZER_PARENT_STATE_NEXT_OUTPUT,
+ FTS_TOKENIZER_PARENT_STATE_FINALIZE
+};
+
+struct fts_tokenizer {
+ const char *name;
+ const struct fts_tokenizer_vfuncs *v;
+ int refcount;
+
+ struct fts_tokenizer *parent;
+ buffer_t *parent_input;
+ enum fts_tokenizer_parent_state parent_state;
+
+ const unsigned char *prev_data;
+ size_t prev_size;
+ size_t prev_skip;
+ bool prev_reply_finished;
+ bool skip_parents; /* Return token as is, do not hand to parents. */
+ /* Instead of handing child tokens separately to parent tokenizer,
+ treat the returned tokens as a continuous stream. The final token
+ isn't returned until the child tokenizer also sees 0-sized data. */
+ bool stream_to_parents;
+ /* Parent stream still needs to be finalized, so any final pending
+ tokens will be returned. This is used only with
+ stream_to_parents=TRUE. */
+ bool finalize_parent_pending;
+};
+
+void fts_tokenizer_register(const struct fts_tokenizer *tok_class);
+void fts_tokenizer_unregister(const struct fts_tokenizer *tok_class);
+
+#endif
diff --git a/src/lib-fts/fts-tokenizer.c b/src/lib-fts/fts-tokenizer.c
new file mode 100644
index 0000000..2ae5a17
--- /dev/null
+++ b/src/lib-fts/fts-tokenizer.c
@@ -0,0 +1,260 @@
+/* Copyright (c) 2014-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "istream.h"
+#include "str.h"
+#include "strfuncs.h"
+#include "fts-tokenizer.h"
+#include "fts-tokenizer-private.h"
+
+static ARRAY(const struct fts_tokenizer *) fts_tokenizer_classes;
+
+void fts_tokenizers_init(void)
+{
+ if (!array_is_created(&fts_tokenizer_classes)) {
+ fts_tokenizer_register(fts_tokenizer_generic);
+ fts_tokenizer_register(fts_tokenizer_email_address);
+ }
+}
+
+void fts_tokenizers_deinit(void)
+{
+ if (array_is_created(&fts_tokenizer_classes))
+ array_free(&fts_tokenizer_classes);
+}
+
+/* private */
+void fts_tokenizer_register(const struct fts_tokenizer *tok_class)
+{
+ if (!array_is_created(&fts_tokenizer_classes))
+ i_array_init(&fts_tokenizer_classes, FTS_TOKENIZER_CLASSES_NR);
+ array_push_back(&fts_tokenizer_classes, &tok_class);
+}
+
+/* private */
+void fts_tokenizer_unregister(const struct fts_tokenizer *tok_class)
+{
+ const struct fts_tokenizer *const *tp;
+ unsigned int idx;
+
+ array_foreach(&fts_tokenizer_classes, tp) {
+ if (strcmp((*tp)->name, tok_class->name) == 0) {
+ idx = array_foreach_idx(&fts_tokenizer_classes, tp);
+ array_delete(&fts_tokenizer_classes, idx, 1);
+ if (array_count(&fts_tokenizer_classes) == 0)
+ array_free(&fts_tokenizer_classes);
+ return;
+ }
+ }
+ i_unreached();
+}
+
+const struct fts_tokenizer *fts_tokenizer_find(const char *name)
+{
+ const struct fts_tokenizer *tok;
+
+ array_foreach_elem(&fts_tokenizer_classes, tok) {
+ if (strcmp(tok->name, name) == 0)
+ return tok;
+ }
+ return NULL;
+}
+
+const char *fts_tokenizer_name(const struct fts_tokenizer *tok)
+{
+ return tok->name;
+}
+
+static void fts_tokenizer_self_reset(struct fts_tokenizer *tok)
+{
+ tok->prev_data = NULL;
+ tok->prev_size = 0;
+ tok->prev_skip = 0;
+ tok->prev_reply_finished = TRUE;
+}
+
+int fts_tokenizer_create(const struct fts_tokenizer *tok_class,
+ struct fts_tokenizer *parent,
+ const char *const *settings,
+ struct fts_tokenizer **tokenizer_r,
+ const char **error_r)
+{
+ struct fts_tokenizer *tok;
+ const char *empty_settings = NULL;
+
+ i_assert(settings == NULL || str_array_length(settings) % 2 == 0);
+
+ if (settings == NULL)
+ settings = &empty_settings;
+
+ if (tok_class->v->create(settings, &tok, error_r) < 0) {
+ *tokenizer_r = NULL;
+ return -1;
+ }
+ tok->refcount = 1;
+ fts_tokenizer_self_reset(tok);
+ if (parent != NULL) {
+ fts_tokenizer_ref(parent);
+ tok->parent = parent;
+ tok->parent_input = buffer_create_dynamic(default_pool, 128);
+ }
+
+ *tokenizer_r = tok;
+ return 0;
+}
+
+void fts_tokenizer_ref(struct fts_tokenizer *tok)
+{
+ i_assert(tok->refcount > 0);
+
+ tok->refcount++;
+}
+
+void fts_tokenizer_unref(struct fts_tokenizer **_tok)
+{
+ struct fts_tokenizer *tok = *_tok;
+
+ i_assert(tok->refcount > 0);
+ *_tok = NULL;
+
+ if (--tok->refcount > 0)
+ return;
+
+ buffer_free(&tok->parent_input);
+ if (tok->parent != NULL)
+ fts_tokenizer_unref(&tok->parent);
+ tok->v->destroy(tok);
+}
+
+static int
+fts_tokenizer_next_self(struct fts_tokenizer *tok,
+ const unsigned char *data, size_t size,
+ const char **token_r, const char **error_r)
+{
+ int ret = 0;
+ size_t skip = 0;
+
+ i_assert(tok->prev_reply_finished ||
+ (data == tok->prev_data && size == tok->prev_size));
+
+ if (tok->prev_reply_finished) {
+ /* whole new data: get the first token */
+ ret = tok->v->next(tok, data, size, &skip, token_r, error_r);
+ } else {
+ /* continuing previous data: skip over the tokens that were
+ already returned from it and get the next token. */
+ i_assert(tok->prev_skip <= size);
+
+ const unsigned char *data_next;
+ if (data != NULL)
+ data_next = data + tok->prev_skip;
+ else {
+ i_assert(tok->prev_skip == 0 && size == 0);
+ data_next = NULL;
+ }
+ ret = tok->v->next(tok, data_next,
+ size - tok->prev_skip, &skip,
+ token_r, error_r);
+ }
+
+ if (ret > 0) {
+ /* A token was successfully returned. There could be more
+ tokens left within the provided data, so remember what part
+ of the data we used so far. */
+ i_assert(skip <= size - tok->prev_skip);
+ tok->prev_data = data;
+ tok->prev_size = size;
+ tok->prev_skip = tok->prev_skip + skip;
+ tok->prev_reply_finished = FALSE;
+ } else if (ret == 0) {
+ /* Need more data to get the next token. The next call will
+ provide a whole new data block, so reset the prev_* state. */
+ fts_tokenizer_self_reset(tok);
+ }
+ return ret;
+}
+
+void fts_tokenizer_reset(struct fts_tokenizer *tok)
+{
+ tok->v->reset(tok);
+ fts_tokenizer_self_reset(tok);
+}
+
+int fts_tokenizer_next(struct fts_tokenizer *tok,
+ const unsigned char *data, size_t size,
+ const char **token_r, const char **error_r)
+{
+ int ret;
+
+ switch (tok->parent_state) {
+ case FTS_TOKENIZER_PARENT_STATE_ADD_DATA:
+ /* Try to get the next token using this tokenizer */
+ ret = fts_tokenizer_next_self(tok, data, size, token_r, error_r);
+ if (ret <= 0) {
+ /* error / more data needed */
+ if (ret == 0 && size == 0 &&
+ tok->finalize_parent_pending) {
+ /* Tokenizer input is being finalized. The
+ child tokenizer is done now, but the parent
+ tokenizer still needs to be finalized. */
+ tok->finalize_parent_pending = FALSE;
+ tok->parent_state =
+ FTS_TOKENIZER_PARENT_STATE_FINALIZE;
+ return fts_tokenizer_next(tok, NULL, 0, token_r, error_r);
+ }
+ break;
+ }
+
+ /* Feed the returned token to the parent tokenizer, if it
+ exists. The parent tokenizer may further split it into
+ smaller pieces. */
+ if (tok->parent == NULL)
+ break;
+ if (tok->skip_parents) {
+ /* Parent tokenizer exists, but it's skipped for now.
+ This can be used by child tokenizers to return a
+ token directly, bypassing the parent tokenizer. */
+ break;
+ }
+ buffer_set_used_size(tok->parent_input, 0);
+ buffer_append(tok->parent_input, *token_r, strlen(*token_r));
+ tok->parent_state++;
+ /* fall through */
+ case FTS_TOKENIZER_PARENT_STATE_NEXT_OUTPUT:
+ /* Return the next token from parent tokenizer */
+ ret = fts_tokenizer_next(tok->parent, tok->parent_input->data,
+ tok->parent_input->used, token_r, error_r);
+ if (ret != 0)
+ break;
+ tok->parent_state++;
+ /* fall through */
+ case FTS_TOKENIZER_PARENT_STATE_FINALIZE:
+ /* No more input is coming from the child tokenizer. Return the
+ final token(s) from the parent tokenizer. */
+ if (!tok->stream_to_parents || size == 0) {
+ ret = fts_tokenizer_next(tok->parent, NULL, 0,
+ token_r, error_r);
+ if (ret != 0)
+ break;
+ } else {
+ tok->finalize_parent_pending = TRUE;
+ }
+ /* We're finished handling the previous child token. See if
+ there are more child tokens available with this same data
+ input. */
+ tok->parent_state = FTS_TOKENIZER_PARENT_STATE_ADD_DATA;
+ return fts_tokenizer_next(tok, data, size, token_r, error_r);
+ default:
+ i_unreached();
+ }
+ /* we must not be returning empty tokens */
+ i_assert(ret <= 0 || (*token_r)[0] != '\0');
+ return ret;
+}
+
+int fts_tokenizer_final(struct fts_tokenizer *tok, const char **token_r,
+ const char **error_r)
+{
+ return fts_tokenizer_next(tok, NULL, 0, token_r, error_r);
+}
diff --git a/src/lib-fts/fts-tokenizer.h b/src/lib-fts/fts-tokenizer.h
new file mode 100644
index 0000000..c202cf7
--- /dev/null
+++ b/src/lib-fts/fts-tokenizer.h
@@ -0,0 +1,87 @@
+#ifndef FTS_TOKENIZER_H
+#define FTS_TOKENIZER_H
+
+/*
+ Settings are given in the form of a const char * const *settings =
+ {"key, "value", "key2", "value2", NULL} array of string pairs. Some
+ keys, like "no_parent" and "search" are a sort of boolean and the
+ value does not matter, just mentioning the key enables the functionality.
+ The array has to be NULL terminated.
+*/
+/* Email address header tokenizer that returns "user@domain.org" input as
+ "user@domain.org" token as well as passing it through to the parent
+ (generic) tokenizer, which also returns "user", "domain" and "org".
+ This allows searching the mails with their individual components, but also
+ allows doing an explicit "user@domain" search, which returns only mails
+ matching that exact address (instead of e.g. a mail with both user@domain2
+ and user2@domain words). */
+/* Settings:
+ "no_parent", Return only our tokens, no data for parent to process.
+ Defaults to disabled. Should normally not be needed.
+
+ "search" Remove addresses from parent data stream, so they are not processed
+ further. Defaults to disabled. Enable by defining the keyword (and any
+ value). */
+extern const struct fts_tokenizer *fts_tokenizer_email_address;
+
+/* Generic email content tokenizer. Cuts text into tokens. */
+/* Settings:
+ "maxlen" Maximum length of token, before an arbitrary cut off is made.
+ Defaults to FTS_DEFAULT_TOKEN_MAX_LENGTH.
+
+ "algorithm", accepted values are "simple" or "tr29". Defines the
+ method for looking for word boundaries. Simple is faster and will
+ work for many texts, especially those using latin alphabets, but
+ leaves corner cases. The tr29 implements a version of Unicode
+ technical report 29 word boundary lookup. It might work better with
+ e.g. texts containing Katakana or hebrew characters, but it is not
+ possible to use a single algorithm for all existing languages. It
+ is also significantly slower than simple. The algorithms also
+ differ in some details, e.g. simple will cut "a.b" and tr29 will
+ not. The default is "simple" */
+extern const struct fts_tokenizer *fts_tokenizer_generic;
+
+/*
+ Tokenizing workflow, find --> create --> filter --> destroy.
+ Do init before first use and deinit after all done.
+ */
+
+/* Register all built-in tokenizers. */
+void fts_tokenizers_init(void);
+void fts_tokenizers_deinit(void);
+
+const struct fts_tokenizer *fts_tokenizer_find(const char *name);
+
+/* Create a new tokenizer. The settings are described above. */
+int fts_tokenizer_create(const struct fts_tokenizer *tok_class,
+ struct fts_tokenizer *parent,
+ const char *const *settings,
+ struct fts_tokenizer **tokenizer_r,
+ const char **error_r);
+void fts_tokenizer_ref(struct fts_tokenizer *tok);
+void fts_tokenizer_unref(struct fts_tokenizer **tok);
+
+/* Reset FTS tokenizer state */
+void fts_tokenizer_reset(struct fts_tokenizer *tok);
+
+/*
+ Returns 1 if *token_r was returned, 0 if more data is needed, -1 on error.
+
+ This function should be called with the same data+size until it
+ returns 0. After that fts_tokenizer_final() should be called until it
+ returns 0 to flush out the final token(s).
+
+ data must contain only valid complete UTF-8 sequences, but otherwise it
+ may be broken into however small pieces. (Input to this function typically
+ comes from message-decoder, which returns only complete UTF-8 sequences.) */
+
+int fts_tokenizer_next(struct fts_tokenizer *tok,
+ const unsigned char *data, size_t size,
+ const char **token_r, const char **error_r);
+/* Returns same as fts_tokenizer_next(). */
+int fts_tokenizer_final(struct fts_tokenizer *tok, const char **token_r,
+ const char **error_r);
+
+const char *fts_tokenizer_name(const struct fts_tokenizer *tok);
+
+#endif
diff --git a/src/lib-fts/stopwords/stopwords_da.txt b/src/lib-fts/stopwords/stopwords_da.txt
new file mode 100644
index 0000000..c4cfd9c
--- /dev/null
+++ b/src/lib-fts/stopwords/stopwords_da.txt
@@ -0,0 +1,109 @@
+ | From svn.tartarus.org/snowball/trunk/website/algorithms/danish/stop.txt
+ | This file is distributed under the BSD License.
+ | See http://snowball.tartarus.org/license.php
+ | Also see http://www.opensource.org/licenses/bsd-license.html
+ | - Encoding was converted to UTF-8.
+ | - This notice was added.
+ |
+
+ | A Danish stop word list. Comments begin with vertical bar. Each stop
+ | word is at the start of a line.
+
+ | This is a ranked list (commonest to rarest) of stopwords derived from
+ | a large text sample.
+
+
+og | and
+i | in
+jeg | I
+det | that (dem. pronoun)/it (pers. pronoun)
+at | that (in front of a sentence)/to (with infinitive)
+en | a/an
+den | it (pers. pronoun)/that (dem. pronoun)
+til | to/at/for/until/against/by/of/into, more
+er | present tense of "to be"
+som | who, as
+på | on/upon/in/on/at/to/after/of/with/for, on
+de | they
+med | with/by/in, along
+han | he
+af | of/by/from/off/for/in/with/on, off
+for | at/for/to/from/by/of/ago, in front/before, because
+ikke | not
+der | who/which, there/those
+var | past tense of "to be"
+mig | me/myself
+sig | oneself/himself/herself/itself/themselves
+men | but
+et | a/an/one, one (number), someone/somebody/one
+har | present tense of "to have"
+om | round/about/for/in/a, about/around/down, if
+vi | we
+min | my
+havde | past tense of "to have"
+ham | him
+hun | she
+nu | now
+over | over/above/across/by/beyond/past/on/about, over/past
+da | then, when/as/since
+fra | from/off/since, off, since
+du | you
+ud | out
+sin | his/her/its/one's
+dem | them
+os | us/ourselves
+op | up
+man | you/one
+hans | his
+hvor | where
+eller | or
+hvad | what
+skal | must/shall etc.
+selv | myself/youself/herself/ourselves etc., even
+her | here
+alle | all/everyone/everybody etc.
+vil | will (verb)
+blev | past tense of "to stay/to remain/to get/to become"
+kunne | could
+ind | in
+når | when
+være | present tense of "to be"
+dog | however/yet/after all
+noget | something
+ville | would
+jo | you know/you see (adv), yes
+deres | their/theirs
+efter | after/behind/according to/for/by/from, later/afterwards
+ned | down
+skulle | should
+denne | this
+end | than
+dette | this
+mit | my/mine
+også | also
+under | under/beneath/below/during, below/underneath
+have | have
+dig | you
+anden | other
+hende | her
+mine | my
+alt | everything
+meget | much/very, plenty of
+sit | his, her, its, one's
+sine | his, her, its, one's
+vor | our
+mod | against
+disse | these
+hvis | if
+din | your/yours
+nogle | some
+hos | by/at
+blive | be/become
+mange | many
+ad | by/through
+bliver | present tense of "to be/to become"
+hendes | her/hers
+været | be
+thi | for (conj)
+jer | you
+sådan | such, like this/like that
diff --git a/src/lib-fts/stopwords/stopwords_de.txt b/src/lib-fts/stopwords/stopwords_de.txt
new file mode 100644
index 0000000..786284d
--- /dev/null
+++ b/src/lib-fts/stopwords/stopwords_de.txt
@@ -0,0 +1,293 @@
+ | From svn.tartarus.org/snowball/trunk/website/algorithms/german/stop.txt
+ | This file is distributed under the BSD License.
+ | See http://snowball.tartarus.org/license.php
+ | Also see http://www.opensource.org/licenses/bsd-license.html
+ | - Encoding was converted to UTF-8.
+ | - This notice was added.
+ |
+
+ | A German stop word list. Comments begin with vertical bar. Each stop
+ | word is at the start of a line.
+
+ | The number of forms in this list is reduced significantly by passing it
+ | through the German stemmer.
+
+
+aber | but
+
+alle | all
+allem
+allen
+aller
+alles
+
+als | than, as
+also | so
+am | an + dem
+an | at
+
+ander | other
+andere
+anderem
+anderen
+anderer
+anderes
+anderm
+andern
+anderr
+anders
+
+auch | also
+auf | on
+aus | out of
+bei | by
+bin | am
+bis | until
+bist | art
+da | there
+damit | with it
+dann | then
+
+der | the
+den
+des
+dem
+die
+das
+
+daß | that
+
+derselbe | the same
+derselben
+denselben
+desselben
+demselben
+dieselbe
+dieselben
+dasselbe
+
+dazu | to that
+
+dein | thy
+deine
+deinem
+deinen
+deiner
+deines
+
+denn | because
+
+derer | of those
+dessen | of him
+
+dich | thee
+dir | to thee
+du | thou
+
+dies | this
+diese
+diesem
+diesen
+dieser
+dieses
+
+
+doch | (several meanings)
+dort | (over) there
+
+
+durch | through
+
+ein | a
+eine
+einem
+einen
+einer
+eines
+
+einig | some
+einige
+einigem
+einigen
+einiger
+einiges
+
+einmal | once
+
+er | he
+ihn | him
+ihm | to him
+
+es | it
+etwas | something
+
+euer | your
+eure
+eurem
+euren
+eurer
+eures
+
+für | for
+gegen | towards
+gewesen | p.p. of sein
+hab | have
+habe | have
+haben | have
+hat | has
+hatte | had
+hatten | had
+hier | here
+hin | there
+hinter | behind
+
+ich | I
+mich | me
+mir | to me
+
+
+ihr | you, to her
+ihre
+ihrem
+ihren
+ihrer
+ihres
+euch | to you
+
+im | in + dem
+in | in
+indem | while
+ins | in + das
+ist | is
+
+jede | each, every
+jedem
+jeden
+jeder
+jedes
+
+jene | that
+jenem
+jenen
+jener
+jenes
+
+jetzt | now
+kann | can
+
+kein | no
+keine
+keinem
+keinen
+keiner
+keines
+
+können | can
+könnte | could
+machen | do
+man | one
+
+manche | some, many a
+manchem
+manchen
+mancher
+manches
+
+mein | my
+meine
+meinem
+meinen
+meiner
+meines
+
+mit | with
+muss | must
+musste | had to
+nach | to(wards)
+nicht | not
+nichts | nothing
+noch | still, yet
+nun | now
+nur | only
+ob | whether
+oder | or
+ohne | without
+sehr | very
+
+sein | his
+seine
+seinem
+seinen
+seiner
+seines
+
+selbst | self
+sich | herself
+
+sie | they, she
+ihnen | to them
+
+sind | are
+so | so
+
+solche | such
+solchem
+solchen
+solcher
+solches
+
+soll | shall
+sollte | should
+sondern | but
+sonst | else
+über | over
+um | about, around
+und | and
+
+uns | us
+unse
+unsem
+unsen
+unser
+unses
+
+unter | under
+viel | much
+vom | von + dem
+von | from
+vor | before
+während | while
+war | was
+waren | were
+warst | wast
+was | what
+weg | away, off
+weil | because
+weiter | further
+
+welche | which
+welchem
+welchen
+welcher
+welches
+
+wenn | when
+werde | will
+werden | will
+wie | how
+wieder | again
+will | want
+wir | we
+wird | will
+wirst | willst
+wo | where
+wollen | want
+wollte | wanted
+würde | would
+würden | would
+zu | to
+zum | zu + dem
+zur | zu + der
+zwar | indeed
+zwischen | between
+
diff --git a/src/lib-fts/stopwords/stopwords_en.txt b/src/lib-fts/stopwords/stopwords_en.txt
new file mode 100644
index 0000000..2c164c0
--- /dev/null
+++ b/src/lib-fts/stopwords/stopwords_en.txt
@@ -0,0 +1,54 @@
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# a couple of test stopwords to test that the words are really being
+# configured from this file:
+stopworda
+stopwordb
+
+# Standard english stop words taken from Lucene's StopAnalyzer
+a
+an
+and
+are
+as
+at
+be
+but
+by
+for
+if
+in
+into
+is
+it
+no
+not
+of
+on
+or
+such
+that
+the
+their
+then
+there
+these
+they
+this
+to
+was
+will
+with
diff --git a/src/lib-fts/stopwords/stopwords_es.txt b/src/lib-fts/stopwords/stopwords_es.txt
new file mode 100644
index 0000000..ed9fdca
--- /dev/null
+++ b/src/lib-fts/stopwords/stopwords_es.txt
@@ -0,0 +1,355 @@
+ | From svn.tartarus.org/snowball/trunk/website/algorithms/spanish/stop.txt
+ | This file is distributed under the BSD License.
+ | See http://snowball.tartarus.org/license.php
+ | Also see http://www.opensource.org/licenses/bsd-license.html
+ | - Encoding was converted to UTF-8.
+ | - This notice was added.
+ |
+
+ | A Spanish stop word list. Comments begin with vertical bar. Each stop
+ | word is at the start of a line.
+
+
+ | The following is a ranked list (commonest to rarest) of stopwords
+ | deriving from a large sample of text.
+
+ | Extra words have been added at the end.
+
+de | from, of
+la | the, her
+que | who, that
+el | the
+en | in
+y | and
+a | to
+los | the, them
+del | de + el
+se | himself, from him etc
+las | the, them
+por | for, by, etc
+un | a
+para | for
+con | with
+no | no
+una | a
+su | his, her
+al | a + el
+ | es from SER
+lo | him
+como | how
+más | more
+pero | pero
+sus | su plural
+le | to him, her
+ya | already
+o | or
+ | fue from SER
+este | this
+ | ha from HABER
+sí | himself etc
+porque | because
+esta | this
+ | son from SER
+entre | between
+ | está from ESTAR
+cuando | when
+muy | very
+sin | without
+sobre | on
+ | ser from SER
+ | tiene from TENER
+también | also
+me | me
+hasta | until
+hay | there is/are
+donde | where
+ | han from HABER
+quien | whom, that
+ | están from ESTAR
+ | estado from ESTAR
+desde | from
+todo | all
+nos | us
+durante | during
+ | estados from ESTAR
+todos | all
+uno | a
+les | to them
+ni | nor
+contra | against
+otros | other
+ | fueron from SER
+ese | that
+eso | that
+ | había from HABER
+ante | before
+ellos | they
+e | and (variant of y)
+esto | this
+mí | me
+antes | before
+algunos | some
+qué | what?
+unos | a
+yo | I
+otro | other
+otras | other
+otra | other
+él | he
+tanto | so much, many
+esa | that
+estos | these
+mucho | much, many
+quienes | who
+nada | nothing
+muchos | many
+cual | who
+ | sea from SER
+poco | few
+ella | she
+estar | to be
+ | haber from HABER
+estas | these
+ | estaba from ESTAR
+ | estamos from ESTAR
+algunas | some
+algo | something
+nosotros | we
+
+ | other forms
+
+mi | me
+mis | mi plural
+tú | thou
+te | thee
+ti | thee
+tu | thy
+tus | tu plural
+ellas | they
+nosotras | we
+vosotros | you
+vosotras | you
+os | you
+mío | mine
+mía |
+míos |
+mías |
+tuyo | thine
+tuya |
+tuyos |
+tuyas |
+suyo | his, hers, theirs
+suya |
+suyos |
+suyas |
+nuestro | ours
+nuestra |
+nuestros |
+nuestras |
+vuestro | yours
+vuestra |
+vuestros |
+vuestras |
+esos | those
+esas | those
+
+ | forms of estar, to be (not including the infinitive):
+estoy
+estás
+está
+estamos
+estáis
+están
+esté
+estés
+estemos
+estéis
+estén
+estaré
+estarás
+estará
+estaremos
+estaréis
+estarán
+estaría
+estarías
+estaríamos
+estaríais
+estarían
+estaba
+estabas
+estábamos
+estabais
+estaban
+estuve
+estuviste
+estuvo
+estuvimos
+estuvisteis
+estuvieron
+estuviera
+estuvieras
+estuviéramos
+estuvierais
+estuvieran
+estuviese
+estuvieses
+estuviésemos
+estuvieseis
+estuviesen
+estando
+estado
+estada
+estados
+estadas
+estad
+
+ | forms of haber, to have (not including the infinitive):
+he
+has
+ha
+hemos
+habéis
+han
+haya
+hayas
+hayamos
+hayáis
+hayan
+habré
+habrás
+habrá
+habremos
+habréis
+habrán
+habría
+habrías
+habríamos
+habríais
+habrían
+había
+habías
+habíamos
+habíais
+habían
+hube
+hubiste
+hubo
+hubimos
+hubisteis
+hubieron
+hubiera
+hubieras
+hubiéramos
+hubierais
+hubieran
+hubiese
+hubieses
+hubiésemos
+hubieseis
+hubiesen
+habiendo
+habido
+habida
+habidos
+habidas
+
+ | forms of ser, to be (not including the infinitive):
+soy
+eres
+es
+somos
+sois
+son
+sea
+seas
+seamos
+seáis
+sean
+seré
+serás
+será
+seremos
+seréis
+serán
+sería
+serías
+seríamos
+seríais
+serían
+era
+eras
+éramos
+erais
+eran
+fui
+fuiste
+fue
+fuimos
+fuisteis
+fueron
+fuera
+fueras
+fuéramos
+fuerais
+fueran
+fuese
+fueses
+fuésemos
+fueseis
+fuesen
+siendo
+sido
+ | sed also means 'thirst'
+
+ | forms of tener, to have (not including the infinitive):
+tengo
+tienes
+tiene
+tenemos
+tenéis
+tienen
+tenga
+tengas
+tengamos
+tengáis
+tengan
+tendré
+tendrás
+tendrá
+tendremos
+tendréis
+tendrán
+tendría
+tendrías
+tendríamos
+tendríais
+tendrían
+tenía
+tenías
+teníamos
+teníais
+tenían
+tuve
+tuviste
+tuvo
+tuvimos
+tuvisteis
+tuvieron
+tuviera
+tuvieras
+tuviéramos
+tuvierais
+tuvieran
+tuviese
+tuvieses
+tuviésemos
+tuvieseis
+tuviesen
+teniendo
+tenido
+tenida
+tenidos
+tenidas
+tened
+
diff --git a/src/lib-fts/stopwords/stopwords_fi.txt b/src/lib-fts/stopwords/stopwords_fi.txt
new file mode 100644
index 0000000..95c74f2
--- /dev/null
+++ b/src/lib-fts/stopwords/stopwords_fi.txt
@@ -0,0 +1,260 @@
+ | From svn.tartarus.org/snowball/trunk/website/algorithms/finnish/stop.txt
+ | This file is distributed under the BSD License.
+ | See http://snowball.tartarus.org/license.php
+ | Also see http://www.opensource.org/licenses/bsd-license.html
+ | - Encoding was converted to UTF-8.
+ | - This notice was added.
+
+| forms of BE
+
+olla
+olen
+olet
+on
+olemme
+olette
+ovat
+ole | negative form
+
+oli
+olisi
+olisit
+olisin
+olisimme
+olisitte
+olisivat
+olit
+olin
+olimme
+olitte
+olivat
+ollut
+olleet
+
+en | negation
+et
+ei
+emme
+ette
+eivät
+
+|Words below in the following cases:
+|Nom Gen Acc Part Iness Elat Illat Adess Ablat Allat Ess Trans
+minä | I
+minun | I
+minut | I
+minua | I
+minussa | I
+minusta | I
+minuun | I
+minulla | I
+minulta | I
+minulle | I
+sinä | you
+sinun | you
+sinut | you
+sinua | you
+sinussa | you
+sinusta | you
+sinuun | you
+sinulla | you
+sinulta | you
+sinulle | you
+hän | he she
+hänen | he she
+hänet | he she
+häntä | he she
+hänessä | he she
+hänestä | he she
+häneen | he she
+hänellä | he she
+häneltä | he she
+hänelle | he she
+me | we
+meidän | we
+meidät | we
+meitä | we
+meissä | we
+meistä | we
+meihin | we
+meillä | we
+meiltä | we
+meille | we
+te | you
+teidän | you
+teidät | you
+teitä | you
+teissä | you
+teistä | you
+teihin | you
+teillä | you
+teiltä | you
+teille | you
+he | they
+heidän | they
+heidät | they
+heitä | they
+heissä | they
+heistä | they
+heihin | they
+heillä | they
+heiltä | they
+heille | they
+tämä | this
+tämän | this
+tätä | this
+tässä | this
+tästä | this
+tähän | this
+tallä | this
+tältä | this
+tälle | this
+tänä | this
+täksi | this
+tuo | that
+tuon | that
+tuotä | that
+tuossa | that
+tuosta | that
+tuohon | that
+tuolla | that
+tuolta | that
+tuolle | that
+tuona | that
+tuoksi | that
+se | it
+sen | it
+sitä | it
+siinä | it
+siitä | it
+siihen | it
+sillä | it
+siltä | it
+sille | it
+sinä | it
+siksi | it
+nämä | these
+näiden | these
+näitä | these
+näissä | these
+näistä | these
+näihin | these
+näillä | these
+näiltä | these
+näille | these
+näinä | these
+näiksi | these
+nuo | those
+noiden | those
+noita | those
+noissa | those
+noista | those
+noihin | those
+noilla | those
+noilta | those
+noille | those
+noina | those
+noiksi | those
+ne | they
+niiden | they
+niitä | they
+niissä | they
+niistä | they
+niihin | they
+niillä | they
+niiltä | they
+niille | they
+niinä | they
+niiksi | they
+kuka | who
+kenen | who
+kenet | who
+ketä | who
+kenessä | who
+kenestä | who
+keneen | who
+kenellä | who
+keneltä | who
+kenelle | who
+kenenä | who
+keneksi | who
+ketkä | who (pl)
+keiden | who (pl)
+ketkä | who (pl)
+keitä | who (pl)
+keissä | who (pl)
+keistä | who (pl)
+keihin | who (pl)
+keillä | who (pl)
+keiltä | who (pl)
+keille | who (pl)
+keinä | who (pl)
+keiksi | who (pl)
+mikä | which what
+minkä | which what
+minkä | which what
+mitä | which what
+missä | which what
+mistä | which what
+mihin | which what
+millä | which what
+miltä | which what
+mille | which what
+minä | which what
+miksi | which what
+mitkä | which what (pl)
+joka | who which
+jonka | who which
+jota | who which
+jossa | who which
+josta | who which
+johon | who which
+jolla | who which
+jolta | who which
+jolle | who which
+jona | who which
+joksi | who which
+jotka | who which (pl)
+joiden | who which (pl)
+joita | who which (pl)
+joissa | who which (pl)
+joista | who which (pl)
+joihin | who which (pl)
+joilla | who which (pl)
+joilta | who which (pl)
+joille | who which (pl)
+joina | who which (pl)
+joiksi | who which (pl)
+
+| conjunctions
+
+että | that
+ja | and
+jos | if
+koska | because
+kuin | than
+mutta | but
+niin | so
+sekä | and
+sillä | for
+tai | or
+vaan | but
+vai | or
+vaikka | although
+
+
+| prepositions
+
+kanssa | with
+mukaan | according to
+noin | about
+poikki | across
+yli | over, across
+
+| other
+
+kun | when
+niin | so
+nyt | now
+itse | self
+
diff --git a/src/lib-fts/stopwords/stopwords_fr.txt b/src/lib-fts/stopwords/stopwords_fr.txt
new file mode 100644
index 0000000..20d12cb
--- /dev/null
+++ b/src/lib-fts/stopwords/stopwords_fr.txt
@@ -0,0 +1,184 @@
+ | From svn.tartarus.org/snowball/trunk/website/algorithms/french/stop.txt
+ | This file is distributed under the BSD License.
+ | See http://snowball.tartarus.org/license.php
+ | Also see http://www.opensource.org/licenses/bsd-license.html
+ | - Encoding was converted to UTF-8.
+ | - This notice was added.
+
+ | A French stop word list. Comments begin with vertical bar. Each stop
+ | word is at the start of a line.
+
+au | a + le
+aux | a + les
+avec | with
+ce | this
+ces | these
+dans | with
+de | of
+des | de + les
+du | de + le
+elle | she
+en | `of them' etc
+et | and
+eux | them
+il | he
+je | I
+la | the
+le | the
+leur | their
+lui | him
+ma | my (fem)
+mais | but
+me | me
+même | same; as in moi-même (myself) etc
+mes | me (pl)
+moi | me
+mon | my (masc)
+ne | not
+nos | our (pl)
+notre | our
+nous | we
+on | one
+ou | where
+par | by
+pas | not
+pour | for
+qu | que before vowel
+que | that
+qui | who
+sa | his, her (fem)
+se | oneself
+ses | his (pl)
+son | his, her (masc)
+sur | on
+ta | thy (fem)
+te | thee
+tes | thy (pl)
+toi | thee
+ton | thy (masc)
+tu | thou
+un | a
+une | a
+vos | your (pl)
+votre | your
+vous | you
+
+ | single letter forms
+
+c | c'
+d | d'
+j | j'
+l | l'
+à | to, at
+m | m'
+n | n'
+s | s'
+t | t'
+y | there
+
+ | forms of être (not including the infinitive):
+été
+étée
+étées
+étés
+étant
+suis
+es
+est
+sommes
+êtes
+sont
+serai
+seras
+sera
+serons
+serez
+seront
+serais
+serait
+serions
+seriez
+seraient
+étais
+était
+étions
+étiez
+étaient
+fus
+fut
+fûmes
+fûtes
+furent
+sois
+soit
+soyons
+soyez
+soient
+fusse
+fusses
+fût
+fussions
+fussiez
+fussent
+
+ | forms of avoir (not including the infinitive):
+ayant
+eu
+eue
+eues
+eus
+ai
+as
+avons
+avez
+ont
+aurai
+auras
+aura
+aurons
+aurez
+auront
+aurais
+aurait
+aurions
+auriez
+auraient
+avais
+avait
+avions
+aviez
+avaient
+eut
+eûmes
+eûtes
+eurent
+aie
+aies
+ait
+ayons
+ayez
+aient
+eusse
+eusses
+eût
+eussions
+eussiez
+eussent
+
+ | Later additions (from Jean-Christophe Deschamps)
+ceci | this
+cela | that
+celà | that
+cet | this
+cette | this
+ici | here
+ils | they
+les | the (pl)
+leurs | their (pl)
+quel | which
+quels | which
+quelle | which
+quelles | which
+sans | without
+soi | oneself
+
diff --git a/src/lib-fts/stopwords/stopwords_it.txt b/src/lib-fts/stopwords/stopwords_it.txt
new file mode 100644
index 0000000..a9e5f67
--- /dev/null
+++ b/src/lib-fts/stopwords/stopwords_it.txt
@@ -0,0 +1,302 @@
+ | From svn.tartarus.org/snowball/trunk/website/algorithms/italian/stop.txt
+ | This file is distributed under the BSD License.
+ | See http://snowball.tartarus.org/license.php
+ | Also see http://www.opensource.org/licenses/bsd-license.html
+ | - Encoding was converted to UTF-8.
+ | - This notice was added.
+ |
+
+ | An Italian stop word list. Comments begin with vertical bar. Each stop
+ | word is at the start of a line.
+
+ad | a (to) before vowel
+al | a + il
+allo | a + lo
+ai | a + i
+agli | a + gli
+all | a + l'
+agl | a + gl'
+alla | a + la
+alle | a + le
+con | with
+col | con + il
+coi | con + i (forms collo, cogli etc are now very rare)
+da | from
+dal | da + il
+dallo | da + lo
+dai | da + i
+dagli | da + gli
+dall | da + l'
+dagl | da + gll'
+dalla | da + la
+dalle | da + le
+di | of
+del | di + il
+dello | di + lo
+dei | di + i
+degli | di + gli
+dell | di + l'
+degl | di + gl'
+della | di + la
+delle | di + le
+in | in
+nel | in + el
+nello | in + lo
+nei | in + i
+negli | in + gli
+nell | in + l'
+negl | in + gl'
+nella | in + la
+nelle | in + le
+su | on
+sul | su + il
+sullo | su + lo
+sui | su + i
+sugli | su + gli
+sull | su + l'
+sugl | su + gl'
+sulla | su + la
+sulle | su + le
+per | through, by
+tra | among
+contro | against
+io | I
+tu | thou
+lui | he
+lei | she
+noi | we
+voi | you
+loro | they
+mio | my
+mia |
+miei |
+mie |
+tuo |
+tua |
+tuoi | thy
+tue |
+suo |
+sua |
+suoi | his, her
+sue |
+nostro | our
+nostra |
+nostri |
+nostre |
+vostro | your
+vostra |
+vostri |
+vostre |
+mi | me
+ti | thee
+ci | us, there
+vi | you, there
+lo | him, the
+la | her, the
+li | them
+le | them, the
+gli | to him, the
+ne | from there etc
+il | the
+un | a
+uno | a
+una | a
+ma | but
+ed | and
+se | if
+perché | why, because
+anche | also
+come | how
+dov | where (as dov')
+dove | where
+che | who, that
+chi | who
+cui | whom
+non | not
+più | more
+quale | who, that
+quanto | how much
+quanti |
+quanta |
+quante |
+quello | that
+quelli |
+quella |
+quelle |
+questo | this
+questi |
+questa |
+queste |
+si | yes
+tutto | all
+tutti | all
+
+ | single letter forms:
+
+a | at
+c | as c' for ce or ci
+e | and
+i | the
+l | as l'
+o | or
+
+ | forms of avere, to have (not including the infinitive):
+
+ho
+hai
+ha
+abbiamo
+avete
+hanno
+abbia
+abbiate
+abbiano
+avrò
+avrai
+avrà
+avremo
+avrete
+avranno
+avrei
+avresti
+avrebbe
+avremmo
+avreste
+avrebbero
+avevo
+avevi
+aveva
+avevamo
+avevate
+avevano
+ebbi
+avesti
+ebbe
+avemmo
+aveste
+ebbero
+avessi
+avesse
+avessimo
+avessero
+avendo
+avuto
+avuta
+avuti
+avute
+
+ | forms of essere, to be (not including the infinitive):
+sono
+sei
+siamo
+siete
+sia
+siate
+siano
+sarò
+sarai
+sarà
+saremo
+sarete
+saranno
+sarei
+saresti
+sarebbe
+saremmo
+sareste
+sarebbero
+ero
+eri
+era
+eravamo
+eravate
+erano
+fui
+fosti
+fu
+fummo
+foste
+furono
+fossi
+fosse
+fossimo
+fossero
+essendo
+
+ | forms of fare, to do (not including the infinitive, fa, fat-):
+faccio
+fai
+facciamo
+fanno
+faccia
+facciate
+facciano
+farò
+farai
+farà
+faremo
+farete
+faranno
+farei
+faresti
+farebbe
+faremmo
+fareste
+farebbero
+facevo
+facevi
+faceva
+facevamo
+facevate
+facevano
+feci
+facesti
+fece
+facemmo
+faceste
+fecero
+facessi
+facesse
+facessimo
+facessero
+facendo
+
+ | forms of stare, to be (not including the infinitive):
+sto
+stai
+sta
+stiamo
+stanno
+stia
+stiate
+stiano
+starò
+starai
+starà
+staremo
+starete
+staranno
+starei
+staresti
+starebbe
+staremmo
+stareste
+starebbero
+stavo
+stavi
+stava
+stavamo
+stavate
+stavano
+stetti
+stesti
+stette
+stemmo
+steste
+stettero
+stessi
+stesse
+stessimo
+stessero
+stando
diff --git a/src/lib-fts/stopwords/stopwords_malformed.txt b/src/lib-fts/stopwords/stopwords_malformed.txt
new file mode 100644
index 0000000..5899230
--- /dev/null
+++ b/src/lib-fts/stopwords/stopwords_malformed.txt
@@ -0,0 +1,2866 @@
+
+
+
+
+<!DOCTYPE html>
+<html lang="en" class="">
+ <head prefix="og: http://ogp.me/ns# fb: http://ogp.me/ns/fb# object: http://ogp.me/ns/object# article: http://ogp.me/ns/article# profile: http://ogp.me/ns/profile#">
+ <meta charset='utf-8'>
+
+ <link crossorigin="anonymous" href="https://assets-cdn.github.com/assets/github-5a3552c77198dc1a3f6ff70409e1b589ad5438e7449023c3b68ca5f49e639fb0.css" media="all" rel="stylesheet" />
+ <link crossorigin="anonymous" href="https://assets-cdn.github.com/assets/github2-e7d851ea8a5986690487ea49678059eb4c52bb84e2b4b13d9cb7b93f6115e21c.css" media="all" rel="stylesheet" />
+
+
+
+
+ <link as="script" href="https://assets-cdn.github.com/assets/frameworks-96e29b697be2d5e4c062e40ce2dab35f90b4cd378cfbbcd6dd51a3f280e7d426.js" rel="preload" />
+ <link as="script" href="https://assets-cdn.github.com/assets/github-061f27987895ca9e650b5c852acbea14f7b6e360f8d0a2bf2e5692911aa3926c.js" rel="preload" />
+
+ <meta http-equiv="X-UA-Compatible" content="IE=edge">
+ <meta http-equiv="Content-Language" content="en">
+ <meta name="viewport" content="width=1020">
+
+
+ <title>lucene-solr/stopwords_de.txt at master · apache/lucene-solr · GitHub</title>
+ <link rel="search" type="application/opensearchdescription+xml" href="/opensearch.xml" title="GitHub">
+ <link rel="fluid-icon" href="https://github.com/fluidicon.png" title="GitHub">
+ <link rel="apple-touch-icon" href="/apple-touch-icon.png">
+ <link rel="apple-touch-icon" sizes="57x57" href="/apple-touch-icon-57x57.png">
+ <link rel="apple-touch-icon" sizes="60x60" href="/apple-touch-icon-60x60.png">
+ <link rel="apple-touch-icon" sizes="72x72" href="/apple-touch-icon-72x72.png">
+ <link rel="apple-touch-icon" sizes="76x76" href="/apple-touch-icon-76x76.png">
+ <link rel="apple-touch-icon" sizes="114x114" href="/apple-touch-icon-114x114.png">
+ <link rel="apple-touch-icon" sizes="120x120" href="/apple-touch-icon-120x120.png">
+ <link rel="apple-touch-icon" sizes="144x144" href="/apple-touch-icon-144x144.png">
+ <link rel="apple-touch-icon" sizes="152x152" href="/apple-touch-icon-152x152.png">
+ <link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon-180x180.png">
+ <meta property="fb:app_id" content="1401488693436528">
+
+ <meta content="https://avatars1.githubusercontent.com/u/47359?v=3&amp;s=400" name="twitter:image:src" /><meta content="@github" name="twitter:site" /><meta content="summary" name="twitter:card" /><meta content="apache/lucene-solr" name="twitter:title" /><meta content="lucene-solr - Mirror of Apache Lucene + Solr" name="twitter:description" />
+ <meta content="https://avatars1.githubusercontent.com/u/47359?v=3&amp;s=400" property="og:image" /><meta content="GitHub" property="og:site_name" /><meta content="object" property="og:type" /><meta content="apache/lucene-solr" property="og:title" /><meta content="https://github.com/apache/lucene-solr" property="og:url" /><meta content="lucene-solr - Mirror of Apache Lucene + Solr" property="og:description" />
+ <meta name="browser-stats-url" content="https://api.github.com/_private/browser/stats">
+ <meta name="browser-errors-url" content="https://api.github.com/_private/browser/errors">
+ <link rel="assets" href="https://assets-cdn.github.com/">
+
+ <meta name="pjax-timeout" content="1000">
+
+
+ <meta name="msapplication-TileImage" content="/windows-tile.png">
+ <meta name="msapplication-TileColor" content="#ffffff">
+ <meta name="selected-link" value="repo_source" data-pjax-transient>
+
+ <meta name="google-site-verification" content="KT5gs8h0wvaagLKAVWq8bbeNwnZZK1r1XQysX3xurLU">
+<meta name="google-site-verification" content="ZzhVyEFwb7w3e0-uOTltm8Jsck2F5StVihD0exw2fsA">
+ <meta name="google-analytics" content="UA-3769691-2">
+
+<meta content="collector.githubapp.com" name="octolytics-host" /><meta content="github" name="octolytics-app-id" /><meta content="D906D48A:6B89:225E586:56D82F8C" name="octolytics-dimension-request_id" />
+<meta content="/&lt;user-name&gt;/&lt;repo-name&gt;/blob/show" data-pjax-transient="true" name="analytics-location" />
+
+
+
+ <meta class="js-ga-set" name="dimension1" content="Logged Out">
+
+
+
+ <meta name="hostname" content="github.com">
+ <meta name="user-login" content="">
+
+ <meta name="expected-hostname" content="github.com">
+ <meta name="js-proxy-site-detection-payload" content="N2EwMDI4NDlkOGY4ZWY5OWJjYWQ1NDQ1NjRhOWMxNDg0NzNmN2IwYmRjOWIxMWUwOTY4NmJjZGRjMGI1YjE1NXx7InJlbW90ZV9hZGRyZXNzIjoiMjE3LjYuMjEyLjEzOCIsInJlcXVlc3RfaWQiOiJEOTA2RDQ4QTo2Qjg5OjIyNUU1ODY6NTZEODJGOEMifQ==">
+
+ <link rel="mask-icon" href="https://assets-cdn.github.com/pinned-octocat.svg" color="#4078c0">
+ <link rel="icon" type="image/x-icon" href="https://assets-cdn.github.com/favicon.ico">
+
+ <meta content="80c998d8816164a29e0791941c3aa941d27d5c54" name="form-nonce" />
+
+ <meta http-equiv="x-pjax-version" content="b5f13da19cd72ae4f78b723c93c03e6b">
+
+
+
+ <meta name="description" content="lucene-solr - Mirror of Apache Lucene + Solr">
+ <meta name="go-import" content="github.com/apache/lucene-solr git https://github.com/apache/lucene-solr.git">
+
+ <meta content="47359" name="octolytics-dimension-user_id" /><meta content="apache" name="octolytics-dimension-user_login" /><meta content="50229487" name="octolytics-dimension-repository_id" /><meta content="apache/lucene-solr" name="octolytics-dimension-repository_nwo" /><meta content="true" name="octolytics-dimension-repository_public" /><meta content="false" name="octolytics-dimension-repository_is_fork" /><meta content="50229487" name="octolytics-dimension-repository_network_root_id" /><meta content="apache/lucene-solr" name="octolytics-dimension-repository_network_root_nwo" />
+ <link href="https://github.com/apache/lucene-solr/commits/master.atom" rel="alternate" title="Recent Commits to lucene-solr:master" type="application/atom+xml">
+
+
+ <link rel="canonical" href="https://github.com/apache/lucene-solr/blob/master/solr/example/files/conf/lang/stopwords_de.txt" data-pjax-transient>
+ </head>
+
+
+ <body class="logged_out env-production vis-public mirror page-blob">
+ <a href="#start-of-content" tabindex="1" class="accessibility-aid js-skip-to-content">Skip to content</a>
+
+
+
+
+
+
+
+
+ <div class="header header-logged-out" role="banner">
+ <div class="container clearfix">
+
+ <a class="header-logo-wordmark" href="https://github.com/" data-ga-click="(Logged out) Header, go to homepage, icon:logo-wordmark">
+ <svg aria-hidden="true" class="octicon octicon-logo-github" height="28" role="img" version="1.1" viewBox="0 0 45 16" width="78"><path d="M8.64 5.19H4.88c-0.11 0-0.19 0.08-0.19 0.17v1.84c0 0.09 0.08 0.17 0.19 0.17h1.47v2.3s-0.33 0.11-1.25 0.11c-1.08 0-2.58-0.39-2.58-3.7s1.58-3.73 3.05-3.73c1.27 0 1.81 0.22 2.17 0.33 0.11 0.03 0.2-0.08 0.2-0.17l0.42-1.78c0-0.05-0.02-0.09-0.06-0.14-0.14-0.09-1.02-0.58-3.2-0.58C2.58 0 0 1.06 0 6.2s2.95 5.92 5.44 5.92c2.06 0 3.31-0.89 3.31-0.89 0.05-0.02 0.06-0.09 0.06-0.13V5.36c0-0.09-0.08-0.17-0.19-0.17h0.02zM27.7 0.44h-2.13c-0.09 0-0.17 0.08-0.17 0.17v4.09h-3.31V0.61c0-0.09-0.08-0.17-0.17-0.17h-2.13c-0.09 0-0.17 0.08-0.17 0.17v11.11c0 0.09 0.09 0.17 0.17 0.17h2.13c0.09 0 0.17-0.08 0.17-0.17V6.97h3.31l-0.02 4.75c0 0.09 0.08 0.17 0.17 0.17h2.13c0.09 0 0.17-0.08 0.17-0.17V0.61c0-0.09-0.08-0.17-0.17-0.17h0.02zM11.19 0.69c-0.77 0-1.38 0.61-1.38 1.38s0.61 1.38 1.38 1.38c0.75 0 1.36-0.61 1.36-1.38s-0.61-1.38-1.36-1.38z m1.22 3.55c0-0.09-0.08-0.17-0.17-0.17H10.11c-0.09 0-0.17 0.09-0.17 0.2 0 0 0 6.17 0 7.34 0 0.2 0.13 0.27 0.3 0.27 0 0 0.91 0 1.92 0 0.2 0 0.25-0.09 0.25-0.27 0-0.39 0-7.36 0-7.36v-0.02z m23.52-0.16h-2.09c-0.11 0-0.17 0.08-0.17 0.19v5.44s-0.55 0.39-1.3 0.39-0.97-0.34-0.97-1.09c0-0.73 0-4.75 0-4.75 0-0.09-0.08-0.17-0.17-0.17h-2.14c-0.09 0-0.17 0.08-0.17 0.17 0 0 0 2.91 0 5.11s1.23 2.75 2.92 2.75c1.39 0 2.52-0.77 2.52-0.77s0.05 0.39 0.08 0.45c0.02 0.05 0.09 0.09 0.16 0.09h1.34c0.11 0 0.17-0.08 0.17-0.17l0.02-7.47c0-0.09-0.08-0.17-0.19-0.17z m5.77-0.25c-1.2 0-2.02 0.53-2.02 0.53V0.59c0-0.09-0.08-0.17-0.17-0.17h-2.13c-0.09 0-0.17 0.08-0.17 0.17l-0.02 11.11c0 0.09 0.09 0.17 0.19 0.17h1.48c0.06 0 0.11-0.02 0.14-0.08 0.05-0.06 0.09-0.52 0.09-0.52s0.88 0.83 2.52 0.83c1.94 0 3.05-0.98 3.05-4.41s-1.77-3.88-2.97-3.88z m-0.83 6.27c-0.73-0.02-1.22-0.36-1.22-0.36V6.22s0.48-0.3 1.08-0.34c0.77-0.08 1.5 0.16 1.5 1.97 0 1.91-0.33 2.28-1.36 2.25z m-22.33-0.05c-0.09 0-0.33 0.05-0.58 0.05-0.78 0-1.05-0.36-1.05-0.83s0-3.13 0-3.13h1.59c0.09 0 0.16-0.08 0.16-0.19V4.25c0-0.09-0.08-0.17-0.16-0.17h-1.59V1.97c0-0.08-0.05-0.13-0.14-0.13H14.61c-0.09 0-0.14 0.05-0.14 0.13v2.17s-1.09 0.27-1.16 0.28c-0.08 0.02-0.13 0.09-0.13 0.17v1.36c0 0.11 0.08 0.19 0.17 0.19h1.11s0 1.44 0 3.28c0 2.44 1.7 2.69 2.86 2.69 0.53 0 1.17-0.17 1.27-0.22 0.06-0.02 0.09-0.09 0.09-0.16v-1.5c0-0.11-0.08-0.19-0.17-0.19h0.02z"></path></svg>
+ </a>
+
+ <div class="header-actions" role="navigation">
+ <a class="btn btn-primary" href="/join?source=header-repo" data-ga-click="(Logged out) Header, clicked Sign up, text:sign-up">Sign up</a>
+ <a class="btn" href="/login?return_to=%2Fapache%2Flucene-solr%2Fblob%2Fmaster%2Fsolr%2Fexample%2Ffiles%2Fconf%2Flang%2Fstopwords_de.txt" data-ga-click="(Logged out) Header, clicked Sign in, text:sign-in">Sign in</a>
+ </div>
+
+ <div class="site-search repo-scope js-site-search" role="search">
+ <!-- </textarea> --><!-- '"` --><form accept-charset="UTF-8" action="/apache/lucene-solr/search" class="js-site-search-form" data-global-search-url="/search" data-repo-search-url="/apache/lucene-solr/search" method="get"><div style="margin:0;padding:0;display:inline"><input name="utf8" type="hidden" value="&#x2713;" /></div>
+ <label class="js-chromeless-input-container form-control">
+ <div class="scope-badge">This repository</div>
+ <input type="text"
+ class="js-site-search-focus js-site-search-field is-clearable chromeless-input"
+ data-hotkey="s"
+ name="q"
+ placeholder="Search"
+ aria-label="Search this repository"
+ data-global-scope-placeholder="Search GitHub"
+ data-repo-scope-placeholder="Search"
+ tabindex="1"
+ autocapitalize="off">
+ </label>
+</form>
+ </div>
+
+ <ul class="header-nav left" role="navigation">
+ <li class="header-nav-item">
+ <a class="header-nav-link" href="/explore" data-ga-click="(Logged out) Header, go to explore, text:explore">Explore</a>
+ </li>
+ <li class="header-nav-item">
+ <a class="header-nav-link" href="/features" data-ga-click="(Logged out) Header, go to features, text:features">Features</a>
+ </li>
+ <li class="header-nav-item">
+ <a class="header-nav-link" href="https://enterprise.github.com/" data-ga-click="(Logged out) Header, go to enterprise, text:enterprise">Enterprise</a>
+ </li>
+ <li class="header-nav-item">
+ <a class="header-nav-link" href="/pricing" data-ga-click="(Logged out) Header, go to pricing, text:pricing">Pricing</a>
+ </li>
+ </ul>
+
+ </div>
+</div>
+
+
+
+ <div id="start-of-content" class="accessibility-aid"></div>
+
+ <div id="js-flash-container">
+</div>
+
+
+ <div role="main" class="main-content">
+ <div itemscope itemtype="http://schema.org/SoftwareSourceCode">
+ <div id="js-repo-pjax-container" class="context-loader-container js-repo-nav-next" data-pjax-container>
+
+<div class="pagehead repohead instapaper_ignore readability-menu experiment-repo-nav">
+ <div class="container repohead-details-container">
+
+
+
+<ul class="pagehead-actions">
+
+ <li>
+ <a href="/login?return_to=%2Fapache%2Flucene-solr"
+ class="btn btn-sm btn-with-count tooltipped tooltipped-n"
+ aria-label="You must be signed in to watch a repository" rel="nofollow">
+ <svg aria-hidden="true" class="octicon octicon-eye" height="16" role="img" version="1.1" viewBox="0 0 16 16" width="16"><path d="M8.06 2C3 2 0 8 0 8s3 6 8.06 6c4.94 0 7.94-6 7.94-6S13 2 8.06 2z m-0.06 10c-2.2 0-4-1.78-4-4 0-2.2 1.8-4 4-4 2.22 0 4 1.8 4 4 0 2.22-1.78 4-4 4z m2-4c0 1.11-0.89 2-2 2s-2-0.89-2-2 0.89-2 2-2 2 0.89 2 2z"></path></svg>
+ Watch
+ </a>
+ <a class="social-count" href="/apache/lucene-solr/watchers">
+ 21
+ </a>
+
+ </li>
+
+ <li>
+ <a href="/login?return_to=%2Fapache%2Flucene-solr"
+ class="btn btn-sm btn-with-count tooltipped tooltipped-n"
+ aria-label="You must be signed in to star a repository" rel="nofollow">
+ <svg aria-hidden="true" class="octicon octicon-star" height="16" role="img" version="1.1" viewBox="0 0 14 16" width="14"><path d="M14 6l-4.9-0.64L7 1 4.9 5.36 0 6l3.6 3.26L2.67 14l4.33-2.33 4.33 2.33L10.4 9.26 14 6z"></path></svg>
+ Star
+ </a>
+
+ <a class="social-count js-social-count" href="/apache/lucene-solr/stargazers">
+ 82
+ </a>
+
+ </li>
+
+ <li>
+ <a href="/login?return_to=%2Fapache%2Flucene-solr"
+ class="btn btn-sm btn-with-count tooltipped tooltipped-n"
+ aria-label="You must be signed in to fork a repository" rel="nofollow">
+ <svg aria-hidden="true" class="octicon octicon-repo-forked" height="16" role="img" version="1.1" viewBox="0 0 10 16" width="10"><path d="M8 1c-1.11 0-2 0.89-2 2 0 0.73 0.41 1.38 1 1.72v1.28L5 8 3 6v-1.28c0.59-0.34 1-0.98 1-1.72 0-1.11-0.89-2-2-2S0 1.89 0 3c0 0.73 0.41 1.38 1 1.72v1.78l3 3v1.78c-0.59 0.34-1 0.98-1 1.72 0 1.11 0.89 2 2 2s2-0.89 2-2c0-0.73-0.41-1.38-1-1.72V9.5l3-3V4.72c0.59-0.34 1-0.98 1-1.72 0-1.11-0.89-2-2-2zM2 4.2c-0.66 0-1.2-0.55-1.2-1.2s0.55-1.2 1.2-1.2 1.2 0.55 1.2 1.2-0.55 1.2-1.2 1.2z m3 10c-0.66 0-1.2-0.55-1.2-1.2s0.55-1.2 1.2-1.2 1.2 0.55 1.2 1.2-0.55 1.2-1.2 1.2z m3-10c-0.66 0-1.2-0.55-1.2-1.2s0.55-1.2 1.2-1.2 1.2 0.55 1.2 1.2-0.55 1.2-1.2 1.2z"></path></svg>
+ Fork
+ </a>
+
+ <a href="/apache/lucene-solr/network" class="social-count">
+ 83
+ </a>
+ </li>
+</ul>
+
+ <h1 class="entry-title public repo-mirror">
+ <svg aria-hidden="true" class="octicon octicon-mirror" height="16" role="img" version="1.1" viewBox="0 0 16 16" width="16"><path d="M15.5 4.7L8.5 0 1.5 4.7c-0.3 0.19-0.5 0.45-0.5 0.8v10.5l7.5-4 7.5 4V5.5c0-0.34-0.2-0.61-0.5-0.8z m-0.5 9.8L9 11.25v-1.25h-1v1.25L2 14.5V5.5L8 1.5v4.5h1V1.5l6 4v9zM6 7h5V5l3 3-3 3V9H6v2L3 8l3-3v2z"></path></svg>
+ <span class="author" itemprop="author"><a href="/apache" class="url fn" rel="author">apache</a></span><!--
+--><span class="path-divider">/</span><!--
+--><strong itemprop="name"><a href="/apache/lucene-solr" data-pjax="#js-repo-pjax-container">lucene-solr</a></strong>
+
+ <span class="page-context-loader">
+ <img alt="" height="16" src="https://assets-cdn.github.com/images/spinners/octocat-spinner-32.gif" width="16" />
+ </span>
+
+ <span class="mirror-flag">
+ <span class="text">mirrored from <a href="git://git.apache.org/lucene-solr.git">git://git.apache.org/lucene-solr.git</a></span>
+ </span>
+</h1>
+
+ </div>
+ <div class="container">
+
+<nav class="reponav js-repo-nav js-sidenav-container-pjax js-octicon-loaders"
+ itemscope
+ itemtype="http://schema.org/BreadcrumbList"
+ role="navigation"
+ data-pjax="#js-repo-pjax-container">
+
+ <span itemscope itemtype="http://schema.org/ListItem" itemprop="itemListElement">
+ <a href="/apache/lucene-solr" aria-selected="true" class="js-selected-navigation-item selected reponav-item" data-hotkey="g c" data-selected-links="repo_source repo_downloads repo_commits repo_releases repo_tags repo_branches /apache/lucene-solr" itemprop="url">
+ <svg aria-hidden="true" class="octicon octicon-code" height="16" role="img" version="1.1" viewBox="0 0 14 16" width="14"><path d="M9.5 3l-1.5 1.5 3.5 3.5L8 11.5l1.5 1.5 4.5-5L9.5 3zM4.5 3L0 8l4.5 5 1.5-1.5L2.5 8l3.5-3.5L4.5 3z"></path></svg>
+ <span itemprop="name">Code</span>
+ <meta itemprop="position" content="1">
+</a> </span>
+
+
+ <span itemscope itemtype="http://schema.org/ListItem" itemprop="itemListElement">
+ <a href="/apache/lucene-solr/pulls" class="js-selected-navigation-item reponav-item" data-hotkey="g p" data-selected-links="repo_pulls /apache/lucene-solr/pulls" itemprop="url">
+ <svg aria-hidden="true" class="octicon octicon-git-pull-request" height="16" role="img" version="1.1" viewBox="0 0 12 16" width="12"><path d="M11 11.28c0-1.73 0-6.28 0-6.28-0.03-0.78-0.34-1.47-0.94-2.06s-1.28-0.91-2.06-0.94c0 0-1.02 0-1 0V0L4 3l3 3V4h1c0.27 0.02 0.48 0.11 0.69 0.31s0.3 0.42 0.31 0.69v6.28c-0.59 0.34-1 0.98-1 1.72 0 1.11 0.89 2 2 2s2-0.89 2-2c0-0.73-0.41-1.38-1-1.72z m-1 2.92c-0.66 0-1.2-0.55-1.2-1.2s0.55-1.2 1.2-1.2 1.2 0.55 1.2 1.2-0.55 1.2-1.2 1.2zM4 3c0-1.11-0.89-2-2-2S0 1.89 0 3c0 0.73 0.41 1.38 1 1.72 0 1.55 0 5.56 0 6.56-0.59 0.34-1 0.98-1 1.72 0 1.11 0.89 2 2 2s2-0.89 2-2c0-0.73-0.41-1.38-1-1.72V4.72c0.59-0.34 1-0.98 1-1.72z m-0.8 10c0 0.66-0.55 1.2-1.2 1.2s-1.2-0.55-1.2-1.2 0.55-1.2 1.2-1.2 1.2 0.55 1.2 1.2z m-1.2-8.8c-0.66 0-1.2-0.55-1.2-1.2s0.55-1.2 1.2-1.2 1.2 0.55 1.2 1.2-0.55 1.2-1.2 1.2z"></path></svg>
+ <span itemprop="name">Pull requests</span>
+ <span class="counter">12</span>
+ <meta itemprop="position" content="3">
+</a> </span>
+
+
+ <a href="/apache/lucene-solr/pulse" class="js-selected-navigation-item reponav-item" data-selected-links="pulse /apache/lucene-solr/pulse">
+ <svg aria-hidden="true" class="octicon octicon-pulse" height="16" role="img" version="1.1" viewBox="0 0 14 16" width="14"><path d="M11.5 8L8.8 5.4 6.6 8.5 5.5 1.6 2.38 8H0V10h3.6L4.5 8.2l0.9 5.4L9 8.5l1.6 1.5H14V8H11.5z"></path></svg>
+ Pulse
+</a>
+ <a href="/apache/lucene-solr/graphs" class="js-selected-navigation-item reponav-item" data-selected-links="repo_graphs repo_contributors /apache/lucene-solr/graphs">
+ <svg aria-hidden="true" class="octicon octicon-graph" height="16" role="img" version="1.1" viewBox="0 0 16 16" width="16"><path d="M16 14v1H0V0h1v14h15z m-11-1H3V8h2v5z m4 0H7V3h2v10z m4 0H11V6h2v7z"></path></svg>
+ Graphs
+</a>
+
+</nav>
+
+ </div>
+</div>
+
+<div class="container new-discussion-timeline experiment-repo-nav">
+ <div class="repository-content">
+
+
+
+<a href="/apache/lucene-solr/blob/6c06d4bc007454df09b2ace36ea839464b435be8/solr/example/files/conf/lang/stopwords_de.txt" class="hidden js-permalink-shortcut" data-hotkey="y">Permalink</a>
+
+<!-- blob contrib key: blob_contributors:v21:0bc50bfe3f2deebcc6f2e691f4b94310 -->
+
+<div class="file-navigation js-zeroclipboard-container">
+
+<div class="select-menu js-menu-container js-select-menu left">
+ <button class="btn btn-sm select-menu-button js-menu-target css-truncate" data-hotkey="w"
+ title="master"
+ type="button" aria-label="Switch branches or tags" tabindex="0" aria-haspopup="true">
+ <i>Branch:</i>
+ <span class="js-select-button css-truncate-target">master</span>
+ </button>
+
+ <div class="select-menu-modal-holder js-menu-content js-navigation-container" data-pjax aria-hidden="true">
+
+ <div class="select-menu-modal">
+ <div class="select-menu-header">
+ <svg aria-label="Close" class="octicon octicon-x js-menu-close" height="16" role="img" version="1.1" viewBox="0 0 12 16" width="12"><path d="M7.48 8l3.75 3.75-1.48 1.48-3.75-3.75-3.75 3.75-1.48-1.48 3.75-3.75L0.77 4.25l1.48-1.48 3.75 3.75 3.75-3.75 1.48 1.48-3.75 3.75z"></path></svg>
+ <span class="select-menu-title">Switch branches/tags</span>
+ </div>
+
+ <div class="select-menu-filters">
+ <div class="select-menu-text-filter">
+ <input type="text" aria-label="Filter branches/tags" id="context-commitish-filter-field" class="js-filterable-field js-navigation-enable" placeholder="Filter branches/tags">
+ </div>
+ <div class="select-menu-tabs">
+ <ul>
+ <li class="select-menu-tab">
+ <a href="#" data-tab-filter="branches" data-filter-placeholder="Filter branches/tags" class="js-select-menu-tab" role="tab">Branches</a>
+ </li>
+ <li class="select-menu-tab">
+ <a href="#" data-tab-filter="tags" data-filter-placeholder="Find a tag…" class="js-select-menu-tab" role="tab">Tags</a>
+ </li>
+ </ul>
+ </div>
+ </div>
+
+ <div class="select-menu-list select-menu-tab-bucket js-select-menu-tab-bucket" data-tab-filter="branches" role="menu">
+
+ <div data-filterable-for="context-commitish-filter-field" data-filterable-type="substring">
+
+
+ <a class="select-menu-item js-navigation-item js-navigation-open "
+ href="/apache/lucene-solr/blob/apiv2/solr/example/files/conf/lang/stopwords_de.txt"
+ data-name="apiv2"
+ data-skip-pjax="true"
+ rel="nofollow">
+ <svg aria-hidden="true" class="octicon octicon-check select-menu-item-icon" height="16" role="img" version="1.1" viewBox="0 0 12 16" width="12"><path d="M12 5L4 13 0 9l1.5-1.5 2.5 2.5 6.5-6.5 1.5 1.5z"></path></svg>
+ <span class="select-menu-item-text css-truncate-target js-select-menu-filter-text" title="apiv2">
+ apiv2
+ </span>
+ </a>
+ <a class="select-menu-item js-navigation-item js-navigation-open "
+ href="/apache/lucene-solr/blob/branch_3x/solr/example/files/conf/lang/stopwords_de.txt"
+ data-name="branch_3x"
+ data-skip-pjax="true"
+ rel="nofollow">
+ <svg aria-hidden="true" class="octicon octicon-check select-menu-item-icon" height="16" role="img" version="1.1" viewBox="0 0 12 16" width="12"><path d="M12 5L4 13 0 9l1.5-1.5 2.5 2.5 6.5-6.5 1.5 1.5z"></path></svg>
+ <span class="select-menu-item-text css-truncate-target js-select-menu-filter-text" title="branch_3x">
+ branch_3x
+ </span>
+ </a>
+ <a class="select-menu-item js-navigation-item js-navigation-open "
+ href="/apache/lucene-solr/blob/branch_4x/solr/example/files/conf/lang/stopwords_de.txt"
+ data-name="branch_4x"
+ data-skip-pjax="true"
+ rel="nofollow">
+ <svg aria-hidden="true" class="octicon octicon-check select-menu-item-icon" height="16" role="img" version="1.1" viewBox="0 0 12 16" width="12"><path d="M12 5L4 13 0 9l1.5-1.5 2.5 2.5 6.5-6.5 1.5 1.5z"></path></svg>
+ <span class="select-menu-item-text css-truncate-target js-select-menu-filter-text" title="branch_4x">
+ branch_4x
+ </span>
+ </a>
+ <a class="select-menu-item js-navigation-item js-navigation-open "
+ href="/apache/lucene-solr/blob/branch_5x/solr/example/files/conf/lang/stopwords_de.txt"
+ data-name="branch_5x"
+ data-skip-pjax="true"
+ rel="nofollow">
+ <svg aria-hidden="true" class="octicon octicon-check select-menu-item-icon" height="16" role="img" version="1.1" viewBox="0 0 12 16" width="12"><path d="M12 5L4 13 0 9l1.5-1.5 2.5 2.5 6.5-6.5 1.5 1.5z"></path></svg>
+ <span class="select-menu-item-text css-truncate-target js-select-menu-filter-text" title="branch_5x">
+ branch_5x
+ </span>
+ </a>
+ <a class="select-menu-item js-navigation-item js-navigation-open "
+ href="/apache/lucene-solr/blob/branch_5_4/solr/example/files/conf/lang/stopwords_de.txt"
+ data-name="branch_5_4"
+ data-skip-pjax="true"
+ rel="nofollow">
+ <svg aria-hidden="true" class="octicon octicon-check select-menu-item-icon" height="16" role="img" version="1.1" viewBox="0 0 12 16" width="12"><path d="M12 5L4 13 0 9l1.5-1.5 2.5 2.5 6.5-6.5 1.5 1.5z"></path></svg>
+ <span class="select-menu-item-text css-truncate-target js-select-menu-filter-text" title="branch_5_4">
+ branch_5_4
+ </span>
+ </a>
+ <a class="select-menu-item js-navigation-item js-navigation-open "
+ href="/apache/lucene-solr/blob/branch_5_5/solr/example/files/conf/lang/stopwords_de.txt"
+ data-name="branch_5_5"
+ data-skip-pjax="true"
+ rel="nofollow">
+ <svg aria-hidden="true" class="octicon octicon-check select-menu-item-icon" height="16" role="img" version="1.1" viewBox="0 0 12 16" width="12"><path d="M12 5L4 13 0 9l1.5-1.5 2.5 2.5 6.5-6.5 1.5 1.5z"></path></svg>
+ <span class="select-menu-item-text css-truncate-target js-select-menu-filter-text" title="branch_5_5">
+ branch_5_5
+ </span>
+ </a>
+ <a class="select-menu-item js-navigation-item js-navigation-open "
+ href="/apache/lucene-solr/blob/branch_6x/solr/example/files/conf/lang/stopwords_de.txt"
+ data-name="branch_6x"
+ data-skip-pjax="true"
+ rel="nofollow">
+ <svg aria-hidden="true" class="octicon octicon-check select-menu-item-icon" height="16" role="img" version="1.1" viewBox="0 0 12 16" width="12"><path d="M12 5L4 13 0 9l1.5-1.5 2.5 2.5 6.5-6.5 1.5 1.5z"></path></svg>
+ <span class="select-menu-item-text css-truncate-target js-select-menu-filter-text" title="branch_6x">
+ branch_6x
+ </span>
+ </a>
+ <a class="select-menu-item js-navigation-item js-navigation-open "
+ href="/apache/lucene-solr/blob/branch_6_0/solr/example/files/conf/lang/stopwords_de.txt"
+ data-name="branch_6_0"
+ data-skip-pjax="true"
+ rel="nofollow">
+ <svg aria-hidden="true" class="octicon octicon-check select-menu-item-icon" height="16" role="img" version="1.1" viewBox="0 0 12 16" width="12"><path d="M12 5L4 13 0 9l1.5-1.5 2.5 2.5 6.5-6.5 1.5 1.5z"></path></svg>
+ <span class="select-menu-item-text css-truncate-target js-select-menu-filter-text" title="branch_6_0">
+ branch_6_0
+ </span>
+ </a>
+ <a class="select-menu-item js-navigation-item js-navigation-open "
+ href="/apache/lucene-solr/blob/jira/SOLR-445/solr/example/files/conf/lang/stopwords_de.txt"
+ data-name="jira/SOLR-445"
+ data-skip-pjax="true"
+ rel="nofollow">
+ <svg aria-hidden="true" class="octicon octicon-check select-menu-item-icon" height="16" role="img" version="1.1" viewBox="0 0 12 16" width="12"><path d="M12 5L4 13 0 9l1.5-1.5 2.5 2.5 6.5-6.5 1.5 1.5z"></path></svg>
+ <span class="select-menu-item-text css-truncate-target js-select-menu-filter-text" title="jira/SOLR-445">
+ jira/SOLR-445
+ </span>
+ </a>
+ <a class="select-menu-item js-navigation-item js-navigation-open "
+ href="/apache/lucene-solr/blob/jira/lucene-5438-nrt-replication/solr/example/files/conf/lang/stopwords_de.txt"
+ data-name="jira/lucene-5438-nrt-replication"
+ data-skip-pjax="true"
+ rel="nofollow">
+ <svg aria-hidden="true" class="octicon octicon-check select-menu-item-icon" height="16" role="img" version="1.1" viewBox="0 0 12 16" width="12"><path d="M12 5L4 13 0 9l1.5-1.5 2.5 2.5 6.5-6.5 1.5 1.5z"></path></svg>
+ <span class="select-menu-item-text css-truncate-target js-select-menu-filter-text" title="jira/lucene-5438-nrt-replication">
+ jira/lucene-5438-nrt-replication
+ </span>
+ </a>
+ <a class="select-menu-item js-navigation-item js-navigation-open "
+ href="/apache/lucene-solr/blob/lucene-6835/solr/example/files/conf/lang/stopwords_de.txt"
+ data-name="lucene-6835"
+ data-skip-pjax="true"
+ rel="nofollow">
+ <svg aria-hidden="true" class="octicon octicon-check select-menu-item-icon" height="16" role="img" version="1.1" viewBox="0 0 12 16" width="12"><path d="M12 5L4 13 0 9l1.5-1.5 2.5 2.5 6.5-6.5 1.5 1.5z"></path></svg>
+ <span class="select-menu-item-text css-truncate-target js-select-menu-filter-text" title="lucene-6835">
+ lucene-6835
+ </span>
+ </a>
+ <a class="select-menu-item js-navigation-item js-navigation-open "
+ href="/apache/lucene-solr/blob/lucene-6997/solr/example/files/conf/lang/stopwords_de.txt"
+ data-name="lucene-6997"
+ data-skip-pjax="true"
+ rel="nofollow">
+ <svg aria-hidden="true" class="octicon octicon-check select-menu-item-icon" height="16" role="img" version="1.1" viewBox="0 0 12 16" width="12"><path d="M12 5L4 13 0 9l1.5-1.5 2.5 2.5 6.5-6.5 1.5 1.5z"></path></svg>
+ <span class="select-menu-item-text css-truncate-target js-select-menu-filter-text" title="lucene-6997">
+ lucene-6997
+ </span>
+ </a>
+ <a class="select-menu-item js-navigation-item js-navigation-open "
+ href="/apache/lucene-solr/blob/lucene-7015/solr/example/files/conf/lang/stopwords_de.txt"
+ data-name="lucene-7015"
+ data-skip-pjax="true"
+ rel="nofollow">
+ <svg aria-hidden="true" class="octicon octicon-check select-menu-item-icon" height="16" role="img" version="1.1" viewBox="0 0 12 16" width="12"><path d="M12 5L4 13 0 9l1.5-1.5 2.5 2.5 6.5-6.5 1.5 1.5z"></path></svg>
+ <span class="select-menu-item-text css-truncate-target js-select-menu-filter-text" title="lucene-7015">
+ lucene-7015
+ </span>
+ </a>
+ <a class="select-menu-item js-navigation-item js-navigation-open selected"
+ href="/apache/lucene-solr/blob/master/solr/example/files/conf/lang/stopwords_de.txt"
+ data-name="master"
+ data-skip-pjax="true"
+ rel="nofollow">
+ <svg aria-hidden="true" class="octicon octicon-check select-menu-item-icon" height="16" role="img" version="1.1" viewBox="0 0 12 16" width="12"><path d="M12 5L4 13 0 9l1.5-1.5 2.5 2.5 6.5-6.5 1.5 1.5z"></path></svg>
+ <span class="select-menu-item-text css-truncate-target js-select-menu-filter-text" title="master">
+ master
+ </span>
+ </a>
+ </div>
+
+ <div class="select-menu-no-results">Nothing to show</div>
+ </div>
+
+ <div class="select-menu-list select-menu-tab-bucket js-select-menu-tab-bucket" data-tab-filter="tags">
+ <div data-filterable-for="context-commitish-filter-field" data-filterable-type="substring">
+
+
+ <a class="select-menu-item js-navigation-item js-navigation-open "
+ href="/apache/lucene-solr/tree/releases/solr/1.4.0/solr/example/files/conf/lang/stopwords_de.txt"
+ data-name="releases/solr/1.4.0"
+ data-skip-pjax="true"
+ rel="nofollow">
+ <svg aria-hidden="true" class="octicon octicon-check select-menu-item-icon" height="16" role="img" version="1.1" viewBox="0 0 12 16" width="12"><path d="M12 5L4 13 0 9l1.5-1.5 2.5 2.5 6.5-6.5 1.5 1.5z"></path></svg>
+ <span class="select-menu-item-text css-truncate-target" title="releases/solr/1.4.0">
+ releases/solr/1.4.0
+ </span>
+ </a>
+ <a class="select-menu-item js-navigation-item js-navigation-open "
+ href="/apache/lucene-solr/tree/releases/solr/1.3.0/solr/example/files/conf/lang/stopwords_de.txt"
+ data-name="releases/solr/1.3.0"
+ data-skip-pjax="true"
+ rel="nofollow">
+ <svg aria-hidden="true" class="octicon octicon-check select-menu-item-icon" height="16" role="img" version="1.1" viewBox="0 0 12 16" width="12"><path d="M12 5L4 13 0 9l1.5-1.5 2.5 2.5 6.5-6.5 1.5 1.5z"></path></svg>
+ <span class="select-menu-item-text css-truncate-target" title="releases/solr/1.3.0">
+ releases/solr/1.3.0
+ </span>
+ </a>
+ <a class="select-menu-item js-navigation-item js-navigation-open "
+ href="/apache/lucene-solr/tree/releases/solr/1.2.0/solr/example/files/conf/lang/stopwords_de.txt"
+ data-name="releases/solr/1.2.0"
+ data-skip-pjax="true"
+ rel="nofollow">
+ <svg aria-hidden="true" class="octicon octicon-check select-menu-item-icon" height="16" role="img" version="1.1" viewBox="0 0 12 16" width="12"><path d="M12 5L4 13 0 9l1.5-1.5 2.5 2.5 6.5-6.5 1.5 1.5z"></path></svg>
+ <span class="select-menu-item-text css-truncate-target" title="releases/solr/1.2.0">
+ releases/solr/1.2.0
+ </span>
+ </a>
+ <a class="select-menu-item js-navigation-item js-navigation-open "
+ href="/apache/lucene-solr/tree/releases/solr/1.1.0/solr/example/files/conf/lang/stopwords_de.txt"
+ data-name="releases/solr/1.1.0"
+ data-skip-pjax="true"
+ rel="nofollow">
+ <svg aria-hidden="true" class="octicon octicon-check select-menu-item-icon" height="16" role="img" version="1.1" viewBox="0 0 12 16" width="12"><path d="M12 5L4 13 0 9l1.5-1.5 2.5 2.5 6.5-6.5 1.5 1.5z"></path></svg>
+ <span class="select-menu-item-text css-truncate-target" title="releases/solr/1.1.0">
+ releases/solr/1.1.0
+ </span>
+ </a>
+ <a class="select-menu-item js-navigation-item js-navigation-open "
+ href="/apache/lucene-solr/tree/releases/lucene/3.0.1/solr/example/files/conf/lang/stopwords_de.txt"
+ data-name="releases/lucene/3.0.1"
+ data-skip-pjax="true"
+ rel="nofollow">
+ <svg aria-hidden="true" class="octicon octicon-check select-menu-item-icon" height="16" role="img" version="1.1" viewBox="0 0 12 16" width="12"><path d="M12 5L4 13 0 9l1.5-1.5 2.5 2.5 6.5-6.5 1.5 1.5z"></path></svg>
+ <span class="select-menu-item-text css-truncate-target" title="releases/lucene/3.0.1">
+ releases/lucene/3.0.1
+ </span>
+ </a>
+ <a class="select-menu-item js-navigation-item js-navigation-open "
+ href="/apache/lucene-solr/tree/releases/lucene/3.0.0/solr/example/files/conf/lang/stopwords_de.txt"
+ data-name="releases/lucene/3.0.0"
+ data-skip-pjax="true"
+ rel="nofollow">
+ <svg aria-hidden="true" class="octicon octicon-check select-menu-item-icon" height="16" role="img" version="1.1" viewBox="0 0 12 16" width="12"><path d="M12 5L4 13 0 9l1.5-1.5 2.5 2.5 6.5-6.5 1.5 1.5z"></path></svg>
+ <span class="select-menu-item-text css-truncate-target" title="releases/lucene/3.0.0">
+ releases/lucene/3.0.0
+ </span>
+ </a>
+ <a class="select-menu-item js-navigation-item js-navigation-open "
+ href="/apache/lucene-solr/tree/releases/lucene/2.9.2/solr/example/files/conf/lang/stopwords_de.txt"
+ data-name="releases/lucene/2.9.2"
+ data-skip-pjax="true"
+ rel="nofollow">
+ <svg aria-hidden="true" class="octicon octicon-check select-menu-item-icon" height="16" role="img" version="1.1" viewBox="0 0 12 16" width="12"><path d="M12 5L4 13 0 9l1.5-1.5 2.5 2.5 6.5-6.5 1.5 1.5z"></path></svg>
+ <span class="select-menu-item-text css-truncate-target" title="releases/lucene/2.9.2">
+ releases/lucene/2.9.2
+ </span>
+ </a>
+ <a class="select-menu-item js-navigation-item js-navigation-open "
+ href="/apache/lucene-solr/tree/releases/lucene/2.9.1/solr/example/files/conf/lang/stopwords_de.txt"
+ data-name="releases/lucene/2.9.1"
+ data-skip-pjax="true"
+ rel="nofollow">
+ <svg aria-hidden="true" class="octicon octicon-check select-menu-item-icon" height="16" role="img" version="1.1" viewBox="0 0 12 16" width="12"><path d="M12 5L4 13 0 9l1.5-1.5 2.5 2.5 6.5-6.5 1.5 1.5z"></path></svg>
+ <span class="select-menu-item-text css-truncate-target" title="releases/lucene/2.9.1">
+ releases/lucene/2.9.1
+ </span>
+ </a>
+ <a class="select-menu-item js-navigation-item js-navigation-open "
+ href="/apache/lucene-solr/tree/releases/lucene/2.9.0/solr/example/files/conf/lang/stopwords_de.txt"
+ data-name="releases/lucene/2.9.0"
+ data-skip-pjax="true"
+ rel="nofollow">
+ <svg aria-hidden="true" class="octicon octicon-check select-menu-item-icon" height="16" role="img" version="1.1" viewBox="0 0 12 16" width="12"><path d="M12 5L4 13 0 9l1.5-1.5 2.5 2.5 6.5-6.5 1.5 1.5z"></path></svg>
+ <span class="select-menu-item-text css-truncate-target" title="releases/lucene/2.9.0">
+ releases/lucene/2.9.0
+ </span>
+ </a>
+ <a class="select-menu-item js-navigation-item js-navigation-open "
+ href="/apache/lucene-solr/tree/releases/lucene/2.4.1/solr/example/files/conf/lang/stopwords_de.txt"
+ data-name="releases/lucene/2.4.1"
+ data-skip-pjax="true"
+ rel="nofollow">
+ <svg aria-hidden="true" class="octicon octicon-check select-menu-item-icon" height="16" role="img" version="1.1" viewBox="0 0 12 16" width="12"><path d="M12 5L4 13 0 9l1.5-1.5 2.5 2.5 6.5-6.5 1.5 1.5z"></path></svg>
+ <span class="select-menu-item-text css-truncate-target" title="releases/lucene/2.4.1">
+ releases/lucene/2.4.1
+ </span>
+ </a>
+ <a class="select-menu-item js-navigation-item js-navigation-open "
+ href="/apache/lucene-solr/tree/releases/lucene/2.4.0/solr/example/files/conf/lang/stopwords_de.txt"
+ data-name="releases/lucene/2.4.0"
+ data-skip-pjax="true"
+ rel="nofollow">
+ <svg aria-hidden="true" class="octicon octicon-check select-menu-item-icon" height="16" role="img" version="1.1" viewBox="0 0 12 16" width="12"><path d="M12 5L4 13 0 9l1.5-1.5 2.5 2.5 6.5-6.5 1.5 1.5z"></path></svg>
+ <span class="select-menu-item-text css-truncate-target" title="releases/lucene/2.4.0">
+ releases/lucene/2.4.0
+ </span>
+ </a>
+ <a class="select-menu-item js-navigation-item js-navigation-open "
+ href="/apache/lucene-solr/tree/releases/lucene/2.3.2/solr/example/files/conf/lang/stopwords_de.txt"
+ data-name="releases/lucene/2.3.2"
+ data-skip-pjax="true"
+ rel="nofollow">
+ <svg aria-hidden="true" class="octicon octicon-check select-menu-item-icon" height="16" role="img" version="1.1" viewBox="0 0 12 16" width="12"><path d="M12 5L4 13 0 9l1.5-1.5 2.5 2.5 6.5-6.5 1.5 1.5z"></path></svg>
+ <span class="select-menu-item-text css-truncate-target" title="releases/lucene/2.3.2">
+ releases/lucene/2.3.2
+ </span>
+ </a>
+ <a class="select-menu-item js-navigation-item js-navigation-open "
+ href="/apache/lucene-solr/tree/releases/lucene/2.3.1/solr/example/files/conf/lang/stopwords_de.txt"
+ data-name="releases/lucene/2.3.1"
+ data-skip-pjax="true"
+ rel="nofollow">
+ <svg aria-hidden="true" class="octicon octicon-check select-menu-item-icon" height="16" role="img" version="1.1" viewBox="0 0 12 16" width="12"><path d="M12 5L4 13 0 9l1.5-1.5 2.5 2.5 6.5-6.5 1.5 1.5z"></path></svg>
+ <span class="select-menu-item-text css-truncate-target" title="releases/lucene/2.3.1">
+ releases/lucene/2.3.1
+ </span>
+ </a>
+ <a class="select-menu-item js-navigation-item js-navigation-open "
+ href="/apache/lucene-solr/tree/releases/lucene/2.3.0/solr/example/files/conf/lang/stopwords_de.txt"
+ data-name="releases/lucene/2.3.0"
+ data-skip-pjax="true"
+ rel="nofollow">
+ <svg aria-hidden="true" class="octicon octicon-check select-menu-item-icon" height="16" role="img" version="1.1" viewBox="0 0 12 16" width="12"><path d="M12 5L4 13 0 9l1.5-1.5 2.5 2.5 6.5-6.5 1.5 1.5z"></path></svg>
+ <span class="select-menu-item-text css-truncate-target" title="releases/lucene/2.3.0">
+ releases/lucene/2.3.0
+ </span>
+ </a>
+ <a class="select-menu-item js-navigation-item js-navigation-open "
+ href="/apache/lucene-solr/tree/releases/lucene/2.2.0/solr/example/files/conf/lang/stopwords_de.txt"
+ data-name="releases/lucene/2.2.0"
+ data-skip-pjax="true"
+ rel="nofollow">
+ <svg aria-hidden="true" class="octicon octicon-check select-menu-item-icon" height="16" role="img" version="1.1" viewBox="0 0 12 16" width="12"><path d="M12 5L4 13 0 9l1.5-1.5 2.5 2.5 6.5-6.5 1.5 1.5z"></path></svg>
+ <span class="select-menu-item-text css-truncate-target" title="releases/lucene/2.2.0">
+ releases/lucene/2.2.0
+ </span>
+ </a>
+ <a class="select-menu-item js-navigation-item js-navigation-open "
+ href="/apache/lucene-solr/tree/releases/lucene/2.1.0/solr/example/files/conf/lang/stopwords_de.txt"
+ data-name="releases/lucene/2.1.0"
+ data-skip-pjax="true"
+ rel="nofollow">
+ <svg aria-hidden="true" class="octicon octicon-check select-menu-item-icon" height="16" role="img" version="1.1" viewBox="0 0 12 16" width="12"><path d="M12 5L4 13 0 9l1.5-1.5 2.5 2.5 6.5-6.5 1.5 1.5z"></path></svg>
+ <span class="select-menu-item-text css-truncate-target" title="releases/lucene/2.1.0">
+ releases/lucene/2.1.0
+ </span>
+ </a>
+ <a class="select-menu-item js-navigation-item js-navigation-open "
+ href="/apache/lucene-solr/tree/releases/lucene/2.0.0/solr/example/files/conf/lang/stopwords_de.txt"
+ data-name="releases/lucene/2.0.0"
+ data-skip-pjax="true"
+ rel="nofollow">
+ <svg aria-hidden="true" class="octicon octicon-check select-menu-item-icon" height="16" role="img" version="1.1" viewBox="0 0 12 16" width="12"><path d="M12 5L4 13 0 9l1.5-1.5 2.5 2.5 6.5-6.5 1.5 1.5z"></path></svg>
+ <span class="select-menu-item-text css-truncate-target" title="releases/lucene/2.0.0">
+ releases/lucene/2.0.0
+ </span>
+ </a>
+ <a class="select-menu-item js-navigation-item js-navigation-open "
+ href="/apache/lucene-solr/tree/releases/lucene/1.9.1/solr/example/files/conf/lang/stopwords_de.txt"
+ data-name="releases/lucene/1.9.1"
+ data-skip-pjax="true"
+ rel="nofollow">
+ <svg aria-hidden="true" class="octicon octicon-check select-menu-item-icon" height="16" role="img" version="1.1" viewBox="0 0 12 16" width="12"><path d="M12 5L4 13 0 9l1.5-1.5 2.5 2.5 6.5-6.5 1.5 1.5z"></path></svg>
+ <span class="select-menu-item-text css-truncate-target" title="releases/lucene/1.9.1">
+ releases/lucene/1.9.1
+ </span>
+ </a>
+ <a class="select-menu-item js-navigation-item js-navigation-open "
+ href="/apache/lucene-solr/tree/releases/lucene/1.9/solr/example/files/conf/lang/stopwords_de.txt"
+ data-name="releases/lucene/1.9"
+ data-skip-pjax="true"
+ rel="nofollow">
+ <svg aria-hidden="true" class="octicon octicon-check select-menu-item-icon" height="16" role="img" version="1.1" viewBox="0 0 12 16" width="12"><path d="M12 5L4 13 0 9l1.5-1.5 2.5 2.5 6.5-6.5 1.5 1.5z"></path></svg>
+ <span class="select-menu-item-text css-truncate-target" title="releases/lucene/1.9">
+ releases/lucene/1.9
+ </span>
+ </a>
+ <a class="select-menu-item js-navigation-item js-navigation-open "
+ href="/apache/lucene-solr/tree/releases/lucene/1.9-rc1/solr/example/files/conf/lang/stopwords_de.txt"
+ data-name="releases/lucene/1.9-rc1"
+ data-skip-pjax="true"
+ rel="nofollow">
+ <svg aria-hidden="true" class="octicon octicon-check select-menu-item-icon" height="16" role="img" version="1.1" viewBox="0 0 12 16" width="12"><path d="M12 5L4 13 0 9l1.5-1.5 2.5 2.5 6.5-6.5 1.5 1.5z"></path></svg>
+ <span class="select-menu-item-text css-truncate-target" title="releases/lucene/1.9-rc1">
+ releases/lucene/1.9-rc1
+ </span>
+ </a>
+ <a class="select-menu-item js-navigation-item js-navigation-open "
+ href="/apache/lucene-solr/tree/releases/lucene/1.4.3/solr/example/files/conf/lang/stopwords_de.txt"
+ data-name="releases/lucene/1.4.3"
+ data-skip-pjax="true"
+ rel="nofollow">
+ <svg aria-hidden="true" class="octicon octicon-check select-menu-item-icon" height="16" role="img" version="1.1" viewBox="0 0 12 16" width="12"><path d="M12 5L4 13 0 9l1.5-1.5 2.5 2.5 6.5-6.5 1.5 1.5z"></path></svg>
+ <span class="select-menu-item-text css-truncate-target" title="releases/lucene/1.4.3">
+ releases/lucene/1.4.3
+ </span>
+ </a>
+ <a class="select-menu-item js-navigation-item js-navigation-open "
+ href="/apache/lucene-solr/tree/releases/lucene/1.4.2/solr/example/files/conf/lang/stopwords_de.txt"
+ data-name="releases/lucene/1.4.2"
+ data-skip-pjax="true"
+ rel="nofollow">
+ <svg aria-hidden="true" class="octicon octicon-check select-menu-item-icon" height="16" role="img" version="1.1" viewBox="0 0 12 16" width="12"><path d="M12 5L4 13 0 9l1.5-1.5 2.5 2.5 6.5-6.5 1.5 1.5z"></path></svg>
+ <span class="select-menu-item-text css-truncate-target" title="releases/lucene/1.4.2">
+ releases/lucene/1.4.2
+ </span>
+ </a>
+ <a class="select-menu-item js-navigation-item js-navigation-open "
+ href="/apache/lucene-solr/tree/releases/lucene/1.4.1/solr/example/files/conf/lang/stopwords_de.txt"
+ data-name="releases/lucene/1.4.1"
+ data-skip-pjax="true"
+ rel="nofollow">
+ <svg aria-hidden="true" class="octicon octicon-check select-menu-item-icon" height="16" role="img" version="1.1" viewBox="0 0 12 16" width="12"><path d="M12 5L4 13 0 9l1.5-1.5 2.5 2.5 6.5-6.5 1.5 1.5z"></path></svg>
+ <span class="select-menu-item-text css-truncate-target" title="releases/lucene/1.4.1">
+ releases/lucene/1.4.1
+ </span>
+ </a>
+ <a class="select-menu-item js-navigation-item js-navigation-open "
+ href="/apache/lucene-solr/tree/releases/lucene/1.4/solr/example/files/conf/lang/stopwords_de.txt"
+ data-name="releases/lucene/1.4"
+ data-skip-pjax="true"
+ rel="nofollow">
+ <svg aria-hidden="true" class="octicon octicon-check select-menu-item-icon" height="16" role="img" version="1.1" viewBox="0 0 12 16" width="12"><path d="M12 5L4 13 0 9l1.5-1.5 2.5 2.5 6.5-6.5 1.5 1.5z"></path></svg>
+ <span class="select-menu-item-text css-truncate-target" title="releases/lucene/1.4">
+ releases/lucene/1.4
+ </span>
+ </a>
+ <a class="select-menu-item js-navigation-item js-navigation-open "
+ href="/apache/lucene-solr/tree/releases/lucene/1.4-rc3/solr/example/files/conf/lang/stopwords_de.txt"
+ data-name="releases/lucene/1.4-rc3"
+ data-skip-pjax="true"
+ rel="nofollow">
+ <svg aria-hidden="true" class="octicon octicon-check select-menu-item-icon" height="16" role="img" version="1.1" viewBox="0 0 12 16" width="12"><path d="M12 5L4 13 0 9l1.5-1.5 2.5 2.5 6.5-6.5 1.5 1.5z"></path></svg>
+ <span class="select-menu-item-text css-truncate-target" title="releases/lucene/1.4-rc3">
+ releases/lucene/1.4-rc3
+ </span>
+ </a>
+ <a class="select-menu-item js-navigation-item js-navigation-open "
+ href="/apache/lucene-solr/tree/releases/lucene/1.4-rc2/solr/example/files/conf/lang/stopwords_de.txt"
+ data-name="releases/lucene/1.4-rc2"
+ data-skip-pjax="true"
+ rel="nofollow">
+ <svg aria-hidden="true" class="octicon octicon-check select-menu-item-icon" height="16" role="img" version="1.1" viewBox="0 0 12 16" width="12"><path d="M12 5L4 13 0 9l1.5-1.5 2.5 2.5 6.5-6.5 1.5 1.5z"></path></svg>
+ <span class="select-menu-item-text css-truncate-target" title="releases/lucene/1.4-rc2">
+ releases/lucene/1.4-rc2
+ </span>
+ </a>
+ <a class="select-menu-item js-navigation-item js-navigation-open "
+ href="/apache/lucene-solr/tree/releases/lucene/1.4-rc1/solr/example/files/conf/lang/stopwords_de.txt"
+ data-name="releases/lucene/1.4-rc1"
+ data-skip-pjax="true"
+ rel="nofollow">
+ <svg aria-hidden="true" class="octicon octicon-check select-menu-item-icon" height="16" role="img" version="1.1" viewBox="0 0 12 16" width="12"><path d="M12 5L4 13 0 9l1.5-1.5 2.5 2.5 6.5-6.5 1.5 1.5z"></path></svg>
+ <span class="select-menu-item-text css-truncate-target" title="releases/lucene/1.4-rc1">
+ releases/lucene/1.4-rc1
+ </span>
+ </a>
+ <a class="select-menu-item js-navigation-item js-navigation-open "
+ href="/apache/lucene-solr/tree/releases/lucene/1.3/solr/example/files/conf/lang/stopwords_de.txt"
+ data-name="releases/lucene/1.3"
+ data-skip-pjax="true"
+ rel="nofollow">
+ <svg aria-hidden="true" class="octicon octicon-check select-menu-item-icon" height="16" role="img" version="1.1" viewBox="0 0 12 16" width="12"><path d="M12 5L4 13 0 9l1.5-1.5 2.5 2.5 6.5-6.5 1.5 1.5z"></path></svg>
+ <span class="select-menu-item-text css-truncate-target" title="releases/lucene/1.3">
+ releases/lucene/1.3
+ </span>
+ </a>
+ <a class="select-menu-item js-navigation-item js-navigation-open "
+ href="/apache/lucene-solr/tree/releases/lucene/1.3-rc3/solr/example/files/conf/lang/stopwords_de.txt"
+ data-name="releases/lucene/1.3-rc3"
+ data-skip-pjax="true"
+ rel="nofollow">
+ <svg aria-hidden="true" class="octicon octicon-check select-menu-item-icon" height="16" role="img" version="1.1" viewBox="0 0 12 16" width="12"><path d="M12 5L4 13 0 9l1.5-1.5 2.5 2.5 6.5-6.5 1.5 1.5z"></path></svg>
+ <span class="select-menu-item-text css-truncate-target" title="releases/lucene/1.3-rc3">
+ releases/lucene/1.3-rc3
+ </span>
+ </a>
+ <a class="select-menu-item js-navigation-item js-navigation-open "
+ href="/apache/lucene-solr/tree/releases/lucene/1.3-rc2/solr/example/files/conf/lang/stopwords_de.txt"
+ data-name="releases/lucene/1.3-rc2"
+ data-skip-pjax="true"
+ rel="nofollow">
+ <svg aria-hidden="true" class="octicon octicon-check select-menu-item-icon" height="16" role="img" version="1.1" viewBox="0 0 12 16" width="12"><path d="M12 5L4 13 0 9l1.5-1.5 2.5 2.5 6.5-6.5 1.5 1.5z"></path></svg>
+ <span class="select-menu-item-text css-truncate-target" title="releases/lucene/1.3-rc2">
+ releases/lucene/1.3-rc2
+ </span>
+ </a>
+ <a class="select-menu-item js-navigation-item js-navigation-open "
+ href="/apache/lucene-solr/tree/releases/lucene/1.3-rc1/solr/example/files/conf/lang/stopwords_de.txt"
+ data-name="releases/lucene/1.3-rc1"
+ data-skip-pjax="true"
+ rel="nofollow">
+ <svg aria-hidden="true" class="octicon octicon-check select-menu-item-icon" height="16" role="img" version="1.1" viewBox="0 0 12 16" width="12"><path d="M12 5L4 13 0 9l1.5-1.5 2.5 2.5 6.5-6.5 1.5 1.5z"></path></svg>
+ <span class="select-menu-item-text css-truncate-target" title="releases/lucene/1.3-rc1">
+ releases/lucene/1.3-rc1
+ </span>
+ </a>
+ <a class="select-menu-item js-navigation-item js-navigation-open "
+ href="/apache/lucene-solr/tree/releases/lucene/1.2/solr/example/files/conf/lang/stopwords_de.txt"
+ data-name="releases/lucene/1.2"
+ data-skip-pjax="true"
+ rel="nofollow">
+ <svg aria-hidden="true" class="octicon octicon-check select-menu-item-icon" height="16" role="img" version="1.1" viewBox="0 0 12 16" width="12"><path d="M12 5L4 13 0 9l1.5-1.5 2.5 2.5 6.5-6.5 1.5 1.5z"></path></svg>
+ <span class="select-menu-item-text css-truncate-target" title="releases/lucene/1.2">
+ releases/lucene/1.2
+ </span>
+ </a>
+ <a class="select-menu-item js-navigation-item js-navigation-open "
+ href="/apache/lucene-solr/tree/releases/lucene/1.2-rc5/solr/example/files/conf/lang/stopwords_de.txt"
+ data-name="releases/lucene/1.2-rc5"
+ data-skip-pjax="true"
+ rel="nofollow">
+ <svg aria-hidden="true" class="octicon octicon-check select-menu-item-icon" height="16" role="img" version="1.1" viewBox="0 0 12 16" width="12"><path d="M12 5L4 13 0 9l1.5-1.5 2.5 2.5 6.5-6.5 1.5 1.5z"></path></svg>
+ <span class="select-menu-item-text css-truncate-target" title="releases/lucene/1.2-rc5">
+ releases/lucene/1.2-rc5
+ </span>
+ </a>
+ <a class="select-menu-item js-navigation-item js-navigation-open "
+ href="/apache/lucene-solr/tree/releases/lucene/1.2-rc4/solr/example/files/conf/lang/stopwords_de.txt"
+ data-name="releases/lucene/1.2-rc4"
+ data-skip-pjax="true"
+ rel="nofollow">
+ <svg aria-hidden="true" class="octicon octicon-check select-menu-item-icon" height="16" role="img" version="1.1" viewBox="0 0 12 16" width="12"><path d="M12 5L4 13 0 9l1.5-1.5 2.5 2.5 6.5-6.5 1.5 1.5z"></path></svg>
+ <span class="select-menu-item-text css-truncate-target" title="releases/lucene/1.2-rc4">
+ releases/lucene/1.2-rc4
+ </span>
+ </a>
+ <a class="select-menu-item js-navigation-item js-navigation-open "
+ href="/apache/lucene-solr/tree/releases/lucene/1.2-rc3/solr/example/files/conf/lang/stopwords_de.txt"
+ data-name="releases/lucene/1.2-rc3"
+ data-skip-pjax="true"
+ rel="nofollow">
+ <svg aria-hidden="true" class="octicon octicon-check select-menu-item-icon" height="16" role="img" version="1.1" viewBox="0 0 12 16" width="12"><path d="M12 5L4 13 0 9l1.5-1.5 2.5 2.5 6.5-6.5 1.5 1.5z"></path></svg>
+ <span class="select-menu-item-text css-truncate-target" title="releases/lucene/1.2-rc3">
+ releases/lucene/1.2-rc3
+ </span>
+ </a>
+ <a class="select-menu-item js-navigation-item js-navigation-open "
+ href="/apache/lucene-solr/tree/releases/lucene/1.2-rc2/solr/example/files/conf/lang/stopwords_de.txt"
+ data-name="releases/lucene/1.2-rc2"
+ data-skip-pjax="true"
+ rel="nofollow">
+ <svg aria-hidden="true" class="octicon octicon-check select-menu-item-icon" height="16" role="img" version="1.1" viewBox="0 0 12 16" width="12"><path d="M12 5L4 13 0 9l1.5-1.5 2.5 2.5 6.5-6.5 1.5 1.5z"></path></svg>
+ <span class="select-menu-item-text css-truncate-target" title="releases/lucene/1.2-rc2">
+ releases/lucene/1.2-rc2
+ </span>
+ </a>
+ <a class="select-menu-item js-navigation-item js-navigation-open "
+ href="/apache/lucene-solr/tree/releases/lucene/1.2-rc1/solr/example/files/conf/lang/stopwords_de.txt"
+ data-name="releases/lucene/1.2-rc1"
+ data-skip-pjax="true"
+ rel="nofollow">
+ <svg aria-hidden="true" class="octicon octicon-check select-menu-item-icon" height="16" role="img" version="1.1" viewBox="0 0 12 16" width="12"><path d="M12 5L4 13 0 9l1.5-1.5 2.5 2.5 6.5-6.5 1.5 1.5z"></path></svg>
+ <span class="select-menu-item-text css-truncate-target" title="releases/lucene/1.2-rc1">
+ releases/lucene/1.2-rc1
+ </span>
+ </a>
+ <a class="select-menu-item js-navigation-item js-navigation-open "
+ href="/apache/lucene-solr/tree/releases/lucene/1.0.1/solr/example/files/conf/lang/stopwords_de.txt"
+ data-name="releases/lucene/1.0.1"
+ data-skip-pjax="true"
+ rel="nofollow">
+ <svg aria-hidden="true" class="octicon octicon-check select-menu-item-icon" height="16" role="img" version="1.1" viewBox="0 0 12 16" width="12"><path d="M12 5L4 13 0 9l1.5-1.5 2.5 2.5 6.5-6.5 1.5 1.5z"></path></svg>
+ <span class="select-menu-item-text css-truncate-target" title="releases/lucene/1.0.1">
+ releases/lucene/1.0.1
+ </span>
+ </a>
+ <a class="select-menu-item js-navigation-item js-navigation-open "
+ href="/apache/lucene-solr/tree/releases/lucene-solr/5.5.0/solr/example/files/conf/lang/stopwords_de.txt"
+ data-name="releases/lucene-solr/5.5.0"
+ data-skip-pjax="true"
+ rel="nofollow">
+ <svg aria-hidden="true" class="octicon octicon-check select-menu-item-icon" height="16" role="img" version="1.1" viewBox="0 0 12 16" width="12"><path d="M12 5L4 13 0 9l1.5-1.5 2.5 2.5 6.5-6.5 1.5 1.5z"></path></svg>
+ <span class="select-menu-item-text css-truncate-target" title="releases/lucene-solr/5.5.0">
+ releases/lucene-solr/5.5.0
+ </span>
+ </a>
+ <a class="select-menu-item js-navigation-item js-navigation-open "
+ href="/apache/lucene-solr/tree/releases/lucene-solr/5.4.1/solr/example/files/conf/lang/stopwords_de.txt"
+ data-name="releases/lucene-solr/5.4.1"
+ data-skip-pjax="true"
+ rel="nofollow">
+ <svg aria-hidden="true" class="octicon octicon-check select-menu-item-icon" height="16" role="img" version="1.1" viewBox="0 0 12 16" width="12"><path d="M12 5L4 13 0 9l1.5-1.5 2.5 2.5 6.5-6.5 1.5 1.5z"></path></svg>
+ <span class="select-menu-item-text css-truncate-target" title="releases/lucene-solr/5.4.1">
+ releases/lucene-solr/5.4.1
+ </span>
+ </a>
+ <a class="select-menu-item js-navigation-item js-navigation-open "
+ href="/apache/lucene-solr/tree/releases/lucene-solr/5.4.0/solr/example/files/conf/lang/stopwords_de.txt"
+ data-name="releases/lucene-solr/5.4.0"
+ data-skip-pjax="true"
+ rel="nofollow">
+ <svg aria-hidden="true" class="octicon octicon-check select-menu-item-icon" height="16" role="img" version="1.1" viewBox="0 0 12 16" width="12"><path d="M12 5L4 13 0 9l1.5-1.5 2.5 2.5 6.5-6.5 1.5 1.5z"></path></svg>
+ <span class="select-menu-item-text css-truncate-target" title="releases/lucene-solr/5.4.0">
+ releases/lucene-solr/5.4.0
+ </span>
+ </a>
+ <a class="select-menu-item js-navigation-item js-navigation-open "
+ href="/apache/lucene-solr/tree/releases/lucene-solr/5.3.2/solr/example/files/conf/lang/stopwords_de.txt"
+ data-name="releases/lucene-solr/5.3.2"
+ data-skip-pjax="true"
+ rel="nofollow">
+ <svg aria-hidden="true" class="octicon octicon-check select-menu-item-icon" height="16" role="img" version="1.1" viewBox="0 0 12 16" width="12"><path d="M12 5L4 13 0 9l1.5-1.5 2.5 2.5 6.5-6.5 1.5 1.5z"></path></svg>
+ <span class="select-menu-item-text css-truncate-target" title="releases/lucene-solr/5.3.2">
+ releases/lucene-solr/5.3.2
+ </span>
+ </a>
+ <a class="select-menu-item js-navigation-item js-navigation-open "
+ href="/apache/lucene-solr/tree/releases/lucene-solr/5.3.1/solr/example/files/conf/lang/stopwords_de.txt"
+ data-name="releases/lucene-solr/5.3.1"
+ data-skip-pjax="true"
+ rel="nofollow">
+ <svg aria-hidden="true" class="octicon octicon-check select-menu-item-icon" height="16" role="img" version="1.1" viewBox="0 0 12 16" width="12"><path d="M12 5L4 13 0 9l1.5-1.5 2.5 2.5 6.5-6.5 1.5 1.5z"></path></svg>
+ <span class="select-menu-item-text css-truncate-target" title="releases/lucene-solr/5.3.1">
+ releases/lucene-solr/5.3.1
+ </span>
+ </a>
+ <a class="select-menu-item js-navigation-item js-navigation-open "
+ href="/apache/lucene-solr/tree/releases/lucene-solr/5.3.0/solr/example/files/conf/lang/stopwords_de.txt"
+ data-name="releases/lucene-solr/5.3.0"
+ data-skip-pjax="true"
+ rel="nofollow">
+ <svg aria-hidden="true" class="octicon octicon-check select-menu-item-icon" height="16" role="img" version="1.1" viewBox="0 0 12 16" width="12"><path d="M12 5L4 13 0 9l1.5-1.5 2.5 2.5 6.5-6.5 1.5 1.5z"></path></svg>
+ <span class="select-menu-item-text css-truncate-target" title="releases/lucene-solr/5.3.0">
+ releases/lucene-solr/5.3.0
+ </span>
+ </a>
+ <a class="select-menu-item js-navigation-item js-navigation-open "
+ href="/apache/lucene-solr/tree/releases/lucene-solr/5.2.1/solr/example/files/conf/lang/stopwords_de.txt"
+ data-name="releases/lucene-solr/5.2.1"
+ data-skip-pjax="true"
+ rel="nofollow">
+ <svg aria-hidden="true" class="octicon octicon-check select-menu-item-icon" height="16" role="img" version="1.1" viewBox="0 0 12 16" width="12"><path d="M12 5L4 13 0 9l1.5-1.5 2.5 2.5 6.5-6.5 1.5 1.5z"></path></svg>
+ <span class="select-menu-item-text css-truncate-target" title="releases/lucene-solr/5.2.1">
+ releases/lucene-solr/5.2.1
+ </span>
+ </a>
+ <a class="select-menu-item js-navigation-item js-navigation-open "
+ href="/apache/lucene-solr/tree/releases/lucene-solr/5.2.0/solr/example/files/conf/lang/stopwords_de.txt"
+ data-name="releases/lucene-solr/5.2.0"
+ data-skip-pjax="true"
+ rel="nofollow">
+ <svg aria-hidden="true" class="octicon octicon-check select-menu-item-icon" height="16" role="img" version="1.1" viewBox="0 0 12 16" width="12"><path d="M12 5L4 13 0 9l1.5-1.5 2.5 2.5 6.5-6.5 1.5 1.5z"></path></svg>
+ <span class="select-menu-item-text css-truncate-target" title="releases/lucene-solr/5.2.0">
+ releases/lucene-solr/5.2.0
+ </span>
+ </a>
+ <a class="select-menu-item js-navigation-item js-navigation-open "
+ href="/apache/lucene-solr/tree/releases/lucene-solr/5.1.0/solr/example/files/conf/lang/stopwords_de.txt"
+ data-name="releases/lucene-solr/5.1.0"
+ data-skip-pjax="true"
+ rel="nofollow">
+ <svg aria-hidden="true" class="octicon octicon-check select-menu-item-icon" height="16" role="img" version="1.1" viewBox="0 0 12 16" width="12"><path d="M12 5L4 13 0 9l1.5-1.5 2.5 2.5 6.5-6.5 1.5 1.5z"></path></svg>
+ <span class="select-menu-item-text css-truncate-target" title="releases/lucene-solr/5.1.0">
+ releases/lucene-solr/5.1.0
+ </span>
+ </a>
+ <a class="select-menu-item js-navigation-item js-navigation-open "
+ href="/apache/lucene-solr/tree/releases/lucene-solr/5.0.0/solr/example/files/conf/lang/stopwords_de.txt"
+ data-name="releases/lucene-solr/5.0.0"
+ data-skip-pjax="true"
+ rel="nofollow">
+ <svg aria-hidden="true" class="octicon octicon-check select-menu-item-icon" height="16" role="img" version="1.1" viewBox="0 0 12 16" width="12"><path d="M12 5L4 13 0 9l1.5-1.5 2.5 2.5 6.5-6.5 1.5 1.5z"></path></svg>
+ <span class="select-menu-item-text css-truncate-target" title="releases/lucene-solr/5.0.0">
+ releases/lucene-solr/5.0.0
+ </span>
+ </a>
+ <a class="select-menu-item js-navigation-item js-navigation-open "
+ href="/apache/lucene-solr/tree/releases/lucene-solr/4.10.4/solr/example/files/conf/lang/stopwords_de.txt"
+ data-name="releases/lucene-solr/4.10.4"
+ data-skip-pjax="true"
+ rel="nofollow">
+ <svg aria-hidden="true" class="octicon octicon-check select-menu-item-icon" height="16" role="img" version="1.1" viewBox="0 0 12 16" width="12"><path d="M12 5L4 13 0 9l1.5-1.5 2.5 2.5 6.5-6.5 1.5 1.5z"></path></svg>
+ <span class="select-menu-item-text css-truncate-target" title="releases/lucene-solr/4.10.4">
+ releases/lucene-solr/4.10.4
+ </span>
+ </a>
+ <a class="select-menu-item js-navigation-item js-navigation-open "
+ href="/apache/lucene-solr/tree/releases/lucene-solr/4.10.3/solr/example/files/conf/lang/stopwords_de.txt"
+ data-name="releases/lucene-solr/4.10.3"
+ data-skip-pjax="true"
+ rel="nofollow">
+ <svg aria-hidden="true" class="octicon octicon-check select-menu-item-icon" height="16" role="img" version="1.1" viewBox="0 0 12 16" width="12"><path d="M12 5L4 13 0 9l1.5-1.5 2.5 2.5 6.5-6.5 1.5 1.5z"></path></svg>
+ <span class="select-menu-item-text css-truncate-target" title="releases/lucene-solr/4.10.3">
+ releases/lucene-solr/4.10.3
+ </span>
+ </a>
+ <a class="select-menu-item js-navigation-item js-navigation-open "
+ href="/apache/lucene-solr/tree/releases/lucene-solr/4.10.2/solr/example/files/conf/lang/stopwords_de.txt"
+ data-name="releases/lucene-solr/4.10.2"
+ data-skip-pjax="true"
+ rel="nofollow">
+ <svg aria-hidden="true" class="octicon octicon-check select-menu-item-icon" height="16" role="img" version="1.1" viewBox="0 0 12 16" width="12"><path d="M12 5L4 13 0 9l1.5-1.5 2.5 2.5 6.5-6.5 1.5 1.5z"></path></svg>
+ <span class="select-menu-item-text css-truncate-target" title="releases/lucene-solr/4.10.2">
+ releases/lucene-solr/4.10.2
+ </span>
+ </a>
+ <a class="select-menu-item js-navigation-item js-navigation-open "
+ href="/apache/lucene-solr/tree/releases/lucene-solr/4.10.1/solr/example/files/conf/lang/stopwords_de.txt"
+ data-name="releases/lucene-solr/4.10.1"
+ data-skip-pjax="true"
+ rel="nofollow">
+ <svg aria-hidden="true" class="octicon octicon-check select-menu-item-icon" height="16" role="img" version="1.1" viewBox="0 0 12 16" width="12"><path d="M12 5L4 13 0 9l1.5-1.5 2.5 2.5 6.5-6.5 1.5 1.5z"></path></svg>
+ <span class="select-menu-item-text css-truncate-target" title="releases/lucene-solr/4.10.1">
+ releases/lucene-solr/4.10.1
+ </span>
+ </a>
+ <a class="select-menu-item js-navigation-item js-navigation-open "
+ href="/apache/lucene-solr/tree/releases/lucene-solr/4.10.0/solr/example/files/conf/lang/stopwords_de.txt"
+ data-name="releases/lucene-solr/4.10.0"
+ data-skip-pjax="true"
+ rel="nofollow">
+ <svg aria-hidden="true" class="octicon octicon-check select-menu-item-icon" height="16" role="img" version="1.1" viewBox="0 0 12 16" width="12"><path d="M12 5L4 13 0 9l1.5-1.5 2.5 2.5 6.5-6.5 1.5 1.5z"></path></svg>
+ <span class="select-menu-item-text css-truncate-target" title="releases/lucene-solr/4.10.0">
+ releases/lucene-solr/4.10.0
+ </span>
+ </a>
+ <a class="select-menu-item js-navigation-item js-navigation-open "
+ href="/apache/lucene-solr/tree/releases/lucene-solr/4.9.1/solr/example/files/conf/lang/stopwords_de.txt"
+ data-name="releases/lucene-solr/4.9.1"
+ data-skip-pjax="true"
+ rel="nofollow">
+ <svg aria-hidden="true" class="octicon octicon-check select-menu-item-icon" height="16" role="img" version="1.1" viewBox="0 0 12 16" width="12"><path d="M12 5L4 13 0 9l1.5-1.5 2.5 2.5 6.5-6.5 1.5 1.5z"></path></svg>
+ <span class="select-menu-item-text css-truncate-target" title="releases/lucene-solr/4.9.1">
+ releases/lucene-solr/4.9.1
+ </span>
+ </a>
+ <a class="select-menu-item js-navigation-item js-navigation-open "
+ href="/apache/lucene-solr/tree/releases/lucene-solr/4.9.0/solr/example/files/conf/lang/stopwords_de.txt"
+ data-name="releases/lucene-solr/4.9.0"
+ data-skip-pjax="true"
+ rel="nofollow">
+ <svg aria-hidden="true" class="octicon octicon-check select-menu-item-icon" height="16" role="img" version="1.1" viewBox="0 0 12 16" width="12"><path d="M12 5L4 13 0 9l1.5-1.5 2.5 2.5 6.5-6.5 1.5 1.5z"></path></svg>
+ <span class="select-menu-item-text css-truncate-target" title="releases/lucene-solr/4.9.0">
+ releases/lucene-solr/4.9.0
+ </span>
+ </a>
+ <a class="select-menu-item js-navigation-item js-navigation-open "
+ href="/apache/lucene-solr/tree/releases/lucene-solr/4.8.1/solr/example/files/conf/lang/stopwords_de.txt"
+ data-name="releases/lucene-solr/4.8.1"
+ data-skip-pjax="true"
+ rel="nofollow">
+ <svg aria-hidden="true" class="octicon octicon-check select-menu-item-icon" height="16" role="img" version="1.1" viewBox="0 0 12 16" width="12"><path d="M12 5L4 13 0 9l1.5-1.5 2.5 2.5 6.5-6.5 1.5 1.5z"></path></svg>
+ <span class="select-menu-item-text css-truncate-target" title="releases/lucene-solr/4.8.1">
+ releases/lucene-solr/4.8.1
+ </span>
+ </a>
+ <a class="select-menu-item js-navigation-item js-navigation-open "
+ href="/apache/lucene-solr/tree/releases/lucene-solr/4.8.0/solr/example/files/conf/lang/stopwords_de.txt"
+ data-name="releases/lucene-solr/4.8.0"
+ data-skip-pjax="true"
+ rel="nofollow">
+ <svg aria-hidden="true" class="octicon octicon-check select-menu-item-icon" height="16" role="img" version="1.1" viewBox="0 0 12 16" width="12"><path d="M12 5L4 13 0 9l1.5-1.5 2.5 2.5 6.5-6.5 1.5 1.5z"></path></svg>
+ <span class="select-menu-item-text css-truncate-target" title="releases/lucene-solr/4.8.0">
+ releases/lucene-solr/4.8.0
+ </span>
+ </a>
+ <a class="select-menu-item js-navigation-item js-navigation-open "
+ href="/apache/lucene-solr/tree/releases/lucene-solr/4.7.2/solr/example/files/conf/lang/stopwords_de.txt"
+ data-name="releases/lucene-solr/4.7.2"
+ data-skip-pjax="true"
+ rel="nofollow">
+ <svg aria-hidden="true" class="octicon octicon-check select-menu-item-icon" height="16" role="img" version="1.1" viewBox="0 0 12 16" width="12"><path d="M12 5L4 13 0 9l1.5-1.5 2.5 2.5 6.5-6.5 1.5 1.5z"></path></svg>
+ <span class="select-menu-item-text css-truncate-target" title="releases/lucene-solr/4.7.2">
+ releases/lucene-solr/4.7.2
+ </span>
+ </a>
+ <a class="select-menu-item js-navigation-item js-navigation-open "
+ href="/apache/lucene-solr/tree/releases/lucene-solr/4.7.1/solr/example/files/conf/lang/stopwords_de.txt"
+ data-name="releases/lucene-solr/4.7.1"
+ data-skip-pjax="true"
+ rel="nofollow">
+ <svg aria-hidden="true" class="octicon octicon-check select-menu-item-icon" height="16" role="img" version="1.1" viewBox="0 0 12 16" width="12"><path d="M12 5L4 13 0 9l1.5-1.5 2.5 2.5 6.5-6.5 1.5 1.5z"></path></svg>
+ <span class="select-menu-item-text css-truncate-target" title="releases/lucene-solr/4.7.1">
+ releases/lucene-solr/4.7.1
+ </span>
+ </a>
+ <a class="select-menu-item js-navigation-item js-navigation-open "
+ href="/apache/lucene-solr/tree/releases/lucene-solr/4.7.0/solr/example/files/conf/lang/stopwords_de.txt"
+ data-name="releases/lucene-solr/4.7.0"
+ data-skip-pjax="true"
+ rel="nofollow">
+ <svg aria-hidden="true" class="octicon octicon-check select-menu-item-icon" height="16" role="img" version="1.1" viewBox="0 0 12 16" width="12"><path d="M12 5L4 13 0 9l1.5-1.5 2.5 2.5 6.5-6.5 1.5 1.5z"></path></svg>
+ <span class="select-menu-item-text css-truncate-target" title="releases/lucene-solr/4.7.0">
+ releases/lucene-solr/4.7.0
+ </span>
+ </a>
+ <a class="select-menu-item js-navigation-item js-navigation-open "
+ href="/apache/lucene-solr/tree/releases/lucene-solr/4.6.1/solr/example/files/conf/lang/stopwords_de.txt"
+ data-name="releases/lucene-solr/4.6.1"
+ data-skip-pjax="true"
+ rel="nofollow">
+ <svg aria-hidden="true" class="octicon octicon-check select-menu-item-icon" height="16" role="img" version="1.1" viewBox="0 0 12 16" width="12"><path d="M12 5L4 13 0 9l1.5-1.5 2.5 2.5 6.5-6.5 1.5 1.5z"></path></svg>
+ <span class="select-menu-item-text css-truncate-target" title="releases/lucene-solr/4.6.1">
+ releases/lucene-solr/4.6.1
+ </span>
+ </a>
+ <a class="select-menu-item js-navigation-item js-navigation-open "
+ href="/apache/lucene-solr/tree/releases/lucene-solr/4.6.0/solr/example/files/conf/lang/stopwords_de.txt"
+ data-name="releases/lucene-solr/4.6.0"
+ data-skip-pjax="true"
+ rel="nofollow">
+ <svg aria-hidden="true" class="octicon octicon-check select-menu-item-icon" height="16" role="img" version="1.1" viewBox="0 0 12 16" width="12"><path d="M12 5L4 13 0 9l1.5-1.5 2.5 2.5 6.5-6.5 1.5 1.5z"></path></svg>
+ <span class="select-menu-item-text css-truncate-target" title="releases/lucene-solr/4.6.0">
+ releases/lucene-solr/4.6.0
+ </span>
+ </a>
+ <a class="select-menu-item js-navigation-item js-navigation-open "
+ href="/apache/lucene-solr/tree/releases/lucene-solr/4.5.1/solr/example/files/conf/lang/stopwords_de.txt"
+ data-name="releases/lucene-solr/4.5.1"
+ data-skip-pjax="true"
+ rel="nofollow">
+ <svg aria-hidden="true" class="octicon octicon-check select-menu-item-icon" height="16" role="img" version="1.1" viewBox="0 0 12 16" width="12"><path d="M12 5L4 13 0 9l1.5-1.5 2.5 2.5 6.5-6.5 1.5 1.5z"></path></svg>
+ <span class="select-menu-item-text css-truncate-target" title="releases/lucene-solr/4.5.1">
+ releases/lucene-solr/4.5.1
+ </span>
+ </a>
+ <a class="select-menu-item js-navigation-item js-navigation-open "
+ href="/apache/lucene-solr/tree/releases/lucene-solr/4.5.0/solr/example/files/conf/lang/stopwords_de.txt"
+ data-name="releases/lucene-solr/4.5.0"
+ data-skip-pjax="true"
+ rel="nofollow">
+ <svg aria-hidden="true" class="octicon octicon-check select-menu-item-icon" height="16" role="img" version="1.1" viewBox="0 0 12 16" width="12"><path d="M12 5L4 13 0 9l1.5-1.5 2.5 2.5 6.5-6.5 1.5 1.5z"></path></svg>
+ <span class="select-menu-item-text css-truncate-target" title="releases/lucene-solr/4.5.0">
+ releases/lucene-solr/4.5.0
+ </span>
+ </a>
+ <a class="select-menu-item js-navigation-item js-navigation-open "
+ href="/apache/lucene-solr/tree/releases/lucene-solr/4.4.0/solr/example/files/conf/lang/stopwords_de.txt"
+ data-name="releases/lucene-solr/4.4.0"
+ data-skip-pjax="true"
+ rel="nofollow">
+ <svg aria-hidden="true" class="octicon octicon-check select-menu-item-icon" height="16" role="img" version="1.1" viewBox="0 0 12 16" width="12"><path d="M12 5L4 13 0 9l1.5-1.5 2.5 2.5 6.5-6.5 1.5 1.5z"></path></svg>
+ <span class="select-menu-item-text css-truncate-target" title="releases/lucene-solr/4.4.0">
+ releases/lucene-solr/4.4.0
+ </span>
+ </a>
+ <a class="select-menu-item js-navigation-item js-navigation-open "
+ href="/apache/lucene-solr/tree/releases/lucene-solr/4.3.1/solr/example/files/conf/lang/stopwords_de.txt"
+ data-name="releases/lucene-solr/4.3.1"
+ data-skip-pjax="true"
+ rel="nofollow">
+ <svg aria-hidden="true" class="octicon octicon-check select-menu-item-icon" height="16" role="img" version="1.1" viewBox="0 0 12 16" width="12"><path d="M12 5L4 13 0 9l1.5-1.5 2.5 2.5 6.5-6.5 1.5 1.5z"></path></svg>
+ <span class="select-menu-item-text css-truncate-target" title="releases/lucene-solr/4.3.1">
+ releases/lucene-solr/4.3.1
+ </span>
+ </a>
+ <a class="select-menu-item js-navigation-item js-navigation-open "
+ href="/apache/lucene-solr/tree/releases/lucene-solr/4.3.0/solr/example/files/conf/lang/stopwords_de.txt"
+ data-name="releases/lucene-solr/4.3.0"
+ data-skip-pjax="true"
+ rel="nofollow">
+ <svg aria-hidden="true" class="octicon octicon-check select-menu-item-icon" height="16" role="img" version="1.1" viewBox="0 0 12 16" width="12"><path d="M12 5L4 13 0 9l1.5-1.5 2.5 2.5 6.5-6.5 1.5 1.5z"></path></svg>
+ <span class="select-menu-item-text css-truncate-target" title="releases/lucene-solr/4.3.0">
+ releases/lucene-solr/4.3.0
+ </span>
+ </a>
+ <a class="select-menu-item js-navigation-item js-navigation-open "
+ href="/apache/lucene-solr/tree/releases/lucene-solr/4.2.1/solr/example/files/conf/lang/stopwords_de.txt"
+ data-name="releases/lucene-solr/4.2.1"
+ data-skip-pjax="true"
+ rel="nofollow">
+ <svg aria-hidden="true" class="octicon octicon-check select-menu-item-icon" height="16" role="img" version="1.1" viewBox="0 0 12 16" width="12"><path d="M12 5L4 13 0 9l1.5-1.5 2.5 2.5 6.5-6.5 1.5 1.5z"></path></svg>
+ <span class="select-menu-item-text css-truncate-target" title="releases/lucene-solr/4.2.1">
+ releases/lucene-solr/4.2.1
+ </span>
+ </a>
+ <a class="select-menu-item js-navigation-item js-navigation-open "
+ href="/apache/lucene-solr/tree/releases/lucene-solr/4.2.0/solr/example/files/conf/lang/stopwords_de.txt"
+ data-name="releases/lucene-solr/4.2.0"
+ data-skip-pjax="true"
+ rel="nofollow">
+ <svg aria-hidden="true" class="octicon octicon-check select-menu-item-icon" height="16" role="img" version="1.1" viewBox="0 0 12 16" width="12"><path d="M12 5L4 13 0 9l1.5-1.5 2.5 2.5 6.5-6.5 1.5 1.5z"></path></svg>
+ <span class="select-menu-item-text css-truncate-target" title="releases/lucene-solr/4.2.0">
+ releases/lucene-solr/4.2.0
+ </span>
+ </a>
+ <a class="select-menu-item js-navigation-item js-navigation-open "
+ href="/apache/lucene-solr/tree/releases/lucene-solr/4.1.0/solr/example/files/conf/lang/stopwords_de.txt"
+ data-name="releases/lucene-solr/4.1.0"
+ data-skip-pjax="true"
+ rel="nofollow">
+ <svg aria-hidden="true" class="octicon octicon-check select-menu-item-icon" height="16" role="img" version="1.1" viewBox="0 0 12 16" width="12"><path d="M12 5L4 13 0 9l1.5-1.5 2.5 2.5 6.5-6.5 1.5 1.5z"></path></svg>
+ <span class="select-menu-item-text css-truncate-target" title="releases/lucene-solr/4.1.0">
+ releases/lucene-solr/4.1.0
+ </span>
+ </a>
+ <a class="select-menu-item js-navigation-item js-navigation-open "
+ href="/apache/lucene-solr/tree/releases/lucene-solr/4.0.0/solr/example/files/conf/lang/stopwords_de.txt"
+ data-name="releases/lucene-solr/4.0.0"
+ data-skip-pjax="true"
+ rel="nofollow">
+ <svg aria-hidden="true" class="octicon octicon-check select-menu-item-icon" height="16" role="img" version="1.1" viewBox="0 0 12 16" width="12"><path d="M12 5L4 13 0 9l1.5-1.5 2.5 2.5 6.5-6.5 1.5 1.5z"></path></svg>
+ <span class="select-menu-item-text css-truncate-target" title="releases/lucene-solr/4.0.0">
+ releases/lucene-solr/4.0.0
+ </span>
+ </a>
+ <a class="select-menu-item js-navigation-item js-navigation-open "
+ href="/apache/lucene-solr/tree/releases/lucene-solr/4.0.0-beta/solr/example/files/conf/lang/stopwords_de.txt"
+ data-name="releases/lucene-solr/4.0.0-beta"
+ data-skip-pjax="true"
+ rel="nofollow">
+ <svg aria-hidden="true" class="octicon octicon-check select-menu-item-icon" height="16" role="img" version="1.1" viewBox="0 0 12 16" width="12"><path d="M12 5L4 13 0 9l1.5-1.5 2.5 2.5 6.5-6.5 1.5 1.5z"></path></svg>
+ <span class="select-menu-item-text css-truncate-target" title="releases/lucene-solr/4.0.0-beta">
+ releases/lucene-solr/4.0.0-beta
+ </span>
+ </a>
+ <a class="select-menu-item js-navigation-item js-navigation-open "
+ href="/apache/lucene-solr/tree/releases/lucene-solr/4.0.0-alpha/solr/example/files/conf/lang/stopwords_de.txt"
+ data-name="releases/lucene-solr/4.0.0-alpha"
+ data-skip-pjax="true"
+ rel="nofollow">
+ <svg aria-hidden="true" class="octicon octicon-check select-menu-item-icon" height="16" role="img" version="1.1" viewBox="0 0 12 16" width="12"><path d="M12 5L4 13 0 9l1.5-1.5 2.5 2.5 6.5-6.5 1.5 1.5z"></path></svg>
+ <span class="select-menu-item-text css-truncate-target" title="releases/lucene-solr/4.0.0-alpha">
+ releases/lucene-solr/4.0.0-alpha
+ </span>
+ </a>
+ <a class="select-menu-item js-navigation-item js-navigation-open "
+ href="/apache/lucene-solr/tree/releases/lucene-solr/3.6.2/solr/example/files/conf/lang/stopwords_de.txt"
+ data-name="releases/lucene-solr/3.6.2"
+ data-skip-pjax="true"
+ rel="nofollow">
+ <svg aria-hidden="true" class="octicon octicon-check select-menu-item-icon" height="16" role="img" version="1.1" viewBox="0 0 12 16" width="12"><path d="M12 5L4 13 0 9l1.5-1.5 2.5 2.5 6.5-6.5 1.5 1.5z"></path></svg>
+ <span class="select-menu-item-text css-truncate-target" title="releases/lucene-solr/3.6.2">
+ releases/lucene-solr/3.6.2
+ </span>
+ </a>
+ <a class="select-menu-item js-navigation-item js-navigation-open "
+ href="/apache/lucene-solr/tree/releases/lucene-solr/3.6.1/solr/example/files/conf/lang/stopwords_de.txt"
+ data-name="releases/lucene-solr/3.6.1"
+ data-skip-pjax="true"
+ rel="nofollow">
+ <svg aria-hidden="true" class="octicon octicon-check select-menu-item-icon" height="16" role="img" version="1.1" viewBox="0 0 12 16" width="12"><path d="M12 5L4 13 0 9l1.5-1.5 2.5 2.5 6.5-6.5 1.5 1.5z"></path></svg>
+ <span class="select-menu-item-text css-truncate-target" title="releases/lucene-solr/3.6.1">
+ releases/lucene-solr/3.6.1
+ </span>
+ </a>
+ <a class="select-menu-item js-navigation-item js-navigation-open "
+ href="/apache/lucene-solr/tree/releases/lucene-solr/3.6.0/solr/example/files/conf/lang/stopwords_de.txt"
+ data-name="releases/lucene-solr/3.6.0"
+ data-skip-pjax="true"
+ rel="nofollow">
+ <svg aria-hidden="true" class="octicon octicon-check select-menu-item-icon" height="16" role="img" version="1.1" viewBox="0 0 12 16" width="12"><path d="M12 5L4 13 0 9l1.5-1.5 2.5 2.5 6.5-6.5 1.5 1.5z"></path></svg>
+ <span class="select-menu-item-text css-truncate-target" title="releases/lucene-solr/3.6.0">
+ releases/lucene-solr/3.6.0
+ </span>
+ </a>
+ <a class="select-menu-item js-navigation-item js-navigation-open "
+ href="/apache/lucene-solr/tree/releases/lucene-solr/3.5.0/solr/example/files/conf/lang/stopwords_de.txt"
+ data-name="releases/lucene-solr/3.5.0"
+ data-skip-pjax="true"
+ rel="nofollow">
+ <svg aria-hidden="true" class="octicon octicon-check select-menu-item-icon" height="16" role="img" version="1.1" viewBox="0 0 12 16" width="12"><path d="M12 5L4 13 0 9l1.5-1.5 2.5 2.5 6.5-6.5 1.5 1.5z"></path></svg>
+ <span class="select-menu-item-text css-truncate-target" title="releases/lucene-solr/3.5.0">
+ releases/lucene-solr/3.5.0
+ </span>
+ </a>
+ <a class="select-menu-item js-navigation-item js-navigation-open "
+ href="/apache/lucene-solr/tree/releases/lucene-solr/3.4.0/solr/example/files/conf/lang/stopwords_de.txt"
+ data-name="releases/lucene-solr/3.4.0"
+ data-skip-pjax="true"
+ rel="nofollow">
+ <svg aria-hidden="true" class="octicon octicon-check select-menu-item-icon" height="16" role="img" version="1.1" viewBox="0 0 12 16" width="12"><path d="M12 5L4 13 0 9l1.5-1.5 2.5 2.5 6.5-6.5 1.5 1.5z"></path></svg>
+ <span class="select-menu-item-text css-truncate-target" title="releases/lucene-solr/3.4.0">
+ releases/lucene-solr/3.4.0
+ </span>
+ </a>
+ <a class="select-menu-item js-navigation-item js-navigation-open "
+ href="/apache/lucene-solr/tree/releases/lucene-solr/3.3/solr/example/files/conf/lang/stopwords_de.txt"
+ data-name="releases/lucene-solr/3.3"
+ data-skip-pjax="true"
+ rel="nofollow">
+ <svg aria-hidden="true" class="octicon octicon-check select-menu-item-icon" height="16" role="img" version="1.1" viewBox="0 0 12 16" width="12"><path d="M12 5L4 13 0 9l1.5-1.5 2.5 2.5 6.5-6.5 1.5 1.5z"></path></svg>
+ <span class="select-menu-item-text css-truncate-target" title="releases/lucene-solr/3.3">
+ releases/lucene-solr/3.3
+ </span>
+ </a>
+ <a class="select-menu-item js-navigation-item js-navigation-open "
+ href="/apache/lucene-solr/tree/releases/lucene-solr/3.2/solr/example/files/conf/lang/stopwords_de.txt"
+ data-name="releases/lucene-solr/3.2"
+ data-skip-pjax="true"
+ rel="nofollow">
+ <svg aria-hidden="true" class="octicon octicon-check select-menu-item-icon" height="16" role="img" version="1.1" viewBox="0 0 12 16" width="12"><path d="M12 5L4 13 0 9l1.5-1.5 2.5 2.5 6.5-6.5 1.5 1.5z"></path></svg>
+ <span class="select-menu-item-text css-truncate-target" title="releases/lucene-solr/3.2">
+ releases/lucene-solr/3.2
+ </span>
+ </a>
+ <a class="select-menu-item js-navigation-item js-navigation-open "
+ href="/apache/lucene-solr/tree/releases/lucene-solr/3.1/solr/example/files/conf/lang/stopwords_de.txt"
+ data-name="releases/lucene-solr/3.1"
+ data-skip-pjax="true"
+ rel="nofollow">
+ <svg aria-hidden="true" class="octicon octicon-check select-menu-item-icon" height="16" role="img" version="1.1" viewBox="0 0 12 16" width="12"><path d="M12 5L4 13 0 9l1.5-1.5 2.5 2.5 6.5-6.5 1.5 1.5z"></path></svg>
+ <span class="select-menu-item-text css-truncate-target" title="releases/lucene-solr/3.1">
+ releases/lucene-solr/3.1
+ </span>
+ </a>
+ <a class="select-menu-item js-navigation-item js-navigation-open "
+ href="/apache/lucene-solr/tree/history/branches/solr/cloud/solr/example/files/conf/lang/stopwords_de.txt"
+ data-name="history/branches/solr/cloud"
+ data-skip-pjax="true"
+ rel="nofollow">
+ <svg aria-hidden="true" class="octicon octicon-check select-menu-item-icon" height="16" role="img" version="1.1" viewBox="0 0 12 16" width="12"><path d="M12 5L4 13 0 9l1.5-1.5 2.5 2.5 6.5-6.5 1.5 1.5z"></path></svg>
+ <span class="select-menu-item-text css-truncate-target" title="history/branches/solr/cloud">
+ history/branches/solr/cloud
+ </span>
+ </a>
+ <a class="select-menu-item js-navigation-item js-navigation-open "
+ href="/apache/lucene-solr/tree/history/branches/solr/branch-1.3/solr/example/files/conf/lang/stopwords_de.txt"
+ data-name="history/branches/solr/branch-1.3"
+ data-skip-pjax="true"
+ rel="nofollow">
+ <svg aria-hidden="true" class="octicon octicon-check select-menu-item-icon" height="16" role="img" version="1.1" viewBox="0 0 12 16" width="12"><path d="M12 5L4 13 0 9l1.5-1.5 2.5 2.5 6.5-6.5 1.5 1.5z"></path></svg>
+ <span class="select-menu-item-text css-truncate-target" title="history/branches/solr/branch-1.3">
+ history/branches/solr/branch-1.3
+ </span>
+ </a>
+ <a class="select-menu-item js-navigation-item js-navigation-open "
+ href="/apache/lucene-solr/tree/history/branches/lucene-solr/throwawaybranch/solr/example/files/conf/lang/stopwords_de.txt"
+ data-name="history/branches/lucene-solr/throwawaybranch"
+ data-skip-pjax="true"
+ rel="nofollow">
+ <svg aria-hidden="true" class="octicon octicon-check select-menu-item-icon" height="16" role="img" version="1.1" viewBox="0 0 12 16" width="12"><path d="M12 5L4 13 0 9l1.5-1.5 2.5 2.5 6.5-6.5 1.5 1.5z"></path></svg>
+ <span class="select-menu-item-text css-truncate-target" title="history/branches/lucene-solr/throwawaybranch">
+ history/branches/lucene-solr/throwawaybranch
+ </span>
+ </a>
+ <a class="select-menu-item js-navigation-item js-navigation-open "
+ href="/apache/lucene-solr/tree/history/branches/lucene-solr/solrcloud/solr/example/files/conf/lang/stopwords_de.txt"
+ data-name="history/branches/lucene-solr/solrcloud"
+ data-skip-pjax="true"
+ rel="nofollow">
+ <svg aria-hidden="true" class="octicon octicon-check select-menu-item-icon" height="16" role="img" version="1.1" viewBox="0 0 12 16" width="12"><path d="M12 5L4 13 0 9l1.5-1.5 2.5 2.5 6.5-6.5 1.5 1.5z"></path></svg>
+ <span class="select-menu-item-text css-truncate-target" title="history/branches/lucene-solr/solrcloud">
+ history/branches/lucene-solr/solrcloud
+ </span>
+ </a>
+ <a class="select-menu-item js-navigation-item js-navigation-open "
+ href="/apache/lucene-solr/tree/history/branches/lucene-solr/solr7790/solr/example/files/conf/lang/stopwords_de.txt"
+ data-name="history/branches/lucene-solr/solr7790"
+ data-skip-pjax="true"
+ rel="nofollow">
+ <svg aria-hidden="true" class="octicon octicon-check select-menu-item-icon" height="16" role="img" version="1.1" viewBox="0 0 12 16" width="12"><path d="M12 5L4 13 0 9l1.5-1.5 2.5 2.5 6.5-6.5 1.5 1.5z"></path></svg>
+ <span class="select-menu-item-text css-truncate-target" title="history/branches/lucene-solr/solr7790">
+ history/branches/lucene-solr/solr7790
+ </span>
+ </a>
+ <a class="select-menu-item js-navigation-item js-navigation-open "
+ href="/apache/lucene-solr/tree/history/branches/lucene-solr/solr7787/solr/example/files/conf/lang/stopwords_de.txt"
+ data-name="history/branches/lucene-solr/solr7787"
+ data-skip-pjax="true"
+ rel="nofollow">
+ <svg aria-hidden="true" class="octicon octicon-check select-menu-item-icon" height="16" role="img" version="1.1" viewBox="0 0 12 16" width="12"><path d="M12 5L4 13 0 9l1.5-1.5 2.5 2.5 6.5-6.5 1.5 1.5z"></path></svg>
+ <span class="select-menu-item-text css-truncate-target" title="history/branches/lucene-solr/solr7787">
+ history/branches/lucene-solr/solr7787
+ </span>
+ </a>
+ <a class="select-menu-item js-navigation-item js-navigation-open "
+ href="/apache/lucene-solr/tree/history/branches/lucene-solr/solr5914/solr/example/files/conf/lang/stopwords_de.txt"
+ data-name="history/branches/lucene-solr/solr5914"
+ data-skip-pjax="true"
+ rel="nofollow">
+ <svg aria-hidden="true" class="octicon octicon-check select-menu-item-icon" height="16" role="img" version="1.1" viewBox="0 0 12 16" width="12"><path d="M12 5L4 13 0 9l1.5-1.5 2.5 2.5 6.5-6.5 1.5 1.5z"></path></svg>
+ <span class="select-menu-item-text css-truncate-target" title="history/branches/lucene-solr/solr5914">
+ history/branches/lucene-solr/solr5914
+ </span>
+ </a>
+ <a class="select-menu-item js-navigation-item js-navigation-open "
+ href="/apache/lucene-solr/tree/history/branches/lucene-solr/solr5473/solr/example/files/conf/lang/stopwords_de.txt"
+ data-name="history/branches/lucene-solr/solr5473"
+ data-skip-pjax="true"
+ rel="nofollow">
+ <svg aria-hidden="true" class="octicon octicon-check select-menu-item-icon" height="16" role="img" version="1.1" viewBox="0 0 12 16" width="12"><path d="M12 5L4 13 0 9l1.5-1.5 2.5 2.5 6.5-6.5 1.5 1.5z"></path></svg>
+ <span class="select-menu-item-text css-truncate-target" title="history/branches/lucene-solr/solr5473">
+ history/branches/lucene-solr/solr5473
+ </span>
+ </a>
+ <a class="select-menu-item js-navigation-item js-navigation-open "
+ href="/apache/lucene-solr/tree/history/branches/lucene-solr/solr4470/solr/example/files/conf/lang/stopwords_de.txt"
+ data-name="history/branches/lucene-solr/solr4470"
+ data-skip-pjax="true"
+ rel="nofollow">
+ <svg aria-hidden="true" class="octicon octicon-check select-menu-item-icon" height="16" role="img" version="1.1" viewBox="0 0 12 16" width="12"><path d="M12 5L4 13 0 9l1.5-1.5 2.5 2.5 6.5-6.5 1.5 1.5z"></path></svg>
+ <span class="select-menu-item-text css-truncate-target" title="history/branches/lucene-solr/solr4470">
+ history/branches/lucene-solr/solr4470
+ </span>
+ </a>
+ <a class="select-menu-item js-navigation-item js-navigation-open "
+ href="/apache/lucene-solr/tree/history/branches/lucene-solr/solr3733/solr/example/files/conf/lang/stopwords_de.txt"
+ data-name="history/branches/lucene-solr/solr3733"
+ data-skip-pjax="true"
+ rel="nofollow">
+ <svg aria-hidden="true" class="octicon octicon-check select-menu-item-icon" height="16" role="img" version="1.1" viewBox="0 0 12 16" width="12"><path d="M12 5L4 13 0 9l1.5-1.5 2.5 2.5 6.5-6.5 1.5 1.5z"></path></svg>
+ <span class="select-menu-item-text css-truncate-target" title="history/branches/lucene-solr/solr3733">
+ history/branches/lucene-solr/solr3733
+ </span>
+ </a>
+ <a class="select-menu-item js-navigation-item js-navigation-open "
+ href="/apache/lucene-solr/tree/history/branches/lucene-solr/solr_3159_jetty8/solr/example/files/conf/lang/stopwords_de.txt"
+ data-name="history/branches/lucene-solr/solr_3159_jetty8"
+ data-skip-pjax="true"
+ rel="nofollow">
+ <svg aria-hidden="true" class="octicon octicon-check select-menu-item-icon" height="16" role="img" version="1.1" viewBox="0 0 12 16" width="12"><path d="M12 5L4 13 0 9l1.5-1.5 2.5 2.5 6.5-6.5 1.5 1.5z"></path></svg>
+ <span class="select-menu-item-text css-truncate-target" title="history/branches/lucene-solr/solr_3159_jetty8">
+ history/branches/lucene-solr/solr_3159_jetty8
+ </span>
+ </a>
+ <a class="select-menu-item js-navigation-item js-navigation-open "
+ href="/apache/lucene-solr/tree/history/branches/lucene-solr/solr2452/solr/example/files/conf/lang/stopwords_de.txt"
+ data-name="history/branches/lucene-solr/solr2452"
+ data-skip-pjax="true"
+ rel="nofollow">
+ <svg aria-hidden="true" class="octicon octicon-check select-menu-item-icon" height="16" role="img" version="1.1" viewBox="0 0 12 16" width="12"><path d="M12 5L4 13 0 9l1.5-1.5 2.5 2.5 6.5-6.5 1.5 1.5z"></path></svg>
+ <span class="select-menu-item-text css-truncate-target" title="history/branches/lucene-solr/solr2452">
+ history/branches/lucene-solr/solr2452
+ </span>
+ </a>
+ <a class="select-menu-item js-navigation-item js-navigation-open "
+ href="/apache/lucene-solr/tree/history/branches/lucene-solr/solr2193/solr/example/files/conf/lang/stopwords_de.txt"
+ data-name="history/branches/lucene-solr/solr2193"
+ data-skip-pjax="true"
+ rel="nofollow">
+ <svg aria-hidden="true" class="octicon octicon-check select-menu-item-icon" height="16" role="img" version="1.1" viewBox="0 0 12 16" width="12"><path d="M12 5L4 13 0 9l1.5-1.5 2.5 2.5 6.5-6.5 1.5 1.5z"></path></svg>
+ <span class="select-menu-item-text css-truncate-target" title="history/branches/lucene-solr/solr2193">
+ history/branches/lucene-solr/solr2193
+ </span>
+ </a>
+ <a class="select-menu-item js-navigation-item js-navigation-open "
+ href="/apache/lucene-solr/tree/history/branches/lucene-solr/solr_guice_restlet/solr/example/files/conf/lang/stopwords_de.txt"
+ data-name="history/branches/lucene-solr/solr_guice_restlet"
+ data-skip-pjax="true"
+ rel="nofollow">
+ <svg aria-hidden="true" class="octicon octicon-check select-menu-item-icon" height="16" role="img" version="1.1" viewBox="0 0 12 16" width="12"><path d="M12 5L4 13 0 9l1.5-1.5 2.5 2.5 6.5-6.5 1.5 1.5z"></path></svg>
+ <span class="select-menu-item-text css-truncate-target" title="history/branches/lucene-solr/solr_guice_restlet">
+ history/branches/lucene-solr/solr_guice_restlet
+ </span>
+ </a>
+ <a class="select-menu-item js-navigation-item js-navigation-open "
+ href="/apache/lucene-solr/tree/history/branches/lucene-solr/solr-5473/solr/example/files/conf/lang/stopwords_de.txt"
+ data-name="history/branches/lucene-solr/solr-5473"
+ data-skip-pjax="true"
+ rel="nofollow">
+ <svg aria-hidden="true" class="octicon octicon-check select-menu-item-icon" height="16" role="img" version="1.1" viewBox="0 0 12 16" width="12"><path d="M12 5L4 13 0 9l1.5-1.5 2.5 2.5 6.5-6.5 1.5 1.5z"></path></svg>
+ <span class="select-menu-item-text css-truncate-target" title="history/branches/lucene-solr/solr-5473">
+ history/branches/lucene-solr/solr-5473
+ </span>
+ </a>
+ <a class="select-menu-item js-navigation-item js-navigation-open "
+ href="/apache/lucene-solr/tree/history/branches/lucene-solr/slowclosing/solr/example/files/conf/lang/stopwords_de.txt"
+ data-name="history/branches/lucene-solr/slowclosing"
+ data-skip-pjax="true"
+ rel="nofollow">
+ <svg aria-hidden="true" class="octicon octicon-check select-menu-item-icon" height="16" role="img" version="1.1" viewBox="0 0 12 16" width="12"><path d="M12 5L4 13 0 9l1.5-1.5 2.5 2.5 6.5-6.5 1.5 1.5z"></path></svg>
+ <span class="select-menu-item-text css-truncate-target" title="history/branches/lucene-solr/slowclosing">
+ history/branches/lucene-solr/slowclosing
+ </span>
+ </a>
+ <a class="select-menu-item js-navigation-item js-navigation-open "
+ href="/apache/lucene-solr/tree/history/branches/lucene-solr/security/solr/example/files/conf/lang/stopwords_de.txt"
+ data-name="history/branches/lucene-solr/security"
+ data-skip-pjax="true"
+ rel="nofollow">
+ <svg aria-hidden="true" class="octicon octicon-check select-menu-item-icon" height="16" role="img" version="1.1" viewBox="0 0 12 16" width="12"><path d="M12 5L4 13 0 9l1.5-1.5 2.5 2.5 6.5-6.5 1.5 1.5z"></path></svg>
+ <span class="select-menu-item-text css-truncate-target" title="history/branches/lucene-solr/security">
+ history/branches/lucene-solr/security
+ </span>
+ </a>
+ <a class="select-menu-item js-navigation-item js-navigation-open "
+ href="/apache/lucene-solr/tree/history/branches/lucene-solr/realtime_search/solr/example/files/conf/lang/stopwords_de.txt"
+ data-name="history/branches/lucene-solr/realtime_search"
+ data-skip-pjax="true"
+ rel="nofollow">
+ <svg aria-hidden="true" class="octicon octicon-check select-menu-item-icon" height="16" role="img" version="1.1" viewBox="0 0 12 16" width="12"><path d="M12 5L4 13 0 9l1.5-1.5 2.5 2.5 6.5-6.5 1.5 1.5z"></path></svg>
+ <span class="select-menu-item-text css-truncate-target" title="history/branches/lucene-solr/realtime_search">
+ history/branches/lucene-solr/realtime_search
+ </span>
+ </a>
+ <a class="select-menu-item js-navigation-item js-navigation-open "
+ href="/apache/lucene-solr/tree/history/branches/lucene-solr/pseudo/solr/example/files/conf/lang/stopwords_de.txt"
+ data-name="history/branches/lucene-solr/pseudo"
+ data-skip-pjax="true"
+ rel="nofollow">
+ <svg aria-hidden="true" class="octicon octicon-check select-menu-item-icon" height="16" role="img" version="1.1" viewBox="0 0 12 16" width="12"><path d="M12 5L4 13 0 9l1.5-1.5 2.5 2.5 6.5-6.5 1.5 1.5z"></path></svg>
+ <span class="select-menu-item-text css-truncate-target" title="history/branches/lucene-solr/pseudo">
+ history/branches/lucene-solr/pseudo
+ </span>
+ </a>
+ </div>
+
+ <div class="select-menu-no-results">Nothing to show</div>
+ </div>
+
+ </div>
+ </div>
+</div>
+
+ <div class="btn-group right">
+ <a href="/apache/lucene-solr/find/master"
+ class="js-show-file-finder btn btn-sm"
+ data-pjax
+ data-hotkey="t">
+ Find file
+ </a>
+ <button aria-label="Copy file path to clipboard" class="js-zeroclipboard btn btn-sm zeroclipboard-button tooltipped tooltipped-s" data-copied-hint="Copied!" type="button">Copy path</button>
+ </div>
+ <div class="breadcrumb js-zeroclipboard-target">
+ <span class="repo-root js-repo-root"><span class="js-path-segment"><a href="/apache/lucene-solr"><span>lucene-solr</span></a></span></span><span class="separator">/</span><span class="js-path-segment"><a href="/apache/lucene-solr/tree/master/solr"><span>solr</span></a></span><span class="separator">/</span><span class="js-path-segment"><a href="/apache/lucene-solr/tree/master/solr/example"><span>example</span></a></span><span class="separator">/</span><span class="js-path-segment"><a href="/apache/lucene-solr/tree/master/solr/example/files"><span>files</span></a></span><span class="separator">/</span><span class="js-path-segment"><a href="/apache/lucene-solr/tree/master/solr/example/files/conf"><span>conf</span></a></span><span class="separator">/</span><span class="js-path-segment"><a href="/apache/lucene-solr/tree/master/solr/example/files/conf/lang"><span>lang</span></a></span><span class="separator">/</span><strong class="final-path">stopwords_de.txt</strong>
+ </div>
+</div>
+
+
+ <div class="commit-tease">
+ <span class="right">
+ <a class="commit-tease-sha" href="/apache/lucene-solr/commit/be4680abb8ca60d4206c17b9b9cb256a4406a633" data-pjax>
+ be4680a
+ </a>
+ <time datetime="2015-05-22T01:23:27Z" is="relative-time">May 22, 2015</time>
+ </span>
+ <div>
+ <img alt="@erikhatcher" class="avatar" height="20" src="https://avatars1.githubusercontent.com/u/209189?v=3&amp;s=40" width="20" />
+ <a href="/erikhatcher" class="user-mention" rel="contributor">erikhatcher</a>
+ <a href="/apache/lucene-solr/commit/be4680abb8ca60d4206c17b9b9cb256a4406a633" class="message" data-pjax="true" title="SOLR-7465: New file indexing example, under example/files
+
+git-svn-id: https://svn.apache.org/repos/asf/lucene/dev/trunk@1680973 13f79535-47bb-0310-9956-ffa450edef68">SOLR-7465: New file indexing example, under example/files</a>
+ </div>
+
+ <div class="commit-tease-contributors">
+ <button type="button" class="btn-link muted-link contributors-toggle" data-facebox="#blob_contributors_box">
+ <strong>1</strong>
+ contributor
+ </button>
+
+ </div>
+
+ <div id="blob_contributors_box" style="display:none">
+ <h2 class="facebox-header" data-facebox-id="facebox-header">Users who have contributed to this file</h2>
+ <ul class="facebox-user-list" data-facebox-id="facebox-description">
+ <li class="facebox-user-list-item">
+ <img alt="@erikhatcher" height="24" src="https://avatars3.githubusercontent.com/u/209189?v=3&amp;s=48" width="24" />
+ <a href="/erikhatcher">erikhatcher</a>
+ </li>
+ </ul>
+ </div>
+ </div>
+
+<div class="file">
+ <div class="file-header">
+ <div class="file-actions">
+
+ <div class="btn-group">
+ <a href="/apache/lucene-solr/raw/master/solr/example/files/conf/lang/stopwords_de.txt" class="btn btn-sm " id="raw-url">Raw</a>
+ <a href="/apache/lucene-solr/blame/master/solr/example/files/conf/lang/stopwords_de.txt" class="btn btn-sm js-update-url-with-hash">Blame</a>
+ <a href="/apache/lucene-solr/commits/master/solr/example/files/conf/lang/stopwords_de.txt" class="btn btn-sm " rel="nofollow">History</a>
+ </div>
+
+
+ <button type="button" class="btn-octicon disabled tooltipped tooltipped-nw"
+ aria-label="You must be signed in to make or propose changes">
+ <svg aria-hidden="true" class="octicon octicon-pencil" height="16" role="img" version="1.1" viewBox="0 0 14 16" width="14"><path d="M0 12v3h3l8-8-3-3L0 12z m3 2H1V12h1v1h1v1z m10.3-9.3l-1.3 1.3-3-3 1.3-1.3c0.39-0.39 1.02-0.39 1.41 0l1.59 1.59c0.39 0.39 0.39 1.02 0 1.41z"></path></svg>
+ </button>
+ <button type="button" class="btn-octicon btn-octicon-danger disabled tooltipped tooltipped-nw"
+ aria-label="You must be signed in to make or propose changes">
+ <svg aria-hidden="true" class="octicon octicon-trashcan" height="16" role="img" version="1.1" viewBox="0 0 12 16" width="12"><path d="M10 2H8c0-0.55-0.45-1-1-1H4c-0.55 0-1 0.45-1 1H1c-0.55 0-1 0.45-1 1v1c0 0.55 0.45 1 1 1v9c0 0.55 0.45 1 1 1h7c0.55 0 1-0.45 1-1V5c0.55 0 1-0.45 1-1v-1c0-0.55-0.45-1-1-1z m-1 12H2V5h1v8h1V5h1v8h1V5h1v8h1V5h1v9z m1-10H1v-1h9v1z"></path></svg>
+ </button>
+ </div>
+
+ <div class="file-info">
+ 295 lines (243 sloc)
+ <span class="file-info-divider"></span>
+ 4.47 KB
+ </div>
+</div>
+
+
+
+ <div itemprop="text" class="blob-wrapper data type-text">
+ <table class="highlight tab-size js-file-line-container" data-tab-size="8">
+ <tr>
+ <td id="L1" class="blob-num js-line-number" data-line-number="1"></td>
+ <td id="LC1" class="blob-code blob-code-inner js-file-line"> | From svn.tartarus.org/snowball/trunk/website/algorithms/german/stop.txt</td>
+ </tr>
+ <tr>
+ <td id="L2" class="blob-num js-line-number" data-line-number="2"></td>
+ <td id="LC2" class="blob-code blob-code-inner js-file-line"> | This file is distributed under the BSD License.</td>
+ </tr>
+ <tr>
+ <td id="L3" class="blob-num js-line-number" data-line-number="3"></td>
+ <td id="LC3" class="blob-code blob-code-inner js-file-line"> | See http://snowball.tartarus.org/license.php</td>
+ </tr>
+ <tr>
+ <td id="L4" class="blob-num js-line-number" data-line-number="4"></td>
+ <td id="LC4" class="blob-code blob-code-inner js-file-line"> | Also see http://www.opensource.org/licenses/bsd-license.html</td>
+ </tr>
+ <tr>
+ <td id="L5" class="blob-num js-line-number" data-line-number="5"></td>
+ <td id="LC5" class="blob-code blob-code-inner js-file-line"> | - Encoding was converted to UTF-8.</td>
+ </tr>
+ <tr>
+ <td id="L6" class="blob-num js-line-number" data-line-number="6"></td>
+ <td id="LC6" class="blob-code blob-code-inner js-file-line"> | - This notice was added.</td>
+ </tr>
+ <tr>
+ <td id="L7" class="blob-num js-line-number" data-line-number="7"></td>
+ <td id="LC7" class="blob-code blob-code-inner js-file-line"> |</td>
+ </tr>
+ <tr>
+ <td id="L8" class="blob-num js-line-number" data-line-number="8"></td>
+ <td id="LC8" class="blob-code blob-code-inner js-file-line"> | NOTE: To use this file with StopFilterFactory, you must specify format=&quot;snowball&quot;</td>
+ </tr>
+ <tr>
+ <td id="L9" class="blob-num js-line-number" data-line-number="9"></td>
+ <td id="LC9" class="blob-code blob-code-inner js-file-line">
+</td>
+ </tr>
+ <tr>
+ <td id="L10" class="blob-num js-line-number" data-line-number="10"></td>
+ <td id="LC10" class="blob-code blob-code-inner js-file-line"> | A German stop word list. Comments begin with vertical bar. Each stop</td>
+ </tr>
+ <tr>
+ <td id="L11" class="blob-num js-line-number" data-line-number="11"></td>
+ <td id="LC11" class="blob-code blob-code-inner js-file-line"> | word is at the start of a line.</td>
+ </tr>
+ <tr>
+ <td id="L12" class="blob-num js-line-number" data-line-number="12"></td>
+ <td id="LC12" class="blob-code blob-code-inner js-file-line">
+</td>
+ </tr>
+ <tr>
+ <td id="L13" class="blob-num js-line-number" data-line-number="13"></td>
+ <td id="LC13" class="blob-code blob-code-inner js-file-line"> | The number of forms in this list is reduced significantly by passing it</td>
+ </tr>
+ <tr>
+ <td id="L14" class="blob-num js-line-number" data-line-number="14"></td>
+ <td id="LC14" class="blob-code blob-code-inner js-file-line"> | through the German stemmer.</td>
+ </tr>
+ <tr>
+ <td id="L15" class="blob-num js-line-number" data-line-number="15"></td>
+ <td id="LC15" class="blob-code blob-code-inner js-file-line">
+</td>
+ </tr>
+ <tr>
+ <td id="L16" class="blob-num js-line-number" data-line-number="16"></td>
+ <td id="LC16" class="blob-code blob-code-inner js-file-line">
+</td>
+ </tr>
+ <tr>
+ <td id="L17" class="blob-num js-line-number" data-line-number="17"></td>
+ <td id="LC17" class="blob-code blob-code-inner js-file-line">aber | but</td>
+ </tr>
+ <tr>
+ <td id="L18" class="blob-num js-line-number" data-line-number="18"></td>
+ <td id="LC18" class="blob-code blob-code-inner js-file-line">
+</td>
+ </tr>
+ <tr>
+ <td id="L19" class="blob-num js-line-number" data-line-number="19"></td>
+ <td id="LC19" class="blob-code blob-code-inner js-file-line">alle | all</td>
+ </tr>
+ <tr>
+ <td id="L20" class="blob-num js-line-number" data-line-number="20"></td>
+ <td id="LC20" class="blob-code blob-code-inner js-file-line">allem</td>
+ </tr>
+ <tr>
+ <td id="L21" class="blob-num js-line-number" data-line-number="21"></td>
+ <td id="LC21" class="blob-code blob-code-inner js-file-line">allen</td>
+ </tr>
+ <tr>
+ <td id="L22" class="blob-num js-line-number" data-line-number="22"></td>
+ <td id="LC22" class="blob-code blob-code-inner js-file-line">aller</td>
+ </tr>
+ <tr>
+ <td id="L23" class="blob-num js-line-number" data-line-number="23"></td>
+ <td id="LC23" class="blob-code blob-code-inner js-file-line">alles</td>
+ </tr>
+ <tr>
+ <td id="L24" class="blob-num js-line-number" data-line-number="24"></td>
+ <td id="LC24" class="blob-code blob-code-inner js-file-line">
+</td>
+ </tr>
+ <tr>
+ <td id="L25" class="blob-num js-line-number" data-line-number="25"></td>
+ <td id="LC25" class="blob-code blob-code-inner js-file-line">als | than, as</td>
+ </tr>
+ <tr>
+ <td id="L26" class="blob-num js-line-number" data-line-number="26"></td>
+ <td id="LC26" class="blob-code blob-code-inner js-file-line">also | so</td>
+ </tr>
+ <tr>
+ <td id="L27" class="blob-num js-line-number" data-line-number="27"></td>
+ <td id="LC27" class="blob-code blob-code-inner js-file-line">am | an + dem</td>
+ </tr>
+ <tr>
+ <td id="L28" class="blob-num js-line-number" data-line-number="28"></td>
+ <td id="LC28" class="blob-code blob-code-inner js-file-line">an | at</td>
+ </tr>
+ <tr>
+ <td id="L29" class="blob-num js-line-number" data-line-number="29"></td>
+ <td id="LC29" class="blob-code blob-code-inner js-file-line">
+</td>
+ </tr>
+ <tr>
+ <td id="L30" class="blob-num js-line-number" data-line-number="30"></td>
+ <td id="LC30" class="blob-code blob-code-inner js-file-line">ander | other</td>
+ </tr>
+ <tr>
+ <td id="L31" class="blob-num js-line-number" data-line-number="31"></td>
+ <td id="LC31" class="blob-code blob-code-inner js-file-line">andere</td>
+ </tr>
+ <tr>
+ <td id="L32" class="blob-num js-line-number" data-line-number="32"></td>
+ <td id="LC32" class="blob-code blob-code-inner js-file-line">anderem</td>
+ </tr>
+ <tr>
+ <td id="L33" class="blob-num js-line-number" data-line-number="33"></td>
+ <td id="LC33" class="blob-code blob-code-inner js-file-line">anderen</td>
+ </tr>
+ <tr>
+ <td id="L34" class="blob-num js-line-number" data-line-number="34"></td>
+ <td id="LC34" class="blob-code blob-code-inner js-file-line">anderer</td>
+ </tr>
+ <tr>
+ <td id="L35" class="blob-num js-line-number" data-line-number="35"></td>
+ <td id="LC35" class="blob-code blob-code-inner js-file-line">anderes</td>
+ </tr>
+ <tr>
+ <td id="L36" class="blob-num js-line-number" data-line-number="36"></td>
+ <td id="LC36" class="blob-code blob-code-inner js-file-line">anderm</td>
+ </tr>
+ <tr>
+ <td id="L37" class="blob-num js-line-number" data-line-number="37"></td>
+ <td id="LC37" class="blob-code blob-code-inner js-file-line">andern</td>
+ </tr>
+ <tr>
+ <td id="L38" class="blob-num js-line-number" data-line-number="38"></td>
+ <td id="LC38" class="blob-code blob-code-inner js-file-line">anderr</td>
+ </tr>
+ <tr>
+ <td id="L39" class="blob-num js-line-number" data-line-number="39"></td>
+ <td id="LC39" class="blob-code blob-code-inner js-file-line">anders</td>
+ </tr>
+ <tr>
+ <td id="L40" class="blob-num js-line-number" data-line-number="40"></td>
+ <td id="LC40" class="blob-code blob-code-inner js-file-line">
+</td>
+ </tr>
+ <tr>
+ <td id="L41" class="blob-num js-line-number" data-line-number="41"></td>
+ <td id="LC41" class="blob-code blob-code-inner js-file-line">auch | also</td>
+ </tr>
+ <tr>
+ <td id="L42" class="blob-num js-line-number" data-line-number="42"></td>
+ <td id="LC42" class="blob-code blob-code-inner js-file-line">auf | on</td>
+ </tr>
+ <tr>
+ <td id="L43" class="blob-num js-line-number" data-line-number="43"></td>
+ <td id="LC43" class="blob-code blob-code-inner js-file-line">aus | out of</td>
+ </tr>
+ <tr>
+ <td id="L44" class="blob-num js-line-number" data-line-number="44"></td>
+ <td id="LC44" class="blob-code blob-code-inner js-file-line">bei | by</td>
+ </tr>
+ <tr>
+ <td id="L45" class="blob-num js-line-number" data-line-number="45"></td>
+ <td id="LC45" class="blob-code blob-code-inner js-file-line">bin | am</td>
+ </tr>
+ <tr>
+ <td id="L46" class="blob-num js-line-number" data-line-number="46"></td>
+ <td id="LC46" class="blob-code blob-code-inner js-file-line">bis | until</td>
+ </tr>
+ <tr>
+ <td id="L47" class="blob-num js-line-number" data-line-number="47"></td>
+ <td id="LC47" class="blob-code blob-code-inner js-file-line">bist | art</td>
+ </tr>
+ <tr>
+ <td id="L48" class="blob-num js-line-number" data-line-number="48"></td>
+ <td id="LC48" class="blob-code blob-code-inner js-file-line">da | there</td>
+ </tr>
+ <tr>
+ <td id="L49" class="blob-num js-line-number" data-line-number="49"></td>
+ <td id="LC49" class="blob-code blob-code-inner js-file-line">damit | with it</td>
+ </tr>
+ <tr>
+ <td id="L50" class="blob-num js-line-number" data-line-number="50"></td>
+ <td id="LC50" class="blob-code blob-code-inner js-file-line">dann | then</td>
+ </tr>
+ <tr>
+ <td id="L51" class="blob-num js-line-number" data-line-number="51"></td>
+ <td id="LC51" class="blob-code blob-code-inner js-file-line">
+</td>
+ </tr>
+ <tr>
+ <td id="L52" class="blob-num js-line-number" data-line-number="52"></td>
+ <td id="LC52" class="blob-code blob-code-inner js-file-line">der | the</td>
+ </tr>
+ <tr>
+ <td id="L53" class="blob-num js-line-number" data-line-number="53"></td>
+ <td id="LC53" class="blob-code blob-code-inner js-file-line">den</td>
+ </tr>
+ <tr>
+ <td id="L54" class="blob-num js-line-number" data-line-number="54"></td>
+ <td id="LC54" class="blob-code blob-code-inner js-file-line">des</td>
+ </tr>
+ <tr>
+ <td id="L55" class="blob-num js-line-number" data-line-number="55"></td>
+ <td id="LC55" class="blob-code blob-code-inner js-file-line">dem</td>
+ </tr>
+ <tr>
+ <td id="L56" class="blob-num js-line-number" data-line-number="56"></td>
+ <td id="LC56" class="blob-code blob-code-inner js-file-line">die</td>
+ </tr>
+ <tr>
+ <td id="L57" class="blob-num js-line-number" data-line-number="57"></td>
+ <td id="LC57" class="blob-code blob-code-inner js-file-line">das</td>
+ </tr>
+ <tr>
+ <td id="L58" class="blob-num js-line-number" data-line-number="58"></td>
+ <td id="LC58" class="blob-code blob-code-inner js-file-line">
+</td>
+ </tr>
+ <tr>
+ <td id="L59" class="blob-num js-line-number" data-line-number="59"></td>
+ <td id="LC59" class="blob-code blob-code-inner js-file-line">daß | that</td>
+ </tr>
+ <tr>
+ <td id="L60" class="blob-num js-line-number" data-line-number="60"></td>
+ <td id="LC60" class="blob-code blob-code-inner js-file-line">
+</td>
+ </tr>
+ <tr>
+ <td id="L61" class="blob-num js-line-number" data-line-number="61"></td>
+ <td id="LC61" class="blob-code blob-code-inner js-file-line">derselbe | the same</td>
+ </tr>
+ <tr>
+ <td id="L62" class="blob-num js-line-number" data-line-number="62"></td>
+ <td id="LC62" class="blob-code blob-code-inner js-file-line">derselben</td>
+ </tr>
+ <tr>
+ <td id="L63" class="blob-num js-line-number" data-line-number="63"></td>
+ <td id="LC63" class="blob-code blob-code-inner js-file-line">denselben</td>
+ </tr>
+ <tr>
+ <td id="L64" class="blob-num js-line-number" data-line-number="64"></td>
+ <td id="LC64" class="blob-code blob-code-inner js-file-line">desselben</td>
+ </tr>
+ <tr>
+ <td id="L65" class="blob-num js-line-number" data-line-number="65"></td>
+ <td id="LC65" class="blob-code blob-code-inner js-file-line">demselben</td>
+ </tr>
+ <tr>
+ <td id="L66" class="blob-num js-line-number" data-line-number="66"></td>
+ <td id="LC66" class="blob-code blob-code-inner js-file-line">dieselbe</td>
+ </tr>
+ <tr>
+ <td id="L67" class="blob-num js-line-number" data-line-number="67"></td>
+ <td id="LC67" class="blob-code blob-code-inner js-file-line">dieselben</td>
+ </tr>
+ <tr>
+ <td id="L68" class="blob-num js-line-number" data-line-number="68"></td>
+ <td id="LC68" class="blob-code blob-code-inner js-file-line">dasselbe</td>
+ </tr>
+ <tr>
+ <td id="L69" class="blob-num js-line-number" data-line-number="69"></td>
+ <td id="LC69" class="blob-code blob-code-inner js-file-line">
+</td>
+ </tr>
+ <tr>
+ <td id="L70" class="blob-num js-line-number" data-line-number="70"></td>
+ <td id="LC70" class="blob-code blob-code-inner js-file-line">dazu | to that</td>
+ </tr>
+ <tr>
+ <td id="L71" class="blob-num js-line-number" data-line-number="71"></td>
+ <td id="LC71" class="blob-code blob-code-inner js-file-line">
+</td>
+ </tr>
+ <tr>
+ <td id="L72" class="blob-num js-line-number" data-line-number="72"></td>
+ <td id="LC72" class="blob-code blob-code-inner js-file-line">dein | thy</td>
+ </tr>
+ <tr>
+ <td id="L73" class="blob-num js-line-number" data-line-number="73"></td>
+ <td id="LC73" class="blob-code blob-code-inner js-file-line">deine</td>
+ </tr>
+ <tr>
+ <td id="L74" class="blob-num js-line-number" data-line-number="74"></td>
+ <td id="LC74" class="blob-code blob-code-inner js-file-line">deinem</td>
+ </tr>
+ <tr>
+ <td id="L75" class="blob-num js-line-number" data-line-number="75"></td>
+ <td id="LC75" class="blob-code blob-code-inner js-file-line">deinen</td>
+ </tr>
+ <tr>
+ <td id="L76" class="blob-num js-line-number" data-line-number="76"></td>
+ <td id="LC76" class="blob-code blob-code-inner js-file-line">deiner</td>
+ </tr>
+ <tr>
+ <td id="L77" class="blob-num js-line-number" data-line-number="77"></td>
+ <td id="LC77" class="blob-code blob-code-inner js-file-line">deines</td>
+ </tr>
+ <tr>
+ <td id="L78" class="blob-num js-line-number" data-line-number="78"></td>
+ <td id="LC78" class="blob-code blob-code-inner js-file-line">
+</td>
+ </tr>
+ <tr>
+ <td id="L79" class="blob-num js-line-number" data-line-number="79"></td>
+ <td id="LC79" class="blob-code blob-code-inner js-file-line">denn | because</td>
+ </tr>
+ <tr>
+ <td id="L80" class="blob-num js-line-number" data-line-number="80"></td>
+ <td id="LC80" class="blob-code blob-code-inner js-file-line">
+</td>
+ </tr>
+ <tr>
+ <td id="L81" class="blob-num js-line-number" data-line-number="81"></td>
+ <td id="LC81" class="blob-code blob-code-inner js-file-line">derer | of those</td>
+ </tr>
+ <tr>
+ <td id="L82" class="blob-num js-line-number" data-line-number="82"></td>
+ <td id="LC82" class="blob-code blob-code-inner js-file-line">dessen | of him</td>
+ </tr>
+ <tr>
+ <td id="L83" class="blob-num js-line-number" data-line-number="83"></td>
+ <td id="LC83" class="blob-code blob-code-inner js-file-line">
+</td>
+ </tr>
+ <tr>
+ <td id="L84" class="blob-num js-line-number" data-line-number="84"></td>
+ <td id="LC84" class="blob-code blob-code-inner js-file-line">dich | thee</td>
+ </tr>
+ <tr>
+ <td id="L85" class="blob-num js-line-number" data-line-number="85"></td>
+ <td id="LC85" class="blob-code blob-code-inner js-file-line">dir | to thee</td>
+ </tr>
+ <tr>
+ <td id="L86" class="blob-num js-line-number" data-line-number="86"></td>
+ <td id="LC86" class="blob-code blob-code-inner js-file-line">du | thou</td>
+ </tr>
+ <tr>
+ <td id="L87" class="blob-num js-line-number" data-line-number="87"></td>
+ <td id="LC87" class="blob-code blob-code-inner js-file-line">
+</td>
+ </tr>
+ <tr>
+ <td id="L88" class="blob-num js-line-number" data-line-number="88"></td>
+ <td id="LC88" class="blob-code blob-code-inner js-file-line">dies | this</td>
+ </tr>
+ <tr>
+ <td id="L89" class="blob-num js-line-number" data-line-number="89"></td>
+ <td id="LC89" class="blob-code blob-code-inner js-file-line">diese</td>
+ </tr>
+ <tr>
+ <td id="L90" class="blob-num js-line-number" data-line-number="90"></td>
+ <td id="LC90" class="blob-code blob-code-inner js-file-line">diesem</td>
+ </tr>
+ <tr>
+ <td id="L91" class="blob-num js-line-number" data-line-number="91"></td>
+ <td id="LC91" class="blob-code blob-code-inner js-file-line">diesen</td>
+ </tr>
+ <tr>
+ <td id="L92" class="blob-num js-line-number" data-line-number="92"></td>
+ <td id="LC92" class="blob-code blob-code-inner js-file-line">dieser</td>
+ </tr>
+ <tr>
+ <td id="L93" class="blob-num js-line-number" data-line-number="93"></td>
+ <td id="LC93" class="blob-code blob-code-inner js-file-line">dieses</td>
+ </tr>
+ <tr>
+ <td id="L94" class="blob-num js-line-number" data-line-number="94"></td>
+ <td id="LC94" class="blob-code blob-code-inner js-file-line">
+</td>
+ </tr>
+ <tr>
+ <td id="L95" class="blob-num js-line-number" data-line-number="95"></td>
+ <td id="LC95" class="blob-code blob-code-inner js-file-line">
+</td>
+ </tr>
+ <tr>
+ <td id="L96" class="blob-num js-line-number" data-line-number="96"></td>
+ <td id="LC96" class="blob-code blob-code-inner js-file-line">doch | (several meanings)</td>
+ </tr>
+ <tr>
+ <td id="L97" class="blob-num js-line-number" data-line-number="97"></td>
+ <td id="LC97" class="blob-code blob-code-inner js-file-line">dort | (over) there</td>
+ </tr>
+ <tr>
+ <td id="L98" class="blob-num js-line-number" data-line-number="98"></td>
+ <td id="LC98" class="blob-code blob-code-inner js-file-line">
+</td>
+ </tr>
+ <tr>
+ <td id="L99" class="blob-num js-line-number" data-line-number="99"></td>
+ <td id="LC99" class="blob-code blob-code-inner js-file-line">
+</td>
+ </tr>
+ <tr>
+ <td id="L100" class="blob-num js-line-number" data-line-number="100"></td>
+ <td id="LC100" class="blob-code blob-code-inner js-file-line">durch | through</td>
+ </tr>
+ <tr>
+ <td id="L101" class="blob-num js-line-number" data-line-number="101"></td>
+ <td id="LC101" class="blob-code blob-code-inner js-file-line">
+</td>
+ </tr>
+ <tr>
+ <td id="L102" class="blob-num js-line-number" data-line-number="102"></td>
+ <td id="LC102" class="blob-code blob-code-inner js-file-line">ein | a</td>
+ </tr>
+ <tr>
+ <td id="L103" class="blob-num js-line-number" data-line-number="103"></td>
+ <td id="LC103" class="blob-code blob-code-inner js-file-line">eine</td>
+ </tr>
+ <tr>
+ <td id="L104" class="blob-num js-line-number" data-line-number="104"></td>
+ <td id="LC104" class="blob-code blob-code-inner js-file-line">einem</td>
+ </tr>
+ <tr>
+ <td id="L105" class="blob-num js-line-number" data-line-number="105"></td>
+ <td id="LC105" class="blob-code blob-code-inner js-file-line">einen</td>
+ </tr>
+ <tr>
+ <td id="L106" class="blob-num js-line-number" data-line-number="106"></td>
+ <td id="LC106" class="blob-code blob-code-inner js-file-line">einer</td>
+ </tr>
+ <tr>
+ <td id="L107" class="blob-num js-line-number" data-line-number="107"></td>
+ <td id="LC107" class="blob-code blob-code-inner js-file-line">eines</td>
+ </tr>
+ <tr>
+ <td id="L108" class="blob-num js-line-number" data-line-number="108"></td>
+ <td id="LC108" class="blob-code blob-code-inner js-file-line">
+</td>
+ </tr>
+ <tr>
+ <td id="L109" class="blob-num js-line-number" data-line-number="109"></td>
+ <td id="LC109" class="blob-code blob-code-inner js-file-line">einig | some</td>
+ </tr>
+ <tr>
+ <td id="L110" class="blob-num js-line-number" data-line-number="110"></td>
+ <td id="LC110" class="blob-code blob-code-inner js-file-line">einige</td>
+ </tr>
+ <tr>
+ <td id="L111" class="blob-num js-line-number" data-line-number="111"></td>
+ <td id="LC111" class="blob-code blob-code-inner js-file-line">einigem</td>
+ </tr>
+ <tr>
+ <td id="L112" class="blob-num js-line-number" data-line-number="112"></td>
+ <td id="LC112" class="blob-code blob-code-inner js-file-line">einigen</td>
+ </tr>
+ <tr>
+ <td id="L113" class="blob-num js-line-number" data-line-number="113"></td>
+ <td id="LC113" class="blob-code blob-code-inner js-file-line">einiger</td>
+ </tr>
+ <tr>
+ <td id="L114" class="blob-num js-line-number" data-line-number="114"></td>
+ <td id="LC114" class="blob-code blob-code-inner js-file-line">einiges</td>
+ </tr>
+ <tr>
+ <td id="L115" class="blob-num js-line-number" data-line-number="115"></td>
+ <td id="LC115" class="blob-code blob-code-inner js-file-line">
+</td>
+ </tr>
+ <tr>
+ <td id="L116" class="blob-num js-line-number" data-line-number="116"></td>
+ <td id="LC116" class="blob-code blob-code-inner js-file-line">einmal | once</td>
+ </tr>
+ <tr>
+ <td id="L117" class="blob-num js-line-number" data-line-number="117"></td>
+ <td id="LC117" class="blob-code blob-code-inner js-file-line">
+</td>
+ </tr>
+ <tr>
+ <td id="L118" class="blob-num js-line-number" data-line-number="118"></td>
+ <td id="LC118" class="blob-code blob-code-inner js-file-line">er | he</td>
+ </tr>
+ <tr>
+ <td id="L119" class="blob-num js-line-number" data-line-number="119"></td>
+ <td id="LC119" class="blob-code blob-code-inner js-file-line">ihn | him</td>
+ </tr>
+ <tr>
+ <td id="L120" class="blob-num js-line-number" data-line-number="120"></td>
+ <td id="LC120" class="blob-code blob-code-inner js-file-line">ihm | to him</td>
+ </tr>
+ <tr>
+ <td id="L121" class="blob-num js-line-number" data-line-number="121"></td>
+ <td id="LC121" class="blob-code blob-code-inner js-file-line">
+</td>
+ </tr>
+ <tr>
+ <td id="L122" class="blob-num js-line-number" data-line-number="122"></td>
+ <td id="LC122" class="blob-code blob-code-inner js-file-line">es | it</td>
+ </tr>
+ <tr>
+ <td id="L123" class="blob-num js-line-number" data-line-number="123"></td>
+ <td id="LC123" class="blob-code blob-code-inner js-file-line">etwas | something</td>
+ </tr>
+ <tr>
+ <td id="L124" class="blob-num js-line-number" data-line-number="124"></td>
+ <td id="LC124" class="blob-code blob-code-inner js-file-line">
+</td>
+ </tr>
+ <tr>
+ <td id="L125" class="blob-num js-line-number" data-line-number="125"></td>
+ <td id="LC125" class="blob-code blob-code-inner js-file-line">euer | your</td>
+ </tr>
+ <tr>
+ <td id="L126" class="blob-num js-line-number" data-line-number="126"></td>
+ <td id="LC126" class="blob-code blob-code-inner js-file-line">eure</td>
+ </tr>
+ <tr>
+ <td id="L127" class="blob-num js-line-number" data-line-number="127"></td>
+ <td id="LC127" class="blob-code blob-code-inner js-file-line">eurem</td>
+ </tr>
+ <tr>
+ <td id="L128" class="blob-num js-line-number" data-line-number="128"></td>
+ <td id="LC128" class="blob-code blob-code-inner js-file-line">euren</td>
+ </tr>
+ <tr>
+ <td id="L129" class="blob-num js-line-number" data-line-number="129"></td>
+ <td id="LC129" class="blob-code blob-code-inner js-file-line">eurer</td>
+ </tr>
+ <tr>
+ <td id="L130" class="blob-num js-line-number" data-line-number="130"></td>
+ <td id="LC130" class="blob-code blob-code-inner js-file-line">eures</td>
+ </tr>
+ <tr>
+ <td id="L131" class="blob-num js-line-number" data-line-number="131"></td>
+ <td id="LC131" class="blob-code blob-code-inner js-file-line">
+</td>
+ </tr>
+ <tr>
+ <td id="L132" class="blob-num js-line-number" data-line-number="132"></td>
+ <td id="LC132" class="blob-code blob-code-inner js-file-line">für | for</td>
+ </tr>
+ <tr>
+ <td id="L133" class="blob-num js-line-number" data-line-number="133"></td>
+ <td id="LC133" class="blob-code blob-code-inner js-file-line">gegen | towards</td>
+ </tr>
+ <tr>
+ <td id="L134" class="blob-num js-line-number" data-line-number="134"></td>
+ <td id="LC134" class="blob-code blob-code-inner js-file-line">gewesen | p.p. of sein</td>
+ </tr>
+ <tr>
+ <td id="L135" class="blob-num js-line-number" data-line-number="135"></td>
+ <td id="LC135" class="blob-code blob-code-inner js-file-line">hab | have</td>
+ </tr>
+ <tr>
+ <td id="L136" class="blob-num js-line-number" data-line-number="136"></td>
+ <td id="LC136" class="blob-code blob-code-inner js-file-line">habe | have</td>
+ </tr>
+ <tr>
+ <td id="L137" class="blob-num js-line-number" data-line-number="137"></td>
+ <td id="LC137" class="blob-code blob-code-inner js-file-line">haben | have</td>
+ </tr>
+ <tr>
+ <td id="L138" class="blob-num js-line-number" data-line-number="138"></td>
+ <td id="LC138" class="blob-code blob-code-inner js-file-line">hat | has</td>
+ </tr>
+ <tr>
+ <td id="L139" class="blob-num js-line-number" data-line-number="139"></td>
+ <td id="LC139" class="blob-code blob-code-inner js-file-line">hatte | had</td>
+ </tr>
+ <tr>
+ <td id="L140" class="blob-num js-line-number" data-line-number="140"></td>
+ <td id="LC140" class="blob-code blob-code-inner js-file-line">hatten | had</td>
+ </tr>
+ <tr>
+ <td id="L141" class="blob-num js-line-number" data-line-number="141"></td>
+ <td id="LC141" class="blob-code blob-code-inner js-file-line">hier | here</td>
+ </tr>
+ <tr>
+ <td id="L142" class="blob-num js-line-number" data-line-number="142"></td>
+ <td id="LC142" class="blob-code blob-code-inner js-file-line">hin | there</td>
+ </tr>
+ <tr>
+ <td id="L143" class="blob-num js-line-number" data-line-number="143"></td>
+ <td id="LC143" class="blob-code blob-code-inner js-file-line">hinter | behind</td>
+ </tr>
+ <tr>
+ <td id="L144" class="blob-num js-line-number" data-line-number="144"></td>
+ <td id="LC144" class="blob-code blob-code-inner js-file-line">
+</td>
+ </tr>
+ <tr>
+ <td id="L145" class="blob-num js-line-number" data-line-number="145"></td>
+ <td id="LC145" class="blob-code blob-code-inner js-file-line">ich | I</td>
+ </tr>
+ <tr>
+ <td id="L146" class="blob-num js-line-number" data-line-number="146"></td>
+ <td id="LC146" class="blob-code blob-code-inner js-file-line">mich | me</td>
+ </tr>
+ <tr>
+ <td id="L147" class="blob-num js-line-number" data-line-number="147"></td>
+ <td id="LC147" class="blob-code blob-code-inner js-file-line">mir | to me</td>
+ </tr>
+ <tr>
+ <td id="L148" class="blob-num js-line-number" data-line-number="148"></td>
+ <td id="LC148" class="blob-code blob-code-inner js-file-line">
+</td>
+ </tr>
+ <tr>
+ <td id="L149" class="blob-num js-line-number" data-line-number="149"></td>
+ <td id="LC149" class="blob-code blob-code-inner js-file-line">
+</td>
+ </tr>
+ <tr>
+ <td id="L150" class="blob-num js-line-number" data-line-number="150"></td>
+ <td id="LC150" class="blob-code blob-code-inner js-file-line">ihr | you, to her</td>
+ </tr>
+ <tr>
+ <td id="L151" class="blob-num js-line-number" data-line-number="151"></td>
+ <td id="LC151" class="blob-code blob-code-inner js-file-line">ihre</td>
+ </tr>
+ <tr>
+ <td id="L152" class="blob-num js-line-number" data-line-number="152"></td>
+ <td id="LC152" class="blob-code blob-code-inner js-file-line">ihrem</td>
+ </tr>
+ <tr>
+ <td id="L153" class="blob-num js-line-number" data-line-number="153"></td>
+ <td id="LC153" class="blob-code blob-code-inner js-file-line">ihren</td>
+ </tr>
+ <tr>
+ <td id="L154" class="blob-num js-line-number" data-line-number="154"></td>
+ <td id="LC154" class="blob-code blob-code-inner js-file-line">ihrer</td>
+ </tr>
+ <tr>
+ <td id="L155" class="blob-num js-line-number" data-line-number="155"></td>
+ <td id="LC155" class="blob-code blob-code-inner js-file-line">ihres</td>
+ </tr>
+ <tr>
+ <td id="L156" class="blob-num js-line-number" data-line-number="156"></td>
+ <td id="LC156" class="blob-code blob-code-inner js-file-line">euch | to you</td>
+ </tr>
+ <tr>
+ <td id="L157" class="blob-num js-line-number" data-line-number="157"></td>
+ <td id="LC157" class="blob-code blob-code-inner js-file-line">
+</td>
+ </tr>
+ <tr>
+ <td id="L158" class="blob-num js-line-number" data-line-number="158"></td>
+ <td id="LC158" class="blob-code blob-code-inner js-file-line">im | in + dem</td>
+ </tr>
+ <tr>
+ <td id="L159" class="blob-num js-line-number" data-line-number="159"></td>
+ <td id="LC159" class="blob-code blob-code-inner js-file-line">in | in</td>
+ </tr>
+ <tr>
+ <td id="L160" class="blob-num js-line-number" data-line-number="160"></td>
+ <td id="LC160" class="blob-code blob-code-inner js-file-line">indem | while</td>
+ </tr>
+ <tr>
+ <td id="L161" class="blob-num js-line-number" data-line-number="161"></td>
+ <td id="LC161" class="blob-code blob-code-inner js-file-line">ins | in + das</td>
+ </tr>
+ <tr>
+ <td id="L162" class="blob-num js-line-number" data-line-number="162"></td>
+ <td id="LC162" class="blob-code blob-code-inner js-file-line">ist | is</td>
+ </tr>
+ <tr>
+ <td id="L163" class="blob-num js-line-number" data-line-number="163"></td>
+ <td id="LC163" class="blob-code blob-code-inner js-file-line">
+</td>
+ </tr>
+ <tr>
+ <td id="L164" class="blob-num js-line-number" data-line-number="164"></td>
+ <td id="LC164" class="blob-code blob-code-inner js-file-line">jede | each, every</td>
+ </tr>
+ <tr>
+ <td id="L165" class="blob-num js-line-number" data-line-number="165"></td>
+ <td id="LC165" class="blob-code blob-code-inner js-file-line">jedem</td>
+ </tr>
+ <tr>
+ <td id="L166" class="blob-num js-line-number" data-line-number="166"></td>
+ <td id="LC166" class="blob-code blob-code-inner js-file-line">jeden</td>
+ </tr>
+ <tr>
+ <td id="L167" class="blob-num js-line-number" data-line-number="167"></td>
+ <td id="LC167" class="blob-code blob-code-inner js-file-line">jeder</td>
+ </tr>
+ <tr>
+ <td id="L168" class="blob-num js-line-number" data-line-number="168"></td>
+ <td id="LC168" class="blob-code blob-code-inner js-file-line">jedes</td>
+ </tr>
+ <tr>
+ <td id="L169" class="blob-num js-line-number" data-line-number="169"></td>
+ <td id="LC169" class="blob-code blob-code-inner js-file-line">
+</td>
+ </tr>
+ <tr>
+ <td id="L170" class="blob-num js-line-number" data-line-number="170"></td>
+ <td id="LC170" class="blob-code blob-code-inner js-file-line">jene | that</td>
+ </tr>
+ <tr>
+ <td id="L171" class="blob-num js-line-number" data-line-number="171"></td>
+ <td id="LC171" class="blob-code blob-code-inner js-file-line">jenem</td>
+ </tr>
+ <tr>
+ <td id="L172" class="blob-num js-line-number" data-line-number="172"></td>
+ <td id="LC172" class="blob-code blob-code-inner js-file-line">jenen</td>
+ </tr>
+ <tr>
+ <td id="L173" class="blob-num js-line-number" data-line-number="173"></td>
+ <td id="LC173" class="blob-code blob-code-inner js-file-line">jener</td>
+ </tr>
+ <tr>
+ <td id="L174" class="blob-num js-line-number" data-line-number="174"></td>
+ <td id="LC174" class="blob-code blob-code-inner js-file-line">jenes</td>
+ </tr>
+ <tr>
+ <td id="L175" class="blob-num js-line-number" data-line-number="175"></td>
+ <td id="LC175" class="blob-code blob-code-inner js-file-line">
+</td>
+ </tr>
+ <tr>
+ <td id="L176" class="blob-num js-line-number" data-line-number="176"></td>
+ <td id="LC176" class="blob-code blob-code-inner js-file-line">jetzt | now</td>
+ </tr>
+ <tr>
+ <td id="L177" class="blob-num js-line-number" data-line-number="177"></td>
+ <td id="LC177" class="blob-code blob-code-inner js-file-line">kann | can</td>
+ </tr>
+ <tr>
+ <td id="L178" class="blob-num js-line-number" data-line-number="178"></td>
+ <td id="LC178" class="blob-code blob-code-inner js-file-line">
+</td>
+ </tr>
+ <tr>
+ <td id="L179" class="blob-num js-line-number" data-line-number="179"></td>
+ <td id="LC179" class="blob-code blob-code-inner js-file-line">kein | no</td>
+ </tr>
+ <tr>
+ <td id="L180" class="blob-num js-line-number" data-line-number="180"></td>
+ <td id="LC180" class="blob-code blob-code-inner js-file-line">keine</td>
+ </tr>
+ <tr>
+ <td id="L181" class="blob-num js-line-number" data-line-number="181"></td>
+ <td id="LC181" class="blob-code blob-code-inner js-file-line">keinem</td>
+ </tr>
+ <tr>
+ <td id="L182" class="blob-num js-line-number" data-line-number="182"></td>
+ <td id="LC182" class="blob-code blob-code-inner js-file-line">keinen</td>
+ </tr>
+ <tr>
+ <td id="L183" class="blob-num js-line-number" data-line-number="183"></td>
+ <td id="LC183" class="blob-code blob-code-inner js-file-line">keiner</td>
+ </tr>
+ <tr>
+ <td id="L184" class="blob-num js-line-number" data-line-number="184"></td>
+ <td id="LC184" class="blob-code blob-code-inner js-file-line">keines</td>
+ </tr>
+ <tr>
+ <td id="L185" class="blob-num js-line-number" data-line-number="185"></td>
+ <td id="LC185" class="blob-code blob-code-inner js-file-line">
+</td>
+ </tr>
+ <tr>
+ <td id="L186" class="blob-num js-line-number" data-line-number="186"></td>
+ <td id="LC186" class="blob-code blob-code-inner js-file-line">können | can</td>
+ </tr>
+ <tr>
+ <td id="L187" class="blob-num js-line-number" data-line-number="187"></td>
+ <td id="LC187" class="blob-code blob-code-inner js-file-line">könnte | could</td>
+ </tr>
+ <tr>
+ <td id="L188" class="blob-num js-line-number" data-line-number="188"></td>
+ <td id="LC188" class="blob-code blob-code-inner js-file-line">machen | do</td>
+ </tr>
+ <tr>
+ <td id="L189" class="blob-num js-line-number" data-line-number="189"></td>
+ <td id="LC189" class="blob-code blob-code-inner js-file-line">man | one</td>
+ </tr>
+ <tr>
+ <td id="L190" class="blob-num js-line-number" data-line-number="190"></td>
+ <td id="LC190" class="blob-code blob-code-inner js-file-line">
+</td>
+ </tr>
+ <tr>
+ <td id="L191" class="blob-num js-line-number" data-line-number="191"></td>
+ <td id="LC191" class="blob-code blob-code-inner js-file-line">manche | some, many a</td>
+ </tr>
+ <tr>
+ <td id="L192" class="blob-num js-line-number" data-line-number="192"></td>
+ <td id="LC192" class="blob-code blob-code-inner js-file-line">manchem</td>
+ </tr>
+ <tr>
+ <td id="L193" class="blob-num js-line-number" data-line-number="193"></td>
+ <td id="LC193" class="blob-code blob-code-inner js-file-line">manchen</td>
+ </tr>
+ <tr>
+ <td id="L194" class="blob-num js-line-number" data-line-number="194"></td>
+ <td id="LC194" class="blob-code blob-code-inner js-file-line">mancher</td>
+ </tr>
+ <tr>
+ <td id="L195" class="blob-num js-line-number" data-line-number="195"></td>
+ <td id="LC195" class="blob-code blob-code-inner js-file-line">manches</td>
+ </tr>
+ <tr>
+ <td id="L196" class="blob-num js-line-number" data-line-number="196"></td>
+ <td id="LC196" class="blob-code blob-code-inner js-file-line">
+</td>
+ </tr>
+ <tr>
+ <td id="L197" class="blob-num js-line-number" data-line-number="197"></td>
+ <td id="LC197" class="blob-code blob-code-inner js-file-line">mein | my</td>
+ </tr>
+ <tr>
+ <td id="L198" class="blob-num js-line-number" data-line-number="198"></td>
+ <td id="LC198" class="blob-code blob-code-inner js-file-line">meine</td>
+ </tr>
+ <tr>
+ <td id="L199" class="blob-num js-line-number" data-line-number="199"></td>
+ <td id="LC199" class="blob-code blob-code-inner js-file-line">meinem</td>
+ </tr>
+ <tr>
+ <td id="L200" class="blob-num js-line-number" data-line-number="200"></td>
+ <td id="LC200" class="blob-code blob-code-inner js-file-line">meinen</td>
+ </tr>
+ <tr>
+ <td id="L201" class="blob-num js-line-number" data-line-number="201"></td>
+ <td id="LC201" class="blob-code blob-code-inner js-file-line">meiner</td>
+ </tr>
+ <tr>
+ <td id="L202" class="blob-num js-line-number" data-line-number="202"></td>
+ <td id="LC202" class="blob-code blob-code-inner js-file-line">meines</td>
+ </tr>
+ <tr>
+ <td id="L203" class="blob-num js-line-number" data-line-number="203"></td>
+ <td id="LC203" class="blob-code blob-code-inner js-file-line">
+</td>
+ </tr>
+ <tr>
+ <td id="L204" class="blob-num js-line-number" data-line-number="204"></td>
+ <td id="LC204" class="blob-code blob-code-inner js-file-line">mit | with</td>
+ </tr>
+ <tr>
+ <td id="L205" class="blob-num js-line-number" data-line-number="205"></td>
+ <td id="LC205" class="blob-code blob-code-inner js-file-line">muss | must</td>
+ </tr>
+ <tr>
+ <td id="L206" class="blob-num js-line-number" data-line-number="206"></td>
+ <td id="LC206" class="blob-code blob-code-inner js-file-line">musste | had to</td>
+ </tr>
+ <tr>
+ <td id="L207" class="blob-num js-line-number" data-line-number="207"></td>
+ <td id="LC207" class="blob-code blob-code-inner js-file-line">nach | to(wards)</td>
+ </tr>
+ <tr>
+ <td id="L208" class="blob-num js-line-number" data-line-number="208"></td>
+ <td id="LC208" class="blob-code blob-code-inner js-file-line">nicht | not</td>
+ </tr>
+ <tr>
+ <td id="L209" class="blob-num js-line-number" data-line-number="209"></td>
+ <td id="LC209" class="blob-code blob-code-inner js-file-line">nichts | nothing</td>
+ </tr>
+ <tr>
+ <td id="L210" class="blob-num js-line-number" data-line-number="210"></td>
+ <td id="LC210" class="blob-code blob-code-inner js-file-line">noch | still, yet</td>
+ </tr>
+ <tr>
+ <td id="L211" class="blob-num js-line-number" data-line-number="211"></td>
+ <td id="LC211" class="blob-code blob-code-inner js-file-line">nun | now</td>
+ </tr>
+ <tr>
+ <td id="L212" class="blob-num js-line-number" data-line-number="212"></td>
+ <td id="LC212" class="blob-code blob-code-inner js-file-line">nur | only</td>
+ </tr>
+ <tr>
+ <td id="L213" class="blob-num js-line-number" data-line-number="213"></td>
+ <td id="LC213" class="blob-code blob-code-inner js-file-line">ob | whether</td>
+ </tr>
+ <tr>
+ <td id="L214" class="blob-num js-line-number" data-line-number="214"></td>
+ <td id="LC214" class="blob-code blob-code-inner js-file-line">oder | or</td>
+ </tr>
+ <tr>
+ <td id="L215" class="blob-num js-line-number" data-line-number="215"></td>
+ <td id="LC215" class="blob-code blob-code-inner js-file-line">ohne | without</td>
+ </tr>
+ <tr>
+ <td id="L216" class="blob-num js-line-number" data-line-number="216"></td>
+ <td id="LC216" class="blob-code blob-code-inner js-file-line">sehr | very</td>
+ </tr>
+ <tr>
+ <td id="L217" class="blob-num js-line-number" data-line-number="217"></td>
+ <td id="LC217" class="blob-code blob-code-inner js-file-line">
+</td>
+ </tr>
+ <tr>
+ <td id="L218" class="blob-num js-line-number" data-line-number="218"></td>
+ <td id="LC218" class="blob-code blob-code-inner js-file-line">sein | his</td>
+ </tr>
+ <tr>
+ <td id="L219" class="blob-num js-line-number" data-line-number="219"></td>
+ <td id="LC219" class="blob-code blob-code-inner js-file-line">seine</td>
+ </tr>
+ <tr>
+ <td id="L220" class="blob-num js-line-number" data-line-number="220"></td>
+ <td id="LC220" class="blob-code blob-code-inner js-file-line">seinem</td>
+ </tr>
+ <tr>
+ <td id="L221" class="blob-num js-line-number" data-line-number="221"></td>
+ <td id="LC221" class="blob-code blob-code-inner js-file-line">seinen</td>
+ </tr>
+ <tr>
+ <td id="L222" class="blob-num js-line-number" data-line-number="222"></td>
+ <td id="LC222" class="blob-code blob-code-inner js-file-line">seiner</td>
+ </tr>
+ <tr>
+ <td id="L223" class="blob-num js-line-number" data-line-number="223"></td>
+ <td id="LC223" class="blob-code blob-code-inner js-file-line">seines</td>
+ </tr>
+ <tr>
+ <td id="L224" class="blob-num js-line-number" data-line-number="224"></td>
+ <td id="LC224" class="blob-code blob-code-inner js-file-line">
+</td>
+ </tr>
+ <tr>
+ <td id="L225" class="blob-num js-line-number" data-line-number="225"></td>
+ <td id="LC225" class="blob-code blob-code-inner js-file-line">selbst | self</td>
+ </tr>
+ <tr>
+ <td id="L226" class="blob-num js-line-number" data-line-number="226"></td>
+ <td id="LC226" class="blob-code blob-code-inner js-file-line">sich | herself</td>
+ </tr>
+ <tr>
+ <td id="L227" class="blob-num js-line-number" data-line-number="227"></td>
+ <td id="LC227" class="blob-code blob-code-inner js-file-line">
+</td>
+ </tr>
+ <tr>
+ <td id="L228" class="blob-num js-line-number" data-line-number="228"></td>
+ <td id="LC228" class="blob-code blob-code-inner js-file-line">sie | they, she</td>
+ </tr>
+ <tr>
+ <td id="L229" class="blob-num js-line-number" data-line-number="229"></td>
+ <td id="LC229" class="blob-code blob-code-inner js-file-line">ihnen | to them</td>
+ </tr>
+ <tr>
+ <td id="L230" class="blob-num js-line-number" data-line-number="230"></td>
+ <td id="LC230" class="blob-code blob-code-inner js-file-line">
+</td>
+ </tr>
+ <tr>
+ <td id="L231" class="blob-num js-line-number" data-line-number="231"></td>
+ <td id="LC231" class="blob-code blob-code-inner js-file-line">sind | are</td>
+ </tr>
+ <tr>
+ <td id="L232" class="blob-num js-line-number" data-line-number="232"></td>
+ <td id="LC232" class="blob-code blob-code-inner js-file-line">so | so</td>
+ </tr>
+ <tr>
+ <td id="L233" class="blob-num js-line-number" data-line-number="233"></td>
+ <td id="LC233" class="blob-code blob-code-inner js-file-line">
+</td>
+ </tr>
+ <tr>
+ <td id="L234" class="blob-num js-line-number" data-line-number="234"></td>
+ <td id="LC234" class="blob-code blob-code-inner js-file-line">solche | such</td>
+ </tr>
+ <tr>
+ <td id="L235" class="blob-num js-line-number" data-line-number="235"></td>
+ <td id="LC235" class="blob-code blob-code-inner js-file-line">solchem</td>
+ </tr>
+ <tr>
+ <td id="L236" class="blob-num js-line-number" data-line-number="236"></td>
+ <td id="LC236" class="blob-code blob-code-inner js-file-line">solchen</td>
+ </tr>
+ <tr>
+ <td id="L237" class="blob-num js-line-number" data-line-number="237"></td>
+ <td id="LC237" class="blob-code blob-code-inner js-file-line">solcher</td>
+ </tr>
+ <tr>
+ <td id="L238" class="blob-num js-line-number" data-line-number="238"></td>
+ <td id="LC238" class="blob-code blob-code-inner js-file-line">solches</td>
+ </tr>
+ <tr>
+ <td id="L239" class="blob-num js-line-number" data-line-number="239"></td>
+ <td id="LC239" class="blob-code blob-code-inner js-file-line">
+</td>
+ </tr>
+ <tr>
+ <td id="L240" class="blob-num js-line-number" data-line-number="240"></td>
+ <td id="LC240" class="blob-code blob-code-inner js-file-line">soll | shall</td>
+ </tr>
+ <tr>
+ <td id="L241" class="blob-num js-line-number" data-line-number="241"></td>
+ <td id="LC241" class="blob-code blob-code-inner js-file-line">sollte | should</td>
+ </tr>
+ <tr>
+ <td id="L242" class="blob-num js-line-number" data-line-number="242"></td>
+ <td id="LC242" class="blob-code blob-code-inner js-file-line">sondern | but</td>
+ </tr>
+ <tr>
+ <td id="L243" class="blob-num js-line-number" data-line-number="243"></td>
+ <td id="LC243" class="blob-code blob-code-inner js-file-line">sonst | else</td>
+ </tr>
+ <tr>
+ <td id="L244" class="blob-num js-line-number" data-line-number="244"></td>
+ <td id="LC244" class="blob-code blob-code-inner js-file-line">über | over</td>
+ </tr>
+ <tr>
+ <td id="L245" class="blob-num js-line-number" data-line-number="245"></td>
+ <td id="LC245" class="blob-code blob-code-inner js-file-line">um | about, around</td>
+ </tr>
+ <tr>
+ <td id="L246" class="blob-num js-line-number" data-line-number="246"></td>
+ <td id="LC246" class="blob-code blob-code-inner js-file-line">und | and</td>
+ </tr>
+ <tr>
+ <td id="L247" class="blob-num js-line-number" data-line-number="247"></td>
+ <td id="LC247" class="blob-code blob-code-inner js-file-line">
+</td>
+ </tr>
+ <tr>
+ <td id="L248" class="blob-num js-line-number" data-line-number="248"></td>
+ <td id="LC248" class="blob-code blob-code-inner js-file-line">uns | us</td>
+ </tr>
+ <tr>
+ <td id="L249" class="blob-num js-line-number" data-line-number="249"></td>
+ <td id="LC249" class="blob-code blob-code-inner js-file-line">unse</td>
+ </tr>
+ <tr>
+ <td id="L250" class="blob-num js-line-number" data-line-number="250"></td>
+ <td id="LC250" class="blob-code blob-code-inner js-file-line">unsem</td>
+ </tr>
+ <tr>
+ <td id="L251" class="blob-num js-line-number" data-line-number="251"></td>
+ <td id="LC251" class="blob-code blob-code-inner js-file-line">unsen</td>
+ </tr>
+ <tr>
+ <td id="L252" class="blob-num js-line-number" data-line-number="252"></td>
+ <td id="LC252" class="blob-code blob-code-inner js-file-line">unser</td>
+ </tr>
+ <tr>
+ <td id="L253" class="blob-num js-line-number" data-line-number="253"></td>
+ <td id="LC253" class="blob-code blob-code-inner js-file-line">unses</td>
+ </tr>
+ <tr>
+ <td id="L254" class="blob-num js-line-number" data-line-number="254"></td>
+ <td id="LC254" class="blob-code blob-code-inner js-file-line">
+</td>
+ </tr>
+ <tr>
+ <td id="L255" class="blob-num js-line-number" data-line-number="255"></td>
+ <td id="LC255" class="blob-code blob-code-inner js-file-line">unter | under</td>
+ </tr>
+ <tr>
+ <td id="L256" class="blob-num js-line-number" data-line-number="256"></td>
+ <td id="LC256" class="blob-code blob-code-inner js-file-line">viel | much</td>
+ </tr>
+ <tr>
+ <td id="L257" class="blob-num js-line-number" data-line-number="257"></td>
+ <td id="LC257" class="blob-code blob-code-inner js-file-line">vom | von + dem</td>
+ </tr>
+ <tr>
+ <td id="L258" class="blob-num js-line-number" data-line-number="258"></td>
+ <td id="LC258" class="blob-code blob-code-inner js-file-line">von | from</td>
+ </tr>
+ <tr>
+ <td id="L259" class="blob-num js-line-number" data-line-number="259"></td>
+ <td id="LC259" class="blob-code blob-code-inner js-file-line">vor | before</td>
+ </tr>
+ <tr>
+ <td id="L260" class="blob-num js-line-number" data-line-number="260"></td>
+ <td id="LC260" class="blob-code blob-code-inner js-file-line">während | while</td>
+ </tr>
+ <tr>
+ <td id="L261" class="blob-num js-line-number" data-line-number="261"></td>
+ <td id="LC261" class="blob-code blob-code-inner js-file-line">war | was</td>
+ </tr>
+ <tr>
+ <td id="L262" class="blob-num js-line-number" data-line-number="262"></td>
+ <td id="LC262" class="blob-code blob-code-inner js-file-line">waren | were</td>
+ </tr>
+ <tr>
+ <td id="L263" class="blob-num js-line-number" data-line-number="263"></td>
+ <td id="LC263" class="blob-code blob-code-inner js-file-line">warst | wast</td>
+ </tr>
+ <tr>
+ <td id="L264" class="blob-num js-line-number" data-line-number="264"></td>
+ <td id="LC264" class="blob-code blob-code-inner js-file-line">was | what</td>
+ </tr>
+ <tr>
+ <td id="L265" class="blob-num js-line-number" data-line-number="265"></td>
+ <td id="LC265" class="blob-code blob-code-inner js-file-line">weg | away, off</td>
+ </tr>
+ <tr>
+ <td id="L266" class="blob-num js-line-number" data-line-number="266"></td>
+ <td id="LC266" class="blob-code blob-code-inner js-file-line">weil | because</td>
+ </tr>
+ <tr>
+ <td id="L267" class="blob-num js-line-number" data-line-number="267"></td>
+ <td id="LC267" class="blob-code blob-code-inner js-file-line">weiter | further</td>
+ </tr>
+ <tr>
+ <td id="L268" class="blob-num js-line-number" data-line-number="268"></td>
+ <td id="LC268" class="blob-code blob-code-inner js-file-line">
+</td>
+ </tr>
+ <tr>
+ <td id="L269" class="blob-num js-line-number" data-line-number="269"></td>
+ <td id="LC269" class="blob-code blob-code-inner js-file-line">welche | which</td>
+ </tr>
+ <tr>
+ <td id="L270" class="blob-num js-line-number" data-line-number="270"></td>
+ <td id="LC270" class="blob-code blob-code-inner js-file-line">welchem</td>
+ </tr>
+ <tr>
+ <td id="L271" class="blob-num js-line-number" data-line-number="271"></td>
+ <td id="LC271" class="blob-code blob-code-inner js-file-line">welchen</td>
+ </tr>
+ <tr>
+ <td id="L272" class="blob-num js-line-number" data-line-number="272"></td>
+ <td id="LC272" class="blob-code blob-code-inner js-file-line">welcher</td>
+ </tr>
+ <tr>
+ <td id="L273" class="blob-num js-line-number" data-line-number="273"></td>
+ <td id="LC273" class="blob-code blob-code-inner js-file-line">welches</td>
+ </tr>
+ <tr>
+ <td id="L274" class="blob-num js-line-number" data-line-number="274"></td>
+ <td id="LC274" class="blob-code blob-code-inner js-file-line">
+</td>
+ </tr>
+ <tr>
+ <td id="L275" class="blob-num js-line-number" data-line-number="275"></td>
+ <td id="LC275" class="blob-code blob-code-inner js-file-line">wenn | when</td>
+ </tr>
+ <tr>
+ <td id="L276" class="blob-num js-line-number" data-line-number="276"></td>
+ <td id="LC276" class="blob-code blob-code-inner js-file-line">werde | will</td>
+ </tr>
+ <tr>
+ <td id="L277" class="blob-num js-line-number" data-line-number="277"></td>
+ <td id="LC277" class="blob-code blob-code-inner js-file-line">werden | will</td>
+ </tr>
+ <tr>
+ <td id="L278" class="blob-num js-line-number" data-line-number="278"></td>
+ <td id="LC278" class="blob-code blob-code-inner js-file-line">wie | how</td>
+ </tr>
+ <tr>
+ <td id="L279" class="blob-num js-line-number" data-line-number="279"></td>
+ <td id="LC279" class="blob-code blob-code-inner js-file-line">wieder | again</td>
+ </tr>
+ <tr>
+ <td id="L280" class="blob-num js-line-number" data-line-number="280"></td>
+ <td id="LC280" class="blob-code blob-code-inner js-file-line">will | want</td>
+ </tr>
+ <tr>
+ <td id="L281" class="blob-num js-line-number" data-line-number="281"></td>
+ <td id="LC281" class="blob-code blob-code-inner js-file-line">wir | we</td>
+ </tr>
+ <tr>
+ <td id="L282" class="blob-num js-line-number" data-line-number="282"></td>
+ <td id="LC282" class="blob-code blob-code-inner js-file-line">wird | will</td>
+ </tr>
+ <tr>
+ <td id="L283" class="blob-num js-line-number" data-line-number="283"></td>
+ <td id="LC283" class="blob-code blob-code-inner js-file-line">wirst | willst</td>
+ </tr>
+ <tr>
+ <td id="L284" class="blob-num js-line-number" data-line-number="284"></td>
+ <td id="LC284" class="blob-code blob-code-inner js-file-line">wo | where</td>
+ </tr>
+ <tr>
+ <td id="L285" class="blob-num js-line-number" data-line-number="285"></td>
+ <td id="LC285" class="blob-code blob-code-inner js-file-line">wollen | want</td>
+ </tr>
+ <tr>
+ <td id="L286" class="blob-num js-line-number" data-line-number="286"></td>
+ <td id="LC286" class="blob-code blob-code-inner js-file-line">wollte | wanted</td>
+ </tr>
+ <tr>
+ <td id="L287" class="blob-num js-line-number" data-line-number="287"></td>
+ <td id="LC287" class="blob-code blob-code-inner js-file-line">würde | would</td>
+ </tr>
+ <tr>
+ <td id="L288" class="blob-num js-line-number" data-line-number="288"></td>
+ <td id="LC288" class="blob-code blob-code-inner js-file-line">würden | would</td>
+ </tr>
+ <tr>
+ <td id="L289" class="blob-num js-line-number" data-line-number="289"></td>
+ <td id="LC289" class="blob-code blob-code-inner js-file-line">zu | to</td>
+ </tr>
+ <tr>
+ <td id="L290" class="blob-num js-line-number" data-line-number="290"></td>
+ <td id="LC290" class="blob-code blob-code-inner js-file-line">zum | zu + dem</td>
+ </tr>
+ <tr>
+ <td id="L291" class="blob-num js-line-number" data-line-number="291"></td>
+ <td id="LC291" class="blob-code blob-code-inner js-file-line">zur | zu + der</td>
+ </tr>
+ <tr>
+ <td id="L292" class="blob-num js-line-number" data-line-number="292"></td>
+ <td id="LC292" class="blob-code blob-code-inner js-file-line">zwar | indeed</td>
+ </tr>
+ <tr>
+ <td id="L293" class="blob-num js-line-number" data-line-number="293"></td>
+ <td id="LC293" class="blob-code blob-code-inner js-file-line">zwischen | between</td>
+ </tr>
+ <tr>
+ <td id="L294" class="blob-num js-line-number" data-line-number="294"></td>
+ <td id="LC294" class="blob-code blob-code-inner js-file-line">
+</td>
+ </tr>
+</table>
+
+ </div>
+
+</div>
+
+<button type="button" data-facebox="#jump-to-line" data-facebox-class="linejump" data-hotkey="l" class="hidden">Jump to Line</button>
+<div id="jump-to-line" style="display:none">
+ <!-- </textarea> --><!-- '"` --><form accept-charset="UTF-8" action="" class="js-jump-to-line-form" method="get"><div style="margin:0;padding:0;display:inline"><input name="utf8" type="hidden" value="&#x2713;" /></div>
+ <input class="linejump-input js-jump-to-line-field" type="text" placeholder="Jump to line&hellip;" aria-label="Jump to line" autofocus>
+ <button type="submit" class="btn">Go</button>
+</form></div>
+
+ </div>
+ <div class="modal-backdrop"></div>
+</div>
+
+
+ </div>
+ </div>
+
+ </div>
+
+ <div class="container site-footer-container">
+ <div class="site-footer" role="contentinfo">
+ <ul class="site-footer-links right">
+ <li><a href="https://status.github.com/" data-ga-click="Footer, go to status, text:status">Status</a></li>
+ <li><a href="https://developer.github.com" data-ga-click="Footer, go to api, text:api">API</a></li>
+ <li><a href="https://training.github.com" data-ga-click="Footer, go to training, text:training">Training</a></li>
+ <li><a href="https://shop.github.com" data-ga-click="Footer, go to shop, text:shop">Shop</a></li>
+ <li><a href="https://github.com/blog" data-ga-click="Footer, go to blog, text:blog">Blog</a></li>
+ <li><a href="https://github.com/about" data-ga-click="Footer, go to about, text:about">About</a></li>
+ <li><a href="https://github.com/pricing" data-ga-click="Footer, go to pricing, text:pricing">Pricing</a></li>
+
+ </ul>
+
+ <a href="https://github.com" aria-label="Homepage" class="site-footer-mark">
+ <svg aria-hidden="true" class="octicon octicon-mark-github" height="24" role="img" title="GitHub " version="1.1" viewBox="0 0 16 16" width="24"><path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59 0.4 0.07 0.55-0.17 0.55-0.38 0-0.19-0.01-0.82-0.01-1.49-2.01 0.37-2.53-0.49-2.69-0.94-0.09-0.23-0.48-0.94-0.82-1.13-0.28-0.15-0.68-0.52-0.01-0.53 0.63-0.01 1.08 0.58 1.23 0.82 0.72 1.21 1.87 0.87 2.33 0.66 0.07-0.52 0.28-0.87 0.51-1.07-1.78-0.2-3.64-0.89-3.64-3.95 0-0.87 0.31-1.59 0.82-2.15-0.08-0.2-0.36-1.02 0.08-2.12 0 0 0.67-0.21 2.2 0.82 0.64-0.18 1.32-0.27 2-0.27 0.68 0 1.36 0.09 2 0.27 1.53-1.04 2.2-0.82 2.2-0.82 0.44 1.1 0.16 1.92 0.08 2.12 0.51 0.56 0.82 1.27 0.82 2.15 0 3.07-1.87 3.75-3.65 3.95 0.29 0.25 0.54 0.73 0.54 1.48 0 1.07-0.01 1.93-0.01 2.2 0 0.21 0.15 0.46 0.55 0.38C13.71 14.53 16 11.53 16 8 16 3.58 12.42 0 8 0z"></path></svg>
+</a>
+ <ul class="site-footer-links">
+ <li>&copy; 2016 <span title="0.13439s from github-fe127-cp1-prd.iad.github.net">GitHub</span>, Inc.</li>
+ <li><a href="https://github.com/site/terms" data-ga-click="Footer, go to terms, text:terms">Terms</a></li>
+ <li><a href="https://github.com/site/privacy" data-ga-click="Footer, go to privacy, text:privacy">Privacy</a></li>
+ <li><a href="https://github.com/security" data-ga-click="Footer, go to security, text:security">Security</a></li>
+ <li><a href="https://github.com/contact" data-ga-click="Footer, go to contact, text:contact">Contact</a></li>
+ <li><a href="https://help.github.com" data-ga-click="Footer, go to help, text:help">Help</a></li>
+ </ul>
+ </div>
+</div>
+
+
+
+
+
+
+
+ <div id="ajax-error-message" class="ajax-error-message flash flash-error">
+ <svg aria-hidden="true" class="octicon octicon-alert" height="16" role="img" version="1.1" viewBox="0 0 16 16" width="16"><path d="M15.72 12.5l-6.85-11.98C8.69 0.21 8.36 0.02 8 0.02s-0.69 0.19-0.87 0.5l-6.85 11.98c-0.18 0.31-0.18 0.69 0 1C0.47 13.81 0.8 14 1.15 14h13.7c0.36 0 0.69-0.19 0.86-0.5S15.89 12.81 15.72 12.5zM9 12H7V10h2V12zM9 9H7V5h2V9z"></path></svg>
+ <button type="button" class="flash-close js-flash-close js-ajax-error-dismiss" aria-label="Dismiss error">
+ <svg aria-hidden="true" class="octicon octicon-x" height="16" role="img" version="1.1" viewBox="0 0 12 16" width="12"><path d="M7.48 8l3.75 3.75-1.48 1.48-3.75-3.75-3.75 3.75-1.48-1.48 3.75-3.75L0.77 4.25l1.48-1.48 3.75 3.75 3.75-3.75 1.48 1.48-3.75 3.75z"></path></svg>
+ </button>
+ Something went wrong with that request. Please try again.
+ </div>
+
+
+ <script crossorigin="anonymous" src="https://assets-cdn.github.com/assets/compat-7db58f8b7b91111107fac755dd8b178fe7db0f209ced51fc339c446ad3f8da2b.js"></script>
+ <script crossorigin="anonymous" src="https://assets-cdn.github.com/assets/frameworks-96e29b697be2d5e4c062e40ce2dab35f90b4cd378cfbbcd6dd51a3f280e7d426.js"></script>
+ <script async="async" crossorigin="anonymous" src="https://assets-cdn.github.com/assets/github-061f27987895ca9e650b5c852acbea14f7b6e360f8d0a2bf2e5692911aa3926c.js"></script>
+
+
+
+ <div class="js-stale-session-flash stale-session-flash flash flash-warn flash-banner hidden">
+ <svg aria-hidden="true" class="octicon octicon-alert" height="16" role="img" version="1.1" viewBox="0 0 16 16" width="16"><path d="M15.72 12.5l-6.85-11.98C8.69 0.21 8.36 0.02 8 0.02s-0.69 0.19-0.87 0.5l-6.85 11.98c-0.18 0.31-0.18 0.69 0 1C0.47 13.81 0.8 14 1.15 14h13.7c0.36 0 0.69-0.19 0.86-0.5S15.89 12.81 15.72 12.5zM9 12H7V10h2V12zM9 9H7V5h2V9z"></path></svg>
+ <span class="signed-in-tab-flash">You signed in with another tab or window. <a href="">Reload</a> to refresh your session.</span>
+ <span class="signed-out-tab-flash">You signed out in another tab or window. <a href="">Reload</a> to refresh your session.</span>
+ </div>
+ <div class="facebox" id="facebox" style="display:none;">
+ <div class="facebox-popup">
+ <div class="facebox-content" role="dialog" aria-labelledby="facebox-header" aria-describedby="facebox-description">
+ </div>
+ <button type="button" class="facebox-close js-facebox-close" aria-label="Close modal">
+ <svg aria-hidden="true" class="octicon octicon-x" height="16" role="img" version="1.1" viewBox="0 0 12 16" width="12"><path d="M7.48 8l3.75 3.75-1.48 1.48-3.75-3.75-3.75 3.75-1.48-1.48 3.75-3.75L0.77 4.25l1.48-1.48 3.75 3.75 3.75-3.75 1.48 1.48-3.75 3.75z"></path></svg>
+ </button>
+ </div>
+</div>
+
+ </body>
+</html>
+
diff --git a/src/lib-fts/stopwords/stopwords_nl.txt b/src/lib-fts/stopwords/stopwords_nl.txt
new file mode 100644
index 0000000..6fc4ce7
--- /dev/null
+++ b/src/lib-fts/stopwords/stopwords_nl.txt
@@ -0,0 +1,118 @@
+ | From svn.tartarus.org/snowball/trunk/website/algorithms/dutch/stop.txt
+ | This file is distributed under the BSD License.
+ | See http://snowball.tartarus.org/license.php
+ | Also see http://www.opensource.org/licenses/bsd-license.html
+ | - Encoding was converted to UTF-8.
+ | - This notice was added.
+ |
+
+ | A Dutch stop word list. Comments begin with vertical bar. Each stop
+ | word is at the start of a line.
+
+ | This is a ranked list (commonest to rarest) of stopwords derived from
+ | a large sample of Dutch text.
+
+ | Dutch stop words frequently exhibit homonym clashes. These are indicated
+ | clearly below.
+
+de | the
+en | and
+van | of, from
+ik | I, the ego
+te | (1) chez, at etc, (2) to, (3) too
+dat | that, which
+die | that, those, who, which
+in | in, inside
+een | a, an, one
+hij | he
+het | the, it
+niet | not, nothing, naught
+zijn | (1) to be, being, (2) his, one's, its
+is | is
+was | (1) was, past tense of all persons sing. of 'zijn' (to be) (2) wax, (3) the washing, (4) rise of river
+op | on, upon, at, in, up, used up
+aan | on, upon, to (as dative)
+met | with, by
+als | like, such as, when
+voor | (1) before, in front of, (2) furrow
+had | had, past tense all persons sing. of 'hebben' (have)
+er | there
+maar | but, only
+om | round, about, for etc
+hem | him
+dan | then
+zou | should/would, past tense all persons sing. of 'zullen'
+of | or, whether, if
+wat | what, something, anything
+mijn | possessive and noun 'mine'
+men | people, 'one'
+dit | this
+zo | so, thus, in this way
+door | through by
+over | over, across
+ze | she, her, they, them
+zich | oneself
+bij | (1) a bee, (2) by, near, at
+ook | also, too
+tot | till, until
+je | you
+mij | me
+uit | out of, from
+der | Old Dutch form of 'van der' still found in surnames
+daar | (1) there, (2) because
+haar | (1) her, their, them, (2) hair
+naar | (1) unpleasant, unwell etc, (2) towards, (3) as
+heb | present first person sing. of 'to have'
+hoe | how, why
+heeft | present third person sing. of 'to have'
+hebben | 'to have' and various parts thereof
+deze | this
+u | you
+want | (1) for, (2) mitten, (3) rigging
+nog | yet, still
+zal | 'shall', first and third person sing. of verb 'zullen' (will)
+me | me
+zij | she, they
+nu | now
+ge | 'thou', still used in Belgium and south Netherlands
+geen | none
+omdat | because
+iets | something, somewhat
+worden | to become, grow, get
+toch | yet, still
+al | all, every, each
+waren | (1) 'were' (2) to wander, (3) wares, (3)
+veel | much, many
+meer | (1) more, (2) lake
+doen | to do, to make
+toen | then, when
+moet | noun 'spot/mote' and present form of 'to must'
+ben | (1) am, (2) 'are' in interrogative second person singular of 'to be'
+zonder | without
+kan | noun 'can' and present form of 'to be able'
+hun | their, them
+dus | so, consequently
+alles | all, everything, anything
+onder | under, beneath
+ja | yes, of course
+eens | once, one day
+hier | here
+wie | who
+werd | imperfect third person sing. of 'become'
+altijd | always
+doch | yet, but etc
+wordt | present third person sing. of 'become'
+wezen | (1) to be, (2) 'been' as in 'been fishing', (3) orphans
+kunnen | to be able
+ons | us/our
+zelf | self
+tegen | against, towards, at
+na | after, near
+reeds | already
+wil | (1) present tense of 'want', (2) 'will', noun, (3) fender
+kon | could; past tense of 'to be able'
+niets | nothing
+uw | your
+iemand | somebody
+geweest | been; past participle of 'be'
+andere | other
diff --git a/src/lib-fts/stopwords/stopwords_no.txt b/src/lib-fts/stopwords/stopwords_no.txt
new file mode 100644
index 0000000..e76f36e
--- /dev/null
+++ b/src/lib-fts/stopwords/stopwords_no.txt
@@ -0,0 +1,192 @@
+ | From svn.tartarus.org/snowball/trunk/website/algorithms/norwegian/stop.txt
+ | This file is distributed under the BSD License.
+ | See http://snowball.tartarus.org/license.php
+ | Also see http://www.opensource.org/licenses/bsd-license.html
+ | - Encoding was converted to UTF-8.
+ | - This notice was added.
+
+ | A Norwegian stop word list. Comments begin with vertical bar. Each stop
+ | word is at the start of a line.
+
+ | This stop word list is for the dominant bokmål dialect. Words unique
+ | to nynorsk are marked *.
+
+ | Revised by Jan Bruusgaard <Jan.Bruusgaard@ssb.no>, Jan 2005
+
+og | and
+i | in
+jeg | I
+det | it/this/that
+at | to (w. inf.)
+en | a/an
+et | a/an
+den | it/this/that
+til | to
+er | is/am/are
+som | who/that
+på | on
+de | they / you(formal)
+med | with
+han | he
+av | of
+ikke | not
+ikkje | not *
+der | there
+så | so
+var | was/were
+meg | me
+seg | you
+men | but
+ett | one
+har | have
+om | about
+vi | we
+min | my
+mitt | my
+ha | have
+hadde | had
+hun | she
+nå | now
+over | over
+da | when/as
+ved | by/know
+fra | from
+du | you
+ut | out
+sin | your
+dem | them
+oss | us
+opp | up
+man | you/one
+kan | can
+hans | his
+hvor | where
+eller | or
+hva | what
+skal | shall/must
+selv | self (reflective)
+sjøl | self (reflective)
+her | here
+alle | all
+vil | will
+bli | become
+ble | became
+blei | became *
+blitt | have become
+kunne | could
+inn | in
+når | when
+være | be
+kom | come
+noen | some
+noe | some
+ville | would
+dere | you
+som | who/which/that
+deres | their/theirs
+kun | only/just
+ja | yes
+etter | after
+ned | down
+skulle | should
+denne | this
+for | for/because
+deg | you
+si | hers/his
+sine | hers/his
+sitt | hers/his
+mot | against
+å | to
+meget | much
+hvorfor | why
+dette | this
+disse | these/those
+uten | without
+hvordan | how
+ingen | none
+din | your
+ditt | your
+blir | become
+samme | same
+hvilken | which
+hvilke | which (plural)
+sånn | such a
+inni | inside/within
+mellom | between
+vår | our
+hver | each
+hvem | who
+vors | us/ours
+hvis | whose
+både | both
+bare | only/just
+enn | than
+fordi | as/because
+før | before
+mange | many
+også | also
+slik | just
+vært | been
+være | to be
+båe | both *
+begge | both
+siden | since
+dykk | your *
+dykkar | yours *
+dei | they *
+deira | them *
+deires | theirs *
+deim | them *
+di | your (fem.) *
+då | as/when *
+eg | I *
+ein | a/an *
+eit | a/an *
+eitt | a/an *
+elles | or *
+honom | he *
+hjå | at *
+ho | she *
+hoe | she *
+henne | her
+hennar | her/hers
+hennes | hers
+hoss | how *
+hossen | how *
+ikkje | not *
+ingi | noone *
+inkje | noone *
+korleis | how *
+korso | how *
+kva | what/which *
+kvar | where *
+kvarhelst | where *
+kven | who/whom *
+kvi | why *
+kvifor | why *
+me | we *
+medan | while *
+mi | my *
+mine | my *
+mykje | much *
+no | now *
+nokon | some (masc./neut.) *
+noka | some (fem.) *
+nokor | some *
+noko | some *
+nokre | some *
+si | his/hers *
+sia | since *
+sidan | since *
+so | so *
+somt | some *
+somme | some *
+um | about*
+upp | up *
+vere | be *
+vore | was *
+verte | become *
+vort | become *
+varte | became *
+vart | became *
+
diff --git a/src/lib-fts/stopwords/stopwords_pt.txt b/src/lib-fts/stopwords/stopwords_pt.txt
new file mode 100644
index 0000000..3744961
--- /dev/null
+++ b/src/lib-fts/stopwords/stopwords_pt.txt
@@ -0,0 +1,252 @@
+ | From svn.tartarus.org/snowball/trunk/website/algorithms/portuguese/stop.txt
+ | This file is distributed under the BSD License.
+ | See http://snowball.tartarus.org/license.php
+ | Also see http://www.opensource.org/licenses/bsd-license.html
+ | - Encoding was converted to UTF-8.
+ | - This notice was added.
+ |
+
+ | A Portuguese stop word list. Comments begin with vertical bar. Each stop
+ | word is at the start of a line.
+
+
+ | The following is a ranked list (commonest to rarest) of stopwords
+ | deriving from a large sample of text.
+
+ | Extra words have been added at the end.
+
+de | of, from
+a | the; to, at; her
+o | the; him
+que | who, that
+e | and
+do | de + o
+da | de + a
+em | in
+um | a
+para | for
+ | é from SER
+com | with
+não | not, no
+uma | a
+os | the; them
+no | em + o
+se | himself etc
+na | em + a
+por | for
+mais | more
+as | the; them
+dos | de + os
+como | as, like
+mas | but
+ | foi from SER
+ao | a + o
+ele | he
+das | de + as
+ | tem from TER
+à | a + a
+seu | his
+sua | her
+ou | or
+ | ser from SER
+quando | when
+muito | much
+ | há from HAV
+nos | em + os; us
+já | already, now
+ | está from EST
+eu | I
+também | also
+só | only, just
+pelo | per + o
+pela | per + a
+até | up to
+isso | that
+ela | he
+entre | between
+ | era from SER
+depois | after
+sem | without
+mesmo | same
+aos | a + os
+ | ter from TER
+seus | his
+quem | whom
+nas | em + as
+me | me
+esse | that
+eles | they
+ | estão from EST
+você | you
+ | tinha from TER
+ | foram from SER
+essa | that
+num | em + um
+nem | nor
+suas | her
+meu | my
+às | a + as
+minha | my
+ | têm from TER
+numa | em + uma
+pelos | per + os
+elas | they
+ | havia from HAV
+ | seja from SER
+qual | which
+ | será from SER
+nós | we
+ | tenho from TER
+lhe | to him, her
+deles | of them
+essas | those
+esses | those
+pelas | per + as
+este | this
+ | fosse from SER
+dele | of him
+
+ | other words. There are many contractions such as naquele = em+aquele,
+ | mo = me+o, but they are rare.
+ | Indefinite article plural forms are also rare.
+
+tu | thou
+te | thee
+vocês | you (plural)
+vos | you
+lhes | to them
+meus | my
+minhas
+teu | thy
+tua
+teus
+tuas
+nosso | our
+nossa
+nossos
+nossas
+
+dela | of her
+delas | of them
+
+esta | this
+estes | these
+estas | these
+aquele | that
+aquela | that
+aqueles | those
+aquelas | those
+isto | this
+aquilo | that
+
+ | forms of estar, to be (not including the infinitive):
+estou
+está
+estamos
+estão
+estive
+esteve
+estivemos
+estiveram
+estava
+estávamos
+estavam
+estivera
+estivéramos
+esteja
+estejamos
+estejam
+estivesse
+estivéssemos
+estivessem
+estiver
+estivermos
+estiverem
+
+ | forms of haver, to have (not including the infinitive):
+hei
+há
+havemos
+hão
+houve
+houvemos
+houveram
+houvera
+houvéramos
+haja
+hajamos
+hajam
+houvesse
+houvéssemos
+houvessem
+houver
+houvermos
+houverem
+houverei
+houverá
+houveremos
+houverão
+houveria
+houveríamos
+houveriam
+
+ | forms of ser, to be (not including the infinitive):
+sou
+somos
+são
+era
+éramos
+eram
+fui
+foi
+fomos
+foram
+fora
+fôramos
+seja
+sejamos
+sejam
+fosse
+fôssemos
+fossem
+for
+formos
+forem
+serei
+será
+seremos
+serão
+seria
+seríamos
+seriam
+
+ | forms of ter, to have (not including the infinitive):
+tenho
+tem
+temos
+tém
+tinha
+tínhamos
+tinham
+tive
+teve
+tivemos
+tiveram
+tivera
+tivéramos
+tenha
+tenhamos
+tenham
+tivesse
+tivéssemos
+tivessem
+tiver
+tivermos
+tiverem
+terei
+terá
+teremos
+terão
+teria
+teríamos
+teriam
diff --git a/src/lib-fts/stopwords/stopwords_ro.txt b/src/lib-fts/stopwords/stopwords_ro.txt
new file mode 100644
index 0000000..4fdee90
--- /dev/null
+++ b/src/lib-fts/stopwords/stopwords_ro.txt
@@ -0,0 +1,233 @@
+# This file was created by Jacques Savoy and is distributed under the BSD license.
+# See http://members.unine.ch/jacques.savoy/clef/index.html.
+# Also see http://www.opensource.org/licenses/bsd-license.html
+acea
+aceasta
+această
+aceea
+acei
+aceia
+acel
+acela
+acele
+acelea
+acest
+acesta
+aceste
+acestea
+aceşti
+aceştia
+acolo
+acum
+ai
+aia
+aibă
+aici
+al
+ăla
+ale
+alea
+ălea
+altceva
+altcineva
+am
+ar
+are
+aş
+aşadar
+asemenea
+asta
+ăsta
+astăzi
+astea
+ăstea
+ăştia
+asupra
+aţi
+au
+avea
+avem
+aveţi
+azi
+bine
+bucur
+bună
+ca
+că
+căci
+când
+care
+cărei
+căror
+cărui
+cât
+câte
+câţi
+către
+câtva
+ce
+cel
+ceva
+chiar
+cînd
+cine
+cineva
+cît
+cîte
+cîţi
+cîtva
+contra
+cu
+cum
+cumva
+curând
+curînd
+da
+dă
+dacă
+dar
+datorită
+de
+deci
+deja
+deoarece
+departe
+deşi
+din
+dinaintea
+dintr
+dintre
+drept
+după
+ea
+ei
+el
+ele
+eram
+este
+eşti
+eu
+face
+fără
+fi
+fie
+fiecare
+fii
+fim
+fiţi
+iar
+ieri
+îi
+îl
+îmi
+împotriva
+în
+înainte
+înaintea
+încât
+încît
+încotro
+între
+întrucât
+întrucît
+îţi
+la
+lângă
+le
+li
+lîngă
+lor
+lui
+mă
+mâine
+mea
+mei
+mele
+mereu
+meu
+mi
+mine
+mult
+multă
+mulţi
+ne
+nicăieri
+nici
+nimeni
+nişte
+noastră
+noastre
+noi
+noştri
+nostru
+nu
+ori
+oricând
+oricare
+oricât
+orice
+oricînd
+oricine
+oricît
+oricum
+oriunde
+până
+pe
+pentru
+peste
+pînă
+poate
+pot
+prea
+prima
+primul
+prin
+printr
+sa
+să
+săi
+sale
+sau
+său
+se
+şi
+sînt
+sîntem
+sînteţi
+spre
+sub
+sunt
+suntem
+sunteţi
+ta
+tăi
+tale
+tău
+te
+ţi
+ţie
+tine
+toată
+toate
+tot
+toţi
+totuşi
+tu
+un
+una
+unde
+undeva
+unei
+unele
+uneori
+unor
+vă
+vi
+voastră
+voastre
+voi
+voştri
+vostru
+vouă
+vreo
+vreun
diff --git a/src/lib-fts/stopwords/stopwords_ru.txt b/src/lib-fts/stopwords/stopwords_ru.txt
new file mode 100644
index 0000000..36b0d9f
--- /dev/null
+++ b/src/lib-fts/stopwords/stopwords_ru.txt
@@ -0,0 +1,242 @@
+ | From svn.tartarus.org/snowball/trunk/website/algorithms/russian/stop.txt
+ | This file is distributed under the BSD License.
+ | See http://snowball.tartarus.org/license.php
+ | Also see http://www.opensource.org/licenses/bsd-license.html
+ | - Encoding was converted to UTF-8.
+ | - This notice was added.
+ |
+
+ | a russian stop word list. comments begin with vertical bar. each stop
+ | word is at the start of a line.
+
+ | this is a ranked list (commonest to rarest) of stopwords derived from
+ | a large text sample.
+
+ | letter `ё' is translated to `е'.
+
+и | and
+в | in/into
+во | alternative form
+не | not
+что | what/that
+он | he
+на | on/onto
+я | i
+с | from
+со | alternative form
+как | how
+а | milder form of `no' (but)
+то | conjunction and form of `that'
+все | all
+она | she
+так | so, thus
+его | him
+но | but
+да | yes/and
+ты | thou
+к | towards, by
+у | around, chez
+же | intensifier particle
+вы | you
+за | beyond, behind
+бы | conditional/subj. particle
+по | up to, along
+только | only
+ее | her
+мне | to me
+было | it was
+вот | here is/are, particle
+от | away from
+меня | me
+еще | still, yet, more
+нет | no, there isnt/arent
+о | about
+из | out of
+ему | to him
+теперь | now
+когда | when
+даже | even
+ну | so, well
+вдруг | suddenly
+ли | interrogative particle
+если | if
+уже | already, but homonym of `narrower'
+или | or
+ни | neither
+быть | to be
+был | he was
+него | prepositional form of его
+до | up to
+вас | you accusative
+нибудь | indef. suffix preceded by hyphen
+опять | again
+уж | already, but homonym of `adder'
+вам | to you
+сказал | he said
+ведь | particle `after all'
+там | there
+потом | then
+себя | oneself
+ничего | nothing
+ей | to her
+может | usually with `быть' as `maybe'
+они | they
+тут | here
+где | where
+есть | there is/are
+надо | got to, must
+ней | prepositional form of ей
+для | for
+мы | we
+тебя | thee
+их | them, their
+чем | than
+была | she was
+сам | self
+чтоб | in order to
+без | without
+будто | as if
+человек | man, person, one
+чего | genitive form of `what'
+раз | once
+тоже | also
+себе | to oneself
+под | beneath
+жизнь | life
+будет | will be
+ж | short form of intensifer particle `же'
+тогда | then
+кто | who
+этот | this
+говорил | was saying
+того | genitive form of `that'
+потому | for that reason
+этого | genitive form of `this'
+какой | which
+совсем | altogether
+ним | prepositional form of `его', `они'
+здесь | here
+этом | prepositional form of `этот'
+один | one
+почти | almost
+мой | my
+тем | instrumental/dative plural of `тот', `то'
+чтобы | full form of `in order that'
+нее | her (acc.)
+кажется | it seems
+сейчас | now
+были | they were
+куда | where to
+зачем | why
+сказать | to say
+всех | all (acc., gen. preposn. plural)
+никогда | never
+сегодня | today
+можно | possible, one can
+при | by
+наконец | finally
+два | two
+об | alternative form of `о', about
+другой | another
+хоть | even
+после | after
+над | above
+больше | more
+тот | that one (masc.)
+через | across, in
+эти | these
+нас | us
+про | about
+всего | in all, only, of all
+них | prepositional form of `они' (they)
+какая | which, feminine
+много | lots
+разве | interrogative particle
+сказала | she said
+три | three
+эту | this, acc. fem. sing.
+моя | my, feminine
+впрочем | moreover, besides
+хорошо | good
+свою | ones own, acc. fem. sing.
+этой | oblique form of `эта', fem. `this'
+перед | in front of
+иногда | sometimes
+лучше | better
+чуть | a little
+том | preposn. form of `that one'
+нельзя | one must not
+такой | such a one
+им | to them
+более | more
+всегда | always
+конечно | of course
+всю | acc. fem. sing of `all'
+между | between
+
+
+ | b: some paradigms
+ |
+ | personal pronouns
+ |
+ | я меня мне мной [мною]
+ | ты тебя тебе тобой [тобою]
+ | он его ему им [него, нему, ним]
+ | она ее эи ею [нее, нэи, нею]
+ | оно его ему им [него, нему, ним]
+ |
+ | мы нас нам нами
+ | вы вас вам вами
+ | они их им ими [них, ним, ними]
+ |
+ | себя себе собой [собою]
+ |
+ | demonstrative pronouns: этот (this), тот (that)
+ |
+ | этот эта это эти
+ | этого эты это эти
+ | этого этой этого этих
+ | этому этой этому этим
+ | этим этой этим [этою] этими
+ | этом этой этом этих
+ |
+ | тот та то те
+ | того ту то те
+ | того той того тех
+ | тому той тому тем
+ | тем той тем [тою] теми
+ | том той том тех
+ |
+ | determinative pronouns
+ |
+ | (a) весь (all)
+ |
+ | весь вся все все
+ | всего всю все все
+ | всего всей всего всех
+ | всему всей всему всем
+ | всем всей всем [всею] всеми
+ | всем всей всем всех
+ |
+ | (b) сам (himself etc)
+ |
+ | сам сама само сами
+ | самого саму само самих
+ | самого самой самого самих
+ | самому самой самому самим
+ | самим самой самим [самою] самими
+ | самом самой самом самих
+ |
+ | stems of verbs `to be', `to have', `to do' and modal
+ |
+ | быть бы буд быв есть суть
+ | име
+ | дел
+ | мог мож мочь
+ | уме
+ | хоч хот
+ | долж
+ | можн
+ | нужн
+ | нельзя
+
diff --git a/src/lib-fts/stopwords/stopwords_sv.txt b/src/lib-fts/stopwords/stopwords_sv.txt
new file mode 100644
index 0000000..22bddfd
--- /dev/null
+++ b/src/lib-fts/stopwords/stopwords_sv.txt
@@ -0,0 +1,131 @@
+ | From svn.tartarus.org/snowball/trunk/website/algorithms/swedish/stop.txt
+ | This file is distributed under the BSD License.
+ | See http://snowball.tartarus.org/license.php
+ | Also see http://www.opensource.org/licenses/bsd-license.html
+ | - Encoding was converted to UTF-8.
+ | - This notice was added.
+
+ | A Swedish stop word list. Comments begin with vertical bar. Each stop
+ | word is at the start of a line.
+
+ | This is a ranked list (commonest to rarest) of stopwords derived from
+ | a large text sample.
+
+ | Swedish stop words occasionally exhibit homonym clashes. For example
+ | så = so, but also seed. These are indicated clearly below.
+
+och | and
+det | it, this/that
+att | to (with infinitive)
+i | in, at
+en | a
+jag | I
+hon | she
+som | who, that
+han | he
+på | on
+den | it, this/that
+med | with
+var | where, each
+sig | him(self) etc
+för | for
+så | so (also: seed)
+till | to
+är | is
+men | but
+ett | a
+om | if; around, about
+hade | had
+de | they, these/those
+av | of
+icke | not, no
+mig | me
+du | you
+henne | her
+då | then, when
+sin | his
+nu | now
+har | have
+inte | inte någon = no one
+hans | his
+honom | him
+skulle | 'sake'
+hennes | her
+där | there
+min | my
+man | one (pronoun)
+ej | nor
+vid | at, by, on (also: vast)
+kunde | could
+något | some etc
+från | from, off
+ut | out
+när | when
+efter | after, behind
+upp | up
+vi | we
+dem | them
+vara | be
+vad | what
+över | over
+än | than
+dig | you
+kan | can
+sina | his
+här | here
+ha | have
+mot | towards
+alla | all
+under | under (also: wonder)
+någon | some etc
+eller | or (else)
+allt | all
+mycket | much
+sedan | since
+ju | why
+denna | this/that
+själv | myself, yourself etc
+detta | this/that
+åt | to
+utan | without
+varit | was
+hur | how
+ingen | no
+mitt | my
+ni | you
+bli | to be, become
+blev | from bli
+oss | us
+din | thy
+dessa | these/those
+några | some etc
+deras | their
+blir | from bli
+mina | my
+samma | (the) same
+vilken | who, that
+er | you, your
+sådan | such a
+vår | our
+blivit | from bli
+dess | its
+inom | within
+mellan | between
+sådant | such a
+varför | why
+varje | each
+vilka | who, that
+ditt | thy
+vem | who
+vilket | who, that
+sitta | his
+sådana | such a
+vart | each
+dina | thy
+vars | whose
+vårt | our
+våra | our
+ert | your
+era | your
+vilkas | whose
+
diff --git a/src/lib-fts/stopwords/stopwords_tr.txt b/src/lib-fts/stopwords/stopwords_tr.txt
new file mode 100644
index 0000000..ac18f34
--- /dev/null
+++ b/src/lib-fts/stopwords/stopwords_tr.txt
@@ -0,0 +1,529 @@
+##
+# Copied from https://github.com/stopwords-iso/stopwords-tr/
+#
+# The MIT License (MIT)
+#
+# Copyright (c) 2016 Gene Diaz
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in all
+# copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+# SOFTWARE.
+##
+acaba
+acep
+adamakıllı
+adeta
+ait
+altmýþ
+altmış
+altý
+altı
+ama
+amma
+anca
+ancak
+arada
+artýk
+aslında
+aynen
+ayrıca
+az
+açıkça
+açıkçası
+bana
+bari
+bazen
+bazý
+bazı
+başkası
+baţka
+belki
+ben
+benden
+beni
+benim
+beri
+beriki
+beþ
+beş
+beţ
+bilcümle
+bile
+bin
+binaen
+binaenaleyh
+bir
+biraz
+birazdan
+birbiri
+birden
+birdenbire
+biri
+birice
+birileri
+birisi
+birkaç
+birkaçı
+birkez
+birlikte
+birçok
+birçoğu
+birþey
+birþeyi
+birşey
+birşeyi
+birţey
+bitevi
+biteviye
+bittabi
+biz
+bizatihi
+bizce
+bizcileyin
+bizden
+bize
+bizi
+bizim
+bizimki
+bizzat
+boşuna
+bu
+buna
+bunda
+bundan
+bunlar
+bunları
+bunların
+bunu
+bunun
+buracıkta
+burada
+buradan
+burası
+böyle
+böylece
+böylecene
+böylelikle
+böylemesine
+böylesine
+büsbütün
+bütün
+cuk
+cümlesi
+da
+daha
+dahi
+dahil
+dahilen
+daima
+dair
+dayanarak
+de
+defa
+dek
+demin
+demincek
+deminden
+denli
+derakap
+derhal
+derken
+deđil
+değil
+değin
+diye
+diđer
+diğer
+diğeri
+doksan
+dokuz
+dolayı
+dolayısıyla
+doğru
+dört
+edecek
+eden
+ederek
+edilecek
+ediliyor
+edilmesi
+ediyor
+elbet
+elbette
+elli
+emme
+en
+enikonu
+epey
+epeyce
+epeyi
+esasen
+esnasında
+etmesi
+etraflı
+etraflıca
+etti
+ettiği
+ettiğini
+evleviyetle
+evvel
+evvela
+evvelce
+evvelden
+evvelemirde
+evveli
+eđer
+eğer
+fakat
+filanca
+gah
+gayet
+gayetle
+gayri
+gayrı
+gelgelelim
+gene
+gerek
+gerçi
+geçende
+geçenlerde
+gibi
+gibilerden
+gibisinden
+gine
+göre
+gırla
+hakeza
+halbuki
+halen
+halihazırda
+haliyle
+handiyse
+hangi
+hangisi
+hani
+hariç
+hasebiyle
+hasılı
+hatta
+hele
+hem
+henüz
+hep
+hepsi
+her
+herhangi
+herkes
+herkesin
+hiç
+hiçbir
+hiçbiri
+hoş
+hulasaten
+iken
+iki
+ila
+ile
+ilen
+ilgili
+ilk
+illa
+illaki
+imdi
+indinde
+inen
+insermi
+ise
+ister
+itibaren
+itibariyle
+itibarıyla
+iyi
+iyice
+iyicene
+için
+iş
+işte
+iţte
+kadar
+kaffesi
+kah
+kala
+kanýmca
+karşın
+katrilyon
+kaynak
+kaçı
+kelli
+kendi
+kendilerine
+kendini
+kendisi
+kendisine
+kendisini
+kere
+kez
+keza
+kezalik
+keşke
+keţke
+ki
+kim
+kimden
+kime
+kimi
+kimisi
+kimse
+kimsecik
+kimsecikler
+külliyen
+kýrk
+kýsaca
+kırk
+kısaca
+lakin
+leh
+lütfen
+maada
+madem
+mademki
+mamafih
+mebni
+međer
+meğer
+meğerki
+meğerse
+milyar
+milyon
+mu
+mü
+mý
+mı
+nasýl
+nasıl
+nasılsa
+nazaran
+naşi
+ne
+neden
+nedeniyle
+nedenle
+nedense
+nerde
+nerden
+nerdeyse
+nere
+nerede
+nereden
+neredeyse
+neresi
+nereye
+netekim
+neye
+neyi
+neyse
+nice
+nihayet
+nihayetinde
+nitekim
+niye
+niçin
+o
+olan
+olarak
+oldu
+olduklarını
+oldukça
+olduğu
+olduğunu
+olmadı
+olmadığı
+olmak
+olması
+olmayan
+olmaz
+olsa
+olsun
+olup
+olur
+olursa
+oluyor
+on
+ona
+onca
+onculayın
+onda
+ondan
+onlar
+onlardan
+onlari
+onlarýn
+onları
+onların
+onu
+onun
+oracık
+oracıkta
+orada
+oradan
+oranca
+oranla
+oraya
+otuz
+oysa
+oysaki
+pek
+pekala
+peki
+pekçe
+peyderpey
+rağmen
+sadece
+sahi
+sahiden
+sana
+sanki
+sekiz
+seksen
+sen
+senden
+seni
+senin
+siz
+sizden
+sizi
+sizin
+sonra
+sonradan
+sonraları
+sonunda
+tabii
+tam
+tamam
+tamamen
+tamamıyla
+tarafından
+tek
+trilyon
+tüm
+var
+vardı
+vasıtasıyla
+ve
+velev
+velhasıl
+velhasılıkelam
+veya
+veyahut
+ya
+yahut
+yakinen
+yakında
+yakından
+yakınlarda
+yalnız
+yalnızca
+yani
+yapacak
+yapmak
+yaptı
+yaptıkları
+yaptığı
+yaptığını
+yapılan
+yapılması
+yapıyor
+yedi
+yeniden
+yenilerde
+yerine
+yetmiþ
+yetmiş
+yetmiţ
+yine
+yirmi
+yok
+yoksa
+yoluyla
+yüz
+yüzünden
+zarfında
+zaten
+zati
+zira
+çabuk
+çabukça
+çeşitli
+çok
+çokları
+çoklarınca
+çokluk
+çoklukla
+çokça
+çoğu
+çoğun
+çoğunca
+çoğunlukla
+çünkü
+öbür
+öbürkü
+öbürü
+önce
+önceden
+önceleri
+öncelikle
+öteki
+ötekisi
+öyle
+öylece
+öylelikle
+öylemesine
+öz
+üzere
+üç
+þey
+þeyden
+þeyi
+þeyler
+þu
+þuna
+þunda
+þundan
+þunu
+şayet
+şey
+şeyden
+şeyi
+şeyler
+şu
+şuna
+şuncacık
+şunda
+şundan
+şunlar
+şunları
+şunu
+şunun
+şura
+şuracık
+şuracıkta
+şurası
+şöyle
+ţayet
+ţimdi
+ţu
+ţöyle
diff --git a/src/lib-fts/test-fts-filter.c b/src/lib-fts/test-fts-filter.c
new file mode 100644
index 0000000..f93cc74
--- /dev/null
+++ b/src/lib-fts/test-fts-filter.c
@@ -0,0 +1,1025 @@
+/* Copyright (c) 2014-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "sha2.h"
+#include "str.h"
+#include "unichar.h"
+#include "test-common.h"
+#include "fts-language.h"
+#include "fts-filter.h"
+
+#include <stdio.h>
+
+static const char *const stopword_settings[] = {"stopwords_dir", TEST_STOPWORDS_DIR, NULL};
+static struct fts_language english_language = { .name = "en" };
+static struct fts_language french_language = { .name = "fr" };
+static struct fts_language norwegian_language = { .name = "no" };
+#if defined(HAVE_LIBICU) && defined(HAVE_FTS_STEMMER)
+static struct fts_language swedish_language = { .name = "sv" };
+#endif
+
+static void test_fts_filter_find(void)
+{
+ test_begin("fts filter find");
+ test_assert(fts_filter_find("stopwords") == fts_filter_stopwords);
+ test_assert(fts_filter_find("snowball") == fts_filter_stemmer_snowball);
+ test_assert(fts_filter_find("normalizer-icu") == fts_filter_normalizer_icu);
+ test_assert(fts_filter_find("lowercase") == fts_filter_lowercase);
+ test_assert(fts_filter_find("contractions") == fts_filter_contractions);
+ test_end();
+}
+
+
+static void test_fts_filter_contractions_fail(void)
+{
+
+ struct fts_filter *filter;
+ const char *error;
+
+ test_begin("fts filter contractions, unsupported language");
+ test_assert(fts_filter_create(fts_filter_contractions, NULL, &english_language, NULL, &filter, &error) != 0);
+ test_assert(error != NULL);
+ test_end();
+}
+
+static void test_fts_filter_contractions_fr(void)
+{
+ static const struct {
+ const char *input;
+ const char *output;
+ } tests[] = {
+ { "foo", "foo" },
+ { "you're", "you're" },
+ { "l'homme", "homme" },
+ { "l\xE2\x80\x99homme", "homme" },
+ { "aujourd'hui", "aujourd'hui" },
+ { "qu\xE2\x80\x99il", "il" },
+ { "qu'il", "il" },
+ { "du'il", "du'il" },
+ { "que", "que" },
+ { "'foobar'", "'foobar'" },
+ { "foo'bar", "foo'bar" },
+ { "a'foo", "a'foo" },
+ { "cu'", "cu'" },
+ { "qu", "qu" },
+ { "d", "d" },
+ { "qu'", NULL },
+ { "j'adore", "adore" },
+ { "quelqu'un", "quelqu'un" },
+ { "l'esprit", "esprit" }
+ };
+ struct fts_filter *filter;
+ const char *error;
+ const char *token;
+ unsigned int i;
+ int ret;
+
+ test_begin("fts filter contractions, French");
+ test_assert(fts_filter_create(fts_filter_contractions, NULL, &french_language, NULL, &filter, &error) == 0);
+
+ for (i = 0; i < N_ELEMENTS(tests); i++) {
+ token = tests[i].input;
+ ret = fts_filter_filter(filter, &token, &error);
+ test_assert(ret >= 0);
+ if (ret > 0)
+ test_assert_idx(strcmp(token, tests[i].output) == 0, i);
+ else if (ret == 0)
+ test_assert_idx(token == NULL && tests[i].output == NULL, i);
+ }
+ fts_filter_unref(&filter);
+ test_end();
+}
+
+static void test_fts_filter_lowercase(void)
+{
+ static const struct {
+ const char *input;
+ const char *output;
+ } tests[] = {
+ { "foo", "foo" },
+ { "FOO", "foo" },
+ { "fOo", "foo" }
+ };
+ struct fts_filter *filter;
+ const char *error;
+ const char *token;
+ unsigned int i;
+
+ test_begin("fts filter lowercase");
+ test_assert(fts_filter_create(fts_filter_lowercase, NULL, &english_language, NULL, &filter, &error) == 0);
+
+ for (i = 0; i < N_ELEMENTS(tests); i++) {
+ token = tests[i].input;
+ test_assert_idx(fts_filter_filter(filter, &token, &error) > 0 &&
+ strcmp(token, tests[i].output) == 0, 0);
+ }
+ fts_filter_unref(&filter);
+ test_end();
+}
+
+#ifdef HAVE_LIBICU
+static void test_fts_filter_lowercase_utf8(void)
+{
+ static const struct {
+ const char *input;
+ const char *output;
+ } tests[] = {
+ { "f\xC3\x85\xC3\x85", "f\xC3\xA5\xC3\xA5" },
+ { "F\xC3\x85\xC3\x85", "f\xC3\xA5\xC3\xA5" },
+ { "F\xC3\x85\xC3\xA5", "f\xC3\xA5\xC3\xA5" }
+ };
+ struct fts_filter *filter;
+ const char *error;
+ const char *token;
+ unsigned int i;
+
+ test_begin("fts filter lowercase, UTF8");
+ test_assert(fts_filter_create(fts_filter_lowercase, NULL, &english_language, NULL, &filter, &error) == 0);
+
+ for (i = 0; i < N_ELEMENTS(tests); i++) {
+ token = tests[i].input;
+ test_assert_idx(fts_filter_filter(filter, &token, &error) > 0 &&
+ strcmp(token, tests[i].output) == 0, 0);
+ }
+ fts_filter_unref(&filter);
+ test_end();
+}
+
+static void test_fts_filter_lowercase_too_long_utf8(void)
+{
+ static const struct {
+ const char *input;
+ const char *output;
+ } tests[] = {
+ { "f\xC3\x85\xC3\x85", "f\xC3\xA5\xC3\xA5" },
+ { "abcdefghijklmnopqrstuvwxyz", "abcdefghijklmnopqrstuvwxy" },
+ { "abc\xC3\x85""defghijklmnopqrstuvwxyz", "abc\xC3\xA5""defghijklmnopqrstuvw" },
+ { "abcdefghijklmnopqrstuvwx\xC3\x85", "abcdefghijklmnopqrstuvwx" }
+ };
+ struct fts_filter *filter;
+ const char *error;
+ const char *token;
+ const char * const settings[] = {"maxlen", "25", NULL};
+ unsigned int i;
+
+ test_begin("fts filter lowercase, too long UTF8");
+ test_assert(fts_filter_create(fts_filter_lowercase, NULL, &english_language, settings, &filter, &error) == 0);
+
+ for (i = 0; i < N_ELEMENTS(tests); i++) {
+ token = tests[i].input;
+ test_assert_idx(fts_filter_filter(filter, &token, &error) > 0 &&
+ strcmp(token, tests[i].output) == 0, 0);
+ }
+ fts_filter_unref(&filter);
+ test_end();
+}
+#endif
+
+static void test_fts_filter_stopwords_eng(void)
+{
+ struct fts_filter *filter;
+ const char *error;
+ int ret;
+ const char *input[] = {"an", "elephant", "and", "a", "bear",
+ "drive", "by", "for", "no", "reason",
+ "they", "will", "not", "sing", NULL};
+ const char *output[] = {NULL, "elephant", NULL, NULL, "bear",
+ "drive", NULL, NULL, NULL, "reason",
+ NULL, NULL, NULL, "sing"};
+ const char **ip, **op;
+ const char *token;
+
+ test_begin("fts filter stopwords, English");
+ test_assert(fts_filter_create(fts_filter_stopwords, NULL, &english_language, stopword_settings, &filter, &error) == 0);
+
+ ip = input;
+ op = output;
+ while (*ip != NULL) {
+ token = *ip;
+ ret = fts_filter_filter(filter, &token, &error);
+ if (ret <= 0) {
+ test_assert(ret == 0);
+ test_assert(*op == NULL);
+ } else {
+ test_assert(*op != NULL);
+ test_assert(strcmp(*ip, token) == 0);
+ }
+ op++;
+ ip++;
+ }
+
+ fts_filter_unref(&filter);
+ test_assert(filter == NULL);
+ test_end();
+}
+
+static void test_fts_filter_stopwords_fin(void)
+{
+ const struct fts_language finnish = { .name = "fi" };
+ struct fts_filter *filter;
+ const char *error;
+ int ret;
+ const char *input[] = {"olla", "vaiko", "eik\xC3\xB6", "olla",
+ "kenest\xC3\xA4", "ja", "joista", "jonka",
+ "testi", NULL};
+ const char *output[] = {NULL, "vaiko", "eik\xC3\xB6", NULL, NULL,
+ NULL, NULL, NULL, "testi"};
+ const char *input2[] =
+ {"kuka", "kenet", "keneen", "testi", "eiv\xC3\xA4t", NULL};
+ const char *output2[] = {NULL, NULL, NULL, "testi", NULL};
+ const char **ip, **op;
+ const char *token;
+
+ test_begin("fts filter stopwords, Finnish");
+ test_assert(fts_filter_create(fts_filter_stopwords, NULL, &finnish, stopword_settings, &filter, &error) == 0);
+
+ ip = input;
+ op = output;
+ while (*ip != NULL) {
+ token = *ip;
+ ret = fts_filter_filter(filter, &token, &error);
+ if (ret <= 0) {
+ test_assert(ret == 0);
+ test_assert(*op == NULL);
+ } else {
+ test_assert(*op != NULL);
+ test_assert(strcmp(*ip, token) == 0);
+ }
+ op++;
+ ip++;
+ }
+
+ fts_filter_unref(&filter);
+ test_assert(filter == NULL);
+
+ test_assert(fts_filter_create(fts_filter_stopwords, NULL, &finnish, stopword_settings, &filter, &error) == 0);
+ ip = input2;
+ op = output2;
+ while (*ip != NULL) {
+ token = *ip;
+ ret = fts_filter_filter(filter, &token, &error);
+ if (ret <= 0) {
+ test_assert(ret == 0);
+ test_assert(*op == NULL);
+ } else {
+ test_assert(*op != NULL);
+ test_assert(strcmp(*ip, token) == 0);
+ }
+ op++;
+ ip++;
+ }
+
+ fts_filter_unref(&filter);
+ test_assert(filter == NULL);
+ test_end();
+}
+
+static void test_fts_filter_stopwords_fra(void)
+{
+ struct fts_filter *filter;
+ const char *error;
+ int ret;
+
+ const char *input[] = {"e\xC3\xBBt", "soyez", "soi", "peut", "que",
+ "quelconque", "\xC3\xA9t\xC3\xA9",
+ "l\xE2\x80\x99""av\xC3\xA8nement",
+ NULL};
+ const char *output[] = {NULL, NULL, NULL, "peut", NULL,
+ "quelconque", NULL,
+ "l\xE2\x80\x99""av\xC3\xA8nement",};
+ const char **ip, **op;
+ const char *token;
+
+ test_begin("fts filter stopwords, French");
+ test_assert(fts_filter_create(fts_filter_stopwords, NULL, &french_language, stopword_settings, &filter, &error) == 0);
+
+ ip = input;
+ op = output;
+ while (*ip != NULL) {
+ token = *ip;
+ ret = fts_filter_filter(filter, &token, &error);
+ if (ret <= 0) {
+ test_assert(ret == 0);
+ test_assert(*op == NULL);
+ } else {
+ test_assert(*op != NULL);
+ test_assert(strcmp(*ip, token) == 0);
+ }
+ op++;
+ ip++;
+ }
+
+ fts_filter_unref(&filter);
+ test_assert(filter == NULL);
+ test_end();
+}
+
+static void test_fts_filter_stopwords_no(void)
+{
+ struct fts_filter *filter;
+ const char *error;
+ int ret;
+
+ const char *input[] = {"og", "d\xC3\xA5", "medlemsstatane", "har",
+ "bunde", "seg", "til", "\xC3\xA5", "fremje",
+ "allmenn", "v\xC3\xB8rdnad", "for", "pakta",
+ "og", "halde", "seg", "etter", "menneskerettane",
+ "og", "den", "grunnleggjande", "fridomen", "i",
+ "samarbeid", "med", "Dei", "Sameinte",
+ "Nasjonane", NULL};
+
+ const char *output[] = {NULL, NULL, "medlemsstatane", NULL,
+ "bunde", NULL, NULL, NULL, "fremje",
+ "allmenn", "v\xC3\xB8rdnad", NULL, "pakta",
+ NULL, "halde", NULL, NULL, "menneskerettane",
+ NULL, NULL, "grunnleggjande", "fridomen", NULL,
+ "samarbeid", NULL, "Dei", "Sameinte",
+ "Nasjonane"};
+ const char **ip, **op;
+ const char *token;
+
+ test_begin("fts filter stopwords, Norwegian");
+ test_assert(fts_filter_create(fts_filter_stopwords, NULL, &norwegian_language, stopword_settings, &filter, &error) == 0);
+
+ ip = input;
+ op = output;
+ while (*ip != NULL) {
+ token = *ip;
+ ret = fts_filter_filter(filter, &token, &error);
+ if (ret <= 0) {
+ test_assert(ret == 0);
+ test_assert(*op == NULL);
+ } else {
+ test_assert(*op != NULL);
+ test_assert(strcmp(*ip, token) == 0);
+ }
+ op++;
+ ip++;
+ }
+
+ fts_filter_unref(&filter);
+ test_assert(filter == NULL);
+ test_end();
+}
+
+static void test_fts_filter_stopwords_fail_lazy_init(void)
+{
+ const struct fts_language unknown = { .name = "bebobidoop" };
+ struct fts_filter *filter = NULL;
+ const char *error = NULL, *token = "foobar";
+
+ test_begin("fts filter stopwords, fail filter() (lazy init)");
+ test_assert(fts_filter_create(fts_filter_stopwords, NULL, &unknown, stopword_settings, &filter, &error) == 0);
+ test_assert(filter != NULL && error == NULL);
+ test_assert(fts_filter_filter(filter, &token, &error) < 0 && error != NULL);
+ fts_filter_unref(&filter);
+ test_end();
+
+}
+
+static void test_fts_filter_stopwords_malformed(void)
+{
+ const struct fts_language malformed = { .name = "malformed" };
+ struct fts_filter *filter = NULL;
+ const char *error = NULL, *token = "foobar";
+
+ test_begin("fts filter stopwords, malformed list");
+ test_assert(fts_filter_create(fts_filter_stopwords, NULL, &malformed, stopword_settings, &filter, &error) == 0);
+ test_expect_error_string("seems empty. Is the file correctly formatted?");
+ test_assert(fts_filter_filter(filter, &token, &error) > 0);
+ test_expect_no_more_errors();
+ fts_filter_unref(&filter);
+ test_end();
+
+}
+
+#ifdef HAVE_FTS_STEMMER
+static void test_fts_filter_stemmer_snowball_stem_english(void)
+{
+ struct fts_filter *stemmer;
+ const char *error;
+ const char *token = NULL;
+ const char * const tokens[] = {
+ "dries" ,"friendlies", "All", "human", "beings", "are",
+ "born", "free", "and", "equal", "in", "dignity", "and",
+ "rights", "They", "are", "endowed", "with", "reason", "and",
+ "conscience", "and", "should", "act", "towards", "one",
+ "another", "in", "a", "spirit", "of", "brotherhood", NULL};
+ const char * const bases[] = {
+ "dri" ,"friend", "All", "human", "be", "are", "born", "free",
+ "and", "equal", "in", "digniti", "and", "right", "They", "are",
+ "endow", "with", "reason", "and", "conscienc", "and", "should",
+ "act", "toward", "one", "anoth", "in", "a", "spirit", "of",
+ "brotherhood", NULL};
+ const char * const *tpp;
+ const char * const *bpp;
+
+ test_begin("fts filter stem English");
+ test_assert(fts_filter_create(fts_filter_stemmer_snowball, NULL, &english_language, NULL, &stemmer, &error) == 0);
+ bpp = bases;
+ for (tpp=tokens; *tpp != NULL; tpp++) {
+ token = *tpp;
+ test_assert(fts_filter_filter(stemmer, &token, &error) > 0);
+ test_assert(token != NULL);
+ test_assert(null_strcmp(token, *bpp) == 0);
+ bpp++;
+ }
+ fts_filter_unref(&stemmer);
+ test_assert(stemmer == NULL);
+ test_end();
+}
+
+static void test_fts_filter_stemmer_snowball_stem_french(void)
+{
+ struct fts_filter *stemmer;
+ const char *error;
+ const char *token = NULL;
+ const char * const tokens[] = {
+ "Tous", "les", "\xC3\xAAtres", "humains", "naissent",
+ "libres", "et", "\xC3\xA9gaux", "en", "dignit\xC3\xA9",
+ "et", "en", "droits", NULL};
+ const char * const bases[] = {
+ "Tous" ,"le", "\xC3\xAAtre", "humain", "naissent", "libr", "et",
+ "\xC3\xA9gal", "en", "dignit", "et", "en", "droit", NULL};
+ const char * const *tpp;
+ const char * const *bpp;
+
+ test_begin("fts filter stem French");
+ test_assert(fts_filter_create(fts_filter_stemmer_snowball, NULL, &french_language, NULL, &stemmer, &error) == 0);
+ bpp = bases;
+ for (tpp=tokens; *tpp != NULL; tpp++) {
+ token = *tpp;
+ test_assert(fts_filter_filter(stemmer, &token, &error) > 0);
+ test_assert(token != NULL);
+ test_assert(null_strcmp(token, *bpp) == 0);
+ bpp++;
+ }
+ fts_filter_unref(&stemmer);
+ test_assert(stemmer == NULL);
+ test_end();
+}
+
+static void test_fts_filter_stopwords_stemmer_eng(void)
+{
+ int ret;
+ struct fts_filter *stemmer;
+ struct fts_filter *filter;
+ const char *error;
+ const char *token = NULL;
+ const char * const tokens[] = {
+ "dries" ,"friendlies", "All", "human", "beings", "are",
+ "born", "free", "and", "equal", "in", "dignity", "and",
+ "rights", "They", "are", "endowed", "with", "reason", "and",
+ "conscience", "and", "should", "act", "towards", "one",
+ "another", "in", "a", "spirit", "of", "brotherhood", NULL};
+ const char * const bases[] = {
+ "dri" ,"friend", "All", "human", "be", NULL, "born", "free",
+ NULL, "equal", NULL, "digniti", NULL, "right", "They", NULL,
+ "endow", NULL, "reason", NULL, "conscienc", NULL, "should",
+ "act", "toward", "one", "anoth", NULL, NULL, "spirit", NULL,
+ "brotherhood", NULL};
+ const char * const *tpp;
+ const char * const *bpp;
+
+ test_begin("fts filters stopwords and stemming chained, English");
+
+ test_assert(fts_filter_create(fts_filter_stopwords, NULL, &english_language, stopword_settings, &filter, &error) == 0);
+ test_assert(fts_filter_create(fts_filter_stemmer_snowball, filter, &english_language, NULL, &stemmer, &error) == 0);
+
+ bpp = bases;
+ for (tpp=tokens; *tpp != NULL; tpp++) {
+ token = *tpp;
+ ret = fts_filter_filter(stemmer, &token, &error);
+ test_assert(ret >= 0);
+ if (ret == 0)
+ test_assert(*bpp == NULL);
+ else {
+ test_assert(*bpp != NULL);
+ test_assert(null_strcmp(*bpp, token) == 0);
+ }
+ bpp++;
+ }
+ fts_filter_unref(&stemmer);
+ fts_filter_unref(&filter);
+ test_assert(stemmer == NULL);
+ test_assert(filter == NULL);
+ test_end();
+}
+#endif
+
+#ifdef HAVE_LIBICU
+static void test_fts_filter_normalizer_swedish_short(void)
+{
+ struct fts_filter *norm = NULL;
+ const char *input[] = {
+ "Vem",
+ "\xC3\x85",
+ "\xC3\x85\xC3\x84\xC3\x96",
+ ("Vem kan segla f\xC3\xB6rutan vind?\n"
+ "\xC3\x85\xC3\x84\xC3\x96\xC3\xB6\xC3\xA4\xC3\xA5")
+ };
+ const char *expected_output[] = {
+ "vem",
+ "a",
+ "aao",
+ "vem kan segla forutan vind?\naaooaa"
+ };
+ const char * const settings[] =
+ {"id", "Any-Lower; NFKD; [: Nonspacing Mark :] Remove; NFC", NULL};
+ const char *error = NULL;
+ const char *token = NULL;
+ unsigned int i;
+
+ test_begin("fts filter normalizer Swedish short text");
+
+ test_assert(fts_filter_create(fts_filter_normalizer_icu, NULL, NULL, settings, &norm, &error) == 0);
+ for (i = 0; i < N_ELEMENTS(input); i++) {
+ token = input[i];
+ test_assert_idx(fts_filter_filter(norm, &token, &error) == 1, i);
+ test_assert_idx(null_strcmp(token, expected_output[i]) == 0, i);
+ }
+ fts_filter_unref(&norm);
+ test_assert(norm == NULL);
+ test_end();
+}
+
+static void test_fts_filter_normalizer_swedish_short_default_id(void)
+{
+ struct fts_filter *norm = NULL;
+ const char *input[] = {
+ "Vem",
+ "\xC3\x85",
+ "\xC3\x85\xC3\x84\xC3\x96",
+ ("Vem kan segla f\xC3\xB6rutan vind?\n"
+ "\xC3\x85\xC3\x84\xC3\x96\xC3\xB6\xC3\xA4\xC3\xA5")
+ };
+ const char *expected_output[] = {
+ "vem",
+ "a",
+ "aao",
+ "vemkanseglaforutanvind?\naaooaa"
+ };
+ const char *error = NULL;
+ const char *token = NULL;
+ unsigned int i;
+
+ test_begin("fts filter normalizer Swedish short text using default ID");
+
+ test_assert(fts_filter_create(fts_filter_normalizer_icu, NULL, NULL, NULL, &norm, &error) == 0);
+ for (i = 0; i < N_ELEMENTS(input); i++) {
+ token = input[i];
+ test_assert_idx(fts_filter_filter(norm, &token, &error) == 1, i);
+ test_assert_idx(null_strcmp(token, expected_output[i]) == 0, i);
+ }
+ fts_filter_unref(&norm);
+ test_assert(norm == NULL);
+ test_end();
+}
+
+/* UDHRDIR comes from Automake AM_CPPFLAGS */
+#define UDHR_FRA_NAME "/udhr_fra.txt"
+static void test_fts_filter_normalizer_french(void)
+{
+ struct fts_filter *norm = NULL;
+ FILE *input;
+ const char * const settings[] =
+ {"id", "Any-Lower; NFKD; [: Nonspacing Mark :] Remove", NULL};
+ char buf[250] = {0};
+ const char *error = NULL;
+ const char *tokens;
+ unsigned char sha512_digest[SHA512_RESULTLEN];
+ struct sha512_ctx ctx;
+ const unsigned char correct_digest[] = {
+ 0x06, 0x80, 0xf1, 0x81, 0xf2, 0xed, 0xfb, 0x6d,
+ 0xcd, 0x7d, 0xcb, 0xbd, 0xc4, 0x87, 0xc3, 0xf6,
+ 0xb8, 0x6a, 0x01, 0x82, 0xdf, 0x0a, 0xb5, 0x92,
+ 0x6b, 0x9b, 0x7b, 0x21, 0x5e, 0x62, 0x40, 0xbd,
+ 0xbf, 0x15, 0xb9, 0x7b, 0x75, 0x9c, 0x4e, 0xc9,
+ 0xe8, 0x48, 0xaa, 0x08, 0x63, 0xf2, 0xa0, 0x6c,
+ 0x20, 0x4c, 0x01, 0xe3, 0xb3, 0x4f, 0x15, 0xc6,
+ 0x8c, 0xd6, 0x7a, 0xb7, 0xc5, 0xc6, 0x85, 0x00};
+ const char *udhr_path;
+
+ test_begin("fts filter normalizer French UDHR");
+
+ udhr_path = t_strconcat(UDHRDIR, UDHR_FRA_NAME, NULL);
+ test_assert(fts_filter_create(fts_filter_normalizer_icu, NULL, NULL, settings, &norm, &error) == 0);
+ input = fopen(udhr_path, "r");
+ test_assert(input != NULL);
+ sha512_init(&ctx);
+ while (NULL != fgets(buf, sizeof(buf), input)) {
+ tokens = buf;
+ if (fts_filter_filter(norm, &tokens, &error) != 1){
+ break;
+ }
+ sha512_loop(&ctx, tokens, strlen(tokens));
+ }
+ fclose(input);
+ sha512_result(&ctx, sha512_digest);
+ test_assert(memcmp(sha512_digest, correct_digest,
+ sizeof(sha512_digest)) == 0);
+ fts_filter_unref(&norm);
+ test_assert(norm == NULL);
+ test_end();
+}
+
+static void test_fts_filter_normalizer_empty(void)
+{
+ /* test just a couple of these */
+ static const char *empty_tokens[] = {
+ "\xC2\xAF", /* U+00AF */
+ "\xCC\x80", /* U+0300 */
+ "\xF3\xA0\x87\xAF", /* U+E01EF */
+ "\xCC\x80\xF3\xA0\x87\xAF" /* U+0300 U+E01EF */
+ };
+ const char * const settings[] =
+ {"id", "Any-Lower; NFKD; [: Nonspacing Mark :] Remove; [\\x20] Remove", NULL};
+ struct fts_filter *norm;
+ const char *error;
+ unsigned int i;
+
+ test_begin("fts filter normalizer empty tokens");
+ test_assert(fts_filter_create(fts_filter_normalizer_icu, NULL, NULL, settings, &norm, &error) == 0);
+ for (i = 0; i < N_ELEMENTS(empty_tokens); i++) {
+ const char *token = empty_tokens[i];
+ test_assert_idx(fts_filter_filter(norm, &token, &error) == 0, i);
+ }
+ fts_filter_unref(&norm);
+ test_end();
+}
+
+static void test_fts_filter_normalizer_baddata(void)
+{
+ const char * const settings[] =
+ {"id", "Any-Lower; NFKD; [: Nonspacing Mark :] Remove", NULL};
+ struct fts_filter *norm;
+ const char *token, *error;
+ string_t *str;
+ unichar_t i;
+
+ test_begin("fts filter normalizer bad data");
+
+ test_assert(fts_filter_create(fts_filter_normalizer_icu, NULL, NULL, settings, &norm, &error) == 0);
+ str = t_str_new(128);
+ for (i = 1; i < 0x1ffff; i++) {
+ if (!uni_is_valid_ucs4(i)) continue;
+ str_truncate(str, 0);
+ uni_ucs4_to_utf8_c(i, str);
+ token = str_c(str);
+ T_BEGIN {
+ test_assert_idx(fts_filter_filter(norm, &token, &error) >= 0, i);
+ } T_END;
+ }
+
+ str_truncate(str, 0);
+ uni_ucs4_to_utf8_c(UNICHAR_T_MAX, str);
+ token = str_c(str);
+ test_assert(fts_filter_filter(norm, &token, &error) >= 0);
+
+ fts_filter_unref(&norm);
+ test_end();
+}
+
+static void test_fts_filter_normalizer_invalid_id(void)
+{
+ struct fts_filter *norm = NULL;
+ const char *settings[] =
+ {"id", "Any-One-Out-There; DKFN; [: Nonspacing Mark :] Remove",
+ NULL};
+ const char *error = NULL, *token = "foo";
+
+ test_begin("fts filter normalizer invalid id");
+ test_assert(fts_filter_create(fts_filter_normalizer_icu, NULL, NULL, settings, &norm, &error) == 0);
+ test_assert(error == NULL);
+ test_assert(fts_filter_filter(norm, &token, &error) < 0 && error != NULL);
+ fts_filter_unref(&norm);
+ test_end();
+}
+
+static void test_fts_filter_normalizer_oversized(void)
+{
+ struct fts_filter *norm = NULL;
+ const char *settings[] =
+ {"id", "Any-Lower; NFKD; [: Nonspacing Mark :] Remove", "maxlen", "250",
+ NULL};
+ const char *error = NULL;
+ const char *token = "\xe4\x95\x91\x25\xe2\x94\xad\xe1\x90\xad\xee\x94\x81\xe2\x8e\x9e"
+ "\xe7\x9a\xb7\xea\xbf\x97\xe3\xb2\x8f\xe4\x9c\xbe\xee\xb4\x98\xe1"
+ "\x8d\x99\xe2\x91\x83\xe3\xb1\xb8\xef\xbf\xbd\xe8\xbb\x9c\xef\xbf"
+ "\xbd\xea\xbb\x98\xea\xb5\xac\xe4\x87\xae\xe4\x88\x93\xe9\x86\x8f"
+ "\xe9\x86\x83\xe6\x8f\x8d\xe7\xa3\x9d\xed\x89\x96\xe2\x89\x85\xe6"
+ "\x8c\x82\xec\x80\x98\xee\x91\x96\xe7\xa8\x8a\xec\xbc\x85\xeb\x9c"
+ "\xbd\xeb\x97\x95\xe3\xa4\x9d\xd7\xb1\xea\xa7\x94\xe0\xbb\xac\xee"
+ "\x95\x87\xd5\x9d\xe8\xba\x87\xee\x8b\xae\xe5\xb8\x80\xe9\x8d\x82"
+ "\xe7\xb6\x8c\xe7\x9b\xa0\xef\x82\x9f\xed\x96\xa4\xe3\x8d\xbc\xe1"
+ "\x81\xbd\xe9\x81\xb2\xea\xac\xac\xec\x9b\x98\xe7\x84\xb2\xee\xaf"
+ "\xbc\xeb\xa2\x9d\xe9\x86\xb3\xe0\xb0\x89\xeb\x80\xb6\xe3\x8c\x9d"
+ "\xe9\x8f\x9e\xe2\xae\x8a\xee\x9e\x9a\xef\xbf\xbd\xe7\xa3\x9b\xe4"
+ "\xa3\x8b\xe4\x82\xb9\xeb\x8e\x93\xec\xb5\x82\xe5\xa7\x81\xe2\x8c"
+ "\x97\xea\xbb\xb4\xe5\x85\xb7\xeb\x96\xbe\xe7\x97\x91\xea\xbb\x98"
+ "\xe6\xae\xb4\xe9\x8a\x85\xc4\xb9\xe4\x90\xb2\xe9\x96\xad\xef\x90"
+ "\x9c\xe5\xa6\xae\xe9\x93\x91\xe8\x87\xa1";
+
+ test_begin("fts filter normalizer over-sized token");
+ test_assert(fts_filter_create(fts_filter_normalizer_icu, NULL, NULL, settings, &norm, &error) == 0);
+ test_assert(error == NULL);
+ test_assert(fts_filter_filter(norm, &token, &error) >= 0);
+ test_assert(strlen(token) <= 250);
+ fts_filter_unref(&norm);
+ test_end();
+}
+
+static void test_fts_filter_normalizer_truncation(void)
+{
+ struct fts_filter *norm = NULL;
+ const char *settings[] =
+ {"id", "Any-Lower;", "maxlen", "10",
+ NULL};
+ const char *error = NULL;
+ const char *token = "abcdefghi\xC3\x85";
+
+ test_begin("fts filter normalizer token truncated mid letter");
+ test_assert(fts_filter_create(fts_filter_normalizer_icu, NULL, NULL,
+ settings, &norm, &error) == 0);
+ test_assert(error == NULL);
+ test_assert(fts_filter_filter(norm, &token, &error) >= 0);
+ test_assert(strcmp(token, "abcdefghi") == 0);
+ fts_filter_unref(&norm);
+ test_end();
+}
+
+#ifdef HAVE_FTS_STEMMER
+static void test_fts_filter_normalizer_stopwords_stemmer_eng(void)
+{
+ int ret;
+ struct fts_filter *normalizer;
+ struct fts_filter *stemmer;
+ struct fts_filter *filter;
+ const char *error;
+ const char * const id_settings[] =
+ //{"id", "Any-Lower; NFKD; [: Nonspacing Mark :] Remove; NFC", NULL};
+ {"id", "Lower", NULL};
+ const char *token = NULL;
+ const char * const tokens[] = {
+ "dries" ,"friendlies", "All", "human", "beings", "are",
+ "born", "free", "and", "equal", "in", "dignity", "and",
+ "rights", "They", "are", "endowed", "with", "reason", "and",
+ "conscience", "and", "should", "act", "towards", "one",
+ "another", "in", "a", "spirit", "of", "brotherhood", "ABCFoo",
+ NULL};
+ const char * const bases[] = {
+ "dri" ,"friend", "all", "human", "be", NULL, "born", "free",
+ NULL, "equal", NULL, "digniti", NULL, "right", NULL, NULL,
+ "endow", NULL, "reason", NULL, "conscienc", NULL, "should",
+ "act", "toward", "one", "anoth", NULL, NULL, "spirit", NULL,
+ "brotherhood", "abcfoo", NULL};
+ const char * const *tpp;
+ const char * const *bpp;
+
+ test_begin("fts filters normalizer, stopwords and stemming chained, English");
+
+ test_assert(fts_filter_create(fts_filter_normalizer_icu, NULL, NULL, id_settings, &normalizer, &error) == 0);
+ test_assert(fts_filter_create(fts_filter_stopwords, normalizer, &english_language, stopword_settings, &filter, &error) == 0);
+ test_assert(fts_filter_create(fts_filter_stemmer_snowball, filter, &english_language, NULL, &stemmer, &error) == 0);
+
+ bpp = bases;
+ for (tpp = tokens; *tpp != NULL; tpp++) {
+ token = *tpp;
+ ret = fts_filter_filter(stemmer, &token, &error);
+ if (ret <= 0) {
+ test_assert(ret == 0);
+ test_assert(*bpp == NULL);
+ } else {
+ test_assert(*bpp != NULL);
+ test_assert(strcmp(*bpp, token) == 0);
+ }
+ bpp++;
+ }
+ fts_filter_unref(&stemmer);
+ fts_filter_unref(&filter);
+ fts_filter_unref(&normalizer);
+ test_assert(stemmer == NULL);
+ test_assert(filter == NULL);
+ test_assert(normalizer == NULL);
+ test_end();
+}
+
+static void test_fts_filter_stopwords_normalizer_stemmer_no(void)
+{
+ int ret;
+ struct fts_filter *normalizer;
+ struct fts_filter *stemmer;
+ struct fts_filter *filter;
+ const char *error;
+ const char *token = NULL;
+ const char * const tokens[] = {
+ /* Nynorsk*/
+ "Alle", "har", "plikter", "andsynes", "samfunnet", "d\xC3\xA5",
+ "personlegdomen", "til", "den", "einskilde", "einast", "der",
+ "kan", "f\xC3\xA5", "frie", "og", "fullgode",
+ "voksterk\xC3\xA5r",
+ /* Bokmal */
+ "Alle", "mennesker", "er", "f\xC3\xB8""dt", "frie", "og", "med",
+ "samme", "menneskeverd", "og", "menneskerettigheter", "De",
+ "er", "utstyrt", "med", "fornuft", "og", "samvittighet",
+ "og", "b\xC3\xB8r", "handle", "mot", "hverandre", "i",
+ "brorskapets", "\xC3\xA5nd", NULL};
+
+ const char * const bases[] = {
+ /* Nynorsk*/
+ "all", NULL, "plikt", "andsyn", "samfunn", NULL,
+ "personlegdom", NULL, NULL, "einskild", "ein", NULL, NULL,
+ "fa", "frie", NULL, "fullgod", "voksterk",
+ /* Bokmal */
+ "all", "mennesk", NULL, "f\xC3\xB8""dt", "frie", NULL, NULL,
+ NULL, "menneskeverd", NULL, "menneskerett", "de", NULL,
+ "utstyrt", NULL, "fornuft", NULL, "samvitt", NULL, "b\xC3\xB8r",
+ "handl", NULL, "hverandr", NULL, "brorskap", "and", NULL};
+ const char * const *tpp;
+ const char * const *bpp;
+
+ test_begin("fts filters with stopwords, default normalizer and stemming chained, Norwegian");
+
+ test_assert(fts_filter_create(fts_filter_stopwords, NULL, &norwegian_language, stopword_settings, &filter, &error) == 0);
+ test_assert(fts_filter_create(fts_filter_normalizer_icu, filter, NULL, NULL, &normalizer, &error) == 0);
+ test_assert(fts_filter_create(fts_filter_stemmer_snowball, normalizer, &norwegian_language, NULL, &stemmer, &error) == 0);
+
+ bpp = bases;
+ for (tpp = tokens; *tpp != NULL; tpp++) {
+ token = *tpp;
+ ret = fts_filter_filter(stemmer, &token, &error);
+ if (ret <= 0) {
+ test_assert(ret == 0);
+ test_assert(*bpp == NULL);
+ } else {
+ test_assert(*bpp != NULL);
+ test_assert(null_strcmp(*bpp, token) == 0);
+ }
+ bpp++;
+ }
+ fts_filter_unref(&stemmer);
+ fts_filter_unref(&normalizer);
+ fts_filter_unref(&filter);
+ test_assert(stemmer == NULL);
+ test_assert(filter == NULL);
+ test_assert(normalizer == NULL);
+ test_end();
+}
+
+static void test_fts_filter_stopwords_normalizer_stemmer_sv(void)
+{
+ int ret;
+ struct fts_filter *normalizer;
+ struct fts_filter *stemmer;
+ struct fts_filter *filter;
+ const char *error;
+ const char *token = NULL;
+ const char * const tokens[] = {
+ "Enär", "erkännandet", "av", "det", "inneboende", "värdet",
+ "hos", "alla", "medlemmar", "av", "människosläktet", "och",
+ "av", "deras", "lika", "och", "oförytterliga", "rättigheter",
+ "är", "grundvalen", "för", "frihet", "rättvisa", "och", "fred",
+ "i", "världen", NULL};
+ const char * const bases[] = {
+ "enar", "erkan", NULL, NULL, "inneboend", "vardet", "hos", NULL,
+ "medlemm", NULL, "manniskoslaktet", NULL, NULL, NULL, "lik",
+ NULL, "oforytter", "ratt", NULL, "grundval", NULL, "frihet",
+ "rattvis", NULL, "fred", NULL, "varld", NULL};
+ const char * const *tpp;
+ const char * const *bpp;
+
+ test_begin("fts filters with stopwords, default normalizer and stemming chained, Swedish");
+
+
+ test_assert(fts_filter_create(fts_filter_stopwords, NULL, &swedish_language, stopword_settings, &filter, &error) == 0);
+ test_assert(fts_filter_create(fts_filter_normalizer_icu, filter, NULL, NULL, &normalizer, &error) == 0);
+ test_assert(fts_filter_create(fts_filter_stemmer_snowball, normalizer, &swedish_language, NULL, &stemmer, &error) == 0);
+
+ bpp = bases;
+ for (tpp = tokens; *tpp != NULL; tpp++) {
+ token = *tpp;
+ ret = fts_filter_filter(stemmer, &token, &error);
+ if (ret <= 0) {
+ test_assert(ret == 0);
+ test_assert(*bpp == NULL);
+ } else {
+ test_assert(*bpp != NULL);
+ test_assert(null_strcmp(*bpp, token) == 0);
+ }
+ bpp++;
+ }
+ fts_filter_unref(&stemmer);
+ fts_filter_unref(&normalizer);
+ fts_filter_unref(&filter);
+ test_assert(stemmer == NULL);
+ test_assert(filter == NULL);
+ test_assert(normalizer == NULL);
+ test_end();
+}
+#endif
+#endif
+
+static void test_fts_filter_english_possessive(void)
+{
+ struct fts_filter *norm = NULL;
+ const char *input[] = {
+ "foo'",
+
+ "foo's",
+ "foo\xC3\xA4's",
+ "foo'S",
+ "foos'S",
+ "foo's's",
+ "foo'ss",
+
+ "foo\xE2\x80\x99s",
+ "foo\xC3\xA4\xE2\x80\x99s",
+ "foo\xE2\x80\x99S",
+ "foos\xE2\x80\x99S",
+ "foo\xE2\x80\x99s\xE2\x80\x99s",
+ "foo\xE2\x80\x99ss"
+ };
+ const char *expected_output[] = {
+ "foo'",
+
+ "foo",
+ "foo\xC3\xA4",
+ "foo",
+ "foos",
+ "foo's",
+ "foo'ss",
+
+ "foo",
+ "foo\xC3\xA4",
+ "foo",
+ "foos",
+ "foo\xE2\x80\x99s",
+ "foo\xE2\x80\x99ss"
+ };
+ const char *error = NULL;
+ const char *token = NULL;
+ unsigned int i;
+
+ test_begin("fts filter english possessive");
+
+ test_assert(fts_filter_create(fts_filter_english_possessive, NULL, NULL, NULL, &norm, &error) == 0);
+ for (i = 0; i < N_ELEMENTS(input); i++) {
+ token = input[i];
+ test_assert_idx(fts_filter_filter(norm, &token, &error) == 1, i);
+ test_assert_idx(null_strcmp(token, expected_output[i]) == 0, i);
+ }
+ fts_filter_unref(&norm);
+ test_assert(norm == NULL);
+ test_end();
+}
+
+/* TODO: Functions to test 1. ref-unref pairs 2. multiple registers +
+ an unregister + find */
+
+int main(void)
+{
+ static void (*const test_functions[])(void) = {
+ test_fts_filter_find,
+ test_fts_filter_contractions_fail,
+ test_fts_filter_contractions_fr,
+ test_fts_filter_lowercase,
+#ifdef HAVE_LIBICU
+ test_fts_filter_lowercase_utf8,
+ test_fts_filter_lowercase_too_long_utf8,
+#endif
+ test_fts_filter_stopwords_eng,
+ test_fts_filter_stopwords_fin,
+ test_fts_filter_stopwords_fra,
+ test_fts_filter_stopwords_no,
+ test_fts_filter_stopwords_fail_lazy_init,
+ test_fts_filter_stopwords_malformed,
+#ifdef HAVE_FTS_STEMMER
+ test_fts_filter_stemmer_snowball_stem_english,
+ test_fts_filter_stemmer_snowball_stem_french,
+ test_fts_filter_stopwords_stemmer_eng,
+#endif
+#ifdef HAVE_LIBICU
+ test_fts_filter_normalizer_swedish_short,
+ test_fts_filter_normalizer_swedish_short_default_id,
+ test_fts_filter_normalizer_french,
+ test_fts_filter_normalizer_empty,
+ test_fts_filter_normalizer_baddata,
+ test_fts_filter_normalizer_invalid_id,
+ test_fts_filter_normalizer_oversized,
+ test_fts_filter_normalizer_truncation,
+#ifdef HAVE_FTS_STEMMER
+ test_fts_filter_normalizer_stopwords_stemmer_eng,
+ test_fts_filter_stopwords_normalizer_stemmer_no,
+ test_fts_filter_stopwords_normalizer_stemmer_sv,
+#endif
+#endif
+ test_fts_filter_english_possessive,
+ NULL
+ };
+ int ret;
+
+ fts_filters_init();
+ ret = test_run(test_functions);
+ fts_filters_deinit();
+ return ret;
+}
diff --git a/src/lib-fts/test-fts-icu.c b/src/lib-fts/test-fts-icu.c
new file mode 100644
index 0000000..b00b613
--- /dev/null
+++ b/src/lib-fts/test-fts-icu.c
@@ -0,0 +1,202 @@
+/* Copyright (c) 2015-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "str.h"
+#include "unichar.h"
+#include "test-common.h"
+#include "fts-icu.h"
+
+#include <unicode/uclean.h>
+
+static void test_fts_icu_utf8_to_utf16_ascii_resize(void)
+{
+ ARRAY_TYPE(icu_utf16) dest;
+
+ test_begin("fts_icu_utf8_to_utf16 ascii resize");
+ t_array_init(&dest, 2);
+ test_assert(buffer_get_writable_size(dest.arr.buffer) == 4);
+ fts_icu_utf8_to_utf16(&dest, "12");
+ test_assert(array_count(&dest) == 2);
+ test_assert(buffer_get_writable_size(dest.arr.buffer) == 4);
+
+ fts_icu_utf8_to_utf16(&dest, "123");
+ test_assert(array_count(&dest) == 3);
+ test_assert(buffer_get_writable_size(dest.arr.buffer) == 7);
+
+ fts_icu_utf8_to_utf16(&dest, "12345");
+ test_assert(array_count(&dest) == 5);
+
+ test_end();
+}
+
+static void test_fts_icu_utf8_to_utf16_32bit_resize(void)
+{
+ ARRAY_TYPE(icu_utf16) dest;
+ unsigned int i;
+
+ test_begin("fts_icu_utf8_to_utf16 32bit resize");
+ for (i = 1; i <= 2; i++) {
+ t_array_init(&dest, i);
+ test_assert(buffer_get_writable_size(dest.arr.buffer) == i*2);
+ fts_icu_utf8_to_utf16(&dest, "\xF0\x90\x90\x80"); /* 0x10400 */
+ test_assert(array_count(&dest) == 2);
+ }
+
+ test_end();
+}
+
+static void test_fts_icu_utf16_to_utf8(void)
+{
+ string_t *dest = t_str_new(64);
+ const UChar src[] = { 0xbd, 'b', 'c' };
+ unsigned int i;
+
+ test_begin("fts_icu_utf16_to_utf8");
+ for (i = N_ELEMENTS(src); i > 0; i--) {
+ fts_icu_utf16_to_utf8(dest, src, i);
+ test_assert(dest->used == i+1);
+ }
+ test_end();
+}
+
+static void test_fts_icu_utf16_to_utf8_resize(void)
+{
+ string_t *dest;
+ const UChar src = UNICODE_REPLACEMENT_CHAR;
+ unsigned int i;
+
+ test_begin("fts_icu_utf16_to_utf8 resize");
+ for (i = 2; i <= 6; i++) {
+ dest = t_str_new(i);
+ test_assert(buffer_get_writable_size(dest) == i);
+ fts_icu_utf16_to_utf8(dest, &src, 1);
+ test_assert(dest->used == 3);
+ test_assert(strcmp(str_c(dest), UNICODE_REPLACEMENT_CHAR_UTF8) == 0);
+ }
+
+ test_end();
+}
+
+static UTransliterator *get_translit(const char *id)
+{
+ UTransliterator *translit;
+ ARRAY_TYPE(icu_utf16) id_utf16;
+ UErrorCode err = U_ZERO_ERROR;
+ UParseError perr;
+
+ t_array_init(&id_utf16, 8);
+ fts_icu_utf8_to_utf16(&id_utf16, id);
+ translit = utrans_openU(array_front(&id_utf16),
+ array_count(&id_utf16),
+ UTRANS_FORWARD, NULL, 0, &perr, &err);
+ test_assert(!U_FAILURE(err));
+ return translit;
+}
+
+static void test_fts_icu_translate(void)
+{
+ const char *translit_id = "Any-Lower";
+ UTransliterator *translit;
+ ARRAY_TYPE(icu_utf16) dest;
+ const UChar src[] = { 0xbd, 'B', 'C' };
+ const char *error;
+ unsigned int i;
+
+ test_begin("fts_icu_translate");
+ t_array_init(&dest, 32);
+ translit = get_translit(translit_id);
+ for (i = N_ELEMENTS(src); i > 0; i--) {
+ array_clear(&dest);
+ test_assert(fts_icu_translate(&dest, src, i,
+ translit, &error) == 0);
+ test_assert(array_count(&dest) == i);
+ }
+ utrans_close(translit);
+ test_end();
+}
+
+static void test_fts_icu_translate_resize(void)
+{
+ const char *translit_id = "Any-Hex";
+ const char *src_utf8 = "FOO";
+ ARRAY_TYPE(icu_utf16) src_utf16, dest;
+ UTransliterator *translit;
+ const char *error;
+ unsigned int i;
+
+ test_begin("fts_icu_translate_resize resize");
+
+ t_array_init(&src_utf16, 8);
+ translit = get_translit(translit_id);
+ for (i = 1; i <= 10; i++) {
+ array_clear(&src_utf16);
+ fts_icu_utf8_to_utf16(&src_utf16, src_utf8);
+ t_array_init(&dest, i);
+ test_assert(buffer_get_writable_size(dest.arr.buffer) == i*2);
+ test_assert(fts_icu_translate(&dest, array_front(&src_utf16),
+ array_count(&src_utf16),
+ translit, &error) == 0);
+ }
+
+ utrans_close(translit);
+ test_end();
+}
+
+static void test_fts_icu_lcase(void)
+{
+ const char *src = "aBcD\xC3\x84\xC3\xA4";
+ string_t *dest = t_str_new(64);
+
+ test_begin("fts_icu_lcase");
+ fts_icu_lcase(dest, src);
+ test_assert(strcmp(str_c(dest), "abcd\xC3\xA4\xC3\xA4") == 0);
+ test_end();
+}
+
+static void test_fts_icu_lcase_resize(void)
+{
+ const char *src = "a\xC3\x84";
+ string_t *dest;
+ unsigned int i;
+
+ test_begin("fts_icu_lcase resize");
+ for (i = 1; i <= 3; i++) {
+ dest = t_str_new(i);
+ test_assert(buffer_get_writable_size(dest) == i);
+ fts_icu_lcase(dest, src);
+ test_assert(strcmp(str_c(dest), "a\xC3\xA4") == 0);
+ test_assert(buffer_get_writable_size(dest) == 3);
+ }
+
+ test_end();
+}
+
+static void test_fts_icu_lcase_resize_invalid_utf8(void)
+{
+ string_t *dest;
+
+ test_begin("fts_icu_lcase resize invalid utf8");
+ dest = t_str_new(1);
+ fts_icu_lcase(dest, ".\x80.");
+ test_end();
+}
+
+int main(void)
+{
+ static void (*const test_functions[])(void) = {
+ test_fts_icu_utf8_to_utf16_ascii_resize,
+ test_fts_icu_utf8_to_utf16_32bit_resize,
+ test_fts_icu_utf16_to_utf8,
+ test_fts_icu_utf16_to_utf8_resize,
+ test_fts_icu_translate,
+ test_fts_icu_translate_resize,
+ test_fts_icu_lcase,
+ test_fts_icu_lcase_resize,
+ test_fts_icu_lcase_resize_invalid_utf8,
+ NULL
+ };
+ int ret = test_run(test_functions);
+ fts_icu_deinit();
+ return ret;
+}
diff --git a/src/lib-fts/test-fts-language.c b/src/lib-fts/test-fts-language.c
new file mode 100644
index 0000000..e5968e9
--- /dev/null
+++ b/src/lib-fts/test-fts-language.c
@@ -0,0 +1,323 @@
+/* Copyright (c) 2014-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "test-common.h"
+#include "fts-language.h"
+/* TODO: These checks will not work without proper libtextcat configuration.
+ As such, they are not really unit test to be coupled with the build. */
+
+const char *const settings[] =
+ {"fts_language_config", TEXTCAT_DATADIR"/fpdb.conf",
+ "fts_language_data", TEXTCAT_DATADIR"/", NULL};
+
+/* Detect Finnish. fi--utf8 */
+static void test_fts_language_detect_finnish(void)
+{
+ struct fts_language_list *lp = NULL;
+ const struct fts_language *lang_r = NULL;
+ const unsigned char finnish[] =
+ "Yhdistyneiden kansakuntien kolmas yleiskokous hyv\xC3\xA4ksyi "\
+ "ja julkisti ihmisoikeuksien yleismaailmallisen julistuksen "\
+ "joulukuun 10. p\xC3\xA4iv\xC3\xA4n\xC3\xA4 1948. Julistuksen "\
+ "hyv\xC3\xA4ksymisen puolesta \xC3\xA4\xC3\xA4nesti 48 maata. "\
+ "Mik\xC3\xA4\xC3\xA4n maa ei \xC3\xA4\xC3\xA4nest\xC3\xA4nyt "\
+ "vastaan. Kahdeksan maata pid\xC3\xA4ttyi "\
+ "\xC3\xA4\xC3\xA4nest\xC3\xA4m\xC3\xA4st\xC3\xA4.";
+ const char names[] = "de, fi, en";
+ const char *unknown, *error;
+ test_begin("fts language detect Finnish");
+ test_assert(fts_language_list_init(settings, &lp, &error) == 0);
+ test_assert(fts_language_list_add_names(lp, names, &unknown) == TRUE);
+ test_assert(fts_language_detect(lp, finnish, sizeof(finnish)-1, &lang_r, &error)
+ == FTS_LANGUAGE_RESULT_OK);
+ test_assert(strcmp(lang_r->name, "fi") == 0);
+ fts_language_list_deinit(&lp);
+ test_end();
+}
+
+/* Detect English */
+static void test_fts_language_detect_english(void)
+{
+ struct fts_language_list *lp = NULL;
+ const struct fts_language *lang_r = NULL;
+ const unsigned char english[] = "Whereas recognition of the inherent dignity and"\
+ " of the equal and inalienable rights of all members of the human"\
+ "family is the foundation of freedom, justice and peace in the "\
+ "world,\n Whereas disregard and contempt for human rights have "\
+ "resulted in barbarous acts which have outraged the conscience"\
+ "of mankind, and the advent of a world in which human beings"\
+ "shall enjoy freedom of speech and belief and freedom from "\
+ "fear and want has been proclaimed as the highest aspiration"\
+ "of the common people, ";
+
+ const char names[] = "fi, de, fr, en";
+ const char *unknown, *error;
+ test_begin("fts language detect English");
+ test_assert(fts_language_list_init(settings, &lp, &error) == 0);
+ test_assert(fts_language_list_add_names(lp, names, &unknown) == TRUE);
+ test_assert(fts_language_detect(lp, english, sizeof(english)-1, &lang_r, &error)
+ == FTS_LANGUAGE_RESULT_OK);
+ test_assert(strcmp(lang_r->name, "en") == 0);
+ fts_language_list_deinit(&lp);
+ test_end();
+}
+
+/* Detect French */
+static void test_fts_language_detect_french(void)
+{
+ struct fts_language_list *lp = NULL;
+ const struct fts_language *lang_r = NULL;
+ const unsigned char french[] =
+ "D\xC3\xA9""claration universelle des droits de l\xE2\x80\x99"
+ "homme Pr\xC3\xA9""ambule Consid\xC3\xA9rant que la "\
+ "reconnaissance de la dignit\xC3\xA9 inh\xC3\xA9rente \xC3\xA0"\
+ " tous les membres de la famille humaine et de leurs droits "\
+ "\xC3\xA9gaux et inali\xC3\xA9nables constitue le fondement de"\
+ " la libert\xC3\xA9, de la justice et de la paix dans le monde,"\
+ " Consid\xC3\xA9rant que la m\xC3\xA9""connaissance et le "\
+ "m\xC3\xA9pris des droits de l\xE2\x80\x99homme ont conduit "\
+ "\xC3\xA0 des actes de barbarie qui r\xC3\xA9voltent la "\
+ "conscience de l\xE2\x80\x99humanit\xC3\xA9 et que "\
+ "l\xE2\x80\x99""av\xC3\xA8nement d\xE2\x80\x99un monde o\xC3\xB9"\
+ " les \xC3\xAAtres humains seront libres de parler et de "\
+ "croire, lib\xC3\xA9r\xC3\xA9s de la terreur et de la "\
+ "mis\xC3\xA8re, a \xC3\xA9t\xC3\xA9 proclam\xC3\xA9 comme la "\
+ "plus haute aspiration de l\xE2\x80\x99homme,";
+
+
+ const char names[] = "de, fi, fr, en";
+ const char *unknown, *error;
+ test_begin("fts language detect French");
+ test_assert(fts_language_list_init(settings, &lp, &error) == 0);
+ test_assert(fts_language_list_add_names(lp, names, &unknown) == TRUE);
+ test_assert(fts_language_detect(lp, french, sizeof(french)-1, &lang_r, &error)
+ == FTS_LANGUAGE_RESULT_OK);
+ test_assert(strcmp(lang_r->name, "fr") == 0);
+ fts_language_list_deinit(&lp);
+ test_end();
+}
+/* Detect German */
+static void test_fts_language_detect_german(void)
+{
+ struct fts_language_list *lp = NULL;
+ const struct fts_language *lang_r = NULL;
+ const unsigned char german[] =
+ "Artikel 1"\
+ "Alle Menschen sind frei und gleich an W\xC3\xBCrde und Rechten "\
+ "geboren. Sie sind mit Vernunft und Gewissen begabt und sollen "\
+ "einander im Geist der Br\xC3\xBC""derlichkeit begegnen." \
+ "Artikel 2 Jeder hat Anspruch auf die in dieser "\
+ "Erkl\xC3\xA4rung verk\xC3\xBCndeten Rechte und Freiheiten ohne"\
+ "irgendeinen Unterschied, etwa nach Rasse, Hautfarbe, "\
+ "Geschlecht, Sprache, Religion, politischer oder sonstiger "\
+ "\xC3\x9C""berzeugung, nationaler oder sozialer Herkunft, "\
+ "Verm\xC3\xB6gen, Geburt oder sonstigem Stand. Des weiteren "\
+ "darf kein Unterschied gemacht werden auf Grund der "\
+ "politischen, rechtlichen oder internationalen Stellung des "\
+ "Landes oder Gebiets, dem eine Person angeh\xC3\xB6rt, "\
+ "gleichg\xC3\xBCltig, ob dieses unabh\xC3\xA4ngig ist, unter "\
+ "Treuhandschaft steht, keine Selbstregierung besitzt oder "\
+ "sonst in seiner Souver\xC3\xA4nit\xC3\xA4t "\
+ "eingeschr\xC3\xA4nkt ist.";
+
+
+
+ const char names[] = "fi, de, fr, en";
+ const char *unknown, *error;
+ test_begin("fts language detect German");
+ test_assert(fts_language_list_init(settings, &lp, &error) == 0);
+ test_assert(fts_language_list_add_names(lp, names, &unknown) == TRUE);
+ test_assert(fts_language_detect(lp, german, sizeof(german)-1, &lang_r, &error)
+ == FTS_LANGUAGE_RESULT_OK);
+ test_assert(strcmp(lang_r->name, "de") == 0);
+ fts_language_list_deinit(&lp);
+ test_end();
+}
+
+/* Detect Swedish */
+static void test_fts_language_detect_swedish(void)
+{
+ struct fts_language_list *lp = NULL;
+ const struct fts_language *lang_r = NULL;
+ const unsigned char swedish[] =
+ "Artikel 1."\
+ "Alla m\xC3\xA4nniskor \xC3\xA4ro f\xC3\xB6""dda fria och lika"\
+ " i v\xC3\xA4rde och r\xC3\xA4ttigheter. De \xC3\xA4ro "\
+ "utrustade med f\xC3\xB6rnuft och samvete och b\xC3\xB6ra "\
+ "handla gentemot varandra i en anda av broderskap.";
+
+
+
+ const char names[] = "fi, de, sv, fr, en";
+ const char *unknown, *error;
+ test_begin("fts language detect Swedish");
+ test_assert(fts_language_list_init(settings, &lp, &error) == 0);
+ test_assert(fts_language_list_add_names(lp, names, &unknown) == TRUE);
+ test_assert(fts_language_detect(lp, swedish, sizeof(swedish)-1, &lang_r, &error)
+ == FTS_LANGUAGE_RESULT_OK);
+ test_assert(strcmp(lang_r->name, "sv") == 0);
+ fts_language_list_deinit(&lp);
+ test_end();
+}
+
+/* Detect Bokmal */
+static void test_fts_language_detect_bokmal(void)
+{
+ struct fts_language_list *lp = NULL;
+ const struct fts_language *lang_r = NULL;
+ const unsigned char bokmal[] =
+ "Artikkel 1.\n"\
+ "Alle mennesker er f\xC3\xB8""dt frie og med samme menneskeverd"\
+ " og menneskerettigheter. De er utstyrt med fornuft og "\
+ "samvittighet og b\xC3\xB8r handle mot hverandre i "\
+ "brorskapets \xC3\xA5nd";
+
+ const char names[] = "fi, de, sv, no, fr, en";
+ const char *unknown, *error;
+ test_begin("fts language detect Bokmal as Norwegian");
+ test_assert(fts_language_list_init(settings, &lp, &error) == 0);
+ test_assert(fts_language_list_add_names(lp, names, &unknown) == TRUE);
+ test_assert(fts_language_detect(lp, bokmal, sizeof(bokmal)-1, &lang_r, &error)
+ == FTS_LANGUAGE_RESULT_OK);
+ test_assert(strcmp(lang_r->name, "no") == 0);
+ fts_language_list_deinit(&lp);
+ test_end();
+}
+
+/* Detect Nynorsk */
+static void test_fts_language_detect_nynorsk(void)
+{
+ struct fts_language_list *lp = NULL;
+ const struct fts_language *lang_r = NULL;
+ const unsigned char nynorsk[] =
+ "Artikkel 1.\n"\
+ "Alle menneske er f\xC3\xB8""dde til fridom og med same "\
+ "menneskeverd og menneskerettar. Dei har f\xC3\xA5tt fornuft "\
+ "og samvit og skal leve med kvarandre som br\xC3\xB8r.";
+
+
+ const char names[] = "fi, de, sv, no, fr, en";
+ const char *unknown, *error;
+ test_begin("fts language detect Nynorsk as Norwegian");
+ test_assert(fts_language_list_init(settings, &lp, &error) == 0);
+ test_assert(fts_language_list_add_names(lp, names, &unknown) == TRUE);
+ test_assert(fts_language_detect(lp, nynorsk, sizeof(nynorsk)-1, &lang_r, &error)
+ == FTS_LANGUAGE_RESULT_OK);
+ test_assert(strcmp(lang_r->name, "no") == 0);
+ fts_language_list_deinit(&lp);
+ test_end();
+}
+
+/* Detect Finnish as English */
+static void test_fts_language_detect_finnish_as_english(void)
+{
+ struct fts_language_list *lp = NULL;
+ const struct fts_language *lang_r = NULL;
+ const unsigned char finnish[] =
+ "Yhdistyneiden kansakuntien kolmas yleiskokous hyv\xC3\xA4ksyi "\
+ "ja julkisti ihmisoikeuksien yleismaailmallisen julistuksen "\
+ "joulukuun 10. p\xC3\xA4iv\xC3\xA4n\xC3\xA4 1948. Julistuksen "\
+ "hyv\xC3\xA4ksymisen puolesta \xC3\xA4\xC3\xA4nesti 48 maata. "\
+ "Mik\xC3\xA4\xC3\xA4n maa ei \xC3\xA4\xC3\xA4nest\xC3\xA4nyt "\
+ "vastaan. Kahdeksan maata pid\xC3\xA4ttyi "\
+ "\xC3\xA4\xC3\xA4nest\xC3\xA4m\xC3\xA4st\xC3\xA4.";
+ const char names[] = "en";
+ const char *unknown, *error;
+ test_begin("fts language detect Finnish as English");
+ test_assert(fts_language_list_init(settings, &lp, &error) == 0);
+ test_assert(fts_language_list_add_names(lp, names, &unknown) == TRUE);
+ test_assert(fts_language_detect(lp, finnish, sizeof(finnish)-1, &lang_r, &error)
+ == FTS_LANGUAGE_RESULT_OK);
+ test_assert(strcmp(lang_r->name, "en") == 0);
+ fts_language_list_deinit(&lp);
+ test_end();
+}
+
+/* Successfully avoid detecting English, when en is not in language list. */
+static void test_fts_language_detect_na(void)
+{
+ struct fts_language_list *lp = NULL;
+ const struct fts_language *lang_r = NULL;
+ const unsigned char english[] = "Whereas recognition of the inherent dignity and"\
+ " of the equal and inalienable rights of all members of the human"\
+ "family is the foundation of freedom, justice and peace in the "\
+ "world,\n Whereas disregard and contempt for human rights have "\
+ "resulted in barbarous acts which have outraged the conscience"\
+ "of mankind, and the advent of a world in which human beings"\
+ "shall enjoy freedom of speech and belief and freedom from "\
+ "fear and want has been proclaimed as the highest aspiration"\
+ "of the common people, ";
+
+ const char names[] = "fi, de, fr";
+ const char *unknown, *error;
+ test_begin("fts language detect not available");
+ test_assert(fts_language_list_init(settings, &lp, &error) == 0);
+ test_assert(fts_language_list_add_names(lp, names, &unknown) == TRUE);
+ test_assert(fts_language_detect(lp, english, sizeof(english)-1, &lang_r, &error)
+ == FTS_LANGUAGE_RESULT_UNKNOWN);
+ fts_language_list_deinit(&lp);
+ test_end();
+}
+
+/* Successfully detect, that Klingon is unknown. */
+static void test_fts_language_detect_unknown(void)
+{
+ struct fts_language_list *lp = NULL;
+ const struct fts_language *lang_r = NULL;
+ const unsigned char klingon[] = "nobwI''a'pu'qoqvam'e' "\
+ "nuHegh'eghrupqa'moHlaHbe'law'lI'neS "\
+ "SeH'eghtaHghach'a'na'chajmo'.";
+
+ const char names[] = "fi, de, fr";
+ const char *unknown, *error;
+ test_begin("fts language detect unknown");
+ test_assert(fts_language_list_init(settings, &lp, &error) == 0);
+ test_assert(fts_language_list_add_names(lp, names, &unknown) == TRUE);
+ test_assert(fts_language_detect(lp, klingon, sizeof(klingon), &lang_r, &error)
+ == FTS_LANGUAGE_RESULT_UNKNOWN);
+ fts_language_list_deinit(&lp);
+ test_end();
+}
+static void test_fts_language_find_builtin(void)
+{
+ const struct fts_language *lp;
+ test_begin("fts language find built-in");
+ lp = fts_language_find("en");
+ i_assert(lp != NULL);
+ test_assert(strcmp(lp->name, "en") == 0);
+ test_end();
+}
+static void test_fts_language_register(void)
+{
+ const struct fts_language *lp;
+ test_begin("fts language register");
+ fts_language_register("jp");
+ lp = fts_language_find("jp");
+ i_assert(lp != NULL);
+ test_assert(strcmp(lp->name, "jp") == 0);
+ test_end();
+}
+
+int main(void)
+{
+ int ret;
+ static void (*const test_functions[])(void) = {
+ test_fts_language_detect_finnish,
+ test_fts_language_detect_english,
+ test_fts_language_detect_french,
+ test_fts_language_detect_german,
+ test_fts_language_detect_swedish,
+ test_fts_language_detect_bokmal,
+ test_fts_language_detect_nynorsk,
+ test_fts_language_detect_finnish_as_english,
+ test_fts_language_detect_na,
+ test_fts_language_detect_unknown,
+ test_fts_language_find_builtin,
+ test_fts_language_register,
+ NULL
+ };
+ fts_languages_init();
+ ret = test_run(test_functions);
+ fts_languages_deinit();
+ return ret;
+}
diff --git a/src/lib-fts/test-fts-tokenizer.c b/src/lib-fts/test-fts-tokenizer.c
new file mode 100644
index 0000000..955dca4
--- /dev/null
+++ b/src/lib-fts/test-fts-tokenizer.c
@@ -0,0 +1,690 @@
+/* Copyright (c) 2014-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "unichar.h"
+#include "str.h"
+#include "test-common.h"
+#include "fts-tokenizer.h"
+#include "fts-tokenizer-common.h"
+#include "fts-tokenizer-private.h"
+#include "fts-tokenizer-generic-private.h"
+
+/*there should be a trailing space ' ' at the end of each string except the last one*/
+#define TEST_INPUT_ADDRESS \
+ "@invalid invalid@ Abc Dfg <abc.dfg@example.com>, " \
+ "Bar Baz <bar@example.org>" \
+ "Foo Bar (comment)foo.bar@host.example.org " \
+ "foo, foo@domain " \
+ "abcdefghijklmnopqrstuvxyz.abcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyz@1bcdefghijklmnopqrstuvxy1.2bcdefghijklmnopqrstuvxy2.3bcdefghijklmnopqrstuvxy3.4bcdefghijklmnopqrstuvxy4.5bcdefghijklmnopqrstuvxy5.6bcdefghijklmnopqrstuvxy6.7bcdefghijklmnopqrstuvxy7.8bcdefghijklmnopqrstuvxy8.9bcdefghijklmnopqrstuvxy9.0bcdefghijklmnopqrstuvxy0.tld " \
+ "trailing, period@blue.com. " \
+ "multi-trialing, mul@trail.com..... " \
+ "m@s " \
+ "hypen@hypen-hypen.com " \
+ "hypen@hypen-hypen-sick.com.-"
+
+static const char *test_inputs[] = {
+ /* generic things and word truncation: */
+ "hello world\r\n\nAnd there\twas: text galor\xC3\xA9\xE2\x80\xA7 "
+ "abc@example.com, "
+ "Bar Baz <bar@example.org>, "
+ "foo@domain "
+ "1234567890123456789012345678\xC3\xA4,"
+ "12345678901234567890123456789\xC3\xA4,"
+ "123456789012345678901234567890\xC3\xA4,"
+ "and longlonglongabcdefghijklmnopqrstuvwxyz more.\n\n "
+ "(\"Hello world\")3.14 3,14 last",
+
+ "1.",
+
+ "' ' '' ''' 'quoted text' 'word' 'hlo words' you're bad'''word '''pre post'''",
+
+ "'1234567890123456789012345678\xC3\xA4,"
+ "123456789012345678901234567x'\xC3\xA4,"
+ "1234567890123456789012345678x're,"
+ "1234567890123456789012345678x',"
+ "1234567890123456789012345678x'',"
+ "12345678901234567890123456789x',"
+ "12345678901234567890123456789x'',"
+ "123456789012345678901234567890x',"
+ "123456789012345678901234567890x'',"
+
+ /* \xe28099 = U+2019 is a smart quote, sometimes used as an apostrophe */
+#define SQ "\xE2\x80\x99"
+ SQ " " SQ " " SQ SQ " " SQ SQ SQ " " SQ "quoted text" SQ SQ "word" SQ " " SQ "hlo words" SQ " you" SQ "re78901234567890123456789012 bad" SQ SQ SQ "word" SQ SQ SQ "pre post" SQ SQ SQ,
+
+ "you" SQ "re" SQ "xyz",
+
+ /* whitespace: with Unicode(utf8) U+FF01(ef bc 81)(U+2000(e2 80 80) and
+ U+205A(e2 81 9a) and U+205F(e2 81 9f) */
+ "hello\xEF\xBC\x81world\r\nAnd\xE2\x80\x80there\twas: text "
+ "galore\xE2\x81\x9F""and\xE2\x81\x9Amore.\n\n",
+
+ /* TR29 MinNumLet U+FF0E at end: u+FF0E is EF BC 8E */
+ "hello world\xEF\xBC\x8E",
+
+ /* TR29 WB5a */
+ "l" SQ "homme l" SQ "humanit\xC3\xA9 d" SQ "immixtions qu" SQ "il aujourd'hui que'euq"
+};
+
+static void test_fts_tokenizer_find(void)
+{
+ test_begin("fts tokenizer find");
+ test_assert(fts_tokenizer_find("email-address") == fts_tokenizer_email_address);
+ test_assert(fts_tokenizer_find("generic") == fts_tokenizer_generic);
+ test_end();
+}
+
+static unsigned int
+test_tokenizer_inputoutput(struct fts_tokenizer *tok, const char *_input,
+ const char *const *expected_output,
+ unsigned int first_outi)
+{
+ const unsigned char *input = (const unsigned char *)_input;
+ const char *token, *error;
+ unsigned int i, outi, max, char_len;
+ size_t input_len = strlen(_input);
+
+ /* test all input at once */
+ outi = first_outi;
+ while (fts_tokenizer_next(tok, input, input_len, &token, &error) > 0) {
+ test_assert_strcmp(token, expected_output[outi]);
+ outi++;
+ }
+ while (fts_tokenizer_next(tok, NULL, 0, &token, &error) > 0) {
+ test_assert_strcmp(token, expected_output[outi]);
+ outi++;
+ }
+ test_assert_idx(expected_output[outi] == NULL, outi);
+
+ /* test input one byte at a time */
+ outi = first_outi;
+ for (i = 0; i < input_len; i += char_len) {
+ char_len = uni_utf8_char_bytes(input[i]);
+ while (fts_tokenizer_next(tok, input+i, char_len, &token, &error) > 0) {
+ test_assert_strcmp(token, expected_output[outi]);
+ outi++;
+ }
+ }
+ while (fts_tokenizer_final(tok, &token, &error) > 0) {
+ test_assert_strcmp(token, expected_output[outi]);
+ outi++;
+ }
+ test_assert_idx(expected_output[outi] == NULL, outi);
+
+ /* test input in random chunks */
+ outi = first_outi;
+ for (i = 0; i < input_len; i += char_len) {
+ max = i_rand_minmax(1, input_len - i);
+ for (char_len = 0; char_len < max; )
+ char_len += uni_utf8_char_bytes(input[i+char_len]);
+ while (fts_tokenizer_next(tok, input+i, char_len, &token, &error) > 0) {
+ test_assert_strcmp(token, expected_output[outi]);
+ outi++;
+ }
+ }
+ while (fts_tokenizer_final(tok, &token, &error) > 0) {
+ test_assert_strcmp(token, expected_output[outi]);
+ outi++;
+ }
+ test_assert_idx(expected_output[outi] == NULL, outi);
+
+ return outi+1;
+}
+
+static void
+test_tokenizer_inputs(struct fts_tokenizer *tok,
+ const char *const *inputs, unsigned int count,
+ const char *const *expected_output)
+{
+ unsigned int i, outi = 0;
+
+ for (i = 0; i < count; i++) {
+ outi = test_tokenizer_inputoutput(tok, inputs[i],
+ expected_output, outi);
+ }
+ test_assert_idx(expected_output[outi] == NULL, outi);
+}
+
+static void test_fts_tokenizer_generic_only(void)
+{
+ static const char *const expected_output[] = {
+ "hello", "world", "And",
+ "there", "was", "text", "galor\xC3\xA9",
+ "abc", "example", "com", "Bar", "Baz",
+ "bar", "example", "org", "foo", "domain",
+ "1234567890123456789012345678\xC3\xA4",
+ "12345678901234567890123456789",
+ "123456789012345678901234567890",
+ "and", "longlonglongabcdefghijklmnopqr",
+ "more", "Hello", "world", "3", "14", "3", "14", "last", NULL,
+
+ "1", NULL,
+
+ "quoted", "text", "word", "hlo", "words", "you're", "bad",
+ "word", "pre", "post", NULL,
+
+ "1234567890123456789012345678\xC3\xA4",
+ "123456789012345678901234567x'",
+ "1234567890123456789012345678x'",
+ "1234567890123456789012345678x",
+ "1234567890123456789012345678x",
+ "12345678901234567890123456789x",
+ "12345678901234567890123456789x",
+ "123456789012345678901234567890",
+ "123456789012345678901234567890",
+
+ "quoted", "text", "word", "hlo", "words", "you're789012345678901234567890", "bad",
+ "word", "pre", "post", NULL,
+
+ "you're'xyz", NULL,
+
+ "hello", "world", "And",
+ "there", "was", "text", "galore",
+ "and", "more", NULL,
+
+ "hello", "world", NULL,
+
+ "l'homme", "l'humanit\xC3\xA9", "d'immixtions", "qu'il", "aujourd'hui", "que'euq", NULL,
+
+ NULL
+ };
+ struct fts_tokenizer *tok;
+ const char *error;
+
+ test_begin("fts tokenizer generic simple");
+ test_assert(fts_tokenizer_create(fts_tokenizer_generic, NULL, NULL, &tok, &error) == 0);
+ test_assert(((struct generic_fts_tokenizer *) tok)->algorithm == BOUNDARY_ALGORITHM_SIMPLE);
+
+ test_tokenizer_inputs(tok, test_inputs, N_ELEMENTS(test_inputs), expected_output);
+ fts_tokenizer_unref(&tok);
+ test_end();
+}
+
+const char *const tr29_settings[] = {"algorithm", "tr29", NULL};
+
+/* TODO: U+206F is in "Format" and therefore currently not word break.
+ This definitely needs to be remapped. */
+static void test_fts_tokenizer_generic_tr29_only(void)
+{
+ static const char *const expected_output[] = {
+ "hello", "world", "And",
+ "there", "was", "text", "galor\xC3\xA9",
+ "abc", "example", "com", "Bar", "Baz",
+ "bar", "example", "org", "foo", "domain",
+ "1234567890123456789012345678\xC3\xA4",
+ "12345678901234567890123456789",
+ "123456789012345678901234567890",
+ "and", "longlonglongabcdefghijklmnopqr",
+ "more", "Hello", "world", "3", "14", "3,14", "last", NULL,
+
+ "1", NULL,
+
+ "quoted", "text", "word", "hlo", "words", "you're", "bad",
+ "word", "pre", "post", NULL,
+
+ "1234567890123456789012345678\xC3\xA4",
+ "123456789012345678901234567x'",
+ "1234567890123456789012345678x'",
+ "1234567890123456789012345678x",
+ "1234567890123456789012345678x",
+ "12345678901234567890123456789x",
+ "12345678901234567890123456789x",
+ "123456789012345678901234567890",
+ "123456789012345678901234567890",
+
+ "quoted", "text", "word", "hlo", "words", "you're789012345678901234567890", "bad",
+ "word", "pre", "post", NULL,
+
+ "you're'xyz", NULL,
+
+ "hello", "world", "And",
+ "there", "was", "text", "galore",
+ "and", "more", NULL,
+
+ "hello", "world", NULL,
+
+ "l'homme", "l'humanit\xC3\xA9", "d'immixtions", "qu'il", "aujourd'hui", "que'euq", NULL,
+ NULL
+ };
+ struct fts_tokenizer *tok;
+ const char *error;
+
+ test_begin("fts tokenizer generic TR29");
+ test_assert(fts_tokenizer_create(fts_tokenizer_generic, NULL, tr29_settings, &tok, &error) == 0);
+ test_tokenizer_inputs(tok, test_inputs, N_ELEMENTS(test_inputs), expected_output);
+ fts_tokenizer_unref(&tok);
+ test_end();
+}
+
+const char *const tr29_settings_wb5a[] = {"algorithm", "tr29", "wb5a", "yes", NULL};
+
+/* TODO: U+206F is in "Format" and therefore currently not word break.
+ This definitely needs to be remapped. */
+static void test_fts_tokenizer_generic_tr29_wb5a(void)
+{
+ static const char *const expected_output[] = {
+ "hello", "world", "And",
+ "there", "was", "text", "galor\xC3\xA9",
+ "abc", "example", "com", "Bar", "Baz",
+ "bar", "example", "org", "foo", "domain",
+ "1234567890123456789012345678\xC3\xA4",
+ "12345678901234567890123456789",
+ "123456789012345678901234567890",
+ "and", "longlonglongabcdefghijklmnopqr",
+ "more", "Hello", "world", "3", "14", "3,14", "last", NULL,
+
+ "1", NULL,
+
+ "quoted", "text", "word", "hlo", "words", "you're", "bad",
+ "word", "pre", "post", NULL,
+
+ "1234567890123456789012345678\xC3\xA4",
+ "123456789012345678901234567x'",
+ "1234567890123456789012345678x'",
+ "1234567890123456789012345678x",
+ "1234567890123456789012345678x",
+ "12345678901234567890123456789x",
+ "12345678901234567890123456789x",
+ "123456789012345678901234567890",
+ "123456789012345678901234567890",
+
+ "quoted", "text", "word", "hlo", "words", "you're789012345678901234567890", "bad",
+ "word", "pre", "post", NULL,
+
+ "you're'xyz", NULL,
+
+ "hello", "world", "And",
+ "there", "was", "text", "galore",
+ "and", "more", NULL,
+
+ "hello", "world", NULL,
+
+ "l", "homme", "l", "humanit\xC3\xA9", "d", "immixtions", "qu", "il", "aujourd'hui", "que'euq", NULL,
+
+ NULL
+ };
+ struct fts_tokenizer *tok;
+ const char *error;
+
+ test_begin("fts tokenizer generic TR29 with WB5a");
+ test_assert(fts_tokenizer_create(fts_tokenizer_generic, NULL, tr29_settings_wb5a, &tok, &error) == 0);
+ test_tokenizer_inputs(tok, test_inputs, N_ELEMENTS(test_inputs), expected_output);
+ fts_tokenizer_unref(&tok);
+ test_end();
+}
+
+static void test_fts_tokenizer_address_only(void)
+{
+ static const char input[] = TEST_INPUT_ADDRESS;
+ static const char *const expected_output[] = {
+ "abc.dfg@example.com", "bar@example.org",
+ "foo.bar@host.example.org", "foo@domain",
+ "period@blue.com", /*trailing period '.' in email */
+ "mul@trail.com",
+ "m@s", /*one letter local-part and domain name */
+ "hypen@hypen-hypen.com",
+ "hypen@hypen-hypen-sick.com",
+ NULL
+ };
+ struct fts_tokenizer *tok;
+ const char *error;
+
+ test_begin("fts tokenizer email address only");
+ test_assert(fts_tokenizer_create(fts_tokenizer_email_address, NULL, NULL, &tok, &error) == 0);
+ test_tokenizer_inputoutput(tok, input, expected_output, 0);
+ fts_tokenizer_unref(&tok);
+ test_end();
+}
+
+static void test_fts_tokenizer_address_parent(const char *name, const char * const *settings)
+{
+ static const char input[] = TEST_INPUT_ADDRESS;
+ static const char *const expected_output[] = {
+ "invalid", "invalid", "Abc", "Dfg", "abc", "dfg", "example", "abc.dfg@example.com", "com",
+ "Bar", "Baz", "bar", "example", "bar@example.org", "org",
+ "Foo", "Bar", "comment", "foo", "bar", "host", "example", "foo.bar@host.example.org", "org",
+ "foo", "foo", "foo@domain", "domain",
+ "abcdefghijklmnopqrstuvxyz", "abcdefghijklmnopqrstuvxyzabcde",
+ "1bcdefghijklmnopqrstuvxy1",
+ "2bcdefghijklmnopqrstuvxy2",
+ "3bcdefghijklmnopqrstuvxy3",
+ "4bcdefghijklmnopqrstuvxy4",
+ "5bcdefghijklmnopqrstuvxy5",
+ "6bcdefghijklmnopqrstuvxy6",
+ "7bcdefghijklmnopqrstuvxy7",
+ "8bcdefghijklmnopqrstuvxy8",
+ "9bcdefghijklmnopqrstuvxy9",
+ "0bcdefghijklmnopqrstuvxy0", "tld",
+ "trailing", "period", "blue", "com", "period@blue.com",
+ "multi", "trialing", "mul", "trail", "com", "mul@trail.com",
+ "m", "m@s", "s",
+ "hypen", "hypen", "hypen", "hypen@hypen-hypen.com", "com",
+ "hypen", "hypen", "hypen", "sick", "com", "hypen@hypen-hypen-sick.com",
+ NULL
+ };
+ struct fts_tokenizer *tok, *gen_tok;
+ const char *error;
+
+ test_begin(t_strdup_printf("fts tokenizer email address + parent %s", name));
+ test_assert(fts_tokenizer_create(fts_tokenizer_generic, NULL, settings, &gen_tok, &error) == 0);
+ test_assert(fts_tokenizer_create(fts_tokenizer_email_address, gen_tok, NULL, &tok, &error) == 0);
+ test_tokenizer_inputoutput(tok, input, expected_output, 0);
+ fts_tokenizer_unref(&tok);
+ fts_tokenizer_unref(&gen_tok);
+ test_end();
+}
+
+const char *const simple_settings[] = {"algorithm", "simple", NULL};
+static void test_fts_tokenizer_address_parent_simple(void)
+{
+ test_fts_tokenizer_address_parent("simple", simple_settings);
+}
+
+static void test_fts_tokenizer_address_parent_tr29(void)
+{
+ test_fts_tokenizer_address_parent("tr29", tr29_settings);
+}
+
+static void test_fts_tokenizer_address_search(void)
+{
+ static const char input[] = TEST_INPUT_ADDRESS;
+ static const char *const expected_output[] = {
+ "invalid", "invalid", "Abc", "Dfg", "abc.dfg@example.com",
+ "Bar", "Baz", "bar@example.org",
+ "Foo", "Bar", "comment", "foo.bar@host.example.org",
+ "foo", "foo@domain",
+ "abcdefghijklmnopqrstuvxyz", "abcdefghijklmnopqrstuvxyzabcde",
+ "1bcdefghijklmnopqrstuvxy1",
+ "2bcdefghijklmnopqrstuvxy2",
+ "3bcdefghijklmnopqrstuvxy3",
+ "4bcdefghijklmnopqrstuvxy4",
+ "5bcdefghijklmnopqrstuvxy5",
+ "6bcdefghijklmnopqrstuvxy6",
+ "7bcdefghijklmnopqrstuvxy7",
+ "8bcdefghijklmnopqrstuvxy8",
+ "9bcdefghijklmnopqrstuvxy9",
+ "0bcdefghijklmnopqrstuvxy0", "tld",
+ "trailing", "period@blue.com",
+ "multi", "trialing", "mul@trail.com",
+ "m@s",
+ "hypen@hypen-hypen.com",
+ "hypen@hypen-hypen-sick.com",
+ NULL
+ };
+ static const char *const settings[] = { "search", "", NULL };
+ struct fts_tokenizer *tok, *gen_tok;
+ const char *token, *error;
+
+ test_begin("fts tokenizer search email address + parent");
+ test_assert(fts_tokenizer_create(fts_tokenizer_generic, NULL, NULL, &gen_tok, &error) == 0);
+ test_assert(fts_tokenizer_create(fts_tokenizer_email_address, gen_tok, settings, &tok, &error) == 0);
+ test_tokenizer_inputoutput(tok, input, expected_output, 0);
+
+ /* make sure state is forgotten at EOF */
+ test_assert(fts_tokenizer_next(tok, (const void *)"foo", 3, &token, &error) == 0);
+ test_assert(fts_tokenizer_final(tok, &token, &error) > 0 &&
+ strcmp(token, "foo") == 0);
+ test_assert(fts_tokenizer_final(tok, &token, &error) == 0);
+
+ test_assert(fts_tokenizer_next(tok, (const void *)"bar@baz", 7, &token, &error) == 0);
+ test_assert(fts_tokenizer_final(tok, &token, &error) > 0 &&
+ strcmp(token, "bar@baz") == 0);
+ test_assert(fts_tokenizer_final(tok, &token, &error) == 0);
+
+ test_assert(fts_tokenizer_next(tok, (const void *)"foo@", 4, &token, &error) == 0);
+ test_assert(fts_tokenizer_final(tok, &token, &error) > 0 &&
+ strcmp(token, "foo") == 0);
+ test_assert(fts_tokenizer_final(tok, &token, &error) == 0);
+
+ /* test reset explicitly */
+ test_assert(fts_tokenizer_next(tok, (const void *)"a", 1, &token, &error) == 0);
+ fts_tokenizer_reset(tok);
+ test_assert(fts_tokenizer_next(tok, (const void *)"b@c", 3, &token, &error) == 0);
+ test_assert(fts_tokenizer_final(tok, &token, &error) > 0 &&
+ strcmp(token, "b@c") == 0);
+ test_assert(fts_tokenizer_final(tok, &token, &error) == 0);
+
+ fts_tokenizer_unref(&tok);
+ fts_tokenizer_unref(&gen_tok);
+ test_end();
+}
+
+static void test_fts_tokenizer_delete_trailing_partial_char(void)
+{
+ static const struct {
+ const char *str;
+ unsigned int truncated_len;
+ } tests[] = {
+ /* non-truncated */
+ { "\x7f", 1 },
+ { "\xC2\x80", 2 },
+ { "\xE0\x80\x80", 3 },
+ { "\xF0\x80\x80\x80", 4 },
+
+ /* truncated */
+ { "\xF0\x80\x80", 0 },
+ { "x\xF0\x80\x80", 1 },
+ };
+ unsigned int i;
+ size_t size;
+
+ test_begin("fts tokenizer delete trailing partial char");
+ for (i = 0; i < N_ELEMENTS(tests); i++) {
+ size = strlen(tests[i].str);
+ fts_tokenizer_delete_trailing_partial_char((const unsigned char *)tests[i].str, &size);
+ test_assert(size == tests[i].truncated_len);
+ }
+ test_end();
+}
+
+static void test_fts_tokenizer_address_maxlen(void)
+{
+ const char *const settings[] = {"maxlen", "5", NULL};
+ const char *input = "...\357\277\275@a";
+ struct fts_tokenizer *tok;
+ const char *token, *error;
+
+ test_begin("fts tokenizer address maxlen");
+ test_assert(fts_tokenizer_create(fts_tokenizer_email_address, NULL, settings, &tok, &error) == 0);
+
+ while (fts_tokenizer_next(tok, (const unsigned char *)input,
+ strlen(input), &token, &error) > 0) ;
+ while (fts_tokenizer_final(tok, &token, &error) > 0) ;
+ fts_tokenizer_unref(&tok);
+ test_end();
+}
+
+static void test_fts_tokenizer_random(void)
+{
+ const unsigned char test_chars[] = { 0, ' ', '.', 'a', 'b', 'c', '-', '@', '\xC3', '\xA4' };
+ const char *const settings[] = {"algorithm", "simple", NULL};
+ const char *const email_settings[] = {"maxlen", "9", NULL};
+ unsigned int i;
+ unsigned char addr[10] = { 0 };
+ string_t *str = t_str_new(20);
+ struct fts_tokenizer *tok, *gen_tok;
+ const char *token, *error;
+
+ test_begin("fts tokenizer random");
+ test_assert(fts_tokenizer_create(fts_tokenizer_generic, NULL, settings, &gen_tok, &error) == 0);
+ test_assert(fts_tokenizer_create(fts_tokenizer_email_address, gen_tok, email_settings, &tok, &error) == 0);
+
+ for (i = 0; i < 10000; i++) T_BEGIN {
+ for (unsigned int j = 0; j < sizeof(addr); j++)
+ addr[j] = test_chars[i_rand_limit(N_ELEMENTS(test_chars))];
+ str_truncate(str, 0);
+ if (uni_utf8_get_valid_data(addr, sizeof(addr), str))
+ str_append_data(str, addr, sizeof(addr));
+ while (fts_tokenizer_next(tok, str_data(str), str_len(str),
+ &token, &error) > 0) ;
+ while (fts_tokenizer_final(tok, &token, &error) > 0) ;
+ } T_END;
+ fts_tokenizer_unref(&tok);
+ fts_tokenizer_unref(&gen_tok);
+ test_end();
+}
+
+
+static void
+test_fts_tokenizer_explicit_prefix(void)
+{
+ const char *input = "* ** "
+ "*pre *both* post* "
+ "mid*dle *mid*dle* "
+ "**twopre **twoboth** twopost**";
+ const char *const expected_star[] = { "pre", "both*", "post*",
+ "mid*", "dle", "mid*", "dle*",
+ "twopre", "twoboth*", "twopost*",
+ NULL, NULL };
+ const char *const expected_nostar[] = { "pre", "both", "post",
+ "mid", "dle", "mid", "dle",
+ "twopre", "twoboth", "twopost",
+ NULL, NULL };
+
+ const char *settings[9] = { "algorithm", "tr29", "wb5a", "yes" };
+ const char **setptr;
+
+ const char *algos[] = { ALGORITHM_SIMPLE_NAME,
+ ALGORITHM_TR29_NAME,
+ ALGORITHM_TR29_NAME "+wb5a" };
+ const char *searches[] = { "indexing", "searching" };
+ const char *prefixes[] = { "fixed", "prefix" };
+
+ for (int algo = 2; algo >= 0; algo--) { /* We overwrite the settings over time */
+ setptr = &settings[algo*2]; /* 4, 2, or 0 settings strings preserved */
+
+ for (int search = 0; search < 2; search++) {
+ const char **setptr2 = setptr;
+ if (search > 0) { *setptr2++ = "search"; *setptr2++ = "yes"; }
+
+ for (int explicitprefix = 0; explicitprefix < 2; explicitprefix++) {
+ const char **setptr3 = setptr2;
+ if (explicitprefix > 0) { *setptr3++ = "explicitprefix"; *setptr3++ = "y"; }
+
+ *setptr3++ = NULL;
+
+ test_begin(t_strdup_printf("prefix search %s:%s:%s",
+ algos[algo],
+ searches[search],
+ prefixes[explicitprefix]));
+ struct fts_tokenizer *tok;
+ const char *error;
+
+ test_assert(fts_tokenizer_create(fts_tokenizer_generic, NULL, settings,
+ &tok, &error) == 0);
+ test_tokenizer_inputs(
+ tok, &input, 1,
+ (search!=0) && (explicitprefix!=0)
+ ? expected_star : expected_nostar);
+
+ fts_tokenizer_unref(&tok);
+ test_end();
+ }
+ }
+ }
+}
+
+static void test_fts_tokenizer_skip_base64(void)
+{
+ /* The skip_base64 works on the data already available in the buffer
+ of the tokenizer, it does not pull more data to see if a base64
+ sequence long enough would match or not. This is why it does not
+ use test_tokenizer_inputoutput that also tests with one-byte-at-once
+ or random chunking, as those are known to fail with the current
+ implementation */
+ struct fts_tokenizer *tok;
+ const char *error;
+ const char *token;
+
+ static const char *input =
+ ",/dirtyleader/456789012345678901234567890123456789/\r\n"
+
+ " /cleanleader/456789012345678901234567890123456789/\r\n"
+ "\t/cleanleader/456789012345678901234567890123456789/\r\n"
+ "\r/cleanleader/456789012345678901234567890123456789/\r\n"
+ "\n/cleanleader/456789012345678901234567890123456789/\r\n"
+ "=/cleanleader/456789012345678901234567890123456789/\r\n"
+ ";/cleanleader/456789012345678901234567890123456789/\r\n"
+ ":/cleanleader/456789012345678901234567890123456789/\r\n"
+ ";/cleanleader/456789012345678901234567890123456789/\r\n"
+
+ "/23456789012345678901234567890123456/dirtytrailer/,\r\n"
+
+ "/23456789012345678901234567890123456/cleantrailer/ \r\n"
+ "/23456789012345678901234567890123456/cleantrailer/\t\r\n"
+ "/23456789012345678901234567890123456/cleantrailer/\r\r\n"
+ "/23456789012345678901234567890123456/cleantrailer/\n\r\n"
+ "/23456789012345678901234567890123456/cleantrailer/=\r\n"
+ "/23456789012345678901234567890123456/cleantrailer/;\r\n"
+ "/23456789012345678901234567890123456/cleantrailer/:\r\n"
+ "/23456789012345678901234567890123456/cleantrailer/?\r\n"
+
+ "J1RrDrZSWxIAphKpYckeKNs10iTeiGMY0hNI32SMoSqCTgH96\r\n" // 49
+ "MziUaLMK6FAOQws3OIuX0tgvQcyhu06ILAWWB1nGPy/bSEAEYg\r\n" // 50
+ "ljWSJo8kxsm4/CiZBpwFfWkd64y+5ZytnKqgkQD87UbQ7FcpZgj\r\n" // 51
+ "pTXUOBszCfdAgfZpWpPiOEQSthPxN9XMaS7HnOTyXtRBPVt96vw=\r\n" // 51=
+ "MJmsWlDKXo7NCSt1wvazf9Xad18qOzpLJkVs/sxKsvLYyPD/zv=\r\n" // 50=
+ "CBLsZ5dUybAEWcDkQwytSL348U/2lvadma7lF4wdNOc8sjUL8=\r\n" // 49=
+
+ "4HWw7lJ15ZW3G1GtH9/NQbylcThN2IJo1kr83Fa2c9z2GFK1/NF+DpAkjbhDA3Al\r\n"
+
+ "alpha bravo charlie delta echo foxtrot golf hotel india\r\n"
+ "=juliet=kilo=lima=mike=november=oscar=papa=qebec=romeo=\r\n";
+
+ static const char *const expected_output[] = {
+ "dirtyleader", "456789012345678901234567890123",
+ "234567890123456789012345678901", "dirtytrailer",
+ "J1RrDrZSWxIAphKpYckeKNs10iTeiG", // 49
+ "CBLsZ5dUybAEWcDkQwytSL348U", "2lvadma7lF4wdNOc8sjUL8", // 49=
+ "alpha", "bravo", "charlie", "delta", "echo", "foxtrot", "golf", "hotel", "india",
+ "juliet", "kilo", "lima", "mike", "november", "oscar", "papa", "qebec", "romeo",
+ NULL
+ };
+
+ test_begin("fts tokenizer skip base64");
+ test_assert(fts_tokenizer_create(fts_tokenizer_generic, NULL, tr29_settings, &tok, &error) == 0);
+
+ size_t index = 0;
+ while (fts_tokenizer_next(tok, (const unsigned char *) input, strlen(input), &token, &error) > 0) {
+ i_assert(index < N_ELEMENTS(expected_output));
+ test_assert_strcmp(token, expected_output[index]);
+ ++index;
+ }
+ while (fts_tokenizer_next(tok, NULL, 0, &token, &error) > 0) {
+ i_assert(index < N_ELEMENTS(expected_output));
+ test_assert_strcmp(token, expected_output[index]);
+ ++index;
+ }
+ i_assert(index < N_ELEMENTS(expected_output));
+ test_assert_idx(expected_output[index] == NULL, index);
+
+ fts_tokenizer_unref(&tok);
+ test_end();
+}
+
+int main(void)
+{
+ static void (*const test_functions[])(void) = {
+ test_fts_tokenizer_skip_base64,
+ test_fts_tokenizer_find,
+ test_fts_tokenizer_generic_only,
+ test_fts_tokenizer_generic_tr29_only,
+ test_fts_tokenizer_generic_tr29_wb5a,
+ test_fts_tokenizer_address_only,
+ test_fts_tokenizer_address_parent_simple,
+ test_fts_tokenizer_address_parent_tr29,
+ test_fts_tokenizer_address_maxlen,
+ test_fts_tokenizer_address_search,
+ test_fts_tokenizer_delete_trailing_partial_char,
+ test_fts_tokenizer_random,
+ test_fts_tokenizer_explicit_prefix,
+ NULL
+ };
+ int ret;
+
+ fts_tokenizers_init();
+ ret = test_run(test_functions);
+ fts_tokenizers_deinit();
+
+ return ret;
+}
diff --git a/src/lib-fts/udhr_fra.txt b/src/lib-fts/udhr_fra.txt
new file mode 100644
index 0000000..5291403
--- /dev/null
+++ b/src/lib-fts/udhr_fra.txt
@@ -0,0 +1,217 @@
+Universal Declaration of Human Rights - French
+© 1996 – 2009 The Office of the High Commissioner for Human Rights
+This plain text version prepared by the “UDHR in Unicode”
+project, http://www.unicode.org/udhr.
+---
+
+Déclaration universelle des droits de l’homme
+ Préambule
+ Considérant que la reconnaissance de la dignité inhérente à tous les membres de la famille humaine et de leurs droits égaux et inaliénables constitue le fondement de la liberté, de la justice et de la paix dans le monde,
+ Considérant que la méconnaissance et le mépris des droits de l’homme ont conduit à des actes de barbarie qui révoltent la conscience de l’humanité et que l’avènement d’un monde où les êtres humains seront libres de parler et de croire, libérés de la terreur et de la misère, a été proclamé comme la plus haute aspiration de l’homme,
+ Considérant qu’il est essentiel que les droits de l’homme soient protégés par un régime de droit pour que l’homme ne soit pas contraint, en suprême recours, à la révolte contre la tyrannie et l’oppression,
+ Considérant qu’il est essentiel d’encourager le développement de relations amicales entre nations,
+ Considérant que dans la Charte les peuples des Nations Unies ont proclamé à nouveau leur foi dans les droits fondamentaux de l’homme, dans la dignité et la valeur de la personne humaine, dans l’égalité des droits des hommes et des femmes, et qu’ils se sont déclarés résolus à favoriser le progrès social et à instaurer de meilleures conditions de vie dans une liberté plus grande,
+ Considérant que les États Membres se sont engagés à assurer, en coopération avec l’Organisation des Nations Unies, le respect universel et effectif des droits de l’homme et des libertés fondamentales,
+ Considérant qu’une conception commune de ces droits et libertés est de la plus haute importance pour remplir pleinement cet engagement,
+ L’Assemblée générale
+ Proclame la présente Déclaration universelle des droits de l’homme comme l’idéal commun à atteindre par tous les peuples et toutes les nations afin que tous les individus et tous les organes de la société, ayant cette Déclaration constamment à l’esprit, s’efforcent, par l’enseignement et l’éducation, de développer le respect de ces droits et libertés et d’en assurer, par des mesures progressives d’ordre national et international, la reconnaissance et l’application universelles et effectives, tant parmi les populations des États Membres eux‐mêmes que parmi celles des territoires placés sous leur juridiction.
+
+ Article premier
+ Tous les êtres humains naissent libres et égaux en dignité et en droits. Ils sont doués de raison et de conscience et doivent agir les uns envers les autres dans un esprit de fraternité.
+
+ Article 2
+ Chacun peut se prévaloir de tous les droits et de toutes les libertés proclamés dans la présente Déclaration, sans distinction aucune, notamment de race, de couleur, de sexe, de langue, de religion, d’opinion politique ou de toute autre opinion, d’origine nationale ou sociale, de fortune, de naissance ou de toute autre situation.
+ De plus, il ne sera fait aucune distinction fondée sur le statut politique, juridique ou international du pays ou du territoire dont une personne est ressortissante, que ce pays ou territoire soit indépendant, sous tutelle, non autonome ou soumis à une limitation quelconque de souveraineté.
+
+ Article 3
+ Tout individu a droit à la vie, à la liberté et à la sûreté de sa personne.
+
+ Article 4
+ Nul ne sera tenu en esclavage ni en servitude ; l’esclavage et la traite des esclaves sont interdits sous toutes leurs formes.
+
+ Article 5
+ Nul ne sera soumis à la torture, ni à des peines ou traitements cruels, inhumains ou dégradants.
+
+ Article 6
+ Chacun a le droit à la reconnaissance en tous lieux de sa personnalité juridique.
+
+ Article 7
+ Tous sont égaux devant la loi et ont droit sans distinction à une égale protection de la loi. Tous ont droit à une protection égale contre toute discrimination qui violerait la présente Déclaration et contre toute provocation à une telle discrimination.
+
+ Article 8
+ Toute personne a droit à un recours effectif devant les juridictions nationales compétentes contre les actes violant les droits fondamentaux qui lui sont reconnus par la constitution ou par la loi.
+
+ Article 9
+ Nul ne peut être arbitrairement arrêté, détenu ni exilé.
+
+ Article 10
+ Toute personne a droit, en pleine égalité, à ce que sa cause soit entendue équitablement et publiquement par un tribunal indépendant et impartial, qui décidera, soit de ses droits et obligations, soit du bien fondé de toute accusation en matière pénale dirigée contre elle.
+
+ Article 11
+
+
+ Toute personne accusée d’un acte délictueux est présumée innocente jusqu’à ce que sa culpabilité ait été légalement établie au cours d’un procès public où toutes les garanties nécessaires à sa défense lui auront été assurées.
+
+
+ Nul ne sera condamné pour des actions ou omissions qui, au moment où elles ont été commises, ne constituaient pas un acte délictueux d’après le droit national ou international. De même, il ne sera infligé aucune peine plus forte que celle qui était applicable au moment où l’acte délictueux a été commis.
+
+
+
+ Article 12
+ Nul ne sera l’objet d’immixtions arbitraires dans sa vie privée, sa famille, son domicile ou sa correspondance, ni d’atteintes à son honneur et à sa réputation. Toute personne a droit à la protection de la loi contre de telles immixtions ou de telles atteintes.
+
+ Article 13
+
+
+ Toute personne a le droit de circuler librement et de choisir sa résidence à l’intérieur d’un État.
+
+
+ Toute personne a le droit de quitter tout pays, y compris le sien, et de revenir dans son pays.
+
+
+
+ Article 14
+
+
+ Devant la persécution, toute personne a le droit de chercher asile et de bénéficier de l’asile en d’autres pays.
+
+
+ Ce droit ne peut être invoqué dans le cas de poursuites réellement fondées sur un crime de droit commun ou sur des agissements contraires aux buts et aux principes des Nations Unies.
+
+
+
+ Article 15
+
+
+ Tout individu a droit à une nationalité.
+
+
+ Nul ne peut être arbitrairement privé de sa nationalité, ni du droit de changer de nationalité.
+
+
+
+ Article 16
+
+
+ A partir de l’âge nubile, l’homme et la femme, sans aucune restriction quant à la race, la nationalité ou la religion, ont le droit de se marier et de fonder une famille. Ils ont des droits égaux au regard du mariage, durant le mariage et lors de sa dissolution.
+
+
+ Le mariage ne peut être conclu qu’avec le libre et plein consentement des futurs époux.
+
+
+ La famille est l’élément naturel et fondamental de la société et a droit à la protection de la société et de l’État.
+
+
+
+ Article 17
+
+
+ Toute personne, aussi bien seule qu’en collectivité, a droit à la propriété.
+
+
+ Nul ne peut être arbitrairement privé de sa propriété.
+
+
+
+ Article 18
+ Toute personne a droit à la liberté de pensée, de conscience et de religion ; ce droit implique la liberté de changer de religion ou de conviction ainsi que la liberté de manifester sa religion ou sa conviction, seule ou en commun, tant en public qu’en privé, par l’enseignement, les pratiques, le culte et l’accomplissement des rites.
+
+ Article 19
+ Tout individu a droit à la liberté d’opinion et d’expression, ce qui implique le droit de ne pas être inquiété pour ses opinions et celui de chercher, de recevoir et de répandre, sans considérations de frontières, les informations et les idées par quelque moyen d’expression que ce soit.
+
+ Article 20
+
+
+ Toute personne a droit à la liberté de réunion et d’association pacifiques.
+
+
+ Nul ne peut être obligé de faire partie d’une association.
+
+
+
+ Article 21
+
+
+ Toute personne a le droit de prendre part à la direction des affaires publiques de son pays, soit directement, soit par l’intermédiaire de représentants librement choisis.
+
+
+ Toute personne a droit à accéder, dans des conditions d’égalité, aux fonctions publiques de son pays.
+
+
+ La volonté du peuple est le fondement de l’autorité des pouvoirs publics ; cette volonté doit s’exprimer par des élections honnêtes qui doivent avoir lieu périodiquement, au suffrage universel égal et au vote secret ou suivant une procédure équivalente assurant la liberté du vote.
+
+
+
+ Article 22
+ Toute personne, en tant que membre de la société, a droit à la sécurité sociale ; elle est fondée à obtenir la satisfaction des droits économiques, sociaux et culturels indispensables à sa dignité et au libre développement de sa personnalité, grâce à l’effort national et à la coopération internationale, compte tenu de l’organisation et des ressources de chaque pays.
+
+ Article 23
+
+
+ Toute personne a droit au travail, au libre choix de son travail, à des conditions équitables et satisfaisantes de travail et à la protection contre le chômage.
+
+
+ Tous ont droit, sans aucune discrimination, à un salaire égal pour un travail égal.
+
+
+ Quiconque travaille a droit à une rémunération équitable et satisfaisante lui assurant ainsi qu’à sa famille une existence conforme à la dignité humaine et complétée, s’il y a lieu, par tous autres moyens de protection sociale.
+
+
+ Toute personne a le droit de fonder avec d’autres des syndicats et de s’affilier à des syndicats pour la défense de ses intérêts.
+
+
+
+ Article 24
+ Toute personne a droit au repos et aux loisirs et notamment à une limitation raisonnable de la durée du travail et à des congés payés périodiques.
+
+ Article 25
+
+
+ Toute personne a droit à un niveau de vie suffisant pour assurer sa santé, son bien‐être et ceux de sa famille, notamment pour l’alimentation, l’habillement, le logement, les soins médicaux ainsi que pour les services sociaux nécessaires ; elle a droit à la sécurité en cas de chômage, de maladie, d’invalidité, de veuvage, de vieillesse ou dans les autres cas de perte de ses moyens de subsistance par suite de circonstances indépendantes de sa volonté.
+
+
+ La maternité et l’enfance ont droit à une aide et à une assistance spéciales. Tous les enfants, qu’ils soient nés dans le mariage ou hors mariage, jouissent de la même protection sociale.
+
+
+
+ Article 26
+
+
+ Toute personne a droit à l’éducation. L’éducation doit être gratuite, au moins en ce qui concerne l’enseignement élémentaire et fondamental. L’enseignement élémentaire est obligatoire. L’enseignement technique et professionnel doit être généralisé ; l’accès aux études supérieures doit être ouvert en pleine égalité à tous en fonction de leur mérite.
+
+
+ L’éducation doit viser au plein épanouissement de la personnalité humaine et au renforcement du respect des droits de l’homme et des libertés fondamentales. Elle doit favoriser la compréhension, la tolérance et l’amitié entre toutes les nations et tous les groupes raciaux ou religieux, ainsi que le développement des activités des Nations Unies pour le maintien de la paix.
+
+
+ Les parents ont, par priorité, le droit de choisir le genre d’éducation à donner à leurs enfants.
+
+
+
+ Article 27
+
+
+ Toute personne a le droit de prendre part librement à la vie culturelle de la communauté, de jouir des arts et de participer au progrès scientifique et aux bienfaits qui en résultent.
+
+
+ Chacun a droit à la protection des intérêts moraux et matériels découlant de toute production scientifique, littéraire ou artistique dont il est l’auteur.
+
+
+
+ Article 28
+ Toute personne a droit à ce que règne, sur le plan social et sur le plan international, un ordre tel que les droits et libertés énoncés dans la présente Déclaration puissent y trouver plein effet.
+
+ Article 29
+
+
+ L’individu a des devoirs envers la communauté dans laquelle seul le libre et plein développement de sa personnalité est possible.
+
+
+ Dans l’exercice de ses droits et dans la jouissance de ses libertés, chacun n’est soumis qu’aux limitations établies par la loi exclusivement en vue d’assurer la reconnaissance et le respect des droits et libertés d’autrui et afin de satisfaire aux justes exigences de la morale, de l’ordre public et du bien‐être général dans une société démocratique.
+
+
+ Ces droits et libertés ne pourront, en aucun cas, s’exercer contrairement aux buts et aux principes des Nations Unies.
+
+
+
+ Article 30
+ Aucune disposition de la présente Déclaration ne peut être interprétée comme impliquant pour un État, un groupement ou un individu un droit quelconque de se livrer à une activité ou d’accomplir un acte visant à la destruction des droits et libertés qui y sont énoncés.
diff --git a/src/lib-fts/word-boundary-data.c b/src/lib-fts/word-boundary-data.c
new file mode 100644
index 0000000..9616572
--- /dev/null
+++ b/src/lib-fts/word-boundary-data.c
@@ -0,0 +1,3951 @@
+/* This file is automatically generated by word-properties.pl from ./WordBreakProperty.txt */
+static const uint32_t CR[]= {
+ 0x0000D
+};
+static const uint32_t LF[]= {
+ 0x0000A
+};
+static const uint32_t Newline[]= {
+ 0x0000B, 0x0000C, 0x00085, 0x02028, 0x02029
+};
+static const uint32_t Extend[]= {
+ 0x00300, 0x00301, 0x00302, 0x00303, 0x00304, 0x00305, 0x00306, 0x00307,
+ 0x00308, 0x00309, 0x0030A, 0x0030B, 0x0030C, 0x0030D, 0x0030E, 0x0030F,
+ 0x00310, 0x00311, 0x00312, 0x00313, 0x00314, 0x00315, 0x00316, 0x00317,
+ 0x00318, 0x00319, 0x0031A, 0x0031B, 0x0031C, 0x0031D, 0x0031E, 0x0031F,
+ 0x00320, 0x00321, 0x00322, 0x00323, 0x00324, 0x00325, 0x00326, 0x00327,
+ 0x00328, 0x00329, 0x0032A, 0x0032B, 0x0032C, 0x0032D, 0x0032E, 0x0032F,
+ 0x00330, 0x00331, 0x00332, 0x00333, 0x00334, 0x00335, 0x00336, 0x00337,
+ 0x00338, 0x00339, 0x0033A, 0x0033B, 0x0033C, 0x0033D, 0x0033E, 0x0033F,
+ 0x00340, 0x00341, 0x00342, 0x00343, 0x00344, 0x00345, 0x00346, 0x00347,
+ 0x00348, 0x00349, 0x0034A, 0x0034B, 0x0034C, 0x0034D, 0x0034E, 0x0034F,
+ 0x00350, 0x00351, 0x00352, 0x00353, 0x00354, 0x00355, 0x00356, 0x00357,
+ 0x00358, 0x00359, 0x0035A, 0x0035B, 0x0035C, 0x0035D, 0x0035E, 0x0035F,
+ 0x00360, 0x00361, 0x00362, 0x00363, 0x00364, 0x00365, 0x00366, 0x00367,
+ 0x00368, 0x00369, 0x0036A, 0x0036B, 0x0036C, 0x0036D, 0x0036E, 0x0036F,
+ 0x00483, 0x00484, 0x00485, 0x00486, 0x00487, 0x00488, 0x00489, 0x00591,
+ 0x00592, 0x00593, 0x00594, 0x00595, 0x00596, 0x00597, 0x00598, 0x00599,
+ 0x0059A, 0x0059B, 0x0059C, 0x0059D, 0x0059E, 0x0059F, 0x005A0, 0x005A1,
+ 0x005A2, 0x005A3, 0x005A4, 0x005A5, 0x005A6, 0x005A7, 0x005A8, 0x005A9,
+ 0x005AA, 0x005AB, 0x005AC, 0x005AD, 0x005AE, 0x005AF, 0x005B0, 0x005B1,
+ 0x005B2, 0x005B3, 0x005B4, 0x005B5, 0x005B6, 0x005B7, 0x005B8, 0x005B9,
+ 0x005BA, 0x005BB, 0x005BC, 0x005BD, 0x005BF, 0x005C1, 0x005C2, 0x005C4,
+ 0x005C5, 0x005C7, 0x00610, 0x00611, 0x00612, 0x00613, 0x00614, 0x00615,
+ 0x00616, 0x00617, 0x00618, 0x00619, 0x0061A, 0x0064B, 0x0064C, 0x0064D,
+ 0x0064E, 0x0064F, 0x00650, 0x00651, 0x00652, 0x00653, 0x00654, 0x00655,
+ 0x00656, 0x00657, 0x00658, 0x00659, 0x0065A, 0x0065B, 0x0065C, 0x0065D,
+ 0x0065E, 0x0065F, 0x00670, 0x006D6, 0x006D7, 0x006D8, 0x006D9, 0x006DA,
+ 0x006DB, 0x006DC, 0x006DF, 0x006E0, 0x006E1, 0x006E2, 0x006E3, 0x006E4,
+ 0x006E7, 0x006E8, 0x006EA, 0x006EB, 0x006EC, 0x006ED, 0x00711, 0x00730,
+ 0x00731, 0x00732, 0x00733, 0x00734, 0x00735, 0x00736, 0x00737, 0x00738,
+ 0x00739, 0x0073A, 0x0073B, 0x0073C, 0x0073D, 0x0073E, 0x0073F, 0x00740,
+ 0x00741, 0x00742, 0x00743, 0x00744, 0x00745, 0x00746, 0x00747, 0x00748,
+ 0x00749, 0x0074A, 0x007A6, 0x007A7, 0x007A8, 0x007A9, 0x007AA, 0x007AB,
+ 0x007AC, 0x007AD, 0x007AE, 0x007AF, 0x007B0, 0x007EB, 0x007EC, 0x007ED,
+ 0x007EE, 0x007EF, 0x007F0, 0x007F1, 0x007F2, 0x007F3, 0x00816, 0x00817,
+ 0x00818, 0x00819, 0x0081B, 0x0081C, 0x0081D, 0x0081E, 0x0081F, 0x00820,
+ 0x00821, 0x00822, 0x00823, 0x00825, 0x00826, 0x00827, 0x00829, 0x0082A,
+ 0x0082B, 0x0082C, 0x0082D, 0x00859, 0x0085A, 0x0085B, 0x008D4, 0x008D5,
+ 0x008D6, 0x008D7, 0x008D8, 0x008D9, 0x008DA, 0x008DB, 0x008DC, 0x008DD,
+ 0x008DE, 0x008DF, 0x008E0, 0x008E1, 0x008E3, 0x008E4, 0x008E5, 0x008E6,
+ 0x008E7, 0x008E8, 0x008E9, 0x008EA, 0x008EB, 0x008EC, 0x008ED, 0x008EE,
+ 0x008EF, 0x008F0, 0x008F1, 0x008F2, 0x008F3, 0x008F4, 0x008F5, 0x008F6,
+ 0x008F7, 0x008F8, 0x008F9, 0x008FA, 0x008FB, 0x008FC, 0x008FD, 0x008FE,
+ 0x008FF, 0x00900, 0x00901, 0x00902, 0x00903, 0x0093A, 0x0093B, 0x0093C,
+ 0x0093E, 0x0093F, 0x00940, 0x00941, 0x00942, 0x00943, 0x00944, 0x00945,
+ 0x00946, 0x00947, 0x00948, 0x00949, 0x0094A, 0x0094B, 0x0094C, 0x0094D,
+ 0x0094E, 0x0094F, 0x00951, 0x00952, 0x00953, 0x00954, 0x00955, 0x00956,
+ 0x00957, 0x00962, 0x00963, 0x00981, 0x00982, 0x00983, 0x009BC, 0x009BE,
+ 0x009BF, 0x009C0, 0x009C1, 0x009C2, 0x009C3, 0x009C4, 0x009C7, 0x009C8,
+ 0x009CB, 0x009CC, 0x009CD, 0x009D7, 0x009E2, 0x009E3, 0x00A01, 0x00A02,
+ 0x00A03, 0x00A3C, 0x00A3E, 0x00A3F, 0x00A40, 0x00A41, 0x00A42, 0x00A47,
+ 0x00A48, 0x00A4B, 0x00A4C, 0x00A4D, 0x00A51, 0x00A70, 0x00A71, 0x00A75,
+ 0x00A81, 0x00A82, 0x00A83, 0x00ABC, 0x00ABE, 0x00ABF, 0x00AC0, 0x00AC1,
+ 0x00AC2, 0x00AC3, 0x00AC4, 0x00AC5, 0x00AC7, 0x00AC8, 0x00AC9, 0x00ACB,
+ 0x00ACC, 0x00ACD, 0x00AE2, 0x00AE3, 0x00B01, 0x00B02, 0x00B03, 0x00B3C,
+ 0x00B3E, 0x00B3F, 0x00B40, 0x00B41, 0x00B42, 0x00B43, 0x00B44, 0x00B47,
+ 0x00B48, 0x00B4B, 0x00B4C, 0x00B4D, 0x00B56, 0x00B57, 0x00B62, 0x00B63,
+ 0x00B82, 0x00BBE, 0x00BBF, 0x00BC0, 0x00BC1, 0x00BC2, 0x00BC6, 0x00BC7,
+ 0x00BC8, 0x00BCA, 0x00BCB, 0x00BCC, 0x00BCD, 0x00BD7, 0x00C00, 0x00C01,
+ 0x00C02, 0x00C03, 0x00C3E, 0x00C3F, 0x00C40, 0x00C41, 0x00C42, 0x00C43,
+ 0x00C44, 0x00C46, 0x00C47, 0x00C48, 0x00C4A, 0x00C4B, 0x00C4C, 0x00C4D,
+ 0x00C55, 0x00C56, 0x00C62, 0x00C63, 0x00C81, 0x00C82, 0x00C83, 0x00CBC,
+ 0x00CBE, 0x00CBF, 0x00CC0, 0x00CC1, 0x00CC2, 0x00CC3, 0x00CC4, 0x00CC6,
+ 0x00CC7, 0x00CC8, 0x00CCA, 0x00CCB, 0x00CCC, 0x00CCD, 0x00CD5, 0x00CD6,
+ 0x00CE2, 0x00CE3, 0x00D01, 0x00D02, 0x00D03, 0x00D3E, 0x00D3F, 0x00D40,
+ 0x00D41, 0x00D42, 0x00D43, 0x00D44, 0x00D46, 0x00D47, 0x00D48, 0x00D4A,
+ 0x00D4B, 0x00D4C, 0x00D4D, 0x00D57, 0x00D62, 0x00D63, 0x00D82, 0x00D83,
+ 0x00DCA, 0x00DCF, 0x00DD0, 0x00DD1, 0x00DD2, 0x00DD3, 0x00DD4, 0x00DD6,
+ 0x00DD8, 0x00DD9, 0x00DDA, 0x00DDB, 0x00DDC, 0x00DDD, 0x00DDE, 0x00DDF,
+ 0x00DF2, 0x00DF3, 0x00E31, 0x00E34, 0x00E35, 0x00E36, 0x00E37, 0x00E38,
+ 0x00E39, 0x00E3A, 0x00E47, 0x00E48, 0x00E49, 0x00E4A, 0x00E4B, 0x00E4C,
+ 0x00E4D, 0x00E4E, 0x00EB1, 0x00EB4, 0x00EB5, 0x00EB6, 0x00EB7, 0x00EB8,
+ 0x00EB9, 0x00EBB, 0x00EBC, 0x00EC8, 0x00EC9, 0x00ECA, 0x00ECB, 0x00ECC,
+ 0x00ECD, 0x00F18, 0x00F19, 0x00F35, 0x00F37, 0x00F39, 0x00F3E, 0x00F3F,
+ 0x00F71, 0x00F72, 0x00F73, 0x00F74, 0x00F75, 0x00F76, 0x00F77, 0x00F78,
+ 0x00F79, 0x00F7A, 0x00F7B, 0x00F7C, 0x00F7D, 0x00F7E, 0x00F7F, 0x00F80,
+ 0x00F81, 0x00F82, 0x00F83, 0x00F84, 0x00F86, 0x00F87, 0x00F8D, 0x00F8E,
+ 0x00F8F, 0x00F90, 0x00F91, 0x00F92, 0x00F93, 0x00F94, 0x00F95, 0x00F96,
+ 0x00F97, 0x00F99, 0x00F9A, 0x00F9B, 0x00F9C, 0x00F9D, 0x00F9E, 0x00F9F,
+ 0x00FA0, 0x00FA1, 0x00FA2, 0x00FA3, 0x00FA4, 0x00FA5, 0x00FA6, 0x00FA7,
+ 0x00FA8, 0x00FA9, 0x00FAA, 0x00FAB, 0x00FAC, 0x00FAD, 0x00FAE, 0x00FAF,
+ 0x00FB0, 0x00FB1, 0x00FB2, 0x00FB3, 0x00FB4, 0x00FB5, 0x00FB6, 0x00FB7,
+ 0x00FB8, 0x00FB9, 0x00FBA, 0x00FBB, 0x00FBC, 0x00FC6, 0x0102B, 0x0102C,
+ 0x0102D, 0x0102E, 0x0102F, 0x01030, 0x01031, 0x01032, 0x01033, 0x01034,
+ 0x01035, 0x01036, 0x01037, 0x01038, 0x01039, 0x0103A, 0x0103B, 0x0103C,
+ 0x0103D, 0x0103E, 0x01056, 0x01057, 0x01058, 0x01059, 0x0105E, 0x0105F,
+ 0x01060, 0x01062, 0x01063, 0x01064, 0x01067, 0x01068, 0x01069, 0x0106A,
+ 0x0106B, 0x0106C, 0x0106D, 0x01071, 0x01072, 0x01073, 0x01074, 0x01082,
+ 0x01083, 0x01084, 0x01085, 0x01086, 0x01087, 0x01088, 0x01089, 0x0108A,
+ 0x0108B, 0x0108C, 0x0108D, 0x0108F, 0x0109A, 0x0109B, 0x0109C, 0x0109D,
+ 0x0135D, 0x0135E, 0x0135F, 0x01712, 0x01713, 0x01714, 0x01732, 0x01733,
+ 0x01734, 0x01752, 0x01753, 0x01772, 0x01773, 0x017B4, 0x017B5, 0x017B6,
+ 0x017B7, 0x017B8, 0x017B9, 0x017BA, 0x017BB, 0x017BC, 0x017BD, 0x017BE,
+ 0x017BF, 0x017C0, 0x017C1, 0x017C2, 0x017C3, 0x017C4, 0x017C5, 0x017C6,
+ 0x017C7, 0x017C8, 0x017C9, 0x017CA, 0x017CB, 0x017CC, 0x017CD, 0x017CE,
+ 0x017CF, 0x017D0, 0x017D1, 0x017D2, 0x017D3, 0x017DD, 0x0180B, 0x0180C,
+ 0x0180D, 0x01885, 0x01886, 0x018A9, 0x01920, 0x01921, 0x01922, 0x01923,
+ 0x01924, 0x01925, 0x01926, 0x01927, 0x01928, 0x01929, 0x0192A, 0x0192B,
+ 0x01930, 0x01931, 0x01932, 0x01933, 0x01934, 0x01935, 0x01936, 0x01937,
+ 0x01938, 0x01939, 0x0193A, 0x0193B, 0x01A17, 0x01A18, 0x01A19, 0x01A1A,
+ 0x01A1B, 0x01A55, 0x01A56, 0x01A57, 0x01A58, 0x01A59, 0x01A5A, 0x01A5B,
+ 0x01A5C, 0x01A5D, 0x01A5E, 0x01A60, 0x01A61, 0x01A62, 0x01A63, 0x01A64,
+ 0x01A65, 0x01A66, 0x01A67, 0x01A68, 0x01A69, 0x01A6A, 0x01A6B, 0x01A6C,
+ 0x01A6D, 0x01A6E, 0x01A6F, 0x01A70, 0x01A71, 0x01A72, 0x01A73, 0x01A74,
+ 0x01A75, 0x01A76, 0x01A77, 0x01A78, 0x01A79, 0x01A7A, 0x01A7B, 0x01A7C,
+ 0x01A7F, 0x01AB0, 0x01AB1, 0x01AB2, 0x01AB3, 0x01AB4, 0x01AB5, 0x01AB6,
+ 0x01AB7, 0x01AB8, 0x01AB9, 0x01ABA, 0x01ABB, 0x01ABC, 0x01ABD, 0x01ABE,
+ 0x01B00, 0x01B01, 0x01B02, 0x01B03, 0x01B04, 0x01B34, 0x01B35, 0x01B36,
+ 0x01B37, 0x01B38, 0x01B39, 0x01B3A, 0x01B3B, 0x01B3C, 0x01B3D, 0x01B3E,
+ 0x01B3F, 0x01B40, 0x01B41, 0x01B42, 0x01B43, 0x01B44, 0x01B6B, 0x01B6C,
+ 0x01B6D, 0x01B6E, 0x01B6F, 0x01B70, 0x01B71, 0x01B72, 0x01B73, 0x01B80,
+ 0x01B81, 0x01B82, 0x01BA1, 0x01BA2, 0x01BA3, 0x01BA4, 0x01BA5, 0x01BA6,
+ 0x01BA7, 0x01BA8, 0x01BA9, 0x01BAA, 0x01BAB, 0x01BAC, 0x01BAD, 0x01BE6,
+ 0x01BE7, 0x01BE8, 0x01BE9, 0x01BEA, 0x01BEB, 0x01BEC, 0x01BED, 0x01BEE,
+ 0x01BEF, 0x01BF0, 0x01BF1, 0x01BF2, 0x01BF3, 0x01C24, 0x01C25, 0x01C26,
+ 0x01C27, 0x01C28, 0x01C29, 0x01C2A, 0x01C2B, 0x01C2C, 0x01C2D, 0x01C2E,
+ 0x01C2F, 0x01C30, 0x01C31, 0x01C32, 0x01C33, 0x01C34, 0x01C35, 0x01C36,
+ 0x01C37, 0x01CD0, 0x01CD1, 0x01CD2, 0x01CD4, 0x01CD5, 0x01CD6, 0x01CD7,
+ 0x01CD8, 0x01CD9, 0x01CDA, 0x01CDB, 0x01CDC, 0x01CDD, 0x01CDE, 0x01CDF,
+ 0x01CE0, 0x01CE1, 0x01CE2, 0x01CE3, 0x01CE4, 0x01CE5, 0x01CE6, 0x01CE7,
+ 0x01CE8, 0x01CED, 0x01CF2, 0x01CF3, 0x01CF4, 0x01CF8, 0x01CF9, 0x01DC0,
+ 0x01DC1, 0x01DC2, 0x01DC3, 0x01DC4, 0x01DC5, 0x01DC6, 0x01DC7, 0x01DC8,
+ 0x01DC9, 0x01DCA, 0x01DCB, 0x01DCC, 0x01DCD, 0x01DCE, 0x01DCF, 0x01DD0,
+ 0x01DD1, 0x01DD2, 0x01DD3, 0x01DD4, 0x01DD5, 0x01DD6, 0x01DD7, 0x01DD8,
+ 0x01DD9, 0x01DDA, 0x01DDB, 0x01DDC, 0x01DDD, 0x01DDE, 0x01DDF, 0x01DE0,
+ 0x01DE1, 0x01DE2, 0x01DE3, 0x01DE4, 0x01DE5, 0x01DE6, 0x01DE7, 0x01DE8,
+ 0x01DE9, 0x01DEA, 0x01DEB, 0x01DEC, 0x01DED, 0x01DEE, 0x01DEF, 0x01DF0,
+ 0x01DF1, 0x01DF2, 0x01DF3, 0x01DF4, 0x01DF5, 0x01DFB, 0x01DFC, 0x01DFD,
+ 0x01DFE, 0x01DFF, 0x0200C, 0x020D0, 0x020D1, 0x020D2, 0x020D3, 0x020D4,
+ 0x020D5, 0x020D6, 0x020D7, 0x020D8, 0x020D9, 0x020DA, 0x020DB, 0x020DC,
+ 0x020DD, 0x020DE, 0x020DF, 0x020E0, 0x020E1, 0x020E2, 0x020E3, 0x020E4,
+ 0x020E5, 0x020E6, 0x020E7, 0x020E8, 0x020E9, 0x020EA, 0x020EB, 0x020EC,
+ 0x020ED, 0x020EE, 0x020EF, 0x020F0, 0x02CEF, 0x02CF0, 0x02CF1, 0x02D7F,
+ 0x02DE0, 0x02DE1, 0x02DE2, 0x02DE3, 0x02DE4, 0x02DE5, 0x02DE6, 0x02DE7,
+ 0x02DE8, 0x02DE9, 0x02DEA, 0x02DEB, 0x02DEC, 0x02DED, 0x02DEE, 0x02DEF,
+ 0x02DF0, 0x02DF1, 0x02DF2, 0x02DF3, 0x02DF4, 0x02DF5, 0x02DF6, 0x02DF7,
+ 0x02DF8, 0x02DF9, 0x02DFA, 0x02DFB, 0x02DFC, 0x02DFD, 0x02DFE, 0x02DFF,
+ 0x0302A, 0x0302B, 0x0302C, 0x0302D, 0x0302E, 0x0302F, 0x03099, 0x0309A,
+ 0x0A66F, 0x0A670, 0x0A671, 0x0A672, 0x0A674, 0x0A675, 0x0A676, 0x0A677,
+ 0x0A678, 0x0A679, 0x0A67A, 0x0A67B, 0x0A67C, 0x0A67D, 0x0A69E, 0x0A69F,
+ 0x0A6F0, 0x0A6F1, 0x0A802, 0x0A806, 0x0A80B, 0x0A823, 0x0A824, 0x0A825,
+ 0x0A826, 0x0A827, 0x0A880, 0x0A881, 0x0A8B4, 0x0A8B5, 0x0A8B6, 0x0A8B7,
+ 0x0A8B8, 0x0A8B9, 0x0A8BA, 0x0A8BB, 0x0A8BC, 0x0A8BD, 0x0A8BE, 0x0A8BF,
+ 0x0A8C0, 0x0A8C1, 0x0A8C2, 0x0A8C3, 0x0A8C4, 0x0A8C5, 0x0A8E0, 0x0A8E1,
+ 0x0A8E2, 0x0A8E3, 0x0A8E4, 0x0A8E5, 0x0A8E6, 0x0A8E7, 0x0A8E8, 0x0A8E9,
+ 0x0A8EA, 0x0A8EB, 0x0A8EC, 0x0A8ED, 0x0A8EE, 0x0A8EF, 0x0A8F0, 0x0A8F1,
+ 0x0A926, 0x0A927, 0x0A928, 0x0A929, 0x0A92A, 0x0A92B, 0x0A92C, 0x0A92D,
+ 0x0A947, 0x0A948, 0x0A949, 0x0A94A, 0x0A94B, 0x0A94C, 0x0A94D, 0x0A94E,
+ 0x0A94F, 0x0A950, 0x0A951, 0x0A952, 0x0A953, 0x0A980, 0x0A981, 0x0A982,
+ 0x0A983, 0x0A9B3, 0x0A9B4, 0x0A9B5, 0x0A9B6, 0x0A9B7, 0x0A9B8, 0x0A9B9,
+ 0x0A9BA, 0x0A9BB, 0x0A9BC, 0x0A9BD, 0x0A9BE, 0x0A9BF, 0x0A9C0, 0x0A9E5,
+ 0x0AA29, 0x0AA2A, 0x0AA2B, 0x0AA2C, 0x0AA2D, 0x0AA2E, 0x0AA2F, 0x0AA30,
+ 0x0AA31, 0x0AA32, 0x0AA33, 0x0AA34, 0x0AA35, 0x0AA36, 0x0AA43, 0x0AA4C,
+ 0x0AA4D, 0x0AA7B, 0x0AA7C, 0x0AA7D, 0x0AAB0, 0x0AAB2, 0x0AAB3, 0x0AAB4,
+ 0x0AAB7, 0x0AAB8, 0x0AABE, 0x0AABF, 0x0AAC1, 0x0AAEB, 0x0AAEC, 0x0AAED,
+ 0x0AAEE, 0x0AAEF, 0x0AAF5, 0x0AAF6, 0x0ABE3, 0x0ABE4, 0x0ABE5, 0x0ABE6,
+ 0x0ABE7, 0x0ABE8, 0x0ABE9, 0x0ABEA, 0x0ABEC, 0x0ABED, 0x0FB1E, 0x0FE00,
+ 0x0FE01, 0x0FE02, 0x0FE03, 0x0FE04, 0x0FE05, 0x0FE06, 0x0FE07, 0x0FE08,
+ 0x0FE09, 0x0FE0A, 0x0FE0B, 0x0FE0C, 0x0FE0D, 0x0FE0E, 0x0FE0F, 0x0FE20,
+ 0x0FE21, 0x0FE22, 0x0FE23, 0x0FE24, 0x0FE25, 0x0FE26, 0x0FE27, 0x0FE28,
+ 0x0FE29, 0x0FE2A, 0x0FE2B, 0x0FE2C, 0x0FE2D, 0x0FE2E, 0x0FE2F, 0x0FF9E,
+ 0x0FF9F, 0x101FD, 0x102E0, 0x10376, 0x10377, 0x10378, 0x10379, 0x1037A,
+ 0x10A01, 0x10A02, 0x10A03, 0x10A05, 0x10A06, 0x10A0C, 0x10A0D, 0x10A0E,
+ 0x10A0F, 0x10A38, 0x10A39, 0x10A3A, 0x10A3F, 0x10AE5, 0x10AE6, 0x11000,
+ 0x11001, 0x11002, 0x11038, 0x11039, 0x1103A, 0x1103B, 0x1103C, 0x1103D,
+ 0x1103E, 0x1103F, 0x11040, 0x11041, 0x11042, 0x11043, 0x11044, 0x11045,
+ 0x11046, 0x1107F, 0x11080, 0x11081, 0x11082, 0x110B0, 0x110B1, 0x110B2,
+ 0x110B3, 0x110B4, 0x110B5, 0x110B6, 0x110B7, 0x110B8, 0x110B9, 0x110BA,
+ 0x11100, 0x11101, 0x11102, 0x11127, 0x11128, 0x11129, 0x1112A, 0x1112B,
+ 0x1112C, 0x1112D, 0x1112E, 0x1112F, 0x11130, 0x11131, 0x11132, 0x11133,
+ 0x11134, 0x11173, 0x11180, 0x11181, 0x11182, 0x111B3, 0x111B4, 0x111B5,
+ 0x111B6, 0x111B7, 0x111B8, 0x111B9, 0x111BA, 0x111BB, 0x111BC, 0x111BD,
+ 0x111BE, 0x111BF, 0x111C0, 0x111CA, 0x111CB, 0x111CC, 0x1122C, 0x1122D,
+ 0x1122E, 0x1122F, 0x11230, 0x11231, 0x11232, 0x11233, 0x11234, 0x11235,
+ 0x11236, 0x11237, 0x1123E, 0x112DF, 0x112E0, 0x112E1, 0x112E2, 0x112E3,
+ 0x112E4, 0x112E5, 0x112E6, 0x112E7, 0x112E8, 0x112E9, 0x112EA, 0x11300,
+ 0x11301, 0x11302, 0x11303, 0x1133C, 0x1133E, 0x1133F, 0x11340, 0x11341,
+ 0x11342, 0x11343, 0x11344, 0x11347, 0x11348, 0x1134B, 0x1134C, 0x1134D,
+ 0x11357, 0x11362, 0x11363, 0x11366, 0x11367, 0x11368, 0x11369, 0x1136A,
+ 0x1136B, 0x1136C, 0x11370, 0x11371, 0x11372, 0x11373, 0x11374, 0x11435,
+ 0x11436, 0x11437, 0x11438, 0x11439, 0x1143A, 0x1143B, 0x1143C, 0x1143D,
+ 0x1143E, 0x1143F, 0x11440, 0x11441, 0x11442, 0x11443, 0x11444, 0x11445,
+ 0x11446, 0x114B0, 0x114B1, 0x114B2, 0x114B3, 0x114B4, 0x114B5, 0x114B6,
+ 0x114B7, 0x114B8, 0x114B9, 0x114BA, 0x114BB, 0x114BC, 0x114BD, 0x114BE,
+ 0x114BF, 0x114C0, 0x114C1, 0x114C2, 0x114C3, 0x115AF, 0x115B0, 0x115B1,
+ 0x115B2, 0x115B3, 0x115B4, 0x115B5, 0x115B8, 0x115B9, 0x115BA, 0x115BB,
+ 0x115BC, 0x115BD, 0x115BE, 0x115BF, 0x115C0, 0x115DC, 0x115DD, 0x11630,
+ 0x11631, 0x11632, 0x11633, 0x11634, 0x11635, 0x11636, 0x11637, 0x11638,
+ 0x11639, 0x1163A, 0x1163B, 0x1163C, 0x1163D, 0x1163E, 0x1163F, 0x11640,
+ 0x116AB, 0x116AC, 0x116AD, 0x116AE, 0x116AF, 0x116B0, 0x116B1, 0x116B2,
+ 0x116B3, 0x116B4, 0x116B5, 0x116B6, 0x116B7, 0x1171D, 0x1171E, 0x1171F,
+ 0x11720, 0x11721, 0x11722, 0x11723, 0x11724, 0x11725, 0x11726, 0x11727,
+ 0x11728, 0x11729, 0x1172A, 0x1172B, 0x11C2F, 0x11C30, 0x11C31, 0x11C32,
+ 0x11C33, 0x11C34, 0x11C35, 0x11C36, 0x11C38, 0x11C39, 0x11C3A, 0x11C3B,
+ 0x11C3C, 0x11C3D, 0x11C3E, 0x11C3F, 0x11C92, 0x11C93, 0x11C94, 0x11C95,
+ 0x11C96, 0x11C97, 0x11C98, 0x11C99, 0x11C9A, 0x11C9B, 0x11C9C, 0x11C9D,
+ 0x11C9E, 0x11C9F, 0x11CA0, 0x11CA1, 0x11CA2, 0x11CA3, 0x11CA4, 0x11CA5,
+ 0x11CA6, 0x11CA7, 0x11CA9, 0x11CAA, 0x11CAB, 0x11CAC, 0x11CAD, 0x11CAE,
+ 0x11CAF, 0x11CB0, 0x11CB1, 0x11CB2, 0x11CB3, 0x11CB4, 0x11CB5, 0x11CB6,
+ 0x16AF0, 0x16AF1, 0x16AF2, 0x16AF3, 0x16AF4, 0x16B30, 0x16B31, 0x16B32,
+ 0x16B33, 0x16B34, 0x16B35, 0x16B36, 0x16F51, 0x16F52, 0x16F53, 0x16F54,
+ 0x16F55, 0x16F56, 0x16F57, 0x16F58, 0x16F59, 0x16F5A, 0x16F5B, 0x16F5C,
+ 0x16F5D, 0x16F5E, 0x16F5F, 0x16F60, 0x16F61, 0x16F62, 0x16F63, 0x16F64,
+ 0x16F65, 0x16F66, 0x16F67, 0x16F68, 0x16F69, 0x16F6A, 0x16F6B, 0x16F6C,
+ 0x16F6D, 0x16F6E, 0x16F6F, 0x16F70, 0x16F71, 0x16F72, 0x16F73, 0x16F74,
+ 0x16F75, 0x16F76, 0x16F77, 0x16F78, 0x16F79, 0x16F7A, 0x16F7B, 0x16F7C,
+ 0x16F7D, 0x16F7E, 0x16F8F, 0x16F90, 0x16F91, 0x16F92, 0x1BC9D, 0x1BC9E,
+ 0x1D165, 0x1D166, 0x1D167, 0x1D168, 0x1D169, 0x1D16D, 0x1D16E, 0x1D16F,
+ 0x1D170, 0x1D171, 0x1D172, 0x1D17B, 0x1D17C, 0x1D17D, 0x1D17E, 0x1D17F,
+ 0x1D180, 0x1D181, 0x1D182, 0x1D185, 0x1D186, 0x1D187, 0x1D188, 0x1D189,
+ 0x1D18A, 0x1D18B, 0x1D1AA, 0x1D1AB, 0x1D1AC, 0x1D1AD, 0x1D242, 0x1D243,
+ 0x1D244, 0x1DA00, 0x1DA01, 0x1DA02, 0x1DA03, 0x1DA04, 0x1DA05, 0x1DA06,
+ 0x1DA07, 0x1DA08, 0x1DA09, 0x1DA0A, 0x1DA0B, 0x1DA0C, 0x1DA0D, 0x1DA0E,
+ 0x1DA0F, 0x1DA10, 0x1DA11, 0x1DA12, 0x1DA13, 0x1DA14, 0x1DA15, 0x1DA16,
+ 0x1DA17, 0x1DA18, 0x1DA19, 0x1DA1A, 0x1DA1B, 0x1DA1C, 0x1DA1D, 0x1DA1E,
+ 0x1DA1F, 0x1DA20, 0x1DA21, 0x1DA22, 0x1DA23, 0x1DA24, 0x1DA25, 0x1DA26,
+ 0x1DA27, 0x1DA28, 0x1DA29, 0x1DA2A, 0x1DA2B, 0x1DA2C, 0x1DA2D, 0x1DA2E,
+ 0x1DA2F, 0x1DA30, 0x1DA31, 0x1DA32, 0x1DA33, 0x1DA34, 0x1DA35, 0x1DA36,
+ 0x1DA3B, 0x1DA3C, 0x1DA3D, 0x1DA3E, 0x1DA3F, 0x1DA40, 0x1DA41, 0x1DA42,
+ 0x1DA43, 0x1DA44, 0x1DA45, 0x1DA46, 0x1DA47, 0x1DA48, 0x1DA49, 0x1DA4A,
+ 0x1DA4B, 0x1DA4C, 0x1DA4D, 0x1DA4E, 0x1DA4F, 0x1DA50, 0x1DA51, 0x1DA52,
+ 0x1DA53, 0x1DA54, 0x1DA55, 0x1DA56, 0x1DA57, 0x1DA58, 0x1DA59, 0x1DA5A,
+ 0x1DA5B, 0x1DA5C, 0x1DA5D, 0x1DA5E, 0x1DA5F, 0x1DA60, 0x1DA61, 0x1DA62,
+ 0x1DA63, 0x1DA64, 0x1DA65, 0x1DA66, 0x1DA67, 0x1DA68, 0x1DA69, 0x1DA6A,
+ 0x1DA6B, 0x1DA6C, 0x1DA75, 0x1DA84, 0x1DA9B, 0x1DA9C, 0x1DA9D, 0x1DA9E,
+ 0x1DA9F, 0x1DAA1, 0x1DAA2, 0x1DAA3, 0x1DAA4, 0x1DAA5, 0x1DAA6, 0x1DAA7,
+ 0x1DAA8, 0x1DAA9, 0x1DAAA, 0x1DAAB, 0x1DAAC, 0x1DAAD, 0x1DAAE, 0x1DAAF,
+ 0x1E000, 0x1E001, 0x1E002, 0x1E003, 0x1E004, 0x1E005, 0x1E006, 0x1E008,
+ 0x1E009, 0x1E00A, 0x1E00B, 0x1E00C, 0x1E00D, 0x1E00E, 0x1E00F, 0x1E010,
+ 0x1E011, 0x1E012, 0x1E013, 0x1E014, 0x1E015, 0x1E016, 0x1E017, 0x1E018,
+ 0x1E01B, 0x1E01C, 0x1E01D, 0x1E01E, 0x1E01F, 0x1E020, 0x1E021, 0x1E023,
+ 0x1E024, 0x1E026, 0x1E027, 0x1E028, 0x1E029, 0x1E02A, 0x1E8D0, 0x1E8D1,
+ 0x1E8D2, 0x1E8D3, 0x1E8D4, 0x1E8D5, 0x1E8D6, 0x1E944, 0x1E945, 0x1E946,
+ 0x1E947, 0x1E948, 0x1E949, 0x1E94A, 0xE0020, 0xE0021, 0xE0022, 0xE0023,
+ 0xE0024, 0xE0025, 0xE0026, 0xE0027, 0xE0028, 0xE0029, 0xE002A, 0xE002B,
+ 0xE002C, 0xE002D, 0xE002E, 0xE002F, 0xE0030, 0xE0031, 0xE0032, 0xE0033,
+ 0xE0034, 0xE0035, 0xE0036, 0xE0037, 0xE0038, 0xE0039, 0xE003A, 0xE003B,
+ 0xE003C, 0xE003D, 0xE003E, 0xE003F, 0xE0040, 0xE0041, 0xE0042, 0xE0043,
+ 0xE0044, 0xE0045, 0xE0046, 0xE0047, 0xE0048, 0xE0049, 0xE004A, 0xE004B,
+ 0xE004C, 0xE004D, 0xE004E, 0xE004F, 0xE0050, 0xE0051, 0xE0052, 0xE0053,
+ 0xE0054, 0xE0055, 0xE0056, 0xE0057, 0xE0058, 0xE0059, 0xE005A, 0xE005B,
+ 0xE005C, 0xE005D, 0xE005E, 0xE005F, 0xE0060, 0xE0061, 0xE0062, 0xE0063,
+ 0xE0064, 0xE0065, 0xE0066, 0xE0067, 0xE0068, 0xE0069, 0xE006A, 0xE006B,
+ 0xE006C, 0xE006D, 0xE006E, 0xE006F, 0xE0070, 0xE0071, 0xE0072, 0xE0073,
+ 0xE0074, 0xE0075, 0xE0076, 0xE0077, 0xE0078, 0xE0079, 0xE007A, 0xE007B,
+ 0xE007C, 0xE007D, 0xE007E, 0xE007F, 0xE0100, 0xE0101, 0xE0102, 0xE0103,
+ 0xE0104, 0xE0105, 0xE0106, 0xE0107, 0xE0108, 0xE0109, 0xE010A, 0xE010B,
+ 0xE010C, 0xE010D, 0xE010E, 0xE010F, 0xE0110, 0xE0111, 0xE0112, 0xE0113,
+ 0xE0114, 0xE0115, 0xE0116, 0xE0117, 0xE0118, 0xE0119, 0xE011A, 0xE011B,
+ 0xE011C, 0xE011D, 0xE011E, 0xE011F, 0xE0120, 0xE0121, 0xE0122, 0xE0123,
+ 0xE0124, 0xE0125, 0xE0126, 0xE0127, 0xE0128, 0xE0129, 0xE012A, 0xE012B,
+ 0xE012C, 0xE012D, 0xE012E, 0xE012F, 0xE0130, 0xE0131, 0xE0132, 0xE0133,
+ 0xE0134, 0xE0135, 0xE0136, 0xE0137, 0xE0138, 0xE0139, 0xE013A, 0xE013B,
+ 0xE013C, 0xE013D, 0xE013E, 0xE013F, 0xE0140, 0xE0141, 0xE0142, 0xE0143,
+ 0xE0144, 0xE0145, 0xE0146, 0xE0147, 0xE0148, 0xE0149, 0xE014A, 0xE014B,
+ 0xE014C, 0xE014D, 0xE014E, 0xE014F, 0xE0150, 0xE0151, 0xE0152, 0xE0153,
+ 0xE0154, 0xE0155, 0xE0156, 0xE0157, 0xE0158, 0xE0159, 0xE015A, 0xE015B,
+ 0xE015C, 0xE015D, 0xE015E, 0xE015F, 0xE0160, 0xE0161, 0xE0162, 0xE0163,
+ 0xE0164, 0xE0165, 0xE0166, 0xE0167, 0xE0168, 0xE0169, 0xE016A, 0xE016B,
+ 0xE016C, 0xE016D, 0xE016E, 0xE016F, 0xE0170, 0xE0171, 0xE0172, 0xE0173,
+ 0xE0174, 0xE0175, 0xE0176, 0xE0177, 0xE0178, 0xE0179, 0xE017A, 0xE017B,
+ 0xE017C, 0xE017D, 0xE017E, 0xE017F, 0xE0180, 0xE0181, 0xE0182, 0xE0183,
+ 0xE0184, 0xE0185, 0xE0186, 0xE0187, 0xE0188, 0xE0189, 0xE018A, 0xE018B,
+ 0xE018C, 0xE018D, 0xE018E, 0xE018F, 0xE0190, 0xE0191, 0xE0192, 0xE0193,
+ 0xE0194, 0xE0195, 0xE0196, 0xE0197, 0xE0198, 0xE0199, 0xE019A, 0xE019B,
+ 0xE019C, 0xE019D, 0xE019E, 0xE019F, 0xE01A0, 0xE01A1, 0xE01A2, 0xE01A3,
+ 0xE01A4, 0xE01A5, 0xE01A6, 0xE01A7, 0xE01A8, 0xE01A9, 0xE01AA, 0xE01AB,
+ 0xE01AC, 0xE01AD, 0xE01AE, 0xE01AF, 0xE01B0, 0xE01B1, 0xE01B2, 0xE01B3,
+ 0xE01B4, 0xE01B5, 0xE01B6, 0xE01B7, 0xE01B8, 0xE01B9, 0xE01BA, 0xE01BB,
+ 0xE01BC, 0xE01BD, 0xE01BE, 0xE01BF, 0xE01C0, 0xE01C1, 0xE01C2, 0xE01C3,
+ 0xE01C4, 0xE01C5, 0xE01C6, 0xE01C7, 0xE01C8, 0xE01C9, 0xE01CA, 0xE01CB,
+ 0xE01CC, 0xE01CD, 0xE01CE, 0xE01CF, 0xE01D0, 0xE01D1, 0xE01D2, 0xE01D3,
+ 0xE01D4, 0xE01D5, 0xE01D6, 0xE01D7, 0xE01D8, 0xE01D9, 0xE01DA, 0xE01DB,
+ 0xE01DC, 0xE01DD, 0xE01DE, 0xE01DF, 0xE01E0, 0xE01E1, 0xE01E2, 0xE01E3,
+ 0xE01E4, 0xE01E5, 0xE01E6, 0xE01E7, 0xE01E8, 0xE01E9, 0xE01EA, 0xE01EB,
+ 0xE01EC, 0xE01ED, 0xE01EE, 0xE01EF
+};
+static const uint32_t Regional_Indicator[]= {
+ 0x1F1E6, 0x1F1E7, 0x1F1E8, 0x1F1E9, 0x1F1EA, 0x1F1EB, 0x1F1EC, 0x1F1ED,
+ 0x1F1EE, 0x1F1EF, 0x1F1F0, 0x1F1F1, 0x1F1F2, 0x1F1F3, 0x1F1F4, 0x1F1F5,
+ 0x1F1F6, 0x1F1F7, 0x1F1F8, 0x1F1F9, 0x1F1FA, 0x1F1FB, 0x1F1FC, 0x1F1FD,
+ 0x1F1FE, 0x1F1FF
+};
+static const uint32_t Format[]= {
+ 0x000AD, 0x00600, 0x00601, 0x00602, 0x00603, 0x00604, 0x00605, 0x0061C,
+ 0x006DD, 0x0070F, 0x008E2, 0x0180E, 0x0200E, 0x0200F, 0x0202A, 0x0202B,
+ 0x0202C, 0x0202D, 0x0202E, 0x02060, 0x02061, 0x02062, 0x02063, 0x02064,
+ 0x02066, 0x02067, 0x02068, 0x02069, 0x0206A, 0x0206B, 0x0206C, 0x0206D,
+ 0x0206E, 0x0206F, 0x0FEFF, 0x0FFF9, 0x0FFFA, 0x0FFFB, 0x110BD, 0x1BCA0,
+ 0x1BCA1, 0x1BCA2, 0x1BCA3, 0x1D173, 0x1D174, 0x1D175, 0x1D176, 0x1D177,
+ 0x1D178, 0x1D179, 0x1D17A, 0xE0001
+};
+static const uint32_t Katakana[]= {
+ 0x03031, 0x03032, 0x03033, 0x03034, 0x03035, 0x0309B, 0x0309C, 0x030A0,
+ 0x030A1, 0x030A2, 0x030A3, 0x030A4, 0x030A5, 0x030A6, 0x030A7, 0x030A8,
+ 0x030A9, 0x030AA, 0x030AB, 0x030AC, 0x030AD, 0x030AE, 0x030AF, 0x030B0,
+ 0x030B1, 0x030B2, 0x030B3, 0x030B4, 0x030B5, 0x030B6, 0x030B7, 0x030B8,
+ 0x030B9, 0x030BA, 0x030BB, 0x030BC, 0x030BD, 0x030BE, 0x030BF, 0x030C0,
+ 0x030C1, 0x030C2, 0x030C3, 0x030C4, 0x030C5, 0x030C6, 0x030C7, 0x030C8,
+ 0x030C9, 0x030CA, 0x030CB, 0x030CC, 0x030CD, 0x030CE, 0x030CF, 0x030D0,
+ 0x030D1, 0x030D2, 0x030D3, 0x030D4, 0x030D5, 0x030D6, 0x030D7, 0x030D8,
+ 0x030D9, 0x030DA, 0x030DB, 0x030DC, 0x030DD, 0x030DE, 0x030DF, 0x030E0,
+ 0x030E1, 0x030E2, 0x030E3, 0x030E4, 0x030E5, 0x030E6, 0x030E7, 0x030E8,
+ 0x030E9, 0x030EA, 0x030EB, 0x030EC, 0x030ED, 0x030EE, 0x030EF, 0x030F0,
+ 0x030F1, 0x030F2, 0x030F3, 0x030F4, 0x030F5, 0x030F6, 0x030F7, 0x030F8,
+ 0x030F9, 0x030FA, 0x030FC, 0x030FD, 0x030FE, 0x030FF, 0x031F0, 0x031F1,
+ 0x031F2, 0x031F3, 0x031F4, 0x031F5, 0x031F6, 0x031F7, 0x031F8, 0x031F9,
+ 0x031FA, 0x031FB, 0x031FC, 0x031FD, 0x031FE, 0x031FF, 0x032D0, 0x032D1,
+ 0x032D2, 0x032D3, 0x032D4, 0x032D5, 0x032D6, 0x032D7, 0x032D8, 0x032D9,
+ 0x032DA, 0x032DB, 0x032DC, 0x032DD, 0x032DE, 0x032DF, 0x032E0, 0x032E1,
+ 0x032E2, 0x032E3, 0x032E4, 0x032E5, 0x032E6, 0x032E7, 0x032E8, 0x032E9,
+ 0x032EA, 0x032EB, 0x032EC, 0x032ED, 0x032EE, 0x032EF, 0x032F0, 0x032F1,
+ 0x032F2, 0x032F3, 0x032F4, 0x032F5, 0x032F6, 0x032F7, 0x032F8, 0x032F9,
+ 0x032FA, 0x032FB, 0x032FC, 0x032FD, 0x032FE, 0x03300, 0x03301, 0x03302,
+ 0x03303, 0x03304, 0x03305, 0x03306, 0x03307, 0x03308, 0x03309, 0x0330A,
+ 0x0330B, 0x0330C, 0x0330D, 0x0330E, 0x0330F, 0x03310, 0x03311, 0x03312,
+ 0x03313, 0x03314, 0x03315, 0x03316, 0x03317, 0x03318, 0x03319, 0x0331A,
+ 0x0331B, 0x0331C, 0x0331D, 0x0331E, 0x0331F, 0x03320, 0x03321, 0x03322,
+ 0x03323, 0x03324, 0x03325, 0x03326, 0x03327, 0x03328, 0x03329, 0x0332A,
+ 0x0332B, 0x0332C, 0x0332D, 0x0332E, 0x0332F, 0x03330, 0x03331, 0x03332,
+ 0x03333, 0x03334, 0x03335, 0x03336, 0x03337, 0x03338, 0x03339, 0x0333A,
+ 0x0333B, 0x0333C, 0x0333D, 0x0333E, 0x0333F, 0x03340, 0x03341, 0x03342,
+ 0x03343, 0x03344, 0x03345, 0x03346, 0x03347, 0x03348, 0x03349, 0x0334A,
+ 0x0334B, 0x0334C, 0x0334D, 0x0334E, 0x0334F, 0x03350, 0x03351, 0x03352,
+ 0x03353, 0x03354, 0x03355, 0x03356, 0x03357, 0x0FF66, 0x0FF67, 0x0FF68,
+ 0x0FF69, 0x0FF6A, 0x0FF6B, 0x0FF6C, 0x0FF6D, 0x0FF6E, 0x0FF6F, 0x0FF70,
+ 0x0FF71, 0x0FF72, 0x0FF73, 0x0FF74, 0x0FF75, 0x0FF76, 0x0FF77, 0x0FF78,
+ 0x0FF79, 0x0FF7A, 0x0FF7B, 0x0FF7C, 0x0FF7D, 0x0FF7E, 0x0FF7F, 0x0FF80,
+ 0x0FF81, 0x0FF82, 0x0FF83, 0x0FF84, 0x0FF85, 0x0FF86, 0x0FF87, 0x0FF88,
+ 0x0FF89, 0x0FF8A, 0x0FF8B, 0x0FF8C, 0x0FF8D, 0x0FF8E, 0x0FF8F, 0x0FF90,
+ 0x0FF91, 0x0FF92, 0x0FF93, 0x0FF94, 0x0FF95, 0x0FF96, 0x0FF97, 0x0FF98,
+ 0x0FF99, 0x0FF9A, 0x0FF9B, 0x0FF9C, 0x0FF9D, 0x1B000
+};
+static const uint32_t Hebrew_Letter[]= {
+ 0x005D0, 0x005D1, 0x005D2, 0x005D3, 0x005D4, 0x005D5, 0x005D6, 0x005D7,
+ 0x005D8, 0x005D9, 0x005DA, 0x005DB, 0x005DC, 0x005DD, 0x005DE, 0x005DF,
+ 0x005E0, 0x005E1, 0x005E2, 0x005E3, 0x005E4, 0x005E5, 0x005E6, 0x005E7,
+ 0x005E8, 0x005E9, 0x005EA, 0x005F0, 0x005F1, 0x005F2, 0x0FB1D, 0x0FB1F,
+ 0x0FB20, 0x0FB21, 0x0FB22, 0x0FB23, 0x0FB24, 0x0FB25, 0x0FB26, 0x0FB27,
+ 0x0FB28, 0x0FB2A, 0x0FB2B, 0x0FB2C, 0x0FB2D, 0x0FB2E, 0x0FB2F, 0x0FB30,
+ 0x0FB31, 0x0FB32, 0x0FB33, 0x0FB34, 0x0FB35, 0x0FB36, 0x0FB38, 0x0FB39,
+ 0x0FB3A, 0x0FB3B, 0x0FB3C, 0x0FB3E, 0x0FB40, 0x0FB41, 0x0FB43, 0x0FB44,
+ 0x0FB46, 0x0FB47, 0x0FB48, 0x0FB49, 0x0FB4A, 0x0FB4B, 0x0FB4C, 0x0FB4D,
+ 0x0FB4E, 0x0FB4F
+};
+static const uint32_t ALetter[]= {
+ 0x00041, 0x00042, 0x00043, 0x00044, 0x00045, 0x00046, 0x00047, 0x00048,
+ 0x00049, 0x0004A, 0x0004B, 0x0004C, 0x0004D, 0x0004E, 0x0004F, 0x00050,
+ 0x00051, 0x00052, 0x00053, 0x00054, 0x00055, 0x00056, 0x00057, 0x00058,
+ 0x00059, 0x0005A, 0x00061, 0x00062, 0x00063, 0x00064, 0x00065, 0x00066,
+ 0x00067, 0x00068, 0x00069, 0x0006A, 0x0006B, 0x0006C, 0x0006D, 0x0006E,
+ 0x0006F, 0x00070, 0x00071, 0x00072, 0x00073, 0x00074, 0x00075, 0x00076,
+ 0x00077, 0x00078, 0x00079, 0x0007A, 0x000AA, 0x000B5, 0x000BA, 0x000C0,
+ 0x000C1, 0x000C2, 0x000C3, 0x000C4, 0x000C5, 0x000C6, 0x000C7, 0x000C8,
+ 0x000C9, 0x000CA, 0x000CB, 0x000CC, 0x000CD, 0x000CE, 0x000CF, 0x000D0,
+ 0x000D1, 0x000D2, 0x000D3, 0x000D4, 0x000D5, 0x000D6, 0x000D8, 0x000D9,
+ 0x000DA, 0x000DB, 0x000DC, 0x000DD, 0x000DE, 0x000DF, 0x000E0, 0x000E1,
+ 0x000E2, 0x000E3, 0x000E4, 0x000E5, 0x000E6, 0x000E7, 0x000E8, 0x000E9,
+ 0x000EA, 0x000EB, 0x000EC, 0x000ED, 0x000EE, 0x000EF, 0x000F0, 0x000F1,
+ 0x000F2, 0x000F3, 0x000F4, 0x000F5, 0x000F6, 0x000F8, 0x000F9, 0x000FA,
+ 0x000FB, 0x000FC, 0x000FD, 0x000FE, 0x000FF, 0x00100, 0x00101, 0x00102,
+ 0x00103, 0x00104, 0x00105, 0x00106, 0x00107, 0x00108, 0x00109, 0x0010A,
+ 0x0010B, 0x0010C, 0x0010D, 0x0010E, 0x0010F, 0x00110, 0x00111, 0x00112,
+ 0x00113, 0x00114, 0x00115, 0x00116, 0x00117, 0x00118, 0x00119, 0x0011A,
+ 0x0011B, 0x0011C, 0x0011D, 0x0011E, 0x0011F, 0x00120, 0x00121, 0x00122,
+ 0x00123, 0x00124, 0x00125, 0x00126, 0x00127, 0x00128, 0x00129, 0x0012A,
+ 0x0012B, 0x0012C, 0x0012D, 0x0012E, 0x0012F, 0x00130, 0x00131, 0x00132,
+ 0x00133, 0x00134, 0x00135, 0x00136, 0x00137, 0x00138, 0x00139, 0x0013A,
+ 0x0013B, 0x0013C, 0x0013D, 0x0013E, 0x0013F, 0x00140, 0x00141, 0x00142,
+ 0x00143, 0x00144, 0x00145, 0x00146, 0x00147, 0x00148, 0x00149, 0x0014A,
+ 0x0014B, 0x0014C, 0x0014D, 0x0014E, 0x0014F, 0x00150, 0x00151, 0x00152,
+ 0x00153, 0x00154, 0x00155, 0x00156, 0x00157, 0x00158, 0x00159, 0x0015A,
+ 0x0015B, 0x0015C, 0x0015D, 0x0015E, 0x0015F, 0x00160, 0x00161, 0x00162,
+ 0x00163, 0x00164, 0x00165, 0x00166, 0x00167, 0x00168, 0x00169, 0x0016A,
+ 0x0016B, 0x0016C, 0x0016D, 0x0016E, 0x0016F, 0x00170, 0x00171, 0x00172,
+ 0x00173, 0x00174, 0x00175, 0x00176, 0x00177, 0x00178, 0x00179, 0x0017A,
+ 0x0017B, 0x0017C, 0x0017D, 0x0017E, 0x0017F, 0x00180, 0x00181, 0x00182,
+ 0x00183, 0x00184, 0x00185, 0x00186, 0x00187, 0x00188, 0x00189, 0x0018A,
+ 0x0018B, 0x0018C, 0x0018D, 0x0018E, 0x0018F, 0x00190, 0x00191, 0x00192,
+ 0x00193, 0x00194, 0x00195, 0x00196, 0x00197, 0x00198, 0x00199, 0x0019A,
+ 0x0019B, 0x0019C, 0x0019D, 0x0019E, 0x0019F, 0x001A0, 0x001A1, 0x001A2,
+ 0x001A3, 0x001A4, 0x001A5, 0x001A6, 0x001A7, 0x001A8, 0x001A9, 0x001AA,
+ 0x001AB, 0x001AC, 0x001AD, 0x001AE, 0x001AF, 0x001B0, 0x001B1, 0x001B2,
+ 0x001B3, 0x001B4, 0x001B5, 0x001B6, 0x001B7, 0x001B8, 0x001B9, 0x001BA,
+ 0x001BB, 0x001BC, 0x001BD, 0x001BE, 0x001BF, 0x001C0, 0x001C1, 0x001C2,
+ 0x001C3, 0x001C4, 0x001C5, 0x001C6, 0x001C7, 0x001C8, 0x001C9, 0x001CA,
+ 0x001CB, 0x001CC, 0x001CD, 0x001CE, 0x001CF, 0x001D0, 0x001D1, 0x001D2,
+ 0x001D3, 0x001D4, 0x001D5, 0x001D6, 0x001D7, 0x001D8, 0x001D9, 0x001DA,
+ 0x001DB, 0x001DC, 0x001DD, 0x001DE, 0x001DF, 0x001E0, 0x001E1, 0x001E2,
+ 0x001E3, 0x001E4, 0x001E5, 0x001E6, 0x001E7, 0x001E8, 0x001E9, 0x001EA,
+ 0x001EB, 0x001EC, 0x001ED, 0x001EE, 0x001EF, 0x001F0, 0x001F1, 0x001F2,
+ 0x001F3, 0x001F4, 0x001F5, 0x001F6, 0x001F7, 0x001F8, 0x001F9, 0x001FA,
+ 0x001FB, 0x001FC, 0x001FD, 0x001FE, 0x001FF, 0x00200, 0x00201, 0x00202,
+ 0x00203, 0x00204, 0x00205, 0x00206, 0x00207, 0x00208, 0x00209, 0x0020A,
+ 0x0020B, 0x0020C, 0x0020D, 0x0020E, 0x0020F, 0x00210, 0x00211, 0x00212,
+ 0x00213, 0x00214, 0x00215, 0x00216, 0x00217, 0x00218, 0x00219, 0x0021A,
+ 0x0021B, 0x0021C, 0x0021D, 0x0021E, 0x0021F, 0x00220, 0x00221, 0x00222,
+ 0x00223, 0x00224, 0x00225, 0x00226, 0x00227, 0x00228, 0x00229, 0x0022A,
+ 0x0022B, 0x0022C, 0x0022D, 0x0022E, 0x0022F, 0x00230, 0x00231, 0x00232,
+ 0x00233, 0x00234, 0x00235, 0x00236, 0x00237, 0x00238, 0x00239, 0x0023A,
+ 0x0023B, 0x0023C, 0x0023D, 0x0023E, 0x0023F, 0x00240, 0x00241, 0x00242,
+ 0x00243, 0x00244, 0x00245, 0x00246, 0x00247, 0x00248, 0x00249, 0x0024A,
+ 0x0024B, 0x0024C, 0x0024D, 0x0024E, 0x0024F, 0x00250, 0x00251, 0x00252,
+ 0x00253, 0x00254, 0x00255, 0x00256, 0x00257, 0x00258, 0x00259, 0x0025A,
+ 0x0025B, 0x0025C, 0x0025D, 0x0025E, 0x0025F, 0x00260, 0x00261, 0x00262,
+ 0x00263, 0x00264, 0x00265, 0x00266, 0x00267, 0x00268, 0x00269, 0x0026A,
+ 0x0026B, 0x0026C, 0x0026D, 0x0026E, 0x0026F, 0x00270, 0x00271, 0x00272,
+ 0x00273, 0x00274, 0x00275, 0x00276, 0x00277, 0x00278, 0x00279, 0x0027A,
+ 0x0027B, 0x0027C, 0x0027D, 0x0027E, 0x0027F, 0x00280, 0x00281, 0x00282,
+ 0x00283, 0x00284, 0x00285, 0x00286, 0x00287, 0x00288, 0x00289, 0x0028A,
+ 0x0028B, 0x0028C, 0x0028D, 0x0028E, 0x0028F, 0x00290, 0x00291, 0x00292,
+ 0x00293, 0x00294, 0x00295, 0x00296, 0x00297, 0x00298, 0x00299, 0x0029A,
+ 0x0029B, 0x0029C, 0x0029D, 0x0029E, 0x0029F, 0x002A0, 0x002A1, 0x002A2,
+ 0x002A3, 0x002A4, 0x002A5, 0x002A6, 0x002A7, 0x002A8, 0x002A9, 0x002AA,
+ 0x002AB, 0x002AC, 0x002AD, 0x002AE, 0x002AF, 0x002B0, 0x002B1, 0x002B2,
+ 0x002B3, 0x002B4, 0x002B5, 0x002B6, 0x002B7, 0x002B8, 0x002B9, 0x002BA,
+ 0x002BB, 0x002BC, 0x002BD, 0x002BE, 0x002BF, 0x002C0, 0x002C1, 0x002C6,
+ 0x002C7, 0x002C8, 0x002C9, 0x002CA, 0x002CB, 0x002CC, 0x002CD, 0x002CE,
+ 0x002CF, 0x002D0, 0x002D1, 0x002E0, 0x002E1, 0x002E2, 0x002E3, 0x002E4,
+ 0x002EC, 0x002EE, 0x00370, 0x00371, 0x00372, 0x00373, 0x00374, 0x00376,
+ 0x00377, 0x0037A, 0x0037B, 0x0037C, 0x0037D, 0x0037F, 0x00386, 0x00388,
+ 0x00389, 0x0038A, 0x0038C, 0x0038E, 0x0038F, 0x00390, 0x00391, 0x00392,
+ 0x00393, 0x00394, 0x00395, 0x00396, 0x00397, 0x00398, 0x00399, 0x0039A,
+ 0x0039B, 0x0039C, 0x0039D, 0x0039E, 0x0039F, 0x003A0, 0x003A1, 0x003A3,
+ 0x003A4, 0x003A5, 0x003A6, 0x003A7, 0x003A8, 0x003A9, 0x003AA, 0x003AB,
+ 0x003AC, 0x003AD, 0x003AE, 0x003AF, 0x003B0, 0x003B1, 0x003B2, 0x003B3,
+ 0x003B4, 0x003B5, 0x003B6, 0x003B7, 0x003B8, 0x003B9, 0x003BA, 0x003BB,
+ 0x003BC, 0x003BD, 0x003BE, 0x003BF, 0x003C0, 0x003C1, 0x003C2, 0x003C3,
+ 0x003C4, 0x003C5, 0x003C6, 0x003C7, 0x003C8, 0x003C9, 0x003CA, 0x003CB,
+ 0x003CC, 0x003CD, 0x003CE, 0x003CF, 0x003D0, 0x003D1, 0x003D2, 0x003D3,
+ 0x003D4, 0x003D5, 0x003D6, 0x003D7, 0x003D8, 0x003D9, 0x003DA, 0x003DB,
+ 0x003DC, 0x003DD, 0x003DE, 0x003DF, 0x003E0, 0x003E1, 0x003E2, 0x003E3,
+ 0x003E4, 0x003E5, 0x003E6, 0x003E7, 0x003E8, 0x003E9, 0x003EA, 0x003EB,
+ 0x003EC, 0x003ED, 0x003EE, 0x003EF, 0x003F0, 0x003F1, 0x003F2, 0x003F3,
+ 0x003F4, 0x003F5, 0x003F7, 0x003F8, 0x003F9, 0x003FA, 0x003FB, 0x003FC,
+ 0x003FD, 0x003FE, 0x003FF, 0x00400, 0x00401, 0x00402, 0x00403, 0x00404,
+ 0x00405, 0x00406, 0x00407, 0x00408, 0x00409, 0x0040A, 0x0040B, 0x0040C,
+ 0x0040D, 0x0040E, 0x0040F, 0x00410, 0x00411, 0x00412, 0x00413, 0x00414,
+ 0x00415, 0x00416, 0x00417, 0x00418, 0x00419, 0x0041A, 0x0041B, 0x0041C,
+ 0x0041D, 0x0041E, 0x0041F, 0x00420, 0x00421, 0x00422, 0x00423, 0x00424,
+ 0x00425, 0x00426, 0x00427, 0x00428, 0x00429, 0x0042A, 0x0042B, 0x0042C,
+ 0x0042D, 0x0042E, 0x0042F, 0x00430, 0x00431, 0x00432, 0x00433, 0x00434,
+ 0x00435, 0x00436, 0x00437, 0x00438, 0x00439, 0x0043A, 0x0043B, 0x0043C,
+ 0x0043D, 0x0043E, 0x0043F, 0x00440, 0x00441, 0x00442, 0x00443, 0x00444,
+ 0x00445, 0x00446, 0x00447, 0x00448, 0x00449, 0x0044A, 0x0044B, 0x0044C,
+ 0x0044D, 0x0044E, 0x0044F, 0x00450, 0x00451, 0x00452, 0x00453, 0x00454,
+ 0x00455, 0x00456, 0x00457, 0x00458, 0x00459, 0x0045A, 0x0045B, 0x0045C,
+ 0x0045D, 0x0045E, 0x0045F, 0x00460, 0x00461, 0x00462, 0x00463, 0x00464,
+ 0x00465, 0x00466, 0x00467, 0x00468, 0x00469, 0x0046A, 0x0046B, 0x0046C,
+ 0x0046D, 0x0046E, 0x0046F, 0x00470, 0x00471, 0x00472, 0x00473, 0x00474,
+ 0x00475, 0x00476, 0x00477, 0x00478, 0x00479, 0x0047A, 0x0047B, 0x0047C,
+ 0x0047D, 0x0047E, 0x0047F, 0x00480, 0x00481, 0x0048A, 0x0048B, 0x0048C,
+ 0x0048D, 0x0048E, 0x0048F, 0x00490, 0x00491, 0x00492, 0x00493, 0x00494,
+ 0x00495, 0x00496, 0x00497, 0x00498, 0x00499, 0x0049A, 0x0049B, 0x0049C,
+ 0x0049D, 0x0049E, 0x0049F, 0x004A0, 0x004A1, 0x004A2, 0x004A3, 0x004A4,
+ 0x004A5, 0x004A6, 0x004A7, 0x004A8, 0x004A9, 0x004AA, 0x004AB, 0x004AC,
+ 0x004AD, 0x004AE, 0x004AF, 0x004B0, 0x004B1, 0x004B2, 0x004B3, 0x004B4,
+ 0x004B5, 0x004B6, 0x004B7, 0x004B8, 0x004B9, 0x004BA, 0x004BB, 0x004BC,
+ 0x004BD, 0x004BE, 0x004BF, 0x004C0, 0x004C1, 0x004C2, 0x004C3, 0x004C4,
+ 0x004C5, 0x004C6, 0x004C7, 0x004C8, 0x004C9, 0x004CA, 0x004CB, 0x004CC,
+ 0x004CD, 0x004CE, 0x004CF, 0x004D0, 0x004D1, 0x004D2, 0x004D3, 0x004D4,
+ 0x004D5, 0x004D6, 0x004D7, 0x004D8, 0x004D9, 0x004DA, 0x004DB, 0x004DC,
+ 0x004DD, 0x004DE, 0x004DF, 0x004E0, 0x004E1, 0x004E2, 0x004E3, 0x004E4,
+ 0x004E5, 0x004E6, 0x004E7, 0x004E8, 0x004E9, 0x004EA, 0x004EB, 0x004EC,
+ 0x004ED, 0x004EE, 0x004EF, 0x004F0, 0x004F1, 0x004F2, 0x004F3, 0x004F4,
+ 0x004F5, 0x004F6, 0x004F7, 0x004F8, 0x004F9, 0x004FA, 0x004FB, 0x004FC,
+ 0x004FD, 0x004FE, 0x004FF, 0x00500, 0x00501, 0x00502, 0x00503, 0x00504,
+ 0x00505, 0x00506, 0x00507, 0x00508, 0x00509, 0x0050A, 0x0050B, 0x0050C,
+ 0x0050D, 0x0050E, 0x0050F, 0x00510, 0x00511, 0x00512, 0x00513, 0x00514,
+ 0x00515, 0x00516, 0x00517, 0x00518, 0x00519, 0x0051A, 0x0051B, 0x0051C,
+ 0x0051D, 0x0051E, 0x0051F, 0x00520, 0x00521, 0x00522, 0x00523, 0x00524,
+ 0x00525, 0x00526, 0x00527, 0x00528, 0x00529, 0x0052A, 0x0052B, 0x0052C,
+ 0x0052D, 0x0052E, 0x0052F, 0x00531, 0x00532, 0x00533, 0x00534, 0x00535,
+ 0x00536, 0x00537, 0x00538, 0x00539, 0x0053A, 0x0053B, 0x0053C, 0x0053D,
+ 0x0053E, 0x0053F, 0x00540, 0x00541, 0x00542, 0x00543, 0x00544, 0x00545,
+ 0x00546, 0x00547, 0x00548, 0x00549, 0x0054A, 0x0054B, 0x0054C, 0x0054D,
+ 0x0054E, 0x0054F, 0x00550, 0x00551, 0x00552, 0x00553, 0x00554, 0x00555,
+ 0x00556, 0x00559, 0x00561, 0x00562, 0x00563, 0x00564, 0x00565, 0x00566,
+ 0x00567, 0x00568, 0x00569, 0x0056A, 0x0056B, 0x0056C, 0x0056D, 0x0056E,
+ 0x0056F, 0x00570, 0x00571, 0x00572, 0x00573, 0x00574, 0x00575, 0x00576,
+ 0x00577, 0x00578, 0x00579, 0x0057A, 0x0057B, 0x0057C, 0x0057D, 0x0057E,
+ 0x0057F, 0x00580, 0x00581, 0x00582, 0x00583, 0x00584, 0x00585, 0x00586,
+ 0x00587, 0x005F3, 0x00620, 0x00621, 0x00622, 0x00623, 0x00624, 0x00625,
+ 0x00626, 0x00627, 0x00628, 0x00629, 0x0062A, 0x0062B, 0x0062C, 0x0062D,
+ 0x0062E, 0x0062F, 0x00630, 0x00631, 0x00632, 0x00633, 0x00634, 0x00635,
+ 0x00636, 0x00637, 0x00638, 0x00639, 0x0063A, 0x0063B, 0x0063C, 0x0063D,
+ 0x0063E, 0x0063F, 0x00640, 0x00641, 0x00642, 0x00643, 0x00644, 0x00645,
+ 0x00646, 0x00647, 0x00648, 0x00649, 0x0064A, 0x0066E, 0x0066F, 0x00671,
+ 0x00672, 0x00673, 0x00674, 0x00675, 0x00676, 0x00677, 0x00678, 0x00679,
+ 0x0067A, 0x0067B, 0x0067C, 0x0067D, 0x0067E, 0x0067F, 0x00680, 0x00681,
+ 0x00682, 0x00683, 0x00684, 0x00685, 0x00686, 0x00687, 0x00688, 0x00689,
+ 0x0068A, 0x0068B, 0x0068C, 0x0068D, 0x0068E, 0x0068F, 0x00690, 0x00691,
+ 0x00692, 0x00693, 0x00694, 0x00695, 0x00696, 0x00697, 0x00698, 0x00699,
+ 0x0069A, 0x0069B, 0x0069C, 0x0069D, 0x0069E, 0x0069F, 0x006A0, 0x006A1,
+ 0x006A2, 0x006A3, 0x006A4, 0x006A5, 0x006A6, 0x006A7, 0x006A8, 0x006A9,
+ 0x006AA, 0x006AB, 0x006AC, 0x006AD, 0x006AE, 0x006AF, 0x006B0, 0x006B1,
+ 0x006B2, 0x006B3, 0x006B4, 0x006B5, 0x006B6, 0x006B7, 0x006B8, 0x006B9,
+ 0x006BA, 0x006BB, 0x006BC, 0x006BD, 0x006BE, 0x006BF, 0x006C0, 0x006C1,
+ 0x006C2, 0x006C3, 0x006C4, 0x006C5, 0x006C6, 0x006C7, 0x006C8, 0x006C9,
+ 0x006CA, 0x006CB, 0x006CC, 0x006CD, 0x006CE, 0x006CF, 0x006D0, 0x006D1,
+ 0x006D2, 0x006D3, 0x006D5, 0x006E5, 0x006E6, 0x006EE, 0x006EF, 0x006FA,
+ 0x006FB, 0x006FC, 0x006FF, 0x00710, 0x00712, 0x00713, 0x00714, 0x00715,
+ 0x00716, 0x00717, 0x00718, 0x00719, 0x0071A, 0x0071B, 0x0071C, 0x0071D,
+ 0x0071E, 0x0071F, 0x00720, 0x00721, 0x00722, 0x00723, 0x00724, 0x00725,
+ 0x00726, 0x00727, 0x00728, 0x00729, 0x0072A, 0x0072B, 0x0072C, 0x0072D,
+ 0x0072E, 0x0072F, 0x0074D, 0x0074E, 0x0074F, 0x00750, 0x00751, 0x00752,
+ 0x00753, 0x00754, 0x00755, 0x00756, 0x00757, 0x00758, 0x00759, 0x0075A,
+ 0x0075B, 0x0075C, 0x0075D, 0x0075E, 0x0075F, 0x00760, 0x00761, 0x00762,
+ 0x00763, 0x00764, 0x00765, 0x00766, 0x00767, 0x00768, 0x00769, 0x0076A,
+ 0x0076B, 0x0076C, 0x0076D, 0x0076E, 0x0076F, 0x00770, 0x00771, 0x00772,
+ 0x00773, 0x00774, 0x00775, 0x00776, 0x00777, 0x00778, 0x00779, 0x0077A,
+ 0x0077B, 0x0077C, 0x0077D, 0x0077E, 0x0077F, 0x00780, 0x00781, 0x00782,
+ 0x00783, 0x00784, 0x00785, 0x00786, 0x00787, 0x00788, 0x00789, 0x0078A,
+ 0x0078B, 0x0078C, 0x0078D, 0x0078E, 0x0078F, 0x00790, 0x00791, 0x00792,
+ 0x00793, 0x00794, 0x00795, 0x00796, 0x00797, 0x00798, 0x00799, 0x0079A,
+ 0x0079B, 0x0079C, 0x0079D, 0x0079E, 0x0079F, 0x007A0, 0x007A1, 0x007A2,
+ 0x007A3, 0x007A4, 0x007A5, 0x007B1, 0x007CA, 0x007CB, 0x007CC, 0x007CD,
+ 0x007CE, 0x007CF, 0x007D0, 0x007D1, 0x007D2, 0x007D3, 0x007D4, 0x007D5,
+ 0x007D6, 0x007D7, 0x007D8, 0x007D9, 0x007DA, 0x007DB, 0x007DC, 0x007DD,
+ 0x007DE, 0x007DF, 0x007E0, 0x007E1, 0x007E2, 0x007E3, 0x007E4, 0x007E5,
+ 0x007E6, 0x007E7, 0x007E8, 0x007E9, 0x007EA, 0x007F4, 0x007F5, 0x007FA,
+ 0x00800, 0x00801, 0x00802, 0x00803, 0x00804, 0x00805, 0x00806, 0x00807,
+ 0x00808, 0x00809, 0x0080A, 0x0080B, 0x0080C, 0x0080D, 0x0080E, 0x0080F,
+ 0x00810, 0x00811, 0x00812, 0x00813, 0x00814, 0x00815, 0x0081A, 0x00824,
+ 0x00828, 0x00840, 0x00841, 0x00842, 0x00843, 0x00844, 0x00845, 0x00846,
+ 0x00847, 0x00848, 0x00849, 0x0084A, 0x0084B, 0x0084C, 0x0084D, 0x0084E,
+ 0x0084F, 0x00850, 0x00851, 0x00852, 0x00853, 0x00854, 0x00855, 0x00856,
+ 0x00857, 0x00858, 0x008A0, 0x008A1, 0x008A2, 0x008A3, 0x008A4, 0x008A5,
+ 0x008A6, 0x008A7, 0x008A8, 0x008A9, 0x008AA, 0x008AB, 0x008AC, 0x008AD,
+ 0x008AE, 0x008AF, 0x008B0, 0x008B1, 0x008B2, 0x008B3, 0x008B4, 0x008B6,
+ 0x008B7, 0x008B8, 0x008B9, 0x008BA, 0x008BB, 0x008BC, 0x008BD, 0x00904,
+ 0x00905, 0x00906, 0x00907, 0x00908, 0x00909, 0x0090A, 0x0090B, 0x0090C,
+ 0x0090D, 0x0090E, 0x0090F, 0x00910, 0x00911, 0x00912, 0x00913, 0x00914,
+ 0x00915, 0x00916, 0x00917, 0x00918, 0x00919, 0x0091A, 0x0091B, 0x0091C,
+ 0x0091D, 0x0091E, 0x0091F, 0x00920, 0x00921, 0x00922, 0x00923, 0x00924,
+ 0x00925, 0x00926, 0x00927, 0x00928, 0x00929, 0x0092A, 0x0092B, 0x0092C,
+ 0x0092D, 0x0092E, 0x0092F, 0x00930, 0x00931, 0x00932, 0x00933, 0x00934,
+ 0x00935, 0x00936, 0x00937, 0x00938, 0x00939, 0x0093D, 0x00950, 0x00958,
+ 0x00959, 0x0095A, 0x0095B, 0x0095C, 0x0095D, 0x0095E, 0x0095F, 0x00960,
+ 0x00961, 0x00971, 0x00972, 0x00973, 0x00974, 0x00975, 0x00976, 0x00977,
+ 0x00978, 0x00979, 0x0097A, 0x0097B, 0x0097C, 0x0097D, 0x0097E, 0x0097F,
+ 0x00980, 0x00985, 0x00986, 0x00987, 0x00988, 0x00989, 0x0098A, 0x0098B,
+ 0x0098C, 0x0098F, 0x00990, 0x00993, 0x00994, 0x00995, 0x00996, 0x00997,
+ 0x00998, 0x00999, 0x0099A, 0x0099B, 0x0099C, 0x0099D, 0x0099E, 0x0099F,
+ 0x009A0, 0x009A1, 0x009A2, 0x009A3, 0x009A4, 0x009A5, 0x009A6, 0x009A7,
+ 0x009A8, 0x009AA, 0x009AB, 0x009AC, 0x009AD, 0x009AE, 0x009AF, 0x009B0,
+ 0x009B2, 0x009B6, 0x009B7, 0x009B8, 0x009B9, 0x009BD, 0x009CE, 0x009DC,
+ 0x009DD, 0x009DF, 0x009E0, 0x009E1, 0x009F0, 0x009F1, 0x00A05, 0x00A06,
+ 0x00A07, 0x00A08, 0x00A09, 0x00A0A, 0x00A0F, 0x00A10, 0x00A13, 0x00A14,
+ 0x00A15, 0x00A16, 0x00A17, 0x00A18, 0x00A19, 0x00A1A, 0x00A1B, 0x00A1C,
+ 0x00A1D, 0x00A1E, 0x00A1F, 0x00A20, 0x00A21, 0x00A22, 0x00A23, 0x00A24,
+ 0x00A25, 0x00A26, 0x00A27, 0x00A28, 0x00A2A, 0x00A2B, 0x00A2C, 0x00A2D,
+ 0x00A2E, 0x00A2F, 0x00A30, 0x00A32, 0x00A33, 0x00A35, 0x00A36, 0x00A38,
+ 0x00A39, 0x00A59, 0x00A5A, 0x00A5B, 0x00A5C, 0x00A5E, 0x00A72, 0x00A73,
+ 0x00A74, 0x00A85, 0x00A86, 0x00A87, 0x00A88, 0x00A89, 0x00A8A, 0x00A8B,
+ 0x00A8C, 0x00A8D, 0x00A8F, 0x00A90, 0x00A91, 0x00A93, 0x00A94, 0x00A95,
+ 0x00A96, 0x00A97, 0x00A98, 0x00A99, 0x00A9A, 0x00A9B, 0x00A9C, 0x00A9D,
+ 0x00A9E, 0x00A9F, 0x00AA0, 0x00AA1, 0x00AA2, 0x00AA3, 0x00AA4, 0x00AA5,
+ 0x00AA6, 0x00AA7, 0x00AA8, 0x00AAA, 0x00AAB, 0x00AAC, 0x00AAD, 0x00AAE,
+ 0x00AAF, 0x00AB0, 0x00AB2, 0x00AB3, 0x00AB5, 0x00AB6, 0x00AB7, 0x00AB8,
+ 0x00AB9, 0x00ABD, 0x00AD0, 0x00AE0, 0x00AE1, 0x00AF9, 0x00B05, 0x00B06,
+ 0x00B07, 0x00B08, 0x00B09, 0x00B0A, 0x00B0B, 0x00B0C, 0x00B0F, 0x00B10,
+ 0x00B13, 0x00B14, 0x00B15, 0x00B16, 0x00B17, 0x00B18, 0x00B19, 0x00B1A,
+ 0x00B1B, 0x00B1C, 0x00B1D, 0x00B1E, 0x00B1F, 0x00B20, 0x00B21, 0x00B22,
+ 0x00B23, 0x00B24, 0x00B25, 0x00B26, 0x00B27, 0x00B28, 0x00B2A, 0x00B2B,
+ 0x00B2C, 0x00B2D, 0x00B2E, 0x00B2F, 0x00B30, 0x00B32, 0x00B33, 0x00B35,
+ 0x00B36, 0x00B37, 0x00B38, 0x00B39, 0x00B3D, 0x00B5C, 0x00B5D, 0x00B5F,
+ 0x00B60, 0x00B61, 0x00B71, 0x00B83, 0x00B85, 0x00B86, 0x00B87, 0x00B88,
+ 0x00B89, 0x00B8A, 0x00B8E, 0x00B8F, 0x00B90, 0x00B92, 0x00B93, 0x00B94,
+ 0x00B95, 0x00B99, 0x00B9A, 0x00B9C, 0x00B9E, 0x00B9F, 0x00BA3, 0x00BA4,
+ 0x00BA8, 0x00BA9, 0x00BAA, 0x00BAE, 0x00BAF, 0x00BB0, 0x00BB1, 0x00BB2,
+ 0x00BB3, 0x00BB4, 0x00BB5, 0x00BB6, 0x00BB7, 0x00BB8, 0x00BB9, 0x00BD0,
+ 0x00C05, 0x00C06, 0x00C07, 0x00C08, 0x00C09, 0x00C0A, 0x00C0B, 0x00C0C,
+ 0x00C0E, 0x00C0F, 0x00C10, 0x00C12, 0x00C13, 0x00C14, 0x00C15, 0x00C16,
+ 0x00C17, 0x00C18, 0x00C19, 0x00C1A, 0x00C1B, 0x00C1C, 0x00C1D, 0x00C1E,
+ 0x00C1F, 0x00C20, 0x00C21, 0x00C22, 0x00C23, 0x00C24, 0x00C25, 0x00C26,
+ 0x00C27, 0x00C28, 0x00C2A, 0x00C2B, 0x00C2C, 0x00C2D, 0x00C2E, 0x00C2F,
+ 0x00C30, 0x00C31, 0x00C32, 0x00C33, 0x00C34, 0x00C35, 0x00C36, 0x00C37,
+ 0x00C38, 0x00C39, 0x00C3D, 0x00C58, 0x00C59, 0x00C5A, 0x00C60, 0x00C61,
+ 0x00C80, 0x00C85, 0x00C86, 0x00C87, 0x00C88, 0x00C89, 0x00C8A, 0x00C8B,
+ 0x00C8C, 0x00C8E, 0x00C8F, 0x00C90, 0x00C92, 0x00C93, 0x00C94, 0x00C95,
+ 0x00C96, 0x00C97, 0x00C98, 0x00C99, 0x00C9A, 0x00C9B, 0x00C9C, 0x00C9D,
+ 0x00C9E, 0x00C9F, 0x00CA0, 0x00CA1, 0x00CA2, 0x00CA3, 0x00CA4, 0x00CA5,
+ 0x00CA6, 0x00CA7, 0x00CA8, 0x00CAA, 0x00CAB, 0x00CAC, 0x00CAD, 0x00CAE,
+ 0x00CAF, 0x00CB0, 0x00CB1, 0x00CB2, 0x00CB3, 0x00CB5, 0x00CB6, 0x00CB7,
+ 0x00CB8, 0x00CB9, 0x00CBD, 0x00CDE, 0x00CE0, 0x00CE1, 0x00CF1, 0x00CF2,
+ 0x00D05, 0x00D06, 0x00D07, 0x00D08, 0x00D09, 0x00D0A, 0x00D0B, 0x00D0C,
+ 0x00D0E, 0x00D0F, 0x00D10, 0x00D12, 0x00D13, 0x00D14, 0x00D15, 0x00D16,
+ 0x00D17, 0x00D18, 0x00D19, 0x00D1A, 0x00D1B, 0x00D1C, 0x00D1D, 0x00D1E,
+ 0x00D1F, 0x00D20, 0x00D21, 0x00D22, 0x00D23, 0x00D24, 0x00D25, 0x00D26,
+ 0x00D27, 0x00D28, 0x00D29, 0x00D2A, 0x00D2B, 0x00D2C, 0x00D2D, 0x00D2E,
+ 0x00D2F, 0x00D30, 0x00D31, 0x00D32, 0x00D33, 0x00D34, 0x00D35, 0x00D36,
+ 0x00D37, 0x00D38, 0x00D39, 0x00D3A, 0x00D3D, 0x00D4E, 0x00D54, 0x00D55,
+ 0x00D56, 0x00D5F, 0x00D60, 0x00D61, 0x00D7A, 0x00D7B, 0x00D7C, 0x00D7D,
+ 0x00D7E, 0x00D7F, 0x00D85, 0x00D86, 0x00D87, 0x00D88, 0x00D89, 0x00D8A,
+ 0x00D8B, 0x00D8C, 0x00D8D, 0x00D8E, 0x00D8F, 0x00D90, 0x00D91, 0x00D92,
+ 0x00D93, 0x00D94, 0x00D95, 0x00D96, 0x00D9A, 0x00D9B, 0x00D9C, 0x00D9D,
+ 0x00D9E, 0x00D9F, 0x00DA0, 0x00DA1, 0x00DA2, 0x00DA3, 0x00DA4, 0x00DA5,
+ 0x00DA6, 0x00DA7, 0x00DA8, 0x00DA9, 0x00DAA, 0x00DAB, 0x00DAC, 0x00DAD,
+ 0x00DAE, 0x00DAF, 0x00DB0, 0x00DB1, 0x00DB3, 0x00DB4, 0x00DB5, 0x00DB6,
+ 0x00DB7, 0x00DB8, 0x00DB9, 0x00DBA, 0x00DBB, 0x00DBD, 0x00DC0, 0x00DC1,
+ 0x00DC2, 0x00DC3, 0x00DC4, 0x00DC5, 0x00DC6, 0x00F00, 0x00F40, 0x00F41,
+ 0x00F42, 0x00F43, 0x00F44, 0x00F45, 0x00F46, 0x00F47, 0x00F49, 0x00F4A,
+ 0x00F4B, 0x00F4C, 0x00F4D, 0x00F4E, 0x00F4F, 0x00F50, 0x00F51, 0x00F52,
+ 0x00F53, 0x00F54, 0x00F55, 0x00F56, 0x00F57, 0x00F58, 0x00F59, 0x00F5A,
+ 0x00F5B, 0x00F5C, 0x00F5D, 0x00F5E, 0x00F5F, 0x00F60, 0x00F61, 0x00F62,
+ 0x00F63, 0x00F64, 0x00F65, 0x00F66, 0x00F67, 0x00F68, 0x00F69, 0x00F6A,
+ 0x00F6B, 0x00F6C, 0x00F88, 0x00F89, 0x00F8A, 0x00F8B, 0x00F8C, 0x010A0,
+ 0x010A1, 0x010A2, 0x010A3, 0x010A4, 0x010A5, 0x010A6, 0x010A7, 0x010A8,
+ 0x010A9, 0x010AA, 0x010AB, 0x010AC, 0x010AD, 0x010AE, 0x010AF, 0x010B0,
+ 0x010B1, 0x010B2, 0x010B3, 0x010B4, 0x010B5, 0x010B6, 0x010B7, 0x010B8,
+ 0x010B9, 0x010BA, 0x010BB, 0x010BC, 0x010BD, 0x010BE, 0x010BF, 0x010C0,
+ 0x010C1, 0x010C2, 0x010C3, 0x010C4, 0x010C5, 0x010C7, 0x010CD, 0x010D0,
+ 0x010D1, 0x010D2, 0x010D3, 0x010D4, 0x010D5, 0x010D6, 0x010D7, 0x010D8,
+ 0x010D9, 0x010DA, 0x010DB, 0x010DC, 0x010DD, 0x010DE, 0x010DF, 0x010E0,
+ 0x010E1, 0x010E2, 0x010E3, 0x010E4, 0x010E5, 0x010E6, 0x010E7, 0x010E8,
+ 0x010E9, 0x010EA, 0x010EB, 0x010EC, 0x010ED, 0x010EE, 0x010EF, 0x010F0,
+ 0x010F1, 0x010F2, 0x010F3, 0x010F4, 0x010F5, 0x010F6, 0x010F7, 0x010F8,
+ 0x010F9, 0x010FA, 0x010FC, 0x010FD, 0x010FE, 0x010FF, 0x01100, 0x01101,
+ 0x01102, 0x01103, 0x01104, 0x01105, 0x01106, 0x01107, 0x01108, 0x01109,
+ 0x0110A, 0x0110B, 0x0110C, 0x0110D, 0x0110E, 0x0110F, 0x01110, 0x01111,
+ 0x01112, 0x01113, 0x01114, 0x01115, 0x01116, 0x01117, 0x01118, 0x01119,
+ 0x0111A, 0x0111B, 0x0111C, 0x0111D, 0x0111E, 0x0111F, 0x01120, 0x01121,
+ 0x01122, 0x01123, 0x01124, 0x01125, 0x01126, 0x01127, 0x01128, 0x01129,
+ 0x0112A, 0x0112B, 0x0112C, 0x0112D, 0x0112E, 0x0112F, 0x01130, 0x01131,
+ 0x01132, 0x01133, 0x01134, 0x01135, 0x01136, 0x01137, 0x01138, 0x01139,
+ 0x0113A, 0x0113B, 0x0113C, 0x0113D, 0x0113E, 0x0113F, 0x01140, 0x01141,
+ 0x01142, 0x01143, 0x01144, 0x01145, 0x01146, 0x01147, 0x01148, 0x01149,
+ 0x0114A, 0x0114B, 0x0114C, 0x0114D, 0x0114E, 0x0114F, 0x01150, 0x01151,
+ 0x01152, 0x01153, 0x01154, 0x01155, 0x01156, 0x01157, 0x01158, 0x01159,
+ 0x0115A, 0x0115B, 0x0115C, 0x0115D, 0x0115E, 0x0115F, 0x01160, 0x01161,
+ 0x01162, 0x01163, 0x01164, 0x01165, 0x01166, 0x01167, 0x01168, 0x01169,
+ 0x0116A, 0x0116B, 0x0116C, 0x0116D, 0x0116E, 0x0116F, 0x01170, 0x01171,
+ 0x01172, 0x01173, 0x01174, 0x01175, 0x01176, 0x01177, 0x01178, 0x01179,
+ 0x0117A, 0x0117B, 0x0117C, 0x0117D, 0x0117E, 0x0117F, 0x01180, 0x01181,
+ 0x01182, 0x01183, 0x01184, 0x01185, 0x01186, 0x01187, 0x01188, 0x01189,
+ 0x0118A, 0x0118B, 0x0118C, 0x0118D, 0x0118E, 0x0118F, 0x01190, 0x01191,
+ 0x01192, 0x01193, 0x01194, 0x01195, 0x01196, 0x01197, 0x01198, 0x01199,
+ 0x0119A, 0x0119B, 0x0119C, 0x0119D, 0x0119E, 0x0119F, 0x011A0, 0x011A1,
+ 0x011A2, 0x011A3, 0x011A4, 0x011A5, 0x011A6, 0x011A7, 0x011A8, 0x011A9,
+ 0x011AA, 0x011AB, 0x011AC, 0x011AD, 0x011AE, 0x011AF, 0x011B0, 0x011B1,
+ 0x011B2, 0x011B3, 0x011B4, 0x011B5, 0x011B6, 0x011B7, 0x011B8, 0x011B9,
+ 0x011BA, 0x011BB, 0x011BC, 0x011BD, 0x011BE, 0x011BF, 0x011C0, 0x011C1,
+ 0x011C2, 0x011C3, 0x011C4, 0x011C5, 0x011C6, 0x011C7, 0x011C8, 0x011C9,
+ 0x011CA, 0x011CB, 0x011CC, 0x011CD, 0x011CE, 0x011CF, 0x011D0, 0x011D1,
+ 0x011D2, 0x011D3, 0x011D4, 0x011D5, 0x011D6, 0x011D7, 0x011D8, 0x011D9,
+ 0x011DA, 0x011DB, 0x011DC, 0x011DD, 0x011DE, 0x011DF, 0x011E0, 0x011E1,
+ 0x011E2, 0x011E3, 0x011E4, 0x011E5, 0x011E6, 0x011E7, 0x011E8, 0x011E9,
+ 0x011EA, 0x011EB, 0x011EC, 0x011ED, 0x011EE, 0x011EF, 0x011F0, 0x011F1,
+ 0x011F2, 0x011F3, 0x011F4, 0x011F5, 0x011F6, 0x011F7, 0x011F8, 0x011F9,
+ 0x011FA, 0x011FB, 0x011FC, 0x011FD, 0x011FE, 0x011FF, 0x01200, 0x01201,
+ 0x01202, 0x01203, 0x01204, 0x01205, 0x01206, 0x01207, 0x01208, 0x01209,
+ 0x0120A, 0x0120B, 0x0120C, 0x0120D, 0x0120E, 0x0120F, 0x01210, 0x01211,
+ 0x01212, 0x01213, 0x01214, 0x01215, 0x01216, 0x01217, 0x01218, 0x01219,
+ 0x0121A, 0x0121B, 0x0121C, 0x0121D, 0x0121E, 0x0121F, 0x01220, 0x01221,
+ 0x01222, 0x01223, 0x01224, 0x01225, 0x01226, 0x01227, 0x01228, 0x01229,
+ 0x0122A, 0x0122B, 0x0122C, 0x0122D, 0x0122E, 0x0122F, 0x01230, 0x01231,
+ 0x01232, 0x01233, 0x01234, 0x01235, 0x01236, 0x01237, 0x01238, 0x01239,
+ 0x0123A, 0x0123B, 0x0123C, 0x0123D, 0x0123E, 0x0123F, 0x01240, 0x01241,
+ 0x01242, 0x01243, 0x01244, 0x01245, 0x01246, 0x01247, 0x01248, 0x0124A,
+ 0x0124B, 0x0124C, 0x0124D, 0x01250, 0x01251, 0x01252, 0x01253, 0x01254,
+ 0x01255, 0x01256, 0x01258, 0x0125A, 0x0125B, 0x0125C, 0x0125D, 0x01260,
+ 0x01261, 0x01262, 0x01263, 0x01264, 0x01265, 0x01266, 0x01267, 0x01268,
+ 0x01269, 0x0126A, 0x0126B, 0x0126C, 0x0126D, 0x0126E, 0x0126F, 0x01270,
+ 0x01271, 0x01272, 0x01273, 0x01274, 0x01275, 0x01276, 0x01277, 0x01278,
+ 0x01279, 0x0127A, 0x0127B, 0x0127C, 0x0127D, 0x0127E, 0x0127F, 0x01280,
+ 0x01281, 0x01282, 0x01283, 0x01284, 0x01285, 0x01286, 0x01287, 0x01288,
+ 0x0128A, 0x0128B, 0x0128C, 0x0128D, 0x01290, 0x01291, 0x01292, 0x01293,
+ 0x01294, 0x01295, 0x01296, 0x01297, 0x01298, 0x01299, 0x0129A, 0x0129B,
+ 0x0129C, 0x0129D, 0x0129E, 0x0129F, 0x012A0, 0x012A1, 0x012A2, 0x012A3,
+ 0x012A4, 0x012A5, 0x012A6, 0x012A7, 0x012A8, 0x012A9, 0x012AA, 0x012AB,
+ 0x012AC, 0x012AD, 0x012AE, 0x012AF, 0x012B0, 0x012B2, 0x012B3, 0x012B4,
+ 0x012B5, 0x012B8, 0x012B9, 0x012BA, 0x012BB, 0x012BC, 0x012BD, 0x012BE,
+ 0x012C0, 0x012C2, 0x012C3, 0x012C4, 0x012C5, 0x012C8, 0x012C9, 0x012CA,
+ 0x012CB, 0x012CC, 0x012CD, 0x012CE, 0x012CF, 0x012D0, 0x012D1, 0x012D2,
+ 0x012D3, 0x012D4, 0x012D5, 0x012D6, 0x012D8, 0x012D9, 0x012DA, 0x012DB,
+ 0x012DC, 0x012DD, 0x012DE, 0x012DF, 0x012E0, 0x012E1, 0x012E2, 0x012E3,
+ 0x012E4, 0x012E5, 0x012E6, 0x012E7, 0x012E8, 0x012E9, 0x012EA, 0x012EB,
+ 0x012EC, 0x012ED, 0x012EE, 0x012EF, 0x012F0, 0x012F1, 0x012F2, 0x012F3,
+ 0x012F4, 0x012F5, 0x012F6, 0x012F7, 0x012F8, 0x012F9, 0x012FA, 0x012FB,
+ 0x012FC, 0x012FD, 0x012FE, 0x012FF, 0x01300, 0x01301, 0x01302, 0x01303,
+ 0x01304, 0x01305, 0x01306, 0x01307, 0x01308, 0x01309, 0x0130A, 0x0130B,
+ 0x0130C, 0x0130D, 0x0130E, 0x0130F, 0x01310, 0x01312, 0x01313, 0x01314,
+ 0x01315, 0x01318, 0x01319, 0x0131A, 0x0131B, 0x0131C, 0x0131D, 0x0131E,
+ 0x0131F, 0x01320, 0x01321, 0x01322, 0x01323, 0x01324, 0x01325, 0x01326,
+ 0x01327, 0x01328, 0x01329, 0x0132A, 0x0132B, 0x0132C, 0x0132D, 0x0132E,
+ 0x0132F, 0x01330, 0x01331, 0x01332, 0x01333, 0x01334, 0x01335, 0x01336,
+ 0x01337, 0x01338, 0x01339, 0x0133A, 0x0133B, 0x0133C, 0x0133D, 0x0133E,
+ 0x0133F, 0x01340, 0x01341, 0x01342, 0x01343, 0x01344, 0x01345, 0x01346,
+ 0x01347, 0x01348, 0x01349, 0x0134A, 0x0134B, 0x0134C, 0x0134D, 0x0134E,
+ 0x0134F, 0x01350, 0x01351, 0x01352, 0x01353, 0x01354, 0x01355, 0x01356,
+ 0x01357, 0x01358, 0x01359, 0x0135A, 0x01380, 0x01381, 0x01382, 0x01383,
+ 0x01384, 0x01385, 0x01386, 0x01387, 0x01388, 0x01389, 0x0138A, 0x0138B,
+ 0x0138C, 0x0138D, 0x0138E, 0x0138F, 0x013A0, 0x013A1, 0x013A2, 0x013A3,
+ 0x013A4, 0x013A5, 0x013A6, 0x013A7, 0x013A8, 0x013A9, 0x013AA, 0x013AB,
+ 0x013AC, 0x013AD, 0x013AE, 0x013AF, 0x013B0, 0x013B1, 0x013B2, 0x013B3,
+ 0x013B4, 0x013B5, 0x013B6, 0x013B7, 0x013B8, 0x013B9, 0x013BA, 0x013BB,
+ 0x013BC, 0x013BD, 0x013BE, 0x013BF, 0x013C0, 0x013C1, 0x013C2, 0x013C3,
+ 0x013C4, 0x013C5, 0x013C6, 0x013C7, 0x013C8, 0x013C9, 0x013CA, 0x013CB,
+ 0x013CC, 0x013CD, 0x013CE, 0x013CF, 0x013D0, 0x013D1, 0x013D2, 0x013D3,
+ 0x013D4, 0x013D5, 0x013D6, 0x013D7, 0x013D8, 0x013D9, 0x013DA, 0x013DB,
+ 0x013DC, 0x013DD, 0x013DE, 0x013DF, 0x013E0, 0x013E1, 0x013E2, 0x013E3,
+ 0x013E4, 0x013E5, 0x013E6, 0x013E7, 0x013E8, 0x013E9, 0x013EA, 0x013EB,
+ 0x013EC, 0x013ED, 0x013EE, 0x013EF, 0x013F0, 0x013F1, 0x013F2, 0x013F3,
+ 0x013F4, 0x013F5, 0x013F8, 0x013F9, 0x013FA, 0x013FB, 0x013FC, 0x013FD,
+ 0x01401, 0x01402, 0x01403, 0x01404, 0x01405, 0x01406, 0x01407, 0x01408,
+ 0x01409, 0x0140A, 0x0140B, 0x0140C, 0x0140D, 0x0140E, 0x0140F, 0x01410,
+ 0x01411, 0x01412, 0x01413, 0x01414, 0x01415, 0x01416, 0x01417, 0x01418,
+ 0x01419, 0x0141A, 0x0141B, 0x0141C, 0x0141D, 0x0141E, 0x0141F, 0x01420,
+ 0x01421, 0x01422, 0x01423, 0x01424, 0x01425, 0x01426, 0x01427, 0x01428,
+ 0x01429, 0x0142A, 0x0142B, 0x0142C, 0x0142D, 0x0142E, 0x0142F, 0x01430,
+ 0x01431, 0x01432, 0x01433, 0x01434, 0x01435, 0x01436, 0x01437, 0x01438,
+ 0x01439, 0x0143A, 0x0143B, 0x0143C, 0x0143D, 0x0143E, 0x0143F, 0x01440,
+ 0x01441, 0x01442, 0x01443, 0x01444, 0x01445, 0x01446, 0x01447, 0x01448,
+ 0x01449, 0x0144A, 0x0144B, 0x0144C, 0x0144D, 0x0144E, 0x0144F, 0x01450,
+ 0x01451, 0x01452, 0x01453, 0x01454, 0x01455, 0x01456, 0x01457, 0x01458,
+ 0x01459, 0x0145A, 0x0145B, 0x0145C, 0x0145D, 0x0145E, 0x0145F, 0x01460,
+ 0x01461, 0x01462, 0x01463, 0x01464, 0x01465, 0x01466, 0x01467, 0x01468,
+ 0x01469, 0x0146A, 0x0146B, 0x0146C, 0x0146D, 0x0146E, 0x0146F, 0x01470,
+ 0x01471, 0x01472, 0x01473, 0x01474, 0x01475, 0x01476, 0x01477, 0x01478,
+ 0x01479, 0x0147A, 0x0147B, 0x0147C, 0x0147D, 0x0147E, 0x0147F, 0x01480,
+ 0x01481, 0x01482, 0x01483, 0x01484, 0x01485, 0x01486, 0x01487, 0x01488,
+ 0x01489, 0x0148A, 0x0148B, 0x0148C, 0x0148D, 0x0148E, 0x0148F, 0x01490,
+ 0x01491, 0x01492, 0x01493, 0x01494, 0x01495, 0x01496, 0x01497, 0x01498,
+ 0x01499, 0x0149A, 0x0149B, 0x0149C, 0x0149D, 0x0149E, 0x0149F, 0x014A0,
+ 0x014A1, 0x014A2, 0x014A3, 0x014A4, 0x014A5, 0x014A6, 0x014A7, 0x014A8,
+ 0x014A9, 0x014AA, 0x014AB, 0x014AC, 0x014AD, 0x014AE, 0x014AF, 0x014B0,
+ 0x014B1, 0x014B2, 0x014B3, 0x014B4, 0x014B5, 0x014B6, 0x014B7, 0x014B8,
+ 0x014B9, 0x014BA, 0x014BB, 0x014BC, 0x014BD, 0x014BE, 0x014BF, 0x014C0,
+ 0x014C1, 0x014C2, 0x014C3, 0x014C4, 0x014C5, 0x014C6, 0x014C7, 0x014C8,
+ 0x014C9, 0x014CA, 0x014CB, 0x014CC, 0x014CD, 0x014CE, 0x014CF, 0x014D0,
+ 0x014D1, 0x014D2, 0x014D3, 0x014D4, 0x014D5, 0x014D6, 0x014D7, 0x014D8,
+ 0x014D9, 0x014DA, 0x014DB, 0x014DC, 0x014DD, 0x014DE, 0x014DF, 0x014E0,
+ 0x014E1, 0x014E2, 0x014E3, 0x014E4, 0x014E5, 0x014E6, 0x014E7, 0x014E8,
+ 0x014E9, 0x014EA, 0x014EB, 0x014EC, 0x014ED, 0x014EE, 0x014EF, 0x014F0,
+ 0x014F1, 0x014F2, 0x014F3, 0x014F4, 0x014F5, 0x014F6, 0x014F7, 0x014F8,
+ 0x014F9, 0x014FA, 0x014FB, 0x014FC, 0x014FD, 0x014FE, 0x014FF, 0x01500,
+ 0x01501, 0x01502, 0x01503, 0x01504, 0x01505, 0x01506, 0x01507, 0x01508,
+ 0x01509, 0x0150A, 0x0150B, 0x0150C, 0x0150D, 0x0150E, 0x0150F, 0x01510,
+ 0x01511, 0x01512, 0x01513, 0x01514, 0x01515, 0x01516, 0x01517, 0x01518,
+ 0x01519, 0x0151A, 0x0151B, 0x0151C, 0x0151D, 0x0151E, 0x0151F, 0x01520,
+ 0x01521, 0x01522, 0x01523, 0x01524, 0x01525, 0x01526, 0x01527, 0x01528,
+ 0x01529, 0x0152A, 0x0152B, 0x0152C, 0x0152D, 0x0152E, 0x0152F, 0x01530,
+ 0x01531, 0x01532, 0x01533, 0x01534, 0x01535, 0x01536, 0x01537, 0x01538,
+ 0x01539, 0x0153A, 0x0153B, 0x0153C, 0x0153D, 0x0153E, 0x0153F, 0x01540,
+ 0x01541, 0x01542, 0x01543, 0x01544, 0x01545, 0x01546, 0x01547, 0x01548,
+ 0x01549, 0x0154A, 0x0154B, 0x0154C, 0x0154D, 0x0154E, 0x0154F, 0x01550,
+ 0x01551, 0x01552, 0x01553, 0x01554, 0x01555, 0x01556, 0x01557, 0x01558,
+ 0x01559, 0x0155A, 0x0155B, 0x0155C, 0x0155D, 0x0155E, 0x0155F, 0x01560,
+ 0x01561, 0x01562, 0x01563, 0x01564, 0x01565, 0x01566, 0x01567, 0x01568,
+ 0x01569, 0x0156A, 0x0156B, 0x0156C, 0x0156D, 0x0156E, 0x0156F, 0x01570,
+ 0x01571, 0x01572, 0x01573, 0x01574, 0x01575, 0x01576, 0x01577, 0x01578,
+ 0x01579, 0x0157A, 0x0157B, 0x0157C, 0x0157D, 0x0157E, 0x0157F, 0x01580,
+ 0x01581, 0x01582, 0x01583, 0x01584, 0x01585, 0x01586, 0x01587, 0x01588,
+ 0x01589, 0x0158A, 0x0158B, 0x0158C, 0x0158D, 0x0158E, 0x0158F, 0x01590,
+ 0x01591, 0x01592, 0x01593, 0x01594, 0x01595, 0x01596, 0x01597, 0x01598,
+ 0x01599, 0x0159A, 0x0159B, 0x0159C, 0x0159D, 0x0159E, 0x0159F, 0x015A0,
+ 0x015A1, 0x015A2, 0x015A3, 0x015A4, 0x015A5, 0x015A6, 0x015A7, 0x015A8,
+ 0x015A9, 0x015AA, 0x015AB, 0x015AC, 0x015AD, 0x015AE, 0x015AF, 0x015B0,
+ 0x015B1, 0x015B2, 0x015B3, 0x015B4, 0x015B5, 0x015B6, 0x015B7, 0x015B8,
+ 0x015B9, 0x015BA, 0x015BB, 0x015BC, 0x015BD, 0x015BE, 0x015BF, 0x015C0,
+ 0x015C1, 0x015C2, 0x015C3, 0x015C4, 0x015C5, 0x015C6, 0x015C7, 0x015C8,
+ 0x015C9, 0x015CA, 0x015CB, 0x015CC, 0x015CD, 0x015CE, 0x015CF, 0x015D0,
+ 0x015D1, 0x015D2, 0x015D3, 0x015D4, 0x015D5, 0x015D6, 0x015D7, 0x015D8,
+ 0x015D9, 0x015DA, 0x015DB, 0x015DC, 0x015DD, 0x015DE, 0x015DF, 0x015E0,
+ 0x015E1, 0x015E2, 0x015E3, 0x015E4, 0x015E5, 0x015E6, 0x015E7, 0x015E8,
+ 0x015E9, 0x015EA, 0x015EB, 0x015EC, 0x015ED, 0x015EE, 0x015EF, 0x015F0,
+ 0x015F1, 0x015F2, 0x015F3, 0x015F4, 0x015F5, 0x015F6, 0x015F7, 0x015F8,
+ 0x015F9, 0x015FA, 0x015FB, 0x015FC, 0x015FD, 0x015FE, 0x015FF, 0x01600,
+ 0x01601, 0x01602, 0x01603, 0x01604, 0x01605, 0x01606, 0x01607, 0x01608,
+ 0x01609, 0x0160A, 0x0160B, 0x0160C, 0x0160D, 0x0160E, 0x0160F, 0x01610,
+ 0x01611, 0x01612, 0x01613, 0x01614, 0x01615, 0x01616, 0x01617, 0x01618,
+ 0x01619, 0x0161A, 0x0161B, 0x0161C, 0x0161D, 0x0161E, 0x0161F, 0x01620,
+ 0x01621, 0x01622, 0x01623, 0x01624, 0x01625, 0x01626, 0x01627, 0x01628,
+ 0x01629, 0x0162A, 0x0162B, 0x0162C, 0x0162D, 0x0162E, 0x0162F, 0x01630,
+ 0x01631, 0x01632, 0x01633, 0x01634, 0x01635, 0x01636, 0x01637, 0x01638,
+ 0x01639, 0x0163A, 0x0163B, 0x0163C, 0x0163D, 0x0163E, 0x0163F, 0x01640,
+ 0x01641, 0x01642, 0x01643, 0x01644, 0x01645, 0x01646, 0x01647, 0x01648,
+ 0x01649, 0x0164A, 0x0164B, 0x0164C, 0x0164D, 0x0164E, 0x0164F, 0x01650,
+ 0x01651, 0x01652, 0x01653, 0x01654, 0x01655, 0x01656, 0x01657, 0x01658,
+ 0x01659, 0x0165A, 0x0165B, 0x0165C, 0x0165D, 0x0165E, 0x0165F, 0x01660,
+ 0x01661, 0x01662, 0x01663, 0x01664, 0x01665, 0x01666, 0x01667, 0x01668,
+ 0x01669, 0x0166A, 0x0166B, 0x0166C, 0x0166F, 0x01670, 0x01671, 0x01672,
+ 0x01673, 0x01674, 0x01675, 0x01676, 0x01677, 0x01678, 0x01679, 0x0167A,
+ 0x0167B, 0x0167C, 0x0167D, 0x0167E, 0x0167F, 0x01681, 0x01682, 0x01683,
+ 0x01684, 0x01685, 0x01686, 0x01687, 0x01688, 0x01689, 0x0168A, 0x0168B,
+ 0x0168C, 0x0168D, 0x0168E, 0x0168F, 0x01690, 0x01691, 0x01692, 0x01693,
+ 0x01694, 0x01695, 0x01696, 0x01697, 0x01698, 0x01699, 0x0169A, 0x016A0,
+ 0x016A1, 0x016A2, 0x016A3, 0x016A4, 0x016A5, 0x016A6, 0x016A7, 0x016A8,
+ 0x016A9, 0x016AA, 0x016AB, 0x016AC, 0x016AD, 0x016AE, 0x016AF, 0x016B0,
+ 0x016B1, 0x016B2, 0x016B3, 0x016B4, 0x016B5, 0x016B6, 0x016B7, 0x016B8,
+ 0x016B9, 0x016BA, 0x016BB, 0x016BC, 0x016BD, 0x016BE, 0x016BF, 0x016C0,
+ 0x016C1, 0x016C2, 0x016C3, 0x016C4, 0x016C5, 0x016C6, 0x016C7, 0x016C8,
+ 0x016C9, 0x016CA, 0x016CB, 0x016CC, 0x016CD, 0x016CE, 0x016CF, 0x016D0,
+ 0x016D1, 0x016D2, 0x016D3, 0x016D4, 0x016D5, 0x016D6, 0x016D7, 0x016D8,
+ 0x016D9, 0x016DA, 0x016DB, 0x016DC, 0x016DD, 0x016DE, 0x016DF, 0x016E0,
+ 0x016E1, 0x016E2, 0x016E3, 0x016E4, 0x016E5, 0x016E6, 0x016E7, 0x016E8,
+ 0x016E9, 0x016EA, 0x016EE, 0x016EF, 0x016F0, 0x016F1, 0x016F2, 0x016F3,
+ 0x016F4, 0x016F5, 0x016F6, 0x016F7, 0x016F8, 0x01700, 0x01701, 0x01702,
+ 0x01703, 0x01704, 0x01705, 0x01706, 0x01707, 0x01708, 0x01709, 0x0170A,
+ 0x0170B, 0x0170C, 0x0170E, 0x0170F, 0x01710, 0x01711, 0x01720, 0x01721,
+ 0x01722, 0x01723, 0x01724, 0x01725, 0x01726, 0x01727, 0x01728, 0x01729,
+ 0x0172A, 0x0172B, 0x0172C, 0x0172D, 0x0172E, 0x0172F, 0x01730, 0x01731,
+ 0x01740, 0x01741, 0x01742, 0x01743, 0x01744, 0x01745, 0x01746, 0x01747,
+ 0x01748, 0x01749, 0x0174A, 0x0174B, 0x0174C, 0x0174D, 0x0174E, 0x0174F,
+ 0x01750, 0x01751, 0x01760, 0x01761, 0x01762, 0x01763, 0x01764, 0x01765,
+ 0x01766, 0x01767, 0x01768, 0x01769, 0x0176A, 0x0176B, 0x0176C, 0x0176E,
+ 0x0176F, 0x01770, 0x01820, 0x01821, 0x01822, 0x01823, 0x01824, 0x01825,
+ 0x01826, 0x01827, 0x01828, 0x01829, 0x0182A, 0x0182B, 0x0182C, 0x0182D,
+ 0x0182E, 0x0182F, 0x01830, 0x01831, 0x01832, 0x01833, 0x01834, 0x01835,
+ 0x01836, 0x01837, 0x01838, 0x01839, 0x0183A, 0x0183B, 0x0183C, 0x0183D,
+ 0x0183E, 0x0183F, 0x01840, 0x01841, 0x01842, 0x01843, 0x01844, 0x01845,
+ 0x01846, 0x01847, 0x01848, 0x01849, 0x0184A, 0x0184B, 0x0184C, 0x0184D,
+ 0x0184E, 0x0184F, 0x01850, 0x01851, 0x01852, 0x01853, 0x01854, 0x01855,
+ 0x01856, 0x01857, 0x01858, 0x01859, 0x0185A, 0x0185B, 0x0185C, 0x0185D,
+ 0x0185E, 0x0185F, 0x01860, 0x01861, 0x01862, 0x01863, 0x01864, 0x01865,
+ 0x01866, 0x01867, 0x01868, 0x01869, 0x0186A, 0x0186B, 0x0186C, 0x0186D,
+ 0x0186E, 0x0186F, 0x01870, 0x01871, 0x01872, 0x01873, 0x01874, 0x01875,
+ 0x01876, 0x01877, 0x01880, 0x01881, 0x01882, 0x01883, 0x01884, 0x01887,
+ 0x01888, 0x01889, 0x0188A, 0x0188B, 0x0188C, 0x0188D, 0x0188E, 0x0188F,
+ 0x01890, 0x01891, 0x01892, 0x01893, 0x01894, 0x01895, 0x01896, 0x01897,
+ 0x01898, 0x01899, 0x0189A, 0x0189B, 0x0189C, 0x0189D, 0x0189E, 0x0189F,
+ 0x018A0, 0x018A1, 0x018A2, 0x018A3, 0x018A4, 0x018A5, 0x018A6, 0x018A7,
+ 0x018A8, 0x018AA, 0x018B0, 0x018B1, 0x018B2, 0x018B3, 0x018B4, 0x018B5,
+ 0x018B6, 0x018B7, 0x018B8, 0x018B9, 0x018BA, 0x018BB, 0x018BC, 0x018BD,
+ 0x018BE, 0x018BF, 0x018C0, 0x018C1, 0x018C2, 0x018C3, 0x018C4, 0x018C5,
+ 0x018C6, 0x018C7, 0x018C8, 0x018C9, 0x018CA, 0x018CB, 0x018CC, 0x018CD,
+ 0x018CE, 0x018CF, 0x018D0, 0x018D1, 0x018D2, 0x018D3, 0x018D4, 0x018D5,
+ 0x018D6, 0x018D7, 0x018D8, 0x018D9, 0x018DA, 0x018DB, 0x018DC, 0x018DD,
+ 0x018DE, 0x018DF, 0x018E0, 0x018E1, 0x018E2, 0x018E3, 0x018E4, 0x018E5,
+ 0x018E6, 0x018E7, 0x018E8, 0x018E9, 0x018EA, 0x018EB, 0x018EC, 0x018ED,
+ 0x018EE, 0x018EF, 0x018F0, 0x018F1, 0x018F2, 0x018F3, 0x018F4, 0x018F5,
+ 0x01900, 0x01901, 0x01902, 0x01903, 0x01904, 0x01905, 0x01906, 0x01907,
+ 0x01908, 0x01909, 0x0190A, 0x0190B, 0x0190C, 0x0190D, 0x0190E, 0x0190F,
+ 0x01910, 0x01911, 0x01912, 0x01913, 0x01914, 0x01915, 0x01916, 0x01917,
+ 0x01918, 0x01919, 0x0191A, 0x0191B, 0x0191C, 0x0191D, 0x0191E, 0x01A00,
+ 0x01A01, 0x01A02, 0x01A03, 0x01A04, 0x01A05, 0x01A06, 0x01A07, 0x01A08,
+ 0x01A09, 0x01A0A, 0x01A0B, 0x01A0C, 0x01A0D, 0x01A0E, 0x01A0F, 0x01A10,
+ 0x01A11, 0x01A12, 0x01A13, 0x01A14, 0x01A15, 0x01A16, 0x01B05, 0x01B06,
+ 0x01B07, 0x01B08, 0x01B09, 0x01B0A, 0x01B0B, 0x01B0C, 0x01B0D, 0x01B0E,
+ 0x01B0F, 0x01B10, 0x01B11, 0x01B12, 0x01B13, 0x01B14, 0x01B15, 0x01B16,
+ 0x01B17, 0x01B18, 0x01B19, 0x01B1A, 0x01B1B, 0x01B1C, 0x01B1D, 0x01B1E,
+ 0x01B1F, 0x01B20, 0x01B21, 0x01B22, 0x01B23, 0x01B24, 0x01B25, 0x01B26,
+ 0x01B27, 0x01B28, 0x01B29, 0x01B2A, 0x01B2B, 0x01B2C, 0x01B2D, 0x01B2E,
+ 0x01B2F, 0x01B30, 0x01B31, 0x01B32, 0x01B33, 0x01B45, 0x01B46, 0x01B47,
+ 0x01B48, 0x01B49, 0x01B4A, 0x01B4B, 0x01B83, 0x01B84, 0x01B85, 0x01B86,
+ 0x01B87, 0x01B88, 0x01B89, 0x01B8A, 0x01B8B, 0x01B8C, 0x01B8D, 0x01B8E,
+ 0x01B8F, 0x01B90, 0x01B91, 0x01B92, 0x01B93, 0x01B94, 0x01B95, 0x01B96,
+ 0x01B97, 0x01B98, 0x01B99, 0x01B9A, 0x01B9B, 0x01B9C, 0x01B9D, 0x01B9E,
+ 0x01B9F, 0x01BA0, 0x01BAE, 0x01BAF, 0x01BBA, 0x01BBB, 0x01BBC, 0x01BBD,
+ 0x01BBE, 0x01BBF, 0x01BC0, 0x01BC1, 0x01BC2, 0x01BC3, 0x01BC4, 0x01BC5,
+ 0x01BC6, 0x01BC7, 0x01BC8, 0x01BC9, 0x01BCA, 0x01BCB, 0x01BCC, 0x01BCD,
+ 0x01BCE, 0x01BCF, 0x01BD0, 0x01BD1, 0x01BD2, 0x01BD3, 0x01BD4, 0x01BD5,
+ 0x01BD6, 0x01BD7, 0x01BD8, 0x01BD9, 0x01BDA, 0x01BDB, 0x01BDC, 0x01BDD,
+ 0x01BDE, 0x01BDF, 0x01BE0, 0x01BE1, 0x01BE2, 0x01BE3, 0x01BE4, 0x01BE5,
+ 0x01C00, 0x01C01, 0x01C02, 0x01C03, 0x01C04, 0x01C05, 0x01C06, 0x01C07,
+ 0x01C08, 0x01C09, 0x01C0A, 0x01C0B, 0x01C0C, 0x01C0D, 0x01C0E, 0x01C0F,
+ 0x01C10, 0x01C11, 0x01C12, 0x01C13, 0x01C14, 0x01C15, 0x01C16, 0x01C17,
+ 0x01C18, 0x01C19, 0x01C1A, 0x01C1B, 0x01C1C, 0x01C1D, 0x01C1E, 0x01C1F,
+ 0x01C20, 0x01C21, 0x01C22, 0x01C23, 0x01C4D, 0x01C4E, 0x01C4F, 0x01C5A,
+ 0x01C5B, 0x01C5C, 0x01C5D, 0x01C5E, 0x01C5F, 0x01C60, 0x01C61, 0x01C62,
+ 0x01C63, 0x01C64, 0x01C65, 0x01C66, 0x01C67, 0x01C68, 0x01C69, 0x01C6A,
+ 0x01C6B, 0x01C6C, 0x01C6D, 0x01C6E, 0x01C6F, 0x01C70, 0x01C71, 0x01C72,
+ 0x01C73, 0x01C74, 0x01C75, 0x01C76, 0x01C77, 0x01C78, 0x01C79, 0x01C7A,
+ 0x01C7B, 0x01C7C, 0x01C7D, 0x01C80, 0x01C81, 0x01C82, 0x01C83, 0x01C84,
+ 0x01C85, 0x01C86, 0x01C87, 0x01C88, 0x01CE9, 0x01CEA, 0x01CEB, 0x01CEC,
+ 0x01CEE, 0x01CEF, 0x01CF0, 0x01CF1, 0x01CF5, 0x01CF6, 0x01D00, 0x01D01,
+ 0x01D02, 0x01D03, 0x01D04, 0x01D05, 0x01D06, 0x01D07, 0x01D08, 0x01D09,
+ 0x01D0A, 0x01D0B, 0x01D0C, 0x01D0D, 0x01D0E, 0x01D0F, 0x01D10, 0x01D11,
+ 0x01D12, 0x01D13, 0x01D14, 0x01D15, 0x01D16, 0x01D17, 0x01D18, 0x01D19,
+ 0x01D1A, 0x01D1B, 0x01D1C, 0x01D1D, 0x01D1E, 0x01D1F, 0x01D20, 0x01D21,
+ 0x01D22, 0x01D23, 0x01D24, 0x01D25, 0x01D26, 0x01D27, 0x01D28, 0x01D29,
+ 0x01D2A, 0x01D2B, 0x01D2C, 0x01D2D, 0x01D2E, 0x01D2F, 0x01D30, 0x01D31,
+ 0x01D32, 0x01D33, 0x01D34, 0x01D35, 0x01D36, 0x01D37, 0x01D38, 0x01D39,
+ 0x01D3A, 0x01D3B, 0x01D3C, 0x01D3D, 0x01D3E, 0x01D3F, 0x01D40, 0x01D41,
+ 0x01D42, 0x01D43, 0x01D44, 0x01D45, 0x01D46, 0x01D47, 0x01D48, 0x01D49,
+ 0x01D4A, 0x01D4B, 0x01D4C, 0x01D4D, 0x01D4E, 0x01D4F, 0x01D50, 0x01D51,
+ 0x01D52, 0x01D53, 0x01D54, 0x01D55, 0x01D56, 0x01D57, 0x01D58, 0x01D59,
+ 0x01D5A, 0x01D5B, 0x01D5C, 0x01D5D, 0x01D5E, 0x01D5F, 0x01D60, 0x01D61,
+ 0x01D62, 0x01D63, 0x01D64, 0x01D65, 0x01D66, 0x01D67, 0x01D68, 0x01D69,
+ 0x01D6A, 0x01D6B, 0x01D6C, 0x01D6D, 0x01D6E, 0x01D6F, 0x01D70, 0x01D71,
+ 0x01D72, 0x01D73, 0x01D74, 0x01D75, 0x01D76, 0x01D77, 0x01D78, 0x01D79,
+ 0x01D7A, 0x01D7B, 0x01D7C, 0x01D7D, 0x01D7E, 0x01D7F, 0x01D80, 0x01D81,
+ 0x01D82, 0x01D83, 0x01D84, 0x01D85, 0x01D86, 0x01D87, 0x01D88, 0x01D89,
+ 0x01D8A, 0x01D8B, 0x01D8C, 0x01D8D, 0x01D8E, 0x01D8F, 0x01D90, 0x01D91,
+ 0x01D92, 0x01D93, 0x01D94, 0x01D95, 0x01D96, 0x01D97, 0x01D98, 0x01D99,
+ 0x01D9A, 0x01D9B, 0x01D9C, 0x01D9D, 0x01D9E, 0x01D9F, 0x01DA0, 0x01DA1,
+ 0x01DA2, 0x01DA3, 0x01DA4, 0x01DA5, 0x01DA6, 0x01DA7, 0x01DA8, 0x01DA9,
+ 0x01DAA, 0x01DAB, 0x01DAC, 0x01DAD, 0x01DAE, 0x01DAF, 0x01DB0, 0x01DB1,
+ 0x01DB2, 0x01DB3, 0x01DB4, 0x01DB5, 0x01DB6, 0x01DB7, 0x01DB8, 0x01DB9,
+ 0x01DBA, 0x01DBB, 0x01DBC, 0x01DBD, 0x01DBE, 0x01DBF, 0x01E00, 0x01E01,
+ 0x01E02, 0x01E03, 0x01E04, 0x01E05, 0x01E06, 0x01E07, 0x01E08, 0x01E09,
+ 0x01E0A, 0x01E0B, 0x01E0C, 0x01E0D, 0x01E0E, 0x01E0F, 0x01E10, 0x01E11,
+ 0x01E12, 0x01E13, 0x01E14, 0x01E15, 0x01E16, 0x01E17, 0x01E18, 0x01E19,
+ 0x01E1A, 0x01E1B, 0x01E1C, 0x01E1D, 0x01E1E, 0x01E1F, 0x01E20, 0x01E21,
+ 0x01E22, 0x01E23, 0x01E24, 0x01E25, 0x01E26, 0x01E27, 0x01E28, 0x01E29,
+ 0x01E2A, 0x01E2B, 0x01E2C, 0x01E2D, 0x01E2E, 0x01E2F, 0x01E30, 0x01E31,
+ 0x01E32, 0x01E33, 0x01E34, 0x01E35, 0x01E36, 0x01E37, 0x01E38, 0x01E39,
+ 0x01E3A, 0x01E3B, 0x01E3C, 0x01E3D, 0x01E3E, 0x01E3F, 0x01E40, 0x01E41,
+ 0x01E42, 0x01E43, 0x01E44, 0x01E45, 0x01E46, 0x01E47, 0x01E48, 0x01E49,
+ 0x01E4A, 0x01E4B, 0x01E4C, 0x01E4D, 0x01E4E, 0x01E4F, 0x01E50, 0x01E51,
+ 0x01E52, 0x01E53, 0x01E54, 0x01E55, 0x01E56, 0x01E57, 0x01E58, 0x01E59,
+ 0x01E5A, 0x01E5B, 0x01E5C, 0x01E5D, 0x01E5E, 0x01E5F, 0x01E60, 0x01E61,
+ 0x01E62, 0x01E63, 0x01E64, 0x01E65, 0x01E66, 0x01E67, 0x01E68, 0x01E69,
+ 0x01E6A, 0x01E6B, 0x01E6C, 0x01E6D, 0x01E6E, 0x01E6F, 0x01E70, 0x01E71,
+ 0x01E72, 0x01E73, 0x01E74, 0x01E75, 0x01E76, 0x01E77, 0x01E78, 0x01E79,
+ 0x01E7A, 0x01E7B, 0x01E7C, 0x01E7D, 0x01E7E, 0x01E7F, 0x01E80, 0x01E81,
+ 0x01E82, 0x01E83, 0x01E84, 0x01E85, 0x01E86, 0x01E87, 0x01E88, 0x01E89,
+ 0x01E8A, 0x01E8B, 0x01E8C, 0x01E8D, 0x01E8E, 0x01E8F, 0x01E90, 0x01E91,
+ 0x01E92, 0x01E93, 0x01E94, 0x01E95, 0x01E96, 0x01E97, 0x01E98, 0x01E99,
+ 0x01E9A, 0x01E9B, 0x01E9C, 0x01E9D, 0x01E9E, 0x01E9F, 0x01EA0, 0x01EA1,
+ 0x01EA2, 0x01EA3, 0x01EA4, 0x01EA5, 0x01EA6, 0x01EA7, 0x01EA8, 0x01EA9,
+ 0x01EAA, 0x01EAB, 0x01EAC, 0x01EAD, 0x01EAE, 0x01EAF, 0x01EB0, 0x01EB1,
+ 0x01EB2, 0x01EB3, 0x01EB4, 0x01EB5, 0x01EB6, 0x01EB7, 0x01EB8, 0x01EB9,
+ 0x01EBA, 0x01EBB, 0x01EBC, 0x01EBD, 0x01EBE, 0x01EBF, 0x01EC0, 0x01EC1,
+ 0x01EC2, 0x01EC3, 0x01EC4, 0x01EC5, 0x01EC6, 0x01EC7, 0x01EC8, 0x01EC9,
+ 0x01ECA, 0x01ECB, 0x01ECC, 0x01ECD, 0x01ECE, 0x01ECF, 0x01ED0, 0x01ED1,
+ 0x01ED2, 0x01ED3, 0x01ED4, 0x01ED5, 0x01ED6, 0x01ED7, 0x01ED8, 0x01ED9,
+ 0x01EDA, 0x01EDB, 0x01EDC, 0x01EDD, 0x01EDE, 0x01EDF, 0x01EE0, 0x01EE1,
+ 0x01EE2, 0x01EE3, 0x01EE4, 0x01EE5, 0x01EE6, 0x01EE7, 0x01EE8, 0x01EE9,
+ 0x01EEA, 0x01EEB, 0x01EEC, 0x01EED, 0x01EEE, 0x01EEF, 0x01EF0, 0x01EF1,
+ 0x01EF2, 0x01EF3, 0x01EF4, 0x01EF5, 0x01EF6, 0x01EF7, 0x01EF8, 0x01EF9,
+ 0x01EFA, 0x01EFB, 0x01EFC, 0x01EFD, 0x01EFE, 0x01EFF, 0x01F00, 0x01F01,
+ 0x01F02, 0x01F03, 0x01F04, 0x01F05, 0x01F06, 0x01F07, 0x01F08, 0x01F09,
+ 0x01F0A, 0x01F0B, 0x01F0C, 0x01F0D, 0x01F0E, 0x01F0F, 0x01F10, 0x01F11,
+ 0x01F12, 0x01F13, 0x01F14, 0x01F15, 0x01F18, 0x01F19, 0x01F1A, 0x01F1B,
+ 0x01F1C, 0x01F1D, 0x01F20, 0x01F21, 0x01F22, 0x01F23, 0x01F24, 0x01F25,
+ 0x01F26, 0x01F27, 0x01F28, 0x01F29, 0x01F2A, 0x01F2B, 0x01F2C, 0x01F2D,
+ 0x01F2E, 0x01F2F, 0x01F30, 0x01F31, 0x01F32, 0x01F33, 0x01F34, 0x01F35,
+ 0x01F36, 0x01F37, 0x01F38, 0x01F39, 0x01F3A, 0x01F3B, 0x01F3C, 0x01F3D,
+ 0x01F3E, 0x01F3F, 0x01F40, 0x01F41, 0x01F42, 0x01F43, 0x01F44, 0x01F45,
+ 0x01F48, 0x01F49, 0x01F4A, 0x01F4B, 0x01F4C, 0x01F4D, 0x01F50, 0x01F51,
+ 0x01F52, 0x01F53, 0x01F54, 0x01F55, 0x01F56, 0x01F57, 0x01F59, 0x01F5B,
+ 0x01F5D, 0x01F5F, 0x01F60, 0x01F61, 0x01F62, 0x01F63, 0x01F64, 0x01F65,
+ 0x01F66, 0x01F67, 0x01F68, 0x01F69, 0x01F6A, 0x01F6B, 0x01F6C, 0x01F6D,
+ 0x01F6E, 0x01F6F, 0x01F70, 0x01F71, 0x01F72, 0x01F73, 0x01F74, 0x01F75,
+ 0x01F76, 0x01F77, 0x01F78, 0x01F79, 0x01F7A, 0x01F7B, 0x01F7C, 0x01F7D,
+ 0x01F80, 0x01F81, 0x01F82, 0x01F83, 0x01F84, 0x01F85, 0x01F86, 0x01F87,
+ 0x01F88, 0x01F89, 0x01F8A, 0x01F8B, 0x01F8C, 0x01F8D, 0x01F8E, 0x01F8F,
+ 0x01F90, 0x01F91, 0x01F92, 0x01F93, 0x01F94, 0x01F95, 0x01F96, 0x01F97,
+ 0x01F98, 0x01F99, 0x01F9A, 0x01F9B, 0x01F9C, 0x01F9D, 0x01F9E, 0x01F9F,
+ 0x01FA0, 0x01FA1, 0x01FA2, 0x01FA3, 0x01FA4, 0x01FA5, 0x01FA6, 0x01FA7,
+ 0x01FA8, 0x01FA9, 0x01FAA, 0x01FAB, 0x01FAC, 0x01FAD, 0x01FAE, 0x01FAF,
+ 0x01FB0, 0x01FB1, 0x01FB2, 0x01FB3, 0x01FB4, 0x01FB6, 0x01FB7, 0x01FB8,
+ 0x01FB9, 0x01FBA, 0x01FBB, 0x01FBC, 0x01FBE, 0x01FC2, 0x01FC3, 0x01FC4,
+ 0x01FC6, 0x01FC7, 0x01FC8, 0x01FC9, 0x01FCA, 0x01FCB, 0x01FCC, 0x01FD0,
+ 0x01FD1, 0x01FD2, 0x01FD3, 0x01FD6, 0x01FD7, 0x01FD8, 0x01FD9, 0x01FDA,
+ 0x01FDB, 0x01FE0, 0x01FE1, 0x01FE2, 0x01FE3, 0x01FE4, 0x01FE5, 0x01FE6,
+ 0x01FE7, 0x01FE8, 0x01FE9, 0x01FEA, 0x01FEB, 0x01FEC, 0x01FF2, 0x01FF3,
+ 0x01FF4, 0x01FF6, 0x01FF7, 0x01FF8, 0x01FF9, 0x01FFA, 0x01FFB, 0x01FFC,
+ 0x02071, 0x0207F, 0x02090, 0x02091, 0x02092, 0x02093, 0x02094, 0x02095,
+ 0x02096, 0x02097, 0x02098, 0x02099, 0x0209A, 0x0209B, 0x0209C, 0x02102,
+ 0x02107, 0x0210A, 0x0210B, 0x0210C, 0x0210D, 0x0210E, 0x0210F, 0x02110,
+ 0x02111, 0x02112, 0x02113, 0x02115, 0x02119, 0x0211A, 0x0211B, 0x0211C,
+ 0x0211D, 0x02124, 0x02126, 0x02128, 0x0212A, 0x0212B, 0x0212C, 0x0212D,
+ 0x0212F, 0x02130, 0x02131, 0x02132, 0x02133, 0x02134, 0x02135, 0x02136,
+ 0x02137, 0x02138, 0x02139, 0x0213C, 0x0213D, 0x0213E, 0x0213F, 0x02145,
+ 0x02146, 0x02147, 0x02148, 0x02149, 0x0214E, 0x02160, 0x02161, 0x02162,
+ 0x02163, 0x02164, 0x02165, 0x02166, 0x02167, 0x02168, 0x02169, 0x0216A,
+ 0x0216B, 0x0216C, 0x0216D, 0x0216E, 0x0216F, 0x02170, 0x02171, 0x02172,
+ 0x02173, 0x02174, 0x02175, 0x02176, 0x02177, 0x02178, 0x02179, 0x0217A,
+ 0x0217B, 0x0217C, 0x0217D, 0x0217E, 0x0217F, 0x02180, 0x02181, 0x02182,
+ 0x02183, 0x02184, 0x02185, 0x02186, 0x02187, 0x02188, 0x024B6, 0x024B7,
+ 0x024B8, 0x024B9, 0x024BA, 0x024BB, 0x024BC, 0x024BD, 0x024BE, 0x024BF,
+ 0x024C0, 0x024C1, 0x024C2, 0x024C3, 0x024C4, 0x024C5, 0x024C6, 0x024C7,
+ 0x024C8, 0x024C9, 0x024CA, 0x024CB, 0x024CC, 0x024CD, 0x024CE, 0x024CF,
+ 0x024D0, 0x024D1, 0x024D2, 0x024D3, 0x024D4, 0x024D5, 0x024D6, 0x024D7,
+ 0x024D8, 0x024D9, 0x024DA, 0x024DB, 0x024DC, 0x024DD, 0x024DE, 0x024DF,
+ 0x024E0, 0x024E1, 0x024E2, 0x024E3, 0x024E4, 0x024E5, 0x024E6, 0x024E7,
+ 0x024E8, 0x024E9, 0x02C00, 0x02C01, 0x02C02, 0x02C03, 0x02C04, 0x02C05,
+ 0x02C06, 0x02C07, 0x02C08, 0x02C09, 0x02C0A, 0x02C0B, 0x02C0C, 0x02C0D,
+ 0x02C0E, 0x02C0F, 0x02C10, 0x02C11, 0x02C12, 0x02C13, 0x02C14, 0x02C15,
+ 0x02C16, 0x02C17, 0x02C18, 0x02C19, 0x02C1A, 0x02C1B, 0x02C1C, 0x02C1D,
+ 0x02C1E, 0x02C1F, 0x02C20, 0x02C21, 0x02C22, 0x02C23, 0x02C24, 0x02C25,
+ 0x02C26, 0x02C27, 0x02C28, 0x02C29, 0x02C2A, 0x02C2B, 0x02C2C, 0x02C2D,
+ 0x02C2E, 0x02C30, 0x02C31, 0x02C32, 0x02C33, 0x02C34, 0x02C35, 0x02C36,
+ 0x02C37, 0x02C38, 0x02C39, 0x02C3A, 0x02C3B, 0x02C3C, 0x02C3D, 0x02C3E,
+ 0x02C3F, 0x02C40, 0x02C41, 0x02C42, 0x02C43, 0x02C44, 0x02C45, 0x02C46,
+ 0x02C47, 0x02C48, 0x02C49, 0x02C4A, 0x02C4B, 0x02C4C, 0x02C4D, 0x02C4E,
+ 0x02C4F, 0x02C50, 0x02C51, 0x02C52, 0x02C53, 0x02C54, 0x02C55, 0x02C56,
+ 0x02C57, 0x02C58, 0x02C59, 0x02C5A, 0x02C5B, 0x02C5C, 0x02C5D, 0x02C5E,
+ 0x02C60, 0x02C61, 0x02C62, 0x02C63, 0x02C64, 0x02C65, 0x02C66, 0x02C67,
+ 0x02C68, 0x02C69, 0x02C6A, 0x02C6B, 0x02C6C, 0x02C6D, 0x02C6E, 0x02C6F,
+ 0x02C70, 0x02C71, 0x02C72, 0x02C73, 0x02C74, 0x02C75, 0x02C76, 0x02C77,
+ 0x02C78, 0x02C79, 0x02C7A, 0x02C7B, 0x02C7C, 0x02C7D, 0x02C7E, 0x02C7F,
+ 0x02C80, 0x02C81, 0x02C82, 0x02C83, 0x02C84, 0x02C85, 0x02C86, 0x02C87,
+ 0x02C88, 0x02C89, 0x02C8A, 0x02C8B, 0x02C8C, 0x02C8D, 0x02C8E, 0x02C8F,
+ 0x02C90, 0x02C91, 0x02C92, 0x02C93, 0x02C94, 0x02C95, 0x02C96, 0x02C97,
+ 0x02C98, 0x02C99, 0x02C9A, 0x02C9B, 0x02C9C, 0x02C9D, 0x02C9E, 0x02C9F,
+ 0x02CA0, 0x02CA1, 0x02CA2, 0x02CA3, 0x02CA4, 0x02CA5, 0x02CA6, 0x02CA7,
+ 0x02CA8, 0x02CA9, 0x02CAA, 0x02CAB, 0x02CAC, 0x02CAD, 0x02CAE, 0x02CAF,
+ 0x02CB0, 0x02CB1, 0x02CB2, 0x02CB3, 0x02CB4, 0x02CB5, 0x02CB6, 0x02CB7,
+ 0x02CB8, 0x02CB9, 0x02CBA, 0x02CBB, 0x02CBC, 0x02CBD, 0x02CBE, 0x02CBF,
+ 0x02CC0, 0x02CC1, 0x02CC2, 0x02CC3, 0x02CC4, 0x02CC5, 0x02CC6, 0x02CC7,
+ 0x02CC8, 0x02CC9, 0x02CCA, 0x02CCB, 0x02CCC, 0x02CCD, 0x02CCE, 0x02CCF,
+ 0x02CD0, 0x02CD1, 0x02CD2, 0x02CD3, 0x02CD4, 0x02CD5, 0x02CD6, 0x02CD7,
+ 0x02CD8, 0x02CD9, 0x02CDA, 0x02CDB, 0x02CDC, 0x02CDD, 0x02CDE, 0x02CDF,
+ 0x02CE0, 0x02CE1, 0x02CE2, 0x02CE3, 0x02CE4, 0x02CEB, 0x02CEC, 0x02CED,
+ 0x02CEE, 0x02CF2, 0x02CF3, 0x02D00, 0x02D01, 0x02D02, 0x02D03, 0x02D04,
+ 0x02D05, 0x02D06, 0x02D07, 0x02D08, 0x02D09, 0x02D0A, 0x02D0B, 0x02D0C,
+ 0x02D0D, 0x02D0E, 0x02D0F, 0x02D10, 0x02D11, 0x02D12, 0x02D13, 0x02D14,
+ 0x02D15, 0x02D16, 0x02D17, 0x02D18, 0x02D19, 0x02D1A, 0x02D1B, 0x02D1C,
+ 0x02D1D, 0x02D1E, 0x02D1F, 0x02D20, 0x02D21, 0x02D22, 0x02D23, 0x02D24,
+ 0x02D25, 0x02D27, 0x02D2D, 0x02D30, 0x02D31, 0x02D32, 0x02D33, 0x02D34,
+ 0x02D35, 0x02D36, 0x02D37, 0x02D38, 0x02D39, 0x02D3A, 0x02D3B, 0x02D3C,
+ 0x02D3D, 0x02D3E, 0x02D3F, 0x02D40, 0x02D41, 0x02D42, 0x02D43, 0x02D44,
+ 0x02D45, 0x02D46, 0x02D47, 0x02D48, 0x02D49, 0x02D4A, 0x02D4B, 0x02D4C,
+ 0x02D4D, 0x02D4E, 0x02D4F, 0x02D50, 0x02D51, 0x02D52, 0x02D53, 0x02D54,
+ 0x02D55, 0x02D56, 0x02D57, 0x02D58, 0x02D59, 0x02D5A, 0x02D5B, 0x02D5C,
+ 0x02D5D, 0x02D5E, 0x02D5F, 0x02D60, 0x02D61, 0x02D62, 0x02D63, 0x02D64,
+ 0x02D65, 0x02D66, 0x02D67, 0x02D6F, 0x02D80, 0x02D81, 0x02D82, 0x02D83,
+ 0x02D84, 0x02D85, 0x02D86, 0x02D87, 0x02D88, 0x02D89, 0x02D8A, 0x02D8B,
+ 0x02D8C, 0x02D8D, 0x02D8E, 0x02D8F, 0x02D90, 0x02D91, 0x02D92, 0x02D93,
+ 0x02D94, 0x02D95, 0x02D96, 0x02DA0, 0x02DA1, 0x02DA2, 0x02DA3, 0x02DA4,
+ 0x02DA5, 0x02DA6, 0x02DA8, 0x02DA9, 0x02DAA, 0x02DAB, 0x02DAC, 0x02DAD,
+ 0x02DAE, 0x02DB0, 0x02DB1, 0x02DB2, 0x02DB3, 0x02DB4, 0x02DB5, 0x02DB6,
+ 0x02DB8, 0x02DB9, 0x02DBA, 0x02DBB, 0x02DBC, 0x02DBD, 0x02DBE, 0x02DC0,
+ 0x02DC1, 0x02DC2, 0x02DC3, 0x02DC4, 0x02DC5, 0x02DC6, 0x02DC8, 0x02DC9,
+ 0x02DCA, 0x02DCB, 0x02DCC, 0x02DCD, 0x02DCE, 0x02DD0, 0x02DD1, 0x02DD2,
+ 0x02DD3, 0x02DD4, 0x02DD5, 0x02DD6, 0x02DD8, 0x02DD9, 0x02DDA, 0x02DDB,
+ 0x02DDC, 0x02DDD, 0x02DDE, 0x02E2F, 0x03005, 0x0303B, 0x0303C, 0x03105,
+ 0x03106, 0x03107, 0x03108, 0x03109, 0x0310A, 0x0310B, 0x0310C, 0x0310D,
+ 0x0310E, 0x0310F, 0x03110, 0x03111, 0x03112, 0x03113, 0x03114, 0x03115,
+ 0x03116, 0x03117, 0x03118, 0x03119, 0x0311A, 0x0311B, 0x0311C, 0x0311D,
+ 0x0311E, 0x0311F, 0x03120, 0x03121, 0x03122, 0x03123, 0x03124, 0x03125,
+ 0x03126, 0x03127, 0x03128, 0x03129, 0x0312A, 0x0312B, 0x0312C, 0x0312D,
+ 0x03131, 0x03132, 0x03133, 0x03134, 0x03135, 0x03136, 0x03137, 0x03138,
+ 0x03139, 0x0313A, 0x0313B, 0x0313C, 0x0313D, 0x0313E, 0x0313F, 0x03140,
+ 0x03141, 0x03142, 0x03143, 0x03144, 0x03145, 0x03146, 0x03147, 0x03148,
+ 0x03149, 0x0314A, 0x0314B, 0x0314C, 0x0314D, 0x0314E, 0x0314F, 0x03150,
+ 0x03151, 0x03152, 0x03153, 0x03154, 0x03155, 0x03156, 0x03157, 0x03158,
+ 0x03159, 0x0315A, 0x0315B, 0x0315C, 0x0315D, 0x0315E, 0x0315F, 0x03160,
+ 0x03161, 0x03162, 0x03163, 0x03164, 0x03165, 0x03166, 0x03167, 0x03168,
+ 0x03169, 0x0316A, 0x0316B, 0x0316C, 0x0316D, 0x0316E, 0x0316F, 0x03170,
+ 0x03171, 0x03172, 0x03173, 0x03174, 0x03175, 0x03176, 0x03177, 0x03178,
+ 0x03179, 0x0317A, 0x0317B, 0x0317C, 0x0317D, 0x0317E, 0x0317F, 0x03180,
+ 0x03181, 0x03182, 0x03183, 0x03184, 0x03185, 0x03186, 0x03187, 0x03188,
+ 0x03189, 0x0318A, 0x0318B, 0x0318C, 0x0318D, 0x0318E, 0x031A0, 0x031A1,
+ 0x031A2, 0x031A3, 0x031A4, 0x031A5, 0x031A6, 0x031A7, 0x031A8, 0x031A9,
+ 0x031AA, 0x031AB, 0x031AC, 0x031AD, 0x031AE, 0x031AF, 0x031B0, 0x031B1,
+ 0x031B2, 0x031B3, 0x031B4, 0x031B5, 0x031B6, 0x031B7, 0x031B8, 0x031B9,
+ 0x031BA, 0x0A000, 0x0A001, 0x0A002, 0x0A003, 0x0A004, 0x0A005, 0x0A006,
+ 0x0A007, 0x0A008, 0x0A009, 0x0A00A, 0x0A00B, 0x0A00C, 0x0A00D, 0x0A00E,
+ 0x0A00F, 0x0A010, 0x0A011, 0x0A012, 0x0A013, 0x0A014, 0x0A015, 0x0A016,
+ 0x0A017, 0x0A018, 0x0A019, 0x0A01A, 0x0A01B, 0x0A01C, 0x0A01D, 0x0A01E,
+ 0x0A01F, 0x0A020, 0x0A021, 0x0A022, 0x0A023, 0x0A024, 0x0A025, 0x0A026,
+ 0x0A027, 0x0A028, 0x0A029, 0x0A02A, 0x0A02B, 0x0A02C, 0x0A02D, 0x0A02E,
+ 0x0A02F, 0x0A030, 0x0A031, 0x0A032, 0x0A033, 0x0A034, 0x0A035, 0x0A036,
+ 0x0A037, 0x0A038, 0x0A039, 0x0A03A, 0x0A03B, 0x0A03C, 0x0A03D, 0x0A03E,
+ 0x0A03F, 0x0A040, 0x0A041, 0x0A042, 0x0A043, 0x0A044, 0x0A045, 0x0A046,
+ 0x0A047, 0x0A048, 0x0A049, 0x0A04A, 0x0A04B, 0x0A04C, 0x0A04D, 0x0A04E,
+ 0x0A04F, 0x0A050, 0x0A051, 0x0A052, 0x0A053, 0x0A054, 0x0A055, 0x0A056,
+ 0x0A057, 0x0A058, 0x0A059, 0x0A05A, 0x0A05B, 0x0A05C, 0x0A05D, 0x0A05E,
+ 0x0A05F, 0x0A060, 0x0A061, 0x0A062, 0x0A063, 0x0A064, 0x0A065, 0x0A066,
+ 0x0A067, 0x0A068, 0x0A069, 0x0A06A, 0x0A06B, 0x0A06C, 0x0A06D, 0x0A06E,
+ 0x0A06F, 0x0A070, 0x0A071, 0x0A072, 0x0A073, 0x0A074, 0x0A075, 0x0A076,
+ 0x0A077, 0x0A078, 0x0A079, 0x0A07A, 0x0A07B, 0x0A07C, 0x0A07D, 0x0A07E,
+ 0x0A07F, 0x0A080, 0x0A081, 0x0A082, 0x0A083, 0x0A084, 0x0A085, 0x0A086,
+ 0x0A087, 0x0A088, 0x0A089, 0x0A08A, 0x0A08B, 0x0A08C, 0x0A08D, 0x0A08E,
+ 0x0A08F, 0x0A090, 0x0A091, 0x0A092, 0x0A093, 0x0A094, 0x0A095, 0x0A096,
+ 0x0A097, 0x0A098, 0x0A099, 0x0A09A, 0x0A09B, 0x0A09C, 0x0A09D, 0x0A09E,
+ 0x0A09F, 0x0A0A0, 0x0A0A1, 0x0A0A2, 0x0A0A3, 0x0A0A4, 0x0A0A5, 0x0A0A6,
+ 0x0A0A7, 0x0A0A8, 0x0A0A9, 0x0A0AA, 0x0A0AB, 0x0A0AC, 0x0A0AD, 0x0A0AE,
+ 0x0A0AF, 0x0A0B0, 0x0A0B1, 0x0A0B2, 0x0A0B3, 0x0A0B4, 0x0A0B5, 0x0A0B6,
+ 0x0A0B7, 0x0A0B8, 0x0A0B9, 0x0A0BA, 0x0A0BB, 0x0A0BC, 0x0A0BD, 0x0A0BE,
+ 0x0A0BF, 0x0A0C0, 0x0A0C1, 0x0A0C2, 0x0A0C3, 0x0A0C4, 0x0A0C5, 0x0A0C6,
+ 0x0A0C7, 0x0A0C8, 0x0A0C9, 0x0A0CA, 0x0A0CB, 0x0A0CC, 0x0A0CD, 0x0A0CE,
+ 0x0A0CF, 0x0A0D0, 0x0A0D1, 0x0A0D2, 0x0A0D3, 0x0A0D4, 0x0A0D5, 0x0A0D6,
+ 0x0A0D7, 0x0A0D8, 0x0A0D9, 0x0A0DA, 0x0A0DB, 0x0A0DC, 0x0A0DD, 0x0A0DE,
+ 0x0A0DF, 0x0A0E0, 0x0A0E1, 0x0A0E2, 0x0A0E3, 0x0A0E4, 0x0A0E5, 0x0A0E6,
+ 0x0A0E7, 0x0A0E8, 0x0A0E9, 0x0A0EA, 0x0A0EB, 0x0A0EC, 0x0A0ED, 0x0A0EE,
+ 0x0A0EF, 0x0A0F0, 0x0A0F1, 0x0A0F2, 0x0A0F3, 0x0A0F4, 0x0A0F5, 0x0A0F6,
+ 0x0A0F7, 0x0A0F8, 0x0A0F9, 0x0A0FA, 0x0A0FB, 0x0A0FC, 0x0A0FD, 0x0A0FE,
+ 0x0A0FF, 0x0A100, 0x0A101, 0x0A102, 0x0A103, 0x0A104, 0x0A105, 0x0A106,
+ 0x0A107, 0x0A108, 0x0A109, 0x0A10A, 0x0A10B, 0x0A10C, 0x0A10D, 0x0A10E,
+ 0x0A10F, 0x0A110, 0x0A111, 0x0A112, 0x0A113, 0x0A114, 0x0A115, 0x0A116,
+ 0x0A117, 0x0A118, 0x0A119, 0x0A11A, 0x0A11B, 0x0A11C, 0x0A11D, 0x0A11E,
+ 0x0A11F, 0x0A120, 0x0A121, 0x0A122, 0x0A123, 0x0A124, 0x0A125, 0x0A126,
+ 0x0A127, 0x0A128, 0x0A129, 0x0A12A, 0x0A12B, 0x0A12C, 0x0A12D, 0x0A12E,
+ 0x0A12F, 0x0A130, 0x0A131, 0x0A132, 0x0A133, 0x0A134, 0x0A135, 0x0A136,
+ 0x0A137, 0x0A138, 0x0A139, 0x0A13A, 0x0A13B, 0x0A13C, 0x0A13D, 0x0A13E,
+ 0x0A13F, 0x0A140, 0x0A141, 0x0A142, 0x0A143, 0x0A144, 0x0A145, 0x0A146,
+ 0x0A147, 0x0A148, 0x0A149, 0x0A14A, 0x0A14B, 0x0A14C, 0x0A14D, 0x0A14E,
+ 0x0A14F, 0x0A150, 0x0A151, 0x0A152, 0x0A153, 0x0A154, 0x0A155, 0x0A156,
+ 0x0A157, 0x0A158, 0x0A159, 0x0A15A, 0x0A15B, 0x0A15C, 0x0A15D, 0x0A15E,
+ 0x0A15F, 0x0A160, 0x0A161, 0x0A162, 0x0A163, 0x0A164, 0x0A165, 0x0A166,
+ 0x0A167, 0x0A168, 0x0A169, 0x0A16A, 0x0A16B, 0x0A16C, 0x0A16D, 0x0A16E,
+ 0x0A16F, 0x0A170, 0x0A171, 0x0A172, 0x0A173, 0x0A174, 0x0A175, 0x0A176,
+ 0x0A177, 0x0A178, 0x0A179, 0x0A17A, 0x0A17B, 0x0A17C, 0x0A17D, 0x0A17E,
+ 0x0A17F, 0x0A180, 0x0A181, 0x0A182, 0x0A183, 0x0A184, 0x0A185, 0x0A186,
+ 0x0A187, 0x0A188, 0x0A189, 0x0A18A, 0x0A18B, 0x0A18C, 0x0A18D, 0x0A18E,
+ 0x0A18F, 0x0A190, 0x0A191, 0x0A192, 0x0A193, 0x0A194, 0x0A195, 0x0A196,
+ 0x0A197, 0x0A198, 0x0A199, 0x0A19A, 0x0A19B, 0x0A19C, 0x0A19D, 0x0A19E,
+ 0x0A19F, 0x0A1A0, 0x0A1A1, 0x0A1A2, 0x0A1A3, 0x0A1A4, 0x0A1A5, 0x0A1A6,
+ 0x0A1A7, 0x0A1A8, 0x0A1A9, 0x0A1AA, 0x0A1AB, 0x0A1AC, 0x0A1AD, 0x0A1AE,
+ 0x0A1AF, 0x0A1B0, 0x0A1B1, 0x0A1B2, 0x0A1B3, 0x0A1B4, 0x0A1B5, 0x0A1B6,
+ 0x0A1B7, 0x0A1B8, 0x0A1B9, 0x0A1BA, 0x0A1BB, 0x0A1BC, 0x0A1BD, 0x0A1BE,
+ 0x0A1BF, 0x0A1C0, 0x0A1C1, 0x0A1C2, 0x0A1C3, 0x0A1C4, 0x0A1C5, 0x0A1C6,
+ 0x0A1C7, 0x0A1C8, 0x0A1C9, 0x0A1CA, 0x0A1CB, 0x0A1CC, 0x0A1CD, 0x0A1CE,
+ 0x0A1CF, 0x0A1D0, 0x0A1D1, 0x0A1D2, 0x0A1D3, 0x0A1D4, 0x0A1D5, 0x0A1D6,
+ 0x0A1D7, 0x0A1D8, 0x0A1D9, 0x0A1DA, 0x0A1DB, 0x0A1DC, 0x0A1DD, 0x0A1DE,
+ 0x0A1DF, 0x0A1E0, 0x0A1E1, 0x0A1E2, 0x0A1E3, 0x0A1E4, 0x0A1E5, 0x0A1E6,
+ 0x0A1E7, 0x0A1E8, 0x0A1E9, 0x0A1EA, 0x0A1EB, 0x0A1EC, 0x0A1ED, 0x0A1EE,
+ 0x0A1EF, 0x0A1F0, 0x0A1F1, 0x0A1F2, 0x0A1F3, 0x0A1F4, 0x0A1F5, 0x0A1F6,
+ 0x0A1F7, 0x0A1F8, 0x0A1F9, 0x0A1FA, 0x0A1FB, 0x0A1FC, 0x0A1FD, 0x0A1FE,
+ 0x0A1FF, 0x0A200, 0x0A201, 0x0A202, 0x0A203, 0x0A204, 0x0A205, 0x0A206,
+ 0x0A207, 0x0A208, 0x0A209, 0x0A20A, 0x0A20B, 0x0A20C, 0x0A20D, 0x0A20E,
+ 0x0A20F, 0x0A210, 0x0A211, 0x0A212, 0x0A213, 0x0A214, 0x0A215, 0x0A216,
+ 0x0A217, 0x0A218, 0x0A219, 0x0A21A, 0x0A21B, 0x0A21C, 0x0A21D, 0x0A21E,
+ 0x0A21F, 0x0A220, 0x0A221, 0x0A222, 0x0A223, 0x0A224, 0x0A225, 0x0A226,
+ 0x0A227, 0x0A228, 0x0A229, 0x0A22A, 0x0A22B, 0x0A22C, 0x0A22D, 0x0A22E,
+ 0x0A22F, 0x0A230, 0x0A231, 0x0A232, 0x0A233, 0x0A234, 0x0A235, 0x0A236,
+ 0x0A237, 0x0A238, 0x0A239, 0x0A23A, 0x0A23B, 0x0A23C, 0x0A23D, 0x0A23E,
+ 0x0A23F, 0x0A240, 0x0A241, 0x0A242, 0x0A243, 0x0A244, 0x0A245, 0x0A246,
+ 0x0A247, 0x0A248, 0x0A249, 0x0A24A, 0x0A24B, 0x0A24C, 0x0A24D, 0x0A24E,
+ 0x0A24F, 0x0A250, 0x0A251, 0x0A252, 0x0A253, 0x0A254, 0x0A255, 0x0A256,
+ 0x0A257, 0x0A258, 0x0A259, 0x0A25A, 0x0A25B, 0x0A25C, 0x0A25D, 0x0A25E,
+ 0x0A25F, 0x0A260, 0x0A261, 0x0A262, 0x0A263, 0x0A264, 0x0A265, 0x0A266,
+ 0x0A267, 0x0A268, 0x0A269, 0x0A26A, 0x0A26B, 0x0A26C, 0x0A26D, 0x0A26E,
+ 0x0A26F, 0x0A270, 0x0A271, 0x0A272, 0x0A273, 0x0A274, 0x0A275, 0x0A276,
+ 0x0A277, 0x0A278, 0x0A279, 0x0A27A, 0x0A27B, 0x0A27C, 0x0A27D, 0x0A27E,
+ 0x0A27F, 0x0A280, 0x0A281, 0x0A282, 0x0A283, 0x0A284, 0x0A285, 0x0A286,
+ 0x0A287, 0x0A288, 0x0A289, 0x0A28A, 0x0A28B, 0x0A28C, 0x0A28D, 0x0A28E,
+ 0x0A28F, 0x0A290, 0x0A291, 0x0A292, 0x0A293, 0x0A294, 0x0A295, 0x0A296,
+ 0x0A297, 0x0A298, 0x0A299, 0x0A29A, 0x0A29B, 0x0A29C, 0x0A29D, 0x0A29E,
+ 0x0A29F, 0x0A2A0, 0x0A2A1, 0x0A2A2, 0x0A2A3, 0x0A2A4, 0x0A2A5, 0x0A2A6,
+ 0x0A2A7, 0x0A2A8, 0x0A2A9, 0x0A2AA, 0x0A2AB, 0x0A2AC, 0x0A2AD, 0x0A2AE,
+ 0x0A2AF, 0x0A2B0, 0x0A2B1, 0x0A2B2, 0x0A2B3, 0x0A2B4, 0x0A2B5, 0x0A2B6,
+ 0x0A2B7, 0x0A2B8, 0x0A2B9, 0x0A2BA, 0x0A2BB, 0x0A2BC, 0x0A2BD, 0x0A2BE,
+ 0x0A2BF, 0x0A2C0, 0x0A2C1, 0x0A2C2, 0x0A2C3, 0x0A2C4, 0x0A2C5, 0x0A2C6,
+ 0x0A2C7, 0x0A2C8, 0x0A2C9, 0x0A2CA, 0x0A2CB, 0x0A2CC, 0x0A2CD, 0x0A2CE,
+ 0x0A2CF, 0x0A2D0, 0x0A2D1, 0x0A2D2, 0x0A2D3, 0x0A2D4, 0x0A2D5, 0x0A2D6,
+ 0x0A2D7, 0x0A2D8, 0x0A2D9, 0x0A2DA, 0x0A2DB, 0x0A2DC, 0x0A2DD, 0x0A2DE,
+ 0x0A2DF, 0x0A2E0, 0x0A2E1, 0x0A2E2, 0x0A2E3, 0x0A2E4, 0x0A2E5, 0x0A2E6,
+ 0x0A2E7, 0x0A2E8, 0x0A2E9, 0x0A2EA, 0x0A2EB, 0x0A2EC, 0x0A2ED, 0x0A2EE,
+ 0x0A2EF, 0x0A2F0, 0x0A2F1, 0x0A2F2, 0x0A2F3, 0x0A2F4, 0x0A2F5, 0x0A2F6,
+ 0x0A2F7, 0x0A2F8, 0x0A2F9, 0x0A2FA, 0x0A2FB, 0x0A2FC, 0x0A2FD, 0x0A2FE,
+ 0x0A2FF, 0x0A300, 0x0A301, 0x0A302, 0x0A303, 0x0A304, 0x0A305, 0x0A306,
+ 0x0A307, 0x0A308, 0x0A309, 0x0A30A, 0x0A30B, 0x0A30C, 0x0A30D, 0x0A30E,
+ 0x0A30F, 0x0A310, 0x0A311, 0x0A312, 0x0A313, 0x0A314, 0x0A315, 0x0A316,
+ 0x0A317, 0x0A318, 0x0A319, 0x0A31A, 0x0A31B, 0x0A31C, 0x0A31D, 0x0A31E,
+ 0x0A31F, 0x0A320, 0x0A321, 0x0A322, 0x0A323, 0x0A324, 0x0A325, 0x0A326,
+ 0x0A327, 0x0A328, 0x0A329, 0x0A32A, 0x0A32B, 0x0A32C, 0x0A32D, 0x0A32E,
+ 0x0A32F, 0x0A330, 0x0A331, 0x0A332, 0x0A333, 0x0A334, 0x0A335, 0x0A336,
+ 0x0A337, 0x0A338, 0x0A339, 0x0A33A, 0x0A33B, 0x0A33C, 0x0A33D, 0x0A33E,
+ 0x0A33F, 0x0A340, 0x0A341, 0x0A342, 0x0A343, 0x0A344, 0x0A345, 0x0A346,
+ 0x0A347, 0x0A348, 0x0A349, 0x0A34A, 0x0A34B, 0x0A34C, 0x0A34D, 0x0A34E,
+ 0x0A34F, 0x0A350, 0x0A351, 0x0A352, 0x0A353, 0x0A354, 0x0A355, 0x0A356,
+ 0x0A357, 0x0A358, 0x0A359, 0x0A35A, 0x0A35B, 0x0A35C, 0x0A35D, 0x0A35E,
+ 0x0A35F, 0x0A360, 0x0A361, 0x0A362, 0x0A363, 0x0A364, 0x0A365, 0x0A366,
+ 0x0A367, 0x0A368, 0x0A369, 0x0A36A, 0x0A36B, 0x0A36C, 0x0A36D, 0x0A36E,
+ 0x0A36F, 0x0A370, 0x0A371, 0x0A372, 0x0A373, 0x0A374, 0x0A375, 0x0A376,
+ 0x0A377, 0x0A378, 0x0A379, 0x0A37A, 0x0A37B, 0x0A37C, 0x0A37D, 0x0A37E,
+ 0x0A37F, 0x0A380, 0x0A381, 0x0A382, 0x0A383, 0x0A384, 0x0A385, 0x0A386,
+ 0x0A387, 0x0A388, 0x0A389, 0x0A38A, 0x0A38B, 0x0A38C, 0x0A38D, 0x0A38E,
+ 0x0A38F, 0x0A390, 0x0A391, 0x0A392, 0x0A393, 0x0A394, 0x0A395, 0x0A396,
+ 0x0A397, 0x0A398, 0x0A399, 0x0A39A, 0x0A39B, 0x0A39C, 0x0A39D, 0x0A39E,
+ 0x0A39F, 0x0A3A0, 0x0A3A1, 0x0A3A2, 0x0A3A3, 0x0A3A4, 0x0A3A5, 0x0A3A6,
+ 0x0A3A7, 0x0A3A8, 0x0A3A9, 0x0A3AA, 0x0A3AB, 0x0A3AC, 0x0A3AD, 0x0A3AE,
+ 0x0A3AF, 0x0A3B0, 0x0A3B1, 0x0A3B2, 0x0A3B3, 0x0A3B4, 0x0A3B5, 0x0A3B6,
+ 0x0A3B7, 0x0A3B8, 0x0A3B9, 0x0A3BA, 0x0A3BB, 0x0A3BC, 0x0A3BD, 0x0A3BE,
+ 0x0A3BF, 0x0A3C0, 0x0A3C1, 0x0A3C2, 0x0A3C3, 0x0A3C4, 0x0A3C5, 0x0A3C6,
+ 0x0A3C7, 0x0A3C8, 0x0A3C9, 0x0A3CA, 0x0A3CB, 0x0A3CC, 0x0A3CD, 0x0A3CE,
+ 0x0A3CF, 0x0A3D0, 0x0A3D1, 0x0A3D2, 0x0A3D3, 0x0A3D4, 0x0A3D5, 0x0A3D6,
+ 0x0A3D7, 0x0A3D8, 0x0A3D9, 0x0A3DA, 0x0A3DB, 0x0A3DC, 0x0A3DD, 0x0A3DE,
+ 0x0A3DF, 0x0A3E0, 0x0A3E1, 0x0A3E2, 0x0A3E3, 0x0A3E4, 0x0A3E5, 0x0A3E6,
+ 0x0A3E7, 0x0A3E8, 0x0A3E9, 0x0A3EA, 0x0A3EB, 0x0A3EC, 0x0A3ED, 0x0A3EE,
+ 0x0A3EF, 0x0A3F0, 0x0A3F1, 0x0A3F2, 0x0A3F3, 0x0A3F4, 0x0A3F5, 0x0A3F6,
+ 0x0A3F7, 0x0A3F8, 0x0A3F9, 0x0A3FA, 0x0A3FB, 0x0A3FC, 0x0A3FD, 0x0A3FE,
+ 0x0A3FF, 0x0A400, 0x0A401, 0x0A402, 0x0A403, 0x0A404, 0x0A405, 0x0A406,
+ 0x0A407, 0x0A408, 0x0A409, 0x0A40A, 0x0A40B, 0x0A40C, 0x0A40D, 0x0A40E,
+ 0x0A40F, 0x0A410, 0x0A411, 0x0A412, 0x0A413, 0x0A414, 0x0A415, 0x0A416,
+ 0x0A417, 0x0A418, 0x0A419, 0x0A41A, 0x0A41B, 0x0A41C, 0x0A41D, 0x0A41E,
+ 0x0A41F, 0x0A420, 0x0A421, 0x0A422, 0x0A423, 0x0A424, 0x0A425, 0x0A426,
+ 0x0A427, 0x0A428, 0x0A429, 0x0A42A, 0x0A42B, 0x0A42C, 0x0A42D, 0x0A42E,
+ 0x0A42F, 0x0A430, 0x0A431, 0x0A432, 0x0A433, 0x0A434, 0x0A435, 0x0A436,
+ 0x0A437, 0x0A438, 0x0A439, 0x0A43A, 0x0A43B, 0x0A43C, 0x0A43D, 0x0A43E,
+ 0x0A43F, 0x0A440, 0x0A441, 0x0A442, 0x0A443, 0x0A444, 0x0A445, 0x0A446,
+ 0x0A447, 0x0A448, 0x0A449, 0x0A44A, 0x0A44B, 0x0A44C, 0x0A44D, 0x0A44E,
+ 0x0A44F, 0x0A450, 0x0A451, 0x0A452, 0x0A453, 0x0A454, 0x0A455, 0x0A456,
+ 0x0A457, 0x0A458, 0x0A459, 0x0A45A, 0x0A45B, 0x0A45C, 0x0A45D, 0x0A45E,
+ 0x0A45F, 0x0A460, 0x0A461, 0x0A462, 0x0A463, 0x0A464, 0x0A465, 0x0A466,
+ 0x0A467, 0x0A468, 0x0A469, 0x0A46A, 0x0A46B, 0x0A46C, 0x0A46D, 0x0A46E,
+ 0x0A46F, 0x0A470, 0x0A471, 0x0A472, 0x0A473, 0x0A474, 0x0A475, 0x0A476,
+ 0x0A477, 0x0A478, 0x0A479, 0x0A47A, 0x0A47B, 0x0A47C, 0x0A47D, 0x0A47E,
+ 0x0A47F, 0x0A480, 0x0A481, 0x0A482, 0x0A483, 0x0A484, 0x0A485, 0x0A486,
+ 0x0A487, 0x0A488, 0x0A489, 0x0A48A, 0x0A48B, 0x0A48C, 0x0A4D0, 0x0A4D1,
+ 0x0A4D2, 0x0A4D3, 0x0A4D4, 0x0A4D5, 0x0A4D6, 0x0A4D7, 0x0A4D8, 0x0A4D9,
+ 0x0A4DA, 0x0A4DB, 0x0A4DC, 0x0A4DD, 0x0A4DE, 0x0A4DF, 0x0A4E0, 0x0A4E1,
+ 0x0A4E2, 0x0A4E3, 0x0A4E4, 0x0A4E5, 0x0A4E6, 0x0A4E7, 0x0A4E8, 0x0A4E9,
+ 0x0A4EA, 0x0A4EB, 0x0A4EC, 0x0A4ED, 0x0A4EE, 0x0A4EF, 0x0A4F0, 0x0A4F1,
+ 0x0A4F2, 0x0A4F3, 0x0A4F4, 0x0A4F5, 0x0A4F6, 0x0A4F7, 0x0A4F8, 0x0A4F9,
+ 0x0A4FA, 0x0A4FB, 0x0A4FC, 0x0A4FD, 0x0A500, 0x0A501, 0x0A502, 0x0A503,
+ 0x0A504, 0x0A505, 0x0A506, 0x0A507, 0x0A508, 0x0A509, 0x0A50A, 0x0A50B,
+ 0x0A50C, 0x0A50D, 0x0A50E, 0x0A50F, 0x0A510, 0x0A511, 0x0A512, 0x0A513,
+ 0x0A514, 0x0A515, 0x0A516, 0x0A517, 0x0A518, 0x0A519, 0x0A51A, 0x0A51B,
+ 0x0A51C, 0x0A51D, 0x0A51E, 0x0A51F, 0x0A520, 0x0A521, 0x0A522, 0x0A523,
+ 0x0A524, 0x0A525, 0x0A526, 0x0A527, 0x0A528, 0x0A529, 0x0A52A, 0x0A52B,
+ 0x0A52C, 0x0A52D, 0x0A52E, 0x0A52F, 0x0A530, 0x0A531, 0x0A532, 0x0A533,
+ 0x0A534, 0x0A535, 0x0A536, 0x0A537, 0x0A538, 0x0A539, 0x0A53A, 0x0A53B,
+ 0x0A53C, 0x0A53D, 0x0A53E, 0x0A53F, 0x0A540, 0x0A541, 0x0A542, 0x0A543,
+ 0x0A544, 0x0A545, 0x0A546, 0x0A547, 0x0A548, 0x0A549, 0x0A54A, 0x0A54B,
+ 0x0A54C, 0x0A54D, 0x0A54E, 0x0A54F, 0x0A550, 0x0A551, 0x0A552, 0x0A553,
+ 0x0A554, 0x0A555, 0x0A556, 0x0A557, 0x0A558, 0x0A559, 0x0A55A, 0x0A55B,
+ 0x0A55C, 0x0A55D, 0x0A55E, 0x0A55F, 0x0A560, 0x0A561, 0x0A562, 0x0A563,
+ 0x0A564, 0x0A565, 0x0A566, 0x0A567, 0x0A568, 0x0A569, 0x0A56A, 0x0A56B,
+ 0x0A56C, 0x0A56D, 0x0A56E, 0x0A56F, 0x0A570, 0x0A571, 0x0A572, 0x0A573,
+ 0x0A574, 0x0A575, 0x0A576, 0x0A577, 0x0A578, 0x0A579, 0x0A57A, 0x0A57B,
+ 0x0A57C, 0x0A57D, 0x0A57E, 0x0A57F, 0x0A580, 0x0A581, 0x0A582, 0x0A583,
+ 0x0A584, 0x0A585, 0x0A586, 0x0A587, 0x0A588, 0x0A589, 0x0A58A, 0x0A58B,
+ 0x0A58C, 0x0A58D, 0x0A58E, 0x0A58F, 0x0A590, 0x0A591, 0x0A592, 0x0A593,
+ 0x0A594, 0x0A595, 0x0A596, 0x0A597, 0x0A598, 0x0A599, 0x0A59A, 0x0A59B,
+ 0x0A59C, 0x0A59D, 0x0A59E, 0x0A59F, 0x0A5A0, 0x0A5A1, 0x0A5A2, 0x0A5A3,
+ 0x0A5A4, 0x0A5A5, 0x0A5A6, 0x0A5A7, 0x0A5A8, 0x0A5A9, 0x0A5AA, 0x0A5AB,
+ 0x0A5AC, 0x0A5AD, 0x0A5AE, 0x0A5AF, 0x0A5B0, 0x0A5B1, 0x0A5B2, 0x0A5B3,
+ 0x0A5B4, 0x0A5B5, 0x0A5B6, 0x0A5B7, 0x0A5B8, 0x0A5B9, 0x0A5BA, 0x0A5BB,
+ 0x0A5BC, 0x0A5BD, 0x0A5BE, 0x0A5BF, 0x0A5C0, 0x0A5C1, 0x0A5C2, 0x0A5C3,
+ 0x0A5C4, 0x0A5C5, 0x0A5C6, 0x0A5C7, 0x0A5C8, 0x0A5C9, 0x0A5CA, 0x0A5CB,
+ 0x0A5CC, 0x0A5CD, 0x0A5CE, 0x0A5CF, 0x0A5D0, 0x0A5D1, 0x0A5D2, 0x0A5D3,
+ 0x0A5D4, 0x0A5D5, 0x0A5D6, 0x0A5D7, 0x0A5D8, 0x0A5D9, 0x0A5DA, 0x0A5DB,
+ 0x0A5DC, 0x0A5DD, 0x0A5DE, 0x0A5DF, 0x0A5E0, 0x0A5E1, 0x0A5E2, 0x0A5E3,
+ 0x0A5E4, 0x0A5E5, 0x0A5E6, 0x0A5E7, 0x0A5E8, 0x0A5E9, 0x0A5EA, 0x0A5EB,
+ 0x0A5EC, 0x0A5ED, 0x0A5EE, 0x0A5EF, 0x0A5F0, 0x0A5F1, 0x0A5F2, 0x0A5F3,
+ 0x0A5F4, 0x0A5F5, 0x0A5F6, 0x0A5F7, 0x0A5F8, 0x0A5F9, 0x0A5FA, 0x0A5FB,
+ 0x0A5FC, 0x0A5FD, 0x0A5FE, 0x0A5FF, 0x0A600, 0x0A601, 0x0A602, 0x0A603,
+ 0x0A604, 0x0A605, 0x0A606, 0x0A607, 0x0A608, 0x0A609, 0x0A60A, 0x0A60B,
+ 0x0A60C, 0x0A610, 0x0A611, 0x0A612, 0x0A613, 0x0A614, 0x0A615, 0x0A616,
+ 0x0A617, 0x0A618, 0x0A619, 0x0A61A, 0x0A61B, 0x0A61C, 0x0A61D, 0x0A61E,
+ 0x0A61F, 0x0A62A, 0x0A62B, 0x0A640, 0x0A641, 0x0A642, 0x0A643, 0x0A644,
+ 0x0A645, 0x0A646, 0x0A647, 0x0A648, 0x0A649, 0x0A64A, 0x0A64B, 0x0A64C,
+ 0x0A64D, 0x0A64E, 0x0A64F, 0x0A650, 0x0A651, 0x0A652, 0x0A653, 0x0A654,
+ 0x0A655, 0x0A656, 0x0A657, 0x0A658, 0x0A659, 0x0A65A, 0x0A65B, 0x0A65C,
+ 0x0A65D, 0x0A65E, 0x0A65F, 0x0A660, 0x0A661, 0x0A662, 0x0A663, 0x0A664,
+ 0x0A665, 0x0A666, 0x0A667, 0x0A668, 0x0A669, 0x0A66A, 0x0A66B, 0x0A66C,
+ 0x0A66D, 0x0A66E, 0x0A67F, 0x0A680, 0x0A681, 0x0A682, 0x0A683, 0x0A684,
+ 0x0A685, 0x0A686, 0x0A687, 0x0A688, 0x0A689, 0x0A68A, 0x0A68B, 0x0A68C,
+ 0x0A68D, 0x0A68E, 0x0A68F, 0x0A690, 0x0A691, 0x0A692, 0x0A693, 0x0A694,
+ 0x0A695, 0x0A696, 0x0A697, 0x0A698, 0x0A699, 0x0A69A, 0x0A69B, 0x0A69C,
+ 0x0A69D, 0x0A6A0, 0x0A6A1, 0x0A6A2, 0x0A6A3, 0x0A6A4, 0x0A6A5, 0x0A6A6,
+ 0x0A6A7, 0x0A6A8, 0x0A6A9, 0x0A6AA, 0x0A6AB, 0x0A6AC, 0x0A6AD, 0x0A6AE,
+ 0x0A6AF, 0x0A6B0, 0x0A6B1, 0x0A6B2, 0x0A6B3, 0x0A6B4, 0x0A6B5, 0x0A6B6,
+ 0x0A6B7, 0x0A6B8, 0x0A6B9, 0x0A6BA, 0x0A6BB, 0x0A6BC, 0x0A6BD, 0x0A6BE,
+ 0x0A6BF, 0x0A6C0, 0x0A6C1, 0x0A6C2, 0x0A6C3, 0x0A6C4, 0x0A6C5, 0x0A6C6,
+ 0x0A6C7, 0x0A6C8, 0x0A6C9, 0x0A6CA, 0x0A6CB, 0x0A6CC, 0x0A6CD, 0x0A6CE,
+ 0x0A6CF, 0x0A6D0, 0x0A6D1, 0x0A6D2, 0x0A6D3, 0x0A6D4, 0x0A6D5, 0x0A6D6,
+ 0x0A6D7, 0x0A6D8, 0x0A6D9, 0x0A6DA, 0x0A6DB, 0x0A6DC, 0x0A6DD, 0x0A6DE,
+ 0x0A6DF, 0x0A6E0, 0x0A6E1, 0x0A6E2, 0x0A6E3, 0x0A6E4, 0x0A6E5, 0x0A6E6,
+ 0x0A6E7, 0x0A6E8, 0x0A6E9, 0x0A6EA, 0x0A6EB, 0x0A6EC, 0x0A6ED, 0x0A6EE,
+ 0x0A6EF, 0x0A717, 0x0A718, 0x0A719, 0x0A71A, 0x0A71B, 0x0A71C, 0x0A71D,
+ 0x0A71E, 0x0A71F, 0x0A722, 0x0A723, 0x0A724, 0x0A725, 0x0A726, 0x0A727,
+ 0x0A728, 0x0A729, 0x0A72A, 0x0A72B, 0x0A72C, 0x0A72D, 0x0A72E, 0x0A72F,
+ 0x0A730, 0x0A731, 0x0A732, 0x0A733, 0x0A734, 0x0A735, 0x0A736, 0x0A737,
+ 0x0A738, 0x0A739, 0x0A73A, 0x0A73B, 0x0A73C, 0x0A73D, 0x0A73E, 0x0A73F,
+ 0x0A740, 0x0A741, 0x0A742, 0x0A743, 0x0A744, 0x0A745, 0x0A746, 0x0A747,
+ 0x0A748, 0x0A749, 0x0A74A, 0x0A74B, 0x0A74C, 0x0A74D, 0x0A74E, 0x0A74F,
+ 0x0A750, 0x0A751, 0x0A752, 0x0A753, 0x0A754, 0x0A755, 0x0A756, 0x0A757,
+ 0x0A758, 0x0A759, 0x0A75A, 0x0A75B, 0x0A75C, 0x0A75D, 0x0A75E, 0x0A75F,
+ 0x0A760, 0x0A761, 0x0A762, 0x0A763, 0x0A764, 0x0A765, 0x0A766, 0x0A767,
+ 0x0A768, 0x0A769, 0x0A76A, 0x0A76B, 0x0A76C, 0x0A76D, 0x0A76E, 0x0A76F,
+ 0x0A770, 0x0A771, 0x0A772, 0x0A773, 0x0A774, 0x0A775, 0x0A776, 0x0A777,
+ 0x0A778, 0x0A779, 0x0A77A, 0x0A77B, 0x0A77C, 0x0A77D, 0x0A77E, 0x0A77F,
+ 0x0A780, 0x0A781, 0x0A782, 0x0A783, 0x0A784, 0x0A785, 0x0A786, 0x0A787,
+ 0x0A788, 0x0A78B, 0x0A78C, 0x0A78D, 0x0A78E, 0x0A78F, 0x0A790, 0x0A791,
+ 0x0A792, 0x0A793, 0x0A794, 0x0A795, 0x0A796, 0x0A797, 0x0A798, 0x0A799,
+ 0x0A79A, 0x0A79B, 0x0A79C, 0x0A79D, 0x0A79E, 0x0A79F, 0x0A7A0, 0x0A7A1,
+ 0x0A7A2, 0x0A7A3, 0x0A7A4, 0x0A7A5, 0x0A7A6, 0x0A7A7, 0x0A7A8, 0x0A7A9,
+ 0x0A7AA, 0x0A7AB, 0x0A7AC, 0x0A7AD, 0x0A7AE, 0x0A7B0, 0x0A7B1, 0x0A7B2,
+ 0x0A7B3, 0x0A7B4, 0x0A7B5, 0x0A7B6, 0x0A7B7, 0x0A7F7, 0x0A7F8, 0x0A7F9,
+ 0x0A7FA, 0x0A7FB, 0x0A7FC, 0x0A7FD, 0x0A7FE, 0x0A7FF, 0x0A800, 0x0A801,
+ 0x0A803, 0x0A804, 0x0A805, 0x0A807, 0x0A808, 0x0A809, 0x0A80A, 0x0A80C,
+ 0x0A80D, 0x0A80E, 0x0A80F, 0x0A810, 0x0A811, 0x0A812, 0x0A813, 0x0A814,
+ 0x0A815, 0x0A816, 0x0A817, 0x0A818, 0x0A819, 0x0A81A, 0x0A81B, 0x0A81C,
+ 0x0A81D, 0x0A81E, 0x0A81F, 0x0A820, 0x0A821, 0x0A822, 0x0A840, 0x0A841,
+ 0x0A842, 0x0A843, 0x0A844, 0x0A845, 0x0A846, 0x0A847, 0x0A848, 0x0A849,
+ 0x0A84A, 0x0A84B, 0x0A84C, 0x0A84D, 0x0A84E, 0x0A84F, 0x0A850, 0x0A851,
+ 0x0A852, 0x0A853, 0x0A854, 0x0A855, 0x0A856, 0x0A857, 0x0A858, 0x0A859,
+ 0x0A85A, 0x0A85B, 0x0A85C, 0x0A85D, 0x0A85E, 0x0A85F, 0x0A860, 0x0A861,
+ 0x0A862, 0x0A863, 0x0A864, 0x0A865, 0x0A866, 0x0A867, 0x0A868, 0x0A869,
+ 0x0A86A, 0x0A86B, 0x0A86C, 0x0A86D, 0x0A86E, 0x0A86F, 0x0A870, 0x0A871,
+ 0x0A872, 0x0A873, 0x0A882, 0x0A883, 0x0A884, 0x0A885, 0x0A886, 0x0A887,
+ 0x0A888, 0x0A889, 0x0A88A, 0x0A88B, 0x0A88C, 0x0A88D, 0x0A88E, 0x0A88F,
+ 0x0A890, 0x0A891, 0x0A892, 0x0A893, 0x0A894, 0x0A895, 0x0A896, 0x0A897,
+ 0x0A898, 0x0A899, 0x0A89A, 0x0A89B, 0x0A89C, 0x0A89D, 0x0A89E, 0x0A89F,
+ 0x0A8A0, 0x0A8A1, 0x0A8A2, 0x0A8A3, 0x0A8A4, 0x0A8A5, 0x0A8A6, 0x0A8A7,
+ 0x0A8A8, 0x0A8A9, 0x0A8AA, 0x0A8AB, 0x0A8AC, 0x0A8AD, 0x0A8AE, 0x0A8AF,
+ 0x0A8B0, 0x0A8B1, 0x0A8B2, 0x0A8B3, 0x0A8F2, 0x0A8F3, 0x0A8F4, 0x0A8F5,
+ 0x0A8F6, 0x0A8F7, 0x0A8FB, 0x0A8FD, 0x0A90A, 0x0A90B, 0x0A90C, 0x0A90D,
+ 0x0A90E, 0x0A90F, 0x0A910, 0x0A911, 0x0A912, 0x0A913, 0x0A914, 0x0A915,
+ 0x0A916, 0x0A917, 0x0A918, 0x0A919, 0x0A91A, 0x0A91B, 0x0A91C, 0x0A91D,
+ 0x0A91E, 0x0A91F, 0x0A920, 0x0A921, 0x0A922, 0x0A923, 0x0A924, 0x0A925,
+ 0x0A930, 0x0A931, 0x0A932, 0x0A933, 0x0A934, 0x0A935, 0x0A936, 0x0A937,
+ 0x0A938, 0x0A939, 0x0A93A, 0x0A93B, 0x0A93C, 0x0A93D, 0x0A93E, 0x0A93F,
+ 0x0A940, 0x0A941, 0x0A942, 0x0A943, 0x0A944, 0x0A945, 0x0A946, 0x0A960,
+ 0x0A961, 0x0A962, 0x0A963, 0x0A964, 0x0A965, 0x0A966, 0x0A967, 0x0A968,
+ 0x0A969, 0x0A96A, 0x0A96B, 0x0A96C, 0x0A96D, 0x0A96E, 0x0A96F, 0x0A970,
+ 0x0A971, 0x0A972, 0x0A973, 0x0A974, 0x0A975, 0x0A976, 0x0A977, 0x0A978,
+ 0x0A979, 0x0A97A, 0x0A97B, 0x0A97C, 0x0A984, 0x0A985, 0x0A986, 0x0A987,
+ 0x0A988, 0x0A989, 0x0A98A, 0x0A98B, 0x0A98C, 0x0A98D, 0x0A98E, 0x0A98F,
+ 0x0A990, 0x0A991, 0x0A992, 0x0A993, 0x0A994, 0x0A995, 0x0A996, 0x0A997,
+ 0x0A998, 0x0A999, 0x0A99A, 0x0A99B, 0x0A99C, 0x0A99D, 0x0A99E, 0x0A99F,
+ 0x0A9A0, 0x0A9A1, 0x0A9A2, 0x0A9A3, 0x0A9A4, 0x0A9A5, 0x0A9A6, 0x0A9A7,
+ 0x0A9A8, 0x0A9A9, 0x0A9AA, 0x0A9AB, 0x0A9AC, 0x0A9AD, 0x0A9AE, 0x0A9AF,
+ 0x0A9B0, 0x0A9B1, 0x0A9B2, 0x0A9CF, 0x0AA00, 0x0AA01, 0x0AA02, 0x0AA03,
+ 0x0AA04, 0x0AA05, 0x0AA06, 0x0AA07, 0x0AA08, 0x0AA09, 0x0AA0A, 0x0AA0B,
+ 0x0AA0C, 0x0AA0D, 0x0AA0E, 0x0AA0F, 0x0AA10, 0x0AA11, 0x0AA12, 0x0AA13,
+ 0x0AA14, 0x0AA15, 0x0AA16, 0x0AA17, 0x0AA18, 0x0AA19, 0x0AA1A, 0x0AA1B,
+ 0x0AA1C, 0x0AA1D, 0x0AA1E, 0x0AA1F, 0x0AA20, 0x0AA21, 0x0AA22, 0x0AA23,
+ 0x0AA24, 0x0AA25, 0x0AA26, 0x0AA27, 0x0AA28, 0x0AA40, 0x0AA41, 0x0AA42,
+ 0x0AA44, 0x0AA45, 0x0AA46, 0x0AA47, 0x0AA48, 0x0AA49, 0x0AA4A, 0x0AA4B,
+ 0x0AAE0, 0x0AAE1, 0x0AAE2, 0x0AAE3, 0x0AAE4, 0x0AAE5, 0x0AAE6, 0x0AAE7,
+ 0x0AAE8, 0x0AAE9, 0x0AAEA, 0x0AAF2, 0x0AAF3, 0x0AAF4, 0x0AB01, 0x0AB02,
+ 0x0AB03, 0x0AB04, 0x0AB05, 0x0AB06, 0x0AB09, 0x0AB0A, 0x0AB0B, 0x0AB0C,
+ 0x0AB0D, 0x0AB0E, 0x0AB11, 0x0AB12, 0x0AB13, 0x0AB14, 0x0AB15, 0x0AB16,
+ 0x0AB20, 0x0AB21, 0x0AB22, 0x0AB23, 0x0AB24, 0x0AB25, 0x0AB26, 0x0AB28,
+ 0x0AB29, 0x0AB2A, 0x0AB2B, 0x0AB2C, 0x0AB2D, 0x0AB2E, 0x0AB30, 0x0AB31,
+ 0x0AB32, 0x0AB33, 0x0AB34, 0x0AB35, 0x0AB36, 0x0AB37, 0x0AB38, 0x0AB39,
+ 0x0AB3A, 0x0AB3B, 0x0AB3C, 0x0AB3D, 0x0AB3E, 0x0AB3F, 0x0AB40, 0x0AB41,
+ 0x0AB42, 0x0AB43, 0x0AB44, 0x0AB45, 0x0AB46, 0x0AB47, 0x0AB48, 0x0AB49,
+ 0x0AB4A, 0x0AB4B, 0x0AB4C, 0x0AB4D, 0x0AB4E, 0x0AB4F, 0x0AB50, 0x0AB51,
+ 0x0AB52, 0x0AB53, 0x0AB54, 0x0AB55, 0x0AB56, 0x0AB57, 0x0AB58, 0x0AB59,
+ 0x0AB5A, 0x0AB5C, 0x0AB5D, 0x0AB5E, 0x0AB5F, 0x0AB60, 0x0AB61, 0x0AB62,
+ 0x0AB63, 0x0AB64, 0x0AB65, 0x0AB70, 0x0AB71, 0x0AB72, 0x0AB73, 0x0AB74,
+ 0x0AB75, 0x0AB76, 0x0AB77, 0x0AB78, 0x0AB79, 0x0AB7A, 0x0AB7B, 0x0AB7C,
+ 0x0AB7D, 0x0AB7E, 0x0AB7F, 0x0AB80, 0x0AB81, 0x0AB82, 0x0AB83, 0x0AB84,
+ 0x0AB85, 0x0AB86, 0x0AB87, 0x0AB88, 0x0AB89, 0x0AB8A, 0x0AB8B, 0x0AB8C,
+ 0x0AB8D, 0x0AB8E, 0x0AB8F, 0x0AB90, 0x0AB91, 0x0AB92, 0x0AB93, 0x0AB94,
+ 0x0AB95, 0x0AB96, 0x0AB97, 0x0AB98, 0x0AB99, 0x0AB9A, 0x0AB9B, 0x0AB9C,
+ 0x0AB9D, 0x0AB9E, 0x0AB9F, 0x0ABA0, 0x0ABA1, 0x0ABA2, 0x0ABA3, 0x0ABA4,
+ 0x0ABA5, 0x0ABA6, 0x0ABA7, 0x0ABA8, 0x0ABA9, 0x0ABAA, 0x0ABAB, 0x0ABAC,
+ 0x0ABAD, 0x0ABAE, 0x0ABAF, 0x0ABB0, 0x0ABB1, 0x0ABB2, 0x0ABB3, 0x0ABB4,
+ 0x0ABB5, 0x0ABB6, 0x0ABB7, 0x0ABB8, 0x0ABB9, 0x0ABBA, 0x0ABBB, 0x0ABBC,
+ 0x0ABBD, 0x0ABBE, 0x0ABBF, 0x0ABC0, 0x0ABC1, 0x0ABC2, 0x0ABC3, 0x0ABC4,
+ 0x0ABC5, 0x0ABC6, 0x0ABC7, 0x0ABC8, 0x0ABC9, 0x0ABCA, 0x0ABCB, 0x0ABCC,
+ 0x0ABCD, 0x0ABCE, 0x0ABCF, 0x0ABD0, 0x0ABD1, 0x0ABD2, 0x0ABD3, 0x0ABD4,
+ 0x0ABD5, 0x0ABD6, 0x0ABD7, 0x0ABD8, 0x0ABD9, 0x0ABDA, 0x0ABDB, 0x0ABDC,
+ 0x0ABDD, 0x0ABDE, 0x0ABDF, 0x0ABE0, 0x0ABE1, 0x0ABE2, 0x0AC00, 0x0AC01,
+ 0x0AC02, 0x0AC03, 0x0AC04, 0x0AC05, 0x0AC06, 0x0AC07, 0x0AC08, 0x0AC09,
+ 0x0AC0A, 0x0AC0B, 0x0AC0C, 0x0AC0D, 0x0AC0E, 0x0AC0F, 0x0AC10, 0x0AC11,
+ 0x0AC12, 0x0AC13, 0x0AC14, 0x0AC15, 0x0AC16, 0x0AC17, 0x0AC18, 0x0AC19,
+ 0x0AC1A, 0x0AC1B, 0x0AC1C, 0x0AC1D, 0x0AC1E, 0x0AC1F, 0x0AC20, 0x0AC21,
+ 0x0AC22, 0x0AC23, 0x0AC24, 0x0AC25, 0x0AC26, 0x0AC27, 0x0AC28, 0x0AC29,
+ 0x0AC2A, 0x0AC2B, 0x0AC2C, 0x0AC2D, 0x0AC2E, 0x0AC2F, 0x0AC30, 0x0AC31,
+ 0x0AC32, 0x0AC33, 0x0AC34, 0x0AC35, 0x0AC36, 0x0AC37, 0x0AC38, 0x0AC39,
+ 0x0AC3A, 0x0AC3B, 0x0AC3C, 0x0AC3D, 0x0AC3E, 0x0AC3F, 0x0AC40, 0x0AC41,
+ 0x0AC42, 0x0AC43, 0x0AC44, 0x0AC45, 0x0AC46, 0x0AC47, 0x0AC48, 0x0AC49,
+ 0x0AC4A, 0x0AC4B, 0x0AC4C, 0x0AC4D, 0x0AC4E, 0x0AC4F, 0x0AC50, 0x0AC51,
+ 0x0AC52, 0x0AC53, 0x0AC54, 0x0AC55, 0x0AC56, 0x0AC57, 0x0AC58, 0x0AC59,
+ 0x0AC5A, 0x0AC5B, 0x0AC5C, 0x0AC5D, 0x0AC5E, 0x0AC5F, 0x0AC60, 0x0AC61,
+ 0x0AC62, 0x0AC63, 0x0AC64, 0x0AC65, 0x0AC66, 0x0AC67, 0x0AC68, 0x0AC69,
+ 0x0AC6A, 0x0AC6B, 0x0AC6C, 0x0AC6D, 0x0AC6E, 0x0AC6F, 0x0AC70, 0x0AC71,
+ 0x0AC72, 0x0AC73, 0x0AC74, 0x0AC75, 0x0AC76, 0x0AC77, 0x0AC78, 0x0AC79,
+ 0x0AC7A, 0x0AC7B, 0x0AC7C, 0x0AC7D, 0x0AC7E, 0x0AC7F, 0x0AC80, 0x0AC81,
+ 0x0AC82, 0x0AC83, 0x0AC84, 0x0AC85, 0x0AC86, 0x0AC87, 0x0AC88, 0x0AC89,
+ 0x0AC8A, 0x0AC8B, 0x0AC8C, 0x0AC8D, 0x0AC8E, 0x0AC8F, 0x0AC90, 0x0AC91,
+ 0x0AC92, 0x0AC93, 0x0AC94, 0x0AC95, 0x0AC96, 0x0AC97, 0x0AC98, 0x0AC99,
+ 0x0AC9A, 0x0AC9B, 0x0AC9C, 0x0AC9D, 0x0AC9E, 0x0AC9F, 0x0ACA0, 0x0ACA1,
+ 0x0ACA2, 0x0ACA3, 0x0ACA4, 0x0ACA5, 0x0ACA6, 0x0ACA7, 0x0ACA8, 0x0ACA9,
+ 0x0ACAA, 0x0ACAB, 0x0ACAC, 0x0ACAD, 0x0ACAE, 0x0ACAF, 0x0ACB0, 0x0ACB1,
+ 0x0ACB2, 0x0ACB3, 0x0ACB4, 0x0ACB5, 0x0ACB6, 0x0ACB7, 0x0ACB8, 0x0ACB9,
+ 0x0ACBA, 0x0ACBB, 0x0ACBC, 0x0ACBD, 0x0ACBE, 0x0ACBF, 0x0ACC0, 0x0ACC1,
+ 0x0ACC2, 0x0ACC3, 0x0ACC4, 0x0ACC5, 0x0ACC6, 0x0ACC7, 0x0ACC8, 0x0ACC9,
+ 0x0ACCA, 0x0ACCB, 0x0ACCC, 0x0ACCD, 0x0ACCE, 0x0ACCF, 0x0ACD0, 0x0ACD1,
+ 0x0ACD2, 0x0ACD3, 0x0ACD4, 0x0ACD5, 0x0ACD6, 0x0ACD7, 0x0ACD8, 0x0ACD9,
+ 0x0ACDA, 0x0ACDB, 0x0ACDC, 0x0ACDD, 0x0ACDE, 0x0ACDF, 0x0ACE0, 0x0ACE1,
+ 0x0ACE2, 0x0ACE3, 0x0ACE4, 0x0ACE5, 0x0ACE6, 0x0ACE7, 0x0ACE8, 0x0ACE9,
+ 0x0ACEA, 0x0ACEB, 0x0ACEC, 0x0ACED, 0x0ACEE, 0x0ACEF, 0x0ACF0, 0x0ACF1,
+ 0x0ACF2, 0x0ACF3, 0x0ACF4, 0x0ACF5, 0x0ACF6, 0x0ACF7, 0x0ACF8, 0x0ACF9,
+ 0x0ACFA, 0x0ACFB, 0x0ACFC, 0x0ACFD, 0x0ACFE, 0x0ACFF, 0x0AD00, 0x0AD01,
+ 0x0AD02, 0x0AD03, 0x0AD04, 0x0AD05, 0x0AD06, 0x0AD07, 0x0AD08, 0x0AD09,
+ 0x0AD0A, 0x0AD0B, 0x0AD0C, 0x0AD0D, 0x0AD0E, 0x0AD0F, 0x0AD10, 0x0AD11,
+ 0x0AD12, 0x0AD13, 0x0AD14, 0x0AD15, 0x0AD16, 0x0AD17, 0x0AD18, 0x0AD19,
+ 0x0AD1A, 0x0AD1B, 0x0AD1C, 0x0AD1D, 0x0AD1E, 0x0AD1F, 0x0AD20, 0x0AD21,
+ 0x0AD22, 0x0AD23, 0x0AD24, 0x0AD25, 0x0AD26, 0x0AD27, 0x0AD28, 0x0AD29,
+ 0x0AD2A, 0x0AD2B, 0x0AD2C, 0x0AD2D, 0x0AD2E, 0x0AD2F, 0x0AD30, 0x0AD31,
+ 0x0AD32, 0x0AD33, 0x0AD34, 0x0AD35, 0x0AD36, 0x0AD37, 0x0AD38, 0x0AD39,
+ 0x0AD3A, 0x0AD3B, 0x0AD3C, 0x0AD3D, 0x0AD3E, 0x0AD3F, 0x0AD40, 0x0AD41,
+ 0x0AD42, 0x0AD43, 0x0AD44, 0x0AD45, 0x0AD46, 0x0AD47, 0x0AD48, 0x0AD49,
+ 0x0AD4A, 0x0AD4B, 0x0AD4C, 0x0AD4D, 0x0AD4E, 0x0AD4F, 0x0AD50, 0x0AD51,
+ 0x0AD52, 0x0AD53, 0x0AD54, 0x0AD55, 0x0AD56, 0x0AD57, 0x0AD58, 0x0AD59,
+ 0x0AD5A, 0x0AD5B, 0x0AD5C, 0x0AD5D, 0x0AD5E, 0x0AD5F, 0x0AD60, 0x0AD61,
+ 0x0AD62, 0x0AD63, 0x0AD64, 0x0AD65, 0x0AD66, 0x0AD67, 0x0AD68, 0x0AD69,
+ 0x0AD6A, 0x0AD6B, 0x0AD6C, 0x0AD6D, 0x0AD6E, 0x0AD6F, 0x0AD70, 0x0AD71,
+ 0x0AD72, 0x0AD73, 0x0AD74, 0x0AD75, 0x0AD76, 0x0AD77, 0x0AD78, 0x0AD79,
+ 0x0AD7A, 0x0AD7B, 0x0AD7C, 0x0AD7D, 0x0AD7E, 0x0AD7F, 0x0AD80, 0x0AD81,
+ 0x0AD82, 0x0AD83, 0x0AD84, 0x0AD85, 0x0AD86, 0x0AD87, 0x0AD88, 0x0AD89,
+ 0x0AD8A, 0x0AD8B, 0x0AD8C, 0x0AD8D, 0x0AD8E, 0x0AD8F, 0x0AD90, 0x0AD91,
+ 0x0AD92, 0x0AD93, 0x0AD94, 0x0AD95, 0x0AD96, 0x0AD97, 0x0AD98, 0x0AD99,
+ 0x0AD9A, 0x0AD9B, 0x0AD9C, 0x0AD9D, 0x0AD9E, 0x0AD9F, 0x0ADA0, 0x0ADA1,
+ 0x0ADA2, 0x0ADA3, 0x0ADA4, 0x0ADA5, 0x0ADA6, 0x0ADA7, 0x0ADA8, 0x0ADA9,
+ 0x0ADAA, 0x0ADAB, 0x0ADAC, 0x0ADAD, 0x0ADAE, 0x0ADAF, 0x0ADB0, 0x0ADB1,
+ 0x0ADB2, 0x0ADB3, 0x0ADB4, 0x0ADB5, 0x0ADB6, 0x0ADB7, 0x0ADB8, 0x0ADB9,
+ 0x0ADBA, 0x0ADBB, 0x0ADBC, 0x0ADBD, 0x0ADBE, 0x0ADBF, 0x0ADC0, 0x0ADC1,
+ 0x0ADC2, 0x0ADC3, 0x0ADC4, 0x0ADC5, 0x0ADC6, 0x0ADC7, 0x0ADC8, 0x0ADC9,
+ 0x0ADCA, 0x0ADCB, 0x0ADCC, 0x0ADCD, 0x0ADCE, 0x0ADCF, 0x0ADD0, 0x0ADD1,
+ 0x0ADD2, 0x0ADD3, 0x0ADD4, 0x0ADD5, 0x0ADD6, 0x0ADD7, 0x0ADD8, 0x0ADD9,
+ 0x0ADDA, 0x0ADDB, 0x0ADDC, 0x0ADDD, 0x0ADDE, 0x0ADDF, 0x0ADE0, 0x0ADE1,
+ 0x0ADE2, 0x0ADE3, 0x0ADE4, 0x0ADE5, 0x0ADE6, 0x0ADE7, 0x0ADE8, 0x0ADE9,
+ 0x0ADEA, 0x0ADEB, 0x0ADEC, 0x0ADED, 0x0ADEE, 0x0ADEF, 0x0ADF0, 0x0ADF1,
+ 0x0ADF2, 0x0ADF3, 0x0ADF4, 0x0ADF5, 0x0ADF6, 0x0ADF7, 0x0ADF8, 0x0ADF9,
+ 0x0ADFA, 0x0ADFB, 0x0ADFC, 0x0ADFD, 0x0ADFE, 0x0ADFF, 0x0AE00, 0x0AE01,
+ 0x0AE02, 0x0AE03, 0x0AE04, 0x0AE05, 0x0AE06, 0x0AE07, 0x0AE08, 0x0AE09,
+ 0x0AE0A, 0x0AE0B, 0x0AE0C, 0x0AE0D, 0x0AE0E, 0x0AE0F, 0x0AE10, 0x0AE11,
+ 0x0AE12, 0x0AE13, 0x0AE14, 0x0AE15, 0x0AE16, 0x0AE17, 0x0AE18, 0x0AE19,
+ 0x0AE1A, 0x0AE1B, 0x0AE1C, 0x0AE1D, 0x0AE1E, 0x0AE1F, 0x0AE20, 0x0AE21,
+ 0x0AE22, 0x0AE23, 0x0AE24, 0x0AE25, 0x0AE26, 0x0AE27, 0x0AE28, 0x0AE29,
+ 0x0AE2A, 0x0AE2B, 0x0AE2C, 0x0AE2D, 0x0AE2E, 0x0AE2F, 0x0AE30, 0x0AE31,
+ 0x0AE32, 0x0AE33, 0x0AE34, 0x0AE35, 0x0AE36, 0x0AE37, 0x0AE38, 0x0AE39,
+ 0x0AE3A, 0x0AE3B, 0x0AE3C, 0x0AE3D, 0x0AE3E, 0x0AE3F, 0x0AE40, 0x0AE41,
+ 0x0AE42, 0x0AE43, 0x0AE44, 0x0AE45, 0x0AE46, 0x0AE47, 0x0AE48, 0x0AE49,
+ 0x0AE4A, 0x0AE4B, 0x0AE4C, 0x0AE4D, 0x0AE4E, 0x0AE4F, 0x0AE50, 0x0AE51,
+ 0x0AE52, 0x0AE53, 0x0AE54, 0x0AE55, 0x0AE56, 0x0AE57, 0x0AE58, 0x0AE59,
+ 0x0AE5A, 0x0AE5B, 0x0AE5C, 0x0AE5D, 0x0AE5E, 0x0AE5F, 0x0AE60, 0x0AE61,
+ 0x0AE62, 0x0AE63, 0x0AE64, 0x0AE65, 0x0AE66, 0x0AE67, 0x0AE68, 0x0AE69,
+ 0x0AE6A, 0x0AE6B, 0x0AE6C, 0x0AE6D, 0x0AE6E, 0x0AE6F, 0x0AE70, 0x0AE71,
+ 0x0AE72, 0x0AE73, 0x0AE74, 0x0AE75, 0x0AE76, 0x0AE77, 0x0AE78, 0x0AE79,
+ 0x0AE7A, 0x0AE7B, 0x0AE7C, 0x0AE7D, 0x0AE7E, 0x0AE7F, 0x0AE80, 0x0AE81,
+ 0x0AE82, 0x0AE83, 0x0AE84, 0x0AE85, 0x0AE86, 0x0AE87, 0x0AE88, 0x0AE89,
+ 0x0AE8A, 0x0AE8B, 0x0AE8C, 0x0AE8D, 0x0AE8E, 0x0AE8F, 0x0AE90, 0x0AE91,
+ 0x0AE92, 0x0AE93, 0x0AE94, 0x0AE95, 0x0AE96, 0x0AE97, 0x0AE98, 0x0AE99,
+ 0x0AE9A, 0x0AE9B, 0x0AE9C, 0x0AE9D, 0x0AE9E, 0x0AE9F, 0x0AEA0, 0x0AEA1,
+ 0x0AEA2, 0x0AEA3, 0x0AEA4, 0x0AEA5, 0x0AEA6, 0x0AEA7, 0x0AEA8, 0x0AEA9,
+ 0x0AEAA, 0x0AEAB, 0x0AEAC, 0x0AEAD, 0x0AEAE, 0x0AEAF, 0x0AEB0, 0x0AEB1,
+ 0x0AEB2, 0x0AEB3, 0x0AEB4, 0x0AEB5, 0x0AEB6, 0x0AEB7, 0x0AEB8, 0x0AEB9,
+ 0x0AEBA, 0x0AEBB, 0x0AEBC, 0x0AEBD, 0x0AEBE, 0x0AEBF, 0x0AEC0, 0x0AEC1,
+ 0x0AEC2, 0x0AEC3, 0x0AEC4, 0x0AEC5, 0x0AEC6, 0x0AEC7, 0x0AEC8, 0x0AEC9,
+ 0x0AECA, 0x0AECB, 0x0AECC, 0x0AECD, 0x0AECE, 0x0AECF, 0x0AED0, 0x0AED1,
+ 0x0AED2, 0x0AED3, 0x0AED4, 0x0AED5, 0x0AED6, 0x0AED7, 0x0AED8, 0x0AED9,
+ 0x0AEDA, 0x0AEDB, 0x0AEDC, 0x0AEDD, 0x0AEDE, 0x0AEDF, 0x0AEE0, 0x0AEE1,
+ 0x0AEE2, 0x0AEE3, 0x0AEE4, 0x0AEE5, 0x0AEE6, 0x0AEE7, 0x0AEE8, 0x0AEE9,
+ 0x0AEEA, 0x0AEEB, 0x0AEEC, 0x0AEED, 0x0AEEE, 0x0AEEF, 0x0AEF0, 0x0AEF1,
+ 0x0AEF2, 0x0AEF3, 0x0AEF4, 0x0AEF5, 0x0AEF6, 0x0AEF7, 0x0AEF8, 0x0AEF9,
+ 0x0AEFA, 0x0AEFB, 0x0AEFC, 0x0AEFD, 0x0AEFE, 0x0AEFF, 0x0AF00, 0x0AF01,
+ 0x0AF02, 0x0AF03, 0x0AF04, 0x0AF05, 0x0AF06, 0x0AF07, 0x0AF08, 0x0AF09,
+ 0x0AF0A, 0x0AF0B, 0x0AF0C, 0x0AF0D, 0x0AF0E, 0x0AF0F, 0x0AF10, 0x0AF11,
+ 0x0AF12, 0x0AF13, 0x0AF14, 0x0AF15, 0x0AF16, 0x0AF17, 0x0AF18, 0x0AF19,
+ 0x0AF1A, 0x0AF1B, 0x0AF1C, 0x0AF1D, 0x0AF1E, 0x0AF1F, 0x0AF20, 0x0AF21,
+ 0x0AF22, 0x0AF23, 0x0AF24, 0x0AF25, 0x0AF26, 0x0AF27, 0x0AF28, 0x0AF29,
+ 0x0AF2A, 0x0AF2B, 0x0AF2C, 0x0AF2D, 0x0AF2E, 0x0AF2F, 0x0AF30, 0x0AF31,
+ 0x0AF32, 0x0AF33, 0x0AF34, 0x0AF35, 0x0AF36, 0x0AF37, 0x0AF38, 0x0AF39,
+ 0x0AF3A, 0x0AF3B, 0x0AF3C, 0x0AF3D, 0x0AF3E, 0x0AF3F, 0x0AF40, 0x0AF41,
+ 0x0AF42, 0x0AF43, 0x0AF44, 0x0AF45, 0x0AF46, 0x0AF47, 0x0AF48, 0x0AF49,
+ 0x0AF4A, 0x0AF4B, 0x0AF4C, 0x0AF4D, 0x0AF4E, 0x0AF4F, 0x0AF50, 0x0AF51,
+ 0x0AF52, 0x0AF53, 0x0AF54, 0x0AF55, 0x0AF56, 0x0AF57, 0x0AF58, 0x0AF59,
+ 0x0AF5A, 0x0AF5B, 0x0AF5C, 0x0AF5D, 0x0AF5E, 0x0AF5F, 0x0AF60, 0x0AF61,
+ 0x0AF62, 0x0AF63, 0x0AF64, 0x0AF65, 0x0AF66, 0x0AF67, 0x0AF68, 0x0AF69,
+ 0x0AF6A, 0x0AF6B, 0x0AF6C, 0x0AF6D, 0x0AF6E, 0x0AF6F, 0x0AF70, 0x0AF71,
+ 0x0AF72, 0x0AF73, 0x0AF74, 0x0AF75, 0x0AF76, 0x0AF77, 0x0AF78, 0x0AF79,
+ 0x0AF7A, 0x0AF7B, 0x0AF7C, 0x0AF7D, 0x0AF7E, 0x0AF7F, 0x0AF80, 0x0AF81,
+ 0x0AF82, 0x0AF83, 0x0AF84, 0x0AF85, 0x0AF86, 0x0AF87, 0x0AF88, 0x0AF89,
+ 0x0AF8A, 0x0AF8B, 0x0AF8C, 0x0AF8D, 0x0AF8E, 0x0AF8F, 0x0AF90, 0x0AF91,
+ 0x0AF92, 0x0AF93, 0x0AF94, 0x0AF95, 0x0AF96, 0x0AF97, 0x0AF98, 0x0AF99,
+ 0x0AF9A, 0x0AF9B, 0x0AF9C, 0x0AF9D, 0x0AF9E, 0x0AF9F, 0x0AFA0, 0x0AFA1,
+ 0x0AFA2, 0x0AFA3, 0x0AFA4, 0x0AFA5, 0x0AFA6, 0x0AFA7, 0x0AFA8, 0x0AFA9,
+ 0x0AFAA, 0x0AFAB, 0x0AFAC, 0x0AFAD, 0x0AFAE, 0x0AFAF, 0x0AFB0, 0x0AFB1,
+ 0x0AFB2, 0x0AFB3, 0x0AFB4, 0x0AFB5, 0x0AFB6, 0x0AFB7, 0x0AFB8, 0x0AFB9,
+ 0x0AFBA, 0x0AFBB, 0x0AFBC, 0x0AFBD, 0x0AFBE, 0x0AFBF, 0x0AFC0, 0x0AFC1,
+ 0x0AFC2, 0x0AFC3, 0x0AFC4, 0x0AFC5, 0x0AFC6, 0x0AFC7, 0x0AFC8, 0x0AFC9,
+ 0x0AFCA, 0x0AFCB, 0x0AFCC, 0x0AFCD, 0x0AFCE, 0x0AFCF, 0x0AFD0, 0x0AFD1,
+ 0x0AFD2, 0x0AFD3, 0x0AFD4, 0x0AFD5, 0x0AFD6, 0x0AFD7, 0x0AFD8, 0x0AFD9,
+ 0x0AFDA, 0x0AFDB, 0x0AFDC, 0x0AFDD, 0x0AFDE, 0x0AFDF, 0x0AFE0, 0x0AFE1,
+ 0x0AFE2, 0x0AFE3, 0x0AFE4, 0x0AFE5, 0x0AFE6, 0x0AFE7, 0x0AFE8, 0x0AFE9,
+ 0x0AFEA, 0x0AFEB, 0x0AFEC, 0x0AFED, 0x0AFEE, 0x0AFEF, 0x0AFF0, 0x0AFF1,
+ 0x0AFF2, 0x0AFF3, 0x0AFF4, 0x0AFF5, 0x0AFF6, 0x0AFF7, 0x0AFF8, 0x0AFF9,
+ 0x0AFFA, 0x0AFFB, 0x0AFFC, 0x0AFFD, 0x0AFFE, 0x0AFFF, 0x0B000, 0x0B001,
+ 0x0B002, 0x0B003, 0x0B004, 0x0B005, 0x0B006, 0x0B007, 0x0B008, 0x0B009,
+ 0x0B00A, 0x0B00B, 0x0B00C, 0x0B00D, 0x0B00E, 0x0B00F, 0x0B010, 0x0B011,
+ 0x0B012, 0x0B013, 0x0B014, 0x0B015, 0x0B016, 0x0B017, 0x0B018, 0x0B019,
+ 0x0B01A, 0x0B01B, 0x0B01C, 0x0B01D, 0x0B01E, 0x0B01F, 0x0B020, 0x0B021,
+ 0x0B022, 0x0B023, 0x0B024, 0x0B025, 0x0B026, 0x0B027, 0x0B028, 0x0B029,
+ 0x0B02A, 0x0B02B, 0x0B02C, 0x0B02D, 0x0B02E, 0x0B02F, 0x0B030, 0x0B031,
+ 0x0B032, 0x0B033, 0x0B034, 0x0B035, 0x0B036, 0x0B037, 0x0B038, 0x0B039,
+ 0x0B03A, 0x0B03B, 0x0B03C, 0x0B03D, 0x0B03E, 0x0B03F, 0x0B040, 0x0B041,
+ 0x0B042, 0x0B043, 0x0B044, 0x0B045, 0x0B046, 0x0B047, 0x0B048, 0x0B049,
+ 0x0B04A, 0x0B04B, 0x0B04C, 0x0B04D, 0x0B04E, 0x0B04F, 0x0B050, 0x0B051,
+ 0x0B052, 0x0B053, 0x0B054, 0x0B055, 0x0B056, 0x0B057, 0x0B058, 0x0B059,
+ 0x0B05A, 0x0B05B, 0x0B05C, 0x0B05D, 0x0B05E, 0x0B05F, 0x0B060, 0x0B061,
+ 0x0B062, 0x0B063, 0x0B064, 0x0B065, 0x0B066, 0x0B067, 0x0B068, 0x0B069,
+ 0x0B06A, 0x0B06B, 0x0B06C, 0x0B06D, 0x0B06E, 0x0B06F, 0x0B070, 0x0B071,
+ 0x0B072, 0x0B073, 0x0B074, 0x0B075, 0x0B076, 0x0B077, 0x0B078, 0x0B079,
+ 0x0B07A, 0x0B07B, 0x0B07C, 0x0B07D, 0x0B07E, 0x0B07F, 0x0B080, 0x0B081,
+ 0x0B082, 0x0B083, 0x0B084, 0x0B085, 0x0B086, 0x0B087, 0x0B088, 0x0B089,
+ 0x0B08A, 0x0B08B, 0x0B08C, 0x0B08D, 0x0B08E, 0x0B08F, 0x0B090, 0x0B091,
+ 0x0B092, 0x0B093, 0x0B094, 0x0B095, 0x0B096, 0x0B097, 0x0B098, 0x0B099,
+ 0x0B09A, 0x0B09B, 0x0B09C, 0x0B09D, 0x0B09E, 0x0B09F, 0x0B0A0, 0x0B0A1,
+ 0x0B0A2, 0x0B0A3, 0x0B0A4, 0x0B0A5, 0x0B0A6, 0x0B0A7, 0x0B0A8, 0x0B0A9,
+ 0x0B0AA, 0x0B0AB, 0x0B0AC, 0x0B0AD, 0x0B0AE, 0x0B0AF, 0x0B0B0, 0x0B0B1,
+ 0x0B0B2, 0x0B0B3, 0x0B0B4, 0x0B0B5, 0x0B0B6, 0x0B0B7, 0x0B0B8, 0x0B0B9,
+ 0x0B0BA, 0x0B0BB, 0x0B0BC, 0x0B0BD, 0x0B0BE, 0x0B0BF, 0x0B0C0, 0x0B0C1,
+ 0x0B0C2, 0x0B0C3, 0x0B0C4, 0x0B0C5, 0x0B0C6, 0x0B0C7, 0x0B0C8, 0x0B0C9,
+ 0x0B0CA, 0x0B0CB, 0x0B0CC, 0x0B0CD, 0x0B0CE, 0x0B0CF, 0x0B0D0, 0x0B0D1,
+ 0x0B0D2, 0x0B0D3, 0x0B0D4, 0x0B0D5, 0x0B0D6, 0x0B0D7, 0x0B0D8, 0x0B0D9,
+ 0x0B0DA, 0x0B0DB, 0x0B0DC, 0x0B0DD, 0x0B0DE, 0x0B0DF, 0x0B0E0, 0x0B0E1,
+ 0x0B0E2, 0x0B0E3, 0x0B0E4, 0x0B0E5, 0x0B0E6, 0x0B0E7, 0x0B0E8, 0x0B0E9,
+ 0x0B0EA, 0x0B0EB, 0x0B0EC, 0x0B0ED, 0x0B0EE, 0x0B0EF, 0x0B0F0, 0x0B0F1,
+ 0x0B0F2, 0x0B0F3, 0x0B0F4, 0x0B0F5, 0x0B0F6, 0x0B0F7, 0x0B0F8, 0x0B0F9,
+ 0x0B0FA, 0x0B0FB, 0x0B0FC, 0x0B0FD, 0x0B0FE, 0x0B0FF, 0x0B100, 0x0B101,
+ 0x0B102, 0x0B103, 0x0B104, 0x0B105, 0x0B106, 0x0B107, 0x0B108, 0x0B109,
+ 0x0B10A, 0x0B10B, 0x0B10C, 0x0B10D, 0x0B10E, 0x0B10F, 0x0B110, 0x0B111,
+ 0x0B112, 0x0B113, 0x0B114, 0x0B115, 0x0B116, 0x0B117, 0x0B118, 0x0B119,
+ 0x0B11A, 0x0B11B, 0x0B11C, 0x0B11D, 0x0B11E, 0x0B11F, 0x0B120, 0x0B121,
+ 0x0B122, 0x0B123, 0x0B124, 0x0B125, 0x0B126, 0x0B127, 0x0B128, 0x0B129,
+ 0x0B12A, 0x0B12B, 0x0B12C, 0x0B12D, 0x0B12E, 0x0B12F, 0x0B130, 0x0B131,
+ 0x0B132, 0x0B133, 0x0B134, 0x0B135, 0x0B136, 0x0B137, 0x0B138, 0x0B139,
+ 0x0B13A, 0x0B13B, 0x0B13C, 0x0B13D, 0x0B13E, 0x0B13F, 0x0B140, 0x0B141,
+ 0x0B142, 0x0B143, 0x0B144, 0x0B145, 0x0B146, 0x0B147, 0x0B148, 0x0B149,
+ 0x0B14A, 0x0B14B, 0x0B14C, 0x0B14D, 0x0B14E, 0x0B14F, 0x0B150, 0x0B151,
+ 0x0B152, 0x0B153, 0x0B154, 0x0B155, 0x0B156, 0x0B157, 0x0B158, 0x0B159,
+ 0x0B15A, 0x0B15B, 0x0B15C, 0x0B15D, 0x0B15E, 0x0B15F, 0x0B160, 0x0B161,
+ 0x0B162, 0x0B163, 0x0B164, 0x0B165, 0x0B166, 0x0B167, 0x0B168, 0x0B169,
+ 0x0B16A, 0x0B16B, 0x0B16C, 0x0B16D, 0x0B16E, 0x0B16F, 0x0B170, 0x0B171,
+ 0x0B172, 0x0B173, 0x0B174, 0x0B175, 0x0B176, 0x0B177, 0x0B178, 0x0B179,
+ 0x0B17A, 0x0B17B, 0x0B17C, 0x0B17D, 0x0B17E, 0x0B17F, 0x0B180, 0x0B181,
+ 0x0B182, 0x0B183, 0x0B184, 0x0B185, 0x0B186, 0x0B187, 0x0B188, 0x0B189,
+ 0x0B18A, 0x0B18B, 0x0B18C, 0x0B18D, 0x0B18E, 0x0B18F, 0x0B190, 0x0B191,
+ 0x0B192, 0x0B193, 0x0B194, 0x0B195, 0x0B196, 0x0B197, 0x0B198, 0x0B199,
+ 0x0B19A, 0x0B19B, 0x0B19C, 0x0B19D, 0x0B19E, 0x0B19F, 0x0B1A0, 0x0B1A1,
+ 0x0B1A2, 0x0B1A3, 0x0B1A4, 0x0B1A5, 0x0B1A6, 0x0B1A7, 0x0B1A8, 0x0B1A9,
+ 0x0B1AA, 0x0B1AB, 0x0B1AC, 0x0B1AD, 0x0B1AE, 0x0B1AF, 0x0B1B0, 0x0B1B1,
+ 0x0B1B2, 0x0B1B3, 0x0B1B4, 0x0B1B5, 0x0B1B6, 0x0B1B7, 0x0B1B8, 0x0B1B9,
+ 0x0B1BA, 0x0B1BB, 0x0B1BC, 0x0B1BD, 0x0B1BE, 0x0B1BF, 0x0B1C0, 0x0B1C1,
+ 0x0B1C2, 0x0B1C3, 0x0B1C4, 0x0B1C5, 0x0B1C6, 0x0B1C7, 0x0B1C8, 0x0B1C9,
+ 0x0B1CA, 0x0B1CB, 0x0B1CC, 0x0B1CD, 0x0B1CE, 0x0B1CF, 0x0B1D0, 0x0B1D1,
+ 0x0B1D2, 0x0B1D3, 0x0B1D4, 0x0B1D5, 0x0B1D6, 0x0B1D7, 0x0B1D8, 0x0B1D9,
+ 0x0B1DA, 0x0B1DB, 0x0B1DC, 0x0B1DD, 0x0B1DE, 0x0B1DF, 0x0B1E0, 0x0B1E1,
+ 0x0B1E2, 0x0B1E3, 0x0B1E4, 0x0B1E5, 0x0B1E6, 0x0B1E7, 0x0B1E8, 0x0B1E9,
+ 0x0B1EA, 0x0B1EB, 0x0B1EC, 0x0B1ED, 0x0B1EE, 0x0B1EF, 0x0B1F0, 0x0B1F1,
+ 0x0B1F2, 0x0B1F3, 0x0B1F4, 0x0B1F5, 0x0B1F6, 0x0B1F7, 0x0B1F8, 0x0B1F9,
+ 0x0B1FA, 0x0B1FB, 0x0B1FC, 0x0B1FD, 0x0B1FE, 0x0B1FF, 0x0B200, 0x0B201,
+ 0x0B202, 0x0B203, 0x0B204, 0x0B205, 0x0B206, 0x0B207, 0x0B208, 0x0B209,
+ 0x0B20A, 0x0B20B, 0x0B20C, 0x0B20D, 0x0B20E, 0x0B20F, 0x0B210, 0x0B211,
+ 0x0B212, 0x0B213, 0x0B214, 0x0B215, 0x0B216, 0x0B217, 0x0B218, 0x0B219,
+ 0x0B21A, 0x0B21B, 0x0B21C, 0x0B21D, 0x0B21E, 0x0B21F, 0x0B220, 0x0B221,
+ 0x0B222, 0x0B223, 0x0B224, 0x0B225, 0x0B226, 0x0B227, 0x0B228, 0x0B229,
+ 0x0B22A, 0x0B22B, 0x0B22C, 0x0B22D, 0x0B22E, 0x0B22F, 0x0B230, 0x0B231,
+ 0x0B232, 0x0B233, 0x0B234, 0x0B235, 0x0B236, 0x0B237, 0x0B238, 0x0B239,
+ 0x0B23A, 0x0B23B, 0x0B23C, 0x0B23D, 0x0B23E, 0x0B23F, 0x0B240, 0x0B241,
+ 0x0B242, 0x0B243, 0x0B244, 0x0B245, 0x0B246, 0x0B247, 0x0B248, 0x0B249,
+ 0x0B24A, 0x0B24B, 0x0B24C, 0x0B24D, 0x0B24E, 0x0B24F, 0x0B250, 0x0B251,
+ 0x0B252, 0x0B253, 0x0B254, 0x0B255, 0x0B256, 0x0B257, 0x0B258, 0x0B259,
+ 0x0B25A, 0x0B25B, 0x0B25C, 0x0B25D, 0x0B25E, 0x0B25F, 0x0B260, 0x0B261,
+ 0x0B262, 0x0B263, 0x0B264, 0x0B265, 0x0B266, 0x0B267, 0x0B268, 0x0B269,
+ 0x0B26A, 0x0B26B, 0x0B26C, 0x0B26D, 0x0B26E, 0x0B26F, 0x0B270, 0x0B271,
+ 0x0B272, 0x0B273, 0x0B274, 0x0B275, 0x0B276, 0x0B277, 0x0B278, 0x0B279,
+ 0x0B27A, 0x0B27B, 0x0B27C, 0x0B27D, 0x0B27E, 0x0B27F, 0x0B280, 0x0B281,
+ 0x0B282, 0x0B283, 0x0B284, 0x0B285, 0x0B286, 0x0B287, 0x0B288, 0x0B289,
+ 0x0B28A, 0x0B28B, 0x0B28C, 0x0B28D, 0x0B28E, 0x0B28F, 0x0B290, 0x0B291,
+ 0x0B292, 0x0B293, 0x0B294, 0x0B295, 0x0B296, 0x0B297, 0x0B298, 0x0B299,
+ 0x0B29A, 0x0B29B, 0x0B29C, 0x0B29D, 0x0B29E, 0x0B29F, 0x0B2A0, 0x0B2A1,
+ 0x0B2A2, 0x0B2A3, 0x0B2A4, 0x0B2A5, 0x0B2A6, 0x0B2A7, 0x0B2A8, 0x0B2A9,
+ 0x0B2AA, 0x0B2AB, 0x0B2AC, 0x0B2AD, 0x0B2AE, 0x0B2AF, 0x0B2B0, 0x0B2B1,
+ 0x0B2B2, 0x0B2B3, 0x0B2B4, 0x0B2B5, 0x0B2B6, 0x0B2B7, 0x0B2B8, 0x0B2B9,
+ 0x0B2BA, 0x0B2BB, 0x0B2BC, 0x0B2BD, 0x0B2BE, 0x0B2BF, 0x0B2C0, 0x0B2C1,
+ 0x0B2C2, 0x0B2C3, 0x0B2C4, 0x0B2C5, 0x0B2C6, 0x0B2C7, 0x0B2C8, 0x0B2C9,
+ 0x0B2CA, 0x0B2CB, 0x0B2CC, 0x0B2CD, 0x0B2CE, 0x0B2CF, 0x0B2D0, 0x0B2D1,
+ 0x0B2D2, 0x0B2D3, 0x0B2D4, 0x0B2D5, 0x0B2D6, 0x0B2D7, 0x0B2D8, 0x0B2D9,
+ 0x0B2DA, 0x0B2DB, 0x0B2DC, 0x0B2DD, 0x0B2DE, 0x0B2DF, 0x0B2E0, 0x0B2E1,
+ 0x0B2E2, 0x0B2E3, 0x0B2E4, 0x0B2E5, 0x0B2E6, 0x0B2E7, 0x0B2E8, 0x0B2E9,
+ 0x0B2EA, 0x0B2EB, 0x0B2EC, 0x0B2ED, 0x0B2EE, 0x0B2EF, 0x0B2F0, 0x0B2F1,
+ 0x0B2F2, 0x0B2F3, 0x0B2F4, 0x0B2F5, 0x0B2F6, 0x0B2F7, 0x0B2F8, 0x0B2F9,
+ 0x0B2FA, 0x0B2FB, 0x0B2FC, 0x0B2FD, 0x0B2FE, 0x0B2FF, 0x0B300, 0x0B301,
+ 0x0B302, 0x0B303, 0x0B304, 0x0B305, 0x0B306, 0x0B307, 0x0B308, 0x0B309,
+ 0x0B30A, 0x0B30B, 0x0B30C, 0x0B30D, 0x0B30E, 0x0B30F, 0x0B310, 0x0B311,
+ 0x0B312, 0x0B313, 0x0B314, 0x0B315, 0x0B316, 0x0B317, 0x0B318, 0x0B319,
+ 0x0B31A, 0x0B31B, 0x0B31C, 0x0B31D, 0x0B31E, 0x0B31F, 0x0B320, 0x0B321,
+ 0x0B322, 0x0B323, 0x0B324, 0x0B325, 0x0B326, 0x0B327, 0x0B328, 0x0B329,
+ 0x0B32A, 0x0B32B, 0x0B32C, 0x0B32D, 0x0B32E, 0x0B32F, 0x0B330, 0x0B331,
+ 0x0B332, 0x0B333, 0x0B334, 0x0B335, 0x0B336, 0x0B337, 0x0B338, 0x0B339,
+ 0x0B33A, 0x0B33B, 0x0B33C, 0x0B33D, 0x0B33E, 0x0B33F, 0x0B340, 0x0B341,
+ 0x0B342, 0x0B343, 0x0B344, 0x0B345, 0x0B346, 0x0B347, 0x0B348, 0x0B349,
+ 0x0B34A, 0x0B34B, 0x0B34C, 0x0B34D, 0x0B34E, 0x0B34F, 0x0B350, 0x0B351,
+ 0x0B352, 0x0B353, 0x0B354, 0x0B355, 0x0B356, 0x0B357, 0x0B358, 0x0B359,
+ 0x0B35A, 0x0B35B, 0x0B35C, 0x0B35D, 0x0B35E, 0x0B35F, 0x0B360, 0x0B361,
+ 0x0B362, 0x0B363, 0x0B364, 0x0B365, 0x0B366, 0x0B367, 0x0B368, 0x0B369,
+ 0x0B36A, 0x0B36B, 0x0B36C, 0x0B36D, 0x0B36E, 0x0B36F, 0x0B370, 0x0B371,
+ 0x0B372, 0x0B373, 0x0B374, 0x0B375, 0x0B376, 0x0B377, 0x0B378, 0x0B379,
+ 0x0B37A, 0x0B37B, 0x0B37C, 0x0B37D, 0x0B37E, 0x0B37F, 0x0B380, 0x0B381,
+ 0x0B382, 0x0B383, 0x0B384, 0x0B385, 0x0B386, 0x0B387, 0x0B388, 0x0B389,
+ 0x0B38A, 0x0B38B, 0x0B38C, 0x0B38D, 0x0B38E, 0x0B38F, 0x0B390, 0x0B391,
+ 0x0B392, 0x0B393, 0x0B394, 0x0B395, 0x0B396, 0x0B397, 0x0B398, 0x0B399,
+ 0x0B39A, 0x0B39B, 0x0B39C, 0x0B39D, 0x0B39E, 0x0B39F, 0x0B3A0, 0x0B3A1,
+ 0x0B3A2, 0x0B3A3, 0x0B3A4, 0x0B3A5, 0x0B3A6, 0x0B3A7, 0x0B3A8, 0x0B3A9,
+ 0x0B3AA, 0x0B3AB, 0x0B3AC, 0x0B3AD, 0x0B3AE, 0x0B3AF, 0x0B3B0, 0x0B3B1,
+ 0x0B3B2, 0x0B3B3, 0x0B3B4, 0x0B3B5, 0x0B3B6, 0x0B3B7, 0x0B3B8, 0x0B3B9,
+ 0x0B3BA, 0x0B3BB, 0x0B3BC, 0x0B3BD, 0x0B3BE, 0x0B3BF, 0x0B3C0, 0x0B3C1,
+ 0x0B3C2, 0x0B3C3, 0x0B3C4, 0x0B3C5, 0x0B3C6, 0x0B3C7, 0x0B3C8, 0x0B3C9,
+ 0x0B3CA, 0x0B3CB, 0x0B3CC, 0x0B3CD, 0x0B3CE, 0x0B3CF, 0x0B3D0, 0x0B3D1,
+ 0x0B3D2, 0x0B3D3, 0x0B3D4, 0x0B3D5, 0x0B3D6, 0x0B3D7, 0x0B3D8, 0x0B3D9,
+ 0x0B3DA, 0x0B3DB, 0x0B3DC, 0x0B3DD, 0x0B3DE, 0x0B3DF, 0x0B3E0, 0x0B3E1,
+ 0x0B3E2, 0x0B3E3, 0x0B3E4, 0x0B3E5, 0x0B3E6, 0x0B3E7, 0x0B3E8, 0x0B3E9,
+ 0x0B3EA, 0x0B3EB, 0x0B3EC, 0x0B3ED, 0x0B3EE, 0x0B3EF, 0x0B3F0, 0x0B3F1,
+ 0x0B3F2, 0x0B3F3, 0x0B3F4, 0x0B3F5, 0x0B3F6, 0x0B3F7, 0x0B3F8, 0x0B3F9,
+ 0x0B3FA, 0x0B3FB, 0x0B3FC, 0x0B3FD, 0x0B3FE, 0x0B3FF, 0x0B400, 0x0B401,
+ 0x0B402, 0x0B403, 0x0B404, 0x0B405, 0x0B406, 0x0B407, 0x0B408, 0x0B409,
+ 0x0B40A, 0x0B40B, 0x0B40C, 0x0B40D, 0x0B40E, 0x0B40F, 0x0B410, 0x0B411,
+ 0x0B412, 0x0B413, 0x0B414, 0x0B415, 0x0B416, 0x0B417, 0x0B418, 0x0B419,
+ 0x0B41A, 0x0B41B, 0x0B41C, 0x0B41D, 0x0B41E, 0x0B41F, 0x0B420, 0x0B421,
+ 0x0B422, 0x0B423, 0x0B424, 0x0B425, 0x0B426, 0x0B427, 0x0B428, 0x0B429,
+ 0x0B42A, 0x0B42B, 0x0B42C, 0x0B42D, 0x0B42E, 0x0B42F, 0x0B430, 0x0B431,
+ 0x0B432, 0x0B433, 0x0B434, 0x0B435, 0x0B436, 0x0B437, 0x0B438, 0x0B439,
+ 0x0B43A, 0x0B43B, 0x0B43C, 0x0B43D, 0x0B43E, 0x0B43F, 0x0B440, 0x0B441,
+ 0x0B442, 0x0B443, 0x0B444, 0x0B445, 0x0B446, 0x0B447, 0x0B448, 0x0B449,
+ 0x0B44A, 0x0B44B, 0x0B44C, 0x0B44D, 0x0B44E, 0x0B44F, 0x0B450, 0x0B451,
+ 0x0B452, 0x0B453, 0x0B454, 0x0B455, 0x0B456, 0x0B457, 0x0B458, 0x0B459,
+ 0x0B45A, 0x0B45B, 0x0B45C, 0x0B45D, 0x0B45E, 0x0B45F, 0x0B460, 0x0B461,
+ 0x0B462, 0x0B463, 0x0B464, 0x0B465, 0x0B466, 0x0B467, 0x0B468, 0x0B469,
+ 0x0B46A, 0x0B46B, 0x0B46C, 0x0B46D, 0x0B46E, 0x0B46F, 0x0B470, 0x0B471,
+ 0x0B472, 0x0B473, 0x0B474, 0x0B475, 0x0B476, 0x0B477, 0x0B478, 0x0B479,
+ 0x0B47A, 0x0B47B, 0x0B47C, 0x0B47D, 0x0B47E, 0x0B47F, 0x0B480, 0x0B481,
+ 0x0B482, 0x0B483, 0x0B484, 0x0B485, 0x0B486, 0x0B487, 0x0B488, 0x0B489,
+ 0x0B48A, 0x0B48B, 0x0B48C, 0x0B48D, 0x0B48E, 0x0B48F, 0x0B490, 0x0B491,
+ 0x0B492, 0x0B493, 0x0B494, 0x0B495, 0x0B496, 0x0B497, 0x0B498, 0x0B499,
+ 0x0B49A, 0x0B49B, 0x0B49C, 0x0B49D, 0x0B49E, 0x0B49F, 0x0B4A0, 0x0B4A1,
+ 0x0B4A2, 0x0B4A3, 0x0B4A4, 0x0B4A5, 0x0B4A6, 0x0B4A7, 0x0B4A8, 0x0B4A9,
+ 0x0B4AA, 0x0B4AB, 0x0B4AC, 0x0B4AD, 0x0B4AE, 0x0B4AF, 0x0B4B0, 0x0B4B1,
+ 0x0B4B2, 0x0B4B3, 0x0B4B4, 0x0B4B5, 0x0B4B6, 0x0B4B7, 0x0B4B8, 0x0B4B9,
+ 0x0B4BA, 0x0B4BB, 0x0B4BC, 0x0B4BD, 0x0B4BE, 0x0B4BF, 0x0B4C0, 0x0B4C1,
+ 0x0B4C2, 0x0B4C3, 0x0B4C4, 0x0B4C5, 0x0B4C6, 0x0B4C7, 0x0B4C8, 0x0B4C9,
+ 0x0B4CA, 0x0B4CB, 0x0B4CC, 0x0B4CD, 0x0B4CE, 0x0B4CF, 0x0B4D0, 0x0B4D1,
+ 0x0B4D2, 0x0B4D3, 0x0B4D4, 0x0B4D5, 0x0B4D6, 0x0B4D7, 0x0B4D8, 0x0B4D9,
+ 0x0B4DA, 0x0B4DB, 0x0B4DC, 0x0B4DD, 0x0B4DE, 0x0B4DF, 0x0B4E0, 0x0B4E1,
+ 0x0B4E2, 0x0B4E3, 0x0B4E4, 0x0B4E5, 0x0B4E6, 0x0B4E7, 0x0B4E8, 0x0B4E9,
+ 0x0B4EA, 0x0B4EB, 0x0B4EC, 0x0B4ED, 0x0B4EE, 0x0B4EF, 0x0B4F0, 0x0B4F1,
+ 0x0B4F2, 0x0B4F3, 0x0B4F4, 0x0B4F5, 0x0B4F6, 0x0B4F7, 0x0B4F8, 0x0B4F9,
+ 0x0B4FA, 0x0B4FB, 0x0B4FC, 0x0B4FD, 0x0B4FE, 0x0B4FF, 0x0B500, 0x0B501,
+ 0x0B502, 0x0B503, 0x0B504, 0x0B505, 0x0B506, 0x0B507, 0x0B508, 0x0B509,
+ 0x0B50A, 0x0B50B, 0x0B50C, 0x0B50D, 0x0B50E, 0x0B50F, 0x0B510, 0x0B511,
+ 0x0B512, 0x0B513, 0x0B514, 0x0B515, 0x0B516, 0x0B517, 0x0B518, 0x0B519,
+ 0x0B51A, 0x0B51B, 0x0B51C, 0x0B51D, 0x0B51E, 0x0B51F, 0x0B520, 0x0B521,
+ 0x0B522, 0x0B523, 0x0B524, 0x0B525, 0x0B526, 0x0B527, 0x0B528, 0x0B529,
+ 0x0B52A, 0x0B52B, 0x0B52C, 0x0B52D, 0x0B52E, 0x0B52F, 0x0B530, 0x0B531,
+ 0x0B532, 0x0B533, 0x0B534, 0x0B535, 0x0B536, 0x0B537, 0x0B538, 0x0B539,
+ 0x0B53A, 0x0B53B, 0x0B53C, 0x0B53D, 0x0B53E, 0x0B53F, 0x0B540, 0x0B541,
+ 0x0B542, 0x0B543, 0x0B544, 0x0B545, 0x0B546, 0x0B547, 0x0B548, 0x0B549,
+ 0x0B54A, 0x0B54B, 0x0B54C, 0x0B54D, 0x0B54E, 0x0B54F, 0x0B550, 0x0B551,
+ 0x0B552, 0x0B553, 0x0B554, 0x0B555, 0x0B556, 0x0B557, 0x0B558, 0x0B559,
+ 0x0B55A, 0x0B55B, 0x0B55C, 0x0B55D, 0x0B55E, 0x0B55F, 0x0B560, 0x0B561,
+ 0x0B562, 0x0B563, 0x0B564, 0x0B565, 0x0B566, 0x0B567, 0x0B568, 0x0B569,
+ 0x0B56A, 0x0B56B, 0x0B56C, 0x0B56D, 0x0B56E, 0x0B56F, 0x0B570, 0x0B571,
+ 0x0B572, 0x0B573, 0x0B574, 0x0B575, 0x0B576, 0x0B577, 0x0B578, 0x0B579,
+ 0x0B57A, 0x0B57B, 0x0B57C, 0x0B57D, 0x0B57E, 0x0B57F, 0x0B580, 0x0B581,
+ 0x0B582, 0x0B583, 0x0B584, 0x0B585, 0x0B586, 0x0B587, 0x0B588, 0x0B589,
+ 0x0B58A, 0x0B58B, 0x0B58C, 0x0B58D, 0x0B58E, 0x0B58F, 0x0B590, 0x0B591,
+ 0x0B592, 0x0B593, 0x0B594, 0x0B595, 0x0B596, 0x0B597, 0x0B598, 0x0B599,
+ 0x0B59A, 0x0B59B, 0x0B59C, 0x0B59D, 0x0B59E, 0x0B59F, 0x0B5A0, 0x0B5A1,
+ 0x0B5A2, 0x0B5A3, 0x0B5A4, 0x0B5A5, 0x0B5A6, 0x0B5A7, 0x0B5A8, 0x0B5A9,
+ 0x0B5AA, 0x0B5AB, 0x0B5AC, 0x0B5AD, 0x0B5AE, 0x0B5AF, 0x0B5B0, 0x0B5B1,
+ 0x0B5B2, 0x0B5B3, 0x0B5B4, 0x0B5B5, 0x0B5B6, 0x0B5B7, 0x0B5B8, 0x0B5B9,
+ 0x0B5BA, 0x0B5BB, 0x0B5BC, 0x0B5BD, 0x0B5BE, 0x0B5BF, 0x0B5C0, 0x0B5C1,
+ 0x0B5C2, 0x0B5C3, 0x0B5C4, 0x0B5C5, 0x0B5C6, 0x0B5C7, 0x0B5C8, 0x0B5C9,
+ 0x0B5CA, 0x0B5CB, 0x0B5CC, 0x0B5CD, 0x0B5CE, 0x0B5CF, 0x0B5D0, 0x0B5D1,
+ 0x0B5D2, 0x0B5D3, 0x0B5D4, 0x0B5D5, 0x0B5D6, 0x0B5D7, 0x0B5D8, 0x0B5D9,
+ 0x0B5DA, 0x0B5DB, 0x0B5DC, 0x0B5DD, 0x0B5DE, 0x0B5DF, 0x0B5E0, 0x0B5E1,
+ 0x0B5E2, 0x0B5E3, 0x0B5E4, 0x0B5E5, 0x0B5E6, 0x0B5E7, 0x0B5E8, 0x0B5E9,
+ 0x0B5EA, 0x0B5EB, 0x0B5EC, 0x0B5ED, 0x0B5EE, 0x0B5EF, 0x0B5F0, 0x0B5F1,
+ 0x0B5F2, 0x0B5F3, 0x0B5F4, 0x0B5F5, 0x0B5F6, 0x0B5F7, 0x0B5F8, 0x0B5F9,
+ 0x0B5FA, 0x0B5FB, 0x0B5FC, 0x0B5FD, 0x0B5FE, 0x0B5FF, 0x0B600, 0x0B601,
+ 0x0B602, 0x0B603, 0x0B604, 0x0B605, 0x0B606, 0x0B607, 0x0B608, 0x0B609,
+ 0x0B60A, 0x0B60B, 0x0B60C, 0x0B60D, 0x0B60E, 0x0B60F, 0x0B610, 0x0B611,
+ 0x0B612, 0x0B613, 0x0B614, 0x0B615, 0x0B616, 0x0B617, 0x0B618, 0x0B619,
+ 0x0B61A, 0x0B61B, 0x0B61C, 0x0B61D, 0x0B61E, 0x0B61F, 0x0B620, 0x0B621,
+ 0x0B622, 0x0B623, 0x0B624, 0x0B625, 0x0B626, 0x0B627, 0x0B628, 0x0B629,
+ 0x0B62A, 0x0B62B, 0x0B62C, 0x0B62D, 0x0B62E, 0x0B62F, 0x0B630, 0x0B631,
+ 0x0B632, 0x0B633, 0x0B634, 0x0B635, 0x0B636, 0x0B637, 0x0B638, 0x0B639,
+ 0x0B63A, 0x0B63B, 0x0B63C, 0x0B63D, 0x0B63E, 0x0B63F, 0x0B640, 0x0B641,
+ 0x0B642, 0x0B643, 0x0B644, 0x0B645, 0x0B646, 0x0B647, 0x0B648, 0x0B649,
+ 0x0B64A, 0x0B64B, 0x0B64C, 0x0B64D, 0x0B64E, 0x0B64F, 0x0B650, 0x0B651,
+ 0x0B652, 0x0B653, 0x0B654, 0x0B655, 0x0B656, 0x0B657, 0x0B658, 0x0B659,
+ 0x0B65A, 0x0B65B, 0x0B65C, 0x0B65D, 0x0B65E, 0x0B65F, 0x0B660, 0x0B661,
+ 0x0B662, 0x0B663, 0x0B664, 0x0B665, 0x0B666, 0x0B667, 0x0B668, 0x0B669,
+ 0x0B66A, 0x0B66B, 0x0B66C, 0x0B66D, 0x0B66E, 0x0B66F, 0x0B670, 0x0B671,
+ 0x0B672, 0x0B673, 0x0B674, 0x0B675, 0x0B676, 0x0B677, 0x0B678, 0x0B679,
+ 0x0B67A, 0x0B67B, 0x0B67C, 0x0B67D, 0x0B67E, 0x0B67F, 0x0B680, 0x0B681,
+ 0x0B682, 0x0B683, 0x0B684, 0x0B685, 0x0B686, 0x0B687, 0x0B688, 0x0B689,
+ 0x0B68A, 0x0B68B, 0x0B68C, 0x0B68D, 0x0B68E, 0x0B68F, 0x0B690, 0x0B691,
+ 0x0B692, 0x0B693, 0x0B694, 0x0B695, 0x0B696, 0x0B697, 0x0B698, 0x0B699,
+ 0x0B69A, 0x0B69B, 0x0B69C, 0x0B69D, 0x0B69E, 0x0B69F, 0x0B6A0, 0x0B6A1,
+ 0x0B6A2, 0x0B6A3, 0x0B6A4, 0x0B6A5, 0x0B6A6, 0x0B6A7, 0x0B6A8, 0x0B6A9,
+ 0x0B6AA, 0x0B6AB, 0x0B6AC, 0x0B6AD, 0x0B6AE, 0x0B6AF, 0x0B6B0, 0x0B6B1,
+ 0x0B6B2, 0x0B6B3, 0x0B6B4, 0x0B6B5, 0x0B6B6, 0x0B6B7, 0x0B6B8, 0x0B6B9,
+ 0x0B6BA, 0x0B6BB, 0x0B6BC, 0x0B6BD, 0x0B6BE, 0x0B6BF, 0x0B6C0, 0x0B6C1,
+ 0x0B6C2, 0x0B6C3, 0x0B6C4, 0x0B6C5, 0x0B6C6, 0x0B6C7, 0x0B6C8, 0x0B6C9,
+ 0x0B6CA, 0x0B6CB, 0x0B6CC, 0x0B6CD, 0x0B6CE, 0x0B6CF, 0x0B6D0, 0x0B6D1,
+ 0x0B6D2, 0x0B6D3, 0x0B6D4, 0x0B6D5, 0x0B6D6, 0x0B6D7, 0x0B6D8, 0x0B6D9,
+ 0x0B6DA, 0x0B6DB, 0x0B6DC, 0x0B6DD, 0x0B6DE, 0x0B6DF, 0x0B6E0, 0x0B6E1,
+ 0x0B6E2, 0x0B6E3, 0x0B6E4, 0x0B6E5, 0x0B6E6, 0x0B6E7, 0x0B6E8, 0x0B6E9,
+ 0x0B6EA, 0x0B6EB, 0x0B6EC, 0x0B6ED, 0x0B6EE, 0x0B6EF, 0x0B6F0, 0x0B6F1,
+ 0x0B6F2, 0x0B6F3, 0x0B6F4, 0x0B6F5, 0x0B6F6, 0x0B6F7, 0x0B6F8, 0x0B6F9,
+ 0x0B6FA, 0x0B6FB, 0x0B6FC, 0x0B6FD, 0x0B6FE, 0x0B6FF, 0x0B700, 0x0B701,
+ 0x0B702, 0x0B703, 0x0B704, 0x0B705, 0x0B706, 0x0B707, 0x0B708, 0x0B709,
+ 0x0B70A, 0x0B70B, 0x0B70C, 0x0B70D, 0x0B70E, 0x0B70F, 0x0B710, 0x0B711,
+ 0x0B712, 0x0B713, 0x0B714, 0x0B715, 0x0B716, 0x0B717, 0x0B718, 0x0B719,
+ 0x0B71A, 0x0B71B, 0x0B71C, 0x0B71D, 0x0B71E, 0x0B71F, 0x0B720, 0x0B721,
+ 0x0B722, 0x0B723, 0x0B724, 0x0B725, 0x0B726, 0x0B727, 0x0B728, 0x0B729,
+ 0x0B72A, 0x0B72B, 0x0B72C, 0x0B72D, 0x0B72E, 0x0B72F, 0x0B730, 0x0B731,
+ 0x0B732, 0x0B733, 0x0B734, 0x0B735, 0x0B736, 0x0B737, 0x0B738, 0x0B739,
+ 0x0B73A, 0x0B73B, 0x0B73C, 0x0B73D, 0x0B73E, 0x0B73F, 0x0B740, 0x0B741,
+ 0x0B742, 0x0B743, 0x0B744, 0x0B745, 0x0B746, 0x0B747, 0x0B748, 0x0B749,
+ 0x0B74A, 0x0B74B, 0x0B74C, 0x0B74D, 0x0B74E, 0x0B74F, 0x0B750, 0x0B751,
+ 0x0B752, 0x0B753, 0x0B754, 0x0B755, 0x0B756, 0x0B757, 0x0B758, 0x0B759,
+ 0x0B75A, 0x0B75B, 0x0B75C, 0x0B75D, 0x0B75E, 0x0B75F, 0x0B760, 0x0B761,
+ 0x0B762, 0x0B763, 0x0B764, 0x0B765, 0x0B766, 0x0B767, 0x0B768, 0x0B769,
+ 0x0B76A, 0x0B76B, 0x0B76C, 0x0B76D, 0x0B76E, 0x0B76F, 0x0B770, 0x0B771,
+ 0x0B772, 0x0B773, 0x0B774, 0x0B775, 0x0B776, 0x0B777, 0x0B778, 0x0B779,
+ 0x0B77A, 0x0B77B, 0x0B77C, 0x0B77D, 0x0B77E, 0x0B77F, 0x0B780, 0x0B781,
+ 0x0B782, 0x0B783, 0x0B784, 0x0B785, 0x0B786, 0x0B787, 0x0B788, 0x0B789,
+ 0x0B78A, 0x0B78B, 0x0B78C, 0x0B78D, 0x0B78E, 0x0B78F, 0x0B790, 0x0B791,
+ 0x0B792, 0x0B793, 0x0B794, 0x0B795, 0x0B796, 0x0B797, 0x0B798, 0x0B799,
+ 0x0B79A, 0x0B79B, 0x0B79C, 0x0B79D, 0x0B79E, 0x0B79F, 0x0B7A0, 0x0B7A1,
+ 0x0B7A2, 0x0B7A3, 0x0B7A4, 0x0B7A5, 0x0B7A6, 0x0B7A7, 0x0B7A8, 0x0B7A9,
+ 0x0B7AA, 0x0B7AB, 0x0B7AC, 0x0B7AD, 0x0B7AE, 0x0B7AF, 0x0B7B0, 0x0B7B1,
+ 0x0B7B2, 0x0B7B3, 0x0B7B4, 0x0B7B5, 0x0B7B6, 0x0B7B7, 0x0B7B8, 0x0B7B9,
+ 0x0B7BA, 0x0B7BB, 0x0B7BC, 0x0B7BD, 0x0B7BE, 0x0B7BF, 0x0B7C0, 0x0B7C1,
+ 0x0B7C2, 0x0B7C3, 0x0B7C4, 0x0B7C5, 0x0B7C6, 0x0B7C7, 0x0B7C8, 0x0B7C9,
+ 0x0B7CA, 0x0B7CB, 0x0B7CC, 0x0B7CD, 0x0B7CE, 0x0B7CF, 0x0B7D0, 0x0B7D1,
+ 0x0B7D2, 0x0B7D3, 0x0B7D4, 0x0B7D5, 0x0B7D6, 0x0B7D7, 0x0B7D8, 0x0B7D9,
+ 0x0B7DA, 0x0B7DB, 0x0B7DC, 0x0B7DD, 0x0B7DE, 0x0B7DF, 0x0B7E0, 0x0B7E1,
+ 0x0B7E2, 0x0B7E3, 0x0B7E4, 0x0B7E5, 0x0B7E6, 0x0B7E7, 0x0B7E8, 0x0B7E9,
+ 0x0B7EA, 0x0B7EB, 0x0B7EC, 0x0B7ED, 0x0B7EE, 0x0B7EF, 0x0B7F0, 0x0B7F1,
+ 0x0B7F2, 0x0B7F3, 0x0B7F4, 0x0B7F5, 0x0B7F6, 0x0B7F7, 0x0B7F8, 0x0B7F9,
+ 0x0B7FA, 0x0B7FB, 0x0B7FC, 0x0B7FD, 0x0B7FE, 0x0B7FF, 0x0B800, 0x0B801,
+ 0x0B802, 0x0B803, 0x0B804, 0x0B805, 0x0B806, 0x0B807, 0x0B808, 0x0B809,
+ 0x0B80A, 0x0B80B, 0x0B80C, 0x0B80D, 0x0B80E, 0x0B80F, 0x0B810, 0x0B811,
+ 0x0B812, 0x0B813, 0x0B814, 0x0B815, 0x0B816, 0x0B817, 0x0B818, 0x0B819,
+ 0x0B81A, 0x0B81B, 0x0B81C, 0x0B81D, 0x0B81E, 0x0B81F, 0x0B820, 0x0B821,
+ 0x0B822, 0x0B823, 0x0B824, 0x0B825, 0x0B826, 0x0B827, 0x0B828, 0x0B829,
+ 0x0B82A, 0x0B82B, 0x0B82C, 0x0B82D, 0x0B82E, 0x0B82F, 0x0B830, 0x0B831,
+ 0x0B832, 0x0B833, 0x0B834, 0x0B835, 0x0B836, 0x0B837, 0x0B838, 0x0B839,
+ 0x0B83A, 0x0B83B, 0x0B83C, 0x0B83D, 0x0B83E, 0x0B83F, 0x0B840, 0x0B841,
+ 0x0B842, 0x0B843, 0x0B844, 0x0B845, 0x0B846, 0x0B847, 0x0B848, 0x0B849,
+ 0x0B84A, 0x0B84B, 0x0B84C, 0x0B84D, 0x0B84E, 0x0B84F, 0x0B850, 0x0B851,
+ 0x0B852, 0x0B853, 0x0B854, 0x0B855, 0x0B856, 0x0B857, 0x0B858, 0x0B859,
+ 0x0B85A, 0x0B85B, 0x0B85C, 0x0B85D, 0x0B85E, 0x0B85F, 0x0B860, 0x0B861,
+ 0x0B862, 0x0B863, 0x0B864, 0x0B865, 0x0B866, 0x0B867, 0x0B868, 0x0B869,
+ 0x0B86A, 0x0B86B, 0x0B86C, 0x0B86D, 0x0B86E, 0x0B86F, 0x0B870, 0x0B871,
+ 0x0B872, 0x0B873, 0x0B874, 0x0B875, 0x0B876, 0x0B877, 0x0B878, 0x0B879,
+ 0x0B87A, 0x0B87B, 0x0B87C, 0x0B87D, 0x0B87E, 0x0B87F, 0x0B880, 0x0B881,
+ 0x0B882, 0x0B883, 0x0B884, 0x0B885, 0x0B886, 0x0B887, 0x0B888, 0x0B889,
+ 0x0B88A, 0x0B88B, 0x0B88C, 0x0B88D, 0x0B88E, 0x0B88F, 0x0B890, 0x0B891,
+ 0x0B892, 0x0B893, 0x0B894, 0x0B895, 0x0B896, 0x0B897, 0x0B898, 0x0B899,
+ 0x0B89A, 0x0B89B, 0x0B89C, 0x0B89D, 0x0B89E, 0x0B89F, 0x0B8A0, 0x0B8A1,
+ 0x0B8A2, 0x0B8A3, 0x0B8A4, 0x0B8A5, 0x0B8A6, 0x0B8A7, 0x0B8A8, 0x0B8A9,
+ 0x0B8AA, 0x0B8AB, 0x0B8AC, 0x0B8AD, 0x0B8AE, 0x0B8AF, 0x0B8B0, 0x0B8B1,
+ 0x0B8B2, 0x0B8B3, 0x0B8B4, 0x0B8B5, 0x0B8B6, 0x0B8B7, 0x0B8B8, 0x0B8B9,
+ 0x0B8BA, 0x0B8BB, 0x0B8BC, 0x0B8BD, 0x0B8BE, 0x0B8BF, 0x0B8C0, 0x0B8C1,
+ 0x0B8C2, 0x0B8C3, 0x0B8C4, 0x0B8C5, 0x0B8C6, 0x0B8C7, 0x0B8C8, 0x0B8C9,
+ 0x0B8CA, 0x0B8CB, 0x0B8CC, 0x0B8CD, 0x0B8CE, 0x0B8CF, 0x0B8D0, 0x0B8D1,
+ 0x0B8D2, 0x0B8D3, 0x0B8D4, 0x0B8D5, 0x0B8D6, 0x0B8D7, 0x0B8D8, 0x0B8D9,
+ 0x0B8DA, 0x0B8DB, 0x0B8DC, 0x0B8DD, 0x0B8DE, 0x0B8DF, 0x0B8E0, 0x0B8E1,
+ 0x0B8E2, 0x0B8E3, 0x0B8E4, 0x0B8E5, 0x0B8E6, 0x0B8E7, 0x0B8E8, 0x0B8E9,
+ 0x0B8EA, 0x0B8EB, 0x0B8EC, 0x0B8ED, 0x0B8EE, 0x0B8EF, 0x0B8F0, 0x0B8F1,
+ 0x0B8F2, 0x0B8F3, 0x0B8F4, 0x0B8F5, 0x0B8F6, 0x0B8F7, 0x0B8F8, 0x0B8F9,
+ 0x0B8FA, 0x0B8FB, 0x0B8FC, 0x0B8FD, 0x0B8FE, 0x0B8FF, 0x0B900, 0x0B901,
+ 0x0B902, 0x0B903, 0x0B904, 0x0B905, 0x0B906, 0x0B907, 0x0B908, 0x0B909,
+ 0x0B90A, 0x0B90B, 0x0B90C, 0x0B90D, 0x0B90E, 0x0B90F, 0x0B910, 0x0B911,
+ 0x0B912, 0x0B913, 0x0B914, 0x0B915, 0x0B916, 0x0B917, 0x0B918, 0x0B919,
+ 0x0B91A, 0x0B91B, 0x0B91C, 0x0B91D, 0x0B91E, 0x0B91F, 0x0B920, 0x0B921,
+ 0x0B922, 0x0B923, 0x0B924, 0x0B925, 0x0B926, 0x0B927, 0x0B928, 0x0B929,
+ 0x0B92A, 0x0B92B, 0x0B92C, 0x0B92D, 0x0B92E, 0x0B92F, 0x0B930, 0x0B931,
+ 0x0B932, 0x0B933, 0x0B934, 0x0B935, 0x0B936, 0x0B937, 0x0B938, 0x0B939,
+ 0x0B93A, 0x0B93B, 0x0B93C, 0x0B93D, 0x0B93E, 0x0B93F, 0x0B940, 0x0B941,
+ 0x0B942, 0x0B943, 0x0B944, 0x0B945, 0x0B946, 0x0B947, 0x0B948, 0x0B949,
+ 0x0B94A, 0x0B94B, 0x0B94C, 0x0B94D, 0x0B94E, 0x0B94F, 0x0B950, 0x0B951,
+ 0x0B952, 0x0B953, 0x0B954, 0x0B955, 0x0B956, 0x0B957, 0x0B958, 0x0B959,
+ 0x0B95A, 0x0B95B, 0x0B95C, 0x0B95D, 0x0B95E, 0x0B95F, 0x0B960, 0x0B961,
+ 0x0B962, 0x0B963, 0x0B964, 0x0B965, 0x0B966, 0x0B967, 0x0B968, 0x0B969,
+ 0x0B96A, 0x0B96B, 0x0B96C, 0x0B96D, 0x0B96E, 0x0B96F, 0x0B970, 0x0B971,
+ 0x0B972, 0x0B973, 0x0B974, 0x0B975, 0x0B976, 0x0B977, 0x0B978, 0x0B979,
+ 0x0B97A, 0x0B97B, 0x0B97C, 0x0B97D, 0x0B97E, 0x0B97F, 0x0B980, 0x0B981,
+ 0x0B982, 0x0B983, 0x0B984, 0x0B985, 0x0B986, 0x0B987, 0x0B988, 0x0B989,
+ 0x0B98A, 0x0B98B, 0x0B98C, 0x0B98D, 0x0B98E, 0x0B98F, 0x0B990, 0x0B991,
+ 0x0B992, 0x0B993, 0x0B994, 0x0B995, 0x0B996, 0x0B997, 0x0B998, 0x0B999,
+ 0x0B99A, 0x0B99B, 0x0B99C, 0x0B99D, 0x0B99E, 0x0B99F, 0x0B9A0, 0x0B9A1,
+ 0x0B9A2, 0x0B9A3, 0x0B9A4, 0x0B9A5, 0x0B9A6, 0x0B9A7, 0x0B9A8, 0x0B9A9,
+ 0x0B9AA, 0x0B9AB, 0x0B9AC, 0x0B9AD, 0x0B9AE, 0x0B9AF, 0x0B9B0, 0x0B9B1,
+ 0x0B9B2, 0x0B9B3, 0x0B9B4, 0x0B9B5, 0x0B9B6, 0x0B9B7, 0x0B9B8, 0x0B9B9,
+ 0x0B9BA, 0x0B9BB, 0x0B9BC, 0x0B9BD, 0x0B9BE, 0x0B9BF, 0x0B9C0, 0x0B9C1,
+ 0x0B9C2, 0x0B9C3, 0x0B9C4, 0x0B9C5, 0x0B9C6, 0x0B9C7, 0x0B9C8, 0x0B9C9,
+ 0x0B9CA, 0x0B9CB, 0x0B9CC, 0x0B9CD, 0x0B9CE, 0x0B9CF, 0x0B9D0, 0x0B9D1,
+ 0x0B9D2, 0x0B9D3, 0x0B9D4, 0x0B9D5, 0x0B9D6, 0x0B9D7, 0x0B9D8, 0x0B9D9,
+ 0x0B9DA, 0x0B9DB, 0x0B9DC, 0x0B9DD, 0x0B9DE, 0x0B9DF, 0x0B9E0, 0x0B9E1,
+ 0x0B9E2, 0x0B9E3, 0x0B9E4, 0x0B9E5, 0x0B9E6, 0x0B9E7, 0x0B9E8, 0x0B9E9,
+ 0x0B9EA, 0x0B9EB, 0x0B9EC, 0x0B9ED, 0x0B9EE, 0x0B9EF, 0x0B9F0, 0x0B9F1,
+ 0x0B9F2, 0x0B9F3, 0x0B9F4, 0x0B9F5, 0x0B9F6, 0x0B9F7, 0x0B9F8, 0x0B9F9,
+ 0x0B9FA, 0x0B9FB, 0x0B9FC, 0x0B9FD, 0x0B9FE, 0x0B9FF, 0x0BA00, 0x0BA01,
+ 0x0BA02, 0x0BA03, 0x0BA04, 0x0BA05, 0x0BA06, 0x0BA07, 0x0BA08, 0x0BA09,
+ 0x0BA0A, 0x0BA0B, 0x0BA0C, 0x0BA0D, 0x0BA0E, 0x0BA0F, 0x0BA10, 0x0BA11,
+ 0x0BA12, 0x0BA13, 0x0BA14, 0x0BA15, 0x0BA16, 0x0BA17, 0x0BA18, 0x0BA19,
+ 0x0BA1A, 0x0BA1B, 0x0BA1C, 0x0BA1D, 0x0BA1E, 0x0BA1F, 0x0BA20, 0x0BA21,
+ 0x0BA22, 0x0BA23, 0x0BA24, 0x0BA25, 0x0BA26, 0x0BA27, 0x0BA28, 0x0BA29,
+ 0x0BA2A, 0x0BA2B, 0x0BA2C, 0x0BA2D, 0x0BA2E, 0x0BA2F, 0x0BA30, 0x0BA31,
+ 0x0BA32, 0x0BA33, 0x0BA34, 0x0BA35, 0x0BA36, 0x0BA37, 0x0BA38, 0x0BA39,
+ 0x0BA3A, 0x0BA3B, 0x0BA3C, 0x0BA3D, 0x0BA3E, 0x0BA3F, 0x0BA40, 0x0BA41,
+ 0x0BA42, 0x0BA43, 0x0BA44, 0x0BA45, 0x0BA46, 0x0BA47, 0x0BA48, 0x0BA49,
+ 0x0BA4A, 0x0BA4B, 0x0BA4C, 0x0BA4D, 0x0BA4E, 0x0BA4F, 0x0BA50, 0x0BA51,
+ 0x0BA52, 0x0BA53, 0x0BA54, 0x0BA55, 0x0BA56, 0x0BA57, 0x0BA58, 0x0BA59,
+ 0x0BA5A, 0x0BA5B, 0x0BA5C, 0x0BA5D, 0x0BA5E, 0x0BA5F, 0x0BA60, 0x0BA61,
+ 0x0BA62, 0x0BA63, 0x0BA64, 0x0BA65, 0x0BA66, 0x0BA67, 0x0BA68, 0x0BA69,
+ 0x0BA6A, 0x0BA6B, 0x0BA6C, 0x0BA6D, 0x0BA6E, 0x0BA6F, 0x0BA70, 0x0BA71,
+ 0x0BA72, 0x0BA73, 0x0BA74, 0x0BA75, 0x0BA76, 0x0BA77, 0x0BA78, 0x0BA79,
+ 0x0BA7A, 0x0BA7B, 0x0BA7C, 0x0BA7D, 0x0BA7E, 0x0BA7F, 0x0BA80, 0x0BA81,
+ 0x0BA82, 0x0BA83, 0x0BA84, 0x0BA85, 0x0BA86, 0x0BA87, 0x0BA88, 0x0BA89,
+ 0x0BA8A, 0x0BA8B, 0x0BA8C, 0x0BA8D, 0x0BA8E, 0x0BA8F, 0x0BA90, 0x0BA91,
+ 0x0BA92, 0x0BA93, 0x0BA94, 0x0BA95, 0x0BA96, 0x0BA97, 0x0BA98, 0x0BA99,
+ 0x0BA9A, 0x0BA9B, 0x0BA9C, 0x0BA9D, 0x0BA9E, 0x0BA9F, 0x0BAA0, 0x0BAA1,
+ 0x0BAA2, 0x0BAA3, 0x0BAA4, 0x0BAA5, 0x0BAA6, 0x0BAA7, 0x0BAA8, 0x0BAA9,
+ 0x0BAAA, 0x0BAAB, 0x0BAAC, 0x0BAAD, 0x0BAAE, 0x0BAAF, 0x0BAB0, 0x0BAB1,
+ 0x0BAB2, 0x0BAB3, 0x0BAB4, 0x0BAB5, 0x0BAB6, 0x0BAB7, 0x0BAB8, 0x0BAB9,
+ 0x0BABA, 0x0BABB, 0x0BABC, 0x0BABD, 0x0BABE, 0x0BABF, 0x0BAC0, 0x0BAC1,
+ 0x0BAC2, 0x0BAC3, 0x0BAC4, 0x0BAC5, 0x0BAC6, 0x0BAC7, 0x0BAC8, 0x0BAC9,
+ 0x0BACA, 0x0BACB, 0x0BACC, 0x0BACD, 0x0BACE, 0x0BACF, 0x0BAD0, 0x0BAD1,
+ 0x0BAD2, 0x0BAD3, 0x0BAD4, 0x0BAD5, 0x0BAD6, 0x0BAD7, 0x0BAD8, 0x0BAD9,
+ 0x0BADA, 0x0BADB, 0x0BADC, 0x0BADD, 0x0BADE, 0x0BADF, 0x0BAE0, 0x0BAE1,
+ 0x0BAE2, 0x0BAE3, 0x0BAE4, 0x0BAE5, 0x0BAE6, 0x0BAE7, 0x0BAE8, 0x0BAE9,
+ 0x0BAEA, 0x0BAEB, 0x0BAEC, 0x0BAED, 0x0BAEE, 0x0BAEF, 0x0BAF0, 0x0BAF1,
+ 0x0BAF2, 0x0BAF3, 0x0BAF4, 0x0BAF5, 0x0BAF6, 0x0BAF7, 0x0BAF8, 0x0BAF9,
+ 0x0BAFA, 0x0BAFB, 0x0BAFC, 0x0BAFD, 0x0BAFE, 0x0BAFF, 0x0BB00, 0x0BB01,
+ 0x0BB02, 0x0BB03, 0x0BB04, 0x0BB05, 0x0BB06, 0x0BB07, 0x0BB08, 0x0BB09,
+ 0x0BB0A, 0x0BB0B, 0x0BB0C, 0x0BB0D, 0x0BB0E, 0x0BB0F, 0x0BB10, 0x0BB11,
+ 0x0BB12, 0x0BB13, 0x0BB14, 0x0BB15, 0x0BB16, 0x0BB17, 0x0BB18, 0x0BB19,
+ 0x0BB1A, 0x0BB1B, 0x0BB1C, 0x0BB1D, 0x0BB1E, 0x0BB1F, 0x0BB20, 0x0BB21,
+ 0x0BB22, 0x0BB23, 0x0BB24, 0x0BB25, 0x0BB26, 0x0BB27, 0x0BB28, 0x0BB29,
+ 0x0BB2A, 0x0BB2B, 0x0BB2C, 0x0BB2D, 0x0BB2E, 0x0BB2F, 0x0BB30, 0x0BB31,
+ 0x0BB32, 0x0BB33, 0x0BB34, 0x0BB35, 0x0BB36, 0x0BB37, 0x0BB38, 0x0BB39,
+ 0x0BB3A, 0x0BB3B, 0x0BB3C, 0x0BB3D, 0x0BB3E, 0x0BB3F, 0x0BB40, 0x0BB41,
+ 0x0BB42, 0x0BB43, 0x0BB44, 0x0BB45, 0x0BB46, 0x0BB47, 0x0BB48, 0x0BB49,
+ 0x0BB4A, 0x0BB4B, 0x0BB4C, 0x0BB4D, 0x0BB4E, 0x0BB4F, 0x0BB50, 0x0BB51,
+ 0x0BB52, 0x0BB53, 0x0BB54, 0x0BB55, 0x0BB56, 0x0BB57, 0x0BB58, 0x0BB59,
+ 0x0BB5A, 0x0BB5B, 0x0BB5C, 0x0BB5D, 0x0BB5E, 0x0BB5F, 0x0BB60, 0x0BB61,
+ 0x0BB62, 0x0BB63, 0x0BB64, 0x0BB65, 0x0BB66, 0x0BB67, 0x0BB68, 0x0BB69,
+ 0x0BB6A, 0x0BB6B, 0x0BB6C, 0x0BB6D, 0x0BB6E, 0x0BB6F, 0x0BB70, 0x0BB71,
+ 0x0BB72, 0x0BB73, 0x0BB74, 0x0BB75, 0x0BB76, 0x0BB77, 0x0BB78, 0x0BB79,
+ 0x0BB7A, 0x0BB7B, 0x0BB7C, 0x0BB7D, 0x0BB7E, 0x0BB7F, 0x0BB80, 0x0BB81,
+ 0x0BB82, 0x0BB83, 0x0BB84, 0x0BB85, 0x0BB86, 0x0BB87, 0x0BB88, 0x0BB89,
+ 0x0BB8A, 0x0BB8B, 0x0BB8C, 0x0BB8D, 0x0BB8E, 0x0BB8F, 0x0BB90, 0x0BB91,
+ 0x0BB92, 0x0BB93, 0x0BB94, 0x0BB95, 0x0BB96, 0x0BB97, 0x0BB98, 0x0BB99,
+ 0x0BB9A, 0x0BB9B, 0x0BB9C, 0x0BB9D, 0x0BB9E, 0x0BB9F, 0x0BBA0, 0x0BBA1,
+ 0x0BBA2, 0x0BBA3, 0x0BBA4, 0x0BBA5, 0x0BBA6, 0x0BBA7, 0x0BBA8, 0x0BBA9,
+ 0x0BBAA, 0x0BBAB, 0x0BBAC, 0x0BBAD, 0x0BBAE, 0x0BBAF, 0x0BBB0, 0x0BBB1,
+ 0x0BBB2, 0x0BBB3, 0x0BBB4, 0x0BBB5, 0x0BBB6, 0x0BBB7, 0x0BBB8, 0x0BBB9,
+ 0x0BBBA, 0x0BBBB, 0x0BBBC, 0x0BBBD, 0x0BBBE, 0x0BBBF, 0x0BBC0, 0x0BBC1,
+ 0x0BBC2, 0x0BBC3, 0x0BBC4, 0x0BBC5, 0x0BBC6, 0x0BBC7, 0x0BBC8, 0x0BBC9,
+ 0x0BBCA, 0x0BBCB, 0x0BBCC, 0x0BBCD, 0x0BBCE, 0x0BBCF, 0x0BBD0, 0x0BBD1,
+ 0x0BBD2, 0x0BBD3, 0x0BBD4, 0x0BBD5, 0x0BBD6, 0x0BBD7, 0x0BBD8, 0x0BBD9,
+ 0x0BBDA, 0x0BBDB, 0x0BBDC, 0x0BBDD, 0x0BBDE, 0x0BBDF, 0x0BBE0, 0x0BBE1,
+ 0x0BBE2, 0x0BBE3, 0x0BBE4, 0x0BBE5, 0x0BBE6, 0x0BBE7, 0x0BBE8, 0x0BBE9,
+ 0x0BBEA, 0x0BBEB, 0x0BBEC, 0x0BBED, 0x0BBEE, 0x0BBEF, 0x0BBF0, 0x0BBF1,
+ 0x0BBF2, 0x0BBF3, 0x0BBF4, 0x0BBF5, 0x0BBF6, 0x0BBF7, 0x0BBF8, 0x0BBF9,
+ 0x0BBFA, 0x0BBFB, 0x0BBFC, 0x0BBFD, 0x0BBFE, 0x0BBFF, 0x0BC00, 0x0BC01,
+ 0x0BC02, 0x0BC03, 0x0BC04, 0x0BC05, 0x0BC06, 0x0BC07, 0x0BC08, 0x0BC09,
+ 0x0BC0A, 0x0BC0B, 0x0BC0C, 0x0BC0D, 0x0BC0E, 0x0BC0F, 0x0BC10, 0x0BC11,
+ 0x0BC12, 0x0BC13, 0x0BC14, 0x0BC15, 0x0BC16, 0x0BC17, 0x0BC18, 0x0BC19,
+ 0x0BC1A, 0x0BC1B, 0x0BC1C, 0x0BC1D, 0x0BC1E, 0x0BC1F, 0x0BC20, 0x0BC21,
+ 0x0BC22, 0x0BC23, 0x0BC24, 0x0BC25, 0x0BC26, 0x0BC27, 0x0BC28, 0x0BC29,
+ 0x0BC2A, 0x0BC2B, 0x0BC2C, 0x0BC2D, 0x0BC2E, 0x0BC2F, 0x0BC30, 0x0BC31,
+ 0x0BC32, 0x0BC33, 0x0BC34, 0x0BC35, 0x0BC36, 0x0BC37, 0x0BC38, 0x0BC39,
+ 0x0BC3A, 0x0BC3B, 0x0BC3C, 0x0BC3D, 0x0BC3E, 0x0BC3F, 0x0BC40, 0x0BC41,
+ 0x0BC42, 0x0BC43, 0x0BC44, 0x0BC45, 0x0BC46, 0x0BC47, 0x0BC48, 0x0BC49,
+ 0x0BC4A, 0x0BC4B, 0x0BC4C, 0x0BC4D, 0x0BC4E, 0x0BC4F, 0x0BC50, 0x0BC51,
+ 0x0BC52, 0x0BC53, 0x0BC54, 0x0BC55, 0x0BC56, 0x0BC57, 0x0BC58, 0x0BC59,
+ 0x0BC5A, 0x0BC5B, 0x0BC5C, 0x0BC5D, 0x0BC5E, 0x0BC5F, 0x0BC60, 0x0BC61,
+ 0x0BC62, 0x0BC63, 0x0BC64, 0x0BC65, 0x0BC66, 0x0BC67, 0x0BC68, 0x0BC69,
+ 0x0BC6A, 0x0BC6B, 0x0BC6C, 0x0BC6D, 0x0BC6E, 0x0BC6F, 0x0BC70, 0x0BC71,
+ 0x0BC72, 0x0BC73, 0x0BC74, 0x0BC75, 0x0BC76, 0x0BC77, 0x0BC78, 0x0BC79,
+ 0x0BC7A, 0x0BC7B, 0x0BC7C, 0x0BC7D, 0x0BC7E, 0x0BC7F, 0x0BC80, 0x0BC81,
+ 0x0BC82, 0x0BC83, 0x0BC84, 0x0BC85, 0x0BC86, 0x0BC87, 0x0BC88, 0x0BC89,
+ 0x0BC8A, 0x0BC8B, 0x0BC8C, 0x0BC8D, 0x0BC8E, 0x0BC8F, 0x0BC90, 0x0BC91,
+ 0x0BC92, 0x0BC93, 0x0BC94, 0x0BC95, 0x0BC96, 0x0BC97, 0x0BC98, 0x0BC99,
+ 0x0BC9A, 0x0BC9B, 0x0BC9C, 0x0BC9D, 0x0BC9E, 0x0BC9F, 0x0BCA0, 0x0BCA1,
+ 0x0BCA2, 0x0BCA3, 0x0BCA4, 0x0BCA5, 0x0BCA6, 0x0BCA7, 0x0BCA8, 0x0BCA9,
+ 0x0BCAA, 0x0BCAB, 0x0BCAC, 0x0BCAD, 0x0BCAE, 0x0BCAF, 0x0BCB0, 0x0BCB1,
+ 0x0BCB2, 0x0BCB3, 0x0BCB4, 0x0BCB5, 0x0BCB6, 0x0BCB7, 0x0BCB8, 0x0BCB9,
+ 0x0BCBA, 0x0BCBB, 0x0BCBC, 0x0BCBD, 0x0BCBE, 0x0BCBF, 0x0BCC0, 0x0BCC1,
+ 0x0BCC2, 0x0BCC3, 0x0BCC4, 0x0BCC5, 0x0BCC6, 0x0BCC7, 0x0BCC8, 0x0BCC9,
+ 0x0BCCA, 0x0BCCB, 0x0BCCC, 0x0BCCD, 0x0BCCE, 0x0BCCF, 0x0BCD0, 0x0BCD1,
+ 0x0BCD2, 0x0BCD3, 0x0BCD4, 0x0BCD5, 0x0BCD6, 0x0BCD7, 0x0BCD8, 0x0BCD9,
+ 0x0BCDA, 0x0BCDB, 0x0BCDC, 0x0BCDD, 0x0BCDE, 0x0BCDF, 0x0BCE0, 0x0BCE1,
+ 0x0BCE2, 0x0BCE3, 0x0BCE4, 0x0BCE5, 0x0BCE6, 0x0BCE7, 0x0BCE8, 0x0BCE9,
+ 0x0BCEA, 0x0BCEB, 0x0BCEC, 0x0BCED, 0x0BCEE, 0x0BCEF, 0x0BCF0, 0x0BCF1,
+ 0x0BCF2, 0x0BCF3, 0x0BCF4, 0x0BCF5, 0x0BCF6, 0x0BCF7, 0x0BCF8, 0x0BCF9,
+ 0x0BCFA, 0x0BCFB, 0x0BCFC, 0x0BCFD, 0x0BCFE, 0x0BCFF, 0x0BD00, 0x0BD01,
+ 0x0BD02, 0x0BD03, 0x0BD04, 0x0BD05, 0x0BD06, 0x0BD07, 0x0BD08, 0x0BD09,
+ 0x0BD0A, 0x0BD0B, 0x0BD0C, 0x0BD0D, 0x0BD0E, 0x0BD0F, 0x0BD10, 0x0BD11,
+ 0x0BD12, 0x0BD13, 0x0BD14, 0x0BD15, 0x0BD16, 0x0BD17, 0x0BD18, 0x0BD19,
+ 0x0BD1A, 0x0BD1B, 0x0BD1C, 0x0BD1D, 0x0BD1E, 0x0BD1F, 0x0BD20, 0x0BD21,
+ 0x0BD22, 0x0BD23, 0x0BD24, 0x0BD25, 0x0BD26, 0x0BD27, 0x0BD28, 0x0BD29,
+ 0x0BD2A, 0x0BD2B, 0x0BD2C, 0x0BD2D, 0x0BD2E, 0x0BD2F, 0x0BD30, 0x0BD31,
+ 0x0BD32, 0x0BD33, 0x0BD34, 0x0BD35, 0x0BD36, 0x0BD37, 0x0BD38, 0x0BD39,
+ 0x0BD3A, 0x0BD3B, 0x0BD3C, 0x0BD3D, 0x0BD3E, 0x0BD3F, 0x0BD40, 0x0BD41,
+ 0x0BD42, 0x0BD43, 0x0BD44, 0x0BD45, 0x0BD46, 0x0BD47, 0x0BD48, 0x0BD49,
+ 0x0BD4A, 0x0BD4B, 0x0BD4C, 0x0BD4D, 0x0BD4E, 0x0BD4F, 0x0BD50, 0x0BD51,
+ 0x0BD52, 0x0BD53, 0x0BD54, 0x0BD55, 0x0BD56, 0x0BD57, 0x0BD58, 0x0BD59,
+ 0x0BD5A, 0x0BD5B, 0x0BD5C, 0x0BD5D, 0x0BD5E, 0x0BD5F, 0x0BD60, 0x0BD61,
+ 0x0BD62, 0x0BD63, 0x0BD64, 0x0BD65, 0x0BD66, 0x0BD67, 0x0BD68, 0x0BD69,
+ 0x0BD6A, 0x0BD6B, 0x0BD6C, 0x0BD6D, 0x0BD6E, 0x0BD6F, 0x0BD70, 0x0BD71,
+ 0x0BD72, 0x0BD73, 0x0BD74, 0x0BD75, 0x0BD76, 0x0BD77, 0x0BD78, 0x0BD79,
+ 0x0BD7A, 0x0BD7B, 0x0BD7C, 0x0BD7D, 0x0BD7E, 0x0BD7F, 0x0BD80, 0x0BD81,
+ 0x0BD82, 0x0BD83, 0x0BD84, 0x0BD85, 0x0BD86, 0x0BD87, 0x0BD88, 0x0BD89,
+ 0x0BD8A, 0x0BD8B, 0x0BD8C, 0x0BD8D, 0x0BD8E, 0x0BD8F, 0x0BD90, 0x0BD91,
+ 0x0BD92, 0x0BD93, 0x0BD94, 0x0BD95, 0x0BD96, 0x0BD97, 0x0BD98, 0x0BD99,
+ 0x0BD9A, 0x0BD9B, 0x0BD9C, 0x0BD9D, 0x0BD9E, 0x0BD9F, 0x0BDA0, 0x0BDA1,
+ 0x0BDA2, 0x0BDA3, 0x0BDA4, 0x0BDA5, 0x0BDA6, 0x0BDA7, 0x0BDA8, 0x0BDA9,
+ 0x0BDAA, 0x0BDAB, 0x0BDAC, 0x0BDAD, 0x0BDAE, 0x0BDAF, 0x0BDB0, 0x0BDB1,
+ 0x0BDB2, 0x0BDB3, 0x0BDB4, 0x0BDB5, 0x0BDB6, 0x0BDB7, 0x0BDB8, 0x0BDB9,
+ 0x0BDBA, 0x0BDBB, 0x0BDBC, 0x0BDBD, 0x0BDBE, 0x0BDBF, 0x0BDC0, 0x0BDC1,
+ 0x0BDC2, 0x0BDC3, 0x0BDC4, 0x0BDC5, 0x0BDC6, 0x0BDC7, 0x0BDC8, 0x0BDC9,
+ 0x0BDCA, 0x0BDCB, 0x0BDCC, 0x0BDCD, 0x0BDCE, 0x0BDCF, 0x0BDD0, 0x0BDD1,
+ 0x0BDD2, 0x0BDD3, 0x0BDD4, 0x0BDD5, 0x0BDD6, 0x0BDD7, 0x0BDD8, 0x0BDD9,
+ 0x0BDDA, 0x0BDDB, 0x0BDDC, 0x0BDDD, 0x0BDDE, 0x0BDDF, 0x0BDE0, 0x0BDE1,
+ 0x0BDE2, 0x0BDE3, 0x0BDE4, 0x0BDE5, 0x0BDE6, 0x0BDE7, 0x0BDE8, 0x0BDE9,
+ 0x0BDEA, 0x0BDEB, 0x0BDEC, 0x0BDED, 0x0BDEE, 0x0BDEF, 0x0BDF0, 0x0BDF1,
+ 0x0BDF2, 0x0BDF3, 0x0BDF4, 0x0BDF5, 0x0BDF6, 0x0BDF7, 0x0BDF8, 0x0BDF9,
+ 0x0BDFA, 0x0BDFB, 0x0BDFC, 0x0BDFD, 0x0BDFE, 0x0BDFF, 0x0BE00, 0x0BE01,
+ 0x0BE02, 0x0BE03, 0x0BE04, 0x0BE05, 0x0BE06, 0x0BE07, 0x0BE08, 0x0BE09,
+ 0x0BE0A, 0x0BE0B, 0x0BE0C, 0x0BE0D, 0x0BE0E, 0x0BE0F, 0x0BE10, 0x0BE11,
+ 0x0BE12, 0x0BE13, 0x0BE14, 0x0BE15, 0x0BE16, 0x0BE17, 0x0BE18, 0x0BE19,
+ 0x0BE1A, 0x0BE1B, 0x0BE1C, 0x0BE1D, 0x0BE1E, 0x0BE1F, 0x0BE20, 0x0BE21,
+ 0x0BE22, 0x0BE23, 0x0BE24, 0x0BE25, 0x0BE26, 0x0BE27, 0x0BE28, 0x0BE29,
+ 0x0BE2A, 0x0BE2B, 0x0BE2C, 0x0BE2D, 0x0BE2E, 0x0BE2F, 0x0BE30, 0x0BE31,
+ 0x0BE32, 0x0BE33, 0x0BE34, 0x0BE35, 0x0BE36, 0x0BE37, 0x0BE38, 0x0BE39,
+ 0x0BE3A, 0x0BE3B, 0x0BE3C, 0x0BE3D, 0x0BE3E, 0x0BE3F, 0x0BE40, 0x0BE41,
+ 0x0BE42, 0x0BE43, 0x0BE44, 0x0BE45, 0x0BE46, 0x0BE47, 0x0BE48, 0x0BE49,
+ 0x0BE4A, 0x0BE4B, 0x0BE4C, 0x0BE4D, 0x0BE4E, 0x0BE4F, 0x0BE50, 0x0BE51,
+ 0x0BE52, 0x0BE53, 0x0BE54, 0x0BE55, 0x0BE56, 0x0BE57, 0x0BE58, 0x0BE59,
+ 0x0BE5A, 0x0BE5B, 0x0BE5C, 0x0BE5D, 0x0BE5E, 0x0BE5F, 0x0BE60, 0x0BE61,
+ 0x0BE62, 0x0BE63, 0x0BE64, 0x0BE65, 0x0BE66, 0x0BE67, 0x0BE68, 0x0BE69,
+ 0x0BE6A, 0x0BE6B, 0x0BE6C, 0x0BE6D, 0x0BE6E, 0x0BE6F, 0x0BE70, 0x0BE71,
+ 0x0BE72, 0x0BE73, 0x0BE74, 0x0BE75, 0x0BE76, 0x0BE77, 0x0BE78, 0x0BE79,
+ 0x0BE7A, 0x0BE7B, 0x0BE7C, 0x0BE7D, 0x0BE7E, 0x0BE7F, 0x0BE80, 0x0BE81,
+ 0x0BE82, 0x0BE83, 0x0BE84, 0x0BE85, 0x0BE86, 0x0BE87, 0x0BE88, 0x0BE89,
+ 0x0BE8A, 0x0BE8B, 0x0BE8C, 0x0BE8D, 0x0BE8E, 0x0BE8F, 0x0BE90, 0x0BE91,
+ 0x0BE92, 0x0BE93, 0x0BE94, 0x0BE95, 0x0BE96, 0x0BE97, 0x0BE98, 0x0BE99,
+ 0x0BE9A, 0x0BE9B, 0x0BE9C, 0x0BE9D, 0x0BE9E, 0x0BE9F, 0x0BEA0, 0x0BEA1,
+ 0x0BEA2, 0x0BEA3, 0x0BEA4, 0x0BEA5, 0x0BEA6, 0x0BEA7, 0x0BEA8, 0x0BEA9,
+ 0x0BEAA, 0x0BEAB, 0x0BEAC, 0x0BEAD, 0x0BEAE, 0x0BEAF, 0x0BEB0, 0x0BEB1,
+ 0x0BEB2, 0x0BEB3, 0x0BEB4, 0x0BEB5, 0x0BEB6, 0x0BEB7, 0x0BEB8, 0x0BEB9,
+ 0x0BEBA, 0x0BEBB, 0x0BEBC, 0x0BEBD, 0x0BEBE, 0x0BEBF, 0x0BEC0, 0x0BEC1,
+ 0x0BEC2, 0x0BEC3, 0x0BEC4, 0x0BEC5, 0x0BEC6, 0x0BEC7, 0x0BEC8, 0x0BEC9,
+ 0x0BECA, 0x0BECB, 0x0BECC, 0x0BECD, 0x0BECE, 0x0BECF, 0x0BED0, 0x0BED1,
+ 0x0BED2, 0x0BED3, 0x0BED4, 0x0BED5, 0x0BED6, 0x0BED7, 0x0BED8, 0x0BED9,
+ 0x0BEDA, 0x0BEDB, 0x0BEDC, 0x0BEDD, 0x0BEDE, 0x0BEDF, 0x0BEE0, 0x0BEE1,
+ 0x0BEE2, 0x0BEE3, 0x0BEE4, 0x0BEE5, 0x0BEE6, 0x0BEE7, 0x0BEE8, 0x0BEE9,
+ 0x0BEEA, 0x0BEEB, 0x0BEEC, 0x0BEED, 0x0BEEE, 0x0BEEF, 0x0BEF0, 0x0BEF1,
+ 0x0BEF2, 0x0BEF3, 0x0BEF4, 0x0BEF5, 0x0BEF6, 0x0BEF7, 0x0BEF8, 0x0BEF9,
+ 0x0BEFA, 0x0BEFB, 0x0BEFC, 0x0BEFD, 0x0BEFE, 0x0BEFF, 0x0BF00, 0x0BF01,
+ 0x0BF02, 0x0BF03, 0x0BF04, 0x0BF05, 0x0BF06, 0x0BF07, 0x0BF08, 0x0BF09,
+ 0x0BF0A, 0x0BF0B, 0x0BF0C, 0x0BF0D, 0x0BF0E, 0x0BF0F, 0x0BF10, 0x0BF11,
+ 0x0BF12, 0x0BF13, 0x0BF14, 0x0BF15, 0x0BF16, 0x0BF17, 0x0BF18, 0x0BF19,
+ 0x0BF1A, 0x0BF1B, 0x0BF1C, 0x0BF1D, 0x0BF1E, 0x0BF1F, 0x0BF20, 0x0BF21,
+ 0x0BF22, 0x0BF23, 0x0BF24, 0x0BF25, 0x0BF26, 0x0BF27, 0x0BF28, 0x0BF29,
+ 0x0BF2A, 0x0BF2B, 0x0BF2C, 0x0BF2D, 0x0BF2E, 0x0BF2F, 0x0BF30, 0x0BF31,
+ 0x0BF32, 0x0BF33, 0x0BF34, 0x0BF35, 0x0BF36, 0x0BF37, 0x0BF38, 0x0BF39,
+ 0x0BF3A, 0x0BF3B, 0x0BF3C, 0x0BF3D, 0x0BF3E, 0x0BF3F, 0x0BF40, 0x0BF41,
+ 0x0BF42, 0x0BF43, 0x0BF44, 0x0BF45, 0x0BF46, 0x0BF47, 0x0BF48, 0x0BF49,
+ 0x0BF4A, 0x0BF4B, 0x0BF4C, 0x0BF4D, 0x0BF4E, 0x0BF4F, 0x0BF50, 0x0BF51,
+ 0x0BF52, 0x0BF53, 0x0BF54, 0x0BF55, 0x0BF56, 0x0BF57, 0x0BF58, 0x0BF59,
+ 0x0BF5A, 0x0BF5B, 0x0BF5C, 0x0BF5D, 0x0BF5E, 0x0BF5F, 0x0BF60, 0x0BF61,
+ 0x0BF62, 0x0BF63, 0x0BF64, 0x0BF65, 0x0BF66, 0x0BF67, 0x0BF68, 0x0BF69,
+ 0x0BF6A, 0x0BF6B, 0x0BF6C, 0x0BF6D, 0x0BF6E, 0x0BF6F, 0x0BF70, 0x0BF71,
+ 0x0BF72, 0x0BF73, 0x0BF74, 0x0BF75, 0x0BF76, 0x0BF77, 0x0BF78, 0x0BF79,
+ 0x0BF7A, 0x0BF7B, 0x0BF7C, 0x0BF7D, 0x0BF7E, 0x0BF7F, 0x0BF80, 0x0BF81,
+ 0x0BF82, 0x0BF83, 0x0BF84, 0x0BF85, 0x0BF86, 0x0BF87, 0x0BF88, 0x0BF89,
+ 0x0BF8A, 0x0BF8B, 0x0BF8C, 0x0BF8D, 0x0BF8E, 0x0BF8F, 0x0BF90, 0x0BF91,
+ 0x0BF92, 0x0BF93, 0x0BF94, 0x0BF95, 0x0BF96, 0x0BF97, 0x0BF98, 0x0BF99,
+ 0x0BF9A, 0x0BF9B, 0x0BF9C, 0x0BF9D, 0x0BF9E, 0x0BF9F, 0x0BFA0, 0x0BFA1,
+ 0x0BFA2, 0x0BFA3, 0x0BFA4, 0x0BFA5, 0x0BFA6, 0x0BFA7, 0x0BFA8, 0x0BFA9,
+ 0x0BFAA, 0x0BFAB, 0x0BFAC, 0x0BFAD, 0x0BFAE, 0x0BFAF, 0x0BFB0, 0x0BFB1,
+ 0x0BFB2, 0x0BFB3, 0x0BFB4, 0x0BFB5, 0x0BFB6, 0x0BFB7, 0x0BFB8, 0x0BFB9,
+ 0x0BFBA, 0x0BFBB, 0x0BFBC, 0x0BFBD, 0x0BFBE, 0x0BFBF, 0x0BFC0, 0x0BFC1,
+ 0x0BFC2, 0x0BFC3, 0x0BFC4, 0x0BFC5, 0x0BFC6, 0x0BFC7, 0x0BFC8, 0x0BFC9,
+ 0x0BFCA, 0x0BFCB, 0x0BFCC, 0x0BFCD, 0x0BFCE, 0x0BFCF, 0x0BFD0, 0x0BFD1,
+ 0x0BFD2, 0x0BFD3, 0x0BFD4, 0x0BFD5, 0x0BFD6, 0x0BFD7, 0x0BFD8, 0x0BFD9,
+ 0x0BFDA, 0x0BFDB, 0x0BFDC, 0x0BFDD, 0x0BFDE, 0x0BFDF, 0x0BFE0, 0x0BFE1,
+ 0x0BFE2, 0x0BFE3, 0x0BFE4, 0x0BFE5, 0x0BFE6, 0x0BFE7, 0x0BFE8, 0x0BFE9,
+ 0x0BFEA, 0x0BFEB, 0x0BFEC, 0x0BFED, 0x0BFEE, 0x0BFEF, 0x0BFF0, 0x0BFF1,
+ 0x0BFF2, 0x0BFF3, 0x0BFF4, 0x0BFF5, 0x0BFF6, 0x0BFF7, 0x0BFF8, 0x0BFF9,
+ 0x0BFFA, 0x0BFFB, 0x0BFFC, 0x0BFFD, 0x0BFFE, 0x0BFFF, 0x0C000, 0x0C001,
+ 0x0C002, 0x0C003, 0x0C004, 0x0C005, 0x0C006, 0x0C007, 0x0C008, 0x0C009,
+ 0x0C00A, 0x0C00B, 0x0C00C, 0x0C00D, 0x0C00E, 0x0C00F, 0x0C010, 0x0C011,
+ 0x0C012, 0x0C013, 0x0C014, 0x0C015, 0x0C016, 0x0C017, 0x0C018, 0x0C019,
+ 0x0C01A, 0x0C01B, 0x0C01C, 0x0C01D, 0x0C01E, 0x0C01F, 0x0C020, 0x0C021,
+ 0x0C022, 0x0C023, 0x0C024, 0x0C025, 0x0C026, 0x0C027, 0x0C028, 0x0C029,
+ 0x0C02A, 0x0C02B, 0x0C02C, 0x0C02D, 0x0C02E, 0x0C02F, 0x0C030, 0x0C031,
+ 0x0C032, 0x0C033, 0x0C034, 0x0C035, 0x0C036, 0x0C037, 0x0C038, 0x0C039,
+ 0x0C03A, 0x0C03B, 0x0C03C, 0x0C03D, 0x0C03E, 0x0C03F, 0x0C040, 0x0C041,
+ 0x0C042, 0x0C043, 0x0C044, 0x0C045, 0x0C046, 0x0C047, 0x0C048, 0x0C049,
+ 0x0C04A, 0x0C04B, 0x0C04C, 0x0C04D, 0x0C04E, 0x0C04F, 0x0C050, 0x0C051,
+ 0x0C052, 0x0C053, 0x0C054, 0x0C055, 0x0C056, 0x0C057, 0x0C058, 0x0C059,
+ 0x0C05A, 0x0C05B, 0x0C05C, 0x0C05D, 0x0C05E, 0x0C05F, 0x0C060, 0x0C061,
+ 0x0C062, 0x0C063, 0x0C064, 0x0C065, 0x0C066, 0x0C067, 0x0C068, 0x0C069,
+ 0x0C06A, 0x0C06B, 0x0C06C, 0x0C06D, 0x0C06E, 0x0C06F, 0x0C070, 0x0C071,
+ 0x0C072, 0x0C073, 0x0C074, 0x0C075, 0x0C076, 0x0C077, 0x0C078, 0x0C079,
+ 0x0C07A, 0x0C07B, 0x0C07C, 0x0C07D, 0x0C07E, 0x0C07F, 0x0C080, 0x0C081,
+ 0x0C082, 0x0C083, 0x0C084, 0x0C085, 0x0C086, 0x0C087, 0x0C088, 0x0C089,
+ 0x0C08A, 0x0C08B, 0x0C08C, 0x0C08D, 0x0C08E, 0x0C08F, 0x0C090, 0x0C091,
+ 0x0C092, 0x0C093, 0x0C094, 0x0C095, 0x0C096, 0x0C097, 0x0C098, 0x0C099,
+ 0x0C09A, 0x0C09B, 0x0C09C, 0x0C09D, 0x0C09E, 0x0C09F, 0x0C0A0, 0x0C0A1,
+ 0x0C0A2, 0x0C0A3, 0x0C0A4, 0x0C0A5, 0x0C0A6, 0x0C0A7, 0x0C0A8, 0x0C0A9,
+ 0x0C0AA, 0x0C0AB, 0x0C0AC, 0x0C0AD, 0x0C0AE, 0x0C0AF, 0x0C0B0, 0x0C0B1,
+ 0x0C0B2, 0x0C0B3, 0x0C0B4, 0x0C0B5, 0x0C0B6, 0x0C0B7, 0x0C0B8, 0x0C0B9,
+ 0x0C0BA, 0x0C0BB, 0x0C0BC, 0x0C0BD, 0x0C0BE, 0x0C0BF, 0x0C0C0, 0x0C0C1,
+ 0x0C0C2, 0x0C0C3, 0x0C0C4, 0x0C0C5, 0x0C0C6, 0x0C0C7, 0x0C0C8, 0x0C0C9,
+ 0x0C0CA, 0x0C0CB, 0x0C0CC, 0x0C0CD, 0x0C0CE, 0x0C0CF, 0x0C0D0, 0x0C0D1,
+ 0x0C0D2, 0x0C0D3, 0x0C0D4, 0x0C0D5, 0x0C0D6, 0x0C0D7, 0x0C0D8, 0x0C0D9,
+ 0x0C0DA, 0x0C0DB, 0x0C0DC, 0x0C0DD, 0x0C0DE, 0x0C0DF, 0x0C0E0, 0x0C0E1,
+ 0x0C0E2, 0x0C0E3, 0x0C0E4, 0x0C0E5, 0x0C0E6, 0x0C0E7, 0x0C0E8, 0x0C0E9,
+ 0x0C0EA, 0x0C0EB, 0x0C0EC, 0x0C0ED, 0x0C0EE, 0x0C0EF, 0x0C0F0, 0x0C0F1,
+ 0x0C0F2, 0x0C0F3, 0x0C0F4, 0x0C0F5, 0x0C0F6, 0x0C0F7, 0x0C0F8, 0x0C0F9,
+ 0x0C0FA, 0x0C0FB, 0x0C0FC, 0x0C0FD, 0x0C0FE, 0x0C0FF, 0x0C100, 0x0C101,
+ 0x0C102, 0x0C103, 0x0C104, 0x0C105, 0x0C106, 0x0C107, 0x0C108, 0x0C109,
+ 0x0C10A, 0x0C10B, 0x0C10C, 0x0C10D, 0x0C10E, 0x0C10F, 0x0C110, 0x0C111,
+ 0x0C112, 0x0C113, 0x0C114, 0x0C115, 0x0C116, 0x0C117, 0x0C118, 0x0C119,
+ 0x0C11A, 0x0C11B, 0x0C11C, 0x0C11D, 0x0C11E, 0x0C11F, 0x0C120, 0x0C121,
+ 0x0C122, 0x0C123, 0x0C124, 0x0C125, 0x0C126, 0x0C127, 0x0C128, 0x0C129,
+ 0x0C12A, 0x0C12B, 0x0C12C, 0x0C12D, 0x0C12E, 0x0C12F, 0x0C130, 0x0C131,
+ 0x0C132, 0x0C133, 0x0C134, 0x0C135, 0x0C136, 0x0C137, 0x0C138, 0x0C139,
+ 0x0C13A, 0x0C13B, 0x0C13C, 0x0C13D, 0x0C13E, 0x0C13F, 0x0C140, 0x0C141,
+ 0x0C142, 0x0C143, 0x0C144, 0x0C145, 0x0C146, 0x0C147, 0x0C148, 0x0C149,
+ 0x0C14A, 0x0C14B, 0x0C14C, 0x0C14D, 0x0C14E, 0x0C14F, 0x0C150, 0x0C151,
+ 0x0C152, 0x0C153, 0x0C154, 0x0C155, 0x0C156, 0x0C157, 0x0C158, 0x0C159,
+ 0x0C15A, 0x0C15B, 0x0C15C, 0x0C15D, 0x0C15E, 0x0C15F, 0x0C160, 0x0C161,
+ 0x0C162, 0x0C163, 0x0C164, 0x0C165, 0x0C166, 0x0C167, 0x0C168, 0x0C169,
+ 0x0C16A, 0x0C16B, 0x0C16C, 0x0C16D, 0x0C16E, 0x0C16F, 0x0C170, 0x0C171,
+ 0x0C172, 0x0C173, 0x0C174, 0x0C175, 0x0C176, 0x0C177, 0x0C178, 0x0C179,
+ 0x0C17A, 0x0C17B, 0x0C17C, 0x0C17D, 0x0C17E, 0x0C17F, 0x0C180, 0x0C181,
+ 0x0C182, 0x0C183, 0x0C184, 0x0C185, 0x0C186, 0x0C187, 0x0C188, 0x0C189,
+ 0x0C18A, 0x0C18B, 0x0C18C, 0x0C18D, 0x0C18E, 0x0C18F, 0x0C190, 0x0C191,
+ 0x0C192, 0x0C193, 0x0C194, 0x0C195, 0x0C196, 0x0C197, 0x0C198, 0x0C199,
+ 0x0C19A, 0x0C19B, 0x0C19C, 0x0C19D, 0x0C19E, 0x0C19F, 0x0C1A0, 0x0C1A1,
+ 0x0C1A2, 0x0C1A3, 0x0C1A4, 0x0C1A5, 0x0C1A6, 0x0C1A7, 0x0C1A8, 0x0C1A9,
+ 0x0C1AA, 0x0C1AB, 0x0C1AC, 0x0C1AD, 0x0C1AE, 0x0C1AF, 0x0C1B0, 0x0C1B1,
+ 0x0C1B2, 0x0C1B3, 0x0C1B4, 0x0C1B5, 0x0C1B6, 0x0C1B7, 0x0C1B8, 0x0C1B9,
+ 0x0C1BA, 0x0C1BB, 0x0C1BC, 0x0C1BD, 0x0C1BE, 0x0C1BF, 0x0C1C0, 0x0C1C1,
+ 0x0C1C2, 0x0C1C3, 0x0C1C4, 0x0C1C5, 0x0C1C6, 0x0C1C7, 0x0C1C8, 0x0C1C9,
+ 0x0C1CA, 0x0C1CB, 0x0C1CC, 0x0C1CD, 0x0C1CE, 0x0C1CF, 0x0C1D0, 0x0C1D1,
+ 0x0C1D2, 0x0C1D3, 0x0C1D4, 0x0C1D5, 0x0C1D6, 0x0C1D7, 0x0C1D8, 0x0C1D9,
+ 0x0C1DA, 0x0C1DB, 0x0C1DC, 0x0C1DD, 0x0C1DE, 0x0C1DF, 0x0C1E0, 0x0C1E1,
+ 0x0C1E2, 0x0C1E3, 0x0C1E4, 0x0C1E5, 0x0C1E6, 0x0C1E7, 0x0C1E8, 0x0C1E9,
+ 0x0C1EA, 0x0C1EB, 0x0C1EC, 0x0C1ED, 0x0C1EE, 0x0C1EF, 0x0C1F0, 0x0C1F1,
+ 0x0C1F2, 0x0C1F3, 0x0C1F4, 0x0C1F5, 0x0C1F6, 0x0C1F7, 0x0C1F8, 0x0C1F9,
+ 0x0C1FA, 0x0C1FB, 0x0C1FC, 0x0C1FD, 0x0C1FE, 0x0C1FF, 0x0C200, 0x0C201,
+ 0x0C202, 0x0C203, 0x0C204, 0x0C205, 0x0C206, 0x0C207, 0x0C208, 0x0C209,
+ 0x0C20A, 0x0C20B, 0x0C20C, 0x0C20D, 0x0C20E, 0x0C20F, 0x0C210, 0x0C211,
+ 0x0C212, 0x0C213, 0x0C214, 0x0C215, 0x0C216, 0x0C217, 0x0C218, 0x0C219,
+ 0x0C21A, 0x0C21B, 0x0C21C, 0x0C21D, 0x0C21E, 0x0C21F, 0x0C220, 0x0C221,
+ 0x0C222, 0x0C223, 0x0C224, 0x0C225, 0x0C226, 0x0C227, 0x0C228, 0x0C229,
+ 0x0C22A, 0x0C22B, 0x0C22C, 0x0C22D, 0x0C22E, 0x0C22F, 0x0C230, 0x0C231,
+ 0x0C232, 0x0C233, 0x0C234, 0x0C235, 0x0C236, 0x0C237, 0x0C238, 0x0C239,
+ 0x0C23A, 0x0C23B, 0x0C23C, 0x0C23D, 0x0C23E, 0x0C23F, 0x0C240, 0x0C241,
+ 0x0C242, 0x0C243, 0x0C244, 0x0C245, 0x0C246, 0x0C247, 0x0C248, 0x0C249,
+ 0x0C24A, 0x0C24B, 0x0C24C, 0x0C24D, 0x0C24E, 0x0C24F, 0x0C250, 0x0C251,
+ 0x0C252, 0x0C253, 0x0C254, 0x0C255, 0x0C256, 0x0C257, 0x0C258, 0x0C259,
+ 0x0C25A, 0x0C25B, 0x0C25C, 0x0C25D, 0x0C25E, 0x0C25F, 0x0C260, 0x0C261,
+ 0x0C262, 0x0C263, 0x0C264, 0x0C265, 0x0C266, 0x0C267, 0x0C268, 0x0C269,
+ 0x0C26A, 0x0C26B, 0x0C26C, 0x0C26D, 0x0C26E, 0x0C26F, 0x0C270, 0x0C271,
+ 0x0C272, 0x0C273, 0x0C274, 0x0C275, 0x0C276, 0x0C277, 0x0C278, 0x0C279,
+ 0x0C27A, 0x0C27B, 0x0C27C, 0x0C27D, 0x0C27E, 0x0C27F, 0x0C280, 0x0C281,
+ 0x0C282, 0x0C283, 0x0C284, 0x0C285, 0x0C286, 0x0C287, 0x0C288, 0x0C289,
+ 0x0C28A, 0x0C28B, 0x0C28C, 0x0C28D, 0x0C28E, 0x0C28F, 0x0C290, 0x0C291,
+ 0x0C292, 0x0C293, 0x0C294, 0x0C295, 0x0C296, 0x0C297, 0x0C298, 0x0C299,
+ 0x0C29A, 0x0C29B, 0x0C29C, 0x0C29D, 0x0C29E, 0x0C29F, 0x0C2A0, 0x0C2A1,
+ 0x0C2A2, 0x0C2A3, 0x0C2A4, 0x0C2A5, 0x0C2A6, 0x0C2A7, 0x0C2A8, 0x0C2A9,
+ 0x0C2AA, 0x0C2AB, 0x0C2AC, 0x0C2AD, 0x0C2AE, 0x0C2AF, 0x0C2B0, 0x0C2B1,
+ 0x0C2B2, 0x0C2B3, 0x0C2B4, 0x0C2B5, 0x0C2B6, 0x0C2B7, 0x0C2B8, 0x0C2B9,
+ 0x0C2BA, 0x0C2BB, 0x0C2BC, 0x0C2BD, 0x0C2BE, 0x0C2BF, 0x0C2C0, 0x0C2C1,
+ 0x0C2C2, 0x0C2C3, 0x0C2C4, 0x0C2C5, 0x0C2C6, 0x0C2C7, 0x0C2C8, 0x0C2C9,
+ 0x0C2CA, 0x0C2CB, 0x0C2CC, 0x0C2CD, 0x0C2CE, 0x0C2CF, 0x0C2D0, 0x0C2D1,
+ 0x0C2D2, 0x0C2D3, 0x0C2D4, 0x0C2D5, 0x0C2D6, 0x0C2D7, 0x0C2D8, 0x0C2D9,
+ 0x0C2DA, 0x0C2DB, 0x0C2DC, 0x0C2DD, 0x0C2DE, 0x0C2DF, 0x0C2E0, 0x0C2E1,
+ 0x0C2E2, 0x0C2E3, 0x0C2E4, 0x0C2E5, 0x0C2E6, 0x0C2E7, 0x0C2E8, 0x0C2E9,
+ 0x0C2EA, 0x0C2EB, 0x0C2EC, 0x0C2ED, 0x0C2EE, 0x0C2EF, 0x0C2F0, 0x0C2F1,
+ 0x0C2F2, 0x0C2F3, 0x0C2F4, 0x0C2F5, 0x0C2F6, 0x0C2F7, 0x0C2F8, 0x0C2F9,
+ 0x0C2FA, 0x0C2FB, 0x0C2FC, 0x0C2FD, 0x0C2FE, 0x0C2FF, 0x0C300, 0x0C301,
+ 0x0C302, 0x0C303, 0x0C304, 0x0C305, 0x0C306, 0x0C307, 0x0C308, 0x0C309,
+ 0x0C30A, 0x0C30B, 0x0C30C, 0x0C30D, 0x0C30E, 0x0C30F, 0x0C310, 0x0C311,
+ 0x0C312, 0x0C313, 0x0C314, 0x0C315, 0x0C316, 0x0C317, 0x0C318, 0x0C319,
+ 0x0C31A, 0x0C31B, 0x0C31C, 0x0C31D, 0x0C31E, 0x0C31F, 0x0C320, 0x0C321,
+ 0x0C322, 0x0C323, 0x0C324, 0x0C325, 0x0C326, 0x0C327, 0x0C328, 0x0C329,
+ 0x0C32A, 0x0C32B, 0x0C32C, 0x0C32D, 0x0C32E, 0x0C32F, 0x0C330, 0x0C331,
+ 0x0C332, 0x0C333, 0x0C334, 0x0C335, 0x0C336, 0x0C337, 0x0C338, 0x0C339,
+ 0x0C33A, 0x0C33B, 0x0C33C, 0x0C33D, 0x0C33E, 0x0C33F, 0x0C340, 0x0C341,
+ 0x0C342, 0x0C343, 0x0C344, 0x0C345, 0x0C346, 0x0C347, 0x0C348, 0x0C349,
+ 0x0C34A, 0x0C34B, 0x0C34C, 0x0C34D, 0x0C34E, 0x0C34F, 0x0C350, 0x0C351,
+ 0x0C352, 0x0C353, 0x0C354, 0x0C355, 0x0C356, 0x0C357, 0x0C358, 0x0C359,
+ 0x0C35A, 0x0C35B, 0x0C35C, 0x0C35D, 0x0C35E, 0x0C35F, 0x0C360, 0x0C361,
+ 0x0C362, 0x0C363, 0x0C364, 0x0C365, 0x0C366, 0x0C367, 0x0C368, 0x0C369,
+ 0x0C36A, 0x0C36B, 0x0C36C, 0x0C36D, 0x0C36E, 0x0C36F, 0x0C370, 0x0C371,
+ 0x0C372, 0x0C373, 0x0C374, 0x0C375, 0x0C376, 0x0C377, 0x0C378, 0x0C379,
+ 0x0C37A, 0x0C37B, 0x0C37C, 0x0C37D, 0x0C37E, 0x0C37F, 0x0C380, 0x0C381,
+ 0x0C382, 0x0C383, 0x0C384, 0x0C385, 0x0C386, 0x0C387, 0x0C388, 0x0C389,
+ 0x0C38A, 0x0C38B, 0x0C38C, 0x0C38D, 0x0C38E, 0x0C38F, 0x0C390, 0x0C391,
+ 0x0C392, 0x0C393, 0x0C394, 0x0C395, 0x0C396, 0x0C397, 0x0C398, 0x0C399,
+ 0x0C39A, 0x0C39B, 0x0C39C, 0x0C39D, 0x0C39E, 0x0C39F, 0x0C3A0, 0x0C3A1,
+ 0x0C3A2, 0x0C3A3, 0x0C3A4, 0x0C3A5, 0x0C3A6, 0x0C3A7, 0x0C3A8, 0x0C3A9,
+ 0x0C3AA, 0x0C3AB, 0x0C3AC, 0x0C3AD, 0x0C3AE, 0x0C3AF, 0x0C3B0, 0x0C3B1,
+ 0x0C3B2, 0x0C3B3, 0x0C3B4, 0x0C3B5, 0x0C3B6, 0x0C3B7, 0x0C3B8, 0x0C3B9,
+ 0x0C3BA, 0x0C3BB, 0x0C3BC, 0x0C3BD, 0x0C3BE, 0x0C3BF, 0x0C3C0, 0x0C3C1,
+ 0x0C3C2, 0x0C3C3, 0x0C3C4, 0x0C3C5, 0x0C3C6, 0x0C3C7, 0x0C3C8, 0x0C3C9,
+ 0x0C3CA, 0x0C3CB, 0x0C3CC, 0x0C3CD, 0x0C3CE, 0x0C3CF, 0x0C3D0, 0x0C3D1,
+ 0x0C3D2, 0x0C3D3, 0x0C3D4, 0x0C3D5, 0x0C3D6, 0x0C3D7, 0x0C3D8, 0x0C3D9,
+ 0x0C3DA, 0x0C3DB, 0x0C3DC, 0x0C3DD, 0x0C3DE, 0x0C3DF, 0x0C3E0, 0x0C3E1,
+ 0x0C3E2, 0x0C3E3, 0x0C3E4, 0x0C3E5, 0x0C3E6, 0x0C3E7, 0x0C3E8, 0x0C3E9,
+ 0x0C3EA, 0x0C3EB, 0x0C3EC, 0x0C3ED, 0x0C3EE, 0x0C3EF, 0x0C3F0, 0x0C3F1,
+ 0x0C3F2, 0x0C3F3, 0x0C3F4, 0x0C3F5, 0x0C3F6, 0x0C3F7, 0x0C3F8, 0x0C3F9,
+ 0x0C3FA, 0x0C3FB, 0x0C3FC, 0x0C3FD, 0x0C3FE, 0x0C3FF, 0x0C400, 0x0C401,
+ 0x0C402, 0x0C403, 0x0C404, 0x0C405, 0x0C406, 0x0C407, 0x0C408, 0x0C409,
+ 0x0C40A, 0x0C40B, 0x0C40C, 0x0C40D, 0x0C40E, 0x0C40F, 0x0C410, 0x0C411,
+ 0x0C412, 0x0C413, 0x0C414, 0x0C415, 0x0C416, 0x0C417, 0x0C418, 0x0C419,
+ 0x0C41A, 0x0C41B, 0x0C41C, 0x0C41D, 0x0C41E, 0x0C41F, 0x0C420, 0x0C421,
+ 0x0C422, 0x0C423, 0x0C424, 0x0C425, 0x0C426, 0x0C427, 0x0C428, 0x0C429,
+ 0x0C42A, 0x0C42B, 0x0C42C, 0x0C42D, 0x0C42E, 0x0C42F, 0x0C430, 0x0C431,
+ 0x0C432, 0x0C433, 0x0C434, 0x0C435, 0x0C436, 0x0C437, 0x0C438, 0x0C439,
+ 0x0C43A, 0x0C43B, 0x0C43C, 0x0C43D, 0x0C43E, 0x0C43F, 0x0C440, 0x0C441,
+ 0x0C442, 0x0C443, 0x0C444, 0x0C445, 0x0C446, 0x0C447, 0x0C448, 0x0C449,
+ 0x0C44A, 0x0C44B, 0x0C44C, 0x0C44D, 0x0C44E, 0x0C44F, 0x0C450, 0x0C451,
+ 0x0C452, 0x0C453, 0x0C454, 0x0C455, 0x0C456, 0x0C457, 0x0C458, 0x0C459,
+ 0x0C45A, 0x0C45B, 0x0C45C, 0x0C45D, 0x0C45E, 0x0C45F, 0x0C460, 0x0C461,
+ 0x0C462, 0x0C463, 0x0C464, 0x0C465, 0x0C466, 0x0C467, 0x0C468, 0x0C469,
+ 0x0C46A, 0x0C46B, 0x0C46C, 0x0C46D, 0x0C46E, 0x0C46F, 0x0C470, 0x0C471,
+ 0x0C472, 0x0C473, 0x0C474, 0x0C475, 0x0C476, 0x0C477, 0x0C478, 0x0C479,
+ 0x0C47A, 0x0C47B, 0x0C47C, 0x0C47D, 0x0C47E, 0x0C47F, 0x0C480, 0x0C481,
+ 0x0C482, 0x0C483, 0x0C484, 0x0C485, 0x0C486, 0x0C487, 0x0C488, 0x0C489,
+ 0x0C48A, 0x0C48B, 0x0C48C, 0x0C48D, 0x0C48E, 0x0C48F, 0x0C490, 0x0C491,
+ 0x0C492, 0x0C493, 0x0C494, 0x0C495, 0x0C496, 0x0C497, 0x0C498, 0x0C499,
+ 0x0C49A, 0x0C49B, 0x0C49C, 0x0C49D, 0x0C49E, 0x0C49F, 0x0C4A0, 0x0C4A1,
+ 0x0C4A2, 0x0C4A3, 0x0C4A4, 0x0C4A5, 0x0C4A6, 0x0C4A7, 0x0C4A8, 0x0C4A9,
+ 0x0C4AA, 0x0C4AB, 0x0C4AC, 0x0C4AD, 0x0C4AE, 0x0C4AF, 0x0C4B0, 0x0C4B1,
+ 0x0C4B2, 0x0C4B3, 0x0C4B4, 0x0C4B5, 0x0C4B6, 0x0C4B7, 0x0C4B8, 0x0C4B9,
+ 0x0C4BA, 0x0C4BB, 0x0C4BC, 0x0C4BD, 0x0C4BE, 0x0C4BF, 0x0C4C0, 0x0C4C1,
+ 0x0C4C2, 0x0C4C3, 0x0C4C4, 0x0C4C5, 0x0C4C6, 0x0C4C7, 0x0C4C8, 0x0C4C9,
+ 0x0C4CA, 0x0C4CB, 0x0C4CC, 0x0C4CD, 0x0C4CE, 0x0C4CF, 0x0C4D0, 0x0C4D1,
+ 0x0C4D2, 0x0C4D3, 0x0C4D4, 0x0C4D5, 0x0C4D6, 0x0C4D7, 0x0C4D8, 0x0C4D9,
+ 0x0C4DA, 0x0C4DB, 0x0C4DC, 0x0C4DD, 0x0C4DE, 0x0C4DF, 0x0C4E0, 0x0C4E1,
+ 0x0C4E2, 0x0C4E3, 0x0C4E4, 0x0C4E5, 0x0C4E6, 0x0C4E7, 0x0C4E8, 0x0C4E9,
+ 0x0C4EA, 0x0C4EB, 0x0C4EC, 0x0C4ED, 0x0C4EE, 0x0C4EF, 0x0C4F0, 0x0C4F1,
+ 0x0C4F2, 0x0C4F3, 0x0C4F4, 0x0C4F5, 0x0C4F6, 0x0C4F7, 0x0C4F8, 0x0C4F9,
+ 0x0C4FA, 0x0C4FB, 0x0C4FC, 0x0C4FD, 0x0C4FE, 0x0C4FF, 0x0C500, 0x0C501,
+ 0x0C502, 0x0C503, 0x0C504, 0x0C505, 0x0C506, 0x0C507, 0x0C508, 0x0C509,
+ 0x0C50A, 0x0C50B, 0x0C50C, 0x0C50D, 0x0C50E, 0x0C50F, 0x0C510, 0x0C511,
+ 0x0C512, 0x0C513, 0x0C514, 0x0C515, 0x0C516, 0x0C517, 0x0C518, 0x0C519,
+ 0x0C51A, 0x0C51B, 0x0C51C, 0x0C51D, 0x0C51E, 0x0C51F, 0x0C520, 0x0C521,
+ 0x0C522, 0x0C523, 0x0C524, 0x0C525, 0x0C526, 0x0C527, 0x0C528, 0x0C529,
+ 0x0C52A, 0x0C52B, 0x0C52C, 0x0C52D, 0x0C52E, 0x0C52F, 0x0C530, 0x0C531,
+ 0x0C532, 0x0C533, 0x0C534, 0x0C535, 0x0C536, 0x0C537, 0x0C538, 0x0C539,
+ 0x0C53A, 0x0C53B, 0x0C53C, 0x0C53D, 0x0C53E, 0x0C53F, 0x0C540, 0x0C541,
+ 0x0C542, 0x0C543, 0x0C544, 0x0C545, 0x0C546, 0x0C547, 0x0C548, 0x0C549,
+ 0x0C54A, 0x0C54B, 0x0C54C, 0x0C54D, 0x0C54E, 0x0C54F, 0x0C550, 0x0C551,
+ 0x0C552, 0x0C553, 0x0C554, 0x0C555, 0x0C556, 0x0C557, 0x0C558, 0x0C559,
+ 0x0C55A, 0x0C55B, 0x0C55C, 0x0C55D, 0x0C55E, 0x0C55F, 0x0C560, 0x0C561,
+ 0x0C562, 0x0C563, 0x0C564, 0x0C565, 0x0C566, 0x0C567, 0x0C568, 0x0C569,
+ 0x0C56A, 0x0C56B, 0x0C56C, 0x0C56D, 0x0C56E, 0x0C56F, 0x0C570, 0x0C571,
+ 0x0C572, 0x0C573, 0x0C574, 0x0C575, 0x0C576, 0x0C577, 0x0C578, 0x0C579,
+ 0x0C57A, 0x0C57B, 0x0C57C, 0x0C57D, 0x0C57E, 0x0C57F, 0x0C580, 0x0C581,
+ 0x0C582, 0x0C583, 0x0C584, 0x0C585, 0x0C586, 0x0C587, 0x0C588, 0x0C589,
+ 0x0C58A, 0x0C58B, 0x0C58C, 0x0C58D, 0x0C58E, 0x0C58F, 0x0C590, 0x0C591,
+ 0x0C592, 0x0C593, 0x0C594, 0x0C595, 0x0C596, 0x0C597, 0x0C598, 0x0C599,
+ 0x0C59A, 0x0C59B, 0x0C59C, 0x0C59D, 0x0C59E, 0x0C59F, 0x0C5A0, 0x0C5A1,
+ 0x0C5A2, 0x0C5A3, 0x0C5A4, 0x0C5A5, 0x0C5A6, 0x0C5A7, 0x0C5A8, 0x0C5A9,
+ 0x0C5AA, 0x0C5AB, 0x0C5AC, 0x0C5AD, 0x0C5AE, 0x0C5AF, 0x0C5B0, 0x0C5B1,
+ 0x0C5B2, 0x0C5B3, 0x0C5B4, 0x0C5B5, 0x0C5B6, 0x0C5B7, 0x0C5B8, 0x0C5B9,
+ 0x0C5BA, 0x0C5BB, 0x0C5BC, 0x0C5BD, 0x0C5BE, 0x0C5BF, 0x0C5C0, 0x0C5C1,
+ 0x0C5C2, 0x0C5C3, 0x0C5C4, 0x0C5C5, 0x0C5C6, 0x0C5C7, 0x0C5C8, 0x0C5C9,
+ 0x0C5CA, 0x0C5CB, 0x0C5CC, 0x0C5CD, 0x0C5CE, 0x0C5CF, 0x0C5D0, 0x0C5D1,
+ 0x0C5D2, 0x0C5D3, 0x0C5D4, 0x0C5D5, 0x0C5D6, 0x0C5D7, 0x0C5D8, 0x0C5D9,
+ 0x0C5DA, 0x0C5DB, 0x0C5DC, 0x0C5DD, 0x0C5DE, 0x0C5DF, 0x0C5E0, 0x0C5E1,
+ 0x0C5E2, 0x0C5E3, 0x0C5E4, 0x0C5E5, 0x0C5E6, 0x0C5E7, 0x0C5E8, 0x0C5E9,
+ 0x0C5EA, 0x0C5EB, 0x0C5EC, 0x0C5ED, 0x0C5EE, 0x0C5EF, 0x0C5F0, 0x0C5F1,
+ 0x0C5F2, 0x0C5F3, 0x0C5F4, 0x0C5F5, 0x0C5F6, 0x0C5F7, 0x0C5F8, 0x0C5F9,
+ 0x0C5FA, 0x0C5FB, 0x0C5FC, 0x0C5FD, 0x0C5FE, 0x0C5FF, 0x0C600, 0x0C601,
+ 0x0C602, 0x0C603, 0x0C604, 0x0C605, 0x0C606, 0x0C607, 0x0C608, 0x0C609,
+ 0x0C60A, 0x0C60B, 0x0C60C, 0x0C60D, 0x0C60E, 0x0C60F, 0x0C610, 0x0C611,
+ 0x0C612, 0x0C613, 0x0C614, 0x0C615, 0x0C616, 0x0C617, 0x0C618, 0x0C619,
+ 0x0C61A, 0x0C61B, 0x0C61C, 0x0C61D, 0x0C61E, 0x0C61F, 0x0C620, 0x0C621,
+ 0x0C622, 0x0C623, 0x0C624, 0x0C625, 0x0C626, 0x0C627, 0x0C628, 0x0C629,
+ 0x0C62A, 0x0C62B, 0x0C62C, 0x0C62D, 0x0C62E, 0x0C62F, 0x0C630, 0x0C631,
+ 0x0C632, 0x0C633, 0x0C634, 0x0C635, 0x0C636, 0x0C637, 0x0C638, 0x0C639,
+ 0x0C63A, 0x0C63B, 0x0C63C, 0x0C63D, 0x0C63E, 0x0C63F, 0x0C640, 0x0C641,
+ 0x0C642, 0x0C643, 0x0C644, 0x0C645, 0x0C646, 0x0C647, 0x0C648, 0x0C649,
+ 0x0C64A, 0x0C64B, 0x0C64C, 0x0C64D, 0x0C64E, 0x0C64F, 0x0C650, 0x0C651,
+ 0x0C652, 0x0C653, 0x0C654, 0x0C655, 0x0C656, 0x0C657, 0x0C658, 0x0C659,
+ 0x0C65A, 0x0C65B, 0x0C65C, 0x0C65D, 0x0C65E, 0x0C65F, 0x0C660, 0x0C661,
+ 0x0C662, 0x0C663, 0x0C664, 0x0C665, 0x0C666, 0x0C667, 0x0C668, 0x0C669,
+ 0x0C66A, 0x0C66B, 0x0C66C, 0x0C66D, 0x0C66E, 0x0C66F, 0x0C670, 0x0C671,
+ 0x0C672, 0x0C673, 0x0C674, 0x0C675, 0x0C676, 0x0C677, 0x0C678, 0x0C679,
+ 0x0C67A, 0x0C67B, 0x0C67C, 0x0C67D, 0x0C67E, 0x0C67F, 0x0C680, 0x0C681,
+ 0x0C682, 0x0C683, 0x0C684, 0x0C685, 0x0C686, 0x0C687, 0x0C688, 0x0C689,
+ 0x0C68A, 0x0C68B, 0x0C68C, 0x0C68D, 0x0C68E, 0x0C68F, 0x0C690, 0x0C691,
+ 0x0C692, 0x0C693, 0x0C694, 0x0C695, 0x0C696, 0x0C697, 0x0C698, 0x0C699,
+ 0x0C69A, 0x0C69B, 0x0C69C, 0x0C69D, 0x0C69E, 0x0C69F, 0x0C6A0, 0x0C6A1,
+ 0x0C6A2, 0x0C6A3, 0x0C6A4, 0x0C6A5, 0x0C6A6, 0x0C6A7, 0x0C6A8, 0x0C6A9,
+ 0x0C6AA, 0x0C6AB, 0x0C6AC, 0x0C6AD, 0x0C6AE, 0x0C6AF, 0x0C6B0, 0x0C6B1,
+ 0x0C6B2, 0x0C6B3, 0x0C6B4, 0x0C6B5, 0x0C6B6, 0x0C6B7, 0x0C6B8, 0x0C6B9,
+ 0x0C6BA, 0x0C6BB, 0x0C6BC, 0x0C6BD, 0x0C6BE, 0x0C6BF, 0x0C6C0, 0x0C6C1,
+ 0x0C6C2, 0x0C6C3, 0x0C6C4, 0x0C6C5, 0x0C6C6, 0x0C6C7, 0x0C6C8, 0x0C6C9,
+ 0x0C6CA, 0x0C6CB, 0x0C6CC, 0x0C6CD, 0x0C6CE, 0x0C6CF, 0x0C6D0, 0x0C6D1,
+ 0x0C6D2, 0x0C6D3, 0x0C6D4, 0x0C6D5, 0x0C6D6, 0x0C6D7, 0x0C6D8, 0x0C6D9,
+ 0x0C6DA, 0x0C6DB, 0x0C6DC, 0x0C6DD, 0x0C6DE, 0x0C6DF, 0x0C6E0, 0x0C6E1,
+ 0x0C6E2, 0x0C6E3, 0x0C6E4, 0x0C6E5, 0x0C6E6, 0x0C6E7, 0x0C6E8, 0x0C6E9,
+ 0x0C6EA, 0x0C6EB, 0x0C6EC, 0x0C6ED, 0x0C6EE, 0x0C6EF, 0x0C6F0, 0x0C6F1,
+ 0x0C6F2, 0x0C6F3, 0x0C6F4, 0x0C6F5, 0x0C6F6, 0x0C6F7, 0x0C6F8, 0x0C6F9,
+ 0x0C6FA, 0x0C6FB, 0x0C6FC, 0x0C6FD, 0x0C6FE, 0x0C6FF, 0x0C700, 0x0C701,
+ 0x0C702, 0x0C703, 0x0C704, 0x0C705, 0x0C706, 0x0C707, 0x0C708, 0x0C709,
+ 0x0C70A, 0x0C70B, 0x0C70C, 0x0C70D, 0x0C70E, 0x0C70F, 0x0C710, 0x0C711,
+ 0x0C712, 0x0C713, 0x0C714, 0x0C715, 0x0C716, 0x0C717, 0x0C718, 0x0C719,
+ 0x0C71A, 0x0C71B, 0x0C71C, 0x0C71D, 0x0C71E, 0x0C71F, 0x0C720, 0x0C721,
+ 0x0C722, 0x0C723, 0x0C724, 0x0C725, 0x0C726, 0x0C727, 0x0C728, 0x0C729,
+ 0x0C72A, 0x0C72B, 0x0C72C, 0x0C72D, 0x0C72E, 0x0C72F, 0x0C730, 0x0C731,
+ 0x0C732, 0x0C733, 0x0C734, 0x0C735, 0x0C736, 0x0C737, 0x0C738, 0x0C739,
+ 0x0C73A, 0x0C73B, 0x0C73C, 0x0C73D, 0x0C73E, 0x0C73F, 0x0C740, 0x0C741,
+ 0x0C742, 0x0C743, 0x0C744, 0x0C745, 0x0C746, 0x0C747, 0x0C748, 0x0C749,
+ 0x0C74A, 0x0C74B, 0x0C74C, 0x0C74D, 0x0C74E, 0x0C74F, 0x0C750, 0x0C751,
+ 0x0C752, 0x0C753, 0x0C754, 0x0C755, 0x0C756, 0x0C757, 0x0C758, 0x0C759,
+ 0x0C75A, 0x0C75B, 0x0C75C, 0x0C75D, 0x0C75E, 0x0C75F, 0x0C760, 0x0C761,
+ 0x0C762, 0x0C763, 0x0C764, 0x0C765, 0x0C766, 0x0C767, 0x0C768, 0x0C769,
+ 0x0C76A, 0x0C76B, 0x0C76C, 0x0C76D, 0x0C76E, 0x0C76F, 0x0C770, 0x0C771,
+ 0x0C772, 0x0C773, 0x0C774, 0x0C775, 0x0C776, 0x0C777, 0x0C778, 0x0C779,
+ 0x0C77A, 0x0C77B, 0x0C77C, 0x0C77D, 0x0C77E, 0x0C77F, 0x0C780, 0x0C781,
+ 0x0C782, 0x0C783, 0x0C784, 0x0C785, 0x0C786, 0x0C787, 0x0C788, 0x0C789,
+ 0x0C78A, 0x0C78B, 0x0C78C, 0x0C78D, 0x0C78E, 0x0C78F, 0x0C790, 0x0C791,
+ 0x0C792, 0x0C793, 0x0C794, 0x0C795, 0x0C796, 0x0C797, 0x0C798, 0x0C799,
+ 0x0C79A, 0x0C79B, 0x0C79C, 0x0C79D, 0x0C79E, 0x0C79F, 0x0C7A0, 0x0C7A1,
+ 0x0C7A2, 0x0C7A3, 0x0C7A4, 0x0C7A5, 0x0C7A6, 0x0C7A7, 0x0C7A8, 0x0C7A9,
+ 0x0C7AA, 0x0C7AB, 0x0C7AC, 0x0C7AD, 0x0C7AE, 0x0C7AF, 0x0C7B0, 0x0C7B1,
+ 0x0C7B2, 0x0C7B3, 0x0C7B4, 0x0C7B5, 0x0C7B6, 0x0C7B7, 0x0C7B8, 0x0C7B9,
+ 0x0C7BA, 0x0C7BB, 0x0C7BC, 0x0C7BD, 0x0C7BE, 0x0C7BF, 0x0C7C0, 0x0C7C1,
+ 0x0C7C2, 0x0C7C3, 0x0C7C4, 0x0C7C5, 0x0C7C6, 0x0C7C7, 0x0C7C8, 0x0C7C9,
+ 0x0C7CA, 0x0C7CB, 0x0C7CC, 0x0C7CD, 0x0C7CE, 0x0C7CF, 0x0C7D0, 0x0C7D1,
+ 0x0C7D2, 0x0C7D3, 0x0C7D4, 0x0C7D5, 0x0C7D6, 0x0C7D7, 0x0C7D8, 0x0C7D9,
+ 0x0C7DA, 0x0C7DB, 0x0C7DC, 0x0C7DD, 0x0C7DE, 0x0C7DF, 0x0C7E0, 0x0C7E1,
+ 0x0C7E2, 0x0C7E3, 0x0C7E4, 0x0C7E5, 0x0C7E6, 0x0C7E7, 0x0C7E8, 0x0C7E9,
+ 0x0C7EA, 0x0C7EB, 0x0C7EC, 0x0C7ED, 0x0C7EE, 0x0C7EF, 0x0C7F0, 0x0C7F1,
+ 0x0C7F2, 0x0C7F3, 0x0C7F4, 0x0C7F5, 0x0C7F6, 0x0C7F7, 0x0C7F8, 0x0C7F9,
+ 0x0C7FA, 0x0C7FB, 0x0C7FC, 0x0C7FD, 0x0C7FE, 0x0C7FF, 0x0C800, 0x0C801,
+ 0x0C802, 0x0C803, 0x0C804, 0x0C805, 0x0C806, 0x0C807, 0x0C808, 0x0C809,
+ 0x0C80A, 0x0C80B, 0x0C80C, 0x0C80D, 0x0C80E, 0x0C80F, 0x0C810, 0x0C811,
+ 0x0C812, 0x0C813, 0x0C814, 0x0C815, 0x0C816, 0x0C817, 0x0C818, 0x0C819,
+ 0x0C81A, 0x0C81B, 0x0C81C, 0x0C81D, 0x0C81E, 0x0C81F, 0x0C820, 0x0C821,
+ 0x0C822, 0x0C823, 0x0C824, 0x0C825, 0x0C826, 0x0C827, 0x0C828, 0x0C829,
+ 0x0C82A, 0x0C82B, 0x0C82C, 0x0C82D, 0x0C82E, 0x0C82F, 0x0C830, 0x0C831,
+ 0x0C832, 0x0C833, 0x0C834, 0x0C835, 0x0C836, 0x0C837, 0x0C838, 0x0C839,
+ 0x0C83A, 0x0C83B, 0x0C83C, 0x0C83D, 0x0C83E, 0x0C83F, 0x0C840, 0x0C841,
+ 0x0C842, 0x0C843, 0x0C844, 0x0C845, 0x0C846, 0x0C847, 0x0C848, 0x0C849,
+ 0x0C84A, 0x0C84B, 0x0C84C, 0x0C84D, 0x0C84E, 0x0C84F, 0x0C850, 0x0C851,
+ 0x0C852, 0x0C853, 0x0C854, 0x0C855, 0x0C856, 0x0C857, 0x0C858, 0x0C859,
+ 0x0C85A, 0x0C85B, 0x0C85C, 0x0C85D, 0x0C85E, 0x0C85F, 0x0C860, 0x0C861,
+ 0x0C862, 0x0C863, 0x0C864, 0x0C865, 0x0C866, 0x0C867, 0x0C868, 0x0C869,
+ 0x0C86A, 0x0C86B, 0x0C86C, 0x0C86D, 0x0C86E, 0x0C86F, 0x0C870, 0x0C871,
+ 0x0C872, 0x0C873, 0x0C874, 0x0C875, 0x0C876, 0x0C877, 0x0C878, 0x0C879,
+ 0x0C87A, 0x0C87B, 0x0C87C, 0x0C87D, 0x0C87E, 0x0C87F, 0x0C880, 0x0C881,
+ 0x0C882, 0x0C883, 0x0C884, 0x0C885, 0x0C886, 0x0C887, 0x0C888, 0x0C889,
+ 0x0C88A, 0x0C88B, 0x0C88C, 0x0C88D, 0x0C88E, 0x0C88F, 0x0C890, 0x0C891,
+ 0x0C892, 0x0C893, 0x0C894, 0x0C895, 0x0C896, 0x0C897, 0x0C898, 0x0C899,
+ 0x0C89A, 0x0C89B, 0x0C89C, 0x0C89D, 0x0C89E, 0x0C89F, 0x0C8A0, 0x0C8A1,
+ 0x0C8A2, 0x0C8A3, 0x0C8A4, 0x0C8A5, 0x0C8A6, 0x0C8A7, 0x0C8A8, 0x0C8A9,
+ 0x0C8AA, 0x0C8AB, 0x0C8AC, 0x0C8AD, 0x0C8AE, 0x0C8AF, 0x0C8B0, 0x0C8B1,
+ 0x0C8B2, 0x0C8B3, 0x0C8B4, 0x0C8B5, 0x0C8B6, 0x0C8B7, 0x0C8B8, 0x0C8B9,
+ 0x0C8BA, 0x0C8BB, 0x0C8BC, 0x0C8BD, 0x0C8BE, 0x0C8BF, 0x0C8C0, 0x0C8C1,
+ 0x0C8C2, 0x0C8C3, 0x0C8C4, 0x0C8C5, 0x0C8C6, 0x0C8C7, 0x0C8C8, 0x0C8C9,
+ 0x0C8CA, 0x0C8CB, 0x0C8CC, 0x0C8CD, 0x0C8CE, 0x0C8CF, 0x0C8D0, 0x0C8D1,
+ 0x0C8D2, 0x0C8D3, 0x0C8D4, 0x0C8D5, 0x0C8D6, 0x0C8D7, 0x0C8D8, 0x0C8D9,
+ 0x0C8DA, 0x0C8DB, 0x0C8DC, 0x0C8DD, 0x0C8DE, 0x0C8DF, 0x0C8E0, 0x0C8E1,
+ 0x0C8E2, 0x0C8E3, 0x0C8E4, 0x0C8E5, 0x0C8E6, 0x0C8E7, 0x0C8E8, 0x0C8E9,
+ 0x0C8EA, 0x0C8EB, 0x0C8EC, 0x0C8ED, 0x0C8EE, 0x0C8EF, 0x0C8F0, 0x0C8F1,
+ 0x0C8F2, 0x0C8F3, 0x0C8F4, 0x0C8F5, 0x0C8F6, 0x0C8F7, 0x0C8F8, 0x0C8F9,
+ 0x0C8FA, 0x0C8FB, 0x0C8FC, 0x0C8FD, 0x0C8FE, 0x0C8FF, 0x0C900, 0x0C901,
+ 0x0C902, 0x0C903, 0x0C904, 0x0C905, 0x0C906, 0x0C907, 0x0C908, 0x0C909,
+ 0x0C90A, 0x0C90B, 0x0C90C, 0x0C90D, 0x0C90E, 0x0C90F, 0x0C910, 0x0C911,
+ 0x0C912, 0x0C913, 0x0C914, 0x0C915, 0x0C916, 0x0C917, 0x0C918, 0x0C919,
+ 0x0C91A, 0x0C91B, 0x0C91C, 0x0C91D, 0x0C91E, 0x0C91F, 0x0C920, 0x0C921,
+ 0x0C922, 0x0C923, 0x0C924, 0x0C925, 0x0C926, 0x0C927, 0x0C928, 0x0C929,
+ 0x0C92A, 0x0C92B, 0x0C92C, 0x0C92D, 0x0C92E, 0x0C92F, 0x0C930, 0x0C931,
+ 0x0C932, 0x0C933, 0x0C934, 0x0C935, 0x0C936, 0x0C937, 0x0C938, 0x0C939,
+ 0x0C93A, 0x0C93B, 0x0C93C, 0x0C93D, 0x0C93E, 0x0C93F, 0x0C940, 0x0C941,
+ 0x0C942, 0x0C943, 0x0C944, 0x0C945, 0x0C946, 0x0C947, 0x0C948, 0x0C949,
+ 0x0C94A, 0x0C94B, 0x0C94C, 0x0C94D, 0x0C94E, 0x0C94F, 0x0C950, 0x0C951,
+ 0x0C952, 0x0C953, 0x0C954, 0x0C955, 0x0C956, 0x0C957, 0x0C958, 0x0C959,
+ 0x0C95A, 0x0C95B, 0x0C95C, 0x0C95D, 0x0C95E, 0x0C95F, 0x0C960, 0x0C961,
+ 0x0C962, 0x0C963, 0x0C964, 0x0C965, 0x0C966, 0x0C967, 0x0C968, 0x0C969,
+ 0x0C96A, 0x0C96B, 0x0C96C, 0x0C96D, 0x0C96E, 0x0C96F, 0x0C970, 0x0C971,
+ 0x0C972, 0x0C973, 0x0C974, 0x0C975, 0x0C976, 0x0C977, 0x0C978, 0x0C979,
+ 0x0C97A, 0x0C97B, 0x0C97C, 0x0C97D, 0x0C97E, 0x0C97F, 0x0C980, 0x0C981,
+ 0x0C982, 0x0C983, 0x0C984, 0x0C985, 0x0C986, 0x0C987, 0x0C988, 0x0C989,
+ 0x0C98A, 0x0C98B, 0x0C98C, 0x0C98D, 0x0C98E, 0x0C98F, 0x0C990, 0x0C991,
+ 0x0C992, 0x0C993, 0x0C994, 0x0C995, 0x0C996, 0x0C997, 0x0C998, 0x0C999,
+ 0x0C99A, 0x0C99B, 0x0C99C, 0x0C99D, 0x0C99E, 0x0C99F, 0x0C9A0, 0x0C9A1,
+ 0x0C9A2, 0x0C9A3, 0x0C9A4, 0x0C9A5, 0x0C9A6, 0x0C9A7, 0x0C9A8, 0x0C9A9,
+ 0x0C9AA, 0x0C9AB, 0x0C9AC, 0x0C9AD, 0x0C9AE, 0x0C9AF, 0x0C9B0, 0x0C9B1,
+ 0x0C9B2, 0x0C9B3, 0x0C9B4, 0x0C9B5, 0x0C9B6, 0x0C9B7, 0x0C9B8, 0x0C9B9,
+ 0x0C9BA, 0x0C9BB, 0x0C9BC, 0x0C9BD, 0x0C9BE, 0x0C9BF, 0x0C9C0, 0x0C9C1,
+ 0x0C9C2, 0x0C9C3, 0x0C9C4, 0x0C9C5, 0x0C9C6, 0x0C9C7, 0x0C9C8, 0x0C9C9,
+ 0x0C9CA, 0x0C9CB, 0x0C9CC, 0x0C9CD, 0x0C9CE, 0x0C9CF, 0x0C9D0, 0x0C9D1,
+ 0x0C9D2, 0x0C9D3, 0x0C9D4, 0x0C9D5, 0x0C9D6, 0x0C9D7, 0x0C9D8, 0x0C9D9,
+ 0x0C9DA, 0x0C9DB, 0x0C9DC, 0x0C9DD, 0x0C9DE, 0x0C9DF, 0x0C9E0, 0x0C9E1,
+ 0x0C9E2, 0x0C9E3, 0x0C9E4, 0x0C9E5, 0x0C9E6, 0x0C9E7, 0x0C9E8, 0x0C9E9,
+ 0x0C9EA, 0x0C9EB, 0x0C9EC, 0x0C9ED, 0x0C9EE, 0x0C9EF, 0x0C9F0, 0x0C9F1,
+ 0x0C9F2, 0x0C9F3, 0x0C9F4, 0x0C9F5, 0x0C9F6, 0x0C9F7, 0x0C9F8, 0x0C9F9,
+ 0x0C9FA, 0x0C9FB, 0x0C9FC, 0x0C9FD, 0x0C9FE, 0x0C9FF, 0x0CA00, 0x0CA01,
+ 0x0CA02, 0x0CA03, 0x0CA04, 0x0CA05, 0x0CA06, 0x0CA07, 0x0CA08, 0x0CA09,
+ 0x0CA0A, 0x0CA0B, 0x0CA0C, 0x0CA0D, 0x0CA0E, 0x0CA0F, 0x0CA10, 0x0CA11,
+ 0x0CA12, 0x0CA13, 0x0CA14, 0x0CA15, 0x0CA16, 0x0CA17, 0x0CA18, 0x0CA19,
+ 0x0CA1A, 0x0CA1B, 0x0CA1C, 0x0CA1D, 0x0CA1E, 0x0CA1F, 0x0CA20, 0x0CA21,
+ 0x0CA22, 0x0CA23, 0x0CA24, 0x0CA25, 0x0CA26, 0x0CA27, 0x0CA28, 0x0CA29,
+ 0x0CA2A, 0x0CA2B, 0x0CA2C, 0x0CA2D, 0x0CA2E, 0x0CA2F, 0x0CA30, 0x0CA31,
+ 0x0CA32, 0x0CA33, 0x0CA34, 0x0CA35, 0x0CA36, 0x0CA37, 0x0CA38, 0x0CA39,
+ 0x0CA3A, 0x0CA3B, 0x0CA3C, 0x0CA3D, 0x0CA3E, 0x0CA3F, 0x0CA40, 0x0CA41,
+ 0x0CA42, 0x0CA43, 0x0CA44, 0x0CA45, 0x0CA46, 0x0CA47, 0x0CA48, 0x0CA49,
+ 0x0CA4A, 0x0CA4B, 0x0CA4C, 0x0CA4D, 0x0CA4E, 0x0CA4F, 0x0CA50, 0x0CA51,
+ 0x0CA52, 0x0CA53, 0x0CA54, 0x0CA55, 0x0CA56, 0x0CA57, 0x0CA58, 0x0CA59,
+ 0x0CA5A, 0x0CA5B, 0x0CA5C, 0x0CA5D, 0x0CA5E, 0x0CA5F, 0x0CA60, 0x0CA61,
+ 0x0CA62, 0x0CA63, 0x0CA64, 0x0CA65, 0x0CA66, 0x0CA67, 0x0CA68, 0x0CA69,
+ 0x0CA6A, 0x0CA6B, 0x0CA6C, 0x0CA6D, 0x0CA6E, 0x0CA6F, 0x0CA70, 0x0CA71,
+ 0x0CA72, 0x0CA73, 0x0CA74, 0x0CA75, 0x0CA76, 0x0CA77, 0x0CA78, 0x0CA79,
+ 0x0CA7A, 0x0CA7B, 0x0CA7C, 0x0CA7D, 0x0CA7E, 0x0CA7F, 0x0CA80, 0x0CA81,
+ 0x0CA82, 0x0CA83, 0x0CA84, 0x0CA85, 0x0CA86, 0x0CA87, 0x0CA88, 0x0CA89,
+ 0x0CA8A, 0x0CA8B, 0x0CA8C, 0x0CA8D, 0x0CA8E, 0x0CA8F, 0x0CA90, 0x0CA91,
+ 0x0CA92, 0x0CA93, 0x0CA94, 0x0CA95, 0x0CA96, 0x0CA97, 0x0CA98, 0x0CA99,
+ 0x0CA9A, 0x0CA9B, 0x0CA9C, 0x0CA9D, 0x0CA9E, 0x0CA9F, 0x0CAA0, 0x0CAA1,
+ 0x0CAA2, 0x0CAA3, 0x0CAA4, 0x0CAA5, 0x0CAA6, 0x0CAA7, 0x0CAA8, 0x0CAA9,
+ 0x0CAAA, 0x0CAAB, 0x0CAAC, 0x0CAAD, 0x0CAAE, 0x0CAAF, 0x0CAB0, 0x0CAB1,
+ 0x0CAB2, 0x0CAB3, 0x0CAB4, 0x0CAB5, 0x0CAB6, 0x0CAB7, 0x0CAB8, 0x0CAB9,
+ 0x0CABA, 0x0CABB, 0x0CABC, 0x0CABD, 0x0CABE, 0x0CABF, 0x0CAC0, 0x0CAC1,
+ 0x0CAC2, 0x0CAC3, 0x0CAC4, 0x0CAC5, 0x0CAC6, 0x0CAC7, 0x0CAC8, 0x0CAC9,
+ 0x0CACA, 0x0CACB, 0x0CACC, 0x0CACD, 0x0CACE, 0x0CACF, 0x0CAD0, 0x0CAD1,
+ 0x0CAD2, 0x0CAD3, 0x0CAD4, 0x0CAD5, 0x0CAD6, 0x0CAD7, 0x0CAD8, 0x0CAD9,
+ 0x0CADA, 0x0CADB, 0x0CADC, 0x0CADD, 0x0CADE, 0x0CADF, 0x0CAE0, 0x0CAE1,
+ 0x0CAE2, 0x0CAE3, 0x0CAE4, 0x0CAE5, 0x0CAE6, 0x0CAE7, 0x0CAE8, 0x0CAE9,
+ 0x0CAEA, 0x0CAEB, 0x0CAEC, 0x0CAED, 0x0CAEE, 0x0CAEF, 0x0CAF0, 0x0CAF1,
+ 0x0CAF2, 0x0CAF3, 0x0CAF4, 0x0CAF5, 0x0CAF6, 0x0CAF7, 0x0CAF8, 0x0CAF9,
+ 0x0CAFA, 0x0CAFB, 0x0CAFC, 0x0CAFD, 0x0CAFE, 0x0CAFF, 0x0CB00, 0x0CB01,
+ 0x0CB02, 0x0CB03, 0x0CB04, 0x0CB05, 0x0CB06, 0x0CB07, 0x0CB08, 0x0CB09,
+ 0x0CB0A, 0x0CB0B, 0x0CB0C, 0x0CB0D, 0x0CB0E, 0x0CB0F, 0x0CB10, 0x0CB11,
+ 0x0CB12, 0x0CB13, 0x0CB14, 0x0CB15, 0x0CB16, 0x0CB17, 0x0CB18, 0x0CB19,
+ 0x0CB1A, 0x0CB1B, 0x0CB1C, 0x0CB1D, 0x0CB1E, 0x0CB1F, 0x0CB20, 0x0CB21,
+ 0x0CB22, 0x0CB23, 0x0CB24, 0x0CB25, 0x0CB26, 0x0CB27, 0x0CB28, 0x0CB29,
+ 0x0CB2A, 0x0CB2B, 0x0CB2C, 0x0CB2D, 0x0CB2E, 0x0CB2F, 0x0CB30, 0x0CB31,
+ 0x0CB32, 0x0CB33, 0x0CB34, 0x0CB35, 0x0CB36, 0x0CB37, 0x0CB38, 0x0CB39,
+ 0x0CB3A, 0x0CB3B, 0x0CB3C, 0x0CB3D, 0x0CB3E, 0x0CB3F, 0x0CB40, 0x0CB41,
+ 0x0CB42, 0x0CB43, 0x0CB44, 0x0CB45, 0x0CB46, 0x0CB47, 0x0CB48, 0x0CB49,
+ 0x0CB4A, 0x0CB4B, 0x0CB4C, 0x0CB4D, 0x0CB4E, 0x0CB4F, 0x0CB50, 0x0CB51,
+ 0x0CB52, 0x0CB53, 0x0CB54, 0x0CB55, 0x0CB56, 0x0CB57, 0x0CB58, 0x0CB59,
+ 0x0CB5A, 0x0CB5B, 0x0CB5C, 0x0CB5D, 0x0CB5E, 0x0CB5F, 0x0CB60, 0x0CB61,
+ 0x0CB62, 0x0CB63, 0x0CB64, 0x0CB65, 0x0CB66, 0x0CB67, 0x0CB68, 0x0CB69,
+ 0x0CB6A, 0x0CB6B, 0x0CB6C, 0x0CB6D, 0x0CB6E, 0x0CB6F, 0x0CB70, 0x0CB71,
+ 0x0CB72, 0x0CB73, 0x0CB74, 0x0CB75, 0x0CB76, 0x0CB77, 0x0CB78, 0x0CB79,
+ 0x0CB7A, 0x0CB7B, 0x0CB7C, 0x0CB7D, 0x0CB7E, 0x0CB7F, 0x0CB80, 0x0CB81,
+ 0x0CB82, 0x0CB83, 0x0CB84, 0x0CB85, 0x0CB86, 0x0CB87, 0x0CB88, 0x0CB89,
+ 0x0CB8A, 0x0CB8B, 0x0CB8C, 0x0CB8D, 0x0CB8E, 0x0CB8F, 0x0CB90, 0x0CB91,
+ 0x0CB92, 0x0CB93, 0x0CB94, 0x0CB95, 0x0CB96, 0x0CB97, 0x0CB98, 0x0CB99,
+ 0x0CB9A, 0x0CB9B, 0x0CB9C, 0x0CB9D, 0x0CB9E, 0x0CB9F, 0x0CBA0, 0x0CBA1,
+ 0x0CBA2, 0x0CBA3, 0x0CBA4, 0x0CBA5, 0x0CBA6, 0x0CBA7, 0x0CBA8, 0x0CBA9,
+ 0x0CBAA, 0x0CBAB, 0x0CBAC, 0x0CBAD, 0x0CBAE, 0x0CBAF, 0x0CBB0, 0x0CBB1,
+ 0x0CBB2, 0x0CBB3, 0x0CBB4, 0x0CBB5, 0x0CBB6, 0x0CBB7, 0x0CBB8, 0x0CBB9,
+ 0x0CBBA, 0x0CBBB, 0x0CBBC, 0x0CBBD, 0x0CBBE, 0x0CBBF, 0x0CBC0, 0x0CBC1,
+ 0x0CBC2, 0x0CBC3, 0x0CBC4, 0x0CBC5, 0x0CBC6, 0x0CBC7, 0x0CBC8, 0x0CBC9,
+ 0x0CBCA, 0x0CBCB, 0x0CBCC, 0x0CBCD, 0x0CBCE, 0x0CBCF, 0x0CBD0, 0x0CBD1,
+ 0x0CBD2, 0x0CBD3, 0x0CBD4, 0x0CBD5, 0x0CBD6, 0x0CBD7, 0x0CBD8, 0x0CBD9,
+ 0x0CBDA, 0x0CBDB, 0x0CBDC, 0x0CBDD, 0x0CBDE, 0x0CBDF, 0x0CBE0, 0x0CBE1,
+ 0x0CBE2, 0x0CBE3, 0x0CBE4, 0x0CBE5, 0x0CBE6, 0x0CBE7, 0x0CBE8, 0x0CBE9,
+ 0x0CBEA, 0x0CBEB, 0x0CBEC, 0x0CBED, 0x0CBEE, 0x0CBEF, 0x0CBF0, 0x0CBF1,
+ 0x0CBF2, 0x0CBF3, 0x0CBF4, 0x0CBF5, 0x0CBF6, 0x0CBF7, 0x0CBF8, 0x0CBF9,
+ 0x0CBFA, 0x0CBFB, 0x0CBFC, 0x0CBFD, 0x0CBFE, 0x0CBFF, 0x0CC00, 0x0CC01,
+ 0x0CC02, 0x0CC03, 0x0CC04, 0x0CC05, 0x0CC06, 0x0CC07, 0x0CC08, 0x0CC09,
+ 0x0CC0A, 0x0CC0B, 0x0CC0C, 0x0CC0D, 0x0CC0E, 0x0CC0F, 0x0CC10, 0x0CC11,
+ 0x0CC12, 0x0CC13, 0x0CC14, 0x0CC15, 0x0CC16, 0x0CC17, 0x0CC18, 0x0CC19,
+ 0x0CC1A, 0x0CC1B, 0x0CC1C, 0x0CC1D, 0x0CC1E, 0x0CC1F, 0x0CC20, 0x0CC21,
+ 0x0CC22, 0x0CC23, 0x0CC24, 0x0CC25, 0x0CC26, 0x0CC27, 0x0CC28, 0x0CC29,
+ 0x0CC2A, 0x0CC2B, 0x0CC2C, 0x0CC2D, 0x0CC2E, 0x0CC2F, 0x0CC30, 0x0CC31,
+ 0x0CC32, 0x0CC33, 0x0CC34, 0x0CC35, 0x0CC36, 0x0CC37, 0x0CC38, 0x0CC39,
+ 0x0CC3A, 0x0CC3B, 0x0CC3C, 0x0CC3D, 0x0CC3E, 0x0CC3F, 0x0CC40, 0x0CC41,
+ 0x0CC42, 0x0CC43, 0x0CC44, 0x0CC45, 0x0CC46, 0x0CC47, 0x0CC48, 0x0CC49,
+ 0x0CC4A, 0x0CC4B, 0x0CC4C, 0x0CC4D, 0x0CC4E, 0x0CC4F, 0x0CC50, 0x0CC51,
+ 0x0CC52, 0x0CC53, 0x0CC54, 0x0CC55, 0x0CC56, 0x0CC57, 0x0CC58, 0x0CC59,
+ 0x0CC5A, 0x0CC5B, 0x0CC5C, 0x0CC5D, 0x0CC5E, 0x0CC5F, 0x0CC60, 0x0CC61,
+ 0x0CC62, 0x0CC63, 0x0CC64, 0x0CC65, 0x0CC66, 0x0CC67, 0x0CC68, 0x0CC69,
+ 0x0CC6A, 0x0CC6B, 0x0CC6C, 0x0CC6D, 0x0CC6E, 0x0CC6F, 0x0CC70, 0x0CC71,
+ 0x0CC72, 0x0CC73, 0x0CC74, 0x0CC75, 0x0CC76, 0x0CC77, 0x0CC78, 0x0CC79,
+ 0x0CC7A, 0x0CC7B, 0x0CC7C, 0x0CC7D, 0x0CC7E, 0x0CC7F, 0x0CC80, 0x0CC81,
+ 0x0CC82, 0x0CC83, 0x0CC84, 0x0CC85, 0x0CC86, 0x0CC87, 0x0CC88, 0x0CC89,
+ 0x0CC8A, 0x0CC8B, 0x0CC8C, 0x0CC8D, 0x0CC8E, 0x0CC8F, 0x0CC90, 0x0CC91,
+ 0x0CC92, 0x0CC93, 0x0CC94, 0x0CC95, 0x0CC96, 0x0CC97, 0x0CC98, 0x0CC99,
+ 0x0CC9A, 0x0CC9B, 0x0CC9C, 0x0CC9D, 0x0CC9E, 0x0CC9F, 0x0CCA0, 0x0CCA1,
+ 0x0CCA2, 0x0CCA3, 0x0CCA4, 0x0CCA5, 0x0CCA6, 0x0CCA7, 0x0CCA8, 0x0CCA9,
+ 0x0CCAA, 0x0CCAB, 0x0CCAC, 0x0CCAD, 0x0CCAE, 0x0CCAF, 0x0CCB0, 0x0CCB1,
+ 0x0CCB2, 0x0CCB3, 0x0CCB4, 0x0CCB5, 0x0CCB6, 0x0CCB7, 0x0CCB8, 0x0CCB9,
+ 0x0CCBA, 0x0CCBB, 0x0CCBC, 0x0CCBD, 0x0CCBE, 0x0CCBF, 0x0CCC0, 0x0CCC1,
+ 0x0CCC2, 0x0CCC3, 0x0CCC4, 0x0CCC5, 0x0CCC6, 0x0CCC7, 0x0CCC8, 0x0CCC9,
+ 0x0CCCA, 0x0CCCB, 0x0CCCC, 0x0CCCD, 0x0CCCE, 0x0CCCF, 0x0CCD0, 0x0CCD1,
+ 0x0CCD2, 0x0CCD3, 0x0CCD4, 0x0CCD5, 0x0CCD6, 0x0CCD7, 0x0CCD8, 0x0CCD9,
+ 0x0CCDA, 0x0CCDB, 0x0CCDC, 0x0CCDD, 0x0CCDE, 0x0CCDF, 0x0CCE0, 0x0CCE1,
+ 0x0CCE2, 0x0CCE3, 0x0CCE4, 0x0CCE5, 0x0CCE6, 0x0CCE7, 0x0CCE8, 0x0CCE9,
+ 0x0CCEA, 0x0CCEB, 0x0CCEC, 0x0CCED, 0x0CCEE, 0x0CCEF, 0x0CCF0, 0x0CCF1,
+ 0x0CCF2, 0x0CCF3, 0x0CCF4, 0x0CCF5, 0x0CCF6, 0x0CCF7, 0x0CCF8, 0x0CCF9,
+ 0x0CCFA, 0x0CCFB, 0x0CCFC, 0x0CCFD, 0x0CCFE, 0x0CCFF, 0x0CD00, 0x0CD01,
+ 0x0CD02, 0x0CD03, 0x0CD04, 0x0CD05, 0x0CD06, 0x0CD07, 0x0CD08, 0x0CD09,
+ 0x0CD0A, 0x0CD0B, 0x0CD0C, 0x0CD0D, 0x0CD0E, 0x0CD0F, 0x0CD10, 0x0CD11,
+ 0x0CD12, 0x0CD13, 0x0CD14, 0x0CD15, 0x0CD16, 0x0CD17, 0x0CD18, 0x0CD19,
+ 0x0CD1A, 0x0CD1B, 0x0CD1C, 0x0CD1D, 0x0CD1E, 0x0CD1F, 0x0CD20, 0x0CD21,
+ 0x0CD22, 0x0CD23, 0x0CD24, 0x0CD25, 0x0CD26, 0x0CD27, 0x0CD28, 0x0CD29,
+ 0x0CD2A, 0x0CD2B, 0x0CD2C, 0x0CD2D, 0x0CD2E, 0x0CD2F, 0x0CD30, 0x0CD31,
+ 0x0CD32, 0x0CD33, 0x0CD34, 0x0CD35, 0x0CD36, 0x0CD37, 0x0CD38, 0x0CD39,
+ 0x0CD3A, 0x0CD3B, 0x0CD3C, 0x0CD3D, 0x0CD3E, 0x0CD3F, 0x0CD40, 0x0CD41,
+ 0x0CD42, 0x0CD43, 0x0CD44, 0x0CD45, 0x0CD46, 0x0CD47, 0x0CD48, 0x0CD49,
+ 0x0CD4A, 0x0CD4B, 0x0CD4C, 0x0CD4D, 0x0CD4E, 0x0CD4F, 0x0CD50, 0x0CD51,
+ 0x0CD52, 0x0CD53, 0x0CD54, 0x0CD55, 0x0CD56, 0x0CD57, 0x0CD58, 0x0CD59,
+ 0x0CD5A, 0x0CD5B, 0x0CD5C, 0x0CD5D, 0x0CD5E, 0x0CD5F, 0x0CD60, 0x0CD61,
+ 0x0CD62, 0x0CD63, 0x0CD64, 0x0CD65, 0x0CD66, 0x0CD67, 0x0CD68, 0x0CD69,
+ 0x0CD6A, 0x0CD6B, 0x0CD6C, 0x0CD6D, 0x0CD6E, 0x0CD6F, 0x0CD70, 0x0CD71,
+ 0x0CD72, 0x0CD73, 0x0CD74, 0x0CD75, 0x0CD76, 0x0CD77, 0x0CD78, 0x0CD79,
+ 0x0CD7A, 0x0CD7B, 0x0CD7C, 0x0CD7D, 0x0CD7E, 0x0CD7F, 0x0CD80, 0x0CD81,
+ 0x0CD82, 0x0CD83, 0x0CD84, 0x0CD85, 0x0CD86, 0x0CD87, 0x0CD88, 0x0CD89,
+ 0x0CD8A, 0x0CD8B, 0x0CD8C, 0x0CD8D, 0x0CD8E, 0x0CD8F, 0x0CD90, 0x0CD91,
+ 0x0CD92, 0x0CD93, 0x0CD94, 0x0CD95, 0x0CD96, 0x0CD97, 0x0CD98, 0x0CD99,
+ 0x0CD9A, 0x0CD9B, 0x0CD9C, 0x0CD9D, 0x0CD9E, 0x0CD9F, 0x0CDA0, 0x0CDA1,
+ 0x0CDA2, 0x0CDA3, 0x0CDA4, 0x0CDA5, 0x0CDA6, 0x0CDA7, 0x0CDA8, 0x0CDA9,
+ 0x0CDAA, 0x0CDAB, 0x0CDAC, 0x0CDAD, 0x0CDAE, 0x0CDAF, 0x0CDB0, 0x0CDB1,
+ 0x0CDB2, 0x0CDB3, 0x0CDB4, 0x0CDB5, 0x0CDB6, 0x0CDB7, 0x0CDB8, 0x0CDB9,
+ 0x0CDBA, 0x0CDBB, 0x0CDBC, 0x0CDBD, 0x0CDBE, 0x0CDBF, 0x0CDC0, 0x0CDC1,
+ 0x0CDC2, 0x0CDC3, 0x0CDC4, 0x0CDC5, 0x0CDC6, 0x0CDC7, 0x0CDC8, 0x0CDC9,
+ 0x0CDCA, 0x0CDCB, 0x0CDCC, 0x0CDCD, 0x0CDCE, 0x0CDCF, 0x0CDD0, 0x0CDD1,
+ 0x0CDD2, 0x0CDD3, 0x0CDD4, 0x0CDD5, 0x0CDD6, 0x0CDD7, 0x0CDD8, 0x0CDD9,
+ 0x0CDDA, 0x0CDDB, 0x0CDDC, 0x0CDDD, 0x0CDDE, 0x0CDDF, 0x0CDE0, 0x0CDE1,
+ 0x0CDE2, 0x0CDE3, 0x0CDE4, 0x0CDE5, 0x0CDE6, 0x0CDE7, 0x0CDE8, 0x0CDE9,
+ 0x0CDEA, 0x0CDEB, 0x0CDEC, 0x0CDED, 0x0CDEE, 0x0CDEF, 0x0CDF0, 0x0CDF1,
+ 0x0CDF2, 0x0CDF3, 0x0CDF4, 0x0CDF5, 0x0CDF6, 0x0CDF7, 0x0CDF8, 0x0CDF9,
+ 0x0CDFA, 0x0CDFB, 0x0CDFC, 0x0CDFD, 0x0CDFE, 0x0CDFF, 0x0CE00, 0x0CE01,
+ 0x0CE02, 0x0CE03, 0x0CE04, 0x0CE05, 0x0CE06, 0x0CE07, 0x0CE08, 0x0CE09,
+ 0x0CE0A, 0x0CE0B, 0x0CE0C, 0x0CE0D, 0x0CE0E, 0x0CE0F, 0x0CE10, 0x0CE11,
+ 0x0CE12, 0x0CE13, 0x0CE14, 0x0CE15, 0x0CE16, 0x0CE17, 0x0CE18, 0x0CE19,
+ 0x0CE1A, 0x0CE1B, 0x0CE1C, 0x0CE1D, 0x0CE1E, 0x0CE1F, 0x0CE20, 0x0CE21,
+ 0x0CE22, 0x0CE23, 0x0CE24, 0x0CE25, 0x0CE26, 0x0CE27, 0x0CE28, 0x0CE29,
+ 0x0CE2A, 0x0CE2B, 0x0CE2C, 0x0CE2D, 0x0CE2E, 0x0CE2F, 0x0CE30, 0x0CE31,
+ 0x0CE32, 0x0CE33, 0x0CE34, 0x0CE35, 0x0CE36, 0x0CE37, 0x0CE38, 0x0CE39,
+ 0x0CE3A, 0x0CE3B, 0x0CE3C, 0x0CE3D, 0x0CE3E, 0x0CE3F, 0x0CE40, 0x0CE41,
+ 0x0CE42, 0x0CE43, 0x0CE44, 0x0CE45, 0x0CE46, 0x0CE47, 0x0CE48, 0x0CE49,
+ 0x0CE4A, 0x0CE4B, 0x0CE4C, 0x0CE4D, 0x0CE4E, 0x0CE4F, 0x0CE50, 0x0CE51,
+ 0x0CE52, 0x0CE53, 0x0CE54, 0x0CE55, 0x0CE56, 0x0CE57, 0x0CE58, 0x0CE59,
+ 0x0CE5A, 0x0CE5B, 0x0CE5C, 0x0CE5D, 0x0CE5E, 0x0CE5F, 0x0CE60, 0x0CE61,
+ 0x0CE62, 0x0CE63, 0x0CE64, 0x0CE65, 0x0CE66, 0x0CE67, 0x0CE68, 0x0CE69,
+ 0x0CE6A, 0x0CE6B, 0x0CE6C, 0x0CE6D, 0x0CE6E, 0x0CE6F, 0x0CE70, 0x0CE71,
+ 0x0CE72, 0x0CE73, 0x0CE74, 0x0CE75, 0x0CE76, 0x0CE77, 0x0CE78, 0x0CE79,
+ 0x0CE7A, 0x0CE7B, 0x0CE7C, 0x0CE7D, 0x0CE7E, 0x0CE7F, 0x0CE80, 0x0CE81,
+ 0x0CE82, 0x0CE83, 0x0CE84, 0x0CE85, 0x0CE86, 0x0CE87, 0x0CE88, 0x0CE89,
+ 0x0CE8A, 0x0CE8B, 0x0CE8C, 0x0CE8D, 0x0CE8E, 0x0CE8F, 0x0CE90, 0x0CE91,
+ 0x0CE92, 0x0CE93, 0x0CE94, 0x0CE95, 0x0CE96, 0x0CE97, 0x0CE98, 0x0CE99,
+ 0x0CE9A, 0x0CE9B, 0x0CE9C, 0x0CE9D, 0x0CE9E, 0x0CE9F, 0x0CEA0, 0x0CEA1,
+ 0x0CEA2, 0x0CEA3, 0x0CEA4, 0x0CEA5, 0x0CEA6, 0x0CEA7, 0x0CEA8, 0x0CEA9,
+ 0x0CEAA, 0x0CEAB, 0x0CEAC, 0x0CEAD, 0x0CEAE, 0x0CEAF, 0x0CEB0, 0x0CEB1,
+ 0x0CEB2, 0x0CEB3, 0x0CEB4, 0x0CEB5, 0x0CEB6, 0x0CEB7, 0x0CEB8, 0x0CEB9,
+ 0x0CEBA, 0x0CEBB, 0x0CEBC, 0x0CEBD, 0x0CEBE, 0x0CEBF, 0x0CEC0, 0x0CEC1,
+ 0x0CEC2, 0x0CEC3, 0x0CEC4, 0x0CEC5, 0x0CEC6, 0x0CEC7, 0x0CEC8, 0x0CEC9,
+ 0x0CECA, 0x0CECB, 0x0CECC, 0x0CECD, 0x0CECE, 0x0CECF, 0x0CED0, 0x0CED1,
+ 0x0CED2, 0x0CED3, 0x0CED4, 0x0CED5, 0x0CED6, 0x0CED7, 0x0CED8, 0x0CED9,
+ 0x0CEDA, 0x0CEDB, 0x0CEDC, 0x0CEDD, 0x0CEDE, 0x0CEDF, 0x0CEE0, 0x0CEE1,
+ 0x0CEE2, 0x0CEE3, 0x0CEE4, 0x0CEE5, 0x0CEE6, 0x0CEE7, 0x0CEE8, 0x0CEE9,
+ 0x0CEEA, 0x0CEEB, 0x0CEEC, 0x0CEED, 0x0CEEE, 0x0CEEF, 0x0CEF0, 0x0CEF1,
+ 0x0CEF2, 0x0CEF3, 0x0CEF4, 0x0CEF5, 0x0CEF6, 0x0CEF7, 0x0CEF8, 0x0CEF9,
+ 0x0CEFA, 0x0CEFB, 0x0CEFC, 0x0CEFD, 0x0CEFE, 0x0CEFF, 0x0CF00, 0x0CF01,
+ 0x0CF02, 0x0CF03, 0x0CF04, 0x0CF05, 0x0CF06, 0x0CF07, 0x0CF08, 0x0CF09,
+ 0x0CF0A, 0x0CF0B, 0x0CF0C, 0x0CF0D, 0x0CF0E, 0x0CF0F, 0x0CF10, 0x0CF11,
+ 0x0CF12, 0x0CF13, 0x0CF14, 0x0CF15, 0x0CF16, 0x0CF17, 0x0CF18, 0x0CF19,
+ 0x0CF1A, 0x0CF1B, 0x0CF1C, 0x0CF1D, 0x0CF1E, 0x0CF1F, 0x0CF20, 0x0CF21,
+ 0x0CF22, 0x0CF23, 0x0CF24, 0x0CF25, 0x0CF26, 0x0CF27, 0x0CF28, 0x0CF29,
+ 0x0CF2A, 0x0CF2B, 0x0CF2C, 0x0CF2D, 0x0CF2E, 0x0CF2F, 0x0CF30, 0x0CF31,
+ 0x0CF32, 0x0CF33, 0x0CF34, 0x0CF35, 0x0CF36, 0x0CF37, 0x0CF38, 0x0CF39,
+ 0x0CF3A, 0x0CF3B, 0x0CF3C, 0x0CF3D, 0x0CF3E, 0x0CF3F, 0x0CF40, 0x0CF41,
+ 0x0CF42, 0x0CF43, 0x0CF44, 0x0CF45, 0x0CF46, 0x0CF47, 0x0CF48, 0x0CF49,
+ 0x0CF4A, 0x0CF4B, 0x0CF4C, 0x0CF4D, 0x0CF4E, 0x0CF4F, 0x0CF50, 0x0CF51,
+ 0x0CF52, 0x0CF53, 0x0CF54, 0x0CF55, 0x0CF56, 0x0CF57, 0x0CF58, 0x0CF59,
+ 0x0CF5A, 0x0CF5B, 0x0CF5C, 0x0CF5D, 0x0CF5E, 0x0CF5F, 0x0CF60, 0x0CF61,
+ 0x0CF62, 0x0CF63, 0x0CF64, 0x0CF65, 0x0CF66, 0x0CF67, 0x0CF68, 0x0CF69,
+ 0x0CF6A, 0x0CF6B, 0x0CF6C, 0x0CF6D, 0x0CF6E, 0x0CF6F, 0x0CF70, 0x0CF71,
+ 0x0CF72, 0x0CF73, 0x0CF74, 0x0CF75, 0x0CF76, 0x0CF77, 0x0CF78, 0x0CF79,
+ 0x0CF7A, 0x0CF7B, 0x0CF7C, 0x0CF7D, 0x0CF7E, 0x0CF7F, 0x0CF80, 0x0CF81,
+ 0x0CF82, 0x0CF83, 0x0CF84, 0x0CF85, 0x0CF86, 0x0CF87, 0x0CF88, 0x0CF89,
+ 0x0CF8A, 0x0CF8B, 0x0CF8C, 0x0CF8D, 0x0CF8E, 0x0CF8F, 0x0CF90, 0x0CF91,
+ 0x0CF92, 0x0CF93, 0x0CF94, 0x0CF95, 0x0CF96, 0x0CF97, 0x0CF98, 0x0CF99,
+ 0x0CF9A, 0x0CF9B, 0x0CF9C, 0x0CF9D, 0x0CF9E, 0x0CF9F, 0x0CFA0, 0x0CFA1,
+ 0x0CFA2, 0x0CFA3, 0x0CFA4, 0x0CFA5, 0x0CFA6, 0x0CFA7, 0x0CFA8, 0x0CFA9,
+ 0x0CFAA, 0x0CFAB, 0x0CFAC, 0x0CFAD, 0x0CFAE, 0x0CFAF, 0x0CFB0, 0x0CFB1,
+ 0x0CFB2, 0x0CFB3, 0x0CFB4, 0x0CFB5, 0x0CFB6, 0x0CFB7, 0x0CFB8, 0x0CFB9,
+ 0x0CFBA, 0x0CFBB, 0x0CFBC, 0x0CFBD, 0x0CFBE, 0x0CFBF, 0x0CFC0, 0x0CFC1,
+ 0x0CFC2, 0x0CFC3, 0x0CFC4, 0x0CFC5, 0x0CFC6, 0x0CFC7, 0x0CFC8, 0x0CFC9,
+ 0x0CFCA, 0x0CFCB, 0x0CFCC, 0x0CFCD, 0x0CFCE, 0x0CFCF, 0x0CFD0, 0x0CFD1,
+ 0x0CFD2, 0x0CFD3, 0x0CFD4, 0x0CFD5, 0x0CFD6, 0x0CFD7, 0x0CFD8, 0x0CFD9,
+ 0x0CFDA, 0x0CFDB, 0x0CFDC, 0x0CFDD, 0x0CFDE, 0x0CFDF, 0x0CFE0, 0x0CFE1,
+ 0x0CFE2, 0x0CFE3, 0x0CFE4, 0x0CFE5, 0x0CFE6, 0x0CFE7, 0x0CFE8, 0x0CFE9,
+ 0x0CFEA, 0x0CFEB, 0x0CFEC, 0x0CFED, 0x0CFEE, 0x0CFEF, 0x0CFF0, 0x0CFF1,
+ 0x0CFF2, 0x0CFF3, 0x0CFF4, 0x0CFF5, 0x0CFF6, 0x0CFF7, 0x0CFF8, 0x0CFF9,
+ 0x0CFFA, 0x0CFFB, 0x0CFFC, 0x0CFFD, 0x0CFFE, 0x0CFFF, 0x0D000, 0x0D001,
+ 0x0D002, 0x0D003, 0x0D004, 0x0D005, 0x0D006, 0x0D007, 0x0D008, 0x0D009,
+ 0x0D00A, 0x0D00B, 0x0D00C, 0x0D00D, 0x0D00E, 0x0D00F, 0x0D010, 0x0D011,
+ 0x0D012, 0x0D013, 0x0D014, 0x0D015, 0x0D016, 0x0D017, 0x0D018, 0x0D019,
+ 0x0D01A, 0x0D01B, 0x0D01C, 0x0D01D, 0x0D01E, 0x0D01F, 0x0D020, 0x0D021,
+ 0x0D022, 0x0D023, 0x0D024, 0x0D025, 0x0D026, 0x0D027, 0x0D028, 0x0D029,
+ 0x0D02A, 0x0D02B, 0x0D02C, 0x0D02D, 0x0D02E, 0x0D02F, 0x0D030, 0x0D031,
+ 0x0D032, 0x0D033, 0x0D034, 0x0D035, 0x0D036, 0x0D037, 0x0D038, 0x0D039,
+ 0x0D03A, 0x0D03B, 0x0D03C, 0x0D03D, 0x0D03E, 0x0D03F, 0x0D040, 0x0D041,
+ 0x0D042, 0x0D043, 0x0D044, 0x0D045, 0x0D046, 0x0D047, 0x0D048, 0x0D049,
+ 0x0D04A, 0x0D04B, 0x0D04C, 0x0D04D, 0x0D04E, 0x0D04F, 0x0D050, 0x0D051,
+ 0x0D052, 0x0D053, 0x0D054, 0x0D055, 0x0D056, 0x0D057, 0x0D058, 0x0D059,
+ 0x0D05A, 0x0D05B, 0x0D05C, 0x0D05D, 0x0D05E, 0x0D05F, 0x0D060, 0x0D061,
+ 0x0D062, 0x0D063, 0x0D064, 0x0D065, 0x0D066, 0x0D067, 0x0D068, 0x0D069,
+ 0x0D06A, 0x0D06B, 0x0D06C, 0x0D06D, 0x0D06E, 0x0D06F, 0x0D070, 0x0D071,
+ 0x0D072, 0x0D073, 0x0D074, 0x0D075, 0x0D076, 0x0D077, 0x0D078, 0x0D079,
+ 0x0D07A, 0x0D07B, 0x0D07C, 0x0D07D, 0x0D07E, 0x0D07F, 0x0D080, 0x0D081,
+ 0x0D082, 0x0D083, 0x0D084, 0x0D085, 0x0D086, 0x0D087, 0x0D088, 0x0D089,
+ 0x0D08A, 0x0D08B, 0x0D08C, 0x0D08D, 0x0D08E, 0x0D08F, 0x0D090, 0x0D091,
+ 0x0D092, 0x0D093, 0x0D094, 0x0D095, 0x0D096, 0x0D097, 0x0D098, 0x0D099,
+ 0x0D09A, 0x0D09B, 0x0D09C, 0x0D09D, 0x0D09E, 0x0D09F, 0x0D0A0, 0x0D0A1,
+ 0x0D0A2, 0x0D0A3, 0x0D0A4, 0x0D0A5, 0x0D0A6, 0x0D0A7, 0x0D0A8, 0x0D0A9,
+ 0x0D0AA, 0x0D0AB, 0x0D0AC, 0x0D0AD, 0x0D0AE, 0x0D0AF, 0x0D0B0, 0x0D0B1,
+ 0x0D0B2, 0x0D0B3, 0x0D0B4, 0x0D0B5, 0x0D0B6, 0x0D0B7, 0x0D0B8, 0x0D0B9,
+ 0x0D0BA, 0x0D0BB, 0x0D0BC, 0x0D0BD, 0x0D0BE, 0x0D0BF, 0x0D0C0, 0x0D0C1,
+ 0x0D0C2, 0x0D0C3, 0x0D0C4, 0x0D0C5, 0x0D0C6, 0x0D0C7, 0x0D0C8, 0x0D0C9,
+ 0x0D0CA, 0x0D0CB, 0x0D0CC, 0x0D0CD, 0x0D0CE, 0x0D0CF, 0x0D0D0, 0x0D0D1,
+ 0x0D0D2, 0x0D0D3, 0x0D0D4, 0x0D0D5, 0x0D0D6, 0x0D0D7, 0x0D0D8, 0x0D0D9,
+ 0x0D0DA, 0x0D0DB, 0x0D0DC, 0x0D0DD, 0x0D0DE, 0x0D0DF, 0x0D0E0, 0x0D0E1,
+ 0x0D0E2, 0x0D0E3, 0x0D0E4, 0x0D0E5, 0x0D0E6, 0x0D0E7, 0x0D0E8, 0x0D0E9,
+ 0x0D0EA, 0x0D0EB, 0x0D0EC, 0x0D0ED, 0x0D0EE, 0x0D0EF, 0x0D0F0, 0x0D0F1,
+ 0x0D0F2, 0x0D0F3, 0x0D0F4, 0x0D0F5, 0x0D0F6, 0x0D0F7, 0x0D0F8, 0x0D0F9,
+ 0x0D0FA, 0x0D0FB, 0x0D0FC, 0x0D0FD, 0x0D0FE, 0x0D0FF, 0x0D100, 0x0D101,
+ 0x0D102, 0x0D103, 0x0D104, 0x0D105, 0x0D106, 0x0D107, 0x0D108, 0x0D109,
+ 0x0D10A, 0x0D10B, 0x0D10C, 0x0D10D, 0x0D10E, 0x0D10F, 0x0D110, 0x0D111,
+ 0x0D112, 0x0D113, 0x0D114, 0x0D115, 0x0D116, 0x0D117, 0x0D118, 0x0D119,
+ 0x0D11A, 0x0D11B, 0x0D11C, 0x0D11D, 0x0D11E, 0x0D11F, 0x0D120, 0x0D121,
+ 0x0D122, 0x0D123, 0x0D124, 0x0D125, 0x0D126, 0x0D127, 0x0D128, 0x0D129,
+ 0x0D12A, 0x0D12B, 0x0D12C, 0x0D12D, 0x0D12E, 0x0D12F, 0x0D130, 0x0D131,
+ 0x0D132, 0x0D133, 0x0D134, 0x0D135, 0x0D136, 0x0D137, 0x0D138, 0x0D139,
+ 0x0D13A, 0x0D13B, 0x0D13C, 0x0D13D, 0x0D13E, 0x0D13F, 0x0D140, 0x0D141,
+ 0x0D142, 0x0D143, 0x0D144, 0x0D145, 0x0D146, 0x0D147, 0x0D148, 0x0D149,
+ 0x0D14A, 0x0D14B, 0x0D14C, 0x0D14D, 0x0D14E, 0x0D14F, 0x0D150, 0x0D151,
+ 0x0D152, 0x0D153, 0x0D154, 0x0D155, 0x0D156, 0x0D157, 0x0D158, 0x0D159,
+ 0x0D15A, 0x0D15B, 0x0D15C, 0x0D15D, 0x0D15E, 0x0D15F, 0x0D160, 0x0D161,
+ 0x0D162, 0x0D163, 0x0D164, 0x0D165, 0x0D166, 0x0D167, 0x0D168, 0x0D169,
+ 0x0D16A, 0x0D16B, 0x0D16C, 0x0D16D, 0x0D16E, 0x0D16F, 0x0D170, 0x0D171,
+ 0x0D172, 0x0D173, 0x0D174, 0x0D175, 0x0D176, 0x0D177, 0x0D178, 0x0D179,
+ 0x0D17A, 0x0D17B, 0x0D17C, 0x0D17D, 0x0D17E, 0x0D17F, 0x0D180, 0x0D181,
+ 0x0D182, 0x0D183, 0x0D184, 0x0D185, 0x0D186, 0x0D187, 0x0D188, 0x0D189,
+ 0x0D18A, 0x0D18B, 0x0D18C, 0x0D18D, 0x0D18E, 0x0D18F, 0x0D190, 0x0D191,
+ 0x0D192, 0x0D193, 0x0D194, 0x0D195, 0x0D196, 0x0D197, 0x0D198, 0x0D199,
+ 0x0D19A, 0x0D19B, 0x0D19C, 0x0D19D, 0x0D19E, 0x0D19F, 0x0D1A0, 0x0D1A1,
+ 0x0D1A2, 0x0D1A3, 0x0D1A4, 0x0D1A5, 0x0D1A6, 0x0D1A7, 0x0D1A8, 0x0D1A9,
+ 0x0D1AA, 0x0D1AB, 0x0D1AC, 0x0D1AD, 0x0D1AE, 0x0D1AF, 0x0D1B0, 0x0D1B1,
+ 0x0D1B2, 0x0D1B3, 0x0D1B4, 0x0D1B5, 0x0D1B6, 0x0D1B7, 0x0D1B8, 0x0D1B9,
+ 0x0D1BA, 0x0D1BB, 0x0D1BC, 0x0D1BD, 0x0D1BE, 0x0D1BF, 0x0D1C0, 0x0D1C1,
+ 0x0D1C2, 0x0D1C3, 0x0D1C4, 0x0D1C5, 0x0D1C6, 0x0D1C7, 0x0D1C8, 0x0D1C9,
+ 0x0D1CA, 0x0D1CB, 0x0D1CC, 0x0D1CD, 0x0D1CE, 0x0D1CF, 0x0D1D0, 0x0D1D1,
+ 0x0D1D2, 0x0D1D3, 0x0D1D4, 0x0D1D5, 0x0D1D6, 0x0D1D7, 0x0D1D8, 0x0D1D9,
+ 0x0D1DA, 0x0D1DB, 0x0D1DC, 0x0D1DD, 0x0D1DE, 0x0D1DF, 0x0D1E0, 0x0D1E1,
+ 0x0D1E2, 0x0D1E3, 0x0D1E4, 0x0D1E5, 0x0D1E6, 0x0D1E7, 0x0D1E8, 0x0D1E9,
+ 0x0D1EA, 0x0D1EB, 0x0D1EC, 0x0D1ED, 0x0D1EE, 0x0D1EF, 0x0D1F0, 0x0D1F1,
+ 0x0D1F2, 0x0D1F3, 0x0D1F4, 0x0D1F5, 0x0D1F6, 0x0D1F7, 0x0D1F8, 0x0D1F9,
+ 0x0D1FA, 0x0D1FB, 0x0D1FC, 0x0D1FD, 0x0D1FE, 0x0D1FF, 0x0D200, 0x0D201,
+ 0x0D202, 0x0D203, 0x0D204, 0x0D205, 0x0D206, 0x0D207, 0x0D208, 0x0D209,
+ 0x0D20A, 0x0D20B, 0x0D20C, 0x0D20D, 0x0D20E, 0x0D20F, 0x0D210, 0x0D211,
+ 0x0D212, 0x0D213, 0x0D214, 0x0D215, 0x0D216, 0x0D217, 0x0D218, 0x0D219,
+ 0x0D21A, 0x0D21B, 0x0D21C, 0x0D21D, 0x0D21E, 0x0D21F, 0x0D220, 0x0D221,
+ 0x0D222, 0x0D223, 0x0D224, 0x0D225, 0x0D226, 0x0D227, 0x0D228, 0x0D229,
+ 0x0D22A, 0x0D22B, 0x0D22C, 0x0D22D, 0x0D22E, 0x0D22F, 0x0D230, 0x0D231,
+ 0x0D232, 0x0D233, 0x0D234, 0x0D235, 0x0D236, 0x0D237, 0x0D238, 0x0D239,
+ 0x0D23A, 0x0D23B, 0x0D23C, 0x0D23D, 0x0D23E, 0x0D23F, 0x0D240, 0x0D241,
+ 0x0D242, 0x0D243, 0x0D244, 0x0D245, 0x0D246, 0x0D247, 0x0D248, 0x0D249,
+ 0x0D24A, 0x0D24B, 0x0D24C, 0x0D24D, 0x0D24E, 0x0D24F, 0x0D250, 0x0D251,
+ 0x0D252, 0x0D253, 0x0D254, 0x0D255, 0x0D256, 0x0D257, 0x0D258, 0x0D259,
+ 0x0D25A, 0x0D25B, 0x0D25C, 0x0D25D, 0x0D25E, 0x0D25F, 0x0D260, 0x0D261,
+ 0x0D262, 0x0D263, 0x0D264, 0x0D265, 0x0D266, 0x0D267, 0x0D268, 0x0D269,
+ 0x0D26A, 0x0D26B, 0x0D26C, 0x0D26D, 0x0D26E, 0x0D26F, 0x0D270, 0x0D271,
+ 0x0D272, 0x0D273, 0x0D274, 0x0D275, 0x0D276, 0x0D277, 0x0D278, 0x0D279,
+ 0x0D27A, 0x0D27B, 0x0D27C, 0x0D27D, 0x0D27E, 0x0D27F, 0x0D280, 0x0D281,
+ 0x0D282, 0x0D283, 0x0D284, 0x0D285, 0x0D286, 0x0D287, 0x0D288, 0x0D289,
+ 0x0D28A, 0x0D28B, 0x0D28C, 0x0D28D, 0x0D28E, 0x0D28F, 0x0D290, 0x0D291,
+ 0x0D292, 0x0D293, 0x0D294, 0x0D295, 0x0D296, 0x0D297, 0x0D298, 0x0D299,
+ 0x0D29A, 0x0D29B, 0x0D29C, 0x0D29D, 0x0D29E, 0x0D29F, 0x0D2A0, 0x0D2A1,
+ 0x0D2A2, 0x0D2A3, 0x0D2A4, 0x0D2A5, 0x0D2A6, 0x0D2A7, 0x0D2A8, 0x0D2A9,
+ 0x0D2AA, 0x0D2AB, 0x0D2AC, 0x0D2AD, 0x0D2AE, 0x0D2AF, 0x0D2B0, 0x0D2B1,
+ 0x0D2B2, 0x0D2B3, 0x0D2B4, 0x0D2B5, 0x0D2B6, 0x0D2B7, 0x0D2B8, 0x0D2B9,
+ 0x0D2BA, 0x0D2BB, 0x0D2BC, 0x0D2BD, 0x0D2BE, 0x0D2BF, 0x0D2C0, 0x0D2C1,
+ 0x0D2C2, 0x0D2C3, 0x0D2C4, 0x0D2C5, 0x0D2C6, 0x0D2C7, 0x0D2C8, 0x0D2C9,
+ 0x0D2CA, 0x0D2CB, 0x0D2CC, 0x0D2CD, 0x0D2CE, 0x0D2CF, 0x0D2D0, 0x0D2D1,
+ 0x0D2D2, 0x0D2D3, 0x0D2D4, 0x0D2D5, 0x0D2D6, 0x0D2D7, 0x0D2D8, 0x0D2D9,
+ 0x0D2DA, 0x0D2DB, 0x0D2DC, 0x0D2DD, 0x0D2DE, 0x0D2DF, 0x0D2E0, 0x0D2E1,
+ 0x0D2E2, 0x0D2E3, 0x0D2E4, 0x0D2E5, 0x0D2E6, 0x0D2E7, 0x0D2E8, 0x0D2E9,
+ 0x0D2EA, 0x0D2EB, 0x0D2EC, 0x0D2ED, 0x0D2EE, 0x0D2EF, 0x0D2F0, 0x0D2F1,
+ 0x0D2F2, 0x0D2F3, 0x0D2F4, 0x0D2F5, 0x0D2F6, 0x0D2F7, 0x0D2F8, 0x0D2F9,
+ 0x0D2FA, 0x0D2FB, 0x0D2FC, 0x0D2FD, 0x0D2FE, 0x0D2FF, 0x0D300, 0x0D301,
+ 0x0D302, 0x0D303, 0x0D304, 0x0D305, 0x0D306, 0x0D307, 0x0D308, 0x0D309,
+ 0x0D30A, 0x0D30B, 0x0D30C, 0x0D30D, 0x0D30E, 0x0D30F, 0x0D310, 0x0D311,
+ 0x0D312, 0x0D313, 0x0D314, 0x0D315, 0x0D316, 0x0D317, 0x0D318, 0x0D319,
+ 0x0D31A, 0x0D31B, 0x0D31C, 0x0D31D, 0x0D31E, 0x0D31F, 0x0D320, 0x0D321,
+ 0x0D322, 0x0D323, 0x0D324, 0x0D325, 0x0D326, 0x0D327, 0x0D328, 0x0D329,
+ 0x0D32A, 0x0D32B, 0x0D32C, 0x0D32D, 0x0D32E, 0x0D32F, 0x0D330, 0x0D331,
+ 0x0D332, 0x0D333, 0x0D334, 0x0D335, 0x0D336, 0x0D337, 0x0D338, 0x0D339,
+ 0x0D33A, 0x0D33B, 0x0D33C, 0x0D33D, 0x0D33E, 0x0D33F, 0x0D340, 0x0D341,
+ 0x0D342, 0x0D343, 0x0D344, 0x0D345, 0x0D346, 0x0D347, 0x0D348, 0x0D349,
+ 0x0D34A, 0x0D34B, 0x0D34C, 0x0D34D, 0x0D34E, 0x0D34F, 0x0D350, 0x0D351,
+ 0x0D352, 0x0D353, 0x0D354, 0x0D355, 0x0D356, 0x0D357, 0x0D358, 0x0D359,
+ 0x0D35A, 0x0D35B, 0x0D35C, 0x0D35D, 0x0D35E, 0x0D35F, 0x0D360, 0x0D361,
+ 0x0D362, 0x0D363, 0x0D364, 0x0D365, 0x0D366, 0x0D367, 0x0D368, 0x0D369,
+ 0x0D36A, 0x0D36B, 0x0D36C, 0x0D36D, 0x0D36E, 0x0D36F, 0x0D370, 0x0D371,
+ 0x0D372, 0x0D373, 0x0D374, 0x0D375, 0x0D376, 0x0D377, 0x0D378, 0x0D379,
+ 0x0D37A, 0x0D37B, 0x0D37C, 0x0D37D, 0x0D37E, 0x0D37F, 0x0D380, 0x0D381,
+ 0x0D382, 0x0D383, 0x0D384, 0x0D385, 0x0D386, 0x0D387, 0x0D388, 0x0D389,
+ 0x0D38A, 0x0D38B, 0x0D38C, 0x0D38D, 0x0D38E, 0x0D38F, 0x0D390, 0x0D391,
+ 0x0D392, 0x0D393, 0x0D394, 0x0D395, 0x0D396, 0x0D397, 0x0D398, 0x0D399,
+ 0x0D39A, 0x0D39B, 0x0D39C, 0x0D39D, 0x0D39E, 0x0D39F, 0x0D3A0, 0x0D3A1,
+ 0x0D3A2, 0x0D3A3, 0x0D3A4, 0x0D3A5, 0x0D3A6, 0x0D3A7, 0x0D3A8, 0x0D3A9,
+ 0x0D3AA, 0x0D3AB, 0x0D3AC, 0x0D3AD, 0x0D3AE, 0x0D3AF, 0x0D3B0, 0x0D3B1,
+ 0x0D3B2, 0x0D3B3, 0x0D3B4, 0x0D3B5, 0x0D3B6, 0x0D3B7, 0x0D3B8, 0x0D3B9,
+ 0x0D3BA, 0x0D3BB, 0x0D3BC, 0x0D3BD, 0x0D3BE, 0x0D3BF, 0x0D3C0, 0x0D3C1,
+ 0x0D3C2, 0x0D3C3, 0x0D3C4, 0x0D3C5, 0x0D3C6, 0x0D3C7, 0x0D3C8, 0x0D3C9,
+ 0x0D3CA, 0x0D3CB, 0x0D3CC, 0x0D3CD, 0x0D3CE, 0x0D3CF, 0x0D3D0, 0x0D3D1,
+ 0x0D3D2, 0x0D3D3, 0x0D3D4, 0x0D3D5, 0x0D3D6, 0x0D3D7, 0x0D3D8, 0x0D3D9,
+ 0x0D3DA, 0x0D3DB, 0x0D3DC, 0x0D3DD, 0x0D3DE, 0x0D3DF, 0x0D3E0, 0x0D3E1,
+ 0x0D3E2, 0x0D3E3, 0x0D3E4, 0x0D3E5, 0x0D3E6, 0x0D3E7, 0x0D3E8, 0x0D3E9,
+ 0x0D3EA, 0x0D3EB, 0x0D3EC, 0x0D3ED, 0x0D3EE, 0x0D3EF, 0x0D3F0, 0x0D3F1,
+ 0x0D3F2, 0x0D3F3, 0x0D3F4, 0x0D3F5, 0x0D3F6, 0x0D3F7, 0x0D3F8, 0x0D3F9,
+ 0x0D3FA, 0x0D3FB, 0x0D3FC, 0x0D3FD, 0x0D3FE, 0x0D3FF, 0x0D400, 0x0D401,
+ 0x0D402, 0x0D403, 0x0D404, 0x0D405, 0x0D406, 0x0D407, 0x0D408, 0x0D409,
+ 0x0D40A, 0x0D40B, 0x0D40C, 0x0D40D, 0x0D40E, 0x0D40F, 0x0D410, 0x0D411,
+ 0x0D412, 0x0D413, 0x0D414, 0x0D415, 0x0D416, 0x0D417, 0x0D418, 0x0D419,
+ 0x0D41A, 0x0D41B, 0x0D41C, 0x0D41D, 0x0D41E, 0x0D41F, 0x0D420, 0x0D421,
+ 0x0D422, 0x0D423, 0x0D424, 0x0D425, 0x0D426, 0x0D427, 0x0D428, 0x0D429,
+ 0x0D42A, 0x0D42B, 0x0D42C, 0x0D42D, 0x0D42E, 0x0D42F, 0x0D430, 0x0D431,
+ 0x0D432, 0x0D433, 0x0D434, 0x0D435, 0x0D436, 0x0D437, 0x0D438, 0x0D439,
+ 0x0D43A, 0x0D43B, 0x0D43C, 0x0D43D, 0x0D43E, 0x0D43F, 0x0D440, 0x0D441,
+ 0x0D442, 0x0D443, 0x0D444, 0x0D445, 0x0D446, 0x0D447, 0x0D448, 0x0D449,
+ 0x0D44A, 0x0D44B, 0x0D44C, 0x0D44D, 0x0D44E, 0x0D44F, 0x0D450, 0x0D451,
+ 0x0D452, 0x0D453, 0x0D454, 0x0D455, 0x0D456, 0x0D457, 0x0D458, 0x0D459,
+ 0x0D45A, 0x0D45B, 0x0D45C, 0x0D45D, 0x0D45E, 0x0D45F, 0x0D460, 0x0D461,
+ 0x0D462, 0x0D463, 0x0D464, 0x0D465, 0x0D466, 0x0D467, 0x0D468, 0x0D469,
+ 0x0D46A, 0x0D46B, 0x0D46C, 0x0D46D, 0x0D46E, 0x0D46F, 0x0D470, 0x0D471,
+ 0x0D472, 0x0D473, 0x0D474, 0x0D475, 0x0D476, 0x0D477, 0x0D478, 0x0D479,
+ 0x0D47A, 0x0D47B, 0x0D47C, 0x0D47D, 0x0D47E, 0x0D47F, 0x0D480, 0x0D481,
+ 0x0D482, 0x0D483, 0x0D484, 0x0D485, 0x0D486, 0x0D487, 0x0D488, 0x0D489,
+ 0x0D48A, 0x0D48B, 0x0D48C, 0x0D48D, 0x0D48E, 0x0D48F, 0x0D490, 0x0D491,
+ 0x0D492, 0x0D493, 0x0D494, 0x0D495, 0x0D496, 0x0D497, 0x0D498, 0x0D499,
+ 0x0D49A, 0x0D49B, 0x0D49C, 0x0D49D, 0x0D49E, 0x0D49F, 0x0D4A0, 0x0D4A1,
+ 0x0D4A2, 0x0D4A3, 0x0D4A4, 0x0D4A5, 0x0D4A6, 0x0D4A7, 0x0D4A8, 0x0D4A9,
+ 0x0D4AA, 0x0D4AB, 0x0D4AC, 0x0D4AD, 0x0D4AE, 0x0D4AF, 0x0D4B0, 0x0D4B1,
+ 0x0D4B2, 0x0D4B3, 0x0D4B4, 0x0D4B5, 0x0D4B6, 0x0D4B7, 0x0D4B8, 0x0D4B9,
+ 0x0D4BA, 0x0D4BB, 0x0D4BC, 0x0D4BD, 0x0D4BE, 0x0D4BF, 0x0D4C0, 0x0D4C1,
+ 0x0D4C2, 0x0D4C3, 0x0D4C4, 0x0D4C5, 0x0D4C6, 0x0D4C7, 0x0D4C8, 0x0D4C9,
+ 0x0D4CA, 0x0D4CB, 0x0D4CC, 0x0D4CD, 0x0D4CE, 0x0D4CF, 0x0D4D0, 0x0D4D1,
+ 0x0D4D2, 0x0D4D3, 0x0D4D4, 0x0D4D5, 0x0D4D6, 0x0D4D7, 0x0D4D8, 0x0D4D9,
+ 0x0D4DA, 0x0D4DB, 0x0D4DC, 0x0D4DD, 0x0D4DE, 0x0D4DF, 0x0D4E0, 0x0D4E1,
+ 0x0D4E2, 0x0D4E3, 0x0D4E4, 0x0D4E5, 0x0D4E6, 0x0D4E7, 0x0D4E8, 0x0D4E9,
+ 0x0D4EA, 0x0D4EB, 0x0D4EC, 0x0D4ED, 0x0D4EE, 0x0D4EF, 0x0D4F0, 0x0D4F1,
+ 0x0D4F2, 0x0D4F3, 0x0D4F4, 0x0D4F5, 0x0D4F6, 0x0D4F7, 0x0D4F8, 0x0D4F9,
+ 0x0D4FA, 0x0D4FB, 0x0D4FC, 0x0D4FD, 0x0D4FE, 0x0D4FF, 0x0D500, 0x0D501,
+ 0x0D502, 0x0D503, 0x0D504, 0x0D505, 0x0D506, 0x0D507, 0x0D508, 0x0D509,
+ 0x0D50A, 0x0D50B, 0x0D50C, 0x0D50D, 0x0D50E, 0x0D50F, 0x0D510, 0x0D511,
+ 0x0D512, 0x0D513, 0x0D514, 0x0D515, 0x0D516, 0x0D517, 0x0D518, 0x0D519,
+ 0x0D51A, 0x0D51B, 0x0D51C, 0x0D51D, 0x0D51E, 0x0D51F, 0x0D520, 0x0D521,
+ 0x0D522, 0x0D523, 0x0D524, 0x0D525, 0x0D526, 0x0D527, 0x0D528, 0x0D529,
+ 0x0D52A, 0x0D52B, 0x0D52C, 0x0D52D, 0x0D52E, 0x0D52F, 0x0D530, 0x0D531,
+ 0x0D532, 0x0D533, 0x0D534, 0x0D535, 0x0D536, 0x0D537, 0x0D538, 0x0D539,
+ 0x0D53A, 0x0D53B, 0x0D53C, 0x0D53D, 0x0D53E, 0x0D53F, 0x0D540, 0x0D541,
+ 0x0D542, 0x0D543, 0x0D544, 0x0D545, 0x0D546, 0x0D547, 0x0D548, 0x0D549,
+ 0x0D54A, 0x0D54B, 0x0D54C, 0x0D54D, 0x0D54E, 0x0D54F, 0x0D550, 0x0D551,
+ 0x0D552, 0x0D553, 0x0D554, 0x0D555, 0x0D556, 0x0D557, 0x0D558, 0x0D559,
+ 0x0D55A, 0x0D55B, 0x0D55C, 0x0D55D, 0x0D55E, 0x0D55F, 0x0D560, 0x0D561,
+ 0x0D562, 0x0D563, 0x0D564, 0x0D565, 0x0D566, 0x0D567, 0x0D568, 0x0D569,
+ 0x0D56A, 0x0D56B, 0x0D56C, 0x0D56D, 0x0D56E, 0x0D56F, 0x0D570, 0x0D571,
+ 0x0D572, 0x0D573, 0x0D574, 0x0D575, 0x0D576, 0x0D577, 0x0D578, 0x0D579,
+ 0x0D57A, 0x0D57B, 0x0D57C, 0x0D57D, 0x0D57E, 0x0D57F, 0x0D580, 0x0D581,
+ 0x0D582, 0x0D583, 0x0D584, 0x0D585, 0x0D586, 0x0D587, 0x0D588, 0x0D589,
+ 0x0D58A, 0x0D58B, 0x0D58C, 0x0D58D, 0x0D58E, 0x0D58F, 0x0D590, 0x0D591,
+ 0x0D592, 0x0D593, 0x0D594, 0x0D595, 0x0D596, 0x0D597, 0x0D598, 0x0D599,
+ 0x0D59A, 0x0D59B, 0x0D59C, 0x0D59D, 0x0D59E, 0x0D59F, 0x0D5A0, 0x0D5A1,
+ 0x0D5A2, 0x0D5A3, 0x0D5A4, 0x0D5A5, 0x0D5A6, 0x0D5A7, 0x0D5A8, 0x0D5A9,
+ 0x0D5AA, 0x0D5AB, 0x0D5AC, 0x0D5AD, 0x0D5AE, 0x0D5AF, 0x0D5B0, 0x0D5B1,
+ 0x0D5B2, 0x0D5B3, 0x0D5B4, 0x0D5B5, 0x0D5B6, 0x0D5B7, 0x0D5B8, 0x0D5B9,
+ 0x0D5BA, 0x0D5BB, 0x0D5BC, 0x0D5BD, 0x0D5BE, 0x0D5BF, 0x0D5C0, 0x0D5C1,
+ 0x0D5C2, 0x0D5C3, 0x0D5C4, 0x0D5C5, 0x0D5C6, 0x0D5C7, 0x0D5C8, 0x0D5C9,
+ 0x0D5CA, 0x0D5CB, 0x0D5CC, 0x0D5CD, 0x0D5CE, 0x0D5CF, 0x0D5D0, 0x0D5D1,
+ 0x0D5D2, 0x0D5D3, 0x0D5D4, 0x0D5D5, 0x0D5D6, 0x0D5D7, 0x0D5D8, 0x0D5D9,
+ 0x0D5DA, 0x0D5DB, 0x0D5DC, 0x0D5DD, 0x0D5DE, 0x0D5DF, 0x0D5E0, 0x0D5E1,
+ 0x0D5E2, 0x0D5E3, 0x0D5E4, 0x0D5E5, 0x0D5E6, 0x0D5E7, 0x0D5E8, 0x0D5E9,
+ 0x0D5EA, 0x0D5EB, 0x0D5EC, 0x0D5ED, 0x0D5EE, 0x0D5EF, 0x0D5F0, 0x0D5F1,
+ 0x0D5F2, 0x0D5F3, 0x0D5F4, 0x0D5F5, 0x0D5F6, 0x0D5F7, 0x0D5F8, 0x0D5F9,
+ 0x0D5FA, 0x0D5FB, 0x0D5FC, 0x0D5FD, 0x0D5FE, 0x0D5FF, 0x0D600, 0x0D601,
+ 0x0D602, 0x0D603, 0x0D604, 0x0D605, 0x0D606, 0x0D607, 0x0D608, 0x0D609,
+ 0x0D60A, 0x0D60B, 0x0D60C, 0x0D60D, 0x0D60E, 0x0D60F, 0x0D610, 0x0D611,
+ 0x0D612, 0x0D613, 0x0D614, 0x0D615, 0x0D616, 0x0D617, 0x0D618, 0x0D619,
+ 0x0D61A, 0x0D61B, 0x0D61C, 0x0D61D, 0x0D61E, 0x0D61F, 0x0D620, 0x0D621,
+ 0x0D622, 0x0D623, 0x0D624, 0x0D625, 0x0D626, 0x0D627, 0x0D628, 0x0D629,
+ 0x0D62A, 0x0D62B, 0x0D62C, 0x0D62D, 0x0D62E, 0x0D62F, 0x0D630, 0x0D631,
+ 0x0D632, 0x0D633, 0x0D634, 0x0D635, 0x0D636, 0x0D637, 0x0D638, 0x0D639,
+ 0x0D63A, 0x0D63B, 0x0D63C, 0x0D63D, 0x0D63E, 0x0D63F, 0x0D640, 0x0D641,
+ 0x0D642, 0x0D643, 0x0D644, 0x0D645, 0x0D646, 0x0D647, 0x0D648, 0x0D649,
+ 0x0D64A, 0x0D64B, 0x0D64C, 0x0D64D, 0x0D64E, 0x0D64F, 0x0D650, 0x0D651,
+ 0x0D652, 0x0D653, 0x0D654, 0x0D655, 0x0D656, 0x0D657, 0x0D658, 0x0D659,
+ 0x0D65A, 0x0D65B, 0x0D65C, 0x0D65D, 0x0D65E, 0x0D65F, 0x0D660, 0x0D661,
+ 0x0D662, 0x0D663, 0x0D664, 0x0D665, 0x0D666, 0x0D667, 0x0D668, 0x0D669,
+ 0x0D66A, 0x0D66B, 0x0D66C, 0x0D66D, 0x0D66E, 0x0D66F, 0x0D670, 0x0D671,
+ 0x0D672, 0x0D673, 0x0D674, 0x0D675, 0x0D676, 0x0D677, 0x0D678, 0x0D679,
+ 0x0D67A, 0x0D67B, 0x0D67C, 0x0D67D, 0x0D67E, 0x0D67F, 0x0D680, 0x0D681,
+ 0x0D682, 0x0D683, 0x0D684, 0x0D685, 0x0D686, 0x0D687, 0x0D688, 0x0D689,
+ 0x0D68A, 0x0D68B, 0x0D68C, 0x0D68D, 0x0D68E, 0x0D68F, 0x0D690, 0x0D691,
+ 0x0D692, 0x0D693, 0x0D694, 0x0D695, 0x0D696, 0x0D697, 0x0D698, 0x0D699,
+ 0x0D69A, 0x0D69B, 0x0D69C, 0x0D69D, 0x0D69E, 0x0D69F, 0x0D6A0, 0x0D6A1,
+ 0x0D6A2, 0x0D6A3, 0x0D6A4, 0x0D6A5, 0x0D6A6, 0x0D6A7, 0x0D6A8, 0x0D6A9,
+ 0x0D6AA, 0x0D6AB, 0x0D6AC, 0x0D6AD, 0x0D6AE, 0x0D6AF, 0x0D6B0, 0x0D6B1,
+ 0x0D6B2, 0x0D6B3, 0x0D6B4, 0x0D6B5, 0x0D6B6, 0x0D6B7, 0x0D6B8, 0x0D6B9,
+ 0x0D6BA, 0x0D6BB, 0x0D6BC, 0x0D6BD, 0x0D6BE, 0x0D6BF, 0x0D6C0, 0x0D6C1,
+ 0x0D6C2, 0x0D6C3, 0x0D6C4, 0x0D6C5, 0x0D6C6, 0x0D6C7, 0x0D6C8, 0x0D6C9,
+ 0x0D6CA, 0x0D6CB, 0x0D6CC, 0x0D6CD, 0x0D6CE, 0x0D6CF, 0x0D6D0, 0x0D6D1,
+ 0x0D6D2, 0x0D6D3, 0x0D6D4, 0x0D6D5, 0x0D6D6, 0x0D6D7, 0x0D6D8, 0x0D6D9,
+ 0x0D6DA, 0x0D6DB, 0x0D6DC, 0x0D6DD, 0x0D6DE, 0x0D6DF, 0x0D6E0, 0x0D6E1,
+ 0x0D6E2, 0x0D6E3, 0x0D6E4, 0x0D6E5, 0x0D6E6, 0x0D6E7, 0x0D6E8, 0x0D6E9,
+ 0x0D6EA, 0x0D6EB, 0x0D6EC, 0x0D6ED, 0x0D6EE, 0x0D6EF, 0x0D6F0, 0x0D6F1,
+ 0x0D6F2, 0x0D6F3, 0x0D6F4, 0x0D6F5, 0x0D6F6, 0x0D6F7, 0x0D6F8, 0x0D6F9,
+ 0x0D6FA, 0x0D6FB, 0x0D6FC, 0x0D6FD, 0x0D6FE, 0x0D6FF, 0x0D700, 0x0D701,
+ 0x0D702, 0x0D703, 0x0D704, 0x0D705, 0x0D706, 0x0D707, 0x0D708, 0x0D709,
+ 0x0D70A, 0x0D70B, 0x0D70C, 0x0D70D, 0x0D70E, 0x0D70F, 0x0D710, 0x0D711,
+ 0x0D712, 0x0D713, 0x0D714, 0x0D715, 0x0D716, 0x0D717, 0x0D718, 0x0D719,
+ 0x0D71A, 0x0D71B, 0x0D71C, 0x0D71D, 0x0D71E, 0x0D71F, 0x0D720, 0x0D721,
+ 0x0D722, 0x0D723, 0x0D724, 0x0D725, 0x0D726, 0x0D727, 0x0D728, 0x0D729,
+ 0x0D72A, 0x0D72B, 0x0D72C, 0x0D72D, 0x0D72E, 0x0D72F, 0x0D730, 0x0D731,
+ 0x0D732, 0x0D733, 0x0D734, 0x0D735, 0x0D736, 0x0D737, 0x0D738, 0x0D739,
+ 0x0D73A, 0x0D73B, 0x0D73C, 0x0D73D, 0x0D73E, 0x0D73F, 0x0D740, 0x0D741,
+ 0x0D742, 0x0D743, 0x0D744, 0x0D745, 0x0D746, 0x0D747, 0x0D748, 0x0D749,
+ 0x0D74A, 0x0D74B, 0x0D74C, 0x0D74D, 0x0D74E, 0x0D74F, 0x0D750, 0x0D751,
+ 0x0D752, 0x0D753, 0x0D754, 0x0D755, 0x0D756, 0x0D757, 0x0D758, 0x0D759,
+ 0x0D75A, 0x0D75B, 0x0D75C, 0x0D75D, 0x0D75E, 0x0D75F, 0x0D760, 0x0D761,
+ 0x0D762, 0x0D763, 0x0D764, 0x0D765, 0x0D766, 0x0D767, 0x0D768, 0x0D769,
+ 0x0D76A, 0x0D76B, 0x0D76C, 0x0D76D, 0x0D76E, 0x0D76F, 0x0D770, 0x0D771,
+ 0x0D772, 0x0D773, 0x0D774, 0x0D775, 0x0D776, 0x0D777, 0x0D778, 0x0D779,
+ 0x0D77A, 0x0D77B, 0x0D77C, 0x0D77D, 0x0D77E, 0x0D77F, 0x0D780, 0x0D781,
+ 0x0D782, 0x0D783, 0x0D784, 0x0D785, 0x0D786, 0x0D787, 0x0D788, 0x0D789,
+ 0x0D78A, 0x0D78B, 0x0D78C, 0x0D78D, 0x0D78E, 0x0D78F, 0x0D790, 0x0D791,
+ 0x0D792, 0x0D793, 0x0D794, 0x0D795, 0x0D796, 0x0D797, 0x0D798, 0x0D799,
+ 0x0D79A, 0x0D79B, 0x0D79C, 0x0D79D, 0x0D79E, 0x0D79F, 0x0D7A0, 0x0D7A1,
+ 0x0D7A2, 0x0D7A3, 0x0D7B0, 0x0D7B1, 0x0D7B2, 0x0D7B3, 0x0D7B4, 0x0D7B5,
+ 0x0D7B6, 0x0D7B7, 0x0D7B8, 0x0D7B9, 0x0D7BA, 0x0D7BB, 0x0D7BC, 0x0D7BD,
+ 0x0D7BE, 0x0D7BF, 0x0D7C0, 0x0D7C1, 0x0D7C2, 0x0D7C3, 0x0D7C4, 0x0D7C5,
+ 0x0D7C6, 0x0D7CB, 0x0D7CC, 0x0D7CD, 0x0D7CE, 0x0D7CF, 0x0D7D0, 0x0D7D1,
+ 0x0D7D2, 0x0D7D3, 0x0D7D4, 0x0D7D5, 0x0D7D6, 0x0D7D7, 0x0D7D8, 0x0D7D9,
+ 0x0D7DA, 0x0D7DB, 0x0D7DC, 0x0D7DD, 0x0D7DE, 0x0D7DF, 0x0D7E0, 0x0D7E1,
+ 0x0D7E2, 0x0D7E3, 0x0D7E4, 0x0D7E5, 0x0D7E6, 0x0D7E7, 0x0D7E8, 0x0D7E9,
+ 0x0D7EA, 0x0D7EB, 0x0D7EC, 0x0D7ED, 0x0D7EE, 0x0D7EF, 0x0D7F0, 0x0D7F1,
+ 0x0D7F2, 0x0D7F3, 0x0D7F4, 0x0D7F5, 0x0D7F6, 0x0D7F7, 0x0D7F8, 0x0D7F9,
+ 0x0D7FA, 0x0D7FB, 0x0FB00, 0x0FB01, 0x0FB02, 0x0FB03, 0x0FB04, 0x0FB05,
+ 0x0FB06, 0x0FB13, 0x0FB14, 0x0FB15, 0x0FB16, 0x0FB17, 0x0FB50, 0x0FB51,
+ 0x0FB52, 0x0FB53, 0x0FB54, 0x0FB55, 0x0FB56, 0x0FB57, 0x0FB58, 0x0FB59,
+ 0x0FB5A, 0x0FB5B, 0x0FB5C, 0x0FB5D, 0x0FB5E, 0x0FB5F, 0x0FB60, 0x0FB61,
+ 0x0FB62, 0x0FB63, 0x0FB64, 0x0FB65, 0x0FB66, 0x0FB67, 0x0FB68, 0x0FB69,
+ 0x0FB6A, 0x0FB6B, 0x0FB6C, 0x0FB6D, 0x0FB6E, 0x0FB6F, 0x0FB70, 0x0FB71,
+ 0x0FB72, 0x0FB73, 0x0FB74, 0x0FB75, 0x0FB76, 0x0FB77, 0x0FB78, 0x0FB79,
+ 0x0FB7A, 0x0FB7B, 0x0FB7C, 0x0FB7D, 0x0FB7E, 0x0FB7F, 0x0FB80, 0x0FB81,
+ 0x0FB82, 0x0FB83, 0x0FB84, 0x0FB85, 0x0FB86, 0x0FB87, 0x0FB88, 0x0FB89,
+ 0x0FB8A, 0x0FB8B, 0x0FB8C, 0x0FB8D, 0x0FB8E, 0x0FB8F, 0x0FB90, 0x0FB91,
+ 0x0FB92, 0x0FB93, 0x0FB94, 0x0FB95, 0x0FB96, 0x0FB97, 0x0FB98, 0x0FB99,
+ 0x0FB9A, 0x0FB9B, 0x0FB9C, 0x0FB9D, 0x0FB9E, 0x0FB9F, 0x0FBA0, 0x0FBA1,
+ 0x0FBA2, 0x0FBA3, 0x0FBA4, 0x0FBA5, 0x0FBA6, 0x0FBA7, 0x0FBA8, 0x0FBA9,
+ 0x0FBAA, 0x0FBAB, 0x0FBAC, 0x0FBAD, 0x0FBAE, 0x0FBAF, 0x0FBB0, 0x0FBB1,
+ 0x0FBD3, 0x0FBD4, 0x0FBD5, 0x0FBD6, 0x0FBD7, 0x0FBD8, 0x0FBD9, 0x0FBDA,
+ 0x0FBDB, 0x0FBDC, 0x0FBDD, 0x0FBDE, 0x0FBDF, 0x0FBE0, 0x0FBE1, 0x0FBE2,
+ 0x0FBE3, 0x0FBE4, 0x0FBE5, 0x0FBE6, 0x0FBE7, 0x0FBE8, 0x0FBE9, 0x0FBEA,
+ 0x0FBEB, 0x0FBEC, 0x0FBED, 0x0FBEE, 0x0FBEF, 0x0FBF0, 0x0FBF1, 0x0FBF2,
+ 0x0FBF3, 0x0FBF4, 0x0FBF5, 0x0FBF6, 0x0FBF7, 0x0FBF8, 0x0FBF9, 0x0FBFA,
+ 0x0FBFB, 0x0FBFC, 0x0FBFD, 0x0FBFE, 0x0FBFF, 0x0FC00, 0x0FC01, 0x0FC02,
+ 0x0FC03, 0x0FC04, 0x0FC05, 0x0FC06, 0x0FC07, 0x0FC08, 0x0FC09, 0x0FC0A,
+ 0x0FC0B, 0x0FC0C, 0x0FC0D, 0x0FC0E, 0x0FC0F, 0x0FC10, 0x0FC11, 0x0FC12,
+ 0x0FC13, 0x0FC14, 0x0FC15, 0x0FC16, 0x0FC17, 0x0FC18, 0x0FC19, 0x0FC1A,
+ 0x0FC1B, 0x0FC1C, 0x0FC1D, 0x0FC1E, 0x0FC1F, 0x0FC20, 0x0FC21, 0x0FC22,
+ 0x0FC23, 0x0FC24, 0x0FC25, 0x0FC26, 0x0FC27, 0x0FC28, 0x0FC29, 0x0FC2A,
+ 0x0FC2B, 0x0FC2C, 0x0FC2D, 0x0FC2E, 0x0FC2F, 0x0FC30, 0x0FC31, 0x0FC32,
+ 0x0FC33, 0x0FC34, 0x0FC35, 0x0FC36, 0x0FC37, 0x0FC38, 0x0FC39, 0x0FC3A,
+ 0x0FC3B, 0x0FC3C, 0x0FC3D, 0x0FC3E, 0x0FC3F, 0x0FC40, 0x0FC41, 0x0FC42,
+ 0x0FC43, 0x0FC44, 0x0FC45, 0x0FC46, 0x0FC47, 0x0FC48, 0x0FC49, 0x0FC4A,
+ 0x0FC4B, 0x0FC4C, 0x0FC4D, 0x0FC4E, 0x0FC4F, 0x0FC50, 0x0FC51, 0x0FC52,
+ 0x0FC53, 0x0FC54, 0x0FC55, 0x0FC56, 0x0FC57, 0x0FC58, 0x0FC59, 0x0FC5A,
+ 0x0FC5B, 0x0FC5C, 0x0FC5D, 0x0FC5E, 0x0FC5F, 0x0FC60, 0x0FC61, 0x0FC62,
+ 0x0FC63, 0x0FC64, 0x0FC65, 0x0FC66, 0x0FC67, 0x0FC68, 0x0FC69, 0x0FC6A,
+ 0x0FC6B, 0x0FC6C, 0x0FC6D, 0x0FC6E, 0x0FC6F, 0x0FC70, 0x0FC71, 0x0FC72,
+ 0x0FC73, 0x0FC74, 0x0FC75, 0x0FC76, 0x0FC77, 0x0FC78, 0x0FC79, 0x0FC7A,
+ 0x0FC7B, 0x0FC7C, 0x0FC7D, 0x0FC7E, 0x0FC7F, 0x0FC80, 0x0FC81, 0x0FC82,
+ 0x0FC83, 0x0FC84, 0x0FC85, 0x0FC86, 0x0FC87, 0x0FC88, 0x0FC89, 0x0FC8A,
+ 0x0FC8B, 0x0FC8C, 0x0FC8D, 0x0FC8E, 0x0FC8F, 0x0FC90, 0x0FC91, 0x0FC92,
+ 0x0FC93, 0x0FC94, 0x0FC95, 0x0FC96, 0x0FC97, 0x0FC98, 0x0FC99, 0x0FC9A,
+ 0x0FC9B, 0x0FC9C, 0x0FC9D, 0x0FC9E, 0x0FC9F, 0x0FCA0, 0x0FCA1, 0x0FCA2,
+ 0x0FCA3, 0x0FCA4, 0x0FCA5, 0x0FCA6, 0x0FCA7, 0x0FCA8, 0x0FCA9, 0x0FCAA,
+ 0x0FCAB, 0x0FCAC, 0x0FCAD, 0x0FCAE, 0x0FCAF, 0x0FCB0, 0x0FCB1, 0x0FCB2,
+ 0x0FCB3, 0x0FCB4, 0x0FCB5, 0x0FCB6, 0x0FCB7, 0x0FCB8, 0x0FCB9, 0x0FCBA,
+ 0x0FCBB, 0x0FCBC, 0x0FCBD, 0x0FCBE, 0x0FCBF, 0x0FCC0, 0x0FCC1, 0x0FCC2,
+ 0x0FCC3, 0x0FCC4, 0x0FCC5, 0x0FCC6, 0x0FCC7, 0x0FCC8, 0x0FCC9, 0x0FCCA,
+ 0x0FCCB, 0x0FCCC, 0x0FCCD, 0x0FCCE, 0x0FCCF, 0x0FCD0, 0x0FCD1, 0x0FCD2,
+ 0x0FCD3, 0x0FCD4, 0x0FCD5, 0x0FCD6, 0x0FCD7, 0x0FCD8, 0x0FCD9, 0x0FCDA,
+ 0x0FCDB, 0x0FCDC, 0x0FCDD, 0x0FCDE, 0x0FCDF, 0x0FCE0, 0x0FCE1, 0x0FCE2,
+ 0x0FCE3, 0x0FCE4, 0x0FCE5, 0x0FCE6, 0x0FCE7, 0x0FCE8, 0x0FCE9, 0x0FCEA,
+ 0x0FCEB, 0x0FCEC, 0x0FCED, 0x0FCEE, 0x0FCEF, 0x0FCF0, 0x0FCF1, 0x0FCF2,
+ 0x0FCF3, 0x0FCF4, 0x0FCF5, 0x0FCF6, 0x0FCF7, 0x0FCF8, 0x0FCF9, 0x0FCFA,
+ 0x0FCFB, 0x0FCFC, 0x0FCFD, 0x0FCFE, 0x0FCFF, 0x0FD00, 0x0FD01, 0x0FD02,
+ 0x0FD03, 0x0FD04, 0x0FD05, 0x0FD06, 0x0FD07, 0x0FD08, 0x0FD09, 0x0FD0A,
+ 0x0FD0B, 0x0FD0C, 0x0FD0D, 0x0FD0E, 0x0FD0F, 0x0FD10, 0x0FD11, 0x0FD12,
+ 0x0FD13, 0x0FD14, 0x0FD15, 0x0FD16, 0x0FD17, 0x0FD18, 0x0FD19, 0x0FD1A,
+ 0x0FD1B, 0x0FD1C, 0x0FD1D, 0x0FD1E, 0x0FD1F, 0x0FD20, 0x0FD21, 0x0FD22,
+ 0x0FD23, 0x0FD24, 0x0FD25, 0x0FD26, 0x0FD27, 0x0FD28, 0x0FD29, 0x0FD2A,
+ 0x0FD2B, 0x0FD2C, 0x0FD2D, 0x0FD2E, 0x0FD2F, 0x0FD30, 0x0FD31, 0x0FD32,
+ 0x0FD33, 0x0FD34, 0x0FD35, 0x0FD36, 0x0FD37, 0x0FD38, 0x0FD39, 0x0FD3A,
+ 0x0FD3B, 0x0FD3C, 0x0FD3D, 0x0FD50, 0x0FD51, 0x0FD52, 0x0FD53, 0x0FD54,
+ 0x0FD55, 0x0FD56, 0x0FD57, 0x0FD58, 0x0FD59, 0x0FD5A, 0x0FD5B, 0x0FD5C,
+ 0x0FD5D, 0x0FD5E, 0x0FD5F, 0x0FD60, 0x0FD61, 0x0FD62, 0x0FD63, 0x0FD64,
+ 0x0FD65, 0x0FD66, 0x0FD67, 0x0FD68, 0x0FD69, 0x0FD6A, 0x0FD6B, 0x0FD6C,
+ 0x0FD6D, 0x0FD6E, 0x0FD6F, 0x0FD70, 0x0FD71, 0x0FD72, 0x0FD73, 0x0FD74,
+ 0x0FD75, 0x0FD76, 0x0FD77, 0x0FD78, 0x0FD79, 0x0FD7A, 0x0FD7B, 0x0FD7C,
+ 0x0FD7D, 0x0FD7E, 0x0FD7F, 0x0FD80, 0x0FD81, 0x0FD82, 0x0FD83, 0x0FD84,
+ 0x0FD85, 0x0FD86, 0x0FD87, 0x0FD88, 0x0FD89, 0x0FD8A, 0x0FD8B, 0x0FD8C,
+ 0x0FD8D, 0x0FD8E, 0x0FD8F, 0x0FD92, 0x0FD93, 0x0FD94, 0x0FD95, 0x0FD96,
+ 0x0FD97, 0x0FD98, 0x0FD99, 0x0FD9A, 0x0FD9B, 0x0FD9C, 0x0FD9D, 0x0FD9E,
+ 0x0FD9F, 0x0FDA0, 0x0FDA1, 0x0FDA2, 0x0FDA3, 0x0FDA4, 0x0FDA5, 0x0FDA6,
+ 0x0FDA7, 0x0FDA8, 0x0FDA9, 0x0FDAA, 0x0FDAB, 0x0FDAC, 0x0FDAD, 0x0FDAE,
+ 0x0FDAF, 0x0FDB0, 0x0FDB1, 0x0FDB2, 0x0FDB3, 0x0FDB4, 0x0FDB5, 0x0FDB6,
+ 0x0FDB7, 0x0FDB8, 0x0FDB9, 0x0FDBA, 0x0FDBB, 0x0FDBC, 0x0FDBD, 0x0FDBE,
+ 0x0FDBF, 0x0FDC0, 0x0FDC1, 0x0FDC2, 0x0FDC3, 0x0FDC4, 0x0FDC5, 0x0FDC6,
+ 0x0FDC7, 0x0FDF0, 0x0FDF1, 0x0FDF2, 0x0FDF3, 0x0FDF4, 0x0FDF5, 0x0FDF6,
+ 0x0FDF7, 0x0FDF8, 0x0FDF9, 0x0FDFA, 0x0FDFB, 0x0FE70, 0x0FE71, 0x0FE72,
+ 0x0FE73, 0x0FE74, 0x0FE76, 0x0FE77, 0x0FE78, 0x0FE79, 0x0FE7A, 0x0FE7B,
+ 0x0FE7C, 0x0FE7D, 0x0FE7E, 0x0FE7F, 0x0FE80, 0x0FE81, 0x0FE82, 0x0FE83,
+ 0x0FE84, 0x0FE85, 0x0FE86, 0x0FE87, 0x0FE88, 0x0FE89, 0x0FE8A, 0x0FE8B,
+ 0x0FE8C, 0x0FE8D, 0x0FE8E, 0x0FE8F, 0x0FE90, 0x0FE91, 0x0FE92, 0x0FE93,
+ 0x0FE94, 0x0FE95, 0x0FE96, 0x0FE97, 0x0FE98, 0x0FE99, 0x0FE9A, 0x0FE9B,
+ 0x0FE9C, 0x0FE9D, 0x0FE9E, 0x0FE9F, 0x0FEA0, 0x0FEA1, 0x0FEA2, 0x0FEA3,
+ 0x0FEA4, 0x0FEA5, 0x0FEA6, 0x0FEA7, 0x0FEA8, 0x0FEA9, 0x0FEAA, 0x0FEAB,
+ 0x0FEAC, 0x0FEAD, 0x0FEAE, 0x0FEAF, 0x0FEB0, 0x0FEB1, 0x0FEB2, 0x0FEB3,
+ 0x0FEB4, 0x0FEB5, 0x0FEB6, 0x0FEB7, 0x0FEB8, 0x0FEB9, 0x0FEBA, 0x0FEBB,
+ 0x0FEBC, 0x0FEBD, 0x0FEBE, 0x0FEBF, 0x0FEC0, 0x0FEC1, 0x0FEC2, 0x0FEC3,
+ 0x0FEC4, 0x0FEC5, 0x0FEC6, 0x0FEC7, 0x0FEC8, 0x0FEC9, 0x0FECA, 0x0FECB,
+ 0x0FECC, 0x0FECD, 0x0FECE, 0x0FECF, 0x0FED0, 0x0FED1, 0x0FED2, 0x0FED3,
+ 0x0FED4, 0x0FED5, 0x0FED6, 0x0FED7, 0x0FED8, 0x0FED9, 0x0FEDA, 0x0FEDB,
+ 0x0FEDC, 0x0FEDD, 0x0FEDE, 0x0FEDF, 0x0FEE0, 0x0FEE1, 0x0FEE2, 0x0FEE3,
+ 0x0FEE4, 0x0FEE5, 0x0FEE6, 0x0FEE7, 0x0FEE8, 0x0FEE9, 0x0FEEA, 0x0FEEB,
+ 0x0FEEC, 0x0FEED, 0x0FEEE, 0x0FEEF, 0x0FEF0, 0x0FEF1, 0x0FEF2, 0x0FEF3,
+ 0x0FEF4, 0x0FEF5, 0x0FEF6, 0x0FEF7, 0x0FEF8, 0x0FEF9, 0x0FEFA, 0x0FEFB,
+ 0x0FEFC, 0x0FF21, 0x0FF22, 0x0FF23, 0x0FF24, 0x0FF25, 0x0FF26, 0x0FF27,
+ 0x0FF28, 0x0FF29, 0x0FF2A, 0x0FF2B, 0x0FF2C, 0x0FF2D, 0x0FF2E, 0x0FF2F,
+ 0x0FF30, 0x0FF31, 0x0FF32, 0x0FF33, 0x0FF34, 0x0FF35, 0x0FF36, 0x0FF37,
+ 0x0FF38, 0x0FF39, 0x0FF3A, 0x0FF41, 0x0FF42, 0x0FF43, 0x0FF44, 0x0FF45,
+ 0x0FF46, 0x0FF47, 0x0FF48, 0x0FF49, 0x0FF4A, 0x0FF4B, 0x0FF4C, 0x0FF4D,
+ 0x0FF4E, 0x0FF4F, 0x0FF50, 0x0FF51, 0x0FF52, 0x0FF53, 0x0FF54, 0x0FF55,
+ 0x0FF56, 0x0FF57, 0x0FF58, 0x0FF59, 0x0FF5A, 0x0FFA0, 0x0FFA1, 0x0FFA2,
+ 0x0FFA3, 0x0FFA4, 0x0FFA5, 0x0FFA6, 0x0FFA7, 0x0FFA8, 0x0FFA9, 0x0FFAA,
+ 0x0FFAB, 0x0FFAC, 0x0FFAD, 0x0FFAE, 0x0FFAF, 0x0FFB0, 0x0FFB1, 0x0FFB2,
+ 0x0FFB3, 0x0FFB4, 0x0FFB5, 0x0FFB6, 0x0FFB7, 0x0FFB8, 0x0FFB9, 0x0FFBA,
+ 0x0FFBB, 0x0FFBC, 0x0FFBD, 0x0FFBE, 0x0FFC2, 0x0FFC3, 0x0FFC4, 0x0FFC5,
+ 0x0FFC6, 0x0FFC7, 0x0FFCA, 0x0FFCB, 0x0FFCC, 0x0FFCD, 0x0FFCE, 0x0FFCF,
+ 0x0FFD2, 0x0FFD3, 0x0FFD4, 0x0FFD5, 0x0FFD6, 0x0FFD7, 0x0FFDA, 0x0FFDB,
+ 0x0FFDC, 0x10000, 0x10001, 0x10002, 0x10003, 0x10004, 0x10005, 0x10006,
+ 0x10007, 0x10008, 0x10009, 0x1000A, 0x1000B, 0x1000D, 0x1000E, 0x1000F,
+ 0x10010, 0x10011, 0x10012, 0x10013, 0x10014, 0x10015, 0x10016, 0x10017,
+ 0x10018, 0x10019, 0x1001A, 0x1001B, 0x1001C, 0x1001D, 0x1001E, 0x1001F,
+ 0x10020, 0x10021, 0x10022, 0x10023, 0x10024, 0x10025, 0x10026, 0x10028,
+ 0x10029, 0x1002A, 0x1002B, 0x1002C, 0x1002D, 0x1002E, 0x1002F, 0x10030,
+ 0x10031, 0x10032, 0x10033, 0x10034, 0x10035, 0x10036, 0x10037, 0x10038,
+ 0x10039, 0x1003A, 0x1003C, 0x1003D, 0x1003F, 0x10040, 0x10041, 0x10042,
+ 0x10043, 0x10044, 0x10045, 0x10046, 0x10047, 0x10048, 0x10049, 0x1004A,
+ 0x1004B, 0x1004C, 0x1004D, 0x10050, 0x10051, 0x10052, 0x10053, 0x10054,
+ 0x10055, 0x10056, 0x10057, 0x10058, 0x10059, 0x1005A, 0x1005B, 0x1005C,
+ 0x1005D, 0x10080, 0x10081, 0x10082, 0x10083, 0x10084, 0x10085, 0x10086,
+ 0x10087, 0x10088, 0x10089, 0x1008A, 0x1008B, 0x1008C, 0x1008D, 0x1008E,
+ 0x1008F, 0x10090, 0x10091, 0x10092, 0x10093, 0x10094, 0x10095, 0x10096,
+ 0x10097, 0x10098, 0x10099, 0x1009A, 0x1009B, 0x1009C, 0x1009D, 0x1009E,
+ 0x1009F, 0x100A0, 0x100A1, 0x100A2, 0x100A3, 0x100A4, 0x100A5, 0x100A6,
+ 0x100A7, 0x100A8, 0x100A9, 0x100AA, 0x100AB, 0x100AC, 0x100AD, 0x100AE,
+ 0x100AF, 0x100B0, 0x100B1, 0x100B2, 0x100B3, 0x100B4, 0x100B5, 0x100B6,
+ 0x100B7, 0x100B8, 0x100B9, 0x100BA, 0x100BB, 0x100BC, 0x100BD, 0x100BE,
+ 0x100BF, 0x100C0, 0x100C1, 0x100C2, 0x100C3, 0x100C4, 0x100C5, 0x100C6,
+ 0x100C7, 0x100C8, 0x100C9, 0x100CA, 0x100CB, 0x100CC, 0x100CD, 0x100CE,
+ 0x100CF, 0x100D0, 0x100D1, 0x100D2, 0x100D3, 0x100D4, 0x100D5, 0x100D6,
+ 0x100D7, 0x100D8, 0x100D9, 0x100DA, 0x100DB, 0x100DC, 0x100DD, 0x100DE,
+ 0x100DF, 0x100E0, 0x100E1, 0x100E2, 0x100E3, 0x100E4, 0x100E5, 0x100E6,
+ 0x100E7, 0x100E8, 0x100E9, 0x100EA, 0x100EB, 0x100EC, 0x100ED, 0x100EE,
+ 0x100EF, 0x100F0, 0x100F1, 0x100F2, 0x100F3, 0x100F4, 0x100F5, 0x100F6,
+ 0x100F7, 0x100F8, 0x100F9, 0x100FA, 0x10140, 0x10141, 0x10142, 0x10143,
+ 0x10144, 0x10145, 0x10146, 0x10147, 0x10148, 0x10149, 0x1014A, 0x1014B,
+ 0x1014C, 0x1014D, 0x1014E, 0x1014F, 0x10150, 0x10151, 0x10152, 0x10153,
+ 0x10154, 0x10155, 0x10156, 0x10157, 0x10158, 0x10159, 0x1015A, 0x1015B,
+ 0x1015C, 0x1015D, 0x1015E, 0x1015F, 0x10160, 0x10161, 0x10162, 0x10163,
+ 0x10164, 0x10165, 0x10166, 0x10167, 0x10168, 0x10169, 0x1016A, 0x1016B,
+ 0x1016C, 0x1016D, 0x1016E, 0x1016F, 0x10170, 0x10171, 0x10172, 0x10173,
+ 0x10174, 0x10280, 0x10281, 0x10282, 0x10283, 0x10284, 0x10285, 0x10286,
+ 0x10287, 0x10288, 0x10289, 0x1028A, 0x1028B, 0x1028C, 0x1028D, 0x1028E,
+ 0x1028F, 0x10290, 0x10291, 0x10292, 0x10293, 0x10294, 0x10295, 0x10296,
+ 0x10297, 0x10298, 0x10299, 0x1029A, 0x1029B, 0x1029C, 0x102A0, 0x102A1,
+ 0x102A2, 0x102A3, 0x102A4, 0x102A5, 0x102A6, 0x102A7, 0x102A8, 0x102A9,
+ 0x102AA, 0x102AB, 0x102AC, 0x102AD, 0x102AE, 0x102AF, 0x102B0, 0x102B1,
+ 0x102B2, 0x102B3, 0x102B4, 0x102B5, 0x102B6, 0x102B7, 0x102B8, 0x102B9,
+ 0x102BA, 0x102BB, 0x102BC, 0x102BD, 0x102BE, 0x102BF, 0x102C0, 0x102C1,
+ 0x102C2, 0x102C3, 0x102C4, 0x102C5, 0x102C6, 0x102C7, 0x102C8, 0x102C9,
+ 0x102CA, 0x102CB, 0x102CC, 0x102CD, 0x102CE, 0x102CF, 0x102D0, 0x10300,
+ 0x10301, 0x10302, 0x10303, 0x10304, 0x10305, 0x10306, 0x10307, 0x10308,
+ 0x10309, 0x1030A, 0x1030B, 0x1030C, 0x1030D, 0x1030E, 0x1030F, 0x10310,
+ 0x10311, 0x10312, 0x10313, 0x10314, 0x10315, 0x10316, 0x10317, 0x10318,
+ 0x10319, 0x1031A, 0x1031B, 0x1031C, 0x1031D, 0x1031E, 0x1031F, 0x10330,
+ 0x10331, 0x10332, 0x10333, 0x10334, 0x10335, 0x10336, 0x10337, 0x10338,
+ 0x10339, 0x1033A, 0x1033B, 0x1033C, 0x1033D, 0x1033E, 0x1033F, 0x10340,
+ 0x10341, 0x10342, 0x10343, 0x10344, 0x10345, 0x10346, 0x10347, 0x10348,
+ 0x10349, 0x1034A, 0x10350, 0x10351, 0x10352, 0x10353, 0x10354, 0x10355,
+ 0x10356, 0x10357, 0x10358, 0x10359, 0x1035A, 0x1035B, 0x1035C, 0x1035D,
+ 0x1035E, 0x1035F, 0x10360, 0x10361, 0x10362, 0x10363, 0x10364, 0x10365,
+ 0x10366, 0x10367, 0x10368, 0x10369, 0x1036A, 0x1036B, 0x1036C, 0x1036D,
+ 0x1036E, 0x1036F, 0x10370, 0x10371, 0x10372, 0x10373, 0x10374, 0x10375,
+ 0x10380, 0x10381, 0x10382, 0x10383, 0x10384, 0x10385, 0x10386, 0x10387,
+ 0x10388, 0x10389, 0x1038A, 0x1038B, 0x1038C, 0x1038D, 0x1038E, 0x1038F,
+ 0x10390, 0x10391, 0x10392, 0x10393, 0x10394, 0x10395, 0x10396, 0x10397,
+ 0x10398, 0x10399, 0x1039A, 0x1039B, 0x1039C, 0x1039D, 0x103A0, 0x103A1,
+ 0x103A2, 0x103A3, 0x103A4, 0x103A5, 0x103A6, 0x103A7, 0x103A8, 0x103A9,
+ 0x103AA, 0x103AB, 0x103AC, 0x103AD, 0x103AE, 0x103AF, 0x103B0, 0x103B1,
+ 0x103B2, 0x103B3, 0x103B4, 0x103B5, 0x103B6, 0x103B7, 0x103B8, 0x103B9,
+ 0x103BA, 0x103BB, 0x103BC, 0x103BD, 0x103BE, 0x103BF, 0x103C0, 0x103C1,
+ 0x103C2, 0x103C3, 0x103C8, 0x103C9, 0x103CA, 0x103CB, 0x103CC, 0x103CD,
+ 0x103CE, 0x103CF, 0x103D1, 0x103D2, 0x103D3, 0x103D4, 0x103D5, 0x10400,
+ 0x10401, 0x10402, 0x10403, 0x10404, 0x10405, 0x10406, 0x10407, 0x10408,
+ 0x10409, 0x1040A, 0x1040B, 0x1040C, 0x1040D, 0x1040E, 0x1040F, 0x10410,
+ 0x10411, 0x10412, 0x10413, 0x10414, 0x10415, 0x10416, 0x10417, 0x10418,
+ 0x10419, 0x1041A, 0x1041B, 0x1041C, 0x1041D, 0x1041E, 0x1041F, 0x10420,
+ 0x10421, 0x10422, 0x10423, 0x10424, 0x10425, 0x10426, 0x10427, 0x10428,
+ 0x10429, 0x1042A, 0x1042B, 0x1042C, 0x1042D, 0x1042E, 0x1042F, 0x10430,
+ 0x10431, 0x10432, 0x10433, 0x10434, 0x10435, 0x10436, 0x10437, 0x10438,
+ 0x10439, 0x1043A, 0x1043B, 0x1043C, 0x1043D, 0x1043E, 0x1043F, 0x10440,
+ 0x10441, 0x10442, 0x10443, 0x10444, 0x10445, 0x10446, 0x10447, 0x10448,
+ 0x10449, 0x1044A, 0x1044B, 0x1044C, 0x1044D, 0x1044E, 0x1044F, 0x10450,
+ 0x10451, 0x10452, 0x10453, 0x10454, 0x10455, 0x10456, 0x10457, 0x10458,
+ 0x10459, 0x1045A, 0x1045B, 0x1045C, 0x1045D, 0x1045E, 0x1045F, 0x10460,
+ 0x10461, 0x10462, 0x10463, 0x10464, 0x10465, 0x10466, 0x10467, 0x10468,
+ 0x10469, 0x1046A, 0x1046B, 0x1046C, 0x1046D, 0x1046E, 0x1046F, 0x10470,
+ 0x10471, 0x10472, 0x10473, 0x10474, 0x10475, 0x10476, 0x10477, 0x10478,
+ 0x10479, 0x1047A, 0x1047B, 0x1047C, 0x1047D, 0x1047E, 0x1047F, 0x10480,
+ 0x10481, 0x10482, 0x10483, 0x10484, 0x10485, 0x10486, 0x10487, 0x10488,
+ 0x10489, 0x1048A, 0x1048B, 0x1048C, 0x1048D, 0x1048E, 0x1048F, 0x10490,
+ 0x10491, 0x10492, 0x10493, 0x10494, 0x10495, 0x10496, 0x10497, 0x10498,
+ 0x10499, 0x1049A, 0x1049B, 0x1049C, 0x1049D, 0x104B0, 0x104B1, 0x104B2,
+ 0x104B3, 0x104B4, 0x104B5, 0x104B6, 0x104B7, 0x104B8, 0x104B9, 0x104BA,
+ 0x104BB, 0x104BC, 0x104BD, 0x104BE, 0x104BF, 0x104C0, 0x104C1, 0x104C2,
+ 0x104C3, 0x104C4, 0x104C5, 0x104C6, 0x104C7, 0x104C8, 0x104C9, 0x104CA,
+ 0x104CB, 0x104CC, 0x104CD, 0x104CE, 0x104CF, 0x104D0, 0x104D1, 0x104D2,
+ 0x104D3, 0x104D8, 0x104D9, 0x104DA, 0x104DB, 0x104DC, 0x104DD, 0x104DE,
+ 0x104DF, 0x104E0, 0x104E1, 0x104E2, 0x104E3, 0x104E4, 0x104E5, 0x104E6,
+ 0x104E7, 0x104E8, 0x104E9, 0x104EA, 0x104EB, 0x104EC, 0x104ED, 0x104EE,
+ 0x104EF, 0x104F0, 0x104F1, 0x104F2, 0x104F3, 0x104F4, 0x104F5, 0x104F6,
+ 0x104F7, 0x104F8, 0x104F9, 0x104FA, 0x104FB, 0x10500, 0x10501, 0x10502,
+ 0x10503, 0x10504, 0x10505, 0x10506, 0x10507, 0x10508, 0x10509, 0x1050A,
+ 0x1050B, 0x1050C, 0x1050D, 0x1050E, 0x1050F, 0x10510, 0x10511, 0x10512,
+ 0x10513, 0x10514, 0x10515, 0x10516, 0x10517, 0x10518, 0x10519, 0x1051A,
+ 0x1051B, 0x1051C, 0x1051D, 0x1051E, 0x1051F, 0x10520, 0x10521, 0x10522,
+ 0x10523, 0x10524, 0x10525, 0x10526, 0x10527, 0x10530, 0x10531, 0x10532,
+ 0x10533, 0x10534, 0x10535, 0x10536, 0x10537, 0x10538, 0x10539, 0x1053A,
+ 0x1053B, 0x1053C, 0x1053D, 0x1053E, 0x1053F, 0x10540, 0x10541, 0x10542,
+ 0x10543, 0x10544, 0x10545, 0x10546, 0x10547, 0x10548, 0x10549, 0x1054A,
+ 0x1054B, 0x1054C, 0x1054D, 0x1054E, 0x1054F, 0x10550, 0x10551, 0x10552,
+ 0x10553, 0x10554, 0x10555, 0x10556, 0x10557, 0x10558, 0x10559, 0x1055A,
+ 0x1055B, 0x1055C, 0x1055D, 0x1055E, 0x1055F, 0x10560, 0x10561, 0x10562,
+ 0x10563, 0x10600, 0x10601, 0x10602, 0x10603, 0x10604, 0x10605, 0x10606,
+ 0x10607, 0x10608, 0x10609, 0x1060A, 0x1060B, 0x1060C, 0x1060D, 0x1060E,
+ 0x1060F, 0x10610, 0x10611, 0x10612, 0x10613, 0x10614, 0x10615, 0x10616,
+ 0x10617, 0x10618, 0x10619, 0x1061A, 0x1061B, 0x1061C, 0x1061D, 0x1061E,
+ 0x1061F, 0x10620, 0x10621, 0x10622, 0x10623, 0x10624, 0x10625, 0x10626,
+ 0x10627, 0x10628, 0x10629, 0x1062A, 0x1062B, 0x1062C, 0x1062D, 0x1062E,
+ 0x1062F, 0x10630, 0x10631, 0x10632, 0x10633, 0x10634, 0x10635, 0x10636,
+ 0x10637, 0x10638, 0x10639, 0x1063A, 0x1063B, 0x1063C, 0x1063D, 0x1063E,
+ 0x1063F, 0x10640, 0x10641, 0x10642, 0x10643, 0x10644, 0x10645, 0x10646,
+ 0x10647, 0x10648, 0x10649, 0x1064A, 0x1064B, 0x1064C, 0x1064D, 0x1064E,
+ 0x1064F, 0x10650, 0x10651, 0x10652, 0x10653, 0x10654, 0x10655, 0x10656,
+ 0x10657, 0x10658, 0x10659, 0x1065A, 0x1065B, 0x1065C, 0x1065D, 0x1065E,
+ 0x1065F, 0x10660, 0x10661, 0x10662, 0x10663, 0x10664, 0x10665, 0x10666,
+ 0x10667, 0x10668, 0x10669, 0x1066A, 0x1066B, 0x1066C, 0x1066D, 0x1066E,
+ 0x1066F, 0x10670, 0x10671, 0x10672, 0x10673, 0x10674, 0x10675, 0x10676,
+ 0x10677, 0x10678, 0x10679, 0x1067A, 0x1067B, 0x1067C, 0x1067D, 0x1067E,
+ 0x1067F, 0x10680, 0x10681, 0x10682, 0x10683, 0x10684, 0x10685, 0x10686,
+ 0x10687, 0x10688, 0x10689, 0x1068A, 0x1068B, 0x1068C, 0x1068D, 0x1068E,
+ 0x1068F, 0x10690, 0x10691, 0x10692, 0x10693, 0x10694, 0x10695, 0x10696,
+ 0x10697, 0x10698, 0x10699, 0x1069A, 0x1069B, 0x1069C, 0x1069D, 0x1069E,
+ 0x1069F, 0x106A0, 0x106A1, 0x106A2, 0x106A3, 0x106A4, 0x106A5, 0x106A6,
+ 0x106A7, 0x106A8, 0x106A9, 0x106AA, 0x106AB, 0x106AC, 0x106AD, 0x106AE,
+ 0x106AF, 0x106B0, 0x106B1, 0x106B2, 0x106B3, 0x106B4, 0x106B5, 0x106B6,
+ 0x106B7, 0x106B8, 0x106B9, 0x106BA, 0x106BB, 0x106BC, 0x106BD, 0x106BE,
+ 0x106BF, 0x106C0, 0x106C1, 0x106C2, 0x106C3, 0x106C4, 0x106C5, 0x106C6,
+ 0x106C7, 0x106C8, 0x106C9, 0x106CA, 0x106CB, 0x106CC, 0x106CD, 0x106CE,
+ 0x106CF, 0x106D0, 0x106D1, 0x106D2, 0x106D3, 0x106D4, 0x106D5, 0x106D6,
+ 0x106D7, 0x106D8, 0x106D9, 0x106DA, 0x106DB, 0x106DC, 0x106DD, 0x106DE,
+ 0x106DF, 0x106E0, 0x106E1, 0x106E2, 0x106E3, 0x106E4, 0x106E5, 0x106E6,
+ 0x106E7, 0x106E8, 0x106E9, 0x106EA, 0x106EB, 0x106EC, 0x106ED, 0x106EE,
+ 0x106EF, 0x106F0, 0x106F1, 0x106F2, 0x106F3, 0x106F4, 0x106F5, 0x106F6,
+ 0x106F7, 0x106F8, 0x106F9, 0x106FA, 0x106FB, 0x106FC, 0x106FD, 0x106FE,
+ 0x106FF, 0x10700, 0x10701, 0x10702, 0x10703, 0x10704, 0x10705, 0x10706,
+ 0x10707, 0x10708, 0x10709, 0x1070A, 0x1070B, 0x1070C, 0x1070D, 0x1070E,
+ 0x1070F, 0x10710, 0x10711, 0x10712, 0x10713, 0x10714, 0x10715, 0x10716,
+ 0x10717, 0x10718, 0x10719, 0x1071A, 0x1071B, 0x1071C, 0x1071D, 0x1071E,
+ 0x1071F, 0x10720, 0x10721, 0x10722, 0x10723, 0x10724, 0x10725, 0x10726,
+ 0x10727, 0x10728, 0x10729, 0x1072A, 0x1072B, 0x1072C, 0x1072D, 0x1072E,
+ 0x1072F, 0x10730, 0x10731, 0x10732, 0x10733, 0x10734, 0x10735, 0x10736,
+ 0x10740, 0x10741, 0x10742, 0x10743, 0x10744, 0x10745, 0x10746, 0x10747,
+ 0x10748, 0x10749, 0x1074A, 0x1074B, 0x1074C, 0x1074D, 0x1074E, 0x1074F,
+ 0x10750, 0x10751, 0x10752, 0x10753, 0x10754, 0x10755, 0x10760, 0x10761,
+ 0x10762, 0x10763, 0x10764, 0x10765, 0x10766, 0x10767, 0x10800, 0x10801,
+ 0x10802, 0x10803, 0x10804, 0x10805, 0x10808, 0x1080A, 0x1080B, 0x1080C,
+ 0x1080D, 0x1080E, 0x1080F, 0x10810, 0x10811, 0x10812, 0x10813, 0x10814,
+ 0x10815, 0x10816, 0x10817, 0x10818, 0x10819, 0x1081A, 0x1081B, 0x1081C,
+ 0x1081D, 0x1081E, 0x1081F, 0x10820, 0x10821, 0x10822, 0x10823, 0x10824,
+ 0x10825, 0x10826, 0x10827, 0x10828, 0x10829, 0x1082A, 0x1082B, 0x1082C,
+ 0x1082D, 0x1082E, 0x1082F, 0x10830, 0x10831, 0x10832, 0x10833, 0x10834,
+ 0x10835, 0x10837, 0x10838, 0x1083C, 0x1083F, 0x10840, 0x10841, 0x10842,
+ 0x10843, 0x10844, 0x10845, 0x10846, 0x10847, 0x10848, 0x10849, 0x1084A,
+ 0x1084B, 0x1084C, 0x1084D, 0x1084E, 0x1084F, 0x10850, 0x10851, 0x10852,
+ 0x10853, 0x10854, 0x10855, 0x10860, 0x10861, 0x10862, 0x10863, 0x10864,
+ 0x10865, 0x10866, 0x10867, 0x10868, 0x10869, 0x1086A, 0x1086B, 0x1086C,
+ 0x1086D, 0x1086E, 0x1086F, 0x10870, 0x10871, 0x10872, 0x10873, 0x10874,
+ 0x10875, 0x10876, 0x10880, 0x10881, 0x10882, 0x10883, 0x10884, 0x10885,
+ 0x10886, 0x10887, 0x10888, 0x10889, 0x1088A, 0x1088B, 0x1088C, 0x1088D,
+ 0x1088E, 0x1088F, 0x10890, 0x10891, 0x10892, 0x10893, 0x10894, 0x10895,
+ 0x10896, 0x10897, 0x10898, 0x10899, 0x1089A, 0x1089B, 0x1089C, 0x1089D,
+ 0x1089E, 0x108E0, 0x108E1, 0x108E2, 0x108E3, 0x108E4, 0x108E5, 0x108E6,
+ 0x108E7, 0x108E8, 0x108E9, 0x108EA, 0x108EB, 0x108EC, 0x108ED, 0x108EE,
+ 0x108EF, 0x108F0, 0x108F1, 0x108F2, 0x108F4, 0x108F5, 0x10900, 0x10901,
+ 0x10902, 0x10903, 0x10904, 0x10905, 0x10906, 0x10907, 0x10908, 0x10909,
+ 0x1090A, 0x1090B, 0x1090C, 0x1090D, 0x1090E, 0x1090F, 0x10910, 0x10911,
+ 0x10912, 0x10913, 0x10914, 0x10915, 0x10920, 0x10921, 0x10922, 0x10923,
+ 0x10924, 0x10925, 0x10926, 0x10927, 0x10928, 0x10929, 0x1092A, 0x1092B,
+ 0x1092C, 0x1092D, 0x1092E, 0x1092F, 0x10930, 0x10931, 0x10932, 0x10933,
+ 0x10934, 0x10935, 0x10936, 0x10937, 0x10938, 0x10939, 0x10980, 0x10981,
+ 0x10982, 0x10983, 0x10984, 0x10985, 0x10986, 0x10987, 0x10988, 0x10989,
+ 0x1098A, 0x1098B, 0x1098C, 0x1098D, 0x1098E, 0x1098F, 0x10990, 0x10991,
+ 0x10992, 0x10993, 0x10994, 0x10995, 0x10996, 0x10997, 0x10998, 0x10999,
+ 0x1099A, 0x1099B, 0x1099C, 0x1099D, 0x1099E, 0x1099F, 0x109A0, 0x109A1,
+ 0x109A2, 0x109A3, 0x109A4, 0x109A5, 0x109A6, 0x109A7, 0x109A8, 0x109A9,
+ 0x109AA, 0x109AB, 0x109AC, 0x109AD, 0x109AE, 0x109AF, 0x109B0, 0x109B1,
+ 0x109B2, 0x109B3, 0x109B4, 0x109B5, 0x109B6, 0x109B7, 0x109BE, 0x109BF,
+ 0x10A00, 0x10A10, 0x10A11, 0x10A12, 0x10A13, 0x10A15, 0x10A16, 0x10A17,
+ 0x10A19, 0x10A1A, 0x10A1B, 0x10A1C, 0x10A1D, 0x10A1E, 0x10A1F, 0x10A20,
+ 0x10A21, 0x10A22, 0x10A23, 0x10A24, 0x10A25, 0x10A26, 0x10A27, 0x10A28,
+ 0x10A29, 0x10A2A, 0x10A2B, 0x10A2C, 0x10A2D, 0x10A2E, 0x10A2F, 0x10A30,
+ 0x10A31, 0x10A32, 0x10A33, 0x10A60, 0x10A61, 0x10A62, 0x10A63, 0x10A64,
+ 0x10A65, 0x10A66, 0x10A67, 0x10A68, 0x10A69, 0x10A6A, 0x10A6B, 0x10A6C,
+ 0x10A6D, 0x10A6E, 0x10A6F, 0x10A70, 0x10A71, 0x10A72, 0x10A73, 0x10A74,
+ 0x10A75, 0x10A76, 0x10A77, 0x10A78, 0x10A79, 0x10A7A, 0x10A7B, 0x10A7C,
+ 0x10A80, 0x10A81, 0x10A82, 0x10A83, 0x10A84, 0x10A85, 0x10A86, 0x10A87,
+ 0x10A88, 0x10A89, 0x10A8A, 0x10A8B, 0x10A8C, 0x10A8D, 0x10A8E, 0x10A8F,
+ 0x10A90, 0x10A91, 0x10A92, 0x10A93, 0x10A94, 0x10A95, 0x10A96, 0x10A97,
+ 0x10A98, 0x10A99, 0x10A9A, 0x10A9B, 0x10A9C, 0x10AC0, 0x10AC1, 0x10AC2,
+ 0x10AC3, 0x10AC4, 0x10AC5, 0x10AC6, 0x10AC7, 0x10AC9, 0x10ACA, 0x10ACB,
+ 0x10ACC, 0x10ACD, 0x10ACE, 0x10ACF, 0x10AD0, 0x10AD1, 0x10AD2, 0x10AD3,
+ 0x10AD4, 0x10AD5, 0x10AD6, 0x10AD7, 0x10AD8, 0x10AD9, 0x10ADA, 0x10ADB,
+ 0x10ADC, 0x10ADD, 0x10ADE, 0x10ADF, 0x10AE0, 0x10AE1, 0x10AE2, 0x10AE3,
+ 0x10AE4, 0x10B00, 0x10B01, 0x10B02, 0x10B03, 0x10B04, 0x10B05, 0x10B06,
+ 0x10B07, 0x10B08, 0x10B09, 0x10B0A, 0x10B0B, 0x10B0C, 0x10B0D, 0x10B0E,
+ 0x10B0F, 0x10B10, 0x10B11, 0x10B12, 0x10B13, 0x10B14, 0x10B15, 0x10B16,
+ 0x10B17, 0x10B18, 0x10B19, 0x10B1A, 0x10B1B, 0x10B1C, 0x10B1D, 0x10B1E,
+ 0x10B1F, 0x10B20, 0x10B21, 0x10B22, 0x10B23, 0x10B24, 0x10B25, 0x10B26,
+ 0x10B27, 0x10B28, 0x10B29, 0x10B2A, 0x10B2B, 0x10B2C, 0x10B2D, 0x10B2E,
+ 0x10B2F, 0x10B30, 0x10B31, 0x10B32, 0x10B33, 0x10B34, 0x10B35, 0x10B40,
+ 0x10B41, 0x10B42, 0x10B43, 0x10B44, 0x10B45, 0x10B46, 0x10B47, 0x10B48,
+ 0x10B49, 0x10B4A, 0x10B4B, 0x10B4C, 0x10B4D, 0x10B4E, 0x10B4F, 0x10B50,
+ 0x10B51, 0x10B52, 0x10B53, 0x10B54, 0x10B55, 0x10B60, 0x10B61, 0x10B62,
+ 0x10B63, 0x10B64, 0x10B65, 0x10B66, 0x10B67, 0x10B68, 0x10B69, 0x10B6A,
+ 0x10B6B, 0x10B6C, 0x10B6D, 0x10B6E, 0x10B6F, 0x10B70, 0x10B71, 0x10B72,
+ 0x10B80, 0x10B81, 0x10B82, 0x10B83, 0x10B84, 0x10B85, 0x10B86, 0x10B87,
+ 0x10B88, 0x10B89, 0x10B8A, 0x10B8B, 0x10B8C, 0x10B8D, 0x10B8E, 0x10B8F,
+ 0x10B90, 0x10B91, 0x10C00, 0x10C01, 0x10C02, 0x10C03, 0x10C04, 0x10C05,
+ 0x10C06, 0x10C07, 0x10C08, 0x10C09, 0x10C0A, 0x10C0B, 0x10C0C, 0x10C0D,
+ 0x10C0E, 0x10C0F, 0x10C10, 0x10C11, 0x10C12, 0x10C13, 0x10C14, 0x10C15,
+ 0x10C16, 0x10C17, 0x10C18, 0x10C19, 0x10C1A, 0x10C1B, 0x10C1C, 0x10C1D,
+ 0x10C1E, 0x10C1F, 0x10C20, 0x10C21, 0x10C22, 0x10C23, 0x10C24, 0x10C25,
+ 0x10C26, 0x10C27, 0x10C28, 0x10C29, 0x10C2A, 0x10C2B, 0x10C2C, 0x10C2D,
+ 0x10C2E, 0x10C2F, 0x10C30, 0x10C31, 0x10C32, 0x10C33, 0x10C34, 0x10C35,
+ 0x10C36, 0x10C37, 0x10C38, 0x10C39, 0x10C3A, 0x10C3B, 0x10C3C, 0x10C3D,
+ 0x10C3E, 0x10C3F, 0x10C40, 0x10C41, 0x10C42, 0x10C43, 0x10C44, 0x10C45,
+ 0x10C46, 0x10C47, 0x10C48, 0x10C80, 0x10C81, 0x10C82, 0x10C83, 0x10C84,
+ 0x10C85, 0x10C86, 0x10C87, 0x10C88, 0x10C89, 0x10C8A, 0x10C8B, 0x10C8C,
+ 0x10C8D, 0x10C8E, 0x10C8F, 0x10C90, 0x10C91, 0x10C92, 0x10C93, 0x10C94,
+ 0x10C95, 0x10C96, 0x10C97, 0x10C98, 0x10C99, 0x10C9A, 0x10C9B, 0x10C9C,
+ 0x10C9D, 0x10C9E, 0x10C9F, 0x10CA0, 0x10CA1, 0x10CA2, 0x10CA3, 0x10CA4,
+ 0x10CA5, 0x10CA6, 0x10CA7, 0x10CA8, 0x10CA9, 0x10CAA, 0x10CAB, 0x10CAC,
+ 0x10CAD, 0x10CAE, 0x10CAF, 0x10CB0, 0x10CB1, 0x10CB2, 0x10CC0, 0x10CC1,
+ 0x10CC2, 0x10CC3, 0x10CC4, 0x10CC5, 0x10CC6, 0x10CC7, 0x10CC8, 0x10CC9,
+ 0x10CCA, 0x10CCB, 0x10CCC, 0x10CCD, 0x10CCE, 0x10CCF, 0x10CD0, 0x10CD1,
+ 0x10CD2, 0x10CD3, 0x10CD4, 0x10CD5, 0x10CD6, 0x10CD7, 0x10CD8, 0x10CD9,
+ 0x10CDA, 0x10CDB, 0x10CDC, 0x10CDD, 0x10CDE, 0x10CDF, 0x10CE0, 0x10CE1,
+ 0x10CE2, 0x10CE3, 0x10CE4, 0x10CE5, 0x10CE6, 0x10CE7, 0x10CE8, 0x10CE9,
+ 0x10CEA, 0x10CEB, 0x10CEC, 0x10CED, 0x10CEE, 0x10CEF, 0x10CF0, 0x10CF1,
+ 0x10CF2, 0x11003, 0x11004, 0x11005, 0x11006, 0x11007, 0x11008, 0x11009,
+ 0x1100A, 0x1100B, 0x1100C, 0x1100D, 0x1100E, 0x1100F, 0x11010, 0x11011,
+ 0x11012, 0x11013, 0x11014, 0x11015, 0x11016, 0x11017, 0x11018, 0x11019,
+ 0x1101A, 0x1101B, 0x1101C, 0x1101D, 0x1101E, 0x1101F, 0x11020, 0x11021,
+ 0x11022, 0x11023, 0x11024, 0x11025, 0x11026, 0x11027, 0x11028, 0x11029,
+ 0x1102A, 0x1102B, 0x1102C, 0x1102D, 0x1102E, 0x1102F, 0x11030, 0x11031,
+ 0x11032, 0x11033, 0x11034, 0x11035, 0x11036, 0x11037, 0x11083, 0x11084,
+ 0x11085, 0x11086, 0x11087, 0x11088, 0x11089, 0x1108A, 0x1108B, 0x1108C,
+ 0x1108D, 0x1108E, 0x1108F, 0x11090, 0x11091, 0x11092, 0x11093, 0x11094,
+ 0x11095, 0x11096, 0x11097, 0x11098, 0x11099, 0x1109A, 0x1109B, 0x1109C,
+ 0x1109D, 0x1109E, 0x1109F, 0x110A0, 0x110A1, 0x110A2, 0x110A3, 0x110A4,
+ 0x110A5, 0x110A6, 0x110A7, 0x110A8, 0x110A9, 0x110AA, 0x110AB, 0x110AC,
+ 0x110AD, 0x110AE, 0x110AF, 0x110D0, 0x110D1, 0x110D2, 0x110D3, 0x110D4,
+ 0x110D5, 0x110D6, 0x110D7, 0x110D8, 0x110D9, 0x110DA, 0x110DB, 0x110DC,
+ 0x110DD, 0x110DE, 0x110DF, 0x110E0, 0x110E1, 0x110E2, 0x110E3, 0x110E4,
+ 0x110E5, 0x110E6, 0x110E7, 0x110E8, 0x11103, 0x11104, 0x11105, 0x11106,
+ 0x11107, 0x11108, 0x11109, 0x1110A, 0x1110B, 0x1110C, 0x1110D, 0x1110E,
+ 0x1110F, 0x11110, 0x11111, 0x11112, 0x11113, 0x11114, 0x11115, 0x11116,
+ 0x11117, 0x11118, 0x11119, 0x1111A, 0x1111B, 0x1111C, 0x1111D, 0x1111E,
+ 0x1111F, 0x11120, 0x11121, 0x11122, 0x11123, 0x11124, 0x11125, 0x11126,
+ 0x11150, 0x11151, 0x11152, 0x11153, 0x11154, 0x11155, 0x11156, 0x11157,
+ 0x11158, 0x11159, 0x1115A, 0x1115B, 0x1115C, 0x1115D, 0x1115E, 0x1115F,
+ 0x11160, 0x11161, 0x11162, 0x11163, 0x11164, 0x11165, 0x11166, 0x11167,
+ 0x11168, 0x11169, 0x1116A, 0x1116B, 0x1116C, 0x1116D, 0x1116E, 0x1116F,
+ 0x11170, 0x11171, 0x11172, 0x11176, 0x11183, 0x11184, 0x11185, 0x11186,
+ 0x11187, 0x11188, 0x11189, 0x1118A, 0x1118B, 0x1118C, 0x1118D, 0x1118E,
+ 0x1118F, 0x11190, 0x11191, 0x11192, 0x11193, 0x11194, 0x11195, 0x11196,
+ 0x11197, 0x11198, 0x11199, 0x1119A, 0x1119B, 0x1119C, 0x1119D, 0x1119E,
+ 0x1119F, 0x111A0, 0x111A1, 0x111A2, 0x111A3, 0x111A4, 0x111A5, 0x111A6,
+ 0x111A7, 0x111A8, 0x111A9, 0x111AA, 0x111AB, 0x111AC, 0x111AD, 0x111AE,
+ 0x111AF, 0x111B0, 0x111B1, 0x111B2, 0x111C1, 0x111C2, 0x111C3, 0x111C4,
+ 0x111DA, 0x111DC, 0x11200, 0x11201, 0x11202, 0x11203, 0x11204, 0x11205,
+ 0x11206, 0x11207, 0x11208, 0x11209, 0x1120A, 0x1120B, 0x1120C, 0x1120D,
+ 0x1120E, 0x1120F, 0x11210, 0x11211, 0x11213, 0x11214, 0x11215, 0x11216,
+ 0x11217, 0x11218, 0x11219, 0x1121A, 0x1121B, 0x1121C, 0x1121D, 0x1121E,
+ 0x1121F, 0x11220, 0x11221, 0x11222, 0x11223, 0x11224, 0x11225, 0x11226,
+ 0x11227, 0x11228, 0x11229, 0x1122A, 0x1122B, 0x11280, 0x11281, 0x11282,
+ 0x11283, 0x11284, 0x11285, 0x11286, 0x11288, 0x1128A, 0x1128B, 0x1128C,
+ 0x1128D, 0x1128F, 0x11290, 0x11291, 0x11292, 0x11293, 0x11294, 0x11295,
+ 0x11296, 0x11297, 0x11298, 0x11299, 0x1129A, 0x1129B, 0x1129C, 0x1129D,
+ 0x1129F, 0x112A0, 0x112A1, 0x112A2, 0x112A3, 0x112A4, 0x112A5, 0x112A6,
+ 0x112A7, 0x112A8, 0x112B0, 0x112B1, 0x112B2, 0x112B3, 0x112B4, 0x112B5,
+ 0x112B6, 0x112B7, 0x112B8, 0x112B9, 0x112BA, 0x112BB, 0x112BC, 0x112BD,
+ 0x112BE, 0x112BF, 0x112C0, 0x112C1, 0x112C2, 0x112C3, 0x112C4, 0x112C5,
+ 0x112C6, 0x112C7, 0x112C8, 0x112C9, 0x112CA, 0x112CB, 0x112CC, 0x112CD,
+ 0x112CE, 0x112CF, 0x112D0, 0x112D1, 0x112D2, 0x112D3, 0x112D4, 0x112D5,
+ 0x112D6, 0x112D7, 0x112D8, 0x112D9, 0x112DA, 0x112DB, 0x112DC, 0x112DD,
+ 0x112DE, 0x11305, 0x11306, 0x11307, 0x11308, 0x11309, 0x1130A, 0x1130B,
+ 0x1130C, 0x1130F, 0x11310, 0x11313, 0x11314, 0x11315, 0x11316, 0x11317,
+ 0x11318, 0x11319, 0x1131A, 0x1131B, 0x1131C, 0x1131D, 0x1131E, 0x1131F,
+ 0x11320, 0x11321, 0x11322, 0x11323, 0x11324, 0x11325, 0x11326, 0x11327,
+ 0x11328, 0x1132A, 0x1132B, 0x1132C, 0x1132D, 0x1132E, 0x1132F, 0x11330,
+ 0x11332, 0x11333, 0x11335, 0x11336, 0x11337, 0x11338, 0x11339, 0x1133D,
+ 0x11350, 0x1135D, 0x1135E, 0x1135F, 0x11360, 0x11361, 0x11400, 0x11401,
+ 0x11402, 0x11403, 0x11404, 0x11405, 0x11406, 0x11407, 0x11408, 0x11409,
+ 0x1140A, 0x1140B, 0x1140C, 0x1140D, 0x1140E, 0x1140F, 0x11410, 0x11411,
+ 0x11412, 0x11413, 0x11414, 0x11415, 0x11416, 0x11417, 0x11418, 0x11419,
+ 0x1141A, 0x1141B, 0x1141C, 0x1141D, 0x1141E, 0x1141F, 0x11420, 0x11421,
+ 0x11422, 0x11423, 0x11424, 0x11425, 0x11426, 0x11427, 0x11428, 0x11429,
+ 0x1142A, 0x1142B, 0x1142C, 0x1142D, 0x1142E, 0x1142F, 0x11430, 0x11431,
+ 0x11432, 0x11433, 0x11434, 0x11447, 0x11448, 0x11449, 0x1144A, 0x11480,
+ 0x11481, 0x11482, 0x11483, 0x11484, 0x11485, 0x11486, 0x11487, 0x11488,
+ 0x11489, 0x1148A, 0x1148B, 0x1148C, 0x1148D, 0x1148E, 0x1148F, 0x11490,
+ 0x11491, 0x11492, 0x11493, 0x11494, 0x11495, 0x11496, 0x11497, 0x11498,
+ 0x11499, 0x1149A, 0x1149B, 0x1149C, 0x1149D, 0x1149E, 0x1149F, 0x114A0,
+ 0x114A1, 0x114A2, 0x114A3, 0x114A4, 0x114A5, 0x114A6, 0x114A7, 0x114A8,
+ 0x114A9, 0x114AA, 0x114AB, 0x114AC, 0x114AD, 0x114AE, 0x114AF, 0x114C4,
+ 0x114C5, 0x114C7, 0x11580, 0x11581, 0x11582, 0x11583, 0x11584, 0x11585,
+ 0x11586, 0x11587, 0x11588, 0x11589, 0x1158A, 0x1158B, 0x1158C, 0x1158D,
+ 0x1158E, 0x1158F, 0x11590, 0x11591, 0x11592, 0x11593, 0x11594, 0x11595,
+ 0x11596, 0x11597, 0x11598, 0x11599, 0x1159A, 0x1159B, 0x1159C, 0x1159D,
+ 0x1159E, 0x1159F, 0x115A0, 0x115A1, 0x115A2, 0x115A3, 0x115A4, 0x115A5,
+ 0x115A6, 0x115A7, 0x115A8, 0x115A9, 0x115AA, 0x115AB, 0x115AC, 0x115AD,
+ 0x115AE, 0x115D8, 0x115D9, 0x115DA, 0x115DB, 0x11600, 0x11601, 0x11602,
+ 0x11603, 0x11604, 0x11605, 0x11606, 0x11607, 0x11608, 0x11609, 0x1160A,
+ 0x1160B, 0x1160C, 0x1160D, 0x1160E, 0x1160F, 0x11610, 0x11611, 0x11612,
+ 0x11613, 0x11614, 0x11615, 0x11616, 0x11617, 0x11618, 0x11619, 0x1161A,
+ 0x1161B, 0x1161C, 0x1161D, 0x1161E, 0x1161F, 0x11620, 0x11621, 0x11622,
+ 0x11623, 0x11624, 0x11625, 0x11626, 0x11627, 0x11628, 0x11629, 0x1162A,
+ 0x1162B, 0x1162C, 0x1162D, 0x1162E, 0x1162F, 0x11644, 0x11680, 0x11681,
+ 0x11682, 0x11683, 0x11684, 0x11685, 0x11686, 0x11687, 0x11688, 0x11689,
+ 0x1168A, 0x1168B, 0x1168C, 0x1168D, 0x1168E, 0x1168F, 0x11690, 0x11691,
+ 0x11692, 0x11693, 0x11694, 0x11695, 0x11696, 0x11697, 0x11698, 0x11699,
+ 0x1169A, 0x1169B, 0x1169C, 0x1169D, 0x1169E, 0x1169F, 0x116A0, 0x116A1,
+ 0x116A2, 0x116A3, 0x116A4, 0x116A5, 0x116A6, 0x116A7, 0x116A8, 0x116A9,
+ 0x116AA, 0x118A0, 0x118A1, 0x118A2, 0x118A3, 0x118A4, 0x118A5, 0x118A6,
+ 0x118A7, 0x118A8, 0x118A9, 0x118AA, 0x118AB, 0x118AC, 0x118AD, 0x118AE,
+ 0x118AF, 0x118B0, 0x118B1, 0x118B2, 0x118B3, 0x118B4, 0x118B5, 0x118B6,
+ 0x118B7, 0x118B8, 0x118B9, 0x118BA, 0x118BB, 0x118BC, 0x118BD, 0x118BE,
+ 0x118BF, 0x118C0, 0x118C1, 0x118C2, 0x118C3, 0x118C4, 0x118C5, 0x118C6,
+ 0x118C7, 0x118C8, 0x118C9, 0x118CA, 0x118CB, 0x118CC, 0x118CD, 0x118CE,
+ 0x118CF, 0x118D0, 0x118D1, 0x118D2, 0x118D3, 0x118D4, 0x118D5, 0x118D6,
+ 0x118D7, 0x118D8, 0x118D9, 0x118DA, 0x118DB, 0x118DC, 0x118DD, 0x118DE,
+ 0x118DF, 0x118FF, 0x11AC0, 0x11AC1, 0x11AC2, 0x11AC3, 0x11AC4, 0x11AC5,
+ 0x11AC6, 0x11AC7, 0x11AC8, 0x11AC9, 0x11ACA, 0x11ACB, 0x11ACC, 0x11ACD,
+ 0x11ACE, 0x11ACF, 0x11AD0, 0x11AD1, 0x11AD2, 0x11AD3, 0x11AD4, 0x11AD5,
+ 0x11AD6, 0x11AD7, 0x11AD8, 0x11AD9, 0x11ADA, 0x11ADB, 0x11ADC, 0x11ADD,
+ 0x11ADE, 0x11ADF, 0x11AE0, 0x11AE1, 0x11AE2, 0x11AE3, 0x11AE4, 0x11AE5,
+ 0x11AE6, 0x11AE7, 0x11AE8, 0x11AE9, 0x11AEA, 0x11AEB, 0x11AEC, 0x11AED,
+ 0x11AEE, 0x11AEF, 0x11AF0, 0x11AF1, 0x11AF2, 0x11AF3, 0x11AF4, 0x11AF5,
+ 0x11AF6, 0x11AF7, 0x11AF8, 0x11C00, 0x11C01, 0x11C02, 0x11C03, 0x11C04,
+ 0x11C05, 0x11C06, 0x11C07, 0x11C08, 0x11C0A, 0x11C0B, 0x11C0C, 0x11C0D,
+ 0x11C0E, 0x11C0F, 0x11C10, 0x11C11, 0x11C12, 0x11C13, 0x11C14, 0x11C15,
+ 0x11C16, 0x11C17, 0x11C18, 0x11C19, 0x11C1A, 0x11C1B, 0x11C1C, 0x11C1D,
+ 0x11C1E, 0x11C1F, 0x11C20, 0x11C21, 0x11C22, 0x11C23, 0x11C24, 0x11C25,
+ 0x11C26, 0x11C27, 0x11C28, 0x11C29, 0x11C2A, 0x11C2B, 0x11C2C, 0x11C2D,
+ 0x11C2E, 0x11C40, 0x11C72, 0x11C73, 0x11C74, 0x11C75, 0x11C76, 0x11C77,
+ 0x11C78, 0x11C79, 0x11C7A, 0x11C7B, 0x11C7C, 0x11C7D, 0x11C7E, 0x11C7F,
+ 0x11C80, 0x11C81, 0x11C82, 0x11C83, 0x11C84, 0x11C85, 0x11C86, 0x11C87,
+ 0x11C88, 0x11C89, 0x11C8A, 0x11C8B, 0x11C8C, 0x11C8D, 0x11C8E, 0x11C8F,
+ 0x12000, 0x12001, 0x12002, 0x12003, 0x12004, 0x12005, 0x12006, 0x12007,
+ 0x12008, 0x12009, 0x1200A, 0x1200B, 0x1200C, 0x1200D, 0x1200E, 0x1200F,
+ 0x12010, 0x12011, 0x12012, 0x12013, 0x12014, 0x12015, 0x12016, 0x12017,
+ 0x12018, 0x12019, 0x1201A, 0x1201B, 0x1201C, 0x1201D, 0x1201E, 0x1201F,
+ 0x12020, 0x12021, 0x12022, 0x12023, 0x12024, 0x12025, 0x12026, 0x12027,
+ 0x12028, 0x12029, 0x1202A, 0x1202B, 0x1202C, 0x1202D, 0x1202E, 0x1202F,
+ 0x12030, 0x12031, 0x12032, 0x12033, 0x12034, 0x12035, 0x12036, 0x12037,
+ 0x12038, 0x12039, 0x1203A, 0x1203B, 0x1203C, 0x1203D, 0x1203E, 0x1203F,
+ 0x12040, 0x12041, 0x12042, 0x12043, 0x12044, 0x12045, 0x12046, 0x12047,
+ 0x12048, 0x12049, 0x1204A, 0x1204B, 0x1204C, 0x1204D, 0x1204E, 0x1204F,
+ 0x12050, 0x12051, 0x12052, 0x12053, 0x12054, 0x12055, 0x12056, 0x12057,
+ 0x12058, 0x12059, 0x1205A, 0x1205B, 0x1205C, 0x1205D, 0x1205E, 0x1205F,
+ 0x12060, 0x12061, 0x12062, 0x12063, 0x12064, 0x12065, 0x12066, 0x12067,
+ 0x12068, 0x12069, 0x1206A, 0x1206B, 0x1206C, 0x1206D, 0x1206E, 0x1206F,
+ 0x12070, 0x12071, 0x12072, 0x12073, 0x12074, 0x12075, 0x12076, 0x12077,
+ 0x12078, 0x12079, 0x1207A, 0x1207B, 0x1207C, 0x1207D, 0x1207E, 0x1207F,
+ 0x12080, 0x12081, 0x12082, 0x12083, 0x12084, 0x12085, 0x12086, 0x12087,
+ 0x12088, 0x12089, 0x1208A, 0x1208B, 0x1208C, 0x1208D, 0x1208E, 0x1208F,
+ 0x12090, 0x12091, 0x12092, 0x12093, 0x12094, 0x12095, 0x12096, 0x12097,
+ 0x12098, 0x12099, 0x1209A, 0x1209B, 0x1209C, 0x1209D, 0x1209E, 0x1209F,
+ 0x120A0, 0x120A1, 0x120A2, 0x120A3, 0x120A4, 0x120A5, 0x120A6, 0x120A7,
+ 0x120A8, 0x120A9, 0x120AA, 0x120AB, 0x120AC, 0x120AD, 0x120AE, 0x120AF,
+ 0x120B0, 0x120B1, 0x120B2, 0x120B3, 0x120B4, 0x120B5, 0x120B6, 0x120B7,
+ 0x120B8, 0x120B9, 0x120BA, 0x120BB, 0x120BC, 0x120BD, 0x120BE, 0x120BF,
+ 0x120C0, 0x120C1, 0x120C2, 0x120C3, 0x120C4, 0x120C5, 0x120C6, 0x120C7,
+ 0x120C8, 0x120C9, 0x120CA, 0x120CB, 0x120CC, 0x120CD, 0x120CE, 0x120CF,
+ 0x120D0, 0x120D1, 0x120D2, 0x120D3, 0x120D4, 0x120D5, 0x120D6, 0x120D7,
+ 0x120D8, 0x120D9, 0x120DA, 0x120DB, 0x120DC, 0x120DD, 0x120DE, 0x120DF,
+ 0x120E0, 0x120E1, 0x120E2, 0x120E3, 0x120E4, 0x120E5, 0x120E6, 0x120E7,
+ 0x120E8, 0x120E9, 0x120EA, 0x120EB, 0x120EC, 0x120ED, 0x120EE, 0x120EF,
+ 0x120F0, 0x120F1, 0x120F2, 0x120F3, 0x120F4, 0x120F5, 0x120F6, 0x120F7,
+ 0x120F8, 0x120F9, 0x120FA, 0x120FB, 0x120FC, 0x120FD, 0x120FE, 0x120FF,
+ 0x12100, 0x12101, 0x12102, 0x12103, 0x12104, 0x12105, 0x12106, 0x12107,
+ 0x12108, 0x12109, 0x1210A, 0x1210B, 0x1210C, 0x1210D, 0x1210E, 0x1210F,
+ 0x12110, 0x12111, 0x12112, 0x12113, 0x12114, 0x12115, 0x12116, 0x12117,
+ 0x12118, 0x12119, 0x1211A, 0x1211B, 0x1211C, 0x1211D, 0x1211E, 0x1211F,
+ 0x12120, 0x12121, 0x12122, 0x12123, 0x12124, 0x12125, 0x12126, 0x12127,
+ 0x12128, 0x12129, 0x1212A, 0x1212B, 0x1212C, 0x1212D, 0x1212E, 0x1212F,
+ 0x12130, 0x12131, 0x12132, 0x12133, 0x12134, 0x12135, 0x12136, 0x12137,
+ 0x12138, 0x12139, 0x1213A, 0x1213B, 0x1213C, 0x1213D, 0x1213E, 0x1213F,
+ 0x12140, 0x12141, 0x12142, 0x12143, 0x12144, 0x12145, 0x12146, 0x12147,
+ 0x12148, 0x12149, 0x1214A, 0x1214B, 0x1214C, 0x1214D, 0x1214E, 0x1214F,
+ 0x12150, 0x12151, 0x12152, 0x12153, 0x12154, 0x12155, 0x12156, 0x12157,
+ 0x12158, 0x12159, 0x1215A, 0x1215B, 0x1215C, 0x1215D, 0x1215E, 0x1215F,
+ 0x12160, 0x12161, 0x12162, 0x12163, 0x12164, 0x12165, 0x12166, 0x12167,
+ 0x12168, 0x12169, 0x1216A, 0x1216B, 0x1216C, 0x1216D, 0x1216E, 0x1216F,
+ 0x12170, 0x12171, 0x12172, 0x12173, 0x12174, 0x12175, 0x12176, 0x12177,
+ 0x12178, 0x12179, 0x1217A, 0x1217B, 0x1217C, 0x1217D, 0x1217E, 0x1217F,
+ 0x12180, 0x12181, 0x12182, 0x12183, 0x12184, 0x12185, 0x12186, 0x12187,
+ 0x12188, 0x12189, 0x1218A, 0x1218B, 0x1218C, 0x1218D, 0x1218E, 0x1218F,
+ 0x12190, 0x12191, 0x12192, 0x12193, 0x12194, 0x12195, 0x12196, 0x12197,
+ 0x12198, 0x12199, 0x1219A, 0x1219B, 0x1219C, 0x1219D, 0x1219E, 0x1219F,
+ 0x121A0, 0x121A1, 0x121A2, 0x121A3, 0x121A4, 0x121A5, 0x121A6, 0x121A7,
+ 0x121A8, 0x121A9, 0x121AA, 0x121AB, 0x121AC, 0x121AD, 0x121AE, 0x121AF,
+ 0x121B0, 0x121B1, 0x121B2, 0x121B3, 0x121B4, 0x121B5, 0x121B6, 0x121B7,
+ 0x121B8, 0x121B9, 0x121BA, 0x121BB, 0x121BC, 0x121BD, 0x121BE, 0x121BF,
+ 0x121C0, 0x121C1, 0x121C2, 0x121C3, 0x121C4, 0x121C5, 0x121C6, 0x121C7,
+ 0x121C8, 0x121C9, 0x121CA, 0x121CB, 0x121CC, 0x121CD, 0x121CE, 0x121CF,
+ 0x121D0, 0x121D1, 0x121D2, 0x121D3, 0x121D4, 0x121D5, 0x121D6, 0x121D7,
+ 0x121D8, 0x121D9, 0x121DA, 0x121DB, 0x121DC, 0x121DD, 0x121DE, 0x121DF,
+ 0x121E0, 0x121E1, 0x121E2, 0x121E3, 0x121E4, 0x121E5, 0x121E6, 0x121E7,
+ 0x121E8, 0x121E9, 0x121EA, 0x121EB, 0x121EC, 0x121ED, 0x121EE, 0x121EF,
+ 0x121F0, 0x121F1, 0x121F2, 0x121F3, 0x121F4, 0x121F5, 0x121F6, 0x121F7,
+ 0x121F8, 0x121F9, 0x121FA, 0x121FB, 0x121FC, 0x121FD, 0x121FE, 0x121FF,
+ 0x12200, 0x12201, 0x12202, 0x12203, 0x12204, 0x12205, 0x12206, 0x12207,
+ 0x12208, 0x12209, 0x1220A, 0x1220B, 0x1220C, 0x1220D, 0x1220E, 0x1220F,
+ 0x12210, 0x12211, 0x12212, 0x12213, 0x12214, 0x12215, 0x12216, 0x12217,
+ 0x12218, 0x12219, 0x1221A, 0x1221B, 0x1221C, 0x1221D, 0x1221E, 0x1221F,
+ 0x12220, 0x12221, 0x12222, 0x12223, 0x12224, 0x12225, 0x12226, 0x12227,
+ 0x12228, 0x12229, 0x1222A, 0x1222B, 0x1222C, 0x1222D, 0x1222E, 0x1222F,
+ 0x12230, 0x12231, 0x12232, 0x12233, 0x12234, 0x12235, 0x12236, 0x12237,
+ 0x12238, 0x12239, 0x1223A, 0x1223B, 0x1223C, 0x1223D, 0x1223E, 0x1223F,
+ 0x12240, 0x12241, 0x12242, 0x12243, 0x12244, 0x12245, 0x12246, 0x12247,
+ 0x12248, 0x12249, 0x1224A, 0x1224B, 0x1224C, 0x1224D, 0x1224E, 0x1224F,
+ 0x12250, 0x12251, 0x12252, 0x12253, 0x12254, 0x12255, 0x12256, 0x12257,
+ 0x12258, 0x12259, 0x1225A, 0x1225B, 0x1225C, 0x1225D, 0x1225E, 0x1225F,
+ 0x12260, 0x12261, 0x12262, 0x12263, 0x12264, 0x12265, 0x12266, 0x12267,
+ 0x12268, 0x12269, 0x1226A, 0x1226B, 0x1226C, 0x1226D, 0x1226E, 0x1226F,
+ 0x12270, 0x12271, 0x12272, 0x12273, 0x12274, 0x12275, 0x12276, 0x12277,
+ 0x12278, 0x12279, 0x1227A, 0x1227B, 0x1227C, 0x1227D, 0x1227E, 0x1227F,
+ 0x12280, 0x12281, 0x12282, 0x12283, 0x12284, 0x12285, 0x12286, 0x12287,
+ 0x12288, 0x12289, 0x1228A, 0x1228B, 0x1228C, 0x1228D, 0x1228E, 0x1228F,
+ 0x12290, 0x12291, 0x12292, 0x12293, 0x12294, 0x12295, 0x12296, 0x12297,
+ 0x12298, 0x12299, 0x1229A, 0x1229B, 0x1229C, 0x1229D, 0x1229E, 0x1229F,
+ 0x122A0, 0x122A1, 0x122A2, 0x122A3, 0x122A4, 0x122A5, 0x122A6, 0x122A7,
+ 0x122A8, 0x122A9, 0x122AA, 0x122AB, 0x122AC, 0x122AD, 0x122AE, 0x122AF,
+ 0x122B0, 0x122B1, 0x122B2, 0x122B3, 0x122B4, 0x122B5, 0x122B6, 0x122B7,
+ 0x122B8, 0x122B9, 0x122BA, 0x122BB, 0x122BC, 0x122BD, 0x122BE, 0x122BF,
+ 0x122C0, 0x122C1, 0x122C2, 0x122C3, 0x122C4, 0x122C5, 0x122C6, 0x122C7,
+ 0x122C8, 0x122C9, 0x122CA, 0x122CB, 0x122CC, 0x122CD, 0x122CE, 0x122CF,
+ 0x122D0, 0x122D1, 0x122D2, 0x122D3, 0x122D4, 0x122D5, 0x122D6, 0x122D7,
+ 0x122D8, 0x122D9, 0x122DA, 0x122DB, 0x122DC, 0x122DD, 0x122DE, 0x122DF,
+ 0x122E0, 0x122E1, 0x122E2, 0x122E3, 0x122E4, 0x122E5, 0x122E6, 0x122E7,
+ 0x122E8, 0x122E9, 0x122EA, 0x122EB, 0x122EC, 0x122ED, 0x122EE, 0x122EF,
+ 0x122F0, 0x122F1, 0x122F2, 0x122F3, 0x122F4, 0x122F5, 0x122F6, 0x122F7,
+ 0x122F8, 0x122F9, 0x122FA, 0x122FB, 0x122FC, 0x122FD, 0x122FE, 0x122FF,
+ 0x12300, 0x12301, 0x12302, 0x12303, 0x12304, 0x12305, 0x12306, 0x12307,
+ 0x12308, 0x12309, 0x1230A, 0x1230B, 0x1230C, 0x1230D, 0x1230E, 0x1230F,
+ 0x12310, 0x12311, 0x12312, 0x12313, 0x12314, 0x12315, 0x12316, 0x12317,
+ 0x12318, 0x12319, 0x1231A, 0x1231B, 0x1231C, 0x1231D, 0x1231E, 0x1231F,
+ 0x12320, 0x12321, 0x12322, 0x12323, 0x12324, 0x12325, 0x12326, 0x12327,
+ 0x12328, 0x12329, 0x1232A, 0x1232B, 0x1232C, 0x1232D, 0x1232E, 0x1232F,
+ 0x12330, 0x12331, 0x12332, 0x12333, 0x12334, 0x12335, 0x12336, 0x12337,
+ 0x12338, 0x12339, 0x1233A, 0x1233B, 0x1233C, 0x1233D, 0x1233E, 0x1233F,
+ 0x12340, 0x12341, 0x12342, 0x12343, 0x12344, 0x12345, 0x12346, 0x12347,
+ 0x12348, 0x12349, 0x1234A, 0x1234B, 0x1234C, 0x1234D, 0x1234E, 0x1234F,
+ 0x12350, 0x12351, 0x12352, 0x12353, 0x12354, 0x12355, 0x12356, 0x12357,
+ 0x12358, 0x12359, 0x1235A, 0x1235B, 0x1235C, 0x1235D, 0x1235E, 0x1235F,
+ 0x12360, 0x12361, 0x12362, 0x12363, 0x12364, 0x12365, 0x12366, 0x12367,
+ 0x12368, 0x12369, 0x1236A, 0x1236B, 0x1236C, 0x1236D, 0x1236E, 0x1236F,
+ 0x12370, 0x12371, 0x12372, 0x12373, 0x12374, 0x12375, 0x12376, 0x12377,
+ 0x12378, 0x12379, 0x1237A, 0x1237B, 0x1237C, 0x1237D, 0x1237E, 0x1237F,
+ 0x12380, 0x12381, 0x12382, 0x12383, 0x12384, 0x12385, 0x12386, 0x12387,
+ 0x12388, 0x12389, 0x1238A, 0x1238B, 0x1238C, 0x1238D, 0x1238E, 0x1238F,
+ 0x12390, 0x12391, 0x12392, 0x12393, 0x12394, 0x12395, 0x12396, 0x12397,
+ 0x12398, 0x12399, 0x12400, 0x12401, 0x12402, 0x12403, 0x12404, 0x12405,
+ 0x12406, 0x12407, 0x12408, 0x12409, 0x1240A, 0x1240B, 0x1240C, 0x1240D,
+ 0x1240E, 0x1240F, 0x12410, 0x12411, 0x12412, 0x12413, 0x12414, 0x12415,
+ 0x12416, 0x12417, 0x12418, 0x12419, 0x1241A, 0x1241B, 0x1241C, 0x1241D,
+ 0x1241E, 0x1241F, 0x12420, 0x12421, 0x12422, 0x12423, 0x12424, 0x12425,
+ 0x12426, 0x12427, 0x12428, 0x12429, 0x1242A, 0x1242B, 0x1242C, 0x1242D,
+ 0x1242E, 0x1242F, 0x12430, 0x12431, 0x12432, 0x12433, 0x12434, 0x12435,
+ 0x12436, 0x12437, 0x12438, 0x12439, 0x1243A, 0x1243B, 0x1243C, 0x1243D,
+ 0x1243E, 0x1243F, 0x12440, 0x12441, 0x12442, 0x12443, 0x12444, 0x12445,
+ 0x12446, 0x12447, 0x12448, 0x12449, 0x1244A, 0x1244B, 0x1244C, 0x1244D,
+ 0x1244E, 0x1244F, 0x12450, 0x12451, 0x12452, 0x12453, 0x12454, 0x12455,
+ 0x12456, 0x12457, 0x12458, 0x12459, 0x1245A, 0x1245B, 0x1245C, 0x1245D,
+ 0x1245E, 0x1245F, 0x12460, 0x12461, 0x12462, 0x12463, 0x12464, 0x12465,
+ 0x12466, 0x12467, 0x12468, 0x12469, 0x1246A, 0x1246B, 0x1246C, 0x1246D,
+ 0x1246E, 0x12480, 0x12481, 0x12482, 0x12483, 0x12484, 0x12485, 0x12486,
+ 0x12487, 0x12488, 0x12489, 0x1248A, 0x1248B, 0x1248C, 0x1248D, 0x1248E,
+ 0x1248F, 0x12490, 0x12491, 0x12492, 0x12493, 0x12494, 0x12495, 0x12496,
+ 0x12497, 0x12498, 0x12499, 0x1249A, 0x1249B, 0x1249C, 0x1249D, 0x1249E,
+ 0x1249F, 0x124A0, 0x124A1, 0x124A2, 0x124A3, 0x124A4, 0x124A5, 0x124A6,
+ 0x124A7, 0x124A8, 0x124A9, 0x124AA, 0x124AB, 0x124AC, 0x124AD, 0x124AE,
+ 0x124AF, 0x124B0, 0x124B1, 0x124B2, 0x124B3, 0x124B4, 0x124B5, 0x124B6,
+ 0x124B7, 0x124B8, 0x124B9, 0x124BA, 0x124BB, 0x124BC, 0x124BD, 0x124BE,
+ 0x124BF, 0x124C0, 0x124C1, 0x124C2, 0x124C3, 0x124C4, 0x124C5, 0x124C6,
+ 0x124C7, 0x124C8, 0x124C9, 0x124CA, 0x124CB, 0x124CC, 0x124CD, 0x124CE,
+ 0x124CF, 0x124D0, 0x124D1, 0x124D2, 0x124D3, 0x124D4, 0x124D5, 0x124D6,
+ 0x124D7, 0x124D8, 0x124D9, 0x124DA, 0x124DB, 0x124DC, 0x124DD, 0x124DE,
+ 0x124DF, 0x124E0, 0x124E1, 0x124E2, 0x124E3, 0x124E4, 0x124E5, 0x124E6,
+ 0x124E7, 0x124E8, 0x124E9, 0x124EA, 0x124EB, 0x124EC, 0x124ED, 0x124EE,
+ 0x124EF, 0x124F0, 0x124F1, 0x124F2, 0x124F3, 0x124F4, 0x124F5, 0x124F6,
+ 0x124F7, 0x124F8, 0x124F9, 0x124FA, 0x124FB, 0x124FC, 0x124FD, 0x124FE,
+ 0x124FF, 0x12500, 0x12501, 0x12502, 0x12503, 0x12504, 0x12505, 0x12506,
+ 0x12507, 0x12508, 0x12509, 0x1250A, 0x1250B, 0x1250C, 0x1250D, 0x1250E,
+ 0x1250F, 0x12510, 0x12511, 0x12512, 0x12513, 0x12514, 0x12515, 0x12516,
+ 0x12517, 0x12518, 0x12519, 0x1251A, 0x1251B, 0x1251C, 0x1251D, 0x1251E,
+ 0x1251F, 0x12520, 0x12521, 0x12522, 0x12523, 0x12524, 0x12525, 0x12526,
+ 0x12527, 0x12528, 0x12529, 0x1252A, 0x1252B, 0x1252C, 0x1252D, 0x1252E,
+ 0x1252F, 0x12530, 0x12531, 0x12532, 0x12533, 0x12534, 0x12535, 0x12536,
+ 0x12537, 0x12538, 0x12539, 0x1253A, 0x1253B, 0x1253C, 0x1253D, 0x1253E,
+ 0x1253F, 0x12540, 0x12541, 0x12542, 0x12543, 0x13000, 0x13001, 0x13002,
+ 0x13003, 0x13004, 0x13005, 0x13006, 0x13007, 0x13008, 0x13009, 0x1300A,
+ 0x1300B, 0x1300C, 0x1300D, 0x1300E, 0x1300F, 0x13010, 0x13011, 0x13012,
+ 0x13013, 0x13014, 0x13015, 0x13016, 0x13017, 0x13018, 0x13019, 0x1301A,
+ 0x1301B, 0x1301C, 0x1301D, 0x1301E, 0x1301F, 0x13020, 0x13021, 0x13022,
+ 0x13023, 0x13024, 0x13025, 0x13026, 0x13027, 0x13028, 0x13029, 0x1302A,
+ 0x1302B, 0x1302C, 0x1302D, 0x1302E, 0x1302F, 0x13030, 0x13031, 0x13032,
+ 0x13033, 0x13034, 0x13035, 0x13036, 0x13037, 0x13038, 0x13039, 0x1303A,
+ 0x1303B, 0x1303C, 0x1303D, 0x1303E, 0x1303F, 0x13040, 0x13041, 0x13042,
+ 0x13043, 0x13044, 0x13045, 0x13046, 0x13047, 0x13048, 0x13049, 0x1304A,
+ 0x1304B, 0x1304C, 0x1304D, 0x1304E, 0x1304F, 0x13050, 0x13051, 0x13052,
+ 0x13053, 0x13054, 0x13055, 0x13056, 0x13057, 0x13058, 0x13059, 0x1305A,
+ 0x1305B, 0x1305C, 0x1305D, 0x1305E, 0x1305F, 0x13060, 0x13061, 0x13062,
+ 0x13063, 0x13064, 0x13065, 0x13066, 0x13067, 0x13068, 0x13069, 0x1306A,
+ 0x1306B, 0x1306C, 0x1306D, 0x1306E, 0x1306F, 0x13070, 0x13071, 0x13072,
+ 0x13073, 0x13074, 0x13075, 0x13076, 0x13077, 0x13078, 0x13079, 0x1307A,
+ 0x1307B, 0x1307C, 0x1307D, 0x1307E, 0x1307F, 0x13080, 0x13081, 0x13082,
+ 0x13083, 0x13084, 0x13085, 0x13086, 0x13087, 0x13088, 0x13089, 0x1308A,
+ 0x1308B, 0x1308C, 0x1308D, 0x1308E, 0x1308F, 0x13090, 0x13091, 0x13092,
+ 0x13093, 0x13094, 0x13095, 0x13096, 0x13097, 0x13098, 0x13099, 0x1309A,
+ 0x1309B, 0x1309C, 0x1309D, 0x1309E, 0x1309F, 0x130A0, 0x130A1, 0x130A2,
+ 0x130A3, 0x130A4, 0x130A5, 0x130A6, 0x130A7, 0x130A8, 0x130A9, 0x130AA,
+ 0x130AB, 0x130AC, 0x130AD, 0x130AE, 0x130AF, 0x130B0, 0x130B1, 0x130B2,
+ 0x130B3, 0x130B4, 0x130B5, 0x130B6, 0x130B7, 0x130B8, 0x130B9, 0x130BA,
+ 0x130BB, 0x130BC, 0x130BD, 0x130BE, 0x130BF, 0x130C0, 0x130C1, 0x130C2,
+ 0x130C3, 0x130C4, 0x130C5, 0x130C6, 0x130C7, 0x130C8, 0x130C9, 0x130CA,
+ 0x130CB, 0x130CC, 0x130CD, 0x130CE, 0x130CF, 0x130D0, 0x130D1, 0x130D2,
+ 0x130D3, 0x130D4, 0x130D5, 0x130D6, 0x130D7, 0x130D8, 0x130D9, 0x130DA,
+ 0x130DB, 0x130DC, 0x130DD, 0x130DE, 0x130DF, 0x130E0, 0x130E1, 0x130E2,
+ 0x130E3, 0x130E4, 0x130E5, 0x130E6, 0x130E7, 0x130E8, 0x130E9, 0x130EA,
+ 0x130EB, 0x130EC, 0x130ED, 0x130EE, 0x130EF, 0x130F0, 0x130F1, 0x130F2,
+ 0x130F3, 0x130F4, 0x130F5, 0x130F6, 0x130F7, 0x130F8, 0x130F9, 0x130FA,
+ 0x130FB, 0x130FC, 0x130FD, 0x130FE, 0x130FF, 0x13100, 0x13101, 0x13102,
+ 0x13103, 0x13104, 0x13105, 0x13106, 0x13107, 0x13108, 0x13109, 0x1310A,
+ 0x1310B, 0x1310C, 0x1310D, 0x1310E, 0x1310F, 0x13110, 0x13111, 0x13112,
+ 0x13113, 0x13114, 0x13115, 0x13116, 0x13117, 0x13118, 0x13119, 0x1311A,
+ 0x1311B, 0x1311C, 0x1311D, 0x1311E, 0x1311F, 0x13120, 0x13121, 0x13122,
+ 0x13123, 0x13124, 0x13125, 0x13126, 0x13127, 0x13128, 0x13129, 0x1312A,
+ 0x1312B, 0x1312C, 0x1312D, 0x1312E, 0x1312F, 0x13130, 0x13131, 0x13132,
+ 0x13133, 0x13134, 0x13135, 0x13136, 0x13137, 0x13138, 0x13139, 0x1313A,
+ 0x1313B, 0x1313C, 0x1313D, 0x1313E, 0x1313F, 0x13140, 0x13141, 0x13142,
+ 0x13143, 0x13144, 0x13145, 0x13146, 0x13147, 0x13148, 0x13149, 0x1314A,
+ 0x1314B, 0x1314C, 0x1314D, 0x1314E, 0x1314F, 0x13150, 0x13151, 0x13152,
+ 0x13153, 0x13154, 0x13155, 0x13156, 0x13157, 0x13158, 0x13159, 0x1315A,
+ 0x1315B, 0x1315C, 0x1315D, 0x1315E, 0x1315F, 0x13160, 0x13161, 0x13162,
+ 0x13163, 0x13164, 0x13165, 0x13166, 0x13167, 0x13168, 0x13169, 0x1316A,
+ 0x1316B, 0x1316C, 0x1316D, 0x1316E, 0x1316F, 0x13170, 0x13171, 0x13172,
+ 0x13173, 0x13174, 0x13175, 0x13176, 0x13177, 0x13178, 0x13179, 0x1317A,
+ 0x1317B, 0x1317C, 0x1317D, 0x1317E, 0x1317F, 0x13180, 0x13181, 0x13182,
+ 0x13183, 0x13184, 0x13185, 0x13186, 0x13187, 0x13188, 0x13189, 0x1318A,
+ 0x1318B, 0x1318C, 0x1318D, 0x1318E, 0x1318F, 0x13190, 0x13191, 0x13192,
+ 0x13193, 0x13194, 0x13195, 0x13196, 0x13197, 0x13198, 0x13199, 0x1319A,
+ 0x1319B, 0x1319C, 0x1319D, 0x1319E, 0x1319F, 0x131A0, 0x131A1, 0x131A2,
+ 0x131A3, 0x131A4, 0x131A5, 0x131A6, 0x131A7, 0x131A8, 0x131A9, 0x131AA,
+ 0x131AB, 0x131AC, 0x131AD, 0x131AE, 0x131AF, 0x131B0, 0x131B1, 0x131B2,
+ 0x131B3, 0x131B4, 0x131B5, 0x131B6, 0x131B7, 0x131B8, 0x131B9, 0x131BA,
+ 0x131BB, 0x131BC, 0x131BD, 0x131BE, 0x131BF, 0x131C0, 0x131C1, 0x131C2,
+ 0x131C3, 0x131C4, 0x131C5, 0x131C6, 0x131C7, 0x131C8, 0x131C9, 0x131CA,
+ 0x131CB, 0x131CC, 0x131CD, 0x131CE, 0x131CF, 0x131D0, 0x131D1, 0x131D2,
+ 0x131D3, 0x131D4, 0x131D5, 0x131D6, 0x131D7, 0x131D8, 0x131D9, 0x131DA,
+ 0x131DB, 0x131DC, 0x131DD, 0x131DE, 0x131DF, 0x131E0, 0x131E1, 0x131E2,
+ 0x131E3, 0x131E4, 0x131E5, 0x131E6, 0x131E7, 0x131E8, 0x131E9, 0x131EA,
+ 0x131EB, 0x131EC, 0x131ED, 0x131EE, 0x131EF, 0x131F0, 0x131F1, 0x131F2,
+ 0x131F3, 0x131F4, 0x131F5, 0x131F6, 0x131F7, 0x131F8, 0x131F9, 0x131FA,
+ 0x131FB, 0x131FC, 0x131FD, 0x131FE, 0x131FF, 0x13200, 0x13201, 0x13202,
+ 0x13203, 0x13204, 0x13205, 0x13206, 0x13207, 0x13208, 0x13209, 0x1320A,
+ 0x1320B, 0x1320C, 0x1320D, 0x1320E, 0x1320F, 0x13210, 0x13211, 0x13212,
+ 0x13213, 0x13214, 0x13215, 0x13216, 0x13217, 0x13218, 0x13219, 0x1321A,
+ 0x1321B, 0x1321C, 0x1321D, 0x1321E, 0x1321F, 0x13220, 0x13221, 0x13222,
+ 0x13223, 0x13224, 0x13225, 0x13226, 0x13227, 0x13228, 0x13229, 0x1322A,
+ 0x1322B, 0x1322C, 0x1322D, 0x1322E, 0x1322F, 0x13230, 0x13231, 0x13232,
+ 0x13233, 0x13234, 0x13235, 0x13236, 0x13237, 0x13238, 0x13239, 0x1323A,
+ 0x1323B, 0x1323C, 0x1323D, 0x1323E, 0x1323F, 0x13240, 0x13241, 0x13242,
+ 0x13243, 0x13244, 0x13245, 0x13246, 0x13247, 0x13248, 0x13249, 0x1324A,
+ 0x1324B, 0x1324C, 0x1324D, 0x1324E, 0x1324F, 0x13250, 0x13251, 0x13252,
+ 0x13253, 0x13254, 0x13255, 0x13256, 0x13257, 0x13258, 0x13259, 0x1325A,
+ 0x1325B, 0x1325C, 0x1325D, 0x1325E, 0x1325F, 0x13260, 0x13261, 0x13262,
+ 0x13263, 0x13264, 0x13265, 0x13266, 0x13267, 0x13268, 0x13269, 0x1326A,
+ 0x1326B, 0x1326C, 0x1326D, 0x1326E, 0x1326F, 0x13270, 0x13271, 0x13272,
+ 0x13273, 0x13274, 0x13275, 0x13276, 0x13277, 0x13278, 0x13279, 0x1327A,
+ 0x1327B, 0x1327C, 0x1327D, 0x1327E, 0x1327F, 0x13280, 0x13281, 0x13282,
+ 0x13283, 0x13284, 0x13285, 0x13286, 0x13287, 0x13288, 0x13289, 0x1328A,
+ 0x1328B, 0x1328C, 0x1328D, 0x1328E, 0x1328F, 0x13290, 0x13291, 0x13292,
+ 0x13293, 0x13294, 0x13295, 0x13296, 0x13297, 0x13298, 0x13299, 0x1329A,
+ 0x1329B, 0x1329C, 0x1329D, 0x1329E, 0x1329F, 0x132A0, 0x132A1, 0x132A2,
+ 0x132A3, 0x132A4, 0x132A5, 0x132A6, 0x132A7, 0x132A8, 0x132A9, 0x132AA,
+ 0x132AB, 0x132AC, 0x132AD, 0x132AE, 0x132AF, 0x132B0, 0x132B1, 0x132B2,
+ 0x132B3, 0x132B4, 0x132B5, 0x132B6, 0x132B7, 0x132B8, 0x132B9, 0x132BA,
+ 0x132BB, 0x132BC, 0x132BD, 0x132BE, 0x132BF, 0x132C0, 0x132C1, 0x132C2,
+ 0x132C3, 0x132C4, 0x132C5, 0x132C6, 0x132C7, 0x132C8, 0x132C9, 0x132CA,
+ 0x132CB, 0x132CC, 0x132CD, 0x132CE, 0x132CF, 0x132D0, 0x132D1, 0x132D2,
+ 0x132D3, 0x132D4, 0x132D5, 0x132D6, 0x132D7, 0x132D8, 0x132D9, 0x132DA,
+ 0x132DB, 0x132DC, 0x132DD, 0x132DE, 0x132DF, 0x132E0, 0x132E1, 0x132E2,
+ 0x132E3, 0x132E4, 0x132E5, 0x132E6, 0x132E7, 0x132E8, 0x132E9, 0x132EA,
+ 0x132EB, 0x132EC, 0x132ED, 0x132EE, 0x132EF, 0x132F0, 0x132F1, 0x132F2,
+ 0x132F3, 0x132F4, 0x132F5, 0x132F6, 0x132F7, 0x132F8, 0x132F9, 0x132FA,
+ 0x132FB, 0x132FC, 0x132FD, 0x132FE, 0x132FF, 0x13300, 0x13301, 0x13302,
+ 0x13303, 0x13304, 0x13305, 0x13306, 0x13307, 0x13308, 0x13309, 0x1330A,
+ 0x1330B, 0x1330C, 0x1330D, 0x1330E, 0x1330F, 0x13310, 0x13311, 0x13312,
+ 0x13313, 0x13314, 0x13315, 0x13316, 0x13317, 0x13318, 0x13319, 0x1331A,
+ 0x1331B, 0x1331C, 0x1331D, 0x1331E, 0x1331F, 0x13320, 0x13321, 0x13322,
+ 0x13323, 0x13324, 0x13325, 0x13326, 0x13327, 0x13328, 0x13329, 0x1332A,
+ 0x1332B, 0x1332C, 0x1332D, 0x1332E, 0x1332F, 0x13330, 0x13331, 0x13332,
+ 0x13333, 0x13334, 0x13335, 0x13336, 0x13337, 0x13338, 0x13339, 0x1333A,
+ 0x1333B, 0x1333C, 0x1333D, 0x1333E, 0x1333F, 0x13340, 0x13341, 0x13342,
+ 0x13343, 0x13344, 0x13345, 0x13346, 0x13347, 0x13348, 0x13349, 0x1334A,
+ 0x1334B, 0x1334C, 0x1334D, 0x1334E, 0x1334F, 0x13350, 0x13351, 0x13352,
+ 0x13353, 0x13354, 0x13355, 0x13356, 0x13357, 0x13358, 0x13359, 0x1335A,
+ 0x1335B, 0x1335C, 0x1335D, 0x1335E, 0x1335F, 0x13360, 0x13361, 0x13362,
+ 0x13363, 0x13364, 0x13365, 0x13366, 0x13367, 0x13368, 0x13369, 0x1336A,
+ 0x1336B, 0x1336C, 0x1336D, 0x1336E, 0x1336F, 0x13370, 0x13371, 0x13372,
+ 0x13373, 0x13374, 0x13375, 0x13376, 0x13377, 0x13378, 0x13379, 0x1337A,
+ 0x1337B, 0x1337C, 0x1337D, 0x1337E, 0x1337F, 0x13380, 0x13381, 0x13382,
+ 0x13383, 0x13384, 0x13385, 0x13386, 0x13387, 0x13388, 0x13389, 0x1338A,
+ 0x1338B, 0x1338C, 0x1338D, 0x1338E, 0x1338F, 0x13390, 0x13391, 0x13392,
+ 0x13393, 0x13394, 0x13395, 0x13396, 0x13397, 0x13398, 0x13399, 0x1339A,
+ 0x1339B, 0x1339C, 0x1339D, 0x1339E, 0x1339F, 0x133A0, 0x133A1, 0x133A2,
+ 0x133A3, 0x133A4, 0x133A5, 0x133A6, 0x133A7, 0x133A8, 0x133A9, 0x133AA,
+ 0x133AB, 0x133AC, 0x133AD, 0x133AE, 0x133AF, 0x133B0, 0x133B1, 0x133B2,
+ 0x133B3, 0x133B4, 0x133B5, 0x133B6, 0x133B7, 0x133B8, 0x133B9, 0x133BA,
+ 0x133BB, 0x133BC, 0x133BD, 0x133BE, 0x133BF, 0x133C0, 0x133C1, 0x133C2,
+ 0x133C3, 0x133C4, 0x133C5, 0x133C6, 0x133C7, 0x133C8, 0x133C9, 0x133CA,
+ 0x133CB, 0x133CC, 0x133CD, 0x133CE, 0x133CF, 0x133D0, 0x133D1, 0x133D2,
+ 0x133D3, 0x133D4, 0x133D5, 0x133D6, 0x133D7, 0x133D8, 0x133D9, 0x133DA,
+ 0x133DB, 0x133DC, 0x133DD, 0x133DE, 0x133DF, 0x133E0, 0x133E1, 0x133E2,
+ 0x133E3, 0x133E4, 0x133E5, 0x133E6, 0x133E7, 0x133E8, 0x133E9, 0x133EA,
+ 0x133EB, 0x133EC, 0x133ED, 0x133EE, 0x133EF, 0x133F0, 0x133F1, 0x133F2,
+ 0x133F3, 0x133F4, 0x133F5, 0x133F6, 0x133F7, 0x133F8, 0x133F9, 0x133FA,
+ 0x133FB, 0x133FC, 0x133FD, 0x133FE, 0x133FF, 0x13400, 0x13401, 0x13402,
+ 0x13403, 0x13404, 0x13405, 0x13406, 0x13407, 0x13408, 0x13409, 0x1340A,
+ 0x1340B, 0x1340C, 0x1340D, 0x1340E, 0x1340F, 0x13410, 0x13411, 0x13412,
+ 0x13413, 0x13414, 0x13415, 0x13416, 0x13417, 0x13418, 0x13419, 0x1341A,
+ 0x1341B, 0x1341C, 0x1341D, 0x1341E, 0x1341F, 0x13420, 0x13421, 0x13422,
+ 0x13423, 0x13424, 0x13425, 0x13426, 0x13427, 0x13428, 0x13429, 0x1342A,
+ 0x1342B, 0x1342C, 0x1342D, 0x1342E, 0x14400, 0x14401, 0x14402, 0x14403,
+ 0x14404, 0x14405, 0x14406, 0x14407, 0x14408, 0x14409, 0x1440A, 0x1440B,
+ 0x1440C, 0x1440D, 0x1440E, 0x1440F, 0x14410, 0x14411, 0x14412, 0x14413,
+ 0x14414, 0x14415, 0x14416, 0x14417, 0x14418, 0x14419, 0x1441A, 0x1441B,
+ 0x1441C, 0x1441D, 0x1441E, 0x1441F, 0x14420, 0x14421, 0x14422, 0x14423,
+ 0x14424, 0x14425, 0x14426, 0x14427, 0x14428, 0x14429, 0x1442A, 0x1442B,
+ 0x1442C, 0x1442D, 0x1442E, 0x1442F, 0x14430, 0x14431, 0x14432, 0x14433,
+ 0x14434, 0x14435, 0x14436, 0x14437, 0x14438, 0x14439, 0x1443A, 0x1443B,
+ 0x1443C, 0x1443D, 0x1443E, 0x1443F, 0x14440, 0x14441, 0x14442, 0x14443,
+ 0x14444, 0x14445, 0x14446, 0x14447, 0x14448, 0x14449, 0x1444A, 0x1444B,
+ 0x1444C, 0x1444D, 0x1444E, 0x1444F, 0x14450, 0x14451, 0x14452, 0x14453,
+ 0x14454, 0x14455, 0x14456, 0x14457, 0x14458, 0x14459, 0x1445A, 0x1445B,
+ 0x1445C, 0x1445D, 0x1445E, 0x1445F, 0x14460, 0x14461, 0x14462, 0x14463,
+ 0x14464, 0x14465, 0x14466, 0x14467, 0x14468, 0x14469, 0x1446A, 0x1446B,
+ 0x1446C, 0x1446D, 0x1446E, 0x1446F, 0x14470, 0x14471, 0x14472, 0x14473,
+ 0x14474, 0x14475, 0x14476, 0x14477, 0x14478, 0x14479, 0x1447A, 0x1447B,
+ 0x1447C, 0x1447D, 0x1447E, 0x1447F, 0x14480, 0x14481, 0x14482, 0x14483,
+ 0x14484, 0x14485, 0x14486, 0x14487, 0x14488, 0x14489, 0x1448A, 0x1448B,
+ 0x1448C, 0x1448D, 0x1448E, 0x1448F, 0x14490, 0x14491, 0x14492, 0x14493,
+ 0x14494, 0x14495, 0x14496, 0x14497, 0x14498, 0x14499, 0x1449A, 0x1449B,
+ 0x1449C, 0x1449D, 0x1449E, 0x1449F, 0x144A0, 0x144A1, 0x144A2, 0x144A3,
+ 0x144A4, 0x144A5, 0x144A6, 0x144A7, 0x144A8, 0x144A9, 0x144AA, 0x144AB,
+ 0x144AC, 0x144AD, 0x144AE, 0x144AF, 0x144B0, 0x144B1, 0x144B2, 0x144B3,
+ 0x144B4, 0x144B5, 0x144B6, 0x144B7, 0x144B8, 0x144B9, 0x144BA, 0x144BB,
+ 0x144BC, 0x144BD, 0x144BE, 0x144BF, 0x144C0, 0x144C1, 0x144C2, 0x144C3,
+ 0x144C4, 0x144C5, 0x144C6, 0x144C7, 0x144C8, 0x144C9, 0x144CA, 0x144CB,
+ 0x144CC, 0x144CD, 0x144CE, 0x144CF, 0x144D0, 0x144D1, 0x144D2, 0x144D3,
+ 0x144D4, 0x144D5, 0x144D6, 0x144D7, 0x144D8, 0x144D9, 0x144DA, 0x144DB,
+ 0x144DC, 0x144DD, 0x144DE, 0x144DF, 0x144E0, 0x144E1, 0x144E2, 0x144E3,
+ 0x144E4, 0x144E5, 0x144E6, 0x144E7, 0x144E8, 0x144E9, 0x144EA, 0x144EB,
+ 0x144EC, 0x144ED, 0x144EE, 0x144EF, 0x144F0, 0x144F1, 0x144F2, 0x144F3,
+ 0x144F4, 0x144F5, 0x144F6, 0x144F7, 0x144F8, 0x144F9, 0x144FA, 0x144FB,
+ 0x144FC, 0x144FD, 0x144FE, 0x144FF, 0x14500, 0x14501, 0x14502, 0x14503,
+ 0x14504, 0x14505, 0x14506, 0x14507, 0x14508, 0x14509, 0x1450A, 0x1450B,
+ 0x1450C, 0x1450D, 0x1450E, 0x1450F, 0x14510, 0x14511, 0x14512, 0x14513,
+ 0x14514, 0x14515, 0x14516, 0x14517, 0x14518, 0x14519, 0x1451A, 0x1451B,
+ 0x1451C, 0x1451D, 0x1451E, 0x1451F, 0x14520, 0x14521, 0x14522, 0x14523,
+ 0x14524, 0x14525, 0x14526, 0x14527, 0x14528, 0x14529, 0x1452A, 0x1452B,
+ 0x1452C, 0x1452D, 0x1452E, 0x1452F, 0x14530, 0x14531, 0x14532, 0x14533,
+ 0x14534, 0x14535, 0x14536, 0x14537, 0x14538, 0x14539, 0x1453A, 0x1453B,
+ 0x1453C, 0x1453D, 0x1453E, 0x1453F, 0x14540, 0x14541, 0x14542, 0x14543,
+ 0x14544, 0x14545, 0x14546, 0x14547, 0x14548, 0x14549, 0x1454A, 0x1454B,
+ 0x1454C, 0x1454D, 0x1454E, 0x1454F, 0x14550, 0x14551, 0x14552, 0x14553,
+ 0x14554, 0x14555, 0x14556, 0x14557, 0x14558, 0x14559, 0x1455A, 0x1455B,
+ 0x1455C, 0x1455D, 0x1455E, 0x1455F, 0x14560, 0x14561, 0x14562, 0x14563,
+ 0x14564, 0x14565, 0x14566, 0x14567, 0x14568, 0x14569, 0x1456A, 0x1456B,
+ 0x1456C, 0x1456D, 0x1456E, 0x1456F, 0x14570, 0x14571, 0x14572, 0x14573,
+ 0x14574, 0x14575, 0x14576, 0x14577, 0x14578, 0x14579, 0x1457A, 0x1457B,
+ 0x1457C, 0x1457D, 0x1457E, 0x1457F, 0x14580, 0x14581, 0x14582, 0x14583,
+ 0x14584, 0x14585, 0x14586, 0x14587, 0x14588, 0x14589, 0x1458A, 0x1458B,
+ 0x1458C, 0x1458D, 0x1458E, 0x1458F, 0x14590, 0x14591, 0x14592, 0x14593,
+ 0x14594, 0x14595, 0x14596, 0x14597, 0x14598, 0x14599, 0x1459A, 0x1459B,
+ 0x1459C, 0x1459D, 0x1459E, 0x1459F, 0x145A0, 0x145A1, 0x145A2, 0x145A3,
+ 0x145A4, 0x145A5, 0x145A6, 0x145A7, 0x145A8, 0x145A9, 0x145AA, 0x145AB,
+ 0x145AC, 0x145AD, 0x145AE, 0x145AF, 0x145B0, 0x145B1, 0x145B2, 0x145B3,
+ 0x145B4, 0x145B5, 0x145B6, 0x145B7, 0x145B8, 0x145B9, 0x145BA, 0x145BB,
+ 0x145BC, 0x145BD, 0x145BE, 0x145BF, 0x145C0, 0x145C1, 0x145C2, 0x145C3,
+ 0x145C4, 0x145C5, 0x145C6, 0x145C7, 0x145C8, 0x145C9, 0x145CA, 0x145CB,
+ 0x145CC, 0x145CD, 0x145CE, 0x145CF, 0x145D0, 0x145D1, 0x145D2, 0x145D3,
+ 0x145D4, 0x145D5, 0x145D6, 0x145D7, 0x145D8, 0x145D9, 0x145DA, 0x145DB,
+ 0x145DC, 0x145DD, 0x145DE, 0x145DF, 0x145E0, 0x145E1, 0x145E2, 0x145E3,
+ 0x145E4, 0x145E5, 0x145E6, 0x145E7, 0x145E8, 0x145E9, 0x145EA, 0x145EB,
+ 0x145EC, 0x145ED, 0x145EE, 0x145EF, 0x145F0, 0x145F1, 0x145F2, 0x145F3,
+ 0x145F4, 0x145F5, 0x145F6, 0x145F7, 0x145F8, 0x145F9, 0x145FA, 0x145FB,
+ 0x145FC, 0x145FD, 0x145FE, 0x145FF, 0x14600, 0x14601, 0x14602, 0x14603,
+ 0x14604, 0x14605, 0x14606, 0x14607, 0x14608, 0x14609, 0x1460A, 0x1460B,
+ 0x1460C, 0x1460D, 0x1460E, 0x1460F, 0x14610, 0x14611, 0x14612, 0x14613,
+ 0x14614, 0x14615, 0x14616, 0x14617, 0x14618, 0x14619, 0x1461A, 0x1461B,
+ 0x1461C, 0x1461D, 0x1461E, 0x1461F, 0x14620, 0x14621, 0x14622, 0x14623,
+ 0x14624, 0x14625, 0x14626, 0x14627, 0x14628, 0x14629, 0x1462A, 0x1462B,
+ 0x1462C, 0x1462D, 0x1462E, 0x1462F, 0x14630, 0x14631, 0x14632, 0x14633,
+ 0x14634, 0x14635, 0x14636, 0x14637, 0x14638, 0x14639, 0x1463A, 0x1463B,
+ 0x1463C, 0x1463D, 0x1463E, 0x1463F, 0x14640, 0x14641, 0x14642, 0x14643,
+ 0x14644, 0x14645, 0x14646, 0x16800, 0x16801, 0x16802, 0x16803, 0x16804,
+ 0x16805, 0x16806, 0x16807, 0x16808, 0x16809, 0x1680A, 0x1680B, 0x1680C,
+ 0x1680D, 0x1680E, 0x1680F, 0x16810, 0x16811, 0x16812, 0x16813, 0x16814,
+ 0x16815, 0x16816, 0x16817, 0x16818, 0x16819, 0x1681A, 0x1681B, 0x1681C,
+ 0x1681D, 0x1681E, 0x1681F, 0x16820, 0x16821, 0x16822, 0x16823, 0x16824,
+ 0x16825, 0x16826, 0x16827, 0x16828, 0x16829, 0x1682A, 0x1682B, 0x1682C,
+ 0x1682D, 0x1682E, 0x1682F, 0x16830, 0x16831, 0x16832, 0x16833, 0x16834,
+ 0x16835, 0x16836, 0x16837, 0x16838, 0x16839, 0x1683A, 0x1683B, 0x1683C,
+ 0x1683D, 0x1683E, 0x1683F, 0x16840, 0x16841, 0x16842, 0x16843, 0x16844,
+ 0x16845, 0x16846, 0x16847, 0x16848, 0x16849, 0x1684A, 0x1684B, 0x1684C,
+ 0x1684D, 0x1684E, 0x1684F, 0x16850, 0x16851, 0x16852, 0x16853, 0x16854,
+ 0x16855, 0x16856, 0x16857, 0x16858, 0x16859, 0x1685A, 0x1685B, 0x1685C,
+ 0x1685D, 0x1685E, 0x1685F, 0x16860, 0x16861, 0x16862, 0x16863, 0x16864,
+ 0x16865, 0x16866, 0x16867, 0x16868, 0x16869, 0x1686A, 0x1686B, 0x1686C,
+ 0x1686D, 0x1686E, 0x1686F, 0x16870, 0x16871, 0x16872, 0x16873, 0x16874,
+ 0x16875, 0x16876, 0x16877, 0x16878, 0x16879, 0x1687A, 0x1687B, 0x1687C,
+ 0x1687D, 0x1687E, 0x1687F, 0x16880, 0x16881, 0x16882, 0x16883, 0x16884,
+ 0x16885, 0x16886, 0x16887, 0x16888, 0x16889, 0x1688A, 0x1688B, 0x1688C,
+ 0x1688D, 0x1688E, 0x1688F, 0x16890, 0x16891, 0x16892, 0x16893, 0x16894,
+ 0x16895, 0x16896, 0x16897, 0x16898, 0x16899, 0x1689A, 0x1689B, 0x1689C,
+ 0x1689D, 0x1689E, 0x1689F, 0x168A0, 0x168A1, 0x168A2, 0x168A3, 0x168A4,
+ 0x168A5, 0x168A6, 0x168A7, 0x168A8, 0x168A9, 0x168AA, 0x168AB, 0x168AC,
+ 0x168AD, 0x168AE, 0x168AF, 0x168B0, 0x168B1, 0x168B2, 0x168B3, 0x168B4,
+ 0x168B5, 0x168B6, 0x168B7, 0x168B8, 0x168B9, 0x168BA, 0x168BB, 0x168BC,
+ 0x168BD, 0x168BE, 0x168BF, 0x168C0, 0x168C1, 0x168C2, 0x168C3, 0x168C4,
+ 0x168C5, 0x168C6, 0x168C7, 0x168C8, 0x168C9, 0x168CA, 0x168CB, 0x168CC,
+ 0x168CD, 0x168CE, 0x168CF, 0x168D0, 0x168D1, 0x168D2, 0x168D3, 0x168D4,
+ 0x168D5, 0x168D6, 0x168D7, 0x168D8, 0x168D9, 0x168DA, 0x168DB, 0x168DC,
+ 0x168DD, 0x168DE, 0x168DF, 0x168E0, 0x168E1, 0x168E2, 0x168E3, 0x168E4,
+ 0x168E5, 0x168E6, 0x168E7, 0x168E8, 0x168E9, 0x168EA, 0x168EB, 0x168EC,
+ 0x168ED, 0x168EE, 0x168EF, 0x168F0, 0x168F1, 0x168F2, 0x168F3, 0x168F4,
+ 0x168F5, 0x168F6, 0x168F7, 0x168F8, 0x168F9, 0x168FA, 0x168FB, 0x168FC,
+ 0x168FD, 0x168FE, 0x168FF, 0x16900, 0x16901, 0x16902, 0x16903, 0x16904,
+ 0x16905, 0x16906, 0x16907, 0x16908, 0x16909, 0x1690A, 0x1690B, 0x1690C,
+ 0x1690D, 0x1690E, 0x1690F, 0x16910, 0x16911, 0x16912, 0x16913, 0x16914,
+ 0x16915, 0x16916, 0x16917, 0x16918, 0x16919, 0x1691A, 0x1691B, 0x1691C,
+ 0x1691D, 0x1691E, 0x1691F, 0x16920, 0x16921, 0x16922, 0x16923, 0x16924,
+ 0x16925, 0x16926, 0x16927, 0x16928, 0x16929, 0x1692A, 0x1692B, 0x1692C,
+ 0x1692D, 0x1692E, 0x1692F, 0x16930, 0x16931, 0x16932, 0x16933, 0x16934,
+ 0x16935, 0x16936, 0x16937, 0x16938, 0x16939, 0x1693A, 0x1693B, 0x1693C,
+ 0x1693D, 0x1693E, 0x1693F, 0x16940, 0x16941, 0x16942, 0x16943, 0x16944,
+ 0x16945, 0x16946, 0x16947, 0x16948, 0x16949, 0x1694A, 0x1694B, 0x1694C,
+ 0x1694D, 0x1694E, 0x1694F, 0x16950, 0x16951, 0x16952, 0x16953, 0x16954,
+ 0x16955, 0x16956, 0x16957, 0x16958, 0x16959, 0x1695A, 0x1695B, 0x1695C,
+ 0x1695D, 0x1695E, 0x1695F, 0x16960, 0x16961, 0x16962, 0x16963, 0x16964,
+ 0x16965, 0x16966, 0x16967, 0x16968, 0x16969, 0x1696A, 0x1696B, 0x1696C,
+ 0x1696D, 0x1696E, 0x1696F, 0x16970, 0x16971, 0x16972, 0x16973, 0x16974,
+ 0x16975, 0x16976, 0x16977, 0x16978, 0x16979, 0x1697A, 0x1697B, 0x1697C,
+ 0x1697D, 0x1697E, 0x1697F, 0x16980, 0x16981, 0x16982, 0x16983, 0x16984,
+ 0x16985, 0x16986, 0x16987, 0x16988, 0x16989, 0x1698A, 0x1698B, 0x1698C,
+ 0x1698D, 0x1698E, 0x1698F, 0x16990, 0x16991, 0x16992, 0x16993, 0x16994,
+ 0x16995, 0x16996, 0x16997, 0x16998, 0x16999, 0x1699A, 0x1699B, 0x1699C,
+ 0x1699D, 0x1699E, 0x1699F, 0x169A0, 0x169A1, 0x169A2, 0x169A3, 0x169A4,
+ 0x169A5, 0x169A6, 0x169A7, 0x169A8, 0x169A9, 0x169AA, 0x169AB, 0x169AC,
+ 0x169AD, 0x169AE, 0x169AF, 0x169B0, 0x169B1, 0x169B2, 0x169B3, 0x169B4,
+ 0x169B5, 0x169B6, 0x169B7, 0x169B8, 0x169B9, 0x169BA, 0x169BB, 0x169BC,
+ 0x169BD, 0x169BE, 0x169BF, 0x169C0, 0x169C1, 0x169C2, 0x169C3, 0x169C4,
+ 0x169C5, 0x169C6, 0x169C7, 0x169C8, 0x169C9, 0x169CA, 0x169CB, 0x169CC,
+ 0x169CD, 0x169CE, 0x169CF, 0x169D0, 0x169D1, 0x169D2, 0x169D3, 0x169D4,
+ 0x169D5, 0x169D6, 0x169D7, 0x169D8, 0x169D9, 0x169DA, 0x169DB, 0x169DC,
+ 0x169DD, 0x169DE, 0x169DF, 0x169E0, 0x169E1, 0x169E2, 0x169E3, 0x169E4,
+ 0x169E5, 0x169E6, 0x169E7, 0x169E8, 0x169E9, 0x169EA, 0x169EB, 0x169EC,
+ 0x169ED, 0x169EE, 0x169EF, 0x169F0, 0x169F1, 0x169F2, 0x169F3, 0x169F4,
+ 0x169F5, 0x169F6, 0x169F7, 0x169F8, 0x169F9, 0x169FA, 0x169FB, 0x169FC,
+ 0x169FD, 0x169FE, 0x169FF, 0x16A00, 0x16A01, 0x16A02, 0x16A03, 0x16A04,
+ 0x16A05, 0x16A06, 0x16A07, 0x16A08, 0x16A09, 0x16A0A, 0x16A0B, 0x16A0C,
+ 0x16A0D, 0x16A0E, 0x16A0F, 0x16A10, 0x16A11, 0x16A12, 0x16A13, 0x16A14,
+ 0x16A15, 0x16A16, 0x16A17, 0x16A18, 0x16A19, 0x16A1A, 0x16A1B, 0x16A1C,
+ 0x16A1D, 0x16A1E, 0x16A1F, 0x16A20, 0x16A21, 0x16A22, 0x16A23, 0x16A24,
+ 0x16A25, 0x16A26, 0x16A27, 0x16A28, 0x16A29, 0x16A2A, 0x16A2B, 0x16A2C,
+ 0x16A2D, 0x16A2E, 0x16A2F, 0x16A30, 0x16A31, 0x16A32, 0x16A33, 0x16A34,
+ 0x16A35, 0x16A36, 0x16A37, 0x16A38, 0x16A40, 0x16A41, 0x16A42, 0x16A43,
+ 0x16A44, 0x16A45, 0x16A46, 0x16A47, 0x16A48, 0x16A49, 0x16A4A, 0x16A4B,
+ 0x16A4C, 0x16A4D, 0x16A4E, 0x16A4F, 0x16A50, 0x16A51, 0x16A52, 0x16A53,
+ 0x16A54, 0x16A55, 0x16A56, 0x16A57, 0x16A58, 0x16A59, 0x16A5A, 0x16A5B,
+ 0x16A5C, 0x16A5D, 0x16A5E, 0x16AD0, 0x16AD1, 0x16AD2, 0x16AD3, 0x16AD4,
+ 0x16AD5, 0x16AD6, 0x16AD7, 0x16AD8, 0x16AD9, 0x16ADA, 0x16ADB, 0x16ADC,
+ 0x16ADD, 0x16ADE, 0x16ADF, 0x16AE0, 0x16AE1, 0x16AE2, 0x16AE3, 0x16AE4,
+ 0x16AE5, 0x16AE6, 0x16AE7, 0x16AE8, 0x16AE9, 0x16AEA, 0x16AEB, 0x16AEC,
+ 0x16AED, 0x16B00, 0x16B01, 0x16B02, 0x16B03, 0x16B04, 0x16B05, 0x16B06,
+ 0x16B07, 0x16B08, 0x16B09, 0x16B0A, 0x16B0B, 0x16B0C, 0x16B0D, 0x16B0E,
+ 0x16B0F, 0x16B10, 0x16B11, 0x16B12, 0x16B13, 0x16B14, 0x16B15, 0x16B16,
+ 0x16B17, 0x16B18, 0x16B19, 0x16B1A, 0x16B1B, 0x16B1C, 0x16B1D, 0x16B1E,
+ 0x16B1F, 0x16B20, 0x16B21, 0x16B22, 0x16B23, 0x16B24, 0x16B25, 0x16B26,
+ 0x16B27, 0x16B28, 0x16B29, 0x16B2A, 0x16B2B, 0x16B2C, 0x16B2D, 0x16B2E,
+ 0x16B2F, 0x16B40, 0x16B41, 0x16B42, 0x16B43, 0x16B63, 0x16B64, 0x16B65,
+ 0x16B66, 0x16B67, 0x16B68, 0x16B69, 0x16B6A, 0x16B6B, 0x16B6C, 0x16B6D,
+ 0x16B6E, 0x16B6F, 0x16B70, 0x16B71, 0x16B72, 0x16B73, 0x16B74, 0x16B75,
+ 0x16B76, 0x16B77, 0x16B7D, 0x16B7E, 0x16B7F, 0x16B80, 0x16B81, 0x16B82,
+ 0x16B83, 0x16B84, 0x16B85, 0x16B86, 0x16B87, 0x16B88, 0x16B89, 0x16B8A,
+ 0x16B8B, 0x16B8C, 0x16B8D, 0x16B8E, 0x16B8F, 0x16F00, 0x16F01, 0x16F02,
+ 0x16F03, 0x16F04, 0x16F05, 0x16F06, 0x16F07, 0x16F08, 0x16F09, 0x16F0A,
+ 0x16F0B, 0x16F0C, 0x16F0D, 0x16F0E, 0x16F0F, 0x16F10, 0x16F11, 0x16F12,
+ 0x16F13, 0x16F14, 0x16F15, 0x16F16, 0x16F17, 0x16F18, 0x16F19, 0x16F1A,
+ 0x16F1B, 0x16F1C, 0x16F1D, 0x16F1E, 0x16F1F, 0x16F20, 0x16F21, 0x16F22,
+ 0x16F23, 0x16F24, 0x16F25, 0x16F26, 0x16F27, 0x16F28, 0x16F29, 0x16F2A,
+ 0x16F2B, 0x16F2C, 0x16F2D, 0x16F2E, 0x16F2F, 0x16F30, 0x16F31, 0x16F32,
+ 0x16F33, 0x16F34, 0x16F35, 0x16F36, 0x16F37, 0x16F38, 0x16F39, 0x16F3A,
+ 0x16F3B, 0x16F3C, 0x16F3D, 0x16F3E, 0x16F3F, 0x16F40, 0x16F41, 0x16F42,
+ 0x16F43, 0x16F44, 0x16F50, 0x16F93, 0x16F94, 0x16F95, 0x16F96, 0x16F97,
+ 0x16F98, 0x16F99, 0x16F9A, 0x16F9B, 0x16F9C, 0x16F9D, 0x16F9E, 0x16F9F,
+ 0x16FE0, 0x1BC00, 0x1BC01, 0x1BC02, 0x1BC03, 0x1BC04, 0x1BC05, 0x1BC06,
+ 0x1BC07, 0x1BC08, 0x1BC09, 0x1BC0A, 0x1BC0B, 0x1BC0C, 0x1BC0D, 0x1BC0E,
+ 0x1BC0F, 0x1BC10, 0x1BC11, 0x1BC12, 0x1BC13, 0x1BC14, 0x1BC15, 0x1BC16,
+ 0x1BC17, 0x1BC18, 0x1BC19, 0x1BC1A, 0x1BC1B, 0x1BC1C, 0x1BC1D, 0x1BC1E,
+ 0x1BC1F, 0x1BC20, 0x1BC21, 0x1BC22, 0x1BC23, 0x1BC24, 0x1BC25, 0x1BC26,
+ 0x1BC27, 0x1BC28, 0x1BC29, 0x1BC2A, 0x1BC2B, 0x1BC2C, 0x1BC2D, 0x1BC2E,
+ 0x1BC2F, 0x1BC30, 0x1BC31, 0x1BC32, 0x1BC33, 0x1BC34, 0x1BC35, 0x1BC36,
+ 0x1BC37, 0x1BC38, 0x1BC39, 0x1BC3A, 0x1BC3B, 0x1BC3C, 0x1BC3D, 0x1BC3E,
+ 0x1BC3F, 0x1BC40, 0x1BC41, 0x1BC42, 0x1BC43, 0x1BC44, 0x1BC45, 0x1BC46,
+ 0x1BC47, 0x1BC48, 0x1BC49, 0x1BC4A, 0x1BC4B, 0x1BC4C, 0x1BC4D, 0x1BC4E,
+ 0x1BC4F, 0x1BC50, 0x1BC51, 0x1BC52, 0x1BC53, 0x1BC54, 0x1BC55, 0x1BC56,
+ 0x1BC57, 0x1BC58, 0x1BC59, 0x1BC5A, 0x1BC5B, 0x1BC5C, 0x1BC5D, 0x1BC5E,
+ 0x1BC5F, 0x1BC60, 0x1BC61, 0x1BC62, 0x1BC63, 0x1BC64, 0x1BC65, 0x1BC66,
+ 0x1BC67, 0x1BC68, 0x1BC69, 0x1BC6A, 0x1BC70, 0x1BC71, 0x1BC72, 0x1BC73,
+ 0x1BC74, 0x1BC75, 0x1BC76, 0x1BC77, 0x1BC78, 0x1BC79, 0x1BC7A, 0x1BC7B,
+ 0x1BC7C, 0x1BC80, 0x1BC81, 0x1BC82, 0x1BC83, 0x1BC84, 0x1BC85, 0x1BC86,
+ 0x1BC87, 0x1BC88, 0x1BC90, 0x1BC91, 0x1BC92, 0x1BC93, 0x1BC94, 0x1BC95,
+ 0x1BC96, 0x1BC97, 0x1BC98, 0x1BC99, 0x1D400, 0x1D401, 0x1D402, 0x1D403,
+ 0x1D404, 0x1D405, 0x1D406, 0x1D407, 0x1D408, 0x1D409, 0x1D40A, 0x1D40B,
+ 0x1D40C, 0x1D40D, 0x1D40E, 0x1D40F, 0x1D410, 0x1D411, 0x1D412, 0x1D413,
+ 0x1D414, 0x1D415, 0x1D416, 0x1D417, 0x1D418, 0x1D419, 0x1D41A, 0x1D41B,
+ 0x1D41C, 0x1D41D, 0x1D41E, 0x1D41F, 0x1D420, 0x1D421, 0x1D422, 0x1D423,
+ 0x1D424, 0x1D425, 0x1D426, 0x1D427, 0x1D428, 0x1D429, 0x1D42A, 0x1D42B,
+ 0x1D42C, 0x1D42D, 0x1D42E, 0x1D42F, 0x1D430, 0x1D431, 0x1D432, 0x1D433,
+ 0x1D434, 0x1D435, 0x1D436, 0x1D437, 0x1D438, 0x1D439, 0x1D43A, 0x1D43B,
+ 0x1D43C, 0x1D43D, 0x1D43E, 0x1D43F, 0x1D440, 0x1D441, 0x1D442, 0x1D443,
+ 0x1D444, 0x1D445, 0x1D446, 0x1D447, 0x1D448, 0x1D449, 0x1D44A, 0x1D44B,
+ 0x1D44C, 0x1D44D, 0x1D44E, 0x1D44F, 0x1D450, 0x1D451, 0x1D452, 0x1D453,
+ 0x1D454, 0x1D456, 0x1D457, 0x1D458, 0x1D459, 0x1D45A, 0x1D45B, 0x1D45C,
+ 0x1D45D, 0x1D45E, 0x1D45F, 0x1D460, 0x1D461, 0x1D462, 0x1D463, 0x1D464,
+ 0x1D465, 0x1D466, 0x1D467, 0x1D468, 0x1D469, 0x1D46A, 0x1D46B, 0x1D46C,
+ 0x1D46D, 0x1D46E, 0x1D46F, 0x1D470, 0x1D471, 0x1D472, 0x1D473, 0x1D474,
+ 0x1D475, 0x1D476, 0x1D477, 0x1D478, 0x1D479, 0x1D47A, 0x1D47B, 0x1D47C,
+ 0x1D47D, 0x1D47E, 0x1D47F, 0x1D480, 0x1D481, 0x1D482, 0x1D483, 0x1D484,
+ 0x1D485, 0x1D486, 0x1D487, 0x1D488, 0x1D489, 0x1D48A, 0x1D48B, 0x1D48C,
+ 0x1D48D, 0x1D48E, 0x1D48F, 0x1D490, 0x1D491, 0x1D492, 0x1D493, 0x1D494,
+ 0x1D495, 0x1D496, 0x1D497, 0x1D498, 0x1D499, 0x1D49A, 0x1D49B, 0x1D49C,
+ 0x1D49E, 0x1D49F, 0x1D4A2, 0x1D4A5, 0x1D4A6, 0x1D4A9, 0x1D4AA, 0x1D4AB,
+ 0x1D4AC, 0x1D4AE, 0x1D4AF, 0x1D4B0, 0x1D4B1, 0x1D4B2, 0x1D4B3, 0x1D4B4,
+ 0x1D4B5, 0x1D4B6, 0x1D4B7, 0x1D4B8, 0x1D4B9, 0x1D4BB, 0x1D4BD, 0x1D4BE,
+ 0x1D4BF, 0x1D4C0, 0x1D4C1, 0x1D4C2, 0x1D4C3, 0x1D4C5, 0x1D4C6, 0x1D4C7,
+ 0x1D4C8, 0x1D4C9, 0x1D4CA, 0x1D4CB, 0x1D4CC, 0x1D4CD, 0x1D4CE, 0x1D4CF,
+ 0x1D4D0, 0x1D4D1, 0x1D4D2, 0x1D4D3, 0x1D4D4, 0x1D4D5, 0x1D4D6, 0x1D4D7,
+ 0x1D4D8, 0x1D4D9, 0x1D4DA, 0x1D4DB, 0x1D4DC, 0x1D4DD, 0x1D4DE, 0x1D4DF,
+ 0x1D4E0, 0x1D4E1, 0x1D4E2, 0x1D4E3, 0x1D4E4, 0x1D4E5, 0x1D4E6, 0x1D4E7,
+ 0x1D4E8, 0x1D4E9, 0x1D4EA, 0x1D4EB, 0x1D4EC, 0x1D4ED, 0x1D4EE, 0x1D4EF,
+ 0x1D4F0, 0x1D4F1, 0x1D4F2, 0x1D4F3, 0x1D4F4, 0x1D4F5, 0x1D4F6, 0x1D4F7,
+ 0x1D4F8, 0x1D4F9, 0x1D4FA, 0x1D4FB, 0x1D4FC, 0x1D4FD, 0x1D4FE, 0x1D4FF,
+ 0x1D500, 0x1D501, 0x1D502, 0x1D503, 0x1D504, 0x1D505, 0x1D507, 0x1D508,
+ 0x1D509, 0x1D50A, 0x1D50D, 0x1D50E, 0x1D50F, 0x1D510, 0x1D511, 0x1D512,
+ 0x1D513, 0x1D514, 0x1D516, 0x1D517, 0x1D518, 0x1D519, 0x1D51A, 0x1D51B,
+ 0x1D51C, 0x1D51E, 0x1D51F, 0x1D520, 0x1D521, 0x1D522, 0x1D523, 0x1D524,
+ 0x1D525, 0x1D526, 0x1D527, 0x1D528, 0x1D529, 0x1D52A, 0x1D52B, 0x1D52C,
+ 0x1D52D, 0x1D52E, 0x1D52F, 0x1D530, 0x1D531, 0x1D532, 0x1D533, 0x1D534,
+ 0x1D535, 0x1D536, 0x1D537, 0x1D538, 0x1D539, 0x1D53B, 0x1D53C, 0x1D53D,
+ 0x1D53E, 0x1D540, 0x1D541, 0x1D542, 0x1D543, 0x1D544, 0x1D546, 0x1D54A,
+ 0x1D54B, 0x1D54C, 0x1D54D, 0x1D54E, 0x1D54F, 0x1D550, 0x1D552, 0x1D553,
+ 0x1D554, 0x1D555, 0x1D556, 0x1D557, 0x1D558, 0x1D559, 0x1D55A, 0x1D55B,
+ 0x1D55C, 0x1D55D, 0x1D55E, 0x1D55F, 0x1D560, 0x1D561, 0x1D562, 0x1D563,
+ 0x1D564, 0x1D565, 0x1D566, 0x1D567, 0x1D568, 0x1D569, 0x1D56A, 0x1D56B,
+ 0x1D56C, 0x1D56D, 0x1D56E, 0x1D56F, 0x1D570, 0x1D571, 0x1D572, 0x1D573,
+ 0x1D574, 0x1D575, 0x1D576, 0x1D577, 0x1D578, 0x1D579, 0x1D57A, 0x1D57B,
+ 0x1D57C, 0x1D57D, 0x1D57E, 0x1D57F, 0x1D580, 0x1D581, 0x1D582, 0x1D583,
+ 0x1D584, 0x1D585, 0x1D586, 0x1D587, 0x1D588, 0x1D589, 0x1D58A, 0x1D58B,
+ 0x1D58C, 0x1D58D, 0x1D58E, 0x1D58F, 0x1D590, 0x1D591, 0x1D592, 0x1D593,
+ 0x1D594, 0x1D595, 0x1D596, 0x1D597, 0x1D598, 0x1D599, 0x1D59A, 0x1D59B,
+ 0x1D59C, 0x1D59D, 0x1D59E, 0x1D59F, 0x1D5A0, 0x1D5A1, 0x1D5A2, 0x1D5A3,
+ 0x1D5A4, 0x1D5A5, 0x1D5A6, 0x1D5A7, 0x1D5A8, 0x1D5A9, 0x1D5AA, 0x1D5AB,
+ 0x1D5AC, 0x1D5AD, 0x1D5AE, 0x1D5AF, 0x1D5B0, 0x1D5B1, 0x1D5B2, 0x1D5B3,
+ 0x1D5B4, 0x1D5B5, 0x1D5B6, 0x1D5B7, 0x1D5B8, 0x1D5B9, 0x1D5BA, 0x1D5BB,
+ 0x1D5BC, 0x1D5BD, 0x1D5BE, 0x1D5BF, 0x1D5C0, 0x1D5C1, 0x1D5C2, 0x1D5C3,
+ 0x1D5C4, 0x1D5C5, 0x1D5C6, 0x1D5C7, 0x1D5C8, 0x1D5C9, 0x1D5CA, 0x1D5CB,
+ 0x1D5CC, 0x1D5CD, 0x1D5CE, 0x1D5CF, 0x1D5D0, 0x1D5D1, 0x1D5D2, 0x1D5D3,
+ 0x1D5D4, 0x1D5D5, 0x1D5D6, 0x1D5D7, 0x1D5D8, 0x1D5D9, 0x1D5DA, 0x1D5DB,
+ 0x1D5DC, 0x1D5DD, 0x1D5DE, 0x1D5DF, 0x1D5E0, 0x1D5E1, 0x1D5E2, 0x1D5E3,
+ 0x1D5E4, 0x1D5E5, 0x1D5E6, 0x1D5E7, 0x1D5E8, 0x1D5E9, 0x1D5EA, 0x1D5EB,
+ 0x1D5EC, 0x1D5ED, 0x1D5EE, 0x1D5EF, 0x1D5F0, 0x1D5F1, 0x1D5F2, 0x1D5F3,
+ 0x1D5F4, 0x1D5F5, 0x1D5F6, 0x1D5F7, 0x1D5F8, 0x1D5F9, 0x1D5FA, 0x1D5FB,
+ 0x1D5FC, 0x1D5FD, 0x1D5FE, 0x1D5FF, 0x1D600, 0x1D601, 0x1D602, 0x1D603,
+ 0x1D604, 0x1D605, 0x1D606, 0x1D607, 0x1D608, 0x1D609, 0x1D60A, 0x1D60B,
+ 0x1D60C, 0x1D60D, 0x1D60E, 0x1D60F, 0x1D610, 0x1D611, 0x1D612, 0x1D613,
+ 0x1D614, 0x1D615, 0x1D616, 0x1D617, 0x1D618, 0x1D619, 0x1D61A, 0x1D61B,
+ 0x1D61C, 0x1D61D, 0x1D61E, 0x1D61F, 0x1D620, 0x1D621, 0x1D622, 0x1D623,
+ 0x1D624, 0x1D625, 0x1D626, 0x1D627, 0x1D628, 0x1D629, 0x1D62A, 0x1D62B,
+ 0x1D62C, 0x1D62D, 0x1D62E, 0x1D62F, 0x1D630, 0x1D631, 0x1D632, 0x1D633,
+ 0x1D634, 0x1D635, 0x1D636, 0x1D637, 0x1D638, 0x1D639, 0x1D63A, 0x1D63B,
+ 0x1D63C, 0x1D63D, 0x1D63E, 0x1D63F, 0x1D640, 0x1D641, 0x1D642, 0x1D643,
+ 0x1D644, 0x1D645, 0x1D646, 0x1D647, 0x1D648, 0x1D649, 0x1D64A, 0x1D64B,
+ 0x1D64C, 0x1D64D, 0x1D64E, 0x1D64F, 0x1D650, 0x1D651, 0x1D652, 0x1D653,
+ 0x1D654, 0x1D655, 0x1D656, 0x1D657, 0x1D658, 0x1D659, 0x1D65A, 0x1D65B,
+ 0x1D65C, 0x1D65D, 0x1D65E, 0x1D65F, 0x1D660, 0x1D661, 0x1D662, 0x1D663,
+ 0x1D664, 0x1D665, 0x1D666, 0x1D667, 0x1D668, 0x1D669, 0x1D66A, 0x1D66B,
+ 0x1D66C, 0x1D66D, 0x1D66E, 0x1D66F, 0x1D670, 0x1D671, 0x1D672, 0x1D673,
+ 0x1D674, 0x1D675, 0x1D676, 0x1D677, 0x1D678, 0x1D679, 0x1D67A, 0x1D67B,
+ 0x1D67C, 0x1D67D, 0x1D67E, 0x1D67F, 0x1D680, 0x1D681, 0x1D682, 0x1D683,
+ 0x1D684, 0x1D685, 0x1D686, 0x1D687, 0x1D688, 0x1D689, 0x1D68A, 0x1D68B,
+ 0x1D68C, 0x1D68D, 0x1D68E, 0x1D68F, 0x1D690, 0x1D691, 0x1D692, 0x1D693,
+ 0x1D694, 0x1D695, 0x1D696, 0x1D697, 0x1D698, 0x1D699, 0x1D69A, 0x1D69B,
+ 0x1D69C, 0x1D69D, 0x1D69E, 0x1D69F, 0x1D6A0, 0x1D6A1, 0x1D6A2, 0x1D6A3,
+ 0x1D6A4, 0x1D6A5, 0x1D6A8, 0x1D6A9, 0x1D6AA, 0x1D6AB, 0x1D6AC, 0x1D6AD,
+ 0x1D6AE, 0x1D6AF, 0x1D6B0, 0x1D6B1, 0x1D6B2, 0x1D6B3, 0x1D6B4, 0x1D6B5,
+ 0x1D6B6, 0x1D6B7, 0x1D6B8, 0x1D6B9, 0x1D6BA, 0x1D6BB, 0x1D6BC, 0x1D6BD,
+ 0x1D6BE, 0x1D6BF, 0x1D6C0, 0x1D6C2, 0x1D6C3, 0x1D6C4, 0x1D6C5, 0x1D6C6,
+ 0x1D6C7, 0x1D6C8, 0x1D6C9, 0x1D6CA, 0x1D6CB, 0x1D6CC, 0x1D6CD, 0x1D6CE,
+ 0x1D6CF, 0x1D6D0, 0x1D6D1, 0x1D6D2, 0x1D6D3, 0x1D6D4, 0x1D6D5, 0x1D6D6,
+ 0x1D6D7, 0x1D6D8, 0x1D6D9, 0x1D6DA, 0x1D6DC, 0x1D6DD, 0x1D6DE, 0x1D6DF,
+ 0x1D6E0, 0x1D6E1, 0x1D6E2, 0x1D6E3, 0x1D6E4, 0x1D6E5, 0x1D6E6, 0x1D6E7,
+ 0x1D6E8, 0x1D6E9, 0x1D6EA, 0x1D6EB, 0x1D6EC, 0x1D6ED, 0x1D6EE, 0x1D6EF,
+ 0x1D6F0, 0x1D6F1, 0x1D6F2, 0x1D6F3, 0x1D6F4, 0x1D6F5, 0x1D6F6, 0x1D6F7,
+ 0x1D6F8, 0x1D6F9, 0x1D6FA, 0x1D6FC, 0x1D6FD, 0x1D6FE, 0x1D6FF, 0x1D700,
+ 0x1D701, 0x1D702, 0x1D703, 0x1D704, 0x1D705, 0x1D706, 0x1D707, 0x1D708,
+ 0x1D709, 0x1D70A, 0x1D70B, 0x1D70C, 0x1D70D, 0x1D70E, 0x1D70F, 0x1D710,
+ 0x1D711, 0x1D712, 0x1D713, 0x1D714, 0x1D716, 0x1D717, 0x1D718, 0x1D719,
+ 0x1D71A, 0x1D71B, 0x1D71C, 0x1D71D, 0x1D71E, 0x1D71F, 0x1D720, 0x1D721,
+ 0x1D722, 0x1D723, 0x1D724, 0x1D725, 0x1D726, 0x1D727, 0x1D728, 0x1D729,
+ 0x1D72A, 0x1D72B, 0x1D72C, 0x1D72D, 0x1D72E, 0x1D72F, 0x1D730, 0x1D731,
+ 0x1D732, 0x1D733, 0x1D734, 0x1D736, 0x1D737, 0x1D738, 0x1D739, 0x1D73A,
+ 0x1D73B, 0x1D73C, 0x1D73D, 0x1D73E, 0x1D73F, 0x1D740, 0x1D741, 0x1D742,
+ 0x1D743, 0x1D744, 0x1D745, 0x1D746, 0x1D747, 0x1D748, 0x1D749, 0x1D74A,
+ 0x1D74B, 0x1D74C, 0x1D74D, 0x1D74E, 0x1D750, 0x1D751, 0x1D752, 0x1D753,
+ 0x1D754, 0x1D755, 0x1D756, 0x1D757, 0x1D758, 0x1D759, 0x1D75A, 0x1D75B,
+ 0x1D75C, 0x1D75D, 0x1D75E, 0x1D75F, 0x1D760, 0x1D761, 0x1D762, 0x1D763,
+ 0x1D764, 0x1D765, 0x1D766, 0x1D767, 0x1D768, 0x1D769, 0x1D76A, 0x1D76B,
+ 0x1D76C, 0x1D76D, 0x1D76E, 0x1D770, 0x1D771, 0x1D772, 0x1D773, 0x1D774,
+ 0x1D775, 0x1D776, 0x1D777, 0x1D778, 0x1D779, 0x1D77A, 0x1D77B, 0x1D77C,
+ 0x1D77D, 0x1D77E, 0x1D77F, 0x1D780, 0x1D781, 0x1D782, 0x1D783, 0x1D784,
+ 0x1D785, 0x1D786, 0x1D787, 0x1D788, 0x1D78A, 0x1D78B, 0x1D78C, 0x1D78D,
+ 0x1D78E, 0x1D78F, 0x1D790, 0x1D791, 0x1D792, 0x1D793, 0x1D794, 0x1D795,
+ 0x1D796, 0x1D797, 0x1D798, 0x1D799, 0x1D79A, 0x1D79B, 0x1D79C, 0x1D79D,
+ 0x1D79E, 0x1D79F, 0x1D7A0, 0x1D7A1, 0x1D7A2, 0x1D7A3, 0x1D7A4, 0x1D7A5,
+ 0x1D7A6, 0x1D7A7, 0x1D7A8, 0x1D7AA, 0x1D7AB, 0x1D7AC, 0x1D7AD, 0x1D7AE,
+ 0x1D7AF, 0x1D7B0, 0x1D7B1, 0x1D7B2, 0x1D7B3, 0x1D7B4, 0x1D7B5, 0x1D7B6,
+ 0x1D7B7, 0x1D7B8, 0x1D7B9, 0x1D7BA, 0x1D7BB, 0x1D7BC, 0x1D7BD, 0x1D7BE,
+ 0x1D7BF, 0x1D7C0, 0x1D7C1, 0x1D7C2, 0x1D7C4, 0x1D7C5, 0x1D7C6, 0x1D7C7,
+ 0x1D7C8, 0x1D7C9, 0x1D7CA, 0x1D7CB, 0x1E800, 0x1E801, 0x1E802, 0x1E803,
+ 0x1E804, 0x1E805, 0x1E806, 0x1E807, 0x1E808, 0x1E809, 0x1E80A, 0x1E80B,
+ 0x1E80C, 0x1E80D, 0x1E80E, 0x1E80F, 0x1E810, 0x1E811, 0x1E812, 0x1E813,
+ 0x1E814, 0x1E815, 0x1E816, 0x1E817, 0x1E818, 0x1E819, 0x1E81A, 0x1E81B,
+ 0x1E81C, 0x1E81D, 0x1E81E, 0x1E81F, 0x1E820, 0x1E821, 0x1E822, 0x1E823,
+ 0x1E824, 0x1E825, 0x1E826, 0x1E827, 0x1E828, 0x1E829, 0x1E82A, 0x1E82B,
+ 0x1E82C, 0x1E82D, 0x1E82E, 0x1E82F, 0x1E830, 0x1E831, 0x1E832, 0x1E833,
+ 0x1E834, 0x1E835, 0x1E836, 0x1E837, 0x1E838, 0x1E839, 0x1E83A, 0x1E83B,
+ 0x1E83C, 0x1E83D, 0x1E83E, 0x1E83F, 0x1E840, 0x1E841, 0x1E842, 0x1E843,
+ 0x1E844, 0x1E845, 0x1E846, 0x1E847, 0x1E848, 0x1E849, 0x1E84A, 0x1E84B,
+ 0x1E84C, 0x1E84D, 0x1E84E, 0x1E84F, 0x1E850, 0x1E851, 0x1E852, 0x1E853,
+ 0x1E854, 0x1E855, 0x1E856, 0x1E857, 0x1E858, 0x1E859, 0x1E85A, 0x1E85B,
+ 0x1E85C, 0x1E85D, 0x1E85E, 0x1E85F, 0x1E860, 0x1E861, 0x1E862, 0x1E863,
+ 0x1E864, 0x1E865, 0x1E866, 0x1E867, 0x1E868, 0x1E869, 0x1E86A, 0x1E86B,
+ 0x1E86C, 0x1E86D, 0x1E86E, 0x1E86F, 0x1E870, 0x1E871, 0x1E872, 0x1E873,
+ 0x1E874, 0x1E875, 0x1E876, 0x1E877, 0x1E878, 0x1E879, 0x1E87A, 0x1E87B,
+ 0x1E87C, 0x1E87D, 0x1E87E, 0x1E87F, 0x1E880, 0x1E881, 0x1E882, 0x1E883,
+ 0x1E884, 0x1E885, 0x1E886, 0x1E887, 0x1E888, 0x1E889, 0x1E88A, 0x1E88B,
+ 0x1E88C, 0x1E88D, 0x1E88E, 0x1E88F, 0x1E890, 0x1E891, 0x1E892, 0x1E893,
+ 0x1E894, 0x1E895, 0x1E896, 0x1E897, 0x1E898, 0x1E899, 0x1E89A, 0x1E89B,
+ 0x1E89C, 0x1E89D, 0x1E89E, 0x1E89F, 0x1E8A0, 0x1E8A1, 0x1E8A2, 0x1E8A3,
+ 0x1E8A4, 0x1E8A5, 0x1E8A6, 0x1E8A7, 0x1E8A8, 0x1E8A9, 0x1E8AA, 0x1E8AB,
+ 0x1E8AC, 0x1E8AD, 0x1E8AE, 0x1E8AF, 0x1E8B0, 0x1E8B1, 0x1E8B2, 0x1E8B3,
+ 0x1E8B4, 0x1E8B5, 0x1E8B6, 0x1E8B7, 0x1E8B8, 0x1E8B9, 0x1E8BA, 0x1E8BB,
+ 0x1E8BC, 0x1E8BD, 0x1E8BE, 0x1E8BF, 0x1E8C0, 0x1E8C1, 0x1E8C2, 0x1E8C3,
+ 0x1E8C4, 0x1E900, 0x1E901, 0x1E902, 0x1E903, 0x1E904, 0x1E905, 0x1E906,
+ 0x1E907, 0x1E908, 0x1E909, 0x1E90A, 0x1E90B, 0x1E90C, 0x1E90D, 0x1E90E,
+ 0x1E90F, 0x1E910, 0x1E911, 0x1E912, 0x1E913, 0x1E914, 0x1E915, 0x1E916,
+ 0x1E917, 0x1E918, 0x1E919, 0x1E91A, 0x1E91B, 0x1E91C, 0x1E91D, 0x1E91E,
+ 0x1E91F, 0x1E920, 0x1E921, 0x1E922, 0x1E923, 0x1E924, 0x1E925, 0x1E926,
+ 0x1E927, 0x1E928, 0x1E929, 0x1E92A, 0x1E92B, 0x1E92C, 0x1E92D, 0x1E92E,
+ 0x1E92F, 0x1E930, 0x1E931, 0x1E932, 0x1E933, 0x1E934, 0x1E935, 0x1E936,
+ 0x1E937, 0x1E938, 0x1E939, 0x1E93A, 0x1E93B, 0x1E93C, 0x1E93D, 0x1E93E,
+ 0x1E93F, 0x1E940, 0x1E941, 0x1E942, 0x1E943, 0x1EE00, 0x1EE01, 0x1EE02,
+ 0x1EE03, 0x1EE05, 0x1EE06, 0x1EE07, 0x1EE08, 0x1EE09, 0x1EE0A, 0x1EE0B,
+ 0x1EE0C, 0x1EE0D, 0x1EE0E, 0x1EE0F, 0x1EE10, 0x1EE11, 0x1EE12, 0x1EE13,
+ 0x1EE14, 0x1EE15, 0x1EE16, 0x1EE17, 0x1EE18, 0x1EE19, 0x1EE1A, 0x1EE1B,
+ 0x1EE1C, 0x1EE1D, 0x1EE1E, 0x1EE1F, 0x1EE21, 0x1EE22, 0x1EE24, 0x1EE27,
+ 0x1EE29, 0x1EE2A, 0x1EE2B, 0x1EE2C, 0x1EE2D, 0x1EE2E, 0x1EE2F, 0x1EE30,
+ 0x1EE31, 0x1EE32, 0x1EE34, 0x1EE35, 0x1EE36, 0x1EE37, 0x1EE39, 0x1EE3B,
+ 0x1EE42, 0x1EE47, 0x1EE49, 0x1EE4B, 0x1EE4D, 0x1EE4E, 0x1EE4F, 0x1EE51,
+ 0x1EE52, 0x1EE54, 0x1EE57, 0x1EE59, 0x1EE5B, 0x1EE5D, 0x1EE5F, 0x1EE61,
+ 0x1EE62, 0x1EE64, 0x1EE67, 0x1EE68, 0x1EE69, 0x1EE6A, 0x1EE6C, 0x1EE6D,
+ 0x1EE6E, 0x1EE6F, 0x1EE70, 0x1EE71, 0x1EE72, 0x1EE74, 0x1EE75, 0x1EE76,
+ 0x1EE77, 0x1EE79, 0x1EE7A, 0x1EE7B, 0x1EE7C, 0x1EE7E, 0x1EE80, 0x1EE81,
+ 0x1EE82, 0x1EE83, 0x1EE84, 0x1EE85, 0x1EE86, 0x1EE87, 0x1EE88, 0x1EE89,
+ 0x1EE8B, 0x1EE8C, 0x1EE8D, 0x1EE8E, 0x1EE8F, 0x1EE90, 0x1EE91, 0x1EE92,
+ 0x1EE93, 0x1EE94, 0x1EE95, 0x1EE96, 0x1EE97, 0x1EE98, 0x1EE99, 0x1EE9A,
+ 0x1EE9B, 0x1EEA1, 0x1EEA2, 0x1EEA3, 0x1EEA5, 0x1EEA6, 0x1EEA7, 0x1EEA8,
+ 0x1EEA9, 0x1EEAB, 0x1EEAC, 0x1EEAD, 0x1EEAE, 0x1EEAF, 0x1EEB0, 0x1EEB1,
+ 0x1EEB2, 0x1EEB3, 0x1EEB4, 0x1EEB5, 0x1EEB6, 0x1EEB7, 0x1EEB8, 0x1EEB9,
+ 0x1EEBA, 0x1EEBB, 0x1F130, 0x1F131, 0x1F132, 0x1F133, 0x1F134, 0x1F135,
+ 0x1F136, 0x1F137, 0x1F138, 0x1F139, 0x1F13A, 0x1F13B, 0x1F13C, 0x1F13D,
+ 0x1F13E, 0x1F13F, 0x1F140, 0x1F141, 0x1F142, 0x1F143, 0x1F144, 0x1F145,
+ 0x1F146, 0x1F147, 0x1F148, 0x1F149, 0x1F150, 0x1F151, 0x1F152, 0x1F153,
+ 0x1F154, 0x1F155, 0x1F156, 0x1F157, 0x1F158, 0x1F159, 0x1F15A, 0x1F15B,
+ 0x1F15C, 0x1F15D, 0x1F15E, 0x1F15F, 0x1F160, 0x1F161, 0x1F162, 0x1F163,
+ 0x1F164, 0x1F165, 0x1F166, 0x1F167, 0x1F168, 0x1F169, 0x1F170, 0x1F171,
+ 0x1F172, 0x1F173, 0x1F174, 0x1F175, 0x1F176, 0x1F177, 0x1F178, 0x1F179,
+ 0x1F17A, 0x1F17B, 0x1F17C, 0x1F17D, 0x1F17E, 0x1F17F, 0x1F180, 0x1F181,
+ 0x1F182, 0x1F183, 0x1F184, 0x1F185, 0x1F186, 0x1F187, 0x1F188, 0x1F189
+};
+static const uint32_t Single_Quote[]= {
+ 0x00027
+};
+static const uint32_t Double_Quote[]= {
+ 0x00022
+};
+static const uint32_t MidNumLet[]= {
+ 0x0002E, 0x02018, 0x02019, 0x02024, 0x0FE52, 0x0FF07, 0x0FF0E
+};
+static const uint32_t MidLetter[]= {
+ 0x0003A, 0x000B7, 0x002D7, 0x00387, 0x005F4, 0x02027, 0x0FE13, 0x0FE55,
+ 0x0FF1A
+};
+static const uint32_t MidNum[]= {
+ 0x0002C, 0x0003B, 0x0037E, 0x00589, 0x0060C, 0x0060D, 0x0066C, 0x007F8,
+ 0x02044, 0x0FE10, 0x0FE14, 0x0FE50, 0x0FE54, 0x0FF0C, 0x0FF1B
+};
+static const uint32_t Numeric[]= {
+ 0x00030, 0x00031, 0x00032, 0x00033, 0x00034, 0x00035, 0x00036, 0x00037,
+ 0x00038, 0x00039, 0x00660, 0x00661, 0x00662, 0x00663, 0x00664, 0x00665,
+ 0x00666, 0x00667, 0x00668, 0x00669, 0x0066B, 0x006F0, 0x006F1, 0x006F2,
+ 0x006F3, 0x006F4, 0x006F5, 0x006F6, 0x006F7, 0x006F8, 0x006F9, 0x007C0,
+ 0x007C1, 0x007C2, 0x007C3, 0x007C4, 0x007C5, 0x007C6, 0x007C7, 0x007C8,
+ 0x007C9, 0x00966, 0x00967, 0x00968, 0x00969, 0x0096A, 0x0096B, 0x0096C,
+ 0x0096D, 0x0096E, 0x0096F, 0x009E6, 0x009E7, 0x009E8, 0x009E9, 0x009EA,
+ 0x009EB, 0x009EC, 0x009ED, 0x009EE, 0x009EF, 0x00A66, 0x00A67, 0x00A68,
+ 0x00A69, 0x00A6A, 0x00A6B, 0x00A6C, 0x00A6D, 0x00A6E, 0x00A6F, 0x00AE6,
+ 0x00AE7, 0x00AE8, 0x00AE9, 0x00AEA, 0x00AEB, 0x00AEC, 0x00AED, 0x00AEE,
+ 0x00AEF, 0x00B66, 0x00B67, 0x00B68, 0x00B69, 0x00B6A, 0x00B6B, 0x00B6C,
+ 0x00B6D, 0x00B6E, 0x00B6F, 0x00BE6, 0x00BE7, 0x00BE8, 0x00BE9, 0x00BEA,
+ 0x00BEB, 0x00BEC, 0x00BED, 0x00BEE, 0x00BEF, 0x00C66, 0x00C67, 0x00C68,
+ 0x00C69, 0x00C6A, 0x00C6B, 0x00C6C, 0x00C6D, 0x00C6E, 0x00C6F, 0x00CE6,
+ 0x00CE7, 0x00CE8, 0x00CE9, 0x00CEA, 0x00CEB, 0x00CEC, 0x00CED, 0x00CEE,
+ 0x00CEF, 0x00D66, 0x00D67, 0x00D68, 0x00D69, 0x00D6A, 0x00D6B, 0x00D6C,
+ 0x00D6D, 0x00D6E, 0x00D6F, 0x00DE6, 0x00DE7, 0x00DE8, 0x00DE9, 0x00DEA,
+ 0x00DEB, 0x00DEC, 0x00DED, 0x00DEE, 0x00DEF, 0x00E50, 0x00E51, 0x00E52,
+ 0x00E53, 0x00E54, 0x00E55, 0x00E56, 0x00E57, 0x00E58, 0x00E59, 0x00ED0,
+ 0x00ED1, 0x00ED2, 0x00ED3, 0x00ED4, 0x00ED5, 0x00ED6, 0x00ED7, 0x00ED8,
+ 0x00ED9, 0x00F20, 0x00F21, 0x00F22, 0x00F23, 0x00F24, 0x00F25, 0x00F26,
+ 0x00F27, 0x00F28, 0x00F29, 0x01040, 0x01041, 0x01042, 0x01043, 0x01044,
+ 0x01045, 0x01046, 0x01047, 0x01048, 0x01049, 0x01090, 0x01091, 0x01092,
+ 0x01093, 0x01094, 0x01095, 0x01096, 0x01097, 0x01098, 0x01099, 0x017E0,
+ 0x017E1, 0x017E2, 0x017E3, 0x017E4, 0x017E5, 0x017E6, 0x017E7, 0x017E8,
+ 0x017E9, 0x01810, 0x01811, 0x01812, 0x01813, 0x01814, 0x01815, 0x01816,
+ 0x01817, 0x01818, 0x01819, 0x01946, 0x01947, 0x01948, 0x01949, 0x0194A,
+ 0x0194B, 0x0194C, 0x0194D, 0x0194E, 0x0194F, 0x019D0, 0x019D1, 0x019D2,
+ 0x019D3, 0x019D4, 0x019D5, 0x019D6, 0x019D7, 0x019D8, 0x019D9, 0x01A80,
+ 0x01A81, 0x01A82, 0x01A83, 0x01A84, 0x01A85, 0x01A86, 0x01A87, 0x01A88,
+ 0x01A89, 0x01A90, 0x01A91, 0x01A92, 0x01A93, 0x01A94, 0x01A95, 0x01A96,
+ 0x01A97, 0x01A98, 0x01A99, 0x01B50, 0x01B51, 0x01B52, 0x01B53, 0x01B54,
+ 0x01B55, 0x01B56, 0x01B57, 0x01B58, 0x01B59, 0x01BB0, 0x01BB1, 0x01BB2,
+ 0x01BB3, 0x01BB4, 0x01BB5, 0x01BB6, 0x01BB7, 0x01BB8, 0x01BB9, 0x01C40,
+ 0x01C41, 0x01C42, 0x01C43, 0x01C44, 0x01C45, 0x01C46, 0x01C47, 0x01C48,
+ 0x01C49, 0x01C50, 0x01C51, 0x01C52, 0x01C53, 0x01C54, 0x01C55, 0x01C56,
+ 0x01C57, 0x01C58, 0x01C59, 0x0A620, 0x0A621, 0x0A622, 0x0A623, 0x0A624,
+ 0x0A625, 0x0A626, 0x0A627, 0x0A628, 0x0A629, 0x0A8D0, 0x0A8D1, 0x0A8D2,
+ 0x0A8D3, 0x0A8D4, 0x0A8D5, 0x0A8D6, 0x0A8D7, 0x0A8D8, 0x0A8D9, 0x0A900,
+ 0x0A901, 0x0A902, 0x0A903, 0x0A904, 0x0A905, 0x0A906, 0x0A907, 0x0A908,
+ 0x0A909, 0x0A9D0, 0x0A9D1, 0x0A9D2, 0x0A9D3, 0x0A9D4, 0x0A9D5, 0x0A9D6,
+ 0x0A9D7, 0x0A9D8, 0x0A9D9, 0x0A9F0, 0x0A9F1, 0x0A9F2, 0x0A9F3, 0x0A9F4,
+ 0x0A9F5, 0x0A9F6, 0x0A9F7, 0x0A9F8, 0x0A9F9, 0x0AA50, 0x0AA51, 0x0AA52,
+ 0x0AA53, 0x0AA54, 0x0AA55, 0x0AA56, 0x0AA57, 0x0AA58, 0x0AA59, 0x0ABF0,
+ 0x0ABF1, 0x0ABF2, 0x0ABF3, 0x0ABF4, 0x0ABF5, 0x0ABF6, 0x0ABF7, 0x0ABF8,
+ 0x0ABF9, 0x104A0, 0x104A1, 0x104A2, 0x104A3, 0x104A4, 0x104A5, 0x104A6,
+ 0x104A7, 0x104A8, 0x104A9, 0x11066, 0x11067, 0x11068, 0x11069, 0x1106A,
+ 0x1106B, 0x1106C, 0x1106D, 0x1106E, 0x1106F, 0x110F0, 0x110F1, 0x110F2,
+ 0x110F3, 0x110F4, 0x110F5, 0x110F6, 0x110F7, 0x110F8, 0x110F9, 0x11136,
+ 0x11137, 0x11138, 0x11139, 0x1113A, 0x1113B, 0x1113C, 0x1113D, 0x1113E,
+ 0x1113F, 0x111D0, 0x111D1, 0x111D2, 0x111D3, 0x111D4, 0x111D5, 0x111D6,
+ 0x111D7, 0x111D8, 0x111D9, 0x112F0, 0x112F1, 0x112F2, 0x112F3, 0x112F4,
+ 0x112F5, 0x112F6, 0x112F7, 0x112F8, 0x112F9, 0x11450, 0x11451, 0x11452,
+ 0x11453, 0x11454, 0x11455, 0x11456, 0x11457, 0x11458, 0x11459, 0x114D0,
+ 0x114D1, 0x114D2, 0x114D3, 0x114D4, 0x114D5, 0x114D6, 0x114D7, 0x114D8,
+ 0x114D9, 0x11650, 0x11651, 0x11652, 0x11653, 0x11654, 0x11655, 0x11656,
+ 0x11657, 0x11658, 0x11659, 0x116C0, 0x116C1, 0x116C2, 0x116C3, 0x116C4,
+ 0x116C5, 0x116C6, 0x116C7, 0x116C8, 0x116C9, 0x11730, 0x11731, 0x11732,
+ 0x11733, 0x11734, 0x11735, 0x11736, 0x11737, 0x11738, 0x11739, 0x118E0,
+ 0x118E1, 0x118E2, 0x118E3, 0x118E4, 0x118E5, 0x118E6, 0x118E7, 0x118E8,
+ 0x118E9, 0x11C50, 0x11C51, 0x11C52, 0x11C53, 0x11C54, 0x11C55, 0x11C56,
+ 0x11C57, 0x11C58, 0x11C59, 0x16A60, 0x16A61, 0x16A62, 0x16A63, 0x16A64,
+ 0x16A65, 0x16A66, 0x16A67, 0x16A68, 0x16A69, 0x16B50, 0x16B51, 0x16B52,
+ 0x16B53, 0x16B54, 0x16B55, 0x16B56, 0x16B57, 0x16B58, 0x16B59, 0x1D7CE,
+ 0x1D7CF, 0x1D7D0, 0x1D7D1, 0x1D7D2, 0x1D7D3, 0x1D7D4, 0x1D7D5, 0x1D7D6,
+ 0x1D7D7, 0x1D7D8, 0x1D7D9, 0x1D7DA, 0x1D7DB, 0x1D7DC, 0x1D7DD, 0x1D7DE,
+ 0x1D7DF, 0x1D7E0, 0x1D7E1, 0x1D7E2, 0x1D7E3, 0x1D7E4, 0x1D7E5, 0x1D7E6,
+ 0x1D7E7, 0x1D7E8, 0x1D7E9, 0x1D7EA, 0x1D7EB, 0x1D7EC, 0x1D7ED, 0x1D7EE,
+ 0x1D7EF, 0x1D7F0, 0x1D7F1, 0x1D7F2, 0x1D7F3, 0x1D7F4, 0x1D7F5, 0x1D7F6,
+ 0x1D7F7, 0x1D7F8, 0x1D7F9, 0x1D7FA, 0x1D7FB, 0x1D7FC, 0x1D7FD, 0x1D7FE,
+ 0x1D7FF, 0x1E950, 0x1E951, 0x1E952, 0x1E953, 0x1E954, 0x1E955, 0x1E956,
+ 0x1E957, 0x1E958, 0x1E959
+};
+static const uint32_t ExtendNumLet[]= {
+ 0x0005F, 0x0202F, 0x0203F, 0x02040, 0x02054, 0x0FE33, 0x0FE34, 0x0FE4D,
+ 0x0FE4E, 0x0FE4F, 0x0FF3F
+};
diff --git a/src/lib-fts/word-break-data.c b/src/lib-fts/word-break-data.c
new file mode 100644
index 0000000..5ced837
--- /dev/null
+++ b/src/lib-fts/word-break-data.c
@@ -0,0 +1,58 @@
+/* This file is automatically generated by word-properties.pl from ./PropList.txt */
+static const uint32_t White_Space[]= {
+ 0x00009, 0x0000A, 0x0000B, 0x0000C, 0x0000D, 0x00020, 0x00085, 0x000A0,
+ 0x01680, 0x02000, 0x02001, 0x02002, 0x02003, 0x02004, 0x02005, 0x02006,
+ 0x02007, 0x02008, 0x02009, 0x0200A, 0x02028, 0x02029, 0x0202F, 0x0205F,
+ 0x03000
+};
+static const uint32_t Dash[]= {
+ 0x0002D, 0x0058A, 0x005BE, 0x01400, 0x01806, 0x02010, 0x02011, 0x02012,
+ 0x02013, 0x02014, 0x02015, 0x02053, 0x0207B, 0x0208B, 0x02212, 0x02E17,
+ 0x02E1A, 0x02E3A, 0x02E3B, 0x02E40, 0x0301C, 0x03030, 0x030A0, 0x0FE31,
+ 0x0FE32, 0x0FE58, 0x0FE63, 0x0FF0D
+};
+static const uint32_t Quotation_Mark[]= {
+ 0x00022, 0x00027, 0x000AB, 0x000BB, 0x02018, 0x02019, 0x0201A, 0x0201B,
+ 0x0201C, 0x0201D, 0x0201E, 0x0201F, 0x02039, 0x0203A, 0x02E42, 0x0300C,
+ 0x0300D, 0x0300E, 0x0300F, 0x0301D, 0x0301E, 0x0301F, 0x0FE41, 0x0FE42,
+ 0x0FE43, 0x0FE44, 0x0FF02, 0x0FF07, 0x0FF62, 0x0FF63
+};
+static const uint32_t Terminal_Punctuation[]= {
+ 0x00021, 0x0002C, 0x0002E, 0x0003A, 0x0003B, 0x0003F, 0x0037E, 0x00387,
+ 0x00589, 0x005C3, 0x0060C, 0x0061B, 0x0061F, 0x006D4, 0x00700, 0x00701,
+ 0x00702, 0x00703, 0x00704, 0x00705, 0x00706, 0x00707, 0x00708, 0x00709,
+ 0x0070A, 0x0070C, 0x007F8, 0x007F9, 0x00830, 0x00831, 0x00832, 0x00833,
+ 0x00834, 0x00835, 0x00836, 0x00837, 0x00838, 0x00839, 0x0083A, 0x0083B,
+ 0x0083C, 0x0083D, 0x0083E, 0x0085E, 0x00964, 0x00965, 0x00E5A, 0x00E5B,
+ 0x00F08, 0x00F0D, 0x00F0E, 0x00F0F, 0x00F10, 0x00F11, 0x00F12, 0x0104A,
+ 0x0104B, 0x01361, 0x01362, 0x01363, 0x01364, 0x01365, 0x01366, 0x01367,
+ 0x01368, 0x0166D, 0x0166E, 0x016EB, 0x016EC, 0x016ED, 0x01735, 0x01736,
+ 0x017D4, 0x017D5, 0x017D6, 0x017DA, 0x01802, 0x01803, 0x01804, 0x01805,
+ 0x01808, 0x01809, 0x01944, 0x01945, 0x01AA8, 0x01AA9, 0x01AAA, 0x01AAB,
+ 0x01B5A, 0x01B5B, 0x01B5D, 0x01B5E, 0x01B5F, 0x01C3B, 0x01C3C, 0x01C3D,
+ 0x01C3E, 0x01C3F, 0x01C7E, 0x01C7F, 0x0203C, 0x0203D, 0x02047, 0x02048,
+ 0x02049, 0x02E2E, 0x02E3C, 0x02E41, 0x03001, 0x03002, 0x0A4FE, 0x0A4FF,
+ 0x0A60D, 0x0A60E, 0x0A60F, 0x0A6F3, 0x0A6F4, 0x0A6F5, 0x0A6F6, 0x0A6F7,
+ 0x0A876, 0x0A877, 0x0A8CE, 0x0A8CF, 0x0A92F, 0x0A9C7, 0x0A9C8, 0x0A9C9,
+ 0x0AA5D, 0x0AA5E, 0x0AA5F, 0x0AADF, 0x0AAF0, 0x0AAF1, 0x0ABEB, 0x0FE50,
+ 0x0FE51, 0x0FE52, 0x0FE54, 0x0FE55, 0x0FE56, 0x0FE57, 0x0FF01, 0x0FF0C,
+ 0x0FF0E, 0x0FF1A, 0x0FF1B, 0x0FF1F, 0x0FF61, 0x0FF64, 0x1039F, 0x103D0,
+ 0x10857, 0x1091F, 0x10A56, 0x10A57, 0x10AF0, 0x10AF1, 0x10AF2, 0x10AF3,
+ 0x10AF4, 0x10AF5, 0x10B3A, 0x10B3B, 0x10B3C, 0x10B3D, 0x10B3E, 0x10B3F,
+ 0x10B99, 0x10B9A, 0x10B9B, 0x10B9C, 0x11047, 0x11048, 0x11049, 0x1104A,
+ 0x1104B, 0x1104C, 0x1104D, 0x110BE, 0x110BF, 0x110C0, 0x110C1, 0x11141,
+ 0x11142, 0x11143, 0x111C5, 0x111C6, 0x111CD, 0x111DE, 0x111DF, 0x11238,
+ 0x11239, 0x1123A, 0x1123B, 0x1123C, 0x112A9, 0x1144B, 0x1144C, 0x1144D,
+ 0x1145B, 0x115C2, 0x115C3, 0x115C4, 0x115C5, 0x115C9, 0x115CA, 0x115CB,
+ 0x115CC, 0x115CD, 0x115CE, 0x115CF, 0x115D0, 0x115D1, 0x115D2, 0x115D3,
+ 0x115D4, 0x115D5, 0x115D6, 0x115D7, 0x11641, 0x11642, 0x1173C, 0x1173D,
+ 0x1173E, 0x11C41, 0x11C42, 0x11C43, 0x11C71, 0x12470, 0x12471, 0x12472,
+ 0x12473, 0x12474, 0x16A6E, 0x16A6F, 0x16AF5, 0x16B37, 0x16B38, 0x16B39,
+ 0x16B44, 0x1BC9F, 0x1DA87, 0x1DA88, 0x1DA89, 0x1DA8A
+};
+static const uint32_t STerm[]= {
+};
+static const uint32_t Pattern_White_Space[]= {
+ 0x00009, 0x0000A, 0x0000B, 0x0000C, 0x0000D, 0x00020, 0x00085, 0x0200E,
+ 0x0200F, 0x02028, 0x02029
+};
diff --git a/src/lib-fts/word-properties.pl b/src/lib-fts/word-properties.pl
new file mode 100644
index 0000000..c600d14
--- /dev/null
+++ b/src/lib-fts/word-properties.pl
@@ -0,0 +1,34 @@
+#!/usr/bin/env perl
+use strict;
+use warnings;
+
+my @categories;
+my $which = shift(@ARGV);
+if ($which eq 'boundaries') {
+ @categories = qw(CR LF Newline Extend Regional_Indicator Format Katakana Hebrew_Letter ALetter
+ Single_Quote Double_Quote MidNumLet MidLetter MidNum Numeric ExtendNumLet);
+} elsif ($which eq 'breaks') {
+ @categories = qw(White_Space Dash Quotation_Mark Terminal_Punctuation STerm Pattern_White_Space);
+} else {
+ die "specify 'boundaries' or 'breaks'";
+}
+
+my $catregexp=join('|', @categories);
+my %catlists = map { $_ => []; } (@categories);
+
+while(<>) {
+ next if (m/^#/ or m/^\s*$/);
+ push(@{$catlists{$3}}, defined($2) ? (hex($1)..hex($2)) : hex($1))
+ if (m/([[:xdigit:]]+)(?:\.\.([[:xdigit:]]+))?\s+; ($catregexp) #/)
+}
+
+print "/* This file is automatically generated by word-properties.pl from $ARGV */\n";
+foreach(@categories) {
+ my $arref=$catlists{$_};
+ print "static const uint32_t ${_}[]= {\n";
+ while(scalar(@$arref)) {
+ print("\t", join(", ", map { sprintf("0x%05X", $_); } splice(@$arref, 0, 8)));
+ print(scalar(@$arref) ? ", \n" : "\n");
+ }
+ print("};\n");
+}
diff --git a/src/lib-http/Makefile.am b/src/lib-http/Makefile.am
new file mode 100644
index 0000000..081d39f
--- /dev/null
+++ b/src/lib-http/Makefile.am
@@ -0,0 +1,211 @@
+noinst_LTLIBRARIES = libhttp.la
+
+AM_CPPFLAGS = \
+ -I$(top_srcdir)/src/lib \
+ -I$(top_srcdir)/src/lib-test \
+ -I$(top_srcdir)/src/lib-dns \
+ -I$(top_srcdir)/src/lib-ssl-iostream \
+ -I$(top_srcdir)/src/lib-master \
+ -DPKG_RUNDIR=\""$(rundir)"\"
+
+libhttp_la_SOURCES = \
+ http-date.c \
+ http-url.c \
+ http-parser.c \
+ http-header.c \
+ http-header-parser.c \
+ http-transfer-chunked.c \
+ http-auth.c \
+ http-message-parser.c \
+ http-request.c \
+ http-request-parser.c \
+ http-response.c \
+ http-response-parser.c \
+ http-client-request.c \
+ http-client-connection.c \
+ http-client-peer.c \
+ http-client-queue.c \
+ http-client-host.c \
+ http-client.c \
+ http-server-ostream.c \
+ http-server-response.c \
+ http-server-request.c \
+ http-server-connection.c \
+ http-server-resource.c \
+ http-server.c
+
+headers = \
+ http-common.h \
+ http-date.h \
+ http-url.h \
+ http-parser.h \
+ http-header.h \
+ http-header-parser.h \
+ http-transfer.h \
+ http-auth.h \
+ http-message-parser.h \
+ http-request.h \
+ http-request-parser.h \
+ http-response.h \
+ http-response-parser.h \
+ http-client-private.h \
+ http-client.h \
+ http-server-private.h \
+ http-server.h
+
+pkginc_libdir=$(pkgincludedir)
+pkginc_lib_HEADERS = $(headers)
+
+test_programs = \
+ test-http-date \
+ test-http-url \
+ test-http-header-parser \
+ test-http-transfer \
+ test-http-auth \
+ test-http-response-parser \
+ test-http-request-parser \
+ test-http-payload \
+ test-http-client-errors \
+ test-http-client-request \
+ test-http-server-errors
+
+test_nocheck_programs = \
+ test-http-client \
+ test-http-server
+
+noinst_PROGRAMS = $(test_programs) $(test_nocheck_programs)
+
+test_libs = \
+ ../lib-test/libtest.la \
+ ../lib/liblib.la \
+ $(MODULE_LIBS)
+
+test_deps = \
+ $(noinst_LTLIBRARIES) \
+ ../lib-test/libtest.la \
+ ../lib/liblib.la
+
+test_http_url_SOURCES = test-http-url.c
+test_http_url_LDADD = http-url.lo http-header.lo $(test_libs)
+test_http_url_DEPENDENCIES = $(test_deps)
+
+test_http_date_SOURCES = test-http-date.c
+test_http_date_LDADD = http-date.lo $(test_libs)
+test_http_date_DEPENDENCIES = $(test_deps)
+
+test_http_header_parser_SOURCES = test-http-header-parser.c
+test_http_header_parser_LDADD = http-parser.lo http-header-parser.lo http-header.lo $(test_libs)
+test_http_header_parser_DEPENDENCIES = $(test_deps)
+
+test_http_transfer_SOURCES = test-http-transfer.c
+test_http_transfer_LDADD = \
+ http-parser.lo \
+ http-header-parser.lo \
+ http-transfer-chunked.lo \
+ http-header.lo \
+ $(test_libs)
+test_http_transfer_DEPENDENCIES = $(test_deps)
+
+test_http_auth_SOURCES = test-http-auth.c
+test_http_auth_LDADD = \
+ http-auth.lo \
+ http-parser.lo \
+ $(test_libs)
+test_http_auth_DEPENDENCIES = $(test_deps)
+
+test_http_response_parser_SOURCES = test-http-response-parser.c
+test_http_response_parser_LDADD = \
+ http-date.lo \
+ http-parser.lo \
+ http-header.lo \
+ http-header-parser.lo \
+ http-transfer-chunked.lo \
+ http-message-parser.lo \
+ http-response-parser.lo \
+ $(test_libs)
+test_http_response_parser_DEPENDENCIES = $(test_deps)
+
+test_http_request_parser_SOURCES = test-http-request-parser.c
+test_http_request_parser_LDADD = \
+ http-date.lo \
+ http-parser.lo \
+ http-url.lo \
+ http-header.lo \
+ http-header-parser.lo \
+ http-transfer-chunked.lo \
+ http-message-parser.lo \
+ http-request-parser.lo \
+ $(test_libs)
+test_http_request_parser_DEPENDENCIES = $(test_deps)
+
+test_http_libs = \
+ libhttp.la \
+ ../lib-dns/libdns.la \
+ ../lib-ssl-iostream/libssl_iostream.la \
+ ../lib-master/libmaster.la \
+ ../lib-auth/libauth.la \
+ ../lib-settings/libsettings.la \
+ $(test_libs)
+test_http_deps = \
+ libhttp.la \
+ ../lib-dns/libdns.la \
+ ../lib-ssl-iostream/libssl_iostream.la \
+ ../lib-master/libmaster.la \
+ ../lib-auth/libauth.la \
+ ../lib-settings/libsettings.la \
+ $(test_deps)
+
+test_http_libs_ssl=
+if BUILD_OPENSSL
+test_http_libs_ssl += ../lib-ssl-iostream/libssl_iostream_openssl.la
+endif
+
+test_http_payload_SOURCES = test-http-payload.c
+test_http_payload_LDFLAGS = -export-dynamic
+test_http_payload_LDADD = \
+ $(test_http_libs) \
+ $(test_http_libs_ssl)
+test_http_payload_DEPENDENCIES = \
+ $(test_http_deps)
+
+test_http_client_SOURCES = test-http-client.c
+test_http_client_LDFLAGS = -export-dynamic
+test_http_client_LDADD = \
+ $(test_http_libs) \
+ $(test_http_libs_ssl)
+
+test_http_client_DEPENDENCIES = \
+ $(test_http_deps)
+
+test_http_client_errors_SOURCES = test-http-client-errors.c
+test_http_client_errors_LDFLAGS = -export-dynamic
+test_http_client_errors_LDADD = \
+ $(test_http_libs)
+test_http_client_errors_DEPENDENCIES = \
+ $(test_http_deps)
+
+test_http_client_request_SOURCES = test-http-client-request.c
+test_http_client_request_LDFLAGS = -export-dynamic
+test_http_client_request_LDADD = \
+ $(test_http_libs)
+test_http_client_request_DEPENDENCIES = \
+ $(test_http_deps)
+
+test_http_server_SOURCES = test-http-server.c
+test_http_server_LDFLAGS = -export-dynamic
+test_http_server_LDADD = \
+ $(test_http_libs)
+test_http_server_DEPENDENCIES = \
+ $(test_http_deps)
+
+test_http_server_errors_SOURCES = test-http-server-errors.c
+test_http_server_errors_LDFLAGS = -export-dynamic
+test_http_server_errors_LDADD = \
+ $(test_http_libs)
+test_http_server_errors_DEPENDENCIES = \
+ $(test_http_deps)
+
+check-local:
+ for bin in $(test_programs); do \
+ if ! $(RUN_TEST) ./$$bin; then exit 1; fi; \
+ done
diff --git a/src/lib-http/Makefile.in b/src/lib-http/Makefile.in
new file mode 100644
index 0000000..6379099
--- /dev/null
+++ b/src/lib-http/Makefile.in
@@ -0,0 +1,1310 @@
+# Makefile.in generated by automake 1.16.1 from Makefile.am.
+# @configure_input@
+
+# Copyright (C) 1994-2018 Free Software Foundation, Inc.
+
+# This Makefile.in is free software; the Free Software Foundation
+# gives unlimited permission to copy and/or distribute it,
+# with or without modifications, as long as this notice is preserved.
+
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY, to the extent permitted by law; without
+# even the implied warranty of MERCHANTABILITY or FITNESS FOR A
+# PARTICULAR PURPOSE.
+
+@SET_MAKE@
+
+
+
+VPATH = @srcdir@
+am__is_gnu_make = { \
+ if test -z '$(MAKELEVEL)'; then \
+ false; \
+ elif test -n '$(MAKE_HOST)'; then \
+ true; \
+ elif test -n '$(MAKE_VERSION)' && test -n '$(CURDIR)'; then \
+ true; \
+ else \
+ false; \
+ fi; \
+}
+am__make_running_with_option = \
+ case $${target_option-} in \
+ ?) ;; \
+ *) echo "am__make_running_with_option: internal error: invalid" \
+ "target option '$${target_option-}' specified" >&2; \
+ exit 1;; \
+ esac; \
+ has_opt=no; \
+ sane_makeflags=$$MAKEFLAGS; \
+ if $(am__is_gnu_make); then \
+ sane_makeflags=$$MFLAGS; \
+ else \
+ case $$MAKEFLAGS in \
+ *\\[\ \ ]*) \
+ bs=\\; \
+ sane_makeflags=`printf '%s\n' "$$MAKEFLAGS" \
+ | sed "s/$$bs$$bs[$$bs $$bs ]*//g"`;; \
+ esac; \
+ fi; \
+ skip_next=no; \
+ strip_trailopt () \
+ { \
+ flg=`printf '%s\n' "$$flg" | sed "s/$$1.*$$//"`; \
+ }; \
+ for flg in $$sane_makeflags; do \
+ test $$skip_next = yes && { skip_next=no; continue; }; \
+ case $$flg in \
+ *=*|--*) continue;; \
+ -*I) strip_trailopt 'I'; skip_next=yes;; \
+ -*I?*) strip_trailopt 'I';; \
+ -*O) strip_trailopt 'O'; skip_next=yes;; \
+ -*O?*) strip_trailopt 'O';; \
+ -*l) strip_trailopt 'l'; skip_next=yes;; \
+ -*l?*) strip_trailopt 'l';; \
+ -[dEDm]) skip_next=yes;; \
+ -[JT]) skip_next=yes;; \
+ esac; \
+ case $$flg in \
+ *$$target_option*) has_opt=yes; break;; \
+ esac; \
+ done; \
+ test $$has_opt = yes
+am__make_dryrun = (target_option=n; $(am__make_running_with_option))
+am__make_keepgoing = (target_option=k; $(am__make_running_with_option))
+pkgdatadir = $(datadir)/@PACKAGE@
+pkgincludedir = $(includedir)/@PACKAGE@
+pkglibdir = $(libdir)/@PACKAGE@
+pkglibexecdir = $(libexecdir)/@PACKAGE@
+am__cd = CDPATH="$${ZSH_VERSION+.}$(PATH_SEPARATOR)" && cd
+install_sh_DATA = $(install_sh) -c -m 644
+install_sh_PROGRAM = $(install_sh) -c
+install_sh_SCRIPT = $(install_sh) -c
+INSTALL_HEADER = $(INSTALL_DATA)
+transform = $(program_transform_name)
+NORMAL_INSTALL = :
+PRE_INSTALL = :
+POST_INSTALL = :
+NORMAL_UNINSTALL = :
+PRE_UNINSTALL = :
+POST_UNINSTALL = :
+build_triplet = @build@
+host_triplet = @host@
+noinst_PROGRAMS = $(am__EXEEXT_1) $(am__EXEEXT_2)
+@BUILD_OPENSSL_TRUE@am__append_1 = ../lib-ssl-iostream/libssl_iostream_openssl.la
+subdir = src/lib-http
+ACLOCAL_M4 = $(top_srcdir)/aclocal.m4
+am__aclocal_m4_deps = $(top_srcdir)/m4/ac_checktype2.m4 \
+ $(top_srcdir)/m4/ac_typeof.m4 $(top_srcdir)/m4/arc4random.m4 \
+ $(top_srcdir)/m4/blockdev.m4 $(top_srcdir)/m4/c99_vsnprintf.m4 \
+ $(top_srcdir)/m4/clock_gettime.m4 $(top_srcdir)/m4/crypt.m4 \
+ $(top_srcdir)/m4/crypt_xpg6.m4 $(top_srcdir)/m4/dbqlk.m4 \
+ $(top_srcdir)/m4/dirent_dtype.m4 $(top_srcdir)/m4/dovecot.m4 \
+ $(top_srcdir)/m4/fd_passing.m4 $(top_srcdir)/m4/fdatasync.m4 \
+ $(top_srcdir)/m4/flexible_array_member.m4 \
+ $(top_srcdir)/m4/glibc.m4 $(top_srcdir)/m4/gmtime_max.m4 \
+ $(top_srcdir)/m4/gmtime_tm_gmtoff.m4 \
+ $(top_srcdir)/m4/ioloop.m4 $(top_srcdir)/m4/iovec.m4 \
+ $(top_srcdir)/m4/ipv6.m4 $(top_srcdir)/m4/libcap.m4 \
+ $(top_srcdir)/m4/libtool.m4 $(top_srcdir)/m4/libwrap.m4 \
+ $(top_srcdir)/m4/linux_mremap.m4 $(top_srcdir)/m4/ltoptions.m4 \
+ $(top_srcdir)/m4/ltsugar.m4 $(top_srcdir)/m4/ltversion.m4 \
+ $(top_srcdir)/m4/lt~obsolete.m4 $(top_srcdir)/m4/mmap_write.m4 \
+ $(top_srcdir)/m4/mntctl.m4 $(top_srcdir)/m4/modules.m4 \
+ $(top_srcdir)/m4/notify.m4 $(top_srcdir)/m4/nsl.m4 \
+ $(top_srcdir)/m4/off_t_max.m4 $(top_srcdir)/m4/pkg.m4 \
+ $(top_srcdir)/m4/pr_set_dumpable.m4 \
+ $(top_srcdir)/m4/q_quotactl.m4 $(top_srcdir)/m4/quota.m4 \
+ $(top_srcdir)/m4/random.m4 $(top_srcdir)/m4/rlimit.m4 \
+ $(top_srcdir)/m4/sendfile.m4 $(top_srcdir)/m4/size_t_signed.m4 \
+ $(top_srcdir)/m4/sockpeercred.m4 $(top_srcdir)/m4/sql.m4 \
+ $(top_srcdir)/m4/ssl.m4 $(top_srcdir)/m4/st_tim.m4 \
+ $(top_srcdir)/m4/static_array.m4 $(top_srcdir)/m4/test_with.m4 \
+ $(top_srcdir)/m4/time_t.m4 $(top_srcdir)/m4/typeof.m4 \
+ $(top_srcdir)/m4/typeof_dev_t.m4 \
+ $(top_srcdir)/m4/uoff_t_max.m4 $(top_srcdir)/m4/vararg.m4 \
+ $(top_srcdir)/m4/want_apparmor.m4 \
+ $(top_srcdir)/m4/want_bsdauth.m4 \
+ $(top_srcdir)/m4/want_bzlib.m4 \
+ $(top_srcdir)/m4/want_cassandra.m4 \
+ $(top_srcdir)/m4/want_cdb.m4 \
+ $(top_srcdir)/m4/want_checkpassword.m4 \
+ $(top_srcdir)/m4/want_clucene.m4 $(top_srcdir)/m4/want_db.m4 \
+ $(top_srcdir)/m4/want_gssapi.m4 $(top_srcdir)/m4/want_icu.m4 \
+ $(top_srcdir)/m4/want_ldap.m4 $(top_srcdir)/m4/want_lua.m4 \
+ $(top_srcdir)/m4/want_lz4.m4 $(top_srcdir)/m4/want_lzma.m4 \
+ $(top_srcdir)/m4/want_mysql.m4 $(top_srcdir)/m4/want_pam.m4 \
+ $(top_srcdir)/m4/want_passwd.m4 $(top_srcdir)/m4/want_pgsql.m4 \
+ $(top_srcdir)/m4/want_prefetch.m4 \
+ $(top_srcdir)/m4/want_shadow.m4 \
+ $(top_srcdir)/m4/want_sodium.m4 $(top_srcdir)/m4/want_solr.m4 \
+ $(top_srcdir)/m4/want_sqlite.m4 \
+ $(top_srcdir)/m4/want_stemmer.m4 \
+ $(top_srcdir)/m4/want_systemd.m4 \
+ $(top_srcdir)/m4/want_textcat.m4 \
+ $(top_srcdir)/m4/want_unwind.m4 $(top_srcdir)/m4/want_zlib.m4 \
+ $(top_srcdir)/m4/want_zstd.m4 $(top_srcdir)/configure.ac
+am__configure_deps = $(am__aclocal_m4_deps) $(CONFIGURE_DEPENDENCIES) \
+ $(ACLOCAL_M4)
+DIST_COMMON = $(srcdir)/Makefile.am $(pkginc_lib_HEADERS) \
+ $(am__DIST_COMMON)
+mkinstalldirs = $(install_sh) -d
+CONFIG_HEADER = $(top_builddir)/config.h
+CONFIG_CLEAN_FILES =
+CONFIG_CLEAN_VPATH_FILES =
+am__EXEEXT_1 = test-http-date$(EXEEXT) test-http-url$(EXEEXT) \
+ test-http-header-parser$(EXEEXT) test-http-transfer$(EXEEXT) \
+ test-http-auth$(EXEEXT) test-http-response-parser$(EXEEXT) \
+ test-http-request-parser$(EXEEXT) test-http-payload$(EXEEXT) \
+ test-http-client-errors$(EXEEXT) \
+ test-http-client-request$(EXEEXT) \
+ test-http-server-errors$(EXEEXT)
+am__EXEEXT_2 = test-http-client$(EXEEXT) test-http-server$(EXEEXT)
+PROGRAMS = $(noinst_PROGRAMS)
+LTLIBRARIES = $(noinst_LTLIBRARIES)
+libhttp_la_LIBADD =
+am_libhttp_la_OBJECTS = http-date.lo http-url.lo http-parser.lo \
+ http-header.lo http-header-parser.lo http-transfer-chunked.lo \
+ http-auth.lo http-message-parser.lo http-request.lo \
+ http-request-parser.lo http-response.lo \
+ http-response-parser.lo http-client-request.lo \
+ http-client-connection.lo http-client-peer.lo \
+ http-client-queue.lo http-client-host.lo http-client.lo \
+ http-server-ostream.lo http-server-response.lo \
+ http-server-request.lo http-server-connection.lo \
+ http-server-resource.lo http-server.lo
+libhttp_la_OBJECTS = $(am_libhttp_la_OBJECTS)
+AM_V_lt = $(am__v_lt_@AM_V@)
+am__v_lt_ = $(am__v_lt_@AM_DEFAULT_V@)
+am__v_lt_0 = --silent
+am__v_lt_1 =
+am_test_http_auth_OBJECTS = test-http-auth.$(OBJEXT)
+test_http_auth_OBJECTS = $(am_test_http_auth_OBJECTS)
+am__DEPENDENCIES_1 =
+am__DEPENDENCIES_2 = ../lib-test/libtest.la ../lib/liblib.la \
+ $(am__DEPENDENCIES_1)
+am_test_http_client_OBJECTS = test-http-client.$(OBJEXT)
+test_http_client_OBJECTS = $(am_test_http_client_OBJECTS)
+am__DEPENDENCIES_3 = libhttp.la ../lib-dns/libdns.la \
+ ../lib-ssl-iostream/libssl_iostream.la \
+ ../lib-master/libmaster.la ../lib-auth/libauth.la \
+ ../lib-settings/libsettings.la $(am__DEPENDENCIES_2)
+test_http_client_LINK = $(LIBTOOL) $(AM_V_lt) --tag=CC \
+ $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=link $(CCLD) \
+ $(AM_CFLAGS) $(CFLAGS) $(test_http_client_LDFLAGS) $(LDFLAGS) \
+ -o $@
+am_test_http_client_errors_OBJECTS = \
+ test-http-client-errors.$(OBJEXT)
+test_http_client_errors_OBJECTS = \
+ $(am_test_http_client_errors_OBJECTS)
+test_http_client_errors_LINK = $(LIBTOOL) $(AM_V_lt) --tag=CC \
+ $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=link $(CCLD) \
+ $(AM_CFLAGS) $(CFLAGS) $(test_http_client_errors_LDFLAGS) \
+ $(LDFLAGS) -o $@
+am_test_http_client_request_OBJECTS = \
+ test-http-client-request.$(OBJEXT)
+test_http_client_request_OBJECTS = \
+ $(am_test_http_client_request_OBJECTS)
+test_http_client_request_LINK = $(LIBTOOL) $(AM_V_lt) --tag=CC \
+ $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=link $(CCLD) \
+ $(AM_CFLAGS) $(CFLAGS) $(test_http_client_request_LDFLAGS) \
+ $(LDFLAGS) -o $@
+am_test_http_date_OBJECTS = test-http-date.$(OBJEXT)
+test_http_date_OBJECTS = $(am_test_http_date_OBJECTS)
+am_test_http_header_parser_OBJECTS = \
+ test-http-header-parser.$(OBJEXT)
+test_http_header_parser_OBJECTS = \
+ $(am_test_http_header_parser_OBJECTS)
+am_test_http_payload_OBJECTS = test-http-payload.$(OBJEXT)
+test_http_payload_OBJECTS = $(am_test_http_payload_OBJECTS)
+test_http_payload_LINK = $(LIBTOOL) $(AM_V_lt) --tag=CC \
+ $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=link $(CCLD) \
+ $(AM_CFLAGS) $(CFLAGS) $(test_http_payload_LDFLAGS) $(LDFLAGS) \
+ -o $@
+am_test_http_request_parser_OBJECTS = \
+ test-http-request-parser.$(OBJEXT)
+test_http_request_parser_OBJECTS = \
+ $(am_test_http_request_parser_OBJECTS)
+am_test_http_response_parser_OBJECTS = \
+ test-http-response-parser.$(OBJEXT)
+test_http_response_parser_OBJECTS = \
+ $(am_test_http_response_parser_OBJECTS)
+am_test_http_server_OBJECTS = test-http-server.$(OBJEXT)
+test_http_server_OBJECTS = $(am_test_http_server_OBJECTS)
+test_http_server_LINK = $(LIBTOOL) $(AM_V_lt) --tag=CC \
+ $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=link $(CCLD) \
+ $(AM_CFLAGS) $(CFLAGS) $(test_http_server_LDFLAGS) $(LDFLAGS) \
+ -o $@
+am_test_http_server_errors_OBJECTS = \
+ test-http-server-errors.$(OBJEXT)
+test_http_server_errors_OBJECTS = \
+ $(am_test_http_server_errors_OBJECTS)
+test_http_server_errors_LINK = $(LIBTOOL) $(AM_V_lt) --tag=CC \
+ $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=link $(CCLD) \
+ $(AM_CFLAGS) $(CFLAGS) $(test_http_server_errors_LDFLAGS) \
+ $(LDFLAGS) -o $@
+am_test_http_transfer_OBJECTS = test-http-transfer.$(OBJEXT)
+test_http_transfer_OBJECTS = $(am_test_http_transfer_OBJECTS)
+am_test_http_url_OBJECTS = test-http-url.$(OBJEXT)
+test_http_url_OBJECTS = $(am_test_http_url_OBJECTS)
+AM_V_P = $(am__v_P_@AM_V@)
+am__v_P_ = $(am__v_P_@AM_DEFAULT_V@)
+am__v_P_0 = false
+am__v_P_1 = :
+AM_V_GEN = $(am__v_GEN_@AM_V@)
+am__v_GEN_ = $(am__v_GEN_@AM_DEFAULT_V@)
+am__v_GEN_0 = @echo " GEN " $@;
+am__v_GEN_1 =
+AM_V_at = $(am__v_at_@AM_V@)
+am__v_at_ = $(am__v_at_@AM_DEFAULT_V@)
+am__v_at_0 = @
+am__v_at_1 =
+DEFAULT_INCLUDES = -I.@am__isrc@ -I$(top_builddir)
+depcomp = $(SHELL) $(top_srcdir)/depcomp
+am__maybe_remake_depfiles = depfiles
+am__depfiles_remade = ./$(DEPDIR)/http-auth.Plo \
+ ./$(DEPDIR)/http-client-connection.Plo \
+ ./$(DEPDIR)/http-client-host.Plo \
+ ./$(DEPDIR)/http-client-peer.Plo \
+ ./$(DEPDIR)/http-client-queue.Plo \
+ ./$(DEPDIR)/http-client-request.Plo \
+ ./$(DEPDIR)/http-client.Plo ./$(DEPDIR)/http-date.Plo \
+ ./$(DEPDIR)/http-header-parser.Plo ./$(DEPDIR)/http-header.Plo \
+ ./$(DEPDIR)/http-message-parser.Plo \
+ ./$(DEPDIR)/http-parser.Plo \
+ ./$(DEPDIR)/http-request-parser.Plo \
+ ./$(DEPDIR)/http-request.Plo \
+ ./$(DEPDIR)/http-response-parser.Plo \
+ ./$(DEPDIR)/http-response.Plo \
+ ./$(DEPDIR)/http-server-connection.Plo \
+ ./$(DEPDIR)/http-server-ostream.Plo \
+ ./$(DEPDIR)/http-server-request.Plo \
+ ./$(DEPDIR)/http-server-resource.Plo \
+ ./$(DEPDIR)/http-server-response.Plo \
+ ./$(DEPDIR)/http-server.Plo \
+ ./$(DEPDIR)/http-transfer-chunked.Plo ./$(DEPDIR)/http-url.Plo \
+ ./$(DEPDIR)/test-http-auth.Po \
+ ./$(DEPDIR)/test-http-client-errors.Po \
+ ./$(DEPDIR)/test-http-client-request.Po \
+ ./$(DEPDIR)/test-http-client.Po ./$(DEPDIR)/test-http-date.Po \
+ ./$(DEPDIR)/test-http-header-parser.Po \
+ ./$(DEPDIR)/test-http-payload.Po \
+ ./$(DEPDIR)/test-http-request-parser.Po \
+ ./$(DEPDIR)/test-http-response-parser.Po \
+ ./$(DEPDIR)/test-http-server-errors.Po \
+ ./$(DEPDIR)/test-http-server.Po \
+ ./$(DEPDIR)/test-http-transfer.Po ./$(DEPDIR)/test-http-url.Po
+am__mv = mv -f
+COMPILE = $(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) \
+ $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS)
+LTCOMPILE = $(LIBTOOL) $(AM_V_lt) --tag=CC $(AM_LIBTOOLFLAGS) \
+ $(LIBTOOLFLAGS) --mode=compile $(CC) $(DEFS) \
+ $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) $(CPPFLAGS) \
+ $(AM_CFLAGS) $(CFLAGS)
+AM_V_CC = $(am__v_CC_@AM_V@)
+am__v_CC_ = $(am__v_CC_@AM_DEFAULT_V@)
+am__v_CC_0 = @echo " CC " $@;
+am__v_CC_1 =
+CCLD = $(CC)
+LINK = $(LIBTOOL) $(AM_V_lt) --tag=CC $(AM_LIBTOOLFLAGS) \
+ $(LIBTOOLFLAGS) --mode=link $(CCLD) $(AM_CFLAGS) $(CFLAGS) \
+ $(AM_LDFLAGS) $(LDFLAGS) -o $@
+AM_V_CCLD = $(am__v_CCLD_@AM_V@)
+am__v_CCLD_ = $(am__v_CCLD_@AM_DEFAULT_V@)
+am__v_CCLD_0 = @echo " CCLD " $@;
+am__v_CCLD_1 =
+SOURCES = $(libhttp_la_SOURCES) $(test_http_auth_SOURCES) \
+ $(test_http_client_SOURCES) $(test_http_client_errors_SOURCES) \
+ $(test_http_client_request_SOURCES) $(test_http_date_SOURCES) \
+ $(test_http_header_parser_SOURCES) \
+ $(test_http_payload_SOURCES) \
+ $(test_http_request_parser_SOURCES) \
+ $(test_http_response_parser_SOURCES) \
+ $(test_http_server_SOURCES) $(test_http_server_errors_SOURCES) \
+ $(test_http_transfer_SOURCES) $(test_http_url_SOURCES)
+DIST_SOURCES = $(libhttp_la_SOURCES) $(test_http_auth_SOURCES) \
+ $(test_http_client_SOURCES) $(test_http_client_errors_SOURCES) \
+ $(test_http_client_request_SOURCES) $(test_http_date_SOURCES) \
+ $(test_http_header_parser_SOURCES) \
+ $(test_http_payload_SOURCES) \
+ $(test_http_request_parser_SOURCES) \
+ $(test_http_response_parser_SOURCES) \
+ $(test_http_server_SOURCES) $(test_http_server_errors_SOURCES) \
+ $(test_http_transfer_SOURCES) $(test_http_url_SOURCES)
+am__can_run_installinfo = \
+ case $$AM_UPDATE_INFO_DIR in \
+ n|no|NO) false;; \
+ *) (install-info --version) >/dev/null 2>&1;; \
+ esac
+am__vpath_adj_setup = srcdirstrip=`echo "$(srcdir)" | sed 's|.|.|g'`;
+am__vpath_adj = case $$p in \
+ $(srcdir)/*) f=`echo "$$p" | sed "s|^$$srcdirstrip/||"`;; \
+ *) f=$$p;; \
+ esac;
+am__strip_dir = f=`echo $$p | sed -e 's|^.*/||'`;
+am__install_max = 40
+am__nobase_strip_setup = \
+ srcdirstrip=`echo "$(srcdir)" | sed 's/[].[^$$\\*|]/\\\\&/g'`
+am__nobase_strip = \
+ for p in $$list; do echo "$$p"; done | sed -e "s|$$srcdirstrip/||"
+am__nobase_list = $(am__nobase_strip_setup); \
+ for p in $$list; do echo "$$p $$p"; done | \
+ sed "s| $$srcdirstrip/| |;"' / .*\//!s/ .*/ ./; s,\( .*\)/[^/]*$$,\1,' | \
+ $(AWK) 'BEGIN { files["."] = "" } { files[$$2] = files[$$2] " " $$1; \
+ if (++n[$$2] == $(am__install_max)) \
+ { print $$2, files[$$2]; n[$$2] = 0; files[$$2] = "" } } \
+ END { for (dir in files) print dir, files[dir] }'
+am__base_list = \
+ sed '$$!N;$$!N;$$!N;$$!N;$$!N;$$!N;$$!N;s/\n/ /g' | \
+ sed '$$!N;$$!N;$$!N;$$!N;s/\n/ /g'
+am__uninstall_files_from_dir = { \
+ test -z "$$files" \
+ || { test ! -d "$$dir" && test ! -f "$$dir" && test ! -r "$$dir"; } \
+ || { echo " ( cd '$$dir' && rm -f" $$files ")"; \
+ $(am__cd) "$$dir" && rm -f $$files; }; \
+ }
+am__installdirs = "$(DESTDIR)$(pkginc_libdir)"
+HEADERS = $(pkginc_lib_HEADERS)
+am__tagged_files = $(HEADERS) $(SOURCES) $(TAGS_FILES) $(LISP)
+# Read a list of newline-separated strings from the standard input,
+# and print each of them once, without duplicates. Input order is
+# *not* preserved.
+am__uniquify_input = $(AWK) '\
+ BEGIN { nonempty = 0; } \
+ { items[$$0] = 1; nonempty = 1; } \
+ END { if (nonempty) { for (i in items) print i; }; } \
+'
+# Make sure the list of sources is unique. This is necessary because,
+# e.g., the same source file might be shared among _SOURCES variables
+# for different programs/libraries.
+am__define_uniq_tagged_files = \
+ list='$(am__tagged_files)'; \
+ unique=`for i in $$list; do \
+ if test -f "$$i"; then echo $$i; else echo $(srcdir)/$$i; fi; \
+ done | $(am__uniquify_input)`
+ETAGS = etags
+CTAGS = ctags
+am__DIST_COMMON = $(srcdir)/Makefile.in $(top_srcdir)/depcomp
+DISTFILES = $(DIST_COMMON) $(DIST_SOURCES) $(TEXINFOS) $(EXTRA_DIST)
+ACLOCAL = @ACLOCAL@
+ACLOCAL_AMFLAGS = @ACLOCAL_AMFLAGS@
+AMTAR = @AMTAR@
+AM_DEFAULT_VERBOSITY = @AM_DEFAULT_VERBOSITY@
+APPARMOR_LIBS = @APPARMOR_LIBS@
+AR = @AR@
+AUTH_CFLAGS = @AUTH_CFLAGS@
+AUTH_LIBS = @AUTH_LIBS@
+AUTOCONF = @AUTOCONF@
+AUTOHEADER = @AUTOHEADER@
+AUTOMAKE = @AUTOMAKE@
+AWK = @AWK@
+BINARY_CFLAGS = @BINARY_CFLAGS@
+BINARY_LDFLAGS = @BINARY_LDFLAGS@
+BISON = @BISON@
+CASSANDRA_CFLAGS = @CASSANDRA_CFLAGS@
+CASSANDRA_LIBS = @CASSANDRA_LIBS@
+CC = @CC@
+CCDEPMODE = @CCDEPMODE@
+CDB_LIBS = @CDB_LIBS@
+CFLAGS = @CFLAGS@
+CLUCENE_CFLAGS = @CLUCENE_CFLAGS@
+CLUCENE_LIBS = @CLUCENE_LIBS@
+COMPRESS_LIBS = @COMPRESS_LIBS@
+CPP = @CPP@
+CPPFLAGS = @CPPFLAGS@
+CRYPT_LIBS = @CRYPT_LIBS@
+CXX = @CXX@
+CXXCPP = @CXXCPP@
+CXXDEPMODE = @CXXDEPMODE@
+CXXFLAGS = @CXXFLAGS@
+CYGPATH_W = @CYGPATH_W@
+DEFS = @DEFS@
+DEPDIR = @DEPDIR@
+DICT_LIBS = @DICT_LIBS@
+DLLIB = @DLLIB@
+DLLTOOL = @DLLTOOL@
+DSYMUTIL = @DSYMUTIL@
+DUMPBIN = @DUMPBIN@
+ECHO_C = @ECHO_C@
+ECHO_N = @ECHO_N@
+ECHO_T = @ECHO_T@
+EGREP = @EGREP@
+EXEEXT = @EXEEXT@
+FGREP = @FGREP@
+FLEX = @FLEX@
+FUZZER_CPPFLAGS = @FUZZER_CPPFLAGS@
+FUZZER_LDFLAGS = @FUZZER_LDFLAGS@
+GREP = @GREP@
+INSTALL = @INSTALL@
+INSTALL_DATA = @INSTALL_DATA@
+INSTALL_PROGRAM = @INSTALL_PROGRAM@
+INSTALL_SCRIPT = @INSTALL_SCRIPT@
+INSTALL_STRIP_PROGRAM = @INSTALL_STRIP_PROGRAM@
+KRB5CONFIG = @KRB5CONFIG@
+KRB5_CFLAGS = @KRB5_CFLAGS@
+KRB5_LIBS = @KRB5_LIBS@
+LD = @LD@
+LDAP_LIBS = @LDAP_LIBS@
+LDFLAGS = @LDFLAGS@
+LD_NO_WHOLE_ARCHIVE = @LD_NO_WHOLE_ARCHIVE@
+LD_WHOLE_ARCHIVE = @LD_WHOLE_ARCHIVE@
+LIBCAP = @LIBCAP@
+LIBDOVECOT = @LIBDOVECOT@
+LIBDOVECOT_COMPRESS = @LIBDOVECOT_COMPRESS@
+LIBDOVECOT_DEPS = @LIBDOVECOT_DEPS@
+LIBDOVECOT_DSYNC = @LIBDOVECOT_DSYNC@
+LIBDOVECOT_LA_LIBS = @LIBDOVECOT_LA_LIBS@
+LIBDOVECOT_LDA = @LIBDOVECOT_LDA@
+LIBDOVECOT_LDAP = @LIBDOVECOT_LDAP@
+LIBDOVECOT_LIBFTS = @LIBDOVECOT_LIBFTS@
+LIBDOVECOT_LIBFTS_DEPS = @LIBDOVECOT_LIBFTS_DEPS@
+LIBDOVECOT_LOGIN = @LIBDOVECOT_LOGIN@
+LIBDOVECOT_LUA = @LIBDOVECOT_LUA@
+LIBDOVECOT_LUA_DEPS = @LIBDOVECOT_LUA_DEPS@
+LIBDOVECOT_SQL = @LIBDOVECOT_SQL@
+LIBDOVECOT_STORAGE = @LIBDOVECOT_STORAGE@
+LIBDOVECOT_STORAGE_DEPS = @LIBDOVECOT_STORAGE_DEPS@
+LIBEXTTEXTCAT_CFLAGS = @LIBEXTTEXTCAT_CFLAGS@
+LIBEXTTEXTCAT_LIBS = @LIBEXTTEXTCAT_LIBS@
+LIBICONV = @LIBICONV@
+LIBICU_CFLAGS = @LIBICU_CFLAGS@
+LIBICU_LIBS = @LIBICU_LIBS@
+LIBOBJS = @LIBOBJS@
+LIBS = @LIBS@
+LIBSODIUM_CFLAGS = @LIBSODIUM_CFLAGS@
+LIBSODIUM_LIBS = @LIBSODIUM_LIBS@
+LIBTIRPC_CFLAGS = @LIBTIRPC_CFLAGS@
+LIBTIRPC_LIBS = @LIBTIRPC_LIBS@
+LIBTOOL = @LIBTOOL@
+LIBUNWIND_CFLAGS = @LIBUNWIND_CFLAGS@
+LIBUNWIND_LIBS = @LIBUNWIND_LIBS@
+LIBWRAP_LIBS = @LIBWRAP_LIBS@
+LINKED_STORAGE_LDADD = @LINKED_STORAGE_LDADD@
+LIPO = @LIPO@
+LN_S = @LN_S@
+LTLIBICONV = @LTLIBICONV@
+LTLIBOBJS = @LTLIBOBJS@
+LT_SYS_LIBRARY_PATH = @LT_SYS_LIBRARY_PATH@
+LUA_CFLAGS = @LUA_CFLAGS@
+LUA_LIBS = @LUA_LIBS@
+MAINT = @MAINT@
+MAKEINFO = @MAKEINFO@
+MANIFEST_TOOL = @MANIFEST_TOOL@
+MKDIR_P = @MKDIR_P@
+MODULE_LIBS = @MODULE_LIBS@
+MODULE_SUFFIX = @MODULE_SUFFIX@
+MYSQL_CFLAGS = @MYSQL_CFLAGS@
+MYSQL_CONFIG = @MYSQL_CONFIG@
+MYSQL_LIBS = @MYSQL_LIBS@
+NM = @NM@
+NMEDIT = @NMEDIT@
+NOPLUGIN_LDFLAGS = @NOPLUGIN_LDFLAGS@
+OBJDUMP = @OBJDUMP@
+OBJEXT = @OBJEXT@
+OTOOL = @OTOOL@
+OTOOL64 = @OTOOL64@
+PACKAGE = @PACKAGE@
+PACKAGE_BUGREPORT = @PACKAGE_BUGREPORT@
+PACKAGE_NAME = @PACKAGE_NAME@
+PACKAGE_STRING = @PACKAGE_STRING@
+PACKAGE_TARNAME = @PACKAGE_TARNAME@
+PACKAGE_URL = @PACKAGE_URL@
+PACKAGE_VERSION = @PACKAGE_VERSION@
+PANDOC = @PANDOC@
+PATH_SEPARATOR = @PATH_SEPARATOR@
+PGSQL_CFLAGS = @PGSQL_CFLAGS@
+PGSQL_LIBS = @PGSQL_LIBS@
+PG_CONFIG = @PG_CONFIG@
+PIE_CFLAGS = @PIE_CFLAGS@
+PIE_LDFLAGS = @PIE_LDFLAGS@
+PKG_CONFIG = @PKG_CONFIG@
+PKG_CONFIG_LIBDIR = @PKG_CONFIG_LIBDIR@
+PKG_CONFIG_PATH = @PKG_CONFIG_PATH@
+QUOTA_LIBS = @QUOTA_LIBS@
+RANLIB = @RANLIB@
+RELRO_LDFLAGS = @RELRO_LDFLAGS@
+RPCGEN = @RPCGEN@
+RUN_TEST = @RUN_TEST@
+SED = @SED@
+SETTING_FILES = @SETTING_FILES@
+SET_MAKE = @SET_MAKE@
+SHELL = @SHELL@
+SQLITE_CFLAGS = @SQLITE_CFLAGS@
+SQLITE_LIBS = @SQLITE_LIBS@
+SQL_CFLAGS = @SQL_CFLAGS@
+SQL_LIBS = @SQL_LIBS@
+SSL_CFLAGS = @SSL_CFLAGS@
+SSL_LIBS = @SSL_LIBS@
+STRIP = @STRIP@
+SYSTEMD_CFLAGS = @SYSTEMD_CFLAGS@
+SYSTEMD_LIBS = @SYSTEMD_LIBS@
+VALGRIND = @VALGRIND@
+VERSION = @VERSION@
+ZSTD_CFLAGS = @ZSTD_CFLAGS@
+ZSTD_LIBS = @ZSTD_LIBS@
+abs_builddir = @abs_builddir@
+abs_srcdir = @abs_srcdir@
+abs_top_builddir = @abs_top_builddir@
+abs_top_srcdir = @abs_top_srcdir@
+ac_ct_AR = @ac_ct_AR@
+ac_ct_CC = @ac_ct_CC@
+ac_ct_CXX = @ac_ct_CXX@
+ac_ct_DUMPBIN = @ac_ct_DUMPBIN@
+am__include = @am__include@
+am__leading_dot = @am__leading_dot@
+am__quote = @am__quote@
+am__tar = @am__tar@
+am__untar = @am__untar@
+bindir = @bindir@
+build = @build@
+build_alias = @build_alias@
+build_cpu = @build_cpu@
+build_os = @build_os@
+build_vendor = @build_vendor@
+builddir = @builddir@
+datadir = @datadir@
+datarootdir = @datarootdir@
+dict_drivers = @dict_drivers@
+docdir = @docdir@
+dvidir = @dvidir@
+exec_prefix = @exec_prefix@
+host = @host@
+host_alias = @host_alias@
+host_cpu = @host_cpu@
+host_os = @host_os@
+host_vendor = @host_vendor@
+htmldir = @htmldir@
+includedir = @includedir@
+infodir = @infodir@
+install_sh = @install_sh@
+libdir = @libdir@
+libexecdir = @libexecdir@
+localedir = @localedir@
+localstatedir = @localstatedir@
+mandir = @mandir@
+mkdir_p = @mkdir_p@
+moduledir = @moduledir@
+oldincludedir = @oldincludedir@
+pdfdir = @pdfdir@
+prefix = @prefix@
+program_transform_name = @program_transform_name@
+psdir = @psdir@
+rundir = @rundir@
+runstatedir = @runstatedir@
+sbindir = @sbindir@
+sharedstatedir = @sharedstatedir@
+sql_drivers = @sql_drivers@
+srcdir = @srcdir@
+ssldir = @ssldir@
+statedir = @statedir@
+sysconfdir = @sysconfdir@
+systemdservicetype = @systemdservicetype@
+systemdsystemunitdir = @systemdsystemunitdir@
+target_alias = @target_alias@
+top_build_prefix = @top_build_prefix@
+top_builddir = @top_builddir@
+top_srcdir = @top_srcdir@
+noinst_LTLIBRARIES = libhttp.la
+AM_CPPFLAGS = \
+ -I$(top_srcdir)/src/lib \
+ -I$(top_srcdir)/src/lib-test \
+ -I$(top_srcdir)/src/lib-dns \
+ -I$(top_srcdir)/src/lib-ssl-iostream \
+ -I$(top_srcdir)/src/lib-master \
+ -DPKG_RUNDIR=\""$(rundir)"\"
+
+libhttp_la_SOURCES = \
+ http-date.c \
+ http-url.c \
+ http-parser.c \
+ http-header.c \
+ http-header-parser.c \
+ http-transfer-chunked.c \
+ http-auth.c \
+ http-message-parser.c \
+ http-request.c \
+ http-request-parser.c \
+ http-response.c \
+ http-response-parser.c \
+ http-client-request.c \
+ http-client-connection.c \
+ http-client-peer.c \
+ http-client-queue.c \
+ http-client-host.c \
+ http-client.c \
+ http-server-ostream.c \
+ http-server-response.c \
+ http-server-request.c \
+ http-server-connection.c \
+ http-server-resource.c \
+ http-server.c
+
+headers = \
+ http-common.h \
+ http-date.h \
+ http-url.h \
+ http-parser.h \
+ http-header.h \
+ http-header-parser.h \
+ http-transfer.h \
+ http-auth.h \
+ http-message-parser.h \
+ http-request.h \
+ http-request-parser.h \
+ http-response.h \
+ http-response-parser.h \
+ http-client-private.h \
+ http-client.h \
+ http-server-private.h \
+ http-server.h
+
+pkginc_libdir = $(pkgincludedir)
+pkginc_lib_HEADERS = $(headers)
+test_programs = \
+ test-http-date \
+ test-http-url \
+ test-http-header-parser \
+ test-http-transfer \
+ test-http-auth \
+ test-http-response-parser \
+ test-http-request-parser \
+ test-http-payload \
+ test-http-client-errors \
+ test-http-client-request \
+ test-http-server-errors
+
+test_nocheck_programs = \
+ test-http-client \
+ test-http-server
+
+test_libs = \
+ ../lib-test/libtest.la \
+ ../lib/liblib.la \
+ $(MODULE_LIBS)
+
+test_deps = \
+ $(noinst_LTLIBRARIES) \
+ ../lib-test/libtest.la \
+ ../lib/liblib.la
+
+test_http_url_SOURCES = test-http-url.c
+test_http_url_LDADD = http-url.lo http-header.lo $(test_libs)
+test_http_url_DEPENDENCIES = $(test_deps)
+test_http_date_SOURCES = test-http-date.c
+test_http_date_LDADD = http-date.lo $(test_libs)
+test_http_date_DEPENDENCIES = $(test_deps)
+test_http_header_parser_SOURCES = test-http-header-parser.c
+test_http_header_parser_LDADD = http-parser.lo http-header-parser.lo http-header.lo $(test_libs)
+test_http_header_parser_DEPENDENCIES = $(test_deps)
+test_http_transfer_SOURCES = test-http-transfer.c
+test_http_transfer_LDADD = \
+ http-parser.lo \
+ http-header-parser.lo \
+ http-transfer-chunked.lo \
+ http-header.lo \
+ $(test_libs)
+
+test_http_transfer_DEPENDENCIES = $(test_deps)
+test_http_auth_SOURCES = test-http-auth.c
+test_http_auth_LDADD = \
+ http-auth.lo \
+ http-parser.lo \
+ $(test_libs)
+
+test_http_auth_DEPENDENCIES = $(test_deps)
+test_http_response_parser_SOURCES = test-http-response-parser.c
+test_http_response_parser_LDADD = \
+ http-date.lo \
+ http-parser.lo \
+ http-header.lo \
+ http-header-parser.lo \
+ http-transfer-chunked.lo \
+ http-message-parser.lo \
+ http-response-parser.lo \
+ $(test_libs)
+
+test_http_response_parser_DEPENDENCIES = $(test_deps)
+test_http_request_parser_SOURCES = test-http-request-parser.c
+test_http_request_parser_LDADD = \
+ http-date.lo \
+ http-parser.lo \
+ http-url.lo \
+ http-header.lo \
+ http-header-parser.lo \
+ http-transfer-chunked.lo \
+ http-message-parser.lo \
+ http-request-parser.lo \
+ $(test_libs)
+
+test_http_request_parser_DEPENDENCIES = $(test_deps)
+test_http_libs = \
+ libhttp.la \
+ ../lib-dns/libdns.la \
+ ../lib-ssl-iostream/libssl_iostream.la \
+ ../lib-master/libmaster.la \
+ ../lib-auth/libauth.la \
+ ../lib-settings/libsettings.la \
+ $(test_libs)
+
+test_http_deps = \
+ libhttp.la \
+ ../lib-dns/libdns.la \
+ ../lib-ssl-iostream/libssl_iostream.la \
+ ../lib-master/libmaster.la \
+ ../lib-auth/libauth.la \
+ ../lib-settings/libsettings.la \
+ $(test_deps)
+
+test_http_libs_ssl = $(am__append_1)
+test_http_payload_SOURCES = test-http-payload.c
+test_http_payload_LDFLAGS = -export-dynamic
+test_http_payload_LDADD = \
+ $(test_http_libs) \
+ $(test_http_libs_ssl)
+
+test_http_payload_DEPENDENCIES = \
+ $(test_http_deps)
+
+test_http_client_SOURCES = test-http-client.c
+test_http_client_LDFLAGS = -export-dynamic
+test_http_client_LDADD = \
+ $(test_http_libs) \
+ $(test_http_libs_ssl)
+
+test_http_client_DEPENDENCIES = \
+ $(test_http_deps)
+
+test_http_client_errors_SOURCES = test-http-client-errors.c
+test_http_client_errors_LDFLAGS = -export-dynamic
+test_http_client_errors_LDADD = \
+ $(test_http_libs)
+
+test_http_client_errors_DEPENDENCIES = \
+ $(test_http_deps)
+
+test_http_client_request_SOURCES = test-http-client-request.c
+test_http_client_request_LDFLAGS = -export-dynamic
+test_http_client_request_LDADD = \
+ $(test_http_libs)
+
+test_http_client_request_DEPENDENCIES = \
+ $(test_http_deps)
+
+test_http_server_SOURCES = test-http-server.c
+test_http_server_LDFLAGS = -export-dynamic
+test_http_server_LDADD = \
+ $(test_http_libs)
+
+test_http_server_DEPENDENCIES = \
+ $(test_http_deps)
+
+test_http_server_errors_SOURCES = test-http-server-errors.c
+test_http_server_errors_LDFLAGS = -export-dynamic
+test_http_server_errors_LDADD = \
+ $(test_http_libs)
+
+test_http_server_errors_DEPENDENCIES = \
+ $(test_http_deps)
+
+all: all-am
+
+.SUFFIXES:
+.SUFFIXES: .c .lo .o .obj
+$(srcdir)/Makefile.in: @MAINTAINER_MODE_TRUE@ $(srcdir)/Makefile.am $(am__configure_deps)
+ @for dep in $?; do \
+ case '$(am__configure_deps)' in \
+ *$$dep*) \
+ ( cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh ) \
+ && { if test -f $@; then exit 0; else break; fi; }; \
+ exit 1;; \
+ esac; \
+ done; \
+ echo ' cd $(top_srcdir) && $(AUTOMAKE) --foreign src/lib-http/Makefile'; \
+ $(am__cd) $(top_srcdir) && \
+ $(AUTOMAKE) --foreign src/lib-http/Makefile
+Makefile: $(srcdir)/Makefile.in $(top_builddir)/config.status
+ @case '$?' in \
+ *config.status*) \
+ cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh;; \
+ *) \
+ echo ' cd $(top_builddir) && $(SHELL) ./config.status $(subdir)/$@ $(am__maybe_remake_depfiles)'; \
+ cd $(top_builddir) && $(SHELL) ./config.status $(subdir)/$@ $(am__maybe_remake_depfiles);; \
+ esac;
+
+$(top_builddir)/config.status: $(top_srcdir)/configure $(CONFIG_STATUS_DEPENDENCIES)
+ cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh
+
+$(top_srcdir)/configure: @MAINTAINER_MODE_TRUE@ $(am__configure_deps)
+ cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh
+$(ACLOCAL_M4): @MAINTAINER_MODE_TRUE@ $(am__aclocal_m4_deps)
+ cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh
+$(am__aclocal_m4_deps):
+
+clean-noinstPROGRAMS:
+ @list='$(noinst_PROGRAMS)'; test -n "$$list" || exit 0; \
+ echo " rm -f" $$list; \
+ rm -f $$list || exit $$?; \
+ test -n "$(EXEEXT)" || exit 0; \
+ list=`for p in $$list; do echo "$$p"; done | sed 's/$(EXEEXT)$$//'`; \
+ echo " rm -f" $$list; \
+ rm -f $$list
+
+clean-noinstLTLIBRARIES:
+ -test -z "$(noinst_LTLIBRARIES)" || rm -f $(noinst_LTLIBRARIES)
+ @list='$(noinst_LTLIBRARIES)'; \
+ locs=`for p in $$list; do echo $$p; done | \
+ sed 's|^[^/]*$$|.|; s|/[^/]*$$||; s|$$|/so_locations|' | \
+ sort -u`; \
+ test -z "$$locs" || { \
+ echo rm -f $${locs}; \
+ rm -f $${locs}; \
+ }
+
+libhttp.la: $(libhttp_la_OBJECTS) $(libhttp_la_DEPENDENCIES) $(EXTRA_libhttp_la_DEPENDENCIES)
+ $(AM_V_CCLD)$(LINK) $(libhttp_la_OBJECTS) $(libhttp_la_LIBADD) $(LIBS)
+
+test-http-auth$(EXEEXT): $(test_http_auth_OBJECTS) $(test_http_auth_DEPENDENCIES) $(EXTRA_test_http_auth_DEPENDENCIES)
+ @rm -f test-http-auth$(EXEEXT)
+ $(AM_V_CCLD)$(LINK) $(test_http_auth_OBJECTS) $(test_http_auth_LDADD) $(LIBS)
+
+test-http-client$(EXEEXT): $(test_http_client_OBJECTS) $(test_http_client_DEPENDENCIES) $(EXTRA_test_http_client_DEPENDENCIES)
+ @rm -f test-http-client$(EXEEXT)
+ $(AM_V_CCLD)$(test_http_client_LINK) $(test_http_client_OBJECTS) $(test_http_client_LDADD) $(LIBS)
+
+test-http-client-errors$(EXEEXT): $(test_http_client_errors_OBJECTS) $(test_http_client_errors_DEPENDENCIES) $(EXTRA_test_http_client_errors_DEPENDENCIES)
+ @rm -f test-http-client-errors$(EXEEXT)
+ $(AM_V_CCLD)$(test_http_client_errors_LINK) $(test_http_client_errors_OBJECTS) $(test_http_client_errors_LDADD) $(LIBS)
+
+test-http-client-request$(EXEEXT): $(test_http_client_request_OBJECTS) $(test_http_client_request_DEPENDENCIES) $(EXTRA_test_http_client_request_DEPENDENCIES)
+ @rm -f test-http-client-request$(EXEEXT)
+ $(AM_V_CCLD)$(test_http_client_request_LINK) $(test_http_client_request_OBJECTS) $(test_http_client_request_LDADD) $(LIBS)
+
+test-http-date$(EXEEXT): $(test_http_date_OBJECTS) $(test_http_date_DEPENDENCIES) $(EXTRA_test_http_date_DEPENDENCIES)
+ @rm -f test-http-date$(EXEEXT)
+ $(AM_V_CCLD)$(LINK) $(test_http_date_OBJECTS) $(test_http_date_LDADD) $(LIBS)
+
+test-http-header-parser$(EXEEXT): $(test_http_header_parser_OBJECTS) $(test_http_header_parser_DEPENDENCIES) $(EXTRA_test_http_header_parser_DEPENDENCIES)
+ @rm -f test-http-header-parser$(EXEEXT)
+ $(AM_V_CCLD)$(LINK) $(test_http_header_parser_OBJECTS) $(test_http_header_parser_LDADD) $(LIBS)
+
+test-http-payload$(EXEEXT): $(test_http_payload_OBJECTS) $(test_http_payload_DEPENDENCIES) $(EXTRA_test_http_payload_DEPENDENCIES)
+ @rm -f test-http-payload$(EXEEXT)
+ $(AM_V_CCLD)$(test_http_payload_LINK) $(test_http_payload_OBJECTS) $(test_http_payload_LDADD) $(LIBS)
+
+test-http-request-parser$(EXEEXT): $(test_http_request_parser_OBJECTS) $(test_http_request_parser_DEPENDENCIES) $(EXTRA_test_http_request_parser_DEPENDENCIES)
+ @rm -f test-http-request-parser$(EXEEXT)
+ $(AM_V_CCLD)$(LINK) $(test_http_request_parser_OBJECTS) $(test_http_request_parser_LDADD) $(LIBS)
+
+test-http-response-parser$(EXEEXT): $(test_http_response_parser_OBJECTS) $(test_http_response_parser_DEPENDENCIES) $(EXTRA_test_http_response_parser_DEPENDENCIES)
+ @rm -f test-http-response-parser$(EXEEXT)
+ $(AM_V_CCLD)$(LINK) $(test_http_response_parser_OBJECTS) $(test_http_response_parser_LDADD) $(LIBS)
+
+test-http-server$(EXEEXT): $(test_http_server_OBJECTS) $(test_http_server_DEPENDENCIES) $(EXTRA_test_http_server_DEPENDENCIES)
+ @rm -f test-http-server$(EXEEXT)
+ $(AM_V_CCLD)$(test_http_server_LINK) $(test_http_server_OBJECTS) $(test_http_server_LDADD) $(LIBS)
+
+test-http-server-errors$(EXEEXT): $(test_http_server_errors_OBJECTS) $(test_http_server_errors_DEPENDENCIES) $(EXTRA_test_http_server_errors_DEPENDENCIES)
+ @rm -f test-http-server-errors$(EXEEXT)
+ $(AM_V_CCLD)$(test_http_server_errors_LINK) $(test_http_server_errors_OBJECTS) $(test_http_server_errors_LDADD) $(LIBS)
+
+test-http-transfer$(EXEEXT): $(test_http_transfer_OBJECTS) $(test_http_transfer_DEPENDENCIES) $(EXTRA_test_http_transfer_DEPENDENCIES)
+ @rm -f test-http-transfer$(EXEEXT)
+ $(AM_V_CCLD)$(LINK) $(test_http_transfer_OBJECTS) $(test_http_transfer_LDADD) $(LIBS)
+
+test-http-url$(EXEEXT): $(test_http_url_OBJECTS) $(test_http_url_DEPENDENCIES) $(EXTRA_test_http_url_DEPENDENCIES)
+ @rm -f test-http-url$(EXEEXT)
+ $(AM_V_CCLD)$(LINK) $(test_http_url_OBJECTS) $(test_http_url_LDADD) $(LIBS)
+
+mostlyclean-compile:
+ -rm -f *.$(OBJEXT)
+
+distclean-compile:
+ -rm -f *.tab.c
+
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/http-auth.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/http-client-connection.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/http-client-host.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/http-client-peer.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/http-client-queue.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/http-client-request.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/http-client.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/http-date.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/http-header-parser.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/http-header.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/http-message-parser.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/http-parser.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/http-request-parser.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/http-request.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/http-response-parser.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/http-response.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/http-server-connection.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/http-server-ostream.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/http-server-request.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/http-server-resource.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/http-server-response.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/http-server.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/http-transfer-chunked.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/http-url.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/test-http-auth.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/test-http-client-errors.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/test-http-client-request.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/test-http-client.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/test-http-date.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/test-http-header-parser.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/test-http-payload.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/test-http-request-parser.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/test-http-response-parser.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/test-http-server-errors.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/test-http-server.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/test-http-transfer.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/test-http-url.Po@am__quote@ # am--include-marker
+
+$(am__depfiles_remade):
+ @$(MKDIR_P) $(@D)
+ @echo '# dummy' >$@-t && $(am__mv) $@-t $@
+
+am--depfiles: $(am__depfiles_remade)
+
+.c.o:
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(COMPILE) -MT $@ -MD -MP -MF $(DEPDIR)/$*.Tpo -c -o $@ $<
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/$*.Tpo $(DEPDIR)/$*.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='$<' object='$@' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(COMPILE) -c -o $@ $<
+
+.c.obj:
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(COMPILE) -MT $@ -MD -MP -MF $(DEPDIR)/$*.Tpo -c -o $@ `$(CYGPATH_W) '$<'`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/$*.Tpo $(DEPDIR)/$*.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='$<' object='$@' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(COMPILE) -c -o $@ `$(CYGPATH_W) '$<'`
+
+.c.lo:
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(LTCOMPILE) -MT $@ -MD -MP -MF $(DEPDIR)/$*.Tpo -c -o $@ $<
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/$*.Tpo $(DEPDIR)/$*.Plo
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='$<' object='$@' libtool=yes @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(LTCOMPILE) -c -o $@ $<
+
+mostlyclean-libtool:
+ -rm -f *.lo
+
+clean-libtool:
+ -rm -rf .libs _libs
+install-pkginc_libHEADERS: $(pkginc_lib_HEADERS)
+ @$(NORMAL_INSTALL)
+ @list='$(pkginc_lib_HEADERS)'; test -n "$(pkginc_libdir)" || list=; \
+ if test -n "$$list"; then \
+ echo " $(MKDIR_P) '$(DESTDIR)$(pkginc_libdir)'"; \
+ $(MKDIR_P) "$(DESTDIR)$(pkginc_libdir)" || exit 1; \
+ fi; \
+ for p in $$list; do \
+ if test -f "$$p"; then d=; else d="$(srcdir)/"; fi; \
+ echo "$$d$$p"; \
+ done | $(am__base_list) | \
+ while read files; do \
+ echo " $(INSTALL_HEADER) $$files '$(DESTDIR)$(pkginc_libdir)'"; \
+ $(INSTALL_HEADER) $$files "$(DESTDIR)$(pkginc_libdir)" || exit $$?; \
+ done
+
+uninstall-pkginc_libHEADERS:
+ @$(NORMAL_UNINSTALL)
+ @list='$(pkginc_lib_HEADERS)'; test -n "$(pkginc_libdir)" || list=; \
+ files=`for p in $$list; do echo $$p; done | sed -e 's|^.*/||'`; \
+ dir='$(DESTDIR)$(pkginc_libdir)'; $(am__uninstall_files_from_dir)
+
+ID: $(am__tagged_files)
+ $(am__define_uniq_tagged_files); mkid -fID $$unique
+tags: tags-am
+TAGS: tags
+
+tags-am: $(TAGS_DEPENDENCIES) $(am__tagged_files)
+ set x; \
+ here=`pwd`; \
+ $(am__define_uniq_tagged_files); \
+ shift; \
+ if test -z "$(ETAGS_ARGS)$$*$$unique"; then :; else \
+ test -n "$$unique" || unique=$$empty_fix; \
+ if test $$# -gt 0; then \
+ $(ETAGS) $(ETAGSFLAGS) $(AM_ETAGSFLAGS) $(ETAGS_ARGS) \
+ "$$@" $$unique; \
+ else \
+ $(ETAGS) $(ETAGSFLAGS) $(AM_ETAGSFLAGS) $(ETAGS_ARGS) \
+ $$unique; \
+ fi; \
+ fi
+ctags: ctags-am
+
+CTAGS: ctags
+ctags-am: $(TAGS_DEPENDENCIES) $(am__tagged_files)
+ $(am__define_uniq_tagged_files); \
+ test -z "$(CTAGS_ARGS)$$unique" \
+ || $(CTAGS) $(CTAGSFLAGS) $(AM_CTAGSFLAGS) $(CTAGS_ARGS) \
+ $$unique
+
+GTAGS:
+ here=`$(am__cd) $(top_builddir) && pwd` \
+ && $(am__cd) $(top_srcdir) \
+ && gtags -i $(GTAGS_ARGS) "$$here"
+cscopelist: cscopelist-am
+
+cscopelist-am: $(am__tagged_files)
+ list='$(am__tagged_files)'; \
+ case "$(srcdir)" in \
+ [\\/]* | ?:[\\/]*) sdir="$(srcdir)" ;; \
+ *) sdir=$(subdir)/$(srcdir) ;; \
+ esac; \
+ for i in $$list; do \
+ if test -f "$$i"; then \
+ echo "$(subdir)/$$i"; \
+ else \
+ echo "$$sdir/$$i"; \
+ fi; \
+ done >> $(top_builddir)/cscope.files
+
+distclean-tags:
+ -rm -f TAGS ID GTAGS GRTAGS GSYMS GPATH tags
+
+distdir: $(BUILT_SOURCES)
+ $(MAKE) $(AM_MAKEFLAGS) distdir-am
+
+distdir-am: $(DISTFILES)
+ @srcdirstrip=`echo "$(srcdir)" | sed 's/[].[^$$\\*]/\\\\&/g'`; \
+ topsrcdirstrip=`echo "$(top_srcdir)" | sed 's/[].[^$$\\*]/\\\\&/g'`; \
+ list='$(DISTFILES)'; \
+ dist_files=`for file in $$list; do echo $$file; done | \
+ sed -e "s|^$$srcdirstrip/||;t" \
+ -e "s|^$$topsrcdirstrip/|$(top_builddir)/|;t"`; \
+ case $$dist_files in \
+ */*) $(MKDIR_P) `echo "$$dist_files" | \
+ sed '/\//!d;s|^|$(distdir)/|;s,/[^/]*$$,,' | \
+ sort -u` ;; \
+ esac; \
+ for file in $$dist_files; do \
+ if test -f $$file || test -d $$file; then d=.; else d=$(srcdir); fi; \
+ if test -d $$d/$$file; then \
+ dir=`echo "/$$file" | sed -e 's,/[^/]*$$,,'`; \
+ if test -d "$(distdir)/$$file"; then \
+ find "$(distdir)/$$file" -type d ! -perm -700 -exec chmod u+rwx {} \;; \
+ fi; \
+ if test -d $(srcdir)/$$file && test $$d != $(srcdir); then \
+ cp -fpR $(srcdir)/$$file "$(distdir)$$dir" || exit 1; \
+ find "$(distdir)/$$file" -type d ! -perm -700 -exec chmod u+rwx {} \;; \
+ fi; \
+ cp -fpR $$d/$$file "$(distdir)$$dir" || exit 1; \
+ else \
+ test -f "$(distdir)/$$file" \
+ || cp -p $$d/$$file "$(distdir)/$$file" \
+ || exit 1; \
+ fi; \
+ done
+check-am: all-am
+ $(MAKE) $(AM_MAKEFLAGS) check-local
+check: check-am
+all-am: Makefile $(PROGRAMS) $(LTLIBRARIES) $(HEADERS)
+installdirs:
+ for dir in "$(DESTDIR)$(pkginc_libdir)"; do \
+ test -z "$$dir" || $(MKDIR_P) "$$dir"; \
+ done
+install: install-am
+install-exec: install-exec-am
+install-data: install-data-am
+uninstall: uninstall-am
+
+install-am: all-am
+ @$(MAKE) $(AM_MAKEFLAGS) install-exec-am install-data-am
+
+installcheck: installcheck-am
+install-strip:
+ if test -z '$(STRIP)'; then \
+ $(MAKE) $(AM_MAKEFLAGS) INSTALL_PROGRAM="$(INSTALL_STRIP_PROGRAM)" \
+ install_sh_PROGRAM="$(INSTALL_STRIP_PROGRAM)" INSTALL_STRIP_FLAG=-s \
+ install; \
+ else \
+ $(MAKE) $(AM_MAKEFLAGS) INSTALL_PROGRAM="$(INSTALL_STRIP_PROGRAM)" \
+ install_sh_PROGRAM="$(INSTALL_STRIP_PROGRAM)" INSTALL_STRIP_FLAG=-s \
+ "INSTALL_PROGRAM_ENV=STRIPPROG='$(STRIP)'" install; \
+ fi
+mostlyclean-generic:
+
+clean-generic:
+
+distclean-generic:
+ -test -z "$(CONFIG_CLEAN_FILES)" || rm -f $(CONFIG_CLEAN_FILES)
+ -test . = "$(srcdir)" || test -z "$(CONFIG_CLEAN_VPATH_FILES)" || rm -f $(CONFIG_CLEAN_VPATH_FILES)
+
+maintainer-clean-generic:
+ @echo "This command is intended for maintainers to use"
+ @echo "it deletes files that may require special tools to rebuild."
+clean: clean-am
+
+clean-am: clean-generic clean-libtool clean-noinstLTLIBRARIES \
+ clean-noinstPROGRAMS mostlyclean-am
+
+distclean: distclean-am
+ -rm -f ./$(DEPDIR)/http-auth.Plo
+ -rm -f ./$(DEPDIR)/http-client-connection.Plo
+ -rm -f ./$(DEPDIR)/http-client-host.Plo
+ -rm -f ./$(DEPDIR)/http-client-peer.Plo
+ -rm -f ./$(DEPDIR)/http-client-queue.Plo
+ -rm -f ./$(DEPDIR)/http-client-request.Plo
+ -rm -f ./$(DEPDIR)/http-client.Plo
+ -rm -f ./$(DEPDIR)/http-date.Plo
+ -rm -f ./$(DEPDIR)/http-header-parser.Plo
+ -rm -f ./$(DEPDIR)/http-header.Plo
+ -rm -f ./$(DEPDIR)/http-message-parser.Plo
+ -rm -f ./$(DEPDIR)/http-parser.Plo
+ -rm -f ./$(DEPDIR)/http-request-parser.Plo
+ -rm -f ./$(DEPDIR)/http-request.Plo
+ -rm -f ./$(DEPDIR)/http-response-parser.Plo
+ -rm -f ./$(DEPDIR)/http-response.Plo
+ -rm -f ./$(DEPDIR)/http-server-connection.Plo
+ -rm -f ./$(DEPDIR)/http-server-ostream.Plo
+ -rm -f ./$(DEPDIR)/http-server-request.Plo
+ -rm -f ./$(DEPDIR)/http-server-resource.Plo
+ -rm -f ./$(DEPDIR)/http-server-response.Plo
+ -rm -f ./$(DEPDIR)/http-server.Plo
+ -rm -f ./$(DEPDIR)/http-transfer-chunked.Plo
+ -rm -f ./$(DEPDIR)/http-url.Plo
+ -rm -f ./$(DEPDIR)/test-http-auth.Po
+ -rm -f ./$(DEPDIR)/test-http-client-errors.Po
+ -rm -f ./$(DEPDIR)/test-http-client-request.Po
+ -rm -f ./$(DEPDIR)/test-http-client.Po
+ -rm -f ./$(DEPDIR)/test-http-date.Po
+ -rm -f ./$(DEPDIR)/test-http-header-parser.Po
+ -rm -f ./$(DEPDIR)/test-http-payload.Po
+ -rm -f ./$(DEPDIR)/test-http-request-parser.Po
+ -rm -f ./$(DEPDIR)/test-http-response-parser.Po
+ -rm -f ./$(DEPDIR)/test-http-server-errors.Po
+ -rm -f ./$(DEPDIR)/test-http-server.Po
+ -rm -f ./$(DEPDIR)/test-http-transfer.Po
+ -rm -f ./$(DEPDIR)/test-http-url.Po
+ -rm -f Makefile
+distclean-am: clean-am distclean-compile distclean-generic \
+ distclean-tags
+
+dvi: dvi-am
+
+dvi-am:
+
+html: html-am
+
+html-am:
+
+info: info-am
+
+info-am:
+
+install-data-am: install-pkginc_libHEADERS
+
+install-dvi: install-dvi-am
+
+install-dvi-am:
+
+install-exec-am:
+
+install-html: install-html-am
+
+install-html-am:
+
+install-info: install-info-am
+
+install-info-am:
+
+install-man:
+
+install-pdf: install-pdf-am
+
+install-pdf-am:
+
+install-ps: install-ps-am
+
+install-ps-am:
+
+installcheck-am:
+
+maintainer-clean: maintainer-clean-am
+ -rm -f ./$(DEPDIR)/http-auth.Plo
+ -rm -f ./$(DEPDIR)/http-client-connection.Plo
+ -rm -f ./$(DEPDIR)/http-client-host.Plo
+ -rm -f ./$(DEPDIR)/http-client-peer.Plo
+ -rm -f ./$(DEPDIR)/http-client-queue.Plo
+ -rm -f ./$(DEPDIR)/http-client-request.Plo
+ -rm -f ./$(DEPDIR)/http-client.Plo
+ -rm -f ./$(DEPDIR)/http-date.Plo
+ -rm -f ./$(DEPDIR)/http-header-parser.Plo
+ -rm -f ./$(DEPDIR)/http-header.Plo
+ -rm -f ./$(DEPDIR)/http-message-parser.Plo
+ -rm -f ./$(DEPDIR)/http-parser.Plo
+ -rm -f ./$(DEPDIR)/http-request-parser.Plo
+ -rm -f ./$(DEPDIR)/http-request.Plo
+ -rm -f ./$(DEPDIR)/http-response-parser.Plo
+ -rm -f ./$(DEPDIR)/http-response.Plo
+ -rm -f ./$(DEPDIR)/http-server-connection.Plo
+ -rm -f ./$(DEPDIR)/http-server-ostream.Plo
+ -rm -f ./$(DEPDIR)/http-server-request.Plo
+ -rm -f ./$(DEPDIR)/http-server-resource.Plo
+ -rm -f ./$(DEPDIR)/http-server-response.Plo
+ -rm -f ./$(DEPDIR)/http-server.Plo
+ -rm -f ./$(DEPDIR)/http-transfer-chunked.Plo
+ -rm -f ./$(DEPDIR)/http-url.Plo
+ -rm -f ./$(DEPDIR)/test-http-auth.Po
+ -rm -f ./$(DEPDIR)/test-http-client-errors.Po
+ -rm -f ./$(DEPDIR)/test-http-client-request.Po
+ -rm -f ./$(DEPDIR)/test-http-client.Po
+ -rm -f ./$(DEPDIR)/test-http-date.Po
+ -rm -f ./$(DEPDIR)/test-http-header-parser.Po
+ -rm -f ./$(DEPDIR)/test-http-payload.Po
+ -rm -f ./$(DEPDIR)/test-http-request-parser.Po
+ -rm -f ./$(DEPDIR)/test-http-response-parser.Po
+ -rm -f ./$(DEPDIR)/test-http-server-errors.Po
+ -rm -f ./$(DEPDIR)/test-http-server.Po
+ -rm -f ./$(DEPDIR)/test-http-transfer.Po
+ -rm -f ./$(DEPDIR)/test-http-url.Po
+ -rm -f Makefile
+maintainer-clean-am: distclean-am maintainer-clean-generic
+
+mostlyclean: mostlyclean-am
+
+mostlyclean-am: mostlyclean-compile mostlyclean-generic \
+ mostlyclean-libtool
+
+pdf: pdf-am
+
+pdf-am:
+
+ps: ps-am
+
+ps-am:
+
+uninstall-am: uninstall-pkginc_libHEADERS
+
+.MAKE: check-am install-am install-strip
+
+.PHONY: CTAGS GTAGS TAGS all all-am am--depfiles check check-am \
+ check-local clean clean-generic clean-libtool \
+ clean-noinstLTLIBRARIES clean-noinstPROGRAMS cscopelist-am \
+ ctags ctags-am distclean distclean-compile distclean-generic \
+ distclean-libtool distclean-tags distdir dvi dvi-am html \
+ html-am info info-am install install-am install-data \
+ install-data-am install-dvi install-dvi-am install-exec \
+ install-exec-am install-html install-html-am install-info \
+ install-info-am install-man install-pdf install-pdf-am \
+ install-pkginc_libHEADERS install-ps install-ps-am \
+ install-strip installcheck installcheck-am installdirs \
+ maintainer-clean maintainer-clean-generic mostlyclean \
+ mostlyclean-compile mostlyclean-generic mostlyclean-libtool \
+ pdf pdf-am ps ps-am tags tags-am uninstall uninstall-am \
+ uninstall-pkginc_libHEADERS
+
+.PRECIOUS: Makefile
+
+
+check-local:
+ for bin in $(test_programs); do \
+ if ! $(RUN_TEST) ./$$bin; then exit 1; fi; \
+ done
+
+# Tell versions [3.59,3.63) of GNU make to not export all variables.
+# Otherwise a system limit (for SysV at least) may be exceeded.
+.NOEXPORT:
diff --git a/src/lib-http/http-auth.c b/src/lib-http/http-auth.c
new file mode 100644
index 0000000..67b7f6b
--- /dev/null
+++ b/src/lib-http/http-auth.c
@@ -0,0 +1,476 @@
+/* Copyright (c) 2013-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "str.h"
+#include "base64.h"
+#include "array.h"
+#include "http-parser.h"
+
+#include "http-auth.h"
+
+/* RFC 7235, Section 2.1:
+
+ challenge = auth-scheme [ 1*SP ( token68 / #auth-param ) ]
+ credentials = auth-scheme [ 1*SP ( token68 / #auth-param ) ]
+
+ auth-scheme = token
+ auth-param = token BWS "=" BWS ( token / quoted-string )
+ token68 = 1*( ALPHA / DIGIT /
+ "-" / "." / "_" / "~" / "+" / "/" ) *"="
+
+ OWS = *( SP / HTAB )
+ ; optional whitespace
+ BWS = OWS
+ ; "bad" whitespace
+ */
+
+/*
+ * Parsing
+ */
+
+static int
+http_parse_token68(struct http_parser *parser, const char **token68_r)
+{
+ const unsigned char *first;
+
+ /* token68 = 1*( ALPHA / DIGIT /
+ "-" / "." / "_" / "~" / "+" / "/" ) *"="
+ */
+
+ /* 1*( ALPHA / DIGIT / "-" / "." / "_" / "~" / "+" / "/" ) */
+ if (parser->cur >= parser->end || !http_char_is_token68(*parser->cur))
+ return 0;
+ first = parser->cur++;
+ while (parser->cur < parser->end && http_char_is_token68(*parser->cur))
+ parser->cur++;
+
+ /* *"=" */
+ while (parser->cur < parser->end && *parser->cur == '=')
+ parser->cur++;
+
+ *token68_r = t_strndup(first, parser->cur - first);
+ return 1;
+}
+
+static int
+http_parse_auth_param(struct http_parser *parser,
+ const char **param_r, const char **value_r)
+{
+ const unsigned char *first = parser->cur, *end_token;
+ int ret;
+
+ /* auth-param = token BWS "=" BWS ( token / quoted-string ) */
+
+ /* token */
+ if ((ret=http_parser_skip_token(parser)) <= 0) {
+ parser->cur = first;
+ return ret;
+ }
+ end_token = parser->cur;
+
+ /* BWS "=" BWS */
+ http_parse_ows(parser);
+ if (parser->cur >= parser->end || *parser->cur != '=') {
+ parser->cur = first;
+ return 0;
+ }
+ parser->cur++;
+ http_parse_ows(parser);
+
+ /* ( token / quoted-string ) */
+ if ((ret=http_parse_token_or_qstring(parser, value_r)) <= 0) {
+ parser->cur = first;
+ return ret;
+ }
+
+ *param_r = t_strndup(first, end_token - first);
+ return 1;
+}
+
+static int
+http_parse_auth_params(struct http_parser *parser,
+ ARRAY_TYPE(http_auth_param) *params)
+{
+ const unsigned char *last = parser->cur;
+ struct http_auth_param param;
+ unsigned int count = 0;
+ int ret;
+
+ i_zero(&param);
+ while ((ret=http_parse_auth_param
+ (parser, &param.name, &param.value)) > 0) {
+ if (!array_is_created(params))
+ t_array_init(params, 4);
+ array_push_back(params, &param);
+ count++;
+
+ last = parser->cur;
+
+ /* OWS "," OWS
+ --> also allow empty elements
+ */
+ for (;;) {
+ http_parse_ows(parser);
+ if (parser->cur >= parser->end || *parser->cur != ',')
+ break;
+ parser->cur++;
+ }
+ }
+
+ parser->cur = last;
+ if (ret < 0)
+ return -1;
+ return (count > 0 ? 1 : 0);
+}
+
+int http_auth_parse_challenges(const unsigned char *data, size_t size,
+ ARRAY_TYPE(http_auth_challenge) *chlngs)
+{
+ struct http_parser parser;
+ int ret;
+
+ http_parser_init(&parser, data, size);
+
+ /* WWW-Authenticate = 1#challenge
+ Proxy-Authenticate = 1#challenge
+
+ challenge = auth-scheme [ 1*SP ( token68 / #auth-param ) ]
+ auth-scheme = token
+ */
+
+ /* 1#element => *( "," OWS ) ... ; RFC 7230, Section 7 */
+ for (;;) {
+ if (parser.cur >= parser.end || *parser.cur != ',')
+ break;
+ parser.cur++;
+ http_parse_ows(&parser);
+ }
+
+ for (;;) {
+ struct http_auth_challenge chlng;
+
+ i_zero(&chlng);
+
+ /* auth-scheme */
+ if ((ret=http_parse_token(&parser, &chlng.scheme)) <= 0) {
+ if (ret < 0)
+ return -1;
+ break;
+ }
+
+ /* [ 1*SP ... ] */
+ if (parser.cur >= parser.end || *parser.cur != ' ')
+ return 1;
+ parser.cur++;
+ while (parser.cur < parser.end && *parser.cur == ' ')
+ parser.cur++;
+
+ /* ( token68 / #auth-param ) */
+ if ((ret=http_parse_auth_params(&parser, &chlng.params)) <= 0) {
+ if (ret < 0)
+ return -1;
+ if (http_parse_token68(&parser, &chlng.data) < 0)
+ return -1;
+ }
+
+ if (!array_is_created(chlngs))
+ t_array_init(chlngs, 4);
+ array_push_back(chlngs, &chlng);
+
+ /* OWS "," OWS
+ --> also allow empty elements
+ */
+ for (;;) {
+ http_parse_ows(&parser);
+ if (parser.cur >= parser.end || *parser.cur != ',')
+ break;
+ parser.cur++;
+ }
+ }
+
+ if (parser.cur != parser.end)
+ return -1;
+ return 1;
+}
+
+int http_auth_parse_credentials(const unsigned char *data, size_t size,
+ struct http_auth_credentials *crdts)
+{
+ struct http_parser parser;
+ int ret;
+
+ http_parser_init(&parser, data, size);
+
+ /* Authorization = credentials
+ Proxy-Authorization = credentials
+
+ credentials = auth-scheme [ 1*SP ( token68 / #auth-param ) ]
+ auth-scheme = token
+ */
+
+ i_zero(crdts);
+
+ /* auth-scheme */
+ if (http_parse_token(&parser, &crdts->scheme) <= 0)
+ return -1;
+
+ /* [ 1*SP ... ] */
+ if (parser.cur >= parser.end || *parser.cur != ' ')
+ return 1;
+ parser.cur++;
+ while (parser.cur < parser.end && *parser.cur == ' ')
+ parser.cur++;
+
+ /* ( token68 / #auth-param ) */
+ if ((ret=http_parse_auth_params(&parser, &crdts->params)) <= 0) {
+ if (ret < 0)
+ return -1;
+ if (http_parse_token68(&parser, &crdts->data) < 0)
+ return -1;
+ }
+
+ if (parser.cur != parser.end)
+ return -1;
+ return 1;
+}
+
+/*
+ * Construction
+ */
+
+static void
+http_auth_create_param(string_t *out, const struct http_auth_param *param)
+{
+ const char *p, *first;
+
+ /* auth-param = token BWS "=" BWS ( token / quoted-string ) */
+
+ str_append(out, param->name);
+ str_append_c(out, '=');
+
+ for (p = param->value; *p != '\0' && http_char_is_token(*p); p++);
+
+ if ( *p != '\0' ) {
+ str_append_c(out, '"');
+ p = first = param->value;
+ while (*p != '\0') {
+ if (*p == '\\' || *p == '"') {
+ str_append_data(out, first, p-first);
+ str_append_c(out, '\\');
+ first = p;
+ }
+ p++;
+ }
+ str_append_data(out, first, p-first);
+ str_append_c(out, '"');
+ } else {
+ str_append(out, param->value);
+ }
+}
+
+static void
+http_auth_create_params(string_t *out,
+ const ARRAY_TYPE(http_auth_param) *params)
+{
+ const struct http_auth_param *prms;
+ unsigned int count, i;
+
+ if (!array_is_created(params))
+ return;
+
+ prms = array_get(params, &count);
+ for (i = 0; i < count; i++) {
+ if (i > 0)
+ str_append(out, ", ");
+ http_auth_create_param(out, &prms[i]);
+ }
+}
+
+static void http_auth_check_token68(const char *data)
+{
+ const char *p = data;
+
+ /* Make sure we're not working with nonsense. */
+ i_assert(http_char_is_token68(*p));
+ for (p++; *p != '\0' && *p != '='; p++)
+ i_assert(http_char_is_token68(*p));
+ for (; *p != '\0'; p++)
+ i_assert(*p == '=');
+}
+
+void http_auth_create_challenge(string_t *out,
+ const struct http_auth_challenge *chlng)
+{
+ /* challenge = auth-scheme [ 1*SP ( token68 / #auth-param ) ]
+ auth-scheme = token
+ */
+
+ /* auth-scheme */
+ str_append(out, chlng->scheme);
+
+ if (chlng->data != NULL) {
+ /* SP token68 */
+ http_auth_check_token68(chlng->data);
+ str_append_c(out, ' ');
+ str_append(out, chlng->data);
+
+ } else {
+ /* SP #auth-param */
+ str_append_c(out, ' ');
+ http_auth_create_params(out, &chlng->params);
+ }
+}
+
+void http_auth_create_challenges(string_t *out,
+ const ARRAY_TYPE(http_auth_challenge) *chlngs)
+{
+ const struct http_auth_challenge *chlgs;
+ unsigned int count, i;
+
+ /* WWW-Authenticate = 1#challenge
+ Proxy-Authenticate = 1#challenge
+ */
+ chlgs = array_get(chlngs, &count);
+ for (i = 0; i < count; i++) {
+ if (i > 0)
+ str_append(out, ", ");
+ http_auth_create_challenge(out, &chlgs[i]);
+ }
+}
+
+void http_auth_create_credentials(string_t *out,
+ const struct http_auth_credentials *crdts)
+{
+ /* Authorization = credentials
+ Proxy-Authorization = credentials
+
+ credentials = auth-scheme [ 1*SP ( token68 / #auth-param ) ]
+ auth-scheme = token
+ */
+
+ /* auth-scheme */
+ str_append(out, crdts->scheme);
+
+ if (crdts->data != NULL) {
+ /* SP token68 */
+ http_auth_check_token68(crdts->data);
+ str_append_c(out, ' ');
+ str_append(out, crdts->data);
+
+ } else {
+ /* SP #auth-param */
+ str_append_c(out, ' ');
+ http_auth_create_params(out, &crdts->params);
+ }
+}
+
+/*
+ * Manipulation
+ */
+
+static void
+http_auth_params_clone(pool_t pool,
+ ARRAY_TYPE(http_auth_param) *dst,
+ const ARRAY_TYPE(http_auth_param) *src)
+{
+ const struct http_auth_param *sparam;
+
+ if (!array_is_created(src))
+ return;
+
+ p_array_init(dst, pool, 4);
+ array_foreach(src, sparam) {
+ struct http_auth_param nparam;
+
+ i_zero(&nparam);
+ nparam.name = p_strdup(pool, sparam->name);
+ nparam.value = p_strdup(pool, sparam->value);
+
+ array_push_back(dst, &nparam);
+ }
+}
+
+void http_auth_challenge_copy(pool_t pool,
+ struct http_auth_challenge *dst,
+ const struct http_auth_challenge *src)
+{
+ dst->scheme = p_strdup(pool, src->scheme);
+ if (src->data != NULL)
+ dst->data = p_strdup(pool, src->data);
+ else
+ http_auth_params_clone(pool, &dst->params, &src->params);
+}
+
+struct http_auth_challenge *
+http_auth_challenge_clone(pool_t pool,
+ const struct http_auth_challenge *src)
+{
+ struct http_auth_challenge *new;
+
+ new = p_new(pool, struct http_auth_challenge, 1);
+ http_auth_challenge_copy(pool, new, src);
+
+ return new;
+}
+
+void http_auth_credentials_copy(pool_t pool,
+ struct http_auth_credentials *dst,
+ const struct http_auth_credentials *src)
+{
+ dst->scheme = p_strdup(pool, src->scheme);
+ if (src->data != NULL)
+ dst->data = p_strdup(pool, src->data);
+ else
+ http_auth_params_clone(pool, &dst->params, &src->params);
+}
+
+struct http_auth_credentials *
+http_auth_credentials_clone(pool_t pool,
+ const struct http_auth_credentials *src)
+{
+ struct http_auth_credentials *new;
+
+ new = p_new(pool, struct http_auth_credentials, 1);
+ http_auth_credentials_copy(pool, new, src);
+
+ return new;
+}
+
+/*
+ * Simple schemes
+ */
+
+void http_auth_basic_challenge_init(struct http_auth_challenge *chlng,
+ const char *realm)
+{
+ i_zero(chlng);
+ chlng->scheme = "Basic";
+ if (realm != NULL) {
+ struct http_auth_param param;
+
+ i_zero(&param);
+ param.name = "realm";
+ param.value = t_strdup(realm);
+
+ t_array_init(&chlng->params, 1);
+ array_push_back(&chlng->params, &param);
+ }
+}
+
+void http_auth_basic_credentials_init(struct http_auth_credentials *crdts,
+ const char *username, const char *password)
+{
+ const char *auth;
+ string_t *data;
+
+ i_assert(username != NULL && *username != '\0');
+ i_assert(strchr(username, ':') == NULL);
+
+ data = t_str_new(64);
+ auth = t_strconcat(username, ":", password, NULL);
+ base64_encode(auth, strlen(auth), data);
+
+ i_zero(crdts);
+ crdts->scheme = "Basic";
+ crdts->data = str_c(data);
+}
diff --git a/src/lib-http/http-auth.h b/src/lib-http/http-auth.h
new file mode 100644
index 0000000..061f9ce
--- /dev/null
+++ b/src/lib-http/http-auth.h
@@ -0,0 +1,79 @@
+#ifndef HTTP_AUTH_H
+#define HTTP_AUTH_H
+
+#include "array-decl.h"
+
+struct http_auth_param;
+struct http_auth_challenge;
+struct http_auth_credentials;
+
+ARRAY_DEFINE_TYPE(http_auth_param, struct http_auth_param);
+ARRAY_DEFINE_TYPE(http_auth_challenge, struct http_auth_challenge);
+
+struct http_auth_param {
+ const char *name;
+ const char *value;
+};
+
+struct http_auth_challenge {
+ const char *scheme;
+ const char *data;
+ ARRAY_TYPE(http_auth_param) params;
+};
+
+struct http_auth_credentials {
+ const char *scheme;
+ const char *data;
+ ARRAY_TYPE(http_auth_param) params;
+};
+
+/*
+ * Parsing
+ */
+
+int http_auth_parse_challenges(const unsigned char *data, size_t size,
+ ARRAY_TYPE(http_auth_challenge) *chlngs);
+int http_auth_parse_credentials(const unsigned char *data, size_t size,
+ struct http_auth_credentials *crdts);
+
+/*
+ * Construction
+ */
+
+void http_auth_create_challenge(string_t *out,
+ const struct http_auth_challenge *chlng);
+void http_auth_create_challenges(string_t *out,
+ const ARRAY_TYPE(http_auth_challenge) *chlngs);
+
+void http_auth_create_credentials(string_t *out,
+ const struct http_auth_credentials *crdts);
+
+/*
+ * Manipulation
+ */
+
+void http_auth_challenge_copy(pool_t pool,
+ struct http_auth_challenge *dst,
+ const struct http_auth_challenge *src);
+struct http_auth_challenge *
+http_auth_challenge_clone(pool_t pool,
+ const struct http_auth_challenge *src);
+
+void http_auth_credentials_copy(pool_t pool,
+ struct http_auth_credentials *dst,
+ const struct http_auth_credentials *src);
+struct http_auth_credentials *
+http_auth_credentials_clone(pool_t pool,
+ const struct http_auth_credentials *src);
+
+/*
+ * Simple schemes
+ */
+
+void http_auth_basic_challenge_init(struct http_auth_challenge *chlng,
+ const char *realm);
+void http_auth_basic_credentials_init(struct http_auth_credentials *crdts,
+ const char *username, const char *password);
+
+#endif
+
diff --git a/src/lib-http/http-client-connection.c b/src/lib-http/http-client-connection.c
new file mode 100644
index 0000000..45dadac
--- /dev/null
+++ b/src/lib-http/http-client-connection.c
@@ -0,0 +1,1954 @@
+/* Copyright (c) 2013-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "net.h"
+#include "str.h"
+#include "hash.h"
+#include "llist.h"
+#include "array.h"
+#include "ioloop.h"
+#include "istream.h"
+#include "istream-timeout.h"
+#include "ostream.h"
+#include "time-util.h"
+#include "file-lock.h"
+#include "iostream-rawlog.h"
+#include "iostream-ssl.h"
+#include "http-response-parser.h"
+
+#include "http-client-private.h"
+
+/*
+ * Connection
+ */
+
+static void http_client_connection_ready(struct http_client_connection *conn);
+static void http_client_connection_input(struct connection *_conn);
+static void
+http_client_connection_disconnect(struct http_client_connection *conn);
+
+static inline const struct http_client_settings *
+http_client_connection_get_settings(struct http_client_connection *conn)
+{
+ if (conn->peer != NULL)
+ return &conn->peer->client->set;
+ return &conn->ppool->peer->cctx->set;
+}
+
+static inline void
+http_client_connection_ref_request(struct http_client_connection *conn,
+ struct http_client_request *req)
+{
+ i_assert(req->conn == NULL);
+ req->conn = conn;
+ http_client_request_ref(req);
+}
+
+static inline bool
+http_client_connection_unref_request(struct http_client_connection *conn,
+ struct http_client_request **_req)
+{
+ struct http_client_request *req = *_req;
+
+ i_assert(req->conn == conn);
+ req->conn = NULL;
+ return http_client_request_unref(_req);
+}
+
+static void
+http_client_connection_unlist_pending(struct http_client_connection *conn)
+{
+ struct http_client_peer *peer = conn->peer;
+ struct http_client_peer_pool *ppool = conn->ppool;
+ ARRAY_TYPE(http_client_connection) *conn_arr;
+ struct http_client_connection *const *conn_idx;
+
+ /* Remove from pending lists */
+
+ conn_arr = &ppool->pending_conns;
+ array_foreach(conn_arr, conn_idx) {
+ if (*conn_idx == conn) {
+ array_delete(conn_arr,
+ array_foreach_idx(conn_arr, conn_idx), 1);
+ break;
+ }
+ }
+
+ if (peer == NULL)
+ return;
+
+ conn_arr = &peer->pending_conns;
+ array_foreach(conn_arr, conn_idx) {
+ if (*conn_idx == conn) {
+ array_delete(conn_arr,
+ array_foreach_idx(conn_arr, conn_idx), 1);
+ break;
+ }
+ }
+}
+
+static inline void
+http_client_connection_failure(struct http_client_connection *conn,
+ const char *reason)
+{
+ struct http_client_peer *peer = conn->peer;
+
+ conn->connect_failed = TRUE;
+ http_client_connection_unlist_pending(conn);
+ http_client_peer_connection_failure(peer, reason);
+}
+
+unsigned int
+http_client_connection_count_pending(struct http_client_connection *conn)
+{
+ unsigned int pending_count = array_count(&conn->request_wait_list);
+
+ if (conn->in_req_callback || conn->pending_request != NULL)
+ pending_count++;
+ return pending_count;
+}
+
+bool http_client_connection_is_idle(struct http_client_connection *conn)
+{
+ return conn->idle;
+}
+
+bool http_client_connection_is_active(struct http_client_connection *conn)
+{
+ if (!conn->connected)
+ return FALSE;
+
+ if (conn->in_req_callback || conn->pending_request != NULL)
+ return TRUE;
+
+ return (array_is_created(&conn->request_wait_list) &&
+ array_count(&conn->request_wait_list) > 0);
+}
+
+static void
+http_client_connection_retry_requests(struct http_client_connection *conn,
+ unsigned int status, const char *error)
+{
+ struct http_client_request *req, **req_idx;
+
+ if (!array_is_created(&conn->request_wait_list))
+ return;
+
+ e_debug(conn->event, "Retrying pending requests");
+
+ array_foreach_modifiable(&conn->request_wait_list, req_idx) {
+ req = *req_idx;
+ /* Drop reference from connection */
+ if (!http_client_connection_unref_request(conn, req_idx))
+ continue;
+ /* Retry the request, which may drop it */
+ if (req->state < HTTP_REQUEST_STATE_FINISHED)
+ http_client_request_retry(req, status, error);
+ }
+ array_clear(&conn->request_wait_list);
+}
+
+static void
+http_client_connection_server_close(struct http_client_connection **_conn)
+{
+ struct http_client_connection *conn = *_conn;
+ struct http_client_peer *peer = conn->peer;
+ struct http_client_request *req, **req_idx;
+
+ e_debug(conn->event, "Server explicitly closed connection");
+
+ array_foreach_modifiable(&conn->request_wait_list, req_idx) {
+ req = *req_idx;
+ /* Drop reference from connection */
+ if (!http_client_connection_unref_request(conn, req_idx))
+ continue;
+ /* Resubmit the request, which may drop it */
+ if (req->state < HTTP_REQUEST_STATE_FINISHED)
+ http_client_request_resubmit(req);
+ }
+ array_clear(&conn->request_wait_list);
+
+ if (peer != NULL) {
+ struct http_client *client = peer->client;
+
+ if (client->waiting)
+ io_loop_stop(client->ioloop);
+ }
+
+ http_client_connection_close(_conn);
+}
+
+static void
+http_client_connection_abort_error(struct http_client_connection **_conn,
+ unsigned int status, const char *error)
+{
+ struct http_client_connection *conn = *_conn;
+ struct http_client_request *req, **req_idx;
+
+ e_debug(conn->event, "Aborting connection: %s", error);
+
+ array_foreach_modifiable(&conn->request_wait_list, req_idx) {
+ req = *req_idx;
+ i_assert(req->submitted);
+ /* Drop reference from connection */
+ if (!http_client_connection_unref_request(conn, req_idx))
+ continue;
+ /* Drop request if not already aborted */
+ http_client_request_error(&req, status, error);
+ }
+ array_clear(&conn->request_wait_list);
+ http_client_connection_close(_conn);
+}
+
+static void
+http_client_connection_abort_any_requests(struct http_client_connection *conn)
+{
+ struct http_client_request *req, **req_idx;
+
+ if (array_is_created(&conn->request_wait_list)) {
+ array_foreach_modifiable(&conn->request_wait_list, req_idx) {
+ req = *req_idx;
+ i_assert(req->submitted);
+ /* Drop reference from connection */
+ if (!http_client_connection_unref_request(conn, req_idx))
+ continue;
+ /* Drop request if not already aborted */
+ http_client_request_error(
+ &req, HTTP_CLIENT_REQUEST_ERROR_ABORTED,
+ "Aborting");
+ }
+ array_clear(&conn->request_wait_list);
+ }
+ if (conn->pending_request != NULL) {
+ req = conn->pending_request;
+ /* Drop reference from connection */
+ if (http_client_connection_unref_request(
+ conn, &conn->pending_request)) {
+ /* Drop request if not already aborted */
+ http_client_request_error(
+ &req, HTTP_CLIENT_REQUEST_ERROR_ABORTED,
+ "Aborting");
+ }
+ }
+}
+
+static const char *
+http_client_connection_get_timing_info(struct http_client_connection *conn)
+{
+ struct http_client_request *const *requestp;
+ unsigned int connected_msecs;
+ string_t *str = t_str_new(64);
+
+ if (array_count(&conn->request_wait_list) > 0) {
+ requestp = array_front(&conn->request_wait_list);
+
+ str_append(str, "Request ");
+ http_client_request_append_stats_text(*requestp, str);
+ } else {
+ str_append(str, "No requests");
+ if (conn->conn.last_input != 0) {
+ str_printfa(str, ", last input %d secs ago",
+ (int)(ioloop_time - conn->conn.last_input));
+ }
+ }
+ connected_msecs = timeval_diff_msecs(&ioloop_timeval,
+ &conn->connected_timestamp);
+ str_printfa(str, ", connected %u.%03u secs ago",
+ connected_msecs/1000, connected_msecs%1000);
+ return str_c(str);
+}
+
+static void
+http_client_connection_abort_temp_error(struct http_client_connection **_conn,
+ unsigned int status, const char *error)
+{
+ struct http_client_connection *conn = *_conn;
+
+ error = t_strdup_printf("%s (%s)", error,
+ http_client_connection_get_timing_info(conn));
+
+ e_debug(conn->event,
+ "Aborting connection with temporary error: %s", error);
+
+ http_client_connection_disconnect(conn);
+ http_client_connection_retry_requests(conn, status, error);
+ http_client_connection_close(_conn);
+}
+
+void http_client_connection_lost(struct http_client_connection **_conn,
+ const char *error)
+{
+ struct http_client_connection *conn = *_conn;
+ const char *sslerr;
+
+ if (error == NULL)
+ error = "Connection lost";
+ else
+ error = t_strdup_printf("Connection lost: %s", error);
+
+ if (conn->ssl_iostream != NULL) {
+ sslerr = ssl_iostream_get_last_error(conn->ssl_iostream);
+ if (sslerr != NULL) {
+ error = t_strdup_printf("%s (last SSL error: %s)",
+ error, sslerr);
+ }
+ if (ssl_iostream_has_handshake_failed(conn->ssl_iostream)) {
+ /* This isn't really a "connection lost", but that we
+ don't trust the remote's SSL certificate. don't
+ retry. */
+ http_client_connection_abort_error(
+ _conn,
+ HTTP_CLIENT_REQUEST_ERROR_BAD_RESPONSE, error);
+ return;
+ }
+ }
+
+ conn->lost_prematurely =
+ (conn->conn.input != NULL &&
+ conn->conn.input->v_offset == 0 &&
+ i_stream_get_data_size(conn->conn.input) == 0);
+ http_client_connection_abort_temp_error(
+ _conn, HTTP_CLIENT_REQUEST_ERROR_CONNECTION_LOST, error);
+}
+
+void http_client_connection_handle_output_error(
+ struct http_client_connection *conn)
+{
+ struct ostream *output = conn->conn.output;
+
+ if (output->stream_errno != EPIPE &&
+ output->stream_errno != ECONNRESET) {
+ http_client_connection_lost(
+ &conn,
+ t_strdup_printf("write(%s) failed: %s",
+ o_stream_get_name(output),
+ o_stream_get_error(output)));
+ } else {
+ http_client_connection_lost(&conn, "Remote disconnected");
+ }
+}
+
+int http_client_connection_check_ready(struct http_client_connection *conn)
+{
+ const struct http_client_settings *set =
+ http_client_connection_get_settings(conn);
+
+ if (conn->in_req_callback) {
+ /* This can happen when a nested ioloop is created inside
+ request callback. we currently don't reuse connections that
+ are occupied this way, but theoretically we could, although
+ that would add quite a bit of complexity.
+ */
+ return 0;
+ }
+
+ if (!conn->connected || conn->output_locked || conn->output_broken ||
+ conn->close_indicated || conn->tunneling ||
+ (http_client_connection_count_pending(conn) >=
+ set->max_pipelined_requests))
+ return 0;
+
+ if (conn->last_ioloop != NULL && conn->last_ioloop != current_ioloop) {
+ conn->last_ioloop = current_ioloop;
+ /* Active ioloop is different from what we saw earlier; we may
+ have missed a disconnection event on this connection. Verify
+ status by reading from connection. */
+ if (i_stream_read(conn->conn.input) == -1) {
+ int stream_errno = conn->conn.input->stream_errno;
+
+ i_assert(conn->conn.input->stream_errno != 0 ||
+ conn->conn.input->eof);
+ http_client_connection_lost(
+ &conn,
+ t_strdup_printf(
+ "read(%s) failed: %s",
+ i_stream_get_name(conn->conn.input),
+ (stream_errno != 0 ?
+ i_stream_get_error(conn->conn.input) :
+ "EOF")));
+ return -1;
+ }
+
+ /* We may have read some data */
+ if (i_stream_get_data_size(conn->conn.input) > 0)
+ i_stream_set_input_pending(conn->conn.input, TRUE);
+ }
+ return 1;
+}
+
+static void
+http_client_connection_detach_peer(struct http_client_connection *conn)
+{
+ struct http_client_peer *peer = conn->peer;
+ struct http_client_connection *const *conn_idx;
+ ARRAY_TYPE(http_client_connection) *conn_arr;
+ bool found = FALSE;
+
+ if (peer == NULL)
+ return;
+
+ http_client_peer_ref(peer);
+ conn_arr = &peer->conns;
+ array_foreach(conn_arr, conn_idx) {
+ if (*conn_idx == conn) {
+ array_delete(conn_arr,
+ array_foreach_idx(conn_arr, conn_idx), 1);
+ found = TRUE;
+ break;
+ }
+ }
+ i_assert(found);
+
+ conn_arr = &peer->pending_conns;
+ array_foreach(conn_arr, conn_idx) {
+ if (*conn_idx == conn) {
+ array_delete(conn_arr,
+ array_foreach_idx(conn_arr, conn_idx), 1);
+ break;
+ }
+ }
+
+ conn->peer = NULL;
+ e_debug(conn->event, "Detached peer");
+
+ if (conn->connect_succeeded)
+ http_client_peer_connection_lost(peer, conn->lost_prematurely);
+ http_client_peer_unref(&peer);
+}
+
+static void
+http_client_connection_idle_timeout(struct http_client_connection *conn)
+{
+ e_debug(conn->event, "Idle connection timed out");
+
+ /* Cannot get here unless connection was established at some point */
+ i_assert(conn->connect_succeeded);
+
+ http_client_connection_close(&conn);
+}
+
+static unsigned int
+http_client_connection_start_idle_timeout(struct http_client_connection *conn)
+{
+ const struct http_client_settings *set =
+ http_client_connection_get_settings(conn);
+ struct http_client_peer_pool *ppool = conn->ppool;
+ struct http_client_peer_shared *pshared = ppool->peer;
+ unsigned int timeout, count, idle_count, max;
+
+ i_assert(conn->to_idle == NULL);
+
+ if (set->max_idle_time_msecs == 0)
+ return UINT_MAX;
+
+ count = array_count(&ppool->conns);
+ idle_count = array_count(&ppool->idle_conns);
+ max = http_client_peer_shared_max_connections(pshared);
+ i_assert(count > 0);
+ i_assert(count >= idle_count + 1);
+ i_assert(max > 0);
+
+ /* Set timeout for this connection */
+ if (idle_count == 0 || max == UINT_MAX) {
+ /* No idle connections yet or infinite connections allowed;
+ use the maximum idle time. */
+ timeout = set->max_idle_time_msecs;
+ } else if (count > max || idle_count >= max) {
+ /* Instant death for (urgent) connections above limit */
+ timeout = 0;
+ } else {
+ unsigned int idle_slots_avail;
+ double idle_time_per_slot;
+
+ /* Kill duplicate connections quicker;
+ linearly based on the number of connections */
+ idle_slots_avail = max - idle_count;
+ idle_time_per_slot = (double)set->max_idle_time_msecs / max;
+ timeout = (unsigned int)(idle_time_per_slot * idle_slots_avail);
+ if (timeout < HTTP_CLIENT_MIN_IDLE_TIMEOUT_MSECS)
+ timeout = HTTP_CLIENT_MIN_IDLE_TIMEOUT_MSECS;
+ }
+
+ conn->to_idle = timeout_add_short_to(
+ conn->conn.ioloop, timeout,
+ http_client_connection_idle_timeout, conn);
+ return timeout;
+}
+
+static void
+http_client_connection_start_idle(struct http_client_connection *conn,
+ const char *reason)
+{
+ struct http_client_peer_pool *ppool = conn->ppool;
+ unsigned int timeout;
+
+ if (conn->idle) {
+ e_debug(conn->event, "%s; already idle", reason);
+ return;
+ }
+
+ timeout = http_client_connection_start_idle_timeout(conn);
+ if (timeout == UINT_MAX)
+ e_debug(conn->event, "%s; going idle", reason);
+ else {
+ e_debug(conn->event, "%s; going idle (timeout = %u msecs)",
+ reason, timeout);
+ }
+
+ conn->idle = TRUE;
+ array_push_back(&ppool->idle_conns, &conn);
+}
+
+void http_client_connection_lost_peer(struct http_client_connection *conn)
+{
+ if (!conn->connected) {
+ http_client_connection_unref(&conn);
+ return;
+ }
+
+ i_assert(!conn->in_req_callback);
+
+ http_client_connection_start_idle(conn, "Lost peer");
+ http_client_connection_detach_peer(conn);
+}
+
+void http_client_connection_check_idle(struct http_client_connection *conn)
+{
+ struct http_client_peer *peer;
+
+ peer = conn->peer;
+ if (peer == NULL) {
+ i_assert(conn->idle);
+ return;
+ }
+
+ if (conn->idle) {
+ /* Already idle */
+ return;
+ }
+
+ if (conn->connected && !http_client_connection_is_active(conn)) {
+ struct http_client *client = peer->client;
+
+ i_assert(conn->to_requests == NULL);
+
+ if (client->waiting)
+ io_loop_stop(client->ioloop);
+
+ http_client_connection_start_idle(
+ conn, "No more requests queued");
+ }
+}
+
+static void
+http_client_connection_stop_idle(struct http_client_connection *conn)
+{
+ struct http_client_connection *const *conn_idx;
+ ARRAY_TYPE(http_client_connection) *conn_arr;
+
+ timeout_remove(&conn->to_idle);
+ conn->idle = FALSE;
+
+ conn_arr = &conn->ppool->idle_conns;
+ array_foreach(conn_arr, conn_idx) {
+ if (*conn_idx == conn) {
+ array_delete(conn_arr,
+ array_foreach_idx(conn_arr, conn_idx), 1);
+ break;
+ }
+ }
+}
+
+void http_client_connection_claim_idle(struct http_client_connection *conn,
+ struct http_client_peer *peer)
+{
+ e_debug(conn->event, "Claimed as idle");
+
+ i_assert(peer->ppool == conn->ppool);
+ http_client_connection_stop_idle(conn);
+
+ if (conn->peer == NULL || conn->peer != peer) {
+ http_client_connection_detach_peer(conn);
+
+ conn->peer = peer;
+ conn->debug = peer->client->set.debug;
+ array_push_back(&peer->conns, &conn);
+ }
+}
+
+static void
+http_client_connection_request_timeout(struct http_client_connection *conn)
+{
+ conn->conn.input->stream_errno = ETIMEDOUT;
+ http_client_connection_abort_temp_error(
+ &conn, HTTP_CLIENT_REQUEST_ERROR_TIMED_OUT,
+ "Request timed out");
+}
+
+void http_client_connection_start_request_timeout(
+ struct http_client_connection *conn)
+{
+ struct http_client_request *const *requestp;
+ unsigned int timeout_msecs;
+
+ if (conn->pending_request != NULL)
+ return;
+
+ i_assert(array_is_created(&conn->request_wait_list));
+ i_assert(array_count(&conn->request_wait_list) > 0);
+ requestp = array_front(&conn->request_wait_list);
+ timeout_msecs = (*requestp)->attempt_timeout_msecs;
+
+ if (timeout_msecs == 0)
+ ;
+ else if (conn->to_requests != NULL)
+ timeout_reset(conn->to_requests);
+ else {
+ conn->to_requests = timeout_add_to(
+ conn->conn.ioloop, timeout_msecs,
+ http_client_connection_request_timeout, conn);
+ }
+}
+
+void http_client_connection_reset_request_timeout(
+ struct http_client_connection *conn)
+{
+ if (conn->to_requests != NULL)
+ timeout_reset(conn->to_requests);
+}
+
+void http_client_connection_stop_request_timeout(
+ struct http_client_connection *conn)
+{
+ timeout_remove(&conn->to_requests);
+}
+
+static void
+http_client_connection_continue_timeout(struct http_client_connection *conn)
+{
+ struct http_client_peer_shared *pshared = conn->ppool->peer;
+ struct http_client_request *const *wait_reqs;
+ struct http_client_request *req;
+ unsigned int wait_count;
+
+ i_assert(conn->pending_request == NULL);
+
+ timeout_remove(&conn->to_response);
+ pshared->no_payload_sync = TRUE;
+
+ e_debug(conn->event,
+ "Expected 100-continue response timed out; "
+ "sending payload anyway");
+
+ wait_reqs = array_get(&conn->request_wait_list, &wait_count);
+ i_assert(wait_count == 1);
+ req = wait_reqs[wait_count-1];
+
+ req->payload_sync_continue = TRUE;
+ if (conn->conn.output != NULL)
+ o_stream_set_flush_pending(conn->conn.output, TRUE);
+}
+
+int http_client_connection_next_request(struct http_client_connection *conn)
+{
+ struct http_client_connection *tmp_conn;
+ struct http_client_peer *peer = conn->peer;
+ struct http_client_peer_shared *pshared = conn->ppool->peer;
+ struct http_client_request *req = NULL;
+ bool pipelined;
+ int ret;
+
+ if ((ret = http_client_connection_check_ready(conn)) <= 0) {
+ if (ret == 0)
+ e_debug(conn->event, "Not ready for next request");
+ return ret;
+ }
+
+ /* Claim request, but no urgent request can be second in line */
+ pipelined = (array_count(&conn->request_wait_list) > 0 ||
+ conn->pending_request != NULL);
+ req = http_client_peer_claim_request(peer, pipelined);
+ if (req == NULL)
+ return 0;
+
+ i_assert(req->state == HTTP_REQUEST_STATE_QUEUED);
+
+ http_client_connection_stop_idle(conn);
+
+ req->payload_sync_continue = FALSE;
+ if (pshared->no_payload_sync)
+ req->payload_sync = FALSE;
+
+ /* Add request to wait list and add a reference */
+ array_push_back(&conn->request_wait_list, &req);
+ http_client_connection_ref_request(conn, req);
+
+ e_debug(conn->event, "Claimed request %s",
+ http_client_request_label(req));
+
+ tmp_conn = conn;
+ http_client_connection_ref(tmp_conn);
+ ret = http_client_request_send(req, pipelined);
+ if (ret == 0 && conn->conn.output != NULL)
+ o_stream_set_flush_pending(conn->conn.output, TRUE);
+ if (!http_client_connection_unref(&tmp_conn) || ret < 0)
+ return -1;
+
+ if (req->connect_tunnel)
+ conn->tunneling = TRUE;
+
+ /* RFC 7231, Section 5.1.1: Expect
+
+ o A client that sends a 100-continue expectation is not required to
+ wait for any specific length of time; such a client MAY proceed
+ to send the message body even if it has not yet received a
+ response. Furthermore, since 100 (Continue) responses cannot be
+ sent through an HTTP/1.0 intermediary, such a client SHOULD NOT
+ wait for an indefinite period before sending the message body.
+ */
+ if (req->payload_sync && !pshared->seen_100_response) {
+ i_assert(!pipelined);
+ i_assert(req->payload_chunked || req->payload_size > 0);
+ i_assert(conn->to_response == NULL);
+ conn->to_response = timeout_add_to(
+ conn->conn.ioloop, HTTP_CLIENT_CONTINUE_TIMEOUT_MSECS,
+ http_client_connection_continue_timeout, conn);
+ }
+
+ return 1;
+}
+
+static void http_client_connection_destroy(struct connection *_conn)
+{
+ struct http_client_connection *conn =
+ (struct http_client_connection *)_conn;
+ const char *error;
+ unsigned int msecs;
+
+ switch (_conn->disconnect_reason) {
+ case CONNECTION_DISCONNECT_CONNECT_TIMEOUT:
+ if (conn->connected_timestamp.tv_sec == 0) {
+ msecs = timeval_diff_msecs(
+ &ioloop_timeval,
+ &conn->connect_start_timestamp);
+ error = t_strdup_printf(
+ "connect(%s) failed: "
+ "Connection timed out in %u.%03u secs",
+ _conn->name, msecs/1000, msecs%1000);
+ } else {
+ msecs = timeval_diff_msecs(&ioloop_timeval,
+ &conn->connected_timestamp);
+ error = t_strdup_printf(
+ "SSL handshaking with %s failed: "
+ "Connection timed out in %u.%03u secs",
+ _conn->name, msecs/1000, msecs%1000);
+ }
+ e_debug(conn->event, "%s", error);
+ http_client_connection_failure(conn, error);
+ break;
+ case CONNECTION_DISCONNECT_CONN_CLOSED:
+ if (conn->connect_failed) {
+ i_assert(!array_is_created(&conn->request_wait_list) ||
+ array_count(&conn->request_wait_list) == 0);
+ break;
+ }
+ http_client_connection_lost(
+ &conn, (_conn->input == NULL ?
+ NULL : i_stream_get_error(_conn->input)));
+ return;
+ default:
+ break;
+ }
+
+ http_client_connection_close(&conn);
+}
+
+static void http_client_payload_finished(struct http_client_connection *conn)
+{
+ timeout_remove(&conn->to_input);
+ connection_input_resume(&conn->conn);
+ if (array_count(&conn->request_wait_list) > 0)
+ http_client_connection_start_request_timeout(conn);
+ else
+ http_client_connection_stop_request_timeout(conn);
+}
+
+static void
+http_client_payload_destroyed_timeout(struct http_client_connection *conn)
+{
+ if (conn->close_indicated) {
+ http_client_connection_server_close(&conn);
+ return;
+ }
+ http_client_connection_input(&conn->conn);
+}
+
+static void http_client_payload_destroyed(struct http_client_request *req)
+{
+ struct http_client_connection *conn = req->conn;
+
+ i_assert(conn != NULL);
+ i_assert(conn->pending_request == req);
+ i_assert(conn->incoming_payload != NULL);
+ i_assert(conn->conn.io == NULL);
+
+ e_debug(conn->event,
+ "Response payload stream destroyed "
+ "(%u ms after initial response)",
+ timeval_diff_msecs(&ioloop_timeval, &req->response_time));
+
+ /* Caller is allowed to change the socket fd to blocking while reading
+ the payload. make sure here that it's switched back. */
+ net_set_nonblock(conn->conn.fd_in, TRUE);
+
+ i_assert(req->response_offset < conn->conn.input->v_offset);
+ req->bytes_in = conn->conn.input->v_offset - req->response_offset;
+
+ /* Drop reference from connection */
+ if (http_client_connection_unref_request(
+ conn, &conn->pending_request)) {
+ /* Finish request if not already aborted */
+ http_client_request_finish(req);
+ }
+
+ conn->incoming_payload = NULL;
+
+ /* Input stream may have pending input. make sure input handler
+ gets called (but don't do it directly, since we get get here
+ somewhere from the API user's code, which we can't really know what
+ state it is in). this call also triggers sending a new request if
+ necessary. */
+ if (!conn->disconnected) {
+ conn->to_input = timeout_add_short_to(
+ conn->conn.ioloop, 0,
+ http_client_payload_destroyed_timeout, conn);
+ }
+
+ /* Room for new requests */
+ if (http_client_connection_check_ready(conn) > 0)
+ http_client_peer_trigger_request_handler(conn->peer);
+}
+
+void http_client_connection_request_destroyed(
+ struct http_client_connection *conn, struct http_client_request *req)
+{
+ struct istream *payload;
+
+ i_assert(req->conn == conn);
+ if (conn->pending_request != req)
+ return;
+
+ e_debug(conn->event, "Pending request destroyed prematurely");
+
+ payload = conn->incoming_payload;
+ if (payload == NULL) {
+ /* Payload already gone */
+ return;
+ }
+
+ /* Destroy the payload, so that the timeout istream is closed */
+ i_stream_ref(payload);
+ i_stream_destroy(&payload);
+
+ payload = conn->incoming_payload;
+ if (payload == NULL) {
+ /* Not going to happen, but check for it anyway */
+ return;
+ }
+
+ /* The application still holds a reference to the payload stream, but it
+ is closed and we don't care about it anymore, so act as though it is
+ destroyed. */
+ i_stream_remove_destroy_callback(payload,
+ http_client_payload_destroyed);
+ http_client_payload_destroyed(req);
+}
+
+static bool
+http_client_connection_return_response(struct http_client_connection *conn,
+ struct http_client_request *req,
+ struct http_response *response)
+{
+ struct http_client_peer_shared *pshared = conn->ppool->peer;
+ struct istream *payload;
+ bool retrying;
+
+ i_assert(!conn->in_req_callback);
+ i_assert(conn->incoming_payload == NULL);
+ i_assert(conn->pending_request == NULL);
+
+ http_client_connection_ref(conn);
+ http_client_connection_ref_request(conn, req);
+ req->state = HTTP_REQUEST_STATE_GOT_RESPONSE;
+
+ if (response->payload != NULL) {
+ /* Wrap the stream to capture the destroy event without
+ destroying the actual payload stream. we are already expected
+ to be on the correct ioloop, so there should be no need to
+ switch the stream's ioloop here. */
+ conn->incoming_payload = response->payload =
+ i_stream_create_timeout(response->payload,
+ req->attempt_timeout_msecs);
+ i_stream_add_destroy_callback(response->payload,
+ http_client_payload_destroyed,
+ req);
+ /* The callback may add its own I/O, so we need to remove
+ our one before calling it */
+ connection_input_halt(&conn->conn);
+ /* We've received the request itself, and we can't reset the
+ timeout during the payload reading. */
+ http_client_connection_stop_request_timeout(conn);
+ }
+
+ conn->in_req_callback = TRUE;
+ retrying = !http_client_request_callback(req, response);
+ if (conn->disconnected) {
+ /* The callback managed to get this connection disconnected */
+ if (!retrying)
+ http_client_request_finish(req);
+ http_client_connection_unref_request(conn, &req);
+ http_client_connection_unref(&conn);
+ return FALSE;
+ }
+ conn->in_req_callback = FALSE;
+
+ if (retrying) {
+ /* Retrying, don't destroy the request */
+ if (response->payload != NULL) {
+ i_stream_remove_destroy_callback(
+ conn->incoming_payload,
+ http_client_payload_destroyed);
+ i_stream_unref(&conn->incoming_payload);
+ connection_input_resume(&conn->conn);
+ }
+ http_client_connection_unref_request(conn, &req);
+ return http_client_connection_unref(&conn);
+ }
+
+ if (response->payload != NULL) {
+ req->state = HTTP_REQUEST_STATE_PAYLOAD_IN;
+ payload = response->payload;
+ response->payload = NULL;
+
+ /* Maintain request reference while payload is pending */
+ conn->pending_request = req;
+
+ /* Request is dereferenced in payload destroy callback */
+ i_stream_unref(&payload);
+
+ if (conn->to_input != NULL && conn->conn.input != NULL) {
+ /* Already finished reading the payload */
+ http_client_payload_finished(conn);
+ }
+ } else {
+ http_client_request_finish(req);
+ http_client_connection_unref_request(conn, &req);
+ }
+
+ if (conn->incoming_payload == NULL && conn->conn.input != NULL) {
+ i_assert(conn->conn.io != NULL ||
+ pshared->addr.type == HTTP_CLIENT_PEER_ADDR_RAW);
+ return http_client_connection_unref(&conn);
+ }
+ http_client_connection_unref(&conn);
+ return FALSE;
+}
+
+static const char *
+http_client_request_add_event_headers(struct http_client_request *req,
+ const struct http_response *response)
+{
+ if (req->event_headers == NULL)
+ return "";
+
+ string_t *str = t_str_new(128);
+ for (unsigned int i = 0; req->event_headers[i] != NULL; i++) {
+ const char *hdr_name = req->event_headers[i];
+ const char *value =
+ http_response_header_get(response, hdr_name);
+
+ if (value == NULL)
+ continue;
+
+ str_append(str, str_len(str) == 0 ? " (" : ", ");
+ event_add_str(req->event,
+ t_strconcat("http_hdr_", hdr_name, NULL), value);
+ str_printfa(str, "%s:%s", hdr_name, value);
+ }
+ if (str_len(str) > 0)
+ str_append_c(str, ')');
+ return str_c(str);
+}
+
+static void http_client_connection_input(struct connection *_conn)
+{
+ struct http_client_connection *conn =
+ (struct http_client_connection *)_conn;
+ struct http_client_peer *peer = conn->peer;
+ struct http_client_peer_shared *pshared = conn->ppool->peer;
+ struct http_response response;
+ struct http_client_request *const *reqs;
+ struct http_client_request *req = NULL, *req_ref;
+ enum http_response_payload_type payload_type;
+ unsigned int count;
+ int finished = 0, ret;
+ const char *error;
+
+ i_assert(conn->incoming_payload == NULL);
+
+ _conn->last_input = ioloop_time;
+
+ if (conn->ssl_iostream != NULL &&
+ !ssl_iostream_is_handshaked(conn->ssl_iostream)) {
+ /* Finish SSL negotiation by reading from input stream */
+ while ((ret = i_stream_read(conn->conn.input)) > 0 ||
+ ret == -2) {
+ if (ssl_iostream_is_handshaked(conn->ssl_iostream))
+ break;
+ }
+ if (ret < 0) {
+ int stream_errno = conn->conn.input->stream_errno;
+
+ /* Failed somehow */
+ i_assert(ret != -2);
+ error = t_strdup_printf(
+ "SSL handshaking with %s failed: "
+ "read(%s) failed: %s",
+ _conn->name,
+ i_stream_get_name(conn->conn.input),
+ (stream_errno != 0 ?
+ i_stream_get_error(conn->conn.input) : "EOF"));
+ http_client_connection_failure(conn, error);
+ e_debug(conn->event, "%s", error);
+ http_client_connection_close(&conn);
+ return;
+ }
+
+ if (!ssl_iostream_is_handshaked(conn->ssl_iostream)) {
+ /* Not finished */
+ i_assert(ret == 0);
+ return;
+ }
+ }
+
+ if (!conn->connect_succeeded) {
+ /* Just got ready for first request */
+ http_client_connection_ready(conn);
+ }
+
+ if (conn->to_input != NULL) {
+ /* We came here from a timeout added by
+ http_client_payload_destroyed(). The IO couldn't be added
+ back immediately in there, because the HTTP API user may
+ still have had its own IO pointed to the same fd. It should
+ be removed by now, so we can add it back. */
+ http_client_payload_finished(conn);
+ finished++;
+ }
+
+ /* We've seen activity from the server; reset request timeout */
+ http_client_connection_reset_request_timeout(conn);
+
+ /* Get first waiting request */
+ reqs = array_get(&conn->request_wait_list, &count);
+ if (count > 0) {
+ req = reqs[0];
+
+ /* Determine whether to expect a response payload */
+ payload_type = http_client_request_get_payload_type(req);
+ } else {
+ req = NULL;
+ payload_type = HTTP_RESPONSE_PAYLOAD_TYPE_ALLOWED;
+ i_assert(conn->to_requests == NULL);
+ }
+
+ /* Drop connection with broken output if last possible input was
+ received */
+ if (conn->output_broken && (count == 0 ||
+ (count == 1 && req->state == HTTP_REQUEST_STATE_ABORTED))) {
+ http_client_connection_server_close(&conn);
+ return;
+ }
+
+ while ((ret = http_response_parse_next(conn->http_parser, payload_type,
+ &response, &error)) > 0) {
+ bool aborted, early = FALSE;
+
+ if (req == NULL) {
+ /* Server sent response without any requests in the wait
+ list */
+ if (response.status == 408) {
+ e_debug(conn->event,
+ "Server explicitly closed connection: "
+ "408 %s", response.reason);
+ } else {
+ e_debug(conn->event,
+ "Got unexpected input from server: "
+ "%u %s", response.status,
+ response.reason);
+ }
+ http_client_connection_close(&conn);
+ return;
+ }
+
+ req->response_time = ioloop_timeval;
+ req->response_offset =
+ http_response_parser_get_last_offset(conn->http_parser);
+ i_assert(req->response_offset != UOFF_T_MAX);
+ i_assert(req->response_offset < conn->conn.input->v_offset);
+ req->bytes_in = conn->conn.input->v_offset -
+ req->response_offset;
+
+ /* Got some response; cancel response timeout */
+ timeout_remove(&conn->to_response);
+
+ /* RFC 7231, Section 6.2:
+
+ A client MUST be able to parse one or more 1xx responses
+ received prior to a final response, even if the client does
+ not expect one. A user agent MAY ignore unexpected 1xx
+ responses.
+ */
+ if (req->payload_sync && response.status == 100) {
+ if (req->payload_sync_continue) {
+ e_debug(conn->event,
+ "Got 100-continue response after timeout");
+ continue;
+ }
+
+ pshared->no_payload_sync = FALSE;
+ pshared->seen_100_response = TRUE;
+ req->payload_sync_continue = TRUE;
+
+ e_debug(conn->event,
+ "Got expected 100-continue response");
+
+ if (req->state == HTTP_REQUEST_STATE_ABORTED) {
+ e_debug(conn->event,
+ "Request aborted before sending payload was complete.");
+ http_client_connection_close(&conn);
+ return;
+ }
+
+ if (conn->conn.output != NULL)
+ o_stream_set_flush_pending(conn->conn.output, TRUE);
+ return;
+ } else if (response.status / 100 == 1) {
+ /* Ignore other 1xx for now */
+ e_debug(conn->event,
+ "Got unexpected %u response; ignoring",
+ response.status);
+ continue;
+ } else if (!req->payload_sync && !req->payload_finished &&
+ req->state == HTTP_REQUEST_STATE_PAYLOAD_OUT) {
+ /* Got early response from server while we're still
+ sending request payload. we cannot recover from this
+ reliably, so we stop sending payload and close the
+ connection once the response is processed */
+ e_debug(conn->event,
+ "Got early input from server; "
+ "request payload not completely sent "
+ "(will close connection)");
+ o_stream_unset_flush_callback(conn->conn.output);
+ conn->output_broken = early = TRUE;
+ }
+
+ const char *suffix =
+ http_client_request_add_event_headers(req, &response);
+ e_debug(conn->event,
+ "Got %u response for request %s: %s%s "
+ "(took %u ms + %u ms in queue)",
+ response.status, http_client_request_label(req),
+ response.reason, suffix,
+ timeval_diff_msecs(&req->response_time, &req->sent_time),
+ timeval_diff_msecs(&req->sent_time, &req->submit_time));
+
+ /* Make sure connection output is unlocked if 100-continue
+ failed */
+ if (req->payload_sync && !req->payload_sync_continue) {
+ e_debug(conn->event, "Unlocked output");
+ conn->output_locked = FALSE;
+ }
+
+ /* Remove request from queue */
+ array_pop_front(&conn->request_wait_list);
+ aborted = (req->state == HTTP_REQUEST_STATE_ABORTED);
+ req_ref = req;
+ if (!http_client_connection_unref_request(conn, &req_ref)) {
+ i_assert(aborted);
+ req = NULL;
+ }
+
+ conn->close_indicated = response.connection_close;
+
+ if (!aborted) {
+ bool handled = FALSE;
+
+ /* Response cannot be 2xx if request payload was not
+ completely sent */
+ if (early && response.status / 100 == 2) {
+ http_client_request_error(
+ &req, HTTP_CLIENT_REQUEST_ERROR_BAD_RESPONSE,
+ "Server responded with success response "
+ "before all payload was sent");
+ http_client_connection_close(&conn);
+ return;
+ }
+
+ /* Don't redirect/retry if we're sending data in small
+ blocks via http_client_request_send_payload()
+ and we're not waiting for 100 continue */
+ if (!req->payload_wait ||
+ (req->payload_sync && !req->payload_sync_continue)) {
+ /* Failed Expect: */
+ if (response.status == 417 && req->payload_sync) {
+ /* Drop Expect: continue */
+ req->payload_sync = FALSE;
+ conn->output_locked = FALSE;
+ pshared->no_payload_sync = TRUE;
+ if (http_client_request_try_retry(req))
+ handled = TRUE;
+ /* Redirection */
+ } else if (!req->client->set.no_auto_redirect &&
+ response.status / 100 == 3 &&
+ response.status != 304 &&
+ response.location != NULL) {
+ /* Redirect (possibly after delay) */
+ if (http_client_request_delay_from_response(
+ req, &response) >= 0) {
+ http_client_request_redirect(
+ req, response.status,
+ response.location);
+ handled = TRUE;
+ }
+ /* Service unavailable */
+ } else if (response.status == 503) {
+ /* Automatically retry after delay if
+ indicated */
+ if (response.retry_after != (time_t)-1 &&
+ http_client_request_delay_from_response(
+ req, &response) > 0 &&
+ http_client_request_try_retry(req))
+ handled = TRUE;
+ /* Request timeout (by server) */
+ } else if (response.status == 408) {
+ /* Automatically retry */
+ if (http_client_request_try_retry(req))
+ handled = TRUE;
+ /* Connection close is implicit,
+ although server should indicate that
+ explicitly */
+ conn->close_indicated = TRUE;
+ }
+ }
+
+ if (!handled) {
+ /* Response for application */
+ if (!http_client_connection_return_response(
+ conn, req, &response))
+ return;
+ }
+ }
+
+ finished++;
+
+ /* Server closing connection? */
+ if (conn->close_indicated) {
+ http_client_connection_server_close(&conn);
+ return;
+ }
+
+ /* Get next waiting request */
+ reqs = array_get(&conn->request_wait_list, &count);
+ if (count > 0) {
+ req = reqs[0];
+
+ /* Determine whether to expect a response payload */
+ payload_type = http_client_request_get_payload_type(req);
+ } else {
+ /* No more requests waiting for the connection */
+ req = NULL;
+ payload_type = HTTP_RESPONSE_PAYLOAD_TYPE_ALLOWED;
+ http_client_connection_stop_request_timeout(conn);
+ }
+
+ /* Drop connection with broken output if last possible input was
+ received */
+ if (conn->output_broken && (count == 0 ||
+ (count == 1 && req->state == HTTP_REQUEST_STATE_ABORTED))) {
+ http_client_connection_server_close(&conn);
+ return;
+ }
+ }
+
+ if (ret <= 0 &&
+ (conn->conn.input->eof || conn->conn.input->stream_errno != 0)) {
+ int stream_errno = conn->conn.input->stream_errno;
+
+ http_client_connection_lost(
+ &conn,
+ t_strdup_printf("read(%s) failed: %s",
+ i_stream_get_name(conn->conn.input),
+ (stream_errno != 0 ?
+ i_stream_get_error(conn->conn.input) :
+ "EOF")));
+ return;
+ }
+
+ if (ret < 0) {
+ http_client_connection_abort_error(
+ &conn, HTTP_CLIENT_REQUEST_ERROR_BAD_RESPONSE, error);
+ return;
+ }
+
+ if (finished > 0) {
+ /* Connection still alive after (at least one) request;
+ we can pipeline -> mark for subsequent connections */
+ pshared->allows_pipelining = TRUE;
+
+ /* Room for new requests */
+ if (peer != NULL &&
+ http_client_connection_check_ready(conn) > 0)
+ http_client_peer_trigger_request_handler(peer);
+ }
+}
+
+static int
+http_client_connection_continue_request(struct http_client_connection *conn)
+{
+ struct http_client_connection *tmp_conn;
+ struct http_client_request *const *reqs;
+ unsigned int count;
+ struct http_client_request *req;
+ bool pipelined;
+ int ret;
+
+ reqs = array_get(&conn->request_wait_list, &count);
+ i_assert(count > 0 || conn->to_requests == NULL);
+ if (count == 0 || !conn->output_locked)
+ return 1;
+
+ req = reqs[count-1];
+ pipelined = (count > 1 || conn->pending_request != NULL);
+
+ if (req->state == HTTP_REQUEST_STATE_ABORTED) {
+ e_debug(conn->event,
+ "Request aborted before sending payload was complete.");
+ if (count == 1) {
+ http_client_connection_close(&conn);
+ return -1;
+ }
+ o_stream_unset_flush_callback(conn->conn.output);
+ conn->output_broken = TRUE;
+ return -1;
+ }
+
+ if (req->payload_sync && !req->payload_sync_continue)
+ return 1;
+
+ tmp_conn = conn;
+ http_client_connection_ref(tmp_conn);
+ ret = http_client_request_send_more(req, pipelined);
+ if (!http_client_connection_unref(&tmp_conn) || ret < 0)
+ return -1;
+
+ if (!conn->output_locked) {
+ /* Room for new requests */
+ if (http_client_connection_check_ready(conn) > 0)
+ http_client_peer_trigger_request_handler(conn->peer);
+ }
+ return ret;
+}
+
+int http_client_connection_output(struct http_client_connection *conn)
+{
+ struct ostream *output = conn->conn.output;
+ int ret;
+
+ /* We've seen activity from the server; reset request timeout */
+ http_client_connection_reset_request_timeout(conn);
+
+ if ((ret = o_stream_flush(output)) <= 0) {
+ if (ret < 0)
+ http_client_connection_handle_output_error(conn);
+ return ret;
+ }
+
+ i_assert(!conn->output_broken);
+
+ if (conn->ssl_iostream != NULL &&
+ !ssl_iostream_is_handshaked(conn->ssl_iostream))
+ return 1;
+
+ return http_client_connection_continue_request(conn);
+}
+
+void http_client_connection_start_tunnel(struct http_client_connection **_conn,
+ struct http_client_tunnel *tunnel)
+{
+ struct http_client_connection *conn = *_conn;
+
+ i_assert(conn->tunneling);
+
+ /* Claim connection streams */
+ i_zero(tunnel);
+ tunnel->input = conn->conn.input;
+ tunnel->output = conn->conn.output;
+ tunnel->fd_in = conn->conn.fd_in;
+ tunnel->fd_out = conn->conn.fd_out;
+
+ /* Detach from connection */
+ conn->conn.input = NULL;
+ conn->conn.output = NULL;
+ conn->conn.fd_in = -1;
+ conn->conn.fd_out = -1;
+ conn->closing = TRUE;
+ conn->connected = FALSE;
+ connection_disconnect(&conn->conn);
+
+ http_client_connection_unref(_conn);
+}
+
+static void http_client_connection_ready(struct http_client_connection *conn)
+{
+ struct http_client_peer *peer = conn->peer;
+ struct http_client_peer_pool *ppool = conn->ppool;
+ struct http_client_peer_shared *pshared = ppool->peer;
+ const struct http_client_settings *set =
+ http_client_connection_get_settings(conn);
+
+ e_debug(conn->event, "Ready for requests");
+ i_assert(!conn->connect_succeeded);
+
+ /* Connected */
+ conn->connected = TRUE;
+ conn->last_ioloop = current_ioloop;
+ timeout_remove(&conn->to_connect);
+
+ /* Indicate connection success */
+ conn->connect_succeeded = TRUE;
+ http_client_connection_unlist_pending(conn);
+ http_client_peer_connection_success(peer);
+
+ /* Start raw log */
+ if (ppool->rawlog_dir != NULL) {
+ iostream_rawlog_create(ppool->rawlog_dir,
+ &conn->conn.input, &conn->conn.output);
+ }
+
+ /* Direct tunneling connections handle connect requests just by
+ providing a raw connection */
+ if (pshared->addr.type == HTTP_CLIENT_PEER_ADDR_RAW) {
+ struct http_client_request *req;
+
+ req = http_client_peer_claim_request(conn->peer, FALSE);
+ if (req != NULL) {
+ struct http_response response;
+
+ conn->tunneling = TRUE;
+
+ i_zero(&response);
+ response.status = 200;
+ response.reason = "OK";
+
+ (void)http_client_connection_return_response(conn, req,
+ &response);
+ return;
+ }
+
+ e_debug(conn->event,
+ "No raw connect requests pending; "
+ "closing useless connection");
+ http_client_connection_close(&conn);
+ return;
+ }
+
+ /* Start protocol I/O */
+ conn->http_parser = http_response_parser_init(
+ conn->conn.input, &set->response_hdr_limits, 0);
+ o_stream_set_finish_via_child(conn->conn.output, FALSE);
+ o_stream_set_flush_callback(conn->conn.output,
+ http_client_connection_output, conn);
+}
+
+static int
+http_client_connection_ssl_handshaked(const char **error_r, void *context)
+{
+ struct http_client_connection *conn = context;
+ struct http_client_peer_shared *pshared = conn->ppool->peer;
+ const struct http_client_settings *set =
+ http_client_connection_get_settings(conn);
+ const char *error, *host = pshared->addr.a.tcp.https_name;
+
+ if (ssl_iostream_check_cert_validity(conn->ssl_iostream,
+ host, &error) == 0)
+ e_debug(conn->event, "SSL handshake successful");
+ else if (set->ssl->allow_invalid_cert) {
+ e_debug(conn->event, "SSL handshake successful, "
+ "ignoring invalid certificate: %s", error);
+ } else {
+ *error_r = error;
+ return -1;
+ }
+ return 0;
+}
+
+static int
+http_client_connection_ssl_init(struct http_client_connection *conn,
+ const char **error_r)
+{
+ struct http_client_peer_pool *ppool = conn->ppool;
+ struct http_client_peer_shared *pshared = ppool->peer;
+ const struct http_client_settings *set =
+ http_client_connection_get_settings(conn);
+ struct ssl_iostream_settings ssl_set;
+ struct ssl_iostream_context *ssl_ctx = ppool->ssl_ctx;
+ const char *error;
+
+ i_assert(ssl_ctx != NULL);
+
+ ssl_set = *set->ssl;
+ if (!set->ssl->allow_invalid_cert)
+ ssl_set.verbose_invalid_cert = TRUE;
+
+ e_debug(conn->event, "Starting SSL handshake");
+
+ connection_input_halt(&conn->conn);
+ if (io_stream_create_ssl_client(ssl_ctx, pshared->addr.a.tcp.https_name,
+ &ssl_set,
+ &conn->conn.input, &conn->conn.output,
+ &conn->ssl_iostream, &error) < 0) {
+ *error_r = t_strdup_printf(
+ "Couldn't initialize SSL client for %s: %s",
+ conn->conn.name, error);
+ return -1;
+ }
+ connection_input_resume(&conn->conn);
+ ssl_iostream_set_handshake_callback(
+ conn->ssl_iostream,
+ http_client_connection_ssl_handshaked, conn);
+ if (ssl_iostream_handshake(conn->ssl_iostream) < 0) {
+ *error_r = t_strdup_printf(
+ "SSL handshake to %s failed: %s", conn->conn.name,
+ ssl_iostream_get_last_error(conn->ssl_iostream));
+ return -1;
+ }
+
+ if (ssl_iostream_is_handshaked(conn->ssl_iostream)) {
+ http_client_connection_ready(conn);
+ } else {
+ /* Wait for handshake to complete; connection input handler does
+ the rest by reading from the input stream */
+ o_stream_set_flush_callback(
+ conn->conn.output, http_client_connection_output, conn);
+ }
+ return 0;
+}
+
+static void
+http_client_connection_connected(struct connection *_conn, bool success)
+{
+ struct http_client_connection *conn =
+ (struct http_client_connection *)_conn;
+ struct http_client_peer_shared *pshared = conn->ppool->peer;
+ const struct http_client_settings *set =
+ http_client_connection_get_settings(conn);
+ const char *error;
+
+ if (!success) {
+ http_client_connection_failure(
+ conn, t_strdup_printf("connect(%s) failed: %m",
+ _conn->name));
+ } else {
+ conn->connected_timestamp = ioloop_timeval;
+ e_debug(conn->event, "Connected");
+
+ (void)net_set_tcp_nodelay(_conn->fd_out, TRUE);
+ if (set->socket_send_buffer_size > 0 &&
+ net_set_send_buffer_size(
+ _conn->fd_out, set->socket_send_buffer_size) < 0) {
+ i_error("net_set_send_buffer_size(%zu) failed: %m",
+ set->socket_send_buffer_size);
+ }
+ if (set->socket_recv_buffer_size > 0 &&
+ net_set_recv_buffer_size(
+ _conn->fd_in, set->socket_recv_buffer_size) < 0) {
+ i_error("net_set_recv_buffer_size(%zu) failed: %m",
+ set->socket_recv_buffer_size);
+ }
+
+ if (http_client_peer_addr_is_https(&pshared->addr)) {
+ if (http_client_connection_ssl_init(conn, &error) < 0) {
+ e_debug(conn->event, "%s", error);
+ http_client_connection_failure(conn, error);
+ http_client_connection_close(&conn);
+ }
+ return;
+ }
+ http_client_connection_ready(conn);
+ }
+}
+
+static const struct connection_settings http_client_connection_set = {
+ .input_max_size = SIZE_MAX,
+ .output_max_size = SIZE_MAX,
+ .client = TRUE,
+ .delayed_unix_client_connected_callback = TRUE,
+ .log_connection_id = TRUE,
+};
+
+static const struct connection_vfuncs http_client_connection_vfuncs = {
+ .destroy = http_client_connection_destroy,
+ .input = http_client_connection_input,
+ .client_connected = http_client_connection_connected,
+};
+
+struct connection_list *http_client_connection_list_init(void)
+{
+ return connection_list_init(&http_client_connection_set,
+ &http_client_connection_vfuncs);
+}
+
+static void
+http_client_connection_delayed_connect_error(
+ struct http_client_connection *conn)
+{
+ timeout_remove(&conn->to_input);
+ errno = conn->connect_errno;
+ http_client_connection_connected(&conn->conn, FALSE);
+ http_client_connection_close(&conn);
+}
+
+static void http_client_connect_timeout(struct http_client_connection *conn)
+{
+ conn->conn.disconnect_reason = CONNECTION_DISCONNECT_CONNECT_TIMEOUT;
+ http_client_connection_destroy(&conn->conn);
+}
+
+static void
+http_client_connection_connect(struct http_client_connection *conn,
+ unsigned int timeout_msecs)
+{
+ struct http_client_context *cctx = conn->ppool->peer->cctx;
+
+ conn->connect_start_timestamp = ioloop_timeval;
+ if (connection_client_connect(&conn->conn) < 0) {
+ conn->connect_errno = errno;
+ e_debug(conn->event, "Connect failed: %m");
+ conn->to_input = timeout_add_short_to(
+ conn->conn.ioloop, 0,
+ http_client_connection_delayed_connect_error, conn);
+ return;
+ }
+
+ /* Don't use connection.h timeout because we want this timeout
+ to include also the SSL handshake */
+ if (timeout_msecs > 0) {
+ conn->to_connect = timeout_add_to(
+ cctx->ioloop, timeout_msecs,
+ http_client_connect_timeout, conn);
+ }
+}
+
+static void
+http_client_connect_tunnel_timeout(struct http_client_connection *conn)
+{
+ struct http_client_peer_shared *pshared = conn->ppool->peer;
+ const char *error, *name = http_client_peer_addr2str(&pshared->addr);
+ unsigned int msecs;
+
+ msecs = timeval_diff_msecs(&ioloop_timeval,
+ &conn->connect_start_timestamp);
+ error = t_strdup_printf("Tunnel connect(%s) failed: "
+ "Connection timed out in %u.%03u secs",
+ name, msecs/1000, msecs%1000);
+
+ e_debug(conn->event, "%s", error);
+ http_client_connection_failure(conn, error);
+ http_client_connection_close(&conn);
+}
+
+static void
+http_client_connection_tunnel_response(const struct http_response *response,
+ struct http_client_connection *conn)
+{
+ struct http_client_peer_shared *pshared = conn->ppool->peer;
+ struct http_client_context *cctx = pshared->cctx;
+ struct http_client_tunnel tunnel;
+ const char *name = http_client_peer_addr2str(&pshared->addr);
+ struct http_client_request *req = conn->connect_request;
+
+ conn->connect_request = NULL;
+
+ if (response->status != 200) {
+ http_client_connection_failure(
+ conn,
+ t_strdup_printf("Tunnel connect(%s) failed: %s", name,
+ http_response_get_message(response)));
+ return;
+ }
+
+ http_client_request_start_tunnel(req, &tunnel);
+
+ conn->conn.event_parent = conn->event;
+ connection_init_from_streams(cctx->conn_list, &conn->conn,
+ name, tunnel.input, tunnel.output);
+ connection_switch_ioloop_to(&conn->conn, cctx->ioloop);
+ i_stream_unref(&tunnel.input);
+ o_stream_unref(&tunnel.output);
+}
+
+static void
+http_client_connection_connect_tunnel(struct http_client_connection *conn,
+ const struct ip_addr *ip, in_port_t port,
+ unsigned int timeout_msecs)
+{
+ struct http_client_context *cctx = conn->ppool->peer->cctx;
+ struct http_client *client = conn->peer->client;
+
+ conn->connect_start_timestamp = ioloop_timeval;
+
+ conn->connect_request = http_client_request_connect_ip(
+ client, ip, port, http_client_connection_tunnel_response, conn);
+ http_client_request_set_urgent(conn->connect_request);
+ http_client_request_submit(conn->connect_request);
+
+ /* Don't use connection.h timeout because we want this timeout
+ to include also the SSL handshake */
+ if (timeout_msecs > 0) {
+ conn->to_connect = timeout_add_to(
+ cctx->ioloop, timeout_msecs,
+ http_client_connect_tunnel_timeout, conn);
+ }
+}
+
+struct http_client_connection *
+http_client_connection_create(struct http_client_peer *peer)
+{
+ struct http_client_peer_shared *pshared = peer->shared;
+ struct http_client_peer_pool *ppool = peer->ppool;
+ struct http_client_context *cctx = pshared->cctx;
+ struct http_client *client = peer->client;
+ const struct http_client_settings *set = &client->set;
+ struct http_client_connection *conn;
+ const struct http_client_peer_addr *addr = &pshared->addr;
+ const char *conn_type = "UNKNOWN";
+ unsigned int timeout_msecs;
+
+ switch (pshared->addr.type) {
+ case HTTP_CLIENT_PEER_ADDR_HTTP:
+ conn_type = "HTTP";
+ break;
+ case HTTP_CLIENT_PEER_ADDR_HTTPS:
+ conn_type = "HTTPS";
+ break;
+ case HTTP_CLIENT_PEER_ADDR_HTTPS_TUNNEL:
+ conn_type = "Tunneled HTTPS";
+ break;
+ case HTTP_CLIENT_PEER_ADDR_RAW:
+ conn_type = "Raw";
+ break;
+ case HTTP_CLIENT_PEER_ADDR_UNIX:
+ conn_type = "Unix";
+ break;
+ }
+
+ timeout_msecs = set->connect_timeout_msecs;
+ if (timeout_msecs == 0)
+ timeout_msecs = set->request_timeout_msecs;
+
+ conn = i_new(struct http_client_connection, 1);
+ conn->refcount = 1;
+ conn->ppool = ppool;
+ conn->peer = peer;
+ conn->debug = client->set.debug;
+ if (pshared->addr.type != HTTP_CLIENT_PEER_ADDR_RAW)
+ i_array_init(&conn->request_wait_list, 16);
+ conn->io_wait_timer = io_wait_timer_add_to(cctx->ioloop);
+
+ conn->conn.event_parent = ppool->peer->cctx->event;
+ connection_init(cctx->conn_list, &conn->conn,
+ http_client_peer_shared_label(pshared));
+ conn->event = conn->conn.event;
+
+ switch (pshared->addr.type) {
+ case HTTP_CLIENT_PEER_ADDR_HTTPS_TUNNEL:
+ http_client_connection_connect_tunnel(
+ conn, &addr->a.tcp.ip, addr->a.tcp.port, timeout_msecs);
+ break;
+ case HTTP_CLIENT_PEER_ADDR_UNIX:
+ connection_init_client_unix(cctx->conn_list, &conn->conn,
+ addr->a.un.path);
+ connection_switch_ioloop_to(&conn->conn, cctx->ioloop);
+ http_client_connection_connect(conn, timeout_msecs);
+ break;
+ default:
+ connection_init_client_ip(cctx->conn_list, &conn->conn, NULL,
+ &addr->a.tcp.ip, addr->a.tcp.port);
+ connection_switch_ioloop_to(&conn->conn, cctx->ioloop);
+ http_client_connection_connect(conn, timeout_msecs);
+ }
+
+ array_push_back(&ppool->pending_conns, &conn);
+ array_push_back(&ppool->conns, &conn);
+ array_push_back(&peer->pending_conns, &conn);
+ array_push_back(&peer->conns, &conn);
+
+ http_client_peer_pool_ref(ppool);
+
+ e_debug(conn->event,
+ "%s connection created (%d parallel connections exist)%s",
+ conn_type, array_count(&ppool->conns),
+ (conn->to_input == NULL ? "" : " [broken]"));
+ return conn;
+}
+
+void http_client_connection_ref(struct http_client_connection *conn)
+{
+ i_assert(conn->refcount > 0);
+ conn->refcount++;
+}
+
+static void
+http_client_connection_disconnect(struct http_client_connection *conn)
+{
+ struct http_client_peer_pool *ppool = conn->ppool;
+ ARRAY_TYPE(http_client_connection) *conn_arr;
+ struct http_client_connection *const *conn_idx;
+
+ if (conn->disconnected)
+ return;
+ conn->disconnected = TRUE;
+
+ e_debug(conn->event, "Connection disconnect");
+
+ conn->closing = TRUE;
+ conn->connected = FALSE;
+
+ http_client_request_abort(&conn->connect_request);
+
+ if (conn->incoming_payload != NULL) {
+ /* The stream is still accessed by lib-http caller. */
+ i_stream_remove_destroy_callback(conn->incoming_payload,
+ http_client_payload_destroyed);
+ conn->incoming_payload = NULL;
+ }
+
+ if (conn->http_parser != NULL)
+ http_response_parser_deinit(&conn->http_parser);
+
+ connection_disconnect(&conn->conn);
+
+ io_remove(&conn->io_req_payload);
+ timeout_remove(&conn->to_requests);
+ timeout_remove(&conn->to_connect);
+ timeout_remove(&conn->to_input);
+ timeout_remove(&conn->to_response);
+
+ /* Remove this connection from the lists */
+ conn_arr = &ppool->conns;
+ array_foreach(conn_arr, conn_idx) {
+ if (*conn_idx == conn) {
+ array_delete(conn_arr,
+ array_foreach_idx(conn_arr, conn_idx), 1);
+ break;
+ }
+ }
+ conn_arr = &ppool->pending_conns;
+ array_foreach(conn_arr, conn_idx) {
+ if (*conn_idx == conn) {
+ array_delete(conn_arr,
+ array_foreach_idx(conn_arr, conn_idx), 1);
+ break;
+ }
+ }
+
+ http_client_connection_detach_peer(conn);
+
+ http_client_connection_stop_idle(conn); // FIXME: needed?
+}
+
+bool http_client_connection_unref(struct http_client_connection **_conn)
+{
+ struct http_client_connection *conn = *_conn;
+ struct http_client_peer_pool *ppool = conn->ppool;
+
+ i_assert(conn->refcount > 0);
+
+ *_conn = NULL;
+
+ if (--conn->refcount > 0)
+ return TRUE;
+
+ e_debug(conn->event, "Connection destroy");
+
+ http_client_connection_disconnect(conn);
+ http_client_connection_abort_any_requests(conn);
+
+ i_assert(conn->io_req_payload == NULL);
+ i_assert(conn->to_requests == NULL);
+ i_assert(conn->to_connect == NULL);
+ i_assert(conn->to_input == NULL);
+ i_assert(conn->to_idle == NULL);
+ i_assert(conn->to_response == NULL);
+
+ if (array_is_created(&conn->request_wait_list))
+ array_free(&conn->request_wait_list);
+
+ ssl_iostream_destroy(&conn->ssl_iostream);
+ connection_deinit(&conn->conn);
+ io_wait_timer_remove(&conn->io_wait_timer);
+
+ i_free(conn);
+
+ http_client_peer_pool_unref(&ppool);
+ return FALSE;
+}
+
+void http_client_connection_close(struct http_client_connection **_conn)
+{
+ struct http_client_connection *conn = *_conn;
+
+ e_debug(conn->event, "Connection close");
+
+ http_client_connection_disconnect(conn);
+ http_client_connection_abort_any_requests(conn);
+ http_client_connection_unref(_conn);
+}
+
+void http_client_connection_switch_ioloop(struct http_client_connection *conn)
+{
+ struct http_client_peer_shared *pshared = conn->ppool->peer;
+ struct http_client_context *cctx = pshared->cctx;
+ struct ioloop *ioloop = cctx->ioloop;
+
+ connection_switch_ioloop_to(&conn->conn, ioloop);
+ if (conn->io_req_payload != NULL) {
+ conn->io_req_payload =
+ io_loop_move_io_to(ioloop, &conn->io_req_payload);
+ }
+ if (conn->to_requests != NULL) {
+ conn->to_requests =
+ io_loop_move_timeout_to(ioloop, &conn->to_requests);
+ }
+ if (conn->to_connect != NULL) {
+ conn->to_connect =
+ io_loop_move_timeout_to(ioloop, &conn->to_connect);
+ }
+ if (conn->to_input != NULL) {
+ conn->to_input =
+ io_loop_move_timeout_to(ioloop, &conn->to_input);
+ }
+ if (conn->to_idle != NULL) {
+ conn->to_idle =
+ io_loop_move_timeout_to(ioloop, &conn->to_idle);
+ }
+ if (conn->to_response != NULL) {
+ conn->to_response =
+ io_loop_move_timeout_to(ioloop, &conn->to_response);
+ }
+ if (conn->incoming_payload != NULL)
+ i_stream_switch_ioloop_to(conn->incoming_payload, ioloop);
+ conn->io_wait_timer =
+ io_wait_timer_move_to(&conn->io_wait_timer, ioloop);
+}
diff --git a/src/lib-http/http-client-host.c b/src/lib-http/http-client-host.c
new file mode 100644
index 0000000..647ab66
--- /dev/null
+++ b/src/lib-http/http-client-host.c
@@ -0,0 +1,500 @@
+/* Copyright (c) 2013-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "net.h"
+#include "str.h"
+#include "hash.h"
+#include "array.h"
+#include "llist.h"
+#include "ioloop.h"
+#include "istream.h"
+#include "ostream.h"
+#include "time-util.h"
+#include "dns-lookup.h"
+#include "http-response-parser.h"
+
+#include "http-client-private.h"
+
+#define HTTP_CLIENT_HOST_MINIMUM_IDLE_TIMEOUT_MSECS 100
+
+static void http_client_host_lookup_done(struct http_client_host *host);
+static void
+http_client_host_lookup_failure(struct http_client_host *host,
+ const char *error);
+static bool http_client_host_is_idle(struct http_client_host *host);
+static void http_client_host_free_shared(struct http_client_host **_host);
+
+/*
+ * Host (shared)
+ */
+
+static void
+http_client_host_shared_idle_timeout(struct http_client_host_shared *hshared)
+{
+ e_debug(hshared->event, "Idle host timed out");
+ http_client_host_shared_free(&hshared);
+}
+
+static void
+http_client_host_shared_check_idle(struct http_client_host_shared *hshared)
+{
+ struct http_client_host *host;
+ int timeout = 0;
+
+ if (hshared->destroyed)
+ return;
+ if (hshared->to_idle != NULL)
+ return;
+
+ host = hshared->hosts_list;
+ while (host != NULL) {
+ if (!http_client_host_is_idle(host))
+ return;
+ host = host->shared_next;
+ }
+
+ if (!hshared->unix_local && !hshared->explicit_ip &&
+ hshared->ips_timeout.tv_sec > 0) {
+ timeout = timeval_diff_msecs(&hshared->ips_timeout,
+ &ioloop_timeval);
+ }
+
+ if (timeout <= HTTP_CLIENT_HOST_MINIMUM_IDLE_TIMEOUT_MSECS)
+ timeout = HTTP_CLIENT_HOST_MINIMUM_IDLE_TIMEOUT_MSECS;
+
+ hshared->to_idle = timeout_add_to(hshared->cctx->ioloop, timeout,
+ http_client_host_shared_idle_timeout,
+ hshared);
+
+ e_debug(hshared->event, "Host is idle (timeout = %u msecs)", timeout);
+}
+
+static void
+http_client_host_shared_lookup_failure(struct http_client_host_shared *hshared,
+ const char *error)
+{
+ struct http_client_host *host;
+
+ e_debug(hshared->event, "DNS lookup failed: %s", error);
+
+ error = t_strdup_printf("Failed to lookup host %s: %s",
+ hshared->name, error);
+
+ host = hshared->hosts_list;
+ while (host != NULL) {
+ http_client_host_lookup_failure(host, error);
+ host = host->shared_next;
+ }
+
+ http_client_host_shared_check_idle(hshared);
+}
+
+static void
+http_client_host_shared_lookup_success(struct http_client_host_shared *hshared,
+ const struct ip_addr *ips,
+ unsigned int ips_count)
+{
+ struct http_client_context *cctx = hshared->cctx;
+
+ i_assert(ips_count > 0);
+
+ e_debug(hshared->event,
+ "DNS lookup successful; got %d IPs", ips_count);
+
+ hshared->ips = i_realloc_type(hshared->ips, struct ip_addr,
+ hshared->ips_count, ips_count);
+ hshared->ips_count = ips_count;
+ memcpy(hshared->ips, ips, sizeof(struct ip_addr) * ips_count);
+
+ hshared->ips_timeout = ioloop_timeval;
+ i_assert(cctx->dns_ttl_msecs > 0);
+ timeval_add_msecs(&hshared->ips_timeout, cctx->dns_ttl_msecs);
+}
+
+static void
+http_client_host_shared_dns_callback(const struct dns_lookup_result *result,
+ struct http_client_host_shared *hshared)
+{
+ struct http_client_host *host;
+
+ hshared->dns_lookup = NULL;
+
+ if (result->ret != 0) {
+ /* Lookup failed */
+ http_client_host_shared_lookup_failure(hshared, result->error);
+ return;
+ }
+
+ http_client_host_shared_lookup_success(hshared, result->ips,
+ result->ips_count);
+
+ /* Notify all sessions */
+ host = hshared->hosts_list;
+ while (host != NULL) {
+ http_client_host_lookup_done(host);
+ host = host->shared_next;
+ }
+}
+
+static void
+http_client_host_shared_lookup(struct http_client_host_shared *hshared)
+{
+ struct http_client_context *cctx = hshared->cctx;
+ struct dns_lookup_settings dns_set;
+ int ret;
+
+ i_assert(!hshared->explicit_ip);
+ i_assert(hshared->dns_lookup == NULL);
+
+ if (cctx->dns_client != NULL) {
+ e_debug(hshared->event, "Performing asynchronous DNS lookup");
+ (void)dns_client_lookup(cctx->dns_client, hshared->name,
+ http_client_host_shared_dns_callback,
+ hshared, &hshared->dns_lookup);
+ } else if (cctx->dns_client_socket_path != NULL) {
+ i_assert(cctx->dns_lookup_timeout_msecs > 0);
+ e_debug(hshared->event, "Performing asynchronous DNS lookup");
+ i_zero(&dns_set);
+ dns_set.dns_client_socket_path = cctx->dns_client_socket_path;
+ dns_set.timeout_msecs = cctx->dns_lookup_timeout_msecs;
+ dns_set.ioloop = cctx->ioloop;
+ dns_set.event_parent = hshared->event;
+ (void)dns_lookup(hshared->name, &dns_set,
+ http_client_host_shared_dns_callback,
+ hshared, &hshared->dns_lookup);
+ } else {
+ struct ip_addr *ips;
+ unsigned int ips_count;
+
+ ret = net_gethostbyname(hshared->name, &ips, &ips_count);
+ if (ret != 0) {
+ http_client_host_shared_lookup_failure(
+ hshared, net_gethosterror(ret));
+ return;
+ }
+
+ http_client_host_shared_lookup_success(hshared, ips, ips_count);
+ }
+}
+
+static int
+http_client_host_shared_refresh(struct http_client_host_shared *hshared)
+{
+ if (hshared->unix_local)
+ return 0;
+ if (hshared->explicit_ip)
+ return 0;
+
+ if (hshared->dns_lookup != NULL)
+ return -1;
+
+ if (hshared->ips_count == 0) {
+ e_debug(hshared->event, "Need to perform DNS lookup");
+ } else {
+ if (timeval_cmp(&hshared->ips_timeout, &ioloop_timeval) > 0)
+ return 0;
+
+ e_debug(hshared->event, "IPs have expired; "
+ "need to refresh DNS lookup");
+ }
+
+ http_client_host_shared_lookup(hshared);
+ if (hshared->dns_lookup != NULL)
+ return -1;
+ return (hshared->ips_count > 0 ? 1 : -1);
+}
+
+static struct http_client_host_shared *
+http_client_host_shared_create(struct http_client_context *cctx,
+ const char *name)
+{
+ struct http_client_host_shared *hshared;
+
+ // FIXME: limit the maximum number of inactive cached hosts
+ hshared = i_new(struct http_client_host_shared, 1);
+ hshared->cctx = cctx;
+ hshared->name = i_strdup(name);
+ hshared->event = event_create(cctx->event);
+ event_set_append_log_prefix(hshared->event,
+ t_strdup_printf("host %s: ", name));
+ DLLIST_PREPEND(&cctx->hosts_list, hshared);
+
+ return hshared;
+}
+
+static struct http_client_host_shared *
+http_client_host_shared_get(struct http_client_context *cctx,
+ const struct http_url *host_url)
+{
+ struct http_client_host_shared *hshared;
+
+ if (host_url == NULL) {
+ hshared = cctx->unix_host;
+ if (hshared == NULL) {
+ hshared = http_client_host_shared_create(
+ cctx, "[unix]");
+ hshared->name = i_strdup("[unix]");
+ hshared->unix_local = TRUE;
+
+ cctx->unix_host = hshared;
+
+ e_debug(hshared->event, "Unix host created");
+ }
+
+ } else {
+ const char *hostname = host_url->host.name;
+ struct ip_addr ip = host_url->host.ip;
+
+ hshared = hash_table_lookup(cctx->hosts, hostname);
+ if (hshared == NULL) {
+ hshared = http_client_host_shared_create(
+ cctx, hostname);
+ hostname = hshared->name;
+ hash_table_insert(cctx->hosts, hostname, hshared);
+
+ if (ip.family != 0 ||
+ net_addr2ip(hshared->name, &ip) == 0) {
+ hshared->ips_count = 1;
+ hshared->ips = i_new(struct ip_addr,
+ hshared->ips_count);
+ hshared->ips[0] = ip;
+ hshared->explicit_ip = TRUE;
+ }
+
+ e_debug(hshared->event, "Host created");
+ }
+ }
+ return hshared;
+}
+
+void http_client_host_shared_free(struct http_client_host_shared **_hshared)
+{
+ struct http_client_host_shared *hshared = *_hshared;
+ struct http_client_context *cctx = hshared->cctx;
+ struct http_client_host *host;
+ const char *hostname = hshared->name;
+
+ if (hshared->destroyed)
+ return;
+ hshared->destroyed = TRUE;
+
+ e_debug(hshared->event, "Host destroy");
+
+ timeout_remove(&hshared->to_idle);
+
+ DLLIST_REMOVE(&cctx->hosts_list, hshared);
+ if (hshared == cctx->unix_host)
+ cctx->unix_host = NULL;
+ else
+ hash_table_remove(cctx->hosts, hostname);
+
+ if (hshared->dns_lookup != NULL)
+ dns_lookup_abort(&hshared->dns_lookup);
+
+ /* Drop client sessions */
+ while (hshared->hosts_list != NULL) {
+ host = hshared->hosts_list;
+ http_client_host_free_shared(&host);
+ }
+
+ event_unref(&hshared->event);
+ i_free(hshared->ips);
+ i_free(hshared->name);
+ i_free(hshared);
+
+ *_hshared = NULL;
+}
+
+static void
+http_client_host_shared_request_submitted(
+ struct http_client_host_shared *hshared)
+{
+ /* Cancel host idle timeout */
+ timeout_remove(&hshared->to_idle);
+}
+
+void http_client_host_shared_switch_ioloop(
+ struct http_client_host_shared *hshared)
+{
+ struct http_client_context *cctx = hshared->cctx;
+
+ if (hshared->dns_lookup != NULL && cctx->dns_client == NULL)
+ dns_lookup_switch_ioloop(hshared->dns_lookup);
+ if (hshared->to_idle != NULL)
+ hshared->to_idle = io_loop_move_timeout(&hshared->to_idle);
+}
+
+/*
+ * Host
+ */
+
+struct http_client_host *
+http_client_host_get(struct http_client *client,
+ const struct http_url *host_url)
+{
+ struct http_client_host_shared *hshared;
+ struct http_client_host *host;
+
+ hshared = http_client_host_shared_get(client->cctx, host_url);
+
+ host = hshared->hosts_list;
+ while (host != NULL) {
+ if (host->client == client)
+ break;
+ host = host->shared_next;
+ }
+
+ if (host == NULL) {
+ host = i_new(struct http_client_host, 1);
+ host->client = client;
+ host->shared = hshared;
+ i_array_init(&host->queues, 4);
+ DLLIST_PREPEND_FULL(&hshared->hosts_list,
+ host, shared_prev, shared_next);
+ DLLIST_PREPEND_FULL(&client->hosts_list, host,
+ client_prev, client_next);
+
+ e_debug(hshared->event, "Host session created");
+ }
+
+ return host;
+}
+
+static void http_client_host_free_shared(struct http_client_host **_host)
+{
+ struct http_client_host *host = *_host;
+ struct http_client *client = host->client;
+ struct http_client_host_shared *hshared = host->shared;
+ struct http_client_queue *queue;
+ ARRAY_TYPE(http_client_queue) queues;
+
+ *_host = NULL;
+
+ e_debug(hshared->event, "Host session destroy");
+
+ DLLIST_REMOVE_FULL(&hshared->hosts_list, host,
+ shared_prev, shared_next);
+ DLLIST_REMOVE_FULL(&client->hosts_list, host,
+ client_prev, client_next);
+
+ /* Drop request queues */
+ t_array_init(&queues, array_count(&host->queues));
+ array_copy(&queues.arr, 0, &host->queues.arr, 0,
+ array_count(&host->queues));
+ array_clear(&host->queues);
+ array_foreach_elem(&queues, queue)
+ http_client_queue_free(queue);
+ array_free(&host->queues);
+
+ i_free(host);
+}
+
+void http_client_host_free(struct http_client_host **_host)
+{
+ struct http_client_host *host = *_host;
+ struct http_client_host_shared *hshared = host->shared;
+
+ http_client_host_free_shared(_host);
+
+ http_client_host_shared_check_idle(hshared);
+}
+
+static void http_client_host_lookup_done(struct http_client_host *host)
+{
+ struct http_client *client = host->client;
+ struct http_client_queue *queue;
+ unsigned int requests = 0;
+
+ /* Notify all queues */
+ array_foreach_elem(&host->queues, queue)
+ requests += http_client_queue_host_lookup_done(queue);
+
+ if (requests == 0 && client->waiting)
+ io_loop_stop(client->ioloop);
+}
+
+static void
+http_client_host_lookup_failure(struct http_client_host *host,
+ const char *error)
+{
+ struct http_client_queue *queue;
+
+ array_foreach_elem(&host->queues, queue)
+ http_client_queue_host_lookup_failure(queue, error);
+}
+
+void http_client_host_submit_request(struct http_client_host *host,
+ struct http_client_request *req)
+{
+ struct http_client *client = req->client;
+ struct http_client_queue *queue;
+ struct http_client_peer_addr addr;
+ const char *error;
+
+ req->host = host;
+
+ http_client_request_get_peer_addr(req, &addr);
+ if (http_client_peer_addr_is_https(&addr) &&
+ client->ssl_ctx == NULL) {
+ if (http_client_init_ssl_ctx(client, &error) < 0) {
+ http_client_request_error(
+ &req, HTTP_CLIENT_REQUEST_ERROR_CONNECT_FAILED,
+ error);
+ return;
+ }
+ }
+
+ /* Add request to queue */
+ queue = http_client_queue_get(host, &addr);
+ http_client_queue_submit_request(queue, req);
+
+ /* Update shared host object (idle timeout) */
+ http_client_host_shared_request_submitted(host->shared);
+
+ /* Queue will trigger host lookup once the request is activated
+ (may be delayed) */
+}
+
+static bool http_client_host_is_idle(struct http_client_host *host)
+{
+ struct http_client_queue *queue;
+ unsigned int requests = 0;
+
+ array_foreach_elem(&host->queues, queue)
+ requests += http_client_queue_requests_active(queue);
+
+ return (requests == 0);
+}
+
+void http_client_host_check_idle(struct http_client_host *host)
+{
+ http_client_host_shared_check_idle(host->shared);
+}
+
+int http_client_host_refresh(struct http_client_host *host)
+{
+ return http_client_host_shared_refresh(host->shared);
+}
+
+bool http_client_host_get_ip_idx(struct http_client_host *host,
+ const struct ip_addr *ip, unsigned int *idx_r)
+{
+ struct http_client_host_shared *hshared = host->shared;
+ unsigned int i;
+
+ for (i = 0; i < hshared->ips_count; i++) {
+ if (net_ip_compare(&hshared->ips[i], ip)) {
+ *idx_r = i;
+ return TRUE;
+ }
+ }
+ return FALSE;
+}
+
+void http_client_host_switch_ioloop(struct http_client_host *host)
+{
+ struct http_client_queue *queue;
+
+ array_foreach_elem(&host->queues, queue)
+ http_client_queue_switch_ioloop(queue);
+}
diff --git a/src/lib-http/http-client-peer.c b/src/lib-http/http-client-peer.c
new file mode 100644
index 0000000..eff40b4
--- /dev/null
+++ b/src/lib-http/http-client-peer.c
@@ -0,0 +1,1383 @@
+/* Copyright (c) 2013-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "net.h"
+#include "time-util.h"
+#include "str.h"
+#include "hash.h"
+#include "array.h"
+#include "llist.h"
+#include "istream.h"
+#include "ostream.h"
+#include "iostream-ssl.h"
+#include "http-response-parser.h"
+
+#include "http-client-private.h"
+
+static void
+http_client_peer_connect_backoff(struct http_client_peer *peer);
+
+static void
+http_client_peer_shared_connection_success(
+ struct http_client_peer_shared *pshared);
+static void
+http_client_peer_shared_connection_failure(
+ struct http_client_peer_shared *pshared);
+static void
+http_client_peer_connection_succeeded_pool(struct http_client_peer *peer);
+static void
+http_client_peer_connection_failed_pool(struct http_client_peer *peer,
+ const char *reason);
+
+/*
+ * Peer address
+ */
+
+unsigned int ATTR_NO_SANITIZE_INTEGER
+http_client_peer_addr_hash(const struct http_client_peer_addr *peer)
+{
+ unsigned int hash = (unsigned int)peer->type;
+
+ switch (peer->type) {
+ case HTTP_CLIENT_PEER_ADDR_HTTPS:
+ case HTTP_CLIENT_PEER_ADDR_HTTPS_TUNNEL:
+ if (peer->a.tcp.https_name != NULL)
+ hash += str_hash(peer->a.tcp.https_name);
+ /* fall through */
+ case HTTP_CLIENT_PEER_ADDR_RAW:
+ case HTTP_CLIENT_PEER_ADDR_HTTP:
+ if (peer->a.tcp.ip.family != 0)
+ hash += net_ip_hash(&peer->a.tcp.ip);
+ hash += peer->a.tcp.port;
+ break;
+ case HTTP_CLIENT_PEER_ADDR_UNIX:
+ hash += str_hash(peer->a.un.path);
+ break;
+ }
+
+ return hash;
+}
+
+int http_client_peer_addr_cmp(const struct http_client_peer_addr *peer1,
+ const struct http_client_peer_addr *peer2)
+{
+ int ret;
+
+ if (peer1->type != peer2->type)
+ return (peer1->type > peer2->type ? 1 : -1);
+ switch (peer1->type) {
+ case HTTP_CLIENT_PEER_ADDR_RAW:
+ case HTTP_CLIENT_PEER_ADDR_HTTP:
+ case HTTP_CLIENT_PEER_ADDR_HTTPS:
+ case HTTP_CLIENT_PEER_ADDR_HTTPS_TUNNEL:
+ /* Queues are created with peer addresses that have an
+ uninitialized IP value, because that is assigned later when
+ the host lookup completes. In all other other contexts, the
+ IP is always initialized, so we do not compare IPs when one
+ of them is unassigned. */
+ if (peer1->a.tcp.ip.family != 0 &&
+ peer2->a.tcp.ip.family != 0 &&
+ (ret = net_ip_cmp(&peer1->a.tcp.ip, &peer2->a.tcp.ip)) != 0)
+ return ret;
+ if (peer1->a.tcp.port != peer2->a.tcp.port)
+ return (peer1->a.tcp.port > peer2->a.tcp.port ? 1 : -1);
+ if (peer1->type != HTTP_CLIENT_PEER_ADDR_HTTPS &&
+ peer1->type != HTTP_CLIENT_PEER_ADDR_HTTPS_TUNNEL)
+ return 0;
+ return null_strcmp(peer1->a.tcp.https_name,
+ peer2->a.tcp.https_name);
+ case HTTP_CLIENT_PEER_ADDR_UNIX:
+ return null_strcmp(peer1->a.un.path, peer2->a.un.path);
+ }
+ i_unreached();
+}
+
+/*
+ * Peer pool
+ */
+
+static struct http_client_peer_pool *
+http_client_peer_pool_create(struct http_client_peer_shared *pshared,
+ struct ssl_iostream_context *ssl_ctx,
+ const char *rawlog_dir)
+{
+ struct http_client_peer_pool *ppool;
+
+ ppool = i_new(struct http_client_peer_pool, 1);
+ ppool->refcount = 1;
+ ppool->peer = pshared;
+ ppool->event = event_create(pshared->cctx->event);
+ event_set_append_log_prefix(
+ ppool->event,
+ t_strdup_printf("peer %s: ",
+ http_client_peer_shared_label(pshared)));
+
+ http_client_peer_shared_ref(pshared);
+
+ i_array_init(&ppool->conns, 16);
+ i_array_init(&ppool->pending_conns, 16);
+ i_array_init(&ppool->idle_conns, 16);
+
+ DLLIST_PREPEND(&pshared->pools_list, ppool);
+
+ ppool->ssl_ctx = ssl_ctx;
+ ppool->rawlog_dir = i_strdup(rawlog_dir);
+
+ e_debug(ppool->event, "Peer pool created");
+
+ return ppool;
+}
+
+void http_client_peer_pool_ref(struct http_client_peer_pool *ppool)
+{
+ if (ppool->destroyed)
+ return;
+ ppool->refcount++;
+}
+
+void http_client_peer_pool_close(struct http_client_peer_pool **_ppool)
+{
+ struct http_client_peer_pool *ppool = *_ppool;
+ struct http_client_connection *conn;
+ ARRAY_TYPE(http_client_connection) conns;
+
+ http_client_peer_pool_ref(ppool);
+
+ /* Make a copy of the connection array; freed connections modify it */
+ t_array_init(&conns, array_count(&ppool->conns));
+ array_copy(&conns.arr, 0, &ppool->conns.arr, 0,
+ array_count(&ppool->conns));
+ array_foreach_elem(&conns, conn)
+ http_client_connection_unref(&conn);
+ i_assert(array_count(&ppool->idle_conns) == 0);
+ i_assert(array_count(&ppool->pending_conns) == 0);
+ i_assert(array_count(&ppool->conns) == 0);
+
+ http_client_peer_pool_unref(_ppool);
+}
+
+void http_client_peer_pool_unref(struct http_client_peer_pool **_ppool)
+{
+ struct http_client_peer_pool *ppool = *_ppool;
+ struct http_client_peer_shared *pshared = ppool->peer;
+
+ *_ppool = NULL;
+
+ if (ppool->destroyed)
+ return;
+
+ i_assert(ppool->refcount > 0);
+ if (--ppool->refcount > 0)
+ return;
+
+ e_debug(ppool->event, "Peer pool destroy");
+ ppool->destroyed = TRUE;
+
+ i_assert(array_count(&ppool->idle_conns) == 0);
+ i_assert(array_count(&ppool->conns) == 0);
+ array_free(&ppool->idle_conns);
+ array_free(&ppool->pending_conns);
+ array_free(&ppool->conns);
+
+ DLLIST_REMOVE(&pshared->pools_list, ppool);
+
+ event_unref(&ppool->event);
+ i_free(ppool->rawlog_dir);
+ i_free(ppool);
+ http_client_peer_shared_unref(&pshared);
+}
+
+static struct http_client_peer_pool *
+http_client_peer_pool_get(struct http_client_peer_shared *pshared,
+ struct http_client *client)
+{
+ struct http_client_peer_pool *ppool;
+ struct ssl_iostream_context *ssl_ctx = client->ssl_ctx;
+ const char *rawlog_dir = client->set.rawlog_dir;
+
+ i_assert(!http_client_peer_addr_is_https(&pshared->addr) ||
+ ssl_ctx != NULL);
+
+ ppool = pshared->pools_list;
+ while (ppool != NULL) {
+ if (ppool->ssl_ctx == ssl_ctx &&
+ null_strcmp(ppool->rawlog_dir, rawlog_dir) == 0)
+ break;
+ ppool = ppool->next;
+ }
+
+ if (ppool == NULL) {
+ ppool = http_client_peer_pool_create(pshared, ssl_ctx,
+ rawlog_dir);
+ } else {
+ e_debug(ppool->event, "Peer pool reused");
+ http_client_peer_pool_ref(ppool);
+ }
+
+ return ppool;
+}
+
+static void
+http_client_peer_pool_connection_success(struct http_client_peer_pool *ppool)
+{
+ e_debug(ppool->event, "Successfully connected "
+ "(%u connections exist, %u pending)",
+ array_count(&ppool->conns), array_count(&ppool->pending_conns));
+
+ http_client_peer_shared_connection_success(ppool->peer);
+
+ if (array_count(&ppool->pending_conns) > 0) {
+ /* If there are other connections attempting to connect, wait
+ for them before notifying other peer objects about the
+ success (which may be premature). */
+ } else {
+ struct http_client_peer *peer;
+
+ /* This was the only/last connection and connecting to it
+ succeeded. notify all interested peers in this pool about the
+ success */
+ peer = ppool->peer->peers_list;
+ while (peer != NULL) {
+ struct http_client_peer *peer_next = peer->shared_next;
+ if (peer->ppool == ppool)
+ http_client_peer_connection_succeeded_pool(peer);
+ peer = peer_next;
+ }
+ }
+}
+
+static void
+http_client_peer_pool_connection_failure(struct http_client_peer_pool *ppool,
+ const char *reason)
+{
+ e_debug(ppool->event,
+ "Failed to make connection "
+ "(%u connections exist, %u pending)",
+ array_count(&ppool->conns), array_count(&ppool->pending_conns));
+
+ http_client_peer_shared_connection_failure(ppool->peer);
+
+ if (array_count(&ppool->pending_conns) > 0) {
+ /* If there are other connections attempting to connect, wait
+ for them before failing the requests. remember that we had
+ trouble with connecting so in future we don't try to create
+ more than one connection until connects work again. */
+ } else {
+ struct http_client_peer *peer;
+
+ /* This was the only/last connection and connecting to it
+ failed. notify all interested peers in this pool about the
+ failure */
+ peer = ppool->peer->peers_list;
+ while (peer != NULL) {
+ struct http_client_peer *peer_next = peer->shared_next;
+ if (peer->ppool == ppool)
+ http_client_peer_connection_failed_pool(peer, reason);
+ peer = peer_next;
+ }
+ }
+}
+
+/*
+ * Peer (shared)
+ */
+
+static struct http_client_peer_shared *
+http_client_peer_shared_create(struct http_client_context *cctx,
+ const struct http_client_peer_addr *addr)
+{
+ struct http_client_peer_shared *pshared;
+
+ pshared = i_new(struct http_client_peer_shared, 1);
+ pshared->refcount = 1;
+ pshared->cctx = cctx;
+
+ pshared->addr = *addr;
+ switch (addr->type) {
+ case HTTP_CLIENT_PEER_ADDR_RAW:
+ case HTTP_CLIENT_PEER_ADDR_HTTP:
+ i_assert(pshared->addr.a.tcp.ip.family != 0);
+ break;
+ case HTTP_CLIENT_PEER_ADDR_HTTPS:
+ case HTTP_CLIENT_PEER_ADDR_HTTPS_TUNNEL:
+ i_assert(pshared->addr.a.tcp.ip.family != 0);
+ pshared->addr_name = i_strdup(addr->a.tcp.https_name);
+ pshared->addr.a.tcp.https_name = pshared->addr_name;
+ break;
+ case HTTP_CLIENT_PEER_ADDR_UNIX:
+ pshared->addr_name = i_strdup(addr->a.un.path);
+ pshared->addr.a.un.path = pshared->addr_name;
+ break;
+ default:
+ break;
+ }
+ pshared->event = event_create(cctx->event);
+ event_set_append_log_prefix(
+ pshared->event,
+ t_strdup_printf("peer %s (shared): ",
+ http_client_peer_shared_label(pshared)));
+
+ hash_table_insert(cctx->peers,
+ (const struct http_client_peer_addr *)&pshared->addr,
+ pshared);
+ DLLIST_PREPEND(&cctx->peers_list, pshared);
+
+ pshared->backoff_initial_time_msecs =
+ cctx->set.connect_backoff_time_msecs;
+ pshared->backoff_max_time_msecs =
+ cctx->set.connect_backoff_max_time_msecs;
+
+ e_debug(pshared->event, "Peer created");
+ return pshared;
+}
+
+void http_client_peer_shared_ref(struct http_client_peer_shared *pshared)
+{
+ pshared->refcount++;
+}
+
+void http_client_peer_shared_unref(struct http_client_peer_shared **_pshared)
+{
+ struct http_client_peer_shared *pshared = *_pshared;
+
+ *_pshared = NULL;
+
+ i_assert(pshared->refcount > 0);
+ if (--pshared->refcount > 0)
+ return;
+
+ e_debug(pshared->event, "Peer destroy");
+
+ i_assert(pshared->pools_list == NULL);
+
+ /* Unlist in client */
+ hash_table_remove(pshared->cctx->peers,
+ (const struct http_client_peer_addr *)&pshared->addr);
+ DLLIST_REMOVE(&pshared->cctx->peers_list, pshared);
+
+ timeout_remove(&pshared->to_backoff);
+
+ event_unref(&pshared->event);
+ i_free(pshared->addr_name);
+ i_free(pshared->label);
+ i_free(pshared);
+}
+
+static struct http_client_peer_shared *
+http_client_peer_shared_get(struct http_client_context *cctx,
+ const struct http_client_peer_addr *addr)
+{
+ struct http_client_peer_shared *pshared;
+
+ pshared = hash_table_lookup(cctx->peers, addr);
+ if (pshared == NULL) {
+ pshared = http_client_peer_shared_create(cctx, addr);
+ } else {
+ e_debug(pshared->event, "Peer reused");
+ http_client_peer_shared_ref(pshared);
+ }
+
+ return pshared;
+}
+
+void http_client_peer_shared_close(struct http_client_peer_shared **_pshared)
+{
+ struct http_client_peer_shared *pshared = *_pshared;
+ struct http_client_peer_pool *pool, *next;
+
+ http_client_peer_shared_ref(pshared);
+ pool = pshared->pools_list;
+ while (pool != NULL) {
+ next = pool->next;
+ http_client_peer_pool_close(&pool);
+ pool = next;
+ }
+ http_client_peer_shared_unref(_pshared);
+}
+
+const char *
+http_client_peer_shared_label(struct http_client_peer_shared *pshared)
+{
+ if (pshared->label == NULL) {
+ switch (pshared->addr.type) {
+ case HTTP_CLIENT_PEER_ADDR_HTTPS_TUNNEL:
+ pshared->label = i_strconcat(
+ http_client_peer_addr2str(&pshared->addr),
+ " (tunnel)", NULL);
+ break;
+ default:
+ pshared->label = i_strdup(
+ http_client_peer_addr2str(&pshared->addr));
+ }
+ }
+ return pshared->label;
+}
+
+static void
+http_client_peer_shared_connect_backoff(struct http_client_peer_shared *pshared)
+{
+ struct http_client_peer *peer;
+
+ i_assert(pshared->to_backoff != NULL);
+
+ e_debug(pshared->event, "Backoff timer expired");
+
+ timeout_remove(&pshared->to_backoff);
+
+ peer = pshared->peers_list;
+ while (peer != NULL) {
+ struct http_client_peer *peer_next = peer->shared_next;
+
+ http_client_peer_connect_backoff(peer);
+ peer = peer_next;
+ }
+}
+
+static bool
+http_client_peer_shared_start_backoff_timer(
+ struct http_client_peer_shared *pshared)
+{
+ if (pshared->to_backoff != NULL)
+ return TRUE;
+
+ if (pshared->last_failure.tv_sec > 0) {
+ int backoff_time_spent =
+ timeval_diff_msecs(&ioloop_timeval,
+ &pshared->last_failure);
+
+ if (backoff_time_spent < (int)pshared->backoff_current_time_msecs) {
+ unsigned int new_time = (unsigned int)
+ (pshared->backoff_current_time_msecs -
+ backoff_time_spent);
+ e_debug(pshared->event,
+ "Starting backoff timer for %d msecs", new_time);
+ pshared->to_backoff = timeout_add_to(
+ pshared->cctx->ioloop, new_time,
+ http_client_peer_shared_connect_backoff, pshared);
+ return TRUE;
+ }
+
+ e_debug(pshared->event,
+ "Backoff time already exceeded by %d msecs",
+ (backoff_time_spent -
+ pshared->backoff_current_time_msecs));
+ }
+ return FALSE;
+}
+
+static void
+http_client_peer_shared_increase_backoff_timer(
+ struct http_client_peer_shared *pshared)
+{
+ if (pshared->backoff_current_time_msecs == 0) {
+ pshared->backoff_current_time_msecs =
+ pshared->backoff_initial_time_msecs;
+ } else {
+ pshared->backoff_current_time_msecs *= 2;
+ }
+ if (pshared->backoff_current_time_msecs >
+ pshared->backoff_max_time_msecs) {
+ pshared->backoff_current_time_msecs =
+ pshared->backoff_max_time_msecs;
+ }
+}
+
+static void
+http_client_peer_shared_reset_backoff_timer(
+ struct http_client_peer_shared *pshared)
+{
+ pshared->backoff_current_time_msecs = 0;
+
+ timeout_remove(&pshared->to_backoff);
+}
+
+static void
+http_client_peer_shared_connection_success(
+ struct http_client_peer_shared *pshared)
+{
+ pshared->last_failure.tv_sec = pshared->last_failure.tv_usec = 0;
+ http_client_peer_shared_reset_backoff_timer(pshared);
+}
+
+static void
+http_client_peer_shared_connection_failure(
+ struct http_client_peer_shared *pshared)
+{
+ struct http_client_peer_pool *ppool;
+ unsigned int pending = 0;
+
+ /* Determine the number of connections still pending */
+ ppool = pshared->pools_list;
+ while (ppool != NULL) {
+ pending += array_count(&ppool->pending_conns);
+ ppool = ppool->next;
+ }
+
+ pshared->last_failure = ioloop_timeval;
+
+ /* Manage backoff timer only when this was the only attempt */
+ if (pending == 0)
+ http_client_peer_shared_increase_backoff_timer(pshared);
+}
+
+static void
+http_client_peer_shared_connection_lost(struct http_client_peer_shared *pshared,
+ bool premature)
+{
+ /* Update backoff timer if the connection was lost prematurely. this
+ prevents reconnecting immediately to a server that is misbehaving by
+ disconnecting before sending a response.
+ */
+ if (premature) {
+ pshared->last_failure = ioloop_timeval;
+ http_client_peer_shared_increase_backoff_timer(pshared);
+ }
+}
+
+void http_client_peer_shared_switch_ioloop(
+ struct http_client_peer_shared *pshared)
+{
+ if (pshared->to_backoff != NULL) {
+ pshared->to_backoff =
+ io_loop_move_timeout(&pshared->to_backoff);
+ }
+}
+
+unsigned int
+http_client_peer_shared_max_connections(struct http_client_peer_shared *pshared)
+{
+ struct http_client_peer *peer;
+ unsigned int max_conns = 0;
+
+ peer = pshared->peers_list;
+ while (peer != NULL) {
+ const struct http_client_settings *set = &peer->client->set;
+ unsigned int client_max_conns = set->max_parallel_connections;
+
+ if ((UINT_MAX - max_conns) <= client_max_conns)
+ return UINT_MAX;
+ max_conns += client_max_conns;
+ peer = peer->shared_next;
+ }
+
+ return max_conns;
+}
+
+/*
+ * Peer
+ */
+
+static void http_client_peer_drop(struct http_client_peer **_peer);
+
+static struct http_client_peer *
+http_client_peer_create(struct http_client *client,
+ struct http_client_peer_shared *pshared)
+{
+ struct http_client_peer *peer;
+
+ peer = i_new(struct http_client_peer, 1);
+ peer->refcount = 1;
+ peer->client = client;
+ peer->shared = pshared;
+
+ peer->event = event_create(client->event);
+ event_set_append_log_prefix(peer->event, t_strdup_printf(
+ "peer %s: ", http_client_peer_shared_label(pshared)));
+
+ i_array_init(&peer->queues, 16);
+ i_array_init(&peer->conns, 16);
+ i_array_init(&peer->pending_conns, 16);
+
+ DLLIST_PREPEND_FULL(&client->peers_list, peer,
+ client_prev, client_next);
+ DLLIST_PREPEND_FULL(&pshared->peers_list, peer,
+ shared_prev, shared_next);
+ pshared->peers_count++;
+
+ http_client_peer_shared_ref(pshared);
+ peer->ppool = http_client_peer_pool_get(pshared, client);
+
+ /* Choose backoff times */
+ if (pshared->peers_list == NULL ||
+ (client->set.connect_backoff_time_msecs <
+ pshared->backoff_initial_time_msecs)) {
+ pshared->backoff_initial_time_msecs =
+ client->set.connect_backoff_time_msecs;
+ }
+ if (pshared->peers_list == NULL ||
+ (client->set.connect_backoff_max_time_msecs >
+ pshared->backoff_max_time_msecs)) {
+ pshared->backoff_max_time_msecs =
+ client->set.connect_backoff_max_time_msecs;
+ }
+
+ e_debug(peer->event, "Peer created");
+ return peer;
+}
+
+void http_client_peer_ref(struct http_client_peer *peer)
+{
+ peer->refcount++;
+}
+
+static void http_client_peer_disconnect(struct http_client_peer *peer)
+{
+ struct http_client_queue *queue;
+ struct http_client *client = peer->client;
+ struct http_client_peer_shared *pshared = peer->shared;
+ struct http_client_connection *conn;
+ ARRAY_TYPE(http_client_connection) conns;
+
+ if (peer->disconnected)
+ return;
+ peer->disconnected = TRUE;
+
+ e_debug(peer->event, "Peer disconnect");
+
+ /* Make a copy of the connection array; freed connections modify it */
+ t_array_init(&conns, array_count(&peer->conns));
+ array_copy(&conns.arr, 0, &peer->conns.arr, 0,
+ array_count(&peer->conns));
+ array_foreach_elem(&conns, conn)
+ http_client_connection_lost_peer(conn);
+ i_assert(array_count(&peer->conns) == 0);
+ array_clear(&peer->pending_conns);
+
+ timeout_remove(&peer->to_req_handling);
+
+ /* Unlist in client */
+ DLLIST_REMOVE_FULL(&client->peers_list, peer,
+ client_prev, client_next);
+ /* Unlist in peer */
+ DLLIST_REMOVE_FULL(&pshared->peers_list, peer,
+ shared_prev, shared_next);
+ pshared->peers_count--;
+
+ /* Unlink all queues */
+ array_foreach_elem(&peer->queues, queue)
+ http_client_queue_peer_disconnected(queue, peer);
+ array_clear(&peer->queues);
+}
+
+bool http_client_peer_unref(struct http_client_peer **_peer)
+{
+ struct http_client_peer *peer = *_peer;
+ struct http_client_peer_pool *ppool = peer->ppool;
+ struct http_client_peer_shared *pshared = peer->shared;
+
+ *_peer = NULL;
+
+ i_assert(peer->refcount > 0);
+ if (--peer->refcount > 0)
+ return TRUE;
+
+ e_debug(peer->event, "Peer destroy");
+
+ http_client_peer_disconnect(peer);
+
+ i_assert(array_count(&peer->queues) == 0);
+
+ event_unref(&peer->event);
+ array_free(&peer->conns);
+ array_free(&peer->pending_conns);
+ array_free(&peer->queues);
+ i_free(peer);
+
+ /* Choose new backoff times */
+ peer = pshared->peers_list;
+ while (peer != NULL) {
+ struct http_client *client = peer->client;
+
+ if (client->set.connect_backoff_time_msecs <
+ pshared->backoff_initial_time_msecs) {
+ pshared->backoff_initial_time_msecs =
+ client->set.connect_backoff_time_msecs;
+ }
+ if (client->set.connect_backoff_max_time_msecs >
+ pshared->backoff_max_time_msecs) {
+ pshared->backoff_max_time_msecs =
+ client->set.connect_backoff_max_time_msecs;
+ }
+ peer = peer->shared_next;
+ }
+
+ http_client_peer_pool_unref(&ppool);
+ http_client_peer_shared_unref(&pshared);
+ return FALSE;
+}
+
+void http_client_peer_close(struct http_client_peer **_peer)
+{
+ struct http_client_peer *peer = *_peer;
+
+ e_debug(peer->event, "Peer close");
+
+ http_client_peer_disconnect(peer);
+
+ (void)http_client_peer_unref(_peer);
+}
+
+static void http_client_peer_drop(struct http_client_peer **_peer)
+{
+ struct http_client_peer *peer = *_peer;
+ struct http_client_peer_shared *pshared = peer->shared;
+ unsigned int conns_active =
+ http_client_peer_active_connections(peer);
+
+ if (conns_active > 0) {
+ e_debug(peer->event,
+ "Not dropping peer (%d connections active)",
+ conns_active);
+ return;
+ }
+
+ if (pshared->to_backoff != NULL)
+ return;
+
+ if (http_client_peer_shared_start_backoff_timer(pshared)) {
+ e_debug(peer->event,
+ "Dropping peer (waiting for backof timeout)");
+
+ /* Will disconnect any pending connections */
+ http_client_peer_trigger_request_handler(peer);
+ } else {
+ e_debug(peer->event, "Dropping peer now");
+ /* Drop peer immediately */
+ http_client_peer_close(_peer);
+ }
+}
+
+struct http_client_peer *
+http_client_peer_get(struct http_client *client,
+ const struct http_client_peer_addr *addr)
+{
+ struct http_client_peer *peer;
+ struct http_client_peer_shared *pshared;
+
+ pshared = http_client_peer_shared_get(client->cctx, addr);
+
+ peer = pshared->peers_list;
+ while (peer != NULL) {
+ if (peer->client == client)
+ break;
+ peer = peer->shared_next;
+ }
+
+ if (peer == NULL)
+ peer = http_client_peer_create(client, pshared);
+
+ http_client_peer_shared_unref(&pshared);
+ return peer;
+}
+
+static void
+http_client_peer_do_connect(struct http_client_peer *peer, unsigned int count)
+{
+ struct http_client_peer_pool *ppool = peer->ppool;
+ struct http_client_connection *const *idle_conns;
+ unsigned int i, idle_count;
+ bool claimed_existing = FALSE;
+
+ if (count == 0)
+ return;
+
+ idle_conns = array_get(&ppool->idle_conns, &idle_count);
+ for (i = 0; i < count && i < idle_count; i++) {
+ http_client_connection_claim_idle(idle_conns[i], peer);
+ claimed_existing = TRUE;
+
+ e_debug(peer->event,
+ "Claimed idle connection "
+ "(%u connections exist, %u pending)",
+ array_count(&peer->conns),
+ array_count(&peer->pending_conns));
+ }
+
+ for (; i < count; i++) {
+ e_debug(peer->event,
+ "Making new connection %u of %u "
+ "(%u connections exist, %u pending)",
+ i+1, count, array_count(&peer->conns),
+ array_count(&peer->pending_conns));
+
+ (void)http_client_connection_create(peer);
+ }
+
+ if (claimed_existing)
+ http_client_peer_connection_success(peer);
+}
+
+static void http_client_peer_connect_backoff(struct http_client_peer *peer)
+{
+ if (peer->connect_backoff && array_count(&peer->queues) == 0) {
+ http_client_peer_close(&peer);
+ return;
+ }
+
+ http_client_peer_do_connect(peer, 1);
+ peer->connect_backoff = FALSE;
+}
+
+static void
+http_client_peer_connect(struct http_client_peer *peer, unsigned int count)
+{
+ if (http_client_peer_shared_start_backoff_timer(peer->shared)) {
+ peer->connect_backoff = TRUE;
+ return;
+ }
+
+ http_client_peer_do_connect(peer, count);
+}
+
+bool http_client_peer_is_connected(struct http_client_peer *peer)
+{
+ struct http_client_connection *conn;
+
+ if (array_count(&peer->ppool->idle_conns) > 0)
+ return TRUE;
+
+ array_foreach_elem(&peer->conns, conn) {
+ if (conn->connected)
+ return TRUE;
+ }
+
+ return FALSE;
+}
+
+static void
+http_client_peer_cancel(struct http_client_peer *peer)
+{
+ struct http_client_connection *conn;
+ ARRAY_TYPE(http_client_connection) conns;
+
+ e_debug(peer->event, "Peer cancel");
+
+ /* Make a copy of the connection array; freed connections modify it */
+ t_array_init(&conns, array_count(&peer->conns));
+ array_copy(&conns.arr, 0, &peer->conns.arr, 0,
+ array_count(&peer->conns));
+ array_foreach_elem(&conns, conn) {
+ if (!http_client_connection_is_active(conn))
+ http_client_connection_close(&conn);
+ }
+ i_assert(array_count(&peer->pending_conns) == 0);
+}
+
+static unsigned int
+http_client_peer_requests_pending(struct http_client_peer *peer,
+ unsigned int *num_urgent_r)
+{
+ struct http_client_queue *queue;
+ unsigned int num_requests = 0, num_urgent = 0, requests, urgent;
+
+ array_foreach_elem(&peer->queues, queue) {
+ requests = http_client_queue_requests_pending(queue, &urgent);
+
+ num_requests += requests;
+ num_urgent += urgent;
+ }
+ *num_urgent_r = num_urgent;
+ return num_requests;
+}
+
+static void http_client_peer_check_idle(struct http_client_peer *peer)
+{
+ struct http_client_connection *conn;
+ unsigned int num_urgent = 0;
+
+ if (array_count(&peer->conns) == 0 &&
+ http_client_peer_requests_pending(peer, &num_urgent) == 0) {
+ /* No connections or pending requests; disconnect immediately */
+ http_client_peer_drop(&peer);
+ return;
+ }
+
+ /* Check all connections for idle status */
+ array_foreach_elem(&peer->conns, conn)
+ http_client_connection_check_idle(conn);
+}
+
+static void
+http_client_peer_handle_requests_real(struct http_client_peer *peer)
+{
+ struct _conn_available {
+ struct http_client_connection *conn;
+ unsigned int pending_requests;
+ };
+ struct http_client_connection *const *conn_idx;
+ ARRAY(struct _conn_available) conns_avail;
+ struct _conn_available *conn_avail_idx;
+ struct http_client_peer_shared *pshared = peer->shared;
+ unsigned int connecting, closing, idle;
+ unsigned int num_pending, num_urgent, new_connections;
+ unsigned int working_conn_count;
+ struct http_client_peer *tmp_peer;
+ bool statistics_dirty = TRUE;
+
+ /* FIXME: limit the number of requests handled in one run to prevent
+ I/O starvation. */
+
+ /* Disconnect pending connections if we're not linked to any queue
+ anymore */
+ if (array_count(&peer->queues) == 0) {
+ if (array_count(&peer->conns) == 0 &&
+ pshared->to_backoff == NULL) {
+ /* Peer is completely unused and inactive; drop it
+ immediately */
+ http_client_peer_drop(&peer);
+ return;
+ }
+ e_debug(peer->event,
+ "Peer no longer used; will now cancel pending connections "
+ "(%u connections exist, %u pending)",
+ array_count(&peer->conns),
+ array_count(&peer->pending_conns));
+
+ http_client_peer_cancel(peer);
+ return;
+ }
+
+ /* Don't do anything unless we have pending requests */
+ num_pending = http_client_peer_requests_pending(peer, &num_urgent);
+ if (num_pending == 0) {
+ e_debug(peer->event,
+ "No requests to service for this peer "
+ "(%u connections exist, %u pending)",
+ array_count(&peer->conns),
+ array_count(&peer->pending_conns));
+ http_client_peer_check_idle(peer);
+ return;
+ }
+
+ http_client_peer_ref(peer);
+ peer->handling_requests = TRUE;
+ t_array_init(&conns_avail, array_count(&peer->conns));
+ do {
+ bool conn_lost = FALSE;
+
+ array_clear(&conns_avail);
+ closing = idle = 0;
+
+ /* Gather connection statistics */
+ array_foreach(&peer->conns, conn_idx) {
+ struct http_client_connection *conn = *conn_idx;
+ int ret;
+
+ ret = http_client_connection_check_ready(conn);
+ if (ret < 0) {
+ conn_lost = TRUE;
+ break;
+ } else if (ret > 0) {
+ struct _conn_available *conn_avail;
+ unsigned int insert_idx, pending_requests;
+
+ /* Compile sorted availability list */
+ pending_requests = http_client_connection_count_pending(conn);
+ if (array_count(&conns_avail) == 0) {
+ insert_idx = 0;
+ } else {
+ insert_idx = array_count(&conns_avail);
+ array_foreach_modifiable(&conns_avail, conn_avail_idx) {
+ if (conn_avail_idx->pending_requests > pending_requests) {
+ insert_idx = array_foreach_idx(&conns_avail, conn_avail_idx);
+ break;
+ }
+ }
+ }
+ conn_avail = array_insert_space(&conns_avail, insert_idx);
+ conn_avail->conn = conn;
+ conn_avail->pending_requests = pending_requests;
+ if (pending_requests == 0)
+ idle++;
+ }
+ /* Count the number of connecting and closing connections */
+ if (conn->closing)
+ closing++;
+ }
+
+ if (conn_lost) {
+ /* Connection array changed while iterating; retry */
+ continue;
+ }
+
+ working_conn_count = array_count(&peer->conns) - closing;
+ statistics_dirty = FALSE;
+
+ /* Use idle connections right away */
+ if (idle > 0) {
+ e_debug(peer->event,
+ "Using %u idle connections to handle %u requests "
+ "(%u total connections ready)",
+ idle, num_pending > idle ? idle : num_pending,
+ array_count(&conns_avail));
+
+ array_foreach_modifiable(&conns_avail, conn_avail_idx) {
+ if (num_pending == 0 ||
+ conn_avail_idx->pending_requests > 0)
+ break;
+ idle--;
+ if (http_client_connection_next_request(conn_avail_idx->conn) <= 0) {
+ /* No longer available (probably connection error/closed) */
+ statistics_dirty = TRUE;
+ conn_avail_idx->conn = NULL;
+ } else {
+ /* Update statistics */
+ conn_avail_idx->pending_requests++;
+ if (num_urgent > 0)
+ num_urgent--;
+ num_pending--;
+ }
+ }
+ }
+
+ /* Don't continue unless we have more pending requests */
+ num_pending = http_client_peer_requests_pending(peer, &num_urgent);
+ if (num_pending == 0) {
+ e_debug(peer->event,
+ "No more requests to service for this peer "
+ "(%u connections exist, %u pending)",
+ array_count(&peer->conns),
+ array_count(&peer->pending_conns));
+ http_client_peer_check_idle(peer);
+ break;
+ }
+ } while (statistics_dirty);
+
+ tmp_peer = peer;
+ if (!http_client_peer_unref(&tmp_peer))
+ return;
+ peer->handling_requests = FALSE;
+
+ if (num_pending == 0)
+ return;
+
+ i_assert(idle == 0);
+ connecting = array_count(&peer->pending_conns);
+
+ /* Determine how many new connections we can set up */
+ if (pshared->last_failure.tv_sec > 0 && working_conn_count > 0 &&
+ working_conn_count == connecting) {
+ /* Don't create new connections until the existing ones have
+ finished connecting successfully. */
+ new_connections = 0;
+ } else {
+ if ((working_conn_count - connecting + num_urgent) >=
+ peer->client->set.max_parallel_connections) {
+ /* Only create connections for urgent requests */
+ new_connections = (num_urgent > connecting ?
+ num_urgent - connecting : 0);
+ } else if (num_pending <= connecting) {
+ /* There are already enough connections being made */
+ new_connections = 0;
+ } else if (working_conn_count == connecting) {
+ /* No connections succeeded so far, don't hammer the
+ server with more than one connection attempt unless
+ its urgent */
+ if (num_urgent > 0) {
+ new_connections = (num_urgent > connecting ?
+ num_urgent - connecting : 0);
+ } else {
+ new_connections = (connecting == 0 ? 1 : 0);
+ }
+ } else if ((num_pending - connecting) >
+ (peer->client->set.max_parallel_connections -
+ working_conn_count)) {
+ /* Create maximum allowed connections */
+ new_connections =
+ (peer->client->set.max_parallel_connections -
+ working_conn_count);
+ } else {
+ /* Create as many connections as we need */
+ new_connections = num_pending - connecting;
+ }
+ }
+
+ /* Create connections */
+ if (new_connections > 0) {
+ e_debug(peer->event,
+ "Creating %u new connections to handle requests "
+ "(already %u usable, connecting to %u, closing %u)",
+ new_connections, working_conn_count - connecting,
+ connecting, closing);
+ http_client_peer_connect(peer, new_connections);
+ return;
+ }
+
+ /* Cannot create new connections for normal request; attempt pipelining
+ */
+ if ((working_conn_count - connecting) >=
+ peer->client->set.max_parallel_connections) {
+ unsigned int pipeline_level = 0, total_handled = 0, handled;
+
+ if (!pshared->allows_pipelining) {
+ e_debug(peer->event,
+ "Will not pipeline until peer has shown support");
+ return;
+ }
+
+ /* Fill pipelines */
+ do {
+ handled = 0;
+ /* Fill smallest pipelines first,
+ until all pipelines are filled to the same level */
+ array_foreach_modifiable(&conns_avail, conn_avail_idx) {
+ if (conn_avail_idx->conn == NULL)
+ continue;
+ if (pipeline_level == 0) {
+ pipeline_level = conn_avail_idx->pending_requests;
+ } else if (conn_avail_idx->pending_requests > pipeline_level) {
+ pipeline_level = conn_avail_idx->pending_requests;
+ break; /* restart from least busy connection */
+ }
+ /* Pipeline it */
+ if (http_client_connection_next_request(conn_avail_idx->conn) <= 0) {
+ /* connection now unavailable */
+ conn_avail_idx->conn = NULL;
+ } else {
+ /* Successfully pipelined */
+ conn_avail_idx->pending_requests++;
+ num_pending--;
+ handled++;
+ }
+ }
+
+ total_handled += handled;
+ } while (num_pending > num_urgent && handled > 0);
+
+ e_debug(peer->event,
+ "Pipelined %u requests "
+ "(filled pipelines up to %u requests)",
+ total_handled, pipeline_level);
+ return;
+ }
+
+ /* Still waiting for connections to finish */
+ e_debug(peer->event, "No request handled; "
+ "waiting for pending connections "
+ "(%u connections exist, %u pending)",
+ array_count(&peer->conns), connecting);
+ return;
+}
+
+static void http_client_peer_handle_requests(struct http_client_peer *peer)
+{
+ timeout_remove(&peer->to_req_handling);
+
+ T_BEGIN {
+ http_client_peer_handle_requests_real(peer);
+ } T_END;
+}
+
+void http_client_peer_trigger_request_handler(struct http_client_peer *peer)
+{
+ /* Trigger request handling through timeout */
+ if (peer->to_req_handling == NULL) {
+ peer->to_req_handling = timeout_add_short_to(
+ peer->client->ioloop, 0,
+ http_client_peer_handle_requests, peer);
+ }
+}
+
+bool http_client_peer_have_queue(struct http_client_peer *peer,
+ struct http_client_queue *queue)
+{
+ struct http_client_queue *const *queue_idx;
+
+ array_foreach(&peer->queues, queue_idx) {
+ if (*queue_idx == queue)
+ return TRUE;
+ }
+ return FALSE;
+}
+
+void http_client_peer_link_queue(struct http_client_peer *peer,
+ struct http_client_queue *queue)
+{
+ if (!http_client_peer_have_queue(peer, queue)) {
+ array_push_back(&peer->queues, &queue);
+
+ e_debug(peer->event, "Linked queue %s (%d queues linked)",
+ queue->name, array_count(&peer->queues));
+ }
+}
+
+void http_client_peer_unlink_queue(struct http_client_peer *peer,
+ struct http_client_queue *queue)
+{
+ struct http_client_queue *const *queue_idx;
+
+ array_foreach(&peer->queues, queue_idx) {
+ if (*queue_idx == queue) {
+ array_delete(&peer->queues,
+ array_foreach_idx(&peer->queues, queue_idx), 1);
+
+ e_debug(peer->event,
+ "Unlinked queue %s (%d queues linked)",
+ queue->name, array_count(&peer->queues));
+
+ if (array_count(&peer->queues) == 0)
+ http_client_peer_check_idle(peer);
+ return;
+ }
+ }
+}
+
+struct http_client_request *
+http_client_peer_claim_request(struct http_client_peer *peer, bool no_urgent)
+{
+ struct http_client_queue *queue;
+ struct http_client_request *req;
+
+ array_foreach_elem(&peer->queues, queue) {
+ req = http_client_queue_claim_request(
+ queue, &peer->shared->addr, no_urgent);
+ if (req != NULL) {
+ req->peer = peer;
+ return req;
+ }
+ }
+
+ return NULL;
+}
+
+void http_client_peer_connection_success(struct http_client_peer *peer)
+{
+ struct http_client_peer_pool *ppool = peer->ppool;
+ struct http_client_queue *queue;
+
+ e_debug(peer->event, "Successfully connected "
+ "(%u connections exist, %u pending)",
+ array_count(&peer->conns), array_count(&peer->pending_conns));
+
+ http_client_peer_pool_connection_success(ppool);
+
+ array_foreach_elem(&peer->queues, queue)
+ http_client_queue_connection_success(queue, peer);
+
+ http_client_peer_trigger_request_handler(peer);
+}
+
+void http_client_peer_connection_failure(struct http_client_peer *peer,
+ const char *reason)
+{
+ struct http_client_peer_pool *ppool = peer->ppool;
+
+ e_debug(peer->event, "Connection failed "
+ "(%u connections exist, %u pending)",
+ array_count(&peer->conns), array_count(&peer->pending_conns));
+
+ http_client_peer_pool_connection_failure(ppool, reason);
+
+ peer->connect_failed = TRUE;
+}
+
+static void
+http_client_peer_connection_succeeded_pool(struct http_client_peer *peer)
+{
+ if (!peer->connect_failed)
+ return;
+ peer->connect_failed = FALSE;
+
+ e_debug(peer->event,
+ "A connection succeeded within our peer pool, "
+ "so this peer can retry connecting as well if needed "
+ "(%u connections exist, %u pending)",
+ array_count(&peer->conns), array_count(&peer->pending_conns));
+
+ /* If there are pending requests for this peer, try creating a new
+ connection for them. if not, this peer will wind itself down. */
+ http_client_peer_trigger_request_handler(peer);
+}
+
+static void
+http_client_peer_connection_failed_pool(struct http_client_peer *peer,
+ const char *reason)
+{
+ struct http_client_queue *queue;
+ ARRAY_TYPE(http_client_queue) queues;
+
+ e_debug(peer->event,
+ "Failed to establish any connection within our peer pool: %s "
+ "(%u connections exist, %u pending)", reason,
+ array_count(&peer->conns), array_count(&peer->pending_conns));
+
+ peer->connect_failed = TRUE;
+
+ /* Make a copy of the queue array; queues get linked/unlinged while the
+ connection failure is handled */
+ t_array_init(&queues, array_count(&peer->queues));
+ array_copy(&queues.arr, 0, &peer->queues.arr, 0,
+ array_count(&peer->queues));
+
+ /* Failed to make any connection. a second connect will probably also
+ fail, so just try another IP for the hosts(s) or abort all requests
+ if this was the only/last option. */
+ array_foreach_elem(&queues, queue)
+ http_client_queue_connection_failure(queue, peer, reason);
+}
+
+void http_client_peer_connection_lost(struct http_client_peer *peer,
+ bool premature)
+{
+ unsigned int num_pending, num_urgent;
+
+ /* We get here when an already connected connection fails. if the
+ connect itself fails, http_client_peer_shared_connection_failure() is
+ called instead. */
+
+ if (peer->disconnected)
+ return;
+
+ http_client_peer_shared_connection_lost(peer->shared, premature);
+
+ num_pending = http_client_peer_requests_pending(peer, &num_urgent);
+
+ e_debug(peer->event,
+ "Lost a connection%s "
+ "(%u queues linked, %u connections left, "
+ "%u connections pending, %u requests pending, "
+ "%u requests urgent)",
+ (premature ? " prematurely" : ""),
+ array_count(&peer->queues), array_count(&peer->conns),
+ array_count(&peer->pending_conns), num_pending, num_urgent);
+
+ if (peer->handling_requests) {
+ /* We got here from the request handler loop */
+ e_debug(peer->event,
+ "Lost a connection while handling requests");
+ return;
+ }
+
+ /* If there are pending requests for this peer, create a new connection
+ for them. if not, this peer will wind itself down. */
+ http_client_peer_trigger_request_handler(peer);
+}
+
+unsigned int
+http_client_peer_active_connections(struct http_client_peer *peer)
+{
+ struct http_client_connection *conn;
+ unsigned int active = 0;
+
+ /* Find idle connections */
+ array_foreach_elem(&peer->conns, conn) {
+ if (http_client_connection_is_active(conn))
+ active++;
+ }
+
+ return active;
+}
+
+unsigned int
+http_client_peer_pending_connections(struct http_client_peer *peer)
+{
+ return array_count(&peer->pending_conns);
+}
+
+void http_client_peer_switch_ioloop(struct http_client_peer *peer)
+{
+ if (peer->to_req_handling != NULL) {
+ peer->to_req_handling =
+ io_loop_move_timeout(&peer->to_req_handling);
+ }
+}
diff --git a/src/lib-http/http-client-private.h b/src/lib-http/http-client-private.h
new file mode 100644
index 0000000..86b1d96
--- /dev/null
+++ b/src/lib-http/http-client-private.h
@@ -0,0 +1,718 @@
+#ifndef HTTP_CLIENT_PRIVATE_H
+#define HTTP_CLIENT_PRIVATE_H
+
+#include "connection.h"
+
+#include "http-url.h"
+#include "http-client.h"
+
+/*
+ * Defaults
+ */
+
+#define HTTP_CLIENT_CONTINUE_TIMEOUT_MSECS (1000*2)
+#define HTTP_CLIENT_DEFAULT_REQUEST_TIMEOUT_MSECS (1000*60*1)
+#define HTTP_CLIENT_DEFAULT_DNS_LOOKUP_TIMEOUT_MSECS (1000*10)
+#define HTTP_CLIENT_DEFAULT_BACKOFF_TIME_MSECS (100)
+#define HTTP_CLIENT_DEFAULT_BACKOFF_MAX_TIME_MSECS (1000*60)
+#define HTTP_CLIENT_DEFAULT_DNS_TTL_MSECS (1000*60*30)
+#define HTTP_CLIENT_MIN_IDLE_TIMEOUT_MSECS 50
+
+/*
+ * Types
+ */
+
+struct http_client_connection;
+struct http_client_peer_pool;
+struct http_client_peer_shared;
+struct http_client_peer;
+struct http_client_queue;
+struct http_client_host_shared;
+struct http_client_host;
+
+ARRAY_DEFINE_TYPE(http_client_request, struct http_client_request *);
+ARRAY_DEFINE_TYPE(http_client_connection, struct http_client_connection *);
+ARRAY_DEFINE_TYPE(http_client_peer, struct http_client_peer *);
+ARRAY_DEFINE_TYPE(http_client_peer_shared, struct http_client_peer_shared *);
+ARRAY_DEFINE_TYPE(http_client_peer_pool, struct http_client_peer_pool *);
+ARRAY_DEFINE_TYPE(http_client_queue, struct http_client_queue *);
+ARRAY_DEFINE_TYPE(http_client_host, struct http_client_host_shared *);
+
+HASH_TABLE_DEFINE_TYPE(http_client_peer_shared,
+ const struct http_client_peer_addr *,
+ struct http_client_peer_shared *);
+HASH_TABLE_DEFINE_TYPE(http_client_host_shared, const char *,
+ struct http_client_host_shared *);
+
+enum http_client_peer_addr_type {
+ HTTP_CLIENT_PEER_ADDR_HTTP = 0,
+ HTTP_CLIENT_PEER_ADDR_HTTPS,
+ HTTP_CLIENT_PEER_ADDR_HTTPS_TUNNEL,
+ HTTP_CLIENT_PEER_ADDR_RAW,
+ HTTP_CLIENT_PEER_ADDR_UNIX,
+};
+
+struct http_client_peer_addr {
+ enum http_client_peer_addr_type type;
+ union {
+ struct {
+ const char *https_name; /* TLS SNI */
+ struct ip_addr ip;
+ in_port_t port;
+ } tcp;
+ struct {
+ const char *path;
+ } un;
+ } a;
+};
+
+/*
+ * Objects
+ */
+
+struct http_client_request {
+ pool_t pool;
+ unsigned int refcount;
+ const char *label;
+ unsigned int id;
+
+ struct http_client_request *prev, *next;
+
+ const char *method, *target;
+ struct http_url origin_url;
+ const char *username, *password;
+
+ const char *host_socket;
+ const struct http_url *host_url;
+ const char *authority;
+
+ struct http_client *client;
+ struct http_client_host *host;
+ struct http_client_queue *queue;
+ struct http_client_peer *peer;
+ struct http_client_connection *conn;
+
+ struct event *event;
+ const char *const *event_headers;
+ unsigned int last_status;
+
+ string_t *headers;
+ time_t date;
+
+ struct istream *payload_input;
+ uoff_t payload_size, payload_offset;
+ struct ostream *payload_output;
+
+ /* Time when request can be sent the next time. This is set by
+ http_client_request_delay*(). Default is 0 = immediately. Retries
+ can update this. */
+ struct timeval release_time;
+ /* Time when http_client_request_submit() was called. */
+ struct timeval submit_time;
+ /* Time when the request was first sent to the server. The HTTP
+ connection already exists at this time. */
+ struct timeval first_sent_time;
+ /* Time when the request was last sent to the server (if it was
+ retried). */
+ struct timeval sent_time;
+ /* Time when the HTTP response header was last received. */
+ struct timeval response_time;
+ /* Time when the request will be aborted. Set by
+ http_client_request_set_timeout(). */
+ struct timeval timeout_time;
+ unsigned int timeout_msecs;
+ unsigned int attempt_timeout_msecs;
+ unsigned int max_attempts;
+
+ uoff_t response_offset, request_offset;
+ uoff_t bytes_in, bytes_out;
+
+ unsigned int attempts, send_attempts;
+ unsigned int redirects;
+ uint64_t sent_global_ioloop_usecs;
+ uint64_t sent_http_ioloop_usecs;
+ uint64_t sent_lock_usecs;
+
+ unsigned int delayed_error_status;
+ const char *delayed_error;
+
+ http_client_request_callback_t *callback;
+ void *context;
+
+ void (*destroy_callback)(void *);
+ void *destroy_context;
+
+ enum http_request_state state;
+
+ bool have_hdr_authorization:1;
+ bool have_hdr_body_spec:1;
+ bool have_hdr_connection:1;
+ bool have_hdr_date:1;
+ bool have_hdr_expect:1;
+ bool have_hdr_host:1;
+ bool have_hdr_user_agent:1;
+
+ bool payload_sync:1;
+ bool payload_sync_continue:1;
+ bool payload_chunked:1;
+ bool payload_wait:1;
+ bool payload_finished:1;
+ bool payload_empty:1;
+ bool urgent:1;
+ bool submitted:1;
+ bool listed:1;
+ bool connect_tunnel:1;
+ bool connect_direct:1;
+ bool ssl_tunnel:1;
+ bool preserve_exact_reason:1;
+};
+
+struct http_client_connection {
+ struct connection conn;
+ struct event *event;
+ unsigned int refcount;
+
+ struct http_client_peer_pool *ppool;
+ struct http_client_peer *peer;
+
+ int connect_errno;
+ struct timeval connect_start_timestamp;
+ struct timeval connected_timestamp;
+ struct http_client_request *connect_request;
+
+ struct ssl_iostream *ssl_iostream;
+ struct http_response_parser *http_parser;
+ struct timeout *to_connect, *to_input, *to_idle, *to_response;
+ struct timeout *to_requests;
+
+ struct http_client_request *pending_request;
+ struct istream *incoming_payload;
+ struct io *io_req_payload;
+ struct ioloop *last_ioloop;
+ struct io_wait_timer *io_wait_timer;
+
+ /* Requests that have been sent, waiting for response */
+ ARRAY_TYPE(http_client_request) request_wait_list;
+
+ bool connected:1; /* Connection is connected */
+ bool idle:1; /* Connection is idle */
+ bool tunneling:1; /* Last sent request turns this
+ connection into tunnel */
+ bool connect_succeeded:1; /* Connection succeeded including SSL */
+ bool connect_failed:1; /* Connection failed */
+ bool lost_prematurely:1; /* Lost connection before receiving any data */
+ bool closing:1;
+ bool disconnected:1;
+ bool close_indicated:1;
+ bool output_locked:1; /* Output is locked; no pipelining */
+ bool output_broken:1; /* Output is broken; no more requests */
+ bool in_req_callback:1; /* Performing request callback (busy) */
+ bool debug:1;
+};
+
+struct http_client_peer_shared {
+ unsigned int refcount;
+ struct http_client_peer_addr addr;
+ char *addr_name;
+ struct event *event;
+
+ char *label;
+
+ struct http_client_context *cctx;
+ struct http_client_peer_shared *prev, *next;
+
+ struct http_client_peer_pool *pools_list;
+
+ struct http_client_peer *peers_list;
+ unsigned int peers_count;
+
+ /* Connection retry */
+ struct timeval last_failure;
+ struct timeout *to_backoff;
+ unsigned int backoff_initial_time_msecs;
+ unsigned int backoff_current_time_msecs;
+ unsigned int backoff_max_time_msecs;
+
+ bool no_payload_sync:1; /* Expect: 100-continue failed before */
+ bool seen_100_response:1; /* Expect: 100-continue succeeded before */
+ bool allows_pipelining:1; /* Peer is known to allow persistent
+ connections */
+};
+
+struct http_client_peer_pool {
+ unsigned int refcount;
+ struct http_client_peer_shared *peer;
+ struct http_client_peer_pool *prev, *next;
+ struct event *event;
+
+ /* All connections to this peer */
+ ARRAY_TYPE(http_client_connection) conns;
+
+ /* Pending connections (not ready connecting) */
+ ARRAY_TYPE(http_client_connection) pending_conns;
+
+ /* Available connections to this peer */
+ ARRAY_TYPE(http_client_connection) idle_conns;
+
+ /* Distinguishing settings for these connections */
+ struct ssl_iostream_context *ssl_ctx;
+ char *rawlog_dir;
+ struct pcap_output *pcap_output;
+
+ bool destroyed:1; /* Peer pool is being destroyed */
+};
+
+struct http_client_peer {
+ unsigned int refcount;
+ struct http_client_peer_shared *shared;
+ struct http_client_peer *shared_prev, *shared_next;
+
+ struct http_client *client;
+ struct http_client_peer *client_prev, *client_next;
+
+ struct http_client_peer_pool *ppool;
+ struct event *event;
+
+ /* Queues using this peer */
+ ARRAY_TYPE(http_client_queue) queues;
+
+ /* Active connections to this peer */
+ ARRAY_TYPE(http_client_connection) conns;
+ /* Pending connections (not ready connecting) */
+ ARRAY_TYPE(http_client_connection) pending_conns;
+
+ /* Zero time-out for consolidating request handling */
+ struct timeout *to_req_handling;
+
+ bool connect_failed:1; /* Last connection attempt failed */
+ bool connect_backoff:1; /* Peer is waiting for backoff timout*/
+ bool disconnected:1; /* Peer is already disconnected */
+ bool handling_requests:1; /* Currently running request handler */
+};
+
+struct http_client_queue {
+ struct http_client *client;
+ struct http_client_queue *prev, *next;
+
+ struct http_client_host *host;
+ char *name;
+ struct event *event;
+
+ struct http_client_peer_addr addr;
+ char *addr_name;
+
+ /* Current index in host->ips */
+ unsigned int ips_connect_idx;
+ /* The first IP that started the current round of connection attempts.
+ initially 0, and later set to the ip index of the last successful
+ connected IP */
+ unsigned int ips_connect_start_idx;
+
+ struct timeval first_connect_time;
+ unsigned int connect_attempts;
+
+ /* Peers we are trying to connect to;
+ this can be more than one when soft connect timeouts are enabled */
+ ARRAY_TYPE(http_client_peer) pending_peers;
+
+ /* Currently active peer */
+ struct http_client_peer *cur_peer;
+
+ /* All requests associated to this queue
+ (ordered by earliest timeout first) */
+ ARRAY_TYPE(http_client_request) requests;
+
+ /* Delayed requests waiting to be released after delay */
+ ARRAY_TYPE(http_client_request) delayed_requests;
+
+ /* Requests pending in queue to be picked up by connections */
+ ARRAY_TYPE(http_client_request) queued_requests, queued_urgent_requests;
+
+ struct timeout *to_connect, *to_request, *to_delayed;
+};
+
+struct http_client_host_shared {
+ struct http_client_host_shared *prev, *next;
+
+ struct http_client_context *cctx;
+ char *name;
+ struct event *event;
+
+ /* The ip addresses DNS returned for this host */
+ unsigned int ips_count;
+ struct ip_addr *ips;
+ struct timeval ips_timeout;
+
+ /* Private instance for each client that uses this host */
+ struct http_client_host *hosts_list;
+
+ /* Active DNS lookup */
+ struct dns_lookup *dns_lookup;
+
+ /* Timeouts */
+ struct timeout *to_idle;
+
+ bool destroyed:1; /* Shared host object is being destroyed */
+ bool unix_local:1;
+ bool explicit_ip:1;
+};
+
+struct http_client_host {
+ struct http_client_host_shared *shared;
+ struct http_client_host *shared_prev, *shared_next;
+
+ struct http_client *client;
+ struct http_client_host *client_prev, *client_next;
+
+ /* Separate queue for each host port */
+ ARRAY_TYPE(http_client_queue) queues;
+};
+
+struct http_client {
+ pool_t pool;
+ struct http_client_context *cctx;
+ struct http_client_settings set;
+
+ struct http_client *prev, *next;
+
+ struct event *event;
+ struct ioloop *ioloop;
+ struct ssl_iostream_context *ssl_ctx;
+
+ /* List of failed requests that are waiting for ioloop */
+ ARRAY(struct http_client_request *) delayed_failing_requests;
+ struct timeout *to_failing_requests;
+
+ struct http_client_host *hosts_list;
+ struct http_client_peer *peers_list;
+
+ struct http_client_request *requests_list;
+ unsigned int requests_count;
+
+ bool waiting:1;
+};
+
+struct http_client_context {
+ pool_t pool;
+ unsigned int refcount;
+ struct event *event;
+ struct ioloop *ioloop;
+
+ struct http_client_settings set;
+
+ struct dns_client *dns_client;
+ const char *dns_client_socket_path;
+ unsigned int dns_ttl_msecs;
+ unsigned int dns_lookup_timeout_msecs;
+
+ struct http_client *clients_list;
+ struct connection_list *conn_list;
+
+ HASH_TABLE_TYPE(http_client_peer_shared) peers;
+ struct http_client_peer_shared *peers_list;
+ HASH_TABLE_TYPE(http_client_host_shared) hosts;
+ struct http_client_host_shared *unix_host;
+ struct http_client_host_shared *hosts_list;
+};
+
+/*
+ * Peer address
+ */
+
+static inline bool
+http_client_peer_addr_is_https(const struct http_client_peer_addr *addr)
+{
+ switch (addr->type) {
+ case HTTP_CLIENT_PEER_ADDR_HTTPS:
+ case HTTP_CLIENT_PEER_ADDR_HTTPS_TUNNEL:
+ return TRUE;
+ default:
+ break;
+ }
+ return FALSE;
+}
+
+static inline const char *
+http_client_peer_addr_get_https_name(const struct http_client_peer_addr *addr)
+{
+ switch (addr->type) {
+ case HTTP_CLIENT_PEER_ADDR_HTTPS:
+ case HTTP_CLIENT_PEER_ADDR_HTTPS_TUNNEL:
+ return addr->a.tcp.https_name;
+ default:
+ break;
+ }
+ return NULL;
+}
+
+static inline const char *
+http_client_peer_addr2str(const struct http_client_peer_addr *addr)
+{
+ switch (addr->type) {
+ case HTTP_CLIENT_PEER_ADDR_HTTP:
+ case HTTP_CLIENT_PEER_ADDR_HTTPS:
+ case HTTP_CLIENT_PEER_ADDR_HTTPS_TUNNEL:
+ case HTTP_CLIENT_PEER_ADDR_RAW:
+ if (addr->a.tcp.ip.family == AF_INET6) {
+ return t_strdup_printf("[%s]:%u",
+ net_ip2addr(&addr->a.tcp.ip),
+ addr->a.tcp.port);
+ }
+ return t_strdup_printf("%s:%u",
+ net_ip2addr(&addr->a.tcp.ip),
+ addr->a.tcp.port);
+ case HTTP_CLIENT_PEER_ADDR_UNIX:
+ return t_strdup_printf("unix:%s", addr->a.un.path);
+ default:
+ break;
+ }
+ i_unreached();
+ return "";
+}
+
+/*
+ * Request
+ */
+
+static inline bool
+http_client_request_to_proxy(const struct http_client_request *req)
+{
+ return (req->host_url != &req->origin_url);
+}
+
+const char *http_client_request_label(struct http_client_request *req);
+
+void http_client_request_ref(struct http_client_request *req);
+/* Returns FALSE if unrefing destroyed the request entirely */
+bool http_client_request_unref(struct http_client_request **_req);
+void http_client_request_destroy(struct http_client_request **_req);
+
+void http_client_request_get_peer_addr(const struct http_client_request *req,
+ struct http_client_peer_addr *addr);
+enum http_response_payload_type
+http_client_request_get_payload_type(struct http_client_request *req);
+int http_client_request_send(struct http_client_request *req, bool pipelined);
+int http_client_request_send_more(struct http_client_request *req,
+ bool pipelined);
+
+bool http_client_request_callback(struct http_client_request *req,
+ struct http_response *response);
+void http_client_request_connect_callback(struct http_client_request *req,
+ const struct http_client_tunnel *tunnel,
+ struct http_response *response);
+
+void http_client_request_resubmit(struct http_client_request *req);
+void http_client_request_retry(struct http_client_request *req,
+ unsigned int status, const char *error);
+void http_client_request_error_delayed(struct http_client_request **_req);
+void http_client_request_error(struct http_client_request **req,
+ unsigned int status, const char *error);
+void http_client_request_redirect(struct http_client_request *req,
+ unsigned int status, const char *location);
+void http_client_request_finish(struct http_client_request *req);
+
+/*
+ * Connection
+ */
+
+struct connection_list *http_client_connection_list_init(void);
+
+struct http_client_connection *
+http_client_connection_create(struct http_client_peer *peer);
+void http_client_connection_ref(struct http_client_connection *conn);
+/* Returns FALSE if unrefing destroyed the connection entirely */
+bool http_client_connection_unref(struct http_client_connection **_conn);
+void http_client_connection_close(struct http_client_connection **_conn);
+
+void http_client_connection_lost(struct http_client_connection **_conn,
+ const char *error) ATTR_NULL(2);
+
+void http_client_connection_peer_closed(struct http_client_connection **_conn);
+void http_client_connection_request_destroyed(
+ struct http_client_connection *conn, struct http_client_request *req);
+
+void http_client_connection_handle_output_error(
+ struct http_client_connection *conn);
+int http_client_connection_output(struct http_client_connection *conn);
+void http_client_connection_start_request_timeout(
+ struct http_client_connection *conn);
+void http_client_connection_reset_request_timeout(
+ struct http_client_connection *conn);
+void http_client_connection_stop_request_timeout(
+ struct http_client_connection *conn);
+unsigned int
+http_client_connection_count_pending(struct http_client_connection *conn);
+int http_client_connection_check_ready(struct http_client_connection *conn);
+bool http_client_connection_is_idle(struct http_client_connection *conn);
+bool http_client_connection_is_active(struct http_client_connection *conn);
+int http_client_connection_next_request(struct http_client_connection *conn);
+void http_client_connection_check_idle(struct http_client_connection *conn);
+void http_client_connection_switch_ioloop(struct http_client_connection *conn);
+void http_client_connection_start_tunnel(struct http_client_connection **_conn,
+ struct http_client_tunnel *tunnel);
+void http_client_connection_lost_peer(struct http_client_connection *conn);
+void http_client_connection_claim_idle(struct http_client_connection *conn,
+ struct http_client_peer *peer);
+
+/*
+ * Peer
+ */
+
+/* address */
+
+unsigned int
+http_client_peer_addr_hash(const struct http_client_peer_addr *peer) ATTR_PURE;
+int http_client_peer_addr_cmp(const struct http_client_peer_addr *peer1,
+ const struct http_client_peer_addr *peer2)
+ ATTR_PURE;
+
+/* connection pool */
+
+void http_client_peer_pool_ref(struct http_client_peer_pool *ppool);
+void http_client_peer_pool_unref(struct http_client_peer_pool **_ppool);
+
+void http_client_peer_pool_close(struct http_client_peer_pool **_ppool);
+
+/* peer (shared) */
+
+const char *
+http_client_peer_shared_label(struct http_client_peer_shared *pshared);
+
+void http_client_peer_shared_ref(struct http_client_peer_shared *pshared);
+void http_client_peer_shared_unref(struct http_client_peer_shared **_pshared);
+void http_client_peer_shared_close(struct http_client_peer_shared **_pshared);
+
+void http_client_peer_shared_switch_ioloop(
+ struct http_client_peer_shared *pshared);
+
+unsigned int
+http_client_peer_shared_max_connections(
+ struct http_client_peer_shared *pshared);
+
+/* peer */
+
+struct http_client_peer *
+http_client_peer_get(struct http_client *client,
+ const struct http_client_peer_addr *addr);
+void http_client_peer_ref(struct http_client_peer *peer);
+bool http_client_peer_unref(struct http_client_peer **_peer);
+void http_client_peer_close(struct http_client_peer **_peer);
+
+bool http_client_peer_have_queue(struct http_client_peer *peer,
+ struct http_client_queue *queue);
+void http_client_peer_link_queue(struct http_client_peer *peer,
+ struct http_client_queue *queue);
+void http_client_peer_unlink_queue(struct http_client_peer *peer,
+ struct http_client_queue *queue);
+struct http_client_request *
+http_client_peer_claim_request(struct http_client_peer *peer, bool no_urgent);
+void http_client_peer_trigger_request_handler(struct http_client_peer *peer);
+void http_client_peer_connection_success(struct http_client_peer *peer);
+void http_client_peer_connection_failure(struct http_client_peer *peer,
+ const char *reason);
+void http_client_peer_connection_lost(struct http_client_peer *peer,
+ bool premature);
+bool http_client_peer_is_connected(struct http_client_peer *peer);
+unsigned int
+http_client_peer_idle_connections(struct http_client_peer *peer);
+unsigned int
+http_client_peer_active_connections(struct http_client_peer *peer);
+unsigned int
+http_client_peer_pending_connections(struct http_client_peer *peer);
+void http_client_peer_switch_ioloop(struct http_client_peer *peer);
+
+/*
+ * Queue
+ */
+
+struct http_client_queue *
+http_client_queue_get(struct http_client_host *host,
+ const struct http_client_peer_addr *addr);
+void http_client_queue_free(struct http_client_queue *queue);
+void http_client_queue_connection_setup(struct http_client_queue *queue);
+unsigned int
+http_client_queue_host_lookup_done(struct http_client_queue *queue);
+void http_client_queue_host_lookup_failure(struct http_client_queue *queue,
+ const char *error);
+void http_client_queue_submit_request(struct http_client_queue *queue,
+ struct http_client_request *req);
+void http_client_queue_drop_request(struct http_client_queue *queue,
+ struct http_client_request *req);
+struct http_client_request *
+http_client_queue_claim_request(struct http_client_queue *queue,
+ const struct http_client_peer_addr *addr,
+ bool no_urgent);
+unsigned int
+http_client_queue_requests_pending(struct http_client_queue *queue,
+ unsigned int *num_urgent_r) ATTR_NULL(2);
+unsigned int http_client_queue_requests_active(struct http_client_queue *queue);
+void http_client_queue_connection_success(struct http_client_queue *queue,
+ struct http_client_peer *peer);
+void http_client_queue_connection_failure(struct http_client_queue *queue,
+ struct http_client_peer *peer,
+ const char *reason);
+void http_client_queue_peer_disconnected(struct http_client_queue *queue,
+ struct http_client_peer *peer);
+void http_client_queue_switch_ioloop(struct http_client_queue *queue);
+
+/*
+ * Host
+ */
+
+/* host (shared) */
+
+void http_client_host_shared_free(struct http_client_host_shared **_hshared);
+void http_client_host_shared_switch_ioloop(
+ struct http_client_host_shared *hshared);
+
+/* host */
+
+static inline unsigned int
+http_client_host_get_ips_count(struct http_client_host *host)
+{
+ return host->shared->ips_count;
+}
+
+static inline const struct ip_addr *
+http_client_host_get_ip(struct http_client_host *host, unsigned int idx)
+{
+ i_assert(idx < host->shared->ips_count);
+ return &host->shared->ips[idx];
+}
+
+static inline bool
+http_client_host_ready(struct http_client_host *host)
+{
+ return host->shared->dns_lookup == NULL;
+}
+
+struct http_client_host *
+http_client_host_get(struct http_client *client,
+ const struct http_url *host_url);
+void http_client_host_free(struct http_client_host **_host);
+void http_client_host_submit_request(struct http_client_host *host,
+ struct http_client_request *req);
+void http_client_host_switch_ioloop(struct http_client_host *host);
+void http_client_host_check_idle(struct http_client_host *host);
+int http_client_host_refresh(struct http_client_host *host);
+bool http_client_host_get_ip_idx(struct http_client_host *host,
+ const struct ip_addr *ip, unsigned int *idx_r);
+
+/*
+ * Client
+ */
+
+int http_client_init_ssl_ctx(struct http_client *client, const char **error_r);
+
+void http_client_delay_request_error(struct http_client *client,
+ struct http_client_request *req);
+void http_client_remove_request_error(struct http_client *client,
+ struct http_client_request *req);
+
+/*
+ * Client shared context
+ */
+
+void http_client_context_switch_ioloop(struct http_client_context *cctx);
+
+#endif
diff --git a/src/lib-http/http-client-queue.c b/src/lib-http/http-client-queue.c
new file mode 100644
index 0000000..5c7915a
--- /dev/null
+++ b/src/lib-http/http-client-queue.c
@@ -0,0 +1,1065 @@
+/* Copyright (c) 2013-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "net.h"
+#include "str.h"
+#include "str-sanitize.h"
+#include "hash.h"
+#include "array.h"
+#include "bsearch-insert-pos.h"
+#include "llist.h"
+#include "ioloop.h"
+#include "istream.h"
+#include "ostream.h"
+#include "time-util.h"
+#include "dns-lookup.h"
+#include "http-response-parser.h"
+
+#include "http-client-private.h"
+
+#define TIMEOUT_CMP_MARGIN_USECS 2000
+
+static void
+http_client_queue_fail_full(struct http_client_queue *queue,
+ unsigned int status, const char *error, bool all);
+static void
+http_client_queue_set_delay_timer(struct http_client_queue *queue,
+ struct timeval time);
+static void
+http_client_queue_set_request_timer(struct http_client_queue *queue,
+ const struct timeval *time);
+
+/*
+ * Queue object
+ */
+
+static struct http_client_queue *
+http_client_queue_find(struct http_client_host *host,
+ const struct http_client_peer_addr *addr)
+{
+ struct http_client_queue *queue;
+
+ array_foreach_elem(&host->queues, queue) {
+ if (http_client_peer_addr_cmp(&queue->addr, addr) == 0)
+ return queue;
+ }
+
+ return NULL;
+}
+
+static struct http_client_queue *
+http_client_queue_create(struct http_client_host *host,
+ const struct http_client_peer_addr *addr)
+{
+ const char *hostname = host->shared->name;
+ struct http_client_queue *queue;
+
+ queue = i_new(struct http_client_queue, 1);
+ queue->client = host->client;
+ queue->host = host;
+ queue->addr = *addr;
+
+ switch (addr->type) {
+ case HTTP_CLIENT_PEER_ADDR_RAW:
+ queue->name = i_strdup_printf("raw://%s:%u",
+ hostname, addr->a.tcp.port);
+ queue->addr.a.tcp.https_name = NULL;
+ break;
+ case HTTP_CLIENT_PEER_ADDR_HTTPS_TUNNEL:
+ case HTTP_CLIENT_PEER_ADDR_HTTPS:
+ queue->name = i_strdup_printf("https://%s:%u",
+ hostname, addr->a.tcp.port);
+ queue->addr_name = i_strdup(addr->a.tcp.https_name);
+ queue->addr.a.tcp.https_name = queue->addr_name;
+ break;
+ case HTTP_CLIENT_PEER_ADDR_HTTP:
+ queue->name = i_strdup_printf("http://%s:%u",
+ hostname, addr->a.tcp.port);
+ queue->addr.a.tcp.https_name = NULL;
+ break;
+ case HTTP_CLIENT_PEER_ADDR_UNIX:
+ queue->name = i_strdup_printf("unix:%s", addr->a.un.path);
+ queue->addr_name = i_strdup(addr->a.un.path);
+ queue->addr.a.un.path = queue->addr_name;
+ break;
+ default:
+ i_unreached();
+ }
+
+ queue->event = event_create(queue->client->event);
+ event_set_append_log_prefix(queue->event,
+ t_strdup_printf("queue %s: ", str_sanitize(queue->name, 256)));
+ queue->ips_connect_idx = 0;
+ i_array_init(&queue->pending_peers, 8);
+ i_array_init(&queue->requests, 16);
+ i_array_init(&queue->queued_requests, 16);
+ i_array_init(&queue->queued_urgent_requests, 16);
+ i_array_init(&queue->delayed_requests, 4);
+ array_push_back(&host->queues, &queue);
+
+ return queue;
+}
+
+struct http_client_queue *
+http_client_queue_get(struct http_client_host *host,
+ const struct http_client_peer_addr *addr)
+{
+ struct http_client_queue *queue;
+
+ queue = http_client_queue_find(host, addr);
+ if (queue == NULL)
+ queue = http_client_queue_create(host, addr);
+
+ return queue;
+}
+
+void http_client_queue_free(struct http_client_queue *queue)
+{
+ struct http_client_peer *peer;
+ ARRAY_TYPE(http_client_peer) peers;
+
+ e_debug(queue->event, "Destroy");
+
+ /* Currently only called when peer is freed, so there is no need to
+ unlink from the peer */
+
+ /* Unlink all peers */
+ if (queue->cur_peer != NULL) {
+ struct http_client_peer *peer = queue->cur_peer;
+
+ queue->cur_peer = NULL;
+ http_client_peer_unlink_queue(peer, queue);
+ }
+ t_array_init(&peers, array_count(&queue->pending_peers));
+ array_copy(&peers.arr, 0, &queue->pending_peers.arr, 0,
+ array_count(&queue->pending_peers));
+ array_foreach_elem(&peers, peer)
+ http_client_peer_unlink_queue(peer, queue);
+ array_free(&queue->pending_peers);
+
+ /* Abort all requests */
+ http_client_queue_fail_full(queue, HTTP_CLIENT_REQUEST_ERROR_ABORTED,
+ "Aborted", TRUE);
+ array_free(&queue->requests);
+ array_free(&queue->queued_requests);
+ array_free(&queue->queued_urgent_requests);
+ array_free(&queue->delayed_requests);
+
+ /* Cancel timeouts */
+ timeout_remove(&queue->to_connect);
+ timeout_remove(&queue->to_delayed);
+
+ /* Free */
+ event_unref(&queue->event);
+ i_free(queue->addr_name);
+ i_free(queue->name);
+ i_free(queue);
+}
+
+/*
+ * Error handling
+ */
+
+static void
+http_client_queue_fail_full(struct http_client_queue *queue,
+ unsigned int status, const char *error, bool all)
+{
+ ARRAY_TYPE(http_client_request) *req_arr, treqs;
+ struct http_client_request *req;
+ unsigned int retained = 0;
+
+ /* Abort requests */
+ req_arr = &queue->requests;
+ t_array_init(&treqs, array_count(req_arr));
+ array_copy(&treqs.arr, 0, &req_arr->arr, 0, array_count(req_arr));
+ array_foreach_elem(&treqs, req) {
+ i_assert(req->state >= HTTP_REQUEST_STATE_QUEUED);
+ if (!all &&
+ req->state != HTTP_REQUEST_STATE_QUEUED)
+ retained++;
+ else
+ http_client_request_error(&req, status, error);
+ }
+
+ /* All queues should be empty now... unless new requests were submitted
+ from the callback. this invariant captures it all: */
+ i_assert((retained +
+ array_count(&queue->delayed_requests) +
+ array_count(&queue->queued_requests) +
+ array_count(&queue->queued_urgent_requests)) ==
+ array_count(&queue->requests));
+}
+
+static void
+http_client_queue_fail(struct http_client_queue *queue,
+ unsigned int status, const char *error)
+{
+ http_client_queue_fail_full(queue, status, error, FALSE);
+}
+
+/*
+ * Connection management
+ */
+
+static bool
+http_client_queue_is_last_connect_ip(struct http_client_queue *queue)
+{
+ const struct http_client_settings *set =
+ &queue->client->set;
+ struct http_client_host *host = queue->host;
+ unsigned int ips_count = http_client_host_get_ips_count(host);
+
+ i_assert(queue->addr.type != HTTP_CLIENT_PEER_ADDR_UNIX);
+ i_assert(queue->ips_connect_idx < ips_count);
+ i_assert(queue->ips_connect_start_idx < ips_count);
+
+ /* If a maximum connect attempts > 1 is set, enforce it directly */
+ if (set->max_connect_attempts > 1 &&
+ queue->connect_attempts >= set->max_connect_attempts)
+ return TRUE;
+
+ /* Otherwise, we'll always go through all the IPs. we don't necessarily
+ start connecting from the first IP, so we'll need to treat the IPs as
+ a ring buffer where we automatically wrap back to the first IP
+ when necessary. */
+ return ((queue->ips_connect_idx + 1) % ips_count ==
+ queue->ips_connect_start_idx);
+}
+
+static void
+http_client_queue_recover_from_lookup(struct http_client_queue *queue)
+{
+ struct http_client_host *host = queue->host;
+ unsigned int ip_idx;
+
+ i_assert(queue->addr.type != HTTP_CLIENT_PEER_ADDR_UNIX);
+
+ if (queue->cur_peer == NULL) {
+ queue->ips_connect_idx = queue->ips_connect_start_idx = 0;
+ return;
+ }
+
+ if (http_client_host_get_ip_idx(
+ host, &queue->cur_peer->shared->addr.a.tcp.ip, &ip_idx)) {
+ /* Continue with current peer */
+ queue->ips_connect_idx = queue->ips_connect_start_idx = ip_idx;
+ } else {
+ /* Reset connect attempts */
+ queue->ips_connect_idx = queue->ips_connect_start_idx = 0;
+ }
+}
+
+static void
+http_client_queue_soft_connect_timeout(struct http_client_queue *queue)
+{
+ struct http_client_host *host = queue->host;
+ const struct http_client_peer_addr *addr = &queue->addr;
+ unsigned int ips_count = http_client_host_get_ips_count(host);
+ const char *https_name;
+
+ i_assert(queue->addr.type != HTTP_CLIENT_PEER_ADDR_UNIX);
+
+ timeout_remove(&queue->to_connect);
+
+ if (http_client_queue_is_last_connect_ip(queue)) {
+ /* No more IPs to try */
+ return;
+ }
+
+ /* If our our previous connection attempt takes longer than the
+ soft_connect_timeout, we start a connection attempt to the next IP in
+ parallel */
+ https_name = http_client_peer_addr_get_https_name(addr);
+ e_debug(queue->event, "Connection to %s%s is taking a long time; "
+ "starting parallel connection attempt to next IP",
+ http_client_peer_addr2str(addr),
+ (https_name == NULL ?
+ "" : t_strdup_printf(" (SSL=%s)", https_name)));
+
+ /* Next IP */
+ queue->ips_connect_idx = (queue->ips_connect_idx + 1) % ips_count;
+
+ /* Setup connection to new peer (can start new soft timeout) */
+ http_client_queue_connection_setup(queue);
+}
+
+static struct http_client_peer *
+http_client_queue_connection_attempt(struct http_client_queue *queue)
+{
+ struct http_client *client = queue->client;
+ struct http_client_host *host = queue->host;
+ struct http_client_peer *peer;
+ struct http_client_peer_addr *addr = &queue->addr;
+ unsigned int num_requests =
+ array_count(&queue->queued_requests) +
+ array_count(&queue->queued_urgent_requests);
+ const char *ssl = "";
+ int ret;
+
+ if (num_requests == 0)
+ return NULL;
+
+ /* Check whether host IPs are still up-to-date */
+ ret = http_client_host_refresh(host);
+ if (ret < 0) {
+ /* Performing asynchronous lookup */
+ timeout_remove(&queue->to_connect);
+ return NULL;
+ }
+ if (ret > 0) {
+ /* New lookup performed */
+ http_client_queue_recover_from_lookup(queue);
+ }
+
+ /* Update our peer address */
+ if (queue->addr.type != HTTP_CLIENT_PEER_ADDR_UNIX) {
+ const struct ip_addr *ip =
+ http_client_host_get_ip(host, queue->ips_connect_idx);
+
+ queue->addr.a.tcp.ip = *ip;
+ ssl = http_client_peer_addr_get_https_name(addr);
+ ssl = (ssl == NULL ? "" : t_strdup_printf(" (SSL=%s)", ssl));
+ }
+
+ /* Already got a peer? */
+ peer = NULL;
+ if (queue->cur_peer != NULL) {
+ i_assert(array_count(&queue->pending_peers) == 0);
+
+ /* Is it still the one we want? */
+ if (http_client_peer_addr_cmp(
+ addr, &queue->cur_peer->shared->addr) == 0) {
+ /* Is it still connected? */
+ if (http_client_peer_is_connected(queue->cur_peer)) {
+ /* Yes */
+ e_debug(queue->event,
+ "Using existing connection to %s%s "
+ "(%u requests pending)",
+ http_client_peer_addr2str(addr),
+ ssl, num_requests);
+
+ /* Handle requests; */
+ http_client_peer_trigger_request_handler(
+ queue->cur_peer);
+ return queue->cur_peer;
+ }
+ /* No */
+ peer = queue->cur_peer;
+ } else {
+ /* Peer is not relevant to this queue anymore */
+ http_client_peer_unlink_queue(queue->cur_peer, queue);
+ }
+
+ queue->cur_peer = NULL;
+ }
+
+ if (peer == NULL)
+ peer = http_client_peer_get(queue->client, addr);
+
+ e_debug(queue->event,
+ "Setting up connection to %s%s (%u requests pending)",
+ http_client_peer_addr2str(addr), ssl, num_requests);
+
+ /* Create provisional link between queue and peer */
+ http_client_peer_link_queue(peer, queue);
+
+ /* Handle requests; creates new connections when needed/possible */
+ http_client_peer_trigger_request_handler(peer);
+
+ if (http_client_peer_is_connected(peer)) {
+ /* Drop any pending peers */
+ if (array_count(&queue->pending_peers) > 0) {
+ struct http_client_peer *pending_peer;
+
+ array_foreach_elem(&queue->pending_peers, pending_peer) {
+ if (pending_peer == peer) {
+ /* This can happen with shared clients
+ */
+ continue;
+ }
+ i_assert(http_client_peer_addr_cmp(
+ &pending_peer->shared->addr, addr) != 0);
+ http_client_peer_unlink_queue(pending_peer, queue);
+ }
+ array_clear(&queue->pending_peers);
+ }
+ queue->cur_peer = peer;
+
+ http_client_peer_trigger_request_handler(queue->cur_peer);
+
+ } else {
+ struct http_client_peer *pending_peer;
+ unsigned int msecs;
+ bool new_peer = TRUE;
+
+ /* Not already connected, wait for connections */
+
+ /* We may be waiting for this peer already */
+ array_foreach_elem(&queue->pending_peers, pending_peer) {
+ if (http_client_peer_addr_cmp(
+ &pending_peer->shared->addr, addr) == 0) {
+ i_assert(pending_peer == peer);
+ new_peer = FALSE;
+ break;
+ }
+ }
+ if (new_peer) {
+ e_debug(queue->event, "Started new connection to %s%s",
+ http_client_peer_addr2str(addr), ssl);
+
+ array_push_back(&queue->pending_peers, &peer);
+ if (queue->connect_attempts++ == 0)
+ queue->first_connect_time = ioloop_timeval;
+ }
+
+ /* Start soft connect time-out
+ (but only if we have another IP left) */
+ if (queue->addr.type != HTTP_CLIENT_PEER_ADDR_UNIX) {
+ msecs = client->set.soft_connect_timeout_msecs;
+ if (!http_client_queue_is_last_connect_ip(queue) &&
+ msecs > 0 && queue->to_connect == NULL) {
+ queue->to_connect = timeout_add_to(
+ client->ioloop, msecs,
+ http_client_queue_soft_connect_timeout,
+ queue);
+ }
+ }
+ }
+
+ return peer;
+}
+
+void http_client_queue_connection_setup(struct http_client_queue *queue)
+{
+ (void)http_client_queue_connection_attempt(queue);
+}
+
+unsigned int
+http_client_queue_host_lookup_done(struct http_client_queue *queue)
+{
+ unsigned int reqs_pending =
+ http_client_queue_requests_pending(queue, NULL);
+
+ http_client_queue_recover_from_lookup(queue);
+ if (reqs_pending > 0)
+ http_client_queue_connection_setup(queue);
+ return reqs_pending;
+}
+
+void http_client_queue_host_lookup_failure(
+ struct http_client_queue *queue, const char *error)
+{
+ http_client_queue_fail(
+ queue, HTTP_CLIENT_REQUEST_ERROR_HOST_LOOKUP_FAILED, error);
+}
+
+void http_client_queue_connection_success(struct http_client_queue *queue,
+ struct http_client_peer *peer)
+{
+ const struct http_client_peer_addr *addr = &peer->shared->addr;
+ struct http_client_host *host = queue->host;
+
+ if (http_client_host_ready(host) &&
+ queue->addr.type != HTTP_CLIENT_PEER_ADDR_UNIX) {
+ /* We achieved at least one connection the the addr->ip */
+ if (!http_client_host_get_ip_idx(
+ host, &addr->a.tcp.ip, &queue->ips_connect_start_idx)) {
+ /* list of IPs changed during connect */
+ queue->ips_connect_start_idx = 0;
+ }
+ }
+
+ /* Reset attempt counter */
+ queue->connect_attempts = 0;
+
+ /* stop soft connect time-out */
+ timeout_remove(&queue->to_connect);
+
+ /* Drop all other attempts to the hport. note that we get here whenever
+ a connection is successfully created, so pending_peers array
+ may be empty. */
+ if (array_count(&queue->pending_peers) > 0) {
+ struct http_client_peer *pending_peer;
+
+ array_foreach_elem(&queue->pending_peers, pending_peer) {
+ if (pending_peer == peer) {
+ /* Don't drop any connections to the
+ successfully connected peer, even if some of
+ the connections are pending. they may be
+ intended for urgent requests. */
+ i_assert(queue->cur_peer == NULL);
+ queue->cur_peer = pending_peer;
+ continue;
+ }
+ /* Unlink this queue from the peer; if this was the
+ last/only queue, the peer will be freed, closing all
+ connections.
+ */
+ http_client_peer_unlink_queue(pending_peer, queue);
+ }
+
+ array_clear(&queue->pending_peers);
+ i_assert(queue->cur_peer != NULL);
+ }
+}
+
+void http_client_queue_connection_failure(struct http_client_queue *queue,
+ struct http_client_peer *peer,
+ const char *reason)
+{
+ const struct http_client_settings *set =
+ &queue->client->set;
+ const struct http_client_peer_addr *addr = &peer->shared->addr;
+ const char *https_name = http_client_peer_addr_get_https_name(addr);
+ struct http_client_host *host = queue->host;
+ unsigned int ips_count = http_client_host_get_ips_count(host);
+ struct http_client_peer *const *peer_idx;
+ unsigned int num_requests =
+ array_count(&queue->queued_requests) +
+ array_count(&queue->queued_urgent_requests);
+
+ e_debug(queue->event,
+ "Failed to set up connection to %s%s: %s "
+ "(%u peers pending, %u requests pending)",
+ http_client_peer_addr2str(addr),
+ (https_name == NULL ?
+ "" : t_strdup_printf(" (SSL=%s)", https_name)),
+ reason, array_count(&queue->pending_peers), num_requests);
+
+ http_client_peer_unlink_queue(peer, queue);
+
+ if (array_count(&queue->pending_peers) == 0) {
+ i_assert(queue->cur_peer == NULL || queue->cur_peer == peer);
+ queue->cur_peer = NULL;
+ } else {
+ bool found = FALSE;
+
+ i_assert(queue->cur_peer == NULL);
+
+ /* We're still doing the initial connections to this hport. if
+ we're also doing parallel connections with soft timeouts
+ (pending_peer_count>1), wait for them to finish first. */
+ array_foreach(&queue->pending_peers, peer_idx) {
+ if (*peer_idx == peer) {
+ array_delete(&queue->pending_peers,
+ array_foreach_idx(
+ &queue->pending_peers,
+ peer_idx), 1);
+ found = TRUE;
+ break;
+ }
+ }
+ i_assert(found);
+ if (array_count(&queue->pending_peers) > 0) {
+ e_debug(queue->event,
+ "Waiting for remaining pending peers.");
+ return;
+ }
+
+ /* One of the connections failed. if we're not using soft
+ timeouts, we need to try to connect to the next IP. if we are
+ using soft timeouts, we've already tried all of the IPs by
+ now. */
+ timeout_remove(&queue->to_connect);
+
+ if (queue->addr.type == HTTP_CLIENT_PEER_ADDR_UNIX) {
+ http_client_queue_fail(
+ queue, HTTP_CLIENT_REQUEST_ERROR_CONNECT_FAILED,
+ reason);
+ return;
+ }
+ }
+
+ if (http_client_queue_is_last_connect_ip(queue)) {
+ if (array_count(&queue->pending_peers) > 0) {
+ /* Other connection attempts still pending */
+ return;
+ }
+
+ /* All IPs failed up until here and we allow no more connect
+ attempts, but try the next ones on the next request. */
+ queue->ips_connect_idx = queue->ips_connect_start_idx =
+ (queue->ips_connect_idx + 1) % ips_count;
+
+ if (set->max_connect_attempts == 0 ||
+ queue->connect_attempts >= set->max_connect_attempts) {
+
+ e_debug(queue->event,
+ "Failed to set up any connection; "
+ "failing all queued requests");
+ if (queue->connect_attempts > 1) {
+ unsigned int total_msecs =
+ timeval_diff_msecs(&ioloop_timeval,
+ &queue->first_connect_time);
+ reason = t_strdup_printf(
+ "%s (%u attempts in %u.%03u secs)",
+ reason, queue->connect_attempts,
+ total_msecs/1000, total_msecs%1000);
+ }
+ queue->connect_attempts = 0;
+ http_client_queue_fail(
+ queue, HTTP_CLIENT_REQUEST_ERROR_CONNECT_FAILED,
+ reason);
+ return;
+ }
+ } else {
+ queue->ips_connect_idx =
+ (queue->ips_connect_idx + 1) % ips_count;
+ }
+
+ if (http_client_queue_connection_attempt(queue) != peer)
+ http_client_peer_unlink_queue(peer, queue);
+ return;
+}
+
+void http_client_queue_peer_disconnected(struct http_client_queue *queue,
+ struct http_client_peer *peer)
+{
+ struct http_client_peer *const *peer_idx;
+
+ if (queue->cur_peer == peer) {
+ queue->cur_peer = NULL;
+ return;
+ }
+
+ array_foreach(&queue->pending_peers, peer_idx) {
+ if (*peer_idx == peer) {
+ array_delete(&queue->pending_peers,
+ array_foreach_idx(&queue->pending_peers,
+ peer_idx), 1);
+ break;
+ }
+ }
+}
+
+/*
+ * Main request queue
+ */
+
+void http_client_queue_drop_request(struct http_client_queue *queue,
+ struct http_client_request *req)
+{
+ struct http_client_request **reqs;
+ unsigned int count, i;
+
+ e_debug(queue->event,
+ "Dropping request %s", http_client_request_label(req));
+
+ /* Drop from queue */
+ if (req->urgent) {
+ reqs = array_get_modifiable(&queue->queued_urgent_requests,
+ &count);
+ for (i = 0; i < count; i++) {
+ if (reqs[i] == req) {
+ array_delete(&queue->queued_urgent_requests,
+ i, 1);
+ break;
+ }
+ }
+ } else {
+ reqs = array_get_modifiable(&queue->queued_requests, &count);
+ for (i = 0; i < count; i++) {
+ if (reqs[i] == req) {
+ array_delete(&queue->queued_requests, i, 1);
+ break;
+ }
+ }
+ }
+
+ /* Drop from delay queue */
+ if (req->release_time.tv_sec > 0) {
+ reqs = array_get_modifiable(&queue->delayed_requests, &count);
+ for (i = 0; i < count; i++) {
+ if (reqs[i] == req)
+ break;
+ }
+ if (i < count) {
+ if (i == 0) {
+ if (queue->to_delayed != NULL) {
+ timeout_remove(&queue->to_delayed);
+ if (count > 1) {
+ i_assert(reqs[1]->release_time.tv_sec > 0);
+ http_client_queue_set_delay_timer(
+ queue, reqs[1]->release_time);
+ }
+ }
+ }
+ array_delete(&queue->delayed_requests, i, 1);
+ }
+ }
+
+ /* Drop from main request list */
+ reqs = array_get_modifiable(&queue->requests, &count);
+ for (i = 0; i < count; i++) {
+ if (reqs[i] == req)
+ break;
+ }
+ i_assert(i < count);
+
+ if (i == 0) {
+ if (queue->to_request != NULL) {
+ timeout_remove(&queue->to_request);
+ if (count > 1 && reqs[1]->timeout_time.tv_sec > 0) {
+ http_client_queue_set_request_timer(queue,
+ &reqs[1]->timeout_time);
+ }
+ }
+ }
+ req->queue = NULL;
+ array_delete(&queue->requests, i, 1);
+
+ if (array_count(&queue->requests) == 0)
+ http_client_host_check_idle(queue->host);
+ return;
+}
+
+static void http_client_queue_request_timeout(struct http_client_queue *queue)
+{
+ struct http_client_request *const *reqs;
+ ARRAY_TYPE(http_client_request) failed_requests;
+ struct timeval new_to = { 0, 0 };
+ string_t *str;
+ size_t prefix_size;
+ unsigned int count, i;
+
+ e_debug(queue->event, "Timeout (now: %s.%03lu)",
+ t_strflocaltime("%Y-%m-%d %H:%M:%S", ioloop_timeval.tv_sec),
+ ((unsigned long)ioloop_timeval.tv_usec) / 1000);
+
+ timeout_remove(&queue->to_request);
+
+ /* Collect failed requests */
+ reqs = array_get(&queue->requests, &count);
+ i_assert(count > 0);
+ t_array_init(&failed_requests, count);
+ for (i = 0; i < count; i++) {
+ if (reqs[i]->timeout_time.tv_sec > 0 &&
+ timeval_cmp_margin(&reqs[i]->timeout_time,
+ &ioloop_timeval,
+ TIMEOUT_CMP_MARGIN_USECS) > 0) {
+ break;
+ }
+ array_push_back(&failed_requests, &reqs[i]);
+ }
+
+ /* Update timeout */
+ if (i < count)
+ new_to = reqs[i]->timeout_time;
+
+ str = t_str_new(64);
+ str_append(str, "Request ");
+ prefix_size = str_len(str);
+
+ /* Abort all failed request */
+ reqs = array_get(&failed_requests, &count);
+ i_assert(count > 0); /* At least one request timed out */
+ for (i = 0; i < count; i++) {
+ struct http_client_request *req = reqs[i];
+
+ str_truncate(str, prefix_size);
+ http_client_request_append_stats_text(req, str);
+
+ e_debug(queue->event,
+ "Absolute timeout expired for request %s (%s)",
+ http_client_request_label(req), str_c(str));
+ http_client_request_error(
+ &req, HTTP_CLIENT_REQUEST_ERROR_TIMED_OUT,
+ t_strdup_printf(
+ "Absolute request timeout expired (%s)",
+ str_c(str)));
+ }
+
+ if (new_to.tv_sec > 0) {
+ e_debug(queue->event, "New timeout");
+ http_client_queue_set_request_timer(queue, &new_to);
+ }
+}
+
+static void
+http_client_queue_set_request_timer(struct http_client_queue *queue,
+ const struct timeval *time)
+{
+ i_assert(time->tv_sec > 0);
+ timeout_remove(&queue->to_request);
+
+ e_debug(queue->event,
+ "Set request timeout to %s.%03lu (now: %s.%03lu)",
+ t_strflocaltime("%Y-%m-%d %H:%M:%S", time->tv_sec),
+ ((unsigned long)time->tv_usec) / 1000,
+ t_strflocaltime("%Y-%m-%d %H:%M:%S", ioloop_timeval.tv_sec),
+ ((unsigned long)ioloop_timeval.tv_usec) / 1000);
+
+ /* Set timer */
+ queue->to_request = timeout_add_absolute_to(
+ queue->client->ioloop, time,
+ http_client_queue_request_timeout, queue);
+}
+
+static int
+http_client_queue_request_timeout_cmp(struct http_client_request *const *req1,
+ struct http_client_request *const *req2)
+{
+ int ret;
+
+ /* 0 means no timeout */
+ if ((*req1)->timeout_time.tv_sec == 0) {
+ if ((*req2)->timeout_time.tv_sec == 0) {
+ /* sort by age */
+ ret = timeval_cmp(&(*req1)->submit_time,
+ &(*req2)->submit_time);
+ if (ret != 0)
+ return ret;
+ } else {
+ return 1;
+ }
+ } else if ((*req2)->timeout_time.tv_sec == 0) {
+ return -1;
+
+ /* Sort by timeout */
+ } else if ((ret = timeval_cmp(&(*req1)->timeout_time,
+ &(*req2)->timeout_time)) != 0) {
+ return ret;
+ }
+
+ /* Sort by minimum attempts for fairness */
+ return ((int)(*req2)->attempts - (int)(*req1)->attempts);
+}
+
+static void
+http_client_queue_submit_now(struct http_client_queue *queue,
+ struct http_client_request *req)
+{
+ ARRAY_TYPE(http_client_request) *req_queue;
+
+ req->release_time.tv_sec = 0;
+ req->release_time.tv_usec = 0;
+
+ if (req->urgent)
+ req_queue = &queue->queued_urgent_requests;
+ else
+ req_queue = &queue->queued_requests;
+
+ /* Enqueue */
+ if (req->timeout_time.tv_sec == 0) {
+ /* No timeout; enqueue at end */
+ array_push_back(req_queue, &req);
+ } else if (timeval_diff_msecs(&req->timeout_time,
+ &ioloop_timeval) <= 1) {
+ /* Pretty much already timed out; don't bother */
+ return;
+ } else {
+ unsigned int insert_idx;
+
+ /* Keep transmission queue sorted earliest timeout first */
+ (void)array_bsearch_insert_pos(
+ req_queue, &req,
+ http_client_queue_request_timeout_cmp, &insert_idx);
+ array_insert(req_queue, insert_idx, &req, 1);
+ }
+
+ http_client_queue_connection_setup(queue);
+}
+
+/*
+ * Delayed request queue
+ */
+
+static void
+http_client_queue_delay_timeout(struct http_client_queue *queue)
+{
+ struct http_client_request *const *reqs;
+ unsigned int count, i, finished;
+
+ timeout_remove(&queue->to_delayed);
+ io_loop_time_refresh();
+
+ finished = 0;
+ reqs = array_get(&queue->delayed_requests, &count);
+ for (i = 0; i < count; i++) {
+ if (timeval_cmp_margin(&reqs[i]->release_time,
+ &ioloop_timeval,
+ TIMEOUT_CMP_MARGIN_USECS) > 0) {
+ break;
+ }
+
+ e_debug(queue->event, "Activated delayed request %s%s",
+ http_client_request_label(reqs[i]),
+ (reqs[i]->urgent ? " (urgent)" : ""));
+ http_client_queue_submit_now(queue, reqs[i]);
+ finished++;
+ }
+ if (i < count)
+ http_client_queue_set_delay_timer(queue, reqs[i]->release_time);
+ array_delete(&queue->delayed_requests, 0, finished);
+}
+
+static void
+http_client_queue_set_delay_timer(struct http_client_queue *queue,
+ struct timeval time)
+{
+ struct http_client *client = queue->client;
+ int usecs = timeval_diff_usecs(&time, &ioloop_timeval);
+ int msecs;
+
+ /* Round up to nearest microsecond */
+ msecs = (usecs + 999) / 1000;
+
+ /* Set timer */
+ timeout_remove(&queue->to_delayed);
+ queue->to_delayed = timeout_add_to(
+ client->ioloop, msecs,
+ http_client_queue_delay_timeout, queue);
+}
+
+static int
+http_client_queue_delayed_cmp(struct http_client_request *const *req1,
+ struct http_client_request *const *req2)
+{
+ return timeval_cmp(&(*req1)->release_time, &(*req2)->release_time);
+}
+
+/*
+ * Request submission
+ */
+
+void http_client_queue_submit_request(struct http_client_queue *queue,
+ struct http_client_request *req)
+{
+ unsigned int insert_idx;
+
+ if (req->queue != NULL)
+ http_client_queue_drop_request(req->queue, req);
+ req->queue = queue;
+
+ /* Check delay vs timeout */
+ if (req->release_time.tv_sec > 0 && req->timeout_time.tv_sec > 0 &&
+ timeval_cmp_margin(&req->release_time, &req->timeout_time,
+ TIMEOUT_CMP_MARGIN_USECS) >= 0) {
+ /* Release time is later than absolute timeout */
+ req->release_time.tv_sec = 0;
+ req->release_time.tv_usec = 0;
+
+ /* Timeout rightaway */
+ req->timeout_time = ioloop_timeval;
+
+ e_debug(queue->event,
+ "Delayed request %s%s already timed out",
+ http_client_request_label(req),
+ (req->urgent ? " (urgent)" : ""));
+ }
+
+ /* Add to main request list */
+ if (req->timeout_time.tv_sec == 0) {
+ /* No timeout; just append */
+ array_push_back(&queue->requests, &req);
+ } else {
+ unsigned int insert_idx;
+
+ /* Keep main request list sorted earliest timeout first */
+ (void)array_bsearch_insert_pos(
+ &queue->requests, &req,
+ http_client_queue_request_timeout_cmp, &insert_idx);
+ array_insert(&queue->requests, insert_idx, &req, 1);
+
+ /* Now first in queue; update timer */
+ if (insert_idx == 0) {
+ http_client_queue_set_request_timer(queue,
+ &req->timeout_time);
+ }
+ }
+
+ /* Handle delay */
+ if (req->release_time.tv_sec > 0) {
+ io_loop_time_refresh();
+
+ if (timeval_cmp_margin(&req->release_time, &ioloop_timeval,
+ TIMEOUT_CMP_MARGIN_USECS) > 0) {
+ e_debug(queue->event,
+ "Delayed request %s%s submitted "
+ "(time remaining: %d msecs)",
+ http_client_request_label(req),
+ (req->urgent ? " (urgent)" : ""),
+ timeval_diff_msecs(&req->release_time,
+ &ioloop_timeval));
+
+ (void)array_bsearch_insert_pos(
+ &queue->delayed_requests, &req,
+ http_client_queue_delayed_cmp, &insert_idx);
+ array_insert(&queue->delayed_requests, insert_idx,
+ &req, 1);
+ if (insert_idx == 0) {
+ http_client_queue_set_delay_timer(
+ queue, req->release_time);
+ }
+ return;
+ }
+ }
+
+ http_client_queue_submit_now(queue, req);
+}
+
+/*
+ * Request retrieval
+ */
+
+struct http_client_request *
+http_client_queue_claim_request(struct http_client_queue *queue,
+ const struct http_client_peer_addr *addr,
+ bool no_urgent)
+{
+ struct http_client_request *const *requests;
+ struct http_client_request *req;
+ unsigned int i, count;
+
+ count = 0;
+ if (!no_urgent)
+ requests = array_get(&queue->queued_urgent_requests, &count);
+
+ if (count == 0)
+ requests = array_get(&queue->queued_requests, &count);
+ if (count == 0)
+ return NULL;
+ i = 0;
+ req = requests[i];
+ if (req->urgent)
+ array_delete(&queue->queued_urgent_requests, i, 1);
+ else
+ array_delete(&queue->queued_requests, i, 1);
+
+ e_debug(queue->event,
+ "Connection to peer %s claimed request %s %s",
+ http_client_peer_addr2str(addr), http_client_request_label(req),
+ (req->urgent ? "(urgent)" : ""));
+
+ return req;
+}
+
+unsigned int
+http_client_queue_requests_pending(struct http_client_queue *queue,
+ unsigned int *num_urgent_r)
+{
+ unsigned int urg_count = array_count(&queue->queued_urgent_requests);
+
+ if (num_urgent_r != NULL)
+ *num_urgent_r = urg_count;
+ return array_count(&queue->queued_requests) + urg_count;
+}
+
+unsigned int http_client_queue_requests_active(struct http_client_queue *queue)
+{
+ return array_count(&queue->requests);
+}
+
+/*
+ * Ioloop
+ */
+
+void http_client_queue_switch_ioloop(struct http_client_queue *queue)
+{
+ if (queue->to_connect != NULL)
+ queue->to_connect = io_loop_move_timeout(&queue->to_connect);
+ if (queue->to_request != NULL)
+ queue->to_request = io_loop_move_timeout(&queue->to_request);
+ if (queue->to_delayed != NULL)
+ queue->to_delayed = io_loop_move_timeout(&queue->to_delayed);
+}
diff --git a/src/lib-http/http-client-request.c b/src/lib-http/http-client-request.c
new file mode 100644
index 0000000..60602e0
--- /dev/null
+++ b/src/lib-http/http-client-request.c
@@ -0,0 +1,1875 @@
+/* Copyright (c) 2013-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "net.h"
+#include "str.h"
+#include "str-sanitize.h"
+#include "hash.h"
+#include "array.h"
+#include "llist.h"
+#include "time-util.h"
+#include "istream.h"
+#include "ostream.h"
+#include "file-lock.h"
+#include "dns-lookup.h"
+#include "http-url.h"
+#include "http-date.h"
+#include "http-auth.h"
+#include "http-response-parser.h"
+#include "http-transfer.h"
+
+#include "http-client-private.h"
+
+const char *http_request_state_names[] = {
+ "new",
+ "queued",
+ "payload_out",
+ "waiting",
+ "got_response",
+ "payload_in",
+ "finished",
+ "aborted"
+};
+
+/*
+ * Request
+ */
+
+static bool
+http_client_request_send_error(struct http_client_request *req,
+ unsigned int status, const char *error);
+
+const char *http_client_request_label(struct http_client_request *req)
+{
+ if (req->label == NULL) {
+ req->label = p_strdup_printf(
+ req->pool, "[Req%u: %s %s%s]", req->id, req->method,
+ http_url_create_host(&req->origin_url), req->target);
+ }
+ return req->label;
+}
+
+static void http_client_request_update_event(struct http_client_request *req)
+{
+ event_add_str(req->event, "method", req->method);
+ event_add_str(req->event, "dest_host", req->origin_url.host.name);
+ event_add_int(req->event, "dest_port",
+ http_url_get_port(&req->origin_url));
+ if (req->target != NULL)
+ event_add_str(req->event, "target", req->target);
+ event_set_append_log_prefix(
+ req->event, t_strdup_printf("request %s: ",
+ str_sanitize(http_client_request_label(req), 256)));
+}
+
+static struct event_passthrough *
+http_client_request_result_event(struct http_client_request *req)
+{
+ struct http_client_connection *conn = req->conn;
+
+ if (conn != NULL) {
+ if (req->state == HTTP_REQUEST_STATE_PAYLOAD_OUT) {
+ /* Got here prematurely; use bytes written so far */
+ i_assert(req->request_offset <
+ conn->conn.output->offset);
+ req->bytes_out = conn->conn.output->offset -
+ req->request_offset;
+ }
+ if (conn->incoming_payload != NULL &&
+ (req->state == HTTP_REQUEST_STATE_GOT_RESPONSE ||
+ req->state == HTTP_REQUEST_STATE_PAYLOAD_IN)) {
+ /* Got here prematurely; use bytes read so far */
+ i_assert(conn->in_req_callback ||
+ conn->pending_request == req);
+ i_assert(req->response_offset <
+ conn->conn.input->v_offset);
+ req->bytes_in = conn->conn.input->v_offset -
+ req->response_offset;
+ }
+ }
+
+ struct event_passthrough *e = event_create_passthrough(req->event);
+ if (req->queue != NULL &&
+ req->queue->addr.type != HTTP_CLIENT_PEER_ADDR_UNIX)
+ e->add_str("dest_ip", net_ip2addr(&req->queue->addr.a.tcp.ip));
+
+ return e->add_int("status_code", req->last_status)->
+ add_int("attempts", req->attempts)->
+ add_int("redirects", req->redirects)->
+ add_int("bytes_in", req->bytes_in)->
+ add_int("bytes_out", req->bytes_out);
+}
+
+static struct http_client_request *
+http_client_request_new(struct http_client *client, const char *method,
+ http_client_request_callback_t *callback, void *context)
+{
+ static unsigned int id_counter = 0;
+ pool_t pool;
+ struct http_client_request *req;
+
+ pool = pool_alloconly_create("http client request", 2048);
+ req = p_new(pool, struct http_client_request, 1);
+ req->pool = pool;
+ req->refcount = 1;
+ req->client = client;
+ req->id = ++id_counter;
+ req->method = p_strdup(pool, method);
+ req->callback = callback;
+ req->context = context;
+ req->date = (time_t)-1;
+ req->event = event_create(client->event);
+ event_strlist_copy_recursive(req->event, event_get_global(),
+ EVENT_REASON_CODE);
+
+ /* Default to client-wide settings: */
+ req->max_attempts = client->set.max_attempts;
+ req->attempt_timeout_msecs = client->set.request_timeout_msecs;
+
+ req->state = HTTP_REQUEST_STATE_NEW;
+ return req;
+}
+
+#undef http_client_request
+struct http_client_request *
+http_client_request(struct http_client *client,
+ const char *method, const char *host, const char *target,
+ http_client_request_callback_t *callback, void *context)
+{
+ struct http_client_request *req;
+
+ req = http_client_request_new(client, method, callback, context);
+ req->origin_url.host.name = p_strdup(req->pool, host);
+ req->target = (target == NULL ? "/" : p_strdup(req->pool, target));
+ http_client_request_update_event(req);
+ return req;
+}
+
+#undef http_client_request_url
+struct http_client_request *
+http_client_request_url(struct http_client *client,
+ const char *method, const struct http_url *target_url,
+ http_client_request_callback_t *callback, void *context)
+{
+ struct http_client_request *req;
+
+ req = http_client_request_new(client, method, callback, context);
+ http_url_copy_authority(req->pool, &req->origin_url, target_url);
+ req->target = p_strdup(req->pool, http_url_create_target(target_url));
+ if (target_url->user != NULL && *target_url->user != '\0' &&
+ target_url->password != NULL) {
+ req->username = p_strdup(req->pool, target_url->user);
+ req->password = p_strdup(req->pool, target_url->password);
+ }
+ http_client_request_update_event(req);
+ return req;
+}
+
+#undef http_client_request_url_str
+struct http_client_request *
+http_client_request_url_str(struct http_client *client,
+ const char *method, const char *url_str,
+ http_client_request_callback_t *callback,
+ void *context)
+{
+ struct http_client_request *req, *tmpreq;
+ struct http_url *target_url;
+ const char *error;
+
+ req = tmpreq = http_client_request_new(client, method,
+ callback, context);
+
+ if (http_url_parse(url_str, NULL, HTTP_URL_ALLOW_USERINFO_PART,
+ req->pool, &target_url, &error) < 0) {
+ req->label = p_strdup_printf(req->pool, "[Req%u: %s %s]",
+ req->id, req->method, url_str);
+ http_client_request_error(
+ &tmpreq, HTTP_CLIENT_REQUEST_ERROR_INVALID_URL,
+ t_strdup_printf("Invalid HTTP URL: %s", error));
+ http_client_request_update_event(req);
+ return req;
+ }
+
+ req->origin_url = *target_url;
+ req->target = p_strdup(req->pool, http_url_create_target(target_url));
+ if (target_url->user != NULL && *target_url->user != '\0' &&
+ target_url->password != NULL) {
+ req->username = p_strdup(req->pool, target_url->user);
+ req->password = p_strdup(req->pool, target_url->password);
+ }
+ http_client_request_update_event(req);
+ return req;
+}
+
+#undef http_client_request_connect
+struct http_client_request *
+http_client_request_connect(struct http_client *client,
+ const char *host, in_port_t port,
+ http_client_request_callback_t *callback,
+ void *context)
+{
+ struct http_client_request *req;
+
+ req = http_client_request_new(client, "CONNECT", callback, context);
+ req->origin_url.host.name = p_strdup(req->pool, host);
+ req->origin_url.port = port;
+ req->connect_tunnel = TRUE;
+ req->target = req->origin_url.host.name;
+ http_client_request_update_event(req);
+ return req;
+}
+
+#undef http_client_request_connect_ip
+struct http_client_request *
+http_client_request_connect_ip(struct http_client *client,
+ const struct ip_addr *ip, in_port_t port,
+ http_client_request_callback_t *callback,
+ void *context)
+{
+ struct http_client_request *req;
+ const char *hostname;
+
+ i_assert(ip->family != 0);
+ hostname = net_ip2addr(ip);
+
+ req = http_client_request_connect(client, hostname, port,
+ callback, context);
+ req->origin_url.host.ip = *ip;
+ return req;
+}
+
+void http_client_request_set_event(struct http_client_request *req,
+ struct event *event)
+{
+ event_unref(&req->event);
+ req->event = event_create(event);
+ event_set_forced_debug(req->event, req->client->set.debug);
+ event_strlist_copy_recursive(req->event, event_get_global(),
+ EVENT_REASON_CODE);
+ http_client_request_update_event(req);
+}
+
+static void http_client_request_add(struct http_client_request *req)
+{
+ struct http_client *client = req->client;
+
+ DLLIST_PREPEND(&client->requests_list, req);
+ client->requests_count++;
+ req->listed = TRUE;
+}
+
+static void http_client_request_remove(struct http_client_request *req)
+{
+ struct http_client *client = req->client;
+
+ if (client == NULL) {
+ i_assert(!req->listed);
+ return;
+ }
+ if (req->listed) {
+ /* Only decrease pending request counter if this request was
+ submitted */
+ DLLIST_REMOVE(&client->requests_list, req);
+ client->requests_count--;
+ }
+ req->listed = FALSE;
+
+ if (client->requests_count == 0 && client->waiting)
+ io_loop_stop(client->ioloop);
+}
+
+void http_client_request_ref(struct http_client_request *req)
+{
+ i_assert(req->refcount > 0);
+ req->refcount++;
+}
+
+bool http_client_request_unref(struct http_client_request **_req)
+{
+ struct http_client_request *req = *_req;
+ struct http_client *client = req->client;
+
+ i_assert(req->refcount > 0);
+
+ *_req = NULL;
+
+ if (--req->refcount > 0)
+ return TRUE;
+
+ if (client == NULL) {
+ e_debug(req->event, "Free (client already destroyed)");
+ } else {
+ e_debug(req->event, "Free (requests left=%d)",
+ client->requests_count);
+ }
+
+ /* Cannot be destroyed while it is still pending */
+ i_assert(req->conn == NULL);
+
+ if (req->queue != NULL)
+ http_client_queue_drop_request(req->queue, req);
+
+ if (req->destroy_callback != NULL) {
+ req->destroy_callback(req->destroy_context);
+ req->destroy_callback = NULL;
+ }
+
+ http_client_request_remove(req);
+
+ if (client != NULL) {
+ if (client->requests_count == 0 && client->waiting)
+ io_loop_stop(client->ioloop);
+ if (req->delayed_error != NULL)
+ http_client_remove_request_error(req->client, req);
+ }
+ i_stream_unref(&req->payload_input);
+ o_stream_unref(&req->payload_output);
+ str_free(&req->headers);
+ event_unref(&req->event);
+ pool_unref(&req->pool);
+ return FALSE;
+}
+
+void http_client_request_destroy(struct http_client_request **_req)
+{
+ struct http_client_request *req = *_req, *tmp_req;
+ struct http_client *client = req->client;
+
+ *_req = NULL;
+
+ if (client == NULL) {
+ e_debug(req->event, "Destroy (client already destroyed)");
+ } else {
+ e_debug(req->event, "Destroy (requests left=%d)",
+ client->requests_count);
+ }
+
+
+ if (req->state < HTTP_REQUEST_STATE_FINISHED)
+ req->state = HTTP_REQUEST_STATE_ABORTED;
+ req->callback = NULL;
+
+ if (req->queue != NULL)
+ http_client_queue_drop_request(req->queue, req);
+
+ if (client != NULL && req->delayed_error != NULL)
+ http_client_remove_request_error(req->client, req);
+ req->delayed_error = NULL;
+
+ if (req->destroy_callback != NULL) {
+ void (*callback)(void *) = req->destroy_callback;
+
+ req->destroy_callback = NULL;
+ callback(req->destroy_context);
+ }
+
+ if (req->conn != NULL)
+ http_client_connection_request_destroyed(req->conn, req);
+
+ tmp_req = req;
+ http_client_request_remove(req);
+ if (http_client_request_unref(&tmp_req))
+ req->client = NULL;
+}
+
+void http_client_request_set_port(struct http_client_request *req,
+ in_port_t port)
+{
+ i_assert(req->state == HTTP_REQUEST_STATE_NEW);
+ req->origin_url.port = port;
+ event_add_int(req->event, "port", port);
+}
+
+void http_client_request_set_ssl(struct http_client_request *req, bool ssl)
+{
+ i_assert(req->state == HTTP_REQUEST_STATE_NEW);
+ req->origin_url.have_ssl = ssl;
+}
+
+void http_client_request_set_urgent(struct http_client_request *req)
+{
+ i_assert(req->state == HTTP_REQUEST_STATE_NEW);
+ req->urgent = TRUE;
+}
+
+void http_client_request_set_preserve_exact_reason(
+ struct http_client_request *req)
+{
+ req->preserve_exact_reason = TRUE;
+}
+
+static bool
+http_client_request_lookup_header_pos(struct http_client_request *req,
+ const char *key, size_t *key_pos_r,
+ size_t *value_pos_r, size_t *next_pos_r)
+{
+ const unsigned char *data, *p;
+ size_t size, line_len;
+ size_t key_len = strlen(key);
+
+ if (req->headers == NULL)
+ return FALSE;
+
+ data = str_data(req->headers);
+ size = str_len(req->headers);
+ while ((p = memchr(data, '\n', size)) != NULL) {
+ line_len = (p+1) - data;
+ if (size > key_len && i_memcasecmp(data, key, key_len) == 0 &&
+ data[key_len] == ':' && data[key_len+1] == ' ') {
+ /* Key was found from header, replace its value */
+ *key_pos_r = str_len(req->headers) - size;
+ *value_pos_r = *key_pos_r + key_len + 2;
+ *next_pos_r = *key_pos_r + line_len;
+ return TRUE;
+ }
+ size -= line_len;
+ data += line_len;
+ }
+ return FALSE;
+}
+
+static void
+http_client_request_add_header_full(struct http_client_request *req,
+ const char *key, const char *value,
+ bool replace_existing)
+{
+ size_t key_pos, value_pos, next_pos;
+
+ i_assert(req->state == HTTP_REQUEST_STATE_NEW ||
+ /* Allow calling for retries */
+ req->state == HTTP_REQUEST_STATE_GOT_RESPONSE ||
+ req->state == HTTP_REQUEST_STATE_ABORTED);
+ /* Make sure key or value can't break HTTP headers entirely */
+ i_assert(strpbrk(key, ":\r\n") == NULL);
+ i_assert(strpbrk(value, "\r\n") == NULL);
+
+ /* Mark presence of special headers */
+ switch (key[0]) {
+ case 'a': case 'A':
+ if (strcasecmp(key, "Authorization") == 0)
+ req->have_hdr_authorization = TRUE;
+ break;
+ case 'c': case 'C':
+ if (strcasecmp(key, "Connection") == 0)
+ req->have_hdr_connection = TRUE;
+ else if (strcasecmp(key, "Content-Length") == 0)
+ req->have_hdr_body_spec = TRUE;
+ break;
+ case 'd': case 'D':
+ if (strcasecmp(key, "Date") == 0)
+ req->have_hdr_date = TRUE;
+ break;
+ case 'e': case 'E':
+ if (strcasecmp(key, "Expect") == 0)
+ req->have_hdr_expect = TRUE;
+ break;
+ case 'h': case 'H':
+ if (strcasecmp(key, "Host") == 0)
+ req->have_hdr_host = TRUE;
+ break;
+ case 'p': case 'P':
+ i_assert(strcasecmp(key, "Proxy-Authorization") != 0);
+ break;
+ case 't': case 'T':
+ if (strcasecmp(key, "Transfer-Encoding") == 0)
+ req->have_hdr_body_spec = TRUE;
+ break;
+ case 'u': case 'U':
+ if (strcasecmp(key, "User-Agent") == 0)
+ req->have_hdr_user_agent = TRUE;
+ break;
+ }
+ if (req->headers == NULL)
+ req->headers = str_new(default_pool, 256);
+ if (!http_client_request_lookup_header_pos(req, key, &key_pos,
+ &value_pos, &next_pos))
+ str_printfa(req->headers, "%s: %s\r\n", key, value);
+ else if (replace_existing) {
+ /* Don't delete CRLF */
+ size_t old_value_len = next_pos - value_pos - 2;
+ str_replace(req->headers, value_pos, old_value_len, value);
+ }
+}
+
+void http_client_request_add_header(struct http_client_request *req,
+ const char *key, const char *value)
+{
+ http_client_request_add_header_full(req, key, value, TRUE);
+}
+
+void http_client_request_add_missing_header(struct http_client_request *req,
+ const char *key, const char *value)
+{
+ http_client_request_add_header_full(req, key, value, FALSE);
+}
+
+void http_client_request_remove_header(struct http_client_request *req,
+ const char *key)
+{
+ size_t key_pos, value_pos, next_pos;
+
+ i_assert(req->state == HTTP_REQUEST_STATE_NEW ||
+ /* Allow calling for retries */
+ req->state == HTTP_REQUEST_STATE_GOT_RESPONSE ||
+ req->state == HTTP_REQUEST_STATE_ABORTED);
+
+ if (http_client_request_lookup_header_pos(req, key, &key_pos,
+ &value_pos, &next_pos))
+ str_delete(req->headers, key_pos, next_pos - key_pos);
+}
+
+const char *http_client_request_lookup_header(struct http_client_request *req,
+ const char *key)
+{
+ size_t key_pos, value_pos, next_pos;
+
+ if (!http_client_request_lookup_header_pos(req, key, &key_pos,
+ &value_pos, &next_pos))
+ return NULL;
+
+ /* Don't return CRLF */
+ return t_strndup(str_data(req->headers) + value_pos,
+ next_pos - value_pos - 2);
+}
+
+void http_client_request_set_date(struct http_client_request *req, time_t date)
+{
+ i_assert(req->state == HTTP_REQUEST_STATE_NEW);
+ req->date = date;
+}
+
+void http_client_request_set_payload(struct http_client_request *req,
+ struct istream *input, bool sync)
+{
+ int ret;
+
+ i_assert(req->state == HTTP_REQUEST_STATE_NEW);
+ i_assert(req->payload_input == NULL);
+
+ i_stream_ref(input);
+ req->payload_input = input;
+ if ((ret = i_stream_get_size(input, TRUE, &req->payload_size)) <= 0) {
+ if (ret < 0) {
+ i_error("i_stream_get_size(%s) failed: %s",
+ i_stream_get_name(input),
+ i_stream_get_error(input));
+ }
+ req->payload_size = 0;
+ req->payload_chunked = TRUE;
+ } else {
+ i_assert(input->v_offset <= req->payload_size);
+ req->payload_size -= input->v_offset;
+ }
+ req->payload_offset = input->v_offset;
+
+ /* Prepare request payload sync using 100 Continue response from server
+ */
+ if ((req->payload_chunked || req->payload_size > 0) && sync)
+ req->payload_sync = TRUE;
+}
+
+void http_client_request_set_payload_data(struct http_client_request *req,
+ const unsigned char *data,
+ size_t size)
+{
+ struct istream *input;
+ unsigned char *payload_data;
+
+ if (size == 0)
+ return;
+
+ payload_data = p_malloc(req->pool, size);
+ memcpy(payload_data, data, size);
+ input = i_stream_create_from_data(payload_data, size);
+
+ http_client_request_set_payload(req, input, FALSE);
+ i_stream_unref(&input);
+}
+
+void http_client_request_set_payload_empty(struct http_client_request *req)
+{
+ req->payload_empty = TRUE;
+}
+
+void http_client_request_set_timeout_msecs(struct http_client_request *req,
+ unsigned int msecs)
+{
+ i_assert(req->state == HTTP_REQUEST_STATE_NEW ||
+ req->state == HTTP_REQUEST_STATE_GOT_RESPONSE);
+
+ req->timeout_msecs = msecs;
+}
+
+void http_client_request_set_timeout(struct http_client_request *req,
+ const struct timeval *time)
+{
+ i_assert(req->state == HTTP_REQUEST_STATE_NEW ||
+ req->state == HTTP_REQUEST_STATE_GOT_RESPONSE);
+
+ req->timeout_time = *time;
+ req->timeout_msecs = 0;
+}
+
+void http_client_request_set_attempt_timeout_msecs(
+ struct http_client_request *req, unsigned int msecs)
+{
+ i_assert(req->state == HTTP_REQUEST_STATE_NEW ||
+ req->state == HTTP_REQUEST_STATE_GOT_RESPONSE);
+
+ req->attempt_timeout_msecs = msecs;
+}
+
+void http_client_request_set_max_attempts(struct http_client_request *req,
+ unsigned int max_attempts)
+{
+ i_assert(req->state == HTTP_REQUEST_STATE_NEW ||
+ req->state == HTTP_REQUEST_STATE_GOT_RESPONSE);
+
+ req->max_attempts = max_attempts;
+}
+
+void http_client_request_set_event_headers(struct http_client_request *req,
+ const char *const *headers)
+{
+ req->event_headers = p_strarray_dup(req->pool, headers);
+}
+
+void http_client_request_set_auth_simple(struct http_client_request *req,
+ const char *username,
+ const char *password)
+{
+ req->username = p_strdup(req->pool, username);
+ req->password = p_strdup(req->pool, password);
+}
+
+void http_client_request_set_proxy_url(struct http_client_request *req,
+ const struct http_url *proxy_url)
+{
+ i_assert(req->state == HTTP_REQUEST_STATE_NEW ||
+ req->state == HTTP_REQUEST_STATE_GOT_RESPONSE);
+
+ req->host_url = http_url_clone_authority(req->pool, proxy_url);
+ req->host_socket = NULL;
+}
+
+void http_client_request_set_proxy_socket(struct http_client_request *req,
+ const char *proxy_socket)
+{
+ i_assert(req->state == HTTP_REQUEST_STATE_NEW ||
+ req->state == HTTP_REQUEST_STATE_GOT_RESPONSE);
+
+ req->host_socket = p_strdup(req->pool, proxy_socket);
+ req->host_url = NULL;
+}
+
+void http_client_request_delay_until(struct http_client_request *req,
+ time_t time)
+{
+ req->release_time.tv_sec = time;
+ req->release_time.tv_usec = 0;
+}
+
+void http_client_request_delay(struct http_client_request *req, time_t seconds)
+{
+ req->release_time = ioloop_timeval;
+ req->release_time.tv_sec += seconds;
+}
+
+void http_client_request_delay_msecs(struct http_client_request *req,
+ unsigned int msecs)
+{
+ req->release_time = ioloop_timeval;
+ timeval_add_msecs(&req->release_time, msecs);
+}
+
+int http_client_request_delay_from_response(
+ struct http_client_request *req, const struct http_response *response)
+{
+ time_t retry_after = response->retry_after;
+ unsigned int max;
+
+ i_assert(req->client != NULL);
+
+ if (retry_after == (time_t)-1)
+ return 0; /* no delay */
+ if (retry_after < ioloop_time)
+ return 0; /* delay already expired */
+ max = (req->client->set.max_auto_retry_delay_secs == 0 ?
+ req->attempt_timeout_msecs / 1000 :
+ req->client->set.max_auto_retry_delay_secs);
+ if ((unsigned int)(retry_after - ioloop_time) > max)
+ return -1; /* delay too long */
+ req->release_time.tv_sec = retry_after;
+ req->release_time.tv_usec = 0;
+ return 1; /* valid delay */
+}
+
+const char *
+http_client_request_get_method(const struct http_client_request *req)
+{
+ return req->method;
+}
+
+const char *
+http_client_request_get_target(const struct http_client_request *req)
+{
+ return req->target;
+}
+
+const struct http_url *
+http_client_request_get_origin_url(const struct http_client_request *req)
+{
+ return &req->origin_url;
+}
+
+enum http_request_state
+http_client_request_get_state(const struct http_client_request *req)
+{
+ return req->state;
+}
+
+unsigned int
+http_client_request_get_attempts(const struct http_client_request *req)
+{
+ return req->attempts;
+}
+
+void http_client_request_get_stats(struct http_client_request *req,
+ struct http_client_request_stats *stats_r)
+{
+ struct http_client *client = req->client;
+ int diff_msecs;
+ uint64_t wait_usecs;
+
+ i_zero(stats_r);
+ if (!req->submitted)
+ return;
+
+ /* Total elapsed time since message was submitted */
+ diff_msecs = timeval_diff_msecs(&ioloop_timeval, &req->submit_time);
+ stats_r->total_msecs = (unsigned int)I_MAX(diff_msecs, 0);
+
+ /* Elapsed time since message was first sent */
+ if (req->first_sent_time.tv_sec > 0) {
+ diff_msecs = timeval_diff_msecs(&ioloop_timeval,
+ &req->first_sent_time);
+ stats_r->first_sent_msecs = (unsigned int)I_MAX(diff_msecs, 0);
+ }
+
+ /* Elapsed time since message was last sent */
+ if (req->sent_time.tv_sec > 0) {
+ diff_msecs = timeval_diff_msecs(&ioloop_timeval,
+ &req->sent_time);
+ stats_r->last_sent_msecs = (unsigned int)I_MAX(diff_msecs, 0);
+ }
+
+ if (req->conn != NULL) {
+ /* Time spent in other ioloops */
+ i_assert(ioloop_global_wait_usecs >=
+ req->sent_global_ioloop_usecs);
+ stats_r->other_ioloop_msecs = (unsigned int)
+ (ioloop_global_wait_usecs -
+ req->sent_global_ioloop_usecs + 999) / 1000;
+
+ /* Time spent in the http-client's own ioloop */
+ if (client != NULL && client->waiting) {
+ wait_usecs =
+ io_wait_timer_get_usecs(req->conn->io_wait_timer);
+ i_assert(wait_usecs >= req->sent_http_ioloop_usecs);
+ stats_r->http_ioloop_msecs = (unsigned int)
+ (wait_usecs -
+ req->sent_http_ioloop_usecs + 999) / 1000;
+
+ i_assert(stats_r->other_ioloop_msecs >=
+ stats_r->http_ioloop_msecs);
+ stats_r->other_ioloop_msecs -= stats_r->http_ioloop_msecs;
+ }
+ }
+
+ /* Total time spent on waiting for file locks */
+ wait_usecs = file_lock_wait_get_total_usecs();
+ i_assert(wait_usecs >= req->sent_lock_usecs);
+ stats_r->lock_msecs = (unsigned int)
+ (wait_usecs - req->sent_lock_usecs + 999) / 1000;
+
+ /* Number of attempts for this request */
+ stats_r->attempts = req->attempts;
+ /* Number of send attempts for this request */
+ stats_r->send_attempts = req->send_attempts;
+}
+
+void http_client_request_append_stats_text(struct http_client_request *req,
+ string_t *str)
+{
+ struct http_client_request_stats stats;
+
+ if (!req->submitted) {
+ str_append(str, "not yet submitted");
+ return;
+ }
+
+ http_client_request_get_stats(req, &stats);
+
+ str_printfa(str, "queued %u.%03u secs ago",
+ stats.total_msecs/1000, stats.total_msecs%1000);
+ if (stats.attempts > 0)
+ str_printfa(str, ", %u times retried", stats.attempts);
+
+ if (stats.send_attempts == 0) {
+ str_append(str, ", not yet sent");
+ } else {
+ str_printfa(str, ", %u send attempts in %u.%03u secs",
+ stats.send_attempts, stats.first_sent_msecs/1000,
+ stats.first_sent_msecs%1000);
+ if (stats.send_attempts > 1) {
+ str_printfa(str, ", %u.%03u in last attempt",
+ stats.last_sent_msecs/1000,
+ stats.last_sent_msecs%1000);
+ }
+ }
+
+ if (stats.http_ioloop_msecs > 0) {
+ str_printfa(str, ", %u.%03u in http ioloop",
+ stats.http_ioloop_msecs/1000,
+ stats.http_ioloop_msecs%1000);
+ }
+ str_printfa(str, ", %u.%03u in other ioloops",
+ stats.other_ioloop_msecs/1000,
+ stats.other_ioloop_msecs%1000);
+
+ if (stats.lock_msecs > 0) {
+ str_printfa(str, ", %u.%03u in locks",
+ stats.lock_msecs/1000, stats.lock_msecs%1000);
+ }
+}
+
+enum http_response_payload_type
+http_client_request_get_payload_type(struct http_client_request *req)
+{
+ /* RFC 7230, Section 3.3:
+
+ The presence of a message body in a response depends on both the
+ request method to which it is responding and the response status code
+ (Section 3.1.2 of [RFC7230]). Responses to the HEAD request method
+ (Section 4.3.2 of [RFC7231]) never include a message body because the
+ associated response header fields (e.g., Transfer-Encoding,
+ Content-Length, etc.), if present, indicate only what their values
+ would have been if the request method had been GET (Section 4.3.1 of
+ [RFC7231]). 2xx (Successful) responses to a CONNECT request method
+ (Section 4.3.6 of [RFC7231]) switch to tunnel mode instead of having
+ a message body.
+ */
+ if (strcmp(req->method, "HEAD") == 0)
+ return HTTP_RESPONSE_PAYLOAD_TYPE_NOT_PRESENT;
+ if (strcmp(req->method, "CONNECT") == 0)
+ return HTTP_RESPONSE_PAYLOAD_TYPE_ONLY_UNSUCCESSFUL;
+ return HTTP_RESPONSE_PAYLOAD_TYPE_ALLOWED;
+}
+
+static void http_client_request_do_submit(struct http_client_request *req)
+{
+ struct http_client *client = req->client;
+ struct http_client_host *host;
+ const char *proxy_socket_path = client->set.proxy_socket_path;
+ const struct http_url *proxy_url = client->set.proxy_url;
+ bool have_proxy =
+ ((proxy_socket_path != NULL) || (proxy_url != NULL) ||
+ (req->host_socket != NULL) || (req->host_url != NULL));
+ const char *authority, *target;
+
+ if (req->state == HTTP_REQUEST_STATE_ABORTED)
+ return;
+ i_assert(client != NULL);
+ i_assert(req->state == HTTP_REQUEST_STATE_NEW);
+
+ authority = http_url_create_authority(&req->origin_url);
+ if (req->connect_tunnel) {
+ /* Connect requests require authority form for request target */
+ target = authority;
+ } else {
+ /* Absolute target URL */
+ target = t_strconcat(http_url_create_host(&req->origin_url),
+ req->target, NULL);
+ }
+
+ /* Determine what host to contact to submit this request */
+ if (have_proxy) {
+ if (req->host_socket != NULL) {
+ /* Specific socket proxy */
+ req->host_url = NULL;
+ } else if (req->host_url != NULL) {
+ /* Specific normal proxy */
+ req->host_socket = NULL;
+ } else if (req->origin_url.have_ssl &&
+ !client->set.no_ssl_tunnel &&
+ !req->connect_tunnel) {
+ /* Tunnel to origin server */
+ req->host_url = &req->origin_url;
+ req->ssl_tunnel = TRUE;
+ } else if (proxy_socket_path != NULL) {
+ /* Proxy on unix socket */
+ req->host_socket = proxy_socket_path;
+ req->host_url = NULL;
+ } else {
+ /* Normal proxy server */
+ req->host_url = proxy_url;
+ req->host_socket = NULL;
+ }
+ } else {
+ /* Origin server */
+ req->host_url = &req->origin_url;
+ }
+
+ /* Use submission date if no date is set explicitly */
+ if (req->date == (time_t)-1)
+ req->date = ioloop_time;
+
+ /* Prepare value for Host header */
+ req->authority = p_strdup(req->pool, authority);
+
+ /* Debug label */
+ req->label = p_strdup_printf(req->pool, "[Req%u: %s %s]",
+ req->id, req->method, target);
+
+ /* Update request target */
+ if (req->connect_tunnel || have_proxy)
+ req->target = p_strdup(req->pool, target);
+
+ if (!have_proxy) {
+ /* If we don't have a proxy, CONNECT requests are handled by
+ creating the requested connection directly */
+ req->connect_direct = req->connect_tunnel;
+ if (req->connect_direct)
+ req->urgent = TRUE;
+ }
+
+ if (req->timeout_time.tv_sec == 0) {
+ if (req->timeout_msecs > 0) {
+ req->timeout_time = ioloop_timeval;
+ timeval_add_msecs(&req->timeout_time,
+ req->timeout_msecs);
+ } else if (client->set.request_absolute_timeout_msecs > 0) {
+ req->timeout_time = ioloop_timeval;
+ timeval_add_msecs(&req->timeout_time,
+ client->set.request_absolute_timeout_msecs);
+ }
+ }
+
+ host = http_client_host_get(client, req->host_url);
+ req->state = HTTP_REQUEST_STATE_QUEUED;
+ req->last_status = 0;
+
+ http_client_host_submit_request(host, req);
+}
+
+void http_client_request_submit(struct http_client_request *req)
+{
+ i_assert(req->client != NULL);
+
+ req->submit_time = ioloop_timeval;
+
+ http_client_request_update_event(req);
+ http_client_request_do_submit(req);
+
+ req->submitted = TRUE;
+ http_client_request_add(req);
+
+ e_debug(req->event, "Submitted (requests left=%d)",
+ req->client->requests_count);
+}
+
+void http_client_request_get_peer_addr(const struct http_client_request *req,
+ struct http_client_peer_addr *addr)
+{
+ const char *host_socket = req->host_socket;
+ const struct http_url *host_url = req->host_url;
+
+ /* The IP address may be unassigned in the returned peer address, since
+ that is only available at this stage when the target URL has an
+ explicit IP address. */
+ i_zero(addr);
+ if (host_socket != NULL) {
+ addr->type = HTTP_CLIENT_PEER_ADDR_UNIX;
+ addr->a.un.path = host_socket;
+ } else if (req->connect_direct) {
+ addr->type = HTTP_CLIENT_PEER_ADDR_RAW;
+ addr->a.tcp.ip = host_url->host.ip;
+ addr->a.tcp.port =
+ http_url_get_port_default(host_url, HTTPS_DEFAULT_PORT);
+ } else if (host_url->have_ssl) {
+ if (req->ssl_tunnel)
+ addr->type = HTTP_CLIENT_PEER_ADDR_HTTPS_TUNNEL;
+ else
+ addr->type = HTTP_CLIENT_PEER_ADDR_HTTPS;
+ addr->a.tcp.ip = host_url->host.ip;
+ addr->a.tcp.https_name = host_url->host.name;
+ addr->a.tcp.port = http_url_get_port(host_url);
+ } else {
+ addr->type = HTTP_CLIENT_PEER_ADDR_HTTP;
+ addr->a.tcp.ip = host_url->host.ip;
+ addr->a.tcp.port = http_url_get_port(host_url);
+ }
+}
+
+static int http_client_request_flush_payload(struct http_client_request *req)
+{
+ struct http_client_connection *conn = req->conn;
+ int ret;
+
+ if (req->payload_output != conn->conn.output &&
+ (ret = o_stream_finish(req->payload_output)) <= 0) {
+ if (ret < 0)
+ http_client_connection_handle_output_error(conn);
+ return ret;
+ }
+
+ return 1;
+}
+
+static int
+http_client_request_finish_payload_out(struct http_client_request *req)
+{
+ struct http_client_connection *conn = req->conn;
+ int ret;
+
+ i_assert(conn != NULL);
+ req->payload_finished = TRUE;
+
+ /* Drop payload output stream */
+ if (req->payload_output != NULL) {
+ ret = http_client_request_flush_payload(req);
+ if (ret < 0)
+ return -1;
+ if (ret == 0) {
+ e_debug(req->event,
+ "Not quite finished sending payload");
+ return 0;
+ }
+ o_stream_unref(&req->payload_output);
+ req->payload_output = NULL;
+ }
+
+ i_assert(req->request_offset < conn->conn.output->offset);
+ req->bytes_out = conn->conn.output->offset - req->request_offset;
+
+ /* Advance state only when request didn't get aborted in the mean time
+ */
+ if (req->state != HTTP_REQUEST_STATE_ABORTED) {
+ i_assert(req->state == HTTP_REQUEST_STATE_PAYLOAD_OUT);
+
+ /* we're now waiting for a response from the server */
+ req->state = HTTP_REQUEST_STATE_WAITING;
+ http_client_connection_start_request_timeout(conn);
+ }
+
+ /* Release connection */
+ conn->output_locked = FALSE;
+
+ e_debug(req->event, "Finished sending%s payload",
+ (req->state == HTTP_REQUEST_STATE_ABORTED ? " aborted" : ""));
+ return 1;
+}
+
+static int
+http_client_request_continue_payload(struct http_client_request **_req,
+ const unsigned char *data, size_t size)
+{
+ struct ioloop *prev_ioloop, *client_ioloop, *prev_client_ioloop;
+ struct http_client_request *req = *_req;
+ struct http_client_connection *conn = req->conn;
+ struct http_client *client = req->client;
+ int ret;
+
+ i_assert(client != NULL);
+ i_assert(req->state == HTTP_REQUEST_STATE_NEW ||
+ req->state == HTTP_REQUEST_STATE_PAYLOAD_OUT);
+ i_assert(req->payload_input == NULL);
+
+ if (conn != NULL)
+ http_client_connection_ref(conn);
+ http_client_request_ref(req);
+ req->payload_wait = TRUE;
+
+ if (data == NULL) {
+ req->payload_input = NULL;
+ if (req->state == HTTP_REQUEST_STATE_PAYLOAD_OUT)
+ (void)http_client_request_finish_payload_out(req);
+ } else {
+ req->payload_input = i_stream_create_from_data(data, size);
+ i_stream_set_name(req->payload_input, "<HTTP request payload>");
+ }
+ req->payload_size = 0;
+ req->payload_chunked = TRUE;
+
+ if (req->state == HTTP_REQUEST_STATE_NEW)
+ http_client_request_submit(req);
+ if (req->state == HTTP_REQUEST_STATE_ABORTED) {
+ /* Request already failed */
+ if (req->delayed_error != NULL) {
+ struct http_client_request *tmpreq = req;
+
+ /* Handle delayed error outside ioloop; the caller
+ expects callbacks occurring, so there is no need for
+ delay. Also, it is very important that any error
+ triggers a callback before
+ http_client_request_send_payload() finishes, since
+ its return value is not always checked.
+ */
+ http_client_remove_request_error(client, req);
+ http_client_request_error_delayed(&tmpreq);
+ }
+ } else {
+ /* Wait for payload data to be written */
+
+ prev_ioloop = current_ioloop;
+ client_ioloop = io_loop_create();
+ prev_client_ioloop = http_client_switch_ioloop(client);
+ if (client->set.dns_client != NULL)
+ dns_client_switch_ioloop(client->set.dns_client);
+
+ client->waiting = TRUE;
+ while (req->state < HTTP_REQUEST_STATE_PAYLOAD_IN) {
+ e_debug(req->event, "Waiting for request to finish");
+
+ if (req->state == HTTP_REQUEST_STATE_PAYLOAD_OUT) {
+ o_stream_set_flush_pending(
+ req->payload_output, TRUE);
+ }
+
+ io_loop_run(client_ioloop);
+
+ if (req->state == HTTP_REQUEST_STATE_PAYLOAD_OUT &&
+ req->payload_input->eof) {
+ i_stream_unref(&req->payload_input);
+ req->payload_input = NULL;
+ break;
+ }
+ }
+ client->waiting = FALSE;
+
+ if (prev_client_ioloop != NULL)
+ io_loop_set_current(prev_client_ioloop);
+ else
+ io_loop_set_current(prev_ioloop);
+ (void)http_client_switch_ioloop(client);
+ if (client->set.dns_client != NULL)
+ dns_client_switch_ioloop(client->set.dns_client);
+ io_loop_set_current(client_ioloop);
+ io_loop_destroy(&client_ioloop);
+ }
+
+ switch (req->state) {
+ case HTTP_REQUEST_STATE_PAYLOAD_IN:
+ case HTTP_REQUEST_STATE_FINISHED:
+ ret = 1;
+ break;
+ case HTTP_REQUEST_STATE_ABORTED:
+ ret = -1;
+ break;
+ default:
+ ret = 0;
+ break;
+ }
+
+ req->payload_wait = FALSE;
+
+ /* callback may have messed with our pointer, so unref using local
+ variable */
+ if (!http_client_request_unref(&req))
+ *_req = NULL;
+
+ if (conn != NULL)
+ http_client_connection_unref(&conn);
+
+ return ret;
+}
+
+int http_client_request_send_payload(struct http_client_request **_req,
+ const unsigned char *data, size_t size)
+{
+ struct http_client_request *req = *_req;
+ int ret;
+
+ i_assert(data != NULL);
+
+ ret = http_client_request_continue_payload(&req, data, size);
+ if (ret < 0) {
+ /* Failed to send payload */
+ *_req = NULL;
+ } else if (ret > 0) {
+ /* Premature end of request;
+ server sent error before all payload could be sent */
+ ret = -1;
+ *_req = NULL;
+ } else {
+ /* Not finished sending payload */
+ i_assert(req != NULL);
+ }
+ return ret;
+}
+
+int http_client_request_finish_payload(struct http_client_request **_req)
+{
+ struct http_client_request *req = *_req;
+ int ret;
+
+ *_req = NULL;
+ ret = http_client_request_continue_payload(&req, NULL, 0);
+ i_assert(ret != 0);
+ return ret < 0 ? -1 : 0;
+}
+
+static void http_client_request_payload_input(struct http_client_request *req)
+{
+ struct http_client_connection *conn = req->conn;
+
+ io_remove(&conn->io_req_payload);
+
+ (void)http_client_connection_output(conn);
+}
+
+int http_client_request_send_more(struct http_client_request *req,
+ bool pipelined)
+{
+ struct http_client_connection *conn = req->conn;
+ struct http_client_context *cctx = conn->ppool->peer->cctx;
+ struct ostream *output = req->payload_output;
+ enum ostream_send_istream_result res;
+ const char *error;
+ uoff_t offset;
+
+ if (req->payload_finished)
+ return http_client_request_finish_payload_out(req);
+
+ i_assert(req->payload_input != NULL);
+ i_assert(req->payload_output != NULL);
+
+ io_remove(&conn->io_req_payload);
+
+ /* Chunked ostream needs to write to the parent stream's buffer */
+ offset = req->payload_input->v_offset;
+ o_stream_set_max_buffer_size(output, IO_BLOCK_SIZE);
+ res = o_stream_send_istream(output, req->payload_input);
+ o_stream_set_max_buffer_size(output, SIZE_MAX);
+
+ i_assert(req->payload_input->v_offset >= offset);
+ e_debug(req->event, "Send more (sent %"PRIuUOFF_T", buffered=%zu)",
+ (uoff_t)(req->payload_input->v_offset - offset),
+ o_stream_get_buffer_used_size(output));
+
+ switch (res) {
+ case OSTREAM_SEND_ISTREAM_RESULT_FINISHED:
+ /* Finished sending */
+ if (!req->payload_chunked &&
+ (req->payload_input->v_offset - req->payload_offset) !=
+ req->payload_size) {
+ error = t_strdup_printf(
+ "BUG: stream '%s' input size changed: "
+ "%"PRIuUOFF_T"-%"PRIuUOFF_T" != %"PRIuUOFF_T,
+ i_stream_get_name(req->payload_input),
+ req->payload_input->v_offset,
+ req->payload_offset, req->payload_size);
+ i_error("%s", error); //FIXME: remove?
+ http_client_connection_lost(&conn, error);
+ return -1;
+ }
+
+ if (req->payload_wait) {
+ /* This chunk of input is finished
+ (client needs to act; disable timeout) */
+ i_assert(!pipelined);
+ conn->output_locked = TRUE;
+ http_client_connection_stop_request_timeout(conn);
+ if (req->client != NULL && req->client->waiting)
+ io_loop_stop(req->client->ioloop);
+ return 0;
+ }
+ /* Finished sending payload */
+ return http_client_request_finish_payload_out(req);
+ case OSTREAM_SEND_ISTREAM_RESULT_WAIT_INPUT:
+ /* Input is blocking (client needs to act; disable timeout) */
+ conn->output_locked = TRUE;
+ if (!pipelined)
+ http_client_connection_stop_request_timeout(conn);
+ conn->io_req_payload = io_add_istream_to(
+ cctx->ioloop, req->payload_input,
+ http_client_request_payload_input, req);
+ return 1;
+ case OSTREAM_SEND_ISTREAM_RESULT_WAIT_OUTPUT:
+ /* Output is blocking (server needs to act; enable timeout) */
+ conn->output_locked = TRUE;
+ if (!pipelined)
+ http_client_connection_start_request_timeout(conn);
+ e_debug(req->event, "Partially sent payload");
+ return 0;
+ case OSTREAM_SEND_ISTREAM_RESULT_ERROR_INPUT:
+ /* We're in the middle of sending a request, so the connection
+ will also have to be aborted */
+ error = t_strdup_printf("read(%s) failed: %s",
+ i_stream_get_name(req->payload_input),
+ i_stream_get_error(req->payload_input));
+
+ /* The payload stream assigned to this request is broken, fail
+ this the request immediately */
+ http_client_request_error(&req,
+ HTTP_CLIENT_REQUEST_ERROR_BROKEN_PAYLOAD,
+ "Broken payload stream");
+
+ http_client_connection_lost(&conn, error);
+ return -1;
+ case OSTREAM_SEND_ISTREAM_RESULT_ERROR_OUTPUT:
+ /* Failed to send request */
+ http_client_connection_handle_output_error(conn);
+ return -1;
+ }
+ i_unreached();
+}
+
+static int
+http_client_request_send_real(struct http_client_request *req, bool pipelined)
+{
+ const struct http_client_settings *set = &req->client->set;
+ struct http_client_connection *conn = req->conn;
+ string_t *rtext = t_str_new(256);
+ struct const_iovec iov[3];
+ int ret;
+
+ i_assert(!req->conn->output_locked);
+ i_assert(req->payload_output == NULL);
+
+ /* Create request line */
+ str_append(rtext, req->method);
+ str_append(rtext, " ");
+ str_append(rtext, req->target);
+ str_append(rtext, " HTTP/1.1\r\n");
+
+ /* Create special headers implicitly if not set explicitly using
+ http_client_request_add_header() */
+ if (!req->have_hdr_host) {
+ str_append(rtext, "Host: ");
+ str_append(rtext, req->authority);
+ str_append(rtext, "\r\n");
+ }
+ if (!req->have_hdr_date) {
+ str_append(rtext, "Date: ");
+ str_append(rtext, http_date_create(req->date));
+ str_append(rtext, "\r\n");
+ }
+ if (!req->have_hdr_authorization &&
+ req->username != NULL && req->password != NULL) {
+ struct http_auth_credentials auth_creds;
+
+ http_auth_basic_credentials_init(&auth_creds,
+ req->username, req->password);
+
+ str_append(rtext, "Authorization: ");
+ http_auth_create_credentials(rtext, &auth_creds);
+ str_append(rtext, "\r\n");
+ }
+ if (http_client_request_to_proxy(req) &&
+ set->proxy_username != NULL && set->proxy_password != NULL) {
+ struct http_auth_credentials auth_creds;
+
+ http_auth_basic_credentials_init(&auth_creds,
+ set->proxy_username, set->proxy_password);
+
+ str_append(rtext, "Proxy-Authorization: ");
+ http_auth_create_credentials(rtext, &auth_creds);
+ str_append(rtext, "\r\n");
+ }
+ if (!req->have_hdr_user_agent && req->client->set.user_agent != NULL) {
+ str_printfa(rtext, "User-Agent: %s\r\n",
+ req->client->set.user_agent);
+ }
+ if (!req->have_hdr_expect && req->payload_sync) {
+ str_append(rtext, "Expect: 100-continue\r\n");
+ }
+ if (req->payload_input != NULL && req->payload_chunked) {
+ // FIXME: can't do this for a HTTP/1.0 server
+ if (!req->have_hdr_body_spec)
+ str_append(rtext, "Transfer-Encoding: chunked\r\n");
+ req->payload_output =
+ http_transfer_chunked_ostream_create(conn->conn.output);
+ o_stream_set_finish_also_parent(req->payload_output, FALSE);
+ } else if (req->payload_input != NULL ||
+ req->payload_empty ||
+ strcasecmp(req->method, "POST") == 0 ||
+ strcasecmp(req->method, "PUT") == 0) {
+
+ /* Send Content-Length if we have specified a payload or when
+ one is normally expected, even if it's 0 bytes. */
+ i_assert(req->payload_input != NULL || req->payload_size == 0);
+ if (!req->have_hdr_body_spec) {
+ str_printfa(rtext, "Content-Length: %"PRIuUOFF_T"\r\n",
+ req->payload_size);
+ }
+ if (req->payload_input != NULL) {
+ req->payload_output = conn->conn.output;
+ o_stream_ref(conn->conn.output);
+ }
+ }
+ if (!req->have_hdr_connection &&
+ !http_client_request_to_proxy(req)) {
+ /* RFC 2068, Section 19.7.1:
+
+ A client MUST NOT send the Keep-Alive connection token to a
+ proxy server as HTTP/1.0 proxy servers do not obey the rules
+ of HTTP/1.1 for parsing the Connection header field.
+ */
+ str_append(rtext, "Connection: Keep-Alive\r\n");
+ }
+
+ /* Request line + implicit headers */
+ iov[0].iov_base = str_data(rtext);
+ iov[0].iov_len = str_len(rtext);
+ /* Explicit headers */
+ if (req->headers != NULL) {
+ iov[1].iov_base = str_data(req->headers);
+ iov[1].iov_len = str_len(req->headers);
+ } else {
+ iov[1].iov_base = "";
+ iov[1].iov_len = 0;
+ }
+ /* End of header */
+ iov[2].iov_base = "\r\n";
+ iov[2].iov_len = 2;
+
+ req->state = HTTP_REQUEST_STATE_PAYLOAD_OUT;
+ req->payload_finished = FALSE;
+
+ req->send_attempts++;
+ if (req->first_sent_time.tv_sec == 0)
+ req->first_sent_time = ioloop_timeval;
+ req->sent_time = ioloop_timeval;
+ req->sent_lock_usecs = file_lock_wait_get_total_usecs();
+ req->sent_global_ioloop_usecs = ioloop_global_wait_usecs;
+ req->sent_http_ioloop_usecs =
+ io_wait_timer_get_usecs(req->conn->io_wait_timer);
+
+ ret = 1;
+ o_stream_cork(conn->conn.output);
+ req->request_offset = conn->conn.output->offset;
+
+ if (o_stream_sendv(conn->conn.output, iov, N_ELEMENTS(iov)) < 0) {
+ http_client_connection_handle_output_error(conn);
+ return -1;
+ }
+
+ e_debug(req->event, "Sent header");
+
+ if (req->payload_output != NULL) {
+ if (!req->payload_sync) {
+ ret = http_client_request_send_more(req, pipelined);
+ if (ret < 0)
+ return -1;
+ } else {
+ e_debug(req->event, "Waiting for 100-continue");
+ conn->output_locked = TRUE;
+ }
+ } else {
+ req->state = HTTP_REQUEST_STATE_WAITING;
+ if (!pipelined)
+ http_client_connection_start_request_timeout(req->conn);
+ conn->output_locked = FALSE;
+ }
+ if (conn->conn.output != NULL) {
+ i_assert(req->request_offset < conn->conn.output->offset);
+ req->bytes_out = conn->conn.output->offset - req->request_offset;
+ if (o_stream_uncork_flush(conn->conn.output) < 0) {
+ http_client_connection_handle_output_error(conn);
+ return -1;
+ }
+ }
+ return ret;
+}
+
+int http_client_request_send(struct http_client_request *req, bool pipelined)
+{
+ int ret;
+
+ T_BEGIN {
+ ret = http_client_request_send_real(req, pipelined);
+ } T_END;
+
+ return ret;
+}
+
+bool http_client_request_callback(struct http_client_request *req,
+ struct http_response *response)
+{
+ http_client_request_callback_t *callback = req->callback;
+ unsigned int orig_attempts = req->attempts;
+
+ req->state = HTTP_REQUEST_STATE_GOT_RESPONSE;
+ req->last_status = response->status;
+
+ req->callback = NULL;
+ if (callback != NULL) {
+ struct http_response response_copy = *response;
+
+ if (req->attempts > 0 && !req->preserve_exact_reason) {
+ unsigned int total_msecs =
+ timeval_diff_msecs(&ioloop_timeval,
+ &req->submit_time);
+ response_copy.reason = t_strdup_printf(
+ "%s (%u retries in %u.%03u secs)",
+ response_copy.reason, req->attempts,
+ total_msecs/1000, total_msecs%1000);
+ }
+
+ callback(&response_copy, req->context);
+ if (req->attempts != orig_attempts) {
+ /* Retrying */
+ req->callback = callback;
+ http_client_request_resubmit(req);
+ return FALSE;
+ } else {
+ /* Release payload early
+ (prevents server/client deadlock in proxy) */
+ i_stream_unref(&req->payload_input);
+ }
+ }
+ return TRUE;
+}
+
+static bool
+http_client_request_send_error(struct http_client_request *req,
+ unsigned int status, const char *error)
+{
+ http_client_request_callback_t *callback;
+ bool sending = (req->state == HTTP_REQUEST_STATE_PAYLOAD_OUT);
+ unsigned int orig_attempts = req->attempts;
+
+ req->state = HTTP_REQUEST_STATE_ABORTED;
+
+ callback = req->callback;
+ req->callback = NULL;
+ if (callback != NULL) {
+ struct http_response response;
+
+ http_response_init(&response, status, error);
+ (void)callback(&response, req->context);
+
+ if (req->attempts != orig_attempts) {
+ /* Retrying */
+ req->callback = callback;
+ http_client_request_resubmit(req);
+ return FALSE;
+ } else {
+ /* Release payload early
+ (prevents server/client deadlock in proxy) */
+ if (!sending && req->payload_input != NULL)
+ i_stream_unref(&req->payload_input);
+ }
+ }
+ if (req->payload_wait) {
+ i_assert(req->client != NULL);
+ io_loop_stop(req->client->ioloop);
+ }
+ return TRUE;
+}
+
+void http_client_request_error_delayed(struct http_client_request **_req)
+{
+ struct http_client_request *req = *_req;
+ const char *error = req->delayed_error;
+ unsigned int status = req->delayed_error_status;
+ bool destroy;
+
+ i_assert(req->state == HTTP_REQUEST_STATE_ABORTED);
+
+ *_req = NULL;
+ req->delayed_error = NULL;
+ req->delayed_error_status = 0;
+
+ i_assert(error != NULL && status != 0);
+ destroy = http_client_request_send_error(req, status, error);
+ if (req->queue != NULL)
+ http_client_queue_drop_request(req->queue, req);
+ if (destroy)
+ http_client_request_destroy(&req);
+}
+
+void http_client_request_error(struct http_client_request **_req,
+ unsigned int status, const char *error)
+{
+ struct http_client_request *req = *_req;
+
+ *_req = NULL;
+
+ i_assert(req->delayed_error_status == 0);
+ i_assert(req->state < HTTP_REQUEST_STATE_FINISHED);
+
+ req->state = HTTP_REQUEST_STATE_ABORTED;
+ req->last_status = status;
+
+ e_debug(http_client_request_result_event(req)->
+ set_name("http_request_finished")->event(),
+ "Error: %u %s", status, error);
+
+ if (req->queue != NULL)
+ http_client_queue_drop_request(req->queue, req);
+
+ if (req->client != NULL &&
+ (!req->submitted ||
+ req->state == HTTP_REQUEST_STATE_GOT_RESPONSE)) {
+ /* We're still in http_client_request_submit() or in the
+ callback during a retry attempt. delay reporting the error,
+ so the caller doesn't have to handle immediate or nested
+ callbacks. */
+ req->delayed_error = p_strdup(req->pool, error);
+ req->delayed_error_status = status;
+ http_client_delay_request_error(req->client, req);
+ } else {
+ if (http_client_request_send_error(req, status, error))
+ http_client_request_destroy(&req);
+ }
+}
+
+void http_client_request_abort(struct http_client_request **_req)
+{
+ struct http_client_request *req = *_req;
+ bool sending;
+
+ if (req == NULL)
+ return;
+
+ sending = (req->state == HTTP_REQUEST_STATE_PAYLOAD_OUT);
+
+ *_req = NULL;
+
+ if (req->state >= HTTP_REQUEST_STATE_FINISHED &&
+ req->delayed_error_status == 0)
+ return;
+
+ req->callback = NULL;
+ req->state = HTTP_REQUEST_STATE_ABORTED;
+ if (req->last_status == 0)
+ req->last_status = HTTP_CLIENT_REQUEST_ERROR_ABORTED;
+
+ if (req->state > HTTP_REQUEST_STATE_NEW &&
+ req->delayed_error_status == 0) {
+ e_debug(http_client_request_result_event(req)->
+ set_name("http_request_finished")->event(),
+ "Aborted");
+ }
+
+ /* Release payload early (prevents server/client deadlock in proxy) */
+ if (!sending && req->payload_input != NULL)
+ i_stream_unref(&req->payload_input);
+
+ if (req->queue != NULL)
+ http_client_queue_drop_request(req->queue, req);
+ if (req->payload_wait) {
+ i_assert(req->client != NULL);
+ i_assert(req->client->ioloop != NULL);
+ io_loop_stop(req->client->ioloop);
+ }
+ http_client_request_destroy(&req);
+}
+
+void http_client_request_finish(struct http_client_request *req)
+{
+ if (req->state >= HTTP_REQUEST_STATE_FINISHED)
+ return;
+
+ i_assert(req->refcount > 0);
+
+ e_debug(http_client_request_result_event(req)->
+ set_name("http_request_finished")->event(),
+ "Finished");
+
+ req->callback = NULL;
+ req->state = HTTP_REQUEST_STATE_FINISHED;
+
+ if (req->queue != NULL)
+ http_client_queue_drop_request(req->queue, req);
+ if (req->payload_wait) {
+ i_assert(req->client != NULL);
+ i_assert(req->client->ioloop != NULL);
+ io_loop_stop(req->client->ioloop);
+ }
+ http_client_request_unref(&req);
+}
+
+static int
+http_client_request_reset(struct http_client_request *req, bool rewind,
+ const char **error_r)
+{
+ /* Rewind payload stream */
+ if (rewind && req->payload_input != NULL && req->payload_size > 0) {
+ if (req->payload_input->v_offset != req->payload_offset &&
+ !req->payload_input->seekable) {
+ *error_r = "Cannot resend payload; "
+ "stream is not seekable";
+ return -1;
+ }
+ i_stream_seek(req->payload_input, req->payload_offset);
+ }
+
+ /* Drop payload output stream from previous attempt */
+ o_stream_unref(&req->payload_output);
+
+ /* Reset payload state */
+ req->payload_finished = FALSE;
+
+ return 0;
+}
+
+void http_client_request_redirect(struct http_client_request *req,
+ unsigned int status, const char *location)
+{
+ struct http_url *url;
+ const char *error, *target, *origin_url;
+
+ i_assert(req->client != NULL);
+ i_assert(!req->payload_wait);
+
+ req->last_status = status;
+
+ /* parse URL */
+ if (http_url_parse(location, NULL, 0,
+ pool_datastack_create(), &url, &error) < 0) {
+ http_client_request_error(
+ &req, HTTP_CLIENT_REQUEST_ERROR_INVALID_REDIRECT,
+ t_strdup_printf("Invalid redirect location: %s",
+ error));
+ return;
+ }
+
+ i_assert(req->redirects <= req->client->set.max_redirects);
+ if (++req->redirects > req->client->set.max_redirects) {
+ if (req->client->set.max_redirects > 0) {
+ http_client_request_error(
+ &req,
+ HTTP_CLIENT_REQUEST_ERROR_INVALID_REDIRECT,
+ t_strdup_printf(
+ "Redirected more than %d times",
+ req->client->set.max_redirects));
+ } else {
+ http_client_request_error(
+ &req,
+ HTTP_CLIENT_REQUEST_ERROR_INVALID_REDIRECT,
+ "Redirect refused");
+ }
+ return;
+ }
+
+ if (http_client_request_reset(req, (status != 303), &error) < 0) {
+ http_client_request_error(
+ &req, HTTP_CLIENT_REQUEST_ERROR_ABORTED,
+ t_strdup_printf("Redirect failed: %s", error));
+ return;
+ }
+
+ target = http_url_create_target(url);
+
+ http_url_copy(req->pool, &req->origin_url, url);
+ req->target = p_strdup(req->pool, target);
+
+ req->host = NULL;
+
+ origin_url = http_url_create(&req->origin_url);
+
+ e_debug(http_client_request_result_event(req)->
+ set_name("http_request_redirected")->event(),
+ "Redirecting to %s%s (redirects=%u)",
+ origin_url, target, req->redirects);
+
+ req->label = p_strdup_printf(req->pool, "[%s %s%s]",
+ req->method, origin_url, req->target);
+
+ /* RFC 7231, Section 6.4.4:
+
+ -> A 303 `See Other' redirect status response is handled a bit
+ differently. Basically, the response content is located elsewhere,
+ but the original (POST) request is handled already.
+ */
+ if (status == 303 && strcasecmp(req->method, "HEAD") != 0 &&
+ strcasecmp(req->method, "GET") != 0) {
+ // FIXME: should we provide the means to skip this step? The
+ // original request was already handled at this point.
+ req->method = p_strdup(req->pool, "GET");
+
+ /* drop payload */
+ i_stream_unref(&req->payload_input);
+ req->payload_size = 0;
+ req->payload_offset = 0;
+ }
+
+ /* Resubmit */
+ req->state = HTTP_REQUEST_STATE_NEW;
+ http_client_request_do_submit(req);
+}
+
+void http_client_request_resubmit(struct http_client_request *req)
+{
+ const char *error;
+
+ i_assert(!req->payload_wait);
+
+ e_debug(req->event, "Resubmitting request");
+
+ if (http_client_request_reset(req, TRUE, &error) < 0) {
+ http_client_request_error(
+ &req, HTTP_CLIENT_REQUEST_ERROR_ABORTED,
+ t_strdup_printf("Resubmission failed: %s", error));
+ return;
+ }
+
+ req->peer = NULL;
+ req->state = HTTP_REQUEST_STATE_QUEUED;
+ req->redirects = 0;
+ req->last_status = 0;
+ http_client_host_submit_request(req->host, req);
+}
+
+void http_client_request_retry(struct http_client_request *req,
+ unsigned int status, const char *error)
+{
+ if (req->client == NULL || req->client->set.no_auto_retry ||
+ !http_client_request_try_retry(req))
+ http_client_request_error(&req, status, error);
+}
+
+bool http_client_request_try_retry(struct http_client_request *req)
+{
+ /* Don't ever retry if we're sending data in small blocks via
+ http_client_request_send_payload() and we're not waiting for a
+ 100 continue (there's no way to rewind the payload for a retry)
+ */
+ if (req->payload_wait &&
+ (!req->payload_sync || req->payload_sync_continue))
+ return FALSE;
+ /* Limit the number of attempts for each request */
+ if (req->attempts+1 >= req->max_attempts)
+ return FALSE;
+ req->attempts++;
+
+ e_debug(http_client_request_result_event(req)->
+ set_name("http_request_retried")->event(),
+ "Retrying (attempts=%d)", req->attempts);
+
+ if (req->callback != NULL)
+ http_client_request_resubmit(req);
+ return TRUE;
+}
+
+#undef http_client_request_set_destroy_callback
+void http_client_request_set_destroy_callback(struct http_client_request *req,
+ void (*callback)(void *),
+ void *context)
+{
+ req->destroy_callback = callback;
+ req->destroy_context = context;
+}
+
+void http_client_request_start_tunnel(struct http_client_request *req,
+ struct http_client_tunnel *tunnel)
+{
+ struct http_client_connection *conn = req->conn;
+
+ i_assert(req->state == HTTP_REQUEST_STATE_GOT_RESPONSE);
+
+ http_client_connection_start_tunnel(&conn, tunnel);
+}
diff --git a/src/lib-http/http-client.c b/src/lib-http/http-client.c
new file mode 100644
index 0000000..5e71b63
--- /dev/null
+++ b/src/lib-http/http-client.c
@@ -0,0 +1,740 @@
+/* Copyright (c) 2013-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "net.h"
+#include "str.h"
+#include "hash.h"
+#include "llist.h"
+#include "array.h"
+#include "ioloop.h"
+#include "istream.h"
+#include "ostream.h"
+#include "connection.h"
+#include "dns-lookup.h"
+#include "iostream-rawlog.h"
+#include "iostream-ssl.h"
+#include "http-url.h"
+
+#include "http-client-private.h"
+
+/* Structure:
+
+ http_client_context:
+
+ Shared context between multiple independent HTTP clients. This allows host
+ name lookup data, peer status and idle connections to be shared between
+ clients.
+
+ http_client:
+
+ Acts much like a browser; it is not dedicated to a single host. Client can
+ accept requests to different hosts, which can be served at different IPs.
+ Redirects are handled in the background by making a new connection.
+ Connections to new hosts are created once needed for servicing a request.
+
+ http_client_request:
+
+ The request semantics are similar to imapc commands. Create a request,
+ optionally modify some aspects of it, and finally submit it. Once finished,
+ a callback is called with the returned response.
+
+ http_client_host_shared:
+
+ We maintain a 'cache' of hosts for which we have looked up IPs. This cache
+ is maintained in client context, so multiple clients can share it. One host
+ can have multiple IPs.
+
+ http_client_host:
+
+ A host object maintains client-specific information for a host. The queues
+ that the client has for this host are listed here. For one host, there is a
+ separate queue for each used server port.
+
+ http_client_queue:
+
+ Requests are queued in a queue object. These queues are maintained for each
+ host:port target and listed in the host object. The queue object is
+ responsible for starting connection attempts to TCP port at the various IPs
+ known for the host.
+
+ http_client_peer_pool:
+
+ A peer pool lists all unused and pending connections to a peer, grouped by
+ a compatible configuration, e.g. in terms of SSL and rawlog. Once needed,
+ peers can claim/request an existing/new connection from the pool.
+
+ http_client_peer_shared:
+
+ The shared peer object records state information about a peer, which is a
+ service access point (ip:port or unix socket path). The peer object also
+ maintains lists of idle and pending connections to this service, which are
+ grouped in pools with compatible client configuration. Each client has a
+ separate (non-shared) peer object for client-specific state information.
+
+ http_client_peer:
+
+ A peer object maintains client-specific information for a peer. Claimed
+ connections are dedicated to one peer (and therefore one client).
+
+ http-client-connection:
+
+ This is an actual connection to a server. Once a connection is ready to
+ handle requests, it claims a request from a queue object. One connection can
+ service multiple hosts and one host can have multiple associated connections,
+ possibly to different ips and ports.
+
+ */
+
+static struct event_category event_category_http_client = {
+ .name = "http-client"
+};
+
+static struct http_client_context *http_client_global_context = NULL;
+
+static void
+http_client_context_add_client(struct http_client_context *cctx,
+ struct http_client *client);
+static void
+http_client_context_remove_client(struct http_client_context *cctx,
+ struct http_client *client);
+
+/*
+ * Client
+ */
+
+struct http_client *
+http_client_init_shared(struct http_client_context *cctx,
+ const struct http_client_settings *set)
+{
+ static unsigned int id = 0;
+ struct http_client *client;
+ const char *log_prefix;
+ pool_t pool;
+ size_t pool_size;
+
+ pool_size = (set != NULL && set->ssl != NULL) ? 8192 : 1024; /* certs will be >4K */
+ pool = pool_alloconly_create("http client", pool_size);
+ client = p_new(pool, struct http_client, 1);
+ client->pool = pool;
+ client->ioloop = current_ioloop;
+
+ /* create private context if none is provided */
+ id++;
+ if (cctx != NULL) {
+ client->cctx = cctx;
+ http_client_context_ref(cctx);
+ log_prefix = t_strdup_printf("http-client[%u]: ", id);
+ } else {
+ i_assert(set != NULL);
+ client->cctx = cctx = http_client_context_create(set);
+ log_prefix = "http-client: ";
+ }
+
+ struct event *parent_event;
+ if (set != NULL && set->event_parent != NULL)
+ parent_event = set->event_parent;
+ else if (cctx->event == NULL)
+ parent_event = NULL;
+ else {
+ /* FIXME: we could use cctx->event, but it already has a log
+ prefix that we don't want.. should we update event API to
+ support replacing parent's log prefix? */
+ parent_event = event_get_parent(cctx->event);
+ }
+ client->event = event_create(parent_event);
+ event_add_category(client->event, &event_category_http_client);
+ event_set_forced_debug(client->event,
+ (set != NULL && set->debug) || (cctx != NULL && cctx->set.debug));
+ event_set_append_log_prefix(client->event, log_prefix);
+
+ /* merge provided settings with context defaults */
+ client->set = cctx->set;
+ if (set != NULL) {
+ client->set.dns_client = set->dns_client;
+ client->set.dns_client_socket_path =
+ p_strdup_empty(pool, set->dns_client_socket_path);
+ client->set.dns_ttl_msecs = set->dns_ttl_msecs;
+
+ if (set->user_agent != NULL && *set->user_agent != '\0')
+ client->set.user_agent = p_strdup_empty(pool, set->user_agent);
+ if (set->rawlog_dir != NULL && *set->rawlog_dir != '\0')
+ client->set.rawlog_dir = p_strdup_empty(pool, set->rawlog_dir);
+
+ if (set->ssl != NULL)
+ client->set.ssl = ssl_iostream_settings_dup(pool, set->ssl);
+
+ if (set->proxy_socket_path != NULL && *set->proxy_socket_path != '\0') {
+ client->set.proxy_socket_path = p_strdup(pool, set->proxy_socket_path);
+ client->set.proxy_url = NULL;
+ } else if (set->proxy_url != NULL) {
+ client->set.proxy_url = http_url_clone(pool, set->proxy_url);
+ client->set.proxy_socket_path = NULL;
+ }
+ if (set->proxy_username != NULL && *set->proxy_username != '\0') {
+ client->set.proxy_username = p_strdup_empty(pool, set->proxy_username);
+ client->set.proxy_password = p_strdup(pool, set->proxy_password);
+ } else if (set->proxy_url != NULL && set->proxy_url->user != NULL &&
+ *set->proxy_url->user != '\0') {
+ client->set.proxy_username =
+ p_strdup_empty(pool, set->proxy_url->user);
+ client->set.proxy_password =
+ p_strdup(pool, set->proxy_url->password);
+ }
+
+ if (set->max_idle_time_msecs > 0)
+ client->set.max_idle_time_msecs = set->max_idle_time_msecs;
+ if (set->max_parallel_connections > 0)
+ client->set.max_parallel_connections = set->max_parallel_connections;
+ if (set->max_pipelined_requests > 0)
+ client->set.max_pipelined_requests = set->max_pipelined_requests;
+ if (set->max_attempts > 0)
+ client->set.max_attempts = set->max_attempts;
+ if (set->max_connect_attempts > 0)
+ client->set.max_connect_attempts = set->max_connect_attempts;
+ if (set->connect_backoff_time_msecs > 0) {
+ client->set.connect_backoff_time_msecs =
+ set->connect_backoff_time_msecs;
+ }
+ if (set->connect_backoff_max_time_msecs > 0) {
+ client->set.connect_backoff_max_time_msecs =
+ set->connect_backoff_max_time_msecs;
+ }
+ client->set.no_auto_redirect =
+ client->set.no_auto_redirect || set->no_auto_redirect;
+ client->set.no_auto_retry =
+ client->set.no_auto_retry || set->no_auto_retry;
+ client->set.no_ssl_tunnel =
+ client->set.no_ssl_tunnel || set->no_ssl_tunnel;
+ if (set->max_redirects > 0)
+ client->set.max_redirects = set->max_redirects;
+ if (set->request_absolute_timeout_msecs > 0) {
+ client->set.request_absolute_timeout_msecs =
+ set->request_absolute_timeout_msecs;
+ }
+ if (set->request_timeout_msecs > 0)
+ client->set.request_timeout_msecs = set->request_timeout_msecs;
+ if (set->connect_timeout_msecs > 0)
+ client->set.connect_timeout_msecs = set->connect_timeout_msecs;
+ if (set->soft_connect_timeout_msecs > 0)
+ client->set.soft_connect_timeout_msecs = set->soft_connect_timeout_msecs;
+ if (set->socket_send_buffer_size > 0)
+ client->set.socket_send_buffer_size = set->socket_send_buffer_size;
+ if (set->socket_recv_buffer_size > 0)
+ client->set.socket_recv_buffer_size = set->socket_recv_buffer_size;
+ if (set->max_auto_retry_delay_secs > 0)
+ client->set.max_auto_retry_delay_secs = set->max_auto_retry_delay_secs;
+ client->set.debug = client->set.debug || set->debug;
+ }
+
+ i_array_init(&client->delayed_failing_requests, 1);
+
+ http_client_context_add_client(cctx, client);
+
+ return client;
+}
+
+struct http_client *
+http_client_init(const struct http_client_settings *set)
+{
+ return http_client_init_shared(http_client_get_global_context(), set);
+}
+
+struct http_client *
+http_client_init_private(const struct http_client_settings *set)
+{
+ return http_client_init_shared(NULL, set);
+}
+
+void http_client_deinit(struct http_client **_client)
+{
+ struct http_client *client = *_client;
+ struct http_client_request *req;
+ struct http_client_host *host;
+ struct http_client_peer *peer;
+
+ *_client = NULL;
+
+ /* destroy requests without calling callbacks */
+ req = client->requests_list;
+ while (req != NULL) {
+ struct http_client_request *next_req = req->next;
+ http_client_request_destroy(&req);
+ req = next_req;
+ }
+ i_assert(client->requests_count == 0);
+
+ /* free peers */
+ while (client->peers_list != NULL) {
+ peer = client->peers_list;
+ http_client_peer_close(&peer);
+ }
+
+ /* free hosts */
+ while (client->hosts_list != NULL) {
+ host = client->hosts_list;
+ http_client_host_free(&host);
+ }
+
+ array_free(&client->delayed_failing_requests);
+ timeout_remove(&client->to_failing_requests);
+
+ if (client->ssl_ctx != NULL)
+ ssl_iostream_context_unref(&client->ssl_ctx);
+ http_client_context_remove_client(client->cctx, client);
+ http_client_context_unref(&client->cctx);
+ event_unref(&client->event);
+ pool_unref(&client->pool);
+}
+
+static void http_client_do_switch_ioloop(struct http_client *client)
+{
+ struct http_client_peer *peer;
+ struct http_client_host *host;
+
+ /* move peers */
+ for (peer = client->peers_list; peer != NULL;
+ peer = peer->client_next)
+ http_client_peer_switch_ioloop(peer);
+
+ /* move hosts/queues */
+ for (host = client->hosts_list; host != NULL;
+ host = host->client_next)
+ http_client_host_switch_ioloop(host);
+
+ /* move timeouts */
+ if (client->to_failing_requests != NULL) {
+ client->to_failing_requests =
+ io_loop_move_timeout(&client->to_failing_requests);
+ }
+}
+
+struct ioloop *http_client_switch_ioloop(struct http_client *client)
+{
+ struct ioloop *prev_ioloop = client->ioloop;
+
+ client->ioloop = current_ioloop;
+
+ http_client_do_switch_ioloop(client);
+ http_client_context_switch_ioloop(client->cctx);
+
+ return prev_ioloop;
+}
+
+void http_client_wait(struct http_client *client)
+{
+ struct ioloop *prev_ioloop, *client_ioloop, *prev_client_ioloop;
+
+ if (client->requests_count == 0)
+ return;
+
+ prev_ioloop = current_ioloop;
+ client_ioloop = io_loop_create();
+ prev_client_ioloop = http_client_switch_ioloop(client);
+ if (client->set.dns_client != NULL)
+ dns_client_switch_ioloop(client->set.dns_client);
+ /* either we're waiting for network I/O or we're getting out of a
+ callback using timeout_add_short(0) */
+ i_assert(io_loop_have_ios(client_ioloop) ||
+ io_loop_have_immediate_timeouts(client_ioloop));
+
+ client->waiting = TRUE;
+ do {
+ e_debug(client->event,
+ "Waiting for %d requests to finish", client->requests_count);
+ io_loop_run(client_ioloop);
+ } while (client->requests_count > 0);
+ client->waiting = FALSE;
+
+ e_debug(client->event, "All requests finished");
+
+ if (prev_client_ioloop != NULL)
+ io_loop_set_current(prev_client_ioloop);
+ else
+ io_loop_set_current(prev_ioloop);
+ (void)http_client_switch_ioloop(client);
+ if (client->set.dns_client != NULL)
+ dns_client_switch_ioloop(client->set.dns_client);
+ io_loop_set_current(client_ioloop);
+ io_loop_destroy(&client_ioloop);
+}
+
+unsigned int http_client_get_pending_request_count(struct http_client *client)
+{
+ return client->requests_count;
+}
+
+int http_client_init_ssl_ctx(struct http_client *client, const char **error_r)
+{
+ const char *error;
+
+ if (client->ssl_ctx != NULL)
+ return 0;
+
+ if (client->set.ssl == NULL) {
+ *error_r = "Requested https connection, but no SSL settings given";
+ return -1;
+ }
+ if (ssl_iostream_client_context_cache_get(client->set.ssl, &client->ssl_ctx, &error) < 0) {
+ *error_r = t_strdup_printf("Couldn't initialize SSL context: %s",
+ error);
+ return -1;
+ }
+ return 0;
+}
+
+/*
+ * Delayed request errors
+ */
+
+static void
+http_client_handle_request_errors(struct http_client *client)
+{
+ struct http_client_request *req;
+
+ timeout_remove(&client->to_failing_requests);
+
+ array_foreach_elem(&client->delayed_failing_requests, req) {
+ i_assert(req->refcount == 1);
+ http_client_request_error_delayed(&req);
+ }
+ array_clear(&client->delayed_failing_requests);
+}
+
+void http_client_delay_request_error(struct http_client *client,
+ struct http_client_request *req)
+{
+ if (client->to_failing_requests == NULL) {
+ client->to_failing_requests =
+ timeout_add_short_to(client->ioloop, 0,
+ http_client_handle_request_errors, client);
+ }
+ array_push_back(&client->delayed_failing_requests, &req);
+}
+
+void http_client_remove_request_error(struct http_client *client,
+ struct http_client_request *req)
+{
+ struct http_client_request *const *reqs;
+ unsigned int i, count;
+
+ reqs = array_get(&client->delayed_failing_requests, &count);
+ for (i = 0; i < count; i++) {
+ if (reqs[i] == req) {
+ array_delete(&client->delayed_failing_requests, i, 1);
+ return;
+ }
+ }
+}
+
+/*
+ * Client shared context
+ */
+
+struct http_client_context *
+http_client_context_create(const struct http_client_settings *set)
+{
+ struct http_client_context *cctx;
+ pool_t pool;
+ size_t pool_size;
+
+ pool_size = (set->ssl != NULL) ? 8192 : 1024; /* certs will be >4K */
+ pool = pool_alloconly_create("http client context", pool_size);
+ cctx = p_new(pool, struct http_client_context, 1);
+ cctx->pool = pool;
+ cctx->refcount = 1;
+ cctx->ioloop = current_ioloop;
+
+ cctx->event = event_create(set->event_parent);
+ event_add_category(cctx->event, &event_category_http_client);
+ event_set_forced_debug(cctx->event, set->debug);
+ event_set_append_log_prefix(cctx->event, "http-client: ");
+
+ cctx->set.dns_client = set->dns_client;
+ cctx->set.dns_client_socket_path =
+ p_strdup_empty(pool, set->dns_client_socket_path);
+ cctx->set.dns_ttl_msecs = (set->dns_ttl_msecs == 0 ?
+ HTTP_CLIENT_DEFAULT_DNS_TTL_MSECS : set->dns_ttl_msecs);
+ cctx->set.user_agent = p_strdup_empty(pool, set->user_agent);
+ cctx->set.rawlog_dir = p_strdup_empty(pool, set->rawlog_dir);
+
+ if (set->ssl != NULL)
+ cctx->set.ssl = ssl_iostream_settings_dup(pool, set->ssl);
+
+ if (set->proxy_socket_path != NULL && *set->proxy_socket_path != '\0') {
+ cctx->set.proxy_socket_path = p_strdup(pool, set->proxy_socket_path);
+ } else if (set->proxy_url != NULL) {
+ cctx->set.proxy_url = http_url_clone(pool, set->proxy_url);
+ }
+ if (set->proxy_username != NULL && *set->proxy_username != '\0') {
+ cctx->set.proxy_username = p_strdup_empty(pool, set->proxy_username);
+ cctx->set.proxy_password = p_strdup(pool, set->proxy_password);
+ } else if (set->proxy_url != NULL) {
+ cctx->set.proxy_username =
+ p_strdup_empty(pool, set->proxy_url->user);
+ cctx->set.proxy_password =
+ p_strdup(pool, set->proxy_url->password);
+ }
+
+ cctx->set.max_idle_time_msecs = set->max_idle_time_msecs;
+ cctx->set.max_pipelined_requests =
+ (set->max_pipelined_requests > 0 ? set->max_pipelined_requests : 1);
+ cctx->set.max_parallel_connections =
+ (set->max_parallel_connections > 0 ? set->max_parallel_connections : 1);
+ cctx->set.max_attempts = set->max_attempts;
+ cctx->set.max_connect_attempts = set->max_connect_attempts;
+ cctx->set.connect_backoff_time_msecs =
+ set->connect_backoff_time_msecs == 0 ?
+ HTTP_CLIENT_DEFAULT_BACKOFF_TIME_MSECS :
+ set->connect_backoff_time_msecs;
+ cctx->set.connect_backoff_max_time_msecs =
+ set->connect_backoff_max_time_msecs == 0 ?
+ HTTP_CLIENT_DEFAULT_BACKOFF_MAX_TIME_MSECS :
+ set->connect_backoff_max_time_msecs;
+ cctx->set.no_auto_redirect = set->no_auto_redirect;
+ cctx->set.no_auto_retry = set->no_auto_retry;
+ cctx->set.no_ssl_tunnel = set->no_ssl_tunnel;
+ cctx->set.max_redirects = set->max_redirects;
+ cctx->set.response_hdr_limits = set->response_hdr_limits;
+ cctx->set.request_absolute_timeout_msecs =
+ set->request_absolute_timeout_msecs;
+ cctx->set.request_timeout_msecs =
+ set->request_timeout_msecs == 0 ?
+ HTTP_CLIENT_DEFAULT_REQUEST_TIMEOUT_MSECS :
+ set->request_timeout_msecs;
+ cctx->set.connect_timeout_msecs = set->connect_timeout_msecs;
+ cctx->set.soft_connect_timeout_msecs = set->soft_connect_timeout_msecs;
+ cctx->set.max_auto_retry_delay_secs = set->max_auto_retry_delay_secs;
+ cctx->set.socket_send_buffer_size = set->socket_send_buffer_size;
+ cctx->set.socket_recv_buffer_size = set->socket_recv_buffer_size;
+ cctx->set.debug = set->debug;
+
+ cctx->conn_list = http_client_connection_list_init();
+
+ hash_table_create(&cctx->hosts, default_pool, 0, str_hash, strcmp);
+
+ hash_table_create(&cctx->peers, default_pool, 0,
+ http_client_peer_addr_hash, http_client_peer_addr_cmp);
+
+ return cctx;
+}
+
+void http_client_context_ref(struct http_client_context *cctx)
+{
+ cctx->refcount++;
+}
+
+void http_client_context_unref(struct http_client_context **_cctx)
+{
+ struct http_client_context *cctx = *_cctx;
+ struct http_client_peer_shared *peer;
+ struct http_client_host_shared *hshared;
+
+ *_cctx = NULL;
+
+ i_assert(cctx->refcount > 0);
+ if (--cctx->refcount > 0)
+ return;
+
+ /* free hosts */
+ while (cctx->hosts_list != NULL) {
+ hshared = cctx->hosts_list;
+ http_client_host_shared_free(&hshared);
+ }
+ hash_table_destroy(&cctx->hosts);
+
+ /* close all idle connections */
+ while (cctx->peers_list != NULL) {
+ peer = cctx->peers_list;
+ http_client_peer_shared_close(&peer);
+ i_assert(peer == NULL);
+ }
+ hash_table_destroy(&cctx->peers);
+
+ connection_list_deinit(&cctx->conn_list);
+
+ event_unref(&cctx->event);
+ pool_unref(&cctx->pool);
+}
+
+static unsigned int
+http_client_get_dns_lookup_timeout_msecs(const struct http_client_settings *set)
+{
+ if (set->connect_timeout_msecs > 0)
+ return set->connect_timeout_msecs;
+ if (set->request_timeout_msecs > 0)
+ return set->request_timeout_msecs;
+ return HTTP_CLIENT_DEFAULT_DNS_LOOKUP_TIMEOUT_MSECS;
+}
+
+static void
+http_client_context_update_settings(struct http_client_context *cctx)
+{
+ struct http_client *client;
+ bool debug;
+
+ /* revert back to context settings */
+ cctx->dns_client = cctx->set.dns_client;
+ cctx->dns_client_socket_path = cctx->set.dns_client_socket_path;
+ cctx->dns_ttl_msecs = cctx->set.dns_ttl_msecs;
+ cctx->dns_lookup_timeout_msecs =
+ http_client_get_dns_lookup_timeout_msecs(&cctx->set);
+ debug = cctx->set.debug;
+
+ i_assert(cctx->dns_ttl_msecs > 0);
+ i_assert(cctx->dns_lookup_timeout_msecs > 0);
+
+ /* override with available client settings */
+ for (client = cctx->clients_list; client != NULL;
+ client = client->next) {
+ unsigned dns_lookup_timeout_msecs =
+ http_client_get_dns_lookup_timeout_msecs(&client->set);
+
+ if (cctx->dns_client == NULL)
+ cctx->dns_client = client->set.dns_client;
+ if (cctx->dns_client_socket_path == NULL) {
+ cctx->dns_client_socket_path =
+ client->set.dns_client_socket_path;
+ }
+ if (client->set.dns_ttl_msecs != 0 &&
+ cctx->dns_ttl_msecs > client->set.dns_ttl_msecs)
+ cctx->dns_ttl_msecs = client->set.dns_ttl_msecs;
+ if (dns_lookup_timeout_msecs != 0 &&
+ cctx->dns_lookup_timeout_msecs > dns_lookup_timeout_msecs) {
+ cctx->dns_lookup_timeout_msecs =
+ dns_lookup_timeout_msecs;
+ }
+ debug = debug || client->set.debug;
+ }
+
+ event_set_forced_debug(cctx->event, debug);
+}
+
+static void
+http_client_context_add_client(struct http_client_context *cctx,
+ struct http_client *client)
+{
+ DLLIST_PREPEND(&cctx->clients_list, client);
+ http_client_context_update_settings(cctx);
+}
+
+static void
+http_client_context_remove_client(struct http_client_context *cctx,
+ struct http_client *client)
+{
+ DLLIST_REMOVE(&cctx->clients_list, client);
+ http_client_context_update_settings(cctx);
+
+ if (cctx->ioloop != current_ioloop &&
+ cctx->ioloop == client->ioloop &&
+ cctx->clients_list != NULL) {
+ struct ioloop *prev_ioloop = current_ioloop;
+
+ io_loop_set_current(cctx->clients_list->ioloop);
+ http_client_context_switch_ioloop(cctx);
+ io_loop_set_current(prev_ioloop);
+ }
+}
+
+static void http_client_context_close(struct http_client_context *cctx)
+{
+ struct connection *_conn, *_conn_next;
+ struct http_client_host_shared *hshared;
+ struct http_client_peer_shared *pshared;
+
+ /* Switching to NULL ioloop;
+ close all hosts, peers, and connections */
+ i_assert(cctx->clients_list == NULL);
+
+ _conn = cctx->conn_list->connections;
+ while (_conn != NULL) {
+ struct http_client_connection *conn =
+ (struct http_client_connection *)_conn;
+ _conn_next = _conn->next;
+ http_client_connection_close(&conn);
+ _conn = _conn_next;
+ }
+ while (cctx->hosts_list != NULL) {
+ hshared = cctx->hosts_list;
+ http_client_host_shared_free(&hshared);
+ }
+ while (cctx->peers_list != NULL) {
+ pshared = cctx->peers_list;
+ http_client_peer_shared_close(&pshared);
+ }
+}
+
+static void
+http_client_context_do_switch_ioloop(struct http_client_context *cctx)
+{
+ struct connection *_conn = cctx->conn_list->connections;
+ struct http_client_host_shared *hshared;
+ struct http_client_peer_shared *pshared;
+
+ /* move connections */
+ /* FIXME: we wouldn't necessarily need to switch all of them
+ immediately, only those that have requests now. but also connections
+ that get new requests before ioloop is switched again.. */
+ for (; _conn != NULL; _conn = _conn->next) {
+ struct http_client_connection *conn =
+ (struct http_client_connection *)_conn;
+
+ http_client_connection_switch_ioloop(conn);
+ }
+
+ /* move backoff timeouts */
+ for (pshared = cctx->peers_list; pshared != NULL;
+ pshared = pshared->next)
+ http_client_peer_shared_switch_ioloop(pshared);
+
+ /* move dns lookups and delayed requests */
+ for (hshared = cctx->hosts_list; hshared != NULL;
+ hshared = hshared->next)
+ http_client_host_shared_switch_ioloop(hshared);
+}
+
+void http_client_context_switch_ioloop(struct http_client_context *cctx)
+{
+ cctx->ioloop = current_ioloop;
+
+ http_client_context_do_switch_ioloop(cctx);
+}
+
+static void
+http_client_global_context_ioloop_switched(
+ struct ioloop *prev_ioloop ATTR_UNUSED)
+{
+ struct http_client_context *cctx = http_client_global_context;
+
+ i_assert(cctx != NULL);
+ if (current_ioloop == NULL) {
+ http_client_context_close(cctx);
+ return;
+ }
+ if (cctx->clients_list == NULL) {
+ /* follow the current ioloop if there is no client */
+ http_client_context_switch_ioloop(cctx);
+ }
+}
+
+static void http_client_global_context_free(void)
+{
+ /* drop ioloop switch callback to make absolutely sure there is no
+ recursion. */
+ io_loop_remove_switch_callback(http_client_global_context_ioloop_switched);
+
+ http_client_context_unref(&http_client_global_context);
+}
+
+struct http_client_context *http_client_get_global_context(void)
+{
+ if (http_client_global_context != NULL)
+ return http_client_global_context;
+
+ struct http_client_settings set;
+ i_zero(&set);
+ http_client_global_context = http_client_context_create(&set);
+ /* keep this a bit higher than lib-ssl-iostream */
+ lib_atexit_priority(http_client_global_context_free, LIB_ATEXIT_PRIORITY_LOW-1);
+ io_loop_add_switch_callback(http_client_global_context_ioloop_switched);
+ return http_client_global_context;
+}
diff --git a/src/lib-http/http-client.h b/src/lib-http/http-client.h
new file mode 100644
index 0000000..4f04222
--- /dev/null
+++ b/src/lib-http/http-client.h
@@ -0,0 +1,496 @@
+#ifndef HTTP_CLIENT_H
+#define HTTP_CLIENT_H
+
+#include "net.h"
+
+#include "http-common.h"
+#include "http-response.h"
+
+struct timeval;
+struct http_response;
+
+struct http_client_request;
+struct http_client;
+struct http_client_context;
+
+struct ssl_iostream_settings;
+
+/*
+ * Client settings
+ */
+
+struct http_client_settings {
+ /* a) If dns_client is set, all lookups are done via it.
+ b) If dns_client_socket_path is set, each DNS lookup does its own
+ dns-lookup UNIX socket connection.
+ c) Otherwise, blocking gethostbyname() lookups are used. */
+ struct dns_client *dns_client;
+ const char *dns_client_socket_path;
+ /* How long to cache DNS records internally
+ (default = HTTP_CLIENT_DEFAULT_DNS_TTL_MSECS) */
+ unsigned int dns_ttl_msecs;
+
+ const struct ssl_iostream_settings *ssl;
+
+ /* User-Agent: header (default: none) */
+ const char *user_agent;
+
+ /* proxy on unix socket */
+ const char *proxy_socket_path;
+ /* URL for normal proxy (ignored if proxy_socket_path is set) */
+ const struct http_url *proxy_url;
+ /* credentials for proxy */
+ const char *proxy_username;
+ const char *proxy_password;
+
+ /* directory for writing raw log data for debugging purposes */
+ const char *rawlog_dir;
+
+ /* maximum time a connection will idle. if parallel connections are idle,
+ the duplicates will end earlier based on how many idle connections exist
+ to that same service */
+ unsigned int max_idle_time_msecs;
+
+ /* maximum number of parallel connections per peer (default = 1) */
+ unsigned int max_parallel_connections;
+
+ /* maximum number of pipelined requests per connection (default = 1) */
+ unsigned int max_pipelined_requests;
+
+ /* don't automatically act upon redirect responses */
+ bool no_auto_redirect;
+
+ /* never automatically retry requests */
+ bool no_auto_retry;
+
+ /* if we use a proxy, delegate SSL negotiation to proxy, rather than
+ creating a CONNECT tunnel through the proxy for the SSL link */
+ bool no_ssl_tunnel;
+
+ /* maximum number of redirects for a request
+ (default = 0; redirects refused)
+ */
+ unsigned int max_redirects;
+
+ /* maximum number of attempts for a request */
+ unsigned int max_attempts;
+
+ /* maximum number of connection attempts to a host before all associated
+ requests fail.
+
+ if > 1, the maximum will be enforced across all IPs for that host,
+ meaning that IPs may be tried more than once eventually if the number
+ of IPs is smaller than the specified maximum attempts. If the number of IPs
+ is higher than the maximum attempts, not all IPs are tried. If <= 1, all
+ IPs are tried at most once.
+ */
+ unsigned int max_connect_attempts;
+
+ /* Initial backoff time; doubled at each connection failure
+ (default = HTTP_CLIENT_DEFAULT_BACKOFF_TIME_MSECS) */
+ unsigned int connect_backoff_time_msecs;
+ /* Maximum backoff time
+ (default = HTTP_CLIENT_DEFAULT_BACKOFF_MAX_TIME_MSECS) */
+ unsigned int connect_backoff_max_time_msecs;
+
+ /* response header limits */
+ struct http_header_limits response_hdr_limits;
+
+ /* max total time to wait for HTTP request to finish
+ this can be overridden/reset for individual requests using
+ http_client_request_set_timeout() and friends.
+ (default is no timeout)
+ */
+ unsigned int request_absolute_timeout_msecs;
+ /* max time to wait for HTTP request to finish before retrying
+ (default = HTTP_CLIENT_DEFAULT_REQUEST_TIMEOUT_MSECS) */
+ unsigned int request_timeout_msecs;
+ /* max time to wait for connect() (and SSL handshake) to finish before
+ retrying (default = request_timeout_msecs) */
+ unsigned int connect_timeout_msecs;
+ /* time to wait for connect() (and SSL handshake) to finish for the first
+ connection before trying the next IP in parallel
+ (default = 0; wait until current connection attempt finishes) */
+ unsigned int soft_connect_timeout_msecs;
+
+ /* maximum acceptable delay in seconds for automatically
+ retrying/redirecting requests. if a server sends a response with a
+ Retry-After header that causes a delay longer than this, the request
+ is not automatically retried and the response is returned */
+ unsigned int max_auto_retry_delay_secs;
+
+ /* the kernel send/receive buffer sizes used for the connection sockets.
+ Configuring this is mainly useful for the test suite. The kernel
+ defaults are used when these settings are 0. */
+ size_t socket_send_buffer_size;
+ size_t socket_recv_buffer_size;
+
+ /* Event to use as parent for the http client event. For specific
+ requests this can be overridden with http_client_request_set_event().
+ */
+ struct event *event_parent;
+
+ /* enable logging debug messages */
+ bool debug;
+};
+
+/*
+ * Request
+ */
+
+enum http_client_request_error {
+ /* The request was aborted */
+ HTTP_CLIENT_REQUEST_ERROR_ABORTED = HTTP_RESPONSE_STATUS_INTERNAL,
+ /* Failed to parse HTTP target url */
+ HTTP_CLIENT_REQUEST_ERROR_INVALID_URL,
+ /* Failed to perform DNS lookup for the host */
+ HTTP_CLIENT_REQUEST_ERROR_HOST_LOOKUP_FAILED,
+ /* Failed to setup any connection for the host and client settings allowed
+ no more attempts */
+ HTTP_CLIENT_REQUEST_ERROR_CONNECT_FAILED,
+ /* Service returned an invalid redirect response for this request */
+ HTTP_CLIENT_REQUEST_ERROR_INVALID_REDIRECT,
+ /* The connection was lost unexpectedly while handling the request and
+ client settings allowed no more attempts */
+ HTTP_CLIENT_REQUEST_ERROR_CONNECTION_LOST,
+ /* The input stream passed to the request using
+ http_client_request_set_payload() returned an error while sending the
+ request. */
+ HTTP_CLIENT_REQUEST_ERROR_BROKEN_PAYLOAD,
+ /* The service returned a bad response */
+ HTTP_CLIENT_REQUEST_ERROR_BAD_RESPONSE,
+ /* The request timed out (either this was the last attempt or the
+ absolute timeout was hit) */
+ HTTP_CLIENT_REQUEST_ERROR_TIMED_OUT,
+};
+
+enum http_request_state {
+ /* New request; not yet submitted */
+ HTTP_REQUEST_STATE_NEW = 0,
+ /* Request is queued; waiting for a connection */
+ HTTP_REQUEST_STATE_QUEUED,
+ /* Request header is sent; still sending request payload to server */
+ HTTP_REQUEST_STATE_PAYLOAD_OUT,
+ /* Request is fully sent; waiting for response */
+ HTTP_REQUEST_STATE_WAITING,
+ /* Response header is received for the request */
+ HTTP_REQUEST_STATE_GOT_RESPONSE,
+ /* Reading response payload; response handler still needs to read more
+ payload. */
+ HTTP_REQUEST_STATE_PAYLOAD_IN,
+ /* Request is finished; still lingering due to references */
+ HTTP_REQUEST_STATE_FINISHED,
+ /* Request is aborted; still lingering due to references */
+ HTTP_REQUEST_STATE_ABORTED
+};
+extern const char *http_request_state_names[];
+
+struct http_client_tunnel {
+ int fd_in, fd_out;
+ struct istream *input;
+ struct ostream *output;
+};
+
+struct http_client_request_stats {
+ /* Total elapsed time since message was submitted */
+ unsigned int total_msecs;
+ /* Elapsed time since message was first sent */
+ unsigned int first_sent_msecs;
+ /* Elapsed time since message was last sent */
+ unsigned int last_sent_msecs;
+
+ /* Time spent in other ioloops */
+ unsigned int other_ioloop_msecs;
+ /* Time spent in the http-client's own ioloop */
+ unsigned int http_ioloop_msecs;
+ /* Total time spent on waiting for file locks */
+ unsigned int lock_msecs;
+
+ /* Number of times this request was retried */
+ unsigned int attempts;
+ /* Number of times the client attempted to actually send the request
+ to a server */
+ unsigned int send_attempts;
+};
+
+typedef void
+http_client_request_callback_t(const struct http_response *response,
+ void *context);
+
+/* create new HTTP request */
+struct http_client_request *
+http_client_request(struct http_client *client,
+ const char *method, const char *host, const char *target,
+ http_client_request_callback_t *callback, void *context);
+#define http_client_request(client, method, host, target, callback, context) \
+ http_client_request(client, method, host, target - \
+ CALLBACK_TYPECHECK(callback, void (*)( \
+ const struct http_response *response, typeof(context))), \
+ (http_client_request_callback_t *)callback, context)
+
+/* create new HTTP request using provided URL. This implicitly sets
+ port, ssl, and username:password if provided. */
+struct http_client_request *
+http_client_request_url(struct http_client *client,
+ const char *method, const struct http_url *target_url,
+ http_client_request_callback_t *callback, void *context);
+#define http_client_request_url(client, method, target_url, callback, context) \
+ http_client_request_url(client, method, target_url - \
+ CALLBACK_TYPECHECK(callback, void (*)( \
+ const struct http_response *response, typeof(context))), \
+ (http_client_request_callback_t *)callback, context)
+struct http_client_request *
+http_client_request_url_str(struct http_client *client,
+ const char *method, const char *url_str,
+ http_client_request_callback_t *callback, void *context);
+#define http_client_request_url_str(client, method, url_str, callback, context) \
+ http_client_request_url_str(client, method, url_str - \
+ CALLBACK_TYPECHECK(callback, void (*)( \
+ const struct http_response *response, typeof(context))), \
+ (http_client_request_callback_t *)callback, context)
+
+/* create new HTTP CONNECT request. If this HTTP is configured to use a proxy,
+ a CONNECT request will be submitted at that proxy, otherwise the connection
+ is created directly. Call http_client_request_start_tunnel() to
+ to take over the connection.
+ */
+struct http_client_request *
+http_client_request_connect(struct http_client *client,
+ const char *host, in_port_t port,
+ http_client_request_callback_t *callback,
+ void *context);
+#define http_client_request_connect(client, host, port, callback, context) \
+ http_client_request_connect(client, host, port - \
+ CALLBACK_TYPECHECK(callback, void (*)( \
+ const struct http_response *response, typeof(context))), \
+ (http_client_request_callback_t *)callback, context)
+
+/* same as http_client_request_connect, but uses an IP rather than a host
+ name. */
+struct http_client_request *
+http_client_request_connect_ip(struct http_client *client,
+ const struct ip_addr *ip, in_port_t port,
+ http_client_request_callback_t *callback,
+ void *context);
+#define http_client_request_connect_ip(client, ip, port, callback, context) \
+ http_client_request_connect_ip(client, ip, port - \
+ CALLBACK_TYPECHECK(callback, void (*)( \
+ const struct http_response *response, typeof(context))), \
+ (http_client_request_callback_t *)callback, context)
+
+void http_client_request_set_event(struct http_client_request *req,
+ struct event *event);
+/* set the port for the service the request is directed at */
+void http_client_request_set_port(struct http_client_request *req,
+ in_port_t port);
+/* indicate whether service the request is directed at uses ssl */
+void http_client_request_set_ssl(struct http_client_request *req,
+ bool ssl);
+/* set the urgent flag: this means that this request will get priority over
+ non-urgent request. Also, if no idle connection is available, a new
+ connection is created. Urgent requests are never pipelined. */
+void http_client_request_set_urgent(struct http_client_request *req);
+void http_client_request_set_preserve_exact_reason(struct http_client_request *req);
+
+/* add a custom header to the request. This can override headers that are
+ otherwise created implicitly. If the same header key was already added,
+ the value is replaced. */
+void http_client_request_add_header(struct http_client_request *req,
+ const char *key, const char *value);
+/* add a custom header to the request. Do nothing if it was already added. */
+void http_client_request_add_missing_header(struct http_client_request *req,
+ const char *key, const char *value);
+/* remove a header added earlier. This has no influence on implicitly created
+ headers. */
+void http_client_request_remove_header(struct http_client_request *req,
+ const char *key);
+/* lookup the value for a header added earlier. Returns NULL if not found. */
+const char *http_client_request_lookup_header(struct http_client_request *req,
+ const char *key);
+
+/* set the value of the "Date" header for the request using a time_t value.
+ Use this instead of setting it directly using
+ http_client_request_add_header() */
+void http_client_request_set_date(struct http_client_request *req,
+ time_t date);
+
+/* assign an input stream for the outgoing payload of this request. The input
+ stream is read asynchronously while the request is sent to the server.
+
+ when sync=TRUE a "100 Continue" response is requested from the service. The
+ client will then postpone sending the payload until a provisional response
+ with code 100 is received. This way, an error response can be sent by the
+ service before any potentially big payload is transmitted. Use this only for
+ payload that can be large. */
+void http_client_request_set_payload(struct http_client_request *req,
+ struct istream *input, bool sync);
+/* assign payload data to the request. The data is copied to the request pool.
+ If your data is already durably allocated during the existence of the
+ request, you should consider using http_client_request_set_payload() with
+ a data input stream instead. This will avoid copying the data unnecessarily.
+ */
+void http_client_request_set_payload_data(struct http_client_request *req,
+ const unsigned char *data, size_t size);
+/* send an empty payload for this request. This means that a Content-Length
+ header is generated with zero size. Calling this function is not necessary
+ for the standard POST and PUT methods, for which this is done implicitly if
+ there is no payload set. */
+void http_client_request_set_payload_empty(struct http_client_request *req);
+
+/* set an absolute timeout for this request specifically, overriding the
+ default client-wide absolute request timeout */
+void http_client_request_set_timeout_msecs(struct http_client_request *req,
+ unsigned int msecs);
+void http_client_request_set_timeout(struct http_client_request *req,
+ const struct timeval *time);
+
+/* Override http_client_settings.request_timeout_msecs */
+void http_client_request_set_attempt_timeout_msecs(struct http_client_request *req,
+ unsigned int msecs);
+/* Override http_client_settings.max_attempts */
+void http_client_request_set_max_attempts(struct http_client_request *req,
+ unsigned int max_attempts);
+
+/* Include the specified HTTP response headers in the http_request_finished
+ event parameters with "http_hdr_" prefix. */
+void http_client_request_set_event_headers(struct http_client_request *req,
+ const char *const *headers);
+
+/* set the username:password credentials for this request for simple
+ authentication. This function is meant for simple schemes that use a
+ password. More complex schemes will need to be handled manually.
+
+ This currently only supports the "basic" authentication scheme. */
+void http_client_request_set_auth_simple(struct http_client_request *req,
+ const char *username, const char *password);
+
+/* Assign a proxy to use for this particular request. This overrides any
+ proxy defined in the client settings. */
+void http_client_request_set_proxy_url(struct http_client_request *req,
+ const struct http_url *proxy_url);
+/* Like http_client_request_set_proxy_url(), but the proxy is behind a unix
+ socket. */
+void http_client_request_set_proxy_socket(struct http_client_request *req,
+ const char *proxy_socket);
+
+/* delay handling of this request to a later time. This way, a request can be
+ submitted that is held for some time until a certain time period has passed.
+ */
+void http_client_request_delay_until(struct http_client_request *req,
+ time_t time);
+void http_client_request_delay(struct http_client_request *req,
+ time_t seconds);
+void http_client_request_delay_msecs(struct http_client_request *req,
+ unsigned int msecs);
+
+/* Try to set request delay based on the Retry-After header. Returns 1 if
+ successful, 0 if it doesn't exist or is already expired, -1 if the delay
+ would be too long. */
+int http_client_request_delay_from_response(struct http_client_request *req,
+ const struct http_response *response);
+
+/* return the HTTP method for the request */
+const char *
+http_client_request_get_method(const struct http_client_request *req)
+ ATTR_PURE;
+/* return the HTTP target for the request */
+const char *
+http_client_request_get_target(const struct http_client_request *req)
+ ATTR_PURE;
+/* return the request state */
+enum http_request_state
+http_client_request_get_state(const struct http_client_request *req)
+ ATTR_PURE;
+/* return number of retry attempts */
+unsigned int
+http_client_request_get_attempts(const struct http_client_request *req)
+ ATTR_PURE;
+/* return origin_url */
+const struct http_url *
+http_client_request_get_origin_url(const struct http_client_request *req)
+ ATTR_PURE;
+
+/* get statistics for the request */
+void http_client_request_get_stats(struct http_client_request *req,
+ struct http_client_request_stats *stats);
+/* append text with request statistics to provided string buffer */
+void http_client_request_append_stats_text(struct http_client_request *req,
+ string_t *str);
+
+/* submit the request. It is queued for transmission to the service */
+void http_client_request_submit(struct http_client_request *req);
+
+/* attempt to retry the request. This function is called within the request
+ callback. It returns false if the request cannot be retried */
+bool http_client_request_try_retry(struct http_client_request *req);
+
+/* abort the request immediately. It may still linger for a while when it is
+ already sent to the service, but the callback will not be called anymore. */
+void http_client_request_abort(struct http_client_request **req);
+
+/* call the specified callback when HTTP request is destroyed. */
+void http_client_request_set_destroy_callback(struct http_client_request *req,
+ void (*callback)(void *),
+ void *context);
+#define http_client_request_set_destroy_callback(req, callback, context) \
+ http_client_request_set_destroy_callback(req, (void(*)(void*))callback, \
+ TRUE ? context : \
+ CALLBACK_TYPECHECK(callback, void (*)(typeof(context))))
+
+/* submits request and blocks until the provided payload is sent. Multiple
+ calls are allowed; payload transmission is ended with
+ http_client_request_finish_payload(). If the sending fails, returns -1
+ and sets req=NULL to indicate that the request was freed, otherwise
+ returns 0 and req is unchanged. */
+int http_client_request_send_payload(struct http_client_request **req,
+ const unsigned char *data, size_t size);
+/* finish sending the payload. Always frees req and sets it to NULL.
+ Returns 0 on success, -1 on error. */
+int http_client_request_finish_payload(struct http_client_request **req);
+
+/* take over the connection this request was sent over for use as a HTTP
+ CONNECT tunnel. This only applies to requests that were created using
+ http_client_request_connect() or http_client_request_connect_ip(). */
+void http_client_request_start_tunnel(struct http_client_request *req,
+ struct http_client_tunnel *tunnel);
+
+/*
+ * Client
+ */
+
+/* Create a client using the global shared client context. */
+struct http_client *
+http_client_init(const struct http_client_settings *set);
+/* Create a client without a shared context. */
+struct http_client *
+http_client_init_private(const struct http_client_settings *set);
+struct http_client *
+http_client_init_shared(struct http_client_context *cctx,
+ const struct http_client_settings *set) ATTR_NULL(1);
+void http_client_deinit(struct http_client **_client);
+
+/* switch this client to the current ioloop */
+struct ioloop *http_client_switch_ioloop(struct http_client *client);
+
+/* blocks until all currently submitted requests are handled */
+void http_client_wait(struct http_client *client);
+
+/* Returns the total number of pending HTTP requests. */
+unsigned int
+http_client_get_pending_request_count(struct http_client *client);
+
+/*
+ * Client shared context
+ */
+
+struct http_client_context *
+http_client_context_create(const struct http_client_settings *set);
+void http_client_context_ref(struct http_client_context *cctx);
+void http_client_context_unref(struct http_client_context **_cctx);
+
+/* Return the default global shared client context, creating it if necessary.
+ The context is freed automatically at exit. Don't unreference the
+ returned context. */
+struct http_client_context *http_client_get_global_context(void);
+
+#endif
diff --git a/src/lib-http/http-common.h b/src/lib-http/http-common.h
new file mode 100644
index 0000000..9aa217f
--- /dev/null
+++ b/src/lib-http/http-common.h
@@ -0,0 +1,7 @@
+#ifndef HTTP_COMMON_H
+#define HTTP_COMMON_H
+
+#define HTTP_DEFAULT_PORT 80
+#define HTTPS_DEFAULT_PORT 443
+
+#endif
diff --git a/src/lib-http/http-date.c b/src/lib-http/http-date.c
new file mode 100644
index 0000000..6f0c5a7
--- /dev/null
+++ b/src/lib-http/http-date.c
@@ -0,0 +1,487 @@
+/* Copyright (c) 2013-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "str.h"
+#include "utc-mktime.h"
+#include "http-date.h"
+
+#include <ctype.h>
+
+/* RFC 7231, Section 7.1.1.1: Date/Time Formats
+
+ The defined syntax is as follows:
+
+ HTTP-date = IMF-fixdate / obs-date
+
+ Preferred format:
+
+ IMF-fixdate = day-name "," SP date1 SP time-of-day SP GMT
+ ; fixed length/zone/capitalization subset of the format
+ ; see Section 3.3 of [RFC5322]
+ day-name = %x4D.6F.6E ; "Mon", case-sensitive
+ / %x54.75.65 ; "Tue", case-sensitive
+ / %x57.65.64 ; "Wed", case-sensitive
+ / %x54.68.75 ; "Thu", case-sensitive
+ / %x46.72.69 ; "Fri", case-sensitive
+ / %x53.61.74 ; "Sat", case-sensitive
+ / %x53.75.6E ; "Sun", case-sensitive
+ date1 = day SP month SP year
+ ; e.g., 02 Jun 1982
+ day = 2DIGIT
+ month = %x4A.61.6E ; "Jan", case-sensitive
+ / %x46.65.62 ; "Feb", case-sensitive
+ / %x4D.61.72 ; "Mar", case-sensitive
+ / %x41.70.72 ; "Apr", case-sensitive
+ / %x4D.61.79 ; "May", case-sensitive
+ / %x4A.75.6E ; "Jun", case-sensitive
+ / %x4A.75.6C ; "Jul", case-sensitive
+ / %x41.75.67 ; "Aug", case-sensitive
+ / %x53.65.70 ; "Sep", case-sensitive
+ / %x4F.63.74 ; "Oct", case-sensitive
+ / %x4E.6F.76 ; "Nov", case-sensitive
+ / %x44.65.63 ; "Dec", case-sensitive
+ year = 4DIGIT
+ GMT = %x47.4D.54 ; "GMT", case-sensitive
+ time-of-day = hour ":" minute ":" second
+ ; 00:00:00 - 23:59:60 (leap second)
+ hour = 2DIGIT
+ minute = 2DIGIT
+ second = 2DIGIT
+
+ Obsolete formats:
+
+ obs-date = rfc850-date / asctime-date
+
+ rfc850-date = day-name-l "," SP date2 SP time-of-day SP GMT
+ date2 = day "-" month "-" 2DIGIT
+ ; e.g., 02-Jun-82
+ day-name-l = %x4D.6F.6E.64.61.79 ; "Monday", case-sensitive
+ / %x54.75.65.73.64.61.79 ; "Tuesday", case-sensitive
+ / %x57.65.64.6E.65.73.64.61.79 ; "Wednesday", case-sensitive
+ / %x54.68.75.72.73.64.61.79 ; "Thursday", case-sensitive
+ / %x46.72.69.64.61.79 ; "Friday", case-sensitive
+ / %x53.61.74.75.72.64.61.79 ; "Saturday", case-sensitive
+ / %x53.75.6E.64.61.79 ; "Sunday", case-sensitive
+
+ asctime-date = day-name SP date3 SP time-of-day SP year
+ date3 = month SP ( 2DIGIT / ( SP 1DIGIT ))
+ ; e.g., Jun 2
+ */
+
+static const char *month_names[] = {
+ "Jan", "Feb", "Mar", "Apr", "May", "Jun",
+ "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"
+};
+
+static const char *weekday_names[] = {
+ "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"
+};
+
+static const char *weekday_names_long[] = {
+ "Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"
+};
+
+struct http_date_parser {
+ const unsigned char *cur, *end;
+
+ struct tm tm;
+ int timezone_offset;
+};
+
+static inline int
+http_date_parse_sp(struct http_date_parser *parser)
+{
+ if (parser->cur >= parser->end)
+ return -1;
+ if (parser->cur[0] != ' ')
+ return 0;
+ parser->cur++;
+ return 1;
+}
+
+static inline int
+http_date_parse_number(struct http_date_parser *parser,
+ int digits, int *number_r)
+{
+ int i;
+
+ if (parser->cur >= parser->end || !i_isdigit(parser->cur[0]))
+ return 0;
+
+ *number_r = parser->cur[0] - '0';
+ parser->cur++;
+
+ for (i=0; i < digits-1; i++) {
+ if (parser->cur >= parser->end || !i_isdigit(parser->cur[0]))
+ return -1;
+ *number_r = ((*number_r) * 10) + parser->cur[0] - '0';
+ parser->cur++;
+ }
+ return 1;
+}
+
+static inline int
+http_date_parse_word(struct http_date_parser *parser,
+ int maxchars, string_t **word_r)
+{
+ string_t *word;
+ int i;
+
+ if (parser->cur >= parser->end || !i_isalpha(parser->cur[0]))
+ return 0;
+
+ word = t_str_new(maxchars);
+ str_append_c(word, parser->cur[0]);
+ parser->cur++;
+
+ for (i=0; i < maxchars-1; i++) {
+ if (parser->cur >= parser->end || !i_isalpha(parser->cur[0]))
+ break;
+ str_append_c(word, parser->cur[0]);
+ parser->cur++;
+ }
+
+ if (parser->cur < parser->end && i_isalpha(parser->cur[0]))
+ return -1;
+ *word_r = word;
+ return 1;
+}
+
+static inline int
+http_date_parse_year(struct http_date_parser *parser)
+{
+ /* year = 4DIGIT */
+ if (http_date_parse_number(parser, 4, &parser->tm.tm_year) <= 0)
+ return -1;
+ if (parser->tm.tm_year < 1900)
+ return -1;
+ parser->tm.tm_year -= 1900;
+ return 1;
+}
+
+static inline int
+http_date_parse_month(struct http_date_parser *parser)
+{
+ string_t *month;
+ int i;
+
+ if (http_date_parse_word(parser, 3, &month) <= 0 || str_len(month) != 3)
+ return -1;
+
+ for (i = 0; i < 12; i++) {
+ if (strcmp(month_names[i], str_c(month)) == 0) {
+ break;
+ }
+ }
+ if (i >= 12)
+ return -1;
+
+ parser->tm.tm_mon = i;
+ return 1;
+}
+
+static inline int
+http_date_parse_day(struct http_date_parser *parser)
+{
+ /* day = 2DIGIT */
+ if (http_date_parse_number(parser, 2, &parser->tm.tm_mday) <= 0)
+ return -1;
+ return 1;
+}
+
+static int
+http_date_parse_time_of_day(struct http_date_parser *parser)
+{
+ /* time-of-day = hour ":" minute ":" second
+ ; 00:00:00 - 23:59:59
+ hour = 2DIGIT
+ minute = 2DIGIT
+ second = 2DIGIT
+ */
+
+ /* hour = 2DIGIT */
+ if (http_date_parse_number(parser, 2, &parser->tm.tm_hour) <= 0)
+ return -1;
+
+ /* ":" */
+ if (parser->cur >= parser->end || parser->cur[0] != ':')
+ return -1;
+ parser->cur++;
+
+ /* minute = 2DIGIT */
+ if (http_date_parse_number(parser, 2, &parser->tm.tm_min) <= 0)
+ return -1;
+
+ /* ":" */
+ if (parser->cur >= parser->end || parser->cur[0] != ':')
+ return -1;
+ parser->cur++;
+
+ /* second = 2DIGIT */
+ if (http_date_parse_number(parser, 2, &parser->tm.tm_sec) <= 0)
+ return -1;
+ return 1;
+}
+
+static inline int
+http_date_parse_time_gmt(struct http_date_parser *parser)
+{
+ string_t *gmt;
+
+ /* Remaining: {...} SP time-of-day SP GMT
+ */
+
+ /* SP time-of-day */
+ if (http_date_parse_sp(parser) <= 0)
+ return -1;
+ if (http_date_parse_time_of_day(parser) <= 0)
+ return -1;
+
+ /* SP GMT */
+ if (http_date_parse_sp(parser) <= 0)
+ return -1;
+ if (http_date_parse_word(parser, 3, &gmt) <= 0 ||
+ strcmp("GMT", str_c(gmt)) != 0)
+ return -1;
+ return 1;
+}
+
+static int
+http_date_parse_format_imf_fixdate(struct http_date_parser *parser)
+{
+ /*
+ IMF-fixdate = day-name "," SP date1 SP time-of-day SP GMT
+ ; fixed length/zone/capitalization subset of the format
+ ; see Section 3.3 of [RFC5322]
+ date1 = day SP month SP year
+ ; e.g., 02 Jun 1982
+
+ Remaining: {...} SP day SP month SP year SP time-of-day SP GMT
+
+ */
+
+ /* SP day */
+ if (http_date_parse_sp(parser) <= 0)
+ return -1;
+ if (http_date_parse_day(parser) <= 0)
+ return -1;
+
+ /* SP month */
+ if (http_date_parse_sp(parser) <= 0)
+ return -1;
+ if (http_date_parse_month(parser) <= 0)
+ return -1;
+
+ /* SP year */
+ if (http_date_parse_sp(parser) <= 0)
+ return -1;
+ if (http_date_parse_year(parser) <= 0)
+ return -1;
+
+ /* SP time-of-day SP GMT */
+ return http_date_parse_time_gmt(parser);
+}
+
+static int
+http_date_parse_format_rfc850(struct http_date_parser *parser)
+{
+ /*
+ rfc850-date = day-name-l "," SP date2 SP time-of-day SP GMT
+ date2 = day "-" month "-" 2DIGIT
+ ; day-month-year (e.g., 02-Jun-82)
+
+ Remaining: "," SP day "-" month "-" 2DIGIT SP time-of-day SP GMT
+ */
+
+ /* "," SP */
+ if (parser->cur >= parser->end || parser->cur[0] != ',')
+ return -1;
+ parser->cur++;
+ if (http_date_parse_sp(parser) <= 0)
+ return -1;
+
+ /* day */
+ if (http_date_parse_day(parser) <= 0)
+ return -1;
+
+ /* "-" */
+ if (parser->cur >= parser->end || parser->cur[0] != '-')
+ return -1;
+ parser->cur++;
+
+ /* month */
+ if (http_date_parse_month(parser) <= 0)
+ return -1;
+
+ /* "-" */
+ if (parser->cur >= parser->end || parser->cur[0] != '-')
+ return -1;
+ parser->cur++;
+
+ /* 2DIGIT */
+ if (http_date_parse_number(parser, 2, &parser->tm.tm_year) <= 0)
+ return -1;
+ if (parser->tm.tm_year < 70)
+ parser->tm.tm_year += 100;
+
+ /* SP time-of-day SP GMT */
+ return http_date_parse_time_gmt(parser);
+}
+
+static int
+http_date_parse_format_asctime(struct http_date_parser *parser)
+{
+ int ret;
+
+ /*
+ asctime-date = day-name SP date3 SP time-of-day SP year
+ date3 = month SP ( 2DIGIT / ( SP 1DIGIT ))
+ ; month day (e.g., Jun 2)
+
+ Remaining: {...} month SP ( 2DIGIT / ( SP 1DIGIT )) SP time-of-day SP year
+ */
+
+ /* month */
+ if (http_date_parse_month(parser) <= 0)
+ return -1;
+
+ /* SP */
+ if (http_date_parse_sp(parser) <= 0)
+ return -1;
+
+ /* SP 1DIGIT / 2DIGIT */
+ if ((ret=http_date_parse_sp(parser)) < 0)
+ return -1;
+ if (ret == 0) {
+ if (http_date_parse_number(parser, 2, &parser->tm.tm_mday) <= 0)
+ return -1;
+ } else {
+ if (http_date_parse_number(parser, 1, &parser->tm.tm_mday) <= 0)
+ return -1;
+ }
+
+ /* SP time-of-day */
+ if (http_date_parse_sp(parser) <= 0)
+ return -1;
+ if (http_date_parse_time_of_day(parser) <= 0)
+ return -1;
+
+ /* SP year */
+ if (http_date_parse_sp(parser) <= 0)
+ return -1;
+
+ return http_date_parse_year(parser);
+}
+
+static int
+http_date_parse_format_any(struct http_date_parser *parser)
+{
+ string_t *dayname;
+ int i;
+
+ /*
+ HTTP-date = IMF-fixdate / obs-date
+ IMF-fixdate = day-name "," SP date1 SP time-of-day SP GMT
+ ; fixed length/zone/capitalization subset of the format
+ ; see Section 3.3 of [RFC5322]
+ obs-date = rfc850-date / asctime-date
+ rfc850-date = day-name-l "," SP date2 SP time-of-day SP GMT
+ asctime-date = day-name SP date3 SP time-of-day SP year
+ */
+
+ if (http_date_parse_word(parser, 9, &dayname) <= 0)
+ return -1;
+
+ if (str_len(dayname) > 3) {
+ /* rfc850-date */
+ for (i = 0; i < 7; i++) {
+ if (strcmp(weekday_names_long[i], str_c(dayname)) == 0) {
+ break;
+ }
+ }
+ if (i >= 7)
+ return -1;
+ return http_date_parse_format_rfc850(parser);
+ }
+
+ /* IMF-fixdate / asctime-date */
+ for (i = 0; i < 7; i++) {
+ if (strcmp(weekday_names[i], str_c(dayname)) == 0) {
+ break;
+ }
+ }
+
+ if (i >= 7 || parser->cur >= parser->end)
+ return -1;
+
+ if (parser->cur[0] == ' ') {
+ /* asctime-date */
+ parser->cur++;
+ return http_date_parse_format_asctime(parser);
+ }
+
+ if (parser->cur[0] != ',')
+ return -1;
+
+ /* IMF-fixdate */
+ parser->cur++;
+ return http_date_parse_format_imf_fixdate(parser);
+}
+
+bool http_date_parse(const unsigned char *data, size_t size,
+ time_t *timestamp_r)
+{
+ struct http_date_parser parser;
+ time_t timestamp;
+
+ i_zero(&parser);
+ parser.cur = data;
+ parser.end = data + size;
+
+ if (http_date_parse_format_any(&parser) <= 0)
+ return FALSE;
+
+ if (parser.cur != parser.end)
+ return FALSE;
+
+ parser.tm.tm_isdst = -1;
+ timestamp = utc_mktime(&parser.tm);
+ if (timestamp == (time_t)-1)
+ return FALSE;
+
+ *timestamp_r = timestamp;
+ return TRUE;
+}
+
+bool http_date_parse_tm(const unsigned char *data, size_t size,
+ struct tm *tm_r)
+{
+ time_t timestamp;
+ struct tm *tm;
+
+ if (!http_date_parse(data, size, &timestamp))
+ return FALSE;
+
+ tm = gmtime(&timestamp);
+ *tm_r = *tm;
+ return TRUE;
+}
+
+const char *http_date_create_tm(struct tm *tm)
+{
+ return t_strdup_printf("%s, %02d %s %04d %02d:%02d:%02d GMT",
+ weekday_names[tm->tm_wday],
+ tm->tm_mday,
+ month_names[tm->tm_mon],
+ tm->tm_year+1900,
+ tm->tm_hour, tm->tm_min, tm->tm_sec);
+}
+
+const char *http_date_create(time_t timestamp)
+{
+ struct tm *tm;
+ tm = gmtime(&timestamp);
+
+ return http_date_create_tm(tm);
+}
+
diff --git a/src/lib-http/http-date.h b/src/lib-http/http-date.h
new file mode 100644
index 0000000..e46299d
--- /dev/null
+++ b/src/lib-http/http-date.h
@@ -0,0 +1,17 @@
+#ifndef HTTP_DATE_H
+#define HTTP_DATE_H
+
+/* Parses HTTP-date string into time_t timestamp. */
+bool http_date_parse(const unsigned char *data, size_t size,
+ time_t *timestamp_r);
+/* Equal to http_date_parse, but writes uncompensated timestamp to tm_r. */
+bool http_date_parse_tm(const unsigned char *data, size_t size,
+ struct tm *tm_r);
+
+/* Create HTTP-date string from given time struct. */
+const char *http_date_create_tm(struct tm *tm);
+
+/* Create HTTP-date string from given time. */
+const char *http_date_create(time_t timestamp);
+
+#endif
diff --git a/src/lib-http/http-header-parser.c b/src/lib-http/http-header-parser.c
new file mode 100644
index 0000000..29de688
--- /dev/null
+++ b/src/lib-http/http-header-parser.c
@@ -0,0 +1,367 @@
+/* Copyright (c) 2013-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "buffer.h"
+#include "istream.h"
+#include "str.h"
+#include "str-sanitize.h"
+#include "http-parser.h"
+#include "http-header.h"
+
+#include "http-header-parser.h"
+
+enum http_header_parse_state {
+ HTTP_HEADER_PARSE_STATE_INIT = 0,
+ HTTP_HEADER_PARSE_STATE_NAME,
+ HTTP_HEADER_PARSE_STATE_COLON,
+ HTTP_HEADER_PARSE_STATE_OWS,
+ HTTP_HEADER_PARSE_STATE_CONTENT,
+ HTTP_HEADER_PARSE_STATE_CR,
+ HTTP_HEADER_PARSE_STATE_LF,
+ HTTP_HEADER_PARSE_STATE_NEW_LINE,
+ HTTP_HEADER_PARSE_STATE_EOH
+};
+
+struct http_header_parser {
+ struct istream *input;
+
+ struct http_header_limits limits;
+ enum http_header_parse_flags flags;
+
+ uoff_t size, field_size;
+ unsigned int field_count;
+
+ const unsigned char *begin, *cur, *end;
+ const char *error;
+
+ string_t *name;
+ buffer_t *value_buf;
+
+ enum http_header_parse_state state;
+};
+
+struct http_header_parser *
+http_header_parser_init(struct istream *input,
+ const struct http_header_limits *limits,
+ enum http_header_parse_flags flags)
+{
+ struct http_header_parser *parser;
+
+ parser = i_new(struct http_header_parser, 1);
+ parser->input = input;
+
+ if (limits != NULL)
+ parser->limits = *limits;
+
+ if (parser->limits.max_size == 0)
+ parser->limits.max_size = UOFF_T_MAX;
+ if (parser->limits.max_field_size == 0)
+ parser->limits.max_field_size = UOFF_T_MAX;
+ if (parser->limits.max_fields == 0)
+ parser->limits.max_fields = (unsigned int)-1;
+
+ parser->flags = flags;
+
+ parser->name = str_new(default_pool, 128);
+ parser->value_buf = buffer_create_dynamic(default_pool, 4096);
+
+ return parser;
+}
+
+void http_header_parser_deinit(struct http_header_parser **_parser)
+{
+ struct http_header_parser *parser = *_parser;
+
+ *_parser = NULL;
+
+ //i_stream_skip(ctx->input, ctx->skip);
+ buffer_free(&parser->value_buf);
+ str_free(&parser->name);
+ i_free(parser);
+}
+
+void http_header_parser_reset(struct http_header_parser *parser)
+{
+ parser->state = HTTP_HEADER_PARSE_STATE_INIT;
+ parser->size = 0;
+ parser->field_size = 0;
+ parser->field_count = 0;
+}
+
+static int http_header_parse_name(struct http_header_parser *parser)
+{
+ const unsigned char *first = parser->cur;
+
+ /* field-name = token
+ token = 1*tchar
+ */
+ while (parser->cur < parser->end && http_char_is_token(*parser->cur))
+ parser->cur++;
+
+ str_append_data(parser->name, first, parser->cur-first);
+
+ if (parser->cur == parser->end)
+ return 0;
+ if (str_len(parser->name) == 0) {
+ parser->error = "Empty header field name";
+ return -1;
+ }
+ return 1;
+}
+
+static int http_header_parse_ows(struct http_header_parser *parser)
+{
+ /* OWS = *( SP / HTAB )
+ ; "optional" whitespace
+ */
+ while (parser->cur < parser->end &&
+ (*parser->cur == ' ' || *parser->cur == '\t'))
+ parser->cur++;
+ return (parser->cur == parser->end ? 0 : 1);
+}
+
+static int http_header_parse_content(struct http_header_parser *parser)
+{
+ const unsigned char *first;
+
+ /* field-content = field-vchar [ 1*( SP / HTAB ) field-vchar ]
+ field-vchar = VCHAR / obs-text
+ */
+ do {
+ first = parser->cur;
+ while (parser->cur < parser->end && http_char_is_text(*parser->cur)) {
+ parser->cur++;
+ }
+ buffer_append(parser->value_buf, first, parser->cur-first);
+
+ if ((parser->flags & HTTP_HEADER_PARSE_FLAG_STRICT) != 0)
+ break;
+
+ /* We'll be lenient here to accommodate for some bad servers. We just
+ drop offending characters */
+ while (parser->cur < parser->end && !http_char_is_text(*parser->cur) &&
+ (*parser->cur != '\r' && *parser->cur != '\n'))
+ parser->cur++;
+ } while (parser->cur < parser->end &&
+ (*parser->cur != '\r' && *parser->cur != '\n'));
+
+ if (parser->cur == parser->end)
+ return 0;
+ return 1;
+}
+
+static inline const char *_chr_sanitize(unsigned char c)
+{
+ if (c >= 0x20 && c < 0x7F)
+ return t_strdup_printf("'%c'", c);
+ return t_strdup_printf("0x%02x", c);
+}
+
+static int http_header_parse(struct http_header_parser *parser)
+{
+ int ret;
+
+ /* RFC 7230, Section 3.2: Header Fields
+
+ 'header' = *( header-field CRLF ) CRLF
+ ; Actually part of HTTP-message syntax
+
+ header-field = field-name ":" OWS field-value OWS
+ field-name = token
+ field-value = *( field-content / obs-fold )
+ field-content = field-vchar [ 1*( SP / HTAB ) field-vchar ]
+ field-vchar = VCHAR / obs-text
+ obs-fold = CRLF 1*( SP / HTAB )
+ ; obsolete line folding
+ ; see Section 3.2.4
+ */
+
+ for (;;) {
+ switch (parser->state) {
+ case HTTP_HEADER_PARSE_STATE_INIT:
+ buffer_set_used_size(parser->value_buf, 0);
+ str_truncate(parser->name, 0);
+ if (*parser->cur == '\r') {
+ /* last CRLF */
+ parser->cur++;
+ parser->state = HTTP_HEADER_PARSE_STATE_EOH;
+ if (parser->cur == parser->end)
+ return 0;
+ break;
+ } else if (*parser->cur == '\n') {
+ /* last LF */
+ parser->state = HTTP_HEADER_PARSE_STATE_EOH;
+ break;
+ }
+ /* next line */
+ parser->state = HTTP_HEADER_PARSE_STATE_NAME;
+ /* fall through */
+ case HTTP_HEADER_PARSE_STATE_NAME:
+ if ((ret=http_header_parse_name(parser)) <= 0)
+ return ret;
+ parser->state = HTTP_HEADER_PARSE_STATE_COLON;
+ /* fall through */
+ case HTTP_HEADER_PARSE_STATE_COLON:
+ if (*parser->cur != ':') {
+ parser->error = t_strdup_printf
+ ("Expected ':' after header field name '%s', but found %s",
+ str_sanitize(str_c(parser->name),64),
+ _chr_sanitize(*parser->cur));
+ return -1;
+ }
+ parser->cur++;
+ if (str_len(parser->name) == 0) {
+ parser->error = "Empty header field name";
+ return -1;
+ }
+ if (++parser->field_count > parser->limits.max_fields) {
+ parser->error = "Excessive number of header fields";
+ return -1;
+ }
+ parser->state = HTTP_HEADER_PARSE_STATE_OWS;
+ /* fall through */
+ case HTTP_HEADER_PARSE_STATE_OWS:
+ if ((ret=http_header_parse_ows(parser)) <= 0)
+ return ret;
+ parser->state = HTTP_HEADER_PARSE_STATE_CONTENT;
+ /* fall through */
+ case HTTP_HEADER_PARSE_STATE_CONTENT:
+ if ((ret=http_header_parse_content(parser)) <= 0)
+ return ret;
+ parser->state = HTTP_HEADER_PARSE_STATE_CR;
+ /* fall through */
+ case HTTP_HEADER_PARSE_STATE_CR:
+ if (*parser->cur == '\r') {
+ parser->cur++;
+ } else if (*parser->cur != '\n') {
+ parser->error = t_strdup_printf
+ ("Invalid character %s in content of header field '%s'",
+ _chr_sanitize(*parser->cur),
+ str_sanitize(str_c(parser->name),64));
+ return -1;
+ }
+ parser->state = HTTP_HEADER_PARSE_STATE_LF;
+ if (parser->cur == parser->end)
+ return 0;
+ /* fall through */
+ case HTTP_HEADER_PARSE_STATE_LF:
+ if (*parser->cur != '\n') {
+ parser->error = t_strdup_printf
+ ("Expected LF after CR at end of header field '%s', but found %s",
+ str_sanitize(str_c(parser->name),64),
+ _chr_sanitize(*parser->cur));
+ return -1;
+ }
+ parser->cur++;
+ parser->state = HTTP_HEADER_PARSE_STATE_NEW_LINE;
+ if (parser->cur == parser->end)
+ return 0;
+ /* fall through */
+ case HTTP_HEADER_PARSE_STATE_NEW_LINE:
+ if (*parser->cur == ' ' || *parser->cur == '\t') {
+ /* obs-fold */
+ buffer_append_c(parser->value_buf, ' ');
+ parser->state = HTTP_HEADER_PARSE_STATE_OWS;
+ break;
+ }
+ /* next header line */
+ parser->state = HTTP_HEADER_PARSE_STATE_INIT;
+ return 1;
+ case HTTP_HEADER_PARSE_STATE_EOH:
+ if (*parser->cur != '\n') {
+ parser->error = t_strdup_printf
+ ("Encountered stray CR at beginning of header line, followed by %s",
+ _chr_sanitize(*parser->cur));
+ return -1;
+ }
+ /* header fully parsed */
+ parser->cur++;
+ return 1;
+
+ default:
+ i_unreached();
+ }
+ }
+
+ i_unreached();
+ return -1;
+}
+
+int http_header_parse_next_field(struct http_header_parser *parser,
+ const char **name_r, const unsigned char **data_r, size_t *size_r,
+ const char **error_r)
+{
+ const uoff_t max_size = parser->limits.max_size;
+ const uoff_t max_field_size = parser->limits.max_field_size;
+ const unsigned char *data;
+ size_t size;
+ int ret;
+
+ *error_r = NULL;
+
+ while ((ret=i_stream_read_more(parser->input, &parser->begin, &size)) > 0) {
+
+ /* check header size limits */
+ if (parser->size >= max_size) {
+ *error_r = "Excessive header size";
+ return -1;
+ }
+ if (parser->field_size > max_field_size) {
+ *error_r = "Excessive header field size";
+ return -1;
+ }
+
+ /* don't parse beyond header size limits */
+ if (size > (max_size - parser->size))
+ size = max_size - parser->size;
+ if (size > (max_field_size - parser->field_size)) {
+ size = max_field_size - parser->field_size;
+ size = (size == 0 ? 1 : size); /* need to parse one more byte */
+ }
+
+ parser->cur = parser->begin;
+ parser->end = parser->cur + size;
+
+ if ((ret=http_header_parse(parser)) < 0) {
+ *error_r = parser->error;
+ return -1;
+ }
+
+ i_stream_skip(parser->input, parser->cur - parser->begin);
+ parser->size += parser->cur - parser->begin;
+ parser->field_size += parser->cur - parser->begin;
+
+ if (ret == 1) {
+ parser->field_size = 0;
+
+ if (parser->state != HTTP_HEADER_PARSE_STATE_EOH) {
+ data = buffer_get_data(parser->value_buf, &size);
+
+ /* trim trailing OWS */
+ while (size > 0 &&
+ (data[size-1] == ' ' || data[size-1] == '\t'))
+ size--;
+
+ *name_r = str_c(parser->name);
+ *data_r = data;
+ *size_r = size;
+ parser->state = HTTP_HEADER_PARSE_STATE_INIT;
+ } else {
+ *name_r = NULL;
+ *data_r = NULL;
+ }
+ return 1;
+ }
+ }
+
+ i_assert(ret != -2);
+ if (ret < 0) {
+ i_assert(parser->input->eof);
+ if (parser->input->stream_errno == 0)
+ *error_r = "Premature end of input";
+ else
+ *error_r = t_strdup_printf("Stream error: %s",
+ i_stream_get_error(parser->input));
+ }
+ return ret;
+}
diff --git a/src/lib-http/http-header-parser.h b/src/lib-http/http-header-parser.h
new file mode 100644
index 0000000..a746520
--- /dev/null
+++ b/src/lib-http/http-header-parser.h
@@ -0,0 +1,24 @@
+#ifndef HTTP_HEADER_PARSER_H
+#define HTTP_HEADER_PARSER_H
+
+struct http_header_limits;
+struct http_header_parser;
+
+enum http_header_parse_flags {
+ /* Strictly adhere to the HTTP protocol specification */
+ HTTP_HEADER_PARSE_FLAG_STRICT = BIT(0)
+};
+
+struct http_header_parser *
+http_header_parser_init(struct istream *input,
+ const struct http_header_limits *limits,
+ enum http_header_parse_flags flags);
+void http_header_parser_deinit(struct http_header_parser **_parser);
+
+void http_header_parser_reset(struct http_header_parser *parser);
+
+int http_header_parse_next_field(struct http_header_parser *parser,
+ const char **name_r, const unsigned char **data_r, size_t *size_r,
+ const char **error_r);
+
+#endif
diff --git a/src/lib-http/http-header.c b/src/lib-http/http-header.c
new file mode 100644
index 0000000..e8ef21a
--- /dev/null
+++ b/src/lib-http/http-header.c
@@ -0,0 +1,98 @@
+/* Copyright (c) 2013-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+
+#include "http-header.h"
+
+struct http_header {
+ ARRAY_TYPE(http_header_field) fields;
+ /* FIXME: ARRAY(struct http_header_field *) *btree; */
+};
+
+struct http_header *
+http_header_create(pool_t pool, unsigned int init_count)
+{
+ struct http_header *header;
+
+ header = p_new(pool, struct http_header, 1);
+ p_array_init(&header->fields, pool, init_count);
+
+ return header;
+}
+
+const struct http_header_field *
+http_header_field_add(struct http_header *header,
+ const char *name, const unsigned char *data, size_t size)
+{
+ struct http_header_field *hfield;
+ pool_t pool = array_get_pool(&header->fields);
+ void *value;
+
+ hfield = array_append_space(&header->fields);
+ hfield->name = p_strdup(pool, name);
+ hfield->size = size;
+
+ value = p_malloc(pool, size+1);
+ memcpy(value, data, size);
+ hfield->value = (const char *)value;
+
+ return hfield;
+}
+
+void http_header_field_delete(struct http_header *header, const char *name)
+{
+ ARRAY_TYPE(http_header_field) *hfields = &header->fields;
+ const struct http_header_field *hfield;
+
+ array_foreach(hfields, hfield) {
+ if (http_header_field_is(hfield, name)) {
+ array_delete(hfields, array_foreach_idx(hfields, hfield), 1);
+ }
+ }
+}
+
+const ARRAY_TYPE(http_header_field) *
+http_header_get_fields(const struct http_header *header)
+{
+ return &header->fields;
+}
+
+const struct http_header_field *
+http_header_field_find(const struct http_header *header, const char *name)
+{
+ const struct http_header_field *hfield;
+
+ array_foreach(&header->fields, hfield) {
+ if (http_header_field_is(hfield, name))
+ return hfield;
+ }
+
+ return NULL;
+}
+
+const char *
+http_header_field_get(const struct http_header *header, const char *name)
+{
+ const struct http_header_field *hfield =
+ http_header_field_find(header, name);
+ return (hfield == NULL ? NULL : hfield->value);
+}
+
+int http_header_field_find_unique(const struct http_header *header,
+ const char *name, const struct http_header_field **hfield_r)
+{
+ const struct http_header_field *hfield, *hfield_found = NULL;
+
+ array_foreach(&header->fields, hfield) {
+ if (http_header_field_is(hfield, name)) {
+ if (hfield_found != NULL)
+ return -1;
+ hfield_found = hfield;
+ }
+ }
+
+ *hfield_r = hfield_found;
+ return (hfield_found == NULL ? 0 : 1);
+}
+
diff --git a/src/lib-http/http-header.h b/src/lib-http/http-header.h
new file mode 100644
index 0000000..59941d9
--- /dev/null
+++ b/src/lib-http/http-header.h
@@ -0,0 +1,45 @@
+#ifndef HTTP_HEADER_H
+#define HTTP_HEADER_H
+
+struct http_header;
+
+struct http_header_limits {
+ uoff_t max_size;
+ uoff_t max_field_size;
+ unsigned int max_fields;
+};
+
+struct http_header_field {
+ const char *name;
+ const char *value;
+ size_t size;
+};
+ARRAY_DEFINE_TYPE(http_header_field, struct http_header_field);
+
+static inline bool http_header_field_is(const struct http_header_field *hfield,
+ const char *name)
+{
+ return (strcasecmp(hfield->name, name) == 0);
+}
+
+struct http_header *
+http_header_create(pool_t pool, unsigned int init_count);
+
+const struct http_header_field *
+http_header_field_add(struct http_header *header,
+ const char *name, const unsigned char *data, size_t size);
+void http_header_field_delete(struct http_header *header, const char *name);
+
+const ARRAY_TYPE(http_header_field) *
+http_header_get_fields(const struct http_header *header) ATTR_PURE;
+
+const struct http_header_field *
+http_header_field_find(const struct http_header *header, const char *name)
+ ATTR_PURE;
+const char *
+http_header_field_get(const struct http_header *header, const char *name)
+ ATTR_PURE;
+int http_header_field_find_unique(const struct http_header *header,
+ const char *name, const struct http_header_field **hfield_r);
+
+#endif
diff --git a/src/lib-http/http-message-parser.c b/src/lib-http/http-message-parser.c
new file mode 100644
index 0000000..a42bddd
--- /dev/null
+++ b/src/lib-http/http-message-parser.c
@@ -0,0 +1,658 @@
+/* Copyright (c) 2013-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "istream.h"
+#include "iostream.h"
+#include "istream-sized.h"
+#include "http-parser.h"
+#include "http-header.h"
+#include "http-header-parser.h"
+#include "http-date.h"
+#include "http-transfer.h"
+#include "http-message-parser.h"
+
+#include <ctype.h>
+
+void http_message_parser_init(struct http_message_parser *parser,
+ struct istream *input,
+ const struct http_header_limits *hdr_limits,
+ uoff_t max_payload_size,
+ enum http_message_parse_flags flags)
+{
+ i_zero(parser);
+ parser->input = input;
+ i_stream_ref(parser->input);
+ if (hdr_limits != NULL)
+ parser->header_limits = *hdr_limits;
+ parser->max_payload_size = max_payload_size;
+ parser->flags = flags;
+}
+
+void http_message_parser_deinit(struct http_message_parser *parser)
+{
+ if (parser->header_parser != NULL)
+ http_header_parser_deinit(&parser->header_parser);
+ pool_unref(&parser->msg.pool);
+ i_stream_unref(&parser->payload);
+ i_stream_unref(&parser->input);
+}
+
+void http_message_parser_restart(struct http_message_parser *parser,
+ pool_t pool)
+{
+ i_assert(parser->payload == NULL);
+
+ if (parser->header_parser == NULL) {
+ enum http_header_parse_flags hdr_flags = 0;
+
+ if ((parser->flags & HTTP_MESSAGE_PARSE_FLAG_STRICT) != 0)
+ hdr_flags |= HTTP_HEADER_PARSE_FLAG_STRICT;
+ parser->header_parser = http_header_parser_init(
+ parser->input, &parser->header_limits, hdr_flags);
+ } else {
+ http_header_parser_reset(parser->header_parser);
+ }
+
+ pool_unref(&parser->msg.pool);
+ i_zero(&parser->msg);
+ if (pool != NULL) {
+ parser->msg.pool = pool;
+ pool_ref(pool);
+ }
+ parser->msg.date = (time_t)-1;
+}
+
+pool_t http_message_parser_get_pool(struct http_message_parser *parser)
+{
+ if (parser->msg.pool == NULL)
+ parser->msg.pool = pool_alloconly_create("http_message", 4096);
+ return parser->msg.pool;
+}
+
+int http_message_parse_version(struct http_message_parser *parser)
+{
+ const unsigned char *p = parser->cur;
+ const size_t size = parser->end - parser->cur;
+
+ parser->error_code = HTTP_MESSAGE_PARSE_ERROR_NONE;
+ parser->error = NULL;
+
+ /* RFC 7230, Section 2.6: Protocol Versioning
+
+ HTTP-version = HTTP-name "/" DIGIT "." DIGIT
+ HTTP-name = %x48.54.54.50 ; "HTTP", case-sensitive
+ */
+ if (size < 8)
+ return 0;
+ if (memcmp(p, "HTTP/", 5) != 0 ||
+ !i_isdigit(p[5]) || p[6] != '.' || !i_isdigit(p[7])) {
+ parser->error = "Bad HTTP version";
+ parser->error_code = HTTP_MESSAGE_PARSE_ERROR_BROKEN_MESSAGE;
+ return -1;
+ }
+ parser->msg.version_major = p[5] - '0';
+ parser->msg.version_minor = p[7] - '0';
+ parser->cur += 8;
+ return 1;
+}
+
+static void
+http_message_parse_finish_payload_error(struct http_message_parser *parser)
+{
+ if (parser->payload->stream_errno == EMSGSIZE) {
+ parser->error_code = HTTP_MESSAGE_PARSE_ERROR_PAYLOAD_TOO_LARGE;
+ parser->error = "Payload is too large";
+ } else if (parser->payload->stream_errno == EIO) {
+ parser->error_code = HTTP_MESSAGE_PARSE_ERROR_BROKEN_MESSAGE;
+ parser->error = t_strdup_printf(
+ "Invalid payload: %s",
+ i_stream_get_error(parser->payload));
+ } else {
+ parser->error_code = HTTP_MESSAGE_PARSE_ERROR_BROKEN_STREAM;
+ parser->error = t_strdup_printf(
+ "Stream error while skipping payload: %s",
+ i_stream_get_error(parser->payload));
+ }
+}
+
+int http_message_parse_finish_payload(struct http_message_parser *parser)
+{
+ const unsigned char *data;
+ size_t size;
+ int ret;
+
+ parser->error_code = HTTP_MESSAGE_PARSE_ERROR_NONE;
+ parser->error = NULL;
+
+ if (parser->payload == NULL)
+ return 1;
+
+ while ((ret = i_stream_read_more(parser->payload, &data, &size)) > 0)
+ i_stream_skip(parser->payload, size);
+ if (ret == 0 || parser->payload->stream_errno != 0) {
+ if (ret < 0)
+ http_message_parse_finish_payload_error(parser);
+ return ret;
+ }
+
+ i_stream_destroy(&parser->payload);
+ return 1;
+}
+
+static int
+http_message_parse_hdr_connection(struct http_message_parser *parser,
+ const unsigned char *data, size_t size)
+{
+ pool_t pool = http_message_parser_get_pool(parser);
+ struct http_parser hparser;
+ const char **opt_idx;
+ const char *option;
+ unsigned int num_tokens = 0;
+
+ /* RFC 7230, Section 6.1: Connection
+
+ Connection = 1#connection-option
+ connection-option = token
+ */
+
+ /* Multiple Connection headers are allowed and combined
+ into one */
+ http_parser_init(&hparser, data, size);
+ for (;;) {
+ if (http_parse_token_list_next(&hparser, &option) <= 0)
+ break;
+ num_tokens++;
+ if (strcasecmp(option, "close") == 0)
+ parser->msg.connection_close = TRUE;
+ if (!array_is_created(&parser->msg.connection_options))
+ p_array_init(&parser->msg.connection_options, pool, 4);
+ opt_idx = array_append_space(&parser->msg.connection_options);
+ *opt_idx = p_strdup(pool, option);
+ }
+
+ if (hparser.cur < hparser.end || num_tokens == 0) {
+ parser->error = "Invalid Connection header";
+ parser->error_code = HTTP_MESSAGE_PARSE_ERROR_BROKEN_MESSAGE;
+ return -1;
+ }
+
+ return 0;
+}
+
+static int
+http_message_parse_hdr_content_length(struct http_message_parser *parser,
+ const struct http_header_field *hdr)
+{
+ if (parser->msg.have_content_length) {
+ /* There is no acceptable way to allow duplicates for this
+ header. */
+ parser->error = "Duplicate Content-Length header";
+ parser->error_code = HTTP_MESSAGE_PARSE_ERROR_BROKEN_MESSAGE;
+ return -1;
+ }
+
+ /* RFC 7230, Section 3.3.2: Content-Length
+
+ Content-Length = 1*DIGIT
+ */
+ if (str_to_uoff(hdr->value, &parser->msg.content_length) < 0) {
+ parser->error = "Invalid Content-Length header";
+ parser->error_code = HTTP_MESSAGE_PARSE_ERROR_BROKEN_MESSAGE;
+ return -1;
+ }
+ parser->msg.have_content_length = TRUE;
+ return 0;
+}
+
+static int
+http_message_parse_hdr_date(struct http_message_parser *parser,
+ const unsigned char *data, size_t size)
+{
+ if (parser->msg.date != (time_t)-1) {
+ if ((parser->flags & HTTP_MESSAGE_PARSE_FLAG_STRICT) != 0) {
+ parser->error = "Duplicate Date header";
+ parser->error_code =
+ HTTP_MESSAGE_PARSE_ERROR_BROKEN_MESSAGE;
+ return -1;
+ }
+ /* Allow the duplicate; last instance is used */
+ }
+
+ /* RFC 7231, Section 7.1.1.2: Date
+
+ Date = HTTP-date
+ */
+ if (!http_date_parse(data, size, &parser->msg.date) &&
+ (parser->flags & HTTP_MESSAGE_PARSE_FLAG_STRICT) != 0) {
+ parser->error = "Invalid Date header";
+ parser->error_code = HTTP_MESSAGE_PARSE_ERROR_BROKEN_MESSAGE;
+ return -1;
+ }
+ return 0;
+}
+
+static int
+http_message_parse_hdr_location(struct http_message_parser *parser,
+ const struct http_header_field *hdr)
+{
+ /* RFC 7231, Section 7.1.2: Location
+
+ Location = URI-reference
+
+ -> not parsed here
+ */
+ /* FIXME: move this to response parser */
+ parser->msg.location = hdr->value;
+ return 0;
+
+}
+
+static int
+http_message_parse_hdr_transfer_encoding(struct http_message_parser *parser,
+ const unsigned char *data, size_t size)
+{
+ pool_t pool = http_message_parser_get_pool(parser);
+ struct http_parser hparser;
+ const char *trenc = NULL;
+
+ /* Multiple Transfer-Encoding headers are allowed and combined into one
+ */
+ if (!array_is_created(&parser->msg.transfer_encoding))
+ p_array_init(&parser->msg.transfer_encoding, pool, 4);
+
+ /* RFC 7230, Section 3.3.1: Transfer-Encoding
+
+ Transfer-Encoding = 1#transfer-coding
+
+ RFC 7230, Section 4: Transfer Codings
+
+ transfer-coding = "chunked" ; RFC 7230, Section 4.1
+ / "compress" ; RFC 7230, Section 4.2.1
+ / "deflate" ; RFC 7230, Section 4.2.2
+ / "gzip" ; RFC 7230, Section 4.2.3
+ / transfer-extension
+ transfer-extension = token *( OWS ";" OWS transfer-parameter )
+ transfer-parameter = token BWS "=" BWS ( token / quoted-string )
+ */
+ http_parser_init(&hparser, data, size);
+ for (;;) {
+ /* transfer-coding */
+ if (http_parse_token(&hparser, &trenc) > 0) {
+ struct http_transfer_coding *coding;
+ bool parse_error;
+
+ coding = array_append_space(
+ &parser->msg.transfer_encoding);
+ coding->name = p_strdup(pool, trenc);
+
+ /* *( OWS ";" OWS transfer-parameter ) */
+ parse_error = FALSE;
+ for (;;) {
+ struct http_transfer_param *param;
+ const char *attribute, *value;
+
+ /* OWS ";" OWS */
+ http_parse_ows(&hparser);
+ if (hparser.cur >= hparser.end ||
+ *hparser.cur != ';')
+ break;
+ hparser.cur++;
+ http_parse_ows(&hparser);
+
+ /* attribute */
+ if (http_parse_token(&hparser,
+ &attribute) <= 0) {
+ parse_error = TRUE;
+ break;
+ }
+
+ /* BWS "=" BWS */
+ http_parse_ows(&hparser);
+ if (hparser.cur >= hparser.end ||
+ *hparser.cur != '=') {
+ parse_error = TRUE;
+ break;
+ }
+ hparser.cur++;
+ http_parse_ows(&hparser);
+
+ /* token / quoted-string */
+ if (http_parse_token_or_qstring(&hparser,
+ &value) <= 0) {
+ parse_error = TRUE;
+ break;
+ }
+
+ if (!array_is_created(&coding->parameters)) {
+ p_array_init(&coding->parameters,
+ pool, 2);
+ }
+ param = array_append_space(&coding->parameters);
+ param->attribute = p_strdup(pool, attribute);
+ param->value = p_strdup(pool, value);
+ }
+ if (parse_error)
+ break;
+
+ } else {
+ /* RFC 7230, Section 7: ABNF List Extension: #rule
+
+ For compatibility with legacy list rules, a recipient
+ MUST parse and ignore a reasonable number of empty
+ list elements: enough to handle common mistakes by
+ senders that merge values, but not so much that they
+ could be used as a denial-of-service mechanism.
+ */
+ // FIXME: limit allowed number of empty list elements
+ // FIXME: handle invalid transfer encoding
+ }
+ http_parse_ows(&hparser);
+ if (hparser.cur >= hparser.end || *hparser.cur != ',')
+ break;
+ hparser.cur++;
+ http_parse_ows(&hparser);
+ }
+
+ if (hparser.cur < hparser.end ||
+ array_count(&parser->msg.transfer_encoding) == 0) {
+ parser->error = "Invalid Transfer-Encoding header";
+ parser->error_code = HTTP_MESSAGE_PARSE_ERROR_BROKEN_MESSAGE;
+ return -1;
+ }
+ return 0;
+}
+
+static int
+http_message_parse_header(struct http_message_parser *parser,
+ const char *name, const unsigned char *data,
+ size_t size)
+{
+ const struct http_header_field *hdr;
+ pool_t pool;
+
+ pool = http_message_parser_get_pool(parser);
+ if (parser->msg.header == NULL)
+ parser->msg.header = http_header_create(pool, 32);
+ hdr = http_header_field_add(parser->msg.header, name, data, size);
+
+ /* RFC 7230, Section 3.2.2: Field Order
+
+ A sender MUST NOT generate multiple header fields with the same field
+ name in a message unless either the entire field value for that
+ header field is defined as a comma-separated list [i.e., #(values)]
+ or the header field is a well-known exception.
+ */
+
+ switch (name[0]) {
+ case 'C': case 'c':
+ /* Connection: */
+ if (strcasecmp(name, "Connection") == 0) {
+ return http_message_parse_hdr_connection(
+ parser, data, size);
+ }
+ /* Content-Length: */
+ if (strcasecmp(name, "Content-Length") == 0)
+ return http_message_parse_hdr_content_length(
+ parser, hdr);
+ break;
+ case 'D': case 'd':
+ /* Date: */
+ if (strcasecmp(name, "Date") == 0)
+ return http_message_parse_hdr_date(parser, data, size);
+ break;
+ case 'L': case 'l':
+ /* Location: */
+ if (strcasecmp(name, "Location") == 0)
+ return http_message_parse_hdr_location(parser, hdr);
+ break;
+ case 'T': case 't':
+ /* Transfer-Encoding: */
+ if (strcasecmp(name, "Transfer-Encoding") == 0) {
+ return http_message_parse_hdr_transfer_encoding(
+ parser, data, size);
+ }
+ break;
+ default:
+ break;
+ }
+ return 0;
+}
+
+static int http_message_parse_eoh(struct http_message_parser *parser)
+{
+ struct http_message *msg = &parser->msg;
+ pool_t pool;
+
+ /* EOH */
+
+ /* Create empty header if there is none */
+ pool = http_message_parser_get_pool(parser);
+ if (msg->header == NULL)
+ msg->header = http_header_create(pool, 1);
+
+ /* handle HTTP/1.0 persistence */
+ if (msg->version_major == 1 && msg->version_minor == 0 &&
+ !msg->connection_close) {
+ const char *option;
+
+ msg->connection_close = TRUE;
+ if (array_is_created(&msg->connection_options)) {
+ array_foreach_elem(&msg->connection_options, option) {
+ if (strcasecmp(option, "Keep-Alive") == 0) {
+ msg->connection_close = FALSE;
+ break;
+ }
+ }
+ }
+ }
+ return 1;
+}
+
+int http_message_parse_headers(struct http_message_parser *parser)
+{
+ const unsigned char *field_data;
+ const char *field_name, *error;
+ size_t field_size;
+ int ret;
+
+ parser->error_code = HTTP_MESSAGE_PARSE_ERROR_NONE;
+ parser->error = NULL;
+
+ /* *( header-field CRLF ) CRLF */
+ while ((ret = http_header_parse_next_field(
+ parser->header_parser, &field_name, &field_data, &field_size,
+ &error)) > 0) {
+ if (field_name == NULL)
+ return http_message_parse_eoh(parser);
+
+ if (http_message_parse_header(parser,
+ field_name, field_data, field_size) < 0)
+ return -1;
+ }
+
+ if (ret < 0) {
+ if (parser->input->eof || parser->input->stream_errno != 0) {
+ parser->error_code =
+ HTTP_MESSAGE_PARSE_ERROR_BROKEN_STREAM;
+ parser->error = "Broken stream";
+ } else {
+ parser->error_code =
+ HTTP_MESSAGE_PARSE_ERROR_BROKEN_MESSAGE;
+ parser->error = t_strdup_printf(
+ "Failed to parse header: %s", error);
+ }
+
+ }
+ return ret;
+}
+
+static const char *
+http_istream_error_callback(const struct istream_sized_error_data *data,
+ struct istream *input)
+{
+ i_assert(data->eof);
+ i_assert(data->v_offset + data->new_bytes < data->wanted_size);
+
+ return t_strdup_printf(
+ "Disconnected while reading message payload at offset %"PRIuUOFF_T
+ " (wanted %"PRIuUOFF_T"): %s", data->v_offset + data->new_bytes,
+ data->wanted_size, io_stream_get_disconnect_reason(input, NULL));
+}
+
+static int
+http_message_parse_body_coding(struct http_message_parser *parser,
+ const struct http_transfer_coding *coding,
+ bool *seen_chunked)
+{
+ if (strcasecmp(coding->name, "chunked") == 0) {
+ *seen_chunked = TRUE;
+
+ if ((parser->error_code == HTTP_MESSAGE_PARSE_ERROR_NONE)
+ && array_is_created(&coding->parameters)
+ && array_count(&coding->parameters) > 0) {
+ const struct http_transfer_param *param =
+ array_front(&coding->parameters);
+
+ parser->error_code = HTTP_MESSAGE_PARSE_ERROR_BAD_MESSAGE;
+ parser->error = t_strdup_printf(
+ "Unexpected parameter `%s' specified"
+ "for the `%s' transfer coding",
+ param->attribute, coding->name);
+ /* recoverable */
+ }
+ } else if (*seen_chunked) {
+ parser->error_code = HTTP_MESSAGE_PARSE_ERROR_BROKEN_MESSAGE;
+ parser->error = "Chunked Transfer-Encoding must be last";
+ return -1;
+ } else if (parser->error_code == HTTP_MESSAGE_PARSE_ERROR_NONE) {
+ parser->error_code = HTTP_MESSAGE_PARSE_ERROR_NOT_IMPLEMENTED;
+ parser->error = t_strdup_printf(
+ "Unknown transfer coding `%s'", coding->name);
+ /* recoverable */
+ }
+ return 0;
+}
+
+static int
+http_message_parse_body_encoded(struct http_message_parser *parser,
+ bool request)
+{
+ const struct http_transfer_coding *coding;
+ bool seen_chunked = FALSE;
+
+ array_foreach(&parser->msg.transfer_encoding, coding) {
+ if (http_message_parse_body_coding(parser, coding,
+ &seen_chunked) < 0)
+ return -1;
+ }
+
+ if (seen_chunked) {
+ parser->payload = http_transfer_chunked_istream_create(
+ parser->input, parser->max_payload_size);
+ } else if (!request) {
+ /* RFC 7230, Section 3.3.3: Message Body Length
+
+ If a Transfer-Encoding header field is present in a response
+ and the chunked transfer coding is not the final encoding,
+ the message body length is determined by reading the
+ connection until it is closed by the server.
+ */
+ /* FIXME: enforce max payload size (relevant to http-client
+ only) */
+ parser->payload =
+ i_stream_create_limit(parser->input, SIZE_MAX);
+ } else {
+ /* RFC 7230, Section 3.3.3: Message Body Length
+
+ If a Transfer-Encoding header field is present in a request
+ and the chunked transfer coding is not the final encoding,
+ the message body length cannot be determined reliably; the
+ server MUST respond with the 400 (Bad Request) status code
+ and then close the connection.
+ */
+ parser->error_code = HTTP_MESSAGE_PARSE_ERROR_BROKEN_MESSAGE;
+ parser->error =
+ "Final Transfer-Encoding in request is not chunked";
+ return -1;
+ }
+
+ /* RFC 7230, Section 3.3.3: Message Body Length
+
+ If a message is received with both a Transfer-Encoding and a
+ Content-Length header field, the Transfer-Encoding overrides the
+ Content-Length. Such a message might indicate an attempt to perform
+ request smuggling (Section 9.5 of [RFC7230]) or response splitting
+ (Section 9.4 of [RFC7230]) and ought to be handled as an error. A
+ sender MUST remove the received Content-Length field prior to
+ forwarding such a message downstream.
+ */
+ // FIXME: make this an error?
+ if (parser->msg.have_content_length)
+ http_header_field_delete(parser->msg.header, "Content-Length");
+
+ return 0;
+}
+
+static int http_message_parse_body_sized(struct http_message_parser *parser)
+{
+ struct istream *input;
+
+ if (parser->max_payload_size > 0
+ && parser->msg.content_length > parser->max_payload_size) {
+ parser->error_code = HTTP_MESSAGE_PARSE_ERROR_PAYLOAD_TOO_LARGE;
+ parser->error = "Payload is too large";
+ return -1;
+ }
+
+ /* Got explicit message size from Content-Length: header */
+ input = i_stream_create_limit(parser->input,
+ parser->msg.content_length);
+ /* Make sure we return failure if HTTP connection closes before we've
+ finished reading the full input. */
+ parser->payload = i_stream_create_sized_with_callback(input,
+ parser->msg.content_length,
+ http_istream_error_callback, input);
+ i_stream_unref(&input);
+ return 0;
+}
+
+static int http_message_parse_body_closed(struct http_message_parser *parser)
+{
+ /* RFC 7230, Section 3.3.3: Message Body Length
+
+ 6. If this is a request message and none of the above are true, then
+ the message body length is zero (no message body is present).
+
+ 7. Otherwise, this is a response message without a declared message
+ body length, so the message body length is determined by the
+ number of octets received prior to the server closing the
+ connection
+ */
+ // FIXME: enforce max payload size (relevant to http-client only)
+ // FIXME: handle request case correctly.
+ parser->payload = i_stream_create_limit(parser->input, SIZE_MAX);
+ return 0;
+}
+
+int http_message_parse_body(struct http_message_parser *parser, bool request)
+{
+ i_assert(parser->payload == NULL);
+
+ parser->error_code = HTTP_MESSAGE_PARSE_ERROR_NONE;
+ parser->error = NULL;
+
+ if (array_is_created(&parser->msg.transfer_encoding)) {
+ if (http_message_parse_body_encoded(parser, request) < 0)
+ return -1;
+ } else if (parser->msg.content_length > 0) {
+ if (http_message_parse_body_sized(parser) < 0)
+ return -1;
+ } else if (!parser->msg.have_content_length && !request) {
+ if (http_message_parse_body_closed(parser) < 0)
+ return -1;
+ }
+ if (parser->error_code != HTTP_MESSAGE_PARSE_ERROR_NONE)
+ return -1;
+ return 0;
+}
diff --git a/src/lib-http/http-message-parser.h b/src/lib-http/http-message-parser.h
new file mode 100644
index 0000000..17c17cb
--- /dev/null
+++ b/src/lib-http/http-message-parser.h
@@ -0,0 +1,77 @@
+#ifndef HTTP_MESSAGE_PARSER_H
+#define HTTP_MESSAGE_PARSER_H
+
+#include "http-response.h"
+#include "http-transfer.h"
+
+#include "http-header.h"
+
+enum http_message_parse_error {
+ HTTP_MESSAGE_PARSE_ERROR_NONE = 0, /* no error */
+ HTTP_MESSAGE_PARSE_ERROR_BROKEN_STREAM, /* stream error */
+ HTTP_MESSAGE_PARSE_ERROR_BROKEN_MESSAGE, /* unrecoverable generic error */
+ HTTP_MESSAGE_PARSE_ERROR_BAD_MESSAGE, /* recoverable generic error */
+ HTTP_MESSAGE_PARSE_ERROR_NOT_IMPLEMENTED, /* used unimplemented feature
+ (recoverable) */
+ HTTP_MESSAGE_PARSE_ERROR_PAYLOAD_TOO_LARGE /* message payload is too large
+ (fatal) */
+};
+
+enum http_message_parse_flags {
+ /* Strictly adhere to the HTTP protocol specification */
+ HTTP_MESSAGE_PARSE_FLAG_STRICT = BIT(0)
+};
+
+struct http_message {
+ pool_t pool;
+
+ unsigned int version_major;
+ unsigned int version_minor;
+
+ struct http_header *header;
+
+ time_t date;
+ uoff_t content_length;
+ const char *location;
+ ARRAY_TYPE(http_transfer_coding) transfer_encoding;
+ ARRAY_TYPE(const_string) connection_options;
+
+ bool connection_close:1;
+ bool have_content_length:1;
+};
+
+struct http_message_parser {
+ struct istream *input;
+
+ struct http_header_limits header_limits;
+ uoff_t max_payload_size;
+ enum http_message_parse_flags flags;
+
+ const unsigned char *begin, *cur, *end;
+
+ const char *error;
+ enum http_message_parse_error error_code;
+
+ struct http_header_parser *header_parser;
+ struct istream *payload;
+
+ pool_t msg_pool;
+ struct http_message msg;
+};
+
+void http_message_parser_init(struct http_message_parser *parser,
+ struct istream *input, const struct http_header_limits *hdr_limits,
+ uoff_t max_payload_size, enum http_message_parse_flags flags)
+ ATTR_NULL(3);
+void http_message_parser_deinit(struct http_message_parser *parser);
+void http_message_parser_restart(struct http_message_parser *parser,
+ pool_t pool);
+
+pool_t http_message_parser_get_pool(struct http_message_parser *parser);
+
+int http_message_parse_finish_payload(struct http_message_parser *parser);
+int http_message_parse_version(struct http_message_parser *parser);
+int http_message_parse_headers(struct http_message_parser *parser);
+int http_message_parse_body(struct http_message_parser *parser, bool request);
+
+#endif
diff --git a/src/lib-http/http-parser.c b/src/lib-http/http-parser.c
new file mode 100644
index 0000000..7cd6c2a
--- /dev/null
+++ b/src/lib-http/http-parser.c
@@ -0,0 +1,208 @@
+/* Copyright (c) 2013-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "net.h"
+#include "str.h"
+#include "strescape.h"
+#include "http-url.h"
+
+#include "http-parser.h"
+
+/*
+ Character definitions:
+
+ tchar = "!" / "#" / "$" / "%" / "&" / "'" / "*"
+ / "+" / "-" / "." / "^" / "_" / "`" / "|" / "~"
+ / DIGIT / ALPHA
+ ; any VCHAR, except special
+ special = "(" / ")" / "<" / ">" / "@" / ","
+ / ";" / ":" / "\" / DQUOTE / "/" / "["
+ / "]" / "?" / "=" / "{" / "}"
+ qdtext = OWS / %x21 / %x23-5B / %x5D-7E / obs-text
+ qdtext-nf = HTAB / SP / %x21 / %x23-5B / %x5D-7E / obs-text
+ ctext = OWS / %x21-27 / %x2A-5B / %x5D-7E / obs-text
+ obs-text = %x80-FF
+ OWS = *( SP / HTAB )
+ VCHAR = %x21-7E
+ 't68char' = ALPHA / DIGIT / "-" / "." / "_" / "~" / "+" / "/"
+
+ 'text' = ( HTAB / SP / VCHAR / obs-text )
+
+ Character bit mappings:
+
+ (1<<0) => ALPHA / DIGIT / "-" / "." / "_" / "~" / "+"
+ (1<<1) => "!" / "#" / "$" / "%" / "&" / "'" / "*" / "^" / "`" / "|"
+ (1<<2) => special
+ (1<<3) => %x21 / %x2A-5B / %x5D-7E
+ (1<<4) => %x23-29
+ (1<<5) => %x22-27
+ (1<<6) => HTAB / SP / obs-text
+ (1<<7) => "/"
+ */
+
+const unsigned char _http_token_char_mask = (1<<0)|(1<<1);
+const unsigned char _http_value_char_mask = (1<<0)|(1<<1)|(1<<2);
+const unsigned char _http_text_char_mask = (1<<0)|(1<<1)|(1<<2)|(1<<6);
+const unsigned char _http_qdtext_char_mask = (1<<3)|(1<<4)|(1<<6);
+const unsigned char _http_ctext_char_mask = (1<<3)|(1<<5)|(1<<6);
+const unsigned char _http_token68_char_mask = (1<<0)|(1<<7);
+
+const unsigned char _http_char_lookup[256] = {
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 64, 0, 0, 0, 0, 0, 0, // 00
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 10
+ 64, 10, 36, 50, 50, 50, 50, 50, 20, 20, 10, 9, 12, 9, 9, 140, // 20
+ 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 12, 12, 12, 12, 12, 12, // 30
+ 12, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, // 40
+ 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 12, 4, 12, 10, 9, // 50
+ 10, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, // 60
+ 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 12, 10, 12, 9, 0, // 70
+
+ 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, // 80
+ 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, // 90
+ 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, // A0
+ 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, // B0
+ 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, // C0
+ 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, // D0
+ 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, // E0
+ 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, // F0
+};
+
+/*
+ * HTTP value parsing
+ */
+
+void http_parser_init(struct http_parser *parser,
+ const unsigned char *data, size_t size)
+{
+ i_zero(parser);
+ parser->begin = data;
+ parser->cur = data;
+ parser->end = data + size;
+}
+
+void http_parse_ows(struct http_parser *parser)
+{
+ /* OWS = *( SP / HTAB ) */
+ if (parser->cur >= parser->end)
+ return;
+ while (parser->cur < parser->end &&
+ (parser->cur[0] == ' ' || parser->cur[0] == '\t')) {
+ parser->cur++;
+ }
+}
+
+int http_parser_skip_token(struct http_parser *parser)
+{
+ /* token = 1*tchar */
+
+ if (parser->cur >= parser->end || !http_char_is_token(*parser->cur))
+ return 0;
+ parser->cur++;
+
+ while (parser->cur < parser->end && http_char_is_token(*parser->cur))
+ parser->cur++;
+ return 1;
+}
+
+int http_parse_token(struct http_parser *parser, const char **token_r)
+{
+ const unsigned char *first = parser->cur;
+ int ret;
+
+ if ((ret=http_parser_skip_token(parser)) <= 0)
+ return ret;
+ *token_r = t_strndup(first, parser->cur - first);
+ return 1;
+}
+
+int http_parse_token_list_next(struct http_parser *parser,
+ const char **token_r)
+{
+ /* http://tools.ietf.org/html/draft-ietf-httpbis-p1-messaging-21;
+ Appendix B:
+
+ For compatibility with legacy list rules, recipients SHOULD accept
+ empty list elements. In other words, consumers would follow the list
+ productions:
+
+ #element => [ ( "," / element ) *( OWS "," [ OWS element ] ) ]
+ 1#element => *( "," OWS ) element *( OWS "," [ OWS element ] )
+ */
+
+ for (;;) {
+ if (http_parse_token(parser, token_r) > 0)
+ break;
+ http_parse_ows(parser);
+ if (parser->cur >= parser->end || parser->cur[0] != ',')
+ return 0;
+ parser->cur++;
+ http_parse_ows(parser);
+ }
+
+ return 1;
+}
+
+int http_parse_quoted_string(struct http_parser *parser, const char **str_r)
+{
+ string_t *str;
+
+ /* quoted-string = DQUOTE *( qdtext / quoted-pair ) DQUOTE
+ qdtext = HTAB / SP / "!" / %x23-5B ; '#'-'['
+ / %x5D-7E ; ']'-'~'
+ / obs-text
+ quoted-pair = "\" ( HTAB / SP / VCHAR / obs-text )
+ obs-text = %x80-FF
+ */
+
+ /* DQUOTE */
+ if (parser->cur >= parser->end || parser->cur[0] != '"')
+ return 0;
+ parser->cur++;
+
+ /* *( qdtext / quoted-pair ) */
+ str = t_str_new(256);
+ for (;;) {
+ const unsigned char *first;
+
+ /* *qdtext */
+ first = parser->cur;
+ while (parser->cur < parser->end && http_char_is_qdtext(*parser->cur))
+ parser->cur++;
+
+ if (parser->cur >= parser->end)
+ return -1;
+
+ str_append_data(str, first, parser->cur - first);
+
+ /* DQUOTE */
+ if (*parser->cur == '"') {
+ parser->cur++;
+ break;
+
+ /* "\" */
+ } else if (*parser->cur == '\\') {
+ parser->cur++;
+
+ if (parser->cur >= parser->end || !http_char_is_text(*parser->cur))
+ return -1;
+ str_append_c(str, *parser->cur);
+ parser->cur++;
+
+ /* ERROR */
+ } else {
+ return -1;
+ }
+ }
+ *str_r = str_c(str);
+ return 1;
+}
+
+int http_parse_token_or_qstring(struct http_parser *parser,
+ const char **word_r)
+{
+ if (parser->cur >= parser->end)
+ return 0;
+ if (parser->cur[0] == '"')
+ return http_parse_quoted_string(parser, word_r);
+ return http_parse_token(parser, word_r);
+}
diff --git a/src/lib-http/http-parser.h b/src/lib-http/http-parser.h
new file mode 100644
index 0000000..915e484
--- /dev/null
+++ b/src/lib-http/http-parser.h
@@ -0,0 +1,63 @@
+#ifndef HTTP_PARSER_H
+#define HTTP_PARSER_H
+
+/*
+ * Character definitions
+ */
+
+extern const unsigned char _http_token_char_mask;
+extern const unsigned char _http_value_char_mask;
+extern const unsigned char _http_text_char_mask;
+extern const unsigned char _http_qdtext_char_mask;
+extern const unsigned char _http_ctext_char_mask;
+extern const unsigned char _http_token68_char_mask;
+
+extern const unsigned char _http_char_lookup[256];
+
+static inline bool http_char_is_token(unsigned char ch) {
+ return (_http_char_lookup[ch] & _http_token_char_mask) != 0;
+}
+
+static inline bool http_char_is_value(unsigned char ch) {
+ return (_http_char_lookup[ch] & _http_value_char_mask) != 0;
+}
+
+static inline bool http_char_is_text(unsigned char ch) {
+ return (_http_char_lookup[ch] & _http_text_char_mask) != 0;
+}
+
+static inline bool http_char_is_qdtext(unsigned char ch) {
+ return (_http_char_lookup[ch] & _http_qdtext_char_mask) != 0;
+}
+
+static inline bool http_char_is_ctext(unsigned char ch) {
+ return (_http_char_lookup[ch] & _http_ctext_char_mask) != 0;
+}
+
+static inline bool http_char_is_token68(unsigned char ch) {
+ return (_http_char_lookup[ch] & _http_token68_char_mask) != 0;
+}
+
+/*
+ * HTTP value parsing
+ */
+
+struct http_parser {
+ const unsigned char *begin, *cur, *end;
+};
+
+void http_parser_init(struct http_parser *parser,
+ const unsigned char *data, size_t size);
+
+void http_parse_ows(struct http_parser *parser);
+
+int http_parser_skip_token(struct http_parser *parser);
+int http_parse_token(struct http_parser *parser, const char **token_r);
+int http_parse_token_list_next(struct http_parser *parser,
+ const char **token_r);
+
+int http_parse_quoted_string(struct http_parser *parser, const char **str_r);
+int http_parse_token_or_qstring(struct http_parser *parser,
+ const char **word_r);
+
+#endif
diff --git a/src/lib-http/http-request-parser.c b/src/lib-http/http-request-parser.c
new file mode 100644
index 0000000..8cb44ba
--- /dev/null
+++ b/src/lib-http/http-request-parser.c
@@ -0,0 +1,635 @@
+/* Copyright (c) 2013-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "istream.h"
+#include "http-url.h"
+#include "http-parser.h"
+#include "http-message-parser.h"
+#include "http-request-parser.h"
+
+#define HTTP_REQUEST_PARSER_MAX_METHOD_LENGTH 32
+
+enum http_request_parser_state {
+ HTTP_REQUEST_PARSE_STATE_INIT = 0,
+ HTTP_REQUEST_PARSE_STATE_SKIP_LINE,
+ HTTP_REQUEST_PARSE_STATE_METHOD,
+ HTTP_REQUEST_PARSE_STATE_SP1,
+ HTTP_REQUEST_PARSE_STATE_TARGET,
+ HTTP_REQUEST_PARSE_STATE_SP2,
+ HTTP_REQUEST_PARSE_STATE_VERSION,
+ HTTP_REQUEST_PARSE_STATE_CR,
+ HTTP_REQUEST_PARSE_STATE_LF,
+ HTTP_REQUEST_PARSE_STATE_HEADER
+};
+
+struct http_request_parser {
+ struct http_message_parser parser;
+ pool_t pool;
+
+ enum http_request_parser_state state;
+
+ struct http_url *default_base_url;
+
+ uoff_t max_target_length;
+
+ enum http_request_parse_error error_code;
+
+ const char *request_method;
+ const char *request_target;
+
+ bool skipping_line:1;
+};
+
+struct http_request_parser *
+http_request_parser_init(struct istream *input,
+ const struct http_url *default_base_url,
+ const struct http_request_limits *limits,
+ enum http_request_parse_flags flags)
+{
+ struct http_request_parser *parser;
+ pool_t pool;
+ struct http_header_limits hdr_limits;
+ uoff_t max_payload_size;
+ enum http_message_parse_flags msg_flags = 0;
+
+ pool = pool_alloconly_create("http request parser", 1024);
+ parser = p_new(pool, struct http_request_parser, 1);
+ parser->pool = pool;
+
+ if (default_base_url != NULL) {
+ parser->default_base_url =
+ http_url_clone_authority(pool, default_base_url);
+ }
+
+ if (limits != NULL) {
+ hdr_limits = limits->header;
+ max_payload_size = limits->max_payload_size;
+ } else {
+ i_zero(&hdr_limits);
+ max_payload_size = 0;
+ }
+
+ /* substitute default limits */
+ if (parser->max_target_length == 0)
+ parser->max_target_length = HTTP_REQUEST_DEFAULT_MAX_TARGET_LENGTH;
+ if (hdr_limits.max_size == 0)
+ hdr_limits.max_size = HTTP_REQUEST_DEFAULT_MAX_HEADER_SIZE;
+ if (hdr_limits.max_field_size == 0)
+ hdr_limits.max_field_size = HTTP_REQUEST_DEFAULT_MAX_HEADER_FIELD_SIZE;
+ if (hdr_limits.max_fields == 0)
+ hdr_limits.max_fields = HTTP_REQUEST_DEFAULT_MAX_HEADER_FIELDS;
+ if (max_payload_size == 0)
+ max_payload_size = HTTP_REQUEST_DEFAULT_MAX_PAYLOAD_SIZE;
+
+ if ((flags & HTTP_REQUEST_PARSE_FLAG_STRICT) != 0)
+ msg_flags |= HTTP_MESSAGE_PARSE_FLAG_STRICT;
+ http_message_parser_init(&parser->parser, input,
+ &hdr_limits, max_payload_size, msg_flags);
+ return parser;
+}
+
+void http_request_parser_deinit(struct http_request_parser **_parser)
+{
+ struct http_request_parser *parser = *_parser;
+
+ *_parser = NULL;
+
+ http_message_parser_deinit(&parser->parser);
+ pool_unref(&parser->pool);
+}
+
+static void
+http_request_parser_restart(struct http_request_parser *parser,
+ pool_t pool)
+{
+ http_message_parser_restart(&parser->parser, pool);
+ parser->request_method = NULL;
+ parser->request_target = NULL;
+}
+
+static int http_request_parse_method(struct http_request_parser *parser)
+{
+ const unsigned char *p = parser->parser.cur;
+ pool_t pool;
+
+ /* method = token
+ */
+ while (p < parser->parser.end && http_char_is_token(*p))
+ p++;
+
+ if ((p - parser->parser.cur) > HTTP_REQUEST_PARSER_MAX_METHOD_LENGTH) {
+ parser->error_code = HTTP_REQUEST_PARSE_ERROR_METHOD_TOO_LONG;
+ parser->parser.error = "HTTP request method is too long";
+ return -1;
+ }
+ if (p == parser->parser.end)
+ return 0;
+ pool = http_message_parser_get_pool(&parser->parser);
+ parser->request_method =
+ p_strdup_until(pool, parser->parser.cur, p);
+ parser->parser.cur = p;
+ return 1;
+}
+
+static int http_request_parse_target(struct http_request_parser *parser)
+{
+ struct http_message_parser *_parser = &parser->parser;
+ const unsigned char *p = parser->parser.cur;
+ pool_t pool;
+
+ /* We'll just parse anything up to the first SP or a control char.
+ We could also implement workarounds for buggy HTTP clients and
+ parse anything up to the HTTP-version and return 301 with the
+ target properly encoded (FIXME). */
+ while (p < _parser->end && *p > ' ')
+ p++;
+
+ /* target is too long when explicit limit is exceeded or when input buffer
+ runs out of space */
+ /* FIXME: put limit on full request line rather than target and method
+ separately */
+ /* FIXME: is it wise to keep target in stream buffer? It can become very
+ large for some applications, increasing the stream buffer size */
+ if ((uoff_t)(p - _parser->cur) > parser->max_target_length ||
+ (p == _parser->end && ((uoff_t)(p - _parser->cur) >=
+ i_stream_get_max_buffer_size(_parser->input)))) {
+ parser->error_code = HTTP_REQUEST_PARSE_ERROR_TARGET_TOO_LONG;
+ parser->parser.error = "HTTP request target is too long";
+ return -1;
+ }
+ if (p == _parser->end)
+ return 0;
+ pool = http_message_parser_get_pool(_parser);
+ parser->request_target = p_strdup_until(pool, _parser->cur, p);
+ parser->parser.cur = p;
+ return 1;
+}
+
+static inline const char *_chr_sanitize(unsigned char c)
+{
+ if (c >= 0x20 && c < 0x7F)
+ return t_strdup_printf("`%c'", c);
+ if (c == 0x0a)
+ return "<LF>";
+ if (c == 0x0d)
+ return "<CR>";
+ return t_strdup_printf("<0x%02x>", c);
+}
+
+static int http_request_parse(struct http_request_parser *parser,
+ pool_t pool)
+{
+ struct http_message_parser *_parser = &parser->parser;
+ int ret;
+
+ /* RFC 7230, Section 3.1.1: Request Line
+
+ request-line = method SP request-target SP HTTP-version CRLF
+ method = token
+ */
+ for (;;) {
+ switch (parser->state) {
+ case HTTP_REQUEST_PARSE_STATE_INIT:
+ http_request_parser_restart(parser, pool);
+ parser->state = HTTP_REQUEST_PARSE_STATE_SKIP_LINE;
+ if (_parser->cur == _parser->end)
+ return 0;
+ /* fall through */
+ case HTTP_REQUEST_PARSE_STATE_SKIP_LINE:
+ if (*_parser->cur == '\r' || *_parser->cur == '\n') {
+ if (parser->skipping_line) {
+ /* second extra CRLF; not allowed */
+ parser->error_code = HTTP_REQUEST_PARSE_ERROR_BROKEN_REQUEST;
+ _parser->error = "Empty request line";
+ return -1;
+ }
+ /* HTTP/1.0 client sent one extra CRLF after body.
+ ignore it. */
+ parser->skipping_line = TRUE;
+ parser->state = HTTP_REQUEST_PARSE_STATE_CR;
+ break;
+ }
+ parser->state = HTTP_REQUEST_PARSE_STATE_METHOD;
+ parser->skipping_line = FALSE;
+ /* fall through */
+ case HTTP_REQUEST_PARSE_STATE_METHOD:
+ if ((ret=http_request_parse_method(parser)) <= 0)
+ return ret;
+ parser->state = HTTP_REQUEST_PARSE_STATE_SP1;
+ if (_parser->cur == _parser->end)
+ return 0;
+ /* fall through */
+ case HTTP_REQUEST_PARSE_STATE_SP1:
+ if (*_parser->cur != ' ') {
+ parser->error_code = HTTP_REQUEST_PARSE_ERROR_BROKEN_REQUEST;
+ _parser->error = t_strdup_printf
+ ("Unexpected character %s in request method",
+ _chr_sanitize(*_parser->cur));
+ return -1;
+ }
+ _parser->cur++;
+ parser->state = HTTP_REQUEST_PARSE_STATE_TARGET;
+ if (_parser->cur >= _parser->end)
+ return 0;
+ /* fall through */
+ case HTTP_REQUEST_PARSE_STATE_TARGET:
+ if ((ret=http_request_parse_target(parser)) <= 0)
+ return ret;
+ parser->state = HTTP_REQUEST_PARSE_STATE_SP2;
+ if (_parser->cur == _parser->end)
+ return 0;
+ /* fall through */
+ case HTTP_REQUEST_PARSE_STATE_SP2:
+ if (*_parser->cur != ' ') {
+ parser->error_code = HTTP_REQUEST_PARSE_ERROR_BROKEN_REQUEST;
+ _parser->error = t_strdup_printf
+ ("Unexpected character %s in request target",
+ _chr_sanitize(*_parser->cur));
+ return -1;
+ }
+ _parser->cur++;
+ parser->state = HTTP_REQUEST_PARSE_STATE_VERSION;
+ if (_parser->cur >= _parser->end)
+ return 0;
+ /* fall through */
+ case HTTP_REQUEST_PARSE_STATE_VERSION:
+ if ((ret=http_message_parse_version(&parser->parser)) <= 0) {
+ if (ret < 0) {
+ parser->error_code = HTTP_REQUEST_PARSE_ERROR_BROKEN_REQUEST;
+ _parser->error = "Invalid HTTP version in request";
+ }
+ return ret;
+ }
+ parser->state = HTTP_REQUEST_PARSE_STATE_CR;
+ if (_parser->cur == _parser->end)
+ return 0;
+ /* fall through */
+ case HTTP_REQUEST_PARSE_STATE_CR:
+ if (*_parser->cur == '\r')
+ _parser->cur++;
+ parser->state = HTTP_REQUEST_PARSE_STATE_LF;
+ if (_parser->cur == _parser->end)
+ return 0;
+ /* fall through */
+ case HTTP_REQUEST_PARSE_STATE_LF:
+ if (*_parser->cur != '\n') {
+ parser->error_code = HTTP_REQUEST_PARSE_ERROR_BROKEN_REQUEST;
+ _parser->error = t_strdup_printf
+ ("Unexpected character %s at end of request line",
+ _chr_sanitize(*_parser->cur));
+ return -1;
+ }
+ _parser->cur++;
+ if (!parser->skipping_line) {
+ parser->state = HTTP_REQUEST_PARSE_STATE_HEADER;
+ return 1;
+ }
+ parser->state = HTTP_REQUEST_PARSE_STATE_INIT;
+ break;
+ case HTTP_REQUEST_PARSE_STATE_HEADER:
+ default:
+ i_unreached();
+ }
+ }
+
+ i_unreached();
+ return -1;
+}
+
+static int http_request_parse_request_line(struct http_request_parser *parser,
+ pool_t pool)
+{
+ struct http_message_parser *_parser = &parser->parser;
+ const unsigned char *begin;
+ size_t size, old_bytes = 0;
+ int ret;
+
+ while ((ret = i_stream_read_bytes(_parser->input, &begin, &size,
+ old_bytes + 1)) > 0) {
+ _parser->begin = _parser->cur = begin;
+ _parser->end = _parser->begin + size;
+
+ if ((ret = http_request_parse(parser, pool)) < 0)
+ return -1;
+
+ i_stream_skip(_parser->input, _parser->cur - begin);
+ if (ret > 0)
+ return 1;
+ old_bytes = i_stream_get_data_size(_parser->input);
+ }
+
+ if (ret == -2) {
+ parser->error_code = HTTP_REQUEST_PARSE_ERROR_BROKEN_REQUEST;
+ _parser->error = "HTTP request line is too long";
+ return -1;
+ }
+ if (ret < 0) {
+ if (_parser->input->eof &&
+ parser->state == HTTP_REQUEST_PARSE_STATE_INIT)
+ return 0;
+ parser->error_code = HTTP_REQUEST_PARSE_ERROR_BROKEN_STREAM;
+ _parser->error = "Broken stream";
+ return -1;
+ }
+ return 0;
+}
+
+static inline enum http_request_parse_error
+http_request_parser_message_error(struct http_request_parser *parser)
+{
+ switch (parser->parser.error_code) {
+ case HTTP_MESSAGE_PARSE_ERROR_BROKEN_STREAM:
+ return HTTP_REQUEST_PARSE_ERROR_BROKEN_STREAM;
+ case HTTP_MESSAGE_PARSE_ERROR_BAD_MESSAGE:
+ return HTTP_REQUEST_PARSE_ERROR_BAD_REQUEST;
+ case HTTP_MESSAGE_PARSE_ERROR_NOT_IMPLEMENTED:
+ return HTTP_REQUEST_PARSE_ERROR_NOT_IMPLEMENTED;
+ case HTTP_MESSAGE_PARSE_ERROR_PAYLOAD_TOO_LARGE:
+ return HTTP_REQUEST_PARSE_ERROR_PAYLOAD_TOO_LARGE;
+ case HTTP_MESSAGE_PARSE_ERROR_BROKEN_MESSAGE:
+ return HTTP_REQUEST_PARSE_ERROR_BROKEN_REQUEST;
+ default:
+ break;
+ }
+ i_unreached();
+ return HTTP_REQUEST_PARSE_ERROR_BROKEN_REQUEST;
+}
+
+bool http_request_parser_pending_payload(
+ struct http_request_parser *parser)
+{
+ if (parser->parser.payload == NULL)
+ return FALSE;
+ return i_stream_have_bytes_left(parser->parser.payload);
+}
+
+static int
+http_request_parse_expect_header(struct http_request_parser *parser,
+ struct http_request *request, const struct http_header_field *hdr)
+{
+ struct http_message_parser *_parser = &parser->parser;
+ struct http_parser hparser;
+ bool parse_error = FALSE;
+ unsigned int num_expectations = 0;
+
+ /* RFC 7231, Section 5.1.1:
+
+ Expect = "100-continue"
+ */
+ // FIXME: simplify; RFC 7231 discarded Expect extension mechanism
+ http_parser_init(&hparser, (const unsigned char *)hdr->value, hdr->size);
+ while (!parse_error) {
+ const char *expect_name, *expect_value;
+
+ /* expect-name */
+ if (http_parse_token(&hparser, &expect_name) > 0) {
+ num_expectations++;
+ if (strcasecmp(expect_name, "100-continue") == 0) {
+ request->expect_100_continue = TRUE;
+ } else {
+ if (parser->error_code == HTTP_REQUEST_PARSE_ERROR_NONE) {
+ parser->error_code = HTTP_REQUEST_PARSE_ERROR_EXPECTATION_FAILED;
+ _parser->error = t_strdup_printf
+ ("Unknown Expectation `%s'", expect_name);
+ }
+ }
+
+ /* BWS "=" BWS */
+ http_parse_ows(&hparser);
+ if (hparser.cur >= hparser.end)
+ break;
+
+ if (*hparser.cur == '=') {
+ hparser.cur++;
+ http_parse_ows(&hparser);
+
+ /* value */
+ if (http_parse_token_or_qstring(&hparser, &expect_value) <= 0) {
+ parse_error = TRUE;
+ break;
+ }
+
+ if (parser->error_code == HTTP_REQUEST_PARSE_ERROR_NONE) {
+ parser->error_code = HTTP_REQUEST_PARSE_ERROR_EXPECTATION_FAILED;
+ _parser->error = t_strdup_printf
+ ("Expectation `%s' has unexpected value", expect_name);
+ }
+ }
+
+ /* *( OWS ";" [ OWS expect-param ] ) */
+ while (!parse_error) {
+ const char *attribute, *value;
+
+ /* OWS ";" */
+ http_parse_ows(&hparser);
+ if (hparser.cur >= hparser.end || *hparser.cur != ';')
+ break;
+ hparser.cur++;
+ http_parse_ows(&hparser);
+
+ /* expect-param */
+ if (http_parse_token(&hparser, &attribute) <= 0) {
+ parse_error = TRUE;
+ break;
+ }
+
+ /* BWS "=" BWS */
+ http_parse_ows(&hparser);
+ if (hparser.cur >= hparser.end || *hparser.cur != '=') {
+ parse_error = TRUE;
+ break;
+ }
+ hparser.cur++;
+ http_parse_ows(&hparser);
+
+ /* value */
+ if (http_parse_token_or_qstring(&hparser, &value) <= 0) {
+ parse_error = TRUE;
+ break;
+ }
+
+ if (parser->error_code == HTTP_REQUEST_PARSE_ERROR_NONE) {
+ parser->error_code = HTTP_REQUEST_PARSE_ERROR_EXPECTATION_FAILED;
+ _parser->error = t_strdup_printf
+ ("Expectation `%s' has unknown parameter `'%s'",
+ expect_name, attribute);
+ }
+ }
+ if (parse_error)
+ break;
+ }
+ http_parse_ows(&hparser);
+ if (hparser.cur >= hparser.end || *hparser.cur != ',')
+ break;
+ hparser.cur++;
+ http_parse_ows(&hparser);
+ }
+
+ if (parse_error || hparser.cur < hparser.end) {
+ parser->error_code = HTTP_REQUEST_PARSE_ERROR_BAD_REQUEST;
+ _parser->error = "Invalid Expect header";
+ return -1;
+ }
+
+ if (parser->error_code != HTTP_REQUEST_PARSE_ERROR_NONE)
+ return -1;
+
+ if (num_expectations == 0) {
+ parser->error_code = HTTP_REQUEST_PARSE_ERROR_BAD_REQUEST;
+ _parser->error = "Empty Expect header";
+ return -1;
+ }
+ return 0;
+}
+
+static int
+http_request_parse_headers(struct http_request_parser *parser,
+ struct http_request *request)
+{
+ const ARRAY_TYPE(http_header_field) *hdrs;
+ const struct http_header_field *hdr;
+
+ hdrs = http_header_get_fields(parser->parser.msg.header);
+ array_foreach(hdrs, hdr) {
+ int ret = 0;
+
+ /* Expect: */
+ if (http_header_field_is(hdr, "Expect"))
+ ret = http_request_parse_expect_header(parser, request, hdr);
+
+ if (ret < 0)
+ return -1;
+ }
+ return 0;
+}
+
+int http_request_parse_finish_payload(
+ struct http_request_parser *parser,
+ enum http_request_parse_error *error_code_r,
+ const char **error_r)
+{
+ int ret;
+
+ *error_code_r = parser->error_code = HTTP_REQUEST_PARSE_ERROR_NONE;
+ *error_r = parser->parser.error = NULL;
+
+ /* make sure we finished streaming payload from previous request
+ before we continue. */
+ if ((ret = http_message_parse_finish_payload(&parser->parser)) <= 0) {
+ if (ret < 0) {
+ *error_code_r = http_request_parser_message_error(parser);
+ *error_r = parser->parser.error;
+ }
+ }
+ return ret;
+}
+
+int http_request_parse_next(struct http_request_parser *parser,
+ pool_t pool, struct http_request *request,
+ enum http_request_parse_error *error_code_r, const char **error_r)
+{
+ const struct http_header_field *hdr;
+ const char *host_hdr, *error;
+ int ret;
+
+ /* initialize and get rid of any payload of previous request */
+ if ((ret=http_request_parse_finish_payload
+ (parser, error_code_r, error_r)) <= 0)
+ return ret;
+
+ /* RFC 7230, Section 3:
+
+ HTTP-message = start-line
+ *( header-field CRLF )
+ CRLF
+ [ message-body ]
+ */
+ if (parser->state != HTTP_REQUEST_PARSE_STATE_HEADER) {
+ ret = http_request_parse_request_line(parser, pool);
+
+ /* assign early for error reporting */
+ request->method = parser->request_method;
+ request->target_raw = parser->request_target;
+ request->version_major = parser->parser.msg.version_major;
+ request->version_minor = parser->parser.msg.version_minor;
+
+ if (ret <= 0) {
+ if (ret < 0) {
+ *error_code_r = parser->error_code;
+ *error_r = parser->parser.error;
+ }
+ return ret;
+ }
+ }
+
+ if ((ret = http_message_parse_headers(&parser->parser)) <= 0) {
+ if (ret < 0) {
+ *error_code_r = http_request_parser_message_error(parser);
+ *error_r = parser->parser.error;
+ }
+ return ret;
+ }
+
+ if (http_message_parse_body(&parser->parser, TRUE) < 0) {
+ *error_code_r = http_request_parser_message_error(parser);
+ *error_r = parser->parser.error;
+ return -1;
+ }
+ parser->state = HTTP_REQUEST_PARSE_STATE_INIT;
+
+ /* RFC 7230, Section 5.4: Host
+
+ A server MUST respond with a 400 (Bad Request) status code to any
+ HTTP/1.1 request message that lacks a Host header field and to any
+ request message that contains more than one Host header field or a
+ Host header field with an invalid field-value.
+ */
+ host_hdr = NULL;
+ if (parser->parser.msg.version_major == 1 &&
+ parser->parser.msg.version_minor > 0) {
+ if ((ret=http_header_field_find_unique(
+ parser->parser.msg.header, "Host", &hdr)) <= 0) {
+ *error_code_r = HTTP_REQUEST_PARSE_ERROR_BAD_REQUEST;
+ if (ret == 0)
+ *error_r = "Missing Host header";
+ else
+ *error_r = "Duplicate Host header";
+ return -1;
+ }
+
+ host_hdr = hdr->value;
+ }
+
+ i_zero(request);
+
+ pool = http_message_parser_get_pool(&parser->parser);
+ if (http_url_request_target_parse(parser->request_target, host_hdr,
+ parser->default_base_url, pool, &request->target, &error) < 0) {
+ *error_code_r = HTTP_REQUEST_PARSE_ERROR_BAD_REQUEST;
+ *error_r = t_strdup_printf("Bad request target `%s': %s",
+ parser->request_target, error);
+ return -1;
+ }
+
+ /* parse request-specific headers */
+ if (http_request_parse_headers(parser, request) < 0) {
+ *error_code_r = parser->error_code;
+ *error_r = parser->parser.error;
+ return -1;
+ }
+
+ request->method = parser->request_method;
+ request->target_raw = parser->request_target;
+ request->version_major = parser->parser.msg.version_major;
+ request->version_minor = parser->parser.msg.version_minor;
+ request->date = parser->parser.msg.date;
+ request->payload = parser->parser.payload;
+ request->header = parser->parser.msg.header;
+ request->connection_options = parser->parser.msg.connection_options;
+ request->connection_close = parser->parser.msg.connection_close;
+
+ /* reset this state early */
+ parser->request_method = NULL;
+ parser->request_target = NULL;
+ return 1;
+}
diff --git a/src/lib-http/http-request-parser.h b/src/lib-http/http-request-parser.h
new file mode 100644
index 0000000..3956062
--- /dev/null
+++ b/src/lib-http/http-request-parser.h
@@ -0,0 +1,43 @@
+#ifndef HTTP_REQUEST_PARSER_H
+#define HTTP_REQUEST_PARSER_H
+
+#include "http-request.h"
+
+enum http_request_parse_error {
+ HTTP_REQUEST_PARSE_ERROR_NONE = 0, /* no error */
+ HTTP_REQUEST_PARSE_ERROR_BROKEN_STREAM, /* stream error */
+ HTTP_REQUEST_PARSE_ERROR_BROKEN_REQUEST, /* unrecoverable generic error */
+ HTTP_REQUEST_PARSE_ERROR_BAD_REQUEST, /* recoverable generic error */
+ HTTP_REQUEST_PARSE_ERROR_NOT_IMPLEMENTED, /* used unimplemented feature
+ (recoverable) */
+ HTTP_REQUEST_PARSE_ERROR_EXPECTATION_FAILED, /* unknown item in Expect:
+ header (recoverable) */
+ HTTP_REQUEST_PARSE_ERROR_METHOD_TOO_LONG, /* method too long (fatal) */
+ HTTP_REQUEST_PARSE_ERROR_TARGET_TOO_LONG, /* target too long (fatal) */
+ HTTP_REQUEST_PARSE_ERROR_PAYLOAD_TOO_LARGE /* payload too large (fatal) */
+};
+
+enum http_request_parse_flags {
+ /* Strictly adhere to the HTTP protocol specification */
+ HTTP_REQUEST_PARSE_FLAG_STRICT = BIT(0)
+};
+
+struct http_request_parser *
+http_request_parser_init(struct istream *input,
+ const struct http_url *default_base_url,
+ const struct http_request_limits *limits,
+ enum http_request_parse_flags flags) ATTR_NULL(2);
+void http_request_parser_deinit(struct http_request_parser **_parser);
+
+int http_request_parse_finish_payload(
+ struct http_request_parser *parser,
+ enum http_request_parse_error *error_code_r,
+ const char **error_r);
+
+int http_request_parse_next(struct http_request_parser *parser,
+ pool_t pool, struct http_request *request,
+ enum http_request_parse_error *error_code_r, const char **error_r);
+
+bool http_request_parser_pending_payload(struct http_request_parser *parser);
+
+#endif
diff --git a/src/lib-http/http-request.c b/src/lib-http/http-request.c
new file mode 100644
index 0000000..aed9975
--- /dev/null
+++ b/src/lib-http/http-request.c
@@ -0,0 +1,32 @@
+/* Copyright (c) 2013-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "istream.h"
+
+#include "http-request.h"
+
+bool http_request_has_connection_option(const struct http_request *req,
+ const char *option)
+{
+ const char *opt;
+
+ if (!array_is_created(&req->connection_options))
+ return FALSE;
+ array_foreach_elem(&req->connection_options, opt) {
+ if (strcasecmp(opt, option) == 0)
+ return TRUE;
+ }
+ return FALSE;
+}
+
+int http_request_get_payload_size(const struct http_request *req,
+ uoff_t *size_r)
+{
+ if (req->payload == NULL) {
+ *size_r = 0;
+ return 1;
+ }
+
+ return i_stream_get_size(req->payload, TRUE, size_r);
+}
diff --git a/src/lib-http/http-request.h b/src/lib-http/http-request.h
new file mode 100644
index 0000000..feee28f
--- /dev/null
+++ b/src/lib-http/http-request.h
@@ -0,0 +1,84 @@
+#ifndef HTTP_REQUEST_H
+#define HTTP_REQUEST_H
+
+#include "http-header.h"
+
+struct http_url;
+
+#define HTTP_REQUEST_DEFAULT_MAX_TARGET_LENGTH (8 * 1024)
+#define HTTP_REQUEST_DEFAULT_MAX_HEADER_SIZE (200 * 1024)
+#define HTTP_REQUEST_DEFAULT_MAX_HEADER_FIELD_SIZE (8 * 1024)
+#define HTTP_REQUEST_DEFAULT_MAX_HEADER_FIELDS 50
+#define HTTP_REQUEST_DEFAULT_MAX_PAYLOAD_SIZE (1 * 1024 * 1024)
+
+struct http_request_limits {
+ uoff_t max_target_length;
+ uoff_t max_payload_size;
+
+ struct http_header_limits header;
+};
+
+enum http_request_target_format {
+ HTTP_REQUEST_TARGET_FORMAT_ORIGIN = 0,
+ HTTP_REQUEST_TARGET_FORMAT_ABSOLUTE,
+ HTTP_REQUEST_TARGET_FORMAT_AUTHORITY,
+ HTTP_REQUEST_TARGET_FORMAT_ASTERISK
+};
+
+struct http_request_target {
+ enum http_request_target_format format;
+ struct http_url *url;
+};
+
+struct http_request {
+ const char *method;
+
+ const char *target_raw;
+ struct http_request_target target;
+
+ unsigned char version_major;
+ unsigned char version_minor;
+
+ time_t date;
+ const struct http_header *header;
+ struct istream *payload;
+
+ ARRAY_TYPE(const_string) connection_options;
+
+ bool connection_close:1;
+ bool expect_100_continue:1;
+};
+
+static inline bool
+http_request_method_is(const struct http_request *req, const char *method)
+{
+ if (req->method == NULL)
+ return FALSE;
+
+ return (strcmp(req->method, method) == 0);
+}
+
+static inline const struct http_header_field *
+http_request_header_find(const struct http_request *req, const char *name)
+{
+ return http_header_field_find(req->header, name);
+}
+
+static inline const char *
+http_request_header_get(const struct http_request *req, const char *name)
+{
+ return http_header_field_get(req->header, name);
+}
+
+static inline const ARRAY_TYPE(http_header_field) *
+http_request_header_get_fields(const struct http_request *req)
+{
+ return http_header_get_fields(req->header);
+}
+
+bool http_request_has_connection_option(const struct http_request *req,
+ const char *option);
+int http_request_get_payload_size(const struct http_request *req,
+ uoff_t *size_r);
+
+#endif
diff --git a/src/lib-http/http-response-parser.c b/src/lib-http/http-response-parser.c
new file mode 100644
index 0000000..e381403
--- /dev/null
+++ b/src/lib-http/http-response-parser.c
@@ -0,0 +1,422 @@
+/* Copyright (c) 2013-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "str.h"
+#include "istream.h"
+#include "http-parser.h"
+#include "http-date.h"
+#include "http-message-parser.h"
+#include "http-response-parser.h"
+
+#include <ctype.h>
+
+enum http_response_parser_state {
+ HTTP_RESPONSE_PARSE_STATE_INIT = 0,
+ HTTP_RESPONSE_PARSE_STATE_VERSION,
+ HTTP_RESPONSE_PARSE_STATE_SP1,
+ HTTP_RESPONSE_PARSE_STATE_STATUS,
+ HTTP_RESPONSE_PARSE_STATE_SP2,
+ HTTP_RESPONSE_PARSE_STATE_REASON,
+ HTTP_RESPONSE_PARSE_STATE_CR,
+ HTTP_RESPONSE_PARSE_STATE_LF,
+ HTTP_RESPONSE_PARSE_STATE_HEADER
+};
+
+struct http_response_parser {
+ struct http_message_parser parser;
+ enum http_response_parser_state state;
+
+ unsigned int response_status;
+ const char *response_reason;
+
+ uoff_t response_offset;
+};
+
+struct http_response_parser *
+http_response_parser_init(struct istream *input,
+ const struct http_header_limits *hdr_limits,
+ enum http_response_parse_flags flags)
+{
+ struct http_response_parser *parser;
+ enum http_message_parse_flags msg_flags = 0;
+
+ /* FIXME: implement status line limit */
+ if ((flags & HTTP_RESPONSE_PARSE_FLAG_STRICT) != 0)
+ msg_flags |= HTTP_MESSAGE_PARSE_FLAG_STRICT;
+ parser = i_new(struct http_response_parser, 1);
+ http_message_parser_init(&parser->parser,
+ input, hdr_limits, 0, msg_flags);
+ return parser;
+}
+
+void http_response_parser_deinit(struct http_response_parser **_parser)
+{
+ struct http_response_parser *parser = *_parser;
+
+ *_parser = NULL;
+
+ http_message_parser_deinit(&parser->parser);
+ i_free(parser);
+}
+
+static void
+http_response_parser_restart(struct http_response_parser *parser)
+{
+ http_message_parser_restart(&parser->parser, NULL);
+ parser->response_status = 0;
+ parser->response_reason = NULL;
+ parser->response_offset = UOFF_T_MAX;
+}
+
+static int http_response_parse_status(struct http_response_parser *parser)
+{
+ const unsigned char *p = parser->parser.cur;
+ const size_t size = parser->parser.end - parser->parser.cur;
+
+ /* status-code = 3DIGIT
+ */
+ if (size < 3)
+ return 0;
+ if (!i_isdigit(p[0]) || !i_isdigit(p[1]) || !i_isdigit(p[2]))
+ return -1;
+ parser->response_status =
+ (p[0] - '0')*100 + (p[1] - '0')*10 + (p[2] - '0');
+ if (parser->response_status < 100 ||
+ parser->response_status >= 600)
+ return -1;
+ parser->parser.cur += 3;
+ return 1;
+}
+
+static int http_response_parse_reason(struct http_response_parser *parser)
+{
+ const unsigned char *p = parser->parser.cur;
+ pool_t pool;
+
+ /* reason-phrase = *( HTAB / SP / VCHAR / obs-text )
+ */
+ // FIXME: limit length
+ while (p < parser->parser.end && http_char_is_text(*p))
+ p++;
+
+ if (p == parser->parser.end)
+ return 0;
+ pool = http_message_parser_get_pool(&parser->parser);
+ parser->response_reason =
+ p_strdup_until(pool, parser->parser.cur, p);
+ parser->parser.cur = p;
+ return 1;
+}
+
+static const char *_reply_sanitize(struct http_message_parser *parser)
+{
+ string_t *str = t_str_new(32);
+ const unsigned char *p;
+ unsigned int i;
+ bool quote_open = FALSE;
+
+ i_assert(parser->cur < parser->end);
+ for (p = parser->cur, i = 0; p < parser->end && i < 20; p++, i++) {
+ if (*p >= 0x20 && *p < 0x7F) {
+ if (!quote_open) {
+ str_append_c(str, '`');
+ quote_open = TRUE;
+ }
+ str_append_c(str, *p);
+ } else {
+ if (quote_open) {
+ str_append_c(str, '\'');
+ quote_open = FALSE;
+ }
+ if (*p == 0x0a)
+ str_append(str, "<LF>");
+ else if (*p == 0x0d)
+ str_append(str, "<CR>");
+ else
+ str_printfa(str, "<0x%02x>", *p);
+ }
+ }
+ if (quote_open)
+ str_append_c(str, '\'');
+ return str_c(str);
+}
+
+static int http_response_parse(struct http_response_parser *parser)
+{
+ struct http_message_parser *_parser = &parser->parser;
+ int ret;
+
+ /* RFC 7230, Section 3.1.2: Status Line
+
+ status-line = HTTP-version SP status-code SP reason-phrase CRLF
+ status-code = 3DIGIT
+ reason-phrase = *( HTAB / SP / VCHAR / obs-text )
+ */
+ switch (parser->state) {
+ case HTTP_RESPONSE_PARSE_STATE_INIT:
+ parser->state = HTTP_RESPONSE_PARSE_STATE_VERSION;
+ parser->response_offset = _parser->input->v_offset +
+ (_parser->cur - _parser->begin);
+ /* fall through */
+ case HTTP_RESPONSE_PARSE_STATE_VERSION:
+ if ((ret=http_message_parse_version(_parser)) <= 0) {
+ if (ret < 0)
+ _parser->error = t_strdup_printf(
+ "Invalid HTTP version in response: %s",
+ _reply_sanitize(_parser));
+ return ret;
+ }
+ parser->state = HTTP_RESPONSE_PARSE_STATE_SP1;
+ if (_parser->cur == _parser->end)
+ return 0;
+ /* fall through */
+ case HTTP_RESPONSE_PARSE_STATE_SP1:
+ if (*_parser->cur != ' ') {
+ _parser->error = t_strdup_printf
+ ("Expected ' ' after response version, but found %s",
+ _reply_sanitize(_parser));
+ return -1;
+ }
+ _parser->cur++;
+ parser->state = HTTP_RESPONSE_PARSE_STATE_STATUS;
+ if (_parser->cur >= _parser->end)
+ return 0;
+ /* fall through */
+ case HTTP_RESPONSE_PARSE_STATE_STATUS:
+ if ((ret=http_response_parse_status(parser)) <= 0) {
+ if (ret < 0)
+ _parser->error = "Invalid HTTP status code in response";
+ return ret;
+ }
+ parser->state = HTTP_RESPONSE_PARSE_STATE_SP2;
+ if (_parser->cur == _parser->end)
+ return 0;
+ /* fall through */
+ case HTTP_RESPONSE_PARSE_STATE_SP2:
+ if (*_parser->cur != ' ') {
+ _parser->error = t_strdup_printf
+ ("Expected ' ' after response status code, but found %s",
+ _reply_sanitize(_parser));
+ return -1;
+ }
+ _parser->cur++;
+ parser->state = HTTP_RESPONSE_PARSE_STATE_REASON;
+ if (_parser->cur >= _parser->end)
+ return 0;
+ /* fall through */
+ case HTTP_RESPONSE_PARSE_STATE_REASON:
+ if ((ret=http_response_parse_reason(parser)) <= 0) {
+ i_assert(ret == 0);
+ return 0;
+ }
+ parser->state = HTTP_RESPONSE_PARSE_STATE_CR;
+ if (_parser->cur == _parser->end)
+ return 0;
+ /* fall through */
+ case HTTP_RESPONSE_PARSE_STATE_CR:
+ if (*_parser->cur == '\r')
+ _parser->cur++;
+ parser->state = HTTP_RESPONSE_PARSE_STATE_LF;
+ if (_parser->cur == _parser->end)
+ return 0;
+ /* fall through */
+ case HTTP_RESPONSE_PARSE_STATE_LF:
+ if (*_parser->cur != '\n') {
+ _parser->error = t_strdup_printf
+ ("Expected line end after response, but found %s",
+ _reply_sanitize(_parser));
+ return -1;
+ }
+ _parser->cur++;
+ parser->state = HTTP_RESPONSE_PARSE_STATE_HEADER;
+ return 1;
+ case HTTP_RESPONSE_PARSE_STATE_HEADER:
+ default:
+ break;
+ }
+
+ i_unreached();
+ return -1;
+}
+
+static int
+http_response_parse_status_line(struct http_response_parser *parser)
+{
+ struct http_message_parser *_parser = &parser->parser;
+ const unsigned char *begin;
+ size_t size, old_bytes = 0;
+ int ret;
+
+ while ((ret = i_stream_read_bytes(_parser->input, &begin, &size,
+ old_bytes + 1)) > 0) {
+ _parser->begin = _parser->cur = begin;
+ _parser->end = _parser->begin + size;
+
+ if ((ret = http_response_parse(parser)) < 0)
+ return -1;
+
+ i_stream_skip(_parser->input, _parser->cur - begin);
+ if (ret > 0)
+ return 1;
+ old_bytes = i_stream_get_data_size(_parser->input);
+ }
+
+ if (ret == -2) {
+ _parser->error = "HTTP status line is too long";
+ return -1;
+ }
+ if (ret < 0) {
+ if (_parser->input->eof &&
+ parser->state == HTTP_RESPONSE_PARSE_STATE_INIT)
+ return 0;
+ _parser->error = t_strdup_printf("Stream error: %s",
+ i_stream_get_error(_parser->input));
+ return -1;
+ }
+ return 0;
+}
+
+static int
+http_response_parse_retry_after(const char *hdrval, time_t resp_time,
+ time_t *retry_after_r)
+{
+ time_t delta;
+
+ /* RFC 7231, Section 7.1.3: Retry-After
+
+ The value of this field can be either an HTTP-date or a number of
+ seconds to delay after the response is received.
+
+ Retry-After = HTTP-date / delta-seconds
+
+ A delay-seconds value is a non-negative decimal integer, representing
+ time in seconds.
+
+ delta-seconds = 1*DIGIT
+ */
+ if (str_to_time(hdrval, &delta) >= 0) {
+ if (resp_time == (time_t)-1) {
+ return -1;
+ }
+ *retry_after_r = resp_time + delta;
+ return 0;
+ }
+
+ return (http_date_parse
+ ((const unsigned char *)hdrval, strlen(hdrval), retry_after_r) ? 0 : -1);
+}
+
+uoff_t http_response_parser_get_last_offset(struct http_response_parser *parser)
+{
+ return parser->response_offset;
+}
+
+int http_response_parse_next(struct http_response_parser *parser,
+ enum http_response_payload_type payload_type,
+ struct http_response *response, const char **error_r)
+{
+ const char *hdrval;
+ time_t retry_after = (time_t)-1;
+ int ret;
+
+ i_zero(response);
+
+ /* make sure we finished streaming payload from previous response
+ before we continue. */
+ if ((ret = http_message_parse_finish_payload(&parser->parser)) <= 0) {
+ *error_r = parser->parser.error;
+ return ret;
+ }
+
+ if (parser->state == HTTP_RESPONSE_PARSE_STATE_INIT)
+ http_response_parser_restart(parser);
+
+ /* RFC 7230, Section 3:
+
+ HTTP-message = start-line
+ *( header-field CRLF )
+ CRLF
+ [ message-body ]
+ */
+ if (parser->state != HTTP_RESPONSE_PARSE_STATE_HEADER) {
+ if ((ret = http_response_parse_status_line(parser)) <= 0) {
+ *error_r = parser->parser.error;
+ return ret;
+ }
+ }
+ if ((ret = http_message_parse_headers(&parser->parser)) <= 0) {
+ *error_r = parser->parser.error;
+ return ret;
+ }
+
+ /* RFC 7230, Section 3.3.2: Content-Length
+
+ A server MUST NOT send a Content-Length header field in any response
+ with a status code of 1xx (Informational) or 204 (No Content).
+ */
+ if ((parser->response_status / 100 == 1 || parser->response_status == 204) &&
+ parser->parser.msg.content_length > 0) {
+ *error_r = t_strdup_printf(
+ "Unexpected Content-Length header field for %u response "
+ "(length=%"PRIuUOFF_T")", parser->response_status,
+ parser->parser.msg.content_length);
+ return -1;
+ }
+
+ /* RFC 7230, Section 3.3.3: Message Body Length
+
+ 1. Any response to a HEAD request and any response with a 1xx
+ (Informational), 204 (No Content), or 304 (Not Modified) status
+ code is always terminated by the first empty line after the
+ header fields, regardless of the header fields present in the
+ message, and thus cannot contain a message body.
+ */
+ if (parser->response_status / 100 == 1 || parser->response_status == 204
+ || parser->response_status == 304) { // HEAD is handled in caller
+ payload_type = HTTP_RESPONSE_PAYLOAD_TYPE_NOT_PRESENT;
+ }
+
+ if ((payload_type == HTTP_RESPONSE_PAYLOAD_TYPE_ALLOWED) ||
+ (payload_type == HTTP_RESPONSE_PAYLOAD_TYPE_ONLY_UNSUCCESSFUL &&
+ parser->response_status / 100 != 2)) {
+ /* [ message-body ] */
+ if (http_message_parse_body(&parser->parser, FALSE) < 0) {
+ *error_r = parser->parser.error;
+ return -1;
+ }
+ }
+
+ /* RFC 7231, Section 7.1.3: Retry-After
+
+ Servers send the "Retry-After" header field to indicate how long the
+ user agent ought to wait before making a follow-up request. When
+ sent with a 503 (Service Unavailable) response, Retry-After indicates
+ how long the service is expected to be unavailable to the client.
+ When sent with any 3xx (Redirection) response, Retry-After indicates
+ the minimum time that the user agent is asked to wait before issuing
+ the redirected request.
+ */
+ if (parser->response_status == 503 || (parser->response_status / 100) == 3) {
+ hdrval = http_header_field_get(parser->parser.msg.header, "Retry-After");
+ if (hdrval != NULL) {
+ (void)http_response_parse_retry_after
+ (hdrval, parser->parser.msg.date, &retry_after);
+ /* broken Retry-After header is ignored */
+ }
+ }
+
+ parser->state = HTTP_RESPONSE_PARSE_STATE_INIT;
+
+ response->status = parser->response_status;
+ response->reason = parser->response_reason;
+ response->version_major = parser->parser.msg.version_major;
+ response->version_minor = parser->parser.msg.version_minor;
+ response->location = parser->parser.msg.location;
+ response->date = parser->parser.msg.date;
+ response->retry_after = retry_after;
+ response->payload = parser->parser.payload;
+ response->header = parser->parser.msg.header;
+ response->connection_options = parser->parser.msg.connection_options;
+ response->connection_close = parser->parser.msg.connection_close;
+ return 1;
+}
diff --git a/src/lib-http/http-response-parser.h b/src/lib-http/http-response-parser.h
new file mode 100644
index 0000000..8777048
--- /dev/null
+++ b/src/lib-http/http-response-parser.h
@@ -0,0 +1,26 @@
+#ifndef HTTP_RESPONSE_PARSER_H
+#define HTTP_RESPONSE_PARSER_H
+
+#include "http-response.h"
+
+struct http_header_limits;
+struct http_response_parser;
+
+enum http_response_parse_flags {
+ /* Strictly adhere to the HTTP protocol specification */
+ HTTP_RESPONSE_PARSE_FLAG_STRICT = BIT(0)
+};
+
+struct http_response_parser *
+http_response_parser_init(struct istream *input,
+ const struct http_header_limits *hdr_limits,
+ enum http_response_parse_flags flags) ATTR_NULL(2);
+void http_response_parser_deinit(struct http_response_parser **_parser);
+
+uoff_t http_response_parser_get_last_offset(struct http_response_parser *parser);
+
+int http_response_parse_next(struct http_response_parser *parser,
+ enum http_response_payload_type payload_type,
+ struct http_response *response, const char **error_r);
+
+#endif
diff --git a/src/lib-http/http-response.c b/src/lib-http/http-response.c
new file mode 100644
index 0000000..e85a730
--- /dev/null
+++ b/src/lib-http/http-response.c
@@ -0,0 +1,46 @@
+/* Copyright (c) 2013-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "ioloop.h"
+#include "istream.h"
+
+#include "http-response.h"
+
+void
+http_response_init(struct http_response *resp,
+ unsigned int status, const char *reason)
+{
+ i_zero(resp);
+ resp->version_major = 1;
+ resp->version_minor = 1;
+ resp->date = ioloop_time;
+ resp->status = status;
+ resp->reason = reason;
+}
+
+bool http_response_has_connection_option(const struct http_response *resp,
+ const char *option)
+{
+ const char *opt;
+
+ if (!array_is_created(&resp->connection_options))
+ return FALSE;
+ array_foreach_elem(&resp->connection_options, opt) {
+ if (strcasecmp(opt, option) == 0)
+ return TRUE;
+ }
+ return FALSE;
+}
+
+int http_response_get_payload_size(const struct http_response *resp,
+ uoff_t *size_r)
+{
+ if (resp->payload == NULL) {
+ *size_r = 0;
+ return 1;
+ }
+
+ return i_stream_get_size(resp->payload, TRUE, size_r);
+}
+
diff --git a/src/lib-http/http-response.h b/src/lib-http/http-response.h
new file mode 100644
index 0000000..30f6147
--- /dev/null
+++ b/src/lib-http/http-response.h
@@ -0,0 +1,87 @@
+#ifndef HTTP_RESPONSE_H
+#define HTTP_RESPONSE_H
+
+#include "array.h"
+
+#include "http-header.h"
+
+#define HTTP_RESPONSE_STATUS_INTERNAL 9000
+
+enum http_response_payload_type {
+ HTTP_RESPONSE_PAYLOAD_TYPE_ALLOWED,
+ HTTP_RESPONSE_PAYLOAD_TYPE_NOT_PRESENT,
+ HTTP_RESPONSE_PAYLOAD_TYPE_ONLY_UNSUCCESSFUL
+};
+
+struct http_response {
+ unsigned char version_major;
+ unsigned char version_minor;
+
+ unsigned int status;
+
+ const char *reason;
+ const char *location;
+
+ time_t date, retry_after;
+ const struct http_header *header;
+ struct istream *payload;
+
+ ARRAY_TYPE(const_string) connection_options;
+
+ bool connection_close:1;
+};
+
+static inline bool
+http_response_is_success(const struct http_response *resp)
+{
+ return ((resp->status / 100) == 2);
+}
+
+static inline bool
+http_response_is_internal_error(const struct http_response *resp)
+{
+ return (resp->status >= HTTP_RESPONSE_STATUS_INTERNAL);
+}
+
+void
+http_response_init(struct http_response *resp,
+ unsigned int status, const char *reason);
+
+static inline const struct http_header_field *
+http_response_header_find(const struct http_response *resp, const char *name)
+{
+ if (resp->header == NULL)
+ return NULL;
+ return http_header_field_find(resp->header, name);
+}
+
+static inline const char *
+http_response_header_get(const struct http_response *resp, const char *name)
+{
+ if (resp->header == NULL)
+ return NULL;
+ return http_header_field_get(resp->header, name);
+}
+
+static inline const ARRAY_TYPE(http_header_field) *
+http_response_header_get_fields(const struct http_response *resp)
+{
+ if (resp->header == NULL)
+ return NULL;
+ return http_header_get_fields(resp->header);
+}
+
+static inline const char *
+http_response_get_message(const struct http_response *resp)
+{
+ if (resp->status >= HTTP_RESPONSE_STATUS_INTERNAL)
+ return resp->reason;
+ return t_strdup_printf("%u %s", resp->status, resp->reason);
+}
+
+bool http_response_has_connection_option(const struct http_response *resp,
+ const char *option);
+int http_response_get_payload_size(const struct http_response *resp,
+ uoff_t *size_r);
+
+#endif
diff --git a/src/lib-http/http-server-connection.c b/src/lib-http/http-server-connection.c
new file mode 100644
index 0000000..801a703
--- /dev/null
+++ b/src/lib-http/http-server-connection.c
@@ -0,0 +1,1180 @@
+/* Copyright (c) 2013-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "llist.h"
+#include "array.h"
+#include "str.h"
+#include "hostpid.h"
+#include "ioloop.h"
+#include "istream.h"
+#include "istream-timeout.h"
+#include "ostream.h"
+#include "connection.h"
+#include "iostream-rawlog.h"
+#include "iostream-ssl.h"
+#include "master-service.h"
+#include "master-service-ssl.h"
+#include "http-date.h"
+#include "http-url.h"
+#include "http-request-parser.h"
+
+#include "http-server-private.h"
+
+static void
+http_server_connection_disconnect(struct http_server_connection *conn,
+ const char *reason);
+
+static bool
+http_server_connection_unref_is_closed(struct http_server_connection *conn);
+
+/*
+ * Logging
+ */
+
+static inline void
+http_server_connection_client_error(struct http_server_connection *conn,
+ const char *format, ...) ATTR_FORMAT(2, 3);
+
+static inline void
+http_server_connection_client_error(struct http_server_connection *conn,
+ const char *format, ...)
+{
+ va_list args;
+
+ va_start(args, format);
+ e_info(conn->event, "%s", t_strdup_vprintf(format, args));
+ va_end(args);
+}
+
+/*
+ * Connection
+ */
+
+static void http_server_connection_input(struct connection *_conn);
+
+static void
+http_server_connection_update_stats(struct http_server_connection *conn)
+{
+ if (conn->conn.input != NULL)
+ conn->stats.input = conn->conn.input->v_offset;
+ if (conn->conn.output != NULL)
+ conn->stats.output = conn->conn.output->offset;
+}
+
+const struct http_server_stats *
+http_server_connection_get_stats(struct http_server_connection *conn)
+{
+ http_server_connection_update_stats(conn);
+ return &conn->stats;
+}
+
+void http_server_connection_input_set_pending(
+ struct http_server_connection *conn)
+{
+ i_stream_set_input_pending(conn->conn.input, TRUE);
+}
+
+void http_server_connection_input_halt(struct http_server_connection *conn)
+{
+ connection_input_halt(&conn->conn);
+}
+
+void http_server_connection_input_resume(struct http_server_connection *conn)
+{
+ if (conn->closed || conn->input_broken || conn->close_indicated ||
+ conn->incoming_payload != NULL) {
+ /* Connection not usable */
+ return;
+ }
+
+ if (conn->in_req_callback) {
+ struct http_server_request *req = conn->request_queue_tail;
+
+ /* Currently running request callback for this connection. Only
+ handle discarded request payload. */
+ if (req == NULL ||
+ req->state != HTTP_SERVER_REQUEST_STATE_SUBMITTED_RESPONSE)
+ return;
+ if (!http_server_connection_pending_payload(conn))
+ return;
+ }
+
+ connection_input_resume(&conn->conn);
+}
+
+static void
+http_server_connection_idle_timeout(struct http_server_connection *conn)
+{
+ http_server_connection_client_error(
+ conn, "Disconnected for inactivity");
+ http_server_connection_close(&conn, "Disconnected for inactivity");
+}
+
+void http_server_connection_start_idle_timeout(
+ struct http_server_connection *conn)
+{
+ unsigned int timeout_msecs =
+ conn->server->set.max_client_idle_time_msecs;
+
+ if (conn->to_idle == NULL && timeout_msecs > 0) {
+ conn->to_idle = timeout_add(timeout_msecs,
+ http_server_connection_idle_timeout,
+ conn);
+ }
+}
+
+void http_server_connection_reset_idle_timeout(
+ struct http_server_connection *conn)
+{
+ if (conn->to_idle != NULL)
+ timeout_reset(conn->to_idle);
+}
+
+void http_server_connection_stop_idle_timeout(
+ struct http_server_connection *conn)
+{
+ timeout_remove(&conn->to_idle);
+}
+
+bool http_server_connection_shut_down(struct http_server_connection *conn)
+{
+ if (conn->request_queue_head == NULL ||
+ conn->request_queue_head->state == HTTP_SERVER_REQUEST_STATE_NEW) {
+ http_server_connection_close(&conn, "Server shutting down");
+ return TRUE;
+ }
+ return FALSE;
+}
+
+static void http_server_connection_ready(struct http_server_connection *conn)
+{
+ const struct http_server_settings *set = &conn->server->set;
+ struct http_url base_url;
+ struct stat st;
+
+ if (conn->server->set.rawlog_dir != NULL &&
+ stat(conn->server->set.rawlog_dir, &st) == 0) {
+ iostream_rawlog_create(conn->server->set.rawlog_dir,
+ &conn->conn.input, &conn->conn.output);
+ }
+
+ i_zero(&base_url);
+ if (set->default_host != NULL)
+ base_url.host.name = set->default_host;
+ else
+ base_url.host.name = my_hostname;
+ base_url.have_ssl = conn->ssl;
+
+ conn->http_parser = http_request_parser_init(
+ conn->conn.input, &base_url, &conn->server->set.request_limits,
+ HTTP_REQUEST_PARSE_FLAG_STRICT);
+ o_stream_set_finish_via_child(conn->conn.output, FALSE);
+ o_stream_set_flush_callback(conn->conn.output,
+ http_server_connection_output, conn);
+}
+
+static void http_server_connection_destroy(struct connection *_conn)
+{
+ struct http_server_connection *conn =
+ (struct http_server_connection *)_conn;
+
+ http_server_connection_disconnect(conn, NULL);
+ http_server_connection_unref(&conn);
+}
+
+static void http_server_payload_destroyed(struct http_server_request *req)
+{
+ struct http_server_connection *conn = req->conn;
+ int stream_errno;
+
+ i_assert(conn != NULL);
+ i_assert(conn->request_queue_tail == req ||
+ req->state >= HTTP_SERVER_REQUEST_STATE_FINISHED);
+ i_assert(conn->conn.io == NULL);
+
+ e_debug(conn->event, "Request payload stream destroyed");
+
+ /* Caller is allowed to change the socket fd to blocking while reading
+ the payload. make sure here that it's switched back. */
+ net_set_nonblock(conn->conn.fd_in, TRUE);
+
+ stream_errno = conn->incoming_payload->stream_errno;
+ conn->incoming_payload = NULL;
+
+ if (conn->payload_handler != NULL)
+ http_server_payload_handler_destroy(&conn->payload_handler);
+
+ /* Handle errors in transfer stream */
+ if (req->response == NULL && stream_errno != 0 &&
+ conn->conn.input->stream_errno == 0) {
+ switch (stream_errno) {
+ case EMSGSIZE:
+ conn->input_broken = TRUE;
+ http_server_connection_client_error(
+ conn, "Client sent excessively large request");
+ http_server_request_fail_close(req, 413,
+ "Payload Too Large");
+ return;
+ case EIO:
+ conn->input_broken = TRUE;
+ http_server_connection_client_error(
+ conn, "Client sent invalid request payload");
+ http_server_request_fail_close(req, 400,
+ "Bad Request");
+ return;
+ default:
+ break;
+ }
+ }
+
+ /* Resource stopped reading payload; update state */
+ switch (req->state) {
+ case HTTP_SERVER_REQUEST_STATE_QUEUED:
+ case HTTP_SERVER_REQUEST_STATE_PAYLOAD_IN:
+ /* Finished reading request */
+ req->state = HTTP_SERVER_REQUEST_STATE_PROCESSING;
+ http_server_connection_stop_idle_timeout(conn);
+ if (req->response != NULL && req->response->submitted)
+ http_server_request_submit_response(req);
+ break;
+ case HTTP_SERVER_REQUEST_STATE_PROCESSING:
+ /* No response submitted yet */
+ break;
+ case HTTP_SERVER_REQUEST_STATE_SUBMITTED_RESPONSE:
+ /* Response submitted, but not all payload is necessarily read
+ */
+ if (http_server_request_is_complete(req))
+ http_server_request_ready_to_respond(req);
+ break;
+ case HTTP_SERVER_REQUEST_STATE_READY_TO_RESPOND:
+ case HTTP_SERVER_REQUEST_STATE_FINISHED:
+ case HTTP_SERVER_REQUEST_STATE_ABORTED:
+ /* Nothing to do */
+ break;
+ default:
+ i_unreached();
+ }
+
+ /* Input stream may have pending input. */
+ http_server_connection_input_resume(conn);
+ http_server_connection_input_set_pending(conn);
+}
+
+static bool
+http_server_connection_handle_request(struct http_server_connection *conn,
+ struct http_server_request *req)
+{
+ const struct http_server_settings *set = &conn->server->set;
+ unsigned int old_refcount;
+ struct istream *payload;
+
+ i_assert(!conn->in_req_callback);
+ i_assert(conn->incoming_payload == NULL);
+
+ if (req->req.version_major != 1) {
+ http_server_request_fail(req, 505,
+ "HTTP Version Not Supported");
+ return TRUE;
+ }
+
+ req->state = HTTP_SERVER_REQUEST_STATE_QUEUED;
+
+ if (req->req.payload != NULL) {
+ /* Wrap the stream to capture the destroy event without
+ destroying the actual payload stream. */
+ conn->incoming_payload = req->req.payload =
+ i_stream_create_timeout(
+ req->req.payload,
+ set->max_client_idle_time_msecs);
+ /* We've received the request itself, and we can't reset the
+ timeout during the payload reading. */
+ http_server_connection_stop_idle_timeout(conn);
+ } else {
+ conn->incoming_payload = req->req.payload =
+ i_stream_create_from_data("", 0);
+ }
+ i_stream_add_destroy_callback(req->req.payload,
+ http_server_payload_destroyed, req);
+ /* The callback may add its own I/O, so we need to remove our one before
+ calling it. */
+ http_server_connection_input_halt(conn);
+
+ old_refcount = req->refcount;
+ conn->in_req_callback = TRUE;
+ T_BEGIN {
+ http_server_request_callback(req);
+ } T_END;
+ if (conn->closed) {
+ /* The callback managed to get this connection destroyed/closed
+ */
+ return FALSE;
+ }
+ conn->in_req_callback = FALSE;
+ req->callback_refcount = req->refcount - old_refcount;
+
+ if (req->req.payload != NULL) {
+ /* Send 100 Continue when appropriate */
+ if (req->req.expect_100_continue && !req->payload_halted &&
+ req->response == NULL) {
+ http_server_connection_output_trigger(conn);
+ }
+
+ /* Delegate payload handling to request handler */
+ if (req->state < HTTP_SERVER_REQUEST_STATE_PAYLOAD_IN)
+ req->state = HTTP_SERVER_REQUEST_STATE_PAYLOAD_IN;
+ payload = req->req.payload;
+ req->req.payload = NULL;
+ i_stream_unref(&payload);
+ }
+
+ if (req->state < HTTP_SERVER_REQUEST_STATE_PROCESSING &&
+ (conn->incoming_payload == NULL ||
+ !i_stream_have_bytes_left(conn->incoming_payload))) {
+ /* Finished reading request */
+ req->state = HTTP_SERVER_REQUEST_STATE_PROCESSING;
+ if (req->response != NULL && req->response->submitted)
+ http_server_request_submit_response(req);
+ }
+
+ i_assert(conn->incoming_payload != NULL || req->callback_refcount > 0 ||
+ (req->response != NULL && req->response->submitted));
+
+ if (conn->incoming_payload == NULL) {
+ http_server_connection_input_resume(conn);
+ http_server_connection_input_set_pending(conn);
+ return TRUE;
+ }
+
+ /* Request payload is still being uploaded by the client */
+ return FALSE;
+}
+
+static int
+http_server_connection_ssl_init(struct http_server_connection *conn)
+{
+ struct http_server *server = conn->server;
+ const char *error;
+ int ret;
+
+ if (http_server_init_ssl_ctx(server, &error) < 0) {
+ e_error(conn->event, "Couldn't initialize SSL: %s", error);
+ return -1;
+ }
+
+ e_debug(conn->event, "Starting SSL handshake");
+
+ http_server_connection_input_halt(conn);
+ if (server->ssl_ctx == NULL) {
+ ret = master_service_ssl_init(master_service,
+ &conn->conn.input,
+ &conn->conn.output,
+ &conn->ssl_iostream, &error);
+ } else {
+ ret = io_stream_create_ssl_server(server->ssl_ctx,
+ server->set.ssl,
+ &conn->conn.input,
+ &conn->conn.output,
+ &conn->ssl_iostream, &error);
+ }
+ if (ret < 0) {
+ e_error(conn->event,
+ "Couldn't initialize SSL server for %s: %s",
+ conn->conn.name, error);
+ return -1;
+ }
+ http_server_connection_input_resume(conn);
+
+ if (ssl_iostream_handshake(conn->ssl_iostream) < 0) {
+ e_error(conn->event, "SSL handshake failed: %s",
+ ssl_iostream_get_last_error(conn->ssl_iostream));
+ return -1;
+ }
+
+ http_server_connection_ready(conn);
+ return 0;
+}
+
+static bool
+http_server_connection_pipeline_is_full(struct http_server_connection *conn)
+{
+ return ((conn->request_queue_count >=
+ conn->server->set.max_pipelined_requests) ||
+ conn->server->shutting_down);
+}
+
+static void
+http_server_connection_pipeline_handle_full(struct http_server_connection *conn)
+{
+ if (conn->server->shutting_down) {
+ e_debug(conn->event, "Pipeline full "
+ "(%u requests pending; server shutting down)",
+ conn->request_queue_count);
+ } else {
+ e_debug(conn->event, "Pipeline full "
+ "(%u requests pending; %u maximum)",
+ conn->request_queue_count,
+ conn->server->set.max_pipelined_requests);
+ }
+ http_server_connection_input_halt(conn);
+}
+
+static bool
+http_server_connection_check_input(struct http_server_connection *conn)
+{
+ struct istream *input = conn->conn.input;
+ int stream_errno;
+
+ if (input == NULL)
+ return FALSE;
+ stream_errno = input->stream_errno;
+
+ if (input->eof || stream_errno != 0) {
+ /* Connection input broken; output may still be intact */
+ if (stream_errno != 0 && stream_errno != EPIPE &&
+ stream_errno != ECONNRESET) {
+ http_server_connection_client_error(
+ conn, "Connection lost: read(%s) failed: %s",
+ i_stream_get_name(input),
+ i_stream_get_error(input));
+ http_server_connection_close(&conn, "Read failure");
+ } else {
+ e_debug(conn->event, "Connection lost: "
+ "Remote disconnected");
+
+ if (conn->request_queue_head == NULL) {
+ /* No pending requests; close */
+ http_server_connection_close(
+ &conn, "Remote closed connection");
+ } else if (conn->request_queue_head->state <
+ HTTP_SERVER_REQUEST_STATE_SUBMITTED_RESPONSE) {
+ /* Unfinished request; close */
+ http_server_connection_close(&conn,
+ "Remote closed connection unexpectedly");
+ } else {
+ /* A request is still processing; only drop
+ input io for now. The other end may only have
+ shutdown one direction */
+ conn->input_broken = TRUE;
+ http_server_connection_input_halt(conn);
+ }
+ }
+ return FALSE;
+ }
+ return TRUE;
+}
+
+static bool
+http_server_connection_finish_request(struct http_server_connection *conn)
+{
+ struct http_server_request *req;
+ enum http_request_parse_error error_code;
+ const char *error;
+ int ret;
+
+ req = conn->request_queue_tail;
+ if (req != NULL &&
+ req->state == HTTP_SERVER_REQUEST_STATE_SUBMITTED_RESPONSE) {
+
+ e_debug(conn->event, "Finish receiving request");
+
+ ret = http_request_parse_finish_payload(conn->http_parser,
+ &error_code, &error);
+ if (ret <= 0 && !http_server_connection_check_input(conn))
+ return FALSE;
+ if (ret < 0) {
+ http_server_connection_ref(conn);
+
+ http_server_connection_client_error(
+ conn, "Client sent invalid request: %s", error);
+
+ switch (error_code) {
+ case HTTP_REQUEST_PARSE_ERROR_PAYLOAD_TOO_LARGE:
+ conn->input_broken = TRUE;
+ http_server_request_fail_close(
+ req, 413, "Payload Too Large");
+ break;
+ case HTTP_REQUEST_PARSE_ERROR_BROKEN_REQUEST:
+ conn->input_broken = TRUE;
+ http_server_request_fail_close(
+ req, 400, "Bad request");
+ break;
+ default:
+ i_unreached();
+ }
+
+ if (http_server_connection_unref_is_closed(conn)) {
+ /* Connection got closed */
+ return FALSE;
+ }
+
+ if (conn->input_broken || conn->close_indicated)
+ http_server_connection_input_halt(conn);
+ return FALSE;
+ }
+ if (ret == 0)
+ return FALSE;
+ http_server_request_ready_to_respond(req);
+ }
+
+ return TRUE;
+}
+
+static void http_server_connection_input(struct connection *_conn)
+{
+ struct http_server_connection *conn =
+ (struct http_server_connection *)_conn;
+ struct http_server_request *req;
+ enum http_request_parse_error error_code;
+ const char *error;
+ bool cont;
+ int ret;
+
+ if (conn->server->shutting_down) {
+ if (!http_server_connection_shut_down(conn))
+ http_server_connection_pipeline_handle_full(conn);
+ return;
+ }
+
+ i_assert(!conn->input_broken && conn->incoming_payload == NULL);
+ i_assert(!conn->close_indicated);
+
+ http_server_connection_reset_idle_timeout(conn);
+
+ if (conn->ssl && conn->ssl_iostream == NULL) {
+ if (http_server_connection_ssl_init(conn) < 0) {
+ /* SSL failed */
+ http_server_connection_close(
+ &conn, "SSL Initialization failed");
+ return;
+ }
+ }
+
+ /* Finish up pending request */
+ if (!http_server_connection_finish_request(conn))
+ return;
+
+ /* Stop handling input here when running ioloop from within request
+ callback; we cannot read the next request, since that could mean
+ recursing request callbacks. */
+ if (conn->in_req_callback) {
+ http_server_connection_input_halt(conn);
+ return;
+ }
+
+ /* Create request object if none was created already */
+ if (conn->request_queue_tail != NULL &&
+ conn->request_queue_tail->state == HTTP_SERVER_REQUEST_STATE_NEW) {
+ if (conn->request_queue_count >
+ conn->server->set.max_pipelined_requests) {
+ /* Pipeline full */
+ http_server_connection_pipeline_handle_full(conn);
+ return;
+ }
+ /* Continue last unfinished request */
+ req = conn->request_queue_tail;
+ } else {
+ if (conn->request_queue_count >=
+ conn->server->set.max_pipelined_requests) {
+ /* Pipeline full */
+ http_server_connection_pipeline_handle_full(conn);
+ return;
+ }
+ /* Start new request */
+ req = http_server_request_new(conn);
+ }
+
+ /* Parse requests */
+ ret = 1;
+ while (!conn->close_indicated && ret != 0) {
+ http_server_connection_ref(conn);
+ while ((ret = http_request_parse_next(
+ conn->http_parser, req->pool, &req->req,
+ &error_code, &error)) > 0) {
+ conn->stats.request_count++;
+ http_server_request_received(req);
+
+ http_server_request_immune_ref(req);
+ T_BEGIN {
+ cont = http_server_connection_handle_request(conn, req);
+ } T_END;
+ if (!cont) {
+ /* Connection closed or request body not read
+ yet. The request may be destroyed now. */
+ http_server_request_immune_unref(&req);
+ http_server_connection_unref(&conn);
+ return;
+ }
+ if (req->req.connection_close)
+ conn->close_indicated = TRUE;
+ http_server_request_immune_unref(&req);
+
+ if (conn->closed) {
+ /* Connection got closed in destroy callback */
+ break;
+ }
+
+ if (conn->close_indicated) {
+ /* Client indicated it will close after this
+ request; stop trying to read more. */
+ break;
+ }
+
+ /* Finish up pending request if possible */
+ if (!http_server_connection_finish_request(conn)) {
+ http_server_connection_unref(&conn);
+ return;
+ }
+
+ if (http_server_connection_pipeline_is_full(conn)) {
+ /* Pipeline full */
+ http_server_connection_pipeline_handle_full(conn);
+ http_server_connection_unref(&conn);
+ return;
+ }
+
+ /* Start new request */
+ req = http_server_request_new(conn);
+ }
+
+ if (http_server_connection_unref_is_closed(conn)) {
+ /* Connection got closed */
+ return;
+ }
+
+ if (ret <= 0 && !http_server_connection_check_input(conn))
+ return;
+
+ if (ret < 0) {
+ http_server_connection_ref(conn);
+
+ http_server_connection_client_error(
+ conn, "Client sent invalid request: %s", error);
+
+ switch (error_code) {
+ case HTTP_REQUEST_PARSE_ERROR_BROKEN_REQUEST:
+ conn->input_broken = TRUE;
+ /* fall through */
+ case HTTP_REQUEST_PARSE_ERROR_BAD_REQUEST:
+ http_server_request_fail(
+ req, 400, "Bad Request");
+ break;
+ case HTTP_REQUEST_PARSE_ERROR_METHOD_TOO_LONG:
+ conn->input_broken = TRUE;
+ /* fall through */
+ case HTTP_REQUEST_PARSE_ERROR_NOT_IMPLEMENTED:
+ http_server_request_fail(
+ req, 501, "Not Implemented");
+ break;
+ case HTTP_REQUEST_PARSE_ERROR_TARGET_TOO_LONG:
+ conn->input_broken = TRUE;
+ http_server_request_fail_close(
+ req, 414, "URI Too Long");
+ break;
+ case HTTP_REQUEST_PARSE_ERROR_EXPECTATION_FAILED:
+ http_server_request_fail(
+ req, 417, "Expectation Failed");
+ break;
+ case HTTP_REQUEST_PARSE_ERROR_PAYLOAD_TOO_LARGE:
+ conn->input_broken = TRUE;
+ http_server_request_fail_close(
+ req, 413, "Payload Too Large");
+ break;
+ default:
+ i_unreached();
+ }
+
+ if (http_server_connection_unref_is_closed(conn)) {
+ /* Connection got closed */
+ return;
+ }
+ }
+
+ if (conn->input_broken || conn->close_indicated) {
+ http_server_connection_input_halt(conn);
+ return;
+ }
+ }
+}
+
+void http_server_connection_handle_output_error(
+ struct http_server_connection *conn)
+{
+ struct ostream *output = conn->conn.output;
+
+ if (conn->closed)
+ return;
+
+ if (output->stream_errno != EPIPE &&
+ output->stream_errno != ECONNRESET) {
+ e_error(conn->event, "Connection lost: write(%s) failed: %s",
+ o_stream_get_name(output),
+ o_stream_get_error(output));
+ http_server_connection_close(
+ &conn, "Write failure");
+ } else {
+ e_debug(conn->event, "Connection lost: Remote disconnected");
+ http_server_connection_close(
+ &conn, "Remote closed connection unexpectedly");
+ }
+}
+
+enum _output_result {
+ /* Error */
+ _OUTPUT_ERROR = -1,
+ /* Output blocked */
+ _OUTPUT_BLOCKED = 0,
+ /* Successful, but no more responses are ready to be sent */
+ _OUTPUT_FINISHED = 1,
+ /* Successful and more responses can be sent */
+ _OUTPUT_AVAILABLE = 2,
+};
+
+static enum _output_result
+http_server_connection_next_response(struct http_server_connection *conn)
+{
+ struct http_server_request *req;
+ int ret;
+
+ if (conn->output_locked)
+ return _OUTPUT_FINISHED;
+
+ req = conn->request_queue_head;
+ if (req == NULL || req->state == HTTP_SERVER_REQUEST_STATE_NEW) {
+ /* No requests pending */
+ e_debug(conn->event, "No more requests pending");
+ http_server_connection_start_idle_timeout(conn);
+ return _OUTPUT_FINISHED;
+ }
+ if (req->state < HTTP_SERVER_REQUEST_STATE_READY_TO_RESPOND) {
+ if (req->state == HTTP_SERVER_REQUEST_STATE_PROCESSING) {
+ /* Server is causing idle time */
+ e_debug(conn->event, "Not ready to respond: "
+ "Server is processing");
+ http_server_connection_stop_idle_timeout(conn);
+ } else {
+ /* Client is causing idle time */
+ e_debug(conn->event, "Not ready to respond: "
+ "Waiting for client");
+ http_server_connection_start_idle_timeout(conn);
+ }
+
+ /* send 100 Continue if appropriate */
+ if (req->state >= HTTP_SERVER_REQUEST_STATE_QUEUED &&
+ conn->incoming_payload != NULL &&
+ req->response == NULL && req->req.version_minor >= 1 &&
+ req->req.expect_100_continue && !req->payload_halted &&
+ !req->sent_100_continue) {
+ static const char *response =
+ "HTTP/1.1 100 Continue\r\n\r\n";
+ struct ostream *output = conn->conn.output;
+
+ if (o_stream_send(output, response,
+ strlen(response)) < 0) {
+ http_server_connection_handle_output_error(conn);
+ return _OUTPUT_ERROR;
+ }
+
+ e_debug(conn->event, "Sent 100 Continue");
+ req->sent_100_continue = TRUE;
+ }
+ return _OUTPUT_FINISHED;
+ }
+
+ i_assert(req->state == HTTP_SERVER_REQUEST_STATE_READY_TO_RESPOND &&
+ req->response != NULL);
+
+ e_debug(conn->event, "Sending response");
+ http_server_connection_start_idle_timeout(conn);
+
+ http_server_request_immune_ref(req);
+ ret = http_server_response_send(req->response);
+ http_server_request_immune_unref(&req);
+
+ if (ret < 0)
+ return _OUTPUT_ERROR;
+
+ http_server_connection_reset_idle_timeout(conn);
+ if (ret == 0)
+ return _OUTPUT_BLOCKED;
+ if (conn->output_locked)
+ return _OUTPUT_FINISHED;
+ return _OUTPUT_AVAILABLE;
+}
+
+static int
+http_server_connection_send_responses(struct http_server_connection *conn)
+{
+ enum _output_result ores = _OUTPUT_AVAILABLE;
+
+ http_server_connection_ref(conn);
+
+ /* Send more responses until no more responses remain, the output
+ blocks again, or the connection is closed */
+ while (!conn->closed && ores == _OUTPUT_AVAILABLE)
+ ores = http_server_connection_next_response(conn);
+
+ if (http_server_connection_unref_is_closed(conn) ||
+ ores == _OUTPUT_ERROR)
+ return -1;
+
+ /* Accept more requests if possible */
+ if (conn->incoming_payload == NULL &&
+ (conn->request_queue_count <
+ conn->server->set.max_pipelined_requests) &&
+ !conn->server->shutting_down)
+ http_server_connection_input_resume(conn);
+
+ switch (ores) {
+ case _OUTPUT_ERROR:
+ case _OUTPUT_AVAILABLE:
+ break;
+ case _OUTPUT_BLOCKED:
+ return 0;
+ case _OUTPUT_FINISHED:
+ return 1;
+ }
+ i_unreached();
+}
+
+int http_server_connection_flush(struct http_server_connection *conn)
+{
+ struct ostream *output = conn->conn.output;
+ int ret;
+
+ if ((ret = o_stream_flush(output)) <= 0) {
+ if (ret < 0)
+ http_server_connection_handle_output_error(conn);
+ return ret;
+ }
+
+ http_server_connection_reset_idle_timeout(conn);
+ return 0;
+}
+
+int http_server_connection_output(struct http_server_connection *conn)
+{
+ bool pipeline_was_full =
+ http_server_connection_pipeline_is_full(conn);
+ int ret = 1;
+
+ if (http_server_connection_flush(conn) < 0)
+ return -1;
+
+ if (!conn->output_locked) {
+ ret = http_server_connection_send_responses(conn);
+ if (ret < 0)
+ return -1;
+ } else if (conn->request_queue_head != NULL) {
+ struct http_server_request *req = conn->request_queue_head;
+ struct http_server_response *resp = req->response;
+
+ i_assert(resp != NULL);
+
+ http_server_connection_ref(conn);
+
+ http_server_request_immune_ref(req);
+ ret = http_server_response_send_more(resp);
+ http_server_request_immune_unref(&req);
+
+ if (http_server_connection_unref_is_closed(conn) || ret < 0)
+ return -1;
+
+ if (!conn->output_locked) {
+ /* Room for more responses */
+ ret = http_server_connection_send_responses(conn);
+ if (ret < 0)
+ return -1;
+ } else if (conn->io_resp_payload != NULL) {
+ /* Server is causing idle time */
+ e_debug(conn->event, "Not ready to continue response: "
+ "Server is producing response");
+ http_server_connection_stop_idle_timeout(conn);
+ } else {
+ /* Client is causing idle time */
+ e_debug(conn->event, "Not ready to continue response: "
+ "Waiting for client");
+ http_server_connection_start_idle_timeout(conn);
+ }
+ }
+
+ if (conn->server->shutting_down &&
+ http_server_connection_shut_down(conn))
+ return 1;
+
+ if (!http_server_connection_pipeline_is_full(conn)) {
+ http_server_connection_input_resume(conn);
+ if (pipeline_was_full && conn->conn.io != NULL)
+ http_server_connection_input_set_pending(conn);
+ }
+
+ return ret;
+}
+
+void http_server_connection_output_trigger(struct http_server_connection *conn)
+{
+ if (conn->conn.output == NULL)
+ return;
+ o_stream_set_flush_pending(conn->conn.output, TRUE);
+}
+
+void http_server_connection_output_halt(struct http_server_connection *conn)
+{
+ conn->output_halted = TRUE;
+
+ if (conn->conn.output == NULL)
+ return;
+
+ o_stream_unset_flush_callback(conn->conn.output);
+}
+
+void http_server_connection_output_resume(struct http_server_connection *conn)
+{
+ if (conn->output_halted) {
+ conn->output_halted = FALSE;
+ o_stream_set_flush_callback(conn->conn.output,
+ http_server_connection_output, conn);
+ }
+}
+
+bool http_server_connection_pending_payload(
+ struct http_server_connection *conn)
+{
+ return http_request_parser_pending_payload(conn->http_parser);
+}
+
+static struct connection_settings http_server_connection_set = {
+ .input_max_size = SIZE_MAX,
+ .output_max_size = SIZE_MAX,
+ .client = FALSE,
+ .log_connection_id = TRUE,
+};
+
+static const struct connection_vfuncs http_server_connection_vfuncs = {
+ .destroy = http_server_connection_destroy,
+ .input = http_server_connection_input
+};
+
+struct connection_list *http_server_connection_list_init(void)
+{
+ return connection_list_init(&http_server_connection_set,
+ &http_server_connection_vfuncs);
+}
+
+struct http_server_connection *
+http_server_connection_create(struct http_server *server,
+ int fd_in, int fd_out, bool ssl,
+ const struct http_server_callbacks *callbacks,
+ void *context)
+{
+ const struct http_server_settings *set = &server->set;
+ struct http_server_connection *conn;
+ struct event *conn_event;
+
+ i_assert(!server->shutting_down);
+
+ conn = i_new(struct http_server_connection, 1);
+ conn->refcount = 1;
+ conn->server = server;
+ conn->ioloop = current_ioloop;
+ conn->ssl = ssl;
+ conn->callbacks = callbacks;
+ conn->context = context;
+
+ net_set_nonblock(fd_in, TRUE);
+ if (fd_in != fd_out)
+ net_set_nonblock(fd_out, TRUE);
+ (void)net_set_tcp_nodelay(fd_out, TRUE);
+
+ if (set->socket_send_buffer_size > 0 &&
+ net_set_send_buffer_size(fd_out,
+ set->socket_send_buffer_size) < 0) {
+ e_error(conn->event,
+ "net_set_send_buffer_size(%zu) failed: %m",
+ set->socket_send_buffer_size);
+ }
+ if (set->socket_recv_buffer_size > 0 &&
+ net_set_recv_buffer_size(fd_in,
+ set->socket_recv_buffer_size) < 0) {
+ e_error(conn->event,
+ "net_set_recv_buffer_size(%zu) failed: %m",
+ set->socket_recv_buffer_size);
+ }
+
+ conn_event = event_create(server->event);
+ conn->conn.event_parent = conn_event;
+ connection_init_server(server->conn_list, &conn->conn, NULL,
+ fd_in, fd_out);
+ conn->event = conn->conn.event;
+ event_unref(&conn_event);
+
+ if (!ssl)
+ http_server_connection_ready(conn);
+ http_server_connection_start_idle_timeout(conn);
+
+ e_debug(conn->event, "Connection created");
+ return conn;
+}
+
+void http_server_connection_ref(struct http_server_connection *conn)
+{
+ i_assert(conn->refcount > 0);
+ conn->refcount++;
+}
+
+static void
+http_server_connection_disconnect(struct http_server_connection *conn,
+ const char *reason)
+{
+ struct http_server_request *req, *req_next;
+
+ if (conn->closed)
+ return;
+
+ if (reason == NULL)
+ reason = "Connection closed";
+ e_debug(conn->event, "Disconnected: %s", reason);
+ conn->disconnect_reason = i_strdup(reason);
+ conn->closed = TRUE;
+
+ /* Preserve statistics */
+ http_server_connection_update_stats(conn);
+
+ if (conn->incoming_payload != NULL) {
+ /* The stream is still accessed by lib-http caller. */
+ i_stream_remove_destroy_callback(conn->incoming_payload,
+ http_server_payload_destroyed);
+ conn->incoming_payload = NULL;
+ }
+ if (conn->payload_handler != NULL)
+ http_server_payload_handler_destroy(&conn->payload_handler);
+
+ /* Drop all requests before connection is closed */
+ req = conn->request_queue_head;
+ while (req != NULL) {
+ req_next = req->next;
+ http_server_request_abort(&req, reason);
+ req = req_next;
+ }
+
+ timeout_remove(&conn->to_input);
+ timeout_remove(&conn->to_idle);
+ io_remove(&conn->io_resp_payload);
+ if (conn->conn.output != NULL)
+ o_stream_uncork(conn->conn.output);
+
+ if (conn->http_parser != NULL)
+ http_request_parser_deinit(&conn->http_parser);
+ connection_disconnect(&conn->conn);
+}
+
+bool http_server_connection_unref(struct http_server_connection **_conn)
+{
+ struct http_server_connection *conn = *_conn;
+
+ i_assert(conn->refcount > 0);
+
+ *_conn = NULL;
+ if (--conn->refcount > 0)
+ return TRUE;
+
+ http_server_connection_disconnect(conn, NULL);
+
+ e_debug(conn->event, "Connection destroy");
+
+ ssl_iostream_destroy(&conn->ssl_iostream);
+ connection_deinit(&conn->conn);
+
+ if (conn->callbacks != NULL &&
+ conn->callbacks->connection_destroy != NULL) T_BEGIN {
+ conn->callbacks->connection_destroy(conn->context,
+ conn->disconnect_reason);
+ } T_END;
+
+ i_free(conn->disconnect_reason);
+ i_free(conn);
+ return FALSE;
+}
+
+static bool
+http_server_connection_unref_is_closed(struct http_server_connection *conn)
+{
+ bool closed = conn->closed;
+
+ if (!http_server_connection_unref(&conn))
+ closed = TRUE;
+ return closed;
+}
+
+void http_server_connection_close(struct http_server_connection **_conn,
+ const char *reason)
+{
+ struct http_server_connection *conn = *_conn;
+
+ http_server_connection_disconnect(conn, reason);
+ http_server_connection_unref(_conn);
+}
+
+void http_server_connection_tunnel(struct http_server_connection **_conn,
+ http_server_tunnel_callback_t callback,
+ void *context)
+{
+ struct http_server_connection *conn = *_conn;
+ struct http_server_tunnel tunnel;
+
+ /* Preserve statistics */
+ http_server_connection_update_stats(conn);
+
+ i_zero(&tunnel);
+ tunnel.input = conn->conn.input;
+ tunnel.output = conn->conn.output;
+ tunnel.fd_in = conn->conn.fd_in;
+ tunnel.fd_out = conn->conn.fd_out;
+
+ conn->conn.input = NULL;
+ conn->conn.output = NULL;
+ conn->conn.fd_in = conn->conn.fd_out = -1;
+ http_server_connection_close(_conn, "Tunnel initiated");
+
+ callback(context, &tunnel);
+}
+
+struct ioloop *
+http_server_connection_switch_ioloop_to(struct http_server_connection *conn,
+ struct ioloop *ioloop)
+{
+ struct ioloop *prev_ioloop = conn->ioloop;
+
+ if (conn->ioloop_switching != NULL)
+ return conn->ioloop_switching;
+
+ conn->ioloop = ioloop;
+ conn->ioloop_switching = prev_ioloop;
+ connection_switch_ioloop_to(&conn->conn, ioloop);
+ if (conn->to_input != NULL) {
+ conn->to_input =
+ io_loop_move_timeout_to(ioloop, &conn->to_input);
+ }
+ if (conn->to_idle != NULL) {
+ conn->to_idle =
+ io_loop_move_timeout_to(ioloop, &conn->to_idle);
+ }
+ if (conn->io_resp_payload != NULL) {
+ conn->io_resp_payload =
+ io_loop_move_io_to(ioloop, &conn->io_resp_payload);
+ }
+ if (conn->payload_handler != NULL) {
+ http_server_payload_handler_switch_ioloop(
+ conn->payload_handler, ioloop);
+ }
+ if (conn->incoming_payload != NULL)
+ i_stream_switch_ioloop_to(conn->incoming_payload, ioloop);
+ conn->ioloop_switching = NULL;
+
+ return prev_ioloop;
+}
+
+struct ioloop *
+http_server_connection_switch_ioloop(struct http_server_connection *conn)
+{
+ return http_server_connection_switch_ioloop_to(conn, current_ioloop);
+}
diff --git a/src/lib-http/http-server-ostream.c b/src/lib-http/http-server-ostream.c
new file mode 100644
index 0000000..566aa70
--- /dev/null
+++ b/src/lib-http/http-server-ostream.c
@@ -0,0 +1,328 @@
+/* Copyright (c) 2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "istream.h"
+#include "dns-lookup.h"
+#include "ostream-wrapper.h"
+
+#include "http-server-private.h"
+
+/*
+ * Payload output stream
+ */
+
+struct http_server_ostream {
+ struct wrapper_ostream wostream;
+
+ struct http_server_connection *conn;
+ struct http_server_response *resp;
+
+ bool response_destroyed:1;
+};
+
+static void http_server_ostream_output_error(struct wrapper_ostream *wostream)
+{
+ struct http_server_ostream *hsostream =
+ (struct http_server_ostream *)wostream;
+ struct http_server_connection *conn = hsostream->conn;
+
+ if (hsostream->response_destroyed)
+ return;
+
+ i_assert(hsostream->resp != NULL);
+ http_server_connection_handle_output_error(conn);
+}
+
+static void http_server_ostream_output_start(struct wrapper_ostream *wostream)
+{
+ struct http_server_ostream *hsostream =
+ (struct http_server_ostream *)wostream;
+ struct http_server_response *resp = hsostream->resp;
+
+ i_assert(hsostream->response_destroyed || resp != NULL);
+
+ if (!hsostream->response_destroyed &&
+ resp->request->state <= HTTP_SERVER_REQUEST_STATE_PROCESSING) {
+ /* implicitly submit the request */
+ http_server_response_submit(resp);
+ }
+}
+
+void http_server_ostream_output_available(
+ struct http_server_ostream *hsostream)
+{
+ struct http_server_response *resp = hsostream->resp;
+
+ i_assert(resp != NULL);
+ i_assert(!hsostream->response_destroyed);
+ wrapper_ostream_output_available(&hsostream->wostream,
+ resp->payload_output);
+}
+
+static bool http_server_ostream_output_ready(struct wrapper_ostream *wostream)
+{
+ struct http_server_ostream *hsostream =
+ (struct http_server_ostream *)wostream;
+ struct http_server_response *resp = hsostream->resp;
+
+ i_assert(resp != NULL);
+ i_assert(!hsostream->response_destroyed);
+ return (resp->request->state >= HTTP_SERVER_REQUEST_STATE_PAYLOAD_OUT);
+}
+
+static int http_server_ostream_output_finish(struct wrapper_ostream *wostream)
+{
+ struct http_server_ostream *hsostream =
+ (struct http_server_ostream *)wostream;
+ struct http_server_response *resp = hsostream->resp;
+
+ i_assert(resp != NULL);
+ i_assert(!hsostream->response_destroyed);
+
+ e_debug(wostream->event, "Finished response payload stream");
+
+ /* finished sending payload */
+ return http_server_response_finish_payload_out(resp);
+}
+
+static void http_server_ostream_output_halt(struct wrapper_ostream *wostream)
+{
+ struct http_server_ostream *hsostream =
+ (struct http_server_ostream *)wostream;
+ struct http_server_connection *conn = hsostream->conn;
+ struct http_server_response *resp = hsostream->resp;
+
+ i_assert(hsostream->response_destroyed || resp != NULL);
+
+ if (hsostream->response_destroyed ||
+ resp->request->state < HTTP_SERVER_REQUEST_STATE_PAYLOAD_OUT)
+ return;
+
+ http_server_connection_output_halt(conn);
+}
+
+static void http_server_ostream_output_resume(struct wrapper_ostream *wostream)
+{
+ struct http_server_ostream *hsostream =
+ (struct http_server_ostream *)wostream;
+ struct http_server_connection *conn = hsostream->conn;
+
+ if (hsostream->response_destroyed)
+ return;
+ i_assert(hsostream->resp != NULL);
+
+ http_server_connection_output_resume(conn);
+}
+
+static void
+http_server_ostream_output_update_timeouts(struct wrapper_ostream *wostream,
+ bool sender_blocking)
+{
+ struct http_server_ostream *hsostream =
+ (struct http_server_ostream *)wostream;
+ struct http_server_connection *conn = hsostream->conn;
+
+ if (hsostream->response_destroyed)
+ return;
+ i_assert(hsostream->resp != NULL);
+
+ if (sender_blocking) {
+ http_server_connection_stop_idle_timeout(conn);
+ return;
+ }
+
+ http_server_connection_start_idle_timeout(conn);
+}
+
+static void http_server_ostream_close(struct wrapper_ostream *wostream)
+{
+ struct http_server_ostream *hsostream =
+ (struct http_server_ostream *)wostream;
+ struct http_server_response *resp = hsostream->resp;
+
+ e_debug(wostream->event, "Response payload stream closed");
+
+ if (hsostream->response_destroyed) {
+ http_server_response_unref(&hsostream->resp);
+ return;
+ }
+ hsostream->response_destroyed = TRUE;
+
+ i_assert(resp != NULL);
+ (void)http_server_response_finish_payload_out(resp);
+ resp->payload_stream = NULL;
+ http_server_response_unref(&hsostream->resp);
+}
+
+static void http_server_ostream_destroy(struct wrapper_ostream *wostream)
+{
+ struct http_server_ostream *hsostream =
+ (struct http_server_ostream *)wostream;
+ struct http_server_response *resp = hsostream->resp;
+ struct http_server_request *req;
+
+ e_debug(wostream->event, "Response payload stream destroyed");
+
+ if (hsostream->response_destroyed) {
+ http_server_response_unref(&hsostream->resp);
+ return;
+ }
+ hsostream->response_destroyed = TRUE;
+ i_assert(resp != NULL);
+
+ req = resp->request;
+ resp->payload_stream = NULL;
+ http_server_request_abort(
+ &req, "Response output stream destroyed prematurely");
+}
+
+static struct ioloop *
+http_server_ostream_wait_begin(struct wrapper_ostream *wostream,
+ struct ioloop *ioloop)
+{
+ struct http_server_ostream *hsostream =
+ (struct http_server_ostream *)wostream;
+ struct http_server_connection *conn = hsostream->conn;
+ struct ioloop *prev_ioloop;
+
+ i_assert(hsostream->resp != NULL);
+ i_assert(!hsostream->response_destroyed);
+
+ http_server_connection_ref(conn);
+
+ /* When the response payload output stream is written from inside the
+ request callback, the incoming payload stream is not destroyed yet,
+ even though it is read to the end. This could lead to problems, so we
+ make an effort to destroy it here.
+ */
+ if (conn->incoming_payload != NULL &&
+ i_stream_read_eof(conn->incoming_payload)) {
+ struct http_server_request *req = hsostream->resp->request;
+ struct istream *payload;
+
+ payload = req->req.payload;
+ req->req.payload = NULL;
+ i_stream_unref(&payload);
+ }
+
+ prev_ioloop = http_server_connection_switch_ioloop_to(conn, ioloop);
+ return prev_ioloop;
+}
+
+static void
+http_server_ostream_wait_end(struct wrapper_ostream *wostream,
+ struct ioloop *prev_ioloop)
+{
+ struct http_server_ostream *hsostream =
+ (struct http_server_ostream *)wostream;
+ struct http_server_connection *conn = hsostream->conn;
+
+ (void)http_server_connection_switch_ioloop_to(conn, prev_ioloop);
+ http_server_connection_unref(&conn);
+}
+
+int http_server_ostream_continue(struct http_server_ostream *hsostream)
+{
+ struct wrapper_ostream *wostream = &hsostream->wostream;
+ struct http_server_response *resp = hsostream->resp;
+
+ i_assert(hsostream->response_destroyed || resp != NULL);
+
+ i_assert(hsostream->response_destroyed ||
+ resp->request->state >= HTTP_SERVER_REQUEST_STATE_PAYLOAD_OUT);
+
+ return wrapper_ostream_continue(wostream);
+}
+
+bool http_server_ostream_get_size(struct http_server_ostream *hsostream,
+ uoff_t *size_r)
+{
+ return wrapper_ostream_get_buffered_size(&hsostream->wostream, size_r);
+}
+
+static void
+http_server_ostream_switch_ioloop_to(struct wrapper_ostream *wostream,
+ struct ioloop *ioloop)
+{
+ struct http_server_ostream *hsostream =
+ (struct http_server_ostream *)wostream;
+ struct http_server_connection *conn = hsostream->conn;
+
+ if (hsostream->response_destroyed)
+ return;
+ i_assert(hsostream->resp != NULL);
+
+ http_server_connection_switch_ioloop_to(conn, ioloop);
+}
+
+struct ostream *
+http_server_ostream_create(struct http_server_response *resp,
+ size_t max_buffer_size, bool blocking)
+{
+ struct http_server_ostream *hsostream;
+
+ i_assert(resp->payload_stream == NULL);
+
+ hsostream = i_new(struct http_server_ostream, 1);
+
+ resp->payload_stream = hsostream;
+ http_server_response_ref(resp);
+ hsostream->conn = resp->request->conn;
+ hsostream->resp = resp;
+
+ hsostream->wostream.output_start = http_server_ostream_output_start;
+ hsostream->wostream.output_ready = http_server_ostream_output_ready;
+ hsostream->wostream.output_error = http_server_ostream_output_error;
+ hsostream->wostream.output_finish = http_server_ostream_output_finish;
+ hsostream->wostream.output_halt = http_server_ostream_output_halt;
+ hsostream->wostream.output_resume = http_server_ostream_output_resume;
+ hsostream->wostream.output_update_timeouts =
+ http_server_ostream_output_update_timeouts;
+
+ hsostream->wostream.wait_begin = http_server_ostream_wait_begin;
+ hsostream->wostream.wait_end = http_server_ostream_wait_end;
+
+ hsostream->wostream.switch_ioloop_to =
+ http_server_ostream_switch_ioloop_to;
+
+ hsostream->wostream.close = http_server_ostream_close;
+ hsostream->wostream.destroy = http_server_ostream_destroy;
+
+ return wrapper_ostream_create(&hsostream->wostream, max_buffer_size,
+ blocking, resp->event);
+}
+
+void http_server_ostream_response_finished(
+ struct http_server_ostream *hsostream)
+{
+ e_debug(hsostream->wostream.event, "Response payload finished");
+
+ wrapper_ostream_output_destroyed(&hsostream->wostream);
+}
+
+void http_server_ostream_response_destroyed(
+ struct http_server_ostream *hsostream)
+{
+ i_assert(hsostream->resp != NULL);
+ hsostream->resp->payload_stream = NULL;
+
+ e_debug(hsostream->wostream.event,
+ "Response payload parent stream lost");
+
+ hsostream->response_destroyed = TRUE;
+ wrapper_ostream_output_destroyed(&hsostream->wostream);
+ wrapper_ostream_notify_error(&hsostream->wostream);
+}
+
+struct ostream *
+http_server_ostream_get_output(struct http_server_ostream *hsostream)
+{
+ return &hsostream->wostream.ostream.ostream;
+}
+
+void http_server_ostream_set_error(struct http_server_ostream *hsostream,
+ int stream_errno, const char *stream_error)
+{
+ wrapper_ostream_set_error(&hsostream->wostream, stream_errno,
+ stream_error);
+}
diff --git a/src/lib-http/http-server-private.h b/src/lib-http/http-server-private.h
new file mode 100644
index 0000000..c07d873
--- /dev/null
+++ b/src/lib-http/http-server-private.h
@@ -0,0 +1,357 @@
+#ifndef HTTP_SERVER_PRIVATE_H
+#define HTTP_SERVER_PRIVATE_H
+
+#include "connection.h"
+
+#include "iostream-pump.h"
+#include "http-server.h"
+#include "llist.h"
+
+struct http_server_ostream;
+struct http_server_payload_handler;
+struct http_server_request;
+struct http_server_connection;
+
+/*
+ * Defaults
+ */
+
+#define HTTP_SERVER_REQUEST_MAX_TARGET_LENGTH 4096
+
+/*
+ * Types
+ */
+
+enum http_server_request_state {
+ /* New request; request header is still being parsed. */
+ HTTP_SERVER_REQUEST_STATE_NEW = 0,
+ /* Queued request; callback to request handler executing. */
+ HTTP_SERVER_REQUEST_STATE_QUEUED,
+ /* Reading request payload; request handler still needs to read more
+ payload. */
+ HTTP_SERVER_REQUEST_STATE_PAYLOAD_IN,
+ /* This request is being processed; request payload is fully read, but
+ no response is yet submitted */
+ HTTP_SERVER_REQUEST_STATE_PROCESSING,
+ /* A response is submitted for this request. If not all request payload
+ was read by the handler, it is first skipped on the input. */
+ HTTP_SERVER_REQUEST_STATE_SUBMITTED_RESPONSE,
+ /* Request is ready for response; a response is submitted and the
+ request payload is fully read */
+ HTTP_SERVER_REQUEST_STATE_READY_TO_RESPOND,
+ /* The response for the request is sent (apart from payload) */
+ HTTP_SERVER_REQUEST_STATE_SENT_RESPONSE,
+ /* Sending response payload to client */
+ HTTP_SERVER_REQUEST_STATE_PAYLOAD_OUT,
+ /* Request is finished; still lingering due to references */
+ HTTP_SERVER_REQUEST_STATE_FINISHED,
+ /* Request is aborted; still lingering due to references */
+ HTTP_SERVER_REQUEST_STATE_ABORTED
+};
+
+/*
+ * Objects
+ */
+
+struct http_server_payload_handler {
+ struct http_server_request *req;
+
+ void (*switch_ioloop)(struct http_server_payload_handler *handler,
+ struct ioloop *ioloop);
+ void (*destroy)(struct http_server_payload_handler *handler);
+
+ bool in_callback:1;
+};
+
+struct http_server_response {
+ struct http_server_request *request;
+ struct event *event;
+
+ unsigned int status;
+ const char *reason;
+
+ string_t *headers;
+ ARRAY_TYPE(string) perm_headers;
+ time_t date;
+ ARRAY_TYPE(http_auth_challenge) auth_challenges;
+
+ struct istream *payload_input;
+ uoff_t payload_size, payload_offset;
+ struct ostream *payload_output;
+ struct http_server_ostream *payload_stream;
+
+ http_server_tunnel_callback_t tunnel_callback;
+ void *tunnel_context;
+
+ bool have_hdr_connection:1;
+ bool have_hdr_date:1;
+ bool have_hdr_body_spec:1;
+
+ bool payload_chunked:1;
+ bool payload_finished:1;
+ bool payload_corked:1;
+ bool submitted:1;
+};
+
+struct http_server_request {
+ struct http_request req;
+ pool_t pool;
+ unsigned int refcount, immune_refcount;
+ unsigned int id;
+ int callback_refcount;
+ struct event *event;
+ uoff_t input_start_offset, output_start_offset;
+
+ enum http_server_request_state state;
+
+ struct http_server_request *prev, *next;
+
+ struct http_server *server;
+ struct http_server_connection *conn;
+
+ struct istream *payload_input;
+
+ struct http_server_response *response;
+
+ void (*destroy_callback)(void *);
+ void *destroy_context;
+
+ bool payload_halted:1;
+ bool sent_100_continue:1;
+ bool destroy_pending:1;
+ bool failed:1;
+ bool connection_close:1;
+};
+
+struct http_server_connection {
+ struct connection conn;
+ struct http_server *server;
+ struct ioloop *ioloop, *ioloop_switching;
+ struct event *event;
+ unsigned int refcount;
+
+ const struct http_server_callbacks *callbacks;
+ void *context;
+
+ struct timeout *to_input, *to_idle;
+ struct ssl_iostream *ssl_iostream;
+ struct http_request_parser *http_parser;
+
+ struct http_server_request *request_queue_head, *request_queue_tail;
+ unsigned int request_queue_count;
+
+ struct istream *incoming_payload;
+ struct http_server_payload_handler *payload_handler;
+
+ struct io *io_resp_payload;
+
+ char *disconnect_reason;
+
+ struct http_server_stats stats;
+
+ bool ssl:1;
+ bool closed:1;
+ bool close_indicated:1;
+ bool input_broken:1;
+ bool output_locked:1;
+ bool output_halted:1;
+ bool in_req_callback:1; /* performing request callback (busy) */
+};
+
+struct http_server_location {
+ const char *path;
+
+ struct http_server_resource *resource;
+};
+
+struct http_server_resource {
+ pool_t pool;
+ struct http_server *server;
+ struct event *event;
+
+ http_server_resource_callback_t *callback;
+ void *context;
+
+ void (*destroy_callback)(void *);
+ void *destroy_context;
+
+ ARRAY(struct http_server_location *) locations;
+};
+
+struct http_server {
+ pool_t pool;
+
+ struct http_server_settings set;
+
+ struct ioloop *ioloop;
+ struct event *event;
+ struct ssl_iostream_context *ssl_ctx;
+
+ struct connection_list *conn_list;
+
+ ARRAY(struct http_server_resource *) resources;
+ ARRAY(struct http_server_location *) locations;
+
+ bool shutting_down:1; /* shutting down server */
+};
+
+/*
+ * Response output stream
+ */
+
+struct ostream *
+http_server_ostream_create(struct http_server_response *resp,
+ size_t max_buffer_size, bool blocking);
+bool http_server_ostream_get_size(struct http_server_ostream *hsostream,
+ uoff_t *size_r);
+int http_server_ostream_continue(struct http_server_ostream *hsostream);
+
+void http_server_ostream_output_available(
+ struct http_server_ostream *hsostream);
+void http_server_ostream_response_finished(
+ struct http_server_ostream *hsostream);
+void http_server_ostream_response_destroyed(
+ struct http_server_ostream *hsostream);
+
+struct ostream *
+http_server_ostream_get_output(struct http_server_ostream *hsostream);
+
+void http_server_ostream_set_error(struct http_server_ostream *hsostream,
+ int stream_errno, const char *stream_error);
+
+/*
+ * Response
+ */
+
+void http_server_response_request_free(struct http_server_response *resp);
+void http_server_response_request_destroy(struct http_server_response *resp);
+void http_server_response_request_abort(struct http_server_response *resp,
+ const char *reason);
+void http_server_response_request_finished(struct http_server_response *resp);
+
+int http_server_response_send(struct http_server_response *resp);
+int http_server_response_send_more(struct http_server_response *resp);
+int http_server_response_finish_payload_out(struct http_server_response *resp);
+
+/*
+ * Request
+ */
+
+static inline bool
+http_server_request_is_new(struct http_server_request *req)
+{
+ return (req->state == HTTP_SERVER_REQUEST_STATE_NEW);
+}
+
+static inline bool
+http_server_request_version_equals(struct http_server_request *req,
+ unsigned int major, unsigned int minor)
+{
+ return (req->req.version_major == major &&
+ req->req.version_minor == minor);
+}
+
+const char *http_server_request_label(struct http_server_request *req);
+
+void http_server_request_update_event(struct http_server_request *req);
+
+struct http_server_request *
+http_server_request_new(struct http_server_connection *conn);
+void http_server_request_destroy(struct http_server_request **_req);
+void http_server_request_abort(struct http_server_request **_req,
+ const char *reason) ATTR_NULL(2);
+
+void http_server_request_immune_ref(struct http_server_request *req);
+void http_server_request_immune_unref(struct http_server_request **_req);
+
+bool http_server_request_is_complete(struct http_server_request *req);
+
+void http_server_request_received(struct http_server_request *req);
+void http_server_request_callback(struct http_server_request *req);
+
+void http_server_request_halt_payload(struct http_server_request *req);
+void http_server_request_continue_payload(struct http_server_request *req);
+
+void http_server_request_submit_response(struct http_server_request *req);
+
+void http_server_request_ready_to_respond(struct http_server_request *req);
+void http_server_request_finished(struct http_server_request *req);
+
+/* Payload handler */
+
+void http_server_payload_handler_destroy(
+ struct http_server_payload_handler **_handler);
+void http_server_payload_handler_switch_ioloop(
+ struct http_server_payload_handler *handler, struct ioloop *ioloop);
+
+/*
+ * Connection
+ */
+
+static inline void
+http_server_connection_add_request(struct http_server_connection *conn,
+ struct http_server_request *sreq)
+{
+ DLLIST2_APPEND(&conn->request_queue_head, &conn->request_queue_tail,
+ sreq);
+ conn->request_queue_count++;
+}
+static inline void
+http_server_connection_remove_request(struct http_server_connection *conn,
+ struct http_server_request *sreq)
+{
+ DLLIST2_REMOVE(&conn->request_queue_head, &conn->request_queue_tail,
+ sreq);
+ conn->request_queue_count--;
+}
+
+struct connection_list *http_server_connection_list_init(void);
+
+bool http_server_connection_shut_down(struct http_server_connection *conn);
+
+void http_server_connection_input_set_pending(
+ struct http_server_connection *conn);
+void http_server_connection_input_halt(struct http_server_connection *conn);
+void http_server_connection_input_resume(struct http_server_connection *conn);
+
+void http_server_connection_start_idle_timeout(
+ struct http_server_connection *conn);
+void http_server_connection_reset_idle_timeout(
+ struct http_server_connection *conn);
+void http_server_connection_stop_idle_timeout(
+ struct http_server_connection *conn);
+
+void http_server_connection_handle_output_error(
+ struct http_server_connection *conn);
+
+void http_server_connection_output_trigger(struct http_server_connection *conn);
+void http_server_connection_output_halt(struct http_server_connection *conn);
+void http_server_connection_output_resume(struct http_server_connection *conn);
+
+int http_server_connection_flush(struct http_server_connection *conn);
+int http_server_connection_output(struct http_server_connection *conn);
+
+void http_server_connection_tunnel(struct http_server_connection **_conn,
+ http_server_tunnel_callback_t callback,
+ void *context);
+
+bool http_server_connection_pending_payload(
+ struct http_server_connection *conn);
+
+/*
+ * Resource
+ */
+
+int http_server_resource_find(struct http_server *server, const char *path,
+ struct http_server_resource **res_r,
+ const char **sub_path_r) ATTR_NULL(2);
+
+bool http_server_resource_callback(struct http_server_request *req);
+
+/*
+ * Server
+ */
+
+int http_server_init_ssl_ctx(struct http_server *server, const char **error_r);
+
+#endif
diff --git a/src/lib-http/http-server-request.c b/src/lib-http/http-server-request.c
new file mode 100644
index 0000000..df2ce1a
--- /dev/null
+++ b/src/lib-http/http-server-request.c
@@ -0,0 +1,1006 @@
+/* Copyright (c) 2013-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "ioloop.h"
+#include "ostream.h"
+#include "istream-private.h"
+#include "str-sanitize.h"
+
+#include "http-server-private.h"
+
+/*
+ * Logging
+ */
+
+static inline void
+http_server_request_client_error(struct http_server_request *req,
+ const char *format, ...) ATTR_FORMAT(2, 3);
+
+static inline void
+http_server_request_client_error(struct http_server_request *req,
+ const char *format, ...)
+{
+ va_list args;
+
+ va_start(args, format);
+ e_info(req->event, "%s", t_strdup_vprintf(format, args));
+ va_end(args);
+}
+
+/*
+ * Request
+ */
+
+const char *http_server_request_label(struct http_server_request *req)
+{
+ if (req->req.target_raw == NULL) {
+ if (req->req.method == NULL)
+ return t_strdup_printf("[Req%u: <NEW>]", req->id);
+ return t_strdup_printf("[Req%u: %s <INCOMPLETE>]",
+ req->id, req->req.method);
+ }
+ return t_strdup_printf("[Req%u: %s %s]", req->id,
+ req->req.method, req->req.target_raw);
+}
+
+void http_server_request_update_event(struct http_server_request *req)
+{
+ if (req->req.method != NULL)
+ event_add_str(req->event, "method", req->req.method);
+ if (req->req.target_raw != NULL)
+ event_add_str(req->event, "target", req->req.target_raw);
+ event_add_int(req->event, "request_id", req->id);
+ event_set_append_log_prefix(
+ req->event, t_strdup_printf("request %s: ",
+ str_sanitize(http_server_request_label(req), 256)));
+}
+
+struct http_server_request *
+http_server_request_new(struct http_server_connection *conn)
+{
+ static unsigned int id_counter = 0;
+ pool_t pool;
+ struct http_server_request *req;
+
+ pool = pool_alloconly_create(
+ MEMPOOL_GROWING"http_server_request", 4096);
+ req = p_new(pool, struct http_server_request, 1);
+ req->pool = pool;
+ req->refcount = 1;
+ req->conn = conn;
+ req->server = conn->server;
+ req->id = ++id_counter;
+ req->event = event_create(conn->event);
+ req->input_start_offset = conn->conn.input->v_offset;
+ req->output_start_offset = conn->conn.output->offset;
+ http_server_request_update_event(req);
+
+ http_server_connection_add_request(conn, req);
+ return req;
+}
+
+void http_server_request_ref(struct http_server_request *req)
+{
+ i_assert(req->refcount > 0);
+ req->refcount++;
+}
+
+bool http_server_request_unref(struct http_server_request **_req)
+{
+ struct http_server_request *req = *_req;
+ struct http_server_connection *conn = req->conn;
+
+ i_assert(req->refcount > 0);
+
+ *_req = NULL;
+ if (--req->refcount > 0)
+ return TRUE;
+
+ e_debug(req->event, "Free");
+
+ if (req->state < HTTP_SERVER_REQUEST_STATE_FINISHED) {
+ req->state = HTTP_SERVER_REQUEST_STATE_ABORTED;
+ http_server_connection_remove_request(conn, req);
+ }
+
+ if (req->destroy_callback != NULL) {
+ req->destroy_callback(req->destroy_context);
+ req->destroy_callback = NULL;
+ }
+
+ if (req->response != NULL)
+ http_server_response_request_free(req->response);
+ event_unref(&req->event);
+ pool_unref(&req->pool);
+ return FALSE;
+}
+
+void http_server_request_connection_close(struct http_server_request *req,
+ bool close)
+{
+ i_assert(req->state < HTTP_SERVER_REQUEST_STATE_SENT_RESPONSE);
+ req->connection_close = close;
+}
+
+void http_server_request_destroy(struct http_server_request **_req)
+{
+ struct http_server_request *req = *_req;
+ struct http_server *server = req->server;
+
+ e_debug(req->event, "Destroy");
+
+ /* Just make sure the request ends in a proper state */
+ if (req->state < HTTP_SERVER_REQUEST_STATE_FINISHED)
+ req->state = HTTP_SERVER_REQUEST_STATE_ABORTED;
+
+ if (server->ioloop != NULL)
+ io_loop_stop(server->ioloop);
+
+ if (req->immune_refcount > 0) {
+ req->destroy_pending = TRUE;
+ http_server_request_unref(_req);
+ return;
+ }
+
+ if (req->response != NULL)
+ http_server_response_request_destroy(req->response);
+
+ if (req->destroy_callback != NULL) {
+ void (*callback)(void *) = req->destroy_callback;
+
+ req->destroy_callback = NULL;
+ callback(req->destroy_context);
+ }
+
+ http_server_request_unref(_req);
+}
+
+#undef http_server_request_set_destroy_callback
+void http_server_request_set_destroy_callback(struct http_server_request *req,
+ void (*callback)(void *),
+ void *context)
+{
+ req->destroy_callback = callback;
+ req->destroy_context = context;
+}
+
+void http_server_request_abort(struct http_server_request **_req,
+ const char *reason)
+{
+ struct http_server_request *req = *_req;
+ struct http_server_connection *conn = req->conn;
+
+ if (req->state >= HTTP_SERVER_REQUEST_STATE_FINISHED)
+ return;
+
+ if (reason == NULL)
+ e_debug(req->event, "Abort");
+ else
+ e_debug(req->event, "Abort: %s", reason);
+
+ if (req->response != NULL)
+ http_server_response_request_abort(req->response, reason);
+
+ req->conn = NULL;
+ if (req->state < HTTP_SERVER_REQUEST_STATE_FINISHED) {
+ if (conn != NULL) {
+ http_server_connection_remove_request(conn, req);
+
+ if (!conn->closed) {
+ /* Send best-effort response if appropriate */
+ if (!conn->output_locked &&
+ req->state >= HTTP_SERVER_REQUEST_STATE_PROCESSING &&
+ req->state < HTTP_SERVER_REQUEST_STATE_SENT_RESPONSE) {
+ static const char *response =
+ "HTTP/1.1 500 Internal Server Error\r\n"
+ "Content-Length: 0\r\n"
+ "\r\n";
+
+ o_stream_nsend(conn->conn.output,
+ response, strlen(response));
+ (void)o_stream_flush(conn->conn.output);
+ }
+
+ /* Close the connection */
+ http_server_connection_close(&conn, reason);
+ }
+ }
+
+ req->state = HTTP_SERVER_REQUEST_STATE_ABORTED;
+ }
+
+ http_server_request_destroy(_req);
+}
+
+void http_server_request_immune_ref(struct http_server_request *req)
+{
+ http_server_request_ref(req);
+ req->immune_refcount++;
+}
+
+void http_server_request_immune_unref(struct http_server_request **_req)
+{
+ struct http_server_request *req = *_req;
+
+ i_assert(req->immune_refcount > 0);
+
+ *_req = NULL;
+ if (--req->immune_refcount == 0 && req->destroy_pending)
+ http_server_request_destroy(&req);
+ else
+ http_server_request_unref(&req);
+}
+
+const struct http_request *
+http_server_request_get(struct http_server_request *req)
+{
+ return &req->req;
+}
+
+pool_t http_server_request_get_pool(struct http_server_request *req)
+{
+ return req->pool;
+}
+
+struct http_server_response *
+http_server_request_get_response(struct http_server_request *req)
+{
+ return req->response;
+}
+
+int http_server_request_get_auth(struct http_server_request *req,
+ struct http_auth_credentials *credentials)
+{
+ const char *auth;
+
+ auth = http_request_header_get(&req->req, "Authorization");
+ if (auth == NULL)
+ return 0;
+
+ if (http_auth_parse_credentials((const unsigned char *)auth,
+ strlen(auth), credentials) < 0)
+ return -1;
+
+ return 1;
+}
+
+bool http_server_request_is_finished(struct http_server_request *req)
+{
+ return (req->response != NULL ||
+ req->state == HTTP_SERVER_REQUEST_STATE_ABORTED);
+}
+
+bool http_server_request_is_complete(struct http_server_request *req)
+{
+ return (req->failed || req->conn->input_broken ||
+ (req->next != NULL && !http_server_request_is_new(req->next)) ||
+ !http_server_connection_pending_payload(req->conn));
+}
+
+void http_server_request_halt_payload(struct http_server_request *req)
+{
+ i_assert(req->state <= HTTP_SERVER_REQUEST_STATE_QUEUED);
+ req->payload_halted = TRUE;
+}
+
+void http_server_request_continue_payload(struct http_server_request *req)
+{
+ i_assert(req->state <= HTTP_SERVER_REQUEST_STATE_QUEUED);
+ req->payload_halted = FALSE;
+ if (req->req.expect_100_continue && !req->sent_100_continue)
+ http_server_connection_output_trigger(req->conn);
+}
+
+static void
+http_server_request_connect_callback(struct http_server_request *req)
+{
+ struct http_server_connection *conn = req->conn;
+
+ if (conn->callbacks->handle_connect_request == NULL) {
+ http_server_request_fail(req, 505, "Not Implemented");
+ return;
+ }
+
+ if (req->req.target.format !=
+ HTTP_REQUEST_TARGET_FORMAT_AUTHORITY) {
+ http_server_request_fail(req, 400, "Bad Request");
+ return;
+ }
+
+ conn->callbacks->handle_connect_request(conn->context, req,
+ req->req.target.url);
+}
+
+static void
+http_server_request_default_handler(struct http_server_request *req)
+{
+ const struct http_request *hreq = &req->req;
+ struct http_server_response *resp;
+
+ if (strcmp(hreq->method, "OPTIONS") == 0 &&
+ hreq->target.format == HTTP_REQUEST_TARGET_FORMAT_ASTERISK) {
+ resp = http_server_response_create(req, 200, "OK");
+ http_server_response_submit(resp);
+ return;
+ }
+
+ http_server_request_fail(req, 404, "Not Found");
+ return;
+}
+
+void http_server_request_received(struct http_server_request *req)
+{
+ http_server_request_update_event(req);
+ struct event_passthrough *e = event_create_passthrough(req->event)->
+ set_name("http_server_request_started");
+ e_debug(e->event(), "Received new request %s "
+ "(%u requests pending; %u maximum)",
+ http_server_request_label(req),
+ req->conn->request_queue_count,
+ req->conn->server->set.max_pipelined_requests);
+}
+
+void http_server_request_callback(struct http_server_request *req)
+{
+ struct http_server_connection *conn = req->conn;
+
+ if (strcmp(req->req.method, "CONNECT") == 0) {
+ /* CONNECT method */
+ http_server_request_connect_callback(req);
+ return;
+ }
+
+ if (http_server_resource_callback(req))
+ return;
+
+ if (array_count(&req->server->resources) > 0)
+ e_debug(req->event, "No matching resource found");
+
+ if (conn->callbacks->handle_request == NULL) {
+ http_server_request_default_handler(req);
+ return;
+ }
+ conn->callbacks->handle_request(conn->context, req);
+}
+
+void http_server_request_ready_to_respond(struct http_server_request *req)
+{
+ e_debug(req->event, "Ready to respond");
+
+ req->state = HTTP_SERVER_REQUEST_STATE_READY_TO_RESPOND;
+ http_server_connection_output_trigger(req->conn);
+}
+
+void http_server_request_submit_response(struct http_server_request *req)
+{
+ struct http_server_connection *conn = req->conn;
+
+ i_assert(conn != NULL && req->response != NULL &&
+ req->response->submitted);
+
+ http_server_request_ref(req);
+
+ if (conn->payload_handler != NULL && conn->payload_handler->req == req)
+ http_server_payload_handler_destroy(&conn->payload_handler);
+
+ switch (req->state) {
+ case HTTP_SERVER_REQUEST_STATE_NEW:
+ case HTTP_SERVER_REQUEST_STATE_QUEUED:
+ case HTTP_SERVER_REQUEST_STATE_PAYLOAD_IN:
+ case HTTP_SERVER_REQUEST_STATE_PROCESSING:
+ case HTTP_SERVER_REQUEST_STATE_SUBMITTED_RESPONSE:
+ if (!http_server_request_is_complete(req)) {
+ e_debug(req->event, "Not ready to respond");
+ req->state = HTTP_SERVER_REQUEST_STATE_SUBMITTED_RESPONSE;
+ http_server_connection_input_resume(req->conn);
+ http_server_connection_input_set_pending(req->conn);
+ break;
+ }
+ http_server_request_ready_to_respond(req);
+ break;
+ case HTTP_SERVER_REQUEST_STATE_READY_TO_RESPOND:
+ http_server_connection_output_trigger(req->conn);
+ break;
+ case HTTP_SERVER_REQUEST_STATE_ABORTED:
+ break;
+ default:
+ i_unreached();
+ }
+
+ http_server_request_unref(&req);
+}
+
+void http_server_request_finished(struct http_server_request *req)
+{
+ struct http_server_connection *conn = req->conn;
+ struct http_server_response *resp = req->response;
+ http_server_tunnel_callback_t tunnel_callback = resp->tunnel_callback;
+ void *tunnel_context = resp->tunnel_context;
+
+ i_assert(req->state < HTTP_SERVER_REQUEST_STATE_FINISHED);
+ req->state = HTTP_SERVER_REQUEST_STATE_FINISHED;
+
+ http_server_connection_remove_request(conn, req);
+ conn->stats.response_count++;
+
+ if (req->response != NULL)
+ http_server_response_request_finished(req->response);
+
+ uoff_t bytes_in = req->conn->conn.input->v_offset -
+ req->input_start_offset;
+ uoff_t bytes_out = req->conn->conn.output->offset -
+ req->output_start_offset;
+ struct event_passthrough *e = event_create_passthrough(req->event)->
+ set_name("http_server_request_finished")->
+ add_int("bytes_in", bytes_in)->
+ add_int("bytes_out", bytes_out);
+ e_debug(e->event(), "Finished request");
+
+ if (tunnel_callback == NULL) {
+ if (req->connection_close) {
+ http_server_connection_close(&conn,
+ t_strdup_printf(
+ "Server closed connection: %u %s",
+ resp->status, resp->reason));
+ http_server_request_destroy(&req);
+ return;
+ } else if (req->conn->input_broken) {
+ http_server_connection_close(
+ &conn, "Connection input is broken");
+ http_server_request_destroy(&req);
+ return;
+ } else if (req->req.connection_close) {
+ http_server_connection_close(
+ &conn, "Client requested connection close");
+ http_server_request_destroy(&req);
+ return;
+ }
+ }
+
+ http_server_request_destroy(&req);
+ if (tunnel_callback != NULL) {
+ http_server_connection_tunnel(&conn, tunnel_callback,
+ tunnel_context);
+ return;
+ }
+
+ http_server_connection_output_trigger(conn);
+}
+
+static struct http_server_response *
+http_server_request_create_fail_response(struct http_server_request *req,
+ unsigned int status,
+ const char *reason, const char *text)
+ ATTR_NULL(4)
+{
+ struct http_server_response *resp;
+
+ req->failed = TRUE;
+
+ i_assert(status / 100 != 1 && status != 204 && status != 304);
+
+ resp = http_server_response_create(req, status, reason);
+ if (!http_request_method_is(&req->req, "HEAD")) {
+ http_server_response_add_header(resp, "Content-Type",
+ "text/plain; charset=utf-8");
+ if (text == NULL)
+ text = reason;
+ text = t_strconcat(text, "\r\n", NULL);
+ http_server_response_set_payload_data(
+ resp, (const unsigned char *)text, strlen(text));
+ }
+
+ return resp;
+}
+
+static void
+http_server_request_fail_full(struct http_server_request *req,
+ unsigned int status, const char *reason,
+ const char *text) ATTR_NULL(4)
+{
+ struct http_server_response *resp;
+
+ req->failed = TRUE;
+ resp = http_server_request_create_fail_response(req, status, reason,
+ text);
+ http_server_response_submit(resp);
+ if (req->conn->input_broken)
+ req->connection_close = TRUE;
+}
+
+void http_server_request_fail(struct http_server_request *req,
+ unsigned int status, const char *reason)
+{
+ http_server_request_fail_full(req, status, reason, NULL);
+}
+
+void http_server_request_fail_close(struct http_server_request *req,
+ unsigned int status, const char *reason)
+{
+ http_server_request_connection_close(req, TRUE);
+ http_server_request_fail_full(req, status, reason, NULL);
+}
+
+void http_server_request_fail_text(struct http_server_request *req,
+ unsigned int status, const char *reason,
+ const char *format, ...)
+{
+ va_list args;
+
+ va_start(args, format);
+ http_server_request_fail_full(req, status, reason,
+ t_strdup_vprintf(format, args));
+ va_end(args);
+}
+
+void http_server_request_fail_auth(struct http_server_request *req,
+ const char *reason,
+ const struct http_auth_challenge *chlng)
+{
+ struct http_server_response *resp;
+
+ req->failed = TRUE;
+
+ if (reason == NULL)
+ reason = "Unauthenticated";
+
+ resp = http_server_request_create_fail_response(req, 401, reason,
+ reason);
+ http_server_response_add_auth(resp, chlng);
+ http_server_response_submit(resp);
+}
+
+void http_server_request_fail_auth_basic(struct http_server_request *req,
+ const char *reason, const char *realm)
+{
+ struct http_auth_challenge chlng;
+
+ http_auth_basic_challenge_init(&chlng, realm);
+ http_server_request_fail_auth(req, reason, &chlng);
+}
+
+void http_server_request_fail_bad_method(struct http_server_request *req,
+ const char *allow)
+{
+ struct http_server_response *resp;
+ const char *reason = "Method Not Allowed";
+
+ req->failed = TRUE;
+
+ resp = http_server_request_create_fail_response(req, 405, reason,
+ reason);
+ http_server_response_add_header(resp, "Allow", allow);
+ http_server_response_submit(resp);
+}
+
+/*
+ * Payload input stream
+ */
+
+struct http_server_istream {
+ struct istream_private istream;
+
+ struct http_server_request *req;
+
+ ssize_t read_status;
+};
+
+static void
+http_server_istream_switch_ioloop_to(struct istream_private *stream,
+ struct ioloop *ioloop)
+{
+ struct http_server_istream *hsristream =
+ (struct http_server_istream *)stream;
+
+ if (hsristream->istream.istream.blocking)
+ return;
+
+ i_assert(ioloop == current_ioloop);
+ http_server_connection_switch_ioloop(hsristream->req->conn);
+}
+
+static void
+http_server_istream_read_any(struct http_server_istream *hsristream)
+{
+ struct istream_private *stream = &hsristream->istream;
+ struct http_server *server = hsristream->req->server;
+ ssize_t ret;
+
+ if ((ret = i_stream_read_copy_from_parent(&stream->istream)) != 0) {
+ hsristream->read_status = ret;
+ io_loop_stop(server->ioloop);
+ }
+}
+
+static ssize_t
+http_server_istream_read(struct istream_private *stream)
+{
+ struct http_server_istream *hsristream =
+ (struct http_server_istream *)stream;
+ struct http_server_request *req = hsristream->req;
+ struct http_server *server;
+ struct http_server_connection *conn;
+ bool blocking = stream->istream.blocking;
+ ssize_t ret;
+
+ if (req == NULL) {
+ /* Request already gone (we shouldn't get here) */
+ stream->istream.stream_errno = EINVAL;
+ return -1;
+ }
+
+ i_stream_seek(stream->parent, stream->parent_start_offset +
+ stream->istream.v_offset);
+
+ server = hsristream->req->server;
+ conn = hsristream->req->conn;
+
+ ret = i_stream_read_copy_from_parent(&stream->istream);
+ if (ret == 0 && blocking) {
+ struct ioloop *prev_ioloop = current_ioloop;
+ struct io *io;
+
+ http_server_connection_ref(conn);
+ http_server_request_ref(req);
+
+ i_assert(server->ioloop == NULL);
+ server->ioloop = io_loop_create();
+ http_server_connection_switch_ioloop(conn);
+
+ if (blocking && req->req.expect_100_continue &&
+ !req->sent_100_continue)
+ http_server_connection_output_trigger(conn);
+
+ hsristream->read_status = 0;
+ io = io_add_istream(&stream->istream,
+ http_server_istream_read_any, hsristream);
+ while (req->state < HTTP_SERVER_REQUEST_STATE_FINISHED &&
+ hsristream->read_status == 0) {
+ io_loop_run(server->ioloop);
+ }
+ io_remove(&io);
+
+ io_loop_set_current(prev_ioloop);
+ http_server_connection_switch_ioloop(conn);
+ io_loop_set_current(server->ioloop);
+ io_loop_destroy(&server->ioloop);
+
+ ret = hsristream->read_status;
+
+ if (!http_server_request_unref(&req))
+ hsristream->req = NULL;
+ http_server_connection_unref(&conn);
+ }
+
+ return ret;
+}
+
+static void
+http_server_istream_destroy(struct iostream_private *stream)
+{
+ struct http_server_istream *hsristream =
+ (struct http_server_istream *)stream;
+ uoff_t v_offset;
+
+ v_offset = hsristream->istream.parent_start_offset +
+ hsristream->istream.istream.v_offset;
+ if (hsristream->istream.parent->seekable ||
+ v_offset > hsristream->istream.parent->v_offset) {
+ /* Get to same position in parent stream */
+ i_stream_seek(hsristream->istream.parent, v_offset);
+ }
+}
+
+struct istream *
+http_server_request_get_payload_input(struct http_server_request *req,
+ bool blocking)
+{
+ struct http_server_istream *hsristream;
+ struct istream *payload = req->req.payload;
+
+ i_assert(req->payload_input == NULL);
+
+ hsristream = i_new(struct http_server_istream, 1);
+ hsristream->req = req;
+ hsristream->istream.max_buffer_size =
+ payload->real_stream->max_buffer_size;
+ hsristream->istream.stream_size_passthrough = TRUE;
+
+ hsristream->istream.read = http_server_istream_read;
+ hsristream->istream.switch_ioloop_to =
+ http_server_istream_switch_ioloop_to;
+ hsristream->istream.iostream.destroy = http_server_istream_destroy;
+
+ hsristream->istream.istream.readable_fd = FALSE;
+ hsristream->istream.istream.blocking = blocking;
+ hsristream->istream.istream.seekable = FALSE;
+
+ req->payload_input = i_stream_create(&hsristream->istream, payload,
+ i_stream_get_fd(payload), 0);
+ i_stream_unref(&req->req.payload);
+ return req->payload_input;
+}
+
+/*
+ * Payload handling
+ */
+
+static void
+http_server_payload_handler_init(struct http_server_payload_handler *handler,
+ struct http_server_request *req)
+{
+ struct http_server_connection *conn = req->conn;
+
+ i_assert(conn->payload_handler == NULL);
+ i_assert(conn->in_req_callback);
+
+ conn->payload_handler = handler;
+
+ handler->req = req;
+}
+
+void http_server_payload_handler_destroy(
+ struct http_server_payload_handler **_handler)
+{
+ struct http_server_payload_handler *handler = *_handler;
+ struct http_server_connection *conn = handler->req->conn;
+
+ if (handler->in_callback) {
+ /* Don't destroy handler while in callback */
+ return;
+ }
+
+ *_handler = NULL;
+ i_assert(conn->payload_handler == NULL);
+
+ if (handler->destroy != NULL)
+ handler->destroy(handler);
+}
+
+void http_server_payload_handler_switch_ioloop(
+ struct http_server_payload_handler *handler, struct ioloop *ioloop)
+{
+ if (handler->switch_ioloop != NULL)
+ handler->switch_ioloop(handler, ioloop);
+}
+
+/* Pump-based */
+
+struct http_server_payload_handler_pump {
+ struct http_server_payload_handler handler;
+
+ struct iostream_pump *pump;
+
+ void (*callback)(void *);
+ void *context;
+};
+
+static void
+payload_handler_pump_destroy(struct http_server_payload_handler *handler)
+{
+ struct http_server_payload_handler_pump *phandler =
+ (struct http_server_payload_handler_pump *)handler;
+
+ iostream_pump_unref(&phandler->pump);
+}
+
+static void
+payload_handler_pump_switch_ioloop(struct http_server_payload_handler *handler,
+ struct ioloop *ioloop)
+{
+ struct http_server_payload_handler_pump *phandler =
+ (struct http_server_payload_handler_pump *)handler;
+
+ iostream_pump_switch_ioloop_to(phandler->pump, ioloop);
+}
+
+static void
+payload_handler_pump_callback(enum iostream_pump_status status,
+ struct http_server_payload_handler_pump *phandler)
+{
+ struct http_server_payload_handler *handler = &phandler->handler;
+ struct http_server_request *req = handler->req;
+ struct http_server_connection *conn = req->conn;
+ struct istream *input = iostream_pump_get_input(phandler->pump);
+ struct ostream *output = iostream_pump_get_output(phandler->pump);
+
+ switch (status) {
+ case IOSTREAM_PUMP_STATUS_INPUT_EOF:
+ if (!i_stream_read_eof(conn->incoming_payload)) {
+ http_server_request_fail_close(req, 413,
+ "Payload Too Large");
+ } else {
+ unsigned int old_refcount = req->refcount;
+
+ handler->in_callback = TRUE;
+ phandler->callback(phandler->context);
+ req->callback_refcount += req->refcount - old_refcount;
+ handler->in_callback = FALSE;
+
+ i_assert(req->callback_refcount > 0 ||
+ (req->response != NULL &&
+ req->response->submitted));
+ }
+ break;
+ case IOSTREAM_PUMP_STATUS_INPUT_ERROR:
+ http_server_request_client_error(
+ req, "iostream_pump: read(%s) failed: %s",
+ i_stream_get_name(input), i_stream_get_error(input));
+ http_server_request_fail_close(req, 400, "Bad Request");
+ break;
+ case IOSTREAM_PUMP_STATUS_OUTPUT_ERROR:
+ if (output->stream_errno != 0) {
+ e_error(req->event,
+ "iostream_pump: write(%s) failed: %s",
+ o_stream_get_name(output),
+ o_stream_get_error(output));
+ }
+ http_server_request_fail_close(req, 500,
+ "Internal Server Error");
+ break;
+ }
+
+ if (conn->payload_handler != NULL)
+ http_server_payload_handler_destroy(&conn->payload_handler);
+}
+
+#undef http_server_request_forward_payload
+void http_server_request_forward_payload(struct http_server_request *req,
+ struct ostream *output,
+ uoff_t max_size,
+ void (*callback)(void *),
+ void *context)
+{
+ struct http_server_connection *conn = req->conn;
+ struct istream *input = conn->incoming_payload;
+ struct http_server_payload_handler_pump *phandler;
+ uoff_t payload_size;
+ int ret;
+
+ i_assert(req->req.payload != NULL);
+
+ if (max_size == UOFF_T_MAX) {
+ i_stream_ref(input);
+ } else {
+ if ((ret = i_stream_get_size(input, TRUE,
+ &payload_size)) != 0) {
+ if (ret < 0) {
+ e_error(req->event,
+ "i_stream_get_size(%s) failed: %s",
+ i_stream_get_name(input),
+ i_stream_get_error(input));
+ http_server_request_fail_close(
+ req, 500, "Internal Server Error");
+ return;
+ }
+ if (payload_size > max_size) {
+ http_server_request_fail_close(
+ req, 413, "Payload Too Large");
+ return;
+ }
+ }
+ input = i_stream_create_limit(input, max_size);
+ }
+
+ phandler = p_new(req->pool, struct http_server_payload_handler_pump, 1);
+ http_server_payload_handler_init(&phandler->handler, req);
+ phandler->handler.switch_ioloop = payload_handler_pump_switch_ioloop;
+ phandler->handler.destroy = payload_handler_pump_destroy;
+ phandler->callback = callback;
+ phandler->context = context;
+
+ phandler->pump = iostream_pump_create(input, output);
+ iostream_pump_set_completion_callback(phandler->pump,
+ payload_handler_pump_callback,
+ phandler);
+ iostream_pump_start(phandler->pump);
+ i_stream_unref(&input);
+}
+
+#undef http_server_request_buffer_payload
+void http_server_request_buffer_payload(struct http_server_request *req,
+ buffer_t *buffer, uoff_t max_size,
+ void (*callback)(void *),
+ void *context)
+{
+ struct ostream *output;
+
+ output = o_stream_create_buffer(buffer);
+ http_server_request_forward_payload(req,
+ output, max_size, callback, context);
+ o_stream_unref(&output);
+}
+
+/* Raw */
+
+struct http_server_payload_handler_raw {
+ struct http_server_payload_handler handler;
+
+ struct io *io;
+
+ void (*callback)(void *context);
+ void *context;
+};
+
+static void
+payload_handler_raw_destroy(struct http_server_payload_handler *handler)
+{
+ struct http_server_payload_handler_raw *rhandler =
+ (struct http_server_payload_handler_raw *)handler;
+
+ io_remove(&rhandler->io);
+}
+
+static void
+payload_handler_raw_switch_ioloop(struct http_server_payload_handler *handler,
+ struct ioloop *ioloop)
+{
+ struct http_server_payload_handler_raw *rhandler =
+ (struct http_server_payload_handler_raw *)handler;
+
+ rhandler->io = io_loop_move_io_to(ioloop, &rhandler->io);
+}
+
+static void
+payload_handler_raw_input(struct http_server_payload_handler_raw *rhandler)
+{
+ struct http_server_payload_handler *handler = &rhandler->handler;
+ struct http_server_request *req = handler->req;
+ struct http_server_connection *conn = req->conn;
+ struct istream *input = conn->incoming_payload;
+ unsigned int old_refcount = req->refcount;
+
+ handler->in_callback = TRUE;
+ rhandler->callback(rhandler->context);
+ req->callback_refcount += req->refcount - old_refcount;
+ handler->in_callback = FALSE;
+
+ if (input != NULL && input->stream_errno != 0) {
+ if (req->response == NULL) {
+ http_server_request_client_error(
+ req, "read(%s) failed: %s",
+ i_stream_get_name(input),
+ i_stream_get_error(input));
+ http_server_request_fail_close(req, 400, "Bad Request");
+ }
+ } else if (input == NULL || !i_stream_have_bytes_left(input)) {
+ i_assert(req->callback_refcount > 0 ||
+ (req->response != NULL && req->response->submitted));
+ } else {
+ return;
+ }
+
+ if (conn->payload_handler != NULL)
+ http_server_payload_handler_destroy(&conn->payload_handler);
+
+}
+
+#undef http_server_request_handle_payload
+void http_server_request_handle_payload(struct http_server_request *req,
+ void (*callback)(void *context),
+ void *context)
+{
+ struct http_server_payload_handler_raw *rhandler;
+ struct http_server_connection *conn = req->conn;
+
+ rhandler = p_new(req->pool, struct http_server_payload_handler_raw, 1);
+ http_server_payload_handler_init(&rhandler->handler, req);
+ rhandler->handler.switch_ioloop = payload_handler_raw_switch_ioloop;
+ rhandler->handler.destroy = payload_handler_raw_destroy;
+ rhandler->callback = callback;
+ rhandler->context = context;
+
+ rhandler->io = io_add_istream(conn->incoming_payload,
+ payload_handler_raw_input, rhandler);
+ i_stream_set_input_pending(conn->incoming_payload, TRUE);
+}
+
+void http_server_request_add_response_header(struct http_server_request *req,
+ const char *key, const char *value)
+{
+ struct http_server_response *resp;
+
+ resp = http_server_response_create(req, 0, "");
+ http_server_response_add_permanent_header(resp, key, value);
+}
diff --git a/src/lib-http/http-server-resource.c b/src/lib-http/http-server-resource.c
new file mode 100644
index 0000000..dc602e4
--- /dev/null
+++ b/src/lib-http/http-server-resource.c
@@ -0,0 +1,276 @@
+/* Copyright (c) 2013-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "bsearch-insert-pos.h"
+#include "str-sanitize.h"
+
+#include "http-url.h"
+#include "http-server-private.h"
+
+static struct event_category event_category_http_server_resource = {
+ .name = "http-server-resource"
+};
+
+/*
+ * Location
+ */
+
+static int
+http_server_location_cmp(struct http_server_location *const *loc1,
+ struct http_server_location *const *loc2)
+{
+ return strcmp((*loc1)->path, (*loc2)->path);
+}
+
+static struct http_server_location *
+http_server_location_add(struct http_server *server, pool_t pool,
+ const char *path)
+{
+ struct http_server_location qloc, *loc;
+ unsigned int insert_idx;
+
+ i_zero(&qloc);
+ qloc.path = path;
+ loc = &qloc;
+
+ if (array_bsearch_insert_pos(&server->locations, &loc,
+ http_server_location_cmp, &insert_idx))
+ return array_idx_elem(&server->locations, insert_idx);
+
+ loc = p_new(pool, struct http_server_location, 1);
+ loc->path = p_strdup(pool, path);
+ array_insert(&server->locations, insert_idx, &loc, 1);
+ return loc;
+}
+
+static int
+http_server_location_find(struct http_server *server, const char *path,
+ struct http_server_location **loc_r,
+ const char **sub_path_r)
+{
+ struct http_server_location qloc, *loc;
+ size_t loc_len;
+ unsigned int insert_idx;
+
+ *sub_path_r = NULL;
+ *loc_r = NULL;
+
+ i_zero(&qloc);
+ qloc.path = path;
+ loc = &qloc;
+
+ if (array_bsearch_insert_pos(&server->locations, &loc,
+ http_server_location_cmp, &insert_idx)) {
+ /* Exact match */
+ *loc_r = array_idx_elem(&server->locations, insert_idx);
+ *sub_path_r = "";
+ return 1;
+ }
+ if (insert_idx == 0) {
+ /* Not found at all */
+ return -1;
+ }
+ loc = array_idx_elem(&server->locations, insert_idx-1);
+
+ loc_len = strlen(loc->path);
+ if (!str_begins(path, loc->path)) {
+ /* Location isn't a prefix of path */
+ return -1;
+ } else if (path[loc_len] != '/') {
+ /* Match doesn't end at '/' */
+ return -1;
+ }
+
+ *sub_path_r = &path[loc_len + 1];
+ *loc_r = loc;
+ return 0;
+}
+
+static void
+http_server_location_remove(struct http_server *server,
+ struct http_server_location *loc)
+{
+ struct http_server_location *const *locp;
+
+ array_foreach(&server->locations, locp) {
+ if (*locp == loc) {
+ array_delete(
+ &server->locations,
+ array_foreach_idx(&server->locations, locp), 1);
+ return;
+ }
+ }
+}
+
+/*
+ * Resource
+ */
+
+static void http_server_resource_update_event(struct http_server_resource *res)
+{
+ struct http_server_location *const *locs;
+ unsigned int locs_count;
+
+ locs = array_get(&res->locations, &locs_count);
+ if (locs_count == 0) {
+ event_set_append_log_prefix(res->event, "resource: ");
+ return;
+ }
+
+ event_add_str(res->event, "path", locs[0]->path);
+ event_set_append_log_prefix(
+ res->event, t_strdup_printf("resource %s: ",
+ str_sanitize(locs[0]->path, 128)));
+}
+
+#undef http_server_resource_create
+struct http_server_resource *
+http_server_resource_create(struct http_server *server, pool_t pool,
+ http_server_resource_callback_t *callback,
+ void *context)
+{
+ struct http_server_resource *res;
+
+ pool_ref(pool);
+
+ res = p_new(pool, struct http_server_resource, 1);
+ res->pool = pool;
+ res->server = server;
+
+ res->callback = callback;
+ res->context = context;
+
+ p_array_init(&res->locations, pool, 4);
+
+ res->event = event_create(server->event);
+ event_add_category(res->event, &event_category_http_server_resource);
+ http_server_resource_update_event(res);
+
+ array_append(&server->resources, &res, 1);
+
+ return res;
+}
+
+void http_server_resource_free(struct http_server_resource **_res)
+{
+ struct http_server_resource *res = *_res;
+ struct http_server_location *loc;
+
+ if (res == NULL)
+ return;
+
+ *_res = NULL;
+
+ e_debug(res->event, "Free");
+
+ if (res->destroy_callback != NULL) {
+ res->destroy_callback(res->destroy_context);
+ res->destroy_callback = NULL;
+ }
+
+ array_foreach_elem(&res->locations, loc)
+ http_server_location_remove(res->server, loc);
+
+ event_unref(&res->event);
+ pool_unref(&res->pool);
+}
+
+pool_t http_server_resource_get_pool(struct http_server_resource *res)
+{
+ return res->pool;
+}
+
+const char *http_server_resource_get_path(struct http_server_resource *res)
+{
+ struct http_server_location *const *locs;
+ unsigned int locs_count;
+
+ locs = array_get(&res->locations, &locs_count);
+ i_assert(locs_count > 0);
+
+ return locs[0]->path;
+}
+
+struct event *http_server_resource_get_event(struct http_server_resource *res)
+{
+ return res->event;
+}
+
+void http_server_resource_add_location(struct http_server_resource *res,
+ const char *path)
+{
+ struct http_server_location *loc;
+
+ i_assert(*path == '\0' || *path == '/');
+
+ loc = http_server_location_add(res->server, res->pool, path);
+ i_assert(loc->resource == NULL);
+
+ loc->resource = res;
+ array_append(&res->locations, &loc, 1);
+
+ if (array_count(&res->locations) == 1)
+ http_server_resource_update_event(res);
+}
+
+int http_server_resource_find(struct http_server *server, const char *path,
+ struct http_server_resource **res_r,
+ const char **sub_path_r)
+{
+ struct http_server_location *loc;
+ int ret;
+
+ if (path == NULL)
+ return -1;
+
+ *res_r = NULL;
+ *sub_path_r = NULL;
+
+ ret = http_server_location_find(server, path, &loc, sub_path_r);
+ if (ret < 0)
+ return -1;
+
+ i_assert(loc->resource != NULL);
+ *res_r = loc->resource;
+ return ret;
+}
+
+bool http_server_resource_callback(struct http_server_request *req)
+{
+ struct http_server *server = req->server;
+ struct http_server_resource *res;
+ const char *sub_path;
+
+ switch (req->req.target.format) {
+ case HTTP_REQUEST_TARGET_FORMAT_ORIGIN:
+ /* According to RFC 7240, Section 5.3.1 only the origin form is
+ applicable to local resources on an origin server.
+ */
+ break;
+ case HTTP_REQUEST_TARGET_FORMAT_ABSOLUTE:
+ case HTTP_REQUEST_TARGET_FORMAT_AUTHORITY:
+ case HTTP_REQUEST_TARGET_FORMAT_ASTERISK:
+ /* Not applicable for a local resource. */
+ return FALSE;
+ }
+
+ if (http_server_resource_find(server, req->req.target.url->path,
+ &res, &sub_path) < 0)
+ return FALSE;
+
+ e_debug(res->event, "Got request: %s", http_server_request_label(req));
+
+ i_assert(res->callback != NULL);
+ res->callback(res->context, req, sub_path);
+ return TRUE;
+}
+
+#undef http_server_resource_set_destroy_callback
+void http_server_resource_set_destroy_callback(struct http_server_resource *res,
+ void (*callback)(void *),
+ void *context)
+{
+ res->destroy_callback = callback;
+ res->destroy_context = context;
+}
diff --git a/src/lib-http/http-server-response.c b/src/lib-http/http-server-response.c
new file mode 100644
index 0000000..8d55593
--- /dev/null
+++ b/src/lib-http/http-server-response.c
@@ -0,0 +1,801 @@
+/* Copyright (c) 2013-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "str.h"
+#include "array.h"
+#include "istream.h"
+#include "ostream-private.h"
+#include "http-date.h"
+#include "http-transfer.h"
+#include "http-server-private.h"
+
+struct http_server_response_payload {
+ struct http_server_response *resp;
+ struct const_iovec *iov;
+ unsigned int iov_count, iov_idx;
+ size_t iov_pos;
+};
+
+/*
+ * Response
+ */
+
+static void http_server_response_update_event(struct http_server_response *resp)
+{
+ event_add_int(resp->event, "status", resp->status);
+ event_set_append_log_prefix(resp->event,
+ t_strdup_printf("%u response: ",
+ resp->status));
+}
+
+struct http_server_response *
+http_server_response_create(struct http_server_request *req,
+ unsigned int status, const char *reason)
+{
+ struct http_server_response *resp;
+
+ i_assert(req->state < HTTP_SERVER_REQUEST_STATE_SENT_RESPONSE);
+
+ if (req->response == NULL) {
+ resp = req->response = p_new(req->pool,
+ struct http_server_response, 1);
+ } else {
+ /* Was already composing a response, but decided to
+ start a new one (would usually be a failure response)
+ */
+ resp = req->response;
+
+ ARRAY_TYPE(string) perm_headers = resp->perm_headers;
+ i_zero(&resp->perm_headers);
+
+ http_server_response_request_free(resp);
+ i_zero(resp);
+
+ resp->perm_headers = perm_headers;
+ }
+
+ resp->request = req;
+ resp->status = status;
+ resp->reason = p_strdup(req->pool, reason);
+ resp->headers = str_new(default_pool, 256);
+ resp->date = (time_t)-1;
+ resp->event = event_create(req->event);
+ http_server_response_update_event(resp);
+
+ if (array_is_created(&resp->perm_headers)) {
+ unsigned int i, count;
+ char *const *headers = array_get(&resp->perm_headers, &count);
+ for (i = 0; i < count; i += 2)
+ http_server_response_add_header(resp, headers[i],
+ headers[i+1]);
+ }
+ return resp;
+}
+
+void http_server_response_request_free(struct http_server_response *resp)
+{
+ e_debug(resp->event, "Free");
+
+ /* Cannot be destroyed while payload output stream still exists */
+ i_assert(resp->payload_stream == NULL);
+
+ i_stream_unref(&resp->payload_input);
+ o_stream_unref(&resp->payload_output);
+ event_unref(&resp->event);
+ str_free(&resp->headers);
+
+ if (array_is_created(&resp->perm_headers)) {
+ char *headers;
+
+ array_foreach_elem(&resp->perm_headers, headers)
+ i_free(headers);
+ array_free(&resp->perm_headers);
+ }
+}
+
+void http_server_response_request_destroy(struct http_server_response *resp)
+{
+ e_debug(resp->event, "Destroy");
+
+ if (resp->payload_stream != NULL)
+ http_server_ostream_response_destroyed(resp->payload_stream);
+}
+
+void http_server_response_request_abort(struct http_server_response *resp,
+ const char *reason)
+{
+ if (reason == NULL)
+ e_debug(resp->event, "Abort");
+ else
+ e_debug(resp->event, "Abort: %s", reason);
+
+ if (resp->payload_stream != NULL) {
+ http_server_ostream_set_error(resp->payload_stream,
+ EPIPE, reason);
+ }
+}
+
+void http_server_response_ref(struct http_server_response *resp)
+{
+ http_server_request_ref(resp->request);
+}
+
+bool http_server_response_unref(struct http_server_response **_resp)
+{
+ struct http_server_response *resp = *_resp;
+ struct http_server_request *req;
+
+ *_resp = NULL;
+ if (resp == NULL)
+ return FALSE;
+
+ req = resp->request;
+ return http_server_request_unref(&req);
+}
+
+void http_server_response_add_header(struct http_server_response *resp,
+ const char *key, const char *value)
+{
+ i_assert(!resp->submitted);
+ i_assert(strchr(key, '\r') == NULL && strchr(key, '\n') == NULL);
+ i_assert(strchr(value, '\r') == NULL && strchr(value, '\n') == NULL);
+
+ /* Mark presence of special headers */
+ switch (key[0]) {
+ case 'c': case 'C':
+ if (strcasecmp(key, "Connection") == 0)
+ resp->have_hdr_connection = TRUE;
+ else if (strcasecmp(key, "Content-Length") == 0)
+ resp->have_hdr_body_spec = TRUE;
+ break;
+ case 'd': case 'D':
+ if (strcasecmp(key, "Date") == 0)
+ resp->have_hdr_date = TRUE;
+ break;
+ case 't': case 'T':
+ if (strcasecmp(key, "Transfer-Encoding") == 0)
+ resp->have_hdr_body_spec = TRUE;
+ break;
+ }
+ str_printfa(resp->headers, "%s: %s\r\n", key, value);
+}
+
+void http_server_response_update_status(struct http_server_response *resp,
+ unsigned int status,
+ const char *reason)
+{
+ i_assert(!resp->submitted);
+ resp->status = status;
+ /* Free not called because pool is alloconly */
+ resp->reason = p_strdup(resp->request->pool, reason);
+}
+
+void http_server_response_set_date(struct http_server_response *resp,
+ time_t date)
+{
+ i_assert(!resp->submitted);
+
+ resp->date = date;
+}
+
+void http_server_response_set_payload(struct http_server_response *resp,
+ struct istream *input)
+{
+ int ret;
+
+ i_assert(!resp->submitted);
+ i_assert(resp->payload_input == NULL);
+ i_assert(resp->payload_stream == NULL);
+
+ i_stream_ref(input);
+ resp->payload_input = input;
+ if ((ret = i_stream_get_size(input, TRUE, &resp->payload_size)) <= 0) {
+ if (ret < 0) {
+ e_error(resp->event, "i_stream_get_size(%s) failed: %s",
+ i_stream_get_name(input),
+ i_stream_get_error(input));
+ }
+ resp->payload_size = 0;
+ resp->payload_chunked = TRUE;
+ } else {
+ i_assert(input->v_offset <= resp->payload_size);
+ resp->payload_size -= input->v_offset;
+ }
+ resp->payload_offset = input->v_offset;
+}
+
+void http_server_response_set_payload_data(struct http_server_response *resp,
+ const unsigned char *data,
+ size_t size)
+{
+ struct istream *input;
+ unsigned char *payload_data;
+
+ i_assert(!resp->submitted);
+ i_assert(resp->payload_input == NULL);
+ i_assert(resp->payload_stream == NULL);
+
+ if (size == 0)
+ return;
+
+ payload_data = p_malloc(resp->request->pool, size);
+ memcpy(payload_data, data, size);
+ input = i_stream_create_from_data(payload_data, size);
+
+ http_server_response_set_payload(resp, input);
+ i_stream_unref(&input);
+}
+
+struct ostream *
+http_server_response_get_payload_output(struct http_server_response *resp,
+ size_t max_buffer_size, bool blocking)
+{
+ struct http_server_request *req = resp->request;
+ struct http_server_connection *conn = req->conn;
+ struct ostream *output;
+
+ i_assert(conn != NULL);
+ i_assert(!resp->submitted);
+ i_assert(resp->payload_input == NULL);
+ i_assert(resp->payload_stream == NULL);
+
+ output = http_server_ostream_create(resp, max_buffer_size, blocking);
+ o_stream_set_name(output,
+ t_strdup_printf("(conn %s: request %s: %u response payload)",
+ conn->conn.label,
+ http_server_request_label(req), resp->status));
+ return output;
+}
+
+void http_server_response_add_auth(struct http_server_response *resp,
+ const struct http_auth_challenge *chlng)
+{
+ struct http_auth_challenge *new;
+ pool_t pool = resp->request->pool;
+
+ if (!array_is_created(&resp->auth_challenges))
+ p_array_init(&resp->auth_challenges, pool, 4);
+
+ new = array_append_space(&resp->auth_challenges);
+ http_auth_challenge_copy(pool, new, chlng);
+}
+
+void http_server_response_add_auth_basic(struct http_server_response *resp,
+ const char *realm)
+{
+ struct http_auth_challenge chlng;
+
+ http_auth_basic_challenge_init(&chlng, realm);
+ http_server_response_add_auth(resp, &chlng);
+}
+
+static void
+http_server_response_do_submit(struct http_server_response *resp)
+{
+ i_assert(!resp->submitted);
+ if (resp->date == (time_t)-1)
+ resp->date = ioloop_time;
+ resp->submitted = TRUE;
+ http_server_request_submit_response(resp->request);
+}
+
+void http_server_response_submit(struct http_server_response *resp)
+{
+ e_debug(resp->event, "Submitted");
+
+ http_server_response_do_submit(resp);
+}
+
+void http_server_response_submit_close(struct http_server_response *resp)
+{
+ http_server_request_connection_close(resp->request, TRUE);
+ http_server_response_submit(resp);
+}
+
+void http_server_response_submit_tunnel(struct http_server_response *resp,
+ http_server_tunnel_callback_t callback,
+ void *context)
+{
+ e_debug(resp->event, "Started tunnelling");
+
+ resp->tunnel_callback = callback;
+ resp->tunnel_context = context;
+ http_server_request_connection_close(resp->request, TRUE);
+ http_server_response_do_submit(resp);
+}
+
+static int
+http_server_response_flush_payload(struct http_server_response *resp)
+{
+ struct http_server_request *req = resp->request;
+ struct http_server_connection *conn = req->conn;
+ int ret;
+
+ if (resp->payload_output != conn->conn.output &&
+ (ret = o_stream_finish(resp->payload_output)) <= 0) {
+ if (ret < 0)
+ http_server_connection_handle_output_error(conn);
+ else
+ http_server_connection_start_idle_timeout(conn);
+ return ret;
+ }
+
+ return 1;
+}
+
+void http_server_response_request_finished(struct http_server_response *resp)
+{
+ e_debug(resp->event, "Finished");
+
+ if (resp->payload_stream != NULL)
+ http_server_ostream_response_finished(resp->payload_stream);
+
+ event_add_int(resp->request->event, "status_code", resp->status);
+}
+
+int http_server_response_finish_payload_out(struct http_server_response *resp)
+{
+ struct http_server_request *req = resp->request;
+ struct http_server_connection *conn = req->conn;
+ int ret;
+
+ if (req->state >= HTTP_SERVER_REQUEST_STATE_FINISHED)
+ return 1;
+
+ resp->payload_finished = TRUE;
+
+ if (resp->payload_output != NULL) {
+ ret = http_server_response_flush_payload(resp);
+ if (ret < 0)
+ return -1;
+ if (ret == 0) {
+ e_debug(resp->event,
+ "Not quite finished sending payload");
+ return 0;
+ }
+ o_stream_unref(&resp->payload_output);
+ resp->payload_output = NULL;
+ }
+
+ e_debug(resp->event, "Finished sending payload");
+
+ http_server_connection_ref(conn);
+ conn->output_locked = FALSE;
+ if (conn->conn.output != NULL && !conn->conn.output->closed) {
+ if (resp->payload_corked &&
+ o_stream_uncork_flush(conn->conn.output) < 0)
+ http_server_connection_handle_output_error(conn);
+ o_stream_set_flush_callback(conn->conn.output,
+ http_server_connection_output,
+ conn);
+ }
+
+ if (conn->request_queue_head == NULL ||
+ (conn->request_queue_head->state !=
+ HTTP_SERVER_REQUEST_STATE_PROCESSING))
+ http_server_connection_start_idle_timeout(conn);
+
+ http_server_request_finished(resp->request);
+ http_server_connection_unref(&conn);
+ return 1;
+}
+
+static int
+http_server_response_output_payload(struct http_server_response **_resp,
+ const unsigned char *data, size_t size)
+{
+ struct http_server_response *resp = *_resp;
+ struct http_server_request *req = resp->request;
+ struct ostream *output;
+ ssize_t sret;
+ int ret;
+
+ i_assert(req->state < HTTP_SERVER_REQUEST_STATE_SUBMITTED_RESPONSE ||
+ req->state == HTTP_SERVER_REQUEST_STATE_PAYLOAD_OUT);
+
+ http_server_response_ref(resp);
+
+ if (resp->payload_stream == NULL) {
+ output = http_server_response_get_payload_output(
+ resp, IO_BLOCK_SIZE, TRUE);
+ } else {
+ output = http_server_ostream_get_output(resp->payload_stream);
+ }
+
+ if (data != NULL) {
+ if ((sret = o_stream_send(output, data, size)) < 0) {
+ *_resp = NULL;
+ o_stream_destroy(&output);
+ http_server_response_unref(&resp);
+ return -1;
+ }
+ i_assert((size_t)sret == size);
+ } else {
+ if ((ret = o_stream_finish(output)) < 0) {
+ *_resp = NULL;
+ o_stream_destroy(&output);
+ http_server_response_unref(&resp);
+ return -1;
+ }
+ i_assert(ret > 0);
+ }
+
+ switch (req->state) {
+ case HTTP_SERVER_REQUEST_STATE_FINISHED:
+ ret = 1;
+ break;
+ case HTTP_SERVER_REQUEST_STATE_ABORTED:
+ e_debug(resp->event,
+ "Request aborted while sending blocking payload");
+ ret = -1;
+ break;
+ default:
+ ret = 0;
+ break;
+ }
+
+ if (data == NULL)
+ o_stream_destroy(&output);
+
+ /* Callback may have messed with our pointer, so unref using local
+ variable */
+ if (!http_server_response_unref(&resp))
+ *_resp = NULL;
+
+ /* Return status */
+ return ret;
+}
+
+int http_server_response_send_payload(struct http_server_response **_resp,
+ const unsigned char *data, size_t size)
+{
+ struct http_server_response *resp = *_resp;
+ int ret;
+
+ resp->payload_corked = TRUE;
+
+ i_assert(data != NULL);
+
+ ret = http_server_response_output_payload(&resp, data, size);
+ if (ret < 0)
+ *_resp = NULL;
+ else {
+ i_assert(ret == 0);
+ i_assert(resp != NULL);
+ }
+ return ret;
+}
+
+int http_server_response_finish_payload(struct http_server_response **_resp)
+{
+ struct http_server_response *resp = *_resp;
+ int ret;
+
+ *_resp = NULL;
+ ret = http_server_response_output_payload(&resp, NULL, 0);
+ i_assert(ret != 0);
+ return ret < 0 ? -1 : 0;
+}
+
+void http_server_response_abort_payload(struct http_server_response **_resp)
+{
+ struct http_server_response *resp = *_resp;
+ struct http_server_request *req = resp->request;
+
+ *_resp = NULL;
+
+ http_server_request_abort(&req, "Aborted sending response payload");
+}
+
+static void
+http_server_response_payload_input(struct http_server_response *resp)
+{
+ struct http_server_connection *conn = resp->request->conn;
+
+ io_remove(&conn->io_resp_payload);
+
+ (void)http_server_connection_output(conn);
+}
+
+int http_server_response_send_more(struct http_server_response *resp)
+{
+ struct http_server_connection *conn = resp->request->conn;
+ struct ostream *output = resp->payload_output;
+ enum ostream_send_istream_result res;
+
+ i_assert(resp->payload_output != NULL);
+
+ if (resp->payload_finished) {
+ e_debug(resp->event, "Finish sending payload (more)");
+ return http_server_response_finish_payload_out(resp);
+ }
+
+ if (resp->payload_stream != NULL) {
+ conn->output_locked = TRUE;
+ return http_server_ostream_continue(resp->payload_stream);
+ }
+
+ i_assert(resp->payload_input != NULL);
+ io_remove(&conn->io_resp_payload);
+
+ /* Chunked ostream needs to write to the parent stream's buffer */
+ o_stream_set_max_buffer_size(output, IO_BLOCK_SIZE);
+ res = o_stream_send_istream(output, resp->payload_input);
+ o_stream_set_max_buffer_size(output, SIZE_MAX);
+
+ switch (res) {
+ case OSTREAM_SEND_ISTREAM_RESULT_FINISHED:
+ /* Finished sending */
+ if (!resp->payload_chunked &&
+ (resp->payload_input->v_offset - resp->payload_offset) !=
+ resp->payload_size) {
+ e_error(resp->event,
+ "Payload stream %s size changed unexpectedly",
+ i_stream_get_name(resp->payload_input));
+ http_server_connection_close(
+ &conn, "Payload read failure");
+ return -1;
+ }
+ /* Finished sending payload */
+ e_debug(resp->event, "Finish sending payload");
+ return http_server_response_finish_payload_out(resp);
+ case OSTREAM_SEND_ISTREAM_RESULT_WAIT_INPUT:
+ /* Input is blocking (server needs to act; disable timeout) */
+ conn->output_locked = TRUE;
+ http_server_connection_stop_idle_timeout(conn);
+ conn->io_resp_payload = io_add_istream(resp->payload_input,
+ http_server_response_payload_input, resp);
+ return 1;
+ case OSTREAM_SEND_ISTREAM_RESULT_WAIT_OUTPUT:
+ /* Output is blocking (client needs to act; enable timeout) */
+ conn->output_locked = TRUE;
+ http_server_connection_start_idle_timeout(conn);
+ o_stream_set_flush_pending(output, TRUE);
+ //e_debug(resp->event, "Partially sent payload");
+ return 0;
+ case OSTREAM_SEND_ISTREAM_RESULT_ERROR_INPUT:
+ /* We're in the middle of sending a response, so the connection
+ will also have to be aborted */
+ e_error(resp->event, "read(%s) failed: %s",
+ i_stream_get_name(resp->payload_input),
+ i_stream_get_error(resp->payload_input));
+ http_server_connection_close(&conn,
+ "Payload read failure");
+ return -1;
+ case OSTREAM_SEND_ISTREAM_RESULT_ERROR_OUTPUT:
+ /* Failed to send response */
+ http_server_connection_handle_output_error(conn);
+ return -1;
+ }
+ i_unreached();
+}
+
+static int http_server_response_send_real(struct http_server_response *resp)
+{
+ struct http_server_request *req = resp->request;
+ struct http_server_connection *conn = req->conn;
+ string_t *rtext = t_str_new(256);
+ struct const_iovec iov[3];
+ uoff_t content_length = 0;
+ bool chunked = FALSE, send_content_length = FALSE, close = FALSE;
+ bool is_head = http_request_method_is(&req->req, "HEAD");
+ int ret;
+
+ i_assert(!conn->output_locked);
+
+ /* Determine response payload to send */
+ if (resp->payload_input != NULL) {
+ i_assert(resp->tunnel_callback == NULL &&
+ resp->status / 100 != 1 &&
+ resp->status != 204 && resp->status != 304);
+ if (resp->payload_chunked) {
+ if (http_server_request_version_equals(req, 1, 0)) {
+ /* Connection close marks end of payload
+ */
+ close = TRUE;
+ } else {
+ /* Input stream with unknown size */
+ chunked = TRUE;
+ }
+ } else {
+ /* Send Content-Length if we have specified a payload,
+ even if it's 0 bytes. */
+ content_length = resp->payload_size;
+ send_content_length = TRUE;
+ }
+ } else if (resp->payload_stream != NULL) {
+ /* HTTP payload output stream */
+ if (!http_server_ostream_get_size(resp->payload_stream,
+ &content_length)) {
+ /* size not known at this point */
+ chunked = TRUE;
+ } else {
+ /* output stream already finished, so data is
+ pre-buffered */
+ send_content_length = TRUE;
+ }
+ } else if (resp->tunnel_callback == NULL && resp->status / 100 != 1 &&
+ resp->status != 204 && resp->status != 304 && !is_head) {
+ /* RFC 7230, Section 3.3: Message Body
+
+ Responses to the HEAD request method (Section 4.3.2 of
+ [RFC7231]) never include a message body because the
+ associated response header fields (e.g., Transfer-Encoding,
+ Content-Length, etc.), if present, indicate only what their
+ values would have been if the request method had been GET
+ (Section 4.3.1 of [RFC7231]). 2xx (Successful) responses to a
+ CONNECT request method (Section 4.3.6 of [RFC7231]) switch to
+ tunnel mode instead of having a message body. All 1xx
+ (Informational), 204 (No Content), and 304 (Not Modified)
+ responses do not include a message body. All other responses
+ do include a message body, although the body might be of zero
+ length.
+
+ RFC 7230, Section 3.3.2: Content-Length
+
+ A server MUST NOT send a Content-Length header field in any
+ 2xx (Successful) response to a CONNECT request (Section 4.3.6
+ of [RFC7231]).
+
+ -> Create empty body if it is missing.
+ */
+ send_content_length = TRUE;
+ }
+
+ /* Initialize output payload stream if needed */
+ if (is_head) {
+ e_debug(resp->event, "A HEAD response has no payload");
+ } else if (chunked) {
+ i_assert(resp->payload_input != NULL ||
+ resp->payload_stream != NULL);
+
+ e_debug(resp->event, "Will send payload in chunks");
+
+ resp->payload_output =
+ http_transfer_chunked_ostream_create(conn->conn.output);
+ } else if (send_content_length) {
+ i_assert(resp->payload_input != NULL || content_length == 0 ||
+ resp->payload_stream != NULL);
+
+ e_debug(resp->event,
+ "Will send payload with explicit size %"PRIuUOFF_T,
+ content_length);
+
+ if (content_length > 0) {
+ resp->payload_output = conn->conn.output;
+ o_stream_ref(conn->conn.output);
+ }
+ } else if (close) {
+ i_assert(resp->payload_input != NULL);
+
+ e_debug(resp->event,
+ "Will close connection after sending payload "
+ "(HTTP/1.0)");
+
+ resp->payload_output = conn->conn.output;
+ o_stream_ref(conn->conn.output);
+ } else {
+ e_debug(resp->event, "Response has no payload");
+ }
+
+ /* Create status line */
+ str_append(rtext, "HTTP/1.1 ");
+ str_printfa(rtext, "%u", resp->status);
+ str_append(rtext, " ");
+ str_append(rtext, resp->reason);
+
+ /* Create special headers implicitly if not set explicitly using
+ http_server_response_add_header() */
+ if (!resp->have_hdr_date) {
+ str_append(rtext, "\r\nDate: ");
+ str_append(rtext, http_date_create(resp->date));
+ str_append(rtext, "\r\n");
+ }
+ if (array_is_created(&resp->auth_challenges)) {
+ str_append(rtext, "WWW-Authenticate: ");
+ http_auth_create_challenges(rtext, &resp->auth_challenges);
+ str_append(rtext, "\r\n");
+ }
+ if (chunked) {
+ if (!resp->have_hdr_body_spec)
+ str_append(rtext, "Transfer-Encoding: chunked\r\n");
+ } else if (send_content_length) {
+ if (!resp->have_hdr_body_spec) {
+ str_printfa(rtext, "Content-Length: %"PRIuUOFF_T"\r\n",
+ content_length);
+ }
+ }
+ if (!resp->have_hdr_connection) {
+ close = (close || req->req.connection_close ||
+ req->connection_close || req->conn->input_broken);
+ if (close && resp->tunnel_callback == NULL)
+ str_append(rtext, "Connection: close\r\n");
+ else if (http_server_request_version_equals(req, 1, 0))
+ str_append(rtext, "Connection: Keep-Alive\r\n");
+ }
+
+ /* Status line + implicit headers */
+ iov[0].iov_base = str_data(rtext);
+ iov[0].iov_len = str_len(rtext);
+ /* Explicit headers */
+ iov[1].iov_base = str_data(resp->headers);
+ iov[1].iov_len = str_len(resp->headers);
+ /* End of header */
+ iov[2].iov_base = "\r\n";
+ iov[2].iov_len = 2;
+
+ req->state = HTTP_SERVER_REQUEST_STATE_PAYLOAD_OUT;
+ o_stream_cork(conn->conn.output);
+
+ if (o_stream_sendv(conn->conn.output, iov, N_ELEMENTS(iov)) < 0) {
+ http_server_connection_handle_output_error(conn);
+ return -1;
+ }
+
+ e_debug(resp->event, "Sent header");
+
+ if (resp->payload_stream != NULL)
+ http_server_ostream_output_available(resp->payload_stream);
+ if (resp->payload_output != NULL) {
+ /* Non-blocking payload */
+ ret = http_server_response_send_more(resp);
+ if (ret < 0)
+ return -1;
+ } else {
+ /* No payload to send */
+ e_debug(resp->event, "No payload to send");
+ if (resp->payload_stream != NULL) {
+ ret = http_server_ostream_continue(resp->payload_stream);
+ if (ret < 0)
+ return -1;
+ }
+ conn->output_locked = FALSE;
+ ret = http_server_response_finish_payload_out(resp);
+ if (ret < 0)
+ return -1;
+ }
+
+ if (conn->conn.output != NULL && !resp->payload_corked &&
+ o_stream_uncork_flush(conn->conn.output) < 0) {
+ http_server_connection_handle_output_error(conn);
+ return -1;
+ }
+ return ret;
+}
+
+int http_server_response_send(struct http_server_response *resp)
+{
+ int ret;
+
+ T_BEGIN {
+ ret = http_server_response_send_real(resp);
+ } T_END;
+ return ret;
+}
+
+void http_server_response_get_status(struct http_server_response *resp,
+ int *status_r, const char **reason_r)
+{
+ i_assert(resp != NULL);
+ *status_r = resp->status;
+ *reason_r = resp->reason;
+}
+
+uoff_t http_server_response_get_total_size(struct http_server_response *resp)
+{
+ i_assert(resp != NULL);
+ return resp->payload_size + str_len(resp->headers);
+}
+
+void http_server_response_add_permanent_header(struct http_server_response *resp,
+ const char *key, const char *value)
+{
+ http_server_response_add_header(resp, key, value);
+
+ if (!array_is_created(&resp->perm_headers))
+ i_array_init(&resp->perm_headers, 4);
+ char *key_dup = i_strdup(key);
+ char *value_dup = i_strdup(value);
+ array_push_back(&resp->perm_headers, &key_dup);
+ array_push_back(&resp->perm_headers, &value_dup);
+}
diff --git a/src/lib-http/http-server.c b/src/lib-http/http-server.c
new file mode 100644
index 0000000..dd89a16
--- /dev/null
+++ b/src/lib-http/http-server.c
@@ -0,0 +1,132 @@
+/* Copyright (c) 2013-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "net.h"
+#include "str.h"
+#include "hash.h"
+#include "array.h"
+#include "ioloop.h"
+#include "istream.h"
+#include "ostream.h"
+#include "connection.h"
+#include "dns-lookup.h"
+#include "iostream-rawlog.h"
+#include "iostream-ssl.h"
+#include "http-url.h"
+
+#include "http-server-private.h"
+
+static struct event_category event_category_http_server = {
+ .name = "http-server"
+};
+
+/*
+ * Server
+ */
+
+struct http_server *http_server_init(const struct http_server_settings *set)
+{
+ struct http_server *server;
+ pool_t pool;
+ size_t pool_size;
+
+ pool_size = (set->ssl != NULL) ? 10240 : 1024; /* ca/cert/key will be >8K */
+ pool = pool_alloconly_create("http server", pool_size);
+ server = p_new(pool, struct http_server, 1);
+ server->pool = pool;
+
+ if (set->default_host != NULL && *set->default_host != '\0')
+ server->set.default_host = p_strdup(pool, set->default_host);
+ if (set->rawlog_dir != NULL && *set->rawlog_dir != '\0')
+ server->set.rawlog_dir = p_strdup(pool, set->rawlog_dir);
+ if (set->ssl != NULL) {
+ server->set.ssl =
+ ssl_iostream_settings_dup(server->pool, set->ssl);
+ }
+ server->set.max_client_idle_time_msecs = set->max_client_idle_time_msecs;
+ server->set.max_pipelined_requests =
+ (set->max_pipelined_requests > 0 ? set->max_pipelined_requests : 1);
+ server->set.request_limits = set->request_limits;
+ server->set.socket_send_buffer_size = set->socket_send_buffer_size;
+ server->set.socket_recv_buffer_size = set->socket_recv_buffer_size;
+ server->set.debug = set->debug;
+
+ server->event = event_create(set->event);
+ event_add_category(server->event, &event_category_http_server);
+ event_set_forced_debug(server->event, set->debug);
+ event_set_append_log_prefix(server->event, "http-server: ");
+
+ server->conn_list = http_server_connection_list_init();
+
+ p_array_init(&server->resources, pool, 4);
+ p_array_init(&server->locations, pool, 4);
+
+ return server;
+}
+
+void http_server_deinit(struct http_server **_server)
+{
+ struct http_server *server = *_server;
+ struct http_server_resource *res;
+
+ *_server = NULL;
+
+ connection_list_deinit(&server->conn_list);
+
+ array_foreach_elem(&server->resources, res)
+ http_server_resource_free(&res);
+ i_assert(array_count(&server->locations) == 0);
+
+ if (server->ssl_ctx != NULL)
+ ssl_iostream_context_unref(&server->ssl_ctx);
+ event_unref(&server->event);
+ pool_unref(&server->pool);
+}
+
+void http_server_switch_ioloop(struct http_server *server)
+{
+ struct connection *_conn = server->conn_list->connections;
+
+ /* move connections */
+ /* FIXME: we wouldn't necessarily need to switch all of them
+ immediately, only those that have requests now. but also connections
+ that get new requests before ioloop is switched again.. */
+ for (; _conn != NULL; _conn = _conn->next) {
+ struct http_server_connection *conn =
+ (struct http_server_connection *)_conn;
+
+ http_server_connection_switch_ioloop(conn);
+ }
+}
+
+void http_server_shut_down(struct http_server *server)
+{
+ struct connection *_conn, *_next;
+
+ server->shutting_down = TRUE;
+
+ for (_conn = server->conn_list->connections;
+ _conn != NULL; _conn = _next) {
+ struct http_server_connection *conn =
+ (struct http_server_connection *)_conn;
+
+ _next = _conn->next;
+ (void)http_server_connection_shut_down(conn);
+ }
+}
+
+int http_server_init_ssl_ctx(struct http_server *server, const char **error_r)
+{
+ const char *error;
+
+ if (server->set.ssl == NULL || server->ssl_ctx != NULL)
+ return 0;
+
+ if (ssl_iostream_server_context_cache_get(server->set.ssl,
+ &server->ssl_ctx, &error) < 0) {
+ *error_r = t_strdup_printf("Couldn't initialize SSL context: %s",
+ error);
+ return -1;
+ }
+ return 0;
+}
diff --git a/src/lib-http/http-server.h b/src/lib-http/http-server.h
new file mode 100644
index 0000000..48708a3
--- /dev/null
+++ b/src/lib-http/http-server.h
@@ -0,0 +1,427 @@
+#ifndef HTTP_SERVER_H
+#define HTTP_SERVER_H
+
+#include "http-common.h"
+#include "http-auth.h"
+#include "http-request.h"
+
+struct istream;
+struct ostream;
+
+struct http_request;
+
+struct http_server;
+struct http_server_resource;
+struct http_server_request;
+struct http_server_response;
+
+/*
+ * Server settings
+ */
+
+struct http_server_settings {
+ const char *default_host;
+
+ const char *rawlog_dir;
+
+ /* SSL settings; if NULL, master_service_ssl_init() is used instead */
+ const struct ssl_iostream_settings *ssl;
+
+ /* The maximum time in milliseconds a client is allowed to be idle
+ before it is disconnected. */
+ unsigned int max_client_idle_time_msecs;
+
+ /* Maximum number of pipelined requests per connection (default = 1) */
+ unsigned int max_pipelined_requests;
+
+ /* Request limits */
+ struct http_request_limits request_limits;
+
+ /* The kernel send/receive buffer sizes used for the connection sockets.
+ Configuring this is mainly useful for the test suite. The kernel
+ defaults are used when these settings are 0. */
+ size_t socket_send_buffer_size;
+ size_t socket_recv_buffer_size;
+
+ /* Event to use for the http server. */
+ struct event *event;
+
+ /* Enable logging debug messages */
+ bool debug;
+};
+
+/*
+ * Response
+ */
+
+/* Connection data for an established HTTP tunnel */
+struct http_server_tunnel {
+ int fd_in, fd_out;
+ struct istream *input;
+ struct ostream *output;
+};
+
+typedef void
+(*http_server_tunnel_callback_t)(void *context,
+ const struct http_server_tunnel *tunnel);
+
+/* Start creating the response for the request. This function can be called
+ only once for each request. */
+struct http_server_response *
+http_server_response_create(struct http_server_request *req,
+ unsigned int status, const char *reason);
+
+/* Reference a server response */
+void http_server_response_ref(struct http_server_response *resp);
+/* Unreference a server response. Returns TRUE if there are still more
+ references, FALSE if not. */
+bool http_server_response_unref(struct http_server_response **_resp);
+
+/* Add a custom header to the response. This can override headers that are
+ otherwise created implicitly. */
+void http_server_response_add_header(struct http_server_response *resp,
+ const char *key, const char *value);
+/* Add a header permanently to the response. Even if another response is
+ created for the request, this header is kept. */
+void http_server_response_add_permanent_header(struct http_server_response *resp,
+ const char *key, const char *value);
+/* Change the response code and text, cannot be used after submission */
+void http_server_response_update_status(struct http_server_response *resp,
+ unsigned int status, const char *reason);
+/* Set the value of the "Date" header for the response using a time_t value.
+ Use this instead of setting it directly using
+ http_server_response_add_header() */
+void http_server_response_set_date(struct http_server_response *resp,
+ time_t date);
+/* Assign an input stream for the outgoing payload of this response. The input
+ stream is read asynchronously while the response is sent to the client. */
+void http_server_response_set_payload(struct http_server_response *resp,
+ struct istream *input);
+/* Assign payload data to the response. The data is copied to the request pool.
+ If your data is already durably allocated during the existence of the
+ response, you should consider using http_server_response_set_payload() with
+ a data input stream instead. This will avoid copying the data unnecessarily.
+ */
+void http_server_response_set_payload_data(struct http_server_response *resp,
+ const unsigned char *data,
+ size_t size);
+
+/* Get an output stream for the outgoing payload of this response. The output
+ stream operates asynchronously when blocking is FALSE. In that case the
+ flush callback is called once more data can be sent. When blocking is TRUE,
+ writing to the stream will block until all data is sent. In every respect,
+ it operates very similar to a normal file output stream. The response is
+ submitted implicitly when the stream is first used; e.g., when it is written,
+ flushed, or o_stream_set_flush_pending(ostream, TRUE) is called. */
+struct ostream *
+http_server_response_get_payload_output(struct http_server_response *resp,
+ size_t max_buffer_size, bool blocking);
+
+/* Get the status code and reason string currently set for this response. */
+void http_server_response_get_status(struct http_server_response *resp,
+ int *status_r, const char **reason_r);
+/* Get the total size of the response when sent over the connection. */
+uoff_t http_server_response_get_total_size(struct http_server_response *resp);
+/* Add authentication challenge to the response. */
+void http_server_response_add_auth(struct http_server_response *resp,
+ const struct http_auth_challenge *chlng);
+/* Add "Basic" authentication challenge to the response. */
+void http_server_response_add_auth_basic(struct http_server_response *resp,
+ const char *realm);
+
+/* Submit the response. It is queued for transmission to the client. */
+void http_server_response_submit(struct http_server_response *resp);
+/* Submit the response and close the connection once it is sent. */
+void http_server_response_submit_close(struct http_server_response *resp);
+/* Submit the response and turn the connection it is sent across into a tunnel
+ once it is sent successfully. The callback is called once that happens. */
+void http_server_response_submit_tunnel(struct http_server_response *resp,
+ http_server_tunnel_callback_t callback,
+ void *context);
+
+/* Submits response and blocks until provided payload is sent. Multiple calls
+ are allowed; payload is sent in chunks this way. Payload transmission is
+ finished with http_server_response_finish_payload(). If the sending fails,
+ returns -1 and sets resp=NULL to indicate that the response was freed,
+ otherwise returns 0 and resp is unchanged.
+
+ An often more convenient ostream wrapper API is available as
+ http_server_response_get_payload_output() with blocking=TRUE.
+ */
+int http_server_response_send_payload(struct http_server_response **resp,
+ const unsigned char *data, size_t size);
+/* Finish sending the payload. Always frees resp and sets it to NULL.
+ Returns 0 on success, -1 on error. */
+int http_server_response_finish_payload(struct http_server_response **resp);
+/* Abort response payload transmission prematurely. This closes the associated
+ connection */
+void http_server_response_abort_payload(struct http_server_response **resp);
+
+/*
+ * Request
+ */
+
+/* Get the parsed HTTP request information for this request. */
+const struct http_request *
+http_server_request_get(struct http_server_request *req);
+
+/* Reference a server request */
+void http_server_request_ref(struct http_server_request *req);
+/* Unreference a server request. Returns TRUE if there are still more
+ references, FALSE if not. */
+bool http_server_request_unref(struct http_server_request **_req);
+
+/* Set flag that determines whether the connection is closed after the
+ request is handled. */
+void http_server_request_connection_close(struct http_server_request *req,
+ bool close);
+
+/* Get the pool for this request. */
+pool_t http_server_request_get_pool(struct http_server_request *req);
+/* Returns the response created for the request with
+ http_server_response_create(), or NULL if none. */
+struct http_server_response *
+http_server_request_get_response(struct http_server_request *req);
+/* Returns TRUE if request is finished either because a response was sent
+ or because the request was aborted. */
+bool http_server_request_is_finished(struct http_server_request *req);
+
+/* Add a header to any HTTP response created for the HTTP request. */
+void http_server_request_add_response_header(struct http_server_request *req,
+ const char *key, const char *value);
+
+/* Return input stream for the request's payload. Optionally, this stream
+ can be made blocking. Do *NOT* meddle with the FD of the http_request
+ payload to achieve the same, because protocol violations will result.
+ */
+struct istream *
+http_server_request_get_payload_input(struct http_server_request *req,
+ bool blocking);
+
+/* Forward the incoming request payload to the provided output stream in the
+ background. Calls the provided callback once the payload was forwarded
+ successfully. If forwarding fails, the client is presented with an
+ appropriate error. If the payload size exceeds max_size, the client will
+ get a 413 error. Before the callback finishes, the application must either
+ have added a reference to the request or have submitted a response. */
+void http_server_request_forward_payload(struct http_server_request *req,
+ struct ostream *output,
+ uoff_t max_size,
+ void (*callback)(void *),
+ void *context);
+#define http_server_request_forward_payload(req, output, max_size, \
+ callback, context) \
+ http_server_request_forward_payload(req, output, max_size, \
+ (void(*)(void*))callback, TRUE ? context : \
+ CALLBACK_TYPECHECK(callback, void (*)(typeof(context))))
+/* Forward the incoming request payload to the provided buffer in the
+ background. Behaves identical to http_server_request_forward_payload()
+ otherwise. */
+void http_server_request_buffer_payload(struct http_server_request *req,
+ buffer_t *buffer, uoff_t max_size,
+ void (*callback)(void *),
+ void *context);
+#define http_server_request_buffer_payload(req, buffer, max_size, \
+ callback, context) \
+ http_server_request_buffer_payload(req, buffer, max_size, \
+ (void(*)(void*))callback, TRUE ? context : \
+ CALLBACK_TYPECHECK(callback, void (*)(typeof(context))))
+/* Handle the incoming request payload by calling the callback each time
+ more data is available. Payload reading automatically finishes when the
+ request payload is fully read. Before the final callback finishes, the
+ application must either have added a reference to the request or have
+ submitted a response. */
+void http_server_request_handle_payload(struct http_server_request *req,
+ void (*callback)(void *context),
+ void *context);
+#define http_server_request_handle_payload(req, callback, context) \
+ http_server_request_handle_payload(req,\
+ (void(*)(void*))callback, TRUE ? context : \
+ CALLBACK_TYPECHECK(callback, void (*)(typeof(context))))
+
+/* Get the authentication credentials provided in this request. Returns 0 if
+ the Authorization header is absent, returns -1 when that header cannot be
+ parsed, and returns 1 otherwise */
+int http_server_request_get_auth(struct http_server_request *req,
+ struct http_auth_credentials *credentials);
+
+/* Send a failure response for the request with given status/reason. */
+void http_server_request_fail(struct http_server_request *req,
+ unsigned int status, const char *reason);
+/* Send a failure response for the request with given status/reason
+ and close the connection. */
+void http_server_request_fail_close(struct http_server_request *req,
+ unsigned int status, const char *reason);
+/* Send a failure response for the request with given status/reason/text.
+ The text is sent as the response payload, if appropriate. */
+void http_server_request_fail_text(struct http_server_request *req,
+ unsigned int status, const char *reason,
+ const char *format, ...) ATTR_FORMAT(4, 5);
+/* Send an authentication failure response for the request with given reason.
+ The provided challenge is set in the WWW-Authenticate header of the
+ response. */
+void http_server_request_fail_auth(struct http_server_request *req,
+ const char *reason,
+ const struct http_auth_challenge *chlng)
+ ATTR_NULL(2);
+/* Send a authentication failure response for the request with given reason.
+ The provided realm is used to construct an Basic challenge in the
+ WWW-Authenticate header of the response. */
+void http_server_request_fail_auth_basic(struct http_server_request *req,
+ const char *reason, const char *realm)
+ ATTR_NULL(2);
+
+/* Send a 405 failure response for a request with an unknown method. The allow
+ parameter is the value used for the mandatory "Allow" header in the response.
+ */
+void http_server_request_fail_bad_method(struct http_server_request *req,
+ const char *allow);
+
+/* Call the specified callback when HTTP request is destroyed. This happens
+ after one of the following:
+
+ a) Response and its payload is fully sent,
+ b) Response was submitted, but it couldn't be sent due to disconnection or
+ some other error,
+ c) http_server_deinit() was called and the request was aborted
+
+ Note client disconnection before response is submitted isn't visible to this.
+ The request payload reading is the responsibility of the caller, which also
+ must handle the read errors by submitting a failure response. */
+void http_server_request_set_destroy_callback(struct http_server_request *req,
+ void (*callback)(void *),
+ void *context);
+#define http_server_request_set_destroy_callback(req, callback, context) \
+ http_server_request_set_destroy_callback( \
+ req, (void(*)(void*))callback, \
+ (TRUE ? context : \
+ CALLBACK_TYPECHECK(callback, void (*)(typeof(context)))))
+
+/*
+ * Connection
+ */
+
+/* Connection statistics */
+struct http_server_stats {
+ /* The number of requests received and responses sent */
+ unsigned int request_count, response_count;
+ /* Bytes sent and received accross the connection */
+ uoff_t input, output;
+};
+
+/* Connection callbacks */
+struct http_server_callbacks {
+ /* Handle the server request. All requests must be sent back a response.
+ The response is sent either with http_server_request_fail*() or
+ http_server_response_submit*(). For simple requests you can send the
+ response back immediately. If you can't do that, you'll need to
+ reference the request (or the request payload input stream). Then the
+ code flow usually goes like this:
+
+ - http_server_request_set_destroy_callback(destroy_callback)
+ - http_server_request_ref()
+ - <do whatever is needed to handle the request>
+ - http_server_response_create()
+ - http_server_response_set_payload() can be used especially with
+ istream-callback to create a large response without temp files.
+ - http_server_response_submit() triggers the destroy_callback
+ after it has finished sending the response and its payload.
+ - In destroy_callback: http_server_request_unref() and any other
+ necessary cleanup - the request handling is now fully finished.
+ */
+ void (*handle_request)(void *context, struct http_server_request *req);
+ void (*handle_connect_request)(void *context,
+ struct http_server_request *req,
+ struct http_url *target);
+
+ /* Called once the connection is destroyed. */
+ void (*connection_destroy)(void *context, const char *reason);
+};
+
+/* Create a HTTP server connection object for the provided fd pair. The
+ callbacks structure is described above. */
+struct http_server_connection *
+http_server_connection_create(struct http_server *server,
+ int fd_in, int fd_out, bool ssl,
+ const struct http_server_callbacks *callbacks,
+ void *context);
+/* Reference the connection */
+void http_server_connection_ref(struct http_server_connection *conn);
+/* Dereference the connection. Returns FALSE if unrefing destroyed the
+ connection entirely */
+bool http_server_connection_unref(struct http_server_connection **_conn);
+/* Dereference and close the connection. The provided reason is passed to the
+ connection_destroy() callback. */
+void http_server_connection_close(struct http_server_connection **_conn,
+ const char *reason);
+/* Get the current statistics for this connection */
+const struct http_server_stats *
+http_server_connection_get_stats(struct http_server_connection *conn);
+
+/* Switch connection to a specific ioloop. */
+struct ioloop *
+http_server_connection_switch_ioloop_to(struct http_server_connection *conn,
+ struct ioloop *ioloop);
+/* Switch connection to the current ioloop. */
+struct ioloop *
+http_server_connection_switch_ioloop(struct http_server_connection *conn);
+
+/*
+ * Resource
+ */
+
+typedef void
+(http_server_resource_callback_t)(void *context,
+ struct http_server_request *req,
+ const char *sub_path);
+
+struct http_server_resource *
+http_server_resource_create(struct http_server *server, pool_t pool,
+ http_server_resource_callback_t *callback,
+ void *context);
+#define http_server_resource_create(server, pool, callback, context) \
+ http_server_resource_create(server, pool, \
+ (http_server_resource_callback_t *)callback, \
+ (TRUE ? context : \
+ CALLBACK_TYPECHECK(callback, void (*)( \
+ typeof(context), struct http_server_request *req, \
+ const char *sub_path))))
+/* Resources are freed upon http_server_deinit(), so calling
+ http_server_resource_free() is only necessary when the resource needs to
+ disappear somewhere in the middle of the server lifetime. */
+void http_server_resource_free(struct http_server_resource **_res);
+
+pool_t http_server_resource_get_pool(struct http_server_resource *res)
+ ATTR_PURE;
+const char *
+http_server_resource_get_path(struct http_server_resource *res) ATTR_PURE;
+struct event *
+http_server_resource_get_event(struct http_server_resource *res) ATTR_PURE;
+
+void http_server_resource_add_location(struct http_server_resource *res,
+ const char *path);
+
+/* Call the specified callback when HTTP resource is destroyed. */
+void http_server_resource_set_destroy_callback(struct http_server_resource *res,
+ void (*callback)(void *),
+ void *context);
+#define http_server_resource_set_destroy_callback(req, callback, context) \
+ http_server_resource_set_destroy_callback(req, \
+ (void(*)(void*))callback, context - \
+ CALLBACK_TYPECHECK(callback, void (*)(typeof(context))))
+
+/*
+ * Server
+ */
+
+struct http_server *http_server_init(const struct http_server_settings *set);
+void http_server_deinit(struct http_server **_server);
+
+/* Shut down the server; accept no new requests and drop connections once
+ they become idle */
+void http_server_shut_down(struct http_server *server);
+
+/* Switch this server to the current ioloop */
+void http_server_switch_ioloop(struct http_server *server);
+
+#endif
diff --git a/src/lib-http/http-transfer-chunked.c b/src/lib-http/http-transfer-chunked.c
new file mode 100644
index 0000000..0e90992
--- /dev/null
+++ b/src/lib-http/http-transfer-chunked.c
@@ -0,0 +1,749 @@
+/* Copyright (c) 2013-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "istream-private.h"
+#include "ostream-private.h"
+#include "http-parser.h"
+#include "http-header-parser.h"
+
+#include "http-transfer.h"
+
+#define MIN_CHUNK_SIZE_WITH_EXTRA 6
+
+/*
+ * Chunked input stream
+ */
+
+enum http_transfer_chunked_parse_state {
+ HTTP_CHUNKED_PARSE_STATE_INIT,
+ HTTP_CHUNKED_PARSE_STATE_SIZE,
+ HTTP_CHUNKED_PARSE_STATE_EXT,
+ HTTP_CHUNKED_PARSE_STATE_EXT_NAME,
+ HTTP_CHUNKED_PARSE_STATE_EXT_EQ,
+ HTTP_CHUNKED_PARSE_STATE_EXT_VALUE,
+ HTTP_CHUNKED_PARSE_STATE_EXT_VALUE_STRING,
+ HTTP_CHUNKED_PARSE_STATE_EXT_VALUE_ESCAPE,
+ HTTP_CHUNKED_PARSE_STATE_EXT_VALUE_TOKEN,
+ HTTP_CHUNKED_PARSE_STATE_CR,
+ HTTP_CHUNKED_PARSE_STATE_LF,
+ HTTP_CHUNKED_PARSE_STATE_DATA,
+ HTTP_CHUNKED_PARSE_STATE_DATA_READY,
+ HTTP_CHUNKED_PARSE_STATE_DATA_CR,
+ HTTP_CHUNKED_PARSE_STATE_DATA_LF,
+ HTTP_CHUNKED_PARSE_STATE_TRAILER,
+ HTTP_CHUNKED_PARSE_STATE_FINISHED,
+};
+
+struct http_transfer_chunked_istream {
+ struct istream_private istream;
+ struct stat statbuf;
+
+ const unsigned char *begin, *cur, *end;
+ enum http_transfer_chunked_parse_state state;
+ unsigned int parsed_chars;
+
+ uoff_t chunk_size, chunk_v_offset, chunk_pos;
+ uoff_t size, max_size;
+
+ struct http_header_parser *header_parser;
+
+ bool finished:1;
+};
+
+/* Chunk parser */
+
+static inline const char *_chr_sanitize(unsigned char c)
+{
+ if (c >= 0x20 && c < 0x7F)
+ return t_strdup_printf("'%c'", c);
+ return t_strdup_printf("0x%02x", c);
+}
+
+static int
+http_transfer_chunked_parse_size(struct http_transfer_chunked_istream *tcstream)
+{
+ uoff_t size = 0, prev;
+
+ /* chunk-size = 1*HEXDIG */
+
+ while (tcstream->cur < tcstream->end) {
+ prev = tcstream->chunk_size;
+
+ if (*tcstream->cur >= '0' && *tcstream->cur <= '9')
+ size = *tcstream->cur-'0';
+ else if (*tcstream->cur >= 'A' && *tcstream->cur <= 'F')
+ size = *tcstream->cur-'A' + 10;
+ else if (*tcstream->cur >= 'a' && *tcstream->cur <= 'f')
+ size = *tcstream->cur-'a' + 10;
+ else {
+ if (tcstream->parsed_chars == 0) {
+ io_stream_set_error(
+ &tcstream->istream.iostream,
+ "Expected chunk size digit, "
+ "but found %s",
+ _chr_sanitize(*tcstream->cur));
+ return -1;
+ }
+ tcstream->parsed_chars = 0;
+ return 1;
+ }
+ tcstream->chunk_size <<= 4;
+ tcstream->chunk_size += size;
+ if (tcstream->chunk_size < prev) {
+ io_stream_set_error(&tcstream->istream.iostream,
+ "Chunk size exceeds integer limit");
+ return -1;
+ }
+ tcstream->parsed_chars++;
+ tcstream->cur++;
+ }
+
+ return 0;
+}
+
+static int
+http_transfer_chunked_skip_token(struct http_transfer_chunked_istream *tcstream)
+{
+ const unsigned char *first = tcstream->cur;
+
+ /* token = 1*tchar */
+ while (tcstream->cur < tcstream->end &&
+ http_char_is_token(*tcstream->cur))
+ tcstream->cur++;
+
+ tcstream->parsed_chars += (tcstream->cur-first);
+ if (tcstream->cur == tcstream->end)
+ return 0;
+ if (tcstream->parsed_chars == 0)
+ return -1;
+ return 1;
+}
+
+static int
+http_transfer_chunked_skip_qdtext(
+ struct http_transfer_chunked_istream *tcstream)
+{
+ /* qdtext = HTAB / SP / %x21 / %x23-5B / %x5D-7E / obs-text */
+ while (tcstream->cur < tcstream->end &&
+ http_char_is_qdtext(*tcstream->cur))
+ tcstream->cur++;
+ if (tcstream->cur == tcstream->end)
+ return 0;
+ return 1;
+}
+
+static int
+http_transfer_chunked_parse(struct http_transfer_chunked_istream *tcstream)
+{
+ int ret;
+
+ /* RFC 7230, Section 4.1: Chunked Transfer Encoding
+
+ chunked-body = *chunk
+ last-chunk
+ trailer-part
+ CRLF
+
+ chunk = chunk-size [ chunk-ext ] CRLF
+ chunk-data CRLF
+ chunk-size = 1*HEXDIG
+ last-chunk = 1*("0") [ chunk-ext ] CRLF
+
+ chunk-ext = *( ";" chunk-ext-name [ "=" chunk-ext-val ] )
+ chunk-ext-name = token
+ chunk-ext-val = token / quoted-string
+ chunk-data = 1*OCTET ; a sequence of chunk-size octets
+ trailer-part = *( header-field CRLF )
+ */
+
+ for (;;) {
+ switch (tcstream->state) {
+ case HTTP_CHUNKED_PARSE_STATE_INIT:
+ tcstream->chunk_size = 0;
+ tcstream->chunk_pos = 0;
+ tcstream->parsed_chars = 0;
+ tcstream->state = HTTP_CHUNKED_PARSE_STATE_SIZE;
+ /* fall through */
+ case HTTP_CHUNKED_PARSE_STATE_SIZE:
+ ret = http_transfer_chunked_parse_size(tcstream);
+ if (ret <= 0)
+ return ret;
+ tcstream->state = HTTP_CHUNKED_PARSE_STATE_EXT;
+ /* fall through */
+ case HTTP_CHUNKED_PARSE_STATE_EXT:
+ if (*tcstream->cur != ';') {
+ tcstream->state = HTTP_CHUNKED_PARSE_STATE_CR;
+ break;
+ }
+ /* chunk-ext */
+ tcstream->cur++;
+ tcstream->state = HTTP_CHUNKED_PARSE_STATE_EXT_NAME;
+ if (tcstream->cur >= tcstream->end)
+ return 0;
+ /* fall through */
+ case HTTP_CHUNKED_PARSE_STATE_EXT_NAME:
+ /* chunk-ext-name = token */
+ ret = http_transfer_chunked_skip_token(tcstream);
+ if (ret <= 0) {
+ if (ret < 0) {
+ io_stream_set_error(
+ &tcstream->istream.iostream,
+ "Invalid chunked extension name");
+ }
+ return ret;
+ }
+ tcstream->state = HTTP_CHUNKED_PARSE_STATE_EXT_EQ;
+ /* fall through */
+ case HTTP_CHUNKED_PARSE_STATE_EXT_EQ:
+ if (*tcstream->cur != '=') {
+ tcstream->state = HTTP_CHUNKED_PARSE_STATE_EXT;
+ break;
+ }
+ tcstream->cur++;
+ tcstream->state = HTTP_CHUNKED_PARSE_STATE_EXT_VALUE;
+ if (tcstream->cur >= tcstream->end)
+ return 0;
+ /* fall through */
+ case HTTP_CHUNKED_PARSE_STATE_EXT_VALUE:
+ /* chunk-ext-val = token / quoted-string */
+ if (*tcstream->cur != '"') {
+ tcstream->state =
+ HTTP_CHUNKED_PARSE_STATE_EXT_VALUE_TOKEN;
+ break;
+ }
+ tcstream->cur++;
+ tcstream->state =
+ HTTP_CHUNKED_PARSE_STATE_EXT_VALUE_STRING;
+ if (tcstream->cur >= tcstream->end)
+ return 0;
+ /* fall through */
+ case HTTP_CHUNKED_PARSE_STATE_EXT_VALUE_STRING:
+ if (*tcstream->cur == '"') {
+ tcstream->cur++;
+ tcstream->state = HTTP_CHUNKED_PARSE_STATE_EXT;
+ if (tcstream->cur >= tcstream->end)
+ return 0;
+ } else if ((ret = http_transfer_chunked_skip_qdtext(tcstream)) <= 0) {
+ if (ret < 0) {
+ io_stream_set_error(
+ &tcstream->istream.iostream,
+ "Invalid chunked extension value");
+ }
+ return ret;
+ } else if (*tcstream->cur == '\\') {
+ tcstream->cur++;
+ tcstream->state = HTTP_CHUNKED_PARSE_STATE_EXT_VALUE_ESCAPE;
+ if (tcstream->cur >= tcstream->end)
+ return 0;
+ } else {
+ io_stream_set_error(
+ &tcstream->istream.iostream,
+ "Invalid character %s in chunked extension value string",
+ _chr_sanitize(*tcstream->cur));
+ return -1;
+ }
+ break;
+ case HTTP_CHUNKED_PARSE_STATE_EXT_VALUE_ESCAPE:
+ /* ( HTAB / SP / VCHAR / obs-text ) */
+ if (!http_char_is_text(*tcstream->cur)) {
+ io_stream_set_error(
+ &tcstream->istream.iostream,
+ "Escaped invalid character %s in chunked extension value string",
+ _chr_sanitize(*tcstream->cur));
+ return -1;
+ }
+ tcstream->state =
+ HTTP_CHUNKED_PARSE_STATE_EXT_VALUE_STRING;
+ if (tcstream->cur >= tcstream->end)
+ return 0;
+ break;
+ case HTTP_CHUNKED_PARSE_STATE_EXT_VALUE_TOKEN:
+ ret = http_transfer_chunked_skip_token(tcstream);
+ if (ret <= 0) {
+ if (ret < 0) {
+ io_stream_set_error(
+ &tcstream->istream.iostream,
+ "Invalid chunked extension value");
+ }
+ return ret;
+ }
+ tcstream->state = HTTP_CHUNKED_PARSE_STATE_EXT;
+ break;
+ case HTTP_CHUNKED_PARSE_STATE_CR:
+ tcstream->state = HTTP_CHUNKED_PARSE_STATE_LF;
+ if (*tcstream->cur == '\r') {
+ tcstream->cur++;
+ if (tcstream->cur >= tcstream->end)
+ return 0;
+ }
+ /* fall through */
+ case HTTP_CHUNKED_PARSE_STATE_LF:
+ if (*tcstream->cur != '\n') {
+ io_stream_set_error(
+ &tcstream->istream.iostream,
+ "Expected new line after chunk size, "
+ "but found %s",
+ _chr_sanitize(*tcstream->cur));
+ return -1;
+ }
+ tcstream->cur++;
+ if (tcstream->chunk_size > 0)
+ tcstream->state = HTTP_CHUNKED_PARSE_STATE_DATA;
+ else
+ tcstream->state = HTTP_CHUNKED_PARSE_STATE_TRAILER;
+ return 1;
+ case HTTP_CHUNKED_PARSE_STATE_DATA_READY:
+ /* fall through */
+ case HTTP_CHUNKED_PARSE_STATE_DATA_CR:
+ tcstream->state = HTTP_CHUNKED_PARSE_STATE_DATA_LF;
+ if (*tcstream->cur == '\r') {
+ tcstream->cur++;
+ if (tcstream->cur >= tcstream->end)
+ return 0;
+ }
+ /* fall through */
+ case HTTP_CHUNKED_PARSE_STATE_DATA_LF:
+ if (*tcstream->cur != '\n') {
+ io_stream_set_error(
+ &tcstream->istream.iostream,
+ "Expected new line after chunk data, but found %s",
+ _chr_sanitize(*tcstream->cur));
+ return -1;
+ }
+ tcstream->cur++;
+ tcstream->state = HTTP_CHUNKED_PARSE_STATE_INIT;
+ break;
+ default:
+ i_unreached();
+ }
+ }
+
+ i_unreached();
+ return -1;
+}
+
+static int
+http_transfer_chunked_parse_next(struct http_transfer_chunked_istream *tcstream)
+{
+ struct istream_private *stream = &tcstream->istream;
+ struct istream *input = tcstream->istream.parent;
+ size_t size;
+ int ret;
+
+ while ((ret = i_stream_read_more(input, &tcstream->begin, &size)) > 0) {
+ tcstream->cur = tcstream->begin;
+ tcstream->end = tcstream->cur + size;
+
+ if ((ret = http_transfer_chunked_parse(tcstream)) < 0) {
+ stream->istream.stream_errno = EIO;
+ return -1;
+ }
+
+ i_stream_skip(input, tcstream->cur - tcstream->begin);
+
+ if (ret > 0) {
+ if (tcstream->state == HTTP_CHUNKED_PARSE_STATE_DATA) {
+ tcstream->chunk_v_offset = input->v_offset;
+
+ tcstream->size += tcstream->chunk_size;
+ if (tcstream->max_size > 0 &&
+ tcstream->size > tcstream->max_size) {
+ io_stream_set_error(
+ &tcstream->istream.iostream,
+ "Total chunked payload size exceeds maximum");
+ stream->istream.stream_errno = EMSGSIZE;
+ return -1;
+ }
+ }
+ return ret;
+ }
+ }
+
+ i_assert(ret != -2);
+
+ if (ret < 0) {
+ if (stream->parent->eof &&
+ stream->parent->stream_errno == 0) {
+ /* unexpected EOF */
+ io_stream_set_error(&tcstream->istream.iostream,
+ "Unexpected end of payload");
+ stream->istream.stream_errno = EIO;
+ } else {
+ /* parent stream error */
+ stream->istream.stream_errno =
+ stream->parent->stream_errno;
+ }
+ }
+ return ret;
+}
+
+/* Input stream */
+
+static ssize_t
+http_transfer_chunked_istream_read_data(
+ struct http_transfer_chunked_istream *tcstream)
+{
+ struct istream_private *stream = &tcstream->istream;
+ const unsigned char *data;
+ size_t size, avail;
+ ssize_t ret = 0;
+
+ i_assert(tcstream->chunk_pos <= tcstream->chunk_size);
+ if (tcstream->chunk_pos == tcstream->chunk_size) {
+ tcstream->state = HTTP_CHUNKED_PARSE_STATE_DATA_READY;
+ return 0;
+ }
+
+ // FIXME: is this even necessary?
+ i_stream_seek(stream->parent,
+ tcstream->chunk_v_offset + tcstream->chunk_pos);
+
+ /* read from parent if necessary */
+ data = i_stream_get_data(stream->parent, &size);
+ if (size == 0) {
+ ret = i_stream_read_memarea(stream->parent);
+ if (ret <= 0) {
+ i_assert(ret != -2); /* 0 sized buffer can't be full */
+ if (stream->parent->eof &&
+ stream->parent->stream_errno == 0) {
+ /* unexpected EOF */
+ io_stream_set_error(
+ &tcstream->istream.iostream,
+ "Unexpected end of payload");
+ stream->istream.stream_errno = EIO;
+ } else {
+ /* parent stream error */
+ stream->istream.stream_errno =
+ stream->parent->stream_errno;
+ }
+ return ret;
+ }
+ data = i_stream_get_data(stream->parent, &size);
+ i_assert(size != 0);
+ }
+
+ size = (size > (tcstream->chunk_size - tcstream->chunk_pos) ?
+ (tcstream->chunk_size - tcstream->chunk_pos) : size);
+
+ /* Allocate buffer space */
+ if (!i_stream_try_alloc(stream, size, &avail))
+ return -2;
+
+ /* Copy payload */
+ size = size > avail ? avail : size;
+ memcpy(&stream->w_buffer[stream->pos], data, size);
+
+ i_stream_skip(stream->parent, size);
+
+ tcstream->chunk_pos += size;
+ i_assert(tcstream->chunk_pos <= tcstream->chunk_size);
+ if (tcstream->chunk_pos == tcstream->chunk_size)
+ tcstream->state = HTTP_CHUNKED_PARSE_STATE_DATA_READY;
+
+ ret = size;
+ stream->pos = stream->pos+size;
+ return ret;
+}
+
+static int
+http_transfer_chunked_parse_trailer(
+ struct http_transfer_chunked_istream *tcstream)
+{
+ struct istream_private *stream = &tcstream->istream;
+ const char *field_name, *error;
+ const unsigned char *field_data;
+ size_t field_size;
+ int ret;
+
+ if (tcstream->header_parser == NULL) {
+ /* NOTE: trailer is currently ignored */
+ /* FIXME: limit trailer size */
+ tcstream->header_parser =
+ http_header_parser_init(tcstream->istream.parent,
+ NULL, 0);
+ }
+
+ while ((ret = http_header_parse_next_field(tcstream->header_parser,
+ &field_name, &field_data,
+ &field_size, &error)) > 0) {
+ if (field_name == NULL)
+ break;
+ }
+
+ if (ret <= 0) {
+ if (ret < 0) {
+ io_stream_set_error(
+ &stream->iostream,
+ "Failed to parse chunked trailer: %s", error);
+ stream->istream.stream_errno = EIO;
+ }
+ return ret;
+ }
+ return 1;
+}
+
+static ssize_t
+http_transfer_chunked_istream_read(struct istream_private *stream)
+{
+ struct http_transfer_chunked_istream *tcstream =
+ (struct http_transfer_chunked_istream *)stream;
+ ssize_t ret = 0;
+
+ for (;;) {
+ switch (tcstream->state) {
+ case HTTP_CHUNKED_PARSE_STATE_FINISHED:
+ tcstream->istream.istream.eof = TRUE;
+ return -1;
+ case HTTP_CHUNKED_PARSE_STATE_DATA:
+ ret = http_transfer_chunked_istream_read_data(tcstream);
+ if (ret != 0)
+ return ret;
+ if (tcstream->state !=
+ HTTP_CHUNKED_PARSE_STATE_DATA_READY)
+ return 0;
+ break;
+ case HTTP_CHUNKED_PARSE_STATE_TRAILER:
+ ret = http_transfer_chunked_parse_trailer(tcstream);
+ if (ret <= 0)
+ return ret;
+ tcstream->state = HTTP_CHUNKED_PARSE_STATE_FINISHED;
+ tcstream->istream.istream.eof = TRUE;
+ return -1;
+ default:
+ ret = http_transfer_chunked_parse_next(tcstream);
+ if (ret <= 0)
+ return ret;
+ }
+ }
+
+ return -1;
+}
+
+static void
+http_transfer_chunked_istream_destroy(struct iostream_private *stream)
+{
+ struct http_transfer_chunked_istream *tcstream =
+ (struct http_transfer_chunked_istream *)stream;
+
+ if (tcstream->header_parser != NULL)
+ http_header_parser_deinit(&tcstream->header_parser);
+
+ // FIXME: copied from istream.c; there's got to be a better way.
+ i_stream_free_buffer(&tcstream->istream);
+}
+
+struct istream *
+http_transfer_chunked_istream_create(struct istream *input, uoff_t max_size)
+{
+ struct http_transfer_chunked_istream *tcstream;
+
+ tcstream = i_new(struct http_transfer_chunked_istream, 1);
+ tcstream->max_size = max_size;
+
+ tcstream->istream.max_buffer_size =
+ input->real_stream->max_buffer_size;
+
+ tcstream->istream.iostream.destroy =
+ http_transfer_chunked_istream_destroy;
+ tcstream->istream.read = http_transfer_chunked_istream_read;
+
+ tcstream->istream.istream.readable_fd = FALSE;
+ tcstream->istream.istream.blocking = input->blocking;
+ tcstream->istream.istream.seekable = FALSE;
+ return i_stream_create(&tcstream->istream, input,
+ i_stream_get_fd(input), 0);
+}
+
+/*
+ * Chunked output stream
+ */
+
+// FIXME: provide support for corking the stream. This means that we'll have
+// to buffer sent data here rather than in the parent steam; we need to know
+// the size of the chunks before we can send them.
+
+#define DEFAULT_MAX_BUFFER_SIZE (1024*32)
+
+struct http_transfer_chunked_ostream {
+ struct ostream_private ostream;
+
+ size_t chunk_size, chunk_pos;
+
+ bool chunk_active:1;
+ bool sent_trailer:1;
+};
+
+static size_t _log16(size_t in)
+{
+ size_t res = 0;
+
+ while (in > 0) {
+ in >>= 4;
+ res++;
+ }
+ return res;
+}
+
+static size_t _max_chunk_size(size_t avail)
+{
+ size_t chunk_extra = 2*2;
+
+ /* Make sure we have room for both chunk data and overhead
+
+ chunk = chunk-size [ chunk-ext ] CRLF
+ chunk-data CRLF
+ chunk-size = 1*HEXDIG
+ */
+ chunk_extra += _log16(avail);
+ return (avail < chunk_extra ? 0 : avail - chunk_extra);
+}
+
+static int
+http_transfer_chunked_ostream_send_trailer(
+ struct http_transfer_chunked_ostream *tcstream)
+{
+ struct ostream_private *stream = &tcstream->ostream;
+ ssize_t sent;
+
+ if (tcstream->sent_trailer)
+ return 1;
+
+ if (o_stream_get_buffer_avail_size(stream->parent) < 5) {
+ if (o_stream_flush_parent(stream) < 0)
+ return -1;
+ if (o_stream_get_buffer_avail_size(stream->parent) < 5)
+ return 0;
+ }
+
+ sent = o_stream_send(tcstream->ostream.parent, "0\r\n\r\n", 5);
+ if (sent < 0) {
+ o_stream_copy_error_from_parent(stream);
+ return -1;
+ }
+ i_assert(sent == 5);
+
+ tcstream->sent_trailer = TRUE;
+ return 1;
+}
+
+static void
+http_transfer_chunked_ostream_close(struct iostream_private *stream,
+ bool close_parent)
+{
+ struct http_transfer_chunked_ostream *tcstream =
+ (struct http_transfer_chunked_ostream *)stream;
+
+ i_assert(tcstream->ostream.finished ||
+ tcstream->ostream.ostream.stream_errno != 0 ||
+ tcstream->ostream.error_handling_disabled);
+ if (close_parent)
+ o_stream_close(tcstream->ostream.parent);
+}
+
+static int
+http_transfer_chunked_ostream_flush(struct ostream_private *stream)
+{
+ struct http_transfer_chunked_ostream *tcstream =
+ (struct http_transfer_chunked_ostream *)stream;
+ int ret;
+
+ if (stream->finished &&
+ (ret = http_transfer_chunked_ostream_send_trailer(tcstream)) <= 0)
+ return ret;
+
+ return o_stream_flush_parent(stream);
+}
+
+static ssize_t
+http_transfer_chunked_ostream_sendv(struct ostream_private *stream,
+ const struct const_iovec *iov,
+ unsigned int iov_count)
+{
+ struct http_transfer_chunked_ostream *tcstream =
+ (struct http_transfer_chunked_ostream *)stream;
+ struct const_iovec *iov_new;
+ unsigned int iov_count_new, i;
+ size_t bytes = 0, max_bytes;
+ ssize_t ret;
+ const char *prefix;
+
+ i_assert(stream->parent->real_stream->max_buffer_size >=
+ MIN_CHUNK_SIZE_WITH_EXTRA);
+
+ if ((ret = o_stream_flush(stream->parent)) <= 0) {
+ /* error / we still couldn't flush existing data to
+ parent stream. */
+ if (ret < 0)
+ o_stream_copy_error_from_parent(stream);
+ return ret;
+ }
+
+ /* check how many bytes we want to send */
+ bytes = 0;
+ for (i = 0; i < iov_count; i++)
+ bytes += iov[i].iov_len;
+
+ /* check if we have room to send at least one byte */
+ max_bytes = o_stream_get_buffer_avail_size(stream->parent);
+ max_bytes = _max_chunk_size(max_bytes);
+ if (max_bytes < MIN_CHUNK_SIZE_WITH_EXTRA)
+ return 0;
+
+ tcstream->chunk_size = (bytes > max_bytes ? max_bytes : bytes);
+
+ /* determine what to send */
+ bytes = tcstream->chunk_size;
+ iov_count_new = 1;
+ for (i = 0; i < iov_count && bytes > 0; i++) {
+ if (bytes <= iov[i].iov_len)
+ break;
+ bytes -= iov[i].iov_len;
+ iov_count_new++;
+ }
+
+ /* create new iovec */
+ prefix = t_strdup_printf("%llx\r\n",
+ (unsigned long long)tcstream->chunk_size);
+ iov_count = iov_count_new + 2;
+ iov_new = t_new(struct const_iovec, iov_count);
+ iov_new[0].iov_base = prefix;
+ iov_new[0].iov_len = strlen(prefix);
+ memcpy(&iov_new[1], iov, sizeof(struct const_iovec) * iov_count_new);
+ iov_new[iov_count-2].iov_len = bytes;
+ iov_new[iov_count-1].iov_base = "\r\n";
+ iov_new[iov_count-1].iov_len = 2;
+
+ /* send */
+ if ((ret = o_stream_sendv(stream->parent, iov_new, iov_count)) <= 0) {
+ i_assert(ret < 0);
+ o_stream_copy_error_from_parent(stream);
+ return -1;
+ }
+
+ /* all must be sent */
+ i_assert((size_t)ret == (tcstream->chunk_size + iov_new[0].iov_len +
+ iov_new[iov_count-1].iov_len));
+
+ stream->ostream.offset += tcstream->chunk_size;
+ return tcstream->chunk_size;
+}
+
+struct ostream *
+http_transfer_chunked_ostream_create(struct ostream *output)
+{
+ struct http_transfer_chunked_ostream *tcstream;
+ size_t max_size;
+
+ tcstream = i_new(struct http_transfer_chunked_ostream, 1);
+ tcstream->ostream.sendv = http_transfer_chunked_ostream_sendv;
+ tcstream->ostream.flush = http_transfer_chunked_ostream_flush;
+ tcstream->ostream.iostream.close = http_transfer_chunked_ostream_close;
+ if (output->real_stream->max_buffer_size > 0)
+ max_size = output->real_stream->max_buffer_size;
+ else
+ max_size = DEFAULT_MAX_BUFFER_SIZE;
+
+ tcstream->ostream.max_buffer_size = _max_chunk_size(max_size);
+ return o_stream_create(&tcstream->ostream, output,
+ o_stream_get_fd(output));
+}
diff --git a/src/lib-http/http-transfer.h b/src/lib-http/http-transfer.h
new file mode 100644
index 0000000..f3f6791
--- /dev/null
+++ b/src/lib-http/http-transfer.h
@@ -0,0 +1,26 @@
+#ifndef HTTP_TRANSFER_H
+#define HTTP_TRANSFER_H
+
+struct http_transfer_param {
+ const char *attribute;
+ const char *value;
+};
+ARRAY_DEFINE_TYPE(http_transfer_param, struct http_transfer_param);
+
+struct http_transfer_coding {
+ const char *name;
+ ARRAY_TYPE(http_transfer_param) parameters;
+
+};
+ARRAY_DEFINE_TYPE(http_transfer_coding, struct http_transfer_coding);
+
+
+// FIXME: we currently lack a means to get error strings from the input stream
+
+struct istream *
+http_transfer_chunked_istream_create(struct istream *input, uoff_t max_size);
+struct ostream *
+ http_transfer_chunked_ostream_create(struct ostream *output);
+
+#endif
+
diff --git a/src/lib-http/http-url.c b/src/lib-http/http-url.c
new file mode 100644
index 0000000..229a58a
--- /dev/null
+++ b/src/lib-http/http-url.c
@@ -0,0 +1,678 @@
+/* Copyright (c) 2013-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "str.h"
+#include "strfuncs.h"
+#include "net.h"
+#include "uri-util.h"
+
+#include "http-url.h"
+#include "http-request.h"
+
+/*
+ * HTTP URL parser
+ */
+
+struct http_url_parser {
+ struct uri_parser parser;
+
+ enum http_url_parse_flags flags;
+
+ struct http_url *url;
+ struct http_url *base;
+
+ enum http_request_target_format req_format;
+
+ bool relative:1;
+ bool request_target:1;
+};
+
+static bool http_url_parse_authority_form(struct http_url_parser *url_parser);
+
+static bool
+http_url_parse_scheme(struct http_url_parser *url_parser, const char **scheme_r)
+{
+ struct uri_parser *parser = &url_parser->parser;
+
+ *scheme_r = NULL;
+ if ((url_parser->flags & HTTP_URL_PARSE_SCHEME_EXTERNAL) != 0)
+ return TRUE;
+
+ if (uri_parse_scheme(parser, scheme_r) <= 0) {
+ parser->cur = parser->begin;
+ return FALSE;
+ }
+
+ return TRUE;
+}
+
+static bool http_url_parse_unknown_scheme(struct http_url_parser *url_parser)
+{
+ struct uri_parser *parser = &url_parser->parser;
+
+ if (url_parser->request_target) {
+ /* Valid as non-HTTP scheme, but also try to parse as authority
+ */
+ parser->cur = parser->begin;
+ if (!http_url_parse_authority_form(url_parser)) {
+ /* indicate non-http-url */
+ url_parser->url = NULL;
+ url_parser->req_format =
+ HTTP_REQUEST_TARGET_FORMAT_ABSOLUTE;
+ }
+ return TRUE;
+ }
+ parser->error = "Not an HTTP URL";
+ return FALSE;
+}
+
+static bool
+http_url_parse_userinfo(struct http_url_parser *url_parser,
+ struct uri_authority *auth,
+ const char **user_r, const char **password_r)
+{
+ struct uri_parser *parser = &url_parser->parser;
+ const char *p;
+
+ *user_r = *password_r = NULL;
+
+ if (auth->enc_userinfo == NULL)
+ return TRUE;
+
+ if ((url_parser->flags & HTTP_URL_ALLOW_USERINFO_PART) == 0) {
+ /* RFC 7230, Section 2.7.1: http URI Scheme
+
+ A sender MUST NOT generate the userinfo subcomponent (and its
+ "@" delimiter) when an "http" URI reference is generated
+ within a message as a request target or header field value.
+ Before making use of an "http" URI reference received from an
+ untrusted source, a recipient SHOULD parse for userinfo and
+ treat its presence as an error; it is likely being used to
+ obscure the authority for the sake of phishing attacks.
+ */
+ parser->error = "HTTP URL does not allow `userinfo@' part";
+ return FALSE;
+ }
+
+ p = strchr(auth->enc_userinfo, ':');
+ if (p == NULL) {
+ if (!uri_data_decode(parser, auth->enc_userinfo, NULL, user_r))
+ return FALSE;
+ } else {
+ if (!uri_data_decode(parser, auth->enc_userinfo, p, user_r))
+ return FALSE;
+ if (!uri_data_decode(parser, p + 1, NULL, password_r))
+ return FALSE;
+ }
+ return TRUE;
+}
+
+static bool http_url_parse_authority(struct http_url_parser *url_parser)
+{
+ struct uri_parser *parser = &url_parser->parser;
+ struct http_url *url = url_parser->url;
+ struct uri_authority auth;
+ const char *user = NULL, *password = NULL;
+ int ret;
+
+ if ((ret = uri_parse_host_authority(parser, &auth)) < 0)
+ return FALSE;
+ if (auth.host.name == NULL || *auth.host.name == '\0') {
+ /* RFC 7230, Section 2.7.1: http URI Scheme
+
+ A sender MUST NOT generate an "http" URI with an empty host
+ identifier. A recipient that processes such a URI reference
+ MUST reject it as invalid.
+ */
+ parser->error = "HTTP URL does not allow empty host identifier";
+ return FALSE;
+ }
+ if (ret > 0) {
+ if (!http_url_parse_userinfo(url_parser, &auth,
+ &user, &password))
+ return FALSE;
+ }
+ if (url != NULL) {
+ uri_host_copy(parser->pool, &url->host, &auth.host);
+ url->port = auth.port;
+ url->user = p_strdup(parser->pool, user);
+ url->password = p_strdup(parser->pool, password);
+ }
+ return TRUE;
+}
+
+static bool http_url_parse_authority_form(struct http_url_parser *url_parser)
+{
+ struct uri_parser *parser = &url_parser->parser;
+
+ if (!http_url_parse_authority(url_parser))
+ return FALSE;
+ if (parser->cur != parser->end)
+ return FALSE;
+ url_parser->req_format = HTTP_REQUEST_TARGET_FORMAT_AUTHORITY;
+ return TRUE;
+}
+
+static int
+http_url_parse_path(struct http_url_parser *url_parser)
+{
+ struct uri_parser *parser = &url_parser->parser;
+ struct http_url *url = url_parser->url, *base = url_parser->base;
+ const char *const *path;
+ int path_relative;
+ string_t *fullpath = NULL;
+ int ret;
+
+ /* path-abempty / path-absolute / path-noscheme / path-empty */
+ if ((ret = uri_parse_path(parser, &path_relative, &path)) < 0)
+ return -1;
+
+ /* Resolve path */
+ if (ret == 0) {
+ if (url_parser->relative && url != NULL)
+ url->path = p_strdup(parser->pool, base->path);
+ return 0;
+ }
+
+ if (url != NULL)
+ fullpath = t_str_new(256);
+
+ if (url_parser->relative && path_relative > 0 && base->path != NULL) {
+ const char *pbegin = base->path;
+ const char *pend = base->path + strlen(base->path);
+ const char *p = pend - 1;
+
+ i_assert(*pbegin == '/');
+
+ /* Discard trailing segments of base path based on how many
+ effective leading '..' segments were found in the relative
+ path.
+ */
+ while (path_relative > 0 && p > pbegin) {
+ while (p > pbegin && *p != '/') p--;
+ if (p >= pbegin) {
+ pend = p;
+ path_relative--;
+ }
+ if (p > pbegin) p--;
+ }
+
+ if (url != NULL && pend > pbegin)
+ str_append_data(fullpath, pbegin, pend - pbegin);
+ }
+
+ /* Append relative path */
+ while (*path != NULL) {
+ const char *part;
+
+ if (!uri_data_decode(parser, *path, NULL, &part))
+ return -1;
+
+ if (url != NULL) {
+ str_append_c(fullpath, '/');
+ str_append(fullpath, part);
+ }
+ path++;
+ }
+
+ if (url != NULL)
+ url->path = p_strdup(parser->pool, str_c(fullpath));
+ return 1;
+}
+
+static bool
+http_url_parse_query(struct http_url_parser *url_parser, bool have_path)
+{
+ struct uri_parser *parser = &url_parser->parser;
+ struct http_url *url = url_parser->url, *base = url_parser->base;
+ const char *query;
+ int ret;
+
+ if ((ret = uri_parse_query(parser, &query)) < 0)
+ return FALSE;
+ if (url == NULL)
+ return TRUE;
+
+ if (ret > 0)
+ url->enc_query = p_strdup(parser->pool, query);
+ else if (url_parser->relative && !have_path)
+ url->enc_query = p_strdup(parser->pool, base->enc_query);
+ return TRUE;
+}
+
+static bool
+http_url_parse_fragment(struct http_url_parser *url_parser, bool have_path)
+{
+ struct uri_parser *parser = &url_parser->parser;
+ struct http_url *url = url_parser->url, *base = url_parser->base;
+ const char *fragment;
+ int ret;
+
+ if ((ret = uri_parse_fragment(parser, &fragment)) < 0)
+ return FALSE;
+ if (ret > 0 &&
+ (url_parser->flags & HTTP_URL_ALLOW_FRAGMENT_PART) == 0) {
+ parser->error =
+ "URL fragment not allowed for HTTP URL in this context";
+ return FALSE;
+ }
+ if (url == NULL)
+ return TRUE;
+
+ if (ret > 0)
+ url->enc_fragment = p_strdup(parser->pool, fragment);
+ else if (url_parser->relative && !have_path)
+ url->enc_fragment = p_strdup(parser->pool, base->enc_fragment);
+ return TRUE;
+}
+
+static bool http_url_do_parse(struct http_url_parser *url_parser)
+{
+ struct uri_parser *parser = &url_parser->parser;
+ struct http_url *url = url_parser->url, *base = url_parser->base;
+ bool relative = TRUE, have_scheme = FALSE, have_authority = FALSE,
+ have_path = FALSE;
+ const char *scheme;
+ int ret;
+
+ /* RFC 7230, Appendix B:
+
+ http-URI = "http://" authority path-abempty [ "?" query ]
+ [ "#" fragment ]
+ https-URI = "https://" authority path-abempty [ "?" query ]
+ [ "#" fragment ]
+ partial-URI = relative-part [ "?" query ]
+
+ request-target = origin-form / absolute-form / authority-form /
+ asterisk-form
+
+ origin-form = absolute-path [ "?" query ]
+ absolute-form = absolute-URI
+ authority-form = authority
+ asterisk-form = "*"
+ ; Not parsed here
+
+ absolute-path = 1*( "/" segment )
+
+ RFC 3986, Appendix A: (implemented in uri-util.h)
+
+ absolute-URI = scheme ":" hier-part [ "?" query ]
+
+ hier-part = "//" authority path-abempty
+ / path-absolute
+ / path-rootless
+ / path-empty
+
+ relative-part = "//" authority path-abempty
+ / path-absolute
+ / path-noscheme
+ / path-empty
+
+ authority = [ userinfo "@" ] host [ ":" port ]
+
+ path-abempty = *( "/" segment )
+ path-absolute = "/" [ segment-nz *( "/" segment ) ]
+ path-noscheme = segment-nz-nc *( "/" segment )
+ path-rootless = segment-nz *( "/" segment )
+ path-empty = 0<pchar>
+
+ segment = *pchar
+ segment-nz = 1*pchar
+ segment-nz-nc = 1*( unreserved / pct-encoded / sub-delims / "@" )
+ ; non-zero-length segment without any colon ":"
+
+ query = *( pchar / "/" / "?" )
+ fragment = *( pchar / "/" / "?" )
+ */
+
+ /* "http:" / "https:" */
+ if (http_url_parse_scheme(url_parser, &scheme)) {
+ if (scheme == NULL) {
+ /* Scheme externally parsed */
+ } else if (strcasecmp(scheme, "https") == 0) {
+ if (url != NULL)
+ url->have_ssl = TRUE;
+ } else if (strcasecmp(scheme, "http") != 0) {
+ return http_url_parse_unknown_scheme(url_parser);
+ }
+
+ relative = FALSE;
+ have_scheme = TRUE;
+ }
+
+ /* "//" authority ; or
+ * ["//"] authority ; when parsing a request target
+ */
+ if (parser->cur < parser->end && parser->cur[0] == '/') {
+ if ((have_scheme || !url_parser->request_target) &&
+ (parser->cur + 1) < parser->end && parser->cur[1] == '/') {
+ parser->cur += 2;
+ relative = FALSE;
+ have_authority = TRUE;
+ } else {
+ /* start of absolute-path */
+ }
+ } else if (url_parser->request_target && !have_scheme) {
+ if (!http_url_parse_authority_form(url_parser)) {
+ /* not non-HTTP scheme and invalid as authority-form */
+ parser->error = "Request target is invalid";
+ return FALSE;
+ }
+ return TRUE;
+ }
+
+ if (have_scheme && !have_authority) {
+ parser->error = "Absolute HTTP URL requires `//' after `http:'";
+ return FALSE;
+ }
+
+ if (have_authority) {
+ if (!http_url_parse_authority(url_parser))
+ return FALSE;
+ }
+
+ /* Relative URLs are only valid when we have a base URL */
+ if (relative) {
+ if (base == NULL) {
+ parser->error = "Relative HTTP URL not allowed";
+ return FALSE;
+ } else if (!have_authority && url != NULL) {
+ uri_host_copy(parser->pool, &url->host, &base->host);
+ url->port = base->port;
+ url->have_ssl = base->have_ssl;
+ url->user = p_strdup_empty(parser->pool, base->user);
+ url->password = p_strdup_empty(parser->pool,
+ base->password);
+ }
+
+ url_parser->relative = TRUE;
+ }
+
+ /* path-abempty / path-absolute / path-noscheme / path-empty */
+ ret = http_url_parse_path(url_parser);
+ if (ret < 0)
+ return FALSE;
+ have_path = (ret > 0);
+
+ /* [ "?" query ] */
+ if (!http_url_parse_query(url_parser, have_path))
+ return FALSE;
+
+ /* [ "#" fragment ] */
+ if (!http_url_parse_fragment(url_parser, have_path))
+ return FALSE;
+
+ /* must be at end of URL now */
+ i_assert(parser->cur == parser->end);
+
+ if (have_scheme)
+ url_parser->req_format = HTTP_REQUEST_TARGET_FORMAT_ABSOLUTE;
+ return TRUE;
+}
+
+/* Public API */
+
+int http_url_parse(const char *url, struct http_url *base,
+ enum http_url_parse_flags flags, pool_t pool,
+ struct http_url **url_r, const char **error_r)
+{
+ struct http_url_parser url_parser;
+
+ /* base != NULL indicates whether relative URLs are allowed. However,
+ certain flags may also dictate whether relative URLs are
+ allowed/required. */
+ i_assert((flags & HTTP_URL_PARSE_SCHEME_EXTERNAL) == 0 || base == NULL);
+
+ i_zero(&url_parser);
+ uri_parser_init(&url_parser.parser, pool, url);
+ url_parser.parser.allow_pct_nul = (flags & HTTP_URL_ALLOW_PCT_NUL) != 0;
+
+ url_parser.url = p_new(pool, struct http_url, 1);
+ url_parser.base = base;
+ url_parser.flags = flags;
+
+ if (!http_url_do_parse(&url_parser)) {
+ *error_r = url_parser.parser.error;
+ return -1;
+ }
+ *url_r = url_parser.url;
+ return 0;
+}
+
+int http_url_request_target_parse(const char *request_target,
+ const char *host_header,
+ const struct http_url *default_base,
+ pool_t pool,
+ struct http_request_target *target,
+ const char **error_r)
+{
+ struct http_url_parser url_parser;
+ struct uri_authority auth;
+ struct http_url base;
+
+ i_zero(&base);
+ if (host_header != NULL && *host_header != '\0') {
+ struct uri_parser *parser;
+
+ i_zero(&url_parser);
+ parser = &url_parser.parser;
+ uri_parser_init(parser, pool, host_header);
+
+ if (uri_parse_host_authority(parser, &auth) <= 0) {
+ *error_r = t_strdup_printf("Invalid Host header: %s",
+ parser->error);
+ return -1;
+ }
+
+ if (parser->cur != parser->end || auth.enc_userinfo != NULL) {
+ *error_r = "Invalid Host header: "
+ "Contains invalid character";
+ return -1;
+ }
+
+ base.host = auth.host;
+ base.port = auth.port;
+ } else if (default_base == NULL) {
+ *error_r = "Empty Host header";
+ return -1;
+ } else {
+ i_assert(default_base != NULL);
+ base = *default_base;
+ }
+
+ if (request_target[0] == '*' && request_target[1] == '\0') {
+ struct http_url *url = p_new(pool, struct http_url, 1);
+
+ uri_host_copy(pool, &url->host, &base.host);
+ url->port = base.port;
+ target->url = url;
+ target->format = HTTP_REQUEST_TARGET_FORMAT_ASTERISK;
+ return 0;
+ }
+
+ i_zero(&url_parser);
+ uri_parser_init(&url_parser.parser, pool, request_target);
+
+ url_parser.url = p_new(pool, struct http_url, 1);
+ url_parser.request_target = TRUE;
+ url_parser.req_format = HTTP_REQUEST_TARGET_FORMAT_ORIGIN;
+ url_parser.base = &base;
+ url_parser.flags = 0;
+
+ if (!http_url_do_parse(&url_parser)) {
+ *error_r = url_parser.parser.error;
+ return -1;
+ }
+
+ target->url = url_parser.url;
+ target->format = url_parser.req_format;
+ return 0;
+}
+
+/*
+ * HTTP URL manipulation
+ */
+
+void http_url_init_authority_from(struct http_url *dest,
+ const struct http_url *src)
+{
+ i_zero(dest);
+ dest->host = src->host;
+ dest->port = src->port;
+ dest->have_ssl = src->have_ssl;
+}
+
+void http_url_copy_authority(pool_t pool, struct http_url *dest,
+ const struct http_url *src)
+{
+ i_zero(dest);
+ uri_host_copy(pool, &dest->host, &src->host);
+ dest->port = src->port;
+ dest->have_ssl = src->have_ssl;
+}
+
+struct http_url *
+http_url_clone_authority(pool_t pool, const struct http_url *src)
+{
+ struct http_url *new_url;
+
+ new_url = p_new(pool, struct http_url, 1);
+ http_url_copy_authority(pool, new_url, src);
+
+ return new_url;
+}
+
+void http_url_copy(pool_t pool, struct http_url *dest,
+ const struct http_url *src)
+{
+ http_url_copy_authority(pool, dest, src);
+ dest->path = p_strdup(pool, src->path);
+ dest->enc_query = p_strdup(pool, src->enc_query);
+ dest->enc_fragment = p_strdup(pool, src->enc_fragment);
+}
+
+void http_url_copy_with_userinfo(pool_t pool, struct http_url *dest,
+ const struct http_url *src)
+{
+ http_url_copy(pool, dest, src);
+ dest->user = p_strdup(pool, src->user);
+ dest->password = p_strdup(pool, src->password);
+}
+
+struct http_url *http_url_clone(pool_t pool, const struct http_url *src)
+{
+ struct http_url *new_url;
+
+ new_url = p_new(pool, struct http_url, 1);
+ http_url_copy(pool, new_url, src);
+
+ return new_url;
+}
+
+struct http_url *
+http_url_clone_with_userinfo(pool_t pool, const struct http_url *src)
+{
+ struct http_url *new_url;
+
+ new_url = p_new(pool, struct http_url, 1);
+ http_url_copy_with_userinfo(pool, new_url, src);
+
+ return new_url;
+}
+
+/*
+ * HTTP URL construction
+ */
+
+static void
+http_url_add_scheme(string_t *urlstr, const struct http_url *url)
+{
+ /* scheme */
+ if (!url->have_ssl)
+ uri_append_scheme(urlstr, "http");
+ else
+ uri_append_scheme(urlstr, "https");
+ str_append(urlstr, "//");
+}
+
+static void
+http_url_add_authority(string_t *urlstr, const struct http_url *url)
+{
+ /* host */
+ uri_append_host(urlstr, &url->host);
+ /* port */
+ uri_append_port(urlstr, url->port);
+}
+
+static void
+http_url_add_target(string_t *urlstr, const struct http_url *url)
+{
+ if (url->path == NULL || *url->path == '\0') {
+ /* Older syntax of RFC 2616 requires this slash at all times for
+ an absolute URL. */
+ str_append_c(urlstr, '/');
+ } else {
+ uri_append_path_data(urlstr, "", url->path);
+ }
+
+ /* query (pre-encoded) */
+ if (url->enc_query != NULL) {
+ str_append_c(urlstr, '?');
+ str_append(urlstr, url->enc_query);
+ }
+}
+
+const char *http_url_create(const struct http_url *url)
+{
+ string_t *urlstr = t_str_new(512);
+
+ http_url_add_scheme(urlstr, url);
+ http_url_add_authority(urlstr, url);
+ http_url_add_target(urlstr, url);
+
+ /* fragment */
+ if (url->enc_fragment != NULL) {
+ str_append_c(urlstr, '#');
+ str_append(urlstr, url->enc_fragment);
+ }
+
+ return str_c(urlstr);
+}
+
+const char *http_url_create_host(const struct http_url *url)
+{
+ string_t *urlstr = t_str_new(512);
+
+ http_url_add_scheme(urlstr, url);
+ http_url_add_authority(urlstr, url);
+
+ return str_c(urlstr);
+}
+
+const char *http_url_create_authority(const struct http_url *url)
+{
+ string_t *urlstr = t_str_new(256);
+
+ http_url_add_authority(urlstr, url);
+
+ return str_c(urlstr);
+}
+
+const char *http_url_create_target(const struct http_url *url)
+{
+ string_t *urlstr = t_str_new(256);
+
+ http_url_add_target(urlstr, url);
+
+ return str_c(urlstr);
+}
+
+void http_url_escape_path(string_t *out, const char *data)
+{
+ uri_append_query_data(out, "&;?=+", data);
+}
+
+void http_url_escape_param(string_t *out, const char *data)
+{
+ uri_append_query_data(out, "&;/?=+", data);
+}
diff --git a/src/lib-http/http-url.h b/src/lib-http/http-url.h
new file mode 100644
index 0000000..62d8922
--- /dev/null
+++ b/src/lib-http/http-url.h
@@ -0,0 +1,108 @@
+#ifndef HTTP_URL_H
+#define HTTP_URL_H
+
+#include "net.h"
+#include "uri-util.h"
+
+#include "http-common.h"
+
+struct http_request_target;
+
+struct http_url {
+ /* server */
+ struct uri_host host;
+ in_port_t port;
+
+ /* userinfo (not parsed by default) */
+ const char *user;
+ const char *password;
+
+ /* path */
+ const char *path;
+
+ /* ?query (still encoded) */
+ const char *enc_query;
+
+ /* #fragment (still encoded) */
+ const char *enc_fragment;
+
+ bool have_ssl:1;
+};
+
+/*
+ * HTTP URL parsing
+ */
+
+enum http_url_parse_flags {
+ /* Scheme part 'http:' is already parsed externally. This implies that
+ this is an absolute HTTP URL. */
+ HTTP_URL_PARSE_SCHEME_EXTERNAL = 0x01,
+ /* Allow '#fragment' part in HTTP URL */
+ HTTP_URL_ALLOW_FRAGMENT_PART = 0x02,
+ /* Allow 'user:password@' part in HTTP URL */
+ HTTP_URL_ALLOW_USERINFO_PART = 0x04,
+ /* Allow URL to contain %00 */
+ HTTP_URL_ALLOW_PCT_NUL = 0x08,
+};
+
+int http_url_parse(const char *url, struct http_url *base,
+ enum http_url_parse_flags flags, pool_t pool,
+ struct http_url **url_r, const char **error_r);
+
+int http_url_request_target_parse(const char *request_target,
+ const char *host_header,
+ const struct http_url *default_base,
+ pool_t pool,
+ struct http_request_target *target,
+ const char **error_r) ATTR_NULL(3);
+
+/*
+ * HTTP URL evaluation
+ */
+
+static inline in_port_t
+http_url_get_port_default(const struct http_url *url, in_port_t default_port)
+{
+ return (url->port != 0 ? url->port : default_port);
+}
+
+static inline in_port_t http_url_get_port(const struct http_url *url)
+{
+ return http_url_get_port_default(
+ url, (url->have_ssl ? HTTPS_DEFAULT_PORT : HTTP_DEFAULT_PORT));
+}
+
+/*
+ * HTTP URL manipulation
+ */
+
+void http_url_init_authority_from(struct http_url *dest,
+ const struct http_url *src);
+void http_url_copy_authority(pool_t pool, struct http_url *dest,
+ const struct http_url *src);
+struct http_url *
+http_url_clone_authority(pool_t pool, const struct http_url *src);
+
+void http_url_copy(pool_t pool, struct http_url *dest,
+ const struct http_url *src);
+void http_url_copy_with_userinfo(pool_t pool, struct http_url *dest,
+ const struct http_url *src);
+
+struct http_url *http_url_clone(pool_t pool,const struct http_url *src);
+struct http_url *
+http_url_clone_with_userinfo(pool_t pool, const struct http_url *src);
+
+/*
+ * HTTP URL construction
+ */
+
+const char *http_url_create(const struct http_url *url);
+
+const char *http_url_create_host(const struct http_url *url);
+const char *http_url_create_authority(const struct http_url *url);
+const char *http_url_create_target(const struct http_url *url);
+
+void http_url_escape_path(string_t *out, const char *data);
+void http_url_escape_param(string_t *out, const char *data);
+
+#endif
diff --git a/src/lib-http/test-http-auth.c b/src/lib-http/test-http-auth.c
new file mode 100644
index 0000000..49ac7ee
--- /dev/null
+++ b/src/lib-http/test-http-auth.c
@@ -0,0 +1,274 @@
+/* Copyright (c) 2013-2018 Dovecot authors, see the included COPYING file */
+
+#include "test-lib.h"
+#include "test-common.h"
+#include "array.h"
+#include "str-sanitize.h"
+#include "http-auth.h"
+
+struct http_auth_challenge_test {
+ const char *scheme;
+ const char *data;
+ struct http_auth_param *params;
+};
+
+struct http_auth_challenges_test {
+ const char *challenges_in;
+
+ struct http_auth_challenge_test *challenges;
+};
+
+/* Valid auth challenges tests */
+static const struct http_auth_challenges_test
+valid_auth_challenges_tests[] = {
+ {
+ .challenges_in = "Basic realm=\"WallyWorld\"",
+ .challenges = (struct http_auth_challenge_test []) {
+ { .scheme = "Basic",
+ .data = NULL,
+ .params = (struct http_auth_param []) {
+ { "realm", "WallyWorld" }, { NULL, NULL }
+ }
+ },{
+ .scheme = NULL
+ }
+ }
+ },{
+ .challenges_in = "Digest "
+ "realm=\"testrealm@host.com\", "
+ "qop=\"auth,auth-int\", "
+ "nonce=\"dcd98b7102dd2f0e8b11d0f600bfb0c093\", "
+ "opaque=\"5ccc069c403ebaf9f0171e9517f40e41\"",
+ .challenges = (struct http_auth_challenge_test []) {
+ { .scheme = "Digest",
+ .data = NULL,
+ .params = (struct http_auth_param []) {
+ { "realm", "testrealm@host.com" },
+ { "qop", "auth,auth-int" },
+ { "nonce", "dcd98b7102dd2f0e8b11d0f600bfb0c093" },
+ { "opaque", "5ccc069c403ebaf9f0171e9517f40e41" },
+ { NULL, NULL }
+ }
+ },{
+ .scheme = NULL
+ }
+ }
+ },{
+ .challenges_in = "Newauth realm=\"apps\", type=1, "
+ "title=\"Login to \\\"apps\\\"\", Basic realm=\"simple\"",
+ .challenges = (struct http_auth_challenge_test []) {
+ { .scheme = "Newauth",
+ .data = NULL,
+ .params = (struct http_auth_param []) {
+ { "realm", "apps" },
+ { "type", "1" },
+ { "title", "Login to \"apps\"" },
+ { NULL, NULL }
+ }
+ },{
+ .scheme = "Basic",
+ .data = NULL,
+ .params = (struct http_auth_param []) {
+ { "realm", "simple" },
+ { NULL, NULL }
+ }
+ },{
+ .scheme = NULL
+ }
+ }
+ }
+};
+
+static const unsigned int valid_auth_challenges_test_count =
+ N_ELEMENTS(valid_auth_challenges_tests);
+
+static void test_http_auth_challenges_valid(void)
+{
+ unsigned int i;
+
+ for (i = 0; i < valid_auth_challenges_test_count; i++) T_BEGIN {
+ const char *challenges_in;
+ ARRAY_TYPE(http_auth_challenge) out;
+ const struct http_auth_challenges_test *test;
+ bool result;
+
+ test = &valid_auth_challenges_tests[i];
+ challenges_in = test->challenges_in;
+
+ test_begin(t_strdup_printf("http auth challenges valid [%d]", i));
+
+ i_zero(&out);
+ result = (http_auth_parse_challenges
+ ((const unsigned char *)challenges_in, strlen(challenges_in),
+ &out) > 0);
+ test_out(t_strdup_printf("parse `%s'", challenges_in), result);
+ if (result) {
+ const struct http_auth_challenge *chalo;
+ const struct http_auth_challenge_test *chalt;
+ unsigned int index;
+
+ index = 0;
+ chalt = test->challenges;
+ array_foreach(&out, chalo) {
+ const struct http_auth_param *paramo, *paramt;
+ unsigned int pindex;
+
+ if (chalt != NULL && chalt->scheme != NULL) {
+ i_assert(chalo->scheme != NULL);
+ test_out(t_strdup_printf("[%d]->scheme = %s",
+ index, str_sanitize(chalo->scheme, 80)),
+ strcmp(chalo->scheme, chalt->scheme) == 0);
+ if (chalo->data == NULL || chalt->data == NULL) {
+ test_out(t_strdup_printf("[%d]->data = %s",
+ index, str_sanitize(chalo->data, 80)),
+ chalo->data == chalt->data);
+ } else {
+ test_out(t_strdup_printf("[%d]->data = %s",
+ index, str_sanitize(chalo->data, 80)),
+ strcmp(chalo->data, chalt->data) == 0);
+ }
+ paramt = chalt->params;
+ pindex = 0;
+ array_foreach(&chalo->params, paramo) {
+ if (paramt->name == NULL) {
+ test_out(t_strdup_printf("[%d]->params[%d]: %s = %s",
+ index, pindex, str_sanitize(paramo->name, 80),
+ str_sanitize(paramo->value, 80)), FALSE);
+ break;
+ } else {
+ test_out(t_strdup_printf("[%d]->params[%d]: %s = %s",
+ index, pindex, str_sanitize(paramo->name, 80),
+ str_sanitize(paramo->value, 80)),
+ strcmp(paramo->name, paramt->name) == 0 &&
+ strcmp(paramo->value, paramt->value) == 0);
+ paramt++;
+ }
+ pindex++;
+ }
+ chalt++;
+ }
+ index++;
+ }
+ }
+
+ test_end();
+ } T_END;
+}
+
+struct http_auth_credentials_test {
+ const char *credentials_in;
+
+ const char *scheme;
+ const char *data;
+ struct http_auth_param *params;
+};
+
+/* Valid auth credentials tests */
+static const struct http_auth_credentials_test
+valid_auth_credentials_tests[] = {
+ {
+ .credentials_in = "Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==",
+ .scheme = "Basic",
+ .data = "QWxhZGRpbjpvcGVuIHNlc2FtZQ==",
+ .params = NULL
+ },{
+ .credentials_in = "Digest username=\"Mufasa\", "
+ "realm=\"testrealm@host.com\", "
+ "nonce=\"dcd98b7102dd2f0e8b11d0f600bfb0c093\", "
+ "uri=\"/dir/index.html\", "
+ "qop=auth, "
+ "nc=00000001, "
+ "cnonce=\"0a4f113b\", "
+ "response=\"6629fae49393a05397450978507c4ef1\", "
+ "opaque=\"5ccc069c403ebaf9f0171e9517f40e41\"",
+ .scheme = "Digest",
+ .data = NULL,
+ .params = (struct http_auth_param []) {
+ { "username", "Mufasa" },
+ { "realm", "testrealm@host.com" },
+ { "nonce", "dcd98b7102dd2f0e8b11d0f600bfb0c093" },
+ { "uri", "/dir/index.html" },
+ { "qop", "auth" },
+ { "nc", "00000001" },
+ { "cnonce", "0a4f113b" },
+ { "response", "6629fae49393a05397450978507c4ef1" },
+ { "opaque", "5ccc069c403ebaf9f0171e9517f40e41" },
+ { NULL, NULL }
+ }
+ }
+};
+
+static const unsigned int valid_auth_credentials_test_count =
+ N_ELEMENTS(valid_auth_credentials_tests);
+
+static void test_http_auth_credentials_valid(void)
+{
+ unsigned int i;
+
+ for (i = 0; i < valid_auth_credentials_test_count; i++) T_BEGIN {
+ const char *credentials_in;
+ struct http_auth_credentials out;
+ const struct http_auth_credentials_test *test;
+ bool result;
+
+ test = &valid_auth_credentials_tests[i];
+ credentials_in = test->credentials_in;
+
+ test_begin(t_strdup_printf("http auth credentials valid [%d]", i));
+
+ result = (http_auth_parse_credentials
+ ((const unsigned char *)credentials_in, strlen(credentials_in),
+ &out) > 0);
+ test_out(t_strdup_printf("parse `%s'", credentials_in), result);
+ if (result) {
+ const struct http_auth_param *paramo, *paramt;
+ unsigned int index;
+
+ i_assert(out.scheme != NULL);
+ test_out(t_strdup_printf("->scheme = %s",
+ str_sanitize(out.scheme, 80)),
+ strcmp(out.scheme, test->scheme) == 0);
+ if (out.data == NULL || test->data == NULL) {
+ test_out(t_strdup_printf("->data = %s",
+ str_sanitize(out.data, 80)),
+ out.data == test->data);
+ } else {
+ test_out(t_strdup_printf("->data = %s",
+ str_sanitize(out.data, 80)),
+ strcmp(out.data, test->data) == 0);
+ }
+ paramt = test->params;
+ index = 0;
+ if (array_is_created(&out.params)) {
+ array_foreach(&out.params, paramo) {
+ if (paramt == NULL || paramt->name == NULL) {
+ test_out(t_strdup_printf("->params[%d]: %s = %s",
+ index++, str_sanitize(paramo->name, 80),
+ str_sanitize(paramo->value, 80)), FALSE);
+ break;
+ } else {
+ test_out(t_strdup_printf("->params[%d]: %s = %s",
+ index++, str_sanitize(paramo->name, 80),
+ str_sanitize(paramo->value, 80)),
+ strcmp(paramo->name, paramt->name) == 0 &&
+ strcmp(paramo->value, paramt->value) == 0);
+ paramt++;
+ }
+ }
+ }
+ }
+
+ test_end();
+ } T_END;
+}
+
+
+int main(void)
+{
+ static void (*const test_functions[])(void) = {
+ test_http_auth_challenges_valid,
+ test_http_auth_credentials_valid,
+ NULL
+ };
+ return test_run(test_functions);
+}
diff --git a/src/lib-http/test-http-client-errors.c b/src/lib-http/test-http-client-errors.c
new file mode 100644
index 0000000..e127041
--- /dev/null
+++ b/src/lib-http/test-http-client-errors.c
@@ -0,0 +1,3944 @@
+/* Copyright (c) 2016-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "str.h"
+#include "str-sanitize.h"
+#include "hostpid.h"
+#include "ioloop.h"
+#include "istream.h"
+#include "istream-chain.h"
+#include "ostream.h"
+#include "time-util.h"
+#include "sleep.h"
+#include "connection.h"
+#include "test-common.h"
+#include "test-subprocess.h"
+#include "http-url.h"
+#include "http-request.h"
+#include "http-client.h"
+
+#include <unistd.h>
+#include <sys/signal.h>
+
+#define CLIENT_PROGRESS_TIMEOUT 10
+#define SERVER_KILL_TIMEOUT_SECS 20
+
+static void main_deinit(void);
+
+/*
+ * Types
+ */
+
+struct server_connection {
+ struct connection conn;
+ void *context;
+
+ pool_t pool;
+ bool version_sent:1;
+};
+
+typedef void (*test_server_init_t)(unsigned int index);
+typedef bool
+(*test_client_init_t)(const struct http_client_settings *client_set);
+typedef void (*test_dns_init_t)(void);
+
+/*
+ * State
+ */
+
+/* common */
+static struct ip_addr bind_ip;
+static in_port_t *bind_ports = 0;
+static struct ioloop *ioloop;
+static bool debug = FALSE;
+
+/* server */
+static struct io *io_listen;
+static int fd_listen = -1;
+static struct connection_list *server_conn_list;
+static size_t server_read_max = 0;
+static unsigned int server_index;
+static int (*test_server_init)(struct server_connection *conn);
+static void (*test_server_deinit)(struct server_connection *conn);
+static void (*test_server_input)(struct server_connection *conn);
+
+/* client */
+static struct timeout *to_client_progress = NULL;
+static struct http_client *http_client = NULL;
+
+/*
+ * Forward declarations
+ */
+
+/* server */
+static void test_server_run(unsigned int index);
+static void server_connection_deinit(struct server_connection **_conn);
+
+/* client */
+static void test_client_defaults(struct http_client_settings *http_set);
+static void test_client_deinit(void);
+
+/* test*/
+static void
+test_run_client_server(const struct http_client_settings *client_set,
+ test_client_init_t client_test,
+ test_server_init_t server_test,
+ unsigned int server_tests_count,
+ test_dns_init_t dns_test) ATTR_NULL(3);
+
+/*
+ * Utility
+ */
+
+static void
+test_client_assert_response(const struct http_response *resp,
+ bool condition)
+{
+ const char *reason = (resp->reason != NULL ? resp->reason : "<NULL>");
+
+ test_assert(resp->reason != NULL && *resp->reason != '\0');
+
+ if (!condition)
+ i_error("BAD RESPONSE: %u %s", resp->status, reason);
+ else if (debug)
+ i_debug("RESPONSE: %u %s", resp->status, resp->reason);
+}
+
+/*
+ * Unconfigured SSL
+ */
+
+/* client */
+
+struct _unconfigured_ssl {
+ unsigned int count;
+};
+
+static void
+test_client_unconfigured_ssl_response(const struct http_response *resp,
+ struct _unconfigured_ssl *ctx)
+{
+ test_client_assert_response(
+ resp, resp->status == HTTP_CLIENT_REQUEST_ERROR_CONNECT_FAILED);
+
+ if (--ctx->count == 0) {
+ i_free(ctx);
+ io_loop_stop(ioloop);
+ }
+}
+
+static bool
+test_client_unconfigured_ssl(const struct http_client_settings *client_set)
+{
+ struct http_client_request *hreq;
+ struct _unconfigured_ssl *ctx;
+
+ ctx = i_new(struct _unconfigured_ssl, 1);
+ ctx->count = 2;
+
+ http_client = http_client_init(client_set);
+
+ hreq = http_client_request(
+ http_client, "GET", "127.0.0.1", "/unconfigured-ssl.txt",
+ test_client_unconfigured_ssl_response, ctx);
+ http_client_request_set_ssl(hreq, TRUE);
+ http_client_request_submit(hreq);
+
+ hreq = http_client_request(
+ http_client, "GET", "127.0.0.1", "/unconfigured-ssl2.txt",
+ test_client_unconfigured_ssl_response, ctx);
+ http_client_request_set_ssl(hreq, TRUE);
+ http_client_request_submit(hreq);
+
+ return TRUE;
+}
+
+/* test */
+
+static void test_unconfigured_ssl(void)
+{
+ struct http_client_settings http_client_set;
+
+ test_client_defaults(&http_client_set);
+
+ test_begin("unconfigured ssl");
+ test_run_client_server(&http_client_set,
+ test_client_unconfigured_ssl, NULL, 0, NULL);
+ test_end();
+}
+
+/*
+ * Unconfigured SSL abort
+ */
+
+/* client */
+
+struct _unconfigured_ssl_abort {
+ unsigned int count;
+};
+
+static void
+test_client_unconfigured_ssl_abort_response1(
+ const struct http_response *resp,
+ struct _unconfigured_ssl_abort *ctx ATTR_UNUSED)
+{
+ if (debug)
+ i_debug("RESPONSE: %u %s", resp->status, resp->reason);
+
+ test_out_quiet("inappropriate callback", FALSE);
+}
+
+static void
+test_client_unconfigured_ssl_abort_response2(
+ const struct http_response *resp, struct _unconfigured_ssl_abort *ctx)
+{
+ test_client_assert_response(
+ resp, resp->status == HTTP_CLIENT_REQUEST_ERROR_CONNECT_FAILED);
+
+ i_free(ctx);
+ io_loop_stop(ioloop);
+}
+
+static bool
+test_client_unconfigured_ssl_abort(
+ const struct http_client_settings *client_set)
+{
+ struct http_client_request *hreq;
+ struct _unconfigured_ssl_abort *ctx;
+
+ ctx = i_new(struct _unconfigured_ssl_abort, 1);
+ ctx->count = 1;
+
+ http_client = http_client_init(client_set);
+
+ hreq = http_client_request(
+ http_client, "GET", "127.0.0.1", "/unconfigured-ssl.txt",
+ test_client_unconfigured_ssl_abort_response1, ctx);
+ http_client_request_set_ssl(hreq, TRUE);
+ http_client_request_submit(hreq);
+ http_client_request_abort(&hreq);
+
+ hreq = http_client_request(
+ http_client, "GET", "127.0.0.1", "/unconfigured-ssl2.txt",
+ test_client_unconfigured_ssl_abort_response2, ctx);
+ http_client_request_set_ssl(hreq, TRUE);
+ http_client_request_submit(hreq);
+
+ return TRUE;
+}
+
+/* test */
+
+static void test_unconfigured_ssl_abort(void)
+{
+ struct http_client_settings http_client_set;
+
+ test_client_defaults(&http_client_set);
+
+ test_begin("unconfigured ssl abort");
+ test_run_client_server(&http_client_set,
+ test_client_unconfigured_ssl_abort,
+ NULL, 0, NULL);
+ test_end();
+}
+
+/*
+ * Invalid URL
+ */
+
+/* client */
+
+struct _invalid_url {
+ unsigned int count;
+};
+
+static void
+test_client_invalid_url_response(const struct http_response *resp,
+ struct _invalid_url *ctx)
+{
+ test_client_assert_response(
+ resp, resp->status == HTTP_CLIENT_REQUEST_ERROR_INVALID_URL);
+
+ if (--ctx->count == 0) {
+ i_free(ctx);
+ io_loop_stop(ioloop);
+ }
+}
+
+static bool
+test_client_invalid_url(const struct http_client_settings *client_set)
+{
+ struct http_client_request *hreq;
+ struct _invalid_url *ctx;
+
+ ctx = i_new(struct _invalid_url, 1);
+ ctx->count = 2;
+
+ http_client = http_client_init(client_set);
+
+ hreq = http_client_request_url_str(
+ http_client, "GET", "imap://example.com/INBOX",
+ test_client_invalid_url_response, ctx);
+ http_client_request_submit(hreq);
+
+ hreq = http_client_request_url_str(
+ http_client, "GET", "http:/www.example.com",
+ test_client_invalid_url_response, ctx);
+ http_client_request_submit(hreq);
+
+ return TRUE;
+}
+
+/* test */
+
+static void test_invalid_url(void)
+{
+ struct http_client_settings http_client_set;
+
+ test_client_defaults(&http_client_set);
+
+ test_begin("invalid url");
+ test_run_client_server(&http_client_set,
+ test_client_invalid_url, NULL, 0, NULL);
+ test_end();
+}
+
+/*
+ * Host lookup failed
+ */
+
+/* client */
+
+struct _host_lookup_failed {
+ unsigned int count;
+};
+
+static void
+test_client_host_lookup_failed_response(const struct http_response *resp,
+ struct _host_lookup_failed *ctx)
+{
+ test_client_assert_response(
+ resp,
+ resp->status == HTTP_CLIENT_REQUEST_ERROR_HOST_LOOKUP_FAILED);
+
+ if (--ctx->count == 0) {
+ i_free(ctx);
+ io_loop_stop(ioloop);
+ }
+}
+
+static bool
+test_client_host_lookup_failed(const struct http_client_settings *client_set)
+{
+ struct http_client_request *hreq;
+ struct _host_lookup_failed *ctx;
+
+ ctx = i_new(struct _host_lookup_failed, 1);
+ ctx->count = 2;
+
+ http_client = http_client_init(client_set);
+
+ hreq = http_client_request(
+ http_client, "GET", "host.in-addr.arpa",
+ "/host-lookup-failed.txt",
+ test_client_host_lookup_failed_response, ctx);
+ http_client_request_submit(hreq);
+
+ hreq = http_client_request(
+ http_client, "GET", "host.in-addr.arpa",
+ "/host-lookup-failed2.txt",
+ test_client_host_lookup_failed_response, ctx);
+ http_client_request_submit(hreq);
+
+ return TRUE;
+}
+
+/* test */
+
+static void test_host_lookup_failed(void)
+{
+ struct http_client_settings http_client_set;
+
+ test_client_defaults(&http_client_set);
+
+ test_begin("host lookup failed");
+ test_run_client_server(&http_client_set,
+ test_client_host_lookup_failed, NULL, 0, NULL);
+ test_end();
+}
+
+/*
+ * Connection refused
+ */
+
+/* server */
+
+static void
+test_server_connection_refused(unsigned int index ATTR_UNUSED)
+{
+ i_close_fd(&fd_listen);
+
+ test_subprocess_notify_signal_send_parent(SIGUSR1);
+}
+
+/* client */
+
+struct _connection_refused {
+ unsigned int count;
+ struct timeout *to;
+};
+
+static void
+test_client_connection_refused_response(const struct http_response *resp,
+ struct _connection_refused *ctx)
+{
+ test_assert(ctx->to == NULL);
+ timeout_remove(&ctx->to);
+
+ test_client_assert_response(
+ resp, resp->status == HTTP_CLIENT_REQUEST_ERROR_CONNECT_FAILED);
+
+ if (--ctx->count == 0) {
+ i_free(ctx);
+ io_loop_stop(ioloop);
+ }
+}
+
+static void
+test_client_connection_refused_timeout(struct _connection_refused *ctx)
+{
+ if (debug)
+ i_debug("TIMEOUT (ok)");
+ timeout_remove(&ctx->to);
+}
+
+static bool
+test_client_connection_refused(const struct http_client_settings *client_set)
+{
+ struct http_client_request *hreq;
+ struct _connection_refused *ctx;
+
+ /* wait for the server side to close the socket */
+ test_subprocess_notify_signal_wait(SIGUSR1, 10000);
+
+ ctx = i_new(struct _connection_refused, 1);
+ ctx->count = 2;
+
+ if (client_set->max_connect_attempts > 0) {
+ ctx->to = timeout_add_short(250,
+ test_client_connection_refused_timeout, ctx);
+ }
+
+ http_client = http_client_init(client_set);
+
+ hreq = http_client_request(
+ http_client, "GET", net_ip2addr(&bind_ip),
+ "/connection-refused.txt",
+ test_client_connection_refused_response, ctx);
+ http_client_request_set_port(hreq, bind_ports[0]);
+ http_client_request_submit(hreq);
+
+ hreq = http_client_request(
+ http_client, "GET", net_ip2addr(&bind_ip),
+ "/connection-refused2.txt",
+ test_client_connection_refused_response, ctx);
+ http_client_request_set_port(hreq, bind_ports[0]);
+ http_client_request_submit(hreq);
+
+ return TRUE;
+}
+
+/* test */
+
+static void test_connection_refused(void)
+{
+ struct http_client_settings http_client_set;
+
+ test_client_defaults(&http_client_set);
+
+ test_begin("connection refused");
+ test_subprocess_notify_signal_reset(SIGUSR1);
+ test_run_client_server(&http_client_set,
+ test_client_connection_refused,
+ test_server_connection_refused, 1, NULL);
+ test_end();
+
+ http_client_set.max_connect_attempts = 3;
+
+ test_begin("connection refused backoff");
+ test_subprocess_notify_signal_reset(SIGUSR1);
+ test_run_client_server(&http_client_set,
+ test_client_connection_refused,
+ test_server_connection_refused, 1, NULL);
+ test_end();
+}
+
+/*
+ * Connection lost prematurely
+ */
+
+/* server */
+
+static void
+test_server_connection_lost_prematurely_input(struct server_connection *conn)
+{
+ server_connection_deinit(&conn);
+}
+
+static void test_server_connection_lost_prematurely(unsigned int index)
+{
+ test_server_input = test_server_connection_lost_prematurely_input;
+ test_server_run(index);
+}
+
+/* client */
+
+struct _connection_lost_prematurely {
+ unsigned int count;
+ struct timeout *to;
+};
+
+static void
+test_client_connection_lost_prematurely_response(
+ const struct http_response *resp,
+ struct _connection_lost_prematurely *ctx)
+{
+ test_assert(ctx->to == NULL);
+ timeout_remove(&ctx->to);
+
+ test_client_assert_response(
+ resp,
+ resp->status == HTTP_CLIENT_REQUEST_ERROR_CONNECTION_LOST);
+
+ if (--ctx->count == 0) {
+ i_free(ctx);
+ io_loop_stop(ioloop);
+ }
+}
+
+static void
+test_client_connection_lost_prematurely_timeout(
+ struct _connection_lost_prematurely *ctx)
+{
+ if (debug)
+ i_debug("TIMEOUT (ok)");
+ timeout_remove(&ctx->to);
+}
+
+static bool
+test_client_connection_lost_prematurely(
+ const struct http_client_settings *client_set)
+{
+ struct http_client_request *hreq;
+ struct _connection_lost_prematurely *ctx;
+
+ ctx = i_new(struct _connection_lost_prematurely, 1);
+ ctx->count = 2;
+
+ ctx->to = timeout_add_short(
+ 250, test_client_connection_lost_prematurely_timeout, ctx);
+
+ http_client = http_client_init(client_set);
+
+ hreq = http_client_request(
+ http_client, "GET", net_ip2addr(&bind_ip),
+ "/connection-refused-retry.txt",
+ test_client_connection_lost_prematurely_response, ctx);
+ http_client_request_set_port(hreq, bind_ports[0]);
+ http_client_request_submit(hreq);
+
+ hreq = http_client_request(
+ http_client, "GET", net_ip2addr(&bind_ip),
+ "/connection-refused-retry2.txt",
+ test_client_connection_lost_prematurely_response, ctx);
+ http_client_request_set_port(hreq, bind_ports[0]);
+ http_client_request_submit(hreq);
+
+ return TRUE;
+}
+
+/* test */
+
+static void test_connection_lost_prematurely(void)
+{
+ struct http_client_settings http_client_set;
+
+ test_client_defaults(&http_client_set);
+ http_client_set.max_connect_attempts = 3;
+ http_client_set.max_attempts = 3;
+
+ test_begin("connection lost prematurely");
+ test_run_client_server(&http_client_set,
+ test_client_connection_lost_prematurely,
+ test_server_connection_lost_prematurely, 1,
+ NULL);
+ test_end();
+}
+
+/*
+ * Connection timed out
+ */
+
+/* client */
+
+struct _connection_timed_out {
+ unsigned int count;
+};
+
+static void
+test_client_connection_timed_out_response(const struct http_response *resp,
+ struct _connection_timed_out *ctx)
+{
+ test_client_assert_response(
+ resp, resp->status == HTTP_CLIENT_REQUEST_ERROR_CONNECT_FAILED);
+
+ if (--ctx->count == 0) {
+ i_free(ctx);
+ io_loop_stop(ioloop);
+ }
+}
+
+static bool
+test_client_connection_timed_out(const struct http_client_settings *client_set)
+{
+ struct http_client_request *hreq;
+ struct _connection_timed_out *ctx;
+
+ ctx = i_new(struct _connection_timed_out, 1);
+ ctx->count = 2;
+
+ http_client = http_client_init(client_set);
+
+ hreq = http_client_request(
+ http_client, "GET", "192.168.0.0", "/connection-timed-out.txt",
+ test_client_connection_timed_out_response, ctx);
+ http_client_request_submit(hreq);
+
+ hreq = http_client_request(
+ http_client, "GET", "192.168.0.0", "/connection-timed-out2.txt",
+ test_client_connection_timed_out_response, ctx);
+ http_client_request_submit(hreq);
+
+ return TRUE;
+}
+
+/* test */
+
+static void test_connection_timed_out(void)
+{
+ struct http_client_settings http_client_set;
+
+ test_client_defaults(&http_client_set);
+ http_client_set.connect_timeout_msecs = 1000;
+ http_client_set.max_attempts = 1;
+
+ test_begin("connection timed out");
+ test_run_client_server(&http_client_set,
+ test_client_connection_timed_out, NULL, 0, NULL);
+ test_end();
+}
+
+/*
+ * Invalid redirect
+ */
+
+/* server */
+
+/* -> not accepted */
+
+static void test_invalid_redirect_input1(struct server_connection *conn)
+{
+ static const char *resp =
+ "HTTP/1.1 302 Redirect\r\n"
+ "Location: http://localhost:4444\r\n"
+ "\r\n";
+
+ o_stream_nsend_str(conn->conn.output, resp);
+ server_connection_deinit(&conn);
+}
+
+static void test_server_invalid_redirect1(unsigned int index)
+{
+ test_server_input = test_invalid_redirect_input1;
+ test_server_run(index);
+}
+
+/* -> bad location */
+
+static void test_invalid_redirect_input2(struct server_connection *conn)
+{
+ static const char *resp =
+ "HTTP/1.1 302 Redirect\r\n"
+ "Location: unix:/var/run/dovecot/auth-master\r\n"
+ "\r\n";
+
+ o_stream_nsend_str(conn->conn.output, resp);
+ server_connection_deinit(&conn);
+}
+
+static void test_server_invalid_redirect2(unsigned int index)
+{
+ test_server_input = test_invalid_redirect_input2;
+ test_server_run(index);
+}
+
+/* -> too many */
+
+static void test_invalid_redirect_input3(struct server_connection *conn)
+{
+ string_t *resp;
+
+ resp = t_str_new(512);
+ str_printfa(resp,
+ "HTTP/1.1 302 Redirect\r\n"
+ "Location: http://%s:%u/friep.txt\r\n"
+ "\r\n",
+ net_ip2addr(&bind_ip), bind_ports[server_index+1]);
+ o_stream_nsend(conn->conn.output, str_data(resp), str_len(resp));
+ server_connection_deinit(&conn);
+}
+
+static void test_server_invalid_redirect3(unsigned int index)
+{
+ test_server_input = test_invalid_redirect_input3;
+ test_server_run(index);
+}
+
+/* client */
+
+static void
+test_client_invalid_redirect_response(const struct http_response *resp,
+ void *context ATTR_UNUSED)
+{
+ test_client_assert_response(
+ resp,
+ resp->status == HTTP_CLIENT_REQUEST_ERROR_INVALID_REDIRECT);
+
+ io_loop_stop(ioloop);
+}
+
+static bool
+test_client_invalid_redirect(const struct http_client_settings *client_set)
+{
+ struct http_client_request *hreq;
+
+ http_client = http_client_init(client_set);
+
+ hreq = http_client_request(
+ http_client, "GET", net_ip2addr(&bind_ip),
+ "/invalid-redirect.txt",
+ test_client_invalid_redirect_response, NULL);
+ http_client_request_set_port(hreq, bind_ports[0]);
+ http_client_request_submit(hreq);
+
+ return TRUE;
+}
+
+/* test */
+
+static void test_invalid_redirect(void)
+{
+ struct http_client_settings http_client_set;
+
+ test_client_defaults(&http_client_set);
+
+ test_begin("invalid redirect: not accepted");
+ http_client_set.max_redirects = 0;
+ test_run_client_server(&http_client_set,
+ test_client_invalid_redirect,
+ test_server_invalid_redirect1, 1, NULL);
+ test_end();
+
+ test_begin("invalid redirect: bad location");
+ http_client_set.max_redirects = 1;
+ test_run_client_server(&http_client_set,
+ test_client_invalid_redirect,
+ test_server_invalid_redirect2, 1, NULL);
+ test_end();
+
+ test_begin("invalid redirect: too many");
+ http_client_set.max_redirects = 1;
+ test_run_client_server(&http_client_set,
+ test_client_invalid_redirect,
+ test_server_invalid_redirect3, 3, NULL);
+ test_end();
+}
+
+/*
+ * Unseekable redirect
+ */
+
+/* server */
+
+static void test_unseekable_redirect_input(struct server_connection *conn)
+{
+ string_t *resp;
+
+ resp = t_str_new(512);
+ str_printfa(resp,
+ "HTTP/1.1 302 Redirect\r\n"
+ "Location: http://%s:%u/frml.txt\r\n"
+ "\r\n",
+ net_ip2addr(&bind_ip), bind_ports[server_index+1]);
+ o_stream_nsend(conn->conn.output, str_data(resp), str_len(resp));
+ server_connection_deinit(&conn);
+}
+
+static void test_server_unseekable_redirect(unsigned int index)
+{
+ test_server_input = test_unseekable_redirect_input;
+ test_server_run(index);
+}
+
+/* client */
+
+static void
+test_client_unseekable_redirect_response(const struct http_response *resp,
+ void *context ATTR_UNUSED)
+{
+ test_client_assert_response(
+ resp, resp->status == HTTP_CLIENT_REQUEST_ERROR_ABORTED);
+
+ io_loop_stop(ioloop);
+}
+
+static bool
+test_client_unseekable_redirect(const struct http_client_settings *client_set)
+{
+ struct http_client_request *hreq;
+ struct istream *input;
+
+ http_client = http_client_init(client_set);
+
+ input = i_stream_create_from_data("FROP", 4);
+ input->seekable = FALSE;
+
+ hreq = http_client_request(
+ http_client, "GET", net_ip2addr(&bind_ip),
+ "/unseekable-redirect.txt",
+ test_client_unseekable_redirect_response, NULL);
+ http_client_request_set_port(hreq, bind_ports[0]);
+ http_client_request_set_payload(hreq, input, FALSE);
+ http_client_request_submit(hreq);
+
+ i_stream_unref(&input);
+ return TRUE;
+}
+
+/* test */
+
+static void test_unseekable_redirect(void)
+{
+ struct http_client_settings http_client_set;
+
+ test_client_defaults(&http_client_set);
+ http_client_set.max_redirects = 1;
+
+ test_begin("unseekable redirect");
+ test_run_client_server(&http_client_set,
+ test_client_unseekable_redirect,
+ test_server_unseekable_redirect, 2, NULL);
+ test_end();
+}
+
+/*
+ * Unseekable retry
+ */
+
+/* server */
+
+static void test_unseekable_retry_input(struct server_connection *conn)
+{
+ server_connection_deinit(&conn);
+}
+
+static void test_server_unseekable_retry(unsigned int index)
+{
+ test_server_input = test_unseekable_retry_input;
+ test_server_run(index);
+}
+
+/* client */
+
+static void
+test_client_unseekable_retry_response(const struct http_response *resp,
+ void *context ATTR_UNUSED)
+{
+ test_client_assert_response(
+ resp, resp->status == HTTP_CLIENT_REQUEST_ERROR_ABORTED);
+
+ io_loop_stop(ioloop);
+}
+
+static bool
+test_client_unseekable_retry(const struct http_client_settings *client_set)
+{
+ struct http_client_request *hreq;
+ struct istream *input;
+
+ http_client = http_client_init(client_set);
+
+ input = i_stream_create_from_data("FROP", 4);
+ input->seekable = FALSE;
+
+ hreq = http_client_request(
+ http_client, "GET", net_ip2addr(&bind_ip),
+ "/unseekable-retry.txt",
+ test_client_unseekable_retry_response, NULL);
+ http_client_request_set_port(hreq, bind_ports[0]);
+ http_client_request_set_payload(hreq, input, FALSE);
+ http_client_request_submit(hreq);
+
+ i_stream_unref(&input);
+ return TRUE;
+}
+
+/* test */
+
+static void test_unseekable_retry(void)
+{
+ struct http_client_settings http_client_set;
+
+ test_client_defaults(&http_client_set);
+ http_client_set.max_attempts = 3;
+
+ test_begin("unseekable retry");
+ test_run_client_server(&http_client_set,
+ test_client_unseekable_retry,
+ test_server_unseekable_retry, 2, NULL);
+ test_end();
+}
+
+/*
+ * Broken payload
+ */
+
+/* server */
+
+static void test_broken_payload_input(struct server_connection *conn)
+{
+ static const char *resp =
+ "HTTP/1.1 200 OK\r\n"
+ "Content-Length: 18\r\n"
+ "\r\n"
+ "Everything is OK\r\n";
+
+ o_stream_nsend_str(conn->conn.output, resp);
+ server_connection_deinit(&conn);
+}
+
+static void test_server_broken_payload(unsigned int index)
+{
+ test_server_input = test_broken_payload_input;
+ test_server_run(index);
+}
+
+/* client */
+
+static void
+test_client_broken_payload_response(const struct http_response *resp,
+ void *context ATTR_UNUSED)
+{
+ test_client_assert_response(
+ resp, resp->status == HTTP_CLIENT_REQUEST_ERROR_BROKEN_PAYLOAD);
+
+ io_loop_stop(ioloop);
+}
+
+static bool
+test_client_broken_payload(const struct http_client_settings *client_set)
+{
+ struct http_client_request *hreq;
+ struct istream *input;
+
+ test_expect_errors(1);
+
+ http_client = http_client_init(client_set);
+
+ input = i_stream_create_error_str(EIO, "Moehahahaha!!");
+ i_stream_set_name(input, "PURE EVIL");
+
+ hreq = http_client_request(
+ http_client, "GET", net_ip2addr(&bind_ip),
+ "/broken-payload.txt",
+ test_client_broken_payload_response, NULL);
+ http_client_request_set_port(hreq, bind_ports[0]);
+ http_client_request_set_payload(hreq, input, FALSE);
+ http_client_request_submit(hreq);
+
+ i_stream_unref(&input);
+ return TRUE;
+}
+
+/* test */
+
+static void test_broken_payload(void)
+{
+ struct http_client_settings http_client_set;
+
+ test_client_defaults(&http_client_set);
+
+ test_begin("broken payload");
+ test_run_client_server(&http_client_set,
+ test_client_broken_payload,
+ test_server_broken_payload, 1, NULL);
+ test_end();
+}
+
+/*
+ * Retry payload
+ */
+
+/* server */
+
+struct _retry_payload_sctx {
+ bool eoh;
+};
+
+static int test_retry_payload_init(struct server_connection *conn)
+{
+ struct _retry_payload_sctx *ctx;
+
+ ctx = p_new(conn->pool, struct _retry_payload_sctx, 1);
+ conn->context = ctx;
+ return 0;
+}
+
+static void test_retry_payload_input(struct server_connection *conn)
+{
+ struct _retry_payload_sctx *ctx = conn->context;
+ const char *line;
+
+ while ((line = i_stream_read_next_line(conn->conn.input)) != NULL) {
+ if (*line == '\0') {
+ ctx->eoh = TRUE;
+ continue;
+ }
+ if (ctx->eoh)
+ break;
+ }
+
+ if (conn->conn.input->stream_errno != 0) {
+ i_fatal("server: Stream error: %s",
+ i_stream_get_error(conn->conn.input));
+ }
+ if (line == NULL) {
+ if (conn->conn.input->eof)
+ i_fatal("server: Client stream ended prematurely");
+ return;
+ }
+
+ i_assert(ctx->eoh);
+
+ if (strcmp(line, "This is the payload we expect.") == 0) {
+ if (debug)
+ i_debug("Expected payload received");
+ o_stream_nsend_str(conn->conn.output,
+ "HTTP/1.1 500 Oh no!\r\n"
+ "Connection: close\r\n"
+ "Content-Length: 17\r\n"
+ "\r\n"
+ "Expected result\r\n");
+ } else {
+ i_error("Unexpected payload received: `%s'",
+ str_sanitize(line, 128));
+ o_stream_nsend_str(conn->conn.output,
+ "HTTP/1.1 501 Oh no!\r\n"
+ "Connection: close\r\n"
+ "Content-Length: 19\r\n"
+ "\r\n"
+ "Unexpected result\r\n");
+ }
+ server_connection_deinit(&conn);
+}
+
+static void test_server_retry_payload(unsigned int index)
+{
+ test_server_init = test_retry_payload_init;
+ test_server_input = test_retry_payload_input;
+ test_server_run(index);
+}
+
+/* client */
+
+struct _retry_payload_ctx {
+ unsigned int count;
+};
+
+struct _retry_payload_request_ctx {
+ struct _retry_payload_ctx *ctx;
+ struct http_client_request *req;
+};
+
+static void
+test_client_retry_payload_response(const struct http_response *resp,
+ struct _retry_payload_request_ctx *rctx)
+{
+ struct _retry_payload_ctx *ctx = rctx->ctx;
+
+ test_client_assert_response(resp, resp->status == 500);
+
+ if (http_client_request_try_retry(rctx->req)) {
+ if (debug)
+ i_debug("retrying");
+ return;
+ }
+ i_free(rctx);
+
+ if (--ctx->count == 0) {
+ i_free(ctx);
+ io_loop_stop(ioloop);
+ }
+}
+
+static bool
+test_client_retry_payload(const struct http_client_settings *client_set)
+{
+ static const char payload[] = "This is the payload we expect.\r\n";
+ struct _retry_payload_ctx *ctx;
+ struct _retry_payload_request_ctx *rctx;
+ struct http_client_request *hreq;
+ struct istream *input;
+
+ ctx = i_new(struct _retry_payload_ctx, 1);
+ ctx->count = 1;
+
+ http_client = http_client_init(client_set);
+
+ input = i_stream_create_from_data(payload, sizeof(payload)-1);
+
+ rctx = i_new(struct _retry_payload_request_ctx, 1);
+ rctx->ctx = ctx;
+
+ rctx->req = hreq = http_client_request(
+ http_client, "GET", net_ip2addr(&bind_ip), "/retry-payload.txt",
+ test_client_retry_payload_response, rctx);
+ http_client_request_set_port(hreq, bind_ports[0]);
+ http_client_request_set_payload(hreq, input, FALSE);
+ http_client_request_submit(hreq);
+
+ i_stream_unref(&input);
+ return TRUE;
+}
+
+/* test */
+
+static void test_retry_payload(void)
+{
+ struct http_client_settings http_client_set;
+
+ test_client_defaults(&http_client_set);
+ http_client_set.max_attempts = 2;
+
+ server_read_max = 0;
+
+ test_begin("retry payload");
+ test_run_client_server(&http_client_set,
+ test_client_retry_payload,
+ test_server_retry_payload, 1, NULL);
+ test_end();
+}
+
+/*
+ * Connection lost
+ */
+
+/* server */
+
+static void test_connection_lost_input(struct server_connection *conn)
+{
+ ssize_t ret;
+
+ if (server_read_max == 0) {
+ server_connection_deinit(&conn);
+ return;
+ }
+
+ i_stream_set_max_buffer_size(conn->conn.input, server_read_max);
+ ret = i_stream_read(conn->conn.input);
+ if (ret == -2) {
+ server_connection_deinit(&conn);
+ return;
+ }
+ if (ret < 0) {
+ i_assert(conn->conn.input->eof);
+ if (conn->conn.input->stream_errno == 0)
+ i_fatal("server: Client stream ended prematurely");
+ else
+ i_fatal("server: Stream error: %s",
+ i_stream_get_error(conn->conn.input));
+ }
+}
+
+static void test_server_connection_lost(unsigned int index)
+{
+ test_server_input = test_connection_lost_input;
+ test_server_run(index);
+}
+
+/* client */
+
+struct _connection_lost_ctx {
+ unsigned int count;
+};
+
+struct _connection_lost_request_ctx {
+ struct _connection_lost_ctx *ctx;
+ struct http_client_request *req;
+};
+
+static void
+test_client_connection_lost_response(const struct http_response *resp,
+ struct _connection_lost_request_ctx *rctx)
+{
+ struct _connection_lost_ctx *ctx = rctx->ctx;
+
+ test_client_assert_response(
+ resp,
+ resp->status == HTTP_CLIENT_REQUEST_ERROR_CONNECTION_LOST);
+
+ if (http_client_request_try_retry(rctx->req)) {
+ if (debug)
+ i_debug("retrying");
+ return;
+ }
+ i_free(rctx);
+
+ if (--ctx->count == 0) {
+ i_free(ctx);
+ io_loop_stop(ioloop);
+ }
+}
+
+static bool
+test_client_connection_lost(const struct http_client_settings *client_set)
+{
+ static const char payload[] =
+ "This is a useless payload that only serves as a means to give "
+ "the server the opportunity to close the connection before the "
+ "payload is finished.";
+ struct _connection_lost_ctx *ctx;
+ struct _connection_lost_request_ctx *rctx;
+ struct http_client_request *hreq;
+ struct istream *input;
+
+ ctx = i_new(struct _connection_lost_ctx, 1);
+ ctx->count = 2;
+
+ http_client = http_client_init(client_set);
+
+ input = i_stream_create_from_data(payload, sizeof(payload)-1);
+
+ rctx = i_new(struct _connection_lost_request_ctx, 1);
+ rctx->ctx = ctx;
+
+ rctx->req = hreq = http_client_request(
+ http_client, "GET", net_ip2addr(&bind_ip),
+ "/connection-lost.txt",
+ test_client_connection_lost_response, rctx);
+ http_client_request_set_port(hreq, bind_ports[0]);
+ http_client_request_set_payload(hreq, input, FALSE);
+ http_client_request_submit(hreq);
+
+ rctx = i_new(struct _connection_lost_request_ctx, 1);
+ rctx->ctx = ctx;
+
+ rctx->req = hreq = http_client_request(
+ http_client, "GET", net_ip2addr(&bind_ip),
+ "/connection-lost2.txt",
+ test_client_connection_lost_response, rctx);
+ http_client_request_set_port(hreq, bind_ports[0]);
+ http_client_request_submit(hreq);
+
+ i_stream_unref(&input);
+ return TRUE;
+}
+
+/* test */
+
+static void test_connection_lost(void)
+{
+ struct http_client_settings http_client_set;
+
+ test_client_defaults(&http_client_set);
+
+ server_read_max = 0;
+
+ test_begin("connection lost: one attempt");
+ http_client_set.max_attempts = 1;
+ test_run_client_server(&http_client_set,
+ test_client_connection_lost,
+ test_server_connection_lost, 1, NULL);
+ test_end();
+
+ test_begin("connection lost: two attempts");
+ http_client_set.max_attempts = 2;
+ test_run_client_server(&http_client_set,
+ test_client_connection_lost,
+ test_server_connection_lost, 1, NULL);
+ test_end();
+
+ test_begin("connection lost: three attempts");
+ http_client_set.max_attempts = 3;
+ test_run_client_server(&http_client_set,
+ test_client_connection_lost,
+ test_server_connection_lost, 1, NULL);
+ test_end();
+
+ test_begin("connection lost: manual retry");
+ http_client_set.max_attempts = 3;
+ http_client_set.no_auto_retry = TRUE;
+ test_run_client_server(&http_client_set,
+ test_client_connection_lost,
+ test_server_connection_lost, 1, NULL);
+ test_end();
+}
+
+/*
+ * Connection lost after 100-continue
+ */
+
+/* server */
+
+static void test_connection_lost_100_input(struct server_connection *conn)
+{
+ static const char *resp =
+ "HTTP/1.1 100 Continue\r\n"
+ "\r\n";
+
+ o_stream_nsend_str(conn->conn.output, resp);
+ server_connection_deinit(&conn);
+}
+
+static void test_server_connection_lost_100(unsigned int index)
+{
+ test_server_input = test_connection_lost_100_input;
+ test_server_run(index);
+}
+
+/* client */
+
+struct _connection_lost_100_ctx {
+ unsigned int count;
+};
+
+static void
+test_client_connection_lost_100_response(const struct http_response *resp,
+ struct _connection_lost_100_ctx *ctx)
+{
+ test_client_assert_response(
+ resp,
+ resp->status == HTTP_CLIENT_REQUEST_ERROR_CONNECTION_LOST);
+
+ if (--ctx->count == 0) {
+ i_free(ctx);
+ io_loop_stop(ioloop);
+ }
+}
+
+static bool
+test_client_connection_lost_100(
+ const struct http_client_settings *client_set)
+{
+ static const char payload[] =
+ "This is a useless payload that only serves as a means to give "
+ "the server the opportunity to close the connection before the "
+ "payload is finished.";
+ struct _connection_lost_100_ctx *ctx;
+ struct http_client_request *hreq;
+ struct istream *input;
+
+ ctx = i_new(struct _connection_lost_100_ctx, 1);
+ ctx->count = 2;
+
+ http_client = http_client_init(client_set);
+
+ input = i_stream_create_from_data(payload, sizeof(payload)-1);
+
+ hreq = http_client_request(
+ http_client, "GET", net_ip2addr(&bind_ip),
+ "/connection-lost.txt",
+ test_client_connection_lost_100_response, ctx);
+ http_client_request_set_port(hreq, bind_ports[0]);
+ http_client_request_set_payload(hreq, input, TRUE);
+ http_client_request_submit(hreq);
+
+ hreq = http_client_request(
+ http_client, "GET", net_ip2addr(&bind_ip),
+ "/connection-lost2.txt",
+ test_client_connection_lost_100_response, ctx);
+ http_client_request_set_port(hreq, bind_ports[0]);
+ http_client_request_set_payload(hreq, input, TRUE);
+ http_client_request_submit(hreq);
+
+ i_stream_unref(&input);
+ return TRUE;
+}
+
+/* test */
+
+static void test_connection_lost_100(void)
+{
+ struct http_client_settings http_client_set;
+
+ test_client_defaults(&http_client_set);
+
+ server_read_max = 0;
+
+ test_begin("connection lost after 100-continue");
+ http_client_set.max_attempts = 1;
+ test_run_client_server(&http_client_set,
+ test_client_connection_lost_100,
+ test_server_connection_lost_100, 1, NULL);
+ test_end();
+}
+
+/*
+ * Connection lost in sub-ioloop
+ */
+
+/* server */
+
+static void
+test_connection_lost_sub_ioloop_input(struct server_connection *conn)
+{
+ static const char *resp =
+ "HTTP/1.1 200 OK\r\n"
+ "Content-Length: 0\r\n"
+ "\r\n";
+
+ o_stream_nsend_str(conn->conn.output, resp);
+ server_connection_deinit(&conn);
+}
+
+static void test_server_connection_lost_sub_ioloop(unsigned int index)
+{
+ test_server_input = test_connection_lost_sub_ioloop_input;
+ test_server_run(index);
+}
+
+/* client */
+
+struct _connection_lost_sub_ioloop_ctx {
+ unsigned int count;
+};
+
+static void
+test_client_connection_lost_sub_ioloop_response2(
+ const struct http_response *resp, struct ioloop *sub_ioloop)
+{
+ test_client_assert_response(
+ resp,
+ (resp->status == 200 ||
+ resp->status == HTTP_CLIENT_REQUEST_ERROR_CONNECTION_LOST));
+
+ io_loop_stop(sub_ioloop);
+}
+
+static void
+test_client_connection_lost_sub_ioloop_response(
+ const struct http_response *resp,
+ struct _connection_lost_sub_ioloop_ctx *ctx)
+{
+ struct http_client_request *hreq;
+ struct ioloop *sub_ioloop;
+
+ if (debug)
+ i_debug("RESPONSE: %u %s", resp->status, resp->reason);
+
+ test_assert(resp->status == 200 ||
+ resp->status == HTTP_CLIENT_REQUEST_ERROR_CONNECTION_LOST);
+ test_assert(resp->reason != NULL && *resp->reason != '\0');
+
+ sub_ioloop = io_loop_create();
+ http_client_switch_ioloop(http_client);
+
+ hreq = http_client_request(
+ http_client, "GET", net_ip2addr(&bind_ip),
+ "/connection-lost-sub-ioloop3.txt",
+ test_client_connection_lost_sub_ioloop_response2, sub_ioloop);
+ http_client_request_set_port(hreq, bind_ports[1]);
+ http_client_request_submit(hreq);
+
+ io_loop_run(sub_ioloop);
+ io_loop_set_current(ioloop);
+ http_client_switch_ioloop(http_client);
+ io_loop_set_current(sub_ioloop);
+ io_loop_destroy(&sub_ioloop);
+
+ if (--ctx->count == 0) {
+ i_free(ctx);
+ io_loop_stop(ioloop);
+ }
+}
+
+static bool
+test_client_connection_lost_sub_ioloop(
+ const struct http_client_settings *client_set)
+{
+ static const char payload[] =
+ "This is a useless payload that only serves as a means to give "
+ "the server the opportunity to close the connection before the "
+ "payload is finished.";
+ struct _connection_lost_sub_ioloop_ctx *ctx;
+ struct http_client_request *hreq;
+ struct istream *input;
+
+ ctx = i_new(struct _connection_lost_sub_ioloop_ctx, 1);
+ ctx->count = 2;
+
+ http_client = http_client_init(client_set);
+
+ input = i_stream_create_from_data(payload, sizeof(payload)-1);
+
+ hreq = http_client_request(
+ http_client, "GET", net_ip2addr(&bind_ip),
+ "/connection-lost-sub-ioloop.txt",
+ test_client_connection_lost_sub_ioloop_response, ctx);
+ http_client_request_set_port(hreq, bind_ports[0]);
+ http_client_request_set_payload(hreq, input, TRUE);
+ http_client_request_submit(hreq);
+
+ hreq = http_client_request(
+ http_client, "GET", net_ip2addr(&bind_ip),
+ "/connection-lost-sub-ioloop2.txt",
+ test_client_connection_lost_sub_ioloop_response, ctx);
+ http_client_request_set_port(hreq, bind_ports[0]);
+ http_client_request_set_payload(hreq, input, TRUE);
+ http_client_request_submit(hreq);
+
+ i_stream_unref(&input);
+ return TRUE;
+}
+
+/* test */
+
+static void test_connection_lost_sub_ioloop(void)
+{
+ struct http_client_settings http_client_set;
+
+ test_client_defaults(&http_client_set);
+
+ server_read_max = 0;
+
+ test_begin("connection lost while running sub-ioloop");
+ http_client_set.max_attempts = 1;
+ test_run_client_server(&http_client_set,
+ test_client_connection_lost_sub_ioloop,
+ test_server_connection_lost_sub_ioloop, 2, NULL);
+ test_end();
+}
+
+/*
+ * Early success
+ */
+
+/* server */
+
+static void test_early_success_input(struct server_connection *conn)
+{
+ static const char *resp =
+ "HTTP/1.1 200 OK\r\n"
+ "Content-Length: 18\r\n"
+ "\r\n"
+ "Everything is OK\r\n";
+
+ i_sleep_msecs(200);
+ o_stream_nsend_str(conn->conn.output, resp);
+ server_connection_deinit(&conn);
+}
+
+static void test_server_early_success(unsigned int index)
+{
+ test_server_input = test_early_success_input;
+ test_server_run(index);
+}
+
+/* client */
+
+struct _early_success_ctx {
+ unsigned int count;
+};
+
+static void
+test_client_early_success_response(const struct http_response *resp,
+ struct _early_success_ctx *ctx)
+{
+ if (ctx->count == 2) {
+ test_client_assert_response(
+ resp,
+ resp->status == HTTP_CLIENT_REQUEST_ERROR_BAD_RESPONSE);
+ } else {
+ test_client_assert_response(resp, resp->status == 200);
+ }
+
+ if (--ctx->count == 0) {
+ io_loop_stop(ioloop);
+ i_free(ctx);
+ }
+}
+
+static bool
+test_client_early_success(const struct http_client_settings *client_set)
+{
+ struct http_client_request *hreq;
+ struct _early_success_ctx *ctx;
+ string_t *payload;
+ unsigned int i;
+
+ ctx = i_new(struct _early_success_ctx, 1);
+ ctx->count = 2;
+
+ http_client = http_client_init(client_set);
+
+ hreq = http_client_request(
+ http_client, "GET", net_ip2addr(&bind_ip),
+ "/early-success.txt",
+ test_client_early_success_response, ctx);
+ http_client_request_set_port(hreq, bind_ports[0]);
+
+ T_BEGIN {
+ struct istream_chain *chain;
+ struct istream *input, *chain_input;
+
+ payload = t_str_new(64*3000);
+ for (i = 0; i < 3000; i++) {
+ str_append(payload,
+ "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\r\n"
+ "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\r\n");
+ }
+
+ chain_input = i_stream_create_chain(&chain, IO_BLOCK_SIZE);
+
+ input = i_stream_create_copy_from_data(str_data(payload),
+ str_len(payload));
+ i_stream_chain_append(chain, input);
+ i_stream_unref(&input);
+
+ http_client_request_set_payload(hreq, chain_input, FALSE);
+ i_stream_unref(&chain_input);
+ } T_END;
+ http_client_request_submit(hreq);
+
+ hreq = http_client_request(
+ http_client, "GET", net_ip2addr(&bind_ip),
+ "/early-success2.txt",
+ test_client_early_success_response, ctx);
+ http_client_request_set_port(hreq, bind_ports[0]);
+ http_client_request_submit(hreq);
+
+ return TRUE;
+}
+
+/* test */
+
+static void test_early_success(void)
+{
+ struct http_client_settings http_client_set;
+
+ test_client_defaults(&http_client_set);
+ http_client_set.socket_send_buffer_size = 4096;
+
+ test_begin("early succes");
+ test_run_client_server(&http_client_set,
+ test_client_early_success,
+ test_server_early_success, 1, NULL);
+ test_end();
+}
+
+/*
+ * Bad response
+ */
+
+/* server */
+
+static void test_bad_response_input(struct server_connection *conn)
+{
+ static const char *resp =
+ "HTTP/1.1 666 Really bad response\r\n"
+ "\r\n";
+
+ o_stream_nsend_str(conn->conn.output, resp);
+ server_connection_deinit(&conn);
+}
+
+static void test_server_bad_response(unsigned int index)
+{
+ test_server_input = test_bad_response_input;
+ test_server_run(index);
+}
+
+/* client */
+
+struct _bad_response_ctx {
+ unsigned int count;
+};
+
+static void
+test_client_bad_response_response(const struct http_response *resp,
+ struct _bad_response_ctx *ctx)
+{
+ test_client_assert_response(
+ resp, resp->status == HTTP_CLIENT_REQUEST_ERROR_BAD_RESPONSE);
+
+ if (--ctx->count == 0) {
+ i_free(ctx);
+ io_loop_stop(ioloop);
+ }
+}
+
+static bool
+test_client_bad_response(const struct http_client_settings *client_set)
+{
+ struct http_client_request *hreq;
+ struct _bad_response_ctx *ctx;
+
+ ctx = i_new(struct _bad_response_ctx, 1);
+ ctx->count = 2;
+
+ http_client = http_client_init(client_set);
+
+ hreq = http_client_request(
+ http_client, "GET", net_ip2addr(&bind_ip),
+ "/bad-response.txt",
+ test_client_bad_response_response, ctx);
+ http_client_request_set_port(hreq, bind_ports[0]);
+ http_client_request_submit(hreq);
+
+ hreq = http_client_request(
+ http_client, "GET", net_ip2addr(&bind_ip),
+ "/bad-response2.txt",
+ test_client_bad_response_response, ctx);
+ http_client_request_set_port(hreq, bind_ports[0]);
+ http_client_request_submit(hreq);
+
+ return TRUE;
+}
+
+/* test */
+
+static void test_bad_response(void)
+{
+ struct http_client_settings http_client_set;
+
+ test_client_defaults(&http_client_set);
+
+ test_begin("bad response");
+ test_run_client_server(&http_client_set,
+ test_client_bad_response,
+ test_server_bad_response, 1, NULL);
+ test_end();
+}
+
+/*
+ * Request timed out
+ */
+
+/* server */
+
+static void
+test_request_timed_out_input(struct server_connection *conn ATTR_UNUSED)
+{
+ /* do nothing */
+}
+
+static void test_server_request_timed_out(unsigned int index)
+{
+ test_server_input = test_request_timed_out_input;
+ test_server_run(index);
+}
+
+/* client */
+
+struct _request_timed_out1_ctx {
+ unsigned int count;
+};
+
+static void
+test_client_request_timed_out1_response(const struct http_response *resp,
+ struct _request_timed_out1_ctx *ctx)
+{
+ test_client_assert_response(
+ resp, resp->status == HTTP_CLIENT_REQUEST_ERROR_TIMED_OUT);
+
+ if (--ctx->count == 0) {
+ i_free(ctx);
+ io_loop_stop(ioloop);
+ }
+}
+
+static bool
+test_client_request_timed_out1(const struct http_client_settings *client_set)
+{
+ struct http_client_request *hreq;
+ struct _request_timed_out1_ctx *ctx;
+
+ ctx = i_new(struct _request_timed_out1_ctx, 1);
+ ctx->count = 2;
+
+ http_client = http_client_init(client_set);
+
+ hreq = http_client_request(
+ http_client, "GET", net_ip2addr(&bind_ip),
+ "/request-timed-out1-1.txt",
+ test_client_request_timed_out1_response, ctx);
+ http_client_request_set_port(hreq, bind_ports[0]);
+ http_client_request_submit(hreq);
+
+ hreq = http_client_request(
+ http_client, "GET", net_ip2addr(&bind_ip),
+ "/request-timed-out1-2.txt",
+ test_client_request_timed_out1_response, ctx);
+ http_client_request_set_port(hreq, bind_ports[0]);
+ http_client_request_submit(hreq);
+
+ return TRUE;
+}
+
+struct _request_timed_out2_ctx {
+ struct timeout *to;
+ unsigned int count;
+ unsigned int max_parallel_connections;
+};
+
+static void
+test_client_request_timed_out2_timeout(struct _request_timed_out2_ctx *ctx)
+{
+ timeout_remove(&ctx->to);
+ i_debug("TIMEOUT");
+}
+
+static void
+test_client_request_timed_out2_response(const struct http_response *resp,
+ struct _request_timed_out2_ctx *ctx)
+{
+ test_client_assert_response(
+ resp, resp->status == HTTP_CLIENT_REQUEST_ERROR_TIMED_OUT);
+ test_assert(ctx->to != NULL);
+
+ if (--ctx->count > 0) {
+ if (ctx->to != NULL && ctx->max_parallel_connections <= 1)
+ timeout_reset(ctx->to);
+ } else {
+ timeout_remove(&ctx->to);
+ i_free(ctx);
+ io_loop_stop(ioloop);
+ }
+}
+
+static bool
+test_client_request_timed_out2(const struct http_client_settings *client_set)
+{
+ struct http_client_request *hreq;
+ struct _request_timed_out2_ctx *ctx;
+
+ ctx = i_new(struct _request_timed_out2_ctx, 1);
+ ctx->count = 2;
+ ctx->max_parallel_connections =
+ client_set->max_parallel_connections;
+
+ ctx->to = timeout_add(2000,
+ test_client_request_timed_out2_timeout, ctx);
+
+ http_client = http_client_init(client_set);
+
+ hreq = http_client_request(
+ http_client, "GET", net_ip2addr(&bind_ip),
+ "/request-timed-out2-1.txt",
+ test_client_request_timed_out2_response, ctx);
+ http_client_request_set_port(hreq, bind_ports[0]);
+ http_client_request_set_attempt_timeout_msecs(hreq, 1000);
+ http_client_request_submit(hreq);
+
+ hreq = http_client_request(
+ http_client, "GET", net_ip2addr(&bind_ip),
+ "/request-timed-out2-2.txt",
+ test_client_request_timed_out2_response, ctx);
+ http_client_request_set_port(hreq, bind_ports[0]);
+ http_client_request_set_attempt_timeout_msecs(hreq, 1000);
+ http_client_request_submit(hreq);
+
+ return TRUE;
+}
+
+/* test */
+
+static void test_request_timed_out(void)
+{
+ struct http_client_settings http_client_set;
+
+ test_client_defaults(&http_client_set);
+
+ test_begin("request timed out: one attempt");
+ http_client_set.request_timeout_msecs = 1000;
+ http_client_set.max_attempts = 1;
+ test_run_client_server(&http_client_set,
+ test_client_request_timed_out1,
+ test_server_request_timed_out, 1, NULL);
+ test_end();
+
+ test_begin("request timed out: two attempts");
+ http_client_set.request_timeout_msecs = 1000;
+ http_client_set.max_attempts = 1;
+ test_run_client_server(&http_client_set,
+ test_client_request_timed_out1,
+ test_server_request_timed_out, 1, NULL);
+ test_end();
+
+ test_begin("request absolutely timed out");
+ http_client_set.request_timeout_msecs = 0;
+ http_client_set.request_absolute_timeout_msecs = 2000;
+ http_client_set.max_attempts = 3;
+ test_run_client_server(&http_client_set,
+ test_client_request_timed_out1,
+ test_server_request_timed_out, 1, NULL);
+ test_end();
+
+ test_begin("request double timed out");
+ http_client_set.request_timeout_msecs = 500;
+ http_client_set.request_absolute_timeout_msecs = 2000;
+ http_client_set.max_attempts = 3;
+ test_run_client_server(&http_client_set,
+ test_client_request_timed_out1,
+ test_server_request_timed_out, 1, NULL);
+ test_end();
+
+ test_begin("request timed out: specific timeout");
+ http_client_set.request_timeout_msecs = 3000;
+ http_client_set.request_absolute_timeout_msecs = 0;
+ http_client_set.max_attempts = 1;
+ http_client_set.max_parallel_connections = 1;
+ test_run_client_server(&http_client_set,
+ test_client_request_timed_out2,
+ test_server_request_timed_out, 1, NULL);
+ test_end();
+
+ test_begin("request timed out: specific timeout (parallel)");
+ http_client_set.request_timeout_msecs = 3000;
+ http_client_set.request_absolute_timeout_msecs = 0;
+ http_client_set.max_attempts = 1;
+ http_client_set.max_parallel_connections = 4;
+ test_run_client_server(&http_client_set,
+ test_client_request_timed_out2,
+ test_server_request_timed_out, 1, NULL);
+ test_end();
+}
+
+/*
+ * Request aborted early
+ */
+
+/* server */
+
+static void
+test_request_aborted_early_input(struct server_connection *conn ATTR_UNUSED)
+{
+ static const char *resp =
+ "HTTP/1.1 404 Not Found\r\n"
+ "\r\n";
+
+ /* wait one second to respond */
+ i_sleep_intr_secs(1);
+
+ /* respond */
+ o_stream_nsend_str(conn->conn.output, resp);
+ server_connection_deinit(&conn);
+}
+
+static void test_server_request_aborted_early(unsigned int index)
+{
+ test_server_input = test_request_aborted_early_input;
+ test_server_run(index);
+}
+
+/* client */
+
+struct _request_aborted_early_ctx {
+ struct http_client_request *req1, *req2;
+ struct timeout *to;
+};
+
+static void
+test_client_request_aborted_early_response(
+ const struct http_response *resp,
+ struct _request_aborted_early_ctx *ctx ATTR_UNUSED)
+{
+ if (debug)
+ i_debug("RESPONSE: %u %s", resp->status, resp->reason);
+
+ /* abort does not trigger callback */
+ test_assert(FALSE);
+}
+
+static void
+test_client_request_aborted_early_timeout(
+ struct _request_aborted_early_ctx *ctx)
+{
+ timeout_remove(&ctx->to);
+
+ if (ctx->req1 != NULL) {
+ /* abort early */
+ http_client_request_abort(&ctx->req1); /* sent */
+ http_client_request_abort(&ctx->req2); /* only queued */
+
+ /* wait a little for server to actually respond to an
+ already aborted request */
+ ctx->to = timeout_add_short(
+ 1000, test_client_request_aborted_early_timeout, ctx);
+ } else {
+ /* all done */
+ i_free(ctx);
+ io_loop_stop(ioloop);
+ }
+}
+
+static bool
+test_client_request_aborted_early(const struct http_client_settings *client_set)
+{
+ struct http_client_request *hreq;
+ struct _request_aborted_early_ctx *ctx;
+
+ ctx = i_new(struct _request_aborted_early_ctx, 1);
+
+ http_client = http_client_init(client_set);
+
+ hreq = ctx->req1 = http_client_request(
+ http_client, "GET", net_ip2addr(&bind_ip),
+ "/request-aborted-early.txt",
+ test_client_request_aborted_early_response, ctx);
+ http_client_request_set_port(hreq, bind_ports[0]);
+ http_client_request_submit(hreq);
+
+ hreq = ctx->req2 = http_client_request(
+ http_client, "GET", net_ip2addr(&bind_ip),
+ "/request-aborted-early2.txt",
+ test_client_request_aborted_early_response, ctx);
+ http_client_request_set_port(hreq, bind_ports[0]);
+ http_client_request_submit(hreq);
+
+ ctx->to = timeout_add_short(
+ 500, test_client_request_aborted_early_timeout, ctx);
+ return TRUE;
+}
+
+/* test */
+
+static void test_request_aborted_early(void)
+{
+ struct http_client_settings http_client_set;
+
+ test_client_defaults(&http_client_set);
+
+ test_begin("request aborted early");
+ test_run_client_server(&http_client_set,
+ test_client_request_aborted_early,
+ test_server_request_aborted_early, 1, NULL);
+ test_end();
+}
+
+/*
+ * Request failed blocking
+ */
+
+/* server */
+
+static void
+test_request_failed_blocking_input(struct server_connection *conn)
+{
+ static const char *resp =
+ "HTTP/1.1 500 Internal Server Error\r\n"
+ "\r\n";
+
+ /* respond */
+ o_stream_nsend_str(conn->conn.output, resp);
+ i_sleep_intr_secs(10);
+ server_connection_deinit(&conn);
+}
+
+static void test_server_request_failed_blocking(unsigned int index)
+{
+ test_server_input = test_request_failed_blocking_input;
+ test_server_run(index);
+}
+
+/* client */
+
+struct _request_failed_blocking_ctx {
+ struct http_client_request *req;
+};
+
+static void
+test_client_request_failed_blocking_response(
+ const struct http_response *resp,
+ struct _request_failed_blocking_ctx *ctx ATTR_UNUSED)
+{
+ test_client_assert_response(resp, resp->status == 500);
+}
+
+static bool
+test_client_request_failed_blocking(
+ const struct http_client_settings *client_set)
+{
+ static const char *payload = "This a test payload!";
+ struct http_client_request *hreq;
+ struct _request_failed_blocking_ctx *ctx;
+ unsigned int n;
+ string_t *data;
+
+ data = str_new(default_pool, 1000000);
+ for (n = 0; n < 50000; n++)
+ str_append(data, payload);
+
+ ctx = i_new(struct _request_failed_blocking_ctx, 1);
+
+ http_client = http_client_init(client_set);
+
+ hreq = ctx->req = http_client_request(
+ http_client, "GET", net_ip2addr(&bind_ip),
+ "/request-failed-blocking.txt",
+ test_client_request_failed_blocking_response, ctx);
+ http_client_request_set_port(hreq, bind_ports[0]);
+
+ test_assert(http_client_request_send_payload(&hreq,
+ str_data(data), str_len(data)) < 0);
+ i_assert(hreq == NULL);
+
+ str_free(&data);
+ i_free(ctx);
+ return FALSE;
+}
+
+/* test */
+
+static void test_request_failed_blocking(void)
+{
+ struct http_client_settings http_client_set;
+
+ test_client_defaults(&http_client_set);
+ http_client_set.socket_send_buffer_size = 4096;
+
+ test_begin("request failed blocking");
+ test_run_client_server(&http_client_set,
+ test_client_request_failed_blocking,
+ test_server_request_failed_blocking, 1, NULL);
+ test_end();
+}
+
+/*
+ * Client deinit early
+ */
+
+/* server */
+
+static void
+test_client_deinit_early_input(struct server_connection *conn ATTR_UNUSED)
+{
+ static const char *resp =
+ "HTTP/1.1 404 Not Found\r\n"
+ "\r\n";
+
+ /* wait one second to respond */
+ i_sleep_intr_secs(1);
+
+ /* respond */
+ o_stream_nsend_str(conn->conn.output, resp);
+ server_connection_deinit(&conn);
+}
+
+static void test_server_client_deinit_early(unsigned int index)
+{
+ test_server_input = test_client_deinit_early_input;
+ test_server_run(index);
+}
+
+/* client */
+
+struct _client_deinit_early_ctx {
+ struct timeout *to;
+};
+
+static void
+test_client_client_deinit_early_response(
+ const struct http_response *resp,
+ struct _client_deinit_early_ctx *ctx ATTR_UNUSED)
+{
+ if (debug)
+ i_debug("RESPONSE: %u %s", resp->status, resp->reason);
+
+ /* abort does not trigger callback */
+ test_assert(FALSE);
+}
+
+static void
+test_client_client_deinit_early_timeout(struct _client_deinit_early_ctx *ctx)
+{
+ timeout_remove(&ctx->to);
+
+ /* deinit early */
+ http_client_deinit(&http_client);
+
+ /* all done */
+ i_free(ctx);
+ io_loop_stop(ioloop);
+}
+
+static bool
+test_client_client_deinit_early(const struct http_client_settings *client_set)
+{
+ struct http_client_request *hreq;
+ struct _client_deinit_early_ctx *ctx;
+
+ ctx = i_new(struct _client_deinit_early_ctx, 1);
+
+ http_client = http_client_init(client_set);
+
+ hreq = http_client_request(
+ http_client, "GET", net_ip2addr(&bind_ip),
+ "/client-deinit-early.txt",
+ test_client_client_deinit_early_response, ctx);
+ http_client_request_set_port(hreq, bind_ports[0]);
+ http_client_request_submit(hreq);
+
+ hreq = http_client_request(
+ http_client, "GET", net_ip2addr(&bind_ip),
+ "/client-deinit-early2.txt",
+ test_client_client_deinit_early_response, ctx);
+ http_client_request_set_port(hreq, bind_ports[0]);
+ http_client_request_submit(hreq);
+
+ ctx->to = timeout_add_short(
+ 500, test_client_client_deinit_early_timeout, ctx);
+ return TRUE;
+}
+
+/* test */
+
+static void test_client_deinit_early(void)
+{
+ struct http_client_settings http_client_set;
+
+ test_client_defaults(&http_client_set);
+
+ test_begin("client deinit early");
+ test_run_client_server(&http_client_set,
+ test_client_client_deinit_early,
+ test_server_client_deinit_early, 1, NULL);
+ test_end();
+}
+
+/*
+ * Retry with delay
+ */
+
+/* server */
+
+static void test_retry_with_delay_input(struct server_connection *conn)
+{
+ string_t *resp;
+
+ resp = t_str_new(512);
+ str_printfa(resp,
+ "HTTP/1.1 500 Internal Server Error\r\n"
+ "\r\n");
+ o_stream_nsend(conn->conn.output, str_data(resp), str_len(resp));
+ server_connection_deinit(&conn);
+}
+
+static void test_server_retry_with_delay(unsigned int index)
+{
+ test_server_input = test_retry_with_delay_input;
+ test_server_run(index);
+}
+
+/* client */
+
+struct _client_retry_with_delay_ctx {
+ struct http_client_request *req;
+ unsigned int retries;
+ struct timeval time;
+};
+
+static void
+test_client_retry_with_delay_response(
+ const struct http_response *resp,
+ struct _client_retry_with_delay_ctx *ctx)
+{
+ int real_delay, exp_delay;
+
+ test_client_assert_response(resp, resp->status == 500);
+
+ if (ctx->retries > 0) {
+ /* check delay */
+ real_delay = timeval_diff_msecs(&ioloop_timeval, &ctx->time);
+ exp_delay = (1 << (ctx->retries-1)) * 50;
+ if (real_delay < exp_delay-2) {
+ i_fatal("Retry delay is too short %d < %d",
+ real_delay, exp_delay);
+ }
+ }
+
+ http_client_request_delay_msecs(ctx->req, (1 << ctx->retries) * 50);
+ ctx->time = ioloop_timeval;
+ if (http_client_request_try_retry(ctx->req)) {
+ ctx->retries++;
+ if (debug)
+ i_debug("retrying");
+ return;
+ }
+
+ i_free(ctx);
+ io_loop_stop(ioloop);
+}
+
+static bool
+test_client_retry_with_delay(const struct http_client_settings *client_set)
+{
+ struct http_client_request *hreq;
+ struct _client_retry_with_delay_ctx *ctx;
+
+ ctx = i_new(struct _client_retry_with_delay_ctx, 1);
+ ctx->time = ioloop_timeval;
+
+ http_client = http_client_init(client_set);
+
+ ctx->req = hreq = http_client_request(
+ http_client, "GET", net_ip2addr(&bind_ip),
+ "/retry-with-delay.txt",
+ test_client_retry_with_delay_response, ctx);
+ http_client_request_set_port(hreq, bind_ports[0]);
+ http_client_request_submit(hreq);
+
+ return TRUE;
+}
+
+/* test */
+
+static void test_retry_with_delay(void)
+{
+ struct http_client_settings http_client_set;
+
+ test_client_defaults(&http_client_set);
+ http_client_set.max_attempts = 3;
+
+ test_begin("retry with delay");
+ test_run_client_server(&http_client_set,
+ test_client_retry_with_delay,
+ test_server_retry_with_delay, 1, NULL);
+ test_end();
+}
+
+/*
+ * DNS service failure
+ */
+
+/* client */
+
+struct _dns_service_failure {
+ unsigned int count;
+};
+
+static void
+test_client_dns_service_failure_response(
+ const struct http_response *resp,
+ struct _dns_service_failure *ctx)
+{
+ test_client_assert_response(
+ resp,
+ resp->status == HTTP_CLIENT_REQUEST_ERROR_HOST_LOOKUP_FAILED);
+
+ if (--ctx->count == 0) {
+ i_free(ctx);
+ io_loop_stop(ioloop);
+ }
+}
+
+static bool
+test_client_dns_service_failure(const struct http_client_settings *client_set)
+{
+ struct http_client_request *hreq;
+ struct _dns_service_failure *ctx;
+
+ ctx = i_new(struct _dns_service_failure, 1);
+ ctx->count = 2;
+
+ http_client = http_client_init(client_set);
+
+ hreq = http_client_request(
+ http_client, "GET", "example.com", "/dns-service-failure.txt",
+ test_client_dns_service_failure_response, ctx);
+ http_client_request_set_port(hreq, 80);
+ http_client_request_submit(hreq);
+
+ hreq = http_client_request(
+ http_client, "GET", "example.com", "/dns-service-failure2.txt",
+ test_client_dns_service_failure_response, ctx);
+ http_client_request_set_port(hreq, 80);
+ http_client_request_submit(hreq);
+
+ return TRUE;
+}
+
+/* test */
+
+static void test_dns_service_failure(void)
+{
+ struct http_client_settings http_client_set;
+
+ test_client_defaults(&http_client_set);
+ http_client_set.dns_client_socket_path = "./frop";
+
+ test_begin("dns service failure");
+ test_run_client_server(&http_client_set,
+ test_client_dns_service_failure,
+ NULL, 0, NULL);
+ test_end();
+}
+
+/*
+ * DNS timeout
+ */
+
+/* dns */
+
+static void test_dns_timeout_input(struct server_connection *conn ATTR_UNUSED)
+{
+ /* hang */
+ i_sleep_intr_secs(100);
+ server_connection_deinit(&conn);
+}
+
+static void test_dns_dns_timeout(void)
+{
+ test_server_input = test_dns_timeout_input;
+ test_server_run(0);
+}
+
+/* client */
+
+struct _dns_timeout {
+ unsigned int count;
+};
+
+static void
+test_client_dns_timeout_response(
+ const struct http_response *resp,
+ struct _dns_timeout *ctx)
+{
+ test_client_assert_response(
+ resp,
+ resp->status == HTTP_CLIENT_REQUEST_ERROR_HOST_LOOKUP_FAILED);
+
+ if (--ctx->count == 0) {
+ i_free(ctx);
+ io_loop_stop(ioloop);
+ }
+}
+
+static bool
+test_client_dns_timeout(const struct http_client_settings *client_set)
+{
+ struct http_client_request *hreq;
+ struct _dns_timeout *ctx;
+
+ ctx = i_new(struct _dns_timeout, 1);
+ ctx->count = 2;
+
+ http_client = http_client_init(client_set);
+
+ hreq = http_client_request(
+ http_client, "GET", "example.com", "/dns-timeout.txt",
+ test_client_dns_timeout_response, ctx);
+ http_client_request_set_port(hreq, 80);
+ http_client_request_submit(hreq);
+
+ hreq = http_client_request(
+ http_client, "GET", "example.com", "/dns-timeout2.txt",
+ test_client_dns_timeout_response, ctx);
+ http_client_request_set_port(hreq, 80);
+ http_client_request_submit(hreq);
+
+ return TRUE;
+}
+
+/* test */
+
+static void test_dns_timeout(void)
+{
+ struct http_client_settings http_client_set;
+
+ test_client_defaults(&http_client_set);
+ http_client_set.request_timeout_msecs = 2000;
+ http_client_set.connect_timeout_msecs = 2000;
+ http_client_set.dns_client_socket_path = "./dns-test";
+
+ test_begin("dns timeout");
+ test_run_client_server(&http_client_set,
+ test_client_dns_timeout, NULL, 0,
+ test_dns_dns_timeout);
+ test_end();
+}
+
+/*
+ * DNS lookup failure
+ */
+
+/* dns */
+
+static void
+test_dns_lookup_failure_input(struct server_connection *conn)
+{
+ if (!conn->version_sent) {
+ conn->version_sent = TRUE;
+ o_stream_nsend_str(conn->conn.output, "VERSION\tdns\t1\t0\n");
+ }
+
+ o_stream_nsend_str(conn->conn.output,
+ t_strdup_printf("%d\tFAIL\n", EAI_FAIL));
+ server_connection_deinit(&conn);
+}
+
+static void test_dns_dns_lookup_failure(void)
+{
+ test_server_input = test_dns_lookup_failure_input;
+ test_server_run(0);
+}
+
+/* client */
+
+struct _dns_lookup_failure {
+ unsigned int count;
+};
+
+static void
+test_client_dns_lookup_failure_response(const struct http_response *resp,
+ struct _dns_lookup_failure *ctx)
+{
+ test_client_assert_response(
+ resp,
+ resp->status == HTTP_CLIENT_REQUEST_ERROR_HOST_LOOKUP_FAILED);
+
+ if (--ctx->count == 0) {
+ i_free(ctx);
+ io_loop_stop(ioloop);
+ }
+}
+
+static bool
+test_client_dns_lookup_failure(const struct http_client_settings *client_set)
+{
+ struct http_client_request *hreq;
+ struct _dns_lookup_failure *ctx;
+
+ ctx = i_new(struct _dns_lookup_failure, 1);
+ ctx->count = 2;
+
+ http_client = http_client_init(client_set);
+
+ hreq = http_client_request(
+ http_client, "GET", "example.com", "/dns-lookup-failure.txt",
+ test_client_dns_lookup_failure_response, ctx);
+ http_client_request_set_port(hreq, 80);
+ http_client_request_submit(hreq);
+
+ hreq = http_client_request(
+ http_client, "GET", "example.com", "/dns-lookup-failure2.txt",
+ test_client_dns_lookup_failure_response, ctx);
+ http_client_request_set_port(hreq, 80);
+ http_client_request_submit(hreq);
+
+ return TRUE;
+}
+
+/* test */
+
+static void test_dns_lookup_failure(void)
+{
+ struct http_client_settings http_client_set;
+
+ test_client_defaults(&http_client_set);
+ http_client_set.dns_client_socket_path = "./dns-test";
+
+ test_begin("dns lookup failure");
+ test_run_client_server(&http_client_set,
+ test_client_dns_lookup_failure, NULL, 0,
+ test_dns_dns_lookup_failure);
+ test_end();
+}
+
+/*
+ * DNS lookup ttl
+ */
+
+/* dns */
+
+static void
+test_dns_lookup_ttl_input(struct server_connection *conn)
+{
+ static unsigned int count = 0;
+ const char *line;
+
+ if (!conn->version_sent) {
+ conn->version_sent = TRUE;
+ o_stream_nsend_str(conn->conn.output, "VERSION\tdns\t1\t0\n");
+ }
+
+ while ((line = i_stream_read_next_line(conn->conn.input)) != NULL) {
+ if (str_begins(line, "VERSION"))
+ continue;
+ if (debug)
+ i_debug("DNS REQUEST %u: %s", count, line);
+
+ if (count == 0) {
+ o_stream_nsend_str(conn->conn.output,
+ "0\t127.0.0.1\n");
+ } else {
+ o_stream_nsend_str(
+ conn->conn.output,
+ t_strdup_printf("%d\tFAIL\n", EAI_FAIL));
+ if (count > 4) {
+ server_connection_deinit(&conn);
+ return;
+ }
+ }
+ count++;
+ }
+}
+
+static void test_dns_dns_lookup_ttl(void)
+{
+ test_server_input = test_dns_lookup_ttl_input;
+ test_server_run(0);
+}
+
+/* server */
+
+static void
+test_server_dns_lookup_ttl_input(struct server_connection *conn)
+{
+ string_t *resp;
+
+ resp = t_str_new(512);
+ str_printfa(resp,
+ "HTTP/1.1 200 OK\r\n"
+ "Connection: close\r\n"
+ "\r\n");
+ o_stream_nsend(conn->conn.output, str_data(resp), str_len(resp));
+ server_connection_deinit(&conn);
+}
+
+static void test_server_dns_lookup_ttl(unsigned int index)
+{
+ test_server_input = test_server_dns_lookup_ttl_input;
+ test_server_run(index);
+}
+
+/* client */
+
+struct _dns_lookup_ttl {
+ struct http_client *client;
+ unsigned int count;
+ struct timeout *to;
+};
+
+static void
+test_client_dns_lookup_ttl_response_stage2(const struct http_response *resp,
+ struct _dns_lookup_ttl *ctx)
+{
+ test_client_assert_response(
+ resp,
+ resp->status == HTTP_CLIENT_REQUEST_ERROR_HOST_LOOKUP_FAILED);
+
+ if (--ctx->count == 0) {
+ i_free(ctx);
+ io_loop_stop(ioloop);
+ }
+}
+
+static void test_client_dns_lookup_ttl_stage2_start(struct _dns_lookup_ttl *ctx)
+{
+ struct http_client_request *hreq;
+
+ timeout_remove(&ctx->to);
+
+ ctx->count = 2;
+
+ hreq = http_client_request(
+ ctx->client, "GET", "example.com",
+ "/dns-lookup-ttl-stage2.txt",
+ test_client_dns_lookup_ttl_response_stage2, ctx);
+ http_client_request_set_port(hreq, bind_ports[0]);
+ http_client_request_submit(hreq);
+
+ hreq = http_client_request(
+ ctx->client, "GET", "example.com",
+ "/dns-lookup-ttl2-stage2.txt",
+ test_client_dns_lookup_ttl_response_stage2, ctx);
+ http_client_request_set_port(hreq, bind_ports[0]);
+ http_client_request_submit(hreq);
+}
+
+static void
+test_client_dns_lookup_ttl_response_stage1(const struct http_response *resp,
+ struct _dns_lookup_ttl *ctx)
+{
+ test_client_assert_response(resp, resp->status == 200);
+
+ if (--ctx->count == 0) {
+ ctx->to = timeout_add(2000,
+ test_client_dns_lookup_ttl_stage2_start, ctx);
+ }
+}
+
+static bool
+test_client_dns_lookup_ttl(const struct http_client_settings *client_set)
+{
+ struct http_client_request *hreq;
+ struct _dns_lookup_ttl *ctx;
+
+ ctx = i_new(struct _dns_lookup_ttl, 1);
+ ctx->count = 2;
+
+ ctx->client = http_client = http_client_init(client_set);
+
+ hreq = http_client_request(
+ ctx->client, "GET", "example.com",
+ "/dns-lookup-ttl-stage1.txt",
+ test_client_dns_lookup_ttl_response_stage1, ctx);
+ http_client_request_set_port(hreq, bind_ports[0]);
+ http_client_request_submit(hreq);
+
+ hreq = http_client_request(
+ ctx->client, "GET", "example.com",
+ "/dns-lookup-ttl2-stage1.txt",
+ test_client_dns_lookup_ttl_response_stage1, ctx);
+ http_client_request_set_port(hreq, bind_ports[0]);
+ http_client_request_submit(hreq);
+
+ return TRUE;
+}
+
+/* test */
+
+static void test_dns_lookup_ttl(void)
+{
+ struct http_client_settings http_client_set;
+
+ test_client_defaults(&http_client_set);
+ http_client_set.dns_client_socket_path = "./dns-test";
+ http_client_set.dns_ttl_msecs = 1000;
+
+ test_begin("dns lookup ttl");
+ test_run_client_server(&http_client_set,
+ test_client_dns_lookup_ttl,
+ test_server_dns_lookup_ttl, 1,
+ test_dns_dns_lookup_ttl);
+ test_end();
+}
+
+/*
+ * Peer reuse failure
+ */
+
+/* server */
+
+static void test_peer_reuse_failure_input(struct server_connection *conn)
+{
+ static unsigned int seq = 0;
+ static const char *resp =
+ "HTTP/1.1 200 OK\r\n"
+ "\r\n";
+
+ o_stream_nsend_str(conn->conn.output, resp);
+ if (seq++ > 2) {
+ server_connection_deinit(&conn);
+ io_loop_stop(current_ioloop);
+ }
+}
+
+static void test_server_peer_reuse_failure(unsigned int index)
+{
+ test_server_input = test_peer_reuse_failure_input;
+ test_server_run(index);
+}
+
+/* client */
+
+struct _peer_reuse_failure {
+ struct timeout *to;
+ bool first:1;
+};
+
+static void
+test_client_peer_reuse_failure_response2(const struct http_response *resp,
+ struct _peer_reuse_failure *ctx)
+{
+ test_client_assert_response(
+ resp, http_response_is_internal_error(resp));
+
+ i_free(ctx);
+ io_loop_stop(ioloop);
+}
+
+static void
+test_client_peer_reuse_failure_next(struct _peer_reuse_failure *ctx)
+{
+ struct http_client_request *hreq;
+
+ timeout_remove(&ctx->to);
+
+ hreq = http_client_request(
+ http_client, "GET", net_ip2addr(&bind_ip),
+ "/peer-reuse-next.txt",
+ test_client_peer_reuse_failure_response2, ctx);
+ http_client_request_set_port(hreq, bind_ports[0]);
+ http_client_request_submit(hreq);
+}
+
+static void
+test_client_peer_reuse_failure_response1(const struct http_response *resp,
+ struct _peer_reuse_failure *ctx)
+{
+ if (ctx->first) {
+ test_client_assert_response(resp, resp->status == 200);
+
+ ctx->first = FALSE;
+ ctx->to = timeout_add_short(
+ 500, test_client_peer_reuse_failure_next, ctx);
+ } else {
+ test_client_assert_response(
+ resp, http_response_is_internal_error(resp));
+ }
+
+ test_assert(resp->reason != NULL && *resp->reason != '\0');
+}
+
+static bool
+test_client_peer_reuse_failure(const struct http_client_settings *client_set)
+{
+ struct http_client_request *hreq;
+ struct _peer_reuse_failure *ctx;
+
+ ctx = i_new(struct _peer_reuse_failure, 1);
+ ctx->first = TRUE;
+
+ http_client = http_client_init(client_set);
+
+ hreq = http_client_request(
+ http_client, "GET", net_ip2addr(&bind_ip), "/peer-reuse.txt",
+ test_client_peer_reuse_failure_response1, ctx);
+ http_client_request_set_port(hreq, bind_ports[0]);
+ http_client_request_submit(hreq);
+
+ hreq = http_client_request(
+ http_client, "GET", net_ip2addr(&bind_ip), "/peer-reuse.txt",
+ test_client_peer_reuse_failure_response1, ctx);
+ http_client_request_set_port(hreq, bind_ports[0]);
+ http_client_request_submit(hreq);
+
+ hreq = http_client_request(
+ http_client, "GET", net_ip2addr(&bind_ip), "/peer-reuse.txt",
+ test_client_peer_reuse_failure_response1, ctx);
+ http_client_request_set_port(hreq, bind_ports[0]);
+ http_client_request_submit(hreq);
+
+ return TRUE;
+}
+
+/* test */
+
+static void test_peer_reuse_failure(void)
+{
+ struct http_client_settings http_client_set;
+
+ test_client_defaults(&http_client_set);
+ http_client_set.max_connect_attempts = 1;
+ http_client_set.max_idle_time_msecs = 500;
+
+ test_begin("peer reuse failure");
+ test_run_client_server(&http_client_set,
+ test_client_peer_reuse_failure,
+ test_server_peer_reuse_failure, 1, NULL);
+ test_end();
+}
+
+/*
+ * Reconnect failure
+ */
+
+/* dns */
+
+static void test_dns_reconnect_failure_input(struct server_connection *conn)
+{
+ static unsigned int count = 0;
+ const char *line;
+
+ if (!conn->version_sent) {
+ conn->version_sent = TRUE;
+ o_stream_nsend_str(conn->conn.output, "VERSION\tdns\t1\t0\n");
+ }
+
+ while ((line = i_stream_read_next_line(conn->conn.input)) != NULL) {
+ if (str_begins(line, "VERSION"))
+ continue;
+ if (debug)
+ i_debug("DNS REQUEST %u: %s", count, line);
+
+ if (count == 0) {
+ o_stream_nsend_str(conn->conn.output,
+ "0\t127.0.0.1\n");
+ } else {
+ o_stream_nsend_str(
+ conn->conn.output,
+ t_strdup_printf("%d\tFAIL\n", EAI_FAIL));
+ if (count > 4) {
+ server_connection_deinit(&conn);
+ return;
+ }
+ }
+ count++;
+ }
+}
+
+static void test_dns_reconnect_failure(void)
+{
+ test_server_input = test_dns_reconnect_failure_input;
+ test_server_run(0);
+}
+
+/* server */
+
+static void test_reconnect_failure_input(struct server_connection *conn)
+{
+ static const char *resp =
+ "HTTP/1.1 200 OK\r\n"
+ "Content-Length: 18\r\n"
+ "\r\n"
+ "Everything is OK\r\n";
+
+ o_stream_nsend_str(conn->conn.output, resp);
+ io_loop_stop(current_ioloop);
+ io_remove(&io_listen);
+ i_close_fd(&fd_listen);
+ server_connection_deinit(&conn);
+}
+
+static void test_server_reconnect_failure(unsigned int index)
+{
+ test_server_input = test_reconnect_failure_input;
+ test_server_run(index);
+}
+
+/* client */
+
+struct _reconnect_failure_ctx {
+ struct timeout *to;
+};
+
+static void
+test_client_reconnect_failure_response2(const struct http_response *resp,
+ struct _reconnect_failure_ctx *ctx)
+{
+ test_client_assert_response(
+ resp, resp->status == HTTP_CLIENT_REQUEST_ERROR_CONNECT_FAILED);
+
+ io_loop_stop(ioloop);
+ i_free(ctx);
+}
+
+static void
+test_client_reconnect_failure_next(struct _reconnect_failure_ctx *ctx)
+{
+ struct http_client_request *hreq;
+
+ if (debug)
+ i_debug("NEXT REQUEST");
+
+ timeout_remove(&ctx->to);
+
+ hreq = http_client_request(
+ http_client, "GET", "example.com", "/reconnect-failure-2.txt",
+ test_client_reconnect_failure_response2, ctx);
+ http_client_request_set_port(hreq, bind_ports[0]);
+ http_client_request_submit(hreq);
+}
+
+static void
+test_client_reconnect_failure_response1(const struct http_response *resp,
+ struct _reconnect_failure_ctx *ctx)
+{
+ test_client_assert_response(resp, resp->status == 200);
+
+ ctx->to = timeout_add_short(
+ 5000, test_client_reconnect_failure_next, ctx);
+}
+
+static bool
+test_client_reconnect_failure(const struct http_client_settings *client_set)
+{
+ struct http_client_request *hreq;
+ struct _reconnect_failure_ctx *ctx;
+
+ ctx = i_new(struct _reconnect_failure_ctx, 1);
+
+ http_client = http_client_init(client_set);
+
+ hreq = http_client_request(
+ http_client, "GET", "example.com", "/reconnect-failure-1.txt",
+ test_client_reconnect_failure_response1, ctx);
+ http_client_request_set_port(hreq, bind_ports[0]);
+ http_client_request_submit(hreq);
+
+ return TRUE;
+}
+
+/* test */
+
+static void test_reconnect_failure(void)
+{
+ struct http_client_settings http_client_set;
+
+ test_client_defaults(&http_client_set);
+ http_client_set.dns_client_socket_path = "./dns-test";
+ http_client_set.dns_ttl_msecs = 10000;
+ http_client_set.max_idle_time_msecs = 1000;
+ http_client_set.max_attempts = 1;
+ http_client_set.request_timeout_msecs = 1000;
+
+ test_begin("reconnect failure");
+ test_run_client_server(&http_client_set,
+ test_client_reconnect_failure,
+ test_server_reconnect_failure, 1,
+ test_dns_reconnect_failure);
+ test_end();
+}
+
+/*
+ * Multi IP attempts
+ */
+
+/* dns */
+
+static void test_multi_ip_attempts_input(struct server_connection *conn)
+{
+ unsigned int count = 0;
+ const char *line;
+
+ if (!conn->version_sent) {
+ conn->version_sent = TRUE;
+ o_stream_nsend_str(conn->conn.output, "VERSION\tdns\t1\t0\n");
+ }
+
+ while ((line = i_stream_read_next_line(conn->conn.input)) != NULL) {
+ if (str_begins(line, "VERSION"))
+ continue;
+ if (debug)
+ i_debug("DNS REQUEST %u: %s", count, line);
+
+ if (strcmp(line, "IP\ttest1.local") == 0) {
+ o_stream_nsend_str(conn->conn.output,
+ "0\t127.0.0.4\t127.0.0.3\t"
+ "127.0.0.2\t127.0.0.1\n");
+ continue;
+ }
+
+ o_stream_nsend_str(conn->conn.output,
+ "0\t10.255.255.1\t192.168.0.0\t"
+ "192.168.255.255\t127.0.0.1\n");
+ }
+}
+
+static void test_dns_multi_ip_attempts(void)
+{
+ test_server_input = test_multi_ip_attempts_input;
+ test_server_run(0);
+}
+
+/* server */
+
+static void test_server_multi_ip_attempts_input(struct server_connection *conn)
+{
+ string_t *resp;
+
+ resp = t_str_new(512);
+ str_printfa(resp,
+ "HTTP/1.1 200 OK\r\n"
+ "Connection: close\r\n"
+ "\r\n");
+ o_stream_nsend(conn->conn.output, str_data(resp), str_len(resp));
+ server_connection_deinit(&conn);
+}
+
+static void test_server_multi_ip_attempts(unsigned int index)
+{
+ test_server_input = test_server_multi_ip_attempts_input;
+ test_server_run(index);
+}
+
+/* client */
+
+struct _multi_ip_attempts {
+ unsigned int count;
+};
+
+static void
+test_client_multi_ip_attempts_response(const struct http_response *resp,
+ struct _multi_ip_attempts *ctx)
+{
+ test_client_assert_response(resp, resp->status == 200);
+
+ if (--ctx->count == 0) {
+ i_free(ctx);
+ io_loop_stop(ioloop);
+ }
+}
+
+static bool
+test_client_multi_ip_attempts1(const struct http_client_settings *client_set)
+{
+ struct http_client_request *hreq;
+ struct _multi_ip_attempts *ctx;
+
+ ctx = i_new(struct _multi_ip_attempts, 1);
+ ctx->count = 2;
+
+ http_client = http_client_init(client_set);
+
+ hreq = http_client_request(
+ http_client, "GET", "test1.local", "/multi-ip-attempts.txt",
+ test_client_multi_ip_attempts_response, ctx);
+ http_client_request_set_port(hreq, bind_ports[0]);
+ http_client_request_submit(hreq);
+
+ hreq = http_client_request(
+ http_client, "GET", "test1.local", "/multi-ip-attempts2.txt",
+ test_client_multi_ip_attempts_response, ctx);
+ http_client_request_set_port(hreq, bind_ports[0]);
+ http_client_request_submit(hreq);
+
+ return TRUE;
+}
+
+static bool
+test_client_multi_ip_attempts2(const struct http_client_settings *client_set)
+{
+ struct http_client_request *hreq;
+ struct _multi_ip_attempts *ctx;
+
+ ctx = i_new(struct _multi_ip_attempts, 1);
+ ctx->count = 2;
+
+ http_client = http_client_init(client_set);
+
+ hreq = http_client_request(
+ http_client, "GET", "test2.local", "/multi-ip-attempts.txt",
+ test_client_multi_ip_attempts_response, ctx);
+ http_client_request_set_port(hreq, bind_ports[0]);
+ http_client_request_submit(hreq);
+
+ hreq = http_client_request(
+ http_client, "GET", "test2.local", "/multi-ip-attempts2.txt",
+ test_client_multi_ip_attempts_response, ctx);
+ http_client_request_set_port(hreq, bind_ports[0]);
+ http_client_request_submit(hreq);
+
+ return TRUE;
+}
+
+/* test */
+
+static void test_multi_ip_attempts(void)
+{
+ struct http_client_settings http_client_set;
+
+ test_client_defaults(&http_client_set);
+ http_client_set.connect_timeout_msecs = 1000;
+ http_client_set.request_timeout_msecs = 1000;
+ http_client_set.dns_client_socket_path = "./dns-test";
+ http_client_set.max_connect_attempts = 4;
+
+ test_begin("multi IP attempts (connection refused)");
+ test_run_client_server(&http_client_set,
+ test_client_multi_ip_attempts1,
+ test_server_multi_ip_attempts, 1,
+ test_dns_multi_ip_attempts);
+ test_end();
+
+ test_begin("multi IP attempts (connect timeout)");
+ test_run_client_server(&http_client_set,
+ test_client_multi_ip_attempts2,
+ test_server_multi_ip_attempts, 1,
+ test_dns_multi_ip_attempts);
+ test_end();
+
+ http_client_set.soft_connect_timeout_msecs = 100;
+
+ test_begin("multi IP attempts (soft connect timeout)");
+ test_run_client_server(&http_client_set,
+ test_client_multi_ip_attempts2,
+ test_server_multi_ip_attempts, 1,
+ test_dns_multi_ip_attempts);
+ test_end();
+}
+
+/*
+ * Idle connections
+ */
+
+/* server */
+
+struct _idle_connections_sctx {
+ bool eoh;
+};
+
+static int test_idle_connections_init(struct server_connection *conn)
+{
+ struct _idle_connections_sctx *ctx;
+
+ ctx = p_new(conn->pool, struct _idle_connections_sctx, 1);
+ conn->context = ctx;
+ return 0;
+}
+
+static void test_idle_connections_input(struct server_connection *conn)
+{
+ struct _idle_connections_sctx *ctx = conn->context;
+ const char *line;
+
+ while ((line = i_stream_read_next_line(conn->conn.input)) != NULL) {
+ if (*line == '\0') {
+ ctx->eoh = TRUE;
+ break;
+ }
+ }
+
+ if (conn->conn.input->stream_errno != 0) {
+ i_fatal("server: Stream error: %s",
+ i_stream_get_error(conn->conn.input));
+ }
+ if (line == NULL) {
+ if (conn->conn.input->eof)
+ server_connection_deinit(&conn);
+ return;
+ }
+
+ i_assert(ctx->eoh);
+ ctx->eoh = FALSE;
+
+ string_t *resp;
+
+ resp = t_str_new(512);
+ str_printfa(resp,
+ "HTTP/1.1 200 OK\r\n"
+ "Content-Length: 0\r\n"
+ "\r\n");
+ o_stream_nsend(conn->conn.output, str_data(resp), str_len(resp));
+ if (o_stream_flush(conn->conn.output) < 0) {
+ i_fatal("server: Flush error: %s",
+ o_stream_get_error(conn->conn.output));
+ }
+}
+
+static void test_server_idle_connections(unsigned int index)
+{
+ test_server_init = test_idle_connections_init;
+ test_server_input = test_idle_connections_input;
+ test_server_run(index);
+}
+
+/* client */
+
+struct _idle_connections {
+ struct http_client *client;
+ unsigned int max, count;
+ struct timeout *to;
+};
+
+static void
+test_client_idle_connections_response_stage2(const struct http_response *resp,
+ struct _idle_connections *ctx)
+{
+ test_client_assert_response(
+ resp, resp->status == 200);
+
+ if (--ctx->count == 0) {
+ i_free(ctx);
+ io_loop_stop(ioloop);
+ }
+}
+
+static void test_client_idle_connections_stage2_start(struct _idle_connections *ctx)
+{
+ struct http_client_request *hreq;
+ unsigned int i;
+
+ if (debug)
+ i_debug("STAGE 2");
+
+ timeout_remove(&ctx->to);
+
+ ctx->count = ctx->max;
+
+ for (i = 0; i < ctx->count; i++) {
+ hreq = http_client_request(
+ ctx->client, "GET", net_ip2addr(&bind_ip),
+ t_strdup_printf("/idle-connections-stage2-%d.txt", i),
+ test_client_idle_connections_response_stage2, ctx);
+ http_client_request_set_port(hreq, bind_ports[0]);
+ http_client_request_submit(hreq);
+ }
+}
+
+static void
+test_client_idle_connections_response_stage1(const struct http_response *resp,
+ struct _idle_connections *ctx)
+{
+ test_client_assert_response(resp, resp->status == 200);
+
+ if (--ctx->count == 0) {
+ if (debug)
+ i_debug("START STAGE 2");
+ ctx->to = timeout_add_short(
+ 550, test_client_idle_connections_stage2_start, ctx);
+ }
+}
+
+static bool
+test_client_idle_connections(const struct http_client_settings *client_set)
+{
+ struct http_client_request *hreq;
+ struct _idle_connections *ctx;
+ unsigned int i;
+
+ if (debug)
+ i_debug("STAGE 1");
+
+ ctx = i_new(struct _idle_connections, 1);
+ ctx->max = client_set->max_parallel_connections;
+ ctx->count = client_set->max_parallel_connections;
+
+ ctx->client = http_client = http_client_init(client_set);
+
+ for (i = 0; i < ctx->count; i++) {
+ hreq = http_client_request(
+ ctx->client, "GET", net_ip2addr(&bind_ip),
+ t_strdup_printf("/idle-connections-stage1-%d.txt", i),
+ test_client_idle_connections_response_stage1, ctx);
+ http_client_request_set_port(hreq, bind_ports[0]);
+ http_client_request_submit(hreq);
+ }
+
+ return TRUE;
+}
+
+/* test */
+
+static void test_idle_connections(void)
+{
+ struct http_client_settings http_client_set;
+
+ test_client_defaults(&http_client_set);
+ http_client_set.max_idle_time_msecs = 1000;
+
+ test_begin("idle connections (max 1)");
+ http_client_set.max_parallel_connections = 1;
+ test_run_client_server(&http_client_set,
+ test_client_idle_connections,
+ test_server_idle_connections, 1, NULL);
+ test_end();
+
+ test_begin("idle connections (max 2)");
+ http_client_set.max_parallel_connections = 2;
+ test_run_client_server(&http_client_set,
+ test_client_idle_connections,
+ test_server_idle_connections, 1, NULL);
+ test_end();
+
+ test_begin("idle connections (max 4)");
+ http_client_set.max_parallel_connections = 4;
+ test_run_client_server(&http_client_set,
+ test_client_idle_connections,
+ test_server_idle_connections, 1, NULL);
+ test_end();
+
+ test_begin("idle connections (max 8)");
+ http_client_set.max_parallel_connections = 8;
+ test_run_client_server(&http_client_set,
+ test_client_idle_connections,
+ test_server_idle_connections, 1, NULL);
+ test_end();
+}
+
+/*
+ * Idle hosts
+ */
+
+/* dns */
+
+static void
+test_dns_idle_hosts_input(struct server_connection *conn)
+{
+ const char *line;
+
+ if (!conn->version_sent) {
+ conn->version_sent = TRUE;
+ o_stream_nsend_str(conn->conn.output, "VERSION\tdns\t1\t0\n");
+ }
+
+ while ((line = i_stream_read_next_line(conn->conn.input)) != NULL) {
+ if (str_begins(line, "VERSION"))
+ continue;
+ if (debug)
+ i_debug("DNS REQUEST: %s", line);
+
+ if (strcmp(line, "IP\thosta") == 0) {
+ o_stream_nsend_str(conn->conn.output,
+ "0\t127.0.0.1\n");
+ } else {
+ i_sleep_msecs(300);
+ o_stream_nsend_str(
+ conn->conn.output,
+ t_strdup_printf("%d\tFAIL\n", EAI_FAIL));
+ }
+ }
+}
+
+static void test_dns_idle_hosts(void)
+{
+ test_server_input = test_dns_idle_hosts_input;
+ test_server_run(0);
+}
+
+/* server */
+
+struct _idle_hosts_sctx {
+ bool eoh;
+};
+
+static int test_idle_hosts_init(struct server_connection *conn)
+{
+ struct _idle_hosts_sctx *ctx;
+
+ ctx = p_new(conn->pool, struct _idle_hosts_sctx, 1);
+ conn->context = ctx;
+ return 0;
+}
+
+static void test_idle_hosts_input(struct server_connection *conn)
+{
+ struct _idle_hosts_sctx *ctx = conn->context;
+ const char *line;
+
+ while ((line = i_stream_read_next_line(conn->conn.input)) != NULL) {
+ if (*line == '\0') {
+ ctx->eoh = TRUE;
+ break;
+ }
+ }
+
+ if (conn->conn.input->stream_errno != 0) {
+ i_fatal("server: Stream error: %s",
+ i_stream_get_error(conn->conn.input));
+ }
+ if (line == NULL) {
+ if (conn->conn.input->eof)
+ server_connection_deinit(&conn);
+ return;
+ }
+
+ i_assert(ctx->eoh);
+ ctx->eoh = FALSE;
+
+ string_t *resp;
+
+ resp = t_str_new(512);
+ str_printfa(resp,
+ "HTTP/1.1 200 OK\r\n"
+ "Content-Length: 0\r\n"
+ "\r\n");
+ o_stream_nsend(conn->conn.output, str_data(resp), str_len(resp));
+ if (o_stream_flush(conn->conn.output) < 0) {
+ i_fatal("server: Flush error: %s",
+ o_stream_get_error(conn->conn.output));
+ }
+}
+
+static void test_server_idle_hosts(unsigned int index)
+{
+ test_server_init = test_idle_hosts_init;
+ test_server_input = test_idle_hosts_input;
+ test_server_run(index);
+}
+
+/* client */
+
+struct _idle_hosts {
+ struct http_client *client;
+ struct http_client_request *hostb_req;
+ unsigned int count;
+};
+
+static void
+test_client_idle_hosts_response_hosta(const struct http_response *resp,
+ struct _idle_hosts *ctx)
+{
+ test_client_assert_response(resp, resp->status == 200);
+
+ if (--ctx->count == 0) {
+ i_free(ctx);
+ io_loop_stop(ioloop);
+ }
+}
+
+static void
+test_client_idle_hosts_response_hostb(const struct http_response *resp,
+ struct _idle_hosts *ctx)
+{
+ test_client_assert_response(resp,
+ resp->status == HTTP_CLIENT_REQUEST_ERROR_HOST_LOOKUP_FAILED);
+
+ if (http_client_request_try_retry(ctx->hostb_req)) {
+ if (debug)
+ i_debug("retrying");
+ return;
+ }
+
+ ctx->hostb_req = NULL;
+}
+
+static bool
+test_client_idle_hosts(const struct http_client_settings *client_set)
+{
+ struct http_client_request *hreq;
+ struct _idle_hosts *ctx;
+
+ ctx = i_new(struct _idle_hosts, 1);
+ ctx->count = 2;
+
+ ctx->client = http_client = http_client_init(client_set);
+
+ hreq = http_client_request(
+ ctx->client, "GET", "hosta",
+ t_strdup_printf("/idle-hosts-a1.txt"),
+ test_client_idle_hosts_response_hosta, ctx);
+ http_client_request_set_port(hreq, bind_ports[0]);
+ http_client_request_submit(hreq);
+
+ hreq = ctx->hostb_req = http_client_request(
+ ctx->client, "GET", "hostb",
+ t_strdup_printf("/idle-hosts-b.txt"),
+ test_client_idle_hosts_response_hostb, ctx);
+ http_client_request_set_port(hreq, bind_ports[0]);
+ http_client_request_submit(hreq);
+
+ hreq = http_client_request(
+ ctx->client, "GET", "hosta",
+ t_strdup_printf("/idle-hosts-a2.txt"),
+ test_client_idle_hosts_response_hosta, ctx);
+ http_client_request_set_port(hreq, bind_ports[0]);
+ http_client_request_delay_msecs(hreq, 600);
+ http_client_request_submit(hreq);
+
+ return TRUE;
+}
+
+/* test */
+
+static void test_idle_hosts(void)
+{
+ struct http_client_settings http_client_set;
+
+ test_client_defaults(&http_client_set);
+ http_client_set.dns_client_socket_path = "./dns-test";
+ http_client_set.dns_ttl_msecs = 400;
+ http_client_set.max_parallel_connections = 1;
+ http_client_set.max_idle_time_msecs = 100;
+ http_client_set.max_attempts = 2;
+
+ test_begin("idle hosts");
+ test_run_client_server(&http_client_set,
+ test_client_idle_hosts,
+ test_server_idle_hosts, 1,
+ test_dns_idle_hosts);
+ test_end();
+}
+
+/*
+ * All tests
+ */
+
+static void (*const test_functions[])(void) = {
+ test_unconfigured_ssl,
+ test_unconfigured_ssl_abort,
+ test_invalid_url,
+ test_host_lookup_failed,
+ test_connection_refused,
+ test_connection_lost_prematurely,
+ test_connection_timed_out,
+ test_invalid_redirect,
+ test_unseekable_redirect,
+ test_unseekable_retry,
+ test_broken_payload,
+ test_retry_payload,
+ test_connection_lost,
+ test_connection_lost_100,
+ test_connection_lost_sub_ioloop,
+ test_early_success,
+ test_bad_response,
+ test_request_timed_out,
+ test_request_aborted_early,
+ test_request_failed_blocking,
+ test_client_deinit_early,
+ test_retry_with_delay,
+ test_dns_service_failure,
+ test_dns_timeout,
+ test_dns_lookup_failure,
+ test_dns_lookup_ttl,
+ test_peer_reuse_failure,
+ test_reconnect_failure,
+ test_multi_ip_attempts,
+ test_idle_connections,
+ test_idle_hosts,
+ NULL
+};
+
+/*
+ * Test client
+ */
+
+static void test_client_defaults(struct http_client_settings *http_set)
+{
+ /* client settings */
+ i_zero(http_set);
+ http_set->max_idle_time_msecs = 5*1000;
+ http_set->max_parallel_connections = 1;
+ http_set->max_pipelined_requests = 1;
+ http_set->max_redirects = 0;
+ http_set->max_attempts = 1;
+ http_set->debug = debug;
+}
+
+static void test_client_progress_timeout(void *context ATTR_UNUSED)
+{
+ /* Terminate test due to lack of progress */
+ test_assert(FALSE);
+ timeout_remove(&to_client_progress);
+ io_loop_stop(current_ioloop);
+}
+
+static bool
+test_client_init(test_client_init_t client_test,
+ const struct http_client_settings *client_set)
+{
+ i_assert(client_test != NULL);
+ if (!client_test(client_set))
+ return FALSE;
+
+ to_client_progress = timeout_add(CLIENT_PROGRESS_TIMEOUT*1000,
+ test_client_progress_timeout, NULL);
+ return TRUE;
+}
+
+static void test_client_deinit(void)
+{
+ timeout_remove(&to_client_progress);
+
+ if (http_client != NULL)
+ http_client_deinit(&http_client);
+}
+
+static void
+test_client_run(test_client_init_t client_test,
+ const struct http_client_settings *client_set)
+{
+ if (test_client_init(client_test, client_set))
+ io_loop_run(ioloop);
+ test_client_deinit();
+}
+
+/*
+ * Test server
+ */
+
+/* client connection */
+
+static void server_connection_input(struct connection *_conn)
+{
+ struct server_connection *conn = (struct server_connection *)_conn;
+
+ test_server_input(conn);
+}
+
+static void server_connection_init(int fd)
+{
+ struct server_connection *conn;
+ pool_t pool;
+
+ net_set_nonblock(fd, TRUE);
+
+ pool = pool_alloconly_create("server connection", 512);
+ conn = p_new(pool, struct server_connection, 1);
+ conn->pool = pool;
+
+ connection_init_server(server_conn_list, &conn->conn,
+ "server connection", fd, fd);
+
+ if (test_server_init != NULL) {
+ if (test_server_init(conn) != 0)
+ return;
+ }
+}
+
+static void server_connection_deinit(struct server_connection **_conn)
+{
+ struct server_connection *conn = *_conn;
+
+ *_conn = NULL;
+
+ if (test_server_deinit != NULL)
+ test_server_deinit(conn);
+
+ connection_deinit(&conn->conn);
+ pool_unref(&conn->pool);
+}
+
+static void server_connection_destroy(struct connection *_conn)
+{
+ struct server_connection *conn = (struct server_connection *)_conn;
+
+ server_connection_deinit(&conn);
+}
+
+static void server_connection_accept(void *context ATTR_UNUSED)
+{
+ int fd;
+
+ /* accept new client */
+ fd = net_accept(fd_listen, NULL, NULL);
+ if (fd == -1)
+ return;
+ if (fd == -2) {
+ i_fatal("test server: accept() failed: %m");
+ }
+
+ server_connection_init(fd);
+}
+
+/* */
+
+static struct connection_settings server_connection_set = {
+ .input_max_size = SIZE_MAX,
+ .output_max_size = SIZE_MAX,
+ .client = FALSE
+};
+
+static const struct connection_vfuncs server_connection_vfuncs = {
+ .destroy = server_connection_destroy,
+ .input = server_connection_input
+};
+
+static void test_server_run(unsigned int index)
+{
+ server_index = index;
+
+ /* open server socket */
+ io_listen = io_add(fd_listen, IO_READ, server_connection_accept, NULL);
+
+ server_conn_list = connection_list_init(&server_connection_set,
+ &server_connection_vfuncs);
+
+ io_loop_run(ioloop);
+
+ /* close server socket */
+ io_remove(&io_listen);
+
+ connection_list_deinit(&server_conn_list);
+}
+
+/*
+ * Tests
+ */
+
+struct test_server_data {
+ unsigned int index;
+ test_server_init_t server_test;
+};
+
+static int test_open_server_fd(in_port_t *bind_port)
+{
+ int fd = net_listen(&bind_ip, bind_port, 128);
+ if (debug)
+ i_debug("server listening on %u", *bind_port);
+ if (fd == -1) {
+ i_fatal("listen(%s:%u) failed: %m",
+ net_ip2addr(&bind_ip), *bind_port);
+ }
+ return fd;
+}
+
+static int test_run_server(struct test_server_data *data)
+{
+ i_set_failure_prefix("SERVER[%u]: ", data->index + 1);
+
+ if (debug)
+ i_debug("PID=%s", my_pid);
+
+ test_subprocess_notify_signal_send_parent(SIGHUP);
+ ioloop = io_loop_create();
+ data->server_test(data->index);
+ io_loop_destroy(&ioloop);
+
+ if (debug)
+ i_debug("Terminated");
+
+ i_close_fd(&fd_listen);
+ i_free(bind_ports);
+ main_deinit();
+ return 0;
+}
+
+static int test_run_dns(test_dns_init_t dns_test)
+{
+ i_set_failure_prefix("DNS: ");
+
+ if (debug)
+ i_debug("PID=%s", my_pid);
+
+ ioloop = io_loop_create();
+ dns_test();
+ io_loop_destroy(&ioloop);
+
+ if (debug)
+ i_debug("Terminated");
+
+ i_close_fd(&fd_listen);
+ i_free(bind_ports);
+ main_deinit();
+ return 0;
+}
+
+static void
+test_run_client(const struct http_client_settings *client_set,
+ test_client_init_t client_test)
+{
+ i_set_failure_prefix("CLIENT: ");
+
+ if (debug)
+ i_debug("PID=%s", my_pid);
+
+ ioloop = io_loop_create();
+ test_client_run(client_test, client_set);
+ io_loop_destroy(&ioloop);
+
+ if (debug)
+ i_debug("Terminated");
+}
+
+static void
+test_run_client_server(const struct http_client_settings *client_set,
+ test_client_init_t client_test,
+ test_server_init_t server_test,
+ unsigned int server_tests_count,
+ test_dns_init_t dns_test)
+{
+ unsigned int i;
+
+ test_subprocess_notify_signal_reset(SIGHUP);
+ test_server_init = NULL;
+ test_server_deinit = NULL;
+ test_server_input = NULL;
+
+ if (server_tests_count > 0) {
+ int fds[server_tests_count];
+
+ bind_ports = i_new(in_port_t, server_tests_count);
+ for (i = 0; i < server_tests_count; i++)
+ fds[i] = test_open_server_fd(&bind_ports[i]);
+
+ for (i = 0; i < server_tests_count; i++) {
+ struct test_server_data data;
+
+ i_zero(&data);
+ data.index = i;
+ data.server_test = server_test;
+
+ /* Fork server */
+ fd_listen = fds[i];
+ test_subprocess_fork(test_run_server, &data, FALSE);
+ i_close_fd(&fd_listen);
+ test_subprocess_notify_signal_wait(SIGHUP, 10000);
+ test_subprocess_notify_signal_reset(SIGHUP);
+ }
+ }
+
+ if (dns_test != NULL) {
+ int fd;
+
+ i_unlink_if_exists("./dns-test");
+ fd = net_listen_unix("./dns-test", 128);
+ if (fd == -1) {
+ i_fatal("listen(./dns-test) failed: %m");
+ }
+
+ /* Fork DNS service */
+ fd_listen = fd;
+ test_subprocess_fork(test_run_dns, dns_test, FALSE);
+ i_close_fd(&fd_listen);
+ }
+
+ /* Run client */
+ test_run_client(client_set, client_test);
+
+ i_unset_failure_prefix();
+ test_subprocess_kill_all(SERVER_KILL_TIMEOUT_SECS);
+ i_free(bind_ports);
+
+ i_unlink_if_exists("./dns-test");
+}
+
+/*
+ * Main
+ */
+
+static void main_init(void)
+{
+ /* nothing yet */
+}
+
+static void main_deinit(void)
+{
+ /* nothing yet; also called from sub-processes */
+}
+
+int main(int argc, char *argv[])
+{
+ int c;
+ int ret;
+
+ lib_init();
+ main_init();
+
+ while ((c = getopt(argc, argv, "D")) > 0) {
+ switch (c) {
+ case 'D':
+ debug = TRUE;
+ break;
+ default:
+ i_fatal("Usage: %s [-D]", argv[0]);
+ }
+ }
+
+ test_subprocesses_init(debug);
+
+ /* listen on localhost */
+ i_zero(&bind_ip);
+ bind_ip.family = AF_INET;
+ bind_ip.u.ip4.s_addr = htonl(INADDR_LOOPBACK);
+
+ ret = test_run(test_functions);
+
+ test_subprocesses_deinit();
+ main_deinit();
+ lib_deinit();
+
+ return ret;
+}
diff --git a/src/lib-http/test-http-client-request.c b/src/lib-http/test-http-client-request.c
new file mode 100644
index 0000000..5b6fa93
--- /dev/null
+++ b/src/lib-http/test-http-client-request.c
@@ -0,0 +1,95 @@
+/* Copyright (c) 2019 Dovecot authors, see the included COPYING file */
+
+#include "test-lib.h"
+#include "test-common.h"
+#include "str.h"
+#include "http-client-private.h"
+
+static void
+test_http_client_request_callback(const struct http_response *response ATTR_UNUSED,
+ void *context ATTR_UNUSED)
+{
+}
+
+static void test_http_client_request_headers(void)
+{
+ struct http_client_settings set;
+ struct http_client *client;
+ struct http_client_request *req;
+
+ test_begin("http client request headers");
+ i_zero(&set);
+ client = http_client_init(&set);
+ req = http_client_request(client, "GET", "host", "target",
+ test_http_client_request_callback, NULL);
+
+ test_assert(http_client_request_lookup_header(req, "qwe") == NULL);
+
+ /* add the first */
+ http_client_request_add_header(req, "qwe", "value1");
+ test_assert_strcmp(http_client_request_lookup_header(req, "qwe"), "value1");
+ test_assert_strcmp(str_c(req->headers), "qwe: value1\r\n");
+
+ /* replace the first with the same length */
+ http_client_request_add_header(req, "qwe", "234567");
+ test_assert_strcmp(http_client_request_lookup_header(req, "qwe"), "234567");
+ test_assert_strcmp(str_c(req->headers), "qwe: 234567\r\n");
+
+ /* replace the first with smaller length */
+ http_client_request_add_header(req, "qwe", "xyz");
+ test_assert_strcmp(http_client_request_lookup_header(req, "qwe"), "xyz");
+ test_assert_strcmp(str_c(req->headers), "qwe: xyz\r\n");
+
+ /* replace the first with longer length */
+ http_client_request_add_header(req, "qwe", "abcdefg");
+ test_assert_strcmp(http_client_request_lookup_header(req, "qwe"), "abcdefg");
+ test_assert_strcmp(str_c(req->headers), "qwe: abcdefg\r\n");
+
+ /* add the second */
+ http_client_request_add_header(req, "xyz", "1234");
+ test_assert_strcmp(http_client_request_lookup_header(req, "qwe"), "abcdefg");
+ test_assert_strcmp(http_client_request_lookup_header(req, "xyz"), "1234");
+ test_assert_strcmp(str_c(req->headers), "qwe: abcdefg\r\nxyz: 1234\r\n");
+
+ /* replace second */
+ http_client_request_add_header(req, "xyz", "yuiop");
+ test_assert_strcmp(http_client_request_lookup_header(req, "qwe"), "abcdefg");
+ test_assert_strcmp(http_client_request_lookup_header(req, "xyz"), "yuiop");
+ test_assert_strcmp(str_c(req->headers), "qwe: abcdefg\r\nxyz: yuiop\r\n");
+
+ /* replace the first again */
+ http_client_request_add_header(req, "qwe", "1234");
+ test_assert_strcmp(http_client_request_lookup_header(req, "qwe"), "1234");
+ test_assert_strcmp(http_client_request_lookup_header(req, "xyz"), "yuiop");
+ test_assert_strcmp(str_c(req->headers), "qwe: 1234\r\nxyz: yuiop\r\n");
+
+ /* remove the headers */
+ http_client_request_remove_header(req, "qwe");
+ test_assert(http_client_request_lookup_header(req, "qwe") == NULL);
+ test_assert_strcmp(http_client_request_lookup_header(req, "xyz"), "yuiop");
+ test_assert_strcmp(str_c(req->headers), "xyz: yuiop\r\n");
+
+ http_client_request_remove_header(req, "xyz");
+ test_assert(http_client_request_lookup_header(req, "qwe") == NULL);
+ test_assert(http_client_request_lookup_header(req, "xyz") == NULL);
+ test_assert_strcmp(str_c(req->headers), "");
+
+ /* test _add_missing_header() */
+ http_client_request_add_missing_header(req, "foo", "bar");
+ test_assert_strcmp(str_c(req->headers), "foo: bar\r\n");
+ http_client_request_add_missing_header(req, "foo", "123");
+ test_assert_strcmp(str_c(req->headers), "foo: bar\r\n");
+
+ http_client_request_abort(&req);
+ http_client_deinit(&client);
+ test_end();
+}
+
+int main(void)
+{
+ static void (*const test_functions[])(void) = {
+ test_http_client_request_headers,
+ NULL
+ };
+ return test_run(test_functions);
+}
diff --git a/src/lib-http/test-http-client.c b/src/lib-http/test-http-client.c
new file mode 100644
index 0000000..fc24bfa
--- /dev/null
+++ b/src/lib-http/test-http-client.c
@@ -0,0 +1,472 @@
+/* Copyright (c) 2013-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "safe-memset.h"
+#include "ioloop.h"
+#include "istream.h"
+#include "write-full.h"
+#include "http-url.h"
+#include "http-client.h"
+#include "dns-lookup.h"
+#include "iostream-ssl.h"
+#ifdef HAVE_OPENSSL
+#include "iostream-openssl.h"
+#endif
+
+#include <fcntl.h>
+#include <unistd.h>
+
+struct http_test_request {
+ struct io *io;
+ struct istream *payload;
+ bool write_output;
+};
+
+static struct ioloop *ioloop;
+
+static void payload_input(struct http_test_request *req)
+{
+ const unsigned char *data;
+ size_t size;
+ int ret;
+
+ /* read payload */
+ while ((ret=i_stream_read_more(req->payload, &data, &size)) > 0) {
+ i_info("DEBUG: got data (size=%d)", (int)size);
+ if (req->write_output)
+ if (write_full(1, data, size) < 0)
+ i_error("REQUEST PAYLOAD WRITE ERROR: %m");
+ i_stream_skip(req->payload, size);
+ }
+
+ if (ret == 0) {
+ i_info("DEBUG: REQUEST: NEED MORE DATA");
+ /* we will be called again for this request */
+ } else {
+ if (req->payload->stream_errno != 0) {
+ i_error("REQUEST PAYLOAD READ ERROR: %s",
+ i_stream_get_error(req->payload));
+ } else
+ i_info("DEBUG: REQUEST: Finished");
+ io_remove(&req->io);
+ i_stream_unref(&req->payload);
+ i_free(req);
+ }
+}
+
+static void
+got_request_response(const struct http_response *response,
+ struct http_test_request *req)
+{
+ io_loop_stop(ioloop);
+
+ if (response->status / 100 != 2) {
+ i_error("HTTP Request failed: %s", response->reason);
+ i_free(req);
+ /* payload (if any) is skipped implicitly */
+ return;
+ }
+
+ i_info("DEBUG: REQUEST SUCCEEDED: %s", response->reason);
+
+ if (response->payload == NULL) {
+ i_free(req);
+ return;
+ }
+
+ i_info("DEBUG: REQUEST: Got payload");
+ i_stream_ref(response->payload);
+ req->payload = response->payload;
+ req->io = io_add_istream(response->payload, payload_input, req);
+ payload_input(req);
+}
+
+static const char *test_query1 = "data=Frop&submit=Submit";
+static const char *test_query2 = "data=This%20is%20a%20test&submit=Submit";
+static const char *test_query3 = "foo=bar";
+
+static void run_tests(struct http_client *http_client)
+{
+ struct http_client_request *http_req;
+ struct http_test_request *test_req;
+ struct istream *post_payload;
+
+ // JigSAW is useful for testing: http://jigsaw.w3.org/HTTP/
+
+ test_req = i_new(struct http_test_request, 1);
+ http_req = http_client_request(http_client,
+ "GET", "pigeonhole.dovecot.org", "/",
+ got_request_response, test_req);
+ http_client_request_submit(http_req);
+
+ test_req = i_new(struct http_test_request, 1);
+ http_req = http_client_request(http_client,
+ "GET", "pigeonhole.dovecot.org", "/download.html",
+ got_request_response, test_req);
+ http_client_request_submit(http_req);
+
+ test_req = i_new(struct http_test_request, 1);
+ http_req = http_client_request(http_client,
+ "GET", "jigsaw.w3.org", "/HTTP/300/301.html",
+ got_request_response, test_req);
+ http_client_request_submit(http_req);
+
+ test_req = i_new(struct http_test_request, 1);
+ http_req = http_client_request(http_client,
+ "GET", "pigeonhole.dovecot.org", "/frop.html",
+ got_request_response, test_req);
+ http_client_request_submit(http_req);
+
+ test_req = i_new(struct http_test_request, 1);
+ http_req = http_client_request(http_client,
+ "GET", "jigsaw.w3.org", "/HTTP/300/307.html",
+ got_request_response, test_req);
+ http_client_request_submit(http_req);
+
+ test_req = i_new(struct http_test_request, 1);
+ http_req = http_client_request(http_client,
+ "GET", "pigeonhole.dovecot.org", "/documentation.html",
+ got_request_response, test_req);
+ http_client_request_set_urgent(http_req);
+ http_client_request_submit(http_req);
+
+ test_req = i_new(struct http_test_request, 1);
+ http_req = http_client_request(http_client,
+ "GET", "jigsaw.w3.org", "/HTTP/300/302.html",
+ got_request_response, test_req);
+ http_client_request_submit(http_req);
+
+ test_req = i_new(struct http_test_request, 1);
+ http_req = http_client_request(http_client,
+ "POST", "test.dovecot.org", "/http/post/index.php",
+ got_request_response, test_req);
+ post_payload = i_stream_create_from_data
+ ((const unsigned char *)test_query1, strlen(test_query1));
+ http_client_request_set_payload(http_req, post_payload, FALSE);
+ i_stream_unref(&post_payload);
+ http_client_request_add_header(http_req,
+ "Content-Type", "application/x-www-form-urlencoded");
+ http_client_request_submit(http_req);
+
+ test_req = i_new(struct http_test_request, 1);
+ http_req = http_client_request(http_client,
+ "POST", "test.dovecot.org", "/http/post/index.php",
+ got_request_response, test_req);
+ post_payload = i_stream_create_from_data
+ ((const unsigned char *)test_query2, strlen(test_query2));
+ http_client_request_set_payload(http_req, post_payload, TRUE);
+ i_stream_unref(&post_payload);
+ http_client_request_add_header(http_req,
+ "Content-Type", "application/x-www-form-urlencoded");
+ http_client_request_submit(http_req);
+
+ test_req = i_new(struct http_test_request, 1);
+ http_req = http_client_request(http_client,
+ "GET", "pigeonhole.dovecot.org", "/",
+ got_request_response, test_req);
+ http_client_request_set_port(http_req, 81);
+ http_client_request_submit(http_req);
+
+ test_req = i_new(struct http_test_request, 1);
+ http_req = http_client_request(http_client,
+ "HEAD", "pigeonhole.dovecot.org", "/download.html",
+ got_request_response, test_req);
+ http_client_request_submit(http_req);
+
+ test_req = i_new(struct http_test_request, 1);
+ http_req = http_client_request(http_client,
+ "GET", "pigeonhole.dovecot.org", "/",
+ got_request_response, test_req);
+ http_client_request_set_ssl(http_req, TRUE);
+ http_client_request_submit(http_req);
+
+ test_req = i_new(struct http_test_request, 1);
+ http_req = http_client_request(http_client,
+ "GET", "pigeonhole.dovecot.org", "/download.html",
+ got_request_response, test_req);
+ http_client_request_set_ssl(http_req, TRUE);
+ http_client_request_submit(http_req);
+
+ test_req = i_new(struct http_test_request, 1);
+ http_req = http_client_request(http_client,
+ "GET", "pigeonhole.dovecot.org", "/documentation.html",
+ got_request_response, test_req);
+ http_client_request_set_ssl(http_req, TRUE);
+ http_client_request_submit(http_req);
+ http_client_request_abort(&http_req);
+ i_free(test_req);
+
+ test_req = i_new(struct http_test_request, 1);
+ http_req = http_client_request(http_client,
+ "POST", "posttestserver.com", "/post.php",
+ got_request_response, test_req);
+ post_payload = i_stream_create_from_data
+ ((const unsigned char *)test_query1, strlen(test_query1));
+ http_client_request_set_payload(http_req, post_payload, TRUE);
+ i_stream_unref(&post_payload);
+ http_client_request_set_ssl(http_req, TRUE);
+ http_client_request_submit(http_req);
+
+ test_req = i_new(struct http_test_request, 1);
+ http_req = http_client_request(http_client,
+ "POST", "posttestserver.com", "/post.php",
+ got_request_response, test_req);
+ post_payload = i_stream_create_from_data
+ ((const unsigned char *)test_query1, strlen(test_query1));
+ http_client_request_set_payload(http_req, post_payload, TRUE);
+ i_stream_unref(&post_payload);
+ http_client_request_set_ssl(http_req, TRUE);
+ http_client_request_submit(http_req);
+
+ test_req = i_new(struct http_test_request, 1);
+ http_req = http_client_request(http_client,
+ "POST", "posttestserver.com", "/post.php",
+ got_request_response, test_req);
+ post_payload = i_stream_create_from_data
+ ((const unsigned char *)test_query1, strlen(test_query1));
+ http_client_request_set_payload(http_req, post_payload, TRUE);
+ i_stream_unref(&post_payload);
+ http_client_request_set_ssl(http_req, TRUE);
+ http_client_request_submit(http_req);
+
+ test_req = i_new(struct http_test_request, 1);
+ http_req = http_client_request(http_client,
+ "GET", "wiki2.dovecot.org", "/Pigeonhole",
+ got_request_response, test_req);
+ http_client_request_submit(http_req);
+
+ test_req = i_new(struct http_test_request, 1);
+ http_req = http_client_request(http_client,
+ "GET", "jigsaw.w3.org", "/HTTP/ChunkedScript",
+ got_request_response, test_req);
+ http_client_request_submit(http_req);
+
+ test_req = i_new(struct http_test_request, 1);
+ http_req = http_client_request(http_client,
+ "POST", "jigsaw.w3.org", "/HTTP/300/Go_307",
+ got_request_response, test_req);
+ post_payload = i_stream_create_from_data
+ ((const unsigned char *)test_query3, strlen(test_query3));
+ http_client_request_set_payload(http_req, post_payload, FALSE);
+ i_stream_unref(&post_payload);
+ http_client_request_submit(http_req);
+
+ test_req = i_new(struct http_test_request, 1);
+ http_req = http_client_request(http_client,
+ "POST", "jigsaw.w3.org", "/HTTP/300/Go_307",
+ got_request_response, test_req);
+ post_payload = i_stream_create_from_data
+ ((const unsigned char *)test_query3, strlen(test_query3));
+ http_client_request_set_payload(http_req, post_payload, FALSE);
+ i_stream_unref(&post_payload);
+ http_client_request_submit(http_req);
+
+ test_req = i_new(struct http_test_request, 1);
+ http_req = http_client_request(http_client,
+ "POST", "jigsaw.w3.org", "/HTTP/300/Go_307",
+ got_request_response, test_req);
+ post_payload = i_stream_create_from_data
+ ((const unsigned char *)test_query3, strlen(test_query3));
+ http_client_request_set_payload(http_req, post_payload, FALSE);
+ i_stream_unref(&post_payload);
+ http_client_request_submit(http_req);
+
+ test_req = i_new(struct http_test_request, 1);
+ http_req = http_client_request(http_client,
+ "GET", "jigsaw.w3.org", "/HTTP/Basic/",
+ got_request_response, test_req);
+ http_client_request_set_auth_simple
+ (http_req, "guest", "guest");
+ http_client_request_submit(http_req);
+
+ test_req = i_new(struct http_test_request, 1);
+ http_req = http_client_request(http_client,
+ "PUT", "test.dovecot.org", "/http/put/put.php",
+ got_request_response, test_req);
+ post_payload = i_stream_create_file("Makefile.am", 10);
+ http_client_request_set_payload(http_req, post_payload, TRUE);
+ i_stream_unref(&post_payload);
+ http_client_request_submit(http_req);
+
+ test_req = i_new(struct http_test_request, 1);
+ http_req = http_client_request_url_str(http_client,
+ "GET", "https://invalid.dovecot.org/",
+ got_request_response, test_req);
+ http_client_request_submit(http_req);
+}
+
+static void
+test_http_request_init(struct http_client *http_client,
+ const char *method, const char *url_str,
+ struct http_client_request **http_req_r,
+ struct http_test_request **test_req_r)
+{
+ struct http_client_request *http_req;
+ struct http_test_request *test_req;
+ struct http_url *url;
+ const char *error;
+
+ if (http_url_parse(url_str, NULL, 0, pool_datastack_create(),
+ &url, &error) < 0)
+ i_fatal("Invalid URL %s: %s", url_str, error);
+
+ test_req = i_new(struct http_test_request, 1);
+ test_req->write_output = TRUE;
+ http_req = http_client_request(http_client,
+ method, url->host.name,
+ t_strconcat("/", url->path, url->enc_query, NULL),
+ got_request_response, test_req);
+ if (url->port != 0)
+ http_client_request_set_port(http_req, url->port);
+ if (url->have_ssl)
+ http_client_request_set_ssl(http_req, TRUE);
+
+ *http_req_r = http_req;
+ *test_req_r = test_req;
+}
+
+static void run_http_get(struct http_client *http_client, const char *url_str)
+{
+ struct http_client_request *http_req;
+ struct http_test_request *test_req;
+
+ test_http_request_init(http_client, "GET", url_str, &http_req, &test_req);
+ http_client_request_submit(http_req);
+}
+
+static void run_http_post(struct http_client *http_client, const char *url_str,
+ const char *path)
+{
+ struct http_client_request *http_req;
+ struct http_test_request *test_req;
+ struct istream *input;
+
+ test_http_request_init(http_client, "POST", url_str, &http_req, &test_req);
+ input = i_stream_create_file(path, IO_BLOCK_SIZE);
+ http_client_request_set_payload(http_req, input, FALSE);
+ i_stream_unref(&input);
+ http_client_request_submit(http_req);
+}
+
+int main(int argc, char *argv[])
+{
+ struct dns_client *dns_client;
+ struct dns_lookup_settings dns_set;
+ struct http_client_settings http_set;
+ struct http_client_context *http_cctx;
+ struct http_client *http_client1, *http_client2, *http_client3, *http_client4;
+ struct ssl_iostream_settings ssl_set;
+ struct stat st;
+ const char *error;
+
+ lib_init();
+#ifdef HAVE_OPENSSL
+ ssl_iostream_openssl_init();
+#endif
+ ioloop = io_loop_create();
+ io_loop_set_running(ioloop);
+
+ /* kludge: use safe_memset() here since otherwise it's not included in
+ the binary in all systems (but is in others! so linking
+ safe-memset.lo directly causes them to fail.) If safe_memset() isn't
+ included, libssl-iostream plugin loading fails. */
+ i_zero_safe(&dns_set);
+ dns_set.dns_client_socket_path = PKG_RUNDIR"/dns-client";
+ dns_set.timeout_msecs = 30*1000;
+ dns_set.idle_timeout_msecs = UINT_MAX;
+
+ /* check if there is a DNS client */
+ if (access(dns_set.dns_client_socket_path, R_OK|W_OK) == 0) {
+ dns_client = dns_client_init(&dns_set);
+
+ if (dns_client_connect(dns_client, &error) < 0)
+ i_fatal("Couldn't initialize DNS client: %s", error);
+ } else {
+ dns_client = NULL;
+ }
+ i_zero(&ssl_set);
+ ssl_set.allow_invalid_cert = TRUE;
+ if (stat("/etc/ssl/certs", &st) == 0 && S_ISDIR(st.st_mode))
+ ssl_set.ca_dir = "/etc/ssl/certs"; /* debian */
+ if (stat("/etc/ssl/certs", &st) == 0 && S_ISREG(st.st_mode))
+ ssl_set.ca_file = "/etc/pki/tls/cert.pem"; /* redhat */
+
+ i_zero(&http_set);
+ http_set.ssl = &ssl_set;
+ http_set.dns_client = dns_client;
+ http_set.max_idle_time_msecs = 5*1000;
+ http_set.max_parallel_connections = 4;
+ http_set.max_pipelined_requests = 4;
+ http_set.max_redirects = 2;
+ http_set.request_timeout_msecs = 10*1000;
+ http_set.max_attempts = 1;
+ http_set.debug = TRUE;
+ http_set.rawlog_dir = "/tmp/http-test";
+
+ http_cctx = http_client_context_create(&http_set);
+
+ http_client1 = http_client_init_shared(http_cctx, NULL);
+ http_client2 = http_client_init_shared(http_cctx, NULL);
+ http_client3 = http_client_init_shared(http_cctx, NULL);
+ http_client4 = http_client_init_shared(http_cctx, NULL);
+
+ switch (argc) {
+ case 1:
+ run_tests(http_client1);
+ run_tests(http_client2);
+ run_tests(http_client3);
+ run_tests(http_client4);
+ break;
+ case 2:
+ run_http_get(http_client1, argv[1]);
+ run_http_get(http_client2, argv[1]);
+ run_http_get(http_client3, argv[1]);
+ run_http_get(http_client4, argv[1]);
+ break;
+ case 3:
+ run_http_post(http_client1, argv[1], argv[2]);
+ run_http_post(http_client2, argv[1], argv[2]);
+ run_http_post(http_client3, argv[1], argv[2]);
+ run_http_post(http_client4, argv[1], argv[2]);
+ break;
+ default:
+ i_fatal("Too many parameters");
+ }
+
+ for (;;) {
+ bool pending = FALSE;
+
+ if (http_client_get_pending_request_count(http_client1) > 0) {
+ i_debug("Requests still pending in client 1");
+ pending = TRUE;
+ } else if (http_client_get_pending_request_count(http_client2) > 0) {
+ i_debug("Requests still pending in client 2");
+ pending = TRUE;
+ } else if (http_client_get_pending_request_count(http_client3) > 0) {
+ i_debug("Requests still pending in client 3");
+ pending = TRUE;
+ } else if (http_client_get_pending_request_count(http_client4) > 0) {
+ i_debug("Requests still pending in client 4");
+ pending = TRUE;
+ }
+ if (!pending)
+ break;
+ io_loop_run(ioloop);
+ }
+ http_client_deinit(&http_client1);
+ http_client_deinit(&http_client2);
+ http_client_deinit(&http_client3);
+ http_client_deinit(&http_client4);
+
+ http_client_context_unref(&http_cctx);
+
+ if (dns_client != NULL)
+ dns_client_deinit(&dns_client);
+
+ io_loop_destroy(&ioloop);
+ ssl_iostream_context_cache_free();
+#ifdef HAVE_OPENSSL
+ ssl_iostream_openssl_deinit();
+#endif
+ lib_deinit();
+}
diff --git a/src/lib-http/test-http-date.c b/src/lib-http/test-http-date.c
new file mode 100644
index 0000000..1d8150a
--- /dev/null
+++ b/src/lib-http/test-http-date.c
@@ -0,0 +1,223 @@
+/* Copyright (c) 2013-2018 Dovecot authors, see the included COPYING file */
+
+#include "test-lib.h"
+#include "test-common.h"
+#include "http-date.h"
+
+#include <time.h>
+
+struct http_date_test {
+ const char *date_in;
+ const char *date_out;
+
+ struct tm tm;
+};
+
+/* Valid date tests */
+static const struct http_date_test valid_date_tests[] = {
+ /* Preferred format: */
+ {
+ .date_in = "Sun, 11 Nov 2007 09:42:43 GMT",
+ .tm = {
+ .tm_year = 107, .tm_mon = 10, .tm_mday = 11,
+ .tm_hour = 9, .tm_min = 42, .tm_sec = 43 },
+ },{
+ .date_in = "Mon, 17 Aug 1992 13:06:27 GMT",
+ .tm = {
+ .tm_year = 92, .tm_mon = 7, .tm_mday = 17,
+ .tm_hour = 13, .tm_min = 06, .tm_sec = 27 },
+ },{
+ .date_in = "Tue, 03 Sep 1974 04:38:08 GMT",
+ .tm = {
+ .tm_year = 74, .tm_mon = 8, .tm_mday = 3,
+ .tm_hour = 4, .tm_min = 38, .tm_sec = 8 },
+ },{
+ .date_in = "Wed, 07 May 1980 06:20:42 GMT",
+ .tm = {
+ .tm_year = 80, .tm_mon = 4, .tm_mday = 7,
+ .tm_hour = 6, .tm_min = 20, .tm_sec = 42 },
+ },{
+ .date_in = "Thu, 15 Oct 1987 18:30:14 GMT",
+ .tm = {
+ .tm_year = 87, .tm_mon = 9, .tm_mday = 15,
+ .tm_hour = 18, .tm_min = 30, .tm_sec = 14 },
+ },{
+ .date_in = "Fri, 20 Dec 1996 00:20:07 GMT",
+ .tm = {
+ .tm_year = 96, .tm_mon = 11, .tm_mday = 20,
+ .tm_hour = 0, .tm_min = 20, .tm_sec = 7 },
+ },{
+ .date_in = "Sat, 19 Jan 2036 19:52:18 GMT",
+ .tm = {
+ .tm_year = 136, .tm_mon = 0, .tm_mday = 19,
+ .tm_hour = 19, .tm_min = 52, .tm_sec = 18 },
+ },{
+ .date_in = "Mon, 17 Apr 2006 14:41:45 GMT",
+ .tm = {
+ .tm_year = 106, .tm_mon = 3, .tm_mday = 17,
+ .tm_hour = 14, .tm_min = 41, .tm_sec = 45 },
+ },{
+ .date_in = "Sun, 06 Mar 2011 16:18:41 GMT",
+ .tm = {
+ .tm_year = 111, .tm_mon = 2, .tm_mday = 6,
+ .tm_hour = 16, .tm_min = 18, .tm_sec = 41 },
+ },{
+ .date_in = "Sat, 14 Jun 1975 16:09:30 GMT",
+ .tm = {
+ .tm_year = 75, .tm_mon = 5, .tm_mday = 14,
+ .tm_hour = 16, .tm_min = 9, .tm_sec = 30 },
+ },{
+ .date_in = "Fri, 05 Feb 2027 06:53:58 GMT",
+ .tm = {
+ .tm_year = 127, .tm_mon = 1, .tm_mday = 5,
+ .tm_hour = 6, .tm_min = 53, .tm_sec = 58 },
+ },{
+ .date_in = "Mon, 09 Jul 2018 02:24:29 GMT",
+ .tm = {
+ .tm_year = 118, .tm_mon = 6, .tm_mday = 9,
+ .tm_hour = 2, .tm_min = 24, .tm_sec = 29 },
+
+ /* Obsolete formats: */
+ },{
+ .date_in = "Wednesday, 02-Jun-82 16:06:23 GMT",
+ .date_out = "Wed, 02 Jun 1982 16:06:23 GMT",
+ .tm = {
+ .tm_year = 82, .tm_mon = 5, .tm_mday = 2,
+ .tm_hour = 16, .tm_min = 6, .tm_sec = 23 },
+ },{
+ .date_in = "Thursday, 23-May-02 12:16:24 GMT",
+ .date_out = "Thu, 23 May 2002 12:16:24 GMT",
+ .tm = {
+ .tm_year = 102, .tm_mon = 4, .tm_mday = 23,
+ .tm_hour = 12, .tm_min = 16, .tm_sec = 24 },
+ },{
+ .date_in = "Sun Nov 6 08:49:37 1994",
+ .date_out = "Sun, 06 Nov 1994 08:49:37 GMT",
+ .tm = {
+ .tm_year = 94, .tm_mon = 10, .tm_mday = 6,
+ .tm_hour = 8, .tm_min = 49, .tm_sec = 37 },
+ },{
+ .date_in = "Mon Apr 30 02:45:01 2012",
+ .date_out = "Mon, 30 Apr 2012 02:45:01 GMT",
+ .tm = {
+ .tm_year = 112, .tm_mon = 3, .tm_mday = 30,
+ .tm_hour = 2, .tm_min = 45, .tm_sec = 01 },
+ }
+};
+
+static const unsigned int valid_date_test_count = N_ELEMENTS(valid_date_tests);
+
+static void test_http_date_valid(void)
+{
+ unsigned int i;
+
+ for (i = 0; i < valid_date_test_count; i++) T_BEGIN {
+ const char *date_in, *date_out, *pdate_out;
+ const struct tm *tm = &valid_date_tests[i].tm;
+ struct tm ptm;
+ bool result;
+
+ date_in = valid_date_tests[i].date_in;
+ date_out = valid_date_tests[i].date_out == NULL ?
+ date_in : valid_date_tests[i].date_out;
+
+ test_begin(t_strdup_printf("http date valid [%d]", i));
+
+ result = http_date_parse_tm
+ ((const unsigned char *)date_in, strlen(date_in), &ptm);
+ test_out(t_strdup_printf("parse %s", date_in), result);
+ if (result) {
+ bool equal = tm->tm_year == ptm.tm_year && tm->tm_mon == ptm.tm_mon &&
+ tm->tm_mday == ptm.tm_mday && tm->tm_hour == ptm.tm_hour &&
+ tm->tm_min == ptm.tm_min && tm->tm_sec == ptm.tm_sec;
+
+ test_out("valid timestamp", equal);
+
+ pdate_out = http_date_create_tm(&ptm);
+ test_out_reason("valid create", strcmp(date_out, pdate_out) == 0,
+ pdate_out);
+ }
+
+ test_end();
+ } T_END;
+}
+
+/* Invalid date tests */
+static const char *invalid_date_tests[] = {
+ "Mom, 09 Jul 2018 02:24:29 GMT",
+ "Mon; 09 Jul 2018 02:24:29 GMT",
+ "Mon, 09 Jul 2018 02:24:29 GMT",
+ "Mon, 90 Jul 2018 02:24:29 GMT",
+ "Mon, 090 Jul 2018 02:24:29 GMT",
+ "Mon, 09 Jul 2018 02:24:29 GMT",
+ "Mon, 09 Lul 2018 02:24:29 GMT",
+ "Mon, 09 July 2018 02:24:29 GMT",
+ "Mon, 09 Jul 2018 02:24:29 GMT",
+ "Mon, 09 Jul 22018 02:24:29 GMT",
+ "Mon, 09 Jul 2018 02:24:29 GMT",
+ "Mon, 09 Jul 2018 032:24:29 GMT",
+ "Mon, 09 Jul 2018 02:224:29 GMT",
+ "Mon, 09 Jul 2018 02:24:239 GMT",
+ "Mon, 09 Jul 2018 02;24:29 GMT",
+ "Mon, 09 Jul 2018 02:24;29 GMT",
+ "Mon, 09 Jul 2018 45:24:29 GMT",
+ "Mon, 09 Jul 2018 02:90:29 GMT",
+ "Mon, 09 Jul 2018 02:24:84 GMT",
+ "Mon, 09 Jul 2018 02:24:29 GMT",
+ "Mon, 09 Jul 2018 02:24:29 UTC",
+ "Mon, 09 Jul 2018 02:24:29 GM",
+ "Mon, 09 Jul 2018 02:24:29 GMTREE",
+ "Thu, 23-May-02 12:16:24 GMT",
+ "Thursday; 23-May-02 12:16:24 GMT",
+ "Thursday, 223-May-02 12:16:24 GMT",
+ "Thursday, 23-Mays-02 12:16:24 GMT",
+ "Thursday, 23-May-2002 12:16:24 GMT",
+ "Thursday, 23-May-02 122:16:24 GMT",
+ "Thursday, 23-May-02 12:164:24 GMT",
+ "Thursday, 23-May-02 12:16:244 GMT",
+ "Thursday, 23-May-02 12:16:24 EET",
+ "Sunday Nov 6 08:49:37 1994",
+ "Sun Nov 6 08:49:37 1994",
+ "Sun November 6 08:49:37 1994",
+ "Sun Nov 6 08:49:37 1994",
+ "Sun Nov 16 08:49:37 1994",
+ "Sun Nov 16 08:49:37 1994",
+ "Sun Nov 6 082:49:37 1994",
+ "Sun Nov 6 08:492:37 1994",
+ "Sun Nov 6 08:49:137 1994",
+ "Sun Nov 6 08:49:37 19914",
+ "Sun Nov 6 08:49:37 0000",
+};
+
+static const unsigned int invalid_date_test_count = N_ELEMENTS(invalid_date_tests);
+
+static void test_http_date_invalid(void)
+{
+ unsigned int i;
+
+ for (i = 0; i < invalid_date_test_count; i++) T_BEGIN {
+ const char *date_in;
+ struct tm tm;
+ bool result;
+
+ date_in = invalid_date_tests[i];
+
+ test_begin(t_strdup_printf("http date invalid [%d]", i));
+
+ result = http_date_parse_tm
+ ((const unsigned char *)date_in, strlen(date_in), &tm);
+ test_out(t_strdup_printf("parse %s", date_in), !result);
+
+ test_end();
+ } T_END;
+}
+
+int main(void)
+{
+ static void (*const test_functions[])(void) = {
+ test_http_date_valid,
+ test_http_date_invalid,
+ NULL
+ };
+ return test_run(test_functions);
+}
diff --git a/src/lib-http/test-http-header-parser.c b/src/lib-http/test-http-header-parser.c
new file mode 100644
index 0000000..0550039
--- /dev/null
+++ b/src/lib-http/test-http-header-parser.c
@@ -0,0 +1,381 @@
+/* Copyright (c) 2013-2018 Dovecot authors, see the included COPYING file */
+
+#include "test-lib.h"
+#include "str-sanitize.h"
+#include "istream.h"
+#include "test-common.h"
+#include "http-response.h"
+#include "http-header-parser.h"
+
+#include <time.h>
+
+struct http_header_parse_result {
+ const char *name;
+ const char *value;
+};
+
+struct http_header_parse_test {
+ const char *header;
+ struct http_header_limits limits;
+ enum http_header_parse_flags flags;
+ const struct http_header_parse_result *fields;
+};
+
+/* Valid header tests */
+
+static const struct http_header_parse_result valid_header_parse_result1[] = {
+ { "Date", "Sat, 06 Oct 2012 16:01:44 GMT" },
+ { "Server", "Apache/2.2.16 (Debian)" },
+ { "Last-Modified", "Mon, 30 Jul 2012 11:09:28 GMT" },
+ { "Etag", "\"3d24677-3261-4c60a1863aa00\"" },
+ { "Accept-Ranges", "bytes" },
+ { "Vary", "Accept-Encoding" },
+ { "Content-Encoding", "gzip" },
+ { "Content-Length", "4092" },
+ { "Keep-Alive", "timeout=15, max=100" },
+ { "Connection", "Keep-Alive" },
+ { "Content-Type", "text/html" },
+ { NULL, NULL }
+};
+
+static const struct http_header_parse_result valid_header_parse_result2[] = {
+ { "Host", "p5-lrqzb4yavu4l7nagydw-428649-i2-v6exp3-ds.metric.example.com" },
+ { "User-Agent", "Mozilla/5.0 (Windows NT 6.1; WOW64; rv:15.0)" },
+ { "Accept", "image/png,image/*;q=0.8,*/*;q=0.5" },
+ { "Accept-Language", "en-us,en;q=0.5" },
+ { "Accept-Encoding", "gzip, deflate" },
+ { "DNT", "1" },
+ { "Connection", "keep-alive" },
+ { "Referer", "http://www.example.nl/" },
+ { NULL, NULL }
+};
+
+static const struct http_header_parse_result valid_header_parse_result3[] = {
+ { "Date", "Sat, 06 Oct 2012 17:12:37 GMT" },
+ { "Server", "Apache/2.2.16 (Debian) PHP/5.3.3-7+squeeze14 with"
+ " Suhosin-Patch proxy_html/3.0.1 mod_python/3.3.1 Python/2.6.6"
+ " mod_ssl/2.2.16 OpenSSL/0.9.8o mod_perl/2.0.4 Perl/v5.10.1" },
+ { "WWW-Authenticate", "Basic realm=\"Munin\"" },
+ { "Vary", "Accept-Encoding" },
+ { "Content-Encoding", "gzip" },
+ { "Content-Length", "445" },
+ { "Keep-Alive", "timeout=15, max=98" },
+ { "Connection", "Keep-Alive" },
+ { "Content-Type", "text/html; charset=iso-8859-1" },
+ { NULL, NULL }
+};
+
+static const struct http_header_parse_result valid_header_parse_result4[] = {
+ { "Age", "58" },
+ { "Date", "Sun, 04 Aug 2013 09:33:09 GMT" },
+ { "Expires", "Sun, 04 Aug 2013 09:34:08 GMT" },
+ { "Cache-Control", "max-age=60" },
+ { "Content-Length", "17336" },
+ { "Connection", "Keep-Alive" },
+ { "Via", "NS-CACHE-9.3" },
+ { "Server", "Apache" },
+ { "Vary", "Host" },
+ { "Last-Modified", "Sun, 04 Aug 2013 09:33:07 GMT" },
+ { "Content-Type", "text/html; charset=utf-8" },
+ { "Content-Encoding", "gzip" },
+ { NULL, NULL }
+};
+
+static const struct http_header_parse_result valid_header_parse_result5[] = {
+ { NULL, NULL }
+};
+
+static const struct http_header_parse_result valid_header_parse_result6[] = {
+ { "X-Frop", "This text\x80 contains obs-text\x81 characters" },
+ { NULL, NULL }
+};
+
+static const struct http_header_parse_result valid_header_parse_result7[] = {
+ { "X-Frop", "This text contains invalid characters" },
+ { NULL, NULL }
+};
+
+static const struct http_header_parse_test valid_header_parse_tests[] = {
+ { .header =
+ "Date: Sat, 06 Oct 2012 16:01:44 GMT\r\n"
+ "Server: Apache/2.2.16 (Debian)\r\n"
+ "Last-Modified: Mon, 30 Jul 2012 11:09:28 GMT\r\n"
+ "Etag: \"3d24677-3261-4c60a1863aa00\"\r\n"
+ "Accept-Ranges: bytes\r\n"
+ "Vary: Accept-Encoding\r\n"
+ "Content-Encoding: gzip\r\n"
+ "Content-Length: 4092\r\n"
+ "Keep-Alive: timeout=15, max=100\r\n"
+ "Connection: Keep-Alive\r\n"
+ "Content-Type: text/html\r\n"
+ "\r\n",
+ .fields = valid_header_parse_result1
+ },{
+ .header =
+ "Host: p5-lrqzb4yavu4l7nagydw-428649-i2-v6exp3-ds.metric.example.com\n"
+ "User-Agent:Mozilla/5.0 (Windows NT 6.1; WOW64; rv:15.0)\n"
+ "Accept:\t\timage/png,image/*;q=0.8,*/*;q=0.5\n"
+ "Accept-Language:\ten-us,en;q=0.5\n"
+ "Accept-Encoding: \t\tgzip, deflate\n"
+ "DNT: 1\n"
+ "Connection: \t\tkeep-alive\n"
+ "Referer: http://www.example.nl/\n"
+ "\n",
+ .fields = valid_header_parse_result2
+ },{
+ .header =
+ "Date: Sat, 06 Oct 2012 17:12:37 GMT\r\n"
+ "Server: Apache/2.2.16 (Debian) PHP/5.3.3-7+squeeze14 with\r\n"
+ " Suhosin-Patch proxy_html/3.0.1 mod_python/3.3.1 Python/2.6.6\r\n"
+ " mod_ssl/2.2.16 OpenSSL/0.9.8o mod_perl/2.0.4 Perl/v5.10.1\r\n"
+ "WWW-Authenticate: Basic realm=\"Munin\"\r\n"
+ "Vary: Accept-Encoding\r\n"
+ "Content-Encoding: gzip\r\n"
+ "Content-Length: 445\r\n"
+ "Keep-Alive: timeout=15, max=98\r\n"
+ "Connection: Keep-Alive\r\n"
+ "Content-Type: text/html; charset=iso-8859-1\r\n"
+ "\r\n",
+ .fields = valid_header_parse_result3
+ },{
+ .header =
+ "Age: 58 \r\n"
+ "Date: Sun, 04 Aug 2013 09:33:09 GMT\r\n"
+ "Expires: Sun, 04 Aug 2013 09:34:08 GMT\r\n"
+ "Cache-Control: max-age=60 \r\n"
+ "Content-Length: 17336 \r\n"
+ "Connection: Keep-Alive\r\n"
+ "Via: NS-CACHE-9.3\r\n"
+ "Server: Apache\r\n"
+ "Vary: Host\r\n"
+ "Last-Modified: Sun, 04 Aug 2013 09:33:07 GMT\r\n"
+ "Content-Type: text/html; charset=utf-8\r\n"
+ "Content-Encoding: gzip\r\n"
+ "\r\n",
+ .fields = valid_header_parse_result4,
+ .limits = {
+ .max_size = 340,
+ .max_field_size = 46,
+ .max_fields = 12
+ }
+ },{
+ .header =
+ "\r\n",
+ .fields = valid_header_parse_result5
+ },{
+ .header =
+ "X-Frop: This text\x80 contains obs-text\x81 characters\r\n"
+ "\r\n",
+ .fields = valid_header_parse_result6
+ },{
+ .header =
+ "X-Frop: This text\x01 contains invalid\x7f characters\r\n"
+ "\r\n",
+ .fields = valid_header_parse_result7
+ }
+};
+
+static const unsigned int valid_header_parse_test_count = N_ELEMENTS(valid_header_parse_tests);
+
+static void test_http_header_parse_valid(void)
+{
+ unsigned int i;
+
+ for (i = 0; i < valid_header_parse_test_count; i++) T_BEGIN {
+ struct istream *input;
+ struct http_header_parser *parser;
+ const struct http_header_limits *limits;
+ const char *header, *field_name, *error = NULL;
+ const unsigned char *field_data;
+ size_t field_size;
+ int ret;
+ unsigned int j, pos, header_len;
+
+ header = valid_header_parse_tests[i].header;
+ header_len = strlen(header);
+ limits = &valid_header_parse_tests[i].limits;
+ input = test_istream_create_data(header, header_len);
+ parser = http_header_parser_init(input, limits,
+ valid_header_parse_tests[i].flags);
+
+ test_begin(t_strdup_printf("http header valid [%d]", i));
+
+ j = 0; pos = 0; test_istream_set_size(input, 0);
+ while ((ret=http_header_parse_next_field
+ (parser, &field_name, &field_data, &field_size, &error)) >= 0) {
+ const struct http_header_parse_result *result;
+ const char *field_value;
+
+ if (ret == 0) {
+ if (pos == header_len)
+ break;
+ test_istream_set_size(input, ++pos);
+ continue;
+ }
+
+ if (field_name == NULL) break;
+
+ result = &valid_header_parse_tests[i].fields[j];
+ field_value = t_strndup(field_data, field_size);
+
+ if (result->name == NULL) {
+ test_out_reason("valid", FALSE, t_strdup_printf
+ ("%s: %s", field_name, str_sanitize(field_value, 100)));
+ break;
+ }
+
+ test_out_reason("valid",
+ strcmp(result->name, field_name) == 0 &&
+ strcmp(result->value, field_value) == 0,
+ t_strdup_printf("%s: %s", field_name,
+ str_sanitize(field_value, 100)));
+ j++;
+ }
+
+ test_out_reason("parse success", ret > 0, error);
+ test_end();
+ i_stream_unref(&input);
+ http_header_parser_deinit(&parser);
+ } T_END;
+}
+
+static const struct http_header_parse_test invalid_header_parse_tests[] = {
+ {
+ .header =
+ "Date: Sat, 06 Oct 2012 16:01:44 GMT\r\n"
+ "Server : Apache/2.2.16 (Debian)\r\n"
+ "Last-Modified: Mon, 30 Jul 2012 11:09:28 GMT\r\n"
+ "\r\n"
+ },{
+ .header =
+ "Date: Sat, 06 Oct 2012 17:18:22 GMT\r\n"
+ "Server: Apache/2.2.3 (CentOS)\r\n"
+ "X Powered By: PHP/5.3.6\r\n"
+ "\r\n"
+ },{
+ .header =
+ "Host: www.example.com\n\r"
+ "Accept: image/png,image/*;q=0.8,*/*;q=0.5\n\r"
+ "Accept-Language: en-us,en;q=0.5\n\r"
+ "Accept-Encoding: gzip, deflate\n\r"
+ "\n\r"
+ },{
+ .header =
+ "Host: p5-lrqzb4yavu4l7nagydw-428649-i2-v6exp3-ds.metric.example.com\n"
+ "User-Agent:Mozilla/5.0 (Windows NT 6.1; WOW64; rv:15.0)\n"
+ "Accept:\t\timage/png,image/*;q=0.8,*/\1;q=0.5\n"
+ "\n",
+ .flags = HTTP_HEADER_PARSE_FLAG_STRICT
+ },{
+ .header =
+ "Date: Sat, 06 Oct 2012 17:18:22 GMT\r\n"
+ "Server: Apache/2.2.3\177 (CentOS)\r\n"
+ "\r\n",
+ .flags = HTTP_HEADER_PARSE_FLAG_STRICT
+ },{
+ .header =
+ "Date: Sat, 06 Oct 2012 17:12:37 GMT\r\n"
+ "Server: Apache/2.2.16 (Debian) PHP/5.3.3-7+squeeze14 with\r\n"
+ "Suhosin-Patch proxy_html/3.0.1 mod_python/3.3.1 Python/2.6.6\r\n"
+ "mod_ssl/2.2.16 OpenSSL/0.9.8o mod_perl/2.0.4 Perl/v5.10.1\r\n"
+ "\r\n"
+ },{
+ .header =
+ "Date: Sat, 06 Oct 2012 17:12:37 GMT\r\n"
+ },{
+ .header =
+ "Age: 58 \r\n"
+ "Date: Sun, 04 Aug 2013 09:33:09 GMT\r\n"
+ "Expires: Sun, 04 Aug 2013 09:34:08 GMT\r\n"
+ "Cache-Control: max-age=60 \r\n"
+ "Content-Length: 17336 \r\n"
+ "Connection: Keep-Alive\r\n"
+ "Via: NS-CACHE-9.3\r\n"
+ "Server: Apache\r\n"
+ "Vary: Host\r\n"
+ "Last-Modified: Sun, 04 Aug 2013 09:33:07 GMT\r\n"
+ "Content-Type: text/html; charset=utf-8\r\n"
+ "Content-Encoding: gzip\r\n"
+ "\r\n",
+ .limits = { .max_size = 339 }
+ },{
+ .header =
+ "Age: 58 \r\n"
+ "Date: Sun, 04 Aug 2013 09:33:09 GMT\r\n"
+ "Expires: Sun, 04 Aug 2013 09:34:08 GMT\r\n"
+ "Cache-Control: max-age=60 \r\n"
+ "Content-Length: 17336 \r\n"
+ "Connection: Keep-Alive\r\n"
+ "Via: NS-CACHE-9.3\r\n"
+ "Server: Apache\r\n"
+ "Vary: Host\r\n"
+ "Last-Modified: Sun, 04 Aug 2013 09:33:07 GMT\r\n"
+ "Content-Type: text/html; charset=utf-8\r\n"
+ "Content-Encoding: gzip\r\n"
+ "\r\n",
+ .fields = valid_header_parse_result4,
+ .limits = { .max_field_size = 45 }
+ },{
+ .header =
+ "Age: 58 \r\n"
+ "Date: Sun, 04 Aug 2013 09:33:09 GMT\r\n"
+ "Expires: Sun, 04 Aug 2013 09:34:08 GMT\r\n"
+ "Cache-Control: max-age=60 \r\n"
+ "Content-Length: 17336 \r\n"
+ "Connection: Keep-Alive\r\n"
+ "Via: NS-CACHE-9.3\r\n"
+ "Server: Apache\r\n"
+ "Vary: Host\r\n"
+ "Last-Modified: Sun, 04 Aug 2013 09:33:07 GMT\r\n"
+ "Content-Type: text/html; charset=utf-8\r\n"
+ "Content-Encoding: gzip\r\n"
+ "\r\n",
+ .fields = valid_header_parse_result4,
+ .limits = { .max_fields = 11 }
+ }
+};
+
+static const unsigned int invalid_header_parse_test_count = N_ELEMENTS(invalid_header_parse_tests);
+
+static void test_http_header_parse_invalid(void)
+{
+ unsigned int i;
+
+ for (i = 0; i < invalid_header_parse_test_count; i++) T_BEGIN {
+ struct istream *input;
+ struct http_header_parser *parser;
+ const struct http_header_limits *limits;
+ const char *header, *field_name, *error = NULL;
+ const unsigned char *field_data;
+ size_t field_size;
+ int ret;
+
+ header = invalid_header_parse_tests[i].header;
+ limits = &invalid_header_parse_tests[i].limits;
+ input = i_stream_create_from_data(header, strlen(header));
+ parser = http_header_parser_init(input, limits,
+ invalid_header_parse_tests[i].flags);
+
+ test_begin(t_strdup_printf("http header invalid [%d]", i));
+
+ while ((ret=http_header_parse_next_field
+ (parser, &field_name, &field_data, &field_size, &error)) > 0) {
+ if (field_name == NULL) break;
+ }
+
+ test_out_reason("parse failure", ret < 0, error);
+ test_end();
+ i_stream_unref(&input);
+ http_header_parser_deinit(&parser);
+ } T_END;
+}
+
+int main(void)
+{
+ static void (*const test_functions[])(void) = {
+ test_http_header_parse_valid,
+ test_http_header_parse_invalid,
+ NULL
+ };
+ return test_run(test_functions);
+}
diff --git a/src/lib-http/test-http-payload.c b/src/lib-http/test-http-payload.c
new file mode 100644
index 0000000..4e63861
--- /dev/null
+++ b/src/lib-http/test-http-payload.c
@@ -0,0 +1,2445 @@
+/* Copyright (c) 2013-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "str.h"
+#include "llist.h"
+#include "path-util.h"
+#include "hostpid.h"
+#include "ioloop.h"
+#include "istream.h"
+#include "ostream.h"
+#include "istream-crlf.h"
+#include "iostream-temp.h"
+#include "iostream-ssl.h"
+#include "iostream-ssl-test.h"
+#ifdef HAVE_OPENSSL
+#include "iostream-openssl.h"
+#endif
+#include "connection.h"
+#include "test-common.h"
+#include "test-subprocess.h"
+#include "http-url.h"
+#include "http-request.h"
+#include "http-server.h"
+#include "http-client.h"
+
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <fcntl.h>
+#include <unistd.h>
+#include <dirent.h>
+
+#define CLIENT_PROGRESS_TIMEOUT 30
+#define SERVER_KILL_TIMEOUT_SECS 20
+
+enum payload_handling {
+ PAYLOAD_HANDLING_LOW_LEVEL,
+ PAYLOAD_HANDLING_FORWARD,
+ PAYLOAD_HANDLING_HANDLER,
+};
+
+static bool debug = FALSE;
+static bool small_socket_buffers = FALSE;
+static const char *failure = NULL;
+static struct timeout *to_continue = NULL;
+static bool files_finished = FALSE;
+static bool running_continue = FALSE;
+
+static struct test_settings {
+ /* client */
+ bool client_blocking;
+ unsigned int max_pending;
+ unsigned int client_ioloop_nesting;
+ bool request_100_continue;
+ unsigned int parallel_clients;
+ bool parallel_clients_global;
+ size_t read_client_partial;
+ bool unknown_size;
+
+ /* server */
+ bool server_blocking;
+ bool server_ostream;
+ enum payload_handling server_payload_handling;
+ size_t read_server_partial;
+ bool server_cork;
+
+ bool ssl;
+} tset;
+
+static struct ip_addr bind_ip;
+static in_port_t bind_port = 0;
+static int fd_listen = -1;
+static struct ioloop *ioloop_nested = NULL;
+static unsigned ioloop_nested_first = 0;
+static unsigned ioloop_nested_last = 0;
+static unsigned ioloop_nested_depth = 0;
+
+static void main_deinit(void);
+
+/*
+ * Test settings
+ */
+
+static void test_init_defaults(void)
+{
+ i_zero(&tset);
+ tset.max_pending = 200;
+ tset.server_payload_handling = PAYLOAD_HANDLING_FORWARD;
+ tset.parallel_clients = 1;
+}
+
+/*
+ * Test files
+ */
+static const char unsafe_characters[] = "\"<>#%{}|\\^~[]` ;/?:@=&";
+
+static ARRAY_TYPE(const_string) files;
+static pool_t files_pool;
+
+static void test_files_read_dir(const char *path)
+{
+ DIR *dirp;
+
+ /* open the directory */
+ if ((dirp = opendir(path)) == NULL) {
+ if (errno == ENOENT || errno == EACCES)
+ return;
+ i_fatal("test files: "
+ "failed to open directory %s: %m", path);
+ }
+
+ /* read entries */
+ for (;;) {
+ const char *file;
+ struct dirent *dp;
+ struct stat st;
+
+ errno = 0;
+ if ((dp = readdir(dirp)) == NULL)
+ break;
+ if (*dp->d_name == '.' ||
+ dp->d_name[strcspn(dp->d_name, unsafe_characters)] != '\0')
+ continue;
+
+ file = t_abspath_to(dp->d_name, path);
+ if (stat(file, &st) == 0) {
+ if (S_ISREG(st.st_mode)) {
+ file += 2; /* skip "./" */
+ file = p_strdup(files_pool, file);
+ array_push_back(&files, &file);
+ } else if (S_ISDIR(st.st_mode)) {
+ test_files_read_dir(file);
+ }
+ }
+ }
+
+ if (errno != 0)
+ i_fatal("test files: "
+ "failed to read directory %s: %m", path);
+
+ /* Close the directory */
+ if (closedir(dirp) < 0)
+ i_error("test files: "
+ "failed to close directory %s: %m", path);
+}
+
+static void test_files_init(void)
+{
+ /* initialize file array */
+ files_pool = pool_alloconly_create(
+ MEMPOOL_GROWING"http_server_request", 4096);
+ p_array_init(&files, files_pool, 512);
+
+ /* obtain all filenames */
+ test_files_read_dir(".");
+}
+
+static void test_files_deinit(void)
+{
+ pool_unref(&files_pool);
+}
+
+static struct istream *
+test_file_open(const char *path, unsigned int *status_r, const char **reason_r)
+ ATTR_NULL(2, 3)
+{
+ int fd;
+
+ if (status_r != NULL)
+ *status_r = 200;
+ if (reason_r != NULL)
+ *reason_r = "OK";
+
+ fd = open(path, O_RDONLY);
+ if (fd < 0) {
+ if (debug)
+ i_debug("test files: open(%s) failed: %m", path);
+
+ switch (errno) {
+ case EFAULT:
+ case ENOENT:
+ if (status_r != NULL)
+ *status_r = 404;
+ if (reason_r != NULL)
+ *reason_r = "Not Found";
+ break;
+ case EISDIR:
+ case EACCES:
+ if (status_r != NULL)
+ *status_r = 403;
+ if (reason_r != NULL)
+ *reason_r = "Forbidden";
+ break;
+ default:
+ if (status_r != NULL)
+ *status_r = 500;
+ if (reason_r != NULL)
+ *reason_r = "Internal Server Error";
+ }
+ return NULL;
+ }
+
+ return i_stream_create_fd_autoclose(&fd, 40960);
+}
+
+/*
+ * Test server
+ */
+
+struct client {
+ pool_t pool;
+ struct client *prev, *next;
+
+ struct http_server_connection *http_conn;
+};
+
+struct client_request {
+ struct client *client;
+ struct http_server_request *server_req;
+
+ const char *path;
+
+ struct istream *data;
+ struct istream *payload_input;
+ struct ostream *payload_output;
+ struct io *io;
+
+ bool all_sent:1;
+};
+
+static const struct http_server_callbacks http_callbacks;
+static struct http_server *http_server;
+
+static struct io *io_listen;
+static struct client *clients;
+
+/* location: /succes */
+
+static void client_handle_success_request(struct client_request *creq)
+{
+ struct http_server_request *req = creq->server_req;
+ const struct http_request *hreq = http_server_request_get(req);
+ struct http_server_response *resp;
+
+ if (strcmp(hreq->method, "GET") != 0) {
+ http_server_request_fail(req,
+ 405, "Method Not Allowed");
+ return;
+ }
+
+ resp = http_server_response_create(req, 200, "OK");
+ http_server_response_submit(resp);
+}
+
+/* location: /download/... */
+
+static void
+client_handle_download_request(struct client_request *creq,
+ const char *path)
+{
+ struct http_server_request *req = creq->server_req;
+ const struct http_request *hreq = http_server_request_get(req);
+ struct http_server_response *resp;
+ const char *fpath, *reason;
+ struct istream *fstream;
+ struct ostream *output;
+ unsigned int status;
+ int ret;
+
+ if (strcmp(hreq->method, "GET") != 0) {
+ http_server_request_fail(req,
+ 405, "Method Not Allowed");
+ return;
+ }
+
+ fpath = t_strconcat(".", path, NULL);
+
+ if (debug) {
+ i_debug("test server: download: "
+ "sending payload for %s", fpath);
+ }
+
+ fstream = test_file_open(fpath, &status, &reason);
+ if (fstream == NULL) {
+ http_server_request_fail(req, status, reason);
+ return;
+ }
+
+ resp = http_server_response_create(req, 200, "OK");
+ http_server_response_add_header(resp, "Content-Type", "text/plain");
+
+ if (tset.server_blocking) {
+ output = http_server_response_get_payload_output(
+ resp, IO_BLOCK_SIZE, TRUE);
+
+ switch (o_stream_send_istream(output, fstream)) {
+ case OSTREAM_SEND_ISTREAM_RESULT_WAIT_INPUT:
+ case OSTREAM_SEND_ISTREAM_RESULT_WAIT_OUTPUT:
+ i_unreached();
+ case OSTREAM_SEND_ISTREAM_RESULT_FINISHED:
+ /* finish it */
+ ret = o_stream_finish(output);
+ i_assert(ret != 0);
+ if (ret > 0)
+ break;
+ /* fall through */
+ case OSTREAM_SEND_ISTREAM_RESULT_ERROR_OUTPUT:
+ i_assert(output->stream_errno != 0);
+ i_fatal("test server: download: "
+ "write(%s) failed: %s",
+ o_stream_get_name(output),
+ o_stream_get_error(output));
+ case OSTREAM_SEND_ISTREAM_RESULT_ERROR_INPUT:
+ i_assert(fstream->stream_errno != 0);
+ i_fatal("test server: download: "
+ "read(%s) failed: %s",
+ i_stream_get_name(fstream),
+ i_stream_get_error(fstream));
+ }
+
+ if (debug) {
+ i_debug("test server: download: "
+ "finished sending blocking payload for %s"
+ "(%"PRIuUOFF_T":%"PRIuUOFF_T")",
+ fpath, fstream->v_offset, output->offset);
+ }
+
+ o_stream_destroy(&output);
+ } else {
+ http_server_response_set_payload(resp, fstream);
+ http_server_response_submit(resp);
+ }
+ i_stream_unref(&fstream);
+}
+
+/* location: /echo */
+
+static int client_request_echo_send_more(struct client_request *creq)
+{
+ struct ostream *output = creq->payload_output;
+ enum ostream_send_istream_result res;
+ uoff_t offset;
+ int ret;
+
+ if ((ret = o_stream_flush(output)) <= 0) {
+ if (ret < 0) {
+ i_fatal("test server: echo: "
+ "write(%s) failed for %s (flush): %s",
+ o_stream_get_name(output), creq->path,
+ o_stream_get_error(output));
+ }
+ return ret;
+ }
+
+ if (creq->all_sent) {
+ if (debug) {
+ i_debug("test server: echo: "
+ "flushed all payload for %s", creq->path);
+ }
+ i_stream_unref(&creq->data);
+ o_stream_destroy(&creq->payload_output);
+ return 1;
+ }
+
+ i_assert(output != NULL);
+ i_assert(creq->data != NULL);
+
+ offset = creq->data->v_offset;
+ o_stream_set_max_buffer_size(output, IO_BLOCK_SIZE);
+ res = o_stream_send_istream(output, creq->data);
+ o_stream_set_max_buffer_size(output, SIZE_MAX);
+
+ i_assert(creq->data->v_offset >= offset);
+ if (debug) {
+ i_debug("test server: echo: sent data for %s "
+ "(sent %"PRIuUOFF_T", buffered %zu)",
+ creq->path, (uoff_t)(creq->data->v_offset - offset),
+ o_stream_get_buffer_used_size(output));
+ }
+
+ switch (res) {
+ case OSTREAM_SEND_ISTREAM_RESULT_FINISHED:
+ /* finish it */
+ creq->all_sent = TRUE;
+ if ((ret = o_stream_finish(output)) < 0) {
+ i_fatal("test server: echo: "
+ "write(%s) failed for %s (finish): %s",
+ o_stream_get_name(output), creq->path,
+ o_stream_get_error(output));
+ }
+ if (debug) {
+ i_debug("test server: echo: "
+ "finished sending payload for %s", creq->path);
+ }
+ if (ret == 0)
+ return 0;
+ if (debug) {
+ i_debug("test server: echo: "
+ "flushed all payload for %s", creq->path);
+ }
+ i_stream_unref(&creq->data);
+ o_stream_destroy(&creq->payload_output);
+ return 1;
+ case OSTREAM_SEND_ISTREAM_RESULT_WAIT_INPUT:
+ i_unreached();
+ case OSTREAM_SEND_ISTREAM_RESULT_WAIT_OUTPUT:
+ if (debug) {
+ i_debug("test server echo: "
+ "partially sent payload for %s", creq->path);
+ }
+ return 1;
+ case OSTREAM_SEND_ISTREAM_RESULT_ERROR_INPUT:
+ i_fatal("test server: echo: "
+ "read(%s) failed for %s: %s",
+ i_stream_get_name(creq->data), creq->path,
+ i_stream_get_error(creq->data));
+ case OSTREAM_SEND_ISTREAM_RESULT_ERROR_OUTPUT:
+ i_fatal("test server: echo: "
+ "write(%s) failed for %s: %s",
+ o_stream_get_name(output), creq->path,
+ o_stream_get_error(output));
+ }
+ i_unreached();
+}
+
+static void
+client_request_echo_ostream_nonblocking(struct client_request *creq,
+ struct http_server_response *resp,
+ struct istream *data)
+{
+ creq->data = data;
+ i_stream_ref(data);
+
+ creq->payload_output = http_server_response_get_payload_output(
+ resp, IO_BLOCK_SIZE, FALSE);
+ if (tset.server_cork)
+ o_stream_cork(creq->payload_output);
+ o_stream_set_flush_callback(creq->payload_output,
+ client_request_echo_send_more, creq);
+ o_stream_set_flush_pending(creq->payload_output, TRUE);
+}
+
+static void
+client_request_echo_blocking(struct client_request *creq,
+ struct http_server_response *resp,
+ struct istream *input)
+{
+ const unsigned char *data;
+ size_t size;
+ int ret;
+
+ while ((ret = i_stream_read_more(input, &data, &size)) > 0) {
+ ret = http_server_response_send_payload(&resp, data, size);
+ i_assert(ret <= 0);
+ if (ret < 0)
+ break;
+ i_stream_skip(input, size);
+ }
+ i_assert(ret < 0);
+ if (input->stream_errno != 0) {
+ i_fatal("test server: echo: "
+ "read(%s) failed for %s: %s",
+ i_stream_get_name(input), creq->path,
+ i_stream_get_error(input));
+ } else if (i_stream_have_bytes_left(input)) {
+ i_fatal("test server: echo: "
+ "failed to send all blocking payload for %s",
+ creq->path);
+ }
+
+ /* finish it */
+ if (http_server_response_finish_payload(&resp) < 0) {
+ i_fatal("test server: echo: "
+ "failed to finish blocking payload for %s", creq->path);
+ }
+
+ if (debug) {
+ i_debug("test server: echo: "
+ "sent all payload for %s", creq->path);
+ }
+}
+
+static void
+client_request_echo_ostream_blocking(struct client_request *creq,
+ struct http_server_response *resp,
+ struct istream *input)
+{
+ struct ostream *payload_output;
+ int ret;
+
+ payload_output = http_server_response_get_payload_output(
+ resp, IO_BLOCK_SIZE, TRUE);
+
+ if (tset.server_cork)
+ o_stream_cork(payload_output);
+
+ switch (o_stream_send_istream(payload_output, input)) {
+ case OSTREAM_SEND_ISTREAM_RESULT_WAIT_INPUT:
+ case OSTREAM_SEND_ISTREAM_RESULT_WAIT_OUTPUT:
+ i_unreached();
+ case OSTREAM_SEND_ISTREAM_RESULT_FINISHED:
+ /* finish it */
+ ret = o_stream_finish(payload_output);
+ i_assert(ret != 0);
+ if (ret > 0)
+ break;
+ /* fall through */
+ case OSTREAM_SEND_ISTREAM_RESULT_ERROR_OUTPUT:
+ i_assert(payload_output->stream_errno != 0);
+ i_fatal("test server: echo: "
+ "write(%s) failed for %s: %s",
+ o_stream_get_name(payload_output), creq->path,
+ o_stream_get_error(payload_output));
+ case OSTREAM_SEND_ISTREAM_RESULT_ERROR_INPUT:
+ i_assert(input->stream_errno != 0);
+ i_fatal("test server: echo: "
+ "read(%s) failed for %s: %s",
+ i_stream_get_name(input), creq->path,
+ i_stream_get_error(input));
+ }
+
+ if (debug) {
+ i_debug("test server: echo: "
+ "sent all payload for %s", creq->path);
+ }
+
+ o_stream_destroy(&payload_output);
+}
+
+static void client_request_finish_payload_in(struct client_request *creq)
+{
+ struct http_server_response *resp;
+ struct istream *payload_input;
+
+ payload_input =
+ iostream_temp_finish(&creq->payload_output, 4096);
+
+ if (debug) {
+ i_debug("test server: echo: "
+ "finished receiving payload for %s", creq->path);
+ }
+
+ resp = http_server_response_create(creq->server_req, 200, "OK");
+ http_server_response_add_header(resp, "Content-Type", "text/plain");
+
+ if (tset.server_ostream) {
+ client_request_echo_ostream_nonblocking(creq, resp,
+ payload_input);
+ } else {
+ http_server_response_set_payload(resp, payload_input);
+ http_server_response_submit(resp);
+ }
+
+ i_stream_unref(&payload_input);
+}
+
+static void client_request_read_echo(struct client_request *creq)
+{
+ enum ostream_send_istream_result res;
+
+ o_stream_set_max_buffer_size(creq->payload_output, IO_BLOCK_SIZE);
+ res = o_stream_send_istream(creq->payload_output, creq->payload_input);
+ o_stream_set_max_buffer_size(creq->payload_output, SIZE_MAX);
+
+ switch (res) {
+ case OSTREAM_SEND_ISTREAM_RESULT_FINISHED:
+ break;
+ case OSTREAM_SEND_ISTREAM_RESULT_WAIT_INPUT:
+ case OSTREAM_SEND_ISTREAM_RESULT_WAIT_OUTPUT:
+ return;
+ case OSTREAM_SEND_ISTREAM_RESULT_ERROR_INPUT:
+ i_fatal("test server: echo: "
+ "Failed to read all echo payload [%s]",
+ creq->path);
+ case OSTREAM_SEND_ISTREAM_RESULT_ERROR_OUTPUT:
+ i_fatal("test server: echo: "
+ "Failed to write all echo payload [%s]",
+ creq->path);
+ }
+
+ client_request_finish_payload_in(creq);
+ i_stream_unref(&creq->payload_input);
+}
+
+static void client_request_read_echo_more(struct client_request *creq)
+{
+ client_request_read_echo(creq);
+
+ if (creq->payload_input != NULL)
+ return;
+
+ io_remove(&creq->io);
+
+ if (debug) {
+ i_debug("test server: echo: "
+ "finished receiving payload for %s",
+ creq->path);
+ }
+}
+
+static void
+client_handle_echo_request(struct client_request *creq,
+ const char *path)
+{
+ struct http_server_request *req = creq->server_req;
+ const struct http_request *hreq = http_server_request_get(req);
+ struct http_server_response *resp;
+ struct ostream *payload_output;
+ uoff_t size;
+
+ creq->path = p_strdup(http_server_request_get_pool(req), path);
+
+ if (strcmp(hreq->method, "PUT") != 0) {
+ http_server_request_fail(req,
+ 405, "Method Not Allowed");
+ return;
+ }
+
+ size = 0;
+ if (http_request_get_payload_size(hreq, &size) > 0 && size == 0) {
+ if (debug) {
+ i_debug("test server: echo: "
+ "empty payload for %s", creq->path);
+ }
+
+ resp = http_server_response_create(creq->server_req, 200, "OK");
+ http_server_response_add_header(
+ resp, "Content-Type", "text/plain");
+ http_server_response_submit(resp);
+ return;
+ }
+
+ payload_output = iostream_temp_create("/tmp/test-http-server", 0);
+
+ if (tset.server_blocking) {
+ struct istream *payload_input;
+
+ payload_input =
+ http_server_request_get_payload_input(req, TRUE);
+
+ if (tset.read_server_partial > 0) {
+ struct istream *partial =
+ i_stream_create_limit(payload_input,
+ tset.read_server_partial);
+ i_stream_unref(&payload_input);
+ payload_input = partial;
+ }
+
+ if (o_stream_send_istream(payload_output, payload_input) !=
+ OSTREAM_SEND_ISTREAM_RESULT_FINISHED) {
+ i_fatal("test server: echo: "
+ "failed to receive blocking echo payload");
+ }
+ i_stream_unref(&payload_input);
+
+ payload_input = iostream_temp_finish(&payload_output, 4096);
+
+ if (debug) {
+ i_debug("test server: echo: "
+ "finished receiving blocking payload for %s",
+ path);
+ }
+
+ resp = http_server_response_create(req, 200, "OK");
+ http_server_response_add_header(resp,
+ "Content-Type", "text/plain");
+
+ if (tset.server_ostream) {
+ client_request_echo_ostream_blocking(creq, resp,
+ payload_input);
+ } else {
+ client_request_echo_blocking(creq, resp, payload_input);
+ }
+ i_stream_unref(&payload_input);
+ } else {
+ creq->payload_output = payload_output;
+
+ switch (tset.server_payload_handling) {
+ case PAYLOAD_HANDLING_LOW_LEVEL:
+ creq->payload_input =
+ http_server_request_get_payload_input(req, FALSE);
+
+ if (tset.read_server_partial > 0) {
+ struct istream *partial =
+ i_stream_create_limit(creq->payload_input,
+ tset.read_server_partial);
+ i_stream_unref(&creq->payload_input);
+ creq->payload_input = partial;
+ }
+
+ creq->io = io_add_istream(creq->payload_input,
+ client_request_read_echo_more, creq);
+ client_request_read_echo_more(creq);
+ break;
+ case PAYLOAD_HANDLING_FORWARD:
+ http_server_request_forward_payload(req,
+ payload_output, SIZE_MAX,
+ client_request_finish_payload_in, creq);
+ break;
+ case PAYLOAD_HANDLING_HANDLER:
+ creq->payload_input =
+ http_server_request_get_payload_input(req, FALSE);
+ http_server_request_handle_payload(req,
+ client_request_read_echo, creq);
+ break;
+ }
+ }
+}
+
+/* request */
+
+static void http_server_request_destroyed(struct client_request *creq);
+
+static struct client_request *
+client_request_init(struct client *client,
+ struct http_server_request *req)
+{
+ struct client_request *creq;
+ pool_t pool = http_server_request_get_pool(req);
+
+ http_server_request_ref(req);
+
+ creq = p_new(pool, struct client_request, 1);
+ creq->client = client;
+ creq->server_req = req;
+
+ http_server_request_set_destroy_callback(req,
+ http_server_request_destroyed, creq);
+
+ return creq;
+}
+
+static void client_request_deinit(struct client_request **_creq)
+{
+ struct client_request *creq = *_creq;
+ struct http_server_request *req = creq->server_req;
+
+ *_creq = NULL;
+
+ i_stream_unref(&creq->data);
+ i_stream_unref(&creq->payload_input);
+ io_remove(&creq->io);
+
+ http_server_request_unref(&req);
+}
+
+static void http_server_request_destroyed(struct client_request *creq)
+{
+ client_request_deinit(&creq);
+}
+
+static void
+client_handle_request(void *context,
+ struct http_server_request *req)
+{
+ const struct http_request *hreq = http_server_request_get(req);
+ const char *path = hreq->target.url->path, *p;
+ struct client *client = (struct client *)context;
+ struct client_request *creq;
+
+ if (debug) {
+ i_debug("test server: request method=`%s' path=`%s'",
+ hreq->method, path);
+ }
+
+ creq = client_request_init(client, req);
+
+ if (strcmp(path, "/success") == 0) {
+ client_handle_success_request(creq);
+ return;
+ }
+
+ if ((p = strchr(path+1, '/')) == NULL) {
+ http_server_request_fail(req, 404, "Not found");
+ return;
+ }
+ if (strncmp(path, "/download", p-path) == 0) {
+ client_handle_download_request(creq, p);
+ return;
+ }
+ if (strncmp(path, "/echo", p-path) == 0) {
+ client_handle_echo_request(creq, p);
+ return;
+ }
+
+ http_server_request_fail(req, 404, "Not found");
+ return;
+}
+
+/* client connection */
+
+static void client_connection_destroy(void *context, const char *reason);
+
+static const struct http_server_callbacks http_callbacks = {
+ .connection_destroy = client_connection_destroy,
+ .handle_request = client_handle_request,
+};
+
+static void client_init(int fd)
+{
+ struct client *client;
+ pool_t pool;
+
+ net_set_nonblock(fd, TRUE);
+
+ pool = pool_alloconly_create("client", 512);
+ client = p_new(pool, struct client, 1);
+ client->pool = pool;
+
+ client->http_conn = http_server_connection_create(
+ http_server, fd, fd, tset.ssl, &http_callbacks, client);
+ DLLIST_PREPEND(&clients, client);
+}
+
+static void client_deinit(struct client **_client)
+{
+ struct client *client = *_client;
+
+ *_client = NULL;
+
+ DLLIST_REMOVE(&clients, client);
+
+ if (client->http_conn != NULL) {
+ http_server_connection_close(&client->http_conn,
+ "deinit");
+ }
+ pool_unref(&client->pool);
+}
+
+static void
+client_connection_destroy(void *context, const char *reason ATTR_UNUSED)
+{
+ struct client *client = context;
+
+ client->http_conn = NULL;
+ client_deinit(&client);
+}
+
+static void client_accept(void *context ATTR_UNUSED)
+{
+ int fd;
+
+ for (;;) {
+ /* accept new client */
+ if ((fd = net_accept(fd_listen, NULL, NULL)) < 0) {
+ if (errno == EAGAIN)
+ break;
+ if (errno == ECONNABORTED)
+ continue;
+ i_fatal("test server: accept() failed: %m");
+ }
+
+ client_init(fd);
+ }
+}
+
+/* */
+
+static void test_server_init(const struct http_server_settings *server_set)
+{
+ /* open server socket */
+ io_listen = io_add(fd_listen, IO_READ, client_accept, NULL);
+
+ http_server = http_server_init(server_set);
+}
+
+static void test_server_deinit(void)
+{
+ /* close server socket */
+ io_remove(&io_listen);
+
+ /* deinitialize */
+ http_server_deinit(&http_server);
+}
+
+/*
+ * Test client
+ */
+
+struct test_client_request {
+ int refcount;
+
+ struct test_client_request *prev, *next;
+ struct http_client *client;
+ struct http_client_request *hreq;
+
+ struct io *io;
+ struct istream *payload;
+ struct istream *file_in, *file_out;
+ unsigned int files_idx;
+};
+
+static struct http_client **http_clients;
+static struct test_client_request *client_requests;
+static unsigned int client_files_first, client_files_last;
+struct timeout *to_client_progress = NULL;
+
+static struct test_client_request *
+test_client_request_new(struct http_client *client)
+{
+ struct test_client_request *tcreq;
+
+ tcreq = i_new(struct test_client_request, 1);
+ tcreq->refcount = 1;
+ tcreq->client = client;
+ DLLIST_PREPEND(&client_requests, tcreq);
+
+ return tcreq;
+}
+
+static void test_client_request_ref(struct test_client_request *tcreq)
+{
+ tcreq->refcount++;
+}
+
+static void test_client_request_unref(struct test_client_request **_tcreq)
+{
+ struct test_client_request *tcreq = *_tcreq;
+
+ *_tcreq = NULL;
+
+ i_assert(tcreq->refcount > 0);
+ if (--tcreq->refcount > 0)
+ return;
+
+ io_remove(&tcreq->io);
+ i_stream_unref(&tcreq->payload);
+ i_stream_unref(&tcreq->file_in);
+ i_stream_unref(&tcreq->file_out);
+
+ DLLIST_REMOVE(&client_requests, tcreq);
+ i_free(tcreq);
+}
+
+static void test_client_request_destroy(struct test_client_request *tcreq)
+{
+ test_client_request_unref(&tcreq);
+}
+
+static void test_client_switch_ioloop(void)
+{
+ struct test_client_request *tcreq;
+
+ if (to_continue != NULL)
+ to_continue = io_loop_move_timeout(&to_continue);
+ if (to_client_progress != NULL)
+ to_client_progress = io_loop_move_timeout(&to_client_progress);
+
+ for (tcreq = client_requests; tcreq != NULL;
+ tcreq = tcreq->next) {
+ if (tcreq->io != NULL)
+ tcreq->io = io_loop_move_io(&tcreq->io);
+ if (tcreq->payload != NULL)
+ i_stream_switch_ioloop(tcreq->payload);
+ }
+}
+
+static void test_client_progress_timeout(void *context ATTR_UNUSED)
+{
+ /* Terminate test due to lack of progress */
+ failure = "Test is hanging";
+ timeout_remove(&to_client_progress);
+ io_loop_stop(current_ioloop);
+}
+
+static void
+test_client_create_clients(const struct http_client_settings *client_set)
+{
+ struct http_client_context *http_context = NULL;
+ unsigned int i;
+
+ if (!small_socket_buffers) {
+ to_client_progress = timeout_add(
+ CLIENT_PROGRESS_TIMEOUT*1000,
+ test_client_progress_timeout, NULL);
+ }
+
+ if (!tset.parallel_clients_global)
+ http_context = http_client_context_create(client_set);
+
+ if (tset.parallel_clients < 1)
+ tset.parallel_clients = 1;
+ http_clients = i_new(struct http_client *, tset.parallel_clients);
+ for (i = 0; i < tset.parallel_clients; i++) {
+ http_clients[i] = (tset.parallel_clients_global ?
+ http_client_init(client_set) :
+ http_client_init_shared(http_context, NULL));
+ }
+
+ if (!tset.parallel_clients_global)
+ http_client_context_unref(&http_context);
+}
+
+/* download */
+
+static void test_client_download_continue(void);
+
+static void test_client_download_finished(struct test_client_request *tcreq)
+{
+ const char **paths;
+ unsigned int files_idx = tcreq->files_idx;
+ unsigned int count;
+
+ paths = array_get_modifiable(&files, &count);
+ i_assert(files_idx < count);
+ i_assert(client_files_first < count);
+ i_assert(paths[files_idx] != NULL);
+
+ paths[files_idx] = NULL;
+ test_client_download_continue();
+}
+
+static void
+test_client_download_payload_input(struct test_client_request *tcreq)
+{
+ struct istream *payload = tcreq->payload;
+ const unsigned char *pdata, *fdata;
+ size_t psize, fsize, pleft;
+ off_t ret;
+
+ if (to_client_progress != NULL)
+ timeout_reset(to_client_progress);
+
+ /* read payload */
+ while ((ret = i_stream_read_more(payload, &pdata, &psize)) > 0) {
+ if (debug) {
+ i_debug("test client: download: "
+ "got data for [%u] (size=%d)",
+ tcreq->files_idx, (int)psize);
+ }
+ /* compare with file on disk */
+ pleft = psize;
+ while ((ret = i_stream_read_more(tcreq->file_in,
+ &fdata, &fsize)) > 0 &&
+ pleft > 0) {
+ fsize = (fsize > pleft ? pleft : fsize);
+ if (memcmp(pdata, fdata, fsize) != 0) {
+ i_fatal("test client: download: "
+ "received data does not match file "
+ "(%"PRIuUOFF_T":%"PRIuUOFF_T")",
+ payload->v_offset,
+ tcreq->file_in->v_offset);
+ }
+ i_stream_skip(tcreq->file_in, fsize);
+ pleft -= fsize;
+ pdata += fsize;
+ }
+ if (ret < 0 && tcreq->file_in->stream_errno != 0) {
+ i_fatal("test client: download: "
+ "failed to read file: %s",
+ i_stream_get_error(tcreq->file_in));
+ }
+ i_stream_skip(payload, psize);
+ }
+
+ if (ret == 0) {
+ if (debug) {
+ i_debug("test client: download: "
+ "need more data for [%u]",
+ tcreq->files_idx);
+ }
+ /* we will be called again for this request */
+ } else {
+ (void)i_stream_read(tcreq->file_in);
+ if (payload->stream_errno != 0) {
+ i_fatal("test client: download: "
+ "failed to read request payload: %s",
+ i_stream_get_error(payload));
+ } if (i_stream_have_bytes_left(tcreq->file_in)) {
+ if (i_stream_read_more(tcreq->file_in,
+ &fdata, &fsize) <= 0)
+ fsize = 0;
+ i_fatal("test client: download: "
+ "payload ended prematurely "
+ "(at least %zu bytes left)", fsize);
+ } else if (debug) {
+ i_debug("test client: download: "
+ "finished request for [%u]",
+ tcreq->files_idx);
+ }
+
+ /* finished */
+ tcreq->payload = NULL;
+ test_client_download_finished(tcreq);
+
+ /* dereference payload stream; finishes the request */
+ i_stream_unref(&tcreq->file_in);
+ io_remove(&tcreq->io); /* holds a reference too */
+ i_stream_unref(&payload);
+ }
+}
+
+static void
+test_client_download_response(const struct http_response *resp,
+ struct test_client_request *tcreq)
+{
+ const char **paths;
+ const char *path;
+ unsigned int count, status;
+ struct istream *fstream;
+ const char *reason;
+
+ if (debug) {
+ i_debug("test client: download: got response for [%u]",
+ tcreq->files_idx);
+ }
+
+ if (to_client_progress != NULL)
+ timeout_reset(to_client_progress);
+
+ paths = array_get_modifiable(&files, &count);
+ i_assert(tcreq->files_idx < count);
+ i_assert(client_files_first < count);
+ path = paths[tcreq->files_idx];
+ i_assert(path != NULL);
+
+ if (debug) {
+ i_debug("test client: download: path for [%u]: %s",
+ tcreq->files_idx, path);
+ }
+
+ fstream = test_file_open(path, &status, &reason);
+ i_assert(fstream != NULL);
+
+ if (status != resp->status) {
+ i_fatal("test client: download: "
+ "got wrong response for %s: %u %s "
+ "(expected: %u %s)", path,
+ resp->status, resp->reason, status, reason);
+ }
+
+ if (resp->status / 100 != 2) {
+ if (debug) {
+ i_debug("test client: download: "
+ "HTTP request for %s failed: %u %s",
+ path, resp->status, resp->reason);
+ }
+ i_stream_unref(&fstream);
+ test_client_download_finished(tcreq);
+ return;
+ }
+
+ if (resp->payload == NULL) {
+ if (debug) {
+ i_debug("test client: download: "
+ "no payload for %s [%u]",
+ path, tcreq->files_idx);
+ }
+ i_stream_unref(&fstream);
+ test_client_download_finished(tcreq);
+ return;
+ }
+
+ i_assert(fstream != NULL);
+ if (tset.read_client_partial == 0) {
+ i_stream_ref(resp->payload);
+ tcreq->payload = resp->payload;
+ tcreq->file_in = fstream;
+ } else {
+ struct istream *payload = resp->payload;
+ tcreq->payload = i_stream_create_limit(
+ payload, tset.read_client_partial);
+ tcreq->file_in = i_stream_create_limit(
+ fstream, tset.read_client_partial);
+ i_stream_unref(&fstream);
+ }
+
+ tcreq->io = io_add_istream(tcreq->payload,
+ test_client_download_payload_input, tcreq);
+ test_client_download_payload_input(tcreq);
+}
+
+static void test_client_download_continue(void)
+{
+ struct test_client_request *tcreq;
+ struct http_client_request *hreq;
+ const char *const *paths;
+ unsigned int count;
+
+ paths = array_get(&files, &count);
+ i_assert(client_files_first <= count);
+ i_assert(client_files_last <= count);
+
+ i_assert(client_files_first <= client_files_last);
+ for (; client_files_first < client_files_last &&
+ paths[client_files_first] == NULL; client_files_first++)
+
+ if (debug) {
+ i_debug("test client: download: received until [%u]",
+ client_files_first-1);
+ }
+
+ if (client_files_first >= count) {
+ io_loop_stop(current_ioloop);
+ return;
+ }
+
+ for (; (client_files_last < count &&
+ (client_files_last - client_files_first) < tset.max_pending);
+ client_files_last++) {
+ struct http_client *http_client =
+ http_clients[client_files_last % tset.parallel_clients];
+ const char *path = paths[client_files_last];
+
+ tcreq = test_client_request_new(http_client);
+ tcreq->files_idx = client_files_last;
+
+ if (debug) {
+ i_debug("test client: download: retrieving %s [%u]",
+ path, tcreq->files_idx);
+ }
+ hreq = tcreq->hreq = http_client_request(
+ http_client, "GET", net_ip2addr(&bind_ip),
+ t_strconcat("/download/", path, NULL),
+ test_client_download_response, tcreq);
+ http_client_request_set_port(hreq, bind_port);
+ http_client_request_set_ssl(hreq, tset.ssl);
+ http_client_request_set_destroy_callback(
+ hreq, test_client_request_destroy, tcreq);
+ http_client_request_submit(hreq);
+ }
+}
+
+static void test_client_download(const struct http_client_settings *client_set)
+{
+ /* create client(s) */
+ test_client_create_clients(client_set);
+
+ /* start querying server */
+ client_files_first = client_files_last = 0;
+ test_client_download_continue();
+}
+
+/* echo */
+
+static void test_client_echo_continue(void *context);
+
+static void test_client_echo_finished(struct test_client_request *tcreq)
+{
+ unsigned int files_idx = tcreq->files_idx;
+ const char **paths;
+ unsigned int count;
+
+ paths = array_get_modifiable(&files, &count);
+ i_assert(files_idx < count);
+ i_assert(client_files_first < count);
+ i_assert(paths[files_idx] != NULL);
+
+ if (tcreq->file_out != NULL)
+ return;
+ if (tcreq->file_in != NULL)
+ return;
+
+ if (debug) {
+ i_debug("test client: echo: finished [%u]: %s",
+ files_idx, paths[files_idx]);
+ }
+
+ paths[files_idx] = NULL;
+ files_finished = TRUE;
+ if (!running_continue && to_continue == NULL) {
+ to_continue = timeout_add_short(0,
+ test_client_echo_continue, NULL);
+ }
+}
+
+static void test_client_echo_payload_input(struct test_client_request *tcreq)
+{
+ struct istream *payload = tcreq->payload;
+ const unsigned char *pdata, *fdata;
+ size_t psize, fsize, pleft;
+ off_t ret;
+
+ if (to_client_progress != NULL)
+ timeout_reset(to_client_progress);
+
+ /* read payload */
+ while ((ret = i_stream_read_more(payload, &pdata, &psize)) > 0) {
+ if (debug) {
+ i_debug("test client: echo: "
+ "got data for [%u] (size=%d)",
+ tcreq->files_idx, (int)psize);
+ }
+ /* compare with file on disk */
+ pleft = psize;
+ while ((ret = i_stream_read_more(tcreq->file_in,
+ &fdata, &fsize)) > 0 &&
+ pleft > 0) {
+ fsize = (fsize > pleft ? pleft : fsize);
+ if (memcmp(pdata, fdata, fsize) != 0) {
+ i_fatal("test client: echo: "
+ "received data does not match file "
+ "(%"PRIuUOFF_T":%"PRIuUOFF_T")",
+ payload->v_offset,
+ tcreq->file_in->v_offset);
+ }
+ i_stream_skip(tcreq->file_in, fsize);
+ pleft -= fsize;
+ pdata += fsize;
+ }
+ if (ret < 0 && tcreq->file_in->stream_errno != 0) {
+ i_fatal("test client: echo: "
+ "failed to read file: %s",
+ i_stream_get_error(tcreq->file_in));
+ }
+ i_stream_skip(payload, psize);
+ }
+
+ if (ret == 0) {
+ if (debug) {
+ i_debug("test client: echo: "
+ "need more data for [%u]",
+ tcreq->files_idx);
+ }
+ /* we will be called again for this request */
+ } else {
+ (void)i_stream_read(tcreq->file_in);
+ if (payload->stream_errno != 0) {
+ i_fatal("test client: echo: "
+ "failed to read request payload: %s",
+ i_stream_get_error(payload));
+ } if (i_stream_have_bytes_left(tcreq->file_in)) {
+ if (i_stream_read_more(tcreq->file_in,
+ &fdata, &fsize) <= 0)
+ fsize = 0;
+ i_fatal("test client: echo: "
+ "payload ended prematurely "
+ "(at least %zu bytes left)", fsize);
+ } else if (debug) {
+ i_debug("test client: echo: "
+ "finished request for [%u]",
+ tcreq->files_idx);
+ }
+
+ /* finished */
+ tcreq->payload = NULL;
+ i_stream_unref(&tcreq->file_in);
+ test_client_echo_finished(tcreq);
+
+ /* dereference payload stream; finishes the request */
+ io_remove(&tcreq->io); /* holds a reference too */
+ i_stream_unref(&payload);
+ }
+}
+
+static void
+test_client_echo_response(const struct http_response *resp,
+ struct test_client_request *tcreq)
+{
+ const char **paths;
+ const char *path;
+ unsigned int count, status;
+ struct istream *fstream;
+
+ if (debug) {
+ i_debug("test client: echo: got response for [%u]",
+ tcreq->files_idx);
+ }
+
+ if (to_client_progress != NULL)
+ timeout_reset(to_client_progress);
+
+ paths = array_get_modifiable(&files, &count);
+ i_assert(tcreq->files_idx < count);
+ i_assert(client_files_first < count);
+ path = paths[tcreq->files_idx];
+ i_assert(path != NULL);
+
+ if (debug) {
+ i_debug("test client: echo: path for [%u]: %s",
+ tcreq->files_idx, path);
+ }
+
+ if (resp->status / 100 != 2) {
+ i_fatal("test client: echo: "
+ "HTTP request for %s failed: %u %s",
+ path, resp->status, resp->reason);
+ }
+
+ fstream = test_file_open(path, &status, NULL);
+ if (fstream == NULL) {
+ i_fatal("test client: echo: failed to open %s", path);
+ }
+
+ if (tset.unknown_size) {
+ struct istream *ustream;
+
+ ustream = i_stream_create_crlf(fstream);
+ i_stream_unref(&fstream);
+ fstream = ustream;
+ }
+
+ if (tset.read_server_partial > 0) {
+ struct istream *partial =
+ i_stream_create_limit(fstream, tset.read_server_partial);
+ i_stream_unref(&fstream);
+ fstream = partial;
+ }
+
+ if (resp->payload == NULL) {
+ // FIXME: check file is empty
+ if (debug) {
+ i_debug("test client: echo: "
+ "no payload for %s [%u]",
+ path, tcreq->files_idx);
+ }
+ i_stream_unref(&fstream);
+ test_client_echo_finished(tcreq);
+ return;
+ }
+
+ i_assert(fstream != NULL);
+ tcreq->file_in = fstream;
+
+ i_stream_ref(resp->payload);
+ tcreq->payload = resp->payload;
+ tcreq->io = io_add_istream(resp->payload,
+ test_client_echo_payload_input, tcreq);
+ test_client_echo_payload_input(tcreq);
+}
+
+static void
+test_client_echo_nonblocking(struct test_client_request *tcreq ATTR_UNUSED,
+ struct http_client_request *hreq,
+ struct istream *fstream)
+{
+ http_client_request_set_payload(hreq, fstream,
+ tset.request_100_continue);
+ http_client_request_submit(hreq);
+}
+
+static void
+test_client_echo_blocking(struct test_client_request *tcreq,
+ struct http_client_request *hreq,
+ struct istream *fstream)
+{
+ const unsigned char *data;
+ size_t size;
+ int ret;
+
+ test_client_request_ref(tcreq);
+ tcreq->file_out = fstream;
+
+ while ((ret = i_stream_read_more(fstream, &data, &size)) > 0) {
+ ret = http_client_request_send_payload(&hreq, data, size);
+ i_assert(ret <= 0);
+ if (ret < 0)
+ break;
+ i_stream_skip(fstream, size);
+ }
+ i_assert(ret < 0);
+ if (fstream->stream_errno != 0) {
+ i_fatal("test client: echo: "
+ "read(%s) failed: %s [%u]",
+ i_stream_get_name(fstream),
+ i_stream_get_error(fstream),
+ tcreq->files_idx);
+ } else if (i_stream_have_bytes_left(fstream)) {
+ i_fatal("test client: echo: "
+ "failed to send all blocking payload [%u]",
+ tcreq->files_idx);
+ }
+
+ /* finish it */
+ if (http_client_request_finish_payload(&hreq) < 0) {
+ i_fatal("test client: echo: "
+ "failed to finish blocking payload [%u]",
+ tcreq->files_idx);
+ }
+ http_client_wait(tcreq->client);
+
+ if (debug) {
+ i_debug("test client: echo: "
+ "sent all payload [%u]",
+ tcreq->files_idx);
+ }
+
+ tcreq->file_out = NULL;
+ test_client_echo_finished(tcreq);
+ test_client_request_unref(&tcreq);
+}
+
+static void test_client_echo_continue(void *context ATTR_UNUSED)
+{
+ struct test_client_request *tcreq;
+ struct http_client_request *hreq;
+ const char **paths;
+ unsigned int count, first_submitted;
+ bool prev_files_finished = files_finished;
+
+ running_continue = TRUE;
+ files_finished = FALSE;
+ timeout_remove(&to_continue);
+
+ paths = array_get_modifiable(&files, &count);
+
+ i_assert(client_files_first <= count);
+ i_assert(client_files_last <= count);
+
+ i_assert(client_files_first <= client_files_last);
+ for (; client_files_first < client_files_last &&
+ paths[client_files_first] == NULL; client_files_first++);
+
+ if (debug) {
+ i_debug("test client: echo: received until [%u/%u]",
+ client_files_first-1, count);
+ }
+
+ if (debug && client_files_first < count) {
+ const char *path = paths[client_files_first];
+ i_debug("test client: echo: next blocking: %s [%d]",
+ (path == NULL ? "none" : path), client_files_first);
+ }
+
+ if (client_files_first >= count || failure != NULL) {
+ running_continue = FALSE;
+ files_finished = prev_files_finished;
+ io_loop_stop(current_ioloop);
+ return;
+ }
+
+ first_submitted = client_files_last;
+ for (; (client_files_last < count &&
+ (client_files_last - client_files_first) < tset.max_pending);
+ client_files_last++) {
+ struct http_client *http_client =
+ http_clients[client_files_last % tset.parallel_clients];
+ struct istream *fstream;
+ const char *path = paths[client_files_last];
+
+ fstream = test_file_open(path, NULL, NULL);
+ if (fstream == NULL) {
+ paths[client_files_last] = NULL;
+ if (debug) {
+ i_debug("test client: echo: "
+ "skipping %s [%u]",
+ path, client_files_last);
+ }
+ continue;
+ }
+
+ if (debug) {
+ i_debug("test client: echo: retrieving %s [%u]",
+ path, client_files_last);
+ }
+
+ if (tset.unknown_size) {
+ struct istream *ustream;
+
+ ustream = i_stream_create_crlf(fstream);
+ i_stream_unref(&fstream);
+ fstream = ustream;
+ }
+
+ tcreq = test_client_request_new(http_client);
+ tcreq->files_idx = client_files_last;
+
+ hreq = tcreq->hreq = http_client_request(http_client,
+ "PUT", net_ip2addr(&bind_ip),
+ t_strconcat("/echo/", path, NULL),
+ test_client_echo_response, tcreq);
+ http_client_request_set_port(hreq, bind_port);
+ http_client_request_set_ssl(hreq, tset.ssl);
+ http_client_request_set_destroy_callback(hreq,
+ test_client_request_destroy, tcreq);
+
+ if (!tset.client_blocking)
+ test_client_echo_nonblocking(tcreq, hreq, fstream);
+ else
+ test_client_echo_blocking(tcreq, hreq, fstream);
+ i_stream_unref(&fstream);
+
+ if (tset.client_blocking && paths[client_files_last] != NULL) {
+ running_continue = FALSE;
+ files_finished = prev_files_finished;
+ return;
+ }
+ }
+
+ if (files_finished && to_continue == NULL) {
+ to_continue = timeout_add_short(
+ 0, test_client_echo_continue, NULL);
+ }
+ running_continue = FALSE;
+ files_finished = prev_files_finished;
+
+ /* run nested ioloop (if requested) if new requests cross a nesting
+ boundary */
+ if (ioloop_nested != NULL) {
+ unsigned int i;
+
+ i_assert(ioloop_nested_first <= count);
+ i_assert(ioloop_nested_last <= count);
+ for (i = ioloop_nested_first; i < ioloop_nested_last; i++) {
+ if (paths[i] != NULL) {
+ if (debug) {
+ i_debug("test client: "
+ "not leaving ioloop [%u]", i);
+ }
+ break;
+ }
+ }
+
+ if (i == ioloop_nested_last)
+ io_loop_stop(ioloop_nested);
+ } else if (tset.client_ioloop_nesting > 0 &&
+ ((client_files_last / tset.client_ioloop_nesting) !=
+ (first_submitted / tset.client_ioloop_nesting))) {
+ struct ioloop *prev_ioloop = current_ioloop;
+ unsigned int i;
+
+ ioloop_nested_first = first_submitted;
+ ioloop_nested_last =
+ first_submitted + tset.client_ioloop_nesting;
+ if (ioloop_nested_last > client_files_last)
+ ioloop_nested_last = client_files_last;
+
+ if (debug) {
+ i_debug("test client: "
+ "echo: entering ioloop for %u...%u (depth=%u)",
+ ioloop_nested_first, ioloop_nested_last,
+ ioloop_nested_depth);
+ }
+
+ ioloop_nested_depth++;
+
+ ioloop_nested = io_loop_create();
+ for (i = 0; i < tset.parallel_clients; i++)
+ http_client_switch_ioloop(http_clients[i]);
+ test_client_switch_ioloop();
+
+ io_loop_run(ioloop_nested);
+
+ io_loop_set_current(prev_ioloop);
+ for (i = 0; i < tset.parallel_clients; i++)
+ http_client_switch_ioloop(http_clients[i]);
+ test_client_switch_ioloop();
+ io_loop_set_current(ioloop_nested);
+ io_loop_destroy(&ioloop_nested);
+ ioloop_nested = NULL;
+
+ ioloop_nested_depth--;
+
+ if (debug) {
+ i_debug("test client: echo: leaving ioloop for %u...%u "
+ "(depth=%u)", ioloop_nested_first,
+ ioloop_nested_last, ioloop_nested_depth);
+ }
+ ioloop_nested_first = ioloop_nested_last = 0;
+
+ if (client_files_first >= count || failure != NULL) {
+ io_loop_stop(current_ioloop);
+ return;
+ }
+ }
+}
+
+static void test_client_echo(const struct http_client_settings *client_set)
+{
+ /* create client */
+ test_client_create_clients(client_set);
+
+ /* start querying server */
+ client_files_first = client_files_last = 0;
+
+ i_assert(to_continue == NULL);
+ to_continue = timeout_add_short(0, test_client_echo_continue, NULL);
+}
+
+/* cleanup */
+
+static void test_client_deinit(void)
+{
+ unsigned int i;
+
+ for (i = 0; i < tset.parallel_clients; i++)
+ http_client_deinit(&http_clients[i]);
+ i_free(http_clients);
+
+ tset.parallel_clients = 1;
+
+ timeout_remove(&to_continue);
+ timeout_remove(&to_client_progress);
+}
+
+/*
+ * Tests
+ */
+
+struct test_server_data {
+ const struct http_server_settings *set;
+};
+
+static void test_open_server_fd(void)
+{
+ i_close_fd(&fd_listen);
+ fd_listen = net_listen(&bind_ip, &bind_port, 128);
+ if (fd_listen == -1) {
+ i_fatal("listen(%s:%u) failed: %m",
+ net_ip2addr(&bind_ip), bind_port);
+ }
+ net_set_nonblock(fd_listen, TRUE);
+}
+
+static int test_run_server(struct test_server_data *data)
+{
+ const struct http_server_settings *server_set = data->set;
+ struct ioloop *ioloop;
+
+ i_set_failure_prefix("SERVER: ");
+
+ if (debug)
+ i_debug("PID=%s", my_pid);
+
+ ioloop_nested = NULL;
+ ioloop_nested_depth = 0;
+ ioloop = io_loop_create();
+ test_server_init(server_set);
+ io_loop_run(ioloop);
+ test_server_deinit();
+ io_loop_destroy(&ioloop);
+
+ if (debug)
+ i_debug("Terminated");
+
+ i_close_fd(&fd_listen);
+ test_files_deinit();
+ main_deinit();
+ return 0;
+}
+
+static void
+test_run_client(
+ const struct http_client_settings *client_set,
+ void (*client_init)(const struct http_client_settings *client_set))
+{
+ struct ioloop *ioloop;
+
+ i_set_failure_prefix("CLIENT: ");
+
+ if (debug)
+ i_debug("PID=%s", my_pid);
+
+ ioloop_nested = NULL;
+ ioloop_nested_depth = 0;
+ ioloop = io_loop_create();
+ client_init(client_set);
+ io_loop_run(ioloop);
+ test_client_deinit();
+ io_loop_destroy(&ioloop);
+
+ if (debug)
+ i_debug("Terminated");
+}
+
+static void
+test_run_client_server(
+ const struct http_client_settings *client_set,
+ const struct http_server_settings *server_set,
+ void (*client_init)(const struct http_client_settings *client_set))
+{
+ struct test_server_data data;
+
+ failure = NULL;
+
+ test_files_init();
+
+ i_zero(&data);
+ data.set = server_set;
+
+ /* Fork server */
+ test_open_server_fd();
+ test_subprocess_fork(test_run_server, &data, FALSE);
+ i_close_fd(&fd_listen);
+
+ /* Run client */
+ test_run_client(client_set, client_init);
+
+ i_unset_failure_prefix();
+ test_subprocess_kill_all(SERVER_KILL_TIMEOUT_SECS);
+ test_files_deinit();
+}
+
+static void
+test_init_server_settings(struct http_server_settings *server_set_r)
+{
+ i_zero(server_set_r);
+ server_set_r->request_limits.max_payload_size = UOFF_T_MAX;
+ server_set_r->debug = debug;
+
+ if (small_socket_buffers) {
+ server_set_r->socket_send_buffer_size = 40960;
+ server_set_r->socket_recv_buffer_size = 40960;
+ }
+}
+
+static void
+test_init_client_settings(struct http_client_settings *client_set_r)
+{
+ i_zero(client_set_r);
+ client_set_r->max_redirects = 0;
+ client_set_r->max_attempts = 1;
+ client_set_r->max_idle_time_msecs = 5* 1000;
+ client_set_r->debug = debug;
+
+ if (small_socket_buffers) {
+ client_set_r->socket_send_buffer_size = 40960;
+ client_set_r->socket_recv_buffer_size = 40960;
+ client_set_r->request_timeout_msecs = 20 * 60 * 1000;
+ client_set_r->connect_timeout_msecs = 20 * 60 * 1000;
+ }
+}
+
+static void
+test_run_sequential(
+ void (*client_init)(const struct http_client_settings *client_set))
+{
+ struct http_server_settings http_server_set;
+ struct http_client_settings http_client_set;
+ struct ssl_iostream_settings ssl_server_set, ssl_client_set;
+
+ /* download files from blocking server */
+
+ /* ssl settings */
+ ssl_iostream_test_settings_server(&ssl_server_set);
+ ssl_server_set.verbose = debug;
+ ssl_iostream_test_settings_client(&ssl_client_set);
+ ssl_client_set.verbose = debug;
+
+ /* server settings */
+ test_init_server_settings(&http_server_set);
+ http_server_set.ssl = &ssl_server_set;
+ http_server_set.max_pipelined_requests = 0;
+
+ /* client settings */
+ test_init_client_settings(&http_client_set);
+ http_client_set.ssl = &ssl_client_set;
+ http_client_set.max_parallel_connections = 1;
+ http_client_set.max_pipelined_requests = 1;
+
+ test_run_client_server(&http_client_set, &http_server_set, client_init);
+
+ test_out_reason("sequential", (failure == NULL), failure);
+}
+
+static void
+test_run_pipeline(
+ void (*client_init)(const struct http_client_settings *client_set))
+{
+ struct http_server_settings http_server_set;
+ struct http_client_settings http_client_set;
+ struct ssl_iostream_settings ssl_server_set, ssl_client_set;
+
+ /* download files from blocking server */
+
+ /* ssl settings */
+ ssl_iostream_test_settings_server(&ssl_server_set);
+ ssl_server_set.verbose = debug;
+ ssl_iostream_test_settings_client(&ssl_client_set);
+ ssl_client_set.verbose = debug;
+
+ /* server settings */
+ test_init_server_settings(&http_server_set);
+ http_server_set.ssl = &ssl_server_set;
+ http_server_set.max_pipelined_requests = 4;
+
+ /* client settings */
+ test_init_client_settings(&http_client_set);
+ http_client_set.ssl = &ssl_client_set;
+ http_client_set.max_parallel_connections = 1;
+ http_client_set.max_pipelined_requests = 8;
+
+ test_run_client_server(&http_client_set, &http_server_set, client_init);
+
+ test_out_reason("pipeline", (failure == NULL), failure);
+}
+
+static void
+test_run_parallel(
+ void (*client_init)(const struct http_client_settings *client_set))
+{
+ struct http_server_settings http_server_set;
+ struct http_client_settings http_client_set;
+ struct ssl_iostream_settings ssl_server_set, ssl_client_set;
+
+ /* download files from blocking server */
+
+ /* ssl settings */
+ ssl_iostream_test_settings_server(&ssl_server_set);
+ ssl_server_set.verbose = debug;
+ ssl_iostream_test_settings_client(&ssl_client_set);
+ ssl_client_set.verbose = debug;
+
+ /* server settings */
+ test_init_server_settings(&http_server_set);
+ http_server_set.ssl = &ssl_server_set;
+ http_server_set.max_pipelined_requests = 4;
+
+ /* client settings */
+ test_init_client_settings(&http_client_set);
+ http_client_set.ssl = &ssl_client_set;
+ http_client_set.max_parallel_connections = 40;
+ http_client_set.max_pipelined_requests = 8;
+
+ test_run_client_server(&http_client_set, &http_server_set, client_init);
+
+ test_out_reason("parallel", (failure == NULL), failure);
+}
+
+static void test_download_server_nonblocking(void)
+{
+ test_begin("http payload download (server non-blocking)");
+ test_init_defaults();
+ test_run_sequential(test_client_download);
+ test_run_pipeline(test_client_download);
+ test_run_parallel(test_client_download);
+ test_end();
+}
+
+static void test_download_server_blocking(void)
+{
+ test_begin("http payload download (server blocking)");
+ test_init_defaults();
+ tset.server_blocking = TRUE;
+ test_run_sequential(test_client_download);
+ test_run_pipeline(test_client_download);
+ test_run_parallel(test_client_download);
+ test_end();
+}
+
+static void test_echo_server_nonblocking(void)
+{
+ test_begin("http payload echo "
+ "(server non-blocking)");
+ test_init_defaults();
+ test_run_sequential(test_client_echo);
+ test_run_pipeline(test_client_echo);
+ test_run_parallel(test_client_echo);
+ test_end();
+
+ test_begin("http payload echo "
+ "(server non-blocking; low-level)");
+ test_init_defaults();
+ tset.server_payload_handling = PAYLOAD_HANDLING_LOW_LEVEL;
+ test_run_sequential(test_client_echo);
+ test_run_pipeline(test_client_echo);
+ test_run_parallel(test_client_echo);
+ test_end();
+
+ test_begin("http payload echo "
+ "(server non-blocking; handler)");
+ test_init_defaults();
+ tset.server_payload_handling = PAYLOAD_HANDLING_HANDLER;
+ test_run_sequential(test_client_echo);
+ test_run_pipeline(test_client_echo);
+ test_run_parallel(test_client_echo);
+ test_end();
+
+ test_begin("http payload echo "
+ "(server non-blocking; size unknown)");
+ test_init_defaults();
+ tset.unknown_size = TRUE;
+ tset.server_payload_handling = PAYLOAD_HANDLING_FORWARD;
+ test_run_sequential(test_client_echo);
+ test_run_pipeline(test_client_echo);
+ test_run_parallel(test_client_echo);
+ test_end();
+
+ test_begin("http payload echo "
+ "(server non-blocking; ostream)");
+ test_init_defaults();
+ tset.server_ostream = TRUE;
+ test_run_sequential(test_client_echo);
+ test_run_pipeline(test_client_echo);
+ test_run_parallel(test_client_echo);
+ test_end();
+
+ test_begin("http payload echo "
+ "(server non-blocking; ostream; cork)");
+ test_init_defaults();
+ tset.server_ostream = TRUE;
+ tset.server_cork = TRUE;
+ test_run_sequential(test_client_echo);
+ test_run_pipeline(test_client_echo);
+ test_run_parallel(test_client_echo);
+ test_end();
+}
+
+static void test_echo_server_blocking(void)
+{
+ test_begin("http payload echo (server blocking)");
+ test_init_defaults();
+ tset.server_blocking = TRUE;
+ test_run_sequential(test_client_echo);
+ test_run_pipeline(test_client_echo);
+ test_run_parallel(test_client_echo);
+ test_end();
+
+ test_begin("http payload echo (server blocking; ostream)");
+ test_init_defaults();
+ tset.server_blocking = TRUE;
+ tset.server_ostream = TRUE;
+ test_run_sequential(test_client_echo);
+ test_run_pipeline(test_client_echo);
+ test_run_parallel(test_client_echo);
+ test_end();
+
+ test_begin("http payload echo (server blocking; ostream; cork)");
+ test_init_defaults();
+ tset.server_ostream = TRUE;
+ tset.server_blocking = TRUE;
+ tset.server_cork = TRUE;
+ test_run_sequential(test_client_echo);
+ test_run_pipeline(test_client_echo);
+ test_run_parallel(test_client_echo);
+ test_end();
+}
+
+static void test_echo_server_nonblocking_sync(void)
+{
+ test_begin("http payload echo "
+ "(server non-blocking; 100-continue)");
+ test_init_defaults();
+ tset.request_100_continue = TRUE;
+ test_run_sequential(test_client_echo);
+ test_run_pipeline(test_client_echo);
+ test_run_parallel(test_client_echo);
+ test_end();
+
+ test_begin("http payload echo "
+ "(server non-blocking; 100-continue; low-level)");
+ test_init_defaults();
+ tset.request_100_continue = TRUE;
+ tset.server_payload_handling = PAYLOAD_HANDLING_LOW_LEVEL;
+ test_run_sequential(test_client_echo);
+ test_run_pipeline(test_client_echo);
+ test_run_parallel(test_client_echo);
+ test_end();
+
+ test_begin("http payload echo "
+ "(server non-blocking; 100-continue; handler)");
+ test_init_defaults();
+ tset.request_100_continue = TRUE;
+ tset.server_payload_handling = PAYLOAD_HANDLING_HANDLER;
+ test_run_sequential(test_client_echo);
+ test_run_pipeline(test_client_echo);
+ test_run_parallel(test_client_echo);
+ test_end();
+}
+
+static void test_echo_server_blocking_sync(void)
+{
+ test_begin("http payload echo (server blocking; 100-continue)");
+ test_init_defaults();
+ tset.server_blocking = TRUE;
+ tset.request_100_continue = TRUE;
+ test_run_sequential(test_client_echo);
+ test_run_pipeline(test_client_echo);
+ test_run_parallel(test_client_echo);
+ test_end();
+
+ test_begin("http payload echo "
+ "(server blocking; ostream; 100-continue)");
+ test_init_defaults();
+ tset.server_blocking = TRUE;
+ tset.server_ostream = TRUE;
+ tset.request_100_continue = TRUE;
+ test_run_sequential(test_client_echo);
+ test_run_pipeline(test_client_echo);
+ test_run_parallel(test_client_echo);
+ test_end();
+}
+
+static void test_echo_server_nonblocking_partial(void)
+{
+ test_begin("http payload echo "
+ "(server non-blocking; partial short)");
+ test_init_defaults();
+ tset.read_server_partial = 1024;
+ test_run_sequential(test_client_echo);
+ test_run_pipeline(test_client_echo);
+ test_run_parallel(test_client_echo);
+ test_end();
+ test_begin("http payload echo "
+ "(server non-blocking; partial long)");
+ test_init_defaults();
+ tset.read_server_partial = IO_BLOCK_SIZE + 1024;
+ test_run_sequential(test_client_echo);
+ test_run_pipeline(test_client_echo);
+ test_run_parallel(test_client_echo);
+ test_end();
+
+ test_begin("http payload echo (server non-blocking; "
+ "partial short; low-level)");
+ test_init_defaults();
+ tset.server_payload_handling = PAYLOAD_HANDLING_LOW_LEVEL;
+ tset.read_server_partial = 1024;
+ test_run_sequential(test_client_echo);
+ test_run_pipeline(test_client_echo);
+ test_run_parallel(test_client_echo);
+ test_end();
+ test_begin("http payload echo "
+ "(server non-blocking; partial long; low-level)");
+ test_init_defaults();
+ tset.server_payload_handling = PAYLOAD_HANDLING_LOW_LEVEL;
+ tset.read_server_partial = IO_BLOCK_SIZE + 1024;
+ test_run_sequential(test_client_echo);
+ test_run_pipeline(test_client_echo);
+ test_run_parallel(test_client_echo);
+ test_end();
+
+ test_begin("http payload echo "
+ "(server non-blocking; partial short; handler)");
+ test_init_defaults();
+ tset.server_payload_handling = PAYLOAD_HANDLING_HANDLER;
+ tset.read_server_partial = 1024;
+ test_run_sequential(test_client_echo);
+ test_run_pipeline(test_client_echo);
+ test_run_parallel(test_client_echo);
+ test_end();
+ test_begin("http payload echo "
+ "(server non-blocking; partial long; handler)");
+ test_init_defaults();
+ tset.server_payload_handling = PAYLOAD_HANDLING_HANDLER;
+ tset.read_server_partial = IO_BLOCK_SIZE + 1024;
+ test_run_sequential(test_client_echo);
+ test_run_pipeline(test_client_echo);
+ test_run_parallel(test_client_echo);
+ test_end();
+
+ test_begin("http payload echo "
+ "(server non-blocking; partial short; ostream)");
+ test_init_defaults();
+ tset.server_ostream = TRUE;
+ tset.read_server_partial = 1024;
+ test_run_sequential(test_client_echo);
+ test_run_pipeline(test_client_echo);
+ test_run_parallel(test_client_echo);
+ test_end();
+ test_begin("http payload echo "
+ "(server non-blocking; partial long; ostream)");
+ test_init_defaults();
+ tset.server_ostream = TRUE;
+ tset.read_server_partial = IO_BLOCK_SIZE + 1024;
+ test_run_sequential(test_client_echo);
+ test_run_pipeline(test_client_echo);
+ test_run_parallel(test_client_echo);
+ test_end();
+
+ test_begin("http payload echo "
+ "(server non-blocking; partial short; ostream; corked)");
+ test_init_defaults();
+ tset.server_ostream = TRUE;
+ tset.server_cork = TRUE;
+ tset.read_server_partial = 1024;
+ test_run_sequential(test_client_echo);
+ test_run_pipeline(test_client_echo);
+ test_run_parallel(test_client_echo);
+ test_end();
+ test_begin("http payload echo "
+ "(server non-blocking; partial long; ostream; corked)");
+ test_init_defaults();
+ tset.server_ostream = TRUE;
+ tset.server_cork = TRUE;
+ tset.read_server_partial = IO_BLOCK_SIZE + 1024;
+ test_run_sequential(test_client_echo);
+ test_run_pipeline(test_client_echo);
+ test_run_parallel(test_client_echo);
+ test_end();
+}
+
+static void test_echo_server_blocking_partial(void)
+{
+ test_begin("http payload echo (server blocking; partial short)");
+ test_init_defaults();
+ tset.server_blocking = TRUE;
+ tset.read_server_partial = 1024;
+ test_run_sequential(test_client_echo);
+ test_run_pipeline(test_client_echo);
+ test_run_parallel(test_client_echo);
+ test_end();
+ test_begin("http payload echo (server blocking; partial long)");
+ test_init_defaults();
+ tset.server_blocking = TRUE;
+ tset.read_server_partial = IO_BLOCK_SIZE + 1024;
+ test_run_sequential(test_client_echo);
+ test_run_pipeline(test_client_echo);
+ test_run_parallel(test_client_echo);
+ test_end();
+
+ test_begin("http payload echo "
+ "(server blocking; partial short; ostream; cork)");
+ test_init_defaults();
+ tset.server_blocking = TRUE;
+ tset.server_ostream = TRUE;
+ tset.server_cork = TRUE;
+ tset.read_server_partial = 1024;
+ test_run_sequential(test_client_echo);
+ test_run_pipeline(test_client_echo);
+ test_run_parallel(test_client_echo);
+ test_end();
+ test_begin("http payload echo "
+ "(server blocking; partial long; ostream; cork)");
+ test_init_defaults();
+ tset.server_blocking = TRUE;
+ tset.server_ostream = TRUE;
+ tset.server_cork = TRUE;
+ tset.read_server_partial = IO_BLOCK_SIZE + 1024;
+ test_run_sequential(test_client_echo);
+ test_run_pipeline(test_client_echo);
+ test_run_parallel(test_client_echo);
+ test_end();
+}
+
+static void test_download_client_partial(void)
+{
+ test_begin("http payload download (client partial)");
+ test_init_defaults();
+ tset.read_client_partial = 1024;
+ test_run_sequential(test_client_download);
+ test_run_pipeline(test_client_download);
+ test_run_parallel(test_client_download);
+ test_end();
+ test_begin("http payload download (client partial long)");
+ test_init_defaults();
+ tset.read_client_partial = IO_BLOCK_SIZE + 1024;
+ test_run_sequential(test_client_download);
+ test_run_pipeline(test_client_download);
+ test_run_parallel(test_client_download);
+ test_end();
+}
+
+static void test_download_client_nested_ioloop(void)
+{
+ test_begin("http payload echo (client nested ioloop)");
+ test_init_defaults();
+ tset.client_ioloop_nesting = 10;
+ test_run_parallel(test_client_echo);
+ test_end();
+}
+
+static void test_echo_client_shared(void)
+{
+ test_begin("http payload download "
+ "(server non-blocking; client shared)");
+ test_init_defaults();
+ tset.parallel_clients = 4;
+ test_run_sequential(test_client_download);
+ tset.parallel_clients = 4;
+ test_run_pipeline(test_client_download);
+ tset.parallel_clients = 4;
+ test_run_parallel(test_client_download);
+ test_end();
+
+ test_begin("http payload download "
+ "(server blocking; client shared)");
+ test_init_defaults();
+ tset.server_blocking = TRUE;
+ tset.parallel_clients = 4;
+ test_run_sequential(test_client_download);
+ tset.parallel_clients = 4;
+ test_run_pipeline(test_client_download);
+ tset.parallel_clients = 4;
+ test_run_parallel(test_client_download);
+ test_end();
+
+ test_begin("http payload echo "
+ "(server non-blocking; client shared)");
+ test_init_defaults();
+ tset.parallel_clients = 4;
+ test_run_sequential(test_client_echo);
+ tset.parallel_clients = 4;
+ test_run_pipeline(test_client_echo);
+ tset.parallel_clients = 4;
+ test_run_parallel(test_client_echo);
+ test_end();
+
+ test_begin("http payload echo "
+ "(server blocking; client shared)");
+ test_init_defaults();
+ tset.server_blocking = TRUE;
+ tset.server_ostream = TRUE;
+ tset.parallel_clients = 4;
+ test_run_sequential(test_client_echo);
+ tset.parallel_clients = 4;
+ test_run_pipeline(test_client_echo);
+ tset.parallel_clients = 4;
+ test_run_parallel(test_client_echo);
+ test_end();
+
+ test_begin("http payload echo "
+ "(server non-blocking; client global)");
+ test_init_defaults();
+ tset.parallel_clients = 4;
+ tset.parallel_clients_global = TRUE;
+ test_run_sequential(test_client_echo);
+ tset.parallel_clients = 4;
+ tset.parallel_clients_global = TRUE;
+ test_run_pipeline(test_client_echo);
+ tset.parallel_clients = 4;
+ tset.parallel_clients_global = TRUE;
+ test_run_parallel(test_client_echo);
+ test_end();
+}
+
+#ifdef HAVE_OPENSSL
+static void test_echo_ssl(void)
+{
+ test_begin("http payload echo (ssl)");
+ test_init_defaults();
+ tset.ssl = TRUE;
+ test_run_sequential(test_client_echo);
+ test_run_pipeline(test_client_echo);
+ test_run_parallel(test_client_echo);
+ test_end();
+
+ test_begin("http payload echo (ssl; unknown size)");
+ test_init_defaults();
+ tset.unknown_size = TRUE;
+ tset.ssl = TRUE;
+ test_run_sequential(test_client_echo);
+ test_run_pipeline(test_client_echo);
+ test_run_parallel(test_client_echo);
+ test_end();
+
+ test_begin("http payload echo (ssl; server ostream, cork)");
+ test_init_defaults();
+ tset.ssl = TRUE;
+ tset.server_ostream = TRUE;
+ tset.server_cork = TRUE;
+ test_run_sequential(test_client_echo);
+ test_run_pipeline(test_client_echo);
+ test_run_parallel(test_client_echo);
+ test_end();
+}
+#endif
+
+static void test_echo_client_blocking(void)
+{
+ test_begin("http payload echo (client blocking)");
+ test_init_defaults();
+ tset.client_blocking = TRUE;
+ test_run_sequential(test_client_echo);
+ test_run_pipeline(test_client_echo);
+ test_run_parallel(test_client_echo);
+ test_end();
+
+ test_begin("http payload echo (client blocking; client shared)");
+ test_init_defaults();
+ tset.client_blocking = TRUE;
+ tset.parallel_clients = 4;
+ test_run_sequential(test_client_echo);
+ tset.parallel_clients = 4;
+ test_run_pipeline(test_client_echo);
+ tset.parallel_clients = 4;
+ test_run_parallel(test_client_echo);
+ test_end();
+
+ test_begin("http payload echo (client blocking; client global)");
+ test_init_defaults();
+ tset.client_blocking = TRUE;
+ tset.parallel_clients = 4;
+ tset.parallel_clients_global = TRUE;
+ test_run_sequential(test_client_echo);
+ tset.parallel_clients = 4;
+ tset.parallel_clients_global = TRUE;
+ test_run_pipeline(test_client_echo);
+ tset.parallel_clients = 4;
+ tset.parallel_clients_global = TRUE;
+ test_run_parallel(test_client_echo);
+ test_end();
+}
+
+static void (*const test_functions[])(void) = {
+ test_download_server_nonblocking,
+ test_download_server_blocking,
+ test_echo_server_nonblocking,
+ test_echo_server_blocking,
+ test_echo_server_nonblocking_sync,
+ test_echo_server_blocking_sync,
+ test_echo_server_nonblocking_partial,
+ test_echo_server_blocking_partial,
+ test_download_client_partial,
+ test_download_client_nested_ioloop,
+ test_echo_client_shared,
+#ifdef HAVE_OPENSSL
+ test_echo_ssl,
+#endif
+ test_echo_client_blocking,
+ NULL
+};
+
+/*
+ * Main
+ */
+
+static void main_init(void)
+{
+#ifdef HAVE_OPENSSL
+ ssl_iostream_openssl_init();
+#endif
+}
+
+static void main_deinit(void)
+{
+ ssl_iostream_context_cache_free();
+#ifdef HAVE_OPENSSL
+ ssl_iostream_openssl_deinit();
+#endif
+}
+
+int main(int argc, char *argv[])
+{
+ int c;
+ int ret;
+
+ lib_init();
+ main_init();
+
+ while ((c = getopt(argc, argv, "DS")) > 0) {
+ switch (c) {
+ case 'D':
+ debug = TRUE;
+ break;
+ case 'S':
+ small_socket_buffers = TRUE;
+ break;
+ default:
+ i_fatal("Usage: %s [-D]", argv[0]);
+ }
+ }
+
+ test_subprocesses_init(debug);
+
+ /* listen on localhost */
+ i_zero(&bind_ip);
+ bind_ip.family = AF_INET;
+ bind_ip.u.ip4.s_addr = htonl(INADDR_LOOPBACK);
+
+ ret = test_run(test_functions);
+
+ test_subprocesses_deinit();
+ main_deinit();
+ lib_deinit();
+ return ret;
+}
diff --git a/src/lib-http/test-http-request-parser.c b/src/lib-http/test-http-request-parser.c
new file mode 100644
index 0000000..414e66b
--- /dev/null
+++ b/src/lib-http/test-http-request-parser.c
@@ -0,0 +1,701 @@
+/* Copyright (c) 2013-2018 Dovecot authors, see the included COPYING file */
+
+#include "test-lib.h"
+#include "buffer.h"
+#include "str.h"
+#include "str-sanitize.h"
+#include "istream.h"
+#include "ostream.h"
+#include "test-common.h"
+#include "http-url.h"
+#include "http-request-parser.h"
+
+#include <time.h>
+
+/*
+ * Test: valid requests
+ */
+
+struct http_request_valid_parse_test {
+ const char *request;
+ const char *method;
+ const char *target_raw;
+ struct {
+ enum http_request_target_format format;
+ struct http_url url;
+ } target;
+ unsigned char version_major;
+ unsigned char version_minor;
+ uoff_t content_length;
+ const char *payload;
+ bool connection_close;
+ bool expect_100_continue;
+
+ enum http_request_parse_flags flags;
+};
+
+static const struct http_url default_base_url = {
+ .host = { .name = "example.org" },
+};
+
+static const struct http_request_valid_parse_test
+valid_request_parse_tests[] = {
+ {
+ .request =
+ "GET / HTTP/1.1\r\n"
+ "Host: example.com\r\n"
+ "\r\n",
+ .method = "GET",
+ .target_raw = "/",
+ .target = {
+ .format = HTTP_REQUEST_TARGET_FORMAT_ORIGIN,
+ .url = {
+ .host = { .name = "example.com" },
+ .path = "/",
+ },
+ },
+ .version_major = 1, .version_minor = 1,
+ },
+ {
+ .request =
+ "GET / HTTP/1.1\r\n"
+ "Host: \r\n"
+ "\r\n",
+ .method = "GET",
+ .target_raw = "/",
+ .target = {
+ .format = HTTP_REQUEST_TARGET_FORMAT_ORIGIN,
+ .url = {
+ .host = { .name = "example.org" },
+ .path = "/",
+ },
+ },
+ .version_major = 1, .version_minor = 1,
+ },
+ {
+ .request =
+ "GET / HTTP/1.0\r\n"
+ "\r\n",
+ .method = "GET",
+ .target_raw = "/",
+ .target = {
+ .format = HTTP_REQUEST_TARGET_FORMAT_ORIGIN,
+ .url = {
+ .host = { .name = "example.org" },
+ .path = "/",
+ },
+ },
+ .version_major = 1, .version_minor = 0,
+ .connection_close = TRUE,
+ },
+ {
+ .request =
+ "OPTIONS * HTTP/1.1\r\n"
+ "Host: example.com\r\n"
+ "Connection: Keep-Alive\r\n"
+ "\r\n",
+ .method = "OPTIONS",
+ .target_raw = "*",
+ .target = {
+ .format = HTTP_REQUEST_TARGET_FORMAT_ASTERISK,
+ .url = {
+ .host = { .name = "example.com" },
+ },
+ },
+ .version_major = 1, .version_minor = 1,
+ },
+ {
+ .request =
+ "OPTIONS * HTTP/1.0\r\n"
+ "Connection: Keep-Alive\r\n"
+ "\r\n",
+ .method = "OPTIONS",
+ .target_raw = "*",
+ .target = {
+ .format = HTTP_REQUEST_TARGET_FORMAT_ASTERISK,
+ .url = {
+ .host = { .name = "example.org" },
+ },
+ },
+ .version_major = 1, .version_minor = 0,
+ },
+ {
+ .request =
+ "CONNECT example.com:443 HTTP/1.2\r\n"
+ "Host: example.com:443\r\n"
+ "\r\n",
+ .method = "CONNECT",
+ .target_raw = "example.com:443",
+ .target = {
+ .format = HTTP_REQUEST_TARGET_FORMAT_AUTHORITY,
+ .url = {
+ .host = { .name = "example.com" },
+ .port = 443,
+ }
+ },
+ .version_major = 1, .version_minor = 2,
+ },
+ {
+ .request =
+ "GET https://www.example.com:443 HTTP/1.1\r\n"
+ "Host: www.example.com:80\r\n"
+ "\r\n",
+ .method = "GET",
+ .target_raw = "https://www.example.com:443",
+ .target = {
+ .format = HTTP_REQUEST_TARGET_FORMAT_ABSOLUTE,
+ .url = {
+ .host = { .name = "www.example.com" },
+ .port = 443,
+ .have_ssl = TRUE,
+ },
+ },
+ .version_major = 1, .version_minor = 1,
+ },
+ {
+ .request =
+ "POST http://api.example.com:8080/commit?user=dirk HTTP/1.1\r\n"
+ "Host: api.example.com:8080\r\n"
+ "Content-Length: 10\r\n"
+ "\r\n"
+ "Content!\r\n",
+ .method = "POST",
+ .target_raw = "http://api.example.com:8080/commit?user=dirk",
+ .target = {
+ .format = HTTP_REQUEST_TARGET_FORMAT_ABSOLUTE,
+ .url = {
+ .host = { .name = "api.example.com" },
+ .port = 8080,
+ .path = "/commit",
+ },
+ },
+ .version_major = 1, .version_minor = 1,
+ .payload = "Content!\r\n",
+ },
+ {
+ .request =
+ "GET http://www.example.com/index.php?seq=1 HTTP/1.1\r\n"
+ "Host: www.example.com\r\n"
+ "Connection: close\r\n"
+ "\r\n",
+ .method = "GET",
+ .target_raw = "http://www.example.com/index.php?seq=1",
+ .target = {
+ .format = HTTP_REQUEST_TARGET_FORMAT_ABSOLUTE,
+ .url = {
+ .host = { .name = "www.example.com" },
+ .path = "/index.php",
+ },
+ },
+ .version_major = 1, .version_minor = 1,
+ .connection_close = TRUE,
+ },
+ {
+ .request =
+ "GET http://www.example.com/index.html HTTP/1.0\r\n"
+ "Host: www.example.com\r\n"
+ "\r\n",
+ .method = "GET",
+ .target_raw = "http://www.example.com/index.html",
+ .target = {
+ .format = HTTP_REQUEST_TARGET_FORMAT_ABSOLUTE,
+ .url = {
+ .host = { .name = "www.example.com" },
+ .path = "/index.html",
+ },
+ },
+ .version_major = 1, .version_minor = 0,
+ .connection_close = TRUE,
+ },
+ {
+ .request =
+ "GET http://www.example.com/index.html HTTP/1.1\r\n"
+ "Host: www.example.com\r\n"
+ "Expect: 100-continue\r\n"
+ "\r\n",
+ .method = "GET",
+ .target_raw = "http://www.example.com/index.html",
+ .target = {
+ .format = HTTP_REQUEST_TARGET_FORMAT_ABSOLUTE,
+ .url = {
+ .host = { .name = "www.example.com" },
+ .path = "/index.html",
+ },
+ },
+ .version_major = 1, .version_minor = 1,
+ .expect_100_continue = TRUE
+ },
+ {
+ .request =
+ "GET / HTTP/1.1\r\n"
+ "Date: Mon, 09 Kul 2018 02:24:29 GMT\r\n"
+ "Host: example.com\r\n"
+ "\r\n",
+ .method = "GET",
+ .target_raw = "/",
+ .target = {
+ .format = HTTP_REQUEST_TARGET_FORMAT_ORIGIN,
+ .url = {
+ .host = { .name = "example.com" },
+ .path = "/",
+ },
+ },
+ .version_major = 1, .version_minor = 1,
+ },
+ {
+ .request =
+ "GET / HTTP/1.1\r\n"
+ "Date: Sun, 07 Oct 2012 19:52:03 GMT\r\n"
+ "Host: example.com\r\n"
+ "Date: Sun, 13 Oct 2013 13:13:13 GMT\r\n"
+ "\r\n",
+ .method = "GET",
+ .target_raw = "/",
+ .target = {
+ .format = HTTP_REQUEST_TARGET_FORMAT_ORIGIN,
+ .url = {
+ .host = { .name = "example.com" },
+ .path = "/",
+ },
+ },
+ .version_major = 1, .version_minor = 1,
+ },
+ {
+ .request =
+ "GET //index.php HTTP/1.1\r\n"
+ "Date: Sun, 07 Oct 2012 19:52:03 GMT\r\n"
+ "Host: example.com\r\n"
+ "Date: Sun, 13 Oct 2013 13:13:13 GMT\r\n"
+ "\r\n",
+ .method = "GET",
+ .target_raw = "//index.php",
+ .target = {
+ .format = HTTP_REQUEST_TARGET_FORMAT_ORIGIN,
+ .url = {
+ .host = { .name = "example.com" },
+ .path = "//index.php",
+ },
+ },
+ .version_major = 1, .version_minor = 1,
+ },
+};
+
+static const unsigned int valid_request_parse_test_count =
+ N_ELEMENTS(valid_request_parse_tests);
+
+static const char *
+_request_target_format(enum http_request_target_format target_format)
+{
+ switch (target_format) {
+ case HTTP_REQUEST_TARGET_FORMAT_ORIGIN:
+ return "origin";
+ case HTTP_REQUEST_TARGET_FORMAT_ABSOLUTE:
+ return "absolute";
+ case HTTP_REQUEST_TARGET_FORMAT_AUTHORITY:
+ return "authority";
+ case HTTP_REQUEST_TARGET_FORMAT_ASTERISK:
+ return "asterisk";
+ }
+ return t_strdup_printf("<<UNKNOWN: %u>>", target_format);
+}
+
+static void
+test_http_request_url_equal(const struct http_url *urlt,
+ const struct http_url *urlp)
+{
+ if (urlp == NULL) {
+ test_out("request->target.url = (null)",
+ (urlt->host.name == NULL && urlt->port == 0));
+ return;
+ }
+ if (urlp->host.name == NULL || urlt->host.name == NULL) {
+ test_out(t_strdup_printf("request->target.url->host.name = %s",
+ urlp->host.name),
+ (urlp->host.name == urlt->host.name));
+ } else {
+ test_out(t_strdup_printf("request->target.url->host.name = %s",
+ urlp->host.name),
+ strcmp(urlp->host.name, urlt->host.name) == 0);
+ }
+ if (urlp->port == 0) {
+ test_out("request->target.url->port = (unspecified)",
+ urlp->port == urlt->port);
+ } else {
+ test_out(t_strdup_printf("request->target.url->port = %u",
+ urlp->port),
+ urlp->port == urlt->port);
+ }
+ test_out(t_strdup_printf("request->target.url->have_ssl = %s",
+ (urlp->have_ssl ? "yes" : "no")),
+ urlp->have_ssl == urlt->have_ssl);
+ if (urlp->path == NULL || urlt->path == NULL) {
+ test_out(t_strdup_printf("request->target.url->path = %s",
+ urlp->path),
+ urlp->path == urlt->path);
+ } else {
+ test_out(t_strdup_printf("request->target.url->path = %s",
+ urlp->path),
+ strcmp(urlp->path, urlt->path) == 0);
+ }
+}
+
+static void
+test_http_request_equal(const struct http_request_valid_parse_test *test,
+ struct http_request request, const char *payload)
+{
+ if (request.method == NULL || test->method == NULL) {
+ test_out(t_strdup_printf("request->method = %s",
+ request.method),
+ request.method == test->method);
+ } else {
+ test_out(t_strdup_printf("request->method = %s",
+ request.method),
+ strcmp(request.method, test->method) == 0);
+ }
+
+ if (request.target_raw == NULL || test->target_raw == NULL) {
+ test_out(t_strdup_printf("request->target_raw = %s",
+ request.target_raw),
+ request.target_raw == test->target_raw);
+ } else {
+ test_out(t_strdup_printf("request->target_raw = %s",
+ request.target_raw),
+ strcmp(request.target_raw, test->target_raw) == 0);
+ }
+
+ test_http_request_url_equal(&test->target.url, request.target.url);
+
+ test_out(t_strdup_printf("request->target_format = %s",
+ _request_target_format(request.target.format)),
+ request.target.format == test->target.format);
+
+ test_out(t_strdup_printf("request->version = %u.%u",
+ request.version_major, request.version_minor),
+ (request.version_major == test->version_major &&
+ request.version_minor == test->version_minor));
+ test_out(t_strdup_printf("request->connection_close = %s",
+ (request.connection_close ? "yes" : "no")),
+ request.connection_close == test->connection_close);
+ test_out(t_strdup_printf("request->expect_100_continue = %s",
+ (request.expect_100_continue ? "yes" : "no")),
+ request.expect_100_continue == test->expect_100_continue);
+
+ if (payload == NULL || test->payload == NULL) {
+ test_out(t_strdup_printf("request->payload = %s",
+ str_sanitize(payload, 80)),
+ payload == test->payload);
+ } else {
+ test_out(t_strdup_printf("request->payload = %s",
+ str_sanitize(payload, 80)),
+ strcmp(payload, test->payload) == 0);
+ }
+}
+
+static void test_http_request_parse_valid(void)
+{
+ unsigned int i;
+ buffer_t *payload_buffer = buffer_create_dynamic(default_pool, 1024);
+
+ for (i = 0; i < valid_request_parse_test_count; i++) T_BEGIN {
+ struct istream *input;
+ struct ostream *output;
+ const struct http_request_valid_parse_test *test;
+ struct http_request_parser *parser;
+ struct http_request request, request_parsed;
+ enum http_request_parse_error error_code;
+ const char *request_text, *payload, *error;
+ unsigned int pos, request_text_len;
+ int ret = 0;
+
+ test = &valid_request_parse_tests[i];
+ request_text = test->request;
+ request_text_len = strlen(request_text);
+ input = test_istream_create_data(request_text,
+ request_text_len);
+ parser = http_request_parser_init(input, &default_base_url,
+ NULL, test->flags);
+
+ test_begin(t_strdup_printf("http request valid [%d]", i));
+
+ payload = NULL;
+ for (pos = 0; pos <= request_text_len && ret == 0; pos++) {
+ test_istream_set_size(input, pos);
+ ret = http_request_parse_next(parser, NULL,
+ &request_parsed,
+ &error_code, &error);
+ }
+ test_istream_set_size(input, request_text_len);
+ i_stream_unref(&input);
+ request = request_parsed;
+
+ while (ret > 0) {
+ if (request.payload != NULL) {
+ buffer_set_used_size(payload_buffer, 0);
+ output = o_stream_create_buffer(payload_buffer);
+ test_out("payload receive",
+ o_stream_send_istream(
+ output, request.payload) ==
+ OSTREAM_SEND_ISTREAM_RESULT_FINISHED);
+ o_stream_destroy(&output);
+ payload = str_c(payload_buffer);
+ } else {
+ payload = NULL;
+ }
+ ret = http_request_parse_next(
+ parser, NULL, &request_parsed,
+ &error_code, &error);
+ }
+
+ test_out_reason("parse success", ret == 0, error);
+
+ if (ret == 0) {
+ /* verify last request only */
+ test_http_request_equal(test, request, payload);
+ }
+ test_end();
+ http_request_parser_deinit(&parser);
+ } T_END;
+
+ buffer_free(&payload_buffer);
+}
+
+/*
+ * Test: invalid requests
+ */
+
+struct http_request_invalid_parse_test {
+ const char *request;
+ enum http_request_parse_flags flags;
+
+ enum http_request_parse_error error_code;
+};
+
+static const struct http_request_invalid_parse_test
+invalid_request_parse_tests[] = {
+ {
+ .request =
+ "GET: / HTTP/1.1\r\n"
+ "Host: example.com\r\n"
+ "\r\n",
+ .error_code = HTTP_REQUEST_PARSE_ERROR_BROKEN_REQUEST
+ },
+ {
+ .request =
+ "GET % HTTP/1.1\r\n"
+ "Host: example.com\r\n"
+ "\r\n",
+ .error_code = HTTP_REQUEST_PARSE_ERROR_BAD_REQUEST
+ },
+ {
+ .request =
+ "GET /frop\" HTTP/1.1\r\n"
+ "Host: example.com\r\n"
+ "\r\n",
+ .error_code = HTTP_REQUEST_PARSE_ERROR_BAD_REQUEST
+ },
+ {
+ .request =
+ "GET / HTCPCP/1.0\r\n"
+ "Host: example.com\r\n"
+ "\r\n",
+ .error_code = HTTP_REQUEST_PARSE_ERROR_BROKEN_REQUEST
+ },
+ {
+ .request =
+ "GET / HTTP/1.0.1\r\n"
+ "Host: example.com\r\n"
+ "\r\n",
+ .error_code = HTTP_REQUEST_PARSE_ERROR_BROKEN_REQUEST
+ },
+ {
+ .request =
+ "GET / HTTP/1.1\r\n"
+ "Host: \"example.com\r\n"
+ "\r\n",
+ .error_code = HTTP_REQUEST_PARSE_ERROR_BAD_REQUEST
+ },
+ {
+ .request =
+ "GET / HTTP/1.1\r\n"
+ "\r\n",
+ .error_code = HTTP_REQUEST_PARSE_ERROR_BAD_REQUEST
+ },
+ {
+ .request =
+ "GET / HTTP/1.1\r\n"
+ "Host: www.example.com\r\n"
+ "Transfer-Encoding: gzip\r\n"
+ "\r\n",
+ .error_code = HTTP_REQUEST_PARSE_ERROR_BROKEN_REQUEST
+ },
+ {
+ .request =
+ "GET / HTTP/1.1\r\n"
+ "Host: www.example.com\r\n"
+ "Expect: payment\r\n"
+ "\r\n",
+ .error_code = HTTP_REQUEST_PARSE_ERROR_EXPECTATION_FAILED
+ },
+ {
+ .request =
+ "GET / HTTP/1.1\r\n"
+ "Host: www.example.com\r\n"
+ "Transfer-Encoding: cuneiform, chunked\r\n"
+ "\r\n",
+ .error_code = HTTP_REQUEST_PARSE_ERROR_NOT_IMPLEMENTED
+ },
+ {
+ .request =
+ "GET / HTTP/1.1\r\n"
+ "Date: Mon, 09 Kul 2018 02:24:29 GMT\r\n"
+ "Host: example.com\r\n"
+ "\r\n",
+ .flags = HTTP_REQUEST_PARSE_FLAG_STRICT,
+ .error_code = HTTP_REQUEST_PARSE_ERROR_BROKEN_REQUEST
+ },
+ {
+ .request =
+ "GET / HTTP/1.1\r\n"
+ "Date: Sun, 07 Oct 2012 19:52:03 GMT\r\n"
+ "Host: example.com\r\n"
+ "Date: Sun, 13 Oct 2013 13:13:13 GMT\r\n"
+ "\r\n",
+ .flags = HTTP_REQUEST_PARSE_FLAG_STRICT,
+ .error_code = HTTP_REQUEST_PARSE_ERROR_BROKEN_REQUEST
+ }
+ // FIXME: test request limits
+};
+
+static unsigned int invalid_request_parse_test_count =
+ N_ELEMENTS(invalid_request_parse_tests);
+
+static const char *
+_request_parse_error(enum http_request_parse_error error)
+{
+ switch (error) {
+ case HTTP_REQUEST_PARSE_ERROR_NONE:
+ return "none?!";
+ case HTTP_REQUEST_PARSE_ERROR_BROKEN_STREAM:
+ return "broken stream";
+ case HTTP_REQUEST_PARSE_ERROR_BROKEN_REQUEST:
+ return "broken request";
+ case HTTP_REQUEST_PARSE_ERROR_BAD_REQUEST:
+ return "bad request";
+ case HTTP_REQUEST_PARSE_ERROR_NOT_IMPLEMENTED:
+ return "not implemented";
+ case HTTP_REQUEST_PARSE_ERROR_EXPECTATION_FAILED:
+ return "expectation failed";
+ case HTTP_REQUEST_PARSE_ERROR_METHOD_TOO_LONG:
+ return "method too long";
+ case HTTP_REQUEST_PARSE_ERROR_TARGET_TOO_LONG:
+ return "target too long";
+ case HTTP_REQUEST_PARSE_ERROR_PAYLOAD_TOO_LARGE:
+ return "payload too large";
+ }
+ return t_strdup_printf("<<UNKNOWN: %u>>", error);
+}
+
+static void test_http_request_parse_invalid(void)
+{
+ const struct http_request_invalid_parse_test *test;
+ struct http_request_parser *parser;
+ struct http_request request;
+ enum http_request_parse_error error_code;
+ const char *request_text, *error;
+ struct istream *input;
+ int ret;
+ unsigned int i;
+
+ for (i = 0; i < invalid_request_parse_test_count; i++) T_BEGIN {
+ test = &invalid_request_parse_tests[i];
+ request_text = test->request;
+ input = i_stream_create_from_data(request_text,
+ strlen(request_text));
+ parser = http_request_parser_init(input, &default_base_url,
+ NULL, test->flags);
+ i_stream_unref(&input);
+
+ test_begin(t_strdup_printf("http request invalid [%d]", i));
+
+ while ((ret=http_request_parse_next
+ (parser, NULL, &request, &error_code, &error)) > 0);
+
+ test_out_reason("parse failure", ret < 0, error);
+ if (ret < 0) {
+ const char *error = _request_parse_error(error_code);
+ test_out(t_strdup_printf("parse error code = %s",
+ error),
+ error_code == test->error_code);
+ }
+ test_end();
+ http_request_parser_deinit(&parser);
+ } T_END;
+}
+
+/*
+ * Bad request tests
+ */
+
+static const unsigned char bad_request_with_nuls[] =
+ "GET / HTTP/1.1\r\n"
+ "Host: example.com\r\n"
+ "User-Agent: text\0client\r\n"
+ "\r\n";
+
+static void test_http_request_parse_bad(void)
+{
+ struct http_request_parser *parser;
+ struct http_request request;
+ enum http_request_parse_error error_code;
+ const char *header, *error;
+ struct istream *input;
+ int ret;
+
+ /* parse failure guarantees http_request_header.size equals
+ strlen(http_request_header.value) */
+ test_begin("http request with NULs (strict)");
+ input = i_stream_create_from_data(bad_request_with_nuls,
+ sizeof(bad_request_with_nuls)-1);
+ parser = http_request_parser_init(input, &default_base_url, NULL,
+ HTTP_REQUEST_PARSE_FLAG_STRICT);
+ i_stream_unref(&input);
+
+ while ((ret=http_request_parse_next
+ (parser, NULL, &request, &error_code, &error)) > 0);
+ test_assert(ret < 0);
+ http_request_parser_deinit(&parser);
+ test_end();
+
+ /* even when lenient, bad characters like NUL must not be returned */
+ test_begin("http request with NULs (lenient)");
+ input = i_stream_create_from_data(bad_request_with_nuls,
+ sizeof(bad_request_with_nuls)-1);
+ parser = http_request_parser_init(input, &default_base_url, NULL, 0);
+ i_stream_unref(&input);
+
+ ret = http_request_parse_next
+ (parser, NULL, &request, &error_code, &error);
+ test_out("parse success", ret > 0);
+ header = http_request_header_get(&request, "user-agent");
+ test_out("header present", header != NULL);
+ if (header != NULL) {
+ test_out(t_strdup_printf("header User-Agent: %s", header),
+ strcmp(header, "textclient") == 0);
+ }
+ ret = http_request_parse_next
+ (parser, NULL, &request, &error_code, &error);
+ test_out("parse end", ret == 0);
+ http_request_parser_deinit(&parser);
+ test_end();
+}
+
+int main(void)
+{
+ static void (*const test_functions[])(void) = {
+ test_http_request_parse_valid,
+ test_http_request_parse_invalid,
+ test_http_request_parse_bad,
+ NULL
+ };
+ return test_run(test_functions);
+}
diff --git a/src/lib-http/test-http-response-parser.c b/src/lib-http/test-http-response-parser.c
new file mode 100644
index 0000000..3964a32
--- /dev/null
+++ b/src/lib-http/test-http-response-parser.c
@@ -0,0 +1,406 @@
+/* Copyright (c) 2013-2018 Dovecot authors, see the included COPYING file */
+
+#include "test-lib.h"
+#include "buffer.h"
+#include "str.h"
+#include "str-sanitize.h"
+#include "istream.h"
+#include "ostream.h"
+#include "test-common.h"
+#include "http-response-parser.h"
+
+#include <time.h>
+
+struct valid_parse_test_response {
+ unsigned char version_major;
+ unsigned char version_minor;
+ unsigned int status;
+ uoff_t content_length;
+ const char *payload;
+};
+
+struct valid_parse_test {
+ const char *input;
+ enum http_response_parse_flags flags;
+
+ const struct valid_parse_test_response *responses;
+ unsigned int responses_count;
+};
+
+/* Valid response tests */
+
+static const struct valid_parse_test_response valid_responses1[] = {
+ {
+ .status = 200,
+ .payload = "This is a piece of stupid text.\r\n"
+ }
+};
+
+static const struct valid_parse_test_response valid_responses2[] = {
+ {
+ .status = 200,
+ .payload = "This is a piece of stupid text.\r\n"
+ },{
+ .status = 200,
+ .payload = "This is a piece of even more stupid text.\r\n"
+ }
+};
+
+static const struct valid_parse_test_response valid_responses3[] = {
+ {
+ .status = 401,
+ .payload = "Frop!"
+ }
+};
+
+static const struct valid_parse_test_response valid_responses4[] = {
+ {
+ .status = 200,
+ .payload = "Invalid date header"
+ }
+};
+
+static const struct valid_parse_test_response valid_responses5[] = {
+ {
+ .status = 200,
+ .payload = "Duplicate headers"
+ }
+};
+
+static const struct valid_parse_test
+valid_response_parse_tests[] = {
+ { .input =
+ "HTTP/1.1 200 OK\r\n"
+ "Date: Sun, 07 Oct 2012 13:02:27 GMT\r\n"
+ "Server: Apache/2.2.16 (Debian)\r\n"
+ "Last-Modified: Tue, 18 Sep 2012 19:31:41 GMT\r\n"
+ "Etag: \"2a8400c-10751f-4c9fef0858140\"\r\n"
+ "Accept-Ranges: bytes\r\n"
+ "Content-Length: 33\r\n"
+ "Keep-Alive: timeout=15, max=100\r\n"
+ "Connection: Keep-Alive\r\n"
+ "Content-Type: text/plain\r\n"
+ "\r\n"
+ "This is a piece of stupid text.\r\n",
+ .responses = valid_responses1,
+ .responses_count = N_ELEMENTS(valid_responses1)
+ },{
+ .input =
+ "HTTP/1.1 200 OK\r\n"
+ "Date: Sun, 07 Oct 2012 13:02:27 GMT\r\n"
+ "Server: Apache/2.2.16 (Debian)\r\n"
+ "Last-Modified: Tue, 18 Sep 2012 19:31:41 GMT\r\n"
+ "Etag: \"2a8400c-10751f-4c9fef0858140\"\r\n"
+ "Accept-Ranges: bytes\r\n"
+ "Content-Length: 33\r\n"
+ "Keep-Alive: timeout=15, max=100\r\n"
+ "Connection: Keep-Alive\r\n"
+ "Content-Type: text/plain\r\n"
+ "\r\n"
+ "This is a piece of stupid text.\r\n"
+ "HTTP/1.1 200 OK\r\n"
+ "Date: Sun, 07 Oct 2012 13:02:27 GMT\r\n"
+ "Server: Apache/2.2.16 (Debian)\r\n"
+ "Last-Modified: Tue, 18 Sep 2012 19:31:41 GMT\r\n"
+ "Etag: \"2a8400c-10751f-4c9fef0858140\"\r\n"
+ "Accept-Ranges: bytes\r\n"
+ "Content-Length: 43\r\n"
+ "Keep-Alive: timeout=15, max=100\r\n"
+ "Connection: Keep-Alive\r\n"
+ "Content-Type: text/plain\r\n"
+ "\r\n"
+ "This is a piece of even more stupid text.\r\n",
+ .responses = valid_responses2,
+ .responses_count = N_ELEMENTS(valid_responses2)
+ },{
+ .input =
+ "HTTP/1.1 401 Authorization Required\r\n"
+ "Date: Sun, 07 Oct 2012 19:52:03 GMT\r\n"
+ "Server: Apache/2.2.16 (Debian) PHP/5.3.3-7+squeeze14\r\n"
+ "WWW-Authenticate: Basic realm=\"Munin\"\r\n"
+ "Vary: Accept-Encoding\r\n"
+ "Content-Encoding: gzip\r\n"
+ "Content-Length: 5\r\n"
+ "Keep-Alive: timeout=15, max=99\r\n"
+ "Connection: Keep-Alive\r\n"
+ "Content-Type: text/html; charset=iso-8859-1\r\n"
+ "\r\n"
+ "Frop!",
+ .responses = valid_responses3,
+ .responses_count = N_ELEMENTS(valid_responses3)
+ },{
+ .input =
+ "HTTP/1.1 200 OK\r\n"
+ "Date: Sun, 07 Ocu 2012 19:52:03 GMT\r\n"
+ "Content-Length: 19\r\n"
+ "Keep-Alive: timeout=15, max=99\r\n"
+ "Connection: Keep-Alive\r\n"
+ "Date: Sun, 13 Oct 2013 13:13:13 GMT\r\n"
+ "\r\n"
+ "Invalid date header",
+ .responses = valid_responses4,
+ .responses_count = N_ELEMENTS(valid_responses4)
+ },{
+ .input =
+ "HTTP/1.1 200 OK\r\n"
+ "Date: Sun, 07 Oct 2012 19:52:03 GMT\r\n"
+ "Server: Apache/2.2.16 (Debian) PHP/5.3.3-7+squeeze14\r\n"
+ "Content-Length: 17\r\n"
+ "Keep-Alive: timeout=15, max=99\r\n"
+ "Connection: Keep-Alive\r\n"
+ "Content-Type: text/html; charset=iso-8859-1\r\n"
+ "Date: Sun, 13 Oct 2013 13:13:13 GMT\r\n"
+ "\r\n"
+ "Duplicate headers",
+ .responses = valid_responses5,
+ .responses_count = N_ELEMENTS(valid_responses5)
+ }
+};
+
+static const unsigned int valid_response_parse_test_count =
+ N_ELEMENTS(valid_response_parse_tests);
+
+static void test_http_response_parse_valid(void)
+{
+ unsigned int i;
+ buffer_t *payload_buffer = buffer_create_dynamic(default_pool, 1024);
+
+ for (i = 0; i < valid_response_parse_test_count; i++) T_BEGIN {
+ struct istream *input;
+ struct ostream *output;
+ const struct valid_parse_test *test;
+ const struct valid_parse_test_response *tresponse;
+ struct http_response_parser *parser;
+ struct http_response presponse;
+ const char *input_text, *payload, *error;
+ unsigned int j, pos, input_text_len;
+ int ret = 0;
+
+ i_zero(&presponse);
+ test = &valid_response_parse_tests[i];
+ input_text = test->input;
+ input_text_len = strlen(input_text);
+ input = test_istream_create_data(input_text, input_text_len);
+ parser = http_response_parser_init(input, NULL,
+ valid_response_parse_tests[i].flags);
+
+ test_begin(t_strdup_printf("http response valid [%d]", i));
+
+ payload = NULL;
+ for (pos = 0; pos < input_text_len && ret == 0; pos++) {
+ test_istream_set_size(input, pos);
+ ret = http_response_parse_next(parser,
+ HTTP_RESPONSE_PAYLOAD_TYPE_ALLOWED, &presponse, &error);
+ }
+ test_istream_set_size(input, input_text_len);
+ i_stream_unref(&input);
+
+ j = 0;
+ test_out("parse success", ret > 0);
+ while (ret > 0) {
+ if (presponse.payload != NULL) {
+ buffer_set_used_size(payload_buffer, 0);
+ output = o_stream_create_buffer(payload_buffer);
+ test_out("payload receive",
+ o_stream_send_istream(output, presponse.payload)
+ == OSTREAM_SEND_ISTREAM_RESULT_FINISHED);
+ o_stream_destroy(&output);
+ payload = str_c(payload_buffer);
+ } else {
+ payload = NULL;
+ }
+
+ test_assert(j < test->responses_count);
+ if (j >= test->responses_count)
+ break;
+ tresponse = &test->responses[j];
+
+ /* verify last response only */
+ test_out(t_strdup_printf("response->status = %d",
+ tresponse->status),
+ presponse.status == tresponse->status);
+ if (payload == NULL || tresponse->payload == NULL) {
+ test_out(t_strdup_printf("response->payload = %s",
+ str_sanitize(payload, 80)),
+ payload == tresponse->payload);
+ } else {
+ test_out(t_strdup_printf("response->payload = %s",
+ str_sanitize(payload, 80)),
+ strcmp(payload, tresponse->payload) == 0);
+ }
+
+ ret = http_response_parse_next(parser,
+ HTTP_RESPONSE_PAYLOAD_TYPE_ALLOWED, &presponse, &error);
+ if (++j == test->responses_count)
+ test_out("parse end", ret == 0);
+ else
+ test_out("parse success", ret > 0);
+ }
+
+ test_assert(ret == 0);
+ test_end();
+ http_response_parser_deinit(&parser);
+ } T_END;
+
+ buffer_free(&payload_buffer);
+}
+
+/*
+ * Invalid response tests
+ */
+
+struct invalid_parse_test {
+ const char *input;
+ enum http_response_parse_flags flags;
+};
+
+static struct invalid_parse_test invalid_response_parse_tests[] = {
+ {
+ .input =
+ "XMPP/1.0 302 Found\r\n"
+ "Location: http://www.example.nl/\r\n"
+ "Cache-Control: private\r\n"
+ },{
+ .input =
+ "HTTP/1.1 302 Found\r\n"
+ "Location: http://www.example.nl/\r\n"
+ "Cache-Control: private\r\n"
+ },{
+ .input =
+ "HTTP/1.1 ABC Found\r\n"
+ "Location: http://www.example.nl/\r\n"
+ "Cache-Control: private\r\n"
+ },{
+ .input =
+ "HTTP/1.1 302 \177\r\n"
+ "Location: http://www.example.nl/\r\n"
+ "Cache-Control: private\r\n"
+ },{
+ .input =
+ "HTTP/1.1 302 Found\n\r"
+ "Location: http://www.example.nl/\n\r"
+ "Cache-Control: private\n\r"
+ },{
+ .input =
+ "HTTP/1.1 200 OK\r\n"
+ "Date: Sun, 07 Ocu 2012 19:52:03 GMT\r\n"
+ "Content-Length: 19\r\n"
+ "Keep-Alive: timeout=15, max=99\r\n"
+ "Connection: Keep-Alive\r\n"
+ "\r\n"
+ "Invalid date header",
+ .flags = HTTP_RESPONSE_PARSE_FLAG_STRICT
+ },{
+ .input =
+ "HTTP/1.1 200 OK\r\n"
+ "Date: Sun, 07 Oct 2012 19:52:03 GMT\r\n"
+ "Server: Apache/2.2.16 (Debian) PHP/5.3.3-7+squeeze14\r\n"
+ "Content-Length: 17\r\n"
+ "Keep-Alive: timeout=15, max=99\r\n"
+ "Connection: Keep-Alive\r\n"
+ "Content-Type: text/html; charset=iso-8859-1\r\n"
+ "Date: Sun, 13 Oct 2013 13:13:13 GMT\r\n"
+ "\r\n"
+ "Duplicate headers",
+ .flags = HTTP_RESPONSE_PARSE_FLAG_STRICT
+ }
+};
+
+static const unsigned int invalid_response_parse_test_count =
+ N_ELEMENTS(invalid_response_parse_tests);
+
+static void test_http_response_parse_invalid(void)
+{
+ struct http_response_parser *parser;
+ struct http_response response;
+ const char *response_text, *error;
+ struct istream *input;
+ int ret;
+ unsigned int i;
+
+ for (i = 0; i < invalid_response_parse_test_count; i++) T_BEGIN {
+ const char *test;
+
+ test = invalid_response_parse_tests[i].input;
+ response_text = test;
+ input = i_stream_create_from_data(response_text, strlen(response_text));
+ parser = http_response_parser_init(input, NULL,
+ invalid_response_parse_tests[i].flags);
+ i_stream_unref(&input);
+
+ test_begin(t_strdup_printf("http response invalid [%d]", i));
+
+ while ((ret=http_response_parse_next(parser, HTTP_RESPONSE_PAYLOAD_TYPE_ALLOWED, &response, &error)) > 0);
+
+ test_out_reason("parse failure", ret < 0, error);
+ test_end();
+ http_response_parser_deinit(&parser);
+ } T_END;
+}
+
+/*
+ * Bad response tests
+ */
+
+static const unsigned char bad_response_with_nuls[] =
+ "HTTP/1.1 200 OK\r\n"
+ "Server: text\0server\r\n"
+ "\r\n";
+
+static void test_http_response_parse_bad(void)
+{
+ struct http_response_parser *parser;
+ struct http_response response;
+ const char *header, *error;
+ struct istream *input;
+ int ret;
+
+ /* parse failure guarantees http_response_header.size equals
+ strlen(http_response_header.value) */
+
+ test_begin("http response with NULs (strict)");
+ input = i_stream_create_from_data(bad_response_with_nuls,
+ sizeof(bad_response_with_nuls)-1);
+ parser = http_response_parser_init(input, NULL,
+ HTTP_RESPONSE_PARSE_FLAG_STRICT);
+ i_stream_unref(&input);
+
+ while ((ret=http_response_parse_next(parser,
+ HTTP_RESPONSE_PAYLOAD_TYPE_ALLOWED, &response, &error)) > 0);
+ test_assert(ret < 0);
+ http_response_parser_deinit(&parser);
+ test_end();
+
+ /* even when lenient, bad characters like NUL must not be returned */
+ test_begin("http response with NULs (lenient)");
+ input = i_stream_create_from_data(bad_response_with_nuls,
+ sizeof(bad_response_with_nuls)-1);
+ parser = http_response_parser_init(input, NULL, 0);
+ i_stream_unref(&input);
+
+ ret = http_response_parse_next(parser,
+ HTTP_RESPONSE_PAYLOAD_TYPE_ALLOWED, &response, &error);
+ test_out("parse success", ret > 0);
+ header = http_response_header_get(&response, "server");
+ test_out("header present", header != NULL);
+ if (header != NULL) {
+ test_out(t_strdup_printf("header Server: %s", header),
+ strcmp(header, "textserver") == 0);
+ }
+ ret = http_response_parse_next(parser,
+ HTTP_RESPONSE_PAYLOAD_TYPE_ALLOWED, &response, &error);
+ test_out("parse end", ret == 0);
+ http_response_parser_deinit(&parser);
+ test_end();
+}
+
+int main(void)
+{
+ static void (*const test_functions[])(void) = {
+ test_http_response_parse_valid,
+ test_http_response_parse_invalid,
+ test_http_response_parse_bad,
+ NULL
+ };
+ return test_run(test_functions);
+}
diff --git a/src/lib-http/test-http-server-errors.c b/src/lib-http/test-http-server-errors.c
new file mode 100644
index 0000000..4a95bde
--- /dev/null
+++ b/src/lib-http/test-http-server-errors.c
@@ -0,0 +1,1042 @@
+/* Copyright (c) 2016-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "str.h"
+#include "hostpid.h"
+#include "ioloop.h"
+#include "istream.h"
+#include "ostream.h"
+#include "time-util.h"
+#include "sleep.h"
+#include "connection.h"
+#include "test-common.h"
+#include "test-subprocess.h"
+#include "http-url.h"
+#include "http-request.h"
+#include "http-server.h"
+
+#include <unistd.h>
+
+#define SERVER_MAX_TIMEOUT_MSECS 10*1000
+#define CLIENT_KILL_TIMEOUT_SECS 20
+
+static void main_deinit(void);
+
+/*
+ * Types
+ */
+
+struct client_connection {
+ struct connection conn;
+
+ pool_t pool;
+};
+
+typedef void
+(*test_server_init_t)(const struct http_server_settings *server_set);
+typedef void (*test_client_init_t)(unsigned int index);
+
+/*
+ * State
+ */
+
+/* common */
+static struct ip_addr bind_ip;
+static in_port_t bind_port = 0;
+static struct ioloop *ioloop;
+static bool debug = FALSE;
+
+/* server */
+static struct http_server *http_server = NULL;
+static struct io *io_listen;
+static int fd_listen = -1;
+static void (*test_server_request)(struct http_server_request *req);
+
+/* client */
+static struct connection_list *client_conn_list;
+static unsigned int client_index;
+static void (*test_client_connected)(struct client_connection *conn);
+static void (*test_client_input)(struct client_connection *conn);
+
+/*
+ * Forward declarations
+ */
+
+/* server */
+static void test_server_defaults(struct http_server_settings *http_set);
+static void test_server_run(const struct http_server_settings *http_set);
+
+/* client */
+static void client_connection_deinit(struct client_connection **_conn);
+static void test_client_run(unsigned int index);
+
+/* test*/
+static void
+test_run_client_server(const struct http_server_settings *server_set,
+ test_server_init_t server_test,
+ test_client_init_t client_test,
+ unsigned int client_tests_count) ATTR_NULL(3);
+
+/*
+ * Slow request
+ */
+
+/* client */
+
+static void
+test_slow_request_input(struct client_connection *conn ATTR_UNUSED)
+{
+ /* do nothing */
+}
+
+static void test_slow_request_connected(struct client_connection *conn)
+{
+ o_stream_nsend_str(conn->conn.output,
+ "GET / HTTP/1.1\r\n"
+ "Host: example.com\r\n"
+ "\r\n");
+}
+
+static void test_client_slow_request(unsigned int index)
+{
+ test_client_input = test_slow_request_input;
+ test_client_connected = test_slow_request_connected;
+ test_client_run(index);
+}
+
+/* server */
+
+struct _slow_request {
+ struct http_server_request *req;
+ struct timeout *to_delay;
+ bool serviced:1;
+};
+
+static void test_server_slow_request_destroyed(struct _slow_request *ctx)
+{
+ test_assert(ctx->serviced);
+ timeout_remove(&ctx->to_delay);
+ i_free(ctx);
+ io_loop_stop(ioloop);
+}
+
+static void test_server_slow_request_delayed(struct _slow_request *ctx)
+{
+ struct http_server_response *resp;
+ struct http_server_request *req = ctx->req;
+
+ resp = http_server_response_create(req, 200, "OK");
+ http_server_response_submit(resp);
+ ctx->serviced = TRUE;
+
+ http_server_request_unref(&req);
+}
+
+static void test_server_slow_request_request(struct http_server_request *req)
+{
+ const struct http_request *hreq = http_server_request_get(req);
+ struct _slow_request *ctx;
+
+ if (debug) {
+ i_debug("REQUEST: %s %s HTTP/%u.%u",
+ hreq->method, hreq->target_raw,
+ hreq->version_major, hreq->version_minor);
+ }
+
+ ctx = i_new(struct _slow_request, 1);
+ ctx->req = req;
+
+ http_server_request_set_destroy_callback(
+ req, test_server_slow_request_destroyed, ctx);
+
+ http_server_request_ref(req);
+ ctx->to_delay =
+ timeout_add(4000, test_server_slow_request_delayed, ctx);
+}
+
+static void
+test_server_slow_request(const struct http_server_settings *server_set)
+{
+ test_server_request = test_server_slow_request_request;
+ test_server_run(server_set);
+}
+
+/* test */
+
+static void test_slow_request(void)
+{
+ struct http_server_settings http_server_set;
+
+ test_server_defaults(&http_server_set);
+ http_server_set.max_client_idle_time_msecs = 1000;
+
+ test_begin("slow request");
+ test_run_client_server(&http_server_set, test_server_slow_request,
+ test_client_slow_request, 1);
+ test_end();
+}
+
+/*
+ * Hanging request payload
+ */
+
+/* client */
+
+static void
+test_hanging_request_payload_connected(struct client_connection *conn)
+{
+ o_stream_nsend_str(conn->conn.output,
+ "GET / HTTP/1.1\r\n"
+ "Host: example.com\r\n"
+ "Content-Length: 1000\r\n"
+ "\r\n"
+ "To be continued... or not");
+}
+
+static void test_client_hanging_request_payload(unsigned int index)
+{
+ test_client_connected = test_hanging_request_payload_connected;
+ test_client_run(index);
+}
+
+/* server */
+
+struct _hanging_request_payload {
+ struct http_server_request *req;
+ struct istream *payload_input;
+ struct io *io;
+ bool serviced:1;
+};
+
+static void
+test_server_hanging_request_payload_destroyed(
+ struct _hanging_request_payload *ctx)
+{
+ test_assert(!ctx->serviced);
+ io_remove(&ctx->io);
+ i_free(ctx);
+ io_loop_stop(ioloop);
+}
+
+static void
+test_server_hanging_request_payload_input(struct _hanging_request_payload *ctx)
+{
+ struct http_server_response *resp;
+ struct http_server_request *req = ctx->req;
+ const unsigned char *data;
+ size_t size;
+ int ret;
+
+ if (debug)
+ i_debug("test server: got more payload");
+
+ while ((ret = i_stream_read_data(ctx->payload_input,
+ &data, &size, 0)) > 0)
+ i_stream_skip(ctx->payload_input, size);
+
+ if (ret == 0)
+ return;
+ if (ctx->payload_input->stream_errno != 0) {
+ if (debug) {
+ i_debug("test server: failed to read payload: %s",
+ i_stream_get_error(ctx->payload_input));
+ }
+ i_stream_unref(&ctx->payload_input);
+ io_remove(&ctx->io);
+ http_server_request_fail_close(req, 400, "Bad request");
+ http_server_request_unref(&req);
+ return;
+ }
+
+ i_assert(ctx->payload_input->eof);
+
+ resp = http_server_response_create(req, 200, "OK");
+ http_server_response_submit(resp);
+ ctx->serviced = TRUE;
+
+ i_stream_unref(&ctx->payload_input);
+ http_server_request_unref(&req);
+}
+
+static void
+test_server_hanging_request_payload_request(struct http_server_request *req)
+{
+ const struct http_request *hreq = http_server_request_get(req);
+ struct _hanging_request_payload *ctx;
+
+ if (debug) {
+ i_debug("REQUEST: %s %s HTTP/%u.%u",
+ hreq->method, hreq->target_raw,
+ hreq->version_major, hreq->version_minor);
+ }
+
+ ctx = i_new(struct _hanging_request_payload, 1);
+ ctx->req = req;
+
+ http_server_request_set_destroy_callback(
+ req, test_server_hanging_request_payload_destroyed, ctx);
+
+ ctx->payload_input = http_server_request_get_payload_input(req, FALSE);
+
+ http_server_request_ref(req);
+ ctx->io = io_add_istream(ctx->payload_input,
+ test_server_hanging_request_payload_input,
+ ctx);
+ test_server_hanging_request_payload_input(ctx);
+}
+
+static void
+test_server_hanging_request_payload(
+ const struct http_server_settings *server_set)
+{
+ test_server_request = test_server_hanging_request_payload_request;
+ test_server_run(server_set);
+}
+
+/* test */
+
+static void test_hanging_request_payload(void)
+{
+ struct http_server_settings http_server_set;
+
+ test_server_defaults(&http_server_set);
+ http_server_set.max_client_idle_time_msecs = 1000;
+
+ test_begin("hanging request payload");
+ test_run_client_server(&http_server_set,
+ test_server_hanging_request_payload,
+ test_client_hanging_request_payload, 1);
+ test_end();
+}
+
+/*
+ * Hanging response payload
+ */
+
+/* client */
+
+static void
+test_hanging_response_payload_connected(struct client_connection *conn)
+{
+ o_stream_nsend_str(conn->conn.output,
+ "GET / HTTP/1.1\r\n"
+ "Host: example.com\r\n"
+ "Content-Length: 18\r\n"
+ "\r\n"
+ "Complete payload\r\n");
+}
+
+static void test_client_hanging_response_payload(unsigned int index)
+{
+ test_client_connected = test_hanging_response_payload_connected;
+ test_client_run(index);
+}
+
+/* server */
+
+struct _hanging_response_payload {
+ struct http_server_request *req;
+ struct istream *payload_input;
+ struct io *io;
+ bool serviced:1;
+};
+
+static void
+test_server_hanging_response_payload_destroyed(
+ struct _hanging_response_payload *ctx)
+{
+ test_assert(!ctx->serviced);
+ io_remove(&ctx->io);
+ i_free(ctx);
+ io_loop_stop(ioloop);
+}
+
+static void
+test_server_hanging_response_payload_request(struct http_server_request *req)
+{
+ const struct http_request *hreq =
+ http_server_request_get(req);
+ struct http_server_response *resp;
+ struct _hanging_response_payload *ctx;
+ string_t *payload;
+ unsigned int i;
+
+ if (debug) {
+ i_debug("REQUEST: %s %s HTTP/%u.%u",
+ hreq->method, hreq->target_raw,
+ hreq->version_major, hreq->version_minor);
+ }
+
+ ctx = i_new(struct _hanging_response_payload, 1);
+ ctx->req = req;
+
+ http_server_request_set_destroy_callback(
+ req, test_server_hanging_response_payload_destroyed, ctx);
+
+ resp = http_server_response_create(req, 200, "OK");
+ T_BEGIN {
+ payload = t_str_new(204800);
+ for (i = 0; i < 3200; i++) {
+ str_append(payload,
+ "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\r\n"
+ "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\r\n");
+ }
+
+ http_server_response_set_payload_data(resp, str_data(payload),
+ str_len(payload));
+ } T_END;
+ http_server_response_submit(resp);
+}
+
+static void
+test_server_hanging_response_payload(
+ const struct http_server_settings *server_set)
+{
+ test_server_request = test_server_hanging_response_payload_request;
+ test_server_run(server_set);
+}
+
+/* test */
+
+static void test_hanging_response_payload(void)
+{
+ struct http_server_settings http_server_set;
+
+ test_server_defaults(&http_server_set);
+ http_server_set.socket_send_buffer_size = 4096;
+ http_server_set.max_client_idle_time_msecs = 1000;
+
+ test_begin("hanging response payload");
+ test_run_client_server(&http_server_set,
+ test_server_hanging_response_payload,
+ test_client_hanging_response_payload, 1);
+ test_end();
+}
+
+/*
+ * Excessive payload length
+ */
+
+/* client */
+
+static void
+test_excessive_payload_length_connected1(struct client_connection *conn)
+{
+ o_stream_nsend_str(
+ conn->conn.output,
+ "GET / HTTP/1.1\r\n"
+ "Host: example.com\r\n"
+ "Content-Length: 150\r\n"
+ "\r\n"
+ "Too long\r\nToo long\r\nToo long\r\nToo long\r\nToo long\r\n"
+ "Too long\r\nToo long\r\nToo long\r\nToo long\r\nToo long\r\n"
+ "Too long\r\nToo long\r\nToo long\r\nToo long\r\nToo long\r\n");
+}
+
+static void test_client_excessive_payload_length1(unsigned int index)
+{
+ test_client_connected = test_excessive_payload_length_connected1;
+ test_client_run(index);
+}
+
+static void
+test_excessive_payload_length_connected2(struct client_connection *conn)
+{
+ o_stream_nsend_str(
+ conn->conn.output,
+ "GET / HTTP/1.1\r\n"
+ "Host: example.com\r\n"
+ "Transfer-Encoding: chunked\r\n"
+ "\r\n"
+ "32\r\n"
+ "Too long\r\nToo long\r\nToo long\r\nToo long\r\nToo long\r\n"
+ "\r\n"
+ "32\r\n"
+ "Too long\r\nToo long\r\nToo long\r\nToo long\r\nToo long\r\n"
+ "\r\n"
+ "32\r\n"
+ "Too long\r\nToo long\r\nToo long\r\nToo long\r\nToo long\r\n"
+ "\r\n"
+ "0\r\n"
+ "\r\n");
+}
+
+static void test_client_excessive_payload_length2(unsigned int index)
+{
+ test_client_connected = test_excessive_payload_length_connected2;
+ test_client_run(index);
+}
+
+/* server */
+
+struct _excessive_payload_length {
+ struct http_server_request *req;
+ buffer_t *buffer;
+ bool serviced:1;
+};
+
+static void
+test_server_excessive_payload_length_destroyed(
+ struct _excessive_payload_length *ctx)
+{
+ struct http_server_response *resp;
+ const char *reason;
+ int status;
+
+ resp = http_server_request_get_response(ctx->req);
+ test_assert(resp != NULL);
+ if (resp != NULL) {
+ http_server_response_get_status(resp, &status, &reason);
+ test_assert(status == 413);
+ }
+
+ test_assert(!ctx->serviced);
+ buffer_free(&ctx->buffer);
+ i_free(ctx);
+ io_loop_stop(ioloop);
+}
+
+static void
+test_server_excessive_payload_length_finished(
+ struct _excessive_payload_length *ctx)
+{
+ struct http_server_response *resp;
+
+ resp = http_server_response_create(ctx->req, 200, "OK");
+ http_server_response_submit(resp);
+ ctx->serviced = TRUE;
+}
+
+static void
+test_server_excessive_payload_length_request(struct http_server_request *req)
+{
+ const struct http_request *hreq = http_server_request_get(req);
+ struct _excessive_payload_length *ctx;
+
+ if (debug) {
+ i_debug("REQUEST: %s %s HTTP/%u.%u",
+ hreq->method, hreq->target_raw,
+ hreq->version_major, hreq->version_minor);
+ }
+
+ ctx = i_new(struct _excessive_payload_length, 1);
+ ctx->req = req;
+ ctx->buffer = buffer_create_dynamic(default_pool, 128);
+
+ http_server_request_set_destroy_callback(
+ req, test_server_excessive_payload_length_destroyed, ctx);
+ http_server_request_buffer_payload(
+ req, ctx->buffer, 128,
+ test_server_excessive_payload_length_finished, ctx);
+}
+
+static void
+test_server_excessive_payload_length(
+ const struct http_server_settings *server_set)
+{
+ test_server_request = test_server_excessive_payload_length_request;
+ test_server_run(server_set);
+}
+
+/* test */
+
+static void test_excessive_payload_length(void)
+{
+ struct http_server_settings http_server_set;
+
+ test_server_defaults(&http_server_set);
+ http_server_set.max_client_idle_time_msecs = 1000;
+
+ test_begin("excessive payload length (length)");
+ test_run_client_server(&http_server_set,
+ test_server_excessive_payload_length,
+ test_client_excessive_payload_length1, 1);
+ test_end();
+
+ test_begin("excessive payload length (chunked)");
+ test_run_client_server(&http_server_set,
+ test_server_excessive_payload_length,
+ test_client_excessive_payload_length2, 1);
+ test_end();
+}
+
+/*
+ * Response ostream disconnect
+ */
+
+/* client */
+
+static void
+test_response_ostream_disconnect_connected(struct client_connection *conn)
+{
+ o_stream_nsend_str(conn->conn.output,
+ "GET / HTTP/1.1\r\n"
+ "Host: example.com\r\n"
+ "Content-Length: 18\r\n"
+ "\r\n"
+ "Complete payload\r\n");
+ i_sleep_intr_msecs(10);
+ client_connection_deinit(&conn);
+ io_loop_stop(ioloop);
+}
+
+static void test_client_response_ostream_disconnect(unsigned int index)
+{
+ test_client_connected = test_response_ostream_disconnect_connected;
+ test_client_run(index);
+}
+
+/* server */
+
+struct _response_ostream_disconnect {
+ struct http_server_request *req;
+ struct istream *payload_input;
+ struct ostream *payload_output;
+ struct io *io;
+ bool finished:1;
+ bool seen_stream_error:1;
+};
+
+static void
+test_server_response_ostream_disconnect_destroyed(
+ struct _response_ostream_disconnect *ctx)
+{
+ test_assert(ctx->seen_stream_error);
+ io_remove(&ctx->io);
+ i_stream_unref(&ctx->payload_input);
+ i_free(ctx);
+ io_loop_stop(ioloop);
+}
+
+static int
+test_server_response_ostream_disconnect_output(
+ struct _response_ostream_disconnect *ctx)
+{
+ struct ostream *output = ctx->payload_output;
+ enum ostream_send_istream_result res;
+ int ret;
+
+ if (ctx->finished) {
+ ret = o_stream_finish(output);
+ if (ret == 0)
+ return ret;
+ if (ret < 0) {
+ if (debug) {
+ i_debug("OUTPUT ERROR: %s",
+ o_stream_get_error(output));
+ }
+ test_assert(output->stream_errno == ECONNRESET ||
+ output->stream_errno == EPIPE);
+
+ ctx->seen_stream_error = TRUE;
+ o_stream_destroy(&ctx->payload_output);
+ return -1;
+ }
+ return 1;
+ }
+
+ o_stream_set_max_buffer_size(output, IO_BLOCK_SIZE);
+ res = o_stream_send_istream(output, ctx->payload_input);
+ o_stream_set_max_buffer_size(output, SIZE_MAX);
+
+ switch (res) {
+ case OSTREAM_SEND_ISTREAM_RESULT_FINISHED:
+ ctx->finished = TRUE;
+ return test_server_response_ostream_disconnect_output(ctx);
+ case OSTREAM_SEND_ISTREAM_RESULT_WAIT_INPUT:
+ i_unreached();
+ case OSTREAM_SEND_ISTREAM_RESULT_WAIT_OUTPUT:
+ if (debug)
+ i_debug("WAIT OUTPUT");
+ return 1;
+ case OSTREAM_SEND_ISTREAM_RESULT_ERROR_INPUT:
+ i_unreached();
+ case OSTREAM_SEND_ISTREAM_RESULT_ERROR_OUTPUT:
+ if (debug) {
+ i_debug("OUTPUT ERROR: %s",
+ o_stream_get_error(output));
+ }
+ test_assert(output->stream_errno == ECONNRESET ||
+ output->stream_errno == EPIPE);
+
+ ctx->seen_stream_error = TRUE;
+ o_stream_destroy(&ctx->payload_output);
+ return -1;
+ }
+ i_unreached();
+}
+
+static void
+test_server_response_ostream_disconnect_request(struct http_server_request *req)
+{
+ const struct http_request *hreq = http_server_request_get(req);
+ struct http_server_response *resp;
+ struct _response_ostream_disconnect *ctx;
+ string_t *data;
+ unsigned int i;
+
+ if (debug) {
+ i_debug("REQUEST: %s %s HTTP/%u.%u",
+ hreq->method, hreq->target_raw,
+ hreq->version_major, hreq->version_minor);
+ }
+
+ ctx = i_new(struct _response_ostream_disconnect, 1);
+ ctx->req = req;
+
+ data = str_new(default_pool, 2048000);
+ for (i = 0; i < 32000; i++) {
+ str_append(data, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\r\n"
+ "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\r\n");
+ }
+ ctx->payload_input = i_stream_create_copy_from_data(
+ str_data(data), str_len(data));
+ str_free(&data);
+
+ resp = http_server_response_create(req, 200, "OK");
+ ctx->payload_output = http_server_response_get_payload_output(
+ resp, IO_BLOCK_SIZE, FALSE);
+
+ o_stream_add_destroy_callback(
+ ctx->payload_output,
+ test_server_response_ostream_disconnect_destroyed, ctx);
+
+ o_stream_set_flush_callback(
+ ctx->payload_output,
+ test_server_response_ostream_disconnect_output, ctx);
+ o_stream_set_flush_pending(ctx->payload_output, TRUE);
+}
+
+static void
+test_server_response_ostream_disconnect(
+ const struct http_server_settings *server_set)
+{
+ test_server_request = test_server_response_ostream_disconnect_request;
+ test_server_run(server_set);
+}
+
+/* test */
+
+static void test_response_ostream_disconnect(void)
+{
+ struct http_server_settings http_server_set;
+
+ test_server_defaults(&http_server_set);
+ http_server_set.socket_send_buffer_size = 4096;
+ http_server_set.max_client_idle_time_msecs = 10000;
+
+ test_begin("response ostream disconnect");
+ test_run_client_server(&http_server_set,
+ test_server_response_ostream_disconnect,
+ test_client_response_ostream_disconnect, 1);
+ test_end();
+}
+
+/*
+ * All tests
+ */
+
+static void (*const test_functions[])(void) = {
+ test_slow_request,
+ test_hanging_request_payload,
+ test_hanging_response_payload,
+ test_excessive_payload_length,
+ test_response_ostream_disconnect,
+ NULL
+};
+
+/*
+ * Test client
+ */
+
+/* client connection */
+
+static void client_connection_input(struct connection *_conn)
+{
+ struct client_connection *conn = (struct client_connection *)_conn;
+
+ if (test_client_input != NULL)
+ test_client_input(conn);
+}
+
+static void client_connection_connected(struct connection *_conn, bool success)
+{
+ struct client_connection *conn = (struct client_connection *)_conn;
+
+ if (success && test_client_connected != NULL)
+ test_client_connected(conn);
+}
+
+static void client_connection_init(const struct ip_addr *ip, in_port_t port)
+{
+ struct client_connection *conn;
+ pool_t pool;
+
+ pool = pool_alloconly_create("client connection", 512);
+ conn = p_new(pool, struct client_connection, 1);
+ conn->pool = pool;
+
+ connection_init_client_ip(client_conn_list, &conn->conn, NULL,
+ ip, port);
+ (void)connection_client_connect(&conn->conn);
+}
+
+static void client_connection_deinit(struct client_connection **_conn)
+{
+ struct client_connection *conn = *_conn;
+
+ *_conn = NULL;
+
+ connection_deinit(&conn->conn);
+ pool_unref(&conn->pool);
+}
+
+static void client_connection_destroy(struct connection *_conn)
+{
+ struct client_connection *conn = (struct client_connection *)_conn;
+
+ client_connection_deinit(&conn);
+}
+
+/* */
+
+static struct connection_settings client_connection_set = {
+ .input_max_size = SIZE_MAX,
+ .output_max_size = SIZE_MAX,
+ .client = TRUE
+};
+
+static const struct connection_vfuncs client_connection_vfuncs = {
+ .destroy = client_connection_destroy,
+ .client_connected = client_connection_connected,
+ .input = client_connection_input
+};
+
+static void test_client_run(unsigned int index)
+{
+ client_index = index;
+
+ if (debug)
+ i_debug("client connecting to %u", bind_port);
+
+ client_conn_list = connection_list_init(&client_connection_set,
+ &client_connection_vfuncs);
+
+ client_connection_init(&bind_ip, bind_port);
+
+ io_loop_run(ioloop);
+
+ /* close server socket */
+ io_remove(&io_listen);
+
+ connection_list_deinit(&client_conn_list);
+}
+
+/*
+ * Test server
+ */
+
+static void test_server_defaults(struct http_server_settings *http_set)
+{
+ /* server settings */
+ i_zero(http_set);
+ http_set->max_client_idle_time_msecs = 5*1000;
+ http_set->max_pipelined_requests = 1;
+ http_set->debug = debug;
+}
+
+/* client connection */
+
+static void
+server_handle_request(void *context ATTR_UNUSED,
+ struct http_server_request *req)
+{
+ test_server_request(req);
+}
+
+struct http_server_callbacks http_server_callbacks = {
+ .handle_request = server_handle_request
+};
+
+static void server_connection_accept(void *context ATTR_UNUSED)
+{
+ int fd;
+
+ /* accept new client */
+ fd = net_accept(fd_listen, NULL, NULL);
+ if (fd == -1)
+ return;
+ if (fd == -2) {
+ i_fatal("test server: accept() failed: %m");
+ }
+
+ (void)http_server_connection_create(http_server, fd, fd, FALSE,
+ &http_server_callbacks, NULL);
+}
+
+/* */
+
+static void test_server_timeout(void *context ATTR_UNUSED)
+{
+ i_fatal("Server timed out");
+}
+
+static void test_server_run(const struct http_server_settings *http_set)
+{
+ struct timeout *to;
+
+ to = timeout_add(SERVER_MAX_TIMEOUT_MSECS, test_server_timeout, NULL);
+
+ /* open server socket */
+ io_listen = io_add(fd_listen, IO_READ, server_connection_accept, NULL);
+
+ http_server = http_server_init(http_set);
+
+ io_loop_run(ioloop);
+
+ /* close server socket */
+ io_remove(&io_listen);
+ timeout_remove(&to);
+
+ http_server_deinit(&http_server);
+}
+
+/*
+ * Tests
+ */
+
+struct test_client_data {
+ unsigned int index;
+ test_client_init_t client_test;
+};
+
+static int test_open_server_fd(void)
+{
+ int fd = net_listen(&bind_ip, &bind_port, 128);
+ if (debug)
+ i_debug("server listening on %u", bind_port);
+ if (fd == -1) {
+ i_fatal("listen(%s:%u) failed: %m",
+ net_ip2addr(&bind_ip), bind_port);
+ }
+ return fd;
+}
+
+static int test_run_client(struct test_client_data *data)
+{
+ i_close_fd(&fd_listen);
+
+ i_set_failure_prefix("CLIENT[%u]: ", data->index + 1);
+
+ if (debug)
+ i_debug("PID=%s", my_pid);
+
+ /* Wait a little for server setup */
+ i_sleep_msecs(100);
+
+ ioloop = io_loop_create();
+ data->client_test(data->index);
+ io_loop_destroy(&ioloop);
+
+ if (debug)
+ i_debug("Terminated");
+
+ main_deinit();
+ return 0;
+}
+
+static void
+test_run_server(const struct http_server_settings *server_set,
+ test_server_init_t server_test)
+{
+ i_set_failure_prefix("SERVER: ");
+
+ if (debug)
+ i_debug("PID=%s", my_pid);
+
+ ioloop = io_loop_create();
+ server_test(server_set);
+ io_loop_destroy(&ioloop);
+
+ if (debug)
+ i_debug("Terminated");
+}
+
+static void
+test_run_client_server(const struct http_server_settings *server_set,
+ test_server_init_t server_test,
+ test_client_init_t client_test,
+ unsigned int client_tests_count)
+{
+ unsigned int i;
+
+ fd_listen = test_open_server_fd();
+
+ if (client_tests_count > 0) {
+ for (i = 0; i < client_tests_count; i++) {
+ struct test_client_data data;
+
+ i_zero(&data);
+ data.index = i;
+ data.client_test = client_test;
+
+ /* Fork client */
+ test_subprocess_fork(test_run_client, &data, FALSE);
+ }
+ }
+
+ /* Run server */
+ test_run_server(server_set, server_test);
+
+ i_unset_failure_prefix();
+ i_close_fd(&fd_listen);
+ test_subprocess_kill_all(CLIENT_KILL_TIMEOUT_SECS);
+}
+
+/*
+ * Main
+ */
+
+static void main_init(void)
+{
+ /* nothing yet */
+}
+
+static void main_deinit(void)
+{
+ /* nothing yet; also called from sub-processes */
+}
+
+int main(int argc, char *argv[])
+{
+ int c;
+ int ret;
+
+ lib_init();
+ main_init();
+
+ while ((c = getopt(argc, argv, "D")) > 0) {
+ switch (c) {
+ case 'D':
+ debug = TRUE;
+ break;
+ default:
+ i_fatal("Usage: %s [-D]", argv[0]);
+ }
+ }
+
+ test_subprocesses_init(debug);
+
+ /* listen on localhost */
+ i_zero(&bind_ip);
+ bind_ip.family = AF_INET;
+ bind_ip.u.ip4.s_addr = htonl(INADDR_LOOPBACK);
+
+ ret = test_run(test_functions);
+
+ test_subprocesses_deinit();
+ main_deinit();
+ lib_deinit();
+
+ return ret;
+}
diff --git a/src/lib-http/test-http-server.c b/src/lib-http/test-http-server.c
new file mode 100644
index 0000000..ceec5ce
--- /dev/null
+++ b/src/lib-http/test-http-server.c
@@ -0,0 +1,245 @@
+/* Copyright (c) 2013-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "lib-signals.h"
+#include "llist.h"
+#include "str.h"
+#include "ioloop.h"
+#include "ostream.h"
+#include "connection.h"
+#include "http-url.h"
+#include "http-server.h"
+
+#include <unistd.h>
+
+static int fd_listen;
+static struct ioloop *ioloop;
+static struct io *io_listen;
+static struct http_server *http_server;
+static bool shut_down = FALSE;
+static struct client *clients_head = NULL, *clients_tail = NULL;
+
+struct client {
+ struct client *prev, *next;
+
+ struct ip_addr server_ip, ip;
+ in_port_t server_port, port;
+ struct http_server_connection *http_conn;
+};
+
+static void
+client_destroy(struct client **_client, const char *reason)
+{
+ struct client *client = *_client;
+
+ if (client->http_conn != NULL) {
+ /* We're not in the lib-http/server's connection destroy callback.
+ If at all possible, avoid destroying client objects directly.
+ */
+ http_server_connection_close(&client->http_conn, reason);
+ }
+ DLLIST2_REMOVE(&clients_head, &clients_tail, client);
+ i_free(client);
+
+ if (clients_head == NULL)
+ io_loop_stop(ioloop);
+}
+
+/* This function just serves as an illustration of what to do when client
+ objects are destroyed by some actor other than lib-http/server. The best way
+ to close all clients is to drop the whole http-server, which will close all
+ connections, which in turn calls the connection_destroy() callbacks. Using a
+ function like this just complicates matters. */
+static void
+clients_destroy_all(void)
+{
+ while (clients_head != NULL) {
+ struct client *client = clients_head;
+ client_destroy(&client, "Shutting down server");
+ }
+}
+
+static void
+client_http_handle_request(void *context,
+ struct http_server_request *req)
+{
+ struct client *client = (struct client *)context;
+ const struct http_request *http_req = http_server_request_get(req);
+ struct http_server_response *http_resp;
+ const char *ipport;
+ string_t *content;
+
+ if (strcmp(http_req->method, "GET") != 0) {
+ /* Unsupported method */
+ http_resp = http_server_response_create(req, 501, "Not Implemented");
+ http_server_response_add_header(http_resp, "Allow", "GET");
+ http_server_response_submit(http_resp);
+ return;
+ }
+
+ /* Compose response payload */
+ content = t_str_new(1024);
+ (void)net_ipport2str(&client->server_ip, client->server_port, &ipport);
+ str_printfa(content, "Server: %s\r\n", ipport);
+ (void)net_ipport2str(&client->ip, client->port, &ipport);
+ str_printfa(content, "Client: %s\r\n", ipport);
+ str_printfa(content, "Host: %s", http_req->target.url->host.name);
+ if (http_req->target.url->port != 0)
+ str_printfa(content, ":%u", http_req->target.url->port);
+ str_append(content, "\r\n");
+ switch (http_req->target.format) {
+ case HTTP_REQUEST_TARGET_FORMAT_ORIGIN:
+ case HTTP_REQUEST_TARGET_FORMAT_ABSOLUTE:
+ str_printfa(content, "Target: %s\r\n",
+ http_url_create(http_req->target.url));
+ break;
+ case HTTP_REQUEST_TARGET_FORMAT_AUTHORITY:
+ str_printfa(content, "Target: %s\r\n",
+ http_url_create_authority(http_req->target.url));
+ break;
+ case HTTP_REQUEST_TARGET_FORMAT_ASTERISK:
+ str_append(content, "Target: *\r\n");
+ break;
+ }
+
+ /* Just respond with the request target */
+ http_resp = http_server_response_create(req, 200, "OK");
+ http_server_response_add_header(http_resp, "Content-Type", "text/plain");
+ http_server_response_set_payload_data(http_resp,
+ str_data(content), str_len(content));
+ http_server_response_submit(http_resp);
+}
+
+static void
+client_http_connection_destroy(void *context, const char *reason)
+{
+ struct client *client = (struct client *)context;
+
+ if (client->http_conn == NULL) {
+ /* already destroying client directly */
+ return;
+ }
+
+ /* HTTP connection is destroyed already now */
+ client->http_conn = NULL;
+
+ /* destroy the client itself */
+ client_destroy(&client, reason);
+}
+
+static const struct http_server_callbacks server_callbacks = {
+ .handle_request = client_http_handle_request,
+ .connection_destroy = client_http_connection_destroy
+};
+
+static void
+client_init(int fd, const struct ip_addr *ip, in_port_t port)
+{
+ struct client *client;
+ struct http_request_limits req_limits;
+
+ i_zero(&req_limits);
+ req_limits.max_target_length = 4096;
+
+ client = i_new(struct client, 1);
+ client->ip = *ip;
+ client->port = port;
+ (void)net_getsockname(fd, &client->server_ip, &client->server_port);
+ client->http_conn = http_server_connection_create(http_server,
+ fd, fd, FALSE, &server_callbacks, client);
+
+ DLLIST2_APPEND(&clients_head, &clients_tail, client);
+}
+
+static void client_accept(void *context ATTR_UNUSED)
+{
+ struct ip_addr client_ip;
+ in_port_t client_port;
+ int fd;
+
+ fd = net_accept(fd_listen, &client_ip, &client_port);
+ if (fd == -1)
+ return;
+ if (fd == -2)
+ i_fatal("accept() failed: %m");
+
+ client_init(fd, &client_ip, client_port);
+}
+
+static void
+sig_die(const siginfo_t *si ATTR_UNUSED, void *context ATTR_UNUSED)
+{
+ if (shut_down) {
+ i_info("Received SIGINT again - stopping immediately");
+ io_loop_stop(current_ioloop);
+ return;
+ }
+
+ i_info("Received SIGINT - shutting down gracefully");
+ shut_down = TRUE;
+ http_server_shut_down(http_server);
+ if (clients_head == NULL)
+ io_loop_stop(ioloop);
+}
+
+int main(int argc, char *argv[])
+{
+ struct http_server_settings http_set;
+ bool debug = FALSE;
+ struct ip_addr my_ip;
+ in_port_t port;
+ int c;
+
+ lib_init();
+
+ while ((c = getopt(argc, argv, "D")) > 0) {
+ switch (c) {
+ case 'D':
+ debug = TRUE;
+ break;
+ default:
+ i_fatal("Usage: %s [-D] <port> [<IP>]", argv[0]);
+ }
+ }
+ argc -= optind;
+ argv += optind;
+
+ if (argc < 1 || net_str2port(argv[0], &port) < 0)
+ i_fatal("Port parameter missing");
+ if (argc < 2)
+ my_ip = net_ip4_any;
+ else if (net_addr2ip(argv[1], &my_ip) < 0)
+ i_fatal("Invalid IP parameter");
+
+ i_zero(&http_set);
+ http_set.max_client_idle_time_msecs = 20*1000; /* defaults to indefinite! */
+ http_set.max_pipelined_requests = 4;
+ http_set.debug = debug;
+
+ ioloop = io_loop_create();
+
+ http_server = http_server_init(&http_set);
+
+ lib_signals_init();
+ lib_signals_ignore(SIGPIPE, TRUE);
+ lib_signals_set_handler(SIGTERM, LIBSIG_FLAG_DELAYED, sig_die, NULL);
+ lib_signals_set_handler(SIGINT, LIBSIG_FLAG_DELAYED, sig_die, NULL);
+
+ fd_listen = net_listen(&my_ip, &port, 128);
+ if (fd_listen == -1)
+ i_fatal("listen(port=%u) failed: %m", port);
+
+ io_listen = io_add(fd_listen, IO_READ, client_accept, NULL);
+
+ io_loop_run(ioloop);
+
+ io_remove(&io_listen);
+ i_close_fd(&fd_listen);
+
+ clients_destroy_all(); /* just an example; avoid doing this */
+
+ http_server_deinit(&http_server);
+ lib_signals_deinit();
+ io_loop_destroy(&ioloop);
+ lib_deinit();
+}
diff --git a/src/lib-http/test-http-transfer.c b/src/lib-http/test-http-transfer.c
new file mode 100644
index 0000000..39b08a6
--- /dev/null
+++ b/src/lib-http/test-http-transfer.c
@@ -0,0 +1,347 @@
+/* Copyright (c) 2013-2018 Dovecot authors, see the included COPYING file */
+
+#include "test-lib.h"
+#include "buffer.h"
+#include "str.h"
+#include "strfuncs.h"
+#include "str-sanitize.h"
+#include "istream.h"
+#include "ostream.h"
+#include "test-common.h"
+#include "http-transfer.h"
+
+#include <time.h>
+
+struct http_transfer_chunked_input_test {
+ const char *in;
+ const char *out;
+};
+
+/* Valid transfer_chunked input tests */
+static struct http_transfer_chunked_input_test
+valid_transfer_chunked_input_tests[] = {
+ { .in = "1E\r\n"
+ "This is a simple test payload."
+ "\r\n"
+ "0\r\n"
+ "\r\n",
+ .out =
+ "This is a simple test payload."
+ },
+ { .in = "20\r\n"
+ "This is a longer test payload..."
+ "\r\n"
+ "23\r\n"
+ "...spread over two separate chunks."
+ "\r\n"
+ "0\r\n"
+ "\r\n",
+ .out =
+ "This is a longer test payload..."
+ "...spread over two separate chunks."
+ },
+ { .in = "26\r\n"
+ "This is an even longer test payload..."
+ "\r\n"
+ "27\r\n"
+ "...spread over three separate chunks..."
+ "\r\n"
+ "1F\r\n"
+ "...and also includes a trailer."
+ "\r\n"
+ "0\r\n"
+ "Checksum: adgfef3fdaf3daf3dfaf3ff3fdag\r\n"
+ "X-Dovecot: Whatever\r\n"
+ "\r\n",
+ .out =
+ "This is an even longer test payload..."
+ "...spread over three separate chunks..."
+ "...and also includes a trailer."
+ },
+ { .in = "26\n"
+ "This is an even longer test payload..."
+ "\n"
+ "27\n"
+ "...spread over three separate chunks..."
+ "\n"
+ "1F\n"
+ "...and also includes a trailer."
+ "\n"
+ "0\n"
+ "Checksum: adgfef3fdaf3daf3dfaf3ff3fdag\n"
+ "X-Dovecot: Whatever\n"
+ "\n",
+ .out =
+ "This is an even longer test payload..."
+ "...spread over three separate chunks..."
+ "...and also includes a trailer."
+ }
+};
+
+static unsigned int valid_transfer_chunked_input_test_count =
+ N_ELEMENTS(valid_transfer_chunked_input_tests);
+
+static void test_http_transfer_chunked_input_valid(void)
+{
+ struct istream *input, *chunked;
+ struct ostream *output;
+ buffer_t *payload_buffer;
+ unsigned int i;
+
+ payload_buffer = buffer_create_dynamic(default_pool, 1024);
+
+ for (i = 0; i < valid_transfer_chunked_input_test_count; i++) T_BEGIN {
+ const char *in, *out, *stream_out;
+
+ in = valid_transfer_chunked_input_tests[i].in;
+ out = valid_transfer_chunked_input_tests[i].out;
+
+ test_begin(t_strdup_printf("http transfer_chunked input valid [%d]", i));
+
+ input = i_stream_create_from_data(in, strlen(in));
+ chunked = http_transfer_chunked_istream_create(input, 0);
+ i_stream_unref(&input);
+
+ buffer_set_used_size(payload_buffer, 0);
+ output = o_stream_create_buffer(payload_buffer);
+ test_out("payload read", o_stream_send_istream(output, chunked) == OSTREAM_SEND_ISTREAM_RESULT_FINISHED
+ && chunked->stream_errno == 0);
+ o_stream_destroy(&output);
+ i_stream_unref(&chunked);
+ stream_out = str_c(payload_buffer);
+
+ test_out(t_strdup_printf("response->payload = %s",
+ str_sanitize(stream_out, 80)),
+ strcmp(stream_out, out) == 0);
+ test_end();
+ } T_END;
+
+ buffer_free(&payload_buffer);
+}
+
+/* Invalid transfer_chunked input tests */
+static const char *
+invalid_transfer_chunked_input_tests[] = {
+ // invalid size
+ "1X\r\n"
+ "This is a simple test payload."
+ "\r\n"
+ "0\r\n"
+ "\r\n",
+ // invalid end
+ "1E\r\n"
+ "This is a simple test payload."
+ "\r\n"
+ "0\r\n"
+ "ah\r\n",
+ // invalid size
+ "20\r\n"
+ "This is a longer test payload..."
+ "\r\n"
+ "2q\r\n"
+ "...spread over two separate chunks."
+ "\r\n"
+ "0\r\n"
+ "\r\n",
+ // invalid end
+ "20\r\n"
+ "This is a longer test payload..."
+ "\r\n"
+ "23\r\n"
+ "...spread over two separate chunks."
+ "\r\n"
+ "0\r\n",
+ // invalid last chunk
+ "20\r\n"
+ "This is a longer test payload..."
+ "\r\n"
+ "23\r\n"
+ "...spread over two separate chunks."
+ "\r\n"
+ "4\r\n"
+ "\r\n",
+ // invalid trailer
+ "26\r\n"
+ "This is an even longer test payload..."
+ "\r\n"
+ "27\r\n"
+ "...spread over three separate chunks..."
+ "\r\n"
+ "1F\r\n"
+ "...and also includes a trailer."
+ "\r\n"
+ "0\r\n"
+ "Checksum adgfef3fdaf3daf3dfaf3ff3fdag\r\n"
+ "\r\n"
+};
+
+static unsigned int invalid_transfer_chunked_input_test_count =
+ N_ELEMENTS(invalid_transfer_chunked_input_tests);
+
+static void test_http_transfer_chunked_input_invalid(void)
+{
+ struct istream *input, *chunked;
+ struct ostream *output;
+ buffer_t *payload_buffer;
+ unsigned int i;
+
+ payload_buffer = buffer_create_dynamic(default_pool, 1024);
+
+ for (i = 0; i < invalid_transfer_chunked_input_test_count; i++) T_BEGIN {
+ const char *in;
+
+ in = invalid_transfer_chunked_input_tests[i];
+
+ test_begin(t_strdup_printf("http transfer_chunked input invalid [%d]", i));
+
+ input = i_stream_create_from_data(in, strlen(in));
+ chunked = http_transfer_chunked_istream_create(input, 0);
+ i_stream_unref(&input);
+
+ buffer_set_used_size(payload_buffer, 0);
+ output = o_stream_create_buffer(payload_buffer);
+ o_stream_nsend_istream(output, chunked);
+ test_out("payload read failure", chunked->stream_errno != 0);
+ i_stream_unref(&chunked);
+ o_stream_destroy(&output);
+
+ test_end();
+ } T_END;
+
+ buffer_free(&payload_buffer);
+}
+
+/* Valid transfer_chunked output tests */
+static const char *valid_transfer_chunked_output_tests[] = {
+ /* The maximum chunk size is set to 16. These tests are tuned to some border
+ cases
+ */
+ "A small payload", // 15 bytes
+ "A longer payload", // 16 bytes
+ "A lengthy payload", // 17 bytes
+ /* Others */
+ "This is a test payload with lots of nonsense.",
+ "Yet another payload.",
+ "This a very long repetitive payload. This a very long repetitive payload. "
+ "This a very long repetitive payload. This a very long repetitive payload. "
+ "This a very long repetitive payload. This a very long repetitive payload. "
+ "This a very long repetitive payload. This a very long repetitive payload. "
+ "This a very long repetitive payload. This a very long repetitive payload. "
+ "This a very long repetitive payload. This a very long repetitive payload. "
+ "This a very long repetitive payload. This a very long repetitive payload. "
+ "This a very long repetitive payload. This a very long repetitive payload. "
+ "This a very long repetitive payload. This a very long repetitive payload. "
+ "This a very long repetitive payload. This a very long repetitive payload. "
+ "This a very long repetitive payload. This a very long repetitive payload. "
+ "This a very long repetitive payload. This a very long repetitive payload. "
+ "This a very long repetitive payload. This a very long repetitive payload. "
+ "This a very long repetitive payload. This a very long repetitive payload. "
+ "This a very long repetitive payload. This a very long repetitive payload. "
+ "This a very long repetitive payload. This a very long repetitive payload. "
+ "This a very long repetitive payload. This a very long repetitive payload. "
+ "This a very long repetitive payload. This a very long repetitive payload. "
+ "This a very long repetitive payload. This a very long repetitive payload. "
+};
+
+static unsigned int valid_transfer_chunked_output_test_count =
+ N_ELEMENTS(valid_transfer_chunked_output_tests);
+
+static void test_http_transfer_chunked_output_valid(void)
+{
+ struct istream *input, *ichunked;
+ struct ostream *output, *ochunked;
+ buffer_t *chunked_buffer, *plain_buffer;
+ unsigned int i;
+
+ chunked_buffer = buffer_create_dynamic(default_pool, 1024);
+ plain_buffer = buffer_create_dynamic(default_pool, 1024);
+
+ for (i = 0; i < valid_transfer_chunked_output_test_count; i++) T_BEGIN {
+ const char *data, *stream_out;
+ const unsigned char *rdata;
+ size_t rsize;
+ ssize_t ret;
+
+ data = valid_transfer_chunked_output_tests[i];
+
+ test_begin(t_strdup_printf("http transfer_chunked output valid [%d]", i));
+
+ /* create input stream */
+ input = i_stream_create_from_data(data, strlen(data));
+
+ /* create buffer output stream */
+ buffer_set_used_size(chunked_buffer, 0);
+ output = o_stream_create_buffer(chunked_buffer);
+
+ /* create chunked output stream */
+ ochunked = http_transfer_chunked_ostream_create(output);
+
+ /* send input through chunked stream; chunk size is limited */
+ for (;;) {
+ ret = i_stream_read_more(input, &rdata, &rsize);
+ if (ret < 0) {
+ if (input->eof)
+ ret = 1;
+ break;
+ }
+ if (rsize == 0)
+ break;
+ if (rsize > 16)
+ rsize = 16;
+
+ ret = o_stream_send(ochunked, rdata, rsize);
+ if (ret < 0)
+ break;
+
+ if ((size_t)ret != rsize) {
+ ret = -1;
+ break;
+ }
+
+ i_stream_skip(input, ret);
+ }
+
+ /* cleanup streams */
+ test_out("payload chunk", ret > 0);
+ test_assert(o_stream_finish(ochunked) > 0);
+ o_stream_destroy(&ochunked);
+ o_stream_destroy(&output);
+ i_stream_destroy(&input);
+
+ /* create chunked input stream */
+ input = i_stream_create_from_data
+ (chunked_buffer->data, chunked_buffer->used);
+ ichunked = http_transfer_chunked_istream_create(input, 0);
+
+ /* read back chunk */
+ buffer_set_used_size(plain_buffer, 0);
+ output = o_stream_create_buffer(plain_buffer);
+ test_out("payload unchunk",
+ o_stream_send_istream(output, ichunked) == OSTREAM_SEND_ISTREAM_RESULT_FINISHED
+ && ichunked->stream_errno == 0);
+ o_stream_destroy(&output);
+ i_stream_destroy(&ichunked);
+ i_stream_destroy(&input);
+
+ /* test output */
+ stream_out = str_c(plain_buffer);
+ test_out(t_strdup_printf("response->payload = %s",
+ str_sanitize(stream_out, 80)),
+ strcmp(stream_out, data) == 0);
+ test_end();
+ } T_END;
+
+ buffer_free(&chunked_buffer);
+ buffer_free(&plain_buffer);
+}
+
+int main(void)
+{
+ static void (*const test_functions[])(void) = {
+ test_http_transfer_chunked_input_valid,
+ test_http_transfer_chunked_input_invalid,
+ test_http_transfer_chunked_output_valid,
+ NULL
+ };
+ return test_run(test_functions);
+}
diff --git a/src/lib-http/test-http-url.c b/src/lib-http/test-http-url.c
new file mode 100644
index 0000000..a9fdf24
--- /dev/null
+++ b/src/lib-http/test-http-url.c
@@ -0,0 +1,950 @@
+/* Copyright (c) 2009-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "net.h"
+#include "http-url.h"
+#include "test-common.h"
+
+struct valid_http_url_test {
+ const char *url;
+ enum http_url_parse_flags flags;
+ struct http_url url_base;
+
+ struct http_url url_parsed;
+};
+
+/* Valid HTTP URL tests */
+static struct valid_http_url_test valid_url_tests[] = {
+ /* Generic tests */
+ {
+ .url = "http://localhost",
+ .url_parsed = {
+ .host = { .name = "localhost" },
+ },
+ },
+ {
+ .url = "http://www.%65%78%61%6d%70%6c%65.com",
+ .url_parsed = {
+ .host = { .name = "www.example.com" },
+ },
+ },
+ {
+ .url = "http://www.dovecot.org:8080",
+ .url_parsed = {
+ .host = { .name = "www.dovecot.org" },
+ .port = 8080,
+ },
+ },
+ {
+ .url = "http://127.0.0.1",
+ .url_parsed = {
+ .host = {
+ .name = "127.0.0.1",
+ .ip = { .family = AF_INET },
+ },
+ },
+ },
+ {
+ .url = "http://[::1]",
+ .url_parsed = {
+ .host = {
+ .name = "[::1]",
+ .ip = { .family = AF_INET6 },
+ },
+ },
+ },
+ {
+ .url = "http://[::1]:8080",
+ .url_parsed = {
+ .host = {
+ .name = "[::1]",
+ .ip = { .family = AF_INET6 },
+ },
+ .port = 8080,
+ },
+ },
+ {
+ .url = "http://user@api.dovecot.org",
+ .flags = HTTP_URL_ALLOW_USERINFO_PART,
+ .url_parsed = {
+ .host = { .name = "api.dovecot.org" },
+ .user = "user",
+ },
+ },
+ {
+ .url = "http://userid:secret@api.dovecot.org",
+ .flags = HTTP_URL_ALLOW_USERINFO_PART,
+ .url_parsed = {
+ .host = { .name = "api.dovecot.org" },
+ .user = "userid",
+ .password = "secret",
+ },
+ },
+ {
+ .url = "http://su%3auserid:secret@api.dovecot.org",
+ .flags = HTTP_URL_ALLOW_USERINFO_PART,
+ .url_parsed = {
+ .host = { .name = "api.dovecot.org" },
+ .user = "su:userid",
+ .password = "secret",
+ },
+ },
+ {
+ .url = "http://www.example.com/"
+ "?question=What%20are%20you%20doing%3f&answer=Nothing.",
+ .url_parsed = {
+ .path = "/",
+ .host = { .name = "www.example.com" },
+ .enc_query = "question=What%20are%20you%20doing%3f&answer=Nothing.",
+ },
+ },
+ /* Empty path segments */
+ {
+ .url = "http://target//index.php",
+ .url_parsed = {
+ .path = "//index.php",
+ .host = { .name = "target" },
+ },
+ },
+ {
+ .url = "http://target//path//index.php",
+ .url_parsed = {
+ .path = "//path//index.php",
+ .host = { .name = "target" },
+ },
+ },
+ {
+ .url = "http://target//path/",
+ .url_parsed = {
+ .path = "//path/",
+ .host = { .name = "target" },
+ },
+ },
+ {
+ .url = "http://target//path//",
+ .url_parsed = {
+ .path = "//path//",
+ .host = { .name = "target" },
+ },
+ },
+ {
+ .url = "http://target//path//to//./index.php",
+ .url_parsed = {
+ .path = "//path//to//index.php",
+ .host = { .name = "target" },
+ },
+ },
+ {
+ .url = "http://target//path//to//../index.php",
+ .url_parsed = {
+ .path = "//path//to/index.php",
+ .host = { .name = "target" },
+ },
+ },
+ {
+ .url = "/index.php",
+ .url_base = {
+ .host = { .name = "target" },
+ },
+ .url_parsed = {
+ .host = { .name = "target" },
+ .path = "/index.php",
+ },
+ },
+ {
+ .url = "//index.php",
+ .url_base = {
+ .host = { .name = "target" },
+ },
+ .url_parsed = {
+ .host = { .name = "index.php" },
+ },
+ },
+ {
+ .url = "/path/to/index.php",
+ .url_base = {
+ .host = { .name = "target" },
+ },
+ .url_parsed = {
+ .host = { .name = "target" },
+ .path = "/path/to/index.php",
+ },
+ },
+ {
+ .url = "//path//to//index.php",
+ .url_base = {
+ .host = { .name = "target" },
+ },
+ .url_parsed = {
+ .host = { .name = "path" },
+ .path = "//to//index.php",
+ },
+ },
+ /* These next 2 URLs don't follow the recommendations in
+ http://tools.ietf.org/html/rfc1034#section-3.5 and
+ http://tools.ietf.org/html/rfc3696
+ However they satisfy the grammar in
+ http://tools.ietf.org/html/rfc1123#section-2 and
+ http://tools.ietf.org/html/rfc952
+ so we should parse them.
+ */
+ {
+ .url = "http://256.0.0.1/that/reverts/to/DNS",
+ .url_parsed = {
+ .path = "/that/reverts/to/DNS",
+ .host = { .name = "256.0.0.1" },
+ },
+ },
+ {
+ .url = "http://127.0.0.284/this/also/reverts/to/DNS",
+ .url_parsed = {
+ .path = "/this/also/reverts/to/DNS",
+ .host = { .name = "127.0.0.284" },
+ },
+ },
+ {
+ .url = "http://www.example.com/#Status%20of%20development",
+ .flags = HTTP_URL_ALLOW_FRAGMENT_PART,
+ .url_parsed = {
+ .path = "/",
+ .host = { .name = "www.example.com" },
+ .enc_fragment = "Status%20of%20development",
+ },
+ },
+ /* RFC 3986, Section 5.4. Reference Resolution Examples
+ *
+ * Within a representation with a well defined base URI of
+ *
+ * http://a/b/c/d;p?q
+ *
+ * a relative reference is transformed to its target URI as follows.
+ *
+ * 5.4.1. Normal Examples
+ */
+ { // "g" = "http://a/b/c/g"
+ .url = "g",
+ .url_base = {
+ .host = { .name = "a" },
+ .path = "/b/c/d;p",
+ .enc_query = "q",
+ },
+ .url_parsed = {
+ .host = { .name = "a" },
+ .path = "/b/c/g",
+ },
+ },
+ { // "./g" = "http://a/b/c/g"
+ .url = "./g",
+ .url_base = {
+ .host = { .name = "a" },
+ .path = "/b/c/d;p",
+ .enc_query = "q",
+ },
+ .url_parsed = {
+ .host = { .name = "a" },
+ .path = "/b/c/g",
+ },
+ },
+ { // "g/" = "http://a/b/c/g/"
+ .url = "g/",
+ .url_base = {
+ .host = { .name = "a" },
+ .path = "/b/c/d;p",
+ .enc_query = "q",
+ },
+ .url_parsed = {
+ .host = { .name = "a" },
+ .path = "/b/c/g/",
+ },
+ },
+ { // "/g" = "http://a/g"
+ .url = "/g",
+ .url_base = {
+ .host = { .name = "a" },
+ .path = "/b/c/d;p",
+ .enc_query = "q",
+ },
+ .url_parsed = {
+ .host = { .name = "a" },
+ .path = "/g",
+ },
+ },
+ { // "//g" = "http://g"
+ .url = "//g",
+ .url_base = {
+ .host = { .name = "a" },
+ .path = "/b/c/d;p",
+ .enc_query = "q",
+ },
+ .url_parsed = {
+ .host = { .name = "g" },
+ },
+ },
+ { // "?y" = "http://a/b/c/d;p?y"
+ .url = "?y",
+ .url_base = {
+ .host = { .name = "a" },
+ .path = "/b/c/d;p",
+ .enc_query = "q",
+ },
+ .url_parsed = {
+ .host = { .name = "a" },
+ .path = "/b/c/d;p",
+ .enc_query = "y",
+ },
+ },
+ { // "g?y" = "http://a/b/c/g?y"
+ .url = "g?y",
+ .url_base = {
+ .host = { .name = "a" },
+ .path = "/b/c/d;p",
+ .enc_query = "q",
+ },
+ .url_parsed = {
+ .host = { .name = "a" },
+ .path = "/b/c/g",
+ .enc_query = "y",
+ },
+ },
+ { // "#s" = "http://a/b/c/d;p?q#s"
+ .url = "#s",
+ .flags = HTTP_URL_ALLOW_FRAGMENT_PART,
+ .url_base = {
+ .host = { .name = "a" },
+ .path = "/b/c/d;p",
+ .enc_query = "q",
+ },
+ .url_parsed = {
+ .host = { .name = "a" },
+ .path = "/b/c/d;p",
+ .enc_query = "q",
+ .enc_fragment = "s",
+ },
+ },
+ { // "g#s" = "http://a/b/c/g#s"
+ .url = "g#s",
+ .flags = HTTP_URL_ALLOW_FRAGMENT_PART,
+ .url_base = {
+ .host = { .name = "a" },
+ .path = "/b/c/d;p",
+ .enc_query = "q",
+ },
+ .url_parsed = {
+ .host = { .name = "a" },
+ .path = "/b/c/g",
+ .enc_fragment = "s",
+ },
+ },
+ { // "g?y#s" = "http://a/b/c/g?y#s"
+ .url = "g?y#s",
+ .flags = HTTP_URL_ALLOW_FRAGMENT_PART,
+ .url_base = {
+ .host = { .name = "a" },
+ .path = "/b/c/d;p",
+ .enc_query = "q",
+ },
+ .url_parsed = {
+ .host = { .name = "a" },
+ .path = "/b/c/g",
+ .enc_query = "y",
+ .enc_fragment = "s",
+ },
+ },
+ { // ";x" = "http://a/b/c/;x"
+ .url = ";x",
+ .url_base = {
+ .host = { .name = "a" },
+ .path = "/b/c/d;p",
+ .enc_query = "q",
+ },
+ .url_parsed = {
+ .host = { .name = "a" },
+ .path = "/b/c/;x",
+ },
+ },
+ { // "g;x" = "http://a/b/c/g;x"
+ .url = "g;x",
+ .url_base = {
+ .host = { .name = "a" },
+ .path = "/b/c/d;p",
+ .enc_query = "q",
+ },
+ .url_parsed = {
+ .host = { .name = "a" },
+ .path = "/b/c/g;x",
+ },
+
+ },
+ { // "g;x?y#s" = "http://a/b/c/g;x?y#s"
+ .url = "g;x?y#s",
+ .flags = HTTP_URL_ALLOW_FRAGMENT_PART,
+ .url_base = {
+ .host = { .name = "a" },
+ .path = "/b/c/d;p",
+ .enc_query = "q",
+ },
+ .url_parsed = {
+ .host = { .name = "a" },
+ .path = "/b/c/g;x",
+ .enc_query = "y",
+ .enc_fragment = "s",
+ },
+ },
+ { // "" = "http://a/b/c/d;p?q"
+ .url = "",
+ .url_base = {
+ .host = { .name = "a" },
+ .path = "/b/c/d;p",
+ .enc_query = "q",
+ },
+ .url_parsed = {
+ .host = { .name = "a" },
+ .path = "/b/c/d;p",
+ .enc_query = "q",
+ },
+ },
+ { // "." = "http://a/b/c/"
+ .url = ".",
+ .url_base = {
+ .host = { .name = "a" },
+ .path = "/b/c/d;p",
+ .enc_query = "q",
+ },
+ .url_parsed = {
+ .host = { .name = "a" },
+ .path = "/b/c/",
+ },
+ },
+ { // "./" = "http://a/b/c/"
+ .url = "./",
+ .url_base = {
+ .host = { .name = "a" },
+ .path = "/b/c/d;p",
+ .enc_query = "q",
+ },
+ .url_parsed = {
+ .host = { .name = "a" },
+ .path = "/b/c/",
+ },
+ },
+ { // ".." = "http://a/b/"
+ .url = "..",
+ .url_base = {
+ .host = { .name = "a" },
+ .path = "/b/c/d;p",
+ .enc_query = "q",
+ },
+ .url_parsed = {
+ .host = { .name = "a" },
+ .path = "/b/",
+ },
+ },
+ { // "../" = "http://a/b/"
+ .url = "../",
+ .url_base = {
+ .host = { .name = "a" },
+ .path = "/b/c/d;p",
+ .enc_query = "q",
+ },
+ .url_parsed = {
+ .host = { .name = "a" },
+ .path = "/b/",
+ },
+ },
+ { // "../g" = "http://a/b/g"
+ .url = "../g",
+ .url_base = {
+ .host = { .name = "a" },
+ .path = "/b/c/d;p",
+ .enc_query = "q",
+ },
+ .url_parsed = {
+ .host = { .name = "a" },
+ .path = "/b/g",
+ },
+ },
+ { // "../.." = "http://a/"
+ .url = "../..",
+ .url_base = {
+ .host = { .name = "a" },
+ .path = "/b/c/d;p",
+ .enc_query = "q",
+ },
+ .url_parsed = {
+ .host = { .name = "a" },
+ .path = "/",
+ },
+ },
+ { // "../../" = "http://a/"
+ .url = "../../",
+ .url_base = {
+ .host = { .name = "a" },
+ .path = "/b/c/d;p",
+ .enc_query = "q",
+ },
+ .url_parsed = {
+ .host = { .name = "a" },
+ .path = "/",
+ },
+ },
+ { // "../../g" = "http://a/g"
+ .url = "../../g",
+ .url_base = {
+ .host = { .name = "a" },
+ .path = "/b/c/d;p",
+ .enc_query = "q",
+ },
+ .url_parsed = {
+ .host = { .name = "a" },
+ .path = "/g",
+ },
+ },
+ /* 5.4.2. Abnormal Examples
+ */
+ { // "../../../g" = "http://a/g"
+ .url = "../../../g",
+ .url_base = {
+ .host = { .name = "a" },
+ .path = "/b/c/d;p",
+ .enc_query = "q",
+ },
+ .url_parsed = {
+ .host = { .name = "a" },
+ .path = "/g",
+ },
+ },
+ { // "../../../../g" = "http://a/g"
+ .url = "../../../../g",
+ .url_base = {
+ .host = { .name = "a" },
+ .path = "/b/c/d;p",
+ .enc_query = "q",
+ },
+ .url_parsed = {
+ .host = { .name = "a" },
+ .path = "/g",
+ },
+ },
+ { // "/./g" = "http://a/g"
+ .url = "/./g",
+ .url_base = {
+ .host = {.name = "a"},
+ .path = "/b/c/d;p",
+ .enc_query = "q",
+ },
+ .url_parsed = {
+ .host = {.name = "a"},
+ .path = "/g",
+ },
+ },
+ { // "/../g" = "http://a/g"
+ .url = "/../g",
+ .url_base = {
+ .host = { .name = "a" },
+ .path = "/b/c/d;p",
+ .enc_query = "q",
+ },
+ .url_parsed = {
+ .host = { .name = "a" },
+ .path = "/g",
+ },
+ },
+ { // "g." = "http://a/b/c/g."
+ .url = "g.",
+ .url_base = {
+ .host = { .name = "a" },
+ .path = "/b/c/d;p",
+ .enc_query = "q",
+ },
+ .url_parsed = {
+ .host = { .name = "a" },
+ .path = "/b/c/g.",
+ },
+ },
+ { // ".g" = "http://a/b/c/.g"
+ .url = ".g",
+ .url_base = {
+ .host = { .name = "a" },
+ .path = "/b/c/d;p",
+ .enc_query = "q",
+ },
+ .url_parsed = {
+ .host = { .name = "a" },
+ .path = "/b/c/.g",
+ },
+ },
+ { // "g.." = "http://a/b/c/g.."
+ .url = "g..",
+ .url_base = {
+ .host = { .name = "a" },
+ .path = "/b/c/d;p",
+ .enc_query = "q",
+ },
+ .url_parsed = {
+ .host = { .name = "a" },
+ .path = "/b/c/g..",
+ },
+ },
+ { // "..g" = "http://a/b/c/..g"
+ .url = "..g",
+ .url_base = {
+ .host = { .name = "a" },
+ .path = "/b/c/d;p",
+ .enc_query = "q",
+ },
+ .url_parsed = {
+ .host = { .name = "a" },
+ .path = "/b/c/..g",
+ },
+ },
+ { // "./../g" = "http://a/b/g"
+ .url = "./../g",
+ .url_base = {
+ .host = {.name = "a"},
+ .path = "/b/c/d;p",
+ .enc_query = "q",
+ },
+ .url_parsed = {
+ .host = {.name = "a"},
+ .path = "/b/g",
+ },
+ },
+ { // "./g/." = "http://a/b/c/g/"
+ .url = "./g/.",
+ .url_base = {
+ .host = { .name = "a" },
+ .path = "/b/c/d;p",
+ .enc_query = "q",
+ },
+ .url_parsed = {
+ .host = { .name = "a" },
+ .path = "/b/c/g/",
+ },
+ },
+ { // "g/./h" = "http://a/b/c/g/h"
+ .url = "g/./h",
+ .url_base = {
+ .host = { .name = "a" },
+ .path = "/b/c/d;p",
+ .enc_query = "q",
+ },
+ .url_parsed = {
+ .host = { .name = "a" },
+ .path = "/b/c/g/h",
+ },
+ },
+ { // "g/../h" = "http://a/b/c/h"
+ .url = "g/../h",
+ .url_base = {
+ .host = { .name = "a" },
+ .path = "/b/c/d;p",
+ .enc_query = "q",
+ },
+ .url_parsed = {
+ .host = { .name = "a" },
+ .path = "/b/c/h",
+ },
+ },
+ { // "g;x=1/./y" = "http://a/b/c/g;x=1/y"
+ .url = "g;x=1/./y",
+ .url_base = {
+ .host = { .name = "a" },
+ .path = "/b/c/d;p",
+ .enc_query = "q",
+ },
+ .url_parsed = {
+ .host = { .name = "a" },
+ .path = "/b/c/g;x=1/y",
+ },
+ },
+ { // "g;x=1/../y" = "http://a/b/c/y"
+ .url = "g;x=1/../y",
+ .url_base = {
+ .host = { .name = "a" },
+ .path = "/b/c/d;p",
+ .enc_query = "q",
+ },
+ .url_parsed = {
+ .host = { .name = "a" },
+ .path = "/b/c/y",
+ },
+ },
+ { // "g?y/./x" = "http://a/b/c/g?y/./x"
+ .url = "g?y/./x",
+ .url_base = {
+ .host = { .name = "a" },
+ .path = "/b/c/d;p",
+ .enc_query = "q",
+ },
+ .url_parsed = {
+ .host = { .name = "a" },
+ .path = "/b/c/g",
+ .enc_query = "y/./x",
+ },
+ },
+ { // "g?y/../x" = "http://a/b/c/g?y/../x"
+ .url = "g?y/../x",
+ .url_base = {
+ .host = { .name = "a" },
+ .path = "/b/c/d;p",
+ .enc_query = "q",
+ },
+ .url_parsed = {
+ .host = { .name = "a" },
+ .path = "/b/c/g",
+ .enc_query = "y/../x",
+ },
+ },
+ { // "g#s/./x" = "http://a/b/c/g#s/./x"
+ .url = "g#s/./x",
+ .flags = HTTP_URL_ALLOW_FRAGMENT_PART,
+ .url_base = {
+ .host = { .name = "a" },
+ .path = "/b/c/d;p",
+ .enc_query = "q",
+ },
+ .url_parsed =
+ {
+ .host = { .name = "a" },
+ .path = "/b/c/g",
+ .enc_fragment = "s/./x",
+ },
+ },
+ { // "g#s/../x" = "http://a/b/c/g#s/../x"
+ .url = "g#s/../x",
+ .flags = HTTP_URL_ALLOW_FRAGMENT_PART,
+ .url_base = {
+ .host = { .name = "a" },
+ .path = "/b/c/d;p",
+ .enc_query = "q",
+ },
+ .url_parsed =
+ {
+ .host = { .name = "a" },
+ .path = "/b/c/g",
+ .enc_fragment = "s/../x",
+ },
+ }
+};
+
+static unsigned int valid_url_test_count = N_ELEMENTS(valid_url_tests);
+
+static void
+test_http_url_equal(struct http_url *urlt, struct http_url *urlp)
+{
+ if (urlp->host.name == NULL || urlt->host.name == NULL) {
+ test_assert(urlp->host.name == urlt->host.name);
+ } else {
+ test_assert(strcmp(urlp->host.name, urlt->host.name) == 0);
+ }
+ test_assert(urlp->port == urlt->port);
+ test_assert(urlp->host.ip.family == urlt->host.ip.family);
+ if (urlp->user == NULL || urlt->user == NULL) {
+ test_assert(urlp->user == urlt->user);
+ } else {
+ test_assert(strcmp(urlp->user, urlt->user) == 0);
+ }
+ if (urlp->password == NULL || urlt->password == NULL) {
+ test_assert(urlp->password == urlt->password);
+ } else {
+ test_assert(strcmp(urlp->password, urlt->password) == 0);
+ }
+ if (urlp->path == NULL || urlt->path == NULL) {
+ test_assert(urlp->path == urlt->path);
+ } else {
+ test_assert(strcmp(urlp->path, urlt->path) == 0);
+ }
+ if (urlp->enc_query == NULL || urlt->enc_query == NULL) {
+ test_assert(urlp->enc_query == urlt->enc_query);
+ } else {
+ test_assert(strcmp(urlp->enc_query, urlt->enc_query) == 0);
+ }
+ if (urlp->enc_fragment == NULL || urlt->enc_fragment == NULL) {
+ test_assert(urlp->enc_fragment == urlt->enc_fragment);
+ } else {
+ test_assert(strcmp(urlp->enc_fragment,
+ urlt->enc_fragment) == 0);
+ }
+}
+
+static void test_http_url_valid(void)
+{
+ unsigned int i;
+
+ for (i = 0; i < valid_url_test_count; i++) T_BEGIN {
+ const char *url = valid_url_tests[i].url;
+ enum http_url_parse_flags flags = valid_url_tests[i].flags;
+ struct http_url *urlt = &valid_url_tests[i].url_parsed;
+ struct http_url *urlb = &valid_url_tests[i].url_base;
+ struct http_url *urlp;
+ const char *error = NULL;
+
+ test_begin(t_strdup_printf("http url valid [%d]", i));
+
+ if (urlb->host.name == NULL) urlb = NULL;
+ if (http_url_parse(url, urlb, flags, pool_datastack_create(),
+ &urlp, &error) < 0)
+ urlp = NULL;
+
+ test_out_reason(t_strdup_printf("http_url_parse(%s)",
+ valid_url_tests[i].url), urlp != NULL, error);
+ if (urlp != NULL)
+ test_http_url_equal(urlt, urlp);
+
+ test_end();
+ } T_END;
+}
+
+struct invalid_http_url_test {
+ const char *url;
+ enum http_url_parse_flags flags;
+ struct http_url url_base;
+};
+
+static struct invalid_http_url_test invalid_url_tests[] = {
+ {
+ .url = "imap://example.com/INBOX"
+ },
+ {
+ .url = "http:/www.example.com"
+ },
+ {
+ .url = ""
+ },
+ {
+ .url = "/index.html"
+ },
+ {
+ .url = "http://www.example.com/index.html\""
+ },
+ {
+ .url = "http:///dovecot.org"
+ },
+ {
+ .url = "http://[]/index.html"
+ },
+ {
+ .url = "http://[v08.234:232:234:234:2221]/index.html"
+ },
+ {
+ .url = "http://[1::34a:34:234::6]/index.html"
+ },
+ {
+ .url = "http://example%a.com/index.html"
+ },
+ {
+ .url = "http://example.com%/index.html"
+ },
+ {
+ .url = "http://example%00.com/index.html"
+ },
+ {
+ .url = "http://example.com:65536/index.html"
+ },
+ {
+ .url = "http://example.com:72817/index.html"
+ },
+ {
+ .url = "http://example.com/settings/%00/"
+ },
+ {
+ .url = "http://example.com/settings/%0r/"
+ },
+ {
+ .url = "http://example.com/settings/misc/%/"
+ },
+ {
+ .url = "http://example.com/?%00"
+ },
+ {
+ .url = "http://www.example.com/network.html#IMAP_Server"
+ },
+ {
+ .url = "http://example.com/#%00",
+ .flags = HTTP_URL_ALLOW_FRAGMENT_PART
+ }
+};
+
+static unsigned int invalid_url_test_count = N_ELEMENTS(invalid_url_tests);
+
+static void test_http_url_invalid(void)
+{
+ unsigned int i;
+
+ for (i = 0; i < invalid_url_test_count; i++) T_BEGIN {
+ const char *url = invalid_url_tests[i].url;
+ enum http_url_parse_flags flags = invalid_url_tests[i].flags;
+ struct http_url *urlb = &invalid_url_tests[i].url_base;
+ struct http_url *urlp;
+ const char *error = NULL;
+
+ if (urlb->host.name == NULL)
+ urlb = NULL;
+
+ test_begin(t_strdup_printf("http url invalid [%d]", i));
+
+ if (http_url_parse(url, urlb, flags,
+ pool_datastack_create(), &urlp, &error) < 0)
+ urlp = NULL;
+ test_out_reason(t_strdup_printf("parse %s", url),
+ urlp == NULL, error);
+
+ test_end();
+ } T_END;
+
+}
+
+static const char *parse_create_url_tests[] = {
+ "http://www.example.com/",
+ "http://10.0.0.1/",
+ "http://[::1]/",
+ "http://www.example.com:993/",
+ "http://www.example.com/index.html",
+ "http://www.example.com/settings/index.html",
+ "http://www.example.com/%23shared/news",
+ "http://www.example.com/query.php?name=Hendrik%20Visser",
+ "http://www.example.com/network.html#IMAP%20Server",
+};
+
+static unsigned int
+parse_create_url_test_count = N_ELEMENTS(parse_create_url_tests);
+
+static void test_http_url_parse_create(void)
+{
+ unsigned int i;
+
+ for (i = 0; i < parse_create_url_test_count; i++) T_BEGIN {
+ const char *url = parse_create_url_tests[i];
+ struct http_url *urlp;
+ const char *error = NULL;
+
+ test_begin(t_strdup_printf("http url parse/create [%d]", i));
+
+ if (http_url_parse
+ (url, NULL, HTTP_URL_ALLOW_FRAGMENT_PART,
+ pool_datastack_create(), &urlp, &error) < 0)
+ urlp = NULL;
+ test_out_reason(t_strdup_printf("parse %s", url),
+ urlp != NULL, error);
+ if (urlp != NULL) {
+ const char *urlnew = http_url_create(urlp);
+ test_out(t_strdup_printf("create %s", urlnew),
+ strcmp(url, urlnew) == 0);
+ }
+
+ test_end();
+ } T_END;
+
+}
+
+int main(void)
+{
+ static void (*const test_functions[])(void) = {
+ test_http_url_valid,
+ test_http_url_invalid,
+ test_http_url_parse_create,
+ NULL
+ };
+ return test_run(test_functions);
+}
+
diff --git a/src/lib-imap-client/Makefile.am b/src/lib-imap-client/Makefile.am
new file mode 100644
index 0000000..8fe8a8d
--- /dev/null
+++ b/src/lib-imap-client/Makefile.am
@@ -0,0 +1,53 @@
+noinst_LTLIBRARIES = libimap_client.la
+
+AM_CPPFLAGS = \
+ -I$(top_srcdir)/src/lib \
+ -I$(top_srcdir)/src/lib-test \
+ -I$(top_srcdir)/src/lib-dns \
+ -I$(top_srcdir)/src/lib-sasl \
+ -I$(top_srcdir)/src/lib-ssl-iostream \
+ -I$(top_srcdir)/src/lib-mail \
+ -I$(top_srcdir)/src/lib-imap
+
+libimap_client_la_SOURCES = \
+ imapc-client.c \
+ imapc-connection.c \
+ imapc-msgmap.c
+
+headers = \
+ imapc-client.h \
+ imapc-client-private.h \
+ imapc-connection.h \
+ imapc-msgmap.h
+
+pkginc_libdir=$(pkgincludedir)
+pkginc_lib_HEADERS = $(headers)
+
+test_programs = \
+ test-imapc-client
+
+noinst_PROGRAMS = $(test_programs)
+
+test_deps = \
+ $(noinst_LTLIBRARIES) \
+ ../lib-ssl-iostream/libssl_iostream.la \
+ ../lib-sasl/libsasl.la \
+ ../lib-imap/libimap.la \
+ ../lib-mail/libmail.la \
+ ../lib-charset/libcharset.la \
+ ../lib-dns/libdns.la \
+ ../lib-test/libtest.la \
+ ../lib/liblib.la
+
+test_libs = \
+ $(test_deps) \
+ $(MODULE_LIBS)
+
+test_imapc_client_SOURCES = test-imapc-client.c
+test_imapc_client_LDADD = $(test_libs)
+test_imapc_client_DEPENDENCIES = $(test_deps)
+
+check-local:
+ for bin in $(test_programs); do \
+ if ! $(RUN_TEST) ./$$bin; then exit 1; fi; \
+ done
diff --git a/src/lib-imap-client/Makefile.in b/src/lib-imap-client/Makefile.in
new file mode 100644
index 0000000..a878047
--- /dev/null
+++ b/src/lib-imap-client/Makefile.in
@@ -0,0 +1,880 @@
+# Makefile.in generated by automake 1.16.1 from Makefile.am.
+# @configure_input@
+
+# Copyright (C) 1994-2018 Free Software Foundation, Inc.
+
+# This Makefile.in is free software; the Free Software Foundation
+# gives unlimited permission to copy and/or distribute it,
+# with or without modifications, as long as this notice is preserved.
+
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY, to the extent permitted by law; without
+# even the implied warranty of MERCHANTABILITY or FITNESS FOR A
+# PARTICULAR PURPOSE.
+
+@SET_MAKE@
+
+
+
+VPATH = @srcdir@
+am__is_gnu_make = { \
+ if test -z '$(MAKELEVEL)'; then \
+ false; \
+ elif test -n '$(MAKE_HOST)'; then \
+ true; \
+ elif test -n '$(MAKE_VERSION)' && test -n '$(CURDIR)'; then \
+ true; \
+ else \
+ false; \
+ fi; \
+}
+am__make_running_with_option = \
+ case $${target_option-} in \
+ ?) ;; \
+ *) echo "am__make_running_with_option: internal error: invalid" \
+ "target option '$${target_option-}' specified" >&2; \
+ exit 1;; \
+ esac; \
+ has_opt=no; \
+ sane_makeflags=$$MAKEFLAGS; \
+ if $(am__is_gnu_make); then \
+ sane_makeflags=$$MFLAGS; \
+ else \
+ case $$MAKEFLAGS in \
+ *\\[\ \ ]*) \
+ bs=\\; \
+ sane_makeflags=`printf '%s\n' "$$MAKEFLAGS" \
+ | sed "s/$$bs$$bs[$$bs $$bs ]*//g"`;; \
+ esac; \
+ fi; \
+ skip_next=no; \
+ strip_trailopt () \
+ { \
+ flg=`printf '%s\n' "$$flg" | sed "s/$$1.*$$//"`; \
+ }; \
+ for flg in $$sane_makeflags; do \
+ test $$skip_next = yes && { skip_next=no; continue; }; \
+ case $$flg in \
+ *=*|--*) continue;; \
+ -*I) strip_trailopt 'I'; skip_next=yes;; \
+ -*I?*) strip_trailopt 'I';; \
+ -*O) strip_trailopt 'O'; skip_next=yes;; \
+ -*O?*) strip_trailopt 'O';; \
+ -*l) strip_trailopt 'l'; skip_next=yes;; \
+ -*l?*) strip_trailopt 'l';; \
+ -[dEDm]) skip_next=yes;; \
+ -[JT]) skip_next=yes;; \
+ esac; \
+ case $$flg in \
+ *$$target_option*) has_opt=yes; break;; \
+ esac; \
+ done; \
+ test $$has_opt = yes
+am__make_dryrun = (target_option=n; $(am__make_running_with_option))
+am__make_keepgoing = (target_option=k; $(am__make_running_with_option))
+pkgdatadir = $(datadir)/@PACKAGE@
+pkgincludedir = $(includedir)/@PACKAGE@
+pkglibdir = $(libdir)/@PACKAGE@
+pkglibexecdir = $(libexecdir)/@PACKAGE@
+am__cd = CDPATH="$${ZSH_VERSION+.}$(PATH_SEPARATOR)" && cd
+install_sh_DATA = $(install_sh) -c -m 644
+install_sh_PROGRAM = $(install_sh) -c
+install_sh_SCRIPT = $(install_sh) -c
+INSTALL_HEADER = $(INSTALL_DATA)
+transform = $(program_transform_name)
+NORMAL_INSTALL = :
+PRE_INSTALL = :
+POST_INSTALL = :
+NORMAL_UNINSTALL = :
+PRE_UNINSTALL = :
+POST_UNINSTALL = :
+build_triplet = @build@
+host_triplet = @host@
+noinst_PROGRAMS = $(am__EXEEXT_1)
+subdir = src/lib-imap-client
+ACLOCAL_M4 = $(top_srcdir)/aclocal.m4
+am__aclocal_m4_deps = $(top_srcdir)/m4/ac_checktype2.m4 \
+ $(top_srcdir)/m4/ac_typeof.m4 $(top_srcdir)/m4/arc4random.m4 \
+ $(top_srcdir)/m4/blockdev.m4 $(top_srcdir)/m4/c99_vsnprintf.m4 \
+ $(top_srcdir)/m4/clock_gettime.m4 $(top_srcdir)/m4/crypt.m4 \
+ $(top_srcdir)/m4/crypt_xpg6.m4 $(top_srcdir)/m4/dbqlk.m4 \
+ $(top_srcdir)/m4/dirent_dtype.m4 $(top_srcdir)/m4/dovecot.m4 \
+ $(top_srcdir)/m4/fd_passing.m4 $(top_srcdir)/m4/fdatasync.m4 \
+ $(top_srcdir)/m4/flexible_array_member.m4 \
+ $(top_srcdir)/m4/glibc.m4 $(top_srcdir)/m4/gmtime_max.m4 \
+ $(top_srcdir)/m4/gmtime_tm_gmtoff.m4 \
+ $(top_srcdir)/m4/ioloop.m4 $(top_srcdir)/m4/iovec.m4 \
+ $(top_srcdir)/m4/ipv6.m4 $(top_srcdir)/m4/libcap.m4 \
+ $(top_srcdir)/m4/libtool.m4 $(top_srcdir)/m4/libwrap.m4 \
+ $(top_srcdir)/m4/linux_mremap.m4 $(top_srcdir)/m4/ltoptions.m4 \
+ $(top_srcdir)/m4/ltsugar.m4 $(top_srcdir)/m4/ltversion.m4 \
+ $(top_srcdir)/m4/lt~obsolete.m4 $(top_srcdir)/m4/mmap_write.m4 \
+ $(top_srcdir)/m4/mntctl.m4 $(top_srcdir)/m4/modules.m4 \
+ $(top_srcdir)/m4/notify.m4 $(top_srcdir)/m4/nsl.m4 \
+ $(top_srcdir)/m4/off_t_max.m4 $(top_srcdir)/m4/pkg.m4 \
+ $(top_srcdir)/m4/pr_set_dumpable.m4 \
+ $(top_srcdir)/m4/q_quotactl.m4 $(top_srcdir)/m4/quota.m4 \
+ $(top_srcdir)/m4/random.m4 $(top_srcdir)/m4/rlimit.m4 \
+ $(top_srcdir)/m4/sendfile.m4 $(top_srcdir)/m4/size_t_signed.m4 \
+ $(top_srcdir)/m4/sockpeercred.m4 $(top_srcdir)/m4/sql.m4 \
+ $(top_srcdir)/m4/ssl.m4 $(top_srcdir)/m4/st_tim.m4 \
+ $(top_srcdir)/m4/static_array.m4 $(top_srcdir)/m4/test_with.m4 \
+ $(top_srcdir)/m4/time_t.m4 $(top_srcdir)/m4/typeof.m4 \
+ $(top_srcdir)/m4/typeof_dev_t.m4 \
+ $(top_srcdir)/m4/uoff_t_max.m4 $(top_srcdir)/m4/vararg.m4 \
+ $(top_srcdir)/m4/want_apparmor.m4 \
+ $(top_srcdir)/m4/want_bsdauth.m4 \
+ $(top_srcdir)/m4/want_bzlib.m4 \
+ $(top_srcdir)/m4/want_cassandra.m4 \
+ $(top_srcdir)/m4/want_cdb.m4 \
+ $(top_srcdir)/m4/want_checkpassword.m4 \
+ $(top_srcdir)/m4/want_clucene.m4 $(top_srcdir)/m4/want_db.m4 \
+ $(top_srcdir)/m4/want_gssapi.m4 $(top_srcdir)/m4/want_icu.m4 \
+ $(top_srcdir)/m4/want_ldap.m4 $(top_srcdir)/m4/want_lua.m4 \
+ $(top_srcdir)/m4/want_lz4.m4 $(top_srcdir)/m4/want_lzma.m4 \
+ $(top_srcdir)/m4/want_mysql.m4 $(top_srcdir)/m4/want_pam.m4 \
+ $(top_srcdir)/m4/want_passwd.m4 $(top_srcdir)/m4/want_pgsql.m4 \
+ $(top_srcdir)/m4/want_prefetch.m4 \
+ $(top_srcdir)/m4/want_shadow.m4 \
+ $(top_srcdir)/m4/want_sodium.m4 $(top_srcdir)/m4/want_solr.m4 \
+ $(top_srcdir)/m4/want_sqlite.m4 \
+ $(top_srcdir)/m4/want_stemmer.m4 \
+ $(top_srcdir)/m4/want_systemd.m4 \
+ $(top_srcdir)/m4/want_textcat.m4 \
+ $(top_srcdir)/m4/want_unwind.m4 $(top_srcdir)/m4/want_zlib.m4 \
+ $(top_srcdir)/m4/want_zstd.m4 $(top_srcdir)/configure.ac
+am__configure_deps = $(am__aclocal_m4_deps) $(CONFIGURE_DEPENDENCIES) \
+ $(ACLOCAL_M4)
+DIST_COMMON = $(srcdir)/Makefile.am $(pkginc_lib_HEADERS) \
+ $(am__DIST_COMMON)
+mkinstalldirs = $(install_sh) -d
+CONFIG_HEADER = $(top_builddir)/config.h
+CONFIG_CLEAN_FILES =
+CONFIG_CLEAN_VPATH_FILES =
+am__EXEEXT_1 = test-imapc-client$(EXEEXT)
+PROGRAMS = $(noinst_PROGRAMS)
+LTLIBRARIES = $(noinst_LTLIBRARIES)
+libimap_client_la_LIBADD =
+am_libimap_client_la_OBJECTS = imapc-client.lo imapc-connection.lo \
+ imapc-msgmap.lo
+libimap_client_la_OBJECTS = $(am_libimap_client_la_OBJECTS)
+AM_V_lt = $(am__v_lt_@AM_V@)
+am__v_lt_ = $(am__v_lt_@AM_DEFAULT_V@)
+am__v_lt_0 = --silent
+am__v_lt_1 =
+am_test_imapc_client_OBJECTS = test-imapc-client.$(OBJEXT)
+test_imapc_client_OBJECTS = $(am_test_imapc_client_OBJECTS)
+am__DEPENDENCIES_1 =
+am__DEPENDENCIES_2 = $(test_deps) $(am__DEPENDENCIES_1)
+AM_V_P = $(am__v_P_@AM_V@)
+am__v_P_ = $(am__v_P_@AM_DEFAULT_V@)
+am__v_P_0 = false
+am__v_P_1 = :
+AM_V_GEN = $(am__v_GEN_@AM_V@)
+am__v_GEN_ = $(am__v_GEN_@AM_DEFAULT_V@)
+am__v_GEN_0 = @echo " GEN " $@;
+am__v_GEN_1 =
+AM_V_at = $(am__v_at_@AM_V@)
+am__v_at_ = $(am__v_at_@AM_DEFAULT_V@)
+am__v_at_0 = @
+am__v_at_1 =
+DEFAULT_INCLUDES = -I.@am__isrc@ -I$(top_builddir)
+depcomp = $(SHELL) $(top_srcdir)/depcomp
+am__maybe_remake_depfiles = depfiles
+am__depfiles_remade = ./$(DEPDIR)/imapc-client.Plo \
+ ./$(DEPDIR)/imapc-connection.Plo ./$(DEPDIR)/imapc-msgmap.Plo \
+ ./$(DEPDIR)/test-imapc-client.Po
+am__mv = mv -f
+COMPILE = $(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) \
+ $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS)
+LTCOMPILE = $(LIBTOOL) $(AM_V_lt) --tag=CC $(AM_LIBTOOLFLAGS) \
+ $(LIBTOOLFLAGS) --mode=compile $(CC) $(DEFS) \
+ $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) $(CPPFLAGS) \
+ $(AM_CFLAGS) $(CFLAGS)
+AM_V_CC = $(am__v_CC_@AM_V@)
+am__v_CC_ = $(am__v_CC_@AM_DEFAULT_V@)
+am__v_CC_0 = @echo " CC " $@;
+am__v_CC_1 =
+CCLD = $(CC)
+LINK = $(LIBTOOL) $(AM_V_lt) --tag=CC $(AM_LIBTOOLFLAGS) \
+ $(LIBTOOLFLAGS) --mode=link $(CCLD) $(AM_CFLAGS) $(CFLAGS) \
+ $(AM_LDFLAGS) $(LDFLAGS) -o $@
+AM_V_CCLD = $(am__v_CCLD_@AM_V@)
+am__v_CCLD_ = $(am__v_CCLD_@AM_DEFAULT_V@)
+am__v_CCLD_0 = @echo " CCLD " $@;
+am__v_CCLD_1 =
+SOURCES = $(libimap_client_la_SOURCES) $(test_imapc_client_SOURCES)
+DIST_SOURCES = $(libimap_client_la_SOURCES) \
+ $(test_imapc_client_SOURCES)
+am__can_run_installinfo = \
+ case $$AM_UPDATE_INFO_DIR in \
+ n|no|NO) false;; \
+ *) (install-info --version) >/dev/null 2>&1;; \
+ esac
+am__vpath_adj_setup = srcdirstrip=`echo "$(srcdir)" | sed 's|.|.|g'`;
+am__vpath_adj = case $$p in \
+ $(srcdir)/*) f=`echo "$$p" | sed "s|^$$srcdirstrip/||"`;; \
+ *) f=$$p;; \
+ esac;
+am__strip_dir = f=`echo $$p | sed -e 's|^.*/||'`;
+am__install_max = 40
+am__nobase_strip_setup = \
+ srcdirstrip=`echo "$(srcdir)" | sed 's/[].[^$$\\*|]/\\\\&/g'`
+am__nobase_strip = \
+ for p in $$list; do echo "$$p"; done | sed -e "s|$$srcdirstrip/||"
+am__nobase_list = $(am__nobase_strip_setup); \
+ for p in $$list; do echo "$$p $$p"; done | \
+ sed "s| $$srcdirstrip/| |;"' / .*\//!s/ .*/ ./; s,\( .*\)/[^/]*$$,\1,' | \
+ $(AWK) 'BEGIN { files["."] = "" } { files[$$2] = files[$$2] " " $$1; \
+ if (++n[$$2] == $(am__install_max)) \
+ { print $$2, files[$$2]; n[$$2] = 0; files[$$2] = "" } } \
+ END { for (dir in files) print dir, files[dir] }'
+am__base_list = \
+ sed '$$!N;$$!N;$$!N;$$!N;$$!N;$$!N;$$!N;s/\n/ /g' | \
+ sed '$$!N;$$!N;$$!N;$$!N;s/\n/ /g'
+am__uninstall_files_from_dir = { \
+ test -z "$$files" \
+ || { test ! -d "$$dir" && test ! -f "$$dir" && test ! -r "$$dir"; } \
+ || { echo " ( cd '$$dir' && rm -f" $$files ")"; \
+ $(am__cd) "$$dir" && rm -f $$files; }; \
+ }
+am__installdirs = "$(DESTDIR)$(pkginc_libdir)"
+HEADERS = $(pkginc_lib_HEADERS)
+am__tagged_files = $(HEADERS) $(SOURCES) $(TAGS_FILES) $(LISP)
+# Read a list of newline-separated strings from the standard input,
+# and print each of them once, without duplicates. Input order is
+# *not* preserved.
+am__uniquify_input = $(AWK) '\
+ BEGIN { nonempty = 0; } \
+ { items[$$0] = 1; nonempty = 1; } \
+ END { if (nonempty) { for (i in items) print i; }; } \
+'
+# Make sure the list of sources is unique. This is necessary because,
+# e.g., the same source file might be shared among _SOURCES variables
+# for different programs/libraries.
+am__define_uniq_tagged_files = \
+ list='$(am__tagged_files)'; \
+ unique=`for i in $$list; do \
+ if test -f "$$i"; then echo $$i; else echo $(srcdir)/$$i; fi; \
+ done | $(am__uniquify_input)`
+ETAGS = etags
+CTAGS = ctags
+am__DIST_COMMON = $(srcdir)/Makefile.in $(top_srcdir)/depcomp
+DISTFILES = $(DIST_COMMON) $(DIST_SOURCES) $(TEXINFOS) $(EXTRA_DIST)
+ACLOCAL = @ACLOCAL@
+ACLOCAL_AMFLAGS = @ACLOCAL_AMFLAGS@
+AMTAR = @AMTAR@
+AM_DEFAULT_VERBOSITY = @AM_DEFAULT_VERBOSITY@
+APPARMOR_LIBS = @APPARMOR_LIBS@
+AR = @AR@
+AUTH_CFLAGS = @AUTH_CFLAGS@
+AUTH_LIBS = @AUTH_LIBS@
+AUTOCONF = @AUTOCONF@
+AUTOHEADER = @AUTOHEADER@
+AUTOMAKE = @AUTOMAKE@
+AWK = @AWK@
+BINARY_CFLAGS = @BINARY_CFLAGS@
+BINARY_LDFLAGS = @BINARY_LDFLAGS@
+BISON = @BISON@
+CASSANDRA_CFLAGS = @CASSANDRA_CFLAGS@
+CASSANDRA_LIBS = @CASSANDRA_LIBS@
+CC = @CC@
+CCDEPMODE = @CCDEPMODE@
+CDB_LIBS = @CDB_LIBS@
+CFLAGS = @CFLAGS@
+CLUCENE_CFLAGS = @CLUCENE_CFLAGS@
+CLUCENE_LIBS = @CLUCENE_LIBS@
+COMPRESS_LIBS = @COMPRESS_LIBS@
+CPP = @CPP@
+CPPFLAGS = @CPPFLAGS@
+CRYPT_LIBS = @CRYPT_LIBS@
+CXX = @CXX@
+CXXCPP = @CXXCPP@
+CXXDEPMODE = @CXXDEPMODE@
+CXXFLAGS = @CXXFLAGS@
+CYGPATH_W = @CYGPATH_W@
+DEFS = @DEFS@
+DEPDIR = @DEPDIR@
+DICT_LIBS = @DICT_LIBS@
+DLLIB = @DLLIB@
+DLLTOOL = @DLLTOOL@
+DSYMUTIL = @DSYMUTIL@
+DUMPBIN = @DUMPBIN@
+ECHO_C = @ECHO_C@
+ECHO_N = @ECHO_N@
+ECHO_T = @ECHO_T@
+EGREP = @EGREP@
+EXEEXT = @EXEEXT@
+FGREP = @FGREP@
+FLEX = @FLEX@
+FUZZER_CPPFLAGS = @FUZZER_CPPFLAGS@
+FUZZER_LDFLAGS = @FUZZER_LDFLAGS@
+GREP = @GREP@
+INSTALL = @INSTALL@
+INSTALL_DATA = @INSTALL_DATA@
+INSTALL_PROGRAM = @INSTALL_PROGRAM@
+INSTALL_SCRIPT = @INSTALL_SCRIPT@
+INSTALL_STRIP_PROGRAM = @INSTALL_STRIP_PROGRAM@
+KRB5CONFIG = @KRB5CONFIG@
+KRB5_CFLAGS = @KRB5_CFLAGS@
+KRB5_LIBS = @KRB5_LIBS@
+LD = @LD@
+LDAP_LIBS = @LDAP_LIBS@
+LDFLAGS = @LDFLAGS@
+LD_NO_WHOLE_ARCHIVE = @LD_NO_WHOLE_ARCHIVE@
+LD_WHOLE_ARCHIVE = @LD_WHOLE_ARCHIVE@
+LIBCAP = @LIBCAP@
+LIBDOVECOT = @LIBDOVECOT@
+LIBDOVECOT_COMPRESS = @LIBDOVECOT_COMPRESS@
+LIBDOVECOT_DEPS = @LIBDOVECOT_DEPS@
+LIBDOVECOT_DSYNC = @LIBDOVECOT_DSYNC@
+LIBDOVECOT_LA_LIBS = @LIBDOVECOT_LA_LIBS@
+LIBDOVECOT_LDA = @LIBDOVECOT_LDA@
+LIBDOVECOT_LDAP = @LIBDOVECOT_LDAP@
+LIBDOVECOT_LIBFTS = @LIBDOVECOT_LIBFTS@
+LIBDOVECOT_LIBFTS_DEPS = @LIBDOVECOT_LIBFTS_DEPS@
+LIBDOVECOT_LOGIN = @LIBDOVECOT_LOGIN@
+LIBDOVECOT_LUA = @LIBDOVECOT_LUA@
+LIBDOVECOT_LUA_DEPS = @LIBDOVECOT_LUA_DEPS@
+LIBDOVECOT_SQL = @LIBDOVECOT_SQL@
+LIBDOVECOT_STORAGE = @LIBDOVECOT_STORAGE@
+LIBDOVECOT_STORAGE_DEPS = @LIBDOVECOT_STORAGE_DEPS@
+LIBEXTTEXTCAT_CFLAGS = @LIBEXTTEXTCAT_CFLAGS@
+LIBEXTTEXTCAT_LIBS = @LIBEXTTEXTCAT_LIBS@
+LIBICONV = @LIBICONV@
+LIBICU_CFLAGS = @LIBICU_CFLAGS@
+LIBICU_LIBS = @LIBICU_LIBS@
+LIBOBJS = @LIBOBJS@
+LIBS = @LIBS@
+LIBSODIUM_CFLAGS = @LIBSODIUM_CFLAGS@
+LIBSODIUM_LIBS = @LIBSODIUM_LIBS@
+LIBTIRPC_CFLAGS = @LIBTIRPC_CFLAGS@
+LIBTIRPC_LIBS = @LIBTIRPC_LIBS@
+LIBTOOL = @LIBTOOL@
+LIBUNWIND_CFLAGS = @LIBUNWIND_CFLAGS@
+LIBUNWIND_LIBS = @LIBUNWIND_LIBS@
+LIBWRAP_LIBS = @LIBWRAP_LIBS@
+LINKED_STORAGE_LDADD = @LINKED_STORAGE_LDADD@
+LIPO = @LIPO@
+LN_S = @LN_S@
+LTLIBICONV = @LTLIBICONV@
+LTLIBOBJS = @LTLIBOBJS@
+LT_SYS_LIBRARY_PATH = @LT_SYS_LIBRARY_PATH@
+LUA_CFLAGS = @LUA_CFLAGS@
+LUA_LIBS = @LUA_LIBS@
+MAINT = @MAINT@
+MAKEINFO = @MAKEINFO@
+MANIFEST_TOOL = @MANIFEST_TOOL@
+MKDIR_P = @MKDIR_P@
+MODULE_LIBS = @MODULE_LIBS@
+MODULE_SUFFIX = @MODULE_SUFFIX@
+MYSQL_CFLAGS = @MYSQL_CFLAGS@
+MYSQL_CONFIG = @MYSQL_CONFIG@
+MYSQL_LIBS = @MYSQL_LIBS@
+NM = @NM@
+NMEDIT = @NMEDIT@
+NOPLUGIN_LDFLAGS = @NOPLUGIN_LDFLAGS@
+OBJDUMP = @OBJDUMP@
+OBJEXT = @OBJEXT@
+OTOOL = @OTOOL@
+OTOOL64 = @OTOOL64@
+PACKAGE = @PACKAGE@
+PACKAGE_BUGREPORT = @PACKAGE_BUGREPORT@
+PACKAGE_NAME = @PACKAGE_NAME@
+PACKAGE_STRING = @PACKAGE_STRING@
+PACKAGE_TARNAME = @PACKAGE_TARNAME@
+PACKAGE_URL = @PACKAGE_URL@
+PACKAGE_VERSION = @PACKAGE_VERSION@
+PANDOC = @PANDOC@
+PATH_SEPARATOR = @PATH_SEPARATOR@
+PGSQL_CFLAGS = @PGSQL_CFLAGS@
+PGSQL_LIBS = @PGSQL_LIBS@
+PG_CONFIG = @PG_CONFIG@
+PIE_CFLAGS = @PIE_CFLAGS@
+PIE_LDFLAGS = @PIE_LDFLAGS@
+PKG_CONFIG = @PKG_CONFIG@
+PKG_CONFIG_LIBDIR = @PKG_CONFIG_LIBDIR@
+PKG_CONFIG_PATH = @PKG_CONFIG_PATH@
+QUOTA_LIBS = @QUOTA_LIBS@
+RANLIB = @RANLIB@
+RELRO_LDFLAGS = @RELRO_LDFLAGS@
+RPCGEN = @RPCGEN@
+RUN_TEST = @RUN_TEST@
+SED = @SED@
+SETTING_FILES = @SETTING_FILES@
+SET_MAKE = @SET_MAKE@
+SHELL = @SHELL@
+SQLITE_CFLAGS = @SQLITE_CFLAGS@
+SQLITE_LIBS = @SQLITE_LIBS@
+SQL_CFLAGS = @SQL_CFLAGS@
+SQL_LIBS = @SQL_LIBS@
+SSL_CFLAGS = @SSL_CFLAGS@
+SSL_LIBS = @SSL_LIBS@
+STRIP = @STRIP@
+SYSTEMD_CFLAGS = @SYSTEMD_CFLAGS@
+SYSTEMD_LIBS = @SYSTEMD_LIBS@
+VALGRIND = @VALGRIND@
+VERSION = @VERSION@
+ZSTD_CFLAGS = @ZSTD_CFLAGS@
+ZSTD_LIBS = @ZSTD_LIBS@
+abs_builddir = @abs_builddir@
+abs_srcdir = @abs_srcdir@
+abs_top_builddir = @abs_top_builddir@
+abs_top_srcdir = @abs_top_srcdir@
+ac_ct_AR = @ac_ct_AR@
+ac_ct_CC = @ac_ct_CC@
+ac_ct_CXX = @ac_ct_CXX@
+ac_ct_DUMPBIN = @ac_ct_DUMPBIN@
+am__include = @am__include@
+am__leading_dot = @am__leading_dot@
+am__quote = @am__quote@
+am__tar = @am__tar@
+am__untar = @am__untar@
+bindir = @bindir@
+build = @build@
+build_alias = @build_alias@
+build_cpu = @build_cpu@
+build_os = @build_os@
+build_vendor = @build_vendor@
+builddir = @builddir@
+datadir = @datadir@
+datarootdir = @datarootdir@
+dict_drivers = @dict_drivers@
+docdir = @docdir@
+dvidir = @dvidir@
+exec_prefix = @exec_prefix@
+host = @host@
+host_alias = @host_alias@
+host_cpu = @host_cpu@
+host_os = @host_os@
+host_vendor = @host_vendor@
+htmldir = @htmldir@
+includedir = @includedir@
+infodir = @infodir@
+install_sh = @install_sh@
+libdir = @libdir@
+libexecdir = @libexecdir@
+localedir = @localedir@
+localstatedir = @localstatedir@
+mandir = @mandir@
+mkdir_p = @mkdir_p@
+moduledir = @moduledir@
+oldincludedir = @oldincludedir@
+pdfdir = @pdfdir@
+prefix = @prefix@
+program_transform_name = @program_transform_name@
+psdir = @psdir@
+rundir = @rundir@
+runstatedir = @runstatedir@
+sbindir = @sbindir@
+sharedstatedir = @sharedstatedir@
+sql_drivers = @sql_drivers@
+srcdir = @srcdir@
+ssldir = @ssldir@
+statedir = @statedir@
+sysconfdir = @sysconfdir@
+systemdservicetype = @systemdservicetype@
+systemdsystemunitdir = @systemdsystemunitdir@
+target_alias = @target_alias@
+top_build_prefix = @top_build_prefix@
+top_builddir = @top_builddir@
+top_srcdir = @top_srcdir@
+noinst_LTLIBRARIES = libimap_client.la
+AM_CPPFLAGS = \
+ -I$(top_srcdir)/src/lib \
+ -I$(top_srcdir)/src/lib-test \
+ -I$(top_srcdir)/src/lib-dns \
+ -I$(top_srcdir)/src/lib-sasl \
+ -I$(top_srcdir)/src/lib-ssl-iostream \
+ -I$(top_srcdir)/src/lib-mail \
+ -I$(top_srcdir)/src/lib-imap
+
+libimap_client_la_SOURCES = \
+ imapc-client.c \
+ imapc-connection.c \
+ imapc-msgmap.c
+
+headers = \
+ imapc-client.h \
+ imapc-client-private.h \
+ imapc-connection.h \
+ imapc-msgmap.h
+
+pkginc_libdir = $(pkgincludedir)
+pkginc_lib_HEADERS = $(headers)
+test_programs = \
+ test-imapc-client
+
+test_deps = \
+ $(noinst_LTLIBRARIES) \
+ ../lib-ssl-iostream/libssl_iostream.la \
+ ../lib-sasl/libsasl.la \
+ ../lib-imap/libimap.la \
+ ../lib-mail/libmail.la \
+ ../lib-charset/libcharset.la \
+ ../lib-dns/libdns.la \
+ ../lib-test/libtest.la \
+ ../lib/liblib.la
+
+test_libs = \
+ $(test_deps) \
+ $(MODULE_LIBS)
+
+test_imapc_client_SOURCES = test-imapc-client.c
+test_imapc_client_LDADD = $(test_libs)
+test_imapc_client_DEPENDENCIES = $(test_deps)
+all: all-am
+
+.SUFFIXES:
+.SUFFIXES: .c .lo .o .obj
+$(srcdir)/Makefile.in: @MAINTAINER_MODE_TRUE@ $(srcdir)/Makefile.am $(am__configure_deps)
+ @for dep in $?; do \
+ case '$(am__configure_deps)' in \
+ *$$dep*) \
+ ( cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh ) \
+ && { if test -f $@; then exit 0; else break; fi; }; \
+ exit 1;; \
+ esac; \
+ done; \
+ echo ' cd $(top_srcdir) && $(AUTOMAKE) --foreign src/lib-imap-client/Makefile'; \
+ $(am__cd) $(top_srcdir) && \
+ $(AUTOMAKE) --foreign src/lib-imap-client/Makefile
+Makefile: $(srcdir)/Makefile.in $(top_builddir)/config.status
+ @case '$?' in \
+ *config.status*) \
+ cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh;; \
+ *) \
+ echo ' cd $(top_builddir) && $(SHELL) ./config.status $(subdir)/$@ $(am__maybe_remake_depfiles)'; \
+ cd $(top_builddir) && $(SHELL) ./config.status $(subdir)/$@ $(am__maybe_remake_depfiles);; \
+ esac;
+
+$(top_builddir)/config.status: $(top_srcdir)/configure $(CONFIG_STATUS_DEPENDENCIES)
+ cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh
+
+$(top_srcdir)/configure: @MAINTAINER_MODE_TRUE@ $(am__configure_deps)
+ cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh
+$(ACLOCAL_M4): @MAINTAINER_MODE_TRUE@ $(am__aclocal_m4_deps)
+ cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh
+$(am__aclocal_m4_deps):
+
+clean-noinstPROGRAMS:
+ @list='$(noinst_PROGRAMS)'; test -n "$$list" || exit 0; \
+ echo " rm -f" $$list; \
+ rm -f $$list || exit $$?; \
+ test -n "$(EXEEXT)" || exit 0; \
+ list=`for p in $$list; do echo "$$p"; done | sed 's/$(EXEEXT)$$//'`; \
+ echo " rm -f" $$list; \
+ rm -f $$list
+
+clean-noinstLTLIBRARIES:
+ -test -z "$(noinst_LTLIBRARIES)" || rm -f $(noinst_LTLIBRARIES)
+ @list='$(noinst_LTLIBRARIES)'; \
+ locs=`for p in $$list; do echo $$p; done | \
+ sed 's|^[^/]*$$|.|; s|/[^/]*$$||; s|$$|/so_locations|' | \
+ sort -u`; \
+ test -z "$$locs" || { \
+ echo rm -f $${locs}; \
+ rm -f $${locs}; \
+ }
+
+libimap_client.la: $(libimap_client_la_OBJECTS) $(libimap_client_la_DEPENDENCIES) $(EXTRA_libimap_client_la_DEPENDENCIES)
+ $(AM_V_CCLD)$(LINK) $(libimap_client_la_OBJECTS) $(libimap_client_la_LIBADD) $(LIBS)
+
+test-imapc-client$(EXEEXT): $(test_imapc_client_OBJECTS) $(test_imapc_client_DEPENDENCIES) $(EXTRA_test_imapc_client_DEPENDENCIES)
+ @rm -f test-imapc-client$(EXEEXT)
+ $(AM_V_CCLD)$(LINK) $(test_imapc_client_OBJECTS) $(test_imapc_client_LDADD) $(LIBS)
+
+mostlyclean-compile:
+ -rm -f *.$(OBJEXT)
+
+distclean-compile:
+ -rm -f *.tab.c
+
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/imapc-client.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/imapc-connection.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/imapc-msgmap.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/test-imapc-client.Po@am__quote@ # am--include-marker
+
+$(am__depfiles_remade):
+ @$(MKDIR_P) $(@D)
+ @echo '# dummy' >$@-t && $(am__mv) $@-t $@
+
+am--depfiles: $(am__depfiles_remade)
+
+.c.o:
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(COMPILE) -MT $@ -MD -MP -MF $(DEPDIR)/$*.Tpo -c -o $@ $<
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/$*.Tpo $(DEPDIR)/$*.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='$<' object='$@' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(COMPILE) -c -o $@ $<
+
+.c.obj:
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(COMPILE) -MT $@ -MD -MP -MF $(DEPDIR)/$*.Tpo -c -o $@ `$(CYGPATH_W) '$<'`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/$*.Tpo $(DEPDIR)/$*.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='$<' object='$@' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(COMPILE) -c -o $@ `$(CYGPATH_W) '$<'`
+
+.c.lo:
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(LTCOMPILE) -MT $@ -MD -MP -MF $(DEPDIR)/$*.Tpo -c -o $@ $<
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/$*.Tpo $(DEPDIR)/$*.Plo
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='$<' object='$@' libtool=yes @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(LTCOMPILE) -c -o $@ $<
+
+mostlyclean-libtool:
+ -rm -f *.lo
+
+clean-libtool:
+ -rm -rf .libs _libs
+install-pkginc_libHEADERS: $(pkginc_lib_HEADERS)
+ @$(NORMAL_INSTALL)
+ @list='$(pkginc_lib_HEADERS)'; test -n "$(pkginc_libdir)" || list=; \
+ if test -n "$$list"; then \
+ echo " $(MKDIR_P) '$(DESTDIR)$(pkginc_libdir)'"; \
+ $(MKDIR_P) "$(DESTDIR)$(pkginc_libdir)" || exit 1; \
+ fi; \
+ for p in $$list; do \
+ if test -f "$$p"; then d=; else d="$(srcdir)/"; fi; \
+ echo "$$d$$p"; \
+ done | $(am__base_list) | \
+ while read files; do \
+ echo " $(INSTALL_HEADER) $$files '$(DESTDIR)$(pkginc_libdir)'"; \
+ $(INSTALL_HEADER) $$files "$(DESTDIR)$(pkginc_libdir)" || exit $$?; \
+ done
+
+uninstall-pkginc_libHEADERS:
+ @$(NORMAL_UNINSTALL)
+ @list='$(pkginc_lib_HEADERS)'; test -n "$(pkginc_libdir)" || list=; \
+ files=`for p in $$list; do echo $$p; done | sed -e 's|^.*/||'`; \
+ dir='$(DESTDIR)$(pkginc_libdir)'; $(am__uninstall_files_from_dir)
+
+ID: $(am__tagged_files)
+ $(am__define_uniq_tagged_files); mkid -fID $$unique
+tags: tags-am
+TAGS: tags
+
+tags-am: $(TAGS_DEPENDENCIES) $(am__tagged_files)
+ set x; \
+ here=`pwd`; \
+ $(am__define_uniq_tagged_files); \
+ shift; \
+ if test -z "$(ETAGS_ARGS)$$*$$unique"; then :; else \
+ test -n "$$unique" || unique=$$empty_fix; \
+ if test $$# -gt 0; then \
+ $(ETAGS) $(ETAGSFLAGS) $(AM_ETAGSFLAGS) $(ETAGS_ARGS) \
+ "$$@" $$unique; \
+ else \
+ $(ETAGS) $(ETAGSFLAGS) $(AM_ETAGSFLAGS) $(ETAGS_ARGS) \
+ $$unique; \
+ fi; \
+ fi
+ctags: ctags-am
+
+CTAGS: ctags
+ctags-am: $(TAGS_DEPENDENCIES) $(am__tagged_files)
+ $(am__define_uniq_tagged_files); \
+ test -z "$(CTAGS_ARGS)$$unique" \
+ || $(CTAGS) $(CTAGSFLAGS) $(AM_CTAGSFLAGS) $(CTAGS_ARGS) \
+ $$unique
+
+GTAGS:
+ here=`$(am__cd) $(top_builddir) && pwd` \
+ && $(am__cd) $(top_srcdir) \
+ && gtags -i $(GTAGS_ARGS) "$$here"
+cscopelist: cscopelist-am
+
+cscopelist-am: $(am__tagged_files)
+ list='$(am__tagged_files)'; \
+ case "$(srcdir)" in \
+ [\\/]* | ?:[\\/]*) sdir="$(srcdir)" ;; \
+ *) sdir=$(subdir)/$(srcdir) ;; \
+ esac; \
+ for i in $$list; do \
+ if test -f "$$i"; then \
+ echo "$(subdir)/$$i"; \
+ else \
+ echo "$$sdir/$$i"; \
+ fi; \
+ done >> $(top_builddir)/cscope.files
+
+distclean-tags:
+ -rm -f TAGS ID GTAGS GRTAGS GSYMS GPATH tags
+
+distdir: $(BUILT_SOURCES)
+ $(MAKE) $(AM_MAKEFLAGS) distdir-am
+
+distdir-am: $(DISTFILES)
+ @srcdirstrip=`echo "$(srcdir)" | sed 's/[].[^$$\\*]/\\\\&/g'`; \
+ topsrcdirstrip=`echo "$(top_srcdir)" | sed 's/[].[^$$\\*]/\\\\&/g'`; \
+ list='$(DISTFILES)'; \
+ dist_files=`for file in $$list; do echo $$file; done | \
+ sed -e "s|^$$srcdirstrip/||;t" \
+ -e "s|^$$topsrcdirstrip/|$(top_builddir)/|;t"`; \
+ case $$dist_files in \
+ */*) $(MKDIR_P) `echo "$$dist_files" | \
+ sed '/\//!d;s|^|$(distdir)/|;s,/[^/]*$$,,' | \
+ sort -u` ;; \
+ esac; \
+ for file in $$dist_files; do \
+ if test -f $$file || test -d $$file; then d=.; else d=$(srcdir); fi; \
+ if test -d $$d/$$file; then \
+ dir=`echo "/$$file" | sed -e 's,/[^/]*$$,,'`; \
+ if test -d "$(distdir)/$$file"; then \
+ find "$(distdir)/$$file" -type d ! -perm -700 -exec chmod u+rwx {} \;; \
+ fi; \
+ if test -d $(srcdir)/$$file && test $$d != $(srcdir); then \
+ cp -fpR $(srcdir)/$$file "$(distdir)$$dir" || exit 1; \
+ find "$(distdir)/$$file" -type d ! -perm -700 -exec chmod u+rwx {} \;; \
+ fi; \
+ cp -fpR $$d/$$file "$(distdir)$$dir" || exit 1; \
+ else \
+ test -f "$(distdir)/$$file" \
+ || cp -p $$d/$$file "$(distdir)/$$file" \
+ || exit 1; \
+ fi; \
+ done
+check-am: all-am
+ $(MAKE) $(AM_MAKEFLAGS) check-local
+check: check-am
+all-am: Makefile $(PROGRAMS) $(LTLIBRARIES) $(HEADERS)
+installdirs:
+ for dir in "$(DESTDIR)$(pkginc_libdir)"; do \
+ test -z "$$dir" || $(MKDIR_P) "$$dir"; \
+ done
+install: install-am
+install-exec: install-exec-am
+install-data: install-data-am
+uninstall: uninstall-am
+
+install-am: all-am
+ @$(MAKE) $(AM_MAKEFLAGS) install-exec-am install-data-am
+
+installcheck: installcheck-am
+install-strip:
+ if test -z '$(STRIP)'; then \
+ $(MAKE) $(AM_MAKEFLAGS) INSTALL_PROGRAM="$(INSTALL_STRIP_PROGRAM)" \
+ install_sh_PROGRAM="$(INSTALL_STRIP_PROGRAM)" INSTALL_STRIP_FLAG=-s \
+ install; \
+ else \
+ $(MAKE) $(AM_MAKEFLAGS) INSTALL_PROGRAM="$(INSTALL_STRIP_PROGRAM)" \
+ install_sh_PROGRAM="$(INSTALL_STRIP_PROGRAM)" INSTALL_STRIP_FLAG=-s \
+ "INSTALL_PROGRAM_ENV=STRIPPROG='$(STRIP)'" install; \
+ fi
+mostlyclean-generic:
+
+clean-generic:
+
+distclean-generic:
+ -test -z "$(CONFIG_CLEAN_FILES)" || rm -f $(CONFIG_CLEAN_FILES)
+ -test . = "$(srcdir)" || test -z "$(CONFIG_CLEAN_VPATH_FILES)" || rm -f $(CONFIG_CLEAN_VPATH_FILES)
+
+maintainer-clean-generic:
+ @echo "This command is intended for maintainers to use"
+ @echo "it deletes files that may require special tools to rebuild."
+clean: clean-am
+
+clean-am: clean-generic clean-libtool clean-noinstLTLIBRARIES \
+ clean-noinstPROGRAMS mostlyclean-am
+
+distclean: distclean-am
+ -rm -f ./$(DEPDIR)/imapc-client.Plo
+ -rm -f ./$(DEPDIR)/imapc-connection.Plo
+ -rm -f ./$(DEPDIR)/imapc-msgmap.Plo
+ -rm -f ./$(DEPDIR)/test-imapc-client.Po
+ -rm -f Makefile
+distclean-am: clean-am distclean-compile distclean-generic \
+ distclean-tags
+
+dvi: dvi-am
+
+dvi-am:
+
+html: html-am
+
+html-am:
+
+info: info-am
+
+info-am:
+
+install-data-am: install-pkginc_libHEADERS
+
+install-dvi: install-dvi-am
+
+install-dvi-am:
+
+install-exec-am:
+
+install-html: install-html-am
+
+install-html-am:
+
+install-info: install-info-am
+
+install-info-am:
+
+install-man:
+
+install-pdf: install-pdf-am
+
+install-pdf-am:
+
+install-ps: install-ps-am
+
+install-ps-am:
+
+installcheck-am:
+
+maintainer-clean: maintainer-clean-am
+ -rm -f ./$(DEPDIR)/imapc-client.Plo
+ -rm -f ./$(DEPDIR)/imapc-connection.Plo
+ -rm -f ./$(DEPDIR)/imapc-msgmap.Plo
+ -rm -f ./$(DEPDIR)/test-imapc-client.Po
+ -rm -f Makefile
+maintainer-clean-am: distclean-am maintainer-clean-generic
+
+mostlyclean: mostlyclean-am
+
+mostlyclean-am: mostlyclean-compile mostlyclean-generic \
+ mostlyclean-libtool
+
+pdf: pdf-am
+
+pdf-am:
+
+ps: ps-am
+
+ps-am:
+
+uninstall-am: uninstall-pkginc_libHEADERS
+
+.MAKE: check-am install-am install-strip
+
+.PHONY: CTAGS GTAGS TAGS all all-am am--depfiles check check-am \
+ check-local clean clean-generic clean-libtool \
+ clean-noinstLTLIBRARIES clean-noinstPROGRAMS cscopelist-am \
+ ctags ctags-am distclean distclean-compile distclean-generic \
+ distclean-libtool distclean-tags distdir dvi dvi-am html \
+ html-am info info-am install install-am install-data \
+ install-data-am install-dvi install-dvi-am install-exec \
+ install-exec-am install-html install-html-am install-info \
+ install-info-am install-man install-pdf install-pdf-am \
+ install-pkginc_libHEADERS install-ps install-ps-am \
+ install-strip installcheck installcheck-am installdirs \
+ maintainer-clean maintainer-clean-generic mostlyclean \
+ mostlyclean-compile mostlyclean-generic mostlyclean-libtool \
+ pdf pdf-am ps ps-am tags tags-am uninstall uninstall-am \
+ uninstall-pkginc_libHEADERS
+
+.PRECIOUS: Makefile
+
+
+check-local:
+ for bin in $(test_programs); do \
+ if ! $(RUN_TEST) ./$$bin; then exit 1; fi; \
+ done
+
+# Tell versions [3.59,3.63) of GNU make to not export all variables.
+# Otherwise a system limit (for SysV at least) may be exceeded.
+.NOEXPORT:
diff --git a/src/lib-imap-client/imapc-client-private.h b/src/lib-imap-client/imapc-client-private.h
new file mode 100644
index 0000000..98c4e8e
--- /dev/null
+++ b/src/lib-imap-client/imapc-client-private.h
@@ -0,0 +1,63 @@
+#ifndef IMAPC_CLIENT_PRIVATE_H
+#define IMAPC_CLIENT_PRIVATE_H
+
+#include "imapc-client.h"
+
+#define IMAPC_CLIENT_IDLE_SEND_DELAY_MSECS 100
+
+struct imapc_client_connection {
+ struct imapc_connection *conn;
+ struct imapc_client *client;
+ struct imapc_client_mailbox *box;
+};
+
+struct imapc_client {
+ pool_t pool;
+ int refcount;
+
+ struct event *event;
+ struct imapc_client_settings set;
+ struct ssl_iostream_context *ssl_ctx;
+
+ imapc_untagged_callback_t *untagged_callback;
+ void *untagged_context;
+
+ imapc_state_change_callback_t *state_change_callback;
+ void *state_change_context;
+
+ imapc_command_callback_t *login_callback;
+ void *login_context;
+
+ ARRAY(struct imapc_client_connection *) conns;
+ bool logging_out;
+
+ struct ioloop *ioloop;
+ bool stop_on_state_finish;
+};
+
+struct imapc_client_mailbox {
+ struct imapc_client *client;
+ struct imapc_connection *conn;
+ struct imapc_msgmap *msgmap;
+ struct timeout *to_send_idle;
+
+ void (*reopen_callback)(void *context);
+ void *reopen_context;
+
+ void *untagged_box_context;
+
+ bool reconnect_ok;
+ bool reconnecting;
+ bool closing;
+};
+
+extern unsigned int imapc_client_cmd_tag_counter;
+
+void imapc_client_ref(struct imapc_client *client);
+void imapc_client_unref(struct imapc_client **client);
+
+void imapc_command_set_mailbox(struct imapc_command *cmd,
+ struct imapc_client_mailbox *box);
+void imapc_client_try_stop(struct imapc_client *client);
+
+#endif
diff --git a/src/lib-imap-client/imapc-client.c b/src/lib-imap-client/imapc-client.c
new file mode 100644
index 0000000..adb4dcb
--- /dev/null
+++ b/src/lib-imap-client/imapc-client.c
@@ -0,0 +1,584 @@
+/* Copyright (c) 2011-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "str.h"
+#include "ioloop.h"
+#include "safe-mkstemp.h"
+#include "iostream-ssl.h"
+#include "imapc-msgmap.h"
+#include "imapc-connection.h"
+#include "imapc-client-private.h"
+
+#include <unistd.h>
+
+const char *imapc_command_state_names[] = {
+ "OK", "NO", "BAD", "(auth failed)", "(disconnected)"
+
+};
+
+const struct imapc_capability_name imapc_capability_names[] = {
+ { "SASL-IR", IMAPC_CAPABILITY_SASL_IR },
+ { "LITERAL+", IMAPC_CAPABILITY_LITERALPLUS },
+ { "QRESYNC", IMAPC_CAPABILITY_QRESYNC },
+ { "IDLE", IMAPC_CAPABILITY_IDLE },
+ { "UIDPLUS", IMAPC_CAPABILITY_UIDPLUS },
+ { "AUTH=PLAIN", IMAPC_CAPABILITY_AUTH_PLAIN },
+ { "STARTTLS", IMAPC_CAPABILITY_STARTTLS },
+ { "X-GM-EXT-1", IMAPC_CAPABILITY_X_GM_EXT_1 },
+ { "CONDSTORE", IMAPC_CAPABILITY_CONDSTORE },
+ { "NAMESPACE", IMAPC_CAPABILITY_NAMESPACE },
+ { "UNSELECT", IMAPC_CAPABILITY_UNSELECT },
+ { "ESEARCH", IMAPC_CAPABILITY_ESEARCH },
+ { "WITHIN", IMAPC_CAPABILITY_WITHIN },
+ { "QUOTA", IMAPC_CAPABILITY_QUOTA },
+ { "ID", IMAPC_CAPABILITY_ID },
+ { "SAVEDATE", IMAPC_CAPABILITY_SAVEDATE },
+
+ { "IMAP4REV1", IMAPC_CAPABILITY_IMAP4REV1 },
+ { NULL, 0 }
+};
+
+unsigned int imapc_client_cmd_tag_counter = 0;
+
+static void
+default_untagged_callback(const struct imapc_untagged_reply *reply ATTR_UNUSED,
+ void *context ATTR_UNUSED)
+{
+}
+
+struct imapc_client *
+imapc_client_init(const struct imapc_client_settings *set,
+ struct event *event_parent)
+{
+ struct imapc_client *client;
+ const char *error;
+ pool_t pool;
+
+ i_assert(set->connect_retry_count == 0 ||
+ set->connect_retry_interval_msecs > 0);
+
+ pool = pool_alloconly_create("imapc client", 1024);
+ client = p_new(pool, struct imapc_client, 1);
+ client->pool = pool;
+ client->refcount = 1;
+ client->event = event_create(event_parent);
+
+ client->set.debug = set->debug;
+ client->set.host = p_strdup(pool, set->host);
+ client->set.port = set->port;
+ client->set.master_user = p_strdup_empty(pool, set->master_user);
+ client->set.username = p_strdup(pool, set->username);
+ client->set.password = p_strdup(pool, set->password);
+ client->set.sasl_mechanisms = p_strdup(pool, set->sasl_mechanisms);
+ client->set.session_id_prefix = p_strdup(pool, set->session_id_prefix);
+ client->set.use_proxyauth = set->use_proxyauth;
+ client->set.dns_client_socket_path =
+ p_strdup(pool, set->dns_client_socket_path);
+ client->set.temp_path_prefix =
+ p_strdup(pool, set->temp_path_prefix);
+ client->set.rawlog_dir = p_strdup(pool, set->rawlog_dir);
+ client->set.max_idle_time = set->max_idle_time;
+ client->set.connect_timeout_msecs = set->connect_timeout_msecs != 0 ?
+ set->connect_timeout_msecs :
+ IMAPC_DEFAULT_CONNECT_TIMEOUT_MSECS;
+ client->set.connect_retry_count = set->connect_retry_count;
+ client->set.connect_retry_interval_msecs = set->connect_retry_interval_msecs;
+ client->set.cmd_timeout_msecs = set->cmd_timeout_msecs != 0 ?
+ set->cmd_timeout_msecs : IMAPC_DEFAULT_COMMAND_TIMEOUT_MSECS;
+ client->set.max_line_length = set->max_line_length != 0 ?
+ set->max_line_length : IMAPC_DEFAULT_MAX_LINE_LENGTH;
+ client->set.throttle_set = set->throttle_set;
+
+ if (client->set.throttle_set.init_msecs == 0)
+ client->set.throttle_set.init_msecs = IMAPC_THROTTLE_DEFAULT_INIT_MSECS;
+ if (client->set.throttle_set.max_msecs == 0)
+ client->set.throttle_set.max_msecs = IMAPC_THROTTLE_DEFAULT_MAX_MSECS;
+ if (client->set.throttle_set.shrink_min_msecs == 0)
+ client->set.throttle_set.shrink_min_msecs = IMAPC_THROTTLE_DEFAULT_SHRINK_MIN_MSECS;
+
+ if (set->ssl_mode != IMAPC_CLIENT_SSL_MODE_NONE) {
+ client->set.ssl_mode = set->ssl_mode;
+ ssl_iostream_settings_init_from(pool, &client->set.ssl_set, &set->ssl_set);
+ client->set.ssl_set.verbose_invalid_cert = !client->set.ssl_set.allow_invalid_cert;
+ if (ssl_iostream_client_context_cache_get(&client->set.ssl_set,
+ &client->ssl_ctx,
+ &error) < 0) {
+ i_error("imapc(%s:%u): Couldn't initialize SSL context: %s",
+ set->host, set->port, error);
+ }
+ }
+ client->untagged_callback = default_untagged_callback;
+
+ p_array_init(&client->conns, pool, 8);
+ return client;
+}
+
+void imapc_client_ref(struct imapc_client *client)
+{
+ i_assert(client->refcount > 0);
+
+ client->refcount++;
+}
+
+void imapc_client_unref(struct imapc_client **_client)
+{
+ struct imapc_client *client = *_client;
+
+ *_client = NULL;
+
+ i_assert(client->refcount > 0);
+ if (--client->refcount > 0)
+ return;
+
+ if (client->ssl_ctx != NULL)
+ ssl_iostream_context_unref(&client->ssl_ctx);
+ event_unref(&client->event);
+ pool_unref(&client->pool);
+}
+
+void imapc_client_disconnect(struct imapc_client *client)
+{
+ struct imapc_client_connection *const *conns, *conn;
+ unsigned int i, count;
+
+ conns = array_get(&client->conns, &count);
+ for (i = count; i > 0; i--) {
+ conn = conns[i-1];
+ array_delete(&client->conns, i-1, 1);
+
+ i_assert(imapc_connection_get_mailbox(conn->conn) == NULL);
+ imapc_connection_deinit(&conn->conn);
+ i_free(conn);
+ }
+}
+
+void imapc_client_deinit(struct imapc_client **_client)
+{
+ struct imapc_client *client = *_client;
+
+ imapc_client_disconnect(client);
+ imapc_client_unref(_client);
+}
+
+void imapc_client_register_untagged(struct imapc_client *client,
+ imapc_untagged_callback_t *callback,
+ void *context)
+{
+ client->untagged_callback = callback;
+ client->untagged_context = context;
+}
+
+static void imapc_client_run_pre(struct imapc_client *client)
+{
+ struct imapc_client_connection *conn;
+ struct ioloop *prev_ioloop = current_ioloop;
+
+ i_assert(client->ioloop == NULL);
+
+ client->ioloop = io_loop_create();
+ io_loop_set_running(client->ioloop);
+
+ array_foreach_elem(&client->conns, conn) {
+ imapc_connection_ioloop_changed(conn->conn);
+ if (imapc_connection_get_state(conn->conn) == IMAPC_CONNECTION_STATE_DISCONNECTED)
+ imapc_connection_connect(conn->conn);
+ }
+
+ if (io_loop_is_running(client->ioloop))
+ io_loop_run(client->ioloop);
+ io_loop_set_current(prev_ioloop);
+}
+
+static void imapc_client_run_post(struct imapc_client *client)
+{
+ struct imapc_client_connection *conn;
+ struct ioloop *ioloop = client->ioloop;
+
+ client->ioloop = NULL;
+ array_foreach_elem(&client->conns, conn)
+ imapc_connection_ioloop_changed(conn->conn);
+
+ io_loop_set_current(ioloop);
+ io_loop_destroy(&ioloop);
+}
+
+void imapc_client_run(struct imapc_client *client)
+{
+ imapc_client_run_pre(client);
+ imapc_client_run_post(client);
+}
+
+void imapc_client_stop(struct imapc_client *client)
+{
+ if (client->ioloop != NULL)
+ io_loop_stop(client->ioloop);
+}
+
+void imapc_client_try_stop(struct imapc_client *client)
+{
+ struct imapc_client_connection *conn;
+ array_foreach_elem(&client->conns, conn)
+ if (imapc_connection_get_state(conn->conn) != IMAPC_CONNECTION_STATE_DISCONNECTED)
+ return;
+ imapc_client_stop(client);
+}
+
+bool imapc_client_is_running(struct imapc_client *client)
+{
+ return client->ioloop != NULL;
+}
+
+static void imapc_client_login_callback(const struct imapc_command_reply *reply,
+ void *context)
+{
+ struct imapc_client_connection *conn = context;
+ struct imapc_client *client = conn->client;
+ struct imapc_client_mailbox *box = conn->box;
+
+ if (box != NULL && box->reconnecting) {
+ box->reconnecting = FALSE;
+
+ if (reply->state == IMAPC_COMMAND_STATE_OK) {
+ /* reopen the mailbox */
+ box->reopen_callback(box->reopen_context);
+ } else {
+ imapc_connection_abort_commands(box->conn, NULL, FALSE);
+ }
+ }
+
+ /* call the login callback only once */
+ if (client->login_callback != NULL) {
+ imapc_command_callback_t *callback = client->login_callback;
+ void *context = client->login_context;
+
+ client->login_callback = NULL;
+ client->login_context = NULL;
+ callback(reply, context);
+ }
+}
+
+static struct imapc_client_connection *
+imapc_client_add_connection(struct imapc_client *client)
+{
+ struct imapc_client_connection *conn;
+
+ conn = i_new(struct imapc_client_connection, 1);
+ conn->client = client;
+ conn->conn = imapc_connection_init(client, imapc_client_login_callback,
+ conn);
+ array_push_back(&client->conns, &conn);
+ return conn;
+}
+
+static struct imapc_connection *
+imapc_client_find_connection(struct imapc_client *client)
+{
+ struct imapc_client_connection *const *connp;
+
+ /* FIXME: stupid algorithm */
+ if (array_count(&client->conns) == 0)
+ return imapc_client_add_connection(client)->conn;
+ connp = array_front(&client->conns);
+ return (*connp)->conn;
+}
+
+struct imapc_command *
+imapc_client_cmd(struct imapc_client *client,
+ imapc_command_callback_t *callback, void *context)
+{
+ struct imapc_connection *conn;
+
+ conn = imapc_client_find_connection(client);
+ return imapc_connection_cmd(conn, callback, context);
+}
+
+static struct imapc_client_connection *
+imapc_client_get_unboxed_connection(struct imapc_client *client)
+{
+ struct imapc_client_connection *const *conns;
+ unsigned int i, count;
+
+ conns = array_get(&client->conns, &count);
+ for (i = 0; i < count; i++) {
+ if (conns[i]->box == NULL)
+ return conns[i];
+ }
+ return imapc_client_add_connection(client);
+}
+
+
+void imapc_client_login(struct imapc_client *client)
+{
+ struct imapc_client_connection *conn;
+
+ i_assert(client->login_callback != NULL);
+ i_assert(array_count(&client->conns) == 0);
+
+ conn = imapc_client_add_connection(client);
+ imapc_connection_connect(conn->conn);
+}
+
+struct imapc_logout_ctx {
+ struct imapc_client *client;
+ unsigned int logout_count;
+};
+
+static void
+imapc_client_logout_callback(const struct imapc_command_reply *reply ATTR_UNUSED,
+ void *context)
+{
+ struct imapc_logout_ctx *ctx = context;
+
+ i_assert(ctx->logout_count > 0);
+
+ if (--ctx->logout_count == 0)
+ imapc_client_stop(ctx->client);
+}
+
+void imapc_client_logout(struct imapc_client *client)
+{
+ struct imapc_logout_ctx ctx = { .client = client };
+ struct imapc_client_connection *conn;
+ struct imapc_command *cmd;
+
+ client->logging_out = TRUE;
+
+ /* send LOGOUT to all connections */
+ array_foreach_elem(&client->conns, conn) {
+ if (imapc_connection_get_state(conn->conn) == IMAPC_CONNECTION_STATE_DISCONNECTED)
+ continue;
+ imapc_connection_set_no_reconnect(conn->conn);
+ ctx.logout_count++;
+ cmd = imapc_connection_cmd(conn->conn,
+ imapc_client_logout_callback, &ctx);
+ imapc_command_set_flags(cmd, IMAPC_COMMAND_FLAG_PRELOGIN |
+ IMAPC_COMMAND_FLAG_LOGOUT);
+ imapc_command_send(cmd, "LOGOUT");
+ }
+
+ /* wait for LOGOUT to finish */
+ while (ctx.logout_count > 0)
+ imapc_client_run(client);
+
+ /* we should have disconnected all clients already, but if there were
+ any timeouts there may be some clients left. */
+ imapc_client_disconnect(client);
+}
+
+struct imapc_client_mailbox *
+imapc_client_mailbox_open(struct imapc_client *client,
+ void *untagged_box_context)
+{
+ struct imapc_client_mailbox *box;
+ struct imapc_client_connection *conn;
+
+ box = i_new(struct imapc_client_mailbox, 1);
+ box->client = client;
+ box->untagged_box_context = untagged_box_context;
+ conn = imapc_client_get_unboxed_connection(client);
+ conn->box = box;
+ box->conn = conn->conn;
+ box->msgmap = imapc_msgmap_init();
+ /* if we get disconnected before the SELECT is finished, allow
+ one reconnect retry. */
+ box->reconnect_ok = TRUE;
+ return box;
+}
+
+void imapc_client_mailbox_set_reopen_cb(struct imapc_client_mailbox *box,
+ void (*callback)(void *context),
+ void *context)
+{
+ box->reopen_callback = callback;
+ box->reopen_context = context;
+}
+
+bool imapc_client_mailbox_can_reconnect(struct imapc_client_mailbox *box)
+{
+ /* the reconnect_ok flag attempts to avoid infinite reconnection loops
+ to a server that keeps disconnecting us (e.g. some of the commands
+ we send keeps crashing it always) */
+ return box->reopen_callback != NULL && box->reconnect_ok;
+}
+
+void imapc_client_mailbox_reconnect(struct imapc_client_mailbox *box,
+ const char *errmsg)
+{
+ imapc_connection_try_reconnect(box->conn, errmsg, 0, FALSE);
+}
+
+void imapc_client_mailbox_close(struct imapc_client_mailbox **_box)
+{
+ struct imapc_client_mailbox *box = *_box;
+ struct imapc_client_connection *conn;
+
+ box->closing = TRUE;
+
+ /* cancel any pending commands */
+ imapc_connection_unselect(box);
+
+ if (box->reconnecting) {
+ /* need to abort the reconnection so it won't try to access
+ the box */
+ imapc_connection_disconnect(box->conn);
+ }
+
+ /* set this only after unselect, which may cancel some commands that
+ reference this box */
+ *_box = NULL;
+
+ array_foreach_elem(&box->client->conns, conn) {
+ if (conn->box == box) {
+ conn->box = NULL;
+ break;
+ }
+ }
+
+ imapc_msgmap_deinit(&box->msgmap);
+ timeout_remove(&box->to_send_idle);
+ i_free(box);
+}
+
+struct imapc_command *
+imapc_client_mailbox_cmd(struct imapc_client_mailbox *box,
+ imapc_command_callback_t *callback, void *context)
+{
+ struct imapc_command *cmd;
+
+ i_assert(!box->closing);
+
+ cmd = imapc_connection_cmd(box->conn, callback, context);
+ imapc_command_set_mailbox(cmd, box);
+ return cmd;
+}
+
+struct imapc_msgmap *
+imapc_client_mailbox_get_msgmap(struct imapc_client_mailbox *box)
+{
+ return box->msgmap;
+}
+
+static void imapc_client_mailbox_idle_send(struct imapc_client_mailbox *box)
+{
+ timeout_remove(&box->to_send_idle);
+ if (imapc_client_mailbox_is_opened(box))
+ imapc_connection_idle(box->conn);
+}
+
+void imapc_client_mailbox_idle(struct imapc_client_mailbox *box)
+{
+ /* send the IDLE with a delay to avoid unnecessary IDLEs that are
+ immediately aborted */
+ if (box->to_send_idle == NULL && imapc_client_mailbox_is_opened(box)) {
+ box->to_send_idle =
+ timeout_add_short(IMAPC_CLIENT_IDLE_SEND_DELAY_MSECS,
+ imapc_client_mailbox_idle_send, box);
+ }
+ /* we're done with all work at this point. */
+ box->reconnect_ok = TRUE;
+}
+
+bool imapc_client_mailbox_is_opened(struct imapc_client_mailbox *box)
+{
+ struct imapc_client_mailbox *selected_box;
+
+ if (box->closing ||
+ imapc_connection_get_state(box->conn) != IMAPC_CONNECTION_STATE_DONE)
+ return FALSE;
+
+ selected_box = imapc_connection_get_mailbox(box->conn);
+ if (selected_box != box) {
+ if (selected_box != NULL)
+ i_error("imapc: Selected mailbox changed unexpectedly");
+ return FALSE;
+ }
+ return TRUE;
+}
+
+static bool
+imapc_client_get_any_capabilities(struct imapc_client *client,
+ enum imapc_capability *capabilities_r)
+{
+ struct imapc_client_connection *conn;
+
+ array_foreach_elem(&client->conns, conn) {
+ if (imapc_connection_get_state(conn->conn) == IMAPC_CONNECTION_STATE_DONE) {
+ *capabilities_r = imapc_connection_get_capabilities(conn->conn);
+ return TRUE;
+ }
+ }
+ return FALSE;
+}
+
+int imapc_client_get_capabilities(struct imapc_client *client,
+ enum imapc_capability *capabilities_r)
+{
+ /* try to find a connection that is already logged in */
+ if (imapc_client_get_any_capabilities(client, capabilities_r))
+ return 0;
+
+ /* if there are no connections yet, create one */
+ if (array_count(&client->conns) == 0)
+ (void)imapc_client_add_connection(client);
+
+ /* wait for any of the connections to login */
+ client->stop_on_state_finish = TRUE;
+ imapc_client_run(client);
+ client->stop_on_state_finish = FALSE;
+ if (imapc_client_get_any_capabilities(client, capabilities_r))
+ return 0;
+
+ /* failed */
+ return -1;
+}
+
+int imapc_client_create_temp_fd(struct imapc_client *client,
+ const char **path_r)
+{
+ string_t *path;
+ int fd;
+
+ if (client->set.temp_path_prefix == NULL) {
+ i_error("imapc: temp_path_prefix not set, "
+ "can't create temp file");
+ return -1;
+ }
+
+ path = t_str_new(128);
+ str_append(path, client->set.temp_path_prefix);
+ fd = safe_mkstemp(path, 0600, (uid_t)-1, (gid_t)-1);
+ if (fd == -1) {
+ i_error("safe_mkstemp(%s) failed: %m", str_c(path));
+ return -1;
+ }
+
+ /* we just want the fd, unlink it */
+ if (i_unlink(str_c(path)) < 0) {
+ /* shouldn't happen.. */
+ i_close_fd(&fd);
+ return -1;
+ }
+ *path_r = str_c(path);
+ return fd;
+}
+
+void imapc_client_register_state_change_callback(struct imapc_client *client,
+ imapc_state_change_callback_t *cb,
+ void *context)
+{
+ i_assert(client->state_change_callback == NULL);
+ i_assert(client->state_change_context == NULL);
+
+ client->state_change_callback = cb;
+ client->state_change_context = context;
+}
+
+void
+imapc_client_set_login_callback(struct imapc_client *client,
+ imapc_command_callback_t *callback, void *context)
+{
+ client->login_callback = callback;
+ client->login_context = context;
+}
+
diff --git a/src/lib-imap-client/imapc-client.h b/src/lib-imap-client/imapc-client.h
new file mode 100644
index 0000000..5c81be6
--- /dev/null
+++ b/src/lib-imap-client/imapc-client.h
@@ -0,0 +1,250 @@
+#ifndef IMAPC_CLIENT_H
+#define IMAPC_CLIENT_H
+
+#include "net.h"
+#include "iostream-ssl.h"
+
+/* IMAP RFC defines this to be at least 30 minutes. */
+#define IMAPC_DEFAULT_MAX_IDLE_TIME (60*29)
+
+enum imapc_command_state {
+ IMAPC_COMMAND_STATE_OK = 0,
+ IMAPC_COMMAND_STATE_NO,
+ IMAPC_COMMAND_STATE_BAD,
+ /* Authentication to IMAP server failed (NO or BAD) */
+ IMAPC_COMMAND_STATE_AUTH_FAILED,
+ /* Client was unexpectedly disconnected. */
+ IMAPC_COMMAND_STATE_DISCONNECTED
+};
+extern const char *imapc_command_state_names[];
+
+enum imapc_capability {
+ IMAPC_CAPABILITY_SASL_IR = 0x01,
+ IMAPC_CAPABILITY_LITERALPLUS = 0x02,
+ IMAPC_CAPABILITY_QRESYNC = 0x04,
+ IMAPC_CAPABILITY_IDLE = 0x08,
+ IMAPC_CAPABILITY_UIDPLUS = 0x10,
+ IMAPC_CAPABILITY_AUTH_PLAIN = 0x20,
+ IMAPC_CAPABILITY_STARTTLS = 0x40,
+ IMAPC_CAPABILITY_X_GM_EXT_1 = 0x80,
+ IMAPC_CAPABILITY_CONDSTORE = 0x100,
+ IMAPC_CAPABILITY_NAMESPACE = 0x200,
+ IMAPC_CAPABILITY_UNSELECT = 0x400,
+ IMAPC_CAPABILITY_ESEARCH = 0x800,
+ IMAPC_CAPABILITY_WITHIN = 0x1000,
+ IMAPC_CAPABILITY_QUOTA = 0x2000,
+ IMAPC_CAPABILITY_ID = 0x4000,
+ IMAPC_CAPABILITY_SAVEDATE = 0x8000,
+
+ IMAPC_CAPABILITY_IMAP4REV1 = 0x40000000
+};
+struct imapc_capability_name {
+ const char *name;
+ enum imapc_capability capability;
+};
+extern const struct imapc_capability_name imapc_capability_names[];
+
+enum imapc_command_flags {
+ /* The command changes the selected mailbox (SELECT, EXAMINE) */
+ IMAPC_COMMAND_FLAG_SELECT = 0x01,
+ /* The command is sent to server before login (or is the login
+ command itself). Non-prelogin commands will be queued until login
+ is successful. */
+ IMAPC_COMMAND_FLAG_PRELOGIN = 0x02,
+ /* Allow command to be automatically retried if disconnected before it
+ finishes. */
+ IMAPC_COMMAND_FLAG_RETRIABLE = 0x04,
+ /* This is the LOGOUT command. Use a small timeout for it. */
+ IMAPC_COMMAND_FLAG_LOGOUT = 0x08,
+ /* Command is being resent after a reconnection. */
+ IMAPC_COMMAND_FLAG_RECONNECTED = 0x10
+};
+
+enum imapc_client_ssl_mode {
+ IMAPC_CLIENT_SSL_MODE_NONE,
+ IMAPC_CLIENT_SSL_MODE_IMMEDIATE,
+ IMAPC_CLIENT_SSL_MODE_STARTTLS
+};
+
+#define IMAPC_DEFAULT_CONNECT_TIMEOUT_MSECS (1000*30)
+#define IMAPC_DEFAULT_COMMAND_TIMEOUT_MSECS (1000*60*5)
+#define IMAPC_DEFAULT_MAX_LINE_LENGTH (SIZE_MAX)
+
+struct imapc_throttling_settings {
+ unsigned int init_msecs;
+ unsigned int max_msecs;
+ unsigned int shrink_min_msecs;
+};
+
+struct imapc_client_settings {
+ const char *host;
+ in_port_t port;
+
+ const char *master_user;
+ const char *username;
+ const char *password;
+ /* Space-separated list of SASL mechanisms to try (in the specified
+ order). The default is to use only LOGIN command or SASL PLAIN. */
+ const char *sasl_mechanisms;
+ bool use_proxyauth; /* Use Sun/Oracle PROXYAUTH command */
+ unsigned int max_idle_time;
+ /* If ID capability is advertised, send a unique "x-session-ext-id",
+ which begins with this prefix. */
+ const char *session_id_prefix;
+
+ const char *dns_client_socket_path;
+ const char *temp_path_prefix;
+ struct ssl_iostream_settings ssl_set;
+
+ enum imapc_client_ssl_mode ssl_mode;
+
+ const char *rawlog_dir;
+ bool debug;
+
+ /* Timeout for logging in. 0 = default. */
+ unsigned int connect_timeout_msecs;
+ /* Number of retries, -1 = infinity */
+ unsigned int connect_retry_count;
+ /* Interval between retries, must be > 0 if retries > 0 */
+ unsigned int connect_retry_interval_msecs;
+
+ /* Timeout for IMAP commands. Reset every time more data is being
+ sent or received. 0 = default. */
+ unsigned int cmd_timeout_msecs;
+
+ /* Maximum allowed line length (not including literals read as
+ streams). 0 = unlimited. */
+ size_t max_line_length;
+
+ struct imapc_throttling_settings throttle_set;
+};
+
+struct imapc_command_reply {
+ enum imapc_command_state state;
+ /* "[RESP TEXT]" produces key=RESP, value=TEXT.
+ "[RESP]" produces key=RESP, value=NULL
+ otherwise both are NULL */
+ const char *resp_text_key, *resp_text_value;
+ /* The full tagged reply, including [RESP TEXT]. */
+ const char *text_full;
+ /* Tagged reply text without [RESP TEXT] */
+ const char *text_without_resp;
+};
+
+struct imapc_arg_file {
+ /* file descriptor containing the value */
+ int fd;
+
+ /* parent_arg.list[list_idx] points to the IMAP_ARG_LITERAL_SIZE
+ argument */
+ const struct imap_arg *parent_arg;
+ unsigned int list_idx;
+};
+
+struct imapc_untagged_reply {
+ /* name of the untagged reply, e.g. EXISTS */
+ const char *name;
+ /* number at the beginning of the reply, or 0 if there wasn't any.
+ Set for EXISTS, EXPUNGE, etc. */
+ uint32_t num;
+ /* the rest of the reply can be read from these args. */
+ const struct imap_arg *args;
+ /* arguments whose contents are stored into files. only
+ "FETCH (BODY[" arguments can be here. */
+ const struct imapc_arg_file *file_args;
+ unsigned int file_args_count;
+
+ /* "* OK [RESP TEXT]" produces key=RESP, value=TEXT.
+ "* OK [RESP]" produces key=RESP, value=NULL
+ otherwise both are NULL */
+ const char *resp_text_key, *resp_text_value;
+
+ /* If this reply occurred while a mailbox was selected, this contains
+ the mailbox's untagged_context. */
+ void *untagged_box_context;
+};
+
+enum imapc_state_change_event {
+ IMAPC_STATE_CHANGE_AUTH_OK,
+ IMAPC_STATE_CHANGE_AUTH_FAILED,
+};
+
+/* Called when tagged reply is received for command. */
+typedef void imapc_command_callback_t(const struct imapc_command_reply *reply,
+ void *context);
+/* Called each time untagged input is received. */
+typedef void imapc_untagged_callback_t(const struct imapc_untagged_reply *reply,
+ void *context);
+typedef void imapc_state_change_callback_t(void *context,
+ enum imapc_state_change_event event,
+ const char *error);
+
+struct imapc_client *
+imapc_client_init(const struct imapc_client_settings *set,
+ struct event *event_parent);
+void imapc_client_disconnect(struct imapc_client *client);
+void imapc_client_deinit(struct imapc_client **client);
+
+/* Set login callback, must be set before calling other commands.
+ This is called only for the first login, not for any reconnects or if there
+ are multiple connections created. */
+void
+imapc_client_set_login_callback(struct imapc_client *client,
+ imapc_command_callback_t *callback, void *context);
+/* Explicitly login to server (also done automatically). */
+void imapc_client_login(struct imapc_client *client);
+/* Send a LOGOUT and wait for disconnection. */
+void imapc_client_logout(struct imapc_client *client);
+
+struct imapc_command *
+imapc_client_cmd(struct imapc_client *client,
+ imapc_command_callback_t *callback, void *context);
+void imapc_command_set_flags(struct imapc_command *cmd,
+ enum imapc_command_flags flags);
+bool imapc_command_connection_is_selected(struct imapc_command *cmd);
+void imapc_command_send(struct imapc_command *cmd, const char *cmd_str);
+void imapc_command_sendf(struct imapc_command *cmd, const char *cmd_fmt, ...)
+ ATTR_FORMAT(2, 3);
+void imapc_command_sendvf(struct imapc_command *cmd,
+ const char *cmd_fmt, va_list args) ATTR_FORMAT(2, 0);
+const char *imapc_command_get_tag(struct imapc_command *cmd);
+void imapc_command_abort(struct imapc_command **cmd);
+
+void imapc_client_register_untagged(struct imapc_client *client,
+ imapc_untagged_callback_t *callback,
+ void *context);
+
+void imapc_client_run(struct imapc_client *client);
+void imapc_client_stop(struct imapc_client *client);
+bool imapc_client_is_running(struct imapc_client *client);
+
+struct imapc_client_mailbox *
+imapc_client_mailbox_open(struct imapc_client *client,
+ void *untagged_box_context);
+void imapc_client_mailbox_set_reopen_cb(struct imapc_client_mailbox *box,
+ void (*callback)(void *context),
+ void *context);
+void imapc_client_mailbox_close(struct imapc_client_mailbox **box);
+bool imapc_client_mailbox_can_reconnect(struct imapc_client_mailbox *box);
+void imapc_client_mailbox_reconnect(struct imapc_client_mailbox *box,
+ const char *errmsg);
+struct imapc_command *
+imapc_client_mailbox_cmd(struct imapc_client_mailbox *box,
+ imapc_command_callback_t *callback, void *context);
+struct imapc_msgmap *
+imapc_client_mailbox_get_msgmap(struct imapc_client_mailbox *box);
+
+void imapc_client_mailbox_idle(struct imapc_client_mailbox *box);
+bool imapc_client_mailbox_is_opened(struct imapc_client_mailbox *box);
+
+int imapc_client_get_capabilities(struct imapc_client *client,
+ enum imapc_capability *capabilities_r);
+
+int imapc_client_create_temp_fd(struct imapc_client *client,
+ const char **path_r);
+
+void imapc_client_register_state_change_callback(struct imapc_client *client,
+ imapc_state_change_callback_t *cb,
+ void *context);
+
+#endif
diff --git a/src/lib-imap-client/imapc-connection.c b/src/lib-imap-client/imapc-connection.c
new file mode 100644
index 0000000..5730f94
--- /dev/null
+++ b/src/lib-imap-client/imapc-connection.c
@@ -0,0 +1,2513 @@
+/* Copyright (c) 2011-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "ioloop.h"
+#include "net.h"
+#include "istream.h"
+#include "ostream.h"
+#include "base64.h"
+#include "write-full.h"
+#include "str.h"
+#include "time-util.h"
+#include "dns-lookup.h"
+#include "dsasl-client.h"
+#include "iostream-rawlog.h"
+#include "iostream-ssl.h"
+#include "imap-quote.h"
+#include "imap-util.h"
+#include "imap-parser.h"
+#include "imapc-client-private.h"
+#include "imapc-connection.h"
+
+#include <unistd.h>
+#include <ctype.h>
+
+#define IMAPC_COMMAND_STATE_AUTHENTICATE_CONTINUE 10000
+#define IMAPC_MAX_INLINE_LITERAL_SIZE (1024*32)
+/* If LOGOUT reply takes longer than this, disconnect. */
+#define IMAPC_LOGOUT_TIMEOUT_MSECS 5000
+
+enum imapc_input_state {
+ IMAPC_INPUT_STATE_NONE = 0,
+ IMAPC_INPUT_STATE_PLUS,
+ IMAPC_INPUT_STATE_UNTAGGED,
+ IMAPC_INPUT_STATE_UNTAGGED_NUM,
+ IMAPC_INPUT_STATE_TAGGED
+};
+
+struct imapc_command_stream {
+ unsigned int pos;
+ uoff_t size;
+ struct istream *input;
+};
+
+struct imapc_command {
+ pool_t pool;
+ buffer_t *data;
+ unsigned int send_pos;
+ unsigned int tag;
+
+ enum imapc_command_flags flags;
+ struct imapc_connection *conn;
+ /* If non-NULL, points to the mailbox where this command should be
+ executed */
+ struct imapc_client_mailbox *box;
+
+ ARRAY(struct imapc_command_stream) streams;
+
+ imapc_command_callback_t *callback;
+ void *context;
+
+ /* This is the AUTHENTICATE command */
+ bool authenticate:1;
+ /* This is the IDLE command */
+ bool idle:1;
+ /* Waiting for '+' literal reply before we can continue */
+ bool wait_for_literal:1;
+ /* Command is fully sent to server */
+ bool sent:1;
+};
+ARRAY_DEFINE_TYPE(imapc_command, struct imapc_command *);
+
+struct imapc_connection_literal {
+ char *temp_path;
+ int fd;
+ uoff_t bytes_left;
+
+ const struct imap_arg *parent_arg;
+ unsigned int list_idx;
+};
+
+struct imapc_connection {
+ struct imapc_client *client;
+ char *name;
+ int refcount;
+
+ int fd;
+ struct io *io;
+ struct istream *input, *raw_input;
+ struct ostream *output, *raw_output;
+ struct imap_parser *parser;
+ struct timeout *to;
+ struct timeout *to_output;
+ struct dns_lookup *dns_lookup;
+ struct dsasl_client *sasl_client;
+
+ struct ssl_iostream *ssl_iostream;
+
+ int (*input_callback)(struct imapc_connection *conn);
+ enum imapc_input_state input_state;
+ unsigned int cur_tag;
+ uint32_t cur_num;
+ struct timeval last_connect;
+ unsigned int reconnect_count;
+
+ struct imapc_client_mailbox *selecting_box, *selected_box;
+ enum imapc_connection_state state;
+ char *disconnect_reason;
+
+ enum imapc_capability capabilities;
+ char **capabilities_list;
+
+ imapc_command_callback_t *login_callback;
+ void *login_context;
+
+ /* commands pending in queue to be sent */
+ ARRAY_TYPE(imapc_command) cmd_send_queue;
+ /* commands that have been sent, waiting for their tagged reply */
+ ARRAY_TYPE(imapc_command) cmd_wait_list;
+ /* commands that were already sent, but were aborted since (due to
+ unselecting mailbox). */
+ ARRAY_TYPE(seq_range) aborted_cmd_tags;
+ unsigned int reconnect_command_count;
+
+ unsigned int ips_count, prev_connect_idx;
+ struct ip_addr *ips;
+
+ struct imapc_connection_literal literal;
+ ARRAY(struct imapc_arg_file) literal_files;
+
+ unsigned int throttle_msecs;
+ unsigned int throttle_shrink_msecs;
+ unsigned int last_successful_throttle_msecs;
+ bool throttle_pending;
+ struct timeval throttle_end_timeval;
+ struct timeout *to_throttle, *to_throttle_shrink;
+
+ bool reconnecting:1;
+ bool reconnect_waiting:1;
+ bool reconnect_ok:1;
+ bool idling:1;
+ bool idle_stopping:1;
+ bool idle_plus_waiting:1;
+ bool select_waiting_reply:1;
+};
+
+static void imapc_connection_capability_cb(const struct imapc_command_reply *reply,
+ void *context);
+static int imapc_connection_output(struct imapc_connection *conn);
+static int imapc_connection_ssl_init(struct imapc_connection *conn);
+static void imapc_command_free(struct imapc_command *cmd);
+static void imapc_command_send_more(struct imapc_connection *conn);
+static void
+imapc_login_callback(struct imapc_connection *conn,
+ const struct imapc_command_reply *reply);
+
+static void
+imapc_auth_ok(struct imapc_connection *conn)
+{
+ if (conn->client->set.debug)
+ i_debug("imapc(%s): Authenticated successfully", conn->name);
+
+ if (conn->client->state_change_callback == NULL)
+ return;
+
+ conn->client->state_change_callback(conn->client->state_change_context,
+ IMAPC_STATE_CHANGE_AUTH_OK, NULL);
+}
+
+static void
+imapc_auth_failed(struct imapc_connection *conn, const struct imapc_command_reply *_reply,
+ const char *error)
+{
+ struct imapc_command_reply reply = *_reply;
+
+ reply.text_without_resp = reply.text_full =
+ t_strdup_printf("Authentication failed: %s", error);
+ if (reply.state != IMAPC_COMMAND_STATE_DISCONNECTED) {
+ reply.state = IMAPC_COMMAND_STATE_AUTH_FAILED;
+ i_error("imapc(%s): %s", conn->name, reply.text_full);
+ }
+ imapc_login_callback(conn, &reply);
+
+ if (conn->client->state_change_callback == NULL)
+ return;
+
+ conn->client->state_change_callback(conn->client->state_change_context,
+ IMAPC_STATE_CHANGE_AUTH_FAILED,
+ error);
+}
+
+struct imapc_connection *
+imapc_connection_init(struct imapc_client *client,
+ imapc_command_callback_t *login_callback,
+ void *login_context)
+{
+ struct imapc_connection *conn;
+
+ conn = i_new(struct imapc_connection, 1);
+ conn->refcount = 1;
+ conn->client = client;
+ conn->login_callback = login_callback;
+ conn->login_context = login_context;
+ conn->fd = -1;
+ conn->name = i_strdup_printf("%s:%u", client->set.host,
+ client->set.port);
+ conn->literal.fd = -1;
+ conn->reconnect_ok = (client->set.connect_retry_count>0);
+ i_array_init(&conn->cmd_send_queue, 8);
+ i_array_init(&conn->cmd_wait_list, 32);
+ i_array_init(&conn->literal_files, 4);
+ i_array_init(&conn->aborted_cmd_tags, 8);
+
+ if (client->set.debug)
+ i_debug("imapc(%s): Created new connection", conn->name);
+
+ imapc_client_ref(client);
+ return conn;
+}
+
+static void imapc_connection_ref(struct imapc_connection *conn)
+{
+ i_assert(conn->refcount > 0);
+
+ conn->refcount++;
+}
+
+static void imapc_connection_unref(struct imapc_connection **_conn)
+{
+ struct imapc_connection *conn = *_conn;
+
+ i_assert(conn->refcount > 0);
+
+ *_conn = NULL;
+ if (--conn->refcount > 0)
+ return;
+
+ i_assert(conn->disconnect_reason == NULL);
+
+ if (conn->capabilities_list != NULL)
+ p_strsplit_free(default_pool, conn->capabilities_list);
+ array_free(&conn->cmd_send_queue);
+ array_free(&conn->cmd_wait_list);
+ array_free(&conn->literal_files);
+ array_free(&conn->aborted_cmd_tags);
+ imapc_client_unref(&conn->client);
+ i_free(conn->ips);
+ i_free(conn->name);
+ i_free(conn);
+}
+
+void imapc_connection_deinit(struct imapc_connection **_conn)
+{
+ imapc_connection_disconnect(*_conn);
+ imapc_connection_unref(_conn);
+}
+
+void imapc_connection_ioloop_changed(struct imapc_connection *conn)
+{
+ if (conn->io != NULL)
+ conn->io = io_loop_move_io(&conn->io);
+ if (conn->to != NULL)
+ conn->to = io_loop_move_timeout(&conn->to);
+ if (conn->to_throttle != NULL)
+ conn->to_throttle = io_loop_move_timeout(&conn->to_throttle);
+ if (conn->to_throttle_shrink != NULL)
+ conn->to_throttle_shrink = io_loop_move_timeout(&conn->to_throttle_shrink);
+ if (conn->output != NULL)
+ o_stream_switch_ioloop(conn->output);
+ if (conn->dns_lookup != NULL)
+ dns_lookup_switch_ioloop(conn->dns_lookup);
+
+ if (conn->client->ioloop == NULL && conn->to_output != NULL) {
+ /* we're only once moving the to_output to the main ioloop,
+ since timeout moves currently also reset the timeout.
+ (the rest of the times this is a no-op) */
+ conn->to_output = io_loop_move_timeout(&conn->to_output);
+ }
+}
+
+static const char *imapc_command_get_readable(struct imapc_command *cmd)
+{
+ string_t *str = t_str_new(256);
+ const unsigned char *data = cmd->data->data;
+ unsigned int i;
+
+ for (i = 0; i < cmd->data->used; i++) {
+ if (data[i] != '\r' && data[i] != '\n')
+ str_append_c(str, data[i]);
+ }
+ return str_c(str);
+}
+
+static void
+imapc_connection_abort_commands_array(ARRAY_TYPE(imapc_command) *cmd_array,
+ ARRAY_TYPE(imapc_command) *dest_array,
+ struct imapc_client_mailbox *only_box,
+ bool keep_retriable)
+{
+ struct imapc_command *cmd;
+ unsigned int i;
+
+ for (i = 0; i < array_count(cmd_array); ) {
+ cmd = array_idx_elem(cmd_array, i);
+
+ if (cmd->box != only_box && only_box != NULL)
+ i++;
+ else if (keep_retriable &&
+ (cmd->flags & IMAPC_COMMAND_FLAG_RETRIABLE) != 0) {
+ cmd->send_pos = 0;
+ cmd->wait_for_literal = 0;
+ cmd->flags |= IMAPC_COMMAND_FLAG_RECONNECTED;
+ i++;
+ } else {
+ array_delete(cmd_array, i, 1);
+ array_push_back(dest_array, &cmd);
+ }
+ }
+}
+
+void imapc_connection_abort_commands(struct imapc_connection *conn,
+ struct imapc_client_mailbox *only_box,
+ bool keep_retriable)
+{
+ struct imapc_command *cmd;
+ ARRAY_TYPE(imapc_command) tmp_array;
+ struct imapc_command_reply reply;
+
+ t_array_init(&tmp_array, 8);
+ imapc_connection_abort_commands_array(&conn->cmd_wait_list, &tmp_array,
+ only_box, keep_retriable);
+ imapc_connection_abort_commands_array(&conn->cmd_send_queue, &tmp_array,
+ only_box, keep_retriable);
+
+ if (array_count(&conn->cmd_wait_list) > 0 && only_box == NULL) {
+ /* need to move all the waiting commands to send queue */
+ array_append_array(&conn->cmd_wait_list,
+ &conn->cmd_send_queue);
+ array_clear(&conn->cmd_send_queue);
+ array_append_array(&conn->cmd_send_queue,
+ &conn->cmd_wait_list);
+ array_clear(&conn->cmd_wait_list);
+ }
+
+ /* abort the commands. we'll do it here later so that if the
+ callback recurses us back here we don't crash */
+ i_zero(&reply);
+ reply.state = IMAPC_COMMAND_STATE_DISCONNECTED;
+ if (only_box != NULL) {
+ reply.text_without_resp = reply.text_full =
+ "Unselecting mailbox";
+ } else {
+ reply.text_without_resp = reply.text_full =
+ "Disconnected from server";
+ }
+ array_foreach_elem(&tmp_array, cmd) {
+ if (cmd->sent && conn->state == IMAPC_CONNECTION_STATE_DONE) {
+ /* We're not disconnected, so the reply will still
+ come. Remember that it needs to be ignored. */
+ seq_range_array_add(&conn->aborted_cmd_tags, cmd->tag);
+ }
+ cmd->callback(&reply, cmd->context);
+ imapc_command_free(cmd);
+ }
+ if (array_count(&conn->cmd_wait_list) == 0)
+ timeout_remove(&conn->to);
+}
+
+static void
+imapc_login_callback(struct imapc_connection *conn,
+ const struct imapc_command_reply *reply)
+{
+ if (conn->login_callback != NULL)
+ conn->login_callback(reply, conn->login_context);
+}
+
+static void imapc_connection_set_state(struct imapc_connection *conn,
+ enum imapc_connection_state state)
+{
+ struct imapc_command_reply reply;
+
+ conn->state = state;
+
+ switch (state) {
+ case IMAPC_CONNECTION_STATE_DISCONNECTED:
+ i_zero(&reply);
+ reply.state = IMAPC_COMMAND_STATE_DISCONNECTED;
+ reply.text_full = "Disconnected from server";
+ if (conn->disconnect_reason != NULL) {
+ reply.text_full = t_strdup_printf("%s: %s",
+ reply.text_full, conn->disconnect_reason);
+ i_free_and_null(conn->disconnect_reason);
+ }
+ reply.text_without_resp = reply.text_full;
+ if (!conn->reconnecting) {
+ imapc_login_callback(conn, &reply);
+ i_free(conn->ips);
+ conn->ips_count = 0;
+ }
+ array_clear(&conn->aborted_cmd_tags);
+ conn->idling = FALSE;
+ conn->idle_plus_waiting = FALSE;
+ conn->idle_stopping = FALSE;
+
+ conn->select_waiting_reply = FALSE;
+ conn->selecting_box = NULL;
+ conn->selected_box = NULL;
+ /* fall through */
+ case IMAPC_CONNECTION_STATE_DONE:
+ /* if we came from imapc_client_get_capabilities(), stop so
+ it can finish up and not just hang indefinitely. */
+ if (conn->client->stop_on_state_finish && !conn->reconnecting)
+ imapc_client_stop(conn->client);
+ break;
+ default:
+ break;
+ }
+}
+
+static void imapc_connection_lfiles_free(struct imapc_connection *conn)
+{
+ struct imapc_arg_file *lfile;
+
+ array_foreach_modifiable(&conn->literal_files, lfile) {
+ if (close(lfile->fd) < 0)
+ i_error("imapc: close(literal file) failed: %m");
+ }
+ array_clear(&conn->literal_files);
+}
+
+static void
+imapc_connection_literal_reset(struct imapc_connection_literal *literal)
+{
+ i_close_fd_path(&literal->fd, literal->temp_path);
+ i_free_and_null(literal->temp_path);
+
+ i_zero(literal);
+ literal->fd = -1;
+}
+
+void imapc_connection_disconnect_full(struct imapc_connection *conn,
+ bool reconnecting)
+{
+ /* timeout may be set also in disconnected state */
+ timeout_remove(&conn->to);
+ conn->reconnecting = reconnecting;
+
+ if (conn->state == IMAPC_CONNECTION_STATE_DISCONNECTED) {
+ i_assert(array_count(&conn->cmd_wait_list) == 0);
+ return;
+ }
+
+ if (conn->client->set.debug)
+ i_debug("imapc(%s): Disconnected", conn->name);
+
+ if (conn->dns_lookup != NULL)
+ dns_lookup_abort(&conn->dns_lookup);
+ imapc_connection_lfiles_free(conn);
+ imapc_connection_literal_reset(&conn->literal);
+ timeout_remove(&conn->to_output);
+ timeout_remove(&conn->to_throttle);
+ timeout_remove(&conn->to_throttle_shrink);
+ if (conn->parser != NULL)
+ imap_parser_unref(&conn->parser);
+ io_remove(&conn->io);
+ ssl_iostream_destroy(&conn->ssl_iostream);
+ if (conn->fd != -1) {
+ i_stream_destroy(&conn->input);
+ o_stream_destroy(&conn->output);
+ net_disconnect(conn->fd);
+ conn->fd = -1;
+ }
+
+ /* get capabilities again after reconnection. this is especially
+ important because post-login capabilities often do not contain AUTH=
+ capabilities. */
+ conn->capabilities = 0;
+ if (conn->capabilities_list != NULL) {
+ p_strsplit_free(default_pool, conn->capabilities_list);
+ conn->capabilities_list = NULL;
+ }
+
+ imapc_connection_set_state(conn, IMAPC_CONNECTION_STATE_DISCONNECTED);
+ imapc_connection_abort_commands(conn, NULL, reconnecting);
+
+ if (!reconnecting) {
+ imapc_client_try_stop(conn->client);
+ }
+}
+
+void imapc_connection_set_no_reconnect(struct imapc_connection *conn)
+{
+ conn->reconnect_ok = FALSE;
+}
+
+void imapc_connection_disconnect(struct imapc_connection *conn)
+{
+ imapc_connection_disconnect_full(conn, FALSE);
+}
+
+static void imapc_connection_set_disconnected(struct imapc_connection *conn)
+{
+ imapc_connection_set_state(conn, IMAPC_CONNECTION_STATE_DISCONNECTED);
+ imapc_connection_abort_commands(conn, NULL, FALSE);
+}
+
+static bool imapc_connection_can_reconnect(struct imapc_connection *conn)
+{
+ if (conn->client->logging_out)
+ return FALSE;
+ if (conn->client->set.connect_retry_count == 0 ||
+ (conn->client->set.connect_retry_count < UINT_MAX &&
+ conn->reconnect_count >= conn->client->set.connect_retry_count))
+ return FALSE;
+
+ if (conn->selected_box != NULL)
+ return imapc_client_mailbox_can_reconnect(conn->selected_box);
+ else {
+ return conn->reconnect_command_count == 0 &&
+ conn->reconnect_ok;
+ }
+}
+
+static void imapc_connection_reconnect(struct imapc_connection *conn)
+{
+ conn->reconnect_ok = FALSE;
+ conn->reconnect_waiting = FALSE;
+
+ if (conn->selected_box != NULL) {
+ i_assert(!conn->selected_box->reconnecting);
+ conn->selected_box->reconnecting = TRUE;
+ /* if we fail again, avoid reconnecting immediately. if the
+ server is broken we could just get into an infinitely
+ failing reconnection loop. */
+ conn->selected_box->reconnect_ok = FALSE;
+ }
+ imapc_connection_disconnect_full(conn, TRUE);
+ imapc_connection_connect(conn);
+}
+
+void imapc_connection_try_reconnect(struct imapc_connection *conn,
+ const char *errstr,
+ unsigned int delay_msecs,
+ bool connect_error)
+{
+ /* Try the next IP address only for connect() problems. */
+ if (conn->prev_connect_idx + 1 < conn->ips_count && connect_error) {
+ i_warning("imapc(%s): %s - trying the next IP", conn->name, errstr);
+ conn->reconnect_ok = TRUE;
+ imapc_connection_disconnect_full(conn, TRUE);
+ imapc_connection_connect(conn);
+ return;
+ }
+
+ if (!imapc_connection_can_reconnect(conn)) {
+ i_error("imapc(%s): %s - disconnecting", conn->name, errstr);
+ imapc_connection_disconnect(conn);
+ } else {
+ conn->reconnecting = TRUE;
+ i_warning("imapc(%s): %s - reconnecting (delay %u ms)", conn->name, errstr, delay_msecs);
+ if (delay_msecs == 0)
+ imapc_connection_reconnect(conn);
+ else {
+ imapc_connection_disconnect_full(conn, TRUE);
+ conn->to = timeout_add(delay_msecs, imapc_connection_reconnect, conn);
+ conn->reconnect_count++;
+ conn->reconnect_waiting = TRUE;
+ }
+ }
+}
+
+static void ATTR_FORMAT(2, 3)
+imapc_connection_input_error(struct imapc_connection *conn,
+ const char *fmt, ...)
+{
+ va_list va;
+
+ va_start(va, fmt);
+ i_error("imapc(%s): Server sent invalid input: %s",
+ conn->name, t_strdup_vprintf(fmt, va));
+ imapc_connection_disconnect(conn);
+ va_end(va);
+}
+
+static bool last_arg_is_fetch_body(const struct imap_arg *args,
+ const struct imap_arg **parent_arg_r,
+ unsigned int *idx_r)
+{
+ const struct imap_arg *list;
+ const char *name;
+ unsigned int count;
+
+ if (args[0].type == IMAP_ARG_ATOM &&
+ imap_arg_atom_equals(&args[1], "FETCH") &&
+ imap_arg_get_list_full(&args[2], &list, &count) && count >= 2 &&
+ list[count].type == IMAP_ARG_LITERAL_SIZE &&
+ imap_arg_get_atom(&list[count-1], &name) &&
+ strncasecmp(name, "BODY[", 5) == 0) {
+ *parent_arg_r = &args[2];
+ *idx_r = count;
+ return TRUE;
+ }
+ return FALSE;
+}
+
+static int
+imapc_connection_read_literal_init(struct imapc_connection *conn, uoff_t size,
+ const struct imap_arg *args)
+{
+ const char *path;
+ const struct imap_arg *parent_arg;
+ unsigned int idx;
+
+ i_assert(conn->literal.fd == -1);
+
+ if (size <= IMAPC_MAX_INLINE_LITERAL_SIZE ||
+ !last_arg_is_fetch_body(args, &parent_arg, &idx)) {
+ /* read the literal directly into parser */
+ return 0;
+ }
+
+ conn->literal.fd = imapc_client_create_temp_fd(conn->client, &path);
+ if (conn->literal.fd == -1)
+ return -1;
+ conn->literal.temp_path = i_strdup(path);
+ conn->literal.bytes_left = size;
+ conn->literal.parent_arg = parent_arg;
+ conn->literal.list_idx = idx;
+ return 1;
+}
+
+static int imapc_connection_read_literal(struct imapc_connection *conn)
+{
+ struct imapc_arg_file *lfile;
+ const unsigned char *data;
+ size_t size;
+
+ if (conn->literal.bytes_left == 0)
+ return 1;
+
+ data = i_stream_get_data(conn->input, &size);
+ if (size > conn->literal.bytes_left)
+ size = conn->literal.bytes_left;
+ if (size > 0) {
+ if (write_full(conn->literal.fd, data, size) < 0) {
+ i_error("imapc(%s): write(%s) failed: %m",
+ conn->name, conn->literal.temp_path);
+ imapc_connection_disconnect(conn);
+ return -1;
+ }
+ i_stream_skip(conn->input, size);
+ conn->literal.bytes_left -= size;
+ }
+ if (conn->literal.bytes_left > 0)
+ return 0;
+
+ /* finished */
+ lfile = array_append_space(&conn->literal_files);
+ lfile->fd = conn->literal.fd;
+ lfile->parent_arg = conn->literal.parent_arg;
+ lfile->list_idx = conn->literal.list_idx;
+
+ conn->literal.fd = -1;
+ imapc_connection_literal_reset(&conn->literal);
+ return 1;
+}
+
+static int
+imapc_connection_read_line_more(struct imapc_connection *conn,
+ const struct imap_arg **imap_args_r)
+{
+ uoff_t literal_size;
+ int ret;
+
+ if ((ret = imapc_connection_read_literal(conn)) <= 0)
+ return ret;
+
+ ret = imap_parser_read_args(conn->parser, 0,
+ IMAP_PARSE_FLAG_LITERAL_SIZE |
+ IMAP_PARSE_FLAG_ATOM_ALLCHARS |
+ IMAP_PARSE_FLAG_LITERAL8 |
+ IMAP_PARSE_FLAG_SERVER_TEXT, imap_args_r);
+ if (ret == -2) {
+ /* need more data */
+ return 0;
+ }
+ if (ret < 0) {
+ enum imap_parser_error parser_error;
+ const char *err_msg = imap_parser_get_error(conn->parser, &parser_error);
+ if (parser_error != IMAP_PARSE_ERROR_BAD_SYNTAX)
+ imapc_connection_input_error(conn, "Error parsing input: %s", err_msg);
+ else
+ i_error("Error parsing input: %s", err_msg);
+ return -1;
+ }
+
+ if (imap_parser_get_literal_size(conn->parser, &literal_size)) {
+ if (imapc_connection_read_literal_init(conn, literal_size,
+ *imap_args_r) <= 0) {
+ imap_parser_read_last_literal(conn->parser);
+ return 2;
+ }
+ return imapc_connection_read_line_more(conn, imap_args_r);
+ }
+ return 1;
+}
+
+static int
+imapc_connection_read_line(struct imapc_connection *conn,
+ const struct imap_arg **imap_args_r)
+{
+ const unsigned char *data;
+ size_t size;
+ int ret;
+
+ while ((ret = imapc_connection_read_line_more(conn, imap_args_r)) == 2)
+ ;
+
+ if (ret > 0) {
+ data = i_stream_get_data(conn->input, &size);
+ if (size >= 2 && data[0] == '\r' && data[1] == '\n')
+ i_stream_skip(conn->input, 2);
+ else if (size >= 1 && data[0] == '\n')
+ i_stream_skip(conn->input, 1);
+ else
+ i_panic("imapc: Missing LF from input line");
+ } else if (ret < 0) {
+ data = i_stream_get_data(conn->input, &size);
+ unsigned char *lf = memchr(data, '\n', size);
+ if (lf != NULL)
+ i_stream_skip(conn->input, (lf - data) + 1);
+ }
+ return ret;
+}
+
+static int
+imapc_connection_parse_capability(struct imapc_connection *conn,
+ const char *value)
+{
+ const char *const *tmp;
+ unsigned int i;
+
+ if (conn->client->set.debug) {
+ i_debug("imapc(%s): Server capabilities: %s",
+ conn->name, value);
+ }
+
+ conn->capabilities = 0;
+ if (conn->capabilities_list != NULL)
+ p_strsplit_free(default_pool, conn->capabilities_list);
+ conn->capabilities_list = p_strsplit(default_pool, value, " ");
+
+ for (tmp = t_strsplit(value, " "); *tmp != NULL; tmp++) {
+ for (i = 0; imapc_capability_names[i].name != NULL; i++) {
+ const struct imapc_capability_name *cap =
+ &imapc_capability_names[i];
+
+ if (strcasecmp(*tmp, cap->name) == 0) {
+ conn->capabilities |= cap->capability;
+ break;
+ }
+ }
+ }
+
+ if ((conn->capabilities & IMAPC_CAPABILITY_IMAP4REV1) == 0) {
+ imapc_connection_input_error(conn,
+ "CAPABILITY list is missing IMAP4REV1");
+ return -1;
+ }
+ return 0;
+}
+
+static int
+imapc_connection_handle_resp_text_code(struct imapc_connection *conn,
+ const char *key, const char *value)
+{
+ if (strcasecmp(key, "CAPABILITY") == 0) {
+ if (imapc_connection_parse_capability(conn, value) < 0)
+ return -1;
+ }
+ if (strcasecmp(key, "CLOSED") == 0) {
+ /* QRESYNC: SELECTing another mailbox */
+ if (conn->selecting_box != NULL) {
+ conn->selected_box = conn->selecting_box;
+ conn->selecting_box = NULL;
+ }
+ }
+ return 0;
+}
+
+static int
+imapc_connection_handle_resp_text(struct imapc_connection *conn,
+ const char *text,
+ const char **key_r, const char **value_r)
+{
+ const char *p, *value;
+
+ i_assert(text[0] == '[');
+
+ p = strchr(text, ']');
+ if (p == NULL) {
+ imapc_connection_input_error(conn, "Missing ']' in resp-text");
+ return -1;
+ }
+ text = t_strdup_until(text + 1, p);
+ value = strchr(text, ' ');
+ if (value != NULL) {
+ *key_r = t_strdup_until(text, value);
+ *value_r = value + 1;
+ } else {
+ *key_r = text;
+ *value_r = "";
+ }
+ return imapc_connection_handle_resp_text_code(conn, *key_r, *value_r);
+}
+
+static int
+imapc_connection_handle_imap_resp_text(struct imapc_connection *conn,
+ const struct imap_arg *args,
+ const char **key_r, const char **value_r)
+{
+ const char *text;
+
+ if (args->type != IMAP_ARG_ATOM)
+ return 0;
+
+ text = imap_args_to_str(args);
+ if (*text != '[') {
+ if (*text == '\0') {
+ imapc_connection_input_error(conn,
+ "Missing text in resp-text");
+ return -1;
+ }
+ return 0;
+ }
+ return imapc_connection_handle_resp_text(conn, text, key_r, value_r);
+}
+
+static bool need_literal(const char *str)
+{
+ unsigned int i;
+
+ for (i = 0; str[i] != '\0'; i++) {
+ unsigned char c = str[i];
+
+ if ((c & 0x80) != 0 || c == '\r' || c == '\n')
+ return TRUE;
+ }
+ return FALSE;
+}
+
+static void imapc_connection_input_reset(struct imapc_connection *conn)
+{
+ conn->input_state = IMAPC_INPUT_STATE_NONE;
+ conn->cur_tag = 0;
+ conn->cur_num = 0;
+ if (conn->parser != NULL)
+ imap_parser_reset(conn->parser);
+ imapc_connection_lfiles_free(conn);
+}
+
+static void
+imapc_connection_auth_finish(struct imapc_connection *conn,
+ const struct imapc_command_reply *reply)
+{
+ if (reply->state != IMAPC_COMMAND_STATE_OK) {
+ imapc_auth_failed(conn, reply, reply->text_full);
+ imapc_connection_disconnect(conn);
+ return;
+ }
+
+ imapc_auth_ok(conn);
+
+ i_assert(array_count(&conn->cmd_wait_list) == 0);
+ timeout_remove(&conn->to);
+ imapc_connection_set_state(conn, IMAPC_CONNECTION_STATE_DONE);
+ imapc_login_callback(conn, reply);
+
+ imapc_command_send_more(conn);
+}
+
+static void imapc_connection_login_cb(const struct imapc_command_reply *reply,
+ void *context)
+{
+ struct imapc_connection *conn = context;
+
+ imapc_connection_auth_finish(conn, reply);
+}
+
+static void
+imapc_connection_proxyauth_login_cb(const struct imapc_command_reply *reply,
+ void *context)
+{
+ struct imapc_connection *conn = context;
+ const struct imapc_client_settings *set = &conn->client->set;
+ struct imapc_command *cmd;
+
+ if (reply->state == IMAPC_COMMAND_STATE_OK) {
+ cmd = imapc_connection_cmd(conn, imapc_connection_login_cb,
+ conn);
+ imapc_command_set_flags(cmd, IMAPC_COMMAND_FLAG_PRELOGIN);
+ imapc_command_sendf(cmd, "PROXYAUTH %s", set->username);
+ imapc_command_send_more(conn);
+ } else {
+ imapc_connection_auth_finish(conn, reply);
+ }
+}
+
+static void
+imapc_connection_authenticate_cb(const struct imapc_command_reply *reply,
+ void *context)
+{
+ struct imapc_connection *conn = context;
+ const unsigned char *sasl_output;
+ size_t input_len, sasl_output_len;
+ buffer_t *buf;
+ const char *error;
+
+ if ((int)reply->state != IMAPC_COMMAND_STATE_AUTHENTICATE_CONTINUE) {
+ dsasl_client_free(&conn->sasl_client);
+ imapc_connection_auth_finish(conn, reply);
+ return;
+ }
+
+ input_len = strlen(reply->text_full);
+ buf = t_buffer_create(MAX_BASE64_DECODED_SIZE(input_len));
+ if (base64_decode(reply->text_full, input_len, NULL, buf) < 0) {
+ imapc_auth_failed(conn, reply,
+ t_strdup_printf("Server sent non-base64 input for AUTHENTICATE: %s",
+ reply->text_full));
+ } else if (dsasl_client_input(conn->sasl_client, buf->data, buf->used, &error) < 0) {
+ imapc_auth_failed(conn, reply, error);
+ } else if (dsasl_client_output(conn->sasl_client, &sasl_output,
+ &sasl_output_len, &error) < 0) {
+ imapc_auth_failed(conn, reply, error);
+ } else {
+ string_t *imap_output =
+ t_str_new(MAX_BASE64_ENCODED_SIZE(sasl_output_len)+2);
+ base64_encode(sasl_output, sasl_output_len, imap_output);
+ str_append(imap_output, "\r\n");
+ o_stream_nsend(conn->output, str_data(imap_output),
+ str_len(imap_output));
+ return;
+ }
+ imapc_connection_disconnect(conn);
+}
+
+static bool imapc_connection_have_auth(struct imapc_connection *conn,
+ const char *mech_name)
+{
+ char *const *capa;
+
+ for (capa = conn->capabilities_list; *capa != NULL; capa++) {
+ if (strncasecmp(*capa, "AUTH=", 5) == 0 &&
+ strcasecmp((*capa)+5, mech_name) == 0)
+ return TRUE;
+ }
+ return FALSE;
+}
+
+static int
+imapc_connection_get_sasl_mech(struct imapc_connection *conn,
+ const struct dsasl_client_mech **mech_r,
+ const char **error_r)
+{
+ const struct imapc_client_settings *set = &conn->client->set;
+ const char *const *mechanisms =
+ t_strsplit_spaces(set->sasl_mechanisms, ", ");
+
+ /* find one of the specified SASL mechanisms */
+ for (; *mechanisms != NULL; mechanisms++) {
+ if (imapc_connection_have_auth(conn, *mechanisms)) {
+ *mech_r = dsasl_client_mech_find(*mechanisms);
+ if (*mech_r != NULL)
+ return 0;
+
+ *error_r = t_strdup_printf(
+ "Support for SASL method '%s' is missing", *mechanisms);
+ return -1;
+ }
+ }
+ *error_r = t_strdup_printf("IMAP server doesn't support any of the requested SASL mechanisms: %s",
+ set->sasl_mechanisms);
+ return -1;
+}
+
+static void imapc_connection_authenticate(struct imapc_connection *conn)
+{
+ const struct imapc_client_settings *set = &conn->client->set;
+ struct imapc_command *cmd;
+ struct dsasl_client_settings sasl_set;
+ const struct dsasl_client_mech *sasl_mech = NULL;
+ const char *error;
+
+ if (conn->client->set.debug) {
+ if (set->master_user == NULL) {
+ i_debug("imapc(%s): Authenticating as %s",
+ conn->name, set->username);
+ } else {
+ i_debug("imapc(%s): Authenticating as %s for user %s",
+ conn->name, set->master_user, set->username);
+ }
+ }
+
+ if (set->sasl_mechanisms != NULL && set->sasl_mechanisms[0] != '\0') {
+ if (imapc_connection_get_sasl_mech(conn, &sasl_mech, &error) < 0) {
+ struct imapc_command_reply reply;
+ i_zero(&reply);
+ reply.state = IMAPC_COMMAND_STATE_DISCONNECTED;
+ reply.text_full = "";
+ imapc_auth_failed(conn, &reply, error);
+ imapc_connection_disconnect(conn);
+ return;
+ }
+ }
+
+ if (set->use_proxyauth && set->master_user != NULL) {
+ /* We can use LOGIN command */
+ cmd = imapc_connection_cmd(conn, imapc_connection_proxyauth_login_cb,
+ conn);
+ imapc_command_set_flags(cmd, IMAPC_COMMAND_FLAG_PRELOGIN);
+ imapc_command_sendf(cmd, "LOGIN %s %s",
+ set->master_user, set->password);
+ return;
+ }
+ if (sasl_mech == NULL &&
+ ((set->master_user == NULL &&
+ !need_literal(set->username) && !need_literal(set->password)) ||
+ (conn->capabilities & IMAPC_CAPABILITY_AUTH_PLAIN) == 0)) {
+ /* We can use LOGIN command */
+ cmd = imapc_connection_cmd(conn, imapc_connection_login_cb,
+ conn);
+ imapc_command_set_flags(cmd, IMAPC_COMMAND_FLAG_PRELOGIN);
+ imapc_command_sendf(cmd, "LOGIN %s %s",
+ set->username, set->password);
+ return;
+ }
+
+ i_zero(&sasl_set);
+ if (set->master_user == NULL)
+ sasl_set.authid = set->username;
+ else {
+ sasl_set.authid = set->master_user;
+ sasl_set.authzid = set->username;
+ }
+ sasl_set.password = set->password;
+
+ if (sasl_mech == NULL)
+ sasl_mech = &dsasl_client_mech_plain;
+ conn->sasl_client = dsasl_client_new(sasl_mech, &sasl_set);
+
+ cmd = imapc_connection_cmd(conn, imapc_connection_authenticate_cb, conn);
+ cmd->authenticate = TRUE;
+ imapc_command_set_flags(cmd, IMAPC_COMMAND_FLAG_PRELOGIN);
+
+ if ((conn->capabilities & IMAPC_CAPABILITY_SASL_IR) != 0) {
+ const unsigned char *sasl_output;
+ size_t sasl_output_len;
+ string_t *sasl_output_base64;
+ const char *error;
+
+ if (dsasl_client_output(conn->sasl_client, &sasl_output,
+ &sasl_output_len, &error) < 0) {
+ i_error("imapc(%s): Failed to create initial SASL reply: %s",
+ conn->name, error);
+ imapc_connection_disconnect(conn);
+ return;
+ }
+ sasl_output_base64 = t_str_new(MAX_BASE64_ENCODED_SIZE(sasl_output_len));
+ base64_encode(sasl_output, sasl_output_len, sasl_output_base64);
+
+ imapc_command_sendf(cmd, "AUTHENTICATE %1s %1s",
+ dsasl_client_mech_get_name(sasl_mech),
+ str_c(sasl_output_base64));
+ } else {
+ imapc_command_sendf(cmd, "AUTHENTICATE %1s",
+ dsasl_client_mech_get_name(sasl_mech));
+ }
+}
+
+static void
+imapc_connection_starttls_cb(const struct imapc_command_reply *reply,
+ void *context)
+{
+ struct imapc_connection *conn = context;
+ struct imapc_command *cmd;
+
+ if (reply->state != IMAPC_COMMAND_STATE_OK) {
+ imapc_connection_input_error(conn, "STARTTLS failed: %s",
+ reply->text_full);
+ return;
+ }
+
+ if (imapc_connection_ssl_init(conn) < 0)
+ imapc_connection_disconnect(conn);
+ else {
+ /* get updated capabilities */
+ cmd = imapc_connection_cmd(conn, imapc_connection_capability_cb,
+ conn);
+ imapc_command_set_flags(cmd, IMAPC_COMMAND_FLAG_PRELOGIN);
+ imapc_command_send(cmd, "CAPABILITY");
+ }
+}
+
+static void
+imapc_connection_id_callback(const struct imapc_command_reply *reply ATTR_UNUSED,
+ void *context ATTR_UNUSED)
+{
+}
+
+static void imapc_connection_send_id(struct imapc_connection *conn)
+{
+ static unsigned int global_id_counter = 0;
+ struct imapc_command *cmd;
+
+ if ((conn->capabilities & IMAPC_CAPABILITY_ID) == 0 ||
+ conn->client->set.session_id_prefix == NULL)
+ return;
+
+ cmd = imapc_connection_cmd(conn, imapc_connection_id_callback, conn);
+ imapc_command_set_flags(cmd, IMAPC_COMMAND_FLAG_PRELOGIN);
+ imapc_command_send(cmd, t_strdup_printf(
+ "ID (\"name\" \"Dovecot\" \"x-session-ext-id\" \"%s-%u\")",
+ conn->client->set.session_id_prefix, ++global_id_counter));
+}
+
+static void imapc_connection_starttls(struct imapc_connection *conn)
+{
+ struct imapc_command *cmd;
+
+ if (conn->client->set.ssl_mode == IMAPC_CLIENT_SSL_MODE_STARTTLS &&
+ conn->ssl_iostream == NULL) {
+ if ((conn->capabilities & IMAPC_CAPABILITY_STARTTLS) == 0) {
+ i_error("imapc(%s): Requested STARTTLS, "
+ "but server doesn't support it",
+ conn->name);
+ imapc_connection_disconnect(conn);
+ return;
+ }
+ cmd = imapc_connection_cmd(conn, imapc_connection_starttls_cb,
+ conn);
+ imapc_command_set_flags(cmd, IMAPC_COMMAND_FLAG_PRELOGIN);
+ imapc_command_send(cmd, "STARTTLS");
+ return;
+ }
+ imapc_connection_send_id(conn);
+ imapc_connection_authenticate(conn);
+}
+
+static void
+imapc_connection_capability_cb(const struct imapc_command_reply *reply,
+ void *context)
+{
+ struct imapc_connection *conn = context;
+
+ if (reply->state != IMAPC_COMMAND_STATE_OK) {
+ imapc_connection_input_error(conn,
+ "Failed to get capabilities: %s", reply->text_full);
+ } else if (conn->capabilities == 0) {
+ imapc_connection_input_error(conn,
+ "Capabilities not returned by server");
+ } else {
+ imapc_connection_starttls(conn);
+ }
+}
+
+static int imapc_connection_input_banner(struct imapc_connection *conn)
+{
+ const struct imap_arg *imap_args;
+ const char *key, *value;
+ struct imapc_command *cmd;
+ int ret;
+
+ if ((ret = imapc_connection_read_line(conn, &imap_args)) <= 0)
+ return ret;
+ /* we already verified that the banner beigns with OK */
+ i_assert(imap_arg_atom_equals(imap_args, "OK"));
+ imap_args++;
+
+ if (imapc_connection_handle_imap_resp_text(conn, imap_args,
+ &key, &value) < 0)
+ return -1;
+ imapc_connection_set_state(conn, IMAPC_CONNECTION_STATE_AUTHENTICATING);
+
+ if (conn->capabilities == 0) {
+ /* capabilities weren't sent in the banner. ask for them. */
+ cmd = imapc_connection_cmd(conn, imapc_connection_capability_cb,
+ conn);
+ imapc_command_set_flags(cmd, IMAPC_COMMAND_FLAG_PRELOGIN);
+ imapc_command_send(cmd, "CAPABILITY");
+ } else {
+ imapc_connection_starttls(conn);
+ }
+ conn->input_callback = NULL;
+ imapc_connection_input_reset(conn);
+ return 1;
+}
+
+static int imapc_connection_input_untagged(struct imapc_connection *conn)
+{
+ const struct imap_arg *imap_args;
+ const unsigned char *data;
+ size_t size;
+ const char *name, *value;
+ struct imap_parser *parser;
+ struct imapc_untagged_reply reply;
+ int ret;
+
+ if (conn->state == IMAPC_CONNECTION_STATE_CONNECTING) {
+ /* input banner */
+ data = i_stream_get_data(conn->input, &size);
+ if (size < 3 && memchr(data, '\n', size) == NULL)
+ return 0;
+ if (i_memcasecmp(data, "OK ", 3) != 0) {
+ imapc_connection_input_error(conn,
+ "Banner doesn't begin with OK: %s",
+ t_strcut(t_strndup(data, size), '\n'));
+ return -1;
+ }
+ conn->input_callback = imapc_connection_input_banner;
+ return 1;
+ }
+
+ if ((ret = imapc_connection_read_line(conn, &imap_args)) == 0)
+ return 0;
+ else if (ret < 0) {
+ imapc_connection_input_reset(conn);
+ return 1;
+ }
+ if (!imap_arg_get_atom(&imap_args[0], &name)) {
+ imapc_connection_input_error(conn, "Invalid untagged reply");
+ return -1;
+ }
+ imap_args++;
+
+ if (conn->input_state == IMAPC_INPUT_STATE_UNTAGGED &&
+ str_to_uint32(name, &conn->cur_num) == 0) {
+ /* <seq> <event> */
+ conn->input_state = IMAPC_INPUT_STATE_UNTAGGED_NUM;
+ if (!imap_arg_get_atom(&imap_args[0], &name)) {
+ imapc_connection_input_error(conn,
+ "Invalid untagged reply");
+ return -1;
+ }
+ imap_args++;
+ }
+ i_zero(&reply);
+
+ if (strcasecmp(name, "OK") == 0) {
+ if (imapc_connection_handle_imap_resp_text(conn, imap_args,
+ &reply.resp_text_key,
+ &reply.resp_text_value) < 0)
+ return -1;
+ } else if (strcasecmp(name, "CAPABILITY") == 0) {
+ value = imap_args_to_str(imap_args);
+ if (imapc_connection_parse_capability(conn, value) < 0)
+ return -1;
+ } else if (strcasecmp(name, "BYE") == 0) {
+ i_free(conn->disconnect_reason);
+ conn->disconnect_reason = i_strdup(imap_args_to_str(imap_args));
+ }
+
+ reply.name = name;
+ reply.num = conn->cur_num;
+ reply.args = imap_args;
+ reply.file_args = array_get(&conn->literal_files,
+ &reply.file_args_count);
+
+ if (conn->selected_box != NULL) {
+ reply.untagged_box_context =
+ conn->selected_box->untagged_box_context;
+ }
+
+ /* the callback may disconnect and destroy the parser */
+ parser = conn->parser;
+ imap_parser_ref(parser);
+ conn->client->untagged_callback(&reply, conn->client->untagged_context);
+ imap_parser_unref(&parser);
+ imapc_connection_input_reset(conn);
+ return 1;
+}
+
+static int imapc_connection_input_plus(struct imapc_connection *conn)
+{
+ struct imapc_command *const *cmds;
+ unsigned int cmds_count;
+ const char *line;
+
+ if ((line = i_stream_next_line(conn->input)) == NULL)
+ return 0;
+
+ cmds = array_get(&conn->cmd_send_queue, &cmds_count);
+ if (conn->idle_plus_waiting) {
+ /* "+ idling" reply for IDLE command */
+ conn->idle_plus_waiting = FALSE;
+ conn->idling = TRUE;
+ /* no timing out while IDLEing */
+ if (conn->to != NULL && !conn->idle_stopping)
+ timeout_remove(&conn->to);
+ } else if (cmds_count > 0 && cmds[0]->wait_for_literal) {
+ /* reply for literal */
+ cmds[0]->wait_for_literal = FALSE;
+ imapc_command_send_more(conn);
+ } else {
+ cmds = array_get(&conn->cmd_wait_list, &cmds_count);
+ if (cmds_count > 0 && cmds[0]->authenticate) {
+ /* continue AUTHENTICATE */
+ struct imapc_command_reply reply;
+
+ i_zero(&reply);
+ reply.state = (enum imapc_command_state)IMAPC_COMMAND_STATE_AUTHENTICATE_CONTINUE;
+ reply.text_full = line;
+ cmds[0]->callback(&reply, cmds[0]->context);
+ } else {
+ imapc_connection_input_error(conn, "Unexpected '+': %s", line);
+ return -1;
+ }
+ }
+
+ imapc_connection_input_reset(conn);
+ return 1;
+}
+
+static void
+imapc_connection_throttle_shrink_timeout(struct imapc_connection *conn)
+{
+ if (conn->throttle_msecs <= 1)
+ conn->throttle_msecs = 0;
+ else
+ conn->throttle_msecs = conn->throttle_msecs*3 / 4;
+
+ if (conn->throttle_shrink_msecs <= conn->client->set.throttle_set.shrink_min_msecs)
+ conn->throttle_shrink_msecs = 0;
+ else
+ conn->throttle_shrink_msecs = conn->throttle_shrink_msecs*3 / 4;
+
+ timeout_remove(&conn->to_throttle_shrink);
+ if (conn->throttle_shrink_msecs > 0) {
+ conn->to_throttle_shrink =
+ timeout_add(conn->throttle_shrink_msecs,
+ imapc_connection_throttle_shrink_timeout, conn);
+ }
+}
+
+static void
+imapc_connection_throttle(struct imapc_connection *conn,
+ const struct imapc_command_reply *reply)
+{
+ timeout_remove(&conn->to_throttle);
+
+ /* If GMail returns [THROTTLED], start slowing down commands.
+ Unfortunately this isn't a nice resp-text-code, but just
+ appended at the end of the line (although we kind of support
+ it as resp-text-code also in here if it's uppercased). */
+ if (strstr(reply->text_full, "[THROTTLED]") != NULL) {
+ if (conn->throttle_msecs == 0)
+ conn->throttle_msecs = conn->client->set.throttle_set.init_msecs;
+ else if (conn->throttle_msecs < conn->last_successful_throttle_msecs)
+ conn->throttle_msecs = conn->last_successful_throttle_msecs;
+ else {
+ conn->throttle_msecs *= 2;
+ if (conn->throttle_msecs > conn->client->set.throttle_set.max_msecs)
+ conn->throttle_msecs = conn->client->set.throttle_set.max_msecs;
+ }
+ if (conn->throttle_shrink_msecs == 0)
+ conn->throttle_shrink_msecs = conn->client->set.throttle_set.shrink_min_msecs;
+ else
+ conn->throttle_shrink_msecs *= 2;
+ if (conn->to_throttle_shrink != NULL)
+ timeout_reset(conn->to_throttle_shrink);
+ } else {
+ if (conn->throttle_shrink_msecs > 0 &&
+ conn->to_throttle_shrink == NULL) {
+ conn->to_throttle_shrink =
+ timeout_add(conn->throttle_shrink_msecs,
+ imapc_connection_throttle_shrink_timeout, conn);
+ }
+ conn->last_successful_throttle_msecs = conn->throttle_msecs;
+ }
+
+ if (conn->throttle_msecs > 0) {
+ conn->throttle_end_timeval = ioloop_timeval;
+ timeval_add_msecs(&conn->throttle_end_timeval,
+ conn->throttle_msecs);
+ conn->throttle_pending = TRUE;
+ }
+}
+
+static void
+imapc_command_reply_free(struct imapc_command *cmd,
+ const struct imapc_command_reply *reply)
+{
+ cmd->callback(reply, cmd->context);
+ imapc_command_free(cmd);
+}
+
+static int imapc_connection_input_tagged(struct imapc_connection *conn)
+{
+ struct imapc_command *const *cmds, *cmd = NULL;
+ unsigned int i, count;
+ char *line, *linep;
+ const char *p;
+ struct imapc_command_reply reply;
+
+ line = i_stream_next_line(conn->input);
+ if (line == NULL)
+ return 0;
+ /* make sure reply texts stays valid if input stream gets freed */
+ line = t_strdup_noconst(line);
+
+ i_zero(&reply);
+
+ linep = strchr(line, ' ');
+ if (linep == NULL)
+ reply.text_full = "";
+ else {
+ *linep = '\0';
+ reply.text_full = linep + 1;
+ }
+
+ if (strcasecmp(line, "ok") == 0)
+ reply.state = IMAPC_COMMAND_STATE_OK;
+ else if (strcasecmp(line, "no") == 0)
+ reply.state = IMAPC_COMMAND_STATE_NO;
+ else if (strcasecmp(line, "bad") == 0)
+ reply.state = IMAPC_COMMAND_STATE_BAD;
+ else {
+ imapc_connection_input_error(conn,
+ "Invalid state in tagged reply: %u %s %s",
+ conn->cur_tag, line, reply.text_full);
+ return -1;
+ }
+
+ if (reply.text_full[0] == '[') {
+ /* get resp-text */
+ if (imapc_connection_handle_resp_text(conn, reply.text_full,
+ &reply.resp_text_key,
+ &reply.resp_text_value) < 0)
+ return -1;
+
+ p = i_strchr_to_next(reply.text_full, ']');
+ i_assert(p != NULL);
+ reply.text_without_resp = p;
+ if (reply.text_without_resp[0] == ' ')
+ reply.text_without_resp++;
+ } else {
+ reply.text_without_resp = reply.text_full;
+ }
+ /* if we've pipelined multiple commands, handle [THROTTLED] reply
+ from only one of them */
+ if (!conn->throttle_pending)
+ imapc_connection_throttle(conn, &reply);
+
+ /* find the command. it's either the first command in send queue
+ (literal failed) or somewhere in wait list. */
+ cmds = array_get(&conn->cmd_send_queue, &count);
+ if (count > 0 && cmds[0]->tag == conn->cur_tag) {
+ cmd = cmds[0];
+ array_pop_front(&conn->cmd_send_queue);
+ } else {
+ cmds = array_get(&conn->cmd_wait_list, &count);
+ for (i = 0; i < count; i++) {
+ if (cmds[i]->tag == conn->cur_tag) {
+ cmd = cmds[i];
+ array_delete(&conn->cmd_wait_list, i, 1);
+ break;
+ }
+ }
+ }
+ if (array_count(&conn->cmd_wait_list) == 0 &&
+ array_count(&conn->cmd_send_queue) == 0 &&
+ conn->state == IMAPC_CONNECTION_STATE_DONE && conn->to != NULL)
+ timeout_remove(&conn->to);
+
+ if (cmd == NULL) {
+ if (seq_range_exists(&conn->aborted_cmd_tags, conn->cur_tag)) {
+ /* sent command was already aborted - ignore it */
+ seq_range_array_remove(&conn->aborted_cmd_tags,
+ conn->cur_tag);
+ imapc_connection_input_reset(conn);
+ return 1;
+ }
+ imapc_connection_input_error(conn,
+ "Unknown tag in a reply: %u %s %s",
+ conn->cur_tag, line, reply.text_full);
+ return -1;
+ }
+ if ((cmd->flags & IMAPC_COMMAND_FLAG_SELECT) != 0)
+ conn->select_waiting_reply = FALSE;
+
+ if (reply.state == IMAPC_COMMAND_STATE_BAD) {
+ i_error("imapc(%s): Command '%s' failed with BAD: %u %s",
+ conn->name, imapc_command_get_readable(cmd),
+ conn->cur_tag, reply.text_full);
+ imapc_connection_disconnect(conn);
+ }
+
+ if (reply.state == IMAPC_COMMAND_STATE_NO &&
+ (cmd->flags & IMAPC_COMMAND_FLAG_SELECT) != 0 &&
+ conn->selected_box != NULL) {
+ /* EXAMINE/SELECT failed: mailbox is no longer selected */
+ imapc_connection_unselect(conn->selected_box);
+ }
+
+ if (conn->reconnect_command_count > 0 &&
+ (cmd->flags & IMAPC_COMMAND_FLAG_RECONNECTED) != 0) {
+ i_assert(conn->reconnect_command_count > 0);
+ if (--conn->reconnect_command_count == 0) {
+ /* we've received replies for all the commands started
+ before reconnection. if we get disconnected now, we
+ can safely reconnect without worrying about infinite
+ reconnect loops. */
+ if (conn->selected_box != NULL)
+ conn->selected_box->reconnect_ok = TRUE;
+ }
+ }
+ if (conn->reconnect_command_count == 0) {
+ /* we've successfully received replies to some commands. */
+ conn->reconnect_ok = TRUE;
+ }
+ imapc_connection_input_reset(conn);
+ imapc_command_reply_free(cmd, &reply);
+ imapc_command_send_more(conn);
+ return 1;
+}
+
+static int imapc_connection_input_one(struct imapc_connection *conn)
+{
+ const char *tag;
+ int ret = -1;
+
+ if (conn->input_callback != NULL)
+ return conn->input_callback(conn);
+
+ switch (conn->input_state) {
+ case IMAPC_INPUT_STATE_NONE:
+ tag = imap_parser_read_word(conn->parser);
+ if (tag == NULL)
+ return 0;
+
+ if (strcmp(tag, "*") == 0) {
+ conn->input_state = IMAPC_INPUT_STATE_UNTAGGED;
+ conn->cur_num = 0;
+ ret = imapc_connection_input_untagged(conn);
+ } else if (strcmp(tag, "+") == 0) {
+ conn->input_state = IMAPC_INPUT_STATE_PLUS;
+ ret = imapc_connection_input_plus(conn);
+ } else {
+ conn->input_state = IMAPC_INPUT_STATE_TAGGED;
+ if (str_to_uint(tag, &conn->cur_tag) < 0 ||
+ conn->cur_tag == 0) {
+ imapc_connection_input_error(conn,
+ "Invalid command tag: %s", tag);
+ ret = -1;
+ } else {
+ ret = imapc_connection_input_tagged(conn);
+ }
+ }
+ break;
+ case IMAPC_INPUT_STATE_PLUS:
+ ret = imapc_connection_input_plus(conn);
+ break;
+ case IMAPC_INPUT_STATE_UNTAGGED:
+ case IMAPC_INPUT_STATE_UNTAGGED_NUM:
+ ret = imapc_connection_input_untagged(conn);
+ break;
+ case IMAPC_INPUT_STATE_TAGGED:
+ ret = imapc_connection_input_tagged(conn);
+ break;
+ }
+ return ret;
+}
+
+static void imapc_connection_input(struct imapc_connection *conn)
+{
+ const char *errstr;
+ string_t *str;
+ ssize_t ret = 0;
+
+ /* we need to read as much as we can with SSL streams to avoid
+ hanging */
+ imapc_connection_ref(conn);
+ while (conn->input != NULL && (ret = i_stream_read(conn->input)) > 0)
+ imapc_connection_input_pending(conn);
+
+ if (ret < 0 && conn->client->logging_out &&
+ conn->disconnect_reason != NULL) {
+ /* expected disconnection */
+ imapc_connection_disconnect(conn);
+ } else if (ret < 0) {
+ /* disconnected or buffer full */
+ str = t_str_new(128);
+ if (conn->disconnect_reason != NULL) {
+ str_printfa(str, "Server disconnected with message: %s",
+ conn->disconnect_reason);
+ } else if (ret == -2) {
+ str_printfa(str, "Server sent too large input "
+ "(buffer full at %zu)",
+ i_stream_get_data_size(conn->input));
+ } else if (conn->ssl_iostream == NULL) {
+ errstr = conn->input->stream_errno == 0 ? "EOF" :
+ i_stream_get_error(conn->input);
+ str_printfa(str, "Server disconnected unexpectedly: %s",
+ errstr);
+ } else {
+ errstr = ssl_iostream_get_last_error(conn->ssl_iostream);
+ if (errstr == NULL) {
+ errstr = conn->input->stream_errno == 0 ? "EOF" :
+ i_stream_get_error(conn->input);
+ }
+ str_printfa(str, "Server disconnected unexpectedly: %s",
+ errstr);
+ }
+ imapc_connection_try_reconnect(conn, str_c(str), 0, FALSE);
+ }
+ imapc_connection_unref(&conn);
+}
+
+static int imapc_connection_ssl_handshaked(const char **error_r, void *context)
+{
+ struct imapc_connection *conn = context;
+ const char *error;
+
+ if (ssl_iostream_check_cert_validity(conn->ssl_iostream,
+ conn->client->set.host, &error) == 0) {
+ if (conn->client->set.debug) {
+ i_debug("imapc(%s): SSL handshake successful",
+ conn->name);
+ }
+ return 0;
+ } else if (conn->client->set.ssl_set.allow_invalid_cert) {
+ if (conn->client->set.debug) {
+ i_debug("imapc(%s): SSL handshake successful, "
+ "ignoring invalid certificate: %s",
+ conn->name, error);
+ }
+ return 0;
+ } else {
+ *error_r = error;
+ return -1;
+ }
+}
+
+static int imapc_connection_ssl_init(struct imapc_connection *conn)
+{
+ const char *error;
+
+ if (conn->client->ssl_ctx == NULL) {
+ i_error("imapc(%s): No SSL context", conn->name);
+ return -1;
+ }
+
+ if (conn->client->set.debug)
+ i_debug("imapc(%s): Starting SSL handshake", conn->name);
+
+ if (conn->raw_input != conn->input) {
+ /* recreate rawlog after STARTTLS */
+ i_stream_ref(conn->raw_input);
+ o_stream_ref(conn->raw_output);
+ i_stream_destroy(&conn->input);
+ o_stream_destroy(&conn->output);
+ conn->input = conn->raw_input;
+ conn->output = conn->raw_output;
+ }
+
+ io_remove(&conn->io);
+ if (io_stream_create_ssl_client(conn->client->ssl_ctx,
+ conn->client->set.host,
+ &conn->client->set.ssl_set,
+ &conn->input, &conn->output,
+ &conn->ssl_iostream, &error) < 0) {
+ i_error("imapc(%s): Couldn't initialize SSL client: %s",
+ conn->name, error);
+ return -1;
+ }
+ conn->io = io_add_istream(conn->input, imapc_connection_input, conn);
+ ssl_iostream_set_handshake_callback(conn->ssl_iostream,
+ imapc_connection_ssl_handshaked,
+ conn);
+ if (ssl_iostream_handshake(conn->ssl_iostream) < 0) {
+ i_error("imapc(%s): SSL handshake failed: %s", conn->name,
+ ssl_iostream_get_last_error(conn->ssl_iostream));
+ return -1;
+ }
+
+ if (*conn->client->set.rawlog_dir != '\0') {
+ iostream_rawlog_create(conn->client->set.rawlog_dir,
+ &conn->input, &conn->output);
+ }
+
+ imap_parser_set_streams(conn->parser, conn->input, NULL);
+ return 0;
+}
+
+static int imapc_connection_connected(struct imapc_connection *conn)
+{
+ const struct ip_addr *ip = &conn->ips[conn->prev_connect_idx];
+ struct ip_addr local_ip;
+ in_port_t local_port;
+ int err;
+
+ i_assert(conn->io == NULL);
+
+ err = net_geterror(conn->fd);
+ if (err != 0) {
+ imapc_connection_try_reconnect(conn, t_strdup_printf(
+ "connect(%s, %u) failed: %s",
+ net_ip2addr(ip), conn->client->set.port,
+ strerror(err)), conn->client->set.connect_retry_interval_msecs, TRUE);
+ return -1;
+ }
+ if (net_getsockname(conn->fd, &local_ip, &local_port) < 0)
+ local_port = 0;
+ i_info("imapc(%s): Connected to %s:%u (local %s:%u)", conn->name,
+ net_ip2addr(ip), conn->client->set.port,
+ net_ip2addr(&local_ip), local_port);
+ conn->io = io_add(conn->fd, IO_READ, imapc_connection_input, conn);
+ o_stream_set_flush_callback(conn->output, imapc_connection_output,
+ conn);
+
+ if (conn->client->set.ssl_mode == IMAPC_CLIENT_SSL_MODE_IMMEDIATE) {
+ if (imapc_connection_ssl_init(conn) < 0)
+ imapc_connection_disconnect(conn);
+ }
+ return imapc_connection_output(conn);
+}
+
+static void imapc_connection_timeout(struct imapc_connection *conn)
+{
+ const struct ip_addr *ip = &conn->ips[conn->prev_connect_idx];
+ const char *errstr;
+ bool connect_error = FALSE;
+
+ switch (conn->state) {
+ case IMAPC_CONNECTION_STATE_CONNECTING:
+ errstr = t_strdup_printf("connect(%s, %u) timed out after %u seconds",
+ net_ip2addr(ip), conn->client->set.port,
+ conn->client->set.connect_timeout_msecs/1000);
+ connect_error = TRUE;
+ break;
+ case IMAPC_CONNECTION_STATE_AUTHENTICATING:
+ errstr = t_strdup_printf("Authentication timed out after %u seconds",
+ conn->client->set.connect_timeout_msecs/1000);
+ break;
+ default:
+ i_unreached();
+ }
+ imapc_connection_try_reconnect(conn, errstr, 0, connect_error);
+}
+
+static void
+imapc_noop_callback(const struct imapc_command_reply *reply ATTR_UNUSED,
+ void *context ATTR_UNUSED)
+{
+}
+
+static void
+imapc_reidle_callback(const struct imapc_command_reply *reply ATTR_UNUSED,
+ void *context)
+{
+ struct imapc_connection *conn = context;
+
+ imapc_connection_idle(conn);
+}
+
+static void imapc_connection_reset_idle(struct imapc_connection *conn)
+{
+ struct imapc_command *cmd;
+
+ if (conn->idling)
+ cmd = imapc_connection_cmd(conn, imapc_reidle_callback, conn);
+ else if (array_count(&conn->cmd_wait_list) == 0)
+ cmd = imapc_connection_cmd(conn, imapc_noop_callback, NULL);
+ else {
+ /* IMAP command reply is taking a long time */
+ return;
+ }
+ imapc_command_send(cmd, "NOOP");
+}
+
+static void imapc_connection_connect_next_ip(struct imapc_connection *conn)
+{
+ const struct ip_addr *ip = NULL;
+ unsigned int i;
+ int fd;
+
+ i_assert(conn->client->set.max_idle_time > 0);
+
+ for (i = 0; i<conn->ips_count;) {
+ conn->prev_connect_idx = (conn->prev_connect_idx+1) % conn->ips_count;
+ ip = &conn->ips[conn->prev_connect_idx];
+ fd = net_connect_ip(ip, conn->client->set.port, NULL);
+ if (fd != -1)
+ break;
+
+ /* failed to connect to one of the IPs immediately
+ (e.g. IPv6 address without connectivity). try all IPs
+ before failing completely. */
+ i_error("net_connect_ip(%s:%u) failed: %m",
+ net_ip2addr(ip), conn->client->set.port);
+ if (conn->prev_connect_idx+1 == conn->ips_count) {
+ imapc_connection_try_reconnect(conn, "No more IP address(es) to try",
+ conn->client->set.connect_retry_interval_msecs, TRUE);
+ return;
+ }
+ }
+
+ i_assert(ip != NULL);
+
+ conn->fd = fd;
+ conn->input = conn->raw_input =
+ i_stream_create_fd(fd, conn->client->set.max_line_length);
+ conn->output = conn->raw_output = o_stream_create_fd(fd, SIZE_MAX);
+ o_stream_set_no_error_handling(conn->output, TRUE);
+
+ if (*conn->client->set.rawlog_dir != '\0' &&
+ conn->client->set.ssl_mode != IMAPC_CLIENT_SSL_MODE_IMMEDIATE) {
+ iostream_rawlog_create(conn->client->set.rawlog_dir,
+ &conn->input, &conn->output);
+ }
+
+ o_stream_set_flush_pending(conn->output, TRUE);
+ o_stream_set_flush_callback(conn->output, imapc_connection_connected,
+ conn);
+ conn->parser = imap_parser_create(conn->input, NULL,
+ conn->client->set.max_line_length);
+ conn->to = timeout_add(conn->client->set.connect_timeout_msecs,
+ imapc_connection_timeout, conn);
+ conn->to_output = timeout_add(conn->client->set.max_idle_time*1000,
+ imapc_connection_reset_idle, conn);
+ if (conn->client->set.debug) {
+ i_debug("imapc(%s): Connecting to %s:%u", conn->name,
+ net_ip2addr(ip), conn->client->set.port);
+ }
+}
+
+static void
+imapc_connection_dns_callback(const struct dns_lookup_result *result,
+ struct imapc_connection *conn)
+{
+ conn->dns_lookup = NULL;
+
+ if (result->ret != 0) {
+ i_error("imapc(%s): dns_lookup(%s) failed: %s",
+ conn->name, conn->client->set.host, result->error);
+ imapc_connection_set_disconnected(conn);
+ return;
+ }
+
+ i_assert(result->ips_count > 0);
+ conn->ips_count = result->ips_count;
+ conn->ips = i_new(struct ip_addr, conn->ips_count);
+ memcpy(conn->ips, result->ips, sizeof(*conn->ips) * conn->ips_count);
+ conn->prev_connect_idx = conn->ips_count - 1;
+
+ imapc_connection_connect_next_ip(conn);
+}
+
+void imapc_connection_connect(struct imapc_connection *conn)
+{
+ struct dns_lookup_settings dns_set;
+ struct ip_addr ip, *ips;
+ unsigned int ips_count;
+ int ret;
+
+ if (conn->fd != -1 || conn->dns_lookup != NULL)
+ return;
+ if (conn->reconnect_waiting) {
+ /* wait for the reconnection delay to finish before
+ doing anything. */
+ return;
+ }
+
+ conn->reconnecting = FALSE;
+ /* if we get disconnected before we've finished all the pending
+ commands, don't reconnect */
+ conn->reconnect_command_count = array_count(&conn->cmd_wait_list) +
+ array_count(&conn->cmd_send_queue);
+
+ imapc_connection_input_reset(conn);
+ conn->last_connect = ioloop_timeval;
+
+ if (conn->client->set.debug) {
+ i_debug("imapc(%s): Looking up IP address "
+ "(reconnect_ok=%s, last_connect=%ld)", conn->name,
+ (conn->reconnect_ok ? "true" : "false"),
+ (long)conn->last_connect.tv_sec);
+ }
+
+ i_zero(&dns_set);
+ dns_set.dns_client_socket_path =
+ conn->client->set.dns_client_socket_path;
+ dns_set.timeout_msecs = conn->client->set.connect_timeout_msecs;
+ dns_set.event_parent = conn->client->event;
+
+ imapc_connection_set_state(conn, IMAPC_CONNECTION_STATE_CONNECTING);
+ if (conn->ips_count > 0) {
+ /* do nothing */
+ } else if (net_addr2ip(conn->client->set.host, &ip) == 0) {
+ conn->ips_count = 1;
+ conn->ips = i_new(struct ip_addr, conn->ips_count);
+ conn->ips[0] = ip;
+ } else if (*dns_set.dns_client_socket_path == '\0') {
+ ret = net_gethostbyname(conn->client->set.host,
+ &ips, &ips_count);
+ if (ret != 0) {
+ i_error("imapc(%s): net_gethostbyname(%s) failed: %s",
+ conn->name, conn->client->set.host,
+ net_gethosterror(ret));
+ imapc_connection_set_disconnected(conn);
+ return;
+ }
+ conn->ips_count = ips_count;
+ conn->ips = i_new(struct ip_addr, ips_count);
+ memcpy(conn->ips, ips, ips_count * sizeof(*ips));
+ } else {
+ (void)dns_lookup(conn->client->set.host, &dns_set,
+ imapc_connection_dns_callback, conn,
+ &conn->dns_lookup);
+ return;
+ }
+ imapc_connection_connect_next_ip(conn);
+}
+
+void imapc_connection_input_pending(struct imapc_connection *conn)
+{
+ int ret = 1;
+
+ if (conn->input == NULL)
+ return;
+
+ if (conn->to != NULL && !conn->idle_stopping)
+ timeout_reset(conn->to);
+
+ o_stream_cork(conn->output);
+ while (ret > 0 && conn->input != NULL) {
+ T_BEGIN {
+ ret = imapc_connection_input_one(conn);
+ } T_END;
+ }
+
+ if (conn->output != NULL)
+ o_stream_uncork(conn->output);
+}
+
+static struct imapc_command *
+imapc_command_begin(imapc_command_callback_t *callback, void *context)
+{
+ struct imapc_command *cmd;
+ pool_t pool;
+
+ i_assert(callback != NULL);
+
+ pool = pool_alloconly_create("imapc command", 2048);
+ cmd = p_new(pool, struct imapc_command, 1);
+ cmd->pool = pool;
+ cmd->callback = callback;
+ cmd->context = context;
+
+ /* use a globally unique tag counter so looking at rawlogs is
+ somewhat easier */
+ if (++imapc_client_cmd_tag_counter == 0)
+ imapc_client_cmd_tag_counter++;
+ cmd->tag = imapc_client_cmd_tag_counter;
+ return cmd;
+}
+
+static void imapc_command_free(struct imapc_command *cmd)
+{
+ struct imapc_command_stream *stream;
+
+ if (array_is_created(&cmd->streams)) {
+ array_foreach_modifiable(&cmd->streams, stream)
+ i_stream_unref(&stream->input);
+ }
+ pool_unref(&cmd->pool);
+}
+
+const char *imapc_command_get_tag(struct imapc_command *cmd)
+{
+ return t_strdup_printf("%u", cmd->tag);
+}
+
+void imapc_command_abort(struct imapc_command **_cmd)
+{
+ struct imapc_command *cmd = *_cmd;
+
+ *_cmd = NULL;
+ imapc_command_free(cmd);
+}
+
+static void imapc_command_timeout(struct imapc_connection *conn)
+{
+ struct imapc_command *const *cmds;
+ unsigned int count;
+
+ cmds = array_get(&conn->cmd_wait_list, &count);
+ i_assert(count > 0);
+
+ imapc_connection_try_reconnect(conn, t_strdup_printf(
+ "Command '%s' timed out", imapc_command_get_readable(cmds[0])), 0, FALSE);
+}
+
+static bool
+parse_sync_literal(const unsigned char *data, unsigned int pos,
+ unsigned int *value_r)
+{
+ unsigned int value = 0, mul = 1;
+
+ /* data should contain "{size}\r\n" and pos points after \n */
+ if (pos <= 4 || data[pos-1] != '\n' || data[pos-2] != '\r' ||
+ data[pos-3] != '}' || !i_isdigit(data[pos-4]))
+ return FALSE;
+ pos -= 4;
+
+ do {
+ value += (data[pos] - '0') * mul;
+ mul = mul*10;
+ pos--;
+ } while (pos > 0 && i_isdigit(data[pos]));
+
+ if (pos == 0 || data[pos] != '{')
+ return FALSE;
+
+ *value_r = value;
+ return TRUE;
+}
+
+static void imapc_command_send_finished(struct imapc_connection *conn,
+ struct imapc_command *cmd)
+{
+ struct imapc_command *const *cmdp;
+
+ i_assert(conn->to != NULL);
+
+ if (cmd->idle)
+ conn->idle_plus_waiting = TRUE;
+ cmd->sent = TRUE;
+
+ /* everything sent. move command to wait list. */
+ cmdp = array_front(&conn->cmd_send_queue);
+ i_assert(*cmdp == cmd);
+ array_pop_front(&conn->cmd_send_queue);
+ array_push_back(&conn->cmd_wait_list, &cmd);
+
+ /* send the next command in queue */
+ imapc_command_send_more(conn);
+}
+
+static struct imapc_command_stream *
+imapc_command_get_sending_stream(struct imapc_command *cmd)
+{
+ struct imapc_command_stream *stream;
+
+ if (!array_is_created(&cmd->streams) || array_count(&cmd->streams) == 0)
+ return NULL;
+
+ stream = array_front_modifiable(&cmd->streams);
+ if (stream->pos != cmd->send_pos)
+ return NULL;
+ return stream;
+}
+
+static int imapc_command_try_send_stream(struct imapc_connection *conn,
+ struct imapc_command *cmd)
+{
+ struct imapc_command_stream *stream;
+ enum ostream_send_istream_result res;
+
+ stream = imapc_command_get_sending_stream(cmd);
+ if (stream == NULL)
+ return -2;
+
+ /* we're sending the stream now */
+ o_stream_set_max_buffer_size(conn->output, 0);
+ res = o_stream_send_istream(conn->output, stream->input);
+ o_stream_set_max_buffer_size(conn->output, SIZE_MAX);
+
+ switch (res) {
+ case OSTREAM_SEND_ISTREAM_RESULT_FINISHED:
+ break;
+ case OSTREAM_SEND_ISTREAM_RESULT_WAIT_INPUT:
+ i_unreached();
+ case OSTREAM_SEND_ISTREAM_RESULT_WAIT_OUTPUT:
+ i_assert(stream->input->v_offset < stream->size);
+ return 0;
+ case OSTREAM_SEND_ISTREAM_RESULT_ERROR_INPUT:
+ i_error("imapc: read(%s) failed: %s",
+ i_stream_get_name(stream->input),
+ i_stream_get_error(stream->input));
+ return -1;
+ case OSTREAM_SEND_ISTREAM_RESULT_ERROR_OUTPUT:
+ /* disconnected */
+ return -1;
+ }
+ i_assert(stream->input->v_offset == stream->size);
+
+ /* finished with the stream */
+ i_stream_unref(&stream->input);
+ array_pop_front(&cmd->streams);
+
+ i_assert(cmd->send_pos != cmd->data->used);
+ return 1;
+}
+
+static void imapc_connection_set_selecting(struct imapc_client_mailbox *box)
+{
+ struct imapc_connection *conn = box->conn;
+
+ i_assert(conn->selecting_box == NULL);
+
+ if (conn->selected_box != NULL &&
+ (conn->capabilities & IMAPC_CAPABILITY_QRESYNC) != 0) {
+ /* server will send a [CLOSED] once selected mailbox is
+ closed */
+ conn->selecting_box = box;
+ } else {
+ /* we'll have to assume that all the future untagged messages
+ are for the mailbox we're selecting */
+ conn->selected_box = box;
+ }
+ conn->select_waiting_reply = TRUE;
+}
+
+static bool imapc_connection_is_throttled(struct imapc_connection *conn)
+{
+ timeout_remove(&conn->to_throttle);
+
+ if (conn->throttle_msecs == 0) {
+ /* we haven't received [THROTTLED] recently */
+ return FALSE;
+ }
+ if (array_count(&conn->cmd_wait_list) > 0) {
+ /* wait until we have received the existing commands' tagged
+ replies to see if we're still throttled */
+ return TRUE;
+ }
+ if (timeval_cmp(&ioloop_timeval, &conn->throttle_end_timeval) >= 0) {
+ /* we reached the throttle timeout - send the next command */
+ conn->throttle_pending = FALSE;
+ return FALSE;
+ }
+
+ /* we're still being throttled - wait for it to end */
+ conn->to_throttle = timeout_add_absolute(&conn->throttle_end_timeval,
+ imapc_command_send_more, conn);
+ return TRUE;
+}
+
+static void imapc_command_send_more(struct imapc_connection *conn)
+{
+ struct imapc_command *const *cmds, *cmd;
+ struct imapc_command_reply reply;
+ const unsigned char *p, *data;
+ unsigned int count, size;
+ size_t seek_pos, start_pos, end_pos;
+ int ret;
+
+ if (imapc_connection_is_throttled(conn))
+ return;
+
+ cmds = array_get(&conn->cmd_send_queue, &count);
+ if (count == 0)
+ return;
+ cmd = cmds[0];
+
+ if ((cmd->flags & IMAPC_COMMAND_FLAG_PRELOGIN) == 0 &&
+ conn->state != IMAPC_CONNECTION_STATE_DONE) {
+ /* wait until we're fully connected */
+ return;
+ }
+ if ((cmd->flags & IMAPC_COMMAND_FLAG_LOGOUT) != 0 &&
+ array_count(&conn->cmd_wait_list) > 0) {
+ /* wait until existing commands have finished */
+ return;
+ }
+ if (conn->select_waiting_reply) {
+ /* wait for SELECT to finish */
+ return;
+ }
+ if (cmd->wait_for_literal) {
+ /* wait until we received '+' */
+ return;
+ }
+
+ i_assert(cmd->send_pos < cmd->data->used);
+
+ if (cmd->box == NULL) {
+ /* non-mailbox command */
+ } else if (cmd->send_pos == 0 &&
+ (cmd->flags & IMAPC_COMMAND_FLAG_SELECT) != 0) {
+ /* SELECT/EXAMINE command */
+ imapc_connection_set_selecting(cmd->box);
+ } else if (!imapc_client_mailbox_is_opened(cmd->box)) {
+ if (cmd->box->reconnecting) {
+ /* wait for SELECT/EXAMINE */
+ return;
+ }
+ /* shouldn't normally happen */
+ i_zero(&reply);
+ reply.text_without_resp = reply.text_full = "Mailbox not open";
+ reply.state = IMAPC_COMMAND_STATE_DISCONNECTED;
+
+ array_pop_front(&conn->cmd_send_queue);
+ imapc_command_reply_free(cmd, &reply);
+ imapc_command_send_more(conn);
+ return;
+ }
+
+ /* add timeout for commands if there's not one yet
+ (pre-login has its own timeout) */
+ if ((cmd->flags & IMAPC_COMMAND_FLAG_LOGOUT) != 0) {
+ /* LOGOUT has a shorter timeout */
+ timeout_remove(&conn->to);
+ conn->to = timeout_add(IMAPC_LOGOUT_TIMEOUT_MSECS,
+ imapc_command_timeout, conn);
+ } else if (conn->to == NULL) {
+ conn->to = timeout_add(conn->client->set.cmd_timeout_msecs,
+ imapc_command_timeout, conn);
+ }
+
+ timeout_reset(conn->to_output);
+ if ((ret = imapc_command_try_send_stream(conn, cmd)) == 0)
+ return;
+ if (ret == -1) {
+ i_zero(&reply);
+ reply.text_without_resp = reply.text_full = "Mailbox not open";
+ reply.state = IMAPC_COMMAND_STATE_DISCONNECTED;
+
+ array_pop_front(&conn->cmd_send_queue);
+ imapc_command_reply_free(cmd, &reply);
+ imapc_command_send_more(conn);
+ return;
+ }
+
+ seek_pos = cmd->send_pos;
+ if (seek_pos != 0 && ret == -2) {
+ /* skip over the literal. we can also get here from
+ AUTHENTICATE command, which doesn't use a literal */
+ if (parse_sync_literal(cmd->data->data, seek_pos, &size)) {
+ seek_pos += size;
+ i_assert(seek_pos <= cmd->data->used);
+ }
+ }
+
+ do {
+ start_pos = seek_pos;
+ p = memchr(CONST_PTR_OFFSET(cmd->data->data, seek_pos), '\n',
+ cmd->data->used - seek_pos);
+ i_assert(p != NULL);
+
+ seek_pos = p - (const unsigned char *)cmd->data->data + 1;
+ /* keep going for LITERAL+ command */
+ } while (start_pos + 3 < seek_pos &&
+ p[-1] == '\r' && p[-2] == '}' && p[-3] == '+');
+ end_pos = seek_pos;
+
+ data = CONST_PTR_OFFSET(cmd->data->data, cmd->send_pos);
+ size = end_pos - cmd->send_pos;
+ o_stream_nsend(conn->output, data, size);
+ cmd->send_pos = end_pos;
+
+ if (cmd->send_pos == cmd->data->used) {
+ i_assert(!array_is_created(&cmd->streams) ||
+ array_count(&cmd->streams) == 0);
+ imapc_command_send_finished(conn, cmd);
+ } else {
+ cmd->wait_for_literal = TRUE;
+ }
+}
+
+static void imapc_connection_send_idle_done(struct imapc_connection *conn)
+{
+ if ((conn->idling || conn->idle_plus_waiting) && !conn->idle_stopping) {
+ conn->idle_stopping = TRUE;
+ o_stream_nsend_str(conn->output, "DONE\r\n");
+ if (conn->to == NULL) {
+ conn->to = timeout_add(conn->client->set.cmd_timeout_msecs,
+ imapc_command_timeout, conn);
+ }
+ }
+}
+
+static void imapc_connection_cmd_send(struct imapc_command *cmd)
+{
+ struct imapc_connection *conn = cmd->conn;
+ struct imapc_command *const *cmds;
+ unsigned int i, count;
+
+ imapc_connection_send_idle_done(conn);
+
+ i_assert((cmd->flags & IMAPC_COMMAND_FLAG_RECONNECTED) == 0);
+
+ if ((cmd->flags & IMAPC_COMMAND_FLAG_PRELOGIN) != 0 &&
+ conn->state == IMAPC_CONNECTION_STATE_AUTHENTICATING) {
+ /* pre-login commands get inserted before everything else */
+ array_push_front(&conn->cmd_send_queue, &cmd);
+ imapc_command_send_more(conn);
+ return;
+ }
+
+ /* add the command just before retried commands */
+ cmds = array_get(&conn->cmd_send_queue, &count);
+ for (i = count; i > 0; i--) {
+ if ((cmds[i-1]->flags & IMAPC_COMMAND_FLAG_RECONNECTED) == 0)
+ break;
+ }
+ array_insert(&conn->cmd_send_queue, i, &cmd, 1);
+ imapc_command_send_more(conn);
+}
+
+static int imapc_connection_output(struct imapc_connection *conn)
+{
+ struct imapc_command *const *cmds;
+ unsigned int count;
+ int ret;
+
+ if (conn->to != NULL)
+ timeout_reset(conn->to);
+
+ if ((ret = o_stream_flush(conn->output)) < 0)
+ return 1;
+
+ imapc_connection_ref(conn);
+ cmds = array_get(&conn->cmd_send_queue, &count);
+ if (count > 0) {
+ if (imapc_command_get_sending_stream(cmds[0]) != NULL &&
+ !cmds[0]->wait_for_literal) {
+ /* we're sending a stream. send more. */
+ imapc_command_send_more(conn);
+ }
+ }
+ imapc_connection_unref(&conn);
+ return ret;
+}
+
+struct imapc_command *
+imapc_connection_cmd(struct imapc_connection *conn,
+ imapc_command_callback_t *callback, void *context)
+{
+ struct imapc_command *cmd;
+
+ cmd = imapc_command_begin(callback, context);
+ cmd->conn = conn;
+ return cmd;
+}
+
+void imapc_command_set_flags(struct imapc_command *cmd,
+ enum imapc_command_flags flags)
+{
+ cmd->flags = flags;
+}
+
+void imapc_command_set_mailbox(struct imapc_command *cmd,
+ struct imapc_client_mailbox *box)
+{
+ cmd->box = box;
+}
+
+bool imapc_command_connection_is_selected(struct imapc_command *cmd)
+{
+ return cmd->conn->selected_box != NULL ||
+ cmd->conn->selecting_box != NULL;
+}
+
+void imapc_command_send(struct imapc_command *cmd, const char *cmd_str)
+{
+ size_t len = strlen(cmd_str);
+
+ cmd->data = str_new(cmd->pool, 6 + len + 2);
+ str_printfa(cmd->data, "%u %s\r\n", cmd->tag, cmd_str);
+ imapc_connection_cmd_send(cmd);
+}
+
+void imapc_command_sendf(struct imapc_command *cmd, const char *cmd_fmt, ...)
+{
+ va_list args;
+
+ va_start(args, cmd_fmt);
+ imapc_command_sendvf(cmd, cmd_fmt, args);
+ va_end(args);
+}
+
+void imapc_command_sendvf(struct imapc_command *cmd,
+ const char *cmd_fmt, va_list args)
+{
+ unsigned int i;
+
+ cmd->data = str_new(cmd->pool, 128);
+ str_printfa(cmd->data, "%u ", cmd->tag);
+
+ for (i = 0; cmd_fmt[i] != '\0'; i++) {
+ if (cmd_fmt[i] != '%') {
+ str_append_c(cmd->data, cmd_fmt[i]);
+ continue;
+ }
+
+ switch (cmd_fmt[++i]) {
+ case '\0':
+ i_unreached();
+ case 'u': {
+ unsigned int arg = va_arg(args, unsigned int);
+
+ str_printfa(cmd->data, "%u", arg);
+ break;
+ }
+ case 'p': {
+ struct istream *input = va_arg(args, struct istream *);
+ struct imapc_command_stream *s;
+ uoff_t size;
+
+ if (!array_is_created(&cmd->streams))
+ p_array_init(&cmd->streams, cmd->pool, 2);
+ if (i_stream_get_size(input, TRUE, &size) < 0)
+ size = 0;
+ str_printfa(cmd->data, "{%"PRIuUOFF_T"}\r\n", size);
+ s = array_append_space(&cmd->streams);
+ s->pos = str_len(cmd->data);
+ s->size = size;
+ s->input = input;
+ i_stream_ref(input);
+ break;
+ }
+ case 's': {
+ const char *arg = va_arg(args, const char *);
+
+ if (!need_literal(arg))
+ imap_append_quoted(cmd->data, arg);
+ else if ((cmd->conn->capabilities &
+ IMAPC_CAPABILITY_LITERALPLUS) != 0) {
+ str_printfa(cmd->data, "{%zu+}\r\n%s",
+ strlen(arg), arg);
+ } else {
+ str_printfa(cmd->data, "{%zu}\r\n%s",
+ strlen(arg), arg);
+ }
+ break;
+ }
+ case '1': {
+ /* %1s - no quoting */
+ const char *arg = va_arg(args, const char *);
+
+ i++;
+ i_assert(cmd_fmt[i] == 's');
+ str_append(cmd->data, arg);
+ break;
+ }
+ }
+ }
+ str_append(cmd->data, "\r\n");
+
+ imapc_connection_cmd_send(cmd);
+}
+
+enum imapc_connection_state
+imapc_connection_get_state(struct imapc_connection *conn)
+{
+ return conn->state;
+}
+
+enum imapc_capability
+imapc_connection_get_capabilities(struct imapc_connection *conn)
+{
+ return conn->capabilities;
+}
+
+void imapc_connection_unselect(struct imapc_client_mailbox *box)
+{
+ struct imapc_connection *conn = box->conn;
+
+ if (conn->selected_box != NULL || conn->selecting_box != NULL) {
+ i_assert(conn->selected_box == box ||
+ conn->selecting_box == box);
+
+ conn->selected_box = NULL;
+ conn->selecting_box = NULL;
+ }
+ imapc_connection_send_idle_done(conn);
+ imapc_connection_abort_commands(conn, box, FALSE);
+}
+
+struct imapc_client_mailbox *
+imapc_connection_get_mailbox(struct imapc_connection *conn)
+{
+ if (conn->selecting_box != NULL)
+ return conn->selecting_box;
+ return conn->selected_box;
+}
+
+static void
+imapc_connection_idle_callback(const struct imapc_command_reply *reply ATTR_UNUSED,
+ void *context)
+{
+ struct imapc_connection *conn = context;
+
+ conn->idling = FALSE;
+ conn->idle_plus_waiting = FALSE;
+ conn->idle_stopping = FALSE;
+}
+
+void imapc_connection_idle(struct imapc_connection *conn)
+{
+ struct imapc_command *cmd;
+
+ if (array_count(&conn->cmd_send_queue) != 0 ||
+ array_count(&conn->cmd_wait_list) != 0 ||
+ conn->idling || conn->idle_plus_waiting ||
+ (conn->capabilities & IMAPC_CAPABILITY_IDLE) == 0)
+ return;
+
+ cmd = imapc_connection_cmd(conn, imapc_connection_idle_callback, conn);
+ cmd->idle = TRUE;
+ imapc_command_send(cmd, "IDLE");
+}
diff --git a/src/lib-imap-client/imapc-connection.h b/src/lib-imap-client/imapc-connection.h
new file mode 100644
index 0000000..8fda692
--- /dev/null
+++ b/src/lib-imap-client/imapc-connection.h
@@ -0,0 +1,63 @@
+#ifndef IMAPC_CONNECTION_H
+#define IMAPC_CONNECTION_H
+
+#include "imapc-client.h"
+
+/* [THROTTLED] handling behavior */
+#define IMAPC_THROTTLE_DEFAULT_INIT_MSECS 50
+#define IMAPC_THROTTLE_DEFAULT_MAX_MSECS (16*1000)
+#define IMAPC_THROTTLE_DEFAULT_SHRINK_MIN_MSECS 500
+
+struct imapc_client;
+struct imapc_connection;
+
+enum imapc_connection_state {
+ /* No connection */
+ IMAPC_CONNECTION_STATE_DISCONNECTED = 0,
+ /* Trying to connect */
+ IMAPC_CONNECTION_STATE_CONNECTING,
+ /* Connected, trying to authenticate */
+ IMAPC_CONNECTION_STATE_AUTHENTICATING,
+ /* Authenticated, ready to accept commands */
+ IMAPC_CONNECTION_STATE_DONE
+};
+
+struct imapc_connection *
+imapc_connection_init(struct imapc_client *client,
+ imapc_command_callback_t *login_callback,
+ void *login_context);
+void imapc_connection_deinit(struct imapc_connection **conn);
+
+void imapc_connection_connect(struct imapc_connection *conn);
+void imapc_connection_set_no_reconnect(struct imapc_connection *conn);
+void imapc_connection_disconnect(struct imapc_connection *conn);
+void imapc_connection_disconnect_full(struct imapc_connection *conn,
+ bool reconnecting);
+void imapc_connection_try_reconnect(struct imapc_connection *conn,
+ const char *errstr,
+ unsigned int delay_msecs,
+ bool connect_error);
+void imapc_connection_abort_commands(struct imapc_connection *conn,
+ struct imapc_client_mailbox *only_box,
+ bool keep_retriable) ATTR_NULL(2);
+void imapc_connection_ioloop_changed(struct imapc_connection *conn);
+void imapc_connection_input_pending(struct imapc_connection *conn);
+
+struct imapc_command *
+imapc_connection_cmd(struct imapc_connection *conn,
+ imapc_command_callback_t *callback, void *context)
+ ATTR_NULL(3);
+
+void imapc_connection_unselect(struct imapc_client_mailbox *box);
+
+enum imapc_connection_state
+imapc_connection_get_state(struct imapc_connection *conn);
+enum imapc_capability
+imapc_connection_get_capabilities(struct imapc_connection *conn);
+
+struct imapc_client_mailbox *
+imapc_connection_get_mailbox(struct imapc_connection *conn);
+
+void imapc_connection_idle(struct imapc_connection *conn);
+
+#endif
diff --git a/src/lib-imap-client/imapc-msgmap.c b/src/lib-imap-client/imapc-msgmap.c
new file mode 100644
index 0000000..6280a24
--- /dev/null
+++ b/src/lib-imap-client/imapc-msgmap.c
@@ -0,0 +1,89 @@
+/* Copyright (c) 2011-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "imapc-msgmap.h"
+#include "sort.h"
+
+struct imapc_msgmap {
+ ARRAY_TYPE(uint32_t) uids;
+ uint32_t uid_next;
+};
+
+struct imapc_msgmap *imapc_msgmap_init(void)
+{
+ struct imapc_msgmap *msgmap;
+
+ msgmap = i_new(struct imapc_msgmap, 1);
+ i_array_init(&msgmap->uids, 128);
+ msgmap->uid_next = 1;
+ return msgmap;
+}
+
+void imapc_msgmap_deinit(struct imapc_msgmap **_msgmap)
+{
+ struct imapc_msgmap *msgmap = *_msgmap;
+
+ *_msgmap = NULL;
+
+ array_free(&msgmap->uids);
+ i_free(msgmap);
+}
+
+uint32_t imapc_msgmap_count(struct imapc_msgmap *msgmap)
+{
+ return array_count(&msgmap->uids);
+}
+
+uint32_t imapc_msgmap_uidnext(struct imapc_msgmap *msgmap)
+{
+ return msgmap->uid_next;
+}
+
+uint32_t imapc_msgmap_rseq_to_uid(struct imapc_msgmap *msgmap, uint32_t rseq)
+{
+ const uint32_t *uidp;
+
+ uidp = array_idx(&msgmap->uids, rseq-1);
+ return *uidp;
+}
+
+bool imapc_msgmap_uid_to_rseq(struct imapc_msgmap *msgmap,
+ uint32_t uid, uint32_t *rseq_r)
+{
+ const uint32_t *p, *first;
+
+ p = array_bsearch(&msgmap->uids, &uid, uint32_cmp);
+ if (p == NULL) {
+ *rseq_r = 0;
+ return FALSE;
+ }
+
+ first = array_front(&msgmap->uids);
+ *rseq_r = (p - first) + 1;
+ return TRUE;
+}
+
+void imapc_msgmap_append(struct imapc_msgmap *msgmap,
+ uint32_t rseq, uint32_t uid)
+{
+ i_assert(rseq == imapc_msgmap_count(msgmap) + 1);
+ i_assert(uid >= msgmap->uid_next);
+
+ msgmap->uid_next = uid + 1;
+ array_push_back(&msgmap->uids, &uid);
+}
+
+void imapc_msgmap_expunge(struct imapc_msgmap *msgmap, uint32_t rseq)
+{
+ i_assert(rseq > 0);
+ i_assert(rseq <= imapc_msgmap_count(msgmap));
+
+ array_delete(&msgmap->uids, rseq-1, 1);
+}
+
+void imapc_msgmap_reset(struct imapc_msgmap *msgmap)
+{
+ array_clear(&msgmap->uids);
+ msgmap->uid_next = 1;
+}
diff --git a/src/lib-imap-client/imapc-msgmap.h b/src/lib-imap-client/imapc-msgmap.h
new file mode 100644
index 0000000..934bf97
--- /dev/null
+++ b/src/lib-imap-client/imapc-msgmap.h
@@ -0,0 +1,18 @@
+#ifndef IMAPC_MSGMAP_H
+#define IMAPC_MSGMAP_H
+
+struct imapc_msgmap *imapc_msgmap_init(void);
+void imapc_msgmap_deinit(struct imapc_msgmap **msgmap);
+
+uint32_t imapc_msgmap_count(struct imapc_msgmap *msgmap);
+uint32_t imapc_msgmap_uidnext(struct imapc_msgmap *msgmap);
+uint32_t imapc_msgmap_rseq_to_uid(struct imapc_msgmap *msgmap, uint32_t rseq);
+bool imapc_msgmap_uid_to_rseq(struct imapc_msgmap *msgmap,
+ uint32_t uid, uint32_t *rseq_r);
+
+void imapc_msgmap_append(struct imapc_msgmap *msgmap,
+ uint32_t rseq, uint32_t uid);
+void imapc_msgmap_expunge(struct imapc_msgmap *msgmap, uint32_t rseq);
+void imapc_msgmap_reset(struct imapc_msgmap *msgmap);
+
+#endif
diff --git a/src/lib-imap-client/test-imapc-client.c b/src/lib-imap-client/test-imapc-client.c
new file mode 100644
index 0000000..c8953f0
--- /dev/null
+++ b/src/lib-imap-client/test-imapc-client.c
@@ -0,0 +1,901 @@
+/* Copyright (c) 2017-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "hostpid.h"
+#include "net.h"
+#include "istream.h"
+#include "ostream.h"
+#include "ioloop.h"
+#include "unlink-directory.h"
+#include "sleep.h"
+#include "test-common.h"
+#include "test-subprocess.h"
+#include "imapc-client-private.h"
+
+#include <stdio.h>
+#include <unistd.h>
+
+#define SERVER_KILL_TIMEOUT_SECS 20
+
+#define IMAPC_COMMAND_STATE_INVALID (enum imapc_command_state)-1
+
+typedef void test_server_init_t(void);
+typedef void test_client_init_t(void);
+
+struct test_server {
+ in_port_t port;
+ pid_t pid;
+
+ int fd_listen, fd;
+ struct istream *input;
+ struct ostream *output;
+};
+
+static struct ip_addr bind_ip;
+static struct test_server server;
+static struct imapc_client *imapc_client;
+static enum imapc_command_state imapc_login_last_reply;
+static ARRAY(enum imapc_command_state) imapc_cmd_last_replies;
+static bool debug = FALSE;
+
+static void main_deinit(void);
+
+/*
+ * Test client
+ */
+
+static struct imapc_client_settings test_imapc_default_settings = {
+ .host = "127.0.0.1",
+ .username = "testuser",
+ .password = "testpass",
+
+ .dns_client_socket_path = "",
+ .temp_path_prefix = ".test-tmp/",
+ .rawlog_dir = "",
+
+ .connect_timeout_msecs = 5000,
+ .connect_retry_count = 3,
+ .connect_retry_interval_msecs = 10,
+
+ .max_idle_time = 10000,
+};
+
+static enum imapc_command_state test_imapc_cmd_last_reply_pop(void)
+{
+ const enum imapc_command_state *replies;
+ enum imapc_command_state reply;
+ unsigned int count;
+
+ replies = array_get(&imapc_cmd_last_replies, &count);
+ if (count == 0)
+ return IMAPC_COMMAND_STATE_INVALID;
+ reply = replies[0];
+ array_pop_front(&imapc_cmd_last_replies);
+ return reply;
+}
+
+static bool test_imapc_cmd_last_reply_expect(enum imapc_command_state state)
+{
+ if (array_count(&imapc_cmd_last_replies) == 0)
+ imapc_client_run(imapc_client);
+ return test_imapc_cmd_last_reply_pop() == state;
+}
+
+static void imapc_login_callback(const struct imapc_command_reply *reply,
+ void *context ATTR_UNUSED)
+{
+ if (debug) {
+ i_debug("Login reply: %s %s",
+ imapc_command_state_names[reply->state],
+ reply->text_full);
+ }
+ imapc_login_last_reply = reply->state;
+ imapc_client_stop(imapc_client);
+}
+
+static void imapc_command_callback(const struct imapc_command_reply *reply,
+ void *context ATTR_UNUSED)
+{
+ if (debug) {
+ i_debug("Command reply: %s %s",
+ imapc_command_state_names[reply->state],
+ reply->text_full);
+ }
+ array_push_back(&imapc_cmd_last_replies, &reply->state);
+ imapc_client_stop(imapc_client);
+}
+
+static void imapc_reopen_callback(void *context)
+{
+ struct imapc_client_mailbox *box = context;
+ struct imapc_command *cmd;
+
+ cmd = imapc_client_mailbox_cmd(box, imapc_command_callback, NULL);
+ imapc_command_set_flags(cmd, IMAPC_COMMAND_FLAG_SELECT);
+ imapc_command_send(cmd, "SELECT");
+}
+
+/*
+ * Test server
+ */
+
+static bool
+test_imapc_server_expect_full(struct test_server *server,
+ const char *expected_line)
+{
+ const char *line = i_stream_read_next_line(server->input);
+
+ if (debug)
+ i_debug("Received: %s", (line == NULL ? "<EOF>" : line));
+
+ if (line == NULL) {
+ printf("imapc client disconnected unexpectedly: %s\n",
+ i_stream_get_error(server->input));
+ return FALSE;
+ } else if (strcmp(line, expected_line) != 0) {
+ printf("imapc client sent '%s' when expecting '%s'\n",
+ line, expected_line);
+ return FALSE;
+ } else {
+ return TRUE;
+ }
+}
+
+static bool test_imapc_server_expect(const char *expected_line)
+{
+ return test_imapc_server_expect_full(&server, expected_line);
+}
+
+static void
+test_server_wait_connection(struct test_server *server, bool send_banner)
+{
+ if (debug)
+ i_debug("Waiting for connection");
+
+ server->fd = net_accept(server->fd_listen, NULL, NULL);
+ i_assert(server->fd >= 0);
+
+ if (debug)
+ i_debug("Client connected");
+
+ fd_set_nonblock(server->fd, FALSE);
+ server->input = i_stream_create_fd(server->fd, SIZE_MAX);
+ server->output = o_stream_create_fd(server->fd, SIZE_MAX);
+ o_stream_set_no_error_handling(server->output, TRUE);
+
+ if (send_banner) {
+ o_stream_nsend_str(server->output,
+ "* OK [CAPABILITY IMAP4rev1 UNSELECT QUOTA] ready\r\n");
+ }
+}
+
+static void test_server_disconnect(struct test_server *server)
+{
+ if (debug)
+ i_debug("Disconnecting client");
+
+ i_stream_unref(&server->input);
+ o_stream_unref(&server->output);
+ i_close_fd(&server->fd);
+}
+
+static void test_server_disconnect_and_wait(bool send_banner)
+{
+ test_server_disconnect(&server);
+ test_server_wait_connection(&server, send_banner);
+}
+
+/*
+ * Test processes
+ */
+
+static int test_open_server_fd(in_port_t *bind_port)
+{
+ int fd = net_listen(&bind_ip, bind_port, 128);
+ if (debug)
+ i_debug("server listening on %u", *bind_port);
+ if (fd == -1) {
+ i_fatal("listen(%s:%u) failed: %m",
+ net_ip2addr(&bind_ip), *bind_port);
+ }
+ fd_set_nonblock(fd, FALSE);
+ return fd;
+}
+
+static int test_run_server(test_server_init_t *server_test)
+{
+ struct ioloop *ioloop;
+
+ i_set_failure_prefix("SERVER: ");
+
+ if (debug)
+ i_debug("PID=%s", my_pid);
+
+ ioloop = io_loop_create();
+ if (server_test != NULL)
+ server_test();
+ test_server_disconnect(&server);
+ io_loop_destroy(&ioloop);
+
+ if (debug)
+ i_debug("Terminated");
+
+ i_close_fd(&server.fd_listen);
+ main_deinit();
+ return 0;
+}
+
+static void
+test_run_client(const struct imapc_client_settings *client_set,
+ test_client_init_t *client_test)
+{
+ struct ioloop *ioloop;
+
+ i_set_failure_prefix("CLIENT: ");
+
+ if (debug)
+ i_debug("PID=%s", my_pid);
+
+ i_sleep_msecs(100); /* wait a little for server setup */
+
+ ioloop = io_loop_create();
+ imapc_client = imapc_client_init(client_set, NULL);
+ client_test();
+ imapc_client_logout(imapc_client);
+ test_assert(array_count(&imapc_cmd_last_replies) == 0);
+ if (imapc_client != NULL)
+ imapc_client_deinit(&imapc_client);
+ io_loop_destroy(&ioloop);
+
+ if (debug)
+ i_debug("Terminated");
+}
+
+static void
+test_run_client_server(const struct imapc_client_settings *client_set,
+ test_client_init_t *client_test,
+ test_server_init_t *server_test)
+{
+ struct imapc_client_settings client_set_copy = *client_set;
+ const char *error;
+
+ imapc_client_cmd_tag_counter = 0;
+ imapc_login_last_reply = IMAPC_COMMAND_STATE_INVALID;
+ t_array_init(&imapc_cmd_last_replies, 4);
+
+ i_zero(&server);
+ server.pid = (pid_t)-1;
+ server.fd = -1;
+ server.fd_listen = test_open_server_fd(&server.port);
+ client_set_copy.port = server.port;
+
+ if (mkdir(client_set->temp_path_prefix, 0700) < 0 && errno != EEXIST)
+ i_fatal("mkdir(%s) failed: %m", client_set->temp_path_prefix);
+
+ if (server_test != NULL) {
+ /* Fork server */
+ test_subprocess_fork(test_run_server, server_test, TRUE);
+ }
+ i_close_fd(&server.fd_listen);
+
+ /* Run client */
+ test_run_client(&client_set_copy, client_test);
+
+ i_unset_failure_prefix();
+ test_subprocess_kill_all(SERVER_KILL_TIMEOUT_SECS);
+ if (unlink_directory(client_set->temp_path_prefix,
+ UNLINK_DIRECTORY_FLAG_RMDIR, &error) < 0)
+ i_fatal("%s", error);
+}
+
+/*
+ * imapc connect failed
+ */
+
+static void test_imapc_connect_failed_client(void)
+{
+ imapc_client_set_login_callback(imapc_client,
+ imapc_login_callback, NULL);
+ imapc_client_login(imapc_client);
+ /* connection refused & one reconnect */
+ test_expect_errors(2);
+ imapc_client_run(imapc_client);
+ test_expect_no_more_errors();
+ test_assert(imapc_login_last_reply == IMAPC_COMMAND_STATE_DISCONNECTED);
+}
+
+static void test_imapc_connect_failed(void)
+{
+ struct imapc_client_settings set = test_imapc_default_settings;
+
+ test_begin("imapc connect failed");
+ test_run_client_server(&set, test_imapc_connect_failed_client, NULL);
+ test_end();
+}
+
+/*
+ * imapc banner hang
+ */
+
+static void test_imapc_banner_hangs_client(void)
+{
+ imapc_client_set_login_callback(imapc_client,
+ imapc_login_callback, NULL);
+ imapc_client_login(imapc_client);
+ test_expect_errors(2);
+ imapc_client_run(imapc_client);
+ test_expect_no_more_errors();
+ test_assert(imapc_login_last_reply == IMAPC_COMMAND_STATE_DISCONNECTED);
+}
+
+static void test_imapc_banner_hangs_server(void)
+{
+ struct test_server server2 = { .fd_listen = server.fd_listen };
+
+ test_server_wait_connection(&server, FALSE);
+ test_server_wait_connection(&server2, FALSE);
+ test_assert(i_stream_read_next_line(server2.input) == NULL);
+ test_server_disconnect(&server2);
+}
+
+static void test_imapc_banner_hangs(void)
+{
+ struct imapc_client_settings set = test_imapc_default_settings;
+ set.connect_timeout_msecs = 500;
+
+ test_begin("imapc banner hangs");
+ test_run_client_server(&set, test_imapc_banner_hangs_client,
+ test_imapc_banner_hangs_server);
+ test_end();
+}
+
+/*
+ * imapc login hangs
+ */
+
+static void test_imapc_login_hangs_client(void)
+{
+ imapc_client_set_login_callback(imapc_client,
+ imapc_login_callback, NULL);
+ imapc_client_login(imapc_client);
+ /* run the first login */
+ test_expect_error_string("Authentication timed out");
+ imapc_client_run(imapc_client);
+ test_expect_no_more_errors();
+ /* imapc_login_callback() has stopped us. run the second reconnect
+ login. */
+ test_expect_error_string("Authentication timed out");
+ imapc_client_run(imapc_client);
+ test_expect_no_more_errors();
+ test_assert(imapc_login_last_reply == IMAPC_COMMAND_STATE_DISCONNECTED);
+}
+
+static void test_imapc_login_hangs_server(void)
+{
+ struct test_server server2 = { .fd_listen = server.fd_listen };
+
+ test_server_wait_connection(&server, TRUE);
+ test_assert(test_imapc_server_expect(
+ "1 LOGIN \"testuser\" \"testpass\""));
+
+ test_server_wait_connection(&server2, TRUE);
+ test_assert(test_imapc_server_expect_full(
+ &server2, "2 LOGIN \"testuser\" \"testpass\""));
+
+ test_assert(i_stream_read_next_line(server2.input) == NULL);
+ test_server_disconnect(&server2);
+}
+
+static void test_imapc_login_hangs(void)
+{
+ struct imapc_client_settings set = test_imapc_default_settings;
+ set.connect_timeout_msecs = 500;
+
+ test_begin("imapc login hangs");
+ test_run_client_server(&set, test_imapc_login_hangs_client,
+ test_imapc_login_hangs_server);
+ test_end();
+}
+
+/*
+ * imapc login fails
+ */
+
+static void test_imapc_login_fails_client(void)
+{
+ imapc_client_set_login_callback(imapc_client,
+ imapc_login_callback, NULL);
+ imapc_client_login(imapc_client);
+ test_expect_error_string("Authentication failed: Test login failed");
+ imapc_client_run(imapc_client);
+ test_expect_no_more_errors();
+ test_assert(imapc_login_last_reply == IMAPC_COMMAND_STATE_AUTH_FAILED);
+}
+
+static void test_imapc_login_fails_server(void)
+{
+ test_server_wait_connection(&server, TRUE);
+ test_assert(test_imapc_server_expect(
+ "1 LOGIN \"testuser\" \"testpass\""));
+ o_stream_nsend_str(server.output, "1 NO Test login failed\r\n");
+}
+
+static void test_imapc_login_fails(void)
+{
+ struct imapc_client_settings set = test_imapc_default_settings;
+
+ test_begin("imapc login fails");
+ test_run_client_server(&set, test_imapc_login_fails_client,
+ test_imapc_login_fails_server);
+ test_end();
+}
+
+/*
+ * imapc reconnect
+ */
+
+static void test_imapc_reconnect_client(void)
+{
+ struct imapc_command *cmd;
+
+ /* login to server */
+ imapc_client_set_login_callback(imapc_client,
+ imapc_login_callback, NULL);
+ imapc_client_login(imapc_client);
+ imapc_client_run(imapc_client);
+ test_assert(imapc_login_last_reply == IMAPC_COMMAND_STATE_OK);
+ imapc_login_last_reply = IMAPC_COMMAND_STATE_INVALID;
+
+ /* disconnect */
+ cmd = imapc_client_cmd(imapc_client, imapc_command_callback, NULL);
+ imapc_command_send(cmd, "DISCONNECT");
+ test_expect_error_string("reconnecting");
+ imapc_client_run(imapc_client);
+ test_expect_no_more_errors();
+ test_assert(test_imapc_cmd_last_reply_pop() ==
+ IMAPC_COMMAND_STATE_DISCONNECTED);
+
+ /* we should be reconnected now. try a command. */
+ cmd = imapc_client_cmd(imapc_client, imapc_command_callback, NULL);
+ imapc_command_send(cmd, "NOOP");
+ imapc_client_run(imapc_client);
+ test_assert(imapc_login_last_reply == IMAPC_COMMAND_STATE_INVALID);
+ test_assert(test_imapc_cmd_last_reply_pop() == IMAPC_COMMAND_STATE_OK);
+}
+
+static void test_imapc_reconnect_server(void)
+{
+ test_server_wait_connection(&server, TRUE);
+ test_assert(test_imapc_server_expect(
+ "1 LOGIN \"testuser\" \"testpass\""));
+ o_stream_nsend_str(server.output, "1 OK \r\n");
+
+ test_assert(test_imapc_server_expect("2 DISCONNECT"));
+ test_server_disconnect_and_wait(TRUE);
+
+ test_assert(test_imapc_server_expect(
+ "4 LOGIN \"testuser\" \"testpass\""));
+ o_stream_nsend_str(server.output, "4 OK \r\n");
+ test_assert(test_imapc_server_expect("3 NOOP"));
+ o_stream_nsend_str(server.output, "3 OK \r\n");
+
+ test_assert(test_imapc_server_expect("5 LOGOUT"));
+ o_stream_nsend_str(server.output, "5 OK \r\n");
+
+ test_assert(i_stream_read_next_line(server.input) == NULL);
+}
+
+static void test_imapc_reconnect(void)
+{
+ struct imapc_client_settings set = test_imapc_default_settings;
+
+ test_begin("imapc reconnect");
+ test_run_client_server(&set, test_imapc_reconnect_client,
+ test_imapc_reconnect_server);
+ test_end();
+}
+
+/*
+ * imapc reconnect resend commands
+ */
+
+static void test_imapc_reconnect_resend_cmds_client(void)
+{
+ struct imapc_command *cmd;
+
+ /* login to server */
+ imapc_client_set_login_callback(imapc_client,
+ imapc_login_callback, NULL);
+ imapc_client_login(imapc_client);
+ imapc_client_run(imapc_client);
+ test_assert(imapc_login_last_reply == IMAPC_COMMAND_STATE_OK);
+ imapc_login_last_reply = IMAPC_COMMAND_STATE_INVALID;
+
+ /* send two commands */
+ cmd = imapc_client_cmd(imapc_client, imapc_command_callback, NULL);
+ imapc_command_set_flags(cmd, IMAPC_COMMAND_FLAG_RETRIABLE);
+ imapc_command_send(cmd, "RETRY1");
+ cmd = imapc_client_cmd(imapc_client, imapc_command_callback, NULL);
+ imapc_command_set_flags(cmd, IMAPC_COMMAND_FLAG_RETRIABLE);
+ imapc_command_send(cmd, "RETRY2");
+
+ /* disconnect & reconnect automatically */
+ cmd = imapc_client_cmd(imapc_client, imapc_command_callback, NULL);
+ imapc_command_send(cmd, "DISCONNECT");
+ test_expect_error_string("reconnecting");
+ imapc_client_run(imapc_client);
+ test_expect_no_more_errors();
+ test_assert(test_imapc_cmd_last_reply_expect(
+ IMAPC_COMMAND_STATE_DISCONNECTED));
+
+ /* continue reconnection */
+ test_assert(test_imapc_cmd_last_reply_expect(IMAPC_COMMAND_STATE_OK));
+ test_assert(test_imapc_cmd_last_reply_expect(IMAPC_COMMAND_STATE_OK));
+}
+
+static void test_imapc_reconnect_resend_cmds_server(void)
+{
+ test_server_wait_connection(&server, TRUE);
+ test_assert(test_imapc_server_expect(
+ "1 LOGIN \"testuser\" \"testpass\""));
+ o_stream_nsend_str(server.output, "1 OK \r\n");
+
+ test_assert(test_imapc_server_expect("2 RETRY1"));
+ test_assert(test_imapc_server_expect("3 RETRY2"));
+ test_assert(test_imapc_server_expect("4 DISCONNECT"));
+ test_server_disconnect_and_wait(TRUE);
+
+ test_assert(test_imapc_server_expect(
+ "5 LOGIN \"testuser\" \"testpass\""));
+ o_stream_nsend_str(server.output, "5 OK \r\n");
+ test_assert(test_imapc_server_expect("2 RETRY1"));
+ o_stream_nsend_str(server.output, "2 OK \r\n");
+ test_assert(test_imapc_server_expect("3 RETRY2"));
+ o_stream_nsend_str(server.output, "3 OK \r\n");
+
+ test_assert(test_imapc_server_expect("6 LOGOUT"));
+ o_stream_nsend_str(server.output, "6 OK \r\n");
+
+ test_assert(i_stream_read_next_line(server.input) == NULL);
+}
+
+static void test_imapc_reconnect_resend_commands(void)
+{
+ struct imapc_client_settings set = test_imapc_default_settings;
+
+ test_begin("imapc reconnect resend commands");
+ test_run_client_server(&set, test_imapc_reconnect_resend_cmds_client,
+ test_imapc_reconnect_resend_cmds_server);
+ test_end();
+}
+
+/*
+ * imapc reconnect resend commands failed
+ */
+
+static void test_imapc_reconnect_resend_cmds_failed_client(void)
+{
+ struct imapc_command *cmd;
+
+ /* login to server */
+ imapc_client_set_login_callback(imapc_client,
+ imapc_login_callback, NULL);
+ imapc_client_login(imapc_client);
+ imapc_client_run(imapc_client);
+ test_assert(imapc_login_last_reply == IMAPC_COMMAND_STATE_OK);
+ imapc_login_last_reply = IMAPC_COMMAND_STATE_INVALID;
+
+ /* send two commands */
+ cmd = imapc_client_cmd(imapc_client, imapc_command_callback, NULL);
+ imapc_command_set_flags(cmd, IMAPC_COMMAND_FLAG_RETRIABLE);
+ imapc_command_send(cmd, "RETRY1");
+ cmd = imapc_client_cmd(imapc_client, imapc_command_callback, NULL);
+ imapc_command_set_flags(cmd, IMAPC_COMMAND_FLAG_RETRIABLE);
+ imapc_command_send(cmd, "RETRY2");
+
+ /* disconnect & try to reconnect automatically */
+ cmd = imapc_client_cmd(imapc_client, imapc_command_callback, NULL);
+ imapc_command_send(cmd, "DISCONNECT");
+ test_expect_error_string("reconnecting");
+ imapc_client_run(imapc_client);
+ test_expect_no_more_errors();
+ test_assert(test_imapc_cmd_last_reply_expect(
+ IMAPC_COMMAND_STATE_DISCONNECTED));
+ test_expect_error_string("timed out");
+ test_assert(test_imapc_cmd_last_reply_expect(
+ IMAPC_COMMAND_STATE_DISCONNECTED));
+ test_expect_no_more_errors();
+ test_assert(test_imapc_cmd_last_reply_expect(
+ IMAPC_COMMAND_STATE_DISCONNECTED));
+}
+
+static void test_imapc_reconnect_resend_cmds_failed_server(void)
+{
+ test_server_wait_connection(&server, TRUE);
+ test_assert(test_imapc_server_expect(
+ "1 LOGIN \"testuser\" \"testpass\""));
+ o_stream_nsend_str(server.output, "1 OK \r\n");
+
+ test_assert(test_imapc_server_expect("2 RETRY1"));
+ test_assert(test_imapc_server_expect("3 RETRY2"));
+ test_assert(test_imapc_server_expect("4 DISCONNECT"));
+ test_server_disconnect(&server);
+
+ i_sleep_intr_secs(60);
+}
+
+static void test_imapc_reconnect_resend_commands_failed(void)
+{
+ struct imapc_client_settings set = test_imapc_default_settings;
+ set.connect_timeout_msecs = 500;
+
+ test_begin("imapc reconnect resend commands failed");
+ test_run_client_server(&set,
+ test_imapc_reconnect_resend_cmds_failed_client,
+ test_imapc_reconnect_resend_cmds_failed_server);
+ test_end();
+}
+
+/*
+ * imapc reconnect mailbox
+ */
+
+static void test_imapc_reconnect_mailbox_client(void)
+{
+ struct imapc_command *cmd;
+ struct imapc_client_mailbox *box;
+
+ /* login to server */
+ imapc_client_set_login_callback(imapc_client,
+ imapc_login_callback, NULL);
+ imapc_client_login(imapc_client);
+ imapc_client_run(imapc_client);
+ test_assert(imapc_login_last_reply == IMAPC_COMMAND_STATE_OK);
+ imapc_login_last_reply = IMAPC_COMMAND_STATE_INVALID;
+
+ /* select a mailbox */
+ box = imapc_client_mailbox_open(imapc_client, NULL);
+ imapc_client_mailbox_set_reopen_cb(box, imapc_reopen_callback, box);
+
+ cmd = imapc_client_mailbox_cmd(box, imapc_command_callback, NULL);
+ imapc_command_set_flags(cmd, IMAPC_COMMAND_FLAG_SELECT);
+ imapc_command_send(cmd, "SELECT");
+ imapc_client_run(imapc_client);
+ test_assert(test_imapc_cmd_last_reply_expect(IMAPC_COMMAND_STATE_OK));
+
+ /* send a command */
+ cmd = imapc_client_mailbox_cmd(box, imapc_command_callback, NULL);
+ imapc_command_set_flags(cmd, IMAPC_COMMAND_FLAG_RETRIABLE);
+ imapc_command_send(cmd, "RETRY");
+
+ /* disconnect & reconnect automatically */
+ cmd = imapc_client_cmd(imapc_client, imapc_command_callback, NULL);
+ imapc_command_send(cmd, "DISCONNECT");
+ test_expect_error_string("reconnecting");
+ imapc_client_run(imapc_client);
+ test_expect_no_more_errors();
+ test_assert(test_imapc_cmd_last_reply_expect(
+ IMAPC_COMMAND_STATE_DISCONNECTED));
+
+ /* continue reconnection */
+ test_assert(test_imapc_cmd_last_reply_expect(IMAPC_COMMAND_STATE_OK));
+ test_assert(test_imapc_cmd_last_reply_expect(IMAPC_COMMAND_STATE_OK));
+
+ imapc_client_mailbox_close(&box);
+}
+
+static void test_imapc_reconnect_mailbox_server(void)
+{
+ test_server_wait_connection(&server, TRUE);
+ test_assert(test_imapc_server_expect(
+ "1 LOGIN \"testuser\" \"testpass\""));
+ o_stream_nsend_str(server.output, "1 OK \r\n");
+
+ test_assert(test_imapc_server_expect("2 SELECT"));
+ o_stream_nsend_str(server.output, "2 OK \r\n");
+
+ test_assert(test_imapc_server_expect("3 RETRY"));
+ test_assert(test_imapc_server_expect("4 DISCONNECT"));
+ test_server_disconnect_and_wait(TRUE);
+
+ test_assert(test_imapc_server_expect(
+ "5 LOGIN \"testuser\" \"testpass\""));
+ o_stream_nsend_str(server.output, "5 OK \r\n");
+ test_assert(test_imapc_server_expect("6 SELECT"));
+ o_stream_nsend_str(server.output, "6 OK \r\n");
+ test_assert(test_imapc_server_expect("3 RETRY"));
+ o_stream_nsend_str(server.output, "3 OK \r\n");
+
+ test_assert(test_imapc_server_expect("7 LOGOUT"));
+ o_stream_nsend_str(server.output, "7 OK \r\n");
+
+ test_assert(i_stream_read_next_line(server.input) == NULL);
+}
+
+static void test_imapc_reconnect_mailbox(void)
+{
+ struct imapc_client_settings set = test_imapc_default_settings;
+
+ test_begin("imapc reconnect mailbox");
+ test_run_client_server(&set, test_imapc_reconnect_mailbox_client,
+ test_imapc_reconnect_mailbox_server);
+ test_end();
+}
+
+/*
+ * imapc_client_get_capabilities()
+ */
+
+static void test_imapc_client_get_capabilities_client(void)
+{
+ enum imapc_capability capabilities;
+
+ test_assert(imapc_client_get_capabilities(imapc_client, &capabilities) == 0);
+ test_assert(capabilities == (IMAPC_CAPABILITY_IMAP4REV1 |
+ IMAPC_CAPABILITY_UNSELECT |
+ IMAPC_CAPABILITY_QUOTA));
+}
+
+static void test_imapc_client_get_capabilities_server(void)
+{
+ test_server_wait_connection(&server, TRUE);
+ test_assert(test_imapc_server_expect(
+ "1 LOGIN \"testuser\" \"testpass\""));
+ o_stream_nsend_str(server.output, "1 OK \r\n");
+
+ test_assert(test_imapc_server_expect("2 LOGOUT"));
+ o_stream_nsend_str(server.output, "2 OK \r\n");
+
+ test_assert(i_stream_read_next_line(server.input) == NULL);
+}
+
+static void test_imapc_client_get_capabilities(void)
+{
+ struct imapc_client_settings set = test_imapc_default_settings;
+
+ test_begin("imapc_client_get_capabilities()");
+ test_run_client_server(&set, test_imapc_client_get_capabilities_client,
+ test_imapc_client_get_capabilities_server);
+ test_end();
+}
+
+/*
+ * imapc_client_get_capabilities() reconnected
+ */
+
+static void test_imapc_client_get_capabilities_reconnected_client(void)
+{
+ enum imapc_capability capabilities;
+
+ test_expect_error_string("Server disconnected unexpectedly");
+ test_assert(imapc_client_get_capabilities(imapc_client,
+ &capabilities) == 0);
+ test_assert(capabilities == (IMAPC_CAPABILITY_IMAP4REV1 |
+ IMAPC_CAPABILITY_UNSELECT |
+ IMAPC_CAPABILITY_QUOTA));
+ test_expect_no_more_errors();
+}
+
+static void test_imapc_client_get_capabilities_reconnected_server(void)
+{
+ test_server_wait_connection(&server, TRUE);
+ test_server_disconnect_and_wait(TRUE);
+
+ test_assert(test_imapc_server_expect(
+ "2 LOGIN \"testuser\" \"testpass\""));
+ o_stream_nsend_str(server.output, "2 OK \r\n");
+
+ test_assert(test_imapc_server_expect("3 LOGOUT"));
+ o_stream_nsend_str(server.output, "3 OK \r\n");
+
+ test_assert(i_stream_read_next_line(server.input) == NULL);
+}
+
+static void test_imapc_client_get_capabilities_reconnected(void)
+{
+ struct imapc_client_settings set = test_imapc_default_settings;
+
+ test_begin("imapc_client_get_capabilities() reconnected");
+
+ test_run_client_server(
+ &set, test_imapc_client_get_capabilities_reconnected_client,
+ test_imapc_client_get_capabilities_reconnected_server);
+ test_end();
+}
+
+/*
+ * imapc_client_get_capabilities() disconnected
+ */
+
+static void test_imapc_client_get_capabilities_disconnected_client(void)
+{
+ enum imapc_capability capabilities;
+
+ test_expect_errors(2);
+ test_assert(imapc_client_get_capabilities(imapc_client,
+ &capabilities) < 0);
+ test_expect_no_more_errors();
+}
+
+static void test_imapc_client_get_capabilities_disconnected_server(void)
+{
+ test_server_wait_connection(&server, TRUE);
+ test_server_disconnect_and_wait(TRUE);
+}
+
+static void test_imapc_client_get_capabilities_disconnected(void)
+{
+ struct imapc_client_settings set = test_imapc_default_settings;
+
+ test_begin("imapc_client_get_capabilities() disconnected");
+
+ test_run_client_server(
+ &set, test_imapc_client_get_capabilities_disconnected_client,
+ test_imapc_client_get_capabilities_disconnected_server);
+ test_end();
+}
+
+/*
+ * Main
+ */
+
+static void main_init(void)
+{
+ /* nothing yet */
+}
+
+static void main_deinit(void)
+{
+ /* nothing yet; also called from sub-processes */
+}
+
+int main(int argc ATTR_UNUSED, char *argv[])
+{
+ int c;
+ int ret;
+
+ static void (*const test_functions[])(void) = {
+ test_imapc_connect_failed,
+ test_imapc_banner_hangs,
+ test_imapc_login_hangs,
+ test_imapc_login_fails,
+ test_imapc_reconnect,
+ test_imapc_reconnect_resend_commands,
+ test_imapc_reconnect_resend_commands_failed,
+ test_imapc_reconnect_mailbox,
+ test_imapc_client_get_capabilities,
+ test_imapc_client_get_capabilities_reconnected,
+ test_imapc_client_get_capabilities_disconnected,
+ NULL
+ };
+
+ lib_init();
+ main_init();
+
+ while ((c = getopt(argc, argv, "D")) > 0) {
+ switch (c) {
+ case 'D':
+ debug = TRUE;
+ break;
+ default:
+ i_fatal("Usage: %s [-D]", argv[0]);
+ }
+ }
+
+ test_subprocesses_init(debug);
+ test_imapc_default_settings.debug = debug;
+
+ /* listen on localhost */
+ i_zero(&bind_ip);
+ bind_ip.family = AF_INET;
+ bind_ip.u.ip4.s_addr = htonl(INADDR_LOOPBACK);
+
+ ret = test_run(test_functions);
+
+ test_subprocesses_deinit();
+ main_deinit();
+ lib_deinit();
+
+ return ret;
+}
diff --git a/src/lib-imap-storage/Makefile.am b/src/lib-imap-storage/Makefile.am
new file mode 100644
index 0000000..b084653
--- /dev/null
+++ b/src/lib-imap-storage/Makefile.am
@@ -0,0 +1,24 @@
+noinst_LTLIBRARIES = libimap-storage.la
+
+AM_CPPFLAGS = \
+ -I$(top_srcdir)/src/lib \
+ -I$(top_srcdir)/src/lib-test \
+ -I$(top_srcdir)/src/lib-charset \
+ -I$(top_srcdir)/src/lib-mail \
+ -I$(top_srcdir)/src/lib-index \
+ -I$(top_srcdir)/src/lib-storage \
+ -I$(top_srcdir)/src/lib-imap
+
+libimap_storage_la_SOURCES = \
+ imap-msgpart.c \
+ imap-msgpart-url.c \
+ imap-metadata.c
+
+headers = \
+ imap-msgpart.h \
+ imap-msgpart-url.h \
+ imap-metadata.h
+
+pkginc_libdir=$(pkgincludedir)
+pkginc_lib_HEADERS = $(headers)
+
diff --git a/src/lib-imap-storage/Makefile.in b/src/lib-imap-storage/Makefile.in
new file mode 100644
index 0000000..c0750d9
--- /dev/null
+++ b/src/lib-imap-storage/Makefile.in
@@ -0,0 +1,825 @@
+# Makefile.in generated by automake 1.16.1 from Makefile.am.
+# @configure_input@
+
+# Copyright (C) 1994-2018 Free Software Foundation, Inc.
+
+# This Makefile.in is free software; the Free Software Foundation
+# gives unlimited permission to copy and/or distribute it,
+# with or without modifications, as long as this notice is preserved.
+
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY, to the extent permitted by law; without
+# even the implied warranty of MERCHANTABILITY or FITNESS FOR A
+# PARTICULAR PURPOSE.
+
+@SET_MAKE@
+
+
+VPATH = @srcdir@
+am__is_gnu_make = { \
+ if test -z '$(MAKELEVEL)'; then \
+ false; \
+ elif test -n '$(MAKE_HOST)'; then \
+ true; \
+ elif test -n '$(MAKE_VERSION)' && test -n '$(CURDIR)'; then \
+ true; \
+ else \
+ false; \
+ fi; \
+}
+am__make_running_with_option = \
+ case $${target_option-} in \
+ ?) ;; \
+ *) echo "am__make_running_with_option: internal error: invalid" \
+ "target option '$${target_option-}' specified" >&2; \
+ exit 1;; \
+ esac; \
+ has_opt=no; \
+ sane_makeflags=$$MAKEFLAGS; \
+ if $(am__is_gnu_make); then \
+ sane_makeflags=$$MFLAGS; \
+ else \
+ case $$MAKEFLAGS in \
+ *\\[\ \ ]*) \
+ bs=\\; \
+ sane_makeflags=`printf '%s\n' "$$MAKEFLAGS" \
+ | sed "s/$$bs$$bs[$$bs $$bs ]*//g"`;; \
+ esac; \
+ fi; \
+ skip_next=no; \
+ strip_trailopt () \
+ { \
+ flg=`printf '%s\n' "$$flg" | sed "s/$$1.*$$//"`; \
+ }; \
+ for flg in $$sane_makeflags; do \
+ test $$skip_next = yes && { skip_next=no; continue; }; \
+ case $$flg in \
+ *=*|--*) continue;; \
+ -*I) strip_trailopt 'I'; skip_next=yes;; \
+ -*I?*) strip_trailopt 'I';; \
+ -*O) strip_trailopt 'O'; skip_next=yes;; \
+ -*O?*) strip_trailopt 'O';; \
+ -*l) strip_trailopt 'l'; skip_next=yes;; \
+ -*l?*) strip_trailopt 'l';; \
+ -[dEDm]) skip_next=yes;; \
+ -[JT]) skip_next=yes;; \
+ esac; \
+ case $$flg in \
+ *$$target_option*) has_opt=yes; break;; \
+ esac; \
+ done; \
+ test $$has_opt = yes
+am__make_dryrun = (target_option=n; $(am__make_running_with_option))
+am__make_keepgoing = (target_option=k; $(am__make_running_with_option))
+pkgdatadir = $(datadir)/@PACKAGE@
+pkgincludedir = $(includedir)/@PACKAGE@
+pkglibdir = $(libdir)/@PACKAGE@
+pkglibexecdir = $(libexecdir)/@PACKAGE@
+am__cd = CDPATH="$${ZSH_VERSION+.}$(PATH_SEPARATOR)" && cd
+install_sh_DATA = $(install_sh) -c -m 644
+install_sh_PROGRAM = $(install_sh) -c
+install_sh_SCRIPT = $(install_sh) -c
+INSTALL_HEADER = $(INSTALL_DATA)
+transform = $(program_transform_name)
+NORMAL_INSTALL = :
+PRE_INSTALL = :
+POST_INSTALL = :
+NORMAL_UNINSTALL = :
+PRE_UNINSTALL = :
+POST_UNINSTALL = :
+build_triplet = @build@
+host_triplet = @host@
+subdir = src/lib-imap-storage
+ACLOCAL_M4 = $(top_srcdir)/aclocal.m4
+am__aclocal_m4_deps = $(top_srcdir)/m4/ac_checktype2.m4 \
+ $(top_srcdir)/m4/ac_typeof.m4 $(top_srcdir)/m4/arc4random.m4 \
+ $(top_srcdir)/m4/blockdev.m4 $(top_srcdir)/m4/c99_vsnprintf.m4 \
+ $(top_srcdir)/m4/clock_gettime.m4 $(top_srcdir)/m4/crypt.m4 \
+ $(top_srcdir)/m4/crypt_xpg6.m4 $(top_srcdir)/m4/dbqlk.m4 \
+ $(top_srcdir)/m4/dirent_dtype.m4 $(top_srcdir)/m4/dovecot.m4 \
+ $(top_srcdir)/m4/fd_passing.m4 $(top_srcdir)/m4/fdatasync.m4 \
+ $(top_srcdir)/m4/flexible_array_member.m4 \
+ $(top_srcdir)/m4/glibc.m4 $(top_srcdir)/m4/gmtime_max.m4 \
+ $(top_srcdir)/m4/gmtime_tm_gmtoff.m4 \
+ $(top_srcdir)/m4/ioloop.m4 $(top_srcdir)/m4/iovec.m4 \
+ $(top_srcdir)/m4/ipv6.m4 $(top_srcdir)/m4/libcap.m4 \
+ $(top_srcdir)/m4/libtool.m4 $(top_srcdir)/m4/libwrap.m4 \
+ $(top_srcdir)/m4/linux_mremap.m4 $(top_srcdir)/m4/ltoptions.m4 \
+ $(top_srcdir)/m4/ltsugar.m4 $(top_srcdir)/m4/ltversion.m4 \
+ $(top_srcdir)/m4/lt~obsolete.m4 $(top_srcdir)/m4/mmap_write.m4 \
+ $(top_srcdir)/m4/mntctl.m4 $(top_srcdir)/m4/modules.m4 \
+ $(top_srcdir)/m4/notify.m4 $(top_srcdir)/m4/nsl.m4 \
+ $(top_srcdir)/m4/off_t_max.m4 $(top_srcdir)/m4/pkg.m4 \
+ $(top_srcdir)/m4/pr_set_dumpable.m4 \
+ $(top_srcdir)/m4/q_quotactl.m4 $(top_srcdir)/m4/quota.m4 \
+ $(top_srcdir)/m4/random.m4 $(top_srcdir)/m4/rlimit.m4 \
+ $(top_srcdir)/m4/sendfile.m4 $(top_srcdir)/m4/size_t_signed.m4 \
+ $(top_srcdir)/m4/sockpeercred.m4 $(top_srcdir)/m4/sql.m4 \
+ $(top_srcdir)/m4/ssl.m4 $(top_srcdir)/m4/st_tim.m4 \
+ $(top_srcdir)/m4/static_array.m4 $(top_srcdir)/m4/test_with.m4 \
+ $(top_srcdir)/m4/time_t.m4 $(top_srcdir)/m4/typeof.m4 \
+ $(top_srcdir)/m4/typeof_dev_t.m4 \
+ $(top_srcdir)/m4/uoff_t_max.m4 $(top_srcdir)/m4/vararg.m4 \
+ $(top_srcdir)/m4/want_apparmor.m4 \
+ $(top_srcdir)/m4/want_bsdauth.m4 \
+ $(top_srcdir)/m4/want_bzlib.m4 \
+ $(top_srcdir)/m4/want_cassandra.m4 \
+ $(top_srcdir)/m4/want_cdb.m4 \
+ $(top_srcdir)/m4/want_checkpassword.m4 \
+ $(top_srcdir)/m4/want_clucene.m4 $(top_srcdir)/m4/want_db.m4 \
+ $(top_srcdir)/m4/want_gssapi.m4 $(top_srcdir)/m4/want_icu.m4 \
+ $(top_srcdir)/m4/want_ldap.m4 $(top_srcdir)/m4/want_lua.m4 \
+ $(top_srcdir)/m4/want_lz4.m4 $(top_srcdir)/m4/want_lzma.m4 \
+ $(top_srcdir)/m4/want_mysql.m4 $(top_srcdir)/m4/want_pam.m4 \
+ $(top_srcdir)/m4/want_passwd.m4 $(top_srcdir)/m4/want_pgsql.m4 \
+ $(top_srcdir)/m4/want_prefetch.m4 \
+ $(top_srcdir)/m4/want_shadow.m4 \
+ $(top_srcdir)/m4/want_sodium.m4 $(top_srcdir)/m4/want_solr.m4 \
+ $(top_srcdir)/m4/want_sqlite.m4 \
+ $(top_srcdir)/m4/want_stemmer.m4 \
+ $(top_srcdir)/m4/want_systemd.m4 \
+ $(top_srcdir)/m4/want_textcat.m4 \
+ $(top_srcdir)/m4/want_unwind.m4 $(top_srcdir)/m4/want_zlib.m4 \
+ $(top_srcdir)/m4/want_zstd.m4 $(top_srcdir)/configure.ac
+am__configure_deps = $(am__aclocal_m4_deps) $(CONFIGURE_DEPENDENCIES) \
+ $(ACLOCAL_M4)
+DIST_COMMON = $(srcdir)/Makefile.am $(pkginc_lib_HEADERS) \
+ $(am__DIST_COMMON)
+mkinstalldirs = $(install_sh) -d
+CONFIG_HEADER = $(top_builddir)/config.h
+CONFIG_CLEAN_FILES =
+CONFIG_CLEAN_VPATH_FILES =
+LTLIBRARIES = $(noinst_LTLIBRARIES)
+libimap_storage_la_LIBADD =
+am_libimap_storage_la_OBJECTS = imap-msgpart.lo imap-msgpart-url.lo \
+ imap-metadata.lo
+libimap_storage_la_OBJECTS = $(am_libimap_storage_la_OBJECTS)
+AM_V_lt = $(am__v_lt_@AM_V@)
+am__v_lt_ = $(am__v_lt_@AM_DEFAULT_V@)
+am__v_lt_0 = --silent
+am__v_lt_1 =
+AM_V_P = $(am__v_P_@AM_V@)
+am__v_P_ = $(am__v_P_@AM_DEFAULT_V@)
+am__v_P_0 = false
+am__v_P_1 = :
+AM_V_GEN = $(am__v_GEN_@AM_V@)
+am__v_GEN_ = $(am__v_GEN_@AM_DEFAULT_V@)
+am__v_GEN_0 = @echo " GEN " $@;
+am__v_GEN_1 =
+AM_V_at = $(am__v_at_@AM_V@)
+am__v_at_ = $(am__v_at_@AM_DEFAULT_V@)
+am__v_at_0 = @
+am__v_at_1 =
+DEFAULT_INCLUDES = -I.@am__isrc@ -I$(top_builddir)
+depcomp = $(SHELL) $(top_srcdir)/depcomp
+am__maybe_remake_depfiles = depfiles
+am__depfiles_remade = ./$(DEPDIR)/imap-metadata.Plo \
+ ./$(DEPDIR)/imap-msgpart-url.Plo ./$(DEPDIR)/imap-msgpart.Plo
+am__mv = mv -f
+COMPILE = $(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) \
+ $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS)
+LTCOMPILE = $(LIBTOOL) $(AM_V_lt) --tag=CC $(AM_LIBTOOLFLAGS) \
+ $(LIBTOOLFLAGS) --mode=compile $(CC) $(DEFS) \
+ $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) $(CPPFLAGS) \
+ $(AM_CFLAGS) $(CFLAGS)
+AM_V_CC = $(am__v_CC_@AM_V@)
+am__v_CC_ = $(am__v_CC_@AM_DEFAULT_V@)
+am__v_CC_0 = @echo " CC " $@;
+am__v_CC_1 =
+CCLD = $(CC)
+LINK = $(LIBTOOL) $(AM_V_lt) --tag=CC $(AM_LIBTOOLFLAGS) \
+ $(LIBTOOLFLAGS) --mode=link $(CCLD) $(AM_CFLAGS) $(CFLAGS) \
+ $(AM_LDFLAGS) $(LDFLAGS) -o $@
+AM_V_CCLD = $(am__v_CCLD_@AM_V@)
+am__v_CCLD_ = $(am__v_CCLD_@AM_DEFAULT_V@)
+am__v_CCLD_0 = @echo " CCLD " $@;
+am__v_CCLD_1 =
+SOURCES = $(libimap_storage_la_SOURCES)
+DIST_SOURCES = $(libimap_storage_la_SOURCES)
+am__can_run_installinfo = \
+ case $$AM_UPDATE_INFO_DIR in \
+ n|no|NO) false;; \
+ *) (install-info --version) >/dev/null 2>&1;; \
+ esac
+am__vpath_adj_setup = srcdirstrip=`echo "$(srcdir)" | sed 's|.|.|g'`;
+am__vpath_adj = case $$p in \
+ $(srcdir)/*) f=`echo "$$p" | sed "s|^$$srcdirstrip/||"`;; \
+ *) f=$$p;; \
+ esac;
+am__strip_dir = f=`echo $$p | sed -e 's|^.*/||'`;
+am__install_max = 40
+am__nobase_strip_setup = \
+ srcdirstrip=`echo "$(srcdir)" | sed 's/[].[^$$\\*|]/\\\\&/g'`
+am__nobase_strip = \
+ for p in $$list; do echo "$$p"; done | sed -e "s|$$srcdirstrip/||"
+am__nobase_list = $(am__nobase_strip_setup); \
+ for p in $$list; do echo "$$p $$p"; done | \
+ sed "s| $$srcdirstrip/| |;"' / .*\//!s/ .*/ ./; s,\( .*\)/[^/]*$$,\1,' | \
+ $(AWK) 'BEGIN { files["."] = "" } { files[$$2] = files[$$2] " " $$1; \
+ if (++n[$$2] == $(am__install_max)) \
+ { print $$2, files[$$2]; n[$$2] = 0; files[$$2] = "" } } \
+ END { for (dir in files) print dir, files[dir] }'
+am__base_list = \
+ sed '$$!N;$$!N;$$!N;$$!N;$$!N;$$!N;$$!N;s/\n/ /g' | \
+ sed '$$!N;$$!N;$$!N;$$!N;s/\n/ /g'
+am__uninstall_files_from_dir = { \
+ test -z "$$files" \
+ || { test ! -d "$$dir" && test ! -f "$$dir" && test ! -r "$$dir"; } \
+ || { echo " ( cd '$$dir' && rm -f" $$files ")"; \
+ $(am__cd) "$$dir" && rm -f $$files; }; \
+ }
+am__installdirs = "$(DESTDIR)$(pkginc_libdir)"
+HEADERS = $(pkginc_lib_HEADERS)
+am__tagged_files = $(HEADERS) $(SOURCES) $(TAGS_FILES) $(LISP)
+# Read a list of newline-separated strings from the standard input,
+# and print each of them once, without duplicates. Input order is
+# *not* preserved.
+am__uniquify_input = $(AWK) '\
+ BEGIN { nonempty = 0; } \
+ { items[$$0] = 1; nonempty = 1; } \
+ END { if (nonempty) { for (i in items) print i; }; } \
+'
+# Make sure the list of sources is unique. This is necessary because,
+# e.g., the same source file might be shared among _SOURCES variables
+# for different programs/libraries.
+am__define_uniq_tagged_files = \
+ list='$(am__tagged_files)'; \
+ unique=`for i in $$list; do \
+ if test -f "$$i"; then echo $$i; else echo $(srcdir)/$$i; fi; \
+ done | $(am__uniquify_input)`
+ETAGS = etags
+CTAGS = ctags
+am__DIST_COMMON = $(srcdir)/Makefile.in $(top_srcdir)/depcomp
+DISTFILES = $(DIST_COMMON) $(DIST_SOURCES) $(TEXINFOS) $(EXTRA_DIST)
+ACLOCAL = @ACLOCAL@
+ACLOCAL_AMFLAGS = @ACLOCAL_AMFLAGS@
+AMTAR = @AMTAR@
+AM_DEFAULT_VERBOSITY = @AM_DEFAULT_VERBOSITY@
+APPARMOR_LIBS = @APPARMOR_LIBS@
+AR = @AR@
+AUTH_CFLAGS = @AUTH_CFLAGS@
+AUTH_LIBS = @AUTH_LIBS@
+AUTOCONF = @AUTOCONF@
+AUTOHEADER = @AUTOHEADER@
+AUTOMAKE = @AUTOMAKE@
+AWK = @AWK@
+BINARY_CFLAGS = @BINARY_CFLAGS@
+BINARY_LDFLAGS = @BINARY_LDFLAGS@
+BISON = @BISON@
+CASSANDRA_CFLAGS = @CASSANDRA_CFLAGS@
+CASSANDRA_LIBS = @CASSANDRA_LIBS@
+CC = @CC@
+CCDEPMODE = @CCDEPMODE@
+CDB_LIBS = @CDB_LIBS@
+CFLAGS = @CFLAGS@
+CLUCENE_CFLAGS = @CLUCENE_CFLAGS@
+CLUCENE_LIBS = @CLUCENE_LIBS@
+COMPRESS_LIBS = @COMPRESS_LIBS@
+CPP = @CPP@
+CPPFLAGS = @CPPFLAGS@
+CRYPT_LIBS = @CRYPT_LIBS@
+CXX = @CXX@
+CXXCPP = @CXXCPP@
+CXXDEPMODE = @CXXDEPMODE@
+CXXFLAGS = @CXXFLAGS@
+CYGPATH_W = @CYGPATH_W@
+DEFS = @DEFS@
+DEPDIR = @DEPDIR@
+DICT_LIBS = @DICT_LIBS@
+DLLIB = @DLLIB@
+DLLTOOL = @DLLTOOL@
+DSYMUTIL = @DSYMUTIL@
+DUMPBIN = @DUMPBIN@
+ECHO_C = @ECHO_C@
+ECHO_N = @ECHO_N@
+ECHO_T = @ECHO_T@
+EGREP = @EGREP@
+EXEEXT = @EXEEXT@
+FGREP = @FGREP@
+FLEX = @FLEX@
+FUZZER_CPPFLAGS = @FUZZER_CPPFLAGS@
+FUZZER_LDFLAGS = @FUZZER_LDFLAGS@
+GREP = @GREP@
+INSTALL = @INSTALL@
+INSTALL_DATA = @INSTALL_DATA@
+INSTALL_PROGRAM = @INSTALL_PROGRAM@
+INSTALL_SCRIPT = @INSTALL_SCRIPT@
+INSTALL_STRIP_PROGRAM = @INSTALL_STRIP_PROGRAM@
+KRB5CONFIG = @KRB5CONFIG@
+KRB5_CFLAGS = @KRB5_CFLAGS@
+KRB5_LIBS = @KRB5_LIBS@
+LD = @LD@
+LDAP_LIBS = @LDAP_LIBS@
+LDFLAGS = @LDFLAGS@
+LD_NO_WHOLE_ARCHIVE = @LD_NO_WHOLE_ARCHIVE@
+LD_WHOLE_ARCHIVE = @LD_WHOLE_ARCHIVE@
+LIBCAP = @LIBCAP@
+LIBDOVECOT = @LIBDOVECOT@
+LIBDOVECOT_COMPRESS = @LIBDOVECOT_COMPRESS@
+LIBDOVECOT_DEPS = @LIBDOVECOT_DEPS@
+LIBDOVECOT_DSYNC = @LIBDOVECOT_DSYNC@
+LIBDOVECOT_LA_LIBS = @LIBDOVECOT_LA_LIBS@
+LIBDOVECOT_LDA = @LIBDOVECOT_LDA@
+LIBDOVECOT_LDAP = @LIBDOVECOT_LDAP@
+LIBDOVECOT_LIBFTS = @LIBDOVECOT_LIBFTS@
+LIBDOVECOT_LIBFTS_DEPS = @LIBDOVECOT_LIBFTS_DEPS@
+LIBDOVECOT_LOGIN = @LIBDOVECOT_LOGIN@
+LIBDOVECOT_LUA = @LIBDOVECOT_LUA@
+LIBDOVECOT_LUA_DEPS = @LIBDOVECOT_LUA_DEPS@
+LIBDOVECOT_SQL = @LIBDOVECOT_SQL@
+LIBDOVECOT_STORAGE = @LIBDOVECOT_STORAGE@
+LIBDOVECOT_STORAGE_DEPS = @LIBDOVECOT_STORAGE_DEPS@
+LIBEXTTEXTCAT_CFLAGS = @LIBEXTTEXTCAT_CFLAGS@
+LIBEXTTEXTCAT_LIBS = @LIBEXTTEXTCAT_LIBS@
+LIBICONV = @LIBICONV@
+LIBICU_CFLAGS = @LIBICU_CFLAGS@
+LIBICU_LIBS = @LIBICU_LIBS@
+LIBOBJS = @LIBOBJS@
+LIBS = @LIBS@
+LIBSODIUM_CFLAGS = @LIBSODIUM_CFLAGS@
+LIBSODIUM_LIBS = @LIBSODIUM_LIBS@
+LIBTIRPC_CFLAGS = @LIBTIRPC_CFLAGS@
+LIBTIRPC_LIBS = @LIBTIRPC_LIBS@
+LIBTOOL = @LIBTOOL@
+LIBUNWIND_CFLAGS = @LIBUNWIND_CFLAGS@
+LIBUNWIND_LIBS = @LIBUNWIND_LIBS@
+LIBWRAP_LIBS = @LIBWRAP_LIBS@
+LINKED_STORAGE_LDADD = @LINKED_STORAGE_LDADD@
+LIPO = @LIPO@
+LN_S = @LN_S@
+LTLIBICONV = @LTLIBICONV@
+LTLIBOBJS = @LTLIBOBJS@
+LT_SYS_LIBRARY_PATH = @LT_SYS_LIBRARY_PATH@
+LUA_CFLAGS = @LUA_CFLAGS@
+LUA_LIBS = @LUA_LIBS@
+MAINT = @MAINT@
+MAKEINFO = @MAKEINFO@
+MANIFEST_TOOL = @MANIFEST_TOOL@
+MKDIR_P = @MKDIR_P@
+MODULE_LIBS = @MODULE_LIBS@
+MODULE_SUFFIX = @MODULE_SUFFIX@
+MYSQL_CFLAGS = @MYSQL_CFLAGS@
+MYSQL_CONFIG = @MYSQL_CONFIG@
+MYSQL_LIBS = @MYSQL_LIBS@
+NM = @NM@
+NMEDIT = @NMEDIT@
+NOPLUGIN_LDFLAGS = @NOPLUGIN_LDFLAGS@
+OBJDUMP = @OBJDUMP@
+OBJEXT = @OBJEXT@
+OTOOL = @OTOOL@
+OTOOL64 = @OTOOL64@
+PACKAGE = @PACKAGE@
+PACKAGE_BUGREPORT = @PACKAGE_BUGREPORT@
+PACKAGE_NAME = @PACKAGE_NAME@
+PACKAGE_STRING = @PACKAGE_STRING@
+PACKAGE_TARNAME = @PACKAGE_TARNAME@
+PACKAGE_URL = @PACKAGE_URL@
+PACKAGE_VERSION = @PACKAGE_VERSION@
+PANDOC = @PANDOC@
+PATH_SEPARATOR = @PATH_SEPARATOR@
+PGSQL_CFLAGS = @PGSQL_CFLAGS@
+PGSQL_LIBS = @PGSQL_LIBS@
+PG_CONFIG = @PG_CONFIG@
+PIE_CFLAGS = @PIE_CFLAGS@
+PIE_LDFLAGS = @PIE_LDFLAGS@
+PKG_CONFIG = @PKG_CONFIG@
+PKG_CONFIG_LIBDIR = @PKG_CONFIG_LIBDIR@
+PKG_CONFIG_PATH = @PKG_CONFIG_PATH@
+QUOTA_LIBS = @QUOTA_LIBS@
+RANLIB = @RANLIB@
+RELRO_LDFLAGS = @RELRO_LDFLAGS@
+RPCGEN = @RPCGEN@
+RUN_TEST = @RUN_TEST@
+SED = @SED@
+SETTING_FILES = @SETTING_FILES@
+SET_MAKE = @SET_MAKE@
+SHELL = @SHELL@
+SQLITE_CFLAGS = @SQLITE_CFLAGS@
+SQLITE_LIBS = @SQLITE_LIBS@
+SQL_CFLAGS = @SQL_CFLAGS@
+SQL_LIBS = @SQL_LIBS@
+SSL_CFLAGS = @SSL_CFLAGS@
+SSL_LIBS = @SSL_LIBS@
+STRIP = @STRIP@
+SYSTEMD_CFLAGS = @SYSTEMD_CFLAGS@
+SYSTEMD_LIBS = @SYSTEMD_LIBS@
+VALGRIND = @VALGRIND@
+VERSION = @VERSION@
+ZSTD_CFLAGS = @ZSTD_CFLAGS@
+ZSTD_LIBS = @ZSTD_LIBS@
+abs_builddir = @abs_builddir@
+abs_srcdir = @abs_srcdir@
+abs_top_builddir = @abs_top_builddir@
+abs_top_srcdir = @abs_top_srcdir@
+ac_ct_AR = @ac_ct_AR@
+ac_ct_CC = @ac_ct_CC@
+ac_ct_CXX = @ac_ct_CXX@
+ac_ct_DUMPBIN = @ac_ct_DUMPBIN@
+am__include = @am__include@
+am__leading_dot = @am__leading_dot@
+am__quote = @am__quote@
+am__tar = @am__tar@
+am__untar = @am__untar@
+bindir = @bindir@
+build = @build@
+build_alias = @build_alias@
+build_cpu = @build_cpu@
+build_os = @build_os@
+build_vendor = @build_vendor@
+builddir = @builddir@
+datadir = @datadir@
+datarootdir = @datarootdir@
+dict_drivers = @dict_drivers@
+docdir = @docdir@
+dvidir = @dvidir@
+exec_prefix = @exec_prefix@
+host = @host@
+host_alias = @host_alias@
+host_cpu = @host_cpu@
+host_os = @host_os@
+host_vendor = @host_vendor@
+htmldir = @htmldir@
+includedir = @includedir@
+infodir = @infodir@
+install_sh = @install_sh@
+libdir = @libdir@
+libexecdir = @libexecdir@
+localedir = @localedir@
+localstatedir = @localstatedir@
+mandir = @mandir@
+mkdir_p = @mkdir_p@
+moduledir = @moduledir@
+oldincludedir = @oldincludedir@
+pdfdir = @pdfdir@
+prefix = @prefix@
+program_transform_name = @program_transform_name@
+psdir = @psdir@
+rundir = @rundir@
+runstatedir = @runstatedir@
+sbindir = @sbindir@
+sharedstatedir = @sharedstatedir@
+sql_drivers = @sql_drivers@
+srcdir = @srcdir@
+ssldir = @ssldir@
+statedir = @statedir@
+sysconfdir = @sysconfdir@
+systemdservicetype = @systemdservicetype@
+systemdsystemunitdir = @systemdsystemunitdir@
+target_alias = @target_alias@
+top_build_prefix = @top_build_prefix@
+top_builddir = @top_builddir@
+top_srcdir = @top_srcdir@
+noinst_LTLIBRARIES = libimap-storage.la
+AM_CPPFLAGS = \
+ -I$(top_srcdir)/src/lib \
+ -I$(top_srcdir)/src/lib-test \
+ -I$(top_srcdir)/src/lib-charset \
+ -I$(top_srcdir)/src/lib-mail \
+ -I$(top_srcdir)/src/lib-index \
+ -I$(top_srcdir)/src/lib-storage \
+ -I$(top_srcdir)/src/lib-imap
+
+libimap_storage_la_SOURCES = \
+ imap-msgpart.c \
+ imap-msgpart-url.c \
+ imap-metadata.c
+
+headers = \
+ imap-msgpart.h \
+ imap-msgpart-url.h \
+ imap-metadata.h
+
+pkginc_libdir = $(pkgincludedir)
+pkginc_lib_HEADERS = $(headers)
+all: all-am
+
+.SUFFIXES:
+.SUFFIXES: .c .lo .o .obj
+$(srcdir)/Makefile.in: @MAINTAINER_MODE_TRUE@ $(srcdir)/Makefile.am $(am__configure_deps)
+ @for dep in $?; do \
+ case '$(am__configure_deps)' in \
+ *$$dep*) \
+ ( cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh ) \
+ && { if test -f $@; then exit 0; else break; fi; }; \
+ exit 1;; \
+ esac; \
+ done; \
+ echo ' cd $(top_srcdir) && $(AUTOMAKE) --foreign src/lib-imap-storage/Makefile'; \
+ $(am__cd) $(top_srcdir) && \
+ $(AUTOMAKE) --foreign src/lib-imap-storage/Makefile
+Makefile: $(srcdir)/Makefile.in $(top_builddir)/config.status
+ @case '$?' in \
+ *config.status*) \
+ cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh;; \
+ *) \
+ echo ' cd $(top_builddir) && $(SHELL) ./config.status $(subdir)/$@ $(am__maybe_remake_depfiles)'; \
+ cd $(top_builddir) && $(SHELL) ./config.status $(subdir)/$@ $(am__maybe_remake_depfiles);; \
+ esac;
+
+$(top_builddir)/config.status: $(top_srcdir)/configure $(CONFIG_STATUS_DEPENDENCIES)
+ cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh
+
+$(top_srcdir)/configure: @MAINTAINER_MODE_TRUE@ $(am__configure_deps)
+ cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh
+$(ACLOCAL_M4): @MAINTAINER_MODE_TRUE@ $(am__aclocal_m4_deps)
+ cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh
+$(am__aclocal_m4_deps):
+
+clean-noinstLTLIBRARIES:
+ -test -z "$(noinst_LTLIBRARIES)" || rm -f $(noinst_LTLIBRARIES)
+ @list='$(noinst_LTLIBRARIES)'; \
+ locs=`for p in $$list; do echo $$p; done | \
+ sed 's|^[^/]*$$|.|; s|/[^/]*$$||; s|$$|/so_locations|' | \
+ sort -u`; \
+ test -z "$$locs" || { \
+ echo rm -f $${locs}; \
+ rm -f $${locs}; \
+ }
+
+libimap-storage.la: $(libimap_storage_la_OBJECTS) $(libimap_storage_la_DEPENDENCIES) $(EXTRA_libimap_storage_la_DEPENDENCIES)
+ $(AM_V_CCLD)$(LINK) $(libimap_storage_la_OBJECTS) $(libimap_storage_la_LIBADD) $(LIBS)
+
+mostlyclean-compile:
+ -rm -f *.$(OBJEXT)
+
+distclean-compile:
+ -rm -f *.tab.c
+
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/imap-metadata.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/imap-msgpart-url.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/imap-msgpart.Plo@am__quote@ # am--include-marker
+
+$(am__depfiles_remade):
+ @$(MKDIR_P) $(@D)
+ @echo '# dummy' >$@-t && $(am__mv) $@-t $@
+
+am--depfiles: $(am__depfiles_remade)
+
+.c.o:
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(COMPILE) -MT $@ -MD -MP -MF $(DEPDIR)/$*.Tpo -c -o $@ $<
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/$*.Tpo $(DEPDIR)/$*.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='$<' object='$@' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(COMPILE) -c -o $@ $<
+
+.c.obj:
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(COMPILE) -MT $@ -MD -MP -MF $(DEPDIR)/$*.Tpo -c -o $@ `$(CYGPATH_W) '$<'`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/$*.Tpo $(DEPDIR)/$*.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='$<' object='$@' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(COMPILE) -c -o $@ `$(CYGPATH_W) '$<'`
+
+.c.lo:
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(LTCOMPILE) -MT $@ -MD -MP -MF $(DEPDIR)/$*.Tpo -c -o $@ $<
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/$*.Tpo $(DEPDIR)/$*.Plo
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='$<' object='$@' libtool=yes @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(LTCOMPILE) -c -o $@ $<
+
+mostlyclean-libtool:
+ -rm -f *.lo
+
+clean-libtool:
+ -rm -rf .libs _libs
+install-pkginc_libHEADERS: $(pkginc_lib_HEADERS)
+ @$(NORMAL_INSTALL)
+ @list='$(pkginc_lib_HEADERS)'; test -n "$(pkginc_libdir)" || list=; \
+ if test -n "$$list"; then \
+ echo " $(MKDIR_P) '$(DESTDIR)$(pkginc_libdir)'"; \
+ $(MKDIR_P) "$(DESTDIR)$(pkginc_libdir)" || exit 1; \
+ fi; \
+ for p in $$list; do \
+ if test -f "$$p"; then d=; else d="$(srcdir)/"; fi; \
+ echo "$$d$$p"; \
+ done | $(am__base_list) | \
+ while read files; do \
+ echo " $(INSTALL_HEADER) $$files '$(DESTDIR)$(pkginc_libdir)'"; \
+ $(INSTALL_HEADER) $$files "$(DESTDIR)$(pkginc_libdir)" || exit $$?; \
+ done
+
+uninstall-pkginc_libHEADERS:
+ @$(NORMAL_UNINSTALL)
+ @list='$(pkginc_lib_HEADERS)'; test -n "$(pkginc_libdir)" || list=; \
+ files=`for p in $$list; do echo $$p; done | sed -e 's|^.*/||'`; \
+ dir='$(DESTDIR)$(pkginc_libdir)'; $(am__uninstall_files_from_dir)
+
+ID: $(am__tagged_files)
+ $(am__define_uniq_tagged_files); mkid -fID $$unique
+tags: tags-am
+TAGS: tags
+
+tags-am: $(TAGS_DEPENDENCIES) $(am__tagged_files)
+ set x; \
+ here=`pwd`; \
+ $(am__define_uniq_tagged_files); \
+ shift; \
+ if test -z "$(ETAGS_ARGS)$$*$$unique"; then :; else \
+ test -n "$$unique" || unique=$$empty_fix; \
+ if test $$# -gt 0; then \
+ $(ETAGS) $(ETAGSFLAGS) $(AM_ETAGSFLAGS) $(ETAGS_ARGS) \
+ "$$@" $$unique; \
+ else \
+ $(ETAGS) $(ETAGSFLAGS) $(AM_ETAGSFLAGS) $(ETAGS_ARGS) \
+ $$unique; \
+ fi; \
+ fi
+ctags: ctags-am
+
+CTAGS: ctags
+ctags-am: $(TAGS_DEPENDENCIES) $(am__tagged_files)
+ $(am__define_uniq_tagged_files); \
+ test -z "$(CTAGS_ARGS)$$unique" \
+ || $(CTAGS) $(CTAGSFLAGS) $(AM_CTAGSFLAGS) $(CTAGS_ARGS) \
+ $$unique
+
+GTAGS:
+ here=`$(am__cd) $(top_builddir) && pwd` \
+ && $(am__cd) $(top_srcdir) \
+ && gtags -i $(GTAGS_ARGS) "$$here"
+cscopelist: cscopelist-am
+
+cscopelist-am: $(am__tagged_files)
+ list='$(am__tagged_files)'; \
+ case "$(srcdir)" in \
+ [\\/]* | ?:[\\/]*) sdir="$(srcdir)" ;; \
+ *) sdir=$(subdir)/$(srcdir) ;; \
+ esac; \
+ for i in $$list; do \
+ if test -f "$$i"; then \
+ echo "$(subdir)/$$i"; \
+ else \
+ echo "$$sdir/$$i"; \
+ fi; \
+ done >> $(top_builddir)/cscope.files
+
+distclean-tags:
+ -rm -f TAGS ID GTAGS GRTAGS GSYMS GPATH tags
+
+distdir: $(BUILT_SOURCES)
+ $(MAKE) $(AM_MAKEFLAGS) distdir-am
+
+distdir-am: $(DISTFILES)
+ @srcdirstrip=`echo "$(srcdir)" | sed 's/[].[^$$\\*]/\\\\&/g'`; \
+ topsrcdirstrip=`echo "$(top_srcdir)" | sed 's/[].[^$$\\*]/\\\\&/g'`; \
+ list='$(DISTFILES)'; \
+ dist_files=`for file in $$list; do echo $$file; done | \
+ sed -e "s|^$$srcdirstrip/||;t" \
+ -e "s|^$$topsrcdirstrip/|$(top_builddir)/|;t"`; \
+ case $$dist_files in \
+ */*) $(MKDIR_P) `echo "$$dist_files" | \
+ sed '/\//!d;s|^|$(distdir)/|;s,/[^/]*$$,,' | \
+ sort -u` ;; \
+ esac; \
+ for file in $$dist_files; do \
+ if test -f $$file || test -d $$file; then d=.; else d=$(srcdir); fi; \
+ if test -d $$d/$$file; then \
+ dir=`echo "/$$file" | sed -e 's,/[^/]*$$,,'`; \
+ if test -d "$(distdir)/$$file"; then \
+ find "$(distdir)/$$file" -type d ! -perm -700 -exec chmod u+rwx {} \;; \
+ fi; \
+ if test -d $(srcdir)/$$file && test $$d != $(srcdir); then \
+ cp -fpR $(srcdir)/$$file "$(distdir)$$dir" || exit 1; \
+ find "$(distdir)/$$file" -type d ! -perm -700 -exec chmod u+rwx {} \;; \
+ fi; \
+ cp -fpR $$d/$$file "$(distdir)$$dir" || exit 1; \
+ else \
+ test -f "$(distdir)/$$file" \
+ || cp -p $$d/$$file "$(distdir)/$$file" \
+ || exit 1; \
+ fi; \
+ done
+check-am: all-am
+check: check-am
+all-am: Makefile $(LTLIBRARIES) $(HEADERS)
+installdirs:
+ for dir in "$(DESTDIR)$(pkginc_libdir)"; do \
+ test -z "$$dir" || $(MKDIR_P) "$$dir"; \
+ done
+install: install-am
+install-exec: install-exec-am
+install-data: install-data-am
+uninstall: uninstall-am
+
+install-am: all-am
+ @$(MAKE) $(AM_MAKEFLAGS) install-exec-am install-data-am
+
+installcheck: installcheck-am
+install-strip:
+ if test -z '$(STRIP)'; then \
+ $(MAKE) $(AM_MAKEFLAGS) INSTALL_PROGRAM="$(INSTALL_STRIP_PROGRAM)" \
+ install_sh_PROGRAM="$(INSTALL_STRIP_PROGRAM)" INSTALL_STRIP_FLAG=-s \
+ install; \
+ else \
+ $(MAKE) $(AM_MAKEFLAGS) INSTALL_PROGRAM="$(INSTALL_STRIP_PROGRAM)" \
+ install_sh_PROGRAM="$(INSTALL_STRIP_PROGRAM)" INSTALL_STRIP_FLAG=-s \
+ "INSTALL_PROGRAM_ENV=STRIPPROG='$(STRIP)'" install; \
+ fi
+mostlyclean-generic:
+
+clean-generic:
+
+distclean-generic:
+ -test -z "$(CONFIG_CLEAN_FILES)" || rm -f $(CONFIG_CLEAN_FILES)
+ -test . = "$(srcdir)" || test -z "$(CONFIG_CLEAN_VPATH_FILES)" || rm -f $(CONFIG_CLEAN_VPATH_FILES)
+
+maintainer-clean-generic:
+ @echo "This command is intended for maintainers to use"
+ @echo "it deletes files that may require special tools to rebuild."
+clean: clean-am
+
+clean-am: clean-generic clean-libtool clean-noinstLTLIBRARIES \
+ mostlyclean-am
+
+distclean: distclean-am
+ -rm -f ./$(DEPDIR)/imap-metadata.Plo
+ -rm -f ./$(DEPDIR)/imap-msgpart-url.Plo
+ -rm -f ./$(DEPDIR)/imap-msgpart.Plo
+ -rm -f Makefile
+distclean-am: clean-am distclean-compile distclean-generic \
+ distclean-tags
+
+dvi: dvi-am
+
+dvi-am:
+
+html: html-am
+
+html-am:
+
+info: info-am
+
+info-am:
+
+install-data-am: install-pkginc_libHEADERS
+
+install-dvi: install-dvi-am
+
+install-dvi-am:
+
+install-exec-am:
+
+install-html: install-html-am
+
+install-html-am:
+
+install-info: install-info-am
+
+install-info-am:
+
+install-man:
+
+install-pdf: install-pdf-am
+
+install-pdf-am:
+
+install-ps: install-ps-am
+
+install-ps-am:
+
+installcheck-am:
+
+maintainer-clean: maintainer-clean-am
+ -rm -f ./$(DEPDIR)/imap-metadata.Plo
+ -rm -f ./$(DEPDIR)/imap-msgpart-url.Plo
+ -rm -f ./$(DEPDIR)/imap-msgpart.Plo
+ -rm -f Makefile
+maintainer-clean-am: distclean-am maintainer-clean-generic
+
+mostlyclean: mostlyclean-am
+
+mostlyclean-am: mostlyclean-compile mostlyclean-generic \
+ mostlyclean-libtool
+
+pdf: pdf-am
+
+pdf-am:
+
+ps: ps-am
+
+ps-am:
+
+uninstall-am: uninstall-pkginc_libHEADERS
+
+.MAKE: install-am install-strip
+
+.PHONY: CTAGS GTAGS TAGS all all-am am--depfiles check check-am clean \
+ clean-generic clean-libtool clean-noinstLTLIBRARIES \
+ cscopelist-am ctags ctags-am distclean distclean-compile \
+ distclean-generic distclean-libtool distclean-tags distdir dvi \
+ dvi-am html html-am info info-am install install-am \
+ install-data install-data-am install-dvi install-dvi-am \
+ install-exec install-exec-am install-html install-html-am \
+ install-info install-info-am install-man install-pdf \
+ install-pdf-am install-pkginc_libHEADERS install-ps \
+ install-ps-am install-strip installcheck installcheck-am \
+ installdirs maintainer-clean maintainer-clean-generic \
+ mostlyclean mostlyclean-compile mostlyclean-generic \
+ mostlyclean-libtool pdf pdf-am ps ps-am tags tags-am uninstall \
+ uninstall-am uninstall-pkginc_libHEADERS
+
+.PRECIOUS: Makefile
+
+
+# Tell versions [3.59,3.63) of GNU make to not export all variables.
+# Otherwise a system limit (for SysV at least) may be exceeded.
+.NOEXPORT:
diff --git a/src/lib-imap-storage/imap-metadata.c b/src/lib-imap-storage/imap-metadata.c
new file mode 100644
index 0000000..eb791a4
--- /dev/null
+++ b/src/lib-imap-storage/imap-metadata.c
@@ -0,0 +1,314 @@
+/* Copyright (c) 2013-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "mail-storage.h"
+
+#include "imap-metadata.h"
+
+struct imap_metadata_transaction {
+ struct mailbox *box;
+ struct mailbox_transaction_context *trans;
+
+ enum mail_error error;
+ char *error_string;
+
+ bool server:1;
+ bool validated_only:1;
+};
+
+bool imap_metadata_verify_entry_name(const char *name,
+ const char **client_error_r)
+{
+ unsigned int i;
+ bool ok;
+
+ if (name[0] != '/') {
+ *client_error_r = "Entry name must begin with '/'";
+ return FALSE;
+ }
+ for (i = 0; name[i] != '\0'; i++) {
+ switch (name[i]) {
+ case '/':
+ if (i > 0 && name[i-1] == '/') {
+ *client_error_r = "Entry name can't contain consecutive '/'";
+ return FALSE;
+ }
+ if (name[i+1] == '\0') {
+ *client_error_r = "Entry name can't end with '/'";
+ return FALSE;
+ }
+ break;
+ case '*':
+ *client_error_r = "Entry name can't contain '*'";
+ return FALSE;
+ case '%':
+ *client_error_r = "Entry name can't contain '%'";
+ return FALSE;
+ default:
+ if (name[i] <= 0x19) {
+ *client_error_r = "Entry name can't contain control chars";
+ return FALSE;
+ }
+ break;
+ }
+ }
+ T_BEGIN {
+ const char *prefix, *p = strchr(name+1, '/');
+
+ prefix = p == NULL ? name : t_strdup_until(name, p);
+ ok = strcasecmp(prefix, IMAP_METADATA_PRIVATE_PREFIX) == 0 ||
+ strcasecmp(prefix, IMAP_METADATA_SHARED_PREFIX) == 0;
+ } T_END;
+ if (!ok) {
+ *client_error_r = "Entry name must begin with /private or /shared";
+ return FALSE;
+ }
+ return TRUE;
+}
+
+static void
+imap_metadata_transaction_set_error(struct imap_metadata_transaction *imtrans,
+ enum mail_error error, const char *string)
+{
+ i_free(imtrans->error_string);
+ imtrans->error_string = i_strdup(string);
+ imtrans->error = error;
+}
+
+static bool
+imap_metadata_entry2key(struct imap_metadata_transaction *imtrans,
+ const char *entry, enum mail_attribute_type *type_r,
+ const char **key_r)
+{
+ const char *key_prefix = (imtrans->server ?
+ MAILBOX_ATTRIBUTE_PREFIX_DOVECOT_PVT_SERVER : NULL);
+
+ /* names are case-insensitive so we'll always lowercase them */
+ entry = t_str_lcase(entry);
+
+ if (str_begins(entry, IMAP_METADATA_PRIVATE_PREFIX)) {
+ *key_r = entry + strlen(IMAP_METADATA_PRIVATE_PREFIX);
+ *type_r = MAIL_ATTRIBUTE_TYPE_PRIVATE;
+ } else {
+ i_assert(str_begins(entry, IMAP_METADATA_SHARED_PREFIX));
+ *key_r = entry + strlen(IMAP_METADATA_SHARED_PREFIX);
+ *type_r = MAIL_ATTRIBUTE_TYPE_SHARED;
+ }
+ if ((*key_r)[0] == '\0') {
+ /* /private or /shared prefix has no value itself */
+ } else {
+ i_assert((*key_r)[0] == '/');
+ *key_r += 1;
+ }
+
+ if (imtrans->validated_only)
+ *type_r |= MAIL_ATTRIBUTE_TYPE_FLAG_VALIDATED;
+
+ if (str_begins(*key_r, MAILBOX_ATTRIBUTE_PREFIX_DOVECOT_PVT)) {
+ /* Dovecot's internal attribute (mailbox or server).
+ don't allow accessing this. */
+ return FALSE;
+ }
+ /* Add the server-prefix (after checking for the above internal
+ attribute). */
+ if (key_prefix != NULL)
+ *key_r = t_strconcat(key_prefix, *key_r, NULL);
+ return TRUE;
+}
+
+static int
+imap_metadata_get_mailbox_transaction(struct imap_metadata_transaction *imtrans)
+{
+ if (imtrans->trans != NULL)
+ return 0;
+
+ if (imtrans->box == NULL || mailbox_open(imtrans->box) < 0)
+ return -1;
+ imtrans->trans = mailbox_transaction_begin(imtrans->box,
+ MAILBOX_TRANSACTION_FLAG_EXTERNAL, __func__);
+ return 0;
+}
+
+int imap_metadata_set(struct imap_metadata_transaction *imtrans,
+ const char *entry, const struct mail_attribute_value *value)
+{
+ enum mail_attribute_type type;
+ const char *key;
+
+ if (!imap_metadata_entry2key(imtrans, entry, &type, &key)) {
+ imap_metadata_transaction_set_error(imtrans, MAIL_ERROR_PARAMS,
+ "Internal mailbox attributes cannot be accessed");
+ return -1;
+ }
+
+ if (imap_metadata_get_mailbox_transaction(imtrans) < 0)
+ return -1;
+ return (value->value == NULL && value->value_stream == NULL ?
+ mailbox_attribute_unset(imtrans->trans, type, key) :
+ mailbox_attribute_set(imtrans->trans, type, key, value));
+}
+
+int imap_metadata_unset(struct imap_metadata_transaction *imtrans,
+ const char *entry)
+{
+ struct mail_attribute_value value;
+
+ i_zero(&value);
+ return imap_metadata_set(imtrans, entry, &value);
+}
+
+int imap_metadata_get(struct imap_metadata_transaction *imtrans,
+ const char *entry, struct mail_attribute_value *value_r)
+{
+ enum mail_attribute_type type;
+ const char *key;
+
+ i_zero(value_r);
+ if (!imap_metadata_entry2key(imtrans, entry, &type, &key))
+ return 0;
+ return mailbox_attribute_get(imtrans->box, type, key, value_r);
+}
+
+int imap_metadata_get_stream(struct imap_metadata_transaction *imtrans,
+ const char *entry, struct mail_attribute_value *value_r)
+{
+ enum mail_attribute_type type;
+ const char *key;
+
+ i_zero(value_r);
+ if (!imap_metadata_entry2key(imtrans, entry, &type, &key))
+ return 0;
+ return mailbox_attribute_get_stream(imtrans->box, type, key, value_r);
+}
+
+struct imap_metadata_iter {
+ struct mailbox_attribute_iter *iter;
+};
+
+struct imap_metadata_iter *
+imap_metadata_iter_init(struct imap_metadata_transaction *imtrans,
+ const char *entry)
+{
+ struct imap_metadata_iter *iter;
+ enum mail_attribute_type type;
+ const char *key;
+
+ iter = i_new(struct imap_metadata_iter, 1);
+ if (imap_metadata_entry2key(imtrans, entry, &type, &key)) {
+ const char *prefix =
+ key[0] == '\0' ? "" : t_strconcat(key, "/", NULL);
+ iter->iter = mailbox_attribute_iter_init(imtrans->box, type,
+ prefix);
+ }
+ return iter;
+}
+
+const char *imap_metadata_iter_next(struct imap_metadata_iter *iter)
+{
+ if (iter->iter == NULL)
+ return NULL;
+ return mailbox_attribute_iter_next(iter->iter);
+}
+
+int imap_metadata_iter_deinit(struct imap_metadata_iter **_iter)
+{
+ struct imap_metadata_iter *iter = *_iter;
+ int ret;
+
+ *_iter = NULL;
+
+ if (iter->iter == NULL)
+ ret = 0;
+ else
+ ret = mailbox_attribute_iter_deinit(&iter->iter);
+ i_free(iter);
+ return ret;
+}
+
+struct imap_metadata_transaction *
+imap_metadata_transaction_begin(struct mailbox *box)
+{
+ struct imap_metadata_transaction *imtrans;
+
+ imtrans = i_new(struct imap_metadata_transaction, 1);
+ imtrans->box = box;
+ return imtrans;
+}
+
+struct imap_metadata_transaction *
+imap_metadata_transaction_begin_server(struct mail_user *user)
+{
+ struct mail_namespace *ns;
+ struct mailbox *box;
+ struct imap_metadata_transaction *imtrans;
+
+ ns = mail_namespace_find_inbox(user->namespaces);
+ /* Server metadata shouldn't depend on INBOX's ACLs, so ignore them. */
+ box = mailbox_alloc(ns->list, "INBOX", MAILBOX_FLAG_IGNORE_ACLS |
+ MAILBOX_FLAG_ATTRIBUTE_SESSION);
+ imtrans = imap_metadata_transaction_begin(box);
+ imtrans->server = TRUE;
+ return imtrans;
+}
+
+void imap_metadata_transaction_validated_only(struct imap_metadata_transaction *imtrans,
+ bool set)
+{
+ imtrans->validated_only = set;
+}
+
+static void
+imap_metadata_transaction_finish(struct imap_metadata_transaction **_imtrans)
+{
+ struct imap_metadata_transaction *imtrans = *_imtrans;
+
+ if (imtrans->server)
+ mailbox_free(&imtrans->box);
+
+ i_free(imtrans->error_string);
+ i_free(imtrans);
+ *_imtrans = NULL;
+}
+
+int imap_metadata_transaction_commit(
+ struct imap_metadata_transaction **_imtrans,
+ enum mail_error *error_code_r, const char **client_error_r)
+{
+ struct imap_metadata_transaction *imtrans = *_imtrans;
+ int ret = 0;
+
+ if (imtrans->trans != NULL) {
+ const char *error = NULL;
+ ret = mailbox_transaction_commit(&imtrans->trans);
+ if (ret < 0)
+ error = mailbox_get_last_error(imtrans->box, error_code_r);
+ if (client_error_r != NULL)
+ *client_error_r = error;
+ }
+ imap_metadata_transaction_finish(_imtrans);
+ return ret;
+}
+
+void imap_metadata_transaction_rollback(
+ struct imap_metadata_transaction **_imtrans)
+{
+ struct imap_metadata_transaction *imtrans = *_imtrans;
+
+ if (imtrans->trans != NULL)
+ mailbox_transaction_rollback(&imtrans->trans);
+ imap_metadata_transaction_finish(_imtrans);
+}
+
+const char *
+imap_metadata_transaction_get_last_error(
+ struct imap_metadata_transaction *imtrans,
+ enum mail_error *error_code_r)
+{
+ if (imtrans->error != MAIL_ERROR_NONE) {
+ if (error_code_r != NULL)
+ *error_code_r = imtrans->error;
+ return imtrans->error_string;
+ }
+ i_assert(imtrans->box != NULL);
+ return mailbox_get_last_error(imtrans->box, error_code_r);
+}
diff --git a/src/lib-imap-storage/imap-metadata.h b/src/lib-imap-storage/imap-metadata.h
new file mode 100644
index 0000000..ef88e61
--- /dev/null
+++ b/src/lib-imap-storage/imap-metadata.h
@@ -0,0 +1,61 @@
+#ifndef IMAP_METADATA_H
+#define IMAP_METADATA_H
+
+#define IMAP_METADATA_PRIVATE_PREFIX "/private"
+#define IMAP_METADATA_SHARED_PREFIX "/shared"
+
+struct imap_metadata_iter;
+struct imap_metadata_transaction;
+
+/* Checks whether IMAP metadata entry name is valid */
+bool imap_metadata_verify_entry_name(
+ const char *name, const char **client_error_r);
+
+/* Set IMAP metadata entry to value. */
+int imap_metadata_set(struct imap_metadata_transaction *imtrans,
+ const char *entry, const struct mail_attribute_value *value);
+/* Delete IMAP metadata entry. This is just a wrapper to
+ imap_metadata_set() with value->value=NULL. */
+int imap_metadata_unset(struct imap_metadata_transaction *imtrans,
+ const char *entry);
+/* Returns value for IMAP metadata entry. Returns 1 if value was returned,
+ 0 if value wasn't found (set to NULL), -1 if error */
+int imap_metadata_get(struct imap_metadata_transaction *imtrans,
+ const char *entry, struct mail_attribute_value *value_r);
+/* Same as imap_metadata_get(), but the returned value may be either an
+ input stream or a string. */
+int imap_metadata_get_stream(struct imap_metadata_transaction *imtrans,
+ const char *entry, struct mail_attribute_value *value_r);
+
+/* Iterate through IMAP metadata entries names under the specified entry. */
+struct imap_metadata_iter *
+imap_metadata_iter_init(struct imap_metadata_transaction *imtrans,
+ const char *entry);
+/* Returns the next IMAP metadata entry name or NULL if there are no more
+ entries. */
+const char *imap_metadata_iter_next(struct imap_metadata_iter *iter);
+int imap_metadata_iter_deinit(struct imap_metadata_iter **_iter);
+
+struct imap_metadata_transaction *
+imap_metadata_transaction_begin(struct mailbox *box);
+struct imap_metadata_transaction *
+imap_metadata_transaction_begin_mailbox(struct mail_user *user,
+ const char *mailbox);
+struct imap_metadata_transaction *
+imap_metadata_transaction_begin_server(struct mail_user *user);
+
+/* Allow access only to validated attributes within this transaction. */
+void imap_metadata_transaction_validated_only(struct imap_metadata_transaction *imtrans,
+ bool set);
+
+int imap_metadata_transaction_commit(
+ struct imap_metadata_transaction **_imtrans,
+ enum mail_error *error_code_r, const char **client_error_r);
+void imap_metadata_transaction_rollback(
+ struct imap_metadata_transaction **_imtrans);
+const char *
+imap_metadata_transaction_get_last_error(
+ struct imap_metadata_transaction *imtrans,
+ enum mail_error *error_code_r);
+
+#endif
diff --git a/src/lib-imap-storage/imap-msgpart-url.c b/src/lib-imap-storage/imap-msgpart-url.c
new file mode 100644
index 0000000..2469dd3
--- /dev/null
+++ b/src/lib-imap-storage/imap-msgpart-url.c
@@ -0,0 +1,287 @@
+/* Copyright (c) 2013-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "net.h"
+#include "istream.h"
+#include "message-parser.h"
+#include "mail-storage.h"
+#include "mail-namespace.h"
+#include "imap-url.h"
+#include "imap-msgpart.h"
+#include "imap-msgpart-url.h"
+
+struct imap_msgpart_url {
+ char *mailbox;
+ uint32_t uidvalidity;
+ uint32_t uid;
+ char *section;
+ uoff_t partial_offset, partial_size;
+
+ struct imap_msgpart *part;
+
+ struct mail_user *user;
+ struct mailbox *selected_box;
+ struct mailbox *box;
+ struct mailbox_transaction_context *trans;
+ struct mail *mail;
+
+ struct imap_msgpart_open_result result;
+
+ bool decode_cte_to_binary:1;
+};
+
+int imap_msgpart_url_create(struct mail_user *user, const struct imap_url *url,
+ struct imap_msgpart_url **mpurl_r,
+ const char **client_error_r)
+{
+ const char *section = url->section == NULL ? "" : url->section;
+ struct imap_msgpart_url *mpurl;
+ struct imap_msgpart *msgpart;
+
+ if (url->mailbox == NULL || url->uid == 0 ||
+ url->search_program != NULL) {
+ *client_error_r = "Invalid messagepart IMAP URL";
+ return -1;
+ }
+ if (imap_msgpart_parse(section, &msgpart) < 0) {
+ *client_error_r = "Invalid section";
+ return -1;
+ }
+
+ mpurl = i_new(struct imap_msgpart_url, 1);
+ mpurl->part = msgpart;
+ mpurl->user = user;
+ mpurl->mailbox = i_strdup(url->mailbox);
+ mpurl->uidvalidity = url->uidvalidity;
+ mpurl->uid = url->uid;
+ if (url->section != NULL)
+ mpurl->section = i_strdup(url->section);
+ mpurl->partial_offset = url->partial_offset;
+ mpurl->partial_size = url->partial_size;
+
+ imap_msgpart_set_partial(msgpart, url->partial_offset,
+ url->partial_size == 0 ?
+ UOFF_T_MAX : url->partial_size);
+
+ *mpurl_r = mpurl;
+ return 0;
+}
+
+int imap_msgpart_url_parse(struct mail_user *user, struct mailbox *selected_box,
+ const char *urlstr, struct imap_msgpart_url **mpurl_r,
+ const char **client_error_r)
+{
+ struct mailbox_status box_status;
+ struct imap_url base_url, *url;
+ const char *error;
+
+ /* build base url */
+ i_zero(&base_url);
+ if (selected_box != NULL) {
+ mailbox_get_open_status(selected_box, STATUS_UIDVALIDITY,
+ &box_status);
+ base_url.mailbox = mailbox_get_vname(selected_box);
+ base_url.uidvalidity = box_status.uidvalidity;
+ }
+
+ /* parse url */
+ if (imap_url_parse(urlstr, &base_url,
+ IMAP_URL_PARSE_REQUIRE_RELATIVE, &url, &error) < 0) {
+ *client_error_r = t_strconcat("Invalid IMAP URL: ", error, NULL);
+ return 0;
+ }
+ if (url->mailbox == NULL) {
+ *client_error_r = "Mailbox-relative IMAP URL, but no mailbox selected";
+ return 0;
+ }
+ if (imap_msgpart_url_create(user, url, mpurl_r, client_error_r) < 0)
+ return -1;
+ (*mpurl_r)->selected_box = selected_box;
+ return 1;
+}
+
+struct mailbox *imap_msgpart_url_get_mailbox(struct imap_msgpart_url *mpurl)
+{
+ return mpurl->box;
+}
+
+int imap_msgpart_url_open_mailbox(struct imap_msgpart_url *mpurl,
+ struct mailbox **box_r, enum mail_error *error_code_r,
+ const char **client_error_r)
+{
+ struct mailbox_status box_status;
+ enum mailbox_flags flags = MAILBOX_FLAG_READONLY;
+ struct mail_namespace *ns;
+ struct mailbox *box;
+
+ if (mpurl->box != NULL) {
+ *box_r = mpurl->box;
+ *error_code_r = MAIL_ERROR_NONE;
+ return 1;
+ }
+
+ /* find mailbox namespace */
+ ns = mail_namespace_find(mpurl->user->namespaces, mpurl->mailbox);
+
+ /* open mailbox */
+ if (mpurl->selected_box != NULL &&
+ mailbox_equals(mpurl->selected_box, ns, mpurl->mailbox))
+ box = mpurl->selected_box;
+ else
+ box = mailbox_alloc(ns->list, mpurl->mailbox, flags);
+ if (mailbox_open(box) < 0) {
+ *client_error_r = mail_storage_get_last_error(mailbox_get_storage(box),
+ error_code_r);
+ if (box != mpurl->selected_box)
+ mailbox_free(&box);
+ return *error_code_r == MAIL_ERROR_TEMP ? -1 : 0;
+ }
+
+ /* verify UIDVALIDITY */
+ mailbox_get_open_status(box, STATUS_UIDVALIDITY, &box_status);
+ if (mpurl->uidvalidity > 0 &&
+ box_status.uidvalidity != mpurl->uidvalidity) {
+ *client_error_r = "Invalid UIDVALIDITY";
+ *error_code_r = MAIL_ERROR_EXPUNGED;
+ if (box != mpurl->selected_box)
+ mailbox_free(&box);
+ return 0;
+ }
+ mpurl->box = box;
+ *box_r = box;
+ return 1;
+}
+
+int imap_msgpart_url_open_mail(struct imap_msgpart_url *mpurl,
+ struct mail **mail_r,
+ const char **client_error_r)
+{
+ struct mailbox_transaction_context *t;
+ struct mailbox *box;
+ enum mail_error error_code;
+ struct mail *mail;
+ int ret;
+
+ if (mpurl->mail != NULL) {
+ *mail_r = mpurl->mail;
+ return 1;
+ }
+
+ /* open mailbox if it is not yet open */
+ if ((ret = imap_msgpart_url_open_mailbox(mpurl, &box, &error_code,
+ client_error_r)) <= 0)
+ return ret;
+
+ /* start transaction */
+ t = mailbox_transaction_begin(box, 0, __func__);
+ mail = mail_alloc(t, MAIL_FETCH_MESSAGE_PARTS |
+ MAIL_FETCH_IMAP_BODYSTRUCTURE, NULL);
+
+ /* find the message */
+ if (!mail_set_uid(mail, mpurl->uid)) {
+ *client_error_r = "Message not found";
+ mail_free(&mail);
+ mailbox_transaction_rollback(&t);
+ return 0;
+ }
+
+ mpurl->trans = t;
+ mpurl->mail = mail;
+ *mail_r = mail;
+ return 1;
+}
+
+struct imap_msgpart *
+imap_msgpart_url_get_part(struct imap_msgpart_url *mpurl)
+{
+ return mpurl->part;
+}
+
+void imap_msgpart_url_set_decode_to_binary(struct imap_msgpart_url *mpurl)
+{
+ imap_msgpart_set_decode_to_binary(mpurl->part);
+}
+
+int imap_msgpart_url_read_part(struct imap_msgpart_url *mpurl,
+ struct imap_msgpart_open_result *result_r,
+ const char **client_error_r)
+{
+ struct mail *mail;
+ int ret;
+
+ if (mpurl->result.input != NULL) {
+ i_stream_seek(mpurl->result.input, 0);
+ *result_r = mpurl->result;
+ return 1;
+ }
+
+ /* open mail if it is not yet open */
+ ret = imap_msgpart_url_open_mail(mpurl, &mail, client_error_r);
+ if (ret <= 0)
+ return ret;
+
+ /* open the referenced part as a stream */
+ ret = imap_msgpart_open(mail, mpurl->part, result_r);
+ if (ret < 0) {
+ *client_error_r = mailbox_get_last_error(mpurl->box, NULL);
+ return ret;
+ }
+
+ mpurl->result = *result_r;
+ return 1;
+}
+
+int imap_msgpart_url_verify(struct imap_msgpart_url *mpurl,
+ const char **client_error_r)
+{
+ struct mail *mail;
+ int ret;
+
+ if (mpurl->result.input != NULL)
+ return 1;
+
+ /* open mail if it is not yet open */
+ ret = imap_msgpart_url_open_mail(mpurl, &mail, client_error_r);
+ return ret;
+}
+
+int imap_msgpart_url_get_bodypartstructure(struct imap_msgpart_url *mpurl,
+ const char **bpstruct_r,
+ const char **client_error_r)
+{
+ struct mail *mail;
+ int ret;
+
+ /* open mail if it is not yet open */
+ ret = imap_msgpart_url_open_mail(mpurl, &mail, client_error_r);
+ if (ret <= 0)
+ return ret;
+
+ ret = imap_msgpart_bodypartstructure(mail, mpurl->part, bpstruct_r);
+ if (ret < 0)
+ *client_error_r = mailbox_get_last_error(mpurl->box, NULL);
+ else if (ret == 0)
+ *client_error_r = "Message part not found";
+ return ret;
+}
+
+void imap_msgpart_url_free(struct imap_msgpart_url **_mpurl)
+{
+ struct imap_msgpart_url *mpurl = *_mpurl;
+
+ *_mpurl = NULL;
+
+ i_stream_unref(&mpurl->result.input);
+ if (mpurl->part != NULL)
+ imap_msgpart_free(&mpurl->part);
+ if (mpurl->mail != NULL)
+ mail_free(&mpurl->mail);
+ if (mpurl->trans != NULL)
+ mailbox_transaction_rollback(&mpurl->trans);
+ if (mpurl->box != NULL && mpurl->box != mpurl->selected_box)
+ mailbox_free(&mpurl->box);
+ if (mpurl->section != NULL)
+ i_free(mpurl->section);
+ i_free(mpurl->mailbox);
+ i_free(mpurl);
+}
diff --git a/src/lib-imap-storage/imap-msgpart-url.h b/src/lib-imap-storage/imap-msgpart-url.h
new file mode 100644
index 0000000..5bc72c2
--- /dev/null
+++ b/src/lib-imap-storage/imap-msgpart-url.h
@@ -0,0 +1,50 @@
+#ifndef IMAP_MSGPART_URL_H
+#define IMAP_MSGPART_URL_H
+
+#include "imap-msgpart.h"
+
+struct imap_url;
+struct imap_msgpart;
+struct imap_msgpart_url;
+
+/* Functions returning int return 1 on success, 0 if URL doesn't point to
+ valid mail, -1 on storage error. */
+
+int imap_msgpart_url_create(struct mail_user *user, const struct imap_url *url,
+ struct imap_msgpart_url **url_r,
+ const char **client_error_r);
+int imap_msgpart_url_parse(struct mail_user *user, struct mailbox *selected_box,
+ const char *urlstr, struct imap_msgpart_url **url_r,
+ const char **client_error_r);
+
+int imap_msgpart_url_open_mailbox(struct imap_msgpart_url *mpurl,
+ struct mailbox **box_r, enum mail_error *error_code_r,
+ const char **client_error_r);
+struct mailbox *imap_msgpart_url_get_mailbox(struct imap_msgpart_url *mpurl);
+int imap_msgpart_url_open_mail(struct imap_msgpart_url *mpurl,
+ struct mail **mail_r,
+ const char **client_error_r);
+
+struct imap_msgpart *
+imap_msgpart_url_get_part(struct imap_msgpart_url *mpurl);
+
+/* Decode MIME parts with Content-Transfer-Encoding: base64/quoted-printable
+ to binary data (IMAP BINARY extension). If something can't be decoded, fails
+ with storage error set to MAIL_ERROR_CONVERSION. */
+void imap_msgpart_url_set_decode_to_binary(struct imap_msgpart_url *mpurl);
+
+/* stream_r is set to NULL when part has zero length, e.g. when partial offset
+ is larger than the size of the referenced part */
+int imap_msgpart_url_read_part(struct imap_msgpart_url *mpurl,
+ struct imap_msgpart_open_result *result_r,
+ const char **client_error_r);
+
+int imap_msgpart_url_get_bodypartstructure(struct imap_msgpart_url *mpurl,
+ const char **bpstruct_r,
+ const char **client_error_r);
+
+int imap_msgpart_url_verify(struct imap_msgpart_url *mpurl,
+ const char **client_error_r);
+void imap_msgpart_url_free(struct imap_msgpart_url **mpurl);
+
+#endif
diff --git a/src/lib-imap-storage/imap-msgpart.c b/src/lib-imap-storage/imap-msgpart.c
new file mode 100644
index 0000000..3bce117
--- /dev/null
+++ b/src/lib-imap-storage/imap-msgpart.c
@@ -0,0 +1,860 @@
+/* Copyright (c) 2013-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "str.h"
+#include "array.h"
+#include "istream.h"
+#include "istream-crlf.h"
+#include "istream-nonuls.h"
+#include "istream-base64.h"
+#include "istream-header-filter.h"
+#include "istream-qp.h"
+#include "ostream.h"
+#include "message-parser.h"
+#include "message-decoder.h"
+#include "mail-storage-private.h"
+#include "mail-namespace.h"
+#include "imap-bodystructure.h"
+#include "imap-parser.h"
+#include "imap-msgpart.h"
+
+enum fetch_type {
+ FETCH_FULL,
+ FETCH_MIME,
+ FETCH_MIME_BODY,
+ FETCH_HEADER,
+ FETCH_HEADER_FIELDS,
+ FETCH_HEADER_FIELDS_NOT,
+ FETCH_BODY
+};
+
+struct imap_msgpart {
+ pool_t pool;
+
+ /* "" for root, otherwise e.g. "1.2.3". the .MIME, .HEADER, etc.
+ suffix not included */
+ const char *section_number;
+ enum fetch_type fetch_type;
+ enum mail_fetch_field wanted_fields;
+
+ /* HEADER.FIELDS[.NOT] (list of headers) */
+ struct mailbox_header_lookup_ctx *header_ctx;
+ const char *const *headers;
+
+ /* which part of the message part to fetch (default: 0..UOFF_T_MAX) */
+ uoff_t partial_offset, partial_size;
+
+ bool decode_cte_to_binary:1;
+};
+
+struct imap_msgpart_open_ctx {
+ /* from matching message_part, set after opening: */
+ uoff_t physical_pos;
+ struct message_size mime_hdr_size;
+ struct message_size mime_body_size;
+};
+
+static struct imap_msgpart *imap_msgpart_type(enum fetch_type fetch_type)
+{
+ struct imap_msgpart *msgpart;
+ pool_t pool;
+
+ pool = pool_alloconly_create("imap msgpart", sizeof(*msgpart)+32);
+ msgpart = p_new(pool, struct imap_msgpart, 1);
+ msgpart->pool = pool;
+ msgpart->partial_size = UOFF_T_MAX;
+ msgpart->fetch_type = fetch_type;
+ msgpart->section_number = "";
+ if (fetch_type == FETCH_HEADER || fetch_type == FETCH_FULL)
+ msgpart->wanted_fields |= MAIL_FETCH_STREAM_HEADER;
+ if (fetch_type == FETCH_BODY || fetch_type == FETCH_FULL)
+ msgpart->wanted_fields |= MAIL_FETCH_STREAM_BODY;
+ return msgpart;
+}
+
+struct imap_msgpart *imap_msgpart_full(void)
+{
+ return imap_msgpart_type(FETCH_FULL);
+}
+
+struct imap_msgpart *imap_msgpart_header(void)
+{
+ return imap_msgpart_type(FETCH_HEADER);
+}
+
+struct imap_msgpart *imap_msgpart_body(void)
+{
+ return imap_msgpart_type(FETCH_BODY);
+}
+
+static struct message_part *
+imap_msgpart_find(struct message_part *parts, const char *section)
+{
+ struct message_part *part = parts;
+ const char *path;
+ unsigned int num;
+
+ path = section;
+ while (*path >= '0' && *path <= '9' && part != NULL) {
+ /* get part number, we have already verified its validity */
+ num = 0;
+ while (*path != '\0' && *path != '.') {
+ i_assert(*path >= '0' && *path <= '9');
+
+ num = num*10 + (*path - '0');
+ path++;
+ }
+
+ if (*path == '.')
+ path++;
+
+ if ((part->flags & MESSAGE_PART_FLAG_MULTIPART) != 0) {
+ /* find the part */
+ part = part->children;
+ for (; num > 1 && part != NULL; num--)
+ part = part->next;
+ } else {
+ /* only 1 allowed with non-multipart messages.
+ if the child isn't message/rfc822, the path must be
+ finished after this. */
+ if (num != 1)
+ part = NULL;
+ else if (*path != '\0' &&
+ (part->flags & MESSAGE_PART_FLAG_MESSAGE_RFC822) == 0)
+ part = NULL;
+ }
+
+ if (part != NULL &&
+ (part->flags & MESSAGE_PART_FLAG_MESSAGE_RFC822) != 0 &&
+ (*path >= '0' && *path <= '9')) {
+ /* if we continue inside the message/rfc822, skip this
+ body part */
+ part = part->children;
+ }
+ }
+ i_assert(part == NULL || *path == '\0');
+ return part;
+}
+
+static int
+imap_msgpart_get_header_fields(pool_t pool, const char *header_list,
+ ARRAY_TYPE(const_string) *fields)
+{
+ struct istream *input;
+ struct imap_parser *parser;
+ const struct imap_arg *args, *hdr_list;
+ unsigned int list_count;
+ unsigned int i;
+ int result = 0;
+
+ input = i_stream_create_from_data(header_list, strlen(header_list));
+ parser = imap_parser_create(input, NULL, SIZE_MAX);
+
+ if (imap_parser_finish_line(parser, 0, 0, &args) > 0 &&
+ imap_arg_get_list_full(args, &hdr_list, &list_count) &&
+ args[1].type == IMAP_ARG_EOL &&
+ list_count > 0) {
+ const char *value;
+
+ p_array_init(fields, pool, list_count);
+ for (i = 0; i < list_count; i++) {
+ if (!imap_arg_get_astring(&hdr_list[i], &value)) {
+ result = -1;
+ break;
+ }
+
+ value = p_strdup(pool, t_str_ucase(value));
+ array_push_back(fields, &value);
+ }
+ /* istream-header-filter requires headers to be sorted */
+ array_sort(fields, i_strcasecmp_p);
+ } else {
+ result = -1;
+ }
+
+ imap_parser_unref(&parser);
+ i_stream_unref(&input);
+ return result;
+}
+
+static int
+imap_msgpart_parse_header_fields(struct imap_msgpart *msgpart,
+ const char *header_list)
+{
+ ARRAY_TYPE(const_string) fields;
+
+ if (header_list[0] == ' ')
+ header_list++;
+
+ /* HEADER.FIELDS (list), HEADER.FIELDS.NOT (list) */
+ if (imap_msgpart_get_header_fields(msgpart->pool, header_list,
+ &fields) < 0)
+ return -1;
+
+ array_append_zero(&fields);
+ msgpart->headers = array_front(&fields);
+ return 0;
+}
+
+int imap_msgpart_parse(const char *section, struct imap_msgpart **msgpart_r)
+{
+ struct imap_msgpart *msgpart;
+ pool_t pool;
+ unsigned int i;
+ bool next_digit;
+ int ret;
+
+ pool = pool_alloconly_create("imap msgpart", 1024);
+ msgpart = *msgpart_r = p_new(pool, struct imap_msgpart, 1);
+ msgpart->pool = pool;
+ msgpart->partial_size = UOFF_T_MAX;
+
+ /* get the section number */
+ next_digit = TRUE;
+ for (i = 0; section[i] != '\0'; i++) {
+ if (section[i] >= '0' && section[i] <= '9') {
+ next_digit = FALSE;
+ } else if (!next_digit && section[i] == '.') {
+ next_digit = TRUE;
+ } else {
+ break;
+ }
+ }
+ if (i == 0) {
+ /* [], [HEADER], etc. */
+ msgpart->section_number = "";
+ } else if (section[i] == '\0') {
+ /* [1.2.3] */
+ if (i > 0 && section[i-1] == '.') {
+ pool_unref(&pool);
+ return -1;
+ }
+ msgpart->section_number = p_strdup(pool, section);
+ section = "";
+ } else {
+ /* [1.2.3.MIME], [1.2.3.HEADER], etc */
+ if (section[i-1] != '.') {
+ pool_unref(&pool);
+ return -1;
+ }
+ msgpart->section_number = p_strndup(pool, section, i-1);
+ section += i;
+ }
+
+ if (*section == '\0') {
+ msgpart->wanted_fields |= MAIL_FETCH_STREAM_BODY;
+ if (*msgpart->section_number == '\0') {
+ /* BODY[] - header+body */
+ msgpart->fetch_type = FETCH_FULL;
+ msgpart->wanted_fields |= MAIL_FETCH_STREAM_HEADER;
+ } else {
+ /* BODY[1] - body only */
+ msgpart->fetch_type = FETCH_MIME_BODY;
+ }
+ return 0;
+ }
+ section = t_str_ucase(section);
+
+ if (strcmp(section, "MIME") == 0) {
+ if (msgpart->section_number[0] == '\0')
+ return -1;
+ msgpart->fetch_type = FETCH_MIME;
+ msgpart->wanted_fields |= MAIL_FETCH_STREAM_BODY;
+ } else if (strcmp(section, "TEXT") == 0) {
+ /* body (for root or for message/rfc822) */
+ msgpart->fetch_type = FETCH_BODY;
+ msgpart->wanted_fields |= MAIL_FETCH_STREAM_BODY;
+ } else if (str_begins(section, "HEADER")) {
+ /* header (for root or for message/rfc822) */
+ if (section[6] == '\0') {
+ msgpart->fetch_type = FETCH_HEADER;
+ ret = 0;
+ } else if (str_begins(section, "HEADER.FIELDS.NOT")) {
+ msgpart->fetch_type = FETCH_HEADER_FIELDS_NOT;
+ ret = imap_msgpart_parse_header_fields(msgpart,
+ section+17);
+ } else if (str_begins(section, "HEADER.FIELDS")) {
+ msgpart->fetch_type = FETCH_HEADER_FIELDS;
+ ret = imap_msgpart_parse_header_fields(msgpart,
+ section+13);
+ } else {
+ ret = -1;
+ }
+ if (ret < 0) {
+ imap_msgpart_free(&msgpart);
+ return -1;
+ }
+ if (msgpart->fetch_type == FETCH_HEADER_FIELDS) {
+ /* we may be able to get this from cache, don't give a
+ wanted_fields hint */
+ } else if (*msgpart->section_number == '\0')
+ msgpart->wanted_fields |= MAIL_FETCH_STREAM_HEADER;
+ else
+ msgpart->wanted_fields |= MAIL_FETCH_STREAM_BODY;
+ } else {
+ imap_msgpart_free(&msgpart);
+ return -1;
+ }
+ return 0;
+}
+
+void imap_msgpart_free(struct imap_msgpart **_msgpart)
+{
+ struct imap_msgpart *msgpart = *_msgpart;
+
+ *_msgpart = NULL;
+
+ imap_msgpart_close_mailbox(msgpart);
+ pool_unref(&msgpart->pool);
+}
+
+bool imap_msgpart_contains_body(const struct imap_msgpart *msgpart)
+{
+ switch (msgpart->fetch_type) {
+ case FETCH_HEADER:
+ case FETCH_HEADER_FIELDS:
+ case FETCH_HEADER_FIELDS_NOT:
+ return FALSE;
+ case FETCH_FULL:
+ case FETCH_MIME:
+ case FETCH_MIME_BODY:
+ case FETCH_BODY:
+ break;
+ }
+ return TRUE;
+}
+
+void imap_msgpart_set_decode_to_binary(struct imap_msgpart *msgpart)
+{
+ msgpart->decode_cte_to_binary = TRUE;
+}
+
+void imap_msgpart_set_partial(struct imap_msgpart *msgpart,
+ uoff_t offset, uoff_t size)
+{
+ msgpart->partial_offset = offset;
+ msgpart->partial_size = size;
+}
+
+uoff_t imap_msgpart_get_partial_offset(struct imap_msgpart *msgpart)
+{
+ return msgpart->partial_offset;
+}
+
+uoff_t imap_msgpart_get_partial_size(struct imap_msgpart *msgpart)
+{
+ return msgpart->partial_size;
+}
+
+enum mail_fetch_field imap_msgpart_get_fetch_data(struct imap_msgpart *msgpart)
+{
+ return msgpart->wanted_fields;
+}
+
+void imap_msgpart_get_wanted_headers(struct imap_msgpart *msgpart,
+ ARRAY_TYPE(const_string) *headers)
+{
+ unsigned int i;
+
+ if (msgpart->fetch_type != FETCH_HEADER_FIELDS)
+ return;
+
+ for (i = 0; msgpart->headers[i] != NULL; i++)
+ array_push_back(headers, &msgpart->headers[i]);
+}
+
+static int
+imap_msgpart_get_partial_header(struct mail *mail, struct istream *mail_input,
+ const struct imap_msgpart *msgpart,
+ uoff_t *virtual_size_r, bool *have_crlfs_r,
+ struct imap_msgpart_open_result *result_r)
+{
+ const char *const *hdr_fields = msgpart->headers;
+ unsigned int hdr_count = str_array_length(hdr_fields);
+ struct message_size hdr_size;
+ struct istream *input;
+ bool has_nuls;
+
+ if (msgpart->fetch_type != FETCH_HEADER_FIELDS) {
+ i_assert(msgpart->fetch_type == FETCH_HEADER_FIELDS_NOT);
+ input = i_stream_create_header_filter(mail_input,
+ HEADER_FILTER_EXCLUDE |
+ HEADER_FILTER_HIDE_BODY,
+ hdr_fields, hdr_count,
+ *null_header_filter_callback,
+ NULL);
+ } else if (msgpart->section_number[0] != '\0') {
+ /* fetching partial headers for a message/rfc822 part. */
+ input = i_stream_create_header_filter(mail_input,
+ HEADER_FILTER_INCLUDE |
+ HEADER_FILTER_HIDE_BODY,
+ hdr_fields, hdr_count,
+ *null_header_filter_callback,
+ NULL);
+ } else {
+ /* mail_get_header_stream() already filtered out the
+ unwanted headers. */
+ input = mail_input;
+ i_stream_ref(input);
+ }
+
+ if (message_get_header_size(input, &hdr_size, &has_nuls) < 0) {
+ mail_set_critical(mail,
+ "read(%s) failed: %s", i_stream_get_name(input),
+ i_stream_get_error(input));
+ i_stream_unref(&input);
+ return -1;
+ }
+ i_stream_seek(input, 0);
+ result_r->input = input;
+ result_r->size = hdr_size.virtual_size;
+ result_r->size_field = 0;
+ *virtual_size_r = hdr_size.virtual_size;
+ *have_crlfs_r = hdr_size.physical_size == hdr_size.virtual_size;
+ return 0;
+}
+
+static struct istream *
+imap_msgpart_crlf_seek(struct mail *mail, struct istream *input,
+ const struct imap_msgpart *msgpart)
+{
+ struct mail_msgpart_partial_cache *cache = &mail->box->partial_cache;
+ struct istream *crlf_input, *errinput;
+ uoff_t physical_start = input->v_offset;
+ uoff_t virtual_skip = msgpart->partial_offset;
+ bool cr_skipped;
+
+ if (virtual_skip == 0) {
+ /* no need to seek */
+ } else if (mail->uid > 0 && cache->uid == mail->uid &&
+ cache->physical_start == physical_start &&
+ cache->virtual_pos < virtual_skip) {
+ /* use cache */
+ i_stream_seek(input, physical_start + cache->physical_pos);
+ virtual_skip -= cache->virtual_pos;
+ }
+ if (message_skip_virtual(input, virtual_skip, &cr_skipped) < 0) {
+ errinput = i_stream_create_error_str(errno, "%s", i_stream_get_error(input));
+ i_stream_set_name(errinput, i_stream_get_name(input));
+ i_stream_unref(&input);
+ return errinput;
+ }
+
+ if (mail->uid > 0 &&
+ (msgpart->partial_offset != 0 ||
+ msgpart->partial_size != UOFF_T_MAX) && !input->eof) {
+ /* update cache */
+ cache->uid = mail->uid;
+ cache->physical_start = physical_start;
+ cache->physical_pos = input->v_offset - physical_start;
+ cache->virtual_pos = msgpart->partial_offset;
+ if (cr_skipped) {
+ /* the physical_pos points to virtual CRLF, but
+ virtual_pos already skipped CR. that can't work,
+ so seek back the virtual CR */
+ cache->virtual_pos--;
+ }
+ }
+ crlf_input = i_stream_create_crlf(input);
+ if (cr_skipped)
+ i_stream_skip(crlf_input, 1);
+ i_stream_unref(&input);
+ return crlf_input;
+}
+
+static void
+imap_msgpart_get_partial(struct mail *mail, const struct imap_msgpart *msgpart,
+ bool convert_nuls, bool use_partial_cache,
+ uoff_t virtual_size, bool have_crlfs,
+ struct imap_msgpart_open_result *result)
+{
+ struct istream *input2;
+ uoff_t bytes_left;
+
+ /* input is already seeked to the beginning of the wanted data */
+
+ if (msgpart->partial_offset >= virtual_size) {
+ /* can't seek past the MIME part */
+ i_stream_unref(&result->input);
+ result->input = i_stream_create_from_data("", 0);
+ result->size = 0;
+ return;
+ }
+
+ if (have_crlfs) {
+ /* input has CRLF linefeeds, we can quickly seek to
+ wanted position */
+ i_stream_skip(result->input, msgpart->partial_offset);
+ } else {
+ /* input has LF linefeeds. it can be slow to seek to wanted
+ position, so try to do caching whenever possible */
+ i_assert(use_partial_cache);
+ result->input = imap_msgpart_crlf_seek(mail, result->input,
+ msgpart);
+ }
+
+ bytes_left = virtual_size - msgpart->partial_offset;
+ if (msgpart->partial_size <= bytes_left) {
+ /* limit output to specified number of bytes */
+ result->size = msgpart->partial_size;
+ } else {
+ /* send all bytes */
+ result->size = bytes_left;
+ }
+
+ if (!mail->has_no_nuls && convert_nuls) {
+ /* IMAP literals must not contain NULs. change them to
+ 0x80 characters. */
+ input2 = i_stream_create_nonuls(result->input, '\x80');
+ i_stream_unref(&result->input);
+ result->input = input2;
+ }
+ input2 = i_stream_create_limit(result->input, result->size);
+ i_stream_unref(&result->input);
+ result->input = input2;
+}
+
+static int
+imap_msgpart_find_part(struct mail *mail, const struct imap_msgpart *msgpart,
+ struct message_part **part_r)
+{
+ struct message_part *parts, *part = NULL;
+
+ if (*msgpart->section_number == '\0') {
+ *part_r = NULL;
+ return 1;
+ }
+
+ if (mail_get_parts(mail, &parts) < 0)
+ return -1;
+ part = imap_msgpart_find(parts, msgpart->section_number);
+ if (part == NULL) {
+ /* MIME part not found. */
+ *part_r = NULL;
+ return 0;
+ }
+
+ switch (msgpart->fetch_type) {
+ case FETCH_MIME:
+ /* What to do if this is a message/rfc822? Does it have
+ MIME headers or not? Possibilities are: a) no, return
+ empty string (UW-IMAP does this), b) return the same as
+ HEADER. Dovecot has done b) for a long time and it's not
+ very clear which one is correct, so we'll just continue
+ with b) */
+ case FETCH_FULL:
+ case FETCH_MIME_BODY:
+ break;
+ case FETCH_HEADER:
+ case FETCH_HEADER_FIELDS:
+ case FETCH_HEADER_FIELDS_NOT:
+ case FETCH_BODY:
+ /* fetching message/rfc822 part's header/body */
+ if ((part->flags & MESSAGE_PART_FLAG_MESSAGE_RFC822) == 0) {
+ *part_r = NULL;
+ return 0;
+ }
+ i_assert(part->children != NULL &&
+ part->children->next == NULL);
+ part = part->children;
+ break;
+ }
+ *part_r = part;
+ return 1;
+}
+
+static int
+imap_msgpart_open_normal(struct mail *mail, struct imap_msgpart *msgpart,
+ const struct message_part *part,
+ uoff_t *virtual_size_r, bool *have_crlfs_r,
+ struct imap_msgpart_open_result *result_r)
+{
+ struct message_size hdr_size, body_size, part_size;
+ struct istream *input = NULL;
+ bool unknown_crlfs = FALSE;
+
+ i_zero(&hdr_size);
+ i_zero(&body_size);
+ i_zero(&part_size);
+
+ if (*msgpart->section_number != '\0') {
+ /* find the MIME part */
+ i_assert(part != NULL);
+
+ if (mail_get_stream_because(mail, NULL, NULL, "MIME part", &input) < 0)
+ return -1;
+
+ i_stream_seek(input, part->physical_pos);
+ hdr_size = part->header_size;
+ body_size = part->body_size;
+ } else switch (msgpart->fetch_type) {
+ case FETCH_FULL:
+ /* fetch the whole message */
+ if (mail_get_stream_because(mail, NULL, NULL, "full mail", &input) < 0 ||
+ mail_get_virtual_size(mail, &body_size.virtual_size) < 0)
+ return -1;
+ result_r->size_field = MAIL_FETCH_VIRTUAL_SIZE;
+
+ i_assert(mail->lookup_abort == MAIL_LOOKUP_ABORT_NEVER);
+ mail->lookup_abort = MAIL_LOOKUP_ABORT_READ_MAIL;
+ if (mail_get_physical_size(mail, &body_size.physical_size) < 0)
+ unknown_crlfs = TRUE;
+ mail->lookup_abort = MAIL_LOOKUP_ABORT_NEVER;
+ break;
+ case FETCH_MIME:
+ case FETCH_MIME_BODY:
+ i_unreached();
+ case FETCH_HEADER:
+ case FETCH_HEADER_FIELDS_NOT:
+ /* fetch the message's header */
+ if (mail_get_hdr_stream(mail, &hdr_size, &input) < 0)
+ return -1;
+ result_r->size_field = MAIL_FETCH_MESSAGE_PARTS;
+ break;
+ case FETCH_HEADER_FIELDS:
+ /* try to lookup the headers from cache */
+ if (msgpart->header_ctx == NULL) {
+ msgpart->header_ctx =
+ mailbox_header_lookup_init(mail->box,
+ msgpart->headers);
+ }
+ if (mail_get_header_stream(mail, msgpart->header_ctx,
+ &input) < 0)
+ return -1;
+ result_r->size_field = 0;
+ break;
+ case FETCH_BODY:
+ /* fetch the message's body */
+ if (mail_get_stream_because(mail, &hdr_size, &body_size,
+ "mail body", &input) < 0)
+ return -1;
+ result_r->size_field = MAIL_FETCH_MESSAGE_PARTS;
+ break;
+ }
+
+ if (msgpart->headers != NULL) {
+ /* return specific headers */
+ return imap_msgpart_get_partial_header(mail, input, msgpart,
+ virtual_size_r,
+ have_crlfs_r, result_r);
+ }
+
+ switch (msgpart->fetch_type) {
+ case FETCH_FULL:
+ part_size.physical_size += body_size.physical_size;
+ part_size.virtual_size += body_size.virtual_size;
+ /* fall through */
+ case FETCH_MIME:
+ case FETCH_HEADER:
+ part_size.physical_size += hdr_size.physical_size;
+ part_size.virtual_size += hdr_size.virtual_size;
+ break;
+ case FETCH_HEADER_FIELDS:
+ case FETCH_HEADER_FIELDS_NOT:
+ i_unreached();
+ case FETCH_BODY:
+ case FETCH_MIME_BODY:
+ i_stream_skip(input, hdr_size.physical_size);
+ part_size.physical_size += body_size.physical_size;
+ part_size.virtual_size += body_size.virtual_size;
+ break;
+ }
+
+ result_r->input = input;
+ i_stream_ref(input);
+ *virtual_size_r = part_size.virtual_size;
+ *have_crlfs_r = !unknown_crlfs &&
+ part_size.virtual_size == part_size.physical_size;
+ return 0;
+}
+
+int imap_msgpart_open(struct mail *mail, struct imap_msgpart *msgpart,
+ struct imap_msgpart_open_result *result_r)
+{
+ struct message_part *part;
+ uoff_t virtual_size;
+ bool include_hdr, binary, use_partial_cache, have_crlfs;
+ int ret;
+
+ i_zero(result_r);
+
+ if ((ret = imap_msgpart_find_part(mail, msgpart, &part)) < 0)
+ return -1;
+ if (ret == 0) {
+ /* MIME part not found. return an empty part. */
+ result_r->input = i_stream_create_from_data("", 0);
+ return 0;
+ }
+
+ if (msgpart->decode_cte_to_binary &&
+ (msgpart->fetch_type == FETCH_FULL ||
+ msgpart->fetch_type == FETCH_BODY ||
+ msgpart->fetch_type == FETCH_MIME_BODY)) {
+ /* binary fetch */
+ include_hdr = msgpart->fetch_type == FETCH_FULL;
+ if (part == NULL) {
+ if (mail_get_parts(mail, &part) < 0)
+ return -1;
+ }
+ if (mail_get_binary_stream(mail, part, include_hdr,
+ &virtual_size, &binary,
+ &result_r->input) < 0)
+ return -1;
+ have_crlfs = TRUE;
+ use_partial_cache = FALSE;
+ } else {
+ if (imap_msgpart_open_normal(mail, msgpart, part, &virtual_size,
+ &have_crlfs, result_r) < 0)
+ return -1;
+ binary = FALSE;
+ use_partial_cache = TRUE;
+ }
+
+ if (binary && msgpart->decode_cte_to_binary)
+ result_r->binary_decoded_input_has_nuls = TRUE;
+
+ imap_msgpart_get_partial(mail, msgpart, !binary, use_partial_cache,
+ virtual_size, have_crlfs, result_r);
+ return 0;
+}
+
+int imap_msgpart_size(struct mail *mail, struct imap_msgpart *msgpart,
+ uoff_t *size_r)
+{
+ struct imap_msgpart_open_result result;
+ struct message_part *part;
+ bool include_hdr;
+ unsigned int lines;
+ int ret;
+
+ if (!msgpart->decode_cte_to_binary ||
+ (msgpart->fetch_type != FETCH_FULL &&
+ msgpart->fetch_type != FETCH_BODY &&
+ msgpart->fetch_type != FETCH_MIME_BODY)) {
+ /* generic implementation */
+ if (imap_msgpart_open(mail, msgpart, &result) < 0)
+ return -1;
+ i_stream_unref(&result.input);
+ *size_r = result.size;
+ return 0;
+ }
+
+ /* binary-optimized implementation: */
+ if ((ret = imap_msgpart_find_part(mail, msgpart, &part)) < 0)
+ return -1;
+ if (ret == 0) {
+ /* MIME part not found. return an empty part. */
+ *size_r = 0;
+ return 0;
+ }
+ if (part == NULL) {
+ if (mail_get_parts(mail, &part) < 0)
+ return -1;
+ }
+ include_hdr = msgpart->fetch_type == FETCH_FULL;
+ return mail_get_binary_size(mail, part, include_hdr, size_r, &lines);
+}
+
+static int
+imap_msgpart_parse_bodystructure(struct mail *mail,
+ struct message_part *all_parts)
+{
+ struct mail_private *pmail = (struct mail_private *)mail;
+ const char *bodystructure, *error;
+
+ if (mail_get_special(mail, MAIL_FETCH_IMAP_BODYSTRUCTURE,
+ &bodystructure) < 0)
+ return -1;
+ if (all_parts->context != NULL) {
+ /* we just parsed the bodystructure */
+ return 0;
+ }
+
+ if (imap_bodystructure_parse(bodystructure, pmail->data_pool,
+ all_parts, &error) < 0) {
+ mail_set_cache_corrupted(mail,
+ MAIL_FETCH_IMAP_BODYSTRUCTURE, t_strdup_printf(
+ "Invalid message_part/BODYSTRUCTURE %s: %s",
+ bodystructure, error));
+ return -1;
+ }
+ return 0;
+}
+
+static int
+imap_msgpart_vsizes_to_binary(struct mail *mail, const struct message_part *part,
+ struct message_part **binpart_r)
+{
+ struct message_part **pos;
+ uoff_t size;
+ unsigned int lines;
+
+ if (mail_get_binary_size(mail, part, FALSE, &size, &lines) < 0)
+ return -1;
+
+ *binpart_r = t_new(struct message_part, 1);
+ **binpart_r = *part;
+ (*binpart_r)->body_size.virtual_size = size;
+ (*binpart_r)->body_size.lines = lines;
+
+ pos = &(*binpart_r)->children;
+ for (part = part->children; part != NULL; part = part->next) {
+ if (imap_msgpart_vsizes_to_binary(mail, part, pos) < 0)
+ return -1;
+ pos = &(*pos)->next;
+ }
+ return 0;
+}
+
+int imap_msgpart_bodypartstructure(struct mail *mail,
+ struct imap_msgpart *msgpart,
+ const char **bpstruct_r)
+{
+ struct message_part *all_parts, *part;
+ string_t *bpstruct;
+ const char *error;
+ int ret;
+
+ /* if we start parsing the body in here, make sure we also parse the
+ BODYSTRUCTURE */
+ mail_add_temp_wanted_fields(mail, MAIL_FETCH_IMAP_BODYSTRUCTURE, NULL);
+
+ if ((ret = imap_msgpart_find_part(mail, msgpart, &part)) < 0)
+ return -1;
+ if (ret == 0) {
+ /* MIME part not found. */
+ *bpstruct_r = NULL;
+ return 0;
+ }
+
+ if (mail_get_parts(mail, &all_parts) < 0)
+ return -1;
+ if (all_parts->context == NULL) {
+ if (imap_msgpart_parse_bodystructure(mail, all_parts) < 0)
+ return -1;
+ }
+ if (part == NULL)
+ part = all_parts;
+
+ if (msgpart->decode_cte_to_binary)
+ ret = imap_msgpart_vsizes_to_binary(mail, part, &part);
+
+ if (ret >= 0) {
+ bpstruct = t_str_new(256);
+ if (imap_bodystructure_write(part, bpstruct, TRUE, &error) < 0) {
+ error = t_strdup_printf(
+ "Invalid message_part/BODYSTRUCTURE: %s", error);
+ mail_set_cache_corrupted(mail, MAIL_FETCH_MESSAGE_PARTS,
+ error);
+ return -1;
+ }
+ *bpstruct_r = str_c(bpstruct);
+ }
+ return ret < 0 ? -1 : 1;
+}
+
+
+void imap_msgpart_close_mailbox(struct imap_msgpart *msgpart)
+{
+ mailbox_header_lookup_unref(&msgpart->header_ctx);
+}
diff --git a/src/lib-imap-storage/imap-msgpart.h b/src/lib-imap-storage/imap-msgpart.h
new file mode 100644
index 0000000..92b56c6
--- /dev/null
+++ b/src/lib-imap-storage/imap-msgpart.h
@@ -0,0 +1,68 @@
+#ifndef IMAP_MSGPART_H
+#define IMAP_MSGPART_H
+
+struct imap_msgpart;
+
+struct imap_msgpart_open_result {
+ /* message contents with CRLF linefeeds */
+ struct istream *input;
+ /* size of input */
+ uoff_t size;
+ /* if size was looked up using cache and it ends up being wrong,
+ this field can be used to log about cache corruption */
+ enum mail_fetch_field size_field;
+ /* TRUE if BINARY decoded content contains NUL characters */
+ bool binary_decoded_input_has_nuls;
+};
+
+struct imap_msgpart *imap_msgpart_full(void);
+struct imap_msgpart *imap_msgpart_header(void);
+struct imap_msgpart *imap_msgpart_body(void);
+/* Parse section into imap_msgpart. Returns 0 and msgpart_r on success,
+ -1 if the section isn't valid. The same imap_msgpart can be used to open
+ multiple messages. */
+int imap_msgpart_parse(const char *section, struct imap_msgpart **msgpart_r);
+void imap_msgpart_free(struct imap_msgpart **msgpart);
+
+/* Returns TRUE if the msgpart might return at least part of the message body.
+ Or alternatively: If FALSE is returned, the msgpart will never return
+ anything except (part of) the message header. MIME headers are counted
+ as part of the message body. */
+bool imap_msgpart_contains_body(const struct imap_msgpart *msgpart);
+/* Decode MIME parts with Content-Transfer-Encoding: base64/quoted-printable
+ to binary data (IMAP BINARY extension). If something can't be decoded, fails
+ with storage error set to MAIL_ERROR_CONVERSION. */
+void imap_msgpart_set_decode_to_binary(struct imap_msgpart *msgpart);
+
+/* Set the fetch to be partial. For unlimited size use UOFF_T_MAX. */
+void imap_msgpart_set_partial(struct imap_msgpart *msgpart,
+ uoff_t offset, uoff_t size);
+uoff_t imap_msgpart_get_partial_offset(struct imap_msgpart *msgpart);
+uoff_t imap_msgpart_get_partial_size(struct imap_msgpart *msgpart);
+/* Return wanted_fields mask. */
+enum mail_fetch_field imap_msgpart_get_fetch_data(struct imap_msgpart *msgpart);
+/* Append all the specifically requested headers to the headers array
+ (no deduplication is done) */
+void imap_msgpart_get_wanted_headers(struct imap_msgpart *msgpart,
+ ARRAY_TYPE(const_string) *headers);
+
+/* Open message part refenced by IMAP section as istream. Returns 0 if
+ successful, -1 if storage error. Returned istream is initially referenced,
+ so i_stream_unref() must be called for it. */
+int imap_msgpart_open(struct mail *mail, struct imap_msgpart *msgpart,
+ struct imap_msgpart_open_result *result_r);
+/* Return msgpart's size without actually opening the stream (if possible). */
+int imap_msgpart_size(struct mail *mail, struct imap_msgpart *msgpart,
+ uoff_t *size_r);
+
+/* Return msgpart's IMAP BODYPARTSTRUCTURE */
+int imap_msgpart_bodypartstructure(struct mail *mail,
+ struct imap_msgpart *msgpart,
+ const char **bpstruct_r);
+
+/* Header context is automatically created by imap_msgpart_open() and destroyed
+ by imap_msgpart_free(), but if you want to use the same imap_msgpart across
+ multiple mailboxes, you need to close the part before closing the mailbox. */
+void imap_msgpart_close_mailbox(struct imap_msgpart *msgpart);
+
+#endif
diff --git a/src/lib-imap-urlauth/Makefile.am b/src/lib-imap-urlauth/Makefile.am
new file mode 100644
index 0000000..6734b72
--- /dev/null
+++ b/src/lib-imap-urlauth/Makefile.am
@@ -0,0 +1,29 @@
+noinst_LTLIBRARIES = libimap-urlauth.la
+
+AM_CPPFLAGS = \
+ -I$(top_srcdir)/src/lib \
+ -I$(top_srcdir)/src/lib-test \
+ -I$(top_srcdir)/src/lib-charset \
+ -I$(top_srcdir)/src/lib-dict \
+ -I$(top_srcdir)/src/lib-mail \
+ -I$(top_srcdir)/src/lib-index \
+ -I$(top_srcdir)/src/lib-storage \
+ -I$(top_srcdir)/src/lib-imap \
+ -I$(top_srcdir)/src/lib-imap-storage
+
+libimap_urlauth_la_SOURCES = \
+ imap-urlauth.c \
+ imap-urlauth-fetch.c \
+ imap-urlauth-backend.c \
+ imap-urlauth-connection.c
+
+headers = \
+ imap-urlauth.h \
+ imap-urlauth-private.h \
+ imap-urlauth-fetch.h \
+ imap-urlauth-backend.h \
+ imap-urlauth-connection.h
+
+pkginc_libdir=$(pkgincludedir)
+pkginc_lib_HEADERS = $(headers)
+
diff --git a/src/lib-imap-urlauth/Makefile.in b/src/lib-imap-urlauth/Makefile.in
new file mode 100644
index 0000000..3edb9bc
--- /dev/null
+++ b/src/lib-imap-urlauth/Makefile.in
@@ -0,0 +1,835 @@
+# Makefile.in generated by automake 1.16.1 from Makefile.am.
+# @configure_input@
+
+# Copyright (C) 1994-2018 Free Software Foundation, Inc.
+
+# This Makefile.in is free software; the Free Software Foundation
+# gives unlimited permission to copy and/or distribute it,
+# with or without modifications, as long as this notice is preserved.
+
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY, to the extent permitted by law; without
+# even the implied warranty of MERCHANTABILITY or FITNESS FOR A
+# PARTICULAR PURPOSE.
+
+@SET_MAKE@
+
+
+VPATH = @srcdir@
+am__is_gnu_make = { \
+ if test -z '$(MAKELEVEL)'; then \
+ false; \
+ elif test -n '$(MAKE_HOST)'; then \
+ true; \
+ elif test -n '$(MAKE_VERSION)' && test -n '$(CURDIR)'; then \
+ true; \
+ else \
+ false; \
+ fi; \
+}
+am__make_running_with_option = \
+ case $${target_option-} in \
+ ?) ;; \
+ *) echo "am__make_running_with_option: internal error: invalid" \
+ "target option '$${target_option-}' specified" >&2; \
+ exit 1;; \
+ esac; \
+ has_opt=no; \
+ sane_makeflags=$$MAKEFLAGS; \
+ if $(am__is_gnu_make); then \
+ sane_makeflags=$$MFLAGS; \
+ else \
+ case $$MAKEFLAGS in \
+ *\\[\ \ ]*) \
+ bs=\\; \
+ sane_makeflags=`printf '%s\n' "$$MAKEFLAGS" \
+ | sed "s/$$bs$$bs[$$bs $$bs ]*//g"`;; \
+ esac; \
+ fi; \
+ skip_next=no; \
+ strip_trailopt () \
+ { \
+ flg=`printf '%s\n' "$$flg" | sed "s/$$1.*$$//"`; \
+ }; \
+ for flg in $$sane_makeflags; do \
+ test $$skip_next = yes && { skip_next=no; continue; }; \
+ case $$flg in \
+ *=*|--*) continue;; \
+ -*I) strip_trailopt 'I'; skip_next=yes;; \
+ -*I?*) strip_trailopt 'I';; \
+ -*O) strip_trailopt 'O'; skip_next=yes;; \
+ -*O?*) strip_trailopt 'O';; \
+ -*l) strip_trailopt 'l'; skip_next=yes;; \
+ -*l?*) strip_trailopt 'l';; \
+ -[dEDm]) skip_next=yes;; \
+ -[JT]) skip_next=yes;; \
+ esac; \
+ case $$flg in \
+ *$$target_option*) has_opt=yes; break;; \
+ esac; \
+ done; \
+ test $$has_opt = yes
+am__make_dryrun = (target_option=n; $(am__make_running_with_option))
+am__make_keepgoing = (target_option=k; $(am__make_running_with_option))
+pkgdatadir = $(datadir)/@PACKAGE@
+pkgincludedir = $(includedir)/@PACKAGE@
+pkglibdir = $(libdir)/@PACKAGE@
+pkglibexecdir = $(libexecdir)/@PACKAGE@
+am__cd = CDPATH="$${ZSH_VERSION+.}$(PATH_SEPARATOR)" && cd
+install_sh_DATA = $(install_sh) -c -m 644
+install_sh_PROGRAM = $(install_sh) -c
+install_sh_SCRIPT = $(install_sh) -c
+INSTALL_HEADER = $(INSTALL_DATA)
+transform = $(program_transform_name)
+NORMAL_INSTALL = :
+PRE_INSTALL = :
+POST_INSTALL = :
+NORMAL_UNINSTALL = :
+PRE_UNINSTALL = :
+POST_UNINSTALL = :
+build_triplet = @build@
+host_triplet = @host@
+subdir = src/lib-imap-urlauth
+ACLOCAL_M4 = $(top_srcdir)/aclocal.m4
+am__aclocal_m4_deps = $(top_srcdir)/m4/ac_checktype2.m4 \
+ $(top_srcdir)/m4/ac_typeof.m4 $(top_srcdir)/m4/arc4random.m4 \
+ $(top_srcdir)/m4/blockdev.m4 $(top_srcdir)/m4/c99_vsnprintf.m4 \
+ $(top_srcdir)/m4/clock_gettime.m4 $(top_srcdir)/m4/crypt.m4 \
+ $(top_srcdir)/m4/crypt_xpg6.m4 $(top_srcdir)/m4/dbqlk.m4 \
+ $(top_srcdir)/m4/dirent_dtype.m4 $(top_srcdir)/m4/dovecot.m4 \
+ $(top_srcdir)/m4/fd_passing.m4 $(top_srcdir)/m4/fdatasync.m4 \
+ $(top_srcdir)/m4/flexible_array_member.m4 \
+ $(top_srcdir)/m4/glibc.m4 $(top_srcdir)/m4/gmtime_max.m4 \
+ $(top_srcdir)/m4/gmtime_tm_gmtoff.m4 \
+ $(top_srcdir)/m4/ioloop.m4 $(top_srcdir)/m4/iovec.m4 \
+ $(top_srcdir)/m4/ipv6.m4 $(top_srcdir)/m4/libcap.m4 \
+ $(top_srcdir)/m4/libtool.m4 $(top_srcdir)/m4/libwrap.m4 \
+ $(top_srcdir)/m4/linux_mremap.m4 $(top_srcdir)/m4/ltoptions.m4 \
+ $(top_srcdir)/m4/ltsugar.m4 $(top_srcdir)/m4/ltversion.m4 \
+ $(top_srcdir)/m4/lt~obsolete.m4 $(top_srcdir)/m4/mmap_write.m4 \
+ $(top_srcdir)/m4/mntctl.m4 $(top_srcdir)/m4/modules.m4 \
+ $(top_srcdir)/m4/notify.m4 $(top_srcdir)/m4/nsl.m4 \
+ $(top_srcdir)/m4/off_t_max.m4 $(top_srcdir)/m4/pkg.m4 \
+ $(top_srcdir)/m4/pr_set_dumpable.m4 \
+ $(top_srcdir)/m4/q_quotactl.m4 $(top_srcdir)/m4/quota.m4 \
+ $(top_srcdir)/m4/random.m4 $(top_srcdir)/m4/rlimit.m4 \
+ $(top_srcdir)/m4/sendfile.m4 $(top_srcdir)/m4/size_t_signed.m4 \
+ $(top_srcdir)/m4/sockpeercred.m4 $(top_srcdir)/m4/sql.m4 \
+ $(top_srcdir)/m4/ssl.m4 $(top_srcdir)/m4/st_tim.m4 \
+ $(top_srcdir)/m4/static_array.m4 $(top_srcdir)/m4/test_with.m4 \
+ $(top_srcdir)/m4/time_t.m4 $(top_srcdir)/m4/typeof.m4 \
+ $(top_srcdir)/m4/typeof_dev_t.m4 \
+ $(top_srcdir)/m4/uoff_t_max.m4 $(top_srcdir)/m4/vararg.m4 \
+ $(top_srcdir)/m4/want_apparmor.m4 \
+ $(top_srcdir)/m4/want_bsdauth.m4 \
+ $(top_srcdir)/m4/want_bzlib.m4 \
+ $(top_srcdir)/m4/want_cassandra.m4 \
+ $(top_srcdir)/m4/want_cdb.m4 \
+ $(top_srcdir)/m4/want_checkpassword.m4 \
+ $(top_srcdir)/m4/want_clucene.m4 $(top_srcdir)/m4/want_db.m4 \
+ $(top_srcdir)/m4/want_gssapi.m4 $(top_srcdir)/m4/want_icu.m4 \
+ $(top_srcdir)/m4/want_ldap.m4 $(top_srcdir)/m4/want_lua.m4 \
+ $(top_srcdir)/m4/want_lz4.m4 $(top_srcdir)/m4/want_lzma.m4 \
+ $(top_srcdir)/m4/want_mysql.m4 $(top_srcdir)/m4/want_pam.m4 \
+ $(top_srcdir)/m4/want_passwd.m4 $(top_srcdir)/m4/want_pgsql.m4 \
+ $(top_srcdir)/m4/want_prefetch.m4 \
+ $(top_srcdir)/m4/want_shadow.m4 \
+ $(top_srcdir)/m4/want_sodium.m4 $(top_srcdir)/m4/want_solr.m4 \
+ $(top_srcdir)/m4/want_sqlite.m4 \
+ $(top_srcdir)/m4/want_stemmer.m4 \
+ $(top_srcdir)/m4/want_systemd.m4 \
+ $(top_srcdir)/m4/want_textcat.m4 \
+ $(top_srcdir)/m4/want_unwind.m4 $(top_srcdir)/m4/want_zlib.m4 \
+ $(top_srcdir)/m4/want_zstd.m4 $(top_srcdir)/configure.ac
+am__configure_deps = $(am__aclocal_m4_deps) $(CONFIGURE_DEPENDENCIES) \
+ $(ACLOCAL_M4)
+DIST_COMMON = $(srcdir)/Makefile.am $(pkginc_lib_HEADERS) \
+ $(am__DIST_COMMON)
+mkinstalldirs = $(install_sh) -d
+CONFIG_HEADER = $(top_builddir)/config.h
+CONFIG_CLEAN_FILES =
+CONFIG_CLEAN_VPATH_FILES =
+LTLIBRARIES = $(noinst_LTLIBRARIES)
+libimap_urlauth_la_LIBADD =
+am_libimap_urlauth_la_OBJECTS = imap-urlauth.lo imap-urlauth-fetch.lo \
+ imap-urlauth-backend.lo imap-urlauth-connection.lo
+libimap_urlauth_la_OBJECTS = $(am_libimap_urlauth_la_OBJECTS)
+AM_V_lt = $(am__v_lt_@AM_V@)
+am__v_lt_ = $(am__v_lt_@AM_DEFAULT_V@)
+am__v_lt_0 = --silent
+am__v_lt_1 =
+AM_V_P = $(am__v_P_@AM_V@)
+am__v_P_ = $(am__v_P_@AM_DEFAULT_V@)
+am__v_P_0 = false
+am__v_P_1 = :
+AM_V_GEN = $(am__v_GEN_@AM_V@)
+am__v_GEN_ = $(am__v_GEN_@AM_DEFAULT_V@)
+am__v_GEN_0 = @echo " GEN " $@;
+am__v_GEN_1 =
+AM_V_at = $(am__v_at_@AM_V@)
+am__v_at_ = $(am__v_at_@AM_DEFAULT_V@)
+am__v_at_0 = @
+am__v_at_1 =
+DEFAULT_INCLUDES = -I.@am__isrc@ -I$(top_builddir)
+depcomp = $(SHELL) $(top_srcdir)/depcomp
+am__maybe_remake_depfiles = depfiles
+am__depfiles_remade = ./$(DEPDIR)/imap-urlauth-backend.Plo \
+ ./$(DEPDIR)/imap-urlauth-connection.Plo \
+ ./$(DEPDIR)/imap-urlauth-fetch.Plo \
+ ./$(DEPDIR)/imap-urlauth.Plo
+am__mv = mv -f
+COMPILE = $(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) \
+ $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS)
+LTCOMPILE = $(LIBTOOL) $(AM_V_lt) --tag=CC $(AM_LIBTOOLFLAGS) \
+ $(LIBTOOLFLAGS) --mode=compile $(CC) $(DEFS) \
+ $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) $(CPPFLAGS) \
+ $(AM_CFLAGS) $(CFLAGS)
+AM_V_CC = $(am__v_CC_@AM_V@)
+am__v_CC_ = $(am__v_CC_@AM_DEFAULT_V@)
+am__v_CC_0 = @echo " CC " $@;
+am__v_CC_1 =
+CCLD = $(CC)
+LINK = $(LIBTOOL) $(AM_V_lt) --tag=CC $(AM_LIBTOOLFLAGS) \
+ $(LIBTOOLFLAGS) --mode=link $(CCLD) $(AM_CFLAGS) $(CFLAGS) \
+ $(AM_LDFLAGS) $(LDFLAGS) -o $@
+AM_V_CCLD = $(am__v_CCLD_@AM_V@)
+am__v_CCLD_ = $(am__v_CCLD_@AM_DEFAULT_V@)
+am__v_CCLD_0 = @echo " CCLD " $@;
+am__v_CCLD_1 =
+SOURCES = $(libimap_urlauth_la_SOURCES)
+DIST_SOURCES = $(libimap_urlauth_la_SOURCES)
+am__can_run_installinfo = \
+ case $$AM_UPDATE_INFO_DIR in \
+ n|no|NO) false;; \
+ *) (install-info --version) >/dev/null 2>&1;; \
+ esac
+am__vpath_adj_setup = srcdirstrip=`echo "$(srcdir)" | sed 's|.|.|g'`;
+am__vpath_adj = case $$p in \
+ $(srcdir)/*) f=`echo "$$p" | sed "s|^$$srcdirstrip/||"`;; \
+ *) f=$$p;; \
+ esac;
+am__strip_dir = f=`echo $$p | sed -e 's|^.*/||'`;
+am__install_max = 40
+am__nobase_strip_setup = \
+ srcdirstrip=`echo "$(srcdir)" | sed 's/[].[^$$\\*|]/\\\\&/g'`
+am__nobase_strip = \
+ for p in $$list; do echo "$$p"; done | sed -e "s|$$srcdirstrip/||"
+am__nobase_list = $(am__nobase_strip_setup); \
+ for p in $$list; do echo "$$p $$p"; done | \
+ sed "s| $$srcdirstrip/| |;"' / .*\//!s/ .*/ ./; s,\( .*\)/[^/]*$$,\1,' | \
+ $(AWK) 'BEGIN { files["."] = "" } { files[$$2] = files[$$2] " " $$1; \
+ if (++n[$$2] == $(am__install_max)) \
+ { print $$2, files[$$2]; n[$$2] = 0; files[$$2] = "" } } \
+ END { for (dir in files) print dir, files[dir] }'
+am__base_list = \
+ sed '$$!N;$$!N;$$!N;$$!N;$$!N;$$!N;$$!N;s/\n/ /g' | \
+ sed '$$!N;$$!N;$$!N;$$!N;s/\n/ /g'
+am__uninstall_files_from_dir = { \
+ test -z "$$files" \
+ || { test ! -d "$$dir" && test ! -f "$$dir" && test ! -r "$$dir"; } \
+ || { echo " ( cd '$$dir' && rm -f" $$files ")"; \
+ $(am__cd) "$$dir" && rm -f $$files; }; \
+ }
+am__installdirs = "$(DESTDIR)$(pkginc_libdir)"
+HEADERS = $(pkginc_lib_HEADERS)
+am__tagged_files = $(HEADERS) $(SOURCES) $(TAGS_FILES) $(LISP)
+# Read a list of newline-separated strings from the standard input,
+# and print each of them once, without duplicates. Input order is
+# *not* preserved.
+am__uniquify_input = $(AWK) '\
+ BEGIN { nonempty = 0; } \
+ { items[$$0] = 1; nonempty = 1; } \
+ END { if (nonempty) { for (i in items) print i; }; } \
+'
+# Make sure the list of sources is unique. This is necessary because,
+# e.g., the same source file might be shared among _SOURCES variables
+# for different programs/libraries.
+am__define_uniq_tagged_files = \
+ list='$(am__tagged_files)'; \
+ unique=`for i in $$list; do \
+ if test -f "$$i"; then echo $$i; else echo $(srcdir)/$$i; fi; \
+ done | $(am__uniquify_input)`
+ETAGS = etags
+CTAGS = ctags
+am__DIST_COMMON = $(srcdir)/Makefile.in $(top_srcdir)/depcomp
+DISTFILES = $(DIST_COMMON) $(DIST_SOURCES) $(TEXINFOS) $(EXTRA_DIST)
+ACLOCAL = @ACLOCAL@
+ACLOCAL_AMFLAGS = @ACLOCAL_AMFLAGS@
+AMTAR = @AMTAR@
+AM_DEFAULT_VERBOSITY = @AM_DEFAULT_VERBOSITY@
+APPARMOR_LIBS = @APPARMOR_LIBS@
+AR = @AR@
+AUTH_CFLAGS = @AUTH_CFLAGS@
+AUTH_LIBS = @AUTH_LIBS@
+AUTOCONF = @AUTOCONF@
+AUTOHEADER = @AUTOHEADER@
+AUTOMAKE = @AUTOMAKE@
+AWK = @AWK@
+BINARY_CFLAGS = @BINARY_CFLAGS@
+BINARY_LDFLAGS = @BINARY_LDFLAGS@
+BISON = @BISON@
+CASSANDRA_CFLAGS = @CASSANDRA_CFLAGS@
+CASSANDRA_LIBS = @CASSANDRA_LIBS@
+CC = @CC@
+CCDEPMODE = @CCDEPMODE@
+CDB_LIBS = @CDB_LIBS@
+CFLAGS = @CFLAGS@
+CLUCENE_CFLAGS = @CLUCENE_CFLAGS@
+CLUCENE_LIBS = @CLUCENE_LIBS@
+COMPRESS_LIBS = @COMPRESS_LIBS@
+CPP = @CPP@
+CPPFLAGS = @CPPFLAGS@
+CRYPT_LIBS = @CRYPT_LIBS@
+CXX = @CXX@
+CXXCPP = @CXXCPP@
+CXXDEPMODE = @CXXDEPMODE@
+CXXFLAGS = @CXXFLAGS@
+CYGPATH_W = @CYGPATH_W@
+DEFS = @DEFS@
+DEPDIR = @DEPDIR@
+DICT_LIBS = @DICT_LIBS@
+DLLIB = @DLLIB@
+DLLTOOL = @DLLTOOL@
+DSYMUTIL = @DSYMUTIL@
+DUMPBIN = @DUMPBIN@
+ECHO_C = @ECHO_C@
+ECHO_N = @ECHO_N@
+ECHO_T = @ECHO_T@
+EGREP = @EGREP@
+EXEEXT = @EXEEXT@
+FGREP = @FGREP@
+FLEX = @FLEX@
+FUZZER_CPPFLAGS = @FUZZER_CPPFLAGS@
+FUZZER_LDFLAGS = @FUZZER_LDFLAGS@
+GREP = @GREP@
+INSTALL = @INSTALL@
+INSTALL_DATA = @INSTALL_DATA@
+INSTALL_PROGRAM = @INSTALL_PROGRAM@
+INSTALL_SCRIPT = @INSTALL_SCRIPT@
+INSTALL_STRIP_PROGRAM = @INSTALL_STRIP_PROGRAM@
+KRB5CONFIG = @KRB5CONFIG@
+KRB5_CFLAGS = @KRB5_CFLAGS@
+KRB5_LIBS = @KRB5_LIBS@
+LD = @LD@
+LDAP_LIBS = @LDAP_LIBS@
+LDFLAGS = @LDFLAGS@
+LD_NO_WHOLE_ARCHIVE = @LD_NO_WHOLE_ARCHIVE@
+LD_WHOLE_ARCHIVE = @LD_WHOLE_ARCHIVE@
+LIBCAP = @LIBCAP@
+LIBDOVECOT = @LIBDOVECOT@
+LIBDOVECOT_COMPRESS = @LIBDOVECOT_COMPRESS@
+LIBDOVECOT_DEPS = @LIBDOVECOT_DEPS@
+LIBDOVECOT_DSYNC = @LIBDOVECOT_DSYNC@
+LIBDOVECOT_LA_LIBS = @LIBDOVECOT_LA_LIBS@
+LIBDOVECOT_LDA = @LIBDOVECOT_LDA@
+LIBDOVECOT_LDAP = @LIBDOVECOT_LDAP@
+LIBDOVECOT_LIBFTS = @LIBDOVECOT_LIBFTS@
+LIBDOVECOT_LIBFTS_DEPS = @LIBDOVECOT_LIBFTS_DEPS@
+LIBDOVECOT_LOGIN = @LIBDOVECOT_LOGIN@
+LIBDOVECOT_LUA = @LIBDOVECOT_LUA@
+LIBDOVECOT_LUA_DEPS = @LIBDOVECOT_LUA_DEPS@
+LIBDOVECOT_SQL = @LIBDOVECOT_SQL@
+LIBDOVECOT_STORAGE = @LIBDOVECOT_STORAGE@
+LIBDOVECOT_STORAGE_DEPS = @LIBDOVECOT_STORAGE_DEPS@
+LIBEXTTEXTCAT_CFLAGS = @LIBEXTTEXTCAT_CFLAGS@
+LIBEXTTEXTCAT_LIBS = @LIBEXTTEXTCAT_LIBS@
+LIBICONV = @LIBICONV@
+LIBICU_CFLAGS = @LIBICU_CFLAGS@
+LIBICU_LIBS = @LIBICU_LIBS@
+LIBOBJS = @LIBOBJS@
+LIBS = @LIBS@
+LIBSODIUM_CFLAGS = @LIBSODIUM_CFLAGS@
+LIBSODIUM_LIBS = @LIBSODIUM_LIBS@
+LIBTIRPC_CFLAGS = @LIBTIRPC_CFLAGS@
+LIBTIRPC_LIBS = @LIBTIRPC_LIBS@
+LIBTOOL = @LIBTOOL@
+LIBUNWIND_CFLAGS = @LIBUNWIND_CFLAGS@
+LIBUNWIND_LIBS = @LIBUNWIND_LIBS@
+LIBWRAP_LIBS = @LIBWRAP_LIBS@
+LINKED_STORAGE_LDADD = @LINKED_STORAGE_LDADD@
+LIPO = @LIPO@
+LN_S = @LN_S@
+LTLIBICONV = @LTLIBICONV@
+LTLIBOBJS = @LTLIBOBJS@
+LT_SYS_LIBRARY_PATH = @LT_SYS_LIBRARY_PATH@
+LUA_CFLAGS = @LUA_CFLAGS@
+LUA_LIBS = @LUA_LIBS@
+MAINT = @MAINT@
+MAKEINFO = @MAKEINFO@
+MANIFEST_TOOL = @MANIFEST_TOOL@
+MKDIR_P = @MKDIR_P@
+MODULE_LIBS = @MODULE_LIBS@
+MODULE_SUFFIX = @MODULE_SUFFIX@
+MYSQL_CFLAGS = @MYSQL_CFLAGS@
+MYSQL_CONFIG = @MYSQL_CONFIG@
+MYSQL_LIBS = @MYSQL_LIBS@
+NM = @NM@
+NMEDIT = @NMEDIT@
+NOPLUGIN_LDFLAGS = @NOPLUGIN_LDFLAGS@
+OBJDUMP = @OBJDUMP@
+OBJEXT = @OBJEXT@
+OTOOL = @OTOOL@
+OTOOL64 = @OTOOL64@
+PACKAGE = @PACKAGE@
+PACKAGE_BUGREPORT = @PACKAGE_BUGREPORT@
+PACKAGE_NAME = @PACKAGE_NAME@
+PACKAGE_STRING = @PACKAGE_STRING@
+PACKAGE_TARNAME = @PACKAGE_TARNAME@
+PACKAGE_URL = @PACKAGE_URL@
+PACKAGE_VERSION = @PACKAGE_VERSION@
+PANDOC = @PANDOC@
+PATH_SEPARATOR = @PATH_SEPARATOR@
+PGSQL_CFLAGS = @PGSQL_CFLAGS@
+PGSQL_LIBS = @PGSQL_LIBS@
+PG_CONFIG = @PG_CONFIG@
+PIE_CFLAGS = @PIE_CFLAGS@
+PIE_LDFLAGS = @PIE_LDFLAGS@
+PKG_CONFIG = @PKG_CONFIG@
+PKG_CONFIG_LIBDIR = @PKG_CONFIG_LIBDIR@
+PKG_CONFIG_PATH = @PKG_CONFIG_PATH@
+QUOTA_LIBS = @QUOTA_LIBS@
+RANLIB = @RANLIB@
+RELRO_LDFLAGS = @RELRO_LDFLAGS@
+RPCGEN = @RPCGEN@
+RUN_TEST = @RUN_TEST@
+SED = @SED@
+SETTING_FILES = @SETTING_FILES@
+SET_MAKE = @SET_MAKE@
+SHELL = @SHELL@
+SQLITE_CFLAGS = @SQLITE_CFLAGS@
+SQLITE_LIBS = @SQLITE_LIBS@
+SQL_CFLAGS = @SQL_CFLAGS@
+SQL_LIBS = @SQL_LIBS@
+SSL_CFLAGS = @SSL_CFLAGS@
+SSL_LIBS = @SSL_LIBS@
+STRIP = @STRIP@
+SYSTEMD_CFLAGS = @SYSTEMD_CFLAGS@
+SYSTEMD_LIBS = @SYSTEMD_LIBS@
+VALGRIND = @VALGRIND@
+VERSION = @VERSION@
+ZSTD_CFLAGS = @ZSTD_CFLAGS@
+ZSTD_LIBS = @ZSTD_LIBS@
+abs_builddir = @abs_builddir@
+abs_srcdir = @abs_srcdir@
+abs_top_builddir = @abs_top_builddir@
+abs_top_srcdir = @abs_top_srcdir@
+ac_ct_AR = @ac_ct_AR@
+ac_ct_CC = @ac_ct_CC@
+ac_ct_CXX = @ac_ct_CXX@
+ac_ct_DUMPBIN = @ac_ct_DUMPBIN@
+am__include = @am__include@
+am__leading_dot = @am__leading_dot@
+am__quote = @am__quote@
+am__tar = @am__tar@
+am__untar = @am__untar@
+bindir = @bindir@
+build = @build@
+build_alias = @build_alias@
+build_cpu = @build_cpu@
+build_os = @build_os@
+build_vendor = @build_vendor@
+builddir = @builddir@
+datadir = @datadir@
+datarootdir = @datarootdir@
+dict_drivers = @dict_drivers@
+docdir = @docdir@
+dvidir = @dvidir@
+exec_prefix = @exec_prefix@
+host = @host@
+host_alias = @host_alias@
+host_cpu = @host_cpu@
+host_os = @host_os@
+host_vendor = @host_vendor@
+htmldir = @htmldir@
+includedir = @includedir@
+infodir = @infodir@
+install_sh = @install_sh@
+libdir = @libdir@
+libexecdir = @libexecdir@
+localedir = @localedir@
+localstatedir = @localstatedir@
+mandir = @mandir@
+mkdir_p = @mkdir_p@
+moduledir = @moduledir@
+oldincludedir = @oldincludedir@
+pdfdir = @pdfdir@
+prefix = @prefix@
+program_transform_name = @program_transform_name@
+psdir = @psdir@
+rundir = @rundir@
+runstatedir = @runstatedir@
+sbindir = @sbindir@
+sharedstatedir = @sharedstatedir@
+sql_drivers = @sql_drivers@
+srcdir = @srcdir@
+ssldir = @ssldir@
+statedir = @statedir@
+sysconfdir = @sysconfdir@
+systemdservicetype = @systemdservicetype@
+systemdsystemunitdir = @systemdsystemunitdir@
+target_alias = @target_alias@
+top_build_prefix = @top_build_prefix@
+top_builddir = @top_builddir@
+top_srcdir = @top_srcdir@
+noinst_LTLIBRARIES = libimap-urlauth.la
+AM_CPPFLAGS = \
+ -I$(top_srcdir)/src/lib \
+ -I$(top_srcdir)/src/lib-test \
+ -I$(top_srcdir)/src/lib-charset \
+ -I$(top_srcdir)/src/lib-dict \
+ -I$(top_srcdir)/src/lib-mail \
+ -I$(top_srcdir)/src/lib-index \
+ -I$(top_srcdir)/src/lib-storage \
+ -I$(top_srcdir)/src/lib-imap \
+ -I$(top_srcdir)/src/lib-imap-storage
+
+libimap_urlauth_la_SOURCES = \
+ imap-urlauth.c \
+ imap-urlauth-fetch.c \
+ imap-urlauth-backend.c \
+ imap-urlauth-connection.c
+
+headers = \
+ imap-urlauth.h \
+ imap-urlauth-private.h \
+ imap-urlauth-fetch.h \
+ imap-urlauth-backend.h \
+ imap-urlauth-connection.h
+
+pkginc_libdir = $(pkgincludedir)
+pkginc_lib_HEADERS = $(headers)
+all: all-am
+
+.SUFFIXES:
+.SUFFIXES: .c .lo .o .obj
+$(srcdir)/Makefile.in: @MAINTAINER_MODE_TRUE@ $(srcdir)/Makefile.am $(am__configure_deps)
+ @for dep in $?; do \
+ case '$(am__configure_deps)' in \
+ *$$dep*) \
+ ( cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh ) \
+ && { if test -f $@; then exit 0; else break; fi; }; \
+ exit 1;; \
+ esac; \
+ done; \
+ echo ' cd $(top_srcdir) && $(AUTOMAKE) --foreign src/lib-imap-urlauth/Makefile'; \
+ $(am__cd) $(top_srcdir) && \
+ $(AUTOMAKE) --foreign src/lib-imap-urlauth/Makefile
+Makefile: $(srcdir)/Makefile.in $(top_builddir)/config.status
+ @case '$?' in \
+ *config.status*) \
+ cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh;; \
+ *) \
+ echo ' cd $(top_builddir) && $(SHELL) ./config.status $(subdir)/$@ $(am__maybe_remake_depfiles)'; \
+ cd $(top_builddir) && $(SHELL) ./config.status $(subdir)/$@ $(am__maybe_remake_depfiles);; \
+ esac;
+
+$(top_builddir)/config.status: $(top_srcdir)/configure $(CONFIG_STATUS_DEPENDENCIES)
+ cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh
+
+$(top_srcdir)/configure: @MAINTAINER_MODE_TRUE@ $(am__configure_deps)
+ cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh
+$(ACLOCAL_M4): @MAINTAINER_MODE_TRUE@ $(am__aclocal_m4_deps)
+ cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh
+$(am__aclocal_m4_deps):
+
+clean-noinstLTLIBRARIES:
+ -test -z "$(noinst_LTLIBRARIES)" || rm -f $(noinst_LTLIBRARIES)
+ @list='$(noinst_LTLIBRARIES)'; \
+ locs=`for p in $$list; do echo $$p; done | \
+ sed 's|^[^/]*$$|.|; s|/[^/]*$$||; s|$$|/so_locations|' | \
+ sort -u`; \
+ test -z "$$locs" || { \
+ echo rm -f $${locs}; \
+ rm -f $${locs}; \
+ }
+
+libimap-urlauth.la: $(libimap_urlauth_la_OBJECTS) $(libimap_urlauth_la_DEPENDENCIES) $(EXTRA_libimap_urlauth_la_DEPENDENCIES)
+ $(AM_V_CCLD)$(LINK) $(libimap_urlauth_la_OBJECTS) $(libimap_urlauth_la_LIBADD) $(LIBS)
+
+mostlyclean-compile:
+ -rm -f *.$(OBJEXT)
+
+distclean-compile:
+ -rm -f *.tab.c
+
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/imap-urlauth-backend.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/imap-urlauth-connection.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/imap-urlauth-fetch.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/imap-urlauth.Plo@am__quote@ # am--include-marker
+
+$(am__depfiles_remade):
+ @$(MKDIR_P) $(@D)
+ @echo '# dummy' >$@-t && $(am__mv) $@-t $@
+
+am--depfiles: $(am__depfiles_remade)
+
+.c.o:
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(COMPILE) -MT $@ -MD -MP -MF $(DEPDIR)/$*.Tpo -c -o $@ $<
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/$*.Tpo $(DEPDIR)/$*.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='$<' object='$@' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(COMPILE) -c -o $@ $<
+
+.c.obj:
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(COMPILE) -MT $@ -MD -MP -MF $(DEPDIR)/$*.Tpo -c -o $@ `$(CYGPATH_W) '$<'`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/$*.Tpo $(DEPDIR)/$*.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='$<' object='$@' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(COMPILE) -c -o $@ `$(CYGPATH_W) '$<'`
+
+.c.lo:
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(LTCOMPILE) -MT $@ -MD -MP -MF $(DEPDIR)/$*.Tpo -c -o $@ $<
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/$*.Tpo $(DEPDIR)/$*.Plo
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='$<' object='$@' libtool=yes @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(LTCOMPILE) -c -o $@ $<
+
+mostlyclean-libtool:
+ -rm -f *.lo
+
+clean-libtool:
+ -rm -rf .libs _libs
+install-pkginc_libHEADERS: $(pkginc_lib_HEADERS)
+ @$(NORMAL_INSTALL)
+ @list='$(pkginc_lib_HEADERS)'; test -n "$(pkginc_libdir)" || list=; \
+ if test -n "$$list"; then \
+ echo " $(MKDIR_P) '$(DESTDIR)$(pkginc_libdir)'"; \
+ $(MKDIR_P) "$(DESTDIR)$(pkginc_libdir)" || exit 1; \
+ fi; \
+ for p in $$list; do \
+ if test -f "$$p"; then d=; else d="$(srcdir)/"; fi; \
+ echo "$$d$$p"; \
+ done | $(am__base_list) | \
+ while read files; do \
+ echo " $(INSTALL_HEADER) $$files '$(DESTDIR)$(pkginc_libdir)'"; \
+ $(INSTALL_HEADER) $$files "$(DESTDIR)$(pkginc_libdir)" || exit $$?; \
+ done
+
+uninstall-pkginc_libHEADERS:
+ @$(NORMAL_UNINSTALL)
+ @list='$(pkginc_lib_HEADERS)'; test -n "$(pkginc_libdir)" || list=; \
+ files=`for p in $$list; do echo $$p; done | sed -e 's|^.*/||'`; \
+ dir='$(DESTDIR)$(pkginc_libdir)'; $(am__uninstall_files_from_dir)
+
+ID: $(am__tagged_files)
+ $(am__define_uniq_tagged_files); mkid -fID $$unique
+tags: tags-am
+TAGS: tags
+
+tags-am: $(TAGS_DEPENDENCIES) $(am__tagged_files)
+ set x; \
+ here=`pwd`; \
+ $(am__define_uniq_tagged_files); \
+ shift; \
+ if test -z "$(ETAGS_ARGS)$$*$$unique"; then :; else \
+ test -n "$$unique" || unique=$$empty_fix; \
+ if test $$# -gt 0; then \
+ $(ETAGS) $(ETAGSFLAGS) $(AM_ETAGSFLAGS) $(ETAGS_ARGS) \
+ "$$@" $$unique; \
+ else \
+ $(ETAGS) $(ETAGSFLAGS) $(AM_ETAGSFLAGS) $(ETAGS_ARGS) \
+ $$unique; \
+ fi; \
+ fi
+ctags: ctags-am
+
+CTAGS: ctags
+ctags-am: $(TAGS_DEPENDENCIES) $(am__tagged_files)
+ $(am__define_uniq_tagged_files); \
+ test -z "$(CTAGS_ARGS)$$unique" \
+ || $(CTAGS) $(CTAGSFLAGS) $(AM_CTAGSFLAGS) $(CTAGS_ARGS) \
+ $$unique
+
+GTAGS:
+ here=`$(am__cd) $(top_builddir) && pwd` \
+ && $(am__cd) $(top_srcdir) \
+ && gtags -i $(GTAGS_ARGS) "$$here"
+cscopelist: cscopelist-am
+
+cscopelist-am: $(am__tagged_files)
+ list='$(am__tagged_files)'; \
+ case "$(srcdir)" in \
+ [\\/]* | ?:[\\/]*) sdir="$(srcdir)" ;; \
+ *) sdir=$(subdir)/$(srcdir) ;; \
+ esac; \
+ for i in $$list; do \
+ if test -f "$$i"; then \
+ echo "$(subdir)/$$i"; \
+ else \
+ echo "$$sdir/$$i"; \
+ fi; \
+ done >> $(top_builddir)/cscope.files
+
+distclean-tags:
+ -rm -f TAGS ID GTAGS GRTAGS GSYMS GPATH tags
+
+distdir: $(BUILT_SOURCES)
+ $(MAKE) $(AM_MAKEFLAGS) distdir-am
+
+distdir-am: $(DISTFILES)
+ @srcdirstrip=`echo "$(srcdir)" | sed 's/[].[^$$\\*]/\\\\&/g'`; \
+ topsrcdirstrip=`echo "$(top_srcdir)" | sed 's/[].[^$$\\*]/\\\\&/g'`; \
+ list='$(DISTFILES)'; \
+ dist_files=`for file in $$list; do echo $$file; done | \
+ sed -e "s|^$$srcdirstrip/||;t" \
+ -e "s|^$$topsrcdirstrip/|$(top_builddir)/|;t"`; \
+ case $$dist_files in \
+ */*) $(MKDIR_P) `echo "$$dist_files" | \
+ sed '/\//!d;s|^|$(distdir)/|;s,/[^/]*$$,,' | \
+ sort -u` ;; \
+ esac; \
+ for file in $$dist_files; do \
+ if test -f $$file || test -d $$file; then d=.; else d=$(srcdir); fi; \
+ if test -d $$d/$$file; then \
+ dir=`echo "/$$file" | sed -e 's,/[^/]*$$,,'`; \
+ if test -d "$(distdir)/$$file"; then \
+ find "$(distdir)/$$file" -type d ! -perm -700 -exec chmod u+rwx {} \;; \
+ fi; \
+ if test -d $(srcdir)/$$file && test $$d != $(srcdir); then \
+ cp -fpR $(srcdir)/$$file "$(distdir)$$dir" || exit 1; \
+ find "$(distdir)/$$file" -type d ! -perm -700 -exec chmod u+rwx {} \;; \
+ fi; \
+ cp -fpR $$d/$$file "$(distdir)$$dir" || exit 1; \
+ else \
+ test -f "$(distdir)/$$file" \
+ || cp -p $$d/$$file "$(distdir)/$$file" \
+ || exit 1; \
+ fi; \
+ done
+check-am: all-am
+check: check-am
+all-am: Makefile $(LTLIBRARIES) $(HEADERS)
+installdirs:
+ for dir in "$(DESTDIR)$(pkginc_libdir)"; do \
+ test -z "$$dir" || $(MKDIR_P) "$$dir"; \
+ done
+install: install-am
+install-exec: install-exec-am
+install-data: install-data-am
+uninstall: uninstall-am
+
+install-am: all-am
+ @$(MAKE) $(AM_MAKEFLAGS) install-exec-am install-data-am
+
+installcheck: installcheck-am
+install-strip:
+ if test -z '$(STRIP)'; then \
+ $(MAKE) $(AM_MAKEFLAGS) INSTALL_PROGRAM="$(INSTALL_STRIP_PROGRAM)" \
+ install_sh_PROGRAM="$(INSTALL_STRIP_PROGRAM)" INSTALL_STRIP_FLAG=-s \
+ install; \
+ else \
+ $(MAKE) $(AM_MAKEFLAGS) INSTALL_PROGRAM="$(INSTALL_STRIP_PROGRAM)" \
+ install_sh_PROGRAM="$(INSTALL_STRIP_PROGRAM)" INSTALL_STRIP_FLAG=-s \
+ "INSTALL_PROGRAM_ENV=STRIPPROG='$(STRIP)'" install; \
+ fi
+mostlyclean-generic:
+
+clean-generic:
+
+distclean-generic:
+ -test -z "$(CONFIG_CLEAN_FILES)" || rm -f $(CONFIG_CLEAN_FILES)
+ -test . = "$(srcdir)" || test -z "$(CONFIG_CLEAN_VPATH_FILES)" || rm -f $(CONFIG_CLEAN_VPATH_FILES)
+
+maintainer-clean-generic:
+ @echo "This command is intended for maintainers to use"
+ @echo "it deletes files that may require special tools to rebuild."
+clean: clean-am
+
+clean-am: clean-generic clean-libtool clean-noinstLTLIBRARIES \
+ mostlyclean-am
+
+distclean: distclean-am
+ -rm -f ./$(DEPDIR)/imap-urlauth-backend.Plo
+ -rm -f ./$(DEPDIR)/imap-urlauth-connection.Plo
+ -rm -f ./$(DEPDIR)/imap-urlauth-fetch.Plo
+ -rm -f ./$(DEPDIR)/imap-urlauth.Plo
+ -rm -f Makefile
+distclean-am: clean-am distclean-compile distclean-generic \
+ distclean-tags
+
+dvi: dvi-am
+
+dvi-am:
+
+html: html-am
+
+html-am:
+
+info: info-am
+
+info-am:
+
+install-data-am: install-pkginc_libHEADERS
+
+install-dvi: install-dvi-am
+
+install-dvi-am:
+
+install-exec-am:
+
+install-html: install-html-am
+
+install-html-am:
+
+install-info: install-info-am
+
+install-info-am:
+
+install-man:
+
+install-pdf: install-pdf-am
+
+install-pdf-am:
+
+install-ps: install-ps-am
+
+install-ps-am:
+
+installcheck-am:
+
+maintainer-clean: maintainer-clean-am
+ -rm -f ./$(DEPDIR)/imap-urlauth-backend.Plo
+ -rm -f ./$(DEPDIR)/imap-urlauth-connection.Plo
+ -rm -f ./$(DEPDIR)/imap-urlauth-fetch.Plo
+ -rm -f ./$(DEPDIR)/imap-urlauth.Plo
+ -rm -f Makefile
+maintainer-clean-am: distclean-am maintainer-clean-generic
+
+mostlyclean: mostlyclean-am
+
+mostlyclean-am: mostlyclean-compile mostlyclean-generic \
+ mostlyclean-libtool
+
+pdf: pdf-am
+
+pdf-am:
+
+ps: ps-am
+
+ps-am:
+
+uninstall-am: uninstall-pkginc_libHEADERS
+
+.MAKE: install-am install-strip
+
+.PHONY: CTAGS GTAGS TAGS all all-am am--depfiles check check-am clean \
+ clean-generic clean-libtool clean-noinstLTLIBRARIES \
+ cscopelist-am ctags ctags-am distclean distclean-compile \
+ distclean-generic distclean-libtool distclean-tags distdir dvi \
+ dvi-am html html-am info info-am install install-am \
+ install-data install-data-am install-dvi install-dvi-am \
+ install-exec install-exec-am install-html install-html-am \
+ install-info install-info-am install-man install-pdf \
+ install-pdf-am install-pkginc_libHEADERS install-ps \
+ install-ps-am install-strip installcheck installcheck-am \
+ installdirs maintainer-clean maintainer-clean-generic \
+ mostlyclean mostlyclean-compile mostlyclean-generic \
+ mostlyclean-libtool pdf pdf-am ps ps-am tags tags-am uninstall \
+ uninstall-am uninstall-pkginc_libHEADERS
+
+.PRECIOUS: Makefile
+
+
+# Tell versions [3.59,3.63) of GNU make to not export all variables.
+# Otherwise a system limit (for SysV at least) may be exceeded.
+.NOEXPORT:
diff --git a/src/lib-imap-urlauth/imap-urlauth-backend.c b/src/lib-imap-urlauth/imap-urlauth-backend.c
new file mode 100644
index 0000000..de2b9c9
--- /dev/null
+++ b/src/lib-imap-urlauth/imap-urlauth-backend.c
@@ -0,0 +1,174 @@
+/* Copyright (c) 2013-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "buffer.h"
+#include "hex-binary.h"
+#include "randgen.h"
+#include "mail-user.h"
+#include "mail-storage.h"
+#include "mailbox-list-iter.h"
+#include "imap-urlauth-private.h"
+#include "imap-urlauth-backend.h"
+
+#define IMAP_URLAUTH_KEY MAILBOX_ATTRIBUTE_PREFIX_DOVECOT"imap-urlauth"
+
+static int
+imap_urlauth_backend_trans_set_mailbox_key(struct mailbox *box,
+ unsigned char mailbox_key_r[IMAP_URLAUTH_KEY_LEN],
+ const char **client_error_r,
+ enum mail_error *error_code_r)
+{
+ struct mail_attribute_value urlauth_key;
+ const char *mailbox_key_hex = NULL;
+ int ret;
+
+ if (mailbox_open(box) < 0) {
+ *client_error_r = mailbox_get_last_error(box, error_code_r);
+ return -1;
+ }
+
+ struct mailbox_transaction_context *t =
+ mailbox_transaction_begin(box,
+ MAILBOX_TRANSACTION_FLAG_EXTERNAL,
+ __func__);
+
+ /* create new key */
+ random_fill(mailbox_key_r, IMAP_URLAUTH_KEY_LEN);
+ mailbox_key_hex = binary_to_hex(mailbox_key_r,
+ IMAP_URLAUTH_KEY_LEN);
+ i_zero(&urlauth_key);
+ urlauth_key.value = mailbox_key_hex;
+ ret = mailbox_attribute_set(t, MAIL_ATTRIBUTE_TYPE_PRIVATE,
+ IMAP_URLAUTH_KEY, &urlauth_key);
+
+ if (mailbox_transaction_commit(&t) < 0) {
+ *client_error_r = mailbox_get_last_error(box, error_code_r);
+ ret = -1;
+ }
+
+ return ret;
+}
+
+static int
+imap_urlauth_backend_trans_get_mailbox_key(struct mailbox *box,
+ bool create,
+ unsigned char mailbox_key_r[IMAP_URLAUTH_KEY_LEN],
+ const char **client_error_r,
+ enum mail_error *error_code_r)
+{
+ struct mail_user *user = mail_storage_get_user(mailbox_get_storage(box));
+ struct mail_attribute_value urlauth_key;
+ const char *mailbox_key_hex = NULL;
+ buffer_t key_buf;
+ int ret;
+
+ *client_error_r = "Internal server error";
+ *error_code_r = MAIL_ERROR_TEMP;
+
+ ret = mailbox_attribute_get(box, MAIL_ATTRIBUTE_TYPE_PRIVATE,
+ IMAP_URLAUTH_KEY, &urlauth_key);
+ if (ret < 0)
+ return -1;
+
+ e_debug(user->event, "imap-urlauth: %skey found for mailbox %s",
+ (ret > 0 ? "" : "no "), mailbox_get_vname(box));
+
+ if (ret == 0) {
+ if (!create)
+ return 0;
+
+ ret = imap_urlauth_backend_trans_set_mailbox_key(box,
+ mailbox_key_r,
+ client_error_r,
+ error_code_r);
+
+ if (ret < 0)
+ return -1;
+ e_debug(user->event, "imap-urlauth: created key for mailbox %s",
+ mailbox_get_vname(box));
+ } else {
+ /* read existing key */
+ buffer_create_from_data(&key_buf, mailbox_key_r,
+ IMAP_URLAUTH_KEY_LEN);
+ mailbox_key_hex = urlauth_key.value;
+ if (strlen(mailbox_key_hex) != 2*IMAP_URLAUTH_KEY_LEN ||
+ hex_to_binary(mailbox_key_hex, &key_buf) < 0 ||
+ key_buf.used != IMAP_URLAUTH_KEY_LEN) {
+ i_error("imap-urlauth: key found for mailbox %s is invalid",
+ mailbox_get_vname(box));
+ return -1;
+ }
+ }
+ return 1;
+}
+
+int imap_urlauth_backend_get_mailbox_key(struct mailbox *box, bool create,
+ unsigned char mailbox_key_r[IMAP_URLAUTH_KEY_LEN],
+ const char **client_error_r,
+ enum mail_error *error_code_r)
+{
+ int ret;
+
+ ret = imap_urlauth_backend_trans_get_mailbox_key(box, create,
+ mailbox_key_r,
+ client_error_r,
+ error_code_r);
+ return ret;
+}
+
+int imap_urlauth_backend_reset_mailbox_key(struct mailbox *box)
+{
+ struct mailbox_transaction_context *t;
+ int ret;
+
+ t = mailbox_transaction_begin(box, MAILBOX_TRANSACTION_FLAG_EXTERNAL,
+ __func__);
+ ret = mailbox_attribute_unset(t, MAIL_ATTRIBUTE_TYPE_PRIVATE,
+ IMAP_URLAUTH_KEY);
+ if (mailbox_transaction_commit(&t) < 0)
+ ret = -1;
+ return ret;
+}
+
+static int imap_urlauth_backend_mailbox_reset_key(struct mailbox *box)
+{
+ const char *errstr;
+ enum mail_error error;
+
+ if (mailbox_open(box) < 0) {
+ errstr = mailbox_get_last_internal_error(box, &error);
+ if (error == MAIL_ERROR_NOTFOUND || error == MAIL_ERROR_PERM)
+ return 0;
+ i_error("urlauth key reset: Couldn't open mailbox %s: %s",
+ mailbox_get_vname(box), errstr);
+ return -1;
+ }
+ return imap_urlauth_backend_reset_mailbox_key(box);
+}
+
+int imap_urlauth_backend_reset_all_keys(struct mail_user *user)
+{
+ const char *const patterns[] = { "*", NULL };
+ struct mailbox_list_iterate_context *iter;
+ const struct mailbox_info *info;
+ struct mailbox *box;
+ int ret = 0;
+
+ iter = mailbox_list_iter_init_namespaces(user->namespaces, patterns,
+ MAIL_NAMESPACE_TYPE_MASK_ALL,
+ MAILBOX_LIST_ITER_NO_AUTO_BOXES |
+ MAILBOX_LIST_ITER_SKIP_ALIASES |
+ MAILBOX_LIST_ITER_RETURN_NO_FLAGS);
+ while ((info = mailbox_list_iter_next(iter)) != NULL) {
+ box = mailbox_alloc(info->ns->list, info->vname, 0);
+ if (imap_urlauth_backend_mailbox_reset_key(box) < 0)
+ ret = -1;
+ mailbox_free(&box);
+ }
+ if (mailbox_list_iter_deinit(&iter) < 0) {
+ i_error("urlauth key reset: Couldn't iterate mailboxes: %s",
+ mailbox_list_get_last_internal_error(user->namespaces->list, NULL));
+ ret = -1;
+ }
+ return ret;
+}
diff --git a/src/lib-imap-urlauth/imap-urlauth-backend.h b/src/lib-imap-urlauth/imap-urlauth-backend.h
new file mode 100644
index 0000000..b76ecc5
--- /dev/null
+++ b/src/lib-imap-urlauth/imap-urlauth-backend.h
@@ -0,0 +1,16 @@
+#ifndef IMAP_URLAUTH_BACKEND_H
+#define IMAP_URLAUTH_BACKEND_H
+
+#define IMAP_URLAUTH_KEY_LEN 64
+
+struct imap_urlauth_backend;
+
+int imap_urlauth_backend_get_mailbox_key(struct mailbox *box, bool create,
+ unsigned char mailbox_key_r[IMAP_URLAUTH_KEY_LEN],
+ const char **client_error_r,
+ enum mail_error *error_code_r);
+int imap_urlauth_backend_reset_mailbox_key(struct mailbox *box);
+int imap_urlauth_backend_reset_all_keys(struct mail_user *user);
+
+#endif
+
diff --git a/src/lib-imap-urlauth/imap-urlauth-connection.c b/src/lib-imap-urlauth/imap-urlauth-connection.c
new file mode 100644
index 0000000..5fce6f7
--- /dev/null
+++ b/src/lib-imap-urlauth/imap-urlauth-connection.c
@@ -0,0 +1,1027 @@
+/* Copyright (c) 2013-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "llist.h"
+#include "str.h"
+#include "str-sanitize.h"
+#include "strescape.h"
+#include "ioloop.h"
+#include "safe-mkstemp.h"
+#include "hostpid.h"
+#include "net.h"
+#include "istream.h"
+#include "ostream.h"
+#include "write-full.h"
+#include "array.h"
+#include "aqueue.h"
+#include "mail-user.h"
+#include "imap-urlauth-fetch.h"
+
+#include "imap-urlauth-connection.h"
+
+enum imap_urlauth_state {
+ IMAP_URLAUTH_STATE_DISCONNECTED = 0,
+ IMAP_URLAUTH_STATE_AUTHENTICATING,
+ IMAP_URLAUTH_STATE_AUTHENTICATED,
+ IMAP_URLAUTH_STATE_SELECTING_TARGET,
+ IMAP_URLAUTH_STATE_UNSELECTING_TARGET,
+ IMAP_URLAUTH_STATE_READY,
+ IMAP_URLAUTH_STATE_REQUEST_PENDING,
+ IMAP_URLAUTH_STATE_REQUEST_WAIT,
+};
+
+struct imap_urlauth_request {
+ struct imap_urlauth_target *target;
+ struct imap_urlauth_request *prev, *next;
+
+ char *url;
+ enum imap_urlauth_fetch_flags flags;
+
+ char *bodypartstruct;
+
+ imap_urlauth_request_callback_t *callback;
+ void *context;
+
+ bool binary_has_nuls;
+};
+
+struct imap_urlauth_target {
+ struct imap_urlauth_target *prev, *next;
+
+ char *userid;
+
+ struct imap_urlauth_request *requests_head, *requests_tail;
+};
+
+struct imap_urlauth_connection {
+ int refcount;
+
+ char *path, *service, *session_id;
+ struct mail_user *user;
+
+ int fd;
+ struct istream *input;
+ struct ostream *output;
+ struct io *io;
+
+ struct timeout *to_reconnect, *to_idle, *to_response;
+ time_t last_reconnect;
+ unsigned int reconnect_attempts;
+ unsigned int idle_timeout_msecs;
+
+ char *literal_temp_path;
+ int literal_fd;
+ buffer_t *literal_buf;
+ uoff_t literal_size, literal_bytes_left;
+
+ enum imap_urlauth_state state;
+
+ /* userid => target struct */
+ struct imap_urlauth_target *targets_head, *targets_tail;
+
+ bool reading_literal:1;
+};
+
+#define IMAP_URLAUTH_RECONNECT_MIN_SECS 2
+#define IMAP_URLAUTH_RECONNECT_MAX_ATTEMPTS 3
+
+#define IMAP_URLAUTH_RESPONSE_TIMEOUT_MSECS 2*60*1000
+
+#define IMAP_URLAUTH_HANDSHAKE "VERSION\timap-urlauth\t2\t0\n"
+
+#define IMAP_URLAUTH_MAX_INLINE_LITERAL_SIZE (1024*32)
+
+static void imap_urlauth_connection_disconnect
+ (struct imap_urlauth_connection *conn, const char *reason);
+static void imap_urlauth_connection_abort
+ (struct imap_urlauth_connection *conn, const char *reason);
+static void imap_urlauth_connection_reconnect
+ (struct imap_urlauth_connection *conn);
+static void imap_urlauth_connection_idle_disconnect
+ (struct imap_urlauth_connection *conn);
+static void imap_urlauth_connection_timeout_abort
+ (struct imap_urlauth_connection *conn);
+static void imap_urlauth_connection_fail
+ (struct imap_urlauth_connection *conn);
+
+struct imap_urlauth_connection *
+imap_urlauth_connection_init(const char *path, const char *service,
+ struct mail_user *user, const char *session_id,
+ unsigned int idle_timeout_msecs)
+{
+ struct imap_urlauth_connection *conn;
+
+ conn = i_new(struct imap_urlauth_connection, 1);
+ conn->refcount = 1;
+ conn->service = i_strdup(service);
+ conn->path = i_strdup(path);
+ if (session_id != NULL)
+ conn->session_id = i_strdup(session_id);
+ conn->user = user;
+ conn->fd = -1;
+ conn->literal_fd = -1;
+ conn->idle_timeout_msecs = idle_timeout_msecs;
+ return conn;
+}
+
+void imap_urlauth_connection_deinit(struct imap_urlauth_connection **_conn)
+{
+ struct imap_urlauth_connection *conn = *_conn;
+
+ *_conn = NULL;
+
+ imap_urlauth_connection_abort(conn, NULL);
+
+ i_free(conn->path);
+ i_free(conn->service);
+ if (conn->session_id != NULL)
+ i_free(conn->session_id);
+
+ i_assert(conn->to_idle == NULL);
+ i_assert(conn->to_reconnect == NULL);
+ i_assert(conn->to_response == NULL);
+
+ i_free(conn);
+}
+
+static void
+imap_urlauth_stop_response_timeout(struct imap_urlauth_connection *conn)
+{
+ timeout_remove(&conn->to_response);
+}
+
+static void
+imap_urlauth_start_response_timeout(struct imap_urlauth_connection *conn)
+{
+ imap_urlauth_stop_response_timeout(conn);
+ conn->to_response = timeout_add(IMAP_URLAUTH_RESPONSE_TIMEOUT_MSECS,
+ imap_urlauth_connection_timeout_abort, conn);
+}
+
+static struct imap_urlauth_target *
+imap_urlauth_connection_get_target(struct imap_urlauth_connection *conn,
+ const char *target_user)
+{
+ struct imap_urlauth_target *target = conn->targets_head;
+
+ while (target != NULL) {
+ if (strcmp(target->userid, target_user) == 0)
+ return target;
+ target = target->next;
+ }
+
+ target = i_new(struct imap_urlauth_target, 1);
+ target->userid = i_strdup(target_user);
+ DLLIST2_APPEND(&conn->targets_head, &conn->targets_tail, target);
+ return target;
+}
+
+static void
+imap_urlauth_target_free(struct imap_urlauth_connection *conn,
+ struct imap_urlauth_target *target)
+{
+ DLLIST2_REMOVE(&conn->targets_head, &conn->targets_tail, target);
+ i_free(target->userid);
+ i_free(target);
+}
+
+static void
+imap_urlauth_connection_select_target(struct imap_urlauth_connection *conn)
+{
+ struct imap_urlauth_target *target = conn->targets_head;
+ const char *cmd;
+
+ if (target == NULL || conn->state != IMAP_URLAUTH_STATE_AUTHENTICATED)
+ return;
+
+ e_debug(conn->user->event,
+ "imap-urlauth: Selecting target user `%s'", target->userid);
+
+ conn->state = IMAP_URLAUTH_STATE_SELECTING_TARGET;
+ cmd = t_strdup_printf("USER\t%s\n", str_tabescape(target->userid));
+ if (o_stream_send_str(conn->output, cmd) < 0) {
+ i_warning("Error sending USER request to imap-urlauth server: %m");
+ imap_urlauth_connection_fail(conn);
+ }
+
+ imap_urlauth_start_response_timeout(conn);
+}
+
+static void
+imap_urlauth_connection_send_request(struct imap_urlauth_connection *conn)
+{
+ struct imap_urlauth_request *urlreq;
+ string_t *cmd;
+
+ if (conn->targets_head == NULL ||
+ (conn->targets_head->requests_head == NULL &&
+ conn->targets_head->next == NULL &&
+ conn->state == IMAP_URLAUTH_STATE_READY)) {
+ e_debug(conn->user->event,
+ "imap-urlauth: No more requests pending; scheduling disconnect");
+ timeout_remove(&conn->to_idle);
+ if (conn->idle_timeout_msecs > 0) {
+ conn->to_idle = timeout_add(conn->idle_timeout_msecs,
+ imap_urlauth_connection_idle_disconnect, conn);
+ }
+ return;
+ }
+
+ if (conn->state == IMAP_URLAUTH_STATE_AUTHENTICATED) {
+ imap_urlauth_connection_select_target(conn);
+ return;
+ }
+
+ if (conn->state != IMAP_URLAUTH_STATE_READY)
+ return;
+
+ urlreq = conn->targets_head->requests_head;
+ if (urlreq == NULL) {
+ if (conn->targets_head->next == NULL)
+ return;
+
+ conn->state = IMAP_URLAUTH_STATE_UNSELECTING_TARGET;
+ imap_urlauth_target_free(conn, conn->targets_head);
+
+ if (o_stream_send_str(conn->output, "END\n") < 0) {
+ i_warning("Error sending END request to imap-urlauth server: %m");
+ imap_urlauth_connection_fail(conn);
+ }
+ imap_urlauth_start_response_timeout(conn);
+ return;
+ }
+
+ e_debug(conn->user->event,
+ "imap-urlauth: Fetching URL `%s'", urlreq->url);
+
+ cmd = t_str_new(128);
+ str_append(cmd, "URL\t");
+ str_append_tabescaped(cmd, urlreq->url);
+ if ((urlreq->flags & IMAP_URLAUTH_FETCH_FLAG_BODYPARTSTRUCTURE) != 0)
+ str_append(cmd, "\tbpstruct");
+ if ((urlreq->flags & IMAP_URLAUTH_FETCH_FLAG_BINARY) != 0)
+ str_append(cmd, "\tbinary");
+ else if ((urlreq->flags & IMAP_URLAUTH_FETCH_FLAG_BODY) != 0)
+ str_append(cmd, "\tbody");
+ str_append_c(cmd, '\n');
+
+ conn->state = IMAP_URLAUTH_STATE_REQUEST_PENDING;
+ if (o_stream_send(conn->output, str_data(cmd), str_len(cmd)) < 0) {
+ i_warning("Error sending URL request to imap-urlauth server: %m");
+ imap_urlauth_connection_fail(conn);
+ }
+
+ imap_urlauth_start_response_timeout(conn);
+}
+
+struct imap_urlauth_request *
+imap_urlauth_request_new(struct imap_urlauth_connection *conn,
+ const char *target_user, const char *url,
+ enum imap_urlauth_fetch_flags flags,
+ imap_urlauth_request_callback_t *callback,
+ void *context)
+{
+ struct imap_urlauth_request *urlreq;
+ struct imap_urlauth_target *target;
+
+ target = imap_urlauth_connection_get_target(conn, target_user);
+
+ urlreq = i_new(struct imap_urlauth_request, 1);
+ urlreq->url = i_strdup(url);
+ urlreq->flags = flags;
+ urlreq->target = target;
+ urlreq->callback = callback;
+ urlreq->context = context;
+
+ DLLIST2_APPEND(&target->requests_head, &target->requests_tail, urlreq);
+
+ timeout_remove(&conn->to_idle);
+
+ e_debug(conn->user->event,
+ "imap-urlauth: Added request for URL `%s' from user `%s'",
+ url, target_user);
+
+ imap_urlauth_connection_send_request(conn);
+ return urlreq;
+}
+
+static void imap_urlauth_request_free(struct imap_urlauth_request *urlreq)
+{
+ struct imap_urlauth_target *target = urlreq->target;
+
+ DLLIST2_REMOVE(&target->requests_head, &target->requests_tail, urlreq);
+ i_free(urlreq->url);
+ i_free(urlreq->bodypartstruct);
+ i_free(urlreq);
+}
+
+static void imap_urlauth_request_drop(struct imap_urlauth_connection *conn,
+ struct imap_urlauth_request *urlreq)
+{
+ if ((conn->state == IMAP_URLAUTH_STATE_REQUEST_PENDING ||
+ conn->state == IMAP_URLAUTH_STATE_REQUEST_WAIT) &&
+ conn->targets_head != NULL &&
+ conn->targets_head->requests_head == urlreq) {
+ /* cannot just drop pending request without breaking
+ protocol state */
+ return;
+ }
+ imap_urlauth_request_free(urlreq);
+
+}
+
+void imap_urlauth_request_abort(struct imap_urlauth_connection *conn,
+ struct imap_urlauth_request *urlreq)
+{
+ imap_urlauth_request_callback_t *callback;
+
+ callback = urlreq->callback;
+ urlreq->callback = NULL;
+ if (callback != NULL) {
+ T_BEGIN {
+ callback(NULL, urlreq->context);
+ } T_END;
+ }
+
+ imap_urlauth_request_drop(conn, urlreq);
+}
+
+static void
+imap_urlauth_request_fail(struct imap_urlauth_connection *conn,
+ struct imap_urlauth_request *urlreq,
+ const char *error)
+{
+ struct imap_urlauth_fetch_reply reply;
+ imap_urlauth_request_callback_t *callback;
+ int ret = 1;
+
+ callback = urlreq->callback;
+ urlreq->callback = NULL;
+ if (callback != NULL) {
+ i_zero(&reply);
+ reply.url = urlreq->url;
+ reply.flags = urlreq->flags;
+ reply.succeeded = FALSE;
+ reply.error = error;
+
+ T_BEGIN {
+ ret = callback(&reply, urlreq->context);
+ } T_END;
+ }
+
+ void *urlreq_context = urlreq->context;
+ imap_urlauth_request_drop(conn, urlreq);
+
+ if (ret < 0) {
+ /* Drop any related requests upon error */
+ imap_urlauth_request_abort_by_context(conn, urlreq_context);
+ }
+
+ if (ret != 0)
+ imap_urlauth_connection_continue(conn);
+}
+
+static void
+imap_urlauth_target_abort(struct imap_urlauth_connection *conn,
+ struct imap_urlauth_target *target)
+{
+ struct imap_urlauth_request *urlreq, *next;
+
+ urlreq = target->requests_head;
+ while (urlreq != NULL) {
+ next = urlreq->next;
+ imap_urlauth_request_abort(conn, urlreq);
+ urlreq = next;
+ }
+
+ imap_urlauth_target_free(conn, target);
+}
+
+static void
+imap_urlauth_target_fail(struct imap_urlauth_connection *conn,
+ struct imap_urlauth_target *target, const char *error)
+{
+ struct imap_urlauth_request *urlreq, *next;
+
+ urlreq = target->requests_head;
+ while (urlreq != NULL) {
+ next = urlreq->next;
+ imap_urlauth_request_fail(conn, urlreq, error);
+ urlreq = next;
+ }
+
+ imap_urlauth_target_free(conn, target);
+}
+
+static void
+imap_urlauth_target_abort_by_context(struct imap_urlauth_connection *conn,
+ struct imap_urlauth_target *target,
+ void *context)
+{
+ struct imap_urlauth_request *urlreq, *next;
+
+ /* abort all matching requests */
+ urlreq = target->requests_head;
+ while (urlreq != NULL) {
+ next = urlreq->next;
+ if (urlreq->context == context)
+ imap_urlauth_request_abort(conn, urlreq);
+ urlreq = next;
+ }
+
+ if (target->requests_head == NULL)
+ imap_urlauth_target_free(conn, target);
+}
+
+static void
+imap_urlauth_connection_abort(struct imap_urlauth_connection *conn,
+ const char *reason)
+{
+ struct imap_urlauth_target *target, *next;
+
+ if (reason == NULL)
+ reason = "Aborting due to error";
+ imap_urlauth_connection_disconnect(conn, reason);
+
+ /* abort all requests */
+ target = conn->targets_head;
+ while (target != NULL) {
+ next = target->next;
+ imap_urlauth_target_abort(conn, target);
+ target = next;
+ }
+}
+
+void imap_urlauth_request_abort_by_context(struct imap_urlauth_connection *conn,
+ void *context)
+{
+ struct imap_urlauth_target *target, *next;
+
+ /* abort all matching requests */
+ target = conn->targets_head;
+ while (target != NULL) {
+ next = target->next;
+ imap_urlauth_target_abort_by_context(conn, target, context);
+ target = next;
+ }
+}
+
+static void imap_urlauth_connection_fail(struct imap_urlauth_connection *conn)
+{
+ if (conn->reconnect_attempts > IMAP_URLAUTH_RECONNECT_MAX_ATTEMPTS) {
+ imap_urlauth_connection_abort(conn,
+ "Connection failed and connection attempts exhausted");
+ } else {
+ imap_urlauth_connection_reconnect(conn);
+ }
+}
+
+static int
+imap_urlauth_connection_create_temp_fd(struct imap_urlauth_connection *conn,
+ const char **path_r)
+{
+ string_t *path;
+ int fd;
+
+ path = t_str_new(128);
+ mail_user_set_get_temp_prefix(path, conn->user->set);
+ fd = safe_mkstemp(path, 0600, (uid_t)-1, (gid_t)-1);
+ if (fd == -1) {
+ i_error("safe_mkstemp(%s) failed: %m", str_c(path));
+ return -1;
+ }
+
+ /* we just want the fd, unlink it */
+ if (i_unlink(str_c(path)) < 0) {
+ /* shouldn't happen.. */
+ i_close_fd(&fd);
+ return -1;
+ }
+
+ *path_r = str_c(path);
+ return fd;
+}
+
+static int
+imap_urlauth_connection_read_literal_init(struct imap_urlauth_connection *conn,
+ uoff_t size)
+{
+ const char *path;
+
+ i_assert(conn->literal_fd == -1 && conn->literal_buf == NULL);
+
+ if (size <= IMAP_URLAUTH_MAX_INLINE_LITERAL_SIZE) {
+ /* read the literal directly */
+ if (size > 0) {
+ conn->literal_buf =
+ buffer_create_dynamic(default_pool, size);
+ }
+ } else {
+ /* read it into a file */
+ conn->literal_fd =
+ imap_urlauth_connection_create_temp_fd(conn, &path);
+ if (conn->literal_fd == -1)
+ return -1;
+ conn->literal_temp_path = i_strdup(path);
+ }
+
+ conn->literal_size = size;
+ conn->literal_bytes_left = size;
+ conn->reading_literal = TRUE;
+ return 1;
+}
+
+void imap_urlauth_connection_continue(struct imap_urlauth_connection *conn)
+{
+ i_assert(conn->targets_head != NULL);
+ i_assert(conn->targets_head->requests_head != NULL);
+
+ if (conn->state != IMAP_URLAUTH_STATE_REQUEST_WAIT)
+ return;
+
+ conn->state = IMAP_URLAUTH_STATE_READY;
+ imap_urlauth_request_free(conn->targets_head->requests_head);
+
+ imap_urlauth_connection_send_request(conn);
+}
+
+static int
+imap_urlauth_connection_read_literal_data(struct imap_urlauth_connection *conn)
+{
+ const unsigned char *data;
+ size_t size;
+
+ /* read data */
+ data = i_stream_get_data(conn->input, &size);
+ if (size > conn->literal_bytes_left)
+ size = conn->literal_bytes_left;
+
+ /* write to buffer or file */
+ if (size > 0) {
+ if (conn->literal_fd >= 0) {
+ if (write_full(conn->literal_fd, data, size) < 0) {
+ i_error("imap-urlauth: write(%s) failed: %m",
+ conn->literal_temp_path);
+ return -1;
+ }
+ } else {
+ i_assert(conn->literal_buf != NULL);
+ buffer_append(conn->literal_buf, data, size);
+ }
+ i_stream_skip(conn->input, size);
+ conn->literal_bytes_left -= size;
+ }
+
+ /* exit if not finished */
+ if (conn->literal_bytes_left > 0)
+ return 0;
+
+ /* read LF guard */
+ data = i_stream_get_data(conn->input, &size);
+ if (size < 1)
+ return 0;
+
+ /* check LF guard */
+ if (data[0] != '\n') {
+ i_error("imap-urlauth: no LF at end of literal (found 0x%x)",
+ data[0]);
+ return -1;
+ }
+ i_stream_skip(conn->input, 1);
+ return 1;
+}
+
+static void literal_stream_destroy(buffer_t *buffer)
+{
+ buffer_free(&buffer);
+}
+
+static int
+imap_urlauth_fetch_reply_set_literal_stream(struct imap_urlauth_connection *conn,
+ struct imap_urlauth_fetch_reply *reply)
+{
+ const unsigned char *data;
+ size_t size;
+ uoff_t fd_size;
+
+ if (conn->literal_fd != -1) {
+ reply->input = i_stream_create_fd_autoclose(&conn->literal_fd,
+ SIZE_MAX);
+ if (i_stream_get_size(reply->input, TRUE, &fd_size) < 1 ||
+ fd_size != conn->literal_size) {
+ i_stream_unref(&reply->input);
+ i_error("imap-urlauth: Failed to obtain proper size from literal stream");
+ imap_urlauth_connection_abort(conn,
+ "Failed during literal transfer");
+ return -1;
+ }
+ } else {
+ data = buffer_get_data(conn->literal_buf, &size);
+ i_assert(size == conn->literal_size);
+ reply->input = i_stream_create_from_data(data, size);
+ i_stream_add_destroy_callback(reply->input,
+ literal_stream_destroy,
+ conn->literal_buf);
+ }
+ reply->size = conn->literal_size;
+ return 0;
+}
+
+static int
+imap_urlauth_connection_read_literal(struct imap_urlauth_connection *conn)
+{
+ struct imap_urlauth_request *urlreq = conn->targets_head->requests_head;
+ struct imap_urlauth_fetch_reply reply;
+ imap_urlauth_request_callback_t *callback;
+ int ret;
+
+ i_assert(conn->reading_literal);
+ i_assert(urlreq != NULL);
+
+ if (conn->literal_size > 0) {
+ ret = imap_urlauth_connection_read_literal_data(conn);
+ if (ret <= 0)
+ return ret;
+ }
+ i_assert(conn->literal_bytes_left == 0);
+
+ /* reply */
+ i_zero(&reply);
+ reply.url = urlreq->url;
+ reply.flags = urlreq->flags;
+ reply.bodypartstruct = urlreq->bodypartstruct;
+ reply.binary_has_nuls = urlreq->binary_has_nuls ? 1 : 0;
+
+ if (conn->literal_size > 0) {
+ if (imap_urlauth_fetch_reply_set_literal_stream(conn, &reply) < 0)
+ return -1;
+ }
+ reply.succeeded = TRUE;
+
+ ret = 1;
+ callback = urlreq->callback;
+ urlreq->callback = NULL;
+ if (callback != NULL) T_BEGIN {
+ ret = callback(&reply, urlreq->context);
+ } T_END;
+
+ if (reply.input != NULL)
+ i_stream_unref(&reply.input);
+
+ if (ret < 0) {
+ /* Drop any related requests upon error */
+ imap_urlauth_request_abort_by_context(conn, urlreq->context);
+ }
+
+ conn->state = IMAP_URLAUTH_STATE_REQUEST_WAIT;
+ if (ret != 0)
+ imap_urlauth_connection_continue(conn);
+
+ /* finished */
+ i_free_and_null(conn->literal_temp_path);
+ conn->literal_fd = -1;
+ conn->literal_buf = NULL;
+ conn->reading_literal = FALSE;
+ return 1;
+}
+
+static int imap_urlauth_input_pending(struct imap_urlauth_connection *conn)
+{
+ struct imap_urlauth_request *urlreq;
+ const char *response, *const *args, *bpstruct = NULL;
+ uoff_t literal_size;
+
+ i_assert(conn->targets_head != NULL);
+ i_assert(conn->targets_head->requests_head != NULL);
+ urlreq = conn->targets_head->requests_head;
+
+ if (conn->reading_literal) {
+ /* Read pending literal; may callback */
+ return imap_urlauth_connection_read_literal(conn);
+ }
+
+ /* "OK"[<metadata-items>]"\t"<literal-size>"\n" or
+ "NO"["\terror="<error>]"\n" */
+ if ((response = i_stream_next_line(conn->input)) == NULL)
+ return 0;
+ imap_urlauth_stop_response_timeout(conn);
+
+ args = t_strsplit_tabescaped(response);
+ if (args[0] == NULL) {
+ i_error("imap-urlauth: Empty URL response: %s",
+ str_sanitize(response, 80));
+ return -1;
+ }
+
+ if (strcmp(args[0], "OK") != 0 || args[1] == NULL) {
+ if (strcmp(args[0], "NO") == 0) {
+ const char *param = args[1], *error = NULL;
+
+ if (param != NULL &&
+ strncasecmp(param, "error=", 6) == 0 &&
+ param[6] != '\0') {
+ error = param+6;
+ }
+ conn->state = IMAP_URLAUTH_STATE_REQUEST_WAIT;
+ imap_urlauth_request_fail(conn,
+ conn->targets_head->requests_head, error);
+ return 1;
+ }
+ i_error("imap-urlauth: Unexpected URL response: %s",
+ str_sanitize(response, 80));
+ return -1;
+ }
+
+ /* read metadata */
+ args++;
+ for (; args[1] != NULL; args++) {
+ const char *param = args[0];
+
+ if (strcasecmp(param, "hasnuls") == 0) {
+ urlreq->binary_has_nuls = TRUE;
+ } else if (strncasecmp(param, "bpstruct=", 9) == 0 &&
+ param[9] != '\0') {
+ bpstruct = param+9;
+ }
+ }
+
+ /* read literal size */
+ if (str_to_uoff(args[0], &literal_size) < 0) {
+ i_error("imap-urlauth: "
+ "Overflowing unsigned integer value for literal size: %s",
+ args[1]);
+ return -1;
+ }
+
+ /* read literal */
+ if (imap_urlauth_connection_read_literal_init(conn, literal_size) < 0)
+ return -1;
+
+ urlreq->bodypartstruct = i_strdup(bpstruct);
+ return imap_urlauth_connection_read_literal(conn);
+}
+
+static int imap_urlauth_input_next(struct imap_urlauth_connection *conn)
+{
+ const char *response;
+ int ret;
+
+ switch (conn->state) {
+ case IMAP_URLAUTH_STATE_AUTHENTICATING:
+ case IMAP_URLAUTH_STATE_UNSELECTING_TARGET:
+ if ((response = i_stream_next_line(conn->input)) == NULL)
+ return 0;
+ imap_urlauth_stop_response_timeout(conn);
+
+ if (strcasecmp(response, "OK") != 0) {
+ if (conn->state == IMAP_URLAUTH_STATE_AUTHENTICATING)
+ i_error("imap-urlauth: Failed to authenticate to service: "
+ "Got unexpected response: %s", str_sanitize(response, 80));
+ else
+ i_error("imap-urlauth: Failed to unselect target user: "
+ "Got unexpected response: %s", str_sanitize(response, 80));
+ imap_urlauth_connection_abort(conn, NULL);
+ return -1;
+ }
+
+ if (conn->state == IMAP_URLAUTH_STATE_AUTHENTICATING) {
+ e_debug(conn->user->event,
+ "imap-urlauth: Successfully authenticated to service");
+ } else {
+ e_debug(conn->user->event,
+ "imap-urlauth: Successfully unselected target user");
+ }
+
+ conn->state = IMAP_URLAUTH_STATE_AUTHENTICATED;
+ imap_urlauth_connection_select_target(conn);
+ return 0;
+ case IMAP_URLAUTH_STATE_SELECTING_TARGET:
+ if ((response = i_stream_next_line(conn->input)) == NULL)
+ return 0;
+ imap_urlauth_stop_response_timeout(conn);
+
+ i_assert(conn->targets_head != NULL);
+
+ if (strcasecmp(response, "NO") == 0) {
+ e_debug(conn->user->event,
+ "imap-urlauth: Failed to select target user %s",
+ conn->targets_head->userid);
+ imap_urlauth_target_fail(conn, conn->targets_head, NULL);
+
+ conn->state = IMAP_URLAUTH_STATE_AUTHENTICATED;
+ imap_urlauth_connection_select_target(conn);
+ return 0;
+ }
+ if (strcasecmp(response, "OK") != 0) {
+ i_error("imap-urlauth: Failed to select target user %s: "
+ "Got unexpected response: %s", conn->targets_head->userid,
+ str_sanitize(response, 80));
+ imap_urlauth_connection_abort(conn, NULL);
+ return -1;
+ }
+
+ e_debug(conn->user->event,
+ "imap-urlauth: Successfully selected target user %s",
+ conn->targets_head->userid);
+ conn->state = IMAP_URLAUTH_STATE_READY;
+ imap_urlauth_connection_send_request(conn);
+ return 0;
+ case IMAP_URLAUTH_STATE_AUTHENTICATED:
+ case IMAP_URLAUTH_STATE_READY:
+ case IMAP_URLAUTH_STATE_REQUEST_WAIT:
+ if ((response = i_stream_next_line(conn->input)) == NULL)
+ return 0;
+
+ i_error("imap-urlauth: Received input while no requests were pending: %s",
+ str_sanitize(response, 80));
+ imap_urlauth_connection_abort(conn, NULL);
+ return -1;
+ case IMAP_URLAUTH_STATE_REQUEST_PENDING:
+ if ((ret = imap_urlauth_input_pending(conn)) < 0)
+ imap_urlauth_connection_fail(conn);
+ return ret;
+ case IMAP_URLAUTH_STATE_DISCONNECTED:
+ break;
+ }
+ i_unreached();
+}
+
+static void imap_urlauth_input(struct imap_urlauth_connection *conn)
+{
+ i_assert(conn->state != IMAP_URLAUTH_STATE_DISCONNECTED);
+
+ if (conn->input->closed) {
+ /* disconnected */
+ i_error("imap-urlauth: Service disconnected unexpectedly");
+ imap_urlauth_connection_fail(conn);
+ return;
+ }
+
+ switch (i_stream_read(conn->input)) {
+ case -1:
+ /* disconnected */
+ i_error("imap-urlauth: Service disconnected unexpectedly");
+ imap_urlauth_connection_fail(conn);
+ return;
+ case -2:
+ /* input buffer full */
+ i_error("imap-urlauth: Service sent too large input");
+ imap_urlauth_connection_abort(conn, NULL);
+ return;
+ }
+
+ while (!conn->input->closed) {
+ if (imap_urlauth_input_next(conn) <= 0)
+ break;
+ }
+}
+
+static int
+imap_urlauth_connection_do_connect(struct imap_urlauth_connection *conn)
+{
+ string_t *str;
+ int fd;
+
+ if (conn->state != IMAP_URLAUTH_STATE_DISCONNECTED) {
+ imap_urlauth_connection_send_request(conn);
+ return 1;
+ }
+
+ if (conn->user->auth_token == NULL) {
+ i_error("imap-urlauth: cannot authenticate because no auth token "
+ "is available for this session (running standalone?).");
+ imap_urlauth_connection_abort(conn, NULL);
+ return -1;
+ }
+
+ e_debug(conn->user->event, "imap-urlauth: Connecting to service at %s", conn->path);
+
+ i_assert(conn->fd == -1);
+ fd = net_connect_unix(conn->path);
+ if (fd == -1) {
+ i_error("imap-urlauth: net_connect_unix(%s) failed: %m",
+ conn->path);
+ imap_urlauth_connection_abort(conn, NULL);
+ return -1;
+ }
+
+ timeout_remove(&conn->to_reconnect);
+
+ conn->fd = fd;
+ conn->input = i_stream_create_fd(fd, SIZE_MAX);
+ conn->output = o_stream_create_fd(fd, SIZE_MAX);
+ conn->io = io_add(fd, IO_READ, imap_urlauth_input, conn);
+ conn->state = IMAP_URLAUTH_STATE_AUTHENTICATING;
+
+ str = t_str_new(128);
+ str_printfa(str, IMAP_URLAUTH_HANDSHAKE"AUTH\t%s\t%s\t",
+ conn->service, my_pid);
+ str_append_tabescaped(str, conn->user->username);
+ str_append_c(str, '\t');
+ if (conn->session_id != NULL)
+ str_append_tabescaped(str, conn->session_id);
+ str_append_c(str, '\t');
+ str_append_tabescaped(str, conn->user->auth_token);
+ str_append_c(str, '\n');
+ if (o_stream_send(conn->output, str_data(str), str_len(str)) < 0) {
+ i_warning("Error sending handshake to imap-urlauth server: %m");
+ imap_urlauth_connection_abort(conn, NULL);
+ return -1;
+ }
+
+ imap_urlauth_start_response_timeout(conn);
+ return 0;
+}
+
+int imap_urlauth_connection_connect(struct imap_urlauth_connection *conn)
+{
+ conn->reconnect_attempts = 0;
+
+ if (conn->to_reconnect == NULL)
+ return imap_urlauth_connection_do_connect(conn);
+ return 0;
+}
+
+static void imap_urlauth_connection_disconnect
+(struct imap_urlauth_connection *conn, const char *reason)
+{
+ conn->state = IMAP_URLAUTH_STATE_DISCONNECTED;
+
+ if (conn->fd != -1) {
+ if (reason == NULL)
+ e_debug(conn->user->event, "imap-urlauth: Disconnecting from service");
+ else
+ e_debug(conn->user->event, "imap-urlauth: Disconnected: %s", reason);
+
+ io_remove(&conn->io);
+ i_stream_destroy(&conn->input);
+ o_stream_destroy(&conn->output);
+ net_disconnect(conn->fd);
+ conn->fd = -1;
+ }
+ conn->reading_literal = FALSE;
+
+ if (conn->literal_fd != -1) {
+ if (close(conn->literal_fd) < 0)
+ i_error("imap-urlauth: close(%s) failed: %m", conn->literal_temp_path);
+
+ i_free_and_null(conn->literal_temp_path);
+ conn->literal_fd = -1;
+ }
+
+ buffer_free(&conn->literal_buf);
+ timeout_remove(&conn->to_reconnect);
+ timeout_remove(&conn->to_idle);
+ imap_urlauth_stop_response_timeout(conn);
+}
+
+static void
+imap_urlauth_connection_do_reconnect(struct imap_urlauth_connection *conn)
+{
+ if (conn->reconnect_attempts >= IMAP_URLAUTH_RECONNECT_MAX_ATTEMPTS) {
+ imap_urlauth_connection_abort(conn,
+ "Connection failed and connection attempts exhausted");
+ return;
+ }
+
+ if (ioloop_time - conn->last_reconnect < IMAP_URLAUTH_RECONNECT_MIN_SECS) {
+ e_debug(conn->user->event, "imap-urlauth: Scheduling reconnect");
+ timeout_remove(&conn->to_reconnect);
+ conn->to_reconnect =
+ timeout_add(IMAP_URLAUTH_RECONNECT_MIN_SECS*1000,
+ imap_urlauth_connection_do_reconnect, conn);
+ } else {
+ conn->reconnect_attempts++;
+ conn->last_reconnect = ioloop_time;
+ (void)imap_urlauth_connection_do_connect(conn);
+ }
+}
+
+static void
+imap_urlauth_connection_reconnect(struct imap_urlauth_connection *conn)
+{
+ imap_urlauth_connection_disconnect(conn, NULL);
+
+ /* don't reconnect if there are no requests */
+ if (conn->targets_head == NULL)
+ return;
+
+ imap_urlauth_connection_do_reconnect(conn);
+}
+
+static void
+imap_urlauth_connection_idle_disconnect(struct imap_urlauth_connection *conn)
+{
+ imap_urlauth_connection_disconnect(conn, "Idle timeout");
+}
+
+static void
+imap_urlauth_connection_timeout_abort(struct imap_urlauth_connection *conn)
+{
+ imap_urlauth_connection_abort(conn, "Service is not responding");
+}
+
+bool imap_urlauth_connection_is_connected(struct imap_urlauth_connection *conn)
+{
+ return conn->fd != -1 && conn->state != IMAP_URLAUTH_STATE_DISCONNECTED;
+}
diff --git a/src/lib-imap-urlauth/imap-urlauth-connection.h b/src/lib-imap-urlauth/imap-urlauth-connection.h
new file mode 100644
index 0000000..b60186a
--- /dev/null
+++ b/src/lib-imap-urlauth/imap-urlauth-connection.h
@@ -0,0 +1,42 @@
+#ifndef IMAP_URLAUTH_CONNECTION_H
+#define IMAP_URLAUTH_CONNECTION_H
+
+struct imap_urlauth_request;
+struct imap_urlauth_fetch_reply;
+
+typedef int
+imap_urlauth_request_callback_t(struct imap_urlauth_fetch_reply *reply,
+ void *context);
+
+/* If reconnect_callback is specified, it's called when connection is lost.
+ If the callback returns FALSE, reconnection isn't attempted. */
+struct imap_urlauth_connection *
+imap_urlauth_connection_init(const char *path, const char *service,
+ struct mail_user *user, const char *session_id,
+ unsigned int idle_timeout_msecs);
+void imap_urlauth_connection_deinit(struct imap_urlauth_connection **conn);
+
+/* Connect to imap-urlauth (even if failed for previous requests). */
+int imap_urlauth_connection_connect(struct imap_urlauth_connection *conn);
+
+/* Continue after request callback returned 0 */
+void imap_urlauth_connection_continue(struct imap_urlauth_connection *conn);
+
+/* Create a new URL fetch request */
+struct imap_urlauth_request *
+imap_urlauth_request_new(struct imap_urlauth_connection *conn,
+ const char *target_user, const char *url,
+ enum imap_urlauth_fetch_flags flags,
+ imap_urlauth_request_callback_t *callback,
+ void *context);
+/* Abort request */
+void imap_urlauth_request_abort(struct imap_urlauth_connection *conn,
+ struct imap_urlauth_request *urlreq);
+/* Abort all requests with matching context value */
+void imap_urlauth_request_abort_by_context(struct imap_urlauth_connection *conn,
+ void *context);
+
+/* Returns TRUE if currently connected imap-urlauth service. */
+bool imap_urlauth_connection_is_connected(struct imap_urlauth_connection *conn);
+
+#endif
diff --git a/src/lib-imap-urlauth/imap-urlauth-fetch.c b/src/lib-imap-urlauth/imap-urlauth-fetch.c
new file mode 100644
index 0000000..d5746f1
--- /dev/null
+++ b/src/lib-imap-urlauth/imap-urlauth-fetch.c
@@ -0,0 +1,530 @@
+/* Copyright (c) 2013-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "llist.h"
+#include "array.h"
+#include "net.h"
+#include "istream.h"
+#include "mail-user.h"
+#include "mail-error.h"
+#include "mail-storage.h"
+#include "imap-url.h"
+#include "imap-msgpart-url.h"
+#include "imap-urlauth-private.h"
+#include "imap-urlauth-fetch.h"
+#include "imap-urlauth-connection.h"
+
+struct imap_urlauth_fetch_url {
+ struct imap_urlauth_fetch_url *prev, *next;
+
+ char *url;
+ enum imap_urlauth_fetch_flags flags;
+};
+
+struct imap_urlauth_fetch {
+ unsigned int refcount;
+ struct imap_urlauth_context *uctx;
+
+ imap_urlauth_fetch_callback_t *callback;
+ void *context;
+
+ /* local urls */
+ struct imap_urlauth_fetch_url *local_urls_head, *local_urls_tail;
+ struct imap_msgpart_url *local_url;
+
+ unsigned int pending_requests;
+
+ struct {
+ char *url;
+ enum imap_urlauth_fetch_flags flags;
+
+ struct istream *input;
+ uoff_t size;
+
+ char *bodypartstruct;
+ char *error;
+
+ bool succeeded:1;
+ bool binary_has_nuls:1;
+ } pending_reply;
+
+ bool failed:1;
+ bool waiting_local:1;
+ bool waiting_service:1;
+};
+
+static void imap_urlauth_fetch_abort_local(struct imap_urlauth_fetch *ufetch)
+{
+ struct imap_urlauth_fetch_url *url, *url_next;
+
+ if (ufetch->local_url != NULL) {
+ ufetch->pending_requests--;
+ imap_msgpart_url_free(&ufetch->local_url);
+ }
+
+ i_free_and_null(ufetch->pending_reply.url);
+ i_free_and_null(ufetch->pending_reply.bodypartstruct);
+ i_free_and_null(ufetch->pending_reply.error);
+ i_stream_unref(&ufetch->pending_reply.input);
+
+ url = ufetch->local_urls_head;
+ for (; url != NULL; url = url_next) {
+ url_next = url->next;
+ i_free(url->url);
+ i_free(url);
+ ufetch->pending_requests--;
+ }
+ ufetch->local_urls_head = ufetch->local_urls_tail = NULL;
+}
+
+static void imap_urlauth_fetch_abort(struct imap_urlauth_fetch *ufetch)
+{
+ if (ufetch->pending_requests > 0)
+ imap_urlauth_request_abort_by_context(ufetch->uctx->conn, ufetch);
+
+ imap_urlauth_fetch_abort_local(ufetch);
+
+ i_assert(ufetch->pending_requests == 0);
+}
+
+static void imap_urlauth_fetch_fail(struct imap_urlauth_fetch *ufetch)
+{
+ imap_urlauth_fetch_abort(ufetch);
+ ufetch->failed = TRUE;
+}
+
+struct imap_urlauth_fetch *
+imap_urlauth_fetch_init(struct imap_urlauth_context *uctx,
+ imap_urlauth_fetch_callback_t *callback, void *context)
+{
+ struct imap_urlauth_fetch *ufetch;
+
+ ufetch = i_new(struct imap_urlauth_fetch, 1);
+ ufetch->refcount = 1;
+ ufetch->uctx = uctx;
+ ufetch->callback = callback;
+ ufetch->context = context;
+ return ufetch;
+}
+
+static void imap_urlauth_fetch_ref(struct imap_urlauth_fetch *ufetch)
+{
+ i_assert(ufetch->refcount > 0);
+ ufetch->refcount++;
+}
+
+static void imap_urlauth_fetch_unref(struct imap_urlauth_fetch **_ufetch)
+{
+ struct imap_urlauth_fetch *ufetch = *_ufetch;
+
+ i_assert(ufetch->refcount > 0);
+
+ *_ufetch = NULL;
+ if (--ufetch->refcount > 0)
+ return;
+
+ ufetch->refcount++;
+ imap_urlauth_fetch_abort(ufetch);
+ ufetch->refcount--;
+ i_assert(ufetch->refcount == 0);
+
+ /* dont leave the connection in limbo; make sure continue is called */
+ if (ufetch->waiting_service)
+ imap_urlauth_connection_continue(ufetch->uctx->conn);
+ i_free(ufetch);
+}
+
+void imap_urlauth_fetch_deinit(struct imap_urlauth_fetch **_ufetch)
+{
+ imap_urlauth_fetch_unref(_ufetch);
+}
+
+static void
+imap_urlauth_fetch_error(struct imap_urlauth_fetch *ufetch, const char *url,
+ enum imap_urlauth_fetch_flags url_flags,
+ const char *error)
+{
+ struct imap_urlauth_fetch_reply reply;
+ int ret;
+
+ ufetch->pending_requests--;
+
+ i_zero(&reply);
+ reply.url = url;
+ reply.flags = url_flags;
+ reply.succeeded = FALSE;
+ reply.error = error;
+
+ T_BEGIN {
+ ret = ufetch->callback(&reply, ufetch->pending_requests == 0,
+ ufetch->context);
+ } T_END;
+
+ if (ret == 0) {
+ ufetch->waiting_local = TRUE;
+ ufetch->pending_requests++;
+ } else if (ret < 0) {
+ imap_urlauth_fetch_fail(ufetch);
+ }
+}
+
+static void
+imap_urlauth_fetch_local(struct imap_urlauth_fetch *ufetch, const char *url,
+ enum imap_urlauth_fetch_flags url_flags,
+ struct imap_url *imap_url)
+{
+ struct imap_urlauth_fetch_reply reply;
+ struct imap_msgpart_open_result mpresult;
+ const char *error, *errormsg = NULL, *bpstruct = NULL;
+ bool debug = ufetch->uctx->user->mail_debug, success;
+ enum mail_error error_code;
+ struct imap_msgpart_url *mpurl = NULL;
+ int ret;
+
+ success = TRUE;
+
+ if (debug)
+ i_debug("Fetching local URLAUTH %s", url);
+
+ if (url_flags == 0)
+ url_flags = IMAP_URLAUTH_FETCH_FLAG_BODY;
+
+ /* fetch URL */
+ if (imap_url == NULL) {
+ ret = imap_urlauth_fetch(ufetch->uctx, url,
+ &mpurl, &error_code, &error);
+ } else {
+ ret = imap_urlauth_fetch_parsed(ufetch->uctx, imap_url,
+ &mpurl, &error_code, &error);
+ }
+ if (ret <= 0) {
+ if (ret == 0) {
+ errormsg = t_strdup_printf("Failed to fetch URLAUTH \"%s\": %s",
+ url, error);
+ if (debug)
+ i_debug("%s", errormsg);
+ }
+ success = FALSE;
+ }
+
+ /* fetch metadata */
+ if (success && (url_flags & IMAP_URLAUTH_FETCH_FLAG_BINARY) != 0)
+ imap_msgpart_url_set_decode_to_binary(mpurl);
+ if (success &&
+ (url_flags & IMAP_URLAUTH_FETCH_FLAG_BODYPARTSTRUCTURE) != 0) {
+ ret = imap_msgpart_url_get_bodypartstructure(mpurl, &bpstruct, &error);
+ if (ret <= 0) {
+ if (ret == 0) {
+ errormsg = t_strdup_printf
+ ("Failed to read URLAUTH \"%s\": %s", url, error);
+ if (debug)
+ i_debug("%s", errormsg);
+ }
+ success = FALSE;
+ }
+ }
+
+ /* if requested, read the message part the URL points to */
+ i_zero(&mpresult);
+ if (success && ((url_flags & IMAP_URLAUTH_FETCH_FLAG_BODY) != 0 ||
+ (url_flags & IMAP_URLAUTH_FETCH_FLAG_BINARY) != 0)) {
+ ret = imap_msgpart_url_read_part(mpurl, &mpresult, &error);
+ if (ret <= 0) {
+ if (ret == 0) {
+ errormsg = t_strdup_printf
+ ("Failed to read URLAUTH \"%s\": %s", url, error);
+ if (debug)
+ i_debug("%s", errormsg);
+ }
+ success = FALSE;
+ }
+ }
+
+ if (debug && success) {
+ if (bpstruct != NULL)
+ i_debug("Fetched URLAUTH yielded BODYPARTSTRUCTURE (%s)", bpstruct);
+ if (mpresult.size == 0 || mpresult.input == NULL)
+ i_debug("Fetched URLAUTH yielded empty result");
+ else {
+ i_debug("Fetched URLAUTH yielded %"PRIuUOFF_T" bytes "
+ "of %smessage data", mpresult.size,
+ (mpresult.binary_decoded_input_has_nuls ? "binary " : ""));
+ }
+ }
+
+ ufetch->pending_requests--;
+
+ if (!success && ret < 0) {
+ if (mpurl != NULL)
+ imap_msgpart_url_free(&mpurl);
+ (void)ufetch->callback(NULL, TRUE, ufetch->context);
+ imap_urlauth_fetch_fail(ufetch);
+ return;
+ }
+
+ i_zero(&reply);
+ reply.url = url;
+ reply.flags = url_flags;
+ reply.error = errormsg;
+ reply.succeeded = success;
+
+ reply.bodypartstruct = bpstruct;
+ reply.binary_has_nuls = mpresult.binary_decoded_input_has_nuls;
+ reply.size = mpresult.size;
+ reply.input = mpresult.input;
+
+ ret = ufetch->callback(&reply, ufetch->pending_requests == 0,
+ ufetch->context);
+ if (ret == 0) {
+ ufetch->local_url = mpurl;
+ ufetch->waiting_local = TRUE;
+ ufetch->pending_requests++;
+ } else {
+
+ if (mpurl != NULL)
+ imap_msgpart_url_free(&mpurl);
+ if (ret < 0)
+ imap_urlauth_fetch_fail(ufetch);
+ }
+}
+
+static int
+imap_urlauth_fetch_request_callback(struct imap_urlauth_fetch_reply *reply,
+ void *context)
+{
+ struct imap_urlauth_fetch *ufetch =
+ (struct imap_urlauth_fetch *)context;
+ int ret = 1;
+
+ if (ufetch->waiting_local && reply != NULL) {
+ i_assert(ufetch->pending_reply.url == NULL);
+ ufetch->pending_reply.url = i_strdup(reply->url);
+ ufetch->pending_reply.flags = reply->flags;
+ ufetch->pending_reply.bodypartstruct =
+ i_strdup(reply->bodypartstruct);
+ ufetch->pending_reply.error = i_strdup(reply->error);
+ if (reply->input != NULL) {
+ ufetch->pending_reply.input = reply->input;
+ i_stream_ref(ufetch->pending_reply.input);
+ }
+ ufetch->pending_reply.size = reply->size;
+ ufetch->pending_reply.succeeded = reply->succeeded;
+ ufetch->pending_reply.binary_has_nuls = reply->binary_has_nuls;
+ ufetch->waiting_service = TRUE;
+ return 0;
+ }
+
+ ufetch->waiting_local = FALSE;
+ ufetch->pending_requests--;
+
+ imap_urlauth_fetch_ref(ufetch);
+
+ if (!ufetch->failed) {
+ bool last = ufetch->pending_requests == 0 || reply == NULL;
+ ret = ufetch->callback(reply, last, ufetch->context);
+ }
+
+ /* report failure only once */
+ if (ret < 0 || reply == NULL) {
+ if (!ufetch->failed)
+ imap_urlauth_fetch_abort_local(ufetch);
+ ufetch->failed = TRUE;
+ } else if (ret == 0) {
+ ufetch->waiting_service = TRUE;
+ ufetch->pending_requests++;
+ }
+
+ imap_urlauth_fetch_unref(&ufetch);
+ return ret;
+}
+
+int imap_urlauth_fetch_url(struct imap_urlauth_fetch *ufetch, const char *url,
+ enum imap_urlauth_fetch_flags url_flags)
+{
+ struct imap_urlauth_context *uctx = ufetch->uctx;
+ enum imap_url_parse_flags url_parse_flags =
+ IMAP_URL_PARSE_ALLOW_URLAUTH;
+ struct mail_user *mail_user = uctx->user;
+ struct imap_url *imap_url;
+ const char *error, *errormsg;
+
+ /* parse the url */
+ if (imap_url_parse(url, NULL, url_parse_flags, &imap_url, &error) < 0) {
+ errormsg = t_strdup_printf(
+ "Failed to fetch URLAUTH \"%s\": %s", url, error);
+ e_debug(mail_user->event, "%s", errormsg);
+ ufetch->pending_requests++;
+ imap_urlauth_fetch_ref(ufetch);
+ imap_urlauth_fetch_error(ufetch, url, url_flags, errormsg);
+ imap_urlauth_fetch_unref(&ufetch);
+ return 1;
+ }
+
+ return imap_urlauth_fetch_url_parsed(ufetch, url, imap_url, url_flags);
+}
+
+int imap_urlauth_fetch_url_parsed(struct imap_urlauth_fetch *ufetch,
+ const char *url, struct imap_url *imap_url,
+ enum imap_urlauth_fetch_flags url_flags)
+{
+ struct imap_urlauth_context *uctx = ufetch->uctx;
+ struct mail_user *mail_user = uctx->user;
+ const char *error, *errormsg;
+ int ret = 0;
+
+ ufetch->failed = FALSE;
+ ufetch->pending_requests++;
+
+ imap_urlauth_fetch_ref(ufetch);
+
+ /* if access user and target user match, handle fetch request locally */
+ if (imap_url->userid != NULL &&
+ strcmp(mail_user->username, imap_url->userid) == 0) {
+
+ if (ufetch->waiting_local) {
+ struct imap_urlauth_fetch_url *url_local;
+
+ url_local = i_new(struct imap_urlauth_fetch_url, 1);
+ url_local->url = i_strdup(url);
+ url_local->flags = url_flags;
+
+ DLLIST2_APPEND(&ufetch->local_urls_head,
+ &ufetch->local_urls_tail, url_local);
+ } else T_BEGIN {
+ imap_urlauth_fetch_local(ufetch, url,
+ url_flags, imap_url);
+ } T_END;
+ imap_url = NULL;
+ /* don't try to fetch remote URLs that are already known to fail access */
+ } else if (!imap_urlauth_check(uctx, imap_url, TRUE, &error)) {
+ errormsg = t_strdup_printf(
+ "Failed to fetch URLAUTH \"%s\": %s", url, error);
+ e_debug(mail_user->event, "%s", errormsg);
+ imap_urlauth_fetch_error(ufetch, url, url_flags, errormsg);
+ imap_url = NULL;
+ }
+
+ /* create request for url */
+ if (imap_url != NULL && imap_url->userid != NULL) {
+ i_assert(uctx->conn != NULL);
+ (void)imap_urlauth_request_new(uctx->conn, imap_url->userid,
+ url, url_flags,
+ imap_urlauth_fetch_request_callback, ufetch);
+ i_assert(uctx->conn != NULL);
+ if (imap_urlauth_connection_connect(uctx->conn) < 0)
+ ret = -1;
+ }
+ if (ret >= 0)
+ ret = (ufetch->pending_requests > 0 ? 0 : 1);
+
+ imap_urlauth_fetch_unref(&ufetch);
+ return ret;
+}
+
+static bool imap_urlauth_fetch_do_continue(struct imap_urlauth_fetch *ufetch)
+{
+ struct imap_urlauth_fetch_url *url, *url_next;
+ int ret;
+
+ if (ufetch->failed)
+ return FALSE;
+
+ if (!ufetch->waiting_local && !ufetch->waiting_service) {
+ /* not currently waiting for anything */
+ return ufetch->pending_requests > 0;
+ }
+
+ /* we finished a request */
+ ufetch->pending_requests--;
+
+ if (!ufetch->waiting_local) {
+ /* not waiting for local request handling */
+ ufetch->waiting_service = FALSE;
+ imap_urlauth_connection_continue(ufetch->uctx->conn);
+ return ufetch->pending_requests > 0;
+ }
+
+ /* finished local request */
+ if (ufetch->local_url != NULL) {
+ imap_msgpart_url_free(&ufetch->local_url);
+ }
+ ufetch->waiting_local = FALSE;
+
+ /* handle pending remote reply */
+ if (ufetch->pending_reply.url != NULL) {
+ struct imap_urlauth_fetch_reply reply;
+
+ ufetch->pending_requests--;
+
+ i_zero(&reply);
+ reply.url = ufetch->pending_reply.url;
+ reply.flags = ufetch->pending_reply.flags;
+ reply.bodypartstruct = ufetch->pending_reply.bodypartstruct;
+ reply.error = ufetch->pending_reply.error;
+ reply.input = ufetch->pending_reply.input;
+ reply.size = ufetch->pending_reply.size;
+ reply.succeeded = ufetch->pending_reply.succeeded;
+ reply.binary_has_nuls = ufetch->pending_reply.binary_has_nuls;
+
+ ret = ufetch->callback(&reply, ufetch->pending_requests == 0,
+ ufetch->context);
+
+ if (ufetch->pending_reply.url != NULL)
+ i_free(ufetch->pending_reply.url);
+ if (ufetch->pending_reply.input != NULL)
+ i_stream_unref(&ufetch->pending_reply.input);
+ if (ufetch->pending_reply.bodypartstruct != NULL)
+ i_free(ufetch->pending_reply.bodypartstruct);
+ if (ufetch->pending_reply.error != NULL)
+ i_free(ufetch->pending_reply.error);
+
+ if (ret < 0) {
+ imap_urlauth_fetch_fail(ufetch);
+ return FALSE;
+ }
+
+ if (ret == 0) {
+ ufetch->waiting_service = TRUE;
+ ufetch->pending_requests++;
+ return TRUE;
+ }
+
+ ufetch->waiting_service = FALSE;
+ imap_urlauth_connection_continue(ufetch->uctx->conn);
+ }
+
+ /* handle pending local urls */
+ url = ufetch->local_urls_head;
+ while (url != NULL) {
+ url_next = url->next;
+ T_BEGIN {
+ imap_urlauth_fetch_local(ufetch, url->url,
+ url->flags, NULL);
+ } T_END;
+ DLLIST2_REMOVE(&ufetch->local_urls_head,
+ &ufetch->local_urls_tail, url);
+ i_free(url->url);
+ i_free(url);
+ if (ufetch->waiting_local)
+ return TRUE;
+ url = url_next;
+ }
+
+ return ufetch->pending_requests > 0;
+}
+
+bool imap_urlauth_fetch_continue(struct imap_urlauth_fetch *ufetch)
+{
+ bool pending;
+
+ imap_urlauth_fetch_ref(ufetch);
+ pending = imap_urlauth_fetch_do_continue(ufetch);
+ imap_urlauth_fetch_unref(&ufetch);
+
+ return pending;
+}
+
+bool imap_urlauth_fetch_is_pending(struct imap_urlauth_fetch *ufetch)
+{
+ return ufetch->pending_requests > 0;
+}
diff --git a/src/lib-imap-urlauth/imap-urlauth-fetch.h b/src/lib-imap-urlauth/imap-urlauth-fetch.h
new file mode 100644
index 0000000..628a95e
--- /dev/null
+++ b/src/lib-imap-urlauth/imap-urlauth-fetch.h
@@ -0,0 +1,56 @@
+#ifndef IMAP_URLAUTH_FETCH_H
+#define IMAP_URLAUTH_FETCH_H
+
+struct imap_url;
+struct imap_urlauth_context;
+struct imap_urlauth_fetch;
+
+enum imap_urlauth_fetch_flags {
+ /* Indicates that this is an extended request */
+ IMAP_URLAUTH_FETCH_FLAG_EXTENDED = 0x01,
+ /* Fetch body part unmodified */
+ IMAP_URLAUTH_FETCH_FLAG_BODY = 0x02,
+ /* Fetch body part as binary, i.e. without content encoding */
+ IMAP_URLAUTH_FETCH_FLAG_BINARY = 0x04,
+ /* Fetch IMAP bodypartstructure */
+ IMAP_URLAUTH_FETCH_FLAG_BODYPARTSTRUCTURE = 0x08,
+};
+
+struct imap_urlauth_fetch_reply {
+ const char *url;
+ enum imap_urlauth_fetch_flags flags;
+
+ struct istream *input;
+ uoff_t size;
+
+ const char *bodypartstruct;
+ const char *error;
+
+ bool succeeded:1;
+ bool binary_has_nuls:1;
+};
+
+/* Callback to handle fetch reply. Returns 1 if handled completely and ready
+ for next reply, 0 if not all data was processed, and -1 for error. If a
+ callback returns 0, imap_urlauth_fetch_continue() must be called once
+ new replies may be processed. If this is the last request to yield a reply,
+ argument last is TRUE. */
+typedef int
+imap_urlauth_fetch_callback_t(struct imap_urlauth_fetch_reply *reply,
+ bool last, void *context);
+
+struct imap_urlauth_fetch *
+imap_urlauth_fetch_init(struct imap_urlauth_context *uctx,
+ imap_urlauth_fetch_callback_t *callback, void *context);
+void imap_urlauth_fetch_deinit(struct imap_urlauth_fetch **ufetch);
+
+int imap_urlauth_fetch_url(struct imap_urlauth_fetch *ufetch, const char *url,
+ enum imap_urlauth_fetch_flags url_flags);
+int imap_urlauth_fetch_url_parsed(struct imap_urlauth_fetch *ufetch,
+ const char *url, struct imap_url *imap_url,
+ enum imap_urlauth_fetch_flags url_flags);
+
+bool imap_urlauth_fetch_continue(struct imap_urlauth_fetch *ufetch);
+bool imap_urlauth_fetch_is_pending(struct imap_urlauth_fetch *ufetch);
+
+#endif
diff --git a/src/lib-imap-urlauth/imap-urlauth-private.h b/src/lib-imap-urlauth/imap-urlauth-private.h
new file mode 100644
index 0000000..3b3622c
--- /dev/null
+++ b/src/lib-imap-urlauth/imap-urlauth-private.h
@@ -0,0 +1,20 @@
+#ifndef IMAP_URLAUTH_PRIVATE_H
+#define IMAP_URLAUTH_PRIVATE_H
+
+#include "imap-urlauth.h"
+
+struct imap_urlauth_context {
+ struct mail_user *user;
+ struct imap_urlauth_connection *conn;
+
+ char *url_host;
+ in_port_t url_port;
+
+ char *access_user;
+ char *access_service;
+ const char **access_applications;
+
+ bool access_anonymous:1;
+};
+
+#endif
diff --git a/src/lib-imap-urlauth/imap-urlauth.c b/src/lib-imap-urlauth/imap-urlauth.c
new file mode 100644
index 0000000..a9c2673
--- /dev/null
+++ b/src/lib-imap-urlauth/imap-urlauth.c
@@ -0,0 +1,486 @@
+/* Copyright (c) 2013-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "hostpid.h"
+#include "var-expand.h"
+#include "hmac.h"
+#include "sha1.h"
+#include "randgen.h"
+#include "safe-memset.h"
+#include "mail-storage.h"
+#include "mail-storage-service.h"
+#include "mail-namespace.h"
+#include "mail-user.h"
+#include "imap-url.h"
+#include "imap-msgpart-url.h"
+#include "imap-urlauth-backend.h"
+#include "imap-urlauth-fetch.h"
+#include "imap-urlauth-connection.h"
+
+#include "imap-urlauth-private.h"
+
+#include <time.h>
+
+#define IMAP_URLAUTH_MECH_INTERNAL_VERSION 0x01
+
+#define IMAP_URLAUTH_NORMAL_TIMEOUT_MSECS 5*1000
+#define IMAP_URLAUTH_SPECIAL_TIMEOUT_MSECS 3*60*1000
+
+#define URL_HOST_ALLOW_ANY "*"
+
+struct imap_urlauth_context *
+imap_urlauth_init(struct mail_user *user,
+ const struct imap_urlauth_config *config)
+{
+ struct imap_urlauth_context *uctx;
+ unsigned int timeout;
+
+ i_assert(*config->url_host != '\0');
+
+ uctx = i_new(struct imap_urlauth_context, 1);
+ uctx->user = user;
+ uctx->url_host = i_strdup(config->url_host);
+ uctx->url_port = config->url_port;
+
+ if (config->access_anonymous)
+ uctx->access_user = i_strdup("anonymous");
+ else
+ uctx->access_user = i_strdup(config->access_user);
+ uctx->access_service = i_strdup(config->access_service);
+ uctx->access_anonymous = config->access_anonymous;
+ if (config->access_applications != NULL &&
+ *config->access_applications != NULL) {
+ uctx->access_applications =
+ p_strarray_dup(default_pool,
+ config->access_applications);
+ timeout = IMAP_URLAUTH_SPECIAL_TIMEOUT_MSECS;
+ } else {
+ timeout = IMAP_URLAUTH_NORMAL_TIMEOUT_MSECS;
+ }
+
+ if (config->socket_path != NULL) {
+ uctx->conn = imap_urlauth_connection_init(config->socket_path,
+ config->access_service, user, config->session_id, timeout);
+ }
+ return uctx;
+}
+
+void imap_urlauth_deinit(struct imap_urlauth_context **_uctx)
+{
+ struct imap_urlauth_context *uctx = *_uctx;
+
+ *_uctx = NULL;
+
+ if (uctx->conn != NULL)
+ imap_urlauth_connection_deinit(&uctx->conn);
+ i_free(uctx->url_host);
+ i_free(uctx->access_user);
+ i_free(uctx->access_service);
+ i_free(uctx->access_applications);
+ i_free(uctx);
+}
+
+static const unsigned char *
+imap_urlauth_internal_generate(const char *rumpurl,
+ const unsigned char mailbox_key[IMAP_URLAUTH_KEY_LEN],
+ size_t *token_len_r)
+{
+ struct hmac_context hmac;
+ unsigned char *token;
+
+ token = t_new(unsigned char, SHA1_RESULTLEN + 1);
+ token[0] = IMAP_URLAUTH_MECH_INTERNAL_VERSION;
+
+ hmac_init(&hmac, mailbox_key, IMAP_URLAUTH_KEY_LEN, &hash_method_sha1);
+ hmac_update(&hmac, rumpurl, strlen(rumpurl));
+ hmac_final(&hmac, token+1);
+
+ *token_len_r = SHA1_RESULTLEN + 1;
+ return token;
+}
+
+static bool
+imap_urlauth_internal_verify(const char *rumpurl,
+ const unsigned char mailbox_key[IMAP_URLAUTH_KEY_LEN],
+ const unsigned char *token, size_t token_len)
+{
+ const unsigned char *valtoken;
+ size_t valtoken_len;
+
+ if (rumpurl == NULL || token == NULL)
+ return FALSE;
+
+ valtoken = imap_urlauth_internal_generate(rumpurl, mailbox_key,
+ &valtoken_len);
+ /* Note: the token length has timing leak here in any case */
+ if (token_len != valtoken_len)
+ return FALSE;
+
+ return mem_equals_timing_safe(token, valtoken, valtoken_len);
+}
+
+static bool
+access_applications_have_access(struct imap_urlauth_context *uctx,
+ struct imap_url *url, const char *const *access_applications)
+{
+ const char *const *application;
+
+ if (access_applications == NULL)
+ return FALSE;
+
+ application = access_applications;
+ for (; *application != NULL; application++) {
+ const char *app = *application;
+ bool have_userid = FALSE;
+ size_t len = strlen(app);
+
+ if (app[len-1] == '+')
+ have_userid = TRUE;
+
+ if (strncasecmp(url->uauth_access_application, app, len-1) == 0) {
+ if (!have_userid) {
+ /* this access application must have no userid */
+ return url->uauth_access_user == NULL;
+ }
+
+ /* this access application must have a userid */
+ return (!uctx->access_anonymous && url->uauth_access_user != NULL);
+ }
+ }
+ return FALSE;
+}
+
+static bool
+imap_urlauth_check_access(struct imap_urlauth_context *uctx,
+ struct imap_url *url, bool ignore_unknown,
+ const char **error_r)
+{
+ const char *userid;
+
+ if (url->uauth_access_application == NULL) {
+ *error_r = "URL is missing URLAUTH";
+ return FALSE;
+ }
+
+ if (strcmp(uctx->access_service, "imap") == 0) {
+ /* these access types are only allowed if URL is accessed through imap */
+ if (strcasecmp(url->uauth_access_application, "user") == 0) {
+ /* user+<access_user> */
+ if (url->uauth_access_user == NULL) {
+ *error_r = "URLAUTH `user' access is missing userid";
+ return FALSE;
+ }
+ if (!uctx->access_anonymous ||
+ strcasecmp(url->uauth_access_user, uctx->access_user) == 0)
+ return TRUE;
+ } else if (strcasecmp(url->uauth_access_application, "authuser") == 0) {
+ /* authuser */
+ if (!uctx->access_anonymous)
+ return TRUE;
+ } else if (strcasecmp(url->uauth_access_application, "anonymous") == 0) {
+ /* anonymous */
+ return TRUE;
+ } else if (ignore_unknown || access_applications_have_access
+ (uctx, url, uctx->access_applications)) {
+ return TRUE;
+ }
+ } else if (strcmp(uctx->access_service, "submission") == 0) {
+ /* accessed directly through submission service */
+
+ if (strcasecmp(url->uauth_access_application, "submit") != 0) {
+ userid = url->uauth_access_user == NULL ? "" :
+ t_strdup_printf("+%s", url->uauth_access_user);
+ *error_r = t_strdup_printf(
+ "No '%s%s' access allowed for submission service",
+ url->uauth_access_application, userid);
+ return FALSE;
+ } else if (url->uauth_access_user == NULL) {
+ *error_r = "URLAUTH `submit' access is missing userid";
+ return FALSE;
+ } else if (!uctx->access_anonymous &&
+ strcasecmp(url->uauth_access_user, uctx->access_user) == 0) {
+ return TRUE;
+ }
+ }
+
+ userid = url->uauth_access_user == NULL ? "" :
+ t_strdup_printf("+%s", url->uauth_access_user);
+
+ if (uctx->access_anonymous) {
+ *error_r = t_strdup_printf(
+ "No '%s%s' access allowed for anonymous user",
+ url->uauth_access_application, userid);
+ } else {
+ *error_r = t_strdup_printf(
+ "No '%s%s' access allowed for user %s",
+ url->uauth_access_application, userid, uctx->access_user);
+ }
+ return FALSE;
+}
+
+static bool
+imap_urlauth_check_hostport(struct imap_urlauth_context *uctx,
+ struct imap_url *url, const char **client_error_r)
+{
+ /* validate host */
+ /* FIXME: allow host ip/ip6 as well? */
+ if (strcmp(uctx->url_host, URL_HOST_ALLOW_ANY) != 0 &&
+ strcmp(url->host.name, uctx->url_host) != 0) {
+ *client_error_r = "Invalid URL: Inappropriate host name";
+ return FALSE;
+ }
+
+ /* validate port */
+ if ((url->port == 0 && uctx->url_port != 143) ||
+ (url->port != 0 && uctx->url_port != url->port)) {
+ *client_error_r = "Invalid URL: Inappropriate server port";
+ return FALSE;
+ }
+ return TRUE;
+}
+
+int imap_urlauth_generate(struct imap_urlauth_context *uctx,
+ const char *mechanism, const char *rumpurl,
+ const char **urlauth_r, const char **client_error_r)
+{
+ struct mail_user *user = uctx->user;
+ enum imap_url_parse_flags url_flags =
+ IMAP_URL_PARSE_ALLOW_URLAUTH;
+ struct imap_url *url;
+ struct imap_msgpart_url *mpurl = NULL;
+ struct mailbox *box;
+ const char *error;
+ enum mail_error error_code;
+ unsigned char mailbox_key[IMAP_URLAUTH_KEY_LEN];
+ const unsigned char *token;
+ size_t token_len;
+ int ret;
+
+ /* validate mechanism */
+ if (strcasecmp(mechanism, "INTERNAL") != 0) {
+ *client_error_r = t_strdup_printf("Unsupported URLAUTH mechanism: %s", mechanism);
+ return 0;
+ }
+
+ /* validate URL */
+ if (imap_url_parse(rumpurl, NULL, url_flags, &url, &error) < 0) {
+ *client_error_r = t_strdup_printf("Invalid URL: %s", error);
+ return 0;
+ }
+
+ if (url->mailbox == NULL || url->uid == 0 || url->search_program != NULL ||
+ url->uauth_rumpurl == NULL || url->uauth_mechanism != NULL) {
+ *client_error_r = "Invalid URL: Must be an URLAUTH rump URL";
+ return 0;
+ }
+
+ /* validate expiry time */
+ if (url->uauth_expire != (time_t)-1) {
+ time_t now = time(NULL);
+
+ if (now > url->uauth_expire) {
+ *client_error_r = t_strdup_printf("URLAUTH has already expired");
+ return 0;
+ }
+ }
+
+ /* validate user */
+ if (url->userid == NULL) {
+ *client_error_r = "Invalid URL: Missing user name";
+ return 0;
+ }
+ if (user->anonymous || strcmp(url->userid, user->username) != 0) {
+ *client_error_r = t_strdup_printf(
+ "Not permitted to generate URLAUTH for user %s",
+ url->userid);
+ return 0;
+ }
+
+ /* validate host:port */
+ if (!imap_urlauth_check_hostport(uctx, url, client_error_r))
+ return 0;
+
+ /* validate mailbox */
+ if ((ret = imap_msgpart_url_create(user, url, &mpurl, &error)) < 0 ||
+ imap_msgpart_url_verify(mpurl, &error) <= 0) {
+ *client_error_r = t_strdup_printf("Invalid URL: %s", error);
+ if (mpurl != NULL)
+ imap_msgpart_url_free(&mpurl);
+ return ret;
+ }
+ box = imap_msgpart_url_get_mailbox(mpurl);
+
+ /* obtain mailbox key */
+ ret = imap_urlauth_backend_get_mailbox_key(box, TRUE, mailbox_key,
+ client_error_r, &error_code);
+ if (ret < 0) {
+ imap_msgpart_url_free(&mpurl);
+ return ret;
+ }
+
+ token = imap_urlauth_internal_generate(rumpurl, mailbox_key, &token_len);
+ imap_msgpart_url_free(&mpurl);
+
+ *urlauth_r = imap_url_add_urlauth(rumpurl, mechanism, token, token_len);
+ return 1;
+}
+
+bool imap_urlauth_check(struct imap_urlauth_context *uctx,
+ struct imap_url *url, bool ignore_unknown_access,
+ const char **error_r)
+{
+ /* validate URL fields */
+ if (url->mailbox == NULL || url->uid == 0 ||
+ url->search_program != NULL || url->uauth_rumpurl == NULL ||
+ url->uauth_mechanism == NULL) {
+ *error_r = "Invalid URL: Must be a full URLAUTH URL";
+ return FALSE;
+ }
+
+ /* check presence of userid */
+ if (url->userid == NULL) {
+ *error_r = "Invalid URLAUTH: Missing user name";
+ return FALSE;
+ }
+
+ /* validate mechanism */
+ if (strcasecmp(url->uauth_mechanism, "INTERNAL") != 0) {
+ *error_r = t_strdup_printf("Unsupported URLAUTH mechanism: %s",
+ url->uauth_mechanism);
+ return FALSE;
+ }
+
+ /* validate expiry time */
+ if (url->uauth_expire != (time_t)-1) {
+ time_t now = time(NULL);
+
+ if (now > url->uauth_expire) {
+ *error_r = t_strdup_printf("URLAUTH has expired");
+ return FALSE;
+ }
+ }
+
+ /* validate access */
+ if (!imap_urlauth_check_access(uctx, url, ignore_unknown_access,
+ error_r))
+ return FALSE;
+ /* validate host:port */
+ if (!imap_urlauth_check_hostport(uctx, url, error_r))
+ return FALSE;
+ return TRUE;
+}
+
+int imap_urlauth_fetch_parsed(struct imap_urlauth_context *uctx,
+ struct imap_url *url,
+ struct imap_msgpart_url **mpurl_r,
+ enum mail_error *error_code_r,
+ const char **error_r)
+{
+ struct mail_user *user = uctx->user;
+ struct imap_msgpart_url *mpurl;
+ struct mailbox *box;
+ const char *error;
+ unsigned char mailbox_key[IMAP_URLAUTH_KEY_LEN];
+ int ret;
+
+ *mpurl_r = NULL;
+ *error_r = NULL;
+ *error_code_r = MAIL_ERROR_NONE;
+
+ /* check urlauth mechanism, access, userid and authority */
+ if (!imap_urlauth_check(uctx, url, FALSE, error_r)) {
+ *error_code_r = MAIL_ERROR_PARAMS;
+ return 0;
+ }
+
+ /* validate target user */
+ if (user->anonymous || strcmp(url->userid, user->username) != 0) {
+ *error_r = t_strdup_printf("Not permitted to fetch URLAUTH for user %s",
+ url->userid);
+ *error_code_r = MAIL_ERROR_PARAMS;
+ return 0;
+ }
+
+ /* validate mailbox */
+ if ((ret = imap_msgpart_url_create(user, url, &mpurl, &error)) < 0) {
+ *error_r = t_strdup_printf("Invalid URLAUTH: %s", error);
+ *error_code_r = MAIL_ERROR_PARAMS;
+ return ret;
+ }
+
+ if ((ret = imap_msgpart_url_open_mailbox(mpurl, &box, error_code_r,
+ &error)) < 0) {
+ *error_r = "Internal server error";
+ imap_msgpart_url_free(&mpurl);
+ return -1;
+ }
+
+ if (ret == 0) {
+ /* RFC says: `If the mailbox cannot be identified, an
+ authorization token is calculated on the rump URL, using
+ random "plausible" keys (selected by the server) as needed,
+ before returning a validation failure. This prevents timing
+ attacks aimed at identifying mailbox names.' */
+ random_fill(mailbox_key, sizeof(mailbox_key));
+ (void)imap_urlauth_internal_verify(url->uauth_rumpurl,
+ mailbox_key, url->uauth_token, url->uauth_token_size);
+
+ *error_r = t_strdup_printf("Invalid URLAUTH: %s", error);
+ imap_msgpart_url_free(&mpurl);
+ return 0;
+ }
+
+ /* obtain mailbox key */
+ ret = imap_urlauth_backend_get_mailbox_key(box, FALSE, mailbox_key,
+ error_r, error_code_r);
+ if (ret < 0) {
+ imap_msgpart_url_free(&mpurl);
+ return -1;
+ }
+
+ if (ret == 0 ||
+ !imap_urlauth_internal_verify(url->uauth_rumpurl, mailbox_key,
+ url->uauth_token,
+ url->uauth_token_size)) {
+ *error_r = "URLAUTH verification failed";
+ *error_code_r = MAIL_ERROR_PERM;
+ imap_msgpart_url_free(&mpurl);
+ ret = 0;
+ } else {
+ ret = 1;
+ }
+
+ safe_memset(mailbox_key, 0, sizeof(mailbox_key));
+ *mpurl_r = mpurl;
+ return ret;
+}
+
+int imap_urlauth_fetch(struct imap_urlauth_context *uctx,
+ const char *urlauth, struct imap_msgpart_url **mpurl_r,
+ enum mail_error *error_code_r, const char **error_r)
+{
+ struct imap_url *url;
+ enum imap_url_parse_flags url_flags = IMAP_URL_PARSE_ALLOW_URLAUTH;
+ const char *error;
+
+ /* validate URL */
+ if (imap_url_parse(urlauth, NULL, url_flags, &url, &error) < 0) {
+ *error_r = t_strdup_printf("Invalid URLAUTH: %s", error);
+ *error_code_r = MAIL_ERROR_PARAMS;
+ return 0;
+ }
+
+ return imap_urlauth_fetch_parsed(uctx, url, mpurl_r,
+ error_code_r, error_r);
+}
+
+int imap_urlauth_reset_mailbox_key(struct imap_urlauth_context *uctx ATTR_UNUSED,
+ struct mailbox *box)
+{
+ return imap_urlauth_backend_reset_mailbox_key(box);
+}
+
+int imap_urlauth_reset_all_keys(struct imap_urlauth_context *uctx)
+{
+ return imap_urlauth_backend_reset_all_keys(uctx->user);
+}
diff --git a/src/lib-imap-urlauth/imap-urlauth.h b/src/lib-imap-urlauth/imap-urlauth.h
new file mode 100644
index 0000000..9c7c30f
--- /dev/null
+++ b/src/lib-imap-urlauth/imap-urlauth.h
@@ -0,0 +1,55 @@
+#ifndef IMAP_URLAUTH_H
+#define IMAP_URLAUTH_H
+
+#include "net.h"
+
+#define IMAP_URLAUTH_SOCKET_NAME "imap-urlauth"
+
+struct imap_url;
+struct imap_msgpart_url;
+struct imap_urlauth_context;
+
+struct imap_urlauth_config {
+ const char *url_host;
+ in_port_t url_port;
+
+ const char *socket_path;
+ const char *session_id;
+
+ /* the user who is requesting access to URLAUTHs */
+ const char *access_user;
+ /* ... is using this service (i.e. imap or submission) */
+ const char *access_service;
+ /* ... represents these applications */
+ const char *const *access_applications;
+ /* ... is anonymous? */
+ bool access_anonymous;
+};
+
+struct imap_urlauth_context *
+imap_urlauth_init(struct mail_user *user,
+ const struct imap_urlauth_config *config);
+void imap_urlauth_deinit(struct imap_urlauth_context **_uctx);
+
+int imap_urlauth_generate(struct imap_urlauth_context *uctx,
+ const char *mechanism, const char *rumpurl,
+ const char **urlauth_r, const char **client_error_r);
+
+bool imap_urlauth_check(struct imap_urlauth_context *uctx,
+ struct imap_url *url, bool ignore_unknown_access,
+ const char **error_r);
+
+int imap_urlauth_fetch_parsed(struct imap_urlauth_context *uctx,
+ struct imap_url *url,
+ struct imap_msgpart_url **mpurl_r,
+ enum mail_error *error_code_r,
+ const char **error_r);
+int imap_urlauth_fetch(struct imap_urlauth_context *uctx,
+ const char *urlauth, struct imap_msgpart_url **mpurl_r,
+ enum mail_error *error_code_r, const char **error_r);
+
+int imap_urlauth_reset_mailbox_key(struct imap_urlauth_context *uctx,
+ struct mailbox *box);
+int imap_urlauth_reset_all_keys(struct imap_urlauth_context *uctx);
+
+#endif
diff --git a/src/lib-imap/Makefile.am b/src/lib-imap/Makefile.am
new file mode 100644
index 0000000..2f59f93
--- /dev/null
+++ b/src/lib-imap/Makefile.am
@@ -0,0 +1,120 @@
+noinst_LTLIBRARIES = libimap.la
+
+AM_CPPFLAGS = \
+ -I$(top_srcdir)/src/lib \
+ -I$(top_srcdir)/src/lib-test \
+ -I$(top_srcdir)/src/lib-charset \
+ -I$(top_srcdir)/src/lib-mail
+
+libimap_la_SOURCES = \
+ imap-arg.c \
+ imap-base-subject.c \
+ imap-bodystructure.c \
+ imap-date.c \
+ imap-envelope.c \
+ imap-id.c \
+ imap-keepalive.c \
+ imap-match.c \
+ imap-parser.c \
+ imap-quote.c \
+ imap-url.c \
+ imap-seqset.c \
+ imap-utf7.c \
+ imap-util.c
+
+headers = \
+ imap-arg.h \
+ imap-base-subject.h \
+ imap-bodystructure.h \
+ imap-date.h \
+ imap-envelope.h \
+ imap-id.h \
+ imap-keepalive.h \
+ imap-match.h \
+ imap-parser.h \
+ imap-resp-code.h \
+ imap-quote.h \
+ imap-url.h \
+ imap-seqset.h \
+ imap-utf7.h \
+ imap-util.h
+
+pkginc_libdir=$(pkgincludedir)
+pkginc_lib_HEADERS = $(headers)
+
+test_programs = \
+ test-imap-bodystructure \
+ test-imap-envelope \
+ test-imap-match \
+ test-imap-parser \
+ test-imap-quote \
+ test-imap-url \
+ test-imap-utf7 \
+ test-imap-util
+
+noinst_PROGRAMS = $(test_programs)
+
+test_libs = \
+ ../lib-test/libtest.la \
+ ../lib/liblib.la
+
+test_deps = $(noinst_LTLIBRARIES) $(test_libs)
+
+test_imap_bodystructure_SOURCES = test-imap-bodystructure.c
+test_imap_bodystructure_LDADD = imap-bodystructure.lo imap-envelope.lo imap-quote.lo imap-parser.lo imap-arg.lo ../lib-mail/libmail.la $(test_libs)
+test_imap_bodystructure_DEPENDENCIES = $(test_deps) ../lib-mail/libmail.la
+
+test_imap_envelope_SOURCES = test-imap-envelope.c
+test_imap_envelope_LDADD = imap-envelope.lo imap-quote.lo imap-parser.lo imap-arg.lo ../lib-mail/libmail.la $(test_libs)
+test_imap_envelope_DEPENDENCIES = $(test_deps) ../lib-mail/libmail.la
+
+test_imap_match_SOURCES = test-imap-match.c
+test_imap_match_LDADD = imap-match.lo $(test_libs)
+test_imap_match_DEPENDENCIES = $(test_deps)
+
+test_imap_parser_SOURCES = test-imap-parser.c
+test_imap_parser_LDADD = imap-parser.lo imap-arg.lo $(test_libs)
+test_imap_parser_DEPENDENCIES = $(test_deps)
+
+test_imap_quote_SOURCES = test-imap-quote.c
+test_imap_quote_LDADD = imap-quote.lo $(test_libs)
+test_imap_quote_DEPENDENCIES = $(test_deps)
+
+test_imap_url_SOURCES = test-imap-url.c
+test_imap_url_LDADD = imap-url.lo $(test_libs)
+test_imap_url_DEPENDENCIES = $(test_deps)
+
+test_imap_utf7_SOURCES = test-imap-utf7.c
+test_imap_utf7_LDADD = imap-utf7.lo $(test_libs)
+test_imap_utf7_DEPENDENCIES = $(test_deps)
+
+test_imap_util_SOURCES = test-imap-util.c
+test_imap_util_LDADD = imap-util.lo imap-arg.lo $(test_libs)
+test_imap_util_DEPENDENCIES = $(test_deps)
+
+check-local:
+ for bin in $(test_programs); do \
+ if ! $(RUN_TEST) ./$$bin; then exit 1; fi; \
+ done
+
+if USE_FUZZER
+noinst_PROGRAMS += \
+ fuzz-imap-utf7 \
+ fuzz-imap-bodystructure
+
+nodist_EXTRA_fuzz_imap_utf7_SOURCES = force-cxx-linking.cxx
+fuzz_imap_utf7_SOURCES = fuzz-imap-utf7.c
+fuzz_imap_utf7_CPPFLAGS = $(FUZZER_CPPFLAGS)
+fuzz_imap_utf7_LDFLAGS = $(FUZZER_LDFLAGS)
+fuzz_imap_utf7_LDADD = libimap.la $(test_libs)
+fuzz_imap_utf7_DEPENDENCIES = libimap.la $(test_deps)
+
+nodist_EXTRA_fuzz_imap_bodystructure_SOURCES = force-cxx-linking.cxx
+fuzz_imap_bodystructure_SOURCES = fuzz-imap-bodystructure.c
+fuzz_imap_bodystructure_CPPFLAGS = $(FUZZER_CPPFLAGS)
+fuzz_imap_bodystructure_LDFLAGS = $(FUZZER_LDFLAGS)
+fuzz_imap_bodystructure_LDADD = libimap.la $(test_libs)
+fuzz_imap_bodystructure_DEPENDENCIES = libimap.la $(test_deps)
+
+
+endif
diff --git a/src/lib-imap/Makefile.in b/src/lib-imap/Makefile.in
new file mode 100644
index 0000000..5fd6ea4
--- /dev/null
+++ b/src/lib-imap/Makefile.in
@@ -0,0 +1,1196 @@
+# Makefile.in generated by automake 1.16.1 from Makefile.am.
+# @configure_input@
+
+# Copyright (C) 1994-2018 Free Software Foundation, Inc.
+
+# This Makefile.in is free software; the Free Software Foundation
+# gives unlimited permission to copy and/or distribute it,
+# with or without modifications, as long as this notice is preserved.
+
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY, to the extent permitted by law; without
+# even the implied warranty of MERCHANTABILITY or FITNESS FOR A
+# PARTICULAR PURPOSE.
+
+@SET_MAKE@
+
+
+
+VPATH = @srcdir@
+am__is_gnu_make = { \
+ if test -z '$(MAKELEVEL)'; then \
+ false; \
+ elif test -n '$(MAKE_HOST)'; then \
+ true; \
+ elif test -n '$(MAKE_VERSION)' && test -n '$(CURDIR)'; then \
+ true; \
+ else \
+ false; \
+ fi; \
+}
+am__make_running_with_option = \
+ case $${target_option-} in \
+ ?) ;; \
+ *) echo "am__make_running_with_option: internal error: invalid" \
+ "target option '$${target_option-}' specified" >&2; \
+ exit 1;; \
+ esac; \
+ has_opt=no; \
+ sane_makeflags=$$MAKEFLAGS; \
+ if $(am__is_gnu_make); then \
+ sane_makeflags=$$MFLAGS; \
+ else \
+ case $$MAKEFLAGS in \
+ *\\[\ \ ]*) \
+ bs=\\; \
+ sane_makeflags=`printf '%s\n' "$$MAKEFLAGS" \
+ | sed "s/$$bs$$bs[$$bs $$bs ]*//g"`;; \
+ esac; \
+ fi; \
+ skip_next=no; \
+ strip_trailopt () \
+ { \
+ flg=`printf '%s\n' "$$flg" | sed "s/$$1.*$$//"`; \
+ }; \
+ for flg in $$sane_makeflags; do \
+ test $$skip_next = yes && { skip_next=no; continue; }; \
+ case $$flg in \
+ *=*|--*) continue;; \
+ -*I) strip_trailopt 'I'; skip_next=yes;; \
+ -*I?*) strip_trailopt 'I';; \
+ -*O) strip_trailopt 'O'; skip_next=yes;; \
+ -*O?*) strip_trailopt 'O';; \
+ -*l) strip_trailopt 'l'; skip_next=yes;; \
+ -*l?*) strip_trailopt 'l';; \
+ -[dEDm]) skip_next=yes;; \
+ -[JT]) skip_next=yes;; \
+ esac; \
+ case $$flg in \
+ *$$target_option*) has_opt=yes; break;; \
+ esac; \
+ done; \
+ test $$has_opt = yes
+am__make_dryrun = (target_option=n; $(am__make_running_with_option))
+am__make_keepgoing = (target_option=k; $(am__make_running_with_option))
+pkgdatadir = $(datadir)/@PACKAGE@
+pkgincludedir = $(includedir)/@PACKAGE@
+pkglibdir = $(libdir)/@PACKAGE@
+pkglibexecdir = $(libexecdir)/@PACKAGE@
+am__cd = CDPATH="$${ZSH_VERSION+.}$(PATH_SEPARATOR)" && cd
+install_sh_DATA = $(install_sh) -c -m 644
+install_sh_PROGRAM = $(install_sh) -c
+install_sh_SCRIPT = $(install_sh) -c
+INSTALL_HEADER = $(INSTALL_DATA)
+transform = $(program_transform_name)
+NORMAL_INSTALL = :
+PRE_INSTALL = :
+POST_INSTALL = :
+NORMAL_UNINSTALL = :
+PRE_UNINSTALL = :
+POST_UNINSTALL = :
+build_triplet = @build@
+host_triplet = @host@
+noinst_PROGRAMS = $(am__EXEEXT_1) $(am__EXEEXT_2)
+@USE_FUZZER_TRUE@am__append_1 = \
+@USE_FUZZER_TRUE@ fuzz-imap-utf7 \
+@USE_FUZZER_TRUE@ fuzz-imap-bodystructure
+
+subdir = src/lib-imap
+ACLOCAL_M4 = $(top_srcdir)/aclocal.m4
+am__aclocal_m4_deps = $(top_srcdir)/m4/ac_checktype2.m4 \
+ $(top_srcdir)/m4/ac_typeof.m4 $(top_srcdir)/m4/arc4random.m4 \
+ $(top_srcdir)/m4/blockdev.m4 $(top_srcdir)/m4/c99_vsnprintf.m4 \
+ $(top_srcdir)/m4/clock_gettime.m4 $(top_srcdir)/m4/crypt.m4 \
+ $(top_srcdir)/m4/crypt_xpg6.m4 $(top_srcdir)/m4/dbqlk.m4 \
+ $(top_srcdir)/m4/dirent_dtype.m4 $(top_srcdir)/m4/dovecot.m4 \
+ $(top_srcdir)/m4/fd_passing.m4 $(top_srcdir)/m4/fdatasync.m4 \
+ $(top_srcdir)/m4/flexible_array_member.m4 \
+ $(top_srcdir)/m4/glibc.m4 $(top_srcdir)/m4/gmtime_max.m4 \
+ $(top_srcdir)/m4/gmtime_tm_gmtoff.m4 \
+ $(top_srcdir)/m4/ioloop.m4 $(top_srcdir)/m4/iovec.m4 \
+ $(top_srcdir)/m4/ipv6.m4 $(top_srcdir)/m4/libcap.m4 \
+ $(top_srcdir)/m4/libtool.m4 $(top_srcdir)/m4/libwrap.m4 \
+ $(top_srcdir)/m4/linux_mremap.m4 $(top_srcdir)/m4/ltoptions.m4 \
+ $(top_srcdir)/m4/ltsugar.m4 $(top_srcdir)/m4/ltversion.m4 \
+ $(top_srcdir)/m4/lt~obsolete.m4 $(top_srcdir)/m4/mmap_write.m4 \
+ $(top_srcdir)/m4/mntctl.m4 $(top_srcdir)/m4/modules.m4 \
+ $(top_srcdir)/m4/notify.m4 $(top_srcdir)/m4/nsl.m4 \
+ $(top_srcdir)/m4/off_t_max.m4 $(top_srcdir)/m4/pkg.m4 \
+ $(top_srcdir)/m4/pr_set_dumpable.m4 \
+ $(top_srcdir)/m4/q_quotactl.m4 $(top_srcdir)/m4/quota.m4 \
+ $(top_srcdir)/m4/random.m4 $(top_srcdir)/m4/rlimit.m4 \
+ $(top_srcdir)/m4/sendfile.m4 $(top_srcdir)/m4/size_t_signed.m4 \
+ $(top_srcdir)/m4/sockpeercred.m4 $(top_srcdir)/m4/sql.m4 \
+ $(top_srcdir)/m4/ssl.m4 $(top_srcdir)/m4/st_tim.m4 \
+ $(top_srcdir)/m4/static_array.m4 $(top_srcdir)/m4/test_with.m4 \
+ $(top_srcdir)/m4/time_t.m4 $(top_srcdir)/m4/typeof.m4 \
+ $(top_srcdir)/m4/typeof_dev_t.m4 \
+ $(top_srcdir)/m4/uoff_t_max.m4 $(top_srcdir)/m4/vararg.m4 \
+ $(top_srcdir)/m4/want_apparmor.m4 \
+ $(top_srcdir)/m4/want_bsdauth.m4 \
+ $(top_srcdir)/m4/want_bzlib.m4 \
+ $(top_srcdir)/m4/want_cassandra.m4 \
+ $(top_srcdir)/m4/want_cdb.m4 \
+ $(top_srcdir)/m4/want_checkpassword.m4 \
+ $(top_srcdir)/m4/want_clucene.m4 $(top_srcdir)/m4/want_db.m4 \
+ $(top_srcdir)/m4/want_gssapi.m4 $(top_srcdir)/m4/want_icu.m4 \
+ $(top_srcdir)/m4/want_ldap.m4 $(top_srcdir)/m4/want_lua.m4 \
+ $(top_srcdir)/m4/want_lz4.m4 $(top_srcdir)/m4/want_lzma.m4 \
+ $(top_srcdir)/m4/want_mysql.m4 $(top_srcdir)/m4/want_pam.m4 \
+ $(top_srcdir)/m4/want_passwd.m4 $(top_srcdir)/m4/want_pgsql.m4 \
+ $(top_srcdir)/m4/want_prefetch.m4 \
+ $(top_srcdir)/m4/want_shadow.m4 \
+ $(top_srcdir)/m4/want_sodium.m4 $(top_srcdir)/m4/want_solr.m4 \
+ $(top_srcdir)/m4/want_sqlite.m4 \
+ $(top_srcdir)/m4/want_stemmer.m4 \
+ $(top_srcdir)/m4/want_systemd.m4 \
+ $(top_srcdir)/m4/want_textcat.m4 \
+ $(top_srcdir)/m4/want_unwind.m4 $(top_srcdir)/m4/want_zlib.m4 \
+ $(top_srcdir)/m4/want_zstd.m4 $(top_srcdir)/configure.ac
+am__configure_deps = $(am__aclocal_m4_deps) $(CONFIGURE_DEPENDENCIES) \
+ $(ACLOCAL_M4)
+DIST_COMMON = $(srcdir)/Makefile.am $(pkginc_lib_HEADERS) \
+ $(am__DIST_COMMON)
+mkinstalldirs = $(install_sh) -d
+CONFIG_HEADER = $(top_builddir)/config.h
+CONFIG_CLEAN_FILES =
+CONFIG_CLEAN_VPATH_FILES =
+am__EXEEXT_1 = test-imap-bodystructure$(EXEEXT) \
+ test-imap-envelope$(EXEEXT) test-imap-match$(EXEEXT) \
+ test-imap-parser$(EXEEXT) test-imap-quote$(EXEEXT) \
+ test-imap-url$(EXEEXT) test-imap-utf7$(EXEEXT) \
+ test-imap-util$(EXEEXT)
+@USE_FUZZER_TRUE@am__EXEEXT_2 = fuzz-imap-utf7$(EXEEXT) \
+@USE_FUZZER_TRUE@ fuzz-imap-bodystructure$(EXEEXT)
+PROGRAMS = $(noinst_PROGRAMS)
+LTLIBRARIES = $(noinst_LTLIBRARIES)
+libimap_la_LIBADD =
+am_libimap_la_OBJECTS = imap-arg.lo imap-base-subject.lo \
+ imap-bodystructure.lo imap-date.lo imap-envelope.lo imap-id.lo \
+ imap-keepalive.lo imap-match.lo imap-parser.lo imap-quote.lo \
+ imap-url.lo imap-seqset.lo imap-utf7.lo imap-util.lo
+libimap_la_OBJECTS = $(am_libimap_la_OBJECTS)
+AM_V_lt = $(am__v_lt_@AM_V@)
+am__v_lt_ = $(am__v_lt_@AM_DEFAULT_V@)
+am__v_lt_0 = --silent
+am__v_lt_1 =
+am__fuzz_imap_bodystructure_SOURCES_DIST = fuzz-imap-bodystructure.c
+@USE_FUZZER_TRUE@am_fuzz_imap_bodystructure_OBJECTS = fuzz_imap_bodystructure-fuzz-imap-bodystructure.$(OBJEXT)
+fuzz_imap_bodystructure_OBJECTS = \
+ $(am_fuzz_imap_bodystructure_OBJECTS)
+fuzz_imap_bodystructure_LINK = $(LIBTOOL) $(AM_V_lt) --tag=CXX \
+ $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=link $(CXXLD) \
+ $(AM_CXXFLAGS) $(CXXFLAGS) $(fuzz_imap_bodystructure_LDFLAGS) \
+ $(LDFLAGS) -o $@
+am__fuzz_imap_utf7_SOURCES_DIST = fuzz-imap-utf7.c
+@USE_FUZZER_TRUE@am_fuzz_imap_utf7_OBJECTS = \
+@USE_FUZZER_TRUE@ fuzz_imap_utf7-fuzz-imap-utf7.$(OBJEXT)
+fuzz_imap_utf7_OBJECTS = $(am_fuzz_imap_utf7_OBJECTS)
+fuzz_imap_utf7_LINK = $(LIBTOOL) $(AM_V_lt) --tag=CXX \
+ $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=link $(CXXLD) \
+ $(AM_CXXFLAGS) $(CXXFLAGS) $(fuzz_imap_utf7_LDFLAGS) \
+ $(LDFLAGS) -o $@
+am_test_imap_bodystructure_OBJECTS = \
+ test-imap-bodystructure.$(OBJEXT)
+test_imap_bodystructure_OBJECTS = \
+ $(am_test_imap_bodystructure_OBJECTS)
+am_test_imap_envelope_OBJECTS = test-imap-envelope.$(OBJEXT)
+test_imap_envelope_OBJECTS = $(am_test_imap_envelope_OBJECTS)
+am_test_imap_match_OBJECTS = test-imap-match.$(OBJEXT)
+test_imap_match_OBJECTS = $(am_test_imap_match_OBJECTS)
+am_test_imap_parser_OBJECTS = test-imap-parser.$(OBJEXT)
+test_imap_parser_OBJECTS = $(am_test_imap_parser_OBJECTS)
+am_test_imap_quote_OBJECTS = test-imap-quote.$(OBJEXT)
+test_imap_quote_OBJECTS = $(am_test_imap_quote_OBJECTS)
+am_test_imap_url_OBJECTS = test-imap-url.$(OBJEXT)
+test_imap_url_OBJECTS = $(am_test_imap_url_OBJECTS)
+am_test_imap_utf7_OBJECTS = test-imap-utf7.$(OBJEXT)
+test_imap_utf7_OBJECTS = $(am_test_imap_utf7_OBJECTS)
+am_test_imap_util_OBJECTS = test-imap-util.$(OBJEXT)
+test_imap_util_OBJECTS = $(am_test_imap_util_OBJECTS)
+AM_V_P = $(am__v_P_@AM_V@)
+am__v_P_ = $(am__v_P_@AM_DEFAULT_V@)
+am__v_P_0 = false
+am__v_P_1 = :
+AM_V_GEN = $(am__v_GEN_@AM_V@)
+am__v_GEN_ = $(am__v_GEN_@AM_DEFAULT_V@)
+am__v_GEN_0 = @echo " GEN " $@;
+am__v_GEN_1 =
+AM_V_at = $(am__v_at_@AM_V@)
+am__v_at_ = $(am__v_at_@AM_DEFAULT_V@)
+am__v_at_0 = @
+am__v_at_1 =
+DEFAULT_INCLUDES = -I.@am__isrc@ -I$(top_builddir)
+depcomp = $(SHELL) $(top_srcdir)/depcomp
+am__maybe_remake_depfiles = depfiles
+am__depfiles_remade = \
+ ./$(DEPDIR)/fuzz_imap_bodystructure-force-cxx-linking.Po \
+ ./$(DEPDIR)/fuzz_imap_bodystructure-fuzz-imap-bodystructure.Po \
+ ./$(DEPDIR)/fuzz_imap_utf7-force-cxx-linking.Po \
+ ./$(DEPDIR)/fuzz_imap_utf7-fuzz-imap-utf7.Po \
+ ./$(DEPDIR)/imap-arg.Plo ./$(DEPDIR)/imap-base-subject.Plo \
+ ./$(DEPDIR)/imap-bodystructure.Plo ./$(DEPDIR)/imap-date.Plo \
+ ./$(DEPDIR)/imap-envelope.Plo ./$(DEPDIR)/imap-id.Plo \
+ ./$(DEPDIR)/imap-keepalive.Plo ./$(DEPDIR)/imap-match.Plo \
+ ./$(DEPDIR)/imap-parser.Plo ./$(DEPDIR)/imap-quote.Plo \
+ ./$(DEPDIR)/imap-seqset.Plo ./$(DEPDIR)/imap-url.Plo \
+ ./$(DEPDIR)/imap-utf7.Plo ./$(DEPDIR)/imap-util.Plo \
+ ./$(DEPDIR)/test-imap-bodystructure.Po \
+ ./$(DEPDIR)/test-imap-envelope.Po \
+ ./$(DEPDIR)/test-imap-match.Po ./$(DEPDIR)/test-imap-parser.Po \
+ ./$(DEPDIR)/test-imap-quote.Po ./$(DEPDIR)/test-imap-url.Po \
+ ./$(DEPDIR)/test-imap-utf7.Po ./$(DEPDIR)/test-imap-util.Po
+am__mv = mv -f
+COMPILE = $(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) \
+ $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS)
+LTCOMPILE = $(LIBTOOL) $(AM_V_lt) --tag=CC $(AM_LIBTOOLFLAGS) \
+ $(LIBTOOLFLAGS) --mode=compile $(CC) $(DEFS) \
+ $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) $(CPPFLAGS) \
+ $(AM_CFLAGS) $(CFLAGS)
+AM_V_CC = $(am__v_CC_@AM_V@)
+am__v_CC_ = $(am__v_CC_@AM_DEFAULT_V@)
+am__v_CC_0 = @echo " CC " $@;
+am__v_CC_1 =
+CCLD = $(CC)
+LINK = $(LIBTOOL) $(AM_V_lt) --tag=CC $(AM_LIBTOOLFLAGS) \
+ $(LIBTOOLFLAGS) --mode=link $(CCLD) $(AM_CFLAGS) $(CFLAGS) \
+ $(AM_LDFLAGS) $(LDFLAGS) -o $@
+AM_V_CCLD = $(am__v_CCLD_@AM_V@)
+am__v_CCLD_ = $(am__v_CCLD_@AM_DEFAULT_V@)
+am__v_CCLD_0 = @echo " CCLD " $@;
+am__v_CCLD_1 =
+CXXCOMPILE = $(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) \
+ $(AM_CPPFLAGS) $(CPPFLAGS) $(AM_CXXFLAGS) $(CXXFLAGS)
+LTCXXCOMPILE = $(LIBTOOL) $(AM_V_lt) --tag=CXX $(AM_LIBTOOLFLAGS) \
+ $(LIBTOOLFLAGS) --mode=compile $(CXX) $(DEFS) \
+ $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) $(CPPFLAGS) \
+ $(AM_CXXFLAGS) $(CXXFLAGS)
+AM_V_CXX = $(am__v_CXX_@AM_V@)
+am__v_CXX_ = $(am__v_CXX_@AM_DEFAULT_V@)
+am__v_CXX_0 = @echo " CXX " $@;
+am__v_CXX_1 =
+CXXLD = $(CXX)
+CXXLINK = $(LIBTOOL) $(AM_V_lt) --tag=CXX $(AM_LIBTOOLFLAGS) \
+ $(LIBTOOLFLAGS) --mode=link $(CXXLD) $(AM_CXXFLAGS) \
+ $(CXXFLAGS) $(AM_LDFLAGS) $(LDFLAGS) -o $@
+AM_V_CXXLD = $(am__v_CXXLD_@AM_V@)
+am__v_CXXLD_ = $(am__v_CXXLD_@AM_DEFAULT_V@)
+am__v_CXXLD_0 = @echo " CXXLD " $@;
+am__v_CXXLD_1 =
+SOURCES = $(libimap_la_SOURCES) $(fuzz_imap_bodystructure_SOURCES) \
+ $(nodist_EXTRA_fuzz_imap_bodystructure_SOURCES) \
+ $(fuzz_imap_utf7_SOURCES) \
+ $(nodist_EXTRA_fuzz_imap_utf7_SOURCES) \
+ $(test_imap_bodystructure_SOURCES) \
+ $(test_imap_envelope_SOURCES) $(test_imap_match_SOURCES) \
+ $(test_imap_parser_SOURCES) $(test_imap_quote_SOURCES) \
+ $(test_imap_url_SOURCES) $(test_imap_utf7_SOURCES) \
+ $(test_imap_util_SOURCES)
+DIST_SOURCES = $(libimap_la_SOURCES) \
+ $(am__fuzz_imap_bodystructure_SOURCES_DIST) \
+ $(am__fuzz_imap_utf7_SOURCES_DIST) \
+ $(test_imap_bodystructure_SOURCES) \
+ $(test_imap_envelope_SOURCES) $(test_imap_match_SOURCES) \
+ $(test_imap_parser_SOURCES) $(test_imap_quote_SOURCES) \
+ $(test_imap_url_SOURCES) $(test_imap_utf7_SOURCES) \
+ $(test_imap_util_SOURCES)
+am__can_run_installinfo = \
+ case $$AM_UPDATE_INFO_DIR in \
+ n|no|NO) false;; \
+ *) (install-info --version) >/dev/null 2>&1;; \
+ esac
+am__vpath_adj_setup = srcdirstrip=`echo "$(srcdir)" | sed 's|.|.|g'`;
+am__vpath_adj = case $$p in \
+ $(srcdir)/*) f=`echo "$$p" | sed "s|^$$srcdirstrip/||"`;; \
+ *) f=$$p;; \
+ esac;
+am__strip_dir = f=`echo $$p | sed -e 's|^.*/||'`;
+am__install_max = 40
+am__nobase_strip_setup = \
+ srcdirstrip=`echo "$(srcdir)" | sed 's/[].[^$$\\*|]/\\\\&/g'`
+am__nobase_strip = \
+ for p in $$list; do echo "$$p"; done | sed -e "s|$$srcdirstrip/||"
+am__nobase_list = $(am__nobase_strip_setup); \
+ for p in $$list; do echo "$$p $$p"; done | \
+ sed "s| $$srcdirstrip/| |;"' / .*\//!s/ .*/ ./; s,\( .*\)/[^/]*$$,\1,' | \
+ $(AWK) 'BEGIN { files["."] = "" } { files[$$2] = files[$$2] " " $$1; \
+ if (++n[$$2] == $(am__install_max)) \
+ { print $$2, files[$$2]; n[$$2] = 0; files[$$2] = "" } } \
+ END { for (dir in files) print dir, files[dir] }'
+am__base_list = \
+ sed '$$!N;$$!N;$$!N;$$!N;$$!N;$$!N;$$!N;s/\n/ /g' | \
+ sed '$$!N;$$!N;$$!N;$$!N;s/\n/ /g'
+am__uninstall_files_from_dir = { \
+ test -z "$$files" \
+ || { test ! -d "$$dir" && test ! -f "$$dir" && test ! -r "$$dir"; } \
+ || { echo " ( cd '$$dir' && rm -f" $$files ")"; \
+ $(am__cd) "$$dir" && rm -f $$files; }; \
+ }
+am__installdirs = "$(DESTDIR)$(pkginc_libdir)"
+HEADERS = $(pkginc_lib_HEADERS)
+am__tagged_files = $(HEADERS) $(SOURCES) $(TAGS_FILES) $(LISP)
+# Read a list of newline-separated strings from the standard input,
+# and print each of them once, without duplicates. Input order is
+# *not* preserved.
+am__uniquify_input = $(AWK) '\
+ BEGIN { nonempty = 0; } \
+ { items[$$0] = 1; nonempty = 1; } \
+ END { if (nonempty) { for (i in items) print i; }; } \
+'
+# Make sure the list of sources is unique. This is necessary because,
+# e.g., the same source file might be shared among _SOURCES variables
+# for different programs/libraries.
+am__define_uniq_tagged_files = \
+ list='$(am__tagged_files)'; \
+ unique=`for i in $$list; do \
+ if test -f "$$i"; then echo $$i; else echo $(srcdir)/$$i; fi; \
+ done | $(am__uniquify_input)`
+ETAGS = etags
+CTAGS = ctags
+am__DIST_COMMON = $(srcdir)/Makefile.in $(top_srcdir)/depcomp
+DISTFILES = $(DIST_COMMON) $(DIST_SOURCES) $(TEXINFOS) $(EXTRA_DIST)
+ACLOCAL = @ACLOCAL@
+ACLOCAL_AMFLAGS = @ACLOCAL_AMFLAGS@
+AMTAR = @AMTAR@
+AM_DEFAULT_VERBOSITY = @AM_DEFAULT_VERBOSITY@
+APPARMOR_LIBS = @APPARMOR_LIBS@
+AR = @AR@
+AUTH_CFLAGS = @AUTH_CFLAGS@
+AUTH_LIBS = @AUTH_LIBS@
+AUTOCONF = @AUTOCONF@
+AUTOHEADER = @AUTOHEADER@
+AUTOMAKE = @AUTOMAKE@
+AWK = @AWK@
+BINARY_CFLAGS = @BINARY_CFLAGS@
+BINARY_LDFLAGS = @BINARY_LDFLAGS@
+BISON = @BISON@
+CASSANDRA_CFLAGS = @CASSANDRA_CFLAGS@
+CASSANDRA_LIBS = @CASSANDRA_LIBS@
+CC = @CC@
+CCDEPMODE = @CCDEPMODE@
+CDB_LIBS = @CDB_LIBS@
+CFLAGS = @CFLAGS@
+CLUCENE_CFLAGS = @CLUCENE_CFLAGS@
+CLUCENE_LIBS = @CLUCENE_LIBS@
+COMPRESS_LIBS = @COMPRESS_LIBS@
+CPP = @CPP@
+CPPFLAGS = @CPPFLAGS@
+CRYPT_LIBS = @CRYPT_LIBS@
+CXX = @CXX@
+CXXCPP = @CXXCPP@
+CXXDEPMODE = @CXXDEPMODE@
+CXXFLAGS = @CXXFLAGS@
+CYGPATH_W = @CYGPATH_W@
+DEFS = @DEFS@
+DEPDIR = @DEPDIR@
+DICT_LIBS = @DICT_LIBS@
+DLLIB = @DLLIB@
+DLLTOOL = @DLLTOOL@
+DSYMUTIL = @DSYMUTIL@
+DUMPBIN = @DUMPBIN@
+ECHO_C = @ECHO_C@
+ECHO_N = @ECHO_N@
+ECHO_T = @ECHO_T@
+EGREP = @EGREP@
+EXEEXT = @EXEEXT@
+FGREP = @FGREP@
+FLEX = @FLEX@
+FUZZER_CPPFLAGS = @FUZZER_CPPFLAGS@
+FUZZER_LDFLAGS = @FUZZER_LDFLAGS@
+GREP = @GREP@
+INSTALL = @INSTALL@
+INSTALL_DATA = @INSTALL_DATA@
+INSTALL_PROGRAM = @INSTALL_PROGRAM@
+INSTALL_SCRIPT = @INSTALL_SCRIPT@
+INSTALL_STRIP_PROGRAM = @INSTALL_STRIP_PROGRAM@
+KRB5CONFIG = @KRB5CONFIG@
+KRB5_CFLAGS = @KRB5_CFLAGS@
+KRB5_LIBS = @KRB5_LIBS@
+LD = @LD@
+LDAP_LIBS = @LDAP_LIBS@
+LDFLAGS = @LDFLAGS@
+LD_NO_WHOLE_ARCHIVE = @LD_NO_WHOLE_ARCHIVE@
+LD_WHOLE_ARCHIVE = @LD_WHOLE_ARCHIVE@
+LIBCAP = @LIBCAP@
+LIBDOVECOT = @LIBDOVECOT@
+LIBDOVECOT_COMPRESS = @LIBDOVECOT_COMPRESS@
+LIBDOVECOT_DEPS = @LIBDOVECOT_DEPS@
+LIBDOVECOT_DSYNC = @LIBDOVECOT_DSYNC@
+LIBDOVECOT_LA_LIBS = @LIBDOVECOT_LA_LIBS@
+LIBDOVECOT_LDA = @LIBDOVECOT_LDA@
+LIBDOVECOT_LDAP = @LIBDOVECOT_LDAP@
+LIBDOVECOT_LIBFTS = @LIBDOVECOT_LIBFTS@
+LIBDOVECOT_LIBFTS_DEPS = @LIBDOVECOT_LIBFTS_DEPS@
+LIBDOVECOT_LOGIN = @LIBDOVECOT_LOGIN@
+LIBDOVECOT_LUA = @LIBDOVECOT_LUA@
+LIBDOVECOT_LUA_DEPS = @LIBDOVECOT_LUA_DEPS@
+LIBDOVECOT_SQL = @LIBDOVECOT_SQL@
+LIBDOVECOT_STORAGE = @LIBDOVECOT_STORAGE@
+LIBDOVECOT_STORAGE_DEPS = @LIBDOVECOT_STORAGE_DEPS@
+LIBEXTTEXTCAT_CFLAGS = @LIBEXTTEXTCAT_CFLAGS@
+LIBEXTTEXTCAT_LIBS = @LIBEXTTEXTCAT_LIBS@
+LIBICONV = @LIBICONV@
+LIBICU_CFLAGS = @LIBICU_CFLAGS@
+LIBICU_LIBS = @LIBICU_LIBS@
+LIBOBJS = @LIBOBJS@
+LIBS = @LIBS@
+LIBSODIUM_CFLAGS = @LIBSODIUM_CFLAGS@
+LIBSODIUM_LIBS = @LIBSODIUM_LIBS@
+LIBTIRPC_CFLAGS = @LIBTIRPC_CFLAGS@
+LIBTIRPC_LIBS = @LIBTIRPC_LIBS@
+LIBTOOL = @LIBTOOL@
+LIBUNWIND_CFLAGS = @LIBUNWIND_CFLAGS@
+LIBUNWIND_LIBS = @LIBUNWIND_LIBS@
+LIBWRAP_LIBS = @LIBWRAP_LIBS@
+LINKED_STORAGE_LDADD = @LINKED_STORAGE_LDADD@
+LIPO = @LIPO@
+LN_S = @LN_S@
+LTLIBICONV = @LTLIBICONV@
+LTLIBOBJS = @LTLIBOBJS@
+LT_SYS_LIBRARY_PATH = @LT_SYS_LIBRARY_PATH@
+LUA_CFLAGS = @LUA_CFLAGS@
+LUA_LIBS = @LUA_LIBS@
+MAINT = @MAINT@
+MAKEINFO = @MAKEINFO@
+MANIFEST_TOOL = @MANIFEST_TOOL@
+MKDIR_P = @MKDIR_P@
+MODULE_LIBS = @MODULE_LIBS@
+MODULE_SUFFIX = @MODULE_SUFFIX@
+MYSQL_CFLAGS = @MYSQL_CFLAGS@
+MYSQL_CONFIG = @MYSQL_CONFIG@
+MYSQL_LIBS = @MYSQL_LIBS@
+NM = @NM@
+NMEDIT = @NMEDIT@
+NOPLUGIN_LDFLAGS = @NOPLUGIN_LDFLAGS@
+OBJDUMP = @OBJDUMP@
+OBJEXT = @OBJEXT@
+OTOOL = @OTOOL@
+OTOOL64 = @OTOOL64@
+PACKAGE = @PACKAGE@
+PACKAGE_BUGREPORT = @PACKAGE_BUGREPORT@
+PACKAGE_NAME = @PACKAGE_NAME@
+PACKAGE_STRING = @PACKAGE_STRING@
+PACKAGE_TARNAME = @PACKAGE_TARNAME@
+PACKAGE_URL = @PACKAGE_URL@
+PACKAGE_VERSION = @PACKAGE_VERSION@
+PANDOC = @PANDOC@
+PATH_SEPARATOR = @PATH_SEPARATOR@
+PGSQL_CFLAGS = @PGSQL_CFLAGS@
+PGSQL_LIBS = @PGSQL_LIBS@
+PG_CONFIG = @PG_CONFIG@
+PIE_CFLAGS = @PIE_CFLAGS@
+PIE_LDFLAGS = @PIE_LDFLAGS@
+PKG_CONFIG = @PKG_CONFIG@
+PKG_CONFIG_LIBDIR = @PKG_CONFIG_LIBDIR@
+PKG_CONFIG_PATH = @PKG_CONFIG_PATH@
+QUOTA_LIBS = @QUOTA_LIBS@
+RANLIB = @RANLIB@
+RELRO_LDFLAGS = @RELRO_LDFLAGS@
+RPCGEN = @RPCGEN@
+RUN_TEST = @RUN_TEST@
+SED = @SED@
+SETTING_FILES = @SETTING_FILES@
+SET_MAKE = @SET_MAKE@
+SHELL = @SHELL@
+SQLITE_CFLAGS = @SQLITE_CFLAGS@
+SQLITE_LIBS = @SQLITE_LIBS@
+SQL_CFLAGS = @SQL_CFLAGS@
+SQL_LIBS = @SQL_LIBS@
+SSL_CFLAGS = @SSL_CFLAGS@
+SSL_LIBS = @SSL_LIBS@
+STRIP = @STRIP@
+SYSTEMD_CFLAGS = @SYSTEMD_CFLAGS@
+SYSTEMD_LIBS = @SYSTEMD_LIBS@
+VALGRIND = @VALGRIND@
+VERSION = @VERSION@
+ZSTD_CFLAGS = @ZSTD_CFLAGS@
+ZSTD_LIBS = @ZSTD_LIBS@
+abs_builddir = @abs_builddir@
+abs_srcdir = @abs_srcdir@
+abs_top_builddir = @abs_top_builddir@
+abs_top_srcdir = @abs_top_srcdir@
+ac_ct_AR = @ac_ct_AR@
+ac_ct_CC = @ac_ct_CC@
+ac_ct_CXX = @ac_ct_CXX@
+ac_ct_DUMPBIN = @ac_ct_DUMPBIN@
+am__include = @am__include@
+am__leading_dot = @am__leading_dot@
+am__quote = @am__quote@
+am__tar = @am__tar@
+am__untar = @am__untar@
+bindir = @bindir@
+build = @build@
+build_alias = @build_alias@
+build_cpu = @build_cpu@
+build_os = @build_os@
+build_vendor = @build_vendor@
+builddir = @builddir@
+datadir = @datadir@
+datarootdir = @datarootdir@
+dict_drivers = @dict_drivers@
+docdir = @docdir@
+dvidir = @dvidir@
+exec_prefix = @exec_prefix@
+host = @host@
+host_alias = @host_alias@
+host_cpu = @host_cpu@
+host_os = @host_os@
+host_vendor = @host_vendor@
+htmldir = @htmldir@
+includedir = @includedir@
+infodir = @infodir@
+install_sh = @install_sh@
+libdir = @libdir@
+libexecdir = @libexecdir@
+localedir = @localedir@
+localstatedir = @localstatedir@
+mandir = @mandir@
+mkdir_p = @mkdir_p@
+moduledir = @moduledir@
+oldincludedir = @oldincludedir@
+pdfdir = @pdfdir@
+prefix = @prefix@
+program_transform_name = @program_transform_name@
+psdir = @psdir@
+rundir = @rundir@
+runstatedir = @runstatedir@
+sbindir = @sbindir@
+sharedstatedir = @sharedstatedir@
+sql_drivers = @sql_drivers@
+srcdir = @srcdir@
+ssldir = @ssldir@
+statedir = @statedir@
+sysconfdir = @sysconfdir@
+systemdservicetype = @systemdservicetype@
+systemdsystemunitdir = @systemdsystemunitdir@
+target_alias = @target_alias@
+top_build_prefix = @top_build_prefix@
+top_builddir = @top_builddir@
+top_srcdir = @top_srcdir@
+noinst_LTLIBRARIES = libimap.la
+AM_CPPFLAGS = \
+ -I$(top_srcdir)/src/lib \
+ -I$(top_srcdir)/src/lib-test \
+ -I$(top_srcdir)/src/lib-charset \
+ -I$(top_srcdir)/src/lib-mail
+
+libimap_la_SOURCES = \
+ imap-arg.c \
+ imap-base-subject.c \
+ imap-bodystructure.c \
+ imap-date.c \
+ imap-envelope.c \
+ imap-id.c \
+ imap-keepalive.c \
+ imap-match.c \
+ imap-parser.c \
+ imap-quote.c \
+ imap-url.c \
+ imap-seqset.c \
+ imap-utf7.c \
+ imap-util.c
+
+headers = \
+ imap-arg.h \
+ imap-base-subject.h \
+ imap-bodystructure.h \
+ imap-date.h \
+ imap-envelope.h \
+ imap-id.h \
+ imap-keepalive.h \
+ imap-match.h \
+ imap-parser.h \
+ imap-resp-code.h \
+ imap-quote.h \
+ imap-url.h \
+ imap-seqset.h \
+ imap-utf7.h \
+ imap-util.h
+
+pkginc_libdir = $(pkgincludedir)
+pkginc_lib_HEADERS = $(headers)
+test_programs = \
+ test-imap-bodystructure \
+ test-imap-envelope \
+ test-imap-match \
+ test-imap-parser \
+ test-imap-quote \
+ test-imap-url \
+ test-imap-utf7 \
+ test-imap-util
+
+test_libs = \
+ ../lib-test/libtest.la \
+ ../lib/liblib.la
+
+test_deps = $(noinst_LTLIBRARIES) $(test_libs)
+test_imap_bodystructure_SOURCES = test-imap-bodystructure.c
+test_imap_bodystructure_LDADD = imap-bodystructure.lo imap-envelope.lo imap-quote.lo imap-parser.lo imap-arg.lo ../lib-mail/libmail.la $(test_libs)
+test_imap_bodystructure_DEPENDENCIES = $(test_deps) ../lib-mail/libmail.la
+test_imap_envelope_SOURCES = test-imap-envelope.c
+test_imap_envelope_LDADD = imap-envelope.lo imap-quote.lo imap-parser.lo imap-arg.lo ../lib-mail/libmail.la $(test_libs)
+test_imap_envelope_DEPENDENCIES = $(test_deps) ../lib-mail/libmail.la
+test_imap_match_SOURCES = test-imap-match.c
+test_imap_match_LDADD = imap-match.lo $(test_libs)
+test_imap_match_DEPENDENCIES = $(test_deps)
+test_imap_parser_SOURCES = test-imap-parser.c
+test_imap_parser_LDADD = imap-parser.lo imap-arg.lo $(test_libs)
+test_imap_parser_DEPENDENCIES = $(test_deps)
+test_imap_quote_SOURCES = test-imap-quote.c
+test_imap_quote_LDADD = imap-quote.lo $(test_libs)
+test_imap_quote_DEPENDENCIES = $(test_deps)
+test_imap_url_SOURCES = test-imap-url.c
+test_imap_url_LDADD = imap-url.lo $(test_libs)
+test_imap_url_DEPENDENCIES = $(test_deps)
+test_imap_utf7_SOURCES = test-imap-utf7.c
+test_imap_utf7_LDADD = imap-utf7.lo $(test_libs)
+test_imap_utf7_DEPENDENCIES = $(test_deps)
+test_imap_util_SOURCES = test-imap-util.c
+test_imap_util_LDADD = imap-util.lo imap-arg.lo $(test_libs)
+test_imap_util_DEPENDENCIES = $(test_deps)
+@USE_FUZZER_TRUE@nodist_EXTRA_fuzz_imap_utf7_SOURCES = force-cxx-linking.cxx
+@USE_FUZZER_TRUE@fuzz_imap_utf7_SOURCES = fuzz-imap-utf7.c
+@USE_FUZZER_TRUE@fuzz_imap_utf7_CPPFLAGS = $(FUZZER_CPPFLAGS)
+@USE_FUZZER_TRUE@fuzz_imap_utf7_LDFLAGS = $(FUZZER_LDFLAGS)
+@USE_FUZZER_TRUE@fuzz_imap_utf7_LDADD = libimap.la $(test_libs)
+@USE_FUZZER_TRUE@fuzz_imap_utf7_DEPENDENCIES = libimap.la $(test_deps)
+@USE_FUZZER_TRUE@nodist_EXTRA_fuzz_imap_bodystructure_SOURCES = force-cxx-linking.cxx
+@USE_FUZZER_TRUE@fuzz_imap_bodystructure_SOURCES = fuzz-imap-bodystructure.c
+@USE_FUZZER_TRUE@fuzz_imap_bodystructure_CPPFLAGS = $(FUZZER_CPPFLAGS)
+@USE_FUZZER_TRUE@fuzz_imap_bodystructure_LDFLAGS = $(FUZZER_LDFLAGS)
+@USE_FUZZER_TRUE@fuzz_imap_bodystructure_LDADD = libimap.la $(test_libs)
+@USE_FUZZER_TRUE@fuzz_imap_bodystructure_DEPENDENCIES = libimap.la $(test_deps)
+all: all-am
+
+.SUFFIXES:
+.SUFFIXES: .c .cxx .lo .o .obj
+$(srcdir)/Makefile.in: @MAINTAINER_MODE_TRUE@ $(srcdir)/Makefile.am $(am__configure_deps)
+ @for dep in $?; do \
+ case '$(am__configure_deps)' in \
+ *$$dep*) \
+ ( cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh ) \
+ && { if test -f $@; then exit 0; else break; fi; }; \
+ exit 1;; \
+ esac; \
+ done; \
+ echo ' cd $(top_srcdir) && $(AUTOMAKE) --foreign src/lib-imap/Makefile'; \
+ $(am__cd) $(top_srcdir) && \
+ $(AUTOMAKE) --foreign src/lib-imap/Makefile
+Makefile: $(srcdir)/Makefile.in $(top_builddir)/config.status
+ @case '$?' in \
+ *config.status*) \
+ cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh;; \
+ *) \
+ echo ' cd $(top_builddir) && $(SHELL) ./config.status $(subdir)/$@ $(am__maybe_remake_depfiles)'; \
+ cd $(top_builddir) && $(SHELL) ./config.status $(subdir)/$@ $(am__maybe_remake_depfiles);; \
+ esac;
+
+$(top_builddir)/config.status: $(top_srcdir)/configure $(CONFIG_STATUS_DEPENDENCIES)
+ cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh
+
+$(top_srcdir)/configure: @MAINTAINER_MODE_TRUE@ $(am__configure_deps)
+ cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh
+$(ACLOCAL_M4): @MAINTAINER_MODE_TRUE@ $(am__aclocal_m4_deps)
+ cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh
+$(am__aclocal_m4_deps):
+
+clean-noinstPROGRAMS:
+ @list='$(noinst_PROGRAMS)'; test -n "$$list" || exit 0; \
+ echo " rm -f" $$list; \
+ rm -f $$list || exit $$?; \
+ test -n "$(EXEEXT)" || exit 0; \
+ list=`for p in $$list; do echo "$$p"; done | sed 's/$(EXEEXT)$$//'`; \
+ echo " rm -f" $$list; \
+ rm -f $$list
+
+clean-noinstLTLIBRARIES:
+ -test -z "$(noinst_LTLIBRARIES)" || rm -f $(noinst_LTLIBRARIES)
+ @list='$(noinst_LTLIBRARIES)'; \
+ locs=`for p in $$list; do echo $$p; done | \
+ sed 's|^[^/]*$$|.|; s|/[^/]*$$||; s|$$|/so_locations|' | \
+ sort -u`; \
+ test -z "$$locs" || { \
+ echo rm -f $${locs}; \
+ rm -f $${locs}; \
+ }
+
+libimap.la: $(libimap_la_OBJECTS) $(libimap_la_DEPENDENCIES) $(EXTRA_libimap_la_DEPENDENCIES)
+ $(AM_V_CCLD)$(LINK) $(libimap_la_OBJECTS) $(libimap_la_LIBADD) $(LIBS)
+
+fuzz-imap-bodystructure$(EXEEXT): $(fuzz_imap_bodystructure_OBJECTS) $(fuzz_imap_bodystructure_DEPENDENCIES) $(EXTRA_fuzz_imap_bodystructure_DEPENDENCIES)
+ @rm -f fuzz-imap-bodystructure$(EXEEXT)
+ $(AM_V_CXXLD)$(fuzz_imap_bodystructure_LINK) $(fuzz_imap_bodystructure_OBJECTS) $(fuzz_imap_bodystructure_LDADD) $(LIBS)
+
+fuzz-imap-utf7$(EXEEXT): $(fuzz_imap_utf7_OBJECTS) $(fuzz_imap_utf7_DEPENDENCIES) $(EXTRA_fuzz_imap_utf7_DEPENDENCIES)
+ @rm -f fuzz-imap-utf7$(EXEEXT)
+ $(AM_V_CXXLD)$(fuzz_imap_utf7_LINK) $(fuzz_imap_utf7_OBJECTS) $(fuzz_imap_utf7_LDADD) $(LIBS)
+
+test-imap-bodystructure$(EXEEXT): $(test_imap_bodystructure_OBJECTS) $(test_imap_bodystructure_DEPENDENCIES) $(EXTRA_test_imap_bodystructure_DEPENDENCIES)
+ @rm -f test-imap-bodystructure$(EXEEXT)
+ $(AM_V_CCLD)$(LINK) $(test_imap_bodystructure_OBJECTS) $(test_imap_bodystructure_LDADD) $(LIBS)
+
+test-imap-envelope$(EXEEXT): $(test_imap_envelope_OBJECTS) $(test_imap_envelope_DEPENDENCIES) $(EXTRA_test_imap_envelope_DEPENDENCIES)
+ @rm -f test-imap-envelope$(EXEEXT)
+ $(AM_V_CCLD)$(LINK) $(test_imap_envelope_OBJECTS) $(test_imap_envelope_LDADD) $(LIBS)
+
+test-imap-match$(EXEEXT): $(test_imap_match_OBJECTS) $(test_imap_match_DEPENDENCIES) $(EXTRA_test_imap_match_DEPENDENCIES)
+ @rm -f test-imap-match$(EXEEXT)
+ $(AM_V_CCLD)$(LINK) $(test_imap_match_OBJECTS) $(test_imap_match_LDADD) $(LIBS)
+
+test-imap-parser$(EXEEXT): $(test_imap_parser_OBJECTS) $(test_imap_parser_DEPENDENCIES) $(EXTRA_test_imap_parser_DEPENDENCIES)
+ @rm -f test-imap-parser$(EXEEXT)
+ $(AM_V_CCLD)$(LINK) $(test_imap_parser_OBJECTS) $(test_imap_parser_LDADD) $(LIBS)
+
+test-imap-quote$(EXEEXT): $(test_imap_quote_OBJECTS) $(test_imap_quote_DEPENDENCIES) $(EXTRA_test_imap_quote_DEPENDENCIES)
+ @rm -f test-imap-quote$(EXEEXT)
+ $(AM_V_CCLD)$(LINK) $(test_imap_quote_OBJECTS) $(test_imap_quote_LDADD) $(LIBS)
+
+test-imap-url$(EXEEXT): $(test_imap_url_OBJECTS) $(test_imap_url_DEPENDENCIES) $(EXTRA_test_imap_url_DEPENDENCIES)
+ @rm -f test-imap-url$(EXEEXT)
+ $(AM_V_CCLD)$(LINK) $(test_imap_url_OBJECTS) $(test_imap_url_LDADD) $(LIBS)
+
+test-imap-utf7$(EXEEXT): $(test_imap_utf7_OBJECTS) $(test_imap_utf7_DEPENDENCIES) $(EXTRA_test_imap_utf7_DEPENDENCIES)
+ @rm -f test-imap-utf7$(EXEEXT)
+ $(AM_V_CCLD)$(LINK) $(test_imap_utf7_OBJECTS) $(test_imap_utf7_LDADD) $(LIBS)
+
+test-imap-util$(EXEEXT): $(test_imap_util_OBJECTS) $(test_imap_util_DEPENDENCIES) $(EXTRA_test_imap_util_DEPENDENCIES)
+ @rm -f test-imap-util$(EXEEXT)
+ $(AM_V_CCLD)$(LINK) $(test_imap_util_OBJECTS) $(test_imap_util_LDADD) $(LIBS)
+
+mostlyclean-compile:
+ -rm -f *.$(OBJEXT)
+
+distclean-compile:
+ -rm -f *.tab.c
+
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/fuzz_imap_bodystructure-force-cxx-linking.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/fuzz_imap_bodystructure-fuzz-imap-bodystructure.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/fuzz_imap_utf7-force-cxx-linking.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/fuzz_imap_utf7-fuzz-imap-utf7.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/imap-arg.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/imap-base-subject.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/imap-bodystructure.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/imap-date.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/imap-envelope.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/imap-id.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/imap-keepalive.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/imap-match.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/imap-parser.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/imap-quote.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/imap-seqset.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/imap-url.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/imap-utf7.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/imap-util.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/test-imap-bodystructure.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/test-imap-envelope.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/test-imap-match.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/test-imap-parser.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/test-imap-quote.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/test-imap-url.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/test-imap-utf7.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/test-imap-util.Po@am__quote@ # am--include-marker
+
+$(am__depfiles_remade):
+ @$(MKDIR_P) $(@D)
+ @echo '# dummy' >$@-t && $(am__mv) $@-t $@
+
+am--depfiles: $(am__depfiles_remade)
+
+.c.o:
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(COMPILE) -MT $@ -MD -MP -MF $(DEPDIR)/$*.Tpo -c -o $@ $<
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/$*.Tpo $(DEPDIR)/$*.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='$<' object='$@' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(COMPILE) -c -o $@ $<
+
+.c.obj:
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(COMPILE) -MT $@ -MD -MP -MF $(DEPDIR)/$*.Tpo -c -o $@ `$(CYGPATH_W) '$<'`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/$*.Tpo $(DEPDIR)/$*.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='$<' object='$@' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(COMPILE) -c -o $@ `$(CYGPATH_W) '$<'`
+
+.c.lo:
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(LTCOMPILE) -MT $@ -MD -MP -MF $(DEPDIR)/$*.Tpo -c -o $@ $<
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/$*.Tpo $(DEPDIR)/$*.Plo
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='$<' object='$@' libtool=yes @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(LTCOMPILE) -c -o $@ $<
+
+fuzz_imap_bodystructure-fuzz-imap-bodystructure.o: fuzz-imap-bodystructure.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(fuzz_imap_bodystructure_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT fuzz_imap_bodystructure-fuzz-imap-bodystructure.o -MD -MP -MF $(DEPDIR)/fuzz_imap_bodystructure-fuzz-imap-bodystructure.Tpo -c -o fuzz_imap_bodystructure-fuzz-imap-bodystructure.o `test -f 'fuzz-imap-bodystructure.c' || echo '$(srcdir)/'`fuzz-imap-bodystructure.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/fuzz_imap_bodystructure-fuzz-imap-bodystructure.Tpo $(DEPDIR)/fuzz_imap_bodystructure-fuzz-imap-bodystructure.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='fuzz-imap-bodystructure.c' object='fuzz_imap_bodystructure-fuzz-imap-bodystructure.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(fuzz_imap_bodystructure_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o fuzz_imap_bodystructure-fuzz-imap-bodystructure.o `test -f 'fuzz-imap-bodystructure.c' || echo '$(srcdir)/'`fuzz-imap-bodystructure.c
+
+fuzz_imap_bodystructure-fuzz-imap-bodystructure.obj: fuzz-imap-bodystructure.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(fuzz_imap_bodystructure_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT fuzz_imap_bodystructure-fuzz-imap-bodystructure.obj -MD -MP -MF $(DEPDIR)/fuzz_imap_bodystructure-fuzz-imap-bodystructure.Tpo -c -o fuzz_imap_bodystructure-fuzz-imap-bodystructure.obj `if test -f 'fuzz-imap-bodystructure.c'; then $(CYGPATH_W) 'fuzz-imap-bodystructure.c'; else $(CYGPATH_W) '$(srcdir)/fuzz-imap-bodystructure.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/fuzz_imap_bodystructure-fuzz-imap-bodystructure.Tpo $(DEPDIR)/fuzz_imap_bodystructure-fuzz-imap-bodystructure.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='fuzz-imap-bodystructure.c' object='fuzz_imap_bodystructure-fuzz-imap-bodystructure.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(fuzz_imap_bodystructure_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o fuzz_imap_bodystructure-fuzz-imap-bodystructure.obj `if test -f 'fuzz-imap-bodystructure.c'; then $(CYGPATH_W) 'fuzz-imap-bodystructure.c'; else $(CYGPATH_W) '$(srcdir)/fuzz-imap-bodystructure.c'; fi`
+
+fuzz_imap_utf7-fuzz-imap-utf7.o: fuzz-imap-utf7.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(fuzz_imap_utf7_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT fuzz_imap_utf7-fuzz-imap-utf7.o -MD -MP -MF $(DEPDIR)/fuzz_imap_utf7-fuzz-imap-utf7.Tpo -c -o fuzz_imap_utf7-fuzz-imap-utf7.o `test -f 'fuzz-imap-utf7.c' || echo '$(srcdir)/'`fuzz-imap-utf7.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/fuzz_imap_utf7-fuzz-imap-utf7.Tpo $(DEPDIR)/fuzz_imap_utf7-fuzz-imap-utf7.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='fuzz-imap-utf7.c' object='fuzz_imap_utf7-fuzz-imap-utf7.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(fuzz_imap_utf7_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o fuzz_imap_utf7-fuzz-imap-utf7.o `test -f 'fuzz-imap-utf7.c' || echo '$(srcdir)/'`fuzz-imap-utf7.c
+
+fuzz_imap_utf7-fuzz-imap-utf7.obj: fuzz-imap-utf7.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(fuzz_imap_utf7_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT fuzz_imap_utf7-fuzz-imap-utf7.obj -MD -MP -MF $(DEPDIR)/fuzz_imap_utf7-fuzz-imap-utf7.Tpo -c -o fuzz_imap_utf7-fuzz-imap-utf7.obj `if test -f 'fuzz-imap-utf7.c'; then $(CYGPATH_W) 'fuzz-imap-utf7.c'; else $(CYGPATH_W) '$(srcdir)/fuzz-imap-utf7.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/fuzz_imap_utf7-fuzz-imap-utf7.Tpo $(DEPDIR)/fuzz_imap_utf7-fuzz-imap-utf7.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='fuzz-imap-utf7.c' object='fuzz_imap_utf7-fuzz-imap-utf7.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(fuzz_imap_utf7_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o fuzz_imap_utf7-fuzz-imap-utf7.obj `if test -f 'fuzz-imap-utf7.c'; then $(CYGPATH_W) 'fuzz-imap-utf7.c'; else $(CYGPATH_W) '$(srcdir)/fuzz-imap-utf7.c'; fi`
+
+.cxx.o:
+@am__fastdepCXX_TRUE@ $(AM_V_CXX)$(CXXCOMPILE) -MT $@ -MD -MP -MF $(DEPDIR)/$*.Tpo -c -o $@ $<
+@am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/$*.Tpo $(DEPDIR)/$*.Po
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ $(AM_V_CXX)source='$<' object='$@' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(CXXCOMPILE) -c -o $@ $<
+
+.cxx.obj:
+@am__fastdepCXX_TRUE@ $(AM_V_CXX)$(CXXCOMPILE) -MT $@ -MD -MP -MF $(DEPDIR)/$*.Tpo -c -o $@ `$(CYGPATH_W) '$<'`
+@am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/$*.Tpo $(DEPDIR)/$*.Po
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ $(AM_V_CXX)source='$<' object='$@' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(CXXCOMPILE) -c -o $@ `$(CYGPATH_W) '$<'`
+
+.cxx.lo:
+@am__fastdepCXX_TRUE@ $(AM_V_CXX)$(LTCXXCOMPILE) -MT $@ -MD -MP -MF $(DEPDIR)/$*.Tpo -c -o $@ $<
+@am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/$*.Tpo $(DEPDIR)/$*.Plo
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ $(AM_V_CXX)source='$<' object='$@' libtool=yes @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(LTCXXCOMPILE) -c -o $@ $<
+
+fuzz_imap_bodystructure-force-cxx-linking.o: force-cxx-linking.cxx
+@am__fastdepCXX_TRUE@ $(AM_V_CXX)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(fuzz_imap_bodystructure_CPPFLAGS) $(CPPFLAGS) $(AM_CXXFLAGS) $(CXXFLAGS) -MT fuzz_imap_bodystructure-force-cxx-linking.o -MD -MP -MF $(DEPDIR)/fuzz_imap_bodystructure-force-cxx-linking.Tpo -c -o fuzz_imap_bodystructure-force-cxx-linking.o `test -f 'force-cxx-linking.cxx' || echo '$(srcdir)/'`force-cxx-linking.cxx
+@am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/fuzz_imap_bodystructure-force-cxx-linking.Tpo $(DEPDIR)/fuzz_imap_bodystructure-force-cxx-linking.Po
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ $(AM_V_CXX)source='force-cxx-linking.cxx' object='fuzz_imap_bodystructure-force-cxx-linking.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(fuzz_imap_bodystructure_CPPFLAGS) $(CPPFLAGS) $(AM_CXXFLAGS) $(CXXFLAGS) -c -o fuzz_imap_bodystructure-force-cxx-linking.o `test -f 'force-cxx-linking.cxx' || echo '$(srcdir)/'`force-cxx-linking.cxx
+
+fuzz_imap_bodystructure-force-cxx-linking.obj: force-cxx-linking.cxx
+@am__fastdepCXX_TRUE@ $(AM_V_CXX)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(fuzz_imap_bodystructure_CPPFLAGS) $(CPPFLAGS) $(AM_CXXFLAGS) $(CXXFLAGS) -MT fuzz_imap_bodystructure-force-cxx-linking.obj -MD -MP -MF $(DEPDIR)/fuzz_imap_bodystructure-force-cxx-linking.Tpo -c -o fuzz_imap_bodystructure-force-cxx-linking.obj `if test -f 'force-cxx-linking.cxx'; then $(CYGPATH_W) 'force-cxx-linking.cxx'; else $(CYGPATH_W) '$(srcdir)/force-cxx-linking.cxx'; fi`
+@am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/fuzz_imap_bodystructure-force-cxx-linking.Tpo $(DEPDIR)/fuzz_imap_bodystructure-force-cxx-linking.Po
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ $(AM_V_CXX)source='force-cxx-linking.cxx' object='fuzz_imap_bodystructure-force-cxx-linking.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(fuzz_imap_bodystructure_CPPFLAGS) $(CPPFLAGS) $(AM_CXXFLAGS) $(CXXFLAGS) -c -o fuzz_imap_bodystructure-force-cxx-linking.obj `if test -f 'force-cxx-linking.cxx'; then $(CYGPATH_W) 'force-cxx-linking.cxx'; else $(CYGPATH_W) '$(srcdir)/force-cxx-linking.cxx'; fi`
+
+fuzz_imap_utf7-force-cxx-linking.o: force-cxx-linking.cxx
+@am__fastdepCXX_TRUE@ $(AM_V_CXX)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(fuzz_imap_utf7_CPPFLAGS) $(CPPFLAGS) $(AM_CXXFLAGS) $(CXXFLAGS) -MT fuzz_imap_utf7-force-cxx-linking.o -MD -MP -MF $(DEPDIR)/fuzz_imap_utf7-force-cxx-linking.Tpo -c -o fuzz_imap_utf7-force-cxx-linking.o `test -f 'force-cxx-linking.cxx' || echo '$(srcdir)/'`force-cxx-linking.cxx
+@am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/fuzz_imap_utf7-force-cxx-linking.Tpo $(DEPDIR)/fuzz_imap_utf7-force-cxx-linking.Po
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ $(AM_V_CXX)source='force-cxx-linking.cxx' object='fuzz_imap_utf7-force-cxx-linking.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(fuzz_imap_utf7_CPPFLAGS) $(CPPFLAGS) $(AM_CXXFLAGS) $(CXXFLAGS) -c -o fuzz_imap_utf7-force-cxx-linking.o `test -f 'force-cxx-linking.cxx' || echo '$(srcdir)/'`force-cxx-linking.cxx
+
+fuzz_imap_utf7-force-cxx-linking.obj: force-cxx-linking.cxx
+@am__fastdepCXX_TRUE@ $(AM_V_CXX)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(fuzz_imap_utf7_CPPFLAGS) $(CPPFLAGS) $(AM_CXXFLAGS) $(CXXFLAGS) -MT fuzz_imap_utf7-force-cxx-linking.obj -MD -MP -MF $(DEPDIR)/fuzz_imap_utf7-force-cxx-linking.Tpo -c -o fuzz_imap_utf7-force-cxx-linking.obj `if test -f 'force-cxx-linking.cxx'; then $(CYGPATH_W) 'force-cxx-linking.cxx'; else $(CYGPATH_W) '$(srcdir)/force-cxx-linking.cxx'; fi`
+@am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/fuzz_imap_utf7-force-cxx-linking.Tpo $(DEPDIR)/fuzz_imap_utf7-force-cxx-linking.Po
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ $(AM_V_CXX)source='force-cxx-linking.cxx' object='fuzz_imap_utf7-force-cxx-linking.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(fuzz_imap_utf7_CPPFLAGS) $(CPPFLAGS) $(AM_CXXFLAGS) $(CXXFLAGS) -c -o fuzz_imap_utf7-force-cxx-linking.obj `if test -f 'force-cxx-linking.cxx'; then $(CYGPATH_W) 'force-cxx-linking.cxx'; else $(CYGPATH_W) '$(srcdir)/force-cxx-linking.cxx'; fi`
+
+mostlyclean-libtool:
+ -rm -f *.lo
+
+clean-libtool:
+ -rm -rf .libs _libs
+install-pkginc_libHEADERS: $(pkginc_lib_HEADERS)
+ @$(NORMAL_INSTALL)
+ @list='$(pkginc_lib_HEADERS)'; test -n "$(pkginc_libdir)" || list=; \
+ if test -n "$$list"; then \
+ echo " $(MKDIR_P) '$(DESTDIR)$(pkginc_libdir)'"; \
+ $(MKDIR_P) "$(DESTDIR)$(pkginc_libdir)" || exit 1; \
+ fi; \
+ for p in $$list; do \
+ if test -f "$$p"; then d=; else d="$(srcdir)/"; fi; \
+ echo "$$d$$p"; \
+ done | $(am__base_list) | \
+ while read files; do \
+ echo " $(INSTALL_HEADER) $$files '$(DESTDIR)$(pkginc_libdir)'"; \
+ $(INSTALL_HEADER) $$files "$(DESTDIR)$(pkginc_libdir)" || exit $$?; \
+ done
+
+uninstall-pkginc_libHEADERS:
+ @$(NORMAL_UNINSTALL)
+ @list='$(pkginc_lib_HEADERS)'; test -n "$(pkginc_libdir)" || list=; \
+ files=`for p in $$list; do echo $$p; done | sed -e 's|^.*/||'`; \
+ dir='$(DESTDIR)$(pkginc_libdir)'; $(am__uninstall_files_from_dir)
+
+ID: $(am__tagged_files)
+ $(am__define_uniq_tagged_files); mkid -fID $$unique
+tags: tags-am
+TAGS: tags
+
+tags-am: $(TAGS_DEPENDENCIES) $(am__tagged_files)
+ set x; \
+ here=`pwd`; \
+ $(am__define_uniq_tagged_files); \
+ shift; \
+ if test -z "$(ETAGS_ARGS)$$*$$unique"; then :; else \
+ test -n "$$unique" || unique=$$empty_fix; \
+ if test $$# -gt 0; then \
+ $(ETAGS) $(ETAGSFLAGS) $(AM_ETAGSFLAGS) $(ETAGS_ARGS) \
+ "$$@" $$unique; \
+ else \
+ $(ETAGS) $(ETAGSFLAGS) $(AM_ETAGSFLAGS) $(ETAGS_ARGS) \
+ $$unique; \
+ fi; \
+ fi
+ctags: ctags-am
+
+CTAGS: ctags
+ctags-am: $(TAGS_DEPENDENCIES) $(am__tagged_files)
+ $(am__define_uniq_tagged_files); \
+ test -z "$(CTAGS_ARGS)$$unique" \
+ || $(CTAGS) $(CTAGSFLAGS) $(AM_CTAGSFLAGS) $(CTAGS_ARGS) \
+ $$unique
+
+GTAGS:
+ here=`$(am__cd) $(top_builddir) && pwd` \
+ && $(am__cd) $(top_srcdir) \
+ && gtags -i $(GTAGS_ARGS) "$$here"
+cscopelist: cscopelist-am
+
+cscopelist-am: $(am__tagged_files)
+ list='$(am__tagged_files)'; \
+ case "$(srcdir)" in \
+ [\\/]* | ?:[\\/]*) sdir="$(srcdir)" ;; \
+ *) sdir=$(subdir)/$(srcdir) ;; \
+ esac; \
+ for i in $$list; do \
+ if test -f "$$i"; then \
+ echo "$(subdir)/$$i"; \
+ else \
+ echo "$$sdir/$$i"; \
+ fi; \
+ done >> $(top_builddir)/cscope.files
+
+distclean-tags:
+ -rm -f TAGS ID GTAGS GRTAGS GSYMS GPATH tags
+
+distdir: $(BUILT_SOURCES)
+ $(MAKE) $(AM_MAKEFLAGS) distdir-am
+
+distdir-am: $(DISTFILES)
+ @srcdirstrip=`echo "$(srcdir)" | sed 's/[].[^$$\\*]/\\\\&/g'`; \
+ topsrcdirstrip=`echo "$(top_srcdir)" | sed 's/[].[^$$\\*]/\\\\&/g'`; \
+ list='$(DISTFILES)'; \
+ dist_files=`for file in $$list; do echo $$file; done | \
+ sed -e "s|^$$srcdirstrip/||;t" \
+ -e "s|^$$topsrcdirstrip/|$(top_builddir)/|;t"`; \
+ case $$dist_files in \
+ */*) $(MKDIR_P) `echo "$$dist_files" | \
+ sed '/\//!d;s|^|$(distdir)/|;s,/[^/]*$$,,' | \
+ sort -u` ;; \
+ esac; \
+ for file in $$dist_files; do \
+ if test -f $$file || test -d $$file; then d=.; else d=$(srcdir); fi; \
+ if test -d $$d/$$file; then \
+ dir=`echo "/$$file" | sed -e 's,/[^/]*$$,,'`; \
+ if test -d "$(distdir)/$$file"; then \
+ find "$(distdir)/$$file" -type d ! -perm -700 -exec chmod u+rwx {} \;; \
+ fi; \
+ if test -d $(srcdir)/$$file && test $$d != $(srcdir); then \
+ cp -fpR $(srcdir)/$$file "$(distdir)$$dir" || exit 1; \
+ find "$(distdir)/$$file" -type d ! -perm -700 -exec chmod u+rwx {} \;; \
+ fi; \
+ cp -fpR $$d/$$file "$(distdir)$$dir" || exit 1; \
+ else \
+ test -f "$(distdir)/$$file" \
+ || cp -p $$d/$$file "$(distdir)/$$file" \
+ || exit 1; \
+ fi; \
+ done
+check-am: all-am
+ $(MAKE) $(AM_MAKEFLAGS) check-local
+check: check-am
+all-am: Makefile $(PROGRAMS) $(LTLIBRARIES) $(HEADERS)
+installdirs:
+ for dir in "$(DESTDIR)$(pkginc_libdir)"; do \
+ test -z "$$dir" || $(MKDIR_P) "$$dir"; \
+ done
+install: install-am
+install-exec: install-exec-am
+install-data: install-data-am
+uninstall: uninstall-am
+
+install-am: all-am
+ @$(MAKE) $(AM_MAKEFLAGS) install-exec-am install-data-am
+
+installcheck: installcheck-am
+install-strip:
+ if test -z '$(STRIP)'; then \
+ $(MAKE) $(AM_MAKEFLAGS) INSTALL_PROGRAM="$(INSTALL_STRIP_PROGRAM)" \
+ install_sh_PROGRAM="$(INSTALL_STRIP_PROGRAM)" INSTALL_STRIP_FLAG=-s \
+ install; \
+ else \
+ $(MAKE) $(AM_MAKEFLAGS) INSTALL_PROGRAM="$(INSTALL_STRIP_PROGRAM)" \
+ install_sh_PROGRAM="$(INSTALL_STRIP_PROGRAM)" INSTALL_STRIP_FLAG=-s \
+ "INSTALL_PROGRAM_ENV=STRIPPROG='$(STRIP)'" install; \
+ fi
+mostlyclean-generic:
+
+clean-generic:
+
+distclean-generic:
+ -test -z "$(CONFIG_CLEAN_FILES)" || rm -f $(CONFIG_CLEAN_FILES)
+ -test . = "$(srcdir)" || test -z "$(CONFIG_CLEAN_VPATH_FILES)" || rm -f $(CONFIG_CLEAN_VPATH_FILES)
+
+maintainer-clean-generic:
+ @echo "This command is intended for maintainers to use"
+ @echo "it deletes files that may require special tools to rebuild."
+clean: clean-am
+
+clean-am: clean-generic clean-libtool clean-noinstLTLIBRARIES \
+ clean-noinstPROGRAMS mostlyclean-am
+
+distclean: distclean-am
+ -rm -f ./$(DEPDIR)/fuzz_imap_bodystructure-force-cxx-linking.Po
+ -rm -f ./$(DEPDIR)/fuzz_imap_bodystructure-fuzz-imap-bodystructure.Po
+ -rm -f ./$(DEPDIR)/fuzz_imap_utf7-force-cxx-linking.Po
+ -rm -f ./$(DEPDIR)/fuzz_imap_utf7-fuzz-imap-utf7.Po
+ -rm -f ./$(DEPDIR)/imap-arg.Plo
+ -rm -f ./$(DEPDIR)/imap-base-subject.Plo
+ -rm -f ./$(DEPDIR)/imap-bodystructure.Plo
+ -rm -f ./$(DEPDIR)/imap-date.Plo
+ -rm -f ./$(DEPDIR)/imap-envelope.Plo
+ -rm -f ./$(DEPDIR)/imap-id.Plo
+ -rm -f ./$(DEPDIR)/imap-keepalive.Plo
+ -rm -f ./$(DEPDIR)/imap-match.Plo
+ -rm -f ./$(DEPDIR)/imap-parser.Plo
+ -rm -f ./$(DEPDIR)/imap-quote.Plo
+ -rm -f ./$(DEPDIR)/imap-seqset.Plo
+ -rm -f ./$(DEPDIR)/imap-url.Plo
+ -rm -f ./$(DEPDIR)/imap-utf7.Plo
+ -rm -f ./$(DEPDIR)/imap-util.Plo
+ -rm -f ./$(DEPDIR)/test-imap-bodystructure.Po
+ -rm -f ./$(DEPDIR)/test-imap-envelope.Po
+ -rm -f ./$(DEPDIR)/test-imap-match.Po
+ -rm -f ./$(DEPDIR)/test-imap-parser.Po
+ -rm -f ./$(DEPDIR)/test-imap-quote.Po
+ -rm -f ./$(DEPDIR)/test-imap-url.Po
+ -rm -f ./$(DEPDIR)/test-imap-utf7.Po
+ -rm -f ./$(DEPDIR)/test-imap-util.Po
+ -rm -f Makefile
+distclean-am: clean-am distclean-compile distclean-generic \
+ distclean-tags
+
+dvi: dvi-am
+
+dvi-am:
+
+html: html-am
+
+html-am:
+
+info: info-am
+
+info-am:
+
+install-data-am: install-pkginc_libHEADERS
+
+install-dvi: install-dvi-am
+
+install-dvi-am:
+
+install-exec-am:
+
+install-html: install-html-am
+
+install-html-am:
+
+install-info: install-info-am
+
+install-info-am:
+
+install-man:
+
+install-pdf: install-pdf-am
+
+install-pdf-am:
+
+install-ps: install-ps-am
+
+install-ps-am:
+
+installcheck-am:
+
+maintainer-clean: maintainer-clean-am
+ -rm -f ./$(DEPDIR)/fuzz_imap_bodystructure-force-cxx-linking.Po
+ -rm -f ./$(DEPDIR)/fuzz_imap_bodystructure-fuzz-imap-bodystructure.Po
+ -rm -f ./$(DEPDIR)/fuzz_imap_utf7-force-cxx-linking.Po
+ -rm -f ./$(DEPDIR)/fuzz_imap_utf7-fuzz-imap-utf7.Po
+ -rm -f ./$(DEPDIR)/imap-arg.Plo
+ -rm -f ./$(DEPDIR)/imap-base-subject.Plo
+ -rm -f ./$(DEPDIR)/imap-bodystructure.Plo
+ -rm -f ./$(DEPDIR)/imap-date.Plo
+ -rm -f ./$(DEPDIR)/imap-envelope.Plo
+ -rm -f ./$(DEPDIR)/imap-id.Plo
+ -rm -f ./$(DEPDIR)/imap-keepalive.Plo
+ -rm -f ./$(DEPDIR)/imap-match.Plo
+ -rm -f ./$(DEPDIR)/imap-parser.Plo
+ -rm -f ./$(DEPDIR)/imap-quote.Plo
+ -rm -f ./$(DEPDIR)/imap-seqset.Plo
+ -rm -f ./$(DEPDIR)/imap-url.Plo
+ -rm -f ./$(DEPDIR)/imap-utf7.Plo
+ -rm -f ./$(DEPDIR)/imap-util.Plo
+ -rm -f ./$(DEPDIR)/test-imap-bodystructure.Po
+ -rm -f ./$(DEPDIR)/test-imap-envelope.Po
+ -rm -f ./$(DEPDIR)/test-imap-match.Po
+ -rm -f ./$(DEPDIR)/test-imap-parser.Po
+ -rm -f ./$(DEPDIR)/test-imap-quote.Po
+ -rm -f ./$(DEPDIR)/test-imap-url.Po
+ -rm -f ./$(DEPDIR)/test-imap-utf7.Po
+ -rm -f ./$(DEPDIR)/test-imap-util.Po
+ -rm -f Makefile
+maintainer-clean-am: distclean-am maintainer-clean-generic
+
+mostlyclean: mostlyclean-am
+
+mostlyclean-am: mostlyclean-compile mostlyclean-generic \
+ mostlyclean-libtool
+
+pdf: pdf-am
+
+pdf-am:
+
+ps: ps-am
+
+ps-am:
+
+uninstall-am: uninstall-pkginc_libHEADERS
+
+.MAKE: check-am install-am install-strip
+
+.PHONY: CTAGS GTAGS TAGS all all-am am--depfiles check check-am \
+ check-local clean clean-generic clean-libtool \
+ clean-noinstLTLIBRARIES clean-noinstPROGRAMS cscopelist-am \
+ ctags ctags-am distclean distclean-compile distclean-generic \
+ distclean-libtool distclean-tags distdir dvi dvi-am html \
+ html-am info info-am install install-am install-data \
+ install-data-am install-dvi install-dvi-am install-exec \
+ install-exec-am install-html install-html-am install-info \
+ install-info-am install-man install-pdf install-pdf-am \
+ install-pkginc_libHEADERS install-ps install-ps-am \
+ install-strip installcheck installcheck-am installdirs \
+ maintainer-clean maintainer-clean-generic mostlyclean \
+ mostlyclean-compile mostlyclean-generic mostlyclean-libtool \
+ pdf pdf-am ps ps-am tags tags-am uninstall uninstall-am \
+ uninstall-pkginc_libHEADERS
+
+.PRECIOUS: Makefile
+
+
+check-local:
+ for bin in $(test_programs); do \
+ if ! $(RUN_TEST) ./$$bin; then exit 1; fi; \
+ done
+
+# Tell versions [3.59,3.63) of GNU make to not export all variables.
+# Otherwise a system limit (for SysV at least) may be exceeded.
+.NOEXPORT:
diff --git a/src/lib-imap/fuzz-imap-bodystructure.c b/src/lib-imap/fuzz-imap-bodystructure.c
new file mode 100644
index 0000000..a9d4499
--- /dev/null
+++ b/src/lib-imap/fuzz-imap-bodystructure.c
@@ -0,0 +1,52 @@
+/* Copyright (c) 2020 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "str.h"
+#include "fuzzer.h"
+#include "imap-bodystructure.c"
+#include <ctype.h>
+
+static const char *str_sanitize_binary(const char *input)
+{
+ string_t *dest = t_str_new(strlen(input));
+ for (;*input != '\0';input++) {
+ if (i_isprint(*input) == 0)
+ str_printfa(dest, "<%02x>", (unsigned char)*input);
+ else
+ str_append_c(dest, *input);
+ }
+ return str_c(dest);
+}
+
+FUZZ_BEGIN_STR(const char *str)
+{
+ pool_t pool =
+ pool_alloconly_create(MEMPOOL_GROWING"fuzz bodystructure", 1024);
+ struct message_part parts;
+ string_t *dest = str_new(pool, 32);
+ const char *error ATTR_UNUSED;
+ i_zero(&parts);
+
+ if (imap_bodystructure_parse(str, pool, &parts, &error) == 0) {
+ if (imap_bodystructure_write(&parts, dest, TRUE, &error) < 0)
+ i_panic("Failed to write bodystructure: %s", error);
+ /* The written bodystructure must be parseable *and*
+ it must come out exactly the same again */
+ if (imap_bodystructure_parse(str_c(dest), pool, &parts, &error) != 0) {
+ i_panic("Failed to reparse bodystructure '%s'",
+ str_sanitize_binary(str_c(dest)));
+ } else {
+ const char *new_str = t_strdup(str_c(dest));
+ str_truncate(dest, 0);
+ if (imap_bodystructure_write(&parts, dest, TRUE, &error) < 0)
+ i_panic("Failed to write reparsed bodystructure: %s", error);
+ if (strcmp(str_c(dest), new_str) != 0) {
+ i_panic("Parsed bodystructure '%s' does not match '%s'",
+ str_sanitize_binary(new_str),
+ str_sanitize_binary(str_c(dest)));
+ }
+ }
+ }
+ pool_unref(&pool);
+}
+FUZZ_END
diff --git a/src/lib-imap/fuzz-imap-utf7.c b/src/lib-imap/fuzz-imap-utf7.c
new file mode 100644
index 0000000..2a561f1
--- /dev/null
+++ b/src/lib-imap/fuzz-imap-utf7.c
@@ -0,0 +1,15 @@
+/* Copyright (c) 2020 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "str.h"
+#include "fuzzer.h"
+#include "imap-utf7.h"
+
+FUZZ_BEGIN_STR(const char *str)
+{
+ string_t *dest = t_str_new(32);
+
+ imap_utf8_to_utf7(str, dest);
+ imap_utf7_to_utf8(str, dest);
+}
+FUZZ_END
diff --git a/src/lib-imap/imap-arg.c b/src/lib-imap/imap-arg.c
new file mode 100644
index 0000000..0b947ce
--- /dev/null
+++ b/src/lib-imap/imap-arg.c
@@ -0,0 +1,137 @@
+/* Copyright (c) 2010-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "imap-arg.h"
+
+bool imap_arg_get_atom(const struct imap_arg *arg, const char **str_r)
+{
+ if (arg->type != IMAP_ARG_ATOM && arg->type != IMAP_ARG_NIL)
+ return FALSE;
+
+ *str_r = arg->_data.str;
+ return TRUE;
+}
+
+bool imap_arg_get_quoted(const struct imap_arg *arg, const char **str_r)
+{
+ if (arg->type != IMAP_ARG_STRING)
+ return FALSE;
+
+ *str_r = arg->_data.str;
+ return TRUE;
+}
+
+bool imap_arg_get_string(const struct imap_arg *arg, const char **str_r)
+{
+ if (arg->type != IMAP_ARG_STRING && arg->type != IMAP_ARG_LITERAL)
+ return FALSE;
+
+ *str_r = arg->_data.str;
+ return TRUE;
+}
+
+bool imap_arg_get_astring(const struct imap_arg *arg, const char **str_r)
+{
+ /* RFC 3501 4.5. specifies that NIL is the same as "NIL" when
+ reading astring. */
+ if (!IMAP_ARG_IS_ASTRING(arg) && arg->type != IMAP_ARG_NIL)
+ return FALSE;
+
+ *str_r = arg->_data.str;
+ return TRUE;
+}
+
+bool imap_arg_get_nstring(const struct imap_arg *arg, const char **str_r)
+{
+ if (arg->type == IMAP_ARG_NIL) {
+ *str_r = NULL;
+ return TRUE;
+ }
+ return imap_arg_get_string(arg, str_r);
+}
+
+bool imap_arg_get_literal_size(const struct imap_arg *arg, uoff_t *size_r)
+{
+ if (arg->type != IMAP_ARG_LITERAL_SIZE &&
+ arg->type != IMAP_ARG_LITERAL_SIZE_NONSYNC)
+ return FALSE;
+
+ *size_r = arg->_data.literal_size;
+ return TRUE;
+}
+
+bool imap_arg_get_list(const struct imap_arg *arg,
+ const struct imap_arg **list_r)
+{
+ unsigned int count;
+
+ return imap_arg_get_list_full(arg, list_r, &count);
+}
+
+bool imap_arg_get_list_full(const struct imap_arg *arg,
+ const struct imap_arg **list_r,
+ unsigned int *list_count_r)
+{
+ unsigned int count;
+
+ if (arg->type != IMAP_ARG_LIST)
+ return FALSE;
+
+ *list_r = array_get(&arg->_data.list, &count);
+
+ if (count > 0 && (*list_r)[count-1].type == IMAP_ARG_EOL)
+ count--;
+ else {
+ /* imap-parser stopped early (e.g. due to reading literal size).
+ The IMAP_ARG_EOL was added to the list only temporarily. */
+ i_assert((*list_r)[count].type == IMAP_ARG_EOL);
+ }
+ *list_count_r = count;
+ return TRUE;
+}
+
+const char *imap_arg_as_astring(const struct imap_arg *arg)
+{
+ const char *str;
+
+ if (!imap_arg_get_astring(arg, &str))
+ i_unreached();
+ return str;
+}
+
+const char *imap_arg_as_nstring(const struct imap_arg *arg)
+{
+ const char *str;
+
+ if (!imap_arg_get_nstring(arg, &str))
+ i_unreached();
+ return str;
+}
+
+uoff_t imap_arg_as_literal_size(const struct imap_arg *arg)
+{
+ uoff_t size;
+
+ if (!imap_arg_get_literal_size(arg, &size))
+ i_unreached();
+ return size;
+}
+
+const struct imap_arg *
+imap_arg_as_list(const struct imap_arg *arg)
+{
+ const struct imap_arg *ret;
+
+ if (!imap_arg_get_list(arg, &ret))
+ i_unreached();
+ return ret;
+}
+
+bool imap_arg_atom_equals(const struct imap_arg *arg, const char *str)
+{
+ const char *value;
+
+ if (!imap_arg_get_atom(arg, &value))
+ return FALSE;
+ return strcasecmp(value, str) == 0;
+}
diff --git a/src/lib-imap/imap-arg.h b/src/lib-imap/imap-arg.h
new file mode 100644
index 0000000..d55ee15
--- /dev/null
+++ b/src/lib-imap/imap-arg.h
@@ -0,0 +1,108 @@
+#ifndef IMAP_ARG_H
+#define IMAP_ARG_H
+
+#include "array.h"
+
+/* ABNF:
+
+ CHAR = %x01-7F
+ CTL = %x00-1F / %x7F
+ SP = %x20
+ DQUOTE = %x22 */
+
+/* ASTRING-CHAR = ATOM-CHAR / resp-specials */
+#define IS_ASTRING_CHAR(c) (IS_ATOM_CHAR(c) || IS_RESP_SPECIAL(c))
+/* ATOM-CHAR = <any CHAR except atom-specials> */
+#define IS_ATOM_CHAR(c) (!IS_ATOM_SPECIAL(c))
+/* atom-specials = "(" / ")" / "{" / SP / CTL / list-wildcards /
+ quoted-specials / resp-specials
+ Since atoms are only 7bit, we'll also optimize a bit by assuming 8bit chars
+ are also atom-specials. */
+#define IS_ATOM_SPECIAL(c) \
+ ((unsigned char)(c) <= 0x20 || (unsigned char)(c) >= 0x7f || \
+ (c) == '(' || (c) == ')' || (c) == '{' || IS_LIST_WILDCARD(c) || \
+ IS_QUOTED_SPECIAL(c) || IS_RESP_SPECIAL(c))
+
+/* list-wildcards = "%" / "*" */
+#define IS_LIST_WILDCARD(c) ((c) == '%' || (c) == '*')
+/* quoted-specials = DQUOTE / "\" */
+#define IS_QUOTED_SPECIAL(c) ((c) == '\"' || (c) == '\\')
+/* resp-specials = "]" */
+#define IS_RESP_SPECIAL(c) ((c) == ']')
+
+enum imap_arg_type {
+ IMAP_ARG_NIL = 0,
+ IMAP_ARG_ATOM,
+ IMAP_ARG_STRING,
+ IMAP_ARG_LIST,
+
+ /* literals are returned as IMAP_ARG_STRING by default */
+ IMAP_ARG_LITERAL,
+ IMAP_ARG_LITERAL_SIZE,
+ IMAP_ARG_LITERAL_SIZE_NONSYNC,
+
+ IMAP_ARG_EOL /* end of argument list */
+};
+
+ARRAY_DEFINE_TYPE(imap_arg_list, struct imap_arg);
+struct imap_arg {
+ enum imap_arg_type type;
+ struct imap_arg *parent; /* always of type IMAP_ARG_LIST */
+
+ /* Set when _data.str is set */
+ size_t str_len;
+
+ union {
+ const char *str;
+ uoff_t literal_size;
+ ARRAY_TYPE(imap_arg_list) list;
+ } _data;
+ bool literal8:1; /* BINARY literal8 used */
+};
+
+/* RFC 3501's astring type. Note that this doesn't return TRUE for
+ IMAP_ARG_NIL, although it should be treated the same as "NIL" string when
+ reading an astring. */
+#define IMAP_ARG_TYPE_IS_ASTRING(type) \
+ ((type) == IMAP_ARG_ATOM || \
+ (type) == IMAP_ARG_STRING || \
+ (type) == IMAP_ARG_LITERAL)
+#define IMAP_ARG_IS_ASTRING(arg) \
+ IMAP_ARG_TYPE_IS_ASTRING((arg)->type)
+#define IMAP_ARG_IS_NSTRING(arg) \
+ (IMAP_ARG_IS_ASTRING(arg) || (arg)->type == IMAP_ARG_NIL)
+#define IMAP_ARG_IS_EOL(arg) \
+ ((arg)->type == IMAP_ARG_EOL)
+
+bool imap_arg_get_atom(const struct imap_arg *arg, const char **str_r)
+ ATTR_WARN_UNUSED_RESULT;
+bool imap_arg_get_quoted(const struct imap_arg *arg, const char **str_r)
+ ATTR_WARN_UNUSED_RESULT;
+bool imap_arg_get_string(const struct imap_arg *arg, const char **str_r)
+ ATTR_WARN_UNUSED_RESULT;
+bool imap_arg_get_astring(const struct imap_arg *arg, const char **str_r)
+ ATTR_WARN_UNUSED_RESULT;
+/* str is set to NULL for NIL. */
+bool imap_arg_get_nstring(const struct imap_arg *arg, const char **str_r)
+ ATTR_WARN_UNUSED_RESULT;
+
+bool imap_arg_get_literal_size(const struct imap_arg *arg, uoff_t *size_r)
+ ATTR_WARN_UNUSED_RESULT;
+
+bool imap_arg_get_list(const struct imap_arg *arg,
+ const struct imap_arg **list_r)
+ ATTR_WARN_UNUSED_RESULT;
+bool imap_arg_get_list_full(const struct imap_arg *arg,
+ const struct imap_arg **list_r,
+ unsigned int *list_count_r) ATTR_WARN_UNUSED_RESULT;
+
+/* Similar to above, but assumes that arg is already of correct type. */
+const char *imap_arg_as_astring(const struct imap_arg *arg);
+const char *imap_arg_as_nstring(const struct imap_arg *arg);
+uoff_t imap_arg_as_literal_size(const struct imap_arg *arg);
+const struct imap_arg *imap_arg_as_list(const struct imap_arg *arg);
+
+/* Returns TRUE if arg is atom and case-insensitively matches str */
+bool imap_arg_atom_equals(const struct imap_arg *arg, const char *str);
+
+#endif
diff --git a/src/lib-imap/imap-base-subject.c b/src/lib-imap/imap-base-subject.c
new file mode 100644
index 0000000..02469d3
--- /dev/null
+++ b/src/lib-imap/imap-base-subject.c
@@ -0,0 +1,248 @@
+/* Copyright (c) 2002-2018 Dovecot authors, see the included COPYING file */
+
+/* Implemented against draft-ietf-imapext-sort-10 and
+ draft-ietf-imapext-thread-12 */
+
+#include "lib.h"
+#include "buffer.h"
+#include "charset-utf8.h"
+#include "message-header-decode.h"
+#include "imap-base-subject.h"
+
+static void pack_whitespace(buffer_t *buf)
+{
+ char *data, *dest;
+ bool last_lwsp;
+
+ data = buffer_get_modifiable_data(buf, NULL);
+
+ /* check if we need to do anything */
+ while (*data != '\0') {
+ if (*data == '\t' || *data == '\n' || *data == '\r' ||
+ (*data == ' ' && (data[1] == ' ' || data[1] == '\t')))
+ break;
+ data++;
+ }
+
+ if (*data == '\0')
+ return;
+
+ /* @UNSAFE: convert/pack the whitespace */
+ dest = data; last_lwsp = FALSE;
+ while (*data != '\0') {
+ if (*data == '\t' || *data == ' ' ||
+ *data == '\r' || *data == '\n') {
+ if (!last_lwsp) {
+ *dest++ = ' ';
+ last_lwsp = TRUE;
+ }
+ } else {
+ *dest++ = *data;
+ last_lwsp = FALSE;
+ }
+ data++;
+ }
+ *dest = '\0';
+
+ data = buffer_get_modifiable_data(buf, NULL);
+ buffer_set_used_size(buf, (size_t) (dest - data)+1);
+}
+
+static void remove_subj_trailers(buffer_t *buf, size_t start_pos,
+ bool *is_reply_or_forward_r)
+{
+ const char *data;
+ size_t orig_size, size;
+
+ /* subj-trailer = "(fwd)" / WSP */
+ data = buffer_get_data(buf, &orig_size);
+
+ if (orig_size < 1) /* size includes trailing \0 */
+ return;
+
+ for (size = orig_size-1; size > start_pos; ) {
+ if (data[size-1] == ' ')
+ size--;
+ else if (size >= 5 &&
+ memcmp(data + size - 5, "(FWD)", 5) == 0) {
+ *is_reply_or_forward_r = TRUE;
+ size -= 5;
+ } else {
+ break;
+ }
+ }
+
+ if (size != orig_size-1) {
+ buffer_set_used_size(buf, size);
+ buffer_append_c(buf, '\0');
+ }
+}
+
+static bool remove_blob(const char **datap)
+{
+ const char *data = *datap;
+
+ if (*data != '[')
+ return FALSE;
+
+ data++;
+ while (*data != '\0' && *data != '[' && *data != ']')
+ data++;
+
+ if (*data != ']')
+ return FALSE;
+
+ data++;
+ if (*data == ' ')
+ data++;
+
+ *datap = data;
+ return TRUE;
+}
+
+static bool remove_subj_leader(buffer_t *buf, size_t *start_pos,
+ bool *is_reply_or_forward_r)
+{
+ const char *data, *orig_data;
+ bool ret = FALSE;
+
+ /* subj-leader = (*subj-blob subj-refwd) / WSP
+
+ subj-blob = "[" *BLOBCHAR "]" *WSP
+ subj-refwd = ("re" / ("fw" ["d"])) *WSP [subj-blob] ":"
+
+ BLOBCHAR = %x01-5a / %x5c / %x5e-7f
+ ; any CHAR except '[' and ']' */
+ orig_data = buf->data;
+ orig_data += *start_pos;
+ data = orig_data;
+
+ if (*data == ' ') {
+ /* independent from checks below - always removed */
+ data++; orig_data++;
+ *start_pos += 1;
+ ret = TRUE;
+ }
+
+ while (*data == '[') {
+ if (!remove_blob(&data))
+ return ret;
+ }
+
+ if (str_begins(data, "RE"))
+ data += 2;
+ else if (str_begins(data, "FWD"))
+ data += 3;
+ else if (str_begins(data, "FW"))
+ data += 2;
+ else
+ return ret;
+
+ if (*data == ' ')
+ data++;
+
+ if (*data == '[' && !remove_blob(&data))
+ return ret;
+
+ if (*data != ':')
+ return ret;
+
+ data++;
+ *start_pos += (size_t)(data - orig_data);
+ *is_reply_or_forward_r = TRUE;
+ return TRUE;
+}
+
+static bool remove_blob_when_nonempty(buffer_t *buf, size_t *start_pos)
+{
+ const char *data, *orig_data;
+
+ orig_data = buf->data;
+ orig_data += *start_pos;
+ data = orig_data;
+ if (*data == '[' && remove_blob(&data) && *data != '\0') {
+ *start_pos += (size_t)(data - orig_data);
+ return TRUE;
+ }
+
+ return FALSE;
+}
+
+static bool remove_subj_fwd_hdr(buffer_t *buf, size_t *start_pos,
+ bool *is_reply_or_forward_r)
+{
+ const char *data = buf->data;
+ size_t size = buf->used;
+
+ /* subj-fwd = subj-fwd-hdr subject subj-fwd-trl
+ subj-fwd-hdr = "[fwd:"
+ subj-fwd-trl = "]" */
+
+ if (!str_begins(data + *start_pos, "[FWD:"))
+ return FALSE;
+
+ if (data[size-2] != ']')
+ return FALSE;
+
+ *is_reply_or_forward_r = TRUE;
+
+ buffer_set_used_size(buf, size-2);
+ buffer_append_c(buf, '\0');
+
+ *start_pos += 5;
+ return TRUE;
+}
+
+const char *imap_get_base_subject_cased(pool_t pool, const char *subject,
+ bool *is_reply_or_forward_r)
+{
+ buffer_t *buf;
+ size_t start_pos, subject_len;
+ bool found;
+
+ *is_reply_or_forward_r = FALSE;
+
+ subject_len = strlen(subject);
+ buf = buffer_create_dynamic(pool, subject_len);
+
+ /* (1) Convert any RFC 2047 encoded-words in the subject to
+ UTF-8. Convert all tabs and continuations to space.
+ Convert all multiple spaces to a single space. */
+ message_header_decode_utf8((const unsigned char *)subject, subject_len,
+ buf, uni_utf8_to_decomposed_titlecase);
+ buffer_append_c(buf, '\0');
+
+ pack_whitespace(buf);
+
+ start_pos = 0;
+ do {
+ /* (2) Remove all trailing text of the subject that matches
+ the subj-trailer ABNF, repeat until no more matches are
+ possible. */
+ remove_subj_trailers(buf, start_pos, is_reply_or_forward_r);
+
+ do {
+ /* (3) Remove all prefix text of the subject that
+ matches the subj-leader ABNF. */
+ found = remove_subj_leader(buf, &start_pos,
+ is_reply_or_forward_r);
+
+ /* (4) If there is prefix text of the subject that
+ matches the subj-blob ABNF, and removing that prefix
+ leaves a non-empty subj-base, then remove the prefix
+ text. */
+ found = remove_blob_when_nonempty(buf, &start_pos) ||
+ found;
+
+ /* (5) Repeat (3) and (4) until no matches remain. */
+ } while (found);
+
+ /* (6) If the resulting text begins with the subj-fwd-hdr ABNF
+ and ends with the subj-fwd-trl ABNF, remove the
+ subj-fwd-hdr and subj-fwd-trl and repeat from step (2). */
+ } while (remove_subj_fwd_hdr(buf, &start_pos, is_reply_or_forward_r));
+
+ /* (7) The resulting text is the "base subject" used in the
+ SORT. */
+ return (const char *)buf->data + start_pos;
+}
diff --git a/src/lib-imap/imap-base-subject.h b/src/lib-imap/imap-base-subject.h
new file mode 100644
index 0000000..f086284
--- /dev/null
+++ b/src/lib-imap/imap-base-subject.h
@@ -0,0 +1,13 @@
+#ifndef IMAP_BASE_SUBJECT_H
+#define IMAP_BASE_SUBJECT_H
+
+/* Returns the base subject of the given string, according to
+ draft-ietf-imapext-sort-10. String is returned so that it's suitable for
+ strcmp() comparing with another base subject.
+
+ is_reply_or_forward is set to TRUE if message looks like reply or forward,
+ according to draft-ietf-imapext-thread-12 rules. */
+const char *imap_get_base_subject_cased(pool_t pool, const char *subject,
+ bool *is_reply_or_forward_r);
+
+#endif
diff --git a/src/lib-imap/imap-bodystructure.c b/src/lib-imap/imap-bodystructure.c
new file mode 100644
index 0000000..522c250
--- /dev/null
+++ b/src/lib-imap/imap-bodystructure.c
@@ -0,0 +1,956 @@
+/* Copyright (c) 2002-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "buffer.h"
+#include "istream.h"
+#include "str.h"
+#include "message-part-data.h"
+#include "message-parser.h"
+#include "rfc822-parser.h"
+#include "rfc2231-parser.h"
+#include "imap-parser.h"
+#include "imap-quote.h"
+#include "imap-envelope.h"
+#include "imap-bodystructure.h"
+
+#define EMPTY_BODY "(\"text\" \"plain\" " \
+ "(\"charset\" \""MESSAGE_PART_DEFAULT_CHARSET"\") NIL NIL \"7bit\" 0 0)"
+#define EMPTY_BODYSTRUCTURE "(\"text\" \"plain\" " \
+ "(\"charset\" \""MESSAGE_PART_DEFAULT_CHARSET"\") NIL NIL \"7bit\" 0 0 " \
+ "NIL NIL NIL NIL)"
+
+/*
+ * IMAP BODY/BODYSTRUCTURE write
+ */
+
+static void
+params_write(const struct message_part_param *params,
+ unsigned int params_count, string_t *str,
+ bool default_charset)
+{
+ unsigned int i;
+ bool seen_charset;
+
+ if (!default_charset && params_count == 0) {
+ str_append(str, "NIL");
+ return;
+ }
+ str_append_c(str, '(');
+
+ seen_charset = FALSE;
+ for (i = 0; i < params_count; i++) {
+ i_assert(params[i].name != NULL);
+ i_assert(params[i].value != NULL);
+
+ if (i > 0)
+ str_append_c(str, ' ');
+ if (default_charset &&
+ strcasecmp(params[i].name, "charset") == 0)
+ seen_charset = TRUE;
+ imap_append_string(str, params[i].name);
+ str_append_c(str, ' ');
+ imap_append_string(str, params[i].value);
+ }
+ if (default_charset && !seen_charset) {
+ if (i > 0)
+ str_append_c(str, ' ');
+ str_append(str, "\"charset\" "
+ "\""MESSAGE_PART_DEFAULT_CHARSET"\"");
+ }
+ str_append_c(str, ')');
+}
+
+static int
+part_write_bodystructure_siblings(const struct message_part *part,
+ string_t *dest, bool extended,
+ const char **error_r)
+{
+ for (; part != NULL; part = part->next) {
+ str_append_c(dest, '(');
+ if (imap_bodystructure_write(part, dest, extended, error_r) < 0)
+ return -1;
+ str_append_c(dest, ')');
+ }
+ return 0;
+}
+
+static void
+part_write_bodystructure_common(const struct message_part_data *data,
+ string_t *str)
+{
+ str_append_c(str, ' ');
+ if (data->content_disposition == NULL)
+ str_append(str, "NIL");
+ else {
+ str_append_c(str, '(');
+ imap_append_string(str, data->content_disposition);
+
+ str_append_c(str, ' ');
+ params_write(data->content_disposition_params,
+ data->content_disposition_params_count, str, FALSE);
+
+ str_append_c(str, ')');
+ }
+
+ str_append_c(str, ' ');
+ if (data->content_language == NULL)
+ str_append(str, "NIL");
+ else {
+ const char *const *lang = data->content_language;
+
+ i_assert(*lang != NULL);
+ str_append_c(str, '(');
+ imap_append_string(str, *lang);
+ lang++;
+ while (*lang != NULL) {
+ str_append_c(str, ' ');
+ imap_append_string(str, *lang);
+ lang++;
+ }
+ str_append_c(str, ')');
+ }
+
+ str_append_c(str, ' ');
+ imap_append_nstring_nolf(str, data->content_location);
+}
+
+static int part_write_body_multipart(const struct message_part *part,
+ string_t *str, bool extended,
+ const char **error_r)
+{
+ const struct message_part_data *data = part->data;
+
+ i_assert(part->data != NULL);
+
+ if (part->children != NULL) {
+ if (part_write_bodystructure_siblings(part->children, str,
+ extended, error_r) < 0)
+ return -1;
+ } else {
+ /* no parts in multipart message,
+ that's not allowed. write a single
+ 0-length text/plain structure */
+ if (!extended)
+ str_append(str, EMPTY_BODY);
+ else
+ str_append(str, EMPTY_BODYSTRUCTURE);
+ }
+
+ str_append_c(str, ' ');
+ imap_append_string(str, data->content_subtype);
+
+ if (!extended)
+ return 0;
+
+ /* BODYSTRUCTURE data */
+
+ str_append_c(str, ' ');
+ params_write(data->content_type_params,
+ data->content_type_params_count, str, FALSE);
+
+ part_write_bodystructure_common(data, str);
+ return 0;
+}
+
+static bool part_is_truncated(const struct message_part *part)
+{
+ const struct message_part_data *data = part->data;
+
+ i_assert((part->flags & MESSAGE_PART_FLAG_MESSAGE_RFC822) == 0);
+ i_assert((part->flags & MESSAGE_PART_FLAG_MULTIPART) == 0);
+
+ if (data->content_type != NULL) {
+ if (strcasecmp(data->content_type, "message") == 0 &&
+ strcasecmp(data->content_subtype, "rfc822") == 0) {
+ /* It's message/rfc822, but without
+ MESSAGE_PART_FLAG_MESSAGE_RFC822. */
+ return TRUE;
+ }
+ if (strcasecmp(data->content_type, "multipart") == 0) {
+ /* It's multipart/, but without
+ MESSAGE_PART_FLAG_MULTIPART. */
+ return TRUE;
+ }
+ } else {
+ /* No Content-Type */
+ if (part->parent != NULL &&
+ (part->parent->flags & MESSAGE_PART_FLAG_MULTIPART_DIGEST) != 0) {
+ /* Parent is MESSAGE_PART_FLAG_MULTIPART_DIGEST
+ (so this should have been message/rfc822). */
+ return TRUE;
+ }
+ }
+ return FALSE;
+}
+
+static int part_write_body(const struct message_part *part,
+ string_t *str, bool extended, const char **error_r)
+{
+ const struct message_part_data *data = part->data;
+ bool text;
+
+ i_assert(part->data != NULL);
+
+ if ((part->flags & MESSAGE_PART_FLAG_MESSAGE_RFC822) != 0) {
+ str_append(str, "\"message\" \"rfc822\"");
+ text = FALSE;
+ } else if (part_is_truncated(part)) {
+ /* Maximum MIME part count was reached while parsing the mail.
+ Write this part out as application/octet-stream instead.
+ We're not using text/plain, because it would require
+ message-parser to use MESSAGE_PART_FLAG_TEXT for this part
+ to avoid losing line count in message_part serialization. */
+ str_append(str, "\"application\" \"octet-stream\"");
+ text = FALSE;
+ } else {
+ /* "content type" "subtype" */
+ if (data->content_type == NULL) {
+ text = TRUE;
+ str_append(str, "\"text\" \"plain\"");
+ } else {
+ text = (strcasecmp(data->content_type, "text") == 0);
+ imap_append_string(str, data->content_type);
+ str_append_c(str, ' ');
+ imap_append_string(str, data->content_subtype);
+ }
+ bool part_is_text = (part->flags & MESSAGE_PART_FLAG_TEXT) != 0;
+ if (text != part_is_text) {
+ *error_r = "text flag mismatch";
+ return -1;
+ }
+ }
+
+ /* ("content type param key" "value" ...) */
+ str_append_c(str, ' ');
+ params_write(data->content_type_params,
+ data->content_type_params_count, str, text);
+
+ str_append_c(str, ' ');
+ imap_append_nstring_nolf(str, data->content_id);
+ str_append_c(str, ' ');
+ imap_append_nstring_nolf(str, data->content_description);
+ str_append_c(str, ' ');
+ if (data->content_transfer_encoding != NULL)
+ imap_append_string(str, data->content_transfer_encoding);
+ else
+ str_append(str, "\"7bit\"");
+ str_printfa(str, " %"PRIuUOFF_T, part->body_size.virtual_size);
+
+ if (text) {
+ /* text/.. contains line count */
+ str_printfa(str, " %u", part->body_size.lines);
+ } else if ((part->flags & MESSAGE_PART_FLAG_MESSAGE_RFC822) != 0) {
+ /* message/rfc822 contains envelope + body + line count */
+ const struct message_part_data *child_data;
+
+ i_assert(part->children != NULL);
+ i_assert(part->children->next == NULL);
+
+ child_data = part->children->data;
+
+ str_append(str, " (");
+ imap_envelope_write(child_data->envelope, str);
+ str_append(str, ") ");
+
+ if (part_write_bodystructure_siblings(part->children, str,
+ extended, error_r) < 0)
+ return -1;
+ str_printfa(str, " %u", part->body_size.lines);
+ }
+
+ if (!extended)
+ return 0;
+
+ /* BODYSTRUCTURE data */
+
+ /* "md5" ("content disposition" ("disposition" "params"))
+ ("body" "language" "params") "location" */
+ str_append_c(str, ' ');
+ imap_append_nstring_nolf(str, data->content_md5);
+ part_write_bodystructure_common(data, str);
+ return 0;
+}
+
+int imap_bodystructure_write(const struct message_part *part,
+ string_t *dest, bool extended,
+ const char **error_r)
+{
+ if ((part->flags & MESSAGE_PART_FLAG_MULTIPART) != 0)
+ return part_write_body_multipart(part, dest, extended, error_r);
+ else
+ return part_write_body(part, dest, extended, error_r);
+}
+
+/*
+ * IMAP BODYSTRUCTURE parsing
+ */
+
+static int
+imap_bodystructure_strlist_parse(const struct imap_arg *arg,
+ pool_t pool, const char *const **list_r)
+{
+ const char *item, **list;
+ const struct imap_arg *list_args;
+ unsigned int list_count, i;
+
+ if (arg->type == IMAP_ARG_NIL) {
+ *list_r = NULL;
+ return 0;
+ }
+ if (imap_arg_get_nstring(arg, &item)) {
+ list = p_new(pool, const char *, 2);
+ list[0] = p_strdup(pool, item);
+ } else {
+ if (!imap_arg_get_list_full(arg, &list_args, &list_count))
+ return -1;
+ if (list_count == 0)
+ return -1;
+
+ list = p_new(pool, const char *, list_count+1);
+ for (i = 0; i < list_count; i++) {
+ if (!imap_arg_get_string(&list_args[i], &item))
+ return -1;
+ list[i] = p_strdup(pool, item);
+ }
+ }
+ *list_r = list;
+ return 0;
+}
+
+static int
+imap_bodystructure_params_parse(const struct imap_arg *arg,
+ pool_t pool, const struct message_part_param **params_r,
+ unsigned int *count_r)
+{
+ struct message_part_param *params;
+ const struct imap_arg *list_args;
+ unsigned int list_count, params_count, i;
+
+ if (arg->type == IMAP_ARG_NIL) {
+ *params_r = NULL;
+ return 0;
+ }
+ if (!imap_arg_get_list_full(arg, &list_args, &list_count))
+ return -1;
+ if ((list_count % 2) != 0)
+ return -1;
+ if (list_count == 0)
+ return -1;
+
+ params_count = list_count/2;
+ params = p_new(pool, struct message_part_param, params_count+1);
+ for (i = 0; i < params_count; i++) {
+ const char *name, *value;
+
+ if (!imap_arg_get_string(&list_args[i*2+0], &name))
+ return -1;
+ if (!imap_arg_get_string(&list_args[i*2+1], &value))
+ return -1;
+ params[i].name = p_strdup(pool, name);
+ params[i].value = p_strdup(pool, value);
+ }
+ *params_r = params;
+ *count_r = params_count;
+ return 0;
+}
+
+static int
+imap_bodystructure_parse_args_common(struct message_part *part,
+ pool_t pool, const struct imap_arg *args,
+ const char **error_r)
+{
+ struct message_part_data *data = part->data;
+ const struct imap_arg *list_args;
+
+ if (args->type == IMAP_ARG_EOL)
+ return 0;
+ if (args->type == IMAP_ARG_NIL)
+ args++;
+ else if (!imap_arg_get_list(args, &list_args)) {
+ *error_r = "Invalid content-disposition list";
+ return -1;
+ } else {
+ if (!imap_arg_get_string
+ (list_args++, &data->content_disposition)) {
+ *error_r = "Invalid content-disposition";
+ return -1;
+ }
+ data->content_disposition = p_strdup(pool, data->content_disposition);
+ if (imap_bodystructure_params_parse(list_args, pool,
+ &data->content_disposition_params,
+ &data->content_disposition_params_count) < 0) {
+ *error_r = "Invalid content-disposition params";
+ return -1;
+ }
+ args++;
+ }
+ if (args->type == IMAP_ARG_EOL)
+ return 0;
+ if (imap_bodystructure_strlist_parse
+ (args++, pool, &data->content_language) < 0) {
+ *error_r = "Invalid content-language";
+ return -1;
+ }
+ if (args->type == IMAP_ARG_EOL)
+ return 0;
+ if (!imap_arg_get_nstring
+ (args++, &data->content_location)) {
+ *error_r = "Invalid content-location";
+ return -1;
+ }
+ data->content_location = p_strdup(pool, data->content_location);
+ return 0;
+}
+
+int
+imap_bodystructure_parse_args(const struct imap_arg *args, pool_t pool,
+ struct message_part **_part,
+ const char **error_r)
+{
+ struct message_part *part = *_part, *child_part;;
+ struct message_part **child_part_p;
+ struct message_part_data *data;
+ const struct imap_arg *list_args;
+ const char *value, *content_type, *subtype, *error;
+ bool multipart, text, message_rfc822, parsing_tree, has_lines;
+ unsigned int lines;
+ uoff_t vsize;
+
+ if (part != NULL) {
+ /* parsing with pre-existing message_part tree */
+ parsing_tree = FALSE;
+ } else {
+ /* parsing message_part tree from BODYSTRUCTURE as well */
+ part = *_part = p_new(pool, struct message_part, 1);
+ parsing_tree = TRUE;
+ }
+ part->data = data = p_new(pool, struct message_part_data, 1);
+
+ multipart = FALSE;
+ if (!parsing_tree) {
+ if ((part->flags & MESSAGE_PART_FLAG_MULTIPART) != 0 &&
+ part->children == NULL) {
+ struct message_part_data dummy_part_data = {
+ .content_type = "text",
+ .content_subtype = "plain",
+ .content_transfer_encoding = "7bit"
+ };
+ struct message_part dummy_part = {
+ .parent = part,
+ .data = &dummy_part_data,
+ .flags = MESSAGE_PART_FLAG_TEXT
+ };
+ struct message_part *dummy_partp = &dummy_part;
+
+ /* no parts in multipart message,
+ that's not allowed. expect a single
+ 0-length text/plain structure */
+ if (args->type != IMAP_ARG_LIST ||
+ (args+1)->type == IMAP_ARG_LIST) {
+ *error_r = "message_part hierarchy "
+ "doesn't match BODYSTRUCTURE";
+ return -1;
+ }
+
+ list_args = imap_arg_as_list(args);
+ if (imap_bodystructure_parse_args(list_args, pool,
+ &dummy_partp, error_r) < 0)
+ return -1;
+ child_part = NULL;
+
+ multipart = TRUE;
+ args++;
+
+ } else {
+ child_part = part->children;
+ while (args->type == IMAP_ARG_LIST) {
+ if ((part->flags & MESSAGE_PART_FLAG_MULTIPART) == 0 ||
+ child_part == NULL) {
+ *error_r = "message_part hierarchy "
+ "doesn't match BODYSTRUCTURE";
+ return -1;
+ }
+
+ list_args = imap_arg_as_list(args);
+ if (imap_bodystructure_parse_args(list_args, pool,
+ &child_part, error_r) < 0)
+ return -1;
+ child_part = child_part->next;
+
+ multipart = TRUE;
+ args++;
+ }
+ }
+ if (multipart) {
+ if (child_part != NULL) {
+ *error_r = "message_part hierarchy "
+ "doesn't match BODYSTRUCTURE";
+ return -1;
+ }
+ } else if ((part->flags & MESSAGE_PART_FLAG_MULTIPART) != 0) {
+ *error_r = "message_part multipart flag "
+ "doesn't match BODYSTRUCTURE";
+ return -1;
+ }
+ } else {
+ child_part_p = &part->children;
+ while (args->type == IMAP_ARG_LIST) {
+ list_args = imap_arg_as_list(args);
+ if (imap_bodystructure_parse_args(list_args, pool,
+ child_part_p, error_r) < 0)
+ return -1;
+ (*child_part_p)->parent = part;
+ child_part_p = &(*child_part_p)->next;
+
+ multipart = TRUE;
+ args++;
+ }
+ if (multipart) {
+ part->flags |= MESSAGE_PART_FLAG_MULTIPART;
+ }
+ }
+
+ if (multipart) {
+ data->content_type = "multipart";
+ if (!imap_arg_get_string(args++, &data->content_subtype)) {
+ *error_r = "Invalid multipart content-type";
+ return -1;
+ }
+ data->content_subtype = p_strdup(pool, data->content_subtype);
+ if (args->type == IMAP_ARG_EOL)
+ return 0;
+ if (imap_bodystructure_params_parse(args++, pool,
+ &data->content_type_params,
+ &data->content_type_params_count) < 0) {
+ *error_r = "Invalid content params";
+ return -1;
+ }
+ return imap_bodystructure_parse_args_common
+ (part, pool, args, error_r);
+ }
+
+ /* "content type" "subtype" */
+ if (!imap_arg_get_string(&args[0], &content_type) ||
+ !imap_arg_get_string(&args[1], &subtype)) {
+ *error_r = "Invalid content-type";
+ return -1;
+ }
+ data->content_type = p_strdup(pool, content_type);
+ data->content_subtype = p_strdup(pool, subtype);
+ args += 2;
+
+ text = strcasecmp(content_type, "text") == 0;
+ message_rfc822 = strcasecmp(content_type, "message") == 0 &&
+ strcasecmp(subtype, "rfc822") == 0;
+
+ if (!parsing_tree) {
+#if 0
+ /* Disabled for now. Earlier Dovecot versions handled broken
+ Content-Type headers by writing them as "text" "plain" to
+ BODYSTRUCTURE reply, but the message_part didn't have
+ MESSAGE_PART_FLAG_TEXT. */
+ if (text != ((part->flags & MESSAGE_PART_FLAG_TEXT) != 0)) {
+ *error_r = "message_part text flag "
+ "doesn't match BODYSTRUCTURE";
+ return -1;
+ }
+#endif
+ if (message_rfc822 !=
+ ((part->flags & MESSAGE_PART_FLAG_MESSAGE_RFC822) != 0)) {
+ *error_r = "message_part message/rfc822 flag "
+ "doesn't match BODYSTRUCTURE";
+ return -1;
+ }
+ } else {
+ if (text)
+ part->flags |= MESSAGE_PART_FLAG_TEXT;
+ if (message_rfc822)
+ part->flags |= MESSAGE_PART_FLAG_MESSAGE_RFC822;
+ }
+
+ /* ("content type param key" "value" ...) | NIL */
+ if (imap_bodystructure_params_parse(args++, pool,
+ &data->content_type_params,
+ &data->content_type_params_count) < 0) {
+ *error_r = "Invalid content params";
+ return -1;
+ }
+
+ /* "content id" "content description" "transfer encoding" size */
+ if (!imap_arg_get_nstring(args++, &data->content_id)) {
+ *error_r = "Invalid content-id";
+ return -1;
+ }
+ if (!imap_arg_get_nstring(args++, &data->content_description)) {
+ *error_r = "Invalid content-description";
+ return -1;
+ }
+ if (!imap_arg_get_string(args++, &data->content_transfer_encoding)) {
+ *error_r = "Invalid content-transfer-encoding";
+ return -1;
+ }
+ data->content_id = p_strdup(pool, data->content_id);
+ data->content_description = p_strdup(pool, data->content_description);
+ data->content_transfer_encoding =
+ p_strdup(pool, data->content_transfer_encoding);
+ if (!imap_arg_get_atom(args++, &value) ||
+ str_to_uoff(value, &vsize) < 0) {
+ *error_r = "Invalid size field";
+ return -1;
+ }
+ if (!parsing_tree) {
+ if (vsize != part->body_size.virtual_size) {
+ *error_r = "message_part virtual_size doesn't match "
+ "size in BODYSTRUCTURE";
+ return -1;
+ }
+ } else {
+ part->body_size.virtual_size = vsize;
+ }
+
+ if (text) {
+ /* text/xxx - text lines */
+ if (!imap_arg_get_atom(args++, &value) ||
+ str_to_uint(value, &lines) < 0) {
+ *error_r = "Invalid lines field";
+ return -1;
+ }
+ i_assert(part->children == NULL);
+ has_lines = TRUE;
+ } else if (message_rfc822) {
+ /* message/rfc822 - envelope + bodystructure + text lines */
+
+ if (!parsing_tree) {
+ i_assert(part->children != NULL &&
+ part->children->next == NULL);
+ }
+
+ if (!imap_arg_get_list(&args[1], &list_args)) {
+ *error_r = "Child bodystructure list expected";
+ return -1;
+ }
+ if (imap_bodystructure_parse_args
+ (list_args, pool, &part->children, error_r) < 0)
+ return -1;
+ if (parsing_tree) {
+ i_assert(part->children != NULL &&
+ part->children->next == NULL);
+ part->children->parent = part;
+ }
+
+ if (!imap_arg_get_list(&args[0], &list_args)) {
+ *error_r = "Envelope list expected";
+ return -1;
+ }
+ if (!imap_envelope_parse_args(list_args, pool,
+ &part->children->data->envelope, &error)) {
+ *error_r = t_strdup_printf
+ ("Invalid envelope list: %s", error);
+ return -1;
+ }
+ args += 2;
+ if (!imap_arg_get_atom(args++, &value) ||
+ str_to_uint(value, &lines) < 0) {
+ *error_r = "Invalid lines field";
+ return -1;
+ }
+ has_lines = TRUE;
+ } else {
+ i_assert(part->children == NULL);
+ lines = 0;
+ has_lines = FALSE;
+ }
+ if (!parsing_tree) {
+ if (has_lines && lines != part->body_size.lines) {
+ *error_r = "message_part lines "
+ "doesn't match lines in BODYSTRUCTURE";
+ return -1;
+ }
+ } else {
+ part->body_size.lines = lines;
+ }
+ if (args->type == IMAP_ARG_EOL)
+ return 0;
+ if (!imap_arg_get_nstring(args++, &data->content_md5)) {
+ *error_r = "Invalid content-md5";
+ return -1;
+ }
+ data->content_md5 = p_strdup(pool, data->content_md5);
+ return imap_bodystructure_parse_args_common
+ (part, pool, args, error_r);
+}
+
+int imap_bodystructure_parse_full(const char *bodystructure,
+ pool_t pool, struct message_part **parts,
+ const char **error_r)
+{
+ struct istream *input;
+ struct imap_parser *parser;
+ const struct imap_arg *args;
+ int ret;
+
+ i_assert(*parts == NULL || (*parts)->next == NULL);
+
+ input = i_stream_create_from_data(bodystructure, strlen(bodystructure));
+ (void)i_stream_read(input);
+
+ parser = imap_parser_create(input, NULL, SIZE_MAX);
+ ret = imap_parser_finish_line(parser, 0,
+ IMAP_PARSE_FLAG_LITERAL_TYPE, &args);
+ if (ret < 0) {
+ *error_r = t_strdup_printf("IMAP parser failed: %s",
+ imap_parser_get_error(parser, NULL));
+ } else if (ret == 0) {
+ *error_r = "Empty bodystructure";
+ ret = -1;
+ } else {
+ T_BEGIN {
+ ret = imap_bodystructure_parse_args
+ (args, pool, parts, error_r);
+ } T_END_PASS_STR_IF(ret < 0, error_r);
+ }
+
+ imap_parser_unref(&parser);
+ i_stream_destroy(&input);
+ return ret;
+}
+
+int imap_bodystructure_parse(const char *bodystructure,
+ pool_t pool, struct message_part *parts,
+ const char **error_r)
+{
+ i_assert(parts != NULL);
+
+ return imap_bodystructure_parse_full(bodystructure,
+ pool, &parts, error_r);
+}
+
+/*
+ * IMAP BODYSTRUCTURE to BODY conversion
+ */
+
+static bool str_append_nstring(string_t *str, const struct imap_arg *arg)
+{
+ const char *cstr;
+
+ if (!imap_arg_get_nstring(arg, &cstr))
+ return FALSE;
+
+ switch (arg->type) {
+ case IMAP_ARG_NIL:
+ str_append(str, "NIL");
+ break;
+ case IMAP_ARG_STRING:
+ str_append_c(str, '"');
+ /* NOTE: we're parsing with no-unescape flag,
+ so don't double-escape it here */
+ str_append(str, cstr);
+ str_append_c(str, '"');
+ break;
+ case IMAP_ARG_LITERAL: {
+ str_printfa(str, "{%zu}\r\n", strlen(cstr));
+ str_append(str, cstr);
+ break;
+ }
+ default:
+ i_unreached();
+ return FALSE;
+ }
+ return TRUE;
+}
+
+static void
+imap_write_envelope_list(const struct imap_arg *args, string_t *str,
+ bool toplevel)
+{
+ const struct imap_arg *children;
+
+ /* don't do any typechecking, just write it out */
+ while (!IMAP_ARG_IS_EOL(args)) {
+ bool list = FALSE;
+
+ if (!str_append_nstring(str, args)) {
+ if (!imap_arg_get_list(args, &children)) {
+ /* everything is either nstring or list */
+ i_unreached();
+ }
+
+ str_append_c(str, '(');
+ imap_write_envelope_list(children, str, FALSE);
+ str_append_c(str, ')');
+
+ list = TRUE;
+ }
+ args++;
+
+ if ((toplevel || !list) && !IMAP_ARG_IS_EOL(args))
+ str_append_c(str, ' ');
+ }
+}
+
+static void
+imap_write_envelope(const struct imap_arg *args, string_t *str)
+{
+ imap_write_envelope_list(args, str, TRUE);
+}
+
+static int imap_parse_bodystructure_args(const struct imap_arg *args,
+ string_t *str, const char **error_r)
+{
+ const struct imap_arg *subargs;
+ const struct imap_arg *list_args;
+ const char *value, *content_type, *subtype;
+ bool multipart, text, message_rfc822;
+ int i;
+
+ multipart = FALSE;
+ while (args->type == IMAP_ARG_LIST) {
+ str_append_c(str, '(');
+ list_args = imap_arg_as_list(args);
+ if (imap_parse_bodystructure_args(list_args, str, error_r) < 0)
+ return -1;
+ str_append_c(str, ')');
+
+ multipart = TRUE;
+ args++;
+ }
+
+ if (multipart) {
+ /* next is subtype of Content-Type. rest is skipped. */
+ str_append_c(str, ' ');
+ if (!str_append_nstring(str, args)) {
+ *error_r = "Invalid multipart content-type";
+ return -1;
+ }
+ return 0;
+ }
+
+ /* "content type" "subtype" */
+ if (!imap_arg_get_string(&args[0], &content_type) ||
+ !imap_arg_get_string(&args[1], &subtype)) {
+ *error_r = "Invalid content-type";
+ return -1;
+ }
+
+ if (!str_append_nstring(str, &args[0]))
+ i_unreached();
+ str_append_c(str, ' ');
+ if (!str_append_nstring(str, &args[1]))
+ i_unreached();
+
+ text = strcasecmp(content_type, "text") == 0;
+ message_rfc822 = strcasecmp(content_type, "message") == 0 &&
+ strcasecmp(subtype, "rfc822") == 0;
+
+ args += 2;
+
+ /* ("content type param key" "value" ...) | NIL */
+ if (imap_arg_get_list(args, &subargs)) {
+ str_append(str, " (");
+ while (!IMAP_ARG_IS_EOL(subargs)) {
+ if (!str_append_nstring(str, &subargs[0])) {
+ *error_r = "Invalid content param key";
+ return -1;
+ }
+ str_append_c(str, ' ');
+ if (!str_append_nstring(str, &subargs[1])) {
+ *error_r = "Invalid content param value";
+ return -1;
+ }
+
+ subargs += 2;
+ if (IMAP_ARG_IS_EOL(subargs))
+ break;
+ str_append_c(str, ' ');
+ }
+ str_append(str, ")");
+ } else if (args->type == IMAP_ARG_NIL) {
+ str_append(str, " NIL");
+ } else {
+ *error_r = "list/NIL expected";
+ return -1;
+ }
+ args++;
+
+ /* "content id" "content description" "transfer encoding" size */
+ for (i = 0; i < 3; i++, args++) {
+ str_append_c(str, ' ');
+
+ if (!str_append_nstring(str, args)) {
+ *error_r = "nstring expected";
+ return -1;
+ }
+ }
+ if (!imap_arg_get_atom(args, &value)) {
+ *error_r = "atom expected for size";
+ return -1;
+ }
+ str_printfa(str, " %s", value);
+ args++;
+
+ if (text) {
+ /* text/xxx - text lines */
+ if (!imap_arg_get_atom(args, &value)) {
+ *error_r = "Text lines expected";
+ return -1;
+ }
+
+ str_append_c(str, ' ');
+ str_append(str, value);
+ } else if (message_rfc822) {
+ /* message/rfc822 - envelope + bodystructure + text lines */
+ str_append_c(str, ' ');
+
+ if (!imap_arg_get_list(&args[0], &list_args)) {
+ *error_r = "Envelope list expected";
+ return -1;
+ }
+ str_append_c(str, '(');
+ imap_write_envelope(list_args, str);
+ str_append(str, ") (");
+
+ if (!imap_arg_get_list(&args[1], &list_args)) {
+ *error_r = "Child bodystructure list expected";
+ return -1;
+ }
+ if (imap_parse_bodystructure_args(list_args, str, error_r) < 0)
+ return -1;
+
+ str_append(str, ") ");
+ if (!imap_arg_get_atom(&args[2], &value)) {
+ *error_r = "Text lines expected";
+ return -1;
+ }
+ str_append(str, value);
+ }
+ return 0;
+}
+
+int imap_body_parse_from_bodystructure(const char *bodystructure,
+ string_t *dest, const char **error_r)
+{
+ struct istream *input;
+ struct imap_parser *parser;
+ const struct imap_arg *args;
+ int ret;
+
+ input = i_stream_create_from_data(bodystructure, strlen(bodystructure));
+ (void)i_stream_read(input);
+
+ parser = imap_parser_create(input, NULL, SIZE_MAX);
+ ret = imap_parser_finish_line(parser, 0, IMAP_PARSE_FLAG_NO_UNESCAPE |
+ IMAP_PARSE_FLAG_LITERAL_TYPE, &args);
+ if (ret < 0) {
+ *error_r = t_strdup_printf("IMAP parser failed: %s",
+ imap_parser_get_error(parser, NULL));
+ } else if (ret == 0) {
+ *error_r = "Empty bodystructure";
+ ret = -1;
+ } else {
+ ret = imap_parse_bodystructure_args(args, dest, error_r);
+ }
+
+ imap_parser_unref(&parser);
+ i_stream_destroy(&input);
+ return ret;
+}
diff --git a/src/lib-imap/imap-bodystructure.h b/src/lib-imap/imap-bodystructure.h
new file mode 100644
index 0000000..a7cc6cd
--- /dev/null
+++ b/src/lib-imap/imap-bodystructure.h
@@ -0,0 +1,39 @@
+#ifndef IMAP_BODYSTRUCTURE_H
+#define IMAP_BODYSTRUCTURE_H
+
+struct message_part;
+struct message_header_line;
+struct imap_arg;
+
+/* Write a BODY/BODYSTRUCTURE from given message_part. The message_part->data
+ field must be set. part->body_size.virtual_size and .lines are also used
+ for writing it. Returns 0 on success, -1 if parts don't internally match
+ (e.g. broken cached mime.parts mixed with parsed message). */
+int imap_bodystructure_write(const struct message_part *part,
+ string_t *dest, bool extended,
+ const char **error_r);
+
+/* Parse BODYSTRUCTURE and save the contents to message_part->data for each
+ message tree node. If the parts argument points to NULL, the message_part
+ tree is created from the BODYSTRUCTURE. Otherwise, existing tree is used.
+ Returns 0 if ok, -1 if bodystructure wasn't valid. */
+int imap_bodystructure_parse_full(const char *bodystructure, pool_t pool,
+ struct message_part **parts, const char **error_r);
+
+/* Same as imap_bodystructure_parse_full(), but read the input from imap_args
+ instead of a string. */
+int imap_bodystructure_parse_args(const struct imap_arg *args, pool_t pool,
+ struct message_part **parts, const char **error_r);
+
+/* Parse BODYSTRUCTURE and save the contents to message_part->data for each
+ message tree node. The parts argument must point to an existing message_part
+ tree. Returns 0 if ok, -1 if bodystructure wasn't valid. */
+int imap_bodystructure_parse(const char *bodystructure, pool_t pool,
+ struct message_part *parts, const char **error_r);
+
+/* Get BODY part from BODYSTRUCTURE and write it to dest.
+ Returns 0 if ok, -1 if bodystructure wasn't valid. */
+int imap_body_parse_from_bodystructure(const char *bodystructure,
+ string_t *dest, const char **error_r);
+
+#endif
diff --git a/src/lib-imap/imap-date.c b/src/lib-imap/imap-date.c
new file mode 100644
index 0000000..2e5569a
--- /dev/null
+++ b/src/lib-imap/imap-date.c
@@ -0,0 +1,247 @@
+/* Copyright (c) 2002-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "utc-offset.h"
+#include "utc-mktime.h"
+#include "imap-date.h"
+
+#include <ctype.h>
+
+static const char *month_names[] = {
+ "Jan", "Feb", "Mar", "Apr", "May", "Jun",
+ "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"
+};
+
+static int parse_timezone(const char *str)
+{
+ int offset;
+
+ /* +|-zone */
+ if ((*str != '+' && *str != '-') ||
+ !i_isdigit(str[1]) || !i_isdigit(str[2]) ||
+ !i_isdigit(str[3]) || !i_isdigit(str[4]))
+ return 0;
+
+ offset = (str[1]-'0') * 10*60 + (str[2]-'0') * 60 +
+ (str[3]-'0') * 10 + (str[4]-'0');
+ return *str == '+' ? offset : -offset;
+}
+
+static const char *imap_parse_date_internal(const char *str, struct tm *tm)
+{
+ int i;
+
+ if (str == NULL || *str == '\0')
+ return NULL;
+
+ i_zero(tm);
+
+ /* "dd-mon-yyyy [hh:mi:ss +|-zone]"
+ dd is 1-2 digits and may be prefixed with space or zero. */
+
+ if (str[0] == ' ') {
+ /* " d-..." */
+ str++;
+ }
+
+ if (!(i_isdigit(str[0]) && (str[1] == '-' ||
+ (i_isdigit(str[1]) && str[2] == '-'))))
+ return NULL;
+
+ tm->tm_mday = (str[0]-'0');
+ if (str[1] == '-')
+ str += 2;
+ else {
+ tm->tm_mday = (tm->tm_mday * 10) + (str[1]-'0');
+ str += 3;
+ }
+
+ /* month name */
+ for (i = 0; i < 12; i++) {
+ if (strncasecmp(month_names[i], str, 3) == 0) {
+ tm->tm_mon = i;
+ break;
+ }
+ }
+ if (i == 12 || str[3] != '-')
+ return NULL;
+ str += 4;
+
+ /* yyyy */
+ if (!i_isdigit(str[0]) || !i_isdigit(str[1]) ||
+ !i_isdigit(str[2]) || !i_isdigit(str[3]))
+ return NULL;
+
+ tm->tm_year = (str[0]-'0') * 1000 + (str[1]-'0') * 100 +
+ (str[2]-'0') * 10 + (str[3]-'0') - 1900;
+
+ str += 4;
+ return str;
+}
+
+static bool imap_mktime(struct tm *tm, time_t *time_r)
+{
+ *time_r = utc_mktime(tm);
+ if (*time_r != (time_t)-1)
+ return TRUE;
+
+ /* the date is outside valid range for time_t. it might still be
+ technically valid though, so try to handle this case.
+ with 64bit time_t the full 0..9999 year range is valid. */
+ if (tm->tm_year <= 100) {
+ /* too old. time_t can be signed or unsigned, handle
+ both cases. */
+ *time_r = (time_t)-1 < (int)0 ? INT_MIN : 0;
+ } else {
+ /* too high. return the highest allowed value.
+ we shouldn't get here with 64bit time_t,
+ but handle that anyway. */
+#if (TIME_T_MAX_BITS == 32 || TIME_T_MAX_BITS == 64)
+ *time_r = (1UL << (TIME_T_MAX_BITS-1)) - 1;
+#else
+ *time_r = (1UL << TIME_T_MAX_BITS) - 1;
+#endif
+ }
+ return FALSE;
+}
+
+bool imap_parse_date(const char *str, time_t *timestamp_r)
+{
+ struct tm tm;
+
+ str = imap_parse_date_internal(str, &tm);
+ if (str == NULL || str[0] != '\0')
+ return FALSE;
+
+ tm.tm_isdst = -1;
+ (void)imap_mktime(&tm, timestamp_r);
+ return TRUE;
+}
+
+bool imap_parse_datetime(const char *str, time_t *timestamp_r,
+ int *timezone_offset_r)
+{
+ struct tm tm;
+
+ str = imap_parse_date_internal(str, &tm);
+ if (str == NULL)
+ return FALSE;
+
+ if (str[0] != ' ')
+ return FALSE;
+ str++;
+
+ /* hh: */
+ if (!i_isdigit(str[0]) || !i_isdigit(str[1]) || str[2] != ':')
+ return FALSE;
+ tm.tm_hour = (str[0]-'0') * 10 + (str[1]-'0');
+ str += 3;
+
+ /* mm: */
+ if (!i_isdigit(str[0]) || !i_isdigit(str[1]) || str[2] != ':')
+ return FALSE;
+ tm.tm_min = (str[0]-'0') * 10 + (str[1]-'0');
+ str += 3;
+
+ /* ss */
+ if (!i_isdigit(str[0]) || !i_isdigit(str[1]) || str[2] != ' ')
+ return FALSE;
+ tm.tm_sec = (str[0]-'0') * 10 + (str[1]-'0');
+ str += 3;
+
+ /* timezone */
+ *timezone_offset_r = parse_timezone(str);
+
+ tm.tm_isdst = -1;
+ if (imap_mktime(&tm, timestamp_r))
+ *timestamp_r -= *timezone_offset_r * 60;
+ return TRUE;
+}
+
+static void imap_to_date_tm(char buf[11], const struct tm *tm)
+{
+ int year;
+
+ /* dd-mon- */
+ buf[0] = (tm->tm_mday / 10) + '0';
+ buf[1] = (tm->tm_mday % 10) + '0';
+ buf[2] = '-';
+ memcpy(buf+3, month_names[tm->tm_mon], 3);
+ buf[6] = '-';
+
+ /* yyyy */
+ year = tm->tm_year + 1900;
+ buf[7] = (year / 1000) + '0';
+ buf[8] = ((year / 100) % 10) + '0';
+ buf[9] = ((year / 10) % 10) + '0';
+ buf[10] = (year % 10) + '0';
+}
+
+static const char *
+imap_to_datetime_tm(const struct tm *tm, int timezone_offset)
+{
+ char *buf;
+
+ /* @UNSAFE: but faster than t_strdup_printf() call.. */
+ buf = t_malloc0(27);
+ imap_to_date_tm(buf, tm);
+ buf[11] = ' ';
+
+ /* hh:mi:ss */
+ buf[12] = (tm->tm_hour / 10) + '0';
+ buf[13] = (tm->tm_hour % 10) + '0';
+ buf[14] = ':';
+ buf[15] = (tm->tm_min / 10) + '0';
+ buf[16] = (tm->tm_min % 10) + '0';
+ buf[17] = ':';
+ buf[18] = (tm->tm_sec / 10) + '0';
+ buf[19] = (tm->tm_sec % 10) + '0';
+ buf[20] = ' ';
+
+ /* timezone */
+ if (timezone_offset >= 0)
+ buf[21] = '+';
+ else {
+ buf[21] = '-';
+ timezone_offset = -timezone_offset;
+ }
+ buf[22] = (timezone_offset / 600) + '0';
+ buf[23] = ((timezone_offset / 60) % 10) + '0';
+ buf[24] = ((timezone_offset % 60) / 10) + '0';
+ buf[25] = (timezone_offset % 10) + '0';
+ buf[26] = '\0';
+
+ return buf;
+}
+
+const char *imap_to_datetime(time_t timestamp)
+{
+ struct tm *tm;
+ int timezone_offset;
+
+ tm = localtime(&timestamp);
+ timezone_offset = utc_offset(tm, timestamp);
+ return imap_to_datetime_tm(tm, timezone_offset);
+}
+
+const char *imap_to_datetime_tz(time_t timestamp, int timezone_offset)
+{
+ const struct tm *tm;
+ time_t adjusted = timestamp + timezone_offset*60;
+
+ tm = gmtime(&adjusted);
+ return imap_to_datetime_tm(tm, timezone_offset);
+}
+
+bool imap_to_date(time_t timestamp, const char **str_r)
+{
+ const struct tm *tm;
+ char *buf;
+
+ tm = localtime(&timestamp);
+
+ buf = t_malloc0(12);
+ imap_to_date_tm(buf, tm);
+ *str_r = buf;
+ return tm->tm_hour == 0 && tm->tm_min == 0 && tm->tm_sec == 0;
+}
diff --git a/src/lib-imap/imap-date.h b/src/lib-imap/imap-date.h
new file mode 100644
index 0000000..b8ea982
--- /dev/null
+++ b/src/lib-imap/imap-date.h
@@ -0,0 +1,25 @@
+#ifndef IMAP_DATE_H
+#define IMAP_DATE_H
+
+/* Parses IMAP date/time string and returns TRUE if the string is valid.
+ time_t is filled with UTC date. timezone_offset is filled with parsed
+ timezone. If no timezone is given, local timezone is assumed.
+
+ If date is too low or too high to fit to time_t, it's set to lowest/highest
+ allowed value. This allows you to use the value directly for comparing
+ timestamps. */
+bool imap_parse_date(const char *str, time_t *timestamp_r);
+bool imap_parse_datetime(const char *str, time_t *timestamp_r,
+ int *timezone_offset_r);
+
+/* Returns given UTC timestamp as IMAP date-time string in local timezone. */
+const char *imap_to_datetime(time_t timestamp);
+/* Returns given UTC timestamp as IMAP date-time string in given timezone. */
+const char *imap_to_datetime_tz(time_t timestamp, int timezone_offset);
+
+/* Returns TRUE if timestamp was successfully converted to a date,
+ FALSE if time would have been required as well (but the string is still
+ returned). */
+bool imap_to_date(time_t timestamp, const char **str_r);
+
+#endif
diff --git a/src/lib-imap/imap-envelope.c b/src/lib-imap/imap-envelope.c
new file mode 100644
index 0000000..87297f4
--- /dev/null
+++ b/src/lib-imap/imap-envelope.c
@@ -0,0 +1,248 @@
+/* Copyright (c) 2002-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "istream.h"
+#include "str.h"
+#include "message-address.h"
+#include "message-part-data.h"
+#include "message-parser.h"
+#include "imap-parser.h"
+#include "imap-envelope.h"
+#include "imap-quote.h"
+
+/*
+ * Envelope write
+ */
+
+static void imap_write_address(string_t *str, struct message_address *addr)
+{
+ if (addr == NULL) {
+ str_append(str, "NIL");
+ return;
+ }
+
+ str_append_c(str, '(');
+ while (addr != NULL) {
+ str_append_c(str, '(');
+ if (addr->name == NULL)
+ str_append(str, "NIL");
+ else {
+ imap_append_string_for_humans(str,
+ (const void *)addr->name, strlen(addr->name));
+ }
+ str_append_c(str, ' ');
+ imap_append_nstring(str, addr->route);
+ str_append_c(str, ' ');
+ imap_append_nstring(str, addr->mailbox);
+ str_append_c(str, ' ');
+ imap_append_nstring(str, addr->domain);
+ str_append_c(str, ')');
+
+ addr = addr->next;
+ }
+ str_append_c(str, ')');
+}
+
+void imap_envelope_write(struct message_part_envelope *data,
+ string_t *str)
+{
+#define NVL(str, nullstr) ((str) != NULL ? (str) : (nullstr))
+ static const char *empty_envelope =
+ "NIL NIL NIL NIL NIL NIL NIL NIL NIL NIL";
+
+ if (data == NULL) {
+ str_append(str, empty_envelope);
+ return;
+ }
+
+ imap_append_nstring_nolf(str, data->date);
+ str_append_c(str, ' ');
+ if (data->subject == NULL)
+ str_append(str, "NIL");
+ else {
+ imap_append_string_for_humans(str,
+ (const unsigned char *)data->subject,
+ strlen(data->subject));
+ }
+
+ str_append_c(str, ' ');
+ imap_write_address(str, data->from);
+ str_append_c(str, ' ');
+ imap_write_address(str, NVL(data->sender, data->from));
+ str_append_c(str, ' ');
+ imap_write_address(str, NVL(data->reply_to, data->from));
+ str_append_c(str, ' ');
+ imap_write_address(str, data->to);
+ str_append_c(str, ' ');
+ imap_write_address(str, data->cc);
+ str_append_c(str, ' ');
+ imap_write_address(str, data->bcc);
+
+ str_append_c(str, ' ');
+ imap_append_nstring_nolf(str, data->in_reply_to);
+ str_append_c(str, ' ');
+ imap_append_nstring_nolf(str, data->message_id);
+}
+
+/*
+ * ENVELOPE parsing
+ */
+
+static bool
+imap_envelope_parse_address(const struct imap_arg *arg,
+ pool_t pool, struct message_address **addr_r)
+{
+ struct message_address *addr;
+ const struct imap_arg *list_args;
+ const char *name, *route, *mailbox, *domain;
+ unsigned int list_count;
+
+ if (!imap_arg_get_list_full(arg, &list_args, &list_count))
+ return FALSE;
+
+ /* we require 4 arguments, strings or NILs */
+ if (list_count < 4)
+ return FALSE;
+
+ if (!imap_arg_get_nstring(&list_args[0], &name))
+ return FALSE;
+ if (!imap_arg_get_nstring(&list_args[1], &route))
+ return FALSE;
+ if (!imap_arg_get_nstring(&list_args[2], &mailbox))
+ return FALSE;
+ if (!imap_arg_get_nstring(&list_args[3], &domain))
+ return FALSE;
+
+ addr = p_new(pool, struct message_address, 1);
+ addr->name = p_strdup(pool, name);
+ addr->route = p_strdup(pool, route);
+ addr->mailbox = p_strdup(pool, mailbox);
+ addr->domain = p_strdup(pool, domain);
+
+ *addr_r = addr;
+ return TRUE;
+}
+
+static bool
+imap_envelope_parse_addresses(const struct imap_arg *arg,
+ pool_t pool, struct message_address **addrs_r)
+{
+ struct message_address *first, *addr, *prev;
+ const struct imap_arg *list_args;
+
+ if (arg->type == IMAP_ARG_NIL) {
+ *addrs_r = NULL;
+ return TRUE;
+ }
+
+ if (!imap_arg_get_list(arg, &list_args))
+ return FALSE;
+
+ first = addr = prev = NULL;
+ for (; !IMAP_ARG_IS_EOL(list_args); list_args++) {
+ if (!imap_envelope_parse_address
+ (list_args, pool, &addr))
+ return FALSE;
+ if (first == NULL)
+ first = addr;
+ if (prev != NULL)
+ prev->next = addr;
+ prev = addr;
+ }
+
+ *addrs_r = first;
+ return TRUE;
+}
+
+bool imap_envelope_parse_args(const struct imap_arg *args,
+ pool_t pool, struct message_part_envelope **envlp_r,
+ const char **error_r)
+{
+ struct message_part_envelope *envlp;
+
+ envlp = p_new(pool, struct message_part_envelope, 1);
+
+ if (!imap_arg_get_nstring(args++, &envlp->date)) {
+ *error_r = "Invalid date field";
+ return FALSE;
+ }
+ envlp->date = p_strdup(pool, envlp->date);
+
+ if (!imap_arg_get_nstring(args++, &envlp->subject)) {
+ *error_r = "Invalid subject field";
+ return FALSE;
+ }
+ envlp->subject = p_strdup(pool, envlp->subject);
+
+ if (!imap_envelope_parse_addresses(args++, pool, &envlp->from)) {
+ *error_r = "Invalid from field";
+ return FALSE;
+ }
+ if (!imap_envelope_parse_addresses(args++, pool, &envlp->sender)) {
+ *error_r = "Invalid sender field";
+ return FALSE;
+ }
+ if (!imap_envelope_parse_addresses(args++, pool, &envlp->reply_to)) {
+ *error_r = "Invalid reply_to field";
+ return FALSE;
+ }
+ if (!imap_envelope_parse_addresses(args++, pool, &envlp->to)) {
+ *error_r = "Invalid to field";
+ return FALSE;
+ }
+ if (!imap_envelope_parse_addresses(args++, pool, &envlp->cc)) {
+ *error_r = "Invalid cc field";
+ return FALSE;
+ }
+ if (!imap_envelope_parse_addresses(args++, pool, &envlp->bcc)) {
+ *error_r = "Invalid bcc field";
+ return FALSE;
+ }
+
+ if (!imap_arg_get_nstring(args++, &envlp->in_reply_to)) {
+ *error_r = "Invalid in_reply_to field";
+ return FALSE;
+ }
+ envlp->in_reply_to = p_strdup(pool, envlp->in_reply_to);
+
+ if (!imap_arg_get_nstring(args++, &envlp->message_id)) {
+ *error_r = "Invalid message_id field";
+ return FALSE;
+ }
+ envlp->message_id = p_strdup(pool, envlp->message_id);
+
+ *envlp_r = envlp;
+ return TRUE;
+}
+
+bool imap_envelope_parse(const char *envelope,
+ pool_t pool, struct message_part_envelope **envlp_r,
+ const char **error_r)
+{
+ struct istream *input;
+ struct imap_parser *parser;
+ const struct imap_arg *args;
+ int ret;
+ bool success;
+
+ input = i_stream_create_from_data(envelope, strlen(envelope));
+ (void)i_stream_read(input);
+
+ parser = imap_parser_create(input, NULL, SIZE_MAX);
+ ret = imap_parser_finish_line(parser, 0,
+ IMAP_PARSE_FLAG_LITERAL_TYPE, &args);
+ if (ret < 0) {
+ *error_r = t_strdup_printf("IMAP parser failed: %s",
+ imap_parser_get_error(parser, NULL));
+ success = FALSE;
+ } else if (ret == 0) {
+ *error_r = "Empty envelope";
+ success = FALSE;
+ } else {
+ success = imap_envelope_parse_args(args, pool, envlp_r, error_r);
+ }
+
+ imap_parser_unref(&parser);
+ i_stream_destroy(&input);
+ return success;
+}
diff --git a/src/lib-imap/imap-envelope.h b/src/lib-imap/imap-envelope.h
new file mode 100644
index 0000000..f9e4c0b
--- /dev/null
+++ b/src/lib-imap/imap-envelope.h
@@ -0,0 +1,20 @@
+#ifndef IMAP_ENVELOPE_H
+#define IMAP_ENVELOPE_H
+
+struct imap_arg;
+struct message_part_envelope;
+
+/* Write envelope to given string */
+void imap_envelope_write(struct message_part_envelope *data,
+ string_t *str);
+
+/* Parse envelope from arguments */
+bool imap_envelope_parse_args(const struct imap_arg *args,
+ pool_t pool, struct message_part_envelope **envlp_r,
+ const char **error_r);
+/* Parse envelope from string */
+bool imap_envelope_parse(const char *envelope,
+ pool_t pool, struct message_part_envelope **envlp_r,
+ const char **error_r);
+
+#endif
diff --git a/src/lib-imap/imap-id.c b/src/lib-imap/imap-id.c
new file mode 100644
index 0000000..f873c57
--- /dev/null
+++ b/src/lib-imap/imap-id.c
@@ -0,0 +1,173 @@
+/* Copyright (c) 2008-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "str.h"
+#include "str-sanitize.h"
+#include "istream.h"
+#include "imap-parser.h"
+#include "imap-quote.h"
+#include "imap-id.h"
+#include "dovecot-version.h"
+
+#ifdef HAVE_SYS_UTSNAME_H
+# include <sys/utsname.h>
+#endif
+
+#ifdef HAVE_UNAME
+static struct utsname utsname_result;
+static bool utsname_set = FALSE;
+
+static const char *imap_id_get_uname(const char *key)
+{
+ if (!utsname_set) {
+ utsname_set = TRUE;
+ if (uname(&utsname_result) < 0) {
+ i_error("uname() failed: %m");
+ i_zero(&utsname_result);
+ }
+ }
+
+ if (strcasecmp(key, "os") == 0)
+ return utsname_result.sysname;
+ if (strcasecmp(key, "os-version") == 0)
+ return utsname_result.release;
+ return NULL;
+}
+#endif
+
+static const char *imap_id_get_default(const char *key)
+{
+ if (strcasecmp(key, "name") == 0)
+ return PACKAGE_NAME;
+ if (strcasecmp(key, "version") == 0)
+ return PACKAGE_VERSION;
+ if (strcasecmp(key, "revision") == 0)
+ return DOVECOT_REVISION;
+ if (strcasecmp(key, "support-url") == 0)
+ return PACKAGE_WEBPAGE;
+ if (strcasecmp(key, "support-email") == 0)
+ return PACKAGE_BUGREPORT;
+#ifdef HAVE_UNAME
+ return imap_id_get_uname(key);
+#endif
+}
+
+static const char *
+imap_id_reply_generate_from_imap_args(const struct imap_arg *args)
+{
+ string_t *str;
+ const char *key, *value;
+
+ if (IMAP_ARG_IS_EOL(args))
+ return "NIL";
+
+ str = t_str_new(256);
+ str_append_c(str, '(');
+ for (; !IMAP_ARG_IS_EOL(args); args++) {
+ if (!imap_arg_get_astring(args, &key)) {
+ /* broken input */
+ if (IMAP_ARG_IS_EOL(&args[1]))
+ break;
+ args++;
+ } else {
+ /* key */
+ if (str_len(str) > 1)
+ str_append_c(str, ' ');
+ imap_append_quoted(str, key);
+ str_append_c(str, ' ');
+ /* value */
+ if (IMAP_ARG_IS_EOL(&args[1])) {
+ str_append(str, "NIL");
+ break;
+ }
+ args++;
+ if (!imap_arg_get_astring(args, &value))
+ value = NULL;
+ else {
+ if (strcmp(value, "*") == 0)
+ value = imap_id_get_default(key);
+ }
+ imap_append_nstring(str, value);
+ }
+ }
+ if (str_len(str) == 1) {
+ /* broken */
+ return "NIL";
+ }
+ str_append_c(str, ')');
+ return str_c(str);
+}
+
+const char *imap_id_reply_generate(const char *settings)
+{
+ struct istream *input;
+ struct imap_parser *parser;
+ const struct imap_arg *args;
+ const char *ret;
+
+ if (settings == NULL)
+ return "NIL";
+
+ input = i_stream_create_from_data(settings, strlen(settings));
+ (void)i_stream_read(input);
+
+ parser = imap_parser_create(input, NULL, SIZE_MAX);
+ if (imap_parser_finish_line(parser, 0, 0, &args) <= 0)
+ ret = "NIL";
+ else
+ ret = imap_id_reply_generate_from_imap_args(args);
+
+ imap_parser_unref(&parser);
+ i_stream_destroy(&input);
+ return ret;
+}
+
+void imap_id_log_reply_append(string_t *reply, const char *key,
+ const char *value)
+{
+ if (str_len(reply) > 0)
+ str_append(reply, ", ");
+ str_append(reply, str_sanitize(key, IMAP_ID_KEY_MAX_LEN));
+ str_append_c(reply, '=');
+ str_append(reply, value == NULL ? "NIL" : str_sanitize(value, 80));
+}
+
+const char *imap_id_args_get_log_reply(const struct imap_arg *args,
+ const char *settings)
+{
+ const char *const *keys, *key, *value;
+ string_t *reply;
+ bool log_all;
+
+ if (settings == NULL || *settings == '\0')
+ return NULL;
+ if (!imap_arg_get_list(args, &args))
+ return NULL;
+
+ log_all = strcmp(settings, "*") == 0;
+ reply = t_str_new(256);
+ keys = t_strsplit_spaces(settings, " ");
+ while (!IMAP_ARG_IS_EOL(&args[0]) &&
+ !IMAP_ARG_IS_EOL(&args[1])) {
+ if (!imap_arg_get_string(args, &key)) {
+ /* broken input */
+ args += 2;
+ continue;
+ }
+ args++;
+ if (strlen(key) > 30) {
+ /* broken: ID spec requires fields to be max. 30
+ octets */
+ args++;
+ continue;
+ }
+
+ if (log_all || str_array_icase_find(keys, key)) {
+ if (!imap_arg_get_nstring(args, &value))
+ value = "";
+ imap_id_log_reply_append(reply, key, value);
+ }
+ args++;
+ }
+ return str_len(reply) == 0 ? NULL : str_c(reply);
+}
diff --git a/src/lib-imap/imap-id.h b/src/lib-imap/imap-id.h
new file mode 100644
index 0000000..853153a
--- /dev/null
+++ b/src/lib-imap/imap-id.h
@@ -0,0 +1,19 @@
+#ifndef IMAP_ID_H
+#define IMAP_ID_H
+
+struct imap_arg;
+
+/* RFC 2971 says keys are max. 30 octets */
+#define IMAP_ID_KEY_MAX_LEN 30
+
+/* Return ID reply based on given settings. */
+const char *imap_id_reply_generate(const char *settings);
+/* Return a line that should be logged based on given args and settings.
+ Returns NULL if nothing should be logged. */
+const char *imap_id_args_get_log_reply(const struct imap_arg *args,
+ const char *settings);
+/* Append [, ]key=value to the reply sanitized. */
+void imap_id_log_reply_append(string_t *reply, const char *key,
+ const char *value);
+
+#endif
diff --git a/src/lib-imap/imap-keepalive.c b/src/lib-imap/imap-keepalive.c
new file mode 100644
index 0000000..0756ce8
--- /dev/null
+++ b/src/lib-imap/imap-keepalive.c
@@ -0,0 +1,47 @@
+/* Copyright (c) 2002-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "crc32.h"
+#include "net.h"
+#include "imap-keepalive.h"
+
+#include <time.h>
+
+static bool imap_remote_ip_is_usable(const struct ip_addr *ip)
+{
+ unsigned int addr;
+
+ if (ip->family == 0)
+ return FALSE;
+ if (ip->family == AF_INET) {
+#define IP4(a,b,c,d) ((unsigned)(a)<<24|(unsigned)(b)<<16|(unsigned)(c)<<8|(unsigned)(d))
+ addr = ip->u.ip4.s_addr;
+ if (addr >= IP4(10,0,0,0) && addr <= IP4(10,255,255,255))
+ return FALSE; /* 10/8 */
+ if (addr >= IP4(192,168,0,0) && addr <= IP4(192,168,255,255))
+ return FALSE; /* 192.168/16 */
+ if (addr >= IP4(172,16,0,0) && addr <= IP4(172,31,255,255))
+ return FALSE; /* 172.16/12 */
+ if (addr >= IP4(127,0,0,0) && addr <= IP4(127,255,255,255))
+ return FALSE; /* 127/8 */
+#undef IP4
+ }
+ else if (ip->family == AF_INET6) {
+ addr = ip->u.ip6.s6_addr[0];
+ if (addr == 0xfc || addr == 0xfd)
+ return FALSE; /* fc00::/7 */
+ }
+ return TRUE;
+}
+
+unsigned int
+imap_keepalive_interval_msecs(const char *username, const struct ip_addr *ip,
+ unsigned int interval_secs)
+{
+ unsigned int client_hash;
+
+ client_hash = ip != NULL && imap_remote_ip_is_usable(ip) ?
+ net_ip_hash(ip) : crc32_str(username);
+ interval_secs -= (time(NULL) + client_hash) % interval_secs;
+ return interval_secs * 1000;
+}
diff --git a/src/lib-imap/imap-keepalive.h b/src/lib-imap/imap-keepalive.h
new file mode 100644
index 0000000..238cdbd
--- /dev/null
+++ b/src/lib-imap/imap-keepalive.h
@@ -0,0 +1,24 @@
+#ifndef IMAP_KEEPALIVE_H
+#define IMAP_KEEPALIVE_H
+
+/* This function can be used to set IMAP IDLE keepalive notification timeout
+ interval so that the client gets the keepalive notifications at exactly the
+ same time for all the IMAP connections. This helps to reduce battery usage
+ in mobile devices.
+
+ One problem with this is that we don't really want to send the notifications
+ to everyone at the same time, because it would cause huge peaks of activity.
+ Basing the notifications on the username works well for one account, but
+ basing it on the IP address allows the client to get all of the
+ notifications at the same time for multiple accounts as well (of course
+ assuming Dovecot is running on all the servers :)
+
+ One potential downside to using IP is that if a proxy hides the client's IP
+ address, the notifications are sent to everyone at the same time. This can
+ be avoided by using a properly configured Dovecot proxy, but we'll also try
+ to avoid this by not doing it for the commonly used intranet IP ranges. */
+unsigned int
+imap_keepalive_interval_msecs(const char *username, const struct ip_addr *ip,
+ unsigned int interval_secs);
+
+#endif
diff --git a/src/lib-imap/imap-match.c b/src/lib-imap/imap-match.c
new file mode 100644
index 0000000..8603e00
--- /dev/null
+++ b/src/lib-imap/imap-match.c
@@ -0,0 +1,382 @@
+/* Copyright (c) 2002-2018 Dovecot authors, see the included COPYING file */
+
+/* imap_match_init() logic originates from Cyrus, but the code is fully
+ rewritten. */
+
+#include "lib.h"
+#include "array.h"
+#include "imap-match.h"
+
+#include <ctype.h>
+
+struct imap_match_pattern {
+ const char *pattern;
+ bool inboxcase;
+};
+
+struct imap_match_glob {
+ pool_t pool;
+
+ struct imap_match_pattern *patterns;
+
+ char sep;
+ char patterns_data[FLEXIBLE_ARRAY_MEMBER];
+};
+
+struct imap_match_context {
+ const char *inboxcase_end;
+
+ char sep;
+ bool inboxcase;
+};
+
+/* name of "INBOX" - must not have repeated substrings */
+static const char inbox[] = "INBOX";
+#define INBOXLEN (sizeof(inbox) - 1)
+
+struct imap_match_glob *
+imap_match_init(pool_t pool, const char *pattern,
+ bool inboxcase, char separator)
+{
+ const char *patterns[2];
+
+ patterns[0] = pattern;
+ patterns[1] = NULL;
+ return imap_match_init_multiple(pool, patterns, inboxcase, separator);
+}
+
+static const char *pattern_compress(const char *pattern)
+{
+ char *dest, *ret;
+
+ dest = ret = t_strdup_noconst(pattern);
+
+ /* @UNSAFE: compress the pattern */
+ while (*pattern != '\0') {
+ if (*pattern == '*' || *pattern == '%') {
+ /* remove duplicate hierarchy wildcards */
+ while (*pattern == '%') pattern++;
+
+ /* "%*" -> "*" */
+ if (*pattern == '*') {
+ /* remove duplicate wildcards */
+ while (*pattern == '*' || *pattern == '%')
+ pattern++;
+ *dest++ = '*';
+ } else {
+ *dest++ = '%';
+ }
+ } else {
+ *dest++ = *pattern++;
+ }
+ }
+ *dest = '\0';
+ return ret;
+}
+
+static bool pattern_is_inboxcase(const char *pattern, char separator)
+{
+ const char *p = pattern, *inboxp = inbox;
+
+ /* skip over exact matches */
+ while (*inboxp == i_toupper(*p) && *p != '\0') {
+ inboxp++; p++;
+ }
+ if (*p != '%') {
+ return *p == '*' || *p == separator ||
+ (*inboxp == '\0' && *p == '\0');
+ }
+
+ /* handle 'I%B%X' style checks */
+ for (; *p != '\0' && *p != '*' && *p != separator; p++) {
+ if (*p != '%') {
+ inboxp = strchr(inboxp, i_toupper(*p));
+ if (inboxp == NULL)
+ return FALSE;
+
+ if (*++inboxp == '\0') {
+ /* now check that it doesn't end with
+ any invalid chars */
+ if (*++p == '%') p++;
+ if (*p != '\0' && *p != '*' &&
+ *p != separator)
+ return FALSE;
+ break;
+ }
+ }
+ }
+ return TRUE;
+}
+
+static struct imap_match_glob *
+imap_match_init_multiple_real(pool_t pool, const char *const *patterns,
+ bool inboxcase, char separator)
+{
+ struct imap_match_glob *glob;
+ struct imap_match_pattern *match_patterns;
+ unsigned int i, patterns_count;
+ size_t len, pos, patterns_data_len = 0;
+
+ patterns_count = str_array_length(patterns);
+ match_patterns = p_new(pool, struct imap_match_pattern,
+ patterns_count + 1);
+
+ /* compress the patterns */
+ for (i = 0; i < patterns_count; i++) {
+ match_patterns[i].pattern = pattern_compress(patterns[i]);
+ match_patterns[i].inboxcase = inboxcase &&
+ pattern_is_inboxcase(match_patterns[i].pattern,
+ separator);
+
+ patterns_data_len += strlen(match_patterns[i].pattern) + 1;
+ }
+ patterns_count = i;
+
+ /* now we know how much memory we need */
+ glob = p_malloc(pool, sizeof(struct imap_match_glob) +
+ patterns_data_len);
+ glob->pool = pool;
+ glob->sep = separator;
+
+ /* copy pattern strings to our allocated memory */
+ for (i = 0, pos = 0; i < patterns_count; i++) {
+ len = strlen(match_patterns[i].pattern) + 1;
+ i_assert(pos + len <= patterns_data_len);
+
+ /* @UNSAFE */
+ memcpy(glob->patterns_data + pos,
+ match_patterns[i].pattern, len);
+ match_patterns[i].pattern = glob->patterns_data + pos;
+ pos += len;
+ }
+ glob->patterns = match_patterns;
+ return glob;
+}
+
+struct imap_match_glob *
+imap_match_init_multiple(pool_t pool, const char *const *patterns,
+ bool inboxcase, char separator)
+{
+ struct imap_match_glob *glob;
+
+ if (pool->datastack_pool) {
+ return imap_match_init_multiple_real(pool, patterns,
+ inboxcase, separator);
+ }
+ T_BEGIN {
+ glob = imap_match_init_multiple_real(pool, patterns,
+ inboxcase, separator);
+ } T_END;
+ return glob;
+}
+
+void imap_match_deinit(struct imap_match_glob **glob)
+{
+ if (glob == NULL || *glob == NULL)
+ return;
+ p_free((*glob)->pool, (*glob)->patterns);
+ p_free((*glob)->pool, *glob);
+ *glob = NULL;
+}
+
+static struct imap_match_glob *
+imap_match_dup_real(pool_t pool, const struct imap_match_glob *glob)
+{
+ ARRAY_TYPE(const_string) patterns;
+ const struct imap_match_pattern *p;
+ bool inboxcase = FALSE;
+
+ t_array_init(&patterns, 8);
+ for (p = glob->patterns; p->pattern != NULL; p++) {
+ if (p->inboxcase)
+ inboxcase = TRUE;
+ array_push_back(&patterns, &p->pattern);
+ }
+ array_append_zero(&patterns);
+ return imap_match_init_multiple_real(pool, array_front(&patterns),
+ inboxcase, glob->sep);
+}
+
+struct imap_match_glob *
+imap_match_dup(pool_t pool, const struct imap_match_glob *glob)
+{
+ struct imap_match_glob *new_glob;
+
+ if (pool->datastack_pool) {
+ return imap_match_dup_real(pool, glob);
+ } else {
+ T_BEGIN {
+ new_glob = imap_match_dup_real(pool, glob);
+ } T_END;
+ return new_glob;
+ }
+}
+
+bool imap_match_globs_equal(const struct imap_match_glob *glob1,
+ const struct imap_match_glob *glob2)
+{
+ const struct imap_match_pattern *p1 = glob1->patterns;
+ const struct imap_match_pattern *p2 = glob2->patterns;
+
+ if (glob1->sep != glob2->sep)
+ return FALSE;
+
+ for (; p1->pattern != NULL && p2->pattern != NULL; p1++, p2++) {
+ if (strcmp(p1->pattern, p2->pattern) != 0)
+ return FALSE;
+ if (p1->inboxcase != p2->inboxcase)
+ return FALSE;
+ }
+ return p1->pattern == p2->pattern;
+}
+
+#define CMP_CUR_CHR(ctx, data, pattern) \
+ (*(data) == *(pattern) || \
+ (i_toupper(*(data)) == i_toupper(*(pattern)) && \
+ (data) < (ctx)->inboxcase_end))
+
+static enum imap_match_result
+match_sub(struct imap_match_context *ctx, const char **data_p,
+ const char **pattern_p)
+{
+ enum imap_match_result ret, match;
+ unsigned int i;
+ const char *data = *data_p, *pattern = *pattern_p;
+
+ /* match all non-wildcards */
+ i = 0;
+ while (pattern[i] != '\0' && pattern[i] != '*' && pattern[i] != '%') {
+ if (!CMP_CUR_CHR(ctx, data+i, pattern+i)) {
+ if (data[i] != '\0')
+ return IMAP_MATCH_NO;
+ if (pattern[i] == ctx->sep)
+ return IMAP_MATCH_CHILDREN;
+ if (i > 0 && pattern[i-1] == ctx->sep) {
+ /* data="foo/" pattern = "foo/bar/%" */
+ return IMAP_MATCH_CHILDREN;
+ }
+ return IMAP_MATCH_NO;
+ }
+ i++;
+ }
+ data += i;
+ pattern += i;
+
+ if (*data == '\0' && *data_p != data && data[-1] == ctx->sep &&
+ *pattern != '\0') {
+ /* data="/" pattern="/%..." */
+ match = IMAP_MATCH_CHILDREN;
+ } else {
+ match = IMAP_MATCH_NO;
+ }
+ while (*pattern == '%') {
+ pattern++;
+
+ if (*pattern == '\0') {
+ /* match, if this is the last hierarchy */
+ while (*data != '\0' && *data != ctx->sep)
+ data++;
+ break;
+ }
+
+ /* skip over this hierarchy */
+ while (*data != '\0') {
+ if (CMP_CUR_CHR(ctx, data, pattern)) {
+ ret = match_sub(ctx, &data, &pattern);
+ if (ret == IMAP_MATCH_YES)
+ break;
+
+ match |= ret;
+ }
+
+ if (*data == ctx->sep)
+ break;
+
+ data++;
+ }
+ }
+
+ if (*pattern != '*') {
+ if (*data == '\0' && *pattern != '\0') {
+ if (*pattern == ctx->sep)
+ match |= IMAP_MATCH_CHILDREN;
+ return match;
+ }
+
+ if (*data != '\0') {
+ if (*pattern == '\0' && *data == ctx->sep)
+ match |= IMAP_MATCH_PARENT;
+ return match;
+ }
+ }
+
+ *data_p = data;
+ *pattern_p = pattern;
+ return IMAP_MATCH_YES;
+}
+
+static enum imap_match_result
+imap_match_pattern(struct imap_match_context *ctx,
+ const char *data, const char *pattern)
+{
+ enum imap_match_result ret, match;
+
+ ctx->inboxcase_end = data;
+ if (ctx->inboxcase && strncasecmp(data, inbox, INBOXLEN) == 0 &&
+ (data[INBOXLEN] == '\0' || data[INBOXLEN] == ctx->sep)) {
+ /* data begins with INBOX/, use case-insensitive comparison
+ for it */
+ ctx->inboxcase_end += INBOXLEN;
+ }
+
+ if (*pattern != '*') {
+ /* handle the pattern up to the first '*' */
+ ret = match_sub(ctx, &data, &pattern);
+ if (ret != IMAP_MATCH_YES || *pattern == '\0')
+ return ret;
+ }
+
+ match = IMAP_MATCH_CHILDREN;
+ while (*pattern == '*') {
+ pattern++;
+
+ if (*pattern == '\0')
+ return IMAP_MATCH_YES;
+
+ while (*data != '\0') {
+ if (CMP_CUR_CHR(ctx, data, pattern)) {
+ ret = match_sub(ctx, &data, &pattern);
+ if (ret == IMAP_MATCH_YES)
+ break;
+ match |= ret;
+ }
+
+ data++;
+ }
+ }
+
+ return *data == '\0' && *pattern == '\0' ?
+ IMAP_MATCH_YES : match;
+}
+
+enum imap_match_result
+imap_match(struct imap_match_glob *glob, const char *data)
+{
+ struct imap_match_context ctx;
+ unsigned int i;
+ enum imap_match_result ret, match;
+
+ match = IMAP_MATCH_NO;
+ ctx.sep = glob->sep;
+ for (i = 0; glob->patterns[i].pattern != NULL; i++) {
+ ctx.inboxcase = glob->patterns[i].inboxcase;
+
+ ret = imap_match_pattern(&ctx, data, glob->patterns[i].pattern);
+ if (ret == IMAP_MATCH_YES)
+ return IMAP_MATCH_YES;
+
+ match |= ret;
+ }
+
+ return match;
+}
diff --git a/src/lib-imap/imap-match.h b/src/lib-imap/imap-match.h
new file mode 100644
index 0000000..f2e5b51
--- /dev/null
+++ b/src/lib-imap/imap-match.h
@@ -0,0 +1,42 @@
+#ifndef IMAP_MATCH_H
+#define IMAP_MATCH_H
+
+enum imap_match_result {
+ IMAP_MATCH_NO = 0x00, /* definite non-match */
+ IMAP_MATCH_YES = 0x01, /* match */
+
+ /* YES and NO are returned alone, but CHILDREN and PARENT may be
+ returned both (eg. "foo*bar" vs. "foobar/baz") */
+
+ /* non-match, but its children could match (eg. "box" vs "box/%") */
+ IMAP_MATCH_CHILDREN = 0x02,
+ /* non-match, but one of its parents does match. This should often be
+ handled with YES matches, because when listing for "%" and "box/foo"
+ exists but "box" doesn't, you should still list "box" as
+ (Nonexistent HasChildren) mailbox. */
+ IMAP_MATCH_PARENT = 0x04
+};
+
+struct imap_match_glob;
+
+/* If inboxcase is TRUE, the "INBOX" string at the beginning of line is
+ compared case-insensitively */
+struct imap_match_glob *
+imap_match_init(pool_t pool, const char *pattern,
+ bool inboxcase, char separator);
+struct imap_match_glob *
+imap_match_init_multiple(pool_t pool, const char *const *patterns,
+ bool inboxcase, char separator);
+void imap_match_deinit(struct imap_match_glob **glob);
+
+struct imap_match_glob *
+imap_match_dup(pool_t pool, const struct imap_match_glob *glob);
+/* Returns TRUE if two globs were created with same init() parameters
+ (but inboxcase is ignored if no patterns can match INBOX) */
+bool imap_match_globs_equal(const struct imap_match_glob *glob1,
+ const struct imap_match_glob *glob2);
+
+enum imap_match_result
+imap_match(struct imap_match_glob *glob, const char *data);
+
+#endif
diff --git a/src/lib-imap/imap-parser.c b/src/lib-imap/imap-parser.c
new file mode 100644
index 0000000..2deb75f
--- /dev/null
+++ b/src/lib-imap/imap-parser.c
@@ -0,0 +1,1023 @@
+/* Copyright (c) 2002-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "istream.h"
+#include "ostream.h"
+#include "strescape.h"
+#include "imap-parser.h"
+
+/* We use this macro to read atoms from input. It should probably contain
+ everything some day, but for now we can't handle some input otherwise:
+
+ ']' is required for parsing section (FETCH BODY[])
+ '%', '*' and ']' are valid list-chars for LIST patterns
+ '\' is used in flags */
+#define IS_ATOM_PARSER_INPUT(c) \
+ ((c) == '(' || (c) == ')' || (c) == '{' || \
+ (c) == '"' || (c) <= 32 || (c) == 0x7f)
+
+#define is_linebreak(c) \
+ ((c) == '\r' || (c) == '\n')
+
+#define LIST_INIT_COUNT 7
+
+enum arg_parse_type {
+ ARG_PARSE_NONE = 0,
+ ARG_PARSE_ATOM,
+ ARG_PARSE_STRING,
+ ARG_PARSE_LITERAL,
+ ARG_PARSE_LITERAL8,
+ ARG_PARSE_LITERAL_DATA,
+ ARG_PARSE_LITERAL_DATA_FORCED,
+ ARG_PARSE_TEXT
+};
+
+struct imap_parser {
+ /* permanent */
+ int refcount;
+ pool_t pool;
+ struct istream *input;
+ struct ostream *output;
+ size_t max_line_size;
+ enum imap_parser_flags flags;
+
+ /* reset by imap_parser_reset(): */
+ size_t line_size;
+ ARRAY_TYPE(imap_arg_list) root_list;
+ ARRAY_TYPE(imap_arg_list) *cur_list;
+ struct imap_arg *list_arg;
+
+ enum arg_parse_type cur_type;
+ size_t cur_pos; /* parser position in input buffer */
+ bool cur_resp_text; /* we're parsing [resp-text-code] */
+
+ int str_first_escape; /* ARG_PARSE_STRING: index to first '\' */
+ uoff_t literal_size; /* ARG_PARSE_LITERAL: string size */
+
+ enum imap_parser_error error;
+ const char *error_msg;
+
+ bool literal_minus:1;
+ bool literal_skip_crlf:1;
+ bool literal_nonsync:1;
+ bool literal8:1;
+ bool literal_size_return:1;
+ bool eol:1;
+ bool args_added_extra_eol:1;
+ bool fatal_error:1;
+};
+
+struct imap_parser *
+imap_parser_create(struct istream *input, struct ostream *output,
+ size_t max_line_size)
+{
+ struct imap_parser *parser;
+
+ parser = i_new(struct imap_parser, 1);
+ parser->refcount = 1;
+ parser->pool = pool_alloconly_create(MEMPOOL_GROWING"IMAP parser",
+ 1024);
+ parser->input = input;
+ parser->output = output;
+ parser->max_line_size = max_line_size;
+
+ p_array_init(&parser->root_list, parser->pool, LIST_INIT_COUNT);
+ parser->cur_list = &parser->root_list;
+ return parser;
+}
+
+void imap_parser_ref(struct imap_parser *parser)
+{
+ i_assert(parser->refcount > 0);
+
+ parser->refcount++;
+}
+
+void imap_parser_unref(struct imap_parser **_parser)
+{
+ struct imap_parser *parser = *_parser;
+
+ *_parser = NULL;
+
+ i_assert(parser->refcount > 0);
+ if (--parser->refcount > 0)
+ return;
+
+ pool_unref(&parser->pool);
+ i_free(parser);
+}
+
+void imap_parser_enable_literal_minus(struct imap_parser *parser)
+{
+ parser->literal_minus = TRUE;
+}
+
+void imap_parser_reset(struct imap_parser *parser)
+{
+ p_clear(parser->pool);
+
+ parser->line_size = 0;
+
+ p_array_init(&parser->root_list, parser->pool, LIST_INIT_COUNT);
+ parser->cur_list = &parser->root_list;
+ parser->list_arg = NULL;
+
+ parser->cur_type = ARG_PARSE_NONE;
+ parser->cur_pos = 0;
+ parser->cur_resp_text = FALSE;
+
+ parser->str_first_escape = 0;
+ parser->literal_size = 0;
+
+ parser->error = IMAP_PARSE_ERROR_NONE;
+ parser->error_msg = NULL;
+
+ parser->literal_skip_crlf = FALSE;
+ parser->eol = FALSE;
+ parser->args_added_extra_eol = FALSE;
+ parser->literal_size_return = FALSE;
+}
+
+void imap_parser_set_streams(struct imap_parser *parser, struct istream *input,
+ struct ostream *output)
+{
+ parser->input = input;
+ parser->output = output;
+}
+
+const char *imap_parser_get_error(struct imap_parser *parser,
+ enum imap_parser_error *error_r)
+{
+ if (error_r != NULL)
+ *error_r = parser->error;
+ return parser->error_msg;
+}
+
+/* skip over everything parsed so far, plus the following whitespace */
+static bool imap_parser_skip_to_next(struct imap_parser *parser,
+ const unsigned char **data,
+ size_t *data_size)
+{
+ size_t i;
+
+ for (i = parser->cur_pos; i < *data_size; i++) {
+ if ((*data)[i] != ' ')
+ break;
+ }
+
+ parser->line_size += i;
+ i_stream_skip(parser->input, i);
+ parser->cur_pos = 0;
+
+ *data += i;
+ *data_size -= i;
+ return *data_size > 0;
+}
+
+static struct imap_arg *imap_arg_create(struct imap_parser *parser)
+{
+ struct imap_arg *arg;
+
+ arg = array_append_space(parser->cur_list);
+ arg->parent = parser->list_arg;
+ return arg;
+}
+
+static void imap_parser_open_list(struct imap_parser *parser)
+{
+ parser->list_arg = imap_arg_create(parser);
+ parser->list_arg->type = IMAP_ARG_LIST;
+ p_array_init(&parser->list_arg->_data.list, parser->pool,
+ LIST_INIT_COUNT);
+ parser->cur_list = &parser->list_arg->_data.list;
+
+ parser->cur_type = ARG_PARSE_NONE;
+}
+
+static bool imap_parser_close_list(struct imap_parser *parser)
+{
+ struct imap_arg *arg;
+
+ if (parser->list_arg == NULL) {
+ /* we're not inside list */
+ if ((parser->flags & IMAP_PARSE_FLAG_INSIDE_LIST) != 0) {
+ parser->eol = TRUE;
+ parser->cur_type = ARG_PARSE_NONE;
+ return TRUE;
+ }
+ parser->error_msg = "Unexpected ')'";
+ parser->error = IMAP_PARSE_ERROR_BAD_SYNTAX;
+ return FALSE;
+ }
+
+ arg = imap_arg_create(parser);
+ arg->type = IMAP_ARG_EOL;
+
+ parser->list_arg = parser->list_arg->parent;
+ if (parser->list_arg == NULL) {
+ parser->cur_list = &parser->root_list;
+ } else {
+ parser->cur_list = &parser->list_arg->_data.list;
+ }
+
+ parser->cur_type = ARG_PARSE_NONE;
+ return TRUE;
+}
+
+static char *
+imap_parser_strdup(struct imap_parser *parser,
+ const void *data, size_t len)
+{
+ char *ret;
+
+ ret = p_malloc(parser->pool, len + 1);
+ memcpy(ret, data, len);
+ return ret;
+}
+
+static void imap_parser_save_arg(struct imap_parser *parser,
+ const unsigned char *data, size_t size)
+{
+ struct imap_arg *arg;
+ char *str;
+
+ arg = imap_arg_create(parser);
+
+ switch (parser->cur_type) {
+ case ARG_PARSE_ATOM:
+ case ARG_PARSE_TEXT:
+ if (size == 3 && i_memcasecmp(data, "NIL", 3) == 0) {
+ /* NIL argument. it might be an actual NIL, but if
+ we're reading astring, it's an atom and we can't
+ lose its case. */
+ arg->type = IMAP_ARG_NIL;
+ } else {
+ /* simply save the string */
+ arg->type = IMAP_ARG_ATOM;
+ }
+ arg->_data.str = imap_parser_strdup(parser, data, size);
+ arg->str_len = size;
+ break;
+ case ARG_PARSE_STRING:
+ /* data is quoted and may contain escapes. */
+ i_assert(size > 0);
+
+ arg->type = IMAP_ARG_STRING;
+ str = p_strndup(parser->pool, data+1, size-1);
+
+ /* remove the escapes */
+ if (parser->str_first_escape >= 0 &&
+ (parser->flags & IMAP_PARSE_FLAG_NO_UNESCAPE) == 0)
+ (void)str_unescape(str);
+ arg->_data.str = str;
+ arg->str_len = strlen(str);
+ break;
+ case ARG_PARSE_LITERAL_DATA:
+ if ((parser->flags & IMAP_PARSE_FLAG_LITERAL_SIZE) != 0) {
+ /* save literal size */
+ arg->type = parser->literal_nonsync ?
+ IMAP_ARG_LITERAL_SIZE_NONSYNC :
+ IMAP_ARG_LITERAL_SIZE;
+ arg->_data.literal_size = parser->literal_size;
+ arg->literal8 = parser->literal8;
+ break;
+ }
+ /* fall through */
+ case ARG_PARSE_LITERAL_DATA_FORCED:
+ if ((parser->flags & IMAP_PARSE_FLAG_LITERAL_TYPE) != 0)
+ arg->type = IMAP_ARG_LITERAL;
+ else
+ arg->type = IMAP_ARG_STRING;
+ arg->_data.str = imap_parser_strdup(parser, data, size);
+ arg->literal8 = parser->literal8;
+ arg->str_len = size;
+ break;
+ default:
+ i_unreached();
+ }
+
+ parser->cur_type = ARG_PARSE_NONE;
+}
+
+static bool is_valid_atom_char(struct imap_parser *parser, char chr)
+{
+ const char *error_msg;
+
+ if (IS_ATOM_PARSER_INPUT((unsigned char)chr))
+ error_msg = "Invalid characters in atom";
+ else if ((((unsigned char)chr) & 0x80) != 0)
+ error_msg = "8bit data in atom";
+ else
+ return TRUE;
+
+ if ((parser->flags & IMAP_PARSE_FLAG_ATOM_ALLCHARS) != 0)
+ return TRUE;
+ parser->error = IMAP_PARSE_ERROR_BAD_SYNTAX;
+ parser->error_msg = error_msg;
+ return FALSE;
+}
+
+static bool imap_parser_read_atom(struct imap_parser *parser,
+ const unsigned char *data, size_t data_size)
+{
+ size_t i;
+
+ /* read until we've found space, CR or LF. */
+ for (i = parser->cur_pos; i < data_size; i++) {
+ if (data[i] == ' ' || is_linebreak(data[i])) {
+ imap_parser_save_arg(parser, data, i);
+ break;
+ } else if (data[i] == ')') {
+ if (parser->list_arg != NULL ||
+ (parser->flags & IMAP_PARSE_FLAG_INSIDE_LIST) != 0) {
+ imap_parser_save_arg(parser, data, i);
+ break;
+ } else if ((parser->flags &
+ IMAP_PARSE_FLAG_ATOM_ALLCHARS) == 0) {
+ parser->error = IMAP_PARSE_ERROR_BAD_SYNTAX;
+ parser->error_msg = "Unexpected ')'";
+ return FALSE;
+ }
+ /* assume it's part of the atom */
+ } else if (!is_valid_atom_char(parser, data[i]))
+ return FALSE;
+ }
+
+ parser->cur_pos = i;
+ return parser->cur_type == ARG_PARSE_NONE;
+}
+
+static bool imap_parser_read_string(struct imap_parser *parser,
+ const unsigned char *data, size_t data_size)
+{
+ size_t i;
+
+ /* read until we've found non-escaped ", CR or LF */
+ for (i = parser->cur_pos; i < data_size; i++) {
+ if (data[i] == '"') {
+ imap_parser_save_arg(parser, data, i);
+
+ i++; /* skip the trailing '"' too */
+ break;
+ }
+
+ if (data[i] == '\0') {
+ parser->error = IMAP_PARSE_ERROR_BAD_SYNTAX;
+ parser->error_msg = "NULs not allowed in strings";
+ return FALSE;
+ }
+
+ if (data[i] == '\\') {
+ if (i+1 == data_size) {
+ /* known data ends with '\' - leave it to
+ next time as well if it happens to be \" */
+ break;
+ }
+
+ /* save the first escaped char */
+ if (parser->str_first_escape < 0)
+ parser->str_first_escape = i;
+
+ /* skip the escaped char */
+ i++;
+ }
+
+ /* check linebreaks here, so escaping CR/LF isn't possible.
+ string always ends with '"', so it's an error if we found
+ a linebreak.. */
+ if (is_linebreak(data[i]) &&
+ (parser->flags & IMAP_PARSE_FLAG_MULTILINE_STR) == 0) {
+ parser->error = IMAP_PARSE_ERROR_BAD_SYNTAX;
+ parser->error_msg = "Missing '\"'";
+ return FALSE;
+ }
+ }
+
+ parser->cur_pos = i;
+ return parser->cur_type == ARG_PARSE_NONE;
+}
+
+static bool imap_parser_literal_end(struct imap_parser *parser)
+{
+ if (parser->literal_minus && parser->literal_nonsync &&
+ parser->literal_size > 4096) {
+ parser->error_msg = "Non-synchronizing literal size too large";
+ parser->error = IMAP_PARSE_ERROR_LITERAL_TOO_BIG;
+ return FALSE;
+ }
+
+ if ((parser->flags & IMAP_PARSE_FLAG_LITERAL_SIZE) == 0) {
+ if (parser->line_size >= parser->max_line_size ||
+ parser->literal_size >
+ parser->max_line_size - parser->line_size) {
+ /* too long string, abort. */
+ parser->error = IMAP_PARSE_ERROR_LITERAL_TOO_BIG;
+ parser->error_msg = "Literal size too large";
+ return FALSE;
+ }
+
+ if (parser->output != NULL && !parser->literal_nonsync) {
+ o_stream_nsend(parser->output, "+ OK\r\n", 6);
+ if (o_stream_is_corked(parser->output)) {
+ /* make sure this continuation is sent to the
+ client as soon as possible */
+ o_stream_uncork(parser->output);
+ o_stream_cork(parser->output);
+ }
+ }
+ }
+
+ parser->cur_type = ARG_PARSE_LITERAL_DATA;
+ parser->literal_skip_crlf = TRUE;
+
+ parser->cur_pos = 0;
+ return TRUE;
+}
+
+static bool imap_parser_read_literal(struct imap_parser *parser,
+ const unsigned char *data,
+ size_t data_size)
+{
+ size_t i;
+
+ /* expecting digits + "}" */
+ for (i = parser->cur_pos; i < data_size; i++) {
+ if (data[i] == '}') {
+ parser->line_size += i+1;
+ i_stream_skip(parser->input, i+1);
+ return imap_parser_literal_end(parser);
+ }
+
+ if (parser->literal_nonsync) {
+ parser->error = IMAP_PARSE_ERROR_BAD_SYNTAX;
+ parser->error_msg = "Expecting '}' after '+'";
+ return FALSE;
+ }
+
+ if (data[i] == '+') {
+ parser->literal_nonsync = TRUE;
+ continue;
+ }
+
+ if (data[i] < '0' || data[i] > '9') {
+ parser->error = IMAP_PARSE_ERROR_BAD_SYNTAX;
+ parser->error_msg = "Invalid literal size";
+ return FALSE;
+ }
+
+ if (parser->literal_size >= ((uoff_t)-1 / 10)) {
+ if (parser->literal_size > ((uoff_t)-1 / 10) ||
+ (uoff_t)(data[i] - '0') > ((uoff_t)-1 % 10)) {
+ parser->error = IMAP_PARSE_ERROR_LITERAL_TOO_BIG;
+ parser->error_msg = "Literal size too large";
+ return FALSE;
+ }
+ }
+ parser->literal_size = parser->literal_size * 10 +
+ (data[i] - '0');
+ }
+
+ parser->cur_pos = i;
+ return FALSE;
+}
+
+static bool imap_parser_read_literal_data(struct imap_parser *parser,
+ const unsigned char *data,
+ size_t data_size)
+{
+ if (parser->literal_skip_crlf) {
+ /* skip \r\n or \n, anything else gives an error */
+ if (data_size == 0)
+ return FALSE;
+
+ if (*data == '\r') {
+ parser->line_size++;
+ data++; data_size--;
+ i_stream_skip(parser->input, 1);
+
+ if (data_size == 0)
+ return FALSE;
+ }
+
+ if (*data != '\n') {
+ parser->error = IMAP_PARSE_ERROR_BAD_SYNTAX;
+ parser->error_msg = "Missing LF after literal size";
+ return FALSE;
+ }
+
+ parser->line_size++;
+ data++; data_size--;
+ i_stream_skip(parser->input, 1);
+
+ parser->literal_skip_crlf = FALSE;
+
+ i_assert(parser->cur_pos == 0);
+ }
+
+ if ((parser->flags & IMAP_PARSE_FLAG_LITERAL_SIZE) == 0 ||
+ parser->cur_type == ARG_PARSE_LITERAL_DATA_FORCED) {
+ /* now we just wait until we've read enough data */
+ if (data_size < parser->literal_size)
+ return FALSE;
+ else {
+ imap_parser_save_arg(parser, data,
+ (size_t)parser->literal_size);
+ parser->cur_pos = (size_t)parser->literal_size;
+ return TRUE;
+ }
+ } else {
+ /* we want to save only literal size, not the literal itself. */
+ parser->literal_size_return = TRUE;
+ imap_parser_save_arg(parser, uchar_empty_ptr, 0);
+ return FALSE;
+ }
+}
+
+static bool imap_parser_is_next_resp_text(struct imap_parser *parser)
+{
+ const struct imap_arg *arg;
+
+ if (parser->cur_list != &parser->root_list ||
+ array_count(parser->cur_list) != 1)
+ return FALSE;
+
+ arg = array_front(&parser->root_list);
+ if (arg->type != IMAP_ARG_ATOM)
+ return FALSE;
+
+ return strcasecmp(arg->_data.str, "OK") == 0 ||
+ strcasecmp(arg->_data.str, "NO") == 0 ||
+ strcasecmp(arg->_data.str, "BAD") == 0 ||
+ strcasecmp(arg->_data.str, "BYE") == 0;
+}
+
+static bool imap_parser_is_next_text(struct imap_parser *parser)
+{
+ const struct imap_arg *arg;
+ size_t len;
+
+ if (parser->cur_list != &parser->root_list)
+ return FALSE;
+
+ arg = array_back(&parser->root_list);
+ if (arg->type != IMAP_ARG_ATOM)
+ return FALSE;
+
+ len = strlen(arg->_data.str);
+ return len > 0 && arg->_data.str[len-1] == ']';
+}
+
+static bool imap_parser_read_text(struct imap_parser *parser,
+ const unsigned char *data, size_t data_size)
+{
+ size_t i;
+
+ /* read until end of line */
+ for (i = parser->cur_pos; i < data_size; i++) {
+ if (is_linebreak(data[i])) {
+ imap_parser_save_arg(parser, data, i);
+ break;
+ }
+ }
+ parser->cur_pos = i;
+ return parser->cur_type == ARG_PARSE_NONE;
+}
+
+/* Returns TRUE if argument was fully processed. Also returns TRUE if
+ an argument inside a list was processed. */
+static bool imap_parser_read_arg(struct imap_parser *parser)
+{
+ const unsigned char *data;
+ size_t data_size;
+
+ data = i_stream_get_data(parser->input, &data_size);
+ if (data_size == 0)
+ return FALSE;
+
+ while (parser->cur_type == ARG_PARSE_NONE) {
+ /* we haven't started parsing yet */
+ if (!imap_parser_skip_to_next(parser, &data, &data_size))
+ return FALSE;
+ i_assert(parser->cur_pos == 0);
+
+ if (parser->cur_resp_text &&
+ imap_parser_is_next_text(parser)) {
+ /* we just parsed [resp-text-code] */
+ parser->cur_type = ARG_PARSE_TEXT;
+ break;
+ }
+
+ switch (data[0]) {
+ case '\r':
+ if (data_size == 1) {
+ /* wait for LF */
+ return FALSE;
+ }
+ if (data[1] != '\n') {
+ parser->error = IMAP_PARSE_ERROR_BAD_SYNTAX;
+ parser->error_msg = "CR sent without LF";
+ return FALSE;
+ }
+ /* fall through */
+ case '\n':
+ /* unexpected end of line */
+ if ((parser->flags & IMAP_PARSE_FLAG_INSIDE_LIST) != 0) {
+ parser->error = IMAP_PARSE_ERROR_BAD_SYNTAX;
+ parser->error_msg = "Missing ')'";
+ return FALSE;
+ }
+ parser->eol = TRUE;
+ return FALSE;
+ case '"':
+ parser->cur_type = ARG_PARSE_STRING;
+ parser->str_first_escape = -1;
+ break;
+ case '~':
+ /* This could be either literal8 or atom */
+ if (data_size == 1) {
+ /* wait for the next char */
+ return FALSE;
+ }
+ if (data[1] != '{') {
+ parser->cur_type = ARG_PARSE_ATOM;
+ break;
+ }
+ if ((parser->flags & IMAP_PARSE_FLAG_LITERAL8) == 0) {
+ parser->error = IMAP_PARSE_ERROR_BAD_SYNTAX;
+ parser->error_msg = "literal8 not allowed here";
+ return FALSE;
+ }
+ parser->cur_type = ARG_PARSE_LITERAL8;
+ parser->literal_size = 0;
+ parser->literal_nonsync = FALSE;
+ parser->literal8 = TRUE;
+ break;
+ case '{':
+ parser->cur_type = ARG_PARSE_LITERAL;
+ parser->literal_size = 0;
+ parser->literal_nonsync = FALSE;
+ parser->literal8 = FALSE;
+ break;
+ case '(':
+ imap_parser_open_list(parser);
+ if ((parser->flags & IMAP_PARSE_FLAG_STOP_AT_LIST) != 0) {
+ i_stream_skip(parser->input, 1);
+ return FALSE;
+ }
+ break;
+ case ')':
+ if (!imap_parser_close_list(parser))
+ return FALSE;
+
+ if (parser->list_arg == NULL) {
+ /* end of argument */
+ parser->cur_pos++;
+ return TRUE;
+ }
+ break;
+ default:
+ if (!is_valid_atom_char(parser, data[0]))
+ return FALSE;
+ parser->cur_type = ARG_PARSE_ATOM;
+ break;
+ }
+
+ parser->cur_pos++;
+ }
+
+ i_assert(data_size > 0);
+
+ switch (parser->cur_type) {
+ case ARG_PARSE_ATOM:
+ if (!imap_parser_read_atom(parser, data, data_size))
+ return FALSE;
+ if ((parser->flags & IMAP_PARSE_FLAG_SERVER_TEXT) == 0)
+ break;
+
+ if (imap_parser_is_next_resp_text(parser)) {
+ /* we just parsed OK/NO/BAD/BYE. after parsing the
+ [resp-text-code] the rest of the message can contain
+ pretty much any random text, which we can't parse
+ as if it was valid IMAP input */
+ parser->cur_resp_text = TRUE;
+ }
+ break;
+ case ARG_PARSE_STRING:
+ if (!imap_parser_read_string(parser, data, data_size))
+ return FALSE;
+ break;
+ case ARG_PARSE_LITERAL8:
+ if (parser->cur_pos == data_size)
+ return FALSE;
+ if (data[parser->cur_pos] != '{') {
+ parser->error = IMAP_PARSE_ERROR_BAD_SYNTAX;
+ parser->error_msg = "Expected '{'";
+ return FALSE;
+ }
+ parser->cur_type = ARG_PARSE_LITERAL;
+ parser->cur_pos++;
+ /* fall through */
+ case ARG_PARSE_LITERAL:
+ if (!imap_parser_read_literal(parser, data, data_size))
+ return FALSE;
+
+ /* pass through to parsing data. since input->skip was
+ modified, we need to get the data start position again. */
+ data = i_stream_get_data(parser->input, &data_size);
+
+ /* fall through */
+ case ARG_PARSE_LITERAL_DATA:
+ case ARG_PARSE_LITERAL_DATA_FORCED:
+ if (!imap_parser_read_literal_data(parser, data, data_size))
+ return FALSE;
+ break;
+ case ARG_PARSE_TEXT:
+ if (!imap_parser_read_text(parser, data, data_size))
+ return FALSE;
+ break;
+ default:
+ i_unreached();
+ }
+
+ i_assert(parser->cur_type == ARG_PARSE_NONE);
+ return TRUE;
+}
+
+static void list_add_ghost_eol(struct imap_arg *list_arg)
+{
+ struct imap_arg *arg;
+
+ i_assert(list_arg->type == IMAP_ARG_LIST);
+
+ arg = array_append_space(&list_arg->_data.list);
+ arg->type = IMAP_ARG_EOL;
+ array_pop_back(&list_arg->_data.list);
+
+ if (list_arg->parent != NULL)
+ list_add_ghost_eol(list_arg->parent);
+}
+
+/* ARG_PARSE_NONE checks that last argument isn't only partially parsed. */
+#define IS_UNFINISHED(parser) \
+ ((parser)->cur_type != ARG_PARSE_NONE || \
+ ((parser)->cur_list != &parser->root_list && \
+ ((parser)->flags & IMAP_PARSE_FLAG_STOP_AT_LIST) == 0))
+
+static int finish_line(struct imap_parser *parser, unsigned int count,
+ const struct imap_arg **args_r)
+{
+ struct imap_arg *arg;
+ int ret = array_count(&parser->root_list);
+
+ parser->line_size += parser->cur_pos;
+ i_stream_skip(parser->input, parser->cur_pos);
+ parser->cur_pos = 0;
+ parser->cur_resp_text = FALSE;
+
+ if (parser->list_arg == NULL) {
+ /* no open list */
+ } else if (!parser->literal_size_return &&
+ (parser->flags & IMAP_PARSE_FLAG_STOP_AT_LIST) == 0) {
+ parser->error = IMAP_PARSE_ERROR_BAD_SYNTAX;
+ parser->error_msg = "Missing ')'";
+ *args_r = NULL;
+ return -1;
+ } else {
+ list_add_ghost_eol(parser->list_arg);
+ }
+
+ arg = array_append_space(&parser->root_list);
+ arg->type = IMAP_ARG_EOL;
+ parser->args_added_extra_eol = TRUE;
+
+ *args_r = array_get(&parser->root_list, &count);
+ return ret;
+}
+
+static void imap_parser_delete_extra_eol(struct imap_parser *parser)
+{
+ array_pop_back(&parser->root_list);
+ parser->args_added_extra_eol = FALSE;
+}
+
+int imap_parser_read_args(struct imap_parser *parser, unsigned int count,
+ enum imap_parser_flags flags,
+ const struct imap_arg **args_r)
+{
+ parser->flags = flags;
+
+ if (parser->args_added_extra_eol) {
+ /* delete EOL */
+ imap_parser_delete_extra_eol(parser);
+ parser->literal_size_return = FALSE;
+ }
+
+ while (!parser->eol && (count == 0 || IS_UNFINISHED(parser) ||
+ array_count(&parser->root_list) < count)) {
+ if (!imap_parser_read_arg(parser))
+ break;
+
+ if (parser->line_size > parser->max_line_size) {
+ parser->error = IMAP_PARSE_ERROR_LINE_TOO_LONG;
+ parser->error_msg = "IMAP command line too large";
+ break;
+ }
+ }
+
+ if (parser->error != IMAP_PARSE_ERROR_NONE) {
+ /* error, abort */
+ parser->line_size += parser->cur_pos;
+ i_stream_skip(parser->input, parser->cur_pos);
+ parser->cur_pos = 0;
+ *args_r = NULL;
+ return -1;
+ } else if ((!IS_UNFINISHED(parser) && count > 0 &&
+ array_count(&parser->root_list) >= count) ||
+ parser->eol || parser->literal_size_return) {
+ /* all arguments read / end of line. */
+ return finish_line(parser, count, args_r);
+ } else {
+ /* need more data */
+ *args_r = NULL;
+ return -2;
+ }
+}
+
+static struct imap_arg *
+imap_parser_get_last_literal_size(struct imap_parser *parser,
+ ARRAY_TYPE(imap_arg_list) **list_r)
+{
+ ARRAY_TYPE(imap_arg_list) *list;
+ struct imap_arg *args;
+ unsigned int count;
+
+ list = &parser->root_list;
+ args = array_get_modifiable(&parser->root_list, &count);
+ i_assert(count > 1 && args[count-1].type == IMAP_ARG_EOL);
+ count--;
+
+ while (args[count-1].type != IMAP_ARG_LITERAL_SIZE &&
+ args[count-1].type != IMAP_ARG_LITERAL_SIZE_NONSYNC) {
+ if (args[count-1].type != IMAP_ARG_LIST)
+ return NULL;
+
+ /* maybe the list ends with literal size */
+ list = &args[count-1]._data.list;
+ args = array_get_modifiable(list, &count);
+ if (count == 0)
+ return NULL;
+ }
+
+ *list_r = list;
+ return &args[count-1];
+}
+
+bool imap_parser_get_literal_size(struct imap_parser *parser, uoff_t *size_r)
+{
+ ARRAY_TYPE(imap_arg_list) *list;
+ struct imap_arg *last_arg;
+
+ last_arg = imap_parser_get_last_literal_size(parser, &list);
+ if (last_arg == NULL)
+ return FALSE;
+
+ return imap_arg_get_literal_size(last_arg, size_r);
+}
+
+void imap_parser_read_last_literal(struct imap_parser *parser)
+{
+ ARRAY_TYPE(imap_arg_list) *list;
+ struct imap_arg *last_arg;
+
+ i_assert(parser->literal_size_return);
+ i_assert(parser->args_added_extra_eol);
+
+ last_arg = imap_parser_get_last_literal_size(parser, &list);
+ i_assert(last_arg != NULL);
+
+ parser->cur_type = ARG_PARSE_LITERAL_DATA_FORCED;
+ i_assert(parser->literal_size == last_arg->_data.literal_size);
+
+ /* delete EOL */
+ imap_parser_delete_extra_eol(parser);
+
+ /* delete literal size */
+ array_pop_back(list);
+ parser->literal_size_return = FALSE;
+}
+
+int imap_parser_finish_line(struct imap_parser *parser, unsigned int count,
+ enum imap_parser_flags flags,
+ const struct imap_arg **args_r)
+{
+ const unsigned char *data;
+ size_t data_size;
+ int ret;
+
+ ret = imap_parser_read_args(parser, count, flags, args_r);
+ if (ret == -1)
+ return -1;
+ if (ret == -2) {
+ /* we should have noticed end of everything except atom */
+ if (parser->cur_type == ARG_PARSE_ATOM) {
+ data = i_stream_get_data(parser->input, &data_size);
+ imap_parser_save_arg(parser, data, data_size);
+ }
+ }
+ return finish_line(parser, count, args_r);
+}
+
+const char *imap_parser_read_word(struct imap_parser *parser)
+{
+ const unsigned char *data;
+ size_t i, data_size;
+
+ data = i_stream_get_data(parser->input, &data_size);
+
+ for (i = 0; i < data_size; i++) {
+ if (data[i] == ' ' || data[i] == '\r' || data[i] == '\n')
+ break;
+ }
+
+ if (i < data_size) {
+ data_size = i + (data[i] == ' ' ? 1 : 0);
+ parser->line_size += data_size;
+ i_stream_skip(parser->input, data_size);
+ return p_strndup(parser->pool, data, i);
+ } else {
+ return NULL;
+ }
+}
+
+static int
+imap_parser_read_next_atom(struct imap_parser *parser, bool parsing_tag,
+ const char **atom_r)
+{
+ const unsigned char *data;
+ size_t i, data_size;
+
+ data = i_stream_get_data(parser->input, &data_size);
+
+ /*
+ tag = 1*<any ASTRING-CHAR except "+">
+ ASTRING-CHAR = ATOM-CHAR / resp-specials
+ ATOM-CHAR = <any CHAR except atom-specials>
+
+ x-command = "X" atom <experimental command arguments>
+ atom = 1*ATOM-CHAR
+ */
+ for (i = 0; i < data_size; i++) {
+ /* explicitly check for atom-specials, because
+ IS_ATOM_PARSER_INPUT() allows some atom-specials */
+ switch (data[i]) {
+ case ' ':
+ case '\r':
+ case '\n':
+ data_size = i + (data[i] == ' ' ? 1 : 0);
+ parser->line_size += data_size;
+ i_stream_skip(parser->input, data_size);
+ *atom_r = p_strndup(parser->pool, data, i);
+ /* don't allow empty string */
+ return i == 0 ? -1 : 1;
+ /* atom-specials: */
+ case '(':
+ case ')':
+ case '{':
+ /* list-wildcards: */
+ case '%':
+ case '*':
+ /* quoted-specials: */
+ case '"':
+ case '\\':
+ /* resp-specials: */
+ case ']':
+ return -1;
+ case '+':
+ if (parsing_tag)
+ return -1;
+ break;
+ default:
+ if ((unsigned char)data[i] < ' ' ||
+ (unsigned char)data[i] >= 0x80)
+ return -1;
+ }
+ }
+ return 0;
+}
+
+int imap_parser_read_tag(struct imap_parser *parser, const char **tag_r)
+{
+ return imap_parser_read_next_atom(parser, TRUE, tag_r);
+}
+
+int imap_parser_read_command_name(struct imap_parser *parser,
+ const char **name_r)
+{
+ return imap_parser_read_next_atom(parser, FALSE, name_r);
+}
+
+int imap_parser_client_read_tag(struct imap_parser *parser,
+ const char **tag_r)
+{
+ return imap_parser_read_next_atom(parser, FALSE, tag_r);
+}
diff --git a/src/lib-imap/imap-parser.h b/src/lib-imap/imap-parser.h
new file mode 100644
index 0000000..cd3748c
--- /dev/null
+++ b/src/lib-imap/imap-parser.h
@@ -0,0 +1,117 @@
+#ifndef IMAP_PARSER_H
+#define IMAP_PARSER_H
+
+#include "imap-arg.h"
+
+enum imap_parser_flags {
+ /* Set this flag if you wish to read only size of literal argument
+ and not convert literal into string. Useful when you need to deal
+ with large literal sizes. The literal must be the last read
+ parameter. */
+ IMAP_PARSE_FLAG_LITERAL_SIZE = 0x01,
+ /* Don't remove '\' chars from string arguments */
+ IMAP_PARSE_FLAG_NO_UNESCAPE = 0x02,
+ /* Return literals as IMAP_ARG_LITERAL instead of IMAP_ARG_STRING */
+ IMAP_PARSE_FLAG_LITERAL_TYPE = 0x04,
+ /* Don't check if atom contains invalid characters */
+ IMAP_PARSE_FLAG_ATOM_ALLCHARS = 0x08,
+ /* Allow strings to contain CRLFs */
+ IMAP_PARSE_FLAG_MULTILINE_STR = 0x10,
+ /* Parse in list context; ')' parses as EOL */
+ IMAP_PARSE_FLAG_INSIDE_LIST = 0x20,
+ /* Parse literal8 and set it as flag to imap_arg. */
+ IMAP_PARSE_FLAG_LITERAL8 = 0x40,
+ /* We're parsing IMAP server replies. Parse the "text" after
+ OK/NO/BAD/BYE replies as a single atom. We assume that the initial
+ "*" or tag was already skipped over. */
+ IMAP_PARSE_FLAG_SERVER_TEXT = 0x80,
+ /* Parse until '(' and return it as an empty list */
+ IMAP_PARSE_FLAG_STOP_AT_LIST = 0x100
+};
+
+enum imap_parser_error {
+ /* not fatal */
+ IMAP_PARSE_ERROR_NONE = 0,
+ IMAP_PARSE_ERROR_BAD_SYNTAX,
+ IMAP_PARSE_ERROR_LINE_TOO_LONG,
+ /* fatal */
+ IMAP_PARSE_ERROR_LITERAL_TOO_BIG
+};
+
+struct imap_parser;
+
+/* Create new IMAP argument parser. output is used for sending command
+ continuation requests for literals.
+
+ max_line_size can be used to approximately limit the maximum amount of
+ memory that gets allocated when parsing a line. Input buffer size limits
+ the maximum size of each parsed token.
+
+ Usually the largest lines are large only because they have a one huge
+ message set token, so you'll probably want to keep input buffer size the
+ same as max_line_size. That means the maximum memory usage is around
+ 2 * max_line_size. */
+struct imap_parser *
+imap_parser_create(struct istream *input, struct ostream *output,
+ size_t max_line_size) ATTR_NULL(2);
+void imap_parser_ref(struct imap_parser *parser);
+void imap_parser_unref(struct imap_parser **parser);
+
+/* Enable LITERAL- parser semantics: non-synchronizing literals must not
+ exceed 4096 bytes */
+void imap_parser_enable_literal_minus(struct imap_parser *parser);
+
+/* Reset the parser to initial state. */
+void imap_parser_reset(struct imap_parser *parser);
+
+/* Change parser's input and output streams */
+void imap_parser_set_streams(struct imap_parser *parser, struct istream *input,
+ struct ostream *output) ATTR_NULL(3);
+
+/* Return the last error in parser. fatal is set to TRUE if there's no way to
+ continue parsing, currently only if too large non-sync literal size was
+ given. */
+const char *imap_parser_get_error(struct imap_parser *parser,
+ enum imap_parser_error *error_r) ATTR_NULL(2);
+
+/* Read a number of arguments. This function doesn't call i_stream_read(), you
+ need to do that. Returns number of arguments read (may be less than count
+ in case of EOL), -2 if more data is needed or -1 if error occurred.
+
+ count-sized array of arguments are stored into args when return value is
+ 0 or larger. If all arguments weren't read, they're set to NIL. count
+ can be set to 0 to read all arguments in the line. Last element in
+ args is always of type IMAP_ARG_EOL. */
+int imap_parser_read_args(struct imap_parser *parser, unsigned int count,
+ enum imap_parser_flags flags,
+ const struct imap_arg **args_r);
+/* If parsing ended with literal size, return it. */
+bool imap_parser_get_literal_size(struct imap_parser *parser, uoff_t *size_r);
+/* IMAP_PARSE_FLAG_LITERAL_SIZE is set and last read argument was a literal.
+ Calling this function causes the literal size to be replaced with the actual
+ literal data when continuing argument parsing. */
+void imap_parser_read_last_literal(struct imap_parser *parser);
+
+/* just like imap_parser_read_args(), but assume \n at end of data in
+ input stream. */
+int imap_parser_finish_line(struct imap_parser *parser, unsigned int count,
+ enum imap_parser_flags flags,
+ const struct imap_arg **args_r);
+
+/* Read one word - used for reading tag and command name.
+ Returns NULL if more data is needed. */
+const char *imap_parser_read_word(struct imap_parser *parser);
+/* Read command tag. Returns 1 if tag was returned, 0 if more data is needed,
+ -1 if input isn't a valid tag. */
+int imap_parser_read_tag(struct imap_parser *parser, const char **tag_r);
+/* Read command name. Returns 1 if command name was returned, 0 if more data is
+ needed, -1 if input isn't a valid command name string. */
+int imap_parser_read_command_name(struct imap_parser *parser,
+ const char **name_r);
+/* For IMAP clients: Read the command tag, which could also be "+" or "*".
+ Returns 1 if tag was returned, 0 if more data is needed, -1 if input isn't
+ valid. */
+int imap_parser_client_read_tag(struct imap_parser *parser,
+ const char **tag_r);
+
+#endif
diff --git a/src/lib-imap/imap-quote.c b/src/lib-imap/imap-quote.c
new file mode 100644
index 0000000..622e21c
--- /dev/null
+++ b/src/lib-imap/imap-quote.c
@@ -0,0 +1,239 @@
+/* Copyright (c) 2002-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "str.h"
+#include "imap-arg.h"
+#include "imap-quote.h"
+
+/* If we have quoted-specials (<">, <\>) in a string, the minimum quoted-string
+ overhead is 3 bytes ("\") while the minimum literal overhead is 5 bytes
+ ("{n}\r\n"). But the literal overhead also depends on the string size. If
+ the string length is less than 10, literal catches up to quoted-string after
+ 3 quoted-specials. If the string length is 10..99, it catches up after 4
+ quoted-specials, and so on. We'll assume that the string lengths are usually
+ in double digits, so we'll switch to literals after seeing 4
+ quoted-specials. */
+#define QUOTED_MAX_ESCAPE_CHARS 4
+
+void imap_append_string(string_t *dest, const char *src)
+{
+ i_assert(src != NULL);
+
+ imap_append_nstring(dest, src);
+}
+
+void imap_append_astring(string_t *dest, const char *src)
+{
+ unsigned int i;
+
+ i_assert(src != NULL);
+
+ for (i = 0; src[i] != '\0'; i++) {
+ if (!IS_ASTRING_CHAR(src[i])) {
+ imap_append_string(dest, src);
+ return;
+ }
+ }
+ /* don't mix up NIL and "NIL"! */
+ if (i == 0 || strcasecmp(src, "NIL") == 0)
+ imap_append_string(dest, src);
+ else
+ str_append(dest, src);
+}
+
+static void
+imap_append_literal(string_t *dest, const char *src, unsigned int pos)
+{
+ size_t full_len = pos + strlen(src+pos);
+
+ str_printfa(dest, "{%zu}\r\n", full_len);
+ buffer_append(dest, src, full_len);
+}
+
+void imap_append_nstring(string_t *dest, const char *src)
+{
+ unsigned int escape_count = 0;
+ size_t i;
+
+ if (src == NULL) {
+ str_append(dest, "NIL");
+ return;
+ }
+
+ /* first check if we can (or want to) write this as quoted or
+ as literal.
+
+ quoted-specials = DQUOTE / "\"
+ QUOTED-CHAR = <any TEXT-CHAR except quoted-specials> /
+ "\" quoted-specials
+ TEXT-CHAR = <any CHAR except CR and LF>
+ */
+ for (i = 0; src[i] != '\0'; i++) {
+ switch (src[i]) {
+ case '"':
+ case '\\':
+ if (escape_count++ < QUOTED_MAX_ESCAPE_CHARS)
+ break;
+ /* fall through */
+ case 13:
+ case 10:
+ imap_append_literal(dest, src, i);
+ return;
+ default:
+ if ((unsigned char)src[i] >= 0x80) {
+ imap_append_literal(dest, src, i);
+ return;
+ }
+ break;
+ }
+ }
+ imap_append_quoted(dest, src);
+}
+
+static void remove_newlines_and_append(string_t *dest, const char *src)
+{
+ size_t src_len;
+ string_t *src_nolf;
+ src_len = strlen(src);
+ src_nolf = t_str_new(src_len + 1);
+ for (size_t i = 0; i < src_len; ++i) {
+ if (src[i] != '\r' && src[i] != '\n') {
+ str_append_c(src_nolf, src[i]);
+ } else if (src[i+1] != ' ' &&
+ src[i+1] != '\t' &&
+ src[i+1] != '\r' &&
+ src[i+1] != '\n' &&
+ src[i+1] != '\0') {
+ /* ensure whitespace between lines if new line doesn't start with whitespace */
+ str_append_c(src_nolf, ' ');
+ }
+ }
+ imap_append_nstring(dest, str_c(src_nolf));
+}
+
+void imap_append_nstring_nolf(string_t *dest, const char *src)
+{
+ if (src == NULL || strpbrk(src, "\r\n") == NULL)
+ imap_append_nstring(dest, src);
+ else if (buffer_get_pool(dest)->datastack_pool)
+ remove_newlines_and_append(dest, src);
+ else T_BEGIN {
+ remove_newlines_and_append(dest, src);
+ } T_END;
+}
+
+void imap_append_quoted(string_t *dest, const char *src)
+{
+ str_append_c(dest, '"');
+ for (; *src != '\0'; src++) {
+ switch (*src) {
+ case 13:
+ case 10:
+ /* not allowed */
+ break;
+ case '"':
+ case '\\':
+ str_append_c(dest, '\\');
+ str_append_c(dest, *src);
+ break;
+ default:
+ if ((unsigned char)*src >= 0x80) {
+ /* 8bit input not allowed in dquotes */
+ break;
+ }
+
+ str_append_c(dest, *src);
+ break;
+ }
+ }
+ str_append_c(dest, '"');
+}
+
+void imap_append_string_for_humans(string_t *dest,
+ const unsigned char *src, size_t size)
+{
+ size_t i, pos, remove_count = 0;
+ bool whitespace_prefix = TRUE, last_lwsp = TRUE, modify = FALSE;
+
+ /* first check if there is anything to change */
+ for (i = 0; i < size; i++) {
+ switch (src[i]) {
+ case 0:
+ /* convert NUL to #0x80 */
+ last_lwsp = FALSE;
+ modify = TRUE;
+ break;
+ case 13:
+ case 10:
+ case '\t':
+ modify = TRUE;
+ /* fall through */
+ case ' ':
+ if (last_lwsp) {
+ modify = TRUE;
+ remove_count++;
+ }
+ last_lwsp = TRUE;
+ break;
+ case '"':
+ case '\\':
+ modify = TRUE;
+ last_lwsp = FALSE;
+ break;
+ default:
+ if ((src[i] & 0x80) != 0)
+ modify = TRUE;
+ last_lwsp = FALSE;
+ break;
+ }
+ if (!last_lwsp)
+ whitespace_prefix = FALSE;
+ }
+ if (last_lwsp && i > 0 && !whitespace_prefix) {
+ modify = TRUE;
+ remove_count++;
+ }
+ if (!modify) {
+ /* fast path: we can simply write it as quoted string
+ without any escaping */
+ str_append_c(dest, '"');
+ str_append_data(dest, src, size);
+ str_append_c(dest, '"');
+ return;
+ }
+ if (size == remove_count) {
+ /* contained only whitespace */
+ str_append(dest, "\"\"");
+ return;
+ }
+
+ str_printfa(dest, "{%zu}\r\n", size - remove_count);
+ pos = str_len(dest);
+
+ last_lwsp = TRUE; whitespace_prefix = TRUE;
+ for (i = 0; i < size; i++) {
+ switch (src[i]) {
+ case 0:
+ str_append_c(dest, 128);
+ last_lwsp = FALSE;
+ break;
+ case 13:
+ case 10:
+ case '\t':
+ case ' ':
+ if (!last_lwsp)
+ str_append_c(dest, ' ');
+ last_lwsp = TRUE;
+ break;
+ default:
+ last_lwsp = FALSE;
+ str_append_c(dest, src[i]);
+ break;
+ }
+ if (!last_lwsp)
+ whitespace_prefix = FALSE;
+ }
+ if (last_lwsp && i > 0 && !whitespace_prefix)
+ str_truncate(dest, str_len(dest)-1);
+ i_assert(str_len(dest) - pos == size - remove_count);
+}
diff --git a/src/lib-imap/imap-quote.h b/src/lib-imap/imap-quote.h
new file mode 100644
index 0000000..a397ec3
--- /dev/null
+++ b/src/lib-imap/imap-quote.h
@@ -0,0 +1,21 @@
+#ifndef IMAP_QUOTE_H
+#define IMAP_QUOTE_H
+
+/* Append "quoted" or literal. */
+void imap_append_string(string_t *dest, const char *src);
+/* Append atom, "quoted" or literal. */
+void imap_append_astring(string_t *dest, const char *src);
+/* Append NIL, "quoted" or literal. */
+void imap_append_nstring(string_t *dest, const char *src);
+/* Append NIL, "quoted" or literal, CRs and LFs skipped. */
+void imap_append_nstring_nolf(string_t *dest, const char *src);
+/* Append "quoted". If src has 8bit chars, skip over them. */
+void imap_append_quoted(string_t *dest, const char *src);
+
+/* Otherwise the same as imap_append_string(), but cleanup the input data
+ so that it's more readable by humans. This includes converting TABs to
+ spaces, multiple spaces into a single space and NULs to #0x80. */
+void imap_append_string_for_humans(string_t *dest,
+ const unsigned char *src, size_t size);
+
+#endif
diff --git a/src/lib-imap/imap-resp-code.h b/src/lib-imap/imap-resp-code.h
new file mode 100644
index 0000000..a7a4de8
--- /dev/null
+++ b/src/lib-imap/imap-resp-code.h
@@ -0,0 +1,28 @@
+#ifndef IMAP_RESP_CODE_H
+#define IMAP_RESP_CODE_H
+
+/* IMAP response codes (RFC 5530) */
+#define IMAP_RESP_CODE_UNAVAILABLE "UNAVAILABLE"
+#define IMAP_RESP_CODE_AUTHFAILED "AUTHENTICATIONFAILED"
+#define IMAP_RESP_CODE_AUTHZFAILED "AUTHORIZATIONFAILED"
+#define IMAP_RESP_CODE_EXPIRED "EXPIRED"
+#define IMAP_RESP_CODE_PRIVACYREQUIRED "PRIVACYREQUIRED"
+#define IMAP_RESP_CODE_CONTACTADMIN "CONTACTADMIN"
+#define IMAP_RESP_CODE_NOPERM "NOPERM"
+#define IMAP_RESP_CODE_INUSE "INUSE"
+#define IMAP_RESP_CODE_EXPUNGEISSUED "EXPUNGEISSUED"
+#define IMAP_RESP_CODE_CORRUPTION "CORRUPTION"
+#define IMAP_RESP_CODE_SERVERBUG "SERVERBUG"
+#define IMAP_RESP_CODE_CLIENTBUG "CLIENTBUG"
+#define IMAP_RESP_CODE_CANNOT "CANNOT"
+#define IMAP_RESP_CODE_LIMIT "LIMIT"
+#define IMAP_RESP_CODE_OVERQUOTA "OVERQUOTA"
+#define IMAP_RESP_CODE_ALREADYEXISTS "ALREADYEXISTS"
+#define IMAP_RESP_CODE_NONEXISTENT "NONEXISTENT"
+
+#define IMAP_RESP_CODE_UNKNOWN_CTE "UNKNOWN-CTE" /* BINARY */
+
+/* IMAP standard (RFC 3501) */
+#define IMAP_RESP_CODE_PARSE "PARSE"
+
+#endif
diff --git a/src/lib-imap/imap-seqset.c b/src/lib-imap/imap-seqset.c
new file mode 100644
index 0000000..5e7ea21
--- /dev/null
+++ b/src/lib-imap/imap-seqset.c
@@ -0,0 +1,105 @@
+/* Copyright (c) 2002-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "imap-seqset.h"
+
+static uint32_t get_next_number(const char **str)
+{
+ uint32_t num;
+
+ num = 0;
+ while (**str != '\0') {
+ if (**str < '0' || **str > '9')
+ break;
+
+ num = num*10 + (**str - '0');
+ (*str)++;
+ }
+
+ if (num == (uint32_t)-1) {
+ /* FIXME: ugly hack, we're using this number to mean the
+ last existing message. In reality UIDs should never get
+ this high, so we can quite safely just drop this one down. */
+ num--;
+ }
+
+ return num;
+}
+
+static int
+get_next_seq_range(const char **str, uint32_t *seq1_r, uint32_t *seq2_r)
+{
+ uint32_t seq1, seq2;
+
+ if (**str == '*') {
+ /* last message */
+ seq1 = (uint32_t)-1;
+ *str += 1;
+ } else {
+ seq1 = get_next_number(str);
+ if (seq1 == 0)
+ return -1;
+ }
+
+ if (**str != ':')
+ seq2 = seq1;
+ else {
+ /* first:last range */
+ *str += 1;
+
+ if (**str == '*') {
+ seq2 = (uint32_t)-1;
+ *str += 1;
+ } else {
+ seq2 = get_next_number(str);
+ if (seq2 == 0)
+ return -1;
+ }
+ }
+
+ if (seq1 > seq2) {
+ /* swap, as specified by RFC-3501 */
+ *seq1_r = seq2;
+ *seq2_r = seq1;
+ } else {
+ *seq1_r = seq1;
+ *seq2_r = seq2;
+ }
+ return 0;
+}
+
+int imap_seq_set_parse(const char *str, ARRAY_TYPE(seq_range) *dest)
+{
+ uint32_t seq1, seq2;
+
+ while (*str != '\0') {
+ if (get_next_seq_range(&str, &seq1, &seq2) < 0)
+ return -1;
+ seq_range_array_add_range(dest, seq1, seq2);
+
+ if (*str == ',')
+ str++;
+ else if (*str != '\0')
+ return -1;
+ }
+ return 0;
+}
+
+int imap_seq_set_nostar_parse(const char *str, ARRAY_TYPE(seq_range) *dest)
+{
+ if (imap_seq_set_parse(str, dest) < 0)
+ return -1;
+
+ if (seq_range_exists(dest, (uint32_t)-1)) {
+ /* '*' used */
+ return -1;
+ }
+ return 0;
+}
+
+int imap_seq_range_parse(const char *str, uint32_t *seq1_r, uint32_t *seq2_r)
+{
+ if (get_next_seq_range(&str, seq1_r, seq2_r) < 0)
+ return -1;
+ return *str == '\0' ? 0 : -1;
+}
diff --git a/src/lib-imap/imap-seqset.h b/src/lib-imap/imap-seqset.h
new file mode 100644
index 0000000..a7e1ffd
--- /dev/null
+++ b/src/lib-imap/imap-seqset.h
@@ -0,0 +1,15 @@
+#ifndef IMAP_SEQSET_H
+#define IMAP_SEQSET_H
+
+#include "seq-range-array.h"
+
+/* Parse IMAP sequence-set and store the result in dest. '*' is stored as
+ (uint32_t)-1. Returns 0 if successful, -1 if input is invalid. */
+int imap_seq_set_parse(const char *str, ARRAY_TYPE(seq_range) *dest);
+/* Like imap_seq_set_parse(), but fail if '*' is used. */
+int imap_seq_set_nostar_parse(const char *str, ARRAY_TYPE(seq_range) *dest);
+
+/* Parse IMAP seq-number / seq-range. */
+int imap_seq_range_parse(const char *str, uint32_t *seq1_r, uint32_t *seq2_r);
+
+#endif
diff --git a/src/lib-imap/imap-url.c b/src/lib-imap/imap-url.c
new file mode 100644
index 0000000..6da6e21
--- /dev/null
+++ b/src/lib-imap/imap-url.c
@@ -0,0 +1,1009 @@
+/* Copyright (c) 2010-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "str.h"
+#include "strfuncs.h"
+#include "str-sanitize.h"
+#include "hex-binary.h"
+#include "net.h"
+#include "iso8601-date.h"
+#include "uri-util.h"
+
+#include "imap-url.h"
+
+#include <ctype.h>
+
+/*
+ * IMAP URL parsing
+ */
+
+/*
+IMAP URL Grammar overview
+
+RFC5092 Section 11:
+
+imapurl = "imap://" iserver ipath-query
+ ; Defines an absolute IMAP URL
+iserver = [iuserinfo "@"] host [ ":" port ]
+ ; This is the same as "authority" defined in [URI-GEN].
+iuserinfo = enc-user [iauth] / [enc-user] iauth
+ ; conforms to the generic syntax of "userinfo" as
+ ; defined in [URI-GEN].
+enc-user = 1*achar
+ ; %-encoded version of [IMAP4] authorization identity or
+ ; "userid".
+iauth = ";AUTH=" ( "*" / enc-auth-type )
+enc-auth-type = 1*achar
+ ; %-encoded version of [IMAP4] "auth-type"
+ipath-query = ["/" [ icommand ]]
+ ; Corresponds to "path-abempty [ "?" query ]" in
+ ; [URI-GEN]
+icommand = imessagelist /
+ imessagepart [iurlauth]
+imessagelist = imailbox-ref [ "?" enc-search ]
+ ; "enc-search" is [URI-GEN] "query".
+imessagepart = imailbox-ref iuid [isection] [ipartial]
+imailbox-ref = enc-mailbox [uidvalidity]
+uidvalidity = ";UIDVALIDITY=" nz-number
+ ; See [IMAP4] for "nz-number" definition
+iuid = "/" iuid-only
+iuid-only = ";UID=" nz-number
+ ; See [IMAP4] for "nz-number" definition
+isection = "/" isection-only
+isection-only = ";SECTION=" enc-section
+ipartial = "/" ipartial-only
+ipartial-only = ";PARTIAL=" partial-range
+enc-search = 1*bchar
+ ; %-encoded version of [IMAPABNF]
+ ; "search-program". Note that IMAP4
+ ; literals may not be used in
+ ; a "search-program", i.e., only
+ ; quoted or non-synchronizing
+ ; literals (if the server supports
+ ; LITERAL+ [LITERAL+]) are allowed.
+enc-mailbox = 1*bchar
+ ; %-encoded version of [IMAP4] "mailbox"
+enc-section = 1*bchar
+ ; %-encoded version of [IMAP4] "section-spec"
+partial-range = number ["." nz-number]
+ ; partial FETCH. The first number is
+ ; the offset of the first byte,
+ ; the second number is the length of
+ ; the fragment.
+bchar = achar / ":" / "@" / "/"
+achar = uchar / "&" / "="
+ ;; Same as [URI-GEN] 'unreserved / sub-delims /
+ ;; pct-encoded', but ";" is disallowed.
+uchar = unreserved / sub-delims-sh / pct-encoded
+sub-delims-sh = "!" / "$" / "'" / "(" / ")" /
+ "*" / "+" / ","
+ ;; Same as [URI-GEN] sub-delims,
+ ;; but without ";", "&" and "=".
+
+The following rules are only used in the presence of the IMAP
+[URLAUTH] extension:
+
+authimapurl = "imap://" iserver "/" imessagepart
+ ; Same as "imapurl" when "[icommand]" is
+ ; "imessagepart"
+authimapurlfull = authimapurl iurlauth
+ ; Same as "imapurl" when "[icommand]" is
+ ; "imessagepart iurlauth"
+authimapurlrump = authimapurl iurlauth-rump
+
+iurlauth = iurlauth-rump iua-verifier
+enc-urlauth = 32*HEXDIG
+iua-verifier = ":" uauth-mechanism ":" enc-urlauth
+iurlauth-rump = [expire] ";URLAUTH=" access
+access = ("submit+" enc-user) / ("user+" enc-user) /
+ "authuser" / "anonymous"
+expire = ";EXPIRE=" date-time
+ ; date-time is defined in [DATETIME]
+uauth-mechanism = "INTERNAL" / 1*(ALPHA / DIGIT / "-" / ".")
+ ; Case-insensitive.
+
+[URI-GEN] RFC3986 Appendix A:
+
+Implemented in src/lib/uri-util.c
+
+*/
+
+/*
+ * Imap URL parser
+ */
+
+struct imap_url_parser {
+ struct uri_parser parser;
+
+ enum imap_url_parse_flags flags;
+
+ struct imap_url *url;
+ const struct imap_url *base;
+
+ bool relative:1;
+};
+
+static int
+imap_url_parse_number(struct uri_parser *parser, const char *data,
+ uint32_t *number_r)
+{
+ /* [IMAP4] RFC3501, Section 9
+ *
+ * number = 1*DIGIT
+ * ; Unsigned 32-bit integer
+ * ; (0 <= n < 4,294,967,296)
+ */
+
+ if (i_isdigit(*data)) {
+ if (str_to_uint32(data, number_r) == 0)
+ return 1;
+ parser->error = "IMAP number is too high";
+ return -1;
+ }
+
+ parser->error = t_strdup_printf(
+ "Value '%s' is not a valid IMAP number", data);
+ return -1;
+}
+
+static int
+imap_url_parse_offset(struct uri_parser *parser, const char *data,
+ uoff_t *number_r)
+{
+ /* Syntax for big (uoff_t) numbers. Not strictly IMAP syntax, but this
+ is handled similarly for Dovecot IMAP FETCH BODY partial <.>
+ implementation. */
+ if (i_isdigit(*data)) {
+ if (str_to_uoff(data, number_r) == 0)
+ return 1;
+ parser->error = "IMAP number is too high";
+ return -1;
+ }
+
+ parser->error = t_strdup_printf(
+ "Value '%s' is not a valid IMAP number", data);
+ return -1;
+}
+
+static int imap_url_parse_iserver(struct imap_url_parser *url_parser)
+{
+ struct uri_parser *parser = &url_parser->parser;
+ struct uri_authority auth;
+ struct imap_url *url = url_parser->url;
+ const char *data;
+ int ret = 0;
+
+ /* imapurl = "imap://" iserver {...}
+ * inetwork-path = "//" iserver {...}
+ * iserver = [iuserinfo "@"] host [":" port]
+ * ; This is the same as "authority" defined
+ * ; in [URI-GEN].
+ * iuserinfo = enc-user [iauth] / [enc-user] iauth
+ * ; conforms to the generic syntax of "userinfo" as
+ * ; defined in [URI-GEN].
+ * enc-user = 1*achar
+ * ; %-encoded version of [IMAP4] authorization identity or
+ * ; "userid".
+ * iauth = ";AUTH=" ( "*" / enc-auth-type )
+ * enc-auth-type = 1*achar
+ * ; %-encoded version of [IMAP4] "auth-type"
+ */
+
+ /* "//" iserver */
+ if ((ret = uri_parse_slashslash_host_authority
+ (parser, &auth)) <= 0)
+ return ret;
+ if (auth.host.name == NULL || *auth.host.name == '\0') {
+ /* This situation is not documented anywhere, but it is not
+ currently useful either and potentially problematic if not
+ handled explicitly everywhere. So, it is denied hier for now.
+ */
+ parser->error = "IMAP URL does not allow empty host identifier";
+ return -1;
+ }
+ /* iuserinfo = enc-user [iauth] / [enc-user] iauth */
+ if (auth.enc_userinfo != NULL) {
+ const char *p, *uend;
+
+ /* Scan for ";AUTH=" */
+ for (p = auth.enc_userinfo; *p != '\0'; p++) {
+ if (*p == ';')
+ break;
+ /* check for unallowed userinfo characters */
+ if (*p == ':') {
+ parser->error = t_strdup_printf(
+ "Stray ':' in userinfo `%s'", auth.enc_userinfo);
+ return -1;
+ }
+ }
+
+ uend = p;
+
+ if (*p == ';') {
+ if (strncasecmp(p, ";AUTH=", 6) != 0) {
+ parser->error = t_strdup_printf(
+ "Stray ';' in userinfo `%s'",
+ auth.enc_userinfo);
+ return -1;
+ }
+
+ for (p += 6; *p != '\0'; p++) {
+ if (*p == ';' || *p == ':') {
+ parser->error = t_strdup_printf(
+ "Stray '%c' in userinfo `%s'", *p, auth.enc_userinfo);
+ return -1;
+ }
+ }
+ }
+
+ /* enc-user */
+ if (url != NULL && uend > auth.enc_userinfo) {
+ if (!uri_data_decode(parser, auth.enc_userinfo, uend, &data))
+ return -1;
+ url->userid = p_strdup(parser->pool, data);
+ }
+
+ /* ( "*" / enc-auth-type ) */
+ if (*uend == ';') {
+ p = uend + 6;
+ if (*p == '\0') {
+ parser->error = "Empty auth-type value after ';AUTH='";
+ return -1;
+ }
+ if (url != NULL) {
+ if (!uri_data_decode(parser, p, NULL, &data))
+ return -1;
+ url->auth_type = p_strdup(parser->pool, data);
+ }
+ }
+ }
+
+ if (url != NULL) {
+ url->host = auth.host;
+ url->port = auth.port;
+ }
+ return 1;
+}
+
+static int
+imap_url_parse_urlauth(struct imap_url_parser *url_parser, const char *urlext)
+{
+ struct uri_parser *parser = &url_parser->parser;
+ struct imap_url *url = url_parser->url;
+ const char *p, *q, *data;
+ buffer_t *uauth_token;
+ time_t expire = (time_t)-1;
+ int tz;
+
+ /* iurlauth = iurlauth-rump iua-verifier
+ * enc-urlauth = 32*HEXDIG
+ * iua-verifier = ":" uauth-mechanism ":" enc-urlauth
+ * iurlauth-rump = [expire] ";URLAUTH=" access
+ * access = ("submit+" enc-user) / ("user+" enc-user) /
+ * "authuser" / "anonymous"
+ * expire = ";EXPIRE=" date-time
+ * ; date-time is defined in [DATETIME]
+ * uauth-mechanism = "INTERNAL" / 1*(ALPHA / DIGIT / "-" / ".")
+ * ; Case-insensitive.
+ */
+
+ /* ";EXPIRE=" date-time */
+ if (strncasecmp(urlext, ";EXPIRE=", 8) == 0) {
+ if ((url_parser->flags & IMAP_URL_PARSE_ALLOW_URLAUTH) == 0) {
+ parser->error = "`;EXPIRE=' is not allowed in this context";
+ return -1;
+ }
+
+ if ((p = strchr(urlext+8, ';')) != NULL) {
+ if (!iso8601_date_parse((const unsigned char *)urlext+8,
+ p-urlext-8, &expire, &tz)) {
+ parser->error = "invalid date-time for `;EXPIRE='";
+ return -1;
+ }
+ urlext = p;
+ }
+ }
+
+ /* ";URLAUTH=" access */
+ if (strncasecmp(urlext, ";URLAUTH=", 9) != 0) {
+ if (expire != (time_t)-1) {
+ parser->error = "`;EXPIRE=' without `;URLAUTH='";
+ return -1;
+ }
+ return 0;
+ }
+ urlext += 9;
+
+ if (url != NULL)
+ url->uauth_expire = expire;
+
+ if ((url_parser->flags & IMAP_URL_PARSE_ALLOW_URLAUTH) == 0) {
+ parser->error = "`;URLAUTH=' is not allowed in this context";
+ return -1;
+ }
+
+ if (url_parser->relative) {
+ parser->error = "IMAP URLAUTH requires absolute URL";
+ return -1;
+ }
+
+ if ((p = strchr(urlext, ':')) == NULL) {
+ size_t len = strlen(urlext);
+ if (len == 0) {
+ parser->error = "Missing URLAUTH access specifier";
+ return -1;
+ }
+ p = urlext+len;
+ } else if (p == urlext) {
+ parser->error = "Empty URLAUTH access specifier";
+ return -1;
+ }
+
+ /* parse access */
+ if ((q = strchr(urlext, '+')) == NULL) {
+ /* application */
+ if (url != NULL) {
+ url->uauth_access_application =
+ p_strdup_until(parser->pool, urlext, p);
+ }
+ } else {
+ /* application "+" enc-user */
+ if (urlext == q) {
+ parser->error = "Empty URLAUTH access application";
+ return -1;
+ }
+ if (q+1 == p) {
+ parser->error = t_strdup_printf(
+ "Empty URLAUTH access user for `%s' application",
+ t_strdup_until(urlext, q));
+ return -1;
+ }
+ if (!uri_data_decode(parser, q+1, p, &data))
+ return -1;
+ if (url != NULL) {
+ url->uauth_access_application =
+ p_strdup_until(parser->pool, urlext, q);
+ url->uauth_access_user = p_strdup(parser->pool, data);
+ }
+ }
+
+ if (url != NULL) {
+ /* get rump url */
+ if ((url_parser->flags & IMAP_URL_PARSE_SCHEME_EXTERNAL) == 0) {
+ url->uauth_rumpurl = p_strdup_until(parser->pool,
+ parser->begin, parser->end-strlen(p));
+ } else {
+ url->uauth_rumpurl = p_strconcat(parser->pool, "imap:",
+ t_strdup_until(parser->begin, parser->end-strlen(p)),
+ NULL);
+ }
+ }
+
+ if (*p == '\0') {
+ /* rump url; caller should check whether this is appropriate */
+ return 1;
+ }
+
+ /* iua-verifier = ":" uauth-mechanism ":" enc-urlauth */
+
+ q = p + 1;
+ if (*q == '\0') {
+ parser->error = "Missing URLAUTH verifier";
+ return -1;
+ }
+ if ((p = strchr(q, ':')) == NULL || p[1] == '\0') {
+ parser->error = "Missing URLAUTH token";
+ return -1;
+ }
+ if (p == q) {
+ parser->error = "Missing URLAUTH mechanism";
+ return -1;
+ }
+ if (url != NULL) {
+ /* get mechanism */
+ url->uauth_mechanism = p_strdup_until(parser->pool, q, p);
+ }
+
+ /* enc-urlauth = 32*HEXDIG */
+
+ q = p+1;
+ if (strlen(q) < 32) {
+ parser->error = "Too short URLAUTH token";
+ return -1;
+ }
+
+ uauth_token = t_buffer_create(64);
+ if (hex_to_binary(q, uauth_token) < 0) {
+ parser->error = "Invalid URLAUTH token";
+ return -1;
+ }
+
+ if (url != NULL) {
+ url->uauth_token = uauth_token->data;
+ url->uauth_token_size = uauth_token->used;
+ }
+ return 1;
+}
+
+static int
+imap_url_parse_path(struct imap_url_parser *url_parser,
+ const char *const *path, int relative,
+ bool *is_messagelist_r)
+{
+ struct uri_parser *parser = &url_parser->parser;
+ struct imap_url *url = url_parser->url;
+ const char *const *segment;
+ string_t *mailbox, *section = NULL;
+ uint32_t uid = 0, uidvalidity = 0;
+ uoff_t partial_offset = 0, partial_size = 0;
+ bool have_partial = FALSE;
+ const char *p, *value, *urlext = NULL;
+ bool mailbox_endslash = FALSE, section_endslash = FALSE;
+ int ret;
+
+ /* icommand = imessagelist /
+ * imessagepart [iurlauth]
+ * imessagelist = imailbox-ref [ "?" enc-search ]
+ * ; "enc-search" is [URI-GEN] "query".
+ * imessagepart = imailbox-ref iuid [isection] [ipartial]
+ * imailbox-ref = enc-mailbox [uidvalidity]
+ * uidvalidity = ";UIDVALIDITY=" nz-number
+ * iuid = "/" iuid-only
+ * iuid-only = ";UID=" nz-number
+ * ; See [IMAP4] for "nz-number" definition
+ * isection = "/" isection-only
+ * isection-only = ";SECTION=" enc-section
+ * ipartial = "/" ipartial-only
+ * ipartial-only = ";PARTIAL=" partial-range
+ * enc-mailbox = 1*bchar
+ * ; %-encoded version of [IMAP4] "mailbox"
+ * enc-section = 1*bchar
+ * ; %-encoded version of [IMAP4] "section-spec"
+ * partial-range = number ["." nz-number]
+ * ; partial FETCH. The first number is
+ * ; the offset of the first byte,
+ * ; the second number is the length of
+ * ; the fragment.
+ */
+
+ /* IMAP URL syntax is quite horrible to parse. It relies upon the
+ generic URI path resolution, but the icommand syntax also relies on
+ ';' separators. We use the generic URI path parse functions to
+ adhere to the URI path resolution rules and glue back together path
+ segments when these are part of the same (mailbox or section) value.
+ */
+
+ mailbox = t_str_new(256);
+ segment = path;
+
+ /* Resolve relative URI path; determine what to copy from the base URI */
+ if (url != NULL && url_parser->base != NULL && relative > 0) {
+ const struct imap_url *base = url_parser->base;
+ int rel = relative;
+
+ /* /;PARTIAL= */
+ if (base->have_partial && --rel <= 0) {
+ have_partial = base->have_partial;
+ partial_offset = base->partial_offset;
+ partial_size = base->partial_size;
+ }
+ /* /;SECTION= */
+ if (base->section != NULL) {
+ p = base->section + strlen(base->section);
+ /* determine what to retain from base section path */
+ for (; p > base->section && rel > 0; p--) {
+ if (*p =='/' && --rel <= 0) break;
+ }
+ if (--rel <= 0 && p > base->section) {
+ if (p[-1] == '/') section_endslash = TRUE;
+ if (section == NULL)
+ section = t_str_new(256);
+ str_append_data(section, base->section, p-base->section);
+ }
+ }
+ /* /;UID= */
+ if (base->uid > 0 && --rel <= 0) {
+ uid = base->uid;
+ }
+ /* /mail/box;UIDVALIDITY= */
+ if (base->mailbox != NULL) {
+ uidvalidity = base->uidvalidity;
+ p = base->mailbox + strlen(base->mailbox);
+ /* mailbox has implicit trailing '/' */
+ if (p[-1] != '/' && base->uid == 0 && rel > 0)
+ rel--;
+ /* determine what to retain from base mailbox path */
+ for (; p > base->mailbox && rel > 0; p--) {
+ if (*p =='/') {
+ uidvalidity = 0;
+ if (--rel <= 0)
+ break;
+ }
+ }
+ if (--rel <= 0 && p > base->mailbox) {
+ if (p[-1] == '/')
+ mailbox_endslash = TRUE;
+ str_append_data(mailbox, base->mailbox,
+ p - base->mailbox);
+ }
+ }
+ }
+
+ /* Scan for last mailbox-ref segment */
+ if (segment != NULL) {
+ if (relative == 0 || (!have_partial && section == NULL)) {
+ p = NULL;
+ while (*segment != NULL) {
+ /* ';' must be pct-encoded; if it is not, this is
+ either the last mailbox-ref path segment containing
+ ';UIDVALIDITY=' or the subsequent iuid ';UID=' path
+ segment */
+ if ((p = strchr(*segment, ';')) != NULL)
+ break;
+
+ if (**segment != '\0') {
+ if (segment > path ||
+ (!mailbox_endslash && str_len(mailbox) > 0))
+ str_append_c(mailbox, '/');
+ if (!uri_data_decode(parser, *segment, NULL, &value))
+ return -1;
+ str_append(mailbox, value);
+ mailbox_endslash = FALSE;
+ }
+ segment++;
+ }
+
+ /* Handle ';' */
+ if (p != NULL) {
+ /* [uidvalidity] */
+ if (strncasecmp(p, ";UIDVALIDITY=", 13) == 0) {
+ /* append last bit of mailbox */
+ if (*segment != p) {
+ if (segment > path ||
+ (!mailbox_endslash && str_len(mailbox) > 0))
+ str_append_c(mailbox, '/');
+ if (!uri_data_decode(parser, *segment, p, &value))
+ return -1;
+ str_append(mailbox, value);
+ }
+
+ /* ";UIDVALIDITY=" nz-number */
+ if (strchr(p+13, ';') != NULL) {
+ parser->error = "Encountered stray ';' after UIDVALIDITY";
+ return -1;
+ }
+
+ /* nz-number */
+ if (p[13] == '\0') {
+ parser->error = "Empty UIDVALIDITY value";
+ return -1;
+ }
+ if (imap_url_parse_number(parser, p+13, &uidvalidity) <= 0)
+ return -1;
+ if (uidvalidity == 0) {
+ parser->error = "UIDVALIDITY cannot be zero";
+ return -1;
+ }
+ segment++;
+ } else if (p != *segment) {
+ parser->error = "Encountered stray ';' in mailbox reference";
+ return -1;
+ }
+ }
+
+ /* iuid */
+ if (*segment != NULL && strncasecmp(*segment, ";UID=", 5) == 0) {
+ /* ";UID=" nz-number */
+ value = (*segment)+5;
+ if ((p = strchr(value,';')) != NULL) {
+ if (segment[1] != NULL ) {
+ /* not the last segment, so it cannot be extension like iurlauth */
+ parser->error = "Encountered stray ';' in UID path segment";
+ return -1;
+ }
+ urlext = p;
+ value = t_strdup_until(value, p);
+ }
+ /* nz-number */
+ if (*value == '\0') {
+ parser->error = "Empty UID value";
+ return -1;
+ }
+ if (imap_url_parse_number(parser, value, &uid) <= 0)
+ return -1;
+ if (uid == 0) {
+ parser->error = "UID cannot be zero";
+ return -1;
+ }
+ segment++;
+ }
+ }
+
+ /* [isection] [ipartial] */
+ if (*segment != NULL && uid > 0) {
+ /* [isection] */
+ if (section != NULL ||
+ strncasecmp(*segment, ";SECTION=", 9) == 0) {
+ /* ";SECTION=" enc-section */
+ if (section == NULL) {
+ section = t_str_new(256);
+ value = (*segment) + 9;
+ } else {
+ value = *segment;
+ }
+
+ /* enc-section can contain slashes, so we merge path segments until one
+ contains ';' */
+ while ((p = strchr(value,';')) == NULL) {
+ if (!section_endslash && str_len(section) > 0)
+ str_append_c(section, '/');
+ if (*value != '\0') {
+ if (!uri_data_decode(parser, value, NULL, &value))
+ return -1;
+ str_append(section, value);
+ section_endslash = FALSE;
+ }
+
+ segment++;
+ if (*segment == NULL)
+ break;
+ value = *segment;
+ }
+
+ if (p != NULL) {
+ /* found ';' */
+ if (p != value) {
+ /* it is not at the beginning of the path segment */
+ if (segment[1] != NULL) {
+ /* not the last segment, so it cannot be extension like iurlauth */
+ parser->error = "Encountered stray ';' in SECTION path segment";
+ return -1;
+ }
+ urlext = p;
+ value = t_strdup_until(value, p);
+ if (!section_endslash && str_len(section) > 0)
+ str_append_c(section, '/');
+ if (!uri_data_decode(parser, value, NULL, &value))
+ return -1;
+ str_append(section, value);
+ segment++;
+ }
+ }
+
+ if (str_len(section) == 0) {
+ parser->error = "Empty SECTION value";
+ return -1;
+ }
+ }
+
+ /* [ipartial] */
+ if (*segment != NULL &&
+ strncasecmp(*segment, ";PARTIAL=", 9) == 0) {
+ have_partial = TRUE;
+
+ /* ";PARTIAL=" partial-range */
+ value = (*segment) + 9;
+ if ((p = strchr(value,';')) != NULL) {
+ urlext = p;
+ value = t_strdup_until(value, p);
+ }
+ if (*value == '\0') {
+ parser->error = "Empty PARTIAL value";
+ return -1;
+ }
+ /* partial-range = number ["." nz-number] */
+ if ((p = strchr(value,'.')) != NULL) {
+ if (p[1] == '\0') {
+ parser->error = "Empty PARTIAL size";
+ return -1;
+ }
+ if (imap_url_parse_offset(parser, p+1, &partial_size) <= 0)
+ return -1;
+ if (partial_size == 0) {
+ parser->error = "PARTIAL size cannot be zero";
+ return -1;
+ }
+ value = t_strdup_until(value, p);
+ if (*value == '\0') {
+ parser->error = "Empty PARTIAL offset";
+ return -1;
+ }
+ }
+ if (imap_url_parse_offset(parser,value, &partial_offset) <= 0)
+ return -1;
+ segment++;
+ }
+ }
+
+ if (*segment != NULL) {
+ if (urlext != NULL || **segment != '\0' || *(segment+1) != NULL ) {
+ parser->error = t_strdup_printf(
+ "Unexpected IMAP URL path segment: `%s'",
+ str_sanitize(*segment, 80));
+ return -1;
+ }
+ }
+ }
+
+ /* ";" {...} at end of URL */
+ if (urlext != NULL) {
+ /* [iurlauth] */
+ if ((ret = imap_url_parse_urlauth(url_parser, urlext)) < 0)
+ return ret;
+ else if (ret == 0) {
+ /* something else */
+ parser->error = t_strdup_printf(
+ "Unrecognized IMAP URL extension: %s",
+ str_sanitize(urlext, 80));
+ return -1;
+ }
+ }
+
+ if (is_messagelist_r != NULL)
+ *is_messagelist_r = (uid == 0);
+
+ if (url != NULL) {
+ if (str_len(mailbox) > 0)
+ url->mailbox = p_strdup(parser->pool, str_c(mailbox));
+ url->uidvalidity = uidvalidity;
+ url->uid = uid;
+ if (section != NULL)
+ url->section = p_strdup(parser->pool, str_c(section));
+ url->have_partial = have_partial;
+ url->partial_offset = partial_offset;
+ url->partial_size = partial_size;
+ }
+ return 1;
+}
+
+static bool imap_url_do_parse(struct imap_url_parser *url_parser)
+{
+ struct uri_parser *parser = &url_parser->parser;
+ const char *const *path;
+ bool is_messagelist = FALSE;
+ bool have_scheme = FALSE;
+ int relative;
+ const char *query;
+ int ret, sret;
+
+ /*
+ * imapurl = "imap://" iserver ipath-query
+ * ; Defines an absolute IMAP URL
+ * iserver = [iuserinfo "@"] host [":" port]
+ * ; This is the same as "authority" defined
+ * ; in [URI-GEN].
+ * ipath-query = ["/" [ icommand ]]
+ * ; Corresponds to "path-abempty [ "?" query ]" in
+ * ; [URI-GEN]
+ * icommand = imessagelist /
+ * imessagepart [iurlauth]
+ * imessagelist = imailbox-ref [ "?" enc-search ]
+ * ; "enc-search" is [URI-GEN] "query".
+ * imessagepart = imailbox-ref iuid [isection] [ipartial]
+ * enc-search = 1*bchar
+ * ; %-encoded version of [IMAPABNF]
+ * ; "search-program". Note that IMAP4
+ * ; literals may not be used in
+ * ; a "search-program", i.e., only
+ * ; quoted or non-synchronizing
+ * ; literals (if the server supports
+ * ; LITERAL+ [LITERAL+]) are allowed.
+ */
+
+ /* "imap:" */
+ if ((url_parser->flags & IMAP_URL_PARSE_SCHEME_EXTERNAL) == 0) {
+ const char *scheme;
+
+ if (uri_parse_scheme(parser, &scheme) <= 0) {
+ parser->cur = parser->begin;
+ } else {
+ if (strcasecmp(scheme, "imap") != 0) {
+ parser->error = "Not an IMAP URL";
+ return FALSE;
+ }
+ have_scheme = TRUE;
+ }
+ } else {
+ have_scheme = TRUE;
+ }
+
+ /* "//" iserver */
+ if ((sret = imap_url_parse_iserver(url_parser)) < 0)
+ return FALSE;
+
+ if (have_scheme && sret == 0) {
+ parser->error = "Absolute IMAP URL requires `//' after `imap:'";
+ return FALSE;
+ }
+
+ if (sret > 0 &&
+ (url_parser->flags & IMAP_URL_PARSE_REQUIRE_RELATIVE) != 0) {
+ parser->error = "Relative URL required";
+ return FALSE;
+ }
+
+ /* ipath-query = ["/" [ icommand ]] ; excludes `[ "?" enc-search ]` */
+ if ((ret = uri_parse_path(parser, &relative, &path)) < 0)
+ return FALSE;
+
+ /* Relative urls are only valid when we have a base url */
+ if (sret == 0) {
+ if (url_parser->base == NULL) {
+ parser->error = "Relative URL not allowed";
+ return FALSE;
+ } else if (url_parser->url != NULL) {
+ struct imap_url *url = url_parser->url;
+ const struct imap_url *base = url_parser->base;
+
+ uri_host_copy(parser->pool, &url->host, &base->host);
+ url->port = base->port;
+ url->userid = p_strdup_empty(parser->pool, base->userid);
+ url->auth_type = p_strdup_empty(parser->pool, base->auth_type);
+ }
+
+ url_parser->relative = TRUE;
+ }
+
+ /* Parse path, i.e. `[ icommand ]` from `*( "/" segment )` */
+ if (ret > 0 || url_parser->relative) {
+ if (imap_url_parse_path(url_parser, path, relative,
+ &is_messagelist) < 0)
+ return FALSE;
+ }
+
+ /* [ "?" enc-search ] */
+ if ((ret = uri_parse_query(parser, &query)) != 0) {
+ if (ret < 0)
+ return FALSE;
+
+ if (!is_messagelist) {
+ parser->error =
+ "Search query part only valid for messagelist-type IMAP URL";
+ return FALSE;
+ } else if (*query == '\0') {
+ parser->error = "Empty IMAP URL search query not allowed";
+ return FALSE;
+ }
+
+ if (url_parser->url != NULL) {
+ if (!uri_data_decode(parser, query, NULL, &query))
+ return FALSE;
+ url_parser->url->search_program =
+ p_strdup(parser->pool, query);
+ }
+ }
+
+ /* IMAP URL has no fragment */
+ if ((ret = uri_parse_fragment(parser, &query)) != 0) {
+ if (ret == 1)
+ parser->error = "Fragment component not allowed in IMAP URL";
+ return FALSE;
+ }
+
+ /* must be at end of URL now */
+ i_assert(parser->cur == parser->end);
+
+ return TRUE;
+}
+
+/* Public API */
+
+int imap_url_parse(const char *url, const struct imap_url *base,
+ enum imap_url_parse_flags flags,
+ struct imap_url **url_r, const char **error_r)
+{
+ struct imap_url_parser url_parser;
+
+ /* base != NULL indicates whether relative URLs are allowed. However, certain
+ flags may also dictate whether relative URLs are allowed/required. */
+ i_assert((flags & IMAP_URL_PARSE_REQUIRE_RELATIVE) == 0 || base != NULL);
+ i_assert((flags & IMAP_URL_PARSE_SCHEME_EXTERNAL) == 0 || base == NULL);
+
+ i_zero(&url_parser);
+ uri_parser_init(&url_parser.parser, pool_datastack_create(), url);
+
+ url_parser.url = t_new(struct imap_url, 1);
+ url_parser.url->uauth_expire = (time_t)-1;
+ url_parser.base = base;
+ url_parser.flags = flags;
+
+ if (!imap_url_do_parse(&url_parser)) {
+ *error_r = url_parser.parser.error;
+ return -1;
+ }
+ *url_r = url_parser.url;
+ return 0;
+}
+
+/*
+ * IMAP URL construction
+ */
+
+static void
+imap_url_append_mailbox(const struct imap_url *url, string_t *urlstr)
+{
+ uri_append_path_data(urlstr, ";", url->mailbox);
+ if (url->uidvalidity != 0)
+ str_printfa(urlstr, ";UIDVALIDITY=%u", url->uidvalidity);
+ if (url->uid == 0) {
+ /* message list */
+ if (url->search_program != NULL) {
+ str_append_c(urlstr, '?');
+ uri_append_query_data(urlstr, ";", url->search_program);
+ }
+ } else {
+ /* message part */
+ str_printfa(urlstr, "/;UID=%u", url->uid);
+ if (url->section != NULL) {
+ str_append(urlstr, "/;SECTION=");
+ uri_append_path_data(urlstr, ";", url->section);
+ }
+ if (url->have_partial) {
+ str_append(urlstr, "/;PARTIAL=");
+ if (url->partial_size == 0) {
+ str_printfa(urlstr, "%"PRIuUOFF_T,
+ url->partial_offset);
+ } else {
+ str_printfa(urlstr, "%"PRIuUOFF_T".%"PRIuUOFF_T,
+ url->partial_offset,
+ url->partial_size);
+ }
+ }
+
+ /* urlauth */
+ if (url->uauth_access_application != NULL) {
+ if (url->uauth_expire != (time_t)-1) {
+ str_append(urlstr, ";EXPIRE=");
+ str_append(urlstr, iso8601_date_create(url->uauth_expire));
+ }
+ str_append(urlstr, ";URLAUTH=");
+ str_append(urlstr, url->uauth_access_application);
+ if (url->uauth_access_user != NULL) {
+ str_append_c(urlstr, '+');
+ uri_append_user_data(urlstr, ";",
+ url->uauth_access_user);
+ }
+ }
+ }
+}
+
+const char *imap_url_create(const struct imap_url *url)
+{
+ string_t *urlstr = t_str_new(512);
+
+ /* scheme */
+ uri_append_scheme(urlstr, "imap");
+ str_append(urlstr, "//");
+
+ /* user */
+ if (url->userid != NULL || url->auth_type != NULL) {
+ if (url->userid != NULL)
+ uri_append_user_data(urlstr, ";:", url->userid);
+ if (url->auth_type != NULL) {
+ str_append(urlstr, ";AUTH=");
+ uri_append_user_data(urlstr, ";:", url->auth_type);
+ }
+ str_append_c(urlstr, '@');
+ }
+
+ /* server */
+ uri_append_host(urlstr, &url->host);
+ uri_append_port(urlstr, url->port);
+
+ /* Older syntax (RFC 2192) requires this slash at all times */
+ str_append_c(urlstr, '/');
+
+ /* mailbox */
+ if (url->mailbox != NULL)
+ imap_url_append_mailbox(url, urlstr);
+ return str_c(urlstr);
+}
+
+const char *
+imap_url_add_urlauth(const char *rumpurl, const char *mechanism,
+ const unsigned char *token, size_t token_len)
+{
+ return t_strconcat(rumpurl, ":", t_str_lcase(mechanism), ":",
+ binary_to_hex(token, token_len), NULL);
+}
diff --git a/src/lib-imap/imap-url.h b/src/lib-imap/imap-url.h
new file mode 100644
index 0000000..9f0b2aa
--- /dev/null
+++ b/src/lib-imap/imap-url.h
@@ -0,0 +1,71 @@
+#ifndef IMAP_URL_H
+#define IMAP_URL_H
+
+#include "uri-util.h"
+
+struct imap_url {
+ /* server */
+ struct uri_host host;
+ in_port_t port;
+
+ /* user */
+ const char *userid;
+ const char *auth_type;
+
+ /* mailbox */
+ const char *mailbox;
+ uint32_t uidvalidity; /* 0 if not set */
+
+ /* message part */
+ uint32_t uid;
+ const char *section;
+ uoff_t partial_offset;
+ uoff_t partial_size; /* 0 if not set */
+
+ /* message list (uid == 0) */
+ const char *search_program;
+
+ /* urlauth */
+ const char *uauth_rumpurl;
+ const char *uauth_access_application;
+ const char *uauth_access_user;
+ const char *uauth_mechanism;
+ const unsigned char *uauth_token;
+ size_t uauth_token_size;
+ time_t uauth_expire; /* (time_t)-1 if not set */
+
+ bool have_partial:1;
+};
+
+/*
+ * IMAP URL parsing
+ */
+
+enum imap_url_parse_flags {
+ /* Scheme part 'imap:' is already parsed externally. This implies that
+ this is an absolute IMAP URL. */
+ IMAP_URL_PARSE_SCHEME_EXTERNAL = 0x01,
+ /* Require relative URL (omitting _both_ scheme and authority), e.g.
+ /MAILBOX/;UID=uid or even ;UID=uid. This flag means that an absolute
+ URL makes no sense in this context. Relative URLs are allowed once a
+ base URL is provided to the parser. */
+ IMAP_URL_PARSE_REQUIRE_RELATIVE = 0x02,
+ /* Allow URLAUTH URL */
+ IMAP_URL_PARSE_ALLOW_URLAUTH = 0x04
+};
+
+/* Parses full IMAP URL. The returned URL is allocated from data stack. */
+int imap_url_parse(const char *url, const struct imap_url *base,
+ enum imap_url_parse_flags flags,
+ struct imap_url **url_r, const char **error_r);
+
+/*
+ * IMAP URL construction
+ */
+
+const char *imap_url_create(const struct imap_url *url);
+
+const char *imap_url_add_urlauth(const char *rumpurl, const char *mechanism,
+ const unsigned char *token, size_t token_len);
+
+#endif
diff --git a/src/lib-imap/imap-utf7.c b/src/lib-imap/imap-utf7.c
new file mode 100644
index 0000000..7ea53f5
--- /dev/null
+++ b/src/lib-imap/imap-utf7.c
@@ -0,0 +1,380 @@
+/* Copyright (c) 2008-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "str.h"
+#include "unichar.h"
+#include "imap-utf7.h"
+
+static const char imap_b64enc[] =
+ "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+,";
+
+#define XX 0xff
+static const unsigned char imap_b64dec[256] = {
+ XX,XX,XX,XX, XX,XX,XX,XX, XX,XX,XX,XX, XX,XX,XX,XX,
+ XX,XX,XX,XX, XX,XX,XX,XX, XX,XX,XX,XX, XX,XX,XX,XX,
+ XX,XX,XX,XX, XX,XX,XX,XX, XX,XX,XX,62, 63,XX,XX,XX,
+ 52,53,54,55, 56,57,58,59, 60,61,XX,XX, XX,XX,XX,XX,
+ XX, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9,10, 11,12,13,14,
+ 15,16,17,18, 19,20,21,22, 23,24,25,XX, XX,XX,XX,XX,
+ XX,26,27,28, 29,30,31,32, 33,34,35,36, 37,38,39,40,
+ 41,42,43,44, 45,46,47,48, 49,50,51,XX, XX,XX,XX,XX,
+ XX,XX,XX,XX, XX,XX,XX,XX, XX,XX,XX,XX, XX,XX,XX,XX,
+ XX,XX,XX,XX, XX,XX,XX,XX, XX,XX,XX,XX, XX,XX,XX,XX,
+ XX,XX,XX,XX, XX,XX,XX,XX, XX,XX,XX,XX, XX,XX,XX,XX,
+ XX,XX,XX,XX, XX,XX,XX,XX, XX,XX,XX,XX, XX,XX,XX,XX,
+ XX,XX,XX,XX, XX,XX,XX,XX, XX,XX,XX,XX, XX,XX,XX,XX,
+ XX,XX,XX,XX, XX,XX,XX,XX, XX,XX,XX,XX, XX,XX,XX,XX,
+ XX,XX,XX,XX, XX,XX,XX,XX, XX,XX,XX,XX, XX,XX,XX,XX,
+ XX,XX,XX,XX, XX,XX,XX,XX, XX,XX,XX,XX, XX,XX,XX,XX
+};
+
+static void
+mbase64_encode(string_t *dest, const unsigned char *in, size_t len)
+{
+ str_append_c(dest, '&');
+ while (len >= 3) {
+ str_append_c(dest, imap_b64enc[in[0] >> 2]);
+ str_append_c(dest, imap_b64enc[((in[0] & 3) << 4) |
+ (in[1] >> 4)]);
+ str_append_c(dest, imap_b64enc[((in[1] & 0x0f) << 2) |
+ ((in[2] & 0xc0) >> 6)]);
+ str_append_c(dest, imap_b64enc[in[2] & 0x3f]);
+ in += 3;
+ len -= 3;
+ }
+ if (len > 0) {
+ str_append_c(dest, imap_b64enc[in[0] >> 2]);
+ if (len == 1)
+ str_append_c(dest, imap_b64enc[(in[0] & 0x03) << 4]);
+ else {
+ str_append_c(dest, imap_b64enc[((in[0] & 0x03) << 4) |
+ (in[1] >> 4)]);
+ str_append_c(dest, imap_b64enc[(in[1] & 0x0f) << 2]);
+ }
+ }
+ str_append_c(dest, '-');
+}
+
+static const char *
+imap_utf8_first_encode_char(const char *str, char escape_char)
+{
+ const char *p;
+
+ for (p = str; *p != '\0'; p++) {
+ if (*p == '&' || *p < 0x20 || *p >= 0x7f || *p == escape_char)
+ return p;
+ }
+ return NULL;
+}
+
+int imap_escaped_utf8_hex_to_char(const char *str, unsigned char *chr_r)
+{
+ unsigned int i = 0;
+ unsigned char c = 0;
+
+ /* NOTE: Only lowercase hex characters are allowed so the output is
+ reversible. */
+ for (;;) {
+ if (str[i] >= '0' && str[i] <= '9')
+ c += str[i] - '0';
+ else if (str[i] >= 'a' && str[i] <= 'f')
+ c += str[i] - 'a' + 10;
+ else
+ return -1;
+ if (++i == 2)
+ break;
+ c *= 0x10;
+ }
+ *chr_r = c;
+ return 0;
+}
+
+static int
+imap_utf8_to_utf7_int(const char *src, char escape_char, string_t *dest)
+{
+ const char *p;
+ unichar_t chr;
+ uint8_t *utf16, *u;
+ uint16_t u16;
+ unsigned char c;
+
+ p = imap_utf8_first_encode_char(src, escape_char);
+ if (p == NULL) {
+ /* no characters that need to be encoded */
+ str_append(dest, src);
+ return 0;
+ }
+
+ /* at least one encoded character */
+ str_append_data(dest, src, p-src);
+ utf16 = t_malloc0(MALLOC_MULTIPLY(strlen(p), 2));
+ while (*p != '\0') {
+ if (*p == escape_char &&
+ imap_escaped_utf8_hex_to_char(p+1, &c) == 0) {
+ str_append_c(dest, c);
+ p += 3;
+ continue;
+ }
+ if (*p == '&') {
+ str_append(dest, "&-");
+ p++;
+ continue;
+ }
+ if (*p >= 0x20 && *p < 0x7f) {
+ str_append_c(dest, *p);
+ p++;
+ continue;
+ }
+
+ u = utf16;
+ while (*p != '\0' && (*p < 0x20 || *p >= 0x7f)) {
+ if (uni_utf8_get_char(p, &chr) <= 0)
+ return -1;
+ /* @UNSAFE */
+ if (chr < UTF16_SURROGATE_BASE) {
+ *u++ = chr >> 8;
+ *u++ = chr & 0xff;
+ } else {
+ u16 = UTF16_SURROGATE_HIGH(chr);
+ *u++ = u16 >> 8;
+ *u++ = u16 & 0xff;
+ u16 = UTF16_SURROGATE_LOW(chr);
+ *u++ = u16 >> 8;
+ *u++ = u16 & 0xff;
+ }
+ p += uni_utf8_char_bytes((unsigned char)*p);
+ }
+ mbase64_encode(dest, utf16, u-utf16);
+ }
+ return 0;
+}
+
+int imap_utf8_to_utf7(const char *src, string_t *dest)
+{
+ return imap_utf8_to_utf7_int(src, '\0', dest);
+}
+
+int imap_escaped_utf8_to_utf7(const char *src, char escape_char, string_t *dest)
+{
+ i_assert(escape_char != '&');
+
+ return imap_utf8_to_utf7_int(src, escape_char, dest);
+}
+
+int t_imap_utf8_to_utf7(const char *src, const char **dest_r)
+{
+ string_t *str;
+ int ret;
+
+ if (imap_utf8_first_encode_char(src, '\0') == NULL) {
+ *dest_r = src;
+ return 0;
+ }
+
+ str = t_str_new(64);
+ ret = imap_utf8_to_utf7(src, str);
+ *dest_r = str_c(str);
+ return ret;
+}
+
+static int utf16buf_to_utf8(string_t *dest, const unsigned char output[4],
+ unsigned int *_pos, unsigned int len)
+{
+ unsigned int pos = *_pos;
+ uint16_t high, low;
+ unichar_t chr;
+
+ if (len % 2 != 0)
+ return -1;
+
+ high = (output[pos % 4] << 8) | output[(pos+1) % 4];
+ if (high < UTF16_SURROGATE_HIGH_FIRST ||
+ high > UTF16_SURROGATE_HIGH_MAX) {
+ /* single byte */
+ size_t oldlen = str_len(dest);
+
+ if (high == 0) {
+ /* Encoded NUL isn't going to work in Dovecot code,
+ even though it's technically valid. Return failure
+ so the callers don't even get a chance to handle the
+ NUL in the string inconsistently. */
+ return -1;
+ }
+ uni_ucs4_to_utf8_c(high, dest);
+ if (str_len(dest) - oldlen == 1) {
+ unsigned char last = str_data(dest)[oldlen];
+ if (last >= 0x20 && last < 0x7f)
+ return -1;
+ }
+ *_pos = (pos + 2) % 4;
+ return 0;
+ }
+
+ if (high > UTF16_SURROGATE_HIGH_LAST)
+ return -1;
+ if (len != 4) {
+ /* missing the second character */
+ return -1;
+ }
+
+ low = (output[(pos+2)%4] << 8) | output[(pos+3) % 4];
+ if (low < UTF16_SURROGATE_LOW_FIRST || low > UTF16_SURROGATE_LOW_LAST)
+ return -1;
+
+ chr = UTF16_SURROGATE_BASE +
+ (((high & UTF16_SURROGATE_MASK) << UTF16_SURROGATE_SHIFT) |
+ (low & UTF16_SURROGATE_MASK));
+ uni_ucs4_to_utf8_c(chr, dest);
+ return 0;
+}
+
+static int mbase64_decode_to_utf8(string_t *dest, const char **_src)
+{
+ const char *src = *_src;
+ unsigned char input[4], output[4];
+ unsigned int outstart = 0, outpos = 0;
+
+ while (*src != '-') {
+ input[0] = imap_b64dec[(uint8_t)src[0]];
+ if (input[0] == 0xff)
+ return -1;
+ input[1] = imap_b64dec[(uint8_t)src[1]];
+ if (input[1] == 0xff)
+ return -1;
+
+ output[outpos % 4] = (input[0] << 2) | (input[1] >> 4);
+ if (++outpos % 4 == outstart) {
+ if (utf16buf_to_utf8(dest, output, &outstart, 4) < 0)
+ return -1;
+ }
+
+ input[2] = imap_b64dec[(uint8_t)src[2]];
+ if (input[2] == 0xff) {
+ if (src[2] != '-')
+ return -1;
+
+ src += 2;
+ break;
+ }
+
+ output[outpos % 4] = ((input[1] << 4) & 0xff) | (input[2] >> 2);
+ if (++outpos % 4 == outstart) {
+ if (utf16buf_to_utf8(dest, output, &outstart, 4) < 0)
+ return -1;
+ }
+
+ input[3] = imap_b64dec[(uint8_t)src[3]];
+ if (input[3] == 0xff) {
+ if (src[3] != '-')
+ return -1;
+
+ src += 3;
+ break;
+ }
+
+ output[outpos % 4] = ((input[2] << 6) & 0xc0) | input[3];
+ if (++outpos % 4 == outstart) {
+ if (utf16buf_to_utf8(dest, output, &outstart, 4) < 0)
+ return -1;
+ }
+
+ src += 4;
+ }
+ if (outstart != outpos % 4) {
+ if (utf16buf_to_utf8(dest, output, &outstart,
+ (4 + outpos - outstart) % 4) < 0)
+ return -1;
+ }
+
+ /* Found the ending '-'. Make sure it's not followed by unnecessary
+ shift. Note that '&' is always escaped as "&-" so it's not an
+ unnecessary shift. */
+ if (src[1] == '&' && src[2] != '-')
+ return -1;
+
+ *_src = src + 1;
+ return 0;
+}
+
+static int
+imap_utf7_to_utf8_int(const char *src, const char *escape_chars, string_t *dest)
+{
+ const char *p;
+
+ for (p = src; *p != '\0'; p++) {
+ if (*p < 0x20 || *p >= 0x7f) {
+ if (escape_chars[0] == '\0')
+ return -1;
+ break;
+ }
+ if (*p == '&' || strchr(escape_chars, *p) != NULL)
+ break;
+ }
+ if (*p == '\0') {
+ /* no IMAP-UTF-7 encoded characters */
+ str_append(dest, src);
+ return 0;
+ }
+
+ /* at least one encoded character */
+ str_append_data(dest, src, p-src);
+ while (*p != '\0') {
+ if (strchr(escape_chars, *p) != NULL ||
+ *p < 0x20 || *p >= 0x7f) {
+ str_printfa(dest, "%c%02x", escape_chars[0],
+ (unsigned char)*p);
+ p++;
+ } else if (*p == '&') {
+ if (*++p == '-') {
+ str_append_c(dest, '&');
+ p++;
+ } else {
+ size_t orig_size = str_len(dest);
+ if (mbase64_decode_to_utf8(dest, &p) < 0) {
+ if (escape_chars[0] == '\0')
+ return -1;
+ str_truncate(dest, orig_size);
+ str_printfa(dest, "%c26", escape_chars[0]);
+ }
+ }
+ } else {
+ str_append_c(dest, *p++);
+ }
+ }
+ return 0;
+}
+
+int imap_utf7_to_utf8(const char *src, string_t *dest)
+{
+ return imap_utf7_to_utf8_int(src, "", dest);
+}
+
+void imap_utf7_to_utf8_escaped(const char *src, const char *escape_chars,
+ string_t *dest)
+{
+ i_assert(escape_chars[0] != '&');
+
+ if (imap_utf7_to_utf8_int(src, escape_chars, dest) < 0)
+ i_unreached();
+}
+
+bool imap_utf7_is_valid(const char *src)
+{
+ const char *p;
+ int ret;
+
+ for (p = src; *p != '\0'; p++) {
+ if (*p < 0x20 || *p >= 0x7f)
+ return FALSE;
+ if (*p == '&') {
+ /* slow scan */
+ T_BEGIN {
+ string_t *tmp = t_str_new(128);
+ ret = imap_utf7_to_utf8(p, tmp);
+ } T_END;
+ if (ret < 0)
+ return FALSE;
+ }
+ }
+ return TRUE;
+}
diff --git a/src/lib-imap/imap-utf7.h b/src/lib-imap/imap-utf7.h
new file mode 100644
index 0000000..d7ae306
--- /dev/null
+++ b/src/lib-imap/imap-utf7.h
@@ -0,0 +1,28 @@
+#ifndef IMAP_UTF7_H
+#define IMAP_UTF7_H
+
+/* Convert an UTF-8 string to IMAP-UTF-7. Returns 0 if ok, -1 if src isn't
+ valid UTF-8. */
+int imap_utf8_to_utf7(const char *src, string_t *dest);
+int t_imap_utf8_to_utf7(const char *src, const char **dest_r);
+/* Like imap_utf8_to_utf7(), but decode all <escape_char><hex> instances.
+ Returns -1 if src isn't valid UTF-8. Note that invalid <escape_char> content
+ isn't treated as an error - it's simply passed through. */
+int imap_escaped_utf8_to_utf7(const char *src, char escape_char, string_t *dest);
+/* For manually parsing the <hex> after <escape_char>. Returns 0 on success,
+ -1 if str doesn't point to valid <hex>. */
+int imap_escaped_utf8_hex_to_char(const char *str, unsigned char *chr_r);
+
+/* Convert IMAP-UTF-7 string to UTF-8. Returns 0 if ok, -1 if src isn't
+ valid IMAP-UTF-7. */
+int imap_utf7_to_utf8(const char *src, string_t *dest);
+/* Like imap_utf7_to_utf8(), but write invalid input as <escape_chars[0]><hex>.
+ All the characters in escape_chars[] are escaped in the same way. This
+ allows converting the escaped output back to the original (broken)
+ IMAP-UTF-7 input. */
+void imap_utf7_to_utf8_escaped(const char *src, const char *escape_chars,
+ string_t *dest);
+/* Returns TRUE if the string is valid IMAP-UTF-7 string. */
+bool imap_utf7_is_valid(const char *src);
+
+#endif
diff --git a/src/lib-imap/imap-util.c b/src/lib-imap/imap-util.c
new file mode 100644
index 0000000..dc1ae24
--- /dev/null
+++ b/src/lib-imap/imap-util.c
@@ -0,0 +1,202 @@
+/* Copyright (c) 2002-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "str.h"
+#include "strescape.h"
+#include "unichar.h"
+#include "mail-types.h"
+#include "imap-parser.h"
+#include "imap-util.h"
+
+void imap_write_flags(string_t *dest, enum mail_flags flags,
+ const char *const *keywords)
+{
+ size_t size;
+
+ size = str_len(dest);
+ if ((flags & MAIL_ANSWERED) != 0)
+ str_append(dest, "\\Answered ");
+ if ((flags & MAIL_FLAGGED) != 0)
+ str_append(dest, "\\Flagged ");
+ if ((flags & MAIL_DELETED) != 0)
+ str_append(dest, "\\Deleted ");
+ if ((flags & MAIL_SEEN) != 0)
+ str_append(dest, "\\Seen ");
+ if ((flags & MAIL_DRAFT) != 0)
+ str_append(dest, "\\Draft ");
+ if ((flags & MAIL_RECENT) != 0)
+ str_append(dest, "\\Recent ");
+
+ if (keywords != NULL) {
+ /* we have keywords too */
+ while (*keywords != NULL) {
+ str_append(dest, *keywords);
+ str_append_c(dest, ' ');
+ keywords++;
+ }
+ }
+
+ if (str_len(dest) != size)
+ str_truncate(dest, str_len(dest)-1);
+}
+
+enum mail_flags imap_parse_system_flag(const char *str)
+{
+ if (strcasecmp(str, "\\Answered") == 0)
+ return MAIL_ANSWERED;
+ else if (strcasecmp(str, "\\Flagged") == 0)
+ return MAIL_FLAGGED;
+ else if (strcasecmp(str, "\\Deleted") == 0)
+ return MAIL_DELETED;
+ else if (strcasecmp(str, "\\Seen") == 0)
+ return MAIL_SEEN;
+ else if (strcasecmp(str, "\\Draft") == 0)
+ return MAIL_DRAFT;
+ else if (strcasecmp(str, "\\Recent") == 0)
+ return MAIL_RECENT;
+ else
+ return 0;
+}
+
+void imap_write_seq_range(string_t *dest, const ARRAY_TYPE(seq_range) *array)
+{
+ const struct seq_range *range;
+ unsigned int i, count;
+
+ range = array_get(array, &count);
+ for (i = 0; i < count; i++) {
+ if (i > 0)
+ str_append_c(dest, ',');
+ str_printfa(dest, "%u", range[i].seq1);
+ if (range[i].seq1 != range[i].seq2)
+ str_printfa(dest, ":%u", range[i].seq2);
+ }
+}
+
+void imap_write_arg(string_t *dest, const struct imap_arg *arg)
+{
+ switch (arg->type) {
+ case IMAP_ARG_NIL:
+ str_append(dest, "NIL");
+ break;
+ case IMAP_ARG_ATOM:
+ str_append(dest, imap_arg_as_astring(arg));
+ break;
+ case IMAP_ARG_STRING: {
+ const char *strarg = imap_arg_as_astring(arg);
+ str_append_c(dest, '"');
+ str_append_escaped(dest, strarg, strlen(strarg));
+ str_append_c(dest, '"');
+ break;
+ }
+ case IMAP_ARG_LITERAL: {
+ const char *strarg = imap_arg_as_astring(arg);
+ str_printfa(dest, "{%zu}\r\n",
+ strlen(strarg));
+ str_append(dest, strarg);
+ break;
+ }
+ case IMAP_ARG_LIST:
+ str_append_c(dest, '(');
+ imap_write_args(dest, imap_arg_as_list(arg));
+ str_append_c(dest, ')');
+ break;
+ case IMAP_ARG_LITERAL_SIZE:
+ case IMAP_ARG_LITERAL_SIZE_NONSYNC:
+ str_printfa(dest, "<%"PRIuUOFF_T" byte literal>",
+ imap_arg_as_literal_size(arg));
+ break;
+ case IMAP_ARG_EOL:
+ i_unreached();
+ }
+}
+
+void imap_write_args(string_t *dest, const struct imap_arg *args)
+{
+ bool first = TRUE;
+
+ for (; !IMAP_ARG_IS_EOL(args); args++) {
+ if (first)
+ first = FALSE;
+ else
+ str_append_c(dest, ' ');
+ imap_write_arg(dest, args);
+ }
+}
+
+static void imap_human_args_fix_control_chars(char *str)
+{
+ size_t i;
+
+ for (i = 0; str[i] != '\0'; i++) {
+ if (str[i] < 0x20 || str[i] == 0x7f)
+ str[i] = '?';
+ }
+}
+
+void imap_write_args_for_human(string_t *dest, const struct imap_arg *args)
+{
+ bool first = TRUE;
+
+ for (; !IMAP_ARG_IS_EOL(args); args++) {
+ if (first)
+ first = FALSE;
+ else
+ str_append_c(dest, ' ');
+
+ switch (args->type) {
+ case IMAP_ARG_NIL:
+ str_append(dest, "NIL");
+ break;
+ case IMAP_ARG_ATOM:
+ /* atom has only printable us-ascii chars */
+ str_append(dest, imap_arg_as_astring(args));
+ break;
+ case IMAP_ARG_STRING:
+ case IMAP_ARG_LITERAL: {
+ const char *strarg = imap_arg_as_astring(args);
+
+ if (strpbrk(strarg, "\r\n") != NULL) {
+ str_printfa(dest, "<%zu byte multi-line literal>",
+ strlen(strarg));
+ break;
+ }
+ strarg = str_escape(strarg);
+
+ str_append_c(dest, '"');
+ size_t start_pos = str_len(dest);
+ /* append only valid UTF-8 chars */
+ if (uni_utf8_get_valid_data((const unsigned char *)strarg,
+ strlen(strarg), dest))
+ str_append(dest, strarg);
+ /* replace all control chars */
+ imap_human_args_fix_control_chars(
+ str_c_modifiable(dest) + start_pos);
+ str_append_c(dest, '"');
+ break;
+ }
+ case IMAP_ARG_LIST:
+ str_append_c(dest, '(');
+ imap_write_args_for_human(dest, imap_arg_as_list(args));
+ str_append_c(dest, ')');
+ break;
+ case IMAP_ARG_LITERAL_SIZE:
+ case IMAP_ARG_LITERAL_SIZE_NONSYNC:
+ str_printfa(dest, "<%"PRIuUOFF_T" byte literal>",
+ imap_arg_as_literal_size(args));
+ break;
+ case IMAP_ARG_EOL:
+ i_unreached();
+ }
+ }
+}
+
+const char *imap_args_to_str(const struct imap_arg *args)
+{
+ string_t *str;
+
+ str = t_str_new(128);
+ imap_write_args(str, args);
+ return str_c(str);
+}
diff --git a/src/lib-imap/imap-util.h b/src/lib-imap/imap-util.h
new file mode 100644
index 0000000..47b7d2c
--- /dev/null
+++ b/src/lib-imap/imap-util.h
@@ -0,0 +1,29 @@
+#ifndef IMAP_UTIL_H
+#define IMAP_UTIL_H
+
+#include "seq-range-array.h"
+#include "mail-types.h"
+
+struct imap_arg;
+
+/* Write flags as a space separated string. */
+void imap_write_flags(string_t *dest, enum mail_flags flags,
+ const char *const *keywords) ATTR_NULL(3);
+/* Parse system flag from a string, or return 0 if it's invalid. */
+enum mail_flags imap_parse_system_flag(const char *str);
+
+/* Write sequence range as IMAP sequence-set */
+void imap_write_seq_range(string_t *dest, const ARRAY_TYPE(seq_range) *array);
+/* Write IMAP arg to the given string. Because IMAP_ARG_LITERAL_SIZE* have no
+ content, they're written as "{size}\r\n<too large>". */
+void imap_write_arg(string_t *dest, const struct imap_arg *arg);
+/* Same as imap_write_arg(), but write all the args until EOL. */
+void imap_write_args(string_t *dest, const struct imap_arg *args);
+/* Write IMAP args in a human-readable format to given string (e.g. for
+ logging). The output is a single valid UTF-8 line without control
+ characters. Multi-line literals are replaced with a generic placeholder. */
+void imap_write_args_for_human(string_t *dest, const struct imap_arg *args);
+/* Like imap_write_args(), but return the string allocated from data stack. */
+const char *imap_args_to_str(const struct imap_arg *args);
+
+#endif
diff --git a/src/lib-imap/test-imap-bodystructure.c b/src/lib-imap/test-imap-bodystructure.c
new file mode 100644
index 0000000..6035118
--- /dev/null
+++ b/src/lib-imap/test-imap-bodystructure.c
@@ -0,0 +1,733 @@
+/* Copyright (c) 2013-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "istream.h"
+#include "str.h"
+#include "message-part-data.h"
+#include "message-part-serialize.h"
+#include "message-parser.h"
+#include "imap-bodystructure.h"
+#include "test-common.h"
+
+struct parse_test {
+ const char *message;
+ const char *body;
+ const char *bodystructure;
+};
+
+struct parse_test parse_tests[] = {
+ {
+ .message =
+ "From: user@domain.org\n"
+ "Date: Sat, 24 Mar 2017 23:00:00 +0200\n"
+ "Mime-Version: 1.0\n"
+ "Content-Type: text/plain; charset=us-ascii\n"
+ "\n"
+ "body\n",
+ .bodystructure =
+ "\"text\" \"plain\" (\"charset\" \"us-ascii\") NIL NIL \"7bit\" 6 1 NIL NIL NIL NIL",
+ .body =
+ "\"text\" \"plain\" (\"charset\" \"us-ascii\") NIL NIL \"7bit\" 6 1"
+ },{
+ .message =
+ "From: user@domain.org\n"
+ "Date: Sat, 24 Mar 2017 23:00:00 +0200\n"
+ "Mime-Version: 1.0\n"
+ "Content-Type: text/plain; charset=utf-8\n"
+ "Content-Transfer-Encoding: 8bit\n"
+ "\n"
+ "body\n"
+ "\n",
+ .bodystructure =
+ "\"text\" \"plain\" (\"charset\" \"utf-8\") NIL NIL \"8bit\" 8 2 NIL NIL NIL NIL",
+ .body =
+ "\"text\" \"plain\" (\"charset\" \"utf-8\") NIL NIL \"8bit\" 8 2"
+ },{
+ .message =
+ "From: user@domain.org\n"
+ "Date: Sat, 24 Mar 2007 23:00:00 +0200\n"
+ "Mime-Version: 1.0\n"
+ "Content-Type: multipart/mixed; boundary=\"foo\n"
+ " bar\"\n"
+ "\n"
+ "--foo bar\n"
+ "Content-Type: text/x-myown; charset=us-ascii\n"
+ "\n"
+ "hello\n"
+ "\n"
+ "--foo bar--\n"
+ "\n",
+ .bodystructure =
+ "(\"text\" \"x-myown\" (\"charset\" \"us-ascii\") NIL NIL \"7bit\" 7 1 NIL NIL NIL NIL) \"mixed\" (\"boundary\" \"foo bar\") NIL NIL NIL",
+ .body =
+ "(\"text\" \"x-myown\" (\"charset\" \"us-ascii\") NIL NIL \"7bit\" 7 1) \"mixed\""
+ },{
+ .message =
+ "From: user@domain.org\n"
+ "Date: Sat, 24 Mar 2017 23:00:00 +0200\n"
+ "Mime-Version: 1.0\n"
+ "Content-Type: multipart/mixed; boundary=\"foo bar\"\n"
+ "\n"
+ "--foo bar\n"
+ "Content-Type: text/plain; charset=us-ascii\n"
+ "\n"
+ "See attached...\n"
+ "\n"
+ "--foo bar\n"
+ "Content-Type: message/rfc822\n"
+ "\n"
+ "From: user@domain.org\n"
+ "Date: Sat, 24 Mar 2017 23:00:00 +0200\n"
+ "Mime-Version: 1.0\n"
+ "Content-Type: text/plain; charset=us-ascii\n"
+ "\n"
+ "body\n"
+ "\n"
+ "--foo bar--\n"
+ "\n",
+ .bodystructure =
+ "(\"text\" \"plain\" (\"charset\" \"us-ascii\") NIL NIL \"7bit\" 17 1 NIL NIL NIL NIL)(\"message\" \"rfc822\" NIL NIL NIL \"7bit\" 133 (\"Sat, 24 Mar 2017 23:00:00 +0200\" NIL ((NIL NIL \"user\" \"domain.org\")) ((NIL NIL \"user\" \"domain.org\")) ((NIL NIL \"user\" \"domain.org\")) NIL NIL NIL NIL NIL) (\"text\" \"plain\" (\"charset\" \"us-ascii\") NIL NIL \"7bit\" 6 1 NIL NIL NIL NIL) 6 NIL NIL NIL NIL) \"mixed\" (\"boundary\" \"foo bar\") NIL NIL NIL",
+ .body =
+ "(\"text\" \"plain\" (\"charset\" \"us-ascii\") NIL NIL \"7bit\" 17 1)(\"message\" \"rfc822\" NIL NIL NIL \"7bit\" 133 (\"Sat, 24 Mar 2017 23:00:00 +0200\" NIL ((NIL NIL \"user\" \"domain.org\")) ((NIL NIL \"user\" \"domain.org\")) ((NIL NIL \"user\" \"domain.org\")) NIL NIL NIL NIL NIL) (\"text\" \"plain\" (\"charset\" \"us-ascii\") NIL NIL \"7bit\" 6 1) 6) \"mixed\""
+ },{
+ .message =
+ "From: user@domain.org\n"
+ "Date: Sat, 24 Mar 2017 23:00:00 +0200\n"
+ "Mime-Version: 1.0\n"
+ "Content-Type: multipart/mixed; boundary=\"foo bar\"\n"
+ "\n"
+ "--foo bar\n"
+ "Content-Type: text/plain; charset=us-ascii\n"
+ "Content-ID: <A.frop.example.com>\n"
+ "Content-Description: Container message\n"
+ "\n"
+ "See attached...\n"
+ "\n"
+ "--foo bar\n"
+ "Content-Type: message/rfc822\n"
+ "Content-ID: <B.frop.example.com>\n"
+ "Content-Description: Forwarded\n"
+ "\n"
+ "From: user@domain.org\n"
+ "Date: Sat, 24 Mar 2017 23:00:00 +0200\n"
+ "Mime-Version: 1.0\n"
+ "Content-Type: text/plain; charset=us-ascii\n"
+ "\n"
+ "body\n"
+ "\n"
+ "--foo bar--\n",
+ .bodystructure =
+ "(\"text\" \"plain\" (\"charset\" \"us-ascii\") \"<A.frop.example.com>\" \"Container message\" \"7bit\" 17 1 NIL NIL NIL NIL)(\"message\" \"rfc822\" NIL \"<B.frop.example.com>\" \"Forwarded\" \"7bit\" 133 (\"Sat, 24 Mar 2017 23:00:00 +0200\" NIL ((NIL NIL \"user\" \"domain.org\")) ((NIL NIL \"user\" \"domain.org\")) ((NIL NIL \"user\" \"domain.org\")) NIL NIL NIL NIL NIL) (\"text\" \"plain\" (\"charset\" \"us-ascii\") NIL NIL \"7bit\" 6 1 NIL NIL NIL NIL) 6 NIL NIL NIL NIL) \"mixed\" (\"boundary\" \"foo bar\") NIL NIL NIL",
+ .body =
+ "(\"text\" \"plain\" (\"charset\" \"us-ascii\") \"<A.frop.example.com>\" \"Container message\" \"7bit\" 17 1)(\"message\" \"rfc822\" NIL \"<B.frop.example.com>\" \"Forwarded\" \"7bit\" 133 (\"Sat, 24 Mar 2017 23:00:00 +0200\" NIL ((NIL NIL \"user\" \"domain.org\")) ((NIL NIL \"user\" \"domain.org\")) ((NIL NIL \"user\" \"domain.org\")) NIL NIL NIL NIL NIL) (\"text\" \"plain\" (\"charset\" \"us-ascii\") NIL NIL \"7bit\" 6 1) 6) \"mixed\""
+ },{
+ .message =
+ "From: user@domain.org\n"
+ "Date: Sat, 24 Mar 2017 23:00:00 +0200\n"
+ "Mime-Version: 1.0\n"
+ "Content-Type: multipart/mixed; boundary=\"foo bar\"\n"
+ "\n"
+ "--foo bar\n"
+ "Content-Type: text/plain; charset=us-ascii; format=\"flowed\";\n"
+ " delsp=\"no\"\n"
+ "Content-Language: la\n"
+ "\n"
+ "Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean commodo\n"
+ "ligula eget dolor. Aenean massa. Cum sociis natoque penatibus et magnis dis\n"
+ "parturient montes, nascetur ridiculus mus. Donec quam felis, ultricies nec,\n"
+ "pellentesque eu, pretium quis, sem. Nulla consequat massa quis enim. Donec\n"
+ "pede justo, fringilla vel, aliquet nec, vulputate eget, arcu. In enim justo,\n"
+ "rhoncus ut, imperdiet a, venenatis vitae, justo. Nullam dictum felis eu pede\n"
+ "mollis pretium. Integer tincidunt. Cras dapibus. Vivamus elementum semper\n"
+ "nisi. Aenean vulputate eleifend tellus. Aenean leo ligula, porttitor eu,\n"
+ "consequat vitae, eleifend ac, enim. Aliquam lorem ante, dapibus in, viverra\n"
+ "quis, feugiat a, tellus. Phasellus viverra nulla ut metus varius laoreet.\n"
+ "Quisque rutrum. Aenean imperdiet. Etiam ultricies nisi vel augue. Curabitur\n"
+ "ullamcorper ultricies nisi. Nam eget dui.\n"
+ "\n"
+ "--foo bar\n"
+ "Content-Type: image/png\n"
+ "Content-Transfer-Encoding: base64\n"
+ "Content-Disposition: attachment; filename=\"pigeon.png\"\n"
+ "\n"
+ "iVBORw0KGgoAAAANSUhEUgAAAB8AAAAfCAYAAAAfrhY5AAAABHNCSVQICAgIfAhkiAAAAAlwSFlz\n"
+ "AAAGJwAABicBTVTYxwAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAAN2SURB\n"
+ "VEiJ7ZfdK7tvHMffZuWpKTlR1my3bTEPU0hJcsABh0QrKYUTJ5KyHCzlyPwBkpSyg1mUpB3ILYna\n"
+ "lEYKcbADm4cDD7NpM2bv75HF1/az8l3fX/1+77pO7vu6rtf1/jx0X3caSeIvSfK3wP/DPykcDsNs\n"
+ "NqOqqgpdXV3Y2NhIGVz6+4PJyUlcXFxAFEV4vV709fVBrVZDpVL9eTo/aGVlhXl5efT5fDw/P2c4\n"
+ "HKbdbmd3dzdTIZDk6+srh4eHqdPpqNfrSZJGo5H7+/sMBoNUKpUpgUsAQCqVQq1WIxgMfgp/dXU1\n"
+ "srKykJ+fD6/X+8ejHiu4wcFB2Gy2uJPq6uqwt7eXOjgAaLXahHCn05laeCKlyvmXVosnrVaLQCCA\n"
+ "4uJiyOVyCIIQGyqVCoIgoKCgIDXwtLQ0HBwc4O3tDR6PB263G263G8fHx1hbW4Pb7cbNzQ1yc3Nj\n"
+ "hxEEAdXV1WhoaEi88cfSf3h4iLXaR01MTFChUFChULCjo4M7OztxW8fn89HlcnF5eZlTU1Nsb29n\n"
+ "W1sb/X5/3PlJwd/19vbG7e1tlpWVcXV19ftGJjk0NESbzRb33aeCy8zMhM/nSxgliUSCxsZGiKKI\n"
+ "ra2tb9N1e3uLSCSC8fFxGAwGDAwMfC7c309TWFjI09PTpFwlis76+jo7OzspCAJNJhN3d3fpcDho\n"
+ "tVqpUCgYCoVIkl8Krr6+Hna7HSUlJd86+yiPx4P5+XlYLBZUVFSgv78fVqsV6enpAIC7uzt4vV5E\n"
+ "IhFIJJL4zp1OJ2traxmJRL51+fLywuXlZba2trK0tJRms5mXl5c8PT3l4uIix8bG2NbWRqVSyfLy\n"
+ "cvb09PDw8DC2Po38eocbGRlBIBDA9PQ0pNKv3Xh2doa5uTksLCwgJycHNTU1yM3NxdHREa6vr6HR\n"
+ "aKDX62NDp9MhIyPjyz5x4SQxOjoKURTR29sLmUyG7Oxs+P1+WCwW7O7uQiKRQKvVQq/Xo7KyMgaS\n"
+ "y+VJpyou/F0nJydYWlqCy+XC1dUV7u/vYTQa8fT0BIfDgaWlpaRBcZVsFc/OznJsbIwkaTKZODMz\n"
+ "k+zShEr6Arm5uYnm5mYAgCiKaGlp+ZnrZJ1Ho1EWFRXx+fmZj4+P1Gg0P3ZNJqj2dxkMhth3PBAI\n"
+ "QCaTIRqNIhQKoamp6cc5/0d4qvXv+mn4z8B/AV1UVu6zi+zUAAAAAElFTkSuQmCC\n"
+ "--foo bar--\n",
+ .bodystructure =
+ "(\"text\" \"plain\" (\"charset\" \"us-ascii\" \"format\" \"flowed\" \"delsp\" \"no\") NIL NIL \"7bit\" 881 12 NIL NIL (\"la\") NIL)(\"image\" \"png\" NIL NIL NIL \"base64\" 1390 NIL (\"attachment\" (\"filename\" \"pigeon.png\")) NIL NIL) \"mixed\" (\"boundary\" \"foo bar\") NIL NIL NIL",
+ .body =
+ "(\"text\" \"plain\" (\"charset\" \"us-ascii\" \"format\" \"flowed\" \"delsp\" \"no\") NIL NIL \"7bit\" 881 12)(\"image\" \"png\" NIL NIL NIL \"base64\" 1390) \"mixed\""
+ },{
+ .message =
+ "From: user@domain.org\n"
+ "Date: Sat, 24 Mar 2007 23:00:00 +0200\n"
+ "Mime-Version: 1.0\n"
+ "Content-Type: multipart/mixed; boundary=\"foo\n"
+ " bar\"\n"
+ "\n"
+ "Root MIME prologue\n"
+ "\n"
+ "--foo bar\n"
+ "Content-Type: text/x-myown; charset=us-ascii; foo=\"quoted\\\"string\"\n"
+ "Content-ID: <foo@example.com>\n"
+ "Content-MD5: Q2hlY2sgSW50ZWdyaXR5IQ==\n"
+ "Content-Disposition: inline; foo=bar\n"
+ "Content-Description: hellodescription\n"
+ "Content-Language: en, fi, se\n"
+ "Content-Location: http://example.com/test.txt\n"
+ "\n"
+ "hello\n"
+ "\n"
+ "--foo bar\n"
+ "Content-Type: message/rfc822\n"
+ "\n"
+ "From: sub@domain.org\n"
+ "To: sub-to1@domain.org, sub-to2@domain.org\n"
+ "Date: Sun, 12 Aug 2012 12:34:56 +0300\n"
+ "Subject: submsg\n"
+ "Content-Type: multipart/alternative; boundary=\"sub1\"\n"
+ "\n"
+ "Sub MIME prologue\n"
+ "--sub1\n"
+ "Content-Type: text/html\n"
+ "Content-Transfer-Encoding: 8bit\n"
+ "\n"
+ "<p>Hello world</p>\n"
+ "\n"
+ "--sub1\n"
+ "Content-Type: text/plain\n"
+ "Content-Transfer-Encoding: ?invalid\n"
+ "\n"
+ "Hello another world\n"
+ "\n"
+ "--sub1--\n"
+ "Sub MIME epilogue\n"
+ "\n"
+ "--foo bar--\n"
+ "Root MIME epilogue\n",
+ .bodystructure =
+ "(\"text\" \"x-myown\" (\"charset\" \"us-ascii\" \"foo\" \"quoted\\\"string\") \"<foo@example.com>\" \"hellodescription\" \"7bit\" 7 1 \"Q2hlY2sgSW50ZWdyaXR5IQ==\" (\"inline\" (\"foo\" \"bar\")) (\"en\" \"fi\" \"se\") \"http://example.com/test.txt\")(\"message\" \"rfc822\" NIL NIL NIL \"7bit\" 412 (\"Sun, 12 Aug 2012 12:34:56 +0300\" \"submsg\" ((NIL NIL \"sub\" \"domain.org\")) ((NIL NIL \"sub\" \"domain.org\")) ((NIL NIL \"sub\" \"domain.org\")) ((NIL NIL \"sub-to1\" \"domain.org\")(NIL NIL \"sub-to2\" \"domain.org\")) NIL NIL NIL NIL) ((\"text\" \"html\" (\"charset\" \"us-ascii\") NIL NIL \"8bit\" 20 1 NIL NIL NIL NIL)(\"text\" \"plain\" (\"charset\" \"us-ascii\") NIL NIL \"7bit\" 21 1 NIL NIL NIL NIL) \"alternative\" (\"boundary\" \"sub1\") NIL NIL NIL) 21 NIL NIL NIL NIL) \"mixed\" (\"boundary\" \"foo bar\") NIL NIL NIL",
+ .body =
+ "(\"text\" \"x-myown\" (\"charset\" \"us-ascii\" \"foo\" \"quoted\\\"string\") \"<foo@example.com>\" \"hellodescription\" \"7bit\" 7 1)(\"message\" \"rfc822\" NIL NIL NIL \"7bit\" 412 (\"Sun, 12 Aug 2012 12:34:56 +0300\" \"submsg\" ((NIL NIL \"sub\" \"domain.org\")) ((NIL NIL \"sub\" \"domain.org\")) ((NIL NIL \"sub\" \"domain.org\")) ((NIL NIL \"sub-to1\" \"domain.org\")(NIL NIL \"sub-to2\" \"domain.org\")) NIL NIL NIL NIL) ((\"text\" \"html\" (\"charset\" \"us-ascii\") NIL NIL \"8bit\" 20 1)(\"text\" \"plain\" (\"charset\" \"us-ascii\") NIL NIL \"7bit\" 21 1) \"alternative\") 21) \"mixed\""
+ },{
+ .message =
+ "Content-Type: multipart/mixed; boundary=\"foo\"\n"
+ "\n",
+ .bodystructure =
+ "(\"text\" \"plain\" (\"charset\" \"us-ascii\") NIL NIL \"7bit\" 0 0 NIL NIL NIL NIL) \"mixed\" (\"boundary\" \"foo\") NIL NIL NIL",
+ .body =
+ "(\"text\" \"plain\" (\"charset\" \"us-ascii\") NIL NIL \"7bit\" 0 0) \"mixed\""
+
+ }
+};
+
+static const unsigned int parse_tests_count = N_ELEMENTS(parse_tests);
+
+struct normalize_test {
+ const char *message;
+ const char *input;
+ const char *output;
+};
+
+struct normalize_test normalize_tests[] = {
+ {
+ .message =
+ "From: user@domain.org\n"
+ "Date: Sat, 24 Mar 2017 23:00:00 +0200\n"
+ "Mime-Version: 1.0\n"
+ "Content-Type: text/plain; charset=us-ascii\n"
+ "\n"
+ "body\n",
+ .input =
+ "\"text\" \"plain\" (\"charset\" \"us-ascii\") NIL NIL \"7bit\" 6 1",
+ .output =
+ "\"text\" \"plain\" (\"charset\" \"us-ascii\") NIL NIL \"7bit\" 6 1 NIL NIL NIL NIL",
+ }, {
+ .message =
+ "From: user@domain.org\n"
+ "Date: Sat, 24 Mar 2017 23:00:00 +0200\n"
+ "Mime-Version: 1.0\n"
+ "Content-Type: text/plain; charset=us-ascii\n"
+ "Content-MD5: ae6ba5b4c6eb1efd4a9fac3708046cbe\n"
+ "\n"
+ "body\n",
+ .input =
+ "\"text\" \"plain\" (\"charset\" \"us-ascii\") NIL NIL \"7bit\" 6 1 \"ae6ba5b4c6eb1efd4a9fac3708046cbe\"",
+ .output =
+ "\"text\" \"plain\" (\"charset\" \"us-ascii\") NIL NIL \"7bit\" 6 1 \"ae6ba5b4c6eb1efd4a9fac3708046cbe\" NIL NIL NIL",
+ }, {
+ .message =
+ "From: user@domain.org\n"
+ "Date: Sat, 24 Mar 2017 23:00:00 +0200\n"
+ "Mime-Version: 1.0\n"
+ "Content-Type: multipart/mixed; boundary=\"foo bar\"\n"
+ "\n"
+ "--foo bar\n"
+ "Content-Type: text/plain; charset=us-ascii\n"
+ "\n"
+ "See attached...\n"
+ "\n"
+ "--foo bar\n"
+ "Content-Type: message/rfc822\n"
+ "\n"
+ "From: user@domain.org\n"
+ "Date: Sat, 24 Mar 2017 23:00:00 +0200\n"
+ "Mime-Version: 1.0\n"
+ "Content-Type: text/plain; charset=us-ascii\n"
+ "\n"
+ "body\n"
+ "\n"
+ "--foo bar--\n"
+ "\n",
+ .input =
+ "(\"text\" \"plain\" (\"charset\" \"us-ascii\") NIL NIL \"7bit\" 17 1)(\"message\" \"rfc822\" NIL NIL NIL \"7bit\" 133 (\"Sat, 24 Mar 2017 23:00:00 +0200\" NIL ((NIL NIL \"user\" \"domain.org\")) ((NIL NIL \"user\" \"domain.org\")) ((NIL NIL \"user\" \"domain.org\")) NIL NIL NIL NIL NIL) (\"text\" \"plain\" (\"charset\" \"us-ascii\") NIL NIL \"7bit\" 6 1) 6) \"mixed\"",
+ .output =
+ "(\"text\" \"plain\" (\"charset\" \"us-ascii\") NIL NIL \"7bit\" 17 1 NIL NIL NIL NIL)(\"message\" \"rfc822\" NIL NIL NIL \"7bit\" 133 (\"Sat, 24 Mar 2017 23:00:00 +0200\" NIL ((NIL NIL \"user\" \"domain.org\")) ((NIL NIL \"user\" \"domain.org\")) ((NIL NIL \"user\" \"domain.org\")) NIL NIL NIL NIL NIL) (\"text\" \"plain\" (\"charset\" \"us-ascii\") NIL NIL \"7bit\" 6 1 NIL NIL NIL NIL) 6 NIL NIL NIL NIL) \"mixed\" NIL NIL NIL NIL"
+ }, {
+ .message =
+ "From: user@domain.org\n"
+ "Date: Sat, 24 Mar 2017 23:00:00 +0200\n"
+ "Mime-Version: 1.0\n"
+ "Content-Type: multipart/mixed; boundary=\"foo bar\"\n"
+ "\n"
+ "--foo bar\n"
+ "Content-Type: text/plain; charset=us-ascii\n"
+ "\n"
+ "See attached...\n"
+ "\n"
+ "--foo bar--\n"
+ "\n",
+ .input =
+ "(\"text\" \"plain\" (\"charset\" \"us-ascii\") NIL NIL \"7bit\" 17 1) \"mixed\" (\"boundary\" \"foo bar\")",
+ .output =
+ "(\"text\" \"plain\" (\"charset\" \"us-ascii\") NIL NIL \"7bit\" 17 1 NIL NIL NIL NIL) \"mixed\" (\"boundary\" \"foo bar\") NIL NIL NIL"
+ }, {
+ .message =
+ "From: user@domain.org\n"
+ "Date: Sat, 24 Mar 2017 23:00:00 +0200\n"
+ "Mime-Version: 1.0\n"
+ "Content-Type: multipart/mixed; boundary=\"foo bar\"\n"
+ "\n"
+ "--foo bar\n"
+ "Content-Type: text/plain; charset=us-ascii\n"
+ "Content-MD5: 6537bae18ed07779c9dc25f24635b0f3\n"
+ "\n"
+ "See attached...\n"
+ "\n"
+ "--foo bar--\n"
+ "\n",
+ .input =
+ "(\"text\" \"plain\" (\"charset\" \"us-ascii\") NIL NIL \"7bit\" 17 1 \"6537bae18ed07779c9dc25f24635b0f3\") \"mixed\" (\"boundary\" \"foo bar\")",
+ .output =
+ "(\"text\" \"plain\" (\"charset\" \"us-ascii\") NIL NIL \"7bit\" 17 1 \"6537bae18ed07779c9dc25f24635b0f3\" NIL NIL NIL) \"mixed\" (\"boundary\" \"foo bar\") NIL NIL NIL"
+ }, {
+ .message =
+ "From: user@domain.org\n"
+ "Date: Sat, 24 Mar 2017 23:00:00 +0200\n"
+ "Mime-Version: 1.0\n"
+ "Content-Type: multipart/mixed; boundary=\"foo bar\"\n"
+ "\n"
+ "--foo bar\n"
+ "Content-Type: text/plain; charset=us-ascii\n"
+ "Content-Language: en\n"
+ "\n"
+ "See attached...\n"
+ "\n"
+ "--foo bar--\n"
+ "\n",
+ .input =
+ "(\"text\" \"plain\" (\"charset\" \"us-ascii\") NIL NIL \"7bit\" 17 1 NIL NIL \"en\") \"mixed\" (\"boundary\" \"foo bar\")",
+ .output =
+ "(\"text\" \"plain\" (\"charset\" \"us-ascii\") NIL NIL \"7bit\" 17 1 NIL NIL (\"en\") NIL) \"mixed\" (\"boundary\" \"foo bar\") NIL NIL NIL"
+ }, {
+ .message =
+ "From: user@domain.org\n"
+ "Date: Sat, 24 Mar 2017 23:00:00 +0200\n"
+ "Mime-Version: 1.0\n"
+ "Content-Type: multipart/mixed; boundary=\"foo bar\"\n"
+ "\n"
+ "--foo bar\n"
+ "Content-Type: text/plain; charset=us-ascii\n"
+ "Content-Location: http://www.example.com/frop.txt\n"
+ "\n"
+ "See attached...\n"
+ "\n"
+ "--foo bar--\n"
+ "\n",
+ .input =
+ "(\"text\" \"plain\" (\"charset\" \"us-ascii\") NIL NIL \"7bit\" 17 1 NIL NIL NIL \"http://www.example.com/frop.txt\") \"mixed\" (\"boundary\" \"foo bar\")",
+ .output =
+ "(\"text\" \"plain\" (\"charset\" \"us-ascii\") NIL NIL \"7bit\" 17 1 NIL NIL NIL \"http://www.example.com/frop.txt\") \"mixed\" (\"boundary\" \"foo bar\") NIL NIL NIL"
+ }
+};
+
+static const unsigned int normalize_tests_count = N_ELEMENTS(normalize_tests);
+
+static struct message_part *
+msg_parse(pool_t pool, const char *message, unsigned int max_nested_mime_parts,
+ unsigned int max_total_mime_parts, bool parse_bodystructure)
+{
+ const struct message_parser_settings parser_set = {
+ .hdr_flags = MESSAGE_HEADER_PARSER_FLAG_SKIP_INITIAL_LWSP |
+ MESSAGE_HEADER_PARSER_FLAG_DROP_CR,
+ .flags = MESSAGE_PARSER_FLAG_SKIP_BODY_BLOCK,
+ .max_nested_mime_parts = max_nested_mime_parts,
+ .max_total_mime_parts = max_total_mime_parts,
+ };
+ struct message_parser_ctx *parser;
+ struct istream *input;
+ struct message_block block;
+ struct message_part *parts;
+ int ret;
+
+ input = i_stream_create_from_data(message, strlen(message));
+ parser = message_parser_init(pool, input, &parser_set);
+ while ((ret = message_parser_parse_next_block(parser, &block)) > 0) {
+ if (parse_bodystructure) {
+ message_part_data_parse_from_header(pool, block.part,
+ block.hdr);
+ }
+ }
+ test_assert(ret < 0);
+
+ message_parser_deinit(&parser, &parts);
+ i_stream_unref(&input);
+ return parts;
+}
+
+static void test_imap_bodystructure_write(void)
+{
+ struct message_part *parts;
+ const char *error;
+ unsigned int i;
+
+ for (i = 0; i < parse_tests_count; i++) T_BEGIN {
+ struct parse_test *test = &parse_tests[i];
+ string_t *str = t_str_new(128);
+ pool_t pool = pool_alloconly_create("imap bodystructure write", 1024);
+
+ test_begin(t_strdup_printf("imap bodystructure write [%u]", i));
+ parts = msg_parse(pool, test->message, 0, 0, TRUE);
+
+ test_assert(imap_bodystructure_write(parts, str, TRUE, &error) == 0);
+ test_assert(strcmp(str_c(str), test->bodystructure) == 0);
+
+ str_truncate(str, 0);
+ test_assert(imap_bodystructure_write(parts, str, FALSE, &error) == 0);
+ test_assert(strcmp(str_c(str), test->body) == 0);
+
+ pool_unref(&pool);
+ test_end();
+ } T_END;
+
+ T_BEGIN {
+ test_begin("imap bodystructure write - corrupted");
+ pool_t pool = pool_alloconly_create("imap bodystructure write", 1024);
+
+ parts = msg_parse(pool, "Subject: hello world", 0, 0, TRUE);
+ i_assert((parts->flags & MESSAGE_PART_FLAG_TEXT) != 0);
+ parts->flags &= ENUM_NEGATE(MESSAGE_PART_FLAG_TEXT);
+
+ string_t *str = t_str_new(128);
+ test_assert(imap_bodystructure_write(parts, str, FALSE, &error) < 0);
+ test_assert_strcmp(error, "text flag mismatch");
+ pool_unref(&pool);
+ test_end();
+ } T_END;
+}
+
+static void test_imap_bodystructure_parse(void)
+{
+ struct message_part *parts;
+ const char *error;
+ unsigned int i;
+ int ret;
+
+ for (i = 0; i < parse_tests_count; i++) T_BEGIN {
+ struct parse_test *test = &parse_tests[i];
+ string_t *str = t_str_new(128);
+ pool_t pool = pool_alloconly_create("imap bodystructure parse", 1024);
+
+ test_begin(t_strdup_printf("imap bodystructure parser [%u]", i));
+ parts = msg_parse(pool, test->message, 0, 0, FALSE);
+
+ test_assert(imap_body_parse_from_bodystructure(test->bodystructure,
+ str, &error) == 0);
+ test_assert(strcmp(str_c(str), test->body) == 0);
+
+ ret = imap_bodystructure_parse(test->bodystructure,
+ pool, parts, &error);
+ test_assert(ret == 0);
+
+ if (ret == 0) {
+ str_truncate(str, 0);
+ test_assert(imap_bodystructure_write(parts, str, TRUE, &error) == 0);
+ test_assert(strcmp(str_c(str), test->bodystructure) == 0);
+ } else {
+ i_error("Invalid BODYSTRUCTURE: %s", error);
+ }
+
+ pool_unref(&pool);
+ test_end();
+ } T_END;
+}
+
+static void test_imap_bodystructure_parse_invalid(void)
+{
+ static const struct parse_test_invalid {
+ const char *message;
+ const char *bodystructure;
+ const char *error;
+ } invalid_bodystructure_tests[] = {
+ /* Make sure NILs aren't allowed where strings are expected */
+ { "foo", "NIL \"plain\" (\"charset\" \"us-ascii\") NIL NIL \"7bit\" 0 0 NIL NIL NIL NIL", "Invalid content-type" },
+ { "foo", "\"text\" NIL (\"charset\" \"us-ascii\") NIL NIL \"7bit\" 0 0 NIL NIL NIL NIL", "Invalid content-type" },
+ { "foo", "\"text\" \"plain\" (NIL \"us-ascii\") NIL NIL \"7bit\" 0 0 NIL NIL NIL NIL", "Invalid content params" },
+ { "foo", "\"text\" \"plain\" (\"charset\" NIL) NIL NIL \"7bit\" 0 0 NIL NIL NIL NIL", "Invalid content params" },
+ { "foo", "\"text\" \"plain\" (\"charset\" \"us-ascii\") NIL NIL NIL 0 0 NIL NIL NIL NIL", "Invalid content-transfer-encoding" },
+ { "foo", "\"text\" \"plain\" (\"charset\" \"us-ascii\") NIL NIL \"7bit\" NIL 0 NIL NIL NIL NIL", "Invalid size field" },
+ { "foo", "\"text\" \"plain\" (\"charset\" \"us-ascii\") NIL NIL \"7bit\" 0 NIL NIL NIL NIL NIL", "Invalid lines field" },
+ { "foo", "\"text\" \"plain\" (\"charset\" \"us-ascii\") NIL NIL \"7bit\" 0 0 NIL (NIL (\"foo\" \"bar\")) NIL NIL", "Invalid content-disposition" },
+ { "foo", "\"text\" \"plain\" (\"charset\" \"us-ascii\") NIL NIL \"7bit\" 0 0 NIL (\"inline\" (NIL \"bar\")) NIL NIL", "Invalid content-disposition params" },
+ { "foo", "\"text\" \"plain\" (\"charset\" \"us-ascii\") NIL NIL \"7bit\" 0 0 NIL (\"inline\" (\"foo\" NIL)) NIL NIL", "Invalid content-disposition params" },
+ { "foo", "\"text\" \"plain\" (\"charset\" \"us-ascii\") NIL NIL \"7bit\" 0 0 NIL NIL (NIL \"bar\") NIL", "Invalid content-language" },
+ { "foo", "\"text\" \"plain\" (\"charset\" \"us-ascii\") NIL NIL \"7bit\" 0 0 NIL NIL (\"foo\" NIL) NIL", "Invalid content-language" },
+
+ /* Make sure atoms aren't allowed anywhere */
+ { "foo", "ATOM \"plain\" (\"charset\" \"us-ascii\") NIL NIL \"7bit\" 0 0 NIL NIL NIL NIL", "Invalid content-type" },
+ { "foo", "\"text\" ATOM (\"charset\" \"us-ascii\") NIL NIL \"7bit\" 0 0 NIL NIL NIL NIL", "Invalid content-type" },
+ { "foo", "\"text\" \"plain\" (ATOM \"us-ascii\") NIL NIL \"7bit\" 0 0 NIL NIL NIL NIL", "Invalid content params" },
+ { "foo", "\"text\" \"plain\" (\"charset\" ATOM) NIL NIL \"7bit\" 0 0 NIL NIL NIL NIL", "Invalid content params" },
+ { "foo", "\"text\" \"plain\" (\"charset\" \"us-ascii\") ATOM NIL \"7bit\" 0 0 NIL NIL NIL NIL", "Invalid content-id" },
+ { "foo", "\"text\" \"plain\" (\"charset\" \"us-ascii\") NIL ATOM \"7bit\" 0 0 NIL NIL NIL NIL", "Invalid content-description" },
+ { "foo", "\"text\" \"plain\" (\"charset\" \"us-ascii\") NIL NIL ATOM 0 0 NIL NIL NIL NIL", "Invalid content-transfer-encoding" },
+ { "foo", "\"text\" \"plain\" (\"charset\" \"us-ascii\") NIL NIL \"7bit\" ATOM 0 NIL NIL NIL NIL", "Invalid size field" },
+ { "foo", "\"text\" \"plain\" (\"charset\" \"us-ascii\") NIL NIL \"7bit\" 0 ATOM NIL NIL NIL NIL", "Invalid lines field" },
+ { "foo", "\"text\" \"plain\" (\"charset\" \"us-ascii\") NIL NIL \"7bit\" 0 0 ATOM NIL NIL NIL", "Invalid content-md5" },
+ { "foo", "\"text\" \"plain\" (\"charset\" \"us-ascii\") NIL NIL \"7bit\" 0 0 NIL ATOM NIL NIL", "Invalid content-disposition list" },
+ { "foo", "\"text\" \"plain\" (\"charset\" \"us-ascii\") NIL NIL \"7bit\" 0 0 NIL (ATOM (\"foo\" \"bar\")) NIL NIL", "Invalid content-disposition" },
+ { "foo", "\"text\" \"plain\" (\"charset\" \"us-ascii\") NIL NIL \"7bit\" 0 0 NIL (\"inline\" (ATOM \"bar\")) NIL NIL", "Invalid content-disposition params" },
+ { "foo", "\"text\" \"plain\" (\"charset\" \"us-ascii\") NIL NIL \"7bit\" 0 0 NIL (\"inline\" (\"foo\" ATOM)) NIL NIL", "Invalid content-disposition params" },
+ { "foo", "\"text\" \"plain\" (\"charset\" \"us-ascii\") NIL NIL \"7bit\" 0 0 NIL NIL ATOM NIL", "Invalid content-language" },
+ { "foo", "\"text\" \"plain\" (\"charset\" \"us-ascii\") NIL NIL \"7bit\" 0 0 NIL NIL (ATOM \"bar\") NIL", "Invalid content-language" },
+ { "foo", "\"text\" \"plain\" (\"charset\" \"us-ascii\") NIL NIL \"7bit\" 0 0 NIL NIL (\"foo\" ATOM) NIL", "Invalid content-language" },
+ { "foo", "\"text\" \"plain\" (\"charset\" \"us-ascii\") NIL NIL \"7bit\" 0 0 NIL NIL NIL ATOM", "Invalid content-location" },
+
+ /* Make sure empty lists aren't allowed anywhere */
+ { "foo", "\"text\" \"plain\" () NIL NIL \"7bit\" 0 0 NIL NIL NIL NIL", "Invalid content params" },
+ { "foo", "\"text\" \"plain\" (\"charset\" \"us-ascii\") () NIL \"7bit\" 0 0 NIL NIL NIL NIL", "Invalid content-id" },
+ { "foo", "\"text\" \"plain\" (\"charset\" \"us-ascii\") NIL () \"7bit\" 0 0 NIL NIL NIL NIL", "Invalid content-description" },
+ { "foo", "\"text\" \"plain\" (\"charset\" \"us-ascii\") NIL NIL () 0 0 NIL NIL NIL NIL", "Invalid content-transfer-encoding" },
+ { "foo", "\"text\" \"plain\" (\"charset\" \"us-ascii\") NIL NIL \"7bit\" () 0 NIL NIL NIL NIL", "Invalid size field" },
+ { "foo", "\"text\" \"plain\" (\"charset\" \"us-ascii\") NIL NIL \"7bit\" 0 () NIL NIL NIL NIL", "Invalid lines field" },
+ { "foo", "\"text\" \"plain\" (\"charset\" \"us-ascii\") NIL NIL \"7bit\" 0 0 () NIL NIL NIL", "Invalid content-md5" },
+ { "foo", "\"text\" \"plain\" (\"charset\" \"us-ascii\") NIL NIL \"7bit\" 0 0 NIL () NIL NIL", "Invalid content-disposition" },
+ { "foo", "\"text\" \"plain\" (\"charset\" \"us-ascii\") NIL NIL \"7bit\" 0 0 NIL NIL () NIL", "Invalid content-language" },
+ { "foo", "\"text\" \"plain\" (\"charset\" \"us-ascii\") NIL NIL \"7bit\" 0 0 NIL NIL NIL ()", "Invalid content-location" },
+ };
+ struct message_part *parts;
+ const char *error;
+ unsigned int i;
+
+ test_begin("imap bodystructure parser invalid");
+ for (i = 0; i < N_ELEMENTS(invalid_bodystructure_tests); i++) T_BEGIN {
+ const struct parse_test_invalid *test =
+ &invalid_bodystructure_tests[i];
+ pool_t pool = pool_alloconly_create("imap bodystructure parse", 1024);
+
+ parts = msg_parse(pool, test->message, 0, 0, FALSE);
+ test_assert_idx(imap_bodystructure_parse(test->bodystructure,
+ pool, parts, &error) == -1, i);
+ test_assert_strcmp_idx(error, test->error, i);
+ pool_unref(&pool);
+ } T_END;
+ test_end();
+}
+
+static void test_imap_bodystructure_parse_full(void)
+{
+ const char *error;
+ unsigned int i;
+ int ret;
+
+ for (i = 0; i < parse_tests_count; i++) T_BEGIN {
+ struct parse_test *test = &parse_tests[i];
+ struct message_part *parts = NULL;
+ string_t *str = t_str_new(128);
+ pool_t pool = pool_alloconly_create("imap bodystructure parse full", 1024);
+
+ test_begin(t_strdup_printf("imap bodystructure parser full [%u]", i));
+
+ ret = imap_bodystructure_parse_full(test->bodystructure,
+ pool, &parts, &error);
+ test_assert(ret == 0);
+
+ if (ret == 0) {
+ str_truncate(str, 0);
+ test_assert(imap_bodystructure_write(parts, str, TRUE, &error) == 0);
+ test_assert(strcmp(str_c(str), test->bodystructure) == 0);
+ } else {
+ i_error("Invalid BODYSTRUCTURE: %s", error);
+ }
+
+ pool_unref(&pool);
+ test_end();
+ } T_END;
+}
+
+static void test_imap_bodystructure_normalize(void)
+{
+ struct message_part *parts;
+ const char *error;
+ unsigned int i;
+ int ret;
+
+ for (i = 0; i < normalize_tests_count; i++) T_BEGIN {
+ struct normalize_test *test = &normalize_tests[i];
+ string_t *str = t_str_new(128);
+ pool_t pool = pool_alloconly_create("imap bodystructure parse", 1024);
+
+ test_begin(t_strdup_printf("imap bodystructure normalize [%u]", i));
+ parts = msg_parse(pool, test->message, 0, 0, FALSE);
+
+ ret = imap_bodystructure_parse(test->input,
+ pool, parts, &error);
+ test_assert(ret == 0);
+
+ if (ret == 0) {
+ str_truncate(str, 0);
+ test_assert(imap_bodystructure_write(parts, str, TRUE, &error) == 0);
+ test_assert(strcmp(str_c(str), test->output) == 0);
+ } else {
+ i_error("Invalid BODYSTRUCTURE: %s", error);
+ }
+
+ pool_unref(&pool);
+ test_end();
+ } T_END;
+}
+
+static const struct {
+ const char *input;
+ const char *bodystructure;
+ unsigned int max_depth;
+ unsigned int max_total;
+} truncation_tests[] = {
+ {
+ .input = "Content-Type: message/rfc822\n"
+ "\n"
+ "Content-Type: message/rfc822\n"
+ "Header2: value2\n"
+ "\n"
+ "Subject: hello world\n"
+ "Header2: value2\n"
+ "Header3: value3\n"
+ "\n"
+ "body line 1\n"
+ "body line 2\n"
+ "body line 4\n"
+ "body line 3\n",
+ .bodystructure = "\"message\" \"rfc822\" NIL NIL NIL \"7bit\" 159 (NIL NIL NIL NIL NIL NIL NIL NIL NIL NIL) (\"application\" \"octet-stream\" NIL NIL NIL \"7bit\" 110 NIL NIL NIL NIL) 11 NIL NIL NIL NIL",
+ .max_depth = 2,
+ },
+ {
+ .input = "Content-Type: multipart/mixed; boundary=1\n"
+ "\n"
+ "--1\n"
+ "Content-Type: multipart/mixed; boundary=2\n"
+ "\n"
+ "--2\n"
+ "Content-Type: multipart/mixed; boundary=3\n"
+ "\n"
+ "--3\n"
+ "\n"
+ "body\n",
+ .bodystructure = "(\"application\" \"octet-stream\" (\"boundary\" \"2\") NIL NIL \"7bit\" 63 NIL NIL NIL NIL) \"mixed\" (\"boundary\" \"1\") NIL NIL NIL",
+ .max_depth = 2,
+ },
+ {
+ .input = "Content-Type: multipart/digest; boundary=1\n"
+ "\n"
+ "--1\n"
+ "\n"
+ "Subject: hdr1\n"
+ "\n"
+ "body1\n"
+ "--1\n"
+ "\n"
+ "Subject: hdr2\n"
+ "\n"
+ "body2\n",
+ .bodystructure = "(\"application\" \"octet-stream\" NIL NIL NIL \"7bit\" 55 NIL NIL NIL NIL) \"digest\" (\"boundary\" \"1\") NIL NIL NIL",
+ .max_total = 2,
+ },
+
+};
+
+static void test_imap_bodystructure_truncation(void)
+{
+ struct message_part *parts;
+ const char *error;
+ string_t *str_body = t_str_new(128);
+ string_t *str_parts = t_str_new(128);
+ pool_t pool = pool_alloconly_create("imap bodystructure parse", 1024);
+
+ test_begin("imap bodystructure truncation");
+
+ for (unsigned int i = 0; i < N_ELEMENTS(truncation_tests); i++) {
+ p_clear(pool);
+ str_truncate(str_body, 0);
+ str_truncate(str_parts, 0);
+
+ parts = msg_parse(pool, truncation_tests[i].input,
+ truncation_tests[i].max_depth,
+ truncation_tests[i].max_total,
+ TRUE);
+
+ /* write out BODYSTRUCTURE and serialize message_parts */
+ test_assert(imap_bodystructure_write(parts, str_body, TRUE, &error) == 0);
+ message_part_serialize(parts, str_parts);
+
+ /* now deserialize message_parts and make sure they can be used
+ to parse BODYSTRUCTURE */
+ parts = message_part_deserialize(pool, str_data(str_parts),
+ str_len(str_parts), &error);
+ test_assert(parts != NULL);
+ test_assert(imap_bodystructure_parse(str_c(str_body), pool,
+ parts, &error) == 0);
+ test_assert_strcmp(str_c(str_body),
+ truncation_tests[i].bodystructure);
+ }
+ pool_unref(&pool);
+ test_end();
+}
+
+int main(void)
+{
+ static void (*const test_functions[])(void) = {
+ test_imap_bodystructure_write,
+ test_imap_bodystructure_parse,
+ test_imap_bodystructure_parse_invalid,
+ test_imap_bodystructure_normalize,
+ test_imap_bodystructure_parse_full,
+ test_imap_bodystructure_truncation,
+ NULL
+ };
+ return test_run(test_functions);
+}
diff --git a/src/lib-imap/test-imap-envelope.c b/src/lib-imap/test-imap-envelope.c
new file mode 100644
index 0000000..1f295e5
--- /dev/null
+++ b/src/lib-imap/test-imap-envelope.c
@@ -0,0 +1,205 @@
+/* Copyright (c) 2013-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "istream.h"
+#include "str.h"
+#include "message-part-data.h"
+#include "message-parser.h"
+#include "imap-envelope.h"
+#include "test-common.h"
+
+struct parse_test {
+ const char *message;
+ const char *envelope;
+};
+
+struct parse_test parse_tests[] = {
+ /* Tests copied from imaptest */
+ {
+ .message =
+ "Message-ID: <msg@id>\n"
+ "In-Reply-To: <reply@to.id>\n"
+ "Date: Thu, 15 Feb 2007 01:02:03 +0200\n"
+ "Subject: subject header\n"
+ "From: From Real <fromuser@fromdomain.org>\n"
+ "To: To Real <touser@todomain.org>\n"
+ "Cc: Cc Real <ccuser@ccdomain.org>\n"
+ "Bcc: Bcc Real <bccuser@bccdomain.org>\n"
+ "Sender: Sender Real <senderuser@senderdomain.org>\n"
+ "Reply-To: ReplyTo Real <replytouser@replytodomain.org>\n"
+ "\n"
+ "body\n",
+ .envelope =
+ "\"Thu, 15 Feb 2007 01:02:03 +0200\" "
+ "\"subject header\" "
+ "((\"From Real\" NIL \"fromuser\" \"fromdomain.org\")) "
+ "((\"Sender Real\" NIL \"senderuser\" \"senderdomain.org\")) "
+ "((\"ReplyTo Real\" NIL \"replytouser\" \"replytodomain.org\")) "
+ "((\"To Real\" NIL \"touser\" \"todomain.org\")) "
+ "((\"Cc Real\" NIL \"ccuser\" \"ccdomain.org\")) "
+ "((\"Bcc Real\" NIL \"bccuser\" \"bccdomain.org\")) "
+ "\"<reply@to.id>\" \"<msg@id>\""
+ }, {
+ .message =
+ "Date: Thu, 15 Feb 2007 01:02:03 +0200\n"
+ "From: user@domain\n"
+ "\n"
+ "body\n",
+ .envelope =
+ "\"Thu, 15 Feb 2007 01:02:03 +0200\" NIL "
+ "((NIL NIL \"user\" \"domain\")) "
+ "((NIL NIL \"user\" \"domain\")) "
+ "((NIL NIL \"user\" \"domain\")) NIL NIL NIL NIL NIL"
+ }, {
+ .message =
+ "Date: Thu, 15 Feb 2007 01:02:03 +0200\n"
+ "From: user@domain\n"
+ "\n"
+ "body\n",
+ .envelope =
+ "\"Thu, 15 Feb 2007 01:02:03 +0200\" NIL "
+ "((NIL NIL \"user\" \"domain\")) "
+ "((NIL NIL \"user\" \"domain\")) "
+ "((NIL NIL \"user\" \"domain\")) NIL NIL NIL NIL NIL"
+ }, {
+ .message =
+ "Date: Thu, 15 Feb 2007 01:02:03 +0200\n"
+ "From: user@domain (Real Name)\n"
+ "To: group: g1@d1.org, g2@d2.org;, group2: g3@d3.org;\n"
+ "Cc: group:;, group2: (foo) ;\n"
+ "\n"
+ "body\n",
+ .envelope =
+ "\"Thu, 15 Feb 2007 01:02:03 +0200\" NIL "
+ "((\"Real Name\" NIL \"user\" \"domain\")) "
+ "((\"Real Name\" NIL \"user\" \"domain\")) "
+ "((\"Real Name\" NIL \"user\" \"domain\")) "
+ "((NIL NIL \"group\" NIL)"
+ "(NIL NIL \"g1\" \"d1.org\")"
+ "(NIL NIL \"g2\" \"d2.org\")"
+ "(NIL NIL NIL NIL)"
+ "(NIL NIL \"group2\" NIL)"
+ "(NIL NIL \"g3\" \"d3.org\")"
+ "(NIL NIL NIL NIL)) "
+ "((NIL NIL \"group\" NIL)(NIL NIL NIL NIL)"
+ "(NIL NIL \"group2\" NIL)(NIL NIL NIL NIL)) "
+ "NIL NIL NIL"
+ }, {
+ .message =
+ "Date: Thu, 15 Feb 2007 01:02:03 +0200\n"
+ "From: user@domain (Real Name)\n"
+ "Sender: \n"
+ "Reply-To: \n"
+ "\n"
+ "body\n",
+ .envelope =
+ "\"Thu, 15 Feb 2007 01:02:03 +0200\" NIL "
+ "((\"Real Name\" NIL \"user\" \"domain\")) "
+ "((\"Real Name\" NIL \"user\" \"domain\")) "
+ "((\"Real Name\" NIL \"user\" \"domain\")) "
+ "NIL NIL NIL NIL NIL"
+ }, {
+ .message =
+ "Date: Thu, 15 Feb 2007 01:02:03 +0200\n"
+ "From: <@route:user@domain>\n"
+ "\n"
+ "body\n",
+ .envelope =
+ "\"Thu, 15 Feb 2007 01:02:03 +0200\" NIL "
+ "((NIL \"@route\" \"user\" \"domain\")) "
+ "((NIL \"@route\" \"user\" \"domain\")) "
+ "((NIL \"@route\" \"user\" \"domain\")) "
+ "NIL NIL NIL NIL NIL"
+ }
+};
+
+static const unsigned int parse_tests_count = N_ELEMENTS(parse_tests);
+
+static struct message_part_envelope *
+msg_parse(pool_t pool, const char *message)
+{
+ const struct message_parser_settings parser_set = {
+ .hdr_flags = MESSAGE_HEADER_PARSER_FLAG_SKIP_INITIAL_LWSP |
+ MESSAGE_HEADER_PARSER_FLAG_DROP_CR,
+ .flags = MESSAGE_PARSER_FLAG_SKIP_BODY_BLOCK,
+ };
+ struct message_parser_ctx *parser;
+ struct message_part_envelope *envlp = NULL;
+ struct istream *input;
+ struct message_block block;
+ struct message_part *parts;
+ int ret;
+
+ input = i_stream_create_from_data(message, strlen(message));
+ parser = message_parser_init(pool, input, &parser_set);
+ while ((ret = message_parser_parse_next_block(parser, &block)) > 0) {
+ i_assert(block.part->parent == NULL);
+ message_part_envelope_parse_from_header(pool, &envlp, block.hdr);
+ }
+ test_assert(ret < 0);
+
+ message_parser_deinit(&parser, &parts);
+ i_stream_unref(&input);
+ return envlp;
+}
+
+static void test_imap_envelope_write(void)
+{
+ struct message_part_envelope *envlp;
+ unsigned int i;
+
+ for (i = 0; i < parse_tests_count; i++) T_BEGIN {
+ struct parse_test *test = &parse_tests[i];
+ string_t *str = t_str_new(128);
+ pool_t pool = pool_alloconly_create("imap envelope write", 1024);
+
+ test_begin(t_strdup_printf("imap envelope write [%u]", i));
+ envlp = msg_parse(pool, test->message);
+
+ imap_envelope_write(envlp, str);
+ test_assert(strcmp(str_c(str), test->envelope) == 0);
+
+ pool_unref(&pool);
+ test_end();
+ } T_END;
+}
+
+static void test_imap_envelope_parse(void)
+{
+ struct message_part_envelope *envlp;
+ const char *error;
+ unsigned int i;
+ bool ret;
+
+ for (i = 0; i < parse_tests_count; i++) T_BEGIN {
+ struct parse_test *test = &parse_tests[i];
+ string_t *str = t_str_new(128);
+ pool_t pool = pool_alloconly_create("imap envelope parse", 1024);
+
+ test_begin(t_strdup_printf("imap envelope parser [%u]", i));
+
+ ret = imap_envelope_parse(test->envelope, pool, &envlp, &error);
+ test_assert(ret);
+
+ if (ret) {
+ str_truncate(str, 0);
+ imap_envelope_write(envlp, str);
+ test_assert(strcmp(str_c(str), test->envelope) == 0);
+ } else {
+ i_error("Invalid envelope: %s", error);
+ }
+
+ pool_unref(&pool);
+ test_end();
+ } T_END;
+}
+
+int main(void)
+{
+ static void (*const test_functions[])(void) = {
+ test_imap_envelope_write,
+ test_imap_envelope_parse,
+ NULL
+ };
+ return test_run(test_functions);
+}
diff --git a/src/lib-imap/test-imap-match.c b/src/lib-imap/test-imap-match.c
new file mode 100644
index 0000000..df911da
--- /dev/null
+++ b/src/lib-imap/test-imap-match.c
@@ -0,0 +1,127 @@
+/* Copyright (c) 2008-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "imap-match.h"
+#include "test-common.h"
+
+struct test_imap_match {
+ const char *pattern;
+ const char *input;
+ enum imap_match_result result;
+};
+
+static void test_imap_match(void)
+{
+ struct test_imap_match test[] = {
+ { "", "", IMAP_MATCH_YES },
+ { "a", "b", IMAP_MATCH_NO },
+ { "foo", "foo", IMAP_MATCH_YES },
+ { "foo", "foo/", IMAP_MATCH_PARENT },
+ { "%", "", IMAP_MATCH_YES },
+ { "%", "foo", IMAP_MATCH_YES },
+ { "%", "foo/", IMAP_MATCH_PARENT },
+ { "%/", "foo/", IMAP_MATCH_YES },
+ { "%", "foo/bar", IMAP_MATCH_PARENT },
+ { "%/%", "foo", IMAP_MATCH_CHILDREN },
+ { "%/%", "foo/", IMAP_MATCH_YES },
+ { "foo/bar/%", "foo", IMAP_MATCH_CHILDREN },
+ { "foo/bar/%", "foo/", IMAP_MATCH_CHILDREN },
+ { "foo*", "foo", IMAP_MATCH_YES },
+ { "foo*", "foo/", IMAP_MATCH_YES },
+ { "foo*", "fobo", IMAP_MATCH_NO },
+ { "*foo*", "bar/foo/", IMAP_MATCH_YES },
+ { "*foo*", "fobo", IMAP_MATCH_CHILDREN },
+ { "foo*bar", "foobar/baz", IMAP_MATCH_CHILDREN | IMAP_MATCH_PARENT },
+ { "*foo*", "fobo", IMAP_MATCH_CHILDREN },
+ { "%/%/%", "foo/", IMAP_MATCH_CHILDREN },
+ { "%/%o/%", "foo/", IMAP_MATCH_CHILDREN },
+ { "%/%o/%", "foo", IMAP_MATCH_CHILDREN },
+ { "inbox", "inbox", IMAP_MATCH_YES },
+ { "inbox", "INBOX", IMAP_MATCH_NO }
+ };
+ struct test_imap_match inbox_test[] = {
+ { "inbox", "inbox", IMAP_MATCH_YES },
+ { "inbox", "iNbOx", IMAP_MATCH_YES },
+ { "i%X", "iNbOx", IMAP_MATCH_YES },
+ { "%I%N%B%O%X%", "inbox", IMAP_MATCH_YES },
+ { "i%X/foo", "iNbOx/foo", IMAP_MATCH_YES },
+ { "%I%N%B%O%X%/foo", "inbox/foo", IMAP_MATCH_YES },
+ { "i%X/foo", "inbx/foo", IMAP_MATCH_NO }
+ };
+ struct imap_match_glob *glob, *glob2;
+ unsigned int i;
+ pool_t pool;
+
+ pool = pool_alloconly_create("imap match", 1024);
+
+ /* first try tests without inboxcasing */
+ test_begin("imap match");
+ for (i = 0; i < N_ELEMENTS(test); i++) {
+ glob = imap_match_init(pool, test[i].pattern,
+ FALSE, '/');
+ test_assert(imap_match(glob, test[i].input) == test[i].result);
+
+ glob2 = imap_match_dup(default_pool, glob);
+ test_assert(imap_match_globs_equal(glob, glob2));
+ p_clear(pool);
+
+ /* test the dup after clearing first one's memory */
+ test_assert(imap_match(glob2, test[i].input) == test[i].result);
+ imap_match_deinit(&glob2);
+ }
+
+ /* inboxcasing tests */
+ for (i = 0; i < N_ELEMENTS(inbox_test); i++) {
+ glob = imap_match_init(pool, inbox_test[i].pattern,
+ TRUE, '/');
+ test_assert(imap_match(glob, inbox_test[i].input) == inbox_test[i].result);
+
+ glob2 = imap_match_dup(default_pool, glob);
+ test_assert(imap_match_globs_equal(glob, glob2));
+ p_clear(pool);
+
+ /* test the dup after clearing first one's memory */
+ test_assert(imap_match(glob2, inbox_test[i].input) == inbox_test[i].result);
+ imap_match_deinit(&glob2);
+ }
+ pool_unref(&pool);
+ test_end();
+}
+
+static void test_imap_match_globs_equal(void)
+{
+ struct imap_match_glob *glob;
+ pool_t pool;
+
+ pool = pool_alloconly_create("imap match globs equal", 1024);
+ test_begin("imap match globs equal");
+
+ glob = imap_match_init(pool, "1", FALSE, '/');
+ test_assert(imap_match_globs_equal(glob,
+ imap_match_init(pool, "1", FALSE, '/')));
+ test_assert(imap_match_globs_equal(glob,
+ imap_match_init(pool, "1", TRUE, '/')));
+ test_assert(!imap_match_globs_equal(glob,
+ imap_match_init(pool, "1", FALSE, '.')));
+ test_assert(!imap_match_globs_equal(glob,
+ imap_match_init(pool, "11", FALSE, '/')));
+
+ glob = imap_match_init(pool, "in%", TRUE, '/');
+ test_assert(!imap_match_globs_equal(glob,
+ imap_match_init(pool, "in%", FALSE, '/')));
+ test_assert(!imap_match_globs_equal(glob,
+ imap_match_init(pool, "In%", TRUE, '/')));
+
+ pool_unref(&pool);
+ test_end();
+}
+
+int main(void)
+{
+ static void (*const test_functions[])(void) = {
+ test_imap_match,
+ test_imap_match_globs_equal,
+ NULL
+ };
+ return test_run(test_functions);
+}
diff --git a/src/lib-imap/test-imap-parser.c b/src/lib-imap/test-imap-parser.c
new file mode 100644
index 0000000..3ca4e34
--- /dev/null
+++ b/src/lib-imap/test-imap-parser.c
@@ -0,0 +1,157 @@
+/* Copyright (c) 2009-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "istream.h"
+#include "imap-parser.h"
+#include "test-common.h"
+
+static void test_imap_parser_crlf(void)
+{
+ static const char *test_input = "foo\r\nx\ry\n";
+ struct istream *input;
+ struct imap_parser *parser;
+ const struct imap_arg *args;
+ unsigned int i;
+ enum imap_parser_error parse_error;
+
+ test_begin("imap parser crlf handling");
+ input = test_istream_create(test_input);
+ parser = imap_parser_create(input, NULL, 1024);
+
+ /* must return -2 until LF is read */
+ for (i = 0; test_input[i] != '\n'; i++) {
+ test_istream_set_size(input, i+1);
+ (void)i_stream_read(input);
+ test_assert(imap_parser_read_args(parser, 0, 0, &args) == -2);
+ }
+ test_istream_set_size(input, i+1);
+ (void)i_stream_read(input);
+ test_assert(imap_parser_read_args(parser, 0, 0, &args) == 1);
+ test_assert(args[0].type == IMAP_ARG_ATOM);
+ test_assert(args[1].type == IMAP_ARG_EOL);
+
+ /* CR without LF should fail with error */
+ imap_parser_reset(parser);
+ i_stream_seek(input, ++i);
+ test_istream_set_size(input, ++i);
+ (void)i_stream_read(input);
+ test_assert(imap_parser_read_args(parser, 0, 0, &args) == -2);
+ test_istream_set_size(input, ++i);
+ (void)i_stream_read(input);
+ test_assert(imap_parser_read_args(parser, 0, 0, &args) == -2);
+ test_istream_set_size(input, ++i);
+ (void)i_stream_read(input);
+ test_assert(imap_parser_read_args(parser, 0, 0, &args) == -1);
+ test_assert(strcmp(imap_parser_get_error
+ (parser, &parse_error), "CR sent without LF") == 0 &&
+ parse_error == IMAP_PARSE_ERROR_BAD_SYNTAX);
+
+ imap_parser_unref(&parser);
+ i_stream_destroy(&input);
+ test_end();
+}
+
+static void test_imap_parser_partial_list(void)
+{
+ static const char *test_input = "((((foo {1000000}\r\n";
+ struct istream *input;
+ struct imap_parser *parser;
+ const struct imap_arg *args, *sub_list;
+
+ test_begin("imap parser partial list");
+ input = test_istream_create(test_input);
+ parser = imap_parser_create(input, NULL, 1024);
+
+ (void)i_stream_read(input);
+ test_assert(imap_parser_read_args(parser, 0,
+ IMAP_PARSE_FLAG_LITERAL_SIZE, &args) == 1);
+ for (unsigned int i = 0; i < 4; i++) {
+ sub_list = imap_arg_as_list(&args[0]);
+ test_assert(IMAP_ARG_IS_EOL(&args[1]));
+ args = sub_list;
+ }
+ test_assert(imap_arg_atom_equals(&args[0], "foo"));
+ test_assert(args[1].type == IMAP_ARG_LITERAL_SIZE);
+ test_assert(IMAP_ARG_IS_EOL(&args[2]));
+
+ imap_parser_unref(&parser);
+ i_stream_destroy(&input);
+ test_end();
+}
+
+static void test_imap_parser_read_tag_cmd(void)
+{
+ enum read_type {
+ BOTH,
+ TAG,
+ COMMAND
+ };
+ struct {
+ const char *input;
+ const char *tag;
+ int ret;
+ enum read_type type;
+ } tests[] = {
+ { "tag foo", "tag", 1, BOTH },
+ { "tag\r", "tag", 1, BOTH },
+ { "tag\rfoo", "tag", 1, BOTH },
+ { "tag\nfoo", "tag", 1, BOTH },
+ { "tag\r\nfoo", "tag", 1, BOTH },
+ { "\n", NULL, -1, BOTH },
+ { "tag", NULL, 0, BOTH },
+ { "tag\t", NULL, -1, BOTH },
+ { "tag\001", NULL, -1, BOTH },
+ { "tag\x80", NULL, -1, BOTH },
+ { "tag(", NULL, -1, BOTH },
+ { "tag)", NULL, -1, BOTH },
+ { "tag{", NULL, -1, BOTH },
+ { "tag/ ", "tag/", 1, BOTH },
+ { "tag%", NULL, -1, BOTH },
+ { "tag*", NULL, -1, BOTH },
+ { "tag\"", NULL, -1, BOTH },
+ { "tag\\", NULL, -1, BOTH },
+ { "tag+", NULL, -1, TAG },
+ { "tag+ ", "tag+", 1, COMMAND },
+ };
+ struct istream *input;
+ struct imap_parser *parser;
+ const char *atom;
+ int ret;
+
+ test_begin("imap_parser_read_tag and imap_parser_read_command_name");
+ for (unsigned int i = 0; i < N_ELEMENTS(tests); i++) {
+ if (tests[i].type != COMMAND) {
+ input = test_istream_create(tests[i].input);
+ test_assert(i_stream_read(input) > 0);
+ parser = imap_parser_create(input, NULL, 1024);
+ ret = imap_parser_read_tag(parser, &atom);
+ test_assert_idx(ret == tests[i].ret, i);
+ test_assert_idx(ret <= 0 || strcmp(tests[i].tag, atom) == 0, i);
+ imap_parser_unref(&parser);
+ i_stream_destroy(&input);
+ }
+
+ if (tests[i].type != TAG) {
+ input = test_istream_create(tests[i].input);
+ test_assert(i_stream_read(input) > 0);
+ parser = imap_parser_create(input, NULL, 1024);
+ ret = imap_parser_read_command_name(parser, &atom);
+ test_assert_idx(ret == tests[i].ret, i);
+ test_assert_idx(ret <= 0 || strcmp(tests[i].tag, atom) == 0, i);
+ imap_parser_unref(&parser);
+ i_stream_destroy(&input);
+ }
+ }
+ test_end();
+}
+
+int main(void)
+{
+ static void (*const test_functions[])(void) = {
+ test_imap_parser_crlf,
+ test_imap_parser_partial_list,
+ test_imap_parser_read_tag_cmd,
+ NULL
+ };
+ return test_run(test_functions);
+}
diff --git a/src/lib-imap/test-imap-quote.c b/src/lib-imap/test-imap-quote.c
new file mode 100644
index 0000000..77b46b3
--- /dev/null
+++ b/src/lib-imap/test-imap-quote.c
@@ -0,0 +1,171 @@
+/* Copyright (c) 2013-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "str.h"
+#include "imap-quote.h"
+#include "test-common.h"
+
+static void test_imap_append_string_for_humans(void)
+{
+ static const struct {
+ const char *input, *output;
+ } tests[] = {
+ { "", "\"\"" },
+ { " ", "\"\"" },
+ { " ", "\"\"" },
+ { "\t", "\"\"" },
+ { " \t", "\"\"" },
+ { " \t ", "\"\"" },
+ { " foo", "{3}\r\nfoo" },
+ { "\tfoo", "{3}\r\nfoo" },
+ { "\t \tfoo", "{3}\r\nfoo" },
+ { " foo ", "{3}\r\nfoo" },
+ { " foo ", "{3}\r\nfoo" },
+ { " foo \t \t", "{3}\r\nfoo" },
+ { "hello\"world", "{11}\r\nhello\"world" },
+ { "hello\\world", "{11}\r\nhello\\world" },
+ { "hello\rworld", "{11}\r\nhello world" },
+ { "hello\nworld", "{11}\r\nhello world" },
+ { "hello\r\nworld", "{11}\r\nhello world" },
+ { "hello\r\n world", "{11}\r\nhello world" },
+ { "hello \r\n world", "{11}\r\nhello world" },
+ };
+ string_t *str = t_str_new(128);
+ unsigned int i;
+
+ test_begin("imap_append_string_for_humans()");
+
+ for (i = 0; i < N_ELEMENTS(tests); i++) {
+ str_truncate(str, 0);
+ imap_append_string_for_humans(str, (const void *)tests[i].input,
+ strlen(tests[i].input));
+ test_assert_idx(strcmp(tests[i].output, str_c(str)) == 0, i);
+ }
+ test_end();
+}
+
+static void test_imap_append_astring(void)
+{
+ static const struct {
+ const char *input, *output;
+ } tests[] = {
+ { "", "\"\"" },
+ { "NIL", "\"NIL\"" },
+ { "niL", "\"niL\"" },
+ { "ni", "ni" },
+ { "\\", "\"\\\\\"" },
+ { "\\\\", "\"\\\\\\\\\"" },
+ { "\\\\\\", "\"\\\\\\\\\\\\\"" },
+ { "\\\\\\\\", "\"\\\\\\\\\\\\\\\\\"" },
+ { "\\\\\\\\\\", "{5}\r\n\\\\\\\\\\" },
+ { "\\\\\\\\\\\\", "{6}\r\n\\\\\\\\\\\\" },
+ { "\"", "\"\\\"\"" },
+ { "\"\"", "\"\\\"\\\"\"" },
+ { "\"\"\"", "\"\\\"\\\"\\\"\"" },
+ { "\"\"\"\"", "\"\\\"\\\"\\\"\\\"\"" },
+ { "\"\"\"\"\"", "{5}\r\n\"\"\"\"\"" },
+ { "\"\"\"\"\"\"", "{6}\r\n\"\"\"\"\"\"" },
+ { "\r", "{1}\r\n\r" },
+ { "\n", "{1}\r\n\n" },
+ { "\r\n", "{2}\r\n\r\n" },
+ { "\x7f", "\"\x7f\"" },
+ { "\x80", "{1}\r\n\x80" },
+ { "\xff", "{1}\r\n\xff" },
+ };
+ string_t *str = t_str_new(128);
+ unsigned int i;
+
+ test_begin("test_imap_append_astring()");
+
+ for (i = 0; i < N_ELEMENTS(tests); i++) {
+ str_truncate(str, 0);
+ imap_append_astring(str, tests[i].input);
+ test_assert_idx(strcmp(tests[i].output, str_c(str)) == 0, i);
+ }
+ test_end();
+}
+
+static void test_imap_append_nstring(void)
+{
+ static const struct {
+ const char *input, *output;
+ } tests[] = {
+ { "", "\"\"" },
+ { NULL, "NIL" },
+ { "NIL", "\"NIL\"" },
+ { "\"America N.\"", "\"\\\"America N.\\\"\"" },
+ { "\"America N.\", \"America S.\"", "\"\\\"America N.\\\", \\\"America S.\\\"\"" },
+ { "\"America N.\", \"America S.\", \"Africa\"", "{36}\r\n\"America N.\", \"America S.\", \"Africa\"" },
+ { "Antarctica\n Australia", "{21}\r\nAntarctica\n Australia" },
+ { "ni", "\"ni\"" }
+ };
+ string_t *str = t_str_new(128);
+ unsigned int i;
+
+ test_begin("test_imap_append_nstring()");
+
+ for (i = 0; i < N_ELEMENTS(tests); i++) {
+ str_truncate(str, 0);
+ imap_append_nstring(str, tests[i].input);
+ test_assert_idx(strcmp(tests[i].output, str_c(str)) == 0, i);
+ }
+ test_end();
+}
+
+static void test_imap_append_nstring_nolf(void)
+{
+ static const struct {
+ const char *input, *output;
+ } tests[] = {
+ { "", "\"\"" },
+ { NULL, "NIL" },
+ { "NIL", "\"NIL\"" },
+ { "ni", "\"ni\"" },
+ { "\"NIL\n foo", "\"\\\"NIL foo\"" },
+ { "\"America N.\", \"America S.\", \"Africa\"", "{36}\r\n\"America N.\", \"America S.\", \"Africa\"" },
+ { "foo\nbar", "\"foo bar\"" },
+ { "foo\r\nbar", "\"foo bar\"" },
+ { "foo\rbar", "\"foo bar\"" },
+ { "foo\n bar", "\"foo bar\"" },
+ { "foo\r\n bar", "\"foo bar\"" },
+ { "foo\r bar", "\"foo bar\"" },
+ { "foo\n\tbar", "\"foo\tbar\"" },
+ { "foo\r\n\tbar", "\"foo\tbar\"" },
+ { "foo\r\tbar", "\"foo\tbar\"" },
+ { "foo\n bar", "\"foo bar\"" },
+ { "foo\r\n bar", "\"foo bar\"" },
+ { "foo\r bar", "\"foo bar\"" },
+ { "\nfoo\r bar\r\n", "\" foo bar\"" }
+ };
+ unsigned int i;
+
+ test_begin("test_imap_append_nstring_nolf()");
+
+ for (i = 0; i < N_ELEMENTS(tests); i++) T_BEGIN {
+ string_t *str = t_str_new(1);
+ string_t *str2 = str_new(default_pool, 1);
+
+ str_truncate(str, 0);
+ imap_append_nstring_nolf(str, tests[i].input);
+ test_assert_idx(strcmp(tests[i].output, str_c(str)) == 0, i);
+
+ str_truncate(str2, 0);
+ imap_append_nstring_nolf(str2, tests[i].input);
+ test_assert_idx(strcmp(tests[i].output, str_c(str2)) == 0, i);
+
+ str_free(&str2);
+ } T_END;
+ test_end();
+}
+
+int main(void)
+{
+ static void (*const test_functions[])(void) = {
+ test_imap_append_string_for_humans,
+ test_imap_append_astring,
+ test_imap_append_nstring,
+ test_imap_append_nstring_nolf,
+ NULL
+ };
+ return test_run(test_functions);
+}
diff --git a/src/lib-imap/test-imap-url.c b/src/lib-imap/test-imap-url.c
new file mode 100644
index 0000000..a63ca33
--- /dev/null
+++ b/src/lib-imap/test-imap-url.c
@@ -0,0 +1,1029 @@
+/* Copyright (c) 2013-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "net.h"
+#include "imap-url.h"
+#include "test-common.h"
+
+struct valid_imap_url_test {
+ const char *url;
+ enum imap_url_parse_flags flags;
+ struct imap_url url_base;
+
+ struct imap_url url_parsed;
+};
+
+/* Valid IMAP URL tests */
+static const struct valid_imap_url_test valid_url_tests[] = {
+ {
+ .url = "imap://localhost",
+ .url_parsed = {
+ .host = { .name = "localhost" } }
+ },{
+ .url = "imap://user@localhost",
+ .url_parsed = {
+ .host = { .name = "localhost" },
+ .userid = "user" }
+ },{
+ .url = "imap://user;AUTH=PLAIN@localhost",
+ .url_parsed = {
+ .host = { .name = "localhost" },
+ .userid = "user",
+ .auth_type = "PLAIN" }
+ },{
+ .url = "imap://;AUTH=PLAIN@localhost",
+ .url_parsed = {
+ .host = { .name = "localhost" },
+ .auth_type = "PLAIN" }
+ },{
+ .url = "imap://%68endri%6B;AUTH=GSS%41PI@%65%78%61%6d%70%6c%65.com",
+ .url_parsed = {
+ .host = { .name = "example.com" },
+ .userid = "hendrik",
+ .auth_type = "GSSAPI" }
+ },{
+ .url = "imap://user@localhost:993",
+ .url_parsed = {
+ .host = { .name = "localhost" },
+ .userid = "user",
+ .port = 993 }
+ },{
+ .url = "imap://user@127.0.0.1",
+ .url_parsed = {
+ .host = {
+ .name = "127.0.0.1",
+ .ip = { .family = AF_INET } },
+ .userid = "user" }
+ },{
+ .url = "imap://user@[::1]",
+ .url_parsed = {
+ .host = {
+ .name = "[::1]",
+ .ip = { .family = AF_INET6 } },
+ .userid = "user" }
+ },{
+ .url = "imap://user@4example.com:423",
+ .url_parsed = {
+ .host = { .name = "4example.com" },
+ .userid = "user",
+ .port = 423 }
+ },{
+ .url = "imap://beelzebub@666.4example.com:999",
+ .url_parsed = {
+ .host = { .name = "666.4example.com" },
+ .userid = "beelzebub",
+ .port = 999 }
+ },{
+ .url = "imap://user@example.com/",
+ .url_parsed = {
+ .host = { .name = "example.com" },
+ .userid = "user",
+ .mailbox = NULL }
+ },{
+ .url = "imap://user@example.com/./",
+ .url_parsed = {
+ .host = { .name = "example.com" },
+ .userid = "user",
+ .mailbox = NULL }
+ },{
+ .url = "imap://user@example.com/INBOX",
+ .url_parsed = {
+ .host = { .name = "example.com" },
+ .userid = "user",
+ .mailbox = "INBOX" }
+ },{
+ .url = "imap://user@example.com/INBOX/",
+ .url_parsed = {
+ .host = { .name = "example.com" },
+ .userid = "user",
+ .mailbox = "INBOX" }
+ },{
+ .url = "imap://user@example.com//",
+ .url_parsed = {
+ .host = { .name = "example.com" },
+ .userid = "user"}
+ },{
+ .url = "imap://user@example.com/INBOX/Trash",
+ .url_parsed = {
+ .host = { .name = "example.com" },
+ .userid = "user",
+ .mailbox = "INBOX/Trash" }
+ },{
+ .url = "imap://user@example.com/INBOX/Trash/..",
+ .url_parsed = {
+ .host = { .name = "example.com" },
+ .userid = "user",
+ .mailbox = "INBOX" }
+ },{
+ .url = "imap://user@example.com/INBOX/Trash/../",
+ .url_parsed = {
+ .host = { .name = "example.com" },
+ .userid = "user",
+ .mailbox = "INBOX" }
+ },{
+ .url = "imap://user@example.com/INBOX/Trash/../..",
+ .url_parsed = {
+ .host = { .name = "example.com" },
+ .userid = "user",
+ .mailbox = NULL }
+ },{
+ .url = "imap://user@example.com/INBOX.Trash",
+ .url_parsed = {
+ .host = { .name = "example.com" },
+ .userid = "user",
+ .mailbox = "INBOX.Trash" }
+ },{
+ .url = "imap://user@example.com/INBOX%3BTrash",
+ .url_parsed = {
+ .host = { .name = "example.com" },
+ .userid = "user",
+ .mailbox = "INBOX;Trash" }
+ },{
+ .url = "imap://user@example.com/INBOX;UIDVALIDITY=1341",
+ .url_parsed = {
+ .host = { .name = "example.com" },
+ .userid = "user",
+ .mailbox = "INBOX", .uidvalidity = 1341 }
+ },{
+ .url = "imap://user@example.com/INBOX/;UIDVALIDITY=23423",
+ .url_parsed = {
+ .host = { .name = "example.com" },
+ .userid = "user",
+ .mailbox = "INBOX", .uidvalidity = 23423 }
+ },{
+ .url = "imap://user@example.com/INBOX/Drafts;UIDVALIDITY=6567",
+ .url_parsed = {
+ .host = { .name = "example.com" },
+ .userid = "user",
+ .mailbox = "INBOX/Drafts", .uidvalidity = 6567 }
+ },{
+ .url = "imap://user@example.com/INBOX/Drafts;UIDVALIDITY=788/;UID=16",
+ .url_parsed = {
+ .host = { .name = "example.com" },
+ .userid = "user",
+ .mailbox = "INBOX/Drafts", .uidvalidity = 788,
+ .uid = 16 }
+ },{
+ .url = "imap://user@example.com/INBOX/Drafts;UIDVALIDITY=788/;UID=16/..",
+ .url_parsed = {
+ .host = { .name = "example.com" },
+ .userid = "user",
+ .mailbox = "INBOX/Drafts", .uidvalidity = 788,
+ .uid = 0 }
+ },{
+ .url = "imap://user@example.com/INBOX/Drafts;UIDVALIDITY=788/;UID=16/../..",
+ .url_parsed = {
+ .host = { .name = "example.com" },
+ .userid = "user",
+ .mailbox = "INBOX", .uidvalidity = 0,
+ .uid = 0 }
+ },{
+ .url = "imap://user@example.com/INBOX/Junk;UIDVALIDITY=27667/"
+ ";UID=434/;SECTION=HEADER",
+ .url_parsed = {
+ .host = { .name = "example.com" },
+ .userid = "user",
+ .mailbox = "INBOX/Junk", .uidvalidity = 27667,
+ .uid = 434, .section = "HEADER" }
+ },{
+ .url = "imap://user@example.com/INBOX/Important/"
+ ";UID=437/;SECTION=1.2.MIME",
+ .url_parsed = {
+ .host = { .name = "example.com" },
+ .userid = "user",
+ .mailbox = "INBOX/Important",
+ .uid = 437, .section = "1.2.MIME" }
+ },{
+ .url = "imap://user@example.com/INBOX/Important/;UID=56/;SECTION=AA/BB",
+ .url_parsed = {
+ .host = { .name = "example.com" },
+ .userid = "user",
+ .mailbox = "INBOX/Important",
+ .uid = 56, .section = "AA/BB" }
+ },{
+ .url = "imap://user@example.com/INBOX/Important/;UID=56/;SECTION=AA/BB/..",
+ .url_parsed = {
+ .host = { .name = "example.com" },
+ .userid = "user",
+ .mailbox = "INBOX/Important",
+ .uid = 56, .section = "AA/" }
+ },{
+ .url = "imap://user@example.com/INBOX/Important/;UID=56/"
+ ";SECTION=AA/BB/../..",
+ .url_parsed = {
+ .host = { .name = "example.com" },
+ .userid = "user",
+ .mailbox = "INBOX/Important",
+ .uid = 56, .section = NULL }
+ },{
+ .url = "imap://user@example.com/INBOX/Important/;UID=234/"
+ ";SECTION=HEADER.FIELDS%20(%22To%22%20%22From%22)",
+ .url_parsed = {
+ .host = { .name = "example.com" },
+ .userid = "user",
+ .mailbox = "INBOX/Important",
+ .uid = 234, .section = "HEADER.FIELDS (\"To\" \"From\")" }
+ },{
+ .url = "imap://user@example.com/INBOX/Important/;UID=234/"
+ ";PARTIAL=10.250",
+ .url_parsed = {
+ .host = { .name = "example.com" },
+ .userid = "user",
+ .mailbox = "INBOX/Important",
+ .uid = 234, .section = NULL, .partial_offset = 10, .partial_size = 250 }
+ },{
+ .url = "imap://hendrik@example.com/INBOX/Important/;UID=34534/"
+ ";SECTION=1.3.TEXT/;PARTIAL=0.34254",
+ .url_parsed = {
+ .host = { .name = "example.com" },
+ .userid = "hendrik",
+ .mailbox = "INBOX/Important",
+ .uid = 34534, .section = "1.3.TEXT",
+ .partial_offset = 0, .partial_size = 34254 }
+ },{
+ .url = "imap://hendrik@example.com/INBOX/Sent"
+ ";UIDVALIDITY=534?SUBJECT%20%22Frop?%22",
+ .url_parsed = {
+ .host = { .name = "example.com" },
+ .userid = "hendrik",
+ .mailbox = "INBOX/Sent", .uidvalidity = 534,
+ .search_program = "SUBJECT \"Frop?\"" }
+ },{
+ .url = "//hendrik@example.org/INBOX/Trash",
+ .url_base = {
+ .host = { .name = "example.com" },
+ .userid = "user" },
+ .url_parsed = {
+ .host = { .name = "example.org" },
+ .userid = "hendrik",
+ .mailbox = "INBOX/Trash" }
+ },{
+ .url = "/INBOX/Trash",
+ .url_base = {
+ .host = { .name = "example.com" },
+ .userid = "user" },
+ .url_parsed = {
+ .host = { .name = "example.com" },
+ .userid = "user",
+ .mailbox = "INBOX/Trash" }
+ },{
+ .url = "user@example.com",
+ .url_base = {
+ .host = { .name = "example.com" },
+ .userid = "user",
+ .mailbox = "INBOX/Accounts" },
+ .url_parsed = {
+ .host = { .name = "example.com" },
+ .userid = "user",
+ .mailbox = "INBOX/Accounts/user@example.com" }
+ },{
+ .url = "Drafts",
+ .url_base = {
+ .host = { .name = "example.com" },
+ .userid = "user",
+ .mailbox = "INBOX/" },
+ .url_parsed = {
+ .host = { .name = "example.com" },
+ .userid = "user",
+ .mailbox = "INBOX/Drafts" }
+ },{
+ .url = "../Drafts",
+ .url_base = {
+ .host = { .name = "example.com" },
+ .userid = "user",
+ .mailbox = "INBOX/Trash" },
+ .url_parsed = {
+ .host = { .name = "example.com" },
+ .userid = "user",
+ .mailbox = "INBOX/Drafts" }
+ },{
+ .url = "../Junk",
+ .url_base = {
+ .host = { .name = "example.com" },
+ .userid = "user",
+ .mailbox = "INBOX/Trash",
+ .uidvalidity = 23452 },
+ .url_parsed = {
+ .host = { .name = "example.com" },
+ .userid = "user",
+ .mailbox = "INBOX/Junk",
+ .uidvalidity = 0 }
+ },{
+ .url = "../Junk;UIDVALIDITY=23",
+ .url_base = {
+ .host = { .name = "example.com" },
+ .userid = "user",
+ .mailbox = "INBOX/Trash",
+ .uidvalidity = 23452 },
+ .url_parsed = {
+ .host = { .name = "example.com" },
+ .userid = "user",
+ .mailbox = "INBOX/Junk",
+ .uidvalidity = 23 }
+ },{
+ .url = "../../%23shared;UIDVALIDITY=23452",
+ .url_base = {
+ .host = { .name = "example.com" },
+ .userid = "user",
+ .mailbox = "INBOX/Trash",
+ .uidvalidity = 764 },
+ .url_parsed = {
+ .host = { .name = "example.com" },
+ .userid = "user",
+ .mailbox = "#shared",
+ .uidvalidity = 23452 }
+ },{
+ .url = "../../%23news;UIDVALIDITY=546/;UID=456",
+ .url_base = {
+ .host = { .name = "example.com" },
+ .userid = "user",
+ .mailbox = "INBOX/Trash",
+ .uidvalidity = 23452,
+ .uid = 65 },
+ .url_parsed = {
+ .host = { .name = "example.com" },
+ .userid = "user",
+ .mailbox = "#news",
+ .uidvalidity = 546,
+ .uid = 456 }
+ },{
+ .url = "",
+ .url_base = {
+ .host = { .name = "example.com" },
+ .userid = "user",
+ .mailbox = "INBOX/Trash",
+ .uidvalidity = 23452 },
+ .url_parsed = {
+ .host = { .name = "example.com" },
+ .userid = "user",
+ .mailbox = "INBOX/Trash",
+ .uidvalidity = 23452 }
+ },{
+ .url = "",
+ .url_base = {
+ .host = { .name = "example.com" },
+ .userid = "user",
+ .mailbox = "INBOX/Trash",
+ .uidvalidity = 23452,
+ .uid = 65 },
+ .url_parsed = {
+ .host = { .name = "example.com" },
+ .userid = "user",
+ .mailbox = "INBOX/Trash",
+ .uidvalidity = 23452,
+ .uid = 65 }
+ },{
+ .url = "",
+ .url_base = {
+ .host = { .name = "example.com" },
+ .userid = "user",
+ .mailbox = "INBOX/Trash",
+ .uidvalidity = 23452,
+ .uid = 65,
+ .section = "AA/BB",
+ .have_partial = TRUE, .partial_offset = 1024, .partial_size = 1024 },
+ .url_parsed = {
+ .host = { .name = "example.com" },
+ .userid = "user",
+ .mailbox = "INBOX/Trash",
+ .uidvalidity = 23452,
+ .uid = 65,
+ .section = "AA/BB",
+ .have_partial = TRUE, .partial_offset = 1024, .partial_size = 1024 }
+ },{
+ .url = "",
+ .url_base = {
+ .host = { .name = "example.com" },
+ .userid = "user",
+ .mailbox = "INBOX/Trash",
+ .uidvalidity = 23452,
+ .uid = 65,
+ .have_partial = TRUE, .partial_offset = 1024, .partial_size = 1024 },
+ .url_parsed = {
+ .host = { .name = "example.com" },
+ .userid = "user",
+ .mailbox = "INBOX/Trash",
+ .uidvalidity = 23452,
+ .uid = 65,
+ .have_partial = TRUE, .partial_offset = 1024, .partial_size = 1024 }
+ },{
+ .url = ";UID=4767",
+ .url_base = {
+ .host = { .name = "example.com" },
+ .userid = "user",
+ .mailbox = "INBOX/Trash",
+ .uidvalidity = 23452,
+ .uid = 65 },
+ .url_parsed = {
+ .host = { .name = "example.com" },
+ .userid = "user",
+ .mailbox = "INBOX/Trash",
+ .uidvalidity = 23452,
+ .uid = 4767 }
+ },{
+ .url = ";UID=4767",
+ .url_base = {
+ .host = { .name = "example.com" },
+ .userid = "user",
+ .mailbox = "INBOX/Trash",
+ .uidvalidity = 23452},
+ .url_parsed = {
+ .host = { .name = "example.com" },
+ .userid = "user",
+ .mailbox = "INBOX/Trash",
+ .uidvalidity = 23452,
+ .uid = 4767 }
+ },{
+ .url = "../;UID=4767",
+ .url_base = {
+ .host = { .name = "example.com" },
+ .userid = "user",
+ .mailbox = "INBOX/Trash",
+ .uidvalidity = 23452,
+ .uid = 65 },
+ .url_parsed = {
+ .host = { .name = "example.com" },
+ .userid = "user",
+ .mailbox = "INBOX",
+ .uidvalidity = 0,
+ .uid = 4767 }
+ },{
+ .url = "../;UID=4767/;SECTION=TEXT",
+ .url_base = {
+ .host = { .name = "example.com" },
+ .userid = "user",
+ .mailbox = "INBOX/Trash",
+ .uidvalidity = 23452,
+ .uid = 65,
+ .section = "1.2.3.MIME" },
+ .url_parsed = {
+ .host = { .name = "example.com" },
+ .userid = "user",
+ .mailbox = "INBOX/Trash",
+ .uidvalidity = 23452,
+ .uid = 4767,
+ .section = "TEXT" }
+ },{
+ .url = ";SECTION=TEXT",
+ .url_base = {
+ .host = { .name = "example.com" },
+ .userid = "user",
+ .mailbox = "INBOX/Drafts",
+ .uidvalidity = 769,
+ .uid = 43,
+ .section = "1.2.3.MIME" },
+ .url_parsed = {
+ .host = { .name = "example.com" },
+ .userid = "user",
+ .mailbox = "INBOX/Drafts",
+ .uidvalidity = 769,
+ .uid = 43,
+ .section = "TEXT" }
+ },{
+ .url = "..",
+ .url_base = {
+ .host = { .name = "example.com" },
+ .userid = "user",
+ .mailbox = "INBOX/Drafts",
+ .uidvalidity = 769,
+ .uid = 43,
+ .section = "AA/BB" },
+ .url_parsed = {
+ .host = { .name = "example.com" },
+ .userid = "user",
+ .mailbox = "INBOX/Drafts",
+ .uidvalidity = 769,
+ .uid = 43 }
+ },{
+ .url = "../;SECTION=CC",
+ .url_base = {
+ .host = { .name = "example.com" },
+ .userid = "user",
+ .mailbox = "INBOX/Drafts",
+ .uidvalidity = 769,
+ .uid = 43,
+ .section = "AA/BB" },
+ .url_parsed = {
+ .host = { .name = "example.com" },
+ .userid = "user",
+ .mailbox = "INBOX/Drafts",
+ .uidvalidity = 769,
+ .uid = 43,
+ .section = "CC" }
+ },{
+ .url = "CC",
+ .url_base = {
+ .host = { .name = "example.com" },
+ .userid = "user",
+ .mailbox = "INBOX/Drafts",
+ .uidvalidity = 769,
+ .uid = 43,
+ .section = "AA/BB" },
+ .url_parsed = {
+ .host = { .name = "example.com" },
+ .userid = "user",
+ .mailbox = "INBOX/Drafts",
+ .uidvalidity = 769,
+ .uid = 43,
+ .section = "AA/CC" }
+ },{
+ .url = ";PARTIAL=1024.1024",
+ .url_base = {
+ .host = { .name = "example.com" },
+ .userid = "user",
+ .mailbox = "INBOX/Drafts",
+ .uidvalidity = 769,
+ .uid = 43,
+ .have_partial = TRUE, .partial_offset = 0, .partial_size = 1024 },
+ .url_parsed = {
+ .host = { .name = "example.com" },
+ .userid = "user",
+ .mailbox = "INBOX/Drafts",
+ .uidvalidity = 769,
+ .uid = 43,
+ .have_partial = TRUE, .partial_offset = 1024, .partial_size = 1024 }
+ },{
+ .url = "../CC/;PARTIAL=0.512",
+ .url_base = {
+ .host = { .name = "example.com" },
+ .userid = "user",
+ .mailbox = "INBOX/Drafts",
+ .uidvalidity = 769,
+ .uid = 43,
+ .section = "AA/BB",
+ .have_partial = TRUE, .partial_offset = 1024, .partial_size = 1024 },
+ .url_parsed = {
+ .host = { .name = "example.com" },
+ .userid = "user",
+ .mailbox = "INBOX/Drafts",
+ .uidvalidity = 769,
+ .uid = 43,
+ .section = "AA/CC",
+ .have_partial = TRUE, .partial_offset = 0, .partial_size = 512 }
+ },{
+ .url = "imap://user@example.com/INBOX/;UID=377;URLAUTH=anonymous",
+ .flags = IMAP_URL_PARSE_ALLOW_URLAUTH,
+ .url_parsed = {
+ .host = { .name = "example.com" },
+ .userid = "user",
+ .mailbox = "INBOX",
+ .uid = 377,
+ .uauth_rumpurl = "imap://user@example.com/INBOX/;UID=377"
+ ";URLAUTH=anonymous",
+ .uauth_access_application = "anonymous"}
+ },{
+ .url = "imap://user@example.com/INBOX/;UID=377"
+ ";URLAUTH=anonymous:internal:4142434445464748494A4B4C4D4E4F5051525354",
+ .flags = IMAP_URL_PARSE_ALLOW_URLAUTH,
+ .url_parsed = {
+ .host = { .name = "example.com" },
+ .userid = "user",
+ .mailbox = "INBOX",
+ .uid = 377,
+ .uauth_rumpurl = "imap://user@example.com/INBOX/;UID=377"
+ ";URLAUTH=anonymous",
+ .uauth_access_application = "anonymous",
+ .uauth_mechanism = "internal",
+ .uauth_token = (const unsigned char *)"ABCDEFGHIJKLMNOPQRST",
+ .uauth_token_size = 20}
+ },{
+ .url = "imap://user@example.com/INBOX/;UID=377"
+ ";EXPIRE=2011-02-12T12:45:14+01:00"
+ ";URLAUTH=user+frop:internal:4142434445464748494A4B4C4D4E4F5051525354",
+ .flags = IMAP_URL_PARSE_ALLOW_URLAUTH,
+ .url_parsed = {
+ .host = { .name = "example.com" },
+ .userid = "user",
+ .mailbox = "INBOX",
+ .uid = 377,
+ .uauth_rumpurl = "imap://user@example.com/INBOX/;UID=377"
+ ";EXPIRE=2011-02-12T12:45:14+01:00;URLAUTH=user+frop",
+ .uauth_access_application = "user",
+ .uauth_access_user = "frop",
+ .uauth_mechanism = "internal",
+ .uauth_token = (const unsigned char *)"ABCDEFGHIJKLMNOPQRST",
+ .uauth_token_size = 20}
+ }
+};
+
+static const unsigned int valid_url_test_count = N_ELEMENTS(valid_url_tests);
+
+static void test_imap_url_valid(void)
+{
+ unsigned int i;
+
+ for (i = 0; i < valid_url_test_count; i++) T_BEGIN {
+ const char *url = valid_url_tests[i].url;
+ enum imap_url_parse_flags flags = valid_url_tests[i].flags;
+ const struct imap_url *urlt = &valid_url_tests[i].url_parsed;
+ const struct imap_url *urlb = &valid_url_tests[i].url_base;
+ struct imap_url *urlp;
+ const char *error = NULL;
+
+ test_begin(t_strdup_printf("imap url valid [%d]", i));
+
+ if (urlb->host.name == NULL) urlb = NULL;
+ if (imap_url_parse(url, urlb, flags, &urlp, &error) < 0)
+ urlp = NULL;
+
+ test_out_reason(t_strdup_printf("imap_url_parse(%s)",
+ valid_url_tests[i].url), urlp != NULL, error);
+ if (urlp != NULL) {
+ if (urlp->host.name == NULL || urlt->host.name == NULL) {
+ test_out_quiet(t_strdup_printf("url->host.name = %s", urlp->host.name),
+ urlp->host.name == urlt->host.name);
+ } else {
+ test_out_quiet(t_strdup_printf("url->host.name = %s", urlp->host.name),
+ strcmp(urlp->host.name, urlt->host.name) == 0);
+ }
+ if (urlp->userid == NULL || urlt->userid == NULL) {
+ test_out_quiet(t_strdup_printf("url->userid = %s", urlp->userid),
+ urlp->userid == urlt->userid);
+ } else {
+ test_out_quiet(t_strdup_printf("url->userid = %s", urlp->userid),
+ strcmp(urlp->userid, urlt->userid) == 0);
+ }
+ if (urlp->auth_type == NULL || urlt->auth_type == NULL) {
+ test_out_quiet(t_strdup_printf("url->auth_type = %s", urlp->auth_type),
+ urlp->auth_type == urlt->auth_type);
+ } else {
+ test_out_quiet(t_strdup_printf("url->auth_type = %s", urlp->auth_type),
+ strcmp(urlp->auth_type, urlt->auth_type) == 0);
+ }
+ if (urlp->port == 0) {
+ test_out_quiet("url->port = (unspecified)",
+ urlp->port == urlt->port);
+ } else {
+ test_out_quiet(t_strdup_printf("url->port = %u", urlp->port),
+ urlp->port == urlt->port);
+ }
+ if (urlp->host.ip.family == 0) {
+ test_out_quiet("url->host.ip = (unspecified)",
+ urlp->host.ip.family == urlt->host.ip.family);
+ } else {
+ test_out_quiet("url->host.ip = (valid)",
+ urlp->host.ip.family == urlt->host.ip.family);
+ }
+ if (urlp->mailbox == NULL || urlt->mailbox == NULL) {
+ test_out_quiet(t_strdup_printf("url->mailbox = %s", urlp->mailbox),
+ urlp->mailbox == urlt->mailbox);
+ } else {
+ test_out_quiet(t_strdup_printf("url->mailbox = %s", urlp->mailbox),
+ strcmp(urlp->mailbox, urlt->mailbox) == 0);
+ }
+ test_out_quiet(t_strdup_printf("url->uidvalidity = %u", urlp->uidvalidity),
+ urlp->uidvalidity == urlt->uidvalidity);
+ test_out_quiet(t_strdup_printf("url->uid = %u", urlp->uid),
+ urlp->uid == urlt->uid);
+ if (urlp->section == NULL || urlt->section == NULL) {
+ test_out_quiet(t_strdup_printf("url->section = %s", urlp->section),
+ urlp->section == urlt->section);
+ } else {
+ test_out_quiet(t_strdup_printf("url->section = %s", urlp->section),
+ strcmp(urlp->section, urlt->section) == 0);
+ }
+ test_out_quiet(t_strdup_printf("url->partial = %"PRIuUOFF_T".%"PRIuUOFF_T,
+ urlp->partial_offset, urlp->partial_size),
+ urlp->partial_offset == urlt->partial_offset &&
+ urlp->partial_size == urlt->partial_size);
+ if (urlp->search_program == NULL || urlt->search_program == NULL) {
+ test_out_quiet(t_strdup_printf(
+ "url->search_program = %s", urlp->search_program),
+ urlp->search_program == urlt->search_program);
+ } else {
+ test_out_quiet(t_strdup_printf(
+ "url->search_program = %s", urlp->search_program),
+ strcmp(urlp->search_program, urlt->search_program) == 0);
+ }
+ if (urlt->uauth_rumpurl != NULL) {
+ if (urlp->uauth_rumpurl == NULL) {
+ test_out_quiet("url->uauth_rumpurl = NULL", FALSE);
+ } else {
+ test_out_quiet(t_strdup_printf(
+ "url->uauth_rumpurl = %s", urlp->uauth_rumpurl),
+ strcmp(urlp->uauth_rumpurl, urlt->uauth_rumpurl) == 0);
+ }
+ if (urlp->uauth_access_application == NULL ||
+ urlt->uauth_access_application == NULL) {
+ test_out_quiet(t_strdup_printf("url->uauth_access_application = %s",
+ urlp->uauth_access_application),
+ urlp->uauth_access_application == urlt->uauth_access_application);
+ } else {
+ test_out_quiet(t_strdup_printf("url->uauth_access_application = %s",
+ urlp->uauth_access_application),
+ strcmp(urlp->uauth_access_application,
+ urlt->uauth_access_application) == 0);
+ }
+ if (urlp->uauth_access_user == NULL ||
+ urlt->uauth_access_user == NULL) {
+ test_out_quiet(t_strdup_printf("url->uauth_access_user = %s",
+ urlp->uauth_access_user),
+ urlp->uauth_access_user == urlt->uauth_access_user);
+ } else {
+ test_out_quiet(t_strdup_printf("url->uauth_access_user = %s",
+ urlp->uauth_access_user),
+ strcmp(urlp->uauth_access_user,
+ urlt->uauth_access_user) == 0);
+ }
+ if (urlp->uauth_mechanism == NULL || urlt->uauth_mechanism == NULL) {
+ test_out_quiet(t_strdup_printf(
+ "url->uauth_mechanism = %s", urlp->uauth_mechanism),
+ urlp->uauth_mechanism == urlt->uauth_mechanism);
+ } else {
+ test_out_quiet(t_strdup_printf(
+ "url->uauth_mechanism = %s", urlp->uauth_mechanism),
+ strcmp(urlp->uauth_mechanism, urlt->uauth_mechanism) == 0);
+ }
+ if (urlp->uauth_token == NULL || urlt->uauth_token == NULL) {
+ test_out_quiet(t_strdup_printf(
+ "url->uauth_token = %s", urlp->uauth_token),
+ urlp->uauth_token == urlt->uauth_token);
+ } else {
+ bool equal = urlp->uauth_token_size == urlt->uauth_token_size;
+ size_t i;
+ test_out_quiet(t_strdup_printf(
+ "url->uauth_token_size = %zu", urlp->uauth_token_size),
+ equal);
+
+ if (equal) {
+ for (i = 0; i < urlp->uauth_token_size; i++) {
+ if (urlp->uauth_token[i] != urlt->uauth_token[i]) {
+ equal = FALSE;
+ break;
+ }
+ }
+ test_out_quiet(t_strdup_printf("url->uauth_token [index=%d]", (int)i),
+ equal);
+ }
+ }
+ }
+ }
+
+ test_end();
+ } T_END;
+}
+
+struct invalid_imap_url_test {
+ const char *url;
+ enum imap_url_parse_flags flags;
+ struct imap_url url_base;
+};
+
+static const struct invalid_imap_url_test invalid_url_tests[] = {
+ {
+ .url = "http://www.dovecot.org"
+ },{
+ .url = "imap:/INBOX"
+ },{
+ .url = "imap://user@example.com/INBOX",
+ .flags = IMAP_URL_PARSE_REQUIRE_RELATIVE,
+ .url_base = {
+ .host = { .name = "example.com" },
+ .userid = "user" }
+ },{
+ .url = ""
+ },{
+ .url = "/INBOX/;UID=377"
+ },{
+ .url = "imap://user@example.com/INBOX/;UID=377/;SECTION=TEXT?ALL"
+ },{
+ .url = "imap://user@example.com/INBOX/?"
+ },{
+ .url = "imap://user@example.com/INBOX/#Fragment"
+ },{
+ .url = "imap://user@example.com/INBOX/\""
+ },{
+ .url = "imap:///INBOX"
+ },{
+ .url = "imap://[]/INBOX"
+ },{
+ .url = "imap://[v08.234:232:234:234:2221]/INBOX"
+ },{
+ .url = "imap://[1::34a:34:234::6]/INBOX"
+ },{
+ .url = "imap://example%a.com/INBOX"
+ },{
+ .url = "imap://example.com%/INBOX"
+ },{
+ .url = "imap://example%00.com/INBOX"
+ },{
+ .url = "imap://example.com:65539/INBOX"
+ },{
+ .url = "imap://user;ATH=frop@example.com"
+ },{
+ .url = "imap://user;AUTH=frop;friep@example.com"
+ },{
+ .url = "imap://user;AUTH=@example.com"
+ },{
+ .url = "imap://user:password@example.com"
+ },{
+ .url = "imap://user;AUTH=A:B@example.com"
+ },{
+ .url = "imap://user%@example.com"
+ },{
+ .url = "imap://user%00@example.com"
+ },{
+ .url = "imap://user%ar;AUTH=*@example.com"
+ },{
+ .url = "imap://;AUTH=FR%etD@example.com"
+ },{
+ .url = "imap://user;AUTH=%@example.com"
+ },{
+ .url = "imap://user;AUTH=%00@example.com"
+ },{
+ .url = "imap://example.com/INBOX/%00/"
+ },{
+ .url = "imap://example.com/INBOX/%0r/"
+ },{
+ .url = "imap://example.com/INBOX/Trash/%/"
+ },{
+ .url = "imap://example.com/INBOX;UIDVALIDITY=23423;FROP=friep/"
+ },{
+ .url = "imap://example.com/INBOX;UIDVALIDITY=0/;UID=377"
+ },{
+ .url = "imap://example.com/INBOX;UIDVALIDITY=/"
+ },{
+ .url = "imap://example.com/INBOX;UIDVALIDITY=33a/"
+ },{
+ .url = "imap://example.com/INBOX;FROP=friep/"
+ },{
+ .url = "imap://example.com/INBOX/;UID=377;FROP=friep/"
+ },{
+ .url = "imap://example.com/INBOX/;UID=0/"
+ },{
+ .url = "imap://example.com/INBOX/;UID=/"
+ },{
+ .url = "imap://example.com/INBOX/;UID=5e6/"
+ },{
+ .url = "imap://example.com/INBOX/;UID=35/;SECTION=ALL;FROP=43/"
+ },{
+ .url = "imap://example.com/INBOX/;UID=35/;SECTION=/"
+ },{
+ .url = "imap://example.com/INBOX/;UID=34/;PARTIAL="
+ },{
+ .url = "imap://example.com/INBOX/;UID=34/;PARTIAL=0."
+ },{
+ .url = "imap://example.com/INBOX/;UID=34/;PARTIAL=0.e10"
+ },{
+ .url = "imap://example.com/INBOX/;UID=34/;PARTIAL=.3"
+ },{
+ .url = "imap://example.com/INBOX/;UID=34/;PARTIAL=5t4.3"
+ },{
+ .url = "imap://example.com/INBOX/;UID=34/;PARTIAL=0.0"
+ },{
+ .url = "imap://example.com/INBOX/;UID=34/;PARTIAL=0.23409823409820938409823"
+ },{
+ .url = "imap://example.com/INBOX/;UID=377/;FROP=34"
+ },{
+ .url = "imap://example.com/INBOX/;UID=377;FROP=34"
+ },{
+ .url = "imap://example.com/INBOX/;UID=377;EXPIRE=2010-02-02T12:00:12Z"
+ },{
+ .url = "imap://example.com/INBOX/;UID=377"
+ ";URLAUTH=anonymous:internal:0ad89fafd79f54afe4523f45aadf2afe"
+ },{
+ .url = "imap://example.com/INBOX/;UID=377;EXPIRE=2011-15-02T00:00:00Z"
+ ";URLAUTH=anonymous:internal:0ad89fafd79f54afe4523f45aadf2afe",
+ .flags = IMAP_URL_PARSE_ALLOW_URLAUTH
+ },{
+ .url = "imap://example.com/INBOX/;UID=377;EXPIRE=2011-10-02T00:00:00Z",
+ .flags = IMAP_URL_PARSE_ALLOW_URLAUTH
+ },{
+ .url = "/INBOX/;UID=377;EXPIRE=2011-10-02T00:00:00Z"
+ ";URLAUTH=anonymous:internal:0ad89fafd79f54afe4523f45aadf2afe",
+ .flags = IMAP_URL_PARSE_ALLOW_URLAUTH
+ },{
+ .url = "imap://example.com/INBOX/;UID=377;URLAUTH=",
+ .flags = IMAP_URL_PARSE_ALLOW_URLAUTH
+ },{
+ .url = "imap://example.com/INBOX/;UID=377"
+ ";URLAUTH=:internal:0ad89fafd79f54afe4523f45aadf2afe",
+ .flags = IMAP_URL_PARSE_ALLOW_URLAUTH
+ },{
+ .url = "imap://example.com/INBOX/;UID=377"
+ ";URLAUTH=user+:internal:0ad89fafd79f54afe4523f45aadf2afe",
+ .flags = IMAP_URL_PARSE_ALLOW_URLAUTH
+ },{
+ .url = "imap://example.com/INBOX/;UID=377"
+ ";URLAUTH=+frop:internal:0ad89fafd79f54afe4523f45aadf2afe",
+ .flags = IMAP_URL_PARSE_ALLOW_URLAUTH
+ },{
+ .url = "imap://example.com/INBOX/;UID=377;URLAUTH=anonymous:",
+ .flags = IMAP_URL_PARSE_ALLOW_URLAUTH
+ },{
+ .url = "imap://example.com/INBOX/;UID=377"
+ ";URLAUTH=anonymous::0ad89fafd79f54afe4523f45aadf2afe",
+ .flags = IMAP_URL_PARSE_ALLOW_URLAUTH
+ },{
+ .url = "imap://example.com/INBOX/;UID=377;URLAUTH=anonymous:internal:",
+ .flags = IMAP_URL_PARSE_ALLOW_URLAUTH
+ },{
+ .url = "imap://example.com/INBOX/;UID=377"
+ ";URLAUTH=anonymous:internal:fd79f54afe4523",
+ .flags = IMAP_URL_PARSE_ALLOW_URLAUTH
+ },{
+ .url = "imap://example.com/INBOX/;UID=377;EXPIRE=2011-10-02T00:00:00Z"
+ ";URLAUTH=anonymous:internal:0ad89fafd79f54afe4523q45aadf2afe",
+ .flags = IMAP_URL_PARSE_ALLOW_URLAUTH
+ },
+};
+
+static const unsigned int invalid_url_test_count = N_ELEMENTS(invalid_url_tests);
+
+static void test_imap_url_invalid(void)
+{
+ unsigned int i;
+
+ for (i = 0; i < invalid_url_test_count; i++) T_BEGIN {
+ const char *url = invalid_url_tests[i].url;
+ enum imap_url_parse_flags flags = invalid_url_tests[i].flags;
+ const struct imap_url *urlb = &invalid_url_tests[i].url_base;
+ struct imap_url *urlp;
+ const char *error = NULL;
+
+ if (urlb->host.name == NULL)
+ urlb = NULL;
+
+ test_begin(t_strdup_printf("imap url invalid [%d]", i));
+
+ if (imap_url_parse(url, urlb, flags, &urlp, &error) < 0)
+ urlp = NULL;
+ test_out_reason(t_strdup_printf("parse %s", url), urlp == NULL, error);
+
+ test_end();
+ } T_END;
+
+}
+
+static const char *parse_create_url_tests[] = {
+ "imap://host.example.com/",
+ "imap://10.0.0.1/",
+ "imap://[::1]/",
+ "imap://user@host.example.com/",
+ "imap://user@host.example.com:993/",
+ "imap://su%3auser@host.example.com/",
+ "imap://user;AUTH=PLAIN@host.example.com/",
+ "imap://user;AUTH=PLAIN@host.example.com/INBOX",
+ "imap://user;AUTH=PLAIN@host.example.com/INBOX/;UID=5",
+ "imap://user;AUTH=PLAIN@host.example.com/INBOX;UIDVALIDITY=15/;UID=5",
+ "imap://user;AUTH=PLAIN@host.example.com/INBOX;UIDVALIDITY=15/;UID=5"
+ "/;SECTION=TEXT",
+ "imap://user;AUTH=PLAIN@host.example.com/INBOX;UIDVALIDITY=15/;UID=5"
+ "/;SECTION=TEXT/;PARTIAL=1",
+ "imap://user;AUTH=PLAIN@host.example.com/INBOX;UIDVALIDITY=15/;UID=5"
+ "/;SECTION=TEXT/;PARTIAL=1.14",
+ "imap://user;AUTH=PLAIN@host.example.com/INBOX;UIDVALIDITY=15/;UID=5"
+ "/;SECTION=TEXT/;PARTIAL=1.14;URLAUTH=anonymous",
+ "imap://user;AUTH=PLAIN@host.example.com/INBOX;UIDVALIDITY=15/;UID=5"
+ "/;SECTION=TEXT/;PARTIAL=1.14;URLAUTH=user+username",
+ "imap://user;AUTH=PLAIN@host.example.com/INBOX?SUBJECT%20%22Frop?%22",
+ "imap://user%3ba@host.example.com/",
+ "imap://user%40example.com@host.example.com/",
+ "imap://user%40example.com;AUTH=STR%23ANGE@host.example.com/",
+ "imap://user;AUTH=PLAIN@host.example.com/INBOX/Important%3bWork",
+ "imap://user@host.example.com/%23shared/news",
+ "imap://user@host.example.com/INBOX;UIDVALIDITY=15/;UID=5"
+ "/;SECTION=HEADER.FIELDS%20(DATE%20FROM)",
+ "imap://user@host.example.com/INBOX;UIDVALIDITY=15/;UID=5"
+ "/;SECTION=TEXT/;PARTIAL=1.14;URLAUTH=user+user%3bname",
+};
+
+static const unsigned int parse_create_url_test_count = N_ELEMENTS(parse_create_url_tests);
+
+static void test_imap_url_parse_create(void)
+{
+ unsigned int i;
+
+ for (i = 0; i < parse_create_url_test_count; i++) T_BEGIN {
+ const char *url = parse_create_url_tests[i];
+ struct imap_url *urlp;
+ const char *error = NULL;
+
+ test_begin(t_strdup_printf("imap url parse/create [%d]", i));
+
+ if (imap_url_parse
+ (url, NULL, IMAP_URL_PARSE_ALLOW_URLAUTH, &urlp, &error) < 0)
+ urlp = NULL;
+ test_out_reason(t_strdup_printf("parse %s", url), urlp != NULL, error);
+ if (urlp != NULL) {
+ const char *urlnew = imap_url_create(urlp);
+ test_out(t_strdup_printf
+ ("create %s", urlnew), strcmp(url, urlnew) == 0);
+ }
+
+ test_end();
+ } T_END;
+
+}
+
+
+int main(void)
+{
+ static void (*const test_functions[])(void) = {
+ test_imap_url_valid,
+ test_imap_url_invalid,
+ test_imap_url_parse_create,
+ NULL
+ };
+ return test_run(test_functions);
+}
diff --git a/src/lib-imap/test-imap-utf7.c b/src/lib-imap/test-imap-utf7.c
new file mode 100644
index 0000000..216eebf
--- /dev/null
+++ b/src/lib-imap/test-imap-utf7.c
@@ -0,0 +1,216 @@
+/* Copyright (c) 2008-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "str.h"
+#include "unichar.h"
+#include "imap-utf7.h"
+#include "test-common.h"
+
+static void test_imap_utf7_by_example(void)
+{
+ static const struct test {
+ const char *utf8;
+ const char *mutf7;
+ } tests[] = {
+ { "&&x&&", "&-&-x&-&-" },
+ { "~peter/mail/\xe5\x8f\xb0\xe5\x8c\x97/\xe6\x97\xa5\xe6\x9c\xac\xe8\xaa\x9e",
+ "~peter/mail/&U,BTFw-/&ZeVnLIqe-" },
+ { "tiet\xc3\xa4&j\xc3\xa4&", "tiet&AOQ-&-j&AOQ-&-" }, /* & is always encoded as &- */
+ { "p\xe4\xe4", NULL },
+ { NULL, "&" },
+ { NULL, "&Jjo" },
+ { NULL, "&Jjo!" },
+ { NULL, "&U,BTFw-&ZeVnLIqe-" } /* unnecessary shift */
+ };
+ string_t *dest, *dest2;
+ unsigned int i;
+
+ dest = t_str_new(256);
+ dest2 = t_str_new(256);
+
+ test_begin("imap mutf7 examples");
+ for (i = 0; i < N_ELEMENTS(tests); i++) {
+ str_truncate(dest, 0);
+ if (tests[i].utf8 != NULL) {
+ if (imap_utf8_to_utf7(tests[i].utf8, dest) < 0)
+ test_assert_idx(tests[i].mutf7 == NULL, i);
+ else
+ test_assert_idx(null_strcmp(tests[i].mutf7, str_c(dest)) == 0, i);
+ } else {
+ /* invalid mUTF-7 - test that escaping works */
+ str_truncate(dest2, 0);
+ imap_utf7_to_utf8_escaped(tests[i].mutf7, "%", dest);
+ imap_escaped_utf8_to_utf7(str_c(dest), '%', dest2);
+ test_assert_idx(strcmp(tests[i].mutf7, str_c(dest2)) == 0, i);
+ }
+ if (tests[i].mutf7 != NULL) {
+ str_truncate(dest, 0);
+ if (imap_utf7_to_utf8(tests[i].mutf7, dest) < 0)
+ test_assert_idx(tests[i].utf8 == NULL, i);
+ else
+ test_assert_idx(null_strcmp(tests[i].utf8, str_c(dest)) == 0, i);
+ test_assert_idx(imap_utf7_is_valid(tests[i].mutf7) != (tests[i].utf8 == NULL), i);
+ }
+ }
+
+ str_truncate(dest, 0);
+ imap_utf7_to_utf8_escaped(".foo%", "%.", dest);
+ test_assert_strcmp(str_c(dest), "%2efoo%25");
+
+ str_truncate(dest, 0);
+ test_assert(imap_escaped_utf8_to_utf7("%foo%2ebar", '%', dest) == 0);
+ test_assert_strcmp(str_c(dest), "%foo.bar");
+
+ test_end();
+}
+
+static void test_imap_utf7_ucs4_cases(void)
+{
+ string_t *src, *dest;
+ const char *orig_src;
+ unsigned int i, j;
+ unichar_t chr;
+
+ src = t_str_new(256);
+ dest = t_str_new(256);
+
+ test_begin("imap mutf7 ucs4 cases");
+ for (chr = 0xffff; chr <= 0x10010; chr++) T_BEGIN {
+ for (i = 1; i <= 10; i++) {
+ str_truncate(src, 0);
+ str_truncate(dest, 0);
+ for (j = 0; j < i; j++) {
+ if (j % 3 == 0)
+ str_append_c(src, 'x');
+ if (j % 5 == 0)
+ str_append_c(src, '&');
+ uni_ucs4_to_utf8_c(chr, src);
+ }
+
+ orig_src = t_strdup(str_c(src));
+ str_truncate(src, 0);
+
+ test_assert_idx(imap_utf8_to_utf7(orig_src, dest) == 0, chr*100+i);
+ test_assert_idx(imap_utf7_to_utf8(str_c(dest), src) == 0, chr*100+i);
+ test_assert_idx(strcmp(str_c(src), orig_src) == 0, chr+100+i);
+ }
+ } T_END;
+ test_end();
+}
+
+static const char mb64[64]="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+,";
+static void test_imap_utf7_non_utf16(void)
+{
+ string_t *dest, *dest2;
+ unsigned int i;
+
+ test_begin("imap mutf7 non-utf16");
+ dest = t_str_new(32);
+ dest2 = t_str_new(32);
+ for (i = 0; i <= 255; ++i) {
+ /* Invalid, code a single 8-bit octet */
+ const char csrc[] = {
+ '&',
+ mb64[i >> 2],
+ mb64[(i & 3) << 4],
+ '-',
+ '\0'
+ };
+ test_assert_idx(!imap_utf7_is_valid(csrc), i);
+
+ /* escaping can reverse the original string */
+ str_truncate(dest, 0);
+ str_truncate(dest2, 0);
+ imap_utf7_to_utf8_escaped(csrc, "%", dest);
+ imap_escaped_utf8_to_utf7(str_c(dest), '%', dest2);
+ test_assert_idx(strcmp(csrc, str_c(dest2)) == 0, i);
+ }
+ for (i = 0; i <= 255; ++i) {
+ /* Invalid, U+00E4 followed by a single octet */
+ const char csrc[] = {
+ '&',
+ mb64[ (0x00 >> 2)],
+ mb64[((0x00 & 0x03) << 4) | (0xe4 >> 4)],
+ mb64[((0xe4 & 0x0f) << 2) | ( i >> 6)],
+ mb64[ i & 0x3f ],
+ '-',
+ '\0'
+ };
+ test_assert_idx(!imap_utf7_is_valid(csrc), i);
+
+ /* escaping can reverse the original string */
+ str_truncate(dest, 0);
+ str_truncate(dest2, 0);
+ imap_utf7_to_utf8_escaped(csrc, "%", dest);
+ imap_escaped_utf8_to_utf7(str_c(dest), '%', dest2);
+ test_assert_idx(strcmp(csrc, str_c(dest2)) == 0, i);
+ }
+ test_end();
+}
+
+static void test_imap_utf7_bad_ascii(void)
+{
+ string_t *dest;
+ char csrc[1+1];
+ unsigned int i;
+
+ dest = t_str_new(256);
+
+ test_begin("imap mutf7 bad ascii");
+ for (i = 1; i <= 0x7f; ++i) {
+ if (i == ' ')
+ i = 0x7f;
+ csrc[0] = i;
+ csrc[1] = '\0';
+ test_assert_idx(!imap_utf7_is_valid(csrc), i);
+ str_truncate(dest, 0);
+ test_assert_idx(imap_utf7_to_utf8(csrc, dest) < 0, i);
+ }
+ test_end();
+}
+
+static void test_imap_utf7_unnecessary(void)
+{
+ string_t *dest;
+ char csrc[1+3+1+1];
+ unsigned int i;
+
+ dest = t_str_new(256);
+
+ test_begin("imap mutf7 unnecessary");
+ for (i = 0x20; i < 0x7f; ++i) {
+ /* Create an invalid escaped encoding of a simple char or '&' */
+ csrc[0] = '&';
+ csrc[1] = mb64[ (0x00 >> 2)];
+ csrc[2] = mb64[((0x00 & 0x03) << 4) | ( i >> 4)];
+ csrc[3] = mb64[(( i & 0x0f) << 2) | 0 ];
+ csrc[4] = '-';
+ csrc[5] = '\0';
+ test_assert_idx(!imap_utf7_is_valid(csrc), i);
+ str_truncate(dest, 0);
+ test_assert_idx(imap_utf7_to_utf8(csrc, dest) < 0, i);
+
+ /* All self-coding characters must self-code */
+ if (i == '&')
+ continue;
+ csrc[0] = i;
+ csrc[1] = '\0';
+ str_truncate(dest, 0);
+ test_assert_idx(imap_utf8_to_utf7(csrc, dest) >= 0, i);
+ test_assert_idx(strcmp(csrc, str_c(dest)) == 0, i);
+ }
+ test_end();
+}
+
+int main(void)
+{
+ static void (*const test_functions[])(void) = {
+ test_imap_utf7_by_example,
+ test_imap_utf7_ucs4_cases,
+ test_imap_utf7_non_utf16,
+ test_imap_utf7_bad_ascii,
+ test_imap_utf7_unnecessary,
+ NULL
+ };
+ return test_run(test_functions);
+}
diff --git a/src/lib-imap/test-imap-util.c b/src/lib-imap/test-imap-util.c
new file mode 100644
index 0000000..90f59a1
--- /dev/null
+++ b/src/lib-imap/test-imap-util.c
@@ -0,0 +1,79 @@
+/* Copyright (c) 2009-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "str.h"
+#include "mail-types.h"
+#include "imap-arg.h"
+#include "imap-util.h"
+#include "test-common.h"
+
+static void test_imap_parse_system_flag(void)
+{
+ test_begin("imap_parse_system_flag");
+ test_assert(imap_parse_system_flag("\\aNswered") == MAIL_ANSWERED);
+ test_assert(imap_parse_system_flag("\\fLagged") == MAIL_FLAGGED);
+ test_assert(imap_parse_system_flag("\\dEleted") == MAIL_DELETED);
+ test_assert(imap_parse_system_flag("\\sEen") == MAIL_SEEN);
+ test_assert(imap_parse_system_flag("\\dRaft") == MAIL_DRAFT);
+ test_assert(imap_parse_system_flag("\\rEcent") == MAIL_RECENT);
+ test_assert(imap_parse_system_flag("answered") == 0);
+ test_assert(imap_parse_system_flag("\\broken") == 0);
+ test_assert(imap_parse_system_flag("\\") == 0);
+ test_assert(imap_parse_system_flag("") == 0);
+ test_end();
+}
+
+static void test_imap_write_arg(void)
+{
+ ARRAY_TYPE(imap_arg_list) list_root, list_sub;
+ struct imap_arg *arg;
+
+ t_array_init(&list_sub, 2);
+ arg = array_append_space(&list_sub);
+ arg->type = IMAP_ARG_ATOM;
+ arg->_data.str = "foo";
+ arg = array_append_space(&list_sub);
+ arg->type = IMAP_ARG_EOL;
+
+ t_array_init(&list_root, 2);
+ arg = array_append_space(&list_root);
+ arg->type = IMAP_ARG_LIST;
+ arg->_data.list = list_sub;
+ arg = array_append_space(&list_root);
+ arg->type = IMAP_ARG_STRING;
+ arg->_data.str = "bar";
+ arg = array_append_space(&list_root);
+ arg->type = IMAP_ARG_EOL;
+
+ const struct {
+ struct imap_arg input;
+ const char *output;
+ } tests[] = {
+ { { .type = IMAP_ARG_NIL }, "NIL" },
+ { { .type = IMAP_ARG_ATOM, ._data.str = "atom" }, "atom" },
+ { { .type = IMAP_ARG_STRING, ._data.str = "s\\t\"ring" }, "\"s\\\\t\\\"ring\"" },
+ { { .type = IMAP_ARG_LITERAL, ._data.str = "l\\i\"t\r\neral" }, "{11}\r\nl\\i\"t\r\neral" },
+ { { .type = IMAP_ARG_LITERAL_SIZE, ._data.literal_size = 12345678 }, "<12345678 byte literal>" },
+ { { .type = IMAP_ARG_LITERAL_SIZE_NONSYNC, ._data.literal_size = 12345678 }, "<12345678 byte literal>" },
+ { { .type = IMAP_ARG_LIST, ._data.list = list_root }, "((foo) \"bar\")" },
+ };
+ string_t *str = t_str_new(100);
+
+ test_begin("imap_write_arg");
+ for (unsigned int i = 0; i < N_ELEMENTS(tests); i++) {
+ str_truncate(str, 0);
+ imap_write_arg(str, &tests[i].input);
+ test_assert_idx(strcmp(str_c(str), tests[i].output) == 0, i);
+ }
+ test_end();
+}
+
+int main(void)
+{
+ static void (*const test_functions[])(void) = {
+ test_imap_parse_system_flag,
+ test_imap_write_arg,
+ NULL
+ };
+ return test_run(test_functions);
+}
diff --git a/src/lib-index/Makefile.am b/src/lib-index/Makefile.am
new file mode 100644
index 0000000..f203201
--- /dev/null
+++ b/src/lib-index/Makefile.am
@@ -0,0 +1,150 @@
+noinst_LTLIBRARIES = libindex.la
+
+AM_CPPFLAGS = \
+ -I$(top_srcdir)/src/lib \
+ -I$(top_srcdir)/src/lib-test \
+ -I$(top_srcdir)/src/lib-mail
+
+libindex_la_SOURCES = \
+ mail-cache.c \
+ mail-cache-decisions.c \
+ mail-cache-fields.c \
+ mail-cache-lookup.c \
+ mail-cache-purge.c \
+ mail-cache-transaction.c \
+ mail-cache-sync-update.c \
+ mail-index.c \
+ mail-index-alloc-cache.c \
+ mail-index-dummy-view.c \
+ mail-index-fsck.c \
+ mail-index-lock.c \
+ mail-index-map.c \
+ mail-index-map-hdr.c \
+ mail-index-map-read.c \
+ mail-index-modseq.c \
+ mail-index-transaction.c \
+ mail-index-transaction-export.c \
+ mail-index-transaction-finish.c \
+ mail-index-transaction-sort-appends.c \
+ mail-index-transaction-update.c \
+ mail-index-transaction-view.c \
+ mail-index-strmap.c \
+ mail-index-sync.c \
+ mail-index-sync-ext.c \
+ mail-index-sync-keywords.c \
+ mail-index-sync-update.c \
+ mail-index-util.c \
+ mail-index-view.c \
+ mail-index-view-sync.c \
+ mail-index-write.c \
+ mail-transaction-log.c \
+ mail-transaction-log-append.c \
+ mail-transaction-log-file.c \
+ mail-transaction-log-modseq.c \
+ mail-transaction-log-view.c \
+ mailbox-log.c
+
+headers = \
+ mail-cache.h \
+ mail-cache-private.h \
+ mail-index.h \
+ mail-index-alloc-cache.h \
+ mail-index-modseq.h \
+ mail-index-private.h \
+ mail-index-strmap.h \
+ mail-index-sync-private.h \
+ mail-index-transaction-private.h \
+ mail-index-util.h \
+ mail-index-view-private.h \
+ mail-transaction-log.h \
+ mail-transaction-log-private.h \
+ mail-transaction-log-view-private.h \
+ mailbox-log.h
+
+test_programs = \
+ test-mail-cache \
+ test-mail-cache-fields \
+ test-mail-cache-purge \
+ test-mail-index \
+ test-mail-index-map \
+ test-mail-index-modseq \
+ test-mail-index-sync-ext \
+ test-mail-index-transaction-finish \
+ test-mail-index-transaction-update \
+ test-mail-index-write \
+ test-mail-transaction-log-append \
+ test-mail-transaction-log-file \
+ test-mail-transaction-log-view
+
+noinst_PROGRAMS = $(test_programs)
+
+test_libs = \
+ mail-index-util.lo \
+ ../lib-test/libtest.la \
+ ../lib/liblib.la
+
+test_deps = $(noinst_LTLIBRARIES) $(test_libs)
+
+test_mail_cache_SOURCES = test-mail-cache-common.c test-mail-cache.c
+test_mail_cache_LDADD = $(noinst_LTLIBRARIES) $(test_libs)
+test_mail_cache_DEPENDENCIES = $(test_deps)
+
+test_mail_cache_fields_SOURCES = test-mail-cache-common.c test-mail-cache-fields.c
+test_mail_cache_fields_LDADD = $(noinst_LTLIBRARIES) $(test_libs)
+test_mail_cache_fields_DEPENDENCIES = $(test_deps)
+
+test_mail_cache_purge_SOURCES = test-mail-cache-common.c test-mail-cache-purge.c
+test_mail_cache_purge_LDADD = $(noinst_LTLIBRARIES) $(test_libs)
+test_mail_cache_purge_DEPENDENCIES = $(test_deps)
+
+test_mail_index_SOURCES = test-mail-index.c
+test_mail_index_LDADD = $(noinst_LTLIBRARIES) $(test_libs)
+test_mail_index_DEPENDENCIES = $(test_deps)
+
+test_mail_index_map_SOURCES = test-mail-index-map.c
+test_mail_index_map_LDADD = $(noinst_LTLIBRARIES) $(test_libs)
+test_mail_index_map_DEPENDENCIES = $(test_deps)
+
+test_mail_index_modseq_SOURCES = test-mail-index-modseq.c
+test_mail_index_modseq_LDADD = $(noinst_LTLIBRARIES) $(test_libs)
+test_mail_index_modseq_DEPENDENCIES = $(test_deps)
+
+test_mail_index_sync_ext_SOURCES = test-mail-index-sync-ext.c
+test_mail_index_sync_ext_LDADD = $(noinst_LTLIBRARIES) $(test_libs)
+test_mail_index_sync_ext_DEPENDENCIES = $(test_deps)
+
+test_mail_index_transaction_finish_SOURCES = test-mail-index-transaction-finish.c
+test_mail_index_transaction_finish_LDADD = mail-index-transaction-finish.lo $(test_libs)
+test_mail_index_transaction_finish_DEPENDENCIES = $(test_deps)
+
+test_mail_index_transaction_update_SOURCES = test-mail-index-transaction-update.c
+test_mail_index_transaction_update_LDADD = mail-index-transaction-update.lo $(test_libs)
+test_mail_index_transaction_update_DEPENDENCIES = $(test_deps)
+
+test_mail_index_write_SOURCES = test-mail-index-write.c
+test_mail_index_write_LDADD = mail-index-write.lo $(test_libs)
+test_mail_index_write_DEPENDENCIES = $(test_deps)
+
+test_mail_transaction_log_append_SOURCES = test-mail-transaction-log-append.c
+test_mail_transaction_log_append_LDADD = mail-transaction-log-append.lo $(test_libs)
+test_mail_transaction_log_append_DEPENDENCIES = $(test_deps)
+
+test_mail_transaction_log_file_SOURCES = test-mail-transaction-log-file.c
+test_mail_transaction_log_file_LDADD = $(noinst_LTLIBRARIES) $(test_libs)
+test_mail_transaction_log_file_DEPENDENCIES = $(test_deps)
+
+test_mail_transaction_log_view_SOURCES = test-mail-transaction-log-view.c
+test_mail_transaction_log_view_LDADD = mail-transaction-log-view.lo $(test_libs)
+test_mail_transaction_log_view_DEPENDENCIES = $(test_deps)
+
+check-local:
+ for bin in $(test_programs); do \
+ if ! $(RUN_TEST) ./$$bin; then exit 1; fi; \
+ done
+
+pkginc_libdir=$(pkgincludedir)
+pkginc_lib_HEADERS = $(headers)
+
+noinst_HEADERS = \
+ test-mail-cache.h \
+ test-mail-index.h
diff --git a/src/lib-index/Makefile.in b/src/lib-index/Makefile.in
new file mode 100644
index 0000000..a25cb29
--- /dev/null
+++ b/src/lib-index/Makefile.in
@@ -0,0 +1,1285 @@
+# Makefile.in generated by automake 1.16.1 from Makefile.am.
+# @configure_input@
+
+# Copyright (C) 1994-2018 Free Software Foundation, Inc.
+
+# This Makefile.in is free software; the Free Software Foundation
+# gives unlimited permission to copy and/or distribute it,
+# with or without modifications, as long as this notice is preserved.
+
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY, to the extent permitted by law; without
+# even the implied warranty of MERCHANTABILITY or FITNESS FOR A
+# PARTICULAR PURPOSE.
+
+@SET_MAKE@
+
+
+
+VPATH = @srcdir@
+am__is_gnu_make = { \
+ if test -z '$(MAKELEVEL)'; then \
+ false; \
+ elif test -n '$(MAKE_HOST)'; then \
+ true; \
+ elif test -n '$(MAKE_VERSION)' && test -n '$(CURDIR)'; then \
+ true; \
+ else \
+ false; \
+ fi; \
+}
+am__make_running_with_option = \
+ case $${target_option-} in \
+ ?) ;; \
+ *) echo "am__make_running_with_option: internal error: invalid" \
+ "target option '$${target_option-}' specified" >&2; \
+ exit 1;; \
+ esac; \
+ has_opt=no; \
+ sane_makeflags=$$MAKEFLAGS; \
+ if $(am__is_gnu_make); then \
+ sane_makeflags=$$MFLAGS; \
+ else \
+ case $$MAKEFLAGS in \
+ *\\[\ \ ]*) \
+ bs=\\; \
+ sane_makeflags=`printf '%s\n' "$$MAKEFLAGS" \
+ | sed "s/$$bs$$bs[$$bs $$bs ]*//g"`;; \
+ esac; \
+ fi; \
+ skip_next=no; \
+ strip_trailopt () \
+ { \
+ flg=`printf '%s\n' "$$flg" | sed "s/$$1.*$$//"`; \
+ }; \
+ for flg in $$sane_makeflags; do \
+ test $$skip_next = yes && { skip_next=no; continue; }; \
+ case $$flg in \
+ *=*|--*) continue;; \
+ -*I) strip_trailopt 'I'; skip_next=yes;; \
+ -*I?*) strip_trailopt 'I';; \
+ -*O) strip_trailopt 'O'; skip_next=yes;; \
+ -*O?*) strip_trailopt 'O';; \
+ -*l) strip_trailopt 'l'; skip_next=yes;; \
+ -*l?*) strip_trailopt 'l';; \
+ -[dEDm]) skip_next=yes;; \
+ -[JT]) skip_next=yes;; \
+ esac; \
+ case $$flg in \
+ *$$target_option*) has_opt=yes; break;; \
+ esac; \
+ done; \
+ test $$has_opt = yes
+am__make_dryrun = (target_option=n; $(am__make_running_with_option))
+am__make_keepgoing = (target_option=k; $(am__make_running_with_option))
+pkgdatadir = $(datadir)/@PACKAGE@
+pkgincludedir = $(includedir)/@PACKAGE@
+pkglibdir = $(libdir)/@PACKAGE@
+pkglibexecdir = $(libexecdir)/@PACKAGE@
+am__cd = CDPATH="$${ZSH_VERSION+.}$(PATH_SEPARATOR)" && cd
+install_sh_DATA = $(install_sh) -c -m 644
+install_sh_PROGRAM = $(install_sh) -c
+install_sh_SCRIPT = $(install_sh) -c
+INSTALL_HEADER = $(INSTALL_DATA)
+transform = $(program_transform_name)
+NORMAL_INSTALL = :
+PRE_INSTALL = :
+POST_INSTALL = :
+NORMAL_UNINSTALL = :
+PRE_UNINSTALL = :
+POST_UNINSTALL = :
+build_triplet = @build@
+host_triplet = @host@
+noinst_PROGRAMS = $(am__EXEEXT_1)
+subdir = src/lib-index
+ACLOCAL_M4 = $(top_srcdir)/aclocal.m4
+am__aclocal_m4_deps = $(top_srcdir)/m4/ac_checktype2.m4 \
+ $(top_srcdir)/m4/ac_typeof.m4 $(top_srcdir)/m4/arc4random.m4 \
+ $(top_srcdir)/m4/blockdev.m4 $(top_srcdir)/m4/c99_vsnprintf.m4 \
+ $(top_srcdir)/m4/clock_gettime.m4 $(top_srcdir)/m4/crypt.m4 \
+ $(top_srcdir)/m4/crypt_xpg6.m4 $(top_srcdir)/m4/dbqlk.m4 \
+ $(top_srcdir)/m4/dirent_dtype.m4 $(top_srcdir)/m4/dovecot.m4 \
+ $(top_srcdir)/m4/fd_passing.m4 $(top_srcdir)/m4/fdatasync.m4 \
+ $(top_srcdir)/m4/flexible_array_member.m4 \
+ $(top_srcdir)/m4/glibc.m4 $(top_srcdir)/m4/gmtime_max.m4 \
+ $(top_srcdir)/m4/gmtime_tm_gmtoff.m4 \
+ $(top_srcdir)/m4/ioloop.m4 $(top_srcdir)/m4/iovec.m4 \
+ $(top_srcdir)/m4/ipv6.m4 $(top_srcdir)/m4/libcap.m4 \
+ $(top_srcdir)/m4/libtool.m4 $(top_srcdir)/m4/libwrap.m4 \
+ $(top_srcdir)/m4/linux_mremap.m4 $(top_srcdir)/m4/ltoptions.m4 \
+ $(top_srcdir)/m4/ltsugar.m4 $(top_srcdir)/m4/ltversion.m4 \
+ $(top_srcdir)/m4/lt~obsolete.m4 $(top_srcdir)/m4/mmap_write.m4 \
+ $(top_srcdir)/m4/mntctl.m4 $(top_srcdir)/m4/modules.m4 \
+ $(top_srcdir)/m4/notify.m4 $(top_srcdir)/m4/nsl.m4 \
+ $(top_srcdir)/m4/off_t_max.m4 $(top_srcdir)/m4/pkg.m4 \
+ $(top_srcdir)/m4/pr_set_dumpable.m4 \
+ $(top_srcdir)/m4/q_quotactl.m4 $(top_srcdir)/m4/quota.m4 \
+ $(top_srcdir)/m4/random.m4 $(top_srcdir)/m4/rlimit.m4 \
+ $(top_srcdir)/m4/sendfile.m4 $(top_srcdir)/m4/size_t_signed.m4 \
+ $(top_srcdir)/m4/sockpeercred.m4 $(top_srcdir)/m4/sql.m4 \
+ $(top_srcdir)/m4/ssl.m4 $(top_srcdir)/m4/st_tim.m4 \
+ $(top_srcdir)/m4/static_array.m4 $(top_srcdir)/m4/test_with.m4 \
+ $(top_srcdir)/m4/time_t.m4 $(top_srcdir)/m4/typeof.m4 \
+ $(top_srcdir)/m4/typeof_dev_t.m4 \
+ $(top_srcdir)/m4/uoff_t_max.m4 $(top_srcdir)/m4/vararg.m4 \
+ $(top_srcdir)/m4/want_apparmor.m4 \
+ $(top_srcdir)/m4/want_bsdauth.m4 \
+ $(top_srcdir)/m4/want_bzlib.m4 \
+ $(top_srcdir)/m4/want_cassandra.m4 \
+ $(top_srcdir)/m4/want_cdb.m4 \
+ $(top_srcdir)/m4/want_checkpassword.m4 \
+ $(top_srcdir)/m4/want_clucene.m4 $(top_srcdir)/m4/want_db.m4 \
+ $(top_srcdir)/m4/want_gssapi.m4 $(top_srcdir)/m4/want_icu.m4 \
+ $(top_srcdir)/m4/want_ldap.m4 $(top_srcdir)/m4/want_lua.m4 \
+ $(top_srcdir)/m4/want_lz4.m4 $(top_srcdir)/m4/want_lzma.m4 \
+ $(top_srcdir)/m4/want_mysql.m4 $(top_srcdir)/m4/want_pam.m4 \
+ $(top_srcdir)/m4/want_passwd.m4 $(top_srcdir)/m4/want_pgsql.m4 \
+ $(top_srcdir)/m4/want_prefetch.m4 \
+ $(top_srcdir)/m4/want_shadow.m4 \
+ $(top_srcdir)/m4/want_sodium.m4 $(top_srcdir)/m4/want_solr.m4 \
+ $(top_srcdir)/m4/want_sqlite.m4 \
+ $(top_srcdir)/m4/want_stemmer.m4 \
+ $(top_srcdir)/m4/want_systemd.m4 \
+ $(top_srcdir)/m4/want_textcat.m4 \
+ $(top_srcdir)/m4/want_unwind.m4 $(top_srcdir)/m4/want_zlib.m4 \
+ $(top_srcdir)/m4/want_zstd.m4 $(top_srcdir)/configure.ac
+am__configure_deps = $(am__aclocal_m4_deps) $(CONFIGURE_DEPENDENCIES) \
+ $(ACLOCAL_M4)
+DIST_COMMON = $(srcdir)/Makefile.am $(noinst_HEADERS) \
+ $(pkginc_lib_HEADERS) $(am__DIST_COMMON)
+mkinstalldirs = $(install_sh) -d
+CONFIG_HEADER = $(top_builddir)/config.h
+CONFIG_CLEAN_FILES =
+CONFIG_CLEAN_VPATH_FILES =
+am__EXEEXT_1 = test-mail-cache$(EXEEXT) \
+ test-mail-cache-fields$(EXEEXT) test-mail-cache-purge$(EXEEXT) \
+ test-mail-index$(EXEEXT) test-mail-index-map$(EXEEXT) \
+ test-mail-index-modseq$(EXEEXT) \
+ test-mail-index-sync-ext$(EXEEXT) \
+ test-mail-index-transaction-finish$(EXEEXT) \
+ test-mail-index-transaction-update$(EXEEXT) \
+ test-mail-index-write$(EXEEXT) \
+ test-mail-transaction-log-append$(EXEEXT) \
+ test-mail-transaction-log-file$(EXEEXT) \
+ test-mail-transaction-log-view$(EXEEXT)
+PROGRAMS = $(noinst_PROGRAMS)
+LTLIBRARIES = $(noinst_LTLIBRARIES)
+libindex_la_LIBADD =
+am_libindex_la_OBJECTS = mail-cache.lo mail-cache-decisions.lo \
+ mail-cache-fields.lo mail-cache-lookup.lo mail-cache-purge.lo \
+ mail-cache-transaction.lo mail-cache-sync-update.lo \
+ mail-index.lo mail-index-alloc-cache.lo \
+ mail-index-dummy-view.lo mail-index-fsck.lo mail-index-lock.lo \
+ mail-index-map.lo mail-index-map-hdr.lo mail-index-map-read.lo \
+ mail-index-modseq.lo mail-index-transaction.lo \
+ mail-index-transaction-export.lo \
+ mail-index-transaction-finish.lo \
+ mail-index-transaction-sort-appends.lo \
+ mail-index-transaction-update.lo \
+ mail-index-transaction-view.lo mail-index-strmap.lo \
+ mail-index-sync.lo mail-index-sync-ext.lo \
+ mail-index-sync-keywords.lo mail-index-sync-update.lo \
+ mail-index-util.lo mail-index-view.lo mail-index-view-sync.lo \
+ mail-index-write.lo mail-transaction-log.lo \
+ mail-transaction-log-append.lo mail-transaction-log-file.lo \
+ mail-transaction-log-modseq.lo mail-transaction-log-view.lo \
+ mailbox-log.lo
+libindex_la_OBJECTS = $(am_libindex_la_OBJECTS)
+AM_V_lt = $(am__v_lt_@AM_V@)
+am__v_lt_ = $(am__v_lt_@AM_DEFAULT_V@)
+am__v_lt_0 = --silent
+am__v_lt_1 =
+am_test_mail_cache_OBJECTS = test-mail-cache-common.$(OBJEXT) \
+ test-mail-cache.$(OBJEXT)
+test_mail_cache_OBJECTS = $(am_test_mail_cache_OBJECTS)
+am_test_mail_cache_fields_OBJECTS = test-mail-cache-common.$(OBJEXT) \
+ test-mail-cache-fields.$(OBJEXT)
+test_mail_cache_fields_OBJECTS = $(am_test_mail_cache_fields_OBJECTS)
+am_test_mail_cache_purge_OBJECTS = test-mail-cache-common.$(OBJEXT) \
+ test-mail-cache-purge.$(OBJEXT)
+test_mail_cache_purge_OBJECTS = $(am_test_mail_cache_purge_OBJECTS)
+am_test_mail_index_OBJECTS = test-mail-index.$(OBJEXT)
+test_mail_index_OBJECTS = $(am_test_mail_index_OBJECTS)
+am_test_mail_index_map_OBJECTS = test-mail-index-map.$(OBJEXT)
+test_mail_index_map_OBJECTS = $(am_test_mail_index_map_OBJECTS)
+am_test_mail_index_modseq_OBJECTS = test-mail-index-modseq.$(OBJEXT)
+test_mail_index_modseq_OBJECTS = $(am_test_mail_index_modseq_OBJECTS)
+am_test_mail_index_sync_ext_OBJECTS = \
+ test-mail-index-sync-ext.$(OBJEXT)
+test_mail_index_sync_ext_OBJECTS = \
+ $(am_test_mail_index_sync_ext_OBJECTS)
+am_test_mail_index_transaction_finish_OBJECTS = \
+ test-mail-index-transaction-finish.$(OBJEXT)
+test_mail_index_transaction_finish_OBJECTS = \
+ $(am_test_mail_index_transaction_finish_OBJECTS)
+am_test_mail_index_transaction_update_OBJECTS = \
+ test-mail-index-transaction-update.$(OBJEXT)
+test_mail_index_transaction_update_OBJECTS = \
+ $(am_test_mail_index_transaction_update_OBJECTS)
+am_test_mail_index_write_OBJECTS = test-mail-index-write.$(OBJEXT)
+test_mail_index_write_OBJECTS = $(am_test_mail_index_write_OBJECTS)
+am_test_mail_transaction_log_append_OBJECTS = \
+ test-mail-transaction-log-append.$(OBJEXT)
+test_mail_transaction_log_append_OBJECTS = \
+ $(am_test_mail_transaction_log_append_OBJECTS)
+am_test_mail_transaction_log_file_OBJECTS = \
+ test-mail-transaction-log-file.$(OBJEXT)
+test_mail_transaction_log_file_OBJECTS = \
+ $(am_test_mail_transaction_log_file_OBJECTS)
+am_test_mail_transaction_log_view_OBJECTS = \
+ test-mail-transaction-log-view.$(OBJEXT)
+test_mail_transaction_log_view_OBJECTS = \
+ $(am_test_mail_transaction_log_view_OBJECTS)
+AM_V_P = $(am__v_P_@AM_V@)
+am__v_P_ = $(am__v_P_@AM_DEFAULT_V@)
+am__v_P_0 = false
+am__v_P_1 = :
+AM_V_GEN = $(am__v_GEN_@AM_V@)
+am__v_GEN_ = $(am__v_GEN_@AM_DEFAULT_V@)
+am__v_GEN_0 = @echo " GEN " $@;
+am__v_GEN_1 =
+AM_V_at = $(am__v_at_@AM_V@)
+am__v_at_ = $(am__v_at_@AM_DEFAULT_V@)
+am__v_at_0 = @
+am__v_at_1 =
+DEFAULT_INCLUDES = -I.@am__isrc@ -I$(top_builddir)
+depcomp = $(SHELL) $(top_srcdir)/depcomp
+am__maybe_remake_depfiles = depfiles
+am__depfiles_remade = ./$(DEPDIR)/mail-cache-decisions.Plo \
+ ./$(DEPDIR)/mail-cache-fields.Plo \
+ ./$(DEPDIR)/mail-cache-lookup.Plo \
+ ./$(DEPDIR)/mail-cache-purge.Plo \
+ ./$(DEPDIR)/mail-cache-sync-update.Plo \
+ ./$(DEPDIR)/mail-cache-transaction.Plo \
+ ./$(DEPDIR)/mail-cache.Plo \
+ ./$(DEPDIR)/mail-index-alloc-cache.Plo \
+ ./$(DEPDIR)/mail-index-dummy-view.Plo \
+ ./$(DEPDIR)/mail-index-fsck.Plo \
+ ./$(DEPDIR)/mail-index-lock.Plo \
+ ./$(DEPDIR)/mail-index-map-hdr.Plo \
+ ./$(DEPDIR)/mail-index-map-read.Plo \
+ ./$(DEPDIR)/mail-index-map.Plo \
+ ./$(DEPDIR)/mail-index-modseq.Plo \
+ ./$(DEPDIR)/mail-index-strmap.Plo \
+ ./$(DEPDIR)/mail-index-sync-ext.Plo \
+ ./$(DEPDIR)/mail-index-sync-keywords.Plo \
+ ./$(DEPDIR)/mail-index-sync-update.Plo \
+ ./$(DEPDIR)/mail-index-sync.Plo \
+ ./$(DEPDIR)/mail-index-transaction-export.Plo \
+ ./$(DEPDIR)/mail-index-transaction-finish.Plo \
+ ./$(DEPDIR)/mail-index-transaction-sort-appends.Plo \
+ ./$(DEPDIR)/mail-index-transaction-update.Plo \
+ ./$(DEPDIR)/mail-index-transaction-view.Plo \
+ ./$(DEPDIR)/mail-index-transaction.Plo \
+ ./$(DEPDIR)/mail-index-util.Plo \
+ ./$(DEPDIR)/mail-index-view-sync.Plo \
+ ./$(DEPDIR)/mail-index-view.Plo \
+ ./$(DEPDIR)/mail-index-write.Plo ./$(DEPDIR)/mail-index.Plo \
+ ./$(DEPDIR)/mail-transaction-log-append.Plo \
+ ./$(DEPDIR)/mail-transaction-log-file.Plo \
+ ./$(DEPDIR)/mail-transaction-log-modseq.Plo \
+ ./$(DEPDIR)/mail-transaction-log-view.Plo \
+ ./$(DEPDIR)/mail-transaction-log.Plo \
+ ./$(DEPDIR)/mailbox-log.Plo \
+ ./$(DEPDIR)/test-mail-cache-common.Po \
+ ./$(DEPDIR)/test-mail-cache-fields.Po \
+ ./$(DEPDIR)/test-mail-cache-purge.Po \
+ ./$(DEPDIR)/test-mail-cache.Po \
+ ./$(DEPDIR)/test-mail-index-map.Po \
+ ./$(DEPDIR)/test-mail-index-modseq.Po \
+ ./$(DEPDIR)/test-mail-index-sync-ext.Po \
+ ./$(DEPDIR)/test-mail-index-transaction-finish.Po \
+ ./$(DEPDIR)/test-mail-index-transaction-update.Po \
+ ./$(DEPDIR)/test-mail-index-write.Po \
+ ./$(DEPDIR)/test-mail-index.Po \
+ ./$(DEPDIR)/test-mail-transaction-log-append.Po \
+ ./$(DEPDIR)/test-mail-transaction-log-file.Po \
+ ./$(DEPDIR)/test-mail-transaction-log-view.Po
+am__mv = mv -f
+COMPILE = $(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) \
+ $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS)
+LTCOMPILE = $(LIBTOOL) $(AM_V_lt) --tag=CC $(AM_LIBTOOLFLAGS) \
+ $(LIBTOOLFLAGS) --mode=compile $(CC) $(DEFS) \
+ $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) $(CPPFLAGS) \
+ $(AM_CFLAGS) $(CFLAGS)
+AM_V_CC = $(am__v_CC_@AM_V@)
+am__v_CC_ = $(am__v_CC_@AM_DEFAULT_V@)
+am__v_CC_0 = @echo " CC " $@;
+am__v_CC_1 =
+CCLD = $(CC)
+LINK = $(LIBTOOL) $(AM_V_lt) --tag=CC $(AM_LIBTOOLFLAGS) \
+ $(LIBTOOLFLAGS) --mode=link $(CCLD) $(AM_CFLAGS) $(CFLAGS) \
+ $(AM_LDFLAGS) $(LDFLAGS) -o $@
+AM_V_CCLD = $(am__v_CCLD_@AM_V@)
+am__v_CCLD_ = $(am__v_CCLD_@AM_DEFAULT_V@)
+am__v_CCLD_0 = @echo " CCLD " $@;
+am__v_CCLD_1 =
+SOURCES = $(libindex_la_SOURCES) $(test_mail_cache_SOURCES) \
+ $(test_mail_cache_fields_SOURCES) \
+ $(test_mail_cache_purge_SOURCES) $(test_mail_index_SOURCES) \
+ $(test_mail_index_map_SOURCES) \
+ $(test_mail_index_modseq_SOURCES) \
+ $(test_mail_index_sync_ext_SOURCES) \
+ $(test_mail_index_transaction_finish_SOURCES) \
+ $(test_mail_index_transaction_update_SOURCES) \
+ $(test_mail_index_write_SOURCES) \
+ $(test_mail_transaction_log_append_SOURCES) \
+ $(test_mail_transaction_log_file_SOURCES) \
+ $(test_mail_transaction_log_view_SOURCES)
+DIST_SOURCES = $(libindex_la_SOURCES) $(test_mail_cache_SOURCES) \
+ $(test_mail_cache_fields_SOURCES) \
+ $(test_mail_cache_purge_SOURCES) $(test_mail_index_SOURCES) \
+ $(test_mail_index_map_SOURCES) \
+ $(test_mail_index_modseq_SOURCES) \
+ $(test_mail_index_sync_ext_SOURCES) \
+ $(test_mail_index_transaction_finish_SOURCES) \
+ $(test_mail_index_transaction_update_SOURCES) \
+ $(test_mail_index_write_SOURCES) \
+ $(test_mail_transaction_log_append_SOURCES) \
+ $(test_mail_transaction_log_file_SOURCES) \
+ $(test_mail_transaction_log_view_SOURCES)
+am__can_run_installinfo = \
+ case $$AM_UPDATE_INFO_DIR in \
+ n|no|NO) false;; \
+ *) (install-info --version) >/dev/null 2>&1;; \
+ esac
+am__vpath_adj_setup = srcdirstrip=`echo "$(srcdir)" | sed 's|.|.|g'`;
+am__vpath_adj = case $$p in \
+ $(srcdir)/*) f=`echo "$$p" | sed "s|^$$srcdirstrip/||"`;; \
+ *) f=$$p;; \
+ esac;
+am__strip_dir = f=`echo $$p | sed -e 's|^.*/||'`;
+am__install_max = 40
+am__nobase_strip_setup = \
+ srcdirstrip=`echo "$(srcdir)" | sed 's/[].[^$$\\*|]/\\\\&/g'`
+am__nobase_strip = \
+ for p in $$list; do echo "$$p"; done | sed -e "s|$$srcdirstrip/||"
+am__nobase_list = $(am__nobase_strip_setup); \
+ for p in $$list; do echo "$$p $$p"; done | \
+ sed "s| $$srcdirstrip/| |;"' / .*\//!s/ .*/ ./; s,\( .*\)/[^/]*$$,\1,' | \
+ $(AWK) 'BEGIN { files["."] = "" } { files[$$2] = files[$$2] " " $$1; \
+ if (++n[$$2] == $(am__install_max)) \
+ { print $$2, files[$$2]; n[$$2] = 0; files[$$2] = "" } } \
+ END { for (dir in files) print dir, files[dir] }'
+am__base_list = \
+ sed '$$!N;$$!N;$$!N;$$!N;$$!N;$$!N;$$!N;s/\n/ /g' | \
+ sed '$$!N;$$!N;$$!N;$$!N;s/\n/ /g'
+am__uninstall_files_from_dir = { \
+ test -z "$$files" \
+ || { test ! -d "$$dir" && test ! -f "$$dir" && test ! -r "$$dir"; } \
+ || { echo " ( cd '$$dir' && rm -f" $$files ")"; \
+ $(am__cd) "$$dir" && rm -f $$files; }; \
+ }
+am__installdirs = "$(DESTDIR)$(pkginc_libdir)"
+HEADERS = $(noinst_HEADERS) $(pkginc_lib_HEADERS)
+am__tagged_files = $(HEADERS) $(SOURCES) $(TAGS_FILES) $(LISP)
+# Read a list of newline-separated strings from the standard input,
+# and print each of them once, without duplicates. Input order is
+# *not* preserved.
+am__uniquify_input = $(AWK) '\
+ BEGIN { nonempty = 0; } \
+ { items[$$0] = 1; nonempty = 1; } \
+ END { if (nonempty) { for (i in items) print i; }; } \
+'
+# Make sure the list of sources is unique. This is necessary because,
+# e.g., the same source file might be shared among _SOURCES variables
+# for different programs/libraries.
+am__define_uniq_tagged_files = \
+ list='$(am__tagged_files)'; \
+ unique=`for i in $$list; do \
+ if test -f "$$i"; then echo $$i; else echo $(srcdir)/$$i; fi; \
+ done | $(am__uniquify_input)`
+ETAGS = etags
+CTAGS = ctags
+am__DIST_COMMON = $(srcdir)/Makefile.in $(top_srcdir)/depcomp
+DISTFILES = $(DIST_COMMON) $(DIST_SOURCES) $(TEXINFOS) $(EXTRA_DIST)
+ACLOCAL = @ACLOCAL@
+ACLOCAL_AMFLAGS = @ACLOCAL_AMFLAGS@
+AMTAR = @AMTAR@
+AM_DEFAULT_VERBOSITY = @AM_DEFAULT_VERBOSITY@
+APPARMOR_LIBS = @APPARMOR_LIBS@
+AR = @AR@
+AUTH_CFLAGS = @AUTH_CFLAGS@
+AUTH_LIBS = @AUTH_LIBS@
+AUTOCONF = @AUTOCONF@
+AUTOHEADER = @AUTOHEADER@
+AUTOMAKE = @AUTOMAKE@
+AWK = @AWK@
+BINARY_CFLAGS = @BINARY_CFLAGS@
+BINARY_LDFLAGS = @BINARY_LDFLAGS@
+BISON = @BISON@
+CASSANDRA_CFLAGS = @CASSANDRA_CFLAGS@
+CASSANDRA_LIBS = @CASSANDRA_LIBS@
+CC = @CC@
+CCDEPMODE = @CCDEPMODE@
+CDB_LIBS = @CDB_LIBS@
+CFLAGS = @CFLAGS@
+CLUCENE_CFLAGS = @CLUCENE_CFLAGS@
+CLUCENE_LIBS = @CLUCENE_LIBS@
+COMPRESS_LIBS = @COMPRESS_LIBS@
+CPP = @CPP@
+CPPFLAGS = @CPPFLAGS@
+CRYPT_LIBS = @CRYPT_LIBS@
+CXX = @CXX@
+CXXCPP = @CXXCPP@
+CXXDEPMODE = @CXXDEPMODE@
+CXXFLAGS = @CXXFLAGS@
+CYGPATH_W = @CYGPATH_W@
+DEFS = @DEFS@
+DEPDIR = @DEPDIR@
+DICT_LIBS = @DICT_LIBS@
+DLLIB = @DLLIB@
+DLLTOOL = @DLLTOOL@
+DSYMUTIL = @DSYMUTIL@
+DUMPBIN = @DUMPBIN@
+ECHO_C = @ECHO_C@
+ECHO_N = @ECHO_N@
+ECHO_T = @ECHO_T@
+EGREP = @EGREP@
+EXEEXT = @EXEEXT@
+FGREP = @FGREP@
+FLEX = @FLEX@
+FUZZER_CPPFLAGS = @FUZZER_CPPFLAGS@
+FUZZER_LDFLAGS = @FUZZER_LDFLAGS@
+GREP = @GREP@
+INSTALL = @INSTALL@
+INSTALL_DATA = @INSTALL_DATA@
+INSTALL_PROGRAM = @INSTALL_PROGRAM@
+INSTALL_SCRIPT = @INSTALL_SCRIPT@
+INSTALL_STRIP_PROGRAM = @INSTALL_STRIP_PROGRAM@
+KRB5CONFIG = @KRB5CONFIG@
+KRB5_CFLAGS = @KRB5_CFLAGS@
+KRB5_LIBS = @KRB5_LIBS@
+LD = @LD@
+LDAP_LIBS = @LDAP_LIBS@
+LDFLAGS = @LDFLAGS@
+LD_NO_WHOLE_ARCHIVE = @LD_NO_WHOLE_ARCHIVE@
+LD_WHOLE_ARCHIVE = @LD_WHOLE_ARCHIVE@
+LIBCAP = @LIBCAP@
+LIBDOVECOT = @LIBDOVECOT@
+LIBDOVECOT_COMPRESS = @LIBDOVECOT_COMPRESS@
+LIBDOVECOT_DEPS = @LIBDOVECOT_DEPS@
+LIBDOVECOT_DSYNC = @LIBDOVECOT_DSYNC@
+LIBDOVECOT_LA_LIBS = @LIBDOVECOT_LA_LIBS@
+LIBDOVECOT_LDA = @LIBDOVECOT_LDA@
+LIBDOVECOT_LDAP = @LIBDOVECOT_LDAP@
+LIBDOVECOT_LIBFTS = @LIBDOVECOT_LIBFTS@
+LIBDOVECOT_LIBFTS_DEPS = @LIBDOVECOT_LIBFTS_DEPS@
+LIBDOVECOT_LOGIN = @LIBDOVECOT_LOGIN@
+LIBDOVECOT_LUA = @LIBDOVECOT_LUA@
+LIBDOVECOT_LUA_DEPS = @LIBDOVECOT_LUA_DEPS@
+LIBDOVECOT_SQL = @LIBDOVECOT_SQL@
+LIBDOVECOT_STORAGE = @LIBDOVECOT_STORAGE@
+LIBDOVECOT_STORAGE_DEPS = @LIBDOVECOT_STORAGE_DEPS@
+LIBEXTTEXTCAT_CFLAGS = @LIBEXTTEXTCAT_CFLAGS@
+LIBEXTTEXTCAT_LIBS = @LIBEXTTEXTCAT_LIBS@
+LIBICONV = @LIBICONV@
+LIBICU_CFLAGS = @LIBICU_CFLAGS@
+LIBICU_LIBS = @LIBICU_LIBS@
+LIBOBJS = @LIBOBJS@
+LIBS = @LIBS@
+LIBSODIUM_CFLAGS = @LIBSODIUM_CFLAGS@
+LIBSODIUM_LIBS = @LIBSODIUM_LIBS@
+LIBTIRPC_CFLAGS = @LIBTIRPC_CFLAGS@
+LIBTIRPC_LIBS = @LIBTIRPC_LIBS@
+LIBTOOL = @LIBTOOL@
+LIBUNWIND_CFLAGS = @LIBUNWIND_CFLAGS@
+LIBUNWIND_LIBS = @LIBUNWIND_LIBS@
+LIBWRAP_LIBS = @LIBWRAP_LIBS@
+LINKED_STORAGE_LDADD = @LINKED_STORAGE_LDADD@
+LIPO = @LIPO@
+LN_S = @LN_S@
+LTLIBICONV = @LTLIBICONV@
+LTLIBOBJS = @LTLIBOBJS@
+LT_SYS_LIBRARY_PATH = @LT_SYS_LIBRARY_PATH@
+LUA_CFLAGS = @LUA_CFLAGS@
+LUA_LIBS = @LUA_LIBS@
+MAINT = @MAINT@
+MAKEINFO = @MAKEINFO@
+MANIFEST_TOOL = @MANIFEST_TOOL@
+MKDIR_P = @MKDIR_P@
+MODULE_LIBS = @MODULE_LIBS@
+MODULE_SUFFIX = @MODULE_SUFFIX@
+MYSQL_CFLAGS = @MYSQL_CFLAGS@
+MYSQL_CONFIG = @MYSQL_CONFIG@
+MYSQL_LIBS = @MYSQL_LIBS@
+NM = @NM@
+NMEDIT = @NMEDIT@
+NOPLUGIN_LDFLAGS = @NOPLUGIN_LDFLAGS@
+OBJDUMP = @OBJDUMP@
+OBJEXT = @OBJEXT@
+OTOOL = @OTOOL@
+OTOOL64 = @OTOOL64@
+PACKAGE = @PACKAGE@
+PACKAGE_BUGREPORT = @PACKAGE_BUGREPORT@
+PACKAGE_NAME = @PACKAGE_NAME@
+PACKAGE_STRING = @PACKAGE_STRING@
+PACKAGE_TARNAME = @PACKAGE_TARNAME@
+PACKAGE_URL = @PACKAGE_URL@
+PACKAGE_VERSION = @PACKAGE_VERSION@
+PANDOC = @PANDOC@
+PATH_SEPARATOR = @PATH_SEPARATOR@
+PGSQL_CFLAGS = @PGSQL_CFLAGS@
+PGSQL_LIBS = @PGSQL_LIBS@
+PG_CONFIG = @PG_CONFIG@
+PIE_CFLAGS = @PIE_CFLAGS@
+PIE_LDFLAGS = @PIE_LDFLAGS@
+PKG_CONFIG = @PKG_CONFIG@
+PKG_CONFIG_LIBDIR = @PKG_CONFIG_LIBDIR@
+PKG_CONFIG_PATH = @PKG_CONFIG_PATH@
+QUOTA_LIBS = @QUOTA_LIBS@
+RANLIB = @RANLIB@
+RELRO_LDFLAGS = @RELRO_LDFLAGS@
+RPCGEN = @RPCGEN@
+RUN_TEST = @RUN_TEST@
+SED = @SED@
+SETTING_FILES = @SETTING_FILES@
+SET_MAKE = @SET_MAKE@
+SHELL = @SHELL@
+SQLITE_CFLAGS = @SQLITE_CFLAGS@
+SQLITE_LIBS = @SQLITE_LIBS@
+SQL_CFLAGS = @SQL_CFLAGS@
+SQL_LIBS = @SQL_LIBS@
+SSL_CFLAGS = @SSL_CFLAGS@
+SSL_LIBS = @SSL_LIBS@
+STRIP = @STRIP@
+SYSTEMD_CFLAGS = @SYSTEMD_CFLAGS@
+SYSTEMD_LIBS = @SYSTEMD_LIBS@
+VALGRIND = @VALGRIND@
+VERSION = @VERSION@
+ZSTD_CFLAGS = @ZSTD_CFLAGS@
+ZSTD_LIBS = @ZSTD_LIBS@
+abs_builddir = @abs_builddir@
+abs_srcdir = @abs_srcdir@
+abs_top_builddir = @abs_top_builddir@
+abs_top_srcdir = @abs_top_srcdir@
+ac_ct_AR = @ac_ct_AR@
+ac_ct_CC = @ac_ct_CC@
+ac_ct_CXX = @ac_ct_CXX@
+ac_ct_DUMPBIN = @ac_ct_DUMPBIN@
+am__include = @am__include@
+am__leading_dot = @am__leading_dot@
+am__quote = @am__quote@
+am__tar = @am__tar@
+am__untar = @am__untar@
+bindir = @bindir@
+build = @build@
+build_alias = @build_alias@
+build_cpu = @build_cpu@
+build_os = @build_os@
+build_vendor = @build_vendor@
+builddir = @builddir@
+datadir = @datadir@
+datarootdir = @datarootdir@
+dict_drivers = @dict_drivers@
+docdir = @docdir@
+dvidir = @dvidir@
+exec_prefix = @exec_prefix@
+host = @host@
+host_alias = @host_alias@
+host_cpu = @host_cpu@
+host_os = @host_os@
+host_vendor = @host_vendor@
+htmldir = @htmldir@
+includedir = @includedir@
+infodir = @infodir@
+install_sh = @install_sh@
+libdir = @libdir@
+libexecdir = @libexecdir@
+localedir = @localedir@
+localstatedir = @localstatedir@
+mandir = @mandir@
+mkdir_p = @mkdir_p@
+moduledir = @moduledir@
+oldincludedir = @oldincludedir@
+pdfdir = @pdfdir@
+prefix = @prefix@
+program_transform_name = @program_transform_name@
+psdir = @psdir@
+rundir = @rundir@
+runstatedir = @runstatedir@
+sbindir = @sbindir@
+sharedstatedir = @sharedstatedir@
+sql_drivers = @sql_drivers@
+srcdir = @srcdir@
+ssldir = @ssldir@
+statedir = @statedir@
+sysconfdir = @sysconfdir@
+systemdservicetype = @systemdservicetype@
+systemdsystemunitdir = @systemdsystemunitdir@
+target_alias = @target_alias@
+top_build_prefix = @top_build_prefix@
+top_builddir = @top_builddir@
+top_srcdir = @top_srcdir@
+noinst_LTLIBRARIES = libindex.la
+AM_CPPFLAGS = \
+ -I$(top_srcdir)/src/lib \
+ -I$(top_srcdir)/src/lib-test \
+ -I$(top_srcdir)/src/lib-mail
+
+libindex_la_SOURCES = \
+ mail-cache.c \
+ mail-cache-decisions.c \
+ mail-cache-fields.c \
+ mail-cache-lookup.c \
+ mail-cache-purge.c \
+ mail-cache-transaction.c \
+ mail-cache-sync-update.c \
+ mail-index.c \
+ mail-index-alloc-cache.c \
+ mail-index-dummy-view.c \
+ mail-index-fsck.c \
+ mail-index-lock.c \
+ mail-index-map.c \
+ mail-index-map-hdr.c \
+ mail-index-map-read.c \
+ mail-index-modseq.c \
+ mail-index-transaction.c \
+ mail-index-transaction-export.c \
+ mail-index-transaction-finish.c \
+ mail-index-transaction-sort-appends.c \
+ mail-index-transaction-update.c \
+ mail-index-transaction-view.c \
+ mail-index-strmap.c \
+ mail-index-sync.c \
+ mail-index-sync-ext.c \
+ mail-index-sync-keywords.c \
+ mail-index-sync-update.c \
+ mail-index-util.c \
+ mail-index-view.c \
+ mail-index-view-sync.c \
+ mail-index-write.c \
+ mail-transaction-log.c \
+ mail-transaction-log-append.c \
+ mail-transaction-log-file.c \
+ mail-transaction-log-modseq.c \
+ mail-transaction-log-view.c \
+ mailbox-log.c
+
+headers = \
+ mail-cache.h \
+ mail-cache-private.h \
+ mail-index.h \
+ mail-index-alloc-cache.h \
+ mail-index-modseq.h \
+ mail-index-private.h \
+ mail-index-strmap.h \
+ mail-index-sync-private.h \
+ mail-index-transaction-private.h \
+ mail-index-util.h \
+ mail-index-view-private.h \
+ mail-transaction-log.h \
+ mail-transaction-log-private.h \
+ mail-transaction-log-view-private.h \
+ mailbox-log.h
+
+test_programs = \
+ test-mail-cache \
+ test-mail-cache-fields \
+ test-mail-cache-purge \
+ test-mail-index \
+ test-mail-index-map \
+ test-mail-index-modseq \
+ test-mail-index-sync-ext \
+ test-mail-index-transaction-finish \
+ test-mail-index-transaction-update \
+ test-mail-index-write \
+ test-mail-transaction-log-append \
+ test-mail-transaction-log-file \
+ test-mail-transaction-log-view
+
+test_libs = \
+ mail-index-util.lo \
+ ../lib-test/libtest.la \
+ ../lib/liblib.la
+
+test_deps = $(noinst_LTLIBRARIES) $(test_libs)
+test_mail_cache_SOURCES = test-mail-cache-common.c test-mail-cache.c
+test_mail_cache_LDADD = $(noinst_LTLIBRARIES) $(test_libs)
+test_mail_cache_DEPENDENCIES = $(test_deps)
+test_mail_cache_fields_SOURCES = test-mail-cache-common.c test-mail-cache-fields.c
+test_mail_cache_fields_LDADD = $(noinst_LTLIBRARIES) $(test_libs)
+test_mail_cache_fields_DEPENDENCIES = $(test_deps)
+test_mail_cache_purge_SOURCES = test-mail-cache-common.c test-mail-cache-purge.c
+test_mail_cache_purge_LDADD = $(noinst_LTLIBRARIES) $(test_libs)
+test_mail_cache_purge_DEPENDENCIES = $(test_deps)
+test_mail_index_SOURCES = test-mail-index.c
+test_mail_index_LDADD = $(noinst_LTLIBRARIES) $(test_libs)
+test_mail_index_DEPENDENCIES = $(test_deps)
+test_mail_index_map_SOURCES = test-mail-index-map.c
+test_mail_index_map_LDADD = $(noinst_LTLIBRARIES) $(test_libs)
+test_mail_index_map_DEPENDENCIES = $(test_deps)
+test_mail_index_modseq_SOURCES = test-mail-index-modseq.c
+test_mail_index_modseq_LDADD = $(noinst_LTLIBRARIES) $(test_libs)
+test_mail_index_modseq_DEPENDENCIES = $(test_deps)
+test_mail_index_sync_ext_SOURCES = test-mail-index-sync-ext.c
+test_mail_index_sync_ext_LDADD = $(noinst_LTLIBRARIES) $(test_libs)
+test_mail_index_sync_ext_DEPENDENCIES = $(test_deps)
+test_mail_index_transaction_finish_SOURCES = test-mail-index-transaction-finish.c
+test_mail_index_transaction_finish_LDADD = mail-index-transaction-finish.lo $(test_libs)
+test_mail_index_transaction_finish_DEPENDENCIES = $(test_deps)
+test_mail_index_transaction_update_SOURCES = test-mail-index-transaction-update.c
+test_mail_index_transaction_update_LDADD = mail-index-transaction-update.lo $(test_libs)
+test_mail_index_transaction_update_DEPENDENCIES = $(test_deps)
+test_mail_index_write_SOURCES = test-mail-index-write.c
+test_mail_index_write_LDADD = mail-index-write.lo $(test_libs)
+test_mail_index_write_DEPENDENCIES = $(test_deps)
+test_mail_transaction_log_append_SOURCES = test-mail-transaction-log-append.c
+test_mail_transaction_log_append_LDADD = mail-transaction-log-append.lo $(test_libs)
+test_mail_transaction_log_append_DEPENDENCIES = $(test_deps)
+test_mail_transaction_log_file_SOURCES = test-mail-transaction-log-file.c
+test_mail_transaction_log_file_LDADD = $(noinst_LTLIBRARIES) $(test_libs)
+test_mail_transaction_log_file_DEPENDENCIES = $(test_deps)
+test_mail_transaction_log_view_SOURCES = test-mail-transaction-log-view.c
+test_mail_transaction_log_view_LDADD = mail-transaction-log-view.lo $(test_libs)
+test_mail_transaction_log_view_DEPENDENCIES = $(test_deps)
+pkginc_libdir = $(pkgincludedir)
+pkginc_lib_HEADERS = $(headers)
+noinst_HEADERS = \
+ test-mail-cache.h \
+ test-mail-index.h
+
+all: all-am
+
+.SUFFIXES:
+.SUFFIXES: .c .lo .o .obj
+$(srcdir)/Makefile.in: @MAINTAINER_MODE_TRUE@ $(srcdir)/Makefile.am $(am__configure_deps)
+ @for dep in $?; do \
+ case '$(am__configure_deps)' in \
+ *$$dep*) \
+ ( cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh ) \
+ && { if test -f $@; then exit 0; else break; fi; }; \
+ exit 1;; \
+ esac; \
+ done; \
+ echo ' cd $(top_srcdir) && $(AUTOMAKE) --foreign src/lib-index/Makefile'; \
+ $(am__cd) $(top_srcdir) && \
+ $(AUTOMAKE) --foreign src/lib-index/Makefile
+Makefile: $(srcdir)/Makefile.in $(top_builddir)/config.status
+ @case '$?' in \
+ *config.status*) \
+ cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh;; \
+ *) \
+ echo ' cd $(top_builddir) && $(SHELL) ./config.status $(subdir)/$@ $(am__maybe_remake_depfiles)'; \
+ cd $(top_builddir) && $(SHELL) ./config.status $(subdir)/$@ $(am__maybe_remake_depfiles);; \
+ esac;
+
+$(top_builddir)/config.status: $(top_srcdir)/configure $(CONFIG_STATUS_DEPENDENCIES)
+ cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh
+
+$(top_srcdir)/configure: @MAINTAINER_MODE_TRUE@ $(am__configure_deps)
+ cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh
+$(ACLOCAL_M4): @MAINTAINER_MODE_TRUE@ $(am__aclocal_m4_deps)
+ cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh
+$(am__aclocal_m4_deps):
+
+clean-noinstPROGRAMS:
+ @list='$(noinst_PROGRAMS)'; test -n "$$list" || exit 0; \
+ echo " rm -f" $$list; \
+ rm -f $$list || exit $$?; \
+ test -n "$(EXEEXT)" || exit 0; \
+ list=`for p in $$list; do echo "$$p"; done | sed 's/$(EXEEXT)$$//'`; \
+ echo " rm -f" $$list; \
+ rm -f $$list
+
+clean-noinstLTLIBRARIES:
+ -test -z "$(noinst_LTLIBRARIES)" || rm -f $(noinst_LTLIBRARIES)
+ @list='$(noinst_LTLIBRARIES)'; \
+ locs=`for p in $$list; do echo $$p; done | \
+ sed 's|^[^/]*$$|.|; s|/[^/]*$$||; s|$$|/so_locations|' | \
+ sort -u`; \
+ test -z "$$locs" || { \
+ echo rm -f $${locs}; \
+ rm -f $${locs}; \
+ }
+
+libindex.la: $(libindex_la_OBJECTS) $(libindex_la_DEPENDENCIES) $(EXTRA_libindex_la_DEPENDENCIES)
+ $(AM_V_CCLD)$(LINK) $(libindex_la_OBJECTS) $(libindex_la_LIBADD) $(LIBS)
+
+test-mail-cache$(EXEEXT): $(test_mail_cache_OBJECTS) $(test_mail_cache_DEPENDENCIES) $(EXTRA_test_mail_cache_DEPENDENCIES)
+ @rm -f test-mail-cache$(EXEEXT)
+ $(AM_V_CCLD)$(LINK) $(test_mail_cache_OBJECTS) $(test_mail_cache_LDADD) $(LIBS)
+
+test-mail-cache-fields$(EXEEXT): $(test_mail_cache_fields_OBJECTS) $(test_mail_cache_fields_DEPENDENCIES) $(EXTRA_test_mail_cache_fields_DEPENDENCIES)
+ @rm -f test-mail-cache-fields$(EXEEXT)
+ $(AM_V_CCLD)$(LINK) $(test_mail_cache_fields_OBJECTS) $(test_mail_cache_fields_LDADD) $(LIBS)
+
+test-mail-cache-purge$(EXEEXT): $(test_mail_cache_purge_OBJECTS) $(test_mail_cache_purge_DEPENDENCIES) $(EXTRA_test_mail_cache_purge_DEPENDENCIES)
+ @rm -f test-mail-cache-purge$(EXEEXT)
+ $(AM_V_CCLD)$(LINK) $(test_mail_cache_purge_OBJECTS) $(test_mail_cache_purge_LDADD) $(LIBS)
+
+test-mail-index$(EXEEXT): $(test_mail_index_OBJECTS) $(test_mail_index_DEPENDENCIES) $(EXTRA_test_mail_index_DEPENDENCIES)
+ @rm -f test-mail-index$(EXEEXT)
+ $(AM_V_CCLD)$(LINK) $(test_mail_index_OBJECTS) $(test_mail_index_LDADD) $(LIBS)
+
+test-mail-index-map$(EXEEXT): $(test_mail_index_map_OBJECTS) $(test_mail_index_map_DEPENDENCIES) $(EXTRA_test_mail_index_map_DEPENDENCIES)
+ @rm -f test-mail-index-map$(EXEEXT)
+ $(AM_V_CCLD)$(LINK) $(test_mail_index_map_OBJECTS) $(test_mail_index_map_LDADD) $(LIBS)
+
+test-mail-index-modseq$(EXEEXT): $(test_mail_index_modseq_OBJECTS) $(test_mail_index_modseq_DEPENDENCIES) $(EXTRA_test_mail_index_modseq_DEPENDENCIES)
+ @rm -f test-mail-index-modseq$(EXEEXT)
+ $(AM_V_CCLD)$(LINK) $(test_mail_index_modseq_OBJECTS) $(test_mail_index_modseq_LDADD) $(LIBS)
+
+test-mail-index-sync-ext$(EXEEXT): $(test_mail_index_sync_ext_OBJECTS) $(test_mail_index_sync_ext_DEPENDENCIES) $(EXTRA_test_mail_index_sync_ext_DEPENDENCIES)
+ @rm -f test-mail-index-sync-ext$(EXEEXT)
+ $(AM_V_CCLD)$(LINK) $(test_mail_index_sync_ext_OBJECTS) $(test_mail_index_sync_ext_LDADD) $(LIBS)
+
+test-mail-index-transaction-finish$(EXEEXT): $(test_mail_index_transaction_finish_OBJECTS) $(test_mail_index_transaction_finish_DEPENDENCIES) $(EXTRA_test_mail_index_transaction_finish_DEPENDENCIES)
+ @rm -f test-mail-index-transaction-finish$(EXEEXT)
+ $(AM_V_CCLD)$(LINK) $(test_mail_index_transaction_finish_OBJECTS) $(test_mail_index_transaction_finish_LDADD) $(LIBS)
+
+test-mail-index-transaction-update$(EXEEXT): $(test_mail_index_transaction_update_OBJECTS) $(test_mail_index_transaction_update_DEPENDENCIES) $(EXTRA_test_mail_index_transaction_update_DEPENDENCIES)
+ @rm -f test-mail-index-transaction-update$(EXEEXT)
+ $(AM_V_CCLD)$(LINK) $(test_mail_index_transaction_update_OBJECTS) $(test_mail_index_transaction_update_LDADD) $(LIBS)
+
+test-mail-index-write$(EXEEXT): $(test_mail_index_write_OBJECTS) $(test_mail_index_write_DEPENDENCIES) $(EXTRA_test_mail_index_write_DEPENDENCIES)
+ @rm -f test-mail-index-write$(EXEEXT)
+ $(AM_V_CCLD)$(LINK) $(test_mail_index_write_OBJECTS) $(test_mail_index_write_LDADD) $(LIBS)
+
+test-mail-transaction-log-append$(EXEEXT): $(test_mail_transaction_log_append_OBJECTS) $(test_mail_transaction_log_append_DEPENDENCIES) $(EXTRA_test_mail_transaction_log_append_DEPENDENCIES)
+ @rm -f test-mail-transaction-log-append$(EXEEXT)
+ $(AM_V_CCLD)$(LINK) $(test_mail_transaction_log_append_OBJECTS) $(test_mail_transaction_log_append_LDADD) $(LIBS)
+
+test-mail-transaction-log-file$(EXEEXT): $(test_mail_transaction_log_file_OBJECTS) $(test_mail_transaction_log_file_DEPENDENCIES) $(EXTRA_test_mail_transaction_log_file_DEPENDENCIES)
+ @rm -f test-mail-transaction-log-file$(EXEEXT)
+ $(AM_V_CCLD)$(LINK) $(test_mail_transaction_log_file_OBJECTS) $(test_mail_transaction_log_file_LDADD) $(LIBS)
+
+test-mail-transaction-log-view$(EXEEXT): $(test_mail_transaction_log_view_OBJECTS) $(test_mail_transaction_log_view_DEPENDENCIES) $(EXTRA_test_mail_transaction_log_view_DEPENDENCIES)
+ @rm -f test-mail-transaction-log-view$(EXEEXT)
+ $(AM_V_CCLD)$(LINK) $(test_mail_transaction_log_view_OBJECTS) $(test_mail_transaction_log_view_LDADD) $(LIBS)
+
+mostlyclean-compile:
+ -rm -f *.$(OBJEXT)
+
+distclean-compile:
+ -rm -f *.tab.c
+
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/mail-cache-decisions.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/mail-cache-fields.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/mail-cache-lookup.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/mail-cache-purge.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/mail-cache-sync-update.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/mail-cache-transaction.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/mail-cache.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/mail-index-alloc-cache.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/mail-index-dummy-view.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/mail-index-fsck.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/mail-index-lock.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/mail-index-map-hdr.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/mail-index-map-read.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/mail-index-map.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/mail-index-modseq.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/mail-index-strmap.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/mail-index-sync-ext.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/mail-index-sync-keywords.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/mail-index-sync-update.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/mail-index-sync.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/mail-index-transaction-export.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/mail-index-transaction-finish.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/mail-index-transaction-sort-appends.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/mail-index-transaction-update.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/mail-index-transaction-view.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/mail-index-transaction.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/mail-index-util.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/mail-index-view-sync.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/mail-index-view.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/mail-index-write.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/mail-index.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/mail-transaction-log-append.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/mail-transaction-log-file.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/mail-transaction-log-modseq.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/mail-transaction-log-view.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/mail-transaction-log.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/mailbox-log.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/test-mail-cache-common.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/test-mail-cache-fields.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/test-mail-cache-purge.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/test-mail-cache.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/test-mail-index-map.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/test-mail-index-modseq.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/test-mail-index-sync-ext.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/test-mail-index-transaction-finish.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/test-mail-index-transaction-update.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/test-mail-index-write.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/test-mail-index.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/test-mail-transaction-log-append.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/test-mail-transaction-log-file.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/test-mail-transaction-log-view.Po@am__quote@ # am--include-marker
+
+$(am__depfiles_remade):
+ @$(MKDIR_P) $(@D)
+ @echo '# dummy' >$@-t && $(am__mv) $@-t $@
+
+am--depfiles: $(am__depfiles_remade)
+
+.c.o:
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(COMPILE) -MT $@ -MD -MP -MF $(DEPDIR)/$*.Tpo -c -o $@ $<
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/$*.Tpo $(DEPDIR)/$*.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='$<' object='$@' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(COMPILE) -c -o $@ $<
+
+.c.obj:
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(COMPILE) -MT $@ -MD -MP -MF $(DEPDIR)/$*.Tpo -c -o $@ `$(CYGPATH_W) '$<'`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/$*.Tpo $(DEPDIR)/$*.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='$<' object='$@' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(COMPILE) -c -o $@ `$(CYGPATH_W) '$<'`
+
+.c.lo:
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(LTCOMPILE) -MT $@ -MD -MP -MF $(DEPDIR)/$*.Tpo -c -o $@ $<
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/$*.Tpo $(DEPDIR)/$*.Plo
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='$<' object='$@' libtool=yes @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(LTCOMPILE) -c -o $@ $<
+
+mostlyclean-libtool:
+ -rm -f *.lo
+
+clean-libtool:
+ -rm -rf .libs _libs
+install-pkginc_libHEADERS: $(pkginc_lib_HEADERS)
+ @$(NORMAL_INSTALL)
+ @list='$(pkginc_lib_HEADERS)'; test -n "$(pkginc_libdir)" || list=; \
+ if test -n "$$list"; then \
+ echo " $(MKDIR_P) '$(DESTDIR)$(pkginc_libdir)'"; \
+ $(MKDIR_P) "$(DESTDIR)$(pkginc_libdir)" || exit 1; \
+ fi; \
+ for p in $$list; do \
+ if test -f "$$p"; then d=; else d="$(srcdir)/"; fi; \
+ echo "$$d$$p"; \
+ done | $(am__base_list) | \
+ while read files; do \
+ echo " $(INSTALL_HEADER) $$files '$(DESTDIR)$(pkginc_libdir)'"; \
+ $(INSTALL_HEADER) $$files "$(DESTDIR)$(pkginc_libdir)" || exit $$?; \
+ done
+
+uninstall-pkginc_libHEADERS:
+ @$(NORMAL_UNINSTALL)
+ @list='$(pkginc_lib_HEADERS)'; test -n "$(pkginc_libdir)" || list=; \
+ files=`for p in $$list; do echo $$p; done | sed -e 's|^.*/||'`; \
+ dir='$(DESTDIR)$(pkginc_libdir)'; $(am__uninstall_files_from_dir)
+
+ID: $(am__tagged_files)
+ $(am__define_uniq_tagged_files); mkid -fID $$unique
+tags: tags-am
+TAGS: tags
+
+tags-am: $(TAGS_DEPENDENCIES) $(am__tagged_files)
+ set x; \
+ here=`pwd`; \
+ $(am__define_uniq_tagged_files); \
+ shift; \
+ if test -z "$(ETAGS_ARGS)$$*$$unique"; then :; else \
+ test -n "$$unique" || unique=$$empty_fix; \
+ if test $$# -gt 0; then \
+ $(ETAGS) $(ETAGSFLAGS) $(AM_ETAGSFLAGS) $(ETAGS_ARGS) \
+ "$$@" $$unique; \
+ else \
+ $(ETAGS) $(ETAGSFLAGS) $(AM_ETAGSFLAGS) $(ETAGS_ARGS) \
+ $$unique; \
+ fi; \
+ fi
+ctags: ctags-am
+
+CTAGS: ctags
+ctags-am: $(TAGS_DEPENDENCIES) $(am__tagged_files)
+ $(am__define_uniq_tagged_files); \
+ test -z "$(CTAGS_ARGS)$$unique" \
+ || $(CTAGS) $(CTAGSFLAGS) $(AM_CTAGSFLAGS) $(CTAGS_ARGS) \
+ $$unique
+
+GTAGS:
+ here=`$(am__cd) $(top_builddir) && pwd` \
+ && $(am__cd) $(top_srcdir) \
+ && gtags -i $(GTAGS_ARGS) "$$here"
+cscopelist: cscopelist-am
+
+cscopelist-am: $(am__tagged_files)
+ list='$(am__tagged_files)'; \
+ case "$(srcdir)" in \
+ [\\/]* | ?:[\\/]*) sdir="$(srcdir)" ;; \
+ *) sdir=$(subdir)/$(srcdir) ;; \
+ esac; \
+ for i in $$list; do \
+ if test -f "$$i"; then \
+ echo "$(subdir)/$$i"; \
+ else \
+ echo "$$sdir/$$i"; \
+ fi; \
+ done >> $(top_builddir)/cscope.files
+
+distclean-tags:
+ -rm -f TAGS ID GTAGS GRTAGS GSYMS GPATH tags
+
+distdir: $(BUILT_SOURCES)
+ $(MAKE) $(AM_MAKEFLAGS) distdir-am
+
+distdir-am: $(DISTFILES)
+ @srcdirstrip=`echo "$(srcdir)" | sed 's/[].[^$$\\*]/\\\\&/g'`; \
+ topsrcdirstrip=`echo "$(top_srcdir)" | sed 's/[].[^$$\\*]/\\\\&/g'`; \
+ list='$(DISTFILES)'; \
+ dist_files=`for file in $$list; do echo $$file; done | \
+ sed -e "s|^$$srcdirstrip/||;t" \
+ -e "s|^$$topsrcdirstrip/|$(top_builddir)/|;t"`; \
+ case $$dist_files in \
+ */*) $(MKDIR_P) `echo "$$dist_files" | \
+ sed '/\//!d;s|^|$(distdir)/|;s,/[^/]*$$,,' | \
+ sort -u` ;; \
+ esac; \
+ for file in $$dist_files; do \
+ if test -f $$file || test -d $$file; then d=.; else d=$(srcdir); fi; \
+ if test -d $$d/$$file; then \
+ dir=`echo "/$$file" | sed -e 's,/[^/]*$$,,'`; \
+ if test -d "$(distdir)/$$file"; then \
+ find "$(distdir)/$$file" -type d ! -perm -700 -exec chmod u+rwx {} \;; \
+ fi; \
+ if test -d $(srcdir)/$$file && test $$d != $(srcdir); then \
+ cp -fpR $(srcdir)/$$file "$(distdir)$$dir" || exit 1; \
+ find "$(distdir)/$$file" -type d ! -perm -700 -exec chmod u+rwx {} \;; \
+ fi; \
+ cp -fpR $$d/$$file "$(distdir)$$dir" || exit 1; \
+ else \
+ test -f "$(distdir)/$$file" \
+ || cp -p $$d/$$file "$(distdir)/$$file" \
+ || exit 1; \
+ fi; \
+ done
+check-am: all-am
+ $(MAKE) $(AM_MAKEFLAGS) check-local
+check: check-am
+all-am: Makefile $(PROGRAMS) $(LTLIBRARIES) $(HEADERS)
+installdirs:
+ for dir in "$(DESTDIR)$(pkginc_libdir)"; do \
+ test -z "$$dir" || $(MKDIR_P) "$$dir"; \
+ done
+install: install-am
+install-exec: install-exec-am
+install-data: install-data-am
+uninstall: uninstall-am
+
+install-am: all-am
+ @$(MAKE) $(AM_MAKEFLAGS) install-exec-am install-data-am
+
+installcheck: installcheck-am
+install-strip:
+ if test -z '$(STRIP)'; then \
+ $(MAKE) $(AM_MAKEFLAGS) INSTALL_PROGRAM="$(INSTALL_STRIP_PROGRAM)" \
+ install_sh_PROGRAM="$(INSTALL_STRIP_PROGRAM)" INSTALL_STRIP_FLAG=-s \
+ install; \
+ else \
+ $(MAKE) $(AM_MAKEFLAGS) INSTALL_PROGRAM="$(INSTALL_STRIP_PROGRAM)" \
+ install_sh_PROGRAM="$(INSTALL_STRIP_PROGRAM)" INSTALL_STRIP_FLAG=-s \
+ "INSTALL_PROGRAM_ENV=STRIPPROG='$(STRIP)'" install; \
+ fi
+mostlyclean-generic:
+
+clean-generic:
+
+distclean-generic:
+ -test -z "$(CONFIG_CLEAN_FILES)" || rm -f $(CONFIG_CLEAN_FILES)
+ -test . = "$(srcdir)" || test -z "$(CONFIG_CLEAN_VPATH_FILES)" || rm -f $(CONFIG_CLEAN_VPATH_FILES)
+
+maintainer-clean-generic:
+ @echo "This command is intended for maintainers to use"
+ @echo "it deletes files that may require special tools to rebuild."
+clean: clean-am
+
+clean-am: clean-generic clean-libtool clean-noinstLTLIBRARIES \
+ clean-noinstPROGRAMS mostlyclean-am
+
+distclean: distclean-am
+ -rm -f ./$(DEPDIR)/mail-cache-decisions.Plo
+ -rm -f ./$(DEPDIR)/mail-cache-fields.Plo
+ -rm -f ./$(DEPDIR)/mail-cache-lookup.Plo
+ -rm -f ./$(DEPDIR)/mail-cache-purge.Plo
+ -rm -f ./$(DEPDIR)/mail-cache-sync-update.Plo
+ -rm -f ./$(DEPDIR)/mail-cache-transaction.Plo
+ -rm -f ./$(DEPDIR)/mail-cache.Plo
+ -rm -f ./$(DEPDIR)/mail-index-alloc-cache.Plo
+ -rm -f ./$(DEPDIR)/mail-index-dummy-view.Plo
+ -rm -f ./$(DEPDIR)/mail-index-fsck.Plo
+ -rm -f ./$(DEPDIR)/mail-index-lock.Plo
+ -rm -f ./$(DEPDIR)/mail-index-map-hdr.Plo
+ -rm -f ./$(DEPDIR)/mail-index-map-read.Plo
+ -rm -f ./$(DEPDIR)/mail-index-map.Plo
+ -rm -f ./$(DEPDIR)/mail-index-modseq.Plo
+ -rm -f ./$(DEPDIR)/mail-index-strmap.Plo
+ -rm -f ./$(DEPDIR)/mail-index-sync-ext.Plo
+ -rm -f ./$(DEPDIR)/mail-index-sync-keywords.Plo
+ -rm -f ./$(DEPDIR)/mail-index-sync-update.Plo
+ -rm -f ./$(DEPDIR)/mail-index-sync.Plo
+ -rm -f ./$(DEPDIR)/mail-index-transaction-export.Plo
+ -rm -f ./$(DEPDIR)/mail-index-transaction-finish.Plo
+ -rm -f ./$(DEPDIR)/mail-index-transaction-sort-appends.Plo
+ -rm -f ./$(DEPDIR)/mail-index-transaction-update.Plo
+ -rm -f ./$(DEPDIR)/mail-index-transaction-view.Plo
+ -rm -f ./$(DEPDIR)/mail-index-transaction.Plo
+ -rm -f ./$(DEPDIR)/mail-index-util.Plo
+ -rm -f ./$(DEPDIR)/mail-index-view-sync.Plo
+ -rm -f ./$(DEPDIR)/mail-index-view.Plo
+ -rm -f ./$(DEPDIR)/mail-index-write.Plo
+ -rm -f ./$(DEPDIR)/mail-index.Plo
+ -rm -f ./$(DEPDIR)/mail-transaction-log-append.Plo
+ -rm -f ./$(DEPDIR)/mail-transaction-log-file.Plo
+ -rm -f ./$(DEPDIR)/mail-transaction-log-modseq.Plo
+ -rm -f ./$(DEPDIR)/mail-transaction-log-view.Plo
+ -rm -f ./$(DEPDIR)/mail-transaction-log.Plo
+ -rm -f ./$(DEPDIR)/mailbox-log.Plo
+ -rm -f ./$(DEPDIR)/test-mail-cache-common.Po
+ -rm -f ./$(DEPDIR)/test-mail-cache-fields.Po
+ -rm -f ./$(DEPDIR)/test-mail-cache-purge.Po
+ -rm -f ./$(DEPDIR)/test-mail-cache.Po
+ -rm -f ./$(DEPDIR)/test-mail-index-map.Po
+ -rm -f ./$(DEPDIR)/test-mail-index-modseq.Po
+ -rm -f ./$(DEPDIR)/test-mail-index-sync-ext.Po
+ -rm -f ./$(DEPDIR)/test-mail-index-transaction-finish.Po
+ -rm -f ./$(DEPDIR)/test-mail-index-transaction-update.Po
+ -rm -f ./$(DEPDIR)/test-mail-index-write.Po
+ -rm -f ./$(DEPDIR)/test-mail-index.Po
+ -rm -f ./$(DEPDIR)/test-mail-transaction-log-append.Po
+ -rm -f ./$(DEPDIR)/test-mail-transaction-log-file.Po
+ -rm -f ./$(DEPDIR)/test-mail-transaction-log-view.Po
+ -rm -f Makefile
+distclean-am: clean-am distclean-compile distclean-generic \
+ distclean-tags
+
+dvi: dvi-am
+
+dvi-am:
+
+html: html-am
+
+html-am:
+
+info: info-am
+
+info-am:
+
+install-data-am: install-pkginc_libHEADERS
+
+install-dvi: install-dvi-am
+
+install-dvi-am:
+
+install-exec-am:
+
+install-html: install-html-am
+
+install-html-am:
+
+install-info: install-info-am
+
+install-info-am:
+
+install-man:
+
+install-pdf: install-pdf-am
+
+install-pdf-am:
+
+install-ps: install-ps-am
+
+install-ps-am:
+
+installcheck-am:
+
+maintainer-clean: maintainer-clean-am
+ -rm -f ./$(DEPDIR)/mail-cache-decisions.Plo
+ -rm -f ./$(DEPDIR)/mail-cache-fields.Plo
+ -rm -f ./$(DEPDIR)/mail-cache-lookup.Plo
+ -rm -f ./$(DEPDIR)/mail-cache-purge.Plo
+ -rm -f ./$(DEPDIR)/mail-cache-sync-update.Plo
+ -rm -f ./$(DEPDIR)/mail-cache-transaction.Plo
+ -rm -f ./$(DEPDIR)/mail-cache.Plo
+ -rm -f ./$(DEPDIR)/mail-index-alloc-cache.Plo
+ -rm -f ./$(DEPDIR)/mail-index-dummy-view.Plo
+ -rm -f ./$(DEPDIR)/mail-index-fsck.Plo
+ -rm -f ./$(DEPDIR)/mail-index-lock.Plo
+ -rm -f ./$(DEPDIR)/mail-index-map-hdr.Plo
+ -rm -f ./$(DEPDIR)/mail-index-map-read.Plo
+ -rm -f ./$(DEPDIR)/mail-index-map.Plo
+ -rm -f ./$(DEPDIR)/mail-index-modseq.Plo
+ -rm -f ./$(DEPDIR)/mail-index-strmap.Plo
+ -rm -f ./$(DEPDIR)/mail-index-sync-ext.Plo
+ -rm -f ./$(DEPDIR)/mail-index-sync-keywords.Plo
+ -rm -f ./$(DEPDIR)/mail-index-sync-update.Plo
+ -rm -f ./$(DEPDIR)/mail-index-sync.Plo
+ -rm -f ./$(DEPDIR)/mail-index-transaction-export.Plo
+ -rm -f ./$(DEPDIR)/mail-index-transaction-finish.Plo
+ -rm -f ./$(DEPDIR)/mail-index-transaction-sort-appends.Plo
+ -rm -f ./$(DEPDIR)/mail-index-transaction-update.Plo
+ -rm -f ./$(DEPDIR)/mail-index-transaction-view.Plo
+ -rm -f ./$(DEPDIR)/mail-index-transaction.Plo
+ -rm -f ./$(DEPDIR)/mail-index-util.Plo
+ -rm -f ./$(DEPDIR)/mail-index-view-sync.Plo
+ -rm -f ./$(DEPDIR)/mail-index-view.Plo
+ -rm -f ./$(DEPDIR)/mail-index-write.Plo
+ -rm -f ./$(DEPDIR)/mail-index.Plo
+ -rm -f ./$(DEPDIR)/mail-transaction-log-append.Plo
+ -rm -f ./$(DEPDIR)/mail-transaction-log-file.Plo
+ -rm -f ./$(DEPDIR)/mail-transaction-log-modseq.Plo
+ -rm -f ./$(DEPDIR)/mail-transaction-log-view.Plo
+ -rm -f ./$(DEPDIR)/mail-transaction-log.Plo
+ -rm -f ./$(DEPDIR)/mailbox-log.Plo
+ -rm -f ./$(DEPDIR)/test-mail-cache-common.Po
+ -rm -f ./$(DEPDIR)/test-mail-cache-fields.Po
+ -rm -f ./$(DEPDIR)/test-mail-cache-purge.Po
+ -rm -f ./$(DEPDIR)/test-mail-cache.Po
+ -rm -f ./$(DEPDIR)/test-mail-index-map.Po
+ -rm -f ./$(DEPDIR)/test-mail-index-modseq.Po
+ -rm -f ./$(DEPDIR)/test-mail-index-sync-ext.Po
+ -rm -f ./$(DEPDIR)/test-mail-index-transaction-finish.Po
+ -rm -f ./$(DEPDIR)/test-mail-index-transaction-update.Po
+ -rm -f ./$(DEPDIR)/test-mail-index-write.Po
+ -rm -f ./$(DEPDIR)/test-mail-index.Po
+ -rm -f ./$(DEPDIR)/test-mail-transaction-log-append.Po
+ -rm -f ./$(DEPDIR)/test-mail-transaction-log-file.Po
+ -rm -f ./$(DEPDIR)/test-mail-transaction-log-view.Po
+ -rm -f Makefile
+maintainer-clean-am: distclean-am maintainer-clean-generic
+
+mostlyclean: mostlyclean-am
+
+mostlyclean-am: mostlyclean-compile mostlyclean-generic \
+ mostlyclean-libtool
+
+pdf: pdf-am
+
+pdf-am:
+
+ps: ps-am
+
+ps-am:
+
+uninstall-am: uninstall-pkginc_libHEADERS
+
+.MAKE: check-am install-am install-strip
+
+.PHONY: CTAGS GTAGS TAGS all all-am am--depfiles check check-am \
+ check-local clean clean-generic clean-libtool \
+ clean-noinstLTLIBRARIES clean-noinstPROGRAMS cscopelist-am \
+ ctags ctags-am distclean distclean-compile distclean-generic \
+ distclean-libtool distclean-tags distdir dvi dvi-am html \
+ html-am info info-am install install-am install-data \
+ install-data-am install-dvi install-dvi-am install-exec \
+ install-exec-am install-html install-html-am install-info \
+ install-info-am install-man install-pdf install-pdf-am \
+ install-pkginc_libHEADERS install-ps install-ps-am \
+ install-strip installcheck installcheck-am installdirs \
+ maintainer-clean maintainer-clean-generic mostlyclean \
+ mostlyclean-compile mostlyclean-generic mostlyclean-libtool \
+ pdf pdf-am ps ps-am tags tags-am uninstall uninstall-am \
+ uninstall-pkginc_libHEADERS
+
+.PRECIOUS: Makefile
+
+
+check-local:
+ for bin in $(test_programs); do \
+ if ! $(RUN_TEST) ./$$bin; then exit 1; fi; \
+ done
+
+# Tell versions [3.59,3.63) of GNU make to not export all variables.
+# Otherwise a system limit (for SysV at least) may be exceeded.
+.NOEXPORT:
diff --git a/src/lib-index/mail-cache-decisions.c b/src/lib-index/mail-cache-decisions.c
new file mode 100644
index 0000000..93435d0
--- /dev/null
+++ b/src/lib-index/mail-cache-decisions.c
@@ -0,0 +1,238 @@
+/* Copyright (c) 2004-2018 Dovecot authors, see the included COPYING file */
+
+/*
+ IMAP clients can work in many different ways. There are basically 2
+ types:
+
+ 1. Online clients that ask for the same information multiple times (e.g.
+ webmails, Pine)
+
+ 2. Offline clients that usually download first some of the interesting
+ message headers and only after that the message bodies (possibly
+ automatically, or possibly only when the user opens the mail). Most
+ non-webmail IMAP clients behave like this.
+
+ Cache file is extremely helpful with the type 1 clients. The first time
+ that client requests message headers or some other metadata they're
+ stored into the cache file. The second time they ask for the same
+ information Dovecot can now get it quickly from the cache file instead
+ of opening the message and parsing the headers.
+
+ For type 2 clients the cache file is also somewhat helpful if client
+ fetches any initial metadata. Some of the information is helpful in any
+ case, for example it's required to know the message's virtual size when
+ downloading the message with IMAP. Without the virtual size being in cache
+ Dovecot first has to read the whole message first to calculate it, which
+ increases CPU usage.
+
+ Only the specified fields that client(s) have asked for earlier are
+ stored into cache file. This allows Dovecot to be adaptive to different
+ clients' needs and still not waste disk space (and cause extra disk
+ I/O!) for fields that client never needs.
+
+ Dovecot can cache fields either permanently or temporarily. Temporarily
+ cached fields are dropped from the cache file after about a week.
+ Dovecot uses two rules to determine when data should be cached
+ permanently instead of temporarily:
+
+ 1. Client accessed messages in non-sequential order within this session.
+ This most likely means it doesn't have a local cache.
+
+ 2. Client accessed a message older than one week.
+
+ These rules might not always work optimally, so Dovecot also re-evaluates
+ the caching decisions once in a while:
+
+ - When caching decision is YES (permanently cache the field), the field's
+ last_used is updated only when the caching decision has been verified to
+ be correct.
+
+ - When caching decision is TEMP, the last_used is updated whenever the field
+ is accessed.
+
+ - When last_used becomes 30 days old (or unaccessed_field_drop_secs) a
+ YES caching decision is changed to TEMP.
+
+ - When last_used becomes 60 days old (or 2*unaccessed_field_drop_secs) a
+ TEMP caching decision is changed to NO.
+*/
+
+#include "lib.h"
+#include "ioloop.h"
+#include "mail-cache-private.h"
+
+const char *mail_cache_decision_to_string(enum mail_cache_decision_type dec)
+{
+ switch (dec & ENUM_NEGATE(MAIL_CACHE_DECISION_FORCED)) {
+ case MAIL_CACHE_DECISION_NO:
+ return "no";
+ case MAIL_CACHE_DECISION_TEMP:
+ return "temp";
+ case MAIL_CACHE_DECISION_YES:
+ return "yes";
+ }
+ i_unreached();
+}
+
+struct event_passthrough *
+mail_cache_decision_changed_event(struct mail_cache *cache, struct event *event,
+ unsigned int field)
+{
+ return event_create_passthrough(event)->
+ set_name("mail_cache_decision_changed")->
+ add_str("field", cache->fields[field].field.name)->
+ add_int("last_used", cache->fields[field].field.last_used);
+}
+
+static void
+mail_cache_update_last_used(struct mail_cache *cache, unsigned int field)
+{
+ cache->fields[field].field.last_used = (uint32_t)ioloop_time;
+ if (cache->field_file_map[field] != (uint32_t)-1)
+ cache->field_header_write_pending = TRUE;
+}
+
+void mail_cache_decision_state_update(struct mail_cache_view *view,
+ uint32_t seq, unsigned int field)
+{
+ struct mail_cache *cache = view->cache;
+ enum mail_cache_decision_type dec;
+ const struct mail_index_header *hdr;
+ uint32_t uid;
+
+ i_assert(field < cache->fields_count);
+
+ if (view->no_decision_updates)
+ return;
+
+ dec = cache->fields[field].field.decision;
+ if (dec == (MAIL_CACHE_DECISION_NO | MAIL_CACHE_DECISION_FORCED)) {
+ /* don't update last_used */
+ return;
+ }
+
+ /* update last_used about once a day */
+ bool last_used_need_update =
+ ioloop_time - cache->fields[field].field.last_used > 3600*24;
+
+ if (dec == MAIL_CACHE_DECISION_NO ||
+ (dec & MAIL_CACHE_DECISION_FORCED) != 0) {
+ /* a) forced decision
+ b) not cached, mail_cache_decision_add() will handle this */
+ if (last_used_need_update)
+ mail_cache_update_last_used(cache, field);
+ return;
+ }
+ if (dec == MAIL_CACHE_DECISION_YES) {
+ if (!last_used_need_update)
+ return;
+ /* update last_used only when we can confirm that the YES
+ decision is still correct. */
+ } else {
+ /* see if we want to change decision from TEMP to YES */
+ i_assert(dec == MAIL_CACHE_DECISION_TEMP);
+ if (last_used_need_update)
+ mail_cache_update_last_used(cache, field);
+ }
+
+ mail_index_lookup_uid(view->view, seq, &uid);
+ hdr = mail_index_get_header(view->view);
+
+ if (uid >= cache->fields[field].uid_highwater &&
+ uid >= hdr->day_first_uid[7]) {
+ cache->fields[field].uid_highwater = uid;
+ } else if (dec == MAIL_CACHE_DECISION_YES) {
+ /* Confirmed that we still want to preserve YES as cache
+ decision. We can update last_used now. */
+ i_assert(last_used_need_update);
+ mail_cache_update_last_used(cache, field);
+ } else {
+ /* a) nonordered access within this session. if client doesn't
+ request messages in growing order, we assume it doesn't
+ have a permanent local cache.
+ b) accessing message older than one week. assume it's a
+ client with no local cache. if it was just a new client
+ generating the local cache for the first time, we'll
+ drop back to TEMP within few months. */
+ i_assert(dec == MAIL_CACHE_DECISION_TEMP);
+ cache->fields[field].field.decision = MAIL_CACHE_DECISION_YES;
+ cache->fields[field].decision_dirty = TRUE;
+ cache->field_header_write_pending = TRUE;
+
+ const char *reason = uid < hdr->day_first_uid[7] ?
+ "old_mail" : "unordered_access";
+ struct event_passthrough *e =
+ mail_cache_decision_changed_event(
+ view->cache, view->cache->event, field)->
+ add_str("reason", reason)->
+ add_int("uid", uid)->
+ add_str("old_decision", "temp")->
+ add_str("new_decision", "yes");
+ e_debug(e->event(), "Changing field %s decision temp -> yes (uid=%u)",
+ cache->fields[field].field.name, uid);
+ }
+}
+
+void mail_cache_decision_add(struct mail_cache_view *view, uint32_t seq,
+ unsigned int field)
+{
+ struct mail_cache *cache = view->cache;
+ struct mail_cache_field_private *priv;
+ uint32_t uid;
+
+ i_assert(field < cache->fields_count);
+
+ if (view->no_decision_updates)
+ return;
+
+ priv = &cache->fields[field];
+ if (priv->field.decision != MAIL_CACHE_DECISION_NO &&
+ priv->field.last_used != 0) {
+ /* a) forced decision
+ b) we're already caching it, so it just wasn't in cache */
+ return;
+ }
+
+ /* field used the first time */
+ if (priv->field.decision == MAIL_CACHE_DECISION_NO)
+ priv->field.decision = MAIL_CACHE_DECISION_TEMP;
+ priv->field.last_used = ioloop_time;
+ priv->decision_dirty = TRUE;
+ cache->field_header_write_pending = TRUE;
+
+ mail_index_lookup_uid(view->view, seq, &uid);
+ priv->uid_highwater = uid;
+
+ const char *new_decision =
+ mail_cache_decision_to_string(priv->field.decision);
+ struct event_passthrough *e =
+ mail_cache_decision_changed_event(cache, cache->event, field)->
+ add_str("reason", "add")->
+ add_int("uid", uid)->
+ add_str("old_decision", "no")->
+ add_str("new_decision", new_decision);
+ e_debug(e->event(), "Adding field %s to cache for the first time (uid=%u)",
+ priv->field.name, uid);
+}
+
+int mail_cache_decisions_copy(struct mail_cache *src, struct mail_cache *dst)
+{
+ if (mail_cache_open_and_verify(src) < 0)
+ return -1;
+ if (MAIL_CACHE_IS_UNUSABLE(src))
+ return 0; /* no caching decisions */
+
+ unsigned int count = 0;
+ struct mail_cache_field *fields =
+ mail_cache_register_get_list(src, pool_datastack_create(), &count);
+ i_assert(fields != NULL || count == 0);
+ if (count > 0)
+ mail_cache_register_fields(dst, fields, count);
+
+ /* Destination cache isn't expected to exist yet, so use purging
+ to create it. Setting field_header_write_pending also guarantees
+ that the fields are updated even if the cache was already created
+ and no purging was done. */
+ dst->field_header_write_pending = TRUE;
+ return mail_cache_purge(dst, 0, "copy cache decisions");
+}
diff --git a/src/lib-index/mail-cache-fields.c b/src/lib-index/mail-cache-fields.c
new file mode 100644
index 0000000..429e0d2
--- /dev/null
+++ b/src/lib-index/mail-cache-fields.c
@@ -0,0 +1,660 @@
+/* Copyright (c) 2004-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "ioloop.h"
+#include "buffer.h"
+#include "hash.h"
+#include "file-cache.h"
+#include "read-full.h"
+#include "write-full.h"
+#include "mmap-util.h"
+#include "mail-cache-private.h"
+
+#include <stddef.h>
+
+#define CACHE_FIELD_IS_NEWLY_WANTED(cache, field_idx) \
+ ((cache)->field_file_map[field_idx] == (uint32_t)-1 && \
+ (cache)->fields[field_idx].used)
+
+static bool field_has_fixed_size(enum mail_cache_field_type type)
+{
+ switch (type) {
+ case MAIL_CACHE_FIELD_FIXED_SIZE:
+ case MAIL_CACHE_FIELD_BITMASK:
+ return TRUE;
+ case MAIL_CACHE_FIELD_VARIABLE_SIZE:
+ case MAIL_CACHE_FIELD_STRING:
+ case MAIL_CACHE_FIELD_HEADER:
+ return FALSE;
+
+ case MAIL_CACHE_FIELD_COUNT:
+ break;
+ }
+
+ i_unreached();
+ return FALSE;
+}
+
+static bool field_decision_is_valid(enum mail_cache_decision_type type)
+{
+ switch (type & ENUM_NEGATE(MAIL_CACHE_DECISION_FORCED)) {
+ case MAIL_CACHE_DECISION_NO:
+ case MAIL_CACHE_DECISION_TEMP:
+ case MAIL_CACHE_DECISION_YES:
+ return TRUE;
+ default:
+ return FALSE;
+ }
+}
+
+static int field_type_verify(struct mail_cache *cache, unsigned int idx,
+ enum mail_cache_field_type type, unsigned int size)
+{
+ const struct mail_cache_field *field = &cache->fields[idx].field;
+
+ if (field->type != type) {
+ mail_cache_set_corrupted(cache,
+ "registered field %s type changed", field->name);
+ return -1;
+ }
+ if (field->field_size != size && field_has_fixed_size(type)) {
+ mail_cache_set_corrupted(cache,
+ "registered field %s size changed", field->name);
+ return -1;
+ }
+ return 0;
+}
+
+static void
+mail_cache_field_update(struct mail_cache *cache,
+ const struct mail_cache_field *newfield)
+{
+ struct mail_cache_field_private *orig;
+ bool initial_registering;
+
+ i_assert(newfield->type < MAIL_CACHE_FIELD_COUNT);
+
+ /* are we still doing the initial cache field registering for
+ internal fields and for mail_*cache_fields settings? */
+ initial_registering = cache->file_fields_count == 0;
+
+ orig = &cache->fields[newfield->idx];
+ if ((newfield->decision & MAIL_CACHE_DECISION_FORCED) != 0 ||
+ ((orig->field.decision & MAIL_CACHE_DECISION_FORCED) == 0 &&
+ newfield->decision > orig->field.decision)) {
+ orig->field.decision = newfield->decision;
+ if (!initial_registering)
+ orig->decision_dirty = TRUE;
+ }
+ if (orig->field.last_used < newfield->last_used) {
+ orig->field.last_used = newfield->last_used;
+ if (!initial_registering)
+ orig->decision_dirty = TRUE;
+ }
+ if (orig->decision_dirty)
+ cache->field_header_write_pending = TRUE;
+
+ (void)field_type_verify(cache, newfield->idx,
+ newfield->type, newfield->field_size);
+}
+
+void mail_cache_register_fields(struct mail_cache *cache,
+ struct mail_cache_field *fields,
+ unsigned int fields_count)
+{
+ char *name;
+ void *value;
+ unsigned int new_idx;
+ unsigned int i, j, registered_count;
+
+ new_idx = cache->fields_count;
+ for (i = 0; i < fields_count; i++) {
+ if (hash_table_lookup_full(cache->field_name_hash,
+ fields[i].name, &name, &value)) {
+ fields[i].idx = POINTER_CAST_TO(value, unsigned int);
+ mail_cache_field_update(cache, &fields[i]);
+ continue;
+ }
+
+ /* check if the same header is being registered in the
+ same field array */
+ for (j = 0; j < i; j++) {
+ if (strcasecmp(fields[i].name, fields[j].name) == 0) {
+ fields[i].idx = fields[j].idx;
+ break;
+ }
+ }
+
+ if (j == i)
+ fields[i].idx = new_idx++;
+ }
+
+ if (new_idx == cache->fields_count)
+ return;
+
+ /* @UNSAFE */
+ cache->fields = i_realloc_type(cache->fields,
+ struct mail_cache_field_private,
+ cache->fields_count, new_idx);
+ cache->field_file_map =
+ i_realloc_type(cache->field_file_map, uint32_t,
+ cache->fields_count, new_idx);
+
+ registered_count = cache->fields_count;
+ for (i = 0; i < fields_count; i++) {
+ unsigned int idx = fields[i].idx;
+
+ if (idx < registered_count)
+ continue;
+
+ /* new index - save it */
+ name = p_strdup(cache->field_pool, fields[i].name);
+ cache->fields[idx].field = fields[i];
+ cache->fields[idx].field.name = name;
+ cache->fields[idx].field.last_used = fields[i].last_used;
+ cache->field_file_map[idx] = (uint32_t)-1;
+
+ if (!field_has_fixed_size(cache->fields[idx].field.type))
+ cache->fields[idx].field.field_size = UINT_MAX;
+
+ hash_table_insert(cache->field_name_hash, name,
+ POINTER_CAST(idx));
+ registered_count++;
+ }
+ i_assert(registered_count == new_idx);
+ cache->fields_count = new_idx;
+}
+
+unsigned int
+mail_cache_register_lookup(struct mail_cache *cache, const char *name)
+{
+ char *key;
+ void *value;
+
+ if (hash_table_lookup_full(cache->field_name_hash, name, &key, &value))
+ return POINTER_CAST_TO(value, unsigned int);
+ else
+ return UINT_MAX;
+}
+
+const struct mail_cache_field *
+mail_cache_register_get_field(struct mail_cache *cache, unsigned int field_idx)
+{
+ i_assert(field_idx < cache->fields_count);
+
+ return &cache->fields[field_idx].field;
+}
+
+struct mail_cache_field *
+mail_cache_register_get_list(struct mail_cache *cache, pool_t pool,
+ unsigned int *count_r)
+{
+ struct mail_cache_field *list;
+ unsigned int i;
+
+ if (!cache->opened)
+ (void)mail_cache_open_and_verify(cache);
+
+ list = cache->fields_count == 0 ? NULL :
+ p_new(pool, struct mail_cache_field, cache->fields_count);
+ for (i = 0; i < cache->fields_count; i++) {
+ list[i] = cache->fields[i].field;
+ list[i].name = p_strdup(pool, list[i].name);
+ }
+
+ *count_r = cache->fields_count;
+ return list;
+}
+
+static int
+mail_cache_header_fields_get_offset(struct mail_cache *cache,
+ uint32_t *offset_r,
+ const struct mail_cache_header_fields **field_hdr_r)
+{
+ const struct mail_cache_header_fields *field_hdr;
+ struct mail_cache_header_fields tmp_field_hdr;
+ const void *data;
+ uint32_t offset = 0, next_offset, field_hdr_size;
+ unsigned int next_count = 0;
+ int ret;
+
+ if (MAIL_CACHE_IS_UNUSABLE(cache)) {
+ *offset_r = 0;
+ if (field_hdr_r != NULL)
+ *field_hdr_r = NULL;
+ return 0;
+ }
+
+ /* find the latest header */
+ offset = 0;
+ next_offset = cache->last_field_header_offset != 0 ?
+ cache->last_field_header_offset :
+ mail_index_offset_to_uint32(cache->hdr->field_header_offset);
+ while (next_offset != 0) {
+ if (next_offset == offset) {
+ mail_cache_set_corrupted(cache,
+ "next_offset in field header loops");
+ return -1;
+ }
+ /* In Dovecot v2.2+ we don't try to use any holes,
+ so next_offset must always be larger than current offset.
+ also makes it easier to guarantee there aren't any loops
+ (which we don't bother doing for old files) */
+ if (next_offset < offset && cache->hdr->minor_version != 0) {
+ mail_cache_set_corrupted(cache,
+ "next_offset in field header decreases");
+ return -1;
+ }
+ offset = next_offset;
+
+ if (cache->mmap_base != NULL || cache->map_with_read) {
+ ret = mail_cache_map(cache, offset, sizeof(*field_hdr),
+ &data);
+ if (ret <= 0) {
+ if (ret < 0)
+ return -1;
+ mail_cache_set_corrupted(cache,
+ "header field next_offset points outside file");
+ return -1;
+ }
+ field_hdr = data;
+ } else {
+ /* if we need to follow multiple offsets to get to
+ the last one, it's faster to just pread() the file
+ instead of going through cache */
+ ret = pread_full(cache->fd, &tmp_field_hdr,
+ sizeof(tmp_field_hdr), offset);
+ if (ret < 0) {
+ mail_cache_set_syscall_error(cache, "pread()");
+ return -1;
+ }
+ if (ret == 0) {
+ mail_cache_set_corrupted(cache,
+ "header field next_offset points outside file");
+ return -1;
+ }
+ field_hdr = &tmp_field_hdr;
+ }
+
+ next_offset =
+ mail_index_offset_to_uint32(field_hdr->next_offset);
+ next_count++;
+ }
+
+ if (offset == 0) {
+ mail_cache_set_corrupted(cache, "missing header fields");
+ return -1;
+ }
+ cache->last_field_header_offset = offset;
+
+ if (next_count > cache->index->optimization_set.cache.purge_header_continue_count) {
+ mail_cache_purge_later(cache, t_strdup_printf(
+ "Too many continued headers (%u)", next_count));
+ }
+
+ if (field_hdr_r != NULL) {
+ /* detect corrupted size later */
+ field_hdr_size = I_MAX(field_hdr->size, sizeof(*field_hdr));
+ if (cache->file_cache != NULL) {
+ /* invalidate the cache fields area to make sure we
+ get the latest cache decisions/last_used fields */
+ file_cache_invalidate(cache->file_cache, offset,
+ field_hdr_size);
+ }
+ if (cache->read_buf != NULL)
+ buffer_set_used_size(cache->read_buf, 0);
+ ret = mail_cache_map(cache, offset, field_hdr_size, &data);
+ if (ret < 0)
+ return -1;
+ if (ret == 0) {
+ mail_cache_set_corrupted(cache,
+ "header field size outside file");
+ return -1;
+ }
+ *field_hdr_r = data;
+ }
+ *offset_r = offset;
+ return 0;
+}
+
+int mail_cache_header_fields_read(struct mail_cache *cache)
+{
+ const struct mail_cache_header_fields *field_hdr;
+ struct mail_cache_field field;
+ const uint32_t *last_used, *sizes;
+ const uint8_t *types, *decisions;
+ const char *p, *names, *end;
+ char *orig_key;
+ void *orig_value;
+ unsigned int fidx, new_fields_count;
+ struct mail_cache_purge_drop_ctx drop_ctx;
+ uint32_t offset, i;
+
+ if (mail_cache_header_fields_get_offset(cache, &offset, &field_hdr) < 0)
+ return -1;
+
+ if (offset == 0) {
+ /* no fields - the file is empty */
+ return 0;
+ }
+
+ /* check the fixed size of the header. name[] has to be checked
+ separately */
+ if (field_hdr->fields_count > INT_MAX / MAIL_CACHE_FIELD_NAMES(1) ||
+ field_hdr->size < MAIL_CACHE_FIELD_NAMES(field_hdr->fields_count)) {
+ mail_cache_set_corrupted(cache, "invalid field header size");
+ return -1;
+ }
+
+ new_fields_count = field_hdr->fields_count;
+ if (new_fields_count != 0) {
+ cache->file_field_map =
+ i_realloc_type(cache->file_field_map, unsigned int,
+ cache->file_fields_count, new_fields_count);
+ } else {
+ i_free_and_null(cache->file_field_map);
+ }
+ cache->file_fields_count = new_fields_count;
+
+ last_used = CONST_PTR_OFFSET(field_hdr, MAIL_CACHE_FIELD_LAST_USED());
+ sizes = CONST_PTR_OFFSET(field_hdr,
+ MAIL_CACHE_FIELD_SIZE(field_hdr->fields_count));
+ types = CONST_PTR_OFFSET(field_hdr,
+ MAIL_CACHE_FIELD_TYPE(field_hdr->fields_count));
+ decisions = CONST_PTR_OFFSET(field_hdr,
+ MAIL_CACHE_FIELD_DECISION(field_hdr->fields_count));
+ names = CONST_PTR_OFFSET(field_hdr,
+ MAIL_CACHE_FIELD_NAMES(field_hdr->fields_count));
+ end = CONST_PTR_OFFSET(field_hdr, field_hdr->size);
+ i_assert(names <= end);
+
+ /* clear the old mapping */
+ for (i = 0; i < cache->fields_count; i++)
+ cache->field_file_map[i] = (uint32_t)-1;
+
+ mail_cache_purge_drop_init(cache, &cache->index->map->hdr, &drop_ctx);
+ i_zero(&field);
+ for (i = 0; i < field_hdr->fields_count; i++) {
+ for (p = names; p != end && *p != '\0'; p++) ;
+ if (p == end || *names == '\0') {
+ mail_cache_set_corrupted(cache,
+ "field header names corrupted");
+ return -1;
+ }
+
+ if (types[i] > MAIL_CACHE_FIELD_COUNT) {
+ mail_cache_set_corrupted(cache, "field type corrupted");
+ return -1;
+ }
+ if (!field_decision_is_valid(decisions[i])) {
+ mail_cache_set_corrupted(cache,
+ "field decision type corrupted");
+ return -1;
+ }
+
+ /* ignore any forced-flags in the file */
+ enum mail_cache_decision_type file_dec =
+ decisions[i] & ENUM_NEGATE(MAIL_CACHE_DECISION_FORCED);
+
+ if (hash_table_lookup_full(cache->field_name_hash, names,
+ &orig_key, &orig_value)) {
+ /* already exists, see if decision can be updated */
+ fidx = POINTER_CAST_TO(orig_value, unsigned int);
+ enum mail_cache_decision_type cur_dec =
+ cache->fields[fidx].field.decision;
+ if ((cur_dec & MAIL_CACHE_DECISION_FORCED) != 0) {
+ /* Forced decision. If the decision has
+ changed, update the fields in the file. */
+ if ((cur_dec & ENUM_NEGATE(MAIL_CACHE_DECISION_FORCED)) != file_dec)
+ cache->field_header_write_pending = TRUE;
+ } else if (cache->fields[fidx].decision_dirty) {
+ /* Decisions have recently been updated
+ internally. Don't change them. */
+ } else {
+ /* Use the decision from the cache file. */
+ cache->fields[fidx].field.decision = file_dec;
+ }
+ if (field_type_verify(cache, fidx,
+ types[i], sizes[i]) < 0)
+ return -1;
+ } else {
+ /* field is currently unknown, so just use whatever
+ exists in the file. */
+ field.name = names;
+ field.type = types[i];
+ field.field_size = sizes[i];
+ field.decision = file_dec;
+ mail_cache_register_fields(cache, &field, 1);
+ fidx = field.idx;
+ }
+ if (cache->field_file_map[fidx] != (uint32_t)-1) {
+ mail_cache_set_corrupted(cache,
+ "Duplicated field in header: %s", names);
+ return -1;
+ }
+ cache->fields[fidx].used = TRUE;
+
+ cache->field_file_map[fidx] = i;
+ cache->file_field_map[i] = fidx;
+
+ /* Update last_used if it's newer than ours. Note that the
+ last_used may have been overwritten while we were reading
+ this cache header. In theory this can mean that the
+ last_used field is only half-updated and contains garbage.
+ This practically won't matter, since the worst that can
+ happen is that we trigger a purge earlier than necessary.
+ The purging re-reads the last_used while cache is locked and
+ correctly figures out whether to drop the field. */
+ if ((time_t)last_used[i] > cache->fields[fidx].field.last_used)
+ cache->fields[fidx].field.last_used = last_used[i];
+
+ switch (mail_cache_purge_drop_test(&drop_ctx, fidx)) {
+ case MAIL_CACHE_PURGE_DROP_DECISION_NONE:
+ break;
+ case MAIL_CACHE_PURGE_DROP_DECISION_DROP:
+ mail_cache_purge_later(cache, t_strdup_printf(
+ "Drop old field %s (last_used=%"PRIdTIME_T")",
+ cache->fields[fidx].field.name,
+ cache->fields[fidx].field.last_used));
+ break;
+ case MAIL_CACHE_PURGE_DROP_DECISION_TO_TEMP:
+ /* This cache decision change can cause the field to be
+ dropped for old mails, so do it via purging. */
+ mail_cache_purge_later(cache, t_strdup_printf(
+ "Change cache decision to temp for old field %s "
+ "(last_used=%"PRIdTIME_T")",
+ cache->fields[fidx].field.name,
+ cache->fields[fidx].field.last_used));
+ break;
+ }
+
+ names = p + 1;
+ }
+ return 0;
+}
+
+static void copy_to_buf(struct mail_cache *cache, buffer_t *dest, bool add_new,
+ size_t offset, size_t size)
+{
+ const void *data;
+ unsigned int i, field;
+
+ /* copy the existing fields */
+ for (i = 0; i < cache->file_fields_count; i++) {
+ field = cache->file_field_map[i];
+ data = CONST_PTR_OFFSET(&cache->fields[field], offset);
+ buffer_append(dest, data, size);
+ }
+ if (!add_new)
+ return;
+
+ /* copy newly wanted fields */
+ for (i = 0; i < cache->fields_count; i++) {
+ if (CACHE_FIELD_IS_NEWLY_WANTED(cache, i)) {
+ data = CONST_PTR_OFFSET(&cache->fields[i], offset);
+ buffer_append(dest, data, size);
+ }
+ }
+}
+
+static void copy_to_buf_byte(struct mail_cache *cache, buffer_t *dest,
+ bool add_new, size_t offset)
+{
+ const int *data;
+ unsigned int i, field;
+ uint8_t byte;
+
+ /* copy the existing fields */
+ for (i = 0; i < cache->file_fields_count; i++) {
+ field = cache->file_field_map[i];
+ data = CONST_PTR_OFFSET(&cache->fields[field], offset);
+ byte = (uint8_t)*data;
+ buffer_append(dest, &byte, 1);
+ }
+ if (!add_new)
+ return;
+
+ /* copy newly wanted fields */
+ for (i = 0; i < cache->fields_count; i++) {
+ if (CACHE_FIELD_IS_NEWLY_WANTED(cache, i)) {
+ data = CONST_PTR_OFFSET(&cache->fields[i], offset);
+ byte = (uint8_t)*data;
+ buffer_append(dest, &byte, 1);
+ }
+ }
+}
+
+static void
+copy_to_buf_last_used(struct mail_cache *cache, buffer_t *dest, bool add_new)
+{
+ size_t offset = offsetof(struct mail_cache_field, last_used);
+#if defined(WORDS_BIGENDIAN) && SIZEOF_VOID_P == 8
+ /* 64bit time_t with big endian CPUs: copy the last 32 bits instead of
+ the first 32 bits (that are always 0). The 32 bits are enough until
+ year 2106, so we're not in a hurry to use 64 bits on disk. */
+ offset += sizeof(uint32_t);
+#endif
+ copy_to_buf(cache, dest, add_new, offset, sizeof(uint32_t));
+}
+
+static int mail_cache_header_fields_update_locked(struct mail_cache *cache)
+{
+ buffer_t *buffer;
+ uint32_t i, offset, dec_offset;
+ int ret = 0;
+
+ if (mail_cache_header_fields_read(cache) < 0 ||
+ mail_cache_header_fields_get_offset(cache, &offset, NULL) < 0)
+ return -1;
+
+ buffer = t_buffer_create(256);
+
+ copy_to_buf_last_used(cache, buffer, FALSE);
+ ret = mail_cache_write(cache, buffer->data, buffer->used,
+ offset + MAIL_CACHE_FIELD_LAST_USED());
+ if (ret == 0) {
+ buffer_set_used_size(buffer, 0);
+ copy_to_buf_byte(cache, buffer, FALSE,
+ offsetof(struct mail_cache_field, decision));
+
+ dec_offset = offset +
+ MAIL_CACHE_FIELD_DECISION(cache->file_fields_count);
+ ret = mail_cache_write(cache, buffer->data, buffer->used,
+ dec_offset);
+ if (ret == 0) {
+ for (i = 0; i < cache->file_fields_count; i++)
+ cache->fields[i].decision_dirty = FALSE;
+ }
+ }
+
+ if (ret == 0)
+ cache->field_header_write_pending = FALSE;
+ return ret;
+}
+
+int mail_cache_header_fields_update(struct mail_cache *cache)
+{
+ int ret;
+
+ if (cache->locked) {
+ T_BEGIN {
+ ret = mail_cache_header_fields_update_locked(cache);
+ } T_END;
+ return ret;
+ }
+
+ if (mail_cache_lock(cache) <= 0)
+ return -1;
+
+ T_BEGIN {
+ ret = mail_cache_header_fields_update_locked(cache);
+ } T_END;
+ i_assert(!cache->hdr_modified);
+ mail_cache_unlock(cache);
+ return ret;
+}
+
+void mail_cache_header_fields_get(struct mail_cache *cache, buffer_t *dest)
+{
+ struct mail_cache_header_fields hdr;
+ unsigned int field;
+ const char *name;
+ uint32_t i;
+
+ i_zero(&hdr);
+ hdr.fields_count = cache->file_fields_count;
+ for (i = 0; i < cache->fields_count; i++) {
+ if (CACHE_FIELD_IS_NEWLY_WANTED(cache, i))
+ hdr.fields_count++;
+ }
+ buffer_append(dest, &hdr, sizeof(hdr));
+
+ /* we have to keep the field order for the existing fields. */
+ copy_to_buf_last_used(cache, dest, TRUE);
+ copy_to_buf(cache, dest, TRUE,
+ offsetof(struct mail_cache_field, field_size),
+ sizeof(uint32_t));
+ copy_to_buf_byte(cache, dest, TRUE,
+ offsetof(struct mail_cache_field, type));
+ copy_to_buf_byte(cache, dest, TRUE,
+ offsetof(struct mail_cache_field, decision));
+
+ i_assert(dest->used == sizeof(hdr) +
+ (sizeof(uint32_t)*2 + 2) * hdr.fields_count);
+
+ /* add existing fields' names */
+ for (i = 0; i < cache->file_fields_count; i++) {
+ field = cache->file_field_map[i];
+ name = cache->fields[field].field.name;
+ buffer_append(dest, name, strlen(name)+1);
+ }
+ /* add newly wanted fields' names */
+ for (i = 0; i < cache->fields_count; i++) {
+ if (CACHE_FIELD_IS_NEWLY_WANTED(cache, i)) {
+ name = cache->fields[i].field.name;
+ buffer_append(dest, name, strlen(name)+1);
+ }
+ }
+
+ hdr.size = dest->used;
+ buffer_write(dest, 0, &hdr, sizeof(hdr));
+
+ if ((hdr.size & 3) != 0)
+ buffer_append_zero(dest, 4 - (hdr.size & 3));
+}
+
+int mail_cache_header_fields_get_next_offset(struct mail_cache *cache,
+ uint32_t *offset_r)
+{
+ if (mail_cache_header_fields_get_offset(cache, offset_r, NULL) < 0)
+ return -1;
+
+ if (*offset_r == 0) {
+ *offset_r = offsetof(struct mail_cache_header,
+ field_header_offset);
+ } else {
+ *offset_r += offsetof(struct mail_cache_header_fields,
+ next_offset);
+ }
+ return 0;
+}
diff --git a/src/lib-index/mail-cache-lookup.c b/src/lib-index/mail-cache-lookup.c
new file mode 100644
index 0000000..e57b40b
--- /dev/null
+++ b/src/lib-index/mail-cache-lookup.c
@@ -0,0 +1,694 @@
+/* Copyright (c) 2003-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "buffer.h"
+#include "str.h"
+#include "mail-cache-private.h"
+
+
+#define CACHE_PREFETCH IO_BLOCK_SIZE
+
+int mail_cache_get_record(struct mail_cache *cache, uint32_t offset,
+ const struct mail_cache_record **rec_r)
+{
+ const struct mail_cache_record *rec;
+ const void *data;
+ int ret;
+
+ i_assert(offset != 0);
+
+ if (offset % sizeof(uint32_t) != 0) {
+ /* records are always 32-bit aligned */
+ mail_cache_set_corrupted(cache, "invalid record offset");
+ return -1;
+ }
+
+ /* we don't know yet how large the record is, so just guess */
+ if (mail_cache_map(cache, offset, sizeof(*rec) + CACHE_PREFETCH,
+ &data) < 0)
+ return -1;
+
+ if (offset + sizeof(*rec) > cache->mmap_length) {
+ mail_cache_set_corrupted(cache, "record points outside file");
+ return -1;
+ }
+ rec = data;
+
+ if (rec->size < sizeof(*rec)) {
+ mail_cache_set_corrupted(cache, "invalid record size");
+ return -1;
+ }
+ if (rec->size > CACHE_PREFETCH) {
+ /* larger than we guessed. map the rest of the record. */
+ if ((ret = mail_cache_map(cache, offset, rec->size, &data)) < 0)
+ return -1;
+ if (ret == 0) {
+ mail_cache_set_corrupted(cache, "record points outside file");
+ return -1;
+ }
+ rec = data;
+ }
+
+ *rec_r = rec;
+ return 0;
+}
+
+uint32_t mail_cache_lookup_cur_offset(struct mail_index_view *view,
+ uint32_t seq, uint32_t *reset_id_r)
+{
+ struct mail_cache *cache = mail_index_view_get_index(view)->cache;
+ struct mail_index_map *map;
+ const void *data;
+ uint32_t offset;
+
+ mail_index_lookup_ext_full(view, seq, cache->ext_id, &map, &data, NULL);
+ if (data == NULL) {
+ /* no cache offsets */
+ return 0;
+ }
+ offset = *((const uint32_t *)data);
+ if (offset == 0)
+ return 0;
+
+ if (!mail_index_ext_get_reset_id(view, map, cache->ext_id, reset_id_r))
+ i_unreached();
+ return offset;
+}
+
+static int
+mail_cache_lookup_offset(struct mail_cache *cache, struct mail_index_view *view,
+ uint32_t seq, uint32_t *offset_r)
+{
+ uint32_t offset, reset_id, reset_id2;
+ int ret;
+
+ offset = mail_cache_lookup_cur_offset(view, seq, &reset_id);
+ if (offset == 0)
+ return 0;
+
+ while (cache->hdr->file_seq != reset_id) {
+ /* reset_it doesn't match - sync the index/cache */
+ if ((ret = mail_cache_sync_reset_id(cache)) <= 0)
+ return ret;
+
+ /* lookup again after syncing */
+ offset = mail_cache_lookup_cur_offset(view, seq, &reset_id2);
+ if (offset == 0)
+ return 0;
+ if (cache->hdr->file_seq == reset_id2)
+ break; /* match - all good */
+ if (reset_id == reset_id2) {
+ /* reset_id didn't change after sync. This means it's
+ pointing to an old already deleted cache file. */
+ return 0;
+ }
+ /* reset_id changed - try again */
+ reset_id = reset_id2;
+ }
+
+ *offset_r = offset;
+ return 1;
+}
+
+bool mail_cache_track_loops(struct mail_cache_loop_track *loop_track,
+ uoff_t offset, uoff_t size)
+{
+ i_assert(offset != 0);
+ i_assert(size != 0);
+
+ /* looping happens only in rare error conditions, so it's enough if we
+ just catch it eventually. we do this by checking if we've seen
+ more record data than possible in the accessed file area. */
+ if (loop_track->size_sum == 0) {
+ /* first call */
+ loop_track->min_offset = offset;
+ loop_track->max_offset = offset + size;
+ } else {
+ if (loop_track->min_offset > offset)
+ loop_track->min_offset = offset;
+ if (loop_track->max_offset < offset + size)
+ loop_track->max_offset = offset + size;
+ }
+
+ loop_track->size_sum += size;
+ return loop_track->size_sum >
+ (loop_track->max_offset - loop_track->min_offset);
+}
+
+void mail_cache_lookup_iter_init(struct mail_cache_view *view, uint32_t seq,
+ struct mail_cache_lookup_iterate_ctx *ctx_r)
+{
+ struct mail_cache_lookup_iterate_ctx *ctx = ctx_r;
+ int ret;
+
+ if (!view->cache->opened)
+ (void)mail_cache_open_and_verify(view->cache);
+
+ i_zero(ctx);
+ ctx->view = view;
+ ctx->seq = seq;
+
+ if (!MAIL_CACHE_IS_UNUSABLE(view->cache)) {
+ /* look up the first offset */
+ ret = mail_cache_lookup_offset(view->cache, view->view, seq,
+ &ctx->offset);
+ if (ret <= 0) {
+ ctx->stop = TRUE;
+ ctx->failed = ret < 0;
+ }
+ }
+ ctx->remap_counter = view->cache->remap_counter;
+
+ i_zero(&view->loop_track);
+}
+
+static bool
+mail_cache_lookup_iter_transaction(struct mail_cache_lookup_iterate_ctx *ctx)
+{
+ ctx->rec = mail_cache_transaction_lookup_rec(ctx->view->transaction,
+ ctx->seq,
+ &ctx->trans_next_idx);
+ if (ctx->rec == NULL)
+ return FALSE;
+
+ ctx->inmemory_field_idx = TRUE;
+ ctx->remap_counter = ctx->view->cache->remap_counter;
+ ctx->pos = sizeof(*ctx->rec);
+ ctx->rec_size = ctx->rec->size;
+ return TRUE;
+}
+
+static int
+mail_cache_lookup_iter_next_record(struct mail_cache_lookup_iterate_ctx *ctx)
+{
+ struct mail_cache_view *view = ctx->view;
+
+ if (ctx->failed)
+ return -1;
+
+ if (ctx->rec != NULL)
+ ctx->offset = ctx->rec->prev_offset;
+ if (ctx->offset == 0) {
+ /* end of this record list. check newly appended data. */
+ if (view->trans_seq1 > ctx->seq ||
+ view->trans_seq2 < ctx->seq)
+ return 0;
+ /* check data still in memory. this works for recent mails
+ even with INDEX=MEMORY */
+ if (!ctx->memory_appends_checked) {
+ if (mail_cache_lookup_iter_transaction(ctx))
+ return 1;
+ ctx->memory_appends_checked = TRUE;
+ }
+ if (MAIL_CACHE_IS_UNUSABLE(view->cache) || ctx->stop)
+ return 0;
+
+ /* check data already written to cache file */
+ if (ctx->disk_appends_checked ||
+ mail_cache_lookup_offset(view->cache, view->trans_view,
+ ctx->seq, &ctx->offset) <= 0)
+ return 0;
+
+ ctx->disk_appends_checked = TRUE;
+ ctx->remap_counter = view->cache->remap_counter;
+ i_zero(&view->loop_track);
+ }
+
+ if (ctx->stop)
+ return 0;
+
+ /* look up the next record */
+ if (mail_cache_get_record(view->cache, ctx->offset, &ctx->rec) < 0)
+ return -1;
+ if (mail_cache_track_loops(&view->loop_track, ctx->offset,
+ ctx->rec->size)) {
+ mail_cache_set_corrupted(view->cache,
+ "record list is circular");
+ return -1;
+ }
+ ctx->inmemory_field_idx = FALSE;
+ ctx->remap_counter = view->cache->remap_counter;
+
+ ctx->pos = sizeof(*ctx->rec);
+ ctx->rec_size = ctx->rec->size;
+ return 1;
+}
+
+static int
+mail_cache_lookup_rec_get_field(struct mail_cache_lookup_iterate_ctx *ctx,
+ unsigned int *field_idx_r)
+{
+ struct mail_cache *cache = ctx->view->cache;
+ uint32_t file_field;
+
+ file_field = *((const uint32_t *)CONST_PTR_OFFSET(ctx->rec, ctx->pos));
+ if (ctx->inmemory_field_idx) {
+ *field_idx_r = file_field;
+ return 0;
+ }
+
+ if (file_field >= cache->file_fields_count) {
+ /* new field, have to re-read fields header to figure
+ out its size. don't do this if we're purging. */
+ if (!cache->locked) {
+ if (mail_cache_header_fields_read(cache) < 0)
+ return -1;
+ }
+ if (file_field >= cache->file_fields_count) {
+ mail_cache_set_corrupted(cache,
+ "field index too large (%u >= %u)",
+ file_field, cache->file_fields_count);
+ return -1;
+ }
+
+ /* field reading might have re-mmaped the file and
+ caused rec pointer to break. need to get it again. */
+ if (mail_cache_get_record(cache, ctx->offset, &ctx->rec) < 0)
+ return -1;
+ ctx->remap_counter = cache->remap_counter;
+ }
+
+ *field_idx_r = cache->file_field_map[file_field];
+ return 0;
+}
+
+int mail_cache_lookup_iter_next(struct mail_cache_lookup_iterate_ctx *ctx,
+ struct mail_cache_iterate_field *field_r)
+{
+ struct mail_cache *cache = ctx->view->cache;
+ unsigned int field_idx;
+ unsigned int data_size;
+ int ret;
+
+ i_assert(ctx->remap_counter == cache->remap_counter);
+
+ if (ctx->pos + sizeof(uint32_t) > ctx->rec_size) {
+ if (ctx->pos != ctx->rec_size) {
+ mail_cache_set_corrupted(cache,
+ "record has invalid size");
+ return -1;
+ }
+
+ if ((ret = mail_cache_lookup_iter_next_record(ctx)) <= 0)
+ return ret;
+ }
+
+ /* return the next field */
+ if (mail_cache_lookup_rec_get_field(ctx, &field_idx) < 0)
+ return -1;
+ ctx->pos += sizeof(uint32_t);
+
+ data_size = cache->fields[field_idx].field.field_size;
+ if (data_size == UINT_MAX &&
+ ctx->pos + sizeof(uint32_t) <= ctx->rec->size) {
+ /* variable size field. get its size from the file. */
+ data_size = *((const uint32_t *)
+ CONST_PTR_OFFSET(ctx->rec, ctx->pos));
+ ctx->pos += sizeof(uint32_t);
+ }
+
+ if (ctx->rec->size - ctx->pos < data_size) {
+ mail_cache_set_corrupted(cache,
+ "record continues outside its allocated size");
+ return -1;
+ }
+
+ field_r->field_idx = field_idx;
+ field_r->data = CONST_PTR_OFFSET(ctx->rec, ctx->pos);
+ field_r->size = data_size;
+ field_r->offset = ctx->offset + ctx->pos;
+
+ /* each record begins from 32bit aligned position */
+ ctx->pos += (data_size + sizeof(uint32_t)-1) & ~(sizeof(uint32_t)-1);
+ return 1;
+}
+
+static int mail_cache_seq(struct mail_cache_view *view, uint32_t seq)
+{
+ struct mail_cache_lookup_iterate_ctx iter;
+ struct mail_cache_iterate_field field;
+ int ret;
+
+ view->cached_exists_value = (view->cached_exists_value + 1) & UINT8_MAX;
+ if (view->cached_exists_value == 0) {
+ /* wrapped, we'll have to clear the buffer */
+ buffer_set_used_size(view->cached_exists_buf, 0);
+ view->cached_exists_value++;
+ }
+ view->cached_exists_seq = seq;
+
+ mail_cache_lookup_iter_init(view, seq, &iter);
+ while ((ret = mail_cache_lookup_iter_next(&iter, &field)) > 0) {
+ buffer_write(view->cached_exists_buf, field.field_idx,
+ &view->cached_exists_value, 1);
+ }
+ return ret;
+}
+
+int mail_cache_field_exists(struct mail_cache_view *view, uint32_t seq,
+ unsigned int field)
+{
+ const uint8_t *data;
+
+ i_assert(seq > 0);
+
+ /* NOTE: view might point to a non-committed transaction that has
+ fields that don't yet exist in the cache file. So don't add any
+ fast-paths checking whether the field exists in the file. */
+
+ /* FIXME: we should discard the cache if view has been synced */
+ if (view->cached_exists_seq != seq) {
+ if (mail_cache_seq(view, seq) < 0)
+ return -1;
+ }
+
+ data = view->cached_exists_buf->data;
+ return (field < view->cached_exists_buf->used &&
+ data[field] == view->cached_exists_value) ? 1 : 0;
+}
+
+bool mail_cache_field_exists_any(struct mail_cache_view *view, uint32_t seq)
+{
+ uint32_t reset_id;
+
+ return mail_cache_lookup_cur_offset(view->view, seq, &reset_id) != 0;
+}
+
+enum mail_cache_decision_type
+mail_cache_field_get_decision(struct mail_cache *cache, unsigned int field_idx)
+{
+ i_assert(field_idx < cache->fields_count);
+
+ return cache->fields[field_idx].field.decision;
+}
+
+static int
+mail_cache_lookup_bitmask(struct mail_cache_lookup_iterate_ctx *iter,
+ unsigned int field_idx, unsigned int field_size,
+ buffer_t *dest_buf)
+{
+ struct mail_cache_iterate_field field;
+ const unsigned char *src;
+ unsigned char *dest;
+ unsigned int i;
+ bool found = FALSE;
+ int ret;
+
+ /* make sure all bits are cleared first */
+ buffer_write_zero(dest_buf, 0, field_size);
+
+ while ((ret = mail_cache_lookup_iter_next(iter, &field)) > 0) {
+ if (field.field_idx != field_idx)
+ continue;
+
+ /* merge all bits */
+ src = field.data;
+ dest = buffer_get_space_unsafe(dest_buf, 0, field.size);
+ for (i = 0; i < field.size; i++)
+ dest[i] |= src[i];
+ found = TRUE;
+ }
+ return ret < 0 ? -1 : (found ? 1 : 0);
+}
+
+int mail_cache_lookup_field(struct mail_cache_view *view, buffer_t *dest_buf,
+ uint32_t seq, unsigned int field_idx)
+{
+ struct mail_cache_lookup_iterate_ctx iter;
+ struct mail_cache_iterate_field field;
+ int ret;
+
+ ret = mail_cache_field_exists(view, seq, field_idx);
+ mail_cache_decision_state_update(view, seq, field_idx);
+ if (ret <= 0)
+ return ret;
+
+ /* the field should exist */
+ mail_cache_lookup_iter_init(view, seq, &iter);
+ if (view->cache->fields[field_idx].field.type == MAIL_CACHE_FIELD_BITMASK) {
+ ret = mail_cache_lookup_bitmask(&iter, field_idx,
+ view->cache->fields[field_idx].field.field_size,
+ dest_buf);
+ } else {
+ /* return the first one that's found. if there are multiple
+ they're all identical. */
+ while ((ret = mail_cache_lookup_iter_next(&iter, &field)) > 0) {
+ if (field.field_idx == field_idx) {
+ buffer_append(dest_buf, field.data, field.size);
+ break;
+ }
+ }
+ }
+ /* NOTE: view->cache->fields may have been reallocated by
+ mail_cache_lookup_*(). */
+ return ret;
+}
+
+struct header_lookup_data {
+ uint32_t data_size;
+ const unsigned char *data;
+};
+
+struct header_lookup_line {
+ uint32_t line_num;
+ struct header_lookup_data *data;
+};
+
+struct header_lookup_context {
+ struct mail_cache_view *view;
+ pool_t pool;
+ ARRAY(struct header_lookup_line) lines;
+};
+
+enum {
+ HDR_FIELD_STATE_DONTWANT = 0,
+ HDR_FIELD_STATE_WANT,
+ HDR_FIELD_STATE_SEEN
+};
+
+static void header_lines_save(struct header_lookup_context *ctx,
+ const struct mail_cache_iterate_field *field)
+{
+ const uint32_t *lines = field->data;
+ uint32_t data_size = field->size;
+ struct header_lookup_line hdr_line;
+ struct header_lookup_data *hdr_data;
+ void *data_dup;
+ unsigned int i, lines_count, pos;
+
+ /* data = { line_nums[], 0, "headers" } */
+ for (i = 0; data_size >= sizeof(uint32_t); i++) {
+ data_size -= sizeof(uint32_t);
+ if (lines[i] == 0)
+ break;
+ }
+ lines_count = i;
+ pos = (lines_count+1) * sizeof(uint32_t);
+
+ hdr_data = p_new(ctx->pool, struct header_lookup_data, 1);
+ hdr_data->data_size = data_size;
+ if (data_size > 0) {
+ hdr_data->data = data_dup =
+ p_malloc(ctx->pool, data_size);
+ memcpy(data_dup, CONST_PTR_OFFSET(field->data, pos), data_size);
+ }
+
+ for (i = 0; i < lines_count; i++) {
+ hdr_line.line_num = lines[i];
+ hdr_line.data = hdr_data;
+ array_push_back(&ctx->lines, &hdr_line);
+ }
+}
+
+static int header_lookup_line_cmp(const struct header_lookup_line *l1,
+ const struct header_lookup_line *l2)
+{
+ return (int)l1->line_num - (int)l2->line_num;
+}
+
+static int
+mail_cache_lookup_headers_real(struct mail_cache_view *view, string_t *dest,
+ uint32_t seq, const unsigned int field_idxs[],
+ unsigned int fields_count, pool_t *pool_r)
+{
+ struct mail_cache_lookup_iterate_ctx iter;
+ struct mail_cache_iterate_field field;
+ struct header_lookup_context ctx;
+ struct header_lookup_line *lines;
+ const unsigned char *p, *start, *end;
+ uint8_t *field_state;
+ unsigned int i, count, max_field = 0;
+ size_t hdr_size;
+ uint8_t want = HDR_FIELD_STATE_WANT;
+ buffer_t *buf;
+ int ret;
+
+ *pool_r = NULL;
+
+ if (fields_count == 0)
+ return 1;
+
+ /* update the decision state regardless of whether the fields
+ actually exist or not. */
+ for (i = 0; i < fields_count; i++)
+ mail_cache_decision_state_update(view, seq, field_idxs[i]);
+
+ /* mark all the fields we want to find. */
+ buf = t_buffer_create(32);
+ for (i = 0; i < fields_count; i++) {
+ if (field_idxs[i] > max_field)
+ max_field = field_idxs[i];
+
+ buffer_write(buf, field_idxs[i], &want, 1);
+ }
+ field_state = buffer_get_modifiable_data(buf, NULL);
+
+ /* lookup the fields */
+ i_zero(&ctx);
+ ctx.view = view;
+ ctx.pool = *pool_r = pool_alloconly_create(MEMPOOL_GROWING"mail cache headers", 1024);
+ t_array_init(&ctx.lines, 32);
+
+ mail_cache_lookup_iter_init(view, seq, &iter);
+ while ((ret = mail_cache_lookup_iter_next(&iter, &field)) > 0) {
+ if (field.field_idx > max_field ||
+ field_state[field.field_idx] != HDR_FIELD_STATE_WANT) {
+ /* a) don't want it, b) duplicate */
+ } else {
+ field_state[field.field_idx] = HDR_FIELD_STATE_SEEN;
+ header_lines_save(&ctx, &field);
+ }
+
+ }
+ if (ret < 0)
+ return -1;
+
+ /* check that all fields were found */
+ for (i = 0; i <= max_field; i++) {
+ if (field_state[i] == HDR_FIELD_STATE_WANT)
+ return 0;
+ }
+
+ /* we need to return headers in the order they existed originally.
+ we can do this by sorting the messages by their line numbers. */
+ array_sort(&ctx.lines, header_lookup_line_cmp);
+ lines = array_get_modifiable(&ctx.lines, &count);
+
+ /* then start filling dest buffer from the headers */
+ for (i = 0; i < count; i++) {
+ start = lines[i].data->data;
+ end = start + lines[i].data->data_size;
+
+ /* find the end of the (multiline) header */
+ for (p = start; p != end; p++) {
+ if (*p == '\n' &&
+ (p+1 == end || (p[1] != ' ' && p[1] != '\t'))) {
+ p++;
+ break;
+ }
+ }
+ hdr_size = (size_t)(p - start);
+ buffer_append(dest, start, hdr_size);
+
+ /* if there are more lines for this header, the following lines
+ continue after this one. so skip this line. */
+ lines[i].data->data += hdr_size;
+ lines[i].data->data_size -= hdr_size;
+ }
+ return 1;
+}
+
+int mail_cache_lookup_headers(struct mail_cache_view *view, string_t *dest,
+ uint32_t seq, const unsigned int field_idxs[],
+ unsigned int fields_count)
+{
+ pool_t pool = NULL;
+ int ret;
+
+ if (buffer_get_pool(dest)->datastack_pool)
+ ret = mail_cache_lookup_headers_real(view, dest, seq,
+ field_idxs, fields_count,
+ &pool);
+ else T_BEGIN {
+ ret = mail_cache_lookup_headers_real(view, dest, seq,
+ field_idxs, fields_count,
+ &pool);
+ } T_END;
+ pool_unref(&pool);
+ return ret;
+}
+
+static uint32_t
+mail_cache_get_highest_seq_with_cache(struct mail_cache_view *view,
+ uint32_t below_seq, uint32_t *reset_id_r)
+{
+ struct mail_cache_missing_reason_cache *rc = &view->reason_cache;
+ uint32_t seq = below_seq-1, highest_checked_seq = 0;
+
+ /* find the newest mail that has anything in cache */
+ if (rc->log_file_head_offset == view->view->log_file_head_offset &&
+ rc->log_file_head_seq == view->view->log_file_head_seq) {
+ /* reason_cache matches the current view - we can use it */
+ highest_checked_seq = rc->highest_checked_seq;
+ } else {
+ rc->log_file_head_offset = view->view->log_file_head_offset;
+ rc->log_file_head_seq = view->view->log_file_head_seq;
+ }
+ rc->highest_checked_seq = below_seq;
+
+ /* first check anything not already in reason_cache */
+ for (; seq > highest_checked_seq; seq--) {
+ if (mail_cache_lookup_cur_offset(view->view, seq, reset_id_r) != 0) {
+ rc->highest_seq_with_cache = seq;
+ rc->reset_id = *reset_id_r;
+ return seq;
+ }
+ }
+ if (seq == 0)
+ return 0;
+ /* then return the result from cache */
+ *reset_id_r = rc->reset_id;
+ return rc->highest_seq_with_cache;
+}
+
+const char *
+mail_cache_get_missing_reason(struct mail_cache_view *view, uint32_t seq)
+{
+ uint32_t offset, reset_id;
+
+ if (mail_index_is_expunged(view->view, seq))
+ return "Mail is already expunged";
+
+ if (MAIL_CACHE_IS_UNUSABLE(view->cache))
+ return "Cache file is unusable";
+
+ offset = mail_cache_lookup_cur_offset(view->view, seq, &reset_id);
+ if (offset != 0) {
+ if (view->cache->hdr->file_seq != reset_id) {
+ return t_strdup_printf(
+ "Index reset_id=%u doesn't match cache reset_id=%u",
+ reset_id, view->cache->hdr->file_seq);
+ }
+ return t_strdup_printf(
+ "Mail has other cached fields, reset_id=%u", reset_id);
+ }
+ seq = mail_cache_get_highest_seq_with_cache(view, seq, &reset_id);
+ if (seq == 0) {
+ return t_strdup_printf("Cache file is empty, reset_id=%u",
+ view->cache->hdr->file_seq);
+ }
+
+ uint32_t uid;
+ mail_index_lookup_uid(view->view, seq, &uid);
+
+ if (view->cache->hdr->file_seq != reset_id) {
+ return t_strdup_printf(
+ "Mail not cached, highest cached seq=%u uid=%u: "
+ "Index reset_id=%u doesn't match cache reset_id=%u",
+ seq, uid, reset_id, view->cache->hdr->file_seq);
+ }
+ return t_strdup_printf(
+ "Mail not cached, highest cached seq=%u uid=%u: reset_id=%u",
+ seq, uid, reset_id);
+}
diff --git a/src/lib-index/mail-cache-private.h b/src/lib-index/mail-cache-private.h
new file mode 100644
index 0000000..c2fee17
--- /dev/null
+++ b/src/lib-index/mail-cache-private.h
@@ -0,0 +1,421 @@
+#ifndef MAIL_CACHE_PRIVATE_H
+#define MAIL_CACHE_PRIVATE_H
+
+#include "file-dotlock.h"
+#include "mail-index-private.h"
+#include "mail-cache.h"
+
+#define MAIL_CACHE_MAJOR_VERSION 1
+#define MAIL_CACHE_MINOR_VERSION 1
+
+#define MAIL_CACHE_LOCK_TIMEOUT 10
+#define MAIL_CACHE_LOCK_CHANGE_TIMEOUT 300
+
+#define MAIL_CACHE_MAX_WRITE_BUFFER (1024*256)
+
+#define MAIL_CACHE_IS_UNUSABLE(cache) \
+ ((cache)->hdr == NULL)
+
+struct mail_cache_header {
+ /* Major version is increased only when you can't have backwards
+ compatibility. If the field doesn't match MAIL_CACHE_MAJOR_VERSION,
+ don't even try to read it. */
+ uint8_t major_version;
+ /* If this isn't the same as sizeof(uoff_t), the cache file can't be
+ safely used with the current implementation. */
+ uint8_t compat_sizeof_uoff_t;
+ /* Minor version is increased when the file format changes in a
+ backwards compatible way. */
+ uint8_t minor_version;
+ uint8_t unused;
+
+ /* Unique index file ID, which must match the main index's indexid.
+ See mail_index_header.indexid. */
+ uint32_t indexid;
+ /* Cache file sequence. Increased on every purge. This must match the
+ main index's reset_id for "cache" extension or the cache offsets
+ aren't valid. When creating the first cache file, use the current
+ UNIX timestamp as the file_seq. */
+ uint32_t file_seq;
+
+ /* Number of cache records that are linked inside the cache file,
+ instead of being directly pointed from the main index. */
+ uint32_t continued_record_count;
+
+ /* Number of messages cached in this file. This does not include
+ the continuation records.
+
+ NOTE: <=v2.1 used this for hole offset, so we can't fully
+ rely on it */
+ uint32_t record_count;
+ /* Currently unused. */
+ uint32_t backwards_compat_used_file_size;
+ /* Number of already expunged messages that currently have cache
+ content in this file. */
+ uint32_t deleted_record_count;
+
+ /* Offset to the first mail_cache_header_fields. */
+ uint32_t field_header_offset;
+};
+
+struct mail_cache_header_fields {
+ /* Offset to the updated version of this header. Use
+ mail_index_offset_to_uint32() to decode it. */
+ uint32_t next_offset;
+ /* Full size of this header. */
+ uint32_t size;
+ /* Number of fields in this header. */
+ uint32_t fields_count;
+
+#if 0
+ /* Last time the field was accessed. Not updated more often than
+ once a day. This field may be overwritten later on, which in theory
+ could cause reading to see a partially updated (corrupted) value.
+ Don't fully trust this field unless it was read while cache is
+ locked. */
+ uint32_t last_used[fields_count];
+ /* (uint32_t)-1 for variable sized fields */
+ uint32_t size[fields_count];
+ /* enum mail_cache_field_type */
+ uint8_t type[fields_count];
+ /* enum mail_cache_decision_type. This field can be overwritten
+ later on to update the caching decision. */
+ uint8_t decision[fields_count];
+ /* NUL-separated list of field names */
+ char name[fields_count][];
+#endif
+};
+
+/* Macros to return offsets to the fields in mail_cache_header_fields. */
+#define MAIL_CACHE_FIELD_LAST_USED() \
+ (sizeof(uint32_t) * 3)
+#define MAIL_CACHE_FIELD_SIZE(count) \
+ (MAIL_CACHE_FIELD_LAST_USED() + sizeof(uint32_t) * (count))
+#define MAIL_CACHE_FIELD_TYPE(count) \
+ (MAIL_CACHE_FIELD_SIZE(count) + sizeof(uint32_t) * (count))
+#define MAIL_CACHE_FIELD_DECISION(count) \
+ (MAIL_CACHE_FIELD_TYPE(count) + sizeof(uint8_t) * (count))
+#define MAIL_CACHE_FIELD_NAMES(count) \
+ (MAIL_CACHE_FIELD_DECISION(count) + sizeof(uint8_t) * (count))
+
+struct mail_cache_record {
+ uint32_t prev_offset;
+ uint32_t size; /* full record size, including this header */
+ /* array of { uint32_t field; [ uint32_t size; ] { .. } } */
+};
+
+struct mail_cache_field_private {
+ struct mail_cache_field field;
+
+ /* Highest message UID whose cache field of this type have been
+ accessed within this session. This is used to track whether messages
+ are accessed in non-ascending order, which indicates an IMAP client
+ that doesn't have a local cache. That will result in the caching
+ decision to change from TEMP to YES. */
+ uint32_t uid_highwater;
+
+ /* Unused fields aren't written to cache file */
+ bool used:1;
+ /* field.decision is pending a write to cache file header. If the
+ cache header is read from disk, don't overwrite it. */
+ bool decision_dirty:1;
+};
+
+struct mail_cache {
+ struct mail_index *index;
+ struct event *event;
+ /* Registered "cache" extension ID */
+ uint32_t ext_id;
+
+ char *filepath;
+ int fd;
+
+ struct dotlock_settings dotlock_settings;
+ struct file_lock *file_lock;
+
+ /* Cache file's inode, device and size when it was last fstat()ed. */
+ ino_t st_ino;
+ dev_t st_dev;
+ uoff_t last_stat_size;
+
+ /* Used to avoid logging mmap() errors too rapidly. */
+ time_t last_mmap_error_time;
+
+ /* a) mmaping the whole file */
+ void *mmap_base;
+ /* b) using file cache */
+ struct file_cache *file_cache;
+ /* c) using small read() calls with MAIL_INDEX_OPEN_FLAG_SAVEONLY */
+ uoff_t read_offset;
+ buffer_t *read_buf;
+ /* Size of the cache file as currently mapped to memory. Used for all
+ of a), b), and c). */
+ size_t mmap_length;
+ /* mail_cache_map() increases this always. Used only for asserts. */
+ unsigned int remap_counter;
+ /* Linked list of all cache views. */
+ struct mail_cache_view *views;
+
+ /* mmap_disable=no: hdr points to data / NULL when cache is invalid.
+ mmap_disable=yes: hdr points to hdr_ro_copy. this is needed because
+ cache invalidation can zero the data any time */
+ const struct mail_cache_header *hdr;
+ struct mail_cache_header hdr_ro_copy;
+ /* hdr_copy gets updated when cache is locked and written when
+ unlocking and hdr_modified=TRUE */
+ struct mail_cache_header hdr_copy;
+ /* If non-0, the offset for the last seen mail_cache_header_fields.
+ Used as a cache to avoid reading through multiple next_offset
+ pointers. */
+ uint32_t last_field_header_offset;
+
+ /* Memory pool used for permanent field allocations. Currently this
+ means mail_cache_field.name and field_name_hash. */
+ pool_t field_pool;
+ /* Size of fields[] and field_file_map[] */
+ unsigned int fields_count;
+ /* All the registered cache fields. */
+ struct mail_cache_field_private *fields;
+ /* mail_cache_field.idx -> file-specific header index. The reverse
+ of this is file_field_map[]. */
+ uint32_t *field_file_map;
+ /* mail_cache_field.name -> mail_cache_field.idx */
+ HASH_TABLE(char *, void *) field_name_hash; /* name -> idx */
+
+ /* file-specific header index -> mail_cache_fields.idx. The reverse
+ of this is field_file_map[]. */
+ unsigned int *file_field_map;
+ /* Size of file_field_map[] */
+ unsigned int file_fields_count;
+
+ /* mail_cache_purge_later() sets these values to trigger purging on
+ the next index sync. need_purge_file_seq is set to the current
+ cache file_seq. If at sync time the file_seq differs, it means
+ the cache was already purged and another purge isn't necessary. */
+ uint32_t need_purge_file_seq;
+ /* Human-readable reason for purging. Used for debugging and events. */
+ char *need_purge_reason;
+
+ /* Cache has been opened (or it doesn't exist). */
+ bool opened:1;
+ /* Cache has been locked with mail_cache_lock(). */
+ bool locked:1;
+ /* TRUE if the last lock attempt failed. The next locking attempt will
+ be non-blocking to avoid unnecessarily waiting on a cache that has
+ been locked for a long time. Since cache isn't strictly required,
+ this could avoid unnecessarily long waits with some edge cases. */
+ bool last_lock_failed:1;
+ /* cache->hdr_copy has been modified. This must be used only while
+ cache is locked. */
+ bool hdr_modified:1;
+ /* At least one of the cache fields' last_used or cache decision has
+ changed. mail_cache_header_fields_update() will be used to overwrite
+ these to the latest mail_cache_header_fields. */
+ bool field_header_write_pending:1;
+ /* Cache is currently being purged. */
+ bool purging:1;
+ /* Access the cache file by reading as little as possible from it
+ (as opposed to mmap()ing it or using file-cache.h API to cache
+ larger parts of it). This is used with MAIL_INDEX_OPEN_FLAG_SAVEONLY
+ to avoid unnecessary cache reads. */
+ bool map_with_read:1;
+};
+
+struct mail_cache_loop_track {
+ /* we're looping if size_sum > (max_offset-min_offset) */
+ uoff_t min_offset, max_offset;
+ uoff_t size_sum;
+};
+
+struct mail_cache_missing_reason_cache {
+ uint32_t highest_checked_seq;
+ uint32_t highest_seq_with_cache;
+
+ uint32_t reset_id;
+ uint32_t log_file_head_seq;
+ uoff_t log_file_head_offset;
+};
+
+struct mail_cache_view {
+ struct mail_cache *cache;
+ struct mail_cache_view *prev, *next;
+ struct mail_index_view *view, *trans_view;
+
+ struct mail_cache_transaction_ctx *transaction;
+ /* mail_cache_add() has been called for some of the messages between
+ trans_seq1..trans_seq2 in an uncommitted transaction. Check also
+ the transaction contents when looking up cache fields for these
+ mails. */
+ uint32_t trans_seq1, trans_seq2;
+
+ /* Used to avoid infinite loops in case cache records point to each
+ others, causing a loop. FIXME: New cache files no longer support
+ overwriting existing data, so this could be removed and replaced
+ with a simple check that prev_offset is always smaller than the
+ current record's offset. */
+ struct mail_cache_loop_track loop_track;
+ /* Used for optimizing mail_cache_get_missing_reason() */
+ struct mail_cache_missing_reason_cache reason_cache;
+
+ /* if cached_exists_buf[field] == cached_exists_value, it's cached.
+ this allows us to avoid constantly clearing the whole buffer.
+ it needs to be cleared only when cached_exists_value is wrapped. */
+ buffer_t *cached_exists_buf;
+ uint8_t cached_exists_value;
+ uint32_t cached_exists_seq;
+
+ /* mail_cache_view_update_cache_decisions() has been used to disable
+ updating cache decisions. */
+ bool no_decision_updates:1;
+};
+
+/* mail_cache_lookup_iter_next() returns the next found field. */
+struct mail_cache_iterate_field {
+ /* mail_cache_field.idx */
+ unsigned int field_idx;
+ /* Size of data */
+ unsigned int size;
+ /* Cache field content in the field type-specific format */
+ const void *data;
+ /* Offset to data in cache file */
+ uoff_t offset;
+};
+
+struct mail_cache_lookup_iterate_ctx {
+ struct mail_cache_view *view;
+ /* This must match mail_cache.remap_counter or the iterator is
+ invalid. */
+ unsigned int remap_counter;
+ /* Message sequence as given to mail_cache_lookup_iter_init() */
+ uint32_t seq;
+
+ /* Pointer to current cache record being iterated. This may point
+ to the cache file or uncommitted transaction. */
+ const struct mail_cache_record *rec;
+ /* Iterator's current position in the cache record. Starts from
+ sizeof(mail_cache_record). */
+ unsigned int pos;
+ /* Copy of rec->size */
+ unsigned int rec_size;
+ /* Cache file offset to the beginning of rec, or 0 if it points to
+ an uncommitted transaction. */
+ uint32_t offset;
+
+ /* Used to loop through all changes in the uncommited transaction,
+ in case there are multiple changes to the same message. */
+ unsigned int trans_next_idx;
+
+ /* Cache has become unusable. Stop the iteration. */
+ bool stop:1;
+ /* I/O error or lock timeout occurred during iteration. Normally there
+ is no locking during iteration, but it may happen while cache is
+ being purged to wait for the purging to finish before cache can be
+ accessed again. */
+ bool failed:1;
+ /* Iteration has finished returning changes from uncommitted
+ transaction's in-memory buffer. */
+ bool memory_appends_checked:1;
+ /* Iteration has finished returning changes from uncommitted
+ transaction that were already written to cache file, but not
+ to main index. */
+ bool disk_appends_checked:1;
+ /* TRUE if the field index numbers in rec as the internal
+ mail_cache_field.idx (instead of the file-specific indexes).
+ This indicates that the rec points to uncommited transaction's
+ in-memory buffer. */
+ bool inmemory_field_idx:1;
+};
+
+/* Explicitly lock the cache file. Returns -1 if error / timed out,
+ 1 if ok, 0 if cache is broken/doesn't exist */
+int mail_cache_lock(struct mail_cache *cache);
+/* Flush pending header updates and unlock. Returns -1 if cache is / just got
+ corrupted, 0 if ok. */
+int mail_cache_flush_and_unlock(struct mail_cache *cache);
+/* Unlock the cache without any header updates. */
+void mail_cache_unlock(struct mail_cache *cache);
+
+int mail_cache_write(struct mail_cache *cache, const void *data, size_t size,
+ uoff_t offset);
+int mail_cache_append(struct mail_cache *cache, const void *data, size_t size,
+ uint32_t *offset);
+
+int mail_cache_header_fields_read(struct mail_cache *cache);
+int mail_cache_header_fields_update(struct mail_cache *cache);
+void mail_cache_header_fields_get(struct mail_cache *cache, buffer_t *dest);
+int mail_cache_header_fields_get_next_offset(struct mail_cache *cache,
+ uint32_t *offset_r);
+void mail_cache_expunge_count(struct mail_cache *cache, unsigned int count);
+
+uint32_t mail_cache_lookup_cur_offset(struct mail_index_view *view,
+ uint32_t seq, uint32_t *reset_id_r);
+int mail_cache_get_record(struct mail_cache *cache, uint32_t offset,
+ const struct mail_cache_record **rec_r);
+uint32_t mail_cache_get_first_new_seq(struct mail_index_view *view);
+
+/* Returns TRUE if offset..size area has been tracked before.
+ Returns FALSE if the area may or may not have been tracked before,
+ but we don't know for sure yet. */
+bool mail_cache_track_loops(struct mail_cache_loop_track *loop_track,
+ uoff_t offset, uoff_t size);
+
+/* Iterate through a message's cached fields. */
+void mail_cache_lookup_iter_init(struct mail_cache_view *view, uint32_t seq,
+ struct mail_cache_lookup_iterate_ctx *ctx_r);
+/* Returns 1 if field was returned, 0 if end of fields, or -1 if error.
+ Note that this may trigger re-reading and reallocating cache fields. */
+int mail_cache_lookup_iter_next(struct mail_cache_lookup_iterate_ctx *ctx,
+ struct mail_cache_iterate_field *field_r);
+const struct mail_cache_record *
+mail_cache_transaction_lookup_rec(struct mail_cache_transaction_ctx *ctx,
+ unsigned int seq,
+ unsigned int *trans_next_idx);
+bool mail_cache_transactions_have_changes(struct mail_cache *cache);
+
+/* Return data from the specified position in the cache file. Returns 1 if
+ successful, 0 if offset/size points outside the cache file, -1 if I/O
+ error. */
+int mail_cache_map(struct mail_cache *cache, size_t offset, size_t size,
+ const void **data_r);
+/* Map the whole cache file into memory. Returns 1 if ok, 0 if corrupted
+ (and deleted), -1 if I/O error. */
+int mail_cache_map_all(struct mail_cache *cache);
+void mail_cache_file_close(struct mail_cache *cache);
+int mail_cache_reopen(struct mail_cache *cache);
+int mail_cache_sync_reset_id(struct mail_cache *cache);
+
+/* Notify the decision handling code that field was looked up for seq.
+ This should be called even for fields that aren't currently in cache file.
+ This is used to update caching decisions for fields that already exist
+ in the cache file. */
+void mail_cache_decision_state_update(struct mail_cache_view *view,
+ uint32_t seq, unsigned int field);
+const char *mail_cache_decision_to_string(enum mail_cache_decision_type dec);
+struct event_passthrough *
+mail_cache_decision_changed_event(struct mail_cache *cache, struct event *event,
+ unsigned int field);
+
+struct mail_cache_purge_drop_ctx {
+ struct mail_cache *cache;
+ time_t max_yes_downgrade_time;
+ time_t max_temp_drop_time;
+};
+enum mail_cache_purge_drop_decision {
+ MAIL_CACHE_PURGE_DROP_DECISION_NONE,
+ MAIL_CACHE_PURGE_DROP_DECISION_DROP,
+ MAIL_CACHE_PURGE_DROP_DECISION_TO_TEMP,
+};
+void mail_cache_purge_drop_init(struct mail_cache *cache,
+ const struct mail_index_header *hdr,
+ struct mail_cache_purge_drop_ctx *ctx_r);
+enum mail_cache_purge_drop_decision
+mail_cache_purge_drop_test(struct mail_cache_purge_drop_ctx *ctx,
+ unsigned int field);
+
+int mail_cache_expunge_handler(struct mail_index_sync_map_ctx *sync_ctx,
+ const void *data, void **sync_context);
+
+void mail_cache_set_syscall_error(struct mail_cache *cache,
+ const char *function) ATTR_COLD;
+
+#endif
diff --git a/src/lib-index/mail-cache-purge.c b/src/lib-index/mail-cache-purge.c
new file mode 100644
index 0000000..bc86bf1
--- /dev/null
+++ b/src/lib-index/mail-cache-purge.c
@@ -0,0 +1,707 @@
+/* Copyright (c) 2003-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "ostream.h"
+#include "nfs-workarounds.h"
+#include "read-full.h"
+#include "file-dotlock.h"
+#include "file-cache.h"
+#include "file-set-size.h"
+#include "mail-cache-private.h"
+
+#include <stdio.h>
+#include <sys/stat.h>
+
+struct mail_cache_copy_context {
+ struct mail_cache *cache;
+ struct event *event;
+ struct mail_cache_purge_drop_ctx drop_ctx;
+
+ buffer_t *buffer, *field_seen;
+ ARRAY(unsigned int) bitmask_pos;
+ uint32_t *field_file_map;
+
+ uint8_t field_seen_value;
+ bool new_msg;
+};
+
+static void
+mail_cache_merge_bitmask(struct mail_cache_copy_context *ctx,
+ const struct mail_cache_iterate_field *field)
+{
+ unsigned char *dest;
+ unsigned int i, *pos;
+
+ pos = array_idx_get_space(&ctx->bitmask_pos, field->field_idx);
+ if (*pos == 0) {
+ /* we decided to drop this field */
+ return;
+ }
+
+ dest = buffer_get_space_unsafe(ctx->buffer, *pos, field->size);
+ for (i = 0; i < field->size; i++)
+ dest[i] |= ((const unsigned char*)field->data)[i];
+}
+
+static void
+mail_cache_purge_field(struct mail_cache_copy_context *ctx,
+ const struct mail_cache_iterate_field *field)
+{
+ struct mail_cache_field *cache_field;
+ enum mail_cache_decision_type dec;
+ uint32_t file_field_idx, size32;
+ uint8_t *field_seen;
+
+ file_field_idx = ctx->field_file_map[field->field_idx];
+ if (file_field_idx == (uint32_t)-1)
+ return;
+
+ cache_field = &ctx->cache->fields[field->field_idx].field;
+
+ field_seen = buffer_get_space_unsafe(ctx->field_seen,
+ field->field_idx, 1);
+ if (*field_seen == ctx->field_seen_value) {
+ /* duplicate */
+ if (cache_field->type == MAIL_CACHE_FIELD_BITMASK)
+ mail_cache_merge_bitmask(ctx, field);
+ return;
+ }
+ *field_seen = ctx->field_seen_value;
+
+ dec = cache_field->decision & ENUM_NEGATE(MAIL_CACHE_DECISION_FORCED);
+ if (ctx->new_msg) {
+ if (dec == MAIL_CACHE_DECISION_NO)
+ return;
+ } else {
+ if (dec != MAIL_CACHE_DECISION_YES)
+ return;
+ }
+
+ buffer_append(ctx->buffer, &file_field_idx, sizeof(file_field_idx));
+
+ if (cache_field->field_size == UINT_MAX) {
+ size32 = (uint32_t)field->size;
+ buffer_append(ctx->buffer, &size32, sizeof(size32));
+ }
+
+ if (cache_field->type == MAIL_CACHE_FIELD_BITMASK) {
+ /* remember the position in case we need to update it */
+ unsigned int pos = ctx->buffer->used;
+
+ array_idx_set(&ctx->bitmask_pos, field->field_idx, &pos);
+ }
+ buffer_append(ctx->buffer, field->data, field->size);
+ if ((field->size & 3) != 0)
+ buffer_append_zero(ctx->buffer, 4 - (field->size & 3));
+}
+
+static uint32_t get_next_file_seq(struct mail_cache *cache)
+{
+ const struct mail_index_ext *ext;
+ struct mail_index_view *view;
+ uint32_t file_seq;
+
+ /* make sure we look up the latest reset_id */
+ if (mail_index_refresh(cache->index) < 0)
+ return -1;
+
+ view = mail_index_view_open(cache->index);
+ ext = mail_index_view_get_ext(view, cache->ext_id);
+ file_seq = ext != NULL ? ext->reset_id + 1 : (uint32_t)ioloop_time;
+
+ if (cache->hdr != NULL && file_seq <= cache->hdr->file_seq)
+ file_seq = cache->hdr->file_seq + 1;
+ mail_index_view_close(&view);
+
+ return file_seq != 0 ? file_seq : 1;
+}
+
+static void
+mail_cache_purge_get_fields(struct mail_cache_copy_context *ctx,
+ unsigned int used_fields_count)
+{
+ struct mail_cache *cache = ctx->cache;
+ unsigned int i, j, idx;
+
+ /* Make mail_cache_header_fields_get() return the fields in
+ the same order as we saved them. */
+ memcpy(cache->field_file_map, ctx->field_file_map,
+ sizeof(uint32_t) * cache->fields_count);
+
+ /* reverse mapping */
+ cache->file_fields_count = used_fields_count;
+ i_free(cache->file_field_map);
+ cache->file_field_map = used_fields_count == 0 ? NULL :
+ i_new(unsigned int, used_fields_count);
+ for (i = j = 0; i < cache->fields_count; i++) {
+ idx = cache->field_file_map[i];
+ if (idx != (uint32_t)-1) {
+ i_assert(idx < used_fields_count &&
+ cache->file_field_map != NULL &&
+ cache->file_field_map[idx] == 0);
+ cache->file_field_map[idx] = i;
+ j++;
+ }
+ }
+ i_assert(j == used_fields_count);
+
+ buffer_set_used_size(ctx->buffer, 0);
+ mail_cache_header_fields_get(cache, ctx->buffer);
+}
+
+static bool
+mail_cache_purge_check_field(struct mail_cache_copy_context *ctx,
+ unsigned int field)
+{
+ struct mail_cache_field_private *priv = &ctx->cache->fields[field];
+ enum mail_cache_decision_type dec = priv->field.decision;
+
+ switch (mail_cache_purge_drop_test(&ctx->drop_ctx, field)) {
+ case MAIL_CACHE_PURGE_DROP_DECISION_NONE:
+ break;
+ case MAIL_CACHE_PURGE_DROP_DECISION_DROP: {
+ const char *dec_str = mail_cache_decision_to_string(dec);
+ struct event_passthrough *e =
+ event_create_passthrough(ctx->event)->
+ set_name("mail_cache_purge_drop_field")->
+ add_str("field", priv->field.name)->
+ add_str("decision", dec_str)->
+ add_int("last_used", priv->field.last_used);
+ e_debug(e->event(), "Purge dropped field %s "
+ "(decision=%s, last_used=%"PRIdTIME_T")",
+ priv->field.name, dec_str, priv->field.last_used);
+ dec = MAIL_CACHE_DECISION_NO;
+ break;
+ }
+ case MAIL_CACHE_PURGE_DROP_DECISION_TO_TEMP: {
+ struct event_passthrough *e =
+ mail_cache_decision_changed_event(
+ ctx->cache, ctx->event, field)->
+ add_str("old_decision", "yes")->
+ add_str("new_decision", "temp");
+ e_debug(e->event(), "Purge changes field %s "
+ "cache decision yes -> temp "
+ "(last_used=%"PRIdTIME_T")",
+ priv->field.name, priv->field.last_used);
+ dec = MAIL_CACHE_DECISION_TEMP;
+ break;
+ }
+ }
+ priv->field.decision = dec;
+
+ /* drop all fields we don't want */
+ if ((dec & ENUM_NEGATE(MAIL_CACHE_DECISION_FORCED)) == MAIL_CACHE_DECISION_NO) {
+ priv->used = FALSE;
+ priv->field.last_used = 0;
+ }
+ return priv->used;
+}
+
+static int
+mail_cache_copy(struct mail_cache *cache, struct mail_index_transaction *trans,
+ struct event *event, int fd, const char *reason,
+ uint32_t *file_seq_r, uoff_t *file_size_r, uint32_t *max_uid_r,
+ uint32_t *ext_first_seq_r, ARRAY_TYPE(uint32_t) *ext_offsets)
+{
+ struct mail_cache_copy_context ctx;
+ struct mail_cache_lookup_iterate_ctx iter;
+ struct mail_cache_iterate_field field;
+ struct mail_index_view *view;
+ struct mail_cache_view *cache_view;
+ const struct mail_index_header *idx_hdr;
+ struct mail_cache_header hdr;
+ struct mail_cache_record cache_rec;
+ struct ostream *output;
+ uint32_t message_count, seq, first_new_seq, ext_offset;
+ unsigned int i, used_fields_count, orig_fields_count, record_count;
+
+ i_assert(reason != NULL);
+
+ *max_uid_r = 0;
+ *ext_first_seq_r = 0;
+
+ /* get the latest info on fields */
+ if (mail_cache_header_fields_read(cache) < 0)
+ return -1;
+
+ view = mail_index_transaction_open_updated_view(trans);
+ cache_view = mail_cache_view_open(cache, view);
+ output = o_stream_create_fd_file(fd, 0, FALSE);
+
+ i_zero(&hdr);
+ hdr.major_version = MAIL_CACHE_MAJOR_VERSION;
+ hdr.minor_version = MAIL_CACHE_MINOR_VERSION;
+ hdr.compat_sizeof_uoff_t = sizeof(uoff_t);
+ hdr.indexid = cache->index->indexid;
+ hdr.file_seq = get_next_file_seq(cache);
+ o_stream_nsend(output, &hdr, sizeof(hdr));
+
+ event_add_str(event, "reason", reason);
+ event_add_int(event, "file_seq", hdr.file_seq);
+ event_set_name(event, "mail_cache_purge_started");
+ e_debug(event, "Purging (new file_seq=%u): %s", hdr.file_seq, reason);
+
+ i_zero(&ctx);
+ ctx.cache = cache;
+ ctx.event = event;
+ ctx.buffer = buffer_create_dynamic(default_pool, 4096);
+ ctx.field_seen = buffer_create_dynamic(default_pool, 64);
+ ctx.field_seen_value = 0;
+ ctx.field_file_map = t_new(uint32_t, cache->fields_count + 1);
+ t_array_init(&ctx.bitmask_pos, 32);
+
+ /* @UNSAFE: drop unused fields and create a field mapping for
+ used fields */
+ idx_hdr = mail_index_get_header(view);
+ mail_cache_purge_drop_init(cache, idx_hdr, &ctx.drop_ctx);
+
+ orig_fields_count = cache->fields_count;
+ if (cache->file_fields_count == 0) {
+ /* creating the initial cache file. add all fields. */
+ for (i = 0; i < orig_fields_count; i++)
+ ctx.field_file_map[i] = i;
+ used_fields_count = i;
+ } else {
+ for (i = used_fields_count = 0; i < orig_fields_count; i++) {
+ if (!mail_cache_purge_check_field(&ctx, i))
+ ctx.field_file_map[i] = (uint32_t)-1;
+ else
+ ctx.field_file_map[i] = used_fields_count++;
+ }
+ }
+
+ /* get sequence of first message which doesn't need its temp fields
+ removed. */
+ first_new_seq = mail_cache_get_first_new_seq(view);
+ message_count = mail_index_view_get_messages_count(view);
+ if (!trans->reset)
+ seq = 1;
+ else {
+ /* Index is being rebuilt. Ignore old messages. */
+ seq = trans->first_new_seq;
+ }
+
+ *ext_first_seq_r = seq;
+ i_array_init(ext_offsets, message_count); record_count = 0;
+ for (; seq <= message_count; seq++) {
+ if (mail_index_transaction_is_expunged(trans, seq)) {
+ array_append_zero(ext_offsets);
+ continue;
+ }
+
+ ctx.new_msg = seq >= first_new_seq;
+ buffer_set_used_size(ctx.buffer, 0);
+
+ ctx.field_seen_value = (ctx.field_seen_value + 1) & UINT8_MAX;
+ if (ctx.field_seen_value == 0) {
+ memset(buffer_get_modifiable_data(ctx.field_seen, NULL),
+ 0, buffer_get_size(ctx.field_seen));
+ ctx.field_seen_value++;
+ }
+ array_clear(&ctx.bitmask_pos);
+
+ i_zero(&cache_rec);
+ buffer_append(ctx.buffer, &cache_rec, sizeof(cache_rec));
+
+ mail_cache_lookup_iter_init(cache_view, seq, &iter);
+ while (mail_cache_lookup_iter_next(&iter, &field) > 0)
+ mail_cache_purge_field(&ctx, &field);
+
+ if (ctx.buffer->used == sizeof(cache_rec) ||
+ ctx.buffer->used > cache->index->optimization_set.cache.record_max_size) {
+ /* nothing cached */
+ ext_offset = 0;
+ } else {
+ mail_index_lookup_uid(view, seq, max_uid_r);
+ cache_rec.size = ctx.buffer->used;
+ ext_offset = output->offset;
+ buffer_write(ctx.buffer, 0, &cache_rec,
+ sizeof(cache_rec));
+ o_stream_nsend(output, ctx.buffer->data, cache_rec.size);
+ record_count++;
+ }
+
+ array_push_back(ext_offsets, &ext_offset);
+ }
+ i_assert(orig_fields_count == cache->fields_count);
+
+ hdr.record_count = record_count;
+ hdr.field_header_offset = mail_index_uint32_to_offset(output->offset);
+ mail_cache_purge_get_fields(&ctx, used_fields_count);
+ o_stream_nsend(output, ctx.buffer->data, ctx.buffer->used);
+
+ hdr.backwards_compat_used_file_size = output->offset;
+ buffer_free(&ctx.buffer);
+ buffer_free(&ctx.field_seen);
+
+ *file_size_r = output->offset;
+ (void)o_stream_seek(output, 0);
+ o_stream_nsend(output, &hdr, sizeof(hdr));
+
+ mail_cache_view_close(&cache_view);
+ mail_index_view_close(&view);
+
+ if (o_stream_finish(output) < 0) {
+ mail_cache_set_syscall_error(cache, "write()");
+ o_stream_destroy(&output);
+ array_free(ext_offsets);
+ return -1;
+ }
+ o_stream_destroy(&output);
+
+ if (cache->index->set.fsync_mode == FSYNC_MODE_ALWAYS) {
+ if (fdatasync(fd) < 0) {
+ mail_cache_set_syscall_error(cache, "fdatasync()");
+ array_free(ext_offsets);
+ return -1;
+ }
+ }
+
+ *file_seq_r = hdr.file_seq;
+ return 0;
+}
+
+static int
+mail_cache_purge_write(struct mail_cache *cache,
+ struct mail_index_transaction *trans,
+ int fd, const char *temp_path, const char *
+ reason, bool *unlock)
+{
+ struct event *event;
+ struct stat st;
+ uint32_t prev_file_seq, file_seq, old_offset, max_uid, ext_first_seq;
+ ARRAY_TYPE(uint32_t) ext_offsets;
+ const uint32_t *offsets;
+ uoff_t prev_file_size, file_size;
+ unsigned int i, count, prev_deleted_records;
+
+ if (cache->hdr == NULL) {
+ prev_file_seq = 0;
+ prev_file_size = 0;
+ prev_deleted_records = 0;
+ } else {
+ prev_file_seq = cache->hdr->file_seq;
+ prev_file_size = cache->last_stat_size;
+ prev_deleted_records = cache->hdr->deleted_record_count;
+ }
+ event = event_create(cache->event);
+ event_add_int(event, "prev_file_seq", prev_file_seq);
+ event_add_int(event, "prev_file_size", prev_file_size);
+ event_add_int(event, "prev_deleted_records", prev_deleted_records);
+
+ if (mail_cache_copy(cache, trans, event, fd, reason,
+ &file_seq, &file_size, &max_uid,
+ &ext_first_seq, &ext_offsets) < 0)
+ return -1;
+
+ if (fstat(fd, &st) < 0) {
+ mail_cache_set_syscall_error(cache, "fstat()");
+ array_free(&ext_offsets);
+ return -1;
+ }
+ if (rename(temp_path, cache->filepath) < 0) {
+ mail_cache_set_syscall_error(cache, "rename()");
+ array_free(&ext_offsets);
+ return -1;
+ }
+
+ event_add_int(event, "file_size", file_size);
+ event_add_int(event, "max_uid", max_uid);
+ event_set_name(event, "mail_cache_purge_finished");
+ e_debug(event, "Purging finished, file_seq changed %u -> %u, "
+ "size=%"PRIuUOFF_T" -> %"PRIuUOFF_T", max_uid=%u",
+ prev_file_seq, file_seq, prev_file_size, file_size, max_uid);
+ event_unref(&event);
+
+ /* once we're sure that the purging was successful,
+ update the offsets */
+ mail_index_ext_reset(trans, cache->ext_id, file_seq, TRUE);
+ offsets = array_get(&ext_offsets, &count);
+ for (i = 0; i < count; i++) {
+ if (offsets[i] != 0) {
+ mail_index_update_ext(trans, ext_first_seq + i,
+ cache->ext_id,
+ &offsets[i], &old_offset);
+ }
+ }
+ array_free(&ext_offsets);
+
+ if (*unlock) {
+ mail_cache_unlock(cache);
+ *unlock = FALSE;
+ }
+
+ mail_cache_file_close(cache);
+ cache->opened = TRUE;
+ cache->fd = fd;
+ cache->st_ino = st.st_ino;
+ cache->st_dev = st.st_dev;
+ cache->field_header_write_pending = FALSE;
+ return 0;
+}
+
+static int
+mail_cache_purge_has_file_changed(struct mail_cache *cache,
+ uint32_t purge_file_seq)
+{
+ struct mail_cache_header hdr;
+ unsigned int i;
+ int fd, ret;
+
+ for (i = 0;; i++) {
+ fd = nfs_safe_open(cache->filepath, O_RDONLY);
+ if (fd == -1) {
+ if (errno == ENOENT)
+ return 0;
+
+ mail_cache_set_syscall_error(cache, "open()");
+ return -1;
+ }
+
+ ret = read_full(fd, &hdr, sizeof(hdr));
+ i_close_fd(&fd);
+
+ if (ret >= 0) {
+ if (ret == 0)
+ return 0;
+ if (purge_file_seq == 0) {
+ /* previously it didn't exist or it
+ was unusable and was just unlinked */
+ return 1;
+ }
+ return hdr.file_seq != purge_file_seq ? 1 : 0;
+ } else if (errno != ESTALE || i >= NFS_ESTALE_RETRY_COUNT) {
+ mail_cache_set_syscall_error(cache, "read()");
+ return -1;
+ }
+ }
+}
+
+static int mail_cache_purge_locked(struct mail_cache *cache,
+ uint32_t purge_file_seq,
+ struct mail_index_transaction *trans,
+ const char *reason, bool *unlock)
+{
+ const char *temp_path;
+ int fd, ret;
+
+ /* we've locked the cache purging now. if somebody else had just
+ recreated the cache, reopen the cache and return success. */
+ if (purge_file_seq != (uint32_t)-1 &&
+ (ret = mail_cache_purge_has_file_changed(cache, purge_file_seq)) != 0) {
+ if (ret < 0)
+ return -1;
+
+ /* was just purged, forget this */
+ mail_cache_purge_later_reset(cache);
+
+ if (*unlock) {
+ (void)mail_cache_unlock(cache);
+ *unlock = FALSE;
+ }
+
+ return mail_cache_reopen(cache) < 0 ? -1 : 0;
+ }
+ if (cache->fd != -1) {
+ /* make sure we have mapped it before reading. */
+ if (mail_cache_map_all(cache) <= 0)
+ return -1;
+ }
+
+ /* we want to recreate the cache. write it first to a temporary file */
+ fd = mail_index_create_tmp_file(cache->index, cache->filepath, &temp_path);
+ if (fd == -1)
+ return -1;
+ if (mail_cache_purge_write(cache, trans, fd, temp_path, reason, unlock) < 0) {
+ i_close_fd(&fd);
+ i_unlink(temp_path);
+ return -1;
+ }
+ if (cache->file_cache != NULL)
+ file_cache_set_fd(cache->file_cache, cache->fd);
+
+ if (mail_cache_map_all(cache) <= 0)
+ return -1;
+ if (mail_cache_header_fields_read(cache) < 0)
+ return -1;
+
+ mail_cache_purge_later_reset(cache);
+ return 0;
+}
+
+static int
+mail_cache_purge_full(struct mail_cache *cache,
+ struct mail_index_transaction *trans,
+ uint32_t purge_file_seq, const char *reason)
+{
+ bool unlock = FALSE;
+ int ret;
+
+ i_assert(!cache->purging);
+ i_assert(cache->index->log_sync_locked);
+
+ if (MAIL_INDEX_IS_IN_MEMORY(cache->index) || cache->index->readonly)
+ return 0;
+
+ /* purging isn't very efficient with small read()s */
+ if (cache->map_with_read) {
+ cache->map_with_read = FALSE;
+ if (cache->read_buf != NULL)
+ buffer_set_used_size(cache->read_buf, 0);
+ cache->hdr = NULL;
+ cache->mmap_length = 0;
+ }
+
+ /* .log lock already prevents other processes from purging cache at
+ the same time, but locking the cache file itself prevents other
+ processes from doing other changes to it (header changes, adding
+ more cached data). */
+ switch (mail_cache_lock(cache)) {
+ case -1:
+ /* lock timeout or some other error */
+ return -1;
+ case 0:
+ /* cache is broken or doesn't exist.
+ just start creating it. */
+ break;
+ default:
+ /* locking succeeded. */
+ unlock = TRUE;
+ }
+ cache->purging = TRUE;
+ ret = mail_cache_purge_locked(cache, purge_file_seq, trans, reason, &unlock);
+ cache->purging = FALSE;
+ if (unlock)
+ mail_cache_unlock(cache);
+ i_assert(!cache->hdr_modified);
+ if (ret < 0) {
+ /* the fields may have been updated in memory already.
+ reverse those changes by re-reading them from file. */
+ (void)mail_cache_header_fields_read(cache);
+ }
+ return ret;
+}
+
+int mail_cache_purge_with_trans(struct mail_cache *cache,
+ struct mail_index_transaction *trans,
+ uint32_t purge_file_seq, const char *reason)
+{
+ return mail_cache_purge_full(cache, trans, purge_file_seq, reason);
+}
+
+int mail_cache_purge(struct mail_cache *cache, uint32_t purge_file_seq,
+ const char *reason)
+{
+ struct mail_index_view *view;
+ struct mail_index_transaction *trans;
+ bool lock_log;
+ int ret;
+
+ lock_log = !cache->index->log_sync_locked;
+ if (lock_log) {
+ uint32_t file_seq;
+ uoff_t file_offset;
+
+ if (mail_transaction_log_sync_lock(cache->index->log,
+ "mail cache purge",
+ &file_seq, &file_offset) < 0)
+ return -1;
+ }
+ /* make sure we see the latest changes in index */
+ ret = mail_index_refresh(cache->index);
+
+ view = mail_index_view_open(cache->index);
+ trans = mail_index_transaction_begin(view,
+ MAIL_INDEX_TRANSACTION_FLAG_EXTERNAL);
+ if (ret < 0)
+ ;
+ else if ((ret = mail_cache_purge_full(cache, trans, purge_file_seq,
+ reason)) < 0)
+ mail_index_transaction_rollback(&trans);
+ else {
+ if (mail_index_transaction_commit(&trans) < 0)
+ ret = -1;
+ }
+ mail_index_view_close(&view);
+ if (lock_log) {
+ mail_transaction_log_sync_unlock(cache->index->log,
+ "mail cache purge");
+ }
+ return ret;
+}
+
+bool mail_cache_need_purge(struct mail_cache *cache, const char **reason_r)
+{
+ if (cache->need_purge_file_seq == 0)
+ return FALSE; /* delayed purging not requested */
+ if (cache->index->readonly)
+ return FALSE; /* no purging when opened as read-only */
+ if ((cache->index->flags & MAIL_INDEX_OPEN_FLAG_SAVEONLY) != 0) {
+ /* Mail deliveries don't really need to purge, even if there
+ could be some work to do. Just delay until the next regular
+ mail access comes before doing any extra work. */
+ return FALSE;
+ }
+
+ i_assert(cache->need_purge_reason != NULL);
+ /* t_strdup() the reason in case it gets freed (or replaced)
+ before it's used */
+ *reason_r = t_strdup(cache->need_purge_reason);
+ return TRUE;
+}
+
+void mail_cache_purge_later(struct mail_cache *cache, const char *reason)
+{
+ i_assert(cache->hdr != NULL);
+
+ cache->need_purge_file_seq = cache->hdr->file_seq;
+ i_free(cache->need_purge_reason);
+ cache->need_purge_reason = i_strdup(reason);
+}
+
+void mail_cache_purge_later_reset(struct mail_cache *cache)
+{
+ cache->need_purge_file_seq = 0;
+ i_free(cache->need_purge_reason);
+}
+
+void mail_cache_purge_drop_init(struct mail_cache *cache,
+ const struct mail_index_header *hdr,
+ struct mail_cache_purge_drop_ctx *ctx_r)
+{
+ i_zero(ctx_r);
+ ctx_r->cache = cache;
+ if (hdr->day_stamp != 0) {
+ const struct mail_index_cache_optimization_settings *opt =
+ &cache->index->optimization_set.cache;
+ ctx_r->max_yes_downgrade_time = hdr->day_stamp -
+ opt->unaccessed_field_drop_secs;
+ ctx_r->max_temp_drop_time = hdr->day_stamp -
+ 2 * opt->unaccessed_field_drop_secs;
+ }
+}
+
+enum mail_cache_purge_drop_decision
+mail_cache_purge_drop_test(struct mail_cache_purge_drop_ctx *ctx,
+ unsigned int field)
+{
+ struct mail_cache_field_private *priv = &ctx->cache->fields[field];
+ enum mail_cache_decision_type dec = priv->field.decision;
+
+ if ((dec & MAIL_CACHE_DECISION_FORCED) != 0)
+ return MAIL_CACHE_PURGE_DROP_DECISION_NONE;
+ if (dec != MAIL_CACHE_DECISION_NO &&
+ priv->field.last_used < ctx->max_temp_drop_time) {
+ /* YES or TEMP decision field hasn't been accessed for a long
+ time now. Drop it. */
+ return MAIL_CACHE_PURGE_DROP_DECISION_DROP;
+ }
+ if (dec == MAIL_CACHE_DECISION_YES &&
+ priv->field.last_used < ctx->max_yes_downgrade_time) {
+ /* YES decision field hasn't been accessed for a while
+ now. Change its decision to TEMP. */
+ return MAIL_CACHE_PURGE_DROP_DECISION_TO_TEMP;
+ }
+ return MAIL_CACHE_PURGE_DROP_DECISION_NONE;
+}
diff --git a/src/lib-index/mail-cache-sync-update.c b/src/lib-index/mail-cache-sync-update.c
new file mode 100644
index 0000000..9073187
--- /dev/null
+++ b/src/lib-index/mail-cache-sync-update.c
@@ -0,0 +1,68 @@
+/* Copyright (c) 2004-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "mail-cache-private.h"
+#include "mail-index-sync-private.h"
+
+struct mail_cache_sync_context {
+ unsigned expunge_count;
+};
+
+void mail_cache_expunge_count(struct mail_cache *cache, unsigned int count)
+{
+ if (mail_cache_lock(cache) > 0) {
+ cache->hdr_copy.deleted_record_count += count;
+ if (cache->hdr_copy.record_count >= count)
+ cache->hdr_copy.record_count -= count;
+ else
+ cache->hdr_copy.record_count = 0;
+ cache->hdr_modified = TRUE;
+ (void)mail_cache_flush_and_unlock(cache);
+ }
+}
+
+static struct mail_cache_sync_context *mail_cache_handler_init(void **context)
+{
+ struct mail_cache_sync_context *ctx;
+
+ if (*context != NULL)
+ ctx = *context;
+ else {
+ *context = i_new(struct mail_cache_sync_context, 1);
+ ctx = *context;
+ }
+ return ctx;
+}
+
+static void mail_cache_handler_deinit(struct mail_index_sync_map_ctx *sync_ctx,
+ struct mail_cache_sync_context *ctx)
+{
+ struct mail_cache *cache = sync_ctx->view->index->cache;
+
+ if (ctx == NULL)
+ return;
+
+ mail_cache_expunge_count(cache, ctx->expunge_count);
+
+ i_free(ctx);
+}
+
+int mail_cache_expunge_handler(struct mail_index_sync_map_ctx *sync_ctx,
+ const void *data, void **sync_context)
+{
+ struct mail_cache_sync_context *ctx = *sync_context;
+ const uint32_t *cache_offset = data;
+
+ if (data == NULL) {
+ mail_cache_handler_deinit(sync_ctx, ctx);
+ *sync_context = NULL;
+ return 0;
+ }
+
+ if (*cache_offset == 0)
+ return 0;
+
+ ctx = mail_cache_handler_init(sync_context);
+ ctx->expunge_count++;
+ return 0;
+}
diff --git a/src/lib-index/mail-cache-transaction.c b/src/lib-index/mail-cache-transaction.c
new file mode 100644
index 0000000..0cdf089
--- /dev/null
+++ b/src/lib-index/mail-cache-transaction.c
@@ -0,0 +1,929 @@
+/* Copyright (c) 2003-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "ioloop.h"
+#include "array.h"
+#include "buffer.h"
+#include "module-context.h"
+#include "file-cache.h"
+#include "file-set-size.h"
+#include "read-full.h"
+#include "write-full.h"
+#include "mail-cache-private.h"
+#include "mail-index-transaction-private.h"
+
+#include <stddef.h>
+#include <sys/stat.h>
+
+#define MAIL_CACHE_INIT_WRITE_BUFFER (1024*16)
+
+#define CACHE_TRANS_CONTEXT(obj) \
+ MODULE_CONTEXT(obj, cache_mail_index_transaction_module)
+#define CACHE_TRANS_CONTEXT_REQUIRE(obj) \
+ MODULE_CONTEXT_REQUIRE(obj, cache_mail_index_transaction_module)
+
+struct mail_cache_transaction_rec {
+ uint32_t seq;
+ uint32_t cache_data_pos;
+};
+
+struct mail_cache_transaction_ctx {
+ union mail_index_transaction_module_context module_ctx;
+ struct mail_index_transaction_vfuncs super;
+
+ struct mail_cache *cache;
+ struct mail_cache_view *view;
+ struct mail_index_transaction *trans;
+
+ uint32_t cache_file_seq;
+ uint32_t first_new_seq;
+
+ buffer_t *cache_data;
+ ARRAY(uint8_t) cache_field_idx_used;
+ ARRAY(struct mail_cache_transaction_rec) cache_data_seq;
+ ARRAY_TYPE(seq_range) cache_data_wanted_seqs;
+ uint32_t prev_seq, min_seq;
+ size_t last_rec_pos;
+
+ unsigned int records_written;
+
+ bool tried_purging:1;
+ bool decisions_refreshed:1;
+ bool have_noncommited_mails:1;
+ bool changes:1;
+};
+
+static MODULE_CONTEXT_DEFINE_INIT(cache_mail_index_transaction_module,
+ &mail_index_module_register);
+
+static int mail_cache_transaction_lock(struct mail_cache_transaction_ctx *ctx);
+static bool
+mail_cache_transaction_update_last_rec_size(struct mail_cache_transaction_ctx *ctx,
+ size_t *size_r);
+static int mail_cache_header_rewrite_fields(struct mail_cache *cache);
+
+static void mail_index_transaction_cache_reset(struct mail_index_transaction *t)
+{
+ struct mail_cache_transaction_ctx *ctx = CACHE_TRANS_CONTEXT_REQUIRE(t);
+ struct mail_index_transaction_vfuncs super = ctx->super;
+
+ mail_cache_transaction_reset(ctx);
+ super.reset(t);
+}
+
+static int
+mail_index_transaction_cache_commit(struct mail_index_transaction *t,
+ struct mail_index_transaction_commit_result *result_r)
+{
+ struct mail_cache_transaction_ctx *ctx = CACHE_TRANS_CONTEXT_REQUIRE(t);
+ struct mail_index_transaction_vfuncs super = ctx->super;
+
+ /* a failed cache commit isn't important enough to fail the entire
+ index transaction, so we'll just ignore it */
+ (void)mail_cache_transaction_commit(&ctx);
+ return super.commit(t, result_r);
+}
+
+static void
+mail_index_transaction_cache_rollback(struct mail_index_transaction *t)
+{
+ struct mail_cache_transaction_ctx *ctx = CACHE_TRANS_CONTEXT_REQUIRE(t);
+ struct mail_index_transaction_vfuncs super = ctx->super;
+
+ mail_cache_transaction_rollback(&ctx);
+ super.rollback(t);
+}
+
+struct mail_cache_transaction_ctx *
+mail_cache_get_transaction(struct mail_cache_view *view,
+ struct mail_index_transaction *t)
+{
+ struct mail_cache_transaction_ctx *ctx;
+
+ ctx = !cache_mail_index_transaction_module.id.module_id_set ? NULL :
+ CACHE_TRANS_CONTEXT(t);
+
+ if (ctx != NULL)
+ return ctx;
+
+ ctx = i_new(struct mail_cache_transaction_ctx, 1);
+ ctx->cache = view->cache;
+ ctx->view = view;
+ ctx->trans = t;
+
+ i_assert(view->transaction == NULL);
+ view->transaction = ctx;
+ view->trans_view = mail_index_transaction_open_updated_view(t);
+
+ ctx->super = t->v;
+ t->v.reset = mail_index_transaction_cache_reset;
+ t->v.commit = mail_index_transaction_cache_commit;
+ t->v.rollback = mail_index_transaction_cache_rollback;
+
+ MODULE_CONTEXT_SET(t, cache_mail_index_transaction_module, ctx);
+ return ctx;
+}
+
+static void
+mail_cache_transaction_forget_flushed(struct mail_cache_transaction_ctx *ctx,
+ bool reset_id_changed)
+{
+ uint32_t new_cache_file_seq = MAIL_CACHE_IS_UNUSABLE(ctx->cache) ? 0 :
+ ctx->cache->hdr->file_seq;
+ if (reset_id_changed && ctx->records_written > 0) {
+ e_warning(ctx->cache->event,
+ "Purging lost %u written cache records "
+ "(reset_id changed %u -> %u)", ctx->records_written,
+ ctx->cache_file_seq, new_cache_file_seq);
+ /* don't increase deleted_record_count in the new file */
+ ctx->records_written = 0;
+ }
+ ctx->cache_file_seq = new_cache_file_seq;
+ /* forget all cache extension updates even if reset_id doesn't change */
+ mail_index_ext_set_reset_id(ctx->trans, ctx->cache->ext_id,
+ ctx->cache_file_seq);
+}
+
+void mail_cache_transaction_reset(struct mail_cache_transaction_ctx *ctx)
+{
+ mail_cache_transaction_forget_flushed(ctx, FALSE);
+ if (ctx->cache_data != NULL)
+ buffer_set_used_size(ctx->cache_data, 0);
+ if (array_is_created(&ctx->cache_data_seq))
+ array_clear(&ctx->cache_data_seq);
+ ctx->prev_seq = 0;
+ ctx->last_rec_pos = 0;
+
+ ctx->changes = FALSE;
+}
+
+void mail_cache_transaction_rollback(struct mail_cache_transaction_ctx **_ctx)
+{
+ struct mail_cache_transaction_ctx *ctx = *_ctx;
+
+ *_ctx = NULL;
+
+ if (ctx->records_written > 0) {
+ /* we already wrote to the cache file. we can't (or don't want
+ to) delete that data, so just mark it as deleted space */
+ if (mail_cache_transaction_lock(ctx) > 0) {
+ ctx->cache->hdr_copy.deleted_record_count +=
+ ctx->records_written;
+ ctx->cache->hdr_modified = TRUE;
+ (void)mail_cache_flush_and_unlock(ctx->cache);
+ }
+ }
+
+ MODULE_CONTEXT_UNSET(ctx->trans, cache_mail_index_transaction_module);
+
+ ctx->view->transaction = NULL;
+ ctx->view->trans_seq1 = ctx->view->trans_seq2 = 0;
+
+ mail_index_view_close(&ctx->view->trans_view);
+ buffer_free(&ctx->cache_data);
+ if (array_is_created(&ctx->cache_data_seq))
+ array_free(&ctx->cache_data_seq);
+ if (array_is_created(&ctx->cache_data_wanted_seqs))
+ array_free(&ctx->cache_data_wanted_seqs);
+ array_free(&ctx->cache_field_idx_used);
+ i_free(ctx);
+}
+
+bool mail_cache_transactions_have_changes(struct mail_cache *cache)
+{
+ struct mail_cache_view *view;
+
+ for (view = cache->views; view != NULL; view = view->next) {
+ if (view->transaction != NULL &&
+ view->transaction->changes)
+ return TRUE;
+ }
+ return FALSE;
+}
+
+static int
+mail_cache_transaction_purge(struct mail_cache_transaction_ctx *ctx,
+ const char *reason)
+{
+ struct mail_cache *cache = ctx->cache;
+
+ ctx->tried_purging = TRUE;
+
+ uint32_t purge_file_seq =
+ MAIL_CACHE_IS_UNUSABLE(cache) ? 0 : cache->hdr->file_seq;
+
+ int ret = mail_cache_purge(cache, purge_file_seq, reason);
+ /* already written cache records must be forgotten, but records in
+ memory can still be written to the new cache file */
+ mail_cache_transaction_forget_flushed(ctx, TRUE);
+ return ret;
+}
+
+static int mail_cache_transaction_lock(struct mail_cache_transaction_ctx *ctx)
+{
+ struct mail_cache *cache = ctx->cache;
+ const uoff_t cache_max_size =
+ cache->index->optimization_set.cache.max_size;
+ int ret;
+
+ if ((ret = mail_cache_lock(cache)) <= 0) {
+ if (ret < 0)
+ return -1;
+
+ if (!ctx->tried_purging) {
+ if (mail_cache_transaction_purge(ctx, "creating cache") < 0)
+ return -1;
+ return mail_cache_transaction_lock(ctx);
+ } else {
+ return 0;
+ }
+ }
+ i_assert(!MAIL_CACHE_IS_UNUSABLE(cache));
+
+ if (!ctx->tried_purging && ctx->cache_data != NULL &&
+ cache->last_stat_size + ctx->cache_data->used > cache_max_size) {
+ /* Looks like cache file is becoming too large. Try to purge
+ it to free up some space. */
+ if (cache->hdr->continued_record_count > 0 ||
+ cache->hdr->deleted_record_count > 0) {
+ mail_cache_unlock(cache);
+ (void)mail_cache_transaction_purge(ctx, "cache is too large");
+ return mail_cache_transaction_lock(ctx);
+ }
+ }
+
+ if (ctx->cache_file_seq == 0)
+ ctx->cache_file_seq = cache->hdr->file_seq;
+ else if (ctx->cache_file_seq != cache->hdr->file_seq) {
+ /* already written cache records must be forgotten, but records
+ in memory can still be written to the new cache file */
+ mail_cache_transaction_forget_flushed(ctx, TRUE);
+ i_assert(ctx->cache_file_seq == cache->hdr->file_seq);
+ }
+ return 1;
+}
+
+const struct mail_cache_record *
+mail_cache_transaction_lookup_rec(struct mail_cache_transaction_ctx *ctx,
+ unsigned int seq,
+ unsigned int *trans_next_idx)
+{
+ const struct mail_cache_transaction_rec *recs;
+ unsigned int i, count;
+
+ recs = array_get(&ctx->cache_data_seq, &count);
+ for (i = *trans_next_idx; i < count; i++) {
+ if (recs[i].seq == seq) {
+ *trans_next_idx = i + 1;
+ return CONST_PTR_OFFSET(ctx->cache_data->data,
+ recs[i].cache_data_pos);
+ }
+ }
+ *trans_next_idx = i + 1;
+ if (seq == ctx->prev_seq && i == count) {
+ /* update the unfinished record's (temporary) size and
+ return it */
+ size_t size;
+ if (!mail_cache_transaction_update_last_rec_size(ctx, &size))
+ return NULL;
+ return CONST_PTR_OFFSET(ctx->cache_data->data,
+ ctx->last_rec_pos);
+ }
+ return NULL;
+}
+
+static void
+mail_cache_transaction_update_index(struct mail_cache_transaction_ctx *ctx,
+ uint32_t write_offset, bool committing)
+{
+ struct mail_cache *cache = ctx->cache;
+ struct mail_index_transaction *trans;
+ const struct mail_cache_record *rec = ctx->cache_data->data;
+ const struct mail_cache_transaction_rec *recs;
+ uint32_t i, seq_count;
+
+ if (committing) {
+ /* The transaction is being committed now. Use it. */
+ trans = ctx->trans;
+ } else if (ctx->have_noncommited_mails) {
+ /* Some of the mails haven't been committed yet. We must use
+ the provided transaction to update the cache records. */
+ trans = ctx->trans;
+ } else {
+ /* We can commit these changes immediately. This way even if
+ the provided transaction runs for a very long time, we
+ still once in a while commit the cache changes so they
+ become visible to other processes as well. */
+ trans = mail_index_transaction_begin(ctx->view->trans_view,
+ MAIL_INDEX_TRANSACTION_FLAG_EXTERNAL);
+ }
+
+ mail_index_ext_using_reset_id(trans, ctx->cache->ext_id,
+ ctx->cache_file_seq);
+
+ /* write the cache_offsets to index file. records' prev_offset
+ is updated to point to old cache record when index is being
+ synced. */
+ recs = array_get(&ctx->cache_data_seq, &seq_count);
+ for (i = 0; i < seq_count; i++) {
+ mail_index_update_ext(trans, recs[i].seq, cache->ext_id,
+ &write_offset, NULL);
+
+ write_offset += rec->size;
+ rec = CONST_PTR_OFFSET(rec, rec->size);
+ ctx->records_written++;
+ }
+ if (trans != ctx->trans) {
+ i_assert(cache->index->log_sync_locked);
+ if (mail_index_transaction_commit(&trans) < 0) {
+ /* failed, but can't really do anything */
+ } else {
+ ctx->records_written = 0;
+ }
+ }
+}
+
+static int
+mail_cache_link_records(struct mail_cache_transaction_ctx *ctx,
+ uint32_t write_offset)
+{
+ struct mail_index_map *map;
+ struct mail_cache_record *rec;
+ const struct mail_cache_transaction_rec *recs;
+ const uint32_t *prev_offsetp;
+ ARRAY_TYPE(uint32_t) seq_offsets;
+ uint32_t i, seq_count, reset_id, prev_offset, *offsetp;
+ const void *data;
+
+ i_assert(ctx->min_seq != 0);
+
+ i_array_init(&seq_offsets, 64);
+ recs = array_get(&ctx->cache_data_seq, &seq_count);
+ rec = buffer_get_modifiable_data(ctx->cache_data, NULL);
+ for (i = 0; i < seq_count; i++) {
+ offsetp = array_idx_get_space(&seq_offsets,
+ recs[i].seq - ctx->min_seq);
+ if (*offsetp != 0)
+ prev_offset = *offsetp;
+ else {
+ mail_index_lookup_ext_full(ctx->view->trans_view, recs[i].seq,
+ ctx->cache->ext_id, &map,
+ &data, NULL);
+ prev_offsetp = data;
+
+ if (prev_offsetp == NULL || *prev_offsetp == 0)
+ prev_offset = 0;
+ else if (mail_index_ext_get_reset_id(ctx->view->trans_view, map,
+ ctx->cache->ext_id,
+ &reset_id) &&
+ reset_id == ctx->cache_file_seq)
+ prev_offset = *prev_offsetp;
+ else
+ prev_offset = 0;
+ if (prev_offset >= write_offset) {
+ mail_cache_set_corrupted(ctx->cache,
+ "Cache record offset points outside existing file");
+ array_free(&seq_offsets);
+ return -1;
+ }
+ }
+
+ if (prev_offset != 0) {
+ /* link this record to previous one */
+ rec->prev_offset = prev_offset;
+ ctx->cache->hdr_copy.continued_record_count++;
+ } else {
+ ctx->cache->hdr_copy.record_count++;
+ }
+ *offsetp = write_offset;
+
+ write_offset += rec->size;
+ rec = PTR_OFFSET(rec, rec->size);
+ }
+ array_free(&seq_offsets);
+ ctx->cache->hdr_modified = TRUE;
+ return 0;
+}
+
+static bool
+mail_cache_transaction_set_used(struct mail_cache_transaction_ctx *ctx)
+{
+ const uint8_t *cache_fields_used;
+ unsigned int field_idx, count;
+ bool missing_file_fields = FALSE;
+
+ cache_fields_used = array_get(&ctx->cache_field_idx_used, &count);
+ i_assert(count <= ctx->cache->fields_count);
+ for (field_idx = 0; field_idx < count; field_idx++) {
+ if (cache_fields_used[field_idx] != 0) {
+ ctx->cache->fields[field_idx].used = TRUE;
+ if (ctx->cache->field_file_map[field_idx] == (uint32_t)-1)
+ missing_file_fields = TRUE;
+ }
+ }
+ return missing_file_fields;
+}
+
+static int
+mail_cache_transaction_update_fields(struct mail_cache_transaction_ctx *ctx)
+{
+ unsigned char *p;
+ const unsigned char *end, *rec_end;
+ uint32_t field_idx, data_size;
+
+ if (mail_cache_transaction_set_used(ctx)) {
+ /* add missing fields to cache */
+ if (mail_cache_header_rewrite_fields(ctx->cache) < 0)
+ return -1;
+ /* make sure they were actually added */
+ if (mail_cache_transaction_set_used(ctx)) {
+ mail_index_set_error(ctx->cache->index,
+ "Cache file %s: Unexpectedly lost newly added field",
+ ctx->cache->filepath);
+ return -1;
+ }
+ }
+
+ /* Go through all the added cache records and replace the in-memory
+ field_idx with the cache file-specific field index. Update only
+ up to last_rec_pos, because that's how far flushing is done. The
+ fields after that keep the in-memory field_idx until the next
+ flush. */
+ p = buffer_get_modifiable_data(ctx->cache_data, NULL);
+ end = CONST_PTR_OFFSET(ctx->cache_data->data, ctx->last_rec_pos);
+ rec_end = p;
+ while (p < end) {
+ if (p >= rec_end) {
+ /* next cache record */
+ i_assert(p == rec_end);
+ const struct mail_cache_record *rec =
+ (const struct mail_cache_record *)p;
+ /* note that the last rec->size==0 */
+ rec_end = CONST_PTR_OFFSET(p, rec->size);
+ p += sizeof(*rec);
+ }
+ /* replace field_idx */
+ uint32_t *file_fieldp = (uint32_t *)p;
+ field_idx = *file_fieldp;
+ *file_fieldp = ctx->cache->field_file_map[field_idx];
+ i_assert(*file_fieldp != (uint32_t)-1);
+ p += sizeof(field_idx);
+
+ /* Skip to next cache field. Next is <data size> if the field
+ is not fixed size. */
+ data_size = ctx->cache->fields[field_idx].field.field_size;
+ if (data_size == UINT_MAX) {
+ memcpy(&data_size, p, sizeof(data_size));
+ p += sizeof(data_size);
+ }
+ /* data & 32bit padding */
+ p += data_size;
+ if ((data_size & 3) != 0)
+ p += 4 - (data_size & 3);
+ }
+ i_assert(p == end);
+ return 0;
+}
+
+static void
+mail_cache_transaction_drop_last_flush(struct mail_cache_transaction_ctx *ctx)
+{
+ buffer_copy(ctx->cache_data, 0,
+ ctx->cache_data, ctx->last_rec_pos, SIZE_MAX);
+ buffer_set_used_size(ctx->cache_data,
+ ctx->cache_data->used - ctx->last_rec_pos);
+ ctx->last_rec_pos = 0;
+ ctx->min_seq = 0;
+
+ array_clear(&ctx->cache_data_seq);
+ array_clear(&ctx->cache_data_wanted_seqs);
+}
+
+static int
+mail_cache_transaction_flush(struct mail_cache_transaction_ctx *ctx,
+ bool committing)
+{
+ struct stat st;
+ uint32_t write_offset = 0;
+ int ret = 0;
+
+ i_assert(!ctx->cache->locked);
+
+ if (array_count(&ctx->cache_data_seq) == 0) {
+ /* we had done some changes, but they were aborted. */
+ i_assert(ctx->last_rec_pos == 0);
+ ctx->min_seq = 0;
+ return 0;
+ }
+
+ /* If we're going to be committing a transaction, the log must be
+ locked before we lock cache or we can deadlock. */
+ bool lock_log = !ctx->cache->index->log_sync_locked &&
+ !committing && !ctx->have_noncommited_mails;
+ if (lock_log) {
+ uint32_t file_seq;
+ uoff_t file_offset;
+
+ if (mail_transaction_log_sync_lock(ctx->cache->index->log,
+ "mail cache transaction flush",
+ &file_seq, &file_offset) < 0)
+ return -1;
+ }
+
+ if (mail_cache_transaction_lock(ctx) <= 0) {
+ if (lock_log) {
+ mail_transaction_log_sync_unlock(ctx->cache->index->log,
+ "mail cache transaction flush: cache lock failed");
+ }
+ return -1;
+ }
+
+ i_assert(ctx->cache_data != NULL);
+ i_assert(ctx->last_rec_pos <= ctx->cache_data->used);
+
+ if (mail_cache_transaction_update_fields(ctx) < 0) {
+ if (lock_log) {
+ mail_transaction_log_sync_unlock(ctx->cache->index->log,
+ "mail cache transaction flush: field update failed");
+ }
+ mail_cache_unlock(ctx->cache);
+ return -1;
+ }
+
+ /* we need to get the final write offset for linking records */
+ if (fstat(ctx->cache->fd, &st) < 0) {
+ if (!ESTALE_FSTAT(errno))
+ mail_cache_set_syscall_error(ctx->cache, "fstat()");
+ ret = -1;
+ } else if ((uoff_t)st.st_size + ctx->last_rec_pos > ctx->cache->index->optimization_set.cache.max_size) {
+ mail_cache_set_corrupted(ctx->cache, "Cache file too large");
+ ret = -1;
+ } else {
+ write_offset = st.st_size;
+ if (mail_cache_link_records(ctx, write_offset) < 0)
+ ret = -1;
+ }
+
+ /* write to cache file */
+ if (ret < 0 ||
+ mail_cache_append(ctx->cache, ctx->cache_data->data,
+ ctx->last_rec_pos, &write_offset) < 0)
+ ret = -1;
+ else {
+ /* update records' cache offsets to index */
+ mail_cache_transaction_update_index(ctx, write_offset,
+ committing);
+ }
+ if (mail_cache_flush_and_unlock(ctx->cache) < 0)
+ ret = -1;
+
+ if (lock_log) {
+ mail_transaction_log_sync_unlock(ctx->cache->index->log,
+ "mail cache transaction flush");
+ }
+ return ret;
+}
+
+static void
+mail_cache_transaction_drop_unwanted(struct mail_cache_transaction_ctx *ctx,
+ size_t space_needed)
+{
+ struct mail_cache_transaction_rec *recs;
+ unsigned int i, count;
+
+ recs = array_get_modifiable(&ctx->cache_data_seq, &count);
+ /* find out how many records to delete. delete all unwanted sequences,
+ and if that's not enough delete some more. */
+ for (i = 0; i < count; i++) {
+ if (seq_range_exists(&ctx->cache_data_wanted_seqs, recs[i].seq)) {
+ if (recs[i].cache_data_pos >= space_needed)
+ break;
+ /* we're going to forcibly delete it - remove it also
+ from the array since it's no longer useful there */
+ seq_range_array_remove(&ctx->cache_data_wanted_seqs,
+ recs[i].seq);
+ }
+ }
+ unsigned int deleted_count = i;
+ size_t deleted_space = i < count ?
+ recs[i].cache_data_pos : ctx->last_rec_pos;
+ for (; i < count; i++)
+ recs[i].cache_data_pos -= deleted_space;
+ ctx->last_rec_pos -= deleted_space;
+ array_delete(&ctx->cache_data_seq, 0, deleted_count);
+ buffer_delete(ctx->cache_data, 0, deleted_space);
+}
+
+static bool
+mail_cache_transaction_update_last_rec_size(struct mail_cache_transaction_ctx *ctx,
+ size_t *size_r)
+{
+ struct mail_cache_record *rec;
+ void *data;
+ size_t size;
+
+ data = buffer_get_modifiable_data(ctx->cache_data, &size);
+ rec = PTR_OFFSET(data, ctx->last_rec_pos);
+ rec->size = size - ctx->last_rec_pos;
+ if (rec->size == sizeof(*rec))
+ return FALSE;
+ i_assert(rec->size > sizeof(*rec));
+ *size_r = rec->size;
+ return TRUE;
+}
+
+static void
+mail_cache_transaction_update_last_rec(struct mail_cache_transaction_ctx *ctx)
+{
+ struct mail_cache_transaction_rec *trans_rec;
+ size_t size;
+
+ if (!mail_cache_transaction_update_last_rec_size(ctx, &size) ||
+ size > ctx->cache->index->optimization_set.cache.record_max_size) {
+ buffer_set_used_size(ctx->cache_data, ctx->last_rec_pos);
+ return;
+ }
+
+ if (ctx->min_seq > ctx->prev_seq || ctx->min_seq == 0)
+ ctx->min_seq = ctx->prev_seq;
+ trans_rec = array_append_space(&ctx->cache_data_seq);
+ trans_rec->seq = ctx->prev_seq;
+ trans_rec->cache_data_pos = ctx->last_rec_pos;
+ ctx->last_rec_pos = ctx->cache_data->used;
+}
+
+static void
+mail_cache_transaction_switch_seq(struct mail_cache_transaction_ctx *ctx)
+{
+ struct mail_cache_record new_rec;
+
+ if (ctx->prev_seq != 0) {
+ /* update previously added cache record's size */
+ mail_cache_transaction_update_last_rec(ctx);
+ } else if (ctx->cache_data == NULL) {
+ ctx->cache_data =
+ buffer_create_dynamic(default_pool,
+ MAIL_CACHE_INIT_WRITE_BUFFER);
+ i_array_init(&ctx->cache_data_seq, 64);
+ i_array_init(&ctx->cache_data_wanted_seqs, 32);
+ i_array_init(&ctx->cache_field_idx_used, 64);
+ }
+
+ i_zero(&new_rec);
+ buffer_append(ctx->cache_data, &new_rec, sizeof(new_rec));
+
+ ctx->prev_seq = 0;
+ ctx->changes = TRUE;
+}
+
+int mail_cache_transaction_commit(struct mail_cache_transaction_ctx **_ctx)
+{
+ struct mail_cache_transaction_ctx *ctx = *_ctx;
+ int ret = 0;
+
+ if (ctx->changes) {
+ if (ctx->prev_seq != 0)
+ mail_cache_transaction_update_last_rec(ctx);
+ if (mail_cache_transaction_flush(ctx, TRUE) < 0)
+ ret = -1;
+ else {
+ /* successfully wrote everything */
+ ctx->records_written = 0;
+ }
+ /* Here would be a good place to do fdatasync() to make sure
+ everything is written before offsets are updated to index.
+ However it slows down I/O needlessly and we're pretty good
+ at catching and fixing cache corruption, so we no longer do
+ it. */
+ }
+ mail_cache_transaction_rollback(_ctx);
+ return ret;
+}
+
+static int
+mail_cache_header_fields_write(struct mail_cache *cache, const buffer_t *buffer)
+{
+ uint32_t offset, hdr_offset;
+
+ i_assert(cache->locked);
+
+ offset = 0;
+ if (mail_cache_append(cache, buffer->data, buffer->used, &offset) < 0)
+ return -1;
+
+ if (cache->index->set.fsync_mode == FSYNC_MODE_ALWAYS) {
+ if (fdatasync(cache->fd) < 0) {
+ mail_cache_set_syscall_error(cache, "fdatasync()");
+ return -1;
+ }
+ }
+ /* find offset to the previous header's "next_offset" field */
+ if (mail_cache_header_fields_get_next_offset(cache, &hdr_offset) < 0)
+ return -1;
+
+ /* update the next_offset offset, so our new header will be found */
+ offset = mail_index_uint32_to_offset(offset);
+ if (mail_cache_write(cache, &offset, sizeof(offset), hdr_offset) < 0)
+ return -1;
+
+ if (hdr_offset == offsetof(struct mail_cache_header,
+ field_header_offset)) {
+ /* we're adding the first field. hdr_copy needs to be kept
+ in sync so unlocking won't overwrite it. */
+ cache->hdr_copy.field_header_offset = hdr_offset;
+ cache->hdr_ro_copy.field_header_offset = hdr_offset;
+ }
+ return 0;
+}
+
+static int mail_cache_header_rewrite_fields(struct mail_cache *cache)
+{
+ int ret;
+
+ /* re-read header to make sure we don't lose any fields. */
+ if (mail_cache_header_fields_read(cache) < 0)
+ return -1;
+
+ T_BEGIN {
+ buffer_t *buffer;
+
+ buffer = t_buffer_create(256);
+ mail_cache_header_fields_get(cache, buffer);
+ ret = mail_cache_header_fields_write(cache, buffer);
+ } T_END;
+
+ if (ret == 0) {
+ /* we wrote all the headers, so there are no pending changes */
+ cache->field_header_write_pending = FALSE;
+ ret = mail_cache_header_fields_read(cache);
+ }
+ return ret;
+}
+
+static void
+mail_cache_transaction_refresh_decisions(struct mail_cache_transaction_ctx *ctx)
+{
+ if (ctx->decisions_refreshed)
+ return;
+
+ /* Read latest caching decisions from the cache file's header once
+ per transaction. */
+ if (!ctx->cache->opened)
+ (void)mail_cache_open_and_verify(ctx->cache);
+ else
+ (void)mail_cache_header_fields_read(ctx->cache);
+ ctx->decisions_refreshed = TRUE;
+}
+
+void mail_cache_add(struct mail_cache_transaction_ctx *ctx, uint32_t seq,
+ unsigned int field_idx, const void *data, size_t data_size)
+{
+ uint32_t data_size32;
+ unsigned int fixed_size;
+ size_t full_size, record_size;
+
+ i_assert(field_idx < ctx->cache->fields_count);
+ i_assert(data_size < (uint32_t)-1);
+
+ if (ctx->cache->fields[field_idx].field.decision ==
+ (MAIL_CACHE_DECISION_NO | MAIL_CACHE_DECISION_FORCED))
+ return;
+
+ if (seq >= ctx->trans->first_new_seq)
+ ctx->have_noncommited_mails = TRUE;
+
+ /* If the cache file exists, make sure the caching decisions have been
+ read. */
+ mail_cache_transaction_refresh_decisions(ctx);
+
+ mail_cache_decision_add(ctx->view, seq, field_idx);
+
+ fixed_size = ctx->cache->fields[field_idx].field.field_size;
+ i_assert(fixed_size == UINT_MAX || fixed_size == data_size);
+
+ data_size32 = (uint32_t)data_size;
+ full_size = sizeof(field_idx) + ((data_size + 3) & ~3U);
+ if (fixed_size == UINT_MAX)
+ full_size += sizeof(data_size32);
+
+ if (ctx->prev_seq != seq) {
+ mail_cache_transaction_switch_seq(ctx);
+ ctx->prev_seq = seq;
+ seq_range_array_add(&ctx->cache_data_wanted_seqs, seq);
+
+ /* remember roughly what we have modified, so cache lookups can
+ look into transactions to see changes. */
+ if (seq < ctx->view->trans_seq1 || ctx->view->trans_seq1 == 0)
+ ctx->view->trans_seq1 = seq;
+ if (seq > ctx->view->trans_seq2)
+ ctx->view->trans_seq2 = seq;
+ }
+
+ if (mail_cache_transaction_update_last_rec_size(ctx, &record_size) &&
+ record_size + full_size >
+ ctx->cache->index->optimization_set.cache.record_max_size) {
+ /* Adding this field would exceed the cache record's maximum
+ size. If we don't add this, it's possible that other fields
+ could still be added. */
+ return;
+ }
+
+ /* Remember that this field has been used within the transaction. Later
+ on we fill mail_cache_field_private.used with it. We can't rely on
+ setting it here, because cache purging may run and clear it. */
+ uint8_t field_idx_set = 1;
+ array_idx_set(&ctx->cache_field_idx_used, field_idx, &field_idx_set);
+
+ /* Remember that this value exists for the mail, in case we try to look
+ it up. Note that this gets forgotten whenever changing the mail. */
+ buffer_write(ctx->view->cached_exists_buf, field_idx,
+ &ctx->view->cached_exists_value, 1);
+
+ if (ctx->cache_data->used + full_size > MAIL_CACHE_MAX_WRITE_BUFFER &&
+ ctx->last_rec_pos > 0) {
+ /* time to flush our buffer. */
+ if (MAIL_INDEX_IS_IN_MEMORY(ctx->cache->index)) {
+ /* just drop the old data to free up memory */
+ size_t space_needed = ctx->cache_data->used +
+ full_size - MAIL_CACHE_MAX_WRITE_BUFFER;
+ mail_cache_transaction_drop_unwanted(ctx, space_needed);
+ } else {
+ if (mail_cache_transaction_flush(ctx, FALSE) < 0) {
+ /* If this is a syscall failure, the already
+ flushed changes could still be finished by
+ writing the offsets to .log file. If this is
+ a corruption/lost cache, the offsets will
+ point to a nonexistent file or be ignored.
+ Either way, we don't really need to handle
+ this failure in any special way. */
+ }
+ /* Regardless of whether the flush succeeded, drop all
+ data that it would have written. This way the flush
+ is attempted only once, but it could still be
+ possible to write new data later. Also don't reset
+ the transaction entirely so that the last partially
+ cached mail can still be accessed from memory. */
+ mail_cache_transaction_drop_last_flush(ctx);
+ }
+ }
+
+ buffer_append(ctx->cache_data, &field_idx, sizeof(field_idx));
+ if (fixed_size == UINT_MAX) {
+ buffer_append(ctx->cache_data, &data_size32,
+ sizeof(data_size32));
+ }
+
+ buffer_append(ctx->cache_data, data, data_size);
+ if ((data_size & 3) != 0)
+ buffer_append_zero(ctx->cache_data, 4 - (data_size & 3));
+}
+
+bool mail_cache_field_want_add(struct mail_cache_transaction_ctx *ctx,
+ uint32_t seq, unsigned int field_idx)
+{
+ enum mail_cache_decision_type decision;
+
+ mail_cache_transaction_refresh_decisions(ctx);
+
+ decision = mail_cache_field_get_decision(ctx->view->cache, field_idx);
+ decision &= ENUM_NEGATE(MAIL_CACHE_DECISION_FORCED);
+ switch (decision) {
+ case MAIL_CACHE_DECISION_NO:
+ return FALSE;
+ case MAIL_CACHE_DECISION_TEMP:
+ /* add it only if it's newer than what we would drop when
+ purging */
+ if (ctx->first_new_seq == 0) {
+ ctx->first_new_seq =
+ mail_cache_get_first_new_seq(ctx->view->view);
+ }
+ if (seq < ctx->first_new_seq)
+ return FALSE;
+ break;
+ default:
+ break;
+ }
+
+ return mail_cache_field_exists(ctx->view, seq, field_idx) == 0;
+}
+
+bool mail_cache_field_can_add(struct mail_cache_transaction_ctx *ctx,
+ uint32_t seq, unsigned int field_idx)
+{
+ enum mail_cache_decision_type decision;
+
+ mail_cache_transaction_refresh_decisions(ctx);
+
+ decision = mail_cache_field_get_decision(ctx->view->cache, field_idx);
+ if (decision == (MAIL_CACHE_DECISION_FORCED | MAIL_CACHE_DECISION_NO))
+ return FALSE;
+
+ return mail_cache_field_exists(ctx->view, seq, field_idx) == 0;
+}
+
+void mail_cache_close_mail(struct mail_cache_transaction_ctx *ctx,
+ uint32_t seq)
+{
+ if (array_is_created(&ctx->cache_data_wanted_seqs))
+ seq_range_array_remove(&ctx->cache_data_wanted_seqs, seq);
+}
diff --git a/src/lib-index/mail-cache.c b/src/lib-index/mail-cache.c
new file mode 100644
index 0000000..bd3c939
--- /dev/null
+++ b/src/lib-index/mail-cache.c
@@ -0,0 +1,1005 @@
+/* Copyright (c) 2003-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "buffer.h"
+#include "hash.h"
+#include "llist.h"
+#include "nfs-workarounds.h"
+#include "file-cache.h"
+#include "mmap-util.h"
+#include "read-full.h"
+#include "write-full.h"
+#include "mail-cache-private.h"
+#include "ioloop.h"
+
+#include <unistd.h>
+
+#define MAIL_CACHE_MIN_HEADER_READ_SIZE 4096
+
+static struct event_category event_category_mail_cache = {
+ .name = "mail-cache",
+};
+
+void mail_cache_set_syscall_error(struct mail_cache *cache,
+ const char *function)
+{
+ mail_index_file_set_syscall_error(cache->index, cache->filepath,
+ function);
+}
+
+static void mail_cache_unlink(struct mail_cache *cache)
+{
+ if (!cache->index->readonly && !MAIL_INDEX_IS_IN_MEMORY(cache->index))
+ i_unlink_if_exists(cache->filepath);
+ /* mark the cache as unusable */
+ cache->hdr = NULL;
+}
+
+void mail_cache_set_corrupted(struct mail_cache *cache, const char *fmt, ...)
+{
+ va_list va;
+
+ mail_cache_unlink(cache);
+
+ va_start(va, fmt);
+ T_BEGIN {
+ const char *reason = t_strdup_vprintf(fmt, va);
+ const char *errstr = t_strdup_printf(
+ "Deleting corrupted cache: %s", reason);
+ e_error(event_create_passthrough(cache->event)->
+ set_name("mail_cache_corrupted")->
+ add_str("reason", reason)->event(), "%s", errstr);
+ mail_index_set_error_nolog(cache->index, errstr);
+ } T_END;
+ va_end(va);
+}
+
+void mail_cache_set_seq_corrupted_reason(struct mail_cache_view *cache_view,
+ uint32_t seq, const char *reason)
+{
+ uint32_t uid, empty = 0;
+ struct mail_cache *cache = cache_view->cache;
+ struct mail_index_view *view = cache_view->view;
+
+ /* drop cache pointer */
+ struct mail_index_transaction *t =
+ mail_index_transaction_begin(view, MAIL_INDEX_TRANSACTION_FLAG_EXTERNAL);
+ mail_index_update_ext(t, seq, cache->ext_id, &empty, NULL);
+
+ if (mail_index_transaction_commit(&t) < 0) {
+ /* I/O error (e.g. out of disk space). Ignore this for now,
+ maybe it works again later. */
+ return;
+ }
+
+ mail_index_lookup_uid(cache_view->view, seq, &uid);
+ const char *errstr = t_strdup_printf(
+ "Deleting corrupted cache record uid=%u: %s", uid, reason);
+ e_error(event_create_passthrough(cache->event)->
+ set_name("mail_cache_record_corrupted")->
+ add_int("uid", uid)->
+ add_str("reason", reason)->event(), "%s", errstr);
+ mail_cache_expunge_count(cache, 1);
+}
+
+void mail_cache_file_close(struct mail_cache *cache)
+{
+ if (cache->mmap_base != NULL) {
+ if (munmap(cache->mmap_base, cache->mmap_length) < 0)
+ mail_cache_set_syscall_error(cache, "munmap()");
+ }
+
+ if (cache->file_cache != NULL)
+ file_cache_set_fd(cache->file_cache, -1);
+ if (cache->read_buf != NULL)
+ buffer_set_used_size(cache->read_buf, 0);
+
+ cache->mmap_base = NULL;
+ cache->hdr = NULL;
+ cache->mmap_length = 0;
+ cache->last_field_header_offset = 0;
+
+ file_lock_free(&cache->file_lock);
+ cache->locked = FALSE;
+
+ if (cache->fd != -1) {
+ if (close(cache->fd) < 0)
+ mail_cache_set_syscall_error(cache, "close()");
+ cache->fd = -1;
+ }
+ cache->opened = FALSE;
+}
+
+static void mail_cache_init_file_cache(struct mail_cache *cache)
+{
+ struct stat st;
+
+ if (cache->file_cache != NULL)
+ file_cache_set_fd(cache->file_cache, cache->fd);
+
+ if (fstat(cache->fd, &st) == 0) {
+ if (cache->file_cache != NULL)
+ (void)file_cache_set_size(cache->file_cache, st.st_size);
+ } else if (!ESTALE_FSTAT(errno)) {
+ mail_cache_set_syscall_error(cache, "fstat()");
+ }
+
+ cache->last_stat_size = st.st_size;
+ cache->st_ino = st.st_ino;
+ cache->st_dev = st.st_dev;
+}
+
+static int mail_cache_try_open(struct mail_cache *cache)
+{
+ int ret;
+
+ i_assert(!cache->opened);
+ cache->opened = TRUE;
+
+ if (MAIL_INDEX_IS_IN_MEMORY(cache->index))
+ return 0;
+
+ i_assert(cache->fd == -1);
+ cache->fd = nfs_safe_open(cache->filepath,
+ cache->index->readonly ? O_RDONLY : O_RDWR);
+ if (cache->fd == -1) {
+ mail_cache_file_close(cache);
+ if (errno == ENOENT) {
+ mail_cache_purge_later_reset(cache);
+ return 0;
+ }
+
+ mail_cache_set_syscall_error(cache, "open()");
+ return -1;
+ }
+
+ mail_cache_init_file_cache(cache);
+
+ if ((ret = mail_cache_map_all(cache)) <= 0) {
+ mail_cache_file_close(cache);
+ return ret;
+ }
+ return 1;
+}
+
+static bool mail_cache_need_reopen(struct mail_cache *cache)
+{
+ struct stat st;
+
+ if (MAIL_INDEX_IS_IN_MEMORY(cache->index)) {
+ /* disabled */
+ return FALSE;
+ }
+
+ if (cache->fd == -1)
+ return TRUE;
+
+ /* see if the file has changed */
+ if ((cache->index->flags & MAIL_INDEX_OPEN_FLAG_NFS_FLUSH) != 0) {
+ i_assert(!cache->locked);
+ nfs_flush_file_handle_cache(cache->filepath);
+ }
+ if (nfs_safe_stat(cache->filepath, &st) < 0) {
+ /* if cache was already marked as corrupted, don't log errors
+ about nonexistent cache file */
+ if (cache->hdr != NULL || errno != ENOENT)
+ mail_cache_set_syscall_error(cache, "stat()");
+ return TRUE;
+ }
+ cache->last_stat_size = st.st_size;
+
+ if (st.st_ino != cache->st_ino ||
+ !CMP_DEV_T(st.st_dev, cache->st_dev)) {
+ /* file changed */
+ return TRUE;
+ }
+
+ if ((cache->index->flags & MAIL_INDEX_OPEN_FLAG_NFS_FLUSH) != 0) {
+ /* if the old file has been deleted, the new file may have
+ the same inode as the old one. we'll catch this here by
+ checking if fstat() fails with ESTALE */
+ if (fstat(cache->fd, &st) < 0) {
+ if (ESTALE_FSTAT(errno))
+ return TRUE;
+ mail_cache_set_syscall_error(cache, "fstat()");
+ return FALSE;
+ }
+ }
+ return FALSE;
+}
+
+int mail_cache_reopen(struct mail_cache *cache)
+{
+ mail_cache_file_close(cache);
+ return mail_cache_open_and_verify(cache);
+}
+
+static void mail_cache_update_need_purge(struct mail_cache *cache)
+{
+ const struct mail_index_cache_optimization_settings *set =
+ &cache->index->optimization_set.cache;
+ const struct mail_cache_header *hdr = cache->hdr;
+ struct stat st;
+ unsigned int msg_count;
+ unsigned int records_count, cont_percentage, delete_percentage;
+ const char *want_purge_reason = NULL;
+
+ if (hdr->minor_version == 0) {
+ /* purge to get ourself into the new header version */
+ mail_cache_purge_later(cache, "Minor version too old");
+ return;
+ }
+
+ msg_count = cache->index->map->rec_map->records_count;
+ if (msg_count == 0)
+ records_count = 1;
+ else if (hdr->record_count == 0 || hdr->record_count > msg_count*2) {
+ /* probably not the real record_count, but hole offset that
+ Dovecot <=v2.1 versions used to use in this position.
+ we already checked that minor_version>0, but this could
+ happen if old Dovecot was used to access mailbox after
+ it had been updated. */
+ records_count = I_MAX(msg_count, 1);
+ } else {
+ records_count = hdr->record_count;
+ }
+
+ cont_percentage = hdr->continued_record_count * 100 / records_count;
+ if (cont_percentage >= set->purge_continued_percentage) {
+ /* too many continued rows, purge */
+ want_purge_reason = t_strdup_printf(
+ "Too many continued records (%u/%u)",
+ hdr->continued_record_count, records_count);
+ }
+
+ delete_percentage = hdr->deleted_record_count * 100 /
+ (records_count + hdr->deleted_record_count);
+ if (delete_percentage >= set->purge_delete_percentage) {
+ /* too many deleted records, purge */
+ want_purge_reason = t_strdup_printf(
+ "Too many deleted records (%u/%u)",
+ hdr->deleted_record_count, records_count);
+ }
+
+ if (want_purge_reason != NULL) {
+ if (fstat(cache->fd, &st) < 0) {
+ if (!ESTALE_FSTAT(errno))
+ mail_cache_set_syscall_error(cache, "fstat()");
+ return;
+ }
+ if ((uoff_t)st.st_size >= set->purge_min_size)
+ mail_cache_purge_later(cache, want_purge_reason);
+ }
+
+}
+
+static bool mail_cache_verify_header(struct mail_cache *cache,
+ const struct mail_cache_header *hdr)
+{
+ /* check that the header is still ok */
+ if (cache->mmap_length < sizeof(struct mail_cache_header)) {
+ mail_cache_set_corrupted(cache, "File too small");
+ return FALSE;
+ }
+
+ if (hdr->major_version != MAIL_CACHE_MAJOR_VERSION) {
+ /* version changed - upgrade silently */
+ mail_cache_set_corrupted(cache, "Unsupported major version (%u)",
+ hdr->major_version);
+ return FALSE;
+ }
+ if (hdr->compat_sizeof_uoff_t != sizeof(uoff_t)) {
+ /* architecture change - handle silently(?) */
+ mail_cache_set_corrupted(cache, "Unsupported uoff_t size (%u)",
+ hdr->compat_sizeof_uoff_t);
+ return FALSE;
+ }
+
+ if (hdr->indexid != cache->index->indexid) {
+ /* index id changed - handle silently */
+ mail_cache_unlink(cache);
+ return FALSE;
+ }
+ if (hdr->file_seq == 0) {
+ mail_cache_set_corrupted(cache, "file_seq is 0");
+ return FALSE;
+ }
+ return TRUE;
+}
+
+static int
+mail_cache_map_finish(struct mail_cache *cache, uoff_t offset, size_t size,
+ const void *hdr_data, bool copy_hdr, bool *corrupted_r)
+{
+ const struct mail_cache_header *hdr = hdr_data;
+
+ *corrupted_r = FALSE;
+
+ if (offset == 0) {
+ /* verify the header validity only with offset=0. this way
+ we won't waste time re-verifying it all the time */
+ if (!mail_cache_verify_header(cache, hdr)) {
+ if (!MAIL_CACHE_IS_UNUSABLE(cache) &&
+ cache->hdr->file_seq != 0)
+ mail_cache_purge_later(cache, "Invalid header");
+ *corrupted_r = TRUE;
+ return -1;
+ }
+ }
+ if (hdr_data != NULL) {
+ if (!copy_hdr)
+ cache->hdr = hdr;
+ else {
+ memcpy(&cache->hdr_ro_copy, hdr,
+ sizeof(cache->hdr_ro_copy));
+ cache->hdr = &cache->hdr_ro_copy;
+ }
+ mail_cache_update_need_purge(cache);
+ } else {
+ i_assert(cache->hdr != NULL);
+ }
+ i_assert(cache->hdr->file_seq != 0);
+
+ if (offset + size > cache->mmap_length)
+ return 0;
+ return 1;
+}
+
+static int
+mail_cache_map_with_read(struct mail_cache *cache, size_t offset, size_t size,
+ const void **data_r, bool *corrupted_r)
+{
+ const void *hdr_data;
+ void *data;
+ ssize_t ret;
+
+ if (cache->read_buf == NULL) {
+ cache->read_buf =
+ buffer_create_dynamic(default_pool, size);
+ } else if (cache->read_offset <= offset &&
+ cache->read_offset + cache->read_buf->used >= offset+size) {
+ /* already mapped */
+ *data_r = CONST_PTR_OFFSET(cache->read_buf->data,
+ offset - cache->read_offset);
+ hdr_data = offset == 0 ? *data_r : NULL;
+ return mail_cache_map_finish(cache, offset, size, hdr_data,
+ TRUE, corrupted_r);
+ } else {
+ buffer_set_used_size(cache->read_buf, 0);
+ }
+ if (offset == 0 && size < MAIL_CACHE_MIN_HEADER_READ_SIZE) {
+ /* we can usually read the fields header after the cache
+ header. we need them both, so try to read them all with one
+ pread() call. */
+ size = MAIL_CACHE_MIN_HEADER_READ_SIZE;
+ }
+
+ data = buffer_append_space_unsafe(cache->read_buf, size);
+ ret = pread(cache->fd, data, size, offset);
+ if (ret < 0) {
+ if (errno != ESTALE)
+ mail_cache_set_syscall_error(cache, "read()");
+
+ buffer_set_used_size(cache->read_buf, 0);
+ cache->hdr = NULL;
+ cache->mmap_length = 0;
+ return -1;
+ }
+ buffer_set_used_size(cache->read_buf, ret);
+
+ cache->read_offset = offset;
+ cache->mmap_length = offset + cache->read_buf->used;
+
+ *data_r = data;
+ hdr_data = offset == 0 ? *data_r : NULL;
+ return mail_cache_map_finish(cache, offset,
+ cache->read_buf->used, hdr_data,
+ TRUE, corrupted_r);
+}
+
+static int
+mail_cache_map_full(struct mail_cache *cache, size_t offset, size_t size,
+ const void **data_r, bool *corrupted_r)
+{
+ struct stat st;
+ const void *data;
+ ssize_t ret;
+ size_t orig_size = size;
+
+ *corrupted_r = FALSE;
+
+ if (size == 0)
+ size = sizeof(struct mail_cache_header);
+
+ /* verify offset + size before trying to allocate a huge amount of
+ memory due to them. note that we may be prefetching more than we
+ actually need, so don't fail too early. */
+ if ((size > cache->mmap_length || offset + size > cache->mmap_length) &&
+ (offset > 0 || size > sizeof(struct mail_cache_header))) {
+ if (fstat(cache->fd, &st) < 0) {
+ e_error(cache->index->event,
+ "fstat(%s) failed: %m", cache->filepath);
+ return -1;
+ }
+ cache->last_stat_size = st.st_size;
+ if (offset >= (uoff_t)st.st_size) {
+ *data_r = NULL;
+ return 0;
+ }
+ if (size > (uoff_t)st.st_size - offset)
+ size = st.st_size - offset;
+ }
+
+ cache->remap_counter++;
+ if (cache->map_with_read)
+ return mail_cache_map_with_read(cache, offset, size, data_r,
+ corrupted_r);
+
+ if (cache->file_cache != NULL) {
+ ret = file_cache_read(cache->file_cache, offset, size);
+ if (ret < 0) {
+ /* In case of ESTALE we'll simply fail without error
+ messages. The caller will then just have to
+ fallback to generating the value itself.
+
+ We can't simply reopen the cache file, because
+ using it requires also having updated file
+ offsets. */
+ if (errno != ESTALE)
+ mail_cache_set_syscall_error(cache, "read()");
+ cache->hdr = NULL;
+ return -1;
+ }
+
+ data = file_cache_get_map(cache->file_cache,
+ &cache->mmap_length);
+ *data_r = offset > cache->mmap_length ? NULL :
+ CONST_PTR_OFFSET(data, offset);
+ return mail_cache_map_finish(cache, offset, size,
+ offset == 0 ? data : NULL, TRUE,
+ corrupted_r);
+ }
+
+ if (offset < cache->mmap_length &&
+ size <= cache->mmap_length - offset) {
+ /* already mapped */
+ i_assert(cache->mmap_base != NULL);
+ *data_r = CONST_PTR_OFFSET(cache->mmap_base, offset);
+ if (orig_size > cache->mmap_length - offset) {
+ /* requested offset/size points outside file */
+ return 0;
+ }
+ return 1;
+ }
+
+ if (cache->mmap_base != NULL) {
+ if (munmap(cache->mmap_base, cache->mmap_length) < 0)
+ mail_cache_set_syscall_error(cache, "munmap()");
+ } else {
+ if (cache->fd == -1) {
+ /* unusable, waiting for purging or
+ index is in memory */
+ i_assert(cache->need_purge_file_seq != 0 ||
+ MAIL_INDEX_IS_IN_MEMORY(cache->index));
+ return -1;
+ }
+ }
+
+ /* map the whole file */
+ cache->hdr = NULL;
+ cache->mmap_length = 0;
+ if (cache->read_buf != NULL)
+ buffer_set_used_size(cache->read_buf, 0);
+
+ cache->mmap_base = mmap_ro_file(cache->fd, &cache->mmap_length);
+ if (cache->mmap_base == MAP_FAILED) {
+ cache->mmap_base = NULL;
+ if (ioloop_time != cache->last_mmap_error_time) {
+ cache->last_mmap_error_time = ioloop_time;
+ mail_cache_set_syscall_error(cache, t_strdup_printf(
+ "mmap(size=%zu)", cache->mmap_length));
+ }
+ cache->mmap_length = 0;
+ return -1;
+ }
+ *data_r = offset > cache->mmap_length ? NULL :
+ CONST_PTR_OFFSET(cache->mmap_base, offset);
+ return mail_cache_map_finish(cache, offset, orig_size,
+ cache->mmap_base, FALSE, corrupted_r);
+}
+
+int mail_cache_map(struct mail_cache *cache, size_t offset, size_t size,
+ const void **data_r)
+{
+ i_assert(offset != 0);
+
+ bool corrupted;
+ int ret = mail_cache_map_full(cache, offset, size, data_r, &corrupted);
+ i_assert(!corrupted);
+ return ret;
+}
+
+int mail_cache_map_all(struct mail_cache *cache)
+{
+ const void *data;
+ bool corrupted;
+
+ int ret = mail_cache_map_full(cache, 0, 0, &data, &corrupted);
+ i_assert(ret != 0);
+ if (corrupted) {
+ i_assert(ret == -1);
+ return 0;
+ }
+ return ret < 0 ? -1 : 1;
+}
+
+int mail_cache_open_and_verify(struct mail_cache *cache)
+{
+ int ret;
+
+ if (cache->opened) {
+ if (!MAIL_CACHE_IS_UNUSABLE(cache))
+ return 1;
+ mail_cache_file_close(cache);
+ }
+ if ((ret = mail_cache_try_open(cache)) < 0) {
+ /* I/O error */
+ mail_cache_file_close(cache);
+ return -1;
+ }
+
+ if (ret > 0) {
+ if (mail_cache_header_fields_read(cache) < 0) {
+ /* corrupted */
+ ret = 0;
+ }
+ }
+ if (ret == 0) {
+ /* cache was corrupted and should have been deleted already. */
+ mail_cache_file_close(cache);
+ }
+ return ret;
+}
+
+struct mail_cache *
+mail_cache_open_or_create_path(struct mail_index *index, const char *path)
+{
+ struct mail_cache *cache;
+
+ cache = i_new(struct mail_cache, 1);
+ cache->index = index;
+ cache->fd = -1;
+ cache->filepath = i_strdup(path);
+ cache->field_pool = pool_alloconly_create("Cache fields", 2048);
+ hash_table_create(&cache->field_name_hash, cache->field_pool, 0,
+ strcase_hash, strcasecmp);
+
+ cache->event = event_create(index->event);
+ event_add_category(cache->event, &event_category_mail_cache);
+
+ cache->dotlock_settings.use_excl_lock =
+ (index->flags & MAIL_INDEX_OPEN_FLAG_DOTLOCK_USE_EXCL) != 0;
+ cache->dotlock_settings.nfs_flush =
+ (index->flags & MAIL_INDEX_OPEN_FLAG_NFS_FLUSH) != 0;
+ cache->dotlock_settings.timeout =
+ I_MIN(MAIL_CACHE_LOCK_TIMEOUT, index->set.max_lock_timeout_secs);
+ cache->dotlock_settings.stale_timeout = MAIL_CACHE_LOCK_CHANGE_TIMEOUT;
+
+ if (!MAIL_INDEX_IS_IN_MEMORY(index) &&
+ (index->flags & MAIL_INDEX_OPEN_FLAG_MMAP_DISABLE) != 0)
+ cache->file_cache = file_cache_new_path(-1, cache->filepath);
+ cache->map_with_read =
+ (cache->index->flags & MAIL_INDEX_OPEN_FLAG_SAVEONLY) != 0;
+
+ cache->ext_id =
+ mail_index_ext_register(index, "cache", 0,
+ sizeof(uint32_t), sizeof(uint32_t));
+ mail_index_register_expunge_handler(index, cache->ext_id,
+ mail_cache_expunge_handler);
+ return cache;
+}
+
+struct mail_cache *mail_cache_open_or_create(struct mail_index *index)
+{
+ const char *path = t_strconcat(index->filepath,
+ MAIL_CACHE_FILE_SUFFIX, NULL);
+ return mail_cache_open_or_create_path(index, path);
+}
+
+void mail_cache_free(struct mail_cache **_cache)
+{
+ struct mail_cache *cache = *_cache;
+
+ *_cache = NULL;
+
+ i_assert(cache->views == NULL);
+
+ if (cache->file_cache != NULL)
+ file_cache_free(&cache->file_cache);
+
+ mail_index_unregister_expunge_handler(cache->index, cache->ext_id);
+ mail_cache_file_close(cache);
+
+ buffer_free(&cache->read_buf);
+ hash_table_destroy(&cache->field_name_hash);
+ pool_unref(&cache->field_pool);
+ event_unref(&cache->event);
+ i_free(cache->need_purge_reason);
+ i_free(cache->field_file_map);
+ i_free(cache->file_field_map);
+ i_free(cache->fields);
+ i_free(cache->filepath);
+ i_free(cache);
+}
+
+static int mail_cache_lock_file(struct mail_cache *cache)
+{
+ unsigned int timeout_secs;
+ bool nonblock = FALSE;
+ int ret;
+
+ if (cache->last_lock_failed) {
+ /* previous locking failed. don't waste time waiting on it
+ again, just try once to see if it's available now. */
+ nonblock = TRUE;
+ }
+
+ i_assert(cache->file_lock == NULL);
+ if (cache->index->set.lock_method != FILE_LOCK_METHOD_DOTLOCK) {
+ timeout_secs = I_MIN(MAIL_CACHE_LOCK_TIMEOUT,
+ cache->index->set.max_lock_timeout_secs);
+
+ ret = mail_index_lock_fd(cache->index, cache->filepath,
+ cache->fd, F_WRLCK,
+ nonblock ? 0 : timeout_secs,
+ &cache->file_lock);
+ } else {
+ struct dotlock *dotlock;
+ enum dotlock_create_flags flags =
+ nonblock ? DOTLOCK_CREATE_FLAG_NONBLOCK : 0;
+
+ ret = file_dotlock_create(&cache->dotlock_settings,
+ cache->filepath, flags, &dotlock);
+ if (ret > 0)
+ cache->file_lock = file_lock_from_dotlock(&dotlock);
+ else if (ret < 0) {
+ mail_cache_set_syscall_error(cache,
+ "file_dotlock_create()");
+ }
+ }
+ cache->last_lock_failed = ret <= 0;
+
+ /* don't bother warning if locking failed due to a timeout. since cache
+ updating isn't all that important we're using a very short timeout
+ so it can be triggered sometimes on heavy load */
+ if (ret <= 0)
+ return ret;
+
+ mail_index_flush_read_cache(cache->index, cache->filepath, cache->fd,
+ TRUE);
+ return 1;
+}
+
+static void mail_cache_unlock_file(struct mail_cache *cache)
+{
+ if (cache->file_lock != NULL)
+ file_unlock(&cache->file_lock);
+}
+
+static bool
+mail_cache_verify_reset_id(struct mail_cache *cache, uint32_t *reset_id_r)
+{
+ const struct mail_index_ext *ext;
+ struct mail_index_view *iview;
+ uint32_t reset_id;
+
+ iview = mail_index_view_open(cache->index);
+ ext = mail_index_view_get_ext(iview, cache->ext_id);
+ reset_id = ext == NULL ? 0 : ext->reset_id;
+ mail_index_view_close(&iview);
+
+ *reset_id_r = reset_id;
+ return cache->hdr->file_seq == reset_id;
+}
+
+static int
+mail_cache_sync_wait_index(struct mail_cache *cache, uint32_t *reset_id_r)
+{
+ const char *lock_reason = "cache reset_id sync";
+ uint32_t file_seq;
+ uoff_t file_offset;
+ bool cache_locked = cache->file_lock != NULL;
+ int ret;
+
+ if (cache->index->log_sync_locked)
+ return 0;
+
+ /* Wait for .log file lock, so we can be sure that there is no cache
+ purging going on. (Because it first recreates the cache file,
+ unlocks it and only then writes the changes to the index and
+ releases the .log lock.) To prevent deadlocks, cache file must be
+ locked after the .log, not before. */
+ if (cache_locked)
+ mail_cache_unlock_file(cache);
+ if (mail_transaction_log_sync_lock(cache->index->log, lock_reason,
+ &file_seq, &file_offset) < 0)
+ return -1;
+ /* Lock the cache file as well so we'll get a guaranteed result on
+ whether the reset_id can be synced or if it's already desynced and
+ the cache just needs to be recreated. */
+ ret = -1;
+ while (mail_cache_lock_file(cache) > 0) {
+ /* Locked the current fd, but it may have already been
+ recreated. Reopen and retry if needed. */
+ if (!mail_cache_need_reopen(cache)) {
+ ret = 1;
+ break;
+ }
+ if ((ret = mail_cache_reopen(cache)) <= 0)
+ break;
+ }
+
+ if (ret <= 0)
+ ;
+ else if (mail_index_refresh(cache->index) < 0)
+ ret = -1;
+ else
+ ret = mail_cache_verify_reset_id(cache, reset_id_r) ? 1 : 0;
+ mail_transaction_log_sync_unlock(cache->index->log, lock_reason);
+ if (ret <= 0 || !cache_locked)
+ mail_cache_unlock_file(cache);
+ return ret;
+}
+
+int mail_cache_sync_reset_id(struct mail_cache *cache)
+{
+ uint32_t reset_id;
+ int ret;
+
+ /* verify that the index reset_id matches the cache's file_seq */
+ if (mail_cache_verify_reset_id(cache, &reset_id))
+ return 1;
+
+ /* Mismatch. See if we can get it synced. */
+ if (cache->index->mapping) {
+ /* Syncing is already locked, and we're in the middle of
+ mapping the index. The cache is unusable. */
+ i_assert(cache->index->log_sync_locked);
+ mail_cache_set_corrupted(cache, "reset_id mismatch during sync");
+ return 0;
+ }
+
+ /* See if reset_id changes after refreshing the index. */
+ if (mail_index_refresh(cache->index) < 0)
+ return -1;
+ if (mail_cache_verify_reset_id(cache, &reset_id))
+ return 1;
+
+ /* Use locking to wait for a potential cache purging to finish.
+ If that didn't work either, the cache is corrupted or lost. */
+ ret = mail_cache_sync_wait_index(cache, &reset_id);
+ if (ret == 0 && cache->fd != -1 && reset_id != 0) {
+ mail_cache_set_corrupted(cache,
+ "reset_id mismatch even after locking "
+ "(file_seq=%u != reset_id=%u)",
+ cache->hdr == NULL ? 0 : cache->hdr->file_seq,
+ reset_id);
+ }
+ return ret;
+}
+
+int mail_cache_lock(struct mail_cache *cache)
+{
+ int ret;
+
+ i_assert(!cache->locked);
+ /* the only reason why we might be in here while mapping the index is
+ if we're coming from mail_cache_expunge_count() while syncing the
+ index. */
+ i_assert(!cache->index->mapping || cache->index->log_sync_locked);
+
+ if (MAIL_INDEX_IS_IN_MEMORY(cache->index) ||
+ cache->index->readonly)
+ return 0;
+
+ /* Make sure at least some cache file is opened. Usually it's the
+ latest one, so delay until it's locked to check whether a newer
+ cache file exists. */
+ if ((ret = mail_cache_open_and_verify(cache)) < 0)
+ return -1;
+ if (ret == 0) {
+ /* Cache doesn't exist or it was just found to be corrupted and
+ was unlinked. Cache purging will create it back. */
+ return 0;
+ }
+
+ for (;;) {
+ if (mail_cache_lock_file(cache) <= 0)
+ return -1;
+ if (!mail_cache_need_reopen(cache)) {
+ /* locked the latest file */
+ break;
+ }
+ if ((ret = mail_cache_reopen(cache)) <= 0) {
+ i_assert(cache->file_lock == NULL);
+ return ret;
+ }
+ i_assert(cache->file_lock == NULL);
+ /* okay, so it was just purged. try again. */
+ }
+
+ if ((ret = mail_cache_sync_reset_id(cache)) <= 0) {
+ mail_cache_unlock_file(cache);
+ return ret;
+ }
+ i_assert(cache->file_lock != NULL);
+
+ /* successfully locked - make sure our header is up to date */
+ cache->locked = TRUE;
+ cache->hdr_modified = FALSE;
+
+ if (cache->file_cache != NULL) {
+ file_cache_invalidate(cache->file_cache, 0,
+ sizeof(struct mail_cache_header));
+ }
+ if (cache->read_buf != NULL)
+ buffer_set_used_size(cache->read_buf, 0);
+ if ((ret = mail_cache_map_all(cache)) <= 0) {
+ mail_cache_unlock(cache);
+ return ret;
+ }
+ cache->hdr_copy = *cache->hdr;
+ return 1;
+}
+
+int mail_cache_flush_and_unlock(struct mail_cache *cache)
+{
+ int ret = 0;
+
+ i_assert(cache->locked);
+
+ if (cache->field_header_write_pending)
+ ret = mail_cache_header_fields_update(cache);
+
+ /* Cache may become unusable during for various reasons, e.g.
+ mail_cache_map(). Also the above mail_cache_header_fields_update()
+ call can make it unusable, so check this after it. */
+ if (MAIL_CACHE_IS_UNUSABLE(cache)) {
+ mail_cache_unlock(cache);
+ return -1;
+ }
+
+ if (cache->hdr_modified) {
+ cache->hdr_modified = FALSE;
+ if (mail_cache_write(cache, &cache->hdr_copy,
+ sizeof(cache->hdr_copy), 0) < 0)
+ ret = -1;
+ cache->hdr_ro_copy = cache->hdr_copy;
+ mail_cache_update_need_purge(cache);
+ }
+
+ mail_cache_unlock(cache);
+ return ret;
+}
+
+void mail_cache_unlock(struct mail_cache *cache)
+{
+ i_assert(cache->locked);
+
+ if (MAIL_CACHE_IS_UNUSABLE(cache)) {
+ /* we found it to be broken during the lock. just clean up. */
+ cache->hdr_modified = FALSE;
+ } else if (cache->index->set.fsync_mode == FSYNC_MODE_ALWAYS) {
+ if (fdatasync(cache->fd) < 0)
+ mail_cache_set_syscall_error(cache, "fdatasync()");
+ }
+
+ cache->locked = FALSE;
+ mail_cache_unlock_file(cache);
+}
+
+int mail_cache_write(struct mail_cache *cache, const void *data, size_t size,
+ uoff_t offset)
+{
+ i_assert(cache->locked);
+
+ if (pwrite_full(cache->fd, data, size, offset) < 0) {
+ mail_cache_set_syscall_error(cache, "pwrite_full()");
+ return -1;
+ }
+
+ if (cache->file_cache != NULL)
+ file_cache_write(cache->file_cache, data, size, offset);
+ if (cache->read_buf != NULL)
+ buffer_set_used_size(cache->read_buf, 0);
+ return 0;
+}
+
+int mail_cache_append(struct mail_cache *cache, const void *data, size_t size,
+ uint32_t *offset)
+{
+ struct stat st;
+
+ if (*offset == 0) {
+ if (fstat(cache->fd, &st) < 0) {
+ if (!ESTALE_FSTAT(errno))
+ mail_cache_set_syscall_error(cache, "fstat()");
+ return -1;
+ }
+ cache->last_stat_size = st.st_size;
+ if ((uoff_t)st.st_size > cache->index->optimization_set.cache.max_size) {
+ mail_cache_set_corrupted(cache, "Cache file too large");
+ return -1;
+ }
+ *offset = st.st_size;
+ }
+ if (*offset >= cache->index->optimization_set.cache.max_size ||
+ cache->index->optimization_set.cache.max_size - *offset < size) {
+ mail_cache_set_corrupted(cache, "Cache file too large");
+ return -1;
+ }
+ if (mail_cache_write(cache, data, size, *offset) < 0)
+ return -1;
+ return 0;
+}
+
+bool mail_cache_exists(struct mail_cache *cache)
+{
+ return !MAIL_CACHE_IS_UNUSABLE(cache);
+}
+
+struct mail_cache_view *
+mail_cache_view_open(struct mail_cache *cache, struct mail_index_view *iview)
+{
+ struct mail_cache_view *view;
+
+ view = i_new(struct mail_cache_view, 1);
+ view->cache = cache;
+ view->view = iview;
+ view->cached_exists_buf =
+ buffer_create_dynamic(default_pool,
+ cache->file_fields_count + 10);
+ DLLIST_PREPEND(&cache->views, view);
+ return view;
+}
+
+void mail_cache_view_close(struct mail_cache_view **_view)
+{
+ struct mail_cache_view *view = *_view;
+
+ i_assert(view->trans_view == NULL);
+
+ *_view = NULL;
+ if (view->cache->field_header_write_pending &&
+ !view->cache->purging)
+ (void)mail_cache_header_fields_update(view->cache);
+
+ DLLIST_REMOVE(&view->cache->views, view);
+ buffer_free(&view->cached_exists_buf);
+ i_free(view);
+}
+
+void mail_cache_view_update_cache_decisions(struct mail_cache_view *view,
+ bool update)
+{
+ view->no_decision_updates = !update;
+}
+
+uint32_t mail_cache_get_first_new_seq(struct mail_index_view *view)
+{
+ const struct mail_index_header *idx_hdr;
+ uint32_t first_new_seq, message_count;
+
+ idx_hdr = mail_index_get_header(view);
+ if (idx_hdr->day_first_uid[7] == 0)
+ return 1;
+
+ if (!mail_index_lookup_seq_range(view, idx_hdr->day_first_uid[7],
+ (uint32_t)-1, &first_new_seq,
+ &message_count)) {
+ /* all messages are too old */
+ return idx_hdr->messages_count+1;
+ }
+ return first_new_seq;
+}
diff --git a/src/lib-index/mail-cache.h b/src/lib-index/mail-cache.h
new file mode 100644
index 0000000..09fde29
--- /dev/null
+++ b/src/lib-index/mail-cache.h
@@ -0,0 +1,193 @@
+#ifndef MAIL_CACHE_H
+#define MAIL_CACHE_H
+
+#include "mail-index.h"
+
+#define MAIL_CACHE_FILE_SUFFIX ".cache"
+
+struct mail_cache;
+struct mail_cache_view;
+struct mail_cache_transaction_ctx;
+
+enum mail_cache_decision_type {
+ /* Not needed currently */
+ MAIL_CACHE_DECISION_NO = 0x00,
+ /* Needed only for new mails. Drop when purging. */
+ MAIL_CACHE_DECISION_TEMP = 0x01,
+ /* Needed. */
+ MAIL_CACHE_DECISION_YES = 0x02,
+
+ /* This decision has been forced manually, don't change it. */
+ MAIL_CACHE_DECISION_FORCED = 0x80
+};
+
+enum mail_cache_field_type {
+ /* Fixed size cache field. The size is specified only in the cache
+ field header, not separately for each record. */
+ MAIL_CACHE_FIELD_FIXED_SIZE,
+ /* Variable sized binary data. */
+ MAIL_CACHE_FIELD_VARIABLE_SIZE,
+ /* Variable sized string. There is no difference internally to how
+ MAIL_CACHE_FIELD_VARIABLE_SIZE is handled, but it helps at least
+ "doveadm dump" to know whether to hex-encode the output. */
+ MAIL_CACHE_FIELD_STRING,
+ /* A fixed size bitmask field. It's possible to add new bits by
+ updating this field. All the added fields are ORed together. */
+ MAIL_CACHE_FIELD_BITMASK,
+ /* Variable sized message header. The data begins with a 0-terminated
+ uint32_t line_numbers[]. The line number exists only for each
+ header, header continuation lines in multiline headers don't get
+ listed. After the line numbers comes the list of headers, including
+ the "header-name: " prefix for each line, LFs and the TABs or spaces
+ for continued lines. */
+ MAIL_CACHE_FIELD_HEADER,
+
+ MAIL_CACHE_FIELD_COUNT
+};
+
+struct mail_cache_field {
+ /* Unique name for the cache field. The field name doesn't matter
+ internally. */
+ const char *name;
+ /* Field index name. Used to optimize accessing the cache field. */
+ unsigned int idx;
+
+ /* Type of the field */
+ enum mail_cache_field_type type;
+ /* Size of the field, if it's a fixed size type. */
+ unsigned int field_size;
+ /* Current caching decision */
+ enum mail_cache_decision_type decision;
+ /* Timestamp when the cache field was last intentionally read (e.g.
+ by an IMAP client). Saving new mails doesn't update this field.
+ This is used to track when an unaccessed field should be dropped. */
+ time_t last_used;
+};
+
+struct mail_cache *mail_cache_open_or_create(struct mail_index *index);
+struct mail_cache *
+mail_cache_open_or_create_path(struct mail_index *index, const char *path);
+void mail_cache_free(struct mail_cache **cache);
+
+/* Register fields. fields[].idx is updated to contain field index.
+ If field already exists and its caching decision is NO, the decision is
+ updated to the input field's decision. */
+void mail_cache_register_fields(struct mail_cache *cache,
+ struct mail_cache_field *fields,
+ unsigned int fields_count);
+/* Returns registered field index, or UINT_MAX if not found. */
+unsigned int
+mail_cache_register_lookup(struct mail_cache *cache, const char *name);
+/* Returns specified field */
+const struct mail_cache_field *
+mail_cache_register_get_field(struct mail_cache *cache, unsigned int field_idx);
+/* Returns a list of all registered fields */
+struct mail_cache_field *
+mail_cache_register_get_list(struct mail_cache *cache, pool_t pool,
+ unsigned int *count_r);
+
+/* Returns TRUE if cache should be purged. */
+bool mail_cache_need_purge(struct mail_cache *cache, const char **reason_r);
+/* Set cache file to be purged later. */
+void mail_cache_purge_later(struct mail_cache *cache, const char *reason);
+/* Don't try to purge the cache file later after all. */
+void mail_cache_purge_later_reset(struct mail_cache *cache);
+/* Purge cache file. Offsets are updated to given transaction.
+ The transaction log must already be exclusively locked.
+
+ The cache purging is done only if the current cache file's file_seq
+ matches purge_file_seq. The idea is that purging isn't done if
+ another process had just purged it. 0 means the cache file is created
+ only if it didn't already exist. (uint32_t)-1 means that purging is
+ done always regardless of file_seq. */
+int mail_cache_purge_with_trans(struct mail_cache *cache,
+ struct mail_index_transaction *trans,
+ uint32_t purge_file_seq, const char *reason);
+int mail_cache_purge(struct mail_cache *cache, uint32_t purge_file_seq,
+ const char *reason);
+/* Returns TRUE if there is at least something in the cache. */
+bool mail_cache_exists(struct mail_cache *cache);
+/* Open and read cache header. Returns 1 if ok, 0 if cache doesn't exist or it
+ was corrupted and just got deleted, -1 if I/O error. */
+int mail_cache_open_and_verify(struct mail_cache *cache);
+
+struct mail_cache_view *
+mail_cache_view_open(struct mail_cache *cache, struct mail_index_view *iview);
+void mail_cache_view_close(struct mail_cache_view **view);
+
+/* Normally cache decisions are updated on lookup/add. Use this function to
+ enable/disable this (useful for precaching data). */
+void mail_cache_view_update_cache_decisions(struct mail_cache_view *view,
+ bool update);
+
+/* Copy caching decisions. This is expected to be called only for a newly
+ created empty mailbox. */
+int mail_cache_decisions_copy(struct mail_cache *src, struct mail_cache *dst);
+
+/* Get index transaction specific cache transaction. */
+struct mail_cache_transaction_ctx *
+mail_cache_get_transaction(struct mail_cache_view *view,
+ struct mail_index_transaction *t);
+
+void mail_cache_transaction_reset(struct mail_cache_transaction_ctx *ctx);
+int mail_cache_transaction_commit(struct mail_cache_transaction_ctx **ctx);
+void mail_cache_transaction_rollback(struct mail_cache_transaction_ctx **ctx);
+
+/* Add new field to given record. Updates are not allowed. Fixed size fields
+ must be exactly the expected size. */
+void mail_cache_add(struct mail_cache_transaction_ctx *ctx, uint32_t seq,
+ unsigned int field_idx, const void *data, size_t data_size);
+/* Returns TRUE if field is wanted to be added and it doesn't already exist.
+ If current caching decisions say not to cache this field, FALSE is returned.
+ If seq is 0, the existence isn't checked. */
+bool mail_cache_field_want_add(struct mail_cache_transaction_ctx *ctx,
+ uint32_t seq, unsigned int field_idx);
+/* Like mail_cache_field_want_add(), but in caching decisions FALSE is
+ returned only if the decision is a forced no. */
+bool mail_cache_field_can_add(struct mail_cache_transaction_ctx *ctx,
+ uint32_t seq, unsigned int field_idx);
+/* Notify cache that the mail is now closed. Any records added with
+ mail_cache_add() are unlikely to be required again. This mainly tells
+ INDEX=MEMORY that it can free up the memory used by the mail. */
+void mail_cache_close_mail(struct mail_cache_transaction_ctx *ctx,
+ uint32_t seq);
+
+/* Returns 1 if field exists, 0 if not, -1 if error. */
+int mail_cache_field_exists(struct mail_cache_view *view, uint32_t seq,
+ unsigned int field_idx);
+/* Returns TRUE if something is cached for the message, FALSE if not. */
+bool mail_cache_field_exists_any(struct mail_cache_view *view, uint32_t seq);
+/* Returns current caching decision for given field. */
+enum mail_cache_decision_type
+mail_cache_field_get_decision(struct mail_cache *cache, unsigned int field_idx);
+/* Notify the decision handling code when field is committed to cache.
+ If this is the first time the field is added to cache, its caching decision
+ is updated to TEMP. */
+void mail_cache_decision_add(struct mail_cache_view *view, uint32_t seq,
+ unsigned int field);
+
+/* Set data_r and size_r to point to wanted field in cache file.
+ Returns 1 if field was found, 0 if not, -1 if error. */
+int mail_cache_lookup_field(struct mail_cache_view *view, buffer_t *dest_buf,
+ uint32_t seq, unsigned int field_idx);
+
+/* Return specified cached headers. Returns 1 if all fields were found,
+ 0 if not, -1 if error. dest is updated only if all fields were found. */
+int mail_cache_lookup_headers(struct mail_cache_view *view, string_t *dest,
+ uint32_t seq, const unsigned int field_idxs[],
+ unsigned int fields_count);
+
+/* "Error in index cache file %s: ...". */
+void mail_cache_set_corrupted(struct mail_cache *cache, const char *fmt, ...)
+ ATTR_FORMAT(2, 3) ATTR_COLD;
+void mail_cache_set_seq_corrupted_reason(struct mail_cache_view *cache_view,
+ uint32_t seq, const char *reason)
+ ATTR_COLD;
+
+/* Returns human-readable reason for why a cached field is missing for
+ the specified mail. This is mainly for debugging purposes, so the exact
+ field doesn't matter here. */
+const char *
+mail_cache_get_missing_reason(struct mail_cache_view *view, uint32_t seq);
+
+#endif
diff --git a/src/lib-index/mail-index-alloc-cache.c b/src/lib-index/mail-index-alloc-cache.c
new file mode 100644
index 0000000..fe52754
--- /dev/null
+++ b/src/lib-index/mail-index-alloc-cache.c
@@ -0,0 +1,315 @@
+/* Copyright (c) 2010-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "ioloop.h"
+#include "module-context.h"
+#include "eacces-error.h"
+#include "mail-index-private.h"
+#include "mail-index-alloc-cache.h"
+
+#define MAIL_INDEX_ALLOC_CACHE_CONTEXT(obj) \
+ MODULE_CONTEXT(obj, mail_index_alloc_cache_index_module)
+
+/* How many seconds to keep index opened for reuse after it's been closed */
+#define INDEX_CACHE_TIMEOUT 10
+/* How many closed indexes to keep */
+#define INDEX_CACHE_MAX 3
+
+struct mail_index_alloc_cache_list {
+ union mail_index_module_context module_ctx;
+ struct mail_index_alloc_cache_list *next;
+
+ struct mail_index *index;
+ char *mailbox_path;
+ int refcount;
+ bool referenced;
+
+ dev_t index_dir_dev;
+ ino_t index_dir_ino;
+
+ time_t destroy_time;
+};
+
+static MODULE_CONTEXT_DEFINE_INIT(mail_index_alloc_cache_index_module,
+ &mail_index_module_register);
+static struct mail_index_alloc_cache_list *indexes = NULL;
+static unsigned int indexes_cache_references_count = 0;
+static struct timeout *to_index = NULL;
+
+static struct mail_index_alloc_cache_list *
+mail_index_alloc_cache_add(struct mail_index *index,
+ const char *mailbox_path, struct stat *st)
+{
+ struct mail_index_alloc_cache_list *list;
+
+ list = i_new(struct mail_index_alloc_cache_list, 1);
+ list->refcount = 1;
+ list->index = index;
+
+ list->mailbox_path = i_strdup(mailbox_path);
+ list->index_dir_dev = st->st_dev;
+ list->index_dir_ino = st->st_ino;
+
+ list->next = indexes;
+ indexes = list;
+
+ MODULE_CONTEXT_SET(index, mail_index_alloc_cache_index_module, list);
+ return list;
+}
+
+static void
+mail_index_alloc_cache_list_unref(struct mail_index_alloc_cache_list *list)
+{
+ i_assert(list->referenced);
+ i_assert(indexes_cache_references_count > 0);
+
+ indexes_cache_references_count--;
+ mail_index_close(list->index);
+ list->referenced = FALSE;
+}
+
+static void
+mail_index_alloc_cache_list_free(struct mail_index_alloc_cache_list *list)
+{
+ i_assert(list->refcount == 0);
+
+ if (list->referenced)
+ mail_index_alloc_cache_list_unref(list);
+ mail_index_free(&list->index);
+ i_free(list->mailbox_path);
+ i_free(list);
+}
+
+static struct mail_index_alloc_cache_list *
+mail_index_alloc_cache_find_and_expire(const char *mailbox_path,
+ const char *index_dir,
+ const struct stat *index_st)
+{
+ struct mail_index_alloc_cache_list **indexp, *rec, *match;
+ unsigned int destroy_count;
+ struct stat st;
+
+ destroy_count = 0; match = NULL;
+ for (indexp = &indexes; *indexp != NULL;) {
+ rec = *indexp;
+
+ if (match != NULL) {
+ /* already found the index. we're just going through
+ the rest of them to drop 0 refcounts */
+ } else if (rec->refcount == 0 && rec->index->open_count == 0) {
+ /* index is already closed. don't even try to
+ reuse it. */
+ } else if (index_dir != NULL && rec->index_dir_ino != 0) {
+ if (index_st->st_ino == rec->index_dir_ino &&
+ CMP_DEV_T(index_st->st_dev, rec->index_dir_dev)) {
+ /* make sure the directory still exists.
+ it might have been renamed and we're trying
+ to access it via its new path now. */
+ if (stat(rec->index->dir, &st) < 0 ||
+ st.st_ino != index_st->st_ino ||
+ !CMP_DEV_T(st.st_dev, index_st->st_dev))
+ rec->destroy_time = 0;
+ else
+ match = rec;
+ }
+ } else if (mailbox_path != NULL && rec->mailbox_path != NULL &&
+ index_dir == NULL && rec->index_dir_ino == 0) {
+ if (strcmp(mailbox_path, rec->mailbox_path) == 0)
+ match = rec;
+ }
+
+ if (rec->refcount == 0 && rec != match) {
+ if (rec->destroy_time <= ioloop_time ||
+ destroy_count >= INDEX_CACHE_MAX) {
+ *indexp = rec->next;
+ mail_index_alloc_cache_list_free(rec);
+ continue;
+ } else {
+ destroy_count++;
+ }
+ }
+
+ indexp = &(*indexp)->next;
+ }
+ return match;
+}
+
+struct mail_index *
+mail_index_alloc_cache_get(struct event *parent_event, const char *mailbox_path,
+ const char *index_dir, const char *prefix)
+{
+ struct mail_index_alloc_cache_list *match;
+ struct stat st;
+
+ /* compare index_dir inodes so we don't break even with symlinks.
+ if index_dir doesn't exist yet or if using in-memory indexes, just
+ compare mailbox paths */
+ i_zero(&st);
+ if (index_dir == NULL) {
+ /* in-memory indexes */
+ } else if (stat(index_dir, &st) < 0) {
+ if (errno == ENOENT) {
+ /* it'll be created later */
+ } else if (errno == EACCES) {
+ e_error(parent_event, "%s",
+ eacces_error_get("stat", index_dir));
+ } else {
+ e_error(parent_event, "stat(%s) failed: %m", index_dir);
+ }
+ }
+
+ match = mail_index_alloc_cache_find_and_expire(mailbox_path,
+ index_dir, &st);
+ if (match == NULL) {
+ struct mail_index *index =
+ mail_index_alloc(parent_event, index_dir, prefix);
+ match = mail_index_alloc_cache_add(index, mailbox_path, &st);
+ } else {
+ match->refcount++;
+ }
+ i_assert(match->index != NULL);
+ return match->index;
+}
+
+struct mail_index *
+mail_index_alloc_cache_find(const char *index_dir)
+{
+ struct mail_index_alloc_cache_list *rec;
+ struct stat st;
+
+ if (stat(index_dir, &st) < 0) {
+ if (errno != ENOENT)
+ i_error("stat(%s) failed: %m", index_dir);
+ return NULL;
+ }
+
+ for (rec = indexes; rec != NULL; rec = rec->next) {
+ if (st.st_ino == rec->index_dir_ino &&
+ CMP_DEV_T(st.st_dev, rec->index_dir_dev))
+ return rec->index;
+ }
+ return NULL;
+}
+
+static bool destroy_unrefed(unsigned int min_destroy_count)
+{
+ struct mail_index_alloc_cache_list **list, *rec;
+ bool destroyed = FALSE;
+ bool seen_ref0 = FALSE;
+
+ for (list = &indexes; *list != NULL;) {
+ rec = *list;
+
+ if (rec->refcount == 0 &&
+ (min_destroy_count > 0 || rec->destroy_time <= ioloop_time)) {
+ *list = rec->next;
+ destroyed = TRUE;
+ mail_index_alloc_cache_list_free(rec);
+ if (min_destroy_count > 0)
+ min_destroy_count--;
+ } else {
+ if (rec->refcount == 0)
+ seen_ref0 = TRUE;
+ if (min_destroy_count > 0 &&
+ rec->index->open_count == 1 &&
+ rec->referenced) {
+ /* we're the only one keeping this index open.
+ we might be here, because the caller is
+ deleting this mailbox and wants its indexes
+ to be closed. so close it. */
+ destroyed = TRUE;
+ mail_index_alloc_cache_list_unref(rec);
+ }
+ list = &(*list)->next;
+ }
+ }
+
+ if (!seen_ref0 && to_index != NULL)
+ timeout_remove(&to_index);
+ return destroyed;
+}
+
+static void ATTR_NULL(1)
+index_removal_timeout(void *context ATTR_UNUSED)
+{
+ destroy_unrefed(0);
+}
+
+void mail_index_alloc_cache_unref(struct mail_index **_index)
+{
+ struct mail_index *index = *_index;
+ struct mail_index_alloc_cache_list *list, **listp;
+
+ *_index = NULL;
+ list = NULL;
+ for (listp = &indexes; *listp != NULL; listp = &(*listp)->next) {
+ if ((*listp)->index == index) {
+ list = *listp;
+ break;
+ }
+ }
+
+ i_assert(list != NULL);
+ i_assert(list->refcount > 0);
+
+ list->refcount--;
+ list->destroy_time = ioloop_time + INDEX_CACHE_TIMEOUT;
+
+ if (list->refcount == 0 && index->open_count == 0) {
+ /* index was already closed. don't even try to cache it. */
+ *listp = list->next;
+ mail_index_alloc_cache_list_free(list);
+ } else if (to_index == NULL) {
+ /* Add to root ioloop in case we got here from an inner
+ ioloop which gets destroyed too early. */
+ to_index = timeout_add_to(io_loop_get_root(),
+ INDEX_CACHE_TIMEOUT*1000/2,
+ index_removal_timeout, NULL);
+ }
+}
+
+void mail_index_alloc_cache_destroy_unrefed(void)
+{
+ destroy_unrefed(UINT_MAX);
+}
+
+void mail_index_alloc_cache_index_opened(struct mail_index *index)
+{
+ struct mail_index_alloc_cache_list *list =
+ MAIL_INDEX_ALLOC_CACHE_CONTEXT(index);
+ struct stat st;
+
+ if (list != NULL && list->index_dir_ino == 0 &&
+ !MAIL_INDEX_IS_IN_MEMORY(index)) {
+ /* newly created index directory. update its stat. */
+ if (stat(index->dir, &st) == 0) {
+ list->index_dir_ino = st.st_ino;
+ list->index_dir_dev = st.st_dev;
+ }
+ }
+}
+
+void mail_index_alloc_cache_index_closing(struct mail_index *index)
+{
+ struct mail_index_alloc_cache_list *list =
+ MAIL_INDEX_ALLOC_CACHE_CONTEXT(index);
+
+ i_assert(index->open_count > 0);
+ if (index->open_count > 1 || list == NULL)
+ return;
+
+ if (list->referenced) {
+ /* we're closing our referenced index */
+ return;
+ }
+ while (indexes_cache_references_count > INDEX_CACHE_MAX) {
+ if (!destroy_unrefed(1)) {
+ /* our cache is full already, don't keep more */
+ return;
+ }
+ }
+ /* keep the index referenced for caching */
+ indexes_cache_references_count++;
+ list->referenced = TRUE;
+ index->open_count++;
+}
diff --git a/src/lib-index/mail-index-alloc-cache.h b/src/lib-index/mail-index-alloc-cache.h
new file mode 100644
index 0000000..5f28563
--- /dev/null
+++ b/src/lib-index/mail-index-alloc-cache.h
@@ -0,0 +1,20 @@
+#ifndef MAIL_INDEX_ALLOC_CACHE_H
+#define MAIL_INDEX_ALLOC_CACHE_H
+
+/* If using in-memory indexes, give index_dir=NULL. */
+struct mail_index * ATTR_NULL(1, 2)
+mail_index_alloc_cache_get(struct event *parent_event, const char *mailbox_path,
+ const char *index_dir, const char *prefix);
+void mail_index_alloc_cache_unref(struct mail_index **index);
+
+/* Find an existing already opened index from a given index directory. */
+struct mail_index *
+mail_index_alloc_cache_find(const char *index_dir);
+
+void mail_index_alloc_cache_destroy_unrefed(void);
+
+/* internal: */
+void mail_index_alloc_cache_index_opened(struct mail_index *index);
+void mail_index_alloc_cache_index_closing(struct mail_index *index);
+
+#endif
diff --git a/src/lib-index/mail-index-dummy-view.c b/src/lib-index/mail-index-dummy-view.c
new file mode 100644
index 0000000..ea69377
--- /dev/null
+++ b/src/lib-index/mail-index-dummy-view.c
@@ -0,0 +1,47 @@
+/* Copyright (c) 2004-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "mail-index-private.h"
+#include "mail-index-view-private.h"
+
+static void dummy_view_close(struct mail_index_view *view ATTR_UNUSED)
+{
+ i_assert(view->refcount == 0);
+
+ array_free(&view->module_contexts);
+ i_free(view);
+}
+
+static uint32_t
+dummy_view_get_message_count(struct mail_index_view *view ATTR_UNUSED)
+{
+ return (uint32_t)-3;
+}
+
+static struct mail_index_view_vfuncs dummy_view_vfuncs = {
+ dummy_view_close,
+ dummy_view_get_message_count,
+ NULL,
+ NULL,
+ NULL,
+ NULL,
+ NULL,
+ NULL,
+ NULL,
+ NULL,
+ NULL
+};
+
+struct mail_index_view *mail_index_dummy_view_open(struct mail_index *index)
+{
+ struct mail_index_view *view;
+
+ view = i_new(struct mail_index_view, 1);
+ view->refcount = 1;
+ view->v = dummy_view_vfuncs;
+ view->index = index;
+ i_array_init(&view->module_contexts,
+ I_MIN(5, mail_index_module_register.id));
+ return view;
+}
diff --git a/src/lib-index/mail-index-fsck.c b/src/lib-index/mail-index-fsck.c
new file mode 100644
index 0000000..6636edf
--- /dev/null
+++ b/src/lib-index/mail-index-fsck.c
@@ -0,0 +1,495 @@
+/* Copyright (c) 2004-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "ioloop.h"
+#include "array.h"
+#include "mail-index-private.h"
+#include "mail-transaction-log-private.h"
+
+static void mail_index_fsck_error(struct mail_index *index,
+ const char *fmt, ...) ATTR_FORMAT(2, 3);
+static void mail_index_fsck_error(struct mail_index *index,
+ const char *fmt, ...)
+{
+ va_list va;
+
+ va_start(va, fmt);
+ mail_index_set_error(index, "Fixed index file %s: %s",
+ index->filepath, t_strdup_vprintf(fmt, va));
+ va_end(va);
+}
+
+#define CHECK(field, oper) \
+ if (hdr->field oper map->hdr.field) { \
+ mail_index_fsck_error(index, #field" %u -> %u", \
+ map->hdr.field, hdr->field); \
+ }
+
+static void
+mail_index_fsck_log_pos(struct mail_index *index, struct mail_index_map *map,
+ struct mail_index_header *hdr)
+{
+ unsigned int hdr_size = index->log->head->hdr.hdr_size;
+ uint32_t file_seq;
+ uoff_t file_offset;
+
+ mail_transaction_log_get_head(index->log, &file_seq, &file_offset);
+ if (hdr->log_file_seq < file_seq) {
+ /* index's log_file_seq is too old. move it to log head. */
+ hdr->log_file_head_offset = hdr->log_file_tail_offset =
+ sizeof(struct mail_transaction_log_header);
+ } else if (hdr->log_file_seq == file_seq) {
+ /* index's log_file_seq matches the current log. make sure the
+ offsets are valid. */
+ if (hdr->log_file_head_offset > file_offset)
+ hdr->log_file_head_offset = file_offset;
+ else if (hdr->log_file_head_offset < hdr_size)
+ hdr->log_file_head_offset = hdr_size;
+
+ if (hdr->log_file_tail_offset > hdr->log_file_head_offset)
+ hdr->log_file_tail_offset = hdr->log_file_head_offset;
+ else if (hdr->log_file_tail_offset != 0 &&
+ hdr->log_file_tail_offset < hdr_size)
+ hdr->log_file_tail_offset = hdr_size;
+ } else {
+ /* index's log_file_seq is newer than exists. move it to
+ end of the current log head. */
+ hdr->log_file_head_offset = hdr->log_file_tail_offset =
+ file_offset;
+ }
+ hdr->log_file_seq = file_seq;
+
+ CHECK(log_file_seq, !=);
+ if (hdr->log_file_seq == map->hdr.log_file_seq) {
+ /* don't bother complaining about these if file changed too */
+ CHECK(log_file_head_offset, !=);
+ CHECK(log_file_tail_offset, !=);
+ }
+}
+
+static void
+mail_index_fsck_header(struct mail_index *index, struct mail_index_map *map,
+ struct mail_index_header *hdr)
+{
+ /* mail_index_map_check_header() has already checked that the index
+ isn't completely broken. */
+ if (hdr->uid_validity == 0 && hdr->next_uid != 1)
+ hdr->uid_validity = ioloop_time;
+
+ if (index->log->head != NULL)
+ mail_index_fsck_log_pos(index, map, hdr);
+}
+
+static bool
+array_has_name(const ARRAY_TYPE(const_string) *names, const char *name)
+{
+ const char *arr_name;
+
+ array_foreach_elem(names, arr_name) {
+ if (strcmp(arr_name, name) == 0)
+ return TRUE;
+ }
+ return FALSE;
+}
+
+static unsigned int
+mail_index_fsck_find_keyword_count(struct mail_index_map *map,
+ const struct mail_index_ext_header *ext_hdr)
+{
+ const struct mail_index_record *rec;
+ const uint8_t *kw;
+ unsigned int r, i, j, cur, max = 0, kw_pos, kw_size;
+
+ kw_pos = ext_hdr->record_offset;
+ kw_size = ext_hdr->record_size;
+
+ rec = map->rec_map->records;
+ for (r = 0; r < map->rec_map->records_count; r++) {
+ kw = CONST_PTR_OFFSET(rec, kw_pos);
+ for (i = cur = 0; i < kw_size; i++) {
+ if (kw[i] != 0) {
+ for (j = 0; j < 8; j++) {
+ if ((kw[i] & (1 << j)) != 0)
+ cur = i * 8 + j + 1;
+ }
+ }
+ }
+ if (cur > max) {
+ max = cur;
+ if (max == kw_size*8)
+ return max;
+ }
+ rec = CONST_PTR_OFFSET(rec, map->hdr.record_size);
+ }
+ return max;
+}
+
+static bool
+keyword_name_is_valid(const char *buffer, unsigned int pos, unsigned int size)
+{
+ for (; pos < size; pos++) {
+ if (buffer[pos] == '\0')
+ return TRUE;
+ if (((unsigned char)buffer[pos] & 0x7f) < 32) {
+ /* control characters aren't valid */
+ return FALSE;
+ }
+ }
+ return FALSE;
+}
+
+static void
+mail_index_fsck_keywords(struct mail_index *index, struct mail_index_map *map,
+ struct mail_index_header *hdr,
+ const struct mail_index_ext_header *ext_hdr,
+ unsigned int ext_offset, unsigned int *offset_p)
+{
+ const struct mail_index_keyword_header *kw_hdr;
+ struct mail_index_keyword_header *new_kw_hdr;
+ const struct mail_index_keyword_header_rec *kw_rec;
+ struct mail_index_keyword_header_rec new_kw_rec;
+ const char *name, *name_buffer, **name_array;
+ unsigned int i, j, name_pos, name_size, rec_pos, hdr_offset, diff;
+ unsigned int changed_count, keywords_count, name_base_pos;
+ ARRAY_TYPE(const_string) names;
+ buffer_t *dest;
+ bool changed = FALSE;
+
+ hdr_offset = ext_offset +
+ mail_index_map_ext_hdr_offset(sizeof(MAIL_INDEX_EXT_KEYWORDS)-1);
+ kw_hdr = MAIL_INDEX_MAP_HDR_OFFSET(map, hdr_offset);
+ keywords_count = kw_hdr->keywords_count;
+
+ kw_rec = (const void *)(kw_hdr + 1);
+ name_buffer = (const char *)(kw_rec + keywords_count);
+
+ name_pos = (size_t)(name_buffer - (const char *)kw_hdr);
+ if (name_pos > ext_hdr->hdr_size) {
+ /* the header is completely broken */
+ keywords_count =
+ mail_index_fsck_find_keyword_count(map, ext_hdr);
+ mail_index_fsck_error(index, "Assuming keywords_count = %u",
+ keywords_count);
+ kw_rec = NULL;
+ name_size = 0;
+ changed = TRUE;
+ } else {
+ name_size = ext_hdr->hdr_size - name_pos;
+ }
+
+ /* create keyword name array. invalid keywords are added as
+ empty strings */
+ t_array_init(&names, keywords_count);
+ for (i = 0; i < keywords_count; i++) {
+ if (name_size == 0 ||
+ !keyword_name_is_valid(name_buffer, kw_rec[i].name_offset,
+ name_size))
+ name = "";
+ else
+ name = name_buffer + kw_rec[i].name_offset;
+
+ if (*name != '\0' && array_has_name(&names, name)) {
+ /* duplicate */
+ name = "";
+ }
+ array_push_back(&names, &name);
+ }
+
+ /* give new names to invalid keywords */
+ changed_count = 0;
+ name_array = array_front_modifiable(&names);
+ for (i = j = 0; i < keywords_count; i++) {
+ while (name_array[i][0] == '\0') {
+ name = t_strdup_printf("unknown-%d", j++);
+ if (!array_has_name(&names, name)) {
+ name_array[i] = name;
+ changed = TRUE;
+ changed_count++;
+ }
+ }
+ }
+
+ if (!changed) {
+ /* nothing was broken */
+ return;
+ }
+
+ mail_index_fsck_error(index, "Renamed %u keywords to unknown-*",
+ changed_count);
+
+ dest = buffer_create_dynamic(default_pool,
+ I_MAX(ext_hdr->hdr_size, 128));
+ new_kw_hdr = buffer_append_space_unsafe(dest, sizeof(*new_kw_hdr));
+ new_kw_hdr->keywords_count = keywords_count;
+
+ /* add keyword records so we can start appending names directly */
+ rec_pos = dest->used;
+ i_zero(&new_kw_rec);
+ (void)buffer_append_space_unsafe(dest, keywords_count * sizeof(*kw_rec));
+
+ /* write the actual records and names */
+ name_base_pos = dest->used;
+ for (i = 0; i < keywords_count; i++) {
+ new_kw_rec.name_offset = dest->used - name_base_pos;
+ buffer_write(dest, rec_pos, &new_kw_rec, sizeof(new_kw_rec));
+ rec_pos += sizeof(*kw_rec);
+
+ buffer_append(dest, name_array[i], strlen(name_array[i]) + 1);
+ }
+
+ /* keep the header size at least the same size as before */
+ if (dest->used < ext_hdr->hdr_size)
+ buffer_append_zero(dest, ext_hdr->hdr_size - dest->used);
+
+ if (dest->used > ext_hdr->hdr_size) {
+ /* need to resize the header */
+ struct mail_index_ext_header new_ext_hdr;
+
+ diff = dest->used - ext_hdr->hdr_size;
+ buffer_copy(map->hdr_copy_buf, hdr_offset + diff,
+ map->hdr_copy_buf, hdr_offset, SIZE_MAX);
+ hdr->header_size += diff;
+ *offset_p += diff;
+
+ new_ext_hdr = *ext_hdr;
+ new_ext_hdr.hdr_size += diff;
+ buffer_write(map->hdr_copy_buf, ext_offset,
+ &new_ext_hdr, sizeof(new_ext_hdr));
+ }
+
+ i_assert(hdr_offset + dest->used <= map->hdr_copy_buf->used);
+ buffer_write(map->hdr_copy_buf, hdr_offset, dest->data, dest->used);
+
+ /* keywords changed unexpectedly, so all views are broken now */
+ index->inconsistency_id++;
+
+ buffer_free(&dest);
+}
+
+static void
+mail_index_fsck_extensions(struct mail_index *index, struct mail_index_map *map,
+ struct mail_index_header *hdr)
+{
+ const struct mail_index_ext_header *ext_hdr;
+ ARRAY_TYPE(const_string) names;
+ const char *name, *error;
+ unsigned int offset, next_offset, i;
+
+ t_array_init(&names, 64);
+ offset = MAIL_INDEX_HEADER_SIZE_ALIGN(hdr->base_header_size);
+ for (i = 0; offset < hdr->header_size; i++) {
+ /* mail_index_map_ext_get_next() uses map->hdr, so make sure
+ it's up-to-date */
+ map->hdr = *hdr;
+
+ next_offset = offset;
+ if (mail_index_map_ext_get_next(map, &next_offset,
+ &ext_hdr, &name) < 0) {
+ /* the extension continued outside header, drop it */
+ mail_index_fsck_error(index,
+ "Dropped extension #%d (%s) "
+ "with invalid header size",
+ i, name);
+ hdr->header_size = offset;
+ buffer_set_used_size(map->hdr_copy_buf, hdr->header_size);
+ break;
+ }
+ if (mail_index_map_ext_hdr_check(hdr, ext_hdr, name,
+ &error) < 0) {
+ mail_index_fsck_error(index,
+ "Dropped broken extension #%d (%s)", i, name);
+ } else if (array_has_name(&names, name)) {
+ mail_index_fsck_error(index,
+ "Dropped duplicate extension %s", name);
+ } else {
+ /* name may change if header buffer is changed */
+ name = t_strdup(name);
+
+ if (strcmp(name, MAIL_INDEX_EXT_KEYWORDS) == 0) {
+ mail_index_fsck_keywords(index, map, hdr,
+ ext_hdr, offset,
+ &next_offset);
+ }
+ array_push_back(&names, &name);
+ offset = next_offset;
+ continue;
+ }
+
+ /* drop the field */
+ hdr->header_size -= next_offset - offset;
+ buffer_copy(map->hdr_copy_buf, offset,
+ map->hdr_copy_buf, next_offset, SIZE_MAX);
+ buffer_set_used_size(map->hdr_copy_buf, hdr->header_size);
+ }
+}
+
+static void
+mail_index_fsck_records(struct mail_index *index, struct mail_index_map *map,
+ struct mail_index_header *hdr)
+{
+ struct mail_index_record *rec, *next_rec;
+ uint32_t i, last_uid;
+ bool logged_unordered_uids = FALSE, logged_zero_uids = FALSE;
+ bool records_dropped = FALSE;
+
+ hdr->messages_count = 0;
+ hdr->seen_messages_count = 0;
+ hdr->deleted_messages_count = 0;
+
+ hdr->first_unseen_uid_lowwater = 0;
+ hdr->first_deleted_uid_lowwater = 0;
+
+ rec = map->rec_map->records; last_uid = 0;
+ for (i = 0; i < map->rec_map->records_count; ) {
+ next_rec = PTR_OFFSET(rec, hdr->record_size);
+ if (rec->uid <= last_uid) {
+ /* log an error once, and skip this record */
+ if (rec->uid == 0) {
+ if (!logged_zero_uids) {
+ mail_index_fsck_error(index,
+ "Record UIDs have zeroes");
+ logged_zero_uids = TRUE;
+ }
+ } else {
+ if (!logged_unordered_uids) {
+ mail_index_fsck_error(index,
+ "Record UIDs unordered");
+ logged_unordered_uids = TRUE;
+ }
+ }
+ /* not the fastest way when we're skipping lots of
+ records, but this should happen rarely so don't
+ bother optimizing. */
+ memmove(rec, next_rec, hdr->record_size *
+ (map->rec_map->records_count - i - 1));
+ map->rec_map->records_count--;
+ records_dropped = TRUE;
+ continue;
+ }
+
+ hdr->messages_count++;
+ if ((rec->flags & MAIL_SEEN) != 0)
+ hdr->seen_messages_count++;
+ if ((rec->flags & MAIL_DELETED) != 0)
+ hdr->deleted_messages_count++;
+
+ if ((rec->flags & MAIL_SEEN) == 0 &&
+ hdr->first_unseen_uid_lowwater == 0)
+ hdr->first_unseen_uid_lowwater = rec->uid;
+ if ((rec->flags & MAIL_DELETED) != 0 &&
+ hdr->first_deleted_uid_lowwater == 0)
+ hdr->first_deleted_uid_lowwater = rec->uid;
+
+ last_uid = rec->uid;
+ rec = next_rec;
+ i++;
+ }
+
+ if (records_dropped) {
+ /* all existing views are broken now */
+ index->inconsistency_id++;
+ }
+
+ if (hdr->next_uid <= last_uid) {
+ mail_index_fsck_error(index, "next_uid %u -> %u",
+ hdr->next_uid, last_uid+1);
+ hdr->next_uid = last_uid+1;
+ }
+
+ if (hdr->first_unseen_uid_lowwater == 0)
+ hdr->first_unseen_uid_lowwater = hdr->next_uid;
+ if (hdr->first_deleted_uid_lowwater == 0)
+ hdr->first_deleted_uid_lowwater = hdr->next_uid;
+ if (hdr->first_recent_uid > hdr->next_uid)
+ hdr->first_recent_uid = hdr->next_uid;
+ if (hdr->first_recent_uid == 0)
+ hdr->first_recent_uid = 1;
+
+ CHECK(uid_validity, !=);
+ CHECK(messages_count, !=);
+ CHECK(seen_messages_count, !=);
+ CHECK(deleted_messages_count, !=);
+
+ CHECK(first_unseen_uid_lowwater, <);
+ CHECK(first_deleted_uid_lowwater, <);
+ CHECK(first_recent_uid, !=);
+}
+
+static void
+mail_index_fsck_map(struct mail_index *index, struct mail_index_map *map)
+{
+ struct mail_index_header hdr;
+
+ if (index->log->head != NULL) {
+ /* Remember the log head position. If we go back in the index's
+ head offset, ignore errors in the log up to this offset. */
+ mail_transaction_log_get_head(index->log,
+ &index->fsck_log_head_file_seq,
+ &index->fsck_log_head_file_offset);
+ }
+ hdr = map->hdr;
+
+ mail_index_fsck_header(index, map, &hdr);
+ mail_index_fsck_extensions(index, map, &hdr);
+ mail_index_fsck_records(index, map, &hdr);
+
+ hdr.flags |= MAIL_INDEX_HDR_FLAG_FSCKD;
+ map->hdr = hdr;
+ i_assert(map->hdr_copy_buf->used == map->hdr.header_size);
+}
+
+int mail_index_fsck(struct mail_index *index)
+{
+ bool orig_locked = index->log_sync_locked;
+ struct mail_index_map *map;
+ uint32_t file_seq;
+ uoff_t file_offset;
+
+ i_warning("fscking index file %s", index->filepath);
+
+ index->fscked = TRUE;
+
+ if (index->log->head == NULL) {
+ /* we're trying to open the index files, but there wasn't
+ any .log file. */
+ if (mail_transaction_log_create(index->log, FALSE) < 0)
+ return -1;
+ }
+
+ if (!orig_locked) {
+ if (mail_transaction_log_sync_lock(index->log, "fscking",
+ &file_seq, &file_offset) < 0)
+ return -1;
+ }
+
+ map = mail_index_map_clone(index->map);
+ mail_index_unmap(&index->map);
+ index->map = map;
+
+ T_BEGIN {
+ mail_index_fsck_map(index, map);
+ } T_END;
+
+ mail_index_write(index, FALSE, "fscking");
+
+ if (!orig_locked)
+ mail_transaction_log_sync_unlock(index->log, "fscking");
+ return 0;
+}
+
+void mail_index_fsck_locked(struct mail_index *index)
+{
+ int ret;
+
+ i_assert(index->log_sync_locked);
+ ret = mail_index_fsck(index);
+ i_assert(ret == 0);
+}
+
+bool mail_index_reset_fscked(struct mail_index *index)
+{
+ bool ret = index->fscked;
+
+ index->fscked = FALSE;
+ return ret;
+}
diff --git a/src/lib-index/mail-index-lock.c b/src/lib-index/mail-index-lock.c
new file mode 100644
index 0000000..cdf62e4
--- /dev/null
+++ b/src/lib-index/mail-index-lock.c
@@ -0,0 +1,63 @@
+/* Copyright (c) 2003-2018 Dovecot authors, see the included COPYING file */
+
+/*
+ Locking should never fail or timeout. Exclusive locks must be kept as short
+ time as possible. Shared locks can be long living, so if we can't get
+ exclusive lock directly, we'll recreate the index. That means the shared
+ lock holders can keep using the old file.
+
+ lock_id is used to figure out if acquired lock is still valid. When index
+ file is reopened, the lock_id can become invalid. It doesn't matter however,
+ as no-one's going to modify the old file anymore.
+
+ lock_id also tells us if we're referring to a shared or an exclusive lock.
+ This allows us to drop back to shared locking once all exclusive locks
+ are dropped. Shared locks have even numbers, exclusive locks have odd numbers.
+ The number is increased by two every time the lock is dropped or index file
+ is reopened.
+*/
+
+#include "lib.h"
+#include "nfs-workarounds.h"
+#include "mail-index-private.h"
+
+#define MAIL_INDEX_SHARED_LOCK_TIMEOUT 120
+
+int mail_index_lock_fd(struct mail_index *index, const char *path, int fd,
+ int lock_type, unsigned int timeout_secs,
+ struct file_lock **lock_r)
+{
+ const char *error;
+ int ret;
+
+ if (fd == -1) {
+ i_assert(MAIL_INDEX_IS_IN_MEMORY(index));
+ return 1;
+ }
+
+ struct file_lock_settings lock_set = {
+ .lock_method = index->set.lock_method,
+ };
+ ret = file_wait_lock(fd, path, lock_type, &lock_set, timeout_secs,
+ lock_r, &error);
+ if (ret < 0)
+ e_error(index->event, "%s", error);
+ return ret;
+}
+
+void mail_index_flush_read_cache(struct mail_index *index, const char *path,
+ int fd, bool locked)
+{
+ if ((index->flags & MAIL_INDEX_OPEN_FLAG_NFS_FLUSH) == 0)
+ return;
+
+ /* Assume flock() is emulated with fcntl(), because that's how most
+ OSes work nowadays. */
+ if (locked &&
+ (index->set.lock_method == FILE_LOCK_METHOD_FCNTL ||
+ index->set.lock_method == FILE_LOCK_METHOD_FLOCK)) {
+ nfs_flush_read_cache_locked(path, fd);
+ } else {
+ nfs_flush_read_cache_unlocked(path, fd);
+ }
+}
diff --git a/src/lib-index/mail-index-map-hdr.c b/src/lib-index/mail-index-map-hdr.c
new file mode 100644
index 0000000..3287a28
--- /dev/null
+++ b/src/lib-index/mail-index-map-hdr.c
@@ -0,0 +1,359 @@
+/* Copyright (c) 2003-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "mail-index-private.h"
+
+int mail_index_map_parse_extensions(struct mail_index_map *map)
+{
+ struct mail_index *index = map->index;
+ const struct mail_index_ext_header *ext_hdr;
+ unsigned int i, old_count, offset;
+ const char *name, *error;
+ uint32_t ext_id, ext_map_idx, ext_offset;
+
+ /* extension headers always start from 64bit offsets, so if base header
+ doesn't happen to be 64bit aligned we'll skip some bytes */
+ offset = MAIL_INDEX_HEADER_SIZE_ALIGN(map->hdr.base_header_size);
+ if (offset >= map->hdr.header_size && map->extension_pool == NULL) {
+ /* nothing to do, skip allocations and all */
+ return 0;
+ }
+
+ old_count = array_count(&index->extensions);
+ mail_index_map_init_extbufs(map, old_count + 5);
+
+ ext_id = (uint32_t)-1;
+ for (i = 0; i < old_count; i++)
+ array_push_back(&map->ext_id_map, &ext_id);
+
+ for (i = 0; offset < map->hdr.header_size; i++) {
+ ext_offset = offset;
+
+ if (mail_index_map_ext_get_next(map, &offset,
+ &ext_hdr, &name) < 0) {
+ mail_index_set_error(index, "Corrupted index file %s: "
+ "Header extension #%d (%s) goes outside header",
+ index->filepath, i, name);
+ return -1;
+ }
+
+ if (mail_index_map_ext_hdr_check(&map->hdr, ext_hdr,
+ name, &error) < 0) {
+ mail_index_set_error(index, "Corrupted index file %s: "
+ "Broken extension #%d (%s): %s",
+ index->filepath, i, name, error);
+ return -1;
+ }
+ if (mail_index_map_lookup_ext(map, name, &ext_map_idx)) {
+ mail_index_set_error(index, "Corrupted index file %s: "
+ "Duplicate header extension %s",
+ index->filepath, name);
+ return -1;
+ }
+
+ (void)mail_index_map_register_ext(map, name, ext_offset, ext_hdr);
+ }
+ return 0;
+}
+
+int mail_index_map_parse_keywords(struct mail_index_map *map)
+{
+ struct mail_index *index = map->index;
+ const struct mail_index_ext *ext;
+ const struct mail_index_keyword_header *kw_hdr;
+ const struct mail_index_keyword_header_rec *kw_rec;
+ const char *name;
+ unsigned int i, name_area_end_offset, old_count;
+ uint32_t idx;
+
+ if (!mail_index_map_lookup_ext(map, MAIL_INDEX_EXT_KEYWORDS, &idx)) {
+ if (array_is_created(&map->keyword_idx_map))
+ array_clear(&map->keyword_idx_map);
+ return 0;
+ }
+ ext = array_idx(&map->extensions, idx);
+
+ /* Extension header contains:
+ - struct mail_index_keyword_header
+ - struct mail_index_keyword_header_rec * keywords_count
+ - const char names[] * keywords_count
+
+ The mail_index_keyword_header_rec are rather unnecessary nowadays.
+ They were originally an optimization when dovecot.index header kept
+ changing constantly, but nowadays the changes are usually read from
+ the .log changes, so re-reading dovecot.index header isn't common.
+ In a later version we could even remove it.
+ */
+ i_assert(ext->hdr_offset < map->hdr.header_size);
+ kw_hdr = MAIL_INDEX_MAP_HDR_OFFSET(map, ext->hdr_offset);
+ kw_rec = (const void *)(kw_hdr + 1);
+ name = (const char *)(kw_rec + kw_hdr->keywords_count);
+
+ old_count = !array_is_created(&map->keyword_idx_map) ? 0 :
+ array_count(&map->keyword_idx_map);
+
+ /* make sure the header is valid */
+ if (kw_hdr->keywords_count < old_count) {
+ mail_index_set_error(index, "Corrupted index file %s: "
+ "Keywords removed unexpectedly",
+ index->filepath);
+ return -1;
+ }
+
+ if ((size_t)(name - (const char *)kw_hdr) > ext->hdr_size) {
+ mail_index_set_error(index, "Corrupted index file %s: "
+ "keywords_count larger than header size",
+ index->filepath);
+ return -1;
+ }
+
+ name_area_end_offset = (const char *)kw_hdr + ext->hdr_size - name;
+ for (i = 0; i < kw_hdr->keywords_count; i++) {
+ if (kw_rec[i].name_offset > name_area_end_offset) {
+ mail_index_set_error(index, "Corrupted index file %s: "
+ "name_offset points outside allocated header",
+ index->filepath);
+ return -1;
+ }
+ }
+ if (name[name_area_end_offset-1] != '\0') {
+ mail_index_set_error(index, "Corrupted index file %s: "
+ "Keyword header doesn't end with NUL",
+ index->filepath);
+ return -1;
+ }
+
+ /* create file -> index mapping */
+ if (!array_is_created(&map->keyword_idx_map))
+ i_array_init(&map->keyword_idx_map, kw_hdr->keywords_count);
+
+ size_t name_offset = 0;
+ /* Check that existing headers are still the same. */
+ for (i = 0; i < array_count(&map->keyword_idx_map); i++) {
+ const char *keyword = name + kw_rec[i].name_offset;
+ const unsigned int *old_idx;
+ unsigned int kw_idx;
+
+ if (kw_rec[i].name_offset != name_offset) {
+ /* this shouldn't happen, but the old code didn't check
+ for this so for safety keep this as a warning. */
+ e_warning(index->event,
+ "Corrupted index file %s: "
+ "Mismatching keyword name_offset",
+ index->filepath);
+ }
+ name_offset += strlen(keyword) + 1;
+
+ old_idx = array_idx(&map->keyword_idx_map, i);
+ if (!mail_index_keyword_lookup(index, keyword, &kw_idx) ||
+ kw_idx != *old_idx) {
+ mail_index_set_error(index, "Corrupted index file %s: "
+ "Keywords changed unexpectedly",
+ index->filepath);
+ return -1;
+ }
+ }
+
+ /* Register the newly seen keywords */
+ i = array_count(&map->keyword_idx_map);
+ for (; i < kw_hdr->keywords_count; i++) {
+ const char *keyword = name + kw_rec[i].name_offset;
+ unsigned int kw_idx;
+
+ if (kw_rec[i].name_offset != name_offset) {
+ /* this shouldn't happen, but the old code didn't check
+ for this so for safety keep this as a warning. */
+ e_warning(index->event,
+ "Corrupted index file %s: "
+ "Mismatching keyword name_offset",
+ index->filepath);
+ }
+ name_offset += strlen(keyword) + 1;
+
+ if (*keyword == '\0') {
+ mail_index_set_error(index, "Corrupted index file %s: "
+ "Empty keyword name in header",
+ index->filepath);
+ return -1;
+ }
+ mail_index_keyword_lookup_or_create(index, keyword, &kw_idx);
+ array_push_back(&map->keyword_idx_map, &kw_idx);
+ }
+ return 0;
+}
+
+bool mail_index_check_header_compat(struct mail_index *index,
+ const struct mail_index_header *hdr,
+ uoff_t file_size, const char **error_r)
+{
+ enum mail_index_header_compat_flags compat_flags = 0;
+
+#ifndef WORDS_BIGENDIAN
+ compat_flags |= MAIL_INDEX_COMPAT_LITTLE_ENDIAN;
+#endif
+
+ if (hdr->major_version != MAIL_INDEX_MAJOR_VERSION) {
+ /* major version change */
+ *error_r = t_strdup_printf("Major version changed (%u != %u)",
+ hdr->major_version, MAIL_INDEX_MAJOR_VERSION);
+ return FALSE;
+ }
+ if ((hdr->flags & MAIL_INDEX_HDR_FLAG_CORRUPTED) != 0) {
+ /* we've already complained about it */
+ *error_r = "Header's corrupted flag is set";
+ return FALSE;
+ }
+
+ if (hdr->compat_flags != compat_flags) {
+ /* architecture change */
+ *error_r = "CPU architecture changed";
+ return FALSE;
+ }
+
+ if (hdr->base_header_size < MAIL_INDEX_HEADER_MIN_SIZE ||
+ hdr->header_size < hdr->base_header_size) {
+ *error_r = t_strdup_printf(
+ "Corrupted header sizes (base %u, full %u)",
+ hdr->base_header_size, hdr->header_size);
+ return FALSE;
+ }
+ if (hdr->header_size > file_size) {
+ *error_r = t_strdup_printf(
+ "Header size is larger than file (%u > %"PRIuUOFF_T")",
+ hdr->header_size, file_size);
+ return FALSE;
+ }
+
+ if (hdr->indexid != index->indexid) {
+ if (index->indexid != 0) {
+ mail_index_set_error(index, "Index file %s: "
+ "indexid changed: %u -> %u",
+ index->filepath, index->indexid,
+ hdr->indexid);
+ }
+ index->indexid = hdr->indexid;
+ mail_transaction_log_indexid_changed(index->log);
+ }
+ return TRUE;
+}
+
+static void mail_index_map_clear_recent_flags(struct mail_index_map *map)
+{
+ struct mail_index_record *rec;
+ uint32_t seq;
+
+ for (seq = 1; seq <= map->hdr.messages_count; seq++) {
+ rec = MAIL_INDEX_REC_AT_SEQ(map, seq);
+ rec->flags &= ENUM_NEGATE(MAIL_RECENT);
+ }
+}
+
+int mail_index_map_check_header(struct mail_index_map *map,
+ const char **error_r)
+{
+ struct mail_index *index = map->index;
+ const struct mail_index_header *hdr = &map->hdr;
+
+ if (!mail_index_check_header_compat(index, hdr, UOFF_T_MAX, error_r))
+ return 0;
+
+ /* following some extra checks that only take a bit of CPU */
+ if (hdr->record_size < sizeof(struct mail_index_record)) {
+ *error_r = t_strdup_printf(
+ "record_size too small (%u < %zu)",
+ hdr->record_size, sizeof(struct mail_index_record));
+ return -1;
+ }
+
+ if (hdr->uid_validity == 0 && hdr->next_uid != 1) {
+ *error_r = t_strdup_printf(
+ "uidvalidity=0, but next_uid=%u", hdr->next_uid);
+ return 0;
+ }
+ if (hdr->next_uid == 0) {
+ *error_r = "next_uid=0";
+ return 0;
+ }
+ if (hdr->messages_count > map->rec_map->records_count) {
+ *error_r = t_strdup_printf(
+ "messages_count is higher in header than record map (%u > %u)",
+ hdr->messages_count, map->rec_map->records_count);
+ return 0;
+ }
+
+ if (hdr->seen_messages_count > hdr->messages_count) {
+ *error_r = t_strdup_printf(
+ "seen_messages_count %u > messages_count %u",
+ hdr->seen_messages_count, hdr->messages_count);
+ return 0;
+ }
+ if (hdr->deleted_messages_count > hdr->messages_count) {
+ *error_r = t_strdup_printf(
+ "deleted_messages_count %u > messages_count %u",
+ hdr->deleted_messages_count, hdr->messages_count);
+ return 0;
+ }
+ switch (hdr->minor_version) {
+ case 0:
+ /* upgrade silently from v1.0 */
+ map->hdr.unused_old_recent_messages_count = 0;
+ if (hdr->first_recent_uid == 0)
+ map->hdr.first_recent_uid = 1;
+ if (index->need_recreate == NULL)
+ index->need_recreate = i_strdup("Upgrading from index version 1.0");
+ /* fall through */
+ case 1:
+ /* pre-v1.1.rc6: make sure the \Recent flags are gone */
+ mail_index_map_clear_recent_flags(map);
+ map->hdr.minor_version = MAIL_INDEX_MINOR_VERSION;
+ /* fall through */
+ case 2:
+ /* pre-v2.2 (although should have been done in v2.1 already):
+ make sure the old unused fields are cleared */
+ map->hdr.unused_old_sync_size_part1 = 0;
+ map->hdr.log2_rotate_time = 0;
+ map->hdr.last_temp_file_scan = 0;
+ }
+ if (hdr->first_recent_uid == 0) {
+ *error_r = "first_recent_uid=0";
+ return 0;
+ }
+ if (hdr->first_recent_uid > hdr->next_uid) {
+ *error_r = t_strdup_printf(
+ "first_recent_uid %u > next_uid %u",
+ hdr->first_recent_uid, hdr->next_uid);
+ return 0;
+ }
+ if (hdr->first_unseen_uid_lowwater > hdr->next_uid) {
+ *error_r = t_strdup_printf(
+ "first_unseen_uid_lowwater %u > next_uid %u",
+ hdr->first_unseen_uid_lowwater, hdr->next_uid);
+ return 0;
+ }
+ if (hdr->first_deleted_uid_lowwater > hdr->next_uid) {
+ *error_r = t_strdup_printf(
+ "first_deleted_uid_lowwater %u > next_uid %u",
+ hdr->first_deleted_uid_lowwater, hdr->next_uid);
+ return 0;
+ }
+
+ if (hdr->messages_count > 0) {
+ /* last message's UID must be smaller than next_uid.
+ also make sure it's not zero. */
+ const struct mail_index_record *rec;
+
+ rec = MAIL_INDEX_REC_AT_SEQ(map, hdr->messages_count);
+ if (rec->uid == 0) {
+ *error_r = "last message has uid=0";
+ return -1;
+ }
+ if (rec->uid >= hdr->next_uid) {
+ *error_r = t_strdup_printf(
+ "last message uid %u >= next_uid %u",
+ rec->uid, hdr->next_uid);
+ return 0;
+ }
+ }
+ return 1;
+}
diff --git a/src/lib-index/mail-index-map-read.c b/src/lib-index/mail-index-map-read.c
new file mode 100644
index 0000000..6999bf1
--- /dev/null
+++ b/src/lib-index/mail-index-map-read.c
@@ -0,0 +1,519 @@
+/* Copyright (c) 2003-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "nfs-workarounds.h"
+#include "mmap-util.h"
+#include "read-full.h"
+#include "mail-index-private.h"
+#include "mail-index-sync-private.h"
+#include "mail-transaction-log-private.h"
+#include "mail-index-modseq.h"
+#include "ioloop.h"
+
+static void mail_index_map_copy_hdr(struct mail_index_map *map,
+ const struct mail_index_header *hdr)
+{
+ if (hdr->base_header_size < sizeof(map->hdr)) {
+ /* header smaller than ours, make a copy so our newer headers
+ won't have garbage in them */
+ i_zero(&map->hdr);
+ memcpy(&map->hdr, hdr, hdr->base_header_size);
+ } else {
+ map->hdr = *hdr;
+ }
+
+ /* FIXME: backwards compatibility, remove later. In case this index is
+ accessed with Dovecot v1.0, avoid recent message counter errors. */
+ map->hdr.unused_old_recent_messages_count = 0;
+}
+
+static int mail_index_mmap(struct mail_index_map *map, uoff_t file_size)
+{
+ struct mail_index *index = map->index;
+ struct mail_index_record_map *rec_map = map->rec_map;
+ const struct mail_index_header *hdr;
+ const char *error;
+
+ i_assert(rec_map->mmap_base == NULL);
+
+ buffer_free(&rec_map->buffer);
+ if (file_size > SSIZE_T_MAX) {
+ /* too large file to map into memory */
+ mail_index_set_error(index, "Index file too large: %s",
+ index->filepath);
+ return -1;
+ }
+
+ rec_map->mmap_base = mmap(NULL, file_size, PROT_READ | PROT_WRITE,
+ MAP_PRIVATE, index->fd, 0);
+ if (rec_map->mmap_base == MAP_FAILED) {
+ rec_map->mmap_base = NULL;
+ if (ioloop_time != index->last_mmap_error_time) {
+ index->last_mmap_error_time = ioloop_time;
+ mail_index_set_syscall_error(index, t_strdup_printf(
+ "mmap(size=%"PRIuUOFF_T")", file_size));
+ }
+ return -1;
+ }
+ rec_map->mmap_size = file_size;
+
+ hdr = rec_map->mmap_base;
+ if (rec_map->mmap_size >
+ offsetof(struct mail_index_header, major_version) &&
+ hdr->major_version != MAIL_INDEX_MAJOR_VERSION) {
+ /* major version change - handle silently */
+ return 0;
+ }
+
+ if (rec_map->mmap_size < MAIL_INDEX_HEADER_MIN_SIZE) {
+ mail_index_set_error(index, "Corrupted index file %s: "
+ "File too small (%zu)",
+ index->filepath, rec_map->mmap_size);
+ return 0;
+ }
+
+ if (!mail_index_check_header_compat(index, hdr, rec_map->mmap_size, &error)) {
+ /* Can't use this file */
+ mail_index_set_error(index, "Corrupted index file %s: %s",
+ index->filepath, error);
+ return 0;
+ }
+
+ rec_map->mmap_used_size = hdr->header_size +
+ hdr->messages_count * hdr->record_size;
+
+ if (rec_map->mmap_used_size <= rec_map->mmap_size)
+ rec_map->records_count = hdr->messages_count;
+ else {
+ rec_map->records_count =
+ (rec_map->mmap_size - hdr->header_size) /
+ hdr->record_size;
+ rec_map->mmap_used_size = hdr->header_size +
+ rec_map->records_count * hdr->record_size;
+ mail_index_set_error(index, "Corrupted index file %s: "
+ "messages_count too large (%u > %u)",
+ index->filepath, hdr->messages_count,
+ rec_map->records_count);
+ }
+
+ mail_index_map_copy_hdr(map, hdr);
+ buffer_set_used_size(map->hdr_copy_buf, 0);
+ buffer_append(map->hdr_copy_buf, rec_map->mmap_base, hdr->header_size);
+
+ rec_map->records = PTR_OFFSET(rec_map->mmap_base, map->hdr.header_size);
+ return 1;
+}
+
+static int mail_index_read_header(struct mail_index *index,
+ void *buf, size_t buf_size, size_t *pos_r)
+{
+ size_t pos;
+ int ret;
+
+ memset(buf, 0, sizeof(struct mail_index_header));
+
+ /* try to read the whole header, but it's not necessarily an error to
+ read less since the older versions of the index format could be
+ smaller. Request reading up to buf_size, but accept if we only got
+ the header. */
+ pos = 0;
+ do {
+ ret = pread(index->fd, PTR_OFFSET(buf, pos),
+ buf_size - pos, pos);
+ if (ret > 0)
+ pos += ret;
+ } while (ret > 0 && pos < sizeof(struct mail_index_header));
+
+ *pos_r = pos;
+ return ret;
+}
+
+static int
+mail_index_try_read_map(struct mail_index_map *map,
+ uoff_t file_size, bool *retry_r, bool try_retry)
+{
+ struct mail_index *index = map->index;
+ const struct mail_index_header *hdr;
+ unsigned char read_buf[IO_BLOCK_SIZE];
+ const char *error;
+ const void *buf;
+ void *data = NULL;
+ ssize_t ret;
+ size_t pos, records_size, initial_buf_pos = 0;
+ unsigned int records_count = 0, extra;
+
+ i_assert(map->rec_map->mmap_base == NULL);
+
+ *retry_r = FALSE;
+ ret = mail_index_read_header(index, read_buf, sizeof(read_buf), &pos);
+ buf = read_buf; hdr = buf;
+
+ if (pos > (ssize_t)offsetof(struct mail_index_header, major_version) &&
+ hdr->major_version != MAIL_INDEX_MAJOR_VERSION) {
+ /* major version change - handle silently */
+ return 0;
+ }
+
+ if (ret >= 0 && pos >= MAIL_INDEX_HEADER_MIN_SIZE &&
+ (ret > 0 || pos >= hdr->base_header_size)) {
+ if (!mail_index_check_header_compat(index, hdr, file_size, &error)) {
+ /* Can't use this file */
+ mail_index_set_error(index, "Corrupted index file %s: %s",
+ index->filepath, error);
+ return 0;
+ }
+
+ initial_buf_pos = pos;
+ if (pos > hdr->header_size)
+ pos = hdr->header_size;
+
+ /* place the base header into memory. */
+ buffer_set_used_size(map->hdr_copy_buf, 0);
+ buffer_append(map->hdr_copy_buf, buf, pos);
+
+ if (pos != hdr->header_size) {
+ /* @UNSAFE: read the rest of the header into memory */
+ data = buffer_append_space_unsafe(map->hdr_copy_buf,
+ hdr->header_size -
+ pos);
+ ret = pread_full(index->fd, data,
+ hdr->header_size - pos, pos);
+ }
+ }
+
+ if (ret > 0) {
+ /* header read, read the records now. */
+ records_size = (size_t)hdr->messages_count * hdr->record_size;
+ records_count = hdr->messages_count;
+
+ if (file_size - hdr->header_size < records_size ||
+ (hdr->record_size != 0 &&
+ records_size / hdr->record_size != hdr->messages_count)) {
+ records_count = (file_size - hdr->header_size) /
+ hdr->record_size;
+ records_size = (size_t)records_count * hdr->record_size;
+ mail_index_set_error(index, "Corrupted index file %s: "
+ "messages_count too large (%u > %u)",
+ index->filepath, hdr->messages_count,
+ records_count);
+ }
+
+ if (map->rec_map->buffer == NULL) {
+ map->rec_map->buffer =
+ buffer_create_dynamic(default_pool,
+ records_size);
+ }
+
+ /* @UNSAFE */
+ buffer_set_used_size(map->rec_map->buffer, 0);
+ if (initial_buf_pos <= hdr->header_size)
+ extra = 0;
+ else {
+ extra = initial_buf_pos - hdr->header_size;
+ buffer_append(map->rec_map->buffer,
+ CONST_PTR_OFFSET(buf, hdr->header_size),
+ extra);
+ }
+ if (records_size > extra) {
+ data = buffer_append_space_unsafe(map->rec_map->buffer,
+ records_size - extra);
+ ret = pread_full(index->fd, data, records_size - extra,
+ hdr->header_size + extra);
+ }
+ }
+
+ if (ret < 0) {
+ if (errno == ESTALE && try_retry) {
+ /* a new index file was renamed over this one. */
+ *retry_r = TRUE;
+ return 0;
+ }
+ mail_index_set_syscall_error(index, "pread_full()");
+ return -1;
+ }
+ if (ret == 0) {
+ mail_index_set_error(index,
+ "Corrupted index file %s: File too small",
+ index->filepath);
+ return 0;
+ }
+
+ map->rec_map->records =
+ buffer_get_modifiable_data(map->rec_map->buffer, NULL);
+ map->rec_map->records_count = records_count;
+
+ mail_index_map_copy_hdr(map, hdr);
+ i_assert(map->hdr_copy_buf->used == map->hdr.header_size);
+ return 1;
+}
+
+static int mail_index_read_map(struct mail_index_map *map, uoff_t file_size)
+{
+ struct mail_index *index = map->index;
+ struct stat st;
+ unsigned int i;
+ int ret;
+ bool try_retry, retry;
+
+ for (i = 0;; i++) {
+ try_retry = i < MAIL_INDEX_ESTALE_RETRY_COUNT;
+ if (file_size == UOFF_T_MAX) {
+ /* fstat() below failed */
+ ret = 0;
+ retry = try_retry;
+ } else {
+ ret = mail_index_try_read_map(map, file_size,
+ &retry, try_retry);
+ }
+ if (ret != 0 || !retry)
+ break;
+
+ /* ESTALE - reopen index file */
+ mail_index_close_file(index);
+
+ ret = mail_index_try_open_only(index);
+ if (ret <= 0) {
+ if (ret == 0) {
+ /* the file was lost */
+ errno = ENOENT;
+ mail_index_set_syscall_error(index, "open()");
+ }
+ return -1;
+ }
+
+ if (fstat(index->fd, &st) == 0)
+ file_size = st.st_size;
+ else {
+ if (!ESTALE_FSTAT(errno)) {
+ mail_index_set_syscall_error(index, "fstat()");
+ return -1;
+ }
+ file_size = UOFF_T_MAX;
+ }
+ }
+ return ret;
+}
+
+/* returns -1 = error, 0 = index files are unusable,
+ 1 = index files are usable or at least repairable */
+static int
+mail_index_map_latest_file(struct mail_index *index, const char **reason_r)
+{
+ struct mail_index_map *old_map, *new_map;
+ struct stat st;
+ uoff_t file_size;
+ bool use_mmap, reopened, unusable = FALSE;
+ const char *error;
+ int ret, try;
+
+ *reason_r = NULL;
+
+ index->reopen_main_index = FALSE;
+ ret = mail_index_reopen_if_changed(index, &reopened, reason_r);
+ if (ret <= 0) {
+ if (ret < 0)
+ return -1;
+
+ /* the index file is lost/broken. let's hope that we can
+ build it from the transaction log. */
+ return 1;
+ }
+ i_assert(index->fd != -1);
+
+ if ((index->flags & MAIL_INDEX_OPEN_FLAG_NFS_FLUSH) != 0)
+ nfs_flush_attr_cache_fd_locked(index->filepath, index->fd);
+
+ if (fstat(index->fd, &st) == 0)
+ file_size = st.st_size;
+ else {
+ if (!ESTALE_FSTAT(errno)) {
+ mail_index_set_syscall_error(index, "fstat()");
+ return -1;
+ }
+ file_size = UOFF_T_MAX;
+ }
+
+ /* mmaping seems to be slower than just reading the file, so even if
+ mmap isn't disabled don't use it unless the file is large enough */
+ use_mmap = (index->flags & MAIL_INDEX_OPEN_FLAG_MMAP_DISABLE) == 0 &&
+ file_size != UOFF_T_MAX && file_size > MAIL_INDEX_MMAP_MIN_SIZE;
+
+ new_map = mail_index_map_alloc(index);
+ if (use_mmap) {
+ ret = mail_index_mmap(new_map, file_size);
+ } else {
+ ret = mail_index_read_map(new_map, file_size);
+ }
+ if (ret == 0) {
+ /* the index files are unusable */
+ unusable = TRUE;
+ }
+
+ for (try = 0; ret > 0; try++) {
+ /* make sure the header is ok before using this mapping */
+ ret = mail_index_map_check_header(new_map, &error);
+ if (ret < 0) {
+ mail_index_set_error(index,
+ "Corrupted index file %s: %s",
+ index->filepath, error);
+ }
+ if (ret > 0) T_BEGIN {
+ if (mail_index_map_parse_extensions(new_map) < 0)
+ ret = 0;
+ else if (mail_index_map_parse_keywords(new_map) < 0)
+ ret = 0;
+ } T_END;
+ if (ret != 0 || try == 2) {
+ if (ret < 0) {
+ *reason_r = "Corrupted index file";
+ unusable = TRUE;
+ ret = 0;
+ }
+ break;
+ }
+
+ /* fsck and try again */
+ old_map = index->map;
+ index->map = new_map;
+ if (mail_index_fsck(index) < 0) {
+ ret = -1;
+ break;
+ }
+
+ /* fsck replaced the map */
+ new_map = index->map;
+ index->map = old_map;
+ }
+ if (ret <= 0) {
+ mail_index_unmap(&new_map);
+ return ret < 0 ? -1 : (unusable ? 0 : 1);
+ }
+ i_assert(new_map->rec_map->records != NULL);
+
+ index->main_index_hdr_log_file_seq = new_map->hdr.log_file_seq;
+ index->main_index_hdr_log_file_tail_offset =
+ new_map->hdr.log_file_tail_offset;
+ mail_index_modseq_hdr_snapshot_update(new_map);
+
+ mail_index_unmap(&index->map);
+ index->map = new_map;
+ *reason_r = t_strdup_printf("Index mapped (file_seq=%u)",
+ index->map->hdr.log_file_seq);
+ return 1;
+}
+
+static int
+mail_index_map_latest_sync(struct mail_index *index,
+ enum mail_index_sync_handler_type type,
+ const char *reason)
+{
+ const char *map_reason, *reopen_reason;
+ bool reopened;
+ int ret;
+
+ if (index->log->head == NULL || index->indexid == 0) {
+ /* we're creating the index file, we don't have any
+ logs yet */
+ return 1;
+ }
+
+ /* and update the map with the latest changes from transaction log */
+ ret = mail_index_sync_map(&index->map, type, &map_reason);
+ if (ret != 0)
+ return ret;
+
+ if (index->fd == -1) {
+ reopen_reason = "Index not open";
+ reopened = FALSE;
+ } else {
+ /* Check if the index was recreated while we were opening it.
+ This is unlikely, but could happen if
+ mail_index_log_optimization_settings.max_size is tiny. */
+ ret = mail_index_reopen_if_changed(index, &reopened, &reopen_reason);
+ if (ret < 0)
+ return -1;
+ if (ret == 0) {
+ /* Index was unexpectedly lost. The mailbox was
+ probably deleted while we were opening it. Handle
+ this as an error. */
+ index->index_deleted = TRUE;
+ return -1;
+ }
+ }
+ if (!reopened) {
+ /* fsck the index and try to reopen */
+ mail_index_set_error(index, "Index %s: %s: %s - fscking "
+ "(reopen_reason: %s)",
+ index->filepath, reason, map_reason,
+ reopen_reason);
+ if (!index->readonly) {
+ if (mail_index_fsck(index) < 0)
+ return -1;
+ }
+ }
+
+ ret = mail_index_map_latest_file(index, &reason);
+ if (ret > 0 && index->indexid != 0) {
+ ret = mail_index_sync_map(&index->map, type, &map_reason);
+ if (ret == 0) {
+ mail_index_set_error(index, "Index %s: %s: %s",
+ index->filepath, reason, map_reason);
+ }
+ }
+ return ret;
+}
+
+int mail_index_map(struct mail_index *index,
+ enum mail_index_sync_handler_type type)
+{
+ const char *reason;
+ int ret;
+
+ i_assert(!index->mapping);
+
+ index->mapping = TRUE;
+
+ if (index->map == NULL)
+ index->map = mail_index_map_alloc(index);
+
+ /* first try updating the existing mapping from transaction log. */
+ if (!index->initial_mapped || index->reopen_main_index) {
+ /* index is being created/opened for the first time */
+ ret = 0;
+ } else if (mail_index_sync_map_want_index_reopen(index->map, type)) {
+ /* it's likely more efficient to reopen the index file than
+ sync from the transaction log. */
+ ret = 0;
+ } else {
+ /* sync the map from the transaction log. */
+ ret = mail_index_sync_map(&index->map, type, &reason);
+ if (ret == 0) {
+ e_debug(index->event,
+ "Couldn't sync map from transaction log: %s - "
+ "reopening index instead",
+ reason);
+ }
+ }
+
+ if (ret == 0) {
+ /* try to open and read the latest index. if it fails, we'll
+ fallback to updating the existing mapping from transaction
+ logs (which we'll also do even if the reopening succeeds).
+ if index files are unusable (e.g. major version change)
+ don't even try to use the transaction log. */
+ ret = mail_index_map_latest_file(index, &reason);
+ if (ret > 0) {
+ ret = mail_index_map_latest_sync(index, type, reason);
+ } else if (ret == 0 && !index->readonly) {
+ /* make sure we don't try to open the file again */
+ if (unlink(index->filepath) < 0 && errno != ENOENT)
+ mail_index_set_syscall_error(index, "unlink()");
+ }
+ }
+
+ if (ret >= 0)
+ index->initial_mapped = TRUE;
+ index->mapping = FALSE;
+ return ret;
+}
diff --git a/src/lib-index/mail-index-map.c b/src/lib-index/mail-index-map.c
new file mode 100644
index 0000000..6ac2b93
--- /dev/null
+++ b/src/lib-index/mail-index-map.c
@@ -0,0 +1,595 @@
+/* Copyright (c) 2003-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "str-sanitize.h"
+#include "mmap-util.h"
+#include "mail-index-private.h"
+#include "mail-index-modseq.h"
+
+void mail_index_map_init_extbufs(struct mail_index_map *map,
+ unsigned int initial_count)
+{
+#define EXTENSION_NAME_APPROX_LEN 20
+#define EXT_GLOBAL_ALLOC_SIZE \
+ ((sizeof(map->extensions) + sizeof(buffer_t)) * 2)
+#define EXT_PER_ALLOC_SIZE \
+ (EXTENSION_NAME_APPROX_LEN + \
+ sizeof(struct mail_index_ext) + sizeof(uint32_t))
+ size_t size;
+
+ if (map->extension_pool == NULL) {
+ size = EXT_GLOBAL_ALLOC_SIZE +
+ initial_count * EXT_PER_ALLOC_SIZE;
+ map->extension_pool =
+ pool_alloconly_create(MEMPOOL_GROWING"map extensions",
+ nearest_power(size));
+ } else {
+ p_clear(map->extension_pool);
+
+ /* try to use the existing pool's size for initial_count so
+ we don't grow it needlessly */
+ size = p_get_max_easy_alloc_size(map->extension_pool);
+ if (size > EXT_GLOBAL_ALLOC_SIZE + EXT_PER_ALLOC_SIZE) {
+ initial_count = (size - EXT_GLOBAL_ALLOC_SIZE) /
+ EXT_PER_ALLOC_SIZE;
+ }
+ }
+
+ p_array_init(&map->extensions, map->extension_pool, initial_count);
+ p_array_init(&map->ext_id_map, map->extension_pool, initial_count);
+}
+
+bool mail_index_map_lookup_ext(struct mail_index_map *map, const char *name,
+ uint32_t *idx_r)
+{
+ const struct mail_index_ext *ext;
+
+ if (!array_is_created(&map->extensions))
+ return FALSE;
+
+ array_foreach(&map->extensions, ext) {
+ if (strcmp(ext->name, name) == 0) {
+ *idx_r = array_foreach_idx(&map->extensions, ext);
+ return TRUE;
+ }
+ }
+ return FALSE;
+}
+
+unsigned int mail_index_map_ext_hdr_offset(unsigned int name_len)
+{
+ size_t size = sizeof(struct mail_index_ext_header) + name_len;
+ return MAIL_INDEX_HEADER_SIZE_ALIGN(size);
+}
+
+uint32_t
+mail_index_map_register_ext(struct mail_index_map *map,
+ const char *name, uint32_t ext_offset,
+ const struct mail_index_ext_header *ext_hdr)
+{
+ struct mail_index_ext *ext;
+ uint32_t idx, ext_map_idx, empty_idx = (uint32_t)-1;
+
+ i_assert(mail_index_ext_name_is_valid(name));
+
+ if (!array_is_created(&map->extensions)) {
+ mail_index_map_init_extbufs(map, 5);
+ idx = 0;
+ } else {
+ idx = array_count(&map->extensions);
+ }
+ i_assert(!mail_index_map_lookup_ext(map, name, &ext_map_idx));
+
+ ext = array_append_space(&map->extensions);
+ ext->name = p_strdup(map->extension_pool, name);
+ ext->ext_offset = ext_offset;
+ ext->hdr_offset = ext_offset == (uint32_t)-1 ? (uint32_t)-1 :
+ ext_offset + mail_index_map_ext_hdr_offset(strlen(name));
+ ext->hdr_size = ext_hdr->hdr_size;
+ ext->record_offset = ext_hdr->record_offset;
+ ext->record_size = ext_hdr->record_size;
+ ext->record_align = ext_hdr->record_align;
+ ext->reset_id = ext_hdr->reset_id;
+
+ ext->index_idx = mail_index_ext_register(map->index, name,
+ ext_hdr->hdr_size,
+ ext_hdr->record_size,
+ ext_hdr->record_align);
+
+ /* Update index ext_id -> map ext_id mapping. Fill non-used
+ ext_ids with (uint32_t)-1 */
+ while (array_count(&map->ext_id_map) < ext->index_idx)
+ array_push_back(&map->ext_id_map, &empty_idx);
+ array_idx_set(&map->ext_id_map, ext->index_idx, &idx);
+ return idx;
+}
+
+int mail_index_map_ext_get_next(struct mail_index_map *map,
+ unsigned int *offset_p,
+ const struct mail_index_ext_header **ext_hdr_r,
+ const char **name_r)
+{
+ const struct mail_index_ext_header *ext_hdr;
+ unsigned int offset, name_offset;
+
+ offset = *offset_p;
+ *name_r = "";
+
+ /* Extension header contains:
+ - struct mail_index_ext_header
+ - name (not 0-terminated)
+ - 64bit alignment padding
+ - extension header contents
+ - 64bit alignment padding
+ */
+ name_offset = offset + sizeof(*ext_hdr);
+ ext_hdr = MAIL_INDEX_MAP_HDR_OFFSET(map, offset);
+ if (offset + sizeof(*ext_hdr) >= map->hdr.header_size)
+ return -1;
+
+ offset += mail_index_map_ext_hdr_offset(ext_hdr->name_size);
+ if (offset > map->hdr.header_size)
+ return -1;
+
+ *name_r = t_strndup(MAIL_INDEX_MAP_HDR_OFFSET(map, name_offset),
+ ext_hdr->name_size);
+ if (strcmp(*name_r, str_sanitize(*name_r, SIZE_MAX)) != 0) {
+ /* we allow only plain ASCII names, so this extension
+ is most likely broken */
+ *name_r = "";
+ }
+
+ /* finally make sure that the hdr_size is small enough.
+ do this last so that we could return a usable name. */
+ offset += MAIL_INDEX_HEADER_SIZE_ALIGN(ext_hdr->hdr_size);
+ if (offset > map->hdr.header_size)
+ return -1;
+
+ *offset_p = offset;
+ *ext_hdr_r = ext_hdr;
+ return 0;
+}
+
+static int
+mail_index_map_ext_hdr_check_record(const struct mail_index_header *hdr,
+ const struct mail_index_ext_header *ext_hdr,
+ const char **error_r)
+{
+ if (ext_hdr->record_align == 0) {
+ *error_r = "Record field alignment is zero";
+ return -1;
+ }
+
+ /* until we get 128 bit CPUs having a larger alignment is pointless */
+ if (ext_hdr->record_align > sizeof(uint64_t)) {
+ *error_r = "Record alignment is too large";
+ return -1;
+ }
+ /* a large record size is most likely a bug somewhere. the maximum
+ record size is limited to 64k anyway, so try to fail earlier. */
+ if (ext_hdr->record_size >= 32768) {
+ *error_r = "Record size is too large";
+ return -1;
+ }
+
+ if (ext_hdr->record_offset == 0) {
+ /* if we get here from extension introduction, record_offset=0
+ and hdr->record_size hasn't been updated yet */
+ return 0;
+ }
+
+ if (ext_hdr->record_offset + ext_hdr->record_size > hdr->record_size) {
+ *error_r = t_strdup_printf("Record field points "
+ "outside record size (%u+%u > %u)",
+ ext_hdr->record_offset,
+ ext_hdr->record_size,
+ hdr->record_size);
+ return -1;
+ }
+
+ if ((ext_hdr->record_offset % ext_hdr->record_align) != 0) {
+ *error_r = t_strdup_printf("Record field alignment %u "
+ "not used", ext_hdr->record_align);
+ return -1;
+ }
+ if ((hdr->record_size % ext_hdr->record_align) != 0) {
+ *error_r = t_strdup_printf("Record size not aligned by %u "
+ "as required by extension",
+ ext_hdr->record_align);
+ return -1;
+ }
+ return 0;
+}
+
+int mail_index_map_ext_hdr_check(const struct mail_index_header *hdr,
+ const struct mail_index_ext_header *ext_hdr,
+ const char *name, const char **error_r)
+{
+ if (ext_hdr->record_size == 0 && ext_hdr->hdr_size == 0) {
+ *error_r = "Invalid field values";
+ return -1;
+ }
+ if (!mail_index_ext_name_is_valid(name)) {
+ *error_r = "Invalid name";
+ return -1;
+ }
+
+ if (ext_hdr->record_size != 0) {
+ if (mail_index_map_ext_hdr_check_record(hdr, ext_hdr,
+ error_r) < 0)
+ return -1;
+ }
+ if (ext_hdr->hdr_size > MAIL_INDEX_EXT_HEADER_MAX_SIZE) {
+ *error_r = t_strdup_printf("Headersize too large (%u)",
+ ext_hdr->hdr_size);
+ return -1;
+ }
+ return 0;
+}
+
+static void mail_index_header_init(struct mail_index *index,
+ struct mail_index_header *hdr)
+{
+ i_assert((sizeof(*hdr) % sizeof(uint64_t)) == 0);
+
+ i_zero(hdr);
+
+ hdr->major_version = MAIL_INDEX_MAJOR_VERSION;
+ hdr->minor_version = MAIL_INDEX_MINOR_VERSION;
+ hdr->base_header_size = sizeof(*hdr);
+ hdr->header_size = sizeof(*hdr);
+ hdr->record_size = sizeof(struct mail_index_record);
+
+#ifndef WORDS_BIGENDIAN
+ hdr->compat_flags |= MAIL_INDEX_COMPAT_LITTLE_ENDIAN;
+#endif
+
+ hdr->indexid = index->indexid;
+ hdr->log_file_seq = 1;
+ hdr->next_uid = 1;
+ hdr->first_recent_uid = 1;
+}
+
+struct mail_index_map *mail_index_map_alloc(struct mail_index *index)
+{
+ struct mail_index_map tmp_map;
+
+ i_zero(&tmp_map);
+ mail_index_header_init(index, &tmp_map.hdr);
+ tmp_map.hdr_copy_buf = t_buffer_create(sizeof(tmp_map.hdr));
+ buffer_append(tmp_map.hdr_copy_buf, &tmp_map.hdr, sizeof(tmp_map.hdr));
+ tmp_map.index = index;
+
+ /* a bit kludgy way to do this, but it initializes everything
+ nicely and correctly */
+ return mail_index_map_clone(&tmp_map);
+}
+
+static void mail_index_record_map_free(struct mail_index_map *map,
+ struct mail_index_record_map *rec_map)
+{
+ if (rec_map->buffer != NULL) {
+ i_assert(rec_map->mmap_base == NULL);
+ buffer_free(&rec_map->buffer);
+ } else if (rec_map->mmap_base != NULL) {
+ i_assert(rec_map->buffer == NULL);
+ if (munmap(rec_map->mmap_base, rec_map->mmap_size) < 0)
+ mail_index_set_syscall_error(map->index, "munmap()");
+ rec_map->mmap_base = NULL;
+ }
+ array_free(&rec_map->maps);
+ if (rec_map->modseq != NULL)
+ mail_index_map_modseq_free(&rec_map->modseq);
+ i_free(rec_map);
+}
+
+static void mail_index_record_map_unlink(struct mail_index_map *map)
+{
+ struct mail_index_map *const *maps;
+ unsigned int idx = UINT_MAX;
+
+ array_foreach(&map->rec_map->maps, maps) {
+ if (*maps == map) {
+ idx = array_foreach_idx(&map->rec_map->maps, maps);
+ break;
+ }
+ }
+ i_assert(idx != UINT_MAX);
+
+ array_delete(&map->rec_map->maps, idx, 1);
+ if (array_count(&map->rec_map->maps) == 0) {
+ mail_index_record_map_free(map, map->rec_map);
+ map->rec_map = NULL;
+ }
+}
+
+void mail_index_unmap(struct mail_index_map **_map)
+{
+ struct mail_index_map *map = *_map;
+
+ *_map = NULL;
+ if (--map->refcount > 0)
+ return;
+
+ i_assert(map->refcount == 0);
+ mail_index_record_map_unlink(map);
+
+ pool_unref(&map->extension_pool);
+ if (array_is_created(&map->keyword_idx_map))
+ array_free(&map->keyword_idx_map);
+ buffer_free(&map->hdr_copy_buf);
+ i_free(map);
+}
+
+static void mail_index_map_copy_records(struct mail_index_record_map *dest,
+ const struct mail_index_record_map *src,
+ unsigned int record_size)
+{
+ size_t size;
+
+ size = src->records_count * record_size;
+ /* +1% so we have a bit of space to grow. useful for huge mailboxes. */
+ dest->buffer = buffer_create_dynamic(default_pool,
+ size + I_MAX(size/100, 1024));
+ buffer_append(dest->buffer, src->records, size);
+
+ dest->records = buffer_get_modifiable_data(dest->buffer, NULL);
+ dest->records_count = src->records_count;
+}
+
+static void mail_index_map_copy_header(struct mail_index_map *dest,
+ const struct mail_index_map *src)
+{
+ /* use src->hdr copy directly, because if we got here
+ from syncing it has the latest changes. */
+ if (src != dest)
+ dest->hdr = src->hdr;
+ if (dest->hdr_copy_buf != NULL) {
+ if (src == dest)
+ return;
+
+ buffer_set_used_size(dest->hdr_copy_buf, 0);
+ } else {
+ dest->hdr_copy_buf =
+ buffer_create_dynamic(default_pool,
+ dest->hdr.header_size);
+ }
+ buffer_append(dest->hdr_copy_buf, &dest->hdr,
+ I_MIN(sizeof(dest->hdr), src->hdr.base_header_size));
+ if (src != dest) {
+ buffer_write(dest->hdr_copy_buf, src->hdr.base_header_size,
+ MAIL_INDEX_MAP_HDR_OFFSET(src, src->hdr.base_header_size),
+ src->hdr.header_size - src->hdr.base_header_size);
+ }
+ i_assert(dest->hdr_copy_buf->used == dest->hdr.header_size);
+}
+
+static struct mail_index_record_map *
+mail_index_record_map_alloc(struct mail_index_map *map)
+{
+ struct mail_index_record_map *rec_map;
+
+ rec_map = i_new(struct mail_index_record_map, 1);
+ i_array_init(&rec_map->maps, 4);
+ array_push_back(&rec_map->maps, &map);
+ return rec_map;
+}
+
+struct mail_index_map *mail_index_map_clone(const struct mail_index_map *map)
+{
+ struct mail_index_map *mem_map;
+ struct mail_index_ext *ext;
+ unsigned int count;
+
+ mem_map = i_new(struct mail_index_map, 1);
+ mem_map->index = map->index;
+ mem_map->refcount = 1;
+ if (map->rec_map == NULL) {
+ mem_map->rec_map = mail_index_record_map_alloc(mem_map);
+ mem_map->rec_map->buffer =
+ buffer_create_dynamic(default_pool, 1024);
+ } else {
+ mem_map->rec_map = map->rec_map;
+ array_push_back(&mem_map->rec_map->maps, &mem_map);
+ }
+
+ mail_index_map_copy_header(mem_map, map);
+
+ /* copy extensions */
+ if (array_is_created(&map->ext_id_map)) {
+ count = array_count(&map->ext_id_map);
+ mail_index_map_init_extbufs(mem_map, count + 2);
+
+ array_append_array(&mem_map->extensions, &map->extensions);
+ array_append_array(&mem_map->ext_id_map, &map->ext_id_map);
+
+ /* fix the name pointers to use our own pool */
+ array_foreach_modifiable(&mem_map->extensions, ext) {
+ i_assert(ext->record_offset + ext->record_size <=
+ mem_map->hdr.record_size);
+ ext->name = p_strdup(mem_map->extension_pool,
+ ext->name);
+ }
+ }
+
+ /* copy keyword map */
+ if (array_is_created(&map->keyword_idx_map)) {
+ i_array_init(&mem_map->keyword_idx_map,
+ array_count(&map->keyword_idx_map) + 4);
+ array_append_array(&mem_map->keyword_idx_map,
+ &map->keyword_idx_map);
+ }
+
+ return mem_map;
+}
+
+void mail_index_record_map_move_to_private(struct mail_index_map *map)
+{
+ struct mail_index_record_map *new_map;
+ const struct mail_index_record *rec;
+
+ if (array_count(&map->rec_map->maps) > 1) {
+ /* Multiple references to the rec_map. Create a clone of the
+ rec_map, which is in memory. */
+ new_map = mail_index_record_map_alloc(map);
+ mail_index_map_copy_records(new_map, map->rec_map,
+ map->hdr.record_size);
+ mail_index_record_map_unlink(map);
+ map->rec_map = new_map;
+ if (map->rec_map->modseq != NULL)
+ new_map->modseq = mail_index_map_modseq_clone(map->rec_map->modseq);
+ } else {
+ new_map = map->rec_map;
+ }
+
+ if (new_map->records_count != map->hdr.messages_count) {
+ /* The rec_map has more messages than what map contains.
+ These messages aren't necessary (and may confuse the caller),
+ so truncate them away. */
+ i_assert(new_map->records_count > map->hdr.messages_count);
+ new_map->records_count = map->hdr.messages_count;
+ if (new_map->records_count == 0)
+ new_map->last_appended_uid = 0;
+ else {
+ rec = MAIL_INDEX_REC_AT_SEQ(map, new_map->records_count);
+ new_map->last_appended_uid = rec->uid;
+ }
+ buffer_set_used_size(new_map->buffer, new_map->records_count *
+ map->hdr.record_size);
+ }
+}
+
+void mail_index_map_move_to_memory(struct mail_index_map *map)
+{
+ struct mail_index_record_map *new_map;
+
+ if (map->rec_map->mmap_base == NULL) {
+ /* rec_map is already in memory */
+ return;
+ }
+
+ /* Move the rec_map contents to memory. If this is the only map that
+ refers to the rec_map, it can be directly replaced and the old
+ content munmap()ed. Otherwise, create a new rec_map for this map. */
+ if (array_count(&map->rec_map->maps) == 1)
+ new_map = map->rec_map;
+ else {
+ new_map = mail_index_record_map_alloc(map);
+ new_map->modseq = map->rec_map->modseq == NULL ? NULL :
+ mail_index_map_modseq_clone(map->rec_map->modseq);
+ }
+
+ mail_index_map_copy_records(new_map, map->rec_map,
+ map->hdr.record_size);
+ mail_index_map_copy_header(map, map);
+
+ if (new_map != map->rec_map) {
+ mail_index_record_map_unlink(map);
+ map->rec_map = new_map;
+ } else {
+ if (munmap(new_map->mmap_base, new_map->mmap_size) < 0)
+ mail_index_set_syscall_error(map->index, "munmap()");
+ new_map->mmap_base = NULL;
+ }
+}
+
+bool mail_index_map_get_ext_idx(struct mail_index_map *map,
+ uint32_t ext_id, uint32_t *idx_r)
+{
+ const uint32_t *id;
+
+ if (!array_is_created(&map->ext_id_map) ||
+ ext_id >= array_count(&map->ext_id_map))
+ return FALSE;
+
+ id = array_idx(&map->ext_id_map, ext_id);
+ *idx_r = *id;
+ return *idx_r != (uint32_t)-1;
+}
+
+static uint32_t mail_index_bsearch_uid(struct mail_index_map *map,
+ uint32_t uid, uint32_t left_idx,
+ int nearest_side)
+{
+ const struct mail_index_record *rec_base, *rec;
+ uint32_t idx, right_idx, record_size;
+
+ i_assert(map->hdr.messages_count <= map->rec_map->records_count);
+
+ rec_base = map->rec_map->records;
+ record_size = map->hdr.record_size;
+
+ idx = left_idx;
+ right_idx = I_MIN(map->hdr.messages_count, uid);
+
+ i_assert(right_idx < INT_MAX);
+ while (left_idx < right_idx) {
+ idx = (left_idx + right_idx) / 2;
+
+ rec = CONST_PTR_OFFSET(rec_base, idx * record_size);
+ if (rec->uid < uid)
+ left_idx = idx+1;
+ else if (rec->uid > uid)
+ right_idx = idx;
+ else
+ break;
+ }
+ i_assert(idx < map->hdr.messages_count);
+
+ rec = CONST_PTR_OFFSET(rec_base, idx * record_size);
+ if (rec->uid != uid) {
+ if (nearest_side > 0) {
+ /* we want uid or larger */
+ return rec->uid > uid ? idx+1 :
+ (idx == map->hdr.messages_count-1 ? 0 : idx+2);
+ } else {
+ /* we want uid or smaller */
+ return rec->uid < uid ? idx + 1 : idx;
+ }
+ }
+
+ return idx+1;
+}
+
+void mail_index_map_lookup_seq_range(struct mail_index_map *map,
+ uint32_t first_uid, uint32_t last_uid,
+ uint32_t *first_seq_r,
+ uint32_t *last_seq_r)
+{
+ i_assert(first_uid > 0);
+ i_assert(first_uid <= last_uid);
+
+ if (map->hdr.messages_count == 0) {
+ *first_seq_r = *last_seq_r = 0;
+ return;
+ }
+
+ *first_seq_r = mail_index_bsearch_uid(map, first_uid, 0, 1);
+ if (*first_seq_r == 0 ||
+ MAIL_INDEX_REC_AT_SEQ(map, *first_seq_r)->uid > last_uid) {
+ *first_seq_r = *last_seq_r = 0;
+ return;
+ }
+
+ if (last_uid >= map->hdr.next_uid-1) {
+ /* we want the last message */
+ last_uid = map->hdr.next_uid-1;
+ if (first_uid > last_uid) {
+ *first_seq_r = *last_seq_r = 0;
+ return;
+ }
+
+ *last_seq_r = map->hdr.messages_count;
+ return;
+ }
+
+ if (first_uid == last_uid)
+ *last_seq_r = *first_seq_r;
+ else {
+ /* optimization - binary lookup only from right side: */
+ *last_seq_r = mail_index_bsearch_uid(map, last_uid,
+ *first_seq_r - 1, -1);
+ }
+ i_assert(*last_seq_r >= *first_seq_r);
+}
diff --git a/src/lib-index/mail-index-modseq.c b/src/lib-index/mail-index-modseq.c
new file mode 100644
index 0000000..4285983
--- /dev/null
+++ b/src/lib-index/mail-index-modseq.c
@@ -0,0 +1,733 @@
+/* Copyright (c) 2008-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "mail-transaction-log-private.h"
+#include "mail-index-private.h"
+#include "mail-index-sync-private.h"
+#include "mail-index-modseq.h"
+
+ARRAY_DEFINE_TYPE(modseqs, uint64_t);
+
+enum modseq_metadata_idx {
+ /* must be in the same order as enum mail_flags */
+ METADATA_MODSEQ_IDX_ANSWERED = 0,
+ METADATA_MODSEQ_IDX_FLAGGED,
+ METADATA_MODSEQ_IDX_DELETED,
+ METADATA_MODSEQ_IDX_SEEN,
+ METADATA_MODSEQ_IDX_DRAFT,
+
+ METADATA_MODSEQ_IDX_KEYWORD_START
+};
+
+struct metadata_modseqs {
+ ARRAY_TYPE(modseqs) modseqs;
+};
+
+struct mail_index_map_modseq {
+ /* indexes use enum modseq_metadata_idx */
+ ARRAY(struct metadata_modseqs) metadata_modseqs;
+};
+
+struct mail_index_modseq_sync {
+ struct mail_index_sync_map_ctx *sync_map_ctx;
+ struct mail_index_view *view;
+ struct mail_transaction_log_view *log_view;
+ struct mail_index_map_modseq *mmap;
+};
+
+void mail_index_modseq_init(struct mail_index *index)
+{
+ index->modseq_ext_id =
+ mail_index_ext_register(index, MAIL_INDEX_MODSEQ_EXT_NAME,
+ sizeof(struct mail_index_modseq_header),
+ sizeof(uint64_t), sizeof(uint64_t));
+}
+
+static uint64_t mail_index_modseq_get_head(struct mail_index *index)
+{
+ return index->log->head == NULL ? 1 :
+ I_MAX(index->log->head->sync_highest_modseq, 1);
+}
+
+void mail_index_modseq_enable(struct mail_index *index)
+{
+ struct mail_index_transaction *trans;
+ struct mail_index_view *view;
+ struct mail_index_modseq_header hdr;
+ uint32_t ext_map_idx;
+
+ if (index->modseqs_enabled)
+ return;
+
+ if (!mail_index_map_get_ext_idx(index->map, index->modseq_ext_id,
+ &ext_map_idx)) {
+ /* modseqs not enabled to the index yet, add them. */
+ view = mail_index_view_open(index);
+ trans = mail_index_transaction_begin(view, 0);
+
+ i_zero(&hdr);
+ hdr.highest_modseq = mail_index_modseq_get_head(index);
+ mail_index_update_header_ext(trans, index->modseq_ext_id,
+ 0, &hdr, sizeof(hdr));
+
+ /* commit also refreshes the index, which syncs the modseqs */
+ (void)mail_index_transaction_commit(&trans);
+ mail_index_view_close(&view);
+
+ /* get the modseq extension to index map */
+ if (!mail_index_map_get_ext_idx(index->map,
+ index->modseq_ext_id,
+ &ext_map_idx)) {
+ /* didn't work for some reason */
+ return;
+ }
+ }
+ index->modseqs_enabled = TRUE;
+}
+
+bool mail_index_have_modseq_tracking(struct mail_index *index)
+{
+ return mail_index_map_get_modseq_header(index->map) != NULL;
+}
+
+void mail_index_modseq_hdr_snapshot_update(struct mail_index_map *map)
+{
+ const struct mail_index_modseq_header *hdr =
+ mail_index_map_get_modseq_header(map);
+ if (hdr != NULL)
+ map->modseq_hdr_snapshot = *hdr;
+ else
+ i_zero(&map->modseq_hdr_snapshot);
+}
+
+const struct mail_index_modseq_header *
+mail_index_map_get_modseq_header(struct mail_index_map *map)
+{
+ const struct mail_index_ext *ext;
+ uint32_t idx;
+
+ if (!mail_index_map_get_ext_idx(map, map->index->modseq_ext_id, &idx))
+ return NULL;
+
+ ext = array_idx(&map->extensions, idx);
+ if (ext->hdr_size != sizeof(struct mail_index_modseq_header))
+ return NULL;
+
+ return MAIL_INDEX_MAP_HDR_OFFSET(map, ext->hdr_offset);
+}
+
+uint64_t mail_index_map_modseq_get_highest(struct mail_index_map *map)
+{
+ const struct mail_index_modseq_header *modseq_hdr;
+
+ modseq_hdr = mail_index_map_get_modseq_header(map);
+ if (modseq_hdr != NULL && modseq_hdr->highest_modseq != 0)
+ return modseq_hdr->highest_modseq;
+ else {
+ /* fallback to returning the log head. if modseqs aren't
+ enabled, we return 0. */
+ return map->index->log->head == NULL ? 0 :
+ map->index->log->head->sync_highest_modseq;
+ }
+}
+
+uint64_t mail_index_modseq_get_highest(struct mail_index_view *view)
+{
+ return mail_index_map_modseq_get_highest(view->map);
+}
+
+static struct mail_index_map_modseq *
+mail_index_map_modseq(struct mail_index_view *view)
+{
+ struct mail_index_map_modseq *mmap = view->map->rec_map->modseq;
+ uint32_t ext_map_idx;
+
+ if (mmap != NULL)
+ return mmap;
+
+ /* don't start tracking until we've seen modseq extension intro */
+ if (!mail_index_map_get_ext_idx(view->map, view->index->modseq_ext_id,
+ &ext_map_idx))
+ return NULL;
+
+ mmap = i_new(struct mail_index_map_modseq, 1);
+ i_array_init(&mmap->metadata_modseqs,
+ METADATA_MODSEQ_IDX_KEYWORD_START +
+ array_count(&view->index->keywords));
+ view->map->rec_map->modseq = mmap;
+ return mmap;
+}
+
+uint64_t mail_index_modseq_lookup(struct mail_index_view *view, uint32_t seq)
+{
+ struct mail_index_map_modseq *mmap = mail_index_map_modseq(view);
+ struct mail_index_map *map;
+ const struct mail_index_ext *ext;
+ const struct mail_index_record *rec;
+ const uint64_t *modseqp;
+ uint32_t ext_map_idx;
+
+ if (mmap == NULL)
+ return mail_index_modseq_get_head(view->index);
+
+ rec = mail_index_lookup_full(view, seq, &map, NULL);
+ if (!mail_index_map_get_ext_idx(map, view->index->modseq_ext_id,
+ &ext_map_idx)) {
+ /* not enabled yet */
+ return mail_index_modseq_get_head(view->index);
+ }
+
+ ext = array_idx(&map->extensions, ext_map_idx);
+ modseqp = CONST_PTR_OFFSET(rec, ext->record_offset);
+ if (*modseqp == 0) {
+ /* If we're here because we just enabled modseqs, we'll return
+ the same modseq (initial highestmodseq) for all messages.
+ The next sync will change these zeros to initial
+ highestmodseq or higher.
+
+ If we're here because a message got appended but modseq
+ wasn't set (older Dovecot?), we'll again use the current
+ highest modseq. This isn't exactly correct, but it gets
+ fixed after the next sync and this situation shouldn't
+ normally happen anyway. */
+ return mail_index_modseq_get_highest(view);
+ }
+ return *modseqp;
+}
+
+int mail_index_modseq_set(struct mail_index_view *view,
+ uint32_t seq, uint64_t min_modseq)
+{
+ struct mail_index_map_modseq *mmap = mail_index_map_modseq(view);
+ const struct mail_index_ext *ext;
+ struct mail_index_record *rec;
+ uint64_t *modseqp;
+ uint32_t ext_map_idx;
+
+ if (mmap == NULL)
+ return -1;
+
+ rec = MAIL_INDEX_REC_AT_SEQ(view->map, seq);
+ if (!mail_index_map_get_ext_idx(view->map, view->index->modseq_ext_id,
+ &ext_map_idx))
+ return -1;
+
+ ext = array_idx(&view->map->extensions, ext_map_idx);
+ modseqp = PTR_OFFSET(rec, ext->record_offset);
+ if (*modseqp > min_modseq)
+ return 0;
+ else {
+ *modseqp = min_modseq;
+ return 1;
+ }
+}
+
+static uint64_t
+modseq_idx_lookup(struct mail_index_map_modseq *mmap,
+ unsigned int idx, uint32_t seq)
+{
+ const struct metadata_modseqs *metadata;
+ const uint64_t *modseqs;
+ unsigned int count;
+
+ metadata = array_get(&mmap->metadata_modseqs, &count);
+ if (idx >= count || !array_is_created(&metadata[idx].modseqs))
+ return 0;
+
+ modseqs = array_get(&metadata[idx].modseqs, &count);
+ return seq > count ? 0 : modseqs[seq-1];
+}
+
+uint64_t mail_index_modseq_lookup_flags(struct mail_index_view *view,
+ enum mail_flags flags_mask,
+ uint32_t seq)
+{
+ struct mail_index_map_modseq *mmap = mail_index_map_modseq(view);
+ unsigned int i;
+ uint64_t modseq, highest_modseq = 0;
+
+ if (mmap != NULL) {
+ /* first try to find a specific match */
+ for (i = 0; i < METADATA_MODSEQ_IDX_KEYWORD_START; i++) {
+ if ((flags_mask & (1 << i)) != 0) {
+ modseq = modseq_idx_lookup(mmap, i, seq);
+ if (highest_modseq < modseq)
+ highest_modseq = modseq;
+ }
+ }
+ }
+
+ if (highest_modseq == 0) {
+ /* no specific matches, fallback to using the highest */
+ highest_modseq = mail_index_modseq_lookup(view, seq);
+ }
+ return highest_modseq;
+}
+
+uint64_t mail_index_modseq_lookup_keywords(struct mail_index_view *view,
+ const struct mail_keywords *keywords,
+ uint32_t seq)
+{
+ struct mail_index_map_modseq *mmap = mail_index_map_modseq(view);
+ unsigned int i, metadata_idx;
+ uint64_t modseq, highest_modseq = 0;
+
+ if (mmap != NULL) {
+ /* first try to find a specific match */
+ for (i = 0; i < keywords->count; i++) {
+ metadata_idx = METADATA_MODSEQ_IDX_KEYWORD_START +
+ keywords->idx[i];
+
+ modseq = modseq_idx_lookup(mmap, metadata_idx, seq);
+ if (highest_modseq < modseq)
+ highest_modseq = modseq;
+ }
+ }
+
+ if (highest_modseq == 0) {
+ /* no specific matches, fallback to using the highest */
+ highest_modseq = mail_index_modseq_lookup(view, seq);
+ }
+ return highest_modseq;
+}
+
+static void
+mail_index_modseq_update(struct mail_index_modseq_sync *ctx,
+ uint64_t modseq, bool nonzeros,
+ uint32_t seq1, uint32_t seq2)
+{
+ const struct mail_index_ext *ext;
+ struct mail_index_record *rec;
+ uint32_t ext_map_idx;
+ uint64_t *modseqp;
+
+ if (!mail_index_map_get_ext_idx(ctx->view->map,
+ ctx->view->index->modseq_ext_id,
+ &ext_map_idx))
+ return;
+
+ ext = array_idx(&ctx->view->map->extensions, ext_map_idx);
+ for (; seq1 <= seq2; seq1++) {
+ rec = MAIL_INDEX_REC_AT_SEQ(ctx->view->map, seq1);
+ modseqp = PTR_OFFSET(rec, ext->record_offset);
+ if (*modseqp == 0 || (nonzeros && *modseqp < modseq))
+ *modseqp = modseq;
+ }
+}
+
+static bool
+mail_index_modseq_update_to_highest(struct mail_index_modseq_sync *ctx,
+ uint32_t seq1, uint32_t seq2)
+{
+ uint64_t modseq;
+
+ if (ctx->mmap == NULL)
+ return FALSE;
+
+ modseq = mail_transaction_log_view_get_prev_modseq(ctx->log_view);
+ mail_index_modseq_update(ctx, modseq, TRUE, seq1, seq2);
+ return TRUE;
+}
+
+static void
+mail_index_modseq_update_old_rec(struct mail_index_modseq_sync *ctx,
+ const struct mail_transaction_header *thdr,
+ const void *tdata)
+{
+ ARRAY_TYPE(seq_range) uids = ARRAY_INIT;
+ const struct seq_range *rec;
+ buffer_t uid_buf;
+ unsigned int i, count;
+ uint32_t seq1, seq2;
+
+ switch (thdr->type & MAIL_TRANSACTION_TYPE_MASK) {
+ case MAIL_TRANSACTION_APPEND: {
+ const struct mail_index_record *appends = tdata;
+
+ count = thdr->size / sizeof(*appends);
+ for (i = 0; i < count; i++) {
+ if (mail_index_lookup_seq(ctx->view,
+ appends[i].uid, &seq1)) {
+ (void)mail_index_modseq_update_to_highest(ctx, seq1, seq1);
+ }
+ }
+ return;
+ }
+ case MAIL_TRANSACTION_FLAG_UPDATE: {
+ buffer_create_from_const_data(&uid_buf, tdata, thdr->size);
+ array_create_from_buffer(&uids, &uid_buf,
+ sizeof(struct mail_transaction_flag_update));
+ break;
+ }
+ case MAIL_TRANSACTION_KEYWORD_UPDATE: {
+ const struct mail_transaction_keyword_update *rec = tdata;
+ unsigned int seqset_offset;
+
+ seqset_offset = sizeof(*rec) + rec->name_size;
+ if ((seqset_offset % 4) != 0)
+ seqset_offset += 4 - (seqset_offset % 4);
+
+ buffer_create_from_const_data(&uid_buf,
+ CONST_PTR_OFFSET(tdata, seqset_offset),
+ thdr->size - seqset_offset);
+ array_create_from_buffer(&uids, &uid_buf, sizeof(uint32_t)*2);
+ break;
+ }
+ case MAIL_TRANSACTION_KEYWORD_RESET:
+ buffer_create_from_const_data(&uid_buf, tdata, thdr->size);
+ array_create_from_buffer(&uids, &uid_buf,
+ sizeof(struct mail_transaction_keyword_reset));
+ break;
+ case MAIL_TRANSACTION_ATTRIBUTE_UPDATE:
+ break;
+ default:
+ return;
+ }
+
+ /* update modseqs */
+ count = array_is_created(&uids) ? array_count(&uids) : 0;
+ for (i = 0; i < count; i++) {
+ rec = array_idx(&uids, i);
+ if (mail_index_lookup_seq_range(ctx->view, rec->seq1, rec->seq2,
+ &seq1, &seq2))
+ (void)mail_index_modseq_update_to_highest(ctx, seq1, seq2);
+ }
+}
+
+static void mail_index_modseq_sync_init(struct mail_index_modseq_sync *ctx)
+{
+ struct mail_index_map *map = ctx->view->map;
+ const struct mail_index_ext *ext;
+ const struct mail_index_modseq_header *hdr;
+ const struct mail_transaction_header *thdr;
+ const void *tdata;
+ const char *reason;
+ uint32_t ext_map_idx;
+ uint32_t end_seq;
+ uoff_t end_offset;
+ uint64_t cur_modseq;
+ bool reset;
+ int ret;
+
+ if (!mail_index_map_get_ext_idx(map, ctx->view->index->modseq_ext_id,
+ &ext_map_idx))
+ i_unreached();
+ ext = array_idx(&map->extensions, ext_map_idx);
+
+ /* get the current highest_modseq. don't change any modseq below it. */
+ hdr = MAIL_INDEX_MAP_HDR_OFFSET(map, ext->hdr_offset);
+
+ /* Scan logs for updates between ext_hdr.log_* .. view position.
+ There are two reasons why there could be any:
+
+ 1) We just enabled modseqs and we're filling the initial values.
+ 2) A non-modseq-aware Dovecot version added new messages and wrote
+ dovecot.index file. */
+ mail_transaction_log_view_get_prev_pos(ctx->view->log_view,
+ &end_seq, &end_offset);
+ if (end_seq < hdr->log_seq ||
+ (end_seq == hdr->log_seq && end_offset <= hdr->log_offset)) {
+ /* modseqs are up to date */
+ return;
+ }
+
+ ctx->log_view = mail_transaction_log_view_open(ctx->view->index->log);
+ ret = mail_transaction_log_view_set(ctx->log_view,
+ I_MAX(1, hdr->log_seq),
+ hdr->log_offset,
+ end_seq, end_offset, &reset, &reason);
+ if (ret <= 0) {
+ /* missing files / error - try with only the last file */
+ ret = mail_transaction_log_view_set(ctx->log_view, end_seq, 0,
+ end_seq, end_offset,
+ &reset, &reason);
+ /* since we don't know if we skipped some changes, set all
+ modseqs to beginning of the latest file. */
+ cur_modseq = mail_transaction_log_view_get_prev_modseq(
+ ctx->log_view);
+ if (cur_modseq < hdr->highest_modseq) {
+ /* should happen only when setting initial modseqs.
+ we may already have returned highest_modseq as
+ some messages' modseq value. don't shrink it. */
+ cur_modseq = hdr->highest_modseq;
+ }
+ mail_index_modseq_update(ctx, cur_modseq, TRUE, 1,
+ map->hdr.messages_count);
+ } else {
+ /* we have all the logs. replace zero modseqs with the current
+ highest modseq (we may have already returned it for them). */
+ mail_index_modseq_update(ctx, hdr->highest_modseq, FALSE, 1,
+ map->hdr.messages_count);
+ }
+ if (ret > 0) {
+ while (mail_transaction_log_view_next(ctx->log_view,
+ &thdr, &tdata) > 0) {
+ T_BEGIN {
+ mail_index_modseq_update_old_rec(ctx, thdr,
+ tdata);
+ } T_END;
+ }
+ }
+ mail_transaction_log_view_close(&ctx->log_view);
+}
+
+struct mail_index_modseq_sync *
+mail_index_modseq_sync_begin(struct mail_index_sync_map_ctx *sync_map_ctx)
+{
+ struct mail_index_modseq_sync *ctx;
+
+ ctx = i_new(struct mail_index_modseq_sync, 1);
+ ctx->sync_map_ctx = sync_map_ctx;
+ ctx->view = sync_map_ctx->view;
+ ctx->mmap = mail_index_map_modseq(ctx->view);
+ if (ctx->mmap != NULL) {
+ mail_index_modseq_sync_init(ctx);
+ ctx->log_view = ctx->view->log_view;
+ }
+ return ctx;
+}
+
+static void mail_index_modseq_update_header(struct mail_index_modseq_sync *ctx)
+{
+ struct mail_index_view *view = ctx->view;
+ struct mail_index_map *map = view->map;
+ const struct mail_index_ext *ext;
+ const struct mail_index_modseq_header *old_modseq_hdr;
+ struct mail_index_modseq_header new_modseq_hdr;
+ uint32_t ext_map_idx, log_seq;
+ uoff_t log_offset;
+ uint64_t highest_modseq;
+
+ if (!mail_index_map_get_ext_idx(map, view->index->modseq_ext_id,
+ &ext_map_idx))
+ return;
+
+ mail_transaction_log_view_get_prev_pos(view->log_view,
+ &log_seq, &log_offset);
+ highest_modseq = mail_transaction_log_view_get_prev_modseq(view->log_view);
+
+ ext = array_idx(&map->extensions, ext_map_idx);
+ old_modseq_hdr = MAIL_INDEX_MAP_HDR_OFFSET(map, ext->hdr_offset);
+
+ if (old_modseq_hdr->log_seq < log_seq ||
+ (old_modseq_hdr->log_seq == log_seq &&
+ old_modseq_hdr->log_offset < log_offset)) {
+ i_zero(&new_modseq_hdr);
+ new_modseq_hdr.highest_modseq = highest_modseq;
+ new_modseq_hdr.log_seq = log_seq;
+ new_modseq_hdr.log_offset = log_offset;
+
+ buffer_write(map->hdr_copy_buf, ext->hdr_offset,
+ &new_modseq_hdr, sizeof(new_modseq_hdr));
+ i_assert(map->hdr_copy_buf->used == map->hdr.header_size);
+ }
+}
+
+void mail_index_modseq_sync_end(struct mail_index_modseq_sync **_ctx)
+{
+ struct mail_index_modseq_sync *ctx = *_ctx;
+
+ *_ctx = NULL;
+ if (ctx->mmap != NULL) {
+ i_assert(ctx->mmap == ctx->view->map->rec_map->modseq);
+ mail_index_modseq_update_header(ctx);
+ }
+ i_free(ctx);
+}
+
+void mail_index_modseq_sync_map_replaced(struct mail_index_modseq_sync *ctx)
+{
+ ctx->mmap = mail_index_map_modseq(ctx->view);
+}
+
+void mail_index_modseq_hdr_update(struct mail_index_modseq_sync *ctx)
+{
+ if (ctx->mmap == NULL) {
+ ctx->mmap = mail_index_map_modseq(ctx->view);
+ i_assert(ctx->mmap != NULL);
+ mail_index_modseq_sync_init(ctx);
+ ctx->log_view = ctx->view->log_view;
+ }
+}
+
+void mail_index_modseq_append(struct mail_index_modseq_sync *ctx, uint32_t seq)
+{
+ (void)mail_index_modseq_update_to_highest(ctx, seq, seq);
+}
+
+void mail_index_modseq_expunge(struct mail_index_modseq_sync *ctx,
+ uint32_t seq1, uint32_t seq2)
+{
+ struct metadata_modseqs *metadata;
+
+ if (ctx->mmap == NULL)
+ return;
+
+ seq1--;
+ array_foreach_modifiable(&ctx->mmap->metadata_modseqs, metadata) {
+ if (array_is_created(&metadata->modseqs))
+ array_delete(&metadata->modseqs, seq1, seq2-seq1);
+ }
+}
+
+static void
+modseqs_update(ARRAY_TYPE(modseqs) *array, uint32_t seq1, uint32_t seq2,
+ uint64_t value)
+{
+ uint64_t *modseqp;
+
+ for (; seq1 <= seq2; seq1++) {
+ modseqp = array_idx_get_space(array, seq1-1);
+ if (*modseqp < value)
+ *modseqp = value;
+ }
+}
+
+static void
+modseqs_idx_update(struct mail_index_modseq_sync *ctx, unsigned int idx,
+ uint32_t seq1, uint32_t seq2)
+{
+ struct metadata_modseqs *metadata;
+ uint64_t modseq;
+
+ if (!ctx->view->index->modseqs_enabled) {
+ /* we want to keep permanent modseqs updated, but don't bother
+ updating in-memory per-flag updates */
+ return;
+ }
+
+ modseq = mail_transaction_log_view_get_prev_modseq(ctx->log_view);
+ metadata = array_idx_get_space(&ctx->mmap->metadata_modseqs, idx);
+ if (!array_is_created(&metadata->modseqs))
+ i_array_init(&metadata->modseqs, seq2 + 16);
+ modseqs_update(&metadata->modseqs, seq1, seq2, modseq);
+}
+
+void mail_index_modseq_update_flags(struct mail_index_modseq_sync *ctx,
+ enum mail_flags flags_mask,
+ uint32_t seq1, uint32_t seq2)
+{
+ unsigned int i;
+
+ if (!mail_index_modseq_update_to_highest(ctx, seq1, seq2))
+ return;
+
+ for (i = 0; i < METADATA_MODSEQ_IDX_KEYWORD_START; i++) {
+ if ((flags_mask & (1 << i)) != 0)
+ modseqs_idx_update(ctx, i, seq1, seq2);
+ }
+}
+
+void mail_index_modseq_update_keyword(struct mail_index_modseq_sync *ctx,
+ unsigned int keyword_idx,
+ uint32_t seq1, uint32_t seq2)
+{
+ if (!mail_index_modseq_update_to_highest(ctx, seq1, seq2))
+ return;
+
+ modseqs_idx_update(ctx, METADATA_MODSEQ_IDX_KEYWORD_START + keyword_idx,
+ seq1, seq2);
+}
+
+void mail_index_modseq_reset_keywords(struct mail_index_modseq_sync *ctx,
+ uint32_t seq1, uint32_t seq2)
+{
+ unsigned int i, count;
+
+ if (!mail_index_modseq_update_to_highest(ctx, seq1, seq2))
+ return;
+
+ count = array_count(&ctx->mmap->metadata_modseqs);
+ for (i = METADATA_MODSEQ_IDX_KEYWORD_START; i < count; i++)
+ modseqs_idx_update(ctx, i, seq1, seq2);
+}
+
+struct mail_index_map_modseq *
+mail_index_map_modseq_clone(const struct mail_index_map_modseq *mmap)
+{
+ struct mail_index_map_modseq *new_mmap;
+ const struct metadata_modseqs *src_metadata;
+ struct metadata_modseqs *dest_metadata;
+ unsigned int i, count;
+
+ src_metadata = array_get(&mmap->metadata_modseqs, &count);
+
+ new_mmap = i_new(struct mail_index_map_modseq, 1);
+ i_array_init(&new_mmap->metadata_modseqs, count + 16);
+
+ for (i = 0; i < count; i++) {
+ dest_metadata = array_append_space(&new_mmap->metadata_modseqs);
+ if (array_is_created(&src_metadata[i].modseqs)) {
+ i_array_init(&dest_metadata->modseqs,
+ array_count(&src_metadata[i].modseqs));
+ array_append_array(&dest_metadata->modseqs,
+ &src_metadata[i].modseqs);
+ }
+ }
+ return new_mmap;
+}
+
+void mail_index_map_modseq_free(struct mail_index_map_modseq **_mmap)
+{
+ struct mail_index_map_modseq *mmap = *_mmap;
+ struct metadata_modseqs *metadata;
+
+ *_mmap = NULL;
+
+ array_foreach_modifiable(&mmap->metadata_modseqs, metadata) {
+ if (array_is_created(&metadata->modseqs))
+ array_free(&metadata->modseqs);
+ }
+ array_free(&mmap->metadata_modseqs);
+ i_free(mmap);
+}
+
+bool mail_index_modseq_get_next_log_offset(struct mail_index_view *view,
+ uint64_t modseq, uint32_t *log_seq_r,
+ uoff_t *log_offset_r)
+{
+ struct mail_transaction_log *log = view->index->log;
+ struct mail_transaction_log_file *file, *prev_file;
+ const char *reason;
+ int ret;
+
+ if (log->files == NULL) {
+ /* we shouldn't normally get here */
+ return FALSE;
+ }
+ while (modseq < log->files->hdr.initial_modseq) {
+ /* try to find the previous log file if it still exists */
+ ret = mail_transaction_log_find_file(log,
+ log->files->hdr.file_seq - 1, FALSE, &file, &reason);
+ if (ret <= 0)
+ return FALSE;
+ }
+
+ prev_file = NULL;
+ for (file = log->files; file != NULL; file = file->next) {
+ if (modseq < file->hdr.initial_modseq)
+ break;
+ prev_file = file;
+ }
+
+ if (prev_file == NULL) {
+ /* the log file has been deleted already */
+ return FALSE;
+ }
+
+ *log_seq_r = prev_file->hdr.file_seq;
+ if (mail_transaction_log_file_get_modseq_next_offset(prev_file, modseq,
+ log_offset_r) < 0)
+ return FALSE;
+
+ if (*log_seq_r > view->log_file_head_seq ||
+ (*log_seq_r == view->log_file_head_seq &&
+ *log_offset_r > view->log_file_head_offset)) {
+ /* modseq is already beyond our view. move it back so the
+ caller won't be confused. */
+ *log_seq_r = view->log_file_head_seq;
+ *log_offset_r = view->log_file_head_offset;
+ }
+ return TRUE;
+}
diff --git a/src/lib-index/mail-index-modseq.h b/src/lib-index/mail-index-modseq.h
new file mode 100644
index 0000000..f0fe48a
--- /dev/null
+++ b/src/lib-index/mail-index-modseq.h
@@ -0,0 +1,66 @@
+#ifndef MAIL_INDEX_MODSEQ_H
+#define MAIL_INDEX_MODSEQ_H
+
+#include "mail-types.h"
+
+#define MAIL_INDEX_MODSEQ_EXT_NAME "modseq"
+
+struct mail_keywords;
+struct mail_index;
+struct mail_index_map;
+struct mail_index_view;
+struct mail_index_modseq;
+struct mail_index_map_modseq;
+struct mail_index_sync_map_ctx;
+
+void mail_index_modseq_init(struct mail_index *index);
+
+/* Save a copy of the current modseq header to map->modseq_hdr_snapshot. This
+ is expected to be called when reading the dovecot.index header before any
+ changes are applied on top of it from dovecot.index.log. */
+void mail_index_modseq_hdr_snapshot_update(struct mail_index_map *map);
+
+const struct mail_index_modseq_header *
+mail_index_map_get_modseq_header(struct mail_index_map *map);
+uint64_t mail_index_map_modseq_get_highest(struct mail_index_map *map);
+void mail_index_modseq_enable(struct mail_index *index);
+bool mail_index_have_modseq_tracking(struct mail_index *index);
+uint64_t mail_index_modseq_get_highest(struct mail_index_view *view);
+
+uint64_t mail_index_modseq_lookup(struct mail_index_view *view, uint32_t seq);
+uint64_t mail_index_modseq_lookup_flags(struct mail_index_view *view,
+ enum mail_flags flags_mask,
+ uint32_t seq);
+uint64_t mail_index_modseq_lookup_keywords(struct mail_index_view *view,
+ const struct mail_keywords *keywords,
+ uint32_t seq);
+int mail_index_modseq_set(struct mail_index_view *view,
+ uint32_t seq, uint64_t min_modseq);
+
+struct mail_index_modseq_sync *
+mail_index_modseq_sync_begin(struct mail_index_sync_map_ctx *sync_map_ctx);
+void mail_index_modseq_sync_end(struct mail_index_modseq_sync **ctx);
+
+void mail_index_modseq_sync_map_replaced(struct mail_index_modseq_sync *ctx);
+void mail_index_modseq_hdr_update(struct mail_index_modseq_sync *ctx);
+void mail_index_modseq_append(struct mail_index_modseq_sync *ctx, uint32_t seq);
+void mail_index_modseq_expunge(struct mail_index_modseq_sync *ctx,
+ uint32_t seq1, uint32_t seq2);
+void mail_index_modseq_update_flags(struct mail_index_modseq_sync *ctx,
+ enum mail_flags flags_mask,
+ uint32_t seq1, uint32_t seq2);
+void mail_index_modseq_update_keyword(struct mail_index_modseq_sync *ctx,
+ unsigned int keyword_idx,
+ uint32_t seq1, uint32_t seq2);
+void mail_index_modseq_reset_keywords(struct mail_index_modseq_sync *ctx,
+ uint32_t seq1, uint32_t seq2);
+
+struct mail_index_map_modseq *
+mail_index_map_modseq_clone(const struct mail_index_map_modseq *mmap);
+void mail_index_map_modseq_free(struct mail_index_map_modseq **mmap);
+
+bool mail_index_modseq_get_next_log_offset(struct mail_index_view *view,
+ uint64_t modseq, uint32_t *log_seq_r,
+ uoff_t *log_offset_r);
+
+#endif
diff --git a/src/lib-index/mail-index-private.h b/src/lib-index/mail-index-private.h
new file mode 100644
index 0000000..26f7e39
--- /dev/null
+++ b/src/lib-index/mail-index-private.h
@@ -0,0 +1,437 @@
+#ifndef MAIL_INDEX_PRIVATE_H
+#define MAIL_INDEX_PRIVATE_H
+
+#include "file-lock.h"
+#include "mail-index.h"
+#include "mail-index-util.h"
+#include "mail-index-view-private.h"
+#include "mail-index-transaction-private.h"
+
+#include <sys/stat.h>
+
+struct mail_transaction_header;
+struct mail_transaction_log_view;
+struct mail_index_sync_map_ctx;
+
+/* How large index files to mmap() instead of reading to memory. */
+#define MAIL_INDEX_MMAP_MIN_SIZE (1024*64)
+/* How many times to retry opening index files if read/fstat returns ESTALE.
+ This happens with NFS when the file has been deleted (ie. index file was
+ rewritten by another computer than us). */
+#define MAIL_INDEX_ESTALE_RETRY_COUNT NFS_ESTALE_RETRY_COUNT
+/* Large extension header sizes are probably caused by file corruption, so
+ try to catch them by limiting the header size. */
+#define MAIL_INDEX_EXT_HEADER_MAX_SIZE (1024*1024*16-1)
+
+#define MAIL_INDEX_IS_IN_MEMORY(index) \
+ ((index)->dir == NULL)
+
+#define MAIL_INDEX_MAP_IS_IN_MEMORY(map) \
+ ((map)->rec_map->mmap_base == NULL)
+
+#define MAIL_INDEX_MAP_IDX(map, idx) \
+ ((struct mail_index_record *) \
+ PTR_OFFSET((map)->rec_map->records, (idx) * (map)->hdr.record_size))
+#define MAIL_INDEX_REC_AT_SEQ(map, seq) \
+ ((struct mail_index_record *) \
+ PTR_OFFSET((map)->rec_map->records, ((seq)-1) * (map)->hdr.record_size))
+
+#define MAIL_TRANSACTION_FLAG_UPDATE_IS_INTERNAL(u) \
+ ((((u)->add_flags | (u)->remove_flags) & MAIL_INDEX_FLAGS_MASK) == 0 && \
+ (u)->modseq_inc_flag == 0)
+
+#define MAIL_INDEX_EXT_KEYWORDS "keywords"
+#define MAIL_INDEX_EXT_NAME_MAX_LENGTH 64
+
+typedef int mail_index_expunge_handler_t(struct mail_index_sync_map_ctx *ctx,
+ const void *data, void **sync_context);
+
+#define MAIL_INDEX_HEADER_SIZE_ALIGN(size) \
+ (((size) + 7) & ~7U)
+
+/* In-memory copy of struct mail_index_ext_header */
+struct mail_index_ext {
+ const char *name;
+ uint32_t index_idx; /* index ext_id */
+ uint32_t reset_id;
+ uint32_t ext_offset; /* points to beginning of mail_index_ext_header */
+ uint32_t hdr_offset; /* points to mail_index_ext_header.data[] */
+ uint32_t hdr_size; /* size of mail_index_ext_header.data[] */
+ uint16_t record_offset;
+ uint16_t record_size;
+ uint16_t record_align;
+};
+
+struct mail_index_ext_header {
+ /* Size of data[], i.e. the extension size in header */
+ uint32_t hdr_size;
+ /* If reset_id changes, all of the extension record data is
+ invalidated. For example with cache files reset_id must match the
+ cache header's file_seq or the cache offsets aren't valid. */
+ uint32_t reset_id;
+ /* Offset of this extension in struct mail_index_record. */
+ uint16_t record_offset;
+ /* Size of this extension in struct mail_index_record. */
+ uint16_t record_size;
+ /* Required alignment of this extension in struct mail_index_record.
+ It's expected that record_offset is correctly aligned. This is used
+ only when rearranging fields due to adding/removing other
+ extensions. */
+ uint16_t record_align;
+ /* Size of name[], which contains the extension's unique name. */
+ uint16_t name_size;
+ /* unsigned char name[name_size]; */
+ /* Extension header data, if any. This starts from the next 64-bit
+ aligned offset after name[]. */
+ /* unsigned char data[hdr_size]; */
+};
+
+struct mail_index_keyword_header {
+ uint32_t keywords_count;
+ /* struct mail_index_keyword_header_rec[] */
+ /* char name[][] */
+};
+
+struct mail_index_keyword_header_rec {
+ uint32_t unused; /* for backwards compatibility */
+ uint32_t name_offset; /* relative to beginning of name[] */
+};
+
+enum mail_index_sync_handler_type {
+ MAIL_INDEX_SYNC_HANDLER_FILE = 0x01,
+ MAIL_INDEX_SYNC_HANDLER_HEAD = 0x02,
+ MAIL_INDEX_SYNC_HANDLER_VIEW = 0x04
+};
+
+struct mail_index_registered_ext {
+ const char *name;
+ uint32_t index_idx; /* index ext_id */
+ uint32_t hdr_size; /* size of mail_index_ext_header.data[] */
+ uint16_t record_size;
+ uint16_t record_align;
+
+ mail_index_expunge_handler_t *expunge_handler;
+};
+
+struct mail_index_modseq_header {
+ /* highest used modseq */
+ uint64_t highest_modseq;
+ /* last tracked log file position */
+ uint32_t log_seq;
+ uint32_t log_offset;
+};
+
+struct mail_index_record_map {
+ ARRAY(struct mail_index_map *) maps;
+
+ void *mmap_base;
+ size_t mmap_size, mmap_used_size;
+
+ buffer_t *buffer;
+
+ void *records; /* struct mail_index_record[] */
+ unsigned int records_count;
+
+ struct mail_index_map_modseq *modseq;
+ uint32_t last_appended_uid;
+};
+
+#define MAIL_INDEX_MAP_HDR_OFFSET(map, hdr_offset) \
+ CONST_PTR_OFFSET((map)->hdr_copy_buf->data, hdr_offset)
+struct mail_index_map {
+ struct mail_index *index;
+ int refcount;
+
+ /* Copy of the base header for convenience. Note that base_header_size
+ may be smaller or larger than this struct. If it's smaller, the last
+ fields in the struct are filled with zeroes. */
+ struct mail_index_header hdr;
+ /* Copy of the full header. */
+ buffer_t *hdr_copy_buf;
+
+ pool_t extension_pool;
+ ARRAY(struct mail_index_ext) extensions;
+ ARRAY(uint32_t) ext_id_map; /* index -> file */
+
+ ARRAY(unsigned int) keyword_idx_map; /* file -> index */
+
+ struct mail_index_modseq_header modseq_hdr_snapshot;
+
+ struct mail_index_record_map *rec_map;
+};
+
+struct mail_index_module_register {
+ unsigned int id;
+};
+
+union mail_index_module_context {
+ struct mail_index_module_register *reg;
+};
+
+struct mail_index_settings {
+ /* Directory path for .cache file. Set via
+ mail_index_set_cache_dir(). */
+ char *cache_dir;
+
+ /* fsyncing behavior. Set via mail_index_set_fsync_mode(). */
+ enum fsync_mode fsync_mode;
+ enum mail_index_fsync_mask fsync_mask;
+
+ /* Index file permissions. Set via mail_index_set_permissions(). */
+ mode_t mode;
+ gid_t gid;
+ char *gid_origin;
+
+ /* Lock settings. Set via mail_index_set_lock_method(). */
+ enum file_lock_method lock_method;
+ unsigned int max_lock_timeout_secs;
+
+ /* Initial extension added to newly created indexes. Set via
+ mail_index_set_ext_init_data(). */
+ uint32_t ext_hdr_init_id;
+ void *ext_hdr_init_data;
+};
+
+struct mail_index_error {
+ /* Human-readable error text */
+ char *text;
+
+ /* Error happened because there's no disk space, i.e. syscall failed
+ with ENOSPC or EDQUOT. */
+ bool nodiskspace:1;
+};
+
+struct mail_index {
+ /* Directory path for the index, or NULL for in-memory indexes. */
+ char *dir;
+ /* Filename prefix for the index, e.g. "dovecot.index." */
+ char *prefix;
+ struct event *event;
+ enum mail_index_open_flags flags;
+ struct mail_index_settings set;
+ struct mail_index_optimization_settings optimization_set;
+
+ struct mail_cache *cache;
+ struct mail_transaction_log *log;
+
+ char *filepath;
+ int fd;
+ /* Linked list of currently opened views */
+ struct mail_index_view *views;
+ /* Latest map */
+ struct mail_index_map *map;
+
+ /* ID number that permanently identifies the index. This is stored in
+ the index files' headers. If the indexids suddenly changes, it means
+ that the index has been completely recreated and needs to be
+ reopened (e.g. the mailbox was deleted and recreated while it
+ was open). */
+ uint32_t indexid;
+ /* Views initially use this same ID value. This ID is incremented
+ whenever something unexpected happens to the index that prevents
+ syncing existing views. When the view's inconsistency_id doesn't
+ match this one, the view is marked as inconsistent. */
+ unsigned int inconsistency_id;
+ /* How many times this index has been opened with mail_index_open(). */
+ unsigned int open_count;
+
+ /* These contain the log_file_seq and log_file_tail_offset that exists
+ in dovecot.index file's header. These are used to figure out if it's
+ time to rewrite the dovecot.index file. Note that these aren't
+ available in index->map->hdr, because it gets updated when
+ transaction log file is read. */
+ uint32_t main_index_hdr_log_file_seq;
+ uint32_t main_index_hdr_log_file_tail_offset;
+
+ /* log file which last updated index_deleted */
+ uint32_t index_delete_changed_file_seq;
+
+ /* transaction log head seq/offset when we last fscked */
+ uint32_t fsck_log_head_file_seq;
+ uoff_t fsck_log_head_file_offset;
+
+ /* syncing will update this if non-NULL */
+ struct mail_index_transaction_commit_result *sync_commit_result;
+ /* Delayed log2_rotate_time update to mail_index_header. This is set
+ and unset within the same sync. */
+ uint32_t hdr_log2_rotate_time_delayed_update;
+
+ /* Registered extensions */
+ pool_t extension_pool;
+ ARRAY(struct mail_index_registered_ext) extensions;
+
+ /* All keywords that have ever been used in this index. Keywords are
+ only added here, never removed. */
+ pool_t keywords_pool;
+ ARRAY_TYPE(keywords) keywords;
+ HASH_TABLE(char *, void *) keywords_hash; /* name -> unsigned int idx */
+
+ /* Registered extension IDs */
+ uint32_t keywords_ext_id;
+ uint32_t modseq_ext_id;
+
+ /* Module-specific contexts. */
+ ARRAY(union mail_index_module_context *) module_contexts;
+
+ /* Last error returned by mail_index_get_error_message().
+ Cleared by mail_index_reset_error(). */
+ struct mail_index_error last_error;
+ /* Timestamp when mmap() failure was logged the last time. This is used
+ to prevent logging the same error too rapidly. This could happen
+ e.g. if mmap()ing a large cache file that exceeeds process's
+ VSZ limit. */
+ time_t last_mmap_error_time;
+ /* If non-NULL, dovecot.index should be recreated as soon as possible.
+ The reason for why the recreation is wanted is stored as human-
+ readable text. */
+ char *need_recreate;
+
+ /* Mapping has noticed non-external MAIL_TRANSACTION_INDEX_DELETED
+ record, i.e. a request to mark the index deleted. The next sync
+ will finish the deletion by writing external
+ MAIL_TRANSACTION_INDEX_DELETED record. */
+ bool index_delete_requested:1;
+ /* Mapping has noticed external MAIL_TRANSACTION_INDEX_DELETED record,
+ or index was unexpectedly deleted under us. No more changes are
+ allowed to the index, except undeletion. */
+ bool index_deleted:1;
+ /* .log is locked for syncing. This is the main exclusive lock for
+ indexes. */
+ bool log_sync_locked:1;
+ /* Main index or .log couldn't be opened read-write */
+ bool readonly:1;
+ /* mail_index_map() is running */
+ bool mapping:1;
+ /* mail_index_sync_*() is running */
+ bool syncing:1;
+ /* Mapping has read more from .log than it preferred. Use
+ mail_index_base_optimization_settings.rewrite_min_log_bytes the next
+ time when checking if index needs a rewrite. */
+ bool index_min_write:1;
+ /* mail_index_modseq_enable() has been called. Track per-flag
+ modseq numbers in memory (global modseqs are tracked anyway). */
+ bool modseqs_enabled:1;
+ /* mail_index_open() is creating new index files */
+ bool initial_create:1;
+ /* TRUE after mail_index_map() has succeeded */
+ bool initial_mapped:1;
+ /* The next mail_index_map() must reopen the main index, because the
+ currently opened one is too old. */
+ bool reopen_main_index:1;
+ /* Index has been fsck'd, but mail_index_reset_fscked() hasn't been
+ called yet. */
+ bool fscked:1;
+};
+
+extern struct mail_index_module_register mail_index_module_register;
+extern struct event_category event_category_mail_index;
+
+/* Add/replace expunge handler for specified extension. */
+void mail_index_register_expunge_handler(struct mail_index *index,
+ uint32_t ext_id,
+ mail_index_expunge_handler_t *callback);
+void mail_index_unregister_expunge_handler(struct mail_index *index,
+ uint32_t ext_id);
+
+int mail_index_create_tmp_file(struct mail_index *index,
+ const char *path_prefix, const char **path_r);
+
+int mail_index_try_open_only(struct mail_index *index);
+void mail_index_close_file(struct mail_index *index);
+/* Returns 1 if index was successfully (re-)opened, 0 if the index no longer
+ exists, -1 if I/O error. If 1 is returned, reopened_r=TRUE if a new index
+ was actually reopened (or if index wasn't even open before this call). */
+int mail_index_reopen_if_changed(struct mail_index *index, bool *reopened_r,
+ const char **reason_r);
+/* Update/rewrite the main index file from index->map */
+void mail_index_write(struct mail_index *index, bool want_rotate,
+ const char *reason);
+
+void mail_index_flush_read_cache(struct mail_index *index, const char *path,
+ int fd, bool locked);
+
+int mail_index_lock_fd(struct mail_index *index, const char *path, int fd,
+ int lock_type, unsigned int timeout_secs,
+ struct file_lock **lock_r);
+
+/* Allocate a new empty map. */
+struct mail_index_map *mail_index_map_alloc(struct mail_index *index);
+/* Replace index->map with the latest index changes. This may reopen the index
+ file and/or it may read the latest changes from transaction log. The log is
+ read up to EOF, but non-synced expunges are skipped.
+
+ If we mmap()ed the index file, the map is returned locked.
+
+ Returns 1 = ok, 0 = corrupted, -1 = error. */
+int mail_index_map(struct mail_index *index,
+ enum mail_index_sync_handler_type type);
+/* Unreference given mapping and unmap it if it's dropped to zero. */
+void mail_index_unmap(struct mail_index_map **map);
+/* Clone a map. It still points to the original rec_map. */
+struct mail_index_map *mail_index_map_clone(const struct mail_index_map *map);
+/* Make sure the map has its own private rec_map, cloning it if necessary. */
+void mail_index_record_map_move_to_private(struct mail_index_map *map);
+/* If map points to mmap()ed index, copy it to the memory. */
+void mail_index_map_move_to_memory(struct mail_index_map *map);
+
+void mail_index_fchown(struct mail_index *index, int fd, const char *path);
+
+bool mail_index_map_lookup_ext(struct mail_index_map *map, const char *name,
+ uint32_t *idx_r);
+bool mail_index_ext_name_is_valid(const char *name);
+uint32_t
+mail_index_map_register_ext(struct mail_index_map *map,
+ const char *name, uint32_t ext_offset,
+ const struct mail_index_ext_header *ext_hdr);
+bool mail_index_map_get_ext_idx(struct mail_index_map *map,
+ uint32_t ext_id, uint32_t *idx_r);
+const struct mail_index_ext *
+mail_index_view_get_ext(struct mail_index_view *view, uint32_t ext_id);
+
+void mail_index_map_lookup_seq_range(struct mail_index_map *map,
+ uint32_t first_uid, uint32_t last_uid,
+ uint32_t *first_seq_r,
+ uint32_t *last_seq_r);
+
+/* Returns 1 on success, 0 on non-critical errors we want to silently fix,
+ -1 if map isn't usable. The caller is responsible for logging the errors
+ if -1 is returned. */
+int mail_index_map_check_header(struct mail_index_map *map,
+ const char **error_r);
+/* Returns 1 if header is usable, 0 or -1 if not. The caller should log an
+ error if -1 is returned, but not if 0 is returned. */
+bool mail_index_check_header_compat(struct mail_index *index,
+ const struct mail_index_header *hdr,
+ uoff_t file_size, const char **error_r);
+int mail_index_map_parse_extensions(struct mail_index_map *map);
+int mail_index_map_parse_keywords(struct mail_index_map *map);
+
+void mail_index_map_init_extbufs(struct mail_index_map *map,
+ unsigned int initial_count);
+int mail_index_map_ext_get_next(struct mail_index_map *map,
+ unsigned int *offset,
+ const struct mail_index_ext_header **ext_hdr_r,
+ const char **name_r);
+int mail_index_map_ext_hdr_check(const struct mail_index_header *hdr,
+ const struct mail_index_ext_header *ext_hdr,
+ const char *name, const char **error_r);
+unsigned int mail_index_map_ext_hdr_offset(unsigned int name_len);
+
+void mail_index_fsck_locked(struct mail_index *index);
+
+/* Log an error and set it as the index's current error that is available
+ with mail_index_get_error_message(). */
+void mail_index_set_error(struct mail_index *index, const char *fmt, ...)
+ ATTR_FORMAT(2, 3) ATTR_COLD;
+/* Same as mail_index_set_error(), but don't log the error. */
+void mail_index_set_error_nolog(struct mail_index *index, const char *str)
+ ATTR_COLD;
+/* "%s failed with index file %s: %m" */
+void mail_index_set_syscall_error(struct mail_index *index,
+ const char *function) ATTR_COLD;
+/* "%s failed with file %s: %m" */
+void mail_index_file_set_syscall_error(struct mail_index *index,
+ const char *filepath,
+ const char *function) ATTR_COLD;
+
+#endif
diff --git a/src/lib-index/mail-index-strmap.c b/src/lib-index/mail-index-strmap.c
new file mode 100644
index 0000000..1287c73
--- /dev/null
+++ b/src/lib-index/mail-index-strmap.c
@@ -0,0 +1,1259 @@
+/* Copyright (c) 2008-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "bsearch-insert-pos.h"
+#include "istream.h"
+#include "ostream.h"
+#include "file-lock.h"
+#include "file-dotlock.h"
+#include "crc32.h"
+#include "safe-mkstemp.h"
+#include "str.h"
+#include "mail-index-private.h"
+#include "mail-index-strmap.h"
+
+#include <stdio.h>
+
+struct mail_index_strmap {
+ struct mail_index *index;
+ char *path;
+ int fd;
+ struct istream *input;
+
+ struct file_lock *file_lock;
+ struct dotlock *dotlock;
+ struct dotlock_settings dotlock_settings;
+};
+
+struct mail_index_strmap_view {
+ struct mail_index_strmap *strmap;
+ struct mail_index_view *view;
+
+ ARRAY_TYPE(mail_index_strmap_rec) recs;
+ ARRAY(uint32_t) recs_crc32;
+ struct hash2_table *hash;
+
+ mail_index_strmap_key_cmp_t *key_compare;
+ mail_index_strmap_rec_cmp_t *rec_compare;
+ mail_index_strmap_remap_t *remap_cb;
+ void *cb_context;
+
+ uoff_t last_read_block_offset;
+ uint32_t last_read_uid;
+ uint32_t last_added_uid;
+ uint32_t total_ref_count;
+
+ uint32_t last_ref_index;
+ uint32_t next_str_idx;
+ uint32_t lost_expunged_uid;
+
+ bool desynced:1;
+};
+
+struct mail_index_strmap_read_context {
+ struct mail_index_strmap_view *view;
+
+ struct istream *input;
+ uoff_t end_offset;
+ uint32_t highest_str_idx;
+ uint32_t uid_lookup_seq;
+ uint32_t lost_expunged_uid;
+
+ const unsigned char *data, *end, *str_idx_base;
+ struct mail_index_strmap_rec rec;
+ uint32_t next_ref_index;
+ unsigned int rec_size;
+
+ bool too_large_uids:1;
+};
+
+struct mail_index_strmap_view_sync {
+ struct mail_index_strmap_view *view;
+};
+
+struct mail_index_strmap_hash_key {
+ const char *str;
+ uint32_t crc32;
+};
+
+/* number of bytes required to store one string idx */
+#define STRMAP_FILE_STRIDX_SIZE (sizeof(uint32_t)*2)
+
+/* renumber the string indexes when highest string idx becomes larger than
+ <number of indexes>*STRMAP_FILE_MAX_STRIDX_MULTIPLIER */
+#define STRMAP_FILE_MAX_STRIDX_MULTIPLIER 2
+
+#define STRIDX_MUST_RENUMBER(highest_idx, n_unique_indexes) \
+ (highest_idx > n_unique_indexes * STRMAP_FILE_MAX_STRIDX_MULTIPLIER)
+
+#define MAIL_INDEX_STRMAP_TIMEOUT_SECS 10
+
+static const struct dotlock_settings default_dotlock_settings = {
+ .timeout = MAIL_INDEX_STRMAP_TIMEOUT_SECS,
+ .stale_timeout = 30
+};
+
+struct mail_index_strmap *
+mail_index_strmap_init(struct mail_index *index, const char *suffix)
+{
+ struct mail_index_strmap *strmap;
+
+ i_assert(index->open_count > 0);
+
+ strmap = i_new(struct mail_index_strmap, 1);
+ strmap->index = index;
+ strmap->path = i_strconcat(index->filepath, suffix, NULL);
+ strmap->fd = -1;
+
+ strmap->dotlock_settings = default_dotlock_settings;
+ strmap->dotlock_settings.use_excl_lock =
+ (index->flags & MAIL_INDEX_OPEN_FLAG_DOTLOCK_USE_EXCL) != 0;
+ strmap->dotlock_settings.nfs_flush =
+ (index->flags & MAIL_INDEX_OPEN_FLAG_NFS_FLUSH) != 0;
+ return strmap;
+}
+
+static bool
+mail_index_strmap_read_rec_next(struct mail_index_strmap_read_context *ctx,
+ uint32_t *crc32_r);
+
+static void
+mail_index_strmap_set_syscall_error(struct mail_index_strmap *strmap,
+ const char *function)
+{
+ i_assert(function != NULL);
+
+ if (ENOSPACE(errno)) {
+ strmap->index->last_error.nodiskspace = TRUE;
+ if ((strmap->index->flags &
+ MAIL_INDEX_OPEN_FLAG_NEVER_IN_MEMORY) == 0)
+ return;
+ }
+
+ mail_index_set_error(strmap->index,
+ "%s failed with strmap index file %s: %m",
+ function, strmap->path);
+}
+
+static void mail_index_strmap_close(struct mail_index_strmap *strmap)
+{
+ if (strmap->file_lock != NULL)
+ file_lock_free(&strmap->file_lock);
+ else if (strmap->dotlock != NULL)
+ file_dotlock_delete(&strmap->dotlock);
+
+ if (strmap->fd != -1) {
+ if (close(strmap->fd) < 0)
+ mail_index_strmap_set_syscall_error(strmap, "close()");
+ strmap->fd = -1;
+ }
+ i_stream_unref(&strmap->input);
+}
+
+void mail_index_strmap_deinit(struct mail_index_strmap **_strmap)
+{
+ struct mail_index_strmap *strmap = *_strmap;
+
+ *_strmap = NULL;
+ mail_index_strmap_close(strmap);
+ i_free(strmap->path);
+ i_free(strmap);
+}
+
+static unsigned int mail_index_strmap_hash_key(const void *_key)
+{
+ const struct mail_index_strmap_hash_key *key = _key;
+
+ return key->crc32;
+}
+
+static bool
+mail_index_strmap_hash_cmp(const void *_key, const void *_value, void *context)
+{
+ const struct mail_index_strmap_hash_key *key = _key;
+ const struct mail_index_strmap_rec *rec = _value;
+ struct mail_index_strmap_view *view = context;
+
+ return view->key_compare(key->str, rec, view->cb_context);
+}
+
+struct mail_index_strmap_view *
+mail_index_strmap_view_open(struct mail_index_strmap *strmap,
+ struct mail_index_view *idx_view,
+ mail_index_strmap_key_cmp_t *key_compare_cb,
+ mail_index_strmap_rec_cmp_t *rec_compare_cb,
+ mail_index_strmap_remap_t *remap_cb,
+ void *context,
+ const ARRAY_TYPE(mail_index_strmap_rec) **recs_r,
+ const struct hash2_table **hash_r)
+{
+ struct mail_index_strmap_view *view;
+
+ view = i_new(struct mail_index_strmap_view, 1);
+ view->strmap = strmap;
+ view->view = idx_view;
+ view->key_compare = key_compare_cb;
+ view->rec_compare = rec_compare_cb;
+ view->remap_cb = remap_cb;
+ view->cb_context = context;
+ view->next_str_idx = 1;
+
+ i_array_init(&view->recs, 64);
+ i_array_init(&view->recs_crc32, 64);
+ view->hash = hash2_create(0, sizeof(struct mail_index_strmap_rec),
+ mail_index_strmap_hash_key,
+ mail_index_strmap_hash_cmp, view);
+ *recs_r = &view->recs;
+ *hash_r = view->hash;
+ return view;
+}
+
+void mail_index_strmap_view_close(struct mail_index_strmap_view **_view)
+{
+ struct mail_index_strmap_view *view = *_view;
+
+ *_view = NULL;
+ array_free(&view->recs);
+ array_free(&view->recs_crc32);
+ hash2_destroy(&view->hash);
+ i_free(view);
+}
+
+uint32_t mail_index_strmap_view_get_highest_idx(struct mail_index_strmap_view *view)
+{
+ return view->next_str_idx-1;
+}
+
+static void mail_index_strmap_view_reset(struct mail_index_strmap_view *view)
+{
+ view->remap_cb(NULL, 0, 0, view->cb_context);
+ array_clear(&view->recs);
+ array_clear(&view->recs_crc32);
+ hash2_clear(view->hash);
+
+ view->last_added_uid = 0;
+ view->lost_expunged_uid = 0;
+ view->desynced = FALSE;
+}
+
+void mail_index_strmap_view_set_corrupted(struct mail_index_strmap_view *view)
+{
+ mail_index_set_error(view->strmap->index,
+ "Corrupted strmap index file: %s",
+ view->strmap->path);
+ i_unlink(view->strmap->path);
+ mail_index_strmap_close(view->strmap);
+ mail_index_strmap_view_reset(view);
+}
+
+static int mail_index_strmap_open(struct mail_index_strmap_view *view)
+{
+ struct mail_index_strmap *strmap = view->strmap;
+ const struct mail_index_header *idx_hdr;
+ struct mail_index_strmap_header hdr;
+ const unsigned char *data;
+ size_t size;
+ int ret;
+
+ i_assert(strmap->fd == -1);
+
+ strmap->fd = open(strmap->path, O_RDWR);
+ if (strmap->fd == -1) {
+ if (errno == ENOENT)
+ return 0;
+ mail_index_strmap_set_syscall_error(strmap, "open()");
+ return -1;
+ }
+ strmap->input = i_stream_create_fd(strmap->fd, SIZE_MAX);
+ ret = i_stream_read_bytes(strmap->input, &data, &size, sizeof(hdr));
+ if (ret <= 0) {
+ if (ret < 0) {
+ mail_index_strmap_set_syscall_error(strmap, "read()");
+ mail_index_strmap_close(strmap);
+ } else {
+ i_assert(ret == 0);
+ mail_index_strmap_view_set_corrupted(view);
+ }
+ return ret;
+ }
+ memcpy(&hdr, data, sizeof(hdr));
+
+ idx_hdr = mail_index_get_header(view->view);
+ if (hdr.version != MAIL_INDEX_STRMAP_VERSION ||
+ hdr.uid_validity != idx_hdr->uid_validity) {
+ /* need to rebuild. if we already had something in the strmap,
+ we can keep it. */
+ i_unlink(strmap->path);
+ mail_index_strmap_close(strmap);
+ return 0;
+ }
+
+ /* we'll read the entire file from the beginning */
+ view->last_added_uid = 0;
+ view->last_read_uid = 0;
+ view->total_ref_count = 0;
+ view->last_read_block_offset = sizeof(struct mail_index_strmap_header);
+ view->next_str_idx = 1;
+
+ mail_index_strmap_view_reset(view);
+ return 0;
+}
+
+static bool mail_index_strmap_need_reopen(struct mail_index_strmap *strmap)
+{
+ struct stat st1, st2;
+
+ /* FIXME: nfs flush */
+ if (fstat(strmap->fd, &st1) < 0) {
+ if (!ESTALE_FSTAT(errno))
+ mail_index_strmap_set_syscall_error(strmap, "fstat()");
+ return TRUE;
+ }
+ if (stat(strmap->path, &st2) < 0) {
+ mail_index_strmap_set_syscall_error(strmap, "stat()");
+ return TRUE;
+ }
+ return st1.st_ino != st2.st_ino || !CMP_DEV_T(st1.st_dev, st2.st_dev);
+}
+
+static int mail_index_strmap_refresh(struct mail_index_strmap_view *view)
+{
+ uint32_t seq;
+
+ if (MAIL_INDEX_IS_IN_MEMORY(view->strmap->index))
+ return -1;
+
+ if (view->strmap->fd != -1) {
+ if (!mail_index_strmap_need_reopen(view->strmap)) {
+ if (view->lost_expunged_uid != 0) {
+ /* last read failed because view had a message
+ that didn't exist in the strmap (because it
+ was expunged by another session). if the
+ message still isn't expunged in this view,
+ just continue using the current strmap. */
+ if (mail_index_lookup_seq(view->view,
+ view->lost_expunged_uid, &seq))
+ return -1;
+ } else if (view->desynced) {
+ /* our view isn't synced with the disk, we
+ can't read strmap without first resetting
+ the view */
+ } else {
+ i_stream_sync(view->strmap->input);
+ return 0;
+ }
+ }
+ mail_index_strmap_close(view->strmap);
+ }
+
+ return mail_index_strmap_open(view);
+}
+
+static int
+mail_index_strmap_read_packed(struct mail_index_strmap_read_context *ctx,
+ uint32_t *num_r)
+{
+ const unsigned char *data;
+ const uint8_t *bytes, *p, *end;
+ size_t size;
+ int ret;
+
+ ret = i_stream_read_bytes(ctx->input, &data, &size, sizeof(*num_r));
+ if (ret <= 0)
+ return ret;
+
+ if (ctx->input->v_offset + size > ctx->end_offset)
+ size = ctx->end_offset - ctx->input->v_offset;
+ bytes = p = (const uint8_t *)data;
+ end = bytes + size;
+
+ if (mail_index_unpack_num(&p, end, num_r) < 0)
+ return -1;
+ i_stream_skip(ctx->input, p - bytes);
+ return 1;
+}
+
+static int
+mail_index_strmap_uid_exists(struct mail_index_strmap_read_context *ctx,
+ uint32_t uid)
+{
+ const struct mail_index_record *rec;
+
+ i_assert(ctx->uid_lookup_seq > 0);
+
+ if (ctx->uid_lookup_seq > ctx->view->view->map->hdr.messages_count) {
+ if (uid >= ctx->view->view->map->hdr.next_uid) {
+ /* thread index has larger UIDs than what we've seen
+ in our view. we'll have to read them again later
+ when we know about them */
+ ctx->too_large_uids = TRUE;
+ }
+ return 0;
+ }
+
+ rec = MAIL_INDEX_REC_AT_SEQ(ctx->view->view->map, ctx->uid_lookup_seq);
+ if (rec->uid == uid) {
+ ctx->uid_lookup_seq++;
+ return 1;
+ } else if (rec->uid > uid) {
+ return 0;
+ } else {
+ /* record that exists in index is missing from strmap.
+ see if it's because the strmap is corrupted or because
+ our current view is a bit stale and the message has already
+ been expunged. */
+ mail_index_refresh(ctx->view->view->index);
+ if (mail_index_is_expunged(ctx->view->view,
+ ctx->uid_lookup_seq))
+ ctx->lost_expunged_uid = rec->uid;
+ return -1;
+ }
+}
+
+static int
+mail_index_strmap_read_rec_first(struct mail_index_strmap_read_context *ctx,
+ uint32_t *crc32_r)
+{
+ size_t size;
+ uint32_t n, i, count, str_idx;
+ int ret;
+
+ /* <uid> <n> <crc32>*count <str_idx>*count
+ where
+ n = 0 -> count=1 (only Message-ID:)
+ n = 1 -> count=2 (Message-ID: + In-Reply-To:)
+ n = 2+ -> count=n (Message-ID: + References:)
+ */
+ if (mail_index_strmap_read_packed(ctx, &n) <= 0)
+ return -1;
+ count = n < 2 ? n + 1 : n;
+ ctx->view->total_ref_count += count;
+
+ ctx->rec_size = count * (sizeof(ctx->rec.str_idx) + sizeof(*crc32_r));
+ ret = mail_index_strmap_uid_exists(ctx, ctx->rec.uid);
+ if (ret < 0)
+ return -1;
+ if (i_stream_read_bytes(ctx->view->strmap->input, &ctx->data, &size, ctx->rec_size) <= 0)
+ return -1;
+ ctx->str_idx_base = ctx->data + count * sizeof(uint32_t);
+
+ if (ret == 0) {
+ /* this message has already been expunged, ignore it.
+ update highest string indexes anyway. */
+ for (i = 0; i < count; i++) {
+ memcpy(&str_idx, ctx->str_idx_base, sizeof(str_idx));
+ if (ctx->highest_str_idx < str_idx)
+ ctx->highest_str_idx = str_idx;
+ ctx->str_idx_base += sizeof(str_idx);
+ }
+ i_stream_skip(ctx->view->strmap->input, ctx->rec_size);
+ return 0;
+ }
+
+ /* everything exists. save it. FIXME: these ref_index values
+ are thread index specific, perhaps something more generic
+ should be used some day */
+ ctx->end = ctx->data + count * sizeof(*crc32_r);
+
+ ctx->next_ref_index = 0;
+ if (!mail_index_strmap_read_rec_next(ctx, crc32_r))
+ i_unreached();
+ ctx->next_ref_index = n == 1 ? 1 : 2;
+ return 1;
+}
+
+static bool
+mail_index_strmap_read_rec_next(struct mail_index_strmap_read_context *ctx,
+ uint32_t *crc32_r)
+{
+ if (ctx->data == ctx->end) {
+ i_stream_skip(ctx->view->strmap->input, ctx->rec_size);
+ return FALSE;
+ }
+
+ /* FIXME: str_idx could be stored as packed relative values
+ (first relative to highest_idx, the rest relative to the
+ previous str_idx) */
+
+ /* read the record contents */
+ memcpy(&ctx->rec.str_idx, ctx->str_idx_base, sizeof(ctx->rec.str_idx));
+ memcpy(crc32_r, ctx->data, sizeof(*crc32_r));
+
+ ctx->rec.ref_index = ctx->next_ref_index++;
+
+ if (ctx->highest_str_idx < ctx->rec.str_idx)
+ ctx->highest_str_idx = ctx->rec.str_idx;
+
+ /* get to the next record */
+ ctx->data += sizeof(*crc32_r);
+ ctx->str_idx_base += sizeof(ctx->rec.str_idx);
+ return TRUE;
+}
+
+static int
+strmap_read_block_init(struct mail_index_strmap_view *view,
+ struct mail_index_strmap_read_context *ctx)
+{
+ struct mail_index_strmap *strmap = view->strmap;
+ const unsigned char *data;
+ size_t size;
+ uint32_t block_size, seq1, seq2;
+ int ret;
+
+ if (view->last_read_uid + 1 >= view->view->map->hdr.next_uid) {
+ /* come back later when we know about the new UIDs */
+ return 0;
+ }
+
+ i_zero(ctx);
+ ret = i_stream_read_bytes(strmap->input, &data, &size,
+ sizeof(block_size));
+ if (ret <= 0) {
+ if (strmap->input->stream_errno == 0) {
+ /* no new data */
+ return 0;
+ }
+ mail_index_strmap_set_syscall_error(strmap, "read()");
+ return -1;
+ }
+ memcpy(&block_size, data, sizeof(block_size));
+ block_size = mail_index_offset_to_uint32(block_size) >> 2;
+ if (block_size == 0) {
+ /* the rest of the file is either not written, or the previous
+ write didn't finish */
+ return 0;
+ }
+ i_stream_skip(strmap->input, sizeof(block_size));
+
+ ctx->view = view;
+ ctx->input = strmap->input;
+ ctx->end_offset = strmap->input->v_offset + block_size;
+ if (ctx->end_offset < strmap->input->v_offset) {
+ /* block size too large */
+ mail_index_strmap_view_set_corrupted(view);
+ return -1;
+ }
+ ctx->rec.uid = view->last_read_uid + 1;
+
+ /* FIXME: when reading multiple blocks we shouldn't have to calculate
+ this every time */
+ if (!mail_index_lookup_seq_range(view->view, ctx->rec.uid, (uint32_t)-1,
+ &seq1, &seq2))
+ seq1 = mail_index_view_get_messages_count(view->view) + 1;
+ ctx->uid_lookup_seq = seq1;
+ return 1;
+}
+
+static int
+strmap_read_block_next(struct mail_index_strmap_read_context *ctx,
+ uint32_t *crc32_r)
+{
+ uint32_t uid_diff;
+ int ret;
+
+ if (mail_index_strmap_read_rec_next(ctx, crc32_r))
+ return 1;
+
+ /* get next UID */
+ do {
+ if (ctx->input->v_offset == ctx->end_offset) {
+ /* this block is done */
+ return 0;
+ }
+ if (mail_index_strmap_read_packed(ctx, &uid_diff) <= 0)
+ return -1;
+
+ ctx->rec.uid += uid_diff;
+ ret = mail_index_strmap_read_rec_first(ctx, crc32_r);
+ } while (ret == 0);
+ return ret;
+}
+
+static int
+strmap_read_block_deinit(struct mail_index_strmap_read_context *ctx, int ret,
+ bool update_block_offset)
+{
+ struct mail_index_strmap_view *view = ctx->view;
+ struct mail_index_strmap *strmap = view->strmap;
+
+ if (ctx->highest_str_idx > view->total_ref_count) {
+ /* if all string indexes are unique, highest_str_index equals
+ total_ref_count. otherwise it's always lower. */
+ mail_index_set_error(strmap->index,
+ "Corrupted strmap index file %s: "
+ "String indexes too high "
+ "(highest=%u max=%u)",
+ strmap->path, ctx->highest_str_idx,
+ view->total_ref_count);
+ mail_index_strmap_view_set_corrupted(view);
+ return -1;
+ }
+ if (ctx->lost_expunged_uid != 0) {
+ /* our view contained a message that had since been expunged. */
+ i_assert(ret < 0);
+ view->lost_expunged_uid = ctx->lost_expunged_uid;
+ } else if (ret < 0) {
+ if (strmap->input->stream_errno != 0)
+ mail_index_strmap_set_syscall_error(strmap, "read()");
+ else
+ mail_index_strmap_view_set_corrupted(view);
+ return -1;
+ } else if (update_block_offset && !ctx->too_large_uids) {
+ view->last_read_block_offset = strmap->input->v_offset;
+ view->last_read_uid = ctx->rec.uid;
+ }
+ if (view->next_str_idx <= ctx->highest_str_idx)
+ view->next_str_idx = ctx->highest_str_idx + 1;
+ return ret;
+}
+
+static bool
+strmap_view_sync_handle_conflict(struct mail_index_strmap_read_context *ctx,
+ const struct mail_index_strmap_rec *hash_rec,
+ struct hash2_iter *iter)
+{
+ uint32_t seq;
+
+ /* hopefully it's a message that has since been expunged */
+ if (!mail_index_lookup_seq(ctx->view->view, hash_rec->uid, &seq)) {
+ /* message is no longer in our view. remove it completely. */
+ hash2_remove_iter(ctx->view->hash, iter);
+ return TRUE;
+ }
+ if (mail_index_is_expunged(ctx->view->view, seq)) {
+ /* it's quite likely a conflict. we may not be able to verify
+ it, so just assume it is. nothing breaks even if we guess
+ wrong, the performance just suffers a bit. */
+ return FALSE;
+ }
+
+ /* 0 means "doesn't match", which is the only acceptable case */
+ return ctx->view->rec_compare(&ctx->rec, hash_rec,
+ ctx->view->cb_context) == 0;
+}
+
+static int
+strmap_view_sync_block_check_conflicts(struct mail_index_strmap_read_context *ctx,
+ uint32_t crc32)
+{
+ struct mail_index_strmap_rec *hash_rec;
+ struct hash2_iter iter;
+
+ if (crc32 == 0) {
+ /* unique string - there are no conflicts */
+ return 0;
+ }
+
+ /* check for conflicting string indexes. they may happen if
+
+ 1) msgid exists only for a message X that has been expunged
+ 2) another process doesn't see X, but sees msgid for another
+ message and writes it using a new string index
+ 3) if we still see X, we now see the same msgid with two
+ string indexes.
+
+ if we detect such a conflict, we can't continue using the
+ strmap index until X has been expunged. */
+ i_zero(&iter);
+ while ((hash_rec = hash2_iterate(ctx->view->hash,
+ crc32, &iter)) != NULL &&
+ hash_rec->str_idx != ctx->rec.str_idx) {
+ /* CRC32 matches, but string index doesn't */
+ if (!strmap_view_sync_handle_conflict(ctx, hash_rec, &iter)) {
+ ctx->lost_expunged_uid = hash_rec->uid;
+ return -1;
+ }
+ }
+ return 0;
+}
+
+static int
+mail_index_strmap_view_sync_block(struct mail_index_strmap_read_context *ctx)
+{
+ struct mail_index_strmap_rec *hash_rec;
+ uint32_t crc32, prev_uid = 0;
+ int ret;
+
+ while ((ret = strmap_read_block_next(ctx, &crc32)) > 0) {
+ if (ctx->rec.uid <= ctx->view->last_added_uid) {
+ if (ctx->rec.uid < ctx->view->last_added_uid ||
+ prev_uid != ctx->rec.uid) {
+ /* we've already added this */
+ continue;
+ }
+ }
+ prev_uid = ctx->rec.uid;
+
+ if (strmap_view_sync_block_check_conflicts(ctx, crc32) < 0) {
+ ret = -1;
+ break;
+ }
+ ctx->view->last_added_uid = ctx->rec.uid;
+
+ /* add the record to records array */
+ array_push_back(&ctx->view->recs, &ctx->rec);
+ array_push_back(&ctx->view->recs_crc32, &crc32);
+
+ /* add a separate copy of the record to hash */
+ hash_rec = hash2_insert_hash(ctx->view->hash, crc32);
+ memcpy(hash_rec, &ctx->rec, sizeof(*hash_rec));
+ }
+ return strmap_read_block_deinit(ctx, ret, TRUE);
+}
+
+struct mail_index_strmap_view_sync *
+mail_index_strmap_view_sync_init(struct mail_index_strmap_view *view,
+ uint32_t *last_uid_r)
+{
+ struct mail_index_strmap_view_sync *sync;
+ struct mail_index_strmap_read_context ctx;
+ int ret;
+
+ sync = i_new(struct mail_index_strmap_view_sync, 1);
+ sync->view = view;
+
+ if (mail_index_strmap_refresh(view) < 0) {
+ /* reading the strmap failed - just ignore and do
+ this in-memory based on whatever we knew last */
+ } else if (view->strmap->input != NULL) {
+ i_stream_seek(view->strmap->input,
+ view->last_read_block_offset);
+ while ((ret = strmap_read_block_init(view, &ctx)) > 0) {
+ if (mail_index_strmap_view_sync_block(&ctx) < 0) {
+ ret = -1;
+ break;
+ }
+ if (ctx.too_large_uids)
+ break;
+ }
+
+ if (ret < 0) {
+ /* something failed - we can still use the strmap as far
+ as we managed to read it, but our view is now out
+ of sync */
+ view->desynced = TRUE;
+ } else {
+ i_assert(view->lost_expunged_uid == 0);
+ }
+ }
+ *last_uid_r = view->last_added_uid;
+ return sync;
+}
+
+static inline uint32_t crc32_str_nonzero(const char *str)
+{
+ /* we'll flip the bits because of a bug in our old crc32 code.
+ this keeps the index format backwards compatible with the new fixed
+ crc32 code. */
+ uint32_t value = crc32_str(str) ^ 0xffffffffU;
+ return value == 0 ? 1 : value;
+}
+
+void mail_index_strmap_view_sync_add(struct mail_index_strmap_view_sync *sync,
+ uint32_t uid, uint32_t ref_index,
+ const char *key)
+{
+ struct mail_index_strmap_view *view = sync->view;
+ struct mail_index_strmap_rec *rec, *old_rec;
+ struct mail_index_strmap_hash_key hash_key;
+ uint32_t str_idx;
+
+ i_assert(uid > view->last_added_uid ||
+ (uid == view->last_added_uid &&
+ ref_index > view->last_ref_index));
+
+ hash_key.str = key;
+ hash_key.crc32 = crc32_str_nonzero(key);
+
+ old_rec = hash2_lookup(view->hash, &hash_key);
+ if (old_rec != NULL) {
+ /* The string already exists, use the same unique idx */
+ str_idx = old_rec->str_idx;
+ } else {
+ /* Newly seen string, assign a new unique idx to it */
+ str_idx = view->next_str_idx++;
+ }
+ i_assert(str_idx != 0);
+
+ rec = hash2_insert(view->hash, &hash_key);
+ rec->uid = uid;
+ rec->ref_index = ref_index;
+ rec->str_idx = str_idx;
+ array_push_back(&view->recs, rec);
+ array_push_back(&view->recs_crc32, &hash_key.crc32);
+
+ view->last_added_uid = uid;
+ view->last_ref_index = ref_index;
+}
+
+void mail_index_strmap_view_sync_add_unique(struct mail_index_strmap_view_sync *sync,
+ uint32_t uid, uint32_t ref_index)
+{
+ struct mail_index_strmap_view *view = sync->view;
+ struct mail_index_strmap_rec rec;
+
+ i_assert(uid > view->last_added_uid ||
+ (uid == view->last_added_uid &&
+ ref_index > view->last_ref_index));
+
+ i_zero(&rec);
+ rec.uid = uid;
+ rec.ref_index = ref_index;
+ rec.str_idx = view->next_str_idx++;
+ array_push_back(&view->recs, &rec);
+ array_append_zero(&view->recs_crc32);
+
+ view->last_added_uid = uid;
+ view->last_ref_index = ref_index;
+}
+
+static void
+mail_index_strmap_zero_terminate(struct mail_index_strmap_view *view)
+{
+ /* zero-terminate the records array */
+ array_append_zero(&view->recs);
+ array_pop_back(&view->recs);
+}
+
+static void mail_index_strmap_view_renumber(struct mail_index_strmap_view *view)
+{
+ struct mail_index_strmap_read_context ctx;
+ struct mail_index_strmap_rec *recs, *hash_rec;
+ uint32_t prev_uid, str_idx, *recs_crc32, *renumber_map;
+ unsigned int i, dest, count, count2;
+ int ret;
+
+ i_zero(&ctx);
+ ctx.view = view;
+ ctx.uid_lookup_seq = 1;
+
+ /* create a map of old -> new index and remove records of
+ expunged messages */
+ renumber_map = i_new(uint32_t, view->next_str_idx);
+ str_idx = 0; prev_uid = 0;
+ recs = array_get_modifiable(&view->recs, &count);
+ recs_crc32 = array_get_modifiable(&view->recs_crc32, &count2);
+ i_assert(count == count2);
+
+ for (i = dest = 0; i < count; ) {
+ if (prev_uid != recs[i].uid) {
+ /* see if this record should be removed */
+ prev_uid = recs[i].uid;
+ ret = mail_index_strmap_uid_exists(&ctx, prev_uid);
+ i_assert(ret >= 0);
+ if (ret == 0) {
+ /* message expunged */
+ do {
+ i++;
+ } while (i < count && recs[i].uid == prev_uid);
+ continue;
+ }
+ }
+
+ i_assert(recs[i].str_idx < view->next_str_idx);
+ if (renumber_map[recs[i].str_idx] == 0)
+ renumber_map[recs[i].str_idx] = ++str_idx;
+ if (i != dest) {
+ recs[dest] = recs[i];
+ recs_crc32[dest] = recs_crc32[i];
+ }
+ i++; dest++;
+ }
+ i_assert(renumber_map[0] == 0);
+ array_delete(&view->recs, dest, i-dest);
+ array_delete(&view->recs_crc32, dest, i-dest);
+ mail_index_strmap_zero_terminate(view);
+
+ /* notify caller of the renumbering */
+ i_assert(str_idx <= view->next_str_idx);
+ view->remap_cb(renumber_map, view->next_str_idx, str_idx + 1,
+ view->cb_context);
+
+ /* renumber the indexes in-place and recreate the hash */
+ recs = array_get_modifiable(&view->recs, &count);
+ hash2_clear(view->hash);
+ for (i = 0; i < count; i++) {
+ recs[i].str_idx = renumber_map[recs[i].str_idx];
+ hash_rec = hash2_insert_hash(view->hash, recs_crc32[i]);
+ memcpy(hash_rec, &recs[i], sizeof(*hash_rec));
+ }
+
+ /* update the new next_str_idx only after remapping */
+ view->next_str_idx = str_idx + 1;
+ i_free(renumber_map);
+}
+
+static void mail_index_strmap_write_block(struct mail_index_strmap_view *view,
+ struct ostream *output,
+ unsigned int i, uint32_t base_uid)
+{
+ const struct mail_index_strmap_rec *recs;
+ const uint32_t *crc32;
+ unsigned int j, n, count, count2, uid_rec_count;
+ uint32_t block_size;
+ uint8_t *p, packed[MAIL_INDEX_PACK_MAX_SIZE*2];
+ uoff_t block_offset, end_offset;
+
+ /* skip over the block size for now, we don't know it yet */
+ block_offset = output->offset;
+ block_size = 0;
+ o_stream_nsend(output, &block_size, sizeof(block_size));
+
+ /* write records */
+ recs = array_get(&view->recs, &count);
+ crc32 = array_get(&view->recs_crc32, &count2);
+ i_assert(count == count2);
+ while (i < count) {
+ /* @UNSAFE: <uid diff> */
+ p = packed;
+ mail_index_pack_num(&p, recs[i].uid - base_uid);
+ base_uid = recs[i].uid;
+
+ /* find how many records belong to this UID */
+ uid_rec_count = 1;
+ for (j = i + 1; j < count; j++) {
+ if (recs[j].uid != base_uid)
+ break;
+ uid_rec_count++;
+ }
+ view->total_ref_count += uid_rec_count;
+
+ /* <n> <crc32>*count <str_idx>*count -
+ FIXME: thread index specific code */
+ i_assert(recs[i].ref_index == 0);
+ if (uid_rec_count == 1) {
+ /* Only Message-ID: header */
+ n = 0;
+ } else if (recs[i+1].ref_index == 1) {
+ /* In-Reply-To: header */
+ n = 1;
+ i_assert(uid_rec_count == 2);
+ } else {
+ /* References: header */
+ n = uid_rec_count;
+ i_assert(recs[i+1].ref_index == 2);
+ }
+
+ mail_index_pack_num(&p, n);
+ o_stream_nsend(output, packed, p-packed);
+ for (j = 0; j < uid_rec_count; j++)
+ o_stream_nsend(output, &crc32[i+j], sizeof(crc32[i+j]));
+ for (j = 0; j < uid_rec_count; j++) {
+ i_assert(j < 2 || recs[i+j].ref_index == j+1);
+ o_stream_nsend(output, &recs[i+j].str_idx,
+ sizeof(recs[i+j].str_idx));
+ }
+ i += uid_rec_count;
+ }
+
+ /* we know the block size now - write it */
+ block_size = output->offset - (block_offset + sizeof(block_size));
+ block_size = mail_index_uint32_to_offset(block_size << 2);
+ i_assert(block_size != 0);
+
+ end_offset = output->offset;
+ (void)o_stream_seek(output, block_offset);
+ o_stream_nsend(output, &block_size, sizeof(block_size));
+ (void)o_stream_seek(output, end_offset);
+
+ if (output->stream_errno != 0)
+ return;
+
+ i_assert(view->last_added_uid == recs[count-1].uid);
+ view->last_read_uid = recs[count-1].uid;
+ view->last_read_block_offset = output->offset;
+}
+
+static void
+mail_index_strmap_recreate_write(struct mail_index_strmap_view *view,
+ struct ostream *output)
+{
+ const struct mail_index_header *idx_hdr;
+ struct mail_index_strmap_header hdr;
+
+ idx_hdr = mail_index_get_header(view->view);
+
+ /* write header */
+ i_zero(&hdr);
+ hdr.version = MAIL_INDEX_STRMAP_VERSION;
+ hdr.uid_validity = idx_hdr->uid_validity;
+ o_stream_nsend(output, &hdr, sizeof(hdr));
+
+ view->total_ref_count = 0;
+ mail_index_strmap_write_block(view, output, 0, 1);
+}
+
+static int mail_index_strmap_recreate(struct mail_index_strmap_view *view)
+{
+ struct mail_index_strmap *strmap = view->strmap;
+ string_t *str;
+ struct ostream *output;
+ const char *temp_path;
+ int fd, ret = 0;
+
+ if (array_count(&view->recs) == 0) {
+ /* everything expunged - just unlink the existing index */
+ if (unlink(strmap->path) < 0 && errno != ENOENT)
+ mail_index_strmap_set_syscall_error(strmap, "unlink()");
+ return 0;
+ }
+
+ str = t_str_new(256);
+ str_append(str, strmap->path);
+ fd = safe_mkstemp_hostpid_group(str, view->view->index->set.mode,
+ view->view->index->set.gid,
+ view->view->index->set.gid_origin);
+ temp_path = str_c(str);
+
+ if (fd == -1) {
+ mail_index_set_error(strmap->index,
+ "safe_mkstemp_hostpid(%s) failed: %m",
+ temp_path);
+ return -1;
+ }
+ output = o_stream_create_fd(fd, 0);
+ o_stream_cork(output);
+ mail_index_strmap_recreate_write(view, output);
+ if (o_stream_finish(output) < 0) {
+ mail_index_set_error(strmap->index, "write(%s) failed: %s",
+ temp_path, o_stream_get_error(output));
+ ret = -1;
+ }
+ o_stream_destroy(&output);
+ if (close(fd) < 0) {
+ mail_index_set_error(strmap->index,
+ "close(%s) failed: %m", temp_path);
+ ret = -1;
+ } else if (ret == 0 && rename(temp_path, strmap->path) < 0) {
+ mail_index_set_error(strmap->index,
+ "rename(%s, %s) failed: %m",
+ temp_path, strmap->path);
+ ret = -1;
+ }
+ if (ret < 0)
+ i_unlink(temp_path);
+ return ret;
+}
+
+static int mail_index_strmap_lock(struct mail_index_strmap *strmap)
+{
+ unsigned int timeout_secs;
+ const char *error;
+ int ret;
+
+ i_assert(strmap->fd != -1);
+
+ if (strmap->index->set.lock_method != FILE_LOCK_METHOD_DOTLOCK) {
+ i_assert(strmap->file_lock == NULL);
+
+ struct file_lock_settings lock_set = {
+ .lock_method = strmap->index->set.lock_method,
+ };
+ timeout_secs = I_MIN(MAIL_INDEX_STRMAP_TIMEOUT_SECS,
+ strmap->index->set.max_lock_timeout_secs);
+ ret = file_wait_lock(strmap->fd, strmap->path, F_WRLCK,
+ &lock_set, timeout_secs,
+ &strmap->file_lock, &error);
+ if (ret <= 0) {
+ mail_index_set_error(strmap->index,
+ "file_wait_lock() failed with strmap index file %s: %s",
+ strmap->path, error);
+ }
+ } else {
+ i_assert(strmap->dotlock == NULL);
+
+ ret = file_dotlock_create(&strmap->dotlock_settings,
+ strmap->path, 0, &strmap->dotlock);
+ if (ret <= 0) {
+ mail_index_strmap_set_syscall_error(strmap,
+ "file_dotlock_create()");
+ }
+ }
+ return ret;
+}
+
+static void mail_index_strmap_unlock(struct mail_index_strmap *strmap)
+{
+ if (strmap->file_lock != NULL)
+ file_unlock(&strmap->file_lock);
+ else if (strmap->dotlock != NULL)
+ file_dotlock_delete(&strmap->dotlock);
+}
+
+static int
+strmap_rec_cmp(const uint32_t *uid, const struct mail_index_strmap_rec *rec)
+{
+ return *uid < rec->uid ? -1 :
+ (*uid > rec->uid ? 1 : 0);
+}
+
+static int
+mail_index_strmap_write_append(struct mail_index_strmap_view *view)
+{
+ struct mail_index_strmap_read_context ctx;
+ const struct mail_index_strmap_rec *old_recs;
+ unsigned int i, old_count;
+ struct ostream *output;
+ uint32_t crc32, next_uid;
+ bool full_block;
+ int ret;
+
+ /* Check first if another process had written new records to the file.
+ If there are any, hopefully they're the same as what we would be
+ writing. There are two problematic cases when messages have been
+ expunged recently:
+
+ 1) The file contains UIDs that we don't have. This means the string
+ indexes won't be compatible anymore, so we'll have to renumber ours
+ to match the ones in the strmap file.
+
+ Currently we don't bother handling 1) case. If indexes don't match
+ what we have, we just don't write anything.
+
+ 2) We have UIDs that don't exist in the file. We can't simply skip
+ those records, because other records may have pointers to them using
+ different string indexes than we have. Even if we renumbered those,
+ future appends by other processes might cause the same problem (they
+ see the string for the first time and assign it a new index, but we
+ already have internally given it another index). So the only
+ sensible choice is to write nothing and hope that the message goes
+ away soon. */
+ next_uid = view->last_read_uid + 1;
+ (void)array_bsearch_insert_pos(&view->recs, &next_uid,
+ strmap_rec_cmp, &i);
+
+ old_recs = array_get(&view->recs, &old_count);
+ if (i < old_count) {
+ while (i > 0 && old_recs[i-1].uid == old_recs[i].uid)
+ i--;
+ }
+
+ i_stream_sync(view->strmap->input);
+ i_stream_seek(view->strmap->input, view->last_read_block_offset);
+ full_block = TRUE; ret = 0;
+ while (i < old_count &&
+ (ret = strmap_read_block_init(view, &ctx)) > 0) {
+ while ((ret = strmap_read_block_next(&ctx, &crc32)) > 0) {
+ if (ctx.rec.uid != old_recs[i].uid ||
+ ctx.rec.str_idx != old_recs[i].str_idx) {
+ /* mismatch */
+ if (ctx.rec.uid > old_recs[i].uid) {
+ /* 1) case */
+ ctx.lost_expunged_uid = ctx.rec.uid;
+ } else if (ctx.rec.uid < old_recs[i].uid) {
+ /* 2) case */
+ ctx.lost_expunged_uid = old_recs[i].uid;
+ } else {
+ /* string index mismatch,
+ shouldn't happen */
+ }
+ ret = -1;
+ break;
+ }
+ if (++i == old_count) {
+ full_block = FALSE;
+ break;
+ }
+ }
+ if (strmap_read_block_deinit(&ctx, ret, full_block) < 0) {
+ ret = -1;
+ break;
+ }
+ }
+ if (ret < 0)
+ return -1;
+ if (i == old_count) {
+ /* nothing new to write */
+ return 0;
+ }
+ i_assert(full_block);
+ i_assert(old_recs[i].uid > view->last_read_uid);
+
+ /* write the new records */
+ output = o_stream_create_fd(view->strmap->fd, 0);
+ (void)o_stream_seek(output, view->last_read_block_offset);
+ o_stream_cork(output);
+ mail_index_strmap_write_block(view, output, i,
+ view->last_read_uid + 1);
+ if (o_stream_finish(output) < 0) {
+ mail_index_strmap_set_syscall_error(view->strmap, "write()");
+ ret = -1;
+ }
+ o_stream_destroy(&output);
+ return ret;
+}
+
+static int mail_index_strmap_write(struct mail_index_strmap_view *view)
+{
+ int ret;
+
+ /* FIXME: this renumbering doesn't work well when running for a long
+ time since records aren't removed from hash often enough */
+ if (STRIDX_MUST_RENUMBER(view->next_str_idx - 1,
+ hash2_count(view->hash))) {
+ mail_index_strmap_view_renumber(view);
+ if (!MAIL_INDEX_IS_IN_MEMORY(view->strmap->index)) {
+ if (mail_index_strmap_recreate(view) < 0) {
+ view->desynced = TRUE;
+ return -1;
+ }
+ }
+ return 0;
+ }
+
+ if (MAIL_INDEX_IS_IN_MEMORY(view->strmap->index) || view->desynced)
+ return 0;
+
+ if (view->strmap->fd == -1) {
+ /* initial file creation */
+ if (mail_index_strmap_recreate(view) < 0) {
+ view->desynced = TRUE;
+ return -1;
+ }
+ return 0;
+ }
+
+ /* append the new records to the strmap file */
+ if (mail_index_strmap_lock(view->strmap) <= 0) {
+ /* timeout / error */
+ ret = -1;
+ } else if (mail_index_strmap_need_reopen(view->strmap)) {
+ /* the file was already recreated - leave the syncing as it is
+ for now and let the next sync re-read the file. */
+ ret = 0;
+ } else {
+ ret = mail_index_strmap_write_append(view);
+ }
+ mail_index_strmap_unlock(view->strmap);
+ if (ret < 0)
+ view->desynced = TRUE;
+ return ret;
+}
+
+void mail_index_strmap_view_sync_commit(struct mail_index_strmap_view_sync **_sync)
+{
+ struct mail_index_strmap_view_sync *sync = *_sync;
+ struct mail_index_strmap_view *view = sync->view;
+
+ *_sync = NULL;
+ i_free(sync);
+
+ (void)mail_index_strmap_write(view);
+ mail_index_strmap_zero_terminate(view);
+
+ /* zero-terminate the records array */
+ array_append_zero(&view->recs);
+ array_pop_back(&view->recs);
+}
+
+void mail_index_strmap_view_sync_rollback(struct mail_index_strmap_view_sync **_sync)
+{
+ struct mail_index_strmap_view_sync *sync = *_sync;
+
+ *_sync = NULL;
+
+ mail_index_strmap_view_reset(sync->view);
+ mail_index_strmap_zero_terminate(sync->view);
+ i_free(sync);
+}
diff --git a/src/lib-index/mail-index-strmap.h b/src/lib-index/mail-index-strmap.h
new file mode 100644
index 0000000..c61afa6
--- /dev/null
+++ b/src/lib-index/mail-index-strmap.h
@@ -0,0 +1,81 @@
+#ifndef MAIL_INDEX_STRMAP_H
+#define MAIL_INDEX_STRMAP_H
+
+#include "hash2.h"
+
+struct mail_index;
+struct mail_index_view;
+
+struct mail_index_strmap_header {
+#define MAIL_INDEX_STRMAP_VERSION 1
+ uint8_t version;
+ uint8_t unused[3];
+
+ uint32_t uid_validity;
+};
+
+struct mail_index_strmap_rec {
+ uint32_t uid;
+ uint32_t ref_index;
+ /* unique index number for the string */
+ uint32_t str_idx;
+};
+ARRAY_DEFINE_TYPE(mail_index_strmap_rec, struct mail_index_strmap_rec);
+
+typedef bool
+mail_index_strmap_key_cmp_t(const char *key,
+ const struct mail_index_strmap_rec *rec,
+ void *context);
+/* Returns 1 if matches, 0 if not, -1 if one of the records is expunged and
+ the result can't be determined */
+typedef int
+mail_index_strmap_rec_cmp_t(const struct mail_index_strmap_rec *rec1,
+ const struct mail_index_strmap_rec *rec2,
+ void *context);
+/* called when string indexes are renumbered. idx_map[old_idx] = new_idx.
+ if new_idx is 0, the record was expunged. As a special case if count=0,
+ the strmap was reset. */
+typedef void mail_index_strmap_remap_t(const uint32_t *idx_map,
+ unsigned int old_count,
+ unsigned int new_count, void *context);
+
+struct mail_index_strmap *
+mail_index_strmap_init(struct mail_index *index, const char *suffix);
+void mail_index_strmap_deinit(struct mail_index_strmap **strmap);
+
+/* Returns strmap records and hash that can be used for read-only access.
+ The records array always terminates with a record containing zeros (but it's
+ not counted in the array count). */
+struct mail_index_strmap_view *
+mail_index_strmap_view_open(struct mail_index_strmap *strmap,
+ struct mail_index_view *idx_view,
+ mail_index_strmap_key_cmp_t *key_compare_cb,
+ mail_index_strmap_rec_cmp_t *rec_compare_cb,
+ mail_index_strmap_remap_t *remap_cb,
+ void *context,
+ const ARRAY_TYPE(mail_index_strmap_rec) **recs_r,
+ const struct hash2_table **hash_r);
+void mail_index_strmap_view_close(struct mail_index_strmap_view **view);
+void mail_index_strmap_view_set_corrupted(struct mail_index_strmap_view *view)
+ ATTR_COLD;
+
+/* Return the highest used string index. */
+uint32_t mail_index_strmap_view_get_highest_idx(struct mail_index_strmap_view *view);
+
+/* Synchronize strmap: Caller adds missing entries, expunged messages may be
+ removed internally and the changes are written to disk. Note that the strmap
+ recs/hash shouldn't be used until _sync_commit() is called, because the
+ string indexes may be renumbered if another process had already written the
+ same changes as us. */
+struct mail_index_strmap_view_sync *
+mail_index_strmap_view_sync_init(struct mail_index_strmap_view *view,
+ uint32_t *last_uid_r);
+void mail_index_strmap_view_sync_add(struct mail_index_strmap_view_sync *sync,
+ uint32_t uid, uint32_t ref_index,
+ const char *key);
+void mail_index_strmap_view_sync_add_unique(struct mail_index_strmap_view_sync *sync,
+ uint32_t uid, uint32_t ref_index);
+void mail_index_strmap_view_sync_commit(struct mail_index_strmap_view_sync **sync);
+void mail_index_strmap_view_sync_rollback(struct mail_index_strmap_view_sync **sync);
+
+#endif
diff --git a/src/lib-index/mail-index-sync-ext.c b/src/lib-index/mail-index-sync-ext.c
new file mode 100644
index 0000000..5d453f3
--- /dev/null
+++ b/src/lib-index/mail-index-sync-ext.c
@@ -0,0 +1,735 @@
+/* Copyright (c) 2004-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "buffer.h"
+#include "mail-index-view-private.h"
+#include "mail-index-sync-private.h"
+#include "mail-index-modseq.h"
+#include "mail-transaction-log.h"
+
+
+void mail_index_sync_init_expunge_handlers(struct mail_index_sync_map_ctx *ctx)
+{
+ const struct mail_index_ext *ext;
+ const struct mail_index_registered_ext *rext;
+ const uint32_t *id_map;
+ void **contexts;
+ struct mail_index_expunge_handler eh;
+ unsigned int ext_count, id_map_count;
+ unsigned int rext_count, context_count, count;
+ uint32_t idx_ext_id, map_ext_id;
+
+ if (!array_is_created(&ctx->view->map->extensions))
+ return;
+
+ i_zero(&eh);
+ if (array_is_created(&ctx->expunge_handlers))
+ array_clear(&ctx->expunge_handlers);
+ else
+ i_array_init(&ctx->expunge_handlers, 64);
+
+ rext = array_get(&ctx->view->index->extensions, &rext_count);
+ ext = array_get(&ctx->view->map->extensions, &ext_count);
+ id_map = array_get(&ctx->view->map->ext_id_map, &id_map_count);
+ contexts = array_get_modifiable(&ctx->extra_contexts, &context_count);
+
+ i_assert(context_count >= rext_count);
+ count = I_MIN(rext_count, id_map_count);
+ for (idx_ext_id = 0; idx_ext_id < count; idx_ext_id++) {
+ if (rext[idx_ext_id].expunge_handler == NULL)
+ continue;
+ map_ext_id = id_map[idx_ext_id];
+ if (map_ext_id == (uint32_t)-1)
+ continue;
+
+ eh.handler = rext[idx_ext_id].expunge_handler;
+ eh.sync_context = &contexts[idx_ext_id];
+ eh.record_offset = ext[map_ext_id].record_offset;
+ array_push_back(&ctx->expunge_handlers, &eh);
+ }
+ ctx->expunge_handlers_set = TRUE;
+ ctx->expunge_handlers_used = TRUE;
+}
+
+void
+mail_index_sync_deinit_expunge_handlers(struct mail_index_sync_map_ctx *ctx)
+{
+ const struct mail_index_expunge_handler *eh;
+
+ if (!array_is_created(&ctx->expunge_handlers))
+ return;
+
+ array_foreach(&ctx->expunge_handlers, eh) {
+ if (eh->sync_context != NULL)
+ eh->handler(ctx, NULL, eh->sync_context);
+ }
+ array_free(&ctx->expunge_handlers);
+}
+
+void mail_index_sync_init_handlers(struct mail_index_sync_map_ctx *ctx)
+{
+ unsigned int count;
+
+ if (!array_is_created(&ctx->view->map->extensions))
+ return;
+
+ /* set space for extra contexts */
+ count = array_count(&ctx->view->index->extensions);
+ i_assert(count > 0);
+
+ if (!array_is_created(&ctx->extra_contexts))
+ i_array_init(&ctx->extra_contexts, count);
+
+ /* make sure the extra_contexts contains everything */
+ (void)array_idx_get_space(&ctx->extra_contexts, count - 1);
+ /* we need to update the expunge handler list in case they had
+ already been called */
+ ctx->expunge_handlers_set = FALSE;
+}
+
+void mail_index_sync_deinit_handlers(struct mail_index_sync_map_ctx *ctx)
+{
+ if (array_is_created(&ctx->extra_contexts))
+ array_free(&ctx->extra_contexts);
+}
+
+static struct mail_index_ext_header *
+get_ext_header(struct mail_index_map *map, const struct mail_index_ext *ext)
+{
+ struct mail_index_ext_header *ext_hdr;
+ void *hdr_base;
+
+ /* do some kludgy jumping to get to it. */
+ hdr_base = buffer_get_modifiable_data(map->hdr_copy_buf, NULL);
+ ext_hdr = PTR_OFFSET(hdr_base, ext->ext_offset);
+ i_assert(memcmp((char *)(ext_hdr + 1),
+ ext->name, strlen(ext->name)) == 0);
+ return ext_hdr;
+}
+
+static int mail_index_ext_align_cmp(const void *p1, const void *p2)
+{
+ const struct mail_index_ext *const *e1 = p1, *const *e2 = p2;
+
+ return (int)(*e2)->record_align - (int)(*e1)->record_align;
+}
+
+static void sync_ext_reorder(struct mail_index_map *map, uint32_t ext_map_idx,
+ uint16_t old_ext_size)
+{
+ struct mail_index_ext *ext, **sorted;
+ struct mail_index_ext_header *ext_hdr;
+ uint16_t *old_offsets, *copy_sizes, min_align, max_align;
+ uint32_t offset, new_record_size, rec_idx;
+ unsigned int i, count;
+ const void *src;
+ buffer_t *new_buffer;
+ size_t new_buffer_size;
+
+ i_assert(MAIL_INDEX_MAP_IS_IN_MEMORY(map) && map->refcount == 1);
+
+ ext = array_get_modifiable(&map->extensions, &count);
+ i_assert(ext_map_idx < count);
+
+ /* @UNSAFE */
+ old_offsets = t_new(uint16_t, count);
+ copy_sizes = t_new(uint16_t, count);
+ sorted = t_new(struct mail_index_ext *, count);
+ for (i = 0; i < count; i++) {
+ old_offsets[i] = ext[i].record_offset;
+ copy_sizes[i] = ext[i].record_size;
+ ext[i].record_offset = 0;
+ sorted[i] = &ext[i];
+ }
+ qsort(sorted, count, sizeof(struct mail_index_ext *),
+ mail_index_ext_align_cmp);
+
+ if (copy_sizes[ext_map_idx] > old_ext_size) {
+ /* we are growing the extension record. remember this
+ so we don't write extra data while copying the record */
+ copy_sizes[ext_map_idx] = old_ext_size;
+ }
+
+ /* we simply try to use the extensions with largest alignment
+ requirement first. FIXME: if the extension sizes don't match
+ alignment, this may not give the minimal layout. */
+ offset = MAIL_INDEX_RECORD_MIN_SIZE;
+ max_align = sizeof(uint32_t);
+ for (;;) {
+ min_align = (uint16_t)-1;
+ for (i = 0; i < count; i++) {
+ if (sorted[i]->record_align > max_align)
+ max_align = sorted[i]->record_align;
+
+ if (sorted[i]->record_offset == 0 &&
+ sorted[i]->record_size > 0) {
+ if ((offset % sorted[i]->record_align) == 0)
+ break;
+ if (sorted[i]->record_align < min_align)
+ min_align = sorted[i]->record_align;
+ }
+ }
+ if (i == count) {
+ if (min_align == (uint16_t)-1) {
+ /* all done */
+ break;
+ }
+ /* we have to leave space here */
+ i_assert(min_align > 1 && min_align < (uint16_t)-1);
+ offset += min_align - (offset % min_align);
+ } else {
+ sorted[i]->record_offset = offset;
+ offset += sorted[i]->record_size;
+ }
+
+ i_assert(offset < (uint16_t)-1);
+ }
+
+ if ((offset % max_align) != 0) {
+ /* keep record size divisible with maximum alignment */
+ offset += max_align - (offset % max_align);
+ }
+ new_record_size = offset;
+ i_assert(new_record_size >= sizeof(struct mail_index_record));
+
+ /* copy the records to new buffer */
+ new_buffer_size = map->rec_map->records_count * new_record_size;
+ new_buffer = buffer_create_dynamic(default_pool, new_buffer_size);
+ src = map->rec_map->records;
+ offset = 0;
+ for (rec_idx = 0; rec_idx < map->rec_map->records_count; rec_idx++) {
+ /* write the base record */
+ buffer_write(new_buffer, offset, src,
+ sizeof(struct mail_index_record));
+
+ /* write extensions */
+ for (i = 0; i < count; i++) {
+ buffer_write(new_buffer, offset + ext[i].record_offset,
+ CONST_PTR_OFFSET(src, old_offsets[i]),
+ copy_sizes[i]);
+ }
+ src = CONST_PTR_OFFSET(src, map->hdr.record_size);
+ offset += new_record_size;
+ }
+
+ if (new_buffer->used != new_buffer_size) {
+ /* we didn't fully write the last record */
+ size_t space = new_buffer_size - new_buffer->used;
+ i_assert(space < new_record_size);
+ buffer_append_zero(new_buffer, space);
+ }
+
+ buffer_free(&map->rec_map->buffer);
+ map->rec_map->buffer = new_buffer;
+ map->rec_map->records =
+ buffer_get_modifiable_data(map->rec_map->buffer, NULL);
+ map->hdr.record_size = new_record_size;
+
+ /* update record offsets in headers */
+ for (i = 0; i < count; i++) {
+ ext_hdr = get_ext_header(map, &ext[i]);
+ ext_hdr->record_offset = ext[i].record_offset;
+ }
+}
+
+static void
+sync_ext_resize(const struct mail_transaction_ext_intro *u,
+ uint32_t ext_map_idx, struct mail_index_sync_map_ctx *ctx,
+ bool no_shrink)
+{
+ struct mail_index_map *map;
+ struct mail_index_ext *ext;
+ struct mail_index_ext_header *ext_hdr;
+ uint32_t old_padded_hdr_size, new_padded_hdr_size, old_record_size;
+ bool reorder = FALSE;
+
+ ext = array_idx_modifiable(&ctx->view->map->extensions, ext_map_idx);
+ old_padded_hdr_size = MAIL_INDEX_HEADER_SIZE_ALIGN(ext->hdr_size);
+ new_padded_hdr_size = MAIL_INDEX_HEADER_SIZE_ALIGN(u->hdr_size);
+
+ if (ext->record_align != u->record_align ||
+ ext->record_size != u->record_size) {
+ /* record changed */
+ } else if (new_padded_hdr_size < old_padded_hdr_size) {
+ /* header is shrunk. do we allow? */
+ if (no_shrink)
+ return;
+ } else if (ext->hdr_size == u->hdr_size) {
+ /* no changes */
+ return;
+ }
+ /* something changed. get ourself a new map before we start changing
+ anything in it. */
+ map = mail_index_sync_get_atomic_map(ctx);
+ /* ext was duplicated to the new map. */
+ ext = array_idx_modifiable(&map->extensions, ext_map_idx);
+
+ if (new_padded_hdr_size < old_padded_hdr_size) {
+ /* header shrank */
+ if (no_shrink)
+ new_padded_hdr_size = old_padded_hdr_size;
+ else {
+ buffer_delete(map->hdr_copy_buf,
+ ext->hdr_offset + new_padded_hdr_size,
+ old_padded_hdr_size - new_padded_hdr_size);
+ ext->hdr_size = u->hdr_size;
+ }
+ } else if (new_padded_hdr_size > old_padded_hdr_size) {
+ /* header grown */
+ buffer_insert_zero(map->hdr_copy_buf,
+ ext->hdr_offset + old_padded_hdr_size,
+ new_padded_hdr_size - old_padded_hdr_size);
+ ext->hdr_size = u->hdr_size;
+ } else {
+ if (ext->hdr_size != u->hdr_size) {
+ /* aligned sizes were the same, but the actual sizes
+ had changed */
+ ext->hdr_size = u->hdr_size;
+ }
+ }
+
+ if (ext->record_align < u->record_align ||
+ (ext->record_align > u->record_align && !no_shrink)) {
+ ext->record_align = u->record_align;
+ reorder = TRUE;
+ }
+
+ old_record_size = ext->record_size;
+ if (ext->record_size < u->record_size ||
+ (ext->record_size > u->record_size && !no_shrink)) {
+ ext->record_size = u->record_size;
+ reorder = TRUE;
+ }
+
+ i_assert((map->hdr_copy_buf->used % sizeof(uint64_t)) == 0);
+ map->hdr.header_size = map->hdr_copy_buf->used;
+
+ ext_hdr = get_ext_header(map, ext);
+ ext_hdr->reset_id = ext->reset_id;
+ ext_hdr->hdr_size = ext->hdr_size;
+ ext_hdr->record_offset = ext->record_offset;
+ ext_hdr->record_size = ext->record_size;
+ ext_hdr->record_align = ext->record_align;
+
+ if (new_padded_hdr_size != old_padded_hdr_size) {
+ /* move all hdr_offset of all extensions after this one */
+ unsigned int i, count = array_count(&map->extensions);
+ ssize_t diff = (ssize_t)new_padded_hdr_size -
+ (ssize_t)old_padded_hdr_size;
+
+ ext = array_front_modifiable(&map->extensions);
+ for (i = ext_map_idx + 1; i < count; i++) {
+ ext[i].ext_offset += diff;
+ ext[i].hdr_offset += diff;
+ }
+ }
+
+ if (reorder)
+ sync_ext_reorder(map, ext_map_idx, old_record_size);
+}
+
+static bool
+mail_index_sync_ext_unknown_complain(struct mail_index_sync_map_ctx *ctx,
+ uint32_t ext_map_idx)
+{
+ unsigned char *p;
+
+ if (ext_map_idx >= 1024) {
+ /* don't try to track too high values */
+ return TRUE;
+ }
+
+ if (ctx->unknown_extensions == NULL) {
+ ctx->unknown_extensions =
+ buffer_create_dynamic(default_pool, ext_map_idx + 8);
+ }
+ p = buffer_get_space_unsafe(ctx->unknown_extensions, ext_map_idx, 1);
+ if (*p != 0) {
+ /* we've already complained once */
+ return FALSE;
+ }
+ *p = 1;
+ return TRUE;
+}
+
+static void
+mail_index_sync_ext_init_new(struct mail_index_sync_map_ctx *ctx,
+ const char *name,
+ const struct mail_index_ext_header *ext_hdr,
+ uint32_t *ext_map_idx_r)
+{
+ struct mail_index_map *map;
+ const struct mail_index_ext *ext;
+ buffer_t *hdr_buf;
+ uint32_t ext_map_idx;
+
+ i_assert(mail_index_ext_name_is_valid(name));
+
+ /* be sure to get a unique mapping before we modify the extensions,
+ otherwise other map users will see the new extension but not the
+ data records that sync_ext_reorder() adds. */
+ map = mail_index_sync_get_atomic_map(ctx);
+
+ hdr_buf = map->hdr_copy_buf;
+ i_assert(hdr_buf->used == map->hdr.header_size);
+
+ if (MAIL_INDEX_HEADER_SIZE_ALIGN(hdr_buf->used) != hdr_buf->used) {
+ /* we need to add padding between base header and extensions */
+ buffer_append_zero(hdr_buf,
+ MAIL_INDEX_HEADER_SIZE_ALIGN(hdr_buf->used) -
+ hdr_buf->used);
+ }
+
+ /* register record offset initially using zero,
+ sync_ext_reorder() will fix it. */
+ ext_map_idx = mail_index_map_register_ext(map, name, hdr_buf->used,
+ ext_hdr);
+ ext = array_idx(&map->extensions, ext_map_idx);
+
+ /* <ext_hdr> <name> [padding] [header data] */
+ i_assert(ext_hdr->name_size == strlen(name));
+ buffer_append(hdr_buf, ext_hdr, sizeof(*ext_hdr));
+ buffer_append(hdr_buf, name, ext_hdr->name_size);
+ /* header must begin and end in correct alignment */
+ buffer_append_zero(hdr_buf,
+ MAIL_INDEX_HEADER_SIZE_ALIGN(hdr_buf->used) - hdr_buf->used +
+ MAIL_INDEX_HEADER_SIZE_ALIGN(ext->hdr_size));
+ i_assert(hdr_buf->used ==
+ ext->hdr_offset + MAIL_INDEX_HEADER_SIZE_ALIGN(ext->hdr_size));
+ i_assert((hdr_buf->used % sizeof(uint64_t)) == 0);
+
+ map->hdr.header_size = hdr_buf->used;
+
+ mail_index_sync_init_handlers(ctx);
+ sync_ext_reorder(map, ext_map_idx, 0);
+ i_assert(ext->record_offset != 0 || ext->record_size == 0);
+
+ *ext_map_idx_r = ext_map_idx;
+}
+
+int mail_index_sync_ext_intro(struct mail_index_sync_map_ctx *ctx,
+ const struct mail_transaction_ext_intro *u)
+{
+ struct mail_index_map *map = ctx->view->map;
+ struct mail_index_ext_header ext_hdr;
+ const struct mail_index_ext *ext;
+ const char *name, *error;
+ uint32_t ext_map_idx;
+ bool no_shrink;
+
+ /* default to ignoring the following extension updates in case this
+ intro is corrupted */
+ ctx->cur_ext_map_idx = (uint32_t)-2;
+ ctx->cur_ext_ignore = TRUE;
+ ctx->cur_ext_record_size = 0;
+
+ if (u->ext_id != (uint32_t)-1 &&
+ (!array_is_created(&map->extensions) ||
+ u->ext_id >= array_count(&map->extensions))) {
+ /* The extension ID is unknown in this map. */
+ if (map->hdr.log_file_seq == 0) {
+ /* This map was generated by
+ view_sync_get_log_lost_changes(). There's no need to
+ update any extensions, because they won't be used
+ anyway. Any extension lookups will be accessed via
+ the latest index map. */
+ i_assert(map->rec_map != ctx->view->index->map->rec_map);
+ return 1;
+ }
+ if (!mail_index_sync_ext_unknown_complain(ctx, u->ext_id))
+ return -1;
+ mail_index_sync_set_corrupted(ctx,
+ "Extension introduction for unknown id %u", u->ext_id);
+ return -1;
+ }
+
+ if (u->ext_id == (uint32_t)-1 && u->name_size == 0) {
+ mail_index_sync_set_corrupted(ctx,
+ "Extension introduction without id or name");
+ return -1;
+ }
+
+ if (u->ext_id != (uint32_t)-1) {
+ name = NULL;
+ ext_map_idx = u->ext_id;
+ } else {
+ name = t_strndup(u + 1, u->name_size);
+ if (!mail_index_map_lookup_ext(map, name, &ext_map_idx))
+ ext_map_idx = (uint32_t)-1;
+ }
+ if (ext_map_idx == (uint32_t)-1)
+ ext = NULL;
+ else {
+ ext = array_idx(&map->extensions, ext_map_idx);
+ name = ext->name;
+ }
+ i_assert(name != NULL);
+
+ if (!ctx->internal_update &&
+ strcmp(name, MAIL_INDEX_EXT_KEYWORDS) == 0) {
+ /* Keyword extension is handled internally by the keyword
+ code. Any attempt to modify them directly could cause
+ assert-crashes later, so prevent them immediately. */
+ mail_index_sync_set_corrupted(ctx,
+ "Extension introduction for keywords");
+ return -1;
+ }
+
+ i_zero(&ext_hdr);
+ ext_hdr.name_size = strlen(name);
+ ext_hdr.reset_id = u->reset_id;
+ ext_hdr.hdr_size = u->hdr_size;
+ ext_hdr.record_size = u->record_size;
+ ext_hdr.record_align = u->record_align;
+ no_shrink = (u->flags & MAIL_TRANSACTION_EXT_INTRO_FLAG_NO_SHRINK) != 0;
+
+ /* make sure the header looks valid before doing anything with it */
+ if (mail_index_map_ext_hdr_check(&map->hdr, &ext_hdr,
+ name, &error) < 0) {
+ mail_index_sync_set_corrupted(ctx,
+ "Broken extension introduction: %s", error);
+ return -1;
+ }
+
+ ctx->cur_ext_record_size = u->record_size;
+ if (ext != NULL) {
+ /* exists already */
+ if (u->reset_id == ext->reset_id) {
+ /* check if we need to resize anything */
+ sync_ext_resize(u, ext_map_idx, ctx, no_shrink);
+ ctx->cur_ext_ignore = FALSE;
+ } else {
+ /* extension was reset and this transaction hadn't
+ yet seen it. ignore this update (except for
+ resets). */
+ ctx->cur_ext_ignore = TRUE;
+ }
+
+ ctx->cur_ext_map_idx = ext_map_idx;
+ return 1;
+ }
+
+ mail_index_sync_ext_init_new(ctx, name, &ext_hdr, &ext_map_idx);
+
+ ctx->cur_ext_ignore = FALSE;
+ ctx->cur_ext_map_idx = ctx->internal_update ?
+ (uint32_t)-1 : ext_map_idx;
+ return 1;
+}
+
+static void mail_index_sync_ext_clear(struct mail_index_view *view,
+ struct mail_index_map *map,
+ struct mail_index_ext *ext)
+{
+ struct mail_index_record *rec;
+ uint32_t seq;
+
+ memset(buffer_get_space_unsafe(map->hdr_copy_buf, ext->hdr_offset,
+ ext->hdr_size), 0, ext->hdr_size);
+ i_assert(map->hdr_copy_buf->used == map->hdr.header_size);
+
+ for (seq = 1; seq <= view->map->rec_map->records_count; seq++) {
+ rec = MAIL_INDEX_REC_AT_SEQ(view->map, seq);
+ memset(PTR_OFFSET(rec, ext->record_offset), 0,
+ ext->record_size);
+ }
+}
+
+int mail_index_sync_ext_reset(struct mail_index_sync_map_ctx *ctx,
+ const struct mail_transaction_ext_reset *u)
+{
+ struct mail_index_map *map;
+ struct mail_index_ext_header *ext_hdr;
+ struct mail_index_ext *ext;
+
+ if (ctx->cur_ext_map_idx == (uint32_t)-1) {
+ mail_index_sync_set_corrupted(ctx,
+ "Extension reset without intro prefix");
+ return -1;
+ }
+ if (ctx->cur_ext_map_idx == (uint32_t)-2 && ctx->cur_ext_ignore) {
+ /* previous extension intro was broken */
+ return -1;
+ }
+ /* since we're resetting the extension, don't check cur_ext_ignore */
+
+ /* a new index file will be created, so the old data won't be
+ accidentally used by other processes. */
+ map = mail_index_sync_get_atomic_map(ctx);
+
+ ext = array_idx_modifiable(&map->extensions, ctx->cur_ext_map_idx);
+ ext->reset_id = u->new_reset_id;
+
+ if (u->preserve_data == 0)
+ mail_index_sync_ext_clear(ctx->view, map, ext);
+
+ ext_hdr = get_ext_header(map, ext);
+ ext_hdr->reset_id = u->new_reset_id;
+ return 1;
+}
+
+int mail_index_sync_ext_hdr_update(struct mail_index_sync_map_ctx *ctx,
+ uint32_t offset, uint32_t size,
+ const void *data)
+{
+ struct mail_index_map *map = ctx->view->map;
+ const struct mail_index_ext *ext;
+
+ if (ctx->cur_ext_map_idx == (uint32_t)-1) {
+ mail_index_sync_set_corrupted(ctx,
+ "Extension header update without intro prefix");
+ return -1;
+ }
+ if (ctx->cur_ext_ignore)
+ return 1;
+
+ ext = array_idx(&map->extensions, ctx->cur_ext_map_idx);
+ if (offset + size > ext->hdr_size) {
+ mail_index_sync_set_corrupted(ctx,
+ "Extension header update points outside header size");
+ return -1;
+ }
+
+ buffer_write(map->hdr_copy_buf, ext->hdr_offset + offset, data, size);
+ i_assert(map->hdr_copy_buf->used == map->hdr.header_size);
+
+ if (ext->index_idx == ctx->view->index->modseq_ext_id)
+ mail_index_modseq_hdr_update(ctx->modseq_ctx);
+ return 1;
+}
+
+int
+mail_index_sync_ext_rec_update(struct mail_index_sync_map_ctx *ctx,
+ const struct mail_transaction_ext_rec_update *u)
+{
+ struct mail_index_view *view = ctx->view;
+ struct mail_index_record *rec;
+ const struct mail_index_ext *ext;
+ void *old_data;
+ uint32_t seq;
+
+ i_assert(ctx->cur_ext_map_idx != (uint32_t)-1);
+ i_assert(!ctx->cur_ext_ignore);
+
+ if (u->uid == 0 || u->uid >= view->map->hdr.next_uid) {
+ mail_index_sync_set_corrupted(ctx,
+ "Extension record update for invalid uid=%u", u->uid);
+ return -1;
+ }
+
+ if (!mail_index_lookup_seq(view, u->uid, &seq))
+ return 1;
+
+ ext = array_idx(&view->map->extensions, ctx->cur_ext_map_idx);
+ i_assert(ext->record_offset + ctx->cur_ext_record_size <=
+ view->map->hdr.record_size);
+
+ rec = MAIL_INDEX_REC_AT_SEQ(view->map, seq);
+ old_data = PTR_OFFSET(rec, ext->record_offset);
+
+ /* @UNSAFE */
+ memcpy(old_data, u + 1, ctx->cur_ext_record_size);
+ if (ctx->cur_ext_record_size < ext->record_size) {
+ memset(PTR_OFFSET(old_data, ctx->cur_ext_record_size), 0,
+ ext->record_size - ctx->cur_ext_record_size);
+ }
+ return 1;
+}
+
+int
+mail_index_sync_ext_atomic_inc(struct mail_index_sync_map_ctx *ctx,
+ const struct mail_transaction_ext_atomic_inc *u)
+{
+ struct mail_index_view *view = ctx->view;
+ struct mail_index_record *rec;
+ const struct mail_index_ext *ext;
+ void *data;
+ uint32_t seq;
+ uint64_t min_value, max_value, orig_num;
+
+ i_assert(ctx->cur_ext_map_idx != (uint32_t)-1);
+ i_assert(!ctx->cur_ext_ignore);
+
+ if (u->uid == 0 || u->uid >= view->map->hdr.next_uid) {
+ mail_index_sync_set_corrupted(ctx,
+ "Extension record inc for invalid uid=%u", u->uid);
+ return -1;
+ }
+
+ if (!mail_index_lookup_seq(view, u->uid, &seq))
+ return 1;
+
+ ext = array_idx(&view->map->extensions, ctx->cur_ext_map_idx);
+ i_assert(ext->record_offset + ctx->cur_ext_record_size <=
+ view->map->hdr.record_size);
+
+ rec = MAIL_INDEX_REC_AT_SEQ(view->map, seq);
+ data = PTR_OFFSET(rec, ext->record_offset);
+
+ min_value = u->diff >= 0 ? 0 : (uint64_t)(-(int64_t)u->diff);
+
+ max_value = ctx->cur_ext_record_size == 8 ? (uint64_t)-1 :
+ ((uint64_t)1 << (ctx->cur_ext_record_size*8)) - 1;
+ if (u->diff <= 0) {
+ /* skip */
+ } else if (max_value >= (uint32_t)u->diff) {
+ max_value -= u->diff;
+ } else {
+ mail_index_sync_set_corrupted(ctx,
+ "Extension record inc diff=%d larger than max value=%u "
+ "(uid=%u)", u->diff, (unsigned int)max_value, u->uid);
+ return -1;
+ }
+
+ switch (ctx->cur_ext_record_size) {
+ case 1: {
+ uint8_t *num = data;
+
+ orig_num = *num;
+ if (orig_num >= min_value && orig_num <= max_value)
+ *num += u->diff;
+ break;
+ }
+ case 2: {
+ uint16_t *num = data;
+ orig_num = *num;
+ if (orig_num >= min_value && orig_num <= max_value)
+ *num += u->diff;
+ break;
+ }
+ case 4: {
+ uint32_t *num = data;
+ orig_num = *num;
+ if (orig_num >= min_value && orig_num <= max_value)
+ *num += u->diff;
+ break;
+ }
+ case 8: {
+ uint64_t *num = data;
+ orig_num = *num;
+ if (orig_num >= min_value && orig_num <= max_value)
+ *num += u->diff;
+ break;
+ }
+ default:
+ mail_index_sync_set_corrupted(ctx,
+ "Extension record inc with invalid size=%u",
+ ctx->cur_ext_record_size);
+ return -1;
+ }
+ if (orig_num < min_value) {
+ mail_index_sync_set_corrupted(ctx,
+ "Extension record inc drops number below zero "
+ "(uid=%u, diff=%d, orig=%"PRIu64")",
+ u->uid, u->diff, orig_num);
+ return -1;
+ } else if (orig_num > max_value) {
+ mail_index_sync_set_corrupted(ctx,
+ "Extension record inc overflows number "
+ "(uid=%u, diff=%d, orig=%"PRIu64")",
+ u->uid, u->diff, orig_num);
+ return -1;
+ }
+ return 1;
+}
diff --git a/src/lib-index/mail-index-sync-keywords.c b/src/lib-index/mail-index-sync-keywords.c
new file mode 100644
index 0000000..2c45156
--- /dev/null
+++ b/src/lib-index/mail-index-sync-keywords.c
@@ -0,0 +1,347 @@
+/* Copyright (c) 2004-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "buffer.h"
+#include "mail-index-modseq.h"
+#include "mail-index-view-private.h"
+#include "mail-index-sync-private.h"
+#include "mail-transaction-log.h"
+
+static bool
+keyword_lookup(struct mail_index_sync_map_ctx *ctx,
+ const char *keyword_name, unsigned int *idx_r)
+{
+ struct mail_index_map *map = ctx->view->map;
+ const unsigned int *idx_map;
+ unsigned int i, count, keyword_idx;
+
+ if (array_is_created(&map->keyword_idx_map) &&
+ mail_index_keyword_lookup(ctx->view->index, keyword_name,
+ &keyword_idx)) {
+ /* FIXME: slow. maybe create index -> file mapping as well */
+ idx_map = array_get(&map->keyword_idx_map, &count);
+ for (i = 0; i < count; i++) {
+ if (idx_map[i] == keyword_idx) {
+ *idx_r = i;
+ return TRUE;
+ }
+ }
+ }
+ return FALSE;
+}
+
+static buffer_t *
+keywords_get_header_buf(struct mail_index_map *map,
+ const struct mail_index_ext *ext,
+ unsigned int new_count, unsigned int *keywords_count_r,
+ size_t *rec_offset_r, size_t *name_offset_root_r,
+ size_t *name_offset_r)
+{
+ buffer_t *buf;
+ const struct mail_index_keyword_header *kw_hdr;
+ const struct mail_index_keyword_header_rec *kw_rec;
+ const char *name;
+ struct mail_index_keyword_header new_kw_hdr;
+ uint32_t offset;
+
+ kw_hdr = MAIL_INDEX_MAP_HDR_OFFSET(map, ext->hdr_offset);
+ kw_rec = (const void *)(kw_hdr + 1);
+ name = (const char *)(kw_rec + kw_hdr->keywords_count);
+
+ if (kw_hdr->keywords_count == 0)
+ return NULL;
+
+ i_assert((size_t)(name - (const char *)kw_hdr) < ext->hdr_size);
+
+ new_kw_hdr = *kw_hdr;
+ new_kw_hdr.keywords_count += new_count;
+ *keywords_count_r = new_kw_hdr.keywords_count;
+
+ offset = kw_rec[kw_hdr->keywords_count-1].name_offset;
+ offset += strlen(name + offset) + 1;
+
+ buf = t_buffer_create(512);
+ buffer_append(buf, &new_kw_hdr, sizeof(new_kw_hdr));
+ buffer_append(buf, kw_rec, sizeof(*kw_rec) * kw_hdr->keywords_count);
+ *rec_offset_r = buf->used;
+ buffer_write(buf, buf->used + sizeof(*kw_rec) * new_count,
+ name, offset);
+ *name_offset_root_r = buf->used;
+ *name_offset_r = offset;
+ return buf;
+}
+
+static void keywords_ext_register(struct mail_index_sync_map_ctx *ctx,
+ uint32_t ext_map_idx, uint32_t reset_id,
+ uint32_t hdr_size, uint32_t keywords_count)
+{
+ buffer_t ext_intro_buf;
+ struct mail_transaction_ext_intro *u;
+ unsigned char ext_intro_data[sizeof(*u) +
+ sizeof(MAIL_INDEX_EXT_KEYWORDS)-1];
+
+ i_assert(keywords_count > 0);
+
+ buffer_create_from_data(&ext_intro_buf, ext_intro_data,
+ sizeof(ext_intro_data));
+
+ u = buffer_append_space_unsafe(&ext_intro_buf, sizeof(*u));
+ u->ext_id = ext_map_idx;
+ u->reset_id = reset_id;
+ u->hdr_size = hdr_size;
+ u->record_size = (keywords_count + CHAR_BIT - 1) / CHAR_BIT;
+ if ((u->record_size % 4) != 0) {
+ /* since we aren't properly aligned anyway,
+ reserve one extra byte for future */
+ u->record_size++;
+ }
+ u->record_align = 1;
+
+ if (ext_map_idx == (uint32_t)-1) {
+ u->name_size = strlen(MAIL_INDEX_EXT_KEYWORDS);
+ buffer_append(&ext_intro_buf, MAIL_INDEX_EXT_KEYWORDS,
+ u->name_size);
+ }
+
+ ctx->internal_update = TRUE;
+ if (mail_index_sync_ext_intro(ctx, u) < 0)
+ i_panic("Keyword extension growing failed");
+ ctx->internal_update = FALSE;
+}
+
+static void
+keywords_header_add(struct mail_index_sync_map_ctx *ctx,
+ const char *keyword_name, unsigned int *keyword_idx_r)
+{
+ struct mail_index_map *map;
+ const struct mail_index_ext *ext = NULL;
+ struct mail_index_keyword_header *kw_hdr;
+ struct mail_index_keyword_header_rec kw_rec;
+ uint32_t ext_map_idx;
+ buffer_t *buf = NULL;
+ size_t keyword_len, rec_offset, name_offset, name_offset_root;
+ unsigned int keywords_count;
+
+ /* if we crash in the middle of writing the header, the
+ keywords are more or less corrupted. avoid that by
+ making sure the header is updated atomically. */
+ map = mail_index_sync_get_atomic_map(ctx);
+
+ if (!mail_index_map_lookup_ext(map, MAIL_INDEX_EXT_KEYWORDS,
+ &ext_map_idx))
+ ext_map_idx = (uint32_t)-1;
+ else {
+ /* update existing header */
+ ext = array_idx(&map->extensions, ext_map_idx);
+ buf = keywords_get_header_buf(map, ext, 1, &keywords_count,
+ &rec_offset, &name_offset_root,
+ &name_offset);
+ }
+
+ if (buf == NULL) {
+ /* create new / replace broken header */
+ const unsigned int initial_keywords_count = 1;
+
+ buf = t_buffer_create(512);
+ kw_hdr = buffer_append_space_unsafe(buf, sizeof(*kw_hdr));
+ kw_hdr->keywords_count = initial_keywords_count;
+
+ keywords_count = kw_hdr->keywords_count;
+ rec_offset = buf->used;
+ name_offset_root = rec_offset +
+ initial_keywords_count * sizeof(kw_rec);
+ name_offset = 0;
+ }
+
+ /* add the keyword */
+ i_zero(&kw_rec);
+ kw_rec.name_offset = name_offset;
+
+ keyword_len = strlen(keyword_name) + 1;
+ buffer_write(buf, rec_offset, &kw_rec, sizeof(kw_rec));
+ buffer_write(buf, name_offset_root, keyword_name, keyword_len);
+
+ rec_offset += sizeof(kw_rec);
+ kw_rec.name_offset += keyword_len;
+ name_offset_root += keyword_len;
+
+ if ((buf->used % 4) != 0)
+ buffer_append_zero(buf, 4 - (buf->used % 4));
+
+ if (ext == NULL || buf->used > ext->hdr_size ||
+ (uint32_t)ext->record_size * CHAR_BIT < keywords_count) {
+ /* if we need to grow the buffer, add some padding */
+ buffer_append_zero(buf, 128);
+ keywords_ext_register(ctx, ext_map_idx,
+ ext == NULL ? 0 : ext->reset_id,
+ buf->used, keywords_count);
+
+ /* map may have changed */
+ map = ctx->view->map;
+
+ if (!mail_index_map_lookup_ext(map, MAIL_INDEX_EXT_KEYWORDS,
+ &ext_map_idx))
+ i_unreached();
+ ext = array_idx(&map->extensions, ext_map_idx);
+
+ i_assert(ext->hdr_size == buf->used);
+ }
+
+ buffer_copy(map->hdr_copy_buf, ext->hdr_offset, buf, 0, buf->used);
+ i_assert(map->hdr_copy_buf->used == map->hdr.header_size);
+
+ if (mail_index_map_parse_keywords(map) < 0)
+ i_panic("Keyword update corrupted keywords header");
+
+ *keyword_idx_r = keywords_count - 1;
+ i_assert(*keyword_idx_r / CHAR_BIT < ext->record_size);
+}
+
+static int
+keywords_update_records(struct mail_index_sync_map_ctx *ctx,
+ const struct mail_index_ext *ext,
+ unsigned int keyword_idx, enum modify_type type,
+ uint32_t uid1, uint32_t uid2)
+{
+ struct mail_index_view *view = ctx->view;
+ struct mail_index_record *rec;
+ unsigned char *data, data_mask;
+ unsigned int data_offset;
+ uint32_t seq1, seq2;
+
+ i_assert(keyword_idx != UINT_MAX);
+
+ if (!mail_index_lookup_seq_range(view, uid1, uid2, &seq1, &seq2))
+ return 1;
+
+ mail_index_modseq_update_keyword(ctx->modseq_ctx, keyword_idx,
+ seq1, seq2);
+
+ data_offset = keyword_idx / CHAR_BIT;
+ data_mask = 1 << (keyword_idx % CHAR_BIT);
+
+ i_assert(data_offset < ext->record_size);
+ data_offset += ext->record_offset;
+
+ i_assert(data_offset >= MAIL_INDEX_RECORD_MIN_SIZE);
+
+ switch (type) {
+ case MODIFY_ADD:
+ for (; seq1 <= seq2; seq1++) {
+ rec = MAIL_INDEX_REC_AT_SEQ(view->map, seq1);
+ data = PTR_OFFSET(rec, data_offset);
+ *data |= data_mask;
+ }
+ break;
+ case MODIFY_REMOVE:
+ data_mask = (unsigned char)~data_mask;
+ for (; seq1 <= seq2; seq1++) {
+ rec = MAIL_INDEX_REC_AT_SEQ(view->map, seq1);
+ data = PTR_OFFSET(rec, data_offset);
+ *data &= data_mask;
+ }
+ break;
+ default:
+ i_unreached();
+ }
+ return 1;
+}
+
+int mail_index_sync_keywords(struct mail_index_sync_map_ctx *ctx,
+ const struct mail_transaction_header *hdr,
+ const struct mail_transaction_keyword_update *rec)
+{
+ struct mail_index_view *view = ctx->view;
+ const char *keyword_name;
+ const struct mail_index_ext *ext;
+ const uint32_t *uid, *end;
+ uint32_t seqset_offset, ext_map_idx;
+ unsigned int keyword_idx;
+ int ret;
+
+ i_assert(rec->name_size > 0);
+
+ seqset_offset = sizeof(*rec) + rec->name_size;
+ if ((seqset_offset % 4) != 0)
+ seqset_offset += 4 - (seqset_offset % 4);
+ i_assert(seqset_offset < hdr->size);
+
+ uid = CONST_PTR_OFFSET(rec, seqset_offset);
+ end = CONST_PTR_OFFSET(rec, hdr->size);
+
+ keyword_name = t_strndup(rec + 1, rec->name_size);
+ if (!keyword_lookup(ctx, keyword_name, &keyword_idx))
+ keywords_header_add(ctx, keyword_name, &keyword_idx);
+
+ /* if the keyword wasn't found, the "keywords" extension was created.
+ if it was found, the record size should already be correct, but
+ in case it isn't just fix it ourself. */
+ if (!mail_index_map_lookup_ext(view->map, MAIL_INDEX_EXT_KEYWORDS,
+ &ext_map_idx))
+ i_unreached();
+
+ ext = array_idx(&view->map->extensions, ext_map_idx);
+ if (keyword_idx / CHAR_BIT >= ext->record_size) {
+ if (rec->modify_type == MODIFY_REMOVE) {
+ /* nothing to do */
+ return 1;
+ }
+
+ /* grow the record size */
+ keywords_ext_register(ctx, ext_map_idx, ext->reset_id,
+ ext->hdr_size,
+ array_count(&view->map->keyword_idx_map));
+ if (!mail_index_map_lookup_ext(view->map,
+ MAIL_INDEX_EXT_KEYWORDS,
+ &ext_map_idx))
+ i_unreached();
+ ext = array_idx(&view->map->extensions, ext_map_idx);
+ }
+
+ while (uid+2 <= end) {
+ ret = keywords_update_records(ctx, ext, keyword_idx,
+ rec->modify_type,
+ uid[0], uid[1]);
+ if (ret <= 0)
+ return ret;
+
+ uid += 2;
+ }
+
+ return 1;
+}
+
+int
+mail_index_sync_keywords_reset(struct mail_index_sync_map_ctx *ctx,
+ const struct mail_transaction_header *hdr,
+ const struct mail_transaction_keyword_reset *r)
+{
+ struct mail_index_map *map = ctx->view->map;
+ struct mail_index_record *rec;
+ const struct mail_index_ext *ext;
+ const struct mail_transaction_keyword_reset *end;
+ uint32_t ext_map_idx, seq1, seq2;
+
+ if (!mail_index_map_lookup_ext(map, MAIL_INDEX_EXT_KEYWORDS,
+ &ext_map_idx)) {
+ /* nothing to do */
+ return 1;
+ }
+
+ ext = array_idx(&map->extensions, ext_map_idx);
+ end = CONST_PTR_OFFSET(r, hdr->size);
+ for (; r != end; r++) {
+ if (!mail_index_lookup_seq_range(ctx->view, r->uid1, r->uid2,
+ &seq1, &seq2))
+ continue;
+
+ mail_index_modseq_reset_keywords(ctx->modseq_ctx, seq1, seq2);
+ for (; seq1 <= seq2; seq1++) {
+ rec = MAIL_INDEX_REC_AT_SEQ(map, seq1);
+ memset(PTR_OFFSET(rec, ext->record_offset),
+ 0, ext->record_size);
+ }
+ }
+ return 1;
+}
diff --git a/src/lib-index/mail-index-sync-private.h b/src/lib-index/mail-index-sync-private.h
new file mode 100644
index 0000000..094c83d
--- /dev/null
+++ b/src/lib-index/mail-index-sync-private.h
@@ -0,0 +1,104 @@
+#ifndef MAIL_INDEX_SYNC_PRIVATE_H
+#define MAIL_INDEX_SYNC_PRIVATE_H
+
+#include "mail-index-private.h"
+#include "mail-transaction-log.h"
+
+struct uid_range {
+ uint32_t uid1, uid2;
+};
+ARRAY_DEFINE_TYPE(uid_range, struct uid_range);
+
+struct mail_index_sync_list {
+ const ARRAY_TYPE(uid_range) *array;
+ unsigned int idx;
+ unsigned int keyword_idx:31;
+ bool keyword_remove:1;
+};
+
+struct mail_index_expunge_handler {
+ mail_index_expunge_handler_t *handler;
+ void *context;
+ void **sync_context;
+ uint32_t record_offset;
+};
+
+struct mail_index_sync_map_ctx {
+ struct mail_index_view *view;
+ struct mail_index_modseq_sync *modseq_ctx;
+ uint32_t cur_ext_map_idx;
+ uint32_t cur_ext_record_size;
+
+ uint32_t ext_intro_seq;
+ uoff_t ext_intro_offset, ext_intro_end_offset;
+
+ ARRAY(struct mail_index_expunge_handler) expunge_handlers;
+ ARRAY(void *) extra_contexts;
+ buffer_t *unknown_extensions;
+
+ enum mail_index_sync_handler_type type;
+
+ bool sync_handlers_initialized:1;
+ bool expunge_handlers_set:1;
+ bool expunge_handlers_used:1;
+ bool cur_ext_ignore:1;
+ bool internal_update:1; /* used by keywords for ext_intro */
+ bool errors:1;
+};
+
+extern struct mail_transaction_map_functions mail_index_map_sync_funcs;
+
+void mail_index_sync_map_init(struct mail_index_sync_map_ctx *sync_map_ctx,
+ struct mail_index_view *view,
+ enum mail_index_sync_handler_type type);
+void mail_index_sync_map_deinit(struct mail_index_sync_map_ctx *sync_map_ctx);
+bool mail_index_sync_map_want_index_reopen(struct mail_index_map *map,
+ enum mail_index_sync_handler_type type);
+int mail_index_sync_map(struct mail_index_map **_map,
+ enum mail_index_sync_handler_type type,
+ const char **reason_r);
+
+int mail_index_sync_record(struct mail_index_sync_map_ctx *ctx,
+ const struct mail_transaction_header *hdr,
+ const void *data);
+
+struct mail_index_map *
+mail_index_sync_get_atomic_map(struct mail_index_sync_map_ctx *ctx);
+
+void mail_index_sync_init_expunge_handlers(struct mail_index_sync_map_ctx *ctx);
+void
+mail_index_sync_deinit_expunge_handlers(struct mail_index_sync_map_ctx *ctx);
+void mail_index_sync_init_handlers(struct mail_index_sync_map_ctx *ctx);
+void mail_index_sync_deinit_handlers(struct mail_index_sync_map_ctx *ctx);
+
+int mail_index_sync_ext_intro(struct mail_index_sync_map_ctx *ctx,
+ const struct mail_transaction_ext_intro *u);
+int mail_index_sync_ext_reset(struct mail_index_sync_map_ctx *ctx,
+ const struct mail_transaction_ext_reset *u);
+int mail_index_sync_ext_hdr_update(struct mail_index_sync_map_ctx *ctx,
+ uint32_t offset, uint32_t size,
+ const void *data);
+int
+mail_index_sync_ext_rec_update(struct mail_index_sync_map_ctx *ctx,
+ const struct mail_transaction_ext_rec_update *u);
+int
+mail_index_sync_ext_atomic_inc(struct mail_index_sync_map_ctx *ctx,
+ const struct mail_transaction_ext_atomic_inc *u);
+
+int mail_index_sync_keywords(struct mail_index_sync_map_ctx *ctx,
+ const struct mail_transaction_header *hdr,
+ const struct mail_transaction_keyword_update *rec);
+int
+mail_index_sync_keywords_reset(struct mail_index_sync_map_ctx *ctx,
+ const struct mail_transaction_header *hdr,
+ const struct mail_transaction_keyword_reset *r);
+
+void mail_index_sync_set_corrupted(struct mail_index_sync_map_ctx *ctx,
+ const char *fmt, ...)
+ ATTR_FORMAT(2, 3) ATTR_COLD;
+
+#ifdef DEBUG
+void mail_index_map_check(struct mail_index_map *map);
+#endif
+
+#endif
diff --git a/src/lib-index/mail-index-sync-update.c b/src/lib-index/mail-index-sync-update.c
new file mode 100644
index 0000000..0c37995
--- /dev/null
+++ b/src/lib-index/mail-index-sync-update.c
@@ -0,0 +1,1087 @@
+/* Copyright (c) 2004-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "ioloop.h"
+#include "array.h"
+#include "mmap-util.h"
+#include "mail-index-modseq.h"
+#include "mail-index-view-private.h"
+#include "mail-index-sync-private.h"
+#include "mail-transaction-log.h"
+#include "mail-transaction-log-private.h"
+
+/* If we have less than this many bytes to sync from log file, don't bother
+ reading the main index */
+#define MAIL_INDEX_SYNC_MIN_READ_INDEX_SIZE 2048
+
+static void
+mail_index_sync_update_log_offset(struct mail_index_sync_map_ctx *ctx,
+ struct mail_index_map *map, bool eol)
+{
+ uint32_t prev_seq;
+ uoff_t prev_offset;
+
+ mail_transaction_log_view_get_prev_pos(ctx->view->log_view,
+ &prev_seq, &prev_offset);
+ if (prev_seq == 0) {
+ /* handling lost changes in view syncing */
+ return;
+ }
+
+ if (!eol) {
+ if (prev_offset == ctx->ext_intro_end_offset &&
+ prev_seq == ctx->ext_intro_seq) {
+ /* previous transaction was an extension introduction.
+ we probably came here from
+ mail_index_sync_ext_reset(). if there are any more
+ views which want to continue syncing it needs the
+ intro. so back up a bit more.
+
+ don't do this in case the last transaction in the
+ log is the extension intro, so we don't keep trying
+ to sync it over and over again. */
+ prev_offset = ctx->ext_intro_offset;
+ }
+ map->hdr.log_file_seq = prev_seq;
+ } else {
+ i_assert(ctx->view->index->log->head->hdr.file_seq == prev_seq);
+ if (map->hdr.log_file_seq != prev_seq) {
+ map->hdr.log_file_seq = prev_seq;
+ map->hdr.log_file_tail_offset = 0;
+ }
+ }
+ map->hdr.log_file_head_offset = prev_offset;
+}
+
+static void mail_index_sync_replace_map(struct mail_index_sync_map_ctx *ctx,
+ struct mail_index_map *map)
+{
+ struct mail_index_view *view = ctx->view;
+
+ i_assert(view->map != map);
+
+ mail_index_sync_update_log_offset(ctx, view->map, FALSE);
+ mail_index_unmap(&view->map);
+ view->map = map;
+
+ if (ctx->type != MAIL_INDEX_SYNC_HANDLER_VIEW)
+ view->index->map = map;
+
+ mail_index_modseq_sync_map_replaced(ctx->modseq_ctx);
+}
+
+static struct mail_index_map *
+mail_index_sync_move_to_private_memory(struct mail_index_sync_map_ctx *ctx)
+{
+ struct mail_index_map *map = ctx->view->map;
+
+ if (map->refcount > 1) {
+ /* Multiple views point to this map. Make a copy of the map
+ (but not rec_map). */
+ map = mail_index_map_clone(map);
+ mail_index_sync_replace_map(ctx, map);
+ i_assert(ctx->view->map == map);
+ }
+
+ if (!MAIL_INDEX_MAP_IS_IN_MEMORY(ctx->view->map)) {
+ /* map points to mmap()ed area, copy it into memory. */
+ mail_index_map_move_to_memory(ctx->view->map);
+ mail_index_modseq_sync_map_replaced(ctx->modseq_ctx);
+ }
+ return map;
+}
+
+struct mail_index_map *
+mail_index_sync_get_atomic_map(struct mail_index_sync_map_ctx *ctx)
+{
+ /* First make sure we have a private map with rec_map pointing to
+ memory. */
+ (void)mail_index_sync_move_to_private_memory(ctx);
+ /* Next make sure the rec_map is also private to us. */
+ mail_index_record_map_move_to_private(ctx->view->map);
+ mail_index_modseq_sync_map_replaced(ctx->modseq_ctx);
+ return ctx->view->map;
+}
+
+static int
+mail_index_header_update_counts(struct mail_index_header *hdr,
+ uint8_t old_flags, uint8_t new_flags,
+ const char **error_r)
+{
+ if (((old_flags ^ new_flags) & MAIL_SEEN) != 0) {
+ /* different seen-flag */
+ if ((old_flags & MAIL_SEEN) != 0) {
+ if (hdr->seen_messages_count == 0) {
+ *error_r = "Seen counter wrong";
+ return -1;
+ }
+ hdr->seen_messages_count--;
+ } else {
+ if (hdr->seen_messages_count >= hdr->messages_count) {
+ *error_r = "Seen counter wrong";
+ return -1;
+ }
+
+ if (++hdr->seen_messages_count == hdr->messages_count)
+ hdr->first_unseen_uid_lowwater = hdr->next_uid;
+ }
+ }
+
+ if (((old_flags ^ new_flags) & MAIL_DELETED) != 0) {
+ /* different deleted-flag */
+ if ((old_flags & MAIL_DELETED) == 0) {
+ hdr->deleted_messages_count++;
+ if (hdr->deleted_messages_count > hdr->messages_count) {
+ *error_r = "Deleted counter wrong";
+ return -1;
+ }
+ } else {
+ if (hdr->deleted_messages_count == 0 ||
+ hdr->deleted_messages_count > hdr->messages_count) {
+ *error_r = "Deleted counter wrong";
+ return -1;
+ }
+
+ if (--hdr->deleted_messages_count == 0)
+ hdr->first_deleted_uid_lowwater = hdr->next_uid;
+ }
+ }
+ return 0;
+}
+
+static void
+mail_index_sync_header_update_counts_all(struct mail_index_sync_map_ctx *ctx,
+ uint32_t uid,
+ uint8_t old_flags, uint8_t new_flags)
+{
+ struct mail_index_map *const *maps;
+ const char *error;
+ unsigned int i, count;
+
+ maps = array_get(&ctx->view->map->rec_map->maps, &count);
+ for (i = 0; i < count; i++) {
+ if (uid >= maps[i]->hdr.next_uid)
+ continue;
+
+ if (mail_index_header_update_counts(&maps[i]->hdr,
+ old_flags, new_flags,
+ &error) < 0)
+ mail_index_sync_set_corrupted(ctx, "%s", error);
+ }
+}
+
+static void
+mail_index_sync_header_update_counts(struct mail_index_sync_map_ctx *ctx,
+ uint32_t uid, uint8_t old_flags,
+ uint8_t new_flags)
+{
+ const char *error;
+
+ if (uid >= ctx->view->map->hdr.next_uid) {
+ mail_index_sync_set_corrupted(ctx, "uid %u >= next_uid %u",
+ uid, ctx->view->map->hdr.next_uid);
+ } else {
+ if (mail_index_header_update_counts(&ctx->view->map->hdr,
+ old_flags, new_flags,
+ &error) < 0)
+ mail_index_sync_set_corrupted(ctx, "%s", error);
+ }
+}
+
+static void
+mail_index_header_update_lowwaters(struct mail_index_sync_map_ctx *ctx,
+ uint32_t uid, enum mail_flags flags)
+{
+ struct mail_index_map *const *maps;
+ unsigned int i, count;
+
+ maps = array_get(&ctx->view->map->rec_map->maps, &count);
+ for (i = 0; i < count; i++) {
+ if ((flags & MAIL_SEEN) == 0 &&
+ uid < maps[i]->hdr.first_unseen_uid_lowwater)
+ maps[i]->hdr.first_unseen_uid_lowwater = uid;
+ if ((flags & MAIL_DELETED) != 0 &&
+ uid < maps[i]->hdr.first_deleted_uid_lowwater)
+ maps[i]->hdr.first_deleted_uid_lowwater = uid;
+ }
+}
+
+static void
+sync_expunge_call_handlers(struct mail_index_sync_map_ctx *ctx,
+ uint32_t seq1, uint32_t seq2)
+{
+ const struct mail_index_expunge_handler *eh;
+ struct mail_index_record *rec;
+ uint32_t seq;
+
+ array_foreach(&ctx->expunge_handlers, eh) {
+ for (seq = seq1; seq <= seq2; seq++) {
+ rec = MAIL_INDEX_REC_AT_SEQ(ctx->view->map, seq);
+ eh->handler(ctx, PTR_OFFSET(rec, eh->record_offset),
+ eh->sync_context);
+ }
+ }
+}
+
+static bool
+sync_expunge_handlers_init(struct mail_index_sync_map_ctx *ctx)
+{
+ /* call expunge handlers only when syncing index file */
+ if (ctx->type != MAIL_INDEX_SYNC_HANDLER_FILE)
+ return FALSE;
+
+ if (!ctx->expunge_handlers_set)
+ mail_index_sync_init_expunge_handlers(ctx);
+
+ if (!array_is_created(&ctx->expunge_handlers))
+ return FALSE;
+ return TRUE;
+}
+
+static void
+sync_expunge_range(struct mail_index_sync_map_ctx *ctx, const ARRAY_TYPE(seq_range) *seqs)
+{
+ struct mail_index_map *map;
+ const struct seq_range *range;
+ unsigned int i, count;
+ uint32_t dest_seq1, prev_seq2, orig_rec_count;
+
+ range = array_get(seqs, &count);
+ if (count == 0)
+ return;
+
+ /* Get a private in-memory rec_map, which we can modify. */
+ map = mail_index_sync_get_atomic_map(ctx);
+
+ /* call the expunge handlers first */
+ if (sync_expunge_handlers_init(ctx)) {
+ for (i = 0; i < count; i++) {
+ sync_expunge_call_handlers(ctx,
+ range[i].seq1, range[i].seq2);
+ }
+ }
+
+ prev_seq2 = 0;
+ dest_seq1 = 1;
+ orig_rec_count = map->rec_map->records_count;
+ for (i = 0; i < count; i++) {
+ uint32_t seq1 = range[i].seq1;
+ uint32_t seq2 = range[i].seq2;
+ struct mail_index_record *rec;
+ uint32_t seq_count, seq;
+
+ i_assert(seq1 > prev_seq2);
+
+ for (seq = seq1; seq <= seq2; seq++) {
+ rec = MAIL_INDEX_REC_AT_SEQ(map, seq);
+ mail_index_sync_header_update_counts(ctx, rec->uid, rec->flags, 0);
+ }
+
+ if (prev_seq2+1 <= seq1-1) {
+ /* @UNSAFE: move (prev_seq2+1) .. (seq1-1) to its
+ final location in the map if necessary */
+ uint32_t move_count = (seq1-1) - (prev_seq2+1) + 1;
+ if (prev_seq2+1-1 != dest_seq1-1)
+ memmove(MAIL_INDEX_REC_AT_SEQ(map, dest_seq1),
+ MAIL_INDEX_REC_AT_SEQ(map, prev_seq2+1),
+ move_count * map->hdr.record_size);
+ dest_seq1 += move_count;
+ }
+ seq_count = seq2 - seq1 + 1;
+ map->rec_map->records_count -= seq_count;
+ map->hdr.messages_count -= seq_count;
+ mail_index_modseq_expunge(ctx->modseq_ctx, seq1, seq2);
+ prev_seq2 = seq2;
+ }
+ /* Final stragglers */
+ if (orig_rec_count > prev_seq2) {
+ uint32_t final_move_count = orig_rec_count - prev_seq2;
+ memmove(MAIL_INDEX_REC_AT_SEQ(map, dest_seq1),
+ MAIL_INDEX_REC_AT_SEQ(map, prev_seq2+1),
+ final_move_count * map->hdr.record_size);
+ }
+}
+
+static void *sync_append_record(struct mail_index_map *map)
+{
+ size_t append_pos;
+ void *ret;
+
+ append_pos = map->rec_map->records_count * map->hdr.record_size;
+ ret = buffer_get_space_unsafe(map->rec_map->buffer, append_pos,
+ map->hdr.record_size);
+ map->rec_map->records =
+ buffer_get_modifiable_data(map->rec_map->buffer, NULL);
+ return ret;
+}
+
+static bool sync_update_ignored_change(struct mail_index_sync_map_ctx *ctx)
+{
+ struct mail_index_transaction_commit_result *result =
+ ctx->view->index->sync_commit_result;
+ uint32_t prev_log_seq;
+ uoff_t prev_log_offset, trans_start_offset, trans_end_offset;
+
+ if (result == NULL)
+ return FALSE;
+
+ /* we'll return TRUE if this modseq change was written within the
+ transaction that was just committed */
+ mail_transaction_log_view_get_prev_pos(ctx->view->log_view,
+ &prev_log_seq, &prev_log_offset);
+ if (prev_log_seq != result->log_file_seq)
+ return FALSE;
+
+ trans_end_offset = result->log_file_offset;
+ trans_start_offset = trans_end_offset - result->commit_size;
+ if (prev_log_offset < trans_start_offset ||
+ prev_log_offset >= trans_end_offset)
+ return FALSE;
+
+ return TRUE;
+}
+
+static int
+sync_modseq_update(struct mail_index_sync_map_ctx *ctx,
+ const struct mail_transaction_modseq_update *u,
+ unsigned int size)
+{
+ struct mail_index_view *view = ctx->view;
+ const struct mail_transaction_modseq_update *end;
+ uint32_t seq;
+ uint64_t min_modseq;
+ int ret;
+
+ end = CONST_PTR_OFFSET(u, size);
+ for (; u < end; u++) {
+ if (u->uid == 0)
+ seq = 0;
+ else if (!mail_index_lookup_seq(view, u->uid, &seq))
+ continue;
+
+ min_modseq = ((uint64_t)u->modseq_high32 << 32) |
+ u->modseq_low32;
+
+ ret = seq == 0 ? 1 :
+ mail_index_modseq_set(view, seq, min_modseq);
+ if (ret < 0) {
+ mail_index_sync_set_corrupted(ctx,
+ "modseqs updated before they were enabled");
+ return -1;
+ }
+ if (ret == 0 && sync_update_ignored_change(ctx))
+ view->index->sync_commit_result->ignored_modseq_changes++;
+ }
+ return 1;
+}
+
+static int sync_append(const struct mail_index_record *rec,
+ struct mail_index_sync_map_ctx *ctx)
+{
+ struct mail_index_view *view = ctx->view;
+ struct mail_index_map *map = view->map;
+ const struct mail_index_record *old_rec;
+ enum mail_flags new_flags;
+ void *dest;
+
+ if (rec->uid < map->hdr.next_uid) {
+ mail_index_sync_set_corrupted(ctx,
+ "Append with UID %u, but next_uid = %u",
+ rec->uid, map->hdr.next_uid);
+ return -1;
+ }
+
+ /* We'll need to append a new record. If map currently points to
+ mmap()ed index, it first needs to be moved to memory since we can't
+ write past the mmap()ed memory area. */
+ map = mail_index_sync_move_to_private_memory(ctx);
+
+ if (rec->uid <= map->rec_map->last_appended_uid) {
+ i_assert(map->hdr.messages_count < map->rec_map->records_count);
+ /* the flags may have changed since it was added to map.
+ use the updated flags already, so flag counters won't get
+ broken. */
+ old_rec = MAIL_INDEX_MAP_IDX(map, map->hdr.messages_count);
+ i_assert(old_rec->uid == rec->uid);
+ new_flags = old_rec->flags;
+ } else {
+ /* don't rely on buffer->used being at the correct position.
+ at least expunges can move it */
+ dest = sync_append_record(map);
+ memcpy(dest, rec, sizeof(*rec));
+ memset(PTR_OFFSET(dest, sizeof(*rec)), 0,
+ map->hdr.record_size - sizeof(*rec));
+ map->rec_map->records_count++;
+ map->rec_map->last_appended_uid = rec->uid;
+ new_flags = rec->flags;
+
+ mail_index_modseq_append(ctx->modseq_ctx,
+ map->rec_map->records_count);
+ }
+
+ map->hdr.messages_count++;
+ map->hdr.next_uid = rec->uid+1;
+
+ if ((new_flags & MAIL_INDEX_MAIL_FLAG_DIRTY) != 0 &&
+ (view->index->flags & MAIL_INDEX_OPEN_FLAG_NO_DIRTY) == 0)
+ map->hdr.flags |= MAIL_INDEX_HDR_FLAG_HAVE_DIRTY;
+
+ mail_index_header_update_lowwaters(ctx, rec->uid, new_flags);
+ mail_index_sync_header_update_counts(ctx, rec->uid, 0, new_flags);
+ return 1;
+}
+
+static int sync_flag_update(const struct mail_transaction_flag_update *u,
+ struct mail_index_sync_map_ctx *ctx)
+{
+ struct mail_index_view *view = ctx->view;
+ struct mail_index_record *rec;
+ uint8_t flag_mask, old_flags;
+ uint32_t seq, seq1, seq2;
+
+ if (!mail_index_lookup_seq_range(view, u->uid1, u->uid2, &seq1, &seq2))
+ return 1;
+
+ if (!MAIL_TRANSACTION_FLAG_UPDATE_IS_INTERNAL(u)) {
+ mail_index_modseq_update_flags(ctx->modseq_ctx,
+ u->add_flags | u->remove_flags,
+ seq1, seq2);
+ }
+
+ if ((u->add_flags & MAIL_INDEX_MAIL_FLAG_DIRTY) != 0 &&
+ (view->index->flags & MAIL_INDEX_OPEN_FLAG_NO_DIRTY) == 0)
+ view->map->hdr.flags |= MAIL_INDEX_HDR_FLAG_HAVE_DIRTY;
+
+ flag_mask = (unsigned char)~u->remove_flags;
+
+ if (((u->add_flags | u->remove_flags) &
+ (MAIL_SEEN | MAIL_DELETED)) == 0) {
+ /* we're not modifying any counted/lowwatered flags */
+ for (seq = seq1; seq <= seq2; seq++) {
+ rec = MAIL_INDEX_REC_AT_SEQ(view->map, seq);
+ rec->flags = (rec->flags & flag_mask) | u->add_flags;
+ }
+ } else {
+ for (seq = seq1; seq <= seq2; seq++) {
+ rec = MAIL_INDEX_REC_AT_SEQ(view->map, seq);
+
+ old_flags = rec->flags;
+ rec->flags = (rec->flags & flag_mask) | u->add_flags;
+
+ mail_index_header_update_lowwaters(ctx, rec->uid,
+ rec->flags);
+ mail_index_sync_header_update_counts_all(ctx, rec->uid,
+ old_flags,
+ rec->flags);
+ }
+ }
+ return 1;
+}
+
+static int sync_header_update(const struct mail_transaction_header_update *u,
+ struct mail_index_sync_map_ctx *ctx)
+{
+#define MAIL_INDEX_HEADER_UPDATE_FIELD_IN_RANGE(u, field) \
+ ((u)->offset <= offsetof(struct mail_index_header, field) && \
+ (u)->offset + (u)->size > offsetof(struct mail_index_header, field))
+ struct mail_index_map *map = ctx->view->map;
+ uint32_t orig_log_file_tail_offset = map->hdr.log_file_tail_offset;
+ uint32_t orig_next_uid = map->hdr.next_uid;
+
+ if (u->offset >= map->hdr.base_header_size ||
+ u->offset + u->size > map->hdr.base_header_size) {
+ mail_index_sync_set_corrupted(ctx,
+ "Header update outside range: %u + %u > %u",
+ u->offset, u->size, map->hdr.base_header_size);
+ return -1;
+ }
+
+ buffer_write(map->hdr_copy_buf, u->offset, u + 1, u->size);
+ i_assert(map->hdr_copy_buf->used == map->hdr.header_size);
+
+ /* @UNSAFE */
+ if ((uint32_t)(u->offset + u->size) <= sizeof(map->hdr)) {
+ memcpy(PTR_OFFSET(&map->hdr, u->offset),
+ u + 1, u->size);
+ } else if (u->offset < sizeof(map->hdr)) {
+ memcpy(PTR_OFFSET(&map->hdr, u->offset),
+ u + 1, sizeof(map->hdr) - u->offset);
+ }
+
+ if (map->hdr.next_uid < orig_next_uid) {
+ /* next_uid update tried to shrink its value. this can happen
+ in some race conditions with e.g. with dsync, so just
+ silently ignore it. */
+ map->hdr.next_uid = orig_next_uid;
+ }
+
+ /* the tail offset updates are intended for internal transaction
+ log handling. we'll update the offset in the header only when
+ the sync is finished. */
+ map->hdr.log_file_tail_offset = orig_log_file_tail_offset;
+ return 1;
+}
+
+static int
+mail_index_sync_record_real(struct mail_index_sync_map_ctx *ctx,
+ const struct mail_transaction_header *hdr,
+ const void *data)
+{
+ int ret = 0;
+
+ switch (hdr->type & MAIL_TRANSACTION_TYPE_MASK) {
+ case MAIL_TRANSACTION_APPEND: {
+ const struct mail_index_record *rec, *end;
+
+ end = CONST_PTR_OFFSET(data, hdr->size);
+ for (rec = data; rec < end; rec++) {
+ ret = sync_append(rec, ctx);
+ if (ret <= 0)
+ break;
+ }
+ break;
+ }
+ case MAIL_TRANSACTION_EXPUNGE:
+ case MAIL_TRANSACTION_EXPUNGE|MAIL_TRANSACTION_EXPUNGE_PROT: {
+ const struct mail_transaction_expunge *rec = data, *end;
+ ARRAY_TYPE(seq_range) seqs;
+ uint32_t seq1, seq2;
+
+ if ((hdr->type & MAIL_TRANSACTION_EXTERNAL) == 0) {
+ /* this is simply a request for expunge */
+ break;
+ }
+ t_array_init(&seqs, 64);
+ end = CONST_PTR_OFFSET(data, hdr->size);
+ for (; rec != end; rec++) {
+ if (mail_index_lookup_seq_range(ctx->view,
+ rec->uid1, rec->uid2, &seq1, &seq2))
+ seq_range_array_add_range(&seqs, seq1, seq2);
+ }
+ sync_expunge_range(ctx, &seqs);
+ break;
+ }
+ case MAIL_TRANSACTION_EXPUNGE_GUID:
+ case MAIL_TRANSACTION_EXPUNGE_GUID|MAIL_TRANSACTION_EXPUNGE_PROT: {
+ const struct mail_transaction_expunge_guid *rec = data, *end;
+ ARRAY_TYPE(seq_range) seqs;
+ uint32_t seq;
+
+ if ((hdr->type & MAIL_TRANSACTION_EXTERNAL) == 0) {
+ /* this is simply a request for expunge */
+ break;
+ }
+ t_array_init(&seqs, 64);
+ end = CONST_PTR_OFFSET(data, hdr->size);
+ for (; rec != end; rec++) {
+ i_assert(rec->uid != 0);
+
+ if (mail_index_lookup_seq(ctx->view, rec->uid, &seq))
+ seq_range_array_add(&seqs, seq);
+ }
+
+ sync_expunge_range(ctx, &seqs);
+ break;
+ }
+ case MAIL_TRANSACTION_FLAG_UPDATE: {
+ const struct mail_transaction_flag_update *rec, *end;
+
+ end = CONST_PTR_OFFSET(data, hdr->size);
+ for (rec = data; rec < end; rec++) {
+ ret = sync_flag_update(rec, ctx);
+ if (ret <= 0)
+ break;
+ }
+ break;
+ }
+ case MAIL_TRANSACTION_HEADER_UPDATE: {
+ const struct mail_transaction_header_update *rec;
+ unsigned int i;
+
+ for (i = 0; i < hdr->size; ) {
+ rec = CONST_PTR_OFFSET(data, i);
+ ret = sync_header_update(rec, ctx);
+ if (ret <= 0)
+ break;
+
+ i += sizeof(*rec) + rec->size;
+ if ((i % 4) != 0)
+ i += 4 - (i % 4);
+ }
+ break;
+ }
+ case MAIL_TRANSACTION_EXT_INTRO: {
+ const struct mail_transaction_ext_intro *rec = data;
+ unsigned int i;
+ uint32_t prev_seq;
+ uoff_t prev_offset;
+
+ mail_transaction_log_view_get_prev_pos(ctx->view->log_view,
+ &prev_seq, &prev_offset);
+ ctx->ext_intro_seq = prev_seq;
+ ctx->ext_intro_offset = prev_offset;
+ ctx->ext_intro_end_offset =
+ prev_offset + hdr->size + sizeof(*hdr);
+
+ for (i = 0; i < hdr->size; ) {
+ if (i + sizeof(*rec) > hdr->size) {
+ /* should be just extra padding */
+ break;
+ }
+
+ rec = CONST_PTR_OFFSET(data, i);
+ /* name_size checked by _log_view_next() */
+ i_assert(i + sizeof(*rec) + rec->name_size <= hdr->size);
+
+ ret = mail_index_sync_ext_intro(ctx, rec);
+ if (ret <= 0)
+ break;
+
+ i += sizeof(*rec) + rec->name_size;
+ if ((i % 4) != 0)
+ i += 4 - (i % 4);
+ }
+ break;
+ }
+ case MAIL_TRANSACTION_EXT_RESET: {
+ struct mail_transaction_ext_reset rec;
+
+ /* old versions have only new_reset_id */
+ if (hdr->size < sizeof(uint32_t)) {
+ mail_index_sync_set_corrupted(ctx,
+ "ext reset: invalid record size");
+ ret = -1;
+ break;
+ }
+ i_zero(&rec);
+ memcpy(&rec, data, I_MIN(hdr->size, sizeof(rec)));
+ ret = mail_index_sync_ext_reset(ctx, &rec);
+ break;
+ }
+ case MAIL_TRANSACTION_EXT_HDR_UPDATE: {
+ const struct mail_transaction_ext_hdr_update *rec;
+ unsigned int i;
+
+ for (i = 0; i < hdr->size; ) {
+ rec = CONST_PTR_OFFSET(data, i);
+
+ if (i + sizeof(*rec) > hdr->size ||
+ i + sizeof(*rec) + rec->size > hdr->size) {
+ mail_index_sync_set_corrupted(ctx,
+ "ext hdr update: invalid record size");
+ ret = -1;
+ break;
+ }
+
+ ret = mail_index_sync_ext_hdr_update(ctx, rec->offset,
+ rec->size, rec + 1);
+ if (ret <= 0)
+ break;
+
+ i += sizeof(*rec) + rec->size;
+ if ((i % 4) != 0)
+ i += 4 - (i % 4);
+ }
+ break;
+ }
+ case MAIL_TRANSACTION_EXT_HDR_UPDATE32: {
+ const struct mail_transaction_ext_hdr_update32 *rec;
+ unsigned int i;
+
+ for (i = 0; i < hdr->size; ) {
+ rec = CONST_PTR_OFFSET(data, i);
+
+ if (i + sizeof(*rec) > hdr->size ||
+ i + sizeof(*rec) + rec->size > hdr->size) {
+ mail_index_sync_set_corrupted(ctx,
+ "ext hdr update: invalid record size");
+ ret = -1;
+ break;
+ }
+
+ ret = mail_index_sync_ext_hdr_update(ctx, rec->offset,
+ rec->size, rec + 1);
+ if (ret <= 0)
+ break;
+
+ i += sizeof(*rec) + rec->size;
+ if ((i % 4) != 0)
+ i += 4 - (i % 4);
+ }
+ break;
+ }
+ case MAIL_TRANSACTION_EXT_REC_UPDATE: {
+ const struct mail_transaction_ext_rec_update *rec;
+ unsigned int i, record_size;
+
+ if (ctx->cur_ext_map_idx == (uint32_t)-1) {
+ mail_index_sync_set_corrupted(ctx,
+ "Extension record updated "
+ "without intro prefix");
+ ret = -1;
+ break;
+ }
+
+ if (ctx->cur_ext_ignore) {
+ ret = 1;
+ break;
+ }
+
+ /* the record is padded to 32bits in the transaction log */
+ record_size = (sizeof(*rec) + ctx->cur_ext_record_size + 3) & ~3U;
+
+ for (i = 0; i < hdr->size; i += record_size) {
+ rec = CONST_PTR_OFFSET(data, i);
+
+ if (i + record_size > hdr->size) {
+ mail_index_sync_set_corrupted(ctx,
+ "ext rec update: invalid record size");
+ ret = -1;
+ break;
+ }
+
+ ret = mail_index_sync_ext_rec_update(ctx, rec);
+ if (ret <= 0)
+ break;
+ }
+ break;
+ }
+ case MAIL_TRANSACTION_EXT_ATOMIC_INC: {
+ const struct mail_transaction_ext_atomic_inc *rec, *end;
+
+ if (ctx->cur_ext_map_idx == (uint32_t)-1) {
+ mail_index_sync_set_corrupted(ctx,
+ "Extension record updated "
+ "without intro prefix");
+ ret = -1;
+ break;
+ }
+
+ if (ctx->cur_ext_ignore) {
+ ret = 1;
+ break;
+ }
+
+ end = CONST_PTR_OFFSET(data, hdr->size);
+ for (rec = data; rec < end; rec++) {
+ ret = mail_index_sync_ext_atomic_inc(ctx, rec);
+ if (ret <= 0)
+ break;
+ }
+ break;
+ }
+ case MAIL_TRANSACTION_KEYWORD_UPDATE: {
+ const struct mail_transaction_keyword_update *rec = data;
+
+ ret = mail_index_sync_keywords(ctx, hdr, rec);
+ break;
+ }
+ case MAIL_TRANSACTION_KEYWORD_RESET: {
+ const struct mail_transaction_keyword_reset *rec = data;
+
+ ret = mail_index_sync_keywords_reset(ctx, hdr, rec);
+ break;
+ }
+ case MAIL_TRANSACTION_MODSEQ_UPDATE: {
+ const struct mail_transaction_modseq_update *rec = data;
+
+ ret = sync_modseq_update(ctx, rec, hdr->size);
+ break;
+ }
+ case MAIL_TRANSACTION_INDEX_DELETED:
+ if ((hdr->type & MAIL_TRANSACTION_EXTERNAL) == 0) {
+ /* next sync finishes the deletion */
+ ctx->view->index->index_delete_requested = TRUE;
+ } else {
+ /* transaction log reading handles this */
+ }
+ break;
+ case MAIL_TRANSACTION_INDEX_UNDELETED:
+ ctx->view->index->index_delete_requested = FALSE;
+ break;
+ case MAIL_TRANSACTION_BOUNDARY:
+ break;
+ case MAIL_TRANSACTION_ATTRIBUTE_UPDATE:
+ break;
+ default:
+ mail_index_sync_set_corrupted(ctx,
+ "Unknown transaction record type 0x%x",
+ (hdr->type & MAIL_TRANSACTION_TYPE_MASK));
+ ret = -1;
+ break;
+ }
+ return ret;
+}
+
+int mail_index_sync_record(struct mail_index_sync_map_ctx *ctx,
+ const struct mail_transaction_header *hdr,
+ const void *data)
+{
+ int ret;
+
+ T_BEGIN {
+ ret = mail_index_sync_record_real(ctx, hdr, data);
+ } T_END;
+ return ret;
+}
+
+void mail_index_sync_map_init(struct mail_index_sync_map_ctx *sync_map_ctx,
+ struct mail_index_view *view,
+ enum mail_index_sync_handler_type type)
+{
+ i_zero(sync_map_ctx);
+ sync_map_ctx->view = view;
+ sync_map_ctx->cur_ext_map_idx = (uint32_t)-1;
+ sync_map_ctx->type = type;
+ sync_map_ctx->modseq_ctx = mail_index_modseq_sync_begin(sync_map_ctx);
+
+ mail_index_sync_init_handlers(sync_map_ctx);
+}
+
+void mail_index_sync_map_deinit(struct mail_index_sync_map_ctx *sync_map_ctx)
+{
+ i_assert(sync_map_ctx->modseq_ctx == NULL);
+
+ buffer_free(&sync_map_ctx->unknown_extensions);
+ if (sync_map_ctx->expunge_handlers_used)
+ mail_index_sync_deinit_expunge_handlers(sync_map_ctx);
+ mail_index_sync_deinit_handlers(sync_map_ctx);
+}
+
+static void mail_index_sync_update_hdr_dirty_flag(struct mail_index_map *map)
+{
+ const struct mail_index_record *rec;
+ uint32_t seq;
+
+ if ((map->hdr.flags & MAIL_INDEX_HDR_FLAG_HAVE_DIRTY) != 0 ||
+ (map->index->flags & MAIL_INDEX_OPEN_FLAG_NO_DIRTY) != 0)
+ return;
+
+ /* do we have dirty flags anymore? */
+ for (seq = 1; seq <= map->rec_map->records_count; seq++) {
+ rec = MAIL_INDEX_REC_AT_SEQ(map, seq);
+ if ((rec->flags & MAIL_INDEX_MAIL_FLAG_DIRTY) != 0) {
+ map->hdr.flags |= MAIL_INDEX_HDR_FLAG_HAVE_DIRTY;
+ break;
+ }
+ }
+}
+
+#ifdef DEBUG
+void mail_index_map_check(struct mail_index_map *map)
+{
+ const struct mail_index_header *hdr = &map->hdr;
+ unsigned int del = 0, seen = 0;
+ uint32_t seq, prev_uid = 0;
+
+ if (getenv("DEBUG_IGNORE_INDEX_CORRUPTION") != NULL)
+ return;
+
+ i_assert(hdr->messages_count <= map->rec_map->records_count);
+ for (seq = 1; seq <= hdr->messages_count; seq++) {
+ const struct mail_index_record *rec;
+
+ rec = MAIL_INDEX_REC_AT_SEQ(map, seq);
+ i_assert(rec->uid > prev_uid);
+ prev_uid = rec->uid;
+
+ if ((rec->flags & MAIL_DELETED) != 0) {
+ i_assert(rec->uid >= hdr->first_deleted_uid_lowwater);
+ del++;
+ }
+ if ((rec->flags & MAIL_SEEN) != 0)
+ seen++;
+ else
+ i_assert(rec->uid >= hdr->first_unseen_uid_lowwater);
+ }
+ i_assert(del == hdr->deleted_messages_count);
+ i_assert(seen == hdr->seen_messages_count);
+}
+#endif
+
+bool mail_index_sync_map_want_index_reopen(struct mail_index_map *map,
+ enum mail_index_sync_handler_type type)
+{
+ struct mail_index *index = map->index;
+
+ if (index->log->head == NULL)
+ return TRUE;
+
+ uoff_t start_offset = type == MAIL_INDEX_SYNC_HANDLER_FILE ?
+ map->hdr.log_file_tail_offset : map->hdr.log_file_head_offset;
+ /* don't check this if mmap is disabled, because reopening
+ index causes sync to get lost. */
+ if ((index->flags & MAIL_INDEX_OPEN_FLAG_MMAP_DISABLE) == 0) {
+ uoff_t log_size, index_size;
+
+ if (index->fd == -1 &&
+ index->log->head->hdr.prev_file_seq != 0) {
+ /* we don't know the index's size, so use the
+ smallest index size we're willing to read */
+ index_size = MAIL_INDEX_SYNC_MIN_READ_INDEX_SIZE;
+ } else {
+ index_size = map->hdr.header_size +
+ map->rec_map->records_count *
+ map->hdr.record_size;
+ }
+
+ /* this isn't necessary correct currently, but it should be
+ close enough */
+ log_size = index->log->head->last_size;
+ if (log_size > start_offset &&
+ log_size - start_offset > index_size)
+ return TRUE;
+ }
+ return FALSE;
+}
+
+int mail_index_sync_map(struct mail_index_map **_map,
+ enum mail_index_sync_handler_type type,
+ const char **reason_r)
+{
+ struct mail_index_map *map = *_map;
+ struct mail_index *index = map->index;
+ struct mail_index_view *view;
+ struct mail_index_sync_map_ctx sync_map_ctx;
+ const struct mail_transaction_header *thdr;
+ const void *tdata;
+ uint32_t prev_seq;
+ uoff_t start_offset, prev_offset;
+ const char *reason, *error;
+ int ret;
+ bool had_dirty, reset;
+
+ i_assert(index->log->head != NULL);
+ i_assert(index->map == map || type == MAIL_INDEX_SYNC_HANDLER_VIEW);
+
+ start_offset = type == MAIL_INDEX_SYNC_HANDLER_FILE ?
+ map->hdr.log_file_tail_offset : map->hdr.log_file_head_offset;
+
+ view = mail_index_view_open_with_map(index, map);
+ ret = mail_transaction_log_view_set(view->log_view,
+ map->hdr.log_file_seq, start_offset,
+ (uint32_t)-1, UOFF_T_MAX,
+ &reset, &reason);
+ if (ret <= 0) {
+ mail_index_view_close(&view);
+ if (ret < 0) {
+ /* I/O failure */
+ return -1;
+ }
+ /* the seq/offset is probably broken */
+ *reason_r = t_strdup_printf(
+ "Lost log for seq=%u offset=%"PRIuUOFF_T": %s "
+ "(initial_mapped=%d)",
+ map->hdr.log_file_seq, start_offset, reason,
+ index->initial_mapped ? 1 : 0);
+ return 0;
+ }
+
+ mail_transaction_log_get_head(index->log, &prev_seq, &prev_offset);
+ if (prev_seq != map->hdr.log_file_seq ||
+ prev_offset - map->hdr.log_file_tail_offset >
+ index->optimization_set.index.rewrite_min_log_bytes) {
+ /* we're reading more from log than we would have preferred.
+ remember that we probably want to rewrite index soon. */
+ index->index_min_write = TRUE;
+ }
+
+ /* view referenced the map. avoid unnecessary map cloning by
+ unreferencing the map while view exists. */
+ map->refcount--;
+
+ had_dirty = (map->hdr.flags & MAIL_INDEX_HDR_FLAG_HAVE_DIRTY) != 0;
+ if (had_dirty)
+ map->hdr.flags &= ENUM_NEGATE(MAIL_INDEX_HDR_FLAG_HAVE_DIRTY);
+
+ mail_transaction_log_view_get_prev_pos(view->log_view,
+ &prev_seq, &prev_offset);
+
+ mail_index_sync_map_init(&sync_map_ctx, view, type);
+ if (reset) {
+ /* Reset the entire index. Leave only indexid and
+ log_file_seq. */
+ mail_transaction_log_view_get_prev_pos(view->log_view,
+ &prev_seq, &prev_offset);
+ map = mail_index_map_alloc(index);
+ if ((index->map->hdr.flags & MAIL_INDEX_HDR_FLAG_FSCKD) != 0)
+ map->hdr.flags |= MAIL_INDEX_HDR_FLAG_FSCKD;
+ map->hdr.log_file_seq = prev_seq;
+ map->hdr.log_file_tail_offset = 0;
+ mail_index_sync_replace_map(&sync_map_ctx, map);
+ }
+ map = NULL;
+
+ /* FIXME: when transaction sync lock is removed, we'll need to handle
+ the case when a transaction is committed while mailbox is being
+ synced ([synced transactions][new transaction][ext transaction]).
+ this means int_offset contains [synced] and ext_offset contains
+ all */
+ while ((ret = mail_transaction_log_view_next(view->log_view, &thdr,
+ &tdata)) > 0) {
+ mail_transaction_log_view_get_prev_pos(view->log_view,
+ &prev_seq, &prev_offset);
+
+ if (LOG_IS_BEFORE(prev_seq, prev_offset,
+ view->map->hdr.log_file_seq,
+ view->map->hdr.log_file_head_offset)) {
+ /* this has been synced already. */
+ i_assert(type == MAIL_INDEX_SYNC_HANDLER_FILE);
+ continue;
+ }
+
+ /* we'll just skip over broken entries */
+ (void)mail_index_sync_record(&sync_map_ctx, thdr, tdata);
+ }
+ map = view->map;
+
+ if (had_dirty)
+ mail_index_sync_update_hdr_dirty_flag(map);
+ mail_index_modseq_sync_end(&sync_map_ctx.modseq_ctx);
+
+ mail_index_sync_update_log_offset(&sync_map_ctx, view->map, TRUE);
+
+#ifdef DEBUG
+ mail_index_map_check(map);
+#endif
+ i_assert(map->hdr.indexid == index->indexid || map->hdr.indexid == 0);
+
+ /* transaction log tracks internally the current tail offset.
+ besides using header updates, it also updates the offset to skip
+ over following external transactions to avoid extra unneeded log
+ reading. */
+ i_assert(map->hdr.log_file_seq == index->log->head->hdr.file_seq);
+ if (map->hdr.log_file_tail_offset < index->log->head->max_tail_offset) {
+ map->hdr.log_file_tail_offset =
+ index->log->head->max_tail_offset;
+ }
+
+ buffer_write(map->hdr_copy_buf, 0, &map->hdr, sizeof(map->hdr));
+ if (!MAIL_INDEX_MAP_IS_IN_MEMORY(map)) {
+ memcpy(map->rec_map->mmap_base, map->hdr_copy_buf->data,
+ map->hdr_copy_buf->used);
+ }
+
+ /* restore refcount before closing the view. this is necessary also
+ if map got cloned, because view closing would otherwise destroy it */
+ map->refcount++;
+ mail_index_sync_map_deinit(&sync_map_ctx);
+ mail_index_view_close(&view);
+
+ i_assert(index->map == map || type == MAIL_INDEX_SYNC_HANDLER_VIEW);
+
+ if (mail_index_map_check_header(map, &error) <= 0) {
+ mail_index_set_error(index,
+ "Synchronization corrupted index header %s: %s",
+ index->filepath, error);
+ (void)mail_index_fsck(index);
+ map = index->map;
+ } else if (sync_map_ctx.errors) {
+ /* make sure the index looks valid now */
+ (void)mail_index_fsck(index);
+ map = index->map;
+ }
+
+ *_map = map;
+ return ret < 0 ? -1 : 1;
+}
diff --git a/src/lib-index/mail-index-sync.c b/src/lib-index/mail-index-sync.c
new file mode 100644
index 0000000..6322ee1
--- /dev/null
+++ b/src/lib-index/mail-index-sync.c
@@ -0,0 +1,1062 @@
+/* Copyright (c) 2003-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "mail-index-view-private.h"
+#include "mail-index-sync-private.h"
+#include "mail-index-transaction-private.h"
+#include "mail-transaction-log-private.h"
+#include "mail-cache-private.h"
+
+#include <stdio.h>
+
+struct mail_index_sync_ctx {
+ struct mail_index *index;
+ struct mail_index_view *view;
+ struct mail_index_transaction *sync_trans, *ext_trans;
+ struct mail_index_transaction_commit_result *sync_commit_result;
+ enum mail_index_sync_flags flags;
+ char *reason;
+
+ const struct mail_transaction_header *hdr;
+ const void *data;
+
+ ARRAY(struct mail_index_sync_list) sync_list;
+ uint32_t next_uid;
+
+ bool no_warning:1;
+ bool seen_external_expunges:1;
+ bool seen_nonexternal_transactions:1;
+ bool fully_synced:1;
+};
+
+static void mail_index_sync_add_expunge(struct mail_index_sync_ctx *ctx)
+{
+ const struct mail_transaction_expunge *e = ctx->data;
+ size_t i, size = ctx->hdr->size / sizeof(*e);
+ uint32_t uid;
+
+ for (i = 0; i < size; i++) {
+ for (uid = e[i].uid1; uid <= e[i].uid2; uid++)
+ mail_index_expunge(ctx->sync_trans, uid);
+ }
+}
+
+static void mail_index_sync_add_expunge_guid(struct mail_index_sync_ctx *ctx)
+{
+ const struct mail_transaction_expunge_guid *e = ctx->data;
+ size_t i, size = ctx->hdr->size / sizeof(*e);
+
+ for (i = 0; i < size; i++) {
+ mail_index_expunge_guid(ctx->sync_trans, e[i].uid,
+ e[i].guid_128);
+ }
+}
+
+static void mail_index_sync_add_flag_update(struct mail_index_sync_ctx *ctx)
+{
+ const struct mail_transaction_flag_update *u = ctx->data;
+ size_t i, size = ctx->hdr->size / sizeof(*u);
+
+ for (i = 0; i < size; i++) {
+ if (u[i].add_flags != 0) {
+ mail_index_update_flags_range(ctx->sync_trans,
+ u[i].uid1, u[i].uid2,
+ MODIFY_ADD,
+ u[i].add_flags);
+ }
+ if (u[i].remove_flags != 0) {
+ mail_index_update_flags_range(ctx->sync_trans,
+ u[i].uid1, u[i].uid2,
+ MODIFY_REMOVE,
+ u[i].remove_flags);
+ }
+ }
+}
+
+static void mail_index_sync_add_keyword_update(struct mail_index_sync_ctx *ctx)
+{
+ const struct mail_transaction_keyword_update *u = ctx->data;
+ const char *keyword_names[2];
+ struct mail_keywords *keywords;
+ const uint32_t *uids;
+ uint32_t uid;
+ size_t uidset_offset, i, size;
+
+ i_assert(u->name_size > 0);
+
+ uidset_offset = sizeof(*u) + u->name_size;
+ if ((uidset_offset % 4) != 0)
+ uidset_offset += 4 - (uidset_offset % 4);
+ uids = CONST_PTR_OFFSET(u, uidset_offset);
+
+ keyword_names[0] = t_strndup(u + 1, u->name_size);
+ keyword_names[1] = NULL;
+ keywords = mail_index_keywords_create(ctx->index, keyword_names);
+
+ size = (ctx->hdr->size - uidset_offset) / sizeof(uint32_t);
+ for (i = 0; i < size; i += 2) {
+ /* FIXME: mail_index_update_keywords_range() */
+ for (uid = uids[i]; uid <= uids[i+1]; uid++) {
+ mail_index_update_keywords(ctx->sync_trans, uid,
+ u->modify_type, keywords);
+ }
+ }
+
+ mail_index_keywords_unref(&keywords);
+}
+
+static void mail_index_sync_add_keyword_reset(struct mail_index_sync_ctx *ctx)
+{
+ const struct mail_transaction_keyword_reset *u = ctx->data;
+ size_t i, size = ctx->hdr->size / sizeof(*u);
+ struct mail_keywords *keywords;
+ uint32_t uid;
+
+ keywords = mail_index_keywords_create(ctx->index, NULL);
+ for (i = 0; i < size; i++) {
+ for (uid = u[i].uid1; uid <= u[i].uid2; uid++) {
+ mail_index_update_keywords(ctx->sync_trans, uid,
+ MODIFY_REPLACE, keywords);
+ }
+ }
+ mail_index_keywords_unref(&keywords);
+}
+
+static bool mail_index_sync_add_transaction(struct mail_index_sync_ctx *ctx)
+{
+ switch (ctx->hdr->type & MAIL_TRANSACTION_TYPE_MASK) {
+ case MAIL_TRANSACTION_EXPUNGE:
+ mail_index_sync_add_expunge(ctx);
+ break;
+ case MAIL_TRANSACTION_EXPUNGE_GUID:
+ mail_index_sync_add_expunge_guid(ctx);
+ break;
+ case MAIL_TRANSACTION_FLAG_UPDATE:
+ mail_index_sync_add_flag_update(ctx);
+ break;
+ case MAIL_TRANSACTION_KEYWORD_UPDATE:
+ mail_index_sync_add_keyword_update(ctx);
+ break;
+ case MAIL_TRANSACTION_KEYWORD_RESET:
+ mail_index_sync_add_keyword_reset(ctx);
+ break;
+ default:
+ return FALSE;
+ }
+ return TRUE;
+}
+
+static void mail_index_sync_add_dirty_updates(struct mail_index_sync_ctx *ctx)
+{
+ struct mail_transaction_flag_update update;
+ const struct mail_index_record *rec;
+ uint32_t seq, messages_count;
+
+ i_zero(&update);
+
+ messages_count = mail_index_view_get_messages_count(ctx->view);
+ for (seq = 1; seq <= messages_count; seq++) {
+ rec = mail_index_lookup(ctx->view, seq);
+ if ((rec->flags & MAIL_INDEX_MAIL_FLAG_DIRTY) == 0)
+ continue;
+
+ mail_index_update_flags(ctx->sync_trans, rec->uid,
+ MODIFY_REPLACE, rec->flags);
+ }
+}
+
+static int
+mail_index_sync_read_and_sort(struct mail_index_sync_ctx *ctx)
+{
+ struct mail_index_transaction *sync_trans = ctx->sync_trans;
+ struct mail_index_sync_list *synclist;
+ const struct mail_index_transaction_keyword_update *keyword_updates;
+ unsigned int i, keyword_count;
+ int ret;
+
+ if ((ctx->view->map->hdr.flags & MAIL_INDEX_HDR_FLAG_HAVE_DIRTY) != 0 &&
+ (ctx->flags & MAIL_INDEX_SYNC_FLAG_FLUSH_DIRTY) != 0 &&
+ (ctx->view->index->flags & MAIL_INDEX_OPEN_FLAG_NO_DIRTY) == 0) {
+ /* show dirty flags as flag updates */
+ mail_index_sync_add_dirty_updates(ctx);
+ }
+
+ /* read all transactions from log into a transaction in memory.
+ skip the external ones, they're already synced to mailbox and
+ included in our view */
+ while ((ret = mail_transaction_log_view_next(ctx->view->log_view,
+ &ctx->hdr,
+ &ctx->data)) > 0) {
+ if ((ctx->hdr->type & MAIL_TRANSACTION_EXTERNAL) != 0) {
+ if ((ctx->hdr->type & (MAIL_TRANSACTION_EXPUNGE |
+ MAIL_TRANSACTION_EXPUNGE_GUID)) != 0)
+ ctx->seen_external_expunges = TRUE;
+ continue;
+ }
+
+ T_BEGIN {
+ if (mail_index_sync_add_transaction(ctx)) {
+ /* update tail_offset if needed */
+ ctx->seen_nonexternal_transactions = TRUE;
+ } else {
+ /* this is an internal change. we don't
+ necessarily need to update tail_offset, so
+ avoid the extra write caused by it. */
+ }
+ } T_END;
+ }
+
+ /* create an array containing all expunge, flag and keyword update
+ arrays so we can easily go through all of the changes. */
+ keyword_count = !array_is_created(&sync_trans->keyword_updates) ? 0 :
+ array_count(&sync_trans->keyword_updates);
+ i_array_init(&ctx->sync_list, keyword_count + 2);
+
+ if (array_is_created(&sync_trans->expunges)) {
+ mail_index_transaction_sort_expunges(sync_trans);
+ synclist = array_append_space(&ctx->sync_list);
+ synclist->array = (void *)&sync_trans->expunges;
+ }
+
+ if (array_is_created(&sync_trans->updates)) {
+ synclist = array_append_space(&ctx->sync_list);
+ synclist->array = (void *)&sync_trans->updates;
+ }
+
+ keyword_updates = keyword_count == 0 ? NULL :
+ array_front(&sync_trans->keyword_updates);
+ for (i = 0; i < keyword_count; i++) {
+ if (array_is_created(&keyword_updates[i].add_seq)) {
+ synclist = array_append_space(&ctx->sync_list);
+ synclist->array =
+ (const void *)&keyword_updates[i].add_seq;
+ synclist->keyword_idx = i;
+ }
+ if (array_is_created(&keyword_updates[i].remove_seq)) {
+ synclist = array_append_space(&ctx->sync_list);
+ synclist->array =
+ (const void *)&keyword_updates[i].remove_seq;
+ synclist->keyword_idx = i;
+ synclist->keyword_remove = TRUE;
+ }
+ }
+
+ return ret;
+}
+
+static bool
+mail_index_need_sync(struct mail_index *index, enum mail_index_sync_flags flags,
+ uint32_t log_file_seq, uoff_t log_file_offset)
+{
+ const struct mail_index_header *hdr = &index->map->hdr;
+ if ((flags & MAIL_INDEX_SYNC_FLAG_REQUIRE_CHANGES) == 0)
+ return TRUE;
+
+ /* sync only if there's something to do */
+ if (hdr->first_recent_uid < hdr->next_uid &&
+ (flags & MAIL_INDEX_SYNC_FLAG_DROP_RECENT) != 0)
+ return TRUE;
+
+ if ((hdr->flags & MAIL_INDEX_HDR_FLAG_HAVE_DIRTY) != 0 &&
+ (flags & MAIL_INDEX_SYNC_FLAG_FLUSH_DIRTY) != 0 &&
+ (index->flags & MAIL_INDEX_OPEN_FLAG_NO_DIRTY) == 0)
+ return TRUE;
+
+ if (log_file_seq == (uint32_t)-1) {
+ /* we want to sync up to transaction log's head */
+ mail_transaction_log_get_head(index->log,
+ &log_file_seq, &log_file_offset);
+ }
+ if ((hdr->log_file_tail_offset < log_file_offset &&
+ hdr->log_file_seq == log_file_seq) ||
+ hdr->log_file_seq < log_file_seq)
+ return TRUE;
+
+ if (index->need_recreate != NULL)
+ return TRUE;
+
+ /* already synced */
+ const char *reason;
+ return mail_cache_need_purge(index->cache, &reason);
+}
+
+static int
+mail_index_sync_set_log_view(struct mail_index_view *view,
+ uint32_t start_file_seq, uoff_t start_file_offset)
+{
+ uint32_t log_seq;
+ uoff_t log_offset;
+ const char *reason;
+ bool reset;
+ int ret;
+
+ mail_transaction_log_get_head(view->index->log, &log_seq, &log_offset);
+
+ ret = mail_transaction_log_view_set(view->log_view,
+ start_file_seq, start_file_offset,
+ log_seq, log_offset, &reset, &reason);
+ if (ret < 0)
+ return -1;
+ if (ret == 0) {
+ /* either corrupted or the file was deleted for
+ some reason. either way, we can't go forward */
+ mail_index_set_error(view->index,
+ "Unexpected transaction log desync with index %s: %s",
+ view->index->filepath, reason);
+ return 0;
+ }
+ return 1;
+}
+
+int mail_index_sync_begin(struct mail_index *index,
+ struct mail_index_sync_ctx **ctx_r,
+ struct mail_index_view **view_r,
+ struct mail_index_transaction **trans_r,
+ enum mail_index_sync_flags flags)
+{
+ int ret;
+
+ ret = mail_index_sync_begin_to(index, ctx_r, view_r, trans_r,
+ (uint32_t)-1, UOFF_T_MAX, flags);
+ i_assert(ret != 0 ||
+ (flags & MAIL_INDEX_SYNC_FLAG_REQUIRE_CHANGES) != 0);
+ return ret;
+}
+
+static int
+mail_index_sync_begin_init(struct mail_index *index,
+ enum mail_index_sync_flags flags,
+ uint32_t log_file_seq, uoff_t log_file_offset)
+{
+ const struct mail_index_header *hdr;
+ uint32_t seq;
+ uoff_t offset;
+ bool locked = FALSE;
+ int ret;
+
+ /* if we require changes, don't lock transaction log yet. first check
+ if there's anything to sync. */
+ if ((flags & MAIL_INDEX_SYNC_FLAG_REQUIRE_CHANGES) == 0) {
+ if (mail_transaction_log_sync_lock(index->log, "syncing",
+ &seq, &offset) < 0)
+ return -1;
+ locked = TRUE;
+ }
+
+ /* The view must contain what we expect the mailbox to look like
+ currently. That allows the backend to update external flag
+ changes (etc.) if the view doesn't match the mailbox.
+
+ We'll update the view to contain everything that exist in the
+ transaction log except for expunges. They're synced in
+ mail_index_sync_commit(). */
+ if ((ret = mail_index_map(index, MAIL_INDEX_SYNC_HANDLER_HEAD)) <= 0) {
+ if (ret == 0) {
+ if (locked)
+ mail_transaction_log_sync_unlock(index->log, "sync init failure");
+ return -1;
+ }
+
+ /* let's try again */
+ if (mail_index_map(index, MAIL_INDEX_SYNC_HANDLER_HEAD) <= 0) {
+ if (locked)
+ mail_transaction_log_sync_unlock(index->log, "sync init failure");
+ return -1;
+ }
+ }
+
+ if (!mail_index_need_sync(index, flags, log_file_seq, log_file_offset) &&
+ !index->index_deleted && index->need_recreate == NULL) {
+ if (locked)
+ mail_transaction_log_sync_unlock(index->log, "syncing determined unnecessary");
+ return 0;
+ }
+
+ if (!locked) {
+ /* it looks like we have something to sync. lock the file and
+ check again. */
+ flags &= ENUM_NEGATE(MAIL_INDEX_SYNC_FLAG_REQUIRE_CHANGES);
+ return mail_index_sync_begin_init(index, flags, log_file_seq,
+ log_file_offset);
+ }
+
+ if (index->index_deleted &&
+ (flags & MAIL_INDEX_SYNC_FLAG_DELETING_INDEX) == 0) {
+ /* index is already deleted. we can't sync. */
+ if (locked)
+ mail_transaction_log_sync_unlock(index->log, "syncing detected deleted index");
+ return -1;
+ }
+
+ hdr = &index->map->hdr;
+ if (hdr->log_file_tail_offset > hdr->log_file_head_offset ||
+ hdr->log_file_seq > seq ||
+ (hdr->log_file_seq == seq && hdr->log_file_tail_offset > offset)) {
+ /* broken sync positions. fix them. */
+ mail_index_set_error(index,
+ "broken sync positions in index file %s",
+ index->filepath);
+ mail_index_fsck_locked(index);
+ }
+ return 1;
+}
+
+static int
+mail_index_sync_begin_to2(struct mail_index *index,
+ struct mail_index_sync_ctx **ctx_r,
+ struct mail_index_view **view_r,
+ struct mail_index_transaction **trans_r,
+ uint32_t log_file_seq, uoff_t log_file_offset,
+ enum mail_index_sync_flags flags, bool *retry_r)
+{
+ const struct mail_index_header *hdr;
+ struct mail_index_sync_ctx *ctx;
+ struct mail_index_view *sync_view;
+ enum mail_index_transaction_flags trans_flags;
+ int ret;
+
+ i_assert(!index->syncing);
+
+ *retry_r = FALSE;
+
+ if (index->map != NULL &&
+ (index->map->hdr.flags & MAIL_INDEX_HDR_FLAG_CORRUPTED) != 0) {
+ /* index is corrupted and need to be reopened */
+ return -1;
+ }
+
+ if (log_file_seq != (uint32_t)-1)
+ flags |= MAIL_INDEX_SYNC_FLAG_REQUIRE_CHANGES;
+
+ ret = mail_index_sync_begin_init(index, flags, log_file_seq,
+ log_file_offset);
+ if (ret <= 0)
+ return ret;
+
+ hdr = &index->map->hdr;
+
+ ctx = i_new(struct mail_index_sync_ctx, 1);
+ ctx->index = index;
+ ctx->flags = flags;
+
+ ctx->view = mail_index_view_open(index);
+
+ sync_view = mail_index_dummy_view_open(index);
+ ctx->sync_trans = mail_index_transaction_begin(sync_view,
+ MAIL_INDEX_TRANSACTION_FLAG_EXTERNAL);
+ mail_index_view_close(&sync_view);
+
+ /* set before any rollbacks are called */
+ index->syncing = TRUE;
+
+ /* we wish to see all the changes from last mailbox sync position to
+ the end of the transaction log */
+ ret = mail_index_sync_set_log_view(ctx->view, hdr->log_file_seq,
+ hdr->log_file_tail_offset);
+ if (ret < 0) {
+ mail_index_sync_rollback(&ctx);
+ return -1;
+ }
+ if (ret == 0) {
+ /* if a log file is missing, there's nothing we can do except
+ to skip over it. fix the problem with fsck and try again. */
+ mail_index_fsck_locked(index);
+ mail_index_sync_rollback(&ctx);
+ *retry_r = TRUE;
+ return 0;
+ }
+
+ /* we need to have all the transactions sorted to optimize
+ caller's mailbox access patterns */
+ if (mail_index_sync_read_and_sort(ctx) < 0) {
+ mail_index_sync_rollback(&ctx);
+ return -1;
+ }
+
+ /* create the transaction after the view has been updated with
+ external transactions and marked as sync view */
+ trans_flags = MAIL_INDEX_TRANSACTION_FLAG_EXTERNAL;
+ if ((ctx->flags & MAIL_INDEX_SYNC_FLAG_AVOID_FLAG_UPDATES) != 0)
+ trans_flags |= MAIL_INDEX_TRANSACTION_FLAG_AVOID_FLAG_UPDATES;
+ if ((ctx->flags & MAIL_INDEX_SYNC_FLAG_FSYNC) != 0)
+ trans_flags |= MAIL_INDEX_TRANSACTION_FLAG_FSYNC;
+ ctx->ext_trans = mail_index_transaction_begin(ctx->view, trans_flags);
+ ctx->ext_trans->sync_transaction = TRUE;
+ ctx->ext_trans->commit_deleted_index =
+ (flags & (MAIL_INDEX_SYNC_FLAG_DELETING_INDEX |
+ MAIL_INDEX_SYNC_FLAG_TRY_DELETING_INDEX)) != 0;
+
+ *ctx_r = ctx;
+ *view_r = ctx->view;
+ *trans_r = ctx->ext_trans;
+ return 1;
+}
+
+int mail_index_sync_begin_to(struct mail_index *index,
+ struct mail_index_sync_ctx **ctx_r,
+ struct mail_index_view **view_r,
+ struct mail_index_transaction **trans_r,
+ uint32_t log_file_seq, uoff_t log_file_offset,
+ enum mail_index_sync_flags flags)
+{
+ bool retry;
+ int ret;
+
+ i_assert(index->open_count > 0);
+
+ ret = mail_index_sync_begin_to2(index, ctx_r, view_r, trans_r,
+ log_file_seq, log_file_offset,
+ flags, &retry);
+ if (retry) {
+ ret = mail_index_sync_begin_to2(index, ctx_r, view_r, trans_r,
+ log_file_seq, log_file_offset,
+ flags, &retry);
+ }
+ return ret;
+}
+
+bool mail_index_sync_has_expunges(struct mail_index_sync_ctx *ctx)
+{
+ return array_is_created(&ctx->sync_trans->expunges) &&
+ array_count(&ctx->sync_trans->expunges) > 0;
+}
+
+static bool mail_index_sync_view_have_any(struct mail_index_view *view,
+ enum mail_index_sync_flags flags,
+ bool expunges_only)
+{
+ const struct mail_transaction_header *hdr;
+ const void *data;
+ uint32_t log_seq;
+ uoff_t log_offset;
+ const char *reason;
+ bool reset;
+ int ret;
+
+ if (view->map->hdr.first_recent_uid < view->map->hdr.next_uid &&
+ (flags & MAIL_INDEX_SYNC_FLAG_DROP_RECENT) != 0)
+ return TRUE;
+
+ if ((view->map->hdr.flags & MAIL_INDEX_HDR_FLAG_HAVE_DIRTY) != 0 &&
+ (flags & MAIL_INDEX_SYNC_FLAG_FLUSH_DIRTY) != 0 &&
+ (view->index->flags & MAIL_INDEX_OPEN_FLAG_NO_DIRTY) == 0)
+ return TRUE;
+
+ mail_transaction_log_get_head(view->index->log, &log_seq, &log_offset);
+ if (mail_transaction_log_view_set(view->log_view,
+ view->map->hdr.log_file_seq,
+ view->map->hdr.log_file_tail_offset,
+ log_seq, log_offset,
+ &reset, &reason) <= 0) {
+ /* let the actual syncing handle the error */
+ return TRUE;
+ }
+
+ while ((ret = mail_transaction_log_view_next(view->log_view,
+ &hdr, &data)) > 0) {
+ if ((hdr->type & MAIL_TRANSACTION_EXTERNAL) != 0)
+ continue;
+
+ switch (hdr->type & MAIL_TRANSACTION_TYPE_MASK) {
+ case MAIL_TRANSACTION_EXPUNGE:
+ case MAIL_TRANSACTION_EXPUNGE_GUID:
+ return TRUE;
+ case MAIL_TRANSACTION_EXT_REC_UPDATE:
+ case MAIL_TRANSACTION_EXT_ATOMIC_INC:
+ /* extension record updates aren't exactly needed
+ to be synced, but cache syncing relies on tail
+ offsets being updated. */
+ case MAIL_TRANSACTION_FLAG_UPDATE:
+ case MAIL_TRANSACTION_KEYWORD_UPDATE:
+ case MAIL_TRANSACTION_KEYWORD_RESET:
+ case MAIL_TRANSACTION_INDEX_DELETED:
+ case MAIL_TRANSACTION_INDEX_UNDELETED:
+ if (!expunges_only)
+ return TRUE;
+ break;
+ default:
+ break;
+ }
+ }
+ return ret < 0;
+}
+
+bool mail_index_sync_have_any(struct mail_index *index,
+ enum mail_index_sync_flags flags)
+{
+ struct mail_index_view *view;
+ bool ret;
+
+ view = mail_index_view_open(index);
+ ret = mail_index_sync_view_have_any(view, flags, FALSE);
+ mail_index_view_close(&view);
+ return ret;
+}
+
+bool mail_index_sync_have_any_expunges(struct mail_index *index)
+{
+ struct mail_index_view *view;
+ bool ret;
+
+ view = mail_index_view_open(index);
+ ret = mail_index_sync_view_have_any(view, 0, TRUE);
+ mail_index_view_close(&view);
+ return ret;
+}
+
+void mail_index_sync_get_offsets(struct mail_index_sync_ctx *ctx,
+ uint32_t *seq1_r, uoff_t *offset1_r,
+ uint32_t *seq2_r, uoff_t *offset2_r)
+{
+ *seq1_r = ctx->view->map->hdr.log_file_seq;
+ *offset1_r = ctx->view->map->hdr.log_file_tail_offset != 0 ?
+ ctx->view->map->hdr.log_file_tail_offset :
+ ctx->view->index->log->head->hdr.hdr_size;
+ mail_transaction_log_get_head(ctx->view->index->log, seq2_r, offset2_r);
+}
+
+static void
+mail_index_sync_get_expunge(struct mail_index_sync_rec *rec,
+ const struct mail_transaction_expunge_guid *exp)
+{
+ rec->type = MAIL_INDEX_SYNC_TYPE_EXPUNGE;
+ rec->uid1 = exp->uid;
+ rec->uid2 = exp->uid;
+ memcpy(rec->guid_128, exp->guid_128, sizeof(rec->guid_128));
+}
+
+static void
+mail_index_sync_get_update(struct mail_index_sync_rec *rec,
+ const struct mail_index_flag_update *update)
+{
+ rec->type = MAIL_INDEX_SYNC_TYPE_FLAGS;
+ rec->uid1 = update->uid1;
+ rec->uid2 = update->uid2;
+
+ rec->add_flags = update->add_flags;
+ rec->remove_flags = update->remove_flags;
+}
+
+static void
+mail_index_sync_get_keyword_update(struct mail_index_sync_rec *rec,
+ const struct uid_range *range,
+ struct mail_index_sync_list *sync_list)
+{
+ rec->type = !sync_list->keyword_remove ?
+ MAIL_INDEX_SYNC_TYPE_KEYWORD_ADD :
+ MAIL_INDEX_SYNC_TYPE_KEYWORD_REMOVE;
+ rec->uid1 = range->uid1;
+ rec->uid2 = range->uid2;
+ rec->keyword_idx = sync_list->keyword_idx;
+}
+
+bool mail_index_sync_next(struct mail_index_sync_ctx *ctx,
+ struct mail_index_sync_rec *sync_rec)
+{
+ struct mail_index_transaction *sync_trans = ctx->sync_trans;
+ struct mail_index_sync_list *sync_list;
+ const struct uid_range *uid_range = NULL;
+ unsigned int i, count, next_i;
+ uint32_t next_found_uid;
+
+ next_i = UINT_MAX;
+ next_found_uid = (uint32_t)-1;
+
+ /* FIXME: replace with a priority queue so we don't have to go
+ through the whole list constantly. and remember to make sure that
+ keyword resets are sent before adds! */
+ /* FIXME: pretty ugly to do this for expunges, which isn't even a
+ seq_range. */
+ sync_list = array_get_modifiable(&ctx->sync_list, &count);
+ for (i = 0; i < count; i++) {
+ if (!array_is_created(sync_list[i].array) ||
+ sync_list[i].idx == array_count(sync_list[i].array))
+ continue;
+
+ uid_range = array_idx(sync_list[i].array, sync_list[i].idx);
+ if (uid_range->uid1 == ctx->next_uid) {
+ /* use this one. */
+ break;
+ }
+ if (uid_range->uid1 < next_found_uid) {
+ next_i = i;
+ next_found_uid = uid_range->uid1;
+ }
+ }
+
+ if (i == count) {
+ if (next_i == UINT_MAX) {
+ /* nothing left in sync_list */
+ ctx->fully_synced = TRUE;
+ return FALSE;
+ }
+ ctx->next_uid = next_found_uid;
+ i = next_i;
+ uid_range = array_idx(sync_list[i].array, sync_list[i].idx);
+ }
+
+ if (sync_list[i].array == (void *)&sync_trans->expunges) {
+ mail_index_sync_get_expunge(sync_rec,
+ (const struct mail_transaction_expunge_guid *)uid_range);
+ } else if (sync_list[i].array == (void *)&sync_trans->updates) {
+ mail_index_sync_get_update(sync_rec,
+ (const struct mail_index_flag_update *)uid_range);
+ } else {
+ mail_index_sync_get_keyword_update(sync_rec, uid_range,
+ &sync_list[i]);
+ }
+ sync_list[i].idx++;
+ return TRUE;
+}
+
+bool mail_index_sync_have_more(struct mail_index_sync_ctx *ctx)
+{
+ const struct mail_index_sync_list *sync_list;
+
+ array_foreach(&ctx->sync_list, sync_list) {
+ if (array_is_created(sync_list->array) &&
+ sync_list->idx != array_count(sync_list->array))
+ return TRUE;
+ }
+ return FALSE;
+}
+
+void mail_index_sync_set_commit_result(struct mail_index_sync_ctx *ctx,
+ struct mail_index_transaction_commit_result *result)
+{
+ ctx->sync_commit_result = result;
+}
+
+void mail_index_sync_reset(struct mail_index_sync_ctx *ctx)
+{
+ struct mail_index_sync_list *sync_list;
+
+ ctx->next_uid = 0;
+ array_foreach_modifiable(&ctx->sync_list, sync_list)
+ sync_list->idx = 0;
+}
+
+void mail_index_sync_no_warning(struct mail_index_sync_ctx *ctx)
+{
+ ctx->no_warning = TRUE;
+}
+
+void mail_index_sync_set_reason(struct mail_index_sync_ctx *ctx,
+ const char *reason)
+{
+ i_free(ctx->reason);
+ ctx->reason = i_strdup(reason);
+}
+
+static void mail_index_sync_end(struct mail_index_sync_ctx **_ctx)
+{
+ struct mail_index_sync_ctx *ctx = *_ctx;
+ const char *lock_reason;
+
+ i_assert(ctx->index->syncing);
+
+ *_ctx = NULL;
+
+ ctx->index->syncing = FALSE;
+ if (ctx->no_warning)
+ lock_reason = NULL;
+ else if (ctx->reason != NULL)
+ lock_reason = ctx->reason;
+ else
+ lock_reason = "Mailbox was synchronized";
+ mail_transaction_log_sync_unlock(ctx->index->log, lock_reason);
+
+ mail_index_view_close(&ctx->view);
+ mail_index_transaction_rollback(&ctx->sync_trans);
+ if (array_is_created(&ctx->sync_list))
+ array_free(&ctx->sync_list);
+ i_free(ctx->reason);
+ i_free(ctx);
+}
+
+static void
+mail_index_sync_update_mailbox_offset(struct mail_index_sync_ctx *ctx)
+{
+ const struct mail_index_header *hdr = &ctx->index->map->hdr;
+ uint32_t seq;
+ uoff_t offset;
+
+ if (!ctx->fully_synced) {
+ /* Everything wasn't synced. This usually means that syncing
+ was used for locking and nothing was synced. Don't update
+ tail offset. */
+ return;
+ }
+ /* All changes were synced. During the syncing other transactions may
+ have been created and committed as well. They're expected to be
+ external transactions. These could be at least:
+ - mdbox finishing expunges
+ - mdbox writing to dovecot.map.index (requires tail offset updates)
+ - sdbox appending messages
+
+ If any expunges were committed, tail_offset must not be updated
+ before mail_index_map(MAIL_INDEX_SYNC_HANDLER_FILE) is called.
+ Otherwise expunge handlers won't be called for them.
+
+ We'll require MAIL_INDEX_SYNC_FLAG_UPDATE_TAIL_OFFSET flag for the
+ few places that actually require tail_offset to include the
+ externally committed transactions. Otherwise tail_offset is updated
+ only up to what was just synced. */
+ if ((ctx->flags & MAIL_INDEX_SYNC_FLAG_UPDATE_TAIL_OFFSET) != 0)
+ mail_transaction_log_get_head(ctx->index->log, &seq, &offset);
+ else {
+ mail_transaction_log_view_get_prev_pos(ctx->view->log_view,
+ &seq, &offset);
+ }
+ mail_transaction_log_set_mailbox_sync_pos(ctx->index->log, seq, offset);
+
+ /* If tail offset has changed, make sure it gets written to
+ transaction log. do this only if we're required to make changes.
+
+ avoid writing a new tail offset if all the transactions were
+ external, because that wouldn't change effective the tail offset.
+ except e.g. mdbox map requires this to happen, so do it
+ optionally. Also update the tail if we've been calling any expunge
+ handlers, so they won't be called multiple times. That could cause
+ at least cache file's [deleted_]record_count to shrink too much. */
+ if ((hdr->log_file_seq != seq || hdr->log_file_tail_offset < offset) &&
+ (ctx->seen_external_expunges ||
+ ctx->seen_nonexternal_transactions ||
+ (ctx->flags & MAIL_INDEX_SYNC_FLAG_UPDATE_TAIL_OFFSET) != 0)) {
+ ctx->ext_trans->log_updates = TRUE;
+ ctx->ext_trans->tail_offset_changed = TRUE;
+ }
+}
+
+static bool mail_index_sync_want_index_write(struct mail_index *index, const char **reason_r)
+{
+ uint32_t log_diff;
+
+ if (index->main_index_hdr_log_file_seq != 0 &&
+ index->main_index_hdr_log_file_seq != index->map->hdr.log_file_seq) {
+ /* dovecot.index points to an old .log file. we were supposed
+ to rewrite the dovecot.index when rotating the log, so
+ we shouldn't usually get here. */
+ *reason_r = "points to old .log file";
+ return TRUE;
+ }
+
+ log_diff = index->map->hdr.log_file_tail_offset -
+ index->main_index_hdr_log_file_tail_offset;
+ if (log_diff > index->optimization_set.index.rewrite_max_log_bytes) {
+ *reason_r = t_strdup_printf(
+ ".log read %u..%u > rewrite_max_log_bytes %"PRIuUOFF_T,
+ index->map->hdr.log_file_tail_offset,
+ index->main_index_hdr_log_file_tail_offset,
+ index->optimization_set.index.rewrite_max_log_bytes);
+ return TRUE;
+ }
+ if (index->index_min_write &&
+ log_diff > index->optimization_set.index.rewrite_min_log_bytes) {
+ *reason_r = t_strdup_printf(
+ ".log read %u..%u > rewrite_min_log_bytes %"PRIuUOFF_T,
+ index->map->hdr.log_file_tail_offset,
+ index->main_index_hdr_log_file_tail_offset,
+ index->optimization_set.index.rewrite_min_log_bytes);
+ return TRUE;
+ }
+
+ if (index->need_recreate != NULL) {
+ *reason_r = t_strdup_printf("Need to recreate index: %s",
+ index->need_recreate);
+ return TRUE;
+ }
+ return FALSE;
+}
+
+int mail_index_sync_commit(struct mail_index_sync_ctx **_ctx)
+{
+ struct mail_index_sync_ctx *ctx = *_ctx;
+ struct mail_index *index = ctx->index;
+ const char *reason = NULL;
+ uint32_t next_uid;
+ bool want_rotate, index_undeleted, delete_index;
+ int ret = 0, ret2;
+
+ index_undeleted = ctx->ext_trans->index_undeleted;
+ delete_index = index->index_delete_requested && !index_undeleted &&
+ (ctx->flags & (MAIL_INDEX_SYNC_FLAG_DELETING_INDEX |
+ MAIL_INDEX_SYNC_FLAG_TRY_DELETING_INDEX)) != 0;
+ if (delete_index) {
+ /* finish this sync by marking the index deleted */
+ mail_index_set_deleted(ctx->ext_trans);
+ } else if (index->index_deleted && !index_undeleted &&
+ (ctx->flags & MAIL_INDEX_SYNC_FLAG_TRY_DELETING_INDEX) == 0) {
+ /* another process just marked the index deleted.
+ finish the sync, but return error. */
+ mail_index_set_error_nolog(index, "Index is marked deleted");
+ ret = -1;
+ }
+
+ mail_index_sync_update_mailbox_offset(ctx);
+
+ if ((ctx->flags & MAIL_INDEX_SYNC_FLAG_DROP_RECENT) != 0) {
+ next_uid = mail_index_transaction_get_next_uid(ctx->ext_trans);
+ if (index->map->hdr.first_recent_uid < next_uid) {
+ mail_index_update_header(ctx->ext_trans,
+ offsetof(struct mail_index_header,
+ first_recent_uid),
+ &next_uid, sizeof(next_uid), FALSE);
+ }
+ }
+ if (index->hdr_log2_rotate_time_delayed_update != 0) {
+ /* We checked whether .log.2 should be deleted in this same
+ sync. It resulted in wanting to change the log2_rotate_time
+ in the header. Do it here as part of the other changes. */
+ uint32_t log2_rotate_time =
+ index->hdr_log2_rotate_time_delayed_update;
+
+ mail_index_update_header(ctx->ext_trans,
+ offsetof(struct mail_index_header, log2_rotate_time),
+ &log2_rotate_time, sizeof(log2_rotate_time), TRUE);
+ index->hdr_log2_rotate_time_delayed_update = 0;
+ }
+
+ ret2 = mail_index_transaction_commit(&ctx->ext_trans);
+ if (ret2 < 0) {
+ mail_index_sync_end(&ctx);
+ return -1;
+ }
+
+ if (delete_index)
+ index->index_deleted = TRUE;
+ else if (index_undeleted) {
+ index->index_deleted = FALSE;
+ index->index_delete_requested = FALSE;
+ }
+
+ /* refresh the mapping with newly committed external transactions
+ and the synced expunges. sync using file handler here so that the
+ expunge handlers get called. */
+ index->sync_commit_result = ctx->sync_commit_result;
+ if (mail_index_map(ctx->index, MAIL_INDEX_SYNC_HANDLER_FILE) <= 0)
+ ret = -1;
+ index->sync_commit_result = NULL;
+
+ /* The previously called expunged handlers will update cache's
+ record_count and deleted_record_count. That also has a side effect
+ of updating whether cache needs to be purged. */
+ if (ret == 0 && mail_cache_need_purge(index->cache, &reason) &&
+ !mail_cache_transactions_have_changes(index->cache)) {
+ if (mail_cache_purge(index->cache,
+ index->cache->need_purge_file_seq,
+ reason) < 0) {
+ /* can't really do anything if it fails */
+ }
+ /* Make sure the newly committed cache record offsets are
+ updated to the current index. This is important if the
+ dovecot.index gets recreated below, because rotation of
+ dovecot.index.log also re-maps the index to make sure
+ everything is up-to-date. But if it wasn't,
+ mail_index_write() will just assert-crash because
+ log_file_head_offset changed. */
+ if (mail_index_map(ctx->index, MAIL_INDEX_SYNC_HANDLER_FILE) <= 0)
+ ret = -1;
+ }
+
+ /* Log rotation is allowed only if everything was synced. Note that
+ tail_offset might not equal head_offset here, because
+ mail_index_sync_update_mailbox_offset() doesn't always update
+ tail_offset to skip over other committed external transactions.
+ However, it's still safe to do the rotation because external
+ transactions don't require syncing. */
+ want_rotate = ctx->fully_synced &&
+ mail_transaction_log_want_rotate(index->log, &reason);
+ if (ret == 0 &&
+ (want_rotate || mail_index_sync_want_index_write(index, &reason))) {
+ i_free(index->need_recreate);
+ index->index_min_write = FALSE;
+ mail_index_write(index, want_rotate, reason);
+ }
+ mail_index_sync_end(_ctx);
+ return ret;
+}
+
+void mail_index_sync_rollback(struct mail_index_sync_ctx **ctx)
+{
+ if ((*ctx)->ext_trans != NULL)
+ mail_index_transaction_rollback(&(*ctx)->ext_trans);
+ mail_index_sync_end(ctx);
+}
+
+void mail_index_sync_flags_apply(const struct mail_index_sync_rec *sync_rec,
+ uint8_t *flags)
+{
+ i_assert(sync_rec->type == MAIL_INDEX_SYNC_TYPE_FLAGS);
+
+ *flags = (*flags & ENUM_NEGATE(sync_rec->remove_flags)) | sync_rec->add_flags;
+}
+
+bool mail_index_sync_keywords_apply(const struct mail_index_sync_rec *sync_rec,
+ ARRAY_TYPE(keyword_indexes) *keywords)
+{
+ const unsigned int *keyword_indexes;
+ unsigned int idx = sync_rec->keyword_idx;
+ unsigned int i, count;
+
+ keyword_indexes = array_get(keywords, &count);
+ switch (sync_rec->type) {
+ case MAIL_INDEX_SYNC_TYPE_KEYWORD_ADD:
+ for (i = 0; i < count; i++) {
+ if (keyword_indexes[i] == idx)
+ return FALSE;
+ }
+
+ array_push_back(keywords, &idx);
+ return TRUE;
+ case MAIL_INDEX_SYNC_TYPE_KEYWORD_REMOVE:
+ for (i = 0; i < count; i++) {
+ if (keyword_indexes[i] == idx) {
+ array_delete(keywords, i, 1);
+ return TRUE;
+ }
+ }
+ return FALSE;
+ default:
+ i_unreached();
+ return FALSE;
+ }
+}
+
+void mail_index_sync_set_corrupted(struct mail_index_sync_map_ctx *ctx,
+ const char *fmt, ...)
+{
+ va_list va;
+ uint32_t seq;
+ uoff_t offset;
+ char *reason, *reason_free = NULL;
+
+ va_start(va, fmt);
+ reason = reason_free = i_strdup_vprintf(fmt, va);
+ va_end(va);
+
+ ctx->errors = TRUE;
+ /* make sure we don't get to this same error again by updating the
+ dovecot.index */
+ if (ctx->view->index->need_recreate == NULL) {
+ ctx->view->index->need_recreate = reason;
+ reason_free = NULL;
+ }
+
+ mail_transaction_log_view_get_prev_pos(ctx->view->log_view,
+ &seq, &offset);
+
+ if (seq < ctx->view->index->fsck_log_head_file_seq ||
+ (seq == ctx->view->index->fsck_log_head_file_seq &&
+ offset < ctx->view->index->fsck_log_head_file_offset)) {
+ /* be silent */
+ } else {
+ mail_index_set_error(ctx->view->index,
+ "Log synchronization error at "
+ "seq=%u,offset=%"PRIuUOFF_T" for %s: %s",
+ seq, offset, ctx->view->index->filepath,
+ reason);
+ }
+ i_free(reason_free);
+}
diff --git a/src/lib-index/mail-index-transaction-export.c b/src/lib-index/mail-index-transaction-export.c
new file mode 100644
index 0000000..2aced67
--- /dev/null
+++ b/src/lib-index/mail-index-transaction-export.c
@@ -0,0 +1,533 @@
+/* Copyright (c) 2003-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "mail-index-private.h"
+#include "mail-index-modseq.h"
+#include "mail-transaction-log-private.h"
+#include "mail-index-transaction-private.h"
+
+struct mail_index_export_context {
+ struct mail_index_transaction *trans;
+ struct mail_transaction_log_append_ctx *append_ctx;
+};
+
+static void
+log_append_buffer(struct mail_index_export_context *ctx,
+ const buffer_t *buf, enum mail_transaction_type type)
+{
+ mail_transaction_log_append_add(ctx->append_ctx, type,
+ buf->data, buf->used);
+}
+
+static void log_append_flag_updates(struct mail_index_export_context *ctx,
+ struct mail_index_transaction *t)
+{
+ ARRAY(struct mail_transaction_flag_update) log_updates;
+ const struct mail_index_flag_update *updates;
+ struct mail_transaction_flag_update *log_update;
+ unsigned int i, count;
+
+ updates = array_get(&t->updates, &count);
+ if (count == 0)
+ return;
+
+ i_array_init(&log_updates, count);
+
+ for (i = 0; i < count; i++) {
+ log_update = array_append_space(&log_updates);
+ log_update->uid1 = updates[i].uid1;
+ log_update->uid2 = updates[i].uid2;
+ log_update->add_flags = updates[i].add_flags & 0xff;
+ log_update->remove_flags = updates[i].remove_flags & 0xff;
+ if ((updates[i].add_flags & MAIL_INDEX_MAIL_FLAG_UPDATE_MODSEQ) != 0)
+ log_update->modseq_inc_flag = 1;
+ }
+ log_append_buffer(ctx, log_updates.arr.buffer,
+ MAIL_TRANSACTION_FLAG_UPDATE);
+ array_free(&log_updates);
+}
+
+static const buffer_t *
+log_get_hdr_update_buffer(struct mail_index_transaction *t, bool prepend)
+{
+ buffer_t *buf;
+ const unsigned char *data, *mask;
+ struct mail_transaction_header_update u;
+ uint16_t offset;
+ int state = 0;
+
+ i_zero(&u);
+
+ data = prepend ? t->pre_hdr_change : t->post_hdr_change;
+ mask = prepend ? t->pre_hdr_mask : t->post_hdr_mask;
+
+ buf = t_buffer_create(256);
+ for (offset = 0; offset <= sizeof(t->pre_hdr_change); offset++) {
+ if (offset < sizeof(t->pre_hdr_change) && mask[offset] != 0) {
+ if (state == 0) {
+ u.offset = offset;
+ state++;
+ }
+ } else {
+ if (state > 0) {
+ u.size = offset - u.offset;
+ buffer_append(buf, &u, sizeof(u));
+ buffer_append(buf, data + u.offset, u.size);
+ state = 0;
+ }
+ }
+ }
+ return buf;
+}
+
+static unsigned int
+ext_hdr_update_get_size(const struct mail_index_transaction_ext_hdr_update *hu)
+{
+ unsigned int i;
+
+ for (i = hu->alloc_size; i > 0; i--) {
+ if (hu->mask[i-1] != 0)
+ return i;
+ }
+ return 0;
+}
+
+static void log_append_ext_intro(struct mail_index_export_context *ctx,
+ uint32_t ext_id, uint32_t reset_id,
+ unsigned int *hdr_size_r)
+{
+ struct mail_index_transaction *t = ctx->trans;
+ const struct mail_index_registered_ext *rext;
+ const struct mail_index_ext *ext;
+ struct mail_transaction_ext_intro *intro, *resizes;
+ buffer_t *buf;
+ uint32_t idx;
+ unsigned int count;
+
+ i_assert(ext_id != (uint32_t)-1);
+
+ if (t->reset ||
+ !mail_index_map_get_ext_idx(t->view->index->map, ext_id, &idx)) {
+ /* new extension */
+ idx = (uint32_t)-1;
+ }
+
+ rext = array_idx(&t->view->index->extensions, ext_id);
+ if (!array_is_created(&t->ext_resizes)) {
+ resizes = NULL;
+ count = 0;
+ } else {
+ resizes = array_get_modifiable(&t->ext_resizes, &count);
+ }
+
+ buf = t_buffer_create(128);
+ if (ext_id < count && resizes[ext_id].name_size != 0) {
+ /* we're resizing the extension. use the resize struct. */
+ intro = &resizes[ext_id];
+
+ if (idx != (uint32_t)-1) {
+ intro->ext_id = idx;
+ intro->name_size = 0;
+ } else {
+ intro->ext_id = (uint32_t)-1;
+ intro->name_size = strlen(rext->name);
+ }
+ buffer_append(buf, intro, sizeof(*intro));
+ } else {
+ /* generate a new intro structure */
+ intro = buffer_append_space_unsafe(buf, sizeof(*intro));
+ intro->ext_id = idx;
+ intro->record_size = rext->record_size;
+ intro->record_align = rext->record_align;
+ if (idx == (uint32_t)-1) {
+ intro->hdr_size = rext->hdr_size;
+ intro->name_size = strlen(rext->name);
+ } else {
+ ext = array_idx(&t->view->index->map->extensions, idx);
+ intro->hdr_size = ext->hdr_size;
+ intro->name_size = 0;
+ }
+ intro->flags = MAIL_TRANSACTION_EXT_INTRO_FLAG_NO_SHRINK;
+
+ /* handle increasing header size automatically */
+ if (array_is_created(&t->ext_hdr_updates) &&
+ ext_id < array_count(&t->ext_hdr_updates)) {
+ const struct mail_index_transaction_ext_hdr_update *hu;
+ unsigned int hdr_update_size;
+
+ hu = array_idx(&t->ext_hdr_updates, ext_id);
+ hdr_update_size = ext_hdr_update_get_size(hu);
+ if (intro->hdr_size < hdr_update_size)
+ intro->hdr_size = hdr_update_size;
+ }
+ }
+ i_assert(intro->record_size != 0 || intro->hdr_size != 0);
+ if (reset_id != 0) {
+ /* we're going to reset this extension in this transaction */
+ intro->reset_id = reset_id;
+ } else if (idx != (uint32_t)-1) {
+ /* use the existing reset_id */
+ const struct mail_index_ext *map_ext =
+ array_idx(&t->view->index->map->extensions, idx);
+ intro->reset_id = map_ext->reset_id;
+ } else {
+ /* new extension, reset_id defaults to 0 */
+ }
+ buffer_append(buf, rext->name, intro->name_size);
+ if ((buf->used % 4) != 0)
+ buffer_append_zero(buf, 4 - (buf->used % 4));
+
+ if (ctx->append_ctx->new_highest_modseq == 0 &&
+ strcmp(rext->name, MAIL_INDEX_MODSEQ_EXT_NAME) == 0) {
+ /* modseq tracking started */
+ ctx->append_ctx->new_highest_modseq = 1;
+ }
+
+ log_append_buffer(ctx, buf, MAIL_TRANSACTION_EXT_INTRO);
+ *hdr_size_r = intro->hdr_size;
+}
+
+static void
+log_append_ext_hdr_update(struct mail_index_export_context *ctx,
+ const struct mail_index_transaction_ext_hdr_update *hdr,
+ unsigned int ext_hdr_size)
+{
+ buffer_t *buf;
+ const unsigned char *data, *mask;
+ struct mail_transaction_ext_hdr_update u;
+ struct mail_transaction_ext_hdr_update32 u32;
+ size_t offset;
+ bool started = FALSE, use_32 = hdr->alloc_size >= 65536;
+
+ i_zero(&u);
+ i_zero(&u32);
+
+ data = hdr->data;
+ mask = hdr->mask;
+
+ buf = buffer_create_dynamic(default_pool, 256);
+ for (offset = 0; offset <= hdr->alloc_size; offset++) {
+ if (offset < hdr->alloc_size && mask[offset] != 0) {
+ if (!started) {
+ u32.offset = offset;
+ started = TRUE;
+ }
+ } else {
+ if (started) {
+ u32.size = offset - u32.offset;
+ if (use_32)
+ buffer_append(buf, &u32, sizeof(u32));
+ else {
+ u.offset = u32.offset;
+ u.size = u32.size;
+ buffer_append(buf, &u, sizeof(u));
+ }
+ i_assert(u32.offset + u32.size <= ext_hdr_size);
+ buffer_append(buf, data + u32.offset, u32.size);
+ started = FALSE;
+ }
+ }
+ }
+ if (buf->used % 4 != 0)
+ buffer_append_zero(buf, 4 - buf->used % 4);
+ log_append_buffer(ctx, buf, use_32 ? MAIL_TRANSACTION_EXT_HDR_UPDATE32 :
+ MAIL_TRANSACTION_EXT_HDR_UPDATE);
+ buffer_free(&buf);
+}
+
+static void
+mail_transaction_log_append_ext_intros(struct mail_index_export_context *ctx)
+{
+ struct mail_index_transaction *t = ctx->trans;
+ const struct mail_transaction_ext_intro *resize;
+ const struct mail_index_transaction_ext_hdr_update *hdrs;
+ struct mail_transaction_ext_reset ext_reset;
+ unsigned int resize_count, ext_count = 0;
+ unsigned int hdrs_count, reset_id_count, reset_count, hdr_size;
+ uint32_t ext_id, reset_id;
+ const struct mail_transaction_ext_reset *reset;
+ const uint32_t *reset_ids;
+ buffer_t reset_buf;
+
+ if (!array_is_created(&t->ext_resizes)) {
+ resize = NULL;
+ resize_count = 0;
+ } else {
+ resize = array_get(&t->ext_resizes, &resize_count);
+ if (ext_count < resize_count)
+ ext_count = resize_count;
+ }
+
+ if (!array_is_created(&t->ext_reset_ids)) {
+ reset_ids = NULL;
+ reset_id_count = 0;
+ } else {
+ reset_ids = array_get(&t->ext_reset_ids, &reset_id_count);
+ }
+
+ if (!array_is_created(&t->ext_resets)) {
+ reset = NULL;
+ reset_count = 0;
+ } else {
+ reset = array_get(&t->ext_resets, &reset_count);
+ if (ext_count < reset_count)
+ ext_count = reset_count;
+ }
+
+ if (!array_is_created(&t->ext_hdr_updates)) {
+ hdrs = NULL;
+ hdrs_count = 0;
+ } else {
+ hdrs = array_get(&t->ext_hdr_updates, &hdrs_count);
+ if (ext_count < hdrs_count)
+ ext_count = hdrs_count;
+ }
+
+ i_zero(&ext_reset);
+ buffer_create_from_data(&reset_buf, &ext_reset, sizeof(ext_reset));
+ buffer_set_used_size(&reset_buf, sizeof(ext_reset));
+
+ for (ext_id = 0; ext_id < ext_count; ext_id++) {
+ if (ext_id < reset_count)
+ ext_reset = reset[ext_id];
+ else
+ ext_reset.new_reset_id = 0;
+ if ((ext_id < resize_count && resize[ext_id].name_size > 0) ||
+ ext_reset.new_reset_id != 0 ||
+ (ext_id < hdrs_count && hdrs[ext_id].alloc_size > 0)) {
+ if (ext_reset.new_reset_id != 0) {
+ /* we're going to reset this extension
+ immediately after the intro */
+ reset_id = 0;
+ } else {
+ reset_id = ext_id < reset_id_count ?
+ reset_ids[ext_id] : 0;
+ }
+ log_append_ext_intro(ctx, ext_id, reset_id, &hdr_size);
+ } else {
+ hdr_size = 0;
+ }
+ if (ext_reset.new_reset_id != 0) {
+ i_assert(ext_id < reset_id_count &&
+ ext_reset.new_reset_id == reset_ids[ext_id]);
+ log_append_buffer(ctx, &reset_buf,
+ MAIL_TRANSACTION_EXT_RESET);
+ }
+ if (ext_id < hdrs_count && hdrs[ext_id].alloc_size > 0) {
+ T_BEGIN {
+ log_append_ext_hdr_update(ctx, &hdrs[ext_id],
+ hdr_size);
+ } T_END;
+ }
+ }
+}
+
+static void log_append_ext_recs(struct mail_index_export_context *ctx,
+ const ARRAY_TYPE(seq_array_array) *arr,
+ enum mail_transaction_type type)
+{
+ struct mail_index_transaction *t = ctx->trans;
+ const ARRAY_TYPE(seq_array) *updates;
+ const uint32_t *reset_ids;
+ unsigned int ext_id, count, reset_id_count, hdr_size;
+ uint32_t reset_id;
+
+ if (!array_is_created(&t->ext_reset_ids)) {
+ reset_ids = NULL;
+ reset_id_count = 0;
+ } else {
+ reset_ids = array_get_modifiable(&t->ext_reset_ids,
+ &reset_id_count);
+ }
+
+ updates = array_get(arr, &count);
+ for (ext_id = 0; ext_id < count; ext_id++) {
+ if (!array_is_created(&updates[ext_id]))
+ continue;
+
+ reset_id = ext_id < reset_id_count ? reset_ids[ext_id] : 0;
+ log_append_ext_intro(ctx, ext_id, reset_id, &hdr_size);
+
+ log_append_buffer(ctx, updates[ext_id].arr.buffer, type);
+ }
+}
+
+static void
+log_append_keyword_update(struct mail_index_export_context *ctx,
+ buffer_t *tmp_buf, enum modify_type modify_type,
+ const char *keyword, const buffer_t *uid_buffer)
+{
+ struct mail_transaction_keyword_update kt_hdr;
+
+ i_assert(uid_buffer->used > 0);
+
+ i_zero(&kt_hdr);
+ kt_hdr.modify_type = modify_type;
+ kt_hdr.name_size = strlen(keyword);
+
+ buffer_set_used_size(tmp_buf, 0);
+ buffer_append(tmp_buf, &kt_hdr, sizeof(kt_hdr));
+ buffer_append(tmp_buf, keyword, kt_hdr.name_size);
+ if ((tmp_buf->used % 4) != 0)
+ buffer_append_zero(tmp_buf, 4 - (tmp_buf->used % 4));
+ buffer_append(tmp_buf, uid_buffer->data, uid_buffer->used);
+
+ log_append_buffer(ctx, tmp_buf, MAIL_TRANSACTION_KEYWORD_UPDATE);
+}
+
+static bool
+log_append_keyword_updates(struct mail_index_export_context *ctx)
+{
+ const struct mail_index_transaction_keyword_update *updates;
+ const char *const *keywords;
+ buffer_t *tmp_buf;
+ unsigned int i, count, keywords_count;
+ bool changed = FALSE;
+
+ tmp_buf = t_buffer_create(64);
+
+ keywords = array_get_modifiable(&ctx->trans->view->index->keywords,
+ &keywords_count);
+ updates = array_get_modifiable(&ctx->trans->keyword_updates, &count);
+ i_assert(count <= keywords_count);
+
+ for (i = 0; i < count; i++) {
+ if (array_is_created(&updates[i].add_seq) &&
+ array_count(&updates[i].add_seq) > 0) {
+ changed = TRUE;
+ log_append_keyword_update(ctx, tmp_buf,
+ MODIFY_ADD, keywords[i],
+ updates[i].add_seq.arr.buffer);
+ }
+ if (array_is_created(&updates[i].remove_seq) &&
+ array_count(&updates[i].remove_seq) > 0) {
+ changed = TRUE;
+ log_append_keyword_update(ctx, tmp_buf,
+ MODIFY_REMOVE, keywords[i],
+ updates[i].remove_seq.arr.buffer);
+ }
+ }
+ return changed;
+}
+
+void mail_index_transaction_export(struct mail_index_transaction *t,
+ struct mail_transaction_log_append_ctx *append_ctx,
+ enum mail_index_transaction_change *changes_r)
+{
+ static uint8_t null4[4] = { 0, 0, 0, 0 };
+ enum mail_index_fsync_mask change_mask = 0;
+ struct mail_index_export_context ctx;
+
+ *changes_r = 0;
+
+ i_zero(&ctx);
+ ctx.trans = t;
+ ctx.append_ctx = append_ctx;
+
+ if (t->index_undeleted) {
+ i_assert(!t->index_deleted);
+ mail_transaction_log_append_add(ctx.append_ctx,
+ MAIL_TRANSACTION_INDEX_UNDELETED, &null4, 4);
+ }
+
+ /* send all extension introductions and resizes before appends
+ to avoid resize overhead as much as possible */
+ mail_transaction_log_append_ext_intros(&ctx);
+
+ if (t->pre_hdr_changed) {
+ log_append_buffer(&ctx, log_get_hdr_update_buffer(t, TRUE),
+ MAIL_TRANSACTION_HEADER_UPDATE);
+ }
+
+ if (append_ctx->output->used > 0)
+ *changes_r |= MAIL_INDEX_TRANSACTION_CHANGE_OTHERS;
+
+ if (t->attribute_updates != NULL) {
+ buffer_append_c(t->attribute_updates, '\0');
+ /* need to have 32bit alignment */
+ if (t->attribute_updates->used % 4 != 0) {
+ buffer_append_zero(t->attribute_updates,
+ 4 - t->attribute_updates->used % 4);
+ }
+ /* append the timestamp and value lengths */
+ buffer_append(t->attribute_updates,
+ t->attribute_updates_suffix->data,
+ t->attribute_updates_suffix->used);
+ i_assert(t->attribute_updates->used % 4 == 0);
+ *changes_r |= MAIL_INDEX_TRANSACTION_CHANGE_ATTRIBUTE;
+ log_append_buffer(&ctx, t->attribute_updates,
+ MAIL_TRANSACTION_ATTRIBUTE_UPDATE);
+ }
+ if (array_is_created(&t->appends)) {
+ change_mask |= MAIL_INDEX_FSYNC_MASK_APPENDS;
+ *changes_r |= MAIL_INDEX_TRANSACTION_CHANGE_APPEND;
+ log_append_buffer(&ctx, t->appends.arr.buffer,
+ MAIL_TRANSACTION_APPEND);
+ }
+
+ if (array_is_created(&t->updates)) {
+ change_mask |= MAIL_INDEX_FSYNC_MASK_FLAGS;
+ *changes_r |= MAIL_INDEX_TRANSACTION_CHANGE_FLAGS;
+ log_append_flag_updates(&ctx, t);
+ }
+
+ if (array_is_created(&t->ext_rec_updates)) {
+ *changes_r |= MAIL_INDEX_TRANSACTION_CHANGE_OTHERS;
+ log_append_ext_recs(&ctx, &t->ext_rec_updates,
+ MAIL_TRANSACTION_EXT_REC_UPDATE);
+ }
+ if (array_is_created(&t->ext_rec_atomics)) {
+ *changes_r |= MAIL_INDEX_TRANSACTION_CHANGE_OTHERS;
+ log_append_ext_recs(&ctx, &t->ext_rec_atomics,
+ MAIL_TRANSACTION_EXT_ATOMIC_INC);
+ }
+
+ if (array_is_created(&t->keyword_updates)) {
+ if (log_append_keyword_updates(&ctx)) {
+ change_mask |= MAIL_INDEX_FSYNC_MASK_KEYWORDS;
+ *changes_r |= MAIL_INDEX_TRANSACTION_CHANGE_KEYWORDS;
+ }
+ }
+ /* keep modseq updates almost last */
+ if (array_is_created(&t->modseq_updates)) {
+ *changes_r |= MAIL_INDEX_TRANSACTION_CHANGE_MODSEQ;
+ log_append_buffer(&ctx, t->modseq_updates.arr.buffer,
+ MAIL_TRANSACTION_MODSEQ_UPDATE);
+ }
+
+ if (array_is_created(&t->expunges)) {
+ /* non-external expunges are only requests, ignore them when
+ checking fsync_mask */
+ if ((t->flags & MAIL_INDEX_TRANSACTION_FLAG_EXTERNAL) != 0) {
+ change_mask |= MAIL_INDEX_FSYNC_MASK_EXPUNGES;
+ *changes_r |= MAIL_INDEX_TRANSACTION_CHANGE_EXPUNGE;
+ } else {
+ *changes_r |= MAIL_INDEX_TRANSACTION_CHANGE_OTHERS;
+ }
+ log_append_buffer(&ctx, t->expunges.arr.buffer,
+ MAIL_TRANSACTION_EXPUNGE_GUID);
+ }
+
+ if (t->post_hdr_changed) {
+ *changes_r |= MAIL_INDEX_TRANSACTION_CHANGE_OTHERS;
+ log_append_buffer(&ctx, log_get_hdr_update_buffer(t, FALSE),
+ MAIL_TRANSACTION_HEADER_UPDATE);
+ }
+
+ if (t->index_deleted) {
+ i_assert(!t->index_undeleted);
+ *changes_r |= MAIL_INDEX_TRANSACTION_CHANGE_OTHERS;
+ mail_transaction_log_append_add(ctx.append_ctx,
+ MAIL_TRANSACTION_INDEX_DELETED,
+ &null4, 4);
+ }
+
+ i_assert((append_ctx->output->used > 0) == (*changes_r != 0));
+
+ append_ctx->index_sync_transaction = t->sync_transaction;
+ append_ctx->tail_offset_changed = t->tail_offset_changed;
+ append_ctx->want_fsync =
+ (t->view->index->set.fsync_mask & change_mask) != 0 ||
+ (t->flags & MAIL_INDEX_TRANSACTION_FLAG_FSYNC) != 0;
+}
diff --git a/src/lib-index/mail-index-transaction-finish.c b/src/lib-index/mail-index-transaction-finish.c
new file mode 100644
index 0000000..360e597
--- /dev/null
+++ b/src/lib-index/mail-index-transaction-finish.c
@@ -0,0 +1,350 @@
+/* Copyright (c) 2003-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "ioloop.h"
+#include "mail-index-private.h"
+#include "mail-index-modseq.h"
+#include "mail-index-transaction-private.h"
+
+int mail_transaction_expunge_guid_cmp(const struct mail_transaction_expunge_guid *e1,
+ const struct mail_transaction_expunge_guid *e2)
+{
+ if (e1->uid < e2->uid)
+ return -1;
+ else if (e1->uid > e2->uid)
+ return 1;
+ else
+ return 0;
+}
+
+void mail_index_transaction_sort_expunges(struct mail_index_transaction *t)
+{
+ if (!t->expunges_nonsorted)
+ return;
+
+ array_sort(&t->expunges, mail_transaction_expunge_guid_cmp);
+ t->expunges_nonsorted = FALSE;
+}
+
+static void
+ext_reset_update_atomic(struct mail_index_transaction *t,
+ uint32_t ext_id, uint32_t expected_reset_id)
+{
+ const struct mail_index_ext *map_ext;
+ struct mail_transaction_ext_reset *reset;
+ uint32_t idx, reset_id;
+
+ if (!mail_index_map_get_ext_idx(t->view->index->map, ext_id, &idx)) {
+ /* new extension */
+ reset_id = 1;
+ } else {
+ map_ext = array_idx(&t->view->index->map->extensions, idx);
+ reset_id = map_ext->reset_id + 1;
+ }
+ if (reset_id != expected_reset_id) {
+ /* ignore this extension update */
+ mail_index_ext_set_reset_id(t, ext_id, 0);
+ return;
+ }
+
+ if (reset_id == 0)
+ reset_id++;
+
+ array_idx_set(&t->ext_reset_ids, ext_id, &reset_id);
+
+ /* reseting existing data is optional */
+ if (array_is_created(&t->ext_resets)) {
+ reset = array_idx_modifiable(&t->ext_resets, ext_id);
+ if (reset->new_reset_id == (uint32_t)-1)
+ reset->new_reset_id = reset_id;
+ }
+}
+
+static void
+transaction_update_atomic_reset_ids(struct mail_index_transaction *t)
+{
+ const uint32_t *expected_reset_ids;
+ unsigned int ext_id, count;
+
+ if (!array_is_created(&t->ext_reset_atomic))
+ return;
+
+ expected_reset_ids = array_get(&t->ext_reset_atomic, &count);
+ for (ext_id = 0; ext_id < count; ext_id++) {
+ if (expected_reset_ids[ext_id] != 0) {
+ ext_reset_update_atomic(t, ext_id,
+ expected_reset_ids[ext_id]);
+ }
+ }
+}
+
+static unsigned int
+mail_transaction_drop_range(struct mail_index_transaction *t,
+ struct mail_index_flag_update update,
+ unsigned int update_idx,
+ ARRAY_TYPE(seq_range) *keeps)
+{
+ const struct seq_range *keep_range;
+ unsigned int i, keep_count;
+
+ keep_range = array_get(keeps, &keep_count);
+ if (keep_count == 1 &&
+ update.uid1 == keep_range[0].seq1 &&
+ update.uid2 == keep_range[0].seq2) {
+ /* everything is kept */
+ return update_idx + 1;
+ }
+
+ array_delete(&t->updates, update_idx, 1);
+
+ /* add back all the updates we want to keep */
+ for (i = 0; i < keep_count; i++, update_idx++) {
+ update.uid1 = keep_range[i].seq1;
+ update.uid2 = keep_range[i].seq2;
+ array_insert(&t->updates, update_idx, &update, 1);
+ }
+ return update_idx;
+}
+
+static void
+mail_index_transaction_finish_flag_updates(struct mail_index_transaction *t)
+{
+ const struct mail_index_flag_update *updates, *u;
+ const struct mail_index_record *rec;
+ unsigned int i, count;
+ ARRAY_TYPE(seq_range) keeps;
+ uint32_t seq;
+
+ if (!t->drop_unnecessary_flag_updates || !array_is_created(&t->updates))
+ return;
+
+ t_array_init(&keeps, 64);
+ updates = array_get(&t->updates, &count);
+ for (i = 0; i < count; ) {
+ /* first get the list of changes to drop */
+ u = &updates[i];
+ array_clear(&keeps);
+ for (seq = u->uid1; seq <= u->uid2; seq++) {
+ rec = mail_index_lookup(t->view, seq);
+ if ((rec->flags & u->add_flags) != u->add_flags ||
+ (rec->flags & u->remove_flags) != 0) {
+ /* keep this change */
+ seq_range_array_add(&keeps, seq);
+ }
+ }
+ i = mail_transaction_drop_range(t, updates[i], i, &keeps);
+ updates = array_get(&t->updates, &count);
+ }
+
+ if (array_count(&t->updates) == 0)
+ array_free(&t->updates);
+}
+
+static void
+mail_index_transaction_check_conflicts(struct mail_index_transaction *t)
+{
+ uint32_t seq;
+ bool ret1, ret2;
+
+ i_assert(t->max_modseq != 0);
+ i_assert(t->conflict_seqs != NULL);
+
+ if (t->max_modseq == mail_index_modseq_get_highest(t->view)) {
+ /* no conflicts possible */
+ return;
+ }
+ if (t->min_flagupdate_seq == 0) {
+ /* no flag updates */
+ return;
+ }
+
+ for (seq = t->min_flagupdate_seq; seq <= t->max_flagupdate_seq; seq++) {
+ if (mail_index_modseq_lookup(t->view, seq) > t->max_modseq) {
+ ret1 = mail_index_cancel_flag_updates(t, seq);
+ ret2 = mail_index_cancel_keyword_updates(t, seq);
+ if (ret1 || ret2) {
+ seq_range_array_add_with_init(t->conflict_seqs,
+ 16, seq);
+ }
+ }
+ }
+ mail_index_transaction_set_log_updates(t);
+}
+
+static uint32_t
+mail_index_transaction_get_uid(struct mail_index_transaction *t, uint32_t seq)
+{
+ const struct mail_index_record *rec;
+
+ i_assert(seq > 0);
+
+ if (seq >= t->first_new_seq)
+ rec = mail_index_transaction_lookup(t, seq);
+ else {
+ i_assert(seq <= t->view->map->hdr.messages_count);
+ rec = MAIL_INDEX_REC_AT_SEQ(t->view->map, seq);
+ }
+ i_assert(rec->uid != 0);
+ return rec->uid;
+}
+
+static void
+mail_index_convert_to_uids(struct mail_index_transaction *t,
+ ARRAY_TYPE(seq_array) *array)
+{
+ uint32_t *seq;
+ unsigned int i, count;
+
+ if (!array_is_created(array))
+ return;
+
+ count = array_count(array);
+ for (i = 0; i < count; i++) {
+ seq = array_idx_modifiable(array, i);
+ *seq = mail_index_transaction_get_uid(t, *seq);
+ }
+}
+
+static uint32_t
+get_nonexpunged_uid2(struct mail_index_transaction *t,
+ uint32_t uid1, uint32_t seq1)
+{
+ seq1++;
+
+ while (mail_index_transaction_get_uid(t, seq1) == uid1 + 1) {
+ seq1++;
+ uid1++;
+ }
+ return uid1;
+}
+
+void mail_index_transaction_seq_range_to_uid(struct mail_index_transaction *t,
+ ARRAY_TYPE(seq_range) *array)
+{
+ struct seq_range *range, *new_range;
+ unsigned int i, count;
+ uint32_t uid1, uid2, prev_uid = 0;
+
+ if (!array_is_created(array))
+ return;
+
+ count = array_count(array);
+ for (i = 0; i < count; i++) {
+ range = array_idx_modifiable(array, i);
+
+ uid1 = mail_index_transaction_get_uid(t, range->seq1);
+ uid2 = mail_index_transaction_get_uid(t, range->seq2);
+ i_assert(uid1 > prev_uid);
+ if (uid2 - uid1 == range->seq2 - range->seq1) {
+ /* simple conversion */
+ range->seq1 = uid1;
+ range->seq2 = uid2;
+ prev_uid = uid2;
+ } else {
+ /* remove expunged UIDs */
+ new_range = array_insert_space(array, i);
+ range = array_idx_modifiable(array, i + 1);
+ count++;
+
+ memcpy(new_range, range, array->arr.element_size);
+ new_range->seq1 = uid1;
+ new_range->seq2 = get_nonexpunged_uid2(t, uid1,
+ range->seq1);
+ i_assert(new_range->seq2 < uid2);
+
+ /* continue the range without the inserted seqs */
+ range->seq1 += new_range->seq2 - new_range->seq1 + 1;
+ prev_uid = new_range->seq2;
+ }
+ }
+}
+
+static void keyword_updates_convert_to_uids(struct mail_index_transaction *t)
+{
+ struct mail_index_transaction_keyword_update *update;
+
+ if (!array_is_created(&t->keyword_updates))
+ return;
+
+ array_foreach_modifiable(&t->keyword_updates, update) {
+ mail_index_transaction_seq_range_to_uid(t, &update->add_seq);
+ mail_index_transaction_seq_range_to_uid(t, &update->remove_seq);
+ }
+}
+
+static void expunges_convert_to_uids(struct mail_index_transaction *t)
+{
+ struct mail_transaction_expunge_guid *expunges;
+ unsigned int src, dest, count;
+
+ if (!array_is_created(&t->expunges))
+ return;
+
+ mail_index_transaction_sort_expunges(t);
+
+ expunges = array_get_modifiable(&t->expunges, &count);
+ if (count == 0)
+ return;
+
+ /* convert uids and drop duplicates */
+ expunges[0].uid = mail_index_transaction_get_uid(t, expunges[0].uid);
+ for (src = dest = 1; src < count; src++) {
+ expunges[dest].uid =
+ mail_index_transaction_get_uid(t, expunges[src].uid);
+ if (expunges[dest-1].uid != expunges[dest].uid) {
+ if (dest != src) {
+ memcpy(expunges[dest].guid_128, expunges[src].guid_128,
+ sizeof(expunges[dest].guid_128));
+ }
+ dest++;
+ }
+ }
+ array_delete(&t->expunges, dest, count-dest);
+}
+
+static void
+mail_index_transaction_convert_to_uids(struct mail_index_transaction *t)
+{
+ ARRAY_TYPE(seq_array) *update;
+
+ if (array_is_created(&t->ext_rec_updates)) {
+ array_foreach_modifiable(&t->ext_rec_updates, update)
+ mail_index_convert_to_uids(t, update);
+ }
+ if (array_is_created(&t->ext_rec_atomics)) {
+ array_foreach_modifiable(&t->ext_rec_atomics, update)
+ mail_index_convert_to_uids(t, update);
+ }
+
+ keyword_updates_convert_to_uids(t);
+ expunges_convert_to_uids(t);
+ mail_index_convert_to_uids(t, (void *)&t->modseq_updates);
+ mail_index_transaction_seq_range_to_uid(t, (void *)&t->updates);
+}
+
+void mail_index_transaction_finish_so_far(struct mail_index_transaction *t)
+{
+ if (array_is_created(&t->appends))
+ mail_index_transaction_sort_appends(t);
+ mail_index_transaction_finish_flag_updates(t);
+ if (t->max_modseq != 0)
+ mail_index_transaction_check_conflicts(t);
+}
+
+void mail_index_transaction_finish(struct mail_index_transaction *t)
+{
+ mail_index_transaction_finish_so_far(t);
+
+ if (array_is_created(&t->appends))
+ mail_index_update_day_headers(t, ioloop_time);
+ if (array_is_created(&t->ext_reset_atomic))
+ transaction_update_atomic_reset_ids(t);
+ /* finally convert all sequences to UIDs before we write them,
+ but after we've checked and removed conflicts */
+ mail_index_transaction_convert_to_uids(t);
+
+ /* and kind of ugly way to update highest modseq */
+ if (t->min_highest_modseq != 0)
+ mail_index_update_modseq(t, 0, t->min_highest_modseq);
+}
diff --git a/src/lib-index/mail-index-transaction-private.h b/src/lib-index/mail-index-transaction-private.h
new file mode 100644
index 0000000..eb438b2
--- /dev/null
+++ b/src/lib-index/mail-index-transaction-private.h
@@ -0,0 +1,165 @@
+#ifndef MAIL_INDEX_TRANSACTION_PRIVATE_H
+#define MAIL_INDEX_TRANSACTION_PRIVATE_H
+
+#include "seq-range-array.h"
+#include "mail-transaction-log.h"
+
+ARRAY_DEFINE_TYPE(seq_array_array, ARRAY_TYPE(seq_array));
+
+struct mail_index_transaction_keyword_update {
+ ARRAY_TYPE(seq_range) add_seq;
+ ARRAY_TYPE(seq_range) remove_seq;
+};
+
+struct mail_index_transaction_ext_hdr_update {
+ size_t alloc_size;
+ /* mask is in bytes, not bits */
+ unsigned char *mask;
+ unsigned char *data;
+};
+
+struct mail_index_transaction_vfuncs {
+ void (*reset)(struct mail_index_transaction *t);
+ int (*commit)(struct mail_index_transaction *t,
+ struct mail_index_transaction_commit_result *result_r);
+ void (*rollback)(struct mail_index_transaction *t);
+};
+
+union mail_index_transaction_module_context {
+ struct mail_index_transaction_vfuncs super;
+ struct mail_index_module_register *reg;
+};
+
+struct mail_index_flag_update {
+ uint32_t uid1, uid2;
+ uint16_t add_flags;
+ uint16_t remove_flags;
+};
+
+struct mail_index_transaction {
+ struct mail_index_transaction *prev, *next;
+ int refcount;
+
+ enum mail_index_transaction_flags flags;
+ struct mail_index_transaction_vfuncs v, *vlast;
+ struct mail_index_view *view;
+ struct mail_index_view *latest_view;
+
+ /* NOTE: If you add anything new, remember to update
+ mail_index_transaction_reset_v() to reset it. */
+ ARRAY(struct mail_index_record) appends;
+ uint32_t first_new_seq, last_new_seq;
+ uint32_t highest_append_uid;
+ /* lowest/highest sequence that updates flags/keywords */
+ uint32_t min_flagupdate_seq, max_flagupdate_seq;
+
+ ARRAY(struct mail_transaction_modseq_update) modseq_updates;
+ ARRAY(struct mail_transaction_expunge_guid) expunges;
+ ARRAY(struct mail_index_flag_update) updates;
+ size_t last_update_idx;
+
+ unsigned char pre_hdr_change[sizeof(struct mail_index_header)];
+ unsigned char pre_hdr_mask[sizeof(struct mail_index_header)];
+ unsigned char post_hdr_change[sizeof(struct mail_index_header)];
+ unsigned char post_hdr_mask[sizeof(struct mail_index_header)];
+
+ ARRAY(struct mail_index_transaction_ext_hdr_update) ext_hdr_updates;
+ ARRAY_TYPE(seq_array_array) ext_rec_updates;
+ ARRAY_TYPE(seq_array_array) ext_rec_atomics;
+ ARRAY(struct mail_transaction_ext_intro) ext_resizes;
+ ARRAY(struct mail_transaction_ext_reset) ext_resets;
+ ARRAY(uint32_t) ext_reset_ids;
+ ARRAY(uint32_t) ext_reset_atomic;
+
+ ARRAY(struct mail_index_transaction_keyword_update) keyword_updates;
+ buffer_t *attribute_updates; /* [+-][ps]key\0.. */
+ buffer_t *attribute_updates_suffix; /* <timestamp>[<value len>].. */
+
+ uint64_t min_highest_modseq;
+ uint64_t max_modseq;
+ ARRAY_TYPE(seq_range) *conflict_seqs;
+
+ /* Module-specific contexts. */
+ ARRAY(union mail_index_transaction_module_context *) module_contexts;
+
+ bool no_appends:1;
+
+ bool sync_transaction:1;
+ bool appends_nonsorted:1;
+ bool expunges_nonsorted:1;
+ bool drop_unnecessary_flag_updates:1;
+ bool pre_hdr_changed:1;
+ bool post_hdr_changed:1;
+ bool reset:1;
+ bool index_deleted:1;
+ bool index_undeleted:1;
+ bool commit_deleted_index:1;
+ bool tail_offset_changed:1;
+ /* non-extension updates. flag updates don't change this because
+ they may be added and removed, so be sure to check that the updates
+ array is non-empty also. */
+ bool log_updates:1;
+ /* extension updates */
+ bool log_ext_updates:1;
+};
+
+#define MAIL_INDEX_TRANSACTION_HAS_CHANGES(t) \
+ ((t)->log_updates || (t)->log_ext_updates || \
+ (array_is_created(&(t)->updates) && array_count(&(t)->updates) > 0) || \
+ (t)->index_deleted || (t)->index_undeleted)
+
+typedef void hook_mail_index_transaction_created_t(struct mail_index_transaction *t);
+
+void mail_index_transaction_hook_register(hook_mail_index_transaction_created_t *hook);
+void mail_index_transaction_hook_unregister(hook_mail_index_transaction_created_t *hook);
+
+struct mail_index_record *
+mail_index_transaction_lookup(struct mail_index_transaction *t, uint32_t seq);
+
+void mail_index_transaction_ref(struct mail_index_transaction *t);
+void mail_index_transaction_unref(struct mail_index_transaction **t);
+void mail_index_transaction_reset_v(struct mail_index_transaction *t);
+
+void mail_index_transaction_sort_appends(struct mail_index_transaction *t);
+void mail_index_transaction_sort_expunges(struct mail_index_transaction *t);
+uint32_t mail_index_transaction_get_next_uid(struct mail_index_transaction *t);
+void mail_index_transaction_set_log_updates(struct mail_index_transaction *t);
+void mail_index_update_day_headers(struct mail_index_transaction *t, time_t day_stamp);
+
+unsigned int
+mail_index_transaction_get_flag_update_pos(struct mail_index_transaction *t,
+ unsigned int left_idx,
+ unsigned int right_idx,
+ uint32_t seq);
+void mail_index_transaction_lookup_latest_keywords(struct mail_index_transaction *t,
+ uint32_t seq,
+ ARRAY_TYPE(keyword_indexes) *keywords);
+
+bool mail_index_cancel_flag_updates(struct mail_index_transaction *t,
+ uint32_t seq);
+bool mail_index_cancel_keyword_updates(struct mail_index_transaction *t,
+ uint32_t seq);
+
+/* As input the array's each element starts with struct seq_range where
+ uid1..uid2 are actually sequences within the transaction view. This function
+ changes the sequences into UIDs. If the transaction has any appends, they
+ must have already been assigned UIDs. */
+void mail_index_transaction_seq_range_to_uid(struct mail_index_transaction *t,
+ ARRAY_TYPE(seq_range) *array);
+void mail_index_transaction_finish_so_far(struct mail_index_transaction *t);
+void mail_index_transaction_finish(struct mail_index_transaction *t);
+void mail_index_transaction_export(struct mail_index_transaction *t,
+ struct mail_transaction_log_append_ctx *append_ctx,
+ enum mail_index_transaction_change *changes_r);
+int mail_transaction_expunge_guid_cmp(const struct mail_transaction_expunge_guid *e1,
+ const struct mail_transaction_expunge_guid *e2);
+unsigned int
+mail_index_transaction_get_flag_update_pos(struct mail_index_transaction *t,
+ unsigned int left_idx,
+ unsigned int right_idx,
+ uint32_t seq);
+
+void mail_index_ext_using_reset_id(struct mail_index_transaction *t,
+ uint32_t ext_id, uint32_t reset_id);
+
+#endif
diff --git a/src/lib-index/mail-index-transaction-sort-appends.c b/src/lib-index/mail-index-transaction-sort-appends.c
new file mode 100644
index 0000000..c2a29d3
--- /dev/null
+++ b/src/lib-index/mail-index-transaction-sort-appends.c
@@ -0,0 +1,184 @@
+/* Copyright (c) 2003-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "seq-range-array.h"
+#include "mail-index-private.h"
+#include "mail-index-transaction-private.h"
+
+
+struct uid_map {
+ uint32_t idx;
+ uint32_t uid;
+};
+
+static int uid_map_cmp(const void *p1, const void *p2)
+{
+ const struct uid_map *m1 = p1, *m2 = p2;
+
+ return m1->uid < m2->uid ? -1 :
+ (m1->uid > m2->uid ? 1 : 0);
+}
+
+static void
+mail_index_transaction_sort_appends_ext(ARRAY_TYPE(seq_array_array) *updates,
+ uint32_t first_new_seq,
+ const uint32_t *old_to_newseq_map)
+{
+ ARRAY_TYPE(seq_array) *ext_rec_arrays;
+ ARRAY_TYPE(seq_array) *old_array;
+ ARRAY_TYPE(seq_array) new_array;
+ unsigned int ext_count;
+ const uint32_t *ext_rec;
+ uint32_t seq;
+ unsigned int i, j, count;
+
+ if (!array_is_created(updates))
+ return;
+
+ ext_rec_arrays = array_get_modifiable(updates, &count);
+ for (j = 0; j < count; j++) {
+ old_array = &ext_rec_arrays[j];
+ if (!array_is_created(old_array))
+ continue;
+
+ ext_count = array_count(old_array);
+ array_create(&new_array, default_pool,
+ old_array->arr.element_size, ext_count);
+ for (i = 0; i < ext_count; i++) {
+ ext_rec = array_idx(old_array, i);
+
+ seq = *ext_rec < first_new_seq ? *ext_rec :
+ old_to_newseq_map[*ext_rec - first_new_seq];
+ (void)mail_index_seq_array_add(&new_array, seq, ext_rec+1,
+ old_array->arr.element_size -
+ sizeof(*ext_rec), NULL);
+ }
+ array_free(old_array);
+ ext_rec_arrays[j] = new_array;
+ }
+}
+
+static void
+sort_appends_seq_range(ARRAY_TYPE(seq_range) *array, uint32_t first_new_seq,
+ const uint32_t *old_to_newseq_map)
+{
+ struct seq_range *range, temp_range;
+ ARRAY_TYPE(seq_range) old_seqs;
+ uint32_t idx, idx1, idx2;
+ unsigned int i, count;
+
+ range = array_get_modifiable(array, &count);
+ for (i = 0; i < count; i++) {
+ if (range[i].seq2 >= first_new_seq)
+ break;
+ }
+ if (i == count) {
+ /* nothing to do */
+ return;
+ }
+
+ i_array_init(&old_seqs, count - i);
+ if (range[i].seq1 < first_new_seq) {
+ temp_range.seq1 = first_new_seq;
+ temp_range.seq2 = range[i].seq2;
+ array_push_back(&old_seqs, &temp_range);
+ range[i].seq2 = first_new_seq - 1;
+ i++;
+ }
+ array_append(&old_seqs, &range[i], count - i);
+ array_delete(array, i, count - i);
+
+ range = array_get_modifiable(&old_seqs, &count);
+ for (i = 0; i < count; i++) {
+ idx1 = range[i].seq1 - first_new_seq;
+ idx2 = range[i].seq2 - first_new_seq;
+ for (idx = idx1; idx <= idx2; idx++)
+ seq_range_array_add(array, old_to_newseq_map[idx]);
+ }
+ array_free(&old_seqs);
+}
+
+static void
+mail_index_transaction_sort_appends_keywords(struct mail_index_transaction *t,
+ const uint32_t *old_to_newseq_map)
+{
+ struct mail_index_transaction_keyword_update *update;
+
+ if (!array_is_created(&t->keyword_updates))
+ return;
+
+ array_foreach_modifiable(&t->keyword_updates, update) {
+ if (array_is_created(&update->add_seq)) {
+ sort_appends_seq_range(&update->add_seq,
+ t->first_new_seq,
+ old_to_newseq_map);
+ }
+ if (array_is_created(&update->remove_seq)) {
+ sort_appends_seq_range(&update->remove_seq,
+ t->first_new_seq,
+ old_to_newseq_map);
+ }
+ }
+}
+
+void mail_index_transaction_sort_appends(struct mail_index_transaction *t)
+{
+ struct mail_index_record *recs, *sorted_recs;
+ struct uid_map *new_uid_map;
+ uint32_t *old_to_newseq_map;
+ unsigned int i, count;
+
+ if (!array_is_created(&t->appends))
+ return;
+ recs = array_get_modifiable(&t->appends, &count);
+ i_assert(count > 0);
+
+ if (!t->appends_nonsorted) {
+ i_assert(recs[0].uid != 0);
+#ifdef DEBUG
+ for (i = 1; i < count; i++)
+ i_assert(recs[i-1].uid < recs[i].uid);
+#endif
+ return;
+ }
+
+ /* first make a copy of the UIDs and map them to sequences */
+ new_uid_map = i_new(struct uid_map, count);
+ for (i = 0; i < count; i++) {
+ i_assert(recs[i].uid != 0);
+ new_uid_map[i].idx = i;
+ new_uid_map[i].uid = recs[i].uid;
+ }
+
+ /* now sort the UID map */
+ qsort(new_uid_map, count, sizeof(*new_uid_map), uid_map_cmp);
+
+ /* sort mail records */
+ sorted_recs = i_new(struct mail_index_record, count);
+ sorted_recs[0] = recs[new_uid_map[0].idx];
+ for (i = 1; i < count; i++) {
+ sorted_recs[i] = recs[new_uid_map[i].idx];
+ if (sorted_recs[i].uid == sorted_recs[i-1].uid)
+ i_panic("Duplicate UIDs added in transaction");
+ }
+ buffer_write(t->appends.arr.buffer, 0, sorted_recs,
+ sizeof(*sorted_recs) * count);
+ i_free(sorted_recs);
+
+ old_to_newseq_map = i_new(uint32_t, count);
+ for (i = 0; i < count; i++)
+ old_to_newseq_map[new_uid_map[i].idx] = i + t->first_new_seq;
+ i_free(new_uid_map);
+
+ mail_index_transaction_sort_appends_ext(&t->ext_rec_updates,
+ t->first_new_seq,
+ old_to_newseq_map);
+ mail_index_transaction_sort_appends_ext(&t->ext_rec_atomics,
+ t->first_new_seq,
+ old_to_newseq_map);
+ mail_index_transaction_sort_appends_keywords(t, old_to_newseq_map);
+ i_free(old_to_newseq_map);
+
+ t->appends_nonsorted = FALSE;
+}
diff --git a/src/lib-index/mail-index-transaction-update.c b/src/lib-index/mail-index-transaction-update.c
new file mode 100644
index 0000000..c7bcbd9
--- /dev/null
+++ b/src/lib-index/mail-index-transaction-update.c
@@ -0,0 +1,1367 @@
+/* Copyright (c) 2003-2018 Dovecot authors, see the included COPYING file */
+
+/* Inside transaction we keep messages stored in sequences in uid fields.
+ Before they're written to transaction log the sequences are changed to
+ UIDs. */
+
+#include "lib.h"
+#include "array.h"
+#include "time-util.h"
+#include "mail-index-private.h"
+#include "mail-index-transaction-private.h"
+
+static bool
+mail_index_transaction_has_ext_changes(struct mail_index_transaction *t);
+
+struct mail_index_record *
+mail_index_transaction_lookup(struct mail_index_transaction *t, uint32_t seq)
+{
+ i_assert(seq >= t->first_new_seq && seq <= t->last_new_seq);
+
+ return array_idx_modifiable(&t->appends, seq - t->first_new_seq);
+}
+
+void mail_index_transaction_reset_v(struct mail_index_transaction *t)
+{
+ ARRAY_TYPE(seq_array) *rec;
+ struct mail_index_transaction_ext_hdr_update *ext_hdr;
+
+ if (array_is_created(&t->ext_rec_updates)) {
+ array_foreach_modifiable(&t->ext_rec_updates, rec) {
+ if (array_is_created(rec))
+ array_free(rec);
+ }
+ array_free(&t->ext_rec_updates);
+ }
+ if (array_is_created(&t->ext_rec_atomics)) {
+ array_foreach_modifiable(&t->ext_rec_atomics, rec) {
+ if (array_is_created(rec))
+ array_free(rec);
+ }
+ array_free(&t->ext_rec_atomics);
+ }
+ if (array_is_created(&t->ext_hdr_updates)) {
+ array_foreach_modifiable(&t->ext_hdr_updates, ext_hdr) {
+ i_free(ext_hdr->data);
+ i_free(ext_hdr->mask);
+ }
+ array_free(&t->ext_hdr_updates);
+ }
+
+ if (array_is_created(&t->keyword_updates)) {
+ struct mail_index_transaction_keyword_update *u;
+
+ array_foreach_modifiable(&t->keyword_updates, u) {
+ if (array_is_created(&u->add_seq))
+ array_free(&u->add_seq);
+ if (array_is_created(&u->remove_seq))
+ array_free(&u->remove_seq);
+ }
+ array_free(&t->keyword_updates);
+ }
+
+ if (array_is_created(&t->appends))
+ array_free(&t->appends);
+ if (array_is_created(&t->modseq_updates))
+ array_free(&t->modseq_updates);
+ if (array_is_created(&t->expunges))
+ array_free(&t->expunges);
+ if (array_is_created(&t->updates))
+ array_free(&t->updates);
+ if (array_is_created(&t->ext_resizes))
+ array_free(&t->ext_resizes);
+ if (array_is_created(&t->ext_resets))
+ array_free(&t->ext_resets);
+ if (array_is_created(&t->ext_reset_ids))
+ array_free(&t->ext_reset_ids);
+ if (array_is_created(&t->ext_reset_atomic))
+ array_free(&t->ext_reset_atomic);
+ buffer_free(&t->attribute_updates);
+ buffer_free(&t->attribute_updates_suffix);
+
+ t->first_new_seq = mail_index_view_get_messages_count(t->view)+1;
+ t->last_new_seq = 0;
+ t->last_update_idx = 0;
+ t->min_flagupdate_seq = 0;
+ t->max_flagupdate_seq = 0;
+ t->min_highest_modseq = 0;
+
+ memset(t->pre_hdr_mask, 0, sizeof(t->pre_hdr_mask));
+ memset(t->post_hdr_mask, 0, sizeof(t->post_hdr_mask));
+
+ t->appends_nonsorted = FALSE;
+ t->expunges_nonsorted = FALSE;
+ t->drop_unnecessary_flag_updates = FALSE;
+ t->pre_hdr_changed = FALSE;
+ t->post_hdr_changed = FALSE;
+ t->reset = FALSE;
+ t->index_deleted = FALSE;
+ t->index_undeleted = FALSE;
+ t->log_updates = FALSE;
+ t->log_ext_updates = FALSE;
+ t->tail_offset_changed = FALSE;
+}
+
+void mail_index_transaction_set_log_updates(struct mail_index_transaction *t)
+{
+ /* flag updates aren't included in log_updates */
+ t->log_updates = array_is_created(&t->appends) ||
+ array_is_created(&t->modseq_updates) ||
+ array_is_created(&t->expunges) ||
+ array_is_created(&t->keyword_updates) ||
+ t->attribute_updates != NULL ||
+ t->pre_hdr_changed || t->post_hdr_changed ||
+ t->min_highest_modseq != 0;
+}
+
+void mail_index_update_day_headers(struct mail_index_transaction *t,
+ time_t day_stamp)
+{
+ struct mail_index_header hdr;
+ const struct mail_index_record *rec;
+ const int max_days = N_ELEMENTS(hdr.day_first_uid);
+ time_t stamp;
+ int i, days;
+
+ hdr = *mail_index_get_header(t->view);
+ rec = array_front(&t->appends);
+
+ stamp = time_to_local_day_start(day_stamp);
+ if ((time_t)hdr.day_stamp >= stamp)
+ return;
+
+ /* get number of days since last message */
+ days = (stamp - hdr.day_stamp) / (3600*24);
+ if (days > max_days)
+ days = max_days;
+
+ /* @UNSAFE: move days forward and fill the missing days with old
+ day_first_uid[0]. */
+ if (days > 0 && days < max_days)
+ memmove(hdr.day_first_uid + days, hdr.day_first_uid,
+ (max_days - days) * sizeof(hdr.day_first_uid[0]));
+ for (i = 1; i < days; i++)
+ hdr.day_first_uid[i] = hdr.day_first_uid[0];
+
+ hdr.day_stamp = stamp;
+ hdr.day_first_uid[0] = rec->uid;
+
+ mail_index_update_header(t,
+ offsetof(struct mail_index_header, day_stamp),
+ &hdr.day_stamp, sizeof(hdr.day_stamp), FALSE);
+ mail_index_update_header(t,
+ offsetof(struct mail_index_header, day_first_uid),
+ hdr.day_first_uid, sizeof(hdr.day_first_uid), FALSE);
+}
+
+void mail_index_append(struct mail_index_transaction *t, uint32_t uid,
+ uint32_t *seq_r)
+{
+ struct mail_index_record *rec;
+
+ i_assert(!t->no_appends);
+
+ t->log_updates = TRUE;
+
+ if (!array_is_created(&t->appends))
+ i_array_init(&t->appends, 32);
+
+ /* sequence number is visible only inside given view,
+ so let it generate it */
+ if (t->last_new_seq != 0)
+ *seq_r = ++t->last_new_seq;
+ else
+ *seq_r = t->last_new_seq = t->first_new_seq;
+
+ rec = array_append_space(&t->appends);
+ if (uid != 0) {
+ rec->uid = uid;
+ if (!t->appends_nonsorted &&
+ t->last_new_seq != t->first_new_seq) {
+ /* if previous record's UID is larger than this one,
+ we'll have to sort the appends later */
+ rec = mail_index_transaction_lookup(t, *seq_r - 1);
+ if (rec->uid > uid)
+ t->appends_nonsorted = TRUE;
+ else if (rec->uid == uid)
+ i_panic("Duplicate UIDs added in transaction");
+ }
+ if (t->highest_append_uid < uid)
+ t->highest_append_uid = uid;
+ }
+}
+
+void mail_index_append_finish_uids(struct mail_index_transaction *t,
+ uint32_t first_uid,
+ ARRAY_TYPE(seq_range) *uids_r)
+{
+ return mail_index_append_finish_uids_full(t, first_uid, first_uid, uids_r);
+}
+
+void mail_index_append_finish_uids_full(struct mail_index_transaction *t,
+ uint32_t min_allowed_uid,
+ uint32_t first_new_uid,
+ ARRAY_TYPE(seq_range) *uids_r)
+{
+ struct mail_index_record *recs;
+ unsigned int i, count;
+ struct seq_range *range;
+ uint32_t next_uid;
+
+ if (!array_is_created(&t->appends))
+ return;
+
+ i_assert(min_allowed_uid <= first_new_uid);
+ i_assert(first_new_uid < (uint32_t)-1);
+
+ /* first find the highest assigned uid */
+ recs = array_get_modifiable(&t->appends, &count);
+ i_assert(count > 0);
+
+ next_uid = first_new_uid;
+ for (i = 0; i < count; i++) {
+ if (next_uid <= recs[i].uid)
+ next_uid = recs[i].uid + 1;
+ }
+ i_assert(next_uid > 0 && next_uid < (uint32_t)-1);
+
+ /* assign missing uids */
+ for (i = 0; i < count; i++) {
+ if (recs[i].uid == 0 || recs[i].uid < min_allowed_uid) {
+ i_assert(next_uid < (uint32_t)-1);
+ recs[i].uid = next_uid++;
+ if (t->highest_append_uid < recs[i].uid)
+ t->highest_append_uid = recs[i].uid;
+ } else {
+ t->appends_nonsorted = TRUE;
+ }
+ }
+
+ /* write the saved uids range */
+ array_clear(uids_r);
+ range = array_append_space(uids_r);
+ range->seq1 = range->seq2 = recs[0].uid;
+ for (i = 1; i < count; i++) {
+ if (range->seq2 + 1 == recs[i].uid)
+ range->seq2++;
+ else {
+ range = array_append_space(uids_r);
+ range->seq1 = range->seq2 = recs[i].uid;
+ }
+ }
+}
+
+void mail_index_update_modseq(struct mail_index_transaction *t, uint32_t seq,
+ uint64_t min_modseq)
+{
+ struct mail_transaction_modseq_update *u;
+
+ /* modseq=1 is the minimum always and it's only for mails that were
+ created/modified before modseqs were enabled. */
+ if (min_modseq <= 1)
+ return;
+
+ if (!array_is_created(&t->modseq_updates))
+ i_array_init(&t->modseq_updates, 32);
+
+ u = array_append_space(&t->modseq_updates);
+ u->uid = seq;
+ u->modseq_low32 = min_modseq & 0xffffffff;
+ u->modseq_high32 = min_modseq >> 32;
+
+ t->log_updates = TRUE;
+}
+
+void mail_index_update_highest_modseq(struct mail_index_transaction *t,
+ uint64_t min_modseq)
+{
+ /* modseq=1 is the minimum always and it's only for mails that were
+ created/modified before modseqs were enabled. */
+ if (min_modseq <= 1)
+ return;
+
+ if (t->min_highest_modseq < min_modseq)
+ t->min_highest_modseq = min_modseq;
+
+ t->log_updates = TRUE;
+}
+
+static void
+mail_index_revert_ext(ARRAY_TYPE(seq_array_array) *ext_updates,
+ uint32_t seq)
+{
+ ARRAY_TYPE(seq_array) *seqs;
+ unsigned int idx;
+
+ if (!array_is_created(ext_updates))
+ return;
+
+ array_foreach_modifiable(ext_updates, seqs) {
+ if (array_is_created(seqs) &&
+ mail_index_seq_array_lookup(seqs, seq, &idx))
+ array_delete(seqs, idx, 1);
+ }
+}
+
+static void
+mail_index_revert_changes_common(struct mail_index_transaction *t, uint32_t seq)
+{
+ struct mail_index_transaction_keyword_update *kw_update;
+ unsigned int i;
+
+ /* remove extension updates */
+ mail_index_revert_ext(&t->ext_rec_updates, seq);
+ mail_index_revert_ext(&t->ext_rec_atomics, seq);
+ t->log_ext_updates = mail_index_transaction_has_ext_changes(t);
+
+ /* remove keywords */
+ if (array_is_created(&t->keyword_updates)) {
+ array_foreach_modifiable(&t->keyword_updates, kw_update) {
+ if (array_is_created(&kw_update->add_seq)) {
+ seq_range_array_remove(&kw_update->add_seq,
+ seq);
+ }
+ if (array_is_created(&kw_update->remove_seq)) {
+ seq_range_array_remove(&kw_update->remove_seq,
+ seq);
+ }
+ }
+ }
+ /* remove modseqs */
+ if (array_is_created(&t->modseq_updates) &&
+ mail_index_seq_array_lookup((void *)&t->modseq_updates, seq, &i))
+ array_delete(&t->modseq_updates, i, 1);
+}
+
+void mail_index_revert_changes(struct mail_index_transaction *t, uint32_t seq)
+{
+ mail_index_revert_changes_common(t, seq);
+ (void)mail_index_cancel_flag_updates(t, seq);
+}
+
+static void
+mail_index_expunge_last_append(struct mail_index_transaction *t, uint32_t seq)
+{
+ i_assert(seq == t->last_new_seq);
+
+ mail_index_revert_changes_common(t, seq);
+
+ /* and finally remove the append itself */
+ array_delete(&t->appends, seq - t->first_new_seq, 1);
+ t->last_new_seq--;
+ if (t->first_new_seq > t->last_new_seq) {
+ t->last_new_seq = 0;
+ t->appends_nonsorted = FALSE;
+ array_free(&t->appends);
+ }
+ mail_index_transaction_set_log_updates(t);
+}
+
+void mail_index_expunge(struct mail_index_transaction *t, uint32_t seq)
+{
+ static guid_128_t null_guid =
+ { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 };
+ mail_index_expunge_guid(t, seq, null_guid);
+}
+
+void mail_index_expunge_guid(struct mail_index_transaction *t, uint32_t seq,
+ const guid_128_t guid_128)
+{
+ const struct mail_transaction_expunge_guid *expunges;
+ struct mail_transaction_expunge_guid *expunge;
+ unsigned int count;
+
+ i_assert(seq > 0);
+ if (seq >= t->first_new_seq) {
+ /* we can handle only the last append. otherwise we'd have to
+ renumber sequences and that gets tricky. for now this is
+ enough, since we typically want to expunge all the
+ appends. */
+ mail_index_expunge_last_append(t, seq);
+ } else {
+ t->log_updates = TRUE;
+
+ /* ignore duplicates here. drop them when committing. */
+ if (!array_is_created(&t->expunges))
+ i_array_init(&t->expunges, 64);
+ else if (!t->expunges_nonsorted) {
+ /* usually expunges are added in increasing order. */
+ expunges = array_get(&t->expunges, &count);
+ if (count > 0 && seq < expunges[count-1].uid)
+ t->expunges_nonsorted = TRUE;
+ }
+ expunge = array_append_space(&t->expunges);
+ expunge->uid = seq;
+ memcpy(expunge->guid_128, guid_128, sizeof(expunge->guid_128));
+ }
+}
+
+static void update_minmax_flagupdate_seq(struct mail_index_transaction *t,
+ uint32_t seq1, uint32_t seq2)
+{
+ if (t->min_flagupdate_seq == 0) {
+ t->min_flagupdate_seq = seq1;
+ t->max_flagupdate_seq = seq2;
+ } else {
+ if (t->min_flagupdate_seq > seq1)
+ t->min_flagupdate_seq = seq1;
+ if (t->max_flagupdate_seq < seq2)
+ t->max_flagupdate_seq = seq2;
+ }
+}
+
+unsigned int
+mail_index_transaction_get_flag_update_pos(struct mail_index_transaction *t,
+ unsigned int left_idx,
+ unsigned int right_idx,
+ uint32_t seq)
+{
+ const struct mail_index_flag_update *updates;
+ unsigned int idx, count;
+
+ updates = array_get(&t->updates, &count);
+ i_assert(left_idx <= right_idx && right_idx <= count);
+ i_assert(count < INT_MAX);
+
+ /* find the first update with either overlapping range,
+ or the update which will come after our insert */
+ idx = left_idx;
+ while (left_idx < right_idx) {
+ idx = (left_idx + right_idx) / 2;
+
+ if (updates[idx].uid2 < seq)
+ left_idx = idx+1;
+ else if (updates[idx].uid1 > seq)
+ right_idx = idx;
+ else
+ break;
+ }
+ if (left_idx > idx)
+ idx++;
+ return idx;
+}
+
+static void
+mail_index_insert_flag_update(struct mail_index_transaction *t,
+ struct mail_index_flag_update u,
+ unsigned int idx)
+{
+ struct mail_index_flag_update *updates, tmp_update;
+ unsigned int count, first_idx, max;
+
+ updates = array_get_modifiable(&t->updates, &count);
+
+ /* overlapping ranges, split/merge them */
+ i_assert(idx == 0 || updates[idx-1].uid2 < u.uid1);
+ i_assert(idx == count || updates[idx].uid2 >= u.uid1);
+
+ /* first we'll just add the changes without trying to merge anything */
+ first_idx = idx;
+ for (; idx < count && u.uid2 >= updates[idx].uid1; idx++) {
+ i_assert(u.uid1 <= updates[idx].uid2);
+ if (u.uid1 != updates[idx].uid1 &&
+ (updates[idx].add_flags != u.add_flags ||
+ updates[idx].remove_flags != u.remove_flags)) {
+ if (u.uid1 < updates[idx].uid1) {
+ /* insert new update */
+ tmp_update = u;
+ tmp_update.uid2 = updates[idx].uid1 - 1;
+ } else {
+ /* split existing update from beginning */
+ tmp_update = updates[idx];
+ tmp_update.uid2 = u.uid1 - 1;
+ updates[idx].uid1 = u.uid1;
+ }
+
+ i_assert(tmp_update.uid1 <= tmp_update.uid2);
+ i_assert(updates[idx].uid1 <= updates[idx].uid2);
+
+ array_insert(&t->updates, idx, &tmp_update, 1);
+ updates = array_get_modifiable(&t->updates, &count);
+ idx++;
+ } else if (u.uid1 < updates[idx].uid1) {
+ updates[idx].uid1 = u.uid1;
+ }
+
+ if (u.uid2 < updates[idx].uid2 &&
+ (updates[idx].add_flags != u.add_flags ||
+ updates[idx].remove_flags != u.remove_flags)) {
+ /* split existing update from end */
+ tmp_update = updates[idx];
+ tmp_update.uid2 = u.uid2;
+ updates[idx].uid1 = u.uid2 + 1;
+
+ i_assert(tmp_update.uid1 <= tmp_update.uid2);
+ i_assert(updates[idx].uid1 <= updates[idx].uid2);
+
+ array_insert(&t->updates, idx, &tmp_update, 1);
+ updates = array_get_modifiable(&t->updates, &count);
+ }
+
+ updates[idx].add_flags =
+ (updates[idx].add_flags | u.add_flags) &
+ ENUM_NEGATE(u.remove_flags);
+ updates[idx].remove_flags =
+ (updates[idx].remove_flags | u.remove_flags) &
+ ENUM_NEGATE(u.add_flags);
+ u.uid1 = updates[idx].uid2 + 1;
+
+ if (updates[idx].add_flags == 0 &&
+ updates[idx].remove_flags == 0) {
+ /* we can remove this update completely */
+ array_delete(&t->updates, idx, 1);
+ updates = array_get_modifiable(&t->updates, &count);
+ }
+
+ if (u.uid1 > u.uid2) {
+ /* break here before idx++ so last_update_idx is set
+ correctly */
+ break;
+ }
+ }
+ i_assert(idx <= count);
+
+ if (u.uid1 <= u.uid2) {
+ i_assert(idx == 0 || updates[idx-1].uid2 < u.uid1);
+ i_assert(idx == count || updates[idx].uid1 > u.uid2);
+ array_insert(&t->updates, idx, &u, 1);
+ }
+ updates = array_get_modifiable(&t->updates, &count);
+ t->last_update_idx = idx == count ? count-1 : idx;
+
+ /* merge everything */
+ idx = first_idx == 0 ? 0 : first_idx - 1;
+ max = count == 0 ? 0 : I_MIN(t->last_update_idx + 1, count-1);
+ for (; idx < max; ) {
+ if (updates[idx].uid2 + 1 == updates[idx+1].uid1 &&
+ updates[idx].add_flags == updates[idx+1].add_flags &&
+ updates[idx].remove_flags == updates[idx+1].remove_flags) {
+ /* merge */
+ updates[idx].uid2 = updates[idx+1].uid2;
+ array_delete(&t->updates, idx + 1, 1);
+ max--;
+ if (t->last_update_idx > idx)
+ t->last_update_idx--;
+ updates = array_get_modifiable(&t->updates, &count);
+ } else {
+ idx++;
+ }
+ }
+}
+
+static void mail_index_record_modify_flags(struct mail_index_record *rec,
+ enum modify_type modify_type,
+ enum mail_flags flags)
+{
+ switch (modify_type) {
+ case MODIFY_REPLACE:
+ rec->flags = flags;
+ break;
+ case MODIFY_ADD:
+ rec->flags |= flags;
+ break;
+ case MODIFY_REMOVE:
+ rec->flags &= ENUM_NEGATE(flags);
+ break;
+ }
+}
+
+void mail_index_update_flags_range(struct mail_index_transaction *t,
+ uint32_t seq1, uint32_t seq2,
+ enum modify_type modify_type,
+ enum mail_flags flags)
+{
+ struct mail_index_record *rec;
+ struct mail_index_flag_update u, *last_update;
+ unsigned int idx, first_idx, count;
+
+ update_minmax_flagupdate_seq(t, seq1, seq2);
+ if (seq2 >= t->first_new_seq) {
+ /* updates for appended messages, modify them directly */
+ uint32_t seq;
+
+ for (seq = I_MAX(t->first_new_seq, seq1); seq <= seq2; seq++) {
+ rec = mail_index_transaction_lookup(t, seq);
+ mail_index_record_modify_flags(rec, modify_type, flags);
+ }
+ if (seq1 >= t->first_new_seq)
+ return;
+
+ /* range contains also existing messages. update them next. */
+ seq2 = t->first_new_seq - 1;
+ }
+
+ i_assert(seq1 <= seq2 && seq1 > 0);
+ i_assert(seq2 <= mail_index_view_get_messages_count(t->view));
+
+ if ((t->flags & MAIL_INDEX_TRANSACTION_FLAG_AVOID_FLAG_UPDATES) != 0)
+ t->drop_unnecessary_flag_updates = TRUE;
+
+ i_zero(&u);
+ u.uid1 = seq1;
+ u.uid2 = seq2;
+
+ switch (modify_type) {
+ case MODIFY_REPLACE:
+ u.add_flags = flags;
+ u.remove_flags = ENUM_NEGATE(flags) & MAIL_INDEX_FLAGS_MASK;
+ break;
+ case MODIFY_ADD:
+ if (flags == 0)
+ return;
+ u.add_flags = flags;
+ break;
+ case MODIFY_REMOVE:
+ if (flags == 0)
+ return;
+ u.remove_flags = flags;
+ break;
+ }
+
+ if (!array_is_created(&t->updates)) {
+ i_array_init(&t->updates, 256);
+ array_push_back(&t->updates, &u);
+ return;
+ }
+
+ last_update = array_get_modifiable(&t->updates, &count);
+ if (t->last_update_idx < count) {
+ /* fast path - hopefully we're updating the next message,
+ or a message that is to be appended as last update */
+ last_update += t->last_update_idx;
+ if (seq1 - 1 == last_update->uid2) {
+ if (u.add_flags == last_update->add_flags &&
+ u.remove_flags == last_update->remove_flags &&
+ (t->last_update_idx + 1 == count ||
+ last_update[1].uid1 > seq2)) {
+ /* we can just update the UID range */
+ last_update->uid2 = seq2;
+ return;
+ }
+ } else if (seq1 > last_update->uid2) {
+ /* hopefully we can just append it */
+ t->last_update_idx++;
+ last_update++;
+ }
+ }
+
+ if (t->last_update_idx == count)
+ array_push_back(&t->updates, &u);
+ else {
+ i_assert(t->last_update_idx < count);
+
+ /* slow path */
+ if (seq1 > last_update->uid2) {
+ /* added after this */
+ first_idx = t->last_update_idx + 1;
+ } else {
+ /* added before this or on top of this */
+ first_idx = 0;
+ count = t->last_update_idx + 1;
+ }
+ idx = mail_index_transaction_get_flag_update_pos(t, first_idx,
+ count, u.uid1);
+ mail_index_insert_flag_update(t, u, idx);
+ }
+}
+
+void mail_index_update_flags(struct mail_index_transaction *t, uint32_t seq,
+ enum modify_type modify_type,
+ enum mail_flags flags)
+{
+ mail_index_update_flags_range(t, seq, seq, modify_type, flags);
+}
+
+static void
+mail_index_attribute_set_full(struct mail_index_transaction *t,
+ const char *key, bool pvt, char prefix,
+ time_t timestamp, uint32_t value_len)
+{
+ uint32_t ts = timestamp;
+
+ if (t->attribute_updates == NULL) {
+ t->attribute_updates = buffer_create_dynamic(default_pool, 64);
+ t->attribute_updates_suffix = buffer_create_dynamic(default_pool, 64);
+ }
+ buffer_append_c(t->attribute_updates, prefix);
+ buffer_append_c(t->attribute_updates, pvt ? 'p' : 's');
+ buffer_append(t->attribute_updates, key, strlen(key)+1);
+
+ buffer_append(t->attribute_updates_suffix, &ts, sizeof(ts));
+ if (prefix == '+') {
+ buffer_append(t->attribute_updates_suffix,
+ &value_len, sizeof(value_len));
+ }
+ t->log_updates = TRUE;
+}
+
+void mail_index_attribute_set(struct mail_index_transaction *t,
+ bool pvt, const char *key,
+ time_t timestamp, uint32_t value_len)
+{
+ mail_index_attribute_set_full(t, key, pvt, '+', timestamp, value_len);
+}
+
+void mail_index_attribute_unset(struct mail_index_transaction *t,
+ bool pvt, const char *key,
+ time_t timestamp)
+{
+ mail_index_attribute_set_full(t, key, pvt, '-', timestamp, 0);
+}
+
+void mail_index_update_header(struct mail_index_transaction *t,
+ size_t offset, const void *data, size_t size,
+ bool prepend)
+{
+ i_assert(offset < sizeof(t->pre_hdr_change));
+ i_assert(size <= sizeof(t->pre_hdr_change) - offset);
+
+ t->log_updates = TRUE;
+
+ if (prepend) {
+ t->pre_hdr_changed = TRUE;
+ memcpy(t->pre_hdr_change + offset, data, size);
+ for (; size > 0; size--)
+ t->pre_hdr_mask[offset++] = 1;
+ } else {
+ t->post_hdr_changed = TRUE;
+ memcpy(t->post_hdr_change + offset, data, size);
+ for (; size > 0; size--)
+ t->post_hdr_mask[offset++] = 1;
+ }
+}
+
+static void
+mail_index_ext_rec_updates_resize(struct mail_index_transaction *t,
+ uint32_t ext_id, uint16_t new_record_size)
+{
+ ARRAY_TYPE(seq_array) *array, old_array;
+ unsigned int i;
+
+ if (!array_is_created(&t->ext_rec_updates))
+ return;
+ array = array_idx_modifiable(&t->ext_rec_updates, ext_id);
+ if (!array_is_created(array))
+ return;
+
+ old_array = *array;
+ i_zero(array);
+ mail_index_seq_array_alloc(array, new_record_size);
+
+ /* copy the records' beginnings. leave the end zero-filled. */
+ for (i = 0; i < array_count(&old_array); i++) {
+ const void *old_record = array_idx(&old_array, i);
+
+ memcpy(array_append_space(array), old_record,
+ old_array.arr.element_size);
+ }
+ array_free(&old_array);
+}
+
+void mail_index_ext_resize(struct mail_index_transaction *t, uint32_t ext_id,
+ uint32_t hdr_size, uint16_t record_size,
+ uint16_t record_align)
+{
+ const struct mail_index_registered_ext *rext;
+ const struct mail_transaction_ext_intro *resizes;
+ unsigned int resizes_count;
+ struct mail_transaction_ext_intro intro;
+ uint32_t old_record_size = 0, old_record_align, old_header_size;
+
+ i_zero(&intro);
+ rext = array_idx(&t->view->index->extensions, ext_id);
+
+ /* get ext_id from transaction's map if it's there */
+ if (!mail_index_map_get_ext_idx(t->view->map, ext_id, &intro.ext_id)) {
+ /* have to create it */
+ intro.ext_id = (uint32_t)-1;
+ old_record_align = rext->record_align;
+ old_header_size = rext->hdr_size;
+ } else {
+ const struct mail_index_ext *ext;
+
+ ext = array_idx(&t->view->map->extensions, intro.ext_id);
+ old_record_align = ext->record_align;
+ old_header_size = ext->hdr_size;
+ }
+
+ /* get the record size. if there are any existing record updates,
+ they're using the registered size, not the map's existing
+ record_size. */
+ if (array_is_created(&t->ext_resizes))
+ resizes = array_get(&t->ext_resizes, &resizes_count);
+ else {
+ resizes = NULL;
+ resizes_count = 0;
+ }
+ if (ext_id < resizes_count && resizes[ext_id].name_size != 0) {
+ /* already resized once. use the resized value. */
+ old_record_size = resizes[ext_id].record_size;
+ } else {
+ /* use the registered values. */
+ old_record_size = rext->record_size;
+ }
+
+ if (record_size != old_record_size && record_size != (uint16_t)-1) {
+ /* if record_size grows, we'll just resize the existing
+ ext_rec_updates array. it's not possible to shrink
+ record_size without data loss. */
+ i_assert(record_size > old_record_size);
+ mail_index_ext_rec_updates_resize(t, ext_id, record_size);
+ }
+
+ t->log_ext_updates = TRUE;
+
+ if (!array_is_created(&t->ext_resizes))
+ i_array_init(&t->ext_resizes, ext_id + 2);
+
+ intro.hdr_size = hdr_size != (uint32_t)-1 ? hdr_size : old_header_size;
+ if (record_size != (uint16_t)-1) {
+ i_assert(record_align != (uint16_t)-1);
+ intro.record_size = record_size;
+ intro.record_align = record_align;
+ } else {
+ i_assert(record_align == (uint16_t)-1);
+ intro.record_size = old_record_size;
+ intro.record_align = old_record_align;
+ }
+ intro.name_size = 1;
+ array_idx_set(&t->ext_resizes, ext_id, &intro);
+}
+
+void mail_index_ext_resize_hdr(struct mail_index_transaction *t,
+ uint32_t ext_id, uint32_t hdr_size)
+{
+ mail_index_ext_resize(t, ext_id, hdr_size, (uint16_t)-1, (uint16_t)-1);
+}
+
+void mail_index_ext_reset(struct mail_index_transaction *t, uint32_t ext_id,
+ uint32_t reset_id, bool clear_data)
+{
+ struct mail_transaction_ext_reset reset;
+
+ i_assert(reset_id != 0);
+
+ i_zero(&reset);
+ reset.new_reset_id = reset_id;
+ reset.preserve_data = clear_data ? 0 : 1;
+
+ mail_index_ext_set_reset_id(t, ext_id, reset_id);
+
+ if (!array_is_created(&t->ext_resets))
+ i_array_init(&t->ext_resets, ext_id + 2);
+ array_idx_set(&t->ext_resets, ext_id, &reset);
+ t->log_ext_updates = TRUE;
+}
+
+void mail_index_ext_reset_inc(struct mail_index_transaction *t, uint32_t ext_id,
+ uint32_t prev_reset_id, bool clear_data)
+{
+ uint32_t expected_reset_id = prev_reset_id + 1;
+
+ mail_index_ext_reset(t, ext_id, (uint32_t)-1, clear_data);
+
+ if (!array_is_created(&t->ext_reset_atomic))
+ i_array_init(&t->ext_reset_atomic, ext_id + 2);
+ array_idx_set(&t->ext_reset_atomic, ext_id, &expected_reset_id);
+}
+
+static bool
+mail_index_transaction_has_ext_updates(const ARRAY_TYPE(seq_array_array) *arr)
+{
+ const ARRAY_TYPE(seq_array) *array;
+
+ if (array_is_created(arr)) {
+ array_foreach(arr, array) {
+ if (array_is_created(array))
+ return TRUE;
+ }
+ }
+ return FALSE;
+}
+
+static bool
+mail_index_transaction_has_ext_changes(struct mail_index_transaction *t)
+{
+ if (mail_index_transaction_has_ext_updates(&t->ext_rec_updates))
+ return TRUE;
+ if (mail_index_transaction_has_ext_updates(&t->ext_rec_atomics))
+ return TRUE;
+
+ if (array_is_created(&t->ext_hdr_updates)) {
+ const struct mail_index_transaction_ext_hdr_update *hdr;
+
+ array_foreach(&t->ext_hdr_updates, hdr) {
+ if (hdr->alloc_size > 0)
+ return TRUE;
+ }
+ }
+ if (array_is_created(&t->ext_resets)) {
+ const struct mail_transaction_ext_reset *reset;
+
+ array_foreach(&t->ext_resets, reset) {
+ if (reset->new_reset_id != 0)
+ return TRUE;
+ }
+ }
+ if (array_is_created(&t->ext_resizes)) {
+ const struct mail_transaction_ext_intro *resize;
+
+ array_foreach(&t->ext_resizes, resize) {
+ if (resize->name_size > 0)
+ return TRUE;
+ }
+ }
+ return FALSE;
+}
+
+static void
+mail_index_ext_update_reset(ARRAY_TYPE(seq_array_array) *arr, uint32_t ext_id)
+{
+ if (array_is_created(arr) && ext_id < array_count(arr)) {
+ /* if extension records have been updated, clear them */
+ ARRAY_TYPE(seq_array) *array;
+
+ array = array_idx_modifiable(arr, ext_id);
+ if (array_is_created(array))
+ array_clear(array);
+ }
+}
+
+static void
+mail_index_ext_reset_changes(struct mail_index_transaction *t, uint32_t ext_id)
+{
+ mail_index_ext_update_reset(&t->ext_rec_updates, ext_id);
+ mail_index_ext_update_reset(&t->ext_rec_atomics, ext_id);
+ if (array_is_created(&t->ext_hdr_updates) &&
+ ext_id < array_count(&t->ext_hdr_updates)) {
+ /* if extension headers have been updated, clear them */
+ struct mail_index_transaction_ext_hdr_update *hdr;
+
+ hdr = array_idx_modifiable(&t->ext_hdr_updates, ext_id);
+ if (hdr->alloc_size > 0) {
+ i_free_and_null(hdr->mask);
+ i_free_and_null(hdr->data);
+ }
+ hdr->alloc_size = 0;
+ }
+ if (array_is_created(&t->ext_resets) &&
+ ext_id < array_count(&t->ext_resets)) {
+ /* clear resets */
+ array_idx_clear(&t->ext_resets, ext_id);
+ }
+ if (array_is_created(&t->ext_resizes) &&
+ ext_id < array_count(&t->ext_resizes)) {
+ /* clear resizes */
+ array_idx_clear(&t->ext_resizes, ext_id);
+ }
+
+ t->log_ext_updates = mail_index_transaction_has_ext_changes(t);
+}
+
+void mail_index_ext_using_reset_id(struct mail_index_transaction *t,
+ uint32_t ext_id, uint32_t reset_id)
+{
+ uint32_t *reset_id_p;
+ bool changed;
+
+ if (!array_is_created(&t->ext_reset_ids))
+ i_array_init(&t->ext_reset_ids, ext_id + 2);
+ reset_id_p = array_idx_get_space(&t->ext_reset_ids, ext_id);
+ changed = *reset_id_p != reset_id && *reset_id_p != 0;
+ *reset_id_p = reset_id;
+ if (changed) {
+ /* reset_id changed, clear existing changes */
+ mail_index_ext_reset_changes(t, ext_id);
+ }
+}
+
+void mail_index_ext_set_reset_id(struct mail_index_transaction *t,
+ uint32_t ext_id, uint32_t reset_id)
+{
+ mail_index_ext_using_reset_id(t, ext_id, reset_id);
+ /* make sure the changes get reset, even if reset_id doesn't change */
+ mail_index_ext_reset_changes(t, ext_id);
+}
+
+void mail_index_update_header_ext(struct mail_index_transaction *t,
+ uint32_t ext_id, size_t offset,
+ const void *data, size_t size)
+{
+ struct mail_index_transaction_ext_hdr_update *hdr;
+ size_t new_size;
+
+ i_assert(offset <= (uint32_t)-1 && size <= (uint32_t)-1 &&
+ offset + size <= (uint32_t)-1);
+
+ if (!array_is_created(&t->ext_hdr_updates))
+ i_array_init(&t->ext_hdr_updates, ext_id + 2);
+
+ hdr = array_idx_get_space(&t->ext_hdr_updates, ext_id);
+ if (hdr->alloc_size < offset || hdr->alloc_size - offset < size) {
+ i_assert(size < SIZE_MAX - offset);
+ new_size = nearest_power(offset + size);
+ hdr->mask = i_realloc(hdr->mask, hdr->alloc_size, new_size);
+ hdr->data = i_realloc(hdr->data, hdr->alloc_size, new_size);
+ hdr->alloc_size = new_size;
+ }
+ memset(hdr->mask + offset, 1, size);
+ memcpy(hdr->data + offset, data, size);
+
+ t->log_ext_updates = TRUE;
+}
+
+void mail_index_update_ext(struct mail_index_transaction *t, uint32_t seq,
+ uint32_t ext_id, const void *data, void *old_data_r)
+{
+ struct mail_index *index = t->view->index;
+ const struct mail_index_registered_ext *rext;
+ const struct mail_transaction_ext_intro *intro;
+ uint16_t record_size;
+ ARRAY_TYPE(seq_array) *array;
+ unsigned int count;
+
+ i_assert(seq > 0 &&
+ (seq <= mail_index_view_get_messages_count(t->view) ||
+ seq <= t->last_new_seq));
+ i_assert(ext_id < array_count(&index->extensions));
+
+ t->log_ext_updates = TRUE;
+
+ if (!array_is_created(&t->ext_resizes)) {
+ intro = NULL;
+ count = 0;
+ } else {
+ intro = array_get(&t->ext_resizes, &count);
+ }
+ if (ext_id < count && intro[ext_id].name_size != 0) {
+ /* resized record */
+ record_size = intro[ext_id].record_size;
+ } else {
+ rext = array_idx(&index->extensions, ext_id);
+ record_size = rext->record_size;
+ }
+ i_assert(record_size > 0);
+
+ if (!array_is_created(&t->ext_rec_updates))
+ i_array_init(&t->ext_rec_updates, ext_id + 2);
+ array = array_idx_get_space(&t->ext_rec_updates, ext_id);
+
+ /* @UNSAFE */
+ if (!mail_index_seq_array_add(array, seq, data, record_size,
+ old_data_r)) {
+ /* not found, clear old_data if it was given */
+ if (old_data_r != NULL)
+ memset(old_data_r, 0, record_size);
+ }
+}
+
+int mail_index_atomic_inc_ext(struct mail_index_transaction *t,
+ uint32_t seq, uint32_t ext_id, int diff)
+{
+ ARRAY_TYPE(seq_array) *array;
+ int32_t old_diff32, diff32 = diff;
+
+ i_assert(seq > 0 &&
+ (seq <= mail_index_view_get_messages_count(t->view) ||
+ seq <= t->last_new_seq));
+ i_assert(ext_id < array_count(&t->view->index->extensions));
+ /* currently non-external transactions can be applied multiple times,
+ causing multiple increments. FIXME: we need this now and it doesn't
+ actually seem to be a real problem at least right now - why? */
+ /*i_assert((t->flags & MAIL_INDEX_TRANSACTION_FLAG_EXTERNAL) != 0);*/
+
+ t->log_ext_updates = TRUE;
+ if (!array_is_created(&t->ext_rec_atomics))
+ i_array_init(&t->ext_rec_atomics, ext_id + 2);
+ array = array_idx_get_space(&t->ext_rec_atomics, ext_id);
+ if (mail_index_seq_array_add(array, seq, &diff32, sizeof(diff32),
+ &old_diff32)) {
+ /* already incremented this sequence in this transaction */
+ diff32 += old_diff32;
+ (void)mail_index_seq_array_add(array, seq, &diff32,
+ sizeof(diff32), NULL);
+ }
+ return diff32;
+}
+
+static bool
+keyword_update_has_changes(struct mail_index_transaction *t, uint32_t seq,
+ enum modify_type modify_type,
+ struct mail_keywords *keywords)
+{
+ ARRAY_TYPE(keyword_indexes) existing;
+ const unsigned int *existing_idx;
+ unsigned int i, j, existing_count;
+ bool found;
+
+ t_array_init(&existing, 32);
+ if (seq < t->first_new_seq)
+ mail_index_transaction_lookup_latest_keywords(t, seq, &existing);
+ existing_idx = array_get(&existing, &existing_count);
+
+ if (modify_type == MODIFY_REPLACE && existing_count != keywords->count)
+ return TRUE;
+
+ for (i = 0; i < keywords->count; i++) {
+ found = FALSE;
+ for (j = 0; j < existing_count; j++) {
+ if (existing_idx[j] == keywords->idx[i]) {
+ found = TRUE;
+ break;
+ }
+ }
+ switch (modify_type) {
+ case MODIFY_ADD:
+ case MODIFY_REPLACE:
+ if (!found)
+ return TRUE;
+ break;
+ case MODIFY_REMOVE:
+ if (found)
+ return TRUE;
+ break;
+ }
+ }
+ return FALSE;
+}
+
+static struct mail_keywords *
+keyword_update_remove_existing(struct mail_index_transaction *t, uint32_t seq)
+{
+ ARRAY_TYPE(keyword_indexes) keywords;
+ uint32_t i, keywords_count;
+
+ t_array_init(&keywords, 32);
+ if (t->view->v.lookup_full == NULL) {
+ /* syncing is saving a list of changes into this transaction.
+ the seq is actual an uid, so we can't lookup the existing
+ keywords. we shouldn't get here unless we're reading
+ pre-v2.2 keyword-reset records from .log files. so we don't
+ really care about performance that much here, */
+ keywords_count = array_count(&t->view->index->keywords);
+ for (i = 0; i < keywords_count; i++)
+ array_push_back(&keywords, &i);
+ } else {
+ mail_index_transaction_lookup_latest_keywords(t, seq, &keywords);
+ }
+ if (array_count(&keywords) == 0)
+ return NULL;
+ return mail_index_keywords_create_from_indexes(t->view->index,
+ &keywords);
+}
+
+void mail_index_update_keywords(struct mail_index_transaction *t, uint32_t seq,
+ enum modify_type modify_type,
+ struct mail_keywords *keywords)
+{
+ struct mail_index_transaction_keyword_update *u;
+ struct mail_keywords *add_keywords = NULL, *remove_keywords = NULL;
+ struct mail_keywords *unref_keywords = NULL;
+ unsigned int i;
+ bool changed;
+
+ i_assert(seq > 0 &&
+ (seq <= mail_index_view_get_messages_count(t->view) ||
+ seq <= t->last_new_seq));
+ i_assert(keywords->index == t->view->index);
+
+ if (keywords->count == 0 && modify_type != MODIFY_REPLACE)
+ return;
+
+ update_minmax_flagupdate_seq(t, seq, seq);
+
+ if (!array_is_created(&t->keyword_updates)) {
+ uint32_t max_idx = keywords->count == 0 ? 3 :
+ keywords->idx[keywords->count-1];
+
+ i_array_init(&t->keyword_updates, max_idx + 1);
+ }
+
+ if ((t->flags & MAIL_INDEX_TRANSACTION_FLAG_AVOID_FLAG_UPDATES) != 0) {
+ T_BEGIN {
+ changed = keyword_update_has_changes(t, seq,
+ modify_type,
+ keywords);
+ } T_END;
+ if (!changed)
+ return;
+ }
+
+ switch (modify_type) {
+ case MODIFY_REPLACE:
+ /* split this into add+remove. remove all existing keywords not
+ included in the keywords list */
+ if (seq < t->first_new_seq) {
+ /* remove the ones currently in index */
+ remove_keywords = keyword_update_remove_existing(t, seq);
+ unref_keywords = remove_keywords;
+ }
+ /* remove from all changes we've done in this transaction */
+ array_foreach_modifiable(&t->keyword_updates, u)
+ seq_range_array_remove(&u->add_seq, seq);
+ add_keywords = keywords;
+ break;
+ case MODIFY_ADD:
+ add_keywords = keywords;
+ break;
+ case MODIFY_REMOVE:
+ remove_keywords = keywords;
+ break;
+ }
+
+ /* Update add_seq and remove_seq arrays which describe the keyword
+ changes. First do the removes, since replace removes everything
+ first. */
+ if (remove_keywords != NULL) {
+ for (i = 0; i < remove_keywords->count; i++) {
+ u = array_idx_get_space(&t->keyword_updates,
+ remove_keywords->idx[i]);
+ seq_range_array_remove(&u->add_seq, seq);
+ /* Don't bother updating remove_seq for new messages,
+ since their initial state is "no keyword" anyway */
+ if (seq < t->first_new_seq) {
+ seq_range_array_add_with_init(&u->remove_seq,
+ 16, seq);
+ }
+ }
+ }
+ if (add_keywords != NULL) {
+ for (i = 0; i < add_keywords->count; i++) {
+ u = array_idx_get_space(&t->keyword_updates,
+ add_keywords->idx[i]);
+ seq_range_array_add_with_init(&u->add_seq, 16, seq);
+ seq_range_array_remove(&u->remove_seq, seq);
+ }
+ }
+ if (unref_keywords != NULL)
+ mail_index_keywords_unref(&unref_keywords);
+
+ t->log_updates = TRUE;
+}
+
+bool mail_index_cancel_flag_updates(struct mail_index_transaction *t,
+ uint32_t seq)
+{
+ struct mail_index_flag_update *updates, tmp_update;
+ unsigned int i, count;
+
+ if (!array_is_created(&t->updates))
+ return FALSE;
+
+ updates = array_get_modifiable(&t->updates, &count);
+ i = mail_index_transaction_get_flag_update_pos(t, 0, count, seq);
+ if (i == count)
+ return FALSE;
+ else {
+ i_assert(seq <= updates[i].uid2);
+ if (seq < updates[i].uid1)
+ return FALSE;
+ }
+
+ /* exists */
+ if (updates[i].uid1 == seq) {
+ if (updates[i].uid2 != seq)
+ updates[i].uid1++;
+ else if (count > 1)
+ array_delete(&t->updates, i, 1);
+ else
+ array_free(&t->updates);
+ } else if (updates[i].uid2 == seq) {
+ updates[i].uid2--;
+ } else {
+ /* need to split it in two */
+ tmp_update = updates[i];
+ tmp_update.uid1 = seq+1;
+ updates[i].uid2 = seq-1;
+ array_insert(&t->updates, i + 1, &tmp_update, 1);
+ }
+ return TRUE;
+}
+
+static bool mail_index_cancel_array(ARRAY_TYPE(seq_range) *array, uint32_t seq)
+{
+ if (array_is_created(array)) {
+ if (seq_range_array_remove(array, seq)) {
+ if (array_count(array) == 0)
+ array_free(array);
+ return TRUE;
+ }
+ }
+ return FALSE;
+}
+
+bool mail_index_cancel_keyword_updates(struct mail_index_transaction *t,
+ uint32_t seq)
+{
+ struct mail_index_transaction_keyword_update *kw;
+ bool ret = FALSE, have_kw_changes = FALSE;
+
+ if (!array_is_created(&t->keyword_updates))
+ return FALSE;
+
+ array_foreach_modifiable(&t->keyword_updates, kw) {
+ if (mail_index_cancel_array(&kw->add_seq, seq))
+ ret = TRUE;
+ if (mail_index_cancel_array(&kw->remove_seq, seq))
+ ret = TRUE;
+ if (array_is_created(&kw->add_seq) ||
+ array_is_created(&kw->remove_seq))
+ have_kw_changes = TRUE;
+ }
+ if (!have_kw_changes)
+ array_free(&t->keyword_updates);
+ return ret;
+}
+
+void mail_index_transaction_reset(struct mail_index_transaction *t)
+{
+ t->v.reset(t);
+}
+
+void mail_index_reset(struct mail_index_transaction *t)
+{
+ mail_index_transaction_reset(t);
+
+ t->reset = TRUE;
+}
+
+void mail_index_unset_fscked(struct mail_index_transaction *t)
+{
+ struct mail_index_header new_hdr =
+ *mail_index_get_header(t->view);
+
+ i_assert(t->view->index->log_sync_locked);
+
+ /* remove fsck'd-flag if it exists. */
+ if ((new_hdr.flags & MAIL_INDEX_HDR_FLAG_FSCKD) != 0) {
+ new_hdr.flags &= ENUM_NEGATE(MAIL_INDEX_HDR_FLAG_FSCKD);
+ mail_index_update_header(t,
+ offsetof(struct mail_index_header, flags),
+ &new_hdr.flags, sizeof(new_hdr.flags), FALSE);
+ }
+}
+
+void mail_index_set_deleted(struct mail_index_transaction *t)
+{
+ i_assert(!t->index_undeleted);
+
+ t->index_deleted = TRUE;
+}
+
+void mail_index_set_undeleted(struct mail_index_transaction *t)
+{
+ i_assert(!t->index_deleted);
+
+ t->index_undeleted = TRUE;
+}
+
+void mail_index_transaction_set_max_modseq(struct mail_index_transaction *t,
+ uint64_t max_modseq,
+ ARRAY_TYPE(seq_range) *seqs)
+{
+ i_assert(array_is_created(seqs));
+
+ t->max_modseq = max_modseq;
+ t->conflict_seqs = seqs;
+}
diff --git a/src/lib-index/mail-index-transaction-view.c b/src/lib-index/mail-index-transaction-view.c
new file mode 100644
index 0000000..240c7fe
--- /dev/null
+++ b/src/lib-index/mail-index-transaction-view.c
@@ -0,0 +1,534 @@
+/* Copyright (c) 2004-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "buffer.h"
+#include "seq-range-array.h"
+#include "mail-index-private.h"
+#include "mail-index-view-private.h"
+#include "mail-index-transaction-private.h"
+
+struct mail_index_view_transaction {
+ struct mail_index_view view;
+ struct mail_index_view_vfuncs *super;
+ struct mail_index_transaction *t;
+
+ struct mail_index_map *lookup_map;
+ struct mail_index_header hdr;
+
+ buffer_t *lookup_return_data;
+ uint32_t lookup_prev_seq;
+
+ unsigned int record_size;
+ unsigned int recs_count;
+ void *recs;
+ ARRAY(void *) all_recs;
+};
+
+static void tview_close(struct mail_index_view *view)
+{
+ struct mail_index_view_transaction *tview =
+ (struct mail_index_view_transaction *)view;
+ struct mail_index_transaction *t = tview->t;
+ void **recs;
+ unsigned int i, count;
+
+ if (tview->lookup_map != NULL)
+ mail_index_unmap(&tview->lookup_map);
+ buffer_free(&tview->lookup_return_data);
+
+ if (array_is_created(&tview->all_recs)) {
+ recs = array_get_modifiable(&tview->all_recs, &count);
+ for (i = 0; i < count; i++)
+ i_free(recs[i]);
+ array_free(&tview->all_recs);
+ }
+
+ tview->super->close(view);
+ mail_index_transaction_unref(&t);
+}
+
+static uint32_t tview_get_message_count(struct mail_index_view *view)
+{
+ struct mail_index_view_transaction *tview =
+ (struct mail_index_view_transaction *)view;
+
+ return view->map->hdr.messages_count +
+ (tview->t->last_new_seq == 0 ? 0 :
+ tview->t->last_new_seq - tview->t->first_new_seq + 1);
+}
+
+static const struct mail_index_header *
+tview_get_header(struct mail_index_view *view)
+{
+ struct mail_index_view_transaction *tview =
+ (struct mail_index_view_transaction *)view;
+ const struct mail_index_header *hdr;
+ uint32_t next_uid;
+
+ /* FIXME: header counters may not be correct */
+ hdr = tview->super->get_header(view);
+
+ next_uid = mail_index_transaction_get_next_uid(tview->t);
+ if (next_uid != hdr->next_uid) {
+ tview->hdr = *hdr;
+ tview->hdr.next_uid = next_uid;
+ hdr = &tview->hdr;
+ }
+ return hdr;
+}
+
+static const struct mail_index_record *
+tview_apply_flag_updates(struct mail_index_view_transaction *tview,
+ struct mail_index_map *map,
+ const struct mail_index_record *rec, uint32_t seq)
+{
+ struct mail_index_transaction *t = tview->t;
+ const struct mail_index_flag_update *updates;
+ struct mail_index_record *trec;
+ unsigned int idx, count;
+
+ /* see if there are any flag updates */
+ if (seq < t->min_flagupdate_seq || seq > t->max_flagupdate_seq ||
+ !array_is_created(&t->updates))
+ return rec;
+
+ updates = array_get(&t->updates, &count);
+ idx = mail_index_transaction_get_flag_update_pos(t, 0, count, seq);
+ if (seq < updates[idx].uid1 || seq > updates[idx].uid2)
+ return rec;
+
+ /* yes, we have flag updates. since we can't modify rec directly and
+ we want to be able to handle multiple mail_index_lookup() calls
+ without the second one overriding the first one's data, we'll
+ create a records array and return data from there.
+
+ it's also possible that the record size increases, so we potentially
+ have to create multiple arrays. they all get eventually freed when
+ the view gets freed. */
+ if (map->hdr.record_size > tview->record_size) {
+ if (!array_is_created(&tview->all_recs))
+ i_array_init(&tview->all_recs, 4);
+ tview->recs_count = t->first_new_seq;
+ tview->record_size = I_MAX(map->hdr.record_size,
+ tview->view.map->hdr.record_size);
+ tview->recs = i_malloc(MALLOC_MULTIPLY(tview->record_size,
+ tview->recs_count));
+ array_push_back(&tview->all_recs, &tview->recs);
+ }
+ i_assert(tview->recs_count == t->first_new_seq);
+ i_assert(seq > 0 && seq <= tview->recs_count);
+
+ trec = PTR_OFFSET(tview->recs, (seq-1) * tview->record_size);
+ memcpy(trec, rec, map->hdr.record_size);
+ trec->flags |= updates[idx].add_flags & 0xff;
+ trec->flags &= ENUM_NEGATE(updates[idx].remove_flags);
+ return trec;
+}
+
+static const struct mail_index_record *
+tview_lookup_full(struct mail_index_view *view, uint32_t seq,
+ struct mail_index_map **map_r, bool *expunged_r)
+{
+ struct mail_index_view_transaction *tview =
+ (struct mail_index_view_transaction *)view;
+ const struct mail_index_record *rec;
+
+ if (seq >= tview->t->first_new_seq) {
+ /* FIXME: is this right to return index map..?
+ it's not there yet. */
+ *map_r = view->index->map;
+ if (expunged_r != NULL)
+ *expunged_r = FALSE;
+ return mail_index_transaction_lookup(tview->t, seq);
+ }
+
+ rec = tview->super->lookup_full(view, seq, map_r, expunged_r);
+ rec = tview_apply_flag_updates(tview, *map_r, rec, seq);
+
+ if (expunged_r != NULL &&
+ mail_index_transaction_is_expunged(tview->t, seq))
+ *expunged_r = TRUE;
+ return rec;
+}
+
+static void
+tview_lookup_uid(struct mail_index_view *view, uint32_t seq, uint32_t *uid_r)
+{
+ struct mail_index_view_transaction *tview =
+ (struct mail_index_view_transaction *)view;
+
+ if (seq >= tview->t->first_new_seq)
+ *uid_r = mail_index_transaction_lookup(tview->t, seq)->uid;
+ else
+ tview->super->lookup_uid(view, seq, uid_r);
+}
+
+static void tview_lookup_seq_range(struct mail_index_view *view,
+ uint32_t first_uid, uint32_t last_uid,
+ uint32_t *first_seq_r, uint32_t *last_seq_r)
+{
+ struct mail_index_view_transaction *tview =
+ (struct mail_index_view_transaction *)view;
+ const struct mail_index_record *rec;
+ uint32_t seq;
+
+ if (!tview->t->reset) {
+ tview->super->lookup_seq_range(view, first_uid, last_uid,
+ first_seq_r, last_seq_r);
+ } else {
+ /* index is being reset. we never want to return old
+ sequences. */
+ *first_seq_r = *last_seq_r = 0;
+ }
+ if (tview->t->last_new_seq == 0) {
+ /* no new messages, the results are final. */
+ return;
+ }
+
+ rec = mail_index_transaction_lookup(tview->t, tview->t->first_new_seq);
+ if (rec->uid == 0) {
+ /* new messages don't have UIDs */
+ return;
+ }
+ if (last_uid < rec->uid) {
+ /* all wanted messages were existing */
+ return;
+ }
+
+ /* at least some of the wanted messages are newly created */
+ if (*first_seq_r == 0) {
+ seq = tview->t->first_new_seq;
+ for (; seq <= tview->t->last_new_seq; seq++) {
+ rec = mail_index_transaction_lookup(tview->t, seq);
+ if (first_uid <= rec->uid)
+ break;
+ }
+ if (seq > tview->t->last_new_seq || rec->uid > last_uid) {
+ /* no messages in range */
+ return;
+ }
+ *first_seq_r = seq;
+
+ if (rec->uid == last_uid) {
+ /* one seq in range */
+ *last_seq_r = seq;
+ return;
+ }
+ }
+
+ seq = tview->t->last_new_seq;
+ for (; seq >= tview->t->first_new_seq; seq--) {
+ rec = mail_index_transaction_lookup(tview->t, seq);
+ if (rec->uid <= last_uid) {
+ *last_seq_r = seq;
+ break;
+ }
+ }
+ i_assert(seq >= tview->t->first_new_seq);
+}
+
+static void tview_lookup_first(struct mail_index_view *view,
+ enum mail_flags flags, uint8_t flags_mask,
+ uint32_t *seq_r)
+{
+ struct mail_index_view_transaction *tview =
+ (struct mail_index_view_transaction *)view;
+ const struct mail_index_record *rec;
+ unsigned int append_count;
+ uint32_t seq, message_count;
+
+ if (!tview->t->reset) {
+ tview->super->lookup_first(view, flags, flags_mask, seq_r);
+ if (*seq_r != 0)
+ return;
+ } else {
+ *seq_r = 0;
+ }
+
+ rec = array_get(&tview->t->appends, &append_count);
+ seq = tview->t->first_new_seq;
+ message_count = tview->t->last_new_seq;
+ i_assert(append_count == message_count - seq + 1);
+
+ for (; seq <= message_count; seq++, rec++) {
+ if ((rec->flags & flags_mask) == (uint8_t)flags) {
+ *seq_r = seq;
+ break;
+ }
+ }
+}
+
+static void keyword_index_add(ARRAY_TYPE(keyword_indexes) *keywords,
+ unsigned int idx)
+{
+ const unsigned int *indexes;
+ unsigned int i, count;
+
+ indexes = array_get(keywords, &count);
+ for (i = 0; i < count; i++) {
+ if (indexes[i] == idx)
+ return;
+ }
+ array_push_back(keywords, &idx);
+}
+
+static void keyword_index_remove(ARRAY_TYPE(keyword_indexes) *keywords,
+ unsigned int idx)
+{
+ const unsigned int *indexes;
+ unsigned int i, count;
+
+ indexes = array_get(keywords, &count);
+ for (i = 0; i < count; i++) {
+ if (indexes[i] == idx) {
+ array_delete(keywords, i, 1);
+ break;
+ }
+ }
+}
+
+static void tview_lookup_keywords(struct mail_index_view *view, uint32_t seq,
+ ARRAY_TYPE(keyword_indexes) *keyword_idx)
+{
+ struct mail_index_view_transaction *tview =
+ (struct mail_index_view_transaction *)view;
+ struct mail_index_transaction *t = tview->t;
+ const struct mail_index_transaction_keyword_update *updates;
+ unsigned int i, count;
+
+ tview->super->lookup_keywords(view, seq, keyword_idx);
+
+ if (seq < t->min_flagupdate_seq || seq > t->max_flagupdate_seq) {
+ /* no keyword updates for this sequence */
+ return;
+ }
+
+ if (array_is_created(&t->keyword_updates))
+ updates = array_get(&t->keyword_updates, &count);
+ else {
+ updates = NULL;
+ count = 0;
+ }
+ for (i = 0; i < count; i++) {
+ if (array_is_created(&updates[i].add_seq) &&
+ seq_range_exists(&updates[i].add_seq, seq))
+ keyword_index_add(keyword_idx, i);
+ else if (array_is_created(&updates[i].remove_seq) &&
+ seq_range_exists(&updates[i].remove_seq, seq))
+ keyword_index_remove(keyword_idx, i);
+ }
+}
+
+static const void *
+tview_return_updated_ext(struct mail_index_view_transaction *tview,
+ uint32_t seq, const void *data, uint32_t ext_id)
+{
+ const struct mail_index_ext *ext;
+ const struct mail_index_registered_ext *rext;
+ const struct mail_transaction_ext_intro *intro;
+ unsigned int record_align, record_size;
+ uint32_t ext_idx;
+ size_t pos;
+
+ /* data begins with a 32bit sequence, followed by the actual
+ extension data */
+ data = CONST_PTR_OFFSET(data, sizeof(uint32_t));
+
+ if (!mail_index_map_get_ext_idx(tview->lookup_map, ext_id, &ext_idx)) {
+ /* we're adding the extension now. */
+ rext = array_idx(&tview->view.index->extensions, ext_id);
+ record_align = rext->record_align;
+ record_size = rext->record_size;
+ } else {
+ ext = array_idx(&tview->lookup_map->extensions, ext_idx);
+ record_align = ext->record_align;
+ record_size = ext->record_size;
+ }
+
+ /* see if the extension has been resized within this transaction */
+ if (array_is_created(&tview->t->ext_resizes) &&
+ ext_id < array_count(&tview->t->ext_resizes)) {
+ intro = array_idx(&tview->t->ext_resizes, ext_id);
+ if (intro[ext_id].name_size != 0) {
+ record_align = intro->record_align;
+ record_size = intro->record_size;
+ }
+ }
+
+ if (record_align <= sizeof(uint32_t)) {
+ /* data is 32bit aligned already */
+ return data;
+ } else {
+ /* assume we want 64bit alignment - copy the data to
+ temporary buffer and return it */
+ if (tview->lookup_return_data == NULL) {
+ tview->lookup_return_data =
+ buffer_create_dynamic(default_pool,
+ record_size + 64);
+ } else if (seq != tview->lookup_prev_seq) {
+ /* clear the buffer between lookups for different
+ messages */
+ buffer_set_used_size(tview->lookup_return_data, 0);
+ }
+ tview->lookup_prev_seq = seq;
+ pos = tview->lookup_return_data->used;
+ buffer_append(tview->lookup_return_data, data, record_size);
+ return CONST_PTR_OFFSET(tview->lookup_return_data->data, pos);
+ }
+}
+
+static bool
+tview_is_ext_reset(struct mail_index_view_transaction *tview, uint32_t ext_id)
+{
+ const struct mail_transaction_ext_reset *resets;
+ unsigned int count;
+
+ if (!array_is_created(&tview->t->ext_resets))
+ return FALSE;
+
+ resets = array_get(&tview->t->ext_resets, &count);
+ return ext_id < count && resets[ext_id].new_reset_id != 0;
+}
+
+static bool
+tview_lookup_ext_update(struct mail_index_view_transaction *tview, uint32_t seq,
+ uint32_t ext_id, struct mail_index_map **map_r,
+ const void **data_r)
+{
+ const ARRAY_TYPE(seq_array) *ext_buf;
+ const void *data;
+ unsigned int idx;
+ uint32_t map_ext_idx;
+
+ ext_buf = array_idx(&tview->t->ext_rec_updates, ext_id);
+ if (!array_is_created(ext_buf) ||
+ !mail_index_seq_array_lookup(ext_buf, seq, &idx))
+ return FALSE;
+
+ if (tview->lookup_map == NULL) {
+ tview->lookup_map =
+ mail_index_map_clone(tview->view.index->map);
+ }
+ if (!mail_index_map_get_ext_idx(tview->lookup_map, ext_id, &map_ext_idx)) {
+ /* extension doesn't yet exist in the map. add it there with
+ the preliminary information (mainly its size) so if caller
+ looks it up, it's going to be found. */
+ const struct mail_index_registered_ext *rext =
+ array_idx(&tview->view.index->extensions, ext_id);
+ struct mail_index_ext_header ext_hdr;
+
+ i_zero(&ext_hdr);
+ ext_hdr.hdr_size = rext->hdr_size;
+ ext_hdr.record_size = ext_buf->arr.element_size - sizeof(uint32_t);
+ ext_hdr.record_align = rext->record_align;
+
+ mail_index_map_register_ext(tview->lookup_map, rext->name,
+ (uint32_t)-1, &ext_hdr);
+ }
+
+ data = array_idx(ext_buf, idx);
+ *map_r = tview->lookup_map;
+ *data_r = tview_return_updated_ext(tview, seq, data, ext_id);
+ return TRUE;
+}
+
+static void
+tview_lookup_ext_full(struct mail_index_view *view, uint32_t seq,
+ uint32_t ext_id, struct mail_index_map **map_r,
+ const void **data_r, bool *expunged_r)
+{
+ struct mail_index_view_transaction *tview =
+ (struct mail_index_view_transaction *)view;
+
+ i_assert(ext_id < array_count(&view->index->extensions));
+
+ if (expunged_r != NULL)
+ *expunged_r = FALSE;
+
+ if (array_is_created(&tview->t->ext_rec_updates) &&
+ ext_id < array_count(&tview->t->ext_rec_updates)) {
+ /* there are some ext updates in transaction.
+ see if there's any for this sequence. */
+ if (tview_lookup_ext_update(tview, seq, ext_id, map_r, data_r))
+ return;
+ }
+
+ /* not updated, return the existing value, unless ext was
+ already reset */
+ if (seq < tview->t->first_new_seq &&
+ !tview_is_ext_reset(tview, ext_id)) {
+ tview->super->lookup_ext_full(view, seq, ext_id,
+ map_r, data_r, expunged_r);
+ } else {
+ *map_r = view->index->map;
+ *data_r = NULL;
+ }
+}
+
+static void tview_get_header_ext(struct mail_index_view *view,
+ struct mail_index_map *map, uint32_t ext_id,
+ const void **data_r, size_t *data_size_r)
+{
+ struct mail_index_view_transaction *tview =
+ (struct mail_index_view_transaction *)view;
+
+ /* FIXME: check updates */
+ tview->super->get_header_ext(view, map, ext_id, data_r, data_size_r);
+}
+
+static bool tview_ext_get_reset_id(struct mail_index_view *view,
+ struct mail_index_map *map,
+ uint32_t ext_id, uint32_t *reset_id_r)
+{
+ struct mail_index_view_transaction *tview =
+ (struct mail_index_view_transaction *)view;
+ const uint32_t *reset_id_p;
+
+ if (array_is_created(&tview->t->ext_reset_ids) &&
+ ext_id < array_count(&tview->t->ext_reset_ids) &&
+ map == tview->lookup_map) {
+ reset_id_p = array_idx(&tview->t->ext_reset_ids, ext_id);
+ *reset_id_r = *reset_id_p;
+ return TRUE;
+ }
+
+ return tview->super->ext_get_reset_id(view, map, ext_id, reset_id_r);
+}
+
+static struct mail_index_view_vfuncs trans_view_vfuncs = {
+ tview_close,
+ tview_get_message_count,
+ tview_get_header,
+ tview_lookup_full,
+ tview_lookup_uid,
+ tview_lookup_seq_range,
+ tview_lookup_first,
+ tview_lookup_keywords,
+ tview_lookup_ext_full,
+ tview_get_header_ext,
+ tview_ext_get_reset_id
+};
+
+struct mail_index_view *
+mail_index_transaction_open_updated_view(struct mail_index_transaction *t)
+{
+ struct mail_index_view_transaction *tview;
+
+ if (t->view->syncing) {
+ /* transaction view is being synced. while it's done, it's not
+ possible to add new messages, but the view itself might
+ change. so we can't make a copy of the view. */
+ mail_index_view_ref(t->view);
+ return t->view;
+ }
+
+ tview = i_new(struct mail_index_view_transaction, 1);
+ mail_index_view_clone(&tview->view, t->view);
+ tview->view.v = trans_view_vfuncs;
+ tview->super = &t->view->v;
+ tview->t = t;
+
+ mail_index_transaction_ref(t);
+ return &tview->view;
+}
diff --git a/src/lib-index/mail-index-transaction.c b/src/lib-index/mail-index-transaction.c
new file mode 100644
index 0000000..0c61170
--- /dev/null
+++ b/src/lib-index/mail-index-transaction.c
@@ -0,0 +1,360 @@
+/* Copyright (c) 2003-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "ioloop.h"
+#include "array.h"
+#include "hook-build.h"
+#include "bsearch-insert-pos.h"
+#include "llist.h"
+#include "mail-index-private.h"
+#include "mail-transaction-log-private.h"
+#include "mail-index-transaction-private.h"
+
+static ARRAY(hook_mail_index_transaction_created_t *)
+ hook_mail_index_transaction_created;
+
+void mail_index_transaction_hook_register(hook_mail_index_transaction_created_t *hook)
+{
+ if (!array_is_created(&hook_mail_index_transaction_created))
+ i_array_init(&hook_mail_index_transaction_created, 8);
+ array_push_back(&hook_mail_index_transaction_created, &hook);
+}
+
+void mail_index_transaction_hook_unregister(hook_mail_index_transaction_created_t *hook)
+{
+ unsigned int idx;
+ bool found = FALSE;
+
+ i_assert(array_is_created(&hook_mail_index_transaction_created));
+ for(idx = 0; idx < array_count(&hook_mail_index_transaction_created); idx++) {
+ hook_mail_index_transaction_created_t *arr_hook =
+ array_idx_elem(&hook_mail_index_transaction_created, idx);
+ if (arr_hook == hook) {
+ array_delete(&hook_mail_index_transaction_created, idx, 1);
+ found = TRUE;
+ break;
+ }
+ }
+ i_assert(found == TRUE);
+ if (array_count(&hook_mail_index_transaction_created) == 0)
+ array_free(&hook_mail_index_transaction_created);
+}
+
+
+struct mail_index_view *
+mail_index_transaction_get_view(struct mail_index_transaction *t)
+{
+ return t->view;
+}
+
+bool mail_index_transaction_is_expunged(struct mail_index_transaction *t,
+ uint32_t seq)
+{
+ struct mail_transaction_expunge_guid key;
+
+ if (!array_is_created(&t->expunges))
+ return FALSE;
+
+ if (t->expunges_nonsorted)
+ mail_index_transaction_sort_expunges(t);
+
+ key.uid = seq;
+ return array_bsearch(&t->expunges, &key,
+ mail_transaction_expunge_guid_cmp) != NULL;
+}
+
+void mail_index_transaction_ref(struct mail_index_transaction *t)
+{
+ t->refcount++;
+}
+
+void mail_index_transaction_unref(struct mail_index_transaction **_t)
+{
+ struct mail_index_transaction *t = *_t;
+
+ *_t = NULL;
+ if (--t->refcount > 0)
+ return;
+
+ mail_index_transaction_reset_v(t);
+
+ DLLIST_REMOVE(&t->view->transactions_list, t);
+ array_free(&t->module_contexts);
+ if (t->latest_view != NULL)
+ mail_index_view_close(&t->latest_view);
+ mail_index_view_close(&t->view);
+ i_free(t);
+}
+
+uint32_t mail_index_transaction_get_next_uid(struct mail_index_transaction *t)
+{
+ const struct mail_index_header *head_hdr, *hdr;
+ unsigned int offset;
+ uint32_t next_uid;
+
+ head_hdr = &t->view->index->map->hdr;
+ hdr = &t->view->map->hdr;
+ next_uid = t->reset || head_hdr->uid_validity != hdr->uid_validity ?
+ 1 : hdr->next_uid;
+ if (array_is_created(&t->appends) && t->highest_append_uid != 0) {
+ /* get next_uid from appends if they have UIDs. it's possible
+ that some appends have too low UIDs, they'll be caught
+ later. */
+ if (next_uid <= t->highest_append_uid)
+ next_uid = t->highest_append_uid + 1;
+ }
+
+ /* see if it's been updated in pre/post header changes */
+ offset = offsetof(struct mail_index_header, next_uid);
+ if (t->post_hdr_mask[offset] != 0) {
+ hdr = (const void *)t->post_hdr_change;
+ if (hdr->next_uid > next_uid)
+ next_uid = hdr->next_uid;
+ }
+ if (t->pre_hdr_mask[offset] != 0) {
+ hdr = (const void *)t->pre_hdr_change;
+ if (hdr->next_uid > next_uid)
+ next_uid = hdr->next_uid;
+ }
+ return next_uid;
+}
+
+void mail_index_transaction_lookup_latest_keywords(struct mail_index_transaction *t,
+ uint32_t seq,
+ ARRAY_TYPE(keyword_indexes) *keywords)
+{
+ uint32_t uid, latest_seq;
+
+ /* seq points to the transaction's primary view */
+ mail_index_lookup_uid(t->view, seq, &uid);
+
+ /* get the latest keywords from the updated index, or fallback to the
+ primary view if the message is already expunged */
+ if (t->latest_view == NULL) {
+ mail_index_refresh(t->view->index);
+ t->latest_view = mail_index_view_open(t->view->index);
+ }
+ if (mail_index_lookup_seq(t->latest_view, uid, &latest_seq))
+ mail_index_lookup_keywords(t->latest_view, latest_seq, keywords);
+ else
+ mail_index_lookup_keywords(t->view, seq, keywords);
+}
+
+static int
+mail_transaction_log_file_refresh(struct mail_index_transaction *t,
+ struct mail_transaction_log_append_ctx *ctx)
+{
+ struct mail_transaction_log_file *file;
+
+ if (t->reset) {
+ /* Reset the whole index, preserving only indexid. Begin by
+ rotating the log. We don't care if we skip some non-synced
+ transactions. */
+ if (mail_transaction_log_rotate(t->view->index->log, TRUE) < 0)
+ return -1;
+
+ if (!MAIL_INDEX_TRANSACTION_HAS_CHANGES(t)) {
+ /* we only wanted to reset */
+ return 0;
+ }
+ }
+ file = t->view->index->log->head;
+
+ /* make sure we have everything mapped */
+ if (mail_index_map(t->view->index, MAIL_INDEX_SYNC_HANDLER_HEAD) <= 0)
+ return -1;
+
+ i_assert(file->sync_offset >= file->buffer_offset);
+ ctx->new_highest_modseq = file->sync_highest_modseq;
+ return 1;
+}
+
+static int
+mail_index_transaction_commit_real(struct mail_index_transaction *t,
+ uoff_t *commit_size_r,
+ enum mail_index_transaction_change *changes_r)
+{
+ struct mail_transaction_log *log = t->view->index->log;
+ struct mail_transaction_log_append_ctx *ctx;
+ enum mail_transaction_type trans_flags = 0;
+ uint32_t log_seq1, log_seq2;
+ uoff_t log_offset1, log_offset2;
+ int ret;
+
+ *changes_r = 0;
+
+ if ((t->flags & MAIL_INDEX_TRANSACTION_FLAG_EXTERNAL) != 0)
+ trans_flags |= MAIL_TRANSACTION_EXTERNAL;
+ if ((t->flags & MAIL_INDEX_TRANSACTION_FLAG_SYNC) != 0)
+ trans_flags |= MAIL_TRANSACTION_SYNC;
+
+ if (mail_transaction_log_append_begin(log->index, trans_flags, &ctx) < 0)
+ return -1;
+ ret = mail_transaction_log_file_refresh(t, ctx);
+ if (ret > 0) T_BEGIN {
+ mail_index_transaction_finish(t);
+ mail_index_transaction_export(t, ctx, changes_r);
+ } T_END;
+
+ mail_transaction_log_get_head(log, &log_seq1, &log_offset1);
+ if (mail_transaction_log_append_commit(&ctx) < 0 || ret < 0)
+ return -1;
+ mail_transaction_log_get_head(log, &log_seq2, &log_offset2);
+ i_assert(log_seq1 == log_seq2);
+
+ if (t->reset) {
+ /* get rid of the old index. it might just confuse readers,
+ especially if it's broken. */
+ i_unlink_if_exists(log->index->filepath);
+ }
+
+ *commit_size_r = log_offset2 - log_offset1;
+
+ if ((t->flags & MAIL_INDEX_TRANSACTION_FLAG_HIDE) != 0 &&
+ log_offset1 != log_offset2) {
+ /* mark the area covered by this transaction hidden */
+ mail_index_view_add_hidden_transaction(t->view, log_seq1,
+ log_offset1, log_offset2 - log_offset1);
+ }
+ return 0;
+}
+
+static int mail_index_transaction_commit_v(struct mail_index_transaction *t,
+ struct mail_index_transaction_commit_result *result_r)
+{
+ struct mail_index *index = t->view->index;
+ bool changed;
+ int ret;
+
+ i_assert(t->first_new_seq >
+ mail_index_view_get_messages_count(t->view));
+
+ changed = MAIL_INDEX_TRANSACTION_HAS_CHANGES(t) || t->reset;
+ ret = !changed ? 0 :
+ mail_index_transaction_commit_real(t, &result_r->commit_size,
+ &result_r->changes_mask);
+ mail_transaction_log_get_head(index->log, &result_r->log_file_seq,
+ &result_r->log_file_offset);
+
+ if (ret == 0 && !index->syncing && changed) {
+ /* if we're committing a normal transaction, we want to
+ have those changes in the index mapping immediately. this
+ is especially important when committing cache offset
+ updates.
+
+ however if we're syncing the index now, the mapping must
+ be done later as MAIL_INDEX_SYNC_HANDLER_FILE so that
+ expunge handlers get run for the newly expunged messages
+ (and sync handlers that require HANDLER_FILE as well). */
+ index->sync_commit_result = result_r;
+ mail_index_refresh(index);
+ index->sync_commit_result = NULL;
+ }
+
+ mail_index_transaction_unref(&t);
+ return ret;
+}
+
+static void mail_index_transaction_rollback_v(struct mail_index_transaction *t)
+{
+ mail_index_transaction_unref(&t);
+}
+
+int mail_index_transaction_commit(struct mail_index_transaction **t)
+{
+ struct mail_index_transaction_commit_result result;
+
+ return mail_index_transaction_commit_full(t, &result);
+}
+
+int mail_index_transaction_commit_full(struct mail_index_transaction **_t,
+ struct mail_index_transaction_commit_result *result_r)
+{
+ struct mail_index_transaction *t = *_t;
+ struct mail_index *index = t->view->index;
+ bool index_undeleted = t->index_undeleted;
+
+ if (mail_index_view_is_inconsistent(t->view)) {
+ mail_index_set_error_nolog(index, "View is inconsistent");
+ mail_index_transaction_rollback(_t);
+ return -1;
+ }
+ if (!index_undeleted && !t->commit_deleted_index) {
+ if (t->view->index->index_deleted ||
+ (t->view->index->index_delete_requested &&
+ !t->view->index->syncing)) {
+ /* no further changes allowed */
+ mail_index_set_error_nolog(index, "Index is marked deleted");
+ mail_index_transaction_rollback(_t);
+ return -1;
+ }
+ }
+
+ *_t = NULL;
+ i_zero(result_r);
+ if (t->v.commit(t, result_r) < 0)
+ return -1;
+
+ if (index_undeleted) {
+ index->index_deleted = FALSE;
+ index->index_delete_requested = FALSE;
+ }
+ return 0;
+}
+
+void mail_index_transaction_rollback(struct mail_index_transaction **_t)
+{
+ struct mail_index_transaction *t = *_t;
+
+ *_t = NULL;
+ t->v.rollback(t);
+}
+
+static struct mail_index_transaction_vfuncs trans_vfuncs = {
+ mail_index_transaction_reset_v,
+ mail_index_transaction_commit_v,
+ mail_index_transaction_rollback_v
+};
+
+struct mail_index_transaction *
+mail_index_transaction_begin(struct mail_index_view *view,
+ enum mail_index_transaction_flags flags)
+{
+ struct mail_index_transaction *t;
+
+ /* don't allow syncing view while there's ongoing transactions */
+ mail_index_view_ref(view);
+
+ t = i_new(struct mail_index_transaction, 1);
+ t->refcount = 1;
+ t->v = trans_vfuncs;
+ t->view = view;
+ t->flags = flags;
+
+ if (view->syncing) {
+ /* transaction view cannot work if new records are being added
+ in two places. make sure it doesn't happen. */
+ t->no_appends = TRUE;
+ t->first_new_seq = (uint32_t)-1;
+ } else {
+ t->first_new_seq =
+ mail_index_view_get_messages_count(t->view) + 1;
+ }
+
+ i_array_init(&t->module_contexts,
+ I_MIN(5, mail_index_module_register.id));
+ DLLIST_PREPEND(&view->transactions_list, t);
+
+ if (array_is_created(&hook_mail_index_transaction_created)) {
+ struct hook_build_context *ctx =
+ hook_build_init((void *)&t->v, sizeof(t->v));
+ hook_mail_index_transaction_created_t *callback;
+ array_foreach_elem(&hook_mail_index_transaction_created, callback) {
+ callback(t);
+ hook_build_update(ctx, t->vlast);
+ }
+ t->vlast = NULL;
+ hook_build_deinit(&ctx);
+ }
+ return t;
+}
diff --git a/src/lib-index/mail-index-util.c b/src/lib-index/mail-index-util.c
new file mode 100644
index 0000000..2b27ae1
--- /dev/null
+++ b/src/lib-index/mail-index-util.c
@@ -0,0 +1,138 @@
+/* Copyright (c) 2003-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "bsearch-insert-pos.h"
+#include "mail-index-private.h"
+
+uint32_t mail_index_uint32_to_offset(uint32_t offset)
+{
+ i_assert(offset < 0x40000000);
+ i_assert((offset & 3) == 0);
+
+ offset >>= 2;
+ offset = 0x00000080 | ((offset & 0x0000007f)) |
+ 0x00008000 | ((offset & 0x00003f80) >> 7 << 8) |
+ 0x00800000 | ((offset & 0x001fc000) >> 14 << 16) |
+ 0x80000000 | ((offset & 0x0fe00000) >> 21 << 24);
+
+ return cpu32_to_be(offset);
+}
+
+uint32_t mail_index_offset_to_uint32(uint32_t offset)
+{
+ offset = be32_to_cpu(offset);
+
+ if ((offset & 0x80808080) != 0x80808080)
+ return 0;
+
+ return (((offset & 0x0000007f)) |
+ ((offset & 0x00007f00) >> 8 << 7) |
+ ((offset & 0x007f0000) >> 16 << 14) |
+ ((offset & 0x7f000000) >> 24 << 21)) << 2;
+}
+
+void mail_index_pack_num(uint8_t **p, uint32_t num)
+{
+ /* number continues as long as the highest bit is set */
+ while (num >= 0x80) {
+ **p = (num & 0x7f) | 0x80;
+ *p += 1;
+ num >>= 7;
+ }
+
+ **p = num;
+ *p += 1;
+}
+
+int mail_index_unpack_num(const uint8_t **p, const uint8_t *end,
+ uint32_t *num_r)
+{
+ const uint8_t *c = *p;
+ uint32_t value = 0;
+ unsigned int bits = 0;
+
+ for (;;) {
+ if (unlikely(c == end)) {
+ /* we should never see EOF */
+ *num_r = 0;
+ return -1;
+ }
+
+ value |= (*c & 0x7f) << bits;
+ if (*c < 0x80)
+ break;
+
+ bits += 7;
+ c++;
+ }
+
+ if (unlikely(bits >= 32)) {
+ /* broken input */
+ *p = end;
+ *num_r = 0;
+ return -1;
+ }
+
+ *p = c + 1;
+ *num_r = value;
+ return 0;
+}
+
+static int mail_index_seq_record_cmp(const uint32_t *key_seq,
+ const uint32_t *data_seq)
+{
+ return (int)*key_seq - (int)*data_seq;
+}
+
+bool mail_index_seq_array_lookup(const ARRAY_TYPE(seq_array) *array,
+ uint32_t seq, unsigned int *idx_r)
+{
+ return array_bsearch_insert_pos(array, &seq,
+ mail_index_seq_record_cmp, idx_r);
+}
+
+void mail_index_seq_array_alloc(ARRAY_TYPE(seq_array) *array,
+ size_t record_size)
+{
+ size_t aligned_record_size = (record_size + 3) & ~3U;
+
+ i_assert(!array_is_created(array));
+
+ array_create(array, default_pool,
+ sizeof(uint32_t) + aligned_record_size,
+ 1024 / (sizeof(uint32_t) + aligned_record_size));
+}
+
+bool mail_index_seq_array_add(ARRAY_TYPE(seq_array) *array, uint32_t seq,
+ const void *record, size_t record_size,
+ void *old_record)
+{
+ void *p;
+ unsigned int idx, aligned_record_size;
+
+ /* records need to be 32bit aligned */
+ aligned_record_size = (record_size + 3) & ~3U;
+
+ if (!array_is_created(array))
+ mail_index_seq_array_alloc(array, record_size);
+ i_assert(array->arr.element_size == sizeof(seq) + aligned_record_size);
+
+ if (mail_index_seq_array_lookup(array, seq, &idx)) {
+ /* already there, update */
+ p = array_idx_modifiable(array, idx);
+ if (old_record != NULL) {
+ /* save the old record before overwriting it */
+ memcpy(old_record, PTR_OFFSET(p, sizeof(seq)),
+ record_size);
+ }
+ memcpy(PTR_OFFSET(p, sizeof(seq)), record, record_size);
+ return TRUE;
+ } else {
+ /* insert */
+ p = array_insert_space(array, idx);
+ memcpy(p, &seq, sizeof(seq));
+ memcpy(PTR_OFFSET(p, sizeof(seq)), record, record_size);
+ return FALSE;
+ }
+}
diff --git a/src/lib-index/mail-index-util.h b/src/lib-index/mail-index-util.h
new file mode 100644
index 0000000..b61e16a
--- /dev/null
+++ b/src/lib-index/mail-index-util.h
@@ -0,0 +1,22 @@
+#ifndef MAIL_INDEX_UTIL_H
+#define MAIL_INDEX_UTIL_H
+
+ARRAY_DEFINE_TYPE(seq_array, uint32_t);
+
+uint32_t mail_index_uint32_to_offset(uint32_t offset);
+uint32_t mail_index_offset_to_uint32(uint32_t offset);
+
+#define MAIL_INDEX_PACK_MAX_SIZE ((sizeof(uint32_t) * 8 + 7) / 7)
+void mail_index_pack_num(uint8_t **p, uint32_t num);
+int mail_index_unpack_num(const uint8_t **p, const uint8_t *end,
+ uint32_t *num_r);
+
+bool mail_index_seq_array_lookup(const ARRAY_TYPE(seq_array) *array,
+ uint32_t seq, unsigned int *idx_r);
+void mail_index_seq_array_alloc(ARRAY_TYPE(seq_array) *array,
+ size_t record_size);
+bool mail_index_seq_array_add(ARRAY_TYPE(seq_array) *array, uint32_t seq,
+ const void *record, size_t record_size,
+ void *old_record) ATTR_NULL(5);
+
+#endif
diff --git a/src/lib-index/mail-index-view-private.h b/src/lib-index/mail-index-view-private.h
new file mode 100644
index 0000000..6e8b091
--- /dev/null
+++ b/src/lib-index/mail-index-view-private.h
@@ -0,0 +1,120 @@
+#ifndef MAIL_INDEX_VIEW_PRIVATE_H
+#define MAIL_INDEX_VIEW_PRIVATE_H
+
+#include "mail-index-private.h"
+
+struct mail_index_view_log_sync_area {
+ uint32_t log_file_seq;
+ unsigned int length;
+ uoff_t log_file_offset;
+};
+ARRAY_DEFINE_TYPE(view_log_sync_area, struct mail_index_view_log_sync_area);
+
+struct mail_index_view_vfuncs {
+ void (*close)(struct mail_index_view *view);
+ uint32_t (*get_messages_count)(struct mail_index_view *view);
+ const struct mail_index_header *
+ (*get_header)(struct mail_index_view *view);
+ const struct mail_index_record *
+ (*lookup_full)(struct mail_index_view *view, uint32_t seq,
+ struct mail_index_map **map_r, bool *expunged_r);
+ void (*lookup_uid)(struct mail_index_view *view, uint32_t seq,
+ uint32_t *uid_r);
+ void (*lookup_seq_range)(struct mail_index_view *view,
+ uint32_t first_uid, uint32_t last_uid,
+ uint32_t *first_seq_r, uint32_t *last_seq_r);
+ void (*lookup_first)(struct mail_index_view *view,
+ enum mail_flags flags, uint8_t flags_mask,
+ uint32_t *seq_r);
+ void (*lookup_keywords)(struct mail_index_view *view, uint32_t seq,
+ ARRAY_TYPE(keyword_indexes) *keyword_idx);
+ void (*lookup_ext_full)(struct mail_index_view *view, uint32_t seq,
+ uint32_t ext_id, struct mail_index_map **map_r,
+ const void **data_r, bool *expunged_r);
+ void (*get_header_ext)(struct mail_index_view *view,
+ struct mail_index_map *map, uint32_t ext_id,
+ const void **data_r, size_t *data_size_r);
+ bool (*ext_get_reset_id)(struct mail_index_view *view,
+ struct mail_index_map *map,
+ uint32_t ext_id, uint32_t *reset_id_r);
+};
+
+union mail_index_view_module_context {
+ struct mail_index_module_register *reg;
+};
+
+struct mail_index_view {
+ struct mail_index_view *prev, *next;
+ int refcount;
+
+ struct mail_index_view_vfuncs v;
+ struct mail_index *index;
+ struct mail_transaction_log_view *log_view;
+
+ /* Source location where the mail_index_view_open() call was done.
+ This helps debugging especially if a view is leaked. */
+ const char *source_filename;
+ unsigned int source_linenum;
+
+ /* Set the view inconsistent if this doesn't match mail_index.indexid */
+ uint32_t indexid;
+ /* Set the view inconsistent if this doesn't match
+ mail_index.inconsistency_id. */
+ unsigned int inconsistency_id;
+ uint64_t highest_modseq;
+
+ struct mail_index_map *map;
+ /* All mappings where we have returned records. They need to be kept
+ valid until view is synchronized. */
+ ARRAY(struct mail_index_map *) map_refs;
+
+ /* expunge <= head. The expunge seq/offset points to the log file
+ how far expunges have been synced. The head seq/offset points to
+ how far non-expunges have been synced. They're usually the same,
+ unless MAIL_INDEX_VIEW_SYNC_FLAG_NOEXPUNGES has been used. */
+ uint32_t log_file_expunge_seq, log_file_head_seq;
+ uoff_t log_file_expunge_offset, log_file_head_offset;
+
+ /* Transaction log areas which are returned as
+ mail_index_view_sync_rec.hidden=TRUE. Used to implement
+ MAIL_INDEX_TRANSACTION_FLAG_HIDE. */
+ ARRAY_TYPE(view_log_sync_area) syncs_hidden;
+
+ /* Module-specific contexts. */
+ ARRAY(union mail_index_view_module_context *) module_contexts;
+
+ /* Linked list of all transactions opened for the view. */
+ struct mail_index_transaction *transactions_list;
+
+ /* View is currently inconsistent. It can't be synced. */
+ bool inconsistent:1;
+ /* this view is being synced */
+ bool syncing:1;
+};
+
+struct mail_index_view *
+mail_index_view_open_with_map(struct mail_index *index,
+ struct mail_index_map *map);
+void mail_index_view_clone(struct mail_index_view *dest,
+ const struct mail_index_view *src,
+ const char *source_filename,
+ unsigned int source_linenum);
+#define mail_index_view_clone(dest, src) \
+ mail_index_view_clone(dest, src, __FILE__, __LINE__)
+
+struct mail_index_view *
+mail_index_view_dup_private(const struct mail_index_view *src,
+ const char *source_filename,
+ unsigned int source_linenum);
+#define mail_index_view_dup_private(src) \
+ mail_index_view_dup_private(src, __FILE__, __LINE__)
+void mail_index_view_ref(struct mail_index_view *view);
+void mail_index_view_unref_maps(struct mail_index_view *view);
+void mail_index_view_add_hidden_transaction(struct mail_index_view *view,
+ uint32_t log_file_seq,
+ uoff_t log_file_offset,
+ unsigned int length);
+
+struct mail_index_view *mail_index_dummy_view_open(struct mail_index *index);
+
+#endif
diff --git a/src/lib-index/mail-index-view-sync.c b/src/lib-index/mail-index-view-sync.c
new file mode 100644
index 0000000..d8a5793
--- /dev/null
+++ b/src/lib-index/mail-index-view-sync.c
@@ -0,0 +1,1045 @@
+/* Copyright (c) 2003-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "buffer.h"
+#include "mail-index-view-private.h"
+#include "mail-index-sync-private.h"
+#include "mail-index-modseq.h"
+#include "mail-transaction-log.h"
+
+
+struct mail_index_view_sync_ctx {
+ struct mail_index_view *view;
+ enum mail_index_view_sync_flags flags;
+ struct mail_index_sync_map_ctx sync_map_ctx;
+
+ /* After syncing view, map is replaced with sync_new_map. */
+ struct mail_index_map *sync_new_map;
+
+ ARRAY_TYPE(seq_range) expunges;
+ unsigned int finish_min_msg_count;
+
+ const struct mail_transaction_header *hdr;
+ const void *data;
+
+ /* temporary variables while handling lost transaction logs: */
+ ARRAY_TYPE(keyword_indexes) lost_old_kw, lost_new_kw;
+ buffer_t *lost_kw_buf;
+ uint32_t lost_new_ext_idx;
+ /* result of lost transaction logs: */
+ ARRAY_TYPE(seq_range) lost_flags;
+ unsigned int lost_flag_idx;
+
+ size_t data_offset;
+ bool failed:1;
+ bool sync_map_update:1;
+ bool skipped_expunges:1;
+ bool last_read:1;
+ bool log_was_lost:1;
+ bool hidden:1;
+};
+
+static int
+view_sync_set_log_view_range(struct mail_index_view *view, bool sync_expunges,
+ bool *reset_r, bool *partial_sync_r,
+ const char **error_r)
+{
+ const struct mail_index_header *hdr = &view->index->map->hdr;
+ uint32_t start_seq, end_seq;
+ uoff_t start_offset, end_offset;
+ const char *reason;
+ int ret;
+
+ *partial_sync_r = FALSE;
+
+ if (sync_expunges) {
+ /* Sync everything after the last expunge syncing position.
+ We'll just skip over the non-expunge transaction records
+ that have already been synced previously. */
+ start_seq = view->log_file_expunge_seq;
+ start_offset = view->log_file_expunge_offset;
+ } else {
+ /* Sync only new changes since the last view sync. */
+ start_seq = view->log_file_head_seq;
+ start_offset = view->log_file_head_offset;
+ }
+ /* Sync the view up to the (already refreshed) index map. */
+ end_seq = hdr->log_file_seq;
+ end_offset = hdr->log_file_head_offset;
+
+ if (end_seq < view->log_file_head_seq ||
+ (end_seq == view->log_file_head_seq &&
+ end_offset < view->log_file_head_offset)) {
+ *error_r = t_strdup_printf(
+ "%s log position went backwards "
+ "(%u,%"PRIuUOFF_T" < %u,%"PRIuUOFF_T")",
+ view->index->filepath, end_seq, end_offset,
+ view->log_file_head_seq, view->log_file_head_offset);
+ return -1;
+ }
+
+ for (;;) {
+ /* the view begins from the first non-synced transaction */
+ ret = mail_transaction_log_view_set(view->log_view,
+ start_seq, start_offset,
+ end_seq, end_offset,
+ reset_r, &reason);
+ if (ret <= 0) {
+ *error_r = t_strdup_printf(
+ "Failed to map view for %s: %s",
+ view->index->filepath, reason);
+ return ret;
+ }
+
+ if (!*reset_r || sync_expunges)
+ break;
+
+ /* log was reset, but we don't want to sync expunges.
+ we can't do this, so sync only up to the reset. */
+ mail_transaction_log_view_get_prev_pos(view->log_view,
+ &end_seq, &end_offset);
+ end_seq--; end_offset = UOFF_T_MAX;
+ if (end_seq < start_seq) {
+ /* we have only this reset log */
+ mail_transaction_log_view_clear(view->log_view,
+ view->log_file_expunge_seq);
+ break;
+ }
+ *partial_sync_r = TRUE;
+ }
+ return 1;
+}
+
+static unsigned int
+view_sync_expunges2seqs(struct mail_index_view_sync_ctx *ctx)
+{
+ struct mail_index_view *view = ctx->view;
+ struct seq_range *src, *src_end, *dest;
+ unsigned int count, expunge_count = 0;
+ uint32_t prev_seq = 0;
+
+ /* convert UIDs to sequences */
+ src = dest = array_get_modifiable(&ctx->expunges, &count);
+ src_end = src + count;
+ for (; src != src_end; src++) {
+ if (!mail_index_lookup_seq_range(view, src->seq1, src->seq2,
+ &dest->seq1, &dest->seq2))
+ count--;
+ else {
+ i_assert(dest->seq1 > prev_seq);
+ prev_seq = dest->seq2;
+
+ expunge_count += dest->seq2 - dest->seq1 + 1;
+ dest++;
+ }
+ }
+ array_delete(&ctx->expunges, count,
+ array_count(&ctx->expunges) - count);
+ return expunge_count;
+}
+
+static void
+view_sync_add_expunge_range(ARRAY_TYPE(seq_range) *dest,
+ const struct seq_range *src, size_t src_size)
+{
+ unsigned int i, src_count;
+
+ i_assert(src_size % sizeof(*src) == 0);
+
+ src_count = src_size / sizeof(*src);
+ for (i = 0; i < src_count; i++)
+ seq_range_array_add_range(dest, src[i].seq1, src[i].seq2);
+}
+
+static void
+view_sync_add_expunge_guids(ARRAY_TYPE(seq_range) *dest,
+ const struct mail_transaction_expunge_guid *src,
+ size_t src_size)
+{
+ unsigned int i, src_count;
+
+ i_assert(src_size % sizeof(*src) == 0);
+
+ src_count = src_size / sizeof(*src);
+ for (i = 0; i < src_count; i++)
+ seq_range_array_add(dest, src[i].uid);
+}
+
+static int
+view_sync_get_expunges(struct mail_index_view_sync_ctx *ctx,
+ unsigned int *expunge_count_r)
+{
+ struct mail_index_view *view = ctx->view;
+ const struct mail_transaction_header *hdr;
+ const void *data;
+ int ret;
+
+ /* get a list of expunge transactions. there may be some that we have
+ already synced, but it doesn't matter because they'll get dropped
+ out when converting to sequences. the uid ranges' validity has
+ already been verified, so we can use them directly. */
+ mail_transaction_log_view_mark(view->log_view);
+ while ((ret = mail_transaction_log_view_next(view->log_view,
+ &hdr, &data)) > 0) {
+ if ((hdr->type & MAIL_TRANSACTION_EXTERNAL) == 0) {
+ /* skip expunge requests */
+ continue;
+ }
+ if ((hdr->type & MAIL_TRANSACTION_EXPUNGE_GUID) != 0) {
+ view_sync_add_expunge_guids(&ctx->expunges,
+ data, hdr->size);
+ } else if ((hdr->type & MAIL_TRANSACTION_EXPUNGE) != 0) {
+ view_sync_add_expunge_range(&ctx->expunges,
+ data, hdr->size);
+ }
+ }
+ mail_transaction_log_view_rewind(view->log_view);
+
+ *expunge_count_r = view_sync_expunges2seqs(ctx);
+ return ret;
+}
+
+static bool have_existing_expunges(struct mail_index_view *view,
+ const struct seq_range *range, size_t size)
+{
+ const struct seq_range *range_end;
+ uint32_t seq1, seq2;
+
+ range_end = CONST_PTR_OFFSET(range, size);
+ for (; range != range_end; range++) {
+ if (mail_index_lookup_seq_range(view, range->seq1, range->seq2,
+ &seq1, &seq2))
+ return TRUE;
+ }
+ return FALSE;
+}
+
+static bool
+have_existing_guid_expunge(struct mail_index_view *view,
+ const struct mail_transaction_expunge_guid *expunges,
+ size_t size)
+{
+ const struct mail_transaction_expunge_guid *expunges_end;
+ uint32_t seq;
+
+ expunges_end = CONST_PTR_OFFSET(expunges, size);
+ for (; expunges != expunges_end; expunges++) {
+ if (mail_index_lookup_seq(view, expunges->uid, &seq))
+ return TRUE;
+ }
+ return FALSE;
+}
+
+static bool view_sync_have_expunges(struct mail_index_view *view)
+{
+ const struct mail_transaction_header *hdr;
+ const void *data;
+ bool have_expunges = FALSE;
+ int ret;
+
+ if (mail_transaction_log_view_is_last(view->log_view))
+ return FALSE;
+
+ mail_transaction_log_view_mark(view->log_view);
+
+ while ((ret = mail_transaction_log_view_next(view->log_view,
+ &hdr, &data)) > 0) {
+ if ((hdr->type & MAIL_TRANSACTION_EXTERNAL) == 0) {
+ /* skip expunge requests */
+ continue;
+ }
+ if ((hdr->type & MAIL_TRANSACTION_EXPUNGE_GUID) != 0) {
+ /* we have an expunge. see if it still exists. */
+ if (have_existing_guid_expunge(view, data, hdr->size)) {
+ have_expunges = TRUE;
+ break;
+ }
+ } else if ((hdr->type & MAIL_TRANSACTION_EXPUNGE) != 0) {
+ /* we have an expunge. see if it still exists. */
+ if (have_existing_expunges(view, data, hdr->size)) {
+ have_expunges = TRUE;
+ break;
+ }
+ }
+ }
+
+ mail_transaction_log_view_rewind(view->log_view);
+
+ /* handle failures as having expunges (which is safer).
+ we'll probably fail later. */
+ return ret < 0 || have_expunges;
+}
+
+static int uint_cmp(const void *p1, const void *p2)
+{
+ const unsigned int *u1 = p1, *u2 = p2;
+
+ if (*u1 < *u2)
+ return -1;
+ if (*u1 > *u2)
+ return 1;
+ return 0;
+}
+
+static bool view_sync_lost_keywords_equal(struct mail_index_view_sync_ctx *ctx)
+{
+ unsigned int *old_idx, *new_idx;
+ unsigned int old_count, new_count;
+
+ old_idx = array_get_modifiable(&ctx->lost_old_kw, &old_count);
+ new_idx = array_get_modifiable(&ctx->lost_new_kw, &new_count);
+ if (old_count != new_count)
+ return FALSE;
+
+ qsort(old_idx, old_count, sizeof(*old_idx), uint_cmp);
+ qsort(new_idx, new_count, sizeof(*new_idx), uint_cmp);
+ return memcmp(old_idx, new_idx, old_count * sizeof(*old_idx)) == 0;
+}
+
+static int view_sync_update_keywords(struct mail_index_view_sync_ctx *ctx,
+ uint32_t uid)
+{
+ struct mail_transaction_header thdr;
+ struct mail_transaction_keyword_update kw_up;
+ const unsigned int *kw_idx;
+ const char *const *kw_names;
+ unsigned int i, count;
+
+ kw_idx = array_get(&ctx->lost_new_kw, &count);
+ if (count == 0)
+ return 0;
+ kw_names = array_front(&ctx->view->index->keywords);
+
+ i_zero(&thdr);
+ thdr.type = MAIL_TRANSACTION_KEYWORD_UPDATE | MAIL_TRANSACTION_EXTERNAL;
+ i_zero(&kw_up);
+ kw_up.modify_type = MODIFY_ADD;
+ /* add new flags one by one */
+ for (i = 0; i < count; i++) {
+ kw_up.name_size = strlen(kw_names[kw_idx[i]]);
+ buffer_set_used_size(ctx->lost_kw_buf, 0);
+ buffer_append(ctx->lost_kw_buf, &kw_up, sizeof(kw_up));
+ buffer_append(ctx->lost_kw_buf, kw_names[kw_idx[i]],
+ kw_up.name_size);
+ if (ctx->lost_kw_buf->used % 4 != 0) {
+ buffer_append_zero(ctx->lost_kw_buf,
+ 4 - ctx->lost_kw_buf->used % 4);
+ }
+ buffer_append(ctx->lost_kw_buf, &uid, sizeof(uid));
+ buffer_append(ctx->lost_kw_buf, &uid, sizeof(uid));
+
+ thdr.size = ctx->lost_kw_buf->used;
+ if (mail_index_sync_record(&ctx->sync_map_ctx, &thdr,
+ ctx->lost_kw_buf->data) < 0)
+ return -1;
+ }
+ return 0;
+}
+
+static int view_sync_apply_lost_changes(struct mail_index_view_sync_ctx *ctx,
+ uint32_t old_seq, uint32_t new_seq)
+{
+ struct mail_index_map *old_map = ctx->view->map;
+ struct mail_index_map *new_map = ctx->view->index->map;
+ const struct mail_index_record *old_rec, *new_rec;
+ struct mail_transaction_header thdr;
+ const struct mail_index_ext *ext;
+ const uint64_t *modseqp;
+ uint64_t new_modseq;
+ bool changed = FALSE;
+
+ old_rec = MAIL_INDEX_REC_AT_SEQ(old_map, old_seq);
+ new_rec = MAIL_INDEX_REC_AT_SEQ(new_map, new_seq);
+
+ i_zero(&thdr);
+ if (old_rec->flags != new_rec->flags) {
+ struct mail_transaction_flag_update flag_update;
+
+ /* check this before syncing the record, since it updates
+ old_rec. */
+ if ((old_rec->flags & MAIL_INDEX_FLAGS_MASK) !=
+ (new_rec->flags & MAIL_INDEX_FLAGS_MASK))
+ changed = TRUE;
+
+ thdr.type = MAIL_TRANSACTION_FLAG_UPDATE |
+ MAIL_TRANSACTION_EXTERNAL;
+ thdr.size = sizeof(flag_update);
+
+ i_zero(&flag_update);
+ flag_update.uid1 = flag_update.uid2 = new_rec->uid;
+ flag_update.add_flags = new_rec->flags;
+ flag_update.remove_flags = ENUM_NEGATE(new_rec->flags) & 0xff;
+ if (mail_index_sync_record(&ctx->sync_map_ctx, &thdr,
+ &flag_update) < 0)
+ return -1;
+ }
+
+ mail_index_map_lookup_keywords(old_map, old_seq, &ctx->lost_old_kw);
+ mail_index_map_lookup_keywords(new_map, new_seq, &ctx->lost_new_kw);
+ if (!view_sync_lost_keywords_equal(ctx)) {
+ struct mail_transaction_keyword_reset kw_reset;
+
+ thdr.type = MAIL_TRANSACTION_KEYWORD_RESET |
+ MAIL_TRANSACTION_EXTERNAL;
+ thdr.size = sizeof(kw_reset);
+
+ /* remove all old flags by resetting them */
+ i_zero(&kw_reset);
+ kw_reset.uid1 = kw_reset.uid2 = new_rec->uid;
+ if (mail_index_sync_record(&ctx->sync_map_ctx, &thdr,
+ &kw_reset) < 0)
+ return -1;
+
+ if (view_sync_update_keywords(ctx, new_rec->uid) < 0)
+ return -1;
+ changed = TRUE;
+ }
+
+ if (changed) {
+ /* flags or keywords changed */
+ } else if (ctx->view->highest_modseq != 0 &&
+ ctx->lost_new_ext_idx != (uint32_t)-1) {
+ /* if modseq has changed include this message in changed flags
+ list, even if we didn't see any changes above. */
+ ext = array_idx(&new_map->extensions, ctx->lost_new_ext_idx);
+ modseqp = CONST_PTR_OFFSET(new_rec, ext->record_offset);
+ new_modseq = *modseqp;
+
+ if (new_modseq > ctx->view->highest_modseq)
+ changed = TRUE;
+ }
+
+ /* without modseqs lost_flags isn't updated perfectly correctly, because
+ by the time we're comparing old flags it may have changed from what
+ we last sent to the client (because the map is shared). This could
+ be avoided by always keeping a private copy of the map in the view,
+ but that's a waste of memory for as rare of a problem as this. */
+ if (changed)
+ seq_range_array_add(&ctx->lost_flags, new_rec->uid);
+ return 0;
+}
+
+static int
+view_sync_get_log_lost_changes(struct mail_index_view_sync_ctx *ctx,
+ unsigned int *expunge_count_r)
+{
+ struct mail_index_view *view = ctx->view;
+ struct mail_index_map *old_map = view->map;
+ struct mail_index_map *new_map = view->index->map;
+ const unsigned int old_count = old_map->hdr.messages_count;
+ const unsigned int new_count = new_map->hdr.messages_count;
+ const struct mail_index_record *old_rec, *new_rec;
+ struct mail_transaction_header thdr;
+ uint32_t seqi, seqj;
+
+ /* we don't update the map in the same order as it's typically done.
+ map->rec_map may already have some messages appended that we don't
+ want. get an atomic map to make sure these get removed. */
+ (void)mail_index_sync_get_atomic_map(&ctx->sync_map_ctx);
+
+ if (!mail_index_map_get_ext_idx(new_map, view->index->modseq_ext_id,
+ &ctx->lost_new_ext_idx))
+ ctx->lost_new_ext_idx = (uint32_t)-1;
+
+ i_array_init(&ctx->lost_flags, 64);
+ t_array_init(&ctx->lost_old_kw, 32);
+ t_array_init(&ctx->lost_new_kw, 32);
+ ctx->lost_kw_buf = t_buffer_create(128);
+
+ /* handle expunges and sync flags */
+ seqi = seqj = 1;
+ while (seqi <= old_count && seqj <= new_count) {
+ old_rec = MAIL_INDEX_REC_AT_SEQ(old_map, seqi);
+ new_rec = MAIL_INDEX_REC_AT_SEQ(new_map, seqj);
+ if (old_rec->uid == new_rec->uid) {
+ /* message found - check if flags have changed */
+ if (view_sync_apply_lost_changes(ctx, seqi, seqj) < 0)
+ return -1;
+ seqi++; seqj++;
+ } else if (old_rec->uid < new_rec->uid) {
+ /* message expunged */
+ seq_range_array_add(&ctx->expunges, old_rec->uid);
+ seqi++;
+ } else {
+ /* new message appeared out of nowhere */
+ mail_index_set_error(view->index,
+ "%s view is inconsistent: "
+ "uid=%u inserted in the middle of mailbox",
+ view->index->filepath, new_rec->uid);
+ return -1;
+ }
+ }
+ /* if there are old messages left, they're all expunged */
+ for (; seqi <= old_count; seqi++) {
+ old_rec = MAIL_INDEX_REC_AT_SEQ(old_map, seqi);
+ seq_range_array_add(&ctx->expunges, old_rec->uid);
+ }
+ /* if there are new messages left, they're all new messages */
+ thdr.type = MAIL_TRANSACTION_APPEND | MAIL_TRANSACTION_EXTERNAL;
+ thdr.size = sizeof(*new_rec);
+ for (; seqj <= new_count; seqj++) {
+ new_rec = MAIL_INDEX_REC_AT_SEQ(new_map, seqj);
+ if (mail_index_sync_record(&ctx->sync_map_ctx,
+ &thdr, new_rec) < 0)
+ return -1;
+ mail_index_map_lookup_keywords(new_map, seqj,
+ &ctx->lost_new_kw);
+ if (view_sync_update_keywords(ctx, new_rec->uid) < 0)
+ return -1;
+ }
+ *expunge_count_r = view_sync_expunges2seqs(ctx);
+
+ /* we have no idea how far we've synced - make sure these aren't used */
+ old_map->hdr.log_file_seq = 0;
+ old_map->hdr.log_file_head_offset = 0;
+ old_map->hdr.log_file_tail_offset = 0;
+
+ if ((ctx->flags & MAIL_INDEX_VIEW_SYNC_FLAG_NOEXPUNGES) != 0) {
+ /* Expunges aren't wanted to be synced. Remember if we skipped
+ over any expunges. If yes, we must not update
+ log_file_expunge_seq/offset at the end of the view sync
+ so that a later sync can finish the expunges. */
+ array_clear(&ctx->expunges);
+ ctx->skipped_expunges = *expunge_count_r > 0;
+ }
+ /* After the view sync is finished, update
+ log_file_head_seq/offset, since we've synced everything
+ (except maybe the expunges) up to this point. */
+ view->log_file_head_seq = new_map->hdr.log_file_seq;
+ view->log_file_head_offset = new_map->hdr.log_file_head_offset;
+ return 0;
+}
+
+static int mail_index_view_sync_init_fix(struct mail_index_view_sync_ctx *ctx)
+{
+ struct mail_index_view *view = ctx->view;
+ uint32_t seq;
+ uoff_t offset;
+ const char *reason;
+ bool reset;
+ int ret;
+
+ /* replace the view's map */
+ view->index->map->refcount++;
+ mail_index_unmap(&view->map);
+ view->map = view->index->map;
+
+ /* update log positions */
+ view->log_file_head_seq = seq = view->map->hdr.log_file_seq;
+ view->log_file_head_offset = offset =
+ view->map->hdr.log_file_head_offset;
+
+ ret = mail_transaction_log_view_set(view->log_view, seq, offset,
+ seq, offset, &reset, &reason);
+ if (ret <= 0) {
+ mail_index_set_error(view->index, "Failed to fix view for %s: %s",
+ view->index->filepath, reason);
+ return ret;
+ }
+ view->inconsistent = FALSE;
+ return 0;
+}
+
+struct mail_index_view_sync_ctx *
+mail_index_view_sync_begin(struct mail_index_view *view,
+ enum mail_index_view_sync_flags flags)
+{
+ struct mail_index_view_sync_ctx *ctx;
+ struct mail_index_map *tmp_map;
+ unsigned int expunge_count = 0;
+ bool reset, partial_sync, sync_expunges, have_expunges;
+ const char *error;
+ int ret;
+
+ i_assert(!view->syncing);
+ i_assert(view->transactions_list == NULL);
+
+ view->syncing = TRUE;
+
+ /* Syncing the view invalidates all previous looked up records.
+ Unreference the mappings this view keeps because of them. */
+ mail_index_view_unref_maps(view);
+
+ ctx = i_new(struct mail_index_view_sync_ctx, 1);
+ ctx->view = view;
+ ctx->flags = flags;
+
+ sync_expunges = (flags & MAIL_INDEX_VIEW_SYNC_FLAG_NOEXPUNGES) == 0;
+ if (sync_expunges)
+ i_array_init(&ctx->expunges, 64);
+ if ((flags & MAIL_INDEX_VIEW_SYNC_FLAG_FIX_INCONSISTENT) != 0) {
+ /* just get this view synced - don't return anything */
+ i_assert(sync_expunges);
+ if (mail_index_view_sync_init_fix(ctx) < 0)
+ ctx->failed = TRUE;
+ return ctx;
+ }
+ if (mail_index_view_is_inconsistent(view)) {
+ mail_index_set_error(view->index, "%s view is inconsistent",
+ view->index->filepath);
+ ctx->failed = TRUE;
+ return ctx;
+ }
+
+ ret = view_sync_set_log_view_range(view, sync_expunges, &reset,
+ &partial_sync, &error);
+ if (ret < 0) {
+ mail_index_set_error(view->index, "%s", error);
+ ctx->failed = TRUE;
+ return ctx;
+ }
+
+ if (ret == 0) {
+ /* Log the warning only when all expunges have been synced
+ by previous syncs. This way when there's a _FLAG_NOEXPUNGES
+ sync, there's no second warning logged when the expunges
+ finally are synced. */
+ if (view->log_file_expunge_seq == view->log_file_head_seq &&
+ view->log_file_expunge_offset == view->log_file_head_offset) {
+ e_warning(view->index->event,
+ "%s - generating missing logs", error);
+ }
+ ctx->log_was_lost = TRUE;
+ if (!sync_expunges)
+ i_array_init(&ctx->expunges, 64);
+ mail_index_sync_map_init(&ctx->sync_map_ctx, view,
+ MAIL_INDEX_SYNC_HANDLER_VIEW);
+ ret = view_sync_get_log_lost_changes(ctx, &expunge_count);
+ mail_index_modseq_sync_end(&ctx->sync_map_ctx.modseq_ctx);
+ mail_index_sync_map_deinit(&ctx->sync_map_ctx);
+ if (ret < 0) {
+ mail_index_set_error(view->index,
+ "%s view syncing failed to apply changes",
+ view->index->filepath);
+ view->inconsistent = TRUE;
+ ctx->failed = TRUE;
+ return ctx;
+ }
+ have_expunges = expunge_count > 0;
+ } else if (sync_expunges) {
+ /* get list of all expunges first */
+ if (view_sync_get_expunges(ctx, &expunge_count) < 0) {
+ ctx->failed = TRUE;
+ return ctx;
+ }
+ have_expunges = expunge_count > 0;
+ } else if (view->log_file_expunge_seq == view->log_file_head_seq &&
+ view->log_file_expunge_offset == view->log_file_head_offset) {
+ /* Previous syncs haven't left any pending expunges. See if
+ this sync will. */
+ have_expunges = view_sync_have_expunges(view);
+ } else {
+ /* Expunges weren't synced in the previous sync either.
+ We already know there are missing expunges. */
+ ctx->skipped_expunges = TRUE;
+ have_expunges = TRUE;
+ }
+
+ ctx->finish_min_msg_count = reset ? 0 :
+ view->map->hdr.messages_count - expunge_count;
+ if (!reset)
+ ;
+ else if ((flags & MAIL_INDEX_VIEW_SYNC_FLAG_2ND_INDEX) != 0 &&
+ view->map->hdr.messages_count == 0) {
+ /* The secondary index is still empty, so it may have
+ just been created for the first time. This is
+ expected, so it shouldn't cause the view to become
+ inconsistent. */
+ if (mail_index_view_sync_init_fix(ctx) < 0)
+ ctx->failed = TRUE;
+ return ctx;
+ } else {
+ view->inconsistent = TRUE;
+ mail_index_set_error(view->index,
+ "%s reset, view is now inconsistent",
+ view->index->filepath);
+ ctx->failed = TRUE;
+ return ctx;
+ }
+
+ if (!have_expunges && !partial_sync) {
+ /* no expunges, we can just replace the map */
+ if (view->index->map->hdr.messages_count <
+ ctx->finish_min_msg_count) {
+ mail_index_set_error(view->index,
+ "Index %s lost messages without expunging "
+ "(%u -> %u)", view->index->filepath,
+ view->map->hdr.messages_count,
+ view->index->map->hdr.messages_count);
+ ctx->finish_min_msg_count = 0;
+ view->inconsistent = TRUE;
+ }
+
+ view->index->map->refcount++;
+ mail_index_unmap(&view->map);
+ view->map = view->index->map;
+ } else {
+ /* a) expunges seen. b) doing a partial sync because we saw
+ a reset.
+
+ Create a private map which we update. If we're syncing
+ expunges the map will finally be replaced with the head map
+ to remove the expunged messages. */
+ ctx->sync_map_update = TRUE;
+
+ if (view->map->refcount > 1) {
+ tmp_map = mail_index_map_clone(view->map);
+ mail_index_unmap(&view->map);
+ view->map = tmp_map;
+ }
+
+ if (sync_expunges) {
+ ctx->sync_new_map = view->index->map;
+ ctx->sync_new_map->refcount++;
+ }
+ }
+ mail_index_sync_map_init(&ctx->sync_map_ctx, view,
+ MAIL_INDEX_SYNC_HANDLER_VIEW);
+
+#ifdef DEBUG
+ mail_index_map_check(view->map);
+#endif
+ return ctx;
+}
+
+static bool
+view_sync_is_hidden(struct mail_index_view *view, uint32_t seq, uoff_t offset)
+{
+ const struct mail_index_view_log_sync_area *sync;
+
+ if (!array_is_created(&view->syncs_hidden))
+ return FALSE;
+
+ array_foreach(&view->syncs_hidden, sync) {
+ if (sync->log_file_offset <= offset &&
+ offset - sync->log_file_offset < sync->length &&
+ sync->log_file_seq == seq)
+ return TRUE;
+ }
+
+ return FALSE;
+}
+
+static bool
+mail_index_view_sync_want(struct mail_index_view_sync_ctx *ctx,
+ const struct mail_transaction_header *hdr)
+{
+ struct mail_index_view *view = ctx->view;
+ uint32_t seq;
+ uoff_t offset, next_offset;
+
+ mail_transaction_log_view_get_prev_pos(view->log_view, &seq, &offset);
+ next_offset = offset + sizeof(*hdr) + hdr->size;
+
+ if ((hdr->type & (MAIL_TRANSACTION_EXPUNGE |
+ MAIL_TRANSACTION_EXPUNGE_GUID)) != 0 &&
+ (hdr->type & MAIL_TRANSACTION_EXTERNAL) != 0) {
+ if ((ctx->flags & MAIL_INDEX_VIEW_SYNC_FLAG_NOEXPUNGES) != 0) {
+ i_assert(!LOG_IS_BEFORE(seq, offset,
+ view->log_file_expunge_seq,
+ view->log_file_expunge_offset));
+ if (!ctx->skipped_expunges) {
+ view->log_file_expunge_seq = seq;
+ view->log_file_expunge_offset = offset;
+ ctx->skipped_expunges = TRUE;
+ }
+ return FALSE;
+ }
+ if (LOG_IS_BEFORE(seq, offset, view->log_file_expunge_seq,
+ view->log_file_expunge_offset)) {
+ /* already synced */
+ return FALSE;
+ }
+ }
+
+ if (LOG_IS_BEFORE(seq, offset, view->log_file_head_seq,
+ view->log_file_head_offset)) {
+ /* already synced */
+ return FALSE;
+ }
+
+ view->log_file_head_seq = seq;
+ view->log_file_head_offset = next_offset;
+ return TRUE;
+}
+
+static int
+mail_index_view_sync_get_next_transaction(struct mail_index_view_sync_ctx *ctx)
+{
+ struct mail_transaction_log_view *log_view = ctx->view->log_view;
+ struct mail_index_view *view = ctx->view;
+ const struct mail_transaction_header *hdr;
+ uint32_t seq;
+ uoff_t offset;
+ int ret;
+ bool synced_to_map;
+
+ do {
+ /* Get the next transaction from log. */
+ ret = mail_transaction_log_view_next(log_view, &ctx->hdr,
+ &ctx->data);
+ if (ret <= 0) {
+ if (ret < 0)
+ return -1;
+
+ ctx->hdr = NULL;
+ ctx->last_read = TRUE;
+ return 0;
+ }
+
+ hdr = ctx->hdr;
+ /* skip records we've already synced */
+ } while (!mail_index_view_sync_want(ctx, hdr));
+
+ mail_transaction_log_view_get_prev_pos(log_view, &seq, &offset);
+
+ /* If we started from a map that we didn't create ourself,
+ some of the transactions may already be synced. at the end
+ of this view sync we'll update file_seq=0 so that this check
+ always becomes FALSE for subsequent syncs. */
+ synced_to_map = view->map->hdr.log_file_seq != 0 &&
+ LOG_IS_BEFORE(seq, offset, view->map->hdr.log_file_seq,
+ view->map->hdr.log_file_head_offset);
+
+ /* Apply transaction to view's mapping if needed (meaning we
+ didn't just re-map the view to head mapping). */
+ if (ctx->sync_map_update && !synced_to_map) {
+ if ((hdr->type & (MAIL_TRANSACTION_EXPUNGE |
+ MAIL_TRANSACTION_EXPUNGE_GUID)) == 0) {
+ ret = mail_index_sync_record(&ctx->sync_map_ctx,
+ hdr, ctx->data);
+ }
+ if (ret < 0)
+ return -1;
+ }
+
+ ctx->hidden = view_sync_is_hidden(view, seq, offset);
+ return 1;
+}
+
+static bool
+mail_index_view_sync_get_rec(struct mail_index_view_sync_ctx *ctx,
+ struct mail_index_view_sync_rec *rec)
+{
+ const struct mail_transaction_header *hdr = ctx->hdr;
+ const void *data = ctx->data;
+
+ switch (hdr->type & MAIL_TRANSACTION_TYPE_MASK) {
+ case MAIL_TRANSACTION_FLAG_UPDATE: {
+ const struct mail_transaction_flag_update *update =
+ CONST_PTR_OFFSET(data, ctx->data_offset);
+
+ /* data contains mail_transaction_flag_update[] */
+ for (;;) {
+ ctx->data_offset += sizeof(*update);
+ if (!MAIL_TRANSACTION_FLAG_UPDATE_IS_INTERNAL(update))
+ break;
+
+ /* skip internal flag changes */
+ if (ctx->data_offset == ctx->hdr->size)
+ return FALSE;
+
+ update = CONST_PTR_OFFSET(data, ctx->data_offset);
+ }
+
+ if (update->add_flags != 0 || update->remove_flags != 0)
+ rec->type = MAIL_INDEX_VIEW_SYNC_TYPE_FLAGS;
+ else
+ rec->type = MAIL_INDEX_VIEW_SYNC_TYPE_MODSEQ;
+ rec->uid1 = update->uid1;
+ rec->uid2 = update->uid2;
+ break;
+ }
+ case MAIL_TRANSACTION_KEYWORD_UPDATE: {
+ const struct mail_transaction_keyword_update *update = data;
+ const uint32_t *uids;
+
+ /* data contains mail_transaction_keyword_update header,
+ the keyword name and an array of { uint32_t uid1, uid2; } */
+
+ if (ctx->data_offset == 0) {
+ /* skip over the header and name */
+ ctx->data_offset = sizeof(*update) + update->name_size;
+ if ((ctx->data_offset % 4) != 0)
+ ctx->data_offset += 4 - (ctx->data_offset % 4);
+ }
+
+ uids = CONST_PTR_OFFSET(data, ctx->data_offset);
+ rec->type = MAIL_INDEX_VIEW_SYNC_TYPE_FLAGS;
+ rec->uid1 = uids[0];
+ rec->uid2 = uids[1];
+
+ ctx->data_offset += sizeof(uint32_t) * 2;
+ break;
+ }
+ case MAIL_TRANSACTION_KEYWORD_RESET: {
+ const struct mail_transaction_keyword_reset *reset =
+ CONST_PTR_OFFSET(data, ctx->data_offset);
+
+ /* data contains mail_transaction_keyword_reset[] */
+ rec->type = MAIL_INDEX_VIEW_SYNC_TYPE_FLAGS;
+ rec->uid1 = reset->uid1;
+ rec->uid2 = reset->uid2;
+ ctx->data_offset += sizeof(*reset);
+ break;
+ }
+ default:
+ ctx->hdr = NULL;
+ return FALSE;
+ }
+
+ rec->hidden = ctx->hidden;
+ return TRUE;
+}
+
+static bool
+mail_index_view_sync_next_lost(struct mail_index_view_sync_ctx *ctx,
+ struct mail_index_view_sync_rec *sync_rec)
+{
+ const struct seq_range *range;
+ unsigned int count;
+
+ range = array_get(&ctx->lost_flags, &count);
+ if (ctx->lost_flag_idx == count) {
+ ctx->last_read = TRUE;
+ return FALSE;
+ }
+
+ sync_rec->type = MAIL_INDEX_VIEW_SYNC_TYPE_FLAGS;
+ sync_rec->uid1 = range[ctx->lost_flag_idx].seq1;
+ sync_rec->uid2 = range[ctx->lost_flag_idx].seq2;
+ sync_rec->hidden = FALSE;
+ ctx->lost_flag_idx++;
+ return TRUE;
+}
+
+bool mail_index_view_sync_next(struct mail_index_view_sync_ctx *ctx,
+ struct mail_index_view_sync_rec *sync_rec)
+{
+ int ret;
+
+ if (ctx->log_was_lost)
+ return mail_index_view_sync_next_lost(ctx, sync_rec);
+
+ do {
+ if (ctx->hdr == NULL || ctx->data_offset == ctx->hdr->size) {
+ ret = mail_index_view_sync_get_next_transaction(ctx);
+ if (ret <= 0) {
+ if (ret < 0)
+ ctx->failed = TRUE;
+ return FALSE;
+ }
+
+ ctx->data_offset = 0;
+ }
+ } while (!mail_index_view_sync_get_rec(ctx, sync_rec));
+
+ return TRUE;
+}
+
+void mail_index_view_sync_get_expunges(struct mail_index_view_sync_ctx *ctx,
+ const ARRAY_TYPE(seq_range) **expunges_r)
+{
+ *expunges_r = &ctx->expunges;
+}
+
+static void
+mail_index_view_sync_clean_log_syncs(struct mail_index_view *view)
+{
+ const struct mail_index_view_log_sync_area *syncs;
+ unsigned int i, count;
+
+ if (!array_is_created(&view->syncs_hidden))
+ return;
+
+ /* Clean up to view's tail */
+ syncs = array_get(&view->syncs_hidden, &count);
+ for (i = 0; i < count; i++) {
+ if ((syncs[i].log_file_offset +
+ syncs[i].length > view->log_file_expunge_offset &&
+ syncs[i].log_file_seq == view->log_file_expunge_seq) ||
+ syncs[i].log_file_seq > view->log_file_expunge_seq)
+ break;
+ }
+ if (i > 0)
+ array_delete(&view->syncs_hidden, 0, i);
+}
+
+int mail_index_view_sync_commit(struct mail_index_view_sync_ctx **_ctx,
+ bool *delayed_expunges_r)
+{
+ struct mail_index_view_sync_ctx *ctx = *_ctx;
+ struct mail_index_view *view = ctx->view;
+ int ret = ctx->failed ? -1 : 0;
+
+ i_assert(view->syncing);
+
+ *_ctx = NULL;
+ *delayed_expunges_r = ctx->skipped_expunges;
+
+ if ((!ctx->last_read || view->inconsistent) &&
+ (ctx->flags & MAIL_INDEX_VIEW_SYNC_FLAG_FIX_INCONSISTENT) == 0) {
+ /* we didn't sync everything */
+ view->inconsistent = TRUE;
+ ret = -1;
+ }
+ if (ctx->sync_map_ctx.modseq_ctx != NULL)
+ mail_index_modseq_sync_end(&ctx->sync_map_ctx.modseq_ctx);
+
+ if (ctx->sync_new_map != NULL) {
+ mail_index_unmap(&view->map);
+ view->map = ctx->sync_new_map;
+ } else if (ctx->sync_map_update) {
+ /* log offsets have no meaning in views. make sure they're not
+ tried to be used wrong by setting them to zero. */
+ view->map->hdr.log_file_seq = 0;
+ view->map->hdr.log_file_head_offset = 0;
+ view->map->hdr.log_file_tail_offset = 0;
+ }
+
+ i_assert(view->map->hdr.messages_count >= ctx->finish_min_msg_count);
+
+ if (!ctx->skipped_expunges) {
+ view->log_file_expunge_seq = view->log_file_head_seq;
+ view->log_file_expunge_offset = view->log_file_head_offset;
+ }
+
+ if (ctx->sync_map_ctx.view != NULL)
+ mail_index_sync_map_deinit(&ctx->sync_map_ctx);
+ mail_index_view_sync_clean_log_syncs(ctx->view);
+
+#ifdef DEBUG
+ mail_index_map_check(view->map);
+#endif
+
+ /* set log view to empty range so unneeded memory gets freed */
+ mail_transaction_log_view_clear(view->log_view,
+ view->log_file_expunge_seq);
+
+ if (array_is_created(&ctx->expunges))
+ array_free(&ctx->expunges);
+ if (array_is_created(&ctx->lost_flags))
+ array_free(&ctx->lost_flags);
+
+ view->highest_modseq = mail_index_map_modseq_get_highest(view->map);
+ view->syncing = FALSE;
+ i_free(ctx);
+ return ret;
+}
+
+void mail_index_view_add_hidden_transaction(struct mail_index_view *view,
+ uint32_t log_file_seq,
+ uoff_t log_file_offset,
+ unsigned int length)
+{
+ struct mail_index_view_log_sync_area *area;
+
+ if (!array_is_created(&view->syncs_hidden))
+ i_array_init(&view->syncs_hidden, 32);
+
+ area = array_append_space(&view->syncs_hidden);
+ area->log_file_seq = log_file_seq;
+ area->log_file_offset = log_file_offset;
+ area->length = length;
+}
diff --git a/src/lib-index/mail-index-view.c b/src/lib-index/mail-index-view.c
new file mode 100644
index 0000000..0850de1
--- /dev/null
+++ b/src/lib-index/mail-index-view.c
@@ -0,0 +1,651 @@
+/* Copyright (c) 2003-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "buffer.h"
+#include "llist.h"
+#include "mail-index-view-private.h"
+#include "mail-transaction-log.h"
+
+#undef mail_index_view_clone
+#undef mail_index_view_dup_private
+
+struct mail_index_view *
+mail_index_view_dup_private(const struct mail_index_view *src,
+ const char *source_filename,
+ unsigned int source_linenum)
+{
+ struct mail_index_view *view;
+ struct mail_index_map *map;
+
+ view = i_new(struct mail_index_view, 1);
+ mail_index_view_clone(view, src, source_filename, source_linenum);
+
+ map = mail_index_map_clone(view->map);
+ mail_index_unmap(&view->map);
+ view->map = map;
+ return view;
+}
+
+void mail_index_view_clone(struct mail_index_view *dest,
+ const struct mail_index_view *src,
+ const char *source_filename,
+ unsigned int source_linenum)
+{
+ i_zero(dest);
+ dest->refcount = 1;
+ dest->v = src->v;
+ dest->index = src->index;
+ if (src->log_view != NULL) {
+ dest->log_view =
+ mail_transaction_log_view_open(src->index->log);
+ }
+
+ dest->indexid = src->indexid;
+ dest->inconsistency_id = src->inconsistency_id;
+ dest->map = src->map;
+ if (dest->map != NULL)
+ dest->map->refcount++;
+
+ dest->log_file_expunge_seq = src->log_file_expunge_seq;
+ dest->log_file_expunge_offset = src->log_file_expunge_offset;
+ dest->log_file_head_seq = src->log_file_head_seq;
+ dest->log_file_head_offset = src->log_file_head_offset;
+
+ i_array_init(&dest->module_contexts,
+ I_MIN(5, mail_index_module_register.id));
+
+ dest->source_filename = source_filename;
+ dest->source_linenum = source_linenum;
+
+ DLLIST_PREPEND(&dest->index->views, dest);
+}
+
+void mail_index_view_ref(struct mail_index_view *view)
+{
+ view->refcount++;
+}
+
+static void view_close(struct mail_index_view *view)
+{
+ i_assert(view->refcount == 0);
+ i_assert(view->index->views != NULL);
+
+ DLLIST_REMOVE(&view->index->views, view);
+
+ mail_transaction_log_view_close(&view->log_view);
+
+ if (array_is_created(&view->syncs_hidden))
+ array_free(&view->syncs_hidden);
+ mail_index_unmap(&view->map);
+ if (array_is_created(&view->map_refs)) {
+ mail_index_view_unref_maps(view);
+ array_free(&view->map_refs);
+ }
+ array_free(&view->module_contexts);
+ i_free(view);
+}
+
+bool mail_index_view_is_inconsistent(struct mail_index_view *view)
+{
+ if (view->index->indexid != view->indexid ||
+ view->index->inconsistency_id != view->inconsistency_id)
+ view->inconsistent = TRUE;
+ return view->inconsistent;
+}
+
+struct mail_index *mail_index_view_get_index(struct mail_index_view *view)
+{
+ return view->index;
+}
+
+bool mail_index_view_have_transactions(struct mail_index_view *view)
+{
+ return view->transactions_list != NULL;
+}
+
+static void mail_index_view_ref_map(struct mail_index_view *view,
+ struct mail_index_map *map)
+{
+ struct mail_index_map *const *maps;
+ unsigned int i, count;
+
+ if (array_is_created(&view->map_refs)) {
+ maps = array_get(&view->map_refs, &count);
+
+ /* if map is already referenced, do nothing */
+ for (i = 0; i < count; i++) {
+ if (maps[i] == map)
+ return;
+ }
+ } else {
+ i_array_init(&view->map_refs, 4);
+ }
+
+ /* reference the given mapping. the reference is dropped when the view
+ is synchronized or closed. */
+ map->refcount++;
+ array_push_back(&view->map_refs, &map);
+}
+
+void mail_index_view_unref_maps(struct mail_index_view *view)
+{
+ struct mail_index_map **maps;
+ unsigned int i, count;
+
+ if (!array_is_created(&view->map_refs))
+ return;
+
+ maps = array_get_modifiable(&view->map_refs, &count);
+ for (i = 0; i < count; i++)
+ mail_index_unmap(&maps[i]);
+
+ array_clear(&view->map_refs);
+}
+
+static uint32_t view_get_messages_count(struct mail_index_view *view)
+{
+ return view->map->hdr.messages_count;
+}
+
+static const struct mail_index_header *
+view_get_header(struct mail_index_view *view)
+{
+ return &view->map->hdr;
+}
+
+static const struct mail_index_record *
+view_lookup_full(struct mail_index_view *view, uint32_t seq,
+ struct mail_index_map **map_r, bool *expunged_r)
+{
+ static struct mail_index_record broken_rec;
+ struct mail_index_map *map;
+ const struct mail_index_record *rec, *head_rec;
+
+ i_assert(seq > 0 && seq <= mail_index_view_get_messages_count(view));
+
+ /* look up the record */
+ rec = MAIL_INDEX_REC_AT_SEQ(view->map, seq);
+ if (unlikely(rec->uid == 0)) {
+ if (!view->inconsistent) {
+ mail_index_set_error(view->index,
+ "Corrupted Index file %s: Record [%u].uid=0",
+ view->index->filepath, seq);
+ (void)mail_index_fsck(view->index);
+ view->inconsistent = TRUE;
+ }
+
+ /* we'll need to return something so the caller doesn't crash */
+ *map_r = view->map;
+ if (expunged_r != NULL)
+ *expunged_r = TRUE;
+ return &broken_rec;
+ }
+ if (view->map == view->index->map) {
+ /* view's mapping is latest. we can use it directly. */
+ *map_r = view->map;
+ if (expunged_r != NULL)
+ *expunged_r = FALSE;
+ return rec;
+ }
+
+ /* look up the record from head mapping. it may contain some changes.
+
+ start looking up from the same sequence as in the old view.
+ if there are no expunges, it's there. otherwise it's somewhere
+ before (since records can't be inserted).
+
+ usually there are only a few expunges, so just going downwards from
+ our initial sequence position is probably faster than binary
+ search. */
+ if (seq > view->index->map->hdr.messages_count)
+ seq = view->index->map->hdr.messages_count;
+ if (seq == 0) {
+ /* everything is expunged from head. use the old record. */
+ *map_r = view->map;
+ if (expunged_r != NULL)
+ *expunged_r = TRUE;
+ return rec;
+ }
+
+ map = view->index->map;
+ do {
+ head_rec = MAIL_INDEX_REC_AT_SEQ(map, seq);
+ if (head_rec->uid <= rec->uid)
+ break;
+ } while (--seq > 0);
+
+ if (head_rec->uid == rec->uid) {
+ /* found it. use it. reference the index mapping so that the
+ returned record doesn't get invalidated after next sync. */
+ mail_index_view_ref_map(view, view->index->map);
+ *map_r = view->index->map;
+ if (expunged_r != NULL)
+ *expunged_r = FALSE;
+ return head_rec;
+ } else {
+ /* expunged from head. use the old record. */
+ *map_r = view->map;
+ if (expunged_r != NULL)
+ *expunged_r = TRUE;
+ return rec;
+ }
+}
+
+static void view_lookup_uid(struct mail_index_view *view, uint32_t seq,
+ uint32_t *uid_r)
+{
+ i_assert(seq > 0 && seq <= mail_index_view_get_messages_count(view));
+
+ *uid_r = MAIL_INDEX_REC_AT_SEQ(view->map, seq)->uid;
+}
+
+static void view_lookup_seq_range(struct mail_index_view *view,
+ uint32_t first_uid, uint32_t last_uid,
+ uint32_t *first_seq_r, uint32_t *last_seq_r)
+{
+ mail_index_map_lookup_seq_range(view->map, first_uid, last_uid,
+ first_seq_r, last_seq_r);
+}
+
+static void view_lookup_first(struct mail_index_view *view,
+ enum mail_flags flags, uint8_t flags_mask,
+ uint32_t *seq_r)
+{
+#define LOW_UPDATE(x) \
+ STMT_START { if ((x) > low_uid) low_uid = x; } STMT_END
+ const struct mail_index_header *hdr = &view->map->hdr;
+ const struct mail_index_record *rec;
+ uint32_t seq, seq2, low_uid = 1;
+
+ *seq_r = 0;
+
+ if ((flags_mask & MAIL_SEEN) != 0 && (flags & MAIL_SEEN) == 0)
+ LOW_UPDATE(hdr->first_unseen_uid_lowwater);
+ if ((flags_mask & MAIL_DELETED) != 0 && (flags & MAIL_DELETED) != 0)
+ LOW_UPDATE(hdr->first_deleted_uid_lowwater);
+
+ if (low_uid == 1)
+ seq = 1;
+ else {
+ if (!mail_index_lookup_seq_range(view, low_uid, hdr->next_uid,
+ &seq, &seq2))
+ return;
+ }
+
+ i_assert(hdr->messages_count <= view->map->rec_map->records_count);
+ for (; seq <= hdr->messages_count; seq++) {
+ rec = MAIL_INDEX_REC_AT_SEQ(view->map, seq);
+ if ((rec->flags & flags_mask) == (uint8_t)flags) {
+ *seq_r = seq;
+ break;
+ }
+ }
+}
+
+static void
+mail_index_data_lookup_keywords(struct mail_index_map *map,
+ const unsigned char *data,
+ ARRAY_TYPE(keyword_indexes) *keyword_idx)
+{
+ const unsigned int *keyword_idx_map;
+ unsigned int i, j, keyword_count, index_idx;
+ uint32_t idx, hdr_size;
+ uint16_t record_size, record_align;
+
+ array_clear(keyword_idx);
+ if (data == NULL) {
+ /* no keywords at all in index */
+ return;
+ }
+ (void)mail_index_ext_get_size(map, map->index->keywords_ext_id,
+ &hdr_size, &record_size,
+ &record_align);
+
+ /* keyword_idx_map[] contains file => index keyword mapping */
+ if (!array_is_created(&map->keyword_idx_map))
+ return;
+
+ keyword_idx_map = array_get(&map->keyword_idx_map, &keyword_count);
+ for (i = 0; i < record_size; i++) {
+ /* first do the quick check to see if there's keywords at all */
+ if (data[i] == 0)
+ continue;
+
+ idx = i * CHAR_BIT;
+ for (j = 0; j < CHAR_BIT; j++, idx++) {
+ if ((data[i] & (1 << j)) == 0)
+ continue;
+
+ if (idx >= keyword_count) {
+ /* extra bits set in keyword bytes.
+ shouldn't happen, but just ignore. */
+ break;
+ }
+
+ index_idx = keyword_idx_map[idx];
+ array_push_back(keyword_idx, &index_idx);
+ }
+ }
+}
+
+static void view_lookup_keywords(struct mail_index_view *view, uint32_t seq,
+ ARRAY_TYPE(keyword_indexes) *keyword_idx)
+{
+ struct mail_index_map *map;
+ const void *data;
+
+ mail_index_lookup_ext_full(view, seq, view->index->keywords_ext_id,
+ &map, &data, NULL);
+ mail_index_data_lookup_keywords(map, data, keyword_idx);
+}
+
+static const void *
+view_map_lookup_ext_full(struct mail_index_map *map,
+ const struct mail_index_record *rec, uint32_t ext_id)
+{
+ const struct mail_index_ext *ext;
+ uint32_t idx;
+
+ if (!mail_index_map_get_ext_idx(map, ext_id, &idx))
+ return NULL;
+
+ ext = array_idx(&map->extensions, idx);
+ return ext->record_offset == 0 ? NULL :
+ CONST_PTR_OFFSET(rec, ext->record_offset);
+}
+
+static void
+view_lookup_ext_full(struct mail_index_view *view, uint32_t seq,
+ uint32_t ext_id, struct mail_index_map **map_r,
+ const void **data_r, bool *expunged_r)
+{
+ const struct mail_index_record *rec;
+
+ rec = view->v.lookup_full(view, seq, map_r, expunged_r);
+ *data_r = view_map_lookup_ext_full(*map_r, rec, ext_id);
+}
+
+static void view_get_header_ext(struct mail_index_view *view,
+ struct mail_index_map *map, uint32_t ext_id,
+ const void **data_r, size_t *data_size_r)
+{
+ const struct mail_index_ext *ext;
+ uint32_t idx;
+
+ if (map == NULL) {
+ /* no mapping given, use head mapping */
+ map = view->index->map;
+ }
+
+ if (!mail_index_map_get_ext_idx(map, ext_id, &idx)) {
+ /* extension doesn't exist in this index file */
+ *data_r = NULL;
+ *data_size_r = 0;
+ return;
+ }
+
+ ext = array_idx(&map->extensions, idx);
+ *data_r = MAIL_INDEX_MAP_HDR_OFFSET(map, ext->hdr_offset);
+ *data_size_r = ext->hdr_size;
+}
+
+static bool view_ext_get_reset_id(struct mail_index_view *view ATTR_UNUSED,
+ struct mail_index_map *map,
+ uint32_t ext_id, uint32_t *reset_id_r)
+{
+ const struct mail_index_ext *ext;
+ uint32_t idx;
+
+ if (!mail_index_map_get_ext_idx(map, ext_id, &idx))
+ return FALSE;
+
+ ext = array_idx(&map->extensions, idx);
+ *reset_id_r = ext->reset_id;
+ return TRUE;
+}
+
+void mail_index_view_close(struct mail_index_view **_view)
+{
+ struct mail_index_view *view = *_view;
+
+ *_view = NULL;
+ if (--view->refcount > 0)
+ return;
+
+ i_assert(view->transactions_list == NULL);
+
+ view->v.close(view);
+}
+
+uint32_t mail_index_view_get_messages_count(struct mail_index_view *view)
+{
+ return view->v.get_messages_count(view);
+}
+
+const struct mail_index_header *
+mail_index_get_header(struct mail_index_view *view)
+{
+ return view->v.get_header(view);
+}
+
+const struct mail_index_record *
+mail_index_lookup(struct mail_index_view *view, uint32_t seq)
+{
+ struct mail_index_map *map;
+
+ return mail_index_lookup_full(view, seq, &map, NULL);
+}
+
+const struct mail_index_record *
+mail_index_lookup_full(struct mail_index_view *view, uint32_t seq,
+ struct mail_index_map **map_r, bool *expunged_r)
+{
+ return view->v.lookup_full(view, seq, map_r, expunged_r);
+}
+
+bool mail_index_is_expunged(struct mail_index_view *view, uint32_t seq)
+{
+ struct mail_index_map *map;
+ bool expunged;
+
+ (void)view->v.lookup_full(view, seq, &map, &expunged);
+ return expunged;
+}
+
+void mail_index_map_lookup_keywords(struct mail_index_map *map, uint32_t seq,
+ ARRAY_TYPE(keyword_indexes) *keyword_idx)
+{
+ const struct mail_index_ext *ext;
+ const struct mail_index_record *rec;
+ const void *data;
+ uint32_t idx;
+
+ if (!mail_index_map_get_ext_idx(map, map->index->keywords_ext_id, &idx))
+ data = NULL;
+ else {
+ rec = MAIL_INDEX_REC_AT_SEQ(map, seq);
+ ext = array_idx(&map->extensions, idx);
+ data = ext->record_offset == 0 ? NULL :
+ CONST_PTR_OFFSET(rec, ext->record_offset);
+ }
+ mail_index_data_lookup_keywords(map, data, keyword_idx);
+}
+
+void mail_index_lookup_keywords(struct mail_index_view *view, uint32_t seq,
+ ARRAY_TYPE(keyword_indexes) *keyword_idx)
+{
+ view->v.lookup_keywords(view, seq, keyword_idx);
+}
+
+void mail_index_lookup_view_flags(struct mail_index_view *view, uint32_t seq,
+ enum mail_flags *flags_r,
+ ARRAY_TYPE(keyword_indexes) *keyword_idx)
+{
+ const struct mail_index_record *rec;
+ const unsigned char *keyword_data;
+
+ i_assert(seq > 0 && seq <= mail_index_view_get_messages_count(view));
+
+ rec = MAIL_INDEX_REC_AT_SEQ(view->map, seq);
+ *flags_r = rec->flags;
+
+ keyword_data = view_map_lookup_ext_full(view->map, rec,
+ view->index->keywords_ext_id);
+ mail_index_data_lookup_keywords(view->map, keyword_data, keyword_idx);
+}
+
+void mail_index_lookup_uid(struct mail_index_view *view, uint32_t seq,
+ uint32_t *uid_r)
+{
+ view->v.lookup_uid(view, seq, uid_r);
+}
+
+bool mail_index_lookup_seq_range(struct mail_index_view *view,
+ uint32_t first_uid, uint32_t last_uid,
+ uint32_t *first_seq_r, uint32_t *last_seq_r)
+{
+ view->v.lookup_seq_range(view, first_uid, last_uid,
+ first_seq_r, last_seq_r);
+ return *first_seq_r != 0;
+}
+
+bool mail_index_lookup_seq(struct mail_index_view *view,
+ uint32_t uid, uint32_t *seq_r)
+{
+ view->v.lookup_seq_range(view, uid, uid, seq_r, seq_r);
+ return *seq_r != 0;
+}
+
+void mail_index_lookup_first(struct mail_index_view *view,
+ enum mail_flags flags, uint8_t flags_mask,
+ uint32_t *seq_r)
+{
+ view->v.lookup_first(view, flags, flags_mask, seq_r);
+}
+
+void mail_index_lookup_ext(struct mail_index_view *view, uint32_t seq,
+ uint32_t ext_id, const void **data_r,
+ bool *expunged_r)
+{
+ struct mail_index_map *map;
+
+ mail_index_lookup_ext_full(view, seq, ext_id, &map, data_r, expunged_r);
+}
+
+void mail_index_lookup_ext_full(struct mail_index_view *view, uint32_t seq,
+ uint32_t ext_id, struct mail_index_map **map_r,
+ const void **data_r, bool *expunged_r)
+{
+ view->v.lookup_ext_full(view, seq, ext_id, map_r, data_r, expunged_r);
+}
+
+void mail_index_get_header_ext(struct mail_index_view *view, uint32_t ext_id,
+ const void **data_r, size_t *data_size_r)
+{
+ view->v.get_header_ext(view, NULL, ext_id, data_r, data_size_r);
+}
+
+void mail_index_map_get_header_ext(struct mail_index_view *view,
+ struct mail_index_map *map, uint32_t ext_id,
+ const void **data_r, size_t *data_size_r)
+{
+ view->v.get_header_ext(view, map, ext_id, data_r, data_size_r);
+}
+
+bool mail_index_ext_get_reset_id(struct mail_index_view *view,
+ struct mail_index_map *map,
+ uint32_t ext_id, uint32_t *reset_id_r)
+{
+ return view->v.ext_get_reset_id(view, map, ext_id, reset_id_r);
+}
+
+void mail_index_ext_get_size(struct mail_index_map *map, uint32_t ext_id,
+ uint32_t *hdr_size_r, uint16_t *record_size_r,
+ uint16_t *record_align_r)
+{
+ const struct mail_index_ext *ext;
+ uint32_t idx;
+
+ i_assert(map != NULL);
+
+ if (!mail_index_map_get_ext_idx(map, ext_id, &idx)) {
+ /* extension doesn't exist in this index file */
+ *hdr_size_r = 0;
+ *record_size_r = 0;
+ *record_align_r = 0;
+ return;
+ }
+
+ ext = array_idx(&map->extensions, idx);
+ *hdr_size_r = ext->hdr_size;
+ *record_size_r = ext->record_size;
+ *record_align_r = ext->record_align;
+}
+
+static struct mail_index_view_vfuncs view_vfuncs = {
+ view_close,
+ view_get_messages_count,
+ view_get_header,
+ view_lookup_full,
+ view_lookup_uid,
+ view_lookup_seq_range,
+ view_lookup_first,
+ view_lookup_keywords,
+ view_lookup_ext_full,
+ view_get_header_ext,
+ view_ext_get_reset_id
+};
+
+struct mail_index_view *
+mail_index_view_open_with_map(struct mail_index *index,
+ struct mail_index_map *map)
+{
+ struct mail_index_view *view;
+
+ view = i_new(struct mail_index_view, 1);
+ view->refcount = 1;
+ view->v = view_vfuncs;
+ view->index = index;
+ view->log_view = mail_transaction_log_view_open(index->log);
+
+ view->indexid = index->indexid;
+ view->inconsistency_id = index->inconsistency_id;
+ view->map = map;
+ view->map->refcount++;
+
+ view->log_file_expunge_seq = view->log_file_head_seq =
+ view->map->hdr.log_file_seq;
+ view->log_file_expunge_offset = view->log_file_head_offset =
+ view->map->hdr.log_file_head_offset;
+
+ i_array_init(&view->module_contexts,
+ I_MIN(5, mail_index_module_register.id));
+ DLLIST_PREPEND(&index->views, view);
+ return view;
+}
+
+#undef mail_index_view_open
+struct mail_index_view *
+mail_index_view_open(struct mail_index *index,
+ const char *source_filename, unsigned int source_linenum)
+{
+ struct mail_index_view *view;
+
+ view = mail_index_view_open_with_map(index, index->map);
+ /* these can be used to debug mail_index_view_close() leaks */
+ view->source_filename = source_filename;
+ view->source_linenum = source_linenum;
+ return view;
+}
+
+const struct mail_index_ext *
+mail_index_view_get_ext(struct mail_index_view *view, uint32_t ext_id)
+{
+ uint32_t idx;
+
+ if (!mail_index_map_get_ext_idx(view->map, ext_id, &idx))
+ return NULL;
+
+ return array_idx(&view->map->extensions, idx);
+}
diff --git a/src/lib-index/mail-index-write.c b/src/lib-index/mail-index-write.c
new file mode 100644
index 0000000..689cb9c
--- /dev/null
+++ b/src/lib-index/mail-index-write.c
@@ -0,0 +1,215 @@
+/* Copyright (c) 2003-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "nfs-workarounds.h"
+#include "read-full.h"
+#include "write-full.h"
+#include "ostream.h"
+#include "mail-index-private.h"
+#include "mail-transaction-log-private.h"
+
+#include <stdio.h>
+
+#define MAIL_INDEX_MIN_UPDATE_SIZE 1024
+/* if we're updating >= count-n messages, recreate the index */
+#define MAIL_INDEX_MAX_OVERWRITE_NEG_SEQ_COUNT 10
+
+static int mail_index_create_backup(struct mail_index *index)
+{
+ const char *backup_path, *tmp_backup_path;
+ int ret;
+
+ if (index->fd != -1) {
+ /* we very much want to avoid creating a backup file that
+ hasn't been written to disk yet */
+ if (fdatasync(index->fd) < 0) {
+ mail_index_set_error(index, "fdatasync(%s) failed: %m",
+ index->filepath);
+ return -1;
+ }
+ }
+
+ backup_path = t_strconcat(index->filepath, ".backup", NULL);
+ tmp_backup_path = t_strconcat(backup_path, ".tmp", NULL);
+ ret = link(index->filepath, tmp_backup_path);
+ if (ret < 0 && errno == EEXIST) {
+ if (unlink(tmp_backup_path) < 0 && errno != ENOENT) {
+ mail_index_set_error(index, "unlink(%s) failed: %m",
+ tmp_backup_path);
+ return -1;
+ }
+ ret = link(index->filepath, tmp_backup_path);
+ }
+ if (ret < 0) {
+ if (errno == ENOENT) {
+ /* no dovecot.index file, ignore */
+ return 0;
+ }
+ mail_index_set_error(index, "link(%s, %s) failed: %m",
+ index->filepath, tmp_backup_path);
+ return -1;
+ }
+
+ if (rename(tmp_backup_path, backup_path) < 0) {
+ mail_index_set_error(index, "rename(%s, %s) failed: %m",
+ tmp_backup_path, backup_path);
+ return -1;
+ }
+ return 0;
+}
+
+static int mail_index_recreate(struct mail_index *index)
+{
+ struct mail_index_map *map = index->map;
+ struct ostream *output;
+ unsigned int base_size;
+ const char *path;
+ int ret = 0, fd;
+
+ i_assert(!MAIL_INDEX_IS_IN_MEMORY(index));
+ i_assert(map->hdr.indexid == index->indexid);
+ i_assert((map->hdr.flags & MAIL_INDEX_HDR_FLAG_CORRUPTED) == 0);
+ i_assert(index->indexid != 0);
+
+ fd = mail_index_create_tmp_file(index, index->filepath, &path);
+ if (fd == -1)
+ return -1;
+
+ output = o_stream_create_fd_file(fd, 0, FALSE);
+ o_stream_cork(output);
+
+ struct mail_index_header hdr = map->hdr;
+ /* Write tail_offset the same as head_offset. This function must not
+ be called unless it's safe to do this. See the explanations in
+ mail_index_sync_commit(). */
+ hdr.log_file_tail_offset = hdr.log_file_head_offset;
+
+ base_size = I_MIN(hdr.base_header_size, sizeof(hdr));
+ o_stream_nsend(output, &hdr, base_size);
+ o_stream_nsend(output, MAIL_INDEX_MAP_HDR_OFFSET(map, base_size),
+ hdr.header_size - base_size);
+ o_stream_nsend(output, map->rec_map->records,
+ map->rec_map->records_count * hdr.record_size);
+ if (o_stream_finish(output) < 0) {
+ mail_index_file_set_syscall_error(index, path, "write()");
+ ret = -1;
+ }
+ o_stream_destroy(&output);
+
+ if (ret == 0 && index->set.fsync_mode != FSYNC_MODE_NEVER) {
+ if (fdatasync(fd) < 0) {
+ mail_index_file_set_syscall_error(index, path,
+ "fdatasync()");
+ ret = -1;
+ }
+ }
+
+ if (close(fd) < 0) {
+ mail_index_file_set_syscall_error(index, path, "close()");
+ ret = -1;
+ }
+
+ if ((index->flags & MAIL_INDEX_OPEN_FLAG_KEEP_BACKUPS) != 0)
+ (void)mail_index_create_backup(index);
+
+ if (ret == 0 && rename(path, index->filepath) < 0) {
+ mail_index_set_error(index, "rename(%s, %s) failed: %m",
+ path, index->filepath);
+ ret = -1;
+ }
+
+ if (ret < 0)
+ i_unlink(path);
+ return ret;
+}
+
+static bool mail_index_should_recreate(struct mail_index *index)
+{
+ struct stat st1, st2;
+
+ if (nfs_safe_stat(index->filepath, &st1) < 0) {
+ if (errno != ENOENT) {
+ mail_index_set_syscall_error(index, "stat()");
+ return FALSE;
+ } else if (index->fd == -1) {
+ /* main index hasn't been created yet */
+ return TRUE;
+ } else {
+ /* mailbox was just deleted? don't log an error */
+ return FALSE;
+ }
+ }
+ if (index->fd == -1) {
+ /* main index was just created by another process */
+ return FALSE;
+ }
+ if (fstat(index->fd, &st2) < 0) {
+ if (!ESTALE_FSTAT(errno))
+ mail_index_set_syscall_error(index, "fstat()");
+ return FALSE;
+ }
+ if (st1.st_ino != st2.st_ino ||
+ !CMP_DEV_T(st1.st_dev, st2.st_dev)) {
+ /* Index has already been recreated since we last read it.
+ We can't trust our decisions about whether to recreate it. */
+ return FALSE;
+ }
+ return TRUE;
+}
+
+void mail_index_write(struct mail_index *index, bool want_rotate,
+ const char *reason)
+{
+ struct mail_index_header *hdr = &index->map->hdr;
+ bool rotated = FALSE;
+
+ i_assert(index->log_sync_locked);
+
+ if (index->readonly)
+ return;
+
+ /* rotate the .log before writing index, so the index will point to
+ the latest log. Note that it's the caller's responsibility to make
+ sure that the .log can be safely rotated (i.e. everything has been
+ synced). */
+ if (want_rotate) {
+ if (mail_transaction_log_rotate(index->log, FALSE) == 0) {
+ struct mail_transaction_log_file *file =
+ index->log->head;
+ /* Log rotation refreshes the index, which may cause the
+ map to change. Because we're locked, it's not
+ supposed to happen and will likely lead to an
+ assert-crash below, but we still need to make sure
+ we're using the latest map to do the checks. */
+ hdr = &index->map->hdr;
+ i_assert(file->hdr.prev_file_seq == hdr->log_file_seq);
+ i_assert(file->hdr.prev_file_offset == hdr->log_file_head_offset);
+ hdr->log_file_seq = file->hdr.file_seq;
+ hdr->log_file_head_offset =
+ hdr->log_file_tail_offset = file->hdr.hdr_size;
+ /* Assume .log.2 was created successfully. If it
+ wasn't, it just causes an extra stat() and gets
+ fixed later on. */
+ hdr->log2_rotate_time = ioloop_time;
+ rotated = TRUE;
+ }
+ }
+
+ if (MAIL_INDEX_IS_IN_MEMORY(index))
+ ;
+ else if (!rotated && !mail_index_should_recreate(index)) {
+ /* make sure we don't keep getting back in here */
+ index->reopen_main_index = TRUE;
+ } else {
+ if (mail_index_recreate(index) < 0) {
+ (void)mail_index_move_to_memory(index);
+ return;
+ }
+ event_set_name(index->event, "mail_index_recreated");
+ e_debug(index->event, "Recreated %s (file_seq=%u) because: %s",
+ index->filepath, hdr->log_file_seq, reason);
+ }
+
+ index->main_index_hdr_log_file_seq = hdr->log_file_seq;
+ index->main_index_hdr_log_file_tail_offset = hdr->log_file_tail_offset;
+}
diff --git a/src/lib-index/mail-index.c b/src/lib-index/mail-index.c
new file mode 100644
index 0000000..8f89309
--- /dev/null
+++ b/src/lib-index/mail-index.c
@@ -0,0 +1,1110 @@
+/* Copyright (c) 2003-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "ioloop.h"
+#include "array.h"
+#include "buffer.h"
+#include "eacces-error.h"
+#include "hash.h"
+#include "str-sanitize.h"
+#include "mmap-util.h"
+#include "nfs-workarounds.h"
+#include "read-full.h"
+#include "write-full.h"
+#include "mail-index-alloc-cache.h"
+#include "mail-index-private.h"
+#include "mail-index-view-private.h"
+#include "mail-index-sync-private.h"
+#include "mail-index-modseq.h"
+#include "mail-transaction-log-private.h"
+#include "mail-transaction-log-view-private.h"
+#include "mail-cache.h"
+
+#include <stdio.h>
+#include <stddef.h>
+#include <time.h>
+#include <sys/stat.h>
+#include <ctype.h>
+
+struct mail_index_module_register mail_index_module_register = { 0 };
+
+struct event_category event_category_mail_index = {
+ .name = "mail-index",
+};
+
+static void mail_index_close_nonopened(struct mail_index *index);
+
+static const struct mail_index_optimization_settings default_optimization_set = {
+ .index = {
+ .rewrite_min_log_bytes = 8 * 1024,
+ .rewrite_max_log_bytes = 128 * 1024,
+ },
+ .log = {
+ .min_size = 32 * 1024,
+ .max_size = 1024 * 1024,
+ .min_age_secs = 5 * 60,
+ .log2_max_age_secs = 3600 * 24 * 2,
+ },
+ .cache = {
+ .unaccessed_field_drop_secs = 3600 * 24 * 30,
+ .record_max_size = 64 * 1024,
+ .max_size = 1024 * 1024 * 1024,
+ .purge_min_size = 32 * 1024,
+ .purge_delete_percentage = 20,
+ .purge_continued_percentage = 200,
+ .purge_header_continue_count = 4,
+ },
+};
+
+struct mail_index *mail_index_alloc(struct event *parent_event,
+ const char *dir, const char *prefix)
+{
+ struct mail_index *index;
+
+ index = i_new(struct mail_index, 1);
+ index->dir = i_strdup(dir);
+ index->prefix = i_strdup(prefix);
+ index->fd = -1;
+ index->event = event_create(parent_event);
+ event_add_category(index->event, &event_category_mail_index);
+
+ index->extension_pool =
+ pool_alloconly_create(MEMPOOL_GROWING"index extension", 1024);
+ p_array_init(&index->extensions, index->extension_pool, 5);
+ i_array_init(&index->module_contexts,
+ I_MIN(5, mail_index_module_register.id));
+
+ index->set.mode = 0600;
+ index->set.gid = (gid_t)-1;
+ index->set.lock_method = FILE_LOCK_METHOD_FCNTL;
+ index->set.max_lock_timeout_secs = UINT_MAX;
+ index->optimization_set = default_optimization_set;
+
+ index->keywords_ext_id =
+ mail_index_ext_register(index, MAIL_INDEX_EXT_KEYWORDS,
+ 128, 2, 1);
+ index->keywords_pool = pool_alloconly_create("keywords", 512);
+ i_array_init(&index->keywords, 16);
+ hash_table_create(&index->keywords_hash, index->keywords_pool, 0,
+ strcase_hash, strcasecmp);
+ index->log = mail_transaction_log_alloc(index);
+ mail_index_modseq_init(index);
+ return index;
+}
+
+void mail_index_free(struct mail_index **_index)
+{
+ struct mail_index *index = *_index;
+
+ *_index = NULL;
+
+ i_assert(index->open_count == 0);
+
+ mail_transaction_log_free(&index->log);
+ hash_table_destroy(&index->keywords_hash);
+ pool_unref(&index->extension_pool);
+ pool_unref(&index->keywords_pool);
+
+ array_free(&index->keywords);
+ array_free(&index->module_contexts);
+
+ event_unref(&index->event);
+ i_free(index->set.cache_dir);
+ i_free(index->set.ext_hdr_init_data);
+ i_free(index->set.gid_origin);
+ i_free(index->last_error.text);
+ i_free(index->dir);
+ i_free(index->prefix);
+ i_free(index->need_recreate);
+ i_free(index);
+}
+
+void mail_index_set_cache_dir(struct mail_index *index, const char *dir)
+{
+ i_free(index->set.cache_dir);
+ index->set.cache_dir = i_strdup(dir);
+}
+
+void mail_index_set_fsync_mode(struct mail_index *index,
+ enum fsync_mode mode,
+ enum mail_index_fsync_mask mask)
+{
+ index->set.fsync_mode = mode;
+ index->set.fsync_mask = mask;
+}
+
+bool mail_index_use_existing_permissions(struct mail_index *index)
+{
+ struct stat st;
+
+ if (MAIL_INDEX_IS_IN_MEMORY(index))
+ return FALSE;
+
+ if (stat(index->dir, &st) < 0) {
+ if (errno != ENOENT)
+ e_error(index->event, "stat(%s) failed: %m", index->dir);
+ return FALSE;
+ }
+
+ index->set.mode = st.st_mode & 0666;
+ if (S_ISDIR(st.st_mode) && (st.st_mode & S_ISGID) != 0) {
+ /* directory's GID is used automatically for new files */
+ index->set.gid = (gid_t)-1;
+ } else if ((st.st_mode & 0070) >> 3 == (st.st_mode & 0007)) {
+ /* group has same permissions as world, so don't bother
+ changing it */
+ index->set.gid = (gid_t)-1;
+ } else if (getegid() == st.st_gid) {
+ /* using our own gid, no need to change it */
+ index->set.gid = (gid_t)-1;
+ } else {
+ index->set.gid = st.st_gid;
+ }
+
+ i_free(index->set.gid_origin);
+ if (index->set.gid != (gid_t)-1)
+ index->set.gid_origin = i_strdup("preserved existing GID");
+ return TRUE;
+}
+
+void mail_index_set_permissions(struct mail_index *index,
+ mode_t mode, gid_t gid, const char *gid_origin)
+{
+ index->set.mode = mode & 0666;
+ index->set.gid = gid;
+
+ i_free(index->set.gid_origin);
+ index->set.gid_origin = i_strdup(gid_origin);
+}
+
+void mail_index_set_lock_method(struct mail_index *index,
+ enum file_lock_method lock_method,
+ unsigned int max_timeout_secs)
+{
+ index->set.lock_method = lock_method;
+ index->set.max_lock_timeout_secs = max_timeout_secs;
+}
+
+void mail_index_set_optimization_settings(struct mail_index *index,
+ const struct mail_index_optimization_settings *set)
+{
+ struct mail_index_optimization_settings *dest =
+ &index->optimization_set;
+
+ /* index */
+ if (set->index.rewrite_min_log_bytes != 0)
+ dest->index.rewrite_min_log_bytes = set->index.rewrite_min_log_bytes;
+ if (set->index.rewrite_max_log_bytes != 0)
+ dest->index.rewrite_max_log_bytes = set->index.rewrite_max_log_bytes;
+
+ /* log */
+ if (set->log.min_size != 0)
+ dest->log.min_size = set->log.min_size;
+ if (set->log.max_size != 0)
+ dest->log.max_size = set->log.max_size;
+ if (set->log.min_age_secs != 0)
+ dest->log.min_age_secs = set->log.min_age_secs;
+ if (set->log.log2_max_age_secs != 0)
+ dest->log.log2_max_age_secs = set->log.log2_max_age_secs;
+
+ /* cache */
+ if (set->cache.unaccessed_field_drop_secs != 0)
+ dest->cache.unaccessed_field_drop_secs =
+ set->cache.unaccessed_field_drop_secs;
+ if (set->cache.max_size != 0)
+ dest->cache.max_size = set->cache.max_size;
+ if (set->cache.purge_min_size != 0)
+ dest->cache.purge_min_size = set->cache.purge_min_size;
+ if (set->cache.purge_delete_percentage != 0)
+ dest->cache.purge_delete_percentage =
+ set->cache.purge_delete_percentage;
+ if (set->cache.purge_continued_percentage != 0)
+ dest->cache.purge_continued_percentage =
+ set->cache.purge_continued_percentage;
+ if (set->cache.purge_header_continue_count != 0)
+ dest->cache.purge_header_continue_count =
+ set->cache.purge_header_continue_count;
+ if (set->cache.record_max_size != 0)
+ dest->cache.record_max_size = set->cache.record_max_size;
+}
+
+void mail_index_set_ext_init_data(struct mail_index *index, uint32_t ext_id,
+ const void *data, size_t size)
+{
+ const struct mail_index_registered_ext *rext;
+
+ i_assert(index->set.ext_hdr_init_data == NULL ||
+ index->set.ext_hdr_init_id == ext_id);
+
+ rext = array_idx(&index->extensions, ext_id);
+ i_assert(rext->hdr_size == size);
+
+ index->set.ext_hdr_init_id = ext_id;
+ i_free(index->set.ext_hdr_init_data);
+ index->set.ext_hdr_init_data = i_malloc(size);
+ memcpy(index->set.ext_hdr_init_data, data, size);
+}
+
+bool mail_index_ext_name_is_valid(const char *name)
+{
+ size_t i;
+
+ for (i = 0; name[i] != '\0'; i++) {
+ if (!i_isalnum(name[i]) && name[i] != '-' && name[i] != '_' &&
+ name[i] != ' ')
+ return FALSE;
+
+ }
+ return i == 0 || i < MAIL_INDEX_EXT_NAME_MAX_LENGTH;
+}
+
+uint32_t mail_index_ext_register(struct mail_index *index, const char *name,
+ uint32_t default_hdr_size,
+ uint16_t default_record_size,
+ uint16_t default_record_align)
+{
+ struct mail_index_registered_ext rext;
+ uint32_t ext_id;
+
+ if (!mail_index_ext_name_is_valid(name))
+ i_panic("mail_index_ext_register(%s): Invalid name", name);
+
+ if (default_record_size != 0 && default_record_align == 0) {
+ i_panic("mail_index_ext_register(%s): "
+ "Invalid record alignment", name);
+ }
+
+ if (mail_index_ext_lookup(index, name, &ext_id))
+ return ext_id;
+
+ i_zero(&rext);
+ rext.name = p_strdup(index->extension_pool, name);
+ rext.index_idx = array_count(&index->extensions);
+ rext.hdr_size = default_hdr_size;
+ rext.record_size = default_record_size;
+ rext.record_align = default_record_align;
+
+ array_push_back(&index->extensions, &rext);
+ return rext.index_idx;
+}
+
+void mail_index_ext_register_resize_defaults(struct mail_index *index,
+ uint32_t ext_id,
+ uint32_t default_hdr_size,
+ uint16_t default_record_size,
+ uint16_t default_record_align)
+{
+ struct mail_index_registered_ext *rext;
+
+ rext = array_idx_modifiable(&index->extensions, ext_id);
+ rext->hdr_size = default_hdr_size;
+ rext->record_size = default_record_size;
+ rext->record_align = default_record_align;
+}
+
+bool mail_index_ext_lookup(struct mail_index *index, const char *name,
+ uint32_t *ext_id_r)
+{
+ const struct mail_index_registered_ext *extensions;
+ unsigned int i, count;
+
+ extensions = array_get(&index->extensions, &count);
+ for (i = 0; i < count; i++) {
+ if (strcmp(extensions[i].name, name) == 0) {
+ *ext_id_r = i;
+ return TRUE;
+ }
+ }
+
+ *ext_id_r = (uint32_t)-1;
+ return FALSE;
+}
+
+void mail_index_register_expunge_handler(struct mail_index *index,
+ uint32_t ext_id,
+ mail_index_expunge_handler_t *cb)
+{
+ struct mail_index_registered_ext *rext;
+
+ rext = array_idx_modifiable(&index->extensions, ext_id);
+ i_assert(rext->expunge_handler == NULL || rext->expunge_handler == cb);
+
+ rext->expunge_handler = cb;
+}
+
+void mail_index_unregister_expunge_handler(struct mail_index *index,
+ uint32_t ext_id)
+{
+ struct mail_index_registered_ext *rext;
+
+ rext = array_idx_modifiable(&index->extensions, ext_id);
+ i_assert(rext->expunge_handler != NULL);
+
+ rext->expunge_handler = NULL;
+}
+
+bool mail_index_keyword_lookup(struct mail_index *index,
+ const char *keyword, unsigned int *idx_r)
+{
+ char *key;
+ void *value;
+
+ /* keywords_hash keeps a name => index mapping of keywords.
+ Keywords are never removed from it, so the index values are valid
+ for the lifetime of the mail_index. */
+ if (hash_table_lookup_full(index->keywords_hash, keyword,
+ &key, &value)) {
+ *idx_r = POINTER_CAST_TO(value, unsigned int);
+ return TRUE;
+ }
+
+ *idx_r = UINT_MAX;
+ return FALSE;
+}
+
+void mail_index_keyword_lookup_or_create(struct mail_index *index,
+ const char *keyword,
+ unsigned int *idx_r)
+{
+ char *keyword_dup;
+
+ i_assert(*keyword != '\0');
+
+ if (mail_index_keyword_lookup(index, keyword, idx_r))
+ return;
+
+ keyword = keyword_dup = p_strdup(index->keywords_pool, keyword);
+ *idx_r = array_count(&index->keywords);
+
+ hash_table_insert(index->keywords_hash, keyword_dup,
+ POINTER_CAST(*idx_r));
+ array_push_back(&index->keywords, &keyword);
+
+ /* keep the array NULL-terminated, but the NULL itself invisible */
+ array_append_zero(&index->keywords);
+ array_pop_back(&index->keywords);
+}
+
+const ARRAY_TYPE(keywords) *mail_index_get_keywords(struct mail_index *index)
+{
+ return &index->keywords;
+}
+
+struct mail_keywords *
+mail_index_keywords_create(struct mail_index *index,
+ const char *const keywords[])
+{
+ struct mail_keywords *k;
+ unsigned int src, dest, i, count;
+
+ count = str_array_length(keywords);
+ if (count == 0) {
+ k = i_new(struct mail_keywords, 1);
+ k->index = index;
+ k->refcount = 1;
+ return k;
+ }
+
+ /* @UNSAFE */
+ k = i_malloc(MALLOC_ADD(sizeof(struct mail_keywords),
+ MALLOC_MULTIPLY(sizeof(k->idx[0]), count)));
+ k->index = index;
+ k->refcount = 1;
+
+ /* look up the keywords from index. they're never removed from there
+ so we can permanently store indexes to them. */
+ for (src = dest = 0; src < count; src++) {
+ mail_index_keyword_lookup_or_create(index, keywords[src],
+ &k->idx[dest]);
+ /* ignore if this is a duplicate */
+ for (i = 0; i < src; i++) {
+ if (k->idx[i] == k->idx[dest])
+ break;
+ }
+ if (i == src)
+ dest++;
+ }
+ k->count = dest;
+ return k;
+}
+
+struct mail_keywords *
+mail_index_keywords_create_from_indexes(struct mail_index *index,
+ const ARRAY_TYPE(keyword_indexes)
+ *keyword_indexes)
+{
+ struct mail_keywords *k;
+ const unsigned int *indexes;
+ unsigned int src, dest, i, count;
+
+ indexes = array_get(keyword_indexes, &count);
+ if (count == 0) {
+ k = i_new(struct mail_keywords, 1);
+ k->index = index;
+ k->refcount = 1;
+ return k;
+ }
+
+ /* @UNSAFE */
+ k = i_malloc(MALLOC_ADD(sizeof(struct mail_keywords),
+ MALLOC_MULTIPLY(sizeof(k->idx[0]), count)));
+ k->index = index;
+ k->refcount = 1;
+
+ /* copy but skip duplicates */
+ for (src = dest = 0; src < count; src++) {
+ for (i = 0; i < src; i++) {
+ if (k->idx[i] == indexes[src])
+ break;
+ }
+ if (i == src)
+ k->idx[dest++] = indexes[src];
+ }
+ k->count = dest;
+ return k;
+}
+
+void mail_index_keywords_ref(struct mail_keywords *keywords)
+{
+ keywords->refcount++;
+}
+
+void mail_index_keywords_unref(struct mail_keywords **_keywords)
+{
+ struct mail_keywords *keywords = *_keywords;
+
+ i_assert(keywords->refcount > 0);
+
+ *_keywords = NULL;
+ if (--keywords->refcount == 0)
+ i_free(keywords);
+}
+
+int mail_index_try_open_only(struct mail_index *index)
+{
+ i_assert(index->fd == -1);
+ i_assert(!MAIL_INDEX_IS_IN_MEMORY(index));
+
+ /* Note that our caller must close index->fd by itself. */
+ if (index->readonly)
+ errno = EACCES;
+ else {
+ index->fd = nfs_safe_open(index->filepath, O_RDWR);
+ index->readonly = FALSE;
+ }
+
+ if (index->fd == -1 && errno == EACCES) {
+ index->fd = open(index->filepath, O_RDONLY);
+ index->readonly = TRUE;
+ }
+
+ if (index->fd == -1) {
+ if (errno != ENOENT) {
+ mail_index_set_syscall_error(index, "open()");
+ return -1;
+ }
+
+ /* have to create it */
+ return 0;
+ }
+ return 1;
+}
+
+static int
+mail_index_try_open(struct mail_index *index)
+{
+ int ret;
+
+ i_assert(index->fd == -1);
+
+ if (MAIL_INDEX_IS_IN_MEMORY(index))
+ return 0;
+
+ ret = mail_index_map(index, MAIL_INDEX_SYNC_HANDLER_HEAD);
+ if (ret == 0 && !index->readonly) {
+ /* it's corrupted - recreate it */
+ if (index->fd != -1) {
+ if (close(index->fd) < 0)
+ mail_index_set_syscall_error(index, "close()");
+ index->fd = -1;
+ }
+ }
+ return ret;
+}
+
+int mail_index_create_tmp_file(struct mail_index *index,
+ const char *path_prefix, const char **path_r)
+{
+ mode_t old_mask;
+ const char *path;
+ int fd;
+
+ i_assert(!MAIL_INDEX_IS_IN_MEMORY(index));
+
+ path = *path_r = t_strconcat(path_prefix, ".tmp", NULL);
+ old_mask = umask(0);
+ fd = open(path, O_RDWR|O_CREAT|O_EXCL, index->set.mode);
+ umask(old_mask);
+ if (fd == -1 && errno == EEXIST) {
+ /* stale temp file. unlink and recreate rather than overwriting,
+ just to make sure locking problems won't cause corruption */
+ if (i_unlink(path) < 0)
+ return -1;
+ old_mask = umask(0);
+ fd = open(path, O_RDWR|O_CREAT|O_EXCL, index->set.mode);
+ umask(old_mask);
+ }
+ if (fd == -1) {
+ mail_index_file_set_syscall_error(index, path, "creat()");
+ return -1;
+ }
+
+ mail_index_fchown(index, fd, path);
+ return fd;
+}
+
+static const char *mail_index_get_cache_path(struct mail_index *index)
+{
+ const char *dir;
+
+ if (index->set.cache_dir != NULL)
+ dir = index->set.cache_dir;
+ else if (index->dir != NULL)
+ dir = index->dir;
+ else
+ return NULL;
+ return t_strconcat(dir, "/", index->prefix,
+ MAIL_CACHE_FILE_SUFFIX, NULL);
+}
+
+static int mail_index_open_files(struct mail_index *index,
+ enum mail_index_open_flags flags)
+{
+ int ret;
+
+ ret = mail_transaction_log_open(index->log);
+ if (ret == 0) {
+ if ((flags & MAIL_INDEX_OPEN_FLAG_CREATE) == 0)
+ return 0;
+
+ /* if dovecot.index exists, read it first so that we can get
+ the correct indexid and log sequence */
+ (void)mail_index_try_open(index);
+
+ if (index->indexid == 0) {
+ /* Create a new indexid for us. If we're opening index
+ into memory, index->map doesn't exist yet. */
+ index->indexid = ioloop_time;
+ index->initial_create = TRUE;
+ if (index->map != NULL)
+ index->map->hdr.indexid = index->indexid;
+ }
+
+ ret = mail_transaction_log_create(index->log, FALSE);
+ if (index->map != NULL) {
+ /* log creation could have changed it if someone else
+ just created it. */
+ index->map->hdr.indexid = index->indexid;
+ }
+ index->initial_create = FALSE;
+ }
+ if (ret >= 0) {
+ ret = index->map != NULL ? 1 : mail_index_try_open(index);
+ if (ret == 0 && !index->readonly) {
+ /* corrupted */
+ mail_transaction_log_close(index->log);
+ ret = mail_transaction_log_create(index->log, TRUE);
+ if (ret == 0) {
+ if (index->map != NULL)
+ mail_index_unmap(&index->map);
+ index->map = mail_index_map_alloc(index);
+ }
+ }
+ }
+ if (ret < 0) {
+ /* open/create failed, fallback to in-memory indexes */
+ if ((flags & MAIL_INDEX_OPEN_FLAG_CREATE) == 0)
+ return -1;
+
+ if (mail_index_move_to_memory(index) < 0)
+ return -1;
+ }
+
+ if (index->cache == NULL) {
+ const char *path = mail_index_get_cache_path(index);
+ index->cache = mail_cache_open_or_create_path(index, path);
+ }
+ return 1;
+}
+
+static int
+mail_index_open_opened(struct mail_index *index,
+ enum mail_index_open_flags flags)
+{
+ int ret;
+
+ i_assert(index->map != NULL);
+
+ if ((index->map->hdr.flags & MAIL_INDEX_HDR_FLAG_CORRUPTED) != 0) {
+ /* index was marked corrupted. we'll probably need to
+ recreate the files. */
+ mail_index_unmap(&index->map);
+ mail_index_close_file(index);
+ mail_transaction_log_close(index->log);
+ if ((ret = mail_index_open_files(index, flags)) <= 0)
+ return ret;
+ }
+
+ index->open_count++;
+ return 1;
+}
+
+int mail_index_open(struct mail_index *index, enum mail_index_open_flags flags)
+{
+ int ret;
+
+ if (index->open_count > 0) {
+ if ((ret = mail_index_open_opened(index, flags)) <= 0) {
+ /* doesn't exist and create flag not used */
+ }
+ return ret;
+ }
+
+ index->filepath = MAIL_INDEX_IS_IN_MEMORY(index) ?
+ i_strdup("(in-memory index)") :
+ i_strconcat(index->dir, "/", index->prefix, NULL);
+
+ mail_index_reset_error(index);
+ index->readonly = FALSE;
+ index->log_sync_locked = FALSE;
+ index->flags = flags;
+ index->readonly = (flags & MAIL_INDEX_OPEN_FLAG_READONLY) != 0;
+ if ((flags & MAIL_INDEX_OPEN_FLAG_DEBUG) != 0)
+ event_set_forced_debug(index->event, TRUE);
+ else
+ event_unset_forced_debug(index->event);
+
+ if ((flags & MAIL_INDEX_OPEN_FLAG_NFS_FLUSH) != 0 &&
+ index->set.fsync_mode != FSYNC_MODE_ALWAYS)
+ i_fatal("nfs flush requires mail_fsync=always");
+ if ((flags & MAIL_INDEX_OPEN_FLAG_NFS_FLUSH) != 0 &&
+ (flags & MAIL_INDEX_OPEN_FLAG_MMAP_DISABLE) == 0)
+ i_fatal("nfs flush requires mmap_disable=yes");
+
+ /* NOTE: increase open_count only after mail_index_open_files().
+ it's used elsewhere to check if we're doing an initial opening
+ of the index files */
+ if ((ret = mail_index_open_files(index, flags)) <= 0) {
+ /* doesn't exist and create flag not used */
+ mail_index_close_nonopened(index);
+ return ret;
+ }
+
+ index->open_count++;
+
+ if (index->log->head == NULL) {
+ mail_index_close(index);
+ mail_index_set_error(index, "Index is corrupted "
+ "(log->view->head == NULL)");
+ return -1;
+ }
+
+ i_assert(index->map != NULL);
+ mail_index_alloc_cache_index_opened(index);
+ return 1;
+}
+
+int mail_index_open_or_create(struct mail_index *index,
+ enum mail_index_open_flags flags)
+{
+ int ret;
+
+ flags |= MAIL_INDEX_OPEN_FLAG_CREATE;
+ ret = mail_index_open(index, flags);
+ i_assert(ret != 0);
+ return ret < 0 ? -1 : 0;
+}
+
+void mail_index_close_file(struct mail_index *index)
+{
+ if (index->fd != -1) {
+ if (close(index->fd) < 0)
+ mail_index_set_syscall_error(index, "close()");
+ index->fd = -1;
+ }
+}
+
+static void mail_index_close_nonopened(struct mail_index *index)
+{
+ i_assert(!index->syncing);
+
+ if (index->views != NULL) {
+ i_panic("Leaked view for index %s: Opened in %s:%u",
+ index->filepath, index->views->source_filename,
+ index->views->source_linenum);
+ }
+ i_assert(index->views == NULL);
+
+ if (index->map != NULL)
+ mail_index_unmap(&index->map);
+
+ mail_index_close_file(index);
+ mail_transaction_log_close(index->log);
+ if (index->cache != NULL)
+ mail_cache_free(&index->cache);
+
+ i_free_and_null(index->filepath);
+
+ index->indexid = 0;
+}
+
+void mail_index_close(struct mail_index *index)
+{
+ i_assert(index->open_count > 0);
+
+ mail_index_alloc_cache_index_closing(index);
+ if (--index->open_count == 0)
+ mail_index_close_nonopened(index);
+}
+
+int mail_index_unlink(struct mail_index *index)
+{
+ const char *path;
+ int last_errno = 0;
+
+ if (MAIL_INDEX_IS_IN_MEMORY(index) || index->readonly)
+ return 0;
+
+ /* main index */
+ if (unlink(index->filepath) < 0 && errno != ENOENT)
+ last_errno = errno;
+
+ /* logs */
+ path = t_strconcat(index->filepath, MAIL_TRANSACTION_LOG_SUFFIX, NULL);
+ if (unlink(path) < 0 && errno != ENOENT)
+ last_errno = errno;
+
+ path = t_strconcat(index->filepath,
+ MAIL_TRANSACTION_LOG_SUFFIX".2", NULL);
+ if (unlink(path) < 0 && errno != ENOENT)
+ last_errno = errno;
+
+ /* cache */
+ path = t_strconcat(index->filepath, MAIL_CACHE_FILE_SUFFIX, NULL);
+ if (unlink(path) < 0 && errno != ENOENT)
+ last_errno = errno;
+
+ if (last_errno == 0)
+ return 0;
+ else {
+ errno = last_errno;
+ return -1;
+ }
+}
+
+int mail_index_reopen_if_changed(struct mail_index *index, bool *reopened_r,
+ const char **reason_r)
+{
+ struct stat st1, st2;
+ int ret;
+
+ *reopened_r = FALSE;
+
+ if (MAIL_INDEX_IS_IN_MEMORY(index)) {
+ *reason_r = "in-memory index";
+ return 0;
+ }
+
+ if (index->fd == -1)
+ goto final;
+
+ if ((index->flags & MAIL_INDEX_OPEN_FLAG_NFS_FLUSH) != 0)
+ nfs_flush_file_handle_cache(index->filepath);
+ if (nfs_safe_stat(index->filepath, &st2) < 0) {
+ if (errno == ENOENT) {
+ *reason_r = "index not found via stat()";
+ return 0;
+ }
+ mail_index_set_syscall_error(index, "stat()");
+ return -1;
+ }
+
+ if (fstat(index->fd, &st1) < 0) {
+ if (!ESTALE_FSTAT(errno)) {
+ mail_index_set_syscall_error(index, "fstat()");
+ return -1;
+ }
+ /* deleted/recreated, reopen */
+ *reason_r = "index is stale";
+ } else if (st1.st_ino == st2.st_ino &&
+ CMP_DEV_T(st1.st_dev, st2.st_dev)) {
+ /* the same file */
+ *reason_r = "index unchanged";
+ return 1;
+ } else {
+ *reason_r = "index inode changed";
+ }
+
+ /* new file, new locks. the old fd can keep its locks, they don't
+ matter anymore as no-one's going to modify the file. */
+ mail_index_close_file(index);
+
+final:
+ if ((ret = mail_index_try_open_only(index)) == 0)
+ *reason_r = "index not found via open()";
+ else if (ret > 0) {
+ *reason_r = "index opened";
+ *reopened_r = TRUE;
+ }
+ return ret;
+}
+
+int mail_index_refresh(struct mail_index *index)
+{
+ int ret;
+
+ ret = mail_index_map(index, MAIL_INDEX_SYNC_HANDLER_HEAD);
+ return ret <= 0 ? -1 : 0;
+}
+
+struct mail_cache *mail_index_get_cache(struct mail_index *index)
+{
+ return index->cache;
+}
+
+void mail_index_set_error(struct mail_index *index, const char *fmt, ...)
+{
+ va_list va;
+
+ i_free(index->last_error.text);
+
+ if (fmt == NULL)
+ index->last_error.text = NULL;
+ else {
+ va_start(va, fmt);
+ index->last_error.text = i_strdup_vprintf(fmt, va);
+ va_end(va);
+
+ e_error(index->event, "%s", index->last_error.text);
+ }
+}
+
+void mail_index_set_error_nolog(struct mail_index *index, const char *str)
+{
+ i_assert(str != NULL);
+
+ char *old_error = index->last_error.text;
+ index->last_error.text = i_strdup(str);
+ i_free(old_error);
+}
+
+bool mail_index_is_in_memory(struct mail_index *index)
+{
+ return MAIL_INDEX_IS_IN_MEMORY(index);
+}
+
+static void mail_index_set_as_in_memory(struct mail_index *index)
+{
+ i_free_and_null(index->dir);
+
+ i_free(index->filepath);
+ index->filepath = i_strdup("(in-memory index)");
+}
+
+int mail_index_move_to_memory(struct mail_index *index)
+{
+ struct mail_index_map *map;
+
+ if (MAIL_INDEX_IS_IN_MEMORY(index))
+ return index->map == NULL ? -1 : 0;
+
+ if ((index->flags & MAIL_INDEX_OPEN_FLAG_NEVER_IN_MEMORY) != 0)
+ return -1;
+
+ if (index->map == NULL) {
+ /* index was never even opened. just mark it as being in
+ memory and let the caller re-open the index. */
+ i_assert(index->fd == -1);
+ mail_index_set_as_in_memory(index);
+ return -1;
+ }
+
+ /* move index map to memory */
+ if (!MAIL_INDEX_MAP_IS_IN_MEMORY(index->map)) {
+ map = mail_index_map_clone(index->map);
+ mail_index_unmap(&index->map);
+ index->map = map;
+ }
+
+ if (index->log != NULL) {
+ /* move transaction log to memory */
+ if (mail_transaction_log_move_to_memory(index->log) < 0)
+ return -1;
+ }
+
+ if (index->fd != -1) {
+ if (close(index->fd) < 0)
+ mail_index_set_syscall_error(index, "close()");
+ index->fd = -1;
+ }
+ mail_index_set_as_in_memory(index);
+ return 0;
+}
+
+void mail_index_mark_corrupted(struct mail_index *index)
+{
+ index->indexid = 0;
+
+ index->map->hdr.flags |= MAIL_INDEX_HDR_FLAG_CORRUPTED;
+ if (!index->readonly) {
+ if (unlink(index->filepath) < 0 &&
+ errno != ENOENT && errno != ESTALE)
+ mail_index_set_syscall_error(index, "unlink()");
+ (void)mail_transaction_log_unlink(index->log);
+ }
+}
+
+bool mail_index_is_deleted(struct mail_index *index)
+{
+ return index->index_delete_requested || index->index_deleted;
+}
+
+int mail_index_get_modification_time(struct mail_index *index, time_t *mtime_r)
+{
+ struct stat st;
+ const char *path;
+
+ *mtime_r = 0;
+ if (MAIL_INDEX_IS_IN_MEMORY(index)) {
+ /* this function doesn't make sense for in-memory indexes */
+ return 0;
+ }
+
+ /* index may not be open, so index->filepath may be NULL */
+ path = t_strconcat(index->dir, "/", index->prefix,
+ MAIL_TRANSACTION_LOG_SUFFIX, NULL);
+ if (stat(path, &st) < 0) {
+ if (errno == ENOENT) {
+ /* .log is always supposed to exist - don't bother
+ trying to stat(dovecot.index) */
+ return 0;
+ }
+ mail_index_file_set_syscall_error(index, path, "stat()");
+ return -1;
+ }
+ *mtime_r = st.st_mtime;
+ return 0;
+}
+
+void mail_index_fchown(struct mail_index *index, int fd, const char *path)
+{
+ mode_t mode;
+
+ if (index->set.gid == (gid_t)-1) {
+ /* no gid changing */
+ return;
+ } else if (fchown(fd, (uid_t)-1, index->set.gid) == 0) {
+ /* success */
+ return;
+ } if ((index->set.mode & 0060) >> 3 == (index->set.mode & 0006)) {
+ /* group and world permissions are the same, so group doesn't
+ really matter. ignore silently. */
+ return;
+ }
+ if (errno != EPERM)
+ mail_index_file_set_syscall_error(index, path, "fchown()");
+ else {
+ mail_index_set_error(index, "%s",
+ eperm_error_get_chgrp("fchown", path, index->set.gid,
+ index->set.gid_origin));
+ }
+
+ /* continue, but change permissions so that only the common
+ subset of group and world is used. this makes sure no one
+ gets any extra permissions. */
+ mode = ((index->set.mode & 0060) >> 3) & (index->set.mode & 0006);
+ mode |= (mode << 3) | (index->set.mode & 0600);
+ if (fchmod(fd, mode) < 0)
+ mail_index_file_set_syscall_error(index, path, "fchmod()");
+}
+
+int mail_index_lock_sync(struct mail_index *index, const char *lock_reason)
+{
+ uint32_t file_seq;
+ uoff_t file_offset;
+
+ return mail_transaction_log_sync_lock(index->log, lock_reason,
+ &file_seq, &file_offset);
+}
+
+void mail_index_unlock(struct mail_index *index, const char *long_lock_reason)
+{
+ mail_transaction_log_sync_unlock(index->log, long_lock_reason);
+}
+
+bool mail_index_is_locked(struct mail_index *index)
+{
+ return index->log_sync_locked;
+}
+
+void mail_index_set_syscall_error(struct mail_index *index,
+ const char *function)
+{
+ mail_index_file_set_syscall_error(index, index->filepath, function);
+}
+
+void mail_index_file_set_syscall_error(struct mail_index *index,
+ const char *filepath,
+ const char *function)
+{
+ const char *errstr;
+
+ i_assert(filepath != NULL);
+ i_assert(function != NULL);
+
+ if (errno == ENOENT) {
+ struct stat st;
+ int old_errno = errno;
+ i_assert(index->log->filepath != NULL);
+ if (nfs_safe_stat(index->log->filepath, &st) < 0 &&
+ errno == ENOENT) {
+ /* the index log has gone away */
+ index->index_deleted = TRUE;
+ errno = old_errno;
+ return;
+ }
+ errno = old_errno;
+ }
+
+ if (ENOSPACE(errno)) {
+ index->last_error.nodiskspace = TRUE;
+ if ((index->flags & MAIL_INDEX_OPEN_FLAG_NEVER_IN_MEMORY) == 0)
+ return;
+ }
+
+ if (errno == EACCES) {
+ function = t_strcut(function, '(');
+ if (strcmp(function, "creat") == 0 ||
+ str_begins(function, "file_dotlock_"))
+ errstr = eacces_error_get_creating(function, filepath);
+ else
+ errstr = eacces_error_get(function, filepath);
+ mail_index_set_error(index, "%s", errstr);
+ } else {
+ const char *suffix = errno != EFBIG ? "" :
+ " (process was started with ulimit -f limit)";
+ mail_index_set_error(index, "%s failed with file %s: "
+ "%m%s", function, filepath, suffix);
+ }
+}
+
+const char *mail_index_get_error_message(struct mail_index *index)
+{
+ return index->last_error.text;
+}
+
+void mail_index_reset_error(struct mail_index *index)
+{
+ i_free(index->last_error.text);
+ i_zero(&index->last_error);
+}
diff --git a/src/lib-index/mail-index.h b/src/lib-index/mail-index.h
new file mode 100644
index 0000000..c1947cf
--- /dev/null
+++ b/src/lib-index/mail-index.h
@@ -0,0 +1,817 @@
+#ifndef MAIL_INDEX_H
+#define MAIL_INDEX_H
+
+#include "file-lock.h"
+#include "fsync-mode.h"
+#include "guid.h"
+#include "mail-types.h"
+#include "seq-range-array.h"
+
+#define MAIL_INDEX_MAJOR_VERSION 7
+#define MAIL_INDEX_MINOR_VERSION 3
+
+#define MAIL_INDEX_HEADER_MIN_SIZE 120
+
+/* Log a warning when transaction log has been locked for this many seconds.
+ This lock is held also between mail_index_sync_begin()..commit(). */
+#define MAIL_TRANSACTION_LOG_LOCK_WARN_SECS 30
+
+enum mail_index_open_flags {
+ /* Create index if it doesn't exist */
+ MAIL_INDEX_OPEN_FLAG_CREATE = 0x01,
+ /* Don't try to mmap() index files */
+ MAIL_INDEX_OPEN_FLAG_MMAP_DISABLE = 0x04,
+ /* Rely on O_EXCL when creating dotlocks */
+ MAIL_INDEX_OPEN_FLAG_DOTLOCK_USE_EXCL = 0x10,
+ /* Flush NFS attr/data/write cache when necessary */
+ MAIL_INDEX_OPEN_FLAG_NFS_FLUSH = 0x40,
+ /* Open the index read-only */
+ MAIL_INDEX_OPEN_FLAG_READONLY = 0x80,
+ /* Create backups of dovecot.index files once in a while */
+ MAIL_INDEX_OPEN_FLAG_KEEP_BACKUPS = 0x100,
+ /* If we run out of disk space, fail modifications instead of moving
+ indexes to memory. */
+ MAIL_INDEX_OPEN_FLAG_NEVER_IN_MEMORY = 0x200,
+ /* We're only going to save new messages to the index.
+ Avoid unnecessary reads. */
+ MAIL_INDEX_OPEN_FLAG_SAVEONLY = 0x400,
+ /* Enable debug logging */
+ MAIL_INDEX_OPEN_FLAG_DEBUG = 0x800,
+ /* MAIL_INDEX_MAIL_FLAG_DIRTY can be used as a backend-specific flag.
+ All special handling of the flag is disabled by this. */
+ MAIL_INDEX_OPEN_FLAG_NO_DIRTY = 0x1000,
+};
+
+enum mail_index_header_compat_flags {
+ /* All fields in these index files are in little-endian format.
+ If the current CPU endianess doesn't match this, the indexes can't
+ be used. There is currently no support to translate endianess. */
+ MAIL_INDEX_COMPAT_LITTLE_ENDIAN = 0x01
+};
+
+enum mail_index_header_flag {
+ /* mail_index_mark_corrupted() was just called by this process.
+ Reopen or recreate it. This flag is never actually written to
+ disk. */
+ MAIL_INDEX_HDR_FLAG_CORRUPTED = 0x0001,
+ /* There are messages with MAIL_INDEX_MAIL_FLAG_DIRTY flag. */
+ MAIL_INDEX_HDR_FLAG_HAVE_DIRTY = 0x0002,
+ /* Index has been fsck'd. The caller may want to resync the index
+ to make sure it's valid and drop this flag. */
+ MAIL_INDEX_HDR_FLAG_FSCKD = 0x0004,
+};
+
+enum mail_index_mail_flags {
+ /* This flag used to contain MAIL_RECENT flag, but is always zero
+ with the current index file format. */
+ MAIL_INDEX_MAIL_FLAG_UNUSED = 0x20,
+ /* For private use by backend. Replacing flags doesn't change this. */
+ MAIL_INDEX_MAIL_FLAG_BACKEND = 0x40,
+ /* Message flags haven't been written to backend. If
+ MAIL_INDEX_OPEN_FLAG_NO_DIRTY is set, this is treated as a
+ backend-specific flag with no special internal handling. */
+ MAIL_INDEX_MAIL_FLAG_DIRTY = 0x80,
+
+ /* Force updating this message's modseq via a flag update record.
+ Note that this flag isn't saved to disk. */
+ MAIL_INDEX_MAIL_FLAG_UPDATE_MODSEQ = 0x100
+};
+
+#define MAIL_INDEX_FLAGS_MASK \
+ (MAIL_ANSWERED | MAIL_FLAGGED | MAIL_DELETED | MAIL_SEEN | MAIL_DRAFT)
+
+struct mail_index_header {
+ /* Major version is increased only when you can't have backwards
+ compatibility. If the field doesn't match MAIL_INDEX_MAJOR_VERSION,
+ don't even try to read it. */
+ uint8_t major_version;
+ /* Minor version is increased when the file format changes in a
+ backwards compatible way. If the field is smaller than
+ MAIL_INDEX_MINOR_VERSION, upgrade the file format and update the
+ minor_version field as well. If minor_version is higher than
+ MAIL_INDEX_MINOR_VERSION, leave it as it is. It likely means that a
+ new Dovecot version is currently being upgraded to, but the file was
+ still accessed by an old version. */
+ uint8_t minor_version;
+
+ /* sizeof(struct mail_index_header) when creating a new index. If the
+ header is smaller, fill the missing fields with 0. If the header is
+ larger, preserve the size and unknown fields. */
+ uint16_t base_header_size;
+ uint32_t header_size; /* base + extended header size */
+ /* sizeof(struct mail_index_record) + extensions */
+ uint32_t record_size;
+
+ uint8_t compat_flags; /* enum mail_index_header_compat_flags */
+ uint8_t unused[3];
+
+ /* Unique index file ID. Initialized with the current UNIX timestamp.
+ This is used to make sure that the main index, transaction log and
+ cache file are all part of the same index. */
+ uint32_t indexid;
+ uint32_t flags; /* enum mail_index_header_flag */
+
+ /* IMAP UIDVALIDITY. Initially can be 0, but must be non-0 after the
+ first mailbox sync. The UIDVALIDITY shouldn't normally change after
+ the mailbox is created. */
+ uint32_t uid_validity;
+ /* UID for the next saved message (must not be lower than this). This
+ value can only increase. */
+ uint32_t next_uid;
+
+ /* Number of messages in the index */
+ uint32_t messages_count;
+ uint32_t unused_old_recent_messages_count;
+ /* Number of messages with MAIL_SEEN flag */
+ uint32_t seen_messages_count;
+ /* Number of messages with MAIL_DELETED flag */
+ uint32_t deleted_messages_count;
+
+ /* The specified UID and all mails after it have MAIL_RECENT flag */
+ uint32_t first_recent_uid;
+ /* There are no UIDs lower than this without MAIL_SEEN flag. There are
+ no guarantees whether this UID has MAIL_SEEN flag, or whether the it
+ even exists. Used to optimize finding the first unseen message. */
+ uint32_t first_unseen_uid_lowwater;
+ /* Similarly to above, used to optimize finding the first deleted
+ message. */
+ uint32_t first_deleted_uid_lowwater;
+
+ /* The index is synced up to this log_file_seq and
+ log_file_head_offset. However, non-external transaction records
+ between tail_offset..head_offset haven't been synced to the
+ mailbox yet. For example there may be pending expunges or flag
+ changes, which will be synced on the next mail_index_sync_*()
+ calls. */
+ uint32_t log_file_seq;
+ uint32_t log_file_tail_offset;
+ uint32_t log_file_head_offset;
+
+ uint32_t unused_old_sync_size_part1;
+ /* Timestamp of when .log was rotated into .log.2. This can be used to
+ optimize checking when it's time to unlink it without stat()ing it.
+ 0 = unknown, -1 = .log.2 doesn't exists. */
+ uint32_t log2_rotate_time;
+ /* Timestamp when the mailbox backend-specific code last checked
+ whether there are old temporary files (left by crashes) that should
+ be deleted. 0 = unknown. */
+ uint32_t last_temp_file_scan;
+
+ /* UNIX timestamp to the beginning of the day (in server's local
+ timezone) when new messages were last added to the index file. */
+ uint32_t day_stamp;
+ /* These fields are updated when day_stamp < today. The [0..6] are
+ first moved to [1..7], then [0] is set to the first appended UID. So
+ they contain the first UID of the day for last 8 days when messages
+ were appended.
+
+ These are used by cache purging to decide when to drop
+ MAIL_CACHE_DECISION_TEMP fields. */
+ uint32_t day_first_uid[8];
+};
+
+#define MAIL_INDEX_RECORD_MIN_SIZE (sizeof(uint32_t) + sizeof(uint8_t))
+struct mail_index_record {
+ uint32_t uid;
+ uint8_t flags; /* enum mail_flags | enum mail_index_mail_flags */
+};
+
+struct mail_keywords {
+ struct mail_index *index;
+ unsigned int count;
+ int refcount;
+
+ /* variable sized list of keyword indexes */
+ unsigned int idx[FLEXIBLE_ARRAY_MEMBER];
+};
+
+enum mail_index_transaction_flags {
+ /* If transaction is marked as hidden, the changes are marked with
+ hidden=TRUE when the view is synchronized. */
+ MAIL_INDEX_TRANSACTION_FLAG_HIDE = 0x01,
+ /* External transactions describe changes to mailbox that have already
+ happened. */
+ MAIL_INDEX_TRANSACTION_FLAG_EXTERNAL = 0x02,
+ /* Don't add flag updates unless they actually change something.
+ This is reliable only when syncing, otherwise someone else might
+ have already committed a transaction that had changed the flags. */
+ MAIL_INDEX_TRANSACTION_FLAG_AVOID_FLAG_UPDATES = 0x04,
+ /* fsync() this transaction (unless fsyncs are disabled) */
+ MAIL_INDEX_TRANSACTION_FLAG_FSYNC = 0x08,
+ /* Sync transaction describes changes to mailbox that already happened
+ to another mailbox with whom we're syncing with (dsync) */
+ MAIL_INDEX_TRANSACTION_FLAG_SYNC = 0x10
+};
+
+enum mail_index_sync_type {
+ MAIL_INDEX_SYNC_TYPE_EXPUNGE = 0x02,
+ MAIL_INDEX_SYNC_TYPE_FLAGS = 0x04,
+ MAIL_INDEX_SYNC_TYPE_KEYWORD_ADD = 0x08,
+ MAIL_INDEX_SYNC_TYPE_KEYWORD_REMOVE = 0x10
+};
+
+enum mail_index_fsync_mask {
+ MAIL_INDEX_FSYNC_MASK_APPENDS = 0x01,
+ MAIL_INDEX_FSYNC_MASK_EXPUNGES = 0x02,
+ MAIL_INDEX_FSYNC_MASK_FLAGS = 0x04,
+ MAIL_INDEX_FSYNC_MASK_KEYWORDS = 0x08
+};
+
+enum mail_index_sync_flags {
+ /* Resync all dirty messages' flags. */
+ MAIL_INDEX_SYNC_FLAG_FLUSH_DIRTY = 0x01,
+ /* Drop recent flags from all messages */
+ MAIL_INDEX_SYNC_FLAG_DROP_RECENT = 0x02,
+ /* Create the transaction with AVOID_FLAG_UPDATES flag */
+ MAIL_INDEX_SYNC_FLAG_AVOID_FLAG_UPDATES = 0x04,
+ /* If there are no new transactions and nothing else to do,
+ return 0 in mail_index_sync_begin() */
+ MAIL_INDEX_SYNC_FLAG_REQUIRE_CHANGES = 0x08,
+ /* Create the transaction with FSYNC flag */
+ MAIL_INDEX_SYNC_FLAG_FSYNC = 0x10,
+ /* If we see "delete index" request transaction, finish it.
+ This flag also allows committing more changes to a deleted index. */
+ MAIL_INDEX_SYNC_FLAG_DELETING_INDEX = 0x20,
+ /* Same as MAIL_INDEX_SYNC_FLAG_DELETING_INDEX, but finish index
+ deletion only once and fail the rest (= avoid race conditions when
+ multiple processes try to mark the index deleted) */
+ MAIL_INDEX_SYNC_FLAG_TRY_DELETING_INDEX = 0x40,
+ /* Update header's tail_offset to head_offset, even if it's the only
+ thing we do and there's no strict need for it. */
+ MAIL_INDEX_SYNC_FLAG_UPDATE_TAIL_OFFSET = 0x80
+};
+
+enum mail_index_view_sync_flags {
+ /* Don't sync expunges */
+ MAIL_INDEX_VIEW_SYNC_FLAG_NOEXPUNGES = 0x01,
+ /* Make sure view isn't inconsistent after syncing. This also means
+ that you don't care about view_sync_next()'s output, so it won't
+ return anything. */
+ MAIL_INDEX_VIEW_SYNC_FLAG_FIX_INCONSISTENT = 0x02,
+ /* Indicate that this is a secondary view, this can be used to indicate
+ that inconsistencies can be expected and if found should be fixed
+ by fully syncing. */
+ MAIL_INDEX_VIEW_SYNC_FLAG_2ND_INDEX = 0x04,
+};
+
+struct mail_index_sync_rec {
+ uint32_t uid1, uid2;
+ enum mail_index_sync_type type;
+
+ /* MAIL_INDEX_SYNC_TYPE_FLAGS: */
+ uint8_t add_flags;
+ uint8_t remove_flags;
+
+ /* MAIL_INDEX_SYNC_TYPE_KEYWORD_ADD, .._REMOVE: */
+ unsigned int keyword_idx;
+
+ /* MAIL_INDEX_SYNC_TYPE_EXPUNGE: */
+ guid_128_t guid_128;
+};
+
+enum mail_index_view_sync_type {
+ /* Flags or keywords changed */
+ MAIL_INDEX_VIEW_SYNC_TYPE_FLAGS = 0x01,
+ MAIL_INDEX_VIEW_SYNC_TYPE_MODSEQ = 0x02
+};
+
+struct mail_index_view_sync_rec {
+ uint32_t uid1, uid2;
+ enum mail_index_view_sync_type type;
+
+ /* TRUE if this was a hidden transaction. */
+ bool hidden:1;
+};
+
+enum mail_index_transaction_change {
+ MAIL_INDEX_TRANSACTION_CHANGE_APPEND = BIT(0),
+ MAIL_INDEX_TRANSACTION_CHANGE_EXPUNGE = BIT(1),
+ MAIL_INDEX_TRANSACTION_CHANGE_FLAGS = BIT(2),
+ MAIL_INDEX_TRANSACTION_CHANGE_KEYWORDS = BIT(3),
+ MAIL_INDEX_TRANSACTION_CHANGE_MODSEQ = BIT(4),
+ MAIL_INDEX_TRANSACTION_CHANGE_ATTRIBUTE = BIT(5),
+
+ MAIL_INDEX_TRANSACTION_CHANGE_OTHERS = BIT(30),
+};
+
+struct mail_index_transaction_commit_result {
+ /* seq/offset points to end of transaction */
+ uint32_t log_file_seq;
+ uoff_t log_file_offset;
+ /* number of bytes in the written transaction.
+ all of it was written to the same file. */
+ uoff_t commit_size;
+
+ enum mail_index_transaction_change changes_mask;
+ unsigned int ignored_modseq_changes;
+};
+
+struct mail_index_base_optimization_settings {
+ /* Rewrite the index when the number of bytes that needs to be read
+ from the .log on refresh is between these min/max values. */
+ uoff_t rewrite_min_log_bytes;
+ uoff_t rewrite_max_log_bytes;
+};
+
+struct mail_index_log_optimization_settings {
+ /* Rotate transaction log after it's a) min_size or larger and it was
+ created at least min_age_secs or b) larger than max_size. */
+ uoff_t min_size;
+ uoff_t max_size;
+ unsigned int min_age_secs;
+
+ /* Delete .log.2 when it's older than log2_stale_secs. Don't be too
+ eager, because older files are useful for QRESYNC and dsync. */
+ unsigned int log2_max_age_secs;
+};
+
+struct mail_index_cache_optimization_settings {
+ /* Drop fields that haven't been accessed for n seconds */
+ unsigned int unaccessed_field_drop_secs;
+ /* If cache record becomes larger than this, don't add it. */
+ unsigned int record_max_size;
+
+ /* Maximum size for the cache file. Internally the limit is 1 GB. */
+ uoff_t max_size;
+ /* Never purge the file if it's smaller than this */
+ uoff_t purge_min_size;
+ /* Purge the file when n% of records are deleted */
+ unsigned int purge_delete_percentage;
+ /* Purge the file when n% of rows contain continued rows.
+ For example 200% means that the record has 2 continued rows, i.e.
+ it exists in 3 separate segments in the cache file. */
+ unsigned int purge_continued_percentage;
+ /* Purge the file when we need to follow more than n next_offsets to
+ find the latest cache header. */
+ unsigned int purge_header_continue_count;
+};
+
+struct mail_index_optimization_settings {
+ struct mail_index_base_optimization_settings index;
+ struct mail_index_log_optimization_settings log;
+ struct mail_index_cache_optimization_settings cache;
+};
+
+struct mail_index;
+struct mail_index_map;
+struct mail_index_view;
+struct mail_index_transaction;
+struct mail_index_sync_ctx;
+struct mail_index_view_sync_ctx;
+
+struct mail_index *mail_index_alloc(struct event *parent_event,
+ const char *dir, const char *prefix);
+void mail_index_free(struct mail_index **index);
+
+/* Change .cache file's directory. */
+void mail_index_set_cache_dir(struct mail_index *index, const char *dir);
+/* Specify how often to do fsyncs. If mode is FSYNC_MODE_OPTIMIZED, the mask
+ can be used to specify which transaction types to fsync. */
+void mail_index_set_fsync_mode(struct mail_index *index, enum fsync_mode mode,
+ enum mail_index_fsync_mask mask);
+/* Try to set the index's permissions based on its index directory. Returns
+ TRUE if successful (directory existed), FALSE if mail_index_set_permissions()
+ should be called. */
+bool mail_index_use_existing_permissions(struct mail_index *index);
+void mail_index_set_permissions(struct mail_index *index,
+ mode_t mode, gid_t gid, const char *gid_origin);
+/* Set locking method and maximum time to wait for a lock
+ (UINT_MAX = default). */
+void mail_index_set_lock_method(struct mail_index *index,
+ enum file_lock_method lock_method,
+ unsigned int max_timeout_secs);
+/* Override the default optimization-related settings. Anything set to 0 will
+ use the default. */
+void mail_index_set_optimization_settings(struct mail_index *index,
+ const struct mail_index_optimization_settings *set);
+/* When creating a new index file or reseting an existing one, add the given
+ extension header data immediately to it. */
+void mail_index_set_ext_init_data(struct mail_index *index, uint32_t ext_id,
+ const void *data, size_t size);
+
+/* Open index. Returns 1 if ok, 0 if index doesn't exist and CREATE flags
+ wasn't given, -1 if error. */
+int mail_index_open(struct mail_index *index, enum mail_index_open_flags flags);
+/* Open or create index. Returns 0 if ok, -1 if error. */
+int mail_index_open_or_create(struct mail_index *index,
+ enum mail_index_open_flags flags);
+void mail_index_close(struct mail_index *index);
+/* unlink() all the index files. */
+int mail_index_unlink(struct mail_index *index);
+
+/* Returns TRUE if index is currently in memory. */
+bool mail_index_is_in_memory(struct mail_index *index);
+/* Move the index into memory. Returns 0 if ok, -1 if error occurred. */
+int mail_index_move_to_memory(struct mail_index *index);
+
+struct mail_cache *mail_index_get_cache(struct mail_index *index);
+
+/* Refresh index so mail_index_lookup*() will return latest values. Note that
+ immediately after this call there may already be changes, so if you need to
+ rely on validity of the returned values, use some external locking for it. */
+int ATTR_NOWARN_UNUSED_RESULT
+mail_index_refresh(struct mail_index *index);
+
+/* View can be used to look into index. Sequence numbers inside view change
+ only when you synchronize it. The view acquires required locks
+ automatically, but you'll have to drop them manually. */
+struct mail_index_view *
+mail_index_view_open(struct mail_index *index,
+ const char *source_filename, unsigned int source_linenum);
+#define mail_index_view_open(index) \
+ mail_index_view_open(index, __FILE__, __LINE__)
+void mail_index_view_close(struct mail_index_view **view);
+
+/* Returns the index for given view. */
+struct mail_index *mail_index_view_get_index(struct mail_index_view *view);
+/* Returns number of mails in view. */
+uint32_t mail_index_view_get_messages_count(struct mail_index_view *view);
+/* Returns TRUE if we lost track of changes for some reason. */
+bool mail_index_view_is_inconsistent(struct mail_index_view *view);
+/* Returns TRUE if there are open transactions open for the view. */
+bool mail_index_view_have_transactions(struct mail_index_view *view);
+
+/* Transaction has to be opened to be able to modify index. You can have
+ multiple transactions open simultaneously. Committed transactions won't
+ show up until you've synchronized the view. Expunges won't show up until
+ you've synchronized the mailbox (mail_index_sync_begin). */
+struct mail_index_transaction *
+mail_index_transaction_begin(struct mail_index_view *view,
+ enum mail_index_transaction_flags flags);
+int mail_index_transaction_commit(struct mail_index_transaction **t);
+int mail_index_transaction_commit_full(struct mail_index_transaction **t,
+ struct mail_index_transaction_commit_result *result_r);
+void mail_index_transaction_rollback(struct mail_index_transaction **t);
+/* Discard all changes in the transaction. */
+void mail_index_transaction_reset(struct mail_index_transaction *t);
+/* When committing transaction, drop flag/keyword updates for messages whose
+ mdoseq is larger than max_modseq. Save those messages' sequences to the
+ given array. */
+void mail_index_transaction_set_max_modseq(struct mail_index_transaction *t,
+ uint64_t max_modseq,
+ ARRAY_TYPE(seq_range) *seqs);
+
+/* Returns the view transaction was created for. */
+struct mail_index_view *
+mail_index_transaction_get_view(struct mail_index_transaction *t);
+/* Returns TRUE if the given sequence is being expunged in this transaction. */
+bool mail_index_transaction_is_expunged(struct mail_index_transaction *t,
+ uint32_t seq);
+
+/* Returns a view containing the mailbox state after changes in transaction
+ are applied. The view can still be used after transaction has been
+ committed. */
+struct mail_index_view *
+mail_index_transaction_open_updated_view(struct mail_index_transaction *t);
+
+/* Begin synchronizing mailbox with index file. Returns 1 if ok,
+ 0 if MAIL_INDEX_SYNC_FLAG_REQUIRE_CHANGES is set and there's nothing to
+ sync, -1 if error.
+
+ mail_index_sync_next() returns all changes from previously committed
+ transactions which haven't yet been committed to the actual mailbox.
+ They're returned in ascending order and they never overlap (if we add more
+ sync types, then they might). You must go through all of them and update
+ the mailbox accordingly.
+
+ Changes done to the returned transaction are expected to describe the
+ mailbox's current state.
+
+ The returned view already contains all the changes (except expunge
+ requests). After applying sync records on top of backend flags they should
+ match flags in the view. If they don't, there have been external changes.
+
+ Returned expunges are treated as expunge requests. They're not really
+ removed from the index until you mark them expunged to the returned
+ transaction. If it's not possible to expunge the message (e.g. permission
+ denied), simply don't mark them expunged.
+
+ Returned sequence numbers describe the mailbox state at the beginning of
+ synchronization, ie. expunges don't affect them. */
+int mail_index_sync_begin(struct mail_index *index,
+ struct mail_index_sync_ctx **ctx_r,
+ struct mail_index_view **view_r,
+ struct mail_index_transaction **trans_r,
+ enum mail_index_sync_flags flags);
+/* Like mail_index_sync_begin(), but returns 1 if OK and if index is already
+ synchronized up to the given log_file_seq+offset, the synchronization isn't
+ started and this function returns 0. This should be done when you wish to
+ sync your committed transaction instead of doing a full mailbox
+ synchronization. */
+int mail_index_sync_begin_to(struct mail_index *index,
+ struct mail_index_sync_ctx **ctx_r,
+ struct mail_index_view **view_r,
+ struct mail_index_transaction **trans_r,
+ uint32_t log_file_seq, uoff_t log_file_offset,
+ enum mail_index_sync_flags flags);
+/* Returns TRUE if it currently looks like syncing would return changes. */
+bool mail_index_sync_have_any(struct mail_index *index,
+ enum mail_index_sync_flags flags);
+/* Returns TRUE if it currently looks like syncing would return expunges. */
+bool mail_index_sync_have_any_expunges(struct mail_index *index);
+/* Returns the log file seq+offsets for the area which this sync is handling. */
+void mail_index_sync_get_offsets(struct mail_index_sync_ctx *ctx,
+ uint32_t *seq1_r, uoff_t *offset1_r,
+ uint32_t *seq2_r, uoff_t *offset2_r);
+/* Returns -1 if error, 0 if sync is finished, 1 if record was filled. */
+bool mail_index_sync_next(struct mail_index_sync_ctx *ctx,
+ struct mail_index_sync_rec *sync_rec);
+/* Returns TRUE if there's more to sync. */
+bool mail_index_sync_have_more(struct mail_index_sync_ctx *ctx);
+/* Returns TRUE if sync has any expunges to handle. */
+bool mail_index_sync_has_expunges(struct mail_index_sync_ctx *ctx);
+/* Reset syncing to initial state after mail_index_sync_begin(), so you can
+ go through all the sync records again with mail_index_sync_next(). */
+void mail_index_sync_reset(struct mail_index_sync_ctx *ctx);
+/* Update result when refreshing index at the end of sync. */
+void mail_index_sync_set_commit_result(struct mail_index_sync_ctx *ctx,
+ struct mail_index_transaction_commit_result *result);
+/* Don't log a warning even if syncing took over
+ MAIL_TRANSACTION_LOG_LOCK_WARN_SECS seconds. Usually this is called because
+ the caller itself already logged a warning about it. */
+void mail_index_sync_no_warning(struct mail_index_sync_ctx *ctx);
+/* If a warning is logged because syncing took over
+ MAIL_TRANSACTION_LOG_LOCK_WARN_SECS seconds, log this as the reason for the
+ syncing. */
+void mail_index_sync_set_reason(struct mail_index_sync_ctx *ctx,
+ const char *reason);
+/* Commit synchronization by writing all changes to mail index file. */
+int mail_index_sync_commit(struct mail_index_sync_ctx **ctx);
+/* Rollback synchronization - none of the changes listed by sync_next() are
+ actually written to index file. */
+void mail_index_sync_rollback(struct mail_index_sync_ctx **ctx);
+
+/* Lock the index exclusively. This is the same locking as what happens when
+ syncing the index. It's not necessary to normally call this function, unless
+ doing something special such as rebuilding the index outside syncing.
+ Returns 0 on success, -1 if locking failed for any reason. */
+int mail_index_lock_sync(struct mail_index *index, const char *lock_reason);
+/* Unlock the locked index. The index must have been locked previously with
+ mail_index_lock_sync(). If the lock had been kept for excessively long,
+ a warning is logged with the long_lock_reason. */
+void mail_index_unlock(struct mail_index *index, const char *long_lock_reason);
+/* Returns TRUE if index is currently exclusively locked. */
+bool mail_index_is_locked(struct mail_index *index);
+
+/* Mark index file corrupted in memory and delete it from disk.
+ Invalidates all views. This should be called only for index files that can
+ safely be recreated without any data loss. */
+void mail_index_mark_corrupted(struct mail_index *index) ATTR_COLD;
+/* Check and fix any found problems. Returns -1 if we couldn't lock for sync,
+ 0 if everything went ok. */
+int mail_index_fsck(struct mail_index *index) ATTR_COLD;
+/* Returns TRUE if mail_index_fsck() has been called since the last
+ mail_index_reset_fscked() call. */
+bool mail_index_reset_fscked(struct mail_index *index);
+
+/* Synchronize changes in view. You have to go through all records, or view
+ will be marked inconsistent. Only sync_mask type records are
+ synchronized. */
+struct mail_index_view_sync_ctx *
+mail_index_view_sync_begin(struct mail_index_view *view,
+ enum mail_index_view_sync_flags flags);
+bool mail_index_view_sync_next(struct mail_index_view_sync_ctx *ctx,
+ struct mail_index_view_sync_rec *sync_rec);
+void
+mail_index_view_sync_get_expunges(struct mail_index_view_sync_ctx *ctx,
+ const ARRAY_TYPE(seq_range) **expunges_r);
+int mail_index_view_sync_commit(struct mail_index_view_sync_ctx **ctx,
+ bool *delayed_expunges_r);
+
+/* Returns the index header. */
+const struct mail_index_header *
+mail_index_get_header(struct mail_index_view *view);
+/* Returns the wanted message record. */
+const struct mail_index_record *
+mail_index_lookup(struct mail_index_view *view, uint32_t seq);
+const struct mail_index_record *
+mail_index_lookup_full(struct mail_index_view *view, uint32_t seq,
+ struct mail_index_map **map_r, bool *expunged_r);
+/* Returns TRUE if the given message has already been expunged from index. */
+bool mail_index_is_expunged(struct mail_index_view *view, uint32_t seq);
+/* Note that returned keyword indexes aren't sorted. */
+void mail_index_lookup_keywords(struct mail_index_view *view, uint32_t seq,
+ ARRAY_TYPE(keyword_indexes) *keyword_idx);
+/* Return keywords from given map. */
+void mail_index_map_lookup_keywords(struct mail_index_map *map, uint32_t seq,
+ ARRAY_TYPE(keyword_indexes) *keyword_idx);
+/* mail_index_lookup[_keywords]() returns the latest flag changes.
+ This function instead attempts to return the flags and keywords done by the
+ last view sync. */
+void mail_index_lookup_view_flags(struct mail_index_view *view, uint32_t seq,
+ enum mail_flags *flags_r,
+ ARRAY_TYPE(keyword_indexes) *keyword_idx);
+/* Returns the UID for given message. May be slightly faster than
+ mail_index_lookup()->uid. */
+void mail_index_lookup_uid(struct mail_index_view *view, uint32_t seq,
+ uint32_t *uid_r);
+/* Convert UID range to sequence range. If no UIDs are found, returns FALSE and
+ sequences are set to 0. Note that any of the returned sequences may have
+ been expunged already. */
+bool mail_index_lookup_seq_range(struct mail_index_view *view,
+ uint32_t first_uid, uint32_t last_uid,
+ uint32_t *first_seq_r, uint32_t *last_seq_r);
+bool mail_index_lookup_seq(struct mail_index_view *view,
+ uint32_t uid, uint32_t *seq_r);
+/* Find first mail with (mail->flags & flags_mask) == flags. Useful mostly for
+ taking advantage of lowwater-fields in headers. */
+void mail_index_lookup_first(struct mail_index_view *view,
+ enum mail_flags flags, uint8_t flags_mask,
+ uint32_t *seq_r);
+
+/* Append a new record to index. */
+void mail_index_append(struct mail_index_transaction *t, uint32_t uid,
+ uint32_t *seq_r);
+/* Assign new UIDs for mails with uid=0 or uid<min_allowed_uid. All the new
+ UIDs are >= first_new_uid, an also higher than the highest seen uid (i.e. it
+ doesn't try to fill UID gaps). Assumes that mailbox is locked in a way that
+ UIDs can be safely assigned. Returns UIDs for all assigned messages, in
+ their sequence order (so UIDs are not necessary ascending). */
+void mail_index_append_finish_uids_full(struct mail_index_transaction *t,
+ uint32_t min_allowed_uid,
+ uint32_t first_new_uid,
+ ARRAY_TYPE(seq_range) *uids_r);
+/* Call mail_index_append_finish_uids_full() with first_uid used for both
+ min_allowed_uid and first_new_uid. */
+void mail_index_append_finish_uids(struct mail_index_transaction *t,
+ uint32_t first_uid,
+ ARRAY_TYPE(seq_range) *uids_r);
+/* Expunge record from index. Note that this doesn't affect sequence numbers
+ until transaction is committed and mailbox is synced. */
+void mail_index_expunge(struct mail_index_transaction *t, uint32_t seq);
+/* Like mail_index_expunge(), but also write message GUID to transaction log. */
+void mail_index_expunge_guid(struct mail_index_transaction *t, uint32_t seq,
+ const guid_128_t guid_128);
+/* Revert all changes done in this transaction to the given existing mail. */
+void mail_index_revert_changes(struct mail_index_transaction *t, uint32_t seq);
+/* Update flags in index. */
+void mail_index_update_flags(struct mail_index_transaction *t, uint32_t seq,
+ enum modify_type modify_type,
+ enum mail_flags flags);
+void mail_index_update_flags_range(struct mail_index_transaction *t,
+ uint32_t seq1, uint32_t seq2,
+ enum modify_type modify_type,
+ enum mail_flags flags);
+/* Specified attribute's value was changed. This is just a notification so the
+ change gets assigned its own modseq and any log readers can find out about
+ this change. */
+void mail_index_attribute_set(struct mail_index_transaction *t,
+ bool pvt, const char *key,
+ time_t timestamp, uint32_t value_len);
+/* Attribute was deleted. */
+void mail_index_attribute_unset(struct mail_index_transaction *t,
+ bool pvt, const char *key, time_t timestamp);
+/* Update message's modseq to be at least min_modseq. */
+void mail_index_update_modseq(struct mail_index_transaction *t, uint32_t seq,
+ uint64_t min_modseq);
+/* Update highest modseq to be at least min_modseq. */
+void mail_index_update_highest_modseq(struct mail_index_transaction *t,
+ uint64_t min_modseq);
+/* Reset the index before committing this transaction. This is usually done
+ only when UIDVALIDITY changes. */
+void mail_index_reset(struct mail_index_transaction *t);
+/* Remove MAIL_INDEX_HDR_FLAG_FSCKD from header if it exists. This must be
+ called only during syncing so that the mailbox is locked. */
+void mail_index_unset_fscked(struct mail_index_transaction *t);
+/* Mark index deleted. No further changes will be possible after the
+ transaction has been committed. */
+void mail_index_set_deleted(struct mail_index_transaction *t);
+/* Mark a deleted index as undeleted. Afterwards index can be changed again. */
+void mail_index_set_undeleted(struct mail_index_transaction *t);
+/* Returns TRUE if index has been set deleted. This gets set only after
+ index has been opened/refreshed and the transaction has been seen. */
+bool mail_index_is_deleted(struct mail_index *index);
+/* Returns the last time the index was modified. This can be called even if the
+ index isn't open. If the index doesn't exist, sets mtime to 0. */
+int mail_index_get_modification_time(struct mail_index *index, time_t *mtime_r);
+
+/* Lookup a keyword, returns TRUE if found, FALSE if not. */
+bool mail_index_keyword_lookup(struct mail_index *index,
+ const char *keyword, unsigned int *idx_r);
+void mail_index_keyword_lookup_or_create(struct mail_index *index,
+ const char *keyword,
+ unsigned int *idx_r);
+/* Return a pointer to array of NULL-terminated list of keywords. Note that
+ the array contents (and thus pointers inside it) may change after calling
+ mail_index_keywords_create() or mail_index_sync_begin(). */
+const ARRAY_TYPE(keywords) *mail_index_get_keywords(struct mail_index *index);
+
+/* Create a keyword list structure. */
+struct mail_keywords *
+mail_index_keywords_create(struct mail_index *index,
+ const char *const keywords[]) ATTR_NULL(2);
+struct mail_keywords *
+mail_index_keywords_create_from_indexes(struct mail_index *index,
+ const ARRAY_TYPE(keyword_indexes)
+ *keyword_indexes);
+void mail_index_keywords_ref(struct mail_keywords *keywords);
+void mail_index_keywords_unref(struct mail_keywords **keywords);
+
+/* Update keywords for given message. */
+void mail_index_update_keywords(struct mail_index_transaction *t, uint32_t seq,
+ enum modify_type modify_type,
+ struct mail_keywords *keywords);
+
+/* Update field in header. If prepend is TRUE, the header change is visible
+ before message syncing begins. */
+void mail_index_update_header(struct mail_index_transaction *t,
+ size_t offset, const void *data, size_t size,
+ bool prepend);
+
+/* Returns the full error message for last error. This message may
+ contain paths etc. so it shouldn't be shown to users. */
+const char *mail_index_get_error_message(struct mail_index *index);
+/* Reset the error message. */
+void mail_index_reset_error(struct mail_index *index);
+
+/* Apply changes in MAIL_INDEX_SYNC_TYPE_FLAGS typed sync records to given
+ flags variable. */
+void mail_index_sync_flags_apply(const struct mail_index_sync_rec *sync_rec,
+ uint8_t *flags);
+/* Apply changes in MAIL_INDEX_SYNC_TYPE_KEYWORD_* typed sync records to given
+ keywords array. Returns TRUE If something was changed. */
+bool mail_index_sync_keywords_apply(const struct mail_index_sync_rec *sync_rec,
+ ARRAY_TYPE(keyword_indexes) *keywords);
+
+/* register index extension. name is a unique identifier for the extension.
+ returns unique identifier for the name. */
+uint32_t mail_index_ext_register(struct mail_index *index, const char *name,
+ uint32_t default_hdr_size,
+ uint16_t default_record_size,
+ uint16_t default_record_align);
+/* Change an already registered extension's default sizes. */
+void mail_index_ext_register_resize_defaults(struct mail_index *index,
+ uint32_t ext_id,
+ uint32_t default_hdr_size,
+ uint16_t default_record_size,
+ uint16_t default_record_align);
+/* Returns TRUE and sets ext_id_r if extension with given name is registered. */
+bool mail_index_ext_lookup(struct mail_index *index, const char *name,
+ uint32_t *ext_id_r);
+/* Resize existing extension data. If size is grown, the new data will be
+ zero-filled. If size is shrinked, the data is simply dropped. */
+void mail_index_ext_resize(struct mail_index_transaction *t, uint32_t ext_id,
+ uint32_t hdr_size, uint16_t record_size,
+ uint16_t record_align);
+/* Resize header, keeping the old record size. */
+void mail_index_ext_resize_hdr(struct mail_index_transaction *t,
+ uint32_t ext_id, uint32_t hdr_size);
+
+/* Reset extension. Any updates for this extension which were issued before the
+ writer had seen this reset are discarded. reset_id is used to figure this
+ out, so it must be different every time. If clear_data=TRUE, records and
+ header is zeroed. */
+void mail_index_ext_reset(struct mail_index_transaction *t, uint32_t ext_id,
+ uint32_t reset_id, bool clear_data);
+/* Like mail_index_ext_reset(), but increase extension's reset_id atomically
+ when the transaction is being committed. If prev_reset_id doesn't match the
+ latest reset_id, the reset_id isn't increased and all extension changes are
+ ignored. */
+void mail_index_ext_reset_inc(struct mail_index_transaction *t, uint32_t ext_id,
+ uint32_t prev_reset_id, bool clear_data);
+/* Discard existing extension updates in this transaction and write new updates
+ using the given reset_id. The difference to mail_index_ext_reset() is that
+ this doesn't clear any existing record or header data. */
+void mail_index_ext_set_reset_id(struct mail_index_transaction *t,
+ uint32_t ext_id, uint32_t reset_id);
+/* Get the current reset_id for given extension. Returns TRUE if it exists. */
+bool mail_index_ext_get_reset_id(struct mail_index_view *view,
+ struct mail_index_map *map,
+ uint32_t ext_id, uint32_t *reset_id_r);
+
+/* Returns extension header. */
+void mail_index_get_header_ext(struct mail_index_view *view, uint32_t ext_id,
+ const void **data_r, size_t *data_size_r);
+void mail_index_map_get_header_ext(struct mail_index_view *view,
+ struct mail_index_map *map, uint32_t ext_id,
+ const void **data_r, size_t *data_size_r);
+/* Returns the wanted extension record for given message. If it doesn't exist,
+ *data_r is set to NULL. expunged_r is TRUE if the message has already been
+ expunged from the index. */
+void mail_index_lookup_ext(struct mail_index_view *view, uint32_t seq,
+ uint32_t ext_id, const void **data_r,
+ bool *expunged_r);
+void mail_index_lookup_ext_full(struct mail_index_view *view, uint32_t seq,
+ uint32_t ext_id, struct mail_index_map **map_r,
+ const void **data_r, bool *expunged_r);
+/* Get current extension sizes. Returns 1 if ok, 0 if extension doesn't exist
+ in view. Any of the _r parameters may be NULL. */
+void mail_index_ext_get_size(struct mail_index_map *map, uint32_t ext_id,
+ uint32_t *hdr_size_r, uint16_t *record_size_r,
+ uint16_t *record_align_r);
+/* Update extension header field. */
+void mail_index_update_header_ext(struct mail_index_transaction *t,
+ uint32_t ext_id, size_t offset,
+ const void *data, size_t size);
+/* Update extension record. If old_data_r is non-NULL and the record extension
+ was already updated in this transaction, it's set to contain the data it's
+ now overwriting. */
+void mail_index_update_ext(struct mail_index_transaction *t, uint32_t seq,
+ uint32_t ext_id, const void *data, void *old_data)
+ ATTR_NULL(5);
+/* Increase/decrease number in extension atomically. Returns the sum of the
+ diffs for this seq. */
+int mail_index_atomic_inc_ext(struct mail_index_transaction *t,
+ uint32_t seq, uint32_t ext_id, int diff);
+
+#endif
diff --git a/src/lib-index/mail-transaction-log-append.c b/src/lib-index/mail-transaction-log-append.c
new file mode 100644
index 0000000..662f72a
--- /dev/null
+++ b/src/lib-index/mail-transaction-log-append.c
@@ -0,0 +1,256 @@
+/* Copyright (c) 2003-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "write-full.h"
+#include "mail-index-private.h"
+#include "mail-transaction-log-private.h"
+
+void mail_transaction_log_append_add(struct mail_transaction_log_append_ctx *ctx,
+ enum mail_transaction_type type,
+ const void *data, size_t size)
+{
+ struct mail_transaction_header hdr;
+
+ i_assert((type & MAIL_TRANSACTION_TYPE_MASK) != 0);
+ i_assert((size % 4) == 0);
+
+ if (size == 0)
+ return;
+
+ i_zero(&hdr);
+ hdr.type = type | ctx->trans_flags;
+ if (type == MAIL_TRANSACTION_EXPUNGE ||
+ type == MAIL_TRANSACTION_EXPUNGE_GUID)
+ hdr.type |= MAIL_TRANSACTION_EXPUNGE_PROT;
+ if (type == MAIL_TRANSACTION_BOUNDARY)
+ hdr.type |= MAIL_TRANSACTION_EXTERNAL;
+ hdr.size = sizeof(hdr) + size;
+ hdr.size = mail_index_uint32_to_offset(hdr.size);
+
+ buffer_append(ctx->output, &hdr, sizeof(hdr));
+ buffer_append(ctx->output, data, size);
+
+ mail_transaction_update_modseq(&hdr, data, &ctx->new_highest_modseq,
+ MAIL_TRANSACTION_LOG_HDR_VERSION(&ctx->log->head->hdr));
+ ctx->transaction_count++;
+}
+
+static int
+log_buffer_move_to_memory(struct mail_transaction_log_append_ctx *ctx)
+{
+ struct mail_transaction_log_file *file = ctx->log->head;
+
+ /* first we need to truncate this latest write so that log syncing
+ doesn't break */
+ if (ftruncate(file->fd, file->sync_offset) < 0) {
+ mail_index_file_set_syscall_error(ctx->log->index,
+ file->filepath,
+ "ftruncate()");
+ }
+
+ if (mail_index_move_to_memory(ctx->log->index) < 0)
+ return -1;
+ i_assert(MAIL_TRANSACTION_LOG_FILE_IN_MEMORY(file));
+
+ i_assert(file->buffer_offset + file->buffer->used == file->sync_offset);
+ buffer_append_buf(file->buffer, ctx->output, 0, SIZE_MAX);
+ file->sync_offset = file->buffer_offset + file->buffer->used;
+ return 0;
+}
+
+static int log_buffer_write(struct mail_transaction_log_append_ctx *ctx)
+{
+ struct mail_transaction_log_file *file = ctx->log->head;
+
+ if (ctx->output->used == 0)
+ return 0;
+
+ if (MAIL_TRANSACTION_LOG_FILE_IN_MEMORY(file)) {
+ if (file->buffer == NULL) {
+ file->buffer = buffer_create_dynamic(default_pool, 4096);
+ file->buffer_offset = sizeof(file->hdr);
+ }
+ buffer_append_buf(file->buffer, ctx->output, 0, SIZE_MAX);
+ file->sync_offset = file->buffer_offset + file->buffer->used;
+ return 0;
+ }
+
+ if (write_full(file->fd, ctx->output->data, ctx->output->used) < 0) {
+ /* write failure, fallback to in-memory indexes. */
+ mail_index_file_set_syscall_error(ctx->log->index,
+ file->filepath,
+ "write_full()");
+ return log_buffer_move_to_memory(ctx);
+ }
+
+ i_assert(!ctx->sync_includes_this ||
+ file->sync_offset + ctx->output->used ==
+ file->max_tail_offset);
+
+ if ((ctx->want_fsync &&
+ file->log->index->set.fsync_mode != FSYNC_MODE_NEVER) ||
+ file->log->index->set.fsync_mode == FSYNC_MODE_ALWAYS) {
+ if (fdatasync(file->fd) < 0) {
+ mail_index_file_set_syscall_error(ctx->log->index,
+ file->filepath,
+ "fdatasync()");
+ return log_buffer_move_to_memory(ctx);
+ }
+ }
+
+ if (file->mmap_base == NULL && file->buffer != NULL) {
+ /* we're reading from a file. avoid re-reading the data that
+ we just wrote. this is also important for some NFS clients,
+ which for some reason sometimes can't read() this data we
+ just wrote in the same process */
+ i_assert(file->buffer_offset +
+ file->buffer->used == file->sync_offset);
+ buffer_append(file->buffer, ctx->output->data,
+ ctx->output->used);
+ }
+ file->sync_offset += ctx->output->used;
+ return 0;
+}
+
+static void
+log_append_sync_offset_if_needed(struct mail_transaction_log_append_ctx *ctx)
+{
+ struct mail_transaction_log_file *file = ctx->log->head;
+ struct mail_transaction_header_update *u;
+ struct mail_transaction_header *hdr;
+ uint32_t offset;
+ buffer_t buf;
+ unsigned char update_data[sizeof(*u) + sizeof(offset)];
+
+ if (!ctx->index_sync_transaction) {
+ /* this is a non-syncing transaction. update the tail offset
+ only if we're already writing something else to transaction
+ log anyway. */
+ i_assert(!ctx->tail_offset_changed);
+ /* FIXME: For now we never do this update, because it would
+ cause errors about shrinking tail offsets with old Dovecot
+ versions. This is anyway just an optimization, so it doesn't
+ matter all that much if we don't do it here. Finish this
+ in v2.3. */
+ /*if (ctx->output->used == 0)*/
+ return;
+ } else if (file->max_tail_offset == file->sync_offset) {
+ /* we're synced all the way to tail offset, so this sync
+ transaction can also be included in the same tail offset. */
+ if (ctx->output->used == 0 && !ctx->tail_offset_changed) {
+ /* nothing to write here after all (e.g. all unchanged
+ flag updates were dropped by export) */
+ return;
+ }
+
+ /* FIXME: when we remove exclusive log locking, we
+ can't rely on this. then write non-changed offset + check
+ real offset + rewrite the new offset if other transactions
+ weren't written in the middle */
+ file->max_tail_offset += ctx->output->used +
+ sizeof(*hdr) + sizeof(*u) + sizeof(offset);
+ ctx->sync_includes_this = TRUE;
+ } else {
+ /* This is a syncing transaction. Since we're finishing a sync,
+ we may need to update the tail offset even if we don't have
+ anything else to do. */
+ }
+ offset = file->max_tail_offset;
+
+ if (file->last_read_hdr_tail_offset == offset)
+ return;
+ i_assert(offset > file->last_read_hdr_tail_offset);
+
+ buffer_create_from_data(&buf, update_data, sizeof(update_data));
+ u = buffer_append_space_unsafe(&buf, sizeof(*u));
+ u->offset = offsetof(struct mail_index_header, log_file_tail_offset);
+ u->size = sizeof(offset);
+ buffer_append(&buf, &offset, sizeof(offset));
+
+ mail_transaction_log_append_add(ctx, MAIL_TRANSACTION_HEADER_UPDATE,
+ buf.data, buf.used);
+}
+
+static int
+mail_transaction_log_append_locked(struct mail_transaction_log_append_ctx *ctx)
+{
+ struct mail_transaction_log_file *file = ctx->log->head;
+ struct mail_transaction_boundary *boundary;
+
+ if (file->sync_offset < file->last_size) {
+ /* there is some garbage at the end of the transaction log
+ (eg. previous write failed). remove it so reader doesn't
+ break because of it. */
+ buffer_set_used_size(file->buffer,
+ file->sync_offset - file->buffer_offset);
+ if (!MAIL_TRANSACTION_LOG_FILE_IN_MEMORY(file)) {
+ if (ftruncate(file->fd, file->sync_offset) < 0) {
+ mail_index_file_set_syscall_error(ctx->log->index,
+ file->filepath, "ftruncate()");
+ }
+ }
+ }
+
+ /* don't include log_file_tail_offset update in the transaction */
+ boundary = buffer_get_space_unsafe(ctx->output,
+ sizeof(struct mail_transaction_header),
+ sizeof(*boundary));
+ boundary->size = ctx->output->used;
+
+ if (ctx->transaction_count <= 2) {
+ /* 0-1 changes. don't bother with the boundary */
+ unsigned int boundary_size =
+ sizeof(struct mail_transaction_header) +
+ sizeof(*boundary);
+
+ buffer_delete(ctx->output, 0, boundary_size);
+ }
+
+ log_append_sync_offset_if_needed(ctx);
+ if (log_buffer_write(ctx) < 0)
+ return -1;
+ file->sync_highest_modseq = ctx->new_highest_modseq;
+ return 0;
+}
+
+int mail_transaction_log_append_begin(struct mail_index *index,
+ enum mail_transaction_type flags,
+ struct mail_transaction_log_append_ctx **ctx_r)
+{
+ struct mail_transaction_log_append_ctx *ctx;
+ struct mail_transaction_boundary boundary;
+
+ if (!index->log_sync_locked) {
+ if (mail_transaction_log_lock_head(index->log, "appending") < 0)
+ return -1;
+ }
+ ctx = i_new(struct mail_transaction_log_append_ctx, 1);
+ ctx->log = index->log;
+ ctx->output = buffer_create_dynamic(default_pool, 1024);
+ ctx->trans_flags = flags;
+
+ i_zero(&boundary);
+ mail_transaction_log_append_add(ctx, MAIL_TRANSACTION_BOUNDARY,
+ &boundary, sizeof(boundary));
+
+ *ctx_r = ctx;
+ return 0;
+}
+
+int mail_transaction_log_append_commit(struct mail_transaction_log_append_ctx **_ctx)
+{
+ struct mail_transaction_log_append_ctx *ctx = *_ctx;
+ struct mail_index *index = ctx->log->index;
+ int ret = 0;
+
+ *_ctx = NULL;
+
+ ret = mail_transaction_log_append_locked(ctx);
+ if (!index->log_sync_locked)
+ mail_transaction_log_file_unlock(index->log->head, "appending");
+
+ buffer_free(&ctx->output);
+ i_free(ctx);
+ return ret;
+}
diff --git a/src/lib-index/mail-transaction-log-file.c b/src/lib-index/mail-transaction-log-file.c
new file mode 100644
index 0000000..1820169
--- /dev/null
+++ b/src/lib-index/mail-transaction-log-file.c
@@ -0,0 +1,1685 @@
+/* Copyright (c) 2003-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "ioloop.h"
+#include "file-dotlock.h"
+#include "nfs-workarounds.h"
+#include "read-full.h"
+#include "write-full.h"
+#include "mmap-util.h"
+#include "mail-index-private.h"
+#include "mail-index-modseq.h"
+#include "mail-transaction-log-private.h"
+
+#define LOG_PREFETCH IO_BLOCK_SIZE
+#define MEMORY_LOG_NAME "(in-memory transaction log file)"
+#define LOG_NEW_DOTLOCK_SUFFIX ".newlock"
+
+static int
+mail_transaction_log_file_sync(struct mail_transaction_log_file *file,
+ bool *retry_r, const char **reason_r);
+
+static void
+log_file_set_syscall_error(struct mail_transaction_log_file *file,
+ const char *function)
+{
+ mail_index_file_set_syscall_error(file->log->index,
+ file->filepath, function);
+}
+
+static void
+mail_transaction_log_mark_corrupted(struct mail_transaction_log_file *file)
+{
+ unsigned int offset =
+ offsetof(struct mail_transaction_log_header, indexid);
+ int flags;
+
+ if (MAIL_TRANSACTION_LOG_FILE_IN_MEMORY(file) ||
+ file->log->index->readonly)
+ return;
+
+ /* indexid=0 marks the log file as corrupted. we opened the file with
+ O_APPEND, and now we need to drop it for pwrite() to work (at least
+ in Linux) */
+ flags = fcntl(file->fd, F_GETFL, 0);
+ if (flags < 0) {
+ mail_index_file_set_syscall_error(file->log->index,
+ file->filepath, "fcntl(F_GETFL)");
+ return;
+ }
+ if (fcntl(file->fd, F_SETFL, flags & ~O_APPEND) < 0) {
+ mail_index_file_set_syscall_error(file->log->index,
+ file->filepath, "fcntl(F_SETFL)");
+ return;
+ }
+ if (pwrite_full(file->fd, &file->hdr.indexid,
+ sizeof(file->hdr.indexid), offset) < 0) {
+ mail_index_file_set_syscall_error(file->log->index,
+ file->filepath, "pwrite()");
+ }
+}
+
+void
+mail_transaction_log_file_set_corrupted(struct mail_transaction_log_file *file,
+ const char *fmt, ...)
+{
+ va_list va;
+
+ file->corrupted = TRUE;
+ file->hdr.indexid = 0;
+ mail_transaction_log_mark_corrupted(file);
+
+ va_start(va, fmt);
+ T_BEGIN {
+ mail_index_set_error(file->log->index,
+ "Corrupted transaction log file %s seq %u: %s "
+ "(sync_offset=%"PRIuUOFF_T")",
+ file->filepath, file->hdr.file_seq,
+ t_strdup_vprintf(fmt, va), file->sync_offset);
+ } T_END;
+ va_end(va);
+}
+
+struct mail_transaction_log_file *
+mail_transaction_log_file_alloc(struct mail_transaction_log *log,
+ const char *path)
+{
+ struct mail_transaction_log_file *file;
+
+ file = i_new(struct mail_transaction_log_file, 1);
+ file->log = log;
+ file->filepath = i_strdup(path);
+ file->fd = -1;
+ return file;
+}
+
+void mail_transaction_log_file_free(struct mail_transaction_log_file **_file)
+{
+ struct mail_transaction_log_file *file = *_file;
+ struct mail_transaction_log_file **p;
+ int old_errno = errno;
+
+ *_file = NULL;
+
+ i_assert(!file->locked);
+ i_assert(file->refcount == 0);
+
+ for (p = &file->log->files; *p != NULL; p = &(*p)->next) {
+ if (*p == file) {
+ *p = file->next;
+ break;
+ }
+ }
+
+ if (file == file->log->head)
+ file->log->head = NULL;
+
+ buffer_free(&file->buffer);
+
+ if (file->mmap_base != NULL) {
+ if (munmap(file->mmap_base, file->mmap_size) < 0)
+ log_file_set_syscall_error(file, "munmap()");
+ }
+
+ if (file->fd != -1) {
+ if (close(file->fd) < 0)
+ log_file_set_syscall_error(file, "close()");
+ }
+
+ i_free(file->filepath);
+ i_free(file->need_rotate);
+ i_free(file);
+
+ errno = old_errno;
+}
+
+static void
+mail_transaction_log_file_skip_to_head(struct mail_transaction_log_file *file)
+{
+ struct mail_transaction_log *log = file->log;
+ struct mail_index_map *map = log->index->map;
+ const struct mail_index_modseq_header *modseq_hdr;
+ uoff_t head_offset;
+
+ if (map == NULL || file->hdr.file_seq != map->hdr.log_file_seq ||
+ map->hdr.log_file_head_offset == 0)
+ return;
+
+ /* we can get a valid log offset from index file. initialize
+ sync_offset from it so we don't have to read the whole log
+ file from beginning. */
+ head_offset = map->hdr.log_file_head_offset;
+
+ modseq_hdr = mail_index_map_get_modseq_header(map);
+ if (head_offset < file->hdr.hdr_size) {
+ mail_index_set_error(log->index,
+ "%s: log_file_head_offset too small",
+ log->index->filepath);
+ file->sync_offset = file->hdr.hdr_size;
+ file->sync_highest_modseq = file->hdr.initial_modseq;
+ } else if (modseq_hdr == NULL && file->hdr.initial_modseq == 0) {
+ /* modseqs not used yet */
+ file->sync_offset = head_offset;
+ file->sync_highest_modseq = 0;
+ } else if (modseq_hdr == NULL ||
+ modseq_hdr->log_seq != file->hdr.file_seq) {
+ /* highest_modseq not synced, start from beginning */
+ file->sync_offset = file->hdr.hdr_size;
+ file->sync_highest_modseq = file->hdr.initial_modseq;
+ } else if (modseq_hdr->log_offset > head_offset) {
+ mail_index_set_error(log->index,
+ "%s: modseq_hdr.log_offset too large",
+ log->index->filepath);
+ file->sync_offset = file->hdr.hdr_size;
+ file->sync_highest_modseq = file->hdr.initial_modseq;
+ } else {
+ /* start from where we last stopped tracking modseqs */
+ file->sync_offset = modseq_hdr->log_offset;
+ file->sync_highest_modseq = modseq_hdr->highest_modseq;
+ }
+ if (file->hdr.file_seq == log->index->map->hdr.log_file_seq) {
+ file->last_read_hdr_tail_offset =
+ log->index->map->hdr.log_file_tail_offset;
+ }
+ if (file->last_read_hdr_tail_offset > file->max_tail_offset)
+ file->max_tail_offset = file->last_read_hdr_tail_offset;
+}
+
+static void
+mail_transaction_log_file_add_to_list(struct mail_transaction_log_file *file)
+{
+ struct mail_transaction_log_file **p;
+ const char *reason;
+ bool retry;
+
+ file->sync_offset = file->hdr.hdr_size;
+ file->sync_highest_modseq = file->hdr.initial_modseq;
+ mail_transaction_log_file_skip_to_head(file);
+
+ /* insert it to correct position */
+ for (p = &file->log->files; *p != NULL; p = &(*p)->next) {
+ if ((*p)->hdr.file_seq > file->hdr.file_seq)
+ break;
+ i_assert((*p)->hdr.file_seq < file->hdr.file_seq);
+ }
+
+ file->next = *p;
+ *p = file;
+
+ if (file->buffer != NULL) {
+ /* if we read any unfinished data, make sure the buffer gets
+ truncated. */
+ (void)mail_transaction_log_file_sync(file, &retry, &reason);
+ buffer_set_used_size(file->buffer,
+ file->sync_offset - file->buffer_offset);
+ }
+}
+
+static int
+mail_transaction_log_init_hdr(struct mail_transaction_log *log,
+ struct mail_transaction_log_header *hdr)
+{
+ struct mail_index *index = log->index;
+ struct mail_transaction_log_file *file;
+
+ i_assert(index->indexid != 0);
+
+ i_zero(hdr);
+ hdr->major_version = MAIL_TRANSACTION_LOG_MAJOR_VERSION;
+ hdr->minor_version = MAIL_TRANSACTION_LOG_MINOR_VERSION;
+ hdr->hdr_size = sizeof(struct mail_transaction_log_header);
+ hdr->indexid = log->index->indexid;
+ hdr->create_stamp = ioloop_time;
+#ifndef WORDS_BIGENDIAN
+ hdr->compat_flags |= MAIL_INDEX_COMPAT_LITTLE_ENDIAN;
+#endif
+
+ if (index->fd != -1) {
+ /* not creating index - make sure we have latest header */
+ if (!index->mapping) {
+ if (mail_index_map(index,
+ MAIL_INDEX_SYNC_HANDLER_HEAD) <= 0)
+ return -1;
+ } else {
+ /* if we got here from mapping, the .log file is
+ corrupted. use whatever values we got from index
+ file */
+ }
+ }
+ if (index->map != NULL) {
+ hdr->prev_file_seq = index->map->hdr.log_file_seq;
+ hdr->prev_file_offset = index->map->hdr.log_file_head_offset;
+ hdr->file_seq = index->map->hdr.log_file_seq + 1;
+ hdr->initial_modseq =
+ mail_index_map_modseq_get_highest(index->map);
+ } else {
+ hdr->file_seq = 1;
+ }
+ if (hdr->initial_modseq == 0) {
+ /* modseq tracking in log files is required for many reasons
+ nowadays, even if per-message modseqs aren't enabled in
+ dovecot.index. */
+ hdr->initial_modseq = 1;
+ }
+
+ if (log->head != NULL) {
+ /* make sure the sequence always increases to avoid crashes
+ later. this catches the buggy case where two processes
+ happen to replace the same log file. */
+ for (file = log->head->next; file != NULL; file = file->next) {
+ if (hdr->file_seq <= file->hdr.file_seq)
+ hdr->file_seq = file->hdr.file_seq + 1;
+ }
+
+ if (hdr->file_seq <= log->head->hdr.file_seq) {
+ /* make sure the sequence grows */
+ hdr->file_seq = log->head->hdr.file_seq+1;
+ }
+ if (hdr->initial_modseq < log->head->sync_highest_modseq) {
+ /* this should be always up-to-date */
+ hdr->initial_modseq = log->head->sync_highest_modseq;
+ }
+ }
+ return 0;
+}
+
+struct mail_transaction_log_file *
+mail_transaction_log_file_alloc_in_memory(struct mail_transaction_log *log)
+{
+ struct mail_transaction_log_file *file;
+
+ file = mail_transaction_log_file_alloc(log, MEMORY_LOG_NAME);
+ if (mail_transaction_log_init_hdr(log, &file->hdr) < 0) {
+ i_free(file);
+ return NULL;
+ }
+
+ file->buffer = buffer_create_dynamic(default_pool, 4096);
+ file->buffer_offset = sizeof(file->hdr);
+
+ mail_transaction_log_file_add_to_list(file);
+ return file;
+}
+
+static int
+mail_transaction_log_file_dotlock(struct mail_transaction_log_file *file)
+{
+ struct dotlock_settings dotlock_set;
+ int ret;
+
+ if (file->log->dotlock_refcount > 0)
+ ret = 1;
+ else {
+ i_assert(file->log->dotlock_refcount == 0);
+ mail_transaction_log_get_dotlock_set(file->log, &dotlock_set);
+ ret = file_dotlock_create(&dotlock_set, file->filepath, 0,
+ &file->log->dotlock);
+ }
+ if (ret > 0) {
+ file->log->dotlock_refcount++;
+ file->locked = TRUE;
+ file->lock_create_time = time(NULL);
+ return 0;
+ }
+ if (ret < 0) {
+ log_file_set_syscall_error(file, "file_dotlock_create()");
+ return -1;
+ }
+
+ mail_index_set_error(file->log->index,
+ "Timeout (%us) while waiting for "
+ "dotlock for transaction log file %s",
+ dotlock_set.timeout, file->filepath);
+ return -1;
+}
+
+static int
+mail_transaction_log_file_undotlock(struct mail_transaction_log_file *file)
+{
+ int ret;
+
+ i_assert(file->log->dotlock_refcount >= 0);
+ if (--file->log->dotlock_refcount > 0)
+ return 0;
+
+ ret = file_dotlock_delete(&file->log->dotlock);
+ if (ret < 0) {
+ log_file_set_syscall_error(file, "file_dotlock_delete()");
+ return -1;
+ }
+
+ if (ret == 0) {
+ mail_index_set_error(file->log->index,
+ "Dotlock was lost for transaction log file %s",
+ file->filepath);
+ return -1;
+ }
+ return 0;
+}
+
+int mail_transaction_log_file_lock(struct mail_transaction_log_file *file)
+{
+ unsigned int lock_timeout_secs;
+ int ret;
+
+ if (file->locked)
+ return 0;
+
+ if (MAIL_TRANSACTION_LOG_FILE_IN_MEMORY(file)) {
+ file->locked = TRUE;
+ return 0;
+ }
+
+ if (file->log->index->set.lock_method == FILE_LOCK_METHOD_DOTLOCK)
+ return mail_transaction_log_file_dotlock(file);
+
+ if (file->log->index->readonly) {
+ mail_index_set_error(file->log->index,
+ "Index is read-only, can't write-lock %s",
+ file->filepath);
+ return -1;
+ }
+
+ i_assert(file->file_lock == NULL);
+ lock_timeout_secs = I_MIN(MAIL_TRANSACTION_LOG_LOCK_TIMEOUT,
+ file->log->index->set.max_lock_timeout_secs);
+ ret = mail_index_lock_fd(file->log->index, file->filepath, file->fd,
+ F_WRLCK, lock_timeout_secs,
+ &file->file_lock);
+ if (ret > 0) {
+ file->locked = TRUE;
+ file->lock_create_time = time(NULL);
+ return 0;
+ }
+ if (ret < 0) {
+ log_file_set_syscall_error(file, "mail_index_wait_lock_fd()");
+ return -1;
+ }
+
+ mail_index_set_error(file->log->index,
+ "Timeout (%us) while waiting for lock for "
+ "transaction log file %s%s",
+ lock_timeout_secs, file->filepath,
+ file_lock_find(file->fd, file->log->index->set.lock_method, F_WRLCK));
+ return -1;
+}
+
+void mail_transaction_log_file_unlock(struct mail_transaction_log_file *file,
+ const char *lock_reason)
+{
+ unsigned int lock_time;
+
+ if (!file->locked)
+ return;
+
+ file->locked = FALSE;
+ file->locked_sync_offset_updated = FALSE;
+
+ if (MAIL_TRANSACTION_LOG_FILE_IN_MEMORY(file))
+ return;
+
+ lock_time = time(NULL) - file->lock_create_time;
+ if (lock_time >= MAIL_TRANSACTION_LOG_LOCK_WARN_SECS && lock_reason != NULL) {
+ i_warning("Transaction log file %s was locked for %u seconds (%s)",
+ file->filepath, lock_time, lock_reason);
+ }
+
+ if (file->log->index->set.lock_method == FILE_LOCK_METHOD_DOTLOCK) {
+ (void)mail_transaction_log_file_undotlock(file);
+ return;
+ }
+
+ file_unlock(&file->file_lock);
+}
+
+static ssize_t
+mail_transaction_log_file_read_header(struct mail_transaction_log_file *file)
+{
+ void *dest;
+ size_t pos, dest_size;
+ ssize_t ret;
+
+ i_assert(file->buffer == NULL && file->mmap_base == NULL);
+
+ i_zero(&file->hdr);
+ if (file->last_size < mmap_get_page_size() && file->last_size > 0) {
+ /* just read the entire transaction log to memory.
+ note that if some of the data hasn't been fully committed
+ yet (hdr.size=0), the buffer must be truncated later */
+ file->buffer = buffer_create_dynamic(default_pool, 4096);
+ file->buffer_offset = 0;
+ dest_size = file->last_size;
+ dest = buffer_append_space_unsafe(file->buffer, dest_size);
+ } else {
+ /* read only the header */
+ dest = &file->hdr;
+ dest_size = sizeof(file->hdr);
+ }
+
+ /* it's not necessarily an error to read less than wanted header size,
+ since older versions of the log format used smaller headers. */
+ pos = 0;
+ do {
+ ret = pread(file->fd, PTR_OFFSET(dest, pos),
+ dest_size - pos, pos);
+ if (ret > 0)
+ pos += ret;
+ } while (ret > 0 && pos < dest_size);
+
+ if (file->buffer != NULL) {
+ buffer_set_used_size(file->buffer, pos);
+ memcpy(&file->hdr, file->buffer->data,
+ I_MIN(pos, sizeof(file->hdr)));
+ }
+
+ return ret < 0 ? -1 : (ssize_t)pos;
+}
+
+static int
+mail_transaction_log_file_fail_dupe(struct mail_transaction_log_file *file)
+{
+ int ret;
+
+ /* mark the old file corrupted. we can't safely remove
+ it from the list however, so return failure. */
+ file->hdr.indexid = 0;
+ if (strcmp(file->filepath, file->log->head->filepath) != 0) {
+ /* only mark .2 corrupted, just to make sure we don't lose any
+ changes from .log in case we're somehow wrong */
+ mail_transaction_log_mark_corrupted(file);
+ ret = 0;
+ } else {
+ ret = -1;
+ }
+ if (!file->corrupted) {
+ file->corrupted = TRUE;
+ mail_index_set_error(file->log->index,
+ "Transaction log %s: "
+ "duplicate transaction log sequence (%u)",
+ file->filepath, file->hdr.file_seq);
+ }
+ return ret;
+}
+
+static int
+mail_transaction_log_file_read_hdr(struct mail_transaction_log_file *file,
+ bool ignore_estale)
+{
+ struct mail_transaction_log_file *f;
+ int ret;
+
+ i_assert(!MAIL_TRANSACTION_LOG_FILE_IN_MEMORY(file));
+
+ if (file->corrupted)
+ return 0;
+
+ ret = mail_transaction_log_file_read_header(file);
+ if (ret < 0) {
+ if (errno != ESTALE || !ignore_estale)
+ log_file_set_syscall_error(file, "pread()");
+ return -1;
+ }
+ if (file->hdr.major_version != MAIL_TRANSACTION_LOG_MAJOR_VERSION) {
+ /* incompatible version - fix silently */
+ return 0;
+ }
+ if (ret < MAIL_TRANSACTION_LOG_HEADER_MIN_SIZE) {
+ mail_transaction_log_file_set_corrupted(file,
+ "unexpected end of file while reading header");
+ return 0;
+ }
+
+ const unsigned int hdr_version =
+ MAIL_TRANSACTION_LOG_HDR_VERSION(&file->hdr);
+ if (MAIL_TRANSACTION_LOG_VERSION_HAVE(hdr_version, COMPAT_FLAGS)) {
+ /* we have compatibility flags */
+ enum mail_index_header_compat_flags compat_flags = 0;
+
+#ifndef WORDS_BIGENDIAN
+ compat_flags |= MAIL_INDEX_COMPAT_LITTLE_ENDIAN;
+#endif
+ if (file->hdr.compat_flags != compat_flags) {
+ /* architecture change */
+ mail_index_set_error(file->log->index,
+ "Rebuilding index file %s: "
+ "CPU architecture changed",
+ file->log->index->filepath);
+ return 0;
+ }
+ }
+ if (file->hdr.hdr_size < MAIL_TRANSACTION_LOG_HEADER_MIN_SIZE) {
+ mail_transaction_log_file_set_corrupted(file,
+ "Header size too small");
+ return 0;
+ }
+ if (file->hdr.hdr_size < sizeof(file->hdr)) {
+ /* @UNSAFE: smaller than we expected - zero out the fields we
+ shouldn't have filled */
+ memset(PTR_OFFSET(&file->hdr, file->hdr.hdr_size), 0,
+ sizeof(file->hdr) - file->hdr.hdr_size);
+ }
+
+ if (file->hdr.indexid == 0) {
+ /* corrupted */
+ file->corrupted = TRUE;
+ mail_index_set_error(file->log->index,
+ "Transaction log file %s: marked corrupted",
+ file->filepath);
+ return 0;
+ }
+ if (file->hdr.indexid != file->log->index->indexid) {
+ if (file->log->index->indexid != 0 &&
+ !file->log->index->initial_create) {
+ /* index file was probably just rebuilt and we don't
+ know about it yet */
+ mail_transaction_log_file_set_corrupted(file,
+ "indexid changed: %u -> %u",
+ file->log->index->indexid, file->hdr.indexid);
+ return 0;
+ }
+
+ /* creating index file. since transaction log is created
+ first, use the indexid in it to create the main index
+ to avoid races. */
+ file->log->index->indexid = file->hdr.indexid;
+ }
+
+ /* make sure we already don't have a file with the same sequence
+ opened. it shouldn't happen unless the old log file was
+ corrupted. */
+ for (f = file->log->files; f != NULL; f = f->next) {
+ if (f->hdr.file_seq == file->hdr.file_seq) {
+ if (strcmp(f->filepath, f->log->head->filepath) != 0) {
+ /* old "f" is the .log.2 */
+ return mail_transaction_log_file_fail_dupe(f);
+ } else {
+ /* new "file" is probably the .log.2 */
+ return mail_transaction_log_file_fail_dupe(file);
+ }
+ }
+ }
+
+ file->sync_highest_modseq = file->hdr.initial_modseq;
+ return 1;
+}
+
+static int
+mail_transaction_log_file_stat(struct mail_transaction_log_file *file,
+ bool ignore_estale)
+{
+ struct stat st;
+
+ if (fstat(file->fd, &st) < 0) {
+ if (!ESTALE_FSTAT(errno) || !ignore_estale)
+ log_file_set_syscall_error(file, "fstat()");
+ return -1;
+ }
+
+ file->st_dev = st.st_dev;
+ file->st_ino = st.st_ino;
+ file->last_mtime = st.st_mtime;
+ file->last_size = st.st_size;
+ return 0;
+}
+
+static bool
+mail_transaction_log_file_is_dupe(struct mail_transaction_log_file *file)
+{
+ struct mail_transaction_log_file *tmp;
+
+ for (tmp = file->log->files; tmp != NULL; tmp = tmp->next) {
+ if (tmp->st_ino == file->st_ino &&
+ CMP_DEV_T(tmp->st_dev, file->st_dev))
+ return TRUE;
+ }
+ return FALSE;
+}
+
+static void log_write_ext_hdr_init_data(struct mail_index *index, buffer_t *buf)
+{
+ const struct mail_index_registered_ext *rext;
+ struct mail_transaction_header *hdr;
+ struct mail_transaction_ext_intro *intro;
+ struct mail_transaction_ext_hdr_update *ext_hdr;
+ unsigned int hdr_offset;
+
+ rext = array_idx(&index->extensions, index->set.ext_hdr_init_id);
+
+ /* introduce the extension */
+ hdr_offset = buf->used;
+ hdr = buffer_append_space_unsafe(buf, sizeof(*hdr));
+ hdr->type = MAIL_TRANSACTION_EXT_INTRO;
+
+ intro = buffer_append_space_unsafe(buf, sizeof(*intro));
+ intro->ext_id = (uint32_t)-1;
+ intro->hdr_size = rext->hdr_size;
+ intro->record_size = rext->record_size;
+ intro->record_align = rext->record_align;
+ intro->name_size = strlen(rext->name);
+ buffer_append(buf, rext->name, intro->name_size);
+ if (buf->used % 4 != 0)
+ buffer_append_zero(buf, 4 - buf->used % 4);
+
+ hdr = buffer_get_space_unsafe(buf, hdr_offset, sizeof(*hdr));
+ hdr->size = mail_index_uint32_to_offset(buf->used - hdr_offset);
+
+ /* add the extension header data */
+ hdr_offset = buf->used;
+ hdr = buffer_append_space_unsafe(buf, sizeof(*hdr));
+ hdr->type = MAIL_TRANSACTION_EXT_HDR_UPDATE;
+
+ ext_hdr = buffer_append_space_unsafe(buf, sizeof(*ext_hdr));
+ ext_hdr->size = rext->hdr_size;
+ buffer_append(buf, index->set.ext_hdr_init_data, rext->hdr_size);
+
+ hdr = buffer_get_space_unsafe(buf, hdr_offset, sizeof(*hdr));
+ hdr->size = mail_index_uint32_to_offset(buf->used - hdr_offset);
+}
+
+static int
+mail_transaction_log_file_create2(struct mail_transaction_log_file *file,
+ int new_fd, bool reset,
+ struct dotlock **dotlock)
+{
+ struct mail_index *index = file->log->index;
+ struct stat st;
+ const char *path2;
+ buffer_t *writebuf;
+ int fd, ret;
+ bool rename_existing, need_lock;
+
+ need_lock = file->log->head != NULL && file->log->head->locked;
+
+ if (fcntl(new_fd, F_SETFL, O_APPEND) < 0) {
+ log_file_set_syscall_error(file, "fcntl(O_APPEND)");
+ return -1;
+ }
+
+ if ((index->flags & MAIL_INDEX_OPEN_FLAG_NFS_FLUSH) != 0) {
+ /* although we check also mtime and file size below, it's done
+ only to fix broken log files. we don't bother flushing
+ attribute cache just for that. */
+ nfs_flush_file_handle_cache(file->filepath);
+ }
+
+ /* log creation is locked now - see if someone already created it.
+ note that if we're rotating, we need to keep the log locked until
+ the file has been rewritten. and because fcntl() locks are stupid,
+ if we go and open()+close() the file and we had it already opened,
+ its locks are lost. so we use stat() to check if the file has been
+ recreated, although it almost never is. */
+ if (reset)
+ rename_existing = FALSE;
+ else if (nfs_safe_stat(file->filepath, &st) < 0) {
+ if (errno != ENOENT) {
+ log_file_set_syscall_error(file, "stat()");
+ return -1;
+ }
+ rename_existing = FALSE;
+ } else if (st.st_ino == file->st_ino &&
+ CMP_DEV_T(st.st_dev, file->st_dev) &&
+ /* inode/dev checks are enough when we're rotating the file,
+ but not when we're replacing a broken log file */
+ st.st_mtime == file->last_mtime &&
+ (uoff_t)st.st_size == file->last_size) {
+ /* no-one else recreated the file */
+ rename_existing = TRUE;
+ } else {
+ /* recreated. use the file if its header is ok */
+ fd = nfs_safe_open(file->filepath, O_RDWR | O_APPEND);
+ if (fd == -1) {
+ if (errno != ENOENT) {
+ log_file_set_syscall_error(file, "open()");
+ return -1;
+ }
+ } else {
+ file->fd = fd;
+ file->last_size = 0;
+ if (mail_transaction_log_file_read_hdr(file,
+ FALSE) > 0 &&
+ mail_transaction_log_file_stat(file, FALSE) == 0) {
+ /* yes, it was ok */
+ file_dotlock_delete(dotlock);
+ mail_transaction_log_file_add_to_list(file);
+ return 0;
+ }
+ file->fd = -1;
+ if (close(fd) < 0)
+ log_file_set_syscall_error(file, "close()");
+ }
+ rename_existing = FALSE;
+ }
+
+ if (index->fd == -1 && !rename_existing) {
+ /* creating the initial index */
+ reset = TRUE;
+ }
+
+ if (mail_transaction_log_init_hdr(file->log, &file->hdr) < 0)
+ return -1;
+
+ if (reset) {
+ /* don't reset modseqs. if we're reseting due to rebuilding
+ indexes we'll probably want to keep uidvalidity and in such
+ cases we really don't want to shrink modseqs. */
+ file->hdr.prev_file_seq = 0;
+ file->hdr.prev_file_offset = 0;
+ }
+
+ writebuf = t_buffer_create(128);
+ buffer_append(writebuf, &file->hdr, sizeof(file->hdr));
+
+ if (index->set.ext_hdr_init_data != NULL && reset)
+ log_write_ext_hdr_init_data(index, writebuf);
+ if (write_full(new_fd, writebuf->data, writebuf->used) < 0) {
+ log_file_set_syscall_error(file, "write_full()");
+ return -1;
+ }
+
+ if (file->log->index->set.fsync_mode == FSYNC_MODE_ALWAYS) {
+ /* the header isn't important, so don't bother calling
+ fdatasync() unless it's required */
+ if (fdatasync(new_fd) < 0) {
+ log_file_set_syscall_error(file, "fdatasync()");
+ return -1;
+ }
+ }
+
+ file->fd = new_fd;
+ ret = mail_transaction_log_file_stat(file, FALSE);
+
+ if (need_lock && ret == 0) {
+ /* we'll need to preserve the lock */
+ if (mail_transaction_log_file_lock(file) < 0)
+ ret = -1;
+ }
+
+ /* if we return -1 the dotlock deletion code closes the fd */
+ file->fd = -1;
+ if (ret < 0)
+ return -1;
+
+ /* keep two log files */
+ if (rename_existing) {
+ /* rename() would be nice and easy way to do this, except then
+ there's a race condition between the rename and
+ file_dotlock_replace(). during that time the log file
+ doesn't exist, which could cause problems. */
+ path2 = t_strconcat(file->filepath, ".2", NULL);
+ if (i_unlink_if_exists(path2) < 0) {
+ /* try to link() anyway */
+ }
+ if (nfs_safe_link(file->filepath, path2, FALSE) < 0 &&
+ errno != ENOENT && errno != EEXIST) {
+ mail_index_set_error(index, "link(%s, %s) failed: %m",
+ file->filepath, path2);
+ /* ignore the error. we don't care that much about the
+ second log file and we're going to overwrite this
+ first one. */
+ }
+ /* NOTE: here's a race condition where both .log and .log.2
+ point to the same file. our reading code should ignore that
+ though by comparing the inodes. */
+ }
+
+ if (file_dotlock_replace(dotlock,
+ DOTLOCK_REPLACE_FLAG_DONT_CLOSE_FD) <= 0) {
+ /* need to unlock to avoid assert-crash in
+ mail_transaction_log_file_free() */
+ mail_transaction_log_file_unlock(file, "creation failed");
+ return -1;
+ }
+
+ /* success */
+ file->fd = new_fd;
+ mail_transaction_log_file_add_to_list(file);
+
+ i_assert(!need_lock || file->locked);
+ return 1;
+}
+
+int mail_transaction_log_file_create(struct mail_transaction_log_file *file,
+ bool reset)
+{
+ struct mail_index *index = file->log->index;
+ struct dotlock_settings new_dotlock_set;
+ struct dotlock *dotlock;
+ mode_t old_mask;
+ int fd, ret;
+
+ i_assert(!MAIL_INDEX_IS_IN_MEMORY(index));
+
+ if (file->log->index->readonly) {
+ mail_index_set_error(index,
+ "Can't create log file %s: Index is read-only",
+ file->filepath);
+ return -1;
+ }
+
+ if (index->indexid == 0) {
+ mail_index_set_error(index,
+ "Can't create log file %s: Index is marked corrupted",
+ file->filepath);
+ return -1;
+ }
+
+ mail_transaction_log_get_dotlock_set(file->log, &new_dotlock_set);
+ new_dotlock_set.lock_suffix = LOG_NEW_DOTLOCK_SUFFIX;
+
+ /* With dotlocking we might already have path.lock created, so this
+ filename has to be different. */
+ old_mask = umask(index->set.mode ^ 0666);
+ fd = file_dotlock_open(&new_dotlock_set, file->filepath, 0, &dotlock);
+ umask(old_mask);
+
+ if (fd == -1) {
+ log_file_set_syscall_error(file, "file_dotlock_open()");
+ return -1;
+ }
+ mail_index_fchown(index, fd, file_dotlock_get_lock_path(dotlock));
+
+ /* either fd gets used or the dotlock gets deleted and returned fd
+ is for the existing file */
+ ret = mail_transaction_log_file_create2(file, fd, reset, &dotlock);
+ if (ret < 0) {
+ if (dotlock != NULL)
+ file_dotlock_delete(&dotlock);
+ return -1;
+ }
+ return ret;
+}
+
+int mail_transaction_log_file_open(struct mail_transaction_log_file *file,
+ const char **reason_r)
+{
+ struct mail_index *index = file->log->index;
+ unsigned int i;
+ bool ignore_estale;
+ int ret;
+
+ for (i = 0;; i++) {
+ if (!index->readonly) {
+ file->fd = nfs_safe_open(file->filepath,
+ O_RDWR | O_APPEND);
+ } else {
+ file->fd = nfs_safe_open(file->filepath, O_RDONLY);
+ }
+ if (file->fd == -1 && errno == EACCES) {
+ file->fd = nfs_safe_open(file->filepath, O_RDONLY);
+ index->readonly = TRUE;
+ }
+ if (file->fd == -1) {
+ if (errno == ENOENT) {
+ *reason_r = "File doesn't exist";
+ return 0;
+ }
+
+ log_file_set_syscall_error(file, "open()");
+ *reason_r = t_strdup_printf("open() failed: %m");
+ return -1;
+ }
+
+ ignore_estale = i < MAIL_INDEX_ESTALE_RETRY_COUNT;
+ if (mail_transaction_log_file_stat(file, ignore_estale) < 0)
+ ret = -1;
+ else if (mail_transaction_log_file_is_dupe(file)) {
+ /* probably our already opened .log file has been
+ renamed to .log.2 and we're trying to reopen it.
+ also possible that hit a race condition where .log
+ and .log.2 are linked. */
+ *reason_r = "File is already open";
+ return 0;
+ } else {
+ ret = mail_transaction_log_file_read_hdr(file,
+ ignore_estale);
+ }
+ if (ret > 0) {
+ /* success */
+ break;
+ }
+
+ if (ret == 0) {
+ /* corrupted */
+ if (index->readonly) {
+ /* don't delete */
+ } else {
+ i_unlink_if_exists(file->filepath);
+ }
+ *reason_r = "File is corrupted";
+ return 0;
+ }
+ if (errno != ESTALE ||
+ i == MAIL_INDEX_ESTALE_RETRY_COUNT) {
+ /* syscall error */
+ *reason_r = t_strdup_printf("fstat() failed: %m");
+ return -1;
+ }
+
+ /* ESTALE - try again */
+ buffer_free(&file->buffer);
+ }
+
+ mail_transaction_log_file_add_to_list(file);
+ return 1;
+}
+
+static int
+log_file_track_mailbox_sync_offset_hdr(struct mail_transaction_log_file *file,
+ const void *data, unsigned int trans_size,
+ const char **error_r)
+{
+ const struct mail_transaction_header_update *u = data;
+ const struct mail_index_header *ihdr;
+ const unsigned int size = trans_size - sizeof(struct mail_transaction_header);
+ const unsigned int offset_pos =
+ offsetof(struct mail_index_header, log_file_tail_offset);
+ const unsigned int offset_size = sizeof(ihdr->log_file_tail_offset);
+ uint32_t tail_offset;
+
+ i_assert(offset_size == sizeof(tail_offset));
+
+ if (size < sizeof(*u) || size < sizeof(*u) + u->size) {
+ *error_r = "header update extends beyond record size";
+ mail_transaction_log_file_set_corrupted(file, "%s", *error_r);
+ return -1;
+ }
+
+ if (u->offset <= offset_pos &&
+ u->offset + u->size >= offset_pos + offset_size) {
+ memcpy(&tail_offset,
+ CONST_PTR_OFFSET(u + 1, offset_pos - u->offset),
+ sizeof(tail_offset));
+
+ if (tail_offset < file->last_read_hdr_tail_offset) {
+ /* ignore shrinking tail offsets */
+ return 1;
+ } else if (tail_offset > file->sync_offset + trans_size) {
+ mail_transaction_log_file_set_corrupted(file,
+ "log_file_tail_offset %u goes past sync offset %"PRIuUOFF_T,
+ tail_offset, file->sync_offset + trans_size);
+ } else {
+ file->last_read_hdr_tail_offset = tail_offset;
+ if (tail_offset > file->max_tail_offset)
+ file->max_tail_offset = tail_offset;
+ return 1;
+ }
+ }
+ return 0;
+}
+
+static bool
+flag_updates_have_non_internal(const struct mail_transaction_flag_update *u,
+ unsigned int count, unsigned int version)
+{
+ /* Hide internal flags from modseqs if the log file's version
+ is new enough. This allows upgrading without the modseqs suddenly
+ shrinking. */
+ if (!MAIL_TRANSACTION_LOG_VERSION_HAVE(version, HIDE_INTERNAL_MODSEQS))
+ return TRUE;
+
+ for (unsigned int i = 0; i < count; i++) {
+ if (!MAIL_TRANSACTION_FLAG_UPDATE_IS_INTERNAL(&u[i]))
+ return TRUE;
+ }
+ return FALSE;
+}
+
+void mail_transaction_update_modseq(const struct mail_transaction_header *hdr,
+ const void *data, uint64_t *cur_modseq,
+ unsigned int version)
+{
+ uint32_t trans_size;
+
+ trans_size = mail_index_offset_to_uint32(hdr->size);
+ i_assert(trans_size != 0);
+
+ if (*cur_modseq != 0) {
+ /* tracking modseqs */
+ } else if ((hdr->type & MAIL_TRANSACTION_TYPE_MASK) ==
+ MAIL_TRANSACTION_EXT_INTRO) {
+ /* modseqs not tracked yet. see if this is a modseq
+ extension introduction. */
+ const struct mail_transaction_ext_intro *intro = data;
+ const unsigned int modseq_ext_len =
+ strlen(MAIL_INDEX_MODSEQ_EXT_NAME);
+
+ if (intro->name_size == modseq_ext_len &&
+ memcmp(intro + 1, MAIL_INDEX_MODSEQ_EXT_NAME,
+ modseq_ext_len) == 0) {
+ /* modseq tracking started */
+ *cur_modseq += 1;
+ }
+ return;
+ } else {
+ /* not tracking modseqs */
+ return;
+ }
+
+ switch (hdr->type & MAIL_TRANSACTION_TYPE_MASK) {
+ case MAIL_TRANSACTION_EXPUNGE | MAIL_TRANSACTION_EXPUNGE_PROT:
+ case MAIL_TRANSACTION_EXPUNGE_GUID | MAIL_TRANSACTION_EXPUNGE_PROT:
+ if ((hdr->type & MAIL_TRANSACTION_EXTERNAL) == 0) {
+ /* ignore expunge requests */
+ break;
+ }
+ /* fall through */
+ case MAIL_TRANSACTION_APPEND:
+ case MAIL_TRANSACTION_KEYWORD_UPDATE:
+ case MAIL_TRANSACTION_KEYWORD_RESET:
+ case MAIL_TRANSACTION_ATTRIBUTE_UPDATE:
+ /* these changes increase modseq */
+ *cur_modseq += 1;
+ break;
+ case MAIL_TRANSACTION_FLAG_UPDATE: {
+ const struct mail_transaction_flag_update *rec = data;
+ unsigned int count;
+
+ count = (trans_size - sizeof(*hdr)) / sizeof(*rec);
+ if (flag_updates_have_non_internal(rec, count, version))
+ *cur_modseq += 1;
+ break;
+ }
+ case MAIL_TRANSACTION_MODSEQ_UPDATE: {
+ const struct mail_transaction_modseq_update *rec, *end;
+
+ end = CONST_PTR_OFFSET(data, trans_size - sizeof(*hdr));
+ for (rec = data; rec < end; rec++) {
+ uint64_t modseq = ((uint64_t)rec->modseq_high32 << 32) |
+ rec->modseq_low32;
+ if (*cur_modseq < modseq)
+ *cur_modseq = modseq;
+ }
+ }
+ }
+}
+
+static int
+log_file_track_sync(struct mail_transaction_log_file *file,
+ const struct mail_transaction_header *hdr,
+ unsigned int trans_size, const char **error_r)
+{
+ const void *data = hdr + 1;
+ int ret;
+
+ mail_transaction_update_modseq(hdr, hdr + 1, &file->sync_highest_modseq,
+ MAIL_TRANSACTION_LOG_HDR_VERSION(&file->hdr));
+ if ((hdr->type & MAIL_TRANSACTION_EXTERNAL) == 0)
+ return 1;
+
+ /* external transactions: */
+ switch (hdr->type & MAIL_TRANSACTION_TYPE_MASK) {
+ case MAIL_TRANSACTION_HEADER_UPDATE:
+ /* see if this updates mailbox_sync_offset */
+ ret = log_file_track_mailbox_sync_offset_hdr(file, data,
+ trans_size, error_r);
+ if (ret != 0)
+ return ret < 0 ? -1 : 1;
+ break;
+ case MAIL_TRANSACTION_INDEX_DELETED:
+ if (file->sync_offset < file->index_undeleted_offset ||
+ file->hdr.file_seq < file->log->index->index_delete_changed_file_seq)
+ break;
+ file->log->index->index_deleted = TRUE;
+ file->log->index->index_delete_requested = FALSE;
+ file->log->index->index_delete_changed_file_seq = file->hdr.file_seq;
+ file->index_deleted_offset = file->sync_offset + trans_size;
+ break;
+ case MAIL_TRANSACTION_INDEX_UNDELETED:
+ if (file->sync_offset < file->index_deleted_offset ||
+ file->hdr.file_seq < file->log->index->index_delete_changed_file_seq)
+ break;
+ file->log->index->index_deleted = FALSE;
+ file->log->index->index_delete_requested = FALSE;
+ file->log->index->index_delete_changed_file_seq = file->hdr.file_seq;
+ file->index_undeleted_offset = file->sync_offset + trans_size;
+ break;
+ case MAIL_TRANSACTION_BOUNDARY: {
+ const struct mail_transaction_boundary *boundary =
+ (const void *)(hdr + 1);
+ size_t wanted_buffer_size;
+
+ wanted_buffer_size = file->sync_offset - file->buffer_offset +
+ boundary->size;
+ if (wanted_buffer_size > file->buffer->used) {
+ /* the full transaction hasn't been written yet */
+ return 0;
+ }
+ break;
+ }
+ }
+
+ if (file->max_tail_offset == file->sync_offset) {
+ /* external transactions aren't synced to mailbox. we can
+ update mailbox sync offset to skip this transaction to
+ avoid re-reading it at the next sync. */
+ file->max_tail_offset += trans_size;
+ }
+ return 1;
+}
+
+static int
+mail_transaction_log_file_sync(struct mail_transaction_log_file *file,
+ bool *retry_r, const char **reason_r)
+{
+ const struct mail_transaction_header *hdr;
+ const void *data;
+ struct stat st;
+ size_t size, avail;
+ uint32_t trans_size = 0;
+ int ret;
+
+ i_assert(file->sync_offset >= file->buffer_offset);
+
+ *retry_r = FALSE;
+
+ data = buffer_get_data(file->buffer, &size);
+ if (file->buffer_offset + size < file->sync_offset) {
+ *reason_r = t_strdup_printf(
+ "log file shrank (%"PRIuUOFF_T" < %"PRIuUOFF_T")",
+ file->buffer_offset + (uoff_t)size, file->sync_offset);
+ mail_transaction_log_file_set_corrupted(file, "%s", *reason_r);
+ /* fix the sync_offset to avoid crashes later on */
+ file->sync_offset = file->buffer_offset + size;
+ return 0;
+ }
+ while (file->sync_offset - file->buffer_offset + sizeof(*hdr) <= size) {
+ hdr = CONST_PTR_OFFSET(data, file->sync_offset -
+ file->buffer_offset);
+ trans_size = mail_index_offset_to_uint32(hdr->size);
+ if (trans_size == 0) {
+ /* unfinished or corrupted */
+ break;
+ }
+ if (trans_size < sizeof(*hdr)) {
+ *reason_r = t_strdup_printf(
+ "hdr.size too small (%u)", trans_size);
+ mail_transaction_log_file_set_corrupted(file, "%s", *reason_r);
+ return 0;
+ }
+
+ if (file->sync_offset - file->buffer_offset + trans_size > size)
+ break;
+
+ /* transaction has been fully written */
+ if ((ret = log_file_track_sync(file, hdr, trans_size, reason_r)) <= 0) {
+ if (ret < 0)
+ return 0;
+ break;
+ }
+
+ file->sync_offset += trans_size;
+ }
+
+ if (file->mmap_base != NULL && !file->locked) {
+ /* Now that all the mmaped pages have page faulted, check if
+ the file had changed while doing that. Only after the last
+ page has faulted, the size returned by fstat() can be
+ trusted. Otherwise it might point to a page boundary while
+ the next page is still being written.
+
+ Without this check we might see partial transactions,
+ sometimes causing "Extension record updated without intro
+ prefix" errors. */
+ if (fstat(file->fd, &st) < 0) {
+ log_file_set_syscall_error(file, "fstat()");
+ *reason_r = t_strdup_printf("fstat() failed: %m");
+ return -1;
+ }
+ if ((uoff_t)st.st_size != file->last_size) {
+ file->last_size = st.st_size;
+ *retry_r = TRUE;
+ *reason_r = "File size changed - retrying";
+ return 0;
+ }
+ }
+
+ avail = file->sync_offset - file->buffer_offset;
+ if (avail != size) {
+ /* There's more data than we could sync at the moment. If the
+ last record's size wasn't valid, we can't know if it will
+ be updated unless we've locked the log. */
+ if (file->locked) {
+ *reason_r = "Unexpected garbage at EOF";
+ mail_transaction_log_file_set_corrupted(file, "%s", *reason_r);
+ return 0;
+ }
+ /* The size field will be updated soon */
+ mail_index_flush_read_cache(file->log->index, file->filepath,
+ file->fd, file->locked);
+ }
+
+ if (file->next != NULL &&
+ file->hdr.file_seq == file->next->hdr.prev_file_seq &&
+ file->next->hdr.prev_file_offset != file->sync_offset) {
+ *reason_r = t_strdup_printf(
+ "Invalid transaction log size "
+ "(%"PRIuUOFF_T" vs %u): %s", file->sync_offset,
+ file->log->head->hdr.prev_file_offset, file->filepath);
+ mail_transaction_log_file_set_corrupted(file, "%s", *reason_r);
+ return 0;
+ }
+
+ return 1;
+}
+
+static int
+mail_transaction_log_file_insert_read(struct mail_transaction_log_file *file,
+ uoff_t offset, const char **reason_r)
+{
+ void *data;
+ size_t size;
+ ssize_t ret;
+
+ size = file->buffer_offset - offset;
+ buffer_copy(file->buffer, size, file->buffer, 0, SIZE_MAX);
+
+ data = buffer_get_space_unsafe(file->buffer, 0, size);
+ ret = pread_full(file->fd, data, size, offset);
+ if (ret > 0) {
+ /* success */
+ file->buffer_offset -= size;
+ return 1;
+ }
+
+ /* failure. don't leave ourself to inconsistent state */
+ buffer_copy(file->buffer, 0, file->buffer, size, SIZE_MAX);
+ buffer_set_used_size(file->buffer, file->buffer->used - size);
+
+ if (ret == 0) {
+ *reason_r = "file shrank unexpectedly";
+ mail_transaction_log_file_set_corrupted(file, "%s", *reason_r);
+ return 0;
+ } else if (errno == ESTALE) {
+ /* log file was deleted in NFS server, fail silently */
+ *reason_r = t_strdup_printf("read() failed: %m");
+ return 0;
+ } else {
+ log_file_set_syscall_error(file, "pread()");
+ *reason_r = t_strdup_printf("read() failed: %m");
+ return -1;
+ }
+}
+
+static int
+mail_transaction_log_file_read_more(struct mail_transaction_log_file *file,
+ const char **reason_r)
+{
+ void *data;
+ size_t size;
+ uint32_t read_offset;
+ ssize_t ret;
+
+ read_offset = file->buffer_offset + file->buffer->used;
+
+ do {
+ data = buffer_append_space_unsafe(file->buffer, LOG_PREFETCH);
+ ret = pread(file->fd, data, LOG_PREFETCH, read_offset);
+ if (ret > 0)
+ read_offset += ret;
+
+ size = read_offset - file->buffer_offset;
+ buffer_set_used_size(file->buffer, size);
+ } while (ret > 0 || (ret < 0 && errno == EINTR));
+
+ file->last_size = read_offset;
+
+ if (ret < 0) {
+ *reason_r = t_strdup_printf("pread() failed: %m");
+ if (errno == ESTALE) {
+ /* log file was deleted in NFS server, fail silently */
+ return 0;
+ }
+ log_file_set_syscall_error(file, "pread()");
+ return -1;
+ }
+ return 1;
+}
+
+static bool
+mail_transaction_log_file_need_nfs_flush(struct mail_transaction_log_file *file)
+{
+ const struct mail_index_header *hdr = &file->log->index->map->hdr;
+ uoff_t max_offset = file->last_size;
+
+ if (file->next != NULL &&
+ file->hdr.file_seq == file->next->hdr.prev_file_seq &&
+ file->next->hdr.prev_file_offset != max_offset) {
+ /* we already have a newer log file which says that we haven't
+ synced the entire file. */
+ return TRUE;
+ }
+
+ if (file->hdr.file_seq == hdr->log_file_seq &&
+ max_offset < hdr->log_file_head_offset)
+ return TRUE;
+
+ return FALSE;
+}
+
+static int
+mail_transaction_log_file_read(struct mail_transaction_log_file *file,
+ uoff_t start_offset, bool nfs_flush,
+ const char **reason_r)
+{
+ bool retry;
+ int ret;
+
+ i_assert(file->mmap_base == NULL);
+
+ /* NFS: if file isn't locked, we're optimistic that we can read enough
+ data without flushing attribute cache. if after reading we notice
+ that we really should have read more, flush the cache and try again.
+ if file is locked, the attribute cache was already flushed when
+ refreshing the log. */
+ if (nfs_flush &&
+ (file->log->index->flags & MAIL_INDEX_OPEN_FLAG_NFS_FLUSH) != 0) {
+ if (!file->locked)
+ nfs_flush_attr_cache_unlocked(file->filepath);
+ else
+ nfs_flush_attr_cache_fd_locked(file->filepath, file->fd);
+ }
+
+ if (file->buffer != NULL && file->buffer_offset > start_offset) {
+ /* we have to insert missing data to beginning of buffer */
+ ret = mail_transaction_log_file_insert_read(file, start_offset, reason_r);
+ if (ret <= 0)
+ return ret;
+ }
+
+ if (file->buffer == NULL) {
+ file->buffer =
+ buffer_create_dynamic(default_pool, LOG_PREFETCH);
+ file->buffer_offset = start_offset;
+ }
+
+ if ((ret = mail_transaction_log_file_read_more(file, reason_r)) <= 0)
+ ;
+ else if (!nfs_flush &&
+ (file->log->index->flags & MAIL_INDEX_OPEN_FLAG_NFS_FLUSH) != 0 &&
+ mail_transaction_log_file_need_nfs_flush(file)) {
+ /* we didn't read enough data. flush and try again. */
+ return mail_transaction_log_file_read(file, start_offset, TRUE, reason_r);
+ } else if ((ret = mail_transaction_log_file_sync(file, &retry, reason_r)) == 0) {
+ i_assert(!retry); /* retry happens only with mmap */
+ }
+ i_assert(file->sync_offset >= file->buffer_offset);
+ buffer_set_used_size(file->buffer,
+ file->sync_offset - file->buffer_offset);
+ return ret;
+}
+
+static bool
+log_file_map_check_offsets(struct mail_transaction_log_file *file,
+ uoff_t start_offset, uoff_t end_offset,
+ const char **reason_r)
+{
+ struct stat st, st2;
+
+ if (start_offset > file->sync_offset) {
+ /* broken start offset */
+ if (MAIL_TRANSACTION_LOG_FILE_IN_MEMORY(file)) {
+ *reason_r = t_strdup_printf(
+ "%s: start_offset (%"PRIuUOFF_T") > "
+ "current sync_offset (%"PRIuUOFF_T")",
+ file->filepath, start_offset, file->sync_offset);
+ return FALSE;
+ }
+
+ if (fstat(file->fd, &st) < 0) {
+ log_file_set_syscall_error(file, "fstat()");
+ st.st_size = -1;
+ }
+ *reason_r = t_strdup_printf(
+ "%s: start_offset (%"PRIuUOFF_T") > "
+ "current sync_offset (%"PRIuUOFF_T"), file size=%"PRIuUOFF_T,
+ file->filepath, start_offset, file->sync_offset,
+ st.st_size);
+ if (stat(file->filepath, &st2) == 0) {
+ if (st.st_ino != st2.st_ino) {
+ *reason_r = t_strdup_printf(
+ "%s, file unexpectedly replaced", *reason_r);
+ }
+ } else if (errno == ENOENT) {
+ *reason_r = t_strdup_printf(
+ "%s, file unexpectedly deleted", *reason_r);
+ } else {
+ log_file_set_syscall_error(file, "stat()");
+ }
+ return FALSE;
+ }
+ if (end_offset != UOFF_T_MAX && end_offset > file->sync_offset) {
+ *reason_r = t_strdup_printf(
+ "%s: end_offset (%"PRIuUOFF_T") > "
+ "current sync_offset (%"PRIuUOFF_T")",
+ file->filepath, start_offset, file->sync_offset);
+ return FALSE;
+ }
+
+ return TRUE;
+}
+
+static int
+mail_transaction_log_file_mmap(struct mail_transaction_log_file *file,
+ const char **reason_r)
+{
+ /* we may have switched to mmaping */
+ buffer_free(&file->buffer);
+
+ file->mmap_size = file->last_size;
+ file->mmap_base = mmap(NULL, file->mmap_size, PROT_READ, MAP_SHARED,
+ file->fd, 0);
+ if (file->mmap_base == MAP_FAILED) {
+ file->mmap_base = NULL;
+ if (ioloop_time != file->last_mmap_error_time) {
+ file->last_mmap_error_time = ioloop_time;
+ log_file_set_syscall_error(file, t_strdup_printf(
+ "mmap(size=%zu)", file->mmap_size));
+ }
+ *reason_r = t_strdup_printf("mmap(size=%zu) failed: %m",
+ file->mmap_size);
+ file->mmap_size = 0;
+ return -1;
+ }
+
+ if (file->mmap_size > mmap_get_page_size()) {
+ if (madvise(file->mmap_base, file->mmap_size,
+ MADV_SEQUENTIAL) < 0)
+ log_file_set_syscall_error(file, "madvise()");
+ }
+
+ buffer_create_from_const_data(&file->mmap_buffer,
+ file->mmap_base, file->mmap_size);
+ file->buffer = &file->mmap_buffer;
+ file->buffer_offset = 0;
+ return 0;
+}
+
+static void
+mail_transaction_log_file_munmap(struct mail_transaction_log_file *file)
+{
+ if (file->mmap_base == NULL)
+ return;
+
+ i_assert(file->buffer != NULL);
+ if (munmap(file->mmap_base, file->mmap_size) < 0)
+ log_file_set_syscall_error(file, "munmap()");
+ file->mmap_base = NULL;
+ file->mmap_size = 0;
+ buffer_free(&file->buffer);
+}
+
+static int
+mail_transaction_log_file_map_mmap(struct mail_transaction_log_file *file,
+ uoff_t start_offset, const char **reason_r)
+{
+ struct stat st;
+ bool retry;
+ int ret;
+
+ /* we are going to mmap() this file, but it's not necessarily
+ mmaped currently. */
+ i_assert(file->buffer_offset == 0 || file->mmap_base == NULL);
+ i_assert(file->mmap_size == 0 || file->mmap_base != NULL);
+
+ if (fstat(file->fd, &st) < 0) {
+ log_file_set_syscall_error(file, "fstat()");
+ *reason_r = t_strdup_printf("fstat() failed: %m");
+ return -1;
+ }
+ file->last_size = st.st_size;
+
+ if ((uoff_t)st.st_size < file->sync_offset) {
+ *reason_r = t_strdup_printf(
+ "file size shrank (%"PRIuUOFF_T" < %"PRIuUOFF_T")",
+ (uoff_t)st.st_size, file->sync_offset);
+ mail_transaction_log_file_set_corrupted(file, "%s", *reason_r);
+ return 0;
+ }
+
+ if (file->buffer != NULL && file->buffer_offset <= start_offset &&
+ (uoff_t)st.st_size == file->buffer_offset + file->buffer->used) {
+ /* we already have the whole file mapped */
+ if ((ret = mail_transaction_log_file_sync(file, &retry, reason_r)) != 0 ||
+ !retry)
+ return ret;
+ /* size changed, re-mmap */
+ }
+
+ do {
+ mail_transaction_log_file_munmap(file);
+
+ if (file->last_size - start_offset < mmap_get_page_size()) {
+ /* just reading the file is probably faster */
+ return mail_transaction_log_file_read(file,
+ start_offset,
+ FALSE, reason_r);
+ }
+
+ if (mail_transaction_log_file_mmap(file, reason_r) < 0)
+ return -1;
+ ret = mail_transaction_log_file_sync(file, &retry, reason_r);
+ } while (retry);
+
+ return ret;
+}
+
+int mail_transaction_log_file_map(struct mail_transaction_log_file *file,
+ uoff_t start_offset, uoff_t end_offset,
+ const char **reason_r)
+{
+ uoff_t map_start_offset = start_offset;
+ size_t size;
+ int ret;
+
+ if (file->hdr.indexid == 0) {
+ /* corrupted */
+ *reason_r = "corrupted, indexid=0";
+ return 0;
+ }
+
+ i_assert(start_offset >= file->hdr.hdr_size);
+ i_assert(start_offset <= end_offset);
+ i_assert(file->buffer == NULL || file->mmap_base != NULL ||
+ file->sync_offset >= file->buffer_offset + file->buffer->used);
+
+ if (file->locked_sync_offset_updated && file == file->log->head &&
+ end_offset == UOFF_T_MAX) {
+ /* we're not interested of going further than sync_offset */
+ if (!log_file_map_check_offsets(file, start_offset,
+ end_offset, reason_r))
+ return 0;
+ i_assert(start_offset <= file->sync_offset);
+ end_offset = file->sync_offset;
+ }
+
+ if (file->buffer != NULL && file->buffer_offset <= start_offset) {
+ /* see if we already have it */
+ size = file->buffer->used;
+ if (file->buffer_offset + size >= end_offset)
+ return 1;
+ }
+
+ if (file->locked) {
+ /* set this only when we've synced to end of file while locked
+ (either end_offset=UOFF_T_MAX or we had to read anyway) */
+ file->locked_sync_offset_updated = TRUE;
+ }
+
+ if (MAIL_TRANSACTION_LOG_FILE_IN_MEMORY(file)) {
+ if (start_offset < file->buffer_offset || file->buffer == NULL) {
+ /* we had moved the log to memory but failed to read
+ the beginning of the log file */
+ *reason_r = "Beginning of the log isn't available";
+ return 0;
+ }
+ return log_file_map_check_offsets(file, start_offset,
+ end_offset, reason_r) ? 1 : 0;
+ }
+
+ if (start_offset > file->sync_offset)
+ mail_transaction_log_file_skip_to_head(file);
+ if (start_offset > file->sync_offset) {
+ /* although we could just skip over the unwanted data, we have
+ to sync everything so that modseqs are calculated
+ correctly */
+ map_start_offset = file->sync_offset;
+ }
+
+ if ((file->log->index->flags & MAIL_INDEX_OPEN_FLAG_MMAP_DISABLE) == 0)
+ ret = mail_transaction_log_file_map_mmap(file, map_start_offset, reason_r);
+ else {
+ mail_transaction_log_file_munmap(file);
+ ret = mail_transaction_log_file_read(file, map_start_offset, FALSE, reason_r);
+ }
+
+ i_assert(file->buffer == NULL || file->mmap_base != NULL ||
+ file->sync_offset >= file->buffer_offset + file->buffer->used);
+ if (ret <= 0)
+ return ret;
+
+ i_assert(file->buffer != NULL);
+ return log_file_map_check_offsets(file, start_offset, end_offset,
+ reason_r) ? 1 : 0;
+}
+
+int mail_transaction_log_file_move_to_memory(struct mail_transaction_log_file *file)
+{
+ const char *error;
+ buffer_t *buf;
+ int ret = 0;
+
+ if (MAIL_TRANSACTION_LOG_FILE_IN_MEMORY(file))
+ return 0;
+
+ if (file->mmap_base != NULL) {
+ /* just copy to memory */
+ i_assert(file->buffer_offset == 0);
+
+ buf = buffer_create_dynamic(default_pool, file->mmap_size);
+ buffer_append(buf, file->mmap_base, file->mmap_size);
+ buffer_free(&file->buffer);
+ file->buffer = buf;
+
+ /* and lose the mmap */
+ if (munmap(file->mmap_base, file->mmap_size) < 0)
+ log_file_set_syscall_error(file, "munmap()");
+ file->mmap_base = NULL;
+ } else if (file->buffer_offset != 0) {
+ /* we don't have the full log in the memory. read it. */
+ ret = mail_transaction_log_file_read(file, 0, FALSE, &error);
+ if (ret <= 0) {
+ mail_index_set_error(file->log->index,
+ "%s: Failed to read into memory: %s", file->filepath, error);
+ }
+ }
+ file->last_size = 0;
+
+ if (close(file->fd) < 0)
+ log_file_set_syscall_error(file, "close()");
+ file->fd = -1;
+
+ i_free(file->filepath);
+ file->filepath = i_strdup(file->log->filepath);
+ return ret < 0 ? -1 : 0;
+}
diff --git a/src/lib-index/mail-transaction-log-modseq.c b/src/lib-index/mail-transaction-log-modseq.c
new file mode 100644
index 0000000..eb2b533
--- /dev/null
+++ b/src/lib-index/mail-transaction-log-modseq.c
@@ -0,0 +1,298 @@
+/* Copyright (c) 2003-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "mail-index-private.h"
+#include "mail-index-modseq.h"
+#include "mail-transaction-log-private.h"
+
+static struct modseq_cache *
+modseq_cache_hit(struct mail_transaction_log_file *file, unsigned int idx)
+{
+ struct modseq_cache cache;
+
+ if (idx > 0) {
+ /* @UNSAFE: move it to top */
+ cache = file->modseq_cache[idx];
+ memmove(file->modseq_cache + 1, file->modseq_cache,
+ sizeof(*file->modseq_cache) * idx);
+ file->modseq_cache[0] = cache;
+ }
+ return &file->modseq_cache[0];
+}
+
+static struct modseq_cache *
+modseq_cache_get_offset(struct mail_transaction_log_file *file, uoff_t offset)
+{
+ unsigned int i, best = UINT_MAX;
+
+ for (i = 0; i < N_ELEMENTS(file->modseq_cache); i++) {
+ if (offset < file->modseq_cache[i].offset)
+ continue;
+
+ if (file->modseq_cache[i].offset == 0)
+ return NULL;
+
+ if (offset == file->modseq_cache[i].offset) {
+ /* exact cache hit */
+ return modseq_cache_hit(file, i);
+ }
+
+ if (best == UINT_MAX ||
+ file->modseq_cache[i].offset <
+ file->modseq_cache[best].offset)
+ best = i;
+ }
+ if (best == UINT_MAX)
+ return NULL;
+ return &file->modseq_cache[best];
+}
+
+static struct modseq_cache *
+modseq_cache_get_modseq(struct mail_transaction_log_file *file, uint64_t modseq)
+{
+ unsigned int i, best = UINT_MAX;
+
+ for (i = 0; i < N_ELEMENTS(file->modseq_cache); i++) {
+ if (modseq < file->modseq_cache[i].highest_modseq)
+ continue;
+
+ if (file->modseq_cache[i].offset == 0)
+ return NULL;
+
+ if (modseq == file->modseq_cache[i].highest_modseq) {
+ /* exact cache hit */
+ return modseq_cache_hit(file, i);
+ }
+
+ if (best == UINT_MAX ||
+ file->modseq_cache[i].highest_modseq <
+ file->modseq_cache[best].highest_modseq)
+ best = i;
+ }
+ if (best == UINT_MAX)
+ return NULL;
+ return &file->modseq_cache[best];
+}
+
+static int
+log_get_synced_record(struct mail_transaction_log_file *file, uoff_t *offset,
+ const struct mail_transaction_header **hdr_r,
+ const char **error_r)
+{
+ const struct mail_transaction_header *hdr;
+ uint32_t trans_size;
+
+ hdr = CONST_PTR_OFFSET(file->buffer->data,
+ *offset - file->buffer_offset);
+
+ /* we've already synced this record at some point. it should
+ be valid. */
+ trans_size = mail_index_offset_to_uint32(hdr->size);
+ if (trans_size < sizeof(*hdr) ||
+ *offset - file->buffer_offset + trans_size > file->buffer->used) {
+ *error_r = t_strdup_printf(
+ "Transaction log corrupted unexpectedly at "
+ "%"PRIuUOFF_T": Invalid size %u (type=%x)",
+ *offset, trans_size, hdr->type);
+ mail_transaction_log_file_set_corrupted(file, "%s", *error_r);
+ return -1;
+ }
+ *offset += trans_size;
+ *hdr_r = hdr;
+ return 0;
+}
+
+int mail_transaction_log_file_get_highest_modseq_at(
+ struct mail_transaction_log_file *file,
+ uoff_t offset, uint64_t *highest_modseq_r,
+ const char **error_r)
+{
+ const struct mail_transaction_header *hdr;
+ struct modseq_cache *cache;
+ uoff_t cur_offset;
+ uint64_t cur_modseq;
+ const char *reason;
+ int ret;
+
+ i_assert(offset <= file->sync_offset);
+
+ if (offset == file->sync_offset) {
+ *highest_modseq_r = file->sync_highest_modseq;
+ return 1;
+ }
+
+ cache = modseq_cache_get_offset(file, offset);
+ if (cache == NULL) {
+ /* nothing usable in cache - scan from beginning */
+ cur_offset = file->hdr.hdr_size;
+ cur_modseq = file->hdr.initial_modseq;
+ } else if (cache->offset == offset) {
+ /* exact cache hit */
+ *highest_modseq_r = cache->highest_modseq;
+ return 1;
+ } else {
+ /* use cache to skip over some records */
+ cur_offset = cache->offset;
+ cur_modseq = cache->highest_modseq;
+ }
+
+ /* See if we can use the "modseq" header in dovecot.index to further
+ reduce how much we have to scan. */
+ const struct mail_index_modseq_header *modseq_hdr =
+ file->log->index->map == NULL ? NULL :
+ &file->log->index->map->modseq_hdr_snapshot;
+ if (modseq_hdr != NULL &&
+ modseq_hdr->log_seq == file->hdr.file_seq &&
+ modseq_hdr->log_offset <= offset &&
+ modseq_hdr->log_offset >= cur_offset) {
+ cur_offset = modseq_hdr->log_offset;
+ cur_modseq = modseq_hdr->highest_modseq;
+ }
+
+ ret = mail_transaction_log_file_map(file, cur_offset, offset, &reason);
+ if (ret <= 0) {
+ *error_r = t_strdup_printf(
+ "Failed to map transaction log %s for getting modseq "
+ "at offset=%"PRIuUOFF_T" with start_offset=%"PRIuUOFF_T": %s",
+ file->filepath, offset, cur_offset, reason);
+ return ret;
+ }
+
+ i_assert(cur_offset >= file->buffer_offset);
+ i_assert(cur_offset + file->buffer->used >= offset);
+ while (cur_offset < offset) {
+ if (log_get_synced_record(file, &cur_offset, &hdr, error_r) < 0)
+ return 0;
+ mail_transaction_update_modseq(hdr, hdr + 1, &cur_modseq,
+ MAIL_TRANSACTION_LOG_HDR_VERSION(&file->hdr));
+ }
+
+ /* @UNSAFE: cache the value */
+ memmove(file->modseq_cache + 1, file->modseq_cache,
+ sizeof(*file->modseq_cache) *
+ (N_ELEMENTS(file->modseq_cache) - 1));
+ file->modseq_cache[0].offset = cur_offset;
+ file->modseq_cache[0].highest_modseq = cur_modseq;
+
+ *highest_modseq_r = cur_modseq;
+ return 1;
+}
+
+static int
+get_modseq_next_offset_at(struct mail_transaction_log_file *file,
+ uint64_t modseq, bool use_highest,
+ uoff_t *cur_offset, uint64_t *cur_modseq,
+ uoff_t *next_offset_r)
+{
+ const struct mail_transaction_header *hdr;
+ const char *reason;
+ int ret;
+
+ /* make sure we've read until end of file. this is especially important
+ with non-head logs which might only have been opened without being
+ synced. */
+ ret = mail_transaction_log_file_map(file, *cur_offset, UOFF_T_MAX, &reason);
+ if (ret <= 0) {
+ mail_index_set_error(file->log->index,
+ "Failed to map transaction log %s for getting offset "
+ "for modseq=%"PRIu64" with start_offset=%"PRIuUOFF_T": %s",
+ file->filepath, modseq, *cur_offset, reason);
+ return -1;
+ }
+
+ /* check sync_highest_modseq again in case sync_offset was updated */
+ if (modseq >= file->sync_highest_modseq && use_highest) {
+ *next_offset_r = file->sync_offset;
+ return 0;
+ }
+
+ i_assert(*cur_offset >= file->buffer_offset);
+ while (*cur_offset < file->sync_offset) {
+ if (log_get_synced_record(file, cur_offset, &hdr, &reason) < 0) {
+ mail_index_set_error(file->log->index,
+ "%s: %s", file->filepath, reason);
+ return -1;
+ }
+ mail_transaction_update_modseq(hdr, hdr + 1, cur_modseq,
+ MAIL_TRANSACTION_LOG_HDR_VERSION(&file->hdr));
+ if (*cur_modseq >= modseq)
+ break;
+ }
+ return 1;
+}
+
+int mail_transaction_log_file_get_modseq_next_offset(
+ struct mail_transaction_log_file *file,
+ uint64_t modseq, uoff_t *next_offset_r)
+{
+ struct modseq_cache *cache;
+ uoff_t cur_offset;
+ uint64_t cur_modseq;
+ int ret;
+
+ if (modseq == file->sync_highest_modseq) {
+ *next_offset_r = file->sync_offset;
+ return 0;
+ }
+ if (modseq == file->hdr.initial_modseq) {
+ *next_offset_r = file->hdr.hdr_size;
+ return 0;
+ }
+
+ cache = modseq_cache_get_modseq(file, modseq);
+ if (cache == NULL) {
+ /* nothing usable in cache - scan from beginning */
+ cur_offset = file->hdr.hdr_size;
+ cur_modseq = file->hdr.initial_modseq;
+ } else if (cache->highest_modseq == modseq) {
+ /* exact cache hit */
+ *next_offset_r = cache->offset;
+ return 0;
+ } else {
+ /* use cache to skip over some records */
+ cur_offset = cache->offset;
+ cur_modseq = cache->highest_modseq;
+ }
+
+ if ((ret = get_modseq_next_offset_at(file, modseq, TRUE, &cur_offset,
+ &cur_modseq, next_offset_r)) <= 0)
+ return ret;
+ if (cur_offset == file->sync_offset) {
+ /* if we got to sync_offset, cur_modseq should be
+ sync_highest_modseq */
+ mail_index_set_error(file->log->index,
+ "%s: Transaction log modseq tracking is corrupted - fixing",
+ file->filepath);
+ /* retry getting the offset by reading from the beginning
+ of the file */
+ cur_offset = file->hdr.hdr_size;
+ cur_modseq = file->hdr.initial_modseq;
+ ret = get_modseq_next_offset_at(file, modseq, FALSE,
+ &cur_offset, &cur_modseq,
+ next_offset_r);
+ if (ret < 0)
+ return -1;
+ i_assert(ret != 0);
+ /* get it fixed on the next sync */
+ if (file->log->index->need_recreate == NULL) {
+ file->log->index->need_recreate =
+ i_strdup("modseq tracking is corrupted");
+ }
+ if (file->need_rotate == NULL) {
+ file->need_rotate =
+ i_strdup("modseq tracking is corrupted");
+ }
+ /* clear cache, since it's unreliable */
+ memset(file->modseq_cache, 0, sizeof(file->modseq_cache));
+ }
+
+ /* @UNSAFE: cache the value */
+ memmove(file->modseq_cache + 1, file->modseq_cache,
+ sizeof(*file->modseq_cache) *
+ (N_ELEMENTS(file->modseq_cache) - 1));
+ file->modseq_cache[0].offset = cur_offset;
+ file->modseq_cache[0].highest_modseq = cur_modseq;
+
+ *next_offset_r = cur_offset;
+ return 0;
+}
diff --git a/src/lib-index/mail-transaction-log-private.h b/src/lib-index/mail-transaction-log-private.h
new file mode 100644
index 0000000..4961fe0
--- /dev/null
+++ b/src/lib-index/mail-transaction-log-private.h
@@ -0,0 +1,199 @@
+#ifndef MAIL_TRANSACTION_LOG_VIEW_H
+#define MAIL_TRANSACTION_LOG_VIEW_H
+
+#include "buffer.h"
+#include "mail-transaction-log.h"
+
+struct dotlock_settings;
+
+/* Synchronization can take a while sometimes, especially when copying lots of
+ mails. */
+#define MAIL_TRANSACTION_LOG_LOCK_TIMEOUT (3*60)
+#define MAIL_TRANSACTION_LOG_DOTLOCK_CHANGE_TIMEOUT (3*60)
+
+#define MAIL_TRANSACTION_LOG_FILE_IN_MEMORY(file) ((file)->fd == -1)
+
+#define LOG_FILE_MODSEQ_CACHE_SIZE 10
+
+struct modseq_cache {
+ uoff_t offset;
+ uint64_t highest_modseq;
+};
+
+struct mail_transaction_log_file {
+ struct mail_transaction_log *log;
+ /* Next file in the mail_transaction_log.files list. Sorted by
+ hdr.file_seq. */
+ struct mail_transaction_log_file *next;
+
+ /* refcount=0 is a valid state. files start that way, and they're
+ freed only when mail_transaction_logs_clean() is called. */
+ int refcount;
+
+ char *filepath;
+ int fd;
+
+ /* Cached values for last stat()/fstat() */
+ ino_t st_ino;
+ dev_t st_dev;
+ time_t last_mtime;
+ uoff_t last_size;
+
+ /* Used to avoid logging mmap() errors too rapidly. */
+ time_t last_mmap_error_time;
+ /* If non-NULL, the log file should be rotated. The string contains a
+ human-readable reason why the rotation was requested. */
+ char *need_rotate;
+
+ /* Copy of the log file header. Set when opened. */
+ struct mail_transaction_log_header hdr;
+ /* Buffer that points to mmap_base */
+ buffer_t mmap_buffer;
+ /* Buffer that can be used to access the log file contents. Either
+ points to mmap_buffer, or it's a copy of the file contents starting
+ from buffer_offset. */
+ buffer_t *buffer;
+ /* Offset to log where the buffer starts from. 0 with mmaped log. */
+ uoff_t buffer_offset;
+ /* If non-NULL, mmap()ed log file */
+ void *mmap_base;
+ size_t mmap_size;
+
+ /* Offset to log file how far it's been read. Usually it's the same
+ as the log file size. However, if the last multi-record transaction
+ wasn't fully written (or is in the middle of being written), this
+ points to the beginning of the MAIL_TRANSACTION_BOUNDARY record. */
+ uoff_t sync_offset;
+ /* highest modseq at sync_offset */
+ uint64_t sync_highest_modseq;
+ /* The last mail_index_header.log_file_tail_offset update that was
+ read from the log. */
+ uoff_t last_read_hdr_tail_offset;
+ /* Update mail_index_header.log_file_tail_offset to this offset the
+ next time a transaction is written. Transaction log handling may
+ increase this automatically by making it skip external transactions
+ after last_read_hdr_tail_offset (to avoid re-reading them
+ needlessly). */
+ uoff_t max_tail_offset;
+
+ /* Last seen offsets for MAIL_TRANSACTION_INDEX_DELETED and
+ MAIL_TRANSACTION_INDEX_UNDELETED records. These are used to update
+ mail_index.index_delete* fields. */
+ uoff_t index_deleted_offset, index_undeleted_offset;
+
+ /* Cache to optimize mail_transaction_log_file_get_modseq_next_offset()
+ so it doesn't always have to start from the beginning of the log
+ file to find the wanted modseq. */
+ struct modseq_cache modseq_cache[LOG_FILE_MODSEQ_CACHE_SIZE];
+
+ /* Lock for the log file fd. If dotlocking is used, this is NULL and
+ mail_transaction_log.dotlock is used instead. */
+ struct file_lock *file_lock;
+ /* Time when the log was successfully locked */
+ time_t lock_create_time;
+
+ /* Log is currently locked. */
+ bool locked:1;
+ /* TRUE if sync_offset has already been updated while this log was
+ locked. This can be used to optimize away unnecessary checks to see
+ whether there's more data written to log after sync_offset. */
+ bool locked_sync_offset_updated:1;
+ /* Log file has found to be corrupted. Stop trying to read it.
+ The indexid is also usually overwritten to be 0 in the log header at
+ this time. */
+ bool corrupted:1;
+};
+
+struct mail_transaction_log {
+ struct mail_index *index;
+ /* Linked list of all transaction log views */
+ struct mail_transaction_log_view *views;
+ /* Paths to .log and .log.2 */
+ char *filepath, *filepath2;
+
+ /* Linked list of all the opened log files. The oldest files may have
+ already been unlinked. The list is sorted by the log file sequence
+ (oldest/lowest first), so that transaction views can use them
+ easily. */
+ struct mail_transaction_log_file *files;
+ /* Latest log file (the last file in the files linked list) */
+ struct mail_transaction_log_file *head;
+ /* open_file is used temporarily while opening the log file.
+ if mail_transaction_log_open() failed, it's left there for
+ mail_transaction_log_create(). */
+ struct mail_transaction_log_file *open_file;
+
+ /* Normally the .log locking is done via their file descriptors, so
+ e.g. rotating a log needs to lock both the old and the new files
+ at the same time. However, when FILE_LOCK_METHOD_DOTLOCK is used,
+ the lock isn't file-specific. There is just a single dotlock that
+ is created by the first log file lock. The second lock simply
+ increases the refcount. (It's not expected that there would be more
+ than 2 locks.) */
+ int dotlock_refcount;
+ struct dotlock *dotlock;
+
+ /* This session has already checked whether an old .log.2 should be
+ unlinked. */
+ bool log_2_unlink_checked:1;
+};
+
+void
+mail_transaction_log_file_set_corrupted(struct mail_transaction_log_file *file,
+ const char *fmt, ...)
+ ATTR_FORMAT(2, 3) ATTR_COLD;
+
+void mail_transaction_log_get_dotlock_set(struct mail_transaction_log *log,
+ struct dotlock_settings *set_r);
+
+struct mail_transaction_log_file *
+mail_transaction_log_file_alloc_in_memory(struct mail_transaction_log *log);
+struct mail_transaction_log_file *
+mail_transaction_log_file_alloc(struct mail_transaction_log *log,
+ const char *path);
+void mail_transaction_log_file_free(struct mail_transaction_log_file **file);
+
+/* Returns 1 if log was opened, 0 if it didn't exist or was already open,
+ -1 if error. */
+int mail_transaction_log_file_open(struct mail_transaction_log_file *file,
+ const char **reason_r);
+int mail_transaction_log_file_create(struct mail_transaction_log_file *file,
+ bool reset);
+int mail_transaction_log_file_lock(struct mail_transaction_log_file *file);
+
+int mail_transaction_log_find_file(struct mail_transaction_log *log,
+ uint32_t file_seq, bool nfs_flush,
+ struct mail_transaction_log_file **file_r,
+ const char **reason_r);
+
+/* Returns 1 if ok, 0 if file is corrupted or offset range is invalid,
+ -1 if I/O error */
+int mail_transaction_log_file_map(struct mail_transaction_log_file *file,
+ uoff_t start_offset, uoff_t end_offset,
+ const char **reason_r);
+int mail_transaction_log_file_move_to_memory(struct mail_transaction_log_file *file);
+
+void mail_transaction_logs_clean(struct mail_transaction_log *log);
+
+bool mail_transaction_log_want_rotate(struct mail_transaction_log *log,
+ const char **reason_r);
+int mail_transaction_log_rotate(struct mail_transaction_log *log, bool reset);
+int mail_transaction_log_lock_head(struct mail_transaction_log *log,
+ const char *lock_reason);
+void mail_transaction_log_file_unlock(struct mail_transaction_log_file *file,
+ const char *lock_reason);
+
+void mail_transaction_update_modseq(const struct mail_transaction_header *hdr,
+ const void *data, uint64_t *cur_modseq,
+ unsigned int version);
+/* Returns 1 if ok, 0 if file is corrupted or offset range is invalid,
+ -1 if I/O error */
+int mail_transaction_log_file_get_highest_modseq_at(
+ struct mail_transaction_log_file *file,
+ uoff_t offset, uint64_t *highest_modseq_r,
+ const char **error_r);
+int mail_transaction_log_file_get_modseq_next_offset(
+ struct mail_transaction_log_file *file,
+ uint64_t modseq, uoff_t *next_offset_r);
+
+#endif
diff --git a/src/lib-index/mail-transaction-log-view-private.h b/src/lib-index/mail-transaction-log-view-private.h
new file mode 100644
index 0000000..e739609
--- /dev/null
+++ b/src/lib-index/mail-transaction-log-view-private.h
@@ -0,0 +1,33 @@
+#ifndef MAIL_TRANSACTION_LOG_VIEW_PRIVATE_H
+#define MAIL_TRANSACTION_LOG_VIEW_PRIVATE_H
+
+#include "mail-transaction-log-private.h"
+
+struct mail_transaction_log_view {
+ struct mail_transaction_log *log;
+ struct mail_transaction_log_view *next;
+
+ uint32_t min_file_seq, max_file_seq;
+ uoff_t min_file_offset, max_file_offset;
+
+ struct mail_transaction_header tmp_hdr;
+
+ /* a list of log files we've referenced. we have to keep this list
+ explicitly because more files may be added into the linked list
+ at any time. */
+ ARRAY(struct mail_transaction_log_file *) file_refs;
+ struct mail_transaction_log_file *cur, *head, *tail;
+ uoff_t cur_offset;
+
+ uint64_t prev_modseq;
+ uint32_t prev_file_seq;
+ uoff_t prev_file_offset;
+
+ struct mail_transaction_log_file *mark_file;
+ uoff_t mark_offset, mark_next_offset;
+ uint64_t mark_modseq;
+
+ bool broken:1;
+};
+
+#endif
diff --git a/src/lib-index/mail-transaction-log-view.c b/src/lib-index/mail-transaction-log-view.c
new file mode 100644
index 0000000..0113bfb
--- /dev/null
+++ b/src/lib-index/mail-transaction-log-view.c
@@ -0,0 +1,909 @@
+/* Copyright (c) 2003-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "str.h"
+#include "mail-index-private.h"
+#include "mail-transaction-log-view-private.h"
+
+struct mail_transaction_log_view *
+mail_transaction_log_view_open(struct mail_transaction_log *log)
+{
+ struct mail_transaction_log_view *view;
+
+ view = i_new(struct mail_transaction_log_view, 1);
+ view->log = log;
+ view->broken = TRUE;
+
+ i_assert(view->log->head != NULL);
+
+ view->head = view->tail = view->log->head;
+ view->head->refcount++;
+ i_array_init(&view->file_refs, 8);
+ array_push_back(&view->file_refs, &view->head);
+
+ view->next = log->views;
+ log->views = view;
+ return view;
+}
+
+static void
+mail_transaction_log_view_unref_all(struct mail_transaction_log_view *view)
+{
+ struct mail_transaction_log_file *const *files;
+ unsigned int i, count;
+
+ files = array_get(&view->file_refs, &count);
+ for (i = 0; i < count; i++)
+ files[i]->refcount--;
+
+ array_clear(&view->file_refs);
+}
+
+void mail_transaction_log_view_close(struct mail_transaction_log_view **_view)
+{
+ struct mail_transaction_log_view *view = *_view;
+ struct mail_transaction_log_view **p;
+
+ *_view = NULL;
+
+ for (p = &view->log->views; *p != NULL; p = &(*p)->next) {
+ if (*p == view) {
+ *p = view->next;
+ break;
+ }
+ }
+
+ mail_transaction_log_view_unref_all(view);
+ mail_transaction_logs_clean(view->log);
+
+ array_free(&view->file_refs);
+ i_free(view);
+}
+
+static const char *
+mail_transaction_log_get_file_seqs(struct mail_transaction_log *log)
+{
+ struct mail_transaction_log_file *file;
+ string_t *str = t_str_new(32);
+
+ if (log->files == NULL)
+ return "";
+
+ for (file = log->files; file != NULL; file = file->next)
+ str_printfa(str, ",%u", file->hdr.file_seq);
+ return str_c(str) + 1;
+}
+
+static void
+view_set_failed_unref(struct mail_transaction_log_file *head,
+ struct mail_transaction_log_file *tail)
+{
+ struct mail_transaction_log_file *file;
+
+ if (tail == NULL) {
+ i_assert(head == NULL);
+ return;
+ }
+
+ for (file = tail; file != head; file = file->next) {
+ i_assert(file != NULL);
+ i_assert(file->refcount > 0);
+ file->refcount--;
+ }
+ i_assert(file != NULL);
+ i_assert(file->refcount > 0);
+ file->refcount--;
+}
+
+int mail_transaction_log_view_set(struct mail_transaction_log_view *view,
+ uint32_t min_file_seq, uoff_t min_file_offset,
+ uint32_t max_file_seq, uoff_t max_file_offset,
+ bool *reset_r, const char **reason_r)
+{
+ struct mail_transaction_log_file *tail, *head, *file, *const *files;
+ uoff_t start_offset, end_offset;
+ unsigned int i;
+ uint32_t seq;
+ int ret;
+
+ *reset_r = FALSE;
+ *reason_r = NULL;
+
+ if (view->log == NULL) {
+ /* transaction log is closed already. this log view shouldn't
+ be used anymore. */
+ *reason_r = "Log already closed";
+ return 0;
+ }
+
+ if (min_file_seq == 0) {
+ /* index file doesn't exist yet. this transaction log should
+ start from the beginning */
+ if (view->log->files->hdr.prev_file_seq != 0) {
+ /* but it doesn't */
+ *reason_r = t_strdup_printf(
+ "Wanted log beginning, but found prev_file_seq=%u",
+ view->log->files->hdr.prev_file_seq);
+ return 0;
+ }
+
+ min_file_seq = view->log->files->hdr.file_seq;
+ min_file_offset = 0;
+
+ if (max_file_seq == 0) {
+ max_file_seq = min_file_seq;
+ max_file_offset = min_file_offset;
+ }
+ }
+
+ for (file = view->log->files; file != NULL; file = file->next) {
+ if (file->hdr.prev_file_seq == min_file_seq)
+ break;
+ }
+
+ if (file != NULL && min_file_offset == file->hdr.prev_file_offset) {
+ /* we can (and sometimes must) skip to the next file */
+ min_file_seq = file->hdr.file_seq;
+ min_file_offset = file->hdr.hdr_size;
+ }
+
+ for (file = view->log->files; file != NULL; file = file->next) {
+ if (file->hdr.prev_file_seq == max_file_seq)
+ break;
+ }
+ if (file != NULL && max_file_offset == file->hdr.prev_file_offset) {
+ /* we can skip to the next file. we've delayed checking for
+ min_file_seq <= max_file_seq until now, because it's not
+ really an error to specify the same position twice (even if
+ in "wrong" order) */
+ i_assert(min_file_seq <= max_file_seq ||
+ min_file_seq <= file->hdr.file_seq);
+ max_file_seq = file->hdr.file_seq;
+ max_file_offset = file->hdr.hdr_size;
+ } else {
+ i_assert(min_file_seq <= max_file_seq);
+ }
+
+ if (min_file_seq == max_file_seq && min_file_offset > max_file_offset) {
+ /* log file offset is probably corrupted in the index file. */
+ *reason_r = t_strdup_printf(
+ "Invalid offset: file_seq=%u, min_file_offset (%"PRIuUOFF_T
+ ") > max_file_offset (%"PRIuUOFF_T")",
+ min_file_seq, min_file_offset, max_file_offset);
+ mail_transaction_log_view_set_corrupted(view, "%s", *reason_r);
+ return 0;
+ }
+
+ tail = head = file = NULL;
+ for (seq = min_file_seq; seq <= max_file_seq; seq++) {
+ const char *reason = NULL;
+
+ if (file == NULL || file->hdr.file_seq != seq) {
+ /* see if we could find the missing file. if we know
+ the max. file sequence or we don't have the the min.
+ file, make sure NFS attribute cache gets flushed if
+ necessary. */
+ bool nfs_flush = seq == min_file_seq ||
+ max_file_seq != (uint32_t)-1;
+
+ ret = mail_transaction_log_find_file(view->log, seq,
+ nfs_flush, &file, &reason);
+ if (ret <= 0) {
+ if (ret < 0) {
+ *reason_r = t_strdup_printf(
+ "Failed to find file seq=%u: %s",
+ seq, reason);
+ view_set_failed_unref(head, tail);
+ return -1;
+ }
+
+ /* not found / corrupted */
+ file = NULL;
+ }
+ }
+
+ if (file == NULL || file->hdr.file_seq != seq) {
+ i_assert(reason != NULL);
+ if (file == NULL && max_file_seq == (uint32_t)-1 &&
+ head == view->log->head) {
+ /* we just wanted to sync everything */
+ i_assert(max_file_offset == UOFF_T_MAX);
+ max_file_seq = seq-1;
+ break;
+ }
+ /* if any of the found files reset the index,
+ ignore any missing files up to it */
+ file = tail != NULL ? tail : view->log->files;
+ for (;; file = file->next) {
+ if (file == NULL ||
+ file->hdr.file_seq > max_file_seq) {
+ /* missing files in the middle */
+ *reason_r = t_strdup_printf(
+ "Missing middle file seq=%u (between %u..%u, we have seqs %s): %s",
+ seq, min_file_seq, max_file_seq,
+ mail_transaction_log_get_file_seqs(view->log), reason);
+ view_set_failed_unref(head, tail);
+ return 0;
+ }
+
+ if (file->hdr.file_seq >= seq &&
+ file->hdr.prev_file_seq == 0) {
+ /* we can ignore the missing file */
+ break;
+ }
+ }
+ /* we're going to rebuild the head/tail. remove the old
+ references first. */
+ view_set_failed_unref(head, tail);
+ seq = file->hdr.file_seq;
+ tail = NULL;
+ }
+
+ if (tail == NULL)
+ tail = file;
+ head = file;
+ /* NOTE: we need to reference immediately or it could become
+ freed by mail_transaction_log_find_file() */
+ file->refcount++;
+ file = file->next;
+ }
+ i_assert(tail != NULL);
+
+ if (min_file_offset == 0) {
+ /* beginning of the file */
+ min_file_offset = tail->hdr.hdr_size;
+ if (min_file_offset > max_file_offset &&
+ min_file_seq == max_file_seq) {
+ /* we don't actually want to show anything */
+ max_file_offset = min_file_offset;
+ }
+ }
+
+ if (min_file_offset < tail->hdr.hdr_size) {
+ /* log file offset is probably corrupted in the index file. */
+ *reason_r = t_strdup_printf(
+ "Invalid min_file_offset: file_seq=%u, min_file_offset (%"PRIuUOFF_T
+ ") < hdr_size (%u)",
+ min_file_seq, min_file_offset, tail->hdr.hdr_size);
+ mail_transaction_log_view_set_corrupted(view, "%s", *reason_r);
+ view_set_failed_unref(head, tail);
+ return 0;
+ }
+ if (max_file_offset < head->hdr.hdr_size) {
+ /* log file offset is probably corrupted in the index file. */
+ *reason_r = t_strdup_printf(
+ "Invalid max_file_offset: file_seq=%u, min_file_offset (%"PRIuUOFF_T
+ ") < hdr_size (%u)",
+ max_file_seq, max_file_offset, head->hdr.hdr_size);
+ mail_transaction_log_view_set_corrupted(view, "%s", *reason_r);
+ view_set_failed_unref(head, tail);
+ return 0;
+ }
+
+ /* we have all of them. update refcounts. */
+ mail_transaction_log_view_unref_all(view);
+
+ /* Reference all used files. */
+ view->tail = tail;
+ view->head = head;
+ for (file = view->tail; ; file = file->next) {
+ array_push_back(&view->file_refs, &file);
+
+ if (file == head)
+ break;
+ }
+
+ view->cur = view->tail;
+ view->cur_offset = view->cur->hdr.file_seq == min_file_seq ?
+ min_file_offset : view->cur->hdr.hdr_size;
+
+ /* Map the files only after we've found them all. Otherwise if we map
+ one file and then another file just happens to get rotated, we could
+ include both files in the view but skip the last transactions from
+ the first file.
+
+ We're mapping the files in reverse order so that _log_file_map()
+ can verify that prev_file_offset matches how far it actually managed
+ to sync the file. */
+ files = array_front(&view->file_refs);
+ for (i = array_count(&view->file_refs); i > 0; i--) {
+ file = files[i-1];
+ start_offset = file->hdr.file_seq == min_file_seq ?
+ min_file_offset : file->hdr.hdr_size;
+ end_offset = file->hdr.file_seq == max_file_seq ?
+ max_file_offset : UOFF_T_MAX;
+ ret = mail_transaction_log_file_map(file, start_offset,
+ end_offset, reason_r);
+ if (ret <= 0) {
+ *reason_r = t_strdup_printf(
+ "Failed to map file seq=%u "
+ "offset=%"PRIuUOFF_T"..%"PRIuUOFF_T" (ret=%d): %s",
+ file->hdr.file_seq, start_offset, end_offset, ret, *reason_r);
+ return ret;
+ }
+
+ if (file->hdr.prev_file_seq == 0) {
+ /* this file resets the index.
+ don't bother reading the others. */
+ if (view->cur != file ||
+ view->cur_offset == file->hdr.hdr_size) {
+ view->cur = file;
+ view->cur_offset = file->hdr.hdr_size;
+ *reset_r = TRUE;
+ break;
+ }
+ i_assert(i == 1);
+ }
+ }
+
+ if (min_file_seq == view->head->hdr.file_seq &&
+ min_file_offset > view->head->sync_offset) {
+ /* log file offset is probably corrupted in the index file. */
+ *reason_r = t_strdup_printf(
+ "Invalid offset: file_seq=%u, min_file_offset (%"PRIuUOFF_T
+ ") > sync_offset (%"PRIuUOFF_T")", min_file_seq,
+ min_file_offset, view->head->sync_offset);
+ mail_transaction_log_view_set_corrupted(view, "%s", *reason_r);
+ return 0;
+ }
+
+ i_assert(max_file_seq == (uint32_t)-1 ||
+ max_file_seq == view->head->hdr.file_seq);
+ i_assert(max_file_offset == UOFF_T_MAX ||
+ max_file_offset <= view->head->sync_offset);
+ i_assert(min_file_seq != max_file_seq ||
+ max_file_seq != view->head->hdr.file_seq ||
+ max_file_offset != UOFF_T_MAX ||
+ min_file_offset <= view->head->sync_offset);
+
+ view->prev_file_seq = view->cur->hdr.file_seq;
+ view->prev_file_offset = view->cur_offset;
+
+ view->min_file_seq = min_file_seq;
+ view->min_file_offset = min_file_offset;
+ view->max_file_seq = max_file_seq;
+ view->max_file_offset = I_MIN(max_file_offset, view->head->sync_offset);
+ view->broken = FALSE;
+
+ ret = mail_transaction_log_file_get_highest_modseq_at(view->cur,
+ view->cur_offset, &view->prev_modseq, reason_r);
+ if (ret <= 0)
+ return ret;
+
+ i_assert(view->cur_offset <= view->cur->sync_offset);
+ return 1;
+}
+
+int mail_transaction_log_view_set_all(struct mail_transaction_log_view *view)
+{
+ struct mail_transaction_log_file *file, *first;
+ const char *reason = NULL;
+ int ret;
+
+ /* make sure .log.2 file is opened */
+ (void)mail_transaction_log_find_file(view->log, 1, FALSE, &file, &reason);
+
+ first = view->log->files;
+ i_assert(first != NULL);
+
+ for (file = view->log->files; file != NULL; file = file->next) {
+ ret = mail_transaction_log_file_map(file, file->hdr.hdr_size,
+ UOFF_T_MAX, &reason);
+ if (ret < 0) {
+ first = NULL;
+ break;
+ }
+ if (ret == 0) {
+ /* corrupted */
+ first = NULL;
+ } else if (file->hdr.prev_file_seq == 0) {
+ /* this file resets the index. skip the old ones. */
+ first = file;
+ }
+ }
+ if (first == NULL) {
+ /* index wasn't reset after corruption was found */
+ i_assert(reason != NULL);
+ mail_index_set_error(view->log->index,
+ "Failed to map transaction log %s for all-view: %s",
+ view->log->filepath, reason);
+ return -1;
+ }
+
+ mail_transaction_log_view_unref_all(view);
+ for (file = first; file != NULL; file = file->next) {
+ array_push_back(&view->file_refs, &file);
+ file->refcount++;
+ }
+
+ view->tail = first;
+ view->cur = view->tail;
+ view->cur_offset = view->tail->hdr.hdr_size;
+
+ view->prev_file_seq = view->cur->hdr.file_seq;
+ view->prev_file_offset = view->cur_offset;
+
+ view->min_file_seq = view->cur->hdr.file_seq;
+ view->min_file_offset = view->cur_offset;
+ view->max_file_seq = view->head->hdr.file_seq;
+ view->max_file_offset = view->head->sync_offset;
+ view->broken = FALSE;
+
+ if (mail_transaction_log_file_get_highest_modseq_at(view->cur,
+ view->cur_offset, &view->prev_modseq, &reason) <= 0) {
+ mail_index_set_error(view->log->index,
+ "Failed to get modseq in %s for all-view: %s",
+ view->log->filepath, reason);
+ return -1;
+ }
+ return 0;
+}
+
+void mail_transaction_log_view_clear(struct mail_transaction_log_view *view,
+ uint32_t oldest_file_seq)
+{
+ struct mail_transaction_log_file *file;
+ const char *reason;
+
+ mail_transaction_log_view_unref_all(view);
+ if (oldest_file_seq != 0 &&
+ mail_transaction_log_find_file(view->log, oldest_file_seq, FALSE,
+ &file, &reason) > 0) {
+ for (; file != NULL; file = file->next) {
+ array_push_back(&view->file_refs, &file);
+ file->refcount++;
+ }
+ }
+
+ view->cur = view->head = view->tail = NULL;
+
+ view->mark_file = NULL;
+ view->mark_offset = 0;
+ view->mark_modseq = 0;
+
+ view->min_file_seq = view->max_file_seq = 0;
+ view->min_file_offset = view->max_file_offset = 0;
+ view->cur_offset = 0;
+
+ view->prev_file_seq = 0;
+ view->prev_file_offset = 0;
+ view->prev_modseq = 0;
+}
+
+void
+mail_transaction_log_view_get_prev_pos(struct mail_transaction_log_view *view,
+ uint32_t *file_seq_r,
+ uoff_t *file_offset_r)
+{
+ *file_seq_r = view->prev_file_seq;
+ *file_offset_r = view->prev_file_offset;
+}
+
+uint64_t
+mail_transaction_log_view_get_prev_modseq(struct mail_transaction_log_view *view)
+{
+ return view->prev_modseq;
+}
+
+static bool
+mail_transaction_log_view_get_last(struct mail_transaction_log_view *view,
+ struct mail_transaction_log_file **last_r,
+ uoff_t *last_offset_r)
+{
+ struct mail_transaction_log_file *cur = view->cur;
+ uoff_t cur_offset = view->cur_offset;
+ bool last = FALSE;
+
+ if (cur == NULL) {
+ *last_r = NULL;
+ return TRUE;
+ }
+
+ for (;;) {
+ if (cur->hdr.file_seq == view->max_file_seq) {
+ /* last file */
+ if (cur_offset == view->max_file_offset ||
+ cur_offset == cur->sync_offset) {
+ /* we're all finished */
+ last = TRUE;
+ }
+ } else if (cur_offset == cur->sync_offset) {
+ /* end of file, go to next one */
+ if (cur->next == NULL) {
+ last = TRUE;
+ } else {
+ cur = cur->next;
+ cur_offset = cur->hdr.hdr_size;
+ continue;
+ }
+ }
+
+ /* not EOF */
+ break;
+ }
+
+ *last_r = cur;
+ *last_offset_r = cur_offset;
+ return last;
+}
+
+bool mail_transaction_log_view_is_last(struct mail_transaction_log_view *view)
+{
+ struct mail_transaction_log_file *cur;
+ uoff_t cur_offset;
+
+ return mail_transaction_log_view_get_last(view, &cur, &cur_offset);
+}
+
+void
+mail_transaction_log_view_set_corrupted(struct mail_transaction_log_view *view,
+ const char *fmt, ...)
+{
+ va_list va;
+
+ view->broken = TRUE;
+
+ va_start(va, fmt);
+ T_BEGIN {
+ mail_transaction_log_file_set_corrupted(view->log->head, "%s",
+ t_strdup_vprintf(fmt, va));
+ } T_END;
+ va_end(va);
+}
+
+bool
+mail_transaction_log_view_is_corrupted(struct mail_transaction_log_view *view)
+{
+ return view->broken;
+}
+
+static bool
+log_view_is_uid_range_valid(struct mail_transaction_log_file *file,
+ enum mail_transaction_type rec_type,
+ const ARRAY_TYPE(seq_range) *uids)
+{
+ const struct seq_range *rec, *prev = NULL;
+ unsigned int i, count = array_count(uids);
+
+ if ((uids->arr.buffer->used % uids->arr.element_size) != 0) {
+ mail_transaction_log_file_set_corrupted(file,
+ "Invalid record size (type=0x%x)", rec_type);
+ return FALSE;
+ } else if (count == 0) {
+ mail_transaction_log_file_set_corrupted(file,
+ "No UID ranges (type=0x%x)", rec_type);
+ return FALSE;
+ }
+
+ for (i = 0; i < count; i++, prev = rec) {
+ rec = array_idx(uids, i);
+ if (rec->seq1 > rec->seq2 || rec->seq1 == 0) {
+ mail_transaction_log_file_set_corrupted(file,
+ "Invalid UID range (%u .. %u, type=0x%x)",
+ rec->seq1, rec->seq2, rec_type);
+ return FALSE;
+ }
+ if (prev != NULL && rec->seq1 <= prev->seq2) {
+ mail_transaction_log_file_set_corrupted(file,
+ "Non-sorted UID ranges (type=0x%x)", rec_type);
+ return FALSE;
+ }
+ }
+ return TRUE;
+}
+
+static bool
+log_view_is_record_valid(struct mail_transaction_log_file *file,
+ const struct mail_transaction_header *hdr,
+ const void *data)
+{
+ enum mail_transaction_type rec_type;
+ ARRAY_TYPE(seq_range) uids = ARRAY_INIT;
+ buffer_t uid_buf;
+ uint32_t rec_size;
+
+ rec_type = hdr->type & MAIL_TRANSACTION_TYPE_MASK;
+ rec_size = mail_index_offset_to_uint32(hdr->size) - sizeof(*hdr);
+
+ /* we want to be extra careful with expunges */
+ if ((hdr->type & MAIL_TRANSACTION_EXPUNGE) != 0) {
+ if (rec_type != (MAIL_TRANSACTION_EXPUNGE |
+ MAIL_TRANSACTION_EXPUNGE_PROT)) {
+ mail_transaction_log_file_set_corrupted(file,
+ "expunge record missing protection mask");
+ return FALSE;
+ }
+ rec_type &= ENUM_NEGATE(MAIL_TRANSACTION_EXPUNGE_PROT);
+ }
+ if ((hdr->type & MAIL_TRANSACTION_EXPUNGE_GUID) != 0) {
+ if (rec_type != (MAIL_TRANSACTION_EXPUNGE_GUID |
+ MAIL_TRANSACTION_EXPUNGE_PROT)) {
+ mail_transaction_log_file_set_corrupted(file,
+ "expunge guid record missing protection mask");
+ return FALSE;
+ }
+ rec_type &= ENUM_NEGATE(MAIL_TRANSACTION_EXPUNGE_PROT);
+ }
+
+ if (rec_size == 0) {
+ mail_transaction_log_file_set_corrupted(file,
+ "Empty record contents (type=0x%x)", rec_type);
+ return FALSE;
+ }
+
+ /* records that are exported by syncing and view syncing will be
+ checked here so that we don't have to implement the same validation
+ multiple times. other records are checked internally by
+ mail_index_sync_record(). */
+ switch (rec_type) {
+ case MAIL_TRANSACTION_APPEND:
+ if ((rec_size % sizeof(struct mail_index_record)) != 0) {
+ mail_transaction_log_file_set_corrupted(file,
+ "Invalid append record size");
+ return FALSE;
+ }
+ break;
+ case MAIL_TRANSACTION_EXPUNGE:
+ buffer_create_from_const_data(&uid_buf, data, rec_size);
+ array_create_from_buffer(&uids, &uid_buf,
+ sizeof(struct mail_transaction_expunge));
+ break;
+ case MAIL_TRANSACTION_EXPUNGE_GUID: {
+ const struct mail_transaction_expunge_guid *recs = data;
+ unsigned int i, count;
+
+ if ((rec_size % sizeof(*recs)) != 0) {
+ mail_transaction_log_file_set_corrupted(file,
+ "Invalid expunge guid record size");
+ return FALSE;
+ }
+ count = rec_size / sizeof(*recs);
+ for (i = 0; i < count; i++) {
+ if (recs[i].uid == 0) {
+ mail_transaction_log_file_set_corrupted(file,
+ "Expunge guid record with uid=0");
+ return FALSE;
+ }
+ }
+ break;
+ }
+ case MAIL_TRANSACTION_FLAG_UPDATE:
+ buffer_create_from_const_data(&uid_buf, data, rec_size);
+ array_create_from_buffer(&uids, &uid_buf,
+ sizeof(struct mail_transaction_flag_update));
+ break;
+ case MAIL_TRANSACTION_KEYWORD_UPDATE: {
+ const struct mail_transaction_keyword_update *rec = data;
+ unsigned int seqset_offset;
+
+ seqset_offset = sizeof(*rec) + rec->name_size;
+ if ((seqset_offset % 4) != 0)
+ seqset_offset += 4 - (seqset_offset % 4);
+
+ if (rec->name_size == 0) {
+ mail_transaction_log_file_set_corrupted(file,
+ "Trying to use empty keyword");
+ return FALSE;
+ }
+ if (seqset_offset > rec_size) {
+ mail_transaction_log_file_set_corrupted(file,
+ "Invalid keyword update record size");
+ return FALSE;
+ }
+
+ buffer_create_from_const_data(&uid_buf,
+ CONST_PTR_OFFSET(data, seqset_offset),
+ rec_size - seqset_offset);
+ array_create_from_buffer(&uids, &uid_buf,
+ sizeof(uint32_t)*2);
+ break;
+ }
+ case MAIL_TRANSACTION_KEYWORD_RESET:
+ buffer_create_from_const_data(&uid_buf, data, rec_size);
+ array_create_from_buffer(&uids, &uid_buf,
+ sizeof(struct mail_transaction_keyword_reset));
+ break;
+ case MAIL_TRANSACTION_EXT_INTRO: {
+ const struct mail_transaction_ext_intro *rec;
+ unsigned int i;
+
+ for (i = 0; i < rec_size; ) {
+ if (i + sizeof(*rec) > rec_size) {
+ /* should be just extra padding */
+ break;
+ }
+
+ rec = CONST_PTR_OFFSET(data, i);
+ if (i + sizeof(*rec) + rec->name_size > rec_size) {
+ mail_transaction_log_file_set_corrupted(file,
+ "ext intro: name_size too large");
+ return FALSE;
+ }
+ i += sizeof(*rec) + rec->name_size;
+ if ((i % 4) != 0)
+ i += 4 - (i % 4);
+ }
+ break;
+ }
+ case MAIL_TRANSACTION_ATTRIBUTE_UPDATE: {
+ const char *attr_changes = data;
+ unsigned int i;
+
+ for (i = 0; i+2 < rec_size && attr_changes[i] != '\0'; ) {
+ if (attr_changes[i] != '+' && attr_changes[i] != '-') {
+ mail_transaction_log_file_set_corrupted(file,
+ "attribute update: Invalid prefix 0x%02x",
+ attr_changes[i]);
+ return FALSE;
+ }
+ i++;
+ if (attr_changes[i] != 'p' && attr_changes[i] != 's') {
+ mail_transaction_log_file_set_corrupted(file,
+ "attribute update: Invalid type 0x%02x",
+ attr_changes[i]);
+ return FALSE;
+ }
+ i++;
+ if (attr_changes[i] == '\0') {
+ mail_transaction_log_file_set_corrupted(file,
+ "attribute update: Empty key");
+ return FALSE;
+ }
+ i += strlen(attr_changes+i) + 1;
+ }
+ if (i == 0 || (i < rec_size && attr_changes[i] != '\0')) {
+ mail_transaction_log_file_set_corrupted(file,
+ "attribute update doesn't end with NUL");
+ return FALSE;
+ }
+ break;
+ }
+ default:
+ break;
+ }
+
+ if (array_is_created(&uids)) {
+ if (!log_view_is_uid_range_valid(file, rec_type, &uids))
+ return FALSE;
+ }
+ return TRUE;
+}
+
+static int
+log_view_get_next(struct mail_transaction_log_view *view,
+ const struct mail_transaction_header **hdr_r,
+ const void **data_r)
+{
+ const struct mail_transaction_header *hdr;
+ struct mail_transaction_log_file *file;
+ const void *data;
+ enum mail_transaction_type rec_type;
+ uint32_t full_size;
+ size_t file_size;
+ int ret;
+
+ if (view->cur == NULL)
+ return 0;
+
+ /* prev_file_offset should point to beginning of previous log record.
+ when we reach EOF, it should be left there, not to beginning of the
+ next file that's not included inside the view. */
+ if (mail_transaction_log_view_get_last(view, &view->cur,
+ &view->cur_offset)) {
+ /* if the last file was the beginning of a file, we want to
+ move prev pointers there */
+ view->prev_file_seq = view->cur->hdr.file_seq;
+ view->prev_file_offset = view->cur_offset;
+ view->cur = NULL;
+ return 0;
+ }
+
+ view->prev_file_seq = view->cur->hdr.file_seq;
+ view->prev_file_offset = view->cur_offset;
+
+ file = view->cur;
+
+ data = file->buffer->data;
+ file_size = file->buffer->used + file->buffer_offset;
+
+ if (view->cur_offset + sizeof(*hdr) > file_size) {
+ mail_transaction_log_file_set_corrupted(file,
+ "offset points outside file "
+ "(%"PRIuUOFF_T" + %zu > %zu)",
+ view->cur_offset, sizeof(*hdr), file_size);
+ return -1;
+ }
+
+ i_assert(view->cur_offset >= file->buffer_offset);
+ hdr = CONST_PTR_OFFSET(data, view->cur_offset - file->buffer_offset);
+ data = CONST_PTR_OFFSET(hdr, sizeof(*hdr));
+
+ rec_type = hdr->type & MAIL_TRANSACTION_TYPE_MASK;
+ full_size = mail_index_offset_to_uint32(hdr->size);
+ if (full_size < sizeof(*hdr)) {
+ mail_transaction_log_file_set_corrupted(file,
+ "record size too small (type=0x%x, "
+ "offset=%"PRIuUOFF_T", size=%u)",
+ rec_type, view->cur_offset, full_size);
+ return -1;
+ }
+
+ if (file_size - view->cur_offset < full_size) {
+ mail_transaction_log_file_set_corrupted(file,
+ "record size too large (type=0x%x, "
+ "offset=%"PRIuUOFF_T", size=%u, end=%zu)",
+ rec_type, view->cur_offset, full_size, file_size);
+ return -1;
+ }
+
+ T_BEGIN {
+ ret = log_view_is_record_valid(file, hdr, data) ? 1 : -1;
+ } T_END;
+ if (ret > 0) {
+ mail_transaction_update_modseq(hdr, data, &view->prev_modseq,
+ MAIL_TRANSACTION_LOG_HDR_VERSION(&file->hdr));
+ *hdr_r = hdr;
+ *data_r = data;
+ view->cur_offset += full_size;
+ }
+ return ret;
+}
+
+int mail_transaction_log_view_next(struct mail_transaction_log_view *view,
+ const struct mail_transaction_header **hdr_r,
+ const void **data_r)
+{
+ const struct mail_transaction_header *hdr;
+ const void *data;
+ int ret = 0;
+
+ if (view->broken)
+ return -1;
+
+ ret = log_view_get_next(view, &hdr, &data);
+ if (ret <= 0) {
+ if (ret < 0)
+ view->cur_offset = view->cur->sync_offset;
+ return ret;
+ }
+
+ /* drop expunge protection */
+ if ((hdr->type & MAIL_TRANSACTION_TYPE_MASK) ==
+ (MAIL_TRANSACTION_EXPUNGE | MAIL_TRANSACTION_EXPUNGE_PROT) ||
+ (hdr->type & MAIL_TRANSACTION_TYPE_MASK) ==
+ (MAIL_TRANSACTION_EXPUNGE_GUID | MAIL_TRANSACTION_EXPUNGE_PROT))
+ view->tmp_hdr.type = hdr->type & ENUM_NEGATE(MAIL_TRANSACTION_EXPUNGE_PROT);
+ else
+ view->tmp_hdr.type = hdr->type;
+
+ /* return record's size */
+ view->tmp_hdr.size = mail_index_offset_to_uint32(hdr->size);
+ i_assert(view->tmp_hdr.size > sizeof(*hdr));
+ view->tmp_hdr.size -= sizeof(*hdr);
+
+ *hdr_r = &view->tmp_hdr;
+ *data_r = data;
+ return 1;
+}
+
+void mail_transaction_log_view_mark(struct mail_transaction_log_view *view)
+{
+ i_assert(view->cur->hdr.file_seq == view->prev_file_seq);
+
+ view->mark_file = view->cur;
+ view->mark_offset = view->prev_file_offset;
+ view->mark_next_offset = view->cur_offset;
+ view->mark_modseq = view->prev_modseq;
+}
+
+void mail_transaction_log_view_rewind(struct mail_transaction_log_view *view)
+{
+ i_assert(view->mark_file != NULL);
+
+ view->cur = view->mark_file;
+ view->cur_offset = view->mark_next_offset;
+ view->prev_file_seq = view->cur->hdr.file_seq;
+ view->prev_file_offset = view->mark_offset;
+ view->prev_modseq = view->mark_modseq;
+}
diff --git a/src/lib-index/mail-transaction-log.c b/src/lib-index/mail-transaction-log.c
new file mode 100644
index 0000000..6e9b1eb
--- /dev/null
+++ b/src/lib-index/mail-transaction-log.c
@@ -0,0 +1,664 @@
+/* Copyright (c) 2003-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "ioloop.h"
+#include "buffer.h"
+#include "file-dotlock.h"
+#include "nfs-workarounds.h"
+#include "mmap-util.h"
+#include "mail-index-private.h"
+#include "mail-transaction-log-private.h"
+
+#include <stddef.h>
+#include <stdio.h>
+#include <sys/stat.h>
+
+static void
+mail_transaction_log_set_head(struct mail_transaction_log *log,
+ struct mail_transaction_log_file *file)
+{
+ i_assert(log->head != file);
+
+ file->refcount++;
+ log->head = file;
+
+ i_assert(log->files != NULL);
+ i_assert(log->files->next != NULL || log->files == file);
+}
+
+struct mail_transaction_log *
+mail_transaction_log_alloc(struct mail_index *index)
+{
+ struct mail_transaction_log *log;
+
+ log = i_new(struct mail_transaction_log, 1);
+ log->index = index;
+ return log;
+}
+
+static void mail_transaction_log_2_unlink_old(struct mail_transaction_log *log)
+{
+ struct stat st;
+ uint32_t log2_rotate_time = log->index->map->hdr.log2_rotate_time;
+
+ if (MAIL_INDEX_IS_IN_MEMORY(log->index))
+ return;
+
+ if (log2_rotate_time == 0) {
+ if (nfs_safe_stat(log->filepath2, &st) == 0)
+ log2_rotate_time = st.st_mtime;
+ else if (errno == ENOENT)
+ log2_rotate_time = (uint32_t)-1;
+ else {
+ mail_index_set_error(log->index,
+ "stat(%s) failed: %m", log->filepath2);
+ return;
+ }
+ }
+
+ if (log2_rotate_time != (uint32_t)-1 &&
+ ioloop_time - (time_t)log2_rotate_time >= (time_t)log->index->optimization_set.log.log2_max_age_secs &&
+ !log->index->readonly) {
+ i_unlink_if_exists(log->filepath2);
+ log2_rotate_time = (uint32_t)-1;
+ }
+
+ if (log2_rotate_time != log->index->map->hdr.log2_rotate_time) {
+ /* Either the log2_rotate_time in header was missing, or we
+ just deleted the .log.2 and need to set it as nonexistent.
+ Either way we need to update the header.
+
+ Write this as part of the next sync's transaction. We're
+ here because we're already opening a sync lock, so it'll
+ always happen. It's also required especially with mdbox map
+ index, which doesn't like changes done outside syncing. */
+ log->index->hdr_log2_rotate_time_delayed_update =
+ log2_rotate_time;
+ }
+}
+
+int mail_transaction_log_open(struct mail_transaction_log *log)
+{
+ struct mail_transaction_log_file *file;
+ const char *reason;
+ int ret;
+
+ i_free(log->filepath);
+ i_free(log->filepath2);
+ log->filepath = i_strconcat(log->index->filepath,
+ MAIL_TRANSACTION_LOG_SUFFIX, NULL);
+ log->filepath2 = i_strconcat(log->filepath, ".2", NULL);
+
+ if (log->open_file != NULL)
+ mail_transaction_log_file_free(&log->open_file);
+
+ if (MAIL_INDEX_IS_IN_MEMORY(log->index))
+ return 0;
+
+ file = mail_transaction_log_file_alloc(log, log->filepath);
+ if ((ret = mail_transaction_log_file_open(file, &reason)) <= 0) {
+ /* leave the file for _create() */
+ log->open_file = file;
+ return ret;
+ }
+ mail_transaction_log_set_head(log, file);
+ return 1;
+}
+
+int mail_transaction_log_create(struct mail_transaction_log *log, bool reset)
+{
+ struct mail_transaction_log_file *file;
+
+ if (MAIL_INDEX_IS_IN_MEMORY(log->index)) {
+ file = mail_transaction_log_file_alloc_in_memory(log);
+ mail_transaction_log_set_head(log, file);
+ return 0;
+ }
+
+ file = mail_transaction_log_file_alloc(log, log->filepath);
+ if (log->open_file != NULL) {
+ /* remember what file we tried to open. if someone else created
+ a new file, use it instead of recreating it */
+ file->st_ino = log->open_file->st_ino;
+ file->st_dev = log->open_file->st_dev;
+ file->last_size = log->open_file->last_size;
+ file->last_mtime = log->open_file->last_mtime;
+ mail_transaction_log_file_free(&log->open_file);
+ }
+
+ if (mail_transaction_log_file_create(file, reset) < 0) {
+ mail_transaction_log_file_free(&file);
+ return -1;
+ }
+
+ mail_transaction_log_set_head(log, file);
+ return 1;
+}
+
+void mail_transaction_log_close(struct mail_transaction_log *log)
+{
+ i_assert(log->views == NULL);
+
+ if (log->open_file != NULL)
+ mail_transaction_log_file_free(&log->open_file);
+ if (log->head != NULL)
+ log->head->refcount--;
+ mail_transaction_logs_clean(log);
+ i_assert(log->files == NULL);
+}
+
+void mail_transaction_log_free(struct mail_transaction_log **_log)
+{
+ struct mail_transaction_log *log = *_log;
+
+ *_log = NULL;
+
+ mail_transaction_log_close(log);
+ log->index->log = NULL;
+ i_free(log->filepath);
+ i_free(log->filepath2);
+ i_free(log);
+}
+
+int mail_transaction_log_move_to_memory(struct mail_transaction_log *log)
+{
+ struct mail_transaction_log_file *file;
+
+ if (!log->index->initial_mapped && log->files != NULL &&
+ log->files->hdr.prev_file_seq != 0) {
+ /* we couldn't read dovecot.index and we don't have the first
+ .log file, so just start from scratch */
+ mail_transaction_log_close(log);
+ }
+
+ i_free(log->filepath);
+ i_free(log->filepath2);
+ log->filepath = i_strconcat(log->index->filepath,
+ MAIL_TRANSACTION_LOG_SUFFIX, NULL);
+ log->filepath2 = i_strconcat(log->filepath, ".2", NULL);
+
+ if (log->head != NULL)
+ return mail_transaction_log_file_move_to_memory(log->head);
+ else {
+ file = mail_transaction_log_file_alloc_in_memory(log);
+ mail_transaction_log_set_head(log, file);
+ return 0;
+ }
+}
+
+void mail_transaction_log_indexid_changed(struct mail_transaction_log *log)
+{
+ struct mail_transaction_log_file *file;
+
+ mail_transaction_logs_clean(log);
+
+ for (file = log->files; file != NULL; file = file->next) {
+ if (file->hdr.indexid != log->index->indexid) {
+ mail_transaction_log_file_set_corrupted(file,
+ "indexid changed: %u -> %u",
+ file->hdr.indexid, log->index->indexid);
+ }
+ }
+
+ if (log->head != NULL &&
+ log->head->hdr.indexid != log->index->indexid) {
+ struct mail_transaction_log_file *old_head = log->head;
+
+ (void)mail_transaction_log_create(log, FALSE);
+ if (--old_head->refcount == 0) {
+ if (old_head == log->head) {
+ /* failed to create a new log */
+ log->head = NULL;
+ }
+ mail_transaction_log_file_free(&old_head);
+ }
+ }
+}
+
+void mail_transaction_logs_clean(struct mail_transaction_log *log)
+{
+ struct mail_transaction_log_file *file, *next;
+
+ /* remove only files from the beginning. this way if a view has
+ referenced an old file, it can still find the new files even if
+ there aren't any references to it currently. */
+ for (file = log->files; file != NULL; file = next) {
+ next = file->next;
+
+ i_assert(file->refcount >= 0);
+ if (file->refcount > 0)
+ break;
+
+ mail_transaction_log_file_free(&file);
+ }
+ /* sanity check: we shouldn't have locked refcount=0 files */
+ for (; file != NULL; file = file->next) {
+ i_assert(!file->locked || file->refcount > 0);
+ }
+ i_assert(log->head == NULL || log->files != NULL);
+}
+
+bool mail_transaction_log_want_rotate(struct mail_transaction_log *log,
+ const char **reason_r)
+{
+ struct mail_transaction_log_file *file = log->head;
+
+ if (file->need_rotate != NULL) {
+ *reason_r = t_strdup(file->need_rotate);
+ return TRUE;
+ }
+
+ if (file->hdr.major_version < MAIL_TRANSACTION_LOG_MAJOR_VERSION ||
+ (file->hdr.major_version == MAIL_TRANSACTION_LOG_MAJOR_VERSION &&
+ file->hdr.minor_version < MAIL_TRANSACTION_LOG_MINOR_VERSION)) {
+ /* upgrade immediately to a new log file format */
+ *reason_r = t_strdup_printf(
+ ".log file format version %u.%u is too old",
+ file->hdr.major_version, file->hdr.minor_version);
+ return TRUE;
+ }
+
+ if (file->sync_offset > log->index->optimization_set.log.max_size) {
+ /* file is too large, definitely rotate */
+ *reason_r = t_strdup_printf(
+ ".log file size %"PRIuUOFF_T" > max_size %"PRIuUOFF_T,
+ file->sync_offset, log->index->optimization_set.log.max_size);
+ return TRUE;
+ }
+ if (file->sync_offset < log->index->optimization_set.log.min_size) {
+ /* file is still too small */
+ return FALSE;
+ }
+ /* rotate if the timestamp is old enough */
+ if (file->hdr.create_stamp <
+ ioloop_time - log->index->optimization_set.log.min_age_secs) {
+ *reason_r = t_strdup_printf(
+ ".log create_stamp %u is older than %u secs",
+ file->hdr.create_stamp,
+ log->index->optimization_set.log.min_age_secs);
+ return TRUE;
+ }
+ return FALSE;
+}
+
+int mail_transaction_log_rotate(struct mail_transaction_log *log, bool reset)
+{
+ struct mail_transaction_log_file *file, *old_head;
+ const char *path = log->head->filepath;
+ struct stat st;
+ int ret;
+
+ i_assert(log->head->locked);
+
+ if (MAIL_INDEX_IS_IN_MEMORY(log->index)) {
+ file = mail_transaction_log_file_alloc_in_memory(log);
+ if (reset) {
+ file->hdr.prev_file_seq = 0;
+ file->hdr.prev_file_offset = 0;
+ }
+ } else {
+ /* we're locked, we shouldn't need to worry about ESTALE
+ problems in here. */
+ if (fstat(log->head->fd, &st) < 0) {
+ mail_index_file_set_syscall_error(log->index,
+ log->head->filepath, "fstat()");
+ return -1;
+ }
+
+ file = mail_transaction_log_file_alloc(log, path);
+
+ file->st_dev = st.st_dev;
+ file->st_ino = st.st_ino;
+ file->last_mtime = st.st_mtime;
+ file->last_size = st.st_size;
+
+ if ((ret = mail_transaction_log_file_create(file, reset)) < 0) {
+ mail_transaction_log_file_free(&file);
+ return -1;
+ }
+ if (ret == 0) {
+ mail_index_set_error(log->index,
+ "Transaction log %s was recreated while we had it locked - "
+ "locking is broken (lock_method=%s)", path,
+ file_lock_method_to_str(log->index->set.lock_method));
+ mail_transaction_log_file_free(&file);
+ return -1;
+ }
+ i_assert(file->locked);
+ }
+
+ old_head = log->head;
+ mail_transaction_log_set_head(log, file);
+
+ e_debug(log->index->event, "Rotated transaction log %s (seq=%u, reset=%s)",
+ file->filepath, file->hdr.file_seq, reset ? "yes" : "no");
+
+ /* the newly created log file is already locked */
+ mail_transaction_log_file_unlock(old_head,
+ !log->index->log_sync_locked ? "rotating" :
+ "rotating while syncing");
+ if (--old_head->refcount == 0)
+ mail_transaction_logs_clean(log);
+ return 0;
+}
+
+static int
+mail_transaction_log_refresh(struct mail_transaction_log *log, bool nfs_flush,
+ const char **reason_r)
+{
+ struct mail_transaction_log_file *file;
+ struct stat st;
+
+ i_assert(log->head != NULL);
+
+ if (MAIL_TRANSACTION_LOG_FILE_IN_MEMORY(log->head)) {
+ *reason_r = "Log is in memory";
+ return 0;
+ }
+
+ if (nfs_flush &&
+ (log->index->flags & MAIL_INDEX_OPEN_FLAG_NFS_FLUSH) != 0)
+ nfs_flush_file_handle_cache(log->filepath);
+ if (nfs_safe_stat(log->filepath, &st) < 0) {
+ if (errno != ENOENT) {
+ mail_index_file_set_syscall_error(log->index,
+ log->filepath,
+ "stat()");
+ *reason_r = t_strdup_printf("stat(%s) failed: %m", log->filepath);
+ return -1;
+ }
+ /* We shouldn't lose dovecot.index.log unless the mailbox was
+ deleted or renamed. Just fail this and let the mailbox
+ opening code figure out whether to create a new log file
+ or not. Anything else can cause unwanted behavior (e.g.
+ mailbox deletion not fully finishing due to .nfs* files and
+ an IDLEing IMAP process creating the index back here). */
+ log->index->index_deleted = TRUE;
+ *reason_r = "Trasnaction log lost while it was open";
+ return -1;
+ } else if (log->head->st_ino == st.st_ino &&
+ CMP_DEV_T(log->head->st_dev, st.st_dev)) {
+ /* NFS: log files get rotated to .log.2 files instead
+ of being unlinked, so we don't bother checking if
+ the existing file has already been unlinked here
+ (in which case inodes could match but point to
+ different files) */
+ *reason_r = "Log inode is unchanged";
+ return 0;
+ }
+
+ file = mail_transaction_log_file_alloc(log, log->filepath);
+ if (mail_transaction_log_file_open(file, reason_r) <= 0) {
+ *reason_r = t_strdup_printf(
+ "Failed to refresh main transaction log: %s", *reason_r);
+ mail_transaction_log_file_free(&file);
+ return -1;
+ }
+
+ i_assert(!file->locked);
+
+ struct mail_transaction_log_file *old_head = log->head;
+ mail_transaction_log_set_head(log, file);
+ if (--old_head->refcount == 0)
+ mail_transaction_logs_clean(log);
+ *reason_r = "Log reopened";
+ return 0;
+}
+
+void mail_transaction_log_get_mailbox_sync_pos(struct mail_transaction_log *log,
+ uint32_t *file_seq_r,
+ uoff_t *file_offset_r)
+{
+ *file_seq_r = log->head->hdr.file_seq;
+ *file_offset_r = log->head->max_tail_offset;
+}
+
+void mail_transaction_log_set_mailbox_sync_pos(struct mail_transaction_log *log,
+ uint32_t file_seq,
+ uoff_t file_offset)
+{
+ i_assert(file_seq == log->head->hdr.file_seq);
+ i_assert(file_offset >= log->head->last_read_hdr_tail_offset);
+
+ if (file_offset >= log->head->max_tail_offset)
+ log->head->max_tail_offset = file_offset;
+}
+
+int mail_transaction_log_find_file(struct mail_transaction_log *log,
+ uint32_t file_seq, bool nfs_flush,
+ struct mail_transaction_log_file **file_r,
+ const char **reason_r)
+{
+ struct mail_transaction_log_file *file;
+ const char *reason;
+ int ret;
+
+ if (file_seq > log->head->hdr.file_seq) {
+ /* see if the .log file has been recreated */
+ if (log->head->locked) {
+ /* transaction log is locked. there's no way a newer
+ file exists. */
+ *reason_r = "Log is locked - newer log can't exist";
+ return 0;
+ }
+
+ if (mail_transaction_log_refresh(log, FALSE, &reason) < 0) {
+ *reason_r = reason;
+ return -1;
+ }
+ if (file_seq > log->head->hdr.file_seq) {
+ if (!nfs_flush ||
+ (log->index->flags & MAIL_INDEX_OPEN_FLAG_NFS_FLUSH) == 0) {
+ *reason_r = t_strdup_printf(
+ "Requested newer log than exists: %s", reason);
+ return 0;
+ }
+ /* try again, this time flush attribute cache */
+ if (mail_transaction_log_refresh(log, TRUE, &reason) < 0) {
+ *reason_r = t_strdup_printf(
+ "Log refresh with NFS flush failed: %s", reason);
+ return -1;
+ }
+ if (file_seq > log->head->hdr.file_seq) {
+ *reason_r = t_strdup_printf(
+ "Requested newer log than exists - "
+ "still after NFS flush: %s", reason);
+ return 0;
+ }
+ }
+ }
+
+ for (file = log->files; file != NULL; file = file->next) {
+ if (file->hdr.file_seq == file_seq) {
+ *file_r = file;
+ return 1;
+ }
+ if (file->hdr.file_seq > file_seq &&
+ file->hdr.prev_file_seq == 0) {
+ /* Fail here mainly to avoid unnecessarily trying to
+ open .log.2 that most likely doesn't even exist. */
+ *reason_r = "Log was reset after requested file_seq";
+ return 0;
+ }
+ }
+
+ if (MAIL_INDEX_IS_IN_MEMORY(log->index)) {
+ *reason_r = "Logs are only in memory";
+ return 0;
+ }
+
+ /* see if we have it in log.2 file */
+ file = mail_transaction_log_file_alloc(log, log->filepath2);
+ if ((ret = mail_transaction_log_file_open(file, reason_r)) <= 0) {
+ *reason_r = t_strdup_printf(
+ "Not found from .log.2: %s", *reason_r);
+ mail_transaction_log_file_free(&file);
+ return ret;
+ }
+
+ /* but is it what we expected? */
+ if (file->hdr.file_seq != file_seq) {
+ *reason_r = t_strdup_printf(".log.2 contains file_seq=%u",
+ file->hdr.file_seq);
+ return 0;
+ }
+
+ *file_r = file;
+ return 1;
+}
+
+int mail_transaction_log_lock_head(struct mail_transaction_log *log,
+ const char *lock_reason)
+{
+ struct mail_transaction_log_file *file;
+ time_t lock_wait_started, lock_secs = 0;
+ const char *reason;
+ int ret = 0;
+
+ /* we want to get the head file locked. this is a bit racy,
+ since by the time we have it locked a new log file may have been
+ created.
+
+ creating new log file requires locking the head file, so if we
+ can lock it and don't see another file, we can be sure no-one is
+ creating a new log at the moment */
+
+ lock_wait_started = time(NULL);
+ for (;;) {
+ file = log->head;
+ if (mail_transaction_log_file_lock(file) < 0)
+ return -1;
+
+ file->refcount++;
+ ret = mail_transaction_log_refresh(log, TRUE, &reason);
+ if (--file->refcount == 0) {
+ mail_transaction_log_file_unlock(file, t_strdup_printf(
+ "trying to lock head for %s", lock_reason));
+ mail_transaction_logs_clean(log);
+ file = NULL;
+ }
+
+ if (ret == 0 && log->head == file) {
+ /* success */
+ i_assert(file != NULL);
+ lock_secs = file->lock_create_time - lock_wait_started;
+ break;
+ }
+
+ if (file != NULL) {
+ mail_transaction_log_file_unlock(file, t_strdup_printf(
+ "trying to lock head for %s", lock_reason));
+ }
+ if (ret < 0)
+ break;
+
+ /* try again */
+ }
+ if (lock_secs > MAIL_TRANSACTION_LOG_LOCK_WARN_SECS) {
+ i_warning("Locking transaction log file %s took %ld seconds (%s)",
+ log->head->filepath, (long)lock_secs, lock_reason);
+ }
+
+ i_assert(ret < 0 || log->head != NULL);
+ return ret;
+}
+
+int mail_transaction_log_sync_lock(struct mail_transaction_log *log,
+ const char *lock_reason,
+ uint32_t *file_seq_r, uoff_t *file_offset_r)
+{
+ const char *reason;
+
+ i_assert(!log->index->log_sync_locked);
+
+ if (!log->log_2_unlink_checked) {
+ /* we need to check once in a while if .log.2 should be deleted
+ to avoid wasting space on such old files. but we also don't
+ want to waste time on checking it when the same mailbox
+ gets opened over and over again rapidly (e.g. pop3). so
+ do this only when there have actually been some changes
+ to mailbox (i.e. when it's being locked here) */
+ log->log_2_unlink_checked = TRUE;
+ mail_transaction_log_2_unlink_old(log);
+ }
+
+ if (mail_transaction_log_lock_head(log, lock_reason) < 0)
+ return -1;
+
+ /* update sync_offset */
+ if (mail_transaction_log_file_map(log->head, log->head->sync_offset,
+ UOFF_T_MAX, &reason) <= 0) {
+ mail_index_set_error(log->index,
+ "Failed to map transaction log %s at "
+ "sync_offset=%"PRIuUOFF_T" after locking: %s",
+ log->head->filepath, log->head->sync_offset, reason);
+ mail_transaction_log_file_unlock(log->head, t_strdup_printf(
+ "%s - map failed", lock_reason));
+ return -1;
+ }
+
+ log->index->log_sync_locked = TRUE;
+ *file_seq_r = log->head->hdr.file_seq;
+ *file_offset_r = log->head->sync_offset;
+ return 0;
+}
+
+void mail_transaction_log_sync_unlock(struct mail_transaction_log *log,
+ const char *lock_reason)
+{
+ i_assert(log->index->log_sync_locked);
+
+ log->index->log_sync_locked = FALSE;
+ mail_transaction_log_file_unlock(log->head, lock_reason);
+}
+
+void mail_transaction_log_get_head(struct mail_transaction_log *log,
+ uint32_t *file_seq_r, uoff_t *file_offset_r)
+{
+ *file_seq_r = log->head->hdr.file_seq;
+ *file_offset_r = log->head->sync_offset;
+}
+
+void mail_transaction_log_get_tail(struct mail_transaction_log *log,
+ uint32_t *file_seq_r)
+{
+ struct mail_transaction_log_file *tail, *file = log->files;
+
+ for (tail = file; file->next != NULL; file = file->next) {
+ if (file->hdr.file_seq + 1 != file->next->hdr.file_seq)
+ tail = file->next;
+ }
+ *file_seq_r = tail->hdr.file_seq;
+}
+
+bool mail_transaction_log_is_head_prev(struct mail_transaction_log *log,
+ uint32_t file_seq, uoff_t file_offset)
+{
+ return log->head->hdr.prev_file_seq == file_seq &&
+ log->head->hdr.prev_file_offset == file_offset;
+}
+
+int mail_transaction_log_unlink(struct mail_transaction_log *log)
+{
+ if (unlink(log->filepath) < 0 &&
+ errno != ENOENT && errno != ESTALE) {
+ mail_index_file_set_syscall_error(log->index, log->filepath,
+ "unlink()");
+ return -1;
+ }
+ return 0;
+}
+
+void mail_transaction_log_get_dotlock_set(struct mail_transaction_log *log,
+ struct dotlock_settings *set_r)
+{
+ struct mail_index *index = log->index;
+
+ i_zero(set_r);
+ set_r->timeout = I_MIN(MAIL_TRANSACTION_LOG_LOCK_TIMEOUT,
+ index->set.max_lock_timeout_secs);
+ set_r->stale_timeout = MAIL_TRANSACTION_LOG_DOTLOCK_CHANGE_TIMEOUT;
+ set_r->nfs_flush = (index->flags & MAIL_INDEX_OPEN_FLAG_NFS_FLUSH) != 0;
+ set_r->use_excl_lock =
+ (index->flags & MAIL_INDEX_OPEN_FLAG_DOTLOCK_USE_EXCL) != 0;
+}
diff --git a/src/lib-index/mail-transaction-log.h b/src/lib-index/mail-transaction-log.h
new file mode 100644
index 0000000..c19bb20
--- /dev/null
+++ b/src/lib-index/mail-transaction-log.h
@@ -0,0 +1,494 @@
+#ifndef MAIL_TRANSACTION_LOG_H
+#define MAIL_TRANSACTION_LOG_H
+
+#include "mail-index.h"
+
+#define MAIL_TRANSACTION_LOG_SUFFIX ".log"
+
+#define MAIL_TRANSACTION_LOG_MAJOR_VERSION 1
+#define MAIL_TRANSACTION_LOG_MINOR_VERSION 3
+/* Minimum allowed mail_transaction_log_header.hdr_size. If it's smaller,
+ assume the file is corrupted. */
+#define MAIL_TRANSACTION_LOG_HEADER_MIN_SIZE 24
+
+/* Helper macro for other MAIL_TRANSACTION_LOG_VERSION_*() macros */
+#define MAIL_TRANSACTION_LOG_VERSION_FULL(major, minor) \
+ ((major) << 8 | (minor))
+/* Returns TRUE if the transaction log version supports the given feature.
+ The wanted_feature is one of the MAIL_TRANSACTION_LOG_VERSION_FEATURE_*
+ macros without the macro prefix, e.g. just COMPAT_FLAGS. */
+#define MAIL_TRANSACTION_LOG_VERSION_HAVE(version, wanted_feature) \
+ ((version) >= MAIL_TRANSACTION_LOG_VERSION_FEATURE_##wanted_feature)
+/* Returns transaction log version from the given mail_transaction_log_header
+ which is compatible for the MAIL_TRANSACTION_LOG_VERSION_HAVE() macro. */
+#define MAIL_TRANSACTION_LOG_HDR_VERSION(hdr) \
+ MAIL_TRANSACTION_LOG_VERSION_FULL((hdr)->major_version, (hdr)->minor_version)
+
+/* Log feature: mail_transaction_log_header.compat_flags is filled. */
+#define MAIL_TRANSACTION_LOG_VERSION_FEATURE_COMPAT_FLAGS \
+ MAIL_TRANSACTION_LOG_VERSION_FULL(1, 2)
+/* Log feature: Don't increase modseq when reading internal flag updates
+ (because they're not client-visible anyway).
+ See MAIL_TRANSACTION_FLAG_UPDATE_IS_INTERNAL(). */
+#define MAIL_TRANSACTION_LOG_VERSION_FEATURE_HIDE_INTERNAL_MODSEQS \
+ MAIL_TRANSACTION_LOG_VERSION_FULL(1, 3)
+
+struct mail_transaction_log_header {
+ /* Major version is increased only when you can't have backwards
+ compatibility. If the field doesn't match
+ MAIL_TRANSACTION_LOG_MAJOR_VERSION, don't even try to read it. */
+ uint8_t major_version;
+ /* Minor version is increased when the file format changes in a
+ backwards compatible way. */
+ uint8_t minor_version;
+ /* Size of the header. If it's larger than this struct, ignore any
+ unknown fields. If it's smaller, assume the rest of the fields
+ are 0. */
+ uint16_t hdr_size;
+
+ /* Unique index file ID, which must match the main index's indexid.
+ See mail_index_header.indexid. This is overwritten to be 0 if the
+ log file is marked as corrupted. */
+ uint32_t indexid;
+ /* Log file sequence number. Increased every time the log is rotated
+ and a new log is created. Using (file_seq, offset) uniquely
+ identifies a position in the transaction log. */
+ uint32_t file_seq;
+ /* The previous log file's sequence and offset when the log was
+ rotated. The offset should be the same as the previous log file's
+ size. If there was no previous log file, or if the index is being
+ reset, these are 0.
+
+ These are mainly useful to optimize syncing when the start position
+ is (prev_file_seq, prev_file_offset). Then it's it's already known
+ that the syncing can be started from this log file wihtout having
+ to open the previous log file only to realize that there is nothing
+ to sync. (Which could have also lead to an error if the .log.2 was
+ already deleted.) */
+ uint32_t prev_file_seq;
+ uint32_t prev_file_offset;
+ /* UNIX timestamp when this file was created. Used in determining when
+ to rotate the log file. */
+ uint32_t create_stamp;
+ /* Modseq value at the beginning of this file. Some transaction records
+ increase the modseq value. (Only with log format v1.1+) */
+ uint64_t initial_modseq;
+
+ /* Same as enum mail_index_header_compat_flags. Needs
+ MAIL_TRANSACTION_LOG_VERSION_FEATURE_COMPAT_FLAGS. */
+ uint8_t compat_flags;
+ /* Unused fields to make the struct 64bit aligned. These can be used
+ to add more fields to the header. */
+ uint8_t unused[3];
+ uint32_t unused2;
+};
+
+enum mail_transaction_type {
+ /* struct mail_transaction_expunge[] - Expunge the UIDs.
+ Must have MAIL_TRANSACTION_EXPUNGE_PROT ORed to this. Avoid using
+ this, use MAIL_TRANSACTION_EXPUNGE_GUID instead. */
+ MAIL_TRANSACTION_EXPUNGE = 0x00000001,
+ /* struct mail_index_record[] - Save new mails with given flags. */
+ MAIL_TRANSACTION_APPEND = 0x00000002,
+ /* struct mail_transaction_flag_update[] - Update message flags
+ (or just modseq). */
+ MAIL_TRANSACTION_FLAG_UPDATE = 0x00000004,
+ /* struct mail_transaction_header_update[] - Update the index's base
+ header (struct mail_index_header). */
+ MAIL_TRANSACTION_HEADER_UPDATE = 0x00000020,
+ /* struct mail_transaction_ext_intro - Start operations for the given
+ extension. This can be used to create a new extension or resize an
+ existing extension, but usually it is just used in front of the
+ other MAIL_TRANSACTION_EXT_* records to specify which extension
+ they're working with. */
+ MAIL_TRANSACTION_EXT_INTRO = 0x00000040,
+ /* struct mail_transaction_ext_reset - Reset the last intro extension
+ by changing its reset_id and optionally zeroing out its old data. */
+ MAIL_TRANSACTION_EXT_RESET = 0x00000080,
+ /* struct mail_transaction_ext_hdr_update[] - Update the last intro
+ extension's header. This might later become deprecated in favor of
+ supporting only MAIL_TRANSACTION_EXT_HDR_UPDATE32, but for now
+ it's still used for <64kB headers. */
+ MAIL_TRANSACTION_EXT_HDR_UPDATE = 0x00000100,
+ /* struct mail_transaction_ext_rec_update[] - Update the last intro
+ extension records for the given UIDs with given content. */
+ MAIL_TRANSACTION_EXT_REC_UPDATE = 0x00000200,
+ /* struct mail_transaction_keyword_update - Add/remove the specified
+ keyword to messages. */
+ MAIL_TRANSACTION_KEYWORD_UPDATE = 0x00000400,
+ /* struct mail_transaction_keyword_reset[] - Clear out all keywords
+ in specified messages. */
+ MAIL_TRANSACTION_KEYWORD_RESET = 0x00000800,
+ /* struct mail_transaction_ext_atomic_inc[] - Atomically increase or
+ decrease the last intro extension record. The record must be 1, 2,
+ 4 or 8 bytes. This can be used e.g. for refcount extensions. */
+ MAIL_TRANSACTION_EXT_ATOMIC_INC = 0x00001000,
+ /* struct mail_transaction_expunge_guid[] - Expunge given UID, but
+ first verify that it matches the given GUID. Must have
+ MAIL_TRANSACTION_EXPUNGE_PROT ORed to this. */
+ MAIL_TRANSACTION_EXPUNGE_GUID = 0x00002000,
+ MAIL_TRANSACTION_MODSEQ_UPDATE = 0x00008000,
+ /* struct mail_transaction_ext_hdr_update32[] - Update the last intro
+ extension's header. Used for >=64kB headers. See also
+ MAIL_TRANSACTION_EXT_HDR_UPDATE. This was added in Dovecot v2.0. */
+ MAIL_TRANSACTION_EXT_HDR_UPDATE32 = 0x00010000,
+ /* Index was marked as deleted using mail_index_set_deleted().
+ There is no record content for this. */
+ MAIL_TRANSACTION_INDEX_DELETED = 0x00020000,
+ /* Index was marked as undeleted using mail_index_set_undeleted().
+ There is no record content for this. */
+ MAIL_TRANSACTION_INDEX_UNDELETED = 0x00040000,
+ /* struct mail_transaction_boundary - Specifies a size of the following
+ records that must be treated as a single transaction. This works
+ so that the transaction log reading code stops if it finds that
+ there is a transaction whose size points outside the currently
+ existing file. An unfinished transaction is truncated away after the
+ next write to the log. FIXME: it would be better to rotate the
+ log instead of truncating it. */
+ MAIL_TRANSACTION_BOUNDARY = 0x00080000,
+ /* Mailbox attribute update. This is a bit complicated format:
+ - [+-][p-s]<name><NUL>
+ - "+" means attribute is set, "-" means unset
+ - "p" means private attribute, "s" means shared
+ - <name> is the attribute name
+ - This can repeat multiple times
+ - <NUL>
+ - 0..3 bytes padding for 32bit alignment
+ - For each attribute update an array of uint32_t integers:
+ - Update timestamp
+ - For each "+" only: Length of the attribute value.
+ */
+ MAIL_TRANSACTION_ATTRIBUTE_UPDATE = 0x00100000,
+
+ /* Mask to get the attribute type only (excluding flags). */
+ MAIL_TRANSACTION_TYPE_MASK = 0x0fffffff,
+
+#define MAIL_TRANSACTION_EXT_MASK \
+ (MAIL_TRANSACTION_EXT_INTRO | MAIL_TRANSACTION_EXT_RESET | \
+ MAIL_TRANSACTION_EXT_HDR_UPDATE | MAIL_TRANSACTION_EXT_HDR_UPDATE32 | \
+ MAIL_TRANSACTION_EXT_REC_UPDATE | MAIL_TRANSACTION_EXT_ATOMIC_INC)
+
+ /* Since we'll expunge mails based on data read from transaction log,
+ try to avoid the possibility of corrupted transaction log expunging
+ messages. This value is ORed to the actual MAIL_TRANSACTION_EXPUNGE*
+ flag. If it's not present, assume corrupted log. */
+ MAIL_TRANSACTION_EXPUNGE_PROT = 0x0000cd90,
+
+ /* External transactions have a bit different meanings depending on the
+ transaction type. Generally they mean to indicate changes that have
+ already occurred, instead of changes that are only being requested
+ to happen on next sync. For example expunges are first requested
+ to be done with internal transactions, and then there's a separate
+ external transaction to indicate that they were actually done. */
+ MAIL_TRANSACTION_EXTERNAL = 0x10000000,
+ /* This change syncs the state with another mailbox (dsync),
+ i.e. the change isn't something that a user requested locally. */
+ MAIL_TRANSACTION_SYNC = 0x20000000
+};
+
+struct mail_transaction_header {
+ /* Size of this header and the following records. This size can be
+ used to calculate how many records there are. The size is written
+ via mail_index_uint32_to_offset(). */
+ uint32_t size;
+ uint32_t type; /* enum mail_transaction_type */
+ /* Header is followed by the type-specific records. */
+};
+
+/* See MAIL_TRANSACTION_MODSEQ_UPDATE. */
+struct mail_transaction_modseq_update {
+ uint32_t uid;
+ /* don't use uint64_t here. it adds extra 32 bits of padding and also
+ causes problems with CPUs that require alignment */
+ uint32_t modseq_low32;
+ uint32_t modseq_high32;
+};
+
+/* See MAIL_TRANSACTION_EXPUNGE. */
+struct mail_transaction_expunge {
+ /* Expunge all mails between uid1..uid2. */
+ uint32_t uid1, uid2;
+};
+/* See MAIL_TRANSACTION_EXPUNGE_GUID. */
+struct mail_transaction_expunge_guid {
+ /* Expunge uid, but only if it matches guid_128. */
+ uint32_t uid;
+ /* GUID of the mail. If it's not 128 bit GUID, first pass it through
+ mail_generate_guid_128_hash() to get 128 bit SHA1 of it. */
+ guid_128_t guid_128;
+};
+
+/* See MAIL_TRANSACTION_FLAG_UPDATE. */
+struct mail_transaction_flag_update {
+ /* Change the flags for all mails between uid1..uid2. */
+ uint32_t uid1, uid2;
+ /* Add these flags to the mails. */
+ uint8_t add_flags;
+ /* Remove these flags to the mails. To replace all existing flags,
+ just set this to 0xff and specify the wanted flags in add_flags. */
+ uint8_t remove_flags;
+ /* If non-0, MAIL_INDEX_MAIL_FLAG_UPDATE_MODSEQ was used to force
+ increasing modseq update to the mails even though no flags were
+ actually changed. This differs from MAIL_TRANSACTION_MODSEQ_UPDATE
+ in that the modseq is just wanted to be increased, doesn't matter
+ to which value specifically. */
+ uint8_t modseq_inc_flag;
+ /* Unused padding */
+ uint8_t padding;
+};
+
+/* See MAIL_TRANSACTION_KEYWORD_UPDATE. */
+struct mail_transaction_keyword_update {
+ /* enum modify_type : MODIFY_ADD / MODIFY_REMOVE */
+ uint8_t modify_type;
+ uint8_t padding;
+ /* Size of name[] */
+ uint16_t name_size;
+ /* unsigned char name[name_size]; */
+ /* Update keywords for the given UIDs. The array's size is calculated
+ from mail_transaction_header.size. */
+ /* array of { uint32_t uid1, uid2; } */
+};
+
+/* See MAIL_TRANSACTION_KEYWORD_RESET. */
+struct mail_transaction_keyword_reset {
+ /* Clear out all keywords for uid1..uid2. */
+ uint32_t uid1, uid2;
+};
+
+/* See MAIL_TRANSACTION_HEADER_UPDATE. */
+struct mail_transaction_header_update {
+ /* Update start offset. */
+ uint16_t offset;
+ /* Size of the following data[] to update. */
+ uint16_t size;
+ /* unsigned char data[size]; */
+ /* 0..3 bytes of padding to get to 32bit alignment. */
+ /* unsigned char padding[]; */
+};
+
+enum {
+ /* Don't shrink hdr_size, record_size or record_align but grow them
+ if necessary. */
+ MAIL_TRANSACTION_EXT_INTRO_FLAG_NO_SHRINK = 0x01
+};
+
+/* See MAIL_TRANSACTION_EXT_INTRO. Also see struct mail_index_ext_header for
+ more explanations of these fields. */
+struct mail_transaction_ext_intro {
+ /* If extension is already known to exist in the index file,
+ set ext_id, but use empty name. If this is a new extension, set
+ name, but use ext_id=(uint32_t)-1. */
+ uint32_t ext_id;
+ uint32_t reset_id;
+ /* Size of the extension header. When growing the header size, it's
+ initially filled with zeros. The header can be written to with
+ ext-hdr-update records. */
+ uint32_t hdr_size;
+ uint16_t record_size;
+ uint16_t record_align;
+ uint16_t flags;
+ uint16_t name_size;
+ /* unsigned char name[]; */
+};
+
+/* See MAIL_TRANSACTION_EXT_RESET. */
+struct mail_transaction_ext_reset {
+ /* New value for extension's reset_id */
+ uint32_t new_reset_id;
+ /* Non-0 if the old extension header and record data should be
+ preserved. Normally all of it is zeroed out. */
+ uint8_t preserve_data;
+ uint8_t unused_padding[3];
+};
+
+/* See MAIL_TRANSACTION_EXT_HDR_UPDATE. */
+struct mail_transaction_ext_hdr_update {
+ /* Update start offset. */
+ uint16_t offset;
+ /* Size of the following data[] to update. */
+ uint16_t size;
+ /* unsigned char data[size]; */
+ /* 0..3 bytes of padding to get to 32bit alignment. */
+ /* unsigned char padding[]; */
+};
+/* See MAIL_TRANSACTION_EXT_HDR_UPDATE32. */
+struct mail_transaction_ext_hdr_update32 {
+ /* Update start offset. */
+ uint32_t offset;
+ /* Size of the following data[] to update. */
+ uint32_t size;
+ /* unsigned char data[size]; */
+ /* 0..3 bytes of padding to get to 32bit alignment. */
+ /* unsigned char padding[]; */
+};
+
+/* See MAIL_TRANSACTION_EXT_REC_UPDATE. */
+struct mail_transaction_ext_rec_update {
+ uint32_t uid;
+ /* unsigned char data[mail_transaction_ext_intro.record_size]; */
+ /* 0..3 bytes of padding to get to 32bit alignment. */
+ /* unsigned char padding[]; */
+};
+
+/* See MAIL_TRANSACTION_EXT_ATOMIC_INC. */
+struct mail_transaction_ext_atomic_inc {
+ uint32_t uid;
+ /* Add this value to the extension record data. Can be negative. */
+ int32_t diff;
+};
+
+/* See MAIL_TRANSACTION_BOUNDARY. */
+struct mail_transaction_boundary {
+ /* Size of the whole transaction, including this record and header. */
+ uint32_t size;
+};
+
+struct mail_transaction_log_append_ctx {
+ struct mail_transaction_log *log;
+ /* All the changes that will be written to the transaction log. */
+ buffer_t *output;
+
+ /* Transaction flags as given to mail_transaction_log_append_begin(). */
+ enum mail_transaction_type trans_flags;
+
+ /* Tracking the current highest_modseq after the changes. This will
+ be used to update mail_transaction_log_file.sync_highest_modseq. */
+ uint64_t new_highest_modseq;
+ /* Number of transaction records added so far. */
+ unsigned int transaction_count;
+
+ /* Copied from mail_index_transaction.sync_transaction */
+ bool index_sync_transaction:1;
+ /* Copied from mail_index_transaction.tail_offset_changed */
+ bool tail_offset_changed:1;
+ /* TRUE if the mail_transaction_log_file has been synced up to the
+ current write offset, and we're writing a syncing transaction
+ (index_sync_transaction=TRUE). This means that the just written
+ transaction can be assumed to be synced already. */
+ bool sync_includes_this:1;
+ /* fdatasync() after writing the transaction. */
+ bool want_fsync:1;
+};
+
+#define LOG_IS_BEFORE(seq1, offset1, seq2, offset2) \
+ (((offset1) < (offset2) && (seq1) == (seq2)) || (seq1) < (seq2))
+
+struct mail_transaction_log *
+mail_transaction_log_alloc(struct mail_index *index);
+void mail_transaction_log_free(struct mail_transaction_log **log);
+
+/* Open the transaction log. Returns 1 if ok, 0 if file doesn't exist or it's
+ is corrupted, -1 if there was some I/O error. */
+int mail_transaction_log_open(struct mail_transaction_log *log);
+/* Create, or recreate, the transaction log. Returns 0 if ok, -1 if error. */
+int mail_transaction_log_create(struct mail_transaction_log *log, bool reset);
+/* Close all the open transactions log files. */
+void mail_transaction_log_close(struct mail_transaction_log *log);
+
+/* Notify of indexid change */
+void mail_transaction_log_indexid_changed(struct mail_transaction_log *log);
+
+/* Returns the file seq/offset where the mailbox is currently synced at.
+ Since the log is rotated only when mailbox is fully synced, the sequence
+ points always to the latest file. This function doesn't actually find the
+ latest sync position, so you'll need to use eg. log_view_set() before
+ calling this. */
+void mail_transaction_log_get_mailbox_sync_pos(struct mail_transaction_log *log,
+ uint32_t *file_seq_r,
+ uoff_t *file_offset_r);
+/* Set the current mailbox sync position. file_seq must always be the latest
+ log file's sequence. The offset written automatically to the log when
+ other transactions are being written. */
+void mail_transaction_log_set_mailbox_sync_pos(struct mail_transaction_log *log,
+ uint32_t file_seq,
+ uoff_t file_offset);
+
+struct mail_transaction_log_view *
+mail_transaction_log_view_open(struct mail_transaction_log *log);
+void mail_transaction_log_view_close(struct mail_transaction_log_view **view);
+
+/* Set view boundaries. Returns 1 if ok, 0 if files are lost, corrupted or the
+ offsets are broken, -1 if I/O error. reset_r=TRUE if the whole index should
+ be reset before applying any changes. */
+int mail_transaction_log_view_set(struct mail_transaction_log_view *view,
+ uint32_t min_file_seq, uoff_t min_file_offset,
+ uint32_t max_file_seq, uoff_t max_file_offset,
+ bool *reset_r, const char **reason_r);
+/* Scan through all of the log files that we can find.
+ Returns -1 if error, 0 if ok. */
+int mail_transaction_log_view_set_all(struct mail_transaction_log_view *view);
+/* Clear the view. If oldest_file_seq > 0, keep it and newer log files
+ referenced so we don't get desynced. */
+void mail_transaction_log_view_clear(struct mail_transaction_log_view *view,
+ uint32_t oldest_file_seq);
+
+/* Read next transaction record from current position. The position is updated.
+ Returns -1 if error, 0 if we're at end of the view, 1 if ok. */
+int mail_transaction_log_view_next(struct mail_transaction_log_view *view,
+ const struct mail_transaction_header **hdr_r,
+ const void **data_r);
+/* Mark the current view's position to the record returned previously with
+ _log_view_next(). */
+void mail_transaction_log_view_mark(struct mail_transaction_log_view *view);
+/* Seek to previously marked position. */
+void mail_transaction_log_view_rewind(struct mail_transaction_log_view *view);
+
+/* Returns the position of the record returned previously with
+ mail_transaction_log_view_next() */
+void
+mail_transaction_log_view_get_prev_pos(struct mail_transaction_log_view *view,
+ uint32_t *file_seq_r,
+ uoff_t *file_offset_r);
+/* Return the modseq of the change returned previously with _view_next(). */
+uint64_t
+mail_transaction_log_view_get_prev_modseq(struct mail_transaction_log_view *view);
+/* Returns TRUE if we're at the end of the view window. */
+bool mail_transaction_log_view_is_last(struct mail_transaction_log_view *view);
+
+/* Marks the log file in current position to be corrupted. */
+void
+mail_transaction_log_view_set_corrupted(struct mail_transaction_log_view *view,
+ const char *fmt, ...)
+ ATTR_FORMAT(2, 3) ATTR_COLD;
+bool
+mail_transaction_log_view_is_corrupted(struct mail_transaction_log_view *view);
+
+int mail_transaction_log_append_begin(struct mail_index *index,
+ enum mail_transaction_type flags,
+ struct mail_transaction_log_append_ctx **ctx_r);
+void mail_transaction_log_append_add(struct mail_transaction_log_append_ctx *ctx,
+ enum mail_transaction_type type,
+ const void *data, size_t size);
+int mail_transaction_log_append_commit(struct mail_transaction_log_append_ctx **ctx);
+
+/* Lock transaction log for index synchronization. This is used as the main
+ exclusive lock for index changes. The index/log can still be read since they
+ don't use locking, but the log can't be written to while it's locked.
+ Returns 0 on success, -1 if locking failed for any reason.
+
+ After successfully locking the transaction log, the log file is also fully
+ mapped into memory and its sync_offset updated. The locked file's sequence
+ and sync_offset are returned. */
+int mail_transaction_log_sync_lock(struct mail_transaction_log *log,
+ const char *lock_reason,
+ uint32_t *file_seq_r, uoff_t *file_offset_r);
+void mail_transaction_log_sync_unlock(struct mail_transaction_log *log,
+ const char *lock_reason);
+/* Returns the current head. Works only when log is locked. */
+void mail_transaction_log_get_head(struct mail_transaction_log *log,
+ uint32_t *file_seq_r, uoff_t *file_offset_r);
+/* Returns the current tail from which all files are open to head. */
+void mail_transaction_log_get_tail(struct mail_transaction_log *log,
+ uint32_t *file_seq_r);
+/* Returns TRUE if given seq/offset is current head log's rotate point. */
+bool mail_transaction_log_is_head_prev(struct mail_transaction_log *log,
+ uint32_t file_seq, uoff_t file_offset);
+
+/* Move currently opened log head file to memory (called by
+ mail_index_move_to_memory()) */
+int mail_transaction_log_move_to_memory(struct mail_transaction_log *log);
+/* Unlink transaction log files */
+int mail_transaction_log_unlink(struct mail_transaction_log *log);
+
+#endif
diff --git a/src/lib-index/mailbox-log.c b/src/lib-index/mailbox-log.c
new file mode 100644
index 0000000..433ba29
--- /dev/null
+++ b/src/lib-index/mailbox-log.c
@@ -0,0 +1,292 @@
+/* Copyright (c) 2009-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "ioloop.h"
+#include "eacces-error.h"
+#include "mailbox-log.h"
+
+#include <stdio.h>
+#include <unistd.h>
+#include <fcntl.h>
+#include <sys/stat.h>
+
+/* How often to reopen the log file to make sure that the changes are written
+ to the latest file. The main problem here is if the value is too high the
+ changes could be written to a file that was already rotated and deleted.
+ That wouldn't happen in any real world situations though, since the file
+ rotation time is probably measured in months or years. Still, each session
+ rarely writes anything here, so the value can just as well be a pretty small
+ one without any performance problems. */
+#define MAILBOX_LOG_REOPEN_SECS (60)
+#define MAILBOX_LOG_ROTATE_SIZE (1024*4)
+
+struct mailbox_log {
+ char *filepath, *filepath2;
+ int fd;
+ struct event *event;
+ time_t open_timestamp;
+
+ mode_t mode;
+ gid_t gid;
+ char *gid_origin;
+};
+
+struct mailbox_log_iter {
+ struct mailbox_log *log;
+
+ int fd;
+ const char *filepath;
+
+ struct mailbox_log_record buf[128];
+ unsigned int idx, count;
+ uoff_t offset;
+ bool failed;
+};
+
+static void mailbox_log_close(struct mailbox_log *log);
+
+struct mailbox_log *
+mailbox_log_alloc(struct event *parent_event, const char *path)
+{
+ struct mailbox_log *log;
+
+ log = i_new(struct mailbox_log, 1);
+ log->event = event_create(parent_event);
+ log->filepath = i_strdup(path);
+ log->filepath2 = i_strconcat(path, ".2", NULL);
+ log->mode = 0644;
+ log->gid = (gid_t)-1;
+ log->fd = -1;
+ return log;
+}
+
+void mailbox_log_free(struct mailbox_log **_log)
+{
+ struct mailbox_log *log = *_log;
+
+ *_log = NULL;
+
+ mailbox_log_close(log);
+ event_unref(&log->event);
+ i_free(log->gid_origin);
+ i_free(log->filepath);
+ i_free(log->filepath2);
+ i_free(log);
+}
+
+static void mailbox_log_close(struct mailbox_log *log)
+{
+ i_close_fd_path(&log->fd, log->filepath);
+}
+
+void mailbox_log_set_permissions(struct mailbox_log *log, mode_t mode,
+ gid_t gid, const char *gid_origin)
+{
+ log->mode = mode;
+ log->gid = gid;
+ i_free(log->gid_origin);
+ log->gid_origin = i_strdup(gid_origin);
+}
+
+static int mailbox_log_open(struct mailbox_log *log)
+{
+ mode_t old_mode;
+
+ i_assert(log->fd == -1);
+
+ log->open_timestamp = ioloop_time;
+ log->fd = open(log->filepath, O_RDWR | O_APPEND);
+ if (log->fd != -1)
+ return 0;
+
+ /* try to create it */
+ old_mode = umask(0666 ^ log->mode);
+ log->fd = open(log->filepath, O_RDWR | O_APPEND | O_CREAT, 0666);
+ umask(old_mode);
+
+ if (log->fd == -1) {
+ if (errno != EACCES)
+ e_error(log->event, "creat(%s) failed: %m",
+ log->filepath);
+ else
+ e_error(log->event, "%s",
+ eacces_error_get("creat", log->filepath));
+ return -1;
+ }
+ if (fchown(log->fd, (uid_t)-1, log->gid) < 0) {
+ if (errno != EPERM)
+ e_error(log->event, "fchown(%s) failed: %m",
+ log->filepath);
+ else {
+ e_error(log->event, "%s",
+ eperm_error_get_chgrp("fchown",
+ log->filepath, log->gid,
+ log->gid_origin));
+ }
+ }
+ return 0;
+}
+
+static int mailbox_log_rotate_if_needed(struct mailbox_log *log)
+{
+ struct stat st;
+
+ if (fstat(log->fd, &st) < 0) {
+ e_error(log->event, "fstat(%s) failed: %m", log->filepath);
+ return -1;
+ }
+
+ if (st.st_size < MAILBOX_LOG_ROTATE_SIZE)
+ return 0;
+
+ if (rename(log->filepath, log->filepath2) < 0 && errno != ENOENT) {
+ e_error(log->event, "rename(%s, %s) failed: %m",
+ log->filepath, log->filepath2);
+ return -1;
+ }
+ return 0;
+}
+
+void mailbox_log_record_set_timestamp(struct mailbox_log_record *rec,
+ time_t stamp)
+{
+ cpu32_to_be_unaligned(stamp, rec->timestamp);
+}
+
+time_t mailbox_log_record_get_timestamp(const struct mailbox_log_record *rec)
+{
+ return (time_t) be32_to_cpu_unaligned(rec->timestamp);
+}
+
+int mailbox_log_append(struct mailbox_log *log,
+ const struct mailbox_log_record *rec)
+{
+ struct stat st;
+ ssize_t ret;
+
+ /* we don't have to be too strict about appending to the latest log
+ file. the records' ordering doesn't matter and iteration goes
+ through both logs anyway. still, if there's a long running session
+ it shouldn't keep writing to a rotated log forever. */
+ if (log->open_timestamp/MAILBOX_LOG_REOPEN_SECS !=
+ ioloop_time/MAILBOX_LOG_REOPEN_SECS)
+ mailbox_log_close(log);
+ if (log->fd == -1) {
+ if (mailbox_log_open(log) < 0)
+ return -1;
+ i_assert(log->fd != -1);
+ }
+
+ /* We don't bother with locking, atomic appends will protect us.
+ If they don't (NFS), the worst that can happen is that a few
+ records get overwritten (because they're all the same size).
+ This whole log isn't supposed to be super-reliable anyway. */
+ ret = write(log->fd, rec, sizeof(*rec));
+ if (ret < 0) {
+ e_error(log->event, "write(%s) failed: %m", log->filepath);
+ return -1;
+ } else if (ret != sizeof(*rec)) {
+ e_error(log->event, "write(%s) wrote %d/%u bytes", log->filepath,
+ (int)ret, (unsigned int)sizeof(*rec));
+ if (fstat(log->fd, &st) == 0) {
+ if (ftruncate(log->fd, st.st_size - ret) < 0) {
+ e_error(log->event, "ftruncate(%s) failed: %m",
+ log->filepath);
+ }
+ }
+ return -1;
+ }
+
+ (void)mailbox_log_rotate_if_needed(log);
+ return 0;
+}
+
+static bool mailbox_log_iter_open_next(struct mailbox_log_iter *iter)
+{
+ i_close_fd_path(&iter->fd, iter->filepath);
+ if (iter->filepath == NULL)
+ iter->filepath = iter->log->filepath2;
+ else if (iter->filepath == iter->log->filepath2)
+ iter->filepath = iter->log->filepath;
+ else
+ return FALSE;
+
+ iter->fd = open(iter->filepath, O_RDONLY | O_APPEND);
+ if (iter->fd != -1)
+ return TRUE;
+ else if (errno == ENOENT) {
+ if (iter->filepath == iter->log->filepath2)
+ return mailbox_log_iter_open_next(iter);
+ } else {
+ e_error(iter->log->event, "open(%s) failed: %m", iter->filepath);
+ iter->failed = TRUE;
+ }
+ return FALSE;
+}
+
+struct mailbox_log_iter *mailbox_log_iter_init(struct mailbox_log *log)
+{
+ struct mailbox_log_iter *iter;
+
+ iter = i_new(struct mailbox_log_iter, 1);
+ iter->log = log;
+ iter->fd = -1;
+ (void)mailbox_log_iter_open_next(iter);
+ return iter;
+}
+
+const struct mailbox_log_record *
+mailbox_log_iter_next(struct mailbox_log_iter *iter)
+{
+ const struct mailbox_log_record *rec;
+ uoff_t offset;
+ ssize_t ret;
+
+ if (iter->idx == iter->count) {
+ if (iter->fd == -1)
+ return NULL;
+
+ ret = pread(iter->fd, iter->buf, sizeof(iter->buf),
+ iter->offset);
+ if (ret < 0) {
+ e_error(iter->log->event, "pread(%s) failed: %m",
+ iter->filepath);
+ iter->failed = TRUE;
+ return NULL;
+ }
+ if (ret == 0) {
+ if (!mailbox_log_iter_open_next(iter))
+ return NULL;
+ iter->idx = iter->count = 0;
+ iter->offset = 0;
+ return mailbox_log_iter_next(iter);
+ }
+ iter->idx = 0;
+ iter->count = ret / sizeof(iter->buf[0]);
+ iter->offset += iter->count * sizeof(iter->buf[0]);
+ }
+ rec = &iter->buf[iter->idx++];
+ if (rec->type < MAILBOX_LOG_RECORD_DELETE_MAILBOX ||
+ rec->type > MAILBOX_LOG_RECORD_UNSUBSCRIBE) {
+ offset = iter->offset -
+ (iter->count - iter->idx) * sizeof(iter->buf[0]);
+ e_error(iter->log->event,
+ "Corrupted mailbox log %s at offset %"PRIuUOFF_T": "
+ "type=%d", iter->filepath, offset, rec->type);
+ i_unlink(iter->filepath);
+ return NULL;
+ }
+ return rec;
+}
+
+int mailbox_log_iter_deinit(struct mailbox_log_iter **_iter)
+{
+ struct mailbox_log_iter *iter = *_iter;
+ int ret = iter->failed ? -1 : 0;
+
+ *_iter = NULL;
+
+ i_close_fd_path(&iter->fd, iter->filepath);
+ i_free(iter);
+ return ret;
+}
diff --git a/src/lib-index/mailbox-log.h b/src/lib-index/mailbox-log.h
new file mode 100644
index 0000000..ef19fc2
--- /dev/null
+++ b/src/lib-index/mailbox-log.h
@@ -0,0 +1,44 @@
+#ifndef MAILBOX_LOG_H
+#define MAILBOX_LOG_H
+
+#include "guid.h"
+
+enum mailbox_log_record_type {
+ MAILBOX_LOG_RECORD_DELETE_MAILBOX = 1,
+ MAILBOX_LOG_RECORD_DELETE_DIR,
+ MAILBOX_LOG_RECORD_RENAME,
+ MAILBOX_LOG_RECORD_SUBSCRIBE,
+ MAILBOX_LOG_RECORD_UNSUBSCRIBE,
+ MAILBOX_LOG_RECORD_CREATE_DIR
+};
+
+struct mailbox_log_record {
+ uint8_t type;
+ uint8_t padding[3];
+ guid_128_t mailbox_guid;
+ uint8_t timestamp[4];
+};
+
+struct mailbox_log *
+mailbox_log_alloc(struct event *parent_event, const char *path);
+void mailbox_log_free(struct mailbox_log **log);
+
+void mailbox_log_set_permissions(struct mailbox_log *log, mode_t mode,
+ gid_t gid, const char *gid_origin);
+
+void mailbox_log_record_set_timestamp(struct mailbox_log_record *rec,
+ time_t stamp);
+time_t mailbox_log_record_get_timestamp(const struct mailbox_log_record *rec);
+
+/* Append a new record to mailbox log. Returns 0 if ok, -1 if error. */
+int mailbox_log_append(struct mailbox_log *log,
+ const struct mailbox_log_record *rec);
+
+/* Iterate through all records in mailbox log. */
+struct mailbox_log_iter *mailbox_log_iter_init(struct mailbox_log *log);
+const struct mailbox_log_record *
+mailbox_log_iter_next(struct mailbox_log_iter *iter);
+/* Returns 0 if ok, -1 if I/O error. */
+int mailbox_log_iter_deinit(struct mailbox_log_iter **iter);
+
+#endif
diff --git a/src/lib-index/test-mail-cache-common.c b/src/lib-index/test-mail-cache-common.c
new file mode 100644
index 0000000..ee34c04
--- /dev/null
+++ b/src/lib-index/test-mail-cache-common.c
@@ -0,0 +1,166 @@
+/* Copyright (c) 2020 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "test-common.h"
+#include "test-mail-cache.h"
+
+static const struct mail_cache_field cache_field_foo = {
+ .name = "foo",
+ .type = MAIL_CACHE_FIELD_STRING,
+ .decision = MAIL_CACHE_DECISION_YES,
+};
+static const struct mail_cache_field cache_field_bar = {
+ .name = "bar",
+ .type = MAIL_CACHE_FIELD_STRING,
+ .decision = MAIL_CACHE_DECISION_YES,
+};
+static const struct mail_cache_field cache_field_baz = {
+ .name = "baz",
+ .type = MAIL_CACHE_FIELD_STRING,
+ .decision = MAIL_CACHE_DECISION_YES,
+};
+
+void test_mail_cache_init(struct mail_index *index,
+ struct test_mail_cache_ctx *ctx_r)
+{
+ i_zero(ctx_r);
+ ctx_r->index = index;
+ ctx_r->cache = index->cache;
+ ctx_r->view = mail_index_view_open(index);
+
+ ctx_r->cache_field = cache_field_foo;
+ ctx_r->cache_field2 = cache_field_bar;
+ ctx_r->cache_field3 = cache_field_baz;
+ /* Try to use different file_field_maps for different index instances
+ by randomizing the registration order. This only works for the 2nd
+ index that is opened, because the initial cache is always created
+ with all cache fields in the same order. */
+ if (i_rand_limit(2) == 0) {
+ mail_cache_register_fields(ctx_r->cache, &ctx_r->cache_field, 1);
+ mail_cache_register_fields(ctx_r->cache, &ctx_r->cache_field2, 1);
+ mail_cache_register_fields(ctx_r->cache, &ctx_r->cache_field3, 1);
+ } else {
+ mail_cache_register_fields(ctx_r->cache, &ctx_r->cache_field3, 1);
+ mail_cache_register_fields(ctx_r->cache, &ctx_r->cache_field2, 1);
+ mail_cache_register_fields(ctx_r->cache, &ctx_r->cache_field, 1);
+ }
+}
+
+void test_mail_cache_deinit(struct test_mail_cache_ctx *ctx)
+{
+ if (ctx->view != NULL)
+ mail_index_view_close(&ctx->view);
+ test_mail_index_close(&ctx->index);
+}
+
+unsigned int test_mail_cache_get_purge_count(struct test_mail_cache_ctx *ctx)
+{
+ const struct mail_cache_header *hdr = ctx->cache->hdr;
+
+ return hdr->file_seq - hdr->indexid;
+}
+
+void test_mail_cache_index_sync(struct test_mail_cache_ctx *ctx)
+{
+ struct mail_index_sync_ctx *sync_ctx;
+ struct mail_index_view *view;
+ struct mail_index_transaction *trans;
+ struct mail_index_sync_rec sync_rec;
+
+ test_assert(mail_index_sync_begin(ctx->index, &sync_ctx,
+ &view, &trans, 0) == 1);
+ while (mail_index_sync_next(sync_ctx, &sync_rec)) {
+ if (sync_rec.type == MAIL_INDEX_SYNC_TYPE_EXPUNGE) {
+ /* we're a bit kludgily assuming that there's only
+ one UID and also that uid==seq */
+ mail_index_expunge(trans, sync_rec.uid1);
+ }
+ }
+ test_assert(mail_index_sync_commit(&sync_ctx) == 0);
+}
+
+void test_mail_cache_view_sync(struct test_mail_cache_ctx *ctx)
+{
+ struct mail_index_view_sync_ctx *sync_ctx;
+ struct mail_index_view_sync_rec sync_rec;
+ bool delayed_expunges;
+
+ sync_ctx = mail_index_view_sync_begin(ctx->view, MAIL_INDEX_VIEW_SYNC_FLAG_FIX_INCONSISTENT);
+ while (mail_index_view_sync_next(sync_ctx, &sync_rec)) ;
+ test_assert(mail_index_view_sync_commit(&sync_ctx, &delayed_expunges) == 0);
+}
+
+void test_mail_cache_purge(void)
+{
+ struct test_mail_cache_ctx ctx;
+
+ test_mail_cache_init(test_mail_index_open(), &ctx);
+ test_assert(mail_cache_purge(ctx.cache, (uint32_t)-1, "test") == 0);
+ test_mail_cache_deinit(&ctx);
+}
+
+void test_mail_cache_add_mail(struct test_mail_cache_ctx *ctx,
+ unsigned int cache_field_idx,
+ const char *cache_data)
+{
+ const struct mail_index_header *hdr = mail_index_get_header(ctx->view);
+ struct mail_index_transaction *trans;
+ struct mail_index_view *updated_view;
+ struct mail_cache_view *cache_view;
+ struct mail_cache_transaction_ctx *cache_trans;
+ uint32_t seq, uid_validity = 12345;
+
+ trans = mail_index_transaction_begin(ctx->view, 0);
+ updated_view = mail_index_transaction_open_updated_view(trans);
+ cache_view = mail_cache_view_open(ctx->cache, updated_view);
+ cache_trans = mail_cache_get_transaction(cache_view, trans);
+
+ if (hdr->uid_validity == 0) {
+ mail_index_update_header(trans,
+ offsetof(struct mail_index_header, uid_validity),
+ &uid_validity, sizeof(uid_validity), TRUE);
+ }
+
+ mail_index_append(trans, hdr->next_uid, &seq);
+ if (cache_field_idx != UINT_MAX) {
+ mail_cache_add(cache_trans, seq, cache_field_idx,
+ cache_data, strlen(cache_data));
+ }
+ test_assert(mail_index_transaction_commit(&trans) == 0);
+ mail_index_view_close(&updated_view);
+ mail_cache_view_close(&cache_view);
+
+ /* View needs to have the latest changes before purge transaction
+ is created. */
+ test_mail_cache_view_sync(ctx);
+}
+
+void test_mail_cache_add_field(struct test_mail_cache_ctx *ctx, uint32_t seq,
+ unsigned int cache_field_idx,
+ const char *cache_data)
+{
+ struct mail_index_transaction *trans;
+ struct mail_cache_view *cache_view;
+ struct mail_cache_transaction_ctx *cache_trans;
+
+ cache_view = mail_cache_view_open(ctx->cache, ctx->view);
+ trans = mail_index_transaction_begin(ctx->view, 0);
+ cache_trans = mail_cache_get_transaction(cache_view, trans);
+ mail_cache_add(cache_trans, seq, cache_field_idx,
+ cache_data, strlen(cache_data));
+ test_assert(mail_index_transaction_commit(&trans) == 0);
+ mail_cache_view_close(&cache_view);
+}
+
+void test_mail_cache_update_day_first_uid7(struct test_mail_cache_ctx *ctx,
+ uint32_t first_new_uid)
+{
+ struct mail_index_transaction *trans;
+
+ trans = mail_index_transaction_begin(ctx->view, 0);
+ mail_index_update_header(trans,
+ offsetof(struct mail_index_header, day_first_uid[7]),
+ &first_new_uid, sizeof(first_new_uid), FALSE);
+ test_assert(mail_index_transaction_commit(&trans) == 0);
+ test_mail_cache_view_sync(ctx);
+}
diff --git a/src/lib-index/test-mail-cache-fields.c b/src/lib-index/test-mail-cache-fields.c
new file mode 100644
index 0000000..18b6a33
--- /dev/null
+++ b/src/lib-index/test-mail-cache-fields.c
@@ -0,0 +1,112 @@
+/* Copyright (c) 2021 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "test-common.h"
+#include "test-mail-cache.h"
+
+static void test_mail_cache_fields_read_write(void)
+{
+ struct mail_cache_field cache_field = {
+ .name = "testfield",
+ .type = MAIL_CACHE_FIELD_STRING,
+ .decision = MAIL_CACHE_DECISION_NO,
+ .last_used = 0x12345678,
+ };
+ struct mail_cache_field cache_field2 = {
+ .name = "testfield2",
+ .type = MAIL_CACHE_FIELD_STRING,
+ .decision = MAIL_CACHE_DECISION_NO,
+ .last_used = 0xaabbccdd,
+ };
+ struct test_mail_cache_ctx ctx;
+
+ test_begin("mail cache fields read-write");
+
+ test_mail_cache_init(test_mail_index_init(), &ctx);
+ mail_cache_register_fields(ctx.cache, &cache_field, 1);
+ test_assert(mail_cache_purge(ctx.cache, (uint32_t)-1, "test") == 0);
+ /* after writing the initial cache file, register another cache field
+ that doesn't exist in it. */
+ mail_cache_register_fields(ctx.cache, &cache_field2, 1);
+
+ struct mail_cache_field_private *priv =
+ &ctx.cache->fields[cache_field.idx];
+ struct mail_cache_field_private *priv2 =
+ &ctx.cache->fields[cache_field2.idx];
+
+ /* No changes */
+ test_assert(mail_cache_header_fields_update(ctx.cache) == 0);
+ test_assert(mail_cache_header_fields_read(ctx.cache) == 0);
+ test_assert(cache_field.last_used == priv->field.last_used &&
+ cache_field.decision == priv->field.decision);
+ test_assert(cache_field2.last_used == priv2->field.last_used &&
+ cache_field2.decision == priv2->field.decision);
+
+ /* Replace decision without marking it dirty. Make sure reading
+ overwrites it. Also make sure an old last_used is overwritten. */
+ priv->field.decision = MAIL_CACHE_DECISION_YES;
+ priv->field.last_used = cache_field.last_used - 1;
+ test_assert(mail_cache_header_fields_read(ctx.cache) == 0);
+ test_assert(cache_field.last_used == priv->field.last_used &&
+ cache_field.decision == priv->field.decision);
+ test_assert(cache_field2.last_used == priv2->field.last_used &&
+ cache_field2.decision == priv2->field.decision);
+
+ /* Replace decision and set it dirty. Make sure reading doesn't
+ overwrite it. Also make sure an old last_used is overwritten. */
+ priv->decision_dirty = TRUE;
+ priv2->decision_dirty = TRUE;
+ priv->field.last_used = cache_field.last_used - 1;
+ priv->field.decision = MAIL_CACHE_DECISION_YES;
+ cache_field.decision = MAIL_CACHE_DECISION_YES;
+ priv2->field.decision = MAIL_CACHE_DECISION_YES;
+ cache_field2.decision = MAIL_CACHE_DECISION_YES;
+ test_assert(mail_cache_header_fields_read(ctx.cache) == 0);
+ test_assert(cache_field.last_used == priv->field.last_used &&
+ cache_field.decision == priv->field.decision);
+ test_assert(cache_field2.last_used == priv2->field.last_used &&
+ cache_field2.decision == priv2->field.decision);
+ test_assert(priv->decision_dirty);
+ test_assert(priv2->decision_dirty);
+
+ /* Make sure a new last_used won't get overwritten by read. */
+ priv->field.last_used = ++cache_field.last_used;
+ priv2->field.last_used = ++cache_field2.last_used;
+ test_assert(mail_cache_header_fields_read(ctx.cache) == 0);
+ test_assert(cache_field.last_used == priv->field.last_used &&
+ cache_field.decision == priv->field.decision);
+ test_assert(cache_field2.last_used == priv2->field.last_used &&
+ cache_field2.decision == priv2->field.decision);
+
+ /* Write the new decision and last_used. Note that cache_field2
+ isn't written, because it doesn't exist in the cache file. */
+ test_assert(mail_cache_header_fields_update(ctx.cache) == 0);
+ test_assert(!priv->decision_dirty);
+ test_assert(priv2->decision_dirty);
+ /* make sure reading reads them back, even if they're changed */
+ priv->field.decision = MAIL_CACHE_DECISION_NO;
+ priv->field.last_used = 1;
+ priv2->field.decision = MAIL_CACHE_DECISION_TEMP;
+ priv2->field.last_used = 2;
+ cache_field2.decision = MAIL_CACHE_DECISION_TEMP;
+ cache_field2.last_used = 2;
+ test_assert(mail_cache_header_fields_read(ctx.cache) == 0);
+ test_assert(cache_field.last_used == priv->field.last_used &&
+ cache_field.decision == priv->field.decision);
+ test_assert(cache_field2.last_used == priv2->field.last_used &&
+ cache_field2.decision == priv2->field.decision);
+
+ test_mail_cache_deinit(&ctx);
+ test_mail_index_delete();
+
+ test_end();
+}
+
+int main(void)
+{
+ static void (*const test_functions[])(void) = {
+ test_mail_cache_fields_read_write,
+ NULL
+ };
+ return test_run(test_functions);
+}
diff --git a/src/lib-index/test-mail-cache-purge.c b/src/lib-index/test-mail-cache-purge.c
new file mode 100644
index 0000000..525754b
--- /dev/null
+++ b/src/lib-index/test-mail-cache-purge.c
@@ -0,0 +1,1076 @@
+/* Copyright (c) 2020 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "str.h"
+#include "array.h"
+#include "test-common.h"
+#include "test-mail-cache.h"
+
+#include <stdio.h>
+#include <sys/wait.h>
+
+static void test_mail_cache_read_during_purge2(void)
+{
+ struct test_mail_cache_ctx ctx;
+ struct mail_cache_view *cache_view;
+ string_t *str = t_str_new(16);
+
+ i_set_failure_prefix("index2: ");
+
+ /* read from cache via 2nd index */
+ test_mail_cache_init(test_mail_index_open(), &ctx);
+
+ cache_view = mail_cache_view_open(ctx.cache, ctx.view);
+ test_assert(mail_cache_lookup_field(cache_view, str, 1,
+ ctx.cache_field.idx) == 1);
+ test_assert(strcmp(str_c(str), "foo1") == 0);
+ mail_cache_view_close(&cache_view);
+
+ test_mail_cache_deinit(&ctx);
+}
+
+static void test_mail_cache_read_during_purge(void)
+{
+ struct test_mail_cache_ctx ctx;
+ struct mail_index_transaction *trans;
+ int status;
+
+ test_begin("mail cache read during purge");
+ test_mail_cache_init(test_mail_index_init(), &ctx);
+ test_mail_cache_add_mail(&ctx, ctx.cache_field.idx, "foo1");
+
+ /* lock the index for cache purge */
+ uint32_t log_seq;
+ uoff_t log_offset;
+ test_assert(mail_transaction_log_sync_lock(ctx.index->log, "purge", &log_seq, &log_offset) == 0);
+
+ /* start purging cache using the 1st index, but don't commit yet */
+ trans = mail_index_transaction_begin(ctx.view, 0);
+ test_assert(mail_cache_purge_with_trans(ctx.cache, trans, (uint32_t)-1, "test") == 0);
+
+ switch (fork()) {
+ case (pid_t)-1:
+ i_fatal("fork() failed: %m");
+ case 0:
+ test_mail_cache_read_during_purge2();
+ /* cleanup so valgrind doesn't complain about memory leaks */
+ mail_index_transaction_rollback(&trans);
+ mail_transaction_log_sync_unlock(ctx.index->log, "purge");
+ test_mail_cache_deinit(&ctx);
+ test_exit(test_has_failed() ? 10 : 0);
+ default:
+ break;
+ }
+
+ /* Wait a bit to make sure the child function has had a chance to run.
+ It's supposed to be waiting on the locked .log file. */
+ usleep(100000);
+ /* finish cache purging */
+ test_assert(mail_index_transaction_commit(&trans) == 0);
+ mail_transaction_log_sync_unlock(ctx.index->log, "purge");
+ mail_index_view_close(&ctx.view);
+
+ /* wait for child to finish execution */
+ if (wait(&status) == -1)
+ i_error("wait() failed: %m");
+ test_assert(status == 0);
+
+ test_assert(test_mail_cache_get_purge_count(&ctx) == 1);
+ test_mail_cache_deinit(&ctx);
+ test_mail_index_delete();
+ test_end();
+}
+
+static void test_mail_cache_write_during_purge2(void)
+{
+ struct test_mail_cache_ctx ctx;
+
+ i_set_failure_prefix("index2: ");
+
+ /* add to cache via 2nd index */
+ test_mail_cache_init(test_mail_index_open(), &ctx);
+ test_mail_cache_add_field(&ctx, 1, ctx.cache_field2.idx, "bar2");
+ test_mail_cache_deinit(&ctx);
+}
+
+static void test_mail_cache_write_during_purge(void)
+{
+ struct test_mail_cache_ctx ctx;
+ struct mail_index_view *view;
+ struct mail_cache_view *cache_view;
+ struct mail_index_transaction *trans;
+ string_t *str = t_str_new(16);
+ int status;
+
+ test_begin("mail cache write during purge");
+ test_mail_cache_init(test_mail_index_init(), &ctx);
+ test_mail_cache_add_mail(&ctx, ctx.cache_field.idx, "foo1");
+
+ /* lock the index for cache purge */
+ uint32_t log_seq;
+ uoff_t log_offset;
+ test_assert(mail_transaction_log_sync_lock(ctx.index->log, "purge", &log_seq, &log_offset) == 0);
+
+ /* start purging cache using the 1st index, but don't commit yet */
+ trans = mail_index_transaction_begin(ctx.view, 0);
+ test_assert(mail_cache_purge_with_trans(ctx.cache, trans, (uint32_t)-1, "test") == 0);
+
+ switch (fork()) {
+ case (pid_t)-1:
+ i_fatal("fork() failed: %m");
+ case 0:
+ test_mail_cache_write_during_purge2();
+ /* cleanup so valgrind doesn't complain about memory leaks */
+ mail_index_transaction_rollback(&trans);
+ mail_transaction_log_sync_unlock(ctx.index->log, "purge");
+ test_mail_cache_deinit(&ctx);
+ test_exit(test_has_failed() ? 10 : 0);
+ default:
+ break;
+ }
+
+ /* Wait a bit to make sure the child function has had a chance to run.
+ It's supposed to be waiting on the locked .log file. */
+ usleep(100000);
+ /* finish cache purge */
+ test_assert(mail_index_transaction_commit(&trans) == 0);
+ mail_transaction_log_sync_unlock(ctx.index->log, "purge");
+ mail_index_view_close(&ctx.view);
+
+ /* wait for child to finish execution */
+ if (wait(&status) == -1)
+ i_error("wait() failed: %m");
+ test_assert(status == 0);
+
+ /* make sure both cache fields are visible */
+ test_assert(mail_index_refresh(ctx.index) == 0);
+
+ view = mail_index_view_open(ctx.index);
+ cache_view = mail_cache_view_open(ctx.cache, view);
+ test_assert(mail_cache_lookup_field(cache_view, str, 1,
+ ctx.cache_field.idx) == 1);
+ test_assert(strcmp(str_c(str), "foo1") == 0);
+ str_truncate(str, 0);
+ test_assert(mail_cache_lookup_field(cache_view, str, 1,
+ ctx.cache_field2.idx) == 1);
+ test_assert(strcmp(str_c(str), "bar2") == 0);
+ mail_cache_view_close(&cache_view);
+ mail_index_view_close(&view);
+
+ test_assert(test_mail_cache_get_purge_count(&ctx) == 1);
+ test_mail_cache_deinit(&ctx);
+ test_mail_index_delete();
+ test_end();
+}
+
+static void test_mail_cache_purge_while_cache_locked(void)
+{
+ struct test_mail_cache_ctx ctx;
+ struct mail_cache_view *cache_view;
+ string_t *str = t_str_new(16);
+ int status;
+
+ test_begin("mail cache purge while cache locked");
+ test_mail_cache_init(test_mail_index_init(), &ctx);
+ test_mail_cache_add_mail(&ctx, ctx.cache_field.idx, "foo1");
+
+ /* lock the cache */
+ test_assert(mail_cache_lock(ctx.cache) == 1);
+
+ /* purge the cache in another process */
+ switch (fork()) {
+ case (pid_t)-1:
+ i_fatal("fork() failed: %m");
+ case 0:
+ test_mail_cache_purge();
+ test_mail_cache_deinit(&ctx);
+ test_exit(test_has_failed() ? 10 : 0);
+ default:
+ break;
+ }
+
+ /* Wait a bit to make sure the child function has had a chance to run.
+ It should start purging, which would wait for our cache lock. */
+ usleep(100000);
+
+ mail_cache_unlock(ctx.cache);
+
+ /* wait for child to finish execution */
+ if (wait(&status) == -1)
+ i_error("wait() failed: %m");
+ test_assert(status == 0);
+
+ /* make sure the cache is still usable */
+ test_assert(mail_index_refresh(ctx.index) == 0);
+ test_mail_cache_view_sync(&ctx);
+
+ cache_view = mail_cache_view_open(ctx.cache, ctx.view);
+ test_assert(mail_cache_lookup_field(cache_view, str, 1,
+ ctx.cache_field.idx) == 1);
+ test_assert(strcmp(str_c(str), "foo1") == 0);
+ mail_cache_view_close(&cache_view);
+
+ test_assert(test_mail_cache_get_purge_count(&ctx) == 1);
+ test_mail_cache_deinit(&ctx);
+ test_mail_index_delete();
+ test_end();
+}
+
+static bool cache_equals(struct mail_cache_view *cache_view, uint32_t seq,
+ unsigned int field_idx, const char *value)
+{
+ string_t *str = str_new(default_pool, 128);
+ int ret = mail_cache_lookup_field(cache_view, str, seq, field_idx);
+ bool match;
+
+ if (value != NULL) {
+ test_assert_idx(ret == 1, seq);
+ match = strcmp(str_c(str), value) == 0;
+ test_assert_idx(match, seq);
+ } else {
+ test_assert_idx(ret == 0, seq);
+ match = ret == 0;
+ }
+ str_free(&str);
+ return match;
+}
+
+static void test_mail_cache_purge_during_write_n(unsigned int num_mails,
+ bool commit_saves)
+{
+ const struct mail_index_optimization_settings optimization_set = {
+ .cache = {
+ .record_max_size = 1024*1024,
+ },
+ };
+ struct test_mail_cache_ctx ctx;
+ struct mail_index_view *updated_view;
+ struct mail_cache_view *cache_view;
+ struct mail_cache_transaction_ctx *cache_trans;
+ struct mail_index_transaction *trans;
+ uint32_t seq;
+
+ test_mail_cache_init(test_mail_index_init(), &ctx);
+ mail_index_set_optimization_settings(ctx.index, &optimization_set);
+
+ /* Add mails */
+ test_mail_cache_add_mail(&ctx, UINT_MAX, "");
+ trans = mail_index_transaction_begin(ctx.view, 0);
+ for (seq = 2; seq <= num_mails; seq++)
+ mail_index_append(trans, seq, &seq);
+
+ if (commit_saves) {
+ test_assert(mail_index_transaction_commit(&trans) == 0);
+ test_mail_cache_view_sync(&ctx);
+ trans = mail_index_transaction_begin(ctx.view, 0);
+ }
+ /* start adding a small cached field to mail1 */
+ updated_view = mail_index_transaction_open_updated_view(trans);
+ cache_view = mail_cache_view_open(ctx.cache, updated_view);
+ cache_trans = mail_cache_get_transaction(cache_view, trans);
+ mail_cache_add(cache_trans, 1, ctx.cache_field.idx, "foo1", 4);
+
+ /* add a huge field to mail2, which triggers flushing */
+ size_t huge_field_size = MAIL_CACHE_MAX_WRITE_BUFFER + 1024;
+ char *huge_field = i_malloc(huge_field_size + 1);
+ memset(huge_field, 'x', huge_field_size);
+ mail_cache_add(cache_trans, 2, ctx.cache_field.idx,
+ huge_field, huge_field_size);
+
+ /* verify that cached fields are still accessible */
+ test_assert(cache_equals(cache_view, 1, ctx.cache_field.idx, "foo1"));
+ test_assert(cache_equals(cache_view, 2, ctx.cache_field.idx, huge_field));
+
+ /* purge using a 2nd index */
+ test_mail_cache_purge();
+
+ if (num_mails == 2) {
+ /* the mails are still accessible after purge */
+ test_assert(cache_equals(cache_view, 1, ctx.cache_field.idx, "foo1"));
+ test_assert(cache_equals(cache_view, 2, ctx.cache_field.idx, huge_field));
+ } else if (!commit_saves) {
+ /* add 3rd mail, which attempts to flush 2nd mail and finds
+ that the first mail is already lost */
+ test_expect_error_string("Purging lost 1 written cache records");
+ mail_cache_add(cache_trans, 3, ctx.cache_field.idx, "foo3", 4);
+ test_expect_no_more_errors();
+
+ test_assert(cache_equals(cache_view, 1, ctx.cache_field.idx, NULL));
+ test_assert(cache_equals(cache_view, 2, ctx.cache_field.idx, huge_field));
+ test_assert(cache_equals(cache_view, 3, ctx.cache_field.idx, "foo3"));
+ } else {
+ /* add 3rd mail, which commits the first two mails */
+ mail_cache_add(cache_trans, 3, ctx.cache_field.idx, "foo3", 4);
+ test_assert(cache_equals(cache_view, 1, ctx.cache_field.idx, "foo1"));
+ test_assert(cache_equals(cache_view, 2, ctx.cache_field.idx, huge_field));
+ test_assert(cache_equals(cache_view, 3, ctx.cache_field.idx, "foo3"));
+ }
+
+ /* finish committing cached fields */
+ if (num_mails == 2 && !commit_saves)
+ test_expect_error_string("Purging lost 1 written cache records");
+ test_assert(mail_index_transaction_commit(&trans) == 0);
+ test_expect_no_more_errors();
+ mail_index_view_close(&updated_view);
+ mail_cache_view_close(&cache_view);
+
+ /* see that we lost the first flush without commit_saves, but not the others */
+ test_mail_cache_view_sync(&ctx);
+ cache_view = mail_cache_view_open(ctx.cache, ctx.view);
+ if (commit_saves)
+ test_assert(cache_equals(cache_view, 1, ctx.cache_field.idx, "foo1"));
+ else
+ test_assert(cache_equals(cache_view, 1, ctx.cache_field.idx, NULL));
+ test_assert(cache_equals(cache_view, 2, ctx.cache_field.idx, huge_field));
+ if (num_mails >= 3)
+ test_assert(cache_equals(cache_view, 3, ctx.cache_field.idx, "foo3"));
+ mail_cache_view_close(&cache_view);
+
+ mail_index_view_close(&ctx.view);
+ test_assert(test_mail_cache_get_purge_count(&ctx) == 1);
+ test_mail_cache_deinit(&ctx);
+ test_mail_index_delete();
+ i_free(huge_field);
+}
+
+static void test_mail_cache_write_lost_during_purge(void)
+{
+ test_begin("mail cache write lost during purge");
+ test_mail_cache_purge_during_write_n(2, FALSE);
+ test_end();
+}
+
+static void test_mail_cache_write_lost_during_purge2(void)
+{
+ test_begin("mail cache write lost during purge (2)");
+ test_mail_cache_purge_during_write_n(3, FALSE);
+ test_end();
+}
+
+static void test_mail_cache_write_autocommit(void)
+{
+ test_begin("mail cache write autocommit");
+ test_mail_cache_purge_during_write_n(2, TRUE);
+ test_end();
+}
+
+static void test_mail_cache_write_autocommit2(void)
+{
+ test_begin("mail cache write autocommit");
+ test_mail_cache_purge_during_write_n(3, TRUE);
+ test_end();
+}
+
+static size_t max_field_size(size_t max_size, size_t current_size)
+{
+ return max_size - current_size
+ - sizeof(struct mail_cache_record)
+ - sizeof(uint32_t) /* field_idx */
+ - sizeof(uint32_t); /* data_size */
+}
+
+static void test_mail_cache_delete_too_large_int(bool exceed_on_first_write)
+{
+ const struct mail_index_optimization_settings optimization_set = {
+ .cache = {
+ .max_size = 1024,
+ },
+ };
+ struct test_mail_cache_ctx ctx;
+ struct stat st;
+
+ test_mail_cache_init(test_mail_index_init(), &ctx);
+ mail_index_set_optimization_settings(ctx.index, &optimization_set);
+ test_mail_cache_add_mail(&ctx, ctx.cache_field.idx, "foo1");
+ test_mail_cache_add_mail(&ctx, ctx.cache_field.idx, "foo2");
+
+ test_assert(stat(ctx.index->cache->filepath, &st) == 0);
+
+ /* create cache file that is exactly max_size */
+ size_t field_size =
+ max_field_size(optimization_set.cache.max_size, st.st_size);
+ if (exceed_on_first_write) {
+ test_expect_error_string("Cache file too large");
+ field_size++;
+ }
+ char *field = i_malloc(field_size + 1);
+ memset(field, 'x', field_size);
+ test_mail_cache_add_field(&ctx, 1, ctx.cache_field2.idx, field);
+ test_expect_no_more_errors();
+ i_free(field);
+
+ if (!exceed_on_first_write) {
+ test_assert(stat(ctx.index->cache->filepath, &st) == 0);
+ test_assert(st.st_size == 1024);
+
+ /* adding anything more will delete the cache. */
+ test_expect_error_string("Cache file too large");
+ test_mail_cache_add_field(&ctx, 1, ctx.cache_field2.idx, "bar1");
+ test_expect_no_more_errors();
+ }
+ test_assert(stat(ctx.index->cache->filepath, &st) < 0 && errno == ENOENT);
+
+ mail_index_view_close(&ctx.view);
+ test_mail_cache_deinit(&ctx);
+ test_mail_index_delete();
+}
+
+static void test_mail_cache_delete_too_large(void)
+{
+ test_begin("mail cache delete too large");
+ test_mail_cache_delete_too_large_int(FALSE);
+ test_end();
+}
+
+static void test_mail_cache_delete_too_large2(void)
+{
+ test_begin("mail cache delete too large (2)");
+ test_mail_cache_delete_too_large_int(TRUE);
+ test_end();
+}
+
+static void test_mail_cache_purge_too_large_int(bool exceed_size)
+{
+ const struct mail_index_optimization_settings optimization_set = {
+ .cache = {
+ .max_size = 1024,
+ },
+ };
+ struct mail_index_transaction *trans;
+ struct mail_cache_view *cache_view;
+ struct test_mail_cache_ctx ctx;
+ struct stat st;
+
+ test_mail_cache_init(test_mail_index_init(), &ctx);
+ mail_index_set_optimization_settings(ctx.index, &optimization_set);
+
+ /* add two mails with some cache field and expunge the first mail */
+ test_mail_cache_add_mail(&ctx, ctx.cache_field.idx, "foo1");
+ test_mail_cache_add_mail(&ctx, ctx.cache_field.idx, "bar2");
+ trans = mail_index_transaction_begin(ctx.view, 0);
+ mail_index_expunge(trans, 1);
+ test_assert(mail_index_transaction_commit(&trans) == 0);
+ test_mail_cache_index_sync(&ctx);
+
+ /* Add a second mail whose cache field size is exactly the
+ max_size [+1 if exceed_size] */
+ test_assert(stat(ctx.index->cache->filepath, &st) == 0);
+ size_t field_size = (exceed_size ? 1 : 0) +
+ max_field_size(optimization_set.cache.max_size, st.st_size);
+ char *field = i_malloc(field_size + 1);
+ memset(field, 'x', field_size);
+ test_mail_cache_add_mail(&ctx, ctx.cache_field.idx, field);
+
+ test_assert(stat(ctx.index->cache->filepath, &st) == 0);
+ if (exceed_size)
+ test_assert((uoff_t)st.st_size < optimization_set.cache.max_size);
+ else
+ test_assert((uoff_t)st.st_size == optimization_set.cache.max_size);
+
+ /* make sure we still find the cache fields */
+ test_mail_cache_view_sync(&ctx);
+ cache_view = mail_cache_view_open(ctx.cache, ctx.view);
+ test_assert(cache_equals(cache_view, 1, ctx.cache_field.idx, "bar2"));
+ test_assert(cache_equals(cache_view, 2, ctx.cache_field.idx, field));
+ mail_cache_view_close(&cache_view);
+
+ i_free(field);
+ if (exceed_size)
+ test_assert(test_mail_cache_get_purge_count(&ctx) == 1);
+ else
+ test_assert(test_mail_cache_get_purge_count(&ctx) == 0);
+ mail_index_view_close(&ctx.view);
+ test_mail_cache_deinit(&ctx);
+ test_mail_index_delete();
+}
+
+static void test_mail_cache_purge_too_large(void)
+{
+ test_begin("mail cache purge too large");
+ test_mail_cache_purge_too_large_int(FALSE);
+ test_end();
+}
+
+static void test_mail_cache_purge_too_large2(void)
+{
+ test_begin("mail cache purge too large (2)");
+ test_mail_cache_purge_too_large_int(TRUE);
+ test_end();
+}
+
+static void test_mail_cache_unexpectedly_lost_int(bool read_first)
+{
+ struct test_mail_cache_ctx ctx;
+ struct mail_cache_view *cache_view;
+
+ test_mail_cache_init(test_mail_index_init(), &ctx);
+ test_mail_cache_add_mail(&ctx, ctx.cache_field.idx, "foo1");
+
+ test_mail_cache_purge();
+
+ /* Unexpectedly delete the cache file under us */
+ i_unlink(ctx.cache->filepath);
+
+ if (read_first) {
+ /* the cache file is already open, so initial reading should
+ work without errors */
+ cache_view = mail_cache_view_open(ctx.cache, ctx.view);
+ test_assert(cache_equals(cache_view, 1, ctx.cache_field.idx, "foo1"));
+ mail_cache_view_close(&cache_view);
+
+ /* if we refresh the index we get new reset_id, which requires
+ reopening the cache and that fails */
+ test_assert(mail_index_refresh(ctx.index) == 0);
+ test_mail_cache_view_sync(&ctx);
+ cache_view = mail_cache_view_open(ctx.cache, ctx.view);
+ test_expect_error_string("test.dovecot.index.cache: No such file or directory");
+ test_assert(cache_equals(cache_view, 1, ctx.cache_field.idx, NULL));
+ test_expect_no_more_errors();
+ mail_cache_view_close(&cache_view);
+ } else {
+ test_expect_error_string("test.dovecot.index.cache: No such file or directory");
+ }
+
+ /* writing after losing the cache should still work */
+ test_mail_cache_add_field(&ctx, 1, ctx.cache_field2.idx, "bar1");
+ test_expect_no_more_errors();
+
+ /* verify that the second cache field is found, but first is lost */
+ cache_view = mail_cache_view_open(ctx.cache, ctx.view);
+ test_assert(cache_equals(cache_view, 1, ctx.cache_field.idx, NULL));
+ test_assert(cache_equals(cache_view, 1, ctx.cache_field2.idx, "bar1"));
+ mail_cache_view_close(&cache_view);
+
+ test_assert(test_mail_cache_get_purge_count(&ctx) == 2);
+ test_mail_cache_deinit(&ctx);
+ test_mail_index_delete();
+}
+
+static void test_mail_cache_unexpectedly_lost(void)
+{
+ test_begin("mail cache unexpectedly lost");
+ test_mail_cache_unexpectedly_lost_int(FALSE);
+ test_end();
+}
+
+static void test_mail_cache_unexpectedly_lost2(void)
+{
+ test_begin("mail cache unexpectedly lost (2)");
+ test_mail_cache_unexpectedly_lost_int(TRUE);
+ test_end();
+}
+
+static void test_mail_cache_resetid_mismatch_int(bool read_first)
+{
+ struct test_mail_cache_ctx ctx;
+ struct mail_cache_view *cache_view;
+ const char *temp_cache_path;
+
+ test_mail_cache_init(test_mail_index_init(), &ctx);
+ test_mail_cache_add_mail(&ctx, ctx.cache_field.idx, "foo1");
+
+ /* make a copy of the first cache file */
+ temp_cache_path = t_strdup_printf("%s.test", ctx.cache->filepath);
+ test_assert(link(ctx.cache->filepath, temp_cache_path) == 0);
+
+ if (read_first) {
+ /* use a secondary index to purge the cache */
+ test_mail_cache_purge();
+
+ /* Replace the new cache file with an old one */
+ test_assert(rename(temp_cache_path, ctx.cache->filepath) == 0);
+
+ /* the cache file is already open, so initial reading should
+ work without errors */
+ cache_view = mail_cache_view_open(ctx.cache, ctx.view);
+ test_assert(cache_equals(cache_view, 1, ctx.cache_field.idx, "foo1"));
+ mail_cache_view_close(&cache_view);
+
+ /* if we refresh the index we get new reset_id, which requires
+ reopening the cache and that fails */
+ test_assert(mail_index_refresh(ctx.index) == 0);
+ test_mail_cache_view_sync(&ctx);
+ cache_view = mail_cache_view_open(ctx.cache, ctx.view);
+
+ test_expect_error_string("reset_id mismatch even after locking");
+ test_assert(cache_equals(cache_view, 1, ctx.cache_field.idx, NULL));
+ test_expect_no_more_errors();
+ mail_cache_view_close(&cache_view);
+ } else {
+ /* purge cache to update reset_id in index */
+ test_assert(mail_cache_purge(ctx.cache, (uint32_t)-1, "test") == 0);
+
+ /* Replace the new cache file with an old one */
+ test_assert(rename(temp_cache_path, ctx.cache->filepath) == 0);
+
+ test_expect_error_string("reset_id mismatch even after locking");
+ }
+
+ /* writing should automatically fix the reset_id mismatch */
+ test_mail_cache_add_field(&ctx, 1, ctx.cache_field2.idx, "bar1");
+ test_expect_no_more_errors();
+
+ /* verify that the second cache field is found, but first is lost */
+ cache_view = mail_cache_view_open(ctx.cache, ctx.view);
+ test_assert(cache_equals(cache_view, 1, ctx.cache_field.idx, NULL));
+ test_assert(cache_equals(cache_view, 1, ctx.cache_field2.idx, "bar1"));
+ mail_cache_view_close(&cache_view);
+
+ test_assert(test_mail_cache_get_purge_count(&ctx) == 2);
+ test_mail_cache_deinit(&ctx);
+ test_mail_index_delete();
+}
+
+static void test_mail_cache_resetid_mismatch(void)
+{
+ test_begin("mail cache resetid mismatch");
+ test_mail_cache_resetid_mismatch_int(FALSE);
+ test_end();
+}
+
+static void test_mail_cache_resetid_mismatch2(void)
+{
+ test_begin("mail cache resetid mismatch (2)");
+ test_mail_cache_resetid_mismatch_int(TRUE);
+ test_end();
+}
+
+enum test_drop {
+ TEST_DROP_NOTHING,
+ TEST_DROP_YES_TO_TEMP_FIRST,
+ TEST_DROP_YES_TO_TEMP_LAST,
+ TEST_DROP_TEMP_TO_NO,
+};
+
+static void test_mail_cache_purge_field_changes_int(enum test_drop drop)
+{
+ enum {
+ TEST_FIELD_NO,
+ TEST_FIELD_NO_FORCED,
+ TEST_FIELD_TEMP,
+ TEST_FIELD_TEMP_FORCED,
+ TEST_FIELD_YES,
+ TEST_FIELD_YES_FORCED,
+ };
+ struct mail_cache_field cache_fields[] = {
+ {
+ .name = "no",
+ .type = MAIL_CACHE_FIELD_STRING,
+ .decision = MAIL_CACHE_DECISION_NO,
+ },
+ {
+ .name = "no-forced",
+ .type = MAIL_CACHE_FIELD_STRING,
+ .decision = MAIL_CACHE_DECISION_NO | MAIL_CACHE_DECISION_FORCED,
+ },
+ {
+ .name = "temp",
+ .type = MAIL_CACHE_FIELD_STRING,
+ .decision = MAIL_CACHE_DECISION_TEMP,
+ },
+ {
+ .name = "temp-forced",
+ .type = MAIL_CACHE_FIELD_STRING,
+ .decision = MAIL_CACHE_DECISION_TEMP | MAIL_CACHE_DECISION_FORCED,
+ },
+ {
+ .name = "yes",
+ .type = MAIL_CACHE_FIELD_STRING,
+ .decision = MAIL_CACHE_DECISION_YES,
+ },
+ {
+ .name = "yes-forced",
+ .type = MAIL_CACHE_FIELD_STRING,
+ .decision = MAIL_CACHE_DECISION_YES | MAIL_CACHE_DECISION_FORCED,
+ },
+ };
+ const struct mail_index_optimization_settings optimization_set = {
+ .cache = {
+ .unaccessed_field_drop_secs = 61,
+ },
+ };
+ struct test_mail_cache_ctx ctx;
+ struct mail_cache_view *cache_view;
+ struct mail_cache_transaction_ctx *cache_trans;
+ struct mail_index_transaction *trans;
+ unsigned int i;
+
+ test_mail_cache_init(test_mail_index_init(), &ctx);
+ mail_index_set_optimization_settings(ctx.index, &optimization_set);
+
+ /* add two mails with all of the cache fields */
+ test_mail_cache_add_mail(&ctx, UINT_MAX, NULL);
+ test_mail_cache_add_mail(&ctx, UINT_MAX, NULL);
+
+ /* Create the cache file before registering any of the cache_fields
+ that we're testing. Otherwise our caching decisions are messed up
+ by purging (which is called to auto-create the cache). */
+ test_assert(mail_cache_purge(ctx.cache, (uint32_t)-1, "test") == 0);
+ mail_cache_register_fields(ctx.cache, cache_fields,
+ N_ELEMENTS(cache_fields));
+
+ trans = mail_index_transaction_begin(ctx.view, 0);
+ cache_view = mail_cache_view_open(ctx.cache, ctx.view);
+ cache_trans = mail_cache_get_transaction(cache_view, trans);
+ for (i = 0; i < N_ELEMENTS(cache_fields); i++) {
+ const char *value = t_strdup_printf("%s-value",
+ cache_fields[i].name);
+ if ((cache_fields[i].decision & ENUM_NEGATE(MAIL_CACHE_DECISION_FORCED)) !=
+ MAIL_CACHE_DECISION_NO) {
+ mail_cache_add(cache_trans, 1, cache_fields[i].idx,
+ value, strlen(value));
+ mail_cache_add(cache_trans, 2, cache_fields[i].idx,
+ value, strlen(value));
+ }
+ }
+
+ /* day_stamp in index is used for deciding when a cache field needs to
+ be dropped. */
+ uint32_t day_stamp = 123456789;
+ mail_index_update_header(trans,
+ offsetof(struct mail_index_header, day_stamp),
+ &day_stamp, sizeof(day_stamp), FALSE);
+ /* day_first_uid[7] is used to determine which mails are "old" and
+ which mails are "new". [7] is the first "new" mail. */
+ uint32_t first_new_uid = 2;
+ mail_index_update_header(trans,
+ offsetof(struct mail_index_header, day_first_uid[7]),
+ &first_new_uid, sizeof(first_new_uid), FALSE);
+ test_assert(mail_index_transaction_commit(&trans) == 0);
+
+ /* set the last_used time just at the boundary of being dropped or
+ being kept */
+ for (i = 0; i < ctx.cache->fields_count; i++) {
+ unsigned int secs = optimization_set.cache.unaccessed_field_drop_secs;
+ switch (drop) {
+ case TEST_DROP_NOTHING:
+ break;
+ case TEST_DROP_YES_TO_TEMP_FIRST:
+ secs++;
+ break;
+ case TEST_DROP_YES_TO_TEMP_LAST:
+ secs *= 2;
+ break;
+ case TEST_DROP_TEMP_TO_NO:
+ secs *= 2;
+ secs++;
+ break;
+ }
+ ctx.cache->fields[i].field.last_used = day_stamp - secs;
+ }
+ test_assert(mail_cache_purge(ctx.cache, (uint32_t)-1, "test") == 0);
+ test_mail_cache_view_sync(&ctx);
+
+ /* verify that caching decisions are as expected after purging */
+ test_assert(ctx.cache->fields[cache_fields[TEST_FIELD_NO].idx].field.decision ==
+ MAIL_CACHE_DECISION_NO);
+ test_assert(ctx.cache->fields[cache_fields[TEST_FIELD_NO_FORCED].idx].field.decision ==
+ (MAIL_CACHE_DECISION_NO | MAIL_CACHE_DECISION_FORCED));
+ test_assert(ctx.cache->fields[cache_fields[TEST_FIELD_TEMP_FORCED].idx].field.decision ==
+ (MAIL_CACHE_DECISION_TEMP | MAIL_CACHE_DECISION_FORCED));
+ test_assert(ctx.cache->fields[cache_fields[TEST_FIELD_YES_FORCED].idx].field.decision ==
+ (MAIL_CACHE_DECISION_YES | MAIL_CACHE_DECISION_FORCED));
+
+ switch (drop) {
+ case TEST_DROP_NOTHING:
+ test_assert(ctx.cache->fields[cache_fields[TEST_FIELD_TEMP].idx].field.decision ==
+ MAIL_CACHE_DECISION_TEMP);
+ test_assert(ctx.cache->fields[cache_fields[TEST_FIELD_YES].idx].field.decision ==
+ MAIL_CACHE_DECISION_YES);
+ break;
+ case TEST_DROP_YES_TO_TEMP_FIRST:
+ case TEST_DROP_YES_TO_TEMP_LAST:
+ test_assert(ctx.cache->fields[cache_fields[TEST_FIELD_TEMP].idx].field.decision ==
+ MAIL_CACHE_DECISION_TEMP);
+ test_assert(ctx.cache->fields[cache_fields[TEST_FIELD_YES].idx].field.decision ==
+ MAIL_CACHE_DECISION_TEMP);
+ break;
+ case TEST_DROP_TEMP_TO_NO:
+ test_assert(ctx.cache->fields[cache_fields[TEST_FIELD_TEMP].idx].field.decision ==
+ MAIL_CACHE_DECISION_NO);
+ test_assert(ctx.cache->fields[cache_fields[TEST_FIELD_YES].idx].field.decision ==
+ MAIL_CACHE_DECISION_NO);
+ break;
+ }
+
+ /* verify that cache fields exist as expected after purging */
+ test_assert(cache_equals(cache_view, 1, cache_fields[TEST_FIELD_NO].idx, NULL));
+ test_assert(cache_equals(cache_view, 2, cache_fields[TEST_FIELD_NO].idx, NULL));
+ test_assert(cache_equals(cache_view, 1, cache_fields[TEST_FIELD_NO_FORCED].idx, NULL));
+ test_assert(cache_equals(cache_view, 2, cache_fields[TEST_FIELD_NO_FORCED].idx, NULL));
+ test_assert(cache_equals(cache_view, 1, cache_fields[TEST_FIELD_TEMP].idx, NULL));
+ if (drop == TEST_DROP_TEMP_TO_NO)
+ test_assert(cache_equals(cache_view, 2, cache_fields[TEST_FIELD_TEMP].idx, NULL));
+ else
+ test_assert(cache_equals(cache_view, 2, cache_fields[TEST_FIELD_TEMP].idx, "temp-value"));
+ test_assert(cache_equals(cache_view, 1, cache_fields[TEST_FIELD_TEMP_FORCED].idx, NULL));
+ test_assert(cache_equals(cache_view, 2, cache_fields[TEST_FIELD_TEMP_FORCED].idx, "temp-forced-value"));
+ if (drop != TEST_DROP_NOTHING)
+ test_assert(cache_equals(cache_view, 1, cache_fields[TEST_FIELD_YES].idx, NULL));
+ else
+ test_assert(cache_equals(cache_view, 1, cache_fields[TEST_FIELD_YES].idx, "yes-value"));
+ test_assert(cache_equals(cache_view, 2, cache_fields[TEST_FIELD_YES_FORCED].idx, "yes-forced-value"));
+
+ test_assert(test_mail_cache_get_purge_count(&ctx) == 1);
+ mail_cache_view_close(&cache_view);
+ test_mail_cache_deinit(&ctx);
+ test_mail_index_delete();
+}
+
+static void test_mail_cache_purge_field_changes(void)
+{
+ test_begin("mail cache purge field changes (nothing)");
+ test_mail_cache_purge_field_changes_int(TEST_DROP_NOTHING);
+ test_end();
+}
+
+static void test_mail_cache_purge_field_changes2(void)
+{
+ test_begin("mail cache purge field changes (yes -> temp, first)");
+ test_mail_cache_purge_field_changes_int(TEST_DROP_YES_TO_TEMP_FIRST);
+ test_end();
+}
+
+static void test_mail_cache_purge_field_changes3(void)
+{
+ test_begin("mail cache purge field changes (yes -> temp, last)");
+ test_mail_cache_purge_field_changes_int(TEST_DROP_YES_TO_TEMP_LAST);
+ test_end();
+}
+
+static void test_mail_cache_purge_field_changes4(void)
+{
+ test_begin("mail cache purge field changes (temp -> no)");
+ test_mail_cache_purge_field_changes_int(TEST_DROP_TEMP_TO_NO);
+ test_end();
+}
+
+static void test_mail_cache_purge_already_done(void)
+{
+ struct test_mail_cache_ctx ctx;
+
+ test_begin("mail cache purge already done");
+ test_mail_cache_init(test_mail_index_init(), &ctx);
+ test_mail_cache_add_mail(&ctx, ctx.cache_field.idx, "foo1");
+
+ test_mail_cache_purge();
+ test_assert(mail_cache_purge(ctx.cache, 1, "test") == 0);
+ test_assert(test_mail_cache_get_purge_count(&ctx) == 1);
+
+ test_assert(mail_cache_purge(ctx.cache, 2, "test") == 0);
+ test_assert(test_mail_cache_get_purge_count(&ctx) == 2);
+
+ test_assert(mail_cache_purge(ctx.cache, 2, "test") == 0);
+ test_assert(test_mail_cache_get_purge_count(&ctx) == 2);
+
+ test_mail_cache_deinit(&ctx);
+ test_mail_index_delete();
+ test_end();
+}
+
+static void test_mail_cache_purge_bitmask(void)
+{
+ struct mail_index_optimization_settings optimization_set = {
+ .cache = {
+ .unaccessed_field_drop_secs = 60,
+ },
+ };
+ struct mail_cache_field bitmask_field = {
+ .name = "bitmask",
+ .type = MAIL_CACHE_FIELD_BITMASK,
+ .field_size = 1,
+ .decision = MAIL_CACHE_DECISION_TEMP,
+ };
+ struct test_mail_cache_ctx ctx;
+ struct mail_cache_view *cache_view;
+
+ test_begin("mail cache purge bitmask");
+ test_mail_cache_init(test_mail_index_init(), &ctx);
+ mail_index_set_optimization_settings(ctx.index, &optimization_set);
+ ioloop_time = 1000000;
+ test_mail_cache_add_mail(&ctx, UINT_MAX, NULL);
+ test_mail_cache_add_mail(&ctx, UINT_MAX, NULL);
+ test_assert(mail_cache_purge(ctx.cache, (uint32_t)-1, "test") == 0);
+ mail_cache_register_fields(ctx.cache, &bitmask_field, 1);
+
+ test_mail_cache_update_day_first_uid7(&ctx, 3);
+
+ test_mail_cache_add_field(&ctx, 1, bitmask_field.idx, "\x01");
+ test_mail_cache_add_field(&ctx, 1, bitmask_field.idx, "\x02");
+ test_mail_cache_add_field(&ctx, 1, bitmask_field.idx, "\x04");
+ test_mail_cache_add_field(&ctx, 2, bitmask_field.idx, "\x01");
+ test_mail_cache_add_field(&ctx, 2, bitmask_field.idx, "\x02");
+ test_mail_cache_add_field(&ctx, 2, bitmask_field.idx, "\x04");
+
+ /* avoid dropping the field */
+ ctx.cache->fields[bitmask_field.idx].field.last_used = ioloop_time;
+
+ /* purge with TEMP decision, which causes the bitmask to be dropped */
+ test_assert(mail_cache_purge(ctx.cache, (uint32_t)-1, "test") == 0);
+
+ cache_view = mail_cache_view_open(ctx.cache, ctx.view);
+ test_assert(cache_equals(cache_view, 1, bitmask_field.idx, NULL));
+ test_assert(cache_equals(cache_view, 2, bitmask_field.idx, NULL));
+ mail_cache_view_close(&cache_view);
+
+ test_mail_cache_deinit(&ctx);
+ test_mail_index_delete();
+ test_end();
+}
+
+
+static void
+test_mail_cache_update_need_purge_continued_records_int(bool big_min_size)
+{
+ struct mail_index_optimization_settings optimization_set = {
+ .cache = {
+ .purge_min_size = big_min_size ? 1024*1024 : 1,
+ .purge_continued_percentage = 30,
+ },
+ };
+ char value[30];
+ struct test_mail_cache_ctx ctx;
+ uint32_t seq;
+
+ test_mail_cache_init(test_mail_index_init(), &ctx);
+ mail_index_set_optimization_settings(ctx.index, &optimization_set);
+
+ for (seq = 1; seq <= 100; seq++) {
+ i_snprintf(value, sizeof(value), "foo%d", seq);
+ test_mail_cache_add_mail(&ctx, ctx.cache_field.idx, value);
+ }
+
+ /* up to 29% no need to purge */
+ for (seq = 1; seq <= 29; seq++) {
+ i_snprintf(value, sizeof(value), "bar%d", seq);
+ test_mail_cache_add_field(&ctx, seq, ctx.cache_field2.idx, value);
+ }
+ test_assert(ctx.cache->need_purge_file_seq == 0);
+
+ /* at 30% need to purge */
+ test_mail_cache_add_field(&ctx, 30, ctx.cache_field2.idx, "bar30");
+ if (big_min_size)
+ test_assert(ctx.cache->need_purge_file_seq == 0);
+ else
+ test_assert(ctx.cache->need_purge_file_seq == ctx.cache->hdr->file_seq);
+
+ test_assert(test_mail_cache_get_purge_count(&ctx) == 0);
+ test_mail_cache_deinit(&ctx);
+ test_mail_index_delete();
+}
+
+static void test_mail_cache_update_need_purge_continued_records(void)
+{
+ test_begin("mail cache update need purge continued records");
+ test_mail_cache_update_need_purge_continued_records_int(FALSE);
+ test_end();
+}
+
+static void test_mail_cache_update_need_purge_continued_records2(void)
+{
+ test_begin("mail cache update need purge continued records (2)");
+ test_mail_cache_update_need_purge_continued_records_int(TRUE);
+ test_end();
+}
+
+static void
+test_mail_cache_update_need_purge_deleted_records_int(bool big_min_size)
+{
+ struct mail_index_optimization_settings optimization_set = {
+ .cache = {
+ .purge_min_size = big_min_size ? 1024*1024 : 1,
+ .purge_delete_percentage = 30,
+ },
+ };
+ char value[30];
+ struct mail_index_transaction *trans;
+ struct test_mail_cache_ctx ctx;
+ uint32_t seq;
+
+ test_mail_cache_init(test_mail_index_init(), &ctx);
+ mail_index_set_optimization_settings(ctx.index, &optimization_set);
+
+ for (seq = 1; seq <= 100; seq++) {
+ i_snprintf(value, sizeof(value), "foo%d", seq);
+ test_mail_cache_add_mail(&ctx, ctx.cache_field.idx, value);
+ }
+
+ /* up to 29% no need to purge */
+ trans = mail_index_transaction_begin(ctx.view, 0);
+ for (seq = 1; seq <= 29; seq++) {
+ i_snprintf(value, sizeof(value), "bar%d", seq);
+ mail_index_expunge(trans, seq);
+ }
+ test_assert(mail_index_transaction_commit(&trans) == 0);
+ test_mail_cache_index_sync(&ctx);
+
+ test_assert(ctx.cache->need_purge_file_seq == 0);
+ test_assert(mail_cache_reopen(ctx.cache) == 1);
+ test_assert(ctx.cache->need_purge_file_seq == 0);
+ test_assert(test_mail_cache_get_purge_count(&ctx) == 0);
+
+ /* at 30% need to purge */
+ trans = mail_index_transaction_begin(ctx.view, 0);
+ mail_index_expunge(trans, 1);
+ test_assert(mail_index_transaction_commit(&trans) == 0);
+ /* syncing will internally purge if !big_min_size */
+ test_mail_cache_index_sync(&ctx);
+
+ test_assert(ctx.cache->need_purge_file_seq == 0);
+ test_assert(mail_cache_reopen(ctx.cache) == 1);
+ test_assert(ctx.cache->need_purge_file_seq == 0);
+ if (big_min_size)
+ test_assert(test_mail_cache_get_purge_count(&ctx) == 0);
+ else
+ test_assert(test_mail_cache_get_purge_count(&ctx) == 1);
+
+ test_mail_cache_deinit(&ctx);
+ test_mail_index_delete();
+}
+
+static void test_mail_cache_update_need_purge_deleted_records(void)
+{
+ test_begin("mail cache update need purge deleted records");
+ test_mail_cache_update_need_purge_deleted_records_int(FALSE);
+ test_end();
+}
+
+static void test_mail_cache_update_need_purge_deleted_records2(void)
+{
+ test_begin("mail cache update need purge deleted records (2)");
+ test_mail_cache_update_need_purge_deleted_records_int(TRUE);
+ test_end();
+}
+
+int main(void)
+{
+ static void (*const test_functions[])(void) = {
+ test_mail_cache_read_during_purge,
+ test_mail_cache_write_during_purge,
+ test_mail_cache_purge_while_cache_locked,
+ test_mail_cache_write_lost_during_purge,
+ test_mail_cache_write_lost_during_purge2,
+ test_mail_cache_write_autocommit,
+ test_mail_cache_write_autocommit2,
+ test_mail_cache_delete_too_large,
+ test_mail_cache_delete_too_large2,
+ test_mail_cache_purge_too_large,
+ test_mail_cache_purge_too_large2,
+ test_mail_cache_unexpectedly_lost,
+ test_mail_cache_unexpectedly_lost2,
+ test_mail_cache_resetid_mismatch,
+ test_mail_cache_resetid_mismatch2,
+ test_mail_cache_purge_field_changes,
+ test_mail_cache_purge_field_changes2,
+ test_mail_cache_purge_field_changes3,
+ test_mail_cache_purge_field_changes4,
+ test_mail_cache_purge_already_done,
+ test_mail_cache_purge_bitmask,
+ test_mail_cache_update_need_purge_continued_records,
+ test_mail_cache_update_need_purge_continued_records2,
+ test_mail_cache_update_need_purge_deleted_records,
+ test_mail_cache_update_need_purge_deleted_records2,
+ NULL
+ };
+ return test_run(test_functions);
+}
diff --git a/src/lib-index/test-mail-cache.c b/src/lib-index/test-mail-cache.c
new file mode 100644
index 0000000..14b3fb6
--- /dev/null
+++ b/src/lib-index/test-mail-cache.c
@@ -0,0 +1,764 @@
+/* Copyright (c) 2020 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "str.h"
+#include "write-full.h"
+#include "test-common.h"
+#include "test-mail-cache.h"
+
+struct test_header_data {
+ uint32_t line1, line2;
+ uint32_t end_of_lines;
+ char headers[8];
+};
+
+enum {
+ TEST_FIELD_NO,
+ TEST_FIELD_NO_FORCED,
+ TEST_FIELD_TEMP,
+ TEST_FIELD_TEMP_FORCED,
+ TEST_FIELD_YES,
+ TEST_FIELD_YES_FORCED,
+ TEST_FIELD_COUNT,
+};
+static const struct mail_cache_field decision_cache_fields[TEST_FIELD_COUNT] = {
+ {
+ .name = "no",
+ .type = MAIL_CACHE_FIELD_STRING,
+ .decision = MAIL_CACHE_DECISION_NO,
+ },
+ {
+ .name = "no-forced",
+ .type = MAIL_CACHE_FIELD_STRING,
+ .decision = MAIL_CACHE_DECISION_NO | MAIL_CACHE_DECISION_FORCED,
+ },
+ {
+ .name = "temp",
+ .type = MAIL_CACHE_FIELD_STRING,
+ .decision = MAIL_CACHE_DECISION_TEMP,
+ },
+ {
+ .name = "temp-forced",
+ .type = MAIL_CACHE_FIELD_STRING,
+ .decision = MAIL_CACHE_DECISION_TEMP | MAIL_CACHE_DECISION_FORCED,
+ },
+ {
+ .name = "yes",
+ .type = MAIL_CACHE_FIELD_STRING,
+ .decision = MAIL_CACHE_DECISION_YES,
+ },
+ {
+ .name = "yes-forced",
+ .type = MAIL_CACHE_FIELD_STRING,
+ .decision = MAIL_CACHE_DECISION_YES | MAIL_CACHE_DECISION_FORCED,
+ },
+};
+
+static void test_mail_cache_fields(void)
+{
+ enum {
+ TEST_FIELD_FIXED,
+ TEST_FIELD_VARIABLE,
+ TEST_FIELD_STRING,
+ TEST_FIELD_BITMASK,
+ TEST_FIELD_HEADER1,
+ TEST_FIELD_HEADER2,
+ };
+ struct mail_cache_field cache_fields[] = {
+ {
+ .name = "fixed",
+ .type = MAIL_CACHE_FIELD_FIXED_SIZE,
+ .field_size = 4,
+ .decision = MAIL_CACHE_DECISION_YES,
+ },
+ {
+ .name = "variable",
+ .type = MAIL_CACHE_FIELD_VARIABLE_SIZE,
+ .decision = MAIL_CACHE_DECISION_YES,
+ },
+ {
+ .name = "string",
+ .type = MAIL_CACHE_FIELD_STRING,
+ .decision = MAIL_CACHE_DECISION_YES,
+ },
+ {
+ .name = "bitmask",
+ .type = MAIL_CACHE_FIELD_BITMASK,
+ .field_size = 4,
+ .decision = MAIL_CACHE_DECISION_YES,
+ },
+ {
+ .name = "header1",
+ .type = MAIL_CACHE_FIELD_HEADER,
+ .decision = MAIL_CACHE_DECISION_YES,
+ },
+ {
+ .name = "header2",
+ .type = MAIL_CACHE_FIELD_HEADER,
+ .decision = MAIL_CACHE_DECISION_YES,
+ },
+ };
+ struct test_mail_cache_ctx ctx;
+ struct mail_index_transaction *trans;
+ struct mail_cache_view *cache_view;
+ struct mail_cache_transaction_ctx *cache_trans;
+ string_t *str = t_str_new(16);
+
+ test_begin("mail cache uncommitted lookups");
+ test_mail_cache_init(test_mail_index_init(), &ctx);
+ mail_cache_register_fields(ctx.cache, cache_fields,
+ N_ELEMENTS(cache_fields));
+
+ test_mail_cache_add_mail(&ctx, UINT_MAX, NULL);
+
+ /* add the cache fields */
+ cache_view = mail_cache_view_open(ctx.cache, ctx.view);
+ trans = mail_index_transaction_begin(ctx.view, 0);
+ cache_trans = mail_cache_get_transaction(cache_view, trans);
+
+ const uint8_t fixed_data[] = { 0x12, 0x34, 0x56, 0x78 };
+ mail_cache_add(cache_trans, 1, cache_fields[TEST_FIELD_FIXED].idx,
+ fixed_data, sizeof(fixed_data));
+ const uint8_t variable_data[] = { 0xab, 0xcd, 0xef };
+ mail_cache_add(cache_trans, 1, cache_fields[TEST_FIELD_VARIABLE].idx,
+ variable_data, sizeof(variable_data));
+ const char string_data[] = { 's', 't', 'r' };
+ mail_cache_add(cache_trans, 1, cache_fields[TEST_FIELD_STRING].idx,
+ string_data, sizeof(string_data));
+ uint8_t bitmask_data[] = { 0x00, 0x01, 0x10, 0x11 };
+ mail_cache_add(cache_trans, 1, cache_fields[TEST_FIELD_BITMASK].idx,
+ bitmask_data, sizeof(bitmask_data));
+ struct test_header_data header_data1 = {
+ .line1 = 15,
+ .line2 = 30,
+ .headers = "foo\nbar\n",
+ };
+ struct test_header_data header_data2 = {
+ .line1 = 10,
+ .line2 = 20,
+ .headers = "123\n456\n",
+ };
+ mail_cache_add(cache_trans, 1, cache_fields[TEST_FIELD_HEADER1].idx,
+ &header_data1, sizeof(header_data1));
+ mail_cache_add(cache_trans, 1, cache_fields[TEST_FIELD_HEADER2].idx,
+ &header_data2, sizeof(header_data2));
+
+ /* make sure the fields can be looked up even though they're
+ not committed */
+ for (int i = 0;; i++) {
+ str_truncate(str, 0);
+ test_assert_idx(mail_cache_lookup_field(cache_view, str, 1,
+ cache_fields[TEST_FIELD_FIXED].idx) == 1, i);
+ test_assert_idx(str_len(str) == sizeof(fixed_data) &&
+ memcmp(str_data(str), fixed_data, str_len(str)) == 0, i);
+ str_truncate(str, 0);
+ test_assert_idx(mail_cache_lookup_field(cache_view, str, 1,
+ cache_fields[TEST_FIELD_VARIABLE].idx) == 1, i);
+ test_assert_idx(str_len(str) == sizeof(variable_data) &&
+ memcmp(str_data(str), variable_data, str_len(str)) == 0, i);
+ str_truncate(str, 0);
+ test_assert_idx(mail_cache_lookup_field(cache_view, str, 1,
+ cache_fields[TEST_FIELD_STRING].idx) == 1, i);
+ test_assert_idx(str_len(str) == sizeof(string_data) &&
+ memcmp(str_data(str), string_data, str_len(str)) == 0, i);
+ str_truncate(str, 0);
+ test_assert_idx(mail_cache_lookup_field(cache_view, str, 1,
+ cache_fields[TEST_FIELD_BITMASK].idx) == 1, i);
+ test_assert_idx(str_len(str) == sizeof(bitmask_data) &&
+ memcmp(str_data(str), bitmask_data, str_len(str)) == 0, i);
+ const unsigned int lookup_header_fields[] = {
+ cache_fields[TEST_FIELD_HEADER2].idx,
+ cache_fields[TEST_FIELD_HEADER1].idx,
+ };
+ str_truncate(str, 0);
+ test_assert_idx(mail_cache_lookup_headers(cache_view, str, 1,
+ lookup_header_fields,
+ N_ELEMENTS(lookup_header_fields)) == 1, i);
+ test_assert_strcmp(str_c(str), "123\nfoo\n456\nbar\n");
+
+ if (trans == NULL)
+ break;
+
+ /* add more bitmask data within the same transaction */
+ uint8_t bitmask_add[4] = { 0x20, 0x20, 0x20, 0x20 };
+ for (unsigned int j = 0; j < sizeof(bitmask_data); j++)
+ bitmask_data[j] |= bitmask_add[j];
+ mail_cache_add(cache_trans, 1, cache_fields[TEST_FIELD_BITMASK].idx,
+ bitmask_add, sizeof(bitmask_add));
+ /* check that we can still read it */
+ str_truncate(str, 0);
+ test_assert_idx(mail_cache_lookup_field(cache_view, str, 1,
+ cache_fields[TEST_FIELD_BITMASK].idx) == 1, i);
+ test_assert_idx(str_len(str) == sizeof(bitmask_data) &&
+ memcmp(str_data(str), bitmask_data, str_len(str)) == 0, i);
+
+ /* commit the transaction and lookup the fields again */
+ test_assert(mail_index_transaction_commit(&trans) == 0);
+ }
+
+ /* add more bitmask data in separate transactions */
+ for (unsigned int i = 0; i < 4; i++) {
+ uint8_t bitmask_add[4] = { 0, 0, 0, 0 };
+ bitmask_add[i] = 0x40;
+ bitmask_data[i] |= 0x40;
+
+ trans = mail_index_transaction_begin(ctx.view, 0);
+ cache_trans = mail_cache_get_transaction(cache_view, trans);
+ mail_cache_add(cache_trans, 1, cache_fields[TEST_FIELD_BITMASK].idx,
+ bitmask_add, sizeof(bitmask_add));
+ test_assert(mail_index_transaction_commit(&trans) == 0);
+ }
+
+ /* verify that bitmask is still as expected */
+ str_truncate(str, 0);
+ test_assert(mail_cache_lookup_field(cache_view, str, 1,
+ cache_fields[TEST_FIELD_BITMASK].idx) == 1);
+ test_assert(str_len(str) == sizeof(bitmask_data) &&
+ memcmp(str_data(str), bitmask_data, str_len(str)) == 0);
+
+ /* verify that bitmask is still as expected after purging */
+ test_assert(mail_cache_purge(ctx.cache, (uint32_t)-1, "test") == 0);
+ test_mail_cache_view_sync(&ctx);
+
+ str_truncate(str, 0);
+ test_assert(mail_cache_lookup_field(cache_view, str, 1,
+ cache_fields[TEST_FIELD_BITMASK].idx) == 1);
+ test_assert(str_len(str) == sizeof(bitmask_data) &&
+ memcmp(str_data(str), bitmask_data, str_len(str)) == 0);
+
+ test_assert(test_mail_cache_get_purge_count(&ctx) == 1);
+ mail_cache_view_close(&cache_view);
+ test_mail_cache_deinit(&ctx);
+ test_mail_index_delete();
+ test_end();
+}
+
+static void test_mail_cache_record_max_size_int(unsigned int field3_size)
+{
+ const struct mail_index_optimization_settings optimization_set = {
+ .cache = {
+ /* lets assume we can write 2 cache fields,
+ each containing 8 bytes */
+ .record_max_size = sizeof(struct mail_cache_record) +
+ 2 * (sizeof(uint32_t) + /* field_idx */
+ sizeof(uint32_t) + /* data_size */
+ 8), /* content max length */
+ },
+ };
+ struct test_mail_cache_ctx ctx;
+ struct mail_index_transaction *trans;
+ struct mail_cache_view *cache_view;
+ struct mail_cache_transaction_ctx *cache_trans;
+ string_t *str = t_str_new(16);
+
+ test_mail_cache_init(test_mail_index_init(), &ctx);
+ mail_index_set_optimization_settings(ctx.index, &optimization_set);
+
+ /* Add the first cache field. In a chain of cache records each one
+ has independent max size. Although this isn't really ideal, because
+ purging merges them and drops the records entirely if the combined
+ length is too large. But for now test least test what is
+ implemented. */
+ test_mail_cache_add_mail(&ctx, ctx.cache_field.idx, "12345678");
+
+ /* add the other field(s) */
+ cache_view = mail_cache_view_open(ctx.cache, ctx.view);
+ trans = mail_index_transaction_begin(ctx.view, 0);
+ cache_trans = mail_cache_get_transaction(cache_view, trans);
+ mail_cache_add(cache_trans, 1, ctx.cache_field2.idx, "abcdefgh", 8);
+ if (field3_size > 0)
+ mail_cache_add(cache_trans, 1, ctx.cache_field3.idx, "ijklmnopq", field3_size);
+ test_assert(mail_index_transaction_commit(&trans) == 0);
+ mail_cache_view_close(&cache_view);
+
+ /* make sure all the fields are visible */
+ test_mail_cache_view_sync(&ctx);
+
+ cache_view = mail_cache_view_open(ctx.cache, ctx.view);
+ test_assert(mail_cache_lookup_field(cache_view, str, 1,
+ ctx.cache_field.idx) == 1);
+ test_assert(mail_cache_lookup_field(cache_view, str, 1,
+ ctx.cache_field2.idx) == 1);
+ if (field3_size == 8) {
+ test_assert(mail_cache_lookup_field(cache_view, str, 1,
+ ctx.cache_field3.idx) == 1);
+ test_assert_strcmp(str_c(str), "12345678abcdefghijklmnop");
+ } else {
+ test_assert_strcmp(str_c(str), "12345678abcdefgh");
+ }
+ mail_cache_view_close(&cache_view);
+ test_assert(test_mail_cache_get_purge_count(&ctx) == 0);
+
+ /* if there are 3 fields, purging realizes that the record is too
+ large and drops it */
+ test_assert(mail_cache_purge(ctx.cache, (uint32_t)-1, "test") == 0);
+ test_assert(mail_cache_reopen(ctx.cache) == 1);
+
+ cache_view = mail_cache_view_open(ctx.cache, ctx.view);
+ if (field3_size == 8) {
+ /* test that none of the fields are in cache */
+ test_assert(mail_cache_lookup_field(cache_view, str, 1,
+ ctx.cache_field.idx) == 0);
+ test_assert(mail_cache_lookup_field(cache_view, str, 1,
+ ctx.cache_field2.idx) == 0);
+ test_assert(mail_cache_lookup_field(cache_view, str, 1,
+ ctx.cache_field3.idx) == 0);
+ } else {
+ str_truncate(str, 0);
+ test_assert(mail_cache_lookup_field(cache_view, str, 1,
+ ctx.cache_field.idx) == 1);
+ test_assert(mail_cache_lookup_field(cache_view, str, 1,
+ ctx.cache_field2.idx) == 1);
+ test_assert_strcmp(str_c(str), "12345678abcdefgh");
+ }
+ mail_cache_view_close(&cache_view);
+ test_assert(test_mail_cache_get_purge_count(&ctx) == 1);
+
+ test_mail_cache_deinit(&ctx);
+ test_mail_index_delete();
+}
+
+static void test_mail_cache_record_max_size(void)
+{
+ test_begin("mail cache record max size");
+ test_mail_cache_record_max_size_int(0);
+ test_end();
+}
+
+static void test_mail_cache_record_max_size2(void)
+{
+ test_begin("mail cache record max size (2)");
+ test_mail_cache_record_max_size_int(8);
+ test_end();
+}
+
+static void test_mail_cache_record_max_size3(void)
+{
+ test_begin("mail cache record max size (3)");
+ test_mail_cache_record_max_size_int(9);
+ test_end();
+}
+
+static void test_mail_cache_record_max_size4(void)
+{
+ const struct mail_index_optimization_settings optimization_set = {
+ .cache = {
+ .record_max_size = sizeof(struct mail_cache_record) +
+ sizeof(uint32_t) + /* field_idx */
+ sizeof(uint32_t) + /* data_size */
+ 8, /* content max length */
+ },
+ };
+ struct test_mail_cache_ctx ctx;
+ struct mail_index_transaction *trans;
+ struct mail_cache_view *cache_view;
+ struct mail_cache_transaction_ctx *cache_trans;
+ string_t *str = t_str_new(16);
+
+ test_begin("mail cache record max size (4)");
+ test_mail_cache_init(test_mail_index_init(), &ctx);
+ mail_index_set_optimization_settings(ctx.index, &optimization_set);
+
+ test_mail_cache_add_mail(&ctx, UINT_MAX, NULL);
+ test_mail_cache_add_mail(&ctx, UINT_MAX, NULL);
+
+ cache_view = mail_cache_view_open(ctx.cache, ctx.view);
+ trans = mail_index_transaction_begin(ctx.view, 0);
+ cache_trans = mail_cache_get_transaction(cache_view, trans);
+ mail_cache_add(cache_trans, 1, ctx.cache_field.idx, "123456789", 9);
+ mail_cache_add(cache_trans, 2, ctx.cache_field.idx, "123456789", 9);
+ test_assert(mail_index_transaction_commit(&trans) == 0);
+ mail_cache_view_close(&cache_view);
+
+ /* make sure none of the fields are visible */
+ test_mail_cache_view_sync(&ctx);
+
+ cache_view = mail_cache_view_open(ctx.cache, ctx.view);
+ test_assert(mail_cache_lookup_field(cache_view, str, 1,
+ ctx.cache_field.idx) == 0);
+ test_assert(mail_cache_lookup_field(cache_view, str, 2,
+ ctx.cache_field.idx) == 0);
+ mail_cache_view_close(&cache_view);
+ test_assert(ctx.cache->hdr == NULL); /* never created */
+
+ test_mail_cache_deinit(&ctx);
+ test_mail_index_delete();
+ test_end();
+}
+
+static void test_mail_cache_add_decisions(void)
+{
+ struct mail_cache_field cache_fields[TEST_FIELD_COUNT];
+ enum mail_cache_decision_type expected_decisions[TEST_FIELD_COUNT];
+ struct test_mail_cache_ctx ctx;
+ struct mail_index_transaction *trans;
+ struct mail_cache_view *cache_view;
+ struct mail_cache_transaction_ctx *cache_trans;
+ unsigned int i;
+
+ test_begin("mail cache add decisions");
+
+ test_mail_cache_init(test_mail_index_init(), &ctx);
+ memcpy(cache_fields, decision_cache_fields, sizeof(cache_fields));
+ mail_cache_register_fields(ctx.cache, cache_fields, TEST_FIELD_COUNT);
+ for (i = 0; i < TEST_FIELD_COUNT; i++)
+ expected_decisions[i] = cache_fields[i].decision;
+
+ /* create the initial cache file */
+ test_mail_cache_add_mail(&ctx, UINT_MAX, NULL);
+ test_mail_cache_add_mail(&ctx, UINT_MAX, NULL);
+ test_assert(mail_cache_purge(ctx.cache, (uint32_t)-1, "test") == 0);
+
+ /* check that decisions haven't changed */
+ for (i = 0; i < TEST_FIELD_COUNT; i++)
+ test_assert_idx(ctx.cache->fields[cache_fields[i].idx].field.decision == expected_decisions[i], i);
+
+ cache_view = mail_cache_view_open(ctx.cache, ctx.view);
+ trans = mail_index_transaction_begin(ctx.view, 0);
+ cache_trans = mail_cache_get_transaction(cache_view, trans);
+
+ /* test that when cache decisions are disabled, it doesn't affect the
+ NO state change */
+ mail_cache_view_update_cache_decisions(cache_view, FALSE);
+ mail_cache_add(cache_trans, 2, cache_fields[TEST_FIELD_NO].idx, "bar", 3);
+ mail_cache_view_update_cache_decisions(cache_view, TRUE);
+ test_assert(ctx.cache->fields[cache_fields[TEST_FIELD_NO].idx].field.decision == MAIL_CACHE_DECISION_NO);
+
+ /* add a cache field of each type */
+ for (i = 0; i < TEST_FIELD_COUNT; i++)
+ mail_cache_add(cache_trans, 1, cache_fields[i].idx, "foo", 3);
+ /* quick check before commit that the state is as expected */
+ test_assert(ctx.cache->fields[cache_fields[TEST_FIELD_NO].idx].field.decision == MAIL_CACHE_DECISION_TEMP);
+ test_assert(ctx.cache->fields[cache_fields[TEST_FIELD_NO].idx].decision_dirty);
+ test_assert(ctx.cache->fields[cache_fields[TEST_FIELD_NO].idx].uid_highwater == 1);
+
+ test_assert(mail_index_transaction_commit(&trans) == 0);
+ mail_cache_view_close(&cache_view);
+
+ /* verify the state: NO state becomes TEMP, others are unchanged */
+ expected_decisions[TEST_FIELD_NO] = MAIL_CACHE_DECISION_TEMP;
+ test_assert(!ctx.cache->field_header_write_pending);
+ for (i = 0; i < TEST_FIELD_COUNT; i++) {
+ const struct mail_cache_field_private *priv =
+ &ctx.cache->fields[cache_fields[i].idx];
+ test_assert_idx(priv->field.decision == expected_decisions[i], i);
+ test_assert_idx(!priv->decision_dirty, i);
+ uint32_t uid_highwater = priv->uid_highwater;
+ if (i != TEST_FIELD_NO_FORCED)
+ test_assert_idx(uid_highwater == 1, i);
+ else
+ test_assert_idx(uid_highwater == 0, i);
+ }
+
+ test_assert(test_mail_cache_get_purge_count(&ctx) == 0);
+ test_mail_cache_deinit(&ctx);
+ test_mail_index_delete();
+ test_end();
+}
+
+static void test_mail_cache_lookup_decisions_int(bool header_lookups)
+{
+ struct mail_cache_field cache_fields[TEST_FIELD_COUNT];
+ enum mail_cache_decision_type expected_decisions[TEST_FIELD_COUNT];
+ uint32_t expected_uid_highwater[TEST_FIELD_COUNT];
+ time_t expected_last_used[TEST_FIELD_COUNT];
+ struct test_mail_cache_ctx ctx;
+ struct mail_index_transaction *trans;
+ struct mail_cache_view *cache_view;
+ unsigned int i;
+ string_t *str = t_str_new(16);
+
+ test_mail_cache_init(test_mail_index_init(), &ctx);
+ /* create the initial mails and the cache file */
+ test_mail_cache_add_mail(&ctx, UINT_MAX, NULL);
+ test_mail_cache_add_mail(&ctx, UINT_MAX, NULL);
+ test_mail_cache_add_mail(&ctx, UINT_MAX, NULL);
+ test_assert(mail_cache_purge(ctx.cache, (uint32_t)-1, "test") == 0);
+
+ /* register fields after the initial purge create the cache */
+ memcpy(cache_fields, decision_cache_fields, sizeof(cache_fields));
+ mail_cache_register_fields(ctx.cache, cache_fields, TEST_FIELD_COUNT);
+ for (i = 0; i < TEST_FIELD_COUNT; i++) {
+ expected_decisions[i] = cache_fields[i].decision;
+ expected_uid_highwater[i] = 0;
+ }
+
+ /* day_first_uid[7] is used to determine which mails are "old" and
+ which mails are "new". [7] is the first "new" mail. */
+ test_mail_cache_update_day_first_uid7(&ctx, 2);
+
+ trans = mail_index_transaction_begin(ctx.view, 0);
+ cache_view = mail_cache_view_open(ctx.cache, ctx.view);
+
+ /* test that nothing changes when cache decision updates are disabled */
+ mail_cache_view_update_cache_decisions(cache_view, FALSE);
+ for (i = 0; i < TEST_FIELD_COUNT; i++) T_BEGIN {
+ const struct mail_cache_field_private *priv =
+ &ctx.cache->fields[cache_fields[i].idx];
+ if (!header_lookups) {
+ test_assert_idx(mail_cache_lookup_field(cache_view,
+ str, 1, cache_fields[i].idx) == 0, i);
+ } else {
+ /* it's a bit wrong to lookup headers using a STRING
+ type cache field, but this is simpler and at least
+ currently there's no assert for it.. */
+ test_assert_idx(mail_cache_lookup_headers(cache_view,
+ str, 2, &cache_fields[i].idx, 1) == 0, i);
+ }
+ test_assert_idx(priv->field.decision == expected_decisions[i], i);
+ test_assert_idx(!priv->decision_dirty, i);
+ test_assert_idx(priv->uid_highwater == 0, i);
+ test_assert_idx(priv->field.last_used == 0, i);
+ } T_END;
+ test_assert(!ctx.cache->field_header_write_pending);
+ mail_cache_view_update_cache_decisions(cache_view, TRUE);
+
+ /* set cache fields for the first "new" mail (seq/UID 2) */
+ ioloop_time = 123456789;
+ for (i = 0; i < TEST_FIELD_COUNT; i++) T_BEGIN {
+ const struct mail_cache_field_private *priv =
+ &ctx.cache->fields[cache_fields[i].idx];
+
+ time_t prev_last_used = priv->field.last_used;
+ ioloop_time++;
+ if (!header_lookups) {
+ test_assert_idx(mail_cache_lookup_field(cache_view,
+ str, 2, cache_fields[i].idx) == 0, i);
+ } else {
+ test_assert_idx(mail_cache_lookup_headers(cache_view,
+ str, 2, &cache_fields[i].idx, 1) == 0, i);
+ }
+ expected_last_used[i] = ioloop_time;
+ switch (i) {
+ case TEST_FIELD_NO_FORCED:
+ expected_last_used[i] = 0;
+ /* fall through */
+ case TEST_FIELD_NO:
+ /* Note that just doing a cache lookup won't change
+ caching decision. Higher level code needs to figure
+ out itself if it wants the field to become cached.
+ This happens only by calling mail_cache_add(). */
+ break;
+ case TEST_FIELD_TEMP:
+ /* Note that uid_highwater isn't permanently saved to
+ the cache file. It's used only within a single
+ session. */
+ expected_uid_highwater[i] = 2;
+ break;
+ case TEST_FIELD_YES:
+ /* YES decision doesn't change last_used until the
+ cache decision has been confirmed again. */
+ expected_last_used[i] = prev_last_used;
+ expected_uid_highwater[i] = 2;
+ break;
+ }
+ test_assert_idx(priv->field.decision == expected_decisions[i], i);
+ test_assert_idx(priv->uid_highwater == expected_uid_highwater[i], i);
+ test_assert_idx(priv->field.last_used == expected_last_used[i], i);
+ test_assert_idx(!priv->decision_dirty, i);
+ } T_END;
+ test_assert(!ctx.cache->field_header_write_pending);
+
+ test_assert(mail_index_transaction_commit(&trans) == 0);
+
+ /* test that after commit and reopening the decisions are still the
+ same. */
+ test_assert(mail_cache_reopen(ctx.cache) == 1);
+ for (i = 0; i < TEST_FIELD_COUNT; i++) {
+ const struct mail_cache_field_private *priv =
+ &ctx.cache->fields[cache_fields[i].idx];
+ test_assert_idx(priv->field.decision == expected_decisions[i], i);
+ test_assert_idx(priv->uid_highwater == expected_uid_highwater[i], i);
+ test_assert_idx(priv->field.last_used == expected_last_used[i], i);
+ test_assert_idx(!priv->decision_dirty, i);
+ }
+
+ /* update the day_first_uid so all mails are now "old" */
+ test_mail_cache_update_day_first_uid7(&ctx, 4);
+
+ for (uint32_t seq = 2; seq >= 1; seq--) {
+ /* Reading a 3rd mail, which is also now "old". It causes
+ TEMP -> YES cache decision (don't read backwards yet,
+ that's a separate test). */
+ expected_decisions[TEST_FIELD_TEMP] = MAIL_CACHE_DECISION_YES;
+ for (i = 0; i < TEST_FIELD_COUNT; i++) T_BEGIN {
+ const struct mail_cache_field_private *priv =
+ &ctx.cache->fields[cache_fields[i].idx];
+
+ /* Keep increasing ioloop_time just to make sure that
+ last_used doesn't change. (It changes only once per
+ 24h) */
+ ioloop_time++;
+ if (!header_lookups) {
+ test_assert_idx(mail_cache_lookup_field(
+ cache_view, str, seq,
+ cache_fields[i].idx) == 0, i);
+ } else {
+ test_assert_idx(mail_cache_lookup_headers(
+ cache_view, str, seq,
+ &cache_fields[i].idx, 1) == 0, i);
+ }
+ if (i == TEST_FIELD_YES && seq == 2) {
+ /* YES decision is confirmed now. The last_used
+ timestamp was updated for the first old
+ mail. */
+ expected_last_used[i] = ioloop_time;
+ }
+ test_assert_idx(priv->field.decision == expected_decisions[i], i);
+ test_assert_idx(priv->uid_highwater == expected_uid_highwater[i], i);
+ test_assert_idx(priv->field.last_used == expected_last_used[i], i);
+ test_assert_idx(priv->decision_dirty == (i == TEST_FIELD_TEMP), i);
+ } T_END;
+ /* restore caching decision */
+ ctx.cache->fields[cache_fields[TEST_FIELD_TEMP].idx].field.decision =
+ MAIL_CACHE_DECISION_TEMP;
+ /* reading mails backwards also causes TEMP -> YES cache
+ decision, even if all mails are "new" */
+ test_mail_cache_update_day_first_uid7(&ctx, 1);
+ }
+
+ test_assert(test_mail_cache_get_purge_count(&ctx) == 0);
+ mail_cache_view_close(&cache_view);
+ test_mail_cache_deinit(&ctx);
+ test_mail_index_delete();
+}
+
+static void test_mail_cache_lookup_decisions(void)
+{
+ test_begin("mail cache lookup decisions");
+ test_mail_cache_lookup_decisions_int(FALSE);
+ test_end();
+}
+
+static void test_mail_cache_lookup_decisions2(void)
+{
+ test_begin("mail cache lookup decisions (2)");
+ test_mail_cache_lookup_decisions_int(TRUE);
+ test_end();
+}
+
+static void test_mail_cache_in_memory(void)
+{
+ const struct mail_index_optimization_settings optimization_set = {
+ .cache = {
+ .record_max_size = MAIL_CACHE_MAX_WRITE_BUFFER*2,
+ },
+ };
+ struct test_mail_cache_ctx ctx;
+ struct mail_index *index;
+ struct mail_index_transaction *trans;
+ struct mail_cache_view *cache_view;
+ struct mail_cache_transaction_ctx *cache_trans;
+
+ test_begin("mail cache add in-memory");
+
+ index = mail_index_alloc(NULL, NULL, "(in-memory)");
+ test_assert(mail_index_open_or_create(index, MAIL_INDEX_OPEN_FLAG_CREATE) == 0);
+ test_mail_cache_init(index, &ctx);
+ mail_index_set_optimization_settings(ctx.index, &optimization_set);
+ cache_view = mail_cache_view_open(ctx.cache, ctx.view);
+
+ test_mail_cache_add_mail(&ctx, UINT_MAX, NULL);
+ test_mail_cache_add_mail(&ctx, UINT_MAX, NULL);
+
+ trans = mail_index_transaction_begin(ctx.view, 0);
+ cache_trans = mail_cache_get_transaction(cache_view, trans);
+
+ size_t blob_size = 1024*130;
+ char *blob = i_malloc(blob_size);
+ memset(blob, 'x', blob_size);
+ mail_cache_add(cache_trans, 1, ctx.cache_field.idx, blob, blob_size);
+ mail_cache_add(cache_trans, 1, ctx.cache_field2.idx, "foo1", 4);
+ mail_cache_add(cache_trans, 2, ctx.cache_field2.idx, "foo2", 4);
+
+ /* all fields are still available */
+ string_t *str = str_new(default_pool, blob_size + 1024);
+ test_assert(mail_cache_lookup_field(cache_view, str, 1,
+ ctx.cache_field.idx) == 1);
+ test_assert(str_len(str) == blob_size);
+ str_truncate(str, 0);
+ test_assert(mail_cache_lookup_field(cache_view, str, 1,
+ ctx.cache_field2.idx) == 1);
+ test_assert_strcmp(str_c(str), "foo1");
+ str_truncate(str, 0);
+ test_assert(mail_cache_lookup_field(cache_view, str, 2,
+ ctx.cache_field2.idx) == 1);
+ test_assert_strcmp(str_c(str), "foo2");
+
+ /* adding a second blob grows memory usage beyond
+ MAIL_CACHE_MAX_WRITE_BUFFER and frees the first cached mail
+ entirely (although in theory it could drop just the big blob) */
+ mail_cache_add(cache_trans, 2, ctx.cache_field.idx, blob, blob_size);
+ test_assert(mail_cache_lookup_field(cache_view, str, 1,
+ ctx.cache_field.idx) == 0);
+ test_assert(mail_cache_lookup_field(cache_view, str, 1,
+ ctx.cache_field2.idx) == 0);
+ str_truncate(str, 0);
+ test_assert(mail_cache_lookup_field(cache_view, str, 2,
+ ctx.cache_field.idx) == 1);
+ test_assert(str_len(str) == blob_size);
+ str_truncate(str, 0);
+ test_assert(mail_cache_lookup_field(cache_view, str, 2,
+ ctx.cache_field2.idx) == 1);
+ test_assert_strcmp(str_c(str), "foo2");
+
+ test_assert(mail_index_transaction_commit(&trans) == 0);
+
+ str_free(&str);
+ i_free(blob);
+
+ mail_cache_view_close(&cache_view);
+ test_mail_cache_deinit(&ctx);
+ test_mail_index_delete();
+ test_end();
+}
+
+static void test_mail_cache_size_corruption(void)
+{
+ struct test_mail_cache_ctx ctx;
+ struct mail_cache_view *cache_view;
+ struct mail_cache_lookup_iterate_ctx iter;
+ struct mail_cache_iterate_field field;
+
+ test_begin("mail cache size corruption");
+
+ test_mail_cache_init(test_mail_index_init(), &ctx);
+ test_mail_cache_add_mail(&ctx, ctx.cache_field.idx, "12345678");
+ cache_view = mail_cache_view_open(ctx.cache, ctx.view);
+
+ /* lookup the added cache field */
+ mail_cache_lookup_iter_init(cache_view, 1, &iter);
+ test_assert(iter.offset > 0);
+
+ uoff_t size_offset = iter.offset +
+ offsetof(struct mail_cache_record, size);
+ uint32_t new_size = 0x10000000;
+ test_assert(pwrite_full(ctx.cache->fd, &new_size, sizeof(new_size),
+ size_offset) == 0);
+ test_expect_error_string("record points outside file");
+ test_assert(mail_cache_lookup_iter_next(&iter, &field) == -1);
+ test_expect_no_more_errors();
+
+ mail_cache_view_close(&cache_view);
+ test_mail_cache_deinit(&ctx);
+ test_mail_index_delete();
+ test_end();
+}
+
+int main(void)
+{
+ static void (*const test_functions[])(void) = {
+ test_mail_cache_fields,
+ test_mail_cache_record_max_size,
+ test_mail_cache_record_max_size2,
+ test_mail_cache_record_max_size3,
+ test_mail_cache_record_max_size4,
+ test_mail_cache_add_decisions,
+ test_mail_cache_lookup_decisions,
+ test_mail_cache_lookup_decisions2,
+ test_mail_cache_in_memory,
+ test_mail_cache_size_corruption,
+ NULL
+ };
+ return test_run(test_functions);
+}
diff --git a/src/lib-index/test-mail-cache.h b/src/lib-index/test-mail-cache.h
new file mode 100644
index 0000000..2a274b5
--- /dev/null
+++ b/src/lib-index/test-mail-cache.h
@@ -0,0 +1,32 @@
+#ifndef TEST_MAIL_CACHE_H
+#define TEST_MAIL_CACHE_H
+
+#include "test-mail-index.h"
+#include "mail-cache-private.h"
+
+struct test_mail_cache_ctx {
+ struct mail_index *index;
+ struct mail_cache *cache;
+ struct mail_index_view *view;
+
+ struct mail_cache_field cache_field, cache_field2, cache_field3;
+};
+
+void test_mail_cache_init(struct mail_index *index,
+ struct test_mail_cache_ctx *ctx_r);
+void test_mail_cache_deinit(struct test_mail_cache_ctx *ctx);
+
+unsigned int test_mail_cache_get_purge_count(struct test_mail_cache_ctx *ctx);
+void test_mail_cache_index_sync(struct test_mail_cache_ctx *ctx);
+void test_mail_cache_view_sync(struct test_mail_cache_ctx *ctx);
+void test_mail_cache_purge(void);
+void test_mail_cache_add_mail(struct test_mail_cache_ctx *ctx,
+ unsigned int cache_field_idx,
+ const char *cache_data);
+void test_mail_cache_add_field(struct test_mail_cache_ctx *ctx, uint32_t seq,
+ unsigned int cache_field_idx,
+ const char *cache_data);
+void test_mail_cache_update_day_first_uid7(struct test_mail_cache_ctx *ctx,
+ uint32_t first_new_uid);
+
+#endif
diff --git a/src/lib-index/test-mail-index-map.c b/src/lib-index/test-mail-index-map.c
new file mode 100644
index 0000000..0b0b3ad
--- /dev/null
+++ b/src/lib-index/test-mail-index-map.c
@@ -0,0 +1,57 @@
+/* Copyright (c) 2016-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "test-common.h"
+#include "mail-index-private.h"
+#include "mail-index-modseq.h"
+#include "mail-index-transaction-private.h"
+
+static void test_mail_index_map_lookup_seq_range_count(unsigned int messages_count)
+{
+ struct mail_index_record_map rec_map;
+ struct mail_index_map map;
+ uint32_t seq, first_uid, last_uid, first_seq, last_seq, max_uid;
+
+ i_zero(&map);
+ i_zero(&rec_map);
+ map.rec_map = &rec_map;
+ map.hdr.messages_count = messages_count;
+ map.hdr.record_size = sizeof(struct mail_index_record);
+ rec_map.records_count = map.hdr.messages_count;
+ rec_map.records = i_new(struct mail_index_record, map.hdr.messages_count);
+
+ for (seq = 1; seq <= map.hdr.messages_count; seq++)
+ MAIL_INDEX_REC_AT_SEQ(&map, seq)->uid = seq*2;
+ max_uid = (seq-1)*2;
+ map.hdr.next_uid = max_uid + 1;
+
+ for (first_uid = 2; first_uid <= max_uid; first_uid++) {
+ for (last_uid = first_uid; last_uid <= max_uid; last_uid++) {
+ if (first_uid == last_uid && first_uid%2 != 0)
+ continue;
+ mail_index_map_lookup_seq_range(&map, first_uid, last_uid, &first_seq, &last_seq);
+ test_assert((first_uid+1)/2 == first_seq && last_uid/2 == last_seq);
+ }
+ }
+ i_free(rec_map.records);
+}
+
+static void test_mail_index_map_lookup_seq_range(void)
+{
+ unsigned int i;
+
+ test_begin("mail index map lookup seq range");
+ for (i = 1; i < 20; i++)
+ test_mail_index_map_lookup_seq_range_count(i);
+ test_end();
+}
+
+int main(void)
+{
+ static void (*const test_functions[])(void) = {
+ test_mail_index_map_lookup_seq_range,
+ NULL
+ };
+ return test_run(test_functions);
+}
diff --git a/src/lib-index/test-mail-index-modseq.c b/src/lib-index/test-mail-index-modseq.c
new file mode 100644
index 0000000..4a2d524
--- /dev/null
+++ b/src/lib-index/test-mail-index-modseq.c
@@ -0,0 +1,77 @@
+/* Copyright (c) 2016-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "test-common.h"
+#include "test-mail-index.h"
+#include "mail-index-modseq.h"
+#include "mail-transaction-log-private.h"
+
+static void test_mail_index_modseq_get_next_log_offset(void)
+{
+ static const struct {
+ uint32_t log_seq;
+ uoff_t log_offset;
+ } tests[] = {
+ { 0, 0 },
+ { 2, 40 },
+ { 2, 148 },
+ { 2, 164 },
+ { 3, 40 },
+ { 3, 56 },
+ { 3, 72 },
+ { 3, 88 },
+ };
+ struct mail_index *index;
+ struct mail_index_view *view, *view2;
+ struct mail_index_transaction *trans;
+ uint32_t seq, uid;
+
+ test_begin("mail_transaction_log_file_get_modseq_next_offset()");
+ index = test_mail_index_init();
+ view = mail_index_view_open(index);
+ mail_index_modseq_enable(index);
+
+ trans = mail_index_transaction_begin(view, 0);
+ uid = 1234;
+ mail_index_update_header(trans,
+ offsetof(struct mail_index_header, uid_validity),
+ &uid, sizeof(uid), TRUE);
+ test_assert(mail_index_transaction_commit(&trans) == 0);
+
+ for (uid = 1; uid <= 3; uid++) {
+ trans = mail_index_transaction_begin(view, 0);
+ mail_index_append(trans, uid, &seq);
+ test_assert(mail_index_transaction_commit(&trans) == 0);
+ }
+ test_assert(mail_transaction_log_file_lock(index->log->head) == 0);
+ test_assert(mail_transaction_log_rotate(index->log, FALSE) == 0);
+ mail_transaction_log_file_unlock(index->log->head, "rotating");
+ for (uid = 4; uid <= 6; uid++) {
+ trans = mail_index_transaction_begin(view, 0);
+ mail_index_append(trans, uid, &seq);
+ test_assert(mail_index_transaction_commit(&trans) == 0);
+ }
+
+ view2 = mail_index_view_open(index);
+ for (uint64_t modseq = 1; modseq <= 7; modseq++) {
+ uint32_t log_seq = 0;
+ uoff_t log_offset;
+
+ test_assert_idx(mail_index_modseq_get_next_log_offset(view2, modseq, &log_seq, &log_offset) == (tests[modseq].log_seq != 0), modseq);
+ test_assert_idx(tests[modseq].log_seq == log_seq && tests[modseq].log_offset == log_offset, modseq);
+ }
+
+ mail_index_view_close(&view);
+ mail_index_view_close(&view2);
+ test_mail_index_deinit(&index);
+ test_end();
+}
+
+int main(void)
+{
+ static void (*const test_functions[])(void) = {
+ test_mail_index_modseq_get_next_log_offset,
+ NULL
+ };
+ return test_run(test_functions);
+}
diff --git a/src/lib-index/test-mail-index-sync-ext.c b/src/lib-index/test-mail-index-sync-ext.c
new file mode 100644
index 0000000..156d50f
--- /dev/null
+++ b/src/lib-index/test-mail-index-sync-ext.c
@@ -0,0 +1,86 @@
+/* Copyright (c) 2010-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "test-common.h"
+#include "mail-transaction-log-view-private.h"
+#include "mail-index-sync-private.h"
+#include "mail-index-modseq.h"
+
+static void test_lookup_seq_range(struct mail_index_view *view ATTR_UNUSED,
+ uint32_t first_uid, uint32_t last_uid,
+ uint32_t *first_seq_r, uint32_t *last_seq_r)
+{
+ *first_seq_r = first_uid;
+ *last_seq_r = last_uid;
+}
+
+static void test_mail_index_sync_ext_atomic_inc(void)
+{
+ struct mail_index_sync_map_ctx ctx;
+ struct mail_transaction_ext_atomic_inc u;
+ struct mail_index_ext *ext;
+ void *ptr;
+
+ test_begin("mail index sync ext atomic inc");
+
+ i_zero(&ctx);
+ ctx.view = t_new(struct mail_index_view, 1);
+ ctx.view->log_view = t_new(struct mail_transaction_log_view, 1);
+ ctx.view->index = t_new(struct mail_index, 1);
+ ctx.view->index->fsck_log_head_file_seq = 10; /* silence errors */
+ ctx.view->v.lookup_seq_range = test_lookup_seq_range;
+ ctx.view->map = t_new(struct mail_index_map, 1);
+ ctx.view->map->hdr.next_uid = 2;
+ ctx.view->map->hdr.record_size = sizeof(struct mail_index_record) + 16;
+ ctx.view->map->rec_map = t_new(struct mail_index_record_map, 1);
+ ctx.view->map->rec_map->records =
+ t_malloc0(ctx.view->map->hdr.record_size);
+ t_array_init(&ctx.view->map->extensions, 4);
+ ext = array_append_space(&ctx.view->map->extensions);
+ ext->record_offset = sizeof(struct mail_index_record);
+ ptr = PTR_OFFSET(ctx.view->map->rec_map->records, ext->record_offset);
+
+ i_zero(&u);
+ test_assert(mail_index_sync_ext_atomic_inc(&ctx, &u) == -1);
+
+ u.uid = 2;
+ test_assert(mail_index_sync_ext_atomic_inc(&ctx, &u) == -1);
+
+ u.uid = 1;
+#define TEST_ATOMIC(_type, _value, _diff, _ret) \
+ { _type *n = ptr; *n = _value; } \
+ ctx.cur_ext_record_size = sizeof(_type); \
+ u.diff = _diff; \
+ test_assert(mail_index_sync_ext_atomic_inc(&ctx, &u) == _ret);
+
+#define TEST_ATOMIC_BLOCK(_type, _max) \
+ TEST_ATOMIC(_type, 1, -1, 1); \
+ TEST_ATOMIC(_type, 1, -2, -1); \
+ TEST_ATOMIC(_type, 0, -1, -1); \
+ TEST_ATOMIC(_type, 0, _max, 1); \
+ TEST_ATOMIC(_type, 1, _max, -1); \
+ TEST_ATOMIC(_type, 0, (_max+1), -1); \
+ TEST_ATOMIC(_type, _max, 1, -1); \
+ TEST_ATOMIC(_type, _max, -_max, 1); \
+ TEST_ATOMIC(_type, _max, -(_max+1), -1);
+
+ TEST_ATOMIC_BLOCK(uint8_t, 255);
+ TEST_ATOMIC_BLOCK(uint16_t, 65535);
+
+ ctx.cur_ext_record_size = 5;
+ u.diff = 0;
+ test_assert(mail_index_sync_ext_atomic_inc(&ctx, &u) == -1);
+
+ i_free(ctx.view->index->need_recreate);
+ test_end();
+}
+
+int main(void)
+{
+ static void (*const test_functions[])(void) = {
+ test_mail_index_sync_ext_atomic_inc,
+ NULL
+ };
+ return test_run(test_functions);
+}
diff --git a/src/lib-index/test-mail-index-transaction-finish.c b/src/lib-index/test-mail-index-transaction-finish.c
new file mode 100644
index 0000000..e32467e
--- /dev/null
+++ b/src/lib-index/test-mail-index-transaction-finish.c
@@ -0,0 +1,297 @@
+/* Copyright (c) 2009-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "test-common.h"
+#include "mail-index-private.h"
+#include "mail-index-modseq.h"
+#include "mail-index-transaction-private.h"
+
+
+static struct mail_index_record recs[20];
+static uint64_t modseqs[N_ELEMENTS(recs)];
+
+bool mail_index_map_get_ext_idx(struct mail_index_map *map ATTR_UNUSED,
+ uint32_t ext_id ATTR_UNUSED,
+ uint32_t *idx_r ATTR_UNUSED) { return FALSE; }
+void mail_index_ext_set_reset_id(struct mail_index_transaction *t ATTR_UNUSED,
+ uint32_t ext_id ATTR_UNUSED,
+ uint32_t reset_id ATTR_UNUSED) { }
+void mail_index_transaction_set_log_updates(struct mail_index_transaction *t ATTR_UNUSED) { }
+void mail_index_update_day_headers(struct mail_index_transaction *t ATTR_UNUSED, time_t day_stamp ATTR_UNUSED) {}
+bool mail_index_cancel_flag_updates(struct mail_index_transaction *t ATTR_UNUSED,
+ uint32_t seq ATTR_UNUSED) { return TRUE; }
+bool mail_index_cancel_keyword_updates(struct mail_index_transaction *t ATTR_UNUSED,
+ uint32_t seq ATTR_UNUSED) { return TRUE; }
+void mail_index_transaction_sort_appends(struct mail_index_transaction *t ATTR_UNUSED) {}
+int mail_index_map(struct mail_index *index ATTR_UNUSED,
+ enum mail_index_sync_handler_type type ATTR_UNUSED) { return 1; }
+void mail_index_update_modseq(struct mail_index_transaction *t ATTR_UNUSED, uint32_t seq ATTR_UNUSED,
+ uint64_t min_modseq ATTR_UNUSED) {}
+
+const struct mail_index_record *
+mail_index_lookup(struct mail_index_view *view ATTR_UNUSED, uint32_t seq)
+{
+ i_assert(seq < N_ELEMENTS(recs));
+ return &recs[seq];
+}
+
+struct mail_index_record *
+mail_index_transaction_lookup(struct mail_index_transaction *t ATTR_UNUSED,
+ uint32_t seq)
+{
+ i_assert(seq < N_ELEMENTS(recs));
+ return &recs[seq];
+}
+
+uint64_t mail_index_modseq_lookup(struct mail_index_view *view ATTR_UNUSED,
+ uint32_t seq)
+{
+ i_assert(seq < N_ELEMENTS(modseqs));
+ return modseqs[seq];
+}
+
+uint64_t mail_index_modseq_get_highest(struct mail_index_view *view ATTR_UNUSED)
+{
+ return modseqs[0];
+}
+
+#define MAIL_INDEX_TRANSACTION_FINISH(t, n_so_far) \
+ for (unsigned int sofar = 0; sofar < n_so_far; sofar++) \
+ mail_index_transaction_finish_so_far(t); \
+ mail_index_transaction_finish(t);
+
+static void
+test_mail_index_transaction_finish_flag_updates(unsigned int n_so_far)
+{
+ struct mail_index_transaction *t;
+ const struct mail_index_flag_update *updates;
+ struct mail_index_flag_update u;
+ unsigned int count;
+
+ t = t_new(struct mail_index_transaction, 1);
+ t->drop_unnecessary_flag_updates = TRUE;
+
+ i_zero(&u);
+ u.add_flags = MAIL_SEEN; u.remove_flags = MAIL_DRAFT;
+
+ test_begin(t_strdup_printf("mail index transaction finish flag updates n_so_far=%u", n_so_far));
+
+ /* test fast path: all changed */
+ t_array_init(&t->updates, 10);
+ u.uid1 = 1; u.uid2 = 2;
+ array_push_back(&t->updates, &u);
+ u.uid1 = 4; u.uid2 = 5;
+ array_push_back(&t->updates, &u);
+ MAIL_INDEX_TRANSACTION_FINISH(t, n_so_far);
+
+ updates = array_get(&t->updates, &count);
+ test_assert(count == 4);
+ test_assert(updates[0].uid1 == 1*2 && updates[0].uid2 == 1*2);
+ test_assert(updates[1].uid1 == 2*2 && updates[1].uid2 == 2*2);
+ test_assert(updates[2].uid1 == 4*2 && updates[2].uid2 == 4*2);
+ test_assert(updates[3].uid1 == 5*2 && updates[3].uid2 == 5*2);
+
+ /* nothing changed */
+ t_array_init(&t->updates, 10);
+ u.uid1 = 1; u.uid2 = 2;
+ array_push_back(&t->updates, &u);
+ u.uid1 = 4; u.uid2 = 5;
+ array_push_back(&t->updates, &u);
+ recs[1].flags = MAIL_SEEN;
+ recs[2].flags = MAIL_SEEN;
+ recs[4].flags = MAIL_SEEN;
+ recs[5].flags = MAIL_SEEN;
+ MAIL_INDEX_TRANSACTION_FINISH(t, n_so_far);
+ test_assert(!array_is_created(&t->updates));
+
+ /* some changes */
+ t_array_init(&t->updates, 10);
+ u.uid1 = 2; u.uid2 = 3;
+ array_push_back(&t->updates, &u);
+ u.uid1 = 5; u.uid2 = 6;
+ array_push_back(&t->updates, &u);
+ MAIL_INDEX_TRANSACTION_FINISH(t, n_so_far);
+
+ updates = array_get(&t->updates, &count);
+ test_assert(count == 2);
+ test_assert(updates[0].uid1 == 3*2 && updates[0].uid2 == 3*2);
+ test_assert(updates[1].uid1 == 6*2 && updates[1].uid2 == 6*2);
+
+ test_end();
+}
+
+static void
+test_mail_index_transaction_finish_check_conflicts(unsigned int n_so_far)
+{
+ struct mail_index_transaction *t;
+ const struct seq_range *conflicts;
+ ARRAY_TYPE(seq_range) conflict_seqs = ARRAY_INIT;
+ unsigned int count;
+
+ t = t_new(struct mail_index_transaction, 1);
+ t->view = t_new(struct mail_index_view, 1);
+ t->min_flagupdate_seq = 5;
+ t->max_flagupdate_seq = 8;
+ t->conflict_seqs = &conflict_seqs;
+
+ modseqs[0] = 1234;
+ modseqs[5] = 5;
+ modseqs[6] = 8;
+ modseqs[7] = 6;
+ modseqs[8] = 7;
+
+ test_begin(t_strdup_printf("mail index transaction finish check conflicts n_so_far=%u", n_so_far));
+
+ /* fast path: no conflicts */
+ t->max_modseq = 1234;
+ MAIL_INDEX_TRANSACTION_FINISH(t, n_so_far);
+ test_assert(!array_is_created(&conflict_seqs));
+
+ /* try some conflicts */
+ t->max_modseq = 6;
+ MAIL_INDEX_TRANSACTION_FINISH(t, n_so_far);
+
+ i_assert(array_is_created(&conflict_seqs));
+
+ conflicts = array_get(&conflict_seqs, &count);
+ test_assert(count == 2);
+ test_assert(conflicts[0].seq1 == 6 && conflicts[0].seq2 == 6);
+ test_assert(conflicts[1].seq1 == 8 && conflicts[1].seq2 == 8);
+
+ test_end();
+ array_free(t->conflict_seqs);
+}
+
+static void
+test_mail_index_transaction_finish_modseq_updates(unsigned int n_so_far)
+{
+ struct mail_index_transaction *t;
+ const struct mail_transaction_modseq_update *ups;
+ struct mail_transaction_modseq_update u;
+ unsigned int count;
+
+ t = t_new(struct mail_index_transaction, 1);
+
+ test_begin(t_strdup_printf("mail index transaction finish modseq updates n_so_far=%u", n_so_far));
+
+ t_array_init(&t->modseq_updates, 10);
+ u.modseq_low32 = 1234567890;
+ u.modseq_high32 = 987654321;
+ u.uid = 1; array_push_back(&t->modseq_updates, &u);
+ u.modseq_low32++;
+ u.modseq_high32++;
+ u.uid = 2; array_push_back(&t->modseq_updates, &u);
+ u.modseq_low32++;
+ u.modseq_high32++;
+ u.uid = 5; array_push_back(&t->modseq_updates, &u);
+ u.modseq_low32 = 1234;
+ u.modseq_high32 = 0;
+ u.uid = 2; array_push_back(&t->modseq_updates, &u);
+
+ MAIL_INDEX_TRANSACTION_FINISH(t, n_so_far);
+
+ ups = array_get(&t->modseq_updates, &count);
+ test_assert(count == 4);
+
+ test_assert(ups[0].uid == 1*2);
+ test_assert(ups[0].modseq_low32 == 1234567890 &&
+ ups[0].modseq_high32 == 987654321);
+ test_assert(ups[1].uid == 2*2);
+ test_assert(ups[1].modseq_low32 == 1234567891 &&
+ ups[1].modseq_high32 == 987654322);
+ test_assert(ups[2].uid == 5*2);
+ test_assert(ups[2].modseq_low32 == 1234567892 &&
+ ups[2].modseq_high32 == 987654323);
+ test_assert(ups[3].uid == 2*2);
+ test_assert(ups[3].modseq_low32 == 1234 &&
+ ups[3].modseq_high32 == 0);
+ test_end();
+}
+
+static void
+test_mail_index_transaction_finish_expunges(unsigned int n_so_far)
+{
+ struct mail_index_transaction *t;
+ guid_128_t guid1, guid2, guid3;
+ const struct mail_transaction_expunge_guid *expunges;
+ struct mail_transaction_expunge_guid expunge;
+ unsigned int i, count;
+
+ for (i = 0; i < sizeof(guid2); i++) {
+ guid1[i] = i + 1;
+ guid2[i] = i ^ 0xff;
+ guid3[i] = i + 0x80;
+ }
+
+ recs[1].uid = 12;
+ recs[2].uid = 15;
+ recs[3].uid = 18;
+
+ t = t_new(struct mail_index_transaction, 1);
+ t->expunges_nonsorted = TRUE;
+
+ test_begin(t_strdup_printf("mail index transaction finish expunges n_so_far=%u", n_so_far));
+
+ t_array_init(&t->expunges, 3);
+ expunge.uid = 2;
+ memcpy(expunge.guid_128, guid2, sizeof(expunge.guid_128));
+ array_push_back(&t->expunges, &expunge);
+ array_push_back(&t->expunges, &expunge);
+ expunge.uid = 1;
+ memcpy(expunge.guid_128, guid1, sizeof(expunge.guid_128));
+ array_push_back(&t->expunges, &expunge);
+ array_push_back(&t->expunges, &expunge);
+ expunge.uid = 3;
+ memcpy(expunge.guid_128, guid3, sizeof(expunge.guid_128));
+ array_push_back(&t->expunges, &expunge);
+ array_push_back(&t->expunges, &expunge);
+
+ MAIL_INDEX_TRANSACTION_FINISH(t, n_so_far);
+
+ expunges = array_get(&t->expunges, &count);
+ test_assert(count == 3);
+ test_assert(expunges[0].uid == 12);
+ test_assert(memcmp(expunges[0].guid_128, guid1, sizeof(guid1)) == 0);
+ test_assert(expunges[1].uid == 15);
+ test_assert(memcmp(expunges[1].guid_128, guid2, sizeof(guid2)) == 0);
+ test_assert(expunges[2].uid == 18);
+ test_assert(memcmp(expunges[2].guid_128, guid3, sizeof(guid3)) == 0);
+ test_end();
+}
+
+static void test_state_reset(void)
+{
+ memset(recs, 0, sizeof(recs));
+ memset(modseqs, 0, sizeof(modseqs));
+ for (unsigned int n = 1; n < N_ELEMENTS(recs); n++)
+ recs[n].uid = n*2;
+}
+
+static void test_mail_index_transaction_finish(void)
+{
+ void (*const test_finish_functions[])(unsigned int) = {
+ test_mail_index_transaction_finish_flag_updates,
+ test_mail_index_transaction_finish_check_conflicts,
+ test_mail_index_transaction_finish_modseq_updates,
+ test_mail_index_transaction_finish_expunges,
+ };
+ unsigned int i, j;
+
+ for (i = 0; i < N_ELEMENTS(test_finish_functions); i++) {
+ for (j = 0; j < 3; j++) {
+ test_state_reset();
+ test_finish_functions[i](j);
+ }
+ }
+}
+
+int main(void)
+{
+ static void (*const test_functions[])(void) = {
+ test_mail_index_transaction_finish,
+ NULL
+ };
+
+ return test_run(test_functions);
+}
diff --git a/src/lib-index/test-mail-index-transaction-update.c b/src/lib-index/test-mail-index-transaction-update.c
new file mode 100644
index 0000000..cdfa951
--- /dev/null
+++ b/src/lib-index/test-mail-index-transaction-update.c
@@ -0,0 +1,683 @@
+/* Copyright (c) 2009-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "env-util.h"
+#include "test-common.h"
+#include "mail-index-private.h"
+#include "mail-index-transaction-private.h"
+
+#include <time.h>
+
+static struct mail_index_header hdr;
+static struct mail_index_record rec;
+
+const struct mail_index_header *
+mail_index_get_header(struct mail_index_view *view ATTR_UNUSED)
+{
+ return &hdr;
+}
+
+const struct mail_index_record *
+mail_index_lookup(struct mail_index_view *view ATTR_UNUSED,
+ uint32_t seq ATTR_UNUSED)
+{
+ return &rec;
+}
+
+void mail_index_lookup_keywords(struct mail_index_view *view ATTR_UNUSED,
+ uint32_t seq ATTR_UNUSED,
+ ARRAY_TYPE(keyword_indexes) *keyword_idx ATTR_UNUSED)
+{
+ array_clear(keyword_idx);
+}
+
+bool mail_index_map_get_ext_idx(struct mail_index_map *map ATTR_UNUSED,
+ uint32_t ext_id ATTR_UNUSED,
+ uint32_t *idx_r ATTR_UNUSED)
+{
+ return FALSE;
+}
+
+uint32_t mail_index_view_get_messages_count(struct mail_index_view *view ATTR_UNUSED)
+{
+ return hdr.messages_count;
+}
+
+void mail_index_transaction_lookup_latest_keywords(struct mail_index_transaction *t ATTR_UNUSED,
+ uint32_t seq ATTR_UNUSED,
+ ARRAY_TYPE(keyword_indexes) *keywords ATTR_UNUSED)
+{
+}
+
+struct mail_keywords *
+mail_index_keywords_create_from_indexes(struct mail_index *index ATTR_UNUSED,
+ const ARRAY_TYPE(keyword_indexes)
+ *keyword_indexes ATTR_UNUSED)
+{
+ return NULL;
+}
+
+void mail_index_keywords_unref(struct mail_keywords **keywords ATTR_UNUSED)
+{
+}
+
+static struct mail_index_transaction *
+mail_index_transaction_new(void)
+{
+ struct mail_index_transaction *t;
+
+ t = t_new(struct mail_index_transaction, 1);
+ t->first_new_seq = hdr.messages_count + 1;
+ return t;
+}
+static void mail_index_transaction_cleanup(struct mail_index_transaction *t)
+{
+ if (array_is_created(&t->appends))
+ array_free(&t->appends);
+ if (array_is_created(&t->updates))
+ array_free(&t->updates);
+ if (array_is_created(&t->modseq_updates))
+ array_free(&t->modseq_updates);
+ if (array_is_created(&t->expunges))
+ array_free(&t->expunges);
+}
+
+static void test_mail_index_append(void)
+{
+ struct mail_index_transaction *t;
+ const struct mail_index_record *appends;
+ ARRAY_TYPE(seq_range) saved_uids_arr;
+ const struct seq_range *saved_uids;
+ unsigned int count;
+ uint32_t seq;
+
+ hdr.messages_count = 4;
+ t = mail_index_transaction_new();
+
+ test_begin("mail index append");
+ mail_index_append(t, 0, &seq);
+ test_assert(t->log_updates);
+ test_assert(seq == 5);
+ mail_index_append(t, 0, &seq);
+ test_assert(seq == 6);
+ test_assert(!t->appends_nonsorted);
+
+ t_array_init(&saved_uids_arr, 128);
+ mail_index_append_finish_uids(t, 123, &saved_uids_arr);
+ saved_uids = array_get(&saved_uids_arr, &count);
+ test_assert(count == 1);
+ test_assert(saved_uids[0].seq1 == 123 && saved_uids[0].seq2 == 124);
+
+ appends = array_get(&t->appends, &count);
+ test_assert(appends[0].uid == 123);
+ test_assert(appends[0].flags == 0);
+ test_assert(appends[1].uid == 124);
+ test_assert(appends[1].flags == 0);
+ test_end();
+ mail_index_transaction_cleanup(t);
+
+ /* test with some uids */
+ t = mail_index_transaction_new();
+
+ test_begin("mail index append with uids");
+ mail_index_append(t, 0, &seq);
+ test_assert(seq == 5);
+ mail_index_append(t, 126, &seq);
+ test_assert(seq == 6);
+ test_assert(!t->appends_nonsorted);
+ mail_index_append(t, 124, &seq);
+ test_assert(seq == 7);
+ test_assert(t->appends_nonsorted);
+ mail_index_append(t, 0, &seq);
+ test_assert(seq == 8);
+ mail_index_append(t, 128, &seq);
+ test_assert(seq == 9);
+ test_assert(t->highest_append_uid == 128);
+
+ mail_index_append_finish_uids(t, 125, &saved_uids_arr);
+ saved_uids = array_get(&saved_uids_arr, &count);
+ test_assert(count == 4);
+ test_assert(saved_uids[0].seq1 == 129 && saved_uids[0].seq2 == 129);
+ test_assert(saved_uids[1].seq1 == 126 && saved_uids[1].seq2 == 126);
+ test_assert(saved_uids[2].seq1 == 130 && saved_uids[2].seq2 == 131);
+ test_assert(saved_uids[3].seq1 == 128 && saved_uids[3].seq2 == 128);
+
+ appends = array_get(&t->appends, &count);
+ test_assert(count == 5);
+ test_assert(appends[0].uid == 129);
+ test_assert(appends[1].uid == 126);
+ test_assert(appends[2].uid == 130);
+ test_assert(appends[3].uid == 131);
+ test_assert(appends[4].uid == 128);
+ test_end();
+
+ mail_index_transaction_cleanup(t);
+}
+
+static void test_mail_index_flag_update_fastpath(void)
+{
+ struct mail_index_transaction *t;
+ const struct mail_index_flag_update *updates;
+ unsigned int count;
+
+ hdr.messages_count = 20;
+ t = mail_index_transaction_new();
+
+ test_begin("mail index flag update fast paths");
+
+ mail_index_update_flags_range(t, 13, 14, MODIFY_REPLACE,
+ MAIL_DELETED);
+ test_assert(t->last_update_idx == 0);
+ test_assert(array_count(&t->updates) == 1);
+
+ mail_index_update_flags_range(t, 15, 15, MODIFY_REPLACE,
+ MAIL_DELETED);
+ test_assert(t->last_update_idx == 0);
+ test_assert(array_count(&t->updates) == 1);
+
+ mail_index_update_flags_range(t, 16, 16, MODIFY_ADD,
+ MAIL_DELETED);
+ test_assert(t->last_update_idx == 1);
+ test_assert(array_count(&t->updates) == 2);
+
+ updates = array_get(&t->updates, &count);
+ test_assert(updates[0].uid1 == 13);
+ test_assert(updates[0].uid2 == 15);
+ test_assert(updates[0].add_flags == MAIL_DELETED);
+ test_assert(updates[0].remove_flags ==
+ (MAIL_ANSWERED | MAIL_FLAGGED | MAIL_SEEN | MAIL_DRAFT));
+ test_assert(updates[1].uid1 == 16);
+ test_assert(updates[1].uid2 == 16);
+ test_assert(updates[1].add_flags == MAIL_DELETED);
+ test_assert(updates[1].remove_flags == 0);
+ test_assert(!t->log_updates);
+ test_end();
+
+ mail_index_transaction_cleanup(t);
+}
+
+static void test_mail_index_flag_update_simple_merges(void)
+{
+ struct mail_index_transaction *t;
+ const struct mail_index_flag_update *updates;
+ unsigned int count;
+
+ hdr.messages_count = 20;
+ t = mail_index_transaction_new();
+
+ test_begin("mail index flag update simple merges");
+
+ mail_index_update_flags_range(t, 6, 8, MODIFY_ADD,
+ MAIL_FLAGGED);
+ test_assert(t->last_update_idx == 0);
+ mail_index_update_flags_range(t, 5, 6, MODIFY_ADD,
+ MAIL_FLAGGED);
+ test_assert(t->last_update_idx == 0);
+ mail_index_update_flags_range(t, 4, 4, MODIFY_ADD,
+ MAIL_FLAGGED);
+ test_assert(t->last_update_idx == 0);
+ mail_index_update_flags_range(t, 7, 9, MODIFY_ADD,
+ MAIL_FLAGGED);
+ test_assert(t->last_update_idx == 0);
+ mail_index_update_flags_range(t, 10, 10, MODIFY_ADD,
+ MAIL_FLAGGED);
+ updates = array_get(&t->updates, &count);
+ test_assert(count == 1);
+ test_assert(updates[0].uid1 == 4);
+ test_assert(updates[0].uid2 == 10);
+ test_assert(updates[0].add_flags == MAIL_FLAGGED);
+ test_assert(updates[0].remove_flags == 0);
+
+ mail_index_update_flags_range(t, 12, 12, MODIFY_ADD,
+ MAIL_FLAGGED);
+ mail_index_update_flags_range(t, 11, 11, MODIFY_ADD,
+ MAIL_FLAGGED);
+ updates = array_get(&t->updates, &count);
+ test_assert(count == 1);
+ test_assert(updates[0].uid1 == 4);
+ test_assert(updates[0].uid2 == 12);
+ test_end();
+
+ mail_index_transaction_cleanup(t);
+}
+
+static void test_mail_index_flag_update_complex_merges(void)
+{
+ struct mail_index_transaction *t;
+ const struct mail_index_flag_update *updates;
+ unsigned int count;
+
+ hdr.messages_count = 20;
+ t = mail_index_transaction_new();
+
+ test_begin("mail index flag update complex merges");
+
+ mail_index_update_flags_range(t, 6, 8, MODIFY_REPLACE,
+ MAIL_SEEN);
+ mail_index_update_flags_range(t, 3, 6, MODIFY_ADD,
+ MAIL_FLAGGED);
+ mail_index_update_flags_range(t, 5, 7, MODIFY_ADD,
+ MAIL_DRAFT);
+ mail_index_update_flags_range(t, 6, 6, MODIFY_REPLACE,
+ MAIL_SEEN | MAIL_ANSWERED);
+ mail_index_update_flags_range(t, 5, 10, MODIFY_REMOVE,
+ MAIL_ANSWERED);
+ mail_index_update_flags_range(t, 7, 12, MODIFY_ADD,
+ MAIL_DELETED);
+
+ updates = array_get(&t->updates, &count);
+ test_assert(count == 7);
+ test_assert(updates[0].uid1 == 3);
+ test_assert(updates[0].uid2 == 4);
+ test_assert(updates[0].add_flags == MAIL_FLAGGED);
+ test_assert(updates[0].remove_flags == 0);
+ test_assert(updates[1].uid1 == 5);
+ test_assert(updates[1].uid2 == 5);
+ test_assert(updates[1].add_flags == (MAIL_DRAFT | MAIL_FLAGGED));
+ test_assert(updates[1].remove_flags == MAIL_ANSWERED);
+ test_assert(updates[2].uid1 == 6);
+ test_assert(updates[2].uid2 == 6);
+ test_assert(updates[2].add_flags == MAIL_SEEN);
+ test_assert(updates[2].remove_flags == (MAIL_ANSWERED | MAIL_FLAGGED | MAIL_DELETED | MAIL_DRAFT));
+ test_assert(updates[3].uid1 == 7);
+ test_assert(updates[3].uid2 == 7);
+ test_assert(updates[3].add_flags == (MAIL_SEEN | MAIL_DRAFT | MAIL_DELETED));
+ test_assert(updates[3].remove_flags == (MAIL_ANSWERED | MAIL_FLAGGED));
+ test_assert(updates[4].uid1 == 8);
+ test_assert(updates[4].uid2 == 8);
+ test_assert(updates[4].add_flags == (MAIL_SEEN | MAIL_DELETED));
+ test_assert(updates[4].remove_flags == (MAIL_ANSWERED | MAIL_FLAGGED | MAIL_DRAFT));
+ test_assert(updates[5].uid1 == 9);
+ test_assert(updates[5].uid2 == 10);
+ test_assert(updates[5].add_flags == MAIL_DELETED);
+ test_assert(updates[5].remove_flags == MAIL_ANSWERED);
+ test_assert(updates[6].uid1 == 11);
+ test_assert(updates[6].uid2 == 12);
+ test_assert(updates[6].add_flags == MAIL_DELETED);
+ test_assert(updates[6].remove_flags == 0);
+
+ test_end();
+
+ mail_index_transaction_cleanup(t);
+}
+
+static void
+flags_array_check(struct mail_index_transaction *t,
+ const enum mail_flags *flags, unsigned int msg_count)
+{
+ const struct mail_index_flag_update *updates;
+ unsigned int i, count, seq;
+
+ if (array_is_created(&t->updates))
+ updates = array_get(&t->updates, &count);
+ else {
+ updates = NULL;
+ count = 0;
+ }
+ for (seq = 1, i = 0; i < count; i++) {
+ if (i > 0) {
+ test_assert(updates[i-1].uid2 < updates[i].uid1);
+ test_assert(updates[i-1].uid2 + 1 != updates[i].uid1 ||
+ updates[i-1].add_flags != updates[i].add_flags ||
+ updates[i-1].remove_flags != updates[i].remove_flags);
+ }
+ for (; seq != updates[i].uid1; seq++)
+ test_assert(flags[seq] == 0);
+ for (; seq <= updates[i].uid2; seq++)
+ test_assert(flags[seq] == updates[i].add_flags);
+ }
+ for (; seq <= msg_count; seq++)
+ test_assert(flags[seq] == 0);
+}
+
+static void test_mail_index_flag_update_random(void)
+{
+ struct mail_index_transaction *t;
+ unsigned int r, seq1, seq2, seq;
+ enum mail_flags *flags, change;
+ enum modify_type modify_type;
+
+ hdr.messages_count = 20;
+ t = mail_index_transaction_new();
+
+ test_begin("mail index flag update random");
+
+ flags = t_new(enum mail_flags, hdr.messages_count + 1);
+ for (r = 0; r < 1000; r++) {
+ change = i_rand_limit(MAIL_FLAGS_NONRECENT + 1);
+ seq1 = i_rand_minmax(1, hdr.messages_count);
+ seq2 = seq1 == hdr.messages_count ? seq1 :
+ i_rand_minmax(seq1, hdr.messages_count);
+
+ switch (i_rand_limit(3)) {
+ case 0:
+ modify_type = MODIFY_ADD;
+ for (seq = seq1; seq <= seq2; seq++)
+ flags[seq] |= change;
+ break;
+ case 1:
+ modify_type = MODIFY_REMOVE;
+ for (seq = seq1; seq <= seq2; seq++)
+ flags[seq] &= ENUM_NEGATE(change);
+ break;
+ case 2:
+ modify_type = MODIFY_REPLACE;
+ for (seq = seq1; seq <= seq2; seq++)
+ flags[seq] = change;
+ break;
+ default:
+ i_unreached();
+ }
+ mail_index_update_flags_range(t, seq1, seq2, modify_type,
+ change);
+ flags_array_check(t, flags, hdr.messages_count);
+ }
+ test_end();
+
+ mail_index_transaction_cleanup(t);
+}
+
+static void test_mail_index_cancel_flag_updates(void)
+{
+ struct mail_index_transaction *t;
+ const struct mail_index_flag_update *updates;
+ unsigned int count;
+
+ hdr.messages_count = 20;
+ t = mail_index_transaction_new();
+
+ test_begin("mail index cancel flag updates");
+
+ mail_index_update_flags_range(t, 5, 7, MODIFY_REPLACE, 0);
+ updates = array_get(&t->updates, &count);
+ test_assert(count == 1);
+ test_assert(updates[0].uid1 == 5 && updates[0].uid2 == 7);
+ test_assert(mail_index_cancel_flag_updates(t, 5));
+ test_assert(updates[0].uid1 == 6 && updates[0].uid2 == 7);
+ test_assert(mail_index_cancel_flag_updates(t, 7));
+ test_assert(updates[0].uid1 == 6 && updates[0].uid2 == 6);
+ test_assert(mail_index_cancel_flag_updates(t, 6));
+ test_assert(!array_is_created(&t->updates));
+
+ mail_index_update_flags_range(t, 5, 7, MODIFY_REPLACE, 0);
+ test_assert(mail_index_cancel_flag_updates(t, 6));
+ updates = array_get(&t->updates, &count);
+ test_assert(count == 2);
+ test_assert(updates[0].uid1 == 5 && updates[0].uid2 == 5);
+ test_assert(updates[1].uid1 == 7 && updates[1].uid2 == 7);
+
+ test_end();
+
+ mail_index_transaction_cleanup(t);
+}
+
+static void test_mail_index_flag_update_appends(void)
+{
+ struct mail_index_transaction *t;
+ const struct mail_index_record *appends;
+ const struct mail_index_flag_update *updates;
+ unsigned int count;
+ uint32_t seq;
+
+ hdr.messages_count = 4;
+ t = mail_index_transaction_new();
+
+ test_begin("mail index flag update appends");
+ mail_index_append(t, 0, &seq);
+ test_assert(seq == 5);
+ mail_index_append(t, 0, &seq);
+ test_assert(seq == 6);
+ mail_index_append(t, 0, &seq);
+ test_assert(seq == 7);
+
+ mail_index_update_flags_range(t, 5, 6, MODIFY_REPLACE,
+ MAIL_SEEN | MAIL_FLAGGED);
+ mail_index_update_flags_range(t, 6, 7, MODIFY_ADD,
+ MAIL_DRAFT | MAIL_FLAGGED);
+ mail_index_update_flags_range(t, 5, 7, MODIFY_REMOVE,
+ MAIL_FLAGGED);
+
+ appends = array_get(&t->appends, &count);
+ test_assert(count == 3);
+ test_assert(appends[0].flags == MAIL_SEEN);
+ test_assert(appends[1].flags == (MAIL_SEEN | MAIL_DRAFT));
+ test_assert(appends[2].flags == MAIL_DRAFT);
+
+ /* mixed existing/appends */
+ mail_index_update_flags_range(t, 4, 5, MODIFY_ADD,
+ MAIL_ANSWERED);
+ test_assert(appends[0].flags == (MAIL_SEEN | MAIL_ANSWERED));
+
+ updates = array_get(&t->updates, &count);
+ test_assert(count == 1);
+ test_assert(updates[0].uid1 == 4);
+ test_assert(updates[0].uid2 == 4);
+ test_assert(updates[0].add_flags == MAIL_ANSWERED);
+ test_end();
+
+ mail_index_transaction_cleanup(t);
+}
+
+static bool test_flag_update_pos(struct mail_index_transaction *t,
+ uint32_t seq, unsigned int idx)
+{
+ unsigned int i, j, count;
+
+ count = array_count(&t->updates);
+ for (i = 0; i < idx; i++) {
+ for (j = idx + 1; j <= count; j++) {
+ if (mail_index_transaction_get_flag_update_pos(t, i, j, seq) != idx) {
+ test_assert(FALSE);
+ return FALSE;
+ }
+ }
+ }
+ return TRUE;
+}
+
+static void test_mail_index_transaction_get_flag_update_pos(void)
+{
+ struct mail_index_transaction *t;
+
+ test_begin("mail index transaction get flag update pos");
+
+ hdr.messages_count = 10;
+ t = mail_index_transaction_new();
+ mail_index_update_flags_range(t, 1, 1, MODIFY_REPLACE, 0);
+ mail_index_update_flags_range(t, 3, 4, MODIFY_REPLACE, 0);
+ mail_index_update_flags_range(t, 6, 7, MODIFY_REPLACE, 0);
+ mail_index_update_flags_range(t, 9, 10, MODIFY_REPLACE, 0);
+
+ test_assert(test_flag_update_pos(t, 1, 0));
+ test_assert(test_flag_update_pos(t, 2, 1));
+ test_assert(test_flag_update_pos(t, 3, 1));
+ test_assert(test_flag_update_pos(t, 4, 1));
+ test_assert(test_flag_update_pos(t, 5, 2));
+ test_assert(test_flag_update_pos(t, 6, 2));
+ test_assert(test_flag_update_pos(t, 7, 2));
+ test_assert(test_flag_update_pos(t, 8, 3));
+ test_assert(test_flag_update_pos(t, 9, 3));
+ test_assert(test_flag_update_pos(t, 10, 3));
+ test_assert(test_flag_update_pos(t, 11, 4));
+ test_assert(test_flag_update_pos(t, 12, 4));
+ test_end();
+
+ mail_index_transaction_cleanup(t);
+}
+
+static void test_mail_index_modseq_update(void)
+{
+ struct mail_index_transaction *t;
+ const struct mail_transaction_modseq_update *ups;
+ unsigned int count;
+
+ test_begin("mail index modseq update");
+
+ hdr.messages_count = 10;
+ t = mail_index_transaction_new();
+
+ mail_index_update_modseq(t, 4, 0x8234fefa02747429ULL);
+ mail_index_update_modseq(t, 6, 0x1234567890abcdefULL);
+ mail_index_update_modseq(t, 2, 0xfeed);
+ mail_index_update_modseq(t, 4, 2);
+ /* modseq=1 updates are ignored: */
+ mail_index_update_modseq(t, 5, 1);
+ mail_index_update_modseq(t, 6, 1);
+
+ ups = array_get(&t->modseq_updates, &count);
+ test_assert(count == 4);
+ test_assert(ups[0].uid == 4 &&
+ ups[0].modseq_high32 == 0x8234fefa &&
+ ups[0].modseq_low32 == 0x02747429);
+ test_assert(ups[1].uid == 6 &&
+ ups[1].modseq_high32 == 0x12345678 &&
+ ups[1].modseq_low32 == 0x90abcdef);
+ test_assert(ups[2].uid == 2 &&
+ ups[2].modseq_high32 == 0 &&
+ ups[2].modseq_low32 == 0xfeed);
+ test_assert(ups[3].uid == 4 &&
+ ups[3].modseq_high32 == 0 &&
+ ups[3].modseq_low32 == 2);
+ test_end();
+
+ mail_index_transaction_cleanup(t);
+}
+
+static void test_mail_index_expunge(void)
+{
+ static guid_128_t empty_guid = { 0, };
+ struct mail_index_transaction *t;
+ const struct mail_transaction_expunge_guid *expunges;
+ guid_128_t guid2, guid3, guid4;
+ unsigned int i, count;
+
+ test_begin("mail index expunge");
+
+ hdr.messages_count = 10;
+ t = mail_index_transaction_new();
+ for (i = 0; i < sizeof(guid2); i++) {
+ guid2[i] = i + 1;
+ guid3[i] = i ^ 0xff;
+ guid4[i] = i + 0x80;
+ }
+
+ mail_index_expunge_guid(t, 4, guid4);
+ test_assert(!t->expunges_nonsorted);
+ mail_index_expunge_guid(t, 2, guid2);
+ test_assert(t->expunges_nonsorted);
+ mail_index_expunge_guid(t, 3, guid3);
+ mail_index_expunge(t, 1);
+ mail_index_expunge(t, 5);
+
+ expunges = array_get(&t->expunges, &count);
+ test_assert(count == 5);
+ test_assert(expunges[0].uid == 4);
+ test_assert(memcmp(expunges[0].guid_128, guid4, sizeof(guid4)) == 0);
+ test_assert(expunges[1].uid == 2);
+ test_assert(memcmp(expunges[1].guid_128, guid2, sizeof(guid2)) == 0);
+ test_assert(expunges[2].uid == 3);
+ test_assert(memcmp(expunges[2].guid_128, guid3, sizeof(guid3)) == 0);
+ test_assert(expunges[3].uid == 1);
+ test_assert(memcmp(expunges[3].guid_128, empty_guid, sizeof(empty_guid)) == 0);
+ test_assert(expunges[4].uid == 5);
+ test_assert(memcmp(expunges[4].guid_128, empty_guid, sizeof(empty_guid)) == 0);
+
+ test_end();
+
+ mail_index_transaction_cleanup(t);
+}
+
+static void test_mail_index_update_day_first_uid(void)
+{
+ struct {
+ uint32_t now;
+ uint32_t old_day_stamp;
+ uint32_t new_day_stamp;
+ uint32_t new_day_first_uid[8];
+ } tests[] = {
+ /* 1487116800 = 2017-02-15 00:00:00 UTC */
+ { 1487116800, 1487116800, 1487116800, { 8, 7, 6, 5, 4, 3, 2, 1 } },
+ /* still same day */
+ { 1487116800+3600*24-1, 1487116800, 1487116800, { 8, 7, 6, 5, 4, 3, 2, 1 } },
+ /* one day earlier */
+ { 1487116800-1, 1487116800, 1487116800, { 8, 7, 6, 5, 4, 3, 2, 1 } },
+ /* next day */
+ { 1487116800+3600*24, 1487116800, 1487116800+3600*24, { 9, 8, 7, 6, 5, 4, 3, 2 } },
+ { 1487116800+3600*24*2-1, 1487116800, 1487116800+3600*24, { 9, 8, 7, 6, 5, 4, 3, 2 } },
+ /* 2 days */
+ { 1487116800+3600*24*2, 1487116800, 1487116800+3600*24*2, { 9, 8, 8, 7, 6, 5, 4, 3 } },
+ /* 3 days */
+ { 1487116800+3600*24*3, 1487116800, 1487116800+3600*24*3, { 9, 8, 8, 8, 7, 6, 5, 4 } },
+ /* 4 days */
+ { 1487116800+3600*24*4, 1487116800, 1487116800+3600*24*4, { 9, 8, 8, 8, 8, 7, 6, 5 } },
+ /* 5 days */
+ { 1487116800+3600*24*5, 1487116800, 1487116800+3600*24*5, { 9, 8, 8, 8, 8, 8, 7, 6 } },
+ /* 6 days */
+ { 1487116800+3600*24*6, 1487116800, 1487116800+3600*24*6, { 9, 8, 8, 8, 8, 8, 8, 7 } },
+ /* 7 days */
+ { 1487116800+3600*24*7, 1487116800, 1487116800+3600*24*7, { 9, 8, 8, 8, 8, 8, 8, 8 } },
+ /* 8 days */
+ { 1487116800+3600*24*8, 1487116800, 1487116800+3600*24*8, { 9, 8, 8, 8, 8, 8, 8, 8 } },
+ /* 366 days */
+ { 1487116800+3600*24*366, 1487116800, 1487116800+3600*24*366, { 9, 8, 8, 8, 8, 8, 8, 8 } },
+ };
+ struct mail_index_transaction *t;
+ struct mail_index_record *rec;
+ unsigned int i, j;
+
+ test_begin("mail index update day first uid");
+
+ /* daylight savings times were confusing these tests, so we'll now
+ just assume that TZ=UTC */
+ test_assert(timezone == 0);
+
+ hdr.messages_count = 10;
+ t = mail_index_transaction_new();
+ t->view = t_new(struct mail_index_view, 1);
+ t->view->map = t_new(struct mail_index_map, 1);
+
+ t_array_init(&t->appends, 1);
+ rec = array_append_space(&t->appends);
+ rec->uid = 9;
+
+ for (i = 0; i < N_ELEMENTS(tests); i++) {
+ i_zero(&hdr);
+ for (j = 0; j < N_ELEMENTS(hdr.day_first_uid); j++)
+ hdr.day_first_uid[j] = 8-j;
+ hdr.day_stamp = tests[i].old_day_stamp + timezone;
+ memcpy(t->post_hdr_change, &hdr, sizeof(hdr));
+ mail_index_update_day_headers(t, tests[i].now + timezone);
+
+ struct mail_index_header new_hdr;
+ memcpy(&new_hdr, t->post_hdr_change, sizeof(new_hdr));
+ test_assert_idx(new_hdr.day_stamp == tests[i].new_day_stamp + timezone, i);
+ test_assert_idx(memcmp(new_hdr.day_first_uid,
+ tests[i].new_day_first_uid,
+ sizeof(uint32_t) * 8) == 0, i);
+ }
+
+ test_end();
+}
+
+int main(void)
+{
+ static void (*const test_functions[])(void) = {
+ test_mail_index_append,
+ test_mail_index_flag_update_fastpath,
+ test_mail_index_flag_update_simple_merges,
+ test_mail_index_flag_update_complex_merges,
+ test_mail_index_flag_update_random,
+ test_mail_index_flag_update_appends,
+ test_mail_index_cancel_flag_updates,
+ test_mail_index_transaction_get_flag_update_pos,
+ test_mail_index_modseq_update,
+ test_mail_index_expunge,
+ test_mail_index_update_day_first_uid,
+ NULL
+ };
+ /* daylight saving time confuses things */
+ env_put("TZ", "UTC");
+ tzset();
+ return test_run(test_functions);
+}
diff --git a/src/lib-index/test-mail-index-write.c b/src/lib-index/test-mail-index-write.c
new file mode 100644
index 0000000..88eaa4a
--- /dev/null
+++ b/src/lib-index/test-mail-index-write.c
@@ -0,0 +1,151 @@
+/* Copyright (c) 2020 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "test-common.h"
+#include "mail-index-private.h"
+#include "mail-transaction-log-private.h"
+
+#define TEST_INDEX_FNAME ".test.index.write"
+#define TEST_INDEXID 123456
+#define LOG_FILE1_HEAD_OFFSET 200
+
+static bool expect_index_rewrite;
+static bool rotate_fail;
+
+static struct mail_transaction_log_file log_file = {
+ .hdr = {
+ .indexid = TEST_INDEXID,
+ .file_seq = 1,
+ },
+};
+static struct mail_transaction_log_file log_file2 = {
+ .hdr = {
+ .indexid = TEST_INDEXID,
+ .file_seq = 2,
+ .prev_file_seq = 1,
+ .prev_file_offset = LOG_FILE1_HEAD_OFFSET,
+ },
+};
+
+void mail_index_set_error(struct mail_index *index ATTR_UNUSED,
+ const char *fmt ATTR_UNUSED, ...)
+{
+}
+
+void mail_index_set_syscall_error(struct mail_index *index ATTR_UNUSED,
+ const char *function)
+{
+ i_error("%s() failed: %m", function);
+}
+
+void mail_index_file_set_syscall_error(struct mail_index *index ATTR_UNUSED,
+ const char *filepath,
+ const char *function)
+{
+ i_error("%s(%s) failed: %m", function, filepath);
+}
+
+int mail_index_create_tmp_file(struct mail_index *index ATTR_UNUSED,
+ const char *path_prefix, const char **path_r)
+{
+ const char *path;
+ int fd;
+
+ test_assert(expect_index_rewrite);
+
+ path = *path_r = t_strconcat(path_prefix, ".tmp", NULL);
+ fd = open(path, O_RDWR|O_CREAT, 0600);
+ if (fd == -1) {
+ i_error("creat() failed: %m");
+ return -1;
+ }
+ return fd;
+}
+
+int mail_index_move_to_memory(struct mail_index *index ATTR_UNUSED)
+{
+ return -1;
+}
+
+int mail_transaction_log_rotate(struct mail_transaction_log *log, bool reset)
+{
+ i_assert(!reset);
+
+ if (rotate_fail)
+ return -1;
+
+ log_file.next = &log_file2;
+ log->head = &log_file2;
+ return 0;
+}
+
+static void test_mail_index_write(void)
+{
+ struct mail_transaction_log log = {
+ .head = &log_file,
+ .files = &log_file,
+ };
+ struct mail_index_record_map rec_map = {
+ .records_count = 0,
+ };
+ buffer_t hdr_copy;
+ struct mail_index_map map = {
+ .hdr = {
+ .indexid = TEST_INDEXID,
+ .log_file_seq = 1,
+ .log_file_tail_offset = 100,
+ .log_file_head_offset = LOG_FILE1_HEAD_OFFSET,
+ },
+ .hdr_copy_buf = &hdr_copy,
+ .rec_map = &rec_map,
+ };
+ buffer_create_from_const_data(&hdr_copy, &map.hdr, sizeof(map.hdr));
+ struct mail_index index = {
+ .event = event_create(NULL),
+ .log = &log,
+ .map = &map,
+ .dir = ".",
+ .fd = -1,
+ .indexid = TEST_INDEXID,
+ .filepath = TEST_INDEX_FNAME,
+ .log_sync_locked = TRUE,
+ };
+
+ test_begin("test_mail_index_write()");
+
+ /* test failed rotation, no index rewrite */
+ rotate_fail = TRUE;
+ expect_index_rewrite = FALSE;
+ test_assert(!index.reopen_main_index);
+ index.fd = 1; /* anything but -1 */
+ mail_index_write(&index, TRUE, "testing");
+ test_assert(log.head == log.files);
+ test_assert(index.reopen_main_index);
+
+ /* test failed rotation, with index rewrite */
+ expect_index_rewrite = TRUE;
+ index.reopen_main_index = FALSE;
+ index.fd = -1;
+ mail_index_write(&index, TRUE, "testing");
+ test_assert(log.head == log.files);
+ test_assert(!index.reopen_main_index);
+
+ /* test successful rotation, with index rewrite */
+ rotate_fail = FALSE;
+ mail_index_write(&index, TRUE, "testing");
+ test_assert(log.head != log.files && log.head == &log_file2);
+ test_assert(!index.reopen_main_index);
+
+ event_unref(&index.event);
+ i_unlink(TEST_INDEX_FNAME);
+ test_end();
+}
+
+int main(void)
+{
+ static void (*const test_functions[])(void) = {
+ test_mail_index_write,
+ NULL
+ };
+ return test_run(test_functions);
+}
diff --git a/src/lib-index/test-mail-index.c b/src/lib-index/test-mail-index.c
new file mode 100644
index 0000000..3e2fd02
--- /dev/null
+++ b/src/lib-index/test-mail-index.c
@@ -0,0 +1,169 @@
+/* Copyright (c) 2019 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "test-common.h"
+#include "test-mail-index.h"
+#include "mail-transaction-log-private.h"
+
+static void test_mail_index_rotate(void)
+{
+ struct mail_index *index, *index2;
+ struct mail_index_view *view;
+ struct mail_index_transaction *trans;
+ struct mail_transaction_log_file *file;
+ const char *reason;
+
+ test_begin("mail index rotate");
+ index = test_mail_index_init();
+ index2 = test_mail_index_open();
+ view = mail_index_view_open(index);
+
+ /* First rotation of the index. The view will point to the old index. */
+ trans = mail_index_transaction_begin(view, 0);
+ mail_index_reset(trans);
+ test_assert(mail_index_transaction_commit(&trans) == 0);
+
+ /* Second rotation of the index. The log head doesn't have any extra
+ references. */
+ trans = mail_index_transaction_begin(view, 0);
+ mail_index_reset(trans);
+ test_assert(mail_index_transaction_commit(&trans) == 0);
+
+ /* The 2nd index's log head also doesn't have any extra references.
+ Check that it doesn't crash. */
+ test_assert(mail_transaction_log_find_file(index2->log, 3, FALSE, &file, &reason) == 0);
+
+ mail_index_view_close(&view);
+ test_mail_index_deinit(&index);
+ test_mail_index_deinit(&index2);
+ test_end();
+}
+
+static void
+test_mail_index_new_extension_rotate_write(struct mail_index *index2,
+ uint32_t uid)
+{
+ struct mail_index_view *view2;
+ struct mail_index_transaction *trans;
+ uint32_t hdr_ext_id, rec_ext_id, file_seq, seq, rec_ext = 0x12345678;
+ uoff_t file_offset;
+
+ /* Rotate the index in the index */
+ test_assert(mail_transaction_log_sync_lock(index2->log, "test",
+ &file_seq, &file_offset) == 0);
+ mail_index_write(index2, TRUE, "test");
+ mail_transaction_log_sync_unlock(index2->log, "test");
+
+ /* Write a new extension header to the 2nd index. */
+ hdr_ext_id = mail_index_ext_register(index2, "test",
+ sizeof(hdr_ext_id), 0, 0);
+ rec_ext_id = mail_index_ext_register(index2, "test-rec", 0,
+ sizeof(uint32_t), sizeof(uint32_t));
+ view2 = mail_index_view_open(index2);
+ trans = mail_index_transaction_begin(view2,
+ MAIL_INDEX_TRANSACTION_FLAG_EXTERNAL);
+ mail_index_update_header_ext(trans, hdr_ext_id, 0,
+ &hdr_ext_id, sizeof(hdr_ext_id));
+ mail_index_append(trans, uid, &seq);
+ mail_index_update_ext(trans, seq, rec_ext_id, &rec_ext, NULL);
+ test_assert(mail_index_transaction_commit(&trans) == 0);
+ mail_index_view_close(&view2);
+}
+
+static void test_mail_index_new_extension_sync(struct mail_index_view *view)
+{
+ struct mail_index_view_sync_ctx *sync_ctx;
+ struct mail_index_view_sync_rec sync_rec;
+ bool delayed_expunges;
+
+ test_assert(mail_index_refresh(view->index) == 0);
+ sync_ctx = mail_index_view_sync_begin(view,
+ MAIL_INDEX_VIEW_SYNC_FLAG_NOEXPUNGES);
+ test_assert(!mail_index_view_sync_next(sync_ctx, &sync_rec));
+ test_assert(mail_index_view_sync_commit(&sync_ctx, &delayed_expunges) == 0);
+}
+
+static void test_mail_index_new_extension(void)
+{
+ struct mail_index *index, *index2;
+ struct mail_index_view *view, *view2;
+ struct mail_index_transaction *trans;
+ uint32_t seq, rec_ext_id, rec_ext = 0x12345678;
+
+ test_begin("mail index new extension");
+ index = test_mail_index_init();
+ index2 = test_mail_index_open();
+ view = mail_index_view_open(index);
+
+ rec_ext_id = mail_index_ext_register(index, "test-rec", 0,
+ sizeof(uint32_t), sizeof(uint32_t));
+
+ /* Save two mails */
+ uint32_t uid_validity = 123456;
+ trans = mail_index_transaction_begin(view,
+ MAIL_INDEX_TRANSACTION_FLAG_EXTERNAL);
+ mail_index_update_header(trans,
+ offsetof(struct mail_index_header, uid_validity),
+ &uid_validity, sizeof(uid_validity), TRUE);
+ mail_index_append(trans, 1, &seq);
+ mail_index_update_ext(trans, seq, rec_ext_id, &rec_ext, NULL);
+ mail_index_append(trans, 2, &seq);
+ mail_index_update_ext(trans, seq, rec_ext_id, &rec_ext, NULL);
+ test_assert(mail_index_transaction_commit(&trans) == 0);
+
+ /* refresh indexes and view */
+ test_assert(mail_index_refresh(index2) == 0);
+ mail_index_view_close(&view);
+ view = mail_index_view_open(index);
+
+ /* Expunge the mail in the 2nd index */
+ view2 = mail_index_view_open(index2);
+ trans = mail_index_transaction_begin(view2,
+ MAIL_INDEX_TRANSACTION_FLAG_EXTERNAL);
+ mail_index_expunge(trans, 1);
+ test_assert(mail_index_transaction_commit(&trans) == 0);
+ mail_index_view_close(&view2);
+
+ /* Sync the first view without expunges */
+ test_mail_index_new_extension_sync(view);
+
+ for (unsigned int i = 0; i < 3; i++)
+ test_mail_index_new_extension_rotate_write(index2, 3 + i);
+
+ /* Sync the first view. It needs to generate the missing view. */
+ test_expect_error_string("generating missing logs");
+ test_mail_index_new_extension_sync(view);
+ test_expect_no_more_errors();
+ test_assert(mail_index_get_header(view)->messages_count == 5);
+
+ /* Make sure the extensions records are still there.
+ Note that this works, because the extensions are looked up from the
+ newly refreshed index, not the old index. */
+ for (seq = 1; seq <= 5; seq++) {
+ const void *data;
+ bool expunged;
+ mail_index_lookup_ext(view, seq, rec_ext_id, &data, &expunged);
+ test_assert_idx(memcmp(data, &rec_ext, sizeof(rec_ext)) == 0, seq);
+ }
+
+ /* Once more rotate and write using the new extension */
+ test_mail_index_new_extension_rotate_write(index2, 6);
+ /* Make sure the first view understands the new extension by ID */
+ test_mail_index_new_extension_sync(view);
+
+ mail_index_view_close(&view);
+ test_mail_index_deinit(&index);
+ test_mail_index_deinit(&index2);
+ test_end();
+}
+
+int main(void)
+{
+ static void (*const test_functions[])(void) = {
+ test_mail_index_rotate,
+ test_mail_index_new_extension,
+ NULL
+ };
+ return test_run(test_functions);
+}
diff --git a/src/lib-index/test-mail-index.h b/src/lib-index/test-mail-index.h
new file mode 100644
index 0000000..75b343f
--- /dev/null
+++ b/src/lib-index/test-mail-index.h
@@ -0,0 +1,51 @@
+#ifndef TEST_MAIL_INDEX_H
+#define TEST_MAIL_INDEX_H
+
+#include "ioloop.h"
+#include "unlink-directory.h"
+#include "mail-index-private.h"
+
+#define TESTDIR_NAME ".dovecot.test"
+
+static inline struct mail_index *test_mail_index_open(void)
+{
+ struct mail_index *index;
+
+ index = mail_index_alloc(NULL, TESTDIR_NAME, "test.dovecot.index");
+ test_assert(mail_index_open_or_create(index, MAIL_INDEX_OPEN_FLAG_CREATE) == 0);
+ return index;
+}
+
+static inline struct mail_index *test_mail_index_init(void)
+{
+ const char *error;
+
+ (void)unlink_directory(TESTDIR_NAME, UNLINK_DIRECTORY_FLAG_RMDIR, &error);
+ if (mkdir(TESTDIR_NAME, 0700) < 0)
+ i_error("mkdir(%s) failed: %m", TESTDIR_NAME);
+
+ ioloop_time = 1;
+
+ return test_mail_index_open();
+}
+
+static inline void test_mail_index_close(struct mail_index **index)
+{
+ mail_index_close(*index);
+ mail_index_free(index);
+}
+
+static inline void test_mail_index_delete(void)
+{
+ const char *error;
+
+ (void)unlink_directory(TESTDIR_NAME, UNLINK_DIRECTORY_FLAG_RMDIR, &error);
+}
+
+static inline void test_mail_index_deinit(struct mail_index **index)
+{
+ test_mail_index_close(index);
+ test_mail_index_delete();
+}
+
+#endif
diff --git a/src/lib-index/test-mail-transaction-log-append.c b/src/lib-index/test-mail-transaction-log-append.c
new file mode 100644
index 0000000..4029432
--- /dev/null
+++ b/src/lib-index/test-mail-transaction-log-append.c
@@ -0,0 +1,176 @@
+/* Copyright (c) 2009-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "buffer.h"
+#include "test-common.h"
+#include "mail-index-private.h"
+#include "mail-transaction-log-private.h"
+
+#include <sys/stat.h>
+
+static bool log_lock_failure = FALSE;
+
+void mail_index_file_set_syscall_error(struct mail_index *index ATTR_UNUSED,
+ const char *filepath ATTR_UNUSED,
+ const char *function ATTR_UNUSED)
+{
+}
+
+int mail_transaction_log_lock_head(struct mail_transaction_log *log ATTR_UNUSED,
+ const char *lock_reason ATTR_UNUSED)
+{
+ return log_lock_failure ? -1 : 0;
+}
+
+void mail_transaction_log_file_unlock(struct mail_transaction_log_file *file ATTR_UNUSED,
+ const char *lock_reason ATTR_UNUSED) {}
+
+void mail_transaction_update_modseq(const struct mail_transaction_header *hdr,
+ const void *data ATTR_UNUSED,
+ uint64_t *cur_modseq,
+ unsigned int version ATTR_UNUSED)
+{
+ if ((hdr->type & MAIL_TRANSACTION_EXPUNGE) != 0)
+ *cur_modseq += 1;
+}
+
+int mail_index_move_to_memory(struct mail_index *index ATTR_UNUSED)
+{
+ return -1;
+}
+
+static void test_append_expunge(struct mail_transaction_log *log)
+{
+ static unsigned int buf[] = { 0x12345678, 0xabcdef09 };
+ struct mail_transaction_log_file *file = log->head;
+ struct mail_transaction_log_append_ctx *ctx;
+ const struct mail_transaction_header *hdr;
+ const unsigned int *bufp;
+ const struct mail_transaction_boundary *bound;
+
+ test_assert(mail_transaction_log_append_begin(log->index, MAIL_TRANSACTION_EXTERNAL, &ctx) == 0);
+ mail_transaction_log_append_add(ctx, MAIL_TRANSACTION_APPEND,
+ &buf[0], sizeof(buf[0]));
+ test_assert(ctx->new_highest_modseq == 0);
+ mail_transaction_log_append_add(ctx, MAIL_TRANSACTION_EXPUNGE,
+ &buf[1], sizeof(buf[1]));
+ test_assert(ctx->new_highest_modseq == 1);
+
+ test_assert(mail_transaction_log_append_commit(&ctx) == 0);
+ test_assert(file->sync_highest_modseq == 1);
+ test_assert(file->sync_offset == file->buffer_offset + file->buffer->used);
+
+ hdr = file->buffer->data;
+ test_assert(hdr->type == (MAIL_TRANSACTION_BOUNDARY |
+ MAIL_TRANSACTION_EXTERNAL));
+ test_assert(mail_index_offset_to_uint32(hdr->size) == sizeof(*hdr) + sizeof(*bound));
+ bound = (const void *)(hdr + 1);
+ test_assert(bound->size == file->buffer->used);
+ hdr = (const void *)(bound + 1);
+
+ test_assert(hdr->type == (MAIL_TRANSACTION_APPEND |
+ MAIL_TRANSACTION_EXTERNAL));
+ test_assert(mail_index_offset_to_uint32(hdr->size) == sizeof(*hdr) + sizeof(buf[0]));
+ bufp = (const void *)(hdr + 1);
+ test_assert(*bufp == buf[0]);
+
+ hdr = (const void *)(bufp + 1);
+ test_assert(hdr->type == (MAIL_TRANSACTION_EXPUNGE |
+ MAIL_TRANSACTION_EXPUNGE_PROT |
+ MAIL_TRANSACTION_EXTERNAL));
+ test_assert(mail_index_offset_to_uint32(hdr->size) == sizeof(*hdr) + sizeof(buf[0]));
+ bufp = (const void *)(hdr + 1);
+ test_assert(*bufp == buf[1]);
+
+ test_assert(file->buffer->used == (size_t)((const char *)(bufp+1) - (const char *)file->buffer->data));
+
+ buffer_set_used_size(file->buffer, 0);
+ file->buffer_offset = 0;
+ test_end();
+}
+
+static void test_append_sync_offset(struct mail_transaction_log *log)
+{
+ struct mail_transaction_log_file *file = log->head;
+ struct mail_transaction_log_append_ctx *ctx;
+ const struct mail_transaction_header *hdr;
+ const struct mail_transaction_header_update *u;
+ const uint32_t *offsetp;
+
+ test_begin("transaction log append: append_sync_offset only");
+ test_assert(mail_transaction_log_append_begin(log->index, 0, &ctx) == 0);
+ ctx->index_sync_transaction = TRUE;
+ file->max_tail_offset = 123;
+ test_assert(mail_transaction_log_append_commit(&ctx) == 0);
+
+ test_assert(file->buffer->used == sizeof(*hdr) + sizeof(*u) + sizeof(*offsetp));
+ hdr = file->buffer->data;
+ test_assert(hdr->type == MAIL_TRANSACTION_HEADER_UPDATE);
+ test_assert(mail_index_offset_to_uint32(hdr->size) == file->buffer->used);
+ u = (const void *)(hdr + 1);
+ test_assert(u->offset == offsetof(struct mail_index_header, log_file_tail_offset));
+ test_assert(u->size == sizeof(*offsetp));
+ offsetp = (const void *)(u+1);
+ test_assert(*offsetp == 123);
+
+ test_end();
+}
+
+static void test_mail_transaction_log_append(void)
+{
+ struct mail_transaction_log *log;
+ struct mail_transaction_log_file *file;
+ struct mail_transaction_log_append_ctx *ctx;
+ char tmp_path[] = "/tmp/dovecot.test.XXXXXX";
+ struct stat st;
+ int fd;
+
+ fd = mkstemp(tmp_path);
+ if (fd == -1)
+ i_fatal("mkstemp(%s) failed: %m", tmp_path);
+
+ test_begin("transaction log append");
+ log = i_new(struct mail_transaction_log, 1);
+ log->index = i_new(struct mail_index, 1);
+ log->index->log = log;
+ log->head = file = i_new(struct mail_transaction_log_file, 1);
+ file->fd = -1;
+
+ test_append_expunge(log);
+
+ test_begin("transaction log append: lock failure");
+ log_lock_failure = TRUE;
+ test_assert(mail_transaction_log_append_begin(log->index, 0, &ctx) < 0);
+ log_lock_failure = FALSE;
+ test_end();
+
+ test_append_sync_offset(log);
+
+ /* do this after head->buffer has already been initialized */
+ test_begin("transaction log append: garbage truncation");
+ file->sync_offset = 1;
+ file->buffer_offset = 1;
+ file->last_size = 3;
+ file->fd = fd;
+ test_assert(mail_transaction_log_append_begin(log->index, 0, &ctx) == 0);
+ test_assert(mail_transaction_log_append_commit(&ctx) == 0);
+ if (fstat(fd, &st) < 0) i_fatal("fstat() failed: %m");
+ test_assert(st.st_size == 1);
+ file->fd = -1;
+ test_end();
+
+ buffer_free(&log->head->buffer);
+ i_free(log->head);
+ i_free(log->index);
+ i_free(log);
+ i_unlink(tmp_path);
+}
+
+int main(void)
+{
+ static void (*const test_functions[])(void) = {
+ test_mail_transaction_log_append,
+ NULL
+ };
+ return test_run(test_functions);
+}
diff --git a/src/lib-index/test-mail-transaction-log-file.c b/src/lib-index/test-mail-transaction-log-file.c
new file mode 100644
index 0000000..6f591ce
--- /dev/null
+++ b/src/lib-index/test-mail-transaction-log-file.c
@@ -0,0 +1,418 @@
+/* Copyright (c) 2009-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "ioloop.h"
+#include "test-common.h"
+#include "mail-index-private.h"
+#include "mail-transaction-log-private.h"
+
+#define TEST_LOG_VERSION MAIL_TRANSACTION_LOG_VERSION_FULL(1, 3)
+
+#define INITIAL_MODSEQ 100
+
+struct update_modseq_test {
+ enum mail_transaction_type type;
+ unsigned int version;
+#define NOUPDATE (INITIAL_MODSEQ)
+#define UPDATE (INITIAL_MODSEQ+1)
+ uint64_t expected_modseq;
+ unsigned int count;
+ union {
+ const struct mail_transaction_flag_update *flag_update;
+ const struct mail_transaction_modseq_update *modseq_update;
+ } v;
+} update_modseq_tests[] = {
+ /* expunges: increase modseq */
+ { MAIL_TRANSACTION_EXPUNGE | MAIL_TRANSACTION_EXPUNGE_PROT | MAIL_TRANSACTION_EXTERNAL, TEST_LOG_VERSION, UPDATE, 1, { } },
+ { MAIL_TRANSACTION_EXPUNGE_GUID | MAIL_TRANSACTION_EXPUNGE_PROT | MAIL_TRANSACTION_EXTERNAL, TEST_LOG_VERSION, UPDATE, 1, { } },
+ /* expunges: don't increase modseq */
+ { MAIL_TRANSACTION_EXPUNGE | MAIL_TRANSACTION_EXPUNGE_PROT, TEST_LOG_VERSION, NOUPDATE, 1, { } },
+ { MAIL_TRANSACTION_EXPUNGE_GUID | MAIL_TRANSACTION_EXPUNGE_PROT, TEST_LOG_VERSION, NOUPDATE, 1, { } },
+ { MAIL_TRANSACTION_EXPUNGE | MAIL_TRANSACTION_EXTERNAL, TEST_LOG_VERSION, NOUPDATE, 1, { } },
+ { MAIL_TRANSACTION_EXPUNGE_GUID | MAIL_TRANSACTION_EXTERNAL, TEST_LOG_VERSION, NOUPDATE, 1, { } },
+
+ /* flag changes: don't increase modseq */
+ { MAIL_TRANSACTION_FLAG_UPDATE, TEST_LOG_VERSION, NOUPDATE, 1, {
+ .flag_update = (const struct mail_transaction_flag_update[]) {
+ { .uid1 = 1, .uid2 = 2, .add_flags = 0 }
+ }
+ } },
+ { MAIL_TRANSACTION_FLAG_UPDATE, TEST_LOG_VERSION, NOUPDATE, 1, {
+ .flag_update = (const struct mail_transaction_flag_update[]) {
+ { .uid1 = 1, .uid2 = 2, .add_flags = MAIL_INDEX_MAIL_FLAG_BACKEND }
+ }
+ } },
+ { MAIL_TRANSACTION_FLAG_UPDATE, TEST_LOG_VERSION, NOUPDATE, 1, {
+ .flag_update = (const struct mail_transaction_flag_update[]) {
+ { .uid1 = 1, .uid2 = 2, .remove_flags = MAIL_INDEX_MAIL_FLAG_BACKEND }
+ }
+ } },
+ { MAIL_TRANSACTION_FLAG_UPDATE, TEST_LOG_VERSION, NOUPDATE, 1, {
+ .flag_update = (const struct mail_transaction_flag_update[]) {
+ { .uid1 = 1, .uid2 = 2, .add_flags = MAIL_INDEX_MAIL_FLAG_DIRTY }
+ }
+ } },
+ { MAIL_TRANSACTION_FLAG_UPDATE, TEST_LOG_VERSION, NOUPDATE, 1, {
+ .flag_update = (const struct mail_transaction_flag_update[]) {
+ { .uid1 = 1, .uid2 = 2, .remove_flags = MAIL_INDEX_MAIL_FLAG_DIRTY }
+ }
+ } },
+ /* flag changes: increase modseq */
+ { MAIL_TRANSACTION_FLAG_UPDATE, TEST_LOG_VERSION, UPDATE, 1, {
+ .flag_update = (const struct mail_transaction_flag_update[]) {
+ { .uid1 = 1, .uid2 = 2, .add_flags = MAIL_SEEN }
+ }
+ } },
+ { MAIL_TRANSACTION_FLAG_UPDATE, TEST_LOG_VERSION, UPDATE, 1, {
+ .flag_update = (const struct mail_transaction_flag_update[]) {
+ { .uid1 = 1, .uid2 = 2, .remove_flags = MAIL_SEEN }
+ }
+ } },
+ { MAIL_TRANSACTION_FLAG_UPDATE, TEST_LOG_VERSION, UPDATE, 1, {
+ .flag_update = (const struct mail_transaction_flag_update[]) {
+ { .uid1 = 1, .uid2 = 2, .add_flags = MAIL_SEEN | MAIL_INDEX_MAIL_FLAG_BACKEND }
+ }
+ } },
+ { MAIL_TRANSACTION_FLAG_UPDATE, TEST_LOG_VERSION, UPDATE, 1, {
+ .flag_update = (const struct mail_transaction_flag_update[]) {
+ { .uid1 = 1, .uid2 = 2, .add_flags = MAIL_SEEN | MAIL_INDEX_MAIL_FLAG_DIRTY }
+ }
+ } },
+ { MAIL_TRANSACTION_FLAG_UPDATE, TEST_LOG_VERSION, UPDATE, 1, {
+ .flag_update = (const struct mail_transaction_flag_update[]) {
+ { .uid1 = 1, .uid2 = 2, .remove_flags = MAIL_SEEN | MAIL_INDEX_MAIL_FLAG_BACKEND }
+ }
+ } },
+ { MAIL_TRANSACTION_FLAG_UPDATE, TEST_LOG_VERSION, UPDATE, 1, {
+ .flag_update = (const struct mail_transaction_flag_update[]) {
+ { .uid1 = 1, .uid2 = 2, .remove_flags = MAIL_SEEN | MAIL_INDEX_MAIL_FLAG_DIRTY }
+ }
+ } },
+ { MAIL_TRANSACTION_FLAG_UPDATE, TEST_LOG_VERSION, UPDATE, 2, {
+ .flag_update = (const struct mail_transaction_flag_update[]) {
+ { .uid1 = 1, .uid2 = 2, .add_flags = MAIL_INDEX_MAIL_FLAG_DIRTY },
+ { .uid1 = 3, .uid2 = 4, .add_flags = MAIL_SEEN }
+ }
+ } },
+ { MAIL_TRANSACTION_FLAG_UPDATE, TEST_LOG_VERSION, UPDATE, 1, {
+ .flag_update = (const struct mail_transaction_flag_update[]) {
+ { .uid1 = 1, .uid2 = 2, .add_flags = 0, .modseq_inc_flag = 1 }
+ }
+ } },
+ { MAIL_TRANSACTION_FLAG_UPDATE, TEST_LOG_VERSION, UPDATE, 2, {
+ .flag_update = (const struct mail_transaction_flag_update[]) {
+ { .uid1 = 1, .uid2 = 2, .add_flags = MAIL_INDEX_MAIL_FLAG_DIRTY },
+ { .uid1 = 1, .uid2 = 2, .add_flags = MAIL_INDEX_MAIL_FLAG_DIRTY, .modseq_inc_flag = 1 }
+ }
+ } },
+ /* flag changes: increase modseq with old version */
+ { MAIL_TRANSACTION_FLAG_UPDATE, MAIL_TRANSACTION_LOG_VERSION_FULL(1, 2), UPDATE, 1, {
+ .flag_update = (const struct mail_transaction_flag_update[]) {
+ { .uid1 = 1, .uid2 = 2, .add_flags = MAIL_INDEX_MAIL_FLAG_BACKEND }
+ }
+ } },
+ { MAIL_TRANSACTION_FLAG_UPDATE, MAIL_TRANSACTION_LOG_VERSION_FULL(1, 2), UPDATE, 1, {
+ .flag_update = (const struct mail_transaction_flag_update[]) {
+ { .uid1 = 1, .uid2 = 2, .add_flags = MAIL_INDEX_MAIL_FLAG_DIRTY }
+ }
+ } },
+ /* modseq updates: don't increase modseq */
+ { MAIL_TRANSACTION_MODSEQ_UPDATE, TEST_LOG_VERSION, NOUPDATE, 1, {
+ .modseq_update = (const struct mail_transaction_modseq_update[]) {
+ { .uid = 1, .modseq_low32 = 50, .modseq_high32 = 0 }
+ }
+ } },
+ { MAIL_TRANSACTION_MODSEQ_UPDATE, TEST_LOG_VERSION, NOUPDATE, 1, {
+ .modseq_update = (const struct mail_transaction_modseq_update[]) {
+ { .uid = 1, .modseq_low32 = 100, .modseq_high32 = 0 }
+ }
+ } },
+ /* modseq updates: increase modseq */
+ { MAIL_TRANSACTION_MODSEQ_UPDATE, TEST_LOG_VERSION, 500, 1, {
+ .modseq_update = (const struct mail_transaction_modseq_update[]) {
+ { .uid = 1, .modseq_low32 = 500, .modseq_high32 = 0 }
+ }
+ } },
+ { MAIL_TRANSACTION_MODSEQ_UPDATE, TEST_LOG_VERSION, 500, 2, {
+ .modseq_update = (const struct mail_transaction_modseq_update[]) {
+ { .uid = 1, .modseq_low32 = 50, .modseq_high32 = 0 },
+ { .uid = 1, .modseq_low32 = 500, .modseq_high32 = 0 }
+ }
+ } },
+ { MAIL_TRANSACTION_MODSEQ_UPDATE, TEST_LOG_VERSION, 500, 1, {
+ .modseq_update = (const struct mail_transaction_modseq_update[]) {
+ { .uid = 1, .modseq_low32 = 500, .modseq_high32 = 0 },
+ { .uid = 1, .modseq_low32 = 200, .modseq_high32 = 0 }
+ }
+ } },
+ { MAIL_TRANSACTION_MODSEQ_UPDATE, TEST_LOG_VERSION, (uint64_t)4294967346, 1, {
+ .modseq_update = (const struct mail_transaction_modseq_update[]) {
+ { .uid = 1, .modseq_low32 = 50, .modseq_high32 = 1 }
+ }
+ } },
+
+ /* appends, keyword changes, attribute changes: increase modseq */
+ { MAIL_TRANSACTION_APPEND, TEST_LOG_VERSION, UPDATE, 1, { } },
+ { MAIL_TRANSACTION_KEYWORD_UPDATE, TEST_LOG_VERSION, UPDATE, 1, { } },
+ { MAIL_TRANSACTION_KEYWORD_RESET, TEST_LOG_VERSION, UPDATE, 1, { } },
+ { MAIL_TRANSACTION_ATTRIBUTE_UPDATE, TEST_LOG_VERSION, UPDATE, 1, { } },
+
+ /* others: don't increase modseq */
+ { MAIL_TRANSACTION_HEADER_UPDATE, TEST_LOG_VERSION, NOUPDATE, 1, { } },
+ { MAIL_TRANSACTION_HEADER_UPDATE, TEST_LOG_VERSION, NOUPDATE, 1, { } },
+ { MAIL_TRANSACTION_EXT_INTRO, TEST_LOG_VERSION, NOUPDATE, 1, { } },
+ { MAIL_TRANSACTION_EXT_RESET, TEST_LOG_VERSION, NOUPDATE, 1, { } },
+ { MAIL_TRANSACTION_EXT_HDR_UPDATE, TEST_LOG_VERSION, NOUPDATE, 1, { } },
+ { MAIL_TRANSACTION_EXT_REC_UPDATE, TEST_LOG_VERSION, NOUPDATE, 1, { } },
+ { MAIL_TRANSACTION_EXT_ATOMIC_INC, TEST_LOG_VERSION, NOUPDATE, 1, { } },
+ { MAIL_TRANSACTION_EXT_HDR_UPDATE32, TEST_LOG_VERSION, NOUPDATE, 1, { } },
+ { MAIL_TRANSACTION_INDEX_DELETED, TEST_LOG_VERSION, NOUPDATE, 1, { } },
+ { MAIL_TRANSACTION_INDEX_UNDELETED, TEST_LOG_VERSION, NOUPDATE, 1, { } },
+};
+
+static size_t update_modseq_test_get_size(const struct update_modseq_test *test)
+{
+ enum mail_transaction_type type =
+ test->type & MAIL_TRANSACTION_TYPE_MASK;
+
+ if (type == (MAIL_TRANSACTION_EXPUNGE | MAIL_TRANSACTION_EXPUNGE_PROT))
+ type = MAIL_TRANSACTION_EXPUNGE;
+ if (type == (MAIL_TRANSACTION_EXPUNGE_GUID | MAIL_TRANSACTION_EXPUNGE_PROT))
+ type = MAIL_TRANSACTION_EXPUNGE_GUID;
+
+ switch (type) {
+ case MAIL_TRANSACTION_EXPUNGE:
+ return sizeof(struct mail_transaction_expunge);
+ case MAIL_TRANSACTION_EXPUNGE_GUID:
+ return sizeof(struct mail_transaction_expunge_guid);
+ case MAIL_TRANSACTION_APPEND:
+ return sizeof(struct mail_index_record);
+ case MAIL_TRANSACTION_KEYWORD_UPDATE:
+ return sizeof(struct mail_transaction_keyword_update);
+ case MAIL_TRANSACTION_KEYWORD_RESET:
+ return sizeof(struct mail_transaction_keyword_reset);
+ case MAIL_TRANSACTION_ATTRIBUTE_UPDATE:
+ return 4;
+ case MAIL_TRANSACTION_FLAG_UPDATE:
+ return sizeof(struct mail_transaction_flag_update);
+ case MAIL_TRANSACTION_MODSEQ_UPDATE:
+ return sizeof(struct mail_transaction_modseq_update);
+ case MAIL_TRANSACTION_HEADER_UPDATE:
+ case MAIL_TRANSACTION_EXT_INTRO:
+ case MAIL_TRANSACTION_EXT_RESET:
+ case MAIL_TRANSACTION_EXT_HDR_UPDATE:
+ case MAIL_TRANSACTION_EXT_REC_UPDATE:
+ case MAIL_TRANSACTION_EXT_ATOMIC_INC:
+ case MAIL_TRANSACTION_EXT_HDR_UPDATE32:
+ case MAIL_TRANSACTION_INDEX_DELETED:
+ case MAIL_TRANSACTION_INDEX_UNDELETED:
+ return 4;
+ case MAIL_TRANSACTION_TYPE_MASK:
+ case MAIL_TRANSACTION_BOUNDARY:
+ case MAIL_TRANSACTION_EXPUNGE_PROT:
+ case MAIL_TRANSACTION_EXTERNAL:
+ case MAIL_TRANSACTION_SYNC:
+ break;
+ }
+ i_unreached();
+}
+
+static void test_mail_transaction_update_modseq(void)
+{
+ struct mail_transaction_header hdr;
+ unsigned char tempbuf[1024] = { 0 };
+
+ test_begin("mail_transaction_update_modseq()");
+ for (unsigned int i = 0; i < N_ELEMENTS(update_modseq_tests); i++) {
+ const struct update_modseq_test *test = &update_modseq_tests[i];
+ const void *data = test->v.flag_update;
+ uint64_t cur_modseq = INITIAL_MODSEQ;
+
+ if (data == NULL)
+ data = tempbuf;
+
+ hdr.type = test->type;
+ hdr.size = sizeof(hdr) + update_modseq_test_get_size(test) * test->count;
+ hdr.size = mail_index_uint32_to_offset(hdr.size);
+ mail_transaction_update_modseq(&hdr, data, &cur_modseq, test->version);
+ test_assert_idx(cur_modseq >= INITIAL_MODSEQ, i);
+ test_assert_idx(test->expected_modseq == cur_modseq, i);
+ }
+ test_end();
+}
+
+static struct mail_index *test_mail_index_open(void)
+{
+ struct mail_index *index = mail_index_alloc(NULL, NULL, "test.dovecot.index");
+ test_assert(mail_index_open_or_create(index, MAIL_INDEX_OPEN_FLAG_CREATE) == 0);
+ struct mail_index_view *view = mail_index_view_open(index);
+
+ struct mail_index_transaction *trans =
+ mail_index_transaction_begin(view, 0);
+ uint32_t uid_validity = 1234;
+ mail_index_update_header(trans,
+ offsetof(struct mail_index_header, uid_validity),
+ &uid_validity, sizeof(uid_validity), TRUE);
+ test_assert(mail_index_transaction_commit(&trans) == 0);
+ mail_index_view_close(&view);
+ return index;
+}
+
+static void test_mail_transaction_log_file_modseq_offsets(void)
+{
+ test_begin("mail_transaction_log_file_get_modseq_next_offset() and _get_highest_modseq_at()");
+
+ struct mail_index *index = test_mail_index_open();
+ struct mail_transaction_log_file *file = index->log->head;
+
+ const unsigned int max_modseq = LOG_FILE_MODSEQ_CACHE_SIZE+2;
+ uoff_t modseq_next_offset[max_modseq+1];
+ uoff_t modseq_alt_next_offset[max_modseq+1];
+
+ /* start with modseq=2, because modseq=1 is the initial state */
+ modseq_next_offset[1] = sizeof(struct mail_transaction_log_header);
+ modseq_alt_next_offset[1] = sizeof(struct mail_transaction_log_header);
+ for (uint64_t modseq = 2; modseq <= max_modseq; modseq++) {
+ uint32_t seq;
+
+ struct mail_index_view *view = mail_index_view_open(index);
+ struct mail_index_transaction *trans =
+ mail_index_transaction_begin(view, 0);
+ mail_index_append(trans, modseq, &seq);
+ test_assert(mail_index_transaction_commit(&trans) == 0);
+ modseq_next_offset[modseq] = file->sync_offset;
+ mail_index_view_close(&view);
+
+ /* add a non-modseq updating change */
+ view = mail_index_view_open(index);
+ trans = mail_index_transaction_begin(view, 0);
+ mail_index_update_flags(trans, seq, MODIFY_ADD,
+ (enum mail_flags)MAIL_INDEX_MAIL_FLAG_DIRTY);
+ test_assert(mail_index_transaction_commit(&trans) == 0);
+ mail_index_view_close(&view);
+ modseq_alt_next_offset[modseq] = file->sync_offset;
+ }
+
+ /* mail_transaction_log_file_get_highest_modseq_at() is simultaneously
+ tested and it can also add offsets to cache. The difference is that
+ it adds the highest possible offset, while
+ mail_transaction_log_file_get_modseq_next_offset() adds the lowest
+ possible offset. So we'll need to allow both. */
+#define MODSEQ_MATCH(modseq, next_offset) \
+ ((next_offset) == modseq_next_offset[modseq] || \
+ (next_offset) == modseq_alt_next_offset[modseq])
+
+ /* 1) mail_transaction_log_file_get_modseq_next_offset() tests */
+ uint64_t modseq;
+ uoff_t next_offset;
+ /* initial_modseq fast path */
+ test_assert(mail_transaction_log_file_get_modseq_next_offset(file, 1, &next_offset) == 0);
+ test_assert(next_offset == modseq_next_offset[1]);
+ /* sync_highest_modseq fast path - it skips to sync_offset instead of
+ using exactly the same max_modseq */
+ test_assert(mail_transaction_log_file_get_modseq_next_offset(file, max_modseq, &next_offset) == 0);
+ test_assert(next_offset == file->sync_offset);
+ test_assert(next_offset != modseq_next_offset[max_modseq]);
+ /* update the offset for the random tests */
+ modseq_next_offset[max_modseq] = file->sync_offset;
+ /* add to cache */
+ test_assert(mail_transaction_log_file_get_modseq_next_offset(file, 2, &next_offset) == 0);
+ test_assert(MODSEQ_MATCH(2, next_offset));
+ /* get it from cache */
+ test_assert(mail_transaction_log_file_get_modseq_next_offset(file, 2, &next_offset) == 0);
+ test_assert(MODSEQ_MATCH(2, next_offset));
+ /* get next value from cache */
+ test_assert(mail_transaction_log_file_get_modseq_next_offset(file, 3, &next_offset) == 0);
+ test_assert(MODSEQ_MATCH(3, next_offset));
+ /* get previous value from cache again */
+ test_assert(mail_transaction_log_file_get_modseq_next_offset(file, 2, &next_offset) == 0);
+ test_assert(MODSEQ_MATCH(2, next_offset));
+ /* do some random testing with cache */
+ for (unsigned int i = 0; i < LOG_FILE_MODSEQ_CACHE_SIZE*10; i++) {
+ modseq = i_rand_minmax(1, max_modseq);
+ test_assert(mail_transaction_log_file_get_modseq_next_offset(file, modseq, &next_offset) == 0);
+ test_assert(MODSEQ_MATCH(modseq, next_offset));
+ }
+ /* go through all modseqs - do this after randomness testing or
+ modseq_alt_next_offset[] matching isn't triggered */
+ for (modseq = 1; modseq <= max_modseq; modseq++) {
+ test_assert(mail_transaction_log_file_get_modseq_next_offset(file, modseq, &next_offset) == 0);
+ test_assert(MODSEQ_MATCH(modseq, next_offset));
+ }
+
+ /* 2) mail_transaction_log_file_get_highest_modseq_at() tests */
+ uint64_t modseq_at;
+ const char *error;
+ /* initial_offset */
+ test_assert(mail_transaction_log_file_get_highest_modseq_at(
+ file, modseq_next_offset[1], &modseq, &error) == 1);
+ test_assert(modseq == 1);
+ /* sync_offset fast path */
+ test_assert(mail_transaction_log_file_get_highest_modseq_at(
+ file, file->sync_offset, &modseq, &error) == 1);
+ test_assert(modseq == max_modseq);
+ /* do some random testing with cache */
+ for (unsigned int i = 0; i < LOG_FILE_MODSEQ_CACHE_SIZE*10; i++) {
+ modseq = i_rand_minmax(1, max_modseq);
+ test_assert(mail_transaction_log_file_get_highest_modseq_at(
+ file, modseq_next_offset[modseq], &modseq_at, &error) == 1);
+ test_assert(modseq_at == modseq);
+ test_assert(mail_transaction_log_file_get_highest_modseq_at(
+ file, modseq_alt_next_offset[modseq], &modseq_at, &error) == 1);
+ test_assert(modseq_at == modseq);
+ }
+ /* go through all modseqs - do this after randomness testing or
+ modseq_alt_next_offset[] matching isn't triggered */
+ for (modseq = 1; modseq <= max_modseq; modseq++) {
+ test_assert(mail_transaction_log_file_get_highest_modseq_at(
+ file, modseq_next_offset[modseq], &modseq_at, &error) == 1);
+ test_assert(modseq_at == modseq);
+ }
+
+ mail_index_close(index);
+ mail_index_free(&index);
+ test_end();
+}
+
+static void
+test_mail_transaction_log_file_get_modseq_next_offset_inconsistency(void)
+{
+ test_begin("mail_transaction_log_file_get_modseq_next_offset() inconsistency");
+
+ struct mail_index *index = test_mail_index_open();
+ struct mail_transaction_log_file *file = index->log->head;
+ uint32_t seq;
+
+ /* add modseq=2 */
+ struct mail_index_view *view = mail_index_view_open(index);
+ struct mail_index_transaction *trans =
+ mail_index_transaction_begin(view, 0);
+ mail_index_append(trans, 1, &seq);
+ test_assert(mail_index_transaction_commit(&trans) == 0);
+ mail_index_view_close(&view);
+
+ /* emulate a broken mail_index_modseq_header header */
+ file->sync_highest_modseq = 3;
+
+ uoff_t next_offset;
+ test_expect_error_string("Transaction log modseq tracking is corrupted");
+ test_assert(mail_transaction_log_file_get_modseq_next_offset(file, 2, &next_offset) == 0);
+ test_expect_no_more_errors();
+ test_assert(next_offset == file->sync_offset);
+
+ mail_index_close(index);
+ mail_index_free(&index);
+ test_end();
+}
+
+int main(void)
+{
+ static void (*const test_functions[])(void) = {
+ test_mail_transaction_update_modseq,
+ test_mail_transaction_log_file_modseq_offsets,
+ test_mail_transaction_log_file_get_modseq_next_offset_inconsistency,
+ NULL
+ };
+ ioloop_time = 1;
+ return test_run(test_functions);
+}
diff --git a/src/lib-index/test-mail-transaction-log-view.c b/src/lib-index/test-mail-transaction-log-view.c
new file mode 100644
index 0000000..17a7628
--- /dev/null
+++ b/src/lib-index/test-mail-transaction-log-view.c
@@ -0,0 +1,268 @@
+/* Copyright (c) 2009-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "test-common.h"
+#include "mail-index-private.h"
+#include "mail-transaction-log-view-private.h"
+
+static struct mail_transaction_log *log;
+static struct mail_transaction_log_view *view;
+static bool clean_refcount0_files = FALSE;
+
+static void
+test_transaction_log_file_add(uint32_t file_seq)
+{
+ struct mail_transaction_log_file **p, *file;
+
+ file = i_new(struct mail_transaction_log_file, 1);
+ file->hdr.file_seq = file_seq;
+ file->hdr.hdr_size = file->sync_offset = sizeof(file->hdr);
+ file->hdr.prev_file_seq = file_seq - 1;
+ file->hdr.prev_file_offset = (uint32_t)-1;
+ file->log = log;
+ file->fd = -1;
+ file->buffer = buffer_create_dynamic(default_pool, 256);
+ file->buffer_offset = file->hdr.hdr_size;
+
+ /* files must be sorted by file_seq */
+ for (p = &log->files; *p != NULL; p = &(*p)->next) {
+ if ((*p)->hdr.file_seq > file->hdr.file_seq) {
+ file->next = *p;
+ break;
+ }
+ }
+ *p = file;
+ log->head = file;
+}
+
+void mail_index_set_error(struct mail_index *index ATTR_UNUSED,
+ const char *fmt ATTR_UNUSED, ...)
+{
+}
+
+void mail_transaction_log_file_set_corrupted(struct mail_transaction_log_file *file ATTR_UNUSED,
+ const char *fmt ATTR_UNUSED, ...)
+{
+}
+
+void mail_transaction_logs_clean(struct mail_transaction_log *log ATTR_UNUSED)
+{
+}
+
+int mail_transaction_log_find_file(struct mail_transaction_log *log,
+ uint32_t file_seq, bool nfs_flush ATTR_UNUSED,
+ struct mail_transaction_log_file **file_r,
+ const char **reason_r)
+{
+ struct mail_transaction_log_file *file, *next;
+
+ for (file = log->files; file != NULL; file = next) {
+ next = file->next;
+ if (file->hdr.file_seq == file_seq) {
+ *file_r = file;
+ return 1;
+ }
+ /* refcount=0 files at the beginning of the list may be freed */
+ if (file->refcount == 0 && file == log->files &&
+ clean_refcount0_files)
+ log->files = next;
+ }
+ if (clean_refcount0_files && file_seq == 4) {
+ /* "clean refcount=0 files" test autocreates this file */
+ test_transaction_log_file_add(4);
+ *file_r = log->head;
+ return 1;
+ }
+ *reason_r = "not found";
+ return 0;
+}
+
+int mail_transaction_log_file_map(struct mail_transaction_log_file *file ATTR_UNUSED,
+ uoff_t start_offset ATTR_UNUSED, uoff_t end_offset ATTR_UNUSED,
+ const char **reason_r ATTR_UNUSED)
+{
+ return 1;
+}
+
+int mail_transaction_log_file_get_highest_modseq_at(
+ struct mail_transaction_log_file *file ATTR_UNUSED,
+ uoff_t offset ATTR_UNUSED, uint64_t *highest_modseq_r,
+ const char **error_r ATTR_UNUSED)
+{
+ *highest_modseq_r = 0;
+ return 1;
+}
+
+void mail_transaction_update_modseq(const struct mail_transaction_header *hdr ATTR_UNUSED,
+ const void *data ATTR_UNUSED,
+ uint64_t *cur_modseq,
+ unsigned int version ATTR_UNUSED)
+{
+ *cur_modseq += 1;
+}
+
+static bool view_is_file_refed(uint32_t file_seq)
+{
+ struct mail_transaction_log_file *const *files;
+ unsigned int i, count;
+ bool ret = FALSE;
+
+ files = array_get(&view->file_refs, &count);
+ for (i = 0; i < count; i++) {
+ if (files[i]->hdr.file_seq == file_seq) {
+ i_assert(!ret); /* could be a test too.. */
+ ret = TRUE;
+ }
+ }
+ return ret;
+}
+
+static size_t
+add_append_record(struct mail_transaction_log_file *file,
+ const struct mail_index_record *rec)
+{
+ struct mail_transaction_header hdr;
+ size_t size;
+
+ i_zero(&hdr);
+ hdr.type = MAIL_TRANSACTION_APPEND | MAIL_TRANSACTION_EXTERNAL;
+ hdr.size = mail_index_uint32_to_offset(sizeof(hdr) + sizeof(*rec));
+
+ buffer_append(file->buffer, &hdr, sizeof(hdr));
+ buffer_append(file->buffer, rec, sizeof(*rec));
+
+ size = sizeof(hdr) + sizeof(*rec);
+ file->sync_offset += size;
+ return size;
+}
+
+static void test_mail_transaction_log_view(void)
+{
+ const struct mail_transaction_header *hdr;
+ const struct mail_index_record *rec;
+ struct mail_index_record append_rec;
+ const void *data;
+ void *oldfile;
+ uint32_t seq;
+ uoff_t offset, last_log_size;
+ const char *reason;
+ bool reset;
+
+ test_begin("init");
+ log = i_new(struct mail_transaction_log, 1);
+ log->index = i_new(struct mail_index, 1);
+ log->index->log = log;
+ log->index->log_sync_locked = TRUE;
+ test_transaction_log_file_add(1);
+ test_transaction_log_file_add(2);
+ test_transaction_log_file_add(3);
+
+ /* add an append record to the 3rd log file */
+ i_zero(&append_rec);
+ append_rec.uid = 1;
+
+ last_log_size = sizeof(struct mail_transaction_log_header) +
+ add_append_record(log->head, &append_rec);
+
+ view = mail_transaction_log_view_open(log);
+ i_assert(view != NULL);
+ test_assert(log->views == view &&
+ !view_is_file_refed(1) && !view_is_file_refed(2) &&
+ view_is_file_refed(3));
+ test_end();
+
+ /* we have files 1-3 opened */
+ test_begin("set all");
+ test_assert(mail_transaction_log_view_set(view, 0, 0, (uint32_t)-1, UOFF_T_MAX, &reset, &reason) == 1 &&
+ reset && view_is_file_refed(1) && view_is_file_refed(2) &&
+ view_is_file_refed(3) &&
+ !mail_transaction_log_view_is_corrupted(view));
+ mail_transaction_log_view_get_prev_pos(view, &seq, &offset);
+ test_assert(seq == 1 && offset == sizeof(struct mail_transaction_log_header));
+ test_assert(mail_transaction_log_view_next(view, &hdr, &data) == 1);
+ test_assert(hdr->type == (MAIL_TRANSACTION_APPEND | MAIL_TRANSACTION_EXTERNAL));
+ rec = data;
+ test_assert(memcmp(rec, &append_rec, sizeof(*rec)) == 0);
+ test_assert(mail_transaction_log_view_next(view, &hdr, &data) == 0);
+ test_assert(mail_transaction_log_view_is_last(view));
+ mail_transaction_log_view_get_prev_pos(view, &seq, &offset);
+ test_assert(seq == 3 && offset == last_log_size);
+ test_end();
+
+ test_begin("set first");
+ test_assert(mail_transaction_log_view_set(view, 0, 0, 0, 0, &reset, &reason) == 1);
+ mail_transaction_log_view_get_prev_pos(view, &seq, &offset);
+ test_assert(seq == 1 && offset == sizeof(struct mail_transaction_log_header));
+ test_assert(mail_transaction_log_view_next(view, &hdr, &data) == 0);
+ mail_transaction_log_view_get_prev_pos(view, &seq, &offset);
+ test_assert(seq == 1 && offset == sizeof(struct mail_transaction_log_header));
+ test_end();
+
+ test_begin("set end");
+ test_assert(mail_transaction_log_view_set(view, 3, last_log_size, (uint32_t)-1, UOFF_T_MAX, &reset, &reason) == 1);
+ mail_transaction_log_view_get_prev_pos(view, &seq, &offset);
+ test_assert(seq == 3 && offset == last_log_size);
+ test_assert(mail_transaction_log_view_next(view, &hdr, &data) == 0);
+ mail_transaction_log_view_get_prev_pos(view, &seq, &offset);
+ test_assert(seq == 3 && offset == last_log_size);
+ test_end();
+
+ test_begin("log clear");
+ mail_transaction_log_view_clear(view, 2);
+ test_assert(!view_is_file_refed(1) && view_is_file_refed(2) &&
+ view_is_file_refed(3));
+ oldfile = log->files;
+ buffer_free(&log->files->buffer);
+ log->files = log->files->next;
+ i_free(oldfile);
+ test_assert(log->files->hdr.file_seq == 2);
+ test_end();
+
+ /* --- first file has been removed --- */
+
+ test_begin("set 2-3");
+ test_assert(mail_transaction_log_view_set(view, 2, 0, (uint32_t)-1, UOFF_T_MAX, &reset, &reason) == 1);
+ test_end();
+
+ test_begin("missing log handing");
+ test_assert(mail_transaction_log_view_set(view, 0, 0, (uint32_t)-1, UOFF_T_MAX, &reset, &reason) == 0);
+ test_end();
+
+ test_begin("closed log handling");
+ view->log = NULL;
+ test_assert(mail_transaction_log_view_set(view, 0, 0, (uint32_t)-1, UOFF_T_MAX, &reset, &reason) == 0);
+ view->log = log;
+ test_end();
+
+ test_begin("clean refcount=0 files");
+ oldfile = log->files;
+ /* clear all references */
+ mail_transaction_log_view_clear(view, 0);
+ clean_refcount0_files = TRUE;
+ /* create a new file during mail_transaction_log_view_set(), which
+ triggers freeing any unreferenced files. */
+ test_assert(mail_transaction_log_view_set(view, 2, 0, 4, UOFF_T_MAX, &reset, &reason) == 1);
+ clean_refcount0_files = FALSE;
+ log->files = oldfile;
+ test_end();
+
+ mail_transaction_log_view_close(&view);
+ i_free(log->index);
+ while (log->files != NULL) {
+ oldfile = log->files;
+ buffer_free(&log->files->buffer);
+ log->files = log->files->next;
+ i_free(oldfile);
+ }
+ i_free(log);
+}
+
+int main(void)
+{
+ static void (*const test_functions[])(void) = {
+ test_mail_transaction_log_view,
+ NULL
+ };
+ return test_run(test_functions);
+}
diff --git a/src/lib-lda/Makefile.am b/src/lib-lda/Makefile.am
new file mode 100644
index 0000000..3ea04f3
--- /dev/null
+++ b/src/lib-lda/Makefile.am
@@ -0,0 +1,35 @@
+noinst_LTLIBRARIES = liblda.la
+
+AM_CPPFLAGS = \
+ -I$(top_srcdir)/src/lib \
+ -I$(top_srcdir)/src/lib-settings \
+ -I$(top_srcdir)/src/lib-smtp \
+ -I$(top_srcdir)/src/lib-index \
+ -I$(top_srcdir)/src/lib-imap \
+ -I$(top_srcdir)/src/lib-smtp \
+ -I$(top_srcdir)/src/lib-mail \
+ -I$(top_srcdir)/src/lib-storage \
+ -I$(top_srcdir)/src/lib-ssl-iostream
+
+liblda_la_SOURCES = \
+ lda-settings.c \
+ mail-deliver.c \
+ mail-send.c
+
+headers = \
+ lda-settings.h \
+ mail-deliver.h \
+ mail-send.h
+
+pkginc_libdir=$(pkgincludedir)
+pkginc_lib_HEADERS = $(headers)
+
+deps=../lib-storage/libdovecot-storage.la ../lib-dovecot/libdovecot.la
+
+pkglib_LTLIBRARIES = libdovecot-lda.la
+libdovecot_lda_la_SOURCES =
+libdovecot_lda_la_LIBADD = liblda.la $(deps)
+libdovecot_lda_la_DEPENDENCIES = liblda.la $(deps)
+libdovecot_lda_la_LDFLAGS = -export-dynamic
+
+
diff --git a/src/lib-lda/Makefile.in b/src/lib-lda/Makefile.in
new file mode 100644
index 0000000..9d2713a
--- /dev/null
+++ b/src/lib-lda/Makefile.in
@@ -0,0 +1,878 @@
+# Makefile.in generated by automake 1.16.1 from Makefile.am.
+# @configure_input@
+
+# Copyright (C) 1994-2018 Free Software Foundation, Inc.
+
+# This Makefile.in is free software; the Free Software Foundation
+# gives unlimited permission to copy and/or distribute it,
+# with or without modifications, as long as this notice is preserved.
+
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY, to the extent permitted by law; without
+# even the implied warranty of MERCHANTABILITY or FITNESS FOR A
+# PARTICULAR PURPOSE.
+
+@SET_MAKE@
+
+
+VPATH = @srcdir@
+am__is_gnu_make = { \
+ if test -z '$(MAKELEVEL)'; then \
+ false; \
+ elif test -n '$(MAKE_HOST)'; then \
+ true; \
+ elif test -n '$(MAKE_VERSION)' && test -n '$(CURDIR)'; then \
+ true; \
+ else \
+ false; \
+ fi; \
+}
+am__make_running_with_option = \
+ case $${target_option-} in \
+ ?) ;; \
+ *) echo "am__make_running_with_option: internal error: invalid" \
+ "target option '$${target_option-}' specified" >&2; \
+ exit 1;; \
+ esac; \
+ has_opt=no; \
+ sane_makeflags=$$MAKEFLAGS; \
+ if $(am__is_gnu_make); then \
+ sane_makeflags=$$MFLAGS; \
+ else \
+ case $$MAKEFLAGS in \
+ *\\[\ \ ]*) \
+ bs=\\; \
+ sane_makeflags=`printf '%s\n' "$$MAKEFLAGS" \
+ | sed "s/$$bs$$bs[$$bs $$bs ]*//g"`;; \
+ esac; \
+ fi; \
+ skip_next=no; \
+ strip_trailopt () \
+ { \
+ flg=`printf '%s\n' "$$flg" | sed "s/$$1.*$$//"`; \
+ }; \
+ for flg in $$sane_makeflags; do \
+ test $$skip_next = yes && { skip_next=no; continue; }; \
+ case $$flg in \
+ *=*|--*) continue;; \
+ -*I) strip_trailopt 'I'; skip_next=yes;; \
+ -*I?*) strip_trailopt 'I';; \
+ -*O) strip_trailopt 'O'; skip_next=yes;; \
+ -*O?*) strip_trailopt 'O';; \
+ -*l) strip_trailopt 'l'; skip_next=yes;; \
+ -*l?*) strip_trailopt 'l';; \
+ -[dEDm]) skip_next=yes;; \
+ -[JT]) skip_next=yes;; \
+ esac; \
+ case $$flg in \
+ *$$target_option*) has_opt=yes; break;; \
+ esac; \
+ done; \
+ test $$has_opt = yes
+am__make_dryrun = (target_option=n; $(am__make_running_with_option))
+am__make_keepgoing = (target_option=k; $(am__make_running_with_option))
+pkgdatadir = $(datadir)/@PACKAGE@
+pkgincludedir = $(includedir)/@PACKAGE@
+pkglibdir = $(libdir)/@PACKAGE@
+pkglibexecdir = $(libexecdir)/@PACKAGE@
+am__cd = CDPATH="$${ZSH_VERSION+.}$(PATH_SEPARATOR)" && cd
+install_sh_DATA = $(install_sh) -c -m 644
+install_sh_PROGRAM = $(install_sh) -c
+install_sh_SCRIPT = $(install_sh) -c
+INSTALL_HEADER = $(INSTALL_DATA)
+transform = $(program_transform_name)
+NORMAL_INSTALL = :
+PRE_INSTALL = :
+POST_INSTALL = :
+NORMAL_UNINSTALL = :
+PRE_UNINSTALL = :
+POST_UNINSTALL = :
+build_triplet = @build@
+host_triplet = @host@
+subdir = src/lib-lda
+ACLOCAL_M4 = $(top_srcdir)/aclocal.m4
+am__aclocal_m4_deps = $(top_srcdir)/m4/ac_checktype2.m4 \
+ $(top_srcdir)/m4/ac_typeof.m4 $(top_srcdir)/m4/arc4random.m4 \
+ $(top_srcdir)/m4/blockdev.m4 $(top_srcdir)/m4/c99_vsnprintf.m4 \
+ $(top_srcdir)/m4/clock_gettime.m4 $(top_srcdir)/m4/crypt.m4 \
+ $(top_srcdir)/m4/crypt_xpg6.m4 $(top_srcdir)/m4/dbqlk.m4 \
+ $(top_srcdir)/m4/dirent_dtype.m4 $(top_srcdir)/m4/dovecot.m4 \
+ $(top_srcdir)/m4/fd_passing.m4 $(top_srcdir)/m4/fdatasync.m4 \
+ $(top_srcdir)/m4/flexible_array_member.m4 \
+ $(top_srcdir)/m4/glibc.m4 $(top_srcdir)/m4/gmtime_max.m4 \
+ $(top_srcdir)/m4/gmtime_tm_gmtoff.m4 \
+ $(top_srcdir)/m4/ioloop.m4 $(top_srcdir)/m4/iovec.m4 \
+ $(top_srcdir)/m4/ipv6.m4 $(top_srcdir)/m4/libcap.m4 \
+ $(top_srcdir)/m4/libtool.m4 $(top_srcdir)/m4/libwrap.m4 \
+ $(top_srcdir)/m4/linux_mremap.m4 $(top_srcdir)/m4/ltoptions.m4 \
+ $(top_srcdir)/m4/ltsugar.m4 $(top_srcdir)/m4/ltversion.m4 \
+ $(top_srcdir)/m4/lt~obsolete.m4 $(top_srcdir)/m4/mmap_write.m4 \
+ $(top_srcdir)/m4/mntctl.m4 $(top_srcdir)/m4/modules.m4 \
+ $(top_srcdir)/m4/notify.m4 $(top_srcdir)/m4/nsl.m4 \
+ $(top_srcdir)/m4/off_t_max.m4 $(top_srcdir)/m4/pkg.m4 \
+ $(top_srcdir)/m4/pr_set_dumpable.m4 \
+ $(top_srcdir)/m4/q_quotactl.m4 $(top_srcdir)/m4/quota.m4 \
+ $(top_srcdir)/m4/random.m4 $(top_srcdir)/m4/rlimit.m4 \
+ $(top_srcdir)/m4/sendfile.m4 $(top_srcdir)/m4/size_t_signed.m4 \
+ $(top_srcdir)/m4/sockpeercred.m4 $(top_srcdir)/m4/sql.m4 \
+ $(top_srcdir)/m4/ssl.m4 $(top_srcdir)/m4/st_tim.m4 \
+ $(top_srcdir)/m4/static_array.m4 $(top_srcdir)/m4/test_with.m4 \
+ $(top_srcdir)/m4/time_t.m4 $(top_srcdir)/m4/typeof.m4 \
+ $(top_srcdir)/m4/typeof_dev_t.m4 \
+ $(top_srcdir)/m4/uoff_t_max.m4 $(top_srcdir)/m4/vararg.m4 \
+ $(top_srcdir)/m4/want_apparmor.m4 \
+ $(top_srcdir)/m4/want_bsdauth.m4 \
+ $(top_srcdir)/m4/want_bzlib.m4 \
+ $(top_srcdir)/m4/want_cassandra.m4 \
+ $(top_srcdir)/m4/want_cdb.m4 \
+ $(top_srcdir)/m4/want_checkpassword.m4 \
+ $(top_srcdir)/m4/want_clucene.m4 $(top_srcdir)/m4/want_db.m4 \
+ $(top_srcdir)/m4/want_gssapi.m4 $(top_srcdir)/m4/want_icu.m4 \
+ $(top_srcdir)/m4/want_ldap.m4 $(top_srcdir)/m4/want_lua.m4 \
+ $(top_srcdir)/m4/want_lz4.m4 $(top_srcdir)/m4/want_lzma.m4 \
+ $(top_srcdir)/m4/want_mysql.m4 $(top_srcdir)/m4/want_pam.m4 \
+ $(top_srcdir)/m4/want_passwd.m4 $(top_srcdir)/m4/want_pgsql.m4 \
+ $(top_srcdir)/m4/want_prefetch.m4 \
+ $(top_srcdir)/m4/want_shadow.m4 \
+ $(top_srcdir)/m4/want_sodium.m4 $(top_srcdir)/m4/want_solr.m4 \
+ $(top_srcdir)/m4/want_sqlite.m4 \
+ $(top_srcdir)/m4/want_stemmer.m4 \
+ $(top_srcdir)/m4/want_systemd.m4 \
+ $(top_srcdir)/m4/want_textcat.m4 \
+ $(top_srcdir)/m4/want_unwind.m4 $(top_srcdir)/m4/want_zlib.m4 \
+ $(top_srcdir)/m4/want_zstd.m4 $(top_srcdir)/configure.ac
+am__configure_deps = $(am__aclocal_m4_deps) $(CONFIGURE_DEPENDENCIES) \
+ $(ACLOCAL_M4)
+DIST_COMMON = $(srcdir)/Makefile.am $(pkginc_lib_HEADERS) \
+ $(am__DIST_COMMON)
+mkinstalldirs = $(install_sh) -d
+CONFIG_HEADER = $(top_builddir)/config.h
+CONFIG_CLEAN_FILES =
+CONFIG_CLEAN_VPATH_FILES =
+am__vpath_adj_setup = srcdirstrip=`echo "$(srcdir)" | sed 's|.|.|g'`;
+am__vpath_adj = case $$p in \
+ $(srcdir)/*) f=`echo "$$p" | sed "s|^$$srcdirstrip/||"`;; \
+ *) f=$$p;; \
+ esac;
+am__strip_dir = f=`echo $$p | sed -e 's|^.*/||'`;
+am__install_max = 40
+am__nobase_strip_setup = \
+ srcdirstrip=`echo "$(srcdir)" | sed 's/[].[^$$\\*|]/\\\\&/g'`
+am__nobase_strip = \
+ for p in $$list; do echo "$$p"; done | sed -e "s|$$srcdirstrip/||"
+am__nobase_list = $(am__nobase_strip_setup); \
+ for p in $$list; do echo "$$p $$p"; done | \
+ sed "s| $$srcdirstrip/| |;"' / .*\//!s/ .*/ ./; s,\( .*\)/[^/]*$$,\1,' | \
+ $(AWK) 'BEGIN { files["."] = "" } { files[$$2] = files[$$2] " " $$1; \
+ if (++n[$$2] == $(am__install_max)) \
+ { print $$2, files[$$2]; n[$$2] = 0; files[$$2] = "" } } \
+ END { for (dir in files) print dir, files[dir] }'
+am__base_list = \
+ sed '$$!N;$$!N;$$!N;$$!N;$$!N;$$!N;$$!N;s/\n/ /g' | \
+ sed '$$!N;$$!N;$$!N;$$!N;s/\n/ /g'
+am__uninstall_files_from_dir = { \
+ test -z "$$files" \
+ || { test ! -d "$$dir" && test ! -f "$$dir" && test ! -r "$$dir"; } \
+ || { echo " ( cd '$$dir' && rm -f" $$files ")"; \
+ $(am__cd) "$$dir" && rm -f $$files; }; \
+ }
+am__installdirs = "$(DESTDIR)$(pkglibdir)" \
+ "$(DESTDIR)$(pkginc_libdir)"
+LTLIBRARIES = $(noinst_LTLIBRARIES) $(pkglib_LTLIBRARIES)
+am_libdovecot_lda_la_OBJECTS =
+libdovecot_lda_la_OBJECTS = $(am_libdovecot_lda_la_OBJECTS)
+AM_V_lt = $(am__v_lt_@AM_V@)
+am__v_lt_ = $(am__v_lt_@AM_DEFAULT_V@)
+am__v_lt_0 = --silent
+am__v_lt_1 =
+libdovecot_lda_la_LINK = $(LIBTOOL) $(AM_V_lt) --tag=CC \
+ $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=link $(CCLD) \
+ $(AM_CFLAGS) $(CFLAGS) $(libdovecot_lda_la_LDFLAGS) $(LDFLAGS) \
+ -o $@
+liblda_la_LIBADD =
+am_liblda_la_OBJECTS = lda-settings.lo mail-deliver.lo mail-send.lo
+liblda_la_OBJECTS = $(am_liblda_la_OBJECTS)
+AM_V_P = $(am__v_P_@AM_V@)
+am__v_P_ = $(am__v_P_@AM_DEFAULT_V@)
+am__v_P_0 = false
+am__v_P_1 = :
+AM_V_GEN = $(am__v_GEN_@AM_V@)
+am__v_GEN_ = $(am__v_GEN_@AM_DEFAULT_V@)
+am__v_GEN_0 = @echo " GEN " $@;
+am__v_GEN_1 =
+AM_V_at = $(am__v_at_@AM_V@)
+am__v_at_ = $(am__v_at_@AM_DEFAULT_V@)
+am__v_at_0 = @
+am__v_at_1 =
+DEFAULT_INCLUDES = -I.@am__isrc@ -I$(top_builddir)
+depcomp = $(SHELL) $(top_srcdir)/depcomp
+am__maybe_remake_depfiles = depfiles
+am__depfiles_remade = ./$(DEPDIR)/lda-settings.Plo \
+ ./$(DEPDIR)/mail-deliver.Plo ./$(DEPDIR)/mail-send.Plo
+am__mv = mv -f
+COMPILE = $(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) \
+ $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS)
+LTCOMPILE = $(LIBTOOL) $(AM_V_lt) --tag=CC $(AM_LIBTOOLFLAGS) \
+ $(LIBTOOLFLAGS) --mode=compile $(CC) $(DEFS) \
+ $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) $(CPPFLAGS) \
+ $(AM_CFLAGS) $(CFLAGS)
+AM_V_CC = $(am__v_CC_@AM_V@)
+am__v_CC_ = $(am__v_CC_@AM_DEFAULT_V@)
+am__v_CC_0 = @echo " CC " $@;
+am__v_CC_1 =
+CCLD = $(CC)
+LINK = $(LIBTOOL) $(AM_V_lt) --tag=CC $(AM_LIBTOOLFLAGS) \
+ $(LIBTOOLFLAGS) --mode=link $(CCLD) $(AM_CFLAGS) $(CFLAGS) \
+ $(AM_LDFLAGS) $(LDFLAGS) -o $@
+AM_V_CCLD = $(am__v_CCLD_@AM_V@)
+am__v_CCLD_ = $(am__v_CCLD_@AM_DEFAULT_V@)
+am__v_CCLD_0 = @echo " CCLD " $@;
+am__v_CCLD_1 =
+SOURCES = $(libdovecot_lda_la_SOURCES) $(liblda_la_SOURCES)
+DIST_SOURCES = $(libdovecot_lda_la_SOURCES) $(liblda_la_SOURCES)
+am__can_run_installinfo = \
+ case $$AM_UPDATE_INFO_DIR in \
+ n|no|NO) false;; \
+ *) (install-info --version) >/dev/null 2>&1;; \
+ esac
+HEADERS = $(pkginc_lib_HEADERS)
+am__tagged_files = $(HEADERS) $(SOURCES) $(TAGS_FILES) $(LISP)
+# Read a list of newline-separated strings from the standard input,
+# and print each of them once, without duplicates. Input order is
+# *not* preserved.
+am__uniquify_input = $(AWK) '\
+ BEGIN { nonempty = 0; } \
+ { items[$$0] = 1; nonempty = 1; } \
+ END { if (nonempty) { for (i in items) print i; }; } \
+'
+# Make sure the list of sources is unique. This is necessary because,
+# e.g., the same source file might be shared among _SOURCES variables
+# for different programs/libraries.
+am__define_uniq_tagged_files = \
+ list='$(am__tagged_files)'; \
+ unique=`for i in $$list; do \
+ if test -f "$$i"; then echo $$i; else echo $(srcdir)/$$i; fi; \
+ done | $(am__uniquify_input)`
+ETAGS = etags
+CTAGS = ctags
+am__DIST_COMMON = $(srcdir)/Makefile.in $(top_srcdir)/depcomp
+DISTFILES = $(DIST_COMMON) $(DIST_SOURCES) $(TEXINFOS) $(EXTRA_DIST)
+ACLOCAL = @ACLOCAL@
+ACLOCAL_AMFLAGS = @ACLOCAL_AMFLAGS@
+AMTAR = @AMTAR@
+AM_DEFAULT_VERBOSITY = @AM_DEFAULT_VERBOSITY@
+APPARMOR_LIBS = @APPARMOR_LIBS@
+AR = @AR@
+AUTH_CFLAGS = @AUTH_CFLAGS@
+AUTH_LIBS = @AUTH_LIBS@
+AUTOCONF = @AUTOCONF@
+AUTOHEADER = @AUTOHEADER@
+AUTOMAKE = @AUTOMAKE@
+AWK = @AWK@
+BINARY_CFLAGS = @BINARY_CFLAGS@
+BINARY_LDFLAGS = @BINARY_LDFLAGS@
+BISON = @BISON@
+CASSANDRA_CFLAGS = @CASSANDRA_CFLAGS@
+CASSANDRA_LIBS = @CASSANDRA_LIBS@
+CC = @CC@
+CCDEPMODE = @CCDEPMODE@
+CDB_LIBS = @CDB_LIBS@
+CFLAGS = @CFLAGS@
+CLUCENE_CFLAGS = @CLUCENE_CFLAGS@
+CLUCENE_LIBS = @CLUCENE_LIBS@
+COMPRESS_LIBS = @COMPRESS_LIBS@
+CPP = @CPP@
+CPPFLAGS = @CPPFLAGS@
+CRYPT_LIBS = @CRYPT_LIBS@
+CXX = @CXX@
+CXXCPP = @CXXCPP@
+CXXDEPMODE = @CXXDEPMODE@
+CXXFLAGS = @CXXFLAGS@
+CYGPATH_W = @CYGPATH_W@
+DEFS = @DEFS@
+DEPDIR = @DEPDIR@
+DICT_LIBS = @DICT_LIBS@
+DLLIB = @DLLIB@
+DLLTOOL = @DLLTOOL@
+DSYMUTIL = @DSYMUTIL@
+DUMPBIN = @DUMPBIN@
+ECHO_C = @ECHO_C@
+ECHO_N = @ECHO_N@
+ECHO_T = @ECHO_T@
+EGREP = @EGREP@
+EXEEXT = @EXEEXT@
+FGREP = @FGREP@
+FLEX = @FLEX@
+FUZZER_CPPFLAGS = @FUZZER_CPPFLAGS@
+FUZZER_LDFLAGS = @FUZZER_LDFLAGS@
+GREP = @GREP@
+INSTALL = @INSTALL@
+INSTALL_DATA = @INSTALL_DATA@
+INSTALL_PROGRAM = @INSTALL_PROGRAM@
+INSTALL_SCRIPT = @INSTALL_SCRIPT@
+INSTALL_STRIP_PROGRAM = @INSTALL_STRIP_PROGRAM@
+KRB5CONFIG = @KRB5CONFIG@
+KRB5_CFLAGS = @KRB5_CFLAGS@
+KRB5_LIBS = @KRB5_LIBS@
+LD = @LD@
+LDAP_LIBS = @LDAP_LIBS@
+LDFLAGS = @LDFLAGS@
+LD_NO_WHOLE_ARCHIVE = @LD_NO_WHOLE_ARCHIVE@
+LD_WHOLE_ARCHIVE = @LD_WHOLE_ARCHIVE@
+LIBCAP = @LIBCAP@
+LIBDOVECOT = @LIBDOVECOT@
+LIBDOVECOT_COMPRESS = @LIBDOVECOT_COMPRESS@
+LIBDOVECOT_DEPS = @LIBDOVECOT_DEPS@
+LIBDOVECOT_DSYNC = @LIBDOVECOT_DSYNC@
+LIBDOVECOT_LA_LIBS = @LIBDOVECOT_LA_LIBS@
+LIBDOVECOT_LDA = @LIBDOVECOT_LDA@
+LIBDOVECOT_LDAP = @LIBDOVECOT_LDAP@
+LIBDOVECOT_LIBFTS = @LIBDOVECOT_LIBFTS@
+LIBDOVECOT_LIBFTS_DEPS = @LIBDOVECOT_LIBFTS_DEPS@
+LIBDOVECOT_LOGIN = @LIBDOVECOT_LOGIN@
+LIBDOVECOT_LUA = @LIBDOVECOT_LUA@
+LIBDOVECOT_LUA_DEPS = @LIBDOVECOT_LUA_DEPS@
+LIBDOVECOT_SQL = @LIBDOVECOT_SQL@
+LIBDOVECOT_STORAGE = @LIBDOVECOT_STORAGE@
+LIBDOVECOT_STORAGE_DEPS = @LIBDOVECOT_STORAGE_DEPS@
+LIBEXTTEXTCAT_CFLAGS = @LIBEXTTEXTCAT_CFLAGS@
+LIBEXTTEXTCAT_LIBS = @LIBEXTTEXTCAT_LIBS@
+LIBICONV = @LIBICONV@
+LIBICU_CFLAGS = @LIBICU_CFLAGS@
+LIBICU_LIBS = @LIBICU_LIBS@
+LIBOBJS = @LIBOBJS@
+LIBS = @LIBS@
+LIBSODIUM_CFLAGS = @LIBSODIUM_CFLAGS@
+LIBSODIUM_LIBS = @LIBSODIUM_LIBS@
+LIBTIRPC_CFLAGS = @LIBTIRPC_CFLAGS@
+LIBTIRPC_LIBS = @LIBTIRPC_LIBS@
+LIBTOOL = @LIBTOOL@
+LIBUNWIND_CFLAGS = @LIBUNWIND_CFLAGS@
+LIBUNWIND_LIBS = @LIBUNWIND_LIBS@
+LIBWRAP_LIBS = @LIBWRAP_LIBS@
+LINKED_STORAGE_LDADD = @LINKED_STORAGE_LDADD@
+LIPO = @LIPO@
+LN_S = @LN_S@
+LTLIBICONV = @LTLIBICONV@
+LTLIBOBJS = @LTLIBOBJS@
+LT_SYS_LIBRARY_PATH = @LT_SYS_LIBRARY_PATH@
+LUA_CFLAGS = @LUA_CFLAGS@
+LUA_LIBS = @LUA_LIBS@
+MAINT = @MAINT@
+MAKEINFO = @MAKEINFO@
+MANIFEST_TOOL = @MANIFEST_TOOL@
+MKDIR_P = @MKDIR_P@
+MODULE_LIBS = @MODULE_LIBS@
+MODULE_SUFFIX = @MODULE_SUFFIX@
+MYSQL_CFLAGS = @MYSQL_CFLAGS@
+MYSQL_CONFIG = @MYSQL_CONFIG@
+MYSQL_LIBS = @MYSQL_LIBS@
+NM = @NM@
+NMEDIT = @NMEDIT@
+NOPLUGIN_LDFLAGS = @NOPLUGIN_LDFLAGS@
+OBJDUMP = @OBJDUMP@
+OBJEXT = @OBJEXT@
+OTOOL = @OTOOL@
+OTOOL64 = @OTOOL64@
+PACKAGE = @PACKAGE@
+PACKAGE_BUGREPORT = @PACKAGE_BUGREPORT@
+PACKAGE_NAME = @PACKAGE_NAME@
+PACKAGE_STRING = @PACKAGE_STRING@
+PACKAGE_TARNAME = @PACKAGE_TARNAME@
+PACKAGE_URL = @PACKAGE_URL@
+PACKAGE_VERSION = @PACKAGE_VERSION@
+PANDOC = @PANDOC@
+PATH_SEPARATOR = @PATH_SEPARATOR@
+PGSQL_CFLAGS = @PGSQL_CFLAGS@
+PGSQL_LIBS = @PGSQL_LIBS@
+PG_CONFIG = @PG_CONFIG@
+PIE_CFLAGS = @PIE_CFLAGS@
+PIE_LDFLAGS = @PIE_LDFLAGS@
+PKG_CONFIG = @PKG_CONFIG@
+PKG_CONFIG_LIBDIR = @PKG_CONFIG_LIBDIR@
+PKG_CONFIG_PATH = @PKG_CONFIG_PATH@
+QUOTA_LIBS = @QUOTA_LIBS@
+RANLIB = @RANLIB@
+RELRO_LDFLAGS = @RELRO_LDFLAGS@
+RPCGEN = @RPCGEN@
+RUN_TEST = @RUN_TEST@
+SED = @SED@
+SETTING_FILES = @SETTING_FILES@
+SET_MAKE = @SET_MAKE@
+SHELL = @SHELL@
+SQLITE_CFLAGS = @SQLITE_CFLAGS@
+SQLITE_LIBS = @SQLITE_LIBS@
+SQL_CFLAGS = @SQL_CFLAGS@
+SQL_LIBS = @SQL_LIBS@
+SSL_CFLAGS = @SSL_CFLAGS@
+SSL_LIBS = @SSL_LIBS@
+STRIP = @STRIP@
+SYSTEMD_CFLAGS = @SYSTEMD_CFLAGS@
+SYSTEMD_LIBS = @SYSTEMD_LIBS@
+VALGRIND = @VALGRIND@
+VERSION = @VERSION@
+ZSTD_CFLAGS = @ZSTD_CFLAGS@
+ZSTD_LIBS = @ZSTD_LIBS@
+abs_builddir = @abs_builddir@
+abs_srcdir = @abs_srcdir@
+abs_top_builddir = @abs_top_builddir@
+abs_top_srcdir = @abs_top_srcdir@
+ac_ct_AR = @ac_ct_AR@
+ac_ct_CC = @ac_ct_CC@
+ac_ct_CXX = @ac_ct_CXX@
+ac_ct_DUMPBIN = @ac_ct_DUMPBIN@
+am__include = @am__include@
+am__leading_dot = @am__leading_dot@
+am__quote = @am__quote@
+am__tar = @am__tar@
+am__untar = @am__untar@
+bindir = @bindir@
+build = @build@
+build_alias = @build_alias@
+build_cpu = @build_cpu@
+build_os = @build_os@
+build_vendor = @build_vendor@
+builddir = @builddir@
+datadir = @datadir@
+datarootdir = @datarootdir@
+dict_drivers = @dict_drivers@
+docdir = @docdir@
+dvidir = @dvidir@
+exec_prefix = @exec_prefix@
+host = @host@
+host_alias = @host_alias@
+host_cpu = @host_cpu@
+host_os = @host_os@
+host_vendor = @host_vendor@
+htmldir = @htmldir@
+includedir = @includedir@
+infodir = @infodir@
+install_sh = @install_sh@
+libdir = @libdir@
+libexecdir = @libexecdir@
+localedir = @localedir@
+localstatedir = @localstatedir@
+mandir = @mandir@
+mkdir_p = @mkdir_p@
+moduledir = @moduledir@
+oldincludedir = @oldincludedir@
+pdfdir = @pdfdir@
+prefix = @prefix@
+program_transform_name = @program_transform_name@
+psdir = @psdir@
+rundir = @rundir@
+runstatedir = @runstatedir@
+sbindir = @sbindir@
+sharedstatedir = @sharedstatedir@
+sql_drivers = @sql_drivers@
+srcdir = @srcdir@
+ssldir = @ssldir@
+statedir = @statedir@
+sysconfdir = @sysconfdir@
+systemdservicetype = @systemdservicetype@
+systemdsystemunitdir = @systemdsystemunitdir@
+target_alias = @target_alias@
+top_build_prefix = @top_build_prefix@
+top_builddir = @top_builddir@
+top_srcdir = @top_srcdir@
+noinst_LTLIBRARIES = liblda.la
+AM_CPPFLAGS = \
+ -I$(top_srcdir)/src/lib \
+ -I$(top_srcdir)/src/lib-settings \
+ -I$(top_srcdir)/src/lib-smtp \
+ -I$(top_srcdir)/src/lib-index \
+ -I$(top_srcdir)/src/lib-imap \
+ -I$(top_srcdir)/src/lib-smtp \
+ -I$(top_srcdir)/src/lib-mail \
+ -I$(top_srcdir)/src/lib-storage \
+ -I$(top_srcdir)/src/lib-ssl-iostream
+
+liblda_la_SOURCES = \
+ lda-settings.c \
+ mail-deliver.c \
+ mail-send.c
+
+headers = \
+ lda-settings.h \
+ mail-deliver.h \
+ mail-send.h
+
+pkginc_libdir = $(pkgincludedir)
+pkginc_lib_HEADERS = $(headers)
+deps = ../lib-storage/libdovecot-storage.la ../lib-dovecot/libdovecot.la
+pkglib_LTLIBRARIES = libdovecot-lda.la
+libdovecot_lda_la_SOURCES =
+libdovecot_lda_la_LIBADD = liblda.la $(deps)
+libdovecot_lda_la_DEPENDENCIES = liblda.la $(deps)
+libdovecot_lda_la_LDFLAGS = -export-dynamic
+all: all-am
+
+.SUFFIXES:
+.SUFFIXES: .c .lo .o .obj
+$(srcdir)/Makefile.in: @MAINTAINER_MODE_TRUE@ $(srcdir)/Makefile.am $(am__configure_deps)
+ @for dep in $?; do \
+ case '$(am__configure_deps)' in \
+ *$$dep*) \
+ ( cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh ) \
+ && { if test -f $@; then exit 0; else break; fi; }; \
+ exit 1;; \
+ esac; \
+ done; \
+ echo ' cd $(top_srcdir) && $(AUTOMAKE) --foreign src/lib-lda/Makefile'; \
+ $(am__cd) $(top_srcdir) && \
+ $(AUTOMAKE) --foreign src/lib-lda/Makefile
+Makefile: $(srcdir)/Makefile.in $(top_builddir)/config.status
+ @case '$?' in \
+ *config.status*) \
+ cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh;; \
+ *) \
+ echo ' cd $(top_builddir) && $(SHELL) ./config.status $(subdir)/$@ $(am__maybe_remake_depfiles)'; \
+ cd $(top_builddir) && $(SHELL) ./config.status $(subdir)/$@ $(am__maybe_remake_depfiles);; \
+ esac;
+
+$(top_builddir)/config.status: $(top_srcdir)/configure $(CONFIG_STATUS_DEPENDENCIES)
+ cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh
+
+$(top_srcdir)/configure: @MAINTAINER_MODE_TRUE@ $(am__configure_deps)
+ cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh
+$(ACLOCAL_M4): @MAINTAINER_MODE_TRUE@ $(am__aclocal_m4_deps)
+ cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh
+$(am__aclocal_m4_deps):
+
+clean-noinstLTLIBRARIES:
+ -test -z "$(noinst_LTLIBRARIES)" || rm -f $(noinst_LTLIBRARIES)
+ @list='$(noinst_LTLIBRARIES)'; \
+ locs=`for p in $$list; do echo $$p; done | \
+ sed 's|^[^/]*$$|.|; s|/[^/]*$$||; s|$$|/so_locations|' | \
+ sort -u`; \
+ test -z "$$locs" || { \
+ echo rm -f $${locs}; \
+ rm -f $${locs}; \
+ }
+
+install-pkglibLTLIBRARIES: $(pkglib_LTLIBRARIES)
+ @$(NORMAL_INSTALL)
+ @list='$(pkglib_LTLIBRARIES)'; test -n "$(pkglibdir)" || list=; \
+ list2=; for p in $$list; do \
+ if test -f $$p; then \
+ list2="$$list2 $$p"; \
+ else :; fi; \
+ done; \
+ test -z "$$list2" || { \
+ echo " $(MKDIR_P) '$(DESTDIR)$(pkglibdir)'"; \
+ $(MKDIR_P) "$(DESTDIR)$(pkglibdir)" || exit 1; \
+ echo " $(LIBTOOL) $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=install $(INSTALL) $(INSTALL_STRIP_FLAG) $$list2 '$(DESTDIR)$(pkglibdir)'"; \
+ $(LIBTOOL) $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=install $(INSTALL) $(INSTALL_STRIP_FLAG) $$list2 "$(DESTDIR)$(pkglibdir)"; \
+ }
+
+uninstall-pkglibLTLIBRARIES:
+ @$(NORMAL_UNINSTALL)
+ @list='$(pkglib_LTLIBRARIES)'; test -n "$(pkglibdir)" || list=; \
+ for p in $$list; do \
+ $(am__strip_dir) \
+ echo " $(LIBTOOL) $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=uninstall rm -f '$(DESTDIR)$(pkglibdir)/$$f'"; \
+ $(LIBTOOL) $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=uninstall rm -f "$(DESTDIR)$(pkglibdir)/$$f"; \
+ done
+
+clean-pkglibLTLIBRARIES:
+ -test -z "$(pkglib_LTLIBRARIES)" || rm -f $(pkglib_LTLIBRARIES)
+ @list='$(pkglib_LTLIBRARIES)'; \
+ locs=`for p in $$list; do echo $$p; done | \
+ sed 's|^[^/]*$$|.|; s|/[^/]*$$||; s|$$|/so_locations|' | \
+ sort -u`; \
+ test -z "$$locs" || { \
+ echo rm -f $${locs}; \
+ rm -f $${locs}; \
+ }
+
+libdovecot-lda.la: $(libdovecot_lda_la_OBJECTS) $(libdovecot_lda_la_DEPENDENCIES) $(EXTRA_libdovecot_lda_la_DEPENDENCIES)
+ $(AM_V_CCLD)$(libdovecot_lda_la_LINK) -rpath $(pkglibdir) $(libdovecot_lda_la_OBJECTS) $(libdovecot_lda_la_LIBADD) $(LIBS)
+
+liblda.la: $(liblda_la_OBJECTS) $(liblda_la_DEPENDENCIES) $(EXTRA_liblda_la_DEPENDENCIES)
+ $(AM_V_CCLD)$(LINK) $(liblda_la_OBJECTS) $(liblda_la_LIBADD) $(LIBS)
+
+mostlyclean-compile:
+ -rm -f *.$(OBJEXT)
+
+distclean-compile:
+ -rm -f *.tab.c
+
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/lda-settings.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/mail-deliver.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/mail-send.Plo@am__quote@ # am--include-marker
+
+$(am__depfiles_remade):
+ @$(MKDIR_P) $(@D)
+ @echo '# dummy' >$@-t && $(am__mv) $@-t $@
+
+am--depfiles: $(am__depfiles_remade)
+
+.c.o:
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(COMPILE) -MT $@ -MD -MP -MF $(DEPDIR)/$*.Tpo -c -o $@ $<
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/$*.Tpo $(DEPDIR)/$*.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='$<' object='$@' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(COMPILE) -c -o $@ $<
+
+.c.obj:
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(COMPILE) -MT $@ -MD -MP -MF $(DEPDIR)/$*.Tpo -c -o $@ `$(CYGPATH_W) '$<'`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/$*.Tpo $(DEPDIR)/$*.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='$<' object='$@' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(COMPILE) -c -o $@ `$(CYGPATH_W) '$<'`
+
+.c.lo:
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(LTCOMPILE) -MT $@ -MD -MP -MF $(DEPDIR)/$*.Tpo -c -o $@ $<
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/$*.Tpo $(DEPDIR)/$*.Plo
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='$<' object='$@' libtool=yes @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(LTCOMPILE) -c -o $@ $<
+
+mostlyclean-libtool:
+ -rm -f *.lo
+
+clean-libtool:
+ -rm -rf .libs _libs
+install-pkginc_libHEADERS: $(pkginc_lib_HEADERS)
+ @$(NORMAL_INSTALL)
+ @list='$(pkginc_lib_HEADERS)'; test -n "$(pkginc_libdir)" || list=; \
+ if test -n "$$list"; then \
+ echo " $(MKDIR_P) '$(DESTDIR)$(pkginc_libdir)'"; \
+ $(MKDIR_P) "$(DESTDIR)$(pkginc_libdir)" || exit 1; \
+ fi; \
+ for p in $$list; do \
+ if test -f "$$p"; then d=; else d="$(srcdir)/"; fi; \
+ echo "$$d$$p"; \
+ done | $(am__base_list) | \
+ while read files; do \
+ echo " $(INSTALL_HEADER) $$files '$(DESTDIR)$(pkginc_libdir)'"; \
+ $(INSTALL_HEADER) $$files "$(DESTDIR)$(pkginc_libdir)" || exit $$?; \
+ done
+
+uninstall-pkginc_libHEADERS:
+ @$(NORMAL_UNINSTALL)
+ @list='$(pkginc_lib_HEADERS)'; test -n "$(pkginc_libdir)" || list=; \
+ files=`for p in $$list; do echo $$p; done | sed -e 's|^.*/||'`; \
+ dir='$(DESTDIR)$(pkginc_libdir)'; $(am__uninstall_files_from_dir)
+
+ID: $(am__tagged_files)
+ $(am__define_uniq_tagged_files); mkid -fID $$unique
+tags: tags-am
+TAGS: tags
+
+tags-am: $(TAGS_DEPENDENCIES) $(am__tagged_files)
+ set x; \
+ here=`pwd`; \
+ $(am__define_uniq_tagged_files); \
+ shift; \
+ if test -z "$(ETAGS_ARGS)$$*$$unique"; then :; else \
+ test -n "$$unique" || unique=$$empty_fix; \
+ if test $$# -gt 0; then \
+ $(ETAGS) $(ETAGSFLAGS) $(AM_ETAGSFLAGS) $(ETAGS_ARGS) \
+ "$$@" $$unique; \
+ else \
+ $(ETAGS) $(ETAGSFLAGS) $(AM_ETAGSFLAGS) $(ETAGS_ARGS) \
+ $$unique; \
+ fi; \
+ fi
+ctags: ctags-am
+
+CTAGS: ctags
+ctags-am: $(TAGS_DEPENDENCIES) $(am__tagged_files)
+ $(am__define_uniq_tagged_files); \
+ test -z "$(CTAGS_ARGS)$$unique" \
+ || $(CTAGS) $(CTAGSFLAGS) $(AM_CTAGSFLAGS) $(CTAGS_ARGS) \
+ $$unique
+
+GTAGS:
+ here=`$(am__cd) $(top_builddir) && pwd` \
+ && $(am__cd) $(top_srcdir) \
+ && gtags -i $(GTAGS_ARGS) "$$here"
+cscopelist: cscopelist-am
+
+cscopelist-am: $(am__tagged_files)
+ list='$(am__tagged_files)'; \
+ case "$(srcdir)" in \
+ [\\/]* | ?:[\\/]*) sdir="$(srcdir)" ;; \
+ *) sdir=$(subdir)/$(srcdir) ;; \
+ esac; \
+ for i in $$list; do \
+ if test -f "$$i"; then \
+ echo "$(subdir)/$$i"; \
+ else \
+ echo "$$sdir/$$i"; \
+ fi; \
+ done >> $(top_builddir)/cscope.files
+
+distclean-tags:
+ -rm -f TAGS ID GTAGS GRTAGS GSYMS GPATH tags
+
+distdir: $(BUILT_SOURCES)
+ $(MAKE) $(AM_MAKEFLAGS) distdir-am
+
+distdir-am: $(DISTFILES)
+ @srcdirstrip=`echo "$(srcdir)" | sed 's/[].[^$$\\*]/\\\\&/g'`; \
+ topsrcdirstrip=`echo "$(top_srcdir)" | sed 's/[].[^$$\\*]/\\\\&/g'`; \
+ list='$(DISTFILES)'; \
+ dist_files=`for file in $$list; do echo $$file; done | \
+ sed -e "s|^$$srcdirstrip/||;t" \
+ -e "s|^$$topsrcdirstrip/|$(top_builddir)/|;t"`; \
+ case $$dist_files in \
+ */*) $(MKDIR_P) `echo "$$dist_files" | \
+ sed '/\//!d;s|^|$(distdir)/|;s,/[^/]*$$,,' | \
+ sort -u` ;; \
+ esac; \
+ for file in $$dist_files; do \
+ if test -f $$file || test -d $$file; then d=.; else d=$(srcdir); fi; \
+ if test -d $$d/$$file; then \
+ dir=`echo "/$$file" | sed -e 's,/[^/]*$$,,'`; \
+ if test -d "$(distdir)/$$file"; then \
+ find "$(distdir)/$$file" -type d ! -perm -700 -exec chmod u+rwx {} \;; \
+ fi; \
+ if test -d $(srcdir)/$$file && test $$d != $(srcdir); then \
+ cp -fpR $(srcdir)/$$file "$(distdir)$$dir" || exit 1; \
+ find "$(distdir)/$$file" -type d ! -perm -700 -exec chmod u+rwx {} \;; \
+ fi; \
+ cp -fpR $$d/$$file "$(distdir)$$dir" || exit 1; \
+ else \
+ test -f "$(distdir)/$$file" \
+ || cp -p $$d/$$file "$(distdir)/$$file" \
+ || exit 1; \
+ fi; \
+ done
+check-am: all-am
+check: check-am
+all-am: Makefile $(LTLIBRARIES) $(HEADERS)
+installdirs:
+ for dir in "$(DESTDIR)$(pkglibdir)" "$(DESTDIR)$(pkginc_libdir)"; do \
+ test -z "$$dir" || $(MKDIR_P) "$$dir"; \
+ done
+install: install-am
+install-exec: install-exec-am
+install-data: install-data-am
+uninstall: uninstall-am
+
+install-am: all-am
+ @$(MAKE) $(AM_MAKEFLAGS) install-exec-am install-data-am
+
+installcheck: installcheck-am
+install-strip:
+ if test -z '$(STRIP)'; then \
+ $(MAKE) $(AM_MAKEFLAGS) INSTALL_PROGRAM="$(INSTALL_STRIP_PROGRAM)" \
+ install_sh_PROGRAM="$(INSTALL_STRIP_PROGRAM)" INSTALL_STRIP_FLAG=-s \
+ install; \
+ else \
+ $(MAKE) $(AM_MAKEFLAGS) INSTALL_PROGRAM="$(INSTALL_STRIP_PROGRAM)" \
+ install_sh_PROGRAM="$(INSTALL_STRIP_PROGRAM)" INSTALL_STRIP_FLAG=-s \
+ "INSTALL_PROGRAM_ENV=STRIPPROG='$(STRIP)'" install; \
+ fi
+mostlyclean-generic:
+
+clean-generic:
+
+distclean-generic:
+ -test -z "$(CONFIG_CLEAN_FILES)" || rm -f $(CONFIG_CLEAN_FILES)
+ -test . = "$(srcdir)" || test -z "$(CONFIG_CLEAN_VPATH_FILES)" || rm -f $(CONFIG_CLEAN_VPATH_FILES)
+
+maintainer-clean-generic:
+ @echo "This command is intended for maintainers to use"
+ @echo "it deletes files that may require special tools to rebuild."
+clean: clean-am
+
+clean-am: clean-generic clean-libtool clean-noinstLTLIBRARIES \
+ clean-pkglibLTLIBRARIES mostlyclean-am
+
+distclean: distclean-am
+ -rm -f ./$(DEPDIR)/lda-settings.Plo
+ -rm -f ./$(DEPDIR)/mail-deliver.Plo
+ -rm -f ./$(DEPDIR)/mail-send.Plo
+ -rm -f Makefile
+distclean-am: clean-am distclean-compile distclean-generic \
+ distclean-tags
+
+dvi: dvi-am
+
+dvi-am:
+
+html: html-am
+
+html-am:
+
+info: info-am
+
+info-am:
+
+install-data-am: install-pkginc_libHEADERS
+
+install-dvi: install-dvi-am
+
+install-dvi-am:
+
+install-exec-am: install-pkglibLTLIBRARIES
+
+install-html: install-html-am
+
+install-html-am:
+
+install-info: install-info-am
+
+install-info-am:
+
+install-man:
+
+install-pdf: install-pdf-am
+
+install-pdf-am:
+
+install-ps: install-ps-am
+
+install-ps-am:
+
+installcheck-am:
+
+maintainer-clean: maintainer-clean-am
+ -rm -f ./$(DEPDIR)/lda-settings.Plo
+ -rm -f ./$(DEPDIR)/mail-deliver.Plo
+ -rm -f ./$(DEPDIR)/mail-send.Plo
+ -rm -f Makefile
+maintainer-clean-am: distclean-am maintainer-clean-generic
+
+mostlyclean: mostlyclean-am
+
+mostlyclean-am: mostlyclean-compile mostlyclean-generic \
+ mostlyclean-libtool
+
+pdf: pdf-am
+
+pdf-am:
+
+ps: ps-am
+
+ps-am:
+
+uninstall-am: uninstall-pkginc_libHEADERS uninstall-pkglibLTLIBRARIES
+
+.MAKE: install-am install-strip
+
+.PHONY: CTAGS GTAGS TAGS all all-am am--depfiles check check-am clean \
+ clean-generic clean-libtool clean-noinstLTLIBRARIES \
+ clean-pkglibLTLIBRARIES cscopelist-am ctags ctags-am distclean \
+ distclean-compile distclean-generic distclean-libtool \
+ distclean-tags distdir dvi dvi-am html html-am info info-am \
+ install install-am install-data install-data-am install-dvi \
+ install-dvi-am install-exec install-exec-am install-html \
+ install-html-am install-info install-info-am install-man \
+ install-pdf install-pdf-am install-pkginc_libHEADERS \
+ install-pkglibLTLIBRARIES install-ps install-ps-am \
+ install-strip installcheck installcheck-am installdirs \
+ maintainer-clean maintainer-clean-generic mostlyclean \
+ mostlyclean-compile mostlyclean-generic mostlyclean-libtool \
+ pdf pdf-am ps ps-am tags tags-am uninstall uninstall-am \
+ uninstall-pkginc_libHEADERS uninstall-pkglibLTLIBRARIES
+
+.PRECIOUS: Makefile
+
+
+# Tell versions [3.59,3.63) of GNU make to not export all variables.
+# Otherwise a system limit (for SysV at least) may be exceeded.
+.NOEXPORT:
diff --git a/src/lib-lda/lda-settings.c b/src/lib-lda/lda-settings.c
new file mode 100644
index 0000000..1bb02f8
--- /dev/null
+++ b/src/lib-lda/lda-settings.c
@@ -0,0 +1,80 @@
+/* Copyright (c) 2005-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "hostpid.h"
+#include "settings-parser.h"
+#include "mail-storage-settings.h"
+#include "smtp-submit-settings.h"
+#include "lda-settings.h"
+
+#include <stddef.h>
+
+static bool lda_settings_check(void *_set, pool_t pool, const char **error_r);
+
+#undef DEF
+#undef DEFLIST
+#define DEF(type, name) \
+ SETTING_DEFINE_STRUCT_##type(#name, name, struct lda_settings)
+#define DEFLIST(field, name, defines) \
+ { .type = SET_DEFLIST, .key = name, \
+ .offset = offsetof(struct lda_settings, field), \
+ .list_info = defines }
+
+static const struct setting_define lda_setting_defines[] = {
+ DEF(STR, hostname),
+ DEF(STR, rejection_subject),
+ DEF(STR, rejection_reason),
+ DEF(STR, deliver_log_format),
+ DEF(STR, recipient_delimiter),
+ DEF(STR, lda_original_recipient_header),
+ DEF(BOOL, quota_full_tempfail),
+ DEF(BOOL, lda_mailbox_autocreate),
+ DEF(BOOL, lda_mailbox_autosubscribe),
+
+ SETTING_DEFINE_LIST_END
+};
+
+static const struct lda_settings lda_default_settings = {
+ .hostname = "",
+ .rejection_subject = "Rejected: %s",
+ .rejection_reason =
+ "Your message to <%t> was automatically rejected:%n%r",
+ .deliver_log_format = "msgid=%m: %$",
+ .recipient_delimiter = "+",
+ .lda_original_recipient_header = "",
+ .quota_full_tempfail = FALSE,
+ .lda_mailbox_autocreate = FALSE,
+ .lda_mailbox_autosubscribe = FALSE
+};
+
+static const struct setting_parser_info *lda_setting_dependencies[] = {
+ &mail_user_setting_parser_info,
+ &smtp_submit_setting_parser_info,
+ NULL
+};
+
+const struct setting_parser_info lda_setting_parser_info = {
+ .module_name = "lda",
+ .defines = lda_setting_defines,
+ .defaults = &lda_default_settings,
+
+ .type_offset = SIZE_MAX,
+ .struct_size = sizeof(struct lda_settings),
+
+ .parent_offset = SIZE_MAX,
+
+#ifndef CONFIG_BINARY
+ .check_func = lda_settings_check,
+#endif
+ .dependencies = lda_setting_dependencies
+};
+
+static bool lda_settings_check(void *_set, pool_t pool,
+ const char **error_r ATTR_UNUSED)
+{
+ struct lda_settings *set = _set;
+
+ if (*set->hostname == '\0')
+ set->hostname = p_strdup(pool, my_hostdomain());
+ return TRUE;
+}
diff --git a/src/lib-lda/lda-settings.h b/src/lib-lda/lda-settings.h
new file mode 100644
index 0000000..8b0c209
--- /dev/null
+++ b/src/lib-lda/lda-settings.h
@@ -0,0 +1,21 @@
+#ifndef LDA_SETTINGS_H
+#define LDA_SETTINGS_H
+
+struct mail_user_settings;
+
+struct lda_settings {
+ const char *hostname;
+ const char *rejection_subject;
+ const char *rejection_reason;
+ const char *deliver_log_format;
+ const char *recipient_delimiter;
+ const char *lda_original_recipient_header;
+
+ bool quota_full_tempfail;
+ bool lda_mailbox_autocreate;
+ bool lda_mailbox_autosubscribe;
+};
+
+extern const struct setting_parser_info lda_setting_parser_info;
+
+#endif
diff --git a/src/lib-lda/mail-deliver.c b/src/lib-lda/mail-deliver.c
new file mode 100644
index 0000000..e0f69da
--- /dev/null
+++ b/src/lib-lda/mail-deliver.c
@@ -0,0 +1,810 @@
+/* Copyright (c) 2005-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "ioloop.h"
+#include "array.h"
+#include "str.h"
+#include "str-sanitize.h"
+#include "time-util.h"
+#include "unichar.h"
+#include "var-expand.h"
+#include "message-address.h"
+#include "smtp-address.h"
+#include "lda-settings.h"
+#include "mail-storage.h"
+#include "mail-namespace.h"
+#include "mail-storage-private.h"
+#include "mail-duplicate.h"
+#include "mail-deliver.h"
+
+#define DUPLICATE_DB_NAME "lda-dupes"
+
+#define MAIL_DELIVER_USER_CONTEXT(obj) \
+ MODULE_CONTEXT_REQUIRE(obj, mail_deliver_user_module)
+#define MAIL_DELIVER_STORAGE_CONTEXT(obj) \
+ MODULE_CONTEXT_REQUIRE(obj, mail_deliver_storage_module)
+
+struct event_category event_category_mail_delivery = {
+ .name = "local-delivery",
+};
+
+struct mail_deliver_user {
+ union mail_user_module_context module_ctx;
+ struct mail_deliver_context *deliver_ctx;
+ bool want_storage_id;
+};
+
+deliver_mail_func_t *deliver_mail = NULL;
+
+struct mail_deliver_mailbox {
+ union mailbox_module_context module_ctx;
+};
+
+struct mail_deliver_transaction {
+ union mailbox_transaction_module_context module_ctx;
+
+ struct mail_deliver_fields deliver_fields;
+};
+
+static const char *lda_log_wanted_headers[] = {
+ "From", "Message-ID", "Subject",
+ NULL
+};
+static enum mail_fetch_field lda_log_wanted_fetch_fields =
+ MAIL_FETCH_PHYSICAL_SIZE | MAIL_FETCH_VIRTUAL_SIZE;
+static MODULE_CONTEXT_DEFINE_INIT(mail_deliver_user_module,
+ &mail_user_module_register);
+static MODULE_CONTEXT_DEFINE_INIT(mail_deliver_storage_module,
+ &mail_storage_module_register);
+
+static struct message_address *
+mail_deliver_get_message_address(struct mail *mail, const char *header)
+{
+ struct message_address *addr;
+ const char *str;
+
+ if (mail_get_first_header(mail, header, &str) <= 0)
+ return NULL;
+ addr = message_address_parse(pool_datastack_create(),
+ (const unsigned char *)str,
+ strlen(str), 1, 0);
+ if (addr == NULL || addr->mailbox == NULL || addr->domain == NULL ||
+ *addr->mailbox == '\0' || *addr->domain == '\0')
+ return NULL;
+ return addr;
+}
+
+const struct smtp_address *
+mail_deliver_get_address(struct mail *mail, const char *header)
+{
+ struct message_address *addr;
+ struct smtp_address *smtp_addr;
+
+ addr = mail_deliver_get_message_address(mail, header);
+ if (addr == NULL ||
+ smtp_address_create_from_msg_temp(addr, &smtp_addr) < 0)
+ return NULL;
+ return smtp_addr;
+}
+
+static void
+mail_deliver_update_event(struct mail_deliver_context *ctx)
+{
+ event_add_str(ctx->event, "message_id", ctx->fields.message_id);
+ event_add_str(ctx->event, "message_subject", ctx->fields.subject);
+ event_add_str(ctx->event, "message_from", ctx->fields.from);
+ if (ctx->fields.psize != UOFF_T_MAX)
+ event_add_int(ctx->event, "message_size", ctx->fields.psize);
+ if (ctx->fields.vsize != UOFF_T_MAX)
+ event_add_int(ctx->event, "message_vsize", ctx->fields.vsize);
+}
+
+static void
+update_str_field(pool_t pool, const char **old_str, const char *new_str)
+{
+ if (new_str == NULL || new_str[0] == '\0')
+ *old_str = NULL;
+ else if (*old_str == NULL || strcmp(*old_str, new_str) != 0)
+ *old_str = p_strdup(pool, new_str);
+}
+
+static void
+mail_deliver_fields_update(struct mail_deliver_fields *fields, pool_t pool,
+ struct mail *mail)
+{
+ const char *message_id = NULL, *subject = NULL, *from_envelope = NULL;
+ static struct message_address *from_addr;
+ const char *from;
+
+ if (fields->filled)
+ return;
+ fields->filled = TRUE;
+
+ if (mail_get_message_id(mail, &message_id) > 0)
+ message_id = str_sanitize(message_id, 200);
+ update_str_field(pool, &fields->message_id, message_id);
+
+ if (mail_get_first_header_utf8(mail, "Subject", &subject) > 0)
+ subject = str_sanitize(subject, 80);
+ update_str_field(pool, &fields->subject, subject);
+
+ from_addr = mail_deliver_get_message_address(mail, "From");
+ from = (from_addr == NULL ? NULL :
+ t_strconcat(from_addr->mailbox, "@", from_addr->domain, NULL));
+ update_str_field(pool, &fields->from, from);
+
+ if (mail_get_special(mail, MAIL_FETCH_FROM_ENVELOPE, &from_envelope) > 0)
+ from_envelope = str_sanitize(from_envelope, 80);
+ update_str_field(pool, &fields->from_envelope, from_envelope);
+
+ if (mail_get_physical_size(mail, &fields->psize) < 0)
+ fields->psize = 0;
+ if (mail_get_virtual_size(mail, &fields->vsize) < 0)
+ fields->vsize = 0;
+}
+
+const struct var_expand_table *
+mail_deliver_ctx_get_log_var_expand_table(struct mail_deliver_context *ctx,
+ const char *message)
+{
+ unsigned int delivery_time_msecs;
+
+ /* If a mail was saved/copied, the fields are already filled and the
+ following call is ignored. Otherwise, only the source mail exists. */
+ mail_deliver_fields_update(&ctx->fields, ctx->pool, ctx->src_mail);
+ /* This call finishes a mail delivery. With Sieve there may be multiple
+ mail deliveries. */
+ ctx->fields.filled = FALSE;
+
+ mail_deliver_update_event(ctx);
+
+ io_loop_time_refresh();
+ delivery_time_msecs = timeval_diff_msecs(&ioloop_timeval,
+ &ctx->delivery_time_started);
+
+ const struct var_expand_table stack_tab[] = {
+ { '$', message, NULL },
+ { 'm', ctx->fields.message_id != NULL ?
+ ctx->fields.message_id : "unspecified", "msgid" },
+ { 's', ctx->fields.subject, "subject" },
+ { 'f', ctx->fields.from, "from" },
+ { 'e', ctx->fields.from_envelope, "from_envelope" },
+ { 'p', dec2str(ctx->fields.psize), "size" },
+ { 'w', dec2str(ctx->fields.vsize), "vsize" },
+ { '\0', dec2str(delivery_time_msecs), "delivery_time" },
+ { '\0', dec2str(ctx->session_time_msecs), "session_time" },
+ { '\0', smtp_address_encode(ctx->rcpt_params.orcpt.addr), "to_envelope" },
+ { '\0', ctx->fields.storage_id, "storage_id" },
+ { '\0', NULL, NULL }
+ };
+ return p_memdup(unsafe_data_stack_pool, stack_tab, sizeof(stack_tab));
+}
+
+void mail_deliver_log(struct mail_deliver_context *ctx, const char *fmt, ...)
+{
+ va_list args;
+ string_t *str;
+ const struct var_expand_table *tab;
+ const char *msg, *error;
+
+ if (*ctx->set->deliver_log_format == '\0')
+ return;
+
+ va_start(args, fmt);
+ msg = t_strdup_vprintf(fmt, args);
+
+ str = t_str_new(256);
+ tab = mail_deliver_ctx_get_log_var_expand_table(ctx, msg);
+ if (var_expand(str, ctx->set->deliver_log_format, tab, &error) <= 0) {
+ e_error(ctx->event,
+ "Failed to expand deliver_log_format=%s: %s",
+ ctx->set->deliver_log_format, error);
+ }
+
+ e_info(ctx->event, "%s", str_c(str));
+ va_end(args);
+}
+
+struct mail_deliver_session *mail_deliver_session_init(void)
+{
+ struct mail_deliver_session *session;
+ pool_t pool;
+
+ pool = pool_alloconly_create("mail deliver session", 1024);
+ session = p_new(pool, struct mail_deliver_session, 1);
+ session->pool = pool;
+ return session;
+}
+
+void mail_deliver_session_deinit(struct mail_deliver_session **_session)
+{
+ struct mail_deliver_session *session = *_session;
+
+ *_session = NULL;
+ pool_unref(&session->pool);
+}
+
+int mail_deliver_save_open(struct mail_deliver_save_open_context *ctx,
+ const char *name, struct mailbox **box_r,
+ enum mail_error *error_r, const char **error_str_r)
+{
+ struct mailbox *box;
+ enum mailbox_flags flags = MAILBOX_FLAG_POST_SESSION;
+
+ *box_r = NULL;
+ *error_r = MAIL_ERROR_NONE;
+ *error_str_r = NULL;
+
+ if (!uni_utf8_str_is_valid(name)) {
+ *error_str_r = "Mailbox name not valid UTF-8";
+ *error_r = MAIL_ERROR_PARAMS;
+ return -1;
+ }
+
+ if (ctx->lda_mailbox_autocreate)
+ flags |= MAILBOX_FLAG_AUTO_CREATE;
+ if (ctx->lda_mailbox_autosubscribe)
+ flags |= MAILBOX_FLAG_AUTO_SUBSCRIBE;
+ *box_r = box = mailbox_alloc_for_user(ctx->user, name, flags);
+
+ if (mailbox_open(box) == 0)
+ return 0;
+ *error_str_r = mailbox_get_last_internal_error(box, error_r);
+ return -1;
+}
+
+static bool mail_deliver_check_duplicate(struct mail_deliver_session *session,
+ struct mailbox *box)
+{
+ struct mailbox_metadata metadata;
+ const guid_128_t *guid;
+
+ if (mailbox_get_metadata(box, MAILBOX_METADATA_GUID, &metadata) < 0) {
+ /* just play it safe and assume a duplicate */
+ return TRUE;
+ }
+
+ /* there shouldn't be all that many recipients,
+ so just do a linear search */
+ if (!array_is_created(&session->inbox_guids))
+ p_array_init(&session->inbox_guids, session->pool, 8);
+ array_foreach(&session->inbox_guids, guid) {
+ if (memcmp(metadata.guid, *guid, sizeof(metadata.guid)) == 0)
+ return TRUE;
+ }
+ array_push_back(&session->inbox_guids, &metadata.guid);
+ return FALSE;
+}
+
+void mail_deliver_deduplicate_guid_if_needed(struct mail_deliver_session *session,
+ struct mail_save_context *save_ctx)
+{
+ struct mailbox_transaction_context *trans =
+ mailbox_save_get_transaction(save_ctx);
+ struct mailbox *box = mailbox_transaction_get_mailbox(trans);
+ guid_128_t guid;
+
+ if (strcmp(mailbox_get_name(box), "INBOX") != 0)
+ return;
+
+ /* avoid storing duplicate GUIDs to delivered mails to INBOX. this
+ happens if mail is delivered to same user multiple times within a
+ session. the problem with this is that if GUIDs are used as POP3
+ UIDLs, some clients can't handle the duplicates well. */
+ if (mail_deliver_check_duplicate(session, box)) {
+ guid_128_generate(guid);
+ mailbox_save_set_guid(save_ctx, guid_128_to_string(guid));
+ }
+}
+
+void mail_deliver_init(struct mail_deliver_context *ctx,
+ struct mail_deliver_input *input)
+{
+ i_zero(ctx);
+ ctx->set = input->set;
+ ctx->smtp_set = input->smtp_set;
+
+ ctx->session = input->session;
+ ctx->pool = input->session->pool;
+ pool_ref(ctx->pool);
+
+ ctx->session_time_msecs = input->session_time_msecs;
+ ctx->delivery_time_started = input->delivery_time_started;
+ ctx->session_id = p_strdup(ctx->pool, input->session_id);
+ ctx->src_mail = input->src_mail;
+ ctx->save_dest_mail = input->save_dest_mail;
+
+ ctx->mail_from = smtp_address_clone(ctx->pool, input->mail_from);
+ smtp_params_mail_copy(ctx->pool, &ctx->mail_params,
+ &input->mail_params);
+ ctx->rcpt_to = smtp_address_clone(ctx->pool, input->rcpt_to);
+ smtp_params_rcpt_copy(ctx->pool, &ctx->rcpt_params,
+ &input->rcpt_params);
+ ctx->rcpt_user = input->rcpt_user;
+ ctx->rcpt_default_mailbox = p_strdup(ctx->pool,
+ input->rcpt_default_mailbox);
+
+ ctx->event = event_create(input->event_parent);
+ event_add_category(ctx->event, &event_category_mail_delivery);
+
+ mail_deliver_fields_update(&ctx->fields, ctx->pool, ctx->src_mail);
+ mail_deliver_update_event(ctx);
+
+ if (ctx->rcpt_to != NULL) {
+ event_add_str(ctx->event, "rcpt_to",
+ smtp_address_encode(ctx->rcpt_to));
+ }
+ smtp_params_rcpt_add_to_event(&ctx->rcpt_params, ctx->event);
+}
+
+void mail_deliver_deinit(struct mail_deliver_context *ctx)
+{
+ event_unref(&ctx->event);
+ pool_unref(&ctx->pool);
+}
+
+static struct mail *
+mail_deliver_open_mail(struct mailbox *box, uint32_t uid,
+ enum mail_fetch_field wanted_fields,
+ struct mailbox_transaction_context **trans_r)
+{
+ struct mailbox_transaction_context *t;
+ struct mail *mail;
+
+ *trans_r = NULL;
+
+ if (mailbox_sync(box, MAILBOX_SYNC_FLAG_FAST) < 0)
+ return NULL;
+
+ t = mailbox_transaction_begin(box, 0, __func__);
+ mail = mail_alloc(t, wanted_fields, NULL);
+
+ if (!mail_set_uid(mail, uid)) {
+ mail_free(&mail);
+ mailbox_transaction_rollback(&t);
+ }
+ *trans_r = t;
+ return mail;
+}
+
+int mail_deliver_save(struct mail_deliver_context *ctx, const char *mailbox,
+ enum mail_flags flags, const char *const *keywords,
+ struct mail_storage **storage_r)
+{
+ struct mail_deliver_save_open_context open_ctx;
+ struct mailbox *box;
+ enum mailbox_transaction_flags trans_flags;
+ struct mailbox_transaction_context *t;
+ struct mail_save_context *save_ctx;
+ struct mailbox_header_lookup_ctx *headers_ctx;
+ struct mail_keywords *kw;
+ struct mail *dest_mail;
+ enum mail_error error;
+ const char *mailbox_name, *errstr, *guid;
+ struct mail_transaction_commit_changes changes;
+ bool default_save;
+ int ret = 0;
+
+ i_assert(ctx->dest_mail == NULL);
+
+ default_save = strcmp(mailbox, ctx->rcpt_default_mailbox) == 0;
+ if (default_save)
+ ctx->tried_default_save = TRUE;
+
+ i_zero(&open_ctx);
+ open_ctx.user = ctx->rcpt_user;
+ open_ctx.lda_mailbox_autocreate = ctx->set->lda_mailbox_autocreate;
+ open_ctx.lda_mailbox_autosubscribe = ctx->set->lda_mailbox_autosubscribe;
+
+ mailbox_name = str_sanitize(mailbox, 80);
+ if (mail_deliver_save_open(&open_ctx, mailbox, &box,
+ &error, &errstr) < 0) {
+ if (box != NULL) {
+ *storage_r = mailbox_get_storage(box);
+ mailbox_free(&box);
+ }
+ mail_deliver_log(ctx, "save failed to open mailbox %s: %s",
+ mailbox_name, errstr);
+ return -1;
+ }
+ *storage_r = mailbox_get_storage(box);
+
+ trans_flags = MAILBOX_TRANSACTION_FLAG_EXTERNAL;
+ if (ctx->save_dest_mail)
+ trans_flags |= MAILBOX_TRANSACTION_FLAG_ASSIGN_UIDS;
+ t = mailbox_transaction_begin(box, trans_flags, __func__);
+
+ kw = str_array_length(keywords) == 0 ? NULL :
+ mailbox_keywords_create_valid(box, keywords);
+ save_ctx = mailbox_save_alloc(t);
+ if (ctx->mail_from != NULL) {
+ mailbox_save_set_from_envelope(save_ctx,
+ smtp_address_encode(ctx->mail_from));
+ }
+ mailbox_save_set_flags(save_ctx, flags, kw);
+
+ headers_ctx = mailbox_header_lookup_init(box, lda_log_wanted_headers);
+ dest_mail = mailbox_save_get_dest_mail(save_ctx);
+ mail_add_temp_wanted_fields(dest_mail, lda_log_wanted_fetch_fields, NULL);
+ mailbox_header_lookup_unref(&headers_ctx);
+ mail_deliver_deduplicate_guid_if_needed(ctx->session, save_ctx);
+
+ if (mailbox_save_using_mail(&save_ctx, ctx->src_mail) < 0)
+ ret = -1;
+ if (kw != NULL)
+ mailbox_keywords_unref(&kw);
+
+ if (ret < 0)
+ mailbox_transaction_rollback(&t);
+ else
+ ret = mailbox_transaction_commit_get_changes(&t, &changes);
+
+ if (ret == 0) {
+ ctx->saved_mail = TRUE;
+ if (ctx->save_dest_mail) {
+ /* copying needs the message body. with maildir we also
+ need to get the GUID in case the message gets
+ expunged. get these early so the copying won't fail
+ later on. */
+ i_assert(array_count(&changes.saved_uids) == 1);
+ const struct seq_range *range =
+ array_front(&changes.saved_uids);
+ i_assert(range->seq1 == range->seq2);
+ ctx->dest_mail = mail_deliver_open_mail(box, range->seq1,
+ MAIL_FETCH_STREAM_BODY | MAIL_FETCH_GUID, &t);
+ if (ctx->dest_mail == NULL) {
+ i_assert(t == NULL);
+ } else if (mail_get_special(ctx->dest_mail, MAIL_FETCH_GUID, &guid) < 0) {
+ mail_free(&ctx->dest_mail);
+ mailbox_transaction_rollback(&t);
+ }
+ }
+ mail_deliver_log(ctx, "saved mail to %s", mailbox_name);
+ pool_unref(&changes.pool);
+ } else {
+ mail_deliver_log(ctx, "save failed to %s: %s", mailbox_name,
+ mail_storage_get_last_internal_error(*storage_r, &error));
+ }
+
+ if (ctx->dest_mail == NULL)
+ mailbox_free(&box);
+ return ret;
+}
+
+const struct smtp_address *
+mail_deliver_get_return_address(struct mail_deliver_context *ctx)
+{
+ struct message_address *addr;
+ struct smtp_address *smtp_addr;
+ const char *path;
+ int ret;
+
+ if (!smtp_address_isnull(ctx->mail_from))
+ return ctx->mail_from;
+
+ if ((ret=mail_get_first_header(ctx->src_mail,
+ "Return-Path", &path)) <= 0) {
+ if (ret < 0) {
+ struct mailbox *box = ctx->src_mail->box;
+ e_warning(ctx->event,
+ "Failed read return-path header: %s",
+ mailbox_get_last_internal_error(box, NULL));
+ }
+ return NULL;
+ }
+ if (message_address_parse_path(pool_datastack_create(),
+ (const unsigned char *)path,
+ strlen(path), &addr) < 0 ||
+ smtp_address_create_from_msg(ctx->pool, addr, &smtp_addr) < 0) {
+ e_warning(ctx->event, "Failed to parse return-path header");
+ return NULL;
+ }
+ return smtp_addr;
+}
+
+const char *mail_deliver_get_new_message_id(struct mail_deliver_context *ctx)
+{
+ static int count = 0;
+ struct mail_user *user = ctx->rcpt_user;
+ const struct mail_storage_settings *mail_set =
+ mail_user_set_get_storage_set(user);
+
+ return t_strdup_printf("<dovecot-%s-%s-%d@%s>",
+ dec2str(ioloop_timeval.tv_sec),
+ dec2str(ioloop_timeval.tv_usec),
+ count++, mail_set->hostname);
+}
+
+static bool mail_deliver_is_tempfailed(struct mail_deliver_context *ctx,
+ struct mail_storage *storage)
+{
+ enum mail_error error;
+
+ if (ctx->tempfail_error != NULL)
+ return TRUE;
+ if (storage != NULL) {
+ (void)mail_storage_get_last_error(storage, &error);
+ return error == MAIL_ERROR_TEMP;
+ }
+ return FALSE;
+}
+
+static int
+mail_do_deliver(struct mail_deliver_context *ctx,
+ struct mail_storage **storage_r)
+{
+ int ret;
+
+ *storage_r = NULL;
+ if (deliver_mail == NULL)
+ ret = -1;
+ else {
+ ctx->dup_db = mail_duplicate_db_init(ctx->rcpt_user,
+ DUPLICATE_DB_NAME);
+ if (deliver_mail(ctx, storage_r) <= 0) {
+ /* if message was saved, don't bounce it even though
+ the script failed later. */
+ ret = ctx->saved_mail ? 0 : -1;
+ } else {
+ /* success. message may or may not have been saved. */
+ ret = 0;
+ }
+ mail_duplicate_db_deinit(&ctx->dup_db);
+ if (ret < 0 && mail_deliver_is_tempfailed(ctx, *storage_r))
+ return -1;
+ }
+
+ if (ret < 0 && !ctx->tried_default_save) {
+ /* plugins didn't handle this. save into the default mailbox. */
+ ret = mail_deliver_save(ctx, ctx->rcpt_default_mailbox, 0, NULL,
+ storage_r);
+ if (ret < 0 && mail_deliver_is_tempfailed(ctx, *storage_r))
+ return -1;
+ }
+ if (ret < 0 && strcasecmp(ctx->rcpt_default_mailbox, "INBOX") != 0) {
+ /* still didn't work. try once more to save it
+ to INBOX. */
+ ret = mail_deliver_save(ctx, "INBOX", 0, NULL, storage_r);
+ }
+ return ret;
+}
+
+int mail_deliver(struct mail_deliver_context *ctx,
+ enum mail_deliver_error *error_code_r,
+ const char **error_r)
+{
+ struct mail_deliver_user *muser =
+ MAIL_DELIVER_USER_CONTEXT(ctx->rcpt_user);
+ struct event_passthrough *e;
+ struct mail_storage *storage = NULL;
+ enum mail_deliver_error error_code = MAIL_DELIVER_ERROR_NONE;
+ const char *error = NULL;
+ int ret;
+
+ i_assert(muser->deliver_ctx == NULL);
+
+ mail_deliver_fields_update(&ctx->fields, ctx->pool, ctx->src_mail);
+ mail_deliver_update_event(ctx);
+
+ muser->want_storage_id =
+ var_has_key(ctx->set->deliver_log_format, '\0', "storage_id");
+
+ muser->deliver_ctx = ctx;
+
+ e = event_create_passthrough(ctx->event)->
+ set_name("mail_delivery_started");
+ e_debug(e->event(), "Local delivery started");
+
+ ret = mail_do_deliver(ctx, &storage);
+
+ if (ret >= 0)
+ i_assert(ret == 0); /* ret > 0 has no defined meaning */
+ else if (ctx->tempfail_error != NULL) {
+ error = ctx->tempfail_error;
+ error_code = MAIL_DELIVER_ERROR_TEMPORARY;
+ } else if (storage != NULL) {
+ enum mail_error mail_error;
+
+ error = mail_storage_get_last_error(storage, &mail_error);
+ if (mail_error == MAIL_ERROR_NOQUOTA) {
+ error_code = MAIL_DELIVER_ERROR_NOQUOTA;
+ } else {
+ error_code = MAIL_DELIVER_ERROR_TEMPORARY;
+ }
+ } else {
+ /* This shouldn't happen */
+ e_error(ctx->event, "BUG: Saving failed to unknown storage");
+ error = "Temporary internal error";
+ error_code = MAIL_DELIVER_ERROR_INTERNAL;
+ }
+
+ e = event_create_passthrough(ctx->event)->
+ set_name("mail_delivery_finished");
+ if (ret == 0) {
+ e_debug(e->event(), "Local delivery finished successfully");
+ } else {
+ e->add_str("error", error);
+ e_debug(e->event(), "Local delivery failed: %s", error);
+ }
+
+ muser->deliver_ctx = NULL;
+
+ *error_code_r = error_code;
+ *error_r = error;
+ return ret;
+}
+
+deliver_mail_func_t *mail_deliver_hook_set(deliver_mail_func_t *new_hook)
+{
+ deliver_mail_func_t *old_hook = deliver_mail;
+
+ deliver_mail = new_hook;
+ return old_hook;
+}
+
+static int mail_deliver_save_finish(struct mail_save_context *ctx)
+{
+ struct mailbox *box = ctx->transaction->box;
+ struct mail_deliver_mailbox *mbox = MAIL_DELIVER_STORAGE_CONTEXT(box);
+ struct mail_deliver_user *muser =
+ MAIL_DELIVER_USER_CONTEXT(box->storage->user);
+ struct mail_deliver_transaction *dt =
+ MAIL_DELIVER_STORAGE_CONTEXT(ctx->transaction);
+
+ if (mbox->module_ctx.super.save_finish(ctx) < 0)
+ return -1;
+
+ /* initialize most of the fields from dest_mail */
+ mail_deliver_fields_update(&dt->deliver_fields,
+ muser->deliver_ctx->pool,
+ ctx->dest_mail);
+ return 0;
+}
+
+static int mail_deliver_copy(struct mail_save_context *ctx, struct mail *mail)
+{
+ struct mailbox *box = ctx->transaction->box;
+ struct mail_deliver_mailbox *mbox = MAIL_DELIVER_STORAGE_CONTEXT(box);
+ struct mail_deliver_user *muser =
+ MAIL_DELIVER_USER_CONTEXT(box->storage->user);
+ struct mail_deliver_transaction *dt =
+ MAIL_DELIVER_STORAGE_CONTEXT(ctx->transaction);
+
+ if (mbox->module_ctx.super.copy(ctx, mail) < 0)
+ return -1;
+
+ /* initialize most of the fields from dest_mail */
+ mail_deliver_fields_update(&dt->deliver_fields,
+ muser->deliver_ctx->pool,
+ ctx->dest_mail);
+ return 0;
+}
+
+static void
+mail_deliver_fields_update_post_commit(struct mailbox *orig_box, uint32_t uid)
+{
+ struct mail_deliver_user *muser =
+ MAIL_DELIVER_USER_CONTEXT(orig_box->storage->user);
+ struct mailbox *box;
+ struct mailbox_transaction_context *t;
+ struct mail *mail;
+ const char *storage_id;
+
+ if (!muser->want_storage_id)
+ return;
+
+ /* getting storage_id requires a whole new mailbox view that is
+ synced, so it'll contain the newly written mail. this is racy, so
+ it's possible another process has already deleted the mail. */
+ box = mailbox_alloc(orig_box->list, orig_box->vname, 0);
+ mail = mail_deliver_open_mail(box, uid, MAIL_FETCH_STORAGE_ID, &t);
+ if (mail != NULL) {
+ if (mail_get_special(mail, MAIL_FETCH_STORAGE_ID, &storage_id) < 0 ||
+ storage_id[0] == '\0')
+ storage_id = NULL;
+ muser->deliver_ctx->fields.storage_id =
+ p_strdup(muser->deliver_ctx->pool, storage_id);
+ mail_free(&mail);
+ (void)mailbox_transaction_commit(&t);
+ } else {
+ muser->deliver_ctx->fields.storage_id = NULL;
+ }
+ mailbox_free(&box);
+}
+
+static struct mailbox_transaction_context *
+mail_deliver_transaction_begin(struct mailbox *box,
+ enum mailbox_transaction_flags flags,
+ const char *reason)
+{
+ struct mail_deliver_mailbox *mbox = MAIL_DELIVER_STORAGE_CONTEXT(box);
+ struct mail_deliver_user *muser =
+ MAIL_DELIVER_USER_CONTEXT(box->storage->user);
+ struct mailbox_transaction_context *t;
+ struct mail_deliver_transaction *dt;
+
+ i_assert(muser->deliver_ctx != NULL);
+
+ t = mbox->module_ctx.super.transaction_begin(box, flags, reason);
+ dt = p_new(muser->deliver_ctx->pool, struct mail_deliver_transaction, 1);
+
+ MODULE_CONTEXT_SET(t, mail_deliver_storage_module, dt);
+ return t;
+}
+
+static int
+mail_deliver_transaction_commit(struct mailbox_transaction_context *ctx,
+ struct mail_transaction_commit_changes *changes_r)
+{
+ struct mailbox *box = ctx->box;
+ struct mail_deliver_mailbox *mbox = MAIL_DELIVER_STORAGE_CONTEXT(box);
+ struct mail_deliver_transaction *dt = MAIL_DELIVER_STORAGE_CONTEXT(ctx);
+ struct mail_deliver_user *muser =
+ MAIL_DELIVER_USER_CONTEXT(box->storage->user);
+
+ i_assert(muser->deliver_ctx != NULL);
+
+ /* sieve creates multiple transactions, saves the mails and
+ then commits all of them at the end. we'll need to keep
+ switching the deliver_ctx->fields for each commit.
+
+ we also want to do this only for commits generated by sieve.
+ other plugins or storage backends may be creating transactions as
+ well, which we need to ignore. */
+ if ((box->flags & MAILBOX_FLAG_POST_SESSION) != 0) {
+ muser->deliver_ctx->fields = dt->deliver_fields;
+ mail_deliver_update_event(muser->deliver_ctx);
+ }
+
+ if (mbox->module_ctx.super.transaction_commit(ctx, changes_r) < 0)
+ return -1;
+
+ if (array_count(&changes_r->saved_uids) > 0) {
+ const struct seq_range *range =
+ array_front(&changes_r->saved_uids);
+
+ mail_deliver_fields_update_post_commit(box, range->seq1);
+ }
+ return 0;
+}
+
+static void mail_deliver_mail_user_created(struct mail_user *user)
+{
+ struct mail_deliver_user *muser;
+
+ muser = p_new(user->pool, struct mail_deliver_user, 1);
+ MODULE_CONTEXT_SET(user, mail_deliver_user_module, muser);
+}
+
+static void mail_deliver_mailbox_allocated(struct mailbox *box)
+{
+ struct mailbox_vfuncs *v = box->vlast;
+ struct mail_deliver_mailbox *mbox;
+ struct mail_deliver_user *muser =
+ MAIL_DELIVER_USER_CONTEXT(box->storage->user);
+
+ /* we are doing something other than lda/lmtp delivery
+ and should not be involved */
+ if (muser->deliver_ctx == NULL)
+ return;
+
+ mbox = p_new(box->pool, struct mail_deliver_mailbox, 1);
+ mbox->module_ctx.super = *v;
+ box->vlast = &mbox->module_ctx.super;
+ v->save_finish = mail_deliver_save_finish;
+ v->copy = mail_deliver_copy;
+ v->transaction_begin = mail_deliver_transaction_begin;
+ v->transaction_commit = mail_deliver_transaction_commit;
+
+ MODULE_CONTEXT_SET(box, mail_deliver_storage_module, mbox);
+ }
+
+static struct mail_storage_hooks mail_deliver_hooks = {
+ .mail_user_created = mail_deliver_mail_user_created,
+ .mailbox_allocated = mail_deliver_mailbox_allocated
+};
+
+void mail_deliver_hooks_init(void)
+{
+ mail_storage_hooks_add_internal(&mail_deliver_hooks);
+}
diff --git a/src/lib-lda/mail-deliver.h b/src/lib-lda/mail-deliver.h
new file mode 100644
index 0000000..5396a82
--- /dev/null
+++ b/src/lib-lda/mail-deliver.h
@@ -0,0 +1,185 @@
+#ifndef MAIL_DELIVER_H
+#define MAIL_DELIVER_H
+
+#include "guid.h"
+#include "mail-types.h"
+#include "mail-error.h"
+#include "smtp-params.h"
+
+#include <sys/time.h>
+
+struct smtp_address;
+struct mail_storage;
+struct mail_save_context;
+struct mailbox;
+
+enum mail_deliver_error {
+ MAIL_DELIVER_ERROR_NONE = 0,
+
+ /* Temporary error */
+ MAIL_DELIVER_ERROR_TEMPORARY,
+ /* Delivery rejected */
+ MAIL_DELIVER_ERROR_REJECTED,
+ /* Out of storage quota for mailbox or user */
+ MAIL_DELIVER_ERROR_NOQUOTA,
+ /* Internal error (BUG) */
+ MAIL_DELIVER_ERROR_INTERNAL,
+};
+
+struct mail_deliver_session {
+ pool_t pool;
+
+ /* List of INBOX GUIDs where this mail has already been saved to */
+ ARRAY(guid_128_t) inbox_guids;
+};
+
+struct mail_deliver_input {
+ const struct lda_settings *set;
+ const struct smtp_submit_settings *smtp_set;
+ struct mail_deliver_session *session;
+ struct event *event_parent;
+
+ unsigned int session_time_msecs;
+ struct timeval delivery_time_started;
+
+ /* Session ID, used as log line prefix if non-NULL. */
+ const char *session_id;
+ /* Mail to save */
+ struct mail *src_mail;
+
+ /* Envelope sender, if known. */
+ const struct smtp_address *mail_from;
+ /* MAIL parameters */
+ struct smtp_params_mail mail_params;
+
+ /* Envelope recipient (final recipient) */
+ const struct smtp_address *rcpt_to;
+ /* RCPT parameters (can contain original recipient) */
+ struct smtp_params_rcpt rcpt_params;
+ /* Destination user */
+ struct mail_user *rcpt_user;
+ /* Mailbox where mail should be saved, unless e.g. Sieve does
+ something to it. */
+ const char *rcpt_default_mailbox;
+
+ bool save_dest_mail:1;
+};
+
+struct mail_deliver_fields {
+ const char *message_id;
+ const char *subject;
+ const char *from;
+ const char *from_envelope;
+ const char *storage_id;
+
+ uoff_t psize, vsize;
+
+ bool filled:1;
+};
+
+struct mail_deliver_context {
+ pool_t pool;
+ const struct lda_settings *set;
+ const struct smtp_submit_settings *smtp_set;
+ struct mail_deliver_session *session;
+ struct event *event;
+
+ unsigned int session_time_msecs;
+ struct timeval delivery_time_started;
+
+ struct mail_duplicate_db *dup_db;
+
+ /* Session ID, used as log line prefix if non-NULL. */
+ const char *session_id;
+ /* Mail to save */
+ struct mail *src_mail;
+
+ /* Envelope sender, if known. */
+ const struct smtp_address *mail_from;
+ /* MAIL parameters */
+ struct smtp_params_mail mail_params;
+
+ /* Envelope recipient (final recipient) */
+ const struct smtp_address *rcpt_to;
+ /* RCPT parameters (can contain original recipient) */
+ struct smtp_params_rcpt rcpt_params;
+ /* Destination user */
+ struct mail_user *rcpt_user;
+ /* Mailbox where mail should be saved, unless e.g. Sieve does
+ something to it. */
+ const char *rcpt_default_mailbox;
+
+ /* Filled with destination mail, if save_dest_mail=TRUE.
+ The caller must free the mail, its transaction and close
+ the mailbox. */
+ struct mail *dest_mail;
+
+ /* Recorded field values for the transaction */
+ struct mail_deliver_fields fields;
+
+ /* Error message for a temporary failure. This is necessary only when
+ there is no storage where to get the error message from. */
+ const char *tempfail_error;
+
+ bool tried_default_save;
+ bool saved_mail;
+ bool save_dest_mail;
+ /* Delivery failed because user is out of quota / disk space */
+ bool mailbox_full;
+ /* Send DSN instead of MDN */
+ bool dsn;
+};
+
+struct mail_deliver_save_open_context {
+ struct mail_user *user;
+ bool lda_mailbox_autocreate;
+ bool lda_mailbox_autosubscribe;
+};
+
+typedef int deliver_mail_func_t(struct mail_deliver_context *ctx,
+ struct mail_storage **storage_r);
+
+extern deliver_mail_func_t *deliver_mail;
+
+const struct var_expand_table *
+mail_deliver_ctx_get_log_var_expand_table(struct mail_deliver_context *ctx,
+ const char *message);
+void mail_deliver_log(struct mail_deliver_context *ctx, const char *fmt, ...)
+ ATTR_FORMAT(2, 3);
+
+const struct smtp_address *
+mail_deliver_get_address(struct mail *mail, const char *header);
+const struct smtp_address *
+mail_deliver_get_return_address(struct mail_deliver_context *ctx);
+const char *mail_deliver_get_new_message_id(struct mail_deliver_context *ctx);
+
+struct mail_deliver_session *mail_deliver_session_init(void);
+void mail_deliver_session_deinit(struct mail_deliver_session **session);
+
+void mail_deliver_init(struct mail_deliver_context *ctx,
+ struct mail_deliver_input *input);
+void mail_deliver_deinit(struct mail_deliver_context *ctx);
+
+/* Try to open mailbox for saving. Returns 0 if ok, -1 if error. The box may
+ be returned even with -1, and the caller must free it then. */
+int mail_deliver_save_open(struct mail_deliver_save_open_context *ctx,
+ const char *name, struct mailbox **box_r,
+ enum mail_error *error_r, const char **error_str_r);
+int mail_deliver_save(struct mail_deliver_context *ctx, const char *mailbox,
+ enum mail_flags flags, const char *const *keywords,
+ struct mail_storage **storage_r) ATTR_NULL(4);
+void mail_deliver_deduplicate_guid_if_needed(struct mail_deliver_session *session,
+ struct mail_save_context *save_ctx);
+
+int mail_deliver(struct mail_deliver_context *ctx,
+ enum mail_deliver_error *error_code_r,
+ const char **error_r);
+
+/* Sets the deliver_mail hook and returns the previous hook,
+ which the new_hook should call if it's non-NULL. */
+deliver_mail_func_t *mail_deliver_hook_set(deliver_mail_func_t *new_hook);
+
+/* Must be called before any storage is created. */
+void mail_deliver_hooks_init(void);
+
+#endif
diff --git a/src/lib-lda/mail-send.c b/src/lib-lda/mail-send.c
new file mode 100644
index 0000000..2027b0b
--- /dev/null
+++ b/src/lib-lda/mail-send.c
@@ -0,0 +1,216 @@
+/* Copyright (c) 2005-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "ioloop.h"
+#include "hostpid.h"
+#include "istream.h"
+#include "ostream.h"
+#include "str.h"
+#include "str-sanitize.h"
+#include "var-expand.h"
+#include "message-date.h"
+#include "message-size.h"
+#include "message-address.h"
+#include "istream-header-filter.h"
+#include "mail-storage.h"
+#include "mail-storage-settings.h"
+#include "iostream-ssl.h"
+#include "lda-settings.h"
+#include "mail-deliver.h"
+#include "smtp-address.h"
+#include "smtp-submit.h"
+#include "mail-send.h"
+
+#include <sys/wait.h>
+
+static const struct var_expand_table *
+get_var_expand_table(struct mail *mail,
+ const struct smtp_address *recipient,
+ const char *reason)
+{
+ const char *subject;
+ if (mail_get_first_header(mail, "Subject", &subject) <= 0)
+ subject = "";
+
+ const struct var_expand_table stack_tab[] = {
+ { 'n', "\r\n", "crlf" },
+ { 'r', reason, "reason" },
+ { 's', str_sanitize(subject, 80), "subject" },
+ { 't', smtp_address_encode(recipient), "to" },
+ { '\0', NULL, NULL }
+ };
+ struct var_expand_table *tab;
+
+ tab = t_malloc_no0(sizeof(stack_tab));
+ memcpy(tab, stack_tab, sizeof(stack_tab));
+ return tab;
+}
+
+int mail_send_rejection(struct mail_deliver_context *ctx,
+ const struct smtp_address *recipient,
+ const char *reason)
+{
+ struct mail_user *user = ctx->rcpt_user;
+ struct ssl_iostream_settings ssl_set;
+ struct mail *mail = ctx->src_mail;
+ struct istream *input;
+ struct smtp_submit_input smtp_input;
+ struct smtp_submit *smtp_submit;
+ struct ostream *output;
+ const struct message_address *postmaster_addr;
+ const struct smtp_address *return_addr;
+ const char *hdr, *value, *msgid, *orig_msgid, *boundary, *error;
+ const struct var_expand_table *vtable;
+ string_t *str;
+ int ret;
+
+ if (mail_get_first_header(mail, "Message-ID", &orig_msgid) < 0)
+ orig_msgid = NULL;
+
+ if (mail_get_first_header(mail, "Auto-Submitted", &value) > 0 &&
+ strcasecmp(value, "no") != 0) {
+ i_info("msgid=%s: Auto-submitted message discarded: %s",
+ orig_msgid == NULL ? "" : str_sanitize(orig_msgid, 80),
+ str_sanitize(reason, 512));
+ return 0;
+ }
+
+ return_addr = mail_deliver_get_return_address(ctx);
+ if (smtp_address_isnull(return_addr)) {
+ i_info("msgid=%s: Return-Path missing, rejection reason: %s",
+ orig_msgid == NULL ? "" : str_sanitize(orig_msgid, 80),
+ str_sanitize(reason, 512));
+ return 0;
+ }
+
+ if (!mail_user_get_postmaster_address(user, &postmaster_addr, &error)) {
+ i_error("msgid=%s: Invalid postmaster_address - can't send rejection: %s",
+ orig_msgid == NULL ? "" : str_sanitize(orig_msgid, 80), error);
+ return -1;
+ }
+
+ e_debug(mail_event(mail), "Sending a rejection to <%s>: %s",
+ smtp_address_encode(return_addr),
+ str_sanitize(reason, 512));
+
+ vtable = get_var_expand_table(mail, recipient, reason);
+
+ mail_user_init_ssl_client_settings(user, &ssl_set);
+
+ i_zero(&smtp_input);
+ smtp_input.ssl = &ssl_set;
+ smtp_submit = smtp_submit_init_simple(&smtp_input, ctx->smtp_set, NULL);
+ smtp_submit_add_rcpt(smtp_submit, return_addr);
+ output = smtp_submit_send(smtp_submit);
+
+ msgid = mail_deliver_get_new_message_id(ctx);
+ boundary = t_strdup_printf("%s/%s", my_pid, user->set->hostname);
+
+ str = t_str_new(512);
+ str_printfa(str, "Message-ID: %s\r\n", msgid);
+ str_printfa(str, "Date: %s\r\n", message_date_create(ioloop_time));
+ str_append(str, "From: ");
+ message_address_write(str, postmaster_addr);
+ str_append(str, "\r\n");
+ str_printfa(str, "To: <%s>\r\n", smtp_address_encode(return_addr));
+ str_append(str, "MIME-Version: 1.0\r\n");
+ str_printfa(str, "Content-Type: "
+ "multipart/report; report-type=%s;\r\n"
+ "\tboundary=\"%s\"\r\n",
+ ctx->dsn ? "delivery-status" : "disposition-notification",
+ boundary);
+ str_append(str, "Subject: ");
+ if (var_expand(str, ctx->set->rejection_subject,
+ vtable, &error) <= 0) {
+ i_error("Failed to expand rejection_subject=%s: %s",
+ ctx->set->rejection_subject, error);
+ }
+ str_append(str, "\r\n");
+
+ str_append(str, "Auto-Submitted: auto-replied (rejected)\r\n");
+ str_append(str, "Precedence: bulk\r\n");
+ str_append(str, "\r\nThis is a MIME-encapsulated message\r\n\r\n");
+
+ /* human readable status report */
+ str_printfa(str, "--%s\r\n", boundary);
+ str_append(str, "Content-Type: text/plain; charset=utf-8\r\n");
+ str_append(str, "Content-Disposition: inline\r\n");
+ str_append(str, "Content-Transfer-Encoding: 8bit\r\n\r\n");
+
+ if (var_expand(str, ctx->set->rejection_reason,
+ vtable, &error) <= 0) {
+ i_error("Failed to expand rejection_reason=%s: %s",
+ ctx->set->rejection_reason, error);
+ }
+ str_append(str, "\r\n");
+
+ if (ctx->dsn) {
+ /* DSN status report: For LDA rejects. currently only used when
+ user is out of quota */
+ str_printfa(str, "--%s\r\n"
+ "Content-Type: message/delivery-status\r\n\r\n",
+ boundary);
+ str_printfa(str, "Reporting-MTA: dns; %s\r\n", user->set->hostname);
+ if (mail_get_first_header(mail, "Original-Recipient", &hdr) > 0)
+ str_printfa(str, "Original-Recipient: rfc822; %s\r\n", hdr);
+ str_printfa(str, "Final-Recipient: rfc822; %s\r\n",
+ smtp_address_encode(recipient));
+ str_append(str, "Action: failed\r\n");
+ str_printfa(str, "Status: %s\r\n", ctx->mailbox_full ? "5.2.2" : "5.2.0");
+ } else {
+ /* MDN status report: For Sieve "reject" */
+ str_printfa(str, "--%s\r\n"
+ "Content-Type: message/disposition-notification\r\n\r\n",
+ boundary);
+ str_printfa(str, "Reporting-UA: %s; Dovecot Mail Delivery Agent\r\n",
+ user->set->hostname);
+ if (mail_get_first_header(mail, "Original-Recipient", &hdr) > 0)
+ str_printfa(str, "Original-Recipient: rfc822; %s\r\n", hdr);
+ str_printfa(str, "Final-Recipient: rfc822; %s\r\n",
+ smtp_address_encode(recipient));
+
+ if (orig_msgid != NULL)
+ str_printfa(str, "Original-Message-ID: %s\r\n", orig_msgid);
+ str_append(str, "Disposition: "
+ "automatic-action/MDN-sent-automatically; deleted\r\n");
+ }
+ str_append(str, "\r\n");
+
+ /* original message's headers */
+ str_printfa(str, "--%s\r\nContent-Type: message/rfc822\r\n\r\n", boundary);
+ o_stream_nsend(output, str_data(str), str_len(str));
+
+ if (mail_get_hdr_stream(mail, NULL, &input) == 0) {
+ /* Note: If you add more headers, they need to be sorted.
+ We'll drop Content-Type because we're not including the message
+ body, and having a multipart Content-Type may confuse some
+ MIME parsers when they don't see the message boundaries. */
+ static const char *const exclude_headers[] = {
+ "Content-Type"
+ };
+
+ input = i_stream_create_header_filter(input,
+ HEADER_FILTER_EXCLUDE | HEADER_FILTER_NO_CR |
+ HEADER_FILTER_HIDE_BODY, exclude_headers,
+ N_ELEMENTS(exclude_headers),
+ *null_header_filter_callback, NULL);
+
+ o_stream_nsend_istream(output, input);
+ i_stream_unref(&input);
+ }
+
+ str_truncate(str, 0);
+ str_printfa(str, "\r\n\r\n--%s--\r\n", boundary);
+ o_stream_nsend(output, str_data(str), str_len(str));
+ if ((ret = smtp_submit_run(smtp_submit, &error)) < 0) {
+ i_error("msgid=%s: Temporarily failed to send rejection: %s",
+ orig_msgid == NULL ? "" : str_sanitize(orig_msgid, 80),
+ str_sanitize(error, 512));
+ } else if (ret == 0) {
+ i_info("msgid=%s: Permanently failed to send rejection: %s",
+ orig_msgid == NULL ? "" : str_sanitize(orig_msgid, 80),
+ str_sanitize(error, 512));
+ }
+ smtp_submit_deinit(&smtp_submit);
+ return ret < 0 ? -1 : 0;
+}
diff --git a/src/lib-lda/mail-send.h b/src/lib-lda/mail-send.h
new file mode 100644
index 0000000..88f87ed
--- /dev/null
+++ b/src/lib-lda/mail-send.h
@@ -0,0 +1,11 @@
+#ifndef MAIL_SEND_H
+#define MAIL_SEND_H
+
+struct mail;
+struct mail_deliver_context;
+
+int mail_send_rejection(struct mail_deliver_context *ctx,
+ const struct smtp_address *recipient,
+ const char *reason);
+
+#endif
diff --git a/src/lib-ldap/Makefile.am b/src/lib-ldap/Makefile.am
new file mode 100644
index 0000000..14f53a6
--- /dev/null
+++ b/src/lib-ldap/Makefile.am
@@ -0,0 +1,42 @@
+pkglib_LTLIBRARIES = libdovecot-ldap.la
+
+AM_CPPFLAGS = \
+ -I$(top_srcdir)/src/lib \
+ -I$(top_srcdir)/src/lib-test \
+ -I$(top_srcdir)/src/lib-settings \
+ -I$(top_srcdir)/src/lib-master \
+ -I$(top_srcdir)/src/lib-ssl-iostream \
+ $(LDAP_CFLAGS)
+
+libdovecot_ldap_la_SOURCES = \
+ ldap-client.c \
+ ldap-connection.c \
+ ldap-connection-pool.c \
+ ldap-iterator.c \
+ ldap-search.c \
+ ldap-compare.c \
+ ldap-entry.c
+
+libdovecot_ldap_la_DEPENDENCIES = ../lib-dovecot/libdovecot.la
+libdovecot_ldap_la_LDFLAGS = -export-dynamic
+libdovecot_ldap_la_LIBADD = ../lib-dovecot/libdovecot.la $(LDAP_LIBS)
+
+headers = \
+ ldap-client.h
+
+noinst_HEADERS = \
+ ldap-connection-pool.h \
+ ldap-private.h
+
+pkginc_libdir=$(pkgincludedir)
+pkginc_lib_HEADERS = $(headers)
+
+test_libs = \
+ ../lib-test/libtest.la \
+ ../lib-ssl-iostream/libssl_iostream.la \
+ ../lib/liblib.la
+
+check-local:
+ for bin in $(test_programs); do \
+ if ! $(RUN_TEST) ./$$bin; then exit 1; fi; \
+ done
diff --git a/src/lib-ldap/Makefile.in b/src/lib-ldap/Makefile.in
new file mode 100644
index 0000000..f161de9
--- /dev/null
+++ b/src/lib-ldap/Makefile.in
@@ -0,0 +1,890 @@
+# Makefile.in generated by automake 1.16.1 from Makefile.am.
+# @configure_input@
+
+# Copyright (C) 1994-2018 Free Software Foundation, Inc.
+
+# This Makefile.in is free software; the Free Software Foundation
+# gives unlimited permission to copy and/or distribute it,
+# with or without modifications, as long as this notice is preserved.
+
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY, to the extent permitted by law; without
+# even the implied warranty of MERCHANTABILITY or FITNESS FOR A
+# PARTICULAR PURPOSE.
+
+@SET_MAKE@
+
+
+VPATH = @srcdir@
+am__is_gnu_make = { \
+ if test -z '$(MAKELEVEL)'; then \
+ false; \
+ elif test -n '$(MAKE_HOST)'; then \
+ true; \
+ elif test -n '$(MAKE_VERSION)' && test -n '$(CURDIR)'; then \
+ true; \
+ else \
+ false; \
+ fi; \
+}
+am__make_running_with_option = \
+ case $${target_option-} in \
+ ?) ;; \
+ *) echo "am__make_running_with_option: internal error: invalid" \
+ "target option '$${target_option-}' specified" >&2; \
+ exit 1;; \
+ esac; \
+ has_opt=no; \
+ sane_makeflags=$$MAKEFLAGS; \
+ if $(am__is_gnu_make); then \
+ sane_makeflags=$$MFLAGS; \
+ else \
+ case $$MAKEFLAGS in \
+ *\\[\ \ ]*) \
+ bs=\\; \
+ sane_makeflags=`printf '%s\n' "$$MAKEFLAGS" \
+ | sed "s/$$bs$$bs[$$bs $$bs ]*//g"`;; \
+ esac; \
+ fi; \
+ skip_next=no; \
+ strip_trailopt () \
+ { \
+ flg=`printf '%s\n' "$$flg" | sed "s/$$1.*$$//"`; \
+ }; \
+ for flg in $$sane_makeflags; do \
+ test $$skip_next = yes && { skip_next=no; continue; }; \
+ case $$flg in \
+ *=*|--*) continue;; \
+ -*I) strip_trailopt 'I'; skip_next=yes;; \
+ -*I?*) strip_trailopt 'I';; \
+ -*O) strip_trailopt 'O'; skip_next=yes;; \
+ -*O?*) strip_trailopt 'O';; \
+ -*l) strip_trailopt 'l'; skip_next=yes;; \
+ -*l?*) strip_trailopt 'l';; \
+ -[dEDm]) skip_next=yes;; \
+ -[JT]) skip_next=yes;; \
+ esac; \
+ case $$flg in \
+ *$$target_option*) has_opt=yes; break;; \
+ esac; \
+ done; \
+ test $$has_opt = yes
+am__make_dryrun = (target_option=n; $(am__make_running_with_option))
+am__make_keepgoing = (target_option=k; $(am__make_running_with_option))
+pkgdatadir = $(datadir)/@PACKAGE@
+pkgincludedir = $(includedir)/@PACKAGE@
+pkglibdir = $(libdir)/@PACKAGE@
+pkglibexecdir = $(libexecdir)/@PACKAGE@
+am__cd = CDPATH="$${ZSH_VERSION+.}$(PATH_SEPARATOR)" && cd
+install_sh_DATA = $(install_sh) -c -m 644
+install_sh_PROGRAM = $(install_sh) -c
+install_sh_SCRIPT = $(install_sh) -c
+INSTALL_HEADER = $(INSTALL_DATA)
+transform = $(program_transform_name)
+NORMAL_INSTALL = :
+PRE_INSTALL = :
+POST_INSTALL = :
+NORMAL_UNINSTALL = :
+PRE_UNINSTALL = :
+POST_UNINSTALL = :
+build_triplet = @build@
+host_triplet = @host@
+subdir = src/lib-ldap
+ACLOCAL_M4 = $(top_srcdir)/aclocal.m4
+am__aclocal_m4_deps = $(top_srcdir)/m4/ac_checktype2.m4 \
+ $(top_srcdir)/m4/ac_typeof.m4 $(top_srcdir)/m4/arc4random.m4 \
+ $(top_srcdir)/m4/blockdev.m4 $(top_srcdir)/m4/c99_vsnprintf.m4 \
+ $(top_srcdir)/m4/clock_gettime.m4 $(top_srcdir)/m4/crypt.m4 \
+ $(top_srcdir)/m4/crypt_xpg6.m4 $(top_srcdir)/m4/dbqlk.m4 \
+ $(top_srcdir)/m4/dirent_dtype.m4 $(top_srcdir)/m4/dovecot.m4 \
+ $(top_srcdir)/m4/fd_passing.m4 $(top_srcdir)/m4/fdatasync.m4 \
+ $(top_srcdir)/m4/flexible_array_member.m4 \
+ $(top_srcdir)/m4/glibc.m4 $(top_srcdir)/m4/gmtime_max.m4 \
+ $(top_srcdir)/m4/gmtime_tm_gmtoff.m4 \
+ $(top_srcdir)/m4/ioloop.m4 $(top_srcdir)/m4/iovec.m4 \
+ $(top_srcdir)/m4/ipv6.m4 $(top_srcdir)/m4/libcap.m4 \
+ $(top_srcdir)/m4/libtool.m4 $(top_srcdir)/m4/libwrap.m4 \
+ $(top_srcdir)/m4/linux_mremap.m4 $(top_srcdir)/m4/ltoptions.m4 \
+ $(top_srcdir)/m4/ltsugar.m4 $(top_srcdir)/m4/ltversion.m4 \
+ $(top_srcdir)/m4/lt~obsolete.m4 $(top_srcdir)/m4/mmap_write.m4 \
+ $(top_srcdir)/m4/mntctl.m4 $(top_srcdir)/m4/modules.m4 \
+ $(top_srcdir)/m4/notify.m4 $(top_srcdir)/m4/nsl.m4 \
+ $(top_srcdir)/m4/off_t_max.m4 $(top_srcdir)/m4/pkg.m4 \
+ $(top_srcdir)/m4/pr_set_dumpable.m4 \
+ $(top_srcdir)/m4/q_quotactl.m4 $(top_srcdir)/m4/quota.m4 \
+ $(top_srcdir)/m4/random.m4 $(top_srcdir)/m4/rlimit.m4 \
+ $(top_srcdir)/m4/sendfile.m4 $(top_srcdir)/m4/size_t_signed.m4 \
+ $(top_srcdir)/m4/sockpeercred.m4 $(top_srcdir)/m4/sql.m4 \
+ $(top_srcdir)/m4/ssl.m4 $(top_srcdir)/m4/st_tim.m4 \
+ $(top_srcdir)/m4/static_array.m4 $(top_srcdir)/m4/test_with.m4 \
+ $(top_srcdir)/m4/time_t.m4 $(top_srcdir)/m4/typeof.m4 \
+ $(top_srcdir)/m4/typeof_dev_t.m4 \
+ $(top_srcdir)/m4/uoff_t_max.m4 $(top_srcdir)/m4/vararg.m4 \
+ $(top_srcdir)/m4/want_apparmor.m4 \
+ $(top_srcdir)/m4/want_bsdauth.m4 \
+ $(top_srcdir)/m4/want_bzlib.m4 \
+ $(top_srcdir)/m4/want_cassandra.m4 \
+ $(top_srcdir)/m4/want_cdb.m4 \
+ $(top_srcdir)/m4/want_checkpassword.m4 \
+ $(top_srcdir)/m4/want_clucene.m4 $(top_srcdir)/m4/want_db.m4 \
+ $(top_srcdir)/m4/want_gssapi.m4 $(top_srcdir)/m4/want_icu.m4 \
+ $(top_srcdir)/m4/want_ldap.m4 $(top_srcdir)/m4/want_lua.m4 \
+ $(top_srcdir)/m4/want_lz4.m4 $(top_srcdir)/m4/want_lzma.m4 \
+ $(top_srcdir)/m4/want_mysql.m4 $(top_srcdir)/m4/want_pam.m4 \
+ $(top_srcdir)/m4/want_passwd.m4 $(top_srcdir)/m4/want_pgsql.m4 \
+ $(top_srcdir)/m4/want_prefetch.m4 \
+ $(top_srcdir)/m4/want_shadow.m4 \
+ $(top_srcdir)/m4/want_sodium.m4 $(top_srcdir)/m4/want_solr.m4 \
+ $(top_srcdir)/m4/want_sqlite.m4 \
+ $(top_srcdir)/m4/want_stemmer.m4 \
+ $(top_srcdir)/m4/want_systemd.m4 \
+ $(top_srcdir)/m4/want_textcat.m4 \
+ $(top_srcdir)/m4/want_unwind.m4 $(top_srcdir)/m4/want_zlib.m4 \
+ $(top_srcdir)/m4/want_zstd.m4 $(top_srcdir)/configure.ac
+am__configure_deps = $(am__aclocal_m4_deps) $(CONFIGURE_DEPENDENCIES) \
+ $(ACLOCAL_M4)
+DIST_COMMON = $(srcdir)/Makefile.am $(noinst_HEADERS) \
+ $(pkginc_lib_HEADERS) $(am__DIST_COMMON)
+mkinstalldirs = $(install_sh) -d
+CONFIG_HEADER = $(top_builddir)/config.h
+CONFIG_CLEAN_FILES =
+CONFIG_CLEAN_VPATH_FILES =
+am__vpath_adj_setup = srcdirstrip=`echo "$(srcdir)" | sed 's|.|.|g'`;
+am__vpath_adj = case $$p in \
+ $(srcdir)/*) f=`echo "$$p" | sed "s|^$$srcdirstrip/||"`;; \
+ *) f=$$p;; \
+ esac;
+am__strip_dir = f=`echo $$p | sed -e 's|^.*/||'`;
+am__install_max = 40
+am__nobase_strip_setup = \
+ srcdirstrip=`echo "$(srcdir)" | sed 's/[].[^$$\\*|]/\\\\&/g'`
+am__nobase_strip = \
+ for p in $$list; do echo "$$p"; done | sed -e "s|$$srcdirstrip/||"
+am__nobase_list = $(am__nobase_strip_setup); \
+ for p in $$list; do echo "$$p $$p"; done | \
+ sed "s| $$srcdirstrip/| |;"' / .*\//!s/ .*/ ./; s,\( .*\)/[^/]*$$,\1,' | \
+ $(AWK) 'BEGIN { files["."] = "" } { files[$$2] = files[$$2] " " $$1; \
+ if (++n[$$2] == $(am__install_max)) \
+ { print $$2, files[$$2]; n[$$2] = 0; files[$$2] = "" } } \
+ END { for (dir in files) print dir, files[dir] }'
+am__base_list = \
+ sed '$$!N;$$!N;$$!N;$$!N;$$!N;$$!N;$$!N;s/\n/ /g' | \
+ sed '$$!N;$$!N;$$!N;$$!N;s/\n/ /g'
+am__uninstall_files_from_dir = { \
+ test -z "$$files" \
+ || { test ! -d "$$dir" && test ! -f "$$dir" && test ! -r "$$dir"; } \
+ || { echo " ( cd '$$dir' && rm -f" $$files ")"; \
+ $(am__cd) "$$dir" && rm -f $$files; }; \
+ }
+am__installdirs = "$(DESTDIR)$(pkglibdir)" \
+ "$(DESTDIR)$(pkginc_libdir)"
+LTLIBRARIES = $(pkglib_LTLIBRARIES)
+am__DEPENDENCIES_1 =
+am_libdovecot_ldap_la_OBJECTS = ldap-client.lo ldap-connection.lo \
+ ldap-connection-pool.lo ldap-iterator.lo ldap-search.lo \
+ ldap-compare.lo ldap-entry.lo
+libdovecot_ldap_la_OBJECTS = $(am_libdovecot_ldap_la_OBJECTS)
+AM_V_lt = $(am__v_lt_@AM_V@)
+am__v_lt_ = $(am__v_lt_@AM_DEFAULT_V@)
+am__v_lt_0 = --silent
+am__v_lt_1 =
+libdovecot_ldap_la_LINK = $(LIBTOOL) $(AM_V_lt) --tag=CC \
+ $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=link $(CCLD) \
+ $(AM_CFLAGS) $(CFLAGS) $(libdovecot_ldap_la_LDFLAGS) \
+ $(LDFLAGS) -o $@
+AM_V_P = $(am__v_P_@AM_V@)
+am__v_P_ = $(am__v_P_@AM_DEFAULT_V@)
+am__v_P_0 = false
+am__v_P_1 = :
+AM_V_GEN = $(am__v_GEN_@AM_V@)
+am__v_GEN_ = $(am__v_GEN_@AM_DEFAULT_V@)
+am__v_GEN_0 = @echo " GEN " $@;
+am__v_GEN_1 =
+AM_V_at = $(am__v_at_@AM_V@)
+am__v_at_ = $(am__v_at_@AM_DEFAULT_V@)
+am__v_at_0 = @
+am__v_at_1 =
+DEFAULT_INCLUDES = -I.@am__isrc@ -I$(top_builddir)
+depcomp = $(SHELL) $(top_srcdir)/depcomp
+am__maybe_remake_depfiles = depfiles
+am__depfiles_remade = ./$(DEPDIR)/ldap-client.Plo \
+ ./$(DEPDIR)/ldap-compare.Plo \
+ ./$(DEPDIR)/ldap-connection-pool.Plo \
+ ./$(DEPDIR)/ldap-connection.Plo ./$(DEPDIR)/ldap-entry.Plo \
+ ./$(DEPDIR)/ldap-iterator.Plo ./$(DEPDIR)/ldap-search.Plo
+am__mv = mv -f
+COMPILE = $(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) \
+ $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS)
+LTCOMPILE = $(LIBTOOL) $(AM_V_lt) --tag=CC $(AM_LIBTOOLFLAGS) \
+ $(LIBTOOLFLAGS) --mode=compile $(CC) $(DEFS) \
+ $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) $(CPPFLAGS) \
+ $(AM_CFLAGS) $(CFLAGS)
+AM_V_CC = $(am__v_CC_@AM_V@)
+am__v_CC_ = $(am__v_CC_@AM_DEFAULT_V@)
+am__v_CC_0 = @echo " CC " $@;
+am__v_CC_1 =
+CCLD = $(CC)
+LINK = $(LIBTOOL) $(AM_V_lt) --tag=CC $(AM_LIBTOOLFLAGS) \
+ $(LIBTOOLFLAGS) --mode=link $(CCLD) $(AM_CFLAGS) $(CFLAGS) \
+ $(AM_LDFLAGS) $(LDFLAGS) -o $@
+AM_V_CCLD = $(am__v_CCLD_@AM_V@)
+am__v_CCLD_ = $(am__v_CCLD_@AM_DEFAULT_V@)
+am__v_CCLD_0 = @echo " CCLD " $@;
+am__v_CCLD_1 =
+SOURCES = $(libdovecot_ldap_la_SOURCES)
+DIST_SOURCES = $(libdovecot_ldap_la_SOURCES)
+am__can_run_installinfo = \
+ case $$AM_UPDATE_INFO_DIR in \
+ n|no|NO) false;; \
+ *) (install-info --version) >/dev/null 2>&1;; \
+ esac
+HEADERS = $(noinst_HEADERS) $(pkginc_lib_HEADERS)
+am__tagged_files = $(HEADERS) $(SOURCES) $(TAGS_FILES) $(LISP)
+# Read a list of newline-separated strings from the standard input,
+# and print each of them once, without duplicates. Input order is
+# *not* preserved.
+am__uniquify_input = $(AWK) '\
+ BEGIN { nonempty = 0; } \
+ { items[$$0] = 1; nonempty = 1; } \
+ END { if (nonempty) { for (i in items) print i; }; } \
+'
+# Make sure the list of sources is unique. This is necessary because,
+# e.g., the same source file might be shared among _SOURCES variables
+# for different programs/libraries.
+am__define_uniq_tagged_files = \
+ list='$(am__tagged_files)'; \
+ unique=`for i in $$list; do \
+ if test -f "$$i"; then echo $$i; else echo $(srcdir)/$$i; fi; \
+ done | $(am__uniquify_input)`
+ETAGS = etags
+CTAGS = ctags
+am__DIST_COMMON = $(srcdir)/Makefile.in $(top_srcdir)/depcomp
+DISTFILES = $(DIST_COMMON) $(DIST_SOURCES) $(TEXINFOS) $(EXTRA_DIST)
+ACLOCAL = @ACLOCAL@
+ACLOCAL_AMFLAGS = @ACLOCAL_AMFLAGS@
+AMTAR = @AMTAR@
+AM_DEFAULT_VERBOSITY = @AM_DEFAULT_VERBOSITY@
+APPARMOR_LIBS = @APPARMOR_LIBS@
+AR = @AR@
+AUTH_CFLAGS = @AUTH_CFLAGS@
+AUTH_LIBS = @AUTH_LIBS@
+AUTOCONF = @AUTOCONF@
+AUTOHEADER = @AUTOHEADER@
+AUTOMAKE = @AUTOMAKE@
+AWK = @AWK@
+BINARY_CFLAGS = @BINARY_CFLAGS@
+BINARY_LDFLAGS = @BINARY_LDFLAGS@
+BISON = @BISON@
+CASSANDRA_CFLAGS = @CASSANDRA_CFLAGS@
+CASSANDRA_LIBS = @CASSANDRA_LIBS@
+CC = @CC@
+CCDEPMODE = @CCDEPMODE@
+CDB_LIBS = @CDB_LIBS@
+CFLAGS = @CFLAGS@
+CLUCENE_CFLAGS = @CLUCENE_CFLAGS@
+CLUCENE_LIBS = @CLUCENE_LIBS@
+COMPRESS_LIBS = @COMPRESS_LIBS@
+CPP = @CPP@
+CPPFLAGS = @CPPFLAGS@
+CRYPT_LIBS = @CRYPT_LIBS@
+CXX = @CXX@
+CXXCPP = @CXXCPP@
+CXXDEPMODE = @CXXDEPMODE@
+CXXFLAGS = @CXXFLAGS@
+CYGPATH_W = @CYGPATH_W@
+DEFS = @DEFS@
+DEPDIR = @DEPDIR@
+DICT_LIBS = @DICT_LIBS@
+DLLIB = @DLLIB@
+DLLTOOL = @DLLTOOL@
+DSYMUTIL = @DSYMUTIL@
+DUMPBIN = @DUMPBIN@
+ECHO_C = @ECHO_C@
+ECHO_N = @ECHO_N@
+ECHO_T = @ECHO_T@
+EGREP = @EGREP@
+EXEEXT = @EXEEXT@
+FGREP = @FGREP@
+FLEX = @FLEX@
+FUZZER_CPPFLAGS = @FUZZER_CPPFLAGS@
+FUZZER_LDFLAGS = @FUZZER_LDFLAGS@
+GREP = @GREP@
+INSTALL = @INSTALL@
+INSTALL_DATA = @INSTALL_DATA@
+INSTALL_PROGRAM = @INSTALL_PROGRAM@
+INSTALL_SCRIPT = @INSTALL_SCRIPT@
+INSTALL_STRIP_PROGRAM = @INSTALL_STRIP_PROGRAM@
+KRB5CONFIG = @KRB5CONFIG@
+KRB5_CFLAGS = @KRB5_CFLAGS@
+KRB5_LIBS = @KRB5_LIBS@
+LD = @LD@
+LDAP_LIBS = @LDAP_LIBS@
+LDFLAGS = @LDFLAGS@
+LD_NO_WHOLE_ARCHIVE = @LD_NO_WHOLE_ARCHIVE@
+LD_WHOLE_ARCHIVE = @LD_WHOLE_ARCHIVE@
+LIBCAP = @LIBCAP@
+LIBDOVECOT = @LIBDOVECOT@
+LIBDOVECOT_COMPRESS = @LIBDOVECOT_COMPRESS@
+LIBDOVECOT_DEPS = @LIBDOVECOT_DEPS@
+LIBDOVECOT_DSYNC = @LIBDOVECOT_DSYNC@
+LIBDOVECOT_LA_LIBS = @LIBDOVECOT_LA_LIBS@
+LIBDOVECOT_LDA = @LIBDOVECOT_LDA@
+LIBDOVECOT_LDAP = @LIBDOVECOT_LDAP@
+LIBDOVECOT_LIBFTS = @LIBDOVECOT_LIBFTS@
+LIBDOVECOT_LIBFTS_DEPS = @LIBDOVECOT_LIBFTS_DEPS@
+LIBDOVECOT_LOGIN = @LIBDOVECOT_LOGIN@
+LIBDOVECOT_LUA = @LIBDOVECOT_LUA@
+LIBDOVECOT_LUA_DEPS = @LIBDOVECOT_LUA_DEPS@
+LIBDOVECOT_SQL = @LIBDOVECOT_SQL@
+LIBDOVECOT_STORAGE = @LIBDOVECOT_STORAGE@
+LIBDOVECOT_STORAGE_DEPS = @LIBDOVECOT_STORAGE_DEPS@
+LIBEXTTEXTCAT_CFLAGS = @LIBEXTTEXTCAT_CFLAGS@
+LIBEXTTEXTCAT_LIBS = @LIBEXTTEXTCAT_LIBS@
+LIBICONV = @LIBICONV@
+LIBICU_CFLAGS = @LIBICU_CFLAGS@
+LIBICU_LIBS = @LIBICU_LIBS@
+LIBOBJS = @LIBOBJS@
+LIBS = @LIBS@
+LIBSODIUM_CFLAGS = @LIBSODIUM_CFLAGS@
+LIBSODIUM_LIBS = @LIBSODIUM_LIBS@
+LIBTIRPC_CFLAGS = @LIBTIRPC_CFLAGS@
+LIBTIRPC_LIBS = @LIBTIRPC_LIBS@
+LIBTOOL = @LIBTOOL@
+LIBUNWIND_CFLAGS = @LIBUNWIND_CFLAGS@
+LIBUNWIND_LIBS = @LIBUNWIND_LIBS@
+LIBWRAP_LIBS = @LIBWRAP_LIBS@
+LINKED_STORAGE_LDADD = @LINKED_STORAGE_LDADD@
+LIPO = @LIPO@
+LN_S = @LN_S@
+LTLIBICONV = @LTLIBICONV@
+LTLIBOBJS = @LTLIBOBJS@
+LT_SYS_LIBRARY_PATH = @LT_SYS_LIBRARY_PATH@
+LUA_CFLAGS = @LUA_CFLAGS@
+LUA_LIBS = @LUA_LIBS@
+MAINT = @MAINT@
+MAKEINFO = @MAKEINFO@
+MANIFEST_TOOL = @MANIFEST_TOOL@
+MKDIR_P = @MKDIR_P@
+MODULE_LIBS = @MODULE_LIBS@
+MODULE_SUFFIX = @MODULE_SUFFIX@
+MYSQL_CFLAGS = @MYSQL_CFLAGS@
+MYSQL_CONFIG = @MYSQL_CONFIG@
+MYSQL_LIBS = @MYSQL_LIBS@
+NM = @NM@
+NMEDIT = @NMEDIT@
+NOPLUGIN_LDFLAGS = @NOPLUGIN_LDFLAGS@
+OBJDUMP = @OBJDUMP@
+OBJEXT = @OBJEXT@
+OTOOL = @OTOOL@
+OTOOL64 = @OTOOL64@
+PACKAGE = @PACKAGE@
+PACKAGE_BUGREPORT = @PACKAGE_BUGREPORT@
+PACKAGE_NAME = @PACKAGE_NAME@
+PACKAGE_STRING = @PACKAGE_STRING@
+PACKAGE_TARNAME = @PACKAGE_TARNAME@
+PACKAGE_URL = @PACKAGE_URL@
+PACKAGE_VERSION = @PACKAGE_VERSION@
+PANDOC = @PANDOC@
+PATH_SEPARATOR = @PATH_SEPARATOR@
+PGSQL_CFLAGS = @PGSQL_CFLAGS@
+PGSQL_LIBS = @PGSQL_LIBS@
+PG_CONFIG = @PG_CONFIG@
+PIE_CFLAGS = @PIE_CFLAGS@
+PIE_LDFLAGS = @PIE_LDFLAGS@
+PKG_CONFIG = @PKG_CONFIG@
+PKG_CONFIG_LIBDIR = @PKG_CONFIG_LIBDIR@
+PKG_CONFIG_PATH = @PKG_CONFIG_PATH@
+QUOTA_LIBS = @QUOTA_LIBS@
+RANLIB = @RANLIB@
+RELRO_LDFLAGS = @RELRO_LDFLAGS@
+RPCGEN = @RPCGEN@
+RUN_TEST = @RUN_TEST@
+SED = @SED@
+SETTING_FILES = @SETTING_FILES@
+SET_MAKE = @SET_MAKE@
+SHELL = @SHELL@
+SQLITE_CFLAGS = @SQLITE_CFLAGS@
+SQLITE_LIBS = @SQLITE_LIBS@
+SQL_CFLAGS = @SQL_CFLAGS@
+SQL_LIBS = @SQL_LIBS@
+SSL_CFLAGS = @SSL_CFLAGS@
+SSL_LIBS = @SSL_LIBS@
+STRIP = @STRIP@
+SYSTEMD_CFLAGS = @SYSTEMD_CFLAGS@
+SYSTEMD_LIBS = @SYSTEMD_LIBS@
+VALGRIND = @VALGRIND@
+VERSION = @VERSION@
+ZSTD_CFLAGS = @ZSTD_CFLAGS@
+ZSTD_LIBS = @ZSTD_LIBS@
+abs_builddir = @abs_builddir@
+abs_srcdir = @abs_srcdir@
+abs_top_builddir = @abs_top_builddir@
+abs_top_srcdir = @abs_top_srcdir@
+ac_ct_AR = @ac_ct_AR@
+ac_ct_CC = @ac_ct_CC@
+ac_ct_CXX = @ac_ct_CXX@
+ac_ct_DUMPBIN = @ac_ct_DUMPBIN@
+am__include = @am__include@
+am__leading_dot = @am__leading_dot@
+am__quote = @am__quote@
+am__tar = @am__tar@
+am__untar = @am__untar@
+bindir = @bindir@
+build = @build@
+build_alias = @build_alias@
+build_cpu = @build_cpu@
+build_os = @build_os@
+build_vendor = @build_vendor@
+builddir = @builddir@
+datadir = @datadir@
+datarootdir = @datarootdir@
+dict_drivers = @dict_drivers@
+docdir = @docdir@
+dvidir = @dvidir@
+exec_prefix = @exec_prefix@
+host = @host@
+host_alias = @host_alias@
+host_cpu = @host_cpu@
+host_os = @host_os@
+host_vendor = @host_vendor@
+htmldir = @htmldir@
+includedir = @includedir@
+infodir = @infodir@
+install_sh = @install_sh@
+libdir = @libdir@
+libexecdir = @libexecdir@
+localedir = @localedir@
+localstatedir = @localstatedir@
+mandir = @mandir@
+mkdir_p = @mkdir_p@
+moduledir = @moduledir@
+oldincludedir = @oldincludedir@
+pdfdir = @pdfdir@
+prefix = @prefix@
+program_transform_name = @program_transform_name@
+psdir = @psdir@
+rundir = @rundir@
+runstatedir = @runstatedir@
+sbindir = @sbindir@
+sharedstatedir = @sharedstatedir@
+sql_drivers = @sql_drivers@
+srcdir = @srcdir@
+ssldir = @ssldir@
+statedir = @statedir@
+sysconfdir = @sysconfdir@
+systemdservicetype = @systemdservicetype@
+systemdsystemunitdir = @systemdsystemunitdir@
+target_alias = @target_alias@
+top_build_prefix = @top_build_prefix@
+top_builddir = @top_builddir@
+top_srcdir = @top_srcdir@
+pkglib_LTLIBRARIES = libdovecot-ldap.la
+AM_CPPFLAGS = \
+ -I$(top_srcdir)/src/lib \
+ -I$(top_srcdir)/src/lib-test \
+ -I$(top_srcdir)/src/lib-settings \
+ -I$(top_srcdir)/src/lib-master \
+ -I$(top_srcdir)/src/lib-ssl-iostream \
+ $(LDAP_CFLAGS)
+
+libdovecot_ldap_la_SOURCES = \
+ ldap-client.c \
+ ldap-connection.c \
+ ldap-connection-pool.c \
+ ldap-iterator.c \
+ ldap-search.c \
+ ldap-compare.c \
+ ldap-entry.c
+
+libdovecot_ldap_la_DEPENDENCIES = ../lib-dovecot/libdovecot.la
+libdovecot_ldap_la_LDFLAGS = -export-dynamic
+libdovecot_ldap_la_LIBADD = ../lib-dovecot/libdovecot.la $(LDAP_LIBS)
+headers = \
+ ldap-client.h
+
+noinst_HEADERS = \
+ ldap-connection-pool.h \
+ ldap-private.h
+
+pkginc_libdir = $(pkgincludedir)
+pkginc_lib_HEADERS = $(headers)
+test_libs = \
+ ../lib-test/libtest.la \
+ ../lib-ssl-iostream/libssl_iostream.la \
+ ../lib/liblib.la
+
+all: all-am
+
+.SUFFIXES:
+.SUFFIXES: .c .lo .o .obj
+$(srcdir)/Makefile.in: @MAINTAINER_MODE_TRUE@ $(srcdir)/Makefile.am $(am__configure_deps)
+ @for dep in $?; do \
+ case '$(am__configure_deps)' in \
+ *$$dep*) \
+ ( cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh ) \
+ && { if test -f $@; then exit 0; else break; fi; }; \
+ exit 1;; \
+ esac; \
+ done; \
+ echo ' cd $(top_srcdir) && $(AUTOMAKE) --foreign src/lib-ldap/Makefile'; \
+ $(am__cd) $(top_srcdir) && \
+ $(AUTOMAKE) --foreign src/lib-ldap/Makefile
+Makefile: $(srcdir)/Makefile.in $(top_builddir)/config.status
+ @case '$?' in \
+ *config.status*) \
+ cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh;; \
+ *) \
+ echo ' cd $(top_builddir) && $(SHELL) ./config.status $(subdir)/$@ $(am__maybe_remake_depfiles)'; \
+ cd $(top_builddir) && $(SHELL) ./config.status $(subdir)/$@ $(am__maybe_remake_depfiles);; \
+ esac;
+
+$(top_builddir)/config.status: $(top_srcdir)/configure $(CONFIG_STATUS_DEPENDENCIES)
+ cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh
+
+$(top_srcdir)/configure: @MAINTAINER_MODE_TRUE@ $(am__configure_deps)
+ cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh
+$(ACLOCAL_M4): @MAINTAINER_MODE_TRUE@ $(am__aclocal_m4_deps)
+ cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh
+$(am__aclocal_m4_deps):
+
+install-pkglibLTLIBRARIES: $(pkglib_LTLIBRARIES)
+ @$(NORMAL_INSTALL)
+ @list='$(pkglib_LTLIBRARIES)'; test -n "$(pkglibdir)" || list=; \
+ list2=; for p in $$list; do \
+ if test -f $$p; then \
+ list2="$$list2 $$p"; \
+ else :; fi; \
+ done; \
+ test -z "$$list2" || { \
+ echo " $(MKDIR_P) '$(DESTDIR)$(pkglibdir)'"; \
+ $(MKDIR_P) "$(DESTDIR)$(pkglibdir)" || exit 1; \
+ echo " $(LIBTOOL) $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=install $(INSTALL) $(INSTALL_STRIP_FLAG) $$list2 '$(DESTDIR)$(pkglibdir)'"; \
+ $(LIBTOOL) $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=install $(INSTALL) $(INSTALL_STRIP_FLAG) $$list2 "$(DESTDIR)$(pkglibdir)"; \
+ }
+
+uninstall-pkglibLTLIBRARIES:
+ @$(NORMAL_UNINSTALL)
+ @list='$(pkglib_LTLIBRARIES)'; test -n "$(pkglibdir)" || list=; \
+ for p in $$list; do \
+ $(am__strip_dir) \
+ echo " $(LIBTOOL) $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=uninstall rm -f '$(DESTDIR)$(pkglibdir)/$$f'"; \
+ $(LIBTOOL) $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=uninstall rm -f "$(DESTDIR)$(pkglibdir)/$$f"; \
+ done
+
+clean-pkglibLTLIBRARIES:
+ -test -z "$(pkglib_LTLIBRARIES)" || rm -f $(pkglib_LTLIBRARIES)
+ @list='$(pkglib_LTLIBRARIES)'; \
+ locs=`for p in $$list; do echo $$p; done | \
+ sed 's|^[^/]*$$|.|; s|/[^/]*$$||; s|$$|/so_locations|' | \
+ sort -u`; \
+ test -z "$$locs" || { \
+ echo rm -f $${locs}; \
+ rm -f $${locs}; \
+ }
+
+libdovecot-ldap.la: $(libdovecot_ldap_la_OBJECTS) $(libdovecot_ldap_la_DEPENDENCIES) $(EXTRA_libdovecot_ldap_la_DEPENDENCIES)
+ $(AM_V_CCLD)$(libdovecot_ldap_la_LINK) -rpath $(pkglibdir) $(libdovecot_ldap_la_OBJECTS) $(libdovecot_ldap_la_LIBADD) $(LIBS)
+
+mostlyclean-compile:
+ -rm -f *.$(OBJEXT)
+
+distclean-compile:
+ -rm -f *.tab.c
+
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/ldap-client.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/ldap-compare.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/ldap-connection-pool.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/ldap-connection.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/ldap-entry.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/ldap-iterator.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/ldap-search.Plo@am__quote@ # am--include-marker
+
+$(am__depfiles_remade):
+ @$(MKDIR_P) $(@D)
+ @echo '# dummy' >$@-t && $(am__mv) $@-t $@
+
+am--depfiles: $(am__depfiles_remade)
+
+.c.o:
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(COMPILE) -MT $@ -MD -MP -MF $(DEPDIR)/$*.Tpo -c -o $@ $<
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/$*.Tpo $(DEPDIR)/$*.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='$<' object='$@' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(COMPILE) -c -o $@ $<
+
+.c.obj:
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(COMPILE) -MT $@ -MD -MP -MF $(DEPDIR)/$*.Tpo -c -o $@ `$(CYGPATH_W) '$<'`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/$*.Tpo $(DEPDIR)/$*.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='$<' object='$@' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(COMPILE) -c -o $@ `$(CYGPATH_W) '$<'`
+
+.c.lo:
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(LTCOMPILE) -MT $@ -MD -MP -MF $(DEPDIR)/$*.Tpo -c -o $@ $<
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/$*.Tpo $(DEPDIR)/$*.Plo
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='$<' object='$@' libtool=yes @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(LTCOMPILE) -c -o $@ $<
+
+mostlyclean-libtool:
+ -rm -f *.lo
+
+clean-libtool:
+ -rm -rf .libs _libs
+install-pkginc_libHEADERS: $(pkginc_lib_HEADERS)
+ @$(NORMAL_INSTALL)
+ @list='$(pkginc_lib_HEADERS)'; test -n "$(pkginc_libdir)" || list=; \
+ if test -n "$$list"; then \
+ echo " $(MKDIR_P) '$(DESTDIR)$(pkginc_libdir)'"; \
+ $(MKDIR_P) "$(DESTDIR)$(pkginc_libdir)" || exit 1; \
+ fi; \
+ for p in $$list; do \
+ if test -f "$$p"; then d=; else d="$(srcdir)/"; fi; \
+ echo "$$d$$p"; \
+ done | $(am__base_list) | \
+ while read files; do \
+ echo " $(INSTALL_HEADER) $$files '$(DESTDIR)$(pkginc_libdir)'"; \
+ $(INSTALL_HEADER) $$files "$(DESTDIR)$(pkginc_libdir)" || exit $$?; \
+ done
+
+uninstall-pkginc_libHEADERS:
+ @$(NORMAL_UNINSTALL)
+ @list='$(pkginc_lib_HEADERS)'; test -n "$(pkginc_libdir)" || list=; \
+ files=`for p in $$list; do echo $$p; done | sed -e 's|^.*/||'`; \
+ dir='$(DESTDIR)$(pkginc_libdir)'; $(am__uninstall_files_from_dir)
+
+ID: $(am__tagged_files)
+ $(am__define_uniq_tagged_files); mkid -fID $$unique
+tags: tags-am
+TAGS: tags
+
+tags-am: $(TAGS_DEPENDENCIES) $(am__tagged_files)
+ set x; \
+ here=`pwd`; \
+ $(am__define_uniq_tagged_files); \
+ shift; \
+ if test -z "$(ETAGS_ARGS)$$*$$unique"; then :; else \
+ test -n "$$unique" || unique=$$empty_fix; \
+ if test $$# -gt 0; then \
+ $(ETAGS) $(ETAGSFLAGS) $(AM_ETAGSFLAGS) $(ETAGS_ARGS) \
+ "$$@" $$unique; \
+ else \
+ $(ETAGS) $(ETAGSFLAGS) $(AM_ETAGSFLAGS) $(ETAGS_ARGS) \
+ $$unique; \
+ fi; \
+ fi
+ctags: ctags-am
+
+CTAGS: ctags
+ctags-am: $(TAGS_DEPENDENCIES) $(am__tagged_files)
+ $(am__define_uniq_tagged_files); \
+ test -z "$(CTAGS_ARGS)$$unique" \
+ || $(CTAGS) $(CTAGSFLAGS) $(AM_CTAGSFLAGS) $(CTAGS_ARGS) \
+ $$unique
+
+GTAGS:
+ here=`$(am__cd) $(top_builddir) && pwd` \
+ && $(am__cd) $(top_srcdir) \
+ && gtags -i $(GTAGS_ARGS) "$$here"
+cscopelist: cscopelist-am
+
+cscopelist-am: $(am__tagged_files)
+ list='$(am__tagged_files)'; \
+ case "$(srcdir)" in \
+ [\\/]* | ?:[\\/]*) sdir="$(srcdir)" ;; \
+ *) sdir=$(subdir)/$(srcdir) ;; \
+ esac; \
+ for i in $$list; do \
+ if test -f "$$i"; then \
+ echo "$(subdir)/$$i"; \
+ else \
+ echo "$$sdir/$$i"; \
+ fi; \
+ done >> $(top_builddir)/cscope.files
+
+distclean-tags:
+ -rm -f TAGS ID GTAGS GRTAGS GSYMS GPATH tags
+
+distdir: $(BUILT_SOURCES)
+ $(MAKE) $(AM_MAKEFLAGS) distdir-am
+
+distdir-am: $(DISTFILES)
+ @srcdirstrip=`echo "$(srcdir)" | sed 's/[].[^$$\\*]/\\\\&/g'`; \
+ topsrcdirstrip=`echo "$(top_srcdir)" | sed 's/[].[^$$\\*]/\\\\&/g'`; \
+ list='$(DISTFILES)'; \
+ dist_files=`for file in $$list; do echo $$file; done | \
+ sed -e "s|^$$srcdirstrip/||;t" \
+ -e "s|^$$topsrcdirstrip/|$(top_builddir)/|;t"`; \
+ case $$dist_files in \
+ */*) $(MKDIR_P) `echo "$$dist_files" | \
+ sed '/\//!d;s|^|$(distdir)/|;s,/[^/]*$$,,' | \
+ sort -u` ;; \
+ esac; \
+ for file in $$dist_files; do \
+ if test -f $$file || test -d $$file; then d=.; else d=$(srcdir); fi; \
+ if test -d $$d/$$file; then \
+ dir=`echo "/$$file" | sed -e 's,/[^/]*$$,,'`; \
+ if test -d "$(distdir)/$$file"; then \
+ find "$(distdir)/$$file" -type d ! -perm -700 -exec chmod u+rwx {} \;; \
+ fi; \
+ if test -d $(srcdir)/$$file && test $$d != $(srcdir); then \
+ cp -fpR $(srcdir)/$$file "$(distdir)$$dir" || exit 1; \
+ find "$(distdir)/$$file" -type d ! -perm -700 -exec chmod u+rwx {} \;; \
+ fi; \
+ cp -fpR $$d/$$file "$(distdir)$$dir" || exit 1; \
+ else \
+ test -f "$(distdir)/$$file" \
+ || cp -p $$d/$$file "$(distdir)/$$file" \
+ || exit 1; \
+ fi; \
+ done
+check-am: all-am
+ $(MAKE) $(AM_MAKEFLAGS) check-local
+check: check-am
+all-am: Makefile $(LTLIBRARIES) $(HEADERS)
+installdirs:
+ for dir in "$(DESTDIR)$(pkglibdir)" "$(DESTDIR)$(pkginc_libdir)"; do \
+ test -z "$$dir" || $(MKDIR_P) "$$dir"; \
+ done
+install: install-am
+install-exec: install-exec-am
+install-data: install-data-am
+uninstall: uninstall-am
+
+install-am: all-am
+ @$(MAKE) $(AM_MAKEFLAGS) install-exec-am install-data-am
+
+installcheck: installcheck-am
+install-strip:
+ if test -z '$(STRIP)'; then \
+ $(MAKE) $(AM_MAKEFLAGS) INSTALL_PROGRAM="$(INSTALL_STRIP_PROGRAM)" \
+ install_sh_PROGRAM="$(INSTALL_STRIP_PROGRAM)" INSTALL_STRIP_FLAG=-s \
+ install; \
+ else \
+ $(MAKE) $(AM_MAKEFLAGS) INSTALL_PROGRAM="$(INSTALL_STRIP_PROGRAM)" \
+ install_sh_PROGRAM="$(INSTALL_STRIP_PROGRAM)" INSTALL_STRIP_FLAG=-s \
+ "INSTALL_PROGRAM_ENV=STRIPPROG='$(STRIP)'" install; \
+ fi
+mostlyclean-generic:
+
+clean-generic:
+
+distclean-generic:
+ -test -z "$(CONFIG_CLEAN_FILES)" || rm -f $(CONFIG_CLEAN_FILES)
+ -test . = "$(srcdir)" || test -z "$(CONFIG_CLEAN_VPATH_FILES)" || rm -f $(CONFIG_CLEAN_VPATH_FILES)
+
+maintainer-clean-generic:
+ @echo "This command is intended for maintainers to use"
+ @echo "it deletes files that may require special tools to rebuild."
+clean: clean-am
+
+clean-am: clean-generic clean-libtool clean-pkglibLTLIBRARIES \
+ mostlyclean-am
+
+distclean: distclean-am
+ -rm -f ./$(DEPDIR)/ldap-client.Plo
+ -rm -f ./$(DEPDIR)/ldap-compare.Plo
+ -rm -f ./$(DEPDIR)/ldap-connection-pool.Plo
+ -rm -f ./$(DEPDIR)/ldap-connection.Plo
+ -rm -f ./$(DEPDIR)/ldap-entry.Plo
+ -rm -f ./$(DEPDIR)/ldap-iterator.Plo
+ -rm -f ./$(DEPDIR)/ldap-search.Plo
+ -rm -f Makefile
+distclean-am: clean-am distclean-compile distclean-generic \
+ distclean-tags
+
+dvi: dvi-am
+
+dvi-am:
+
+html: html-am
+
+html-am:
+
+info: info-am
+
+info-am:
+
+install-data-am: install-pkginc_libHEADERS
+
+install-dvi: install-dvi-am
+
+install-dvi-am:
+
+install-exec-am: install-pkglibLTLIBRARIES
+
+install-html: install-html-am
+
+install-html-am:
+
+install-info: install-info-am
+
+install-info-am:
+
+install-man:
+
+install-pdf: install-pdf-am
+
+install-pdf-am:
+
+install-ps: install-ps-am
+
+install-ps-am:
+
+installcheck-am:
+
+maintainer-clean: maintainer-clean-am
+ -rm -f ./$(DEPDIR)/ldap-client.Plo
+ -rm -f ./$(DEPDIR)/ldap-compare.Plo
+ -rm -f ./$(DEPDIR)/ldap-connection-pool.Plo
+ -rm -f ./$(DEPDIR)/ldap-connection.Plo
+ -rm -f ./$(DEPDIR)/ldap-entry.Plo
+ -rm -f ./$(DEPDIR)/ldap-iterator.Plo
+ -rm -f ./$(DEPDIR)/ldap-search.Plo
+ -rm -f Makefile
+maintainer-clean-am: distclean-am maintainer-clean-generic
+
+mostlyclean: mostlyclean-am
+
+mostlyclean-am: mostlyclean-compile mostlyclean-generic \
+ mostlyclean-libtool
+
+pdf: pdf-am
+
+pdf-am:
+
+ps: ps-am
+
+ps-am:
+
+uninstall-am: uninstall-pkginc_libHEADERS uninstall-pkglibLTLIBRARIES
+
+.MAKE: check-am install-am install-strip
+
+.PHONY: CTAGS GTAGS TAGS all all-am am--depfiles check check-am \
+ check-local clean clean-generic clean-libtool \
+ clean-pkglibLTLIBRARIES cscopelist-am ctags ctags-am distclean \
+ distclean-compile distclean-generic distclean-libtool \
+ distclean-tags distdir dvi dvi-am html html-am info info-am \
+ install install-am install-data install-data-am install-dvi \
+ install-dvi-am install-exec install-exec-am install-html \
+ install-html-am install-info install-info-am install-man \
+ install-pdf install-pdf-am install-pkginc_libHEADERS \
+ install-pkglibLTLIBRARIES install-ps install-ps-am \
+ install-strip installcheck installcheck-am installdirs \
+ maintainer-clean maintainer-clean-generic mostlyclean \
+ mostlyclean-compile mostlyclean-generic mostlyclean-libtool \
+ pdf pdf-am ps ps-am tags tags-am uninstall uninstall-am \
+ uninstall-pkginc_libHEADERS uninstall-pkglibLTLIBRARIES
+
+.PRECIOUS: Makefile
+
+
+check-local:
+ for bin in $(test_programs); do \
+ if ! $(RUN_TEST) ./$$bin; then exit 1; fi; \
+ done
+
+# Tell versions [3.59,3.63) of GNU make to not export all variables.
+# Otherwise a system limit (for SysV at least) may be exceeded.
+.NOEXPORT:
diff --git a/src/lib-ldap/ldap-client.c b/src/lib-ldap/ldap-client.c
new file mode 100644
index 0000000..bbae910
--- /dev/null
+++ b/src/lib-ldap/ldap-client.c
@@ -0,0 +1,74 @@
+/* Copyright (c) 2016-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "ldap-connection-pool.h"
+#include "ldap-private.h"
+
+/* Max number of ldap-connections that can be created. For now this is
+ unlimited since we're assuming our callers aren't calling us with many
+ different settings. */
+#define LDAP_CONN_POOL_MAX_CONNECTIONS UINT_MAX
+
+struct ldap_client {
+ struct ldap_connection_list *list;
+};
+
+static struct ldap_connection_pool *ldap_conn_pool = NULL;
+
+int ldap_client_init(const struct ldap_client_settings *set,
+ struct ldap_client **client_r, const char **error_r)
+{
+ struct ldap_client *client;
+
+ if (ldap_conn_pool == NULL)
+ ldap_conn_pool = ldap_connection_pool_init(LDAP_CONN_POOL_MAX_CONNECTIONS);
+
+ client = i_new(struct ldap_client, 1);
+ if (ldap_connection_pool_get(ldap_conn_pool, client, set,
+ &client->list, error_r) < 0) {
+ i_free(client);
+ return -1;
+ }
+ *client_r = client;
+ return 0;
+}
+
+void ldap_client_deinit(struct ldap_client **_client)
+{
+ struct ldap_client *client = *_client;
+
+ *_client = NULL;
+
+ ldap_connection_pool_unref(ldap_conn_pool, &client->list);
+ i_free(client);
+}
+
+void ldap_client_switch_ioloop(struct ldap_client *client)
+{
+ ldap_connection_switch_ioloop(client->list->conn);
+}
+
+#undef ldap_search_start
+void ldap_search_start(struct ldap_client *client,
+ const struct ldap_search_input *input,
+ ldap_result_callback_t *callback, void *context)
+{
+ /* FIXME: we could support multiple concurrent LDAP connections to
+ the same host. */
+ ldap_connection_search_start(client->list->conn, input, callback, context);
+}
+
+#undef ldap_compare_start
+void ldap_compare_start(struct ldap_client *client,
+ const struct ldap_compare_input *input,
+ ldap_result_callback_t *callback, void *context)
+{
+ ldap_connection_compare_start(client->list->conn, input, callback, context);
+}
+
+void ldap_clients_cleanup(void)
+{
+ if (ldap_conn_pool != NULL &&
+ !ldap_connection_pool_have_references(ldap_conn_pool))
+ ldap_connection_pool_deinit(&ldap_conn_pool);
+}
diff --git a/src/lib-ldap/ldap-client.h b/src/lib-ldap/ldap-client.h
new file mode 100644
index 0000000..1a231dd
--- /dev/null
+++ b/src/lib-ldap/ldap-client.h
@@ -0,0 +1,102 @@
+#ifndef LDAP_CLIENT_H
+#define LDAP_CLIENT_H
+
+enum ldap_scope {
+ LDAP_SEARCH_SCOPE_BASE = 0x0000,
+ LDAP_SEARCH_SCOPE_ONE = 0x0001,
+ LDAP_SEARCH_SCOPE_SUBTREE = 0x0002
+};
+
+struct ldap_client;
+struct ldap_result;
+struct ldap_search_iterator;
+struct ldap_entry;
+
+/* Called when the LDAP result has finished. The callback must verify first
+ if the result is valid or not by calling ldap_result_has_failed() or
+ ldap_result_get_error(). The result is freed automatically after this
+ callback finishes. */
+typedef void ldap_result_callback_t(struct ldap_result *result, void *context);
+
+struct ldap_client_settings {
+ /* NOTE: when adding here, remember to update
+ ldap_connection_have_settings() and ldap_connection_init() */
+ const char *uri;
+ const char *bind_dn;
+ const char *password;
+
+ const struct ssl_iostream_settings *ssl_set;
+
+ unsigned int timeout_secs;
+ unsigned int max_idle_time_secs;
+ unsigned int debug;
+ bool require_ssl;
+ bool start_tls;
+};
+
+struct ldap_search_input {
+ const char *base_dn;
+ const char *filter;
+ const char *const *attributes;
+ enum ldap_scope scope;
+
+ unsigned int size_limit;
+
+ unsigned int timeout_secs;
+};
+
+struct ldap_compare_input {
+ const char *dn;
+ const char *attr;
+ const char *value;
+
+ unsigned int timeout_secs;
+};
+
+/* Initialize LDAP. Returns 0 on success, or -1 and error_r if initialization
+ failed with the given settings. */
+int ldap_client_init(const struct ldap_client_settings *set,
+ struct ldap_client **client_r, const char **error_r);
+void ldap_client_deinit(struct ldap_client **client);
+void ldap_client_switch_ioloop(struct ldap_client *client);
+
+/* Deinitialize all pooled LDAP connections if there are no references left.
+ This allows freeing the memory at deinit, but still allows multiple
+ independent code parts to use lib-ldap and call this function. */
+void ldap_clients_cleanup(void);
+
+void ldap_search_start(struct ldap_client *client,
+ const struct ldap_search_input *input,
+ ldap_result_callback_t *callback,
+ void *context);
+#define ldap_search_start(client, input, callback, context) \
+ ldap_search_start(client, input - \
+ CALLBACK_TYPECHECK(callback, void (*)( \
+ struct ldap_result *, typeof(context))), \
+ (ldap_result_callback_t *)callback, context)
+
+/* Returns TRUE if the LDAP query failed and result must not be used further. */
+bool ldap_result_has_failed(struct ldap_result *result);
+/* Returns the error string if the query had failed, or NULL if it hasn't. */
+const char *ldap_result_get_error(struct ldap_result *result);
+
+struct ldap_search_iterator* ldap_search_iterator_init(struct ldap_result *result);
+const struct ldap_entry *ldap_search_iterator_next(struct ldap_search_iterator *iter);
+void ldap_search_iterator_deinit(struct ldap_search_iterator **iter);
+
+void ldap_compare_start(struct ldap_client *client,
+ const struct ldap_compare_input *input,
+ ldap_result_callback_t *callback, void *context);
+#define ldap_compare_start(client, input, callback, context) \
+ ldap_compare_start(client, input - \
+ CALLBACK_TYPECHECK(callback, void (*)( \
+ struct ldap_result *, typeof(context))), \
+ (ldap_result_callback_t *)callback, context)
+/* Returns TRUE if the comparison matched, FALSE if not. */
+bool ldap_compare_result(struct ldap_result *result);
+
+const char *ldap_entry_dn(const struct ldap_entry *entry);
+const char *const *ldap_entry_get_attributes(const struct ldap_entry *entry);
+const char *const *ldap_entry_get_attribute(const struct ldap_entry *entry, const char *attribute);
+
+#endif
diff --git a/src/lib-ldap/ldap-compare.c b/src/lib-ldap/ldap-compare.c
new file mode 100644
index 0000000..9b17373
--- /dev/null
+++ b/src/lib-ldap/ldap-compare.c
@@ -0,0 +1,121 @@
+/* Copyright (c) 2016-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "ldap-private.h"
+
+static int
+ldap_compare_callback(struct ldap_connection *conn,
+ struct ldap_op_queue_entry *req,
+ LDAPMessage *message, bool *finished_r)
+{
+ int msgtype = ldap_msgtype(message);
+ struct ldap_result res;
+ char *result_errmsg;
+ int ret, result_err;
+
+ if (msgtype != LDAP_RES_COMPARE) {
+ *finished_r = FALSE;
+ return 0;
+ }
+ *finished_r = TRUE;
+
+ ret = ldap_parse_result(conn->conn, message,
+ &result_err, NULL,
+ &result_errmsg, NULL, NULL, 0);
+ i_zero(&res);
+ res.openldap_ret = ret;
+ if (ret != LDAP_SUCCESS) {
+ res.error_string = t_strdup_printf(
+ "ldap_parse_result() failed to parse compare: %s",
+ ldap_err2string(ret));
+ } else if (result_err == LDAP_COMPARE_TRUE) {
+ res.compare_true = TRUE;
+ } else if (result_err == LDAP_COMPARE_FALSE) {
+ res.compare_true = FALSE;
+ } else {
+ const struct ldap_compare_input *input = &req->input.compare;
+ const char *error = result_errmsg != NULL ?
+ result_errmsg : ldap_err2string(result_err);
+ res.openldap_ret = result_err;
+ res.error_string = t_strdup_printf(
+ "ldap_compare_ext(dn=%s, attr=%s) failed: %s",
+ input->dn, input->attr, error);
+ }
+
+ req->result_callback(&res, req->result_callback_ctx);
+
+ if (result_errmsg != NULL)
+ ldap_memfree(result_errmsg);
+ return res.openldap_ret;
+}
+
+static int
+ldap_compare_send(struct ldap_connection *conn, struct ldap_op_queue_entry *req,
+ const char **error_r)
+{
+ const struct ldap_compare_input *input = &req->input.compare;
+ struct berval bv = {
+ .bv_len = strlen(input->value),
+ .bv_val = (void*)input->value
+ };
+
+ LDAPControl manageDSAIT = {
+ LDAP_CONTROL_MANAGEDSAIT, {0, 0}, 0
+ };
+
+ /* try to use ManageDSAIT if available */
+ LDAPControl *sctrls[] = {
+ &manageDSAIT,
+ NULL
+ };
+
+ int ret = ldap_compare_ext(conn->conn,
+ input->dn,
+ input->attr,
+ &bv,
+ sctrls,
+ NULL,
+ &(req->msgid));
+
+ if (ret != LDAP_SUCCESS) {
+ *error_r = t_strdup_printf(
+ "ldap_compare_ext(dn=%s, attr=%s) failed: %s",
+ input->dn, input->attr, ldap_err2string(ret));
+ }
+ return ret;
+}
+
+void ldap_connection_compare_start(struct ldap_connection *conn,
+ const struct ldap_compare_input *input,
+ ldap_result_callback_t *callback,
+ void *context)
+{
+ struct ldap_op_queue_entry *req;
+ pool_t pool = pool_alloconly_create(MEMPOOL_GROWING "ldap compare", 128);
+ req = p_new(pool, struct ldap_op_queue_entry, 1);
+ req->pool = pool;
+
+ req->internal_response_cb = ldap_compare_callback;
+
+ req->input.compare = *input;
+ req->result_callback = callback;
+ req->result_callback_ctx = context;
+
+ /* copy strings */
+ req->input.compare.dn = p_strdup(req->pool, input->dn);
+ req->input.compare.attr = p_strdup(req->pool, input->attr);
+ req->input.compare.value = p_strdup(req->pool, input->value);
+
+ req->send_request_cb = ldap_compare_send;
+ req->timeout_secs = input->timeout_secs;
+
+ ldap_connection_queue_request(conn, req);
+}
+
+bool ldap_compare_result(struct ldap_result *result)
+{
+ i_assert(result->openldap_ret == LDAP_SUCCESS);
+ i_assert(result->error_string == NULL);
+
+ return result->compare_true;
+}
diff --git a/src/lib-ldap/ldap-connection-pool.c b/src/lib-ldap/ldap-connection-pool.c
new file mode 100644
index 0000000..7635ce0
--- /dev/null
+++ b/src/lib-ldap/ldap-connection-pool.c
@@ -0,0 +1,113 @@
+/* Copyright (c) 2016-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "llist.h"
+#include "ldap-private.h"
+#include "ldap-connection-pool.h"
+
+struct ldap_connection_pool {
+ struct ldap_connection_list *conn_list;
+ unsigned int conn_count;
+
+ unsigned int max_connections;
+};
+
+static void ldap_connection_list_remove(struct ldap_connection_pool *pool,
+ struct ldap_connection_list *list)
+{
+ DLLIST_REMOVE(&pool->conn_list, list);
+ pool->conn_count--;
+
+ ldap_connection_deinit(&list->conn);
+ i_free(list);
+}
+
+static void
+ldap_connection_pool_shrink_to(struct ldap_connection_pool *pool,
+ unsigned int max_count)
+{
+ struct ldap_connection_list *list, *next;
+
+ list = pool->conn_list;
+ for (; list != NULL && pool->conn_count > max_count; list = next) {
+ next = list->next;
+ if (list->refcount == 0)
+ ldap_connection_list_remove(pool, list);
+ }
+}
+
+struct ldap_connection_pool *
+ldap_connection_pool_init(unsigned int max_connections)
+{
+ struct ldap_connection_pool *pool;
+
+ pool = i_new(struct ldap_connection_pool, 1);
+ pool->max_connections = max_connections;
+ return pool;
+}
+
+void ldap_connection_pool_deinit(struct ldap_connection_pool **_pool)
+{
+ struct ldap_connection_pool *pool = *_pool;
+
+ *_pool = NULL;
+
+ ldap_connection_pool_shrink_to(pool, 0);
+ i_assert(pool->conn_list == NULL);
+ i_free(pool);
+}
+
+int ldap_connection_pool_get(struct ldap_connection_pool *pool,
+ struct ldap_client *client,
+ const struct ldap_client_settings *set,
+ struct ldap_connection_list **list_r,
+ const char **error_r)
+{
+ struct ldap_connection_list *list;
+ struct ldap_connection *conn;
+
+ for (list = pool->conn_list; list != NULL; list = list->next) {
+ if (ldap_connection_have_settings(list->conn, set)) {
+ list->refcount++;
+ *list_r = list;
+ return 0;
+ }
+ }
+ if (ldap_connection_init(client, set, &conn, error_r) < 0)
+ return -1;
+
+ list = i_new(struct ldap_connection_list, 1);
+ list->conn = conn;
+ list->refcount++;
+
+ DLLIST_PREPEND(&pool->conn_list, list);
+ pool->conn_count++;
+
+ ldap_connection_pool_shrink_to(pool, pool->max_connections);
+ *list_r = list;
+ return 0;
+}
+
+void ldap_connection_pool_unref(struct ldap_connection_pool *pool,
+ struct ldap_connection_list **_list)
+{
+ struct ldap_connection_list *list = *_list;
+
+ *_list = NULL;
+
+ i_assert(list->refcount > 0);
+
+ if (--list->refcount == 0)
+ ldap_connection_pool_shrink_to(pool, pool->max_connections);
+}
+
+bool ldap_connection_pool_have_references(struct ldap_connection_pool *pool)
+{
+ struct ldap_connection_list *list;
+
+ for (list = pool->conn_list; list != NULL; list = list->next) {
+ if (list->refcount > 0)
+ return TRUE;
+ }
+ return FALSE;
+}
diff --git a/src/lib-ldap/ldap-connection-pool.h b/src/lib-ldap/ldap-connection-pool.h
new file mode 100644
index 0000000..00cf165
--- /dev/null
+++ b/src/lib-ldap/ldap-connection-pool.h
@@ -0,0 +1,27 @@
+#ifndef LDAP_CONNECTION_POOL_H
+#define LDAP_CONNECTION_POOL_H
+
+struct ldap_client;
+struct ldap_client_settings;
+
+struct ldap_connection_list {
+ struct ldap_connection_list *prev, *next;
+ struct ldap_connection *conn;
+ int refcount;
+};
+
+struct ldap_connection_pool *
+ldap_connection_pool_init(unsigned int max_connections);
+void ldap_connection_pool_deinit(struct ldap_connection_pool **_pool);
+/* Returns TRUE if there are connections with refcount>0 */
+bool ldap_connection_pool_have_references(struct ldap_connection_pool *pool);
+
+int ldap_connection_pool_get(struct ldap_connection_pool *pool,
+ struct ldap_client *client,
+ const struct ldap_client_settings *set,
+ struct ldap_connection_list **list_r,
+ const char **error_r);
+void ldap_connection_pool_unref(struct ldap_connection_pool *pool,
+ struct ldap_connection_list **list);
+
+#endif
diff --git a/src/lib-ldap/ldap-connection.c b/src/lib-ldap/ldap-connection.c
new file mode 100644
index 0000000..95a91ea
--- /dev/null
+++ b/src/lib-ldap/ldap-connection.c
@@ -0,0 +1,714 @@
+/* Copyright (c) 2016-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "aqueue.h"
+#include "ioloop.h"
+#include "ldap-private.h"
+
+static
+void ldap_connection_read_more(struct ldap_connection *conn);
+static
+int ldap_connect_next_message(struct ldap_connection *conn, struct ldap_op_queue_entry *req, bool *finished_r);
+static
+void ldap_connection_abort_request(struct ldap_op_queue_entry *req);
+static
+void ldap_connection_request_destroy(struct ldap_op_queue_entry **req);
+static
+int ldap_connection_connect(struct ldap_connection *conn);
+static
+void ldap_connection_send_next(struct ldap_connection *conn);
+
+void ldap_connection_deinit(struct ldap_connection **_conn)
+{
+ struct ldap_connection *conn = *_conn;
+
+ *_conn = NULL;
+
+ ldap_connection_kill(conn);
+
+ unsigned int n = aqueue_count(conn->request_queue);
+ for (unsigned int i = 0; i < n; i++) {
+ struct ldap_op_queue_entry *req =
+ array_idx_elem(&conn->request_array,
+ aqueue_idx(conn->request_queue, i));
+ timeout_remove(&req->to_abort);
+ }
+ pool_unref(&conn->pool);
+}
+
+static
+int ldap_connection_setup(struct ldap_connection *conn, const char **error_r)
+{
+ int ret, opt;
+
+ ret = ldap_initialize(&conn->conn, conn->set.uri);
+ if (ret != LDAP_SUCCESS) {
+ *error_r = t_strdup_printf("ldap_initialize(uri=%s) failed: %s",
+ conn->set.uri, ldap_err2string(ret));
+ return -1;
+ }
+
+ if (conn->ssl_set.verify_remote_cert) {
+ opt = LDAP_OPT_X_TLS_HARD;
+ } else {
+ opt = LDAP_OPT_X_TLS_ALLOW;
+ }
+
+ ldap_set_option(conn->conn, LDAP_OPT_X_TLS, &opt);
+ ldap_set_option(conn->conn, LDAP_OPT_X_TLS_REQUIRE_CERT, &opt);
+#ifdef LDAP_OPT_X_TLS_PROTOCOL_MIN
+ /* refuse to connect to SSLv2 as it's completely insecure */
+ opt = LDAP_OPT_X_TLS_PROTOCOL_SSL3;
+ ldap_set_option(conn->conn, LDAP_OPT_X_TLS_PROTOCOL_MIN, &opt);
+#endif
+ opt = conn->set.timeout_secs;
+ /* default timeout */
+ ldap_set_option(conn->conn, LDAP_OPT_TIMEOUT, &opt);
+ ldap_set_option(conn->conn, LDAP_OPT_NETWORK_TIMEOUT, &opt);
+ /* timelimit */
+ ldap_set_option(conn->conn, LDAP_OPT_TIMELIMIT, &opt);
+
+ if (conn->ssl_set.ca_file != NULL)
+ ldap_set_option(conn->conn, LDAP_OPT_X_TLS_CACERTFILE, conn->ssl_set.ca_file);
+ if (conn->ssl_set.ca_dir != NULL)
+ ldap_set_option(conn->conn, LDAP_OPT_X_TLS_CACERTDIR, conn->ssl_set.ca_dir);
+
+ if (conn->ssl_set.cert.cert != NULL)
+ ldap_set_option(conn->conn, LDAP_OPT_X_TLS_CERTFILE, conn->ssl_set.cert.cert);
+ if (conn->ssl_set.cert.key != NULL)
+ ldap_set_option(conn->conn, LDAP_OPT_X_TLS_KEYFILE, conn->ssl_set.cert.key);
+
+ opt = conn->set.debug;
+ ldap_set_option(NULL, LDAP_OPT_DEBUG_LEVEL, &opt);
+
+ opt = LDAP_VERSION3;
+ ldap_set_option(conn->conn, LDAP_OPT_PROTOCOL_VERSION, &opt);
+
+ ldap_set_option(conn->conn, LDAP_OPT_REFERRALS, 0);
+
+#ifdef LDAP_OPT_X_TLS_NEWCTX
+ opt = 0;
+ ldap_set_option(conn->conn, LDAP_OPT_X_TLS_NEWCTX, &opt);
+#endif
+
+ return 0;
+}
+
+bool ldap_connection_have_settings(struct ldap_connection *conn,
+ const struct ldap_client_settings *set)
+{
+ const struct ldap_client_settings *conn_set = &conn->set;
+
+ if (strcmp(conn_set->uri, set->uri) != 0)
+ return FALSE;
+ if (null_strcmp(conn_set->bind_dn, set->bind_dn) != 0)
+ return FALSE;
+ if (null_strcmp(conn_set->password, set->password) != 0)
+ return FALSE;
+ if (conn_set->timeout_secs != set->timeout_secs ||
+ conn_set->max_idle_time_secs != set->max_idle_time_secs ||
+ conn_set->debug != set->debug ||
+ conn_set->require_ssl != set->require_ssl ||
+ conn_set->start_tls != set->start_tls)
+ return FALSE;
+
+ if (set->ssl_set == NULL || !set->start_tls)
+ return TRUE;
+
+ /* check SSL settings */
+ if (null_strcmp(conn->ssl_set.min_protocol, set->ssl_set->min_protocol) != 0)
+ return FALSE;
+ if (null_strcmp(conn->ssl_set.cipher_list, set->ssl_set->cipher_list) != 0)
+ return FALSE;
+ if (null_strcmp(conn->ssl_set.ca_file, set->ssl_set->ca_file) != 0)
+ return FALSE;
+ if (null_strcmp(conn->ssl_set.cert.cert, set->ssl_set->cert.cert) != 0)
+ return FALSE;
+ if (null_strcmp(conn->ssl_set.cert.key, set->ssl_set->cert.key) != 0)
+ return FALSE;
+ return TRUE;
+}
+
+int ldap_connection_init(struct ldap_client *client,
+ const struct ldap_client_settings *set,
+ struct ldap_connection **conn_r, const char **error_r)
+{
+ i_assert(set->uri != NULL);
+
+ if (set->require_ssl &&
+ !set->start_tls &&
+ strncmp("ldaps://",set->uri,8) != 0) {
+ *error_r = t_strdup_printf("ldap_connection_init(uri=%s) failed: %s", set->uri,
+ "uri does not start with ldaps and ssl required without start TLS");
+ return -1;
+ }
+
+ pool_t pool = pool_alloconly_create("ldap connection", 1024);
+ struct ldap_connection *conn = p_new(pool, struct ldap_connection, 1);
+ conn->pool = pool;
+
+ conn->client = client;
+ conn->set = *set;
+ /* deep copy relevant strings */
+ conn->set.uri = p_strdup(pool, set->uri);
+ conn->set.bind_dn = p_strdup(pool, set->bind_dn);
+ if (set->password != NULL) {
+ conn->set.password = p_strdup(pool, set->password);
+ ber_str2bv(conn->set.password, strlen(conn->set.password), 0, &conn->cred);
+ }
+ /* cannot use these */
+ conn->ssl_set.ca = NULL;
+ conn->ssl_set.cert.key_password = NULL;
+ conn->ssl_set.cert_username_field = NULL;
+ conn->ssl_set.crypto_device = NULL;
+
+ if (set->ssl_set != NULL) {
+ /* keep in sync with ldap_connection_have_settings() */
+ conn->set.ssl_set = &conn->ssl_set;
+ conn->ssl_set.min_protocol = p_strdup(pool, set->ssl_set->min_protocol);
+ conn->ssl_set.cipher_list = p_strdup(pool, set->ssl_set->cipher_list);
+ conn->ssl_set.ca_file = p_strdup(pool, set->ssl_set->ca_file);
+ conn->ssl_set.cert.cert = p_strdup(pool, set->ssl_set->cert.cert);
+ conn->ssl_set.cert.key = p_strdup(pool, set->ssl_set->cert.key);
+ }
+ i_assert(ldap_connection_have_settings(conn, set));
+
+ if (ldap_connection_setup(conn, error_r) < 0) {
+ ldap_connection_deinit(&conn);
+ return -1;
+ }
+
+ p_array_init(&conn->request_array, conn->pool, 10);
+ conn->request_queue = aqueue_init(&conn->request_array.arr);
+
+ *conn_r = conn;
+ return 0;
+}
+
+void ldap_connection_switch_ioloop(struct ldap_connection *conn)
+{
+ if (conn->io != NULL)
+ conn->io = io_loop_move_io(&conn->io);
+ if (conn->to_disconnect != NULL)
+ conn->to_disconnect = io_loop_move_timeout(&conn->to_disconnect);
+ if (conn->to_reconnect != NULL)
+ conn->to_reconnect = io_loop_move_timeout(&conn->to_reconnect);
+ unsigned int n = aqueue_count(conn->request_queue);
+
+ for (unsigned int i = 0; i < n; i++) {
+ struct ldap_op_queue_entry *req =
+ array_idx_elem(&conn->request_array,
+ aqueue_idx(conn->request_queue, i));
+ if (req->to_abort != NULL)
+ req->to_abort = io_loop_move_timeout(&req->to_abort);
+ }
+}
+
+static void
+ldap_connection_result_failure(struct ldap_connection *conn,
+ struct ldap_op_queue_entry *req,
+ int ret, const char *error)
+{
+ struct ldap_result res;
+ i_zero(&res);
+ res.conn = conn;
+ res.openldap_ret = ret;
+ res.error_string = error;
+ if (req->result_callback != NULL)
+ req->result_callback(&res, req->result_callback_ctx);
+ else
+ i_error("%s", error);
+ ldap_connection_kill(conn);
+}
+
+static
+void ldap_connection_result_success(struct ldap_connection *conn,
+ struct ldap_op_queue_entry *req)
+{
+ struct ldap_result res;
+ i_zero(&res);
+ res.conn = conn;
+ res.openldap_ret = LDAP_SUCCESS;
+ if (req->result_callback != NULL)
+ req->result_callback(&res, req->result_callback_ctx);
+}
+
+static struct ldap_op_queue_entry *
+ldap_connection_next_unsent_request(struct ldap_connection *conn,
+ unsigned int *index_r)
+{
+ struct ldap_op_queue_entry *last_req = NULL;
+ *index_r = 0;
+
+ for (unsigned int i = 0; i < aqueue_count(conn->request_queue); i++) {
+ struct ldap_op_queue_entry *req =
+ array_idx_elem(&conn->request_array,
+ aqueue_idx(conn->request_queue, i));
+ if (req->msgid > -1)
+ break;
+ *index_r = i;
+ last_req = req;
+ }
+ return last_req;
+}
+
+static
+void ldap_connection_send_next(struct ldap_connection *conn)
+{
+ unsigned int index;
+ struct ldap_op_queue_entry *req;
+
+ timeout_remove(&conn->to_reconnect);
+
+ if (conn->state == LDAP_STATE_DISCONNECT) {
+ if (ldap_connection_connect(conn) == -1)
+ conn->to_reconnect = timeout_add(1000, ldap_connection_send_next, conn);
+ return;
+ }
+
+ if (conn->state != LDAP_STATE_CONNECT) {
+ return;
+ }
+
+ if (conn->pending > 10) return; /* try again later */
+
+ req = ldap_connection_next_unsent_request(conn, &index);
+ /* nothing to actually send */
+ if (req == NULL) return;
+
+ i_assert(req->msgid == -1);
+
+ const char *error;
+ int ret;
+ if ((ret = req->send_request_cb(conn, req, &error)) != LDAP_SUCCESS) {
+ /* did not succeed */
+ struct ldap_result res;
+
+ i_zero(&res);
+ res.openldap_ret = ret;
+ res.error_string = error;
+ if (req->result_callback != NULL)
+ req->result_callback(&res, req->result_callback_ctx);
+
+ ldap_connection_request_destroy(&req);
+ aqueue_delete(conn->request_queue, index);
+ } else conn->pending++;
+}
+
+static
+void ldap_connection_request_destroy(struct ldap_op_queue_entry **_req)
+{
+ struct ldap_op_queue_entry *req = *_req;
+
+ *_req = NULL;
+
+ timeout_remove(&req->to_abort);
+ pool_unref(&req->pool);
+}
+
+void ldap_connection_queue_request(struct ldap_connection *conn, struct ldap_op_queue_entry *req)
+{
+ req->msgid = -1;
+ req->conn = conn;
+ aqueue_append(conn->request_queue, &req);
+ if (req->timeout_secs > 0)
+ req->to_abort = timeout_add(req->timeout_secs * 1000, ldap_connection_abort_request, req);
+
+ ldap_connection_send_next(conn);
+}
+
+static int
+ldap_connection_connect_parse(struct ldap_connection *conn,
+ struct ldap_op_queue_entry *req,
+ LDAPMessage *message, bool *finished_r)
+{
+ int ret, result_err;
+ char *retoid, *result_errmsg;
+ int msgtype = ldap_msgtype(message);
+
+ *finished_r = TRUE;
+ ret = ldap_parse_result(conn->conn, message, &result_err, NULL,
+ &result_errmsg, NULL, NULL, 0);
+
+ switch(conn->state) {
+ case LDAP_STATE_TLS:
+ if (msgtype != LDAP_RES_EXTENDED) {
+ *finished_r = FALSE;
+ return LDAP_SUCCESS;
+ }
+ if (ret != 0) {
+ ldap_connection_result_failure(conn, req, ret, t_strdup_printf(
+ "ldap_start_tls(uri=%s) failed: %s",
+ conn->set.uri, ldap_err2string(ret)));
+ return ret;
+ } else if (result_err != 0) {
+ if (conn->set.require_ssl) {
+ ldap_connection_result_failure(conn, req, result_err, t_strdup_printf(
+ "ldap_start_tls(uri=%s) failed: %s",
+ conn->set.uri, result_errmsg));
+ ldap_memfree(result_errmsg);
+ return LDAP_INVALID_CREDENTIALS; /* make sure it disconnects */
+ }
+ } else {
+ ret = ldap_parse_extended_result(conn->conn, message, &retoid, NULL, 0);
+ /* retoid can be NULL even if ret == 0 */
+ if (ret == 0) {
+ ret = ldap_install_tls(conn->conn);
+ if (ret != 0) {
+ // if this fails we have to abort
+ ldap_connection_result_failure(conn, req, ret, t_strdup_printf(
+ "ldap_start_tls(uri=%s) failed: %s",
+ conn->set.uri, ldap_err2string(ret)));
+ return LDAP_INVALID_CREDENTIALS;
+ }
+ }
+ if (ret != LDAP_SUCCESS) {
+ if (conn->set.require_ssl) {
+ ldap_connection_result_failure(conn, req, ret, t_strdup_printf(
+ "ldap_start_tls(uri=%s) failed: %s",
+ conn->set.uri, ldap_err2string(ret)));
+ return LDAP_UNAVAILABLE;
+ }
+ } else {
+ if (conn->set.debug > 0)
+ i_debug("Using TLS connection to remote LDAP server");
+ }
+ ldap_memfree(retoid);
+ }
+ conn->state = LDAP_STATE_AUTH;
+ return ldap_connect_next_message(conn, req, finished_r);
+ case LDAP_STATE_AUTH:
+ if (ret != LDAP_SUCCESS) {
+ ldap_connection_result_failure(conn, req, ret, t_strdup_printf(
+ "ldap_parse_result() failed for connect: %s",
+ ldap_err2string(ret)));
+ return ret;
+ }
+ if (result_err != LDAP_SUCCESS) {
+ const char *error = result_errmsg != NULL ?
+ result_errmsg : ldap_err2string(result_err);
+ ldap_connection_result_failure(conn, req, result_err, t_strdup_printf(
+ "Connect failed: %s", error));
+ ldap_memfree(result_errmsg);
+ return result_err;
+ }
+ if (msgtype != LDAP_RES_BIND) return 0;
+ ret = ldap_parse_sasl_bind_result(conn->conn, message, &conn->scred, 0);
+ if (ret != LDAP_SUCCESS) {
+ const char *error = t_strdup_printf(
+ "Cannot bind with server: %s", ldap_err2string(ret));
+ ldap_connection_result_failure(conn, req, ret, error);
+ return 1;
+ }
+ conn->state = LDAP_STATE_CONNECT;
+ return ldap_connect_next_message(conn, req, finished_r);
+ default:
+ i_unreached();
+ }
+ i_unreached();
+}
+
+static
+void ldap_connection_abort_request(struct ldap_op_queue_entry *req)
+{
+ struct ldap_result res;
+
+ /* too bad */
+ timeout_remove(&req->to_abort);
+ if (req->msgid > -1)
+ ldap_abandon_ext(req->conn->conn, req->msgid, NULL, NULL);
+
+ i_zero(&res);
+ res.openldap_ret = LDAP_TIMEOUT;
+ res.error_string = "Aborting LDAP request after timeout";
+ if (req->result_callback != NULL)
+ req->result_callback(&res, req->result_callback_ctx);
+
+ unsigned int n = aqueue_count(req->conn->request_queue);
+ for (unsigned int i = 0; i < n; i++) {
+ struct ldap_op_queue_entry *arr_req =
+ array_idx_elem(&req->conn->request_array,
+ aqueue_idx(req->conn->request_queue, i));
+ if (req == arr_req) {
+ aqueue_delete(req->conn->request_queue, i);
+ ldap_connection_request_destroy(&req);
+ return;
+ }
+ }
+ i_unreached();
+}
+
+static
+void ldap_connection_abort_all_requests(struct ldap_connection *conn)
+{
+ struct ldap_result res;
+ i_zero(&res);
+ res.openldap_ret = LDAP_TIMEOUT;
+ res.error_string = "Aborting LDAP requests due to failure";
+
+ unsigned int n = aqueue_count(conn->request_queue);
+ for (unsigned int i = 0; i < n; i++) {
+ struct ldap_op_queue_entry **reqp =
+ array_idx_modifiable(&conn->request_array,
+ aqueue_idx(conn->request_queue, i));
+ timeout_remove(&(*reqp)->to_abort);
+ if ((*reqp)->result_callback != NULL)
+ (*reqp)->result_callback(&res, (*reqp)->result_callback_ctx);
+ ldap_connection_request_destroy(reqp);
+ }
+ aqueue_clear(conn->request_queue);
+}
+
+static int
+ldap_connect_next_message(struct ldap_connection *conn,
+ struct ldap_op_queue_entry *req, bool *finished_r)
+{
+ int ret;
+
+ *finished_r = TRUE;
+
+ switch(conn->state) {
+ case LDAP_STATE_DISCONNECT:
+ /* if we should not disable SSL, and the URI is not ldaps:// */
+ if (!conn->set.start_tls || strstr(conn->set.uri, "ldaps://") == NULL) {
+ ret = ldap_start_tls(conn->conn, NULL, NULL, &req->msgid);
+ if (ret != LDAP_SUCCESS) {
+ ldap_connection_result_failure(conn, req, ret, t_strdup_printf(
+ "ldap_start_tls(uri=%s) failed: %s",
+ conn->set.uri, ldap_err2string(ret)));
+ return ret;
+ }
+ conn->state = LDAP_STATE_TLS;
+ break;
+ }
+ conn->state = LDAP_STATE_AUTH;
+ /* fall through */
+ case LDAP_STATE_AUTH:
+ ret = ldap_sasl_bind(conn->conn,
+ conn->set.bind_dn,
+ LDAP_SASL_SIMPLE,
+ &conn->cred,
+ NULL,
+ NULL,
+ &req->msgid);
+ if (ret != LDAP_SUCCESS) {
+ ldap_connection_result_failure(conn, req, ret, t_strdup_printf(
+ "ldap_sasl_bind(uri=%s, dn=%s) failed: %s",
+ conn->set.uri, conn->set.bind_dn, ldap_err2string(ret)));
+ return ret;
+ }
+ break;
+ case LDAP_STATE_CONNECT:
+ ldap_connection_result_success(conn, req);
+ return LDAP_SUCCESS; /* we are done here */
+ default:
+ i_unreached();
+ };
+
+ req->conn = conn;
+ *finished_r = FALSE;
+ return LDAP_SUCCESS;
+}
+
+static
+int ldap_connection_connect(struct ldap_connection *conn)
+{
+ const char *error;
+ int fd;
+ Sockbuf *sb;
+ bool finished;
+
+ if (conn->conn == NULL) {
+ /* try to reconnect after disconnection */
+ if (ldap_connection_setup(conn, &error) < 0)
+ i_error("%s", error);
+ }
+
+ pool_t pool = pool_alloconly_create(MEMPOOL_GROWING "ldap bind", 128);
+ struct ldap_op_queue_entry *req = p_new(pool, struct ldap_op_queue_entry, 1);
+ req->pool = pool;
+
+ req->internal_response_cb = ldap_connection_connect_parse;
+ req->timeout_secs = conn->set.timeout_secs;
+
+ if (ldap_connect_next_message(conn, req, &finished) != LDAP_SUCCESS ||
+ conn->conn == NULL) {
+ pool_unref(&pool);
+ return -1;
+ }
+ conn->pending++;
+ aqueue_append(conn->request_queue, &req);
+ /* start timeout */
+ if (req->timeout_secs > 0)
+ req->to_abort = timeout_add(req->timeout_secs * 1000, ldap_connection_abort_request, req);
+
+ ldap_get_option(conn->conn, LDAP_OPT_SOCKBUF, &sb);
+ ber_sockbuf_ctrl(sb, LBER_SB_OPT_GET_FD, &fd);
+ conn->io = io_add(fd, IO_READ, ldap_connection_read_more, conn);
+ if (conn->set.max_idle_time_secs > 0)
+ conn->to_disconnect = timeout_add(conn->set.max_idle_time_secs * 1000, ldap_connection_kill, conn);
+ return 0;
+}
+
+void ldap_connection_kill(struct ldap_connection *conn)
+{
+ io_remove_closed(&conn->io);
+ timeout_remove(&conn->to_disconnect);
+ timeout_remove(&conn->to_reconnect);
+ if (conn->request_queue != NULL) {
+ unsigned int n = aqueue_count(conn->request_queue);
+
+ for (unsigned int i = 0; i < n; i++) {
+ struct ldap_op_queue_entry *req =
+ array_idx_elem(&conn->request_array,
+ aqueue_idx(conn->request_queue, i));
+ if (req->msgid > -1)
+ ldap_abandon_ext(conn->conn, req->msgid, NULL, NULL);
+ req->msgid = -1;
+ }
+ }
+ if (conn->conn != NULL) {
+ ldap_unbind_ext(conn->conn, NULL, NULL);
+ ldap_memfree(conn->scred);
+ }
+ conn->conn = NULL;
+ conn->state = LDAP_STATE_DISCONNECT;
+}
+
+int ldap_connection_check(struct ldap_connection *conn)
+{
+ /* it's not connected */
+ if (conn->state == LDAP_STATE_DISCONNECT) return -1;
+ return 0;
+}
+
+static struct ldap_op_queue_entry *
+ldap_connection_find_req_by_msgid(struct ldap_connection *conn, int msgid,
+ unsigned int *idx_r)
+{
+ unsigned int i, n = aqueue_count(conn->request_queue);
+ for (i = 0; i < n; i++) {
+ struct ldap_op_queue_entry *req =
+ array_idx_elem(&conn->request_array,
+ aqueue_idx(conn->request_queue, i));
+ if (req->msgid == msgid) {
+ *idx_r = i;
+ return req;
+ }
+ }
+ return NULL;
+}
+
+static int
+ldap_connection_handle_message(struct ldap_connection *conn,
+ LDAPMessage *message)
+{
+ struct ldap_op_queue_entry *req;
+ unsigned int i = 0;
+ bool finished = FALSE;
+ int err = LDAP_SUCCESS;
+
+ /* we need to look at who it was for */
+ req = ldap_connection_find_req_by_msgid(conn, ldap_msgid(message), &i);
+ if (req != NULL)
+ err = req->internal_response_cb(conn, req, message, &finished);
+ ldap_msgfree(message);
+
+ switch(err) {
+ case LDAP_SUCCESS:
+ break;
+ case LDAP_SERVER_DOWN:
+#ifdef LDAP_CONNECT_ERROR
+ case LDAP_CONNECT_ERROR:
+#endif
+ case LDAP_UNAVAILABLE:
+ case LDAP_OPERATIONS_ERROR:
+ case LDAP_BUSY:
+ /* requeue */
+ ldap_connection_kill(conn);
+ ldap_connection_send_next(conn);
+ finished = FALSE;
+ break;
+ case LDAP_INVALID_CREDENTIALS: {
+ /* fail everything */
+ ldap_connection_kill(conn);
+ ldap_connection_abort_all_requests(conn);
+ return 0;
+ }
+ case LDAP_SIZELIMIT_EXCEEDED:
+ case LDAP_TIMELIMIT_EXCEEDED:
+ case LDAP_NO_SUCH_ATTRIBUTE:
+ case LDAP_UNDEFINED_TYPE:
+ case LDAP_INAPPROPRIATE_MATCHING:
+ case LDAP_CONSTRAINT_VIOLATION:
+ case LDAP_TYPE_OR_VALUE_EXISTS:
+ case LDAP_INVALID_SYNTAX:
+ case LDAP_NO_SUCH_OBJECT:
+ case LDAP_ALIAS_PROBLEM:
+ case LDAP_INVALID_DN_SYNTAX:
+ case LDAP_IS_LEAF:
+ case LDAP_ALIAS_DEREF_PROBLEM:
+ case LDAP_FILTER_ERROR:
+ case LDAP_LOCAL_ERROR:
+ finished = TRUE;
+ break;
+ default:
+ /* ignore */
+ break;
+ }
+
+ if (finished) {
+ i_assert(req != NULL);
+ ldap_connection_request_destroy(&req);
+ conn->pending--;
+ aqueue_delete(conn->request_queue, i);
+ return 1;
+ }
+ return 0;
+}
+
+static
+void ldap_connection_read_more(struct ldap_connection *conn)
+{
+ struct timeval tv = {
+ .tv_sec = 0,
+ .tv_usec = 0
+ };
+
+ LDAPMessage *message;
+ int ret;
+
+ /* try get a message */
+ ret = ldap_result(conn->conn, LDAP_RES_ANY, 0, &tv, &message);
+ if (ret > 0)
+ ret = ldap_connection_handle_message(conn, message);
+
+ if (ret == -1) {
+ if (ldap_get_option(conn->conn, LDAP_OPT_RESULT_CODE, &ret) != LDAP_SUCCESS)
+ i_unreached();
+ if (ret != LDAP_SERVER_DOWN)
+ i_error("ldap_result() failed: %s", ldap_err2string(ret));
+ else
+ i_error("Connection lost to LDAP server, reconnecting");
+ /* kill me */
+ ldap_connection_kill(conn);
+ } else if (ret != 0) {
+ ldap_connection_send_next(conn);
+ }
+ /* reset timeout */
+ if (conn->to_disconnect != NULL)
+ timeout_reset(conn->to_disconnect);
+}
+
+bool ldap_result_has_failed(struct ldap_result *result)
+{
+ i_assert((result->openldap_ret == LDAP_SUCCESS) == (result->error_string == NULL));
+ return result->openldap_ret != LDAP_SUCCESS;
+}
+
+const char *ldap_result_get_error(struct ldap_result *result)
+{
+ i_assert((result->openldap_ret == LDAP_SUCCESS) == (result->error_string == NULL));
+ return result->error_string;
+}
diff --git a/src/lib-ldap/ldap-entry.c b/src/lib-ldap/ldap-entry.c
new file mode 100644
index 0000000..639b931
--- /dev/null
+++ b/src/lib-ldap/ldap-entry.c
@@ -0,0 +1,72 @@
+/* Copyright (c) 2016-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "ldap-private.h"
+
+int ldap_entry_init(struct ldap_entry *obj, struct ldap_result *result,
+ LDAPMessage *message)
+{
+ ARRAY_TYPE(const_string) attr_names;
+ struct berval **values;
+ int count;
+ BerElement *bptr;
+ char *tmp;
+ tmp = ldap_get_dn(result->conn->conn, message);
+ obj->dn = p_strdup(result->pool, tmp);
+ obj->result = result;
+ ldap_memfree(tmp);
+
+ tmp = ldap_first_attribute(result->conn->conn, message, &bptr);
+
+ p_array_init(&attr_names, result->pool, 8);
+ p_array_init(&obj->attributes, result->pool, 8);
+
+ while(tmp != NULL) {
+ struct ldap_attribute *attr = p_new(result->pool, struct ldap_attribute, 1);
+ attr->name = p_strdup(result->pool, tmp);
+ array_push_back(&attr_names, &attr->name);
+ values = ldap_get_values_len(result->conn->conn, message, tmp);
+ if (values != NULL) {
+ count = ldap_count_values_len(values);
+ p_array_init(&attr->values, result->pool, count);
+ for(int i = 0; i < count; i++) {
+ const char *ptr = p_strndup(result->pool, values[i]->bv_val, values[i]->bv_len);
+ array_push_back(&attr->values, &ptr);
+ }
+ ldap_value_free_len(values);
+ }
+ array_append_zero(&attr->values);
+ ldap_memfree(tmp);
+ array_push_back(&obj->attributes, attr);
+ tmp = ldap_next_attribute(result->conn->conn, message, bptr);
+ }
+
+ ber_free(bptr, 0);
+
+ array_append_zero(&attr_names);
+ obj->attr_names = array_front(&attr_names);
+
+ return 0;
+}
+
+const char *ldap_entry_dn(const struct ldap_entry *entry)
+{
+ return entry->dn;
+}
+
+const char *const *ldap_entry_get_attributes(const struct ldap_entry *entry)
+{
+ return entry->attr_names;
+}
+
+const char *const *ldap_entry_get_attribute(const struct ldap_entry *entry, const char *attribute)
+{
+ const struct ldap_attribute *attr;
+ array_foreach(&entry->attributes, attr) {
+ if (strcasecmp(attr->name, attribute) == 0) {
+ return array_front(&attr->values);
+ }
+ }
+ return NULL;
+}
diff --git a/src/lib-ldap/ldap-iterator.c b/src/lib-ldap/ldap-iterator.c
new file mode 100644
index 0000000..d825f6c
--- /dev/null
+++ b/src/lib-ldap/ldap-iterator.c
@@ -0,0 +1,29 @@
+/* Copyright (c) 2016-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "ldap-private.h"
+
+struct ldap_search_iterator* ldap_search_iterator_init(struct ldap_result *result)
+{
+ struct ldap_search_iterator *iter;
+
+ i_assert(result->openldap_ret == LDAP_SUCCESS);
+ i_assert(result->error_string == NULL);
+
+ iter = p_new(result->pool, struct ldap_search_iterator, 1);
+ iter->result = result;
+ return iter;
+}
+
+const struct ldap_entry *ldap_search_iterator_next(struct ldap_search_iterator *iter)
+{
+ if (iter->idx >= array_count(&iter->result->entries))
+ return NULL;
+ return array_idx(&iter->result->entries, iter->idx++);
+}
+
+void ldap_search_iterator_deinit(struct ldap_search_iterator **iter)
+{
+ *iter = NULL;
+}
diff --git a/src/lib-ldap/ldap-private.h b/src/lib-ldap/ldap-private.h
new file mode 100644
index 0000000..fa724f4
--- /dev/null
+++ b/src/lib-ldap/ldap-private.h
@@ -0,0 +1,129 @@
+#ifndef LDAP_PRIVATE_H
+#define LDAP_PRIVATE_H
+
+#include "iostream-ssl.h"
+#include "ldap-client.h"
+
+#include <ldap.h>
+
+#define DOVE_LDAP_CONTINUE 0
+#define DOVE_LDAP_COMPLETE 1
+#define DOVE_LDAP_REQUEUE 2
+
+struct ldap_connection;
+struct ldap_result;
+
+struct ldap_op_queue_entry;
+/* Handle an LDAP response. Returns 0 on success, otherwise the OpenLDAP error
+ number. */
+typedef int ldap_response_callback_t(struct ldap_connection *conn,
+ struct ldap_op_queue_entry *entry,
+ LDAPMessage *msg, bool *finished_r);
+/* Send the request. Returns 0 on success, otherwise the OpenLDAP error number
+ and sets error_r string. */
+typedef int ldap_send_request_t(struct ldap_connection *conn,
+ struct ldap_op_queue_entry *entry,
+ const char **error_r);
+
+struct ldap_op_queue_entry {
+ pool_t pool;
+ struct ldap_connection *conn;
+ ldap_response_callback_t *internal_response_cb;
+ void *ctx;
+
+ int msgid;
+
+ unsigned int timeout_secs;
+ struct timeout *to_abort;
+
+ ldap_send_request_t *send_request_cb;
+
+ ldap_result_callback_t *result_callback;
+ void *result_callback_ctx;
+
+ struct {
+ struct ldap_search_input search;
+ struct ldap_compare_input compare;
+ } input;
+};
+
+struct ldap_connection {
+ pool_t pool;
+ struct ldap_client *client;
+
+ LDAP *conn;
+ enum {
+ LDAP_STATE_DISCONNECT,
+ LDAP_STATE_TLS,
+ LDAP_STATE_AUTH,
+ LDAP_STATE_CONNECT
+ } state;
+
+ BerValue cred; /* needed for SASL */
+ BerVarray scred;
+
+ struct ldap_client_settings set;
+ struct ssl_iostream_settings ssl_set;
+
+ struct aqueue *request_queue;
+ ARRAY(struct ldap_op_queue_entry *) request_array;
+
+ unsigned int sent;
+ unsigned int pending;
+
+ struct io *io;
+ struct timeout *to_disconnect;
+ struct timeout *to_reconnect;
+};
+
+struct ldap_attribute {
+ const char *name;
+ ARRAY_TYPE(const_string) values;
+};
+
+struct ldap_entry {
+ struct ldap_result *result;
+ char *dn;
+ ARRAY(struct ldap_attribute) attributes;
+ const char *const *attr_names;
+};
+
+struct ldap_result {
+ pool_t pool;
+ struct ldap_connection *conn;
+
+ ARRAY(struct ldap_entry) entries;
+ int openldap_ret;
+ bool compare_true;
+ const char *error_string;
+};
+
+struct ldap_search_iterator {
+ unsigned int idx;
+ struct ldap_result *result;
+};
+
+int ldap_connection_init(struct ldap_client *client,
+ const struct ldap_client_settings *set,
+ struct ldap_connection **conn_r, const char **error_r);
+void ldap_connection_deinit(struct ldap_connection **_conn);
+void ldap_connection_switch_ioloop(struct ldap_connection *conn);
+bool ldap_connection_have_settings(struct ldap_connection *conn,
+ const struct ldap_client_settings *set);
+
+void ldap_connection_search_start(struct ldap_connection *conn,
+ const struct ldap_search_input *input,
+ ldap_result_callback_t *callback,
+ void *context);
+void ldap_connection_compare_start(struct ldap_connection *conn,
+ const struct ldap_compare_input *input,
+ ldap_result_callback_t *callback,
+ void *context);
+
+void ldap_connection_kill(struct ldap_connection *conn);
+int ldap_connection_check(struct ldap_connection *conn);
+void ldap_connection_queue_request(struct ldap_connection *conn, struct ldap_op_queue_entry *req);
+
+int ldap_entry_init(struct ldap_entry *obj, struct ldap_result *result, LDAPMessage *message);
+
+#endif
diff --git a/src/lib-ldap/ldap-search.c b/src/lib-ldap/ldap-search.c
new file mode 100644
index 0000000..694fd6d
--- /dev/null
+++ b/src/lib-ldap/ldap-search.c
@@ -0,0 +1,169 @@
+/* Copyright (c) 2016-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "ldap-private.h"
+
+#include <stdio.h>
+#include <sys/time.h>
+
+struct ldap_search_ctx {
+ const struct ldap_search_input *input;
+ struct ldap_result res;
+};
+
+static void
+ldap_search_result_failure(struct ldap_op_queue_entry *req,
+ int ret, const char *error)
+{
+ struct ldap_search_ctx *sctx = req->ctx;
+ sctx->res.openldap_ret = ret;
+ sctx->res.error_string = error;
+ req->result_callback(&sctx->res, req->result_callback_ctx);
+}
+
+static void ldap_search_result_success(struct ldap_op_queue_entry *req)
+{
+ struct ldap_search_ctx *sctx = req->ctx;
+ sctx->res.openldap_ret = LDAP_SUCCESS;
+ req->result_callback(&sctx->res, req->result_callback_ctx);
+}
+
+static int
+ldap_search_callback(struct ldap_connection *conn,
+ struct ldap_op_queue_entry *req,
+ LDAPMessage *message, bool *finished_r)
+{
+ struct ldap_search_ctx *sctx = req->ctx;
+ int msgtype = ldap_msgtype(message);
+ char *result_errmsg = NULL;
+ int ret, result_err;
+
+ if (msgtype != LDAP_RES_SEARCH_ENTRY &&
+ msgtype != LDAP_RES_SEARCH_RESULT) {
+ *finished_r = FALSE;
+ return LDAP_SUCCESS;
+ }
+ *finished_r = TRUE;
+
+ ret = ldap_parse_result(conn->conn, message, &result_err, NULL,
+ &result_errmsg, NULL, NULL, 0);
+ if (ret == LDAP_NO_RESULTS_RETURNED) {
+ /*ret = LDAP_SUCCESS;*/
+ } else if (ret != LDAP_SUCCESS) {
+ ldap_search_result_failure(req, ret, t_strdup_printf(
+ "ldap_parse_result() failed for search: %s", ldap_err2string(ret)));
+ return ret;
+ } else if (result_err != LDAP_SUCCESS) {
+ const struct ldap_search_input *input = &req->input.search;
+ const char *error = result_errmsg != NULL ?
+ result_errmsg : ldap_err2string(result_err);
+ ldap_search_result_failure(req, result_err, t_strdup_printf(
+ "ldap_search_ext(base=%s, scope=%d, filter=%s) failed: %s",
+ input->base_dn, input->scope, input->filter, error));
+ ldap_memfree(result_errmsg);
+ return result_err;
+ }
+
+ LDAPMessage *res = ldap_first_entry(conn->conn, message);
+
+ while(res != NULL) {
+ struct ldap_entry *obj = p_new(req->pool, struct ldap_entry, 1);
+ ldap_entry_init(obj, &sctx->res, message);
+ array_push_back(&sctx->res.entries, obj);
+ res = ldap_next_entry(conn->conn, res);
+ }
+
+ if (msgtype == LDAP_RES_SEARCH_RESULT) {
+ ldap_search_result_success(req);
+ return LDAP_SUCCESS;
+ }
+
+ *finished_r = FALSE;
+ return LDAP_SUCCESS;
+}
+
+static int
+ldap_search_send(struct ldap_connection *conn, struct ldap_op_queue_entry *req,
+ const char **error_r)
+{
+ const struct ldap_search_input *input = &req->input.search;
+ LDAPControl manageDSAIT = {
+ LDAP_CONTROL_MANAGEDSAIT, {0, 0}, 0
+ };
+ /* try to use ManageDSAIT if available */
+ LDAPControl *sctrls[] = {
+ &manageDSAIT,
+ NULL
+ };
+
+ struct timeval tv = {
+ .tv_sec = req->timeout_secs,
+ .tv_usec = 0
+ };
+
+ int ret = ldap_search_ext(conn->conn,
+ input->base_dn,
+ input->scope,
+ input->filter,
+ (char**)input->attributes,
+ 0,
+ sctrls,
+ NULL,
+ &tv,
+ input->size_limit,
+ &req->msgid);
+
+ if (ret != LDAP_SUCCESS) {
+ *error_r = t_strdup_printf(
+ "ldap_search_ext(base=%s, scope=%d, filter=%s) failed: %s",
+ input->base_dn, input->scope, input->filter,
+ ldap_err2string(ret));
+ }
+ return ret;
+}
+
+void ldap_connection_search_start(struct ldap_connection *conn,
+ const struct ldap_search_input *input,
+ ldap_result_callback_t *callback,
+ void *context)
+{
+ struct ldap_op_queue_entry *req;
+ pool_t pool = pool_alloconly_create(MEMPOOL_GROWING "ldap search", 128);
+ req = p_new(pool, struct ldap_op_queue_entry, 1);
+ req->pool = pool;
+
+ struct ldap_search_ctx *sctx = p_new(pool, struct ldap_search_ctx, 1);
+ sctx->res.conn = conn;
+ sctx->res.pool = pool;
+
+ p_array_init(&sctx->res.entries, req->pool, 8);
+
+ req->internal_response_cb = ldap_search_callback;
+
+ req->result_callback = callback;
+ req->result_callback_ctx = context;
+ req->input.search = *input;
+
+ /* copy strings */
+ req->input.search.base_dn = p_strdup(req->pool, input->base_dn);
+ req->input.search.filter = p_strdup(req->pool, input->filter);
+
+ if (input->attributes != NULL) {
+ ARRAY_TYPE(const_string) arr;
+ p_array_init(&arr, req->pool, 8);
+ for (const char *const *ptr = input->attributes; *ptr != NULL; ptr++) {
+ const char *tmp = p_strdup(req->pool, *ptr);
+ array_push_back(&arr, &tmp);
+ }
+ array_append_zero(&arr);
+ req->input.search.attributes = array_front_modifiable(&arr);
+ }
+
+ req->send_request_cb = ldap_search_send;
+ sctx->input = &req->input.search;
+ req->ctx = sctx;
+ req->timeout_secs = input->timeout_secs;
+
+ ldap_connection_queue_request(conn, req);
+}
diff --git a/src/lib-lua/Makefile.am b/src/lib-lua/Makefile.am
new file mode 100644
index 0000000..3416489
--- /dev/null
+++ b/src/lib-lua/Makefile.am
@@ -0,0 +1,68 @@
+AM_CPPFLAGS = \
+ -I$(top_srcdir)/src/lib \
+ -I$(top_srcdir)/src/lib-test \
+ -I$(top_srcdir)/src/lib-dict \
+ -I$(top_srcdir)/src/lib-http \
+ -I$(top_srcdir)/src/lib-ssl-iostream \
+ -I$(top_srcdir)/src/lib-master \
+ $(LUA_CFLAGS)
+
+pkglib_LTLIBRARIES = libdovecot-lua.la
+libdovecot_lua_la_SOURCES = \
+ dlua-script.c \
+ dlua-pushstring.c \
+ dlua-error.c \
+ dlua-dovecot.c \
+ dlua-dovecot-http.c \
+ dlua-compat.c \
+ dlua-resume.c \
+ dlua-table.c \
+ dlua-thread.c
+
+test_programs = test-lua
+
+LIBDICT_LUA=
+if DLUA_WITH_YIELDS
+LIBDICT_LUA += ../lib-dict/libdict_lua.la
+test_programs += test-dict-lua
+endif
+
+# Note: the only things this lib should depend on are libdovecot and lua.
+libdovecot_lua_la_DEPENDENCIES = \
+ ../lib-dovecot/libdovecot.la \
+ $(LIBDICT_LUA)
+libdovecot_lua_la_LIBADD = \
+ ../lib-dovecot/libdovecot.la \
+ $(LIBDICT_LUA) \
+ $(LUA_LIBS)
+libdovecot_lua_la_LDFLAGS = -export-dynamic
+
+headers = \
+ dlua-compat.h \
+ dlua-script.h \
+ dlua-script-private.h \
+ dlua-wrapper.h
+
+pkginc_libdir=$(pkgincludedir)
+pkginc_lib_HEADERS = $(headers)
+
+noinst_PROGRAMS = $(test_programs)
+
+test_libs =\
+ libdovecot-lua.la \
+ ../lib-dovecot/libdovecot.la
+
+test_lua_SOURCES = test-lua.c
+test_lua_CFLAGS = $(AM_CPPFLAGS) $(BINARY_CFLAGS)
+test_lua_LDFLAGS = $(BINARY_LDFLAGS)
+test_lua_LDADD = $(test_libs) $(LUA_LIBS)
+test_lua_DEPENDENCIES = $(test_libs)
+
+test_dict_lua_SOURCES = test-dict-lua.c
+test_dict_lua_LDADD = $(test_libs) $(LUA_LIBS)
+test_dict_lua_DEPENDENCIES = $(test_libs)
+
+check-local:
+ for bin in $(test_programs); do \
+ if ! $(RUN_TEST) ./$$bin; then exit 1; fi; \
+ done
diff --git a/src/lib-lua/Makefile.in b/src/lib-lua/Makefile.in
new file mode 100644
index 0000000..687319a
--- /dev/null
+++ b/src/lib-lua/Makefile.in
@@ -0,0 +1,971 @@
+# Makefile.in generated by automake 1.16.1 from Makefile.am.
+# @configure_input@
+
+# Copyright (C) 1994-2018 Free Software Foundation, Inc.
+
+# This Makefile.in is free software; the Free Software Foundation
+# gives unlimited permission to copy and/or distribute it,
+# with or without modifications, as long as this notice is preserved.
+
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY, to the extent permitted by law; without
+# even the implied warranty of MERCHANTABILITY or FITNESS FOR A
+# PARTICULAR PURPOSE.
+
+@SET_MAKE@
+
+
+
+VPATH = @srcdir@
+am__is_gnu_make = { \
+ if test -z '$(MAKELEVEL)'; then \
+ false; \
+ elif test -n '$(MAKE_HOST)'; then \
+ true; \
+ elif test -n '$(MAKE_VERSION)' && test -n '$(CURDIR)'; then \
+ true; \
+ else \
+ false; \
+ fi; \
+}
+am__make_running_with_option = \
+ case $${target_option-} in \
+ ?) ;; \
+ *) echo "am__make_running_with_option: internal error: invalid" \
+ "target option '$${target_option-}' specified" >&2; \
+ exit 1;; \
+ esac; \
+ has_opt=no; \
+ sane_makeflags=$$MAKEFLAGS; \
+ if $(am__is_gnu_make); then \
+ sane_makeflags=$$MFLAGS; \
+ else \
+ case $$MAKEFLAGS in \
+ *\\[\ \ ]*) \
+ bs=\\; \
+ sane_makeflags=`printf '%s\n' "$$MAKEFLAGS" \
+ | sed "s/$$bs$$bs[$$bs $$bs ]*//g"`;; \
+ esac; \
+ fi; \
+ skip_next=no; \
+ strip_trailopt () \
+ { \
+ flg=`printf '%s\n' "$$flg" | sed "s/$$1.*$$//"`; \
+ }; \
+ for flg in $$sane_makeflags; do \
+ test $$skip_next = yes && { skip_next=no; continue; }; \
+ case $$flg in \
+ *=*|--*) continue;; \
+ -*I) strip_trailopt 'I'; skip_next=yes;; \
+ -*I?*) strip_trailopt 'I';; \
+ -*O) strip_trailopt 'O'; skip_next=yes;; \
+ -*O?*) strip_trailopt 'O';; \
+ -*l) strip_trailopt 'l'; skip_next=yes;; \
+ -*l?*) strip_trailopt 'l';; \
+ -[dEDm]) skip_next=yes;; \
+ -[JT]) skip_next=yes;; \
+ esac; \
+ case $$flg in \
+ *$$target_option*) has_opt=yes; break;; \
+ esac; \
+ done; \
+ test $$has_opt = yes
+am__make_dryrun = (target_option=n; $(am__make_running_with_option))
+am__make_keepgoing = (target_option=k; $(am__make_running_with_option))
+pkgdatadir = $(datadir)/@PACKAGE@
+pkgincludedir = $(includedir)/@PACKAGE@
+pkglibdir = $(libdir)/@PACKAGE@
+pkglibexecdir = $(libexecdir)/@PACKAGE@
+am__cd = CDPATH="$${ZSH_VERSION+.}$(PATH_SEPARATOR)" && cd
+install_sh_DATA = $(install_sh) -c -m 644
+install_sh_PROGRAM = $(install_sh) -c
+install_sh_SCRIPT = $(install_sh) -c
+INSTALL_HEADER = $(INSTALL_DATA)
+transform = $(program_transform_name)
+NORMAL_INSTALL = :
+PRE_INSTALL = :
+POST_INSTALL = :
+NORMAL_UNINSTALL = :
+PRE_UNINSTALL = :
+POST_UNINSTALL = :
+build_triplet = @build@
+host_triplet = @host@
+@DLUA_WITH_YIELDS_TRUE@am__append_1 = ../lib-dict/libdict_lua.la
+@DLUA_WITH_YIELDS_TRUE@am__append_2 = test-dict-lua
+noinst_PROGRAMS = $(am__EXEEXT_2)
+subdir = src/lib-lua
+ACLOCAL_M4 = $(top_srcdir)/aclocal.m4
+am__aclocal_m4_deps = $(top_srcdir)/m4/ac_checktype2.m4 \
+ $(top_srcdir)/m4/ac_typeof.m4 $(top_srcdir)/m4/arc4random.m4 \
+ $(top_srcdir)/m4/blockdev.m4 $(top_srcdir)/m4/c99_vsnprintf.m4 \
+ $(top_srcdir)/m4/clock_gettime.m4 $(top_srcdir)/m4/crypt.m4 \
+ $(top_srcdir)/m4/crypt_xpg6.m4 $(top_srcdir)/m4/dbqlk.m4 \
+ $(top_srcdir)/m4/dirent_dtype.m4 $(top_srcdir)/m4/dovecot.m4 \
+ $(top_srcdir)/m4/fd_passing.m4 $(top_srcdir)/m4/fdatasync.m4 \
+ $(top_srcdir)/m4/flexible_array_member.m4 \
+ $(top_srcdir)/m4/glibc.m4 $(top_srcdir)/m4/gmtime_max.m4 \
+ $(top_srcdir)/m4/gmtime_tm_gmtoff.m4 \
+ $(top_srcdir)/m4/ioloop.m4 $(top_srcdir)/m4/iovec.m4 \
+ $(top_srcdir)/m4/ipv6.m4 $(top_srcdir)/m4/libcap.m4 \
+ $(top_srcdir)/m4/libtool.m4 $(top_srcdir)/m4/libwrap.m4 \
+ $(top_srcdir)/m4/linux_mremap.m4 $(top_srcdir)/m4/ltoptions.m4 \
+ $(top_srcdir)/m4/ltsugar.m4 $(top_srcdir)/m4/ltversion.m4 \
+ $(top_srcdir)/m4/lt~obsolete.m4 $(top_srcdir)/m4/mmap_write.m4 \
+ $(top_srcdir)/m4/mntctl.m4 $(top_srcdir)/m4/modules.m4 \
+ $(top_srcdir)/m4/notify.m4 $(top_srcdir)/m4/nsl.m4 \
+ $(top_srcdir)/m4/off_t_max.m4 $(top_srcdir)/m4/pkg.m4 \
+ $(top_srcdir)/m4/pr_set_dumpable.m4 \
+ $(top_srcdir)/m4/q_quotactl.m4 $(top_srcdir)/m4/quota.m4 \
+ $(top_srcdir)/m4/random.m4 $(top_srcdir)/m4/rlimit.m4 \
+ $(top_srcdir)/m4/sendfile.m4 $(top_srcdir)/m4/size_t_signed.m4 \
+ $(top_srcdir)/m4/sockpeercred.m4 $(top_srcdir)/m4/sql.m4 \
+ $(top_srcdir)/m4/ssl.m4 $(top_srcdir)/m4/st_tim.m4 \
+ $(top_srcdir)/m4/static_array.m4 $(top_srcdir)/m4/test_with.m4 \
+ $(top_srcdir)/m4/time_t.m4 $(top_srcdir)/m4/typeof.m4 \
+ $(top_srcdir)/m4/typeof_dev_t.m4 \
+ $(top_srcdir)/m4/uoff_t_max.m4 $(top_srcdir)/m4/vararg.m4 \
+ $(top_srcdir)/m4/want_apparmor.m4 \
+ $(top_srcdir)/m4/want_bsdauth.m4 \
+ $(top_srcdir)/m4/want_bzlib.m4 \
+ $(top_srcdir)/m4/want_cassandra.m4 \
+ $(top_srcdir)/m4/want_cdb.m4 \
+ $(top_srcdir)/m4/want_checkpassword.m4 \
+ $(top_srcdir)/m4/want_clucene.m4 $(top_srcdir)/m4/want_db.m4 \
+ $(top_srcdir)/m4/want_gssapi.m4 $(top_srcdir)/m4/want_icu.m4 \
+ $(top_srcdir)/m4/want_ldap.m4 $(top_srcdir)/m4/want_lua.m4 \
+ $(top_srcdir)/m4/want_lz4.m4 $(top_srcdir)/m4/want_lzma.m4 \
+ $(top_srcdir)/m4/want_mysql.m4 $(top_srcdir)/m4/want_pam.m4 \
+ $(top_srcdir)/m4/want_passwd.m4 $(top_srcdir)/m4/want_pgsql.m4 \
+ $(top_srcdir)/m4/want_prefetch.m4 \
+ $(top_srcdir)/m4/want_shadow.m4 \
+ $(top_srcdir)/m4/want_sodium.m4 $(top_srcdir)/m4/want_solr.m4 \
+ $(top_srcdir)/m4/want_sqlite.m4 \
+ $(top_srcdir)/m4/want_stemmer.m4 \
+ $(top_srcdir)/m4/want_systemd.m4 \
+ $(top_srcdir)/m4/want_textcat.m4 \
+ $(top_srcdir)/m4/want_unwind.m4 $(top_srcdir)/m4/want_zlib.m4 \
+ $(top_srcdir)/m4/want_zstd.m4 $(top_srcdir)/configure.ac
+am__configure_deps = $(am__aclocal_m4_deps) $(CONFIGURE_DEPENDENCIES) \
+ $(ACLOCAL_M4)
+DIST_COMMON = $(srcdir)/Makefile.am $(pkginc_lib_HEADERS) \
+ $(am__DIST_COMMON)
+mkinstalldirs = $(install_sh) -d
+CONFIG_HEADER = $(top_builddir)/config.h
+CONFIG_CLEAN_FILES =
+CONFIG_CLEAN_VPATH_FILES =
+@DLUA_WITH_YIELDS_TRUE@am__EXEEXT_1 = test-dict-lua$(EXEEXT)
+am__EXEEXT_2 = test-lua$(EXEEXT) $(am__EXEEXT_1)
+PROGRAMS = $(noinst_PROGRAMS)
+am__vpath_adj_setup = srcdirstrip=`echo "$(srcdir)" | sed 's|.|.|g'`;
+am__vpath_adj = case $$p in \
+ $(srcdir)/*) f=`echo "$$p" | sed "s|^$$srcdirstrip/||"`;; \
+ *) f=$$p;; \
+ esac;
+am__strip_dir = f=`echo $$p | sed -e 's|^.*/||'`;
+am__install_max = 40
+am__nobase_strip_setup = \
+ srcdirstrip=`echo "$(srcdir)" | sed 's/[].[^$$\\*|]/\\\\&/g'`
+am__nobase_strip = \
+ for p in $$list; do echo "$$p"; done | sed -e "s|$$srcdirstrip/||"
+am__nobase_list = $(am__nobase_strip_setup); \
+ for p in $$list; do echo "$$p $$p"; done | \
+ sed "s| $$srcdirstrip/| |;"' / .*\//!s/ .*/ ./; s,\( .*\)/[^/]*$$,\1,' | \
+ $(AWK) 'BEGIN { files["."] = "" } { files[$$2] = files[$$2] " " $$1; \
+ if (++n[$$2] == $(am__install_max)) \
+ { print $$2, files[$$2]; n[$$2] = 0; files[$$2] = "" } } \
+ END { for (dir in files) print dir, files[dir] }'
+am__base_list = \
+ sed '$$!N;$$!N;$$!N;$$!N;$$!N;$$!N;$$!N;s/\n/ /g' | \
+ sed '$$!N;$$!N;$$!N;$$!N;s/\n/ /g'
+am__uninstall_files_from_dir = { \
+ test -z "$$files" \
+ || { test ! -d "$$dir" && test ! -f "$$dir" && test ! -r "$$dir"; } \
+ || { echo " ( cd '$$dir' && rm -f" $$files ")"; \
+ $(am__cd) "$$dir" && rm -f $$files; }; \
+ }
+am__installdirs = "$(DESTDIR)$(pkglibdir)" \
+ "$(DESTDIR)$(pkginc_libdir)"
+LTLIBRARIES = $(pkglib_LTLIBRARIES)
+am__DEPENDENCIES_1 =
+am_libdovecot_lua_la_OBJECTS = dlua-script.lo dlua-pushstring.lo \
+ dlua-error.lo dlua-dovecot.lo dlua-dovecot-http.lo \
+ dlua-compat.lo dlua-resume.lo dlua-table.lo dlua-thread.lo
+libdovecot_lua_la_OBJECTS = $(am_libdovecot_lua_la_OBJECTS)
+AM_V_lt = $(am__v_lt_@AM_V@)
+am__v_lt_ = $(am__v_lt_@AM_DEFAULT_V@)
+am__v_lt_0 = --silent
+am__v_lt_1 =
+libdovecot_lua_la_LINK = $(LIBTOOL) $(AM_V_lt) --tag=CC \
+ $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=link $(CCLD) \
+ $(AM_CFLAGS) $(CFLAGS) $(libdovecot_lua_la_LDFLAGS) $(LDFLAGS) \
+ -o $@
+am_test_dict_lua_OBJECTS = test-dict-lua.$(OBJEXT)
+test_dict_lua_OBJECTS = $(am_test_dict_lua_OBJECTS)
+am_test_lua_OBJECTS = test_lua-test-lua.$(OBJEXT)
+test_lua_OBJECTS = $(am_test_lua_OBJECTS)
+test_lua_LINK = $(LIBTOOL) $(AM_V_lt) --tag=CC $(AM_LIBTOOLFLAGS) \
+ $(LIBTOOLFLAGS) --mode=link $(CCLD) $(test_lua_CFLAGS) \
+ $(CFLAGS) $(test_lua_LDFLAGS) $(LDFLAGS) -o $@
+AM_V_P = $(am__v_P_@AM_V@)
+am__v_P_ = $(am__v_P_@AM_DEFAULT_V@)
+am__v_P_0 = false
+am__v_P_1 = :
+AM_V_GEN = $(am__v_GEN_@AM_V@)
+am__v_GEN_ = $(am__v_GEN_@AM_DEFAULT_V@)
+am__v_GEN_0 = @echo " GEN " $@;
+am__v_GEN_1 =
+AM_V_at = $(am__v_at_@AM_V@)
+am__v_at_ = $(am__v_at_@AM_DEFAULT_V@)
+am__v_at_0 = @
+am__v_at_1 =
+DEFAULT_INCLUDES = -I.@am__isrc@ -I$(top_builddir)
+depcomp = $(SHELL) $(top_srcdir)/depcomp
+am__maybe_remake_depfiles = depfiles
+am__depfiles_remade = ./$(DEPDIR)/dlua-compat.Plo \
+ ./$(DEPDIR)/dlua-dovecot-http.Plo ./$(DEPDIR)/dlua-dovecot.Plo \
+ ./$(DEPDIR)/dlua-error.Plo ./$(DEPDIR)/dlua-pushstring.Plo \
+ ./$(DEPDIR)/dlua-resume.Plo ./$(DEPDIR)/dlua-script.Plo \
+ ./$(DEPDIR)/dlua-table.Plo ./$(DEPDIR)/dlua-thread.Plo \
+ ./$(DEPDIR)/test-dict-lua.Po ./$(DEPDIR)/test_lua-test-lua.Po
+am__mv = mv -f
+COMPILE = $(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) \
+ $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS)
+LTCOMPILE = $(LIBTOOL) $(AM_V_lt) --tag=CC $(AM_LIBTOOLFLAGS) \
+ $(LIBTOOLFLAGS) --mode=compile $(CC) $(DEFS) \
+ $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) $(CPPFLAGS) \
+ $(AM_CFLAGS) $(CFLAGS)
+AM_V_CC = $(am__v_CC_@AM_V@)
+am__v_CC_ = $(am__v_CC_@AM_DEFAULT_V@)
+am__v_CC_0 = @echo " CC " $@;
+am__v_CC_1 =
+CCLD = $(CC)
+LINK = $(LIBTOOL) $(AM_V_lt) --tag=CC $(AM_LIBTOOLFLAGS) \
+ $(LIBTOOLFLAGS) --mode=link $(CCLD) $(AM_CFLAGS) $(CFLAGS) \
+ $(AM_LDFLAGS) $(LDFLAGS) -o $@
+AM_V_CCLD = $(am__v_CCLD_@AM_V@)
+am__v_CCLD_ = $(am__v_CCLD_@AM_DEFAULT_V@)
+am__v_CCLD_0 = @echo " CCLD " $@;
+am__v_CCLD_1 =
+SOURCES = $(libdovecot_lua_la_SOURCES) $(test_dict_lua_SOURCES) \
+ $(test_lua_SOURCES)
+DIST_SOURCES = $(libdovecot_lua_la_SOURCES) $(test_dict_lua_SOURCES) \
+ $(test_lua_SOURCES)
+am__can_run_installinfo = \
+ case $$AM_UPDATE_INFO_DIR in \
+ n|no|NO) false;; \
+ *) (install-info --version) >/dev/null 2>&1;; \
+ esac
+HEADERS = $(pkginc_lib_HEADERS)
+am__tagged_files = $(HEADERS) $(SOURCES) $(TAGS_FILES) $(LISP)
+# Read a list of newline-separated strings from the standard input,
+# and print each of them once, without duplicates. Input order is
+# *not* preserved.
+am__uniquify_input = $(AWK) '\
+ BEGIN { nonempty = 0; } \
+ { items[$$0] = 1; nonempty = 1; } \
+ END { if (nonempty) { for (i in items) print i; }; } \
+'
+# Make sure the list of sources is unique. This is necessary because,
+# e.g., the same source file might be shared among _SOURCES variables
+# for different programs/libraries.
+am__define_uniq_tagged_files = \
+ list='$(am__tagged_files)'; \
+ unique=`for i in $$list; do \
+ if test -f "$$i"; then echo $$i; else echo $(srcdir)/$$i; fi; \
+ done | $(am__uniquify_input)`
+ETAGS = etags
+CTAGS = ctags
+am__DIST_COMMON = $(srcdir)/Makefile.in $(top_srcdir)/depcomp
+DISTFILES = $(DIST_COMMON) $(DIST_SOURCES) $(TEXINFOS) $(EXTRA_DIST)
+ACLOCAL = @ACLOCAL@
+ACLOCAL_AMFLAGS = @ACLOCAL_AMFLAGS@
+AMTAR = @AMTAR@
+AM_DEFAULT_VERBOSITY = @AM_DEFAULT_VERBOSITY@
+APPARMOR_LIBS = @APPARMOR_LIBS@
+AR = @AR@
+AUTH_CFLAGS = @AUTH_CFLAGS@
+AUTH_LIBS = @AUTH_LIBS@
+AUTOCONF = @AUTOCONF@
+AUTOHEADER = @AUTOHEADER@
+AUTOMAKE = @AUTOMAKE@
+AWK = @AWK@
+BINARY_CFLAGS = @BINARY_CFLAGS@
+BINARY_LDFLAGS = @BINARY_LDFLAGS@
+BISON = @BISON@
+CASSANDRA_CFLAGS = @CASSANDRA_CFLAGS@
+CASSANDRA_LIBS = @CASSANDRA_LIBS@
+CC = @CC@
+CCDEPMODE = @CCDEPMODE@
+CDB_LIBS = @CDB_LIBS@
+CFLAGS = @CFLAGS@
+CLUCENE_CFLAGS = @CLUCENE_CFLAGS@
+CLUCENE_LIBS = @CLUCENE_LIBS@
+COMPRESS_LIBS = @COMPRESS_LIBS@
+CPP = @CPP@
+CPPFLAGS = @CPPFLAGS@
+CRYPT_LIBS = @CRYPT_LIBS@
+CXX = @CXX@
+CXXCPP = @CXXCPP@
+CXXDEPMODE = @CXXDEPMODE@
+CXXFLAGS = @CXXFLAGS@
+CYGPATH_W = @CYGPATH_W@
+DEFS = @DEFS@
+DEPDIR = @DEPDIR@
+DICT_LIBS = @DICT_LIBS@
+DLLIB = @DLLIB@
+DLLTOOL = @DLLTOOL@
+DSYMUTIL = @DSYMUTIL@
+DUMPBIN = @DUMPBIN@
+ECHO_C = @ECHO_C@
+ECHO_N = @ECHO_N@
+ECHO_T = @ECHO_T@
+EGREP = @EGREP@
+EXEEXT = @EXEEXT@
+FGREP = @FGREP@
+FLEX = @FLEX@
+FUZZER_CPPFLAGS = @FUZZER_CPPFLAGS@
+FUZZER_LDFLAGS = @FUZZER_LDFLAGS@
+GREP = @GREP@
+INSTALL = @INSTALL@
+INSTALL_DATA = @INSTALL_DATA@
+INSTALL_PROGRAM = @INSTALL_PROGRAM@
+INSTALL_SCRIPT = @INSTALL_SCRIPT@
+INSTALL_STRIP_PROGRAM = @INSTALL_STRIP_PROGRAM@
+KRB5CONFIG = @KRB5CONFIG@
+KRB5_CFLAGS = @KRB5_CFLAGS@
+KRB5_LIBS = @KRB5_LIBS@
+LD = @LD@
+LDAP_LIBS = @LDAP_LIBS@
+LDFLAGS = @LDFLAGS@
+LD_NO_WHOLE_ARCHIVE = @LD_NO_WHOLE_ARCHIVE@
+LD_WHOLE_ARCHIVE = @LD_WHOLE_ARCHIVE@
+LIBCAP = @LIBCAP@
+LIBDOVECOT = @LIBDOVECOT@
+LIBDOVECOT_COMPRESS = @LIBDOVECOT_COMPRESS@
+LIBDOVECOT_DEPS = @LIBDOVECOT_DEPS@
+LIBDOVECOT_DSYNC = @LIBDOVECOT_DSYNC@
+LIBDOVECOT_LA_LIBS = @LIBDOVECOT_LA_LIBS@
+LIBDOVECOT_LDA = @LIBDOVECOT_LDA@
+LIBDOVECOT_LDAP = @LIBDOVECOT_LDAP@
+LIBDOVECOT_LIBFTS = @LIBDOVECOT_LIBFTS@
+LIBDOVECOT_LIBFTS_DEPS = @LIBDOVECOT_LIBFTS_DEPS@
+LIBDOVECOT_LOGIN = @LIBDOVECOT_LOGIN@
+LIBDOVECOT_LUA = @LIBDOVECOT_LUA@
+LIBDOVECOT_LUA_DEPS = @LIBDOVECOT_LUA_DEPS@
+LIBDOVECOT_SQL = @LIBDOVECOT_SQL@
+LIBDOVECOT_STORAGE = @LIBDOVECOT_STORAGE@
+LIBDOVECOT_STORAGE_DEPS = @LIBDOVECOT_STORAGE_DEPS@
+LIBEXTTEXTCAT_CFLAGS = @LIBEXTTEXTCAT_CFLAGS@
+LIBEXTTEXTCAT_LIBS = @LIBEXTTEXTCAT_LIBS@
+LIBICONV = @LIBICONV@
+LIBICU_CFLAGS = @LIBICU_CFLAGS@
+LIBICU_LIBS = @LIBICU_LIBS@
+LIBOBJS = @LIBOBJS@
+LIBS = @LIBS@
+LIBSODIUM_CFLAGS = @LIBSODIUM_CFLAGS@
+LIBSODIUM_LIBS = @LIBSODIUM_LIBS@
+LIBTIRPC_CFLAGS = @LIBTIRPC_CFLAGS@
+LIBTIRPC_LIBS = @LIBTIRPC_LIBS@
+LIBTOOL = @LIBTOOL@
+LIBUNWIND_CFLAGS = @LIBUNWIND_CFLAGS@
+LIBUNWIND_LIBS = @LIBUNWIND_LIBS@
+LIBWRAP_LIBS = @LIBWRAP_LIBS@
+LINKED_STORAGE_LDADD = @LINKED_STORAGE_LDADD@
+LIPO = @LIPO@
+LN_S = @LN_S@
+LTLIBICONV = @LTLIBICONV@
+LTLIBOBJS = @LTLIBOBJS@
+LT_SYS_LIBRARY_PATH = @LT_SYS_LIBRARY_PATH@
+LUA_CFLAGS = @LUA_CFLAGS@
+LUA_LIBS = @LUA_LIBS@
+MAINT = @MAINT@
+MAKEINFO = @MAKEINFO@
+MANIFEST_TOOL = @MANIFEST_TOOL@
+MKDIR_P = @MKDIR_P@
+MODULE_LIBS = @MODULE_LIBS@
+MODULE_SUFFIX = @MODULE_SUFFIX@
+MYSQL_CFLAGS = @MYSQL_CFLAGS@
+MYSQL_CONFIG = @MYSQL_CONFIG@
+MYSQL_LIBS = @MYSQL_LIBS@
+NM = @NM@
+NMEDIT = @NMEDIT@
+NOPLUGIN_LDFLAGS = @NOPLUGIN_LDFLAGS@
+OBJDUMP = @OBJDUMP@
+OBJEXT = @OBJEXT@
+OTOOL = @OTOOL@
+OTOOL64 = @OTOOL64@
+PACKAGE = @PACKAGE@
+PACKAGE_BUGREPORT = @PACKAGE_BUGREPORT@
+PACKAGE_NAME = @PACKAGE_NAME@
+PACKAGE_STRING = @PACKAGE_STRING@
+PACKAGE_TARNAME = @PACKAGE_TARNAME@
+PACKAGE_URL = @PACKAGE_URL@
+PACKAGE_VERSION = @PACKAGE_VERSION@
+PANDOC = @PANDOC@
+PATH_SEPARATOR = @PATH_SEPARATOR@
+PGSQL_CFLAGS = @PGSQL_CFLAGS@
+PGSQL_LIBS = @PGSQL_LIBS@
+PG_CONFIG = @PG_CONFIG@
+PIE_CFLAGS = @PIE_CFLAGS@
+PIE_LDFLAGS = @PIE_LDFLAGS@
+PKG_CONFIG = @PKG_CONFIG@
+PKG_CONFIG_LIBDIR = @PKG_CONFIG_LIBDIR@
+PKG_CONFIG_PATH = @PKG_CONFIG_PATH@
+QUOTA_LIBS = @QUOTA_LIBS@
+RANLIB = @RANLIB@
+RELRO_LDFLAGS = @RELRO_LDFLAGS@
+RPCGEN = @RPCGEN@
+RUN_TEST = @RUN_TEST@
+SED = @SED@
+SETTING_FILES = @SETTING_FILES@
+SET_MAKE = @SET_MAKE@
+SHELL = @SHELL@
+SQLITE_CFLAGS = @SQLITE_CFLAGS@
+SQLITE_LIBS = @SQLITE_LIBS@
+SQL_CFLAGS = @SQL_CFLAGS@
+SQL_LIBS = @SQL_LIBS@
+SSL_CFLAGS = @SSL_CFLAGS@
+SSL_LIBS = @SSL_LIBS@
+STRIP = @STRIP@
+SYSTEMD_CFLAGS = @SYSTEMD_CFLAGS@
+SYSTEMD_LIBS = @SYSTEMD_LIBS@
+VALGRIND = @VALGRIND@
+VERSION = @VERSION@
+ZSTD_CFLAGS = @ZSTD_CFLAGS@
+ZSTD_LIBS = @ZSTD_LIBS@
+abs_builddir = @abs_builddir@
+abs_srcdir = @abs_srcdir@
+abs_top_builddir = @abs_top_builddir@
+abs_top_srcdir = @abs_top_srcdir@
+ac_ct_AR = @ac_ct_AR@
+ac_ct_CC = @ac_ct_CC@
+ac_ct_CXX = @ac_ct_CXX@
+ac_ct_DUMPBIN = @ac_ct_DUMPBIN@
+am__include = @am__include@
+am__leading_dot = @am__leading_dot@
+am__quote = @am__quote@
+am__tar = @am__tar@
+am__untar = @am__untar@
+bindir = @bindir@
+build = @build@
+build_alias = @build_alias@
+build_cpu = @build_cpu@
+build_os = @build_os@
+build_vendor = @build_vendor@
+builddir = @builddir@
+datadir = @datadir@
+datarootdir = @datarootdir@
+dict_drivers = @dict_drivers@
+docdir = @docdir@
+dvidir = @dvidir@
+exec_prefix = @exec_prefix@
+host = @host@
+host_alias = @host_alias@
+host_cpu = @host_cpu@
+host_os = @host_os@
+host_vendor = @host_vendor@
+htmldir = @htmldir@
+includedir = @includedir@
+infodir = @infodir@
+install_sh = @install_sh@
+libdir = @libdir@
+libexecdir = @libexecdir@
+localedir = @localedir@
+localstatedir = @localstatedir@
+mandir = @mandir@
+mkdir_p = @mkdir_p@
+moduledir = @moduledir@
+oldincludedir = @oldincludedir@
+pdfdir = @pdfdir@
+prefix = @prefix@
+program_transform_name = @program_transform_name@
+psdir = @psdir@
+rundir = @rundir@
+runstatedir = @runstatedir@
+sbindir = @sbindir@
+sharedstatedir = @sharedstatedir@
+sql_drivers = @sql_drivers@
+srcdir = @srcdir@
+ssldir = @ssldir@
+statedir = @statedir@
+sysconfdir = @sysconfdir@
+systemdservicetype = @systemdservicetype@
+systemdsystemunitdir = @systemdsystemunitdir@
+target_alias = @target_alias@
+top_build_prefix = @top_build_prefix@
+top_builddir = @top_builddir@
+top_srcdir = @top_srcdir@
+AM_CPPFLAGS = \
+ -I$(top_srcdir)/src/lib \
+ -I$(top_srcdir)/src/lib-test \
+ -I$(top_srcdir)/src/lib-dict \
+ -I$(top_srcdir)/src/lib-http \
+ -I$(top_srcdir)/src/lib-ssl-iostream \
+ -I$(top_srcdir)/src/lib-master \
+ $(LUA_CFLAGS)
+
+pkglib_LTLIBRARIES = libdovecot-lua.la
+libdovecot_lua_la_SOURCES = \
+ dlua-script.c \
+ dlua-pushstring.c \
+ dlua-error.c \
+ dlua-dovecot.c \
+ dlua-dovecot-http.c \
+ dlua-compat.c \
+ dlua-resume.c \
+ dlua-table.c \
+ dlua-thread.c
+
+test_programs = test-lua $(am__append_2)
+LIBDICT_LUA = $(am__append_1)
+
+# Note: the only things this lib should depend on are libdovecot and lua.
+libdovecot_lua_la_DEPENDENCIES = \
+ ../lib-dovecot/libdovecot.la \
+ $(LIBDICT_LUA)
+
+libdovecot_lua_la_LIBADD = \
+ ../lib-dovecot/libdovecot.la \
+ $(LIBDICT_LUA) \
+ $(LUA_LIBS)
+
+libdovecot_lua_la_LDFLAGS = -export-dynamic
+headers = \
+ dlua-compat.h \
+ dlua-script.h \
+ dlua-script-private.h \
+ dlua-wrapper.h
+
+pkginc_libdir = $(pkgincludedir)
+pkginc_lib_HEADERS = $(headers)
+test_libs = \
+ libdovecot-lua.la \
+ ../lib-dovecot/libdovecot.la
+
+test_lua_SOURCES = test-lua.c
+test_lua_CFLAGS = $(AM_CPPFLAGS) $(BINARY_CFLAGS)
+test_lua_LDFLAGS = $(BINARY_LDFLAGS)
+test_lua_LDADD = $(test_libs) $(LUA_LIBS)
+test_lua_DEPENDENCIES = $(test_libs)
+test_dict_lua_SOURCES = test-dict-lua.c
+test_dict_lua_LDADD = $(test_libs) $(LUA_LIBS)
+test_dict_lua_DEPENDENCIES = $(test_libs)
+all: all-am
+
+.SUFFIXES:
+.SUFFIXES: .c .lo .o .obj
+$(srcdir)/Makefile.in: @MAINTAINER_MODE_TRUE@ $(srcdir)/Makefile.am $(am__configure_deps)
+ @for dep in $?; do \
+ case '$(am__configure_deps)' in \
+ *$$dep*) \
+ ( cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh ) \
+ && { if test -f $@; then exit 0; else break; fi; }; \
+ exit 1;; \
+ esac; \
+ done; \
+ echo ' cd $(top_srcdir) && $(AUTOMAKE) --foreign src/lib-lua/Makefile'; \
+ $(am__cd) $(top_srcdir) && \
+ $(AUTOMAKE) --foreign src/lib-lua/Makefile
+Makefile: $(srcdir)/Makefile.in $(top_builddir)/config.status
+ @case '$?' in \
+ *config.status*) \
+ cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh;; \
+ *) \
+ echo ' cd $(top_builddir) && $(SHELL) ./config.status $(subdir)/$@ $(am__maybe_remake_depfiles)'; \
+ cd $(top_builddir) && $(SHELL) ./config.status $(subdir)/$@ $(am__maybe_remake_depfiles);; \
+ esac;
+
+$(top_builddir)/config.status: $(top_srcdir)/configure $(CONFIG_STATUS_DEPENDENCIES)
+ cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh
+
+$(top_srcdir)/configure: @MAINTAINER_MODE_TRUE@ $(am__configure_deps)
+ cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh
+$(ACLOCAL_M4): @MAINTAINER_MODE_TRUE@ $(am__aclocal_m4_deps)
+ cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh
+$(am__aclocal_m4_deps):
+
+clean-noinstPROGRAMS:
+ @list='$(noinst_PROGRAMS)'; test -n "$$list" || exit 0; \
+ echo " rm -f" $$list; \
+ rm -f $$list || exit $$?; \
+ test -n "$(EXEEXT)" || exit 0; \
+ list=`for p in $$list; do echo "$$p"; done | sed 's/$(EXEEXT)$$//'`; \
+ echo " rm -f" $$list; \
+ rm -f $$list
+
+install-pkglibLTLIBRARIES: $(pkglib_LTLIBRARIES)
+ @$(NORMAL_INSTALL)
+ @list='$(pkglib_LTLIBRARIES)'; test -n "$(pkglibdir)" || list=; \
+ list2=; for p in $$list; do \
+ if test -f $$p; then \
+ list2="$$list2 $$p"; \
+ else :; fi; \
+ done; \
+ test -z "$$list2" || { \
+ echo " $(MKDIR_P) '$(DESTDIR)$(pkglibdir)'"; \
+ $(MKDIR_P) "$(DESTDIR)$(pkglibdir)" || exit 1; \
+ echo " $(LIBTOOL) $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=install $(INSTALL) $(INSTALL_STRIP_FLAG) $$list2 '$(DESTDIR)$(pkglibdir)'"; \
+ $(LIBTOOL) $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=install $(INSTALL) $(INSTALL_STRIP_FLAG) $$list2 "$(DESTDIR)$(pkglibdir)"; \
+ }
+
+uninstall-pkglibLTLIBRARIES:
+ @$(NORMAL_UNINSTALL)
+ @list='$(pkglib_LTLIBRARIES)'; test -n "$(pkglibdir)" || list=; \
+ for p in $$list; do \
+ $(am__strip_dir) \
+ echo " $(LIBTOOL) $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=uninstall rm -f '$(DESTDIR)$(pkglibdir)/$$f'"; \
+ $(LIBTOOL) $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=uninstall rm -f "$(DESTDIR)$(pkglibdir)/$$f"; \
+ done
+
+clean-pkglibLTLIBRARIES:
+ -test -z "$(pkglib_LTLIBRARIES)" || rm -f $(pkglib_LTLIBRARIES)
+ @list='$(pkglib_LTLIBRARIES)'; \
+ locs=`for p in $$list; do echo $$p; done | \
+ sed 's|^[^/]*$$|.|; s|/[^/]*$$||; s|$$|/so_locations|' | \
+ sort -u`; \
+ test -z "$$locs" || { \
+ echo rm -f $${locs}; \
+ rm -f $${locs}; \
+ }
+
+libdovecot-lua.la: $(libdovecot_lua_la_OBJECTS) $(libdovecot_lua_la_DEPENDENCIES) $(EXTRA_libdovecot_lua_la_DEPENDENCIES)
+ $(AM_V_CCLD)$(libdovecot_lua_la_LINK) -rpath $(pkglibdir) $(libdovecot_lua_la_OBJECTS) $(libdovecot_lua_la_LIBADD) $(LIBS)
+
+test-dict-lua$(EXEEXT): $(test_dict_lua_OBJECTS) $(test_dict_lua_DEPENDENCIES) $(EXTRA_test_dict_lua_DEPENDENCIES)
+ @rm -f test-dict-lua$(EXEEXT)
+ $(AM_V_CCLD)$(LINK) $(test_dict_lua_OBJECTS) $(test_dict_lua_LDADD) $(LIBS)
+
+test-lua$(EXEEXT): $(test_lua_OBJECTS) $(test_lua_DEPENDENCIES) $(EXTRA_test_lua_DEPENDENCIES)
+ @rm -f test-lua$(EXEEXT)
+ $(AM_V_CCLD)$(test_lua_LINK) $(test_lua_OBJECTS) $(test_lua_LDADD) $(LIBS)
+
+mostlyclean-compile:
+ -rm -f *.$(OBJEXT)
+
+distclean-compile:
+ -rm -f *.tab.c
+
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/dlua-compat.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/dlua-dovecot-http.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/dlua-dovecot.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/dlua-error.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/dlua-pushstring.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/dlua-resume.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/dlua-script.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/dlua-table.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/dlua-thread.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/test-dict-lua.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/test_lua-test-lua.Po@am__quote@ # am--include-marker
+
+$(am__depfiles_remade):
+ @$(MKDIR_P) $(@D)
+ @echo '# dummy' >$@-t && $(am__mv) $@-t $@
+
+am--depfiles: $(am__depfiles_remade)
+
+.c.o:
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(COMPILE) -MT $@ -MD -MP -MF $(DEPDIR)/$*.Tpo -c -o $@ $<
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/$*.Tpo $(DEPDIR)/$*.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='$<' object='$@' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(COMPILE) -c -o $@ $<
+
+.c.obj:
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(COMPILE) -MT $@ -MD -MP -MF $(DEPDIR)/$*.Tpo -c -o $@ `$(CYGPATH_W) '$<'`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/$*.Tpo $(DEPDIR)/$*.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='$<' object='$@' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(COMPILE) -c -o $@ `$(CYGPATH_W) '$<'`
+
+.c.lo:
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(LTCOMPILE) -MT $@ -MD -MP -MF $(DEPDIR)/$*.Tpo -c -o $@ $<
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/$*.Tpo $(DEPDIR)/$*.Plo
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='$<' object='$@' libtool=yes @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(LTCOMPILE) -c -o $@ $<
+
+test_lua-test-lua.o: test-lua.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) $(CPPFLAGS) $(test_lua_CFLAGS) $(CFLAGS) -MT test_lua-test-lua.o -MD -MP -MF $(DEPDIR)/test_lua-test-lua.Tpo -c -o test_lua-test-lua.o `test -f 'test-lua.c' || echo '$(srcdir)/'`test-lua.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/test_lua-test-lua.Tpo $(DEPDIR)/test_lua-test-lua.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='test-lua.c' object='test_lua-test-lua.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) $(CPPFLAGS) $(test_lua_CFLAGS) $(CFLAGS) -c -o test_lua-test-lua.o `test -f 'test-lua.c' || echo '$(srcdir)/'`test-lua.c
+
+test_lua-test-lua.obj: test-lua.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) $(CPPFLAGS) $(test_lua_CFLAGS) $(CFLAGS) -MT test_lua-test-lua.obj -MD -MP -MF $(DEPDIR)/test_lua-test-lua.Tpo -c -o test_lua-test-lua.obj `if test -f 'test-lua.c'; then $(CYGPATH_W) 'test-lua.c'; else $(CYGPATH_W) '$(srcdir)/test-lua.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/test_lua-test-lua.Tpo $(DEPDIR)/test_lua-test-lua.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='test-lua.c' object='test_lua-test-lua.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) $(CPPFLAGS) $(test_lua_CFLAGS) $(CFLAGS) -c -o test_lua-test-lua.obj `if test -f 'test-lua.c'; then $(CYGPATH_W) 'test-lua.c'; else $(CYGPATH_W) '$(srcdir)/test-lua.c'; fi`
+
+mostlyclean-libtool:
+ -rm -f *.lo
+
+clean-libtool:
+ -rm -rf .libs _libs
+install-pkginc_libHEADERS: $(pkginc_lib_HEADERS)
+ @$(NORMAL_INSTALL)
+ @list='$(pkginc_lib_HEADERS)'; test -n "$(pkginc_libdir)" || list=; \
+ if test -n "$$list"; then \
+ echo " $(MKDIR_P) '$(DESTDIR)$(pkginc_libdir)'"; \
+ $(MKDIR_P) "$(DESTDIR)$(pkginc_libdir)" || exit 1; \
+ fi; \
+ for p in $$list; do \
+ if test -f "$$p"; then d=; else d="$(srcdir)/"; fi; \
+ echo "$$d$$p"; \
+ done | $(am__base_list) | \
+ while read files; do \
+ echo " $(INSTALL_HEADER) $$files '$(DESTDIR)$(pkginc_libdir)'"; \
+ $(INSTALL_HEADER) $$files "$(DESTDIR)$(pkginc_libdir)" || exit $$?; \
+ done
+
+uninstall-pkginc_libHEADERS:
+ @$(NORMAL_UNINSTALL)
+ @list='$(pkginc_lib_HEADERS)'; test -n "$(pkginc_libdir)" || list=; \
+ files=`for p in $$list; do echo $$p; done | sed -e 's|^.*/||'`; \
+ dir='$(DESTDIR)$(pkginc_libdir)'; $(am__uninstall_files_from_dir)
+
+ID: $(am__tagged_files)
+ $(am__define_uniq_tagged_files); mkid -fID $$unique
+tags: tags-am
+TAGS: tags
+
+tags-am: $(TAGS_DEPENDENCIES) $(am__tagged_files)
+ set x; \
+ here=`pwd`; \
+ $(am__define_uniq_tagged_files); \
+ shift; \
+ if test -z "$(ETAGS_ARGS)$$*$$unique"; then :; else \
+ test -n "$$unique" || unique=$$empty_fix; \
+ if test $$# -gt 0; then \
+ $(ETAGS) $(ETAGSFLAGS) $(AM_ETAGSFLAGS) $(ETAGS_ARGS) \
+ "$$@" $$unique; \
+ else \
+ $(ETAGS) $(ETAGSFLAGS) $(AM_ETAGSFLAGS) $(ETAGS_ARGS) \
+ $$unique; \
+ fi; \
+ fi
+ctags: ctags-am
+
+CTAGS: ctags
+ctags-am: $(TAGS_DEPENDENCIES) $(am__tagged_files)
+ $(am__define_uniq_tagged_files); \
+ test -z "$(CTAGS_ARGS)$$unique" \
+ || $(CTAGS) $(CTAGSFLAGS) $(AM_CTAGSFLAGS) $(CTAGS_ARGS) \
+ $$unique
+
+GTAGS:
+ here=`$(am__cd) $(top_builddir) && pwd` \
+ && $(am__cd) $(top_srcdir) \
+ && gtags -i $(GTAGS_ARGS) "$$here"
+cscopelist: cscopelist-am
+
+cscopelist-am: $(am__tagged_files)
+ list='$(am__tagged_files)'; \
+ case "$(srcdir)" in \
+ [\\/]* | ?:[\\/]*) sdir="$(srcdir)" ;; \
+ *) sdir=$(subdir)/$(srcdir) ;; \
+ esac; \
+ for i in $$list; do \
+ if test -f "$$i"; then \
+ echo "$(subdir)/$$i"; \
+ else \
+ echo "$$sdir/$$i"; \
+ fi; \
+ done >> $(top_builddir)/cscope.files
+
+distclean-tags:
+ -rm -f TAGS ID GTAGS GRTAGS GSYMS GPATH tags
+
+distdir: $(BUILT_SOURCES)
+ $(MAKE) $(AM_MAKEFLAGS) distdir-am
+
+distdir-am: $(DISTFILES)
+ @srcdirstrip=`echo "$(srcdir)" | sed 's/[].[^$$\\*]/\\\\&/g'`; \
+ topsrcdirstrip=`echo "$(top_srcdir)" | sed 's/[].[^$$\\*]/\\\\&/g'`; \
+ list='$(DISTFILES)'; \
+ dist_files=`for file in $$list; do echo $$file; done | \
+ sed -e "s|^$$srcdirstrip/||;t" \
+ -e "s|^$$topsrcdirstrip/|$(top_builddir)/|;t"`; \
+ case $$dist_files in \
+ */*) $(MKDIR_P) `echo "$$dist_files" | \
+ sed '/\//!d;s|^|$(distdir)/|;s,/[^/]*$$,,' | \
+ sort -u` ;; \
+ esac; \
+ for file in $$dist_files; do \
+ if test -f $$file || test -d $$file; then d=.; else d=$(srcdir); fi; \
+ if test -d $$d/$$file; then \
+ dir=`echo "/$$file" | sed -e 's,/[^/]*$$,,'`; \
+ if test -d "$(distdir)/$$file"; then \
+ find "$(distdir)/$$file" -type d ! -perm -700 -exec chmod u+rwx {} \;; \
+ fi; \
+ if test -d $(srcdir)/$$file && test $$d != $(srcdir); then \
+ cp -fpR $(srcdir)/$$file "$(distdir)$$dir" || exit 1; \
+ find "$(distdir)/$$file" -type d ! -perm -700 -exec chmod u+rwx {} \;; \
+ fi; \
+ cp -fpR $$d/$$file "$(distdir)$$dir" || exit 1; \
+ else \
+ test -f "$(distdir)/$$file" \
+ || cp -p $$d/$$file "$(distdir)/$$file" \
+ || exit 1; \
+ fi; \
+ done
+check-am: all-am
+ $(MAKE) $(AM_MAKEFLAGS) check-local
+check: check-am
+all-am: Makefile $(PROGRAMS) $(LTLIBRARIES) $(HEADERS)
+installdirs:
+ for dir in "$(DESTDIR)$(pkglibdir)" "$(DESTDIR)$(pkginc_libdir)"; do \
+ test -z "$$dir" || $(MKDIR_P) "$$dir"; \
+ done
+install: install-am
+install-exec: install-exec-am
+install-data: install-data-am
+uninstall: uninstall-am
+
+install-am: all-am
+ @$(MAKE) $(AM_MAKEFLAGS) install-exec-am install-data-am
+
+installcheck: installcheck-am
+install-strip:
+ if test -z '$(STRIP)'; then \
+ $(MAKE) $(AM_MAKEFLAGS) INSTALL_PROGRAM="$(INSTALL_STRIP_PROGRAM)" \
+ install_sh_PROGRAM="$(INSTALL_STRIP_PROGRAM)" INSTALL_STRIP_FLAG=-s \
+ install; \
+ else \
+ $(MAKE) $(AM_MAKEFLAGS) INSTALL_PROGRAM="$(INSTALL_STRIP_PROGRAM)" \
+ install_sh_PROGRAM="$(INSTALL_STRIP_PROGRAM)" INSTALL_STRIP_FLAG=-s \
+ "INSTALL_PROGRAM_ENV=STRIPPROG='$(STRIP)'" install; \
+ fi
+mostlyclean-generic:
+
+clean-generic:
+
+distclean-generic:
+ -test -z "$(CONFIG_CLEAN_FILES)" || rm -f $(CONFIG_CLEAN_FILES)
+ -test . = "$(srcdir)" || test -z "$(CONFIG_CLEAN_VPATH_FILES)" || rm -f $(CONFIG_CLEAN_VPATH_FILES)
+
+maintainer-clean-generic:
+ @echo "This command is intended for maintainers to use"
+ @echo "it deletes files that may require special tools to rebuild."
+clean: clean-am
+
+clean-am: clean-generic clean-libtool clean-noinstPROGRAMS \
+ clean-pkglibLTLIBRARIES mostlyclean-am
+
+distclean: distclean-am
+ -rm -f ./$(DEPDIR)/dlua-compat.Plo
+ -rm -f ./$(DEPDIR)/dlua-dovecot-http.Plo
+ -rm -f ./$(DEPDIR)/dlua-dovecot.Plo
+ -rm -f ./$(DEPDIR)/dlua-error.Plo
+ -rm -f ./$(DEPDIR)/dlua-pushstring.Plo
+ -rm -f ./$(DEPDIR)/dlua-resume.Plo
+ -rm -f ./$(DEPDIR)/dlua-script.Plo
+ -rm -f ./$(DEPDIR)/dlua-table.Plo
+ -rm -f ./$(DEPDIR)/dlua-thread.Plo
+ -rm -f ./$(DEPDIR)/test-dict-lua.Po
+ -rm -f ./$(DEPDIR)/test_lua-test-lua.Po
+ -rm -f Makefile
+distclean-am: clean-am distclean-compile distclean-generic \
+ distclean-tags
+
+dvi: dvi-am
+
+dvi-am:
+
+html: html-am
+
+html-am:
+
+info: info-am
+
+info-am:
+
+install-data-am: install-pkginc_libHEADERS
+
+install-dvi: install-dvi-am
+
+install-dvi-am:
+
+install-exec-am: install-pkglibLTLIBRARIES
+
+install-html: install-html-am
+
+install-html-am:
+
+install-info: install-info-am
+
+install-info-am:
+
+install-man:
+
+install-pdf: install-pdf-am
+
+install-pdf-am:
+
+install-ps: install-ps-am
+
+install-ps-am:
+
+installcheck-am:
+
+maintainer-clean: maintainer-clean-am
+ -rm -f ./$(DEPDIR)/dlua-compat.Plo
+ -rm -f ./$(DEPDIR)/dlua-dovecot-http.Plo
+ -rm -f ./$(DEPDIR)/dlua-dovecot.Plo
+ -rm -f ./$(DEPDIR)/dlua-error.Plo
+ -rm -f ./$(DEPDIR)/dlua-pushstring.Plo
+ -rm -f ./$(DEPDIR)/dlua-resume.Plo
+ -rm -f ./$(DEPDIR)/dlua-script.Plo
+ -rm -f ./$(DEPDIR)/dlua-table.Plo
+ -rm -f ./$(DEPDIR)/dlua-thread.Plo
+ -rm -f ./$(DEPDIR)/test-dict-lua.Po
+ -rm -f ./$(DEPDIR)/test_lua-test-lua.Po
+ -rm -f Makefile
+maintainer-clean-am: distclean-am maintainer-clean-generic
+
+mostlyclean: mostlyclean-am
+
+mostlyclean-am: mostlyclean-compile mostlyclean-generic \
+ mostlyclean-libtool
+
+pdf: pdf-am
+
+pdf-am:
+
+ps: ps-am
+
+ps-am:
+
+uninstall-am: uninstall-pkginc_libHEADERS uninstall-pkglibLTLIBRARIES
+
+.MAKE: check-am install-am install-strip
+
+.PHONY: CTAGS GTAGS TAGS all all-am am--depfiles check check-am \
+ check-local clean clean-generic clean-libtool \
+ clean-noinstPROGRAMS clean-pkglibLTLIBRARIES cscopelist-am \
+ ctags ctags-am distclean distclean-compile distclean-generic \
+ distclean-libtool distclean-tags distdir dvi dvi-am html \
+ html-am info info-am install install-am install-data \
+ install-data-am install-dvi install-dvi-am install-exec \
+ install-exec-am install-html install-html-am install-info \
+ install-info-am install-man install-pdf install-pdf-am \
+ install-pkginc_libHEADERS install-pkglibLTLIBRARIES install-ps \
+ install-ps-am install-strip installcheck installcheck-am \
+ installdirs maintainer-clean maintainer-clean-generic \
+ mostlyclean mostlyclean-compile mostlyclean-generic \
+ mostlyclean-libtool pdf pdf-am ps ps-am tags tags-am uninstall \
+ uninstall-am uninstall-pkginc_libHEADERS \
+ uninstall-pkglibLTLIBRARIES
+
+.PRECIOUS: Makefile
+
+
+check-local:
+ for bin in $(test_programs); do \
+ if ! $(RUN_TEST) ./$$bin; then exit 1; fi; \
+ done
+
+# Tell versions [3.59,3.63) of GNU make to not export all variables.
+# Otherwise a system limit (for SysV at least) may be exceeded.
+.NOEXPORT:
diff --git a/src/lib-lua/dlua-compat.c b/src/lib-lua/dlua-compat.c
new file mode 100644
index 0000000..c676186
--- /dev/null
+++ b/src/lib-lua/dlua-compat.c
@@ -0,0 +1,157 @@
+/* Copyright (c) 2017-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "strnum.h"
+#include "dlua-script-private.h"
+
+#if LUA_VERSION_NUM == 502
+# error "Lua 5.2 is not supported. Use Lua 5.1 or 5.3 instead."
+#endif
+
+#ifndef HAVE_LUAL_SETFUNCS
+void luaL_setfuncs(lua_State *L, const luaL_Reg *l, int nup)
+{
+ luaL_checkstack(L, nup + 1, "too many upvalues");
+ for (; l->name != NULL; l++) {
+ int i;
+ lua_pushstring(L, l->name);
+ for (i = 0; i < nup; i++)
+ lua_pushvalue(L, -(nup + 1));
+ lua_pushcclosure(L, l->func, nup);
+ lua_settable(L, -(nup + 3));
+ }
+ lua_pop(L, nup);
+}
+#endif
+
+#ifndef HAVE_LUAL_SETMETATABLE
+void luaL_setmetatable(lua_State *L, const char *tname)
+{
+ luaL_checkstack(L, 1, "not enough stack slots");
+ luaL_getmetatable(L, tname);
+ lua_setmetatable(L, -2);
+}
+#endif
+
+#ifndef HAVE_LUA_ISINTEGER
+# if LUA_VERSION_NUM >= 503
+# error "Lua 5.3+ should have lua_isinteger()"
+# endif
+/*
+ * Lua 5.3 added lua_isinteger() which tells us whether or not the input is
+ * an integer. In Lua 5.1 and 5.2, we have to emulate it.
+ */
+#undef lua_isinteger
+int lua_isinteger(lua_State *L, int idx)
+{
+ int isnum;
+
+ if (lua_type(L, idx) != LUA_TNUMBER)
+ return 0;
+
+ (void) lua_tointegerx(L, idx, &isnum);
+
+ return isnum;
+}
+#endif
+
+#ifndef HAVE_LUA_SETI
+void lua_seti(lua_State *L, int index, lua_Integer n)
+{
+ /* stack: value (top) */
+ lua_pushinteger(L, n);
+ /* stack: value, n (top) */
+ lua_insert(L, -2);
+ /* stack: n, value (top) */
+
+ /* adjust relative stack position */
+ if (index < 0)
+ index--;
+
+ lua_settable(L, index);
+}
+#endif
+
+#ifndef HAVE_LUA_TOINTEGERX
+# if LUA_VERSION_NUM >= 502
+# error "Lua 5.2+ should have lua_tointegerx()"
+# endif
+/*
+ * Lua 5.2 added lua_tointegerx() which tells us whether or not the
+ * input was an integer. In Lua 5.1, we have to emulate it to the best of
+ * our ability.
+ */
+lua_Integer lua_tointegerx(lua_State *L, int idx, int *isnum_r)
+{
+ lua_Integer integer;
+ lua_Number number;
+ const char *str;
+
+ /*
+ * Unfortunately, Lua 5.1 doesn't provide MIN/MAX value macros for
+ * the lua_Integer type, so we hardcode the assumption that it is
+ * the same size as ptrdiff_t. This matches what Lua does by
+ * default.
+ *
+ * If this compile-time assertion fails, don't forget to change the
+ * PTRDIFF_{MIN,MAX} usage below as well.
+ */
+ (void) COMPILE_ERROR_IF_TRUE(sizeof(lua_Integer) != sizeof(ptrdiff_t));
+
+ switch (lua_type(L, idx)) {
+ case LUA_TSTRING:
+ /* convert using str_to_long() */
+ str = lua_tostring(L, idx);
+
+ if (strncasecmp(str, "0x", 2) == 0) {
+ /* hex */
+ uintmax_t tmp;
+
+ /* skip over leading 0x */
+ str += 2;
+
+ if (str_to_uintmax_hex(str, &tmp) < 0)
+ break;
+
+ *isnum_r = (tmp <= PTRDIFF_MAX) ? 1 : 0;
+ return tmp;
+ } else {
+ /* try decimal */
+ intmax_t tmp;
+
+ if (str_to_intmax(str, &tmp) < 0)
+ break;
+
+ *isnum_r = ((tmp >= PTRDIFF_MIN) && (tmp <= PTRDIFF_MAX)) ? 1 : 0;
+ return tmp;
+ }
+
+ break;
+ case LUA_TNUMBER:
+ /* use lua helper macro */
+ number = lua_tonumber(L, idx);
+
+ /* Lua 5.1-only macro from luaconf.h */
+ lua_number2integer(integer, number);
+
+ *isnum_r = (((lua_Number) integer) == number) ? 1 : 0;
+
+ return integer;
+ default:
+ break;
+ }
+
+ /* not an integer */
+ *isnum_r = 0;
+ return 0;
+}
+#endif
+
+#if LUA_VERSION_NUM > 501 && LUA_VERSION_NUM < 504
+# undef lua_resume
+int lua_resume_compat(lua_State *L, lua_State *from, int nargs, int *nresults)
+{
+ *nresults = 1;
+ return lua_resume(L, from, nargs);
+}
+#endif
diff --git a/src/lib-lua/dlua-compat.h b/src/lib-lua/dlua-compat.h
new file mode 100644
index 0000000..052bb83
--- /dev/null
+++ b/src/lib-lua/dlua-compat.h
@@ -0,0 +1,69 @@
+#ifndef DLUA_COMPAT_H
+#define DLUA_COMPAT_H
+
+/*
+ * In general, make whatever Lua version we have behave more like Lua 5.3.
+ */
+
+#if !defined(LUA_OK)
+# define LUA_OK 0
+#endif
+
+/* functionality missing from <= 5.2 */
+#if LUA_VERSION_NUM <= 502
+# define luaL_newmetatable(L, tn) \
+ ((luaL_newmetatable(L, tn) != 0) ? \
+ (lua_pushstring((L), (tn)), lua_setfield((L), -2, "__name"), 1) : \
+ 0)
+#endif
+
+/* functionality missing from <= 5.1 */
+#if LUA_VERSION_NUM <= 501
+# define lua_load(L, r, s, fn, m) lua_load(L, r, s, fn)
+# define luaL_newlibtable(L, l) (lua_createtable(L, 0, sizeof(l)/sizeof(*(l))-1))
+# define luaL_newlib(L, l) (luaL_newlibtable(L, l), luaL_register(L, NULL, l))
+#endif
+
+#ifndef HAVE_LUAL_SETFUNCS
+void luaL_setfuncs (lua_State *L, const luaL_Reg *l, int nup);
+#endif
+
+#ifndef HAVE_LUAL_SETMETATABLE
+void luaL_setmetatable (lua_State *L, const char *tname);
+#endif
+
+#ifndef HAVE_LUA_ISINTEGER
+/*
+ * Lua 5.3 can actually keep track of intergers vs. numbers. As a
+ * consequence, lua_isinteger() tells us if the internal representation of
+ * the number is an integer (vs. a number). In previous versions, there was
+ * no way to check for this and our compatibility wrapper is not quite
+ * capable of matching the 5.3 behavior exactly. Therefore, it returns 1
+ * when the number is representable as an integer instead.
+ */
+int lua_isinteger(lua_State *L, int idx);
+#endif
+
+#ifndef HAVE_LUA_SETI
+void lua_seti(lua_State *L, int index, lua_Integer n);
+#endif
+
+#ifndef HAVE_LUA_TOINTEGERX
+/*
+ * Lua 5.2 and 5.3 both have lua_tointegerx(), but their behavior is subtly
+ * different. Our compatibility wrapper matches the 5.3 behavior.
+ */
+lua_Integer lua_tointegerx(lua_State *L, int idx, int *isnum_r);
+#endif
+
+#if LUA_VERSION_NUM > 501 && LUA_VERSION_NUM < 504
+/*
+ * lua_resume() compatibility function. Lua 5.4 expects an extra "nresults"
+ * argeument.
+ */
+# define lua_resume(L, from, nargs, nresults) \
+ lua_resume_compat(L, from, nargs, nresults)
+int lua_resume_compat(lua_State *L, lua_State *from, int nargs, int *nresults);
+#endif
+
+#endif
diff --git a/src/lib-lua/dlua-dovecot-http.c b/src/lib-lua/dlua-dovecot-http.c
new file mode 100644
index 0000000..3c8c167
--- /dev/null
+++ b/src/lib-lua/dlua-dovecot-http.c
@@ -0,0 +1,519 @@
+/* Copyright (c) 2021 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "ioloop.h"
+#include "str.h"
+#include "dlua-script-private.h"
+#include "http-url.h"
+#include "http-client.h"
+#include "http-client-private.h"
+#include "istream.h"
+#include "iostream-ssl.h"
+#include "master-service.h"
+#include "master-service-ssl-settings.h"
+
+#define DLUA_DOVECOT_HTTP "http"
+#define DLUA_HTTP_CLIENT "struct http_client"
+#define DLUA_HTTP_CLIENT_REQUEST "struct http_client_request"
+#define DLUA_HTTP_RESPONSE "struct dlua_http_response"
+
+struct dlua_http_response {
+ unsigned char version_major;
+ unsigned char version_minor;
+ unsigned int status;
+ const char *reason;
+ const char *location;
+ string_t *payload;
+ time_t date, retry_after;
+ ARRAY_TYPE(http_header_field) headers;
+ pool_t pool;
+ const char *error;
+ struct event *event;
+};
+
+struct dlua_http_response_payload_context {
+ struct io *io;
+ struct istream *payload_istream;
+ string_t *payload_str;
+ char *error;
+ struct event *event;
+ pool_t pool;
+};
+
+static struct http_client_request *
+dlua_check_http_request(lua_State *L, int arg)
+{
+ if (!lua_istable(L, arg)) {
+ (void)luaL_error(L, "Bad argument #%d, expected %s got %s",
+ arg, DLUA_HTTP_CLIENT_REQUEST,
+ lua_typename(L, lua_type(L, arg)));
+ }
+ lua_pushliteral(L, "item");
+ lua_rawget(L, arg);
+ struct http_client_request **bp = lua_touserdata(L, -1);
+ lua_pop(L, 1);
+ return *bp;
+}
+
+static int dlua_http_request_gc(lua_State *L)
+{
+ struct http_client_request **req = lua_touserdata(L, 1);
+ http_client_request_unref(req);
+ return 0;
+}
+
+static int dlua_http_request_add_header(lua_State *L)
+{
+ DLUA_REQUIRE_ARGS(L, 3);
+
+ struct http_client_request *req = dlua_check_http_request(L, 1);
+
+ const char *name = luaL_checkstring(L, 2);
+ const char *value = luaL_checkstring(L, 3);
+ http_client_request_add_header(req, name, value);
+ return 0;
+}
+
+static int dlua_http_request_remove_header(lua_State *L)
+{
+ DLUA_REQUIRE_ARGS(L, 2);
+
+ struct http_client_request *req = dlua_check_http_request(L, 1);
+
+ const char *name = luaL_checkstring(L, 2);
+ http_client_request_remove_header(req, name);
+ return 0;
+}
+
+static int dlua_http_request_set_payload(lua_State *L)
+{
+ DLUA_REQUIRE_ARGS(L, 2);
+
+ struct http_client_request *req = dlua_check_http_request(L, 1);
+ struct istream *payload_istream;
+
+ const char *payload = luaL_checkstring(L, 2);
+ payload_istream = i_stream_create_copy_from_data(payload,
+ strlen(payload));
+ http_client_request_set_payload(req, payload_istream, TRUE);
+ i_stream_unref(&payload_istream);
+ return 0;
+}
+
+static int dlua_http_request_submit(lua_State *L)
+{
+ DLUA_REQUIRE_ARGS(L, 1);
+
+ struct http_client_request *req = dlua_check_http_request(L, 1);
+
+ /* Clear the GC hook for this request. It will be freed after it's
+ submitted. */
+ lua_getfield(L, -1, "item");
+ if (lua_getmetatable(L, -1) != 1)
+ return luaL_error(L, "Cound't get metatable for the request");
+ lua_pushnil(L);
+ lua_setfield(L, -2, "__gc");
+ lua_pop(L, 2);
+
+ http_client_request_submit(req);
+ http_client_wait(req->client);
+ return 1;
+}
+
+static luaL_Reg lua_dovecot_http_request_methods[] = {
+ { "add_header", dlua_http_request_add_header },
+ { "remove_header", dlua_http_request_remove_header },
+ { "set_payload", dlua_http_request_set_payload },
+ { "submit", dlua_http_request_submit },
+ { NULL, NULL }
+};
+
+static void dlua_push_http_request(lua_State *L, struct http_client_request *req)
+{
+ luaL_checkstack(L, 3, "out of memory");
+ lua_createtable(L, 0, 1);
+ luaL_setmetatable(L, DLUA_HTTP_CLIENT_REQUEST);
+
+ /* we need to attach gc to userdata to support older lua*/
+ struct http_client_request **ptr = lua_newuserdata(L, sizeof(struct http_client_request*));
+ *ptr = req;
+ lua_createtable(L, 0, 1);
+ lua_pushcfunction(L, dlua_http_request_gc);
+ lua_setfield(L, -2, "__gc");
+ lua_setmetatable(L, -2);
+ lua_setfield(L, -2, "item");
+
+ luaL_setfuncs(L, lua_dovecot_http_request_methods, 0);
+}
+
+
+static struct http_client *dlua_check_http_client(lua_State *L, int arg)
+{
+ if (!lua_istable(L, arg)) {
+ (void)luaL_error(L, "Bad argument #%d, expected %s got %s",
+ arg, DLUA_HTTP_CLIENT,
+ lua_typename(L, lua_type(L, arg)));
+ }
+ lua_pushliteral(L, "item");
+ lua_rawget(L, arg);
+ struct http_client **bp = lua_touserdata(L, -1);
+ lua_pop(L, 1);
+ return *bp;
+}
+
+static int dlua_http_client_gc(lua_State *L)
+{
+ struct http_client **_client = lua_touserdata(L, 1);
+ http_client_deinit(_client);
+ return 0;
+}
+static int dlua_http_resp_gc(lua_State *L)
+{
+ struct dlua_http_response **_resp = lua_touserdata(L, 1);
+ array_free(&(*_resp)->headers);
+ pool_unref(&(*_resp)->pool);
+ return 0;
+}
+
+static struct dlua_http_response *dlua_check_http_response(lua_State *L, int arg)
+{
+ if (!lua_istable(L, arg)) {
+ (void)luaL_error(L, "Bad argument #%d, expected %s got %s",
+ arg, DLUA_HTTP_RESPONSE,
+ lua_typename(L, lua_type(L, arg)));
+ }
+ lua_pushliteral(L, "item");
+ lua_rawget(L, arg);
+ struct dlua_http_response **bp = lua_touserdata(L, -1);
+ lua_pop(L, 1);
+ return *bp;
+}
+
+static int dlua_http_response_get_status(lua_State *L)
+{
+ DLUA_REQUIRE_ARGS(L, 1);
+
+ const struct dlua_http_response *resp = dlua_check_http_response(L, 1);
+ lua_pushinteger(L, resp->status);
+ return 1;
+}
+
+static int dlua_http_response_get_payload(lua_State *L)
+{
+ DLUA_REQUIRE_ARGS(L, 1);
+
+ const struct dlua_http_response *resp = dlua_check_http_response(L, 1);
+ lua_pushlstring(L, resp->payload->data, resp->payload->used);
+ return 1;
+}
+
+static int dlua_http_response_get_header(lua_State *L)
+{
+ DLUA_REQUIRE_ARGS(L, 2);
+
+ const struct dlua_http_response *resp = dlua_check_http_response(L, 1);
+ const char *name = luaL_checkstring(L, 2);
+ const char *value = "";
+
+ const struct http_header_field *hfield;
+ array_foreach(&resp->headers, hfield) {
+ if (http_header_field_is(hfield, name)) {
+ value = hfield->value;
+ break;
+ }
+ }
+
+ lua_pushstring(L, value);
+ return 1;
+}
+
+static int dlua_http_response_get_reason(lua_State *L)
+{
+ DLUA_REQUIRE_ARGS(L, 1);
+
+ const struct dlua_http_response *resp = dlua_check_http_response(L, 1);
+ lua_pushstring(L, resp->reason);
+ return 1;
+}
+
+static const luaL_Reg dovecot_http_response_methods[] = {
+ { "status", dlua_http_response_get_status },
+ { "payload", dlua_http_response_get_payload },
+ { "header", dlua_http_response_get_header },
+ { "reason", dlua_http_response_get_reason },
+ { NULL, NULL }
+};
+
+static void
+dlua_push_http_response(lua_State *L, const struct dlua_http_response *resp)
+{
+ luaL_checkstack(L, 3, "out of memory");
+ lua_createtable(L, 0, 1);
+ luaL_setmetatable(L, DLUA_HTTP_RESPONSE);
+
+ /* we need to attach gc to userdata to support older lua*/
+ const struct dlua_http_response **ptr = lua_newuserdata(L, sizeof(struct dlua_http_response*));
+ *ptr = resp;
+ lua_createtable(L, 0, 1);
+ lua_pushcfunction(L, dlua_http_resp_gc);
+ lua_setfield(L, -2, "__gc");
+ lua_setmetatable(L, -2);
+ lua_setfield(L, -2, "item");
+
+ luaL_setfuncs(L, dovecot_http_response_methods, 0);
+}
+
+static void dlua_http_response_input_payload(struct dlua_http_response_payload_context *ctx)
+{
+ const unsigned char *data;
+ size_t size;
+ int ret;
+
+ /* read payload */
+ while ((ret=i_stream_read_more(ctx->payload_istream, &data, &size)) > 0) {
+ str_append_data(ctx->payload_str, data, size);
+ i_stream_skip(ctx->payload_istream, size);
+ }
+
+ if (ctx->payload_istream->stream_errno != 0) {
+ ctx->error = p_strdup_printf(ctx->pool,
+ "Response payload read error: %s",
+ i_stream_get_error(ctx->payload_istream));
+ }
+ if (ret == 0) {
+ e_debug(ctx->event, "DEBUG: REQUEST: NEED MORE DATA");
+ /* we will be called again for this request */
+ } else {
+ if (ctx->payload_istream->stream_errno != 0) {
+ e_error(ctx->event, "ERROR: REQUEST PAYLOAD READ ERROR: %s",
+ i_stream_get_error(ctx->payload_istream));
+ } else
+ e_debug(ctx->event, "DEBUG: REQUEST: Finished");
+ io_remove(&ctx->io);
+ i_free(ctx);
+ }
+}
+
+static void dlua_http_response_read_payload(const struct http_response *response,
+ struct dlua_http_response *dlua_resp)
+{
+ struct dlua_http_response_payload_context *ctx =
+ i_new(struct dlua_http_response_payload_context ,1);
+ ctx->payload_istream = response->payload;
+ ctx->io = io_add_istream(response->payload,
+ dlua_http_response_input_payload, ctx);
+ ctx->payload_str = dlua_resp->payload;
+ ctx->pool = dlua_resp->pool;
+ ctx->event = dlua_resp->event;
+ dlua_http_response_input_payload(ctx);
+}
+
+static void
+dlua_http_request_callback(const struct http_response *response, lua_State *L)
+{
+ struct dlua_script *script = dlua_script_from_state(L);
+
+ /* we need a keep a copy of http_response, otherwise the data will be
+ * lost when the object is freed. */
+ pool_t pool = pool_alloconly_create("http_response", 1024);
+ struct dlua_http_response *resp = p_new(pool, struct dlua_http_response, 1);
+ resp->pool = pool;
+ resp->date = response->date;
+ resp->version_major = response->version_major;
+ resp->version_minor = response->version_minor;
+ resp->status = response->status;
+ resp->reason = p_strdup(resp->pool, response->reason);
+ resp->location = p_strdup(resp->pool, response->location);
+ resp->date = response->date;
+ resp->retry_after = response->retry_after;
+ resp->payload = str_new(resp->pool, 528);
+ resp->event = script->event;
+ p_array_init(&resp->headers, resp->pool, 2);
+
+ const ARRAY_TYPE(http_header_field) *hdrs;
+ const struct http_header_field *hdr;
+ struct http_header_field *hdr_cpy;
+
+ hdrs = http_response_header_get_fields(response);
+ if (hdrs != NULL) {
+ array_foreach(hdrs, hdr) {
+ hdr_cpy = array_append_space(&resp->headers);
+ hdr_cpy->name = p_strdup(resp->pool, hdr->name);
+ hdr_cpy->size = hdr->size;
+ hdr_cpy->value = p_strdup(resp->pool, hdr->value);
+ }
+ }
+
+ if (response->payload != NULL) {
+ /* got payload */
+ dlua_http_response_read_payload(response, resp);
+ }
+
+ dlua_push_http_response(L, resp);
+}
+
+static int dlua_http_request_new(lua_State *L)
+{
+ DLUA_REQUIRE_ARGS(L, 2);
+
+ const char *url, *method = "GET";
+ struct http_client_request *http_req;
+ struct http_url *http_url;
+ const char *error;
+ struct http_client *client = dlua_check_http_client(L, 1);
+
+ luaL_checktype(L, 2, LUA_TTABLE);
+
+ lua_getfield(L, -1, "url");
+ if (lua_isnil(L, -1))
+ return luaL_error(L, "cannot create request: url not specified");
+ else
+ url = luaL_checkstring(L, -1);
+ lua_pop(L, 1);
+ lua_getfield(L, -1, "method");
+ if (!lua_isnil(L, -1))
+ method = luaL_checkstring(L, -1);
+ lua_pop(L, 1);
+
+ if (http_url_parse(url, NULL, HTTP_URL_ALLOW_USERINFO_PART, pool_datastack_create(),
+ &http_url, &error) < 0) {
+ return luaL_error(L, "Failed to parse url %s: %s", url, error);
+ return -1;
+ }
+
+ if (http_url->have_ssl && client->set.ssl == NULL) {
+ return luaL_error(L, "TLS not enabled, cannot submit https request");
+ }
+ http_req = http_client_request_url(client, method, http_url,
+ dlua_http_request_callback, L);
+
+ dlua_push_http_request(L, http_req);
+ return 1;
+}
+
+static const luaL_Reg dovecot_http_client_methods[] = {
+ { "request", dlua_http_request_new },
+ { NULL, NULL }
+};
+
+static void dlua_push_http_client(lua_State *L, struct http_client *client)
+{
+ luaL_checkstack(L, 3, "out of memory");
+ lua_createtable(L, 0, 1);
+ luaL_setmetatable(L, DLUA_HTTP_CLIENT);
+
+ /* we need to attach gc to userdata to support older lua*/
+ struct http_client **ptr = lua_newuserdata(L, sizeof(struct http_client*));
+ *ptr = client;
+ lua_createtable(L, 0, 1);
+ lua_pushcfunction(L, dlua_http_client_gc);
+ lua_setfield(L, -2, "__gc");
+ lua_setmetatable(L, -2);
+ lua_setfield(L, -2, "item");
+
+ luaL_setfuncs(L, dovecot_http_client_methods, 0);
+}
+
+#define CLIENT_SETTING_STR(field) \
+ if (dlua_table_get_string_by_str(L, -1, #field, &(set->field)) < 0) { \
+ *error_r = t_strdup_printf("%s: string expected", #field); return -1; }
+#define CLIENT_SETTING_UINT(field) \
+ if (dlua_table_get_uint_by_str(L, -1, #field, &(set->field)) < 0) { \
+ *error_r = t_strdup_printf("%s: non-negative number expected", #field); return -1; }
+#define CLIENT_SETTING_BOOL(field) \
+ if (dlua_table_get_bool_by_str(L, -1, #field, &(set->field)) < 0) { \
+ *error_r = t_strdup_printf("%s: boolean expected", #field); return -1; }
+
+static int parse_client_settings(lua_State *L, struct http_client_settings *set,
+ const char **error_r)
+{
+ struct http_url *parsed_url;
+ const char *proxy_url;
+
+ set->dns_client_socket_path = "dns-client";
+ CLIENT_SETTING_STR(user_agent);
+ CLIENT_SETTING_STR(rawlog_dir);
+ CLIENT_SETTING_UINT(max_idle_time_msecs);
+/* FIXME: Enable when asynchronous calls are supported
+* CLIENT_SETTING_UINT(max_parallel_connections);
+* CLIENT_SETTING_UINT(max_pipelined_requests);
+*/
+ CLIENT_SETTING_BOOL(no_auto_redirect);
+ CLIENT_SETTING_BOOL(no_auto_retry);
+ CLIENT_SETTING_UINT(max_redirects);
+ CLIENT_SETTING_UINT(max_attempts);
+ CLIENT_SETTING_UINT(max_connect_attempts);
+ CLIENT_SETTING_UINT(connect_backoff_time_msecs);
+ CLIENT_SETTING_UINT(connect_backoff_max_time_msecs);
+ CLIENT_SETTING_UINT(request_absolute_timeout_msecs);
+ CLIENT_SETTING_UINT(request_timeout_msecs);
+ CLIENT_SETTING_UINT(connect_timeout_msecs);
+ CLIENT_SETTING_UINT(soft_connect_timeout_msecs);
+ CLIENT_SETTING_UINT(max_auto_retry_delay_secs);
+ CLIENT_SETTING_BOOL(debug);
+
+ if (dlua_table_get_string_by_str(L, -1, "proxy_url", &proxy_url) > 0) {
+ if (http_url_parse(proxy_url, NULL, HTTP_URL_ALLOW_USERINFO_PART,
+ pool_datastack_create(), &parsed_url, error_r) < 0) {
+ *error_r = t_strdup_printf("proxy_url is invalid: %s",
+ *error_r);
+ return -1;
+ }
+ set->proxy_url = parsed_url;
+ set->proxy_username = parsed_url->user;
+ set->proxy_password = parsed_url->password;
+ }
+
+ lua_getfield(L, -1, "event_parent");
+ if (!lua_isnil(L, -1))
+ set->event_parent = dlua_check_event(L, -1);
+
+ return 0;
+}
+
+static int dlua_http_client_new(lua_State *L)
+{
+ DLUA_REQUIRE_ARGS(L, 1);
+ luaL_checktype(L, 1, LUA_TTABLE);
+
+ struct http_client *client;
+ struct http_client_settings http_set;
+ struct ssl_iostream_settings ssl_set;
+ const char *error;
+
+ i_zero(&http_set);
+
+ if (parse_client_settings(L, &http_set, &error) < 0)
+ luaL_error(L, "Invalid HTTP client setting: %s", error);
+
+ const struct master_service_ssl_settings *master_ssl_set =
+ master_service_ssl_settings_get(master_service);
+ master_service_ssl_client_settings_to_iostream_set(master_ssl_set,
+ pool_datastack_create(), &ssl_set);
+ http_set.ssl = &ssl_set;
+
+ client = http_client_init(&http_set);
+ dlua_push_http_client(L, client);
+ return 1;
+}
+
+static const luaL_Reg dovecot_http_methods[] = {
+ { "client", dlua_http_client_new },
+ { NULL, NULL }
+};
+
+void dlua_dovecot_http_register(struct dlua_script *script)
+{
+ i_assert(script != NULL);
+
+ lua_State *L = script->L;
+
+ /* push dovecot table on the stack */
+ dlua_get_dovecot(L);
+
+ /* populate http methods in a table and add them as dovecot.http */
+ lua_newtable(L);
+ luaL_setfuncs(L, dovecot_http_methods, 0);
+ lua_setfield(script->L, -2, DLUA_DOVECOT_HTTP);
+ lua_pop(script->L, 1);
+}
diff --git a/src/lib-lua/dlua-dovecot.c b/src/lib-lua/dlua-dovecot.c
new file mode 100644
index 0000000..124f533
--- /dev/null
+++ b/src/lib-lua/dlua-dovecot.c
@@ -0,0 +1,681 @@
+/* Copyright (c) 2017-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "ioloop.h"
+#include "str.h"
+#include "dlua-script-private.h"
+
+#include <libgen.h>
+
+#define LUA_SCRIPT_DOVECOT "dovecot"
+#define DLUA_EVENT_PASSTHROUGH "struct event_passthrough"
+#define DLUA_EVENT "struct event"
+
+static void dlua_event_log(lua_State *L, struct event *event,
+ enum log_type log_type, const char *str);
+
+static void dlua_get_file_line(lua_State *L, int arg, const char **file_r,
+ unsigned int *line_r)
+{
+ const char *ptr;
+ lua_Debug ar;
+ lua_getstack(L, arg, &ar);
+ lua_getinfo(L, "Sl", &ar);
+ /* basename would be better, but basename needs memory
+ allocation, since it might modify the buffer contents,
+ so we use this which is good enough */
+ if (ar.source[0] != '@')
+ ptr = "<non-file location>";
+ else if ((ptr = strrchr(ar.source, '/')) == NULL)
+ ptr = ar.source;
+ else
+ ptr++;
+ *file_r = ptr;
+ *line_r = ar.currentline;
+}
+
+static struct event_passthrough *
+dlua_check_event_passthrough(lua_State *L, int arg)
+{
+ if (!lua_istable(L, arg)) {
+ (void)luaL_error(L, "Bad argument #%d, expected %s got %s",
+ arg, DLUA_EVENT,
+ lua_typename(L, lua_type(L, arg)));
+ }
+ lua_pushliteral(L, "item");
+ lua_rawget(L, arg);
+ void *bp = (void*)lua_touserdata(L, -1);
+ lua_pop(L, 1);
+ return (struct event_passthrough*)bp;
+}
+
+static void dlua_push_event_passthrough(lua_State *L,
+ struct event_passthrough *event)
+{
+ luaL_checkstack(L, 3, "out of memory");
+ lua_createtable(L, 0, 1);
+ luaL_setmetatable(L, DLUA_EVENT_PASSTHROUGH);
+
+ lua_pushlightuserdata(L, event);
+ lua_setfield(L, -2, "item");
+}
+
+static int dlua_event_pt_append_log_prefix(lua_State *L)
+{
+ DLUA_REQUIRE_ARGS(L, 2);
+ struct event_passthrough *event = dlua_check_event_passthrough(L, 1);
+ const char *prefix = luaL_checkstring(L, 2);
+
+ event->append_log_prefix(prefix);
+
+ lua_pushvalue(L, 1);
+
+ return 1;
+}
+
+static int dlua_event_pt_replace_log_prefix(lua_State *L)
+{
+ DLUA_REQUIRE_ARGS(L, 2);
+ struct event_passthrough *event = dlua_check_event_passthrough(L, 1);
+ const char *prefix = luaL_checkstring(L, 2);
+
+ event->replace_log_prefix(prefix);
+
+ lua_pushvalue(L, 1);
+
+ return 1;
+}
+
+static int dlua_event_pt_set_name(lua_State *L)
+{
+ DLUA_REQUIRE_ARGS(L, 2);
+ struct event_passthrough *event = dlua_check_event_passthrough(L, 1);
+ const char *name = luaL_checkstring(L, 2);
+
+ event->set_name(name);
+
+ lua_pushvalue(L, 1);
+
+ return 1;
+}
+
+
+static int dlua_event_pt_set_always_log_source(lua_State *L)
+{
+ DLUA_REQUIRE_ARGS(L, 1);
+ struct event_passthrough *event = dlua_check_event_passthrough(L, 1);
+
+ event->set_always_log_source();
+
+ lua_pushvalue(L, 1);
+
+ return 1;
+}
+
+static int dlua_event_pt_add_str(lua_State *L)
+{
+ DLUA_REQUIRE_ARGS(L, 3);
+ struct event_passthrough *event = dlua_check_event_passthrough(L, 1);
+ const char *name = luaL_checkstring(L, 2);
+ const char *value = luaL_checkstring(L, 3);
+
+ event->add_str(name, value);
+
+ lua_pushvalue(L, 1);
+
+ return 1;
+}
+
+static int dlua_event_pt_add_int(lua_State *L)
+{
+ DLUA_REQUIRE_ARGS(L, 3);
+ struct event_passthrough *event = dlua_check_event_passthrough(L, 1);
+ const char *name = luaL_checkstring(L, 2);
+ lua_Integer value = luaL_checkinteger(L, 3);
+
+ event->add_int(name, value);
+
+ lua_pushvalue(L, 1);
+
+ return 1;
+}
+
+static int dlua_event_pt_add_timeval(lua_State *L)
+{
+ DLUA_REQUIRE_ARGS(L, 3);
+ struct event_passthrough *event = dlua_check_event_passthrough(L, 1);
+ const char *name = luaL_checkstring(L, 2);
+ /* this is time in seconds */
+ lua_Integer value = luaL_checkinteger(L, 3);
+ struct timeval tv = {
+ .tv_sec = value,
+ };
+
+ event->add_timeval(name, &tv);
+
+ lua_pushvalue(L, 1);
+
+ return 1;
+}
+
+static int dlua_event_pt_inc_int(lua_State *L)
+{
+ DLUA_REQUIRE_ARGS(L, 3);
+ struct event_passthrough *event = dlua_check_event_passthrough(L, 1);
+ const char *name = luaL_checkstring(L, 2);
+ lua_Integer value = luaL_checkinteger(L, 3);
+
+ event->inc_int(name, value);
+
+ lua_pushvalue(L, 1);
+
+ return 1;
+}
+
+static int dlua_event_pt_log_debug(lua_State *L)
+{
+ DLUA_REQUIRE_ARGS(L, 2);
+ struct event_passthrough *event = dlua_check_event_passthrough(L, 1);
+ const char *str = luaL_checkstring(L, 2);
+
+ dlua_event_log(L, event->event(), LOG_TYPE_DEBUG, str);
+
+ lua_pushvalue(L, 1);
+
+ return 1;
+}
+
+static int dlua_event_pt_log_info(lua_State *L)
+{
+ DLUA_REQUIRE_ARGS(L, 2);
+ struct event_passthrough *event = dlua_check_event_passthrough(L, 1);
+ const char *str = luaL_checkstring(L, 2);
+
+ dlua_event_log(L, event->event(), LOG_TYPE_INFO, str);
+
+ lua_pushvalue(L, 1);
+
+ return 1;
+}
+
+static int dlua_event_pt_log_warning(lua_State *L)
+{
+ DLUA_REQUIRE_ARGS(L, 2);
+ struct event_passthrough *event = dlua_check_event_passthrough(L, 1);
+ const char *str = luaL_checkstring(L, 2);
+
+ dlua_event_log(L, event->event(), LOG_TYPE_WARNING, str);
+
+ lua_pushvalue(L, 1);
+
+ return 1;
+}
+
+static int dlua_event_pt_log_error(lua_State *L)
+{
+ DLUA_REQUIRE_ARGS(L, 2);
+ struct event_passthrough *event = dlua_check_event_passthrough(L, 1);
+ const char *str = luaL_checkstring(L, 2);
+
+ dlua_event_log(L, event->event(), LOG_TYPE_ERROR, str);
+
+ lua_pushvalue(L, 1);
+
+ return 1;
+}
+
+static const luaL_Reg event_passthrough_methods[] ={
+ { "append_log_prefix", dlua_event_pt_append_log_prefix },
+ { "replace_log_prefix", dlua_event_pt_replace_log_prefix },
+ { "set_always_log_source", dlua_event_pt_set_always_log_source },
+ { "set_name", dlua_event_pt_set_name },
+ { "add_str", dlua_event_pt_add_str },
+ { "add_int", dlua_event_pt_add_int },
+ { "add_timeval", dlua_event_pt_add_timeval },
+ { "inc_int", dlua_event_pt_inc_int },
+ { "log_debug", dlua_event_pt_log_debug },
+ { "log_info", dlua_event_pt_log_info },
+ { "log_warning", dlua_event_pt_log_warning },
+ { "log_error", dlua_event_pt_log_error },
+ { NULL, NULL }
+};
+
+static int dlua_event_gc(lua_State *L)
+{
+ struct event **event = lua_touserdata(L, 1);
+ event_unref(event);
+ return 0;
+}
+
+struct event *dlua_check_event(lua_State *L, int arg)
+{
+ if (!lua_istable(L, arg)) {
+ (void)luaL_error(L, "Bad argument #%d, expected %s got %s",
+ arg, DLUA_EVENT,
+ lua_typename(L, lua_type(L, arg)));
+ }
+ lua_pushliteral(L, "item");
+ lua_rawget(L, arg);
+ struct event **bp = (void*)lua_touserdata(L, -1);
+ lua_pop(L, 1);
+ return *bp;
+}
+
+void dlua_push_event(lua_State *L, struct event *event)
+{
+ luaL_checkstack(L, 3, "out of memory");
+ lua_createtable(L, 0, 1);
+ luaL_setmetatable(L, DLUA_EVENT);
+
+ /* we need to attach gc to userdata to support older lua*/
+ struct event **ptr = lua_newuserdata(L, sizeof(struct event*));
+ *ptr = event;
+ event_ref(event);
+ lua_createtable(L, 0, 1);
+ lua_pushcfunction(L, dlua_event_gc);
+ lua_setfield(L, -2, "__gc");
+ lua_setmetatable(L, -2);
+ lua_setfield(L, -2, "item");
+}
+
+static int dlua_event_append_log_prefix(lua_State *L)
+{
+ DLUA_REQUIRE_ARGS(L, 2);
+ struct event *event = dlua_check_event(L, 1);
+ const char *prefix = luaL_checkstring(L, 2);
+
+ event_set_append_log_prefix(event, prefix);
+
+ lua_pushvalue(L, 1);
+
+ return 1;
+}
+
+static int dlua_event_replace_log_prefix(lua_State *L)
+{
+ DLUA_REQUIRE_ARGS(L, 2);
+ struct event *event = dlua_check_event(L, 1);
+ const char *prefix = luaL_checkstring(L, 2);
+
+ event_replace_log_prefix(event, prefix);
+
+ lua_pushvalue(L, 1);
+
+ return 1;
+}
+
+static int dlua_event_set_name(lua_State *L)
+{
+ DLUA_REQUIRE_ARGS(L, 2);
+ struct event *event = dlua_check_event(L, 1);
+ const char *name = luaL_checkstring(L, 2);
+
+ event_set_name(event, name);
+
+ lua_pushvalue(L, 1);
+
+ return 1;
+}
+
+
+static int dlua_event_set_always_log_source(lua_State *L)
+{
+ DLUA_REQUIRE_ARGS(L, 1);
+ struct event *event = dlua_check_event(L, 1);
+
+ event_set_always_log_source(event);
+
+ lua_pushvalue(L, 1);
+
+ return 1;
+}
+
+static int dlua_event_add_str(lua_State *L)
+{
+ DLUA_REQUIRE_ARGS(L, 3);
+ struct event *event = dlua_check_event(L, 1);
+ const char *name = luaL_checkstring(L, 2);
+ const char *value = luaL_checkstring(L, 3);
+
+ event_add_str(event, name, value);
+
+ lua_pushvalue(L, 1);
+
+ return 1;
+}
+
+static int dlua_event_add_int(lua_State *L)
+{
+ DLUA_REQUIRE_ARGS(L, 3);
+ struct event *event = dlua_check_event(L, 1);
+ const char *name = luaL_checkstring(L, 2);
+ lua_Integer value = luaL_checkinteger(L, 3);
+
+ event_add_int(event, name, value);
+
+ lua_pushvalue(L, 1);
+
+ return 1;
+}
+
+static int dlua_event_add_timeval(lua_State *L)
+{
+ DLUA_REQUIRE_ARGS(L, 3);
+ struct event *event = dlua_check_event(L, 1);
+ const char *name = luaL_checkstring(L, 2);
+ /* this is time in seconds */
+ lua_Integer value = luaL_checkinteger(L, 3);
+ struct timeval tv = {
+ .tv_sec = value,
+ };
+
+ event_add_timeval(event, name, &tv);
+
+ lua_pushvalue(L, 1);
+
+ return 1;
+}
+
+static int dlua_event_inc_int(lua_State *L)
+{
+ DLUA_REQUIRE_ARGS(L, 3);
+ struct event *event = dlua_check_event(L, 1);
+ const char *name = luaL_checkstring(L, 2);
+ lua_Integer value = luaL_checkinteger(L, 3);
+
+ event_inc_int(event, name, value);
+
+ lua_pushvalue(L, 1);
+
+ return 1;
+}
+
+static int dlua_event_log_debug(lua_State *L)
+{
+ DLUA_REQUIRE_ARGS(L, 2);
+ struct event *event = dlua_check_event(L, 1);
+ const char *str = luaL_checkstring(L, 2);
+
+ dlua_event_log(L, event, LOG_TYPE_DEBUG, str);
+
+ lua_pushvalue(L, 1);
+
+ return 1;
+}
+
+static int dlua_event_log_info(lua_State *L)
+{
+ DLUA_REQUIRE_ARGS(L, 2);
+ struct event *event = dlua_check_event(L, 1);
+ const char *str = luaL_checkstring(L, 2);
+
+ dlua_event_log(L, event, LOG_TYPE_INFO, str);
+
+ lua_pushvalue(L, 1);
+
+ return 1;
+}
+
+static int dlua_event_log_warning(lua_State *L)
+{
+ DLUA_REQUIRE_ARGS(L, 2);
+ struct event *event = dlua_check_event(L, 1);
+ const char *str = luaL_checkstring(L, 2);
+
+ dlua_event_log(L, event, LOG_TYPE_WARNING, str);
+
+ lua_pushvalue(L, 1);
+
+ return 1;
+}
+
+static int dlua_event_log_error(lua_State *L)
+{
+ DLUA_REQUIRE_ARGS(L, 2);
+ struct event *event = dlua_check_event(L, 1);
+ const char *str = luaL_checkstring(L, 2);
+
+ dlua_event_log(L, event, LOG_TYPE_ERROR, str);
+
+ lua_pushvalue(L, 1);
+
+ return 1;
+}
+
+#undef event_create_passthrough
+#undef event_create
+static int dlua_event_passthrough_event(lua_State *L)
+{
+ DLUA_REQUIRE_ARGS(L, 1);
+ struct event *event = dlua_check_event(L, 1);
+ const char *file;
+ unsigned int line;
+
+ dlua_get_file_line(L, 1, &file, &line);
+ struct event_passthrough *e =
+ event_create_passthrough(event, file, line);
+ dlua_push_event_passthrough(L, e);
+
+ return 1;
+}
+
+static int dlua_event_new(lua_State *L)
+{
+ struct dlua_script *script = dlua_script_from_state(L);
+ DLUA_REQUIRE_ARGS_IN(L, 0, 1);
+ struct event *event, *parent = script->event;
+ const char *file;
+ unsigned int line;
+
+ if (lua_gettop(L) == 1)
+ parent = dlua_check_event(L, 1);
+ dlua_get_file_line(L, 1, &file, &line);
+ event = event_create(parent, file, line);
+ dlua_push_event(L, event);
+ event_unref(&event);
+ return 1;
+}
+
+static const luaL_Reg event_methods[] ={
+ { "append_log_prefix", dlua_event_append_log_prefix },
+ { "replace_log_prefix", dlua_event_replace_log_prefix },
+ { "set_always_log_source", dlua_event_set_always_log_source },
+ { "set_name", dlua_event_set_name },
+ { "add_str", dlua_event_add_str },
+ { "add_int", dlua_event_add_int },
+ { "add_timeval", dlua_event_add_timeval },
+ { "inc_int", dlua_event_inc_int },
+ { "log_debug", dlua_event_log_debug },
+ { "log_info", dlua_event_log_info },
+ { "log_warning", dlua_event_log_warning },
+ { "log_error", dlua_event_log_error },
+ { "passthrough_event", dlua_event_passthrough_event },
+ { NULL, NULL }
+};
+
+static void dlua_event_register(struct dlua_script *script){
+ i_assert(script != NULL);
+
+ luaL_newmetatable(script->L, DLUA_EVENT_PASSTHROUGH);
+ lua_pushvalue(script->L, -1);
+ lua_setfield(script->L, -2, "__index");
+ luaL_setfuncs(script->L, event_passthrough_methods, 0);
+ lua_pop(script->L, 1);
+
+ luaL_newmetatable(script->L, DLUA_EVENT);
+ lua_pushvalue(script->L, -1);
+ lua_setfield(script->L, -2, "__index");
+ luaL_setfuncs(script->L, event_methods, 0);
+ lua_pop(script->L, 1);
+}
+
+static int dlua_i_debug(lua_State *L)
+{
+ DLUA_REQUIRE_ARGS(L, 1);
+ const char *msg = luaL_checkstring(L, 1);
+ i_debug("%s", msg);
+ return 0;
+}
+
+static int dlua_i_info(lua_State *L)
+{
+ DLUA_REQUIRE_ARGS(L, 1);
+ const char *msg = luaL_checkstring(L, 1);
+ i_info("%s", msg);
+ return 0;
+}
+
+static int dlua_i_warning(lua_State *L)
+{
+ DLUA_REQUIRE_ARGS(L, 1);
+ const char *msg = luaL_checkstring(L, 1);
+ i_warning("%s", msg);
+ return 0;
+}
+
+static int dlua_i_error(lua_State *L)
+{
+ DLUA_REQUIRE_ARGS(L, 1);
+ const char *msg = luaL_checkstring(L, 1);
+ i_error("%s", msg);
+ return 0;
+}
+
+static int dlua_has_flag(lua_State *L)
+{
+ DLUA_REQUIRE_ARGS(L, 2);
+ /* we rather deal with unsigned value here */
+ lua_Integer value = luaL_checkinteger(L, 1);
+ lua_Integer flag = luaL_checkinteger(L, 2);
+
+ lua_pushboolean(L, (value & flag) == flag);
+ return 1;
+}
+
+static int dlua_set_flag(lua_State *L)
+{
+ DLUA_REQUIRE_ARGS(L, 2);
+ lua_Integer value = luaL_checkinteger(L, 1);
+ lua_Integer flag = luaL_checkinteger(L, 2);
+
+ lua_pushinteger(L, value | flag);
+ return 1;
+}
+
+static int dlua_clear_flag(lua_State *L)
+{
+ DLUA_REQUIRE_ARGS(L, 2);
+ lua_Integer value = luaL_checkinteger(L, 1);
+ lua_Integer flag = luaL_checkinteger(L, 2);
+
+ lua_pushinteger(L, value & (lua_Integer)~flag);
+ return 1;
+}
+
+static int dlua_script_strict_index(lua_State *L)
+{
+ DLUA_REQUIRE_ARGS(L, 2);
+ const char *name = luaL_checkstring(L, 2);
+ return luaL_error(L, "attempt to write to read undeclared global variable %s",
+ name);
+}
+
+static int dlua_script_strict_newindex(lua_State *L)
+{
+ DLUA_REQUIRE_ARGS(L, 3);
+ if (lua_type(L, 3) == LUA_TFUNCTION) {
+ /* allow defining global functions */
+ lua_rawset(L, 1);
+ } else {
+ const char *name = luaL_checkstring(L, 2);
+ return luaL_error(L, "attempt to write to undeclared global variable %s",
+ name);
+ }
+ return 0;
+}
+
+static luaL_Reg env_strict_metamethods[] = {
+ { "__index", dlua_script_strict_index },
+ { "__newindex", dlua_script_strict_newindex },
+ { NULL, NULL }
+};
+
+static int dlua_restrict_global_variables(lua_State *L)
+{
+ DLUA_REQUIRE_ARGS(L, 1);
+
+ if (lua_toboolean(L, 1)) {
+ /* disable defining global variables */
+ lua_getglobal(L, "_G");
+ lua_newtable(L);
+ luaL_setfuncs(L, env_strict_metamethods, 0);
+ } else {
+ /* revert restrictions */
+ lua_getglobal(L, "_G");
+ lua_newtable(L);
+ }
+ lua_setmetatable(L, -2);
+ lua_pop(L, 1);
+ return 0;
+}
+
+static luaL_Reg lua_dovecot_methods[] = {
+ { "i_debug", dlua_i_debug },
+ { "i_info", dlua_i_info },
+ { "i_warning", dlua_i_warning },
+ { "i_error", dlua_i_error },
+ { "event", dlua_event_new },
+ { "has_flag", dlua_has_flag },
+ { "set_flag", dlua_set_flag },
+ { "clear_flag", dlua_clear_flag },
+ { "restrict_global_variables", dlua_restrict_global_variables },
+ { NULL, NULL }
+};
+
+void dlua_get_dovecot(lua_State *L)
+{
+ lua_getglobal(L, LUA_SCRIPT_DOVECOT);
+}
+
+void dlua_dovecot_register(struct dlua_script *script)
+{
+ i_assert(script != NULL);
+
+ dlua_event_register(script);
+
+ /* Create table for holding values */
+ lua_newtable(script->L);
+
+ /* push new metatable to stack */
+ luaL_newmetatable(script->L, LUA_SCRIPT_DOVECOT);
+ /* this will register functions to the metatable itself */
+ luaL_setfuncs(script->L, lua_dovecot_methods, 0);
+ /* point __index to self */
+ lua_pushvalue(script->L, -1);
+ lua_setfield(script->L, -1, "__index");
+ /* set table's metatable, pops stack */
+ lua_setmetatable(script->L, -2);
+
+ /* register table as global */
+ lua_setglobal(script->L, LUA_SCRIPT_DOVECOT);
+
+ /* register http methods */
+ dlua_dovecot_http_register(script);
+}
+
+#undef event_want_level
+static void dlua_event_log(lua_State *L, struct event *event,
+ enum log_type log_type, const char *str)
+{
+ struct event_log_params parms;
+ i_zero(&parms);
+ parms.log_type = log_type;
+ dlua_get_file_line(L, 1, &parms.source_filename, &parms.source_linenum);
+ if (log_type != LOG_TYPE_DEBUG ||
+ event_want_level(event, LOG_TYPE_DEBUG, parms.source_filename,
+ parms.source_linenum)) {
+ event_log(event, &parms, "%s", str);
+ } else {
+ event_send_abort(event);
+ }
+}
diff --git a/src/lib-lua/dlua-error.c b/src/lib-lua/dlua-error.c
new file mode 100644
index 0000000..90011ed
--- /dev/null
+++ b/src/lib-lua/dlua-error.c
@@ -0,0 +1,13 @@
+/* Copyright (c) 2021 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "dlua-script-private.h"
+
+int dluaL_error(lua_State *L, const char *fmt, ...)
+{
+ va_list argp;
+ va_start(argp, fmt);
+ (void)dlua_push_vfstring(L, fmt, argp);
+ va_end(argp);
+ return lua_error(L);
+}
diff --git a/src/lib-lua/dlua-pushstring.c b/src/lib-lua/dlua-pushstring.c
new file mode 100644
index 0000000..436fd29
--- /dev/null
+++ b/src/lib-lua/dlua-pushstring.c
@@ -0,0 +1,26 @@
+/* Copyright (c) 2021 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "str.h"
+#include "dlua-script-private.h"
+
+const char *dlua_push_vfstring(lua_State *L, const char *fmt, va_list argp)
+{
+ const char *str;
+ T_BEGIN {
+ str = t_strdup_vprintf(fmt, argp);
+ (void)lua_pushstring(L, str);
+ str = lua_tostring(L, -1);
+ } T_END;
+ return str;
+}
+
+const char *dlua_push_fstring(lua_State *L, const char *fmt, ...)
+{
+ const char *str;
+ va_list argp;
+ va_start(argp, fmt);
+ str = dlua_push_vfstring(L, fmt, argp);
+ va_end(argp);
+ return str;
+}
diff --git a/src/lib-lua/dlua-resume.c b/src/lib-lua/dlua-resume.c
new file mode 100644
index 0000000..ac46f10
--- /dev/null
+++ b/src/lib-lua/dlua-resume.c
@@ -0,0 +1,208 @@
+/* Copyright (c) 2021 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "ioloop.h"
+#include "dlua-script-private.h"
+
+#define PCALL_RESUME_STATE "pcall-resume-state"
+
+#define RESUME_TIMEOUT "resume-timeout"
+#define RESUME_NARGS "resume-nargs"
+
+struct dlua_pcall_resume_state {
+ dlua_pcall_yieldable_callback_t *callback;
+ void *context;
+ struct timeout *to;
+ int status;
+};
+
+#ifdef DLUA_WITH_YIELDS
+static void call_resume_callback(lua_State *L)
+{
+ struct dlua_pcall_resume_state *state = dlua_tls_get_ptr(L, PCALL_RESUME_STATE);
+
+ timeout_remove(&state->to);
+
+ dlua_tls_clear(L, PCALL_RESUME_STATE);
+
+ state->callback(L, state->context, state->status);
+
+ i_free(state);
+}
+
+static void queue_resume_callback(lua_State *L, int status)
+{
+ struct dlua_pcall_resume_state *state = dlua_tls_get_ptr(L, PCALL_RESUME_STATE);
+
+ i_assert(status != LUA_YIELD);
+
+ if (status != LUA_OK) {
+ int ret;
+
+ /* error occured: run debug.traceback() */
+
+ /* stack: ..., error (top) */
+ lua_getglobal(L, "debug");
+
+ /* stack: ..., error, debug table (top) */
+ lua_getfield(L, -1, "traceback");
+
+ /* stack: ..., error, debug table, traceback function (top) */
+ lua_remove(L, -2);
+
+ /* stack: ..., error, traceback function (top) */
+ lua_pushvalue(L, -2); /* duplicate original error */
+
+ /* stack: ..., error, traceback function, error (top) */
+
+ /*
+ * Note that we kept the original error on the stack as well
+ * as passed it to debug.traceback(). The reason for that
+ * is that debug.traceback() itself can fail. If it fails,
+ * it'll generate its own error - which, ultimately, we
+ * don't care about. For example, consider the following
+ * function:
+ *
+ * function foo()
+ * debug.traceback = nil
+ * error("abc")
+ * end
+ *
+ * If we executed this function, it would error out - but
+ * it'd also cause our pcall to debug.traceback() to fail
+ * with "attempt to call a nil value". We want to discard
+ * the nil error, and just use the original ("abc"). This
+ * is ok because debug.traceback() simply "improves" the
+ * passed in error message to include a traceback and no
+ * traceback is better than a very mysterious error message.
+ */
+ ret = lua_pcall(L, 1, 1, 0);
+
+ /* stack: ..., orig error, traceback result/error (top) */
+
+ if (ret != LUA_OK) {
+ /* traceback failed, remove its error */
+ lua_remove(L, -1);
+ } else {
+ /* traceback succeeded, remove original error */
+ lua_remove(L, -2);
+ }
+ }
+
+ /*
+ * Mangle the passed in status to match dlua_pcall(). Namely, turn
+ * it into -1 on error, and 0+ to indicate the number of return
+ * values.
+ */
+ if (status == LUA_OK)
+ state->status = lua_gettop(L);
+ else
+ state->status = -1;
+
+ i_assert(state->to == NULL);
+ state->to = timeout_add_short(0, call_resume_callback, L);
+}
+
+static void dlua_pcall_yieldable_continue(lua_State *L)
+{
+ struct timeout *to;
+ int nargs, nresults;
+ int ret;
+
+ nargs = dlua_tls_get_int(L, RESUME_NARGS);
+ to = dlua_tls_get_ptr(L, RESUME_TIMEOUT);
+
+ timeout_remove(&to);
+
+ dlua_tls_clear(L, RESUME_TIMEOUT);
+ dlua_tls_clear(L, RESUME_NARGS);
+
+ ret = lua_resume(L, L, nargs, &nresults);
+ if (ret == LUA_YIELD) {
+ /*
+ * thread yielded - nothing to do
+ *
+ * We assume something will call lua_resume(). We don't
+ * care if it is a io related callback or just a timeout.
+ */
+ } else if (ret == LUA_OK) {
+ /* thread completed - invoke callback */
+ queue_resume_callback(L, ret);
+ } else {
+ /* error occurred - invoke callback */
+ queue_resume_callback(L, ret);
+ }
+}
+
+void dlua_pcall_yieldable_resume(lua_State *L, int nargs)
+{
+ struct timeout *to;
+
+ to = timeout_add_short(0, dlua_pcall_yieldable_continue, L);
+
+ dlua_tls_set_ptr(L, RESUME_TIMEOUT, to);
+ dlua_tls_set_int(L, RESUME_NARGS, nargs);
+}
+
+/*
+ * Call a function with nargs arguments in a way that supports yielding.
+ * When the function execution completes, the passed in callback is called.
+ *
+ * Returns -1 on error or 0 on success.
+ */
+#undef dlua_pcall_yieldable
+int dlua_pcall_yieldable(lua_State *L, const char *func_name, int nargs,
+ dlua_pcall_yieldable_callback_t *callback,
+ void *context, const char **error_r)
+{
+ struct dlua_pcall_resume_state *state;
+ int ret;
+ int nresults;
+
+ i_assert(lua_status(L) == LUA_OK);
+
+ lua_getglobal(L, func_name);
+
+ if (!lua_isfunction(L, -1)) {
+ /* clean up the stack - function + arguments */
+ lua_pop(L, nargs + 1);
+ *error_r = t_strdup_printf("'%s' is not a function", func_name);
+ return -1;
+ }
+
+ /* allocate and stash in TLS callback state */
+ state = i_new(struct dlua_pcall_resume_state, 1);
+ state->callback = callback;
+ state->context = context;
+
+ dlua_tls_set_ptr(L, PCALL_RESUME_STATE, state);
+
+ /* stack: args, func (top) */
+ lua_insert(L, -(nargs + 1));
+
+ /* stack: func, args (top) */
+ ret = lua_resume(L, L, nargs, &nresults);
+ if (ret == LUA_YIELD) {
+ /*
+ * thread yielded - nothing to do
+ *
+ * We assume something will call lua_resume(). We don't
+ * care if it is a io related callback or just a timeout.
+ */
+ } else {
+ /*
+ * thread completed / errored
+ *
+ * Since there is nothing that will come back to this lua
+ * thread, we need to make sure the callback is called.
+ *
+ * We handle errors the same as successful completion in
+ * order to avoid forcing the callers to check for lua
+ * errors in two places - the call here and in the callback.
+ */
+ queue_resume_callback(L, ret);
+ }
+
+ return 0;
+}
+#endif
diff --git a/src/lib-lua/dlua-script-private.h b/src/lib-lua/dlua-script-private.h
new file mode 100644
index 0000000..e6c8273
--- /dev/null
+++ b/src/lib-lua/dlua-script-private.h
@@ -0,0 +1,264 @@
+#ifndef LUA_SCRIPT_PRIVATE_H
+#define LUA_SCRIPT_PRIVATE_H 1
+
+#include "dlua-script.h"
+#include "lualib.h"
+#include "lauxlib.h"
+#include "dlua-compat.h"
+
+/* consistency helpers */
+#define lua_isstring(L, n) (lua_isstring((L), (n)) == 1)
+#define lua_isnumber(L, n) (lua_isnumber((L), (n)) == 1)
+#define lua_toboolean(L, n) (lua_toboolean((L), (n)) == 1)
+#define lua_pushboolean(L, b) lua_pushboolean((L), (b) ? 1 : 0)
+#define lua_isinteger(L, n) (lua_isinteger((L), (n)) == 1)
+
+#define DLUA_TABLE_STRING(n, val) { .name = (n),\
+ .type = DLUA_TABLE_VALUE_STRING, .v.s = (val) }
+#define DLUA_TABLE_INTEGER(n, val) { .name = (n), \
+ .type = DLUA_TABLE_VALUE_INTEGER, .v.i = (val) }
+#define DLUA_TABLE_ENUM(n) { .name = #n, \
+ .type = DLUA_TABLE_VALUE_INTEGER, .v.i = (n) }
+#define DLUA_TABLE_DOUBLE(n, val) { .name = (n), \
+ .type = DLUA_TABLE_VALUE_DOUBLE, .v.d = (val) }
+#define DLUA_TABLE_BOOLEAN(n, val) { .name = (n), \
+ .type = DLUA_TABLE_VALUE_BOOLEAN, .v.b = (val) }
+#define DLUA_TABLE_NULL(n, s) { .name = (n), \
+ .type = DLUA_TABLE_VALUE_NULL }
+#define DLUA_TABLE_END { .name = NULL }
+
+#define DLUA_REQUIRE_ARGS_IN(L, x, y) \
+ STMT_START { \
+ if (lua_gettop(L) < (x) || lua_gettop(L) > (y)) { \
+ return luaL_error((L), "expected %d to %d arguments, got %d", \
+ (x), (y), lua_gettop(L)); \
+ } \
+ } STMT_END
+#define DLUA_REQUIRE_ARGS(L, x) \
+ STMT_START { \
+ if (lua_gettop(L) != (x)) { \
+ return luaL_error((L), "expected %d arguments, got %d", \
+ (x), lua_gettop(L)); \
+ } \
+ } STMT_END
+
+struct dlua_script {
+ struct dlua_script *prev,*next;
+ pool_t pool;
+
+ lua_State *L; /* base lua context */
+
+ struct event *event;
+ const char *filename;
+ struct istream *in;
+ ssize_t last_read;
+
+ int ref;
+ bool init:1;
+};
+
+enum dlua_table_value_type {
+ DLUA_TABLE_VALUE_STRING = 0,
+ DLUA_TABLE_VALUE_INTEGER,
+ DLUA_TABLE_VALUE_DOUBLE,
+ DLUA_TABLE_VALUE_BOOLEAN,
+ DLUA_TABLE_VALUE_NULL
+};
+
+struct dlua_table_values {
+ const char *name;
+ enum dlua_table_value_type type;
+ union {
+ const char *s;
+ ptrdiff_t i;
+ double d;
+ bool b;
+ } v;
+};
+
+typedef void dlua_pcall_yieldable_callback_t(lua_State *L, void *context, int status);
+
+extern struct event_category event_category_lua;
+
+/* assorted wrappers for lua_foo(), but operating on a struct dlua_script */
+void dlua_register(struct dlua_script *script, const char *name,
+ lua_CFunction f);
+
+/* Get dlua_script from lua_State */
+struct dlua_script *dlua_script_from_state(lua_State *L);
+
+/* register 'dovecot' global */
+void dlua_dovecot_register(struct dlua_script *script);
+
+/* push 'dovecot' global on top of stack */
+void dlua_get_dovecot(lua_State *L);
+
+/* register 'http' methods to 'dovecot' */
+void dlua_dovecot_http_register(struct dlua_script *script);
+
+/* assign values to table on idx */
+void dlua_set_members(lua_State *L, const struct dlua_table_values *values, int idx);
+
+/* push event to top of stack */
+void dlua_push_event(lua_State *L, struct event *event);
+
+/* get event from given stack position */
+struct event *dlua_check_event(lua_State *L, int arg);
+
+/* improved lua_pushfstring, can handle full C format support */
+const char *dlua_push_vfstring(lua_State *L, const char *fmt, va_list argp) ATTR_FORMAT(2, 0);
+const char *dlua_push_fstring(lua_State *L, const char *fmt, ...) ATTR_FORMAT(2, 3);
+
+/* improved luaL_error, can handle full C format support */
+int dluaL_error(lua_State *L, const char *fmt, ...) ATTR_FORMAT(2, 3);
+#define luaL_error(...) dluaL_error(__VA_ARGS__)
+
+/*
+ * Returns field from a Lua table
+ *
+ * There are different variants of these that allow for different key types
+ * and different value types. In general, the function name scheme is:
+ *
+ * dlua_table_get_<return type>_by_<key type>
+ *
+ * The _by_{str,int} variants use the supplied field value as the table key.
+ *
+ * The _by_thread variants use the current thread's thread object as the
+ * table key.
+ *
+ * Returns:
+ * -1 = incompatible value type
+ * 0 = nil or not found
+ * 1 = value found
+ */
+int dlua_table_get_luainteger_by_str(lua_State *L, int idx, const char *field, lua_Integer *value_r);
+int dlua_table_get_int_by_str(lua_State *L, int idx, const char *field, int *value_r);
+int dlua_table_get_intmax_by_str(lua_State *L, int idx, const char *field, intmax_t *value_r);
+int dlua_table_get_uint_by_str(lua_State *L, int idx, const char *field, unsigned int *value_r);
+int dlua_table_get_uintmax_by_str(lua_State *L, int idx, const char *field, uintmax_t *value_r);
+int dlua_table_get_number_by_str(lua_State *L, int idx, const char *field, lua_Number *value_r);
+int dlua_table_get_bool_by_str(lua_State *L, int idx, const char *field, bool *value_r);
+int dlua_table_get_string_by_str(lua_State *L, int idx, const char *field, const char **value_r);
+int dlua_table_get_data_by_str(lua_State *L, int idx, const char *field, const unsigned char **value_r, size_t *len_r);
+
+int dlua_table_get_luainteger_by_int(lua_State *L, int idx, lua_Integer field, lua_Integer *value_r);
+int dlua_table_get_int_by_int(lua_State *L, int idx, lua_Integer field, int *value_r);
+int dlua_table_get_intmax_by_int(lua_State *L, int idx, lua_Integer field, intmax_t *value_r);
+int dlua_table_get_uint_by_int(lua_State *L, int idx, lua_Integer field, unsigned int *value_r);
+int dlua_table_get_uintmax_by_int(lua_State *L, int idx, lua_Integer field, uintmax_t *value_r);
+int dlua_table_get_number_by_int(lua_State *L, int idx, lua_Integer field, lua_Number *value_r);
+int dlua_table_get_bool_by_int(lua_State *L, int idx, lua_Integer field, bool *value_r);
+int dlua_table_get_string_by_int(lua_State *L, int idx, lua_Integer field, const char **value_r);
+int dlua_table_get_data_by_int(lua_State *L, int idx, lua_Integer field, const unsigned char **value_r, size_t *len_r);
+
+int dlua_table_get_luainteger_by_thread(lua_State *L, int idx, lua_Integer *value_r);
+int dlua_table_get_int_by_thread(lua_State *L, int idx, int *value_r);
+int dlua_table_get_intmax_by_thread(lua_State *L, int idx, intmax_t *value_r);
+int dlua_table_get_uint_by_thread(lua_State *L, int idx, unsigned int *value_r);
+int dlua_table_get_uintmax_by_thread(lua_State *L, int idx, uintmax_t *value_r);
+int dlua_table_get_number_by_thread(lua_State *L, int idx, lua_Number *value_r);
+int dlua_table_get_bool_by_thread(lua_State *L, int idx, bool *value_r);
+int dlua_table_get_string_by_thread(lua_State *L, int idx, const char **value_r);
+int dlua_table_get_data_by_thread(lua_State *L, int idx, const unsigned char **value_r, size_t *len_r);
+
+/*
+ * Pushes onto the stack the value t[k], where t is the value at the given
+ * index and k is field argument. Unlike lua_gettable(), this function
+ * checks the type of the retrieved value against the passed in type.
+ * [-1,+0..1,e]
+ *
+ * There are different variants of these that allow for different key types.
+ * In general, the function name scheme is:
+ *
+ * dlua_table_get_by_<key type>
+ *
+ * The _by_{str,int} variants use the supplied field value as the table key.
+ *
+ * The _by_thread variants use the current thread's thread object as the
+ * table key.
+ *
+ * Returns:
+ * -1 = incompatible value type (nothing is pushed)
+ * 0 = nil or not found (nothing is pushed)
+ * 1 = value found (retrieved value is pushed to the top of the stack)
+ */
+int dlua_table_get_by_str(lua_State *L, int idx, int type, const char *field);
+int dlua_table_get_by_int(lua_State *L, int idx, int type, lua_Integer field);
+int dlua_table_get_by_thread(lua_State *L, int idx, int type);
+
+/* call a function in a script.
+
+ **NOTE**: This function works differently than lua_pcall:
+
+ return value:
+ -1 = error
+ 0+ = number of result(s)
+
+*/
+int dlua_pcall(lua_State *L, const char *func_name, int nargs, int nresults,
+ const char **error_r);
+
+/* dumps current stack as i_debug lines */
+void dlua_dump_stack(lua_State *L);
+
+/* Create new thread and keep track of it. */
+lua_State *dlua_script_new_thread(struct dlua_script *script);
+
+/* Close thread. */
+void dlua_script_close_thread(struct dlua_script *script, lua_State **_L);
+
+#ifdef DLUA_WITH_YIELDS
+/*
+ * Call a function with nargs in a way that supports yielding.
+ *
+ * When the specified function returns, the callback will be called with the
+ * supplied context pointer and a status integer indicating whether an error
+ * occurred (-1) or whether execution completed successfully (0+). In the
+ * case of a successful completion, the status will indicate the number of
+ * results returned by the function. On failure, the top of the stack
+ * contains the error object.
+ *
+ * Returns:
+ * -1 = if function name refers to a non-function type
+ * 0 = function called, callback will be called in the future
+ */
+int dlua_pcall_yieldable(lua_State *L, const char *func_name, int nargs,
+ dlua_pcall_yieldable_callback_t *callback,
+ void *context, const char **error_r);
+#define dlua_pcall_yieldable(L, func_name, nargs, callback, context, error_r) \
+ dlua_pcall_yieldable(L, TRUE ? func_name : \
+ CALLBACK_TYPECHECK(callback, void (*)(lua_State *, typeof(context), int)), \
+ nargs, (dlua_pcall_yieldable_callback_t *)callback, context, error_r)
+/*
+ * Resume yielded function execution.
+ *
+ * The nargs argument indicates how many items from the top of the stack
+ * should be "returned" by the yield.
+ *
+ * This function is to be called from other API callbacks to resume
+ * execution of the Lua script. For example, if a Lua script invokes a
+ * function to perform I/O, the function would start the async I/O and yield
+ * from the script. Eventually, the I/O completion callback executes, which
+ * would call dlua_pcall_yieldable_resume() to continue executing the Lua
+ * script with the supplied arguments.
+ *
+ * Note: The actual execution doesn't resume immediately. Rather, it is
+ * scheduled to start in the near future via a timeout.
+ */
+void dlua_pcall_yieldable_resume(lua_State *L, int nargs);
+#endif
+
+/* initialize/free script's thread table */
+void dlua_init_thread_table(struct dlua_script *script);
+void dlua_free_thread_table(struct dlua_script *script);
+
+/* thread local storage (TLS) getters & setters */
+void dlua_tls_set_ptr(lua_State *L, const char *name, void *ptr);
+void *dlua_tls_get_ptr(lua_State *L, const char *name);
+void dlua_tls_set_int(lua_State *L, const char *name, lua_Integer i);
+lua_Integer dlua_tls_get_int(lua_State *L, const char *name);
+
+/* free a thread local storage (TLS) value */
+void dlua_tls_clear(lua_State *L, const char *name);
+
+#endif
diff --git a/src/lib-lua/dlua-script.c b/src/lib-lua/dlua-script.c
new file mode 100644
index 0000000..48fe286
--- /dev/null
+++ b/src/lib-lua/dlua-script.c
@@ -0,0 +1,453 @@
+/* Copyright (c) 2017-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "llist.h"
+#include "istream.h"
+#include "sha1.h"
+#include "str.h"
+#include "hex-binary.h"
+#include "eacces-error.h"
+#include "ioloop.h"
+#include "dlua-script-private.h"
+
+#include <fcntl.h>
+#include <unistd.h>
+
+/* the registry entry with a pointer to struct dlua_script */
+#define LUA_SCRIPT_REGISTRY_KEY "DLUA_SCRIPT"
+
+#define LUA_SCRIPT_INIT_FN "script_init"
+#define LUA_SCRIPT_DEINIT_FN "script_deinit"
+
+struct event_category event_category_lua = {
+ .name = "lua",
+};
+
+static struct dlua_script *dlua_scripts = NULL;
+
+static int
+dlua_script_create_finish(struct dlua_script *script, const char **error_r);
+
+static void *dlua_alloc(void *ctx, void *ptr, size_t osize, size_t nsize)
+{
+ struct dlua_script *script =
+ (struct dlua_script*)ctx;
+
+ if (nsize == 0) {
+ p_free(script->pool, ptr);
+ return NULL;
+ } else {
+ return p_realloc(script->pool, ptr, osize, nsize);
+ }
+}
+
+static const char *dlua_reader(lua_State *L, void *ctx, size_t *size_r)
+{
+ struct dlua_script *script =
+ (struct dlua_script*)ctx;
+ const unsigned char *data;
+ i_stream_skip(script->in, script->last_read);
+ if (i_stream_read_more(script->in, &data, size_r) == -1 &&
+ script->in->stream_errno != 0) {
+ luaL_error(L, "read(%s) failed: %s",
+ script->filename,
+ i_stream_get_error(script->in));
+ *size_r = 0;
+ return NULL;
+ }
+ script->last_read = *size_r;
+ return (const char*)data;
+}
+
+struct dlua_script *dlua_script_from_state(lua_State *L)
+{
+ struct dlua_script *script;
+
+ /* get light pointer from globals */
+ lua_pushstring(L, LUA_SCRIPT_REGISTRY_KEY);
+ lua_gettable(L, LUA_REGISTRYINDEX);
+ script = lua_touserdata(L, -1);
+ lua_pop(L, 1);
+ i_assert(script != NULL);
+
+ return script;
+}
+
+int dlua_pcall(lua_State *L, const char *func_name, int nargs, int nresults,
+ const char **error_r)
+{
+ /* record the stack position */
+ int ret = 0, debugh_idx, top = lua_gettop(L) - nargs;
+
+ lua_getglobal(L, func_name);
+
+ if (lua_isfunction(L, -1)) {
+ /* stack on entry
+ args
+ func <-- top
+ */
+ /* move func name before arguments */
+ lua_insert(L, -(nargs + 1));
+ /* stack now
+ func
+ args <-- top
+ */
+ lua_getglobal(L, "debug");
+ lua_getfield(L, -1, "traceback");
+ lua_replace(L, -2);
+ /* stack now
+ func
+ args
+ traceback <-- top
+ */
+ /* move error handler before func name */
+ lua_insert(L, -(nargs + 2));
+ /* stack now
+ traceback
+ func
+ args <-- top
+ */
+ /* record where traceback is so it's easy to get rid of even
+ if LUA_MULTRET is used. */
+ debugh_idx = lua_gettop(L) - nargs - 1;
+ ret = lua_pcall(L, nargs, nresults, -(nargs + 2));
+ if (ret != LUA_OK) {
+ *error_r = t_strdup_printf("lua_pcall(%s, %d, %d) failed: %s",
+ func_name, nargs, nresults,
+ lua_tostring(L, -1));
+ /* Remove error and debug handler */
+ lua_pop(L, 2);
+ ret = -1;
+ } else {
+ /* remove debug handler from known location */
+ lua_remove(L, debugh_idx);
+ if (nresults == LUA_MULTRET)
+ nresults = lua_gettop(L) - top;
+ ret = nresults;
+ }
+ } else {
+ /* ensure stack is clean, remove function and arguments */
+ lua_pop(L, nargs + 1);
+ *error_r = t_strdup_printf("'%s' is not a function",
+ func_name);
+ ret = -1;
+ }
+#ifdef DEBUG
+ if ((ret == -1 && lua_gettop(L) != top) ||
+ (ret >= 0 &&
+ lua_gettop(L) != top + ret)) {
+ i_debug("LUA STACK UNCLEAN BEGIN for %s", func_name);
+ dlua_dump_stack(L);
+ i_debug("LUA STACK UNCLEAN END");
+ }
+#endif
+ /* enforce that stack is clean after call */
+ if (ret == -1)
+ i_assert(lua_gettop(L) == top);
+ else
+ i_assert(ret >= 0 && lua_gettop(L) == top + ret);
+ return ret;
+}
+
+static void dlua_call_deinit_function(struct dlua_script *script)
+{
+ const char *error;
+ if (!dlua_script_has_function(script, LUA_SCRIPT_DEINIT_FN))
+ return;
+ if (dlua_pcall(script->L, LUA_SCRIPT_DEINIT_FN, 0, 0, &error) < 0)
+ e_error(script->event, LUA_SCRIPT_DEINIT_FN"() failed: %s",
+ error);
+}
+
+int dlua_script_init(struct dlua_script *script, const char **error_r)
+{
+ if (script->init)
+ return 0;
+ script->init = TRUE;
+
+ if (dlua_script_create_finish(script, error_r) < 0)
+ return -1;
+
+ /* lets not fail on missing function... */
+ if (!dlua_script_has_function(script, LUA_SCRIPT_INIT_FN))
+ return 0;
+
+ int ret = 0;
+
+ if (dlua_pcall(script->L, LUA_SCRIPT_INIT_FN, 0, 1, error_r) < 0)
+ return -1;
+
+ if (lua_isinteger(script->L, -1)) {
+ ret = lua_tointeger(script->L, -1);
+ if (ret != 0) {
+ *error_r = "Script init failed";
+ ret = -1;
+ }
+ } else {
+ *error_r = LUA_SCRIPT_INIT_FN"() returned non-number";
+ ret = -1;
+ }
+
+ lua_pop(script->L, 1);
+
+ i_assert(lua_gettop(script->L) == 0);
+ return ret;
+}
+
+static int dlua_atpanic(lua_State *L)
+{
+ struct dlua_script *script = dlua_script_from_state(L);
+ const char *error = lua_tostring(script->L, -1);
+ i_panic("Lua script '%s': %s", script->filename, error);
+}
+
+static struct dlua_script *dlua_create_script(const char *name,
+ struct event *event_parent)
+{
+ pool_t pool = pool_allocfree_create(t_strdup_printf("lua script %s", name));
+ struct dlua_script *script = p_new(pool, struct dlua_script, 1);
+ script->pool = pool;
+ script->filename = p_strdup(pool, name);
+ /* lua API says that lua_newstate will return NULL only if it's out of
+ memory. this cannot really happen with our allocator as it will
+ call i_fatal_status anyways if it runs out of memory */
+ script->L = lua_newstate(dlua_alloc, script);
+ i_assert(script->L != NULL);
+ script->ref = 1;
+ lua_atpanic(script->L, dlua_atpanic);
+ luaL_openlibs(script->L);
+ script->event = event_create(event_parent);
+ event_add_str(script->event, "script", script->filename);
+ event_add_category(script->event, &event_category_lua);
+
+ dlua_init_thread_table(script);
+
+ DLLIST_PREPEND(&dlua_scripts, script);
+ return script;
+}
+
+static int dlua_run_script(struct dlua_script *script, const char **error_r)
+{
+ /* put the error handler before script being called */
+ lua_getglobal(script->L, "debug");
+ lua_getfield(script->L, -1, "traceback");
+ lua_replace(script->L, -2);
+ lua_insert(script->L, -2);
+
+ /* we don't want anything to be returned here */
+ /* stack before lua_pcall
+ debug.traceback
+ loaded script as function
+ */
+ int err = lua_pcall(script->L, 0, 0, 1);
+ if (err != LUA_OK) {
+ *error_r = t_strdup_printf("lua_pcall(%s) failed: %s",
+ script->filename,
+ lua_tostring(script->L, -1));
+ /* pop error and debug handler */
+ lua_pop(script->L, 2);
+ err = -1;
+ } else {
+ /* pop debug handler */
+ lua_pop(script->L, 1);
+ }
+ return err;
+}
+
+static int
+dlua_script_create_finish(struct dlua_script *script, const char **error_r)
+{
+ /* store pointer as light data to registry before calling the script */
+ lua_pushstring(script->L, LUA_SCRIPT_REGISTRY_KEY);
+ lua_pushlightuserdata(script->L, script);
+ lua_settable(script->L, LUA_REGISTRYINDEX);
+
+ if (dlua_run_script(script, error_r) < 0)
+ return -1;
+ i_assert(lua_gettop(script->L) == 0);
+ return 0;
+}
+
+int dlua_script_create_string(const char *str, struct dlua_script **script_r,
+ struct event *event_parent, const char **error_r)
+{
+ struct dlua_script *script;
+ unsigned char scripthash[SHA1_RESULTLEN];
+ const char *fn;
+
+ *script_r = NULL;
+ sha1_get_digest(str, strlen(str), scripthash);
+ fn = binary_to_hex(scripthash, sizeof(scripthash));
+
+ script = dlua_create_script(fn, event_parent);
+ if (luaL_loadstring(script->L, str) == LUA_OK) {
+ *script_r = script;
+ return 0;
+ }
+ *error_r = t_strdup_printf("lua_load(<string>) failed: %s",
+ lua_tostring(script->L, -1));
+ lua_pop(script->L, 1);
+ dlua_script_unref(&script);
+ return -1;
+}
+
+int dlua_script_create_file(const char *file, struct dlua_script **script_r,
+ struct event *event_parent, const char **error_r)
+{
+ struct dlua_script *script;
+
+ /* lua reports file access errors poorly */
+ if (access(file, O_RDONLY) < 0) {
+ if (errno == EACCES)
+ *error_r = eacces_error_get("access", file);
+ else
+ *error_r = t_strdup_printf("access(%s) failed: %m",
+ file);
+ return -1;
+ }
+
+ script = dlua_create_script(file, event_parent);
+ if (luaL_loadfile(script->L, file) != LUA_OK) {
+ *error_r = t_strdup_printf("lua_load(%s) failed: %s",
+ file, lua_tostring(script->L, -1));
+ dlua_script_unref(&script);
+ return -1;
+ }
+
+ *script_r = script;
+ return 0;
+}
+
+int dlua_script_create_stream(struct istream *is, struct dlua_script **script_r,
+ struct event *event_parent, const char **error_r)
+{
+ struct dlua_script *script;
+ const char *filename = i_stream_get_name(is);
+
+ i_assert(filename != NULL && *filename != '\0');
+
+ script = dlua_create_script(filename, event_parent);
+ script->in = is;
+ script->filename = p_strdup(script->pool, filename);
+ if (lua_load(script->L, dlua_reader, script, filename, 0) != LUA_OK) {
+ *error_r = t_strdup_printf("lua_load(%s) failed: %s",
+ filename, lua_tostring(script->L, -1));
+ dlua_script_unref(&script);
+ return -1;
+ }
+
+ *script_r = script;
+ return 0;
+}
+
+static void dlua_script_destroy(struct dlua_script *script)
+{
+ dlua_call_deinit_function(script);
+
+ /* close all threads */
+ dlua_free_thread_table(script);
+
+ /* close base lua */
+ lua_close(script->L);
+
+ /* remove from list */
+ DLLIST_REMOVE(&dlua_scripts, script);
+
+ event_unref(&script->event);
+ /* then just release memory */
+ pool_unref(&script->pool);
+}
+
+void dlua_script_ref(struct dlua_script *script)
+{
+ i_assert(script->ref > 0);
+ script->ref++;
+}
+
+void dlua_script_unref(struct dlua_script **_script)
+{
+ struct dlua_script *script = *_script;
+ *_script = NULL;
+
+ if (script == NULL) return;
+
+ i_assert(script->ref > 0);
+ if (--script->ref > 0)
+ return;
+
+ dlua_script_destroy(script);
+}
+
+bool dlua_script_has_function(struct dlua_script *script, const char *fn)
+{
+ i_assert(script != NULL);
+ lua_getglobal(script->L, "_G");
+ lua_pushstring(script->L, fn);
+ lua_rawget(script->L, -2);
+ bool ret = lua_isfunction(script->L, -1);
+ lua_pop(script->L, 2);
+ return ret;
+}
+
+void dlua_set_members(lua_State *L, const struct dlua_table_values *values,
+ int idx)
+{
+ i_assert(L != NULL);
+ i_assert(lua_istable(L, idx));
+ while(values->name != NULL) {
+ switch(values->type) {
+ case DLUA_TABLE_VALUE_STRING:
+ lua_pushstring(L, values->v.s);
+ break;
+ case DLUA_TABLE_VALUE_INTEGER:
+ lua_pushnumber(L, values->v.i);
+ break;
+ case DLUA_TABLE_VALUE_DOUBLE:
+ lua_pushnumber(L, values->v.d);
+ break;
+ case DLUA_TABLE_VALUE_BOOLEAN:
+ lua_pushboolean(L, values->v.b);
+ break;
+ case DLUA_TABLE_VALUE_NULL:
+ lua_pushnil(L);
+ break;
+ default:
+ i_unreached();
+ }
+ lua_setfield(L, idx - 1, values->name);
+ values++;
+ }
+}
+
+void dlua_dump_stack(lua_State *L)
+{
+ /* get everything in stack */
+ int top = lua_gettop(L);
+ for (int i = 1; i <= top; i++) T_BEGIN { /* repeat for each level */
+ int t = lua_type(L, i);
+ string_t *line = t_str_new(32);
+ str_printfa(line, "#%d: ", i);
+ switch (t) {
+ case LUA_TSTRING: /* strings */
+ str_printfa(line, "`%s'", lua_tostring(L, i));
+ break;
+ case LUA_TBOOLEAN: /* booleans */
+ str_printfa(line, "`%s'", lua_toboolean(L, i) ? "true" : "false");
+ break;
+ case LUA_TNUMBER: /* numbers */
+ str_printfa(line, "%g", lua_tonumber(L, i));
+ break;
+ default: /* other values */
+ str_printfa(line, "%s", lua_typename(L, t));
+ break;
+ }
+ i_debug("%s", str_c(line));
+ } T_END;
+}
+
+/* assorted wrappers */
+void dlua_register(struct dlua_script *script, const char *name,
+ lua_CFunction f)
+{
+ lua_register(script->L, name, f);
+}
diff --git a/src/lib-lua/dlua-script.h b/src/lib-lua/dlua-script.h
new file mode 100644
index 0000000..015475b
--- /dev/null
+++ b/src/lib-lua/dlua-script.h
@@ -0,0 +1,28 @@
+#ifndef LUA_SCRIPT_H
+#define LUA_SCRIPT_H 1
+
+struct dlua_script;
+
+/* Parse and load a lua script, without actually running it. */
+int dlua_script_create_string(const char *str, struct dlua_script **script_r,
+ struct event *event_parent, const char **error_r);
+int dlua_script_create_file(const char *file, struct dlua_script **script_r,
+ struct event *event_parent, const char **error_r);
+/* Remember to set script name using i_stream_set_name */
+int dlua_script_create_stream(struct istream *is, struct dlua_script **script_r,
+ struct event *event_parent, const char **error_r);
+
+/* run dlua_script_init function */
+int dlua_script_init(struct dlua_script *script, const char **error_r);
+
+/* Reference lua script */
+void dlua_script_ref(struct dlua_script *script);
+
+/* Unreference a script, calls deinit and frees when no more
+ references exist */
+void dlua_script_unref(struct dlua_script **_script);
+
+/* see if particular function is registered */
+bool dlua_script_has_function(struct dlua_script *script, const char *fn);
+
+#endif
diff --git a/src/lib-lua/dlua-table.c b/src/lib-lua/dlua-table.c
new file mode 100644
index 0000000..8467a7e
--- /dev/null
+++ b/src/lib-lua/dlua-table.c
@@ -0,0 +1,301 @@
+/* Copyright (c) 2021 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "dlua-script-private.h"
+
+/*
+ * Adjust the index by the specified delta.
+ *
+ * In a couple of places we need to adjust the passed in index to reflect
+ * additional items pushed onto the stack. We cannot blindly adjust the
+ * index because the index could be one of three things and only one of them
+ * is supposed to be ajusted:
+ *
+ * 1. negative number: index relative to top of stack, adjust
+ * 2. positive number: absolute index, don't adjust
+ * 3. special registry index: don't adjust
+ */
+static inline int adj(int idx, int delta)
+{
+ if ((idx == LUA_REGISTRYINDEX) || (idx > 0))
+ return idx;
+ else
+ return idx - delta;
+}
+
+/*
+ * Pushes onto the stack the value t[k], where t is the value at the given
+ * index and k is the value at the top of the stack. Unlike lua_gettable(),
+ * this function checks the type of the retreived value against the passed
+ * in type. [-1,+0..1,e]
+ *
+ * Return value:
+ * -1 = incompatible type
+ * 0 = nil or none
+ * 1 = found
+ */
+static int dlua_table_get(lua_State *L, int idx, int type)
+{
+ /* can only work with tables */
+ if (!lua_istable(L, idx))
+ return -1;
+
+ lua_gettable(L, idx);
+
+ /* check if the field was there */
+ if (lua_isnoneornil(L, -1)) {
+ lua_pop(L, 1);
+ return 0;
+ }
+
+ /* check that the field is the expected type */
+ if (lua_type(L, -1) != type) {
+ lua_pop(L, 1);
+ return -1;
+ }
+
+ return 1;
+}
+
+/* Get by string name [-0,+1,e] */
+int dlua_table_get_by_str(lua_State *L, int idx, int type, const char *field)
+{
+ /* push the key */
+ lua_pushstring(L, field);
+
+ return dlua_table_get(L, adj(idx, 1), type);
+}
+
+/* Get by int name [-0,+1,e] */
+int dlua_table_get_by_int(lua_State *L, int idx, int type, lua_Integer field)
+{
+ /* push the key */
+ lua_pushinteger(L, field);
+
+ return dlua_table_get(L, adj(idx, 1), type);
+}
+
+/* Get by thread [-0,+1,e] */
+int dlua_table_get_by_thread(lua_State *L, int idx, int type)
+{
+ /* push the key */
+ lua_pushthread(L);
+
+ return dlua_table_get(L, adj(idx, 1), type);
+}
+
+/* generate a set of functions to access fields of an integral data type */
+#define GET_INTTYPE(fxn, ctype, minval, maxval, unsigned_check) \
+int fxn##_by_str(lua_State *L, int idx, const char *field, \
+ ctype *value_r) \
+{ \
+ lua_Integer tmp; \
+ int ret; \
+ \
+ ret = dlua_table_get_luainteger_by_str(L, idx, field, &tmp); \
+ if (ret < 1) \
+ return ret; \
+ \
+ if (unsigned_check) { \
+ if ((tmp < 0) || (((uintmax_t) tmp) > (maxval))) \
+ return -1; \
+ } else { \
+ if ((tmp < (minval)) || (tmp > (intmax_t) (maxval))) \
+ return -1; \
+ } \
+ \
+ *value_r = (ctype) tmp; \
+ \
+ return 1; \
+} \
+int fxn##_by_int(lua_State *L, int idx, lua_Integer field, \
+ ctype *value_r) \
+{ \
+ lua_Integer tmp; \
+ int ret; \
+ \
+ ret = dlua_table_get_luainteger_by_int(L, idx, field, &tmp); \
+ if (ret < 1) \
+ return ret; \
+ \
+ if (unsigned_check) { \
+ if ((tmp < 0) || (((uintmax_t) tmp) > (maxval))) \
+ return -1; \
+ } else { \
+ if ((tmp < (minval)) || (tmp > (intmax_t) (maxval))) \
+ return -1; \
+ } \
+ \
+ *value_r = (ctype) tmp; \
+ \
+ return 1; \
+} \
+int fxn##_by_thread(lua_State *L, int idx, ctype *value_r) \
+{ \
+ lua_Integer tmp; \
+ int ret; \
+ \
+ ret = dlua_table_get_luainteger_by_thread(L, idx, &tmp); \
+ if (ret < 1) \
+ return ret; \
+ \
+ if (unsigned_check) { \
+ if ((tmp < 0) || (((uintmax_t) tmp) > (maxval))) \
+ return -1; \
+ } else { \
+ if ((tmp < (minval)) || (tmp > (intmax_t) (maxval))) \
+ return -1; \
+ } \
+ \
+ *value_r = (ctype) tmp; \
+ \
+ return 1; \
+}
+
+/* generate a set of functions to access fields of a binary data type */
+#define GET_DATAPTR(fxn) \
+int fxn##_by_str(lua_State *L, int idx, const char *field, \
+ const unsigned char **value_r, size_t *len_r) \
+{ \
+ int ret; \
+ \
+ ret = dlua_table_get_by_str(L, idx, LUA_TSTRING, field); \
+ if (ret < 1) \
+ return ret; \
+ \
+ *value_r = (const unsigned char *) lua_tolstring(L, -1, len_r); \
+ lua_pop(L, 1); \
+ \
+ return 1; \
+} \
+int fxn##_by_int(lua_State *L, int idx, lua_Integer field, \
+ const unsigned char **value_r, size_t *len_r) \
+{ \
+ int ret; \
+ \
+ ret = dlua_table_get_by_int(L, idx, LUA_TSTRING, field); \
+ if (ret < 1) \
+ return ret; \
+ \
+ *value_r = (const unsigned char *) lua_tolstring(L, -1, len_r); \
+ lua_pop(L, 1); \
+ \
+ return 1; \
+} \
+int fxn##_by_thread(lua_State *L, int idx, \
+ const unsigned char **value_r, size_t *len_r) \
+{ \
+ int ret; \
+ \
+ ret = dlua_table_get_by_thread(L, idx, LUA_TSTRING); \
+ if (ret < 1) \
+ return ret; \
+ \
+ *value_r = (const unsigned char *) lua_tolstring(L, -1, len_r); \
+ lua_pop(L, 1); \
+ \
+ return 1; \
+}
+
+/* generate a set of functions to access fields of a generic-ish type */
+#define GET_GENERIC(fxn, ctype, ltype, cvt) \
+int fxn##_by_str(lua_State *L, int idx, const char *field, ctype *value_r)\
+{ \
+ int ret; \
+ \
+ ret = dlua_table_get_by_str(L, idx, (ltype), field); \
+ if (ret < 1) \
+ return ret; \
+ \
+ *value_r = cvt(L, -1); \
+ lua_pop(L, 1); \
+ \
+ return 1; \
+} \
+int fxn##_by_int(lua_State *L, int idx, lua_Integer field, ctype *value_r)\
+{ \
+ int ret; \
+ \
+ ret = dlua_table_get_by_int(L, idx, (ltype), field); \
+ if (ret < 1) \
+ return ret; \
+ \
+ *value_r = cvt(L, -1); \
+ lua_pop(L, 1); \
+ \
+ return 1; \
+} \
+int fxn##_by_thread(lua_State *L, int idx, ctype *value_r) \
+{ \
+ int ret; \
+ \
+ ret = dlua_table_get_by_thread(L, idx, (ltype)); \
+ if (ret < 1) \
+ return ret; \
+ \
+ *value_r = cvt(L, -1); \
+ lua_pop(L, 1); \
+ \
+ return 1; \
+}
+
+GET_INTTYPE(dlua_table_get_int, int, INT_MIN, INT_MAX, FALSE);
+GET_INTTYPE(dlua_table_get_intmax, intmax_t, INTMAX_MIN, INTMAX_MAX, FALSE);
+GET_INTTYPE(dlua_table_get_uint, unsigned int, 0, UINT_MAX, TRUE);
+GET_INTTYPE(dlua_table_get_uintmax, uintmax_t, 0, UINTMAX_MAX, TRUE);
+
+/* we need to use lua_tointegerx which takes an extra argument */
+int dlua_table_get_luainteger_by_str(lua_State *L, int idx, const char *field,
+ lua_Integer *value_r)
+{
+ int isnum;
+ int ret;
+
+ ret = dlua_table_get_by_str(L, idx, LUA_TNUMBER, field);
+ if (ret < 1)
+ return ret;
+
+ *value_r = lua_tointegerx(L, -1, &isnum);
+ lua_pop(L, 1);
+
+ return (isnum == 1) ? 1 : -1;
+}
+
+/* we need to use lua_tointegerx which takes an extra argument */
+int dlua_table_get_luainteger_by_int(lua_State *L, int idx, lua_Integer field,
+ lua_Integer *value_r)
+{
+ int isnum;
+ int ret;
+
+ ret = dlua_table_get_by_int(L, idx, LUA_TNUMBER, field);
+ if (ret < 1)
+ return ret;
+
+ *value_r = lua_tointegerx(L, -1, &isnum);
+ lua_pop(L, 1);
+
+ return (isnum == 1) ? 1 : -1;
+}
+
+/* we need to use lua_tointegerx which takes an extra argument */
+int dlua_table_get_luainteger_by_thread(lua_State *L, int idx,
+ lua_Integer *value_r)
+{
+ int isnum;
+ int ret;
+
+ ret = dlua_table_get_by_thread(L, idx, LUA_TNUMBER);
+ if (ret < 1)
+ return ret;
+
+ *value_r = lua_tointegerx(L, -1, &isnum);
+ lua_pop(L, 1);
+
+ return (isnum == 1) ? 1 : -1;
+}
+
+GET_GENERIC(dlua_table_get_number, lua_Number, LUA_TNUMBER, lua_tonumber);
+GET_GENERIC(dlua_table_get_bool, bool, LUA_TBOOLEAN, lua_toboolean);
+GET_GENERIC(dlua_table_get_string, const char *, LUA_TSTRING, lua_tostring);
+GET_DATAPTR(dlua_table_get_data);
diff --git a/src/lib-lua/dlua-thread.c b/src/lib-lua/dlua-thread.c
new file mode 100644
index 0000000..a0441f8
--- /dev/null
+++ b/src/lib-lua/dlua-thread.c
@@ -0,0 +1,276 @@
+/* Copyright (c) 2021 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "dlua-script-private.h"
+
+/*
+ * dlua support for threads & thread local storage (TLS)
+ *
+ * The following code keeps a table in (global) registry. This table is
+ * indexed by the thread objects, each mapping to another table used to hold
+ * thread local storage. That is:
+ *
+ * registry[thread] = {} -- threads table
+ * registry[thread]["foo"] = ... -- TLS value for "foo"
+ *
+ * This serves two purposes:
+ *
+ * (1) It provides TLS.
+ * (2) It acts as a reference to the thread object, preventing it from
+ * being garbage collected.
+ *
+ * The table is allocated during struct dlua_script's creation and is freed
+ * during the scripts destruction. Any lua threads created using
+ * dlua_script_new_thread() will automatically get added to this table.
+ */
+
+/* the registry entry with a table with all the lua threads */
+#define LUA_THREAD_REGISTRY_KEY "DLUA_THREADS"
+
+static void warn_about_tls_leaks(struct dlua_script *script, lua_State *L);
+static void get_tls_table(lua_State *L);
+
+void dlua_init_thread_table(struct dlua_script *script)
+{
+ lua_newtable(script->L);
+ lua_setfield(script->L, LUA_REGISTRYINDEX, LUA_THREAD_REGISTRY_KEY);
+
+ /*
+ * Note that we are *not* adding the main lua state since it is not
+ * a thread. This implies that it will not have any TLS.
+ */
+}
+
+static void warn_about_leaked_threads(struct dlua_script *script)
+{
+ lua_State *L = script->L;
+
+ lua_getfield(L, LUA_REGISTRYINDEX, LUA_THREAD_REGISTRY_KEY);
+
+ i_assert(lua_type(L, -1) == LUA_TTABLE);
+
+ lua_pushnil(L);
+ while (lua_next(L, -2) != 0) {
+ /* stack: table, thread, per-thread table */
+
+ /* check the key */
+ if (lua_type(L, -2) != LUA_TTHREAD) {
+ e_error(script->event, "Unexpected %s key in thread table",
+ lua_typename(L, lua_type(L, -2)));
+ } else {
+ e_error(script->event, "Lua thread %p leaked", lua_tothread(L, -2));
+ }
+
+ /* check the value */
+ if (lua_type(L, -1) != LUA_TTABLE) {
+ e_error(script->event, "Unexpected %s value in thread table",
+ lua_typename(L, lua_type(L, -1)));
+ } else {
+ warn_about_tls_leaks(script, L);
+ }
+
+ /* pop the value for lua_next() */
+ lua_pop(L, 1);
+ }
+
+ lua_pop(L, 1);
+}
+
+void dlua_free_thread_table(struct dlua_script *script)
+{
+ /* all threads should have been closed by now */
+ warn_about_leaked_threads(script);
+
+ /* set the thread table to nil - letting GC clean everything up */
+ lua_pushnil(script->L);
+ lua_setfield(script->L, LUA_REGISTRYINDEX, LUA_THREAD_REGISTRY_KEY);
+}
+
+lua_State *dlua_script_new_thread(struct dlua_script *script)
+{
+ lua_State *thread;
+
+ /* get the threads table */
+ lua_getfield(script->L, LUA_REGISTRYINDEX, LUA_THREAD_REGISTRY_KEY);
+
+ /* allocate a new thread */
+ thread = lua_newthread(script->L);
+ i_assert(thread != NULL);
+
+ /* allocate new TLS table */
+ lua_newtable(script->L);
+
+ /* stack: threads-table, thread, TLS-table (top) */
+
+ /* threads-table[thread] = TLS-table */
+ lua_settable(script->L, -3);
+
+ /* pop threads table */
+ lua_pop(script->L, 1);
+
+ return thread;
+}
+
+static void log_tls_leak(struct dlua_script *script, lua_State *L, bool full)
+{
+ const char *name = NULL;
+
+ /* stack: TLS key, TLS value (top) */
+
+ if (full) {
+ lua_getmetatable(L, -1);
+
+ if (dlua_table_get_string_by_str(L, -1, "__name", &name) < 0)
+ name = NULL;
+
+ lua_pop(L, 1); /* pop the metatable */
+ }
+
+ e_error(script->event, "Lua TLS data in %p thread leaked: key '%s', "
+ "value %s %p (%s)", L, lua_tostring(L, -2),
+ full ? "userdata" : "lightuserdata",
+ lua_touserdata(L, -1), (name != NULL) ? name : "<no name>");
+}
+
+static void warn_about_tls_leaks(struct dlua_script *script, lua_State *L)
+{
+ i_assert(lua_type(L, -1) == LUA_TTABLE);
+
+ lua_pushnil(L);
+ while (lua_next(L, -2) != 0) {
+ /* stack: table, key, value (top) */
+
+ switch (lua_type(L, -1)) {
+ case LUA_TNIL:
+ case LUA_TNUMBER:
+ case LUA_TBOOLEAN:
+ case LUA_TSTRING:
+ case LUA_TFUNCTION:
+ case LUA_TTHREAD:
+ /* these are trivially freed by the Lua GC */
+ break;
+ case LUA_TTABLE:
+ /* recurse into the table */
+ warn_about_tls_leaks(script, L);
+ break;
+ case LUA_TUSERDATA:
+ log_tls_leak(script, L, TRUE);
+ break;
+ case LUA_TLIGHTUSERDATA:
+ log_tls_leak(script, L, FALSE);
+ break;
+ }
+
+ /* pop the value for lua_next() */
+ lua_pop(L, 1);
+ }
+}
+
+void dlua_script_close_thread(struct dlua_script *script, lua_State **_L)
+{
+ if (*_L == NULL)
+ return;
+
+ /* log any TLS userdata leaks */
+ get_tls_table(*_L);
+ warn_about_tls_leaks(script, *_L);
+ lua_pop(*_L, 1);
+
+ /* get the threads table */
+ lua_getfield(*_L, LUA_REGISTRYINDEX, LUA_THREAD_REGISTRY_KEY);
+
+ /* push the thread to destroy */
+ i_assert(lua_pushthread(*_L) != 1);
+
+ lua_pushnil(*_L);
+
+ /* stack: threads-table, thread, nil (top) */
+
+ /*
+ * threads-table[thread] = nil
+ *
+ * This assignment (1) frees all TLS for the thread, and (2) removes
+ * the reference to the thread saving it from GC.
+ */
+ lua_settable(*_L, -3);
+
+ /* pop threads table */
+ lua_pop(*_L, 1);
+
+ *_L = NULL;
+}
+
+/* get the current thread's TLS table */
+static void get_tls_table(lua_State *L)
+{
+ int ret;
+
+ /* get the threads table */
+ ret = dlua_table_get_by_str(L, LUA_REGISTRYINDEX, LUA_TTABLE,
+ LUA_THREAD_REGISTRY_KEY);
+ if (ret < 1)
+ luaL_error(L, "lua threads table is %s",
+ (ret == 0) ? "missing" : "not a table");
+
+ /* get the TLS-table */
+ ret = dlua_table_get_by_thread(L, -1, LUA_TTABLE);
+ if (ret < 1)
+ luaL_error(L, "lua TLS table for thread %p is not a table", L);
+
+ /* stack: threads-table, TLS-table (top) */
+
+ /* remove threads-table from stack */
+ lua_remove(L, -2);
+}
+
+void dlua_tls_set_ptr(lua_State *L, const char *name, void *ptr)
+{
+ get_tls_table(L);
+ lua_pushlightuserdata(L, ptr);
+ lua_setfield(L, -2, name);
+ lua_pop(L, 1);
+}
+
+void *dlua_tls_get_ptr(lua_State *L, const char *name)
+{
+ void *ptr;
+
+ get_tls_table(L);
+ lua_getfield(L, -1, name);
+
+ ptr = lua_touserdata(L, -1);
+
+ lua_pop(L, 2);
+
+ return ptr;
+}
+
+void dlua_tls_set_int(lua_State *L, const char *name, lua_Integer i)
+{
+ get_tls_table(L);
+ lua_pushinteger(L, i);
+ lua_setfield(L, -2, name);
+ lua_pop(L, 1);
+}
+
+lua_Integer dlua_tls_get_int(lua_State *L, const char *name)
+{
+ lua_Integer i;
+
+ get_tls_table(L);
+ lua_getfield(L, -1, name);
+
+ i = lua_tointeger(L, -1);
+
+ lua_pop(L, 2);
+
+ return i;
+}
+
+void dlua_tls_clear(lua_State *L, const char *name)
+{
+ get_tls_table(L);
+ lua_pushnil(L);
+ lua_setfield(L, -2, name);
+ lua_pop(L, 1);
+}
diff --git a/src/lib-lua/dlua-wrapper.h b/src/lib-lua/dlua-wrapper.h
new file mode 100644
index 0000000..3a9f459
--- /dev/null
+++ b/src/lib-lua/dlua-wrapper.h
@@ -0,0 +1,186 @@
+/*
+ * Copyright (c) 2020 Josef 'Jeff' Sipek <jeffpc@josefsipek.net>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+#ifndef DLUA_WRAPPER_H
+#define DLUA_WRAPPER_H
+
+/*
+ * The following macro generates everything necessary to wrap a C structure
+ * and easily push it onto Lua stacks as well as check that a value on the
+ * stack is of this type.
+ *
+ * To generate the necessary API, simply use the macro in your .c file. The
+ * arguments consist of:
+ *
+ * <typename> = the name for the structure to use in generated symbols
+ * <type> = the exposed structure's C type
+ * <putref> = the function to remove a reference from the C structure,
+ * called from the automatically generated __gc metamethod
+ * <extra_fxns_arg> = a C array of luaL_Reg structs passed to luaL_setfuncs
+ * to add Lua methods to the type
+ *
+ * For example, to expose struct timespec with a tostring method, one would
+ * use the following in a .c file:
+ *
+ * // struct timespec isn't refcounted
+ * static inline void timespec_putref(struct timespec *ts)
+ * {
+ * }
+ *
+ * static int timespec_tostring(lua_State *L);
+ *
+ * static const luaL_Reg timespec_fxns[] = {
+ * { "__tostring", timespec_tostring },
+ * { NULL, NULL },
+ * };
+ *
+ * DLUA_WRAP_C_DATA(timespec, struct timespec, timespec_putref, timespec_fxns)
+ *
+ * static int timespec_tostring(lua_State *L)
+ * {
+ * struct timespec *ts;
+ *
+ * ts = xlua_timespec_getptr(L, -1, NULL);
+ *
+ * lua_pushfstring(L, "%d.%09ld", ts->tv_sec, ts->tv_nsec);
+ *
+ * return 1;
+ * }
+ *
+ *
+ * The two functions making up the exposed structure API are:
+ *
+ * static void xlua_push<typename>(lua_State *, <type> *, bool);
+ * static inline <type> *xlua_<typename>_getptr(lua_State *, int, bool *);
+ *
+ * The first pushes the supplied pointer onto the Lua stack, while the
+ * second returns the previously pushed C pointer (or generates a Lua error
+ * if there is a type mismatch).
+ *
+ * The push function tracks the passed in bool argument alongside the C
+ * pointer itself. The getptr function fills in the bool pointer (if not
+ * NULL) with the pushed bool value. While this bool isn't used directly by
+ * the generated code and therefore it can be used for anything, the
+ * intention is to allow the API consumers to mark certain pointers as
+ * "read-only" to prevent Lua scripts from attempting to mutate them. This
+ * allows one to push const pointers while "notifying" the methods that
+ * mutation of any of the members is undefined behavior.
+ *
+ * Also note that the functions are static. That is, they are intended to
+ * only be used in the file where they are generated since they are somewhat
+ * low-level functions. If some public form of a push/get function is
+ * desired, it is up to the API consumer to write wrappers around these and
+ * expose them to the rest of the codebase.
+ *
+ * Revisiting the struct timespec example above, the generated API would
+ * be:
+ *
+ * static void xlua_pushtimespec(lua_State *, struct timespec *, bool);
+ * static inline struct timespec *xlua_timespec_getptr(lua_State *, int,
+ * bool *);
+ */
+#define DLUA_WRAP_C_DATA(typename, type, putref, extra_fxns_arg) \
+struct lua_wrapper_##typename { \
+ type *ptr; \
+ bool ro; \
+}; \
+ \
+static inline type *xlua_##typename##_getptr(lua_State *state, int idx, \
+ bool *ro_r) \
+{ \
+ struct lua_wrapper_##typename *wrapper; \
+ \
+ wrapper = luaL_checkudata(state, idx, #type); \
+ \
+ if (ro_r != NULL) \
+ *ro_r = wrapper->ro; \
+ \
+ return wrapper->ptr; \
+} \
+ \
+static int xlua_wrapper_##typename##_gc(lua_State *state) \
+{ \
+ putref(xlua_##typename##_getptr(state, -1, NULL)); \
+ \
+ return 0; \
+} \
+ \
+static const luaL_Reg provided_##typename##_fxns[] = { \
+ { "__gc", xlua_wrapper_##typename##_gc }, \
+ { NULL, NULL }, \
+}; \
+ \
+/* push [-0,+1,e] */ \
+static void xlua_push##typename(lua_State *state, type *ptr, bool ro) \
+{ \
+ struct lua_wrapper_##typename *wrapper; \
+ \
+ if (ptr == NULL) { \
+ lua_pushnil(state); \
+ return; \
+ } \
+ \
+ wrapper = lua_newuserdata(state, sizeof(struct lua_wrapper_##typename)); \
+ i_assert(wrapper != NULL); \
+ \
+ wrapper->ptr = (ptr); \
+ wrapper->ro = ro; \
+ \
+ /* get the current metatable */ \
+ luaL_getmetatable(state, #type); \
+ if (lua_type(state, -1) != LUA_TTABLE) { \
+ /* initialize a new metatable */ \
+ const luaL_Reg *extra_fxns = (extra_fxns_arg); \
+ lua_CFunction index; \
+ \
+ lua_pop(state, 1); \
+ luaL_newmetatable(state, #type); \
+ luaL_setfuncs(state, provided_##typename##_fxns, 0); \
+ \
+ index = NULL; \
+ if (extra_fxns != NULL) { \
+ unsigned i; \
+ \
+ luaL_setfuncs(state, extra_fxns, 0); \
+ \
+ for (i = 0; extra_fxns[i].name != NULL; i++) { \
+ if (strcmp(extra_fxns[i].name, \
+ "__index") == 0) { \
+ index = extra_fxns[i].func; \
+ break; \
+ } \
+ } \
+ } \
+ \
+ if (index == NULL) { \
+ /* set __index == metatable */ \
+ lua_pushliteral(state, "__index"); \
+ lua_pushvalue(state, -2); /* dup the table */ \
+ lua_settable(state, -3); \
+ } \
+ } \
+ \
+ /* set the metatable */ \
+ lua_setmetatable(state, -2); \
+}
+
+#endif
diff --git a/src/lib-lua/test-dict-lua.c b/src/lib-lua/test-dict-lua.c
new file mode 100644
index 0000000..66d958e
--- /dev/null
+++ b/src/lib-lua/test-dict-lua.c
@@ -0,0 +1,99 @@
+/* Copyright (c) 2021 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "ioloop.h"
+#include "dlua-script-private.h"
+#include "dict-private.h"
+#include "dict-lua.h"
+#include "test-common.h"
+
+#include <fcntl.h>
+#include <sys/stat.h>
+
+static void test_dict_register(void)
+{
+ dict_driver_register(&dict_driver_file);
+}
+
+static void test_dict_finished(lua_State *L, struct ioloop *ioloop, int res)
+{
+ if (res < 0)
+ i_error("%s", lua_tostring(L, -1));
+ io_loop_stop(ioloop);
+}
+
+static void test_dict_lua(void)
+{
+ static const char *luascript =
+"function test_dict(dict)\n"
+" local trans = dict:transaction_begin()\n"
+" trans:set('shared/testkey', 'testvalue')\n"
+" trans:set('shared/testkey2', 'testvalue2')\n"
+" trans:commit()\n"
+"\n"
+" assert(dict:lookup('shared/testkey')[1] == 'testvalue')\n"
+" assert(dict:lookup('shared/testkey2')[1] == 'testvalue2')\n"
+"\n"
+" local key, values\n"
+" local table = {}\n"
+" for key, values in dict:iterate('shared/', 0) do\n"
+" assert(#values == 1)\n"
+" table[key] = values[1]\n"
+" end\n"
+" assert(table['shared/testkey'] == 'testvalue')\n"
+" assert(table['shared/testkey2'] == 'testvalue2')\n"
+"\n"
+" trans = dict:transaction_begin()\n"
+" trans:set_timestamp({['tv_sec'] = 1631278269, ['tv_nsec'] = 999999999})\n"
+" trans:set('shared/testkey', 'updated')\n"
+" trans:unset('shared/testkey2')\n"
+" trans:commit()\n"
+"\n"
+" assert(dict:lookup('shared/testkey')[1] == 'updated')\n"
+" assert(dict:lookup('shared/testkey2') == nil)\n"
+"end\n";
+ struct dict_settings set = {
+ .base_dir = NULL,
+ };
+ struct dict *dict;
+ const char *error;
+
+ test_begin("dict lua");
+ struct ioloop *ioloop = io_loop_create();
+ i_unlink_if_exists(".test.dict");
+ if (dict_init("file:.test.dict", &set, &dict, &error) < 0)
+ i_fatal("dict_init(.test.dict) failed: %s", error);
+
+ struct dlua_script *script;
+ if (dlua_script_create_string(luascript, &script, NULL, &error) < 0)
+ i_fatal("dlua_script_create_string() failed: %s", error);
+ dlua_dovecot_register(script);
+ if (dlua_script_init(script, &error) < 0)
+ i_fatal("dlua_script_init() failed: %s", error);
+
+ lua_State *thread = dlua_script_new_thread(script);
+ dlua_push_dict(thread, dict);
+ if (dlua_pcall_yieldable(thread, "test_dict", 1, test_dict_finished,
+ ioloop, &error) < 0)
+ i_fatal("dlua_pcall() failed: %s", error);
+ io_loop_run(ioloop);
+ i_assert(lua_gettop(thread) == 0);
+ dlua_script_close_thread(script, &thread);
+
+ dlua_script_unref(&script);
+ dict_deinit(&dict);
+ io_loop_destroy(&ioloop);
+
+ i_unlink(".test.dict");
+ test_end();
+}
+
+int main(void)
+{
+ static void (*const test_functions[])(void) = {
+ test_dict_register,
+ test_dict_lua,
+ NULL
+ };
+ return test_run(test_functions);
+}
diff --git a/src/lib-lua/test-lua.c b/src/lib-lua/test-lua.c
new file mode 100644
index 0000000..ff5abe8
--- /dev/null
+++ b/src/lib-lua/test-lua.c
@@ -0,0 +1,473 @@
+/* Copyright (c) 2018 Dovecot authors, see the included COPYING file */
+
+#include "test-lib.h"
+#include "dlua-script-private.h"
+
+#include <math.h>
+
+static int dlua_test_assert(lua_State *L)
+{
+ struct dlua_script *script = dlua_script_from_state(L);
+ const char *what = luaL_checkstring(script->L, 1);
+ bool cond = lua_toboolean(script->L, 2);
+
+ if (!cond) {
+ lua_Debug ar;
+ i_assert(lua_getinfo(L, ">Sl", &ar) == 0);
+ test_assert_failed(what, ar.source, ar.currentline);
+ }
+
+ return 0;
+}
+
+#define GENERATE_GETTERS(name, ctype) \
+static void check_table_get_##name##_ok(struct dlua_script *script, \
+ int idx, ctype expected_value, \
+ const char *str_key, \
+ lua_Integer int_key) \
+{ \
+ ctype value; \
+ int ret; \
+ \
+ /* check string key */ \
+ ret = dlua_table_get_##name##_by_str(script->L, idx, \
+ str_key, &value); \
+ test_assert(ret == 1); \
+ test_assert(value == expected_value); \
+ \
+ /* check int key */ \
+ ret = dlua_table_get_##name##_by_int(script->L, idx, \
+ int_key, &value); \
+ test_assert(ret == 1); \
+ test_assert(value == expected_value); \
+} \
+static void check_table_get_##name##_err(struct dlua_script *script, \
+ int idx, int expected_ret, \
+ const char *str_key, \
+ lua_Integer int_key) \
+{ \
+ ctype value; \
+ int ret; \
+ \
+ /* check string key */ \
+ ret = dlua_table_get_##name##_by_str(script->L, idx, \
+ str_key, &value); \
+ test_assert(ret == expected_ret); \
+ \
+ /* check int key */ \
+ ret = dlua_table_get_##name##_by_int(script->L, idx, \
+ int_key, &value); \
+ test_assert(ret == expected_ret); \
+}
+
+GENERATE_GETTERS(luainteger, lua_Integer);
+GENERATE_GETTERS(int, int);
+GENERATE_GETTERS(intmax, intmax_t);
+GENERATE_GETTERS(uint, unsigned int);
+GENERATE_GETTERS(uintmax, uintmax_t);
+GENERATE_GETTERS(number, lua_Number);
+GENERATE_GETTERS(bool, bool);
+
+/* the string comparison requires us to open-code this */
+static void check_table_get_string_ok(struct dlua_script *script,
+ int idx, const char *expected_value,
+ const char *str_key,
+ lua_Integer int_key)
+{
+ const char *value;
+ int ret;
+
+ /* check string key */
+ ret = dlua_table_get_string_by_str(script->L, idx,
+ str_key, &value);
+ test_assert(ret == 1);
+ test_assert_strcmp(value, expected_value);
+
+ /* check int key */
+ ret = dlua_table_get_string_by_int(script->L, idx,
+ int_key, &value);
+ test_assert(ret == 1);
+ test_assert_strcmp(value, expected_value);
+
+ /* TODO: check thread key, which is difficult */
+}
+
+/* the string comparison of the _ok function requires us to open-code this */
+static void check_table_get_string_err(struct dlua_script *script,
+ int idx, int expected_ret,
+ const char *str_key,
+ lua_Integer int_key)
+{
+ const char *value;
+ int ret;
+
+ /* check string key */
+ ret = dlua_table_get_string_by_str(script->L, idx,
+ str_key, &value);
+ test_assert(ret == expected_ret);
+
+ /* check int key */
+ ret = dlua_table_get_string_by_int(script->L, idx,
+ int_key, &value);
+ test_assert(ret == expected_ret);
+
+ /* TODO: check thread key, which is difficult */
+}
+
+static void check_table_missing(struct dlua_script *script, int idx,
+ const char *str_key,
+ lua_Integer int_key)
+{
+ check_table_get_luainteger_err(script, idx, 0, str_key, int_key);
+ check_table_get_int_err(script, idx, 0, str_key, int_key);
+ check_table_get_intmax_err(script, idx, 0, str_key, int_key);
+ check_table_get_uint_err(script, idx, 0, str_key, int_key);
+ check_table_get_uintmax_err(script, idx, 0, str_key, int_key);
+ check_table_get_number_err(script, idx, 0, str_key, int_key);
+ check_table_get_bool_err(script, idx, 0, str_key, int_key);
+ check_table_get_string_err(script, idx, 0, str_key, int_key);
+}
+
+static void test_lua(void)
+{
+ static const char *luascript =
+"function script_init(req)\n"
+" dovecot.i_debug(\"lua script init called\")\n"
+" local e = dovecot.event()\n"
+" e:log_debug(\"lua script init called from event\")\n"
+" return 0\n"
+"end\n"
+"function lua_function()\n"
+"end\n"
+"function lua_test_flags()\n"
+" local flag = 0\n"
+" flag = dovecot.set_flag(flag, 2)\n"
+" flag = dovecot.set_flag(flag, 4)\n"
+" flag = dovecot.set_flag(flag, 16)\n"
+" test_assert(\"has_flag(flag, 8) == false\", dovecot.has_flag(flag, 8) == false)\n"
+" test_assert(\"has_flag(flag, 4) == true\", dovecot.has_flag(flag, 4) == true)\n"
+" flag = dovecot.clear_flag(flag, 4)\n"
+" test_assert(\"has_flag(flag, 4) == false\", dovecot.has_flag(flag, 4) == false)\n"
+" test_assert(\"has_flag(flag, 16) == true\", dovecot.has_flag(flag, 16) == true)\n"
+"end\n"
+"function lua_test_get_table()\n"
+" t = {}\n"
+" -- zero\n"
+" t[\"zero\"] = 0\n"
+" t[-2] = 0\n"
+" -- small positive values\n"
+" t[\"small-positive-int\"] = 1\n"
+" t[-1] = 1\n"
+" -- small negative values\n"
+" t[\"small-negative-int\"] = -5\n"
+" t[0] = -5\n"
+" -- large positive float\n"
+" t[\"large-positive-int\"] = 2^48\n"
+" t[1] = 2^48\n"
+" -- large negative float\n"
+" t[\"large-negative-int\"] = -2^48\n"
+" t[2] = -2^48\n"
+" -- small float\n"
+" t[\"small-float\"] = 1.525\n"
+" t[3] = 1.525\n"
+" -- bool: true\n"
+" t[\"bool-true\"] = true\n"
+" t[4] = true\n"
+" -- bool: false\n"
+" t[\"bool-false\"] = false\n"
+" t[5] = false\n"
+" -- string\n"
+" t[\"str\"] = \"string\"\n"
+" t[6] = \"string\"\n"
+" return t\n"
+"end\n";
+
+ const char *error = NULL;
+ struct dlua_script *script = NULL;
+
+ test_begin("lua script");
+
+ test_assert(dlua_script_create_string(luascript, &script, NULL, &error) == 0);
+ if (error != NULL)
+ i_fatal("dlua_script_init failed: %s", error);
+
+ dlua_dovecot_register(script);
+
+ dlua_register(script, "test_assert", dlua_test_assert);
+
+ test_assert(dlua_script_init(script, &error) == 0);
+ test_assert(dlua_script_has_function(script, "lua_function"));
+
+ test_assert(dlua_pcall(script->L, "lua_test_flags", 0, 0, &error) == 0);
+
+ lua_getglobal(script->L, "lua_test_get_table");
+ test_assert(lua_pcall(script->L, 0, 1, 0) == 0);
+
+ /*
+ * Check table getters
+ */
+
+ /* lua_Integer */
+ check_table_get_luainteger_ok(script, -1, 0, "zero", -2);
+ check_table_get_luainteger_ok(script, -1, 1, "small-positive-int", -1);
+ check_table_get_luainteger_ok(script, -1, -5, "small-negative-int", 0);
+ check_table_get_luainteger_ok(script, -1, 1ll<<48, "large-positive-int", 1);
+ check_table_get_luainteger_ok(script, -1, -(1ll<<48), "large-negative-int", 2);
+ check_table_get_luainteger_err(script, -1, -1, "small-float", 3);
+ check_table_get_luainteger_err(script, -1, -1, "bool-true", 4);
+ check_table_get_luainteger_err(script, -1, -1, "bool-false", 5);
+ check_table_get_luainteger_err(script, -1, -1, "str", 6);
+
+ /* int */
+ check_table_get_int_ok(script, -1, 0, "zero", -2);
+ check_table_get_int_ok(script, -1, 1, "small-positive-int", -1);
+ check_table_get_int_ok(script, -1, -5, "small-negative-int", 0);
+ check_table_get_int_err(script, -1, -1, "large-positive-int", 1);
+ check_table_get_int_err(script, -1, -1, "large-negative-int", 2);
+ check_table_get_int_err(script, -1, -1, "small-float", 3);
+ check_table_get_int_err(script, -1, -1, "bool-true", 4);
+ check_table_get_int_err(script, -1, -1, "bool-false", 5);
+ check_table_get_int_err(script, -1, -1, "str", 6);
+
+ /* intmax_t */
+ check_table_get_intmax_ok(script, -1, 0, "zero", -2);
+ check_table_get_intmax_ok(script, -1, 1, "small-positive-int", -1);
+ check_table_get_intmax_ok(script, -1, -5, "small-negative-int", 0);
+ check_table_get_intmax_ok(script, -1, 1ll<<48, "large-positive-int", 1);
+ check_table_get_intmax_ok(script, -1, -(1ll<<48), "large-negative-int", 2);
+ check_table_get_intmax_err(script, -1, -1, "small-float", 3);
+ check_table_get_intmax_err(script, -1, -1, "bool-true", 4);
+ check_table_get_intmax_err(script, -1, -1, "bool-false", 5);
+ check_table_get_intmax_err(script, -1, -1, "str", 6);
+
+ /* unsigned int */
+ check_table_get_uint_ok(script, -1, 0, "zero", -2);
+ check_table_get_uint_ok(script, -1, 1, "small-positive-int", -1);
+ check_table_get_uint_err(script, -1, -1, "small-negative-int", 0);
+ check_table_get_uint_err(script, -1, -1, "large-positive-int", 1);
+ check_table_get_uint_err(script, -1, -1, "large-negative-int", 2);
+ check_table_get_uint_err(script, -1, -1, "small-float", 3);
+ check_table_get_uint_err(script, -1, -1, "bool-true", 4);
+ check_table_get_uint_err(script, -1, -1, "bool-false", 5);
+ check_table_get_uint_err(script, -1, -1, "str", 6);
+
+ /* uintmax_t */
+ check_table_get_uintmax_ok(script, -1, 0, "zero", -2);
+ check_table_get_uintmax_ok(script, -1, 1, "small-positive-int", -1);
+ check_table_get_uintmax_err(script, -1, -1, "small-negative-int", 0);
+ check_table_get_uintmax_ok(script, -1, 1ll<<48, "large-positive-int", 1);
+ check_table_get_uintmax_err(script, -1, -1, "large-negative-int", 2);
+ check_table_get_uintmax_err(script, -1, -1, "small-float", 3);
+ check_table_get_uintmax_err(script, -1, -1, "bool-true", 4);
+ check_table_get_uintmax_err(script, -1, -1, "bool-false", 5);
+ check_table_get_uintmax_err(script, -1, -1, "str", 6);
+
+ /* lua_Number */
+ check_table_get_number_ok(script, -1, 0, "zero", -2);
+ check_table_get_number_ok(script, -1, 1, "small-positive-int", -1);
+ check_table_get_number_ok(script, -1, -5, "small-negative-int", 0);
+ check_table_get_number_ok(script, -1, 1ll<<48, "large-positive-int", 1);
+ check_table_get_number_ok(script, -1, -(1ll<<48), "large-negative-int", 2);
+ check_table_get_number_ok(script, -1, 1.525, "small-float", 3);
+ check_table_get_number_err(script, -1, -1, "bool-true", 4);
+ check_table_get_number_err(script, -1, -1, "bool-false", 5);
+ check_table_get_number_err(script, -1, -1, "str", 6);
+
+ /* bool */
+ check_table_get_bool_err(script, -1, -1, "zero", -2);
+ check_table_get_bool_err(script, -1, -1, "small-positive-int", -1);
+ check_table_get_bool_err(script, -1, -1, "small-negative-int", 0);
+ check_table_get_bool_err(script, -1, -1, "large-positive-int", 1);
+ check_table_get_bool_err(script, -1, -1, "large-negative-int", 2);
+ check_table_get_bool_err(script, -1, -1, "small-float", 3);
+ check_table_get_bool_ok(script, -1, TRUE, "bool-true", 4);
+ check_table_get_bool_ok(script, -1, FALSE, "bool-false", 5);
+ check_table_get_bool_err(script, -1, -1, "str", 6);
+
+ /* const char * */
+ check_table_get_string_err(script, -1, -1, "zero", -2);
+ check_table_get_string_err(script, -1, -1, "small-positive-int", -1);
+ check_table_get_string_err(script, -1, -1, "small-negative-int", 0);
+ check_table_get_string_err(script, -1, -1, "large-positive-int", 1);
+ check_table_get_string_err(script, -1, -1, "large-negative-int", 2);
+ check_table_get_string_err(script, -1, -1, "small-float", 3);
+ check_table_get_string_err(script, -1, -1, "bool-true", 4);
+ check_table_get_string_err(script, -1, -1, "bool-false", 5);
+ check_table_get_string_ok(script, -1, "string", "str", 6);
+
+ check_table_missing(script, -1, "missing", -10);
+
+ lua_pop(script->L, 1);
+
+ dlua_script_unref(&script);
+
+ test_end();
+}
+
+static void test_tls(void)
+{
+ const char *error = NULL;
+ struct dlua_script *script = NULL;
+ lua_State *L1, *L2;
+
+ test_begin("lua thread local storage");
+
+ test_assert(dlua_script_create_string("", &script, NULL, &error) == 0);
+ if (error != NULL)
+ i_fatal("dlua_script_init failed: %s", error);
+
+ L1 = dlua_script_new_thread(script);
+ L2 = dlua_script_new_thread(script);
+
+ dlua_tls_set_ptr(L1, "ptr", L1);
+ test_assert(dlua_tls_get_ptr(L1, "ptr") == L1);
+ test_assert(dlua_tls_get_ptr(L2, "ptr") == NULL);
+ test_assert(dlua_tls_get_int(L1, "int") == 0);
+ test_assert(dlua_tls_get_int(L2, "int") == 0);
+
+ dlua_tls_set_ptr(L2, "ptr", L2);
+ test_assert(dlua_tls_get_ptr(L1, "ptr") == L1);
+ test_assert(dlua_tls_get_ptr(L2, "ptr") == L2);
+ test_assert(dlua_tls_get_int(L1, "int") == 0);
+ test_assert(dlua_tls_get_int(L2, "int") == 0);
+
+ dlua_tls_set_int(L1, "int", 1);
+ dlua_tls_set_int(L2, "int", 2);
+ test_assert(dlua_tls_get_int(L1, "int") == 1);
+ test_assert(dlua_tls_get_int(L2, "int") == 2);
+
+ dlua_tls_clear(L1, "ptr");
+ test_assert(dlua_tls_get_ptr(L1, "ptr") == NULL);
+ test_assert(dlua_tls_get_ptr(L2, "ptr") == L2);
+ test_assert(dlua_tls_get_int(L1, "int") == 1);
+ test_assert(dlua_tls_get_int(L2, "int") == 2);
+
+ dlua_script_close_thread(script, &L1);
+
+ test_assert(dlua_tls_get_ptr(L2, "ptr") == L2);
+ test_assert(dlua_tls_get_int(L2, "int") == 2);
+
+ dlua_tls_clear(L2, "ptr");
+ dlua_script_close_thread(script, &L2);
+
+ dlua_script_unref(&script);
+
+ test_end();
+}
+
+/* check lua_tointegerx against top-of-stack item */
+static void check_tointegerx_compat(lua_State *L, bool expected_isnum,
+ bool expected_isint,
+ lua_Integer expected_value)
+{
+ lua_Integer value;
+ int isnum;
+
+ value = lua_tointegerx(L, -1, &isnum);
+ test_assert((isnum == 1) == expected_isnum);
+
+ if (isnum == 1)
+ test_assert(value == expected_value);
+
+ test_assert(lua_isinteger(L, -1) == expected_isint);
+
+ lua_pop(L, 1);
+}
+
+static void test_compat_tointegerx_and_isinteger(void)
+{
+ static const struct {
+ const char *input;
+ lua_Integer output;
+ bool isnum;
+ } str_tests[] = {
+ { "-1", -1, TRUE },
+ { "0", 0, TRUE },
+ { "1", 1, TRUE },
+ { "-2147483648", -2147483648, TRUE },
+ { "2147483647", 2147483647, TRUE },
+ { "0x123", 0x123, TRUE },
+ { "0123", 123, TRUE }, /* NB: lua doesn't use leading zero for octal */
+ { "0xabcdef", 0xabcdef, TRUE },
+ { "0xabcdefg", 0, FALSE },
+ { "abc", 0, FALSE },
+ { "1.525", 0, FALSE },
+ { "52.51", 0, FALSE },
+ };
+ static const struct {
+ lua_Number input;
+ lua_Integer output;
+ bool isnum;
+ } num_tests[] = {
+ { -1, -1, TRUE },
+ { 0, 0, TRUE },
+ { 1, 1, TRUE },
+ { INT_MIN, INT_MIN, TRUE },
+ { INT_MAX, INT_MAX, TRUE },
+ { 1.525, 0, FALSE },
+ { 52.51, 0, FALSE },
+ { NAN, 0, FALSE },
+ { +INFINITY, 0, FALSE },
+ { -INFINITY, 0, FALSE },
+ };
+ static const struct {
+ lua_Integer input;
+ lua_Integer output;
+ } int_tests[] = {
+ { -1, -1 },
+ { 0, 0 },
+ { 1, 1 },
+ { INT_MIN, INT_MIN },
+ { INT_MAX, INT_MAX },
+ };
+ struct dlua_script *script;
+ const char *error;
+ size_t i;
+
+ test_begin("lua compat tostringx/isinteger");
+
+ test_assert(dlua_script_create_string("", &script, NULL, &error) == 0);
+
+ for (i = 0; i < N_ELEMENTS(str_tests); i++) {
+ lua_pushstring(script->L, str_tests[i].input);
+ check_tointegerx_compat(script->L, str_tests[i].isnum, FALSE,
+ str_tests[i].output);
+ }
+
+ for (i = 0; i < N_ELEMENTS(num_tests); i++) {
+ bool isint;
+
+ /* See lua_isinteger() comment in dlua-compat.h */
+#if LUA_VERSION_NUM >= 503
+ isint = FALSE;
+#else
+ isint = num_tests[i].isnum;
+#endif
+
+ lua_pushnumber(script->L, num_tests[i].input);
+ check_tointegerx_compat(script->L, num_tests[i].isnum,
+ isint,
+ num_tests[i].output);
+ }
+
+ for (i = 0; i < N_ELEMENTS(int_tests); i++) {
+ lua_pushinteger(script->L, int_tests[i].input);
+ check_tointegerx_compat(script->L, TRUE, TRUE,
+ int_tests[i].output);
+ }
+
+ dlua_script_unref(&script);
+
+ test_end();
+}
+
+int main(void) {
+ void (*tests[])(void) = {
+ test_lua,
+ test_tls,
+ test_compat_tointegerx_and_isinteger,
+ NULL
+ };
+
+ return test_run(tests);
+}
diff --git a/src/lib-mail/Makefile.am b/src/lib-mail/Makefile.am
new file mode 100644
index 0000000..67bb1cc
--- /dev/null
+++ b/src/lib-mail/Makefile.am
@@ -0,0 +1,262 @@
+noinst_LTLIBRARIES = libmail.la
+
+AM_CPPFLAGS = \
+ -I$(top_srcdir)/src/lib \
+ -I$(top_srcdir)/src/lib-test \
+ -I$(top_srcdir)/src/lib-charset \
+ -I$(top_srcdir)/src/lib-smtp
+
+libmail_la_SOURCES = \
+ istream-attachment-connector.c \
+ istream-attachment-extractor.c \
+ istream-binary-converter.c \
+ istream-dot.c \
+ istream-header-filter.c \
+ istream-nonuls.c \
+ istream-qp-decoder.c \
+ istream-qp-encoder.c \
+ mail-html2text.c \
+ mail-user-hash.c \
+ mbox-from.c \
+ message-address.c \
+ message-binary-part.c \
+ message-date.c \
+ message-decoder.c \
+ message-header-decode.c \
+ message-header-encode.c \
+ message-header-hash.c \
+ message-header-parser.c \
+ message-id.c \
+ message-parser.c \
+ message-parser-from-parts.c \
+ message-part.c \
+ message-part-data.c \
+ message-part-serialize.c \
+ message-search.c \
+ message-size.c \
+ message-snippet.c \
+ ostream-dot.c \
+ qp-decoder.c \
+ qp-encoder.c \
+ quoted-printable.c \
+ rfc2231-parser.c \
+ rfc822-parser.c
+
+noinst_HEADERS = \
+ html-entities.h \
+ message-parser-private.h
+
+headers = \
+ istream-attachment-connector.h \
+ istream-attachment-extractor.h \
+ istream-binary-converter.h \
+ istream-dot.h \
+ istream-header-filter.h \
+ istream-nonuls.h \
+ istream-qp.h \
+ mail-user-hash.h \
+ mbox-from.h \
+ mail-html2text.h \
+ mail-types.h \
+ message-address.h \
+ message-binary-part.h \
+ message-date.h \
+ message-decoder.h \
+ message-header-decode.h \
+ message-header-encode.h \
+ message-header-hash.h \
+ message-header-parser.h \
+ message-id.h \
+ message-parser.h \
+ message-part.h \
+ message-part-data.h \
+ message-part-serialize.h \
+ message-search.h \
+ message-size.h \
+ message-snippet.h \
+ ostream-dot.h \
+ qp-decoder.h \
+ qp-encoder.h \
+ quoted-printable.h \
+ rfc2231-parser.h \
+ rfc822-parser.h
+
+pkginc_libdir=$(pkgincludedir)
+pkginc_lib_HEADERS = $(headers)
+
+test_programs = \
+ test-istream-dot \
+ test-istream-attachment \
+ test-istream-binary-converter \
+ test-istream-header-filter \
+ test-istream-qp-decoder \
+ test-istream-qp-encoder \
+ test-mail-html2text \
+ test-mail-user-hash \
+ test-mbox-from \
+ test-message-address \
+ test-message-date \
+ test-message-decoder \
+ test-message-header-decode \
+ test-message-header-encode \
+ test-message-header-hash \
+ test-message-header-parser \
+ test-message-id \
+ test-message-parser \
+ test-message-part \
+ test-message-part-serialize \
+ test-message-search \
+ test-message-size \
+ test-message-snippet \
+ test-ostream-dot \
+ test-qp-decoder \
+ test-qp-encoder \
+ test-quoted-printable \
+ test-rfc2231-parser \
+ test-rfc822-parser
+
+fuzz_programs =
+
+if USE_FUZZER
+fuzz_programs += fuzz-message-parser
+
+nodist_EXTRA_fuzz_message_parser_SOURCES = force-cxx-linking.cxx
+
+fuzz_message_parser_CPPFLAGS = $(FUZZER_CPPFLAGS)
+fuzz_message_parser_LDFLAGS = $(FUZZER_LDFLAGS)
+fuzz_message_parser_SOURCES = fuzz-message-parser.c
+fuzz_message_parser_LDADD = $(test_libs)
+fuzz_message_parser_DEPENDENCIES = $(test_deps)
+
+endif
+
+noinst_PROGRAMS = $(fuzz_programs) $(test_programs)
+
+test_libs = \
+ $(noinst_LTLIBRARIES) \
+ ../lib-charset/libcharset.la \
+ ../lib-test/libtest.la \
+ ../lib/liblib.la
+
+test_deps = $(noinst_LTLIBRARIES) $(test_libs)
+
+test_istream_dot_SOURCES = test-istream-dot.c
+test_istream_dot_LDADD = $(test_libs)
+test_istream_dot_DEPENDENCIES = $(test_deps)
+
+test_istream_qp_decoder_SOURCES = test-istream-qp-decoder.c
+test_istream_qp_decoder_LDADD = $(test_libs)
+test_istream_qp_decoder_DEPENDENCIES = $(test_deps)
+
+test_istream_qp_encoder_SOURCES = test-istream-qp-encoder.c
+test_istream_qp_encoder_LDADD = $(test_libs)
+test_istream_qp_encoder_DEPENDENCIES = $(test_deps)
+
+test_istream_binary_converter_SOURCES = test-istream-binary-converter.c
+test_istream_binary_converter_LDADD = $(test_libs)
+test_istream_binary_converter_DEPENDENCIES = $(test_deps)
+
+test_istream_attachment_SOURCES = test-istream-attachment.c
+test_istream_attachment_LDADD = $(test_libs)
+test_istream_attachment_DEPENDENCIES = $(test_deps)
+
+test_istream_header_filter_SOURCES = test-istream-header-filter.c
+test_istream_header_filter_LDADD = $(test_libs)
+test_istream_header_filter_DEPENDENCIES = $(test_deps)
+
+test_mbox_from_SOURCES = test-mbox-from.c
+test_mbox_from_LDADD = $(test_libs)
+test_mbox_from_DEPENDENCIES = $(test_deps)
+
+test_message_address_SOURCES = test-message-address.c
+test_message_address_LDADD = $(test_libs)
+test_message_address_DEPENDENCIES = $(test_deps)
+
+test_message_date_SOURCES = test-message-date.c
+test_message_date_LDADD = $(test_libs)
+test_message_date_DEPENDENCIES = $(test_deps)
+
+test_message_decoder_SOURCES = test-message-decoder.c
+test_message_decoder_LDADD = $(test_libs) ../lib-charset/libcharset.la
+test_message_decoder_DEPENDENCIES = $(test_deps) ../lib-charset/libcharset.la
+
+test_message_header_decode_SOURCES = test-message-header-decode.c
+test_message_header_decode_LDADD = $(test_libs)
+test_message_header_decode_DEPENDENCIES = $(test_deps)
+
+test_message_header_encode_SOURCES = test-message-header-encode.c
+test_message_header_encode_LDADD = $(test_libs)
+test_message_header_encode_DEPENDENCIES = $(test_deps)
+
+test_message_header_hash_SOURCES = test-message-header-hash.c
+test_message_header_hash_LDADD = $(test_libs)
+test_message_header_hash_DEPENDENCIES = $(test_deps)
+
+test_message_header_parser_SOURCES = test-message-header-parser.c
+test_message_header_parser_LDADD = $(test_libs)
+test_message_header_parser_DEPENDENCIES = $(test_deps)
+
+test_message_id_SOURCES = test-message-id.c
+test_message_id_LDADD = $(test_libs)
+test_message_id_DEPENDENCIES = $(test_deps)
+
+test_message_parser_SOURCES = test-message-parser.c
+test_message_parser_LDADD = $(test_libs)
+test_message_parser_DEPENDENCIES = $(test_deps)
+
+test_message_part_SOURCES = test-message-part.c
+test_message_part_LDADD = $(test_libs)
+test_message_part_DEPENDENCIES = $(test_deps)
+
+test_message_search_SOURCES = test-message-search.c
+test_message_search_LDADD = $(test_libs) ../lib-charset/libcharset.la
+test_message_search_DEPENDENCIES = $(test_deps) ../lib-charset/libcharset.la
+
+test_message_size_SOURCES = test-message-size.c
+test_message_size_LDADD = $(test_libs)
+test_message_size_DEPENDENCIES = $(test_deps)
+
+test_message_snippet_SOURCES = test-message-snippet.c
+test_message_snippet_LDADD = $(test_message_decoder_LDADD)
+test_message_snippet_DEPENDENCIES = $(test_deps)
+
+test_mail_html2text_SOURCES = test-mail-html2text.c
+test_mail_html2text_LDADD = $(test_libs)
+test_mail_html2text_DEPENDENCIES = $(test_deps)
+
+test_ostream_dot_SOURCES = test-ostream-dot.c
+test_ostream_dot_LDADD = $(test_libs)
+test_ostream_dot_DEPENDENCIES = $(test_deps)
+
+test_qp_decoder_SOURCES = test-qp-decoder.c
+test_qp_decoder_LDADD = $(test_libs)
+test_qp_decoder_DEPENDENCIES = $(test_deps)
+
+test_qp_encoder_SOURCES = test-qp-encoder.c
+test_qp_encoder_LDADD = $(test_libs)
+test_qp_encoder_DEPENDENCIES = $(test_deps)
+
+test_quoted_printable_SOURCES = test-quoted-printable.c
+test_quoted_printable_LDADD = $(test_libs)
+test_quoted_printable_DEPENDENCIES = $(test_deps)
+
+test_rfc2231_parser_SOURCES = test-rfc2231-parser.c
+test_rfc2231_parser_LDADD = $(test_libs)
+test_rfc2231_parser_DEPENDENCIES = $(test_deps)
+
+test_rfc822_parser_SOURCES = test-rfc822-parser.c
+test_rfc822_parser_LDADD = $(test_libs)
+test_rfc822_parser_DEPENDENCIES = $(test_deps)
+
+test_mail_user_hash_SOURCES = test-mail-user-hash.c
+test_mail_user_hash_LDADD = $(test_libs)
+test_mail_user_hash_DEPENDENCIES = $(test_deps)
+
+test_message_part_serialize_SOURCES = test-message-part-serialize.c
+test_message_part_serialize_LDADD = $(test_libs)
+test_message_part_serialize_DEPENDENCIES = $(test_deps)
+
+check-local:
+ for bin in $(test_programs); do \
+ if ! $(RUN_TEST) ./$$bin; then exit 1; fi; \
+ done
diff --git a/src/lib-mail/Makefile.in b/src/lib-mail/Makefile.in
new file mode 100644
index 0000000..c19f328
--- /dev/null
+++ b/src/lib-mail/Makefile.in
@@ -0,0 +1,1629 @@
+# Makefile.in generated by automake 1.16.1 from Makefile.am.
+# @configure_input@
+
+# Copyright (C) 1994-2018 Free Software Foundation, Inc.
+
+# This Makefile.in is free software; the Free Software Foundation
+# gives unlimited permission to copy and/or distribute it,
+# with or without modifications, as long as this notice is preserved.
+
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY, to the extent permitted by law; without
+# even the implied warranty of MERCHANTABILITY or FITNESS FOR A
+# PARTICULAR PURPOSE.
+
+@SET_MAKE@
+
+
+
+VPATH = @srcdir@
+am__is_gnu_make = { \
+ if test -z '$(MAKELEVEL)'; then \
+ false; \
+ elif test -n '$(MAKE_HOST)'; then \
+ true; \
+ elif test -n '$(MAKE_VERSION)' && test -n '$(CURDIR)'; then \
+ true; \
+ else \
+ false; \
+ fi; \
+}
+am__make_running_with_option = \
+ case $${target_option-} in \
+ ?) ;; \
+ *) echo "am__make_running_with_option: internal error: invalid" \
+ "target option '$${target_option-}' specified" >&2; \
+ exit 1;; \
+ esac; \
+ has_opt=no; \
+ sane_makeflags=$$MAKEFLAGS; \
+ if $(am__is_gnu_make); then \
+ sane_makeflags=$$MFLAGS; \
+ else \
+ case $$MAKEFLAGS in \
+ *\\[\ \ ]*) \
+ bs=\\; \
+ sane_makeflags=`printf '%s\n' "$$MAKEFLAGS" \
+ | sed "s/$$bs$$bs[$$bs $$bs ]*//g"`;; \
+ esac; \
+ fi; \
+ skip_next=no; \
+ strip_trailopt () \
+ { \
+ flg=`printf '%s\n' "$$flg" | sed "s/$$1.*$$//"`; \
+ }; \
+ for flg in $$sane_makeflags; do \
+ test $$skip_next = yes && { skip_next=no; continue; }; \
+ case $$flg in \
+ *=*|--*) continue;; \
+ -*I) strip_trailopt 'I'; skip_next=yes;; \
+ -*I?*) strip_trailopt 'I';; \
+ -*O) strip_trailopt 'O'; skip_next=yes;; \
+ -*O?*) strip_trailopt 'O';; \
+ -*l) strip_trailopt 'l'; skip_next=yes;; \
+ -*l?*) strip_trailopt 'l';; \
+ -[dEDm]) skip_next=yes;; \
+ -[JT]) skip_next=yes;; \
+ esac; \
+ case $$flg in \
+ *$$target_option*) has_opt=yes; break;; \
+ esac; \
+ done; \
+ test $$has_opt = yes
+am__make_dryrun = (target_option=n; $(am__make_running_with_option))
+am__make_keepgoing = (target_option=k; $(am__make_running_with_option))
+pkgdatadir = $(datadir)/@PACKAGE@
+pkgincludedir = $(includedir)/@PACKAGE@
+pkglibdir = $(libdir)/@PACKAGE@
+pkglibexecdir = $(libexecdir)/@PACKAGE@
+am__cd = CDPATH="$${ZSH_VERSION+.}$(PATH_SEPARATOR)" && cd
+install_sh_DATA = $(install_sh) -c -m 644
+install_sh_PROGRAM = $(install_sh) -c
+install_sh_SCRIPT = $(install_sh) -c
+INSTALL_HEADER = $(INSTALL_DATA)
+transform = $(program_transform_name)
+NORMAL_INSTALL = :
+PRE_INSTALL = :
+POST_INSTALL = :
+NORMAL_UNINSTALL = :
+PRE_UNINSTALL = :
+POST_UNINSTALL = :
+build_triplet = @build@
+host_triplet = @host@
+@USE_FUZZER_TRUE@am__append_1 = fuzz-message-parser
+noinst_PROGRAMS = $(am__EXEEXT_2) $(am__EXEEXT_3)
+subdir = src/lib-mail
+ACLOCAL_M4 = $(top_srcdir)/aclocal.m4
+am__aclocal_m4_deps = $(top_srcdir)/m4/ac_checktype2.m4 \
+ $(top_srcdir)/m4/ac_typeof.m4 $(top_srcdir)/m4/arc4random.m4 \
+ $(top_srcdir)/m4/blockdev.m4 $(top_srcdir)/m4/c99_vsnprintf.m4 \
+ $(top_srcdir)/m4/clock_gettime.m4 $(top_srcdir)/m4/crypt.m4 \
+ $(top_srcdir)/m4/crypt_xpg6.m4 $(top_srcdir)/m4/dbqlk.m4 \
+ $(top_srcdir)/m4/dirent_dtype.m4 $(top_srcdir)/m4/dovecot.m4 \
+ $(top_srcdir)/m4/fd_passing.m4 $(top_srcdir)/m4/fdatasync.m4 \
+ $(top_srcdir)/m4/flexible_array_member.m4 \
+ $(top_srcdir)/m4/glibc.m4 $(top_srcdir)/m4/gmtime_max.m4 \
+ $(top_srcdir)/m4/gmtime_tm_gmtoff.m4 \
+ $(top_srcdir)/m4/ioloop.m4 $(top_srcdir)/m4/iovec.m4 \
+ $(top_srcdir)/m4/ipv6.m4 $(top_srcdir)/m4/libcap.m4 \
+ $(top_srcdir)/m4/libtool.m4 $(top_srcdir)/m4/libwrap.m4 \
+ $(top_srcdir)/m4/linux_mremap.m4 $(top_srcdir)/m4/ltoptions.m4 \
+ $(top_srcdir)/m4/ltsugar.m4 $(top_srcdir)/m4/ltversion.m4 \
+ $(top_srcdir)/m4/lt~obsolete.m4 $(top_srcdir)/m4/mmap_write.m4 \
+ $(top_srcdir)/m4/mntctl.m4 $(top_srcdir)/m4/modules.m4 \
+ $(top_srcdir)/m4/notify.m4 $(top_srcdir)/m4/nsl.m4 \
+ $(top_srcdir)/m4/off_t_max.m4 $(top_srcdir)/m4/pkg.m4 \
+ $(top_srcdir)/m4/pr_set_dumpable.m4 \
+ $(top_srcdir)/m4/q_quotactl.m4 $(top_srcdir)/m4/quota.m4 \
+ $(top_srcdir)/m4/random.m4 $(top_srcdir)/m4/rlimit.m4 \
+ $(top_srcdir)/m4/sendfile.m4 $(top_srcdir)/m4/size_t_signed.m4 \
+ $(top_srcdir)/m4/sockpeercred.m4 $(top_srcdir)/m4/sql.m4 \
+ $(top_srcdir)/m4/ssl.m4 $(top_srcdir)/m4/st_tim.m4 \
+ $(top_srcdir)/m4/static_array.m4 $(top_srcdir)/m4/test_with.m4 \
+ $(top_srcdir)/m4/time_t.m4 $(top_srcdir)/m4/typeof.m4 \
+ $(top_srcdir)/m4/typeof_dev_t.m4 \
+ $(top_srcdir)/m4/uoff_t_max.m4 $(top_srcdir)/m4/vararg.m4 \
+ $(top_srcdir)/m4/want_apparmor.m4 \
+ $(top_srcdir)/m4/want_bsdauth.m4 \
+ $(top_srcdir)/m4/want_bzlib.m4 \
+ $(top_srcdir)/m4/want_cassandra.m4 \
+ $(top_srcdir)/m4/want_cdb.m4 \
+ $(top_srcdir)/m4/want_checkpassword.m4 \
+ $(top_srcdir)/m4/want_clucene.m4 $(top_srcdir)/m4/want_db.m4 \
+ $(top_srcdir)/m4/want_gssapi.m4 $(top_srcdir)/m4/want_icu.m4 \
+ $(top_srcdir)/m4/want_ldap.m4 $(top_srcdir)/m4/want_lua.m4 \
+ $(top_srcdir)/m4/want_lz4.m4 $(top_srcdir)/m4/want_lzma.m4 \
+ $(top_srcdir)/m4/want_mysql.m4 $(top_srcdir)/m4/want_pam.m4 \
+ $(top_srcdir)/m4/want_passwd.m4 $(top_srcdir)/m4/want_pgsql.m4 \
+ $(top_srcdir)/m4/want_prefetch.m4 \
+ $(top_srcdir)/m4/want_shadow.m4 \
+ $(top_srcdir)/m4/want_sodium.m4 $(top_srcdir)/m4/want_solr.m4 \
+ $(top_srcdir)/m4/want_sqlite.m4 \
+ $(top_srcdir)/m4/want_stemmer.m4 \
+ $(top_srcdir)/m4/want_systemd.m4 \
+ $(top_srcdir)/m4/want_textcat.m4 \
+ $(top_srcdir)/m4/want_unwind.m4 $(top_srcdir)/m4/want_zlib.m4 \
+ $(top_srcdir)/m4/want_zstd.m4 $(top_srcdir)/configure.ac
+am__configure_deps = $(am__aclocal_m4_deps) $(CONFIGURE_DEPENDENCIES) \
+ $(ACLOCAL_M4)
+DIST_COMMON = $(srcdir)/Makefile.am $(noinst_HEADERS) \
+ $(pkginc_lib_HEADERS) $(am__DIST_COMMON)
+mkinstalldirs = $(install_sh) -d
+CONFIG_HEADER = $(top_builddir)/config.h
+CONFIG_CLEAN_FILES =
+CONFIG_CLEAN_VPATH_FILES =
+@USE_FUZZER_TRUE@am__EXEEXT_1 = fuzz-message-parser$(EXEEXT)
+am__EXEEXT_2 = $(am__EXEEXT_1)
+am__EXEEXT_3 = test-istream-dot$(EXEEXT) \
+ test-istream-attachment$(EXEEXT) \
+ test-istream-binary-converter$(EXEEXT) \
+ test-istream-header-filter$(EXEEXT) \
+ test-istream-qp-decoder$(EXEEXT) \
+ test-istream-qp-encoder$(EXEEXT) test-mail-html2text$(EXEEXT) \
+ test-mail-user-hash$(EXEEXT) test-mbox-from$(EXEEXT) \
+ test-message-address$(EXEEXT) test-message-date$(EXEEXT) \
+ test-message-decoder$(EXEEXT) \
+ test-message-header-decode$(EXEEXT) \
+ test-message-header-encode$(EXEEXT) \
+ test-message-header-hash$(EXEEXT) \
+ test-message-header-parser$(EXEEXT) test-message-id$(EXEEXT) \
+ test-message-parser$(EXEEXT) test-message-part$(EXEEXT) \
+ test-message-part-serialize$(EXEEXT) \
+ test-message-search$(EXEEXT) test-message-size$(EXEEXT) \
+ test-message-snippet$(EXEEXT) test-ostream-dot$(EXEEXT) \
+ test-qp-decoder$(EXEEXT) test-qp-encoder$(EXEEXT) \
+ test-quoted-printable$(EXEEXT) test-rfc2231-parser$(EXEEXT) \
+ test-rfc822-parser$(EXEEXT)
+PROGRAMS = $(noinst_PROGRAMS)
+LTLIBRARIES = $(noinst_LTLIBRARIES)
+libmail_la_LIBADD =
+am_libmail_la_OBJECTS = istream-attachment-connector.lo \
+ istream-attachment-extractor.lo istream-binary-converter.lo \
+ istream-dot.lo istream-header-filter.lo istream-nonuls.lo \
+ istream-qp-decoder.lo istream-qp-encoder.lo mail-html2text.lo \
+ mail-user-hash.lo mbox-from.lo message-address.lo \
+ message-binary-part.lo message-date.lo message-decoder.lo \
+ message-header-decode.lo message-header-encode.lo \
+ message-header-hash.lo message-header-parser.lo message-id.lo \
+ message-parser.lo message-parser-from-parts.lo message-part.lo \
+ message-part-data.lo message-part-serialize.lo \
+ message-search.lo message-size.lo message-snippet.lo \
+ ostream-dot.lo qp-decoder.lo qp-encoder.lo quoted-printable.lo \
+ rfc2231-parser.lo rfc822-parser.lo
+libmail_la_OBJECTS = $(am_libmail_la_OBJECTS)
+AM_V_lt = $(am__v_lt_@AM_V@)
+am__v_lt_ = $(am__v_lt_@AM_DEFAULT_V@)
+am__v_lt_0 = --silent
+am__v_lt_1 =
+am__fuzz_message_parser_SOURCES_DIST = fuzz-message-parser.c
+@USE_FUZZER_TRUE@am_fuzz_message_parser_OBJECTS = fuzz_message_parser-fuzz-message-parser.$(OBJEXT)
+fuzz_message_parser_OBJECTS = $(am_fuzz_message_parser_OBJECTS)
+fuzz_message_parser_LINK = $(LIBTOOL) $(AM_V_lt) --tag=CXX \
+ $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=link $(CXXLD) \
+ $(AM_CXXFLAGS) $(CXXFLAGS) $(fuzz_message_parser_LDFLAGS) \
+ $(LDFLAGS) -o $@
+am_test_istream_attachment_OBJECTS = \
+ test-istream-attachment.$(OBJEXT)
+test_istream_attachment_OBJECTS = \
+ $(am_test_istream_attachment_OBJECTS)
+am_test_istream_binary_converter_OBJECTS = \
+ test-istream-binary-converter.$(OBJEXT)
+test_istream_binary_converter_OBJECTS = \
+ $(am_test_istream_binary_converter_OBJECTS)
+am_test_istream_dot_OBJECTS = test-istream-dot.$(OBJEXT)
+test_istream_dot_OBJECTS = $(am_test_istream_dot_OBJECTS)
+am_test_istream_header_filter_OBJECTS = \
+ test-istream-header-filter.$(OBJEXT)
+test_istream_header_filter_OBJECTS = \
+ $(am_test_istream_header_filter_OBJECTS)
+am_test_istream_qp_decoder_OBJECTS = \
+ test-istream-qp-decoder.$(OBJEXT)
+test_istream_qp_decoder_OBJECTS = \
+ $(am_test_istream_qp_decoder_OBJECTS)
+am_test_istream_qp_encoder_OBJECTS = \
+ test-istream-qp-encoder.$(OBJEXT)
+test_istream_qp_encoder_OBJECTS = \
+ $(am_test_istream_qp_encoder_OBJECTS)
+am_test_mail_html2text_OBJECTS = test-mail-html2text.$(OBJEXT)
+test_mail_html2text_OBJECTS = $(am_test_mail_html2text_OBJECTS)
+am_test_mail_user_hash_OBJECTS = test-mail-user-hash.$(OBJEXT)
+test_mail_user_hash_OBJECTS = $(am_test_mail_user_hash_OBJECTS)
+am_test_mbox_from_OBJECTS = test-mbox-from.$(OBJEXT)
+test_mbox_from_OBJECTS = $(am_test_mbox_from_OBJECTS)
+am_test_message_address_OBJECTS = test-message-address.$(OBJEXT)
+test_message_address_OBJECTS = $(am_test_message_address_OBJECTS)
+am_test_message_date_OBJECTS = test-message-date.$(OBJEXT)
+test_message_date_OBJECTS = $(am_test_message_date_OBJECTS)
+am_test_message_decoder_OBJECTS = test-message-decoder.$(OBJEXT)
+test_message_decoder_OBJECTS = $(am_test_message_decoder_OBJECTS)
+am_test_message_header_decode_OBJECTS = \
+ test-message-header-decode.$(OBJEXT)
+test_message_header_decode_OBJECTS = \
+ $(am_test_message_header_decode_OBJECTS)
+am_test_message_header_encode_OBJECTS = \
+ test-message-header-encode.$(OBJEXT)
+test_message_header_encode_OBJECTS = \
+ $(am_test_message_header_encode_OBJECTS)
+am_test_message_header_hash_OBJECTS = \
+ test-message-header-hash.$(OBJEXT)
+test_message_header_hash_OBJECTS = \
+ $(am_test_message_header_hash_OBJECTS)
+am_test_message_header_parser_OBJECTS = \
+ test-message-header-parser.$(OBJEXT)
+test_message_header_parser_OBJECTS = \
+ $(am_test_message_header_parser_OBJECTS)
+am_test_message_id_OBJECTS = test-message-id.$(OBJEXT)
+test_message_id_OBJECTS = $(am_test_message_id_OBJECTS)
+am_test_message_parser_OBJECTS = test-message-parser.$(OBJEXT)
+test_message_parser_OBJECTS = $(am_test_message_parser_OBJECTS)
+am_test_message_part_OBJECTS = test-message-part.$(OBJEXT)
+test_message_part_OBJECTS = $(am_test_message_part_OBJECTS)
+am_test_message_part_serialize_OBJECTS = \
+ test-message-part-serialize.$(OBJEXT)
+test_message_part_serialize_OBJECTS = \
+ $(am_test_message_part_serialize_OBJECTS)
+am_test_message_search_OBJECTS = test-message-search.$(OBJEXT)
+test_message_search_OBJECTS = $(am_test_message_search_OBJECTS)
+am_test_message_size_OBJECTS = test-message-size.$(OBJEXT)
+test_message_size_OBJECTS = $(am_test_message_size_OBJECTS)
+am_test_message_snippet_OBJECTS = test-message-snippet.$(OBJEXT)
+test_message_snippet_OBJECTS = $(am_test_message_snippet_OBJECTS)
+am_test_ostream_dot_OBJECTS = test-ostream-dot.$(OBJEXT)
+test_ostream_dot_OBJECTS = $(am_test_ostream_dot_OBJECTS)
+am_test_qp_decoder_OBJECTS = test-qp-decoder.$(OBJEXT)
+test_qp_decoder_OBJECTS = $(am_test_qp_decoder_OBJECTS)
+am_test_qp_encoder_OBJECTS = test-qp-encoder.$(OBJEXT)
+test_qp_encoder_OBJECTS = $(am_test_qp_encoder_OBJECTS)
+am_test_quoted_printable_OBJECTS = test-quoted-printable.$(OBJEXT)
+test_quoted_printable_OBJECTS = $(am_test_quoted_printable_OBJECTS)
+am_test_rfc2231_parser_OBJECTS = test-rfc2231-parser.$(OBJEXT)
+test_rfc2231_parser_OBJECTS = $(am_test_rfc2231_parser_OBJECTS)
+am_test_rfc822_parser_OBJECTS = test-rfc822-parser.$(OBJEXT)
+test_rfc822_parser_OBJECTS = $(am_test_rfc822_parser_OBJECTS)
+AM_V_P = $(am__v_P_@AM_V@)
+am__v_P_ = $(am__v_P_@AM_DEFAULT_V@)
+am__v_P_0 = false
+am__v_P_1 = :
+AM_V_GEN = $(am__v_GEN_@AM_V@)
+am__v_GEN_ = $(am__v_GEN_@AM_DEFAULT_V@)
+am__v_GEN_0 = @echo " GEN " $@;
+am__v_GEN_1 =
+AM_V_at = $(am__v_at_@AM_V@)
+am__v_at_ = $(am__v_at_@AM_DEFAULT_V@)
+am__v_at_0 = @
+am__v_at_1 =
+DEFAULT_INCLUDES = -I.@am__isrc@ -I$(top_builddir)
+depcomp = $(SHELL) $(top_srcdir)/depcomp
+am__maybe_remake_depfiles = depfiles
+am__depfiles_remade = \
+ ./$(DEPDIR)/fuzz_message_parser-force-cxx-linking.Po \
+ ./$(DEPDIR)/fuzz_message_parser-fuzz-message-parser.Po \
+ ./$(DEPDIR)/istream-attachment-connector.Plo \
+ ./$(DEPDIR)/istream-attachment-extractor.Plo \
+ ./$(DEPDIR)/istream-binary-converter.Plo \
+ ./$(DEPDIR)/istream-dot.Plo \
+ ./$(DEPDIR)/istream-header-filter.Plo \
+ ./$(DEPDIR)/istream-nonuls.Plo \
+ ./$(DEPDIR)/istream-qp-decoder.Plo \
+ ./$(DEPDIR)/istream-qp-encoder.Plo \
+ ./$(DEPDIR)/mail-html2text.Plo ./$(DEPDIR)/mail-user-hash.Plo \
+ ./$(DEPDIR)/mbox-from.Plo ./$(DEPDIR)/message-address.Plo \
+ ./$(DEPDIR)/message-binary-part.Plo \
+ ./$(DEPDIR)/message-date.Plo ./$(DEPDIR)/message-decoder.Plo \
+ ./$(DEPDIR)/message-header-decode.Plo \
+ ./$(DEPDIR)/message-header-encode.Plo \
+ ./$(DEPDIR)/message-header-hash.Plo \
+ ./$(DEPDIR)/message-header-parser.Plo \
+ ./$(DEPDIR)/message-id.Plo \
+ ./$(DEPDIR)/message-parser-from-parts.Plo \
+ ./$(DEPDIR)/message-parser.Plo \
+ ./$(DEPDIR)/message-part-data.Plo \
+ ./$(DEPDIR)/message-part-serialize.Plo \
+ ./$(DEPDIR)/message-part.Plo ./$(DEPDIR)/message-search.Plo \
+ ./$(DEPDIR)/message-size.Plo ./$(DEPDIR)/message-snippet.Plo \
+ ./$(DEPDIR)/ostream-dot.Plo ./$(DEPDIR)/qp-decoder.Plo \
+ ./$(DEPDIR)/qp-encoder.Plo ./$(DEPDIR)/quoted-printable.Plo \
+ ./$(DEPDIR)/rfc2231-parser.Plo ./$(DEPDIR)/rfc822-parser.Plo \
+ ./$(DEPDIR)/test-istream-attachment.Po \
+ ./$(DEPDIR)/test-istream-binary-converter.Po \
+ ./$(DEPDIR)/test-istream-dot.Po \
+ ./$(DEPDIR)/test-istream-header-filter.Po \
+ ./$(DEPDIR)/test-istream-qp-decoder.Po \
+ ./$(DEPDIR)/test-istream-qp-encoder.Po \
+ ./$(DEPDIR)/test-mail-html2text.Po \
+ ./$(DEPDIR)/test-mail-user-hash.Po \
+ ./$(DEPDIR)/test-mbox-from.Po \
+ ./$(DEPDIR)/test-message-address.Po \
+ ./$(DEPDIR)/test-message-date.Po \
+ ./$(DEPDIR)/test-message-decoder.Po \
+ ./$(DEPDIR)/test-message-header-decode.Po \
+ ./$(DEPDIR)/test-message-header-encode.Po \
+ ./$(DEPDIR)/test-message-header-hash.Po \
+ ./$(DEPDIR)/test-message-header-parser.Po \
+ ./$(DEPDIR)/test-message-id.Po \
+ ./$(DEPDIR)/test-message-parser.Po \
+ ./$(DEPDIR)/test-message-part-serialize.Po \
+ ./$(DEPDIR)/test-message-part.Po \
+ ./$(DEPDIR)/test-message-search.Po \
+ ./$(DEPDIR)/test-message-size.Po \
+ ./$(DEPDIR)/test-message-snippet.Po \
+ ./$(DEPDIR)/test-ostream-dot.Po ./$(DEPDIR)/test-qp-decoder.Po \
+ ./$(DEPDIR)/test-qp-encoder.Po \
+ ./$(DEPDIR)/test-quoted-printable.Po \
+ ./$(DEPDIR)/test-rfc2231-parser.Po \
+ ./$(DEPDIR)/test-rfc822-parser.Po
+am__mv = mv -f
+COMPILE = $(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) \
+ $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS)
+LTCOMPILE = $(LIBTOOL) $(AM_V_lt) --tag=CC $(AM_LIBTOOLFLAGS) \
+ $(LIBTOOLFLAGS) --mode=compile $(CC) $(DEFS) \
+ $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) $(CPPFLAGS) \
+ $(AM_CFLAGS) $(CFLAGS)
+AM_V_CC = $(am__v_CC_@AM_V@)
+am__v_CC_ = $(am__v_CC_@AM_DEFAULT_V@)
+am__v_CC_0 = @echo " CC " $@;
+am__v_CC_1 =
+CCLD = $(CC)
+LINK = $(LIBTOOL) $(AM_V_lt) --tag=CC $(AM_LIBTOOLFLAGS) \
+ $(LIBTOOLFLAGS) --mode=link $(CCLD) $(AM_CFLAGS) $(CFLAGS) \
+ $(AM_LDFLAGS) $(LDFLAGS) -o $@
+AM_V_CCLD = $(am__v_CCLD_@AM_V@)
+am__v_CCLD_ = $(am__v_CCLD_@AM_DEFAULT_V@)
+am__v_CCLD_0 = @echo " CCLD " $@;
+am__v_CCLD_1 =
+CXXCOMPILE = $(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) \
+ $(AM_CPPFLAGS) $(CPPFLAGS) $(AM_CXXFLAGS) $(CXXFLAGS)
+LTCXXCOMPILE = $(LIBTOOL) $(AM_V_lt) --tag=CXX $(AM_LIBTOOLFLAGS) \
+ $(LIBTOOLFLAGS) --mode=compile $(CXX) $(DEFS) \
+ $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) $(CPPFLAGS) \
+ $(AM_CXXFLAGS) $(CXXFLAGS)
+AM_V_CXX = $(am__v_CXX_@AM_V@)
+am__v_CXX_ = $(am__v_CXX_@AM_DEFAULT_V@)
+am__v_CXX_0 = @echo " CXX " $@;
+am__v_CXX_1 =
+CXXLD = $(CXX)
+CXXLINK = $(LIBTOOL) $(AM_V_lt) --tag=CXX $(AM_LIBTOOLFLAGS) \
+ $(LIBTOOLFLAGS) --mode=link $(CXXLD) $(AM_CXXFLAGS) \
+ $(CXXFLAGS) $(AM_LDFLAGS) $(LDFLAGS) -o $@
+AM_V_CXXLD = $(am__v_CXXLD_@AM_V@)
+am__v_CXXLD_ = $(am__v_CXXLD_@AM_DEFAULT_V@)
+am__v_CXXLD_0 = @echo " CXXLD " $@;
+am__v_CXXLD_1 =
+SOURCES = $(libmail_la_SOURCES) $(fuzz_message_parser_SOURCES) \
+ $(nodist_EXTRA_fuzz_message_parser_SOURCES) \
+ $(test_istream_attachment_SOURCES) \
+ $(test_istream_binary_converter_SOURCES) \
+ $(test_istream_dot_SOURCES) \
+ $(test_istream_header_filter_SOURCES) \
+ $(test_istream_qp_decoder_SOURCES) \
+ $(test_istream_qp_encoder_SOURCES) \
+ $(test_mail_html2text_SOURCES) $(test_mail_user_hash_SOURCES) \
+ $(test_mbox_from_SOURCES) $(test_message_address_SOURCES) \
+ $(test_message_date_SOURCES) $(test_message_decoder_SOURCES) \
+ $(test_message_header_decode_SOURCES) \
+ $(test_message_header_encode_SOURCES) \
+ $(test_message_header_hash_SOURCES) \
+ $(test_message_header_parser_SOURCES) \
+ $(test_message_id_SOURCES) $(test_message_parser_SOURCES) \
+ $(test_message_part_SOURCES) \
+ $(test_message_part_serialize_SOURCES) \
+ $(test_message_search_SOURCES) $(test_message_size_SOURCES) \
+ $(test_message_snippet_SOURCES) $(test_ostream_dot_SOURCES) \
+ $(test_qp_decoder_SOURCES) $(test_qp_encoder_SOURCES) \
+ $(test_quoted_printable_SOURCES) \
+ $(test_rfc2231_parser_SOURCES) $(test_rfc822_parser_SOURCES)
+DIST_SOURCES = $(libmail_la_SOURCES) \
+ $(am__fuzz_message_parser_SOURCES_DIST) \
+ $(test_istream_attachment_SOURCES) \
+ $(test_istream_binary_converter_SOURCES) \
+ $(test_istream_dot_SOURCES) \
+ $(test_istream_header_filter_SOURCES) \
+ $(test_istream_qp_decoder_SOURCES) \
+ $(test_istream_qp_encoder_SOURCES) \
+ $(test_mail_html2text_SOURCES) $(test_mail_user_hash_SOURCES) \
+ $(test_mbox_from_SOURCES) $(test_message_address_SOURCES) \
+ $(test_message_date_SOURCES) $(test_message_decoder_SOURCES) \
+ $(test_message_header_decode_SOURCES) \
+ $(test_message_header_encode_SOURCES) \
+ $(test_message_header_hash_SOURCES) \
+ $(test_message_header_parser_SOURCES) \
+ $(test_message_id_SOURCES) $(test_message_parser_SOURCES) \
+ $(test_message_part_SOURCES) \
+ $(test_message_part_serialize_SOURCES) \
+ $(test_message_search_SOURCES) $(test_message_size_SOURCES) \
+ $(test_message_snippet_SOURCES) $(test_ostream_dot_SOURCES) \
+ $(test_qp_decoder_SOURCES) $(test_qp_encoder_SOURCES) \
+ $(test_quoted_printable_SOURCES) \
+ $(test_rfc2231_parser_SOURCES) $(test_rfc822_parser_SOURCES)
+am__can_run_installinfo = \
+ case $$AM_UPDATE_INFO_DIR in \
+ n|no|NO) false;; \
+ *) (install-info --version) >/dev/null 2>&1;; \
+ esac
+am__vpath_adj_setup = srcdirstrip=`echo "$(srcdir)" | sed 's|.|.|g'`;
+am__vpath_adj = case $$p in \
+ $(srcdir)/*) f=`echo "$$p" | sed "s|^$$srcdirstrip/||"`;; \
+ *) f=$$p;; \
+ esac;
+am__strip_dir = f=`echo $$p | sed -e 's|^.*/||'`;
+am__install_max = 40
+am__nobase_strip_setup = \
+ srcdirstrip=`echo "$(srcdir)" | sed 's/[].[^$$\\*|]/\\\\&/g'`
+am__nobase_strip = \
+ for p in $$list; do echo "$$p"; done | sed -e "s|$$srcdirstrip/||"
+am__nobase_list = $(am__nobase_strip_setup); \
+ for p in $$list; do echo "$$p $$p"; done | \
+ sed "s| $$srcdirstrip/| |;"' / .*\//!s/ .*/ ./; s,\( .*\)/[^/]*$$,\1,' | \
+ $(AWK) 'BEGIN { files["."] = "" } { files[$$2] = files[$$2] " " $$1; \
+ if (++n[$$2] == $(am__install_max)) \
+ { print $$2, files[$$2]; n[$$2] = 0; files[$$2] = "" } } \
+ END { for (dir in files) print dir, files[dir] }'
+am__base_list = \
+ sed '$$!N;$$!N;$$!N;$$!N;$$!N;$$!N;$$!N;s/\n/ /g' | \
+ sed '$$!N;$$!N;$$!N;$$!N;s/\n/ /g'
+am__uninstall_files_from_dir = { \
+ test -z "$$files" \
+ || { test ! -d "$$dir" && test ! -f "$$dir" && test ! -r "$$dir"; } \
+ || { echo " ( cd '$$dir' && rm -f" $$files ")"; \
+ $(am__cd) "$$dir" && rm -f $$files; }; \
+ }
+am__installdirs = "$(DESTDIR)$(pkginc_libdir)"
+HEADERS = $(noinst_HEADERS) $(pkginc_lib_HEADERS)
+am__tagged_files = $(HEADERS) $(SOURCES) $(TAGS_FILES) $(LISP)
+# Read a list of newline-separated strings from the standard input,
+# and print each of them once, without duplicates. Input order is
+# *not* preserved.
+am__uniquify_input = $(AWK) '\
+ BEGIN { nonempty = 0; } \
+ { items[$$0] = 1; nonempty = 1; } \
+ END { if (nonempty) { for (i in items) print i; }; } \
+'
+# Make sure the list of sources is unique. This is necessary because,
+# e.g., the same source file might be shared among _SOURCES variables
+# for different programs/libraries.
+am__define_uniq_tagged_files = \
+ list='$(am__tagged_files)'; \
+ unique=`for i in $$list; do \
+ if test -f "$$i"; then echo $$i; else echo $(srcdir)/$$i; fi; \
+ done | $(am__uniquify_input)`
+ETAGS = etags
+CTAGS = ctags
+am__DIST_COMMON = $(srcdir)/Makefile.in $(top_srcdir)/depcomp
+DISTFILES = $(DIST_COMMON) $(DIST_SOURCES) $(TEXINFOS) $(EXTRA_DIST)
+ACLOCAL = @ACLOCAL@
+ACLOCAL_AMFLAGS = @ACLOCAL_AMFLAGS@
+AMTAR = @AMTAR@
+AM_DEFAULT_VERBOSITY = @AM_DEFAULT_VERBOSITY@
+APPARMOR_LIBS = @APPARMOR_LIBS@
+AR = @AR@
+AUTH_CFLAGS = @AUTH_CFLAGS@
+AUTH_LIBS = @AUTH_LIBS@
+AUTOCONF = @AUTOCONF@
+AUTOHEADER = @AUTOHEADER@
+AUTOMAKE = @AUTOMAKE@
+AWK = @AWK@
+BINARY_CFLAGS = @BINARY_CFLAGS@
+BINARY_LDFLAGS = @BINARY_LDFLAGS@
+BISON = @BISON@
+CASSANDRA_CFLAGS = @CASSANDRA_CFLAGS@
+CASSANDRA_LIBS = @CASSANDRA_LIBS@
+CC = @CC@
+CCDEPMODE = @CCDEPMODE@
+CDB_LIBS = @CDB_LIBS@
+CFLAGS = @CFLAGS@
+CLUCENE_CFLAGS = @CLUCENE_CFLAGS@
+CLUCENE_LIBS = @CLUCENE_LIBS@
+COMPRESS_LIBS = @COMPRESS_LIBS@
+CPP = @CPP@
+CPPFLAGS = @CPPFLAGS@
+CRYPT_LIBS = @CRYPT_LIBS@
+CXX = @CXX@
+CXXCPP = @CXXCPP@
+CXXDEPMODE = @CXXDEPMODE@
+CXXFLAGS = @CXXFLAGS@
+CYGPATH_W = @CYGPATH_W@
+DEFS = @DEFS@
+DEPDIR = @DEPDIR@
+DICT_LIBS = @DICT_LIBS@
+DLLIB = @DLLIB@
+DLLTOOL = @DLLTOOL@
+DSYMUTIL = @DSYMUTIL@
+DUMPBIN = @DUMPBIN@
+ECHO_C = @ECHO_C@
+ECHO_N = @ECHO_N@
+ECHO_T = @ECHO_T@
+EGREP = @EGREP@
+EXEEXT = @EXEEXT@
+FGREP = @FGREP@
+FLEX = @FLEX@
+FUZZER_CPPFLAGS = @FUZZER_CPPFLAGS@
+FUZZER_LDFLAGS = @FUZZER_LDFLAGS@
+GREP = @GREP@
+INSTALL = @INSTALL@
+INSTALL_DATA = @INSTALL_DATA@
+INSTALL_PROGRAM = @INSTALL_PROGRAM@
+INSTALL_SCRIPT = @INSTALL_SCRIPT@
+INSTALL_STRIP_PROGRAM = @INSTALL_STRIP_PROGRAM@
+KRB5CONFIG = @KRB5CONFIG@
+KRB5_CFLAGS = @KRB5_CFLAGS@
+KRB5_LIBS = @KRB5_LIBS@
+LD = @LD@
+LDAP_LIBS = @LDAP_LIBS@
+LDFLAGS = @LDFLAGS@
+LD_NO_WHOLE_ARCHIVE = @LD_NO_WHOLE_ARCHIVE@
+LD_WHOLE_ARCHIVE = @LD_WHOLE_ARCHIVE@
+LIBCAP = @LIBCAP@
+LIBDOVECOT = @LIBDOVECOT@
+LIBDOVECOT_COMPRESS = @LIBDOVECOT_COMPRESS@
+LIBDOVECOT_DEPS = @LIBDOVECOT_DEPS@
+LIBDOVECOT_DSYNC = @LIBDOVECOT_DSYNC@
+LIBDOVECOT_LA_LIBS = @LIBDOVECOT_LA_LIBS@
+LIBDOVECOT_LDA = @LIBDOVECOT_LDA@
+LIBDOVECOT_LDAP = @LIBDOVECOT_LDAP@
+LIBDOVECOT_LIBFTS = @LIBDOVECOT_LIBFTS@
+LIBDOVECOT_LIBFTS_DEPS = @LIBDOVECOT_LIBFTS_DEPS@
+LIBDOVECOT_LOGIN = @LIBDOVECOT_LOGIN@
+LIBDOVECOT_LUA = @LIBDOVECOT_LUA@
+LIBDOVECOT_LUA_DEPS = @LIBDOVECOT_LUA_DEPS@
+LIBDOVECOT_SQL = @LIBDOVECOT_SQL@
+LIBDOVECOT_STORAGE = @LIBDOVECOT_STORAGE@
+LIBDOVECOT_STORAGE_DEPS = @LIBDOVECOT_STORAGE_DEPS@
+LIBEXTTEXTCAT_CFLAGS = @LIBEXTTEXTCAT_CFLAGS@
+LIBEXTTEXTCAT_LIBS = @LIBEXTTEXTCAT_LIBS@
+LIBICONV = @LIBICONV@
+LIBICU_CFLAGS = @LIBICU_CFLAGS@
+LIBICU_LIBS = @LIBICU_LIBS@
+LIBOBJS = @LIBOBJS@
+LIBS = @LIBS@
+LIBSODIUM_CFLAGS = @LIBSODIUM_CFLAGS@
+LIBSODIUM_LIBS = @LIBSODIUM_LIBS@
+LIBTIRPC_CFLAGS = @LIBTIRPC_CFLAGS@
+LIBTIRPC_LIBS = @LIBTIRPC_LIBS@
+LIBTOOL = @LIBTOOL@
+LIBUNWIND_CFLAGS = @LIBUNWIND_CFLAGS@
+LIBUNWIND_LIBS = @LIBUNWIND_LIBS@
+LIBWRAP_LIBS = @LIBWRAP_LIBS@
+LINKED_STORAGE_LDADD = @LINKED_STORAGE_LDADD@
+LIPO = @LIPO@
+LN_S = @LN_S@
+LTLIBICONV = @LTLIBICONV@
+LTLIBOBJS = @LTLIBOBJS@
+LT_SYS_LIBRARY_PATH = @LT_SYS_LIBRARY_PATH@
+LUA_CFLAGS = @LUA_CFLAGS@
+LUA_LIBS = @LUA_LIBS@
+MAINT = @MAINT@
+MAKEINFO = @MAKEINFO@
+MANIFEST_TOOL = @MANIFEST_TOOL@
+MKDIR_P = @MKDIR_P@
+MODULE_LIBS = @MODULE_LIBS@
+MODULE_SUFFIX = @MODULE_SUFFIX@
+MYSQL_CFLAGS = @MYSQL_CFLAGS@
+MYSQL_CONFIG = @MYSQL_CONFIG@
+MYSQL_LIBS = @MYSQL_LIBS@
+NM = @NM@
+NMEDIT = @NMEDIT@
+NOPLUGIN_LDFLAGS = @NOPLUGIN_LDFLAGS@
+OBJDUMP = @OBJDUMP@
+OBJEXT = @OBJEXT@
+OTOOL = @OTOOL@
+OTOOL64 = @OTOOL64@
+PACKAGE = @PACKAGE@
+PACKAGE_BUGREPORT = @PACKAGE_BUGREPORT@
+PACKAGE_NAME = @PACKAGE_NAME@
+PACKAGE_STRING = @PACKAGE_STRING@
+PACKAGE_TARNAME = @PACKAGE_TARNAME@
+PACKAGE_URL = @PACKAGE_URL@
+PACKAGE_VERSION = @PACKAGE_VERSION@
+PANDOC = @PANDOC@
+PATH_SEPARATOR = @PATH_SEPARATOR@
+PGSQL_CFLAGS = @PGSQL_CFLAGS@
+PGSQL_LIBS = @PGSQL_LIBS@
+PG_CONFIG = @PG_CONFIG@
+PIE_CFLAGS = @PIE_CFLAGS@
+PIE_LDFLAGS = @PIE_LDFLAGS@
+PKG_CONFIG = @PKG_CONFIG@
+PKG_CONFIG_LIBDIR = @PKG_CONFIG_LIBDIR@
+PKG_CONFIG_PATH = @PKG_CONFIG_PATH@
+QUOTA_LIBS = @QUOTA_LIBS@
+RANLIB = @RANLIB@
+RELRO_LDFLAGS = @RELRO_LDFLAGS@
+RPCGEN = @RPCGEN@
+RUN_TEST = @RUN_TEST@
+SED = @SED@
+SETTING_FILES = @SETTING_FILES@
+SET_MAKE = @SET_MAKE@
+SHELL = @SHELL@
+SQLITE_CFLAGS = @SQLITE_CFLAGS@
+SQLITE_LIBS = @SQLITE_LIBS@
+SQL_CFLAGS = @SQL_CFLAGS@
+SQL_LIBS = @SQL_LIBS@
+SSL_CFLAGS = @SSL_CFLAGS@
+SSL_LIBS = @SSL_LIBS@
+STRIP = @STRIP@
+SYSTEMD_CFLAGS = @SYSTEMD_CFLAGS@
+SYSTEMD_LIBS = @SYSTEMD_LIBS@
+VALGRIND = @VALGRIND@
+VERSION = @VERSION@
+ZSTD_CFLAGS = @ZSTD_CFLAGS@
+ZSTD_LIBS = @ZSTD_LIBS@
+abs_builddir = @abs_builddir@
+abs_srcdir = @abs_srcdir@
+abs_top_builddir = @abs_top_builddir@
+abs_top_srcdir = @abs_top_srcdir@
+ac_ct_AR = @ac_ct_AR@
+ac_ct_CC = @ac_ct_CC@
+ac_ct_CXX = @ac_ct_CXX@
+ac_ct_DUMPBIN = @ac_ct_DUMPBIN@
+am__include = @am__include@
+am__leading_dot = @am__leading_dot@
+am__quote = @am__quote@
+am__tar = @am__tar@
+am__untar = @am__untar@
+bindir = @bindir@
+build = @build@
+build_alias = @build_alias@
+build_cpu = @build_cpu@
+build_os = @build_os@
+build_vendor = @build_vendor@
+builddir = @builddir@
+datadir = @datadir@
+datarootdir = @datarootdir@
+dict_drivers = @dict_drivers@
+docdir = @docdir@
+dvidir = @dvidir@
+exec_prefix = @exec_prefix@
+host = @host@
+host_alias = @host_alias@
+host_cpu = @host_cpu@
+host_os = @host_os@
+host_vendor = @host_vendor@
+htmldir = @htmldir@
+includedir = @includedir@
+infodir = @infodir@
+install_sh = @install_sh@
+libdir = @libdir@
+libexecdir = @libexecdir@
+localedir = @localedir@
+localstatedir = @localstatedir@
+mandir = @mandir@
+mkdir_p = @mkdir_p@
+moduledir = @moduledir@
+oldincludedir = @oldincludedir@
+pdfdir = @pdfdir@
+prefix = @prefix@
+program_transform_name = @program_transform_name@
+psdir = @psdir@
+rundir = @rundir@
+runstatedir = @runstatedir@
+sbindir = @sbindir@
+sharedstatedir = @sharedstatedir@
+sql_drivers = @sql_drivers@
+srcdir = @srcdir@
+ssldir = @ssldir@
+statedir = @statedir@
+sysconfdir = @sysconfdir@
+systemdservicetype = @systemdservicetype@
+systemdsystemunitdir = @systemdsystemunitdir@
+target_alias = @target_alias@
+top_build_prefix = @top_build_prefix@
+top_builddir = @top_builddir@
+top_srcdir = @top_srcdir@
+noinst_LTLIBRARIES = libmail.la
+AM_CPPFLAGS = \
+ -I$(top_srcdir)/src/lib \
+ -I$(top_srcdir)/src/lib-test \
+ -I$(top_srcdir)/src/lib-charset \
+ -I$(top_srcdir)/src/lib-smtp
+
+libmail_la_SOURCES = \
+ istream-attachment-connector.c \
+ istream-attachment-extractor.c \
+ istream-binary-converter.c \
+ istream-dot.c \
+ istream-header-filter.c \
+ istream-nonuls.c \
+ istream-qp-decoder.c \
+ istream-qp-encoder.c \
+ mail-html2text.c \
+ mail-user-hash.c \
+ mbox-from.c \
+ message-address.c \
+ message-binary-part.c \
+ message-date.c \
+ message-decoder.c \
+ message-header-decode.c \
+ message-header-encode.c \
+ message-header-hash.c \
+ message-header-parser.c \
+ message-id.c \
+ message-parser.c \
+ message-parser-from-parts.c \
+ message-part.c \
+ message-part-data.c \
+ message-part-serialize.c \
+ message-search.c \
+ message-size.c \
+ message-snippet.c \
+ ostream-dot.c \
+ qp-decoder.c \
+ qp-encoder.c \
+ quoted-printable.c \
+ rfc2231-parser.c \
+ rfc822-parser.c
+
+noinst_HEADERS = \
+ html-entities.h \
+ message-parser-private.h
+
+headers = \
+ istream-attachment-connector.h \
+ istream-attachment-extractor.h \
+ istream-binary-converter.h \
+ istream-dot.h \
+ istream-header-filter.h \
+ istream-nonuls.h \
+ istream-qp.h \
+ mail-user-hash.h \
+ mbox-from.h \
+ mail-html2text.h \
+ mail-types.h \
+ message-address.h \
+ message-binary-part.h \
+ message-date.h \
+ message-decoder.h \
+ message-header-decode.h \
+ message-header-encode.h \
+ message-header-hash.h \
+ message-header-parser.h \
+ message-id.h \
+ message-parser.h \
+ message-part.h \
+ message-part-data.h \
+ message-part-serialize.h \
+ message-search.h \
+ message-size.h \
+ message-snippet.h \
+ ostream-dot.h \
+ qp-decoder.h \
+ qp-encoder.h \
+ quoted-printable.h \
+ rfc2231-parser.h \
+ rfc822-parser.h
+
+pkginc_libdir = $(pkgincludedir)
+pkginc_lib_HEADERS = $(headers)
+test_programs = \
+ test-istream-dot \
+ test-istream-attachment \
+ test-istream-binary-converter \
+ test-istream-header-filter \
+ test-istream-qp-decoder \
+ test-istream-qp-encoder \
+ test-mail-html2text \
+ test-mail-user-hash \
+ test-mbox-from \
+ test-message-address \
+ test-message-date \
+ test-message-decoder \
+ test-message-header-decode \
+ test-message-header-encode \
+ test-message-header-hash \
+ test-message-header-parser \
+ test-message-id \
+ test-message-parser \
+ test-message-part \
+ test-message-part-serialize \
+ test-message-search \
+ test-message-size \
+ test-message-snippet \
+ test-ostream-dot \
+ test-qp-decoder \
+ test-qp-encoder \
+ test-quoted-printable \
+ test-rfc2231-parser \
+ test-rfc822-parser
+
+fuzz_programs = $(am__append_1)
+@USE_FUZZER_TRUE@nodist_EXTRA_fuzz_message_parser_SOURCES = force-cxx-linking.cxx
+@USE_FUZZER_TRUE@fuzz_message_parser_CPPFLAGS = $(FUZZER_CPPFLAGS)
+@USE_FUZZER_TRUE@fuzz_message_parser_LDFLAGS = $(FUZZER_LDFLAGS)
+@USE_FUZZER_TRUE@fuzz_message_parser_SOURCES = fuzz-message-parser.c
+@USE_FUZZER_TRUE@fuzz_message_parser_LDADD = $(test_libs)
+@USE_FUZZER_TRUE@fuzz_message_parser_DEPENDENCIES = $(test_deps)
+test_libs = \
+ $(noinst_LTLIBRARIES) \
+ ../lib-charset/libcharset.la \
+ ../lib-test/libtest.la \
+ ../lib/liblib.la
+
+test_deps = $(noinst_LTLIBRARIES) $(test_libs)
+test_istream_dot_SOURCES = test-istream-dot.c
+test_istream_dot_LDADD = $(test_libs)
+test_istream_dot_DEPENDENCIES = $(test_deps)
+test_istream_qp_decoder_SOURCES = test-istream-qp-decoder.c
+test_istream_qp_decoder_LDADD = $(test_libs)
+test_istream_qp_decoder_DEPENDENCIES = $(test_deps)
+test_istream_qp_encoder_SOURCES = test-istream-qp-encoder.c
+test_istream_qp_encoder_LDADD = $(test_libs)
+test_istream_qp_encoder_DEPENDENCIES = $(test_deps)
+test_istream_binary_converter_SOURCES = test-istream-binary-converter.c
+test_istream_binary_converter_LDADD = $(test_libs)
+test_istream_binary_converter_DEPENDENCIES = $(test_deps)
+test_istream_attachment_SOURCES = test-istream-attachment.c
+test_istream_attachment_LDADD = $(test_libs)
+test_istream_attachment_DEPENDENCIES = $(test_deps)
+test_istream_header_filter_SOURCES = test-istream-header-filter.c
+test_istream_header_filter_LDADD = $(test_libs)
+test_istream_header_filter_DEPENDENCIES = $(test_deps)
+test_mbox_from_SOURCES = test-mbox-from.c
+test_mbox_from_LDADD = $(test_libs)
+test_mbox_from_DEPENDENCIES = $(test_deps)
+test_message_address_SOURCES = test-message-address.c
+test_message_address_LDADD = $(test_libs)
+test_message_address_DEPENDENCIES = $(test_deps)
+test_message_date_SOURCES = test-message-date.c
+test_message_date_LDADD = $(test_libs)
+test_message_date_DEPENDENCIES = $(test_deps)
+test_message_decoder_SOURCES = test-message-decoder.c
+test_message_decoder_LDADD = $(test_libs) ../lib-charset/libcharset.la
+test_message_decoder_DEPENDENCIES = $(test_deps) ../lib-charset/libcharset.la
+test_message_header_decode_SOURCES = test-message-header-decode.c
+test_message_header_decode_LDADD = $(test_libs)
+test_message_header_decode_DEPENDENCIES = $(test_deps)
+test_message_header_encode_SOURCES = test-message-header-encode.c
+test_message_header_encode_LDADD = $(test_libs)
+test_message_header_encode_DEPENDENCIES = $(test_deps)
+test_message_header_hash_SOURCES = test-message-header-hash.c
+test_message_header_hash_LDADD = $(test_libs)
+test_message_header_hash_DEPENDENCIES = $(test_deps)
+test_message_header_parser_SOURCES = test-message-header-parser.c
+test_message_header_parser_LDADD = $(test_libs)
+test_message_header_parser_DEPENDENCIES = $(test_deps)
+test_message_id_SOURCES = test-message-id.c
+test_message_id_LDADD = $(test_libs)
+test_message_id_DEPENDENCIES = $(test_deps)
+test_message_parser_SOURCES = test-message-parser.c
+test_message_parser_LDADD = $(test_libs)
+test_message_parser_DEPENDENCIES = $(test_deps)
+test_message_part_SOURCES = test-message-part.c
+test_message_part_LDADD = $(test_libs)
+test_message_part_DEPENDENCIES = $(test_deps)
+test_message_search_SOURCES = test-message-search.c
+test_message_search_LDADD = $(test_libs) ../lib-charset/libcharset.la
+test_message_search_DEPENDENCIES = $(test_deps) ../lib-charset/libcharset.la
+test_message_size_SOURCES = test-message-size.c
+test_message_size_LDADD = $(test_libs)
+test_message_size_DEPENDENCIES = $(test_deps)
+test_message_snippet_SOURCES = test-message-snippet.c
+test_message_snippet_LDADD = $(test_message_decoder_LDADD)
+test_message_snippet_DEPENDENCIES = $(test_deps)
+test_mail_html2text_SOURCES = test-mail-html2text.c
+test_mail_html2text_LDADD = $(test_libs)
+test_mail_html2text_DEPENDENCIES = $(test_deps)
+test_ostream_dot_SOURCES = test-ostream-dot.c
+test_ostream_dot_LDADD = $(test_libs)
+test_ostream_dot_DEPENDENCIES = $(test_deps)
+test_qp_decoder_SOURCES = test-qp-decoder.c
+test_qp_decoder_LDADD = $(test_libs)
+test_qp_decoder_DEPENDENCIES = $(test_deps)
+test_qp_encoder_SOURCES = test-qp-encoder.c
+test_qp_encoder_LDADD = $(test_libs)
+test_qp_encoder_DEPENDENCIES = $(test_deps)
+test_quoted_printable_SOURCES = test-quoted-printable.c
+test_quoted_printable_LDADD = $(test_libs)
+test_quoted_printable_DEPENDENCIES = $(test_deps)
+test_rfc2231_parser_SOURCES = test-rfc2231-parser.c
+test_rfc2231_parser_LDADD = $(test_libs)
+test_rfc2231_parser_DEPENDENCIES = $(test_deps)
+test_rfc822_parser_SOURCES = test-rfc822-parser.c
+test_rfc822_parser_LDADD = $(test_libs)
+test_rfc822_parser_DEPENDENCIES = $(test_deps)
+test_mail_user_hash_SOURCES = test-mail-user-hash.c
+test_mail_user_hash_LDADD = $(test_libs)
+test_mail_user_hash_DEPENDENCIES = $(test_deps)
+test_message_part_serialize_SOURCES = test-message-part-serialize.c
+test_message_part_serialize_LDADD = $(test_libs)
+test_message_part_serialize_DEPENDENCIES = $(test_deps)
+all: all-am
+
+.SUFFIXES:
+.SUFFIXES: .c .cxx .lo .o .obj
+$(srcdir)/Makefile.in: @MAINTAINER_MODE_TRUE@ $(srcdir)/Makefile.am $(am__configure_deps)
+ @for dep in $?; do \
+ case '$(am__configure_deps)' in \
+ *$$dep*) \
+ ( cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh ) \
+ && { if test -f $@; then exit 0; else break; fi; }; \
+ exit 1;; \
+ esac; \
+ done; \
+ echo ' cd $(top_srcdir) && $(AUTOMAKE) --foreign src/lib-mail/Makefile'; \
+ $(am__cd) $(top_srcdir) && \
+ $(AUTOMAKE) --foreign src/lib-mail/Makefile
+Makefile: $(srcdir)/Makefile.in $(top_builddir)/config.status
+ @case '$?' in \
+ *config.status*) \
+ cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh;; \
+ *) \
+ echo ' cd $(top_builddir) && $(SHELL) ./config.status $(subdir)/$@ $(am__maybe_remake_depfiles)'; \
+ cd $(top_builddir) && $(SHELL) ./config.status $(subdir)/$@ $(am__maybe_remake_depfiles);; \
+ esac;
+
+$(top_builddir)/config.status: $(top_srcdir)/configure $(CONFIG_STATUS_DEPENDENCIES)
+ cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh
+
+$(top_srcdir)/configure: @MAINTAINER_MODE_TRUE@ $(am__configure_deps)
+ cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh
+$(ACLOCAL_M4): @MAINTAINER_MODE_TRUE@ $(am__aclocal_m4_deps)
+ cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh
+$(am__aclocal_m4_deps):
+
+clean-noinstPROGRAMS:
+ @list='$(noinst_PROGRAMS)'; test -n "$$list" || exit 0; \
+ echo " rm -f" $$list; \
+ rm -f $$list || exit $$?; \
+ test -n "$(EXEEXT)" || exit 0; \
+ list=`for p in $$list; do echo "$$p"; done | sed 's/$(EXEEXT)$$//'`; \
+ echo " rm -f" $$list; \
+ rm -f $$list
+
+clean-noinstLTLIBRARIES:
+ -test -z "$(noinst_LTLIBRARIES)" || rm -f $(noinst_LTLIBRARIES)
+ @list='$(noinst_LTLIBRARIES)'; \
+ locs=`for p in $$list; do echo $$p; done | \
+ sed 's|^[^/]*$$|.|; s|/[^/]*$$||; s|$$|/so_locations|' | \
+ sort -u`; \
+ test -z "$$locs" || { \
+ echo rm -f $${locs}; \
+ rm -f $${locs}; \
+ }
+
+libmail.la: $(libmail_la_OBJECTS) $(libmail_la_DEPENDENCIES) $(EXTRA_libmail_la_DEPENDENCIES)
+ $(AM_V_CCLD)$(LINK) $(libmail_la_OBJECTS) $(libmail_la_LIBADD) $(LIBS)
+
+fuzz-message-parser$(EXEEXT): $(fuzz_message_parser_OBJECTS) $(fuzz_message_parser_DEPENDENCIES) $(EXTRA_fuzz_message_parser_DEPENDENCIES)
+ @rm -f fuzz-message-parser$(EXEEXT)
+ $(AM_V_CXXLD)$(fuzz_message_parser_LINK) $(fuzz_message_parser_OBJECTS) $(fuzz_message_parser_LDADD) $(LIBS)
+
+test-istream-attachment$(EXEEXT): $(test_istream_attachment_OBJECTS) $(test_istream_attachment_DEPENDENCIES) $(EXTRA_test_istream_attachment_DEPENDENCIES)
+ @rm -f test-istream-attachment$(EXEEXT)
+ $(AM_V_CCLD)$(LINK) $(test_istream_attachment_OBJECTS) $(test_istream_attachment_LDADD) $(LIBS)
+
+test-istream-binary-converter$(EXEEXT): $(test_istream_binary_converter_OBJECTS) $(test_istream_binary_converter_DEPENDENCIES) $(EXTRA_test_istream_binary_converter_DEPENDENCIES)
+ @rm -f test-istream-binary-converter$(EXEEXT)
+ $(AM_V_CCLD)$(LINK) $(test_istream_binary_converter_OBJECTS) $(test_istream_binary_converter_LDADD) $(LIBS)
+
+test-istream-dot$(EXEEXT): $(test_istream_dot_OBJECTS) $(test_istream_dot_DEPENDENCIES) $(EXTRA_test_istream_dot_DEPENDENCIES)
+ @rm -f test-istream-dot$(EXEEXT)
+ $(AM_V_CCLD)$(LINK) $(test_istream_dot_OBJECTS) $(test_istream_dot_LDADD) $(LIBS)
+
+test-istream-header-filter$(EXEEXT): $(test_istream_header_filter_OBJECTS) $(test_istream_header_filter_DEPENDENCIES) $(EXTRA_test_istream_header_filter_DEPENDENCIES)
+ @rm -f test-istream-header-filter$(EXEEXT)
+ $(AM_V_CCLD)$(LINK) $(test_istream_header_filter_OBJECTS) $(test_istream_header_filter_LDADD) $(LIBS)
+
+test-istream-qp-decoder$(EXEEXT): $(test_istream_qp_decoder_OBJECTS) $(test_istream_qp_decoder_DEPENDENCIES) $(EXTRA_test_istream_qp_decoder_DEPENDENCIES)
+ @rm -f test-istream-qp-decoder$(EXEEXT)
+ $(AM_V_CCLD)$(LINK) $(test_istream_qp_decoder_OBJECTS) $(test_istream_qp_decoder_LDADD) $(LIBS)
+
+test-istream-qp-encoder$(EXEEXT): $(test_istream_qp_encoder_OBJECTS) $(test_istream_qp_encoder_DEPENDENCIES) $(EXTRA_test_istream_qp_encoder_DEPENDENCIES)
+ @rm -f test-istream-qp-encoder$(EXEEXT)
+ $(AM_V_CCLD)$(LINK) $(test_istream_qp_encoder_OBJECTS) $(test_istream_qp_encoder_LDADD) $(LIBS)
+
+test-mail-html2text$(EXEEXT): $(test_mail_html2text_OBJECTS) $(test_mail_html2text_DEPENDENCIES) $(EXTRA_test_mail_html2text_DEPENDENCIES)
+ @rm -f test-mail-html2text$(EXEEXT)
+ $(AM_V_CCLD)$(LINK) $(test_mail_html2text_OBJECTS) $(test_mail_html2text_LDADD) $(LIBS)
+
+test-mail-user-hash$(EXEEXT): $(test_mail_user_hash_OBJECTS) $(test_mail_user_hash_DEPENDENCIES) $(EXTRA_test_mail_user_hash_DEPENDENCIES)
+ @rm -f test-mail-user-hash$(EXEEXT)
+ $(AM_V_CCLD)$(LINK) $(test_mail_user_hash_OBJECTS) $(test_mail_user_hash_LDADD) $(LIBS)
+
+test-mbox-from$(EXEEXT): $(test_mbox_from_OBJECTS) $(test_mbox_from_DEPENDENCIES) $(EXTRA_test_mbox_from_DEPENDENCIES)
+ @rm -f test-mbox-from$(EXEEXT)
+ $(AM_V_CCLD)$(LINK) $(test_mbox_from_OBJECTS) $(test_mbox_from_LDADD) $(LIBS)
+
+test-message-address$(EXEEXT): $(test_message_address_OBJECTS) $(test_message_address_DEPENDENCIES) $(EXTRA_test_message_address_DEPENDENCIES)
+ @rm -f test-message-address$(EXEEXT)
+ $(AM_V_CCLD)$(LINK) $(test_message_address_OBJECTS) $(test_message_address_LDADD) $(LIBS)
+
+test-message-date$(EXEEXT): $(test_message_date_OBJECTS) $(test_message_date_DEPENDENCIES) $(EXTRA_test_message_date_DEPENDENCIES)
+ @rm -f test-message-date$(EXEEXT)
+ $(AM_V_CCLD)$(LINK) $(test_message_date_OBJECTS) $(test_message_date_LDADD) $(LIBS)
+
+test-message-decoder$(EXEEXT): $(test_message_decoder_OBJECTS) $(test_message_decoder_DEPENDENCIES) $(EXTRA_test_message_decoder_DEPENDENCIES)
+ @rm -f test-message-decoder$(EXEEXT)
+ $(AM_V_CCLD)$(LINK) $(test_message_decoder_OBJECTS) $(test_message_decoder_LDADD) $(LIBS)
+
+test-message-header-decode$(EXEEXT): $(test_message_header_decode_OBJECTS) $(test_message_header_decode_DEPENDENCIES) $(EXTRA_test_message_header_decode_DEPENDENCIES)
+ @rm -f test-message-header-decode$(EXEEXT)
+ $(AM_V_CCLD)$(LINK) $(test_message_header_decode_OBJECTS) $(test_message_header_decode_LDADD) $(LIBS)
+
+test-message-header-encode$(EXEEXT): $(test_message_header_encode_OBJECTS) $(test_message_header_encode_DEPENDENCIES) $(EXTRA_test_message_header_encode_DEPENDENCIES)
+ @rm -f test-message-header-encode$(EXEEXT)
+ $(AM_V_CCLD)$(LINK) $(test_message_header_encode_OBJECTS) $(test_message_header_encode_LDADD) $(LIBS)
+
+test-message-header-hash$(EXEEXT): $(test_message_header_hash_OBJECTS) $(test_message_header_hash_DEPENDENCIES) $(EXTRA_test_message_header_hash_DEPENDENCIES)
+ @rm -f test-message-header-hash$(EXEEXT)
+ $(AM_V_CCLD)$(LINK) $(test_message_header_hash_OBJECTS) $(test_message_header_hash_LDADD) $(LIBS)
+
+test-message-header-parser$(EXEEXT): $(test_message_header_parser_OBJECTS) $(test_message_header_parser_DEPENDENCIES) $(EXTRA_test_message_header_parser_DEPENDENCIES)
+ @rm -f test-message-header-parser$(EXEEXT)
+ $(AM_V_CCLD)$(LINK) $(test_message_header_parser_OBJECTS) $(test_message_header_parser_LDADD) $(LIBS)
+
+test-message-id$(EXEEXT): $(test_message_id_OBJECTS) $(test_message_id_DEPENDENCIES) $(EXTRA_test_message_id_DEPENDENCIES)
+ @rm -f test-message-id$(EXEEXT)
+ $(AM_V_CCLD)$(LINK) $(test_message_id_OBJECTS) $(test_message_id_LDADD) $(LIBS)
+
+test-message-parser$(EXEEXT): $(test_message_parser_OBJECTS) $(test_message_parser_DEPENDENCIES) $(EXTRA_test_message_parser_DEPENDENCIES)
+ @rm -f test-message-parser$(EXEEXT)
+ $(AM_V_CCLD)$(LINK) $(test_message_parser_OBJECTS) $(test_message_parser_LDADD) $(LIBS)
+
+test-message-part$(EXEEXT): $(test_message_part_OBJECTS) $(test_message_part_DEPENDENCIES) $(EXTRA_test_message_part_DEPENDENCIES)
+ @rm -f test-message-part$(EXEEXT)
+ $(AM_V_CCLD)$(LINK) $(test_message_part_OBJECTS) $(test_message_part_LDADD) $(LIBS)
+
+test-message-part-serialize$(EXEEXT): $(test_message_part_serialize_OBJECTS) $(test_message_part_serialize_DEPENDENCIES) $(EXTRA_test_message_part_serialize_DEPENDENCIES)
+ @rm -f test-message-part-serialize$(EXEEXT)
+ $(AM_V_CCLD)$(LINK) $(test_message_part_serialize_OBJECTS) $(test_message_part_serialize_LDADD) $(LIBS)
+
+test-message-search$(EXEEXT): $(test_message_search_OBJECTS) $(test_message_search_DEPENDENCIES) $(EXTRA_test_message_search_DEPENDENCIES)
+ @rm -f test-message-search$(EXEEXT)
+ $(AM_V_CCLD)$(LINK) $(test_message_search_OBJECTS) $(test_message_search_LDADD) $(LIBS)
+
+test-message-size$(EXEEXT): $(test_message_size_OBJECTS) $(test_message_size_DEPENDENCIES) $(EXTRA_test_message_size_DEPENDENCIES)
+ @rm -f test-message-size$(EXEEXT)
+ $(AM_V_CCLD)$(LINK) $(test_message_size_OBJECTS) $(test_message_size_LDADD) $(LIBS)
+
+test-message-snippet$(EXEEXT): $(test_message_snippet_OBJECTS) $(test_message_snippet_DEPENDENCIES) $(EXTRA_test_message_snippet_DEPENDENCIES)
+ @rm -f test-message-snippet$(EXEEXT)
+ $(AM_V_CCLD)$(LINK) $(test_message_snippet_OBJECTS) $(test_message_snippet_LDADD) $(LIBS)
+
+test-ostream-dot$(EXEEXT): $(test_ostream_dot_OBJECTS) $(test_ostream_dot_DEPENDENCIES) $(EXTRA_test_ostream_dot_DEPENDENCIES)
+ @rm -f test-ostream-dot$(EXEEXT)
+ $(AM_V_CCLD)$(LINK) $(test_ostream_dot_OBJECTS) $(test_ostream_dot_LDADD) $(LIBS)
+
+test-qp-decoder$(EXEEXT): $(test_qp_decoder_OBJECTS) $(test_qp_decoder_DEPENDENCIES) $(EXTRA_test_qp_decoder_DEPENDENCIES)
+ @rm -f test-qp-decoder$(EXEEXT)
+ $(AM_V_CCLD)$(LINK) $(test_qp_decoder_OBJECTS) $(test_qp_decoder_LDADD) $(LIBS)
+
+test-qp-encoder$(EXEEXT): $(test_qp_encoder_OBJECTS) $(test_qp_encoder_DEPENDENCIES) $(EXTRA_test_qp_encoder_DEPENDENCIES)
+ @rm -f test-qp-encoder$(EXEEXT)
+ $(AM_V_CCLD)$(LINK) $(test_qp_encoder_OBJECTS) $(test_qp_encoder_LDADD) $(LIBS)
+
+test-quoted-printable$(EXEEXT): $(test_quoted_printable_OBJECTS) $(test_quoted_printable_DEPENDENCIES) $(EXTRA_test_quoted_printable_DEPENDENCIES)
+ @rm -f test-quoted-printable$(EXEEXT)
+ $(AM_V_CCLD)$(LINK) $(test_quoted_printable_OBJECTS) $(test_quoted_printable_LDADD) $(LIBS)
+
+test-rfc2231-parser$(EXEEXT): $(test_rfc2231_parser_OBJECTS) $(test_rfc2231_parser_DEPENDENCIES) $(EXTRA_test_rfc2231_parser_DEPENDENCIES)
+ @rm -f test-rfc2231-parser$(EXEEXT)
+ $(AM_V_CCLD)$(LINK) $(test_rfc2231_parser_OBJECTS) $(test_rfc2231_parser_LDADD) $(LIBS)
+
+test-rfc822-parser$(EXEEXT): $(test_rfc822_parser_OBJECTS) $(test_rfc822_parser_DEPENDENCIES) $(EXTRA_test_rfc822_parser_DEPENDENCIES)
+ @rm -f test-rfc822-parser$(EXEEXT)
+ $(AM_V_CCLD)$(LINK) $(test_rfc822_parser_OBJECTS) $(test_rfc822_parser_LDADD) $(LIBS)
+
+mostlyclean-compile:
+ -rm -f *.$(OBJEXT)
+
+distclean-compile:
+ -rm -f *.tab.c
+
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/fuzz_message_parser-force-cxx-linking.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/fuzz_message_parser-fuzz-message-parser.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/istream-attachment-connector.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/istream-attachment-extractor.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/istream-binary-converter.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/istream-dot.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/istream-header-filter.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/istream-nonuls.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/istream-qp-decoder.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/istream-qp-encoder.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/mail-html2text.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/mail-user-hash.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/mbox-from.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/message-address.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/message-binary-part.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/message-date.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/message-decoder.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/message-header-decode.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/message-header-encode.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/message-header-hash.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/message-header-parser.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/message-id.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/message-parser-from-parts.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/message-parser.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/message-part-data.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/message-part-serialize.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/message-part.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/message-search.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/message-size.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/message-snippet.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/ostream-dot.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/qp-decoder.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/qp-encoder.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/quoted-printable.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/rfc2231-parser.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/rfc822-parser.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/test-istream-attachment.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/test-istream-binary-converter.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/test-istream-dot.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/test-istream-header-filter.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/test-istream-qp-decoder.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/test-istream-qp-encoder.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/test-mail-html2text.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/test-mail-user-hash.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/test-mbox-from.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/test-message-address.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/test-message-date.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/test-message-decoder.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/test-message-header-decode.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/test-message-header-encode.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/test-message-header-hash.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/test-message-header-parser.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/test-message-id.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/test-message-parser.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/test-message-part-serialize.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/test-message-part.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/test-message-search.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/test-message-size.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/test-message-snippet.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/test-ostream-dot.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/test-qp-decoder.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/test-qp-encoder.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/test-quoted-printable.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/test-rfc2231-parser.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/test-rfc822-parser.Po@am__quote@ # am--include-marker
+
+$(am__depfiles_remade):
+ @$(MKDIR_P) $(@D)
+ @echo '# dummy' >$@-t && $(am__mv) $@-t $@
+
+am--depfiles: $(am__depfiles_remade)
+
+.c.o:
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(COMPILE) -MT $@ -MD -MP -MF $(DEPDIR)/$*.Tpo -c -o $@ $<
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/$*.Tpo $(DEPDIR)/$*.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='$<' object='$@' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(COMPILE) -c -o $@ $<
+
+.c.obj:
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(COMPILE) -MT $@ -MD -MP -MF $(DEPDIR)/$*.Tpo -c -o $@ `$(CYGPATH_W) '$<'`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/$*.Tpo $(DEPDIR)/$*.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='$<' object='$@' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(COMPILE) -c -o $@ `$(CYGPATH_W) '$<'`
+
+.c.lo:
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(LTCOMPILE) -MT $@ -MD -MP -MF $(DEPDIR)/$*.Tpo -c -o $@ $<
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/$*.Tpo $(DEPDIR)/$*.Plo
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='$<' object='$@' libtool=yes @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(LTCOMPILE) -c -o $@ $<
+
+fuzz_message_parser-fuzz-message-parser.o: fuzz-message-parser.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(fuzz_message_parser_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT fuzz_message_parser-fuzz-message-parser.o -MD -MP -MF $(DEPDIR)/fuzz_message_parser-fuzz-message-parser.Tpo -c -o fuzz_message_parser-fuzz-message-parser.o `test -f 'fuzz-message-parser.c' || echo '$(srcdir)/'`fuzz-message-parser.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/fuzz_message_parser-fuzz-message-parser.Tpo $(DEPDIR)/fuzz_message_parser-fuzz-message-parser.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='fuzz-message-parser.c' object='fuzz_message_parser-fuzz-message-parser.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(fuzz_message_parser_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o fuzz_message_parser-fuzz-message-parser.o `test -f 'fuzz-message-parser.c' || echo '$(srcdir)/'`fuzz-message-parser.c
+
+fuzz_message_parser-fuzz-message-parser.obj: fuzz-message-parser.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(fuzz_message_parser_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT fuzz_message_parser-fuzz-message-parser.obj -MD -MP -MF $(DEPDIR)/fuzz_message_parser-fuzz-message-parser.Tpo -c -o fuzz_message_parser-fuzz-message-parser.obj `if test -f 'fuzz-message-parser.c'; then $(CYGPATH_W) 'fuzz-message-parser.c'; else $(CYGPATH_W) '$(srcdir)/fuzz-message-parser.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/fuzz_message_parser-fuzz-message-parser.Tpo $(DEPDIR)/fuzz_message_parser-fuzz-message-parser.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='fuzz-message-parser.c' object='fuzz_message_parser-fuzz-message-parser.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(fuzz_message_parser_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o fuzz_message_parser-fuzz-message-parser.obj `if test -f 'fuzz-message-parser.c'; then $(CYGPATH_W) 'fuzz-message-parser.c'; else $(CYGPATH_W) '$(srcdir)/fuzz-message-parser.c'; fi`
+
+.cxx.o:
+@am__fastdepCXX_TRUE@ $(AM_V_CXX)$(CXXCOMPILE) -MT $@ -MD -MP -MF $(DEPDIR)/$*.Tpo -c -o $@ $<
+@am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/$*.Tpo $(DEPDIR)/$*.Po
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ $(AM_V_CXX)source='$<' object='$@' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(CXXCOMPILE) -c -o $@ $<
+
+.cxx.obj:
+@am__fastdepCXX_TRUE@ $(AM_V_CXX)$(CXXCOMPILE) -MT $@ -MD -MP -MF $(DEPDIR)/$*.Tpo -c -o $@ `$(CYGPATH_W) '$<'`
+@am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/$*.Tpo $(DEPDIR)/$*.Po
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ $(AM_V_CXX)source='$<' object='$@' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(CXXCOMPILE) -c -o $@ `$(CYGPATH_W) '$<'`
+
+.cxx.lo:
+@am__fastdepCXX_TRUE@ $(AM_V_CXX)$(LTCXXCOMPILE) -MT $@ -MD -MP -MF $(DEPDIR)/$*.Tpo -c -o $@ $<
+@am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/$*.Tpo $(DEPDIR)/$*.Plo
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ $(AM_V_CXX)source='$<' object='$@' libtool=yes @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(LTCXXCOMPILE) -c -o $@ $<
+
+fuzz_message_parser-force-cxx-linking.o: force-cxx-linking.cxx
+@am__fastdepCXX_TRUE@ $(AM_V_CXX)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(fuzz_message_parser_CPPFLAGS) $(CPPFLAGS) $(AM_CXXFLAGS) $(CXXFLAGS) -MT fuzz_message_parser-force-cxx-linking.o -MD -MP -MF $(DEPDIR)/fuzz_message_parser-force-cxx-linking.Tpo -c -o fuzz_message_parser-force-cxx-linking.o `test -f 'force-cxx-linking.cxx' || echo '$(srcdir)/'`force-cxx-linking.cxx
+@am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/fuzz_message_parser-force-cxx-linking.Tpo $(DEPDIR)/fuzz_message_parser-force-cxx-linking.Po
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ $(AM_V_CXX)source='force-cxx-linking.cxx' object='fuzz_message_parser-force-cxx-linking.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(fuzz_message_parser_CPPFLAGS) $(CPPFLAGS) $(AM_CXXFLAGS) $(CXXFLAGS) -c -o fuzz_message_parser-force-cxx-linking.o `test -f 'force-cxx-linking.cxx' || echo '$(srcdir)/'`force-cxx-linking.cxx
+
+fuzz_message_parser-force-cxx-linking.obj: force-cxx-linking.cxx
+@am__fastdepCXX_TRUE@ $(AM_V_CXX)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(fuzz_message_parser_CPPFLAGS) $(CPPFLAGS) $(AM_CXXFLAGS) $(CXXFLAGS) -MT fuzz_message_parser-force-cxx-linking.obj -MD -MP -MF $(DEPDIR)/fuzz_message_parser-force-cxx-linking.Tpo -c -o fuzz_message_parser-force-cxx-linking.obj `if test -f 'force-cxx-linking.cxx'; then $(CYGPATH_W) 'force-cxx-linking.cxx'; else $(CYGPATH_W) '$(srcdir)/force-cxx-linking.cxx'; fi`
+@am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/fuzz_message_parser-force-cxx-linking.Tpo $(DEPDIR)/fuzz_message_parser-force-cxx-linking.Po
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ $(AM_V_CXX)source='force-cxx-linking.cxx' object='fuzz_message_parser-force-cxx-linking.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(fuzz_message_parser_CPPFLAGS) $(CPPFLAGS) $(AM_CXXFLAGS) $(CXXFLAGS) -c -o fuzz_message_parser-force-cxx-linking.obj `if test -f 'force-cxx-linking.cxx'; then $(CYGPATH_W) 'force-cxx-linking.cxx'; else $(CYGPATH_W) '$(srcdir)/force-cxx-linking.cxx'; fi`
+
+mostlyclean-libtool:
+ -rm -f *.lo
+
+clean-libtool:
+ -rm -rf .libs _libs
+install-pkginc_libHEADERS: $(pkginc_lib_HEADERS)
+ @$(NORMAL_INSTALL)
+ @list='$(pkginc_lib_HEADERS)'; test -n "$(pkginc_libdir)" || list=; \
+ if test -n "$$list"; then \
+ echo " $(MKDIR_P) '$(DESTDIR)$(pkginc_libdir)'"; \
+ $(MKDIR_P) "$(DESTDIR)$(pkginc_libdir)" || exit 1; \
+ fi; \
+ for p in $$list; do \
+ if test -f "$$p"; then d=; else d="$(srcdir)/"; fi; \
+ echo "$$d$$p"; \
+ done | $(am__base_list) | \
+ while read files; do \
+ echo " $(INSTALL_HEADER) $$files '$(DESTDIR)$(pkginc_libdir)'"; \
+ $(INSTALL_HEADER) $$files "$(DESTDIR)$(pkginc_libdir)" || exit $$?; \
+ done
+
+uninstall-pkginc_libHEADERS:
+ @$(NORMAL_UNINSTALL)
+ @list='$(pkginc_lib_HEADERS)'; test -n "$(pkginc_libdir)" || list=; \
+ files=`for p in $$list; do echo $$p; done | sed -e 's|^.*/||'`; \
+ dir='$(DESTDIR)$(pkginc_libdir)'; $(am__uninstall_files_from_dir)
+
+ID: $(am__tagged_files)
+ $(am__define_uniq_tagged_files); mkid -fID $$unique
+tags: tags-am
+TAGS: tags
+
+tags-am: $(TAGS_DEPENDENCIES) $(am__tagged_files)
+ set x; \
+ here=`pwd`; \
+ $(am__define_uniq_tagged_files); \
+ shift; \
+ if test -z "$(ETAGS_ARGS)$$*$$unique"; then :; else \
+ test -n "$$unique" || unique=$$empty_fix; \
+ if test $$# -gt 0; then \
+ $(ETAGS) $(ETAGSFLAGS) $(AM_ETAGSFLAGS) $(ETAGS_ARGS) \
+ "$$@" $$unique; \
+ else \
+ $(ETAGS) $(ETAGSFLAGS) $(AM_ETAGSFLAGS) $(ETAGS_ARGS) \
+ $$unique; \
+ fi; \
+ fi
+ctags: ctags-am
+
+CTAGS: ctags
+ctags-am: $(TAGS_DEPENDENCIES) $(am__tagged_files)
+ $(am__define_uniq_tagged_files); \
+ test -z "$(CTAGS_ARGS)$$unique" \
+ || $(CTAGS) $(CTAGSFLAGS) $(AM_CTAGSFLAGS) $(CTAGS_ARGS) \
+ $$unique
+
+GTAGS:
+ here=`$(am__cd) $(top_builddir) && pwd` \
+ && $(am__cd) $(top_srcdir) \
+ && gtags -i $(GTAGS_ARGS) "$$here"
+cscopelist: cscopelist-am
+
+cscopelist-am: $(am__tagged_files)
+ list='$(am__tagged_files)'; \
+ case "$(srcdir)" in \
+ [\\/]* | ?:[\\/]*) sdir="$(srcdir)" ;; \
+ *) sdir=$(subdir)/$(srcdir) ;; \
+ esac; \
+ for i in $$list; do \
+ if test -f "$$i"; then \
+ echo "$(subdir)/$$i"; \
+ else \
+ echo "$$sdir/$$i"; \
+ fi; \
+ done >> $(top_builddir)/cscope.files
+
+distclean-tags:
+ -rm -f TAGS ID GTAGS GRTAGS GSYMS GPATH tags
+
+distdir: $(BUILT_SOURCES)
+ $(MAKE) $(AM_MAKEFLAGS) distdir-am
+
+distdir-am: $(DISTFILES)
+ @srcdirstrip=`echo "$(srcdir)" | sed 's/[].[^$$\\*]/\\\\&/g'`; \
+ topsrcdirstrip=`echo "$(top_srcdir)" | sed 's/[].[^$$\\*]/\\\\&/g'`; \
+ list='$(DISTFILES)'; \
+ dist_files=`for file in $$list; do echo $$file; done | \
+ sed -e "s|^$$srcdirstrip/||;t" \
+ -e "s|^$$topsrcdirstrip/|$(top_builddir)/|;t"`; \
+ case $$dist_files in \
+ */*) $(MKDIR_P) `echo "$$dist_files" | \
+ sed '/\//!d;s|^|$(distdir)/|;s,/[^/]*$$,,' | \
+ sort -u` ;; \
+ esac; \
+ for file in $$dist_files; do \
+ if test -f $$file || test -d $$file; then d=.; else d=$(srcdir); fi; \
+ if test -d $$d/$$file; then \
+ dir=`echo "/$$file" | sed -e 's,/[^/]*$$,,'`; \
+ if test -d "$(distdir)/$$file"; then \
+ find "$(distdir)/$$file" -type d ! -perm -700 -exec chmod u+rwx {} \;; \
+ fi; \
+ if test -d $(srcdir)/$$file && test $$d != $(srcdir); then \
+ cp -fpR $(srcdir)/$$file "$(distdir)$$dir" || exit 1; \
+ find "$(distdir)/$$file" -type d ! -perm -700 -exec chmod u+rwx {} \;; \
+ fi; \
+ cp -fpR $$d/$$file "$(distdir)$$dir" || exit 1; \
+ else \
+ test -f "$(distdir)/$$file" \
+ || cp -p $$d/$$file "$(distdir)/$$file" \
+ || exit 1; \
+ fi; \
+ done
+check-am: all-am
+ $(MAKE) $(AM_MAKEFLAGS) check-local
+check: check-am
+all-am: Makefile $(PROGRAMS) $(LTLIBRARIES) $(HEADERS)
+installdirs:
+ for dir in "$(DESTDIR)$(pkginc_libdir)"; do \
+ test -z "$$dir" || $(MKDIR_P) "$$dir"; \
+ done
+install: install-am
+install-exec: install-exec-am
+install-data: install-data-am
+uninstall: uninstall-am
+
+install-am: all-am
+ @$(MAKE) $(AM_MAKEFLAGS) install-exec-am install-data-am
+
+installcheck: installcheck-am
+install-strip:
+ if test -z '$(STRIP)'; then \
+ $(MAKE) $(AM_MAKEFLAGS) INSTALL_PROGRAM="$(INSTALL_STRIP_PROGRAM)" \
+ install_sh_PROGRAM="$(INSTALL_STRIP_PROGRAM)" INSTALL_STRIP_FLAG=-s \
+ install; \
+ else \
+ $(MAKE) $(AM_MAKEFLAGS) INSTALL_PROGRAM="$(INSTALL_STRIP_PROGRAM)" \
+ install_sh_PROGRAM="$(INSTALL_STRIP_PROGRAM)" INSTALL_STRIP_FLAG=-s \
+ "INSTALL_PROGRAM_ENV=STRIPPROG='$(STRIP)'" install; \
+ fi
+mostlyclean-generic:
+
+clean-generic:
+
+distclean-generic:
+ -test -z "$(CONFIG_CLEAN_FILES)" || rm -f $(CONFIG_CLEAN_FILES)
+ -test . = "$(srcdir)" || test -z "$(CONFIG_CLEAN_VPATH_FILES)" || rm -f $(CONFIG_CLEAN_VPATH_FILES)
+
+maintainer-clean-generic:
+ @echo "This command is intended for maintainers to use"
+ @echo "it deletes files that may require special tools to rebuild."
+clean: clean-am
+
+clean-am: clean-generic clean-libtool clean-noinstLTLIBRARIES \
+ clean-noinstPROGRAMS mostlyclean-am
+
+distclean: distclean-am
+ -rm -f ./$(DEPDIR)/fuzz_message_parser-force-cxx-linking.Po
+ -rm -f ./$(DEPDIR)/fuzz_message_parser-fuzz-message-parser.Po
+ -rm -f ./$(DEPDIR)/istream-attachment-connector.Plo
+ -rm -f ./$(DEPDIR)/istream-attachment-extractor.Plo
+ -rm -f ./$(DEPDIR)/istream-binary-converter.Plo
+ -rm -f ./$(DEPDIR)/istream-dot.Plo
+ -rm -f ./$(DEPDIR)/istream-header-filter.Plo
+ -rm -f ./$(DEPDIR)/istream-nonuls.Plo
+ -rm -f ./$(DEPDIR)/istream-qp-decoder.Plo
+ -rm -f ./$(DEPDIR)/istream-qp-encoder.Plo
+ -rm -f ./$(DEPDIR)/mail-html2text.Plo
+ -rm -f ./$(DEPDIR)/mail-user-hash.Plo
+ -rm -f ./$(DEPDIR)/mbox-from.Plo
+ -rm -f ./$(DEPDIR)/message-address.Plo
+ -rm -f ./$(DEPDIR)/message-binary-part.Plo
+ -rm -f ./$(DEPDIR)/message-date.Plo
+ -rm -f ./$(DEPDIR)/message-decoder.Plo
+ -rm -f ./$(DEPDIR)/message-header-decode.Plo
+ -rm -f ./$(DEPDIR)/message-header-encode.Plo
+ -rm -f ./$(DEPDIR)/message-header-hash.Plo
+ -rm -f ./$(DEPDIR)/message-header-parser.Plo
+ -rm -f ./$(DEPDIR)/message-id.Plo
+ -rm -f ./$(DEPDIR)/message-parser-from-parts.Plo
+ -rm -f ./$(DEPDIR)/message-parser.Plo
+ -rm -f ./$(DEPDIR)/message-part-data.Plo
+ -rm -f ./$(DEPDIR)/message-part-serialize.Plo
+ -rm -f ./$(DEPDIR)/message-part.Plo
+ -rm -f ./$(DEPDIR)/message-search.Plo
+ -rm -f ./$(DEPDIR)/message-size.Plo
+ -rm -f ./$(DEPDIR)/message-snippet.Plo
+ -rm -f ./$(DEPDIR)/ostream-dot.Plo
+ -rm -f ./$(DEPDIR)/qp-decoder.Plo
+ -rm -f ./$(DEPDIR)/qp-encoder.Plo
+ -rm -f ./$(DEPDIR)/quoted-printable.Plo
+ -rm -f ./$(DEPDIR)/rfc2231-parser.Plo
+ -rm -f ./$(DEPDIR)/rfc822-parser.Plo
+ -rm -f ./$(DEPDIR)/test-istream-attachment.Po
+ -rm -f ./$(DEPDIR)/test-istream-binary-converter.Po
+ -rm -f ./$(DEPDIR)/test-istream-dot.Po
+ -rm -f ./$(DEPDIR)/test-istream-header-filter.Po
+ -rm -f ./$(DEPDIR)/test-istream-qp-decoder.Po
+ -rm -f ./$(DEPDIR)/test-istream-qp-encoder.Po
+ -rm -f ./$(DEPDIR)/test-mail-html2text.Po
+ -rm -f ./$(DEPDIR)/test-mail-user-hash.Po
+ -rm -f ./$(DEPDIR)/test-mbox-from.Po
+ -rm -f ./$(DEPDIR)/test-message-address.Po
+ -rm -f ./$(DEPDIR)/test-message-date.Po
+ -rm -f ./$(DEPDIR)/test-message-decoder.Po
+ -rm -f ./$(DEPDIR)/test-message-header-decode.Po
+ -rm -f ./$(DEPDIR)/test-message-header-encode.Po
+ -rm -f ./$(DEPDIR)/test-message-header-hash.Po
+ -rm -f ./$(DEPDIR)/test-message-header-parser.Po
+ -rm -f ./$(DEPDIR)/test-message-id.Po
+ -rm -f ./$(DEPDIR)/test-message-parser.Po
+ -rm -f ./$(DEPDIR)/test-message-part-serialize.Po
+ -rm -f ./$(DEPDIR)/test-message-part.Po
+ -rm -f ./$(DEPDIR)/test-message-search.Po
+ -rm -f ./$(DEPDIR)/test-message-size.Po
+ -rm -f ./$(DEPDIR)/test-message-snippet.Po
+ -rm -f ./$(DEPDIR)/test-ostream-dot.Po
+ -rm -f ./$(DEPDIR)/test-qp-decoder.Po
+ -rm -f ./$(DEPDIR)/test-qp-encoder.Po
+ -rm -f ./$(DEPDIR)/test-quoted-printable.Po
+ -rm -f ./$(DEPDIR)/test-rfc2231-parser.Po
+ -rm -f ./$(DEPDIR)/test-rfc822-parser.Po
+ -rm -f Makefile
+distclean-am: clean-am distclean-compile distclean-generic \
+ distclean-tags
+
+dvi: dvi-am
+
+dvi-am:
+
+html: html-am
+
+html-am:
+
+info: info-am
+
+info-am:
+
+install-data-am: install-pkginc_libHEADERS
+
+install-dvi: install-dvi-am
+
+install-dvi-am:
+
+install-exec-am:
+
+install-html: install-html-am
+
+install-html-am:
+
+install-info: install-info-am
+
+install-info-am:
+
+install-man:
+
+install-pdf: install-pdf-am
+
+install-pdf-am:
+
+install-ps: install-ps-am
+
+install-ps-am:
+
+installcheck-am:
+
+maintainer-clean: maintainer-clean-am
+ -rm -f ./$(DEPDIR)/fuzz_message_parser-force-cxx-linking.Po
+ -rm -f ./$(DEPDIR)/fuzz_message_parser-fuzz-message-parser.Po
+ -rm -f ./$(DEPDIR)/istream-attachment-connector.Plo
+ -rm -f ./$(DEPDIR)/istream-attachment-extractor.Plo
+ -rm -f ./$(DEPDIR)/istream-binary-converter.Plo
+ -rm -f ./$(DEPDIR)/istream-dot.Plo
+ -rm -f ./$(DEPDIR)/istream-header-filter.Plo
+ -rm -f ./$(DEPDIR)/istream-nonuls.Plo
+ -rm -f ./$(DEPDIR)/istream-qp-decoder.Plo
+ -rm -f ./$(DEPDIR)/istream-qp-encoder.Plo
+ -rm -f ./$(DEPDIR)/mail-html2text.Plo
+ -rm -f ./$(DEPDIR)/mail-user-hash.Plo
+ -rm -f ./$(DEPDIR)/mbox-from.Plo
+ -rm -f ./$(DEPDIR)/message-address.Plo
+ -rm -f ./$(DEPDIR)/message-binary-part.Plo
+ -rm -f ./$(DEPDIR)/message-date.Plo
+ -rm -f ./$(DEPDIR)/message-decoder.Plo
+ -rm -f ./$(DEPDIR)/message-header-decode.Plo
+ -rm -f ./$(DEPDIR)/message-header-encode.Plo
+ -rm -f ./$(DEPDIR)/message-header-hash.Plo
+ -rm -f ./$(DEPDIR)/message-header-parser.Plo
+ -rm -f ./$(DEPDIR)/message-id.Plo
+ -rm -f ./$(DEPDIR)/message-parser-from-parts.Plo
+ -rm -f ./$(DEPDIR)/message-parser.Plo
+ -rm -f ./$(DEPDIR)/message-part-data.Plo
+ -rm -f ./$(DEPDIR)/message-part-serialize.Plo
+ -rm -f ./$(DEPDIR)/message-part.Plo
+ -rm -f ./$(DEPDIR)/message-search.Plo
+ -rm -f ./$(DEPDIR)/message-size.Plo
+ -rm -f ./$(DEPDIR)/message-snippet.Plo
+ -rm -f ./$(DEPDIR)/ostream-dot.Plo
+ -rm -f ./$(DEPDIR)/qp-decoder.Plo
+ -rm -f ./$(DEPDIR)/qp-encoder.Plo
+ -rm -f ./$(DEPDIR)/quoted-printable.Plo
+ -rm -f ./$(DEPDIR)/rfc2231-parser.Plo
+ -rm -f ./$(DEPDIR)/rfc822-parser.Plo
+ -rm -f ./$(DEPDIR)/test-istream-attachment.Po
+ -rm -f ./$(DEPDIR)/test-istream-binary-converter.Po
+ -rm -f ./$(DEPDIR)/test-istream-dot.Po
+ -rm -f ./$(DEPDIR)/test-istream-header-filter.Po
+ -rm -f ./$(DEPDIR)/test-istream-qp-decoder.Po
+ -rm -f ./$(DEPDIR)/test-istream-qp-encoder.Po
+ -rm -f ./$(DEPDIR)/test-mail-html2text.Po
+ -rm -f ./$(DEPDIR)/test-mail-user-hash.Po
+ -rm -f ./$(DEPDIR)/test-mbox-from.Po
+ -rm -f ./$(DEPDIR)/test-message-address.Po
+ -rm -f ./$(DEPDIR)/test-message-date.Po
+ -rm -f ./$(DEPDIR)/test-message-decoder.Po
+ -rm -f ./$(DEPDIR)/test-message-header-decode.Po
+ -rm -f ./$(DEPDIR)/test-message-header-encode.Po
+ -rm -f ./$(DEPDIR)/test-message-header-hash.Po
+ -rm -f ./$(DEPDIR)/test-message-header-parser.Po
+ -rm -f ./$(DEPDIR)/test-message-id.Po
+ -rm -f ./$(DEPDIR)/test-message-parser.Po
+ -rm -f ./$(DEPDIR)/test-message-part-serialize.Po
+ -rm -f ./$(DEPDIR)/test-message-part.Po
+ -rm -f ./$(DEPDIR)/test-message-search.Po
+ -rm -f ./$(DEPDIR)/test-message-size.Po
+ -rm -f ./$(DEPDIR)/test-message-snippet.Po
+ -rm -f ./$(DEPDIR)/test-ostream-dot.Po
+ -rm -f ./$(DEPDIR)/test-qp-decoder.Po
+ -rm -f ./$(DEPDIR)/test-qp-encoder.Po
+ -rm -f ./$(DEPDIR)/test-quoted-printable.Po
+ -rm -f ./$(DEPDIR)/test-rfc2231-parser.Po
+ -rm -f ./$(DEPDIR)/test-rfc822-parser.Po
+ -rm -f Makefile
+maintainer-clean-am: distclean-am maintainer-clean-generic
+
+mostlyclean: mostlyclean-am
+
+mostlyclean-am: mostlyclean-compile mostlyclean-generic \
+ mostlyclean-libtool
+
+pdf: pdf-am
+
+pdf-am:
+
+ps: ps-am
+
+ps-am:
+
+uninstall-am: uninstall-pkginc_libHEADERS
+
+.MAKE: check-am install-am install-strip
+
+.PHONY: CTAGS GTAGS TAGS all all-am am--depfiles check check-am \
+ check-local clean clean-generic clean-libtool \
+ clean-noinstLTLIBRARIES clean-noinstPROGRAMS cscopelist-am \
+ ctags ctags-am distclean distclean-compile distclean-generic \
+ distclean-libtool distclean-tags distdir dvi dvi-am html \
+ html-am info info-am install install-am install-data \
+ install-data-am install-dvi install-dvi-am install-exec \
+ install-exec-am install-html install-html-am install-info \
+ install-info-am install-man install-pdf install-pdf-am \
+ install-pkginc_libHEADERS install-ps install-ps-am \
+ install-strip installcheck installcheck-am installdirs \
+ maintainer-clean maintainer-clean-generic mostlyclean \
+ mostlyclean-compile mostlyclean-generic mostlyclean-libtool \
+ pdf pdf-am ps ps-am tags tags-am uninstall uninstall-am \
+ uninstall-pkginc_libHEADERS
+
+.PRECIOUS: Makefile
+
+
+check-local:
+ for bin in $(test_programs); do \
+ if ! $(RUN_TEST) ./$$bin; then exit 1; fi; \
+ done
+
+# Tell versions [3.59,3.63) of GNU make to not export all variables.
+# Otherwise a system limit (for SysV at least) may be exceeded.
+.NOEXPORT:
diff --git a/src/lib-mail/fuzz-message-parser.c b/src/lib-mail/fuzz-message-parser.c
new file mode 100644
index 0000000..1cb4bdc
--- /dev/null
+++ b/src/lib-mail/fuzz-message-parser.c
@@ -0,0 +1,28 @@
+/* Copyright (c) 2020 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "istream.h"
+#include "test-common.h"
+#include "test-common.h"
+#include "fuzzer.h"
+#include "message-parser.h"
+
+FUZZ_BEGIN_DATA(const unsigned char *data, size_t size)
+{
+ struct istream *input = test_istream_create_data(data, size);
+ const struct message_parser_settings set = {
+ .hdr_flags = 0,
+ .flags = MESSAGE_PARSER_FLAG_INCLUDE_MULTIPART_BLOCKS,
+ .max_nested_mime_parts = 0,
+ .max_total_mime_parts = 0,
+ };
+ struct message_parser_ctx *ctx =
+ message_parser_init(pool_datastack_create(), input, &set);
+ struct message_block block ATTR_UNUSED;
+ i_zero(&block);
+ while(message_parser_parse_next_block(ctx, &block) > -1);
+ struct message_part *part ATTR_UNUSED;
+ message_parser_deinit(&ctx, &part);
+ i_stream_unref(&input);
+}
+FUZZ_END
diff --git a/src/lib-mail/html-entities.h b/src/lib-mail/html-entities.h
new file mode 100644
index 0000000..a3a9f96
--- /dev/null
+++ b/src/lib-mail/html-entities.h
@@ -0,0 +1,253 @@
+{ "quot", 0x0022 },
+{ "amp", 0x0026 },
+{ "apos", 0x0027 },
+{ "lt", 0x003C },
+{ "gt", 0x003E },
+{ "nbsp", 0x00A0 },
+{ "iexcl", 0x00A1 },
+{ "cent", 0x00A2 },
+{ "pound", 0x00A3 },
+{ "curren", 0x00A4 },
+{ "yen", 0x00A5 },
+{ "brvbar", 0x00A6 },
+{ "sect", 0x00A7 },
+{ "uml", 0x00A8 },
+{ "copy", 0x00A9 },
+{ "ordf", 0x00AA },
+{ "laquo", 0x00AB },
+{ "not", 0x00AC },
+{ "shy", 0x00AD },
+{ "reg", 0x00AE },
+{ "macr", 0x00AF },
+{ "deg", 0x00B0 },
+{ "plusmn", 0x00B1 },
+{ "sup2", 0x00B2 },
+{ "sup3", 0x00B3 },
+{ "acute", 0x00B4 },
+{ "micro", 0x00B5 },
+{ "para", 0x00B6 },
+{ "middot", 0x00B7 },
+{ "cedil", 0x00B8 },
+{ "sup1", 0x00B9 },
+{ "ordm", 0x00BA },
+{ "raquo", 0x00BB },
+{ "frac14", 0x00BC },
+{ "frac12", 0x00BD },
+{ "frac34", 0x00BE },
+{ "iquest", 0x00BF },
+{ "Agrave", 0x00C0 },
+{ "Aacute", 0x00C1 },
+{ "Acirc", 0x00C2 },
+{ "Atilde", 0x00C3 },
+{ "Auml", 0x00C4 },
+{ "Aring", 0x00C5 },
+{ "AElig", 0x00C6 },
+{ "Ccedil", 0x00C7 },
+{ "Egrave", 0x00C8 },
+{ "Eacute", 0x00C9 },
+{ "Ecirc", 0x00CA },
+{ "Euml", 0x00CB },
+{ "Igrave", 0x00CC },
+{ "Iacute", 0x00CD },
+{ "Icirc", 0x00CE },
+{ "Iuml", 0x00CF },
+{ "ETH", 0x00D0 },
+{ "Ntilde", 0x00D1 },
+{ "Ograve", 0x00D2 },
+{ "Oacute", 0x00D3 },
+{ "Ocirc", 0x00D4 },
+{ "Otilde", 0x00D5 },
+{ "Ouml", 0x00D6 },
+{ "times", 0x00D7 },
+{ "Oslash", 0x00D8 },
+{ "Ugrave", 0x00D9 },
+{ "Uacute", 0x00DA },
+{ "Ucirc", 0x00DB },
+{ "Uuml", 0x00DC },
+{ "Yacute", 0x00DD },
+{ "THORN", 0x00DE },
+{ "szlig", 0x00DF },
+{ "agrave", 0x00E0 },
+{ "aacute", 0x00E1 },
+{ "acirc", 0x00E2 },
+{ "atilde", 0x00E3 },
+{ "auml", 0x00E4 },
+{ "aring", 0x00E5 },
+{ "aelig", 0x00E6 },
+{ "ccedil", 0x00E7 },
+{ "egrave", 0x00E8 },
+{ "eacute", 0x00E9 },
+{ "ecirc", 0x00EA },
+{ "euml", 0x00EB },
+{ "igrave", 0x00EC },
+{ "iacute", 0x00ED },
+{ "icirc", 0x00EE },
+{ "iuml", 0x00EF },
+{ "eth", 0x00F0 },
+{ "ntilde", 0x00F1 },
+{ "ograve", 0x00F2 },
+{ "oacute", 0x00F3 },
+{ "ocirc", 0x00F4 },
+{ "otilde", 0x00F5 },
+{ "ouml", 0x00F6 },
+{ "divide", 0x00F7 },
+{ "oslash", 0x00F8 },
+{ "ugrave", 0x00F9 },
+{ "uacute", 0x00FA },
+{ "ucirc", 0x00FB },
+{ "uuml", 0x00FC },
+{ "yacute", 0x00FD },
+{ "thorn", 0x00FE },
+{ "yuml", 0x00FF },
+{ "OElig", 0x0152 },
+{ "oelig", 0x0153 },
+{ "Scaron", 0x0160 },
+{ "scaron", 0x0161 },
+{ "Yuml", 0x0178 },
+{ "fnof", 0x0192 },
+{ "circ", 0x02C6 },
+{ "tilde", 0x02DC },
+{ "Alpha", 0x0391 },
+{ "Beta", 0x0392 },
+{ "Gamma", 0x0393 },
+{ "Delta", 0x0394 },
+{ "Epsilon", 0x0395 },
+{ "Zeta", 0x0396 },
+{ "Eta", 0x0397 },
+{ "Theta", 0x0398 },
+{ "Iota", 0x0399 },
+{ "Kappa", 0x039A },
+{ "Lambda", 0x039B },
+{ "Mu", 0x039C },
+{ "Nu", 0x039D },
+{ "Xi", 0x039E },
+{ "Omicron", 0x039F },
+{ "Pi", 0x03A0 },
+{ "Rho", 0x03A1 },
+{ "Sigma", 0x03A3 },
+{ "Tau", 0x03A4 },
+{ "Upsilon", 0x03A5 },
+{ "Phi", 0x03A6 },
+{ "Chi", 0x03A7 },
+{ "Psi", 0x03A8 },
+{ "Omega", 0x03A9 },
+{ "alpha", 0x03B1 },
+{ "beta", 0x03B2 },
+{ "gamma", 0x03B3 },
+{ "delta", 0x03B4 },
+{ "epsilon", 0x03B5 },
+{ "zeta", 0x03B6 },
+{ "eta", 0x03B7 },
+{ "theta", 0x03B8 },
+{ "iota", 0x03B9 },
+{ "kappa", 0x03BA },
+{ "lambda", 0x03BB },
+{ "mu", 0x03BC },
+{ "nu", 0x03BD },
+{ "xi", 0x03BE },
+{ "omicron", 0x03BF },
+{ "pi", 0x03C0 },
+{ "rho", 0x03C1 },
+{ "sigmaf", 0x03C2 },
+{ "sigma", 0x03C3 },
+{ "tau", 0x03C4 },
+{ "upsilon", 0x03C5 },
+{ "phi", 0x03C6 },
+{ "chi", 0x03C7 },
+{ "psi", 0x03C8 },
+{ "omega", 0x03C9 },
+{ "thetasym", 0x03D1 },
+{ "upsih", 0x03D2 },
+{ "piv", 0x03D6 },
+{ "ensp", 0x2002 },
+{ "emsp", 0x2003 },
+{ "thinsp", 0x2009 },
+{ "zwnj", 0x200C },
+{ "zwj", 0x200D },
+{ "lrm", 0x200E },
+{ "rlm", 0x200F },
+{ "ndash", 0x2013 },
+{ "mdash", 0x2014 },
+{ "lsquo", 0x2018 },
+{ "rsquo", 0x2019 },
+{ "sbquo", 0x201A },
+{ "ldquo", 0x201C },
+{ "rdquo", 0x201D },
+{ "bdquo", 0x201E },
+{ "dagger", 0x2020 },
+{ "Dagger", 0x2021 },
+{ "bull", 0x2022 },
+{ "hellip", 0x2026 },
+{ "permil", 0x2030 },
+{ "prime", 0x2032 },
+{ "Prime", 0x2033 },
+{ "lsaquo", 0x2039 },
+{ "rsaquo", 0x203A },
+{ "oline", 0x203E },
+{ "frasl", 0x2044 },
+{ "euro", 0x20AC },
+{ "image", 0x2111 },
+{ "weierp", 0x2118 },
+{ "real", 0x211C },
+{ "trade", 0x2122 },
+{ "alefsym", 0x2135 },
+{ "larr", 0x2190 },
+{ "uarr", 0x2191 },
+{ "rarr", 0x2192 },
+{ "darr", 0x2193 },
+{ "harr", 0x2194 },
+{ "crarr", 0x21B5 },
+{ "lArr", 0x21D0 },
+{ "uArr", 0x21D1 },
+{ "rArr", 0x21D2 },
+{ "dArr", 0x21D3 },
+{ "hArr", 0x21D4 },
+{ "forall", 0x2200 },
+{ "part", 0x2202 },
+{ "exist", 0x2203 },
+{ "empty", 0x2205 },
+{ "nabla", 0x2207 },
+{ "isin", 0x2208 },
+{ "notin", 0x2209 },
+{ "ni", 0x220B },
+{ "prod", 0x220F },
+{ "sum", 0x2211 },
+{ "minus", 0x2212 },
+{ "lowast", 0x2217 },
+{ "radic", 0x221A },
+{ "prop", 0x221D },
+{ "infin", 0x221E },
+{ "ang", 0x2220 },
+{ "and", 0x2227 },
+{ "or", 0x2228 },
+{ "cap", 0x2229 },
+{ "cup", 0x222A },
+{ "int", 0x222B },
+{ "there4", 0x2234 },
+{ "sim", 0x223C },
+{ "cong", 0x2245 },
+{ "asymp", 0x2248 },
+{ "ne", 0x2260 },
+{ "equiv", 0x2261 },
+{ "le", 0x2264 },
+{ "ge", 0x2265 },
+{ "sub", 0x2282 },
+{ "sup", 0x2283 },
+{ "nsub", 0x2284 },
+{ "sube", 0x2286 },
+{ "supe", 0x2287 },
+{ "oplus", 0x2295 },
+{ "otimes", 0x2297 },
+{ "perp", 0x22A5 },
+{ "sdot", 0x22C5 },
+{ "lceil", 0x2308 },
+{ "rceil", 0x2309 },
+{ "lfloor", 0x230A },
+{ "rfloor", 0x230B },
+{ "lang", 0x27E8 },
+{ "rang", 0x27E9 },
+{ "loz", 0x25CA },
+{ "spades", 0x2660 },
+{ "clubs", 0x2663 },
+{ "hearts", 0x2665 },
+{ "diams", 0x2666 }
diff --git a/src/lib-mail/istream-attachment-connector.c b/src/lib-mail/istream-attachment-connector.c
new file mode 100644
index 0000000..cb66563
--- /dev/null
+++ b/src/lib-mail/istream-attachment-connector.c
@@ -0,0 +1,149 @@
+/* Copyright (c) 2003-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "istream.h"
+#include "istream-concat.h"
+#include "istream-sized.h"
+#include "istream-base64.h"
+#include "istream-attachment-connector.h"
+
+struct istream_attachment_connector {
+ pool_t pool;
+ struct istream *base_input;
+ uoff_t base_input_offset, msg_size;
+
+ uoff_t encoded_offset;
+ ARRAY(struct istream *) streams;
+};
+
+struct istream_attachment_connector *
+istream_attachment_connector_begin(struct istream *base_input, uoff_t msg_size)
+{
+ struct istream_attachment_connector *conn;
+ pool_t pool;
+
+ pool = pool_alloconly_create("istream-attachment-connector", 1024);
+ conn = p_new(pool, struct istream_attachment_connector, 1);
+ conn->pool = pool;
+ conn->base_input = base_input;
+ conn->base_input_offset = base_input->v_offset;
+ conn->msg_size = msg_size;
+ p_array_init(&conn->streams, pool, 8);
+ i_stream_ref(conn->base_input);
+ return conn;
+}
+
+int istream_attachment_connector_add(struct istream_attachment_connector *conn,
+ struct istream *decoded_input,
+ uoff_t start_offset, uoff_t encoded_size,
+ unsigned int base64_blocks_per_line,
+ bool base64_have_crlf,
+ const char **error_r)
+{
+ struct istream *input, *input2;
+ uoff_t base_prefix_size;
+
+ if (start_offset < conn->encoded_offset) {
+ *error_r = t_strdup_printf(
+ "Attachment %s points before the previous attachment "
+ "(%"PRIuUOFF_T" < %"PRIuUOFF_T")",
+ i_stream_get_name(decoded_input),
+ start_offset, conn->encoded_offset);
+ return -1;
+ }
+ base_prefix_size = start_offset - conn->encoded_offset;
+ if (start_offset + encoded_size > conn->msg_size) {
+ *error_r = t_strdup_printf(
+ "Attachment %s points outside message "
+ "(%"PRIuUOFF_T" + %"PRIuUOFF_T" > %"PRIuUOFF_T")",
+ i_stream_get_name(decoded_input),
+ start_offset, encoded_size,
+ conn->msg_size);
+ return -1;
+ }
+
+ if (base_prefix_size > 0) {
+ /* add a part of the base message before the attachment */
+ input = i_stream_create_min_sized_range(conn->base_input,
+ conn->base_input_offset, base_prefix_size);
+ i_stream_set_name(input, t_strdup_printf("%s middle",
+ i_stream_get_name(conn->base_input)));
+ array_push_back(&conn->streams, &input);
+ conn->base_input_offset += base_prefix_size;
+ conn->encoded_offset += base_prefix_size;
+ }
+ conn->encoded_offset += encoded_size;
+
+ if (base64_blocks_per_line == 0) {
+ input = decoded_input;
+ i_stream_ref(input);
+ } else {
+ input = i_stream_create_base64_encoder(decoded_input,
+ base64_blocks_per_line*4,
+ base64_have_crlf);
+ i_stream_set_name(input, t_strdup_printf("%s[base64:%u b/l%s]",
+ i_stream_get_name(decoded_input),
+ base64_blocks_per_line,
+ base64_have_crlf ? ",crlf" : ""));
+ }
+ input2 = i_stream_create_sized(input, encoded_size);
+ array_push_back(&conn->streams, &input2);
+ i_stream_unref(&input);
+ return 0;
+}
+
+static void
+istream_attachment_connector_free(struct istream_attachment_connector *conn)
+{
+ struct istream *stream;
+
+ array_foreach_elem(&conn->streams, stream)
+ i_stream_unref(&stream);
+ i_stream_unref(&conn->base_input);
+ pool_unref(&conn->pool);
+}
+
+struct istream *
+istream_attachment_connector_finish(struct istream_attachment_connector **_conn)
+{
+ struct istream_attachment_connector *conn = *_conn;
+ struct istream **inputs, *input;
+ uoff_t trailer_size;
+
+ *_conn = NULL;
+
+ if (conn->base_input_offset != conn->msg_size) {
+ i_assert(conn->base_input_offset < conn->msg_size);
+
+ if (conn->msg_size != UOFF_T_MAX) {
+ trailer_size = conn->msg_size - conn->encoded_offset;
+ input = i_stream_create_sized_range(conn->base_input,
+ conn->base_input_offset,
+ trailer_size);
+ i_stream_set_name(input, t_strdup_printf(
+ "%s trailer", i_stream_get_name(conn->base_input)));
+ } else {
+ input = i_stream_create_range(conn->base_input,
+ conn->base_input_offset,
+ UOFF_T_MAX);
+ }
+ array_push_back(&conn->streams, &input);
+ }
+ array_append_zero(&conn->streams);
+
+ inputs = array_front_modifiable(&conn->streams);
+ input = i_stream_create_concat(inputs);
+
+ istream_attachment_connector_free(conn);
+ return input;
+}
+
+void istream_attachment_connector_abort(struct istream_attachment_connector **_conn)
+{
+ struct istream_attachment_connector *conn = *_conn;
+
+ *_conn = NULL;
+
+ istream_attachment_connector_free(conn);
+}
diff --git a/src/lib-mail/istream-attachment-connector.h b/src/lib-mail/istream-attachment-connector.h
new file mode 100644
index 0000000..df829cd
--- /dev/null
+++ b/src/lib-mail/istream-attachment-connector.h
@@ -0,0 +1,28 @@
+#ifndef ISTREAM_ATTACHMENT_CONNECTOR_H
+#define ISTREAM_ATTACHMENT_CONNECTOR_H
+
+/* Start building a message stream. The base_input contains the message
+ without attachments. The final stream must be exactly msg_size bytes.
+ If the original msg_size isn't known, it can be set to UOFF_T_MAX. */
+struct istream_attachment_connector *
+istream_attachment_connector_begin(struct istream *base_input, uoff_t msg_size);
+
+/* Add the given input stream as attachment. The attachment starts at the given
+ start_offset in the (original) message. If base64_blocks_per_line is
+ non-zero, the input is base64-encoded with the given settings. The
+ (resulting base64-encoded) input must have exactly encoded_size bytes.
+
+ Returns 0 if the input was ok, -1 if we've already reached msg_size or
+ attachment offsets/sizes aren't valid. */
+int istream_attachment_connector_add(struct istream_attachment_connector *conn,
+ struct istream *decoded_input,
+ uoff_t start_offset, uoff_t encoded_size,
+ unsigned int base64_blocks_per_line,
+ bool base64_have_crlf,
+ const char **error_r);
+
+struct istream *
+istream_attachment_connector_finish(struct istream_attachment_connector **conn);
+void istream_attachment_connector_abort(struct istream_attachment_connector **conn);
+
+#endif
diff --git a/src/lib-mail/istream-attachment-extractor.c b/src/lib-mail/istream-attachment-extractor.c
new file mode 100644
index 0000000..7d4ac01
--- /dev/null
+++ b/src/lib-mail/istream-attachment-extractor.c
@@ -0,0 +1,740 @@
+/* Copyright (c) 2013-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "istream-private.h"
+#include "ostream.h"
+#include "base64.h"
+#include "buffer.h"
+#include "str.h"
+#include "hash-format.h"
+#include "rfc822-parser.h"
+#include "message-parser.h"
+#include "istream-attachment-extractor.h"
+
+#define BASE64_ATTACHMENT_MAX_EXTRA_BYTES 1024
+
+enum mail_attachment_state {
+ MAIL_ATTACHMENT_STATE_NO,
+ MAIL_ATTACHMENT_STATE_MAYBE,
+ MAIL_ATTACHMENT_STATE_YES
+};
+
+enum base64_state {
+ BASE64_STATE_0 = 0,
+ BASE64_STATE_1,
+ BASE64_STATE_2,
+ BASE64_STATE_3,
+ BASE64_STATE_CR,
+ BASE64_STATE_EOB,
+ BASE64_STATE_EOM
+};
+
+struct attachment_istream_part {
+ char *content_type, *content_disposition;
+ enum mail_attachment_state state;
+ /* start offset of the message part in the original input stream */
+ uoff_t start_offset;
+
+ /* for saving attachments base64-decoded: */
+ enum base64_state base64_state;
+ unsigned int base64_line_blocks, cur_base64_blocks;
+ uoff_t base64_bytes;
+ bool base64_have_crlf; /* CRLF linefeeds */
+ bool base64_failed;
+
+ int temp_fd;
+ struct ostream *temp_output;
+ buffer_t *part_buf;
+};
+
+struct attachment_istream {
+ struct istream_private istream;
+ pool_t pool;
+
+ struct istream_attachment_settings set;
+ void *context;
+
+ struct message_parser_ctx *parser;
+ struct message_part *cur_part;
+ struct attachment_istream_part part;
+
+ bool retry_read;
+};
+
+static void stream_add_data(struct attachment_istream *astream,
+ const void *data, size_t size)
+{
+ if (size > 0) {
+ memcpy(i_stream_alloc(&astream->istream, size), data, size);
+ astream->istream.pos += size;
+ }
+}
+
+static void parse_content_type(struct attachment_istream *astream,
+ const struct message_header_line *hdr)
+{
+ struct rfc822_parser_context parser;
+ string_t *content_type;
+
+ if (astream->part.content_type != NULL)
+ return;
+
+ rfc822_parser_init(&parser, hdr->full_value, hdr->full_value_len, NULL);
+ rfc822_skip_lwsp(&parser);
+
+ T_BEGIN {
+ content_type = t_str_new(64);
+ (void)rfc822_parse_content_type(&parser, content_type);
+ astream->part.content_type = i_strdup(str_c(content_type));
+ } T_END;
+ rfc822_parser_deinit(&parser);
+}
+
+static void
+parse_content_disposition(struct attachment_istream *astream,
+ const struct message_header_line *hdr)
+{
+ /* just pass it without parsing to is_attachment() callback */
+ i_free(astream->part.content_disposition);
+ astream->part.content_disposition =
+ i_strndup(hdr->full_value, hdr->full_value_len);
+}
+
+static void astream_parse_header(struct attachment_istream *astream,
+ struct message_header_line *hdr)
+{
+ if (!hdr->continued) {
+ stream_add_data(astream, hdr->name, hdr->name_len);
+ stream_add_data(astream, hdr->middle, hdr->middle_len);
+ }
+ stream_add_data(astream, hdr->value, hdr->value_len);
+ if (!hdr->no_newline) {
+ if (hdr->crlf_newline)
+ stream_add_data(astream, "\r\n", 2);
+ else
+ stream_add_data(astream, "\n", 1);
+ }
+
+ if (hdr->continues) {
+ hdr->use_full_value = TRUE;
+ return;
+ }
+
+ if (strcasecmp(hdr->name, "Content-Type") == 0)
+ parse_content_type(astream, hdr);
+ else if (strcasecmp(hdr->name, "Content-Disposition") == 0)
+ parse_content_disposition(astream, hdr);
+}
+
+static bool astream_want_attachment(struct attachment_istream *astream,
+ struct message_part *part)
+{
+ struct istream_attachment_header ahdr;
+
+ if ((part->flags & MESSAGE_PART_FLAG_MULTIPART) != 0) {
+ /* multiparts may contain attachments as children,
+ but they're never themselves */
+ return FALSE;
+ }
+ if (astream->set.want_attachment == NULL)
+ return TRUE;
+
+ i_zero(&ahdr);
+ ahdr.part = part;
+ ahdr.content_type = astream->part.content_type;
+ ahdr.content_disposition = astream->part.content_disposition;
+ return astream->set.want_attachment(&ahdr, astream->context);
+}
+
+static int astream_base64_decode_lf(struct attachment_istream_part *part)
+{
+ if (part->base64_have_crlf && part->base64_state != BASE64_STATE_CR) {
+ /* mixed LF vs CRLFs */
+ return -1;
+ }
+ part->base64_state = BASE64_STATE_0;
+ if (part->cur_base64_blocks < part->base64_line_blocks) {
+ /* last line */
+ part->base64_state = BASE64_STATE_EOM;
+ return 0;
+ } else if (part->base64_line_blocks == 0) {
+ /* first line */
+ if (part->cur_base64_blocks == 0)
+ return -1;
+ part->base64_line_blocks = part->cur_base64_blocks;
+ } else if (part->cur_base64_blocks == part->base64_line_blocks) {
+ /* line is ok */
+ } else {
+ return -1;
+ }
+ part->cur_base64_blocks = 0;
+ return 1;
+}
+
+static int
+astream_try_base64_decode_char(struct attachment_istream_part *part,
+ size_t pos, char chr)
+{
+ switch (part->base64_state) {
+ case BASE64_STATE_0:
+ if (base64_is_valid_char(chr))
+ part->base64_state++;
+ else if (chr == '\r')
+ part->base64_state = BASE64_STATE_CR;
+ else if (chr == '\n') {
+ return astream_base64_decode_lf(part);
+ } else {
+ return -1;
+ }
+ break;
+ case BASE64_STATE_1:
+ if (!base64_is_valid_char(chr))
+ return -1;
+ part->base64_state++;
+ break;
+ case BASE64_STATE_2:
+ if (base64_is_valid_char(chr))
+ part->base64_state++;
+ else if (chr == '=')
+ part->base64_state = BASE64_STATE_EOB;
+ else
+ return -1;
+ break;
+ case BASE64_STATE_3:
+ part->base64_bytes = part->temp_output->offset + pos + 1;
+ if (base64_is_valid_char(chr)) {
+ part->base64_state = BASE64_STATE_0;
+ part->cur_base64_blocks++;
+ } else if (chr == '=') {
+ part->base64_state = BASE64_STATE_EOM;
+ part->cur_base64_blocks++;
+
+ if (part->cur_base64_blocks > part->base64_line_blocks &&
+ part->base64_line_blocks > 0) {
+ /* too many blocks */
+ return -1;
+ }
+ return 0;
+ } else {
+ return -1;
+ }
+ break;
+ case BASE64_STATE_CR:
+ if (chr != '\n')
+ return -1;
+ if (!part->base64_have_crlf) {
+ if (part->base64_line_blocks != 0) {
+ /* mixed LF vs CRLFs */
+ return -1;
+ }
+ part->base64_have_crlf = TRUE;
+ }
+ return astream_base64_decode_lf(part);
+ case BASE64_STATE_EOB:
+ if (chr != '=')
+ return -1;
+
+ part->base64_bytes = part->temp_output->offset + pos + 1;
+ part->base64_state = BASE64_STATE_EOM;
+ part->cur_base64_blocks++;
+
+ if (part->cur_base64_blocks > part->base64_line_blocks &&
+ part->base64_line_blocks > 0) {
+ /* too many blocks */
+ return -1;
+ }
+ return 0;
+ case BASE64_STATE_EOM:
+ i_unreached();
+ }
+ return 1;
+}
+
+static void
+astream_try_base64_decode(struct attachment_istream_part *part,
+ const unsigned char *data, size_t size)
+{
+ size_t i;
+ int ret;
+
+ if (part->base64_failed || part->base64_state == BASE64_STATE_EOM)
+ return;
+
+ for (i = 0; i < size; i++) {
+ ret = astream_try_base64_decode_char(part, i, (char)data[i]);
+ if (ret <= 0) {
+ if (ret < 0)
+ part->base64_failed = TRUE;
+ break;
+ }
+ }
+}
+
+static int astream_open_output(struct attachment_istream *astream)
+{
+ int fd;
+
+ i_assert(astream->part.temp_fd == -1);
+
+ fd = astream->set.open_temp_fd(astream->context);
+ if (fd == -1)
+ return -1;
+
+ astream->part.temp_fd = fd;
+ astream->part.temp_output = o_stream_create_fd(fd, 0);
+ o_stream_cork(astream->part.temp_output);
+ return 0;
+}
+
+static void astream_add_body(struct attachment_istream *astream,
+ const struct message_block *block)
+{
+ struct attachment_istream_part *part = &astream->part;
+ buffer_t *part_buf;
+ size_t new_size;
+
+ switch (part->state) {
+ case MAIL_ATTACHMENT_STATE_NO:
+ stream_add_data(astream, block->data, block->size);
+ break;
+ case MAIL_ATTACHMENT_STATE_MAYBE:
+ /* we'll write data to in-memory buffer until we reach
+ attachment min_size */
+ if (part->part_buf == NULL) {
+ part->part_buf =
+ buffer_create_dynamic(default_pool,
+ astream->set.min_size);
+ }
+ part_buf = part->part_buf;
+ new_size = part_buf->used + block->size;
+ if (new_size < astream->set.min_size) {
+ buffer_append(part_buf, block->data, block->size);
+ break;
+ }
+ /* attachment is large enough. we'll first copy the buffered
+ data from memory to temp file */
+ if (astream_open_output(astream) < 0) {
+ /* failed, fallback to just saving it inline */
+ part->state = MAIL_ATTACHMENT_STATE_NO;
+ stream_add_data(astream, part_buf->data, part_buf->used);
+ stream_add_data(astream, block->data, block->size);
+ break;
+ }
+ part->state = MAIL_ATTACHMENT_STATE_YES;
+ astream_try_base64_decode(part, part_buf->data, part_buf->used);
+ hash_format_loop(astream->set.hash_format,
+ part_buf->data, part_buf->used);
+ o_stream_nsend(part->temp_output,
+ part_buf->data, part_buf->used);
+ buffer_set_used_size(part_buf, 0);
+ /* fall through - write the new data to temp file */
+ case MAIL_ATTACHMENT_STATE_YES:
+ astream_try_base64_decode(part, block->data, block->size);
+ hash_format_loop(astream->set.hash_format,
+ block->data, block->size);
+ o_stream_nsend(part->temp_output, block->data, block->size);
+ break;
+ }
+}
+
+static int astream_decode_base64(struct attachment_istream *astream,
+ buffer_t **extra_buf_r)
+{
+ struct attachment_istream_part *part = &astream->part;
+ struct base64_decoder b64dec;
+ struct istream *input, *base64_input;
+ struct ostream *output;
+ const unsigned char *data;
+ size_t size;
+ ssize_t ret;
+ buffer_t *buf;
+ int outfd;
+ bool failed = FALSE;
+
+ *extra_buf_r = NULL;
+
+ if (part->base64_bytes < astream->set.min_size ||
+ part->temp_output->offset > part->base64_bytes +
+ BASE64_ATTACHMENT_MAX_EXTRA_BYTES) {
+ /* only a small part of the MIME part is base64-encoded. */
+ return -1;
+ }
+
+ if (part->base64_line_blocks == 0) {
+ /* only one line of base64 */
+ part->base64_line_blocks = part->cur_base64_blocks;
+ i_assert(part->base64_line_blocks > 0);
+ }
+
+ /* decode base64 data and write it to another temp file */
+ outfd = astream->set.open_temp_fd(astream->context);
+ if (outfd == -1)
+ return -1;
+
+ buf = buffer_create_dynamic(default_pool, 1024);
+ input = i_stream_create_fd(part->temp_fd, IO_BLOCK_SIZE);
+ base64_input = i_stream_create_limit(input, part->base64_bytes);
+ output = o_stream_create_fd_file(outfd, 0, FALSE);
+ o_stream_cork(output);
+
+ base64_decode_init(&b64dec, &base64_scheme, 0);
+ hash_format_reset(astream->set.hash_format);
+ size_t bytes_needed = 1;
+ while ((ret = i_stream_read_bytes(base64_input, &data, &size,
+ bytes_needed)) > 0) {
+ buffer_set_used_size(buf, 0);
+ if (base64_decode_more(&b64dec, data, size, &size, buf) < 0) {
+ i_error("istream-attachment: BUG: "
+ "Attachment base64 data unexpectedly broke");
+ failed = TRUE;
+ break;
+ }
+ i_stream_skip(base64_input, size);
+ o_stream_nsend(output, buf->data, buf->used);
+ hash_format_loop(astream->set.hash_format,
+ buf->data, buf->used);
+ bytes_needed = i_stream_get_data_size(base64_input) + 1;
+ }
+ if (ret != -1) {
+ i_assert(failed);
+ } else if (base64_input->stream_errno != 0) {
+ i_error("istream-attachment: read(%s) failed: %s",
+ i_stream_get_name(base64_input),
+ i_stream_get_error(base64_input));
+ failed = TRUE;
+ }
+ if (base64_decode_finish(&b64dec) < 0) {
+ i_error("istream-attachment: BUG: "
+ "Attachment base64 data unexpectedly broke");
+ failed = TRUE;
+ }
+ if (o_stream_finish(output) < 0) {
+ i_error("istream-attachment: write(%s) failed: %s",
+ o_stream_get_name(output), o_stream_get_error(output));
+ failed = TRUE;
+ }
+
+ buffer_free(&buf);
+ i_stream_unref(&base64_input);
+ o_stream_unref(&output);
+
+ if (input->v_offset != part->temp_output->offset && !failed) {
+ /* write the rest of the data to the message stream */
+ *extra_buf_r = buffer_create_dynamic(default_pool, 1024);
+ while ((ret = i_stream_read_more(input, &data, &size)) > 0) {
+ buffer_append(*extra_buf_r, data, size);
+ i_stream_skip(input, size);
+ }
+ i_assert(ret == -1);
+ if (input->stream_errno != 0) {
+ i_error("istream-attachment: read(%s) failed: %s",
+ i_stream_get_name(input),
+ i_stream_get_error(input));
+ failed = TRUE;
+ }
+ }
+ i_stream_unref(&input);
+
+ if (failed) {
+ i_close_fd(&outfd);
+ return -1;
+ }
+
+ /* successfully wrote it. switch to using it. */
+ o_stream_destroy(&part->temp_output);
+ i_close_fd(&part->temp_fd);
+ part->temp_fd = outfd;
+ return 0;
+}
+
+static int
+astream_part_finish(struct attachment_istream *astream, const char **error_r)
+{
+ struct attachment_istream_part *part = &astream->part;
+ struct istream_attachment_info info;
+ struct istream *input;
+ struct ostream *output;
+ string_t *digest_str;
+ buffer_t *extra_buf = NULL;
+ const unsigned char *data;
+ size_t size;
+ int ret = 0;
+
+ if (o_stream_finish(part->temp_output) < 0) {
+ *error_r = t_strdup_printf("write(%s) failed: %s",
+ o_stream_get_name(part->temp_output),
+ o_stream_get_error(part->temp_output));
+ return -1;
+ }
+
+ i_zero(&info);
+ info.start_offset = astream->part.start_offset;
+ /* base64_bytes contains how many valid base64 bytes there are so far.
+ if the base64 ends properly, it'll specify how much of the MIME part
+ is saved as an attachment. the rest of the data (typically
+ linefeeds) is added back to main stream */
+ info.encoded_size = part->base64_bytes;
+ /* get the hash before base64-decoder resets it */
+ digest_str = t_str_new(128);
+ hash_format_write(astream->set.hash_format, digest_str);
+ info.hash = str_c(digest_str);
+
+ /* if it looks like we can decode base64 without any data loss,
+ do it and write the decoded data to another temp file. */
+ if (!part->base64_failed) {
+ if (part->base64_state == BASE64_STATE_0 &&
+ part->base64_bytes > 0) {
+ /* there is no trailing LF or '=' characters,
+ but it's not completely empty */
+ part->base64_state = BASE64_STATE_EOM;
+ }
+ if (part->base64_state == BASE64_STATE_EOM) {
+ /* base64 data looks ok. */
+ if (astream_decode_base64(astream, &extra_buf) < 0)
+ part->base64_failed = TRUE;
+ } else {
+ part->base64_failed = TRUE;
+ }
+ }
+
+ /* open attachment output file */
+ info.part = astream->cur_part;
+ if (!part->base64_failed) {
+ info.base64_blocks_per_line = part->base64_line_blocks;
+ info.base64_have_crlf = part->base64_have_crlf;
+ /* base64-decoder updated the hash, use it */
+ str_truncate(digest_str, 0);
+ hash_format_write(astream->set.hash_format, digest_str);
+ info.hash = str_c(digest_str);
+ } else {
+ /* couldn't decode base64, so write the entire MIME part
+ as attachment */
+ info.encoded_size = part->temp_output->offset;
+ }
+ if (astream->set.open_attachment_ostream(&info, &output, error_r,
+ astream->context) < 0) {
+ buffer_free(&extra_buf);
+ return -1;
+ }
+
+ /* copy data to attachment from temp file */
+ input = i_stream_create_fd(part->temp_fd, IO_BLOCK_SIZE);
+ while (i_stream_read_more(input, &data, &size) > 0) {
+ o_stream_nsend(output, data, size);
+ i_stream_skip(input, size);
+ }
+
+ if (input->stream_errno != 0) {
+ *error_r = t_strdup_printf("read(%s) failed: %s",
+ i_stream_get_name(input), i_stream_get_error(input));
+ ret = -1;
+ }
+ i_stream_destroy(&input);
+
+ if (astream->set.close_attachment_ostream(output, ret == 0, error_r,
+ astream->context) < 0)
+ ret = -1;
+ if (ret == 0 && extra_buf != NULL)
+ stream_add_data(astream, extra_buf->data, extra_buf->used);
+ buffer_free(&extra_buf);
+ return ret;
+}
+
+static void astream_part_reset(struct attachment_istream *astream)
+{
+ struct attachment_istream_part *part = &astream->part;
+
+ o_stream_destroy(&part->temp_output);
+ i_close_fd(&part->temp_fd);
+
+ i_free_and_null(part->content_type);
+ i_free_and_null(part->content_disposition);
+ buffer_free(&part->part_buf);
+
+ i_zero(part);
+ part->temp_fd = -1;
+ hash_format_reset(astream->set.hash_format);
+}
+
+static int
+astream_end_of_part(struct attachment_istream *astream, const char **error_r)
+{
+ struct attachment_istream_part *part = &astream->part;
+ size_t old_size;
+ int ret = 0;
+
+ /* MIME part changed. we're now parsing the end of a boundary,
+ possibly followed by message epilogue */
+ switch (part->state) {
+ case MAIL_ATTACHMENT_STATE_NO:
+ break;
+ case MAIL_ATTACHMENT_STATE_MAYBE:
+ /* MIME part wasn't large enough to be an attachment */
+ if (part->part_buf != NULL) {
+ stream_add_data(astream, part->part_buf->data,
+ part->part_buf->used);
+ ret = part->part_buf->used > 0 ? 1 : 0;
+ }
+ break;
+ case MAIL_ATTACHMENT_STATE_YES:
+ old_size = astream->istream.pos - astream->istream.skip;
+ if (astream_part_finish(astream, error_r) < 0)
+ ret = -1;
+ else {
+ /* finished base64 may have added a few more trailing
+ bytes to the stream */
+ ret = astream->istream.pos -
+ astream->istream.skip - old_size;
+ }
+ break;
+ }
+ part->state = MAIL_ATTACHMENT_STATE_NO;
+ astream_part_reset(astream);
+ return ret;
+}
+
+static int astream_read_next(struct attachment_istream *astream, bool *retry_r)
+{
+ struct istream_private *stream = &astream->istream;
+ struct message_block block;
+ size_t old_size, new_size;
+ const char *error;
+ int ret;
+
+ *retry_r = FALSE;
+
+ if (stream->pos - stream->skip >= i_stream_get_max_buffer_size(&stream->istream))
+ return -2;
+
+ old_size = stream->pos - stream->skip;
+ switch (message_parser_parse_next_block(astream->parser, &block)) {
+ case -1:
+ /* done / error */
+ ret = astream_end_of_part(astream, &error);
+ if (ret > 0) {
+ /* final data */
+ new_size = stream->pos - stream->skip;
+ return new_size - old_size;
+ }
+ stream->istream.eof = TRUE;
+ stream->istream.stream_errno = stream->parent->stream_errno;
+
+ if (ret < 0) {
+ io_stream_set_error(&stream->iostream, "%s", error);
+ stream->istream.stream_errno = EIO;
+ }
+ astream->cur_part = NULL;
+ return -1;
+ case 0:
+ /* need more data */
+ return 0;
+ default:
+ break;
+ }
+
+ if (block.part != astream->cur_part && astream->cur_part != NULL) {
+ /* end of a MIME part */
+ if (astream_end_of_part(astream, &error) < 0) {
+ io_stream_set_error(&stream->iostream, "%s", error);
+ stream->istream.stream_errno = EIO;
+ return -1;
+ }
+ }
+ astream->cur_part = block.part;
+
+ if (block.hdr != NULL) {
+ /* parsing a header */
+ astream_parse_header(astream, block.hdr);
+ } else if (block.size == 0) {
+ /* end of headers */
+ if (astream_want_attachment(astream, block.part)) {
+ astream->part.state = MAIL_ATTACHMENT_STATE_MAYBE;
+ astream->part.start_offset = stream->parent->v_offset;
+ }
+ } else {
+ astream_add_body(astream, &block);
+ }
+ new_size = stream->pos - stream->skip;
+ *retry_r = new_size == old_size;
+ return new_size - old_size;
+}
+
+static ssize_t
+i_stream_attachment_extractor_read(struct istream_private *stream)
+{
+ struct attachment_istream *astream =
+ (struct attachment_istream *)stream;
+ bool retry;
+ ssize_t ret;
+
+ do {
+ ret = astream_read_next(astream, &retry);
+ } while (retry && astream->set.drain_parent_input);
+
+ astream->retry_read = retry;
+ return ret;
+}
+
+static void i_stream_attachment_extractor_close(struct iostream_private *stream,
+ bool close_parent)
+{
+ struct attachment_istream *astream =
+ (struct attachment_istream *)stream;
+ struct message_part *parts;
+
+ if (astream->parser != NULL) {
+ message_parser_deinit(&astream->parser, &parts);
+ }
+ hash_format_deinit_free(&astream->set.hash_format);
+ pool_unref(&astream->pool);
+ if (close_parent)
+ i_stream_close(astream->istream.parent);
+}
+
+struct istream *
+i_stream_create_attachment_extractor(struct istream *input,
+ struct istream_attachment_settings *set,
+ void *context)
+{
+ const struct message_parser_settings parser_set = {
+ .flags = MESSAGE_PARSER_FLAG_INCLUDE_MULTIPART_BLOCKS |
+ MESSAGE_PARSER_FLAG_INCLUDE_BOUNDARIES,
+ };
+ struct attachment_istream *astream;
+
+ i_assert(set->min_size > 0);
+ i_assert(set->hash_format != NULL);
+ i_assert(set->open_attachment_ostream != NULL);
+ i_assert(set->close_attachment_ostream != NULL);
+
+ astream = i_new(struct attachment_istream, 1);
+ astream->part.temp_fd = -1;
+ astream->set = *set;
+ astream->context = context;
+ astream->retry_read = TRUE;
+
+ /* make sure the caller doesn't try to double-free this */
+ set->hash_format = NULL;
+
+ astream->istream.max_buffer_size = input->real_stream->max_buffer_size;
+
+ astream->istream.read = i_stream_attachment_extractor_read;
+ astream->istream.iostream.close = i_stream_attachment_extractor_close;
+
+ astream->istream.istream.readable_fd = FALSE;
+ astream->istream.istream.blocking = input->blocking;
+ astream->istream.istream.seekable = FALSE;
+
+ astream->pool = pool_alloconly_create("istream attachment", 1024);
+ astream->parser = message_parser_init(astream->pool, input, &parser_set);
+ return i_stream_create(&astream->istream, input,
+ i_stream_get_fd(input), 0);
+}
+
+bool i_stream_attachment_extractor_can_retry(struct istream *input)
+{
+ struct attachment_istream *astream =
+ (struct attachment_istream *)input->real_stream;
+
+ return astream->retry_read;
+}
diff --git a/src/lib-mail/istream-attachment-extractor.h b/src/lib-mail/istream-attachment-extractor.h
new file mode 100644
index 0000000..151566e
--- /dev/null
+++ b/src/lib-mail/istream-attachment-extractor.h
@@ -0,0 +1,62 @@
+#ifndef ISTREAM_ATTACHMENT_H
+#define ISTREAM_ATTACHMENT_H
+
+struct istream_attachment_header {
+ struct message_part *part;
+ const char *content_type, *content_disposition;
+};
+
+struct istream_attachment_info {
+ const char *hash;
+ /* offset within input stream where the attachment starts */
+ uoff_t start_offset;
+ /* original (base64-encoded) size of the attachment */
+ uoff_t encoded_size;
+
+ unsigned int base64_blocks_per_line;
+ bool base64_have_crlf;
+
+ const struct message_part *part;
+};
+
+struct istream_attachment_settings {
+ /* Minimum size of of a MIME part to be saved separately. */
+ uoff_t min_size;
+ /* Format to use when calculating attachment's hash. */
+ struct hash_format *hash_format;
+ /* Set this to TRUE if parent stream can be read from as long as
+ wanted. This is useful when parsing attachments, which the extractor
+ hides from read() output, so they would return a lot of 0.
+ On the other hand if you have a tee-istream, it's not a good idea
+ to let it get to "buffer full" state. */
+ bool drain_parent_input;
+
+ /* Returns TRUE if message part is wanted to be stored as separate
+ attachment. If NULL, assume we want the attachment. */
+ bool (*want_attachment)(const struct istream_attachment_header *hdr,
+ void *context);
+ /* Create a temporary file. */
+ int (*open_temp_fd)(void *context);
+ /* Create output stream for attachment */
+ int (*open_attachment_ostream)(struct istream_attachment_info *info,
+ struct ostream **output_r,
+ const char **error_r, void *context);
+ /* Finish output stream. If success==FALSE, *error contains the error
+ and the error shouldn't be replaced (other than maybe enhanced).
+ Otherwise, if close_attachment_ostream() fails and returns -1, it
+ should also set *error. */
+ int (*close_attachment_ostream)(struct ostream *output, bool success,
+ const char **error, void *context);
+};
+
+struct istream *
+i_stream_create_attachment_extractor(struct istream *input,
+ struct istream_attachment_settings *set,
+ void *context) ATTR_NULL(3);
+
+/* Returns TRUE if the last read returned 0 only because
+ drain_parent_input=FALSE and we didn't have anything to return, but
+ retrying a read from parent stream could give something the next time. */
+bool i_stream_attachment_extractor_can_retry(struct istream *input);
+
+#endif
diff --git a/src/lib-mail/istream-binary-converter.c b/src/lib-mail/istream-binary-converter.c
new file mode 100644
index 0000000..856b854
--- /dev/null
+++ b/src/lib-mail/istream-binary-converter.c
@@ -0,0 +1,309 @@
+/* Copyright (c) 2013-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "buffer.h"
+#include "base64.h"
+#include "istream-private.h"
+#include "message-parser.h"
+#include "istream-binary-converter.h"
+
+#define BASE64_BLOCK_INPUT_SIZE 3
+#define BASE64_BLOCK_SIZE 4
+#define BASE64_BLOCKS_PER_LINE (76/BASE64_BLOCK_SIZE)
+#define MAX_HDR_BUFFER_SIZE (1024*32)
+
+struct binary_converter_istream {
+ struct istream_private istream;
+
+ pool_t pool;
+ struct message_parser_ctx *parser;
+ struct message_part *convert_part;
+ char base64_delayed[BASE64_BLOCK_INPUT_SIZE-1];
+ unsigned int base64_delayed_len;
+ unsigned int base64_block_pos;
+
+ buffer_t *hdr_buf;
+ size_t cte_header_len;
+ bool content_type_seen:1;
+};
+
+static void stream_add_data(struct binary_converter_istream *bstream,
+ const void *data, size_t size);
+
+static bool part_can_convert(const struct message_part *part)
+{
+ /* some MUAs use "c-t-e: binary" for multiparts.
+ we don't want to convert them. */
+ return (part->flags & MESSAGE_PART_FLAG_MULTIPART) == 0;
+}
+
+static void
+stream_finish_convert_decision(struct binary_converter_istream *bstream)
+{
+ buffer_t *buf = bstream->hdr_buf;
+ const unsigned char *data;
+
+ i_assert(bstream->convert_part != NULL);
+
+ bstream->hdr_buf = NULL;
+ if (!part_can_convert(bstream->convert_part)) {
+ bstream->convert_part = NULL;
+ stream_add_data(bstream, buf->data, buf->used);
+ } else {
+ stream_add_data(bstream,
+ "Content-Transfer-Encoding: base64\r\n", 35);
+
+ data = CONST_PTR_OFFSET(buf->data, bstream->cte_header_len);
+ stream_add_data(bstream, data,
+ buf->used - bstream->cte_header_len);
+ }
+ buffer_free(&buf);
+}
+
+static void stream_add_data(struct binary_converter_istream *bstream,
+ const void *data, size_t size)
+{
+ if (size == 0)
+ return;
+
+ if (bstream->hdr_buf != NULL) {
+ if (bstream->hdr_buf->used + size <= MAX_HDR_BUFFER_SIZE) {
+ buffer_append(bstream->hdr_buf, data, size);
+ return;
+ }
+ /* buffer is getting too large. just finish the decision. */
+ stream_finish_convert_decision(bstream);
+ }
+
+ memcpy(i_stream_alloc(&bstream->istream, size), data, size);
+ bstream->istream.pos += size;
+}
+
+static void stream_encode_base64(struct binary_converter_istream *bstream,
+ const void *_data, size_t size)
+{
+ struct istream_private *stream = &bstream->istream;
+ const unsigned char *data = _data;
+ buffer_t buf;
+ void *dest;
+ size_t encode_size, max_encoded_size;
+ unsigned char base64_block[BASE64_BLOCK_INPUT_SIZE];
+ unsigned int base64_block_len, missing_len, encode_blocks;
+
+ if (bstream->base64_delayed_len > 0) {
+ if (bstream->base64_delayed_len == 1 && size == 1) {
+ bstream->base64_delayed[1] = data[0];
+ bstream->base64_delayed_len++;
+ return;
+ }
+ memcpy(base64_block, bstream->base64_delayed,
+ bstream->base64_delayed_len);
+ base64_block_len = bstream->base64_delayed_len;
+ if (size == 0) {
+ /* finish base64 */
+ } else {
+ missing_len = BASE64_BLOCK_INPUT_SIZE - base64_block_len;
+ i_assert(size >= missing_len);
+ memcpy(base64_block + base64_block_len,
+ data, missing_len);
+ data += missing_len;
+ size -= missing_len;
+ base64_block_len = BASE64_BLOCK_INPUT_SIZE;
+ }
+
+ if (bstream->base64_block_pos == BASE64_BLOCKS_PER_LINE) {
+ memcpy(i_stream_alloc(stream, 2), "\r\n", 2);
+ stream->pos += 2;
+ bstream->base64_block_pos = 0;
+ }
+
+ dest = i_stream_alloc(stream, BASE64_BLOCK_SIZE);
+ buffer_create_from_data(&buf, dest, BASE64_BLOCK_SIZE);
+ base64_encode(base64_block, base64_block_len, &buf);
+ stream->pos += buf.used;
+ bstream->base64_block_pos++;
+ bstream->base64_delayed_len = 0;
+ }
+
+ while (size >= BASE64_BLOCK_INPUT_SIZE) {
+ if (bstream->base64_block_pos == BASE64_BLOCKS_PER_LINE) {
+ memcpy(i_stream_alloc(stream, 2), "\r\n", 2);
+ stream->pos += 2;
+ bstream->base64_block_pos = 0;
+ }
+
+ /* try to encode one full line of base64 blocks */
+ encode_size = I_MIN(size, BASE64_BLOCKS_PER_LINE*BASE64_BLOCK_SIZE);
+ if (encode_size % BASE64_BLOCK_INPUT_SIZE != 0)
+ encode_size -= encode_size % BASE64_BLOCK_INPUT_SIZE;
+ encode_blocks = encode_size/BASE64_BLOCK_INPUT_SIZE;
+ if (bstream->base64_block_pos + encode_blocks > BASE64_BLOCKS_PER_LINE) {
+ encode_blocks = BASE64_BLOCKS_PER_LINE -
+ bstream->base64_block_pos;
+ encode_size = encode_blocks * BASE64_BLOCK_INPUT_SIZE;
+ }
+
+ max_encoded_size = MAX_BASE64_ENCODED_SIZE(encode_size);
+ dest = i_stream_alloc(stream, max_encoded_size);
+ buffer_create_from_data(&buf, dest, max_encoded_size);
+ base64_encode(data, encode_size, &buf);
+ stream->pos += buf.used;
+ bstream->base64_block_pos += encode_blocks;
+
+ data += encode_size;
+ size -= encode_size;
+ }
+ if (size > 0) {
+ /* encode these when more data is available */
+ i_assert(size < BASE64_BLOCK_INPUT_SIZE);
+ memcpy(bstream->base64_delayed, data, size);
+ bstream->base64_delayed_len = size;
+ }
+}
+
+static void stream_add_hdr(struct binary_converter_istream *bstream,
+ const struct message_header_line *hdr)
+{
+ if (!hdr->continued) {
+ stream_add_data(bstream, hdr->name, hdr->name_len);
+ stream_add_data(bstream, hdr->middle, hdr->middle_len);
+ }
+
+ stream_add_data(bstream, hdr->value, hdr->value_len);
+ if (!hdr->no_newline)
+ stream_add_data(bstream, "\r\n", 2);
+}
+
+static ssize_t i_stream_binary_converter_read(struct istream_private *stream)
+{
+ /* @UNSAFE */
+ struct binary_converter_istream *bstream =
+ (struct binary_converter_istream *)stream;
+ struct message_block block;
+ size_t old_size, new_size;
+
+ if (stream->pos - stream->skip >= i_stream_get_max_buffer_size(&stream->istream))
+ return -2;
+ old_size = stream->pos - stream->skip;
+
+ switch (message_parser_parse_next_block(bstream->parser, &block)) {
+ case -1:
+ /* done / error */
+ if (bstream->convert_part != NULL &&
+ bstream->base64_delayed_len > 0) {
+ /* flush any pending base64 output */
+ stream_encode_base64(bstream, "", 0);
+ new_size = stream->pos - stream->skip;
+ i_assert(old_size != new_size);
+ return new_size - old_size;
+ }
+ stream->istream.eof = TRUE;
+ stream->istream.stream_errno = stream->parent->stream_errno;
+ return -1;
+ case 0:
+ /* need more data */
+ return 0;
+ default:
+ break;
+ }
+
+ if (block.part != bstream->convert_part &&
+ bstream->convert_part != NULL) {
+ /* end of base64 encoded part */
+ stream_encode_base64(bstream, "", 0);
+ }
+
+ if (block.hdr != NULL) {
+ /* parsing a header */
+ if (strcasecmp(block.hdr->name, "Content-Type") == 0)
+ bstream->content_type_seen = TRUE;
+
+ if (strcasecmp(block.hdr->name, "Content-Transfer-Encoding") == 0 &&
+ !block.hdr->continued && !block.hdr->continues &&
+ block.hdr->value_len == 6 &&
+ i_memcasecmp(block.hdr->value, "binary", 6) == 0 &&
+ part_can_convert(block.part) &&
+ bstream->convert_part != block.part) {
+ /* looks like we want to convert this body part to
+ base64, but if we haven't seen Content-Type yet
+ delay the decision until we've read the rest of
+ the header */
+ i_assert(block.part != NULL);
+ bstream->convert_part = block.part;
+ bstream->base64_block_pos = 0;
+ if (!bstream->content_type_seen) {
+ i_assert(bstream->hdr_buf == NULL);
+ bstream->hdr_buf = buffer_create_dynamic(default_pool, 512);
+ stream_add_hdr(bstream, block.hdr);
+ bstream->cte_header_len = bstream->hdr_buf->used;
+ } else {
+ stream_add_data(bstream,
+ "Content-Transfer-Encoding: base64\r\n", 35);
+ }
+ } else if (block.hdr->eoh && bstream->hdr_buf != NULL) {
+ /* finish the decision about decoding */
+ stream_finish_convert_decision(bstream);
+ stream_add_data(bstream, "\r\n", 2);
+ } else {
+ stream_add_hdr(bstream, block.hdr);
+ }
+ } else if (block.size == 0) {
+ /* end of header */
+ if (bstream->hdr_buf != NULL) {
+ /* message has no body */
+ bstream->convert_part = NULL;
+ stream_add_data(bstream, bstream->hdr_buf->data,
+ bstream->hdr_buf->used);
+ buffer_free(&bstream->hdr_buf);
+ }
+ bstream->content_type_seen = FALSE;
+ } else if (block.part == bstream->convert_part) {
+ /* convert body part to base64 */
+ stream_encode_base64(bstream, block.data, block.size);
+ } else {
+ stream_add_data(bstream, block.data, block.size);
+ }
+ new_size = stream->pos - stream->skip;
+ if (new_size == old_size)
+ return i_stream_binary_converter_read(stream);
+ return new_size - old_size;
+}
+
+static void i_stream_binary_converter_close(struct iostream_private *stream,
+ bool close_parent)
+{
+ struct binary_converter_istream *bstream =
+ (struct binary_converter_istream *)stream;
+ struct message_part *parts;
+
+ if (bstream->parser != NULL) {
+ message_parser_deinit(&bstream->parser, &parts);
+ }
+ pool_unref(&bstream->pool);
+ if (close_parent)
+ i_stream_close(bstream->istream.parent);
+}
+
+struct istream *i_stream_create_binary_converter(struct istream *input)
+{
+ const struct message_parser_settings parser_set = {
+ .flags = MESSAGE_PARSER_FLAG_INCLUDE_MULTIPART_BLOCKS |
+ MESSAGE_PARSER_FLAG_INCLUDE_BOUNDARIES,
+ };
+ struct binary_converter_istream *bstream;
+
+ bstream = i_new(struct binary_converter_istream, 1);
+ bstream->istream.max_buffer_size = input->real_stream->max_buffer_size;
+
+ bstream->istream.read = i_stream_binary_converter_read;
+ bstream->istream.iostream.close = i_stream_binary_converter_close;
+
+ bstream->istream.istream.readable_fd = FALSE;
+ bstream->istream.istream.blocking = input->blocking;
+ bstream->istream.istream.seekable = FALSE;
+
+ bstream->pool = pool_alloconly_create("istream binary converter", 128);
+ bstream->parser = message_parser_init(bstream->pool, input, &parser_set);
+ return i_stream_create(&bstream->istream, input,
+ i_stream_get_fd(input), 0);
+}
diff --git a/src/lib-mail/istream-binary-converter.h b/src/lib-mail/istream-binary-converter.h
new file mode 100644
index 0000000..6567e6e
--- /dev/null
+++ b/src/lib-mail/istream-binary-converter.h
@@ -0,0 +1,6 @@
+#ifndef ISTREAM_BINARY_CONVERTER_H
+#define ISTREAM_BINARY_CONVERTER_H
+
+struct istream *i_stream_create_binary_converter(struct istream *input);
+
+#endif
diff --git a/src/lib-mail/istream-dot.c b/src/lib-mail/istream-dot.c
new file mode 100644
index 0000000..9c8f3f7
--- /dev/null
+++ b/src/lib-mail/istream-dot.c
@@ -0,0 +1,236 @@
+/* Copyright (c) 2007-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "istream-private.h"
+#include "istream-dot.h"
+
+struct dot_istream {
+ struct istream_private istream;
+
+ char pending[3]; /* max. \r\n */
+
+ /* how far in string "\r\n.\r" are we */
+ unsigned int state;
+ /* state didn't actually start with \r */
+ bool state_no_cr:1;
+ /* state didn't contain \n either (only at the beginnign of stream) */
+ bool state_no_lf:1;
+ /* we've seen the "." line, keep returning EOF */
+ bool dot_eof:1;
+
+ bool send_last_lf:1;
+};
+
+static int i_stream_dot_read_some(struct dot_istream *dstream)
+{
+ struct istream_private *stream = &dstream->istream;
+ size_t size, avail;
+ ssize_t ret;
+
+ size = i_stream_get_data_size(stream->parent);
+ if (size == 0) {
+ ret = i_stream_read_memarea(stream->parent);
+ if (ret <= 0) {
+ i_assert(ret != -2); /* 0 sized buffer can't be full */
+ if (stream->parent->stream_errno != 0) {
+ stream->istream.stream_errno =
+ stream->parent->stream_errno;
+ } else if (ret < 0 && stream->parent->eof) {
+ /* we didn't see "." line */
+ io_stream_set_error(&stream->iostream,
+ "dot-input stream ends without '.' line");
+ stream->istream.stream_errno = EPIPE;
+ }
+ return ret;
+ }
+ size = i_stream_get_data_size(stream->parent);
+ i_assert(size != 0);
+ }
+
+ if (!i_stream_try_alloc(stream, size, &avail))
+ return -2;
+ return 1;
+}
+
+static bool flush_pending(struct dot_istream *dstream, size_t *destp)
+{
+ struct istream_private *stream = &dstream->istream;
+ size_t dest = *destp;
+ unsigned int i = 0;
+
+ for (; dstream->pending[i] != '\0' && dest < stream->buffer_size; i++)
+ stream->w_buffer[dest++] = dstream->pending[i];
+ memmove(dstream->pending, dstream->pending + i,
+ sizeof(dstream->pending) - i);
+ *destp = dest;
+ return dest < stream->buffer_size;
+}
+
+static bool flush_dot_state(struct dot_istream *dstream, size_t *destp)
+{
+ unsigned int i = 0;
+
+ if (!dstream->state_no_cr)
+ dstream->pending[i++] = '\r';
+ if (dstream->state_no_lf)
+ dstream->state_no_lf = FALSE;
+ else if (dstream->state > 1)
+ dstream->pending[i++] = '\n';
+ dstream->pending[i] = '\0';
+
+ if (dstream->state != 4)
+ dstream->state = 0;
+ else {
+ /* \r\n.\r seen, go back to \r state */
+ dstream->state = 1;
+ }
+ return flush_pending(dstream, destp);
+}
+
+static void i_stream_dot_eof(struct dot_istream *dstream, size_t *destp)
+{
+ if (dstream->send_last_lf) {
+ dstream->state = 2;
+ (void)flush_dot_state(dstream, destp);
+ }
+ dstream->dot_eof = TRUE;
+}
+
+static ssize_t
+i_stream_dot_return(struct istream_private *stream, size_t dest, ssize_t ret)
+{
+ if (dest != stream->pos) {
+ i_assert(dest > stream->pos);
+ ret = dest - stream->pos;
+ stream->pos = dest;
+ }
+ return ret;
+}
+
+static ssize_t i_stream_dot_read(struct istream_private *stream)
+{
+ /* @UNSAFE */
+ struct dot_istream *dstream = (struct dot_istream *)stream;
+ const unsigned char *data;
+ size_t i, dest, size, avail;
+ ssize_t ret, ret1;
+
+ if (dstream->pending[0] != '\0') {
+ if (!i_stream_try_alloc(stream, 1, &avail))
+ return -2;
+ dest = stream->pos;
+ (void)flush_pending(dstream, &dest);
+ } else {
+ dest = stream->pos;
+ }
+
+ if (dstream->dot_eof) {
+ stream->istream.eof = TRUE;
+ return i_stream_dot_return(stream, dest, -1);
+ }
+
+ /* we have to update stream->pos before reading more data */
+ ret1 = i_stream_dot_return(stream, dest, 0);
+ if ((ret = i_stream_dot_read_some(dstream)) <= 0) {
+ if (stream->istream.stream_errno != 0)
+ return -1;
+ if (ret1 != 0)
+ return ret1;
+ dest = stream->pos;
+ if (ret == -1 && dstream->state != 0)
+ (void)flush_dot_state(dstream, &dest);
+ return i_stream_dot_return(stream, dest, ret);
+ }
+ dest = stream->pos;
+
+ data = i_stream_get_data(stream->parent, &size);
+ for (i = 0; i < size && dest < stream->buffer_size; i++) {
+ switch (dstream->state) {
+ case 0:
+ break;
+ case 1:
+ /* CR seen */
+ if (data[i] == '\n')
+ dstream->state++;
+ else {
+ if (!flush_dot_state(dstream, &dest))
+ goto end;
+ }
+ break;
+ case 2:
+ /* [CR]LF seen */
+ if (data[i] == '.')
+ dstream->state++;
+ else {
+ if (!flush_dot_state(dstream, &dest))
+ goto end;
+ }
+ break;
+ case 3:
+ /* [CR]LF. seen */
+ if (data[i] == '\r')
+ dstream->state++;
+ else if (data[i] == '\n') {
+ /* EOF */
+ i_stream_dot_eof(dstream, &dest);
+ i++;
+ goto end;
+ } else {
+ /* drop the initial dot */
+ if (!flush_dot_state(dstream, &dest))
+ goto end;
+ }
+ break;
+ case 4:
+ /* [CR]LF.CR seen */
+ if (data[i] == '\n') {
+ /* EOF */
+ i_stream_dot_eof(dstream, &dest);
+ i++;
+ goto end;
+ } else {
+ /* drop the initial dot */
+ if (!flush_dot_state(dstream, &dest))
+ goto end;
+ }
+ }
+ if (dstream->state == 0) {
+ if (data[i] == '\r') {
+ dstream->state = 1;
+ dstream->state_no_cr = FALSE;
+ } else if (data[i] == '\n') {
+ dstream->state = 2;
+ dstream->state_no_cr = TRUE;
+ } else {
+ stream->w_buffer[dest++] = data[i];
+ }
+ }
+ }
+end:
+ i_stream_skip(stream->parent, i);
+
+ ret = i_stream_dot_return(stream, dest, 0) + ret1;
+ if (ret == 0)
+ return i_stream_dot_read(stream);
+ i_assert(ret > 0);
+ return ret;
+}
+
+struct istream *i_stream_create_dot(struct istream *input, bool send_last_lf)
+{
+ struct dot_istream *dstream;
+
+ dstream = i_new(struct dot_istream, 1);
+ dstream->istream.max_buffer_size = input->real_stream->max_buffer_size;
+ dstream->istream.read = i_stream_dot_read;
+
+ dstream->istream.istream.readable_fd = FALSE;
+ dstream->istream.istream.blocking = input->blocking;
+ dstream->istream.istream.seekable = FALSE;
+ dstream->send_last_lf = send_last_lf;
+ dstream->state = 2;
+ dstream->state_no_cr = TRUE;
+ dstream->state_no_lf = TRUE;
+ return i_stream_create(&dstream->istream, input,
+ i_stream_get_fd(input), 0);
+}
diff --git a/src/lib-mail/istream-dot.h b/src/lib-mail/istream-dot.h
new file mode 100644
index 0000000..129bc70
--- /dev/null
+++ b/src/lib-mail/istream-dot.h
@@ -0,0 +1,9 @@
+#ifndef ISTREAM_DOT_H
+#define ISTREAM_DOT_H
+
+/* Create input stream for reading SMTP DATA style message: Drop initial "."
+ from lines beginning with it. Return EOF on line that contains only ".".
+ If send_last_lf=FALSE, the trailing [CR]LF before "." line isn't returned. */
+struct istream *i_stream_create_dot(struct istream *input, bool send_last_lf);
+
+#endif
diff --git a/src/lib-mail/istream-header-filter.c b/src/lib-mail/istream-header-filter.c
new file mode 100644
index 0000000..1783dbd
--- /dev/null
+++ b/src/lib-mail/istream-header-filter.c
@@ -0,0 +1,762 @@
+/* Copyright (c) 2003-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "memarea.h"
+#include "sort.h"
+#include "message-parser.h"
+#include "istream-private.h"
+#include "istream-header-filter.h"
+
+struct header_filter_istream_snapshot {
+ struct istream_snapshot snapshot;
+ struct header_filter_istream *mstream;
+ buffer_t *hdr_buf;
+};
+
+struct header_filter_istream {
+ struct istream_private istream;
+ pool_t pool;
+
+ struct message_header_parser_ctx *hdr_ctx;
+
+ const char **headers;
+ unsigned int headers_count;
+
+ header_filter_callback *callback;
+ void *context;
+
+ buffer_t *hdr_buf;
+ struct message_size header_size;
+ uoff_t skip_count;
+ uoff_t last_lf_offset;
+
+ unsigned int cur_line, parsed_lines;
+ ARRAY(unsigned int) match_change_lines;
+
+ bool header_read:1;
+ bool seen_eoh:1;
+ bool header_parsed:1;
+ bool headers_edited:1;
+ bool exclude:1;
+ bool crlf:1;
+ bool crlf_preserve:1;
+ bool hide_body:1;
+ bool add_missing_eoh:1;
+ bool end_body_with_lf:1;
+ bool last_lf_added:1;
+ bool last_orig_crlf:1;
+ bool last_added_newline:1;
+ bool eoh_not_matched:1;
+ bool callbacks_called:1;
+ bool prev_matched:1;
+ bool snapshot_pending:1;
+};
+
+header_filter_callback *null_header_filter_callback = NULL;
+
+static ssize_t i_stream_header_filter_read(struct istream_private *stream);
+
+static void i_stream_header_filter_destroy(struct iostream_private *stream)
+{
+ struct header_filter_istream *mstream =
+ (struct header_filter_istream *)stream;
+
+ if (mstream->hdr_ctx != NULL)
+ message_parse_header_deinit(&mstream->hdr_ctx);
+ if (array_is_created(&mstream->match_change_lines))
+ array_free(&mstream->match_change_lines);
+ if (!mstream->snapshot_pending)
+ buffer_free(&mstream->hdr_buf);
+ else {
+ /* Clear hdr_buf to make sure
+ i_stream_header_filter_snapshot_free() frees it. */
+ mstream->hdr_buf = NULL;
+ }
+ pool_unref(&mstream->pool);
+}
+
+static ssize_t
+read_mixed(struct header_filter_istream *mstream, size_t body_highwater_size)
+{
+ const unsigned char *data;
+ size_t pos;
+ ssize_t ret;
+
+ if (mstream->hide_body) {
+ mstream->istream.istream.eof = TRUE;
+ return -1;
+ }
+
+ data = i_stream_get_data(mstream->istream.parent, &pos);
+ if (pos <= body_highwater_size) {
+ i_assert(pos == body_highwater_size ||
+ (mstream->end_body_with_lf &&
+ pos+1 == body_highwater_size));
+
+ ret = i_stream_read_memarea(mstream->istream.parent);
+ mstream->istream.istream.stream_errno =
+ mstream->istream.parent->stream_errno;
+ mstream->istream.istream.eof = mstream->istream.parent->eof;
+
+ if (ret <= 0) {
+ data = mstream->hdr_buf->data;
+ pos = mstream->hdr_buf->used;
+ i_assert(pos > 0);
+
+ if (mstream->end_body_with_lf && data[pos-1] != '\n' &&
+ ret == -1 && mstream->istream.istream.eof) {
+ /* add missing trailing LF to body */
+ if (mstream->crlf)
+ buffer_append_c(mstream->hdr_buf, '\r');
+ buffer_append_c(mstream->hdr_buf, '\n');
+ mstream->istream.buffer = mstream->hdr_buf->data;
+ mstream->istream.pos = mstream->hdr_buf->used;
+ return mstream->hdr_buf->used - pos;
+ }
+ return ret;
+ }
+
+ data = i_stream_get_data(mstream->istream.parent, &pos);
+ }
+ buffer_append(mstream->hdr_buf, data + body_highwater_size,
+ pos - body_highwater_size);
+
+ mstream->istream.buffer = buffer_get_data(mstream->hdr_buf, &pos);
+ ret = (ssize_t)(pos - mstream->istream.pos - mstream->istream.skip);
+ i_assert(ret > 0);
+ mstream->istream.pos = pos;
+ return ret;
+}
+
+static int cmp_uint(const unsigned int *i1, const unsigned int *i2)
+{
+ return *i1 < *i2 ? -1 :
+ (*i1 > *i2 ? 1 : 0);
+}
+
+static bool match_line_changed(struct header_filter_istream *mstream)
+{
+ if (!array_is_created(&mstream->match_change_lines))
+ return FALSE;
+
+ return array_bsearch(&mstream->match_change_lines, &mstream->cur_line,
+ cmp_uint) != NULL;
+}
+
+static void add_eol(struct header_filter_istream *mstream, bool orig_crlf)
+{
+ if (mstream->crlf || (orig_crlf && mstream->crlf_preserve))
+ buffer_append(mstream->hdr_buf, "\r\n", 2);
+ else
+ buffer_append_c(mstream->hdr_buf, '\n');
+ mstream->last_orig_crlf = orig_crlf;
+ mstream->last_added_newline = TRUE;
+}
+
+static ssize_t hdr_stream_update_pos(struct header_filter_istream *mstream)
+{
+ ssize_t ret;
+ size_t pos;
+
+ mstream->istream.buffer = buffer_get_data(mstream->hdr_buf, &pos);
+ ret = (ssize_t)(pos - mstream->istream.pos - mstream->istream.skip);
+ i_assert(ret >= 0);
+ mstream->istream.pos = pos;
+ return ret;
+}
+
+static void hdr_buf_realloc_if_needed(struct header_filter_istream *mstream)
+{
+ if (!mstream->snapshot_pending)
+ return;
+
+ /* hdr_buf exists in a snapshot. Leave it be and create a copy of it
+ that we modify. */
+ buffer_t *old_buf = mstream->hdr_buf;
+ mstream->hdr_buf = buffer_create_dynamic(default_pool,
+ I_MAX(1024, old_buf->used));
+ buffer_append(mstream->hdr_buf, old_buf->data, old_buf->used);
+ mstream->snapshot_pending = FALSE;
+
+ mstream->istream.buffer = mstream->hdr_buf->data;
+}
+
+static ssize_t read_header(struct header_filter_istream *mstream)
+{
+ struct message_header_line *hdr;
+ uoff_t highwater_offset;
+ size_t max_buffer_size;
+ ssize_t ret, ret2;
+ int hdr_ret;
+
+ if (mstream->hdr_ctx == NULL) {
+ mstream->hdr_ctx =
+ message_parse_header_init(mstream->istream.parent,
+ NULL, 0);
+ }
+
+ /* remove skipped data from hdr_buf */
+ hdr_buf_realloc_if_needed(mstream);
+ buffer_copy(mstream->hdr_buf, 0,
+ mstream->hdr_buf, mstream->istream.skip, SIZE_MAX);
+
+ mstream->istream.pos -= mstream->istream.skip;
+ mstream->istream.skip = 0;
+ buffer_set_used_size(mstream->hdr_buf, mstream->istream.pos);
+
+ if (mstream->header_read) {
+ i_assert(mstream->istream.skip == 0);
+ highwater_offset = mstream->istream.istream.v_offset +
+ mstream->istream.pos;
+ if (highwater_offset >= mstream->header_size.virtual_size) {
+ /* we want to return mixed headers and body */
+ size_t body_highwater_size = highwater_offset -
+ mstream->header_size.virtual_size;
+ return read_mixed(mstream, body_highwater_size);
+ }
+ }
+
+ max_buffer_size = i_stream_get_max_buffer_size(&mstream->istream.istream);
+ if (mstream->hdr_buf->used >= max_buffer_size) {
+ i_assert(max_buffer_size > 0);
+ return -2;
+ }
+
+ while ((hdr_ret = message_parse_header_next(mstream->hdr_ctx,
+ &hdr)) > 0) {
+ bool matched;
+
+ if (!hdr->continued)
+ mstream->cur_line++;
+ if (hdr->eoh) {
+ mstream->seen_eoh = TRUE;
+ matched = FALSE;
+ if (mstream->header_parsed && !mstream->headers_edited) {
+ if (mstream->eoh_not_matched)
+ matched = !matched;
+ } else if (mstream->callback != NULL) {
+ mstream->callback(mstream, hdr, &matched,
+ mstream->context);
+ mstream->callbacks_called = TRUE;
+ }
+
+ if (matched) {
+ mstream->eoh_not_matched = TRUE;
+ continue;
+ }
+
+ add_eol(mstream, hdr->crlf_newline);
+ continue;
+ }
+
+ if (hdr->continued) {
+ /* Header line continued - use only the first line's
+ matched-result. Otherwise multiline headers might
+ end up being only partially picked, which wouldn't
+ be very good. However, allow callbacks to modify
+ the headers in any way they want. */
+ matched = mstream->prev_matched;
+ } else if (mstream->headers_count == 0) {
+ /* no include/exclude headers - default matching */
+ matched = FALSE;
+ } else {
+ matched = i_bsearch(hdr->name, mstream->headers,
+ mstream->headers_count,
+ sizeof(*mstream->headers),
+ bsearch_strcasecmp) != NULL;
+ }
+ if (mstream->callback == NULL) {
+ /* nothing gets excluded */
+ } else if (!mstream->header_parsed || mstream->headers_edited) {
+ /* first time in this line or we have actually modified
+ the header so we always want to call the callbacks */
+ bool orig_matched = matched;
+
+ mstream->parsed_lines = mstream->cur_line;
+ mstream->callback(mstream, hdr, &matched,
+ mstream->context);
+ mstream->callbacks_called = TRUE;
+ if (matched != orig_matched &&
+ !hdr->continued && !mstream->headers_edited) {
+ if (!array_is_created(&mstream->match_change_lines))
+ i_array_init(&mstream->match_change_lines, 8);
+ array_push_back(&mstream->match_change_lines,
+ &mstream->cur_line);
+ }
+ } else if (!hdr->continued) {
+ /* second time in this line. was it excluded by the
+ callback the first time? */
+ if (match_line_changed(mstream))
+ matched = !matched;
+ }
+ mstream->prev_matched = matched;
+
+ if (matched == mstream->exclude) {
+ /* ignore */
+ } else {
+ if (!hdr->continued) {
+ buffer_append(mstream->hdr_buf,
+ hdr->name, hdr->name_len);
+ buffer_append(mstream->hdr_buf,
+ hdr->middle, hdr->middle_len);
+ }
+ buffer_append(mstream->hdr_buf,
+ hdr->value, hdr->value_len);
+ if (!hdr->no_newline)
+ add_eol(mstream, hdr->crlf_newline);
+
+ if (mstream->skip_count >= mstream->hdr_buf->used) {
+ /* we need more */
+ mstream->skip_count -= mstream->hdr_buf->used;
+ buffer_set_used_size(mstream->hdr_buf, 0);
+ } else {
+ if (mstream->skip_count > 0) {
+ mstream->istream.skip =
+ mstream->skip_count;
+ mstream->skip_count = 0;
+ }
+ break;
+ }
+ }
+ if (mstream->hdr_buf->used >= max_buffer_size)
+ break;
+ }
+ if (mstream->hdr_buf->used > 0) {
+ const unsigned char *data = mstream->hdr_buf->data;
+ mstream->last_added_newline =
+ data[mstream->hdr_buf->used-1] == '\n';
+ }
+
+ if (hdr_ret < 0) {
+ if (mstream->istream.parent->stream_errno != 0) {
+ mstream->istream.istream.stream_errno =
+ mstream->istream.parent->stream_errno;
+ mstream->istream.istream.eof =
+ mstream->istream.parent->eof;
+ return -1;
+ }
+ if (!mstream->seen_eoh && mstream->add_missing_eoh) {
+ bool matched = FALSE;
+
+ mstream->seen_eoh = TRUE;
+
+ if (!mstream->last_added_newline)
+ add_eol(mstream, mstream->last_orig_crlf);
+
+ if (mstream->header_parsed && !mstream->headers_edited) {
+ if (mstream->eoh_not_matched)
+ matched = !matched;
+ } else if (mstream->callback != NULL) {
+ struct message_header_line fake_eoh_hdr = {
+ .eoh = TRUE,
+ .name = "",
+ };
+ mstream->callback(mstream, &fake_eoh_hdr,
+ &matched, mstream->context);
+ mstream->callbacks_called = TRUE;
+ }
+
+ if (matched) {
+ mstream->seen_eoh = FALSE;
+ } else {
+ add_eol(mstream, mstream->last_orig_crlf);
+ }
+ }
+ }
+
+ /* don't copy eof here because we're only returning headers here.
+ the body will be returned in separate read() call. */
+ ret = hdr_stream_update_pos(mstream);
+
+ if (hdr_ret == 0) {
+ /* need more data to finish parsing headers. we may have some
+ data already available though. */
+ return ret;
+ }
+
+ if (hdr == NULL) {
+ /* finished */
+ message_parse_header_deinit(&mstream->hdr_ctx);
+ mstream->hdr_ctx = NULL;
+
+ if ((!mstream->header_parsed || mstream->headers_edited ||
+ mstream->callbacks_called) &&
+ mstream->callback != NULL) {
+ bool matched = FALSE;
+ mstream->callback(mstream, NULL,
+ &matched, mstream->context);
+ /* check if the callback added more headers.
+ this is allowed only if EOH wasn't added yet. */
+ ret2 = hdr_stream_update_pos(mstream);
+ if (!mstream->seen_eoh)
+ ret += ret2;
+ else {
+ i_assert(ret2 == 0);
+ }
+ }
+ mstream->header_parsed = TRUE;
+ mstream->header_read = TRUE;
+ mstream->callbacks_called = FALSE;
+
+ mstream->header_size.physical_size =
+ mstream->istream.parent->v_offset;
+ mstream->header_size.virtual_size =
+ mstream->istream.istream.v_offset +
+ mstream->istream.pos;
+ }
+
+ if (ret == 0) {
+ /* we're at the end of headers. */
+ i_assert(hdr == NULL);
+ i_assert(mstream->istream.istream.v_offset +
+ mstream->istream.pos ==
+ mstream->header_size.virtual_size);
+
+ return i_stream_header_filter_read(&mstream->istream);
+ }
+
+ return ret;
+}
+
+static ssize_t
+handle_end_body_with_lf(struct header_filter_istream *mstream, ssize_t ret)
+{
+ struct istream_private *stream = &mstream->istream;
+ const unsigned char *data;
+ size_t size;
+ uoff_t last_offset;
+ bool last_lf;
+
+ data = i_stream_get_data(stream->parent, &size);
+ if (stream->parent->v_offset + size == 0 && size == 0)
+ last_offset = UOFF_T_MAX;
+ else
+ last_offset = stream->parent->v_offset + size - 1;
+
+ if (mstream->last_lf_offset == last_offset)
+ last_lf = TRUE;
+ else if (size > 0)
+ last_lf = data[size-1] == '\n';
+ else
+ last_lf = FALSE;
+
+ if (ret == -1 && stream->parent->eof && !last_lf) {
+ /* missing LF, need to add it */
+ i_assert(!mstream->last_lf_added);
+ i_assert(size == 0 || data[size-1] != '\n');
+
+ hdr_buf_realloc_if_needed(mstream);
+ buffer_set_used_size(mstream->hdr_buf, 0);
+ buffer_append(mstream->hdr_buf, data, size);
+ if (mstream->crlf)
+ buffer_append_c(mstream->hdr_buf, '\r');
+ buffer_append_c(mstream->hdr_buf, '\n');
+ mstream->last_lf_offset = last_offset;
+ mstream->last_lf_added = TRUE;
+
+ stream->skip = 0;
+ stream->pos = mstream->hdr_buf->used;
+ stream->buffer = mstream->hdr_buf->data;
+ return mstream->crlf ? 2 : 1;
+ } else {
+ mstream->last_lf_offset = last_lf ? last_offset : UOFF_T_MAX;
+ }
+ return ret;
+}
+
+static ssize_t i_stream_header_filter_read(struct istream_private *stream)
+{
+ struct header_filter_istream *mstream =
+ (struct header_filter_istream *)stream;
+ uoff_t v_offset;
+ ssize_t ret;
+
+ if (mstream->last_lf_added) {
+ stream->istream.eof = TRUE;
+ return -1;
+ }
+
+ if (!mstream->header_read ||
+ stream->istream.v_offset < mstream->header_size.virtual_size)
+ return read_header(mstream);
+
+ if (mstream->hide_body) {
+ stream->istream.eof = TRUE;
+ return -1;
+ }
+
+ v_offset = stream->parent_start_offset + stream->istream.v_offset -
+ mstream->header_size.virtual_size +
+ mstream->header_size.physical_size;
+ i_stream_seek(stream->parent, v_offset);
+ ret = i_stream_read_copy_from_parent(&stream->istream);
+ if (mstream->end_body_with_lf)
+ ret = handle_end_body_with_lf(mstream, ret);
+ return ret;
+}
+
+static void
+i_stream_header_filter_seek_to_header(struct header_filter_istream *mstream,
+ uoff_t v_offset)
+{
+ i_stream_seek(mstream->istream.parent,
+ mstream->istream.parent_start_offset);
+ mstream->istream.parent_expected_offset =
+ mstream->istream.parent_start_offset;
+ mstream->istream.access_counter =
+ mstream->istream.parent->real_stream->access_counter;
+
+ if (mstream->hdr_ctx != NULL)
+ message_parse_header_deinit(&mstream->hdr_ctx);
+ mstream->skip_count = v_offset;
+ mstream->cur_line = 0;
+ mstream->prev_matched = FALSE;
+ mstream->header_read = FALSE;
+ mstream->seen_eoh = FALSE;
+ mstream->last_added_newline = TRUE;
+}
+
+static int skip_header(struct header_filter_istream *mstream)
+{
+ size_t pos;
+
+ if (mstream->header_read)
+ return 0;
+
+ if (mstream->istream.access_counter !=
+ mstream->istream.parent->real_stream->access_counter) {
+ /* need to re-parse headers */
+ i_stream_header_filter_seek_to_header(mstream, 0);
+ }
+
+ while (!mstream->header_read &&
+ i_stream_read_memarea(&mstream->istream.istream) != -1) {
+ pos = i_stream_get_data_size(&mstream->istream.istream);
+ i_stream_skip(&mstream->istream.istream, pos);
+ }
+ return mstream->istream.istream.stream_errno != 0 ? -1 : 0;
+}
+
+static void
+stream_reset_to(struct header_filter_istream *mstream, uoff_t v_offset)
+{
+ hdr_buf_realloc_if_needed(mstream);
+ mstream->istream.istream.v_offset = v_offset;
+ mstream->istream.skip = mstream->istream.pos = 0;
+ mstream->istream.buffer = NULL;
+ buffer_set_used_size(mstream->hdr_buf, 0);
+}
+
+static void i_stream_header_filter_seek(struct istream_private *stream,
+ uoff_t v_offset, bool mark ATTR_UNUSED)
+{
+ struct header_filter_istream *mstream =
+ (struct header_filter_istream *)stream;
+
+ if (stream->istream.v_offset == v_offset) {
+ /* just reset the input buffer */
+ stream_reset_to(mstream, v_offset);
+ i_stream_seek(mstream->istream.parent,
+ mstream->istream.parent_expected_offset);
+ return;
+ }
+ /* if last_lf_added=TRUE, we're currently at EOF. So reset it only if
+ we're seeking backwards, otherwise we would just add a duplicate */
+ mstream->last_lf_added = FALSE;
+
+ if (v_offset == 0) {
+ /* seeking to beginning of headers. */
+ stream_reset_to(mstream, 0);
+ i_stream_header_filter_seek_to_header(mstream, 0);
+ return;
+ }
+
+ /* if we haven't parsed the whole header yet, we don't know if we
+ want to seek inside header or body. so make sure we've parsed the
+ header. */
+ if (skip_header(mstream) < 0)
+ return;
+ stream_reset_to(mstream, v_offset);
+
+ if (v_offset < mstream->header_size.virtual_size) {
+ /* seek into headers. we'll have to re-parse them, use
+ skip_count to set the wanted position */
+ i_stream_header_filter_seek_to_header(mstream, v_offset);
+ } else {
+ /* body */
+ v_offset -= mstream->header_size.virtual_size;
+ v_offset += mstream->header_size.physical_size;
+ i_stream_seek(stream->parent,
+ stream->parent_start_offset + v_offset);
+ }
+}
+
+static void ATTR_NORETURN
+i_stream_header_filter_sync(struct istream_private *stream ATTR_UNUSED)
+{
+ i_panic("istream-header-filter sync() not implemented");
+}
+
+static int
+i_stream_header_filter_stat(struct istream_private *stream, bool exact)
+{
+ struct header_filter_istream *mstream =
+ (struct header_filter_istream *)stream;
+ const struct stat *st;
+ uoff_t old_offset;
+
+ if (i_stream_stat(stream->parent, exact, &st) < 0) {
+ stream->istream.stream_errno = stream->parent->stream_errno;
+ return -1;
+ }
+ stream->statbuf = *st;
+ if (stream->statbuf.st_size == -1 || !exact)
+ return 0;
+
+ /* fix the filtered header size */
+ old_offset = stream->istream.v_offset;
+ if (skip_header(mstream) < 0)
+ return -1;
+
+ if (mstream->hide_body) {
+ /* no body */
+ stream->statbuf.st_size = mstream->header_size.physical_size;
+ } else if (!mstream->end_body_with_lf) {
+ /* no last-LF */
+ } else if (mstream->last_lf_added) {
+ /* yes, we have added LF */
+ stream->statbuf.st_size += mstream->crlf ? 2 : 1;
+ } else if (mstream->last_lf_offset != UOFF_T_MAX) {
+ /* no, we didn't need to add LF */
+ } else {
+ /* check if we need to add LF */
+ i_stream_seek(stream->parent, st->st_size - 1);
+ (void)i_stream_read_memarea(stream->parent);
+ if (stream->parent->stream_errno != 0) {
+ stream->istream.stream_errno =
+ stream->parent->stream_errno;
+ return -1;
+ }
+ i_assert(stream->parent->eof);
+ ssize_t ret = handle_end_body_with_lf(mstream, -1);
+ if (ret > 0)
+ stream->statbuf.st_size += ret;
+ }
+
+ stream->statbuf.st_size -=
+ (off_t)mstream->header_size.physical_size -
+ (off_t)mstream->header_size.virtual_size;
+ i_stream_seek(&stream->istream, old_offset);
+ return 0;
+}
+
+static void
+i_stream_header_filter_snapshot_free(struct istream_snapshot *_snapshot)
+{
+ struct header_filter_istream_snapshot *snapshot =
+ container_of(_snapshot, struct header_filter_istream_snapshot, snapshot);
+
+ if (snapshot->mstream->hdr_buf != snapshot->hdr_buf)
+ buffer_free(&snapshot->hdr_buf);
+ else {
+ i_assert(snapshot->mstream->snapshot_pending);
+ snapshot->mstream->snapshot_pending = FALSE;
+ }
+ i_free(snapshot);
+}
+
+static struct istream_snapshot *
+i_stream_header_filter_snapshot(struct istream_private *stream,
+ struct istream_snapshot *prev_snapshot)
+{
+ struct header_filter_istream *mstream =
+ (struct header_filter_istream *)stream;
+ struct header_filter_istream_snapshot *snapshot;
+
+ if (stream->buffer != mstream->hdr_buf->data) {
+ /* reading body */
+ return i_stream_default_snapshot(stream, prev_snapshot);
+ }
+
+ /* snapshot the header buffer */
+ snapshot = i_new(struct header_filter_istream_snapshot, 1);
+ snapshot->mstream = mstream;
+ snapshot->hdr_buf = mstream->hdr_buf;
+ snapshot->snapshot.free = i_stream_header_filter_snapshot_free;
+ snapshot->snapshot.prev_snapshot = prev_snapshot;
+ mstream->snapshot_pending = TRUE;
+ return &snapshot->snapshot;
+}
+
+#undef i_stream_create_header_filter
+struct istream *
+i_stream_create_header_filter(struct istream *input,
+ enum header_filter_flags flags,
+ const char *const *headers,
+ unsigned int headers_count,
+ header_filter_callback *callback, void *context)
+{
+ struct header_filter_istream *mstream;
+ unsigned int i, j;
+ int ret;
+
+ i_assert((flags & (HEADER_FILTER_INCLUDE|HEADER_FILTER_EXCLUDE)) != 0);
+
+ mstream = i_new(struct header_filter_istream, 1);
+ mstream->pool = pool_alloconly_create(MEMPOOL_GROWING
+ "header filter stream", 256);
+ mstream->istream.max_buffer_size = input->real_stream->max_buffer_size;
+
+ mstream->headers = headers_count == 0 ? NULL :
+ p_new(mstream->pool, const char *, headers_count);
+ for (i = j = 0; i < headers_count; i++) {
+ ret = j == 0 ? -1 :
+ strcasecmp(mstream->headers[j-1], headers[i]);
+ if (ret == 0) {
+ /* drop duplicate */
+ continue;
+ }
+ i_assert(ret < 0);
+ mstream->headers[j++] = p_strdup(mstream->pool, headers[i]);
+ }
+ mstream->headers_count = j;
+ mstream->hdr_buf = buffer_create_dynamic(default_pool, 1024);
+
+ mstream->callback = callback;
+ mstream->context = context;
+ mstream->exclude = (flags & HEADER_FILTER_EXCLUDE) != 0;
+ if ((flags & HEADER_FILTER_CRLF_PRESERVE) != 0)
+ mstream->crlf_preserve = TRUE;
+ else if ((flags & HEADER_FILTER_NO_CR) != 0)
+ mstream->crlf = FALSE;
+ else
+ mstream->crlf = TRUE;
+ mstream->hide_body = (flags & HEADER_FILTER_HIDE_BODY) != 0;
+ mstream->add_missing_eoh = (flags & HEADER_FILTER_ADD_MISSING_EOH) != 0;
+ mstream->end_body_with_lf =
+ (flags & HEADER_FILTER_END_BODY_WITH_LF) != 0;
+ mstream->last_lf_offset = UOFF_T_MAX;
+ mstream->last_added_newline = TRUE;
+
+ mstream->istream.iostream.destroy = i_stream_header_filter_destroy;
+ mstream->istream.read = i_stream_header_filter_read;
+ mstream->istream.seek = i_stream_header_filter_seek;
+ mstream->istream.sync = i_stream_header_filter_sync;
+ mstream->istream.stat = i_stream_header_filter_stat;
+ mstream->istream.snapshot = i_stream_header_filter_snapshot;
+
+ mstream->istream.istream.readable_fd = FALSE;
+ mstream->istream.istream.blocking = input->blocking;
+ mstream->istream.istream.seekable = input->seekable;
+
+ return i_stream_create(&mstream->istream, input, -1, 0);
+}
+
+void i_stream_header_filter_add(struct header_filter_istream *input,
+ const void *data, size_t size)
+{
+ hdr_buf_realloc_if_needed(input);
+ buffer_append(input->hdr_buf, data, size);
+ input->headers_edited = TRUE;
+}
diff --git a/src/lib-mail/istream-header-filter.h b/src/lib-mail/istream-header-filter.h
new file mode 100644
index 0000000..7c5ca36
--- /dev/null
+++ b/src/lib-mail/istream-header-filter.h
@@ -0,0 +1,52 @@
+#ifndef ISTREAM_HEADER_FILTER_H
+#define ISTREAM_HEADER_FILTER_H
+
+struct header_filter_istream;
+
+enum header_filter_flags {
+ /* Include only specified headers in output.*/
+ HEADER_FILTER_INCLUDE = 0x01,
+ /* Exclude specified headers from output. */
+ HEADER_FILTER_EXCLUDE = 0x02,
+
+ /* Use LF linefeeds instead of CRLF. */
+ HEADER_FILTER_NO_CR = 0x04,
+ /* Return EOF at the beginning of message body. */
+ HEADER_FILTER_HIDE_BODY = 0x08,
+ /* If the empty "end of headers" line doesn't exist, add it. */
+ HEADER_FILTER_ADD_MISSING_EOH = 0x10,
+ /* If body doesn't end with [CR]LF, add it/them. */
+ HEADER_FILTER_END_BODY_WITH_LF = 0x20,
+ /* Preserve the original LF or CRLF. */
+ HEADER_FILTER_CRLF_PRESERVE = 0x40
+};
+
+struct message_header_line;
+
+typedef void header_filter_callback(struct header_filter_istream *input,
+ struct message_header_line *hdr,
+ bool *matched, void *context);
+
+extern header_filter_callback *null_header_filter_callback;
+
+/* NOTE: headers list must be sorted. */
+struct istream *
+i_stream_create_header_filter(struct istream *input,
+ enum header_filter_flags flags,
+ const char *const *headers,
+ unsigned int headers_count,
+ header_filter_callback *callback, void *context)
+ ATTR_NULL(6);
+#define i_stream_create_header_filter(input, flags, headers, headers_count, \
+ callback, context) \
+ i_stream_create_header_filter(input, flags, headers, headers_count - \
+ CALLBACK_TYPECHECK(callback, void (*)( \
+ struct header_filter_istream *, \
+ struct message_header_line *, bool *, typeof(context))), \
+ (header_filter_callback *)callback, context)
+
+/* Add more data to headers. Should called from the filter callback. */
+void i_stream_header_filter_add(struct header_filter_istream *input,
+ const void *data, size_t size);
+
+#endif
diff --git a/src/lib-mail/istream-nonuls.c b/src/lib-mail/istream-nonuls.c
new file mode 100644
index 0000000..28f33b9
--- /dev/null
+++ b/src/lib-mail/istream-nonuls.c
@@ -0,0 +1,79 @@
+/* Copyright (c) 2007-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "istream-private.h"
+#include "istream-nonuls.h"
+
+struct nonuls_istream {
+ struct istream_private istream;
+ char replace_chr;
+};
+
+static int i_stream_read_parent(struct istream_private *stream)
+{
+ ssize_t ret;
+
+ if (i_stream_get_data_size(stream->parent) > 0)
+ return 1;
+
+ ret = i_stream_read_memarea(stream->parent);
+ if (ret <= 0) {
+ stream->istream.stream_errno = stream->parent->stream_errno;
+ stream->istream.eof = stream->parent->eof;
+ return ret;
+ }
+ i_assert(i_stream_get_data_size(stream->parent) != 0);
+ return 1;
+}
+
+static ssize_t i_stream_nonuls_read(struct istream_private *stream)
+{
+ struct nonuls_istream *nstream = (struct nonuls_istream *)stream;
+ const unsigned char *data, *p;
+ size_t i, size, avail_size;
+ int ret;
+
+ if ((ret = i_stream_read_parent(stream)) <= 0)
+ return ret;
+
+ data = i_stream_get_data(stream->parent, &size);
+ if (!i_stream_try_alloc(stream, size, &avail_size))
+ return -2;
+ if (size > avail_size)
+ size = avail_size;
+ i_assert(size > 0);
+
+ p = memchr(data, '\0', size);
+ if (p == NULL) {
+ /* no NULs in this block */
+ memcpy(stream->w_buffer+stream->pos, data, size);
+ } else {
+ i = p-data;
+ memcpy(stream->w_buffer+stream->pos, data, i);
+ for (; i < size; i++) {
+ stream->w_buffer[stream->pos+i] = data[i] == '\0' ?
+ nstream->replace_chr : data[i];
+ }
+ }
+ stream->pos += size;
+ i_stream_skip(stream->parent, size);
+ return size;
+}
+
+struct istream *i_stream_create_nonuls(struct istream *input, char replace_chr)
+{
+ struct nonuls_istream *nstream;
+
+ nstream = i_new(struct nonuls_istream, 1);
+ nstream->istream.max_buffer_size = input->real_stream->max_buffer_size;
+ nstream->istream.stream_size_passthrough = TRUE;
+
+ nstream->istream.read = i_stream_nonuls_read;
+
+ nstream->istream.istream.readable_fd = FALSE;
+ nstream->istream.istream.blocking = input->blocking;
+ nstream->istream.istream.seekable = FALSE;
+ nstream->replace_chr = replace_chr;
+ return i_stream_create(&nstream->istream, input,
+ i_stream_get_fd(input), 0);
+}
diff --git a/src/lib-mail/istream-nonuls.h b/src/lib-mail/istream-nonuls.h
new file mode 100644
index 0000000..73dd4c5
--- /dev/null
+++ b/src/lib-mail/istream-nonuls.h
@@ -0,0 +1,7 @@
+#ifndef ISTREAM_NONULS_H
+#define ISTREAM_NONULS_H
+
+/* Translate all NUL characters to the specified replace_chr. */
+struct istream *i_stream_create_nonuls(struct istream *input, char replace_chr);
+
+#endif
diff --git a/src/lib-mail/istream-qp-decoder.c b/src/lib-mail/istream-qp-decoder.c
new file mode 100644
index 0000000..7ee0580
--- /dev/null
+++ b/src/lib-mail/istream-qp-decoder.c
@@ -0,0 +1,140 @@
+/* Copyright (c) 2013-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "buffer.h"
+#include "hex-binary.h"
+#include "qp-decoder.h"
+#include "istream-private.h"
+#include "istream-qp.h"
+
+struct qp_decoder_istream {
+ struct istream_private istream;
+ buffer_t *buf;
+ struct qp_decoder *qp;
+};
+
+static void i_stream_qp_decoder_close(struct iostream_private *stream,
+ bool close_parent)
+{
+ struct qp_decoder_istream *bstream =
+ (struct qp_decoder_istream *)stream;
+
+ if (bstream->qp != NULL)
+ qp_decoder_deinit(&bstream->qp);
+ buffer_free(&bstream->buf);
+ if (close_parent)
+ i_stream_close(bstream->istream.parent);
+}
+
+static ssize_t i_stream_qp_decoder_read(struct istream_private *stream)
+{
+ struct qp_decoder_istream *bstream =
+ (struct qp_decoder_istream *)stream;
+ const unsigned char *data;
+ size_t size, error_pos, max_buffer_size;
+ const char *error;
+ int ret;
+
+ max_buffer_size = i_stream_get_max_buffer_size(&stream->istream);
+ for (;;) {
+ /* remove skipped data from buffer */
+ if (stream->skip > 0) {
+ i_assert(stream->skip <= bstream->buf->used);
+ buffer_delete(bstream->buf, 0, stream->skip);
+ stream->pos -= stream->skip;
+ stream->skip = 0;
+ }
+
+ stream->buffer = bstream->buf->data;
+
+ i_assert(stream->pos <= bstream->buf->used);
+ if (stream->pos >= max_buffer_size) {
+ /* stream buffer still at maximum */
+ return -2;
+ }
+
+ /* if something is already decoded, return as much of it as
+ we can */
+ if (bstream->buf->used > 0) {
+ size_t new_pos, bytes;
+
+ /* only return up to max_buffer_size bytes, even when buffer
+ actually has more, as not to confuse the caller */
+ new_pos = I_MIN(bstream->buf->used, max_buffer_size);
+ bytes = new_pos - stream->pos;
+ stream->pos = new_pos;
+
+ return (ssize_t)bytes;
+ }
+
+ /* need to read more input */
+ ret = i_stream_read_more_memarea(stream->parent, &data, &size);
+ if (ret <= 0) {
+ stream->istream.stream_errno = stream->parent->stream_errno;
+ stream->istream.eof = stream->parent->eof;
+ if (ret != -1 || stream->istream.stream_errno != 0)
+ return ret;
+ /* end of quoted-printable stream. verify that the
+ ending is ok. */
+ if (qp_decoder_finish(bstream->qp, &error) == 0) {
+ i_assert(bstream->buf->used == 0);
+ return -1;
+ }
+ io_stream_set_error(&stream->iostream,
+ "Invalid quoted-printable input trailer: %s", error);
+ stream->istream.stream_errno = EPIPE;
+ return -1;
+ }
+ if (qp_decoder_more(bstream->qp, data, size,
+ &error_pos, &error) < 0) {
+ i_assert(error_pos < size);
+ io_stream_set_error(&stream->iostream,
+ "Invalid quoted-printable input 0x%s: %s",
+ binary_to_hex(data+error_pos, I_MIN(size-error_pos, 8)), error);
+ stream->istream.stream_errno = EINVAL;
+ return -1;
+ }
+ i_stream_skip(stream->parent, size);
+ }
+}
+
+static void
+i_stream_qp_decoder_seek(struct istream_private *stream,
+ uoff_t v_offset, bool mark)
+{
+ struct qp_decoder_istream *bstream =
+ (struct qp_decoder_istream *)stream;
+ const char *error;
+
+ if (v_offset < stream->istream.v_offset) {
+ /* seeking backwards - go back to beginning and seek
+ forward from there. */
+ stream->parent_expected_offset = stream->parent_start_offset;
+ stream->skip = stream->pos = 0;
+ stream->istream.v_offset = 0;
+ i_stream_seek(stream->parent, 0);
+ (void)qp_decoder_finish(bstream->qp, &error);
+ buffer_set_used_size(bstream->buf, 0);
+ }
+ i_stream_default_seek_nonseekable(stream, v_offset, mark);
+}
+
+struct istream *i_stream_create_qp_decoder(struct istream *input)
+{
+ struct qp_decoder_istream *bstream;
+
+ bstream = i_new(struct qp_decoder_istream, 1);
+ bstream->istream.max_buffer_size = input->real_stream->max_buffer_size;
+ bstream->buf = buffer_create_dynamic(default_pool, 128);
+ bstream->qp = qp_decoder_init(bstream->buf);
+
+ bstream->istream.iostream.close = i_stream_qp_decoder_close;
+ bstream->istream.read = i_stream_qp_decoder_read;
+ bstream->istream.seek = i_stream_qp_decoder_seek;
+
+ bstream->istream.istream.readable_fd = FALSE;
+ bstream->istream.istream.blocking = input->blocking;
+ bstream->istream.istream.seekable = input->seekable;
+ return i_stream_create(&bstream->istream, input,
+ i_stream_get_fd(input), 0);
+}
diff --git a/src/lib-mail/istream-qp-encoder.c b/src/lib-mail/istream-qp-encoder.c
new file mode 100644
index 0000000..25b7589
--- /dev/null
+++ b/src/lib-mail/istream-qp-encoder.c
@@ -0,0 +1,127 @@
+/* Copyright (c) 2017-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "buffer.h"
+#include "hex-binary.h"
+#include "qp-encoder.h"
+#include "istream-private.h"
+#include "istream-qp.h"
+
+struct qp_encoder_istream {
+ struct istream_private istream;
+ buffer_t *buf;
+ struct qp_encoder *qp;
+};
+
+static void i_stream_qp_encoder_close(struct iostream_private *stream,
+ bool close_parent)
+{
+ struct qp_encoder_istream *bstream =
+ (struct qp_encoder_istream *)stream;
+
+ if (bstream->qp != NULL)
+ qp_encoder_deinit(&bstream->qp);
+ buffer_free(&bstream->buf);
+ if (close_parent)
+ i_stream_close(bstream->istream.parent);
+}
+
+static ssize_t i_stream_qp_encoder_read(struct istream_private *stream)
+{
+ struct qp_encoder_istream *bstream =
+ (struct qp_encoder_istream *)stream;
+ const unsigned char *data;
+ size_t size;
+ int ret;
+
+ for(;;) {
+ if (stream->skip > 0) {
+ i_assert(stream->skip <= bstream->buf->used);
+ buffer_delete(bstream->buf, 0, stream->skip);
+ stream->pos -= stream->skip;
+ stream->skip = 0;
+ }
+
+ stream->buffer = bstream->buf->data;
+ i_assert(stream->pos <= bstream->buf->used);
+
+ if (stream->pos >= bstream->istream.max_buffer_size) {
+ /* stream buffer still at maximum */
+ return -2;
+ }
+
+ /* if something is already interpolated, return as much of it as
+ we can */
+ if (bstream->buf->used > 0) {
+ size_t new_pos, bytes;
+
+ /* only return up to max_buffer_size bytes, even when buffer
+ actually has more, as not to confuse the caller */
+ if (bstream->buf->used <= bstream->istream.max_buffer_size) {
+ new_pos = bstream->buf->used;
+ if (stream->parent->eof)
+ stream->istream.eof = TRUE;
+ } else {
+ new_pos = bstream->istream.max_buffer_size;
+ }
+
+ bytes = new_pos - stream->pos;
+ stream->pos = new_pos;
+ return (ssize_t)bytes;
+ }
+
+ /* need to read more input */
+ ret = i_stream_read_more_memarea(stream->parent, &data, &size);
+ if (ret == 0)
+ return ret;
+ if (size == 0 && ret == -1) {
+ stream->istream.stream_errno =
+ stream->parent->stream_errno;
+ stream->istream.eof = stream->parent->eof;
+ return ret;
+ }
+ qp_encoder_more(bstream->qp, data, size);
+ i_stream_skip(stream->parent, size);
+ }
+}
+
+static void
+i_stream_qp_encoder_seek(struct istream_private *stream,
+ uoff_t v_offset, bool mark)
+{
+ struct qp_encoder_istream *bstream =
+ (struct qp_encoder_istream *)stream;
+
+ if (v_offset < stream->istream.v_offset) {
+ /* seeking backwards - go back to beginning and seek
+ forward from there. */
+ stream->parent_expected_offset = stream->parent_start_offset;
+ stream->skip = stream->pos = 0;
+ stream->istream.v_offset = 0;
+ i_stream_seek(stream->parent, 0);
+ qp_encoder_finish(bstream->qp);
+ buffer_set_used_size(bstream->buf, 0);
+ }
+ i_stream_default_seek_nonseekable(stream, v_offset, mark);
+}
+
+struct istream *i_stream_create_qp_encoder(struct istream *input,
+ enum qp_encoder_flag flags)
+{
+ struct qp_encoder_istream *bstream;
+
+ bstream = i_new(struct qp_encoder_istream, 1);
+ bstream->istream.max_buffer_size = input->real_stream->max_buffer_size;
+ bstream->buf = buffer_create_dynamic(default_pool, 128);
+ bstream->qp = qp_encoder_init(bstream->buf, ISTREAM_QP_ENCODER_MAX_LINE_LENGTH, flags);
+
+ bstream->istream.iostream.close = i_stream_qp_encoder_close;
+ bstream->istream.read = i_stream_qp_encoder_read;
+ bstream->istream.seek = i_stream_qp_encoder_seek;
+
+ bstream->istream.istream.readable_fd = FALSE;
+ bstream->istream.istream.blocking = input->blocking;
+ bstream->istream.istream.seekable = input->seekable;
+ return i_stream_create(&bstream->istream, input,
+ i_stream_get_fd(input), 0);
+}
diff --git a/src/lib-mail/istream-qp.h b/src/lib-mail/istream-qp.h
new file mode 100644
index 0000000..464129b
--- /dev/null
+++ b/src/lib-mail/istream-qp.h
@@ -0,0 +1,12 @@
+#ifndef ISTREAM_QP_H
+#define ISTREAM_QP_H
+
+#include "qp-encoder.h"
+
+#define ISTREAM_QP_ENCODER_MAX_LINE_LENGTH 75
+
+struct istream *i_stream_create_qp_decoder(struct istream *input);
+struct istream *i_stream_create_qp_encoder(struct istream *input,
+ enum qp_encoder_flag flags);
+
+#endif
diff --git a/src/lib-mail/mail-html2text.c b/src/lib-mail/mail-html2text.c
new file mode 100644
index 0000000..9332b64
--- /dev/null
+++ b/src/lib-mail/mail-html2text.c
@@ -0,0 +1,354 @@
+/* Copyright (c) 2011-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "buffer.h"
+#include "unichar.h"
+#include "message-parser.h"
+#include "mail-html2text.h"
+
+/* Zero-width space (&#x200B;) apparently also belongs here, but that gets a
+ bit tricky to handle.. is it actually used anywhere? */
+#define HTML_WHITESPACE(c) \
+ ((c) == ' ' || (c) == '\t' || (c) == '\r' || (c) == '\n')
+
+enum html_state {
+ /* regular text */
+ HTML_STATE_TEXT,
+ /* tag outside "quoted string" */
+ HTML_STATE_TAG,
+ /* tag inside "double quoted string" */
+ HTML_STATE_TAG_DQUOTED,
+ /* tag -> "escape\ */
+ HTML_STATE_TAG_DQUOTED_ESCAPE,
+ /* tag inside 'single quoted string' */
+ HTML_STATE_TAG_SQUOTED,
+ /* tag -> 'escape\ */
+ HTML_STATE_TAG_SQUOTED_ESCAPE,
+ /* comment */
+ HTML_STATE_COMMENT,
+ /* comment is ending, we've seen "--" and now just waiting for ">" */
+ HTML_STATE_COMMENT_END,
+ /* (java)script */
+ HTML_STATE_SCRIPT,
+ /* CSS style */
+ HTML_STATE_STYLE,
+ /* <![CDATA[...]]> */
+ HTML_STATE_CDATA
+};
+
+struct mail_html2text {
+ enum mail_html2text_flags flags;
+ enum html_state state;
+ buffer_t *input;
+ unsigned int quote_level;
+ bool add_newline;
+};
+
+static struct {
+ const char *name;
+ unichar_t chr;
+} html_entities[] = {
+#include "html-entities.h"
+};
+
+struct mail_html2text *
+mail_html2text_init(enum mail_html2text_flags flags)
+{
+ struct mail_html2text *ht;
+
+ ht = i_new(struct mail_html2text, 1);
+ ht->flags = flags;
+ ht->input = buffer_create_dynamic(default_pool, 512);
+ return ht;
+}
+
+static size_t
+parse_tag_name(struct mail_html2text *ht,
+ const unsigned char *data, size_t size)
+{
+ size_t i;
+
+ if (size >= 3 && memcmp(data, "!--", 3) == 0) {
+ ht->state = HTML_STATE_COMMENT;
+ return 3 + 1;
+ }
+ if (size >= 7 && i_memcasecmp(data, "script", 6) == 0 &&
+ (HTML_WHITESPACE(data[6]) || data[6] == '>')) {
+ ht->state = HTML_STATE_SCRIPT;
+ return 7 + 1;
+ }
+ if (size >= 6 && i_memcasecmp(data, "style", 5) == 0 &&
+ (HTML_WHITESPACE(data[5]) || data[5] == '>')) {
+ ht->state = HTML_STATE_STYLE;
+ return 6 + 1;
+ }
+ if (size >= 8 && i_memcasecmp(data, "![CDATA[", 8) == 0) {
+ ht->state = HTML_STATE_CDATA;
+ return 8 + 1;
+ }
+
+ if (size >= 11 && i_memcasecmp(data, "blockquote", 10) == 0 &&
+ (HTML_WHITESPACE(data[10]) || data[10] == '>')) {
+ ht->quote_level++;
+ ht->state = HTML_STATE_TAG;
+ return 1;
+ } else if (ht->quote_level > 0 &&
+ size >= 12 && i_memcasecmp(data, "/blockquote>", 12) == 0) {
+ ht->quote_level--;
+ if ((ht->flags & MAIL_HTML2TEXT_FLAG_SKIP_QUOTED) == 0)
+ ht->add_newline = TRUE;
+ ht->state = HTML_STATE_TAG;
+ return 1;
+ }
+ if (size < 12) {
+ /* can we see the whole tag name? */
+ for (i = 0; i < size; i++) {
+ if (HTML_WHITESPACE(data[i]) || data[i] == '>')
+ break;
+ }
+ if (i == size) {
+ /* need more data */
+ return 0;
+ }
+ }
+ ht->state = HTML_STATE_TAG;
+ return 1;
+}
+
+static bool html_entity_get_unichar(const char *name, unichar_t *chr_r)
+{
+ unichar_t chr;
+
+ for (size_t i = 0; i < N_ELEMENTS(html_entities); i++) {
+ if (strcmp(html_entities[i].name, name) == 0) {
+ *chr_r = html_entities[i].chr;
+ return TRUE;
+ }
+ }
+
+ /* maybe it's just encoded binary byte
+ it can be &#nnn; or &#xnnn;
+ */
+ if (name[0] == '#' &&
+ ((name[1] == 'x' &&
+ str_to_uint32_hex(name+2, &chr) == 0) ||
+ str_to_uint32(name+1, &chr) == 0) &&
+ uni_is_valid_ucs4(chr)) {
+ *chr_r = chr;
+ return TRUE;
+ }
+
+ return FALSE;
+}
+
+static size_t parse_entity(const unsigned char *data, size_t size,
+ buffer_t *output)
+{
+ char entity[10];
+ unichar_t chr;
+ size_t i;
+
+ for (i = 0; i < size; i++) {
+ if (HTML_WHITESPACE(data[i]) || i >= sizeof(entity)) {
+ /* broken entity */
+ return 1;
+ }
+ if (data[i] == ';')
+ break;
+ }
+ if (i == size)
+ return 0;
+
+ i_assert(i < sizeof(entity));
+ memcpy(entity, data, i); entity[i] = '\0';
+
+ if (html_entity_get_unichar(entity, &chr))
+ uni_ucs4_to_utf8_c(chr, output);
+ return i + 1 + 1;
+}
+
+static void mail_html2text_add_space(buffer_t *output)
+{
+ const unsigned char *data = output->data;
+
+ if (output->used > 0 && data[output->used-1] != ' ' &&
+ data[output->used-1] != '\n')
+ buffer_append_c(output, ' ');
+}
+
+static size_t
+parse_data(struct mail_html2text *ht,
+ const unsigned char *data, size_t size, buffer_t *output)
+{
+ size_t i, ret;
+
+ for (i = 0; i < size; i++) {
+ unsigned char c = data[i];
+
+ switch (ht->state) {
+ case HTML_STATE_TEXT:
+ if (c == '<') {
+ ret = parse_tag_name(ht, data+i+1, size-i-1);
+ if (ret == 0)
+ return i;
+ i += ret - 1;
+ } else if (ht->quote_level > 0 &&
+ (ht->flags & MAIL_HTML2TEXT_FLAG_SKIP_QUOTED) != 0) {
+ break;
+ } else if (c == '&') {
+ ret = parse_entity(data+i+1, size-i-1, output);
+ if (ret == 0)
+ return i;
+ i += ret - 1;
+ } else {
+ buffer_append_c(output, c);
+ }
+ break;
+ case HTML_STATE_TAG:
+ if (c == '"')
+ ht->state = HTML_STATE_TAG_DQUOTED;
+ else if (c == '\'')
+ ht->state = HTML_STATE_TAG_SQUOTED;
+ else if (c == '>') {
+ ht->state = HTML_STATE_TEXT;
+ if (ht->quote_level > 0 &&
+ (ht->flags & MAIL_HTML2TEXT_FLAG_SKIP_QUOTED) == 0) {
+ buffer_append(output, "\n>", 2);
+ } else if (ht->add_newline) {
+ buffer_append_c(output, '\n');
+ }
+ ht->add_newline = FALSE;
+ mail_html2text_add_space(output);
+ }
+ break;
+ case HTML_STATE_TAG_DQUOTED:
+ if (c == '"')
+ ht->state = HTML_STATE_TAG;
+ else if (c == '\\')
+ ht->state = HTML_STATE_TAG_DQUOTED_ESCAPE;
+ break;
+ case HTML_STATE_TAG_DQUOTED_ESCAPE:
+ ht->state = HTML_STATE_TAG_DQUOTED;
+ break;
+ case HTML_STATE_TAG_SQUOTED:
+ if (c == '\'')
+ ht->state = HTML_STATE_TAG;
+ else if (c == '\\')
+ ht->state = HTML_STATE_TAG_SQUOTED_ESCAPE;
+ break;
+ case HTML_STATE_TAG_SQUOTED_ESCAPE:
+ ht->state = HTML_STATE_TAG_SQUOTED;
+ break;
+ case HTML_STATE_COMMENT:
+ if (c == '-') {
+ if (i+1 == size)
+ return i;
+ if (data[i+1] == '-') {
+ ht->state = HTML_STATE_COMMENT_END;
+ i++;
+ }
+ }
+ break;
+ case HTML_STATE_COMMENT_END:
+ if (c == '>')
+ ht->state = HTML_STATE_TEXT;
+ else if (!HTML_WHITESPACE(c))
+ ht->state = HTML_STATE_COMMENT;
+ break;
+ case HTML_STATE_SCRIPT:
+ if (c == '<') {
+ unsigned int max_len = I_MIN(size-i, 9);
+
+ if (i_memcasecmp(data+i, "</script>", max_len) == 0) {
+ if (max_len < 9)
+ return i;
+ mail_html2text_add_space(output);
+ ht->state = HTML_STATE_TEXT;
+ i += 8;
+ }
+ }
+ break;
+ case HTML_STATE_STYLE:
+ if (c == '<') {
+ unsigned int max_len = I_MIN(size-i, 8);
+
+ if (i_memcasecmp(data+i, "</style>", max_len) == 0) {
+ if (max_len < 8)
+ return i;
+ mail_html2text_add_space(output);
+ ht->state = HTML_STATE_TEXT;
+ i += 7;
+ }
+ }
+ break;
+ case HTML_STATE_CDATA:
+ if (c == ']') {
+ unsigned int max_len = I_MIN(size-i, 3);
+
+ if (i_memcasecmp(data+i, "]]>", max_len) == 0) {
+ if (max_len < 3)
+ return i;
+ ht->state = HTML_STATE_TEXT;
+ i += 2;
+ break;
+ }
+ }
+ if (ht->quote_level == 0 ||
+ (ht->flags & MAIL_HTML2TEXT_FLAG_SKIP_QUOTED) == 0)
+ buffer_append_c(output, c);
+ break;
+ }
+ }
+ return i;
+}
+
+void mail_html2text_more(struct mail_html2text *ht,
+ const unsigned char *data, size_t size,
+ buffer_t *output)
+{
+ size_t pos, inc_size, buf_orig_size;
+
+ i_assert(size > 0);
+
+ while (ht->input->used > 0) {
+ /* we didn't get enough input the last time to know
+ what to do. */
+ buf_orig_size = ht->input->used;
+
+ inc_size = I_MIN(size, 128);
+ buffer_append(ht->input, data, inc_size);
+ pos = parse_data(ht, ht->input->data,
+ ht->input->used, output);
+ if (pos == 0) {
+ /* we need to add more data into buffer */
+ data += inc_size;
+ size -= inc_size;
+ if (size == 0)
+ return;
+ } else if (pos >= buf_orig_size) {
+ /* we parsed forward */
+ data += pos - buf_orig_size;
+ size -= pos - buf_orig_size;
+ buffer_set_used_size(ht->input, 0);
+ } else {
+ /* invalid input - eat away what we parsed so far
+ and retry */
+ buffer_set_used_size(ht->input, buf_orig_size);
+ buffer_delete(ht->input, 0, pos);
+ }
+ }
+ pos = parse_data(ht, data, size, output);
+ buffer_append(ht->input, data + pos, size - pos);
+}
+
+void mail_html2text_deinit(struct mail_html2text **_ht)
+{
+ struct mail_html2text *ht = *_ht;
+
+ if (ht == NULL)
+ return;
+
+ *_ht = NULL;
+ buffer_free(&ht->input);
+ i_free(ht);
+}
diff --git a/src/lib-mail/mail-html2text.h b/src/lib-mail/mail-html2text.h
new file mode 100644
index 0000000..6af484e
--- /dev/null
+++ b/src/lib-mail/mail-html2text.h
@@ -0,0 +1,22 @@
+#ifndef MAIL_HTML2TEXT_H
+#define MAIL_HTML2TEXT_H
+
+enum mail_html2text_flags {
+ MAIL_HTML2TEXT_FLAG_SKIP_QUOTED = 0x01
+};
+
+struct mail_html2text *
+mail_html2text_init(enum mail_html2text_flags flags);
+void mail_html2text_more(struct mail_html2text *ht,
+ const unsigned char *data, size_t size,
+ buffer_t *output);
+void mail_html2text_deinit(struct mail_html2text **ht);
+
+static inline bool
+mail_html2text_content_type_match(const char *content_type)
+{
+ return strcasecmp(content_type, "text/html") == 0 ||
+ strcasecmp(content_type, "application/xhtml+xml") == 0;
+}
+
+#endif
diff --git a/src/lib-mail/mail-types.h b/src/lib-mail/mail-types.h
new file mode 100644
index 0000000..4703c79
--- /dev/null
+++ b/src/lib-mail/mail-types.h
@@ -0,0 +1,25 @@
+#ifndef MAIL_TYPES_H
+#define MAIL_TYPES_H
+
+enum mail_flags {
+ MAIL_ANSWERED = 0x01,
+ MAIL_FLAGGED = 0x02,
+ MAIL_DELETED = 0x04,
+ MAIL_SEEN = 0x08,
+ MAIL_DRAFT = 0x10,
+ MAIL_RECENT = 0x20,
+
+ MAIL_FLAGS_MASK = 0x3f,
+ MAIL_FLAGS_NONRECENT = (MAIL_FLAGS_MASK ^ MAIL_RECENT)
+};
+
+enum modify_type {
+ MODIFY_ADD,
+ MODIFY_REMOVE,
+ MODIFY_REPLACE
+};
+
+ARRAY_DEFINE_TYPE(keywords, const char *);
+ARRAY_DEFINE_TYPE(keyword_indexes, unsigned int);
+
+#endif
diff --git a/src/lib-mail/mail-user-hash.c b/src/lib-mail/mail-user-hash.c
new file mode 100644
index 0000000..88724a6
--- /dev/null
+++ b/src/lib-mail/mail-user-hash.c
@@ -0,0 +1,48 @@
+/* Copyright (c) 2013-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "md5.h"
+#include "str.h"
+#include "var-expand.h"
+#include "mail-user-hash.h"
+
+bool mail_user_hash(const char *username, const char *format,
+ unsigned int *hash_r, const char **error_r)
+{
+ unsigned char md5[MD5_RESULTLEN];
+ unsigned int i, hash = 0;
+ int ret = 1;
+
+ if (strcmp(format, "%u") == 0) {
+ /* fast path */
+ md5_get_digest(username, strlen(username), md5);
+ } else if (strcmp(format, "%Lu") == 0) {
+ /* almost as fast path */
+ T_BEGIN {
+ md5_get_digest(t_str_lcase(username),
+ strlen(username), md5);
+ } T_END;
+ } else T_BEGIN {
+ const struct var_expand_table tab[] = {
+ { 'u', username, "user" },
+ { 'n', t_strcut(username, '@'), "username" },
+ { 'd', i_strchr_to_next(username, '@'), "domain" },
+ { '\0', NULL, NULL }
+ };
+ string_t *str = t_str_new(128);
+
+ ret = var_expand(str, format, tab, error_r);
+ i_assert(ret >= 0);
+ md5_get_digest(str_data(str), str_len(str), md5);
+ } T_END_PASS_STR_IF(ret == 0, error_r);
+ for (i = 0; i < sizeof(hash); i++)
+ hash = (hash << CHAR_BIT) | md5[i];
+ if (hash == 0) {
+ /* Make sure we don't return the hash as 0, since it's often
+ treated in a special way that won't work well. For example
+ trying to insert it into a hash table will assert-crash. */
+ hash = 1;
+ }
+ *hash_r = hash;
+ return ret > 0;
+}
diff --git a/src/lib-mail/mail-user-hash.h b/src/lib-mail/mail-user-hash.h
new file mode 100644
index 0000000..e3a9e59
--- /dev/null
+++ b/src/lib-mail/mail-user-hash.h
@@ -0,0 +1,10 @@
+#ifndef MAIL_USER_HASH
+#define MAIL_USER_HASH
+
+/* Get a hash for username, based on given format. The format can use
+ %n, %d and %u variables. The returned hash is never 0.
+ Returns TRUE if ok, FALSE if format is invalid. */
+bool mail_user_hash(const char *username, const char *format,
+ unsigned int *hash_r, const char **error_r);
+
+#endif
diff --git a/src/lib-mail/mbox-from.c b/src/lib-mail/mbox-from.c
new file mode 100644
index 0000000..452e32e
--- /dev/null
+++ b/src/lib-mail/mbox-from.c
@@ -0,0 +1,301 @@
+/* Copyright (c) 2002-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "ioloop.h"
+#include "str.h"
+#include "utc-mktime.h"
+#include "utc-offset.h"
+#include "mbox-from.h"
+
+#include <time.h>
+#include <ctype.h>
+
+static const char *weekdays[] = {
+ "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"
+};
+
+static const char *months[] = {
+ "Jan", "Feb", "Mar", "Apr", "May", "Jun",
+ "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"
+};
+
+static int mbox_parse_month(const unsigned char *msg, struct tm *tm)
+{
+ int i;
+
+ for (i = 0; i < 12; i++) {
+ if (i_memcasecmp(months[i], msg, 3) == 0) {
+ tm->tm_mon = i;
+ break;
+ }
+ }
+
+ if (i == 12 && memcmp(msg, "???", 3) == 0) {
+ /* just a hack to parse one special mbox I have :) */
+ i = 0;
+ }
+
+ if (i == 12 || msg[3] != ' ')
+ return -1;
+ return 0;
+}
+
+static int mbox_parse_year(const unsigned char *msg, struct tm *tm)
+{
+ if (!i_isdigit(msg[0]) || !i_isdigit(msg[1]) ||
+ !i_isdigit(msg[2]) || !i_isdigit(msg[3]))
+ return -1;
+
+ tm->tm_year = (msg[0]-'0') * 1000 + (msg[1]-'0') * 100 +
+ (msg[2]-'0') * 10 + (msg[3]-'0') - 1900;
+ return 0;
+}
+
+int mbox_from_parse(const unsigned char *msg, size_t size,
+ time_t *time_r, int *tz_offset_r, char **sender_r)
+{
+ const unsigned char *msg_start, *sender_end, *msg_end;
+ struct tm tm;
+ bool esc, alt_stamp, seen_timezone = FALSE;
+ int timezone_secs = 0;
+ time_t t;
+
+ *time_r = (time_t)-1;
+ *sender_r = NULL;
+
+ /* <sender> <date> <moreinfo> */
+ msg_start = msg;
+ msg_end = msg + size;
+
+ /* get sender */
+ if (msg < msg_end && *msg == '"') {
+ /* "x y z"@domain - skip the quoted part */
+ esc = FALSE;
+ msg++;
+ while (msg < msg_end && (*msg != '"' || esc)) {
+ if (*msg == '\r' || *msg == '\n')
+ return -1;
+ esc = *msg == '\\';
+ msg++;
+ }
+ msg++;
+ }
+
+ while (msg < msg_end && *msg != ' ') {
+ if (*msg == '\r' || *msg == '\n')
+ return -1;
+ msg++;
+ }
+ sender_end = msg;
+ while (msg < msg_end && *msg == ' ') msg++;
+
+ /* next 29 chars should be in the date in asctime() format, eg.
+ "Thu Nov 9 22:33:52 2001 +0300"
+
+ - Some put the timezone before the year
+ - Some use a named timezone before or after year, which we ignore
+ - Some don't include seconds (-3)
+ - Some don't include timezone (-5)
+ */
+ if (msg+29-3-5 > msg_end)
+ return -1;
+
+ i_zero(&tm);
+
+ /* skip weekday */
+ msg += 4;
+
+ /* month */
+ if (mbox_parse_month(msg, &tm) < 0) {
+ /* Try alternate timestamp: "Thu, 9 Nov 2002 22:33:52" */
+ alt_stamp = TRUE;
+ msg++;
+
+ if (!i_isdigit(msg[0]))
+ return -1;
+ tm.tm_mday = msg[0]-'0';
+ msg++;
+
+ if (i_isdigit(msg[0])) {
+ tm.tm_mday = tm.tm_mday*10 + msg[0]-'0';
+ msg++;
+ }
+ if (msg[0] != ' ')
+ return -1;
+ msg++;
+
+ if (mbox_parse_month(msg, &tm) < 0)
+ return -1;
+ msg += 4;
+
+ if (mbox_parse_year(msg, &tm) < 0)
+ return -1;
+ msg += 5;
+ } else {
+ alt_stamp = FALSE;
+ msg += 4;
+
+ /* day. single digit is usually preceded by extra space */
+ if (msg[0] == ' ')
+ msg++;
+ if (msg[1] == ' ') {
+ if (!i_isdigit(msg[0]))
+ return -1;
+ tm.tm_mday = msg[0]-'0';
+ msg += 2;
+ } else {
+ if (!i_isdigit(msg[0]) || !i_isdigit(msg[1]) || msg[2] != ' ')
+ return -1;
+ tm.tm_mday = (msg[0]-'0') * 10 + (msg[1]-'0');
+ msg += 3;
+ }
+ }
+ if (tm.tm_mday == 0)
+ tm.tm_mday = 1;
+
+ /* hour */
+ if (!i_isdigit(msg[0]) || !i_isdigit(msg[1]) || msg[2] != ':')
+ return -1;
+ tm.tm_hour = (msg[0]-'0') * 10 + (msg[1]-'0');
+ msg += 3;
+
+ /* minute */
+ if (!i_isdigit(msg[0]) || !i_isdigit(msg[1]))
+ return -1;
+ tm.tm_min = (msg[0]-'0') * 10 + (msg[1]-'0');
+ msg += 2;
+
+ /* optional second */
+ if (msg[0] == ':') {
+ msg++;
+ if (!i_isdigit(msg[0]) || !i_isdigit(msg[1]))
+ return -1;
+ tm.tm_sec = (msg[0]-'0') * 10 + (msg[1]-'0');
+ msg += 2;
+
+ if (!alt_stamp) {
+ if (msg[0] == ' ')
+ msg++;
+ else
+ return -1;
+ }
+ } else if (!alt_stamp) {
+ if (msg[0] != ' ')
+ return -1;
+ msg++;
+ }
+
+ /* optional named timezone */
+ if (alt_stamp)
+ ;
+ else if (!i_isdigit(msg[0]) || !i_isdigit(msg[1]) ||
+ !i_isdigit(msg[2]) || !i_isdigit(msg[3])) {
+ /* skip to next space */
+ while (msg < msg_end && *msg != ' ') {
+ if (*msg == '\r' || *msg == '\n')
+ return -1;
+ msg++;
+ }
+ if (msg+5 > msg_end)
+ return -1;
+ msg++;
+ } else if ((msg[0] == '-' || msg[0] == '+') &&
+ i_isdigit(msg[1]) && i_isdigit(msg[2]) &&
+ i_isdigit(msg[3]) && i_isdigit(msg[4]) && msg[5] == ' ') {
+ /* numeric timezone, use it */
+ seen_timezone = TRUE;
+ timezone_secs = (msg[1]-'0') * 10*60*60 + (msg[2]-'0') * 60*60 +
+ (msg[3]-'0') * 10 + (msg[4]-'0');
+ if (msg[0] == '-') timezone_secs = -timezone_secs;
+ msg += 6;
+ }
+
+ if (!alt_stamp) {
+ /* year */
+ if (mbox_parse_year(msg, &tm) < 0)
+ return -1;
+ msg += 4;
+ }
+
+ tm.tm_isdst = -1;
+ if (!seen_timezone && msg != msg_end &&
+ msg[0] == ' ' && (msg[1] == '-' || msg[1] == '+') &&
+ i_isdigit(msg[2]) && i_isdigit(msg[3]) &&
+ i_isdigit(msg[4]) && i_isdigit(msg[5])) {
+ seen_timezone = TRUE;
+ timezone_secs = (msg[2]-'0') * 10*60*60 + (msg[3]-'0') * 60*60 +
+ (msg[4]-'0') * 10 + (msg[5]-'0');
+ if (msg[1] == '-') timezone_secs = -timezone_secs;
+ }
+
+ if (seen_timezone) {
+ t = utc_mktime(&tm);
+ if (t == (time_t)-1)
+ return -1;
+
+ t -= timezone_secs;
+ *time_r = t;
+ *tz_offset_r = timezone_secs/60;
+ } else {
+ /* assume local timezone */
+ *time_r = mktime(&tm);
+ *tz_offset_r = utc_offset(localtime(time_r), *time_r);
+ }
+
+ *sender_r = i_strdup_until(msg_start, sender_end);
+ return 0;
+}
+
+const char *mbox_from_create(const char *sender, time_t timestamp)
+{
+ string_t *str;
+ struct tm *tm;
+ int year;
+
+ str = t_str_new(256);
+ str_append(str, "From ");
+ str_append(str, sender);
+ str_append(str, " ");
+
+ /* we could use simply asctime(), but i18n etc. may break it.
+ Example: "Thu Nov 29 22:33:52 2001" */
+ tm = localtime(&timestamp);
+
+ /* week day */
+ str_append(str, weekdays[tm->tm_wday]);
+ str_append_c(str, ' ');
+
+ /* month */
+ str_append(str, months[tm->tm_mon]);
+ str_append_c(str, ' ');
+
+ /* day */
+ str_append_c(str, (tm->tm_mday / 10) + '0');
+ str_append_c(str, (tm->tm_mday % 10) + '0');
+ str_append_c(str, ' ');
+
+ /* hour */
+ str_append_c(str, (tm->tm_hour / 10) + '0');
+ str_append_c(str, (tm->tm_hour % 10) + '0');
+ str_append_c(str, ':');
+
+ /* minute */
+ str_append_c(str, (tm->tm_min / 10) + '0');
+ str_append_c(str, (tm->tm_min % 10) + '0');
+ str_append_c(str, ':');
+
+ /* second */
+ str_append_c(str, (tm->tm_sec / 10) + '0');
+ str_append_c(str, (tm->tm_sec % 10) + '0');
+ str_append_c(str, ' ');
+
+ /* year */
+ year = tm->tm_year + 1900;
+ str_append_c(str, (year / 1000) + '0');
+ str_append_c(str, ((year / 100) % 10) + '0');
+ str_append_c(str, ((year / 10) % 10) + '0');
+ str_append_c(str, (year % 10) + '0');
+
+ str_append_c(str, '\n');
+ return str_c(str);
+}
diff --git a/src/lib-mail/mbox-from.h b/src/lib-mail/mbox-from.h
new file mode 100644
index 0000000..fbca066
--- /dev/null
+++ b/src/lib-mail/mbox-from.h
@@ -0,0 +1,12 @@
+#ifndef MBOX_FROM_H
+#define MBOX_FROM_H
+
+/* Parse time and sender from mbox-compatible From_-line. msg points to the
+ data after "From ". */
+int mbox_from_parse(const unsigned char *msg, size_t size,
+ time_t *time_r, int *tz_offset_r, char **sender_r);
+/* Return a mbox-compatible From_-line using given sender and time.
+ The returned string begins with "From ". */
+const char *mbox_from_create(const char *sender, time_t timestamp);
+
+#endif
diff --git a/src/lib-mail/message-address.c b/src/lib-mail/message-address.c
new file mode 100644
index 0000000..fb06afa
--- /dev/null
+++ b/src/lib-mail/message-address.c
@@ -0,0 +1,672 @@
+/* Copyright (c) 2002-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "str.h"
+#include "strescape.h"
+#include "smtp-address.h"
+#include "message-parser.h"
+#include "message-address.h"
+#include "rfc822-parser.h"
+
+struct message_address_parser_context {
+ pool_t pool;
+ struct rfc822_parser_context parser;
+
+ struct message_address *first_addr, *last_addr, addr;
+ string_t *str;
+
+ bool fill_missing, non_strict_dots;
+};
+
+static void add_address(struct message_address_parser_context *ctx)
+{
+ struct message_address *addr;
+
+ addr = p_new(ctx->pool, struct message_address, 1);
+
+ memcpy(addr, &ctx->addr, sizeof(ctx->addr));
+ i_zero(&ctx->addr);
+
+ if (ctx->first_addr == NULL)
+ ctx->first_addr = addr;
+ else
+ ctx->last_addr->next = addr;
+ ctx->last_addr = addr;
+}
+
+/* quote with "" and escape all '\', '"' and "'" characters if need */
+static void str_append_maybe_escape(string_t *dest, const char *cstr, bool escape_dot)
+{
+ const char *p;
+
+ /* see if we need to quote it */
+ for (p = cstr; *p != '\0'; p++) {
+ if (!IS_ATEXT(*p) && (escape_dot || *p != '.'))
+ break;
+ }
+
+ if (*p == '\0') {
+ str_append_data(dest, cstr, (size_t) (p - cstr));
+ return;
+ }
+
+ /* see if we need to escape it */
+ for (p = cstr; *p != '\0'; p++) {
+ if (IS_ESCAPED_CHAR(*p))
+ break;
+ }
+
+ if (*p == '\0') {
+ /* only quote */
+ str_append_c(dest, '"');
+ str_append_data(dest, cstr, (size_t) (p - cstr));
+ str_append_c(dest, '"');
+ return;
+ }
+
+ /* quote and escape */
+ str_append_c(dest, '"');
+ str_append_data(dest, cstr, (size_t) (p - cstr));
+
+ for (; *p != '\0'; p++) {
+ if (IS_ESCAPED_CHAR(*p))
+ str_append_c(dest, '\\');
+ str_append_c(dest, *p);
+ }
+
+ str_append_c(dest, '"');
+}
+
+static int
+parse_nonstrict_dot_atom(struct rfc822_parser_context *ctx, string_t *str)
+{
+ int ret = -1;
+
+ do {
+ while (*ctx->data == '.') {
+ str_append_c(str, '.');
+ ctx->data++;
+ if (ctx->data == ctx->end) {
+ /* @domain is missing, but local-part
+ parsing was successful */
+ return 0;
+ }
+ ret = 1;
+ }
+ if (*ctx->data == '@')
+ break;
+ ret = rfc822_parse_atom(ctx, str);
+ } while (ret > 0 && *ctx->data == '.');
+ return ret;
+}
+
+static int parse_local_part(struct message_address_parser_context *ctx)
+{
+ int ret;
+
+ /*
+ local-part = dot-atom / quoted-string / obs-local-part
+ obs-local-part = word *("." word)
+ */
+ i_assert(ctx->parser.data < ctx->parser.end);
+
+ str_truncate(ctx->str, 0);
+ if (*ctx->parser.data == '"')
+ ret = rfc822_parse_quoted_string(&ctx->parser, ctx->str);
+ else if (!ctx->non_strict_dots)
+ ret = rfc822_parse_dot_atom(&ctx->parser, ctx->str);
+ else
+ ret = parse_nonstrict_dot_atom(&ctx->parser, ctx->str);
+ if (ret < 0)
+ return -1;
+
+ ctx->addr.mailbox = p_strdup(ctx->pool, str_c(ctx->str));
+ return ret;
+}
+
+static int parse_domain(struct message_address_parser_context *ctx)
+{
+ int ret;
+
+ str_truncate(ctx->str, 0);
+ if ((ret = rfc822_parse_domain(&ctx->parser, ctx->str)) < 0)
+ return -1;
+
+ ctx->addr.domain = p_strdup(ctx->pool, str_c(ctx->str));
+ return ret;
+}
+
+static int parse_domain_list(struct message_address_parser_context *ctx)
+{
+ int ret;
+
+ /* obs-domain-list = "@" domain *(*(CFWS / "," ) [CFWS] "@" domain) */
+ str_truncate(ctx->str, 0);
+ for (;;) {
+ if (ctx->parser.data >= ctx->parser.end)
+ return 0;
+
+ if (*ctx->parser.data != '@')
+ break;
+
+ if (str_len(ctx->str) > 0)
+ str_append_c(ctx->str, ',');
+
+ str_append_c(ctx->str, '@');
+ if ((ret = rfc822_parse_domain(&ctx->parser, ctx->str)) <= 0)
+ return ret;
+
+ while (rfc822_skip_lwsp(&ctx->parser) > 0 &&
+ *ctx->parser.data == ',')
+ ctx->parser.data++;
+ }
+ ctx->addr.route = p_strdup(ctx->pool, str_c(ctx->str));
+ return 1;
+}
+
+static int parse_angle_addr(struct message_address_parser_context *ctx,
+ bool parsing_path)
+{
+ /* "<" [ "@" route ":" ] local-part "@" domain ">" */
+ i_assert(*ctx->parser.data == '<');
+ ctx->parser.data++;
+
+ if (rfc822_skip_lwsp(&ctx->parser) <= 0)
+ return -1;
+
+ if (*ctx->parser.data == '@') {
+ if (parse_domain_list(ctx) > 0 && *ctx->parser.data == ':') {
+ ctx->parser.data++;
+ } else if (parsing_path && (ctx->parser.data >= ctx->parser.end || *ctx->parser.data != ':')) {
+ return -1;
+ } else {
+ if (ctx->fill_missing)
+ ctx->addr.route = "INVALID_ROUTE";
+ if (ctx->parser.data >= ctx->parser.end)
+ return -1;
+ /* try to continue anyway */
+ }
+ if (rfc822_skip_lwsp(&ctx->parser) <= 0)
+ return -1;
+ }
+
+ if (*ctx->parser.data == '>') {
+ /* <> address isn't valid */
+ } else {
+ if (parse_local_part(ctx) <= 0)
+ return -1;
+ if (*ctx->parser.data == '@') {
+ if (parse_domain(ctx) <= 0)
+ return -1;
+ }
+ }
+
+ if (*ctx->parser.data != '>')
+ return -1;
+ ctx->parser.data++;
+
+ return rfc822_skip_lwsp(&ctx->parser);
+}
+
+static int parse_name_addr(struct message_address_parser_context *ctx)
+{
+ /*
+ name-addr = [display-name] angle-addr
+ display-name = phrase
+ */
+ str_truncate(ctx->str, 0);
+ if (rfc822_parse_phrase(&ctx->parser, ctx->str) <= 0 ||
+ *ctx->parser.data != '<')
+ return -1;
+
+ ctx->addr.name = p_strdup(ctx->pool, str_c(ctx->str));
+ if (*ctx->addr.name == '\0') {
+ /* Cope with "<address>" without display name */
+ ctx->addr.name = NULL;
+ }
+ if (parse_angle_addr(ctx, FALSE) < 0) {
+ /* broken */
+ if (ctx->fill_missing)
+ ctx->addr.domain = "SYNTAX_ERROR";
+ ctx->addr.invalid_syntax = TRUE;
+ }
+ return ctx->parser.data < ctx->parser.end ? 1 : 0;
+}
+
+static int parse_addr_spec(struct message_address_parser_context *ctx)
+{
+ /* addr-spec = local-part "@" domain */
+ int ret, ret2 = -2;
+
+ i_assert(ctx->parser.data < ctx->parser.end);
+
+ str_truncate(ctx->parser.last_comment, 0);
+
+ bool quoted_string = *ctx->parser.data == '"';
+ ret = parse_local_part(ctx);
+ if (ret <= 0) {
+ /* end of input or parsing local-part failed */
+ ctx->addr.invalid_syntax = TRUE;
+ }
+ if (ret != 0 && ctx->parser.data < ctx->parser.end &&
+ *ctx->parser.data == '@') {
+ ret2 = parse_domain(ctx);
+ if (ret2 <= 0)
+ ret = ret2;
+ }
+
+ if (str_len(ctx->parser.last_comment) > 0)
+ ctx->addr.name = p_strdup(ctx->pool, str_c(ctx->parser.last_comment));
+ else if (ret2 == -2) {
+ /* So far we've read user without @domain and without
+ (Display Name). We'll assume that a single "user" (already
+ read into addr.mailbox) is a mailbox, but if it's followed
+ by anything else it's a display-name. */
+ str_append_c(ctx->str, ' ');
+ size_t orig_str_len = str_len(ctx->str);
+ (void)rfc822_parse_phrase(&ctx->parser, ctx->str);
+ if (str_len(ctx->str) != orig_str_len) {
+ ctx->addr.mailbox = NULL;
+ ctx->addr.name = p_strdup(ctx->pool, str_c(ctx->str));
+ } else {
+ if (!quoted_string)
+ ctx->addr.domain = "";
+ }
+ ctx->addr.invalid_syntax = TRUE;
+ ret = -1;
+ }
+ return ret;
+}
+
+static void add_fixed_address(struct message_address_parser_context *ctx)
+{
+ if (ctx->addr.mailbox == NULL) {
+ ctx->addr.mailbox = !ctx->fill_missing ? "" : "MISSING_MAILBOX";
+ ctx->addr.invalid_syntax = TRUE;
+ }
+ if (ctx->addr.domain == NULL || ctx->addr.domain[0] == '\0') {
+ ctx->addr.domain = !ctx->fill_missing ? "" : "MISSING_DOMAIN";
+ ctx->addr.invalid_syntax = TRUE;
+ }
+ add_address(ctx);
+}
+
+static int parse_mailbox(struct message_address_parser_context *ctx)
+{
+ const unsigned char *start;
+ int ret;
+
+ /* mailbox = name-addr / addr-spec */
+ start = ctx->parser.data;
+ if ((ret = parse_name_addr(ctx)) < 0) {
+ /* nope, should be addr-spec */
+ ctx->parser.data = start;
+ ret = parse_addr_spec(ctx);
+ if (ctx->addr.invalid_syntax && ctx->addr.name == NULL &&
+ ctx->addr.mailbox != NULL && ctx->addr.domain == NULL) {
+ ctx->addr.name = ctx->addr.mailbox;
+ ctx->addr.mailbox = NULL;
+ }
+ }
+
+ if (ret < 0)
+ ctx->addr.invalid_syntax = TRUE;
+ add_fixed_address(ctx);
+ return ret;
+}
+
+static int parse_group(struct message_address_parser_context *ctx)
+{
+ int ret;
+
+ /*
+ group = display-name ":" [mailbox-list / CFWS] ";" [CFWS]
+ display-name = phrase
+ */
+ str_truncate(ctx->str, 0);
+ if (rfc822_parse_phrase(&ctx->parser, ctx->str) <= 0 ||
+ *ctx->parser.data != ':')
+ return -1;
+
+ /* from now on don't return -1 even if there are problems, so that
+ the caller knows this is a group */
+ ctx->parser.data++;
+ if ((ret = rfc822_skip_lwsp(&ctx->parser)) <= 0)
+ ctx->addr.invalid_syntax = TRUE;
+
+ ctx->addr.mailbox = p_strdup(ctx->pool, str_c(ctx->str));
+ add_address(ctx);
+
+ if (ret > 0 && *ctx->parser.data != ';') {
+ for (;;) {
+ /* mailbox-list =
+ (mailbox *("," mailbox)) / obs-mbox-list */
+ if (parse_mailbox(ctx) <= 0) {
+ /* broken mailbox - try to continue anyway. */
+ }
+ if (ctx->parser.data >= ctx->parser.end ||
+ *ctx->parser.data != ',')
+ break;
+ ctx->parser.data++;
+ if (rfc822_skip_lwsp(&ctx->parser) <= 0) {
+ ret = -1;
+ break;
+ }
+ }
+ }
+ if (ret >= 0) {
+ if (ctx->parser.data >= ctx->parser.end ||
+ *ctx->parser.data != ';')
+ ret = -1;
+ else {
+ ctx->parser.data++;
+ ret = rfc822_skip_lwsp(&ctx->parser);
+ }
+ }
+ if (ret < 0)
+ ctx->addr.invalid_syntax = TRUE;
+
+ add_address(ctx);
+ return ret == 0 ? 0 : 1;
+}
+
+static int parse_address(struct message_address_parser_context *ctx)
+{
+ const unsigned char *start;
+ int ret;
+
+ /* address = mailbox / group */
+ start = ctx->parser.data;
+ if ((ret = parse_group(ctx)) < 0) {
+ /* not a group, try mailbox */
+ ctx->parser.data = start;
+ ret = parse_mailbox(ctx);
+ }
+ return ret;
+}
+
+static int parse_address_list(struct message_address_parser_context *ctx,
+ unsigned int max_addresses)
+{
+ int ret = 0;
+
+ /* address-list = (address *("," address)) / obs-addr-list */
+ while (max_addresses > 0) {
+ max_addresses--;
+ if ((ret = parse_address(ctx)) == 0)
+ break;
+ if (ctx->parser.data >= ctx->parser.end ||
+ *ctx->parser.data != ',') {
+ ret = -1;
+ break;
+ }
+ ctx->parser.data++;
+ if ((ret = rfc822_skip_lwsp(&ctx->parser)) <= 0) {
+ if (ret < 0) {
+ /* ends with some garbage */
+ add_fixed_address(ctx);
+ }
+ break;
+ }
+ }
+ return ret;
+}
+
+static int parse_path(struct message_address_parser_context *ctx)
+{
+ int ret;
+
+ if (rfc822_skip_lwsp(&ctx->parser) <= 0)
+ return -1;
+ if (*ctx->parser.data != '<') {
+ /* Cope with paths that omit < and >. This is a syntax
+ violation, but we allow it to account for a rather wide
+ selection of software that does not follow the standards.
+ */
+ if ((ret=parse_local_part(ctx)) > 0 &&
+ *ctx->parser.data == '@') {
+ ret = parse_domain(ctx);
+ }
+ } else {
+ ret = parse_angle_addr(ctx, TRUE);
+ }
+ if (ret < 0 || (ret=rfc822_skip_lwsp(&ctx->parser)) < 0 ||
+ ctx->parser.data != ctx->parser.end ||
+ (ctx->addr.mailbox != NULL &&
+ (ctx->addr.domain == NULL || *ctx->addr.domain == '\0')) ||
+ (ctx->addr.mailbox == NULL && ctx->addr.domain != NULL)) {
+ ctx->addr.invalid_syntax = TRUE;
+ ret = -1;
+ }
+ add_address(ctx);
+ return ret;
+}
+
+static struct message_address *
+message_address_parse_real(pool_t pool, const unsigned char *data, size_t size,
+ unsigned int max_addresses,
+ enum message_address_parse_flags flags)
+{
+ struct message_address_parser_context ctx;
+
+ i_zero(&ctx);
+
+ rfc822_parser_init(&ctx.parser, data, size, t_str_new(128));
+ ctx.parser.nul_replacement_str = RFC822_NUL_REPLACEMENT_STR;
+ ctx.pool = pool;
+ ctx.str = t_str_new(128);
+ ctx.fill_missing = (flags & MESSAGE_ADDRESS_PARSE_FLAG_FILL_MISSING) != 0;
+ ctx.non_strict_dots = (flags & MESSAGE_ADDRESS_PARSE_FLAG_STRICT_DOTS) == 0;
+
+ if (rfc822_skip_lwsp(&ctx.parser) <= 0) {
+ /* no addresses */
+ } else {
+ (void)parse_address_list(&ctx, max_addresses);
+ }
+ rfc822_parser_deinit(&ctx.parser);
+ return ctx.first_addr;
+}
+
+static int
+message_address_parse_path_real(pool_t pool, const unsigned char *data,
+ size_t size, struct message_address **addr_r)
+{
+ struct message_address_parser_context ctx;
+ int ret;
+
+ i_zero(&ctx);
+ *addr_r = NULL;
+
+ rfc822_parser_init(&ctx.parser, data, size, NULL);
+ ctx.pool = pool;
+ ctx.str = t_str_new(128);
+
+ ret = parse_path(&ctx);
+
+ rfc822_parser_deinit(&ctx.parser);
+ *addr_r = ctx.first_addr;
+ return (ret < 0 ? -1 : 0);
+}
+
+struct message_address *
+message_address_parse(pool_t pool, const unsigned char *data, size_t size,
+ unsigned int max_addresses,
+ enum message_address_parse_flags flags)
+{
+ struct message_address *addr;
+
+ if (pool->datastack_pool) {
+ return message_address_parse_real(pool, data, size,
+ max_addresses, flags);
+ }
+ T_BEGIN {
+ addr = message_address_parse_real(pool, data, size,
+ max_addresses, flags);
+ } T_END;
+ return addr;
+}
+
+int message_address_parse_path(pool_t pool, const unsigned char *data,
+ size_t size, struct message_address **addr_r)
+{
+ int ret;
+
+ if (pool->datastack_pool) {
+ return message_address_parse_path_real(pool, data, size, addr_r);
+ }
+ T_BEGIN {
+ ret = message_address_parse_path_real(pool, data, size, addr_r);
+ } T_END;
+ return ret;
+}
+
+void message_address_write(string_t *str, const struct message_address *addr)
+{
+ const char *tmp;
+ bool first = TRUE, in_group = FALSE;
+
+ if (addr == NULL)
+ return;
+
+ /* <> path */
+ if (addr->mailbox == NULL && addr->domain == NULL) {
+ i_assert(addr->next == NULL);
+ str_append(str, "<>");
+ return;
+ }
+
+ /* a) mailbox@domain
+ b) name <@route:mailbox@domain>
+ c) group: .. ; */
+
+ while (addr != NULL) {
+ if (first)
+ first = FALSE;
+ else
+ str_append(str, ", ");
+
+ if (addr->domain == NULL) {
+ if (!in_group) {
+ /* beginning of group. mailbox is the group
+ name, others are NULL. */
+ if (addr->mailbox != NULL && *addr->mailbox != '\0') {
+ /* check for MIME encoded-word */
+ if (strstr(addr->mailbox, "=?") != NULL)
+ /* MIME encoded-word MUST NOT appear within a 'quoted-string'
+ so escaping and quoting of phrase is not possible, instead
+ use obsolete RFC822 phrase syntax which allow spaces */
+ str_append(str, addr->mailbox);
+ else
+ str_append_maybe_escape(str, addr->mailbox, TRUE);
+ } else {
+ /* empty group name needs to be quoted */
+ str_append(str, "\"\"");
+ }
+ str_append(str, ": ");
+ first = TRUE;
+ } else {
+ /* end of group. all fields should be NULL. */
+ i_assert(addr->mailbox == NULL);
+
+ /* cut out the ", " */
+ tmp = str_c(str)+str_len(str)-2;
+ i_assert((tmp[0] == ',' || tmp[0] == ':') && tmp[1] == ' ');
+ if (tmp[0] == ',' && tmp[1] == ' ')
+ str_truncate(str, str_len(str)-2);
+ else if (tmp[0] == ':' && tmp[1] == ' ')
+ str_truncate(str, str_len(str)-1);
+ str_append_c(str, ';');
+ }
+
+ in_group = !in_group;
+ } else {
+ /* "Display Name" <mailbox@domain> */
+ i_assert(addr->mailbox != NULL);
+
+ if (addr->name != NULL) {
+ /* check for MIME encoded-word */
+ if (strstr(addr->name, "=?") != NULL)
+ /* MIME encoded-word MUST NOT appear within a 'quoted-string'
+ so escaping and quoting of phrase is not possible, instead
+ use obsolete RFC822 phrase syntax which allow spaces */
+ str_append(str, addr->name);
+ else
+ str_append_maybe_escape(str, addr->name, TRUE);
+ }
+ if (addr->route != NULL ||
+ addr->mailbox[0] != '\0' ||
+ addr->domain[0] != '\0') {
+ if (addr->name != NULL && addr->name[0] != '\0')
+ str_append_c(str, ' ');
+ str_append_c(str, '<');
+ if (addr->route != NULL) {
+ str_append(str, addr->route);
+ str_append_c(str, ':');
+ }
+ if (addr->mailbox[0] == '\0')
+ str_append(str, "\"\"");
+ else
+ str_append_maybe_escape(str, addr->mailbox, FALSE);
+ if (addr->domain[0] != '\0') {
+ str_append_c(str, '@');
+ str_append(str, addr->domain);
+ }
+ str_append_c(str, '>');
+ }
+ }
+
+ addr = addr->next;
+ }
+}
+
+const char *message_address_to_string(const struct message_address *addr)
+{
+ string_t *str = t_str_new(256);
+ message_address_write(str, addr);
+ return str_c(str);
+}
+
+const char *message_address_first_to_string(const struct message_address *addr)
+{
+ struct message_address first_addr;
+
+ first_addr = *addr;
+ first_addr.next = NULL;
+ first_addr.route = NULL;
+ return message_address_to_string(&first_addr);
+}
+
+void message_address_init(struct message_address *addr,
+ const char *name, const char *mailbox, const char *domain)
+{
+ i_zero(addr);
+ addr->name = name;
+ addr->mailbox = mailbox;
+ addr->domain = domain;
+}
+
+void message_address_init_from_smtp(struct message_address *addr,
+ const char *name, const struct smtp_address *smtp_addr)
+{
+ i_zero(addr);
+ addr->name = name;
+ addr->mailbox = smtp_addr->localpart;
+ addr->domain = smtp_addr->domain;
+}
+
+static const char *address_headers[] = {
+ "From", "Sender", "Reply-To",
+ "To", "Cc", "Bcc",
+ "Resent-From", "Resent-Sender", "Resent-To", "Resent-Cc", "Resent-Bcc"
+};
+
+bool message_header_is_address(const char *hdr_name)
+{
+ unsigned int i;
+
+ for (i = 0; i < N_ELEMENTS(address_headers); i++) {
+ if (strcasecmp(hdr_name, address_headers[i]) == 0)
+ return TRUE;
+ }
+ return FALSE;
+}
diff --git a/src/lib-mail/message-address.h b/src/lib-mail/message-address.h
new file mode 100644
index 0000000..8370397
--- /dev/null
+++ b/src/lib-mail/message-address.h
@@ -0,0 +1,61 @@
+#ifndef MESSAGE_ADDRESS_H
+#define MESSAGE_ADDRESS_H
+
+struct smtp_address;
+
+enum message_address_parse_flags {
+ /* If enabled, missing mailbox and domain are set to MISSING_MAILBOX
+ and MISSING_DOMAIN strings. Otherwise they're set to "". */
+ MESSAGE_ADDRESS_PARSE_FLAG_FILL_MISSING = BIT(0),
+ /* Require local-part to strictly adhere to RFC5322 when parsing dots.
+ For example ".user", "us..ser" and "user." will be invalid. This
+ isn't enabled by default, because these kind of invalid addresses
+ are commonly used in Japan. */
+ MESSAGE_ADDRESS_PARSE_FLAG_STRICT_DOTS = BIT(1),
+};
+
+/* group: ... ; will be stored like:
+ {name = NULL, NULL, "group", NULL}, ..., {NULL, NULL, NULL, NULL}
+*/
+struct message_address {
+ struct message_address *next;
+
+ /* display-name */
+ const char *name;
+ /* route string contains the @ prefix */
+ const char *route;
+ /* local-part */
+ const char *mailbox;
+ const char *domain;
+ /* there were errors when parsing this address */
+ bool invalid_syntax;
+};
+
+/* Parse message addresses from given data. Note that giving an empty string
+ will return NULL since there are no addresses. */
+struct message_address *
+message_address_parse(pool_t pool, const unsigned char *data, size_t size,
+ unsigned int max_addresses,
+ enum message_address_parse_flags flags);
+
+/* Parse RFC 5322 "path" (Return-Path header) from given data. Returns -1 if
+ the path is invalid and 0 otherwise.
+ */
+int message_address_parse_path(pool_t pool, const unsigned char *data,
+ size_t size, struct message_address **addr_r);
+
+void message_address_init(struct message_address *addr,
+ const char *name, const char *mailbox, const char *domain)
+ ATTR_NULL(1);
+void message_address_init_from_smtp(struct message_address *addr,
+ const char *name, const struct smtp_address *smtp_addr)
+ ATTR_NULL(1);
+
+void message_address_write(string_t *str, const struct message_address *addr);
+const char *message_address_to_string(const struct message_address *addr);
+const char *message_address_first_to_string(const struct message_address *addr);
+
+/* Returns TRUE if header is known to be an address */
+bool message_header_is_address(const char *hdr_name);
+
+#endif
diff --git a/src/lib-mail/message-binary-part.c b/src/lib-mail/message-binary-part.c
new file mode 100644
index 0000000..20416ff
--- /dev/null
+++ b/src/lib-mail/message-binary-part.c
@@ -0,0 +1,44 @@
+/* Copyright (c) 2013-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "numpack.h"
+#include "message-binary-part.h"
+
+void message_binary_part_serialize(const struct message_binary_part *parts,
+ buffer_t *dest)
+{
+ const struct message_binary_part *part;
+
+ for (part = parts; part != NULL; part = part->next) {
+ numpack_encode(dest, part->physical_pos);
+ numpack_encode(dest, part->binary_hdr_size);
+ numpack_encode(dest, part->binary_body_size);
+ numpack_encode(dest, part->binary_body_lines_count);
+ }
+}
+
+int message_binary_part_deserialize(pool_t pool, const void *data, size_t size,
+ struct message_binary_part **parts_r)
+{
+ const uint8_t *p = data, *end = p + size;
+ uint64_t n1, n2, n3, n4;
+ struct message_binary_part *part = NULL, *prev_part = NULL;
+
+ while (p != end) {
+ part = p_new(pool, struct message_binary_part, 1);
+ part->next = prev_part;
+ prev_part = part;
+ if (numpack_decode(&p, end, &n1) < 0 ||
+ numpack_decode(&p, end, &n2) < 0 ||
+ numpack_decode(&p, end, &n3) < 0 ||
+ numpack_decode(&p, end, &n4) < 0 ||
+ n4 > UINT_MAX)
+ return -1;
+ part->physical_pos = n1;
+ part->binary_hdr_size = n2;
+ part->binary_body_size = n3;
+ part->binary_body_lines_count = n4;
+ }
+ *parts_r = part;
+ return 0;
+}
diff --git a/src/lib-mail/message-binary-part.h b/src/lib-mail/message-binary-part.h
new file mode 100644
index 0000000..f30033a
--- /dev/null
+++ b/src/lib-mail/message-binary-part.h
@@ -0,0 +1,29 @@
+#ifndef MESSAGE_BINARY_PART_H
+#define MESSAGE_BINARY_PART_H
+
+struct message_binary_part {
+ struct message_binary_part *next;
+
+ /* Absolute position from beginning of message. This can be used to
+ match the part to struct message_part. */
+ uoff_t physical_pos;
+ /* Decoded binary header/body size. The binary header size may differ
+ from message_part's, because Content-Transfer-Encoding is changed to
+ "binary". */
+ uoff_t binary_hdr_size;
+ uoff_t binary_body_size;
+ /* BODYSTRUCTURE for text/ and message/rfc822 parts includes lines
+ count. Decoding may change these numbers. */
+ unsigned int binary_body_lines_count;
+};
+
+/* Serialize message binary_part. */
+void message_binary_part_serialize(const struct message_binary_part *parts,
+ buffer_t *dest);
+
+/* Generate struct message_binary_part from serialized data. Returns 0 if ok,
+ -1 if any problems are detected. */
+int message_binary_part_deserialize(pool_t pool, const void *data, size_t size,
+ struct message_binary_part **parts_r);
+
+#endif
diff --git a/src/lib-mail/message-date.c b/src/lib-mail/message-date.c
new file mode 100644
index 0000000..7a7529a
--- /dev/null
+++ b/src/lib-mail/message-date.c
@@ -0,0 +1,283 @@
+/* Copyright (c) 2002-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "str.h"
+#include "utc-offset.h"
+#include "utc-mktime.h"
+#include "rfc822-parser.h"
+#include "message-date.h"
+
+#include <ctype.h>
+
+/* RFC specifies ':' as the only allowed separator,
+ but be forgiving also for some broken ones */
+#define IS_TIME_SEP(c) \
+ ((c) == ':' || (c) == '.')
+
+struct message_date_parser_context {
+ struct rfc822_parser_context parser;
+ string_t *str;
+};
+
+static const char *month_names[] = {
+ "Jan", "Feb", "Mar", "Apr", "May", "Jun",
+ "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"
+};
+
+static const char *weekday_names[] = {
+ "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"
+};
+
+static int parse_timezone(const unsigned char *str, size_t len)
+{
+ int offset;
+ char chr;
+
+ if (len == 5 && (*str == '+' || *str == '-')) {
+ /* numeric offset */
+ if (!i_isdigit(str[1]) || !i_isdigit(str[2]) ||
+ !i_isdigit(str[3]) || !i_isdigit(str[4]))
+ return 0;
+
+ offset = ((str[1]-'0') * 10 + (str[2]-'0')) * 60 +
+ (str[3]-'0') * 10 + (str[4]-'0');
+ return *str == '+' ? offset : -offset;
+ }
+
+ if (len == 1) {
+ /* military zone - handle them the correct way, not as
+ RFC822 says. RFC2822 though suggests that they'd be
+ considered as unspecified.. */
+ chr = i_toupper(*str);
+ if (chr < 'J')
+ return (*str-'A'+1) * 60;
+ if (chr == 'J')
+ return 0;
+ if (chr <= 'M')
+ return (*str-'A') * 60;
+ if (chr < 'Z')
+ return ('M'-*str) * 60;
+ return 0;
+ }
+
+ if (len == 2 && i_toupper(str[0]) == 'U' && i_toupper(str[1]) == 'T') {
+ /* UT - Universal Time */
+ return 0;
+ }
+
+ if (len == 3) {
+ /* GMT | [ECMP][DS]T */
+ if (str[2] != 'T')
+ return 0;
+
+ switch (i_toupper(*str)) {
+ case 'E':
+ offset = -5 * 60;
+ break;
+ case 'C':
+ offset = -6 * 60;
+ break;
+ case 'M':
+ offset = -7 * 60;
+ break;
+ case 'P':
+ offset = -8 * 60;
+ break;
+ default:
+ /* GMT and others */
+ return 0;
+ }
+
+ if (i_toupper(str[1]) == 'D')
+ return offset + 60;
+ if (i_toupper(str[1]) == 'S')
+ return offset;
+ }
+
+ return 0;
+}
+
+static int next_token(struct message_date_parser_context *ctx,
+ const unsigned char **value, size_t *value_len)
+{
+ int ret;
+
+ str_truncate(ctx->str, 0);
+ ret = ctx->parser.data >= ctx->parser.end ? 0 :
+ rfc822_parse_atom(&ctx->parser, ctx->str);
+
+ *value = str_data(ctx->str);
+ *value_len = str_len(ctx->str);
+ return ret < 0 ? -1 : *value_len > 0;
+}
+
+static bool
+message_date_parser_tokens(struct message_date_parser_context *ctx,
+ time_t *timestamp_r, int *timezone_offset_r)
+{
+ struct tm tm;
+ const unsigned char *value;
+ size_t i, len;
+ int ret;
+
+ /* [weekday_name "," ] dd month_name [yy]yy hh:mi[:ss] timezone */
+ i_zero(&tm);
+
+ rfc822_skip_lwsp(&ctx->parser);
+
+ /* skip the optional weekday */
+ if (next_token(ctx, &value, &len) <= 0)
+ return FALSE;
+ if (len == 3) {
+ if (*ctx->parser.data != ',')
+ return FALSE;
+ ctx->parser.data++;
+ rfc822_skip_lwsp(&ctx->parser);
+
+ if (next_token(ctx, &value, &len) <= 0)
+ return FALSE;
+ }
+
+ /* dd */
+ if (len < 1 || len > 2 || !i_isdigit(value[0]))
+ return FALSE;
+
+ tm.tm_mday = value[0]-'0';
+ if (len == 2) {
+ if (!i_isdigit(value[1]))
+ return FALSE;
+ tm.tm_mday = (tm.tm_mday * 10) + (value[1]-'0');
+ }
+
+ /* month name */
+ if (next_token(ctx, &value, &len) <= 0 || len < 3)
+ return FALSE;
+
+ for (i = 0; i < 12; i++) {
+ if (i_memcasecmp(month_names[i], value, 3) == 0) {
+ tm.tm_mon = i;
+ break;
+ }
+ }
+ if (i == 12)
+ return FALSE;
+
+ /* [yy]yy */
+ if (next_token(ctx, &value, &len) <= 0 || (len != 2 && len != 4))
+ return FALSE;
+
+ for (i = 0; i < len; i++) {
+ if (!i_isdigit(value[i]))
+ return FALSE;
+ tm.tm_year = tm.tm_year * 10 + (value[i]-'0');
+ }
+
+ if (len == 2) {
+ /* two digit year, assume 1970+ */
+ if (tm.tm_year < 70)
+ tm.tm_year += 100;
+ } else {
+ if (tm.tm_year < 1900)
+ return FALSE;
+ tm.tm_year -= 1900;
+ }
+
+ /* hh, allow also single digit */
+ if (next_token(ctx, &value, &len) <= 0 ||
+ len < 1 || len > 2 || !i_isdigit(value[0]))
+ return FALSE;
+ tm.tm_hour = value[0]-'0';
+ if (len == 2) {
+ if (!i_isdigit(value[1]))
+ return FALSE;
+ tm.tm_hour = tm.tm_hour * 10 + (value[1]-'0');
+ }
+
+ /* :mm (may be the last token) */
+ if (!IS_TIME_SEP(*ctx->parser.data))
+ return FALSE;
+ ctx->parser.data++;
+ rfc822_skip_lwsp(&ctx->parser);
+
+ if (next_token(ctx, &value, &len) < 0 || len != 2 ||
+ !i_isdigit(value[0]) || !i_isdigit(value[1]))
+ return FALSE;
+ tm.tm_min = (value[0]-'0') * 10 + (value[1]-'0');
+
+ /* [:ss] */
+ if (ctx->parser.data < ctx->parser.end &&
+ IS_TIME_SEP(*ctx->parser.data)) {
+ ctx->parser.data++;
+ rfc822_skip_lwsp(&ctx->parser);
+
+ if (next_token(ctx, &value, &len) <= 0 || len != 2 ||
+ !i_isdigit(value[0]) || !i_isdigit(value[1]))
+ return FALSE;
+ tm.tm_sec = (value[0]-'0') * 10 + (value[1]-'0');
+ }
+
+ if ((ret = next_token(ctx, &value, &len)) < 0)
+ return FALSE;
+ if (ret == 0) {
+ /* missing timezone */
+ *timezone_offset_r = 0;
+ } else {
+ /* timezone. invalid timezones are treated as GMT, because
+ we may not know all the possible timezones that are used
+ and it's better to give at least a mostly correct reply.
+ FIXME: perhaps some different strict version of this
+ function would be useful? */
+ *timezone_offset_r = parse_timezone(value, len);
+ }
+
+ tm.tm_isdst = -1;
+ *timestamp_r = utc_mktime(&tm);
+ if (*timestamp_r == (time_t)-1)
+ return FALSE;
+
+ *timestamp_r -= *timezone_offset_r * 60;
+
+ return TRUE;
+}
+
+bool message_date_parse(const unsigned char *data, size_t size,
+ time_t *timestamp_r, int *timezone_offset_r)
+{
+ bool success;
+
+ T_BEGIN {
+ struct message_date_parser_context ctx;
+
+ rfc822_parser_init(&ctx.parser, data, size, NULL);
+ ctx.str = t_str_new(128);
+ success = message_date_parser_tokens(&ctx, timestamp_r,
+ timezone_offset_r);
+ rfc822_parser_deinit(&ctx.parser);
+ } T_END;
+
+ return success;
+}
+
+const char *message_date_create(time_t timestamp)
+{
+ struct tm *tm;
+ int offset;
+ bool negative;
+
+ tm = localtime(&timestamp);
+ offset = utc_offset(tm, timestamp);
+ if (offset >= 0)
+ negative = FALSE;
+ else {
+ negative = TRUE;
+ offset = -offset;
+ }
+
+ return t_strdup_printf("%s, %02d %s %04d %02d:%02d:%02d %c%02d%02d",
+ weekday_names[tm->tm_wday],
+ tm->tm_mday,
+ month_names[tm->tm_mon],
+ tm->tm_year+1900,
+ tm->tm_hour, tm->tm_min, tm->tm_sec,
+ negative ? '-' : '+', offset / 60, offset % 60);
+}
diff --git a/src/lib-mail/message-date.h b/src/lib-mail/message-date.h
new file mode 100644
index 0000000..fb1364b
--- /dev/null
+++ b/src/lib-mail/message-date.h
@@ -0,0 +1,12 @@
+#ifndef MESSAGE_DATE_H
+#define MESSAGE_DATE_H
+
+/* Parses RFC2822 date/time string. timezone_offset is filled with the
+ timezone's difference to UTC in minutes. */
+bool message_date_parse(const unsigned char *data, size_t size,
+ time_t *timestamp_r, int *timezone_offset_r);
+
+/* Create RFC2822 date/time string from given time in local timezone. */
+const char *message_date_create(time_t timestamp);
+
+#endif
diff --git a/src/lib-mail/message-decoder.c b/src/lib-mail/message-decoder.c
new file mode 100644
index 0000000..845b3d5
--- /dev/null
+++ b/src/lib-mail/message-decoder.c
@@ -0,0 +1,390 @@
+/* Copyright (c) 2006-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "buffer.h"
+#include "base64.h"
+#include "str.h"
+#include "unichar.h"
+#include "charset-utf8.h"
+#include "qp-decoder.h"
+#include "rfc822-parser.h"
+#include "rfc2231-parser.h"
+#include "message-parser.h"
+#include "message-header-decode.h"
+#include "message-decoder.h"
+
+struct message_decoder_context {
+ enum message_decoder_flags flags;
+ normalizer_func_t *normalizer;
+ struct message_part *prev_part;
+
+ struct message_header_line hdr;
+ buffer_t *buf, *buf2;
+
+ char *charset_trans_charset;
+ struct charset_translation *charset_trans;
+ char translation_buf[CHARSET_MAX_PENDING_BUF_SIZE];
+ size_t translation_size;
+
+ struct qp_decoder *qp;
+ struct base64_decoder base64_decoder;
+
+ char *content_type, *content_charset;
+ enum message_cte message_cte;
+
+ bool binary_input:1;
+};
+
+static void
+message_decode_body_init_charset(struct message_decoder_context *ctx,
+ struct message_part *part);
+
+struct message_decoder_context *
+message_decoder_init(normalizer_func_t *normalizer,
+ enum message_decoder_flags flags)
+{
+ struct message_decoder_context *ctx;
+
+ ctx = i_new(struct message_decoder_context, 1);
+ ctx->flags = flags;
+ ctx->normalizer = normalizer;
+ ctx->buf = buffer_create_dynamic(default_pool, 8192);
+ ctx->buf2 = buffer_create_dynamic(default_pool, 8192);
+ base64_decode_init(&ctx->base64_decoder, &base64_scheme, 0);
+ return ctx;
+}
+
+void message_decoder_deinit(struct message_decoder_context **_ctx)
+{
+ struct message_decoder_context *ctx = *_ctx;
+
+ *_ctx = NULL;
+
+ if (ctx->charset_trans != NULL)
+ charset_to_utf8_end(&ctx->charset_trans);
+ if (ctx->qp != NULL)
+ qp_decoder_deinit(&ctx->qp);
+
+ buffer_free(&ctx->buf);
+ buffer_free(&ctx->buf2);
+ i_free(ctx->charset_trans_charset);
+ i_free(ctx->content_type);
+ i_free(ctx->content_charset);
+ i_free(ctx);
+}
+
+void message_decoder_set_return_binary(struct message_decoder_context *ctx,
+ bool set)
+{
+ if (set)
+ ctx->flags |= MESSAGE_DECODER_FLAG_RETURN_BINARY;
+ else
+ ctx->flags &= ENUM_NEGATE(MESSAGE_DECODER_FLAG_RETURN_BINARY);
+ message_decode_body_init_charset(ctx, ctx->prev_part);
+}
+
+enum message_cte message_decoder_parse_cte(const struct message_header_line *hdr)
+{
+ struct rfc822_parser_context parser;
+ enum message_cte message_cte;
+ string_t *value;
+
+ value = t_str_new(64);
+ rfc822_parser_init(&parser, hdr->full_value, hdr->full_value_len, NULL);
+
+ rfc822_skip_lwsp(&parser);
+
+ /* Ensure we do not accidentically accept confused values like
+ 'base64 binary' or embedded NULs */
+ if (rfc822_parse_mime_token(&parser, value) == 1) {
+ rfc822_skip_lwsp(&parser);
+ /* RFC 2045 does not permit parameters for CTE,
+ but in case someone uses them, we accept
+ parameter separator ';' to be lenient. */
+ if (*parser.data != ';')
+ return MESSAGE_CTE_UNKNOWN;
+ }
+
+ message_cte = MESSAGE_CTE_UNKNOWN;
+ switch (str_len(value)) {
+ case 4:
+ if (i_memcasecmp(str_data(value), "7bit", 4) == 0 ||
+ i_memcasecmp(str_data(value), "8bit", 4) == 0)
+ message_cte = MESSAGE_CTE_78BIT;
+ break;
+ case 6:
+ if (i_memcasecmp(str_data(value), "base64", 6) == 0)
+ message_cte = MESSAGE_CTE_BASE64;
+ else if (i_memcasecmp(str_data(value), "binary", 6) == 0)
+ message_cte = MESSAGE_CTE_BINARY;
+ break;
+ case 16:
+ if (i_memcasecmp(str_data(value), "quoted-printable", 16) == 0)
+ message_cte = MESSAGE_CTE_QP;
+ break;
+ }
+ rfc822_parser_deinit(&parser);
+ return message_cte;
+}
+
+static void
+parse_content_type(struct message_decoder_context *ctx,
+ struct message_header_line *hdr)
+{
+ struct rfc822_parser_context parser;
+ const char *const *results;
+ string_t *str;
+ int ret;
+
+ if (ctx->content_type != NULL)
+ return;
+
+ rfc822_parser_init(&parser, hdr->full_value, hdr->full_value_len, NULL);
+ rfc822_skip_lwsp(&parser);
+ str = t_str_new(64);
+ ret = rfc822_parse_content_type(&parser, str);
+ ctx->content_type = i_strdup(str_c(str));
+ if (ret < 0) {
+ rfc822_parser_deinit(&parser);
+ return;
+ }
+
+ rfc2231_parse(&parser, &results);
+ for (; *results != NULL; results += 2) {
+ if (strcasecmp(results[0], "charset") == 0) {
+ ctx->content_charset = i_strdup(results[1]);
+ break;
+ }
+ }
+ rfc822_parser_deinit(&parser);
+}
+
+static bool message_decode_header(struct message_decoder_context *ctx,
+ struct message_header_line *hdr,
+ struct message_block *output)
+{
+ size_t value_len;
+
+ if (hdr->continues) {
+ hdr->use_full_value = TRUE;
+ return FALSE;
+ }
+
+ T_BEGIN {
+ if (hdr->name_len == 12 &&
+ strcasecmp(hdr->name, "Content-Type") == 0)
+ parse_content_type(ctx, hdr);
+ if (hdr->name_len == 25 &&
+ strcasecmp(hdr->name, "Content-Transfer-Encoding") == 0)
+ ctx->message_cte = message_decoder_parse_cte(hdr);
+ } T_END;
+
+ buffer_set_used_size(ctx->buf, 0);
+ message_header_decode_utf8(hdr->full_value, hdr->full_value_len,
+ ctx->buf, ctx->normalizer);
+ value_len = ctx->buf->used;
+
+ if (ctx->normalizer != NULL) {
+ (void)ctx->normalizer(hdr->name, hdr->name_len, ctx->buf);
+ buffer_append_c(ctx->buf, '\0');
+ } else {
+ if (!uni_utf8_get_valid_data((const unsigned char *)hdr->name,
+ hdr->name_len, ctx->buf))
+ buffer_append_c(ctx->buf, '\0');
+ }
+
+ ctx->hdr = *hdr;
+ ctx->hdr.full_value = ctx->buf->data;
+ ctx->hdr.full_value_len = value_len;
+ ctx->hdr.value_len = 0;
+ if (ctx->buf->used != value_len) {
+ ctx->hdr.name = CONST_PTR_OFFSET(ctx->buf->data,
+ ctx->hdr.full_value_len);
+ ctx->hdr.name_len = ctx->buf->used - 1 - value_len;
+ }
+
+ output->hdr = &ctx->hdr;
+ return TRUE;
+}
+
+static void translation_buf_decode(struct message_decoder_context *ctx,
+ const unsigned char **data, size_t *size)
+{
+ unsigned char trans_buf[CHARSET_MAX_PENDING_BUF_SIZE+1];
+ size_t data_wanted, skip;
+ size_t trans_size, orig_size;
+
+ /* @UNSAFE: move the previously untranslated bytes to trans_buf
+ and see if we have now enough data to get the next character
+ translated */
+ memcpy(trans_buf, ctx->translation_buf, ctx->translation_size);
+ data_wanted = sizeof(trans_buf) - ctx->translation_size;
+ if (data_wanted > *size)
+ data_wanted = *size;
+ memcpy(trans_buf + ctx->translation_size, *data, data_wanted);
+
+ orig_size = trans_size = ctx->translation_size + data_wanted;
+ (void)charset_to_utf8(ctx->charset_trans, trans_buf,
+ &trans_size, ctx->buf2);
+
+ if (trans_size <= ctx->translation_size) {
+ /* need more data to finish the translation. */
+ i_assert(orig_size < CHARSET_MAX_PENDING_BUF_SIZE);
+ memcpy(ctx->translation_buf, trans_buf, orig_size);
+ ctx->translation_size = orig_size;
+ *data += *size;
+ *size = 0;
+ return;
+ }
+ skip = trans_size - ctx->translation_size;
+
+ i_assert(*size >= skip);
+ *data += skip;
+ *size -= skip;
+
+ ctx->translation_size = 0;
+}
+
+static void
+message_decode_body_init_charset(struct message_decoder_context *ctx,
+ struct message_part *part)
+{
+ ctx->binary_input = ctx->content_charset == NULL &&
+ (ctx->flags & MESSAGE_DECODER_FLAG_RETURN_BINARY) != 0 &&
+ (part->flags & (MESSAGE_PART_FLAG_TEXT |
+ MESSAGE_PART_FLAG_MESSAGE_RFC822)) == 0;
+
+ if (ctx->binary_input)
+ return;
+
+ if (ctx->charset_trans != NULL && ctx->content_charset != NULL &&
+ strcasecmp(ctx->content_charset, ctx->charset_trans_charset) == 0) {
+ /* already have the correct translation selected */
+ charset_to_utf8_reset(ctx->charset_trans);
+ return;
+ }
+
+ if (ctx->charset_trans != NULL)
+ charset_to_utf8_end(&ctx->charset_trans);
+ i_free_and_null(ctx->charset_trans_charset);
+
+ ctx->charset_trans_charset = i_strdup(ctx->content_charset != NULL ?
+ ctx->content_charset : "UTF-8");
+ if (charset_to_utf8_begin(ctx->charset_trans_charset, ctx->normalizer,
+ &ctx->charset_trans) < 0)
+ ctx->charset_trans = charset_utf8_to_utf8_begin(ctx->normalizer);
+}
+
+static bool message_decode_body(struct message_decoder_context *ctx,
+ struct message_block *input,
+ struct message_block *output)
+{
+ const unsigned char *data = NULL;
+ size_t pos, size = 0;
+ const char *error;
+
+ switch (ctx->message_cte) {
+ case MESSAGE_CTE_UNKNOWN:
+ /* just skip this body */
+ return FALSE;
+
+ case MESSAGE_CTE_78BIT:
+ case MESSAGE_CTE_BINARY:
+ data = input->data;
+ size = input->size;
+ break;
+ case MESSAGE_CTE_QP: {
+ buffer_set_used_size(ctx->buf, 0);
+ if (ctx->qp == NULL)
+ ctx->qp = qp_decoder_init(ctx->buf);
+ (void)qp_decoder_more(ctx->qp, input->data, input->size,
+ &pos, &error);
+ data = ctx->buf->data;
+ size = ctx->buf->used;
+ break;
+ }
+ case MESSAGE_CTE_BASE64:
+ buffer_set_used_size(ctx->buf, 0);
+ if (!base64_decode_is_finished(&ctx->base64_decoder)) {
+ if (base64_decode_more(&ctx->base64_decoder,
+ input->data, input->size,
+ &pos, ctx->buf) <= 0) {
+ /* ignore the rest of the input in this
+ MIME part */
+ (void)base64_decode_finish(&ctx->base64_decoder);
+ }
+ }
+ data = ctx->buf->data;
+ size = ctx->buf->used;
+ break;
+ }
+
+ if (ctx->binary_input) {
+ output->data = data;
+ output->size = size;
+ } else {
+ buffer_set_used_size(ctx->buf2, 0);
+ if (ctx->translation_size != 0)
+ translation_buf_decode(ctx, &data, &size);
+
+ pos = size;
+ (void)charset_to_utf8(ctx->charset_trans,
+ data, &pos, ctx->buf2);
+ if (pos != size) {
+ ctx->translation_size = size - pos;
+ i_assert(ctx->translation_size <=
+ sizeof(ctx->translation_buf));
+ memcpy(ctx->translation_buf, data + pos,
+ ctx->translation_size);
+ }
+ output->data = ctx->buf2->data;
+ output->size = ctx->buf2->used;
+ }
+
+ output->hdr = NULL;
+ return TRUE;
+}
+
+bool message_decoder_decode_next_block(struct message_decoder_context *ctx,
+ struct message_block *input,
+ struct message_block *output)
+{
+ if (input->part != ctx->prev_part) {
+ /* MIME part changed. */
+ message_decoder_decode_reset(ctx);
+ }
+
+ output->part = input->part;
+ ctx->prev_part = input->part;
+
+ if (input->hdr != NULL) {
+ output->size = 0;
+ return message_decode_header(ctx, input->hdr, output);
+ } else if (input->size != 0)
+ return message_decode_body(ctx, input, output);
+ else {
+ output->hdr = NULL;
+ output->size = 0;
+ message_decode_body_init_charset(ctx, input->part);
+ return TRUE;
+ }
+}
+
+const char *
+message_decoder_current_content_type(struct message_decoder_context *ctx)
+{
+ return ctx->content_type;
+}
+
+void message_decoder_decode_reset(struct message_decoder_context *ctx)
+{
+ const char *error;
+
+ base64_decode_reset(&ctx->base64_decoder);
+
+ if (ctx->qp != NULL)
+ (void)qp_decoder_finish(ctx->qp, &error);
+ i_free_and_null(ctx->content_type);
+ i_free_and_null(ctx->content_charset);
+ ctx->message_cte = MESSAGE_CTE_78BIT;
+}
diff --git a/src/lib-mail/message-decoder.h b/src/lib-mail/message-decoder.h
new file mode 100644
index 0000000..9928ae6
--- /dev/null
+++ b/src/lib-mail/message-decoder.h
@@ -0,0 +1,54 @@
+#ifndef MESSAGE_DECODER_H
+#define MESSAGE_DECODER_H
+
+#include "unichar.h"
+
+struct message_header_line;
+
+enum message_cte {
+ MESSAGE_CTE_UNKNOWN = 0,
+ MESSAGE_CTE_78BIT,
+ MESSAGE_CTE_BINARY,
+ MESSAGE_CTE_QP,
+ MESSAGE_CTE_BASE64
+};
+
+enum message_decoder_flags {
+ /* Return binary MIME parts as-is without any conversion. */
+ MESSAGE_DECODER_FLAG_RETURN_BINARY = 0x02
+};
+
+struct message_block;
+
+/* Decode message's contents as UTF-8, both the headers and the MIME bodies.
+ The bodies are decoded from quoted-printable and base64 formats if needed. */
+struct message_decoder_context *
+message_decoder_init(normalizer_func_t *normalizer,
+ enum message_decoder_flags flags);
+void message_decoder_deinit(struct message_decoder_context **ctx);
+
+/* Change the MESSAGE_DECODER_FLAG_RETURN_BINARY flag */
+void message_decoder_set_return_binary(struct message_decoder_context *ctx,
+ bool set);
+
+/* Decode input and return decoded output. Headers are returned only in their
+ full multiline forms.
+
+ Returns TRUE if output is given, FALSE if more data is needed. If the input
+ ends in a partial character, it's returned in the next output. */
+bool message_decoder_decode_next_block(struct message_decoder_context *ctx,
+ struct message_block *input,
+ struct message_block *output);
+
+/* Returns the parsed Content-Type of the current MIME part. If there is no
+ explicit Content-Type, returns NULL. */
+const char *
+message_decoder_current_content_type(struct message_decoder_context *ctx);
+
+/* Call whenever message changes */
+void message_decoder_decode_reset(struct message_decoder_context *ctx);
+
+/* Decode Content-Transfer-Encoding header. */
+enum message_cte message_decoder_parse_cte(const struct message_header_line *hdr);
+
+#endif
diff --git a/src/lib-mail/message-header-decode.c b/src/lib-mail/message-header-decode.c
new file mode 100644
index 0000000..18f6ca2
--- /dev/null
+++ b/src/lib-mail/message-header-decode.c
@@ -0,0 +1,188 @@
+/* Copyright (c) 2002-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "base64.h"
+#include "buffer.h"
+#include "unichar.h"
+#include "charset-utf8.h"
+#include "quoted-printable.h"
+#include "message-header-decode.h"
+
+static size_t
+message_header_decode_encoded(const unsigned char *data, size_t size,
+ buffer_t *decodebuf, size_t *charsetlen_r)
+{
+#define QCOUNT 3
+ unsigned int num = 0;
+ size_t i, start_pos[QCOUNT] = {0, 0, 0};
+
+ /* data should contain "charset?encoding?text?=" */
+ for (i = 0; i < size; i++) {
+ if (data[i] == '?') {
+ start_pos[num++] = i;
+ if (num == QCOUNT)
+ break;
+ }
+ }
+
+ if (i+1 >= size || data[i+1] != '=') {
+ /* invalid block */
+ return 0;
+ }
+
+ i_assert(num == QCOUNT);
+
+ buffer_append(decodebuf, data, start_pos[0]);
+ buffer_append_c(decodebuf, '\0');
+ *charsetlen_r = decodebuf->used;
+
+ switch (data[start_pos[0]+1]) {
+ case 'q':
+ case 'Q':
+ if (quoted_printable_q_decode(data + start_pos[1] + 1,
+ start_pos[2] - start_pos[1] - 1,
+ decodebuf) < 0) {
+ /* we skipped over some invalid data */
+ }
+ break;
+ case 'b':
+ case 'B':
+ if (base64_decode(data + start_pos[1] + 1,
+ start_pos[2] - start_pos[1] - 1,
+ NULL, decodebuf) < 0) {
+ /* contains invalid data. show what we got so far. */
+ }
+ break;
+ default:
+ /* unknown encoding */
+ return 0;
+ }
+
+ return start_pos[2] + 2;
+}
+
+static bool is_only_lwsp(const unsigned char *data, size_t size)
+{
+ size_t i;
+
+ for (i = 0; i < size; i++) {
+ if (!(data[i] == ' ' || data[i] == '\t' ||
+ data[i] == '\r' || data[i] == '\n'))
+ return FALSE;
+ }
+ return TRUE;
+}
+
+void message_header_decode(const unsigned char *data, size_t size,
+ message_header_decode_callback_t *callback,
+ void *context)
+{
+ buffer_t *decodebuf = NULL;
+ size_t charsetlen = 0;
+ size_t pos, start_pos, ret;
+
+ /* =?charset?Q|B?text?= */
+ start_pos = 0;
+ for (pos = 0; pos + 1 < size; ) {
+ if (data[pos] != '=' || data[pos+1] != '?') {
+ pos++;
+ continue;
+ }
+
+ /* encoded string beginning */
+ if (pos != start_pos &&
+ !is_only_lwsp(data+start_pos, pos-start_pos)) {
+ /* send the unencoded data so far */
+ if (!callback(data + start_pos, pos - start_pos,
+ NULL, context)) {
+ start_pos = size;
+ break;
+ }
+ }
+
+ if (decodebuf == NULL) {
+ decodebuf = buffer_create_dynamic(default_pool,
+ size - pos);
+ } else {
+ buffer_set_used_size(decodebuf, 0);
+ }
+
+ pos += 2;
+ ret = message_header_decode_encoded(data + pos, size - pos,
+ decodebuf, &charsetlen);
+ if (ret == 0) {
+ start_pos = pos-2;
+ continue;
+ }
+ pos += ret;
+
+ if (decodebuf->used > charsetlen) {
+ /* decodebuf contains <charset> NUL <text> */
+ if (!callback(CONST_PTR_OFFSET(decodebuf->data,
+ charsetlen),
+ decodebuf->used - charsetlen,
+ decodebuf->data, context)) {
+ start_pos = size;
+ break;
+ }
+ }
+
+ start_pos = pos;
+ }
+
+ if (size != start_pos) {
+ i_assert(size > start_pos);
+ (void)callback(data + start_pos, size - start_pos,
+ NULL, context);
+ }
+ buffer_free(&decodebuf);
+}
+
+struct decode_utf8_context {
+ buffer_t *dest;
+ normalizer_func_t *normalizer;
+ bool changed:1;
+};
+
+static bool
+decode_utf8_callback(const unsigned char *data, size_t size,
+ const char *charset, void *context)
+{
+ struct decode_utf8_context *ctx = context;
+ struct charset_translation *t;
+
+ if (charset == NULL || charset_is_utf8(charset)) {
+ /* ASCII / UTF-8 */
+ if (ctx->normalizer != NULL) {
+ (void)ctx->normalizer(data, size, ctx->dest);
+ } else {
+ if (uni_utf8_get_valid_data(data, size, ctx->dest))
+ buffer_append(ctx->dest, data, size);
+ }
+ return TRUE;
+ }
+
+ if (charset_to_utf8_begin(charset, ctx->normalizer, &t) < 0) {
+ /* data probably still contains some valid ASCII characters.
+ append them. */
+ if (uni_utf8_get_valid_data(data, size, ctx->dest))
+ buffer_append(ctx->dest, data, size);
+ return TRUE;
+ }
+
+ /* ignore any errors */
+ (void)charset_to_utf8(t, data, &size, ctx->dest);
+ charset_to_utf8_end(&t);
+ return TRUE;
+}
+
+void message_header_decode_utf8(const unsigned char *data, size_t size,
+ buffer_t *dest, normalizer_func_t *normalizer)
+{
+ struct decode_utf8_context ctx;
+
+ i_zero(&ctx);
+ ctx.dest = dest;
+ ctx.normalizer = normalizer;
+ message_header_decode(data, size, decode_utf8_callback, &ctx);
+}
diff --git a/src/lib-mail/message-header-decode.h b/src/lib-mail/message-header-decode.h
new file mode 100644
index 0000000..5f7a5ad
--- /dev/null
+++ b/src/lib-mail/message-header-decode.h
@@ -0,0 +1,22 @@
+#ifndef MESSAGE_HEADER_DECODE_H
+#define MESSAGE_HEADER_DECODE_H
+
+#include "unichar.h"
+
+/* Return FALSE if you wish to stop decoding. charset is NULL when it's not
+ RFC2047-encoded. */
+typedef bool message_header_decode_callback_t(const unsigned char *data,
+ size_t size, const char *charset,
+ void *context);
+
+/* Decode RFC2047 encoded words. Call specified function for each
+ decoded block. */
+void message_header_decode(const unsigned char *data, size_t size,
+ message_header_decode_callback_t *callback,
+ void *context);
+
+/* Append decoded RFC2047 header as UTF-8 to given buffer. */
+void message_header_decode_utf8(const unsigned char *data, size_t size,
+ buffer_t *dest, normalizer_func_t *normalizer);
+
+#endif
diff --git a/src/lib-mail/message-header-encode.c b/src/lib-mail/message-header-encode.c
new file mode 100644
index 0000000..a9410a8
--- /dev/null
+++ b/src/lib-mail/message-header-encode.c
@@ -0,0 +1,406 @@
+/* Copyright (c) 2009-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "str.h"
+#include "unichar.h"
+#include "base64.h"
+#include "message-header-encode.h"
+
+#define MIME_WRAPPER_LEN (strlen("=?utf-8?q?""?="))
+#define MIME_MAX_LINE_LEN 76
+
+#define IS_LWSP(c) \
+ ((c) == ' ' || (c) == '\t' || (c) == '\n')
+
+static bool
+input_idx_need_encoding(const unsigned char *input, size_t i, size_t len)
+{
+ switch (input[i]) {
+ case '\r':
+ if (i+1 == len || input[i+1] != '\n')
+ return TRUE;
+ i++;
+ /* fall through - verify the LF as well */
+ case '\n':
+ if (i+1 == len) {
+ /* trailing LF - we need to drop it */
+ return TRUE;
+ }
+ i_assert(i+1 < len);
+ if (input[i+1] != '\t' && input[i+1] != ' ') {
+ /* LF not followed by whitespace - we need to
+ add the whitespace */
+ return TRUE;
+ }
+ break;
+ case '\t':
+ /* TAB doesn't need to be encoded */
+ break;
+ case '=':
+ /* <LWSP>=? - we need to check backwards a bit to see if
+ there is LWSP (note that we don't want to return TRUE for
+ the LWSP itself yet, so we need to do this backwards
+ check) */
+ if ((i == 0 || IS_LWSP(input[i-1])) && i+2 <= len &&
+ memcmp(input + i, "=?", 2) == 0)
+ return TRUE;
+ break;
+ default:
+ /* 8bit chars */
+ if ((input[i] & 0x80) != 0)
+ return TRUE;
+ /* control chars */
+ if (input[i] < 32)
+ return TRUE;
+ break;
+ }
+ return FALSE;
+}
+
+void message_header_encode_q(const unsigned char *input, size_t len,
+ string_t *output, size_t first_line_len)
+{
+ static const unsigned char *rep_char =
+ (const unsigned char *)UNICODE_REPLACEMENT_CHAR_UTF8;
+ static const unsigned int rep_char_len =
+ UNICODE_REPLACEMENT_CHAR_UTF8_LEN;
+ size_t line_len_left;
+ bool invalid_char = FALSE;
+
+ if (len == 0)
+ return;
+
+ line_len_left = MIME_MAX_LINE_LEN - MIME_WRAPPER_LEN;
+
+ if (first_line_len >= MIME_MAX_LINE_LEN - MIME_WRAPPER_LEN - 3) {
+ str_append(output, "\n\t");
+ line_len_left--;
+ } else {
+ line_len_left -= first_line_len;
+ }
+
+ str_append(output, "=?utf-8?q?");
+ for (;;) {
+ unichar_t ch;
+ int nch = 1;
+ size_t n_in, n_out = 0, j;
+
+ /* Determine how many bytes are to be consumed from input and
+ written to output. */
+ switch (input[0]) {
+ case ' ':
+ /* Space is translated to a single '_'. */
+ n_out = 1;
+ n_in = 1;
+ break;
+ case '=':
+ case '?':
+ case '_':
+ /* Special characters are escaped. */
+ n_in = 1;
+ n_out = 3;
+ break;
+ default:
+ nch = uni_utf8_get_char_n(input, len, &ch);
+ if (nch <= 0) {
+ /* Invalid UTF-8 character */
+ n_in = 1;
+ if (!invalid_char) {
+ /* First octet of bad stuff; will emit
+ replacement character. */
+ n_out = rep_char_len * 3;
+ } else {
+ /* Emit only one replacement char for
+ a burst of bad stuff. */
+ n_out = 0;
+ }
+ } else if (nch > 1) {
+ /* Unicode characters are escaped as several
+ escape sequences for each octet. */
+ n_in = nch;
+ n_out = nch * 3;
+ } else if (ch < 0x20 || ch > 0x7e) {
+ /* Control characters are escaped. */
+ i_assert(ch < 0x80);
+ n_in = 1;
+ n_out = 3;
+ } else {
+ /* Other ASCII characters are written to output
+ directly. */
+ n_in = 1;
+ n_out = 1;
+ }
+ }
+ invalid_char = (nch <= 0);
+
+ /* Start a new line once unsufficient space is available to
+ write more to the current line. */
+ if (line_len_left < n_out) {
+ str_append(output, "?=\n\t=?utf-8?q?");
+ line_len_left = MIME_MAX_LINE_LEN -
+ MIME_WRAPPER_LEN - 1;
+ }
+
+ /* Encode the character */
+ if (input[0] == ' ') {
+ /* Write special escape sequence for space character */
+ str_append_c(output, '_');
+ } else if (invalid_char) {
+ /* Write replacement character for invalid UTF-8 code
+ point. */
+ for (j = 0; n_out > 0 && j < rep_char_len; j++)
+ str_printfa(output, "=%02X", rep_char[j]);
+ } else if (n_out > 1) {
+ /* Write one or more escape sequences for a special
+ character, a control character, or a valid UTF-8
+ code point. */
+ for (j = 0; j < n_in; j++)
+ str_printfa(output, "=%02X", input[j]);
+ } else {
+ /* Write other ASCII characters directly to output. */
+ str_append_c(output, input[0]);
+ }
+
+ /* Update sizes and pointers */
+ i_assert(len >= n_in);
+ line_len_left -= n_out;
+ input += n_in;
+ len -= n_in;
+
+ if (len == 0)
+ break;
+ }
+ str_append(output, "?=");
+}
+
+void message_header_encode_b(const unsigned char *input, size_t len,
+ string_t *output, size_t first_line_len)
+{
+ static const unsigned char *rep_char =
+ (const unsigned char *)UNICODE_REPLACEMENT_CHAR_UTF8;
+ static const unsigned int rep_char_len =
+ UNICODE_REPLACEMENT_CHAR_UTF8_LEN;
+ struct base64_encoder b64enc;
+ size_t line_len_left;
+
+ if (len == 0)
+ return;
+
+ line_len_left = MIME_MAX_LINE_LEN - MIME_WRAPPER_LEN;
+
+ if (first_line_len >= MIME_MAX_LINE_LEN - MIME_WRAPPER_LEN - 3) {
+ str_append(output, "\n\t");
+ line_len_left--;
+ } else {
+ line_len_left -= first_line_len;
+ }
+
+ str_append(output, "=?utf-8?b?");
+ base64_encode_init(&b64enc, &base64_scheme, 0, 0);
+ for (;;) {
+ unichar_t ch;
+ size_t space, max, old_bufsize, n_in, n_out;
+ int nch = 1;
+
+ /* Determine how many octets can be encoded on (the remainder
+ of) this line */
+ space = base64_encode_get_full_space(&b64enc, line_len_left);
+ max = I_MIN(space, len);
+
+ /* Check UTF-8 code points in the input and determine a proper
+ boundary for the end of this fragment if the encoded size
+ exceeds the maximum (remaining) line length. */
+ for (n_in = 0; n_in < max;) {
+ nch = uni_utf8_get_char_n(&input[n_in],
+ len - n_in, &ch);
+ if (nch <= 0)
+ break;
+ if ((n_in + nch) > max)
+ break;
+ n_in += nch;
+ }
+
+ /* Encode this fragment up until the maximum fragment size or
+ the first invalid UTF-8 code point in the input. */
+ if (n_in > 0) {
+ old_bufsize = output->used;
+ if (!base64_encode_more(&b64enc, input, n_in,
+ &n_in, output))
+ i_unreached();
+ n_out = output->used - old_bufsize;
+
+ /* Update sizes and pointers */
+ i_assert(len >= n_in);
+ i_assert(line_len_left >= n_out);
+ input += n_in;
+ len -= n_in;
+ line_len_left -= n_out;
+ }
+
+ /* Determine whether a repacement character needs to be written
+ and how much space there is left for it on the current line.
+ */
+ space = 0;
+ if (nch <= 0) {
+ space = base64_encode_get_full_space(
+ &b64enc, line_len_left);
+ }
+
+ /* Start a new line once insufficient space is available. */
+ if ((nch > 0 && len > 0) ||
+ (nch <= 0 && space < rep_char_len)) {
+ old_bufsize = output->used;
+ if (!base64_encode_finish(&b64enc, output))
+ i_unreached();
+ n_out = output->used - old_bufsize;
+ i_assert(line_len_left >= n_out);
+
+ str_append(output, "?=\n\t=?utf-8?b?");
+ line_len_left = MIME_MAX_LINE_LEN -
+ MIME_WRAPPER_LEN - 1;
+ base64_encode_reset(&b64enc);
+ }
+
+ /* Write replacement character if needed. */
+ n_in = 0;
+ n_out = 0;
+ if (nch <= 0) {
+ old_bufsize = output->used;
+ if (!base64_encode_more(&b64enc, rep_char, rep_char_len,
+ NULL, output))
+ i_unreached();
+
+ n_in = 1;
+ n_out = output->used - old_bufsize;
+
+ /* Skip more invalid characters in the input. */
+ for (; n_in < len; n_in++) {
+ nch = uni_utf8_get_char_n(&input[n_in],
+ len - n_in, &ch);
+ if (nch > 0)
+ break;
+ }
+ }
+
+ /* Update sizes and pointers */
+ i_assert(line_len_left >= n_out);
+ input += n_in;
+ len -= n_in;
+ line_len_left -= n_out;
+
+ if (len == 0)
+ break;
+ }
+ if (!base64_encode_finish(&b64enc, output))
+ i_unreached();
+ str_append(output, "?=");
+}
+
+void message_header_encode(const char *input, string_t *output)
+{
+ message_header_encode_data((const void *)input, strlen(input), output);
+}
+
+void message_header_encode_data(const unsigned char *input, size_t len,
+ string_t *output)
+{
+ size_t i, j, first_line_len, cur_line_len, last_idx;
+ size_t enc_chars, enc_len, base64_len, q_len;
+ const unsigned char *next_line_input;
+ size_t next_line_len = 0;
+ bool use_q, cr;
+
+ /* find the first word that needs encoding */
+ for (i = 0; i < len; i++) {
+ if (input_idx_need_encoding(input, i, len))
+ break;
+ }
+ if (i == len) {
+ /* no encoding necessary */
+ str_append_data(output, input, len);
+ return;
+ }
+ /* go back to the beginning of the word so it is fully encoded */
+ if (input[i] != '\r' && input[i] != '\n') {
+ while (i > 0 && !IS_LWSP(input[i-1]))
+ i--;
+ }
+
+ /* write the prefix */
+ str_append_data(output, input, i);
+ first_line_len = j = i;
+ while (j > 0 && input[j-1] != '\n') j--;
+ if (j != 0)
+ first_line_len = j;
+
+ input += i;
+ len -= i;
+
+ /* we'll encode data only up to the next LF, the rest is handled
+ recursively. */
+ next_line_input = memchr(input, '\n', len);
+ if (next_line_input != NULL) {
+ cur_line_len = next_line_input - input;
+ if (cur_line_len > 0 && input[cur_line_len-1] == '\r') {
+ cur_line_len--;
+ next_line_input = input + cur_line_len;
+ }
+ next_line_len = len - cur_line_len;
+ len = cur_line_len;
+ }
+
+ /* find the last word that needs encoding */
+ last_idx = 0; enc_chars = 0;
+ for (i = 0; i < len; i++) {
+ if (input_idx_need_encoding(input, i, len)) {
+ last_idx = i + 1;
+ enc_chars++;
+ }
+ }
+ while (last_idx < len && !IS_LWSP(input[last_idx]))
+ last_idx++;
+
+ /* figure out if we should use Q or B encoding. Prefer Q if it's not
+ too much larger. */
+ enc_len = last_idx;
+ base64_len = MAX_BASE64_ENCODED_SIZE(enc_len);
+ q_len = enc_len + enc_chars*3;
+ use_q = q_len*2/3 <= base64_len;
+
+ /* and do it */
+ if (enc_len == 0)
+ ;
+ else if (use_q)
+ message_header_encode_q(input, enc_len, output, first_line_len);
+ else
+ message_header_encode_b(input, enc_len, output, first_line_len);
+ str_append_data(output, input + last_idx, len - last_idx);
+
+ if (next_line_input != NULL) {
+ /* we're at [CR]LF */
+ i = 0;
+ if (next_line_input[0] == '\r') {
+ cr = TRUE;
+ i++;
+ } else {
+ cr = FALSE;
+ }
+ i_assert(next_line_input[i] == '\n');
+ if (++i == next_line_len)
+ return; /* drop trailing [CR]LF */
+
+ if (cr)
+ str_append_c(output, '\r');
+ str_append_c(output, '\n');
+
+ if (next_line_input[i] == ' ' || next_line_input[i] == '\t') {
+ str_append_c(output, next_line_input[i]);
+ i++;
+ } else {
+ /* make it valid folding whitespace by adding a TAB */
+ str_append_c(output, '\t');
+ }
+ message_header_encode_data(next_line_input+i, next_line_len-i,
+ output);
+ }
+}
diff --git a/src/lib-mail/message-header-encode.h b/src/lib-mail/message-header-encode.h
new file mode 100644
index 0000000..fdd3b19
--- /dev/null
+++ b/src/lib-mail/message-header-encode.h
@@ -0,0 +1,27 @@
+#ifndef MESSAGE_HEADER_ENCODE_H
+#define MESSAGE_HEADER_ENCODE_H
+
+/* Encode UTF-8 input into output wherever necessary using either Q or B
+ encoding depending on which takes less space (approximately). The encoded
+ output is split into multiple lines if necessary (max 76 chars/line).
+ Existing folding whitespace is preserved. Bare [CR]LF will be preserved by
+ adding a TAB after it to make it a valid folding whitespace. All the control
+ characters are encoded, including NUL, CR and LF. Sequences of one or more
+ invalid UTF-8 characters are replaced by a single Unicode replacement
+ character (U+fffd). */
+void message_header_encode(const char *input, string_t *output);
+void message_header_encode_data(const unsigned char *input, size_t len,
+ string_t *output);
+
+/* Encode the whole UTF-8 input using "Q" or "B" encoding into output.
+ The output is split into multiple lines if necessary (max 76 chars/line).
+ The first line's length is given as parameter. All the control characters
+ are encoded, including NUL, CR and LF. Sequences of one or more invalid UTF-8
+ characters are replaced by a single Unicode replacement character (U+fffd).
+ */
+void message_header_encode_q(const unsigned char *input, size_t len,
+ string_t *output, size_t first_line_len);
+void message_header_encode_b(const unsigned char *input, size_t len,
+ string_t *output, size_t first_line_len);
+
+#endif
diff --git a/src/lib-mail/message-header-hash.c b/src/lib-mail/message-header-hash.c
new file mode 100644
index 0000000..05eb791
--- /dev/null
+++ b/src/lib-mail/message-header-hash.c
@@ -0,0 +1,72 @@
+/* Copyright (c) 2013-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "hash-method.h"
+#include "message-header-hash.h"
+
+void message_header_hash_more(struct message_header_hash_context *ctx,
+ const struct hash_method *method, void *context,
+ unsigned int version,
+ const unsigned char *data, size_t size)
+{
+ size_t i, start;
+
+ i_assert(version >= 1 && version <= MESSAGE_HEADER_HASH_MAX_VERSION);
+
+ if (version == 1) {
+ method->loop(context, data, size);
+ return;
+ }
+ /* - Dovecot IMAP replaces NULs with 0x80 character.
+ - Dovecot POP3 with outlook-no-nuls workaround replaces NULs
+ with 0x80 character.
+ - Zimbra replaces 8bit chars with '?' in header fetches,
+ but not body fetches.
+ - Yahoo replaces 8bit chars with '?' in partial header
+ fetches, but not POP3 TOP. UTF-8 character sequence writes only a
+ single '?'
+
+ So we'll just replace all control and 8bit chars with '?' and
+ remove any repeated '?', which hopefully will satisfy everybody.
+
+ Also:
+ - Zimbra removes trailing spaces and tabs from IMAP BODY[HEADER],
+ but not IMAP BODY[] or POP3 TOP. Just strip away all spaces with
+ version 3 and tabs also with version 4.
+ */
+ for (i = start = 0; i < size; i++) {
+ bool cur_is_questionmark = FALSE;
+
+ switch (data[i]) {
+ case ' ':
+ if (version >= 3) {
+ /* strip away spaces */
+ method->loop(context, data + start, i-start);
+ start = i+1;
+ }
+ break;
+ case '\t':
+ if (version >= 4) {
+ /* strip away tabs */
+ method->loop(context, data + start, i-start);
+ start = i+1;
+ }
+ break;
+ case '\n':
+ break;
+ default:
+ if (data[i] < 0x20 || data[i] >= 0x7f || data[i] == '?') {
+ /* remove repeated '?' */
+ if (start < i || !ctx->prev_was_questionmark) {
+ method->loop(context, data + start, i-start);
+ method->loop(context, "?", 1);
+ }
+ start = i+1;
+ cur_is_questionmark = TRUE;
+ }
+ break;
+ }
+ ctx->prev_was_questionmark = cur_is_questionmark;
+ }
+ method->loop(context, data + start, i-start);
+}
diff --git a/src/lib-mail/message-header-hash.h b/src/lib-mail/message-header-hash.h
new file mode 100644
index 0000000..633d0c1
--- /dev/null
+++ b/src/lib-mail/message-header-hash.h
@@ -0,0 +1,18 @@
+#ifndef MESSAGE_HEADER_HASH_H
+#define MESSAGE_HEADER_HASH_H
+
+#define MESSAGE_HEADER_HASH_MAX_VERSION 4
+
+struct hash_method;
+
+struct message_header_hash_context {
+ bool prev_was_questionmark;
+};
+
+/* Initialize ctx with zeros. */
+void message_header_hash_more(struct message_header_hash_context *ctx,
+ const struct hash_method *method, void *context,
+ unsigned int version,
+ const unsigned char *data, size_t size);
+
+#endif
diff --git a/src/lib-mail/message-header-parser.c b/src/lib-mail/message-header-parser.c
new file mode 100644
index 0000000..c5026f1
--- /dev/null
+++ b/src/lib-mail/message-header-parser.c
@@ -0,0 +1,474 @@
+/* Copyright (c) 2002-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "buffer.h"
+#include "istream.h"
+#include "str.h"
+#include "strfuncs.h"
+#include "unichar.h"
+#include "message-size.h"
+#include "message-header-parser.h"
+
+/* RFC 5322 2.1.1 and 2.2 */
+#define MESSAGE_HEADER_NAME_MAX_LEN 1000
+
+struct message_header_parser_ctx {
+ struct message_header_line line;
+
+ struct istream *input;
+ struct message_size *hdr_size;
+
+ string_t *name;
+ buffer_t *value_buf;
+
+ enum message_header_parser_flags flags;
+ bool skip_line:1;
+ bool has_nuls:1;
+};
+
+struct message_header_parser_ctx *
+message_parse_header_init(struct istream *input, struct message_size *hdr_size,
+ enum message_header_parser_flags flags)
+{
+ struct message_header_parser_ctx *ctx;
+
+ ctx = i_new(struct message_header_parser_ctx, 1);
+ ctx->input = input;
+ ctx->hdr_size = hdr_size;
+ ctx->name = str_new(default_pool, 128);
+ ctx->flags = flags;
+ ctx->value_buf = buffer_create_dynamic(default_pool, 4096);
+ i_stream_ref(input);
+
+ if (hdr_size != NULL)
+ i_zero(hdr_size);
+ return ctx;
+}
+
+void message_parse_header_deinit(struct message_header_parser_ctx **_ctx)
+{
+ struct message_header_parser_ctx *ctx = *_ctx;
+
+ i_stream_unref(&ctx->input);
+ buffer_free(&ctx->value_buf);
+ str_free(&ctx->name);
+ i_free(ctx);
+
+ *_ctx = NULL;
+}
+
+int message_parse_header_next(struct message_header_parser_ctx *ctx,
+ struct message_header_line **hdr_r)
+{
+ struct message_header_line *line = &ctx->line;
+ const unsigned char *msg;
+ size_t i, size, startpos, colon_pos, parse_size, skip = 0;
+ int ret;
+ bool continued, continues, last_no_newline, last_crlf;
+ bool no_newline, crlf_newline;
+
+ *hdr_r = NULL;
+ if (line->eoh)
+ return -1;
+
+ if (line->continues)
+ colon_pos = 0;
+ else {
+ /* new header line */
+ line->name_offset = ctx->input->v_offset;
+ colon_pos = UINT_MAX;
+ buffer_set_used_size(ctx->value_buf, 0);
+ }
+
+ no_newline = FALSE;
+ crlf_newline = FALSE;
+ continued = line->continues;
+ continues = FALSE;
+
+ for (startpos = 0;;) {
+ ret = i_stream_read_bytes(ctx->input, &msg, &size, startpos+2);
+ if (ret >= 0) {
+ /* we want to know one byte in advance to find out
+ if it's multiline header */
+ parse_size = size == 0 ? 0 : size-1;
+ } else {
+ parse_size = size;
+ }
+
+ if (ret <= 0 && startpos == parse_size) {
+ if (ret == -1) {
+ if (startpos > 0) {
+ /* header ended unexpectedly. */
+ no_newline = TRUE;
+ skip = startpos;
+ break;
+ }
+ /* error / EOF with no bytes */
+ i_assert(skip == 0);
+ return -1;
+ }
+
+ if (size > 0 && !ctx->skip_line && !continued &&
+ (msg[0] == '\n' ||
+ (msg[0] == '\r' && size > 1 && msg[1] == '\n'))) {
+ /* end of headers - this mostly happens just
+ with mbox where headers are read separately
+ from body */
+ size = 0;
+ if (ctx->hdr_size != NULL)
+ ctx->hdr_size->lines++;
+ if (msg[0] == '\r') {
+ skip = 2;
+ crlf_newline = TRUE;
+ } else {
+ skip = 1;
+ if (ctx->hdr_size != NULL)
+ ctx->hdr_size->virtual_size++;
+ }
+ break;
+ }
+ if (ret == 0 && !ctx->input->eof) {
+ /* stream is nonblocking - need more data */
+ i_assert(skip == 0);
+ return 0;
+ }
+ i_assert(size > 0);
+
+ /* a) line is larger than input buffer
+ b) header ended unexpectedly */
+ if (ret == -2) {
+ /* go back to last LWSP if found. */
+ size_t min_pos = !continued ? colon_pos : 0;
+ for (i = size-1; i > min_pos; i--) {
+ if (IS_LWSP(msg[i])) {
+ size = i;
+ break;
+ }
+ }
+ if (i == min_pos && (msg[size-1] == '\r' ||
+ msg[size-1] == '\n')) {
+ /* we may or may not have a full header,
+ but we don't know until we get the
+ next character. leave out the
+ linefeed and finish the header on
+ the next run. */
+ size--;
+ if (size > 0 && msg[size-1] == '\r')
+ size--;
+ }
+ /* the buffer really has to be more than 2 to
+ avoid CRLF looping forever */
+ i_assert(size > 0);
+
+ continues = TRUE;
+ }
+ no_newline = TRUE;
+ skip = size;
+ break;
+ }
+
+ /* find ':' */
+ if (colon_pos == UINT_MAX) {
+ for (i = startpos; i < parse_size; i++) {
+ if (msg[i] > ':')
+ continue;
+
+ if (msg[i] == ':' && !ctx->skip_line) {
+ colon_pos = i;
+ line->full_value_offset =
+ ctx->input->v_offset + i + 1;
+ break;
+ }
+ if (msg[i] == '\n') {
+ /* end of headers, or error */
+ break;
+ }
+
+ if (msg[i] == '\0')
+ ctx->has_nuls = TRUE;
+ }
+ } else {
+ i = startpos;
+ }
+
+ /* find '\n' */
+ for (; i < parse_size; i++) {
+ if (msg[i] <= '\n') {
+ if (msg[i] == '\n')
+ break;
+ if (msg[i] == '\0')
+ ctx->has_nuls = TRUE;
+ }
+ }
+
+ if (i < parse_size && i+1 == size && ret == -2) {
+ /* we don't know if the line continues. */
+ i++;
+ } else if (i < parse_size) {
+ /* got a line */
+ if (ctx->skip_line) {
+ /* skipping a line with a huge header name */
+ if (ctx->hdr_size != NULL) {
+ ctx->hdr_size->lines++;
+ ctx->hdr_size->physical_size += i + 1;
+ ctx->hdr_size->virtual_size += i + 1;
+ }
+ if (i == 0 || msg[i-1] != '\r') {
+ /* missing CR */
+ if (ctx->hdr_size != NULL)
+ ctx->hdr_size->virtual_size++;
+ }
+
+ i_stream_skip(ctx->input, i + 1);
+ startpos = 0;
+ ctx->skip_line = FALSE;
+ continue;
+ }
+ continues = i+1 < size && IS_LWSP(msg[i+1]);
+
+ if (ctx->hdr_size != NULL)
+ ctx->hdr_size->lines++;
+ if (i == 0 || msg[i-1] != '\r') {
+ /* missing CR */
+ if (ctx->hdr_size != NULL)
+ ctx->hdr_size->virtual_size++;
+ size = i;
+ } else {
+ size = i-1;
+ crlf_newline = TRUE;
+ }
+
+ skip = i+1;
+ break;
+ }
+
+ startpos = i;
+ }
+
+ last_crlf = line->crlf_newline &&
+ (ctx->flags & MESSAGE_HEADER_PARSER_FLAG_DROP_CR) == 0;
+ last_no_newline = line->no_newline ||
+ (ctx->flags & MESSAGE_HEADER_PARSER_FLAG_CLEAN_ONELINE) != 0;
+
+ line->continues = continues;
+ line->continued = continued;
+ line->crlf_newline = crlf_newline;
+ line->no_newline = no_newline;
+ if (size == 0 && !continued) {
+ /* end of headers */
+ line->eoh = TRUE;
+ line->name_len = line->value_len = line->full_value_len = 0;
+ line->name = ""; line->value = line->full_value = NULL;
+ line->middle = NULL; line->middle_len = 0;
+ line->full_value_offset = line->name_offset;
+ line->continues = FALSE;
+ } else if (line->continued) {
+ line->value = msg;
+ line->value_len = size;
+ } else if (colon_pos == UINT_MAX) {
+ /* missing ':', assume the whole line is value */
+ line->value = msg;
+ line->value_len = size;
+ line->full_value_offset = line->name_offset;
+
+ line->name = "";
+ line->name_len = 0;
+
+ line->middle = uchar_empty_ptr;
+ line->middle_len = 0;
+ } else {
+ size_t pos;
+
+ line->value = msg + colon_pos+1;
+ line->value_len = size - colon_pos - 1;
+ if ((ctx->flags & MESSAGE_HEADER_PARSER_FLAG_SKIP_INITIAL_LWSP) != 0) {
+ /* get value. skip all LWSP after ':'. Note that
+ RFC2822 doesn't say we should, but history behind
+ it..
+
+ Exception to this is if the value consists only of
+ LWSP, then skip only the one LWSP after ':'. */
+ for (pos = 0; pos < line->value_len; pos++) {
+ if (!IS_LWSP(line->value[pos]))
+ break;
+ }
+
+ if (pos == line->value_len) {
+ /* everything was LWSP */
+ if (line->value_len > 0 &&
+ IS_LWSP(line->value[0]))
+ pos = 1;
+ }
+ } else {
+ pos = line->value_len > 0 &&
+ IS_LWSP(line->value[0]) ? 1 : 0;
+ }
+
+ line->value += pos;
+ line->value_len -= pos;
+ line->full_value_offset += pos;
+
+ /* get name, skip LWSP before ':' */
+ while (colon_pos > 0 && IS_LWSP(msg[colon_pos-1]))
+ colon_pos--;
+
+ /* Treat overlong header names as if the full header line was
+ a value. Callers can usually handle large values better than
+ large names. */
+ if (colon_pos > MESSAGE_HEADER_NAME_MAX_LEN) {
+ line->name = "";
+ line->name_len = 0;
+ line->middle = uchar_empty_ptr;
+ line->middle_len = 0;
+ line->value = msg;
+ line->value_len = size;
+ line->full_value_offset = line->name_offset;
+ } else {
+ str_truncate(ctx->name, 0);
+ /* use buffer_append() so the name won't be truncated if there
+ are NULs. */
+ buffer_append(ctx->name, msg, colon_pos);
+ str_append_c(ctx->name, '\0');
+
+ /* keep middle stored also in ctx->name so it's available
+ with use_full_value */
+ line->middle = msg + colon_pos;
+ line->middle_len = (size_t)(line->value - line->middle);
+ str_append_data(ctx->name, line->middle, line->middle_len);
+
+ line->name = str_c(ctx->name);
+ line->name_len = colon_pos;
+ line->middle = str_data(ctx->name) + line->name_len + 1;
+ }
+ }
+
+ if (!line->continued) {
+ /* first header line. make a copy of the line since we can't
+ really trust input stream not to lose it. */
+ buffer_append(ctx->value_buf, line->value, line->value_len);
+ line->value = line->full_value = ctx->value_buf->data;
+ line->full_value_len = line->value_len;
+ } else if (line->use_full_value) {
+ /* continue saving the full value. */
+ if (last_no_newline) {
+ /* line is longer than fit into our buffer, so we
+ were forced to break it into multiple
+ message_header_lines */
+ } else {
+ if (last_crlf)
+ buffer_append_c(ctx->value_buf, '\r');
+ buffer_append_c(ctx->value_buf, '\n');
+ }
+ if ((ctx->flags & MESSAGE_HEADER_PARSER_FLAG_CLEAN_ONELINE) != 0 &&
+ line->value_len > 0 && line->value[0] != ' ' &&
+ IS_LWSP(line->value[0])) {
+ buffer_append_c(ctx->value_buf, ' ');
+ buffer_append(ctx->value_buf,
+ line->value + 1, line->value_len - 1);
+ } else {
+ buffer_append(ctx->value_buf,
+ line->value, line->value_len);
+ }
+ line->full_value = ctx->value_buf->data;
+ line->full_value_len = ctx->value_buf->used;
+ } else {
+ /* we didn't want full_value, and this is a continued line. */
+ line->full_value = NULL;
+ line->full_value_len = 0;
+ }
+
+ /* always reset it */
+ line->use_full_value = FALSE;
+
+ if (ctx->hdr_size != NULL) {
+ ctx->hdr_size->physical_size += skip;
+ ctx->hdr_size->virtual_size += skip;
+ }
+ i_stream_skip(ctx->input, skip);
+
+ *hdr_r = line;
+ return 1;
+}
+
+bool message_parse_header_has_nuls(const struct message_header_parser_ctx *ctx)
+{
+ return ctx->has_nuls;
+}
+
+#undef message_parse_header
+void message_parse_header(struct istream *input, struct message_size *hdr_size,
+ enum message_header_parser_flags flags,
+ message_header_callback_t *callback, void *context)
+{
+ struct message_header_parser_ctx *hdr_ctx;
+ struct message_header_line *hdr;
+ int ret;
+
+ hdr_ctx = message_parse_header_init(input, hdr_size, flags);
+ while ((ret = message_parse_header_next(hdr_ctx, &hdr)) > 0)
+ callback(hdr, context);
+ i_assert(ret != 0);
+ message_parse_header_deinit(&hdr_ctx);
+
+ /* call after the final skipping */
+ callback(NULL, context);
+}
+
+void message_header_line_write(buffer_t *output,
+ const struct message_header_line *hdr)
+{
+ if (!hdr->continued) {
+ buffer_append(output, hdr->name, strlen(hdr->name));
+ buffer_append(output, hdr->middle, hdr->middle_len);
+ }
+ buffer_append(output, hdr->value, hdr->value_len);
+ if (!hdr->no_newline) {
+ if (hdr->crlf_newline)
+ buffer_append_c(output, '\r');
+ buffer_append_c(output, '\n');
+ }
+}
+
+const char *
+message_header_strdup(pool_t pool, const unsigned char *data, size_t size)
+{
+ if (memchr(data, '\0', size) == NULL) {
+ /* fast path */
+ char *dest = p_malloc(pool, size+1);
+ memcpy(dest, data, size);
+ return dest;
+ }
+
+ /* slow path - this could be made faster, but it should be
+ rare so keep it simple */
+ string_t *str = str_new(pool, size+2);
+ for (size_t i = 0; i < size; i++) {
+ if (data[i] != '\0')
+ str_append_c(str, data[i]);
+ else
+ str_append(str, UNICODE_REPLACEMENT_CHAR_UTF8);
+ }
+ return str_c(str);
+}
+
+bool message_header_name_is_valid(const char *name)
+{
+ /*
+ field-name = 1*ftext
+
+ ftext = %d33-57 / ; Printable US-ASCII
+ %d59-126 ; characters not including
+ ; ":".
+ */
+ for (unsigned int i = 0; name[i] != '\0'; i++) {
+ unsigned char c = name[i];
+ if (c >= 33 && c <= 57) {
+ /* before ":" */
+ } else if (c >= 59 && c <= 126) {
+ /* after ":" */
+ } else {
+ return FALSE;
+ }
+ }
+ return TRUE;
+}
diff --git a/src/lib-mail/message-header-parser.h b/src/lib-mail/message-header-parser.h
new file mode 100644
index 0000000..ce0825c
--- /dev/null
+++ b/src/lib-mail/message-header-parser.h
@@ -0,0 +1,85 @@
+#ifndef MESSAGE_HEADER_PARSER_H
+#define MESSAGE_HEADER_PARSER_H
+
+#define IS_LWSP(c) \
+ ((c) == ' ' || (c) == '\t')
+
+struct message_size;
+struct message_header_parser_ctx;
+
+enum message_header_parser_flags {
+ /* Don't add LWSP after "header: " to value. */
+ MESSAGE_HEADER_PARSER_FLAG_SKIP_INITIAL_LWSP = 0x01,
+ /* Don't add CRs to full_value even if input had them */
+ MESSAGE_HEADER_PARSER_FLAG_DROP_CR = 0x02,
+ /* Convert [CR+]LF+LWSP to a space character in full_value */
+ MESSAGE_HEADER_PARSER_FLAG_CLEAN_ONELINE = 0x04
+};
+
+struct message_header_line {
+ const char *name;
+ size_t name_len;
+
+ const unsigned char *value;
+ size_t value_len;
+
+ const unsigned char *full_value;
+ size_t full_value_len;
+
+ const unsigned char *middle;
+ size_t middle_len;
+
+ uoff_t name_offset, full_value_offset;
+
+ bool continues:1; /* multiline header, continues in next line */
+ bool continued:1; /* multiline header, continues */
+ bool eoh:1; /* "end of headers" line */
+ bool no_newline:1; /* no \n after this line */
+ bool crlf_newline:1; /* newline was \r\n */
+ bool use_full_value:1; /* set if you want full_value */
+};
+
+/* called once with hdr = NULL at the end of headers */
+typedef void message_header_callback_t(struct message_header_line *hdr,
+ void *context);
+
+struct message_header_parser_ctx *
+message_parse_header_init(struct istream *input, struct message_size *hdr_size,
+ enum message_header_parser_flags flags) ATTR_NULL(2);
+void message_parse_header_deinit(struct message_header_parser_ctx **ctx);
+
+/* Read and return next header line. Returns 1 if header is returned, 0 if
+ input stream is non-blocking and more data needs to be read, -1 when all is
+ done or error occurred (see stream's error status). */
+int message_parse_header_next(struct message_header_parser_ctx *ctx,
+ struct message_header_line **hdr_r);
+
+/* Returns TRUE if the parser has seen NUL characters. */
+bool message_parse_header_has_nuls(const struct message_header_parser_ctx *ctx)
+ ATTR_PURE;
+
+/* Read and parse the header from the given stream. */
+void message_parse_header(struct istream *input, struct message_size *hdr_size,
+ enum message_header_parser_flags flags,
+ message_header_callback_t *callback, void *context)
+ ATTR_NULL(2);
+#define message_parse_header(input, hdr_size, flags, callback, context) \
+ message_parse_header(input, hdr_size, flags - \
+ CALLBACK_TYPECHECK(callback, void (*)( \
+ struct message_header_line *hdr, typeof(context))), \
+ (message_header_callback_t *)callback, context)
+
+/* Write the header line to buffer exactly as it was read, including the
+ newline. */
+void message_header_line_write(buffer_t *output,
+ const struct message_header_line *hdr);
+
+/* Duplicate the given header value data and return it. Replaces any NULs with
+ UNICODE_REPLACEMENT_CHAR_UTF8. */
+const char *
+message_header_strdup(pool_t pool, const unsigned char *data, size_t size);
+
+/* Returns TRUE if message header name is valid. */
+bool message_header_name_is_valid(const char *name);
+
+#endif
diff --git a/src/lib-mail/message-id.c b/src/lib-mail/message-id.c
new file mode 100644
index 0000000..68e2be0
--- /dev/null
+++ b/src/lib-mail/message-id.c
@@ -0,0 +1,129 @@
+/* Copyright (c) 2006-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "str.h"
+#include "rfc822-parser.h"
+#include "message-id.h"
+
+static bool get_untokenized_msgid(const char **msgid_p, string_t *msgid)
+{
+ struct rfc822_parser_context parser;
+ int ret;
+ bool success = FALSE;
+
+ rfc822_parser_init(&parser, (const unsigned char *)*msgid_p,
+ strlen(*msgid_p), NULL);
+
+ /*
+ msg-id = [CFWS] "<" id-left "@" id-right ">" [CFWS]
+ id-left = dot-atom-text / no-fold-quote / obs-id-left
+ id-right = dot-atom-text / no-fold-literal / obs-id-right
+ no-fold-quote = DQUOTE *(qtext / quoted-pair) DQUOTE
+ no-fold-literal = "[" *(dtext / quoted-pair) "]"
+ */
+
+ rfc822_skip_lwsp(&parser);
+
+ if (*parser.data == '"')
+ ret = rfc822_parse_quoted_string(&parser, msgid);
+ else
+ ret = rfc822_parse_dot_atom(&parser, msgid);
+ if (ret > 0 && *parser.data == '@') {
+ str_append_c(msgid, '@');
+ parser.data++;
+ rfc822_skip_lwsp(&parser);
+
+ if (rfc822_parse_dot_atom(&parser, msgid) > 0 &&
+ *parser.data == '>') {
+ *msgid_p = (const char *)parser.data + 1;
+ success = TRUE;
+ }
+ }
+ rfc822_parser_deinit(&parser);
+ return success;
+}
+
+static void strip_lwsp(char *str)
+{
+ /* @UNSAFE */
+ char *dest;
+
+ /* find the first lwsp */
+ while (*str != ' ' && *str != '\t' && *str != '\r' && *str != '\n') {
+ if (*str == '\0')
+ return;
+ str++;
+ }
+
+ for (dest = str; *str != '\0'; str++) {
+ if (*str != ' ' && *str != '\t' && *str != '\r' && *str != '\n')
+ *dest++ = *str;
+ }
+ *dest = '\0';
+}
+
+const char *message_id_get_next(const char **msgid_p)
+{
+ const char *msgid = *msgid_p;
+ const char *p;
+ string_t *str = NULL;
+ bool found_at;
+
+ if (*msgid_p == NULL)
+ return NULL;
+
+ for (;;) {
+ /* skip until '<' */
+ while (*msgid != '<') {
+ if (*msgid == '\0') {
+ *msgid_p = msgid;
+ return NULL;
+ }
+ msgid++;
+ }
+ msgid++;
+
+ /* check it through quickly to see if it's already normalized */
+ p = msgid; found_at = FALSE;
+ for (;; p++) {
+ if ((unsigned char)*p >= 'A') /* matches most */
+ continue;
+
+ if (*p == '@')
+ found_at = TRUE;
+ if (*p == '>' || *p == '"' || *p == '(' || *p == '[')
+ break;
+
+ if (*p == '\0') {
+ *msgid_p = p;
+ return NULL;
+ }
+ }
+
+ if (*p == '>') {
+ *msgid_p = p+1;
+ if (found_at) {
+ char *s;
+
+ s = p_strdup_until(unsafe_data_stack_pool,
+ msgid, p);
+ strip_lwsp(s);
+ return s;
+ }
+ } else {
+ /* ok, do it the slow way */
+ *msgid_p = msgid;
+
+ if (str == NULL) {
+ /* allocate only once, so we don't leak
+ with multiple invalid message IDs */
+ str = t_str_new(256);
+ }
+ if (get_untokenized_msgid(msgid_p, str))
+ return str_c(str);
+ }
+
+ /* invalid message id, see if there's another valid one */
+ msgid = *msgid_p;
+ }
+}
diff --git a/src/lib-mail/message-id.h b/src/lib-mail/message-id.h
new file mode 100644
index 0000000..fcfff44
--- /dev/null
+++ b/src/lib-mail/message-id.h
@@ -0,0 +1,8 @@
+#ifndef MESSAGE_ID_H
+#define MESSAGE_ID_H
+
+/* Returns the next valid message ID from a given Message-ID header.
+ The return value is allocated from data stack. */
+const char *message_id_get_next(const char **msgid_p);
+
+#endif
diff --git a/src/lib-mail/message-parser-from-parts.c b/src/lib-mail/message-parser-from-parts.c
new file mode 100644
index 0000000..8e21ec8
--- /dev/null
+++ b/src/lib-mail/message-parser-from-parts.c
@@ -0,0 +1,365 @@
+/* Copyright (c) 2002-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "istream.h"
+#include "message-parser-private.h"
+
+static int preparsed_parse_epilogue_init(struct message_parser_ctx *ctx,
+ struct message_block *block_r);
+static int preparsed_parse_next_header_init(struct message_parser_ctx *ctx,
+ struct message_block *block_r);
+
+static int preparsed_parse_eof(struct message_parser_ctx *ctx ATTR_UNUSED,
+ struct message_block *block_r ATTR_UNUSED)
+{
+ return -1;
+}
+
+static void preparsed_skip_to_next(struct message_parser_ctx *ctx)
+{
+ ctx->parse_next_block = preparsed_parse_next_header_init;
+ while (ctx->part != NULL) {
+ if (ctx->part->next != NULL) {
+ ctx->part = ctx->part->next;
+ break;
+ }
+
+ /* parse epilogue of multipart parent if requested */
+ if (ctx->part->parent != NULL &&
+ (ctx->part->parent->flags & MESSAGE_PART_FLAG_MULTIPART) != 0 &&
+ (ctx->flags & MESSAGE_PARSER_FLAG_INCLUDE_MULTIPART_BLOCKS) != 0) {
+ /* check for presence of epilogue */
+ uoff_t part_end = ctx->part->physical_pos +
+ ctx->part->header_size.physical_size +
+ ctx->part->body_size.physical_size;
+ uoff_t parent_end = ctx->part->parent->physical_pos +
+ ctx->part->parent->header_size.physical_size +
+ ctx->part->parent->body_size.physical_size;
+
+ if (parent_end > part_end) {
+ ctx->parse_next_block = preparsed_parse_epilogue_init;
+ break;
+ }
+ }
+ ctx->part = ctx->part->parent;
+ }
+ if (ctx->part == NULL)
+ ctx->parse_next_block = preparsed_parse_eof;
+}
+
+static int preparsed_parse_body_finish(struct message_parser_ctx *ctx,
+ struct message_block *block_r)
+{
+ i_stream_skip(ctx->input, ctx->skip);
+ ctx->skip = 0;
+
+ preparsed_skip_to_next(ctx);
+ return ctx->parse_next_block(ctx, block_r);
+}
+
+static int preparsed_parse_prologue_finish(struct message_parser_ctx *ctx,
+ struct message_block *block_r)
+{
+ i_stream_skip(ctx->input, ctx->skip);
+ ctx->skip = 0;
+
+ ctx->parse_next_block = preparsed_parse_next_header_init;
+ ctx->part = ctx->part->children;
+ return ctx->parse_next_block(ctx, block_r);
+}
+
+static int preparsed_parse_body_more(struct message_parser_ctx *ctx,
+ struct message_block *block_r)
+{
+ uoff_t end_offset = ctx->part->physical_pos +
+ ctx->part->header_size.physical_size +
+ ctx->part->body_size.physical_size;
+ bool full;
+ int ret;
+
+ if ((ret = message_parser_read_more(ctx, block_r, &full)) <= 0)
+ return ret;
+
+ if (ctx->input->v_offset + block_r->size >= end_offset) {
+ block_r->size = end_offset - ctx->input->v_offset;
+ ctx->parse_next_block = preparsed_parse_body_finish;
+ }
+ ctx->skip = block_r->size;
+ return 1;
+}
+
+static int preparsed_parse_prologue_more(struct message_parser_ctx *ctx,
+ struct message_block *block_r)
+{
+ uoff_t boundary_min_start, end_offset;
+ const unsigned char *cur;
+ bool full;
+ int ret;
+
+ i_assert(ctx->part->children != NULL);
+ end_offset = ctx->part->children->physical_pos;
+
+ if ((ret = message_parser_read_more(ctx, block_r, &full)) <= 0)
+ return ret;
+
+ if (ctx->input->v_offset + block_r->size >= end_offset) {
+ /* we've got the full prologue: clip off the initial boundary */
+ block_r->size = end_offset - ctx->input->v_offset;
+ cur = block_r->data + block_r->size - 1;
+
+ /* [\r]\n--boundary[\r]\n */
+ if (block_r->size < 5 || *cur != '\n') {
+ ctx->broken_reason = "Prologue boundary end not at expected position";
+ return -1;
+ }
+
+ cur--;
+ if (*cur == '\r') cur--;
+
+ /* find newline just before boundary */
+ for (; cur >= block_r->data; cur--) {
+ if (*cur == '\n') break;
+ }
+
+ if (cur[0] != '\n' || cur[1] != '-' || cur[2] != '-') {
+ ctx->broken_reason = "Prologue boundary beginning not at expected position";
+ return -1;
+ }
+
+ if (cur != block_r->data && cur[-1] == '\r') cur--;
+
+ /* clip boundary */
+ block_r->size = cur - block_r->data;
+
+ ctx->parse_next_block = preparsed_parse_prologue_finish;
+ ctx->skip = block_r->size;
+ return 1;
+ }
+
+ /* retain enough data in the stream buffer to contain initial boundary */
+ if (end_offset > BOUNDARY_END_MAX_LEN)
+ boundary_min_start = end_offset - BOUNDARY_END_MAX_LEN;
+ else
+ boundary_min_start = 0;
+
+ if (ctx->input->v_offset + block_r->size >= boundary_min_start) {
+ if (boundary_min_start <= ctx->input->v_offset)
+ return 0;
+ block_r->size = boundary_min_start - ctx->input->v_offset;
+ }
+ ctx->skip = block_r->size;
+ return 1;
+}
+
+static int preparsed_parse_epilogue_more(struct message_parser_ctx *ctx,
+ struct message_block *block_r)
+{
+ uoff_t end_offset = ctx->part->physical_pos +
+ ctx->part->header_size.physical_size +
+ ctx->part->body_size.physical_size;
+ bool full;
+ int ret;
+
+ if ((ret = message_parser_read_more(ctx, block_r, &full)) <= 0)
+ return ret;
+
+ if (ctx->input->v_offset + block_r->size >= end_offset) {
+ block_r->size = end_offset - ctx->input->v_offset;
+ ctx->parse_next_block = preparsed_parse_body_finish;
+ }
+ ctx->skip = block_r->size;
+ return 1;
+}
+
+static int preparsed_parse_epilogue_boundary(struct message_parser_ctx *ctx,
+ struct message_block *block_r)
+{
+ uoff_t end_offset = ctx->part->physical_pos +
+ ctx->part->header_size.physical_size +
+ ctx->part->body_size.physical_size;
+ const unsigned char *data, *cur;
+ size_t size;
+ bool full;
+ int ret;
+
+ if (end_offset - ctx->input->v_offset < 7) {
+ ctx->broken_reason = "Epilogue position is wrong";
+ return -1;
+ }
+
+ if ((ret = message_parser_read_more(ctx, block_r, &full)) <= 0)
+ return ret;
+
+ /* [\r]\n--boundary--[\r]\n */
+ if (block_r->size < 7) {
+ ctx->want_count = 7;
+ return 0;
+ }
+
+ data = block_r->data;
+ size = block_r->size;
+ cur = data;
+
+ if (*cur == '\r') cur++;
+
+ if (cur[0] != '\n' || cur[1] != '-' || data[2] != '-') {
+ ctx->broken_reason = "Epilogue boundary start not at expected position";
+ return -1;
+ }
+
+ /* find the end of the line */
+ cur += 3;
+ if ((cur = memchr(cur, '\n', size - (cur-data))) == NULL) {
+ if (end_offset < ctx->input->v_offset + size) {
+ ctx->broken_reason = "Epilogue boundary end not at expected position";
+ return -1;
+ } else if (ctx->input->v_offset + size < end_offset &&
+ size < BOUNDARY_END_MAX_LEN &&
+ !ctx->input->eof && !full) {
+ ctx->want_count = BOUNDARY_END_MAX_LEN;
+ return 0;
+ }
+ }
+
+ block_r->size = 0;
+ ctx->parse_next_block = preparsed_parse_epilogue_more;
+ ctx->skip = cur - data + 1;
+ return 0;
+}
+
+static int preparsed_parse_body_init(struct message_parser_ctx *ctx,
+ struct message_block *block_r)
+{
+ uoff_t offset = ctx->part->physical_pos +
+ ctx->part->header_size.physical_size;
+
+ if (offset < ctx->input->v_offset) {
+ /* header was actually larger than the cached size suggested */
+ ctx->broken_reason = "Header larger than its cached size";
+ return -1;
+ }
+ i_stream_skip(ctx->input, offset - ctx->input->v_offset);
+
+ /* multipart messages may begin with --boundary--, which makes them
+ not have any children. */
+ if ((ctx->part->flags & MESSAGE_PART_FLAG_MULTIPART) == 0 ||
+ ctx->part->children == NULL)
+ ctx->parse_next_block = preparsed_parse_body_more;
+ else
+ ctx->parse_next_block = preparsed_parse_prologue_more;
+ return ctx->parse_next_block(ctx, block_r);
+}
+
+static int preparsed_parse_epilogue_init(struct message_parser_ctx *ctx,
+ struct message_block *block_r)
+{
+ uoff_t offset = ctx->part->physical_pos +
+ ctx->part->header_size.physical_size +
+ ctx->part->body_size.physical_size;
+
+ ctx->part = ctx->part->parent;
+
+ if (offset < ctx->input->v_offset) {
+ /* last child was actually larger than the cached size
+ suggested */
+ ctx->broken_reason = "Part larger than its cached size";
+ return -1;
+ }
+ i_stream_skip(ctx->input, offset - ctx->input->v_offset);
+
+ ctx->parse_next_block = preparsed_parse_epilogue_boundary;
+ return ctx->parse_next_block(ctx, block_r);
+}
+
+static int preparsed_parse_finish_header(struct message_parser_ctx *ctx,
+ struct message_block *block_r)
+{
+ if (ctx->part->children != NULL) {
+ if ((ctx->part->flags & MESSAGE_PART_FLAG_MULTIPART) != 0 &&
+ (ctx->flags & MESSAGE_PARSER_FLAG_INCLUDE_MULTIPART_BLOCKS) != 0)
+ ctx->parse_next_block = preparsed_parse_body_init;
+ else {
+ ctx->parse_next_block = preparsed_parse_next_header_init;
+ ctx->part = ctx->part->children;
+ }
+ } else if ((ctx->flags & MESSAGE_PARSER_FLAG_SKIP_BODY_BLOCK) == 0) {
+ ctx->parse_next_block = preparsed_parse_body_init;
+ } else {
+ preparsed_skip_to_next(ctx);
+ }
+ return ctx->parse_next_block(ctx, block_r);
+}
+
+static int preparsed_parse_next_header(struct message_parser_ctx *ctx,
+ struct message_block *block_r)
+{
+ struct message_header_line *hdr;
+ int ret;
+
+ ret = message_parse_header_next(ctx->hdr_parser_ctx, &hdr);
+ if (ret == 0 || (ret < 0 && ctx->input->stream_errno != 0)) {
+ ctx->want_count = i_stream_get_data_size(ctx->input) + 1;
+ return ret;
+ }
+
+ if (hdr != NULL) {
+ block_r->hdr = hdr;
+ block_r->size = 0;
+ return 1;
+ }
+ message_parse_header_deinit(&ctx->hdr_parser_ctx);
+
+ ctx->parse_next_block = preparsed_parse_finish_header;
+
+ /* return empty block as end of headers */
+ block_r->hdr = NULL;
+ block_r->size = 0;
+
+ i_assert(ctx->skip == 0);
+ if (ctx->input->v_offset != ctx->part->physical_pos +
+ ctx->part->header_size.physical_size) {
+ ctx->broken_reason = "Cached header size mismatch";
+ return -1;
+ }
+ return 1;
+}
+
+static int preparsed_parse_next_header_init(struct message_parser_ctx *ctx,
+ struct message_block *block_r)
+{
+ struct istream *hdr_input;
+
+ i_assert(ctx->hdr_parser_ctx == NULL);
+
+ i_assert(ctx->part->physical_pos >= ctx->input->v_offset);
+ i_stream_skip(ctx->input, ctx->part->physical_pos -
+ ctx->input->v_offset);
+
+ /* the header may become truncated by --boundaries. limit the header
+ stream's size to what it's supposed to be to avoid duplicating (and
+ keeping in sync!) all the same complicated logic as in
+ parse_next_header(). */
+ hdr_input = i_stream_create_limit(ctx->input, ctx->part->header_size.physical_size);
+ ctx->hdr_parser_ctx =
+ message_parse_header_init(hdr_input, NULL, ctx->hdr_flags);
+ i_stream_unref(&hdr_input);
+
+ ctx->parse_next_block = preparsed_parse_next_header;
+ return preparsed_parse_next_header(ctx, block_r);
+}
+
+struct message_parser_ctx *
+message_parser_init_from_parts(struct message_part *parts,
+ struct istream *input,
+ const struct message_parser_settings *set)
+{
+ struct message_parser_ctx *ctx;
+
+ i_assert(parts != NULL);
+
+ ctx = message_parser_init_int(input, set);
+ ctx->preparsed = TRUE;
+ ctx->parts = ctx->part = parts;
+ ctx->parse_next_block = preparsed_parse_next_header_init;
+ return ctx;
+}
diff --git a/src/lib-mail/message-parser-private.h b/src/lib-mail/message-parser-private.h
new file mode 100644
index 0000000..41c32da
--- /dev/null
+++ b/src/lib-mail/message-parser-private.h
@@ -0,0 +1,62 @@
+#ifndef MESSAGE_PARSER_PRIVATE_H
+#define MESSAGE_PARSER_PRIVATE_H
+
+#include "message-parser.h"
+
+/* RFC-2046 requires boundaries are max. 70 chars + "--" prefix + "--" suffix.
+ We'll add a bit more just in case. */
+#define BOUNDARY_STRING_MAX_LEN (70 + 10)
+#define BOUNDARY_END_MAX_LEN (BOUNDARY_STRING_MAX_LEN + 2 + 2)
+
+struct message_boundary {
+ struct message_boundary *next;
+
+ struct message_part *part;
+ char *boundary;
+ size_t len;
+
+ bool epilogue_found:1;
+};
+
+struct message_parser_ctx {
+ pool_t part_pool;
+ struct istream *input;
+ struct message_part *parts, *part;
+ const char *broken_reason;
+ unsigned int nested_parts_count;
+ unsigned int total_parts_count;
+
+ enum message_header_parser_flags hdr_flags;
+ enum message_parser_flags flags;
+ unsigned int max_nested_mime_parts;
+ unsigned int max_total_mime_parts;
+
+ char *last_boundary;
+ struct message_boundary *boundaries;
+
+ struct message_part **next_part;
+ ARRAY(struct message_part **) next_part_stack;
+
+ size_t skip;
+ unsigned char last_chr;
+ unsigned int want_count;
+
+ struct message_header_parser_ctx *hdr_parser_ctx;
+ unsigned int prev_hdr_newline_size;
+
+ int (*parse_next_block)(struct message_parser_ctx *ctx,
+ struct message_block *block_r);
+
+ bool part_seen_content_type:1;
+ bool multipart:1;
+ bool preparsed:1;
+ bool eof:1;
+};
+
+struct message_parser_ctx *
+message_parser_init_int(struct istream *input,
+ const struct message_parser_settings *set);
+int message_parser_read_more(struct message_parser_ctx *ctx,
+ struct message_block *block_r, bool *full_r);
+
+#endif
diff --git a/src/lib-mail/message-parser.c b/src/lib-mail/message-parser.c
new file mode 100644
index 0000000..9a9c9a3
--- /dev/null
+++ b/src/lib-mail/message-parser.c
@@ -0,0 +1,907 @@
+/* Copyright (c) 2002-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "str.h"
+#include "istream.h"
+#include "rfc822-parser.h"
+#include "rfc2231-parser.h"
+#include "message-parser-private.h"
+
+message_part_header_callback_t *null_message_part_header_callback = NULL;
+
+static int parse_next_header_init(struct message_parser_ctx *ctx,
+ struct message_block *block_r);
+static int parse_next_body_to_boundary(struct message_parser_ctx *ctx,
+ struct message_block *block_r);
+static int parse_next_body_to_eof(struct message_parser_ctx *ctx,
+ struct message_block *block_r);
+
+static struct message_boundary *
+boundary_find(struct message_boundary *boundaries,
+ const unsigned char *data, size_t len, bool trailing_dashes)
+{
+ struct message_boundary *best = NULL;
+
+ /* As MIME spec says: search from latest one to oldest one so that we
+ don't break if the same boundary is used in nested parts. Also the
+ full message line doesn't have to match the boundary, only the
+ beginning. However, if there are multiple prefixes whose beginning
+ matches, use the longest matching one. */
+ while (boundaries != NULL) {
+ if (boundaries->len <= len &&
+ memcmp(boundaries->boundary, data, boundaries->len) == 0 &&
+ (best == NULL || best->len < boundaries->len)) {
+ best = boundaries;
+ /* If we see "foo--", it could either mean that there
+ is a boundary named "foo" that ends now or there's
+ a boundary "foo--" which continues. */
+ if (best->len == len ||
+ (best->len == len-2 && trailing_dashes)) {
+ /* This is exactly the wanted boundary. There
+ can't be a better one. */
+ break;
+ }
+ }
+
+ boundaries = boundaries->next;
+ }
+
+ return best;
+}
+
+static void parse_body_add_block(struct message_parser_ctx *ctx,
+ struct message_block *block)
+{
+ unsigned int missing_cr_count = 0;
+ const unsigned char *cur, *next, *data = block->data;
+
+ i_assert(block->size > 0);
+
+ block->hdr = NULL;
+
+ /* check if we have NULs */
+ if (memchr(data, '\0', block->size) != NULL)
+ ctx->part->flags |= MESSAGE_PART_FLAG_HAS_NULS;
+
+ /* count number of lines and missing CRs */
+ if (*data == '\n') {
+ ctx->part->body_size.lines++;
+ if (ctx->last_chr != '\r')
+ missing_cr_count++;
+ }
+
+ cur = data + 1;
+ while ((next = memchr(cur, '\n', block->size - (cur - data))) != NULL) {
+ ctx->part->body_size.lines++;
+ if (next[-1] != '\r')
+ missing_cr_count++;
+
+ cur = next + 1;
+ }
+ ctx->last_chr = data[block->size - 1];
+ ctx->skip += block->size;
+
+ ctx->part->body_size.physical_size += block->size;
+ ctx->part->body_size.virtual_size += block->size + missing_cr_count;
+}
+
+int message_parser_read_more(struct message_parser_ctx *ctx,
+ struct message_block *block_r, bool *full_r)
+{
+ int ret;
+
+ if (ctx->skip > 0) {
+ i_stream_skip(ctx->input, ctx->skip);
+ ctx->skip = 0;
+ }
+
+ *full_r = FALSE;
+ ret = i_stream_read_bytes(ctx->input, &block_r->data,
+ &block_r->size, ctx->want_count + 1);
+ if (ret <= 0) {
+ switch (ret) {
+ case 0:
+ if (!ctx->input->eof) {
+ i_assert(!ctx->input->blocking);
+ return 0;
+ }
+ break;
+ case -1:
+ i_assert(ctx->input->eof ||
+ ctx->input->stream_errno != 0);
+ ctx->eof = TRUE;
+ if (block_r->size != 0) {
+ /* EOF, but we still have some data.
+ return it. */
+ return 1;
+ }
+ return -1;
+ case -2:
+ *full_r = TRUE;
+ break;
+ default:
+ i_unreached();
+ }
+ }
+
+ if (!*full_r) {
+ /* reset number of wanted characters if we actually got them */
+ ctx->want_count = 1;
+ }
+ return 1;
+}
+
+static void
+message_part_append(struct message_parser_ctx *ctx)
+{
+ struct message_part *parent = ctx->part;
+ struct message_part *part;
+
+ i_assert(!ctx->preparsed);
+ i_assert(parent != NULL);
+ i_assert((parent->flags & (MESSAGE_PART_FLAG_MULTIPART |
+ MESSAGE_PART_FLAG_MESSAGE_RFC822)) != 0);
+
+ part = p_new(ctx->part_pool, struct message_part, 1);
+ part->parent = parent;
+
+ /* set child position */
+ part->physical_pos =
+ parent->physical_pos +
+ parent->body_size.physical_size +
+ parent->header_size.physical_size;
+
+ /* add to parent's linked list */
+ *ctx->next_part = part;
+ /* update the parent's end-of-linked-list pointer */
+ struct message_part **next_part = &part->next;
+ array_push_back(&ctx->next_part_stack, &next_part);
+ /* This part is now the new parent for the next message_part_append()
+ call. Its linked list begins with the children pointer. */
+ ctx->next_part = &part->children;
+
+ ctx->part = part;
+ ctx->nested_parts_count++;
+ ctx->total_parts_count++;
+ i_assert(ctx->nested_parts_count < ctx->max_nested_mime_parts);
+ i_assert(ctx->total_parts_count <= ctx->max_total_mime_parts);
+}
+
+static void message_part_finish(struct message_parser_ctx *ctx)
+{
+ struct message_part **const *parent_next_partp;
+
+ if (!ctx->preparsed) {
+ i_assert(ctx->nested_parts_count > 0);
+ ctx->nested_parts_count--;
+
+ parent_next_partp = array_back(&ctx->next_part_stack);
+ array_pop_back(&ctx->next_part_stack);
+ ctx->next_part = *parent_next_partp;
+ }
+
+ message_size_add(&ctx->part->parent->body_size, &ctx->part->body_size);
+ message_size_add(&ctx->part->parent->body_size, &ctx->part->header_size);
+ ctx->part->parent->children_count += 1 + ctx->part->children_count;
+ ctx->part = ctx->part->parent;
+}
+
+static void message_boundary_free(struct message_boundary *b)
+{
+ i_free(b->boundary);
+ i_free(b);
+}
+
+static void
+boundary_remove_until(struct message_parser_ctx *ctx,
+ struct message_boundary *boundary)
+{
+ while (ctx->boundaries != boundary) {
+ struct message_boundary *cur = ctx->boundaries;
+
+ i_assert(cur != NULL);
+ ctx->boundaries = cur->next;
+ message_boundary_free(cur);
+
+ }
+ ctx->boundaries = boundary;
+}
+
+static void parse_next_body_multipart_init(struct message_parser_ctx *ctx)
+{
+ struct message_boundary *b;
+
+ b = i_new(struct message_boundary, 1);
+ b->part = ctx->part;
+ b->boundary = ctx->last_boundary;
+ ctx->last_boundary = NULL;
+ b->len = strlen(b->boundary);
+
+ b->next = ctx->boundaries;
+ ctx->boundaries = b;
+}
+
+static int parse_next_body_message_rfc822_init(struct message_parser_ctx *ctx,
+ struct message_block *block_r)
+{
+ message_part_append(ctx);
+ return parse_next_header_init(ctx, block_r);
+}
+
+static int
+boundary_line_find(struct message_parser_ctx *ctx,
+ const unsigned char *data, size_t size, bool full,
+ struct message_boundary **boundary_r)
+{
+ *boundary_r = NULL;
+
+ if (size < 2) {
+ i_assert(!full);
+
+ if (ctx->input->eof)
+ return -1;
+ ctx->want_count = 2;
+ return 0;
+ }
+
+ if (data[0] != '-' || data[1] != '-') {
+ /* not a boundary, just skip this line */
+ return -1;
+ }
+
+ if (ctx->total_parts_count >= ctx->max_total_mime_parts) {
+ /* can't add any more MIME parts. just stop trying to find
+ more boundaries. */
+ ctx->part->flags |= MESSAGE_PART_FLAG_OVERFLOW;
+ return -1;
+ }
+
+ /* need to find the end of line */
+ data += 2;
+ size -= 2;
+ const unsigned char *lf_pos = memchr(data, '\n', size);
+ if (lf_pos == NULL &&
+ size+2 < BOUNDARY_END_MAX_LEN &&
+ !ctx->input->eof && !full) {
+ /* no LF found */
+ ctx->want_count = BOUNDARY_END_MAX_LEN;
+ return 0;
+ }
+ size_t find_size = size;
+ bool trailing_dashes = FALSE;
+
+ if (lf_pos != NULL) {
+ find_size = lf_pos - data;
+ if (find_size > 0 && data[find_size-1] == '\r')
+ find_size--;
+ if (find_size > 2 && data[find_size-1] == '-' &&
+ data[find_size-2] == '-')
+ trailing_dashes = TRUE;
+ } else if (find_size > BOUNDARY_END_MAX_LEN)
+ find_size = BOUNDARY_END_MAX_LEN;
+
+ *boundary_r = boundary_find(ctx->boundaries, data, find_size,
+ trailing_dashes);
+ if (*boundary_r == NULL)
+ return -1;
+
+ (*boundary_r)->epilogue_found =
+ size >= (*boundary_r)->len + 2 &&
+ memcmp(data + (*boundary_r)->len, "--", 2) == 0;
+ return 1;
+}
+
+static int parse_next_mime_header_init(struct message_parser_ctx *ctx,
+ struct message_block *block_r)
+{
+ message_part_append(ctx);
+ ctx->part->flags |= MESSAGE_PART_FLAG_IS_MIME;
+
+ return parse_next_header_init(ctx, block_r);
+}
+
+static int parse_next_body_skip_boundary_line(struct message_parser_ctx *ctx,
+ struct message_block *block_r)
+{
+ const unsigned char *ptr;
+ int ret;
+ bool full;
+
+ if ((ret = message_parser_read_more(ctx, block_r, &full)) <= 0)
+ return ret;
+
+ ptr = memchr(block_r->data, '\n', block_r->size);
+ if (ptr == NULL) {
+ parse_body_add_block(ctx, block_r);
+ if (block_r->size > 0 &&
+ (ctx->flags & MESSAGE_PARSER_FLAG_INCLUDE_BOUNDARIES) != 0)
+ return 1;
+ return 0;
+ }
+
+ /* found the LF */
+ block_r->size = (ptr - block_r->data) + 1;
+ parse_body_add_block(ctx, block_r);
+
+ if (ctx->boundaries == NULL || ctx->boundaries->part != ctx->part) {
+ /* epilogue */
+ if (ctx->boundaries != NULL)
+ ctx->parse_next_block = parse_next_body_to_boundary;
+ else
+ ctx->parse_next_block = parse_next_body_to_eof;
+ } else {
+ /* a new MIME part begins */
+ ctx->parse_next_block = parse_next_mime_header_init;
+ }
+ if (block_r->size > 0 &&
+ (ctx->flags & MESSAGE_PARSER_FLAG_INCLUDE_BOUNDARIES) != 0)
+ return 1;
+ return ctx->parse_next_block(ctx, block_r);
+}
+
+static int parse_part_finish(struct message_parser_ctx *ctx,
+ struct message_boundary *boundary,
+ struct message_block *block_r, bool first_line)
+{
+ size_t line_size;
+ size_t boundary_len = boundary->len;
+ bool boundary_epilogue_found = boundary->epilogue_found;
+
+ i_assert(ctx->last_boundary == NULL);
+
+ /* get back to parent MIME part, summing the child MIME part sizes
+ into parent's body sizes */
+ while (ctx->part != boundary->part) {
+ message_part_finish(ctx);
+ i_assert(ctx->part != NULL);
+ }
+
+ if (boundary->epilogue_found) {
+ /* this boundary isn't needed anymore */
+ boundary_remove_until(ctx, boundary->next);
+ } else {
+ /* forget about the boundaries we possibly skipped */
+ boundary_remove_until(ctx, boundary);
+ }
+
+ /* the boundary itself should already be in buffer. add that. */
+ block_r->data = i_stream_get_data(ctx->input, &block_r->size);
+ i_assert(block_r->size >= ctx->skip);
+ block_r->data += ctx->skip;
+ /* [[\r]\n]--<boundary>[--] */
+ if (first_line)
+ line_size = 0;
+ else if (block_r->data[0] == '\r') {
+ i_assert(block_r->data[1] == '\n');
+ line_size = 2;
+ } else {
+ i_assert(block_r->data[0] == '\n');
+ line_size = 1;
+ }
+ line_size += 2 + boundary_len + (boundary_epilogue_found ? 2 : 0);
+ i_assert(block_r->size >= ctx->skip + line_size);
+ block_r->size = line_size;
+ parse_body_add_block(ctx, block_r);
+
+ ctx->parse_next_block = parse_next_body_skip_boundary_line;
+
+ if ((ctx->flags & MESSAGE_PARSER_FLAG_INCLUDE_BOUNDARIES) != 0)
+ return 1;
+ return ctx->parse_next_block(ctx, block_r);
+}
+
+static int parse_next_body_to_boundary(struct message_parser_ctx *ctx,
+ struct message_block *block_r)
+{
+ struct message_boundary *boundary = NULL;
+ const unsigned char *data, *cur, *next, *end;
+ size_t boundary_start;
+ int ret;
+ bool full;
+
+ if ((ret = message_parser_read_more(ctx, block_r, &full)) <= 0)
+ return ret;
+
+ data = block_r->data;
+ if (ctx->last_chr == '\n') {
+ /* handle boundary in first line of message. alternatively
+ it's an empty line. */
+ ret = boundary_line_find(ctx, block_r->data,
+ block_r->size, full, &boundary);
+ if (ret >= 0) {
+ return ret == 0 ? 0 :
+ parse_part_finish(ctx, boundary, block_r, TRUE);
+ }
+ }
+
+ i_assert(block_r->size > 0);
+ boundary_start = 0;
+
+ /* skip to beginning of the next line. the first line was
+ handled already. */
+ cur = data; end = data + block_r->size;
+ while ((next = memchr(cur, '\n', end - cur)) != NULL) {
+ cur = next + 1;
+
+ boundary_start = next - data;
+ if (next > data && next[-1] == '\r')
+ boundary_start--;
+
+ if (boundary_start != 0) {
+ /* we can at least skip data until the first [CR]LF.
+ input buffer can't be full anymore. */
+ full = FALSE;
+ }
+
+ ret = boundary_line_find(ctx, cur, end - cur, full, &boundary);
+ if (ret >= 0) {
+ /* found / need more data */
+ if (ret == 0 && boundary_start == 0)
+ ctx->want_count += cur - block_r->data;
+ break;
+ }
+ }
+
+ if (next != NULL) {
+ /* found / need more data */
+ i_assert(ret >= 0);
+ i_assert(!(ret == 0 && full));
+ } else if (boundary_start == 0) {
+ /* no linefeeds in this block. we can just skip it. */
+ ret = 0;
+ if (block_r->data[block_r->size-1] == '\r' && !ctx->eof) {
+ /* this may be the beginning of the \r\n--boundary */
+ block_r->size--;
+ }
+ boundary_start = block_r->size;
+ } else {
+ /* the boundary wasn't found from this data block,
+ we'll need more data. */
+ ret = 0;
+ ctx->want_count = (block_r->size - boundary_start) + 1;
+ }
+
+ if (ret > 0 || (ret == 0 && !ctx->eof)) {
+ /* a) we found the boundary
+ b) we need more data and haven't reached EOF yet
+ so leave CR+LF + last line to buffer */
+ block_r->size = boundary_start;
+ }
+ if (block_r->size != 0) {
+ parse_body_add_block(ctx, block_r);
+
+ if ((ctx->part->flags & MESSAGE_PART_FLAG_MULTIPART) != 0 &&
+ (ctx->flags & MESSAGE_PARSER_FLAG_INCLUDE_MULTIPART_BLOCKS) == 0)
+ return 0;
+
+ return 1;
+ }
+ return ret <= 0 ? ret :
+ parse_part_finish(ctx, boundary, block_r, FALSE);
+}
+
+static int parse_next_body_to_eof(struct message_parser_ctx *ctx,
+ struct message_block *block_r)
+{
+ bool full;
+ int ret;
+
+ if ((ret = message_parser_read_more(ctx, block_r, &full)) <= 0)
+ return ret;
+
+ parse_body_add_block(ctx, block_r);
+
+ if ((ctx->part->flags & MESSAGE_PART_FLAG_MULTIPART) != 0 &&
+ (ctx->flags & MESSAGE_PARSER_FLAG_INCLUDE_MULTIPART_BLOCKS) == 0)
+ return 0;
+
+ return 1;
+}
+
+static void parse_content_type(struct message_parser_ctx *ctx,
+ struct message_header_line *hdr)
+{
+ struct rfc822_parser_context parser;
+ const char *const *results;
+ string_t *content_type;
+ int ret;
+
+ if (ctx->part_seen_content_type)
+ return;
+ ctx->part_seen_content_type = TRUE;
+
+ rfc822_parser_init(&parser, hdr->full_value, hdr->full_value_len, NULL);
+ rfc822_skip_lwsp(&parser);
+
+ content_type = t_str_new(64);
+ ret = rfc822_parse_content_type(&parser, content_type);
+
+ if (strcasecmp(str_c(content_type), "message/rfc822") == 0)
+ ctx->part->flags |= MESSAGE_PART_FLAG_MESSAGE_RFC822;
+ else if (strncasecmp(str_c(content_type), "text", 4) == 0 &&
+ (str_len(content_type) == 4 ||
+ str_data(content_type)[4] == '/'))
+ ctx->part->flags |= MESSAGE_PART_FLAG_TEXT;
+ else if (strncasecmp(str_c(content_type), "multipart/", 10) == 0) {
+ ctx->part->flags |= MESSAGE_PART_FLAG_MULTIPART;
+
+ if (strcasecmp(str_c(content_type)+10, "digest") == 0)
+ ctx->part->flags |= MESSAGE_PART_FLAG_MULTIPART_DIGEST;
+ }
+
+ if (ret < 0 ||
+ (ctx->part->flags & MESSAGE_PART_FLAG_MULTIPART) == 0 ||
+ ctx->last_boundary != NULL) {
+ rfc822_parser_deinit(&parser);
+ return;
+ }
+
+ rfc2231_parse(&parser, &results);
+ for (; *results != NULL; results += 2) {
+ if (strcasecmp(results[0], "boundary") == 0) {
+ /* truncate excessively long boundaries */
+ i_free(ctx->last_boundary);
+ ctx->last_boundary =
+ i_strndup(results[1], BOUNDARY_STRING_MAX_LEN);
+ break;
+ }
+ }
+ rfc822_parser_deinit(&parser);
+}
+
+static bool block_is_at_eoh(const struct message_block *block)
+{
+ if (block->size < 1)
+ return FALSE;
+ if (block->data[0] == '\n')
+ return TRUE;
+ if (block->data[0] == '\r') {
+ if (block->size < 2)
+ return FALSE;
+ if (block->data[1] == '\n')
+ return TRUE;
+ }
+ return FALSE;
+}
+
+static bool parse_too_many_nested_mime_parts(struct message_parser_ctx *ctx)
+{
+ return ctx->nested_parts_count+1 >= ctx->max_nested_mime_parts;
+}
+
+#define MUTEX_FLAGS \
+ (MESSAGE_PART_FLAG_MESSAGE_RFC822 | MESSAGE_PART_FLAG_MULTIPART)
+
+static int parse_next_header(struct message_parser_ctx *ctx,
+ struct message_block *block_r)
+{
+ struct message_part *part = ctx->part;
+ struct message_header_line *hdr;
+ struct message_boundary *boundary;
+ bool full;
+ int ret;
+
+ if ((ret = message_parser_read_more(ctx, block_r, &full)) == 0)
+ return ret;
+
+ if (ret > 0 && block_is_at_eoh(block_r) &&
+ ctx->last_boundary != NULL &&
+ (part->flags & MESSAGE_PART_FLAG_IS_MIME) != 0) {
+ /* we are at the end of headers and we've determined that we're
+ going to start a multipart. add the boundary already here
+ at this point so we can reliably determine whether the
+ "\n--boundary" belongs to us or to a previous boundary.
+ this is a problem if the boundary prefixes are identical,
+ because MIME requires only the prefix to match. */
+ if (!parse_too_many_nested_mime_parts(ctx)) {
+ parse_next_body_multipart_init(ctx);
+ ctx->multipart = TRUE;
+ } else {
+ part->flags |= MESSAGE_PART_FLAG_OVERFLOW;
+ part->flags &= ENUM_NEGATE(MESSAGE_PART_FLAG_MULTIPART);
+ }
+ }
+
+ /* before parsing the header see if we can find a --boundary from here.
+ we're guaranteed to be at the beginning of the line here. */
+ if (ret > 0) {
+ ret = ctx->boundaries == NULL ? -1 :
+ boundary_line_find(ctx, block_r->data,
+ block_r->size, full, &boundary);
+ if (ret > 0 && boundary->part == ctx->part) {
+ /* our own body begins with our own --boundary.
+ we don't want to handle that yet. */
+ ret = -1;
+ }
+ }
+ if (ret < 0) {
+ /* no boundary */
+ ret = message_parse_header_next(ctx->hdr_parser_ctx, &hdr);
+ if (ret == 0 || (ret < 0 && ctx->input->stream_errno != 0)) {
+ ctx->want_count = i_stream_get_data_size(ctx->input) + 1;
+ return ret;
+ }
+ } else if (ret == 0) {
+ /* need more data */
+ return 0;
+ } else {
+ /* boundary found. stop parsing headers here. The previous
+ [CR]LF belongs to the MIME boundary though. */
+ if (ctx->prev_hdr_newline_size > 0) {
+ i_assert(ctx->part->header_size.lines > 0);
+ /* remove the newline size from the MIME header */
+ ctx->part->header_size.lines--;
+ ctx->part->header_size.physical_size -=
+ ctx->prev_hdr_newline_size;
+ ctx->part->header_size.virtual_size -= 2;
+ /* add the newline size to the parent's body */
+ ctx->part->parent->body_size.lines++;
+ ctx->part->parent->body_size.physical_size +=
+ ctx->prev_hdr_newline_size;
+ ctx->part->parent->body_size.virtual_size += 2;
+ }
+ hdr = NULL;
+ }
+
+ if (hdr != NULL) {
+ if (hdr->eoh)
+ ;
+ else if (strcasecmp(hdr->name, "Mime-Version") == 0) {
+ /* it's MIME. Content-* headers are valid */
+ part->flags |= MESSAGE_PART_FLAG_IS_MIME;
+ } else if (strcasecmp(hdr->name, "Content-Type") == 0) {
+ if ((ctx->flags &
+ MESSAGE_PARSER_FLAG_MIME_VERSION_STRICT) == 0)
+ part->flags |= MESSAGE_PART_FLAG_IS_MIME;
+
+ if (hdr->continues)
+ hdr->use_full_value = TRUE;
+ else T_BEGIN {
+ parse_content_type(ctx, hdr);
+ } T_END;
+ }
+
+ block_r->hdr = hdr;
+ block_r->size = 0;
+ ctx->prev_hdr_newline_size = hdr->no_newline ? 0 :
+ (hdr->crlf_newline ? 2 : 1);
+ return 1;
+ }
+
+ /* end of headers */
+ if ((part->flags & MESSAGE_PART_FLAG_IS_MIME) == 0) {
+ /* It's not MIME. Reset everything we found from
+ Content-Type. */
+ i_assert(!ctx->multipart);
+ part->flags = 0;
+ }
+ i_free(ctx->last_boundary);
+
+ if (!ctx->part_seen_content_type ||
+ (part->flags & MESSAGE_PART_FLAG_IS_MIME) == 0) {
+ if (part->parent != NULL &&
+ (part->parent->flags &
+ MESSAGE_PART_FLAG_MULTIPART_DIGEST) != 0) {
+ /* when there's no content-type specified and we're
+ below multipart/digest, assume message/rfc822
+ content-type */
+ part->flags |= MESSAGE_PART_FLAG_MESSAGE_RFC822;
+ } else {
+ /* otherwise we default to text/plain */
+ part->flags |= MESSAGE_PART_FLAG_TEXT;
+ }
+ }
+
+ if (message_parse_header_has_nuls(ctx->hdr_parser_ctx))
+ part->flags |= MESSAGE_PART_FLAG_HAS_NULS;
+ message_parse_header_deinit(&ctx->hdr_parser_ctx);
+
+ i_assert((part->flags & MUTEX_FLAGS) != MUTEX_FLAGS);
+
+ ctx->last_chr = '\n';
+ if (ctx->multipart) {
+ i_assert(ctx->last_boundary == NULL);
+ ctx->multipart = FALSE;
+ ctx->parse_next_block = parse_next_body_to_boundary;
+ } else if ((part->flags & MESSAGE_PART_FLAG_MESSAGE_RFC822) == 0) {
+ /* Not message/rfc822 */
+ if (ctx->boundaries != NULL)
+ ctx->parse_next_block = parse_next_body_to_boundary;
+ else
+ ctx->parse_next_block = parse_next_body_to_eof;
+ } else if (!parse_too_many_nested_mime_parts(ctx) &&
+ ctx->total_parts_count < ctx->max_total_mime_parts) {
+ /* message/rfc822 - not reached MIME part limits yet */
+ ctx->parse_next_block = parse_next_body_message_rfc822_init;
+ } else {
+ /* message/rfc822 - already reached MIME part limits */
+ part->flags |= MESSAGE_PART_FLAG_OVERFLOW;
+ part->flags &= ENUM_NEGATE(MESSAGE_PART_FLAG_MESSAGE_RFC822);
+ if (ctx->boundaries != NULL)
+ ctx->parse_next_block = parse_next_body_to_boundary;
+ else
+ ctx->parse_next_block = parse_next_body_to_eof;
+ }
+
+ ctx->want_count = 1;
+
+ /* return empty block as end of headers */
+ block_r->hdr = NULL;
+ block_r->size = 0;
+ return 1;
+}
+
+static int parse_next_header_init(struct message_parser_ctx *ctx,
+ struct message_block *block_r)
+{
+ i_assert(ctx->hdr_parser_ctx == NULL);
+
+ ctx->hdr_parser_ctx =
+ message_parse_header_init(ctx->input, &ctx->part->header_size,
+ ctx->hdr_flags);
+ ctx->part_seen_content_type = FALSE;
+ ctx->prev_hdr_newline_size = 0;
+
+ ctx->parse_next_block = parse_next_header;
+ return parse_next_header(ctx, block_r);
+}
+
+struct message_parser_ctx *
+message_parser_init_int(struct istream *input,
+ const struct message_parser_settings *set)
+{
+ struct message_parser_ctx *ctx;
+
+ ctx = i_new(struct message_parser_ctx, 1);
+ ctx->hdr_flags = set->hdr_flags;
+ ctx->flags = set->flags;
+ ctx->max_nested_mime_parts = set->max_nested_mime_parts != 0 ?
+ set->max_nested_mime_parts :
+ MESSAGE_PARSER_DEFAULT_MAX_NESTED_MIME_PARTS;
+ ctx->max_total_mime_parts = set->max_total_mime_parts != 0 ?
+ set->max_total_mime_parts :
+ MESSAGE_PARSER_DEFAULT_MAX_TOTAL_MIME_PARTS;
+ ctx->input = input;
+ i_stream_ref(input);
+ return ctx;
+}
+
+struct message_parser_ctx *
+message_parser_init(pool_t part_pool, struct istream *input,
+ const struct message_parser_settings *set)
+{
+ struct message_parser_ctx *ctx;
+
+ ctx = message_parser_init_int(input, set);
+ ctx->part_pool = part_pool;
+ ctx->parts = ctx->part = p_new(part_pool, struct message_part, 1);
+ ctx->next_part = &ctx->part->children;
+ ctx->parse_next_block = parse_next_header_init;
+ ctx->total_parts_count = 1;
+ i_array_init(&ctx->next_part_stack, 4);
+ return ctx;
+}
+
+void message_parser_deinit(struct message_parser_ctx **_ctx,
+ struct message_part **parts_r)
+{
+ const char *error;
+
+ i_assert((**_ctx).preparsed == FALSE);
+ if (message_parser_deinit_from_parts(_ctx, parts_r, &error) < 0)
+ i_panic("message_parser_deinit_from_parts: %s", error);
+}
+
+int message_parser_deinit_from_parts(struct message_parser_ctx **_ctx,
+ struct message_part **parts_r,
+ const char **error_r)
+{
+ struct message_parser_ctx *ctx = *_ctx;
+ int ret = ctx->broken_reason != NULL ? -1 : 0;
+
+ *_ctx = NULL;
+ *parts_r = ctx->parts;
+ *error_r = ctx->broken_reason;
+
+ if (ctx->hdr_parser_ctx != NULL)
+ message_parse_header_deinit(&ctx->hdr_parser_ctx);
+ if (ctx->part != NULL) {
+ /* If the whole message has been parsed, the parts are
+ usually finished in message_parser_parse_next_block().
+ However, it's possible that the caller finishes reading
+ through the istream without calling
+ message_parser_parse_next_block() afterwards. In that case
+ we still need to finish these parts. */
+ while (ctx->part->parent != NULL)
+ message_part_finish(ctx);
+ }
+ boundary_remove_until(ctx, NULL);
+ i_assert(ctx->nested_parts_count == 0);
+
+ i_stream_unref(&ctx->input);
+ array_free(&ctx->next_part_stack);
+ i_free(ctx->last_boundary);
+ i_free(ctx);
+ i_assert(ret < 0 || *parts_r != NULL);
+ return ret;
+}
+
+int message_parser_parse_next_block(struct message_parser_ctx *ctx,
+ struct message_block *block_r)
+{
+ int ret;
+ bool eof = FALSE, full;
+
+ i_zero(block_r);
+
+ while ((ret = ctx->parse_next_block(ctx, block_r)) == 0) {
+ ret = message_parser_read_more(ctx, block_r, &full);
+ if (ret == 0) {
+ i_assert(!ctx->input->blocking);
+ return 0;
+ }
+ if (ret == -1) {
+ i_assert(!eof);
+ eof = TRUE;
+ }
+ }
+
+ block_r->part = ctx->part;
+
+ if (ret < 0 && ctx->part != NULL) {
+ /* Successful EOF or unexpected failure */
+ i_assert(ctx->input->eof || ctx->input->closed ||
+ ctx->input->stream_errno != 0 ||
+ ctx->broken_reason != NULL);
+ while (ctx->part->parent != NULL)
+ message_part_finish(ctx);
+ }
+
+ if (block_r->size == 0) {
+ /* data isn't supposed to be read, so make sure it's NULL */
+ block_r->data = NULL;
+ }
+ return ret;
+}
+
+#undef message_parser_parse_header
+void message_parser_parse_header(struct message_parser_ctx *ctx,
+ struct message_size *hdr_size,
+ message_part_header_callback_t *callback,
+ void *context)
+{
+ struct message_block block;
+ int ret;
+
+ while ((ret = message_parser_parse_next_block(ctx, &block)) > 0) {
+ callback(block.part, block.hdr, context);
+
+ if (block.hdr == NULL)
+ break;
+ }
+ i_assert(ret != 0);
+ i_assert(ctx->part != NULL);
+
+ if (ret < 0) {
+ /* well, can't return error so fake end of headers */
+ callback(ctx->part, NULL, context);
+ }
+
+ *hdr_size = ctx->part->header_size;
+}
+
+#undef message_parser_parse_body
+void message_parser_parse_body(struct message_parser_ctx *ctx,
+ message_part_header_callback_t *hdr_callback,
+ void *context)
+{
+ struct message_block block;
+ int ret;
+
+ while ((ret = message_parser_parse_next_block(ctx, &block)) > 0) {
+ if (block.size == 0 && hdr_callback != NULL)
+ hdr_callback(block.part, block.hdr, context);
+ }
+ i_assert(ret != 0);
+}
diff --git a/src/lib-mail/message-parser.h b/src/lib-mail/message-parser.h
new file mode 100644
index 0000000..f19e526
--- /dev/null
+++ b/src/lib-mail/message-parser.h
@@ -0,0 +1,112 @@
+#ifndef MESSAGE_PARSER_H
+#define MESSAGE_PARSER_H
+
+#include "message-header-parser.h"
+#include "message-part.h"
+
+enum message_parser_flags {
+ /* Don't return message bodies in message_blocks. */
+ MESSAGE_PARSER_FLAG_SKIP_BODY_BLOCK = 0x01,
+ /* Buggy software creates Content-Type: headers without Mime-Version:
+ header. By default we allow this and assume message is MIME if
+ Content-Type: is found. This flag disables this. */
+ MESSAGE_PARSER_FLAG_MIME_VERSION_STRICT = 0x02,
+ /* Return multipart (preamble and epilogue) blocks */
+ MESSAGE_PARSER_FLAG_INCLUDE_MULTIPART_BLOCKS = 0x04,
+ /* Return --boundary lines */
+ MESSAGE_PARSER_FLAG_INCLUDE_BOUNDARIES = 0x08
+};
+
+#define MESSAGE_PARSER_DEFAULT_MAX_NESTED_MIME_PARTS 100
+#define MESSAGE_PARSER_DEFAULT_MAX_TOTAL_MIME_PARTS 10000
+
+struct message_parser_settings {
+ enum message_header_parser_flags hdr_flags;
+ enum message_parser_flags flags;
+
+ /* Maximum nested MIME parts.
+ 0 = MESSAGE_PARSER_DEFAULT_MAX_NESTED_MIME_PARTS. */
+ unsigned int max_nested_mime_parts;
+ /* Maximum MIME parts in total.
+ 0 = MESSAGE_PARSER_DEFAULT_MAX_TOTAL_MIME_PARTS. */
+ unsigned int max_total_mime_parts;
+};
+
+struct message_parser_ctx;
+
+struct message_block {
+ /* Message part this block belongs to */
+ struct message_part *part;
+
+ /* non-NULL if a header line was read */
+ struct message_header_line *hdr;
+
+ /* hdr = NULL, size = 0 block returned at the end of headers for the
+ empty line between header and body (unless the header is truncated).
+ Later on data and size>0 is returned for blocks of mail body that
+ is read (see message_parser_flags for what is actually returned) */
+ const unsigned char *data;
+ size_t size;
+};
+
+/* called once with hdr = NULL at the end of headers */
+typedef void message_part_header_callback_t(struct message_part *part,
+ struct message_header_line *hdr,
+ void *context);
+
+extern message_part_header_callback_t *null_message_part_header_callback;
+
+/* Initialize message parser. part_spool specifies where struct message_parts
+ are allocated from. */
+struct message_parser_ctx *
+message_parser_init(pool_t part_pool, struct istream *input,
+ const struct message_parser_settings *set);
+/* Deinitialize message parser. The ctx must NOT have been created by
+ message_parser_init_from_parts(). */
+void message_parser_deinit(struct message_parser_ctx **ctx,
+ struct message_part **parts_r);
+/* Use preparsed parts to speed up parsing. */
+struct message_parser_ctx *
+message_parser_init_from_parts(struct message_part *parts,
+ struct istream *input,
+ const struct message_parser_settings *set);
+/* Same as message_parser_deinit(), but return an error message describing
+ why the preparsed parts didn't match the message. This can also safely be
+ called even when preparsed parts weren't used - it'll always just return
+ success in that case. */
+int message_parser_deinit_from_parts(struct message_parser_ctx **_ctx,
+ struct message_part **parts_r,
+ const char **error_r);
+
+/* Read the next block of a message. Returns 1 if block is returned, 0 if
+ input stream is non-blocking and more data needs to be read, -1 when all is
+ done or error occurred (see stream's error status). */
+int message_parser_parse_next_block(struct message_parser_ctx *ctx,
+ struct message_block *block_r);
+
+/* Read and parse header. */
+void message_parser_parse_header(struct message_parser_ctx *ctx,
+ struct message_size *hdr_size,
+ message_part_header_callback_t *callback,
+ void *context) ATTR_NULL(4);
+#define message_parser_parse_header(ctx, hdr_size, callback, context) \
+ message_parser_parse_header(ctx, hdr_size - \
+ CALLBACK_TYPECHECK(callback, void (*)( \
+ struct message_part *, \
+ struct message_header_line *, typeof(context))), \
+ (message_part_header_callback_t *)callback, context)
+
+/* Read and parse body. If message is a MIME multipart or message/rfc822
+ message, hdr_callback is called for all headers. body_callback is called
+ for the body content. */
+void message_parser_parse_body(struct message_parser_ctx *ctx,
+ message_part_header_callback_t *hdr_callback,
+ void *context) ATTR_NULL(3);
+#define message_parser_parse_body(ctx, callback, context) \
+ message_parser_parse_body(ctx, \
+ (message_part_header_callback_t *)callback, \
+ (void *)((uintptr_t)context - CALLBACK_TYPECHECK(callback, \
+ void (*)(struct message_part *, \
+ struct message_header_line *, typeof(context)))))
+
+#endif
diff --git a/src/lib-mail/message-part-data.c b/src/lib-mail/message-part-data.c
new file mode 100644
index 0000000..a5771f8
--- /dev/null
+++ b/src/lib-mail/message-part-data.c
@@ -0,0 +1,594 @@
+/* Copyright (c) 2014-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "str.h"
+#include "wildcard-match.h"
+#include "array.h"
+#include "rfc822-parser.h"
+#include "rfc2231-parser.h"
+#include "message-address.h"
+#include "message-header-parser.h"
+
+#include "message-part-data.h"
+
+const char *message_part_envelope_headers[] = {
+ "Date", "Subject", "From", "Sender", "Reply-To",
+ "To", "Cc", "Bcc", "In-Reply-To", "Message-ID",
+ NULL
+};
+
+/*
+ *
+ */
+
+bool message_part_data_is_plain_7bit(const struct message_part *part)
+{
+ const struct message_part_data *data = part->data;
+
+ i_assert(data != NULL);
+ i_assert(part->parent == NULL);
+
+ /* if content-type is text/xxx we don't have to check any
+ multipart stuff */
+ if ((part->flags & MESSAGE_PART_FLAG_TEXT) == 0)
+ return FALSE;
+ if (part->next != NULL || part->children != NULL)
+ return FALSE; /* shouldn't happen normally.. */
+
+ /* must be text/plain */
+ if (data->content_subtype != NULL &&
+ strcasecmp(data->content_subtype, "plain") != 0)
+ return FALSE;
+
+ /* only allowed parameter is charset=us-ascii, which is also default */
+ if (data->content_type_params_count == 0) {
+ /* charset defaults to us-ascii */
+ } else if (data->content_type_params_count != 1 ||
+ strcasecmp(data->content_type_params[0].name, "charset") != 0 ||
+ strcasecmp(data->content_type_params[0].value,
+ MESSAGE_PART_DEFAULT_CHARSET) != 0)
+ return FALSE;
+
+ if (data->content_id != NULL ||
+ data->content_description != NULL)
+ return FALSE;
+
+ if (data->content_transfer_encoding != NULL &&
+ strcasecmp(data->content_transfer_encoding, "7bit") != 0)
+ return FALSE;
+
+ /* BODYSTRUCTURE checks: */
+ if (data->content_md5 != NULL ||
+ data->content_disposition != NULL ||
+ data->content_language != NULL ||
+ data->content_location != NULL)
+ return FALSE;
+
+ return TRUE;
+}
+
+bool message_part_data_get_filename(const struct message_part *part,
+ const char **filename_r)
+{
+ const struct message_part_data *data = part->data;
+ const struct message_part_param *params;
+ unsigned int params_count, i;
+
+ i_assert(data != NULL);
+
+ params = data->content_disposition_params;
+ params_count = data->content_disposition_params_count;
+
+ if (data->content_disposition != NULL &&
+ strcasecmp(data->content_disposition, "attachment") != 0) {
+ return FALSE;
+ }
+ for (i = 0; i < params_count; i++) {
+ if (strcasecmp(params[i].name, "filename") == 0 &&
+ params[i].value != NULL) {
+ *filename_r = params[i].value;
+ return TRUE;
+ }
+ }
+ return FALSE;
+}
+
+/*
+ * Header parsing
+ */
+
+/* Message part envelope */
+
+enum envelope_field {
+ ENVELOPE_FIELD_DATE = 0,
+ ENVELOPE_FIELD_SUBJECT,
+ ENVELOPE_FIELD_FROM,
+ ENVELOPE_FIELD_SENDER,
+ ENVELOPE_FIELD_REPLY_TO,
+ ENVELOPE_FIELD_TO,
+ ENVELOPE_FIELD_CC,
+ ENVELOPE_FIELD_BCC,
+ ENVELOPE_FIELD_IN_REPLY_TO,
+ ENVELOPE_FIELD_MESSAGE_ID,
+
+ ENVELOPE_FIELD_UNKNOWN
+};
+
+static enum envelope_field
+envelope_get_field(const char *name)
+{
+ switch (*name) {
+ case 'B':
+ case 'b':
+ if (strcasecmp(name, "Bcc") == 0)
+ return ENVELOPE_FIELD_BCC;
+ break;
+ case 'C':
+ case 'c':
+ if (strcasecmp(name, "Cc") == 0)
+ return ENVELOPE_FIELD_CC;
+ break;
+ case 'D':
+ case 'd':
+ if (strcasecmp(name, "Date") == 0)
+ return ENVELOPE_FIELD_DATE;
+ break;
+ case 'F':
+ case 'f':
+ if (strcasecmp(name, "From") == 0)
+ return ENVELOPE_FIELD_FROM;
+ break;
+ case 'I':
+ case 'i':
+ if (strcasecmp(name, "In-reply-to") == 0)
+ return ENVELOPE_FIELD_IN_REPLY_TO;
+ break;
+ case 'M':
+ case 'm':
+ if (strcasecmp(name, "Message-id") == 0)
+ return ENVELOPE_FIELD_MESSAGE_ID;
+ break;
+ case 'R':
+ case 'r':
+ if (strcasecmp(name, "Reply-to") == 0)
+ return ENVELOPE_FIELD_REPLY_TO;
+ break;
+ case 'S':
+ case 's':
+ if (strcasecmp(name, "Subject") == 0)
+ return ENVELOPE_FIELD_SUBJECT;
+ if (strcasecmp(name, "Sender") == 0)
+ return ENVELOPE_FIELD_SENDER;
+ break;
+ case 'T':
+ case 't':
+ if (strcasecmp(name, "To") == 0)
+ return ENVELOPE_FIELD_TO;
+ break;
+ }
+
+ return ENVELOPE_FIELD_UNKNOWN;
+}
+
+void message_part_envelope_parse_from_header(pool_t pool,
+ struct message_part_envelope **data,
+ struct message_header_line *hdr)
+{
+ struct message_part_envelope *d;
+ enum envelope_field field;
+ struct message_address **addr_p, *addr;
+ const char **str_p;
+
+ if (*data == NULL) {
+ *data = p_new(pool, struct message_part_envelope, 1);
+ }
+
+ if (hdr == NULL)
+ return;
+ field = envelope_get_field(hdr->name);
+ if (field == ENVELOPE_FIELD_UNKNOWN)
+ return;
+
+ if (hdr->continues) {
+ /* wait for full value */
+ hdr->use_full_value = TRUE;
+ return;
+ }
+
+ d = *data;
+ addr_p = NULL; str_p = NULL;
+ switch (field) {
+ case ENVELOPE_FIELD_DATE:
+ str_p = &d->date;
+ break;
+ case ENVELOPE_FIELD_SUBJECT:
+ str_p = &d->subject;
+ break;
+ case ENVELOPE_FIELD_MESSAGE_ID:
+ str_p = &d->message_id;
+ break;
+ case ENVELOPE_FIELD_IN_REPLY_TO:
+ str_p = &d->in_reply_to;
+ break;
+
+ case ENVELOPE_FIELD_CC:
+ addr_p = &d->cc;
+ break;
+ case ENVELOPE_FIELD_BCC:
+ addr_p = &d->bcc;
+ break;
+ case ENVELOPE_FIELD_FROM:
+ addr_p = &d->from;
+ break;
+ case ENVELOPE_FIELD_SENDER:
+ addr_p = &d->sender;
+ break;
+ case ENVELOPE_FIELD_TO:
+ addr_p = &d->to;
+ break;
+ case ENVELOPE_FIELD_REPLY_TO:
+ addr_p = &d->reply_to;
+ break;
+ case ENVELOPE_FIELD_UNKNOWN:
+ i_unreached();
+ }
+
+ if (addr_p != NULL) {
+ addr = message_address_parse(pool, hdr->full_value,
+ hdr->full_value_len,
+ UINT_MAX,
+ MESSAGE_ADDRESS_PARSE_FLAG_FILL_MISSING);
+ /* Merge multiple headers the same as if they were comma
+ separated in a single line. This is better from security
+ point of view, because attacker could intentionally write
+ addresses in a way that e.g. the first From header is
+ validated while MUA only shows the second From header. */
+ while (*addr_p != NULL)
+ addr_p = &(*addr_p)->next;
+ *addr_p = addr;
+ } else if (str_p != NULL) {
+ *str_p = message_header_strdup(pool, hdr->full_value,
+ hdr->full_value_len);
+ }
+}
+
+/* Message part data */
+
+static void
+parse_mime_parameters(struct rfc822_parser_context *parser,
+ pool_t pool, const struct message_part_param **params_r,
+ unsigned int *params_count_r)
+{
+ const char *const *results;
+ struct message_part_param *params;
+ unsigned int params_count, i;
+
+ rfc2231_parse(parser, &results);
+
+ params_count = str_array_length(results);
+ i_assert((params_count % 2) == 0);
+ params_count /= 2;
+
+ if (params_count > 0) {
+ params = p_new(pool, struct message_part_param, params_count);
+ for (i = 0; i < params_count; i++) {
+ params[i].name = p_strdup(pool, results[i*2+0]);
+ params[i].value = p_strdup(pool, results[i*2+1]);
+ }
+ *params_r = params;
+ }
+
+ *params_count_r = params_count;
+}
+
+static void
+parse_content_type(struct message_part_data *data,
+ pool_t pool, struct message_header_line *hdr)
+{
+ struct rfc822_parser_context parser;
+ string_t *str;
+ const char *value;
+ unsigned int i;
+ int ret;
+
+ rfc822_parser_init(&parser, hdr->full_value, hdr->full_value_len, NULL);
+ rfc822_skip_lwsp(&parser);
+
+ str = t_str_new(256);
+ ret = rfc822_parse_content_type(&parser, str);
+
+ /* Save content type and subtype */
+ value = str_c(str);
+ for (i = 0; value[i] != '\0'; i++) {
+ if (value[i] == '/') {
+ data->content_subtype = p_strdup(pool, value + i+1);
+ break;
+ }
+ }
+ str_truncate(str, i);
+ data->content_type = p_strdup(pool, str_c(str));
+ if (data->content_subtype == NULL) {
+ /* The Content-Type is invalid. Don't leave it NULL so that
+ callers can assume that if content_type != NULL,
+ content_subtype != NULL also. */
+ data->content_subtype = p_strdup(pool, "");
+ }
+
+ if (ret < 0) {
+ /* Content-Type is broken, but we wanted to get it as well as
+ we could. Don't try to read the parameters anymore though.
+
+ We don't completely ignore a broken Content-Type, because
+ then it would be written as text/plain. This would cause a
+ mismatch with the message_part's MESSAGE_PART_FLAG_TEXT. */
+ return;
+ }
+
+ parse_mime_parameters(&parser, pool,
+ &data->content_type_params,
+ &data->content_type_params_count);
+ rfc822_parser_deinit(&parser);
+}
+
+static void
+parse_content_transfer_encoding(struct message_part_data *data,
+ pool_t pool, struct message_header_line *hdr)
+{
+ struct rfc822_parser_context parser;
+ string_t *str;
+
+ rfc822_parser_init(&parser, hdr->full_value, hdr->full_value_len, NULL);
+ rfc822_skip_lwsp(&parser);
+
+ str = t_str_new(256);
+ if (rfc822_parse_mime_token(&parser, str) >= 0 &&
+ rfc822_skip_lwsp(&parser) == 0 && str_len(str) > 0) {
+ data->content_transfer_encoding =
+ p_strdup(pool, str_c(str));
+ }
+ rfc822_parser_deinit(&parser);
+}
+
+static void
+parse_content_disposition(struct message_part_data *data,
+ pool_t pool, struct message_header_line *hdr)
+{
+ struct rfc822_parser_context parser;
+ string_t *str;
+
+ rfc822_parser_init(&parser, hdr->full_value, hdr->full_value_len, NULL);
+ rfc822_skip_lwsp(&parser);
+
+ str = t_str_new(256);
+ if (rfc822_parse_mime_token(&parser, str) < 0) {
+ rfc822_parser_deinit(&parser);
+ return;
+ }
+ data->content_disposition = p_strdup(pool, str_c(str));
+
+ parse_mime_parameters(&parser, pool,
+ &data->content_disposition_params,
+ &data->content_disposition_params_count);
+ rfc822_parser_deinit(&parser);
+}
+
+static void
+parse_content_language(struct message_part_data *data,
+ pool_t pool, const unsigned char *value, size_t value_len)
+{
+ struct rfc822_parser_context parser;
+ ARRAY_TYPE(const_string) langs;
+ string_t *str;
+
+ /* Language-Header = "Content-Language" ":" 1#Language-tag
+ Language-Tag = Primary-tag *( "-" Subtag )
+ Primary-tag = 1*8ALPHA
+ Subtag = 1*8ALPHA */
+
+ rfc822_parser_init(&parser, value, value_len, NULL);
+
+ t_array_init(&langs, 16);
+ str = t_str_new(128);
+
+ rfc822_skip_lwsp(&parser);
+ while (rfc822_parse_atom(&parser, str) >= 0) {
+ const char *lang = p_strdup(pool, str_c(str));
+
+ array_push_back(&langs, &lang);
+ str_truncate(str, 0);
+
+ if (parser.data >= parser.end || *parser.data != ',')
+ break;
+ parser.data++;
+ rfc822_skip_lwsp(&parser);
+ }
+ rfc822_parser_deinit(&parser);
+
+ if (array_count(&langs) > 0) {
+ array_append_zero(&langs);
+ data->content_language =
+ p_strarray_dup(pool, array_front(&langs));
+ }
+}
+
+static void
+parse_content_header(struct message_part_data *data,
+ pool_t pool, struct message_header_line *hdr)
+{
+ const char *name = hdr->name + strlen("Content-");
+
+ if (hdr->continues) {
+ hdr->use_full_value = TRUE;
+ return;
+ }
+
+ switch (*name) {
+ case 'i':
+ case 'I':
+ if (strcasecmp(name, "ID") == 0 && data->content_id == NULL)
+ data->content_id =
+ message_header_strdup(pool, hdr->full_value,
+ hdr->full_value_len);
+ break;
+
+ case 'm':
+ case 'M':
+ if (strcasecmp(name, "MD5") == 0 && data->content_md5 == NULL)
+ data->content_md5 =
+ message_header_strdup(pool, hdr->full_value,
+ hdr->full_value_len);
+ break;
+
+ case 't':
+ case 'T':
+ if (strcasecmp(name, "Type") == 0 && data->content_type == NULL)
+ parse_content_type(data, pool, hdr);
+ else if (strcasecmp(name, "Transfer-Encoding") == 0 &&
+ data->content_transfer_encoding == NULL)
+ parse_content_transfer_encoding(data, pool, hdr);
+ break;
+
+ case 'l':
+ case 'L':
+ if (strcasecmp(name, "Language") == 0 &&
+ data->content_language == NULL) {
+ parse_content_language(data, pool,
+ hdr->full_value, hdr->full_value_len);
+ } else if (strcasecmp(name, "Location") == 0 &&
+ data->content_location == NULL) {
+ data->content_location =
+ message_header_strdup(pool, hdr->full_value,
+ hdr->full_value_len);
+ }
+ break;
+
+ case 'd':
+ case 'D':
+ if (strcasecmp(name, "Description") == 0 &&
+ data->content_description == NULL)
+ data->content_description =
+ message_header_strdup(pool, hdr->full_value,
+ hdr->full_value_len);
+ else if (strcasecmp(name, "Disposition") == 0 &&
+ data->content_disposition_params == NULL)
+ parse_content_disposition(data, pool, hdr);
+ break;
+ }
+}
+
+void message_part_data_parse_from_header(pool_t pool,
+ struct message_part *part,
+ struct message_header_line *hdr)
+{
+ struct message_part_data *part_data;
+ struct message_part_envelope *envelope;
+ bool parent_rfc822;
+
+ if (hdr == NULL) {
+ if (part->data == NULL) {
+ /* no Content-* headers. add an empty context
+ structure anyway. */
+ part->data = p_new(pool, struct message_part_data, 1);
+ } else if ((part->flags & MESSAGE_PART_FLAG_IS_MIME) == 0) {
+ /* If there was no Mime-Version, forget all
+ the Content-stuff */
+ part_data = part->data;
+ envelope = part_data->envelope;
+
+ i_zero(part_data);
+ part_data->envelope = envelope;
+ }
+ return;
+ }
+
+ if (hdr->eoh)
+ return;
+
+ parent_rfc822 = part->parent != NULL &&
+ (part->parent->flags & MESSAGE_PART_FLAG_MESSAGE_RFC822) != 0;
+ if (!parent_rfc822 && strncasecmp(hdr->name, "Content-", 8) != 0)
+ return;
+
+ if (part->data == NULL) {
+ /* initialize message part data */
+ part->data = p_new(pool, struct message_part_data, 1);
+ }
+ part_data = part->data;
+
+ if (strncasecmp(hdr->name, "Content-", 8) == 0) {
+ T_BEGIN {
+ parse_content_header(part_data, pool, hdr);
+ } T_END;
+ }
+
+ if (parent_rfc822) {
+ /* message/rfc822, we need the envelope */
+ message_part_envelope_parse_from_header(pool, &part_data->envelope, hdr);
+ }
+}
+
+bool message_part_has_content_types(struct message_part *part,
+ const char *const *types)
+{
+ struct message_part_data *data = part->data;
+ bool ret = TRUE;
+ const char *const *ptr;
+ const char *content_type;
+
+ i_assert(data != NULL);
+
+ if (data->content_type == NULL)
+ return FALSE;
+ else if (data->content_subtype == NULL)
+ content_type = t_strdup_printf("%s/", data->content_type);
+ else
+ content_type = t_strdup_printf("%s/%s", data->content_type,
+ data->content_subtype);
+ for(ptr = types; *ptr != NULL; ptr++) {
+ bool exclude = (**ptr == '!');
+ if (wildcard_match_icase(content_type, (*ptr)+(exclude?1:0)))
+ ret = !exclude;
+ }
+
+ return ret;
+}
+
+bool message_part_has_parameter(struct message_part *part, const char *parameter,
+ bool has_value)
+{
+ struct message_part_data *data = part->data;
+
+ i_assert(data != NULL);
+
+ for (unsigned int i = 0; i < data->content_disposition_params_count; i++) {
+ const struct message_part_param *param =
+ &data->content_disposition_params[i];
+ if (strcasecmp(param->name, parameter) == 0 &&
+ (!has_value || *param->value != '\0')) {
+ return TRUE;
+ }
+ }
+ return FALSE;
+}
+
+bool message_part_is_attachment(struct message_part *part,
+ const struct message_part_attachment_settings *set)
+{
+ struct message_part_data *data = part->data;
+
+ i_assert(data != NULL);
+
+ /* see if the content-type is excluded */
+ if (set->content_type_filter != NULL &&
+ !message_part_has_content_types(part, set->content_type_filter))
+ return FALSE;
+
+ /* accept any attachment, or any inlined attachment with filename,
+ unless inlined ones are excluded */
+ if (null_strcasecmp(data->content_disposition, "attachment") == 0 ||
+ (!set->exclude_inlined &&
+ null_strcasecmp(data->content_disposition, "inline") == 0 &&
+ message_part_has_parameter(part, "filename", FALSE)))
+ return TRUE;
+ return FALSE;
+}
diff --git a/src/lib-mail/message-part-data.h b/src/lib-mail/message-part-data.h
new file mode 100644
index 0000000..5ff9ffe
--- /dev/null
+++ b/src/lib-mail/message-part-data.h
@@ -0,0 +1,101 @@
+#ifndef MESSAGE_PART_DATA_H
+#define MESSAGE_PART_DATA_H
+
+#include "message-part.h"
+
+#define MESSAGE_PART_DEFAULT_CHARSET "us-ascii"
+
+struct message_header_line;
+
+struct message_part_param {
+ const char *name;
+ const char *value;
+};
+
+struct message_part_envelope {
+ const char *date, *subject;
+ struct message_address *from, *sender, *reply_to;
+ struct message_address *to, *cc, *bcc;
+
+ const char *in_reply_to, *message_id;
+};
+
+struct message_part_data {
+ const char *content_type, *content_subtype;
+ const struct message_part_param *content_type_params;
+ unsigned int content_type_params_count;
+
+ const char *content_transfer_encoding;
+ const char *content_id;
+ const char *content_description;
+ const char *content_disposition;
+ const struct message_part_param *content_disposition_params;
+ unsigned int content_disposition_params_count;
+ const char *content_md5;
+ const char *const *content_language;
+ const char *content_location;
+
+ struct message_part_envelope *envelope;
+};
+
+struct message_part_attachment_settings {
+ /* By default, all attachments with content-disposition=attachment
+ or content-disposition=inline;filename=... are consired as an
+ attachment.
+
+ If content_type_filter is set to an array of masks, then
+ anything starting with ! is excluded, and anything without
+ is considered negating exclusion. Setting foo/bar alone will */
+// not do anything, but setting !foo/*, foo/bar, will exclude
+ /* all attachments with foo/anything content type, but will
+ accept foo/bar.
+
+ Setting exclude_inlined, will exclude **any** inlined attachment
+ regardless of what content_type_filter is.
+ */
+ const char *const *content_type_filter;
+ bool exclude_inlined;
+};
+
+extern const char *message_part_envelope_headers[];
+
+/*
+ *
+ */
+
+/* Returns TRUE if this message part has content-type "text/plain",
+ charset "us-ascii" and content-transfer-encoding "7bit" */
+bool message_part_data_is_plain_7bit(const struct message_part *part)
+ ATTR_PURE;
+
+/* Returns TRUE if this message part has a filename. The filename is
+ returned in filename_r. */
+bool message_part_data_get_filename(const struct message_part *part,
+ const char **filename_r);
+
+/* See message_part_attachment_settings */
+bool message_part_has_content_types(struct message_part *part, const char *const *types);
+
+/* Returns TRUE if message part has given parameter, and has non-empty
+ value if has_value is TRUE. */
+bool message_part_has_parameter(struct message_part *part, const char *parameter,
+ bool has_value);
+
+/* Check if part is attachment according to given settings */
+bool message_part_is_attachment(struct message_part *part,
+ const struct message_part_attachment_settings *set);
+/*
+ * Header parsing
+ */
+
+/* Update envelope data based from given header field */
+void message_part_envelope_parse_from_header(pool_t pool,
+ struct message_part_envelope **_data,
+ struct message_header_line *hdr);
+
+/* Parse a single header. Note that this modifies part->context. */
+void message_part_data_parse_from_header(pool_t pool,
+ struct message_part *part,
+ struct message_header_line *hdr);
+
+#endif
diff --git a/src/lib-mail/message-part-serialize.c b/src/lib-mail/message-part-serialize.c
new file mode 100644
index 0000000..23f3a01
--- /dev/null
+++ b/src/lib-mail/message-part-serialize.c
@@ -0,0 +1,269 @@
+/* Copyright (c) 2002-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "buffer.h"
+#include "message-parser.h"
+#include "message-part-serialize.h"
+
+/*
+ root part
+ root's first children
+ children's first children
+ ...
+ root's next children
+ ...
+
+ part
+ unsigned int flags
+ (not root part)
+ uoff_t physical_pos
+ uoff_t header_physical_size
+ uoff_t header_virtual_size
+ uoff_t body_physical_size
+ uoff_t body_virtual_size
+ (flags & (MESSAGE_PART_FLAG_TEXT | MESSAGE_PART_FLAG_MESSAGE_RFC822))
+ unsigned int body_lines
+ (flags & (MESSAGE_PART_FLAG_MULTIPART | MESSAGE_PART_FLAG_MESSAGE_RFC822))
+ unsigned int children_count
+
+*/
+
+#define MINIMUM_SERIALIZED_SIZE \
+ (sizeof(unsigned int) + sizeof(uoff_t) * 4)
+
+struct deserialize_context {
+ pool_t pool;
+ const unsigned char *data, *end;
+
+ uoff_t pos;
+ const char *error;
+};
+
+static void part_serialize(struct message_part *part, buffer_t *dest,
+ unsigned int *children_count_r)
+{
+ unsigned int count, children_count;
+ size_t children_offset;
+ bool root = part->parent == NULL;
+
+ count = 0;
+ while (part != NULL) {
+ /* create serialized part */
+ buffer_append(dest, &part->flags, sizeof(part->flags));
+ if (root)
+ root = FALSE;
+ else {
+ buffer_append(dest, &part->physical_pos,
+ sizeof(part->physical_pos));
+ }
+ buffer_append(dest, &part->header_size.physical_size,
+ sizeof(part->header_size.physical_size));
+ buffer_append(dest, &part->header_size.virtual_size,
+ sizeof(part->header_size.virtual_size));
+ buffer_append(dest, &part->body_size.physical_size,
+ sizeof(part->body_size.physical_size));
+ buffer_append(dest, &part->body_size.virtual_size,
+ sizeof(part->body_size.virtual_size));
+
+ if ((part->flags & (MESSAGE_PART_FLAG_TEXT |
+ MESSAGE_PART_FLAG_MESSAGE_RFC822)) != 0) {
+ buffer_append(dest, &part->body_size.lines,
+ sizeof(part->body_size.lines));
+ }
+
+ if ((part->flags & (MESSAGE_PART_FLAG_MULTIPART |
+ MESSAGE_PART_FLAG_MESSAGE_RFC822)) != 0) {
+ children_offset = dest->used;
+ children_count = 0;
+ buffer_append(dest, &children_count,
+ sizeof(children_count));
+
+ if (part->children != NULL) {
+ part_serialize(part->children, dest,
+ &children_count);
+
+ buffer_write(dest, children_offset,
+ &children_count,
+ sizeof(children_count));
+ }
+ } else {
+ i_assert(part->children == NULL);
+ }
+
+ count++;
+ part = part->next;
+ }
+
+ *children_count_r = count;
+}
+
+void message_part_serialize(struct message_part *part, buffer_t *dest)
+{
+ unsigned int children_count;
+
+ part_serialize(part, dest, &children_count);
+}
+
+static bool read_next(struct deserialize_context *ctx,
+ void *buffer, size_t buffer_size)
+{
+ if (ctx->data + buffer_size > ctx->end) {
+ ctx->error = "Not enough data";
+ return FALSE;
+ }
+
+ memcpy(buffer, ctx->data, buffer_size);
+ ctx->data += buffer_size;
+ return TRUE;
+}
+
+static bool ATTR_NULL(2)
+message_part_deserialize_part(struct deserialize_context *ctx,
+ struct message_part *parent,
+ unsigned int siblings,
+ struct message_part **part_r)
+{
+ struct message_part *p, *part, *first_part, **next_part;
+ unsigned int children_count;
+ uoff_t pos;
+ bool root = parent == NULL;
+
+ first_part = NULL;
+ next_part = NULL;
+ while (siblings > 0) {
+ siblings--;
+
+ part = p_new(ctx->pool, struct message_part, 1);
+ part->parent = parent;
+ for (p = parent; p != NULL; p = p->parent)
+ p->children_count++;
+
+ if (!read_next(ctx, &part->flags, sizeof(part->flags)))
+ return FALSE;
+
+ if (root)
+ root = FALSE;
+ else {
+ if (!read_next(ctx, &part->physical_pos,
+ sizeof(part->physical_pos)))
+ return FALSE;
+ }
+
+ if (part->physical_pos < ctx->pos) {
+ ctx->error = "physical_pos less than expected";
+ return FALSE;
+ }
+
+ if (!read_next(ctx, &part->header_size.physical_size,
+ sizeof(part->header_size.physical_size)))
+ return FALSE;
+
+ if (!read_next(ctx, &part->header_size.virtual_size,
+ sizeof(part->header_size.virtual_size)))
+ return FALSE;
+
+ if (part->header_size.virtual_size <
+ part->header_size.physical_size) {
+ ctx->error = "header_size.virtual_size too small";
+ return FALSE;
+ }
+
+ if (!read_next(ctx, &part->body_size.physical_size,
+ sizeof(part->body_size.physical_size)))
+ return FALSE;
+
+ if (!read_next(ctx, &part->body_size.virtual_size,
+ sizeof(part->body_size.virtual_size)))
+ return FALSE;
+
+ if ((part->flags & (MESSAGE_PART_FLAG_TEXT |
+ MESSAGE_PART_FLAG_MESSAGE_RFC822)) != 0) {
+ if (!read_next(ctx, &part->body_size.lines,
+ sizeof(part->body_size.lines)))
+ return FALSE;
+ }
+
+ if (part->body_size.virtual_size <
+ part->body_size.physical_size) {
+ ctx->error = "body_size.virtual_size too small";
+ return FALSE;
+ }
+
+ if ((part->flags & (MESSAGE_PART_FLAG_MULTIPART |
+ MESSAGE_PART_FLAG_MESSAGE_RFC822)) != 0) {
+ if (!read_next(ctx, &children_count,
+ sizeof(children_count)))
+ return FALSE;
+ } else {
+ children_count = 0;
+ }
+
+ if ((part->flags & MESSAGE_PART_FLAG_MESSAGE_RFC822) != 0) {
+ /* Only one child is possible */
+ if (children_count == 0) {
+ ctx->error =
+ "message/rfc822 part has no children";
+ return FALSE;
+ }
+ if (children_count != 1) {
+ ctx->error = "message/rfc822 part "
+ "has multiple children";
+ return FALSE;
+ }
+ }
+
+ if (children_count > 0) {
+ /* our children must be after our physical_pos+header
+ and the last child must be within our size. */
+ ctx->pos = part->physical_pos +
+ part->header_size.physical_size;
+ pos = ctx->pos + part->body_size.physical_size;
+
+ if (!message_part_deserialize_part(ctx, part,
+ children_count,
+ &part->children))
+ return FALSE;
+
+ if (ctx->pos > pos) {
+ ctx->error =
+ "child part location exceeds our size";
+ return FALSE;
+ }
+ ctx->pos = pos; /* save it for above check for parent */
+ }
+
+ if (first_part == NULL)
+ first_part = part;
+ if (next_part != NULL)
+ *next_part = part;
+ next_part = &part->next;
+ }
+
+ *part_r = first_part;
+ return TRUE;
+}
+
+struct message_part *
+message_part_deserialize(pool_t pool, const void *data, size_t size,
+ const char **error_r)
+{
+ struct deserialize_context ctx;
+ struct message_part *part;
+
+ i_zero(&ctx);
+ ctx.pool = pool;
+ ctx.data = data;
+ ctx.end = ctx.data + size;
+
+ if (!message_part_deserialize_part(&ctx, NULL, 1, &part)) {
+ *error_r = ctx.error;
+ return NULL;
+ }
+
+ if (ctx.data != ctx.end) {
+ *error_r = "Too much data";
+ return NULL;
+ }
+
+ return part;
+}
diff --git a/src/lib-mail/message-part-serialize.h b/src/lib-mail/message-part-serialize.h
new file mode 100644
index 0000000..197c3b6
--- /dev/null
+++ b/src/lib-mail/message-part-serialize.h
@@ -0,0 +1,16 @@
+#ifndef MESSAGE_PART_SERIALIZE_H
+#define MESSAGE_PART_SERIALIZE_H
+
+struct message_part;
+struct message_size;
+
+/* Serialize message part. */
+void message_part_serialize(struct message_part *part, buffer_t *dest);
+
+/* Generate struct message_part from serialized data. Returns NULL and sets
+ error if any problems are detected. */
+struct message_part *
+message_part_deserialize(pool_t pool, const void *data, size_t size,
+ const char **error_r);
+
+#endif
diff --git a/src/lib-mail/message-part.c b/src/lib-mail/message-part.c
new file mode 100644
index 0000000..340dbbb
--- /dev/null
+++ b/src/lib-mail/message-part.c
@@ -0,0 +1,103 @@
+/* Copyright (c) 2014-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "message-part.h"
+
+static const struct message_part *
+message_part_root(const struct message_part *part)
+{
+ while (part->parent != NULL)
+ part = part->parent;
+ return part;
+}
+
+static bool message_part_find(const struct message_part *siblings,
+ const struct message_part *part,
+ unsigned int *n)
+{
+ const struct message_part *p;
+
+ for (p = siblings; p != NULL; p = p->next) {
+ if (p == part)
+ return TRUE;
+ *n += 1;
+ if (message_part_find(p->children, part, n))
+ return TRUE;
+ }
+ return FALSE;
+}
+
+unsigned int message_part_to_idx(const struct message_part *part)
+{
+ const struct message_part *root;
+ unsigned int n = 0;
+
+ root = message_part_root(part);
+ if (!message_part_find(root, part, &n))
+ i_unreached();
+ return n;
+}
+
+static struct message_part *
+message_sub_part_by_idx(struct message_part *parts,
+ unsigned int idx)
+{
+ struct message_part *part = parts;
+
+ for (; part != NULL && idx > 0; part = part->next) {
+ if (part->children_count >= idx)
+ return message_sub_part_by_idx(part->children, idx-1);
+ idx -= part->children_count + 1;
+ }
+ return part;
+}
+
+struct message_part *
+message_part_by_idx(struct message_part *parts, unsigned int idx)
+{
+ i_assert(parts->parent == NULL);
+
+ return message_sub_part_by_idx(parts, idx);
+}
+
+bool message_part_is_equal(const struct message_part *p1,
+ const struct message_part *p2)
+{
+ /* This cannot be p1 && p2, because then we would return
+ TRUE when either part is NULL, and we should return FALSE */
+ while (p1 != NULL || p2 != NULL) {
+ /* If either part is NULL, return false */
+ if ((p1 != NULL) != (p2 != NULL))
+ return FALSE;
+
+ /* Expect that both either have children, or both
+ do not have children */
+ if ((p1->children != NULL) != (p2->children != NULL))
+ return FALSE;
+
+ /* If there are children, ensure they are equal */
+ if (p1->children != NULL) {
+ if (!message_part_is_equal(p1->children, p2->children))
+ return FALSE;
+ }
+
+ /* If any of these properties differ, then parts are not equal */
+ if (p1->physical_pos != p2->physical_pos ||
+ p1->header_size.physical_size != p2->header_size.physical_size ||
+ p1->header_size.virtual_size != p2->header_size.virtual_size ||
+ p1->header_size.lines != p2->header_size.lines ||
+ p1->body_size.physical_size != p2->body_size.physical_size ||
+ p1->body_size.virtual_size != p2->body_size.virtual_size ||
+ p1->body_size.lines != p2->body_size.lines ||
+ p1->children_count != p2->children_count ||
+ p1->flags != p2->flags)
+ return FALSE;
+
+ /* Move forward */
+ p1 = p1->next;
+ p2 = p2->next;
+ }
+
+ /* Parts are equal */
+ return TRUE;
+}
diff --git a/src/lib-mail/message-part.h b/src/lib-mail/message-part.h
new file mode 100644
index 0000000..d8f99ab
--- /dev/null
+++ b/src/lib-mail/message-part.h
@@ -0,0 +1,67 @@
+#ifndef MESSAGE_PART_H
+#define MESSAGE_PART_H
+
+#include "message-size.h"
+
+struct message_part_data;
+
+/* Note that these flags are used directly by message-parser-serialize, so
+ existing flags can't be changed without breaking backwards compatibility */
+enum message_part_flags {
+ MESSAGE_PART_FLAG_MULTIPART = 0x01,
+ MESSAGE_PART_FLAG_MULTIPART_DIGEST = 0x02,
+ MESSAGE_PART_FLAG_MESSAGE_RFC822 = 0x04,
+
+ /* content-type: text/... */
+ MESSAGE_PART_FLAG_TEXT = 0x08,
+
+ MESSAGE_PART_FLAG_UNUSED = 0x10,
+
+ /* message part header or body contains NULs */
+ MESSAGE_PART_FLAG_HAS_NULS = 0x20,
+
+ /* Mime-Version header exists. */
+ MESSAGE_PART_FLAG_IS_MIME = 0x40,
+ /* Message parsing was aborted because there were too many MIME parts.
+ This MIME part points to a blob which wasn't actually parsed to
+ see if it would contain further MIME parts. */
+ MESSAGE_PART_FLAG_OVERFLOW = 0x80,
+};
+
+struct message_part {
+ struct message_part *parent;
+ struct message_part *next;
+ struct message_part *children;
+
+ uoff_t physical_pos; /* absolute position from beginning of message */
+ struct message_size header_size;
+ struct message_size body_size;
+
+ struct message_part_data *data;
+
+ /* total number of message_parts under children */
+ unsigned int children_count;
+ enum message_part_flags flags;
+ void *context;
+};
+
+/* Return index number for the message part. The indexes are in the same order
+ as they exist in the flat RFC822 message. The root part is 0, its first
+ child is 1 and so on. */
+unsigned int message_part_to_idx(const struct message_part *part);
+/* Find message part by its index number, or return NULL if the index
+ doesn't exist. */
+struct message_part *
+message_part_by_idx(struct message_part *parts, unsigned int idx);
+
+/* Returns TRUE when message parts are considered equal. Equality is determined
+ to be TRUE, when
+
+ - both parts are NULL
+ - both parts are not NULL, and
+ - both parts children are equal
+ - both parts have same position, sizes, line counts and flags. */
+bool message_part_is_equal(const struct message_part *p1,
+ const struct message_part *p2) ATTR_NULL(1, 2);
+
+#endif
diff --git a/src/lib-mail/message-search.c b/src/lib-mail/message-search.c
new file mode 100644
index 0000000..5f54485
--- /dev/null
+++ b/src/lib-mail/message-search.c
@@ -0,0 +1,246 @@
+/* Copyright (c) 2002-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "buffer.h"
+#include "istream.h"
+#include "str.h"
+#include "str-find.h"
+#include "rfc822-parser.h"
+#include "message-decoder.h"
+#include "message-parser.h"
+#include "message-search.h"
+
+struct message_search_context {
+ enum message_search_flags flags;
+ normalizer_func_t *normalizer;
+
+ struct str_find_context *str_find_ctx;
+ struct message_part *prev_part;
+
+ struct message_decoder_context *decoder;
+ bool content_type_text:1; /* text/any or message/any */
+};
+
+struct message_search_context *
+message_search_init(const char *normalized_key_utf8,
+ normalizer_func_t *normalizer,
+ enum message_search_flags flags)
+{
+ struct message_search_context *ctx;
+
+ i_assert(*normalized_key_utf8 != '\0');
+
+ ctx = i_new(struct message_search_context, 1);
+ ctx->flags = flags;
+ ctx->decoder = message_decoder_init(normalizer, 0);
+ ctx->str_find_ctx = str_find_init(default_pool, normalized_key_utf8);
+ return ctx;
+}
+
+void message_search_deinit(struct message_search_context **_ctx)
+{
+ struct message_search_context *ctx = *_ctx;
+
+ *_ctx = NULL;
+ str_find_deinit(&ctx->str_find_ctx);
+ message_decoder_deinit(&ctx->decoder);
+ i_free(ctx);
+}
+
+static void parse_content_type(struct message_search_context *ctx,
+ struct message_header_line *hdr)
+{
+ struct rfc822_parser_context parser;
+ string_t *content_type;
+
+ rfc822_parser_init(&parser, hdr->full_value, hdr->full_value_len, NULL);
+ rfc822_skip_lwsp(&parser);
+
+ content_type = t_str_new(64);
+ (void)rfc822_parse_content_type(&parser, content_type);
+ ctx->content_type_text =
+ strncasecmp(str_c(content_type), "text/", 5) == 0 ||
+ strncasecmp(str_c(content_type), "message/", 8) == 0;
+ rfc822_parser_deinit(&parser);
+}
+
+static void handle_header(struct message_search_context *ctx,
+ struct message_header_line *hdr)
+{
+ if (hdr->name_len == 12 &&
+ strcasecmp(hdr->name, "Content-Type") == 0) {
+ if (hdr->continues) {
+ hdr->use_full_value = TRUE;
+ return;
+ }
+ T_BEGIN {
+ parse_content_type(ctx, hdr);
+ } T_END;
+ }
+}
+
+static bool search_header(struct message_search_context *ctx,
+ const struct message_header_line *hdr)
+{
+ static const unsigned char crlf[2] = { '\r', '\n' };
+
+ return str_find_more(ctx->str_find_ctx,
+ (const unsigned char *)hdr->name, hdr->name_len) ||
+ str_find_more(ctx->str_find_ctx,
+ hdr->middle, hdr->middle_len) ||
+ str_find_more(ctx->str_find_ctx, hdr->full_value,
+ hdr->full_value_len) ||
+ (!hdr->no_newline &&
+ str_find_more(ctx->str_find_ctx, crlf, 2));
+}
+
+static bool message_search_more_decoded2(struct message_search_context *ctx,
+ struct message_block *block)
+{
+ if (block->hdr != NULL) {
+ if (search_header(ctx, block->hdr))
+ return TRUE;
+ } else {
+ if (str_find_more(ctx->str_find_ctx, block->data, block->size))
+ return TRUE;
+ }
+ return FALSE;
+}
+
+bool message_search_more(struct message_search_context *ctx,
+ struct message_block *raw_block)
+{
+ struct message_block decoded_block;
+
+ return message_search_more_get_decoded(ctx, raw_block, &decoded_block);
+}
+
+bool message_search_more_get_decoded(struct message_search_context *ctx,
+ struct message_block *raw_block,
+ struct message_block *decoded_block_r)
+{
+ struct message_header_line *hdr = raw_block->hdr;
+ struct message_block decoded_block;
+
+ i_zero(decoded_block_r);
+ decoded_block_r->part = raw_block->part;
+
+ if (raw_block->part != ctx->prev_part) {
+ /* part changes. we must change this before looking at
+ content type */
+ message_search_reset(ctx);
+ ctx->prev_part = raw_block->part;
+
+ if (hdr == NULL) {
+ /* we're returning to a multipart message. */
+ ctx->content_type_text = FALSE;
+ }
+ }
+
+ if (hdr != NULL) {
+ handle_header(ctx, hdr);
+ if ((ctx->flags & MESSAGE_SEARCH_FLAG_SKIP_HEADERS) != 0) {
+ /* we want to search only message bodies, but
+ but decoder needs some headers so that it can
+ decode the body properly. */
+ if (hdr->name_len != 12 && hdr->name_len != 25)
+ return FALSE;
+ if (strcasecmp(hdr->name, "Content-Type") != 0 &&
+ strcasecmp(hdr->name,
+ "Content-Transfer-Encoding") != 0)
+ return FALSE;
+ }
+ } else {
+ /* body */
+ if (!ctx->content_type_text)
+ return FALSE;
+ }
+ if (!message_decoder_decode_next_block(ctx->decoder, raw_block,
+ &decoded_block))
+ return FALSE;
+
+ if (decoded_block.hdr != NULL &&
+ (ctx->flags & MESSAGE_SEARCH_FLAG_SKIP_HEADERS) != 0) {
+ /* Content-* header */
+ return FALSE;
+ }
+
+ *decoded_block_r = decoded_block;
+ return message_search_more_decoded2(ctx, &decoded_block);
+}
+
+bool message_search_more_decoded(struct message_search_context *ctx,
+ struct message_block *block)
+{
+ if (block->part != ctx->prev_part) {
+ /* part changes */
+ message_search_reset(ctx);
+ ctx->prev_part = block->part;
+ }
+
+ return message_search_more_decoded2(ctx, block);
+}
+
+void message_search_reset(struct message_search_context *ctx)
+{
+ /* Content-Type defaults to text/plain */
+ ctx->content_type_text = TRUE;
+
+ ctx->prev_part = NULL;
+ str_find_reset(ctx->str_find_ctx);
+ message_decoder_decode_reset(ctx->decoder);
+}
+
+static int
+message_search_msg_real(struct message_search_context *ctx,
+ struct istream *input, struct message_part *parts,
+ const char **error_r)
+{
+ const struct message_parser_settings parser_set = {
+ .hdr_flags = MESSAGE_HEADER_PARSER_FLAG_CLEAN_ONELINE,
+ };
+ struct message_parser_ctx *parser_ctx;
+ struct message_block raw_block;
+ struct message_part *new_parts;
+ int ret;
+
+ message_search_reset(ctx);
+
+ if (parts != NULL) {
+ parser_ctx = message_parser_init_from_parts(parts,
+ input, &parser_set);
+ } else {
+ parser_ctx = message_parser_init(pool_datastack_create(),
+ input, &parser_set);
+ }
+
+ while ((ret = message_parser_parse_next_block(parser_ctx,
+ &raw_block)) > 0) {
+ if (message_search_more(ctx, &raw_block)) {
+ ret = 1;
+ break;
+ }
+ }
+ i_assert(ret != 0);
+ if (ret < 0 && input->stream_errno == 0) {
+ /* normal exit */
+ ret = 0;
+ }
+ if (message_parser_deinit_from_parts(&parser_ctx, &new_parts, error_r) < 0) {
+ /* broken parts */
+ ret = -1;
+ }
+ return ret;
+}
+
+int message_search_msg(struct message_search_context *ctx,
+ struct istream *input, struct message_part *parts,
+ const char **error_r)
+{
+ int ret;
+
+ T_BEGIN {
+ ret = message_search_msg_real(ctx, input, parts, error_r);
+ } T_END_PASS_STR_IF(ret < 0, error_r);
+ return ret;
+}
diff --git a/src/lib-mail/message-search.h b/src/lib-mail/message-search.h
new file mode 100644
index 0000000..7d3786a
--- /dev/null
+++ b/src/lib-mail/message-search.h
@@ -0,0 +1,40 @@
+#ifndef MESSAGE_SEARCH_H
+#define MESSAGE_SEARCH_H
+
+struct message_block;
+struct message_part;
+struct message_search_context;
+
+enum message_search_flags {
+ /* Skip the main header and all the MIME headers. */
+ MESSAGE_SEARCH_FLAG_SKIP_HEADERS = 0x01
+};
+
+/* The key must be given in UTF-8 charset */
+struct message_search_context *
+message_search_init(const char *normalized_key_utf8,
+ normalizer_func_t *normalizer,
+ enum message_search_flags flags);
+void message_search_deinit(struct message_search_context **ctx);
+
+/* Returns TRUE if key is found from input buffer, FALSE if not. */
+bool message_search_more(struct message_search_context *ctx,
+ struct message_block *raw_block);
+/* Same as message_search_more(), but return the decoded block. If the same
+ input is being fed to multiple searches, this avoids duplicating the work
+ by doing the following searches with message_search_more_decoded() */
+bool message_search_more_get_decoded(struct message_search_context *ctx,
+ struct message_block *raw_block,
+ struct message_block *decoded_block_r);
+/* The data has already passed through decoder. */
+bool message_search_more_decoded(struct message_search_context *ctx,
+ struct message_block *block);
+void message_search_reset(struct message_search_context *ctx);
+/* Search a full message. Returns 1 if match was found, 0 if not,
+ -1 if error (if stream_error == 0, the parts contained broken data) */
+int message_search_msg(struct message_search_context *ctx,
+ struct istream *input, struct message_part *parts,
+ const char **error_r)
+ ATTR_NULL(3);
+
+#endif
diff --git a/src/lib-mail/message-size.c b/src/lib-mail/message-size.c
new file mode 100644
index 0000000..472abeb
--- /dev/null
+++ b/src/lib-mail/message-size.c
@@ -0,0 +1,174 @@
+/* Copyright (c) 2002-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "istream.h"
+#include "message-parser.h"
+#include "message-size.h"
+
+int message_get_header_size(struct istream *input, struct message_size *hdr,
+ bool *has_nuls_r)
+{
+ const unsigned char *msg;
+ size_t i, size, startpos, missing_cr_count;
+ int ret;
+
+ memset(hdr, 0, sizeof(struct message_size));
+ *has_nuls_r = FALSE;
+
+ missing_cr_count = 0; startpos = 0;
+ while ((ret = i_stream_read_bytes(input, &msg, &size, startpos + 1)) > 0) {
+ for (i = startpos; i < size; i++) {
+ if (msg[i] != '\n') {
+ if (msg[i] == '\0')
+ *has_nuls_r = TRUE;
+ continue;
+ }
+
+ hdr->lines++;
+ if (i == 0 || msg[i-1] != '\r') {
+ /* missing CR */
+ missing_cr_count++;
+ }
+
+ if (i == 0 || (i == 1 && msg[i-1] == '\r')) {
+ /* no headers at all */
+ break;
+ }
+
+ if ((i > 0 && msg[i-1] == '\n') ||
+ (i > 1 && msg[i-2] == '\n' && msg[i-1] == '\r')) {
+ /* \n\n or \n\r\n - end of headers */
+ break;
+ }
+ }
+
+ if (i < size) {
+ /* end of header */
+ startpos = i+1;
+ break;
+ }
+
+ /* leave the last two characters, they may be \r\n */
+ startpos = size == 1 ? 1 : 2;
+ i_stream_skip(input, i - startpos);
+
+ hdr->physical_size += i - startpos;
+ }
+ i_assert(ret == -1 || ret > 0);
+
+ ret = input->stream_errno != 0 ? -1 : 0;
+ i_stream_skip(input, startpos);
+ hdr->physical_size += startpos;
+
+ hdr->virtual_size = hdr->physical_size + missing_cr_count;
+ i_assert(hdr->virtual_size >= hdr->physical_size);
+ return ret;
+}
+
+int message_get_body_size(struct istream *input, struct message_size *body,
+ bool *has_nuls_r)
+{
+ const unsigned char *msg;
+ size_t i, size, missing_cr_count;
+ int ret;
+
+ memset(body, 0, sizeof(struct message_size));
+ *has_nuls_r = FALSE;
+
+ missing_cr_count = 0;
+ if ((ret = i_stream_read_more(input, &msg, &size)) <= 0) {
+ i_assert(ret == -1);
+ return ret < 0 && input->stream_errno != 0 ? -1 : 0;
+ }
+
+ if (msg[0] == '\n')
+ missing_cr_count++;
+
+ do {
+ for (i = 1; i < size; i++) {
+ if (msg[i] > '\n')
+ continue;
+
+ if (msg[i] == '\n') {
+ if (msg[i-1] != '\r') {
+ /* missing CR */
+ missing_cr_count++;
+ }
+
+ /* increase after making sure we didn't break
+ at virtual \r */
+ body->lines++;
+ } else if (msg[i] == '\0') {
+ *has_nuls_r = TRUE;
+ }
+ }
+
+ /* leave the last character, it may be \r */
+ i_stream_skip(input, i - 1);
+ body->physical_size += i - 1;
+ } while ((ret = i_stream_read_bytes(input, &msg, &size, 2)) > 0);
+ i_assert(ret == -1);
+
+ ret = input->stream_errno != 0 ? -1 : 0;
+
+ i_stream_skip(input, 1);
+ body->physical_size++;
+
+ body->virtual_size = body->physical_size + missing_cr_count;
+ i_assert(body->virtual_size >= body->physical_size);
+ return ret;
+}
+
+void message_size_add(struct message_size *dest,
+ const struct message_size *src)
+{
+ dest->virtual_size += src->virtual_size;
+ dest->physical_size += src->physical_size;
+ dest->lines += src->lines;
+}
+
+int message_skip_virtual(struct istream *input, uoff_t virtual_skip,
+ bool *last_cr_r)
+{
+ const unsigned char *msg;
+ size_t i, size;
+ bool cr_skipped = FALSE;
+ int ret;
+
+ *last_cr_r = FALSE;
+ if (virtual_skip == 0)
+ return 0;
+
+ while ((ret = i_stream_read_more(input, &msg, &size)) > 0) {
+ for (i = 0; i < size && virtual_skip > 0; i++) {
+ virtual_skip--;
+
+ if (msg[i] == '\r') {
+ /* CR */
+ if (virtual_skip == 0)
+ *last_cr_r = TRUE;
+ } else if (msg[i] == '\n') {
+ /* LF */
+ if ((i == 0 && !cr_skipped) ||
+ (i > 0 && msg[i-1] != '\r')) {
+ if (virtual_skip == 0) {
+ /* CR/LF boundary */
+ *last_cr_r = TRUE;
+ break;
+ }
+
+ virtual_skip--;
+ }
+ }
+ }
+ i_stream_skip(input, i);
+
+ if (i < size)
+ return 0;
+
+ i_assert(i > 0);
+ cr_skipped = msg[i-1] == '\r';
+ }
+ i_assert(ret == -1);
+ return input->stream_errno == 0 ? 0 : -1;
+}
diff --git a/src/lib-mail/message-size.h b/src/lib-mail/message-size.h
new file mode 100644
index 0000000..5fe6b8a
--- /dev/null
+++ b/src/lib-mail/message-size.h
@@ -0,0 +1,28 @@
+#ifndef MESSAGE_SIZE_H
+#define MESSAGE_SIZE_H
+
+struct message_size {
+ uoff_t physical_size;
+ uoff_t virtual_size;
+ unsigned int lines;
+};
+
+/* Calculate size of message header. Leave the input point to first
+ character in body. */
+int message_get_header_size(struct istream *input, struct message_size *hdr,
+ bool *has_nuls_r);
+/* Calculate size of message body. */
+int message_get_body_size(struct istream *input, struct message_size *body,
+ bool *has_nuls_r);
+
+/* Sum contents of src into dest. */
+void message_size_add(struct message_size *dest,
+ const struct message_size *src);
+
+/* Skip number of virtual bytes from buffer. last_cr_r is set to TRUE if the
+ last character we skipped was '\r', meaning that the next character should
+ be '\n', which shouldn't be treated as "\r\n". */
+int message_skip_virtual(struct istream *input, uoff_t virtual_skip,
+ bool *last_cr_r);
+
+#endif
diff --git a/src/lib-mail/message-snippet.c b/src/lib-mail/message-snippet.c
new file mode 100644
index 0000000..2982e2e
--- /dev/null
+++ b/src/lib-mail/message-snippet.c
@@ -0,0 +1,207 @@
+/* Copyright (c) 2015-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "buffer.h"
+#include "str.h"
+#include "istream.h"
+#include "mail-html2text.h"
+#include "message-parser.h"
+#include "message-decoder.h"
+#include "message-snippet.h"
+
+#include <ctype.h>
+
+enum snippet_state {
+ /* beginning of the line */
+ SNIPPET_STATE_NEWLINE = 0,
+ /* within normal text */
+ SNIPPET_STATE_NORMAL,
+ /* within quoted text - skip until EOL */
+ SNIPPET_STATE_QUOTED
+};
+
+struct snippet_data {
+ string_t *snippet;
+ unsigned int chars_left;
+};
+
+struct snippet_context {
+ struct snippet_data snippet;
+ struct snippet_data quoted_snippet;
+ enum snippet_state state;
+ bool add_whitespace;
+ struct mail_html2text *html2text;
+ buffer_t *plain_output;
+};
+
+static void snippet_add_content(struct snippet_context *ctx,
+ struct snippet_data *target,
+ const unsigned char *data, size_t size,
+ size_t *count_r)
+{
+ i_assert(target != NULL);
+ if (size == 0)
+ return;
+ if (size >= 3 &&
+ ((data[0] == 0xEF && data[1] == 0xBB && data[2] == 0xBF) ||
+ (data[0] == 0xBF && data[1] == 0xBB && data[2] == 0xEF))) {
+ *count_r = 3;
+ return;
+ }
+ if (data[0] == '\0') {
+ /* skip NULs without increasing snippet size */
+ return;
+ }
+ if (i_isspace(*data)) {
+ /* skip any leading whitespace */
+ if (str_len(target->snippet) > 0)
+ ctx->add_whitespace = TRUE;
+ if (data[0] == '\n')
+ ctx->state = SNIPPET_STATE_NEWLINE;
+ return;
+ }
+ if (target->chars_left == 0)
+ return;
+ target->chars_left--;
+ if (ctx->add_whitespace) {
+ if (target->chars_left == 0) {
+ /* don't add a trailing whitespace */
+ return;
+ }
+ str_append_c(target->snippet, ' ');
+ ctx->add_whitespace = FALSE;
+ target->chars_left--;
+ }
+ *count_r = uni_utf8_char_bytes(data[0]);
+ i_assert(*count_r <= size);
+ str_append_data(target->snippet, data, *count_r);
+}
+
+static bool snippet_generate(struct snippet_context *ctx,
+ const unsigned char *data, size_t size)
+{
+ size_t i, count;
+ struct snippet_data *target;
+
+ if (ctx->html2text != NULL) {
+ buffer_set_used_size(ctx->plain_output, 0);
+ mail_html2text_more(ctx->html2text, data, size,
+ ctx->plain_output);
+ data = ctx->plain_output->data;
+ size = ctx->plain_output->used;
+ }
+
+ if (ctx->state == SNIPPET_STATE_QUOTED)
+ target = &ctx->quoted_snippet;
+ else
+ target = &ctx->snippet;
+
+ /* message-decoder should feed us only valid and complete
+ UTF-8 input */
+
+ for (i = 0; i < size; i += count) {
+ count = 1;
+ switch (ctx->state) {
+ case SNIPPET_STATE_NEWLINE:
+ if (data[i] == '>') {
+ ctx->state = SNIPPET_STATE_QUOTED;
+ i++;
+ target = &ctx->quoted_snippet;
+ } else {
+ ctx->state = SNIPPET_STATE_NORMAL;
+ target = &ctx->snippet;
+ }
+ /* fallthrough */
+ case SNIPPET_STATE_NORMAL:
+ case SNIPPET_STATE_QUOTED:
+ snippet_add_content(ctx, target, CONST_PTR_OFFSET(data, i),
+ size-i, &count);
+ /* break here if we have enough non-quoted data,
+ quoted data does not need to break here as it's
+ only used if the actual snippet is left empty. */
+ if (ctx->snippet.chars_left == 0)
+ return FALSE;
+ break;
+ }
+ }
+ return TRUE;
+}
+
+static void snippet_copy(const char *src, string_t *dst)
+{
+ while (*src != '\0' && i_isspace(*src)) src++;
+ str_append(dst, src);
+}
+
+int message_snippet_generate(struct istream *input,
+ unsigned int max_snippet_chars,
+ string_t *snippet)
+{
+ const struct message_parser_settings parser_set = { .flags = 0 };
+ struct message_parser_ctx *parser;
+ struct message_part *parts;
+ struct message_part *skip_part = NULL;
+ struct message_decoder_context *decoder;
+ struct message_block raw_block, block;
+ struct snippet_context ctx;
+ pool_t pool;
+ int ret;
+
+ i_zero(&ctx);
+ pool = pool_alloconly_create("message snippet", 2048);
+ ctx.snippet.snippet = str_new(pool, max_snippet_chars);
+ ctx.snippet.chars_left = max_snippet_chars;
+ ctx.quoted_snippet.snippet = str_new(pool, max_snippet_chars);
+ ctx.quoted_snippet.chars_left = max_snippet_chars - 1; /* -1 for '>' */
+ parser = message_parser_init(pool_datastack_create(), input, &parser_set);
+ decoder = message_decoder_init(NULL, 0);
+ while ((ret = message_parser_parse_next_block(parser, &raw_block)) > 0) {
+ if (raw_block.part == skip_part)
+ continue;
+ if (!message_decoder_decode_next_block(decoder, &raw_block, &block))
+ continue;
+ if (block.size == 0) {
+ const char *ct;
+
+ if (block.hdr != NULL)
+ continue;
+
+ /* We already have a snippet, don't look for more in
+ subsequent parts. */
+ if (ctx.snippet.snippet->used != 0 ||
+ ctx.quoted_snippet.snippet->used != 0)
+ break;
+
+ skip_part = NULL;
+
+ /* end of headers - verify that we can use this
+ Content-Type. we get here only once, because we
+ always handle only one non-multipart MIME part. */
+ ct = message_decoder_current_content_type(decoder);
+ if (ct == NULL)
+ /* text/plain */ ;
+ else if (mail_html2text_content_type_match(ct)) {
+ mail_html2text_deinit(&ctx.html2text);
+ ctx.html2text = mail_html2text_init(0);
+ if (ctx.plain_output == NULL) {
+ ctx.plain_output =
+ buffer_create_dynamic(pool, 1024);
+ }
+ } else if (strncasecmp(ct, "text/", 5) != 0)
+ skip_part = raw_block.part;
+ } else if (!snippet_generate(&ctx, block.data, block.size))
+ break;
+ }
+ i_assert(ret != 0);
+ message_decoder_deinit(&decoder);
+ message_parser_deinit(&parser, &parts);
+ mail_html2text_deinit(&ctx.html2text);
+ if (ctx.snippet.snippet->used != 0)
+ snippet_copy(str_c(ctx.snippet.snippet), snippet);
+ else if (ctx.quoted_snippet.snippet->used != 0) {
+ str_append_c(snippet, '>');
+ snippet_copy(str_c(ctx.quoted_snippet.snippet), snippet);
+ }
+ pool_unref(&pool);
+ return input->stream_errno == 0 ? 0 : -1;
+}
diff --git a/src/lib-mail/message-snippet.h b/src/lib-mail/message-snippet.h
new file mode 100644
index 0000000..fe9c3b6
--- /dev/null
+++ b/src/lib-mail/message-snippet.h
@@ -0,0 +1,14 @@
+#ifndef MESSAGE_SNIPPET_H
+#define MESSAGE_SNIPPET_H
+
+/* Generate UTF-8 text snippet from the beginning of the given mail input
+ stream. The stream is expected to start at the MIME part's headers whose
+ snippet is being generated. Returns 0 if ok, -1 if I/O error.
+
+ Currently only Content-Type: text/ is supported, others will result in an
+ empty string. */
+int message_snippet_generate(struct istream *input,
+ unsigned int max_snippet_chars,
+ string_t *snippet);
+
+#endif
diff --git a/src/lib-mail/ostream-dot.c b/src/lib-mail/ostream-dot.c
new file mode 100644
index 0000000..f5bfa72
--- /dev/null
+++ b/src/lib-mail/ostream-dot.c
@@ -0,0 +1,235 @@
+/* Copyright (c) 2013-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "ostream-private.h"
+#include "ostream-dot.h"
+
+enum dot_ostream_state {
+ STREAM_STATE_INIT = 0,
+ STREAM_STATE_NONE,
+ STREAM_STATE_CR,
+ STREAM_STATE_CRLF,
+ STREAM_STATE_DONE
+};
+
+struct dot_ostream {
+ struct ostream_private ostream;
+
+ enum dot_ostream_state state;
+ bool force_extra_crlf;
+};
+
+static int o_stream_dot_finish(struct ostream_private *stream)
+{
+ struct dot_ostream *dstream = (struct dot_ostream *)stream;
+ int ret;
+
+ if (dstream->state == STREAM_STATE_DONE)
+ return 1;
+
+ if (o_stream_get_buffer_avail_size(stream->parent) < 5) {
+ /* make space for the dot line */
+ if ((ret = o_stream_flush(stream->parent)) <= 0) {
+ if (ret < 0)
+ o_stream_copy_error_from_parent(stream);
+ return ret;
+ }
+ }
+
+ if (dstream->state == STREAM_STATE_CRLF &&
+ !dstream->force_extra_crlf) {
+ ret = o_stream_send(stream->parent, ".\r\n", 3);
+ i_assert(ret == 3);
+ } else {
+ ret = o_stream_send(stream->parent, "\r\n.\r\n", 5);
+ i_assert(ret == 5);
+ }
+ dstream->state = STREAM_STATE_DONE;
+ return 1;
+}
+
+static int
+o_stream_dot_flush(struct ostream_private *stream)
+{
+ int ret;
+
+ if (stream->finished) {
+ if ((ret = o_stream_dot_finish(stream)) <= 0)
+ return ret;
+ }
+
+ return o_stream_flush_parent(stream);
+}
+
+static void
+o_stream_dot_close(struct iostream_private *stream, bool close_parent)
+{
+ struct dot_ostream *dstream = (struct dot_ostream *)stream;
+
+ if (close_parent)
+ o_stream_close(dstream->ostream.parent);
+}
+
+static ssize_t
+o_stream_dot_sendv(struct ostream_private *stream,
+ const struct const_iovec *iov, unsigned int iov_count)
+{
+ struct dot_ostream *dstream = (struct dot_ostream *)stream;
+ ARRAY(struct const_iovec) iov_arr;
+ const struct const_iovec *iov_new;
+ size_t max_bytes, sent, added;
+ unsigned int count, i;
+ ssize_t ret;
+
+ i_assert(dstream->state != STREAM_STATE_DONE);
+
+ if ((ret=o_stream_flush(stream->parent)) <= 0) {
+ /* error / we still couldn't flush existing data to
+ parent stream. */
+ if (ret < 0)
+ o_stream_copy_error_from_parent(stream);
+ return ret;
+ }
+
+ /* check for dots */
+ t_array_init(&iov_arr, iov_count + 32);
+ max_bytes = o_stream_get_buffer_avail_size(stream->parent);
+ i_assert(max_bytes > 0); /* FIXME: not supported currently */
+
+ sent = added = 0;
+ for (i = 0; i < iov_count && max_bytes > 0; i++) {
+ size_t size = iov[i].iov_len, chunk;
+ const char *data = iov[i].iov_base, *p, *pend;
+ struct const_iovec iovn;
+
+ p = data;
+ pend = CONST_PTR_OFFSET(data, size);
+ for (; p < pend && (size_t)(p-data)+2 < max_bytes; p++) {
+ char add = 0;
+
+ switch (dstream->state) {
+ /* none */
+ case STREAM_STATE_NONE:
+ switch (*p) {
+ case '\n':
+ dstream->state = STREAM_STATE_CRLF;
+ /* add missing CR */
+ add = '\r';
+ break;
+ case '\r':
+ dstream->state = STREAM_STATE_CR;
+ break;
+ }
+ break;
+ /* got CR */
+ case STREAM_STATE_CR:
+ switch (*p) {
+ case '\r':
+ break;
+ case '\n':
+ dstream->state = STREAM_STATE_CRLF;
+ break;
+ default:
+ dstream->state = STREAM_STATE_NONE;
+ break;
+ }
+ break;
+ /* got CRLF, or the first line */
+ case STREAM_STATE_INIT:
+ case STREAM_STATE_CRLF:
+ switch (*p) {
+ case '\r':
+ dstream->state = STREAM_STATE_CR;
+ break;
+ case '\n':
+ dstream->state = STREAM_STATE_CRLF;
+ /* add missing CR */
+ add = '\r';
+ break;
+ case '.':
+ /* add dot */
+ add = '.';
+ /* fall through */
+ default:
+ dstream->state = STREAM_STATE_NONE;
+ break;
+ }
+ break;
+ case STREAM_STATE_DONE:
+ i_unreached();
+ }
+
+ if (add != 0) {
+ chunk = (size_t)(p - data);
+ if (chunk > 0) {
+ /* forward chunk to new iovec */
+ iovn.iov_base = data;
+ iovn.iov_len = chunk;
+ array_push_back(&iov_arr, &iovn);
+ data = p;
+ i_assert(max_bytes >= chunk);
+ max_bytes -= chunk;
+ sent += chunk;
+ }
+ /* insert byte (substitute one with pair) */
+ data++;
+ iovn.iov_base = (add == '\r' ? "\r\n" : "..");
+ iovn.iov_len = 2;
+ array_push_back(&iov_arr, &iovn);
+ i_assert(max_bytes >= 2);
+ max_bytes -= 2;
+ added++;
+ sent++;
+ }
+ }
+
+ if (max_bytes == 0)
+ break;
+ chunk = ((size_t)(p-data) >= max_bytes ?
+ max_bytes : (size_t)(p - data));
+ if (chunk > 0) {
+ iovn.iov_base = data;
+ iovn.iov_len = chunk;
+ array_push_back(&iov_arr, &iovn);
+ i_assert(max_bytes >= chunk);
+ max_bytes -= chunk;
+ sent += chunk;
+ }
+ }
+
+ /* send */
+ iov_new = array_get(&iov_arr, &count);
+ if (count == 0) {
+ ret = 0;
+ } else if ((ret=o_stream_sendv(stream->parent, iov_new, count)) <= 0) {
+ i_assert(ret < 0);
+ o_stream_copy_error_from_parent(stream);
+ return -1;
+ }
+
+ /* all must be sent */
+ i_assert((size_t)ret == sent + added);
+
+ stream->ostream.offset += sent;
+ return sent;
+}
+
+struct ostream *
+o_stream_create_dot(struct ostream *output, bool force_extra_crlf)
+{
+ struct dot_ostream *dstream;
+
+ dstream = i_new(struct dot_ostream, 1);
+ dstream->ostream.sendv = o_stream_dot_sendv;
+ dstream->ostream.iostream.close = o_stream_dot_close;
+ dstream->ostream.flush = o_stream_dot_flush;
+ dstream->ostream.max_buffer_size = output->real_stream->max_buffer_size;
+ dstream->force_extra_crlf = force_extra_crlf;
+ (void)o_stream_create(&dstream->ostream, output, o_stream_get_fd(output));
+ /* ostream-dot is always used inside another ostream that shouldn't
+ get finished when the "." line is written. Disable it here so all
+ of the callers don't have to set this. */
+ o_stream_set_finish_also_parent(&dstream->ostream.ostream, FALSE);
+ return &dstream->ostream.ostream;
+}
diff --git a/src/lib-mail/ostream-dot.h b/src/lib-mail/ostream-dot.h
new file mode 100644
index 0000000..4e22089
--- /dev/null
+++ b/src/lib-mail/ostream-dot.h
@@ -0,0 +1,15 @@
+#ifndef OSTREAM_DOT_H
+#define OSTREAM_DOT_H
+
+/* Create output stream for writing SMTP DATA style message: Add additional "."
+ to lines that start with ".". Write a line that contains only "." upon
+ o_stream_flush(). (This is also called at close(), but it shouldn't be
+ relied on since it could fail due to output buffer being full.)
+
+ If output ends with CRLF, force_extra_crlf controls whether additional CRLF
+ is written before the "." line. This parameter should match
+ i_stream_create_dot()'s send_last_lf parameter (reversed). */
+struct ostream *o_stream_create_dot(struct ostream *output,
+ bool force_extra_crlf);
+
+#endif
diff --git a/src/lib-mail/qp-decoder.c b/src/lib-mail/qp-decoder.c
new file mode 100644
index 0000000..7684803
--- /dev/null
+++ b/src/lib-mail/qp-decoder.c
@@ -0,0 +1,285 @@
+/* Copyright (c) 2002-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "buffer.h"
+#include "hex-binary.h"
+#include "qp-decoder.h"
+
+/* quoted-printable lines can be max 76 characters. if we've seen more than
+ that much whitespace, it means there really shouldn't be anything else left
+ in the line except trailing whitespace. */
+#define QP_MAX_WHITESPACE_LEN 76
+
+#define QP_IS_TRAILING_WHITESPACE(c) \
+ ((c) == ' ' || (c) == '\t')
+
+enum qp_state {
+ STATE_TEXT = 0,
+ STATE_WHITESPACE,
+ STATE_EQUALS,
+ STATE_EQUALS_WHITESPACE,
+ STATE_HEX2,
+ STATE_CR,
+ STATE_SOFTCR
+};
+
+struct qp_decoder {
+ buffer_t *dest;
+ buffer_t *whitespace;
+ enum qp_state state;
+ char hexchar;
+};
+
+struct qp_decoder *qp_decoder_init(buffer_t *dest)
+{
+ struct qp_decoder *qp;
+
+ qp = i_new(struct qp_decoder, 1);
+ qp->dest = dest;
+ qp->whitespace = buffer_create_dynamic(default_pool, 80);
+ return qp;
+}
+
+void qp_decoder_deinit(struct qp_decoder **_qp)
+{
+ struct qp_decoder *qp = *_qp;
+
+ buffer_free(&qp->whitespace);
+ i_free(qp);
+}
+
+static size_t
+qp_decoder_more_text(struct qp_decoder *qp, const unsigned char *src,
+ size_t src_size)
+{
+ size_t i, start = 0, ret = src_size;
+
+ for (i = 0; i < src_size; i++) {
+ if (src[i] > '=') {
+ /* fast path */
+ continue;
+ }
+ switch (src[i]) {
+ case '=':
+ qp->state = STATE_EQUALS;
+ break;
+ case '\r':
+ qp->state = STATE_CR;
+ break;
+ case '\n':
+ /* LF without preceding CR */
+ buffer_append(qp->dest, src+start, i-start);
+ buffer_append(qp->dest, "\r\n", 2);
+ start = i+1;
+ continue;
+ case ' ':
+ case '\t':
+ i_assert(qp->whitespace->used == 0);
+ qp->state = STATE_WHITESPACE;
+ buffer_append_c(qp->whitespace, src[i]);
+ break;
+ default:
+ continue;
+ }
+ ret = i+1;
+ break;
+ }
+ buffer_append(qp->dest, src+start, i-start);
+ return ret;
+}
+
+static void qp_decoder_invalid(struct qp_decoder *qp, const char **error_r)
+{
+ switch (qp->state) {
+ case STATE_EQUALS:
+ buffer_append_c(qp->dest, '=');
+ *error_r = "'=' not followed by two hex digits";
+ break;
+ case STATE_HEX2:
+ buffer_append_c(qp->dest, '=');
+ buffer_append_c(qp->dest, qp->hexchar);
+ *error_r = "'=<hex>' not followed by a hex digit";
+ break;
+ case STATE_EQUALS_WHITESPACE:
+ buffer_append_c(qp->dest, '=');
+ buffer_append_buf(qp->dest, qp->whitespace, 0, SIZE_MAX);
+ buffer_set_used_size(qp->whitespace, 0);
+ *error_r = "'=<whitespace>' not followed by newline";
+ break;
+ case STATE_CR:
+ buffer_append_buf(qp->dest, qp->whitespace, 0, SIZE_MAX);
+ buffer_set_used_size(qp->whitespace, 0);
+ buffer_append_c(qp->dest, '\r');
+ *error_r = "CR not followed by LF";
+ break;
+ case STATE_SOFTCR:
+ buffer_append_c(qp->dest, '=');
+ buffer_append_buf(qp->dest, qp->whitespace, 0, SIZE_MAX);
+ buffer_set_used_size(qp->whitespace, 0);
+ buffer_append_c(qp->dest, '\r');
+ *error_r = "CR not followed by LF";
+ break;
+ case STATE_TEXT:
+ case STATE_WHITESPACE:
+ i_unreached();
+ }
+ qp->state = STATE_TEXT;
+ i_assert(*error_r != NULL);
+}
+
+int qp_decoder_more(struct qp_decoder *qp, const unsigned char *src,
+ size_t src_size, size_t *invalid_src_pos_r,
+ const char **error_r)
+{
+ const char *error;
+ size_t i;
+
+ *invalid_src_pos_r = SIZE_MAX;
+ *error_r = NULL;
+
+ for (i = 0; i < src_size; ) {
+ switch (qp->state) {
+ case STATE_TEXT:
+ i += qp_decoder_more_text(qp, src+i, src_size-i);
+ /* don't increment i any more than we already did,
+ so continue instead of break */
+ continue;
+ case STATE_WHITESPACE:
+ if (QP_IS_TRAILING_WHITESPACE(src[i])) {
+ /* more whitespace */
+ if (qp->whitespace->used <= QP_MAX_WHITESPACE_LEN)
+ buffer_append_c(qp->whitespace, src[i]);
+ } else if (src[i] == '\r') {
+ qp->state = STATE_CR;
+ } else if (src[i] == '\n') {
+ /* drop the trailing whitespace */
+ buffer_append(qp->dest, "\r\n", 2);
+ buffer_set_used_size(qp->whitespace, 0);
+ } else {
+ /* this wasn't trailing whitespace.
+ put it back. */
+ buffer_append_buf(qp->dest, qp->whitespace,
+ 0, SIZE_MAX);
+ if (qp->whitespace->used > QP_MAX_WHITESPACE_LEN) {
+ /* we already truncated some of the
+ whitespace away, because the line
+ is too long */
+ if (*invalid_src_pos_r == SIZE_MAX) {
+ *invalid_src_pos_r = i;
+ *error_r = "Too much whitespace";
+ }
+ }
+ buffer_set_used_size(qp->whitespace, 0);
+ qp->state = STATE_TEXT;
+ continue; /* don't increment i */
+ }
+ break;
+ case STATE_EQUALS:
+ if ((src[i] >= '0' && src[i] <= '9') ||
+ (src[i] >= 'A' && src[i] <= 'F') ||
+ /* lowercase hex isn't strictly valid, but allow */
+ (src[i] >= 'a' && src[i] <= 'f')) {
+ qp->hexchar = src[i];
+ qp->state = STATE_HEX2;
+ } else if (QP_IS_TRAILING_WHITESPACE(src[i])) {
+ i_assert(qp->whitespace->used == 0);
+ buffer_append_c(qp->whitespace, src[i]);
+ qp->state = STATE_EQUALS_WHITESPACE;
+ } else if (src[i] == '\r')
+ qp->state = STATE_SOFTCR;
+ else if (src[i] == '\n') {
+ qp->state = STATE_TEXT;
+ } else {
+ /* invalid input */
+ qp_decoder_invalid(qp, &error);
+ if (*invalid_src_pos_r == SIZE_MAX) {
+ *invalid_src_pos_r = i;
+ *error_r = error;
+ }
+ continue; /* don't increment i */
+ }
+ break;
+ case STATE_HEX2:
+ if ((src[i] >= '0' && src[i] <= '9') ||
+ (src[i] >= 'A' && src[i] <= 'F') ||
+ (src[i] >= 'a' && src[i] <= 'f')) {
+ char data[3];
+
+ data[0] = qp->hexchar;
+ data[1] = src[i];
+ data[2] = '\0';
+ if (hex_to_binary(data, qp->dest) < 0)
+ i_unreached();
+ qp->state = STATE_TEXT;
+ } else {
+ /* invalid input */
+ qp_decoder_invalid(qp, &error);
+ if (*invalid_src_pos_r == SIZE_MAX) {
+ *invalid_src_pos_r = i;
+ *error_r = error;
+ }
+ continue; /* don't increment i */
+ }
+ break;
+ case STATE_EQUALS_WHITESPACE:
+ if (QP_IS_TRAILING_WHITESPACE(src[i])) {
+ if (qp->whitespace->used <= QP_MAX_WHITESPACE_LEN)
+ buffer_append_c(qp->whitespace, src[i]);
+ else {
+ /* if this isn't going to get truncated
+ anyway, it's going to be an error */
+ }
+ } else if (src[i] == '\r')
+ qp->state = STATE_SOFTCR;
+ else if (src[i] == '\n') {
+ buffer_set_used_size(qp->whitespace, 0);
+ qp->state = STATE_TEXT;
+ } else {
+ /* =<whitespace> not followed by [CR]LF
+ is invalid. */
+ qp_decoder_invalid(qp, &error);
+ if (*invalid_src_pos_r == SIZE_MAX) {
+ *invalid_src_pos_r = i;
+ *error_r = error;
+ }
+ continue; /* don't increment i */
+ }
+ break;
+ case STATE_CR:
+ case STATE_SOFTCR:
+ if (src[i] == '\n') {
+ buffer_set_used_size(qp->whitespace, 0);
+ if (qp->state != STATE_SOFTCR)
+ buffer_append(qp->dest, "\r\n", 2);
+ qp->state = STATE_TEXT;
+ } else {
+ qp_decoder_invalid(qp, &error);
+ if (*invalid_src_pos_r == SIZE_MAX) {
+ *invalid_src_pos_r = i;
+ *error_r = error;
+ }
+ continue; /* don't increment i */
+ }
+ break;
+ }
+ i++;
+ }
+ i_assert((*invalid_src_pos_r == SIZE_MAX) == (*error_r == NULL));
+ return *invalid_src_pos_r == SIZE_MAX ? 0 : -1;
+}
+
+int qp_decoder_finish(struct qp_decoder *qp, const char **error_r)
+{
+ int ret;
+
+ if (qp->state == STATE_TEXT || qp->state == STATE_WHITESPACE) {
+ ret = 0;
+ *error_r = NULL;
+ } else {
+ qp_decoder_invalid(qp, error_r);
+ ret = -1;
+ }
+ qp->state = STATE_TEXT;
+ buffer_set_used_size(qp->whitespace, 0);
+ return ret;
+}
diff --git a/src/lib-mail/qp-decoder.h b/src/lib-mail/qp-decoder.h
new file mode 100644
index 0000000..f69711f
--- /dev/null
+++ b/src/lib-mail/qp-decoder.h
@@ -0,0 +1,19 @@
+#ifndef QP_DECODER_H
+#define QP_DECODER_H
+
+/* Initialize quoted-printable decoder. Write all the decoded output to dest. */
+struct qp_decoder *qp_decoder_init(buffer_t *dest);
+void qp_decoder_deinit(struct qp_decoder **qp);
+
+/* Translate more quoted printable data into binary. Returns 0 if input was
+ valid, -1 if there were some decoding errors (which were skipped over).
+ LFs without preceding CR are returned as CRLF (but =0A isn't). */
+int qp_decoder_more(struct qp_decoder *qp, const unsigned char *src,
+ size_t src_size, size_t *invalid_src_pos_r,
+ const char **error_r);
+/* Finish decoding any pending input. Returns the same as qp_decoder_more().
+ This function also resets the entire decoder state, so the same decoder can
+ be used to decode more data if wanted. */
+int qp_decoder_finish(struct qp_decoder *qp, const char **error_r);
+
+#endif
diff --git a/src/lib-mail/qp-encoder.c b/src/lib-mail/qp-encoder.c
new file mode 100644
index 0000000..37a4412
--- /dev/null
+++ b/src/lib-mail/qp-encoder.c
@@ -0,0 +1,162 @@
+/* Copyright (c) 2017-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "str.h"
+#include "istream.h"
+#include "istream-private.h"
+#include "qp-encoder.h"
+#include <ctype.h>
+
+enum qp_encoder_last_char {
+ QP_ENCODER_LAST_ANY = 0,
+ QP_ENCODER_LAST_CR,
+ QP_ENCODER_LAST_WHITE_SPACE,
+};
+
+struct qp_encoder {
+ const char *linebreak;
+ string_t *dest;
+ size_t line_len;
+ size_t max_len;
+ enum qp_encoder_flag flags;
+ enum qp_encoder_last_char last_char;
+ bool add_header_preamble:1;
+};
+
+struct qp_encoder *
+qp_encoder_init(string_t *dest, unsigned int max_len, enum qp_encoder_flag flags)
+{
+ i_assert(max_len > 0);
+
+ if ((flags & QP_ENCODER_FLAG_HEADER_FORMAT) != 0 &&
+ (flags & QP_ENCODER_FLAG_BINARY_DATA) != 0)
+ i_panic("qp encoder cannot do header format with binary data");
+
+ struct qp_encoder *qp = i_new(struct qp_encoder, 1);
+ qp->flags = flags;
+ qp->dest = dest;
+ qp->max_len = max_len;
+
+ if ((flags & QP_ENCODER_FLAG_HEADER_FORMAT) != 0) {
+ qp->linebreak = "?=\r\n =?utf-8?Q?";
+ qp->add_header_preamble = TRUE;
+ } else {
+ qp->linebreak = "=\r\n";
+ }
+ return qp;
+}
+
+void qp_encoder_deinit(struct qp_encoder **qp)
+{
+ i_free(*qp);
+}
+
+static inline void
+qp_encode_or_break(struct qp_encoder *qp, unsigned char c)
+{
+ bool encode = FALSE;
+
+ if ((qp->flags & QP_ENCODER_FLAG_HEADER_FORMAT) != 0) {
+ if (c == ' ')
+ c = '_';
+ else if (c != '\t' &&
+ (c == '?' || c == '_' || c == '=' || c < 33 || c > 126))
+ encode = TRUE;
+ } else if (c != ' ' && c != '\t' &&
+ (c == '=' || c < 33 || c > 126)) {
+ encode = TRUE;
+ }
+
+ /* Include terminating = as well */
+ if ((c == ' ' || c == '\t') && qp->line_len + 4 >= qp->max_len) {
+ const char *ptr = strchr(qp->linebreak, '\n');
+ str_printfa(qp->dest, "=%02X%s", c, qp->linebreak);
+ if (ptr != NULL)
+ qp->line_len = strlen(ptr+1);
+ else
+ qp->line_len = 0;
+ return;
+ }
+
+ /* Include terminating = as well */
+ if (qp->line_len + (encode?4:2) >= qp->max_len) {
+ str_append(qp->dest, qp->linebreak);
+ qp->line_len = 0;
+ }
+
+ if (encode) {
+ str_printfa(qp->dest, "=%02X", c);
+ qp->line_len += 3;
+ } else {
+ str_append_c(qp->dest, c);
+ qp->line_len += 1;
+ }
+}
+
+void qp_encoder_more(struct qp_encoder *qp, const void *_src, size_t src_size)
+{
+ const unsigned char *src = _src;
+ i_assert(_src != NULL || src_size == 0);
+ if (src_size == 0)
+ return;
+ if (qp->add_header_preamble) {
+ size_t used = qp->dest->used;
+ qp->add_header_preamble = FALSE;
+ str_append(qp->dest, "=?utf-8?Q?");
+ qp->line_len = qp->dest->used - used;
+ }
+ for(unsigned int i = 0; i < src_size; i++) {
+ unsigned char c = src[i];
+ /* if input is not binary data and we encounter newline
+ convert it as crlf, or if the last byte was CR, preserve
+ CRLF */
+ if (c == '\n' &&
+ ((qp->flags & (QP_ENCODER_FLAG_BINARY_DATA|QP_ENCODER_FLAG_HEADER_FORMAT)) == 0 ||
+ qp->last_char == QP_ENCODER_LAST_CR)) {
+ str_append_c(qp->dest, '\r');
+ str_append_c(qp->dest, '\n');
+ /* reset line length here */
+ qp->line_len = 0;
+ qp->last_char = QP_ENCODER_LAST_ANY;
+ continue;
+ } else if (qp->last_char == QP_ENCODER_LAST_CR) {
+ qp_encode_or_break(qp, '\r');
+ qp->last_char = QP_ENCODER_LAST_ANY;
+ }
+
+ if (c == ' ' || c == '\t')
+ qp->last_char = QP_ENCODER_LAST_WHITE_SPACE;
+ else if (c == '\r')
+ qp->last_char = QP_ENCODER_LAST_CR;
+ else
+ qp->last_char = QP_ENCODER_LAST_ANY;
+
+ if (c != '\r')
+ qp_encode_or_break(qp, c);
+ }
+}
+
+void qp_encoder_finish(struct qp_encoder *qp)
+{
+ switch (qp->last_char) {
+ case QP_ENCODER_LAST_CR:
+ /* Last char was CR which was not yet encoded */
+ qp_encode_or_break(qp, '\r');
+ break;
+ case QP_ENCODER_LAST_WHITE_SPACE:
+ /* Last char was a white space, it must be followed by
+ * a printable char. */
+ str_append_c(qp->dest, '=');
+ break;
+ default:
+ break;
+ }
+
+ if ((qp->flags & QP_ENCODER_FLAG_HEADER_FORMAT) != 0 &&
+ !qp->add_header_preamble)
+ str_append(qp->dest, "?=");
+ if ((qp->flags & QP_ENCODER_FLAG_HEADER_FORMAT) != 0)
+ qp->add_header_preamble = TRUE;
+ qp->line_len = 0;
+ qp->last_char = QP_ENCODER_LAST_ANY;
+}
diff --git a/src/lib-mail/qp-encoder.h b/src/lib-mail/qp-encoder.h
new file mode 100644
index 0000000..f4a6148
--- /dev/null
+++ b/src/lib-mail/qp-encoder.h
@@ -0,0 +1,25 @@
+#ifndef QP_ENCODER_H
+#define QP_ENCODER_H 1
+
+enum qp_encoder_flag {
+ /* encode spaces as underscores, encode crlfs, adds =?utf-8?q?..?= encapsulation */
+ QP_ENCODER_FLAG_HEADER_FORMAT = 0x1,
+ /* treat input as true binary, no lf => crlf conversion, only CRLF is preserved */
+ QP_ENCODER_FLAG_BINARY_DATA = 0x2,
+};
+
+/* Initialize quoted-printable encoder. Write all the encoded output to dest. */
+struct qp_encoder *qp_encoder_init(string_t *dest, unsigned int max_length,
+ enum qp_encoder_flag flags);
+void qp_encoder_deinit(struct qp_encoder **qp);
+
+/* Translate more (binary) data into quoted printable.
+ If QP_ENCODER_FLAG_BINARY_DATA is not set, text is assumed to be in
+ UTF-8 (but not enforced). No other character sets are supported. */
+void qp_encoder_more(struct qp_encoder *qp, const void *src, size_t src_size);
+/* Finish encoding any pending input.
+ This function also resets the entire encoder state, so the same encoder can
+ be used to encode more data if wanted. */
+void qp_encoder_finish(struct qp_encoder *qp);
+
+#endif
diff --git a/src/lib-mail/quoted-printable.c b/src/lib-mail/quoted-printable.c
new file mode 100644
index 0000000..9bfbe35
--- /dev/null
+++ b/src/lib-mail/quoted-printable.c
@@ -0,0 +1,49 @@
+/* Copyright (c) 2002-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "buffer.h"
+#include "hex-binary.h"
+#include "quoted-printable.h"
+
+int quoted_printable_q_decode(const unsigned char *src, size_t src_size,
+ buffer_t *dest)
+{
+ char hexbuf[3];
+ size_t src_pos, next;
+ bool errors = FALSE;
+
+ hexbuf[2] = '\0';
+
+ next = 0;
+ for (src_pos = 0; src_pos < src_size; src_pos++) {
+ if (src[src_pos] != '_' && src[src_pos] != '=')
+ continue;
+
+ buffer_append(dest, src + next, src_pos - next);
+ next = src_pos;
+
+ if (src[src_pos] == '_') {
+ buffer_append_c(dest, ' ');
+ next++;
+ continue;
+ }
+
+ if (src_pos+2 >= src_size)
+ break;
+
+ /* =<hex> */
+ hexbuf[0] = src[src_pos+1];
+ hexbuf[1] = src[src_pos+2];
+
+ if (hex_to_binary(hexbuf, dest) == 0) {
+ src_pos += 2;
+ next = src_pos+1;
+ } else {
+ /* non-hex data, show as-is */
+ errors = TRUE;
+ next = src_pos;
+ }
+ }
+ buffer_append(dest, src + next, src_size - next);
+ return errors ? -1 : 0;
+}
diff --git a/src/lib-mail/quoted-printable.h b/src/lib-mail/quoted-printable.h
new file mode 100644
index 0000000..2233912
--- /dev/null
+++ b/src/lib-mail/quoted-printable.h
@@ -0,0 +1,8 @@
+#ifndef QUOTED_PRINTABLE_H
+#define QUOTED_PRINTABLE_H
+
+/* Decode MIME "Q" encoding. */
+int quoted_printable_q_decode(const unsigned char *src, size_t src_size,
+ buffer_t *dest);
+
+#endif
diff --git a/src/lib-mail/rfc2231-parser.c b/src/lib-mail/rfc2231-parser.c
new file mode 100644
index 0000000..119d48d
--- /dev/null
+++ b/src/lib-mail/rfc2231-parser.c
@@ -0,0 +1,179 @@
+/* Copyright (c) 2008-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "str.h"
+#include "rfc822-parser.h"
+#include "rfc2231-parser.h"
+
+
+struct rfc2231_parameter {
+ const char *key, *value;
+ unsigned int idx;
+ bool extended;
+};
+
+static int rfc2231_parameter_cmp(const struct rfc2231_parameter *r1,
+ const struct rfc2231_parameter *r2)
+{
+ int ret;
+
+ ret = strcmp(r1->key, r2->key);
+ if (ret != 0)
+ return ret;
+
+ return r1->idx < r2->idx ? -1 :
+ (r1-> idx > r2->idx ? 1 : 0);
+}
+
+static void rfc2231_escape(string_t *dest, const char *src)
+{
+ for (; *src != '\0'; src++) {
+ if (*src == '%')
+ str_append(dest, "%25");
+ else
+ str_append_c(dest, *src);
+ }
+}
+
+int rfc2231_parse(struct rfc822_parser_context *ctx,
+ const char *const **result_r)
+{
+ ARRAY_TYPE(const_string) result;
+ ARRAY(struct rfc2231_parameter) rfc2231_params_arr;
+ struct rfc2231_parameter rfc2231_param;
+ const struct rfc2231_parameter *rfc2231_params;
+ const char *key, *p, *p2;
+ string_t *str;
+ unsigned int i, j, count, next, next_idx;
+ bool ok, have_extended, broken = FALSE;
+ const char *prev_replacement_str;
+ int ret;
+
+ /* Temporarily replace the nul_replacement_char while we're parsing
+ the content-params. It'll be restored before we return. */
+ prev_replacement_str = ctx->nul_replacement_str;
+ ctx->nul_replacement_str = RFC822_NUL_REPLACEMENT_STR;
+
+ /* Get a list of all parameters. RFC 2231 uses key*<n>[*]=value pairs,
+ which we want to merge to a key[*]=value pair. Save them to a
+ separate array. */
+ i_zero(&rfc2231_param);
+ t_array_init(&result, 8);
+ t_array_init(&rfc2231_params_arr, 8);
+ str = t_str_new(64);
+ while ((ret = rfc822_parse_content_param(ctx, &key, str)) != 0) {
+ if (ret < 0) {
+ /* try to continue anyway.. */
+ broken = TRUE;
+ if (ctx->data >= ctx->end)
+ break;
+ ctx->data++;
+ continue;
+ }
+ p = strchr(key, '*');
+ if (p != NULL) {
+ p2 = p;
+ if (p[1] != '\0') {
+ p++;
+ rfc2231_param.idx = 0;
+ for (; *p >= '0' && *p <= '9'; p++) {
+ rfc2231_param.idx =
+ rfc2231_param.idx*10 + *p - '0';
+ }
+ }
+ if (*p != '*')
+ rfc2231_param.extended = FALSE;
+ else {
+ rfc2231_param.extended = TRUE;
+ p++;
+ }
+ if (*p != '\0')
+ p = NULL;
+ else {
+ rfc2231_param.key = t_strdup_until(key, p2);
+ rfc2231_param.value = t_strdup(str_c(str));
+ array_push_back(&rfc2231_params_arr,
+ &rfc2231_param);
+ }
+ }
+ if (p == NULL) {
+ const char *value = t_strdup(str_c(str));
+ array_push_back(&result, &key);
+ array_push_back(&result, &value);
+ }
+ }
+ ctx->nul_replacement_str = prev_replacement_str;
+
+ if (array_count(&rfc2231_params_arr) == 0) {
+ /* No RFC 2231 parameters */
+ array_append_zero(&result); /* NULL-terminate */
+ *result_r = array_front(&result);
+ return broken ? -1 : 0;
+ }
+
+ /* Merge the RFC 2231 parameters. Since their order isn't guaranteed to
+ be ascending, start by sorting them. */
+ array_sort(&rfc2231_params_arr, rfc2231_parameter_cmp);
+ rfc2231_params = array_get(&rfc2231_params_arr, &count);
+
+ /* keys are now sorted primarily by their name and secondarily by
+ their index. If any indexes are missing, fallback to assuming
+ these aren't RFC 2231 encoded parameters. */
+ for (i = 0; i < count; i = next) {
+ ok = TRUE;
+ have_extended = FALSE;
+ next_idx = 0;
+ for (j = i; j < count; j++) {
+ if (strcasecmp(rfc2231_params[i].key,
+ rfc2231_params[j].key) != 0)
+ break;
+ if (rfc2231_params[j].idx != next_idx) {
+ /* missing indexes */
+ ok = FALSE;
+ }
+ if (rfc2231_params[j].extended)
+ have_extended = TRUE;
+ next_idx++;
+ }
+ next = j;
+
+ if (!ok) {
+ /* missing indexes */
+ for (j = i; j < next; j++) {
+ key = t_strdup_printf(
+ rfc2231_params[j].extended ?
+ "%s*%u*" : "%s*%u",
+ rfc2231_params[j].key,
+ rfc2231_params[j].idx);
+ array_push_back(&result, &key);
+ array_push_back(&result,
+ &rfc2231_params[j].value);
+ }
+ } else {
+ /* everything was successful */
+ str_truncate(str, 0);
+ if (!rfc2231_params[i].extended && have_extended)
+ str_append(str, "''");
+ for (j = i; j < next; j++) {
+ if (!rfc2231_params[j].extended &&
+ have_extended) {
+ rfc2231_escape(str,
+ rfc2231_params[j].value);
+ } else {
+ str_append(str,
+ rfc2231_params[j].value);
+ }
+ }
+ key = rfc2231_params[i].key;
+ if (have_extended)
+ key = t_strconcat(key, "*", NULL);
+ const char *value = t_strdup(str_c(str));
+ array_push_back(&result, &key);
+ array_push_back(&result, &value);
+ }
+ }
+ array_append_zero(&result); /* NULL-terminate */
+ *result_r = array_front(&result);
+ return broken ? -1 : 0;
+}
diff --git a/src/lib-mail/rfc2231-parser.h b/src/lib-mail/rfc2231-parser.h
new file mode 100644
index 0000000..95cadc2
--- /dev/null
+++ b/src/lib-mail/rfc2231-parser.h
@@ -0,0 +1,13 @@
+#ifndef RFC2231_PARSER_H
+#define RFC2231_PARSER_H
+
+/* Parse all content parameters using rfc822_parse_content_param() and return
+ them as a NULL-terminated [key, value] array. RFC 2231-style continuations
+ are merged to a single key. NULs are converted into unicode replacement
+ character (U+FFFD). Returns -1 if some of the input was invalid (but valid
+ key/value pairs are still returned), 0 if everything looked ok. */
+int ATTR_NOWARN_UNUSED_RESULT
+rfc2231_parse(struct rfc822_parser_context *ctx,
+ const char *const **result_r);
+
+#endif
diff --git a/src/lib-mail/rfc822-parser.c b/src/lib-mail/rfc822-parser.c
new file mode 100644
index 0000000..c8595b4
--- /dev/null
+++ b/src/lib-mail/rfc822-parser.c
@@ -0,0 +1,522 @@
+/* Copyright (c) 2005-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "str.h"
+#include "strescape.h"
+#include "rfc822-parser.h"
+
+/*
+ atext = ALPHA / DIGIT / ; Any character except controls,
+ "!" / "#" / ; SP, and specials.
+ "$" / "%" / ; Used for atoms
+ "&" / "'" /
+ "*" / "+" /
+ "-" / "/" /
+ "=" / "?" /
+ "^" / "_" /
+ "`" / "{" /
+ "|" / "}" /
+ "~"
+
+ MIME:
+
+ token := 1*<any (US-ASCII) CHAR except SPACE, CTLs,
+ or tspecials>
+ tspecials := "(" / ")" / "<" / ">" / "@" /
+ "," / ";" / ":" / "\" / <">
+ "/" / "[" / "]" / "?" / "="
+
+ So token is same as dot-atom, except stops also at '/', '?' and '='.
+*/
+
+/* atext chars are marked with 1, alpha and digits with 2,
+ atext-but-mime-tspecials with 4 */
+unsigned char rfc822_atext_chars[256] = {
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, /* 0-15 */
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, /* 16-31 */
+ 0, 1, 0, 1, 1, 1, 1, 1, 0, 0, 1, 1, 0, 1, 0, 4, /* 32-47 */
+ 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 0, 0, 0, 4, 0, 4, /* 48-63 */
+ 0, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, /* 64-79 */
+ 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 0, 0, 0, 1, 1, /* 80-95 */
+ 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, /* 96-111 */
+ 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 1, 0, /* 112-127 */
+
+ 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
+ 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
+ 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
+ 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
+ 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
+ 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
+ 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
+ 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2
+};
+
+void rfc822_parser_init(struct rfc822_parser_context *ctx,
+ const unsigned char *data, size_t size,
+ string_t *last_comment)
+{
+ i_zero(ctx);
+ ctx->data = data;
+ ctx->end = data + size;
+ ctx->last_comment = last_comment;
+}
+
+int rfc822_skip_comment(struct rfc822_parser_context *ctx)
+{
+ const unsigned char *start;
+ size_t len;
+ int level = 1;
+
+ i_assert(*ctx->data == '(');
+
+ if (ctx->last_comment != NULL)
+ str_truncate(ctx->last_comment, 0);
+
+ start = ++ctx->data;
+ for (; ctx->data < ctx->end; ctx->data++) {
+ switch (*ctx->data) {
+ case '\0':
+ if (ctx->last_comment != NULL &&
+ ctx->nul_replacement_str != NULL) {
+ str_append_data(ctx->last_comment, start,
+ ctx->data - start);
+ str_append(ctx->last_comment,
+ ctx->nul_replacement_str);
+ start = ctx->data + 1;
+ }
+ break;
+ case '(':
+ level++;
+ break;
+ case ')':
+ if (--level == 0) {
+ if (ctx->last_comment != NULL) {
+ str_append_data(ctx->last_comment, start,
+ ctx->data - start);
+ }
+ ctx->data++;
+ return ctx->data < ctx->end ? 1 : 0;
+ }
+ break;
+ case '\n':
+ /* folding whitespace, remove the (CR)LF */
+ if (ctx->last_comment == NULL)
+ break;
+ len = ctx->data - start;
+ if (len > 0 && start[len-1] == '\r')
+ len--;
+ str_append_data(ctx->last_comment, start, len);
+ start = ctx->data + 1;
+ break;
+ case '\\':
+ ctx->data++;
+ if (ctx->data >= ctx->end)
+ return -1;
+
+ if (*ctx->data == '\r' || *ctx->data == '\n' ||
+ *ctx->data == '\0') {
+ /* quoted-pair doesn't allow CR/LF/NUL.
+ They are part of the obs-qp though, so don't
+ return them as error. */
+ ctx->data--;
+ break;
+ }
+ if (ctx->last_comment != NULL) {
+ str_append_data(ctx->last_comment, start,
+ ctx->data - start - 1);
+ }
+ start = ctx->data;
+ break;
+ }
+ }
+
+ /* missing ')' */
+ return -1;
+}
+
+int rfc822_skip_lwsp(struct rfc822_parser_context *ctx)
+{
+ for (; ctx->data < ctx->end;) {
+ if (*ctx->data == ' ' || *ctx->data == '\t' ||
+ *ctx->data == '\r' || *ctx->data == '\n') {
+ ctx->data++;
+ continue;
+ }
+
+ if (*ctx->data != '(')
+ break;
+
+ if (rfc822_skip_comment(ctx) < 0)
+ return -1;
+ }
+ return ctx->data < ctx->end ? 1 : 0;
+}
+
+int rfc822_parse_atom(struct rfc822_parser_context *ctx, string_t *str)
+{
+ const unsigned char *start;
+
+ /*
+ atom = [CFWS] 1*atext [CFWS]
+ atext =
+ ; Any character except controls, SP, and specials.
+ */
+ if (ctx->data >= ctx->end || !IS_ATEXT(*ctx->data))
+ return -1;
+
+ for (start = ctx->data++; ctx->data < ctx->end; ctx->data++) {
+ if (IS_ATEXT(*ctx->data))
+ continue;
+
+ str_append_data(str, start, ctx->data - start);
+ return rfc822_skip_lwsp(ctx);
+ }
+
+ str_append_data(str, start, ctx->data - start);
+ return 0;
+}
+
+int rfc822_parse_dot_atom(struct rfc822_parser_context *ctx, string_t *str)
+{
+ const unsigned char *start;
+ int ret;
+
+ /*
+ dot-atom = [CFWS] dot-atom-text [CFWS]
+ dot-atom-text = 1*atext *("." 1*atext)
+
+ atext =
+ ; Any character except controls, SP, and specials.
+
+ For RFC-822 compatibility allow LWSP around '.'
+ */
+ if (ctx->data >= ctx->end || !IS_ATEXT(*ctx->data))
+ return -1;
+
+ for (start = ctx->data++; ctx->data < ctx->end; ) {
+ if (IS_ATEXT(*ctx->data)) {
+ ctx->data++;
+ continue;
+ }
+
+ if (start == ctx->data)
+ return -1;
+ str_append_data(str, start, ctx->data - start);
+
+ if ((ret = rfc822_skip_lwsp(ctx)) <= 0)
+ return ret;
+
+ if (*ctx->data != '.')
+ return 1;
+
+ ctx->data++;
+ str_append_c(str, '.');
+
+ if (rfc822_skip_lwsp(ctx) <= 0)
+ return -1;
+ start = ctx->data;
+ }
+
+ i_assert(start != ctx->data);
+ str_append_data(str, start, ctx->data - start);
+ return 0;
+}
+
+int rfc822_parse_mime_token(struct rfc822_parser_context *ctx, string_t *str)
+{
+ const unsigned char *start;
+
+ for (start = ctx->data; ctx->data < ctx->end; ctx->data++) {
+ if (IS_ATEXT_NON_TSPECIAL(*ctx->data) || *ctx->data == '.')
+ continue;
+
+ str_append_data(str, start, ctx->data - start);
+ return rfc822_skip_lwsp(ctx);
+ }
+
+ str_append_data(str, start, ctx->data - start);
+ return 0;
+}
+
+int rfc822_parse_quoted_string(struct rfc822_parser_context *ctx, string_t *str)
+{
+ const unsigned char *start;
+ size_t len;
+
+ i_assert(ctx->data < ctx->end);
+ i_assert(*ctx->data == '"');
+ ctx->data++;
+
+ for (start = ctx->data; ctx->data < ctx->end; ctx->data++) {
+ switch (*ctx->data) {
+ case '\0':
+ if (ctx->nul_replacement_str != NULL) {
+ str_append_data(str, start, ctx->data - start);
+ str_append(str, ctx->nul_replacement_str);
+ start = ctx->data + 1;
+ }
+ break;
+ case '"':
+ str_append_data(str, start, ctx->data - start);
+ ctx->data++;
+ return rfc822_skip_lwsp(ctx);
+ case '\n':
+ /* folding whitespace, remove the (CR)LF */
+ len = ctx->data - start;
+ if (len > 0 && start[len-1] == '\r')
+ len--;
+ str_append_data(str, start, len);
+ start = ctx->data + 1;
+ break;
+ case '\\':
+ ctx->data++;
+ if (ctx->data >= ctx->end)
+ return -1;
+
+ if (*ctx->data == '\r' || *ctx->data == '\n' ||
+ *ctx->data == '\0') {
+ /* quoted-pair doesn't allow CR/LF/NUL.
+ They are part of the obs-qp though, so don't
+ return them as error. */
+ ctx->data--;
+ break;
+ }
+ str_append_data(str, start, ctx->data - start - 1);
+ start = ctx->data;
+ break;
+ }
+ }
+
+ /* missing '"' */
+ return -1;
+}
+
+static int
+rfc822_parse_atom_or_dot(struct rfc822_parser_context *ctx, string_t *str)
+{
+ const unsigned char *start;
+
+ /*
+ atom = [CFWS] 1*atext [CFWS]
+ atext =
+ ; Any character except controls, SP, and specials.
+
+ The difference between this function and rfc822_parse_dot_atom()
+ is that this doesn't just silently skip over all the whitespace.
+ */
+ for (start = ctx->data; ctx->data < ctx->end; ctx->data++) {
+ if (IS_ATEXT(*ctx->data) || *ctx->data == '.')
+ continue;
+
+ str_append_data(str, start, ctx->data - start);
+ return rfc822_skip_lwsp(ctx);
+ }
+
+ str_append_data(str, start, ctx->data - start);
+ return 0;
+}
+
+int rfc822_parse_phrase(struct rfc822_parser_context *ctx, string_t *str)
+{
+ int ret;
+
+ /*
+ phrase = 1*word / obs-phrase
+ word = atom / quoted-string
+ obs-phrase = word *(word / "." / CFWS)
+ */
+
+ if (ctx->data >= ctx->end)
+ return 0;
+ if (*ctx->data == '.')
+ return -1;
+
+ for (;;) {
+ if (*ctx->data == '"')
+ ret = rfc822_parse_quoted_string(ctx, str);
+ else
+ ret = rfc822_parse_atom_or_dot(ctx, str);
+
+ if (ret <= 0)
+ return ret;
+
+ if (!IS_ATEXT(*ctx->data) && *ctx->data != '"'
+ && *ctx->data != '.')
+ break;
+ str_append_c(str, ' ');
+ }
+ return rfc822_skip_lwsp(ctx);
+}
+
+static int
+rfc822_parse_domain_literal(struct rfc822_parser_context *ctx, string_t *str)
+{
+ const unsigned char *start;
+ size_t len;
+
+ /*
+ domain-literal = [CFWS] "[" *([FWS] dcontent) [FWS] "]" [CFWS]
+ dcontent = dtext / quoted-pair
+ dtext = NO-WS-CTL / ; Non white space controls
+ %d33-90 / ; The rest of the US-ASCII
+ %d94-126 ; characters not including "[",
+ ; "]", or "\"
+ */
+ i_assert(ctx->data < ctx->end);
+ i_assert(*ctx->data == '[');
+
+ for (start = ctx->data++; ctx->data < ctx->end; ctx->data++) {
+ switch (*ctx->data) {
+ case '\0':
+ if (ctx->nul_replacement_str != NULL) {
+ str_append_data(str, start, ctx->data - start);
+ str_append(str, ctx->nul_replacement_str);
+ start = ctx->data + 1;
+ }
+ break;
+ case '[':
+ /* not allowed */
+ return -1;
+ case ']':
+ str_append_data(str, start, ctx->data - start + 1);
+ ctx->data++;
+ return rfc822_skip_lwsp(ctx);
+ case '\n':
+ /* folding whitespace, remove the (CR)LF */
+ len = ctx->data - start;
+ if (len > 0 && start[len-1] == '\r')
+ len--;
+ str_append_data(str, start, len);
+ start = ctx->data + 1;
+ break;
+ case '\\':
+ /* note: the '\' is preserved in the output */
+ ctx->data++;
+ if (ctx->data >= ctx->end)
+ return -1;
+
+ if (*ctx->data == '\r' || *ctx->data == '\n' ||
+ *ctx->data == '\0') {
+ /* quoted-pair doesn't allow CR/LF/NUL.
+ They are part of the obs-qp though, so don't
+ return them as error. */
+ str_append_data(str, start, ctx->data - start);
+ start = ctx->data;
+ ctx->data--;
+ break;
+ }
+ }
+ }
+
+ /* missing ']' */
+ return -1;
+}
+
+int rfc822_parse_domain(struct rfc822_parser_context *ctx, string_t *str)
+{
+ /*
+ domain = dot-atom / domain-literal / obs-domain
+ domain-literal = [CFWS] "[" *([FWS] dcontent) [FWS] "]" [CFWS]
+ obs-domain = atom *("." atom)
+ */
+ i_assert(ctx->data < ctx->end);
+ i_assert(*ctx->data == '@');
+ ctx->data++;
+
+ if (rfc822_skip_lwsp(ctx) <= 0)
+ return -1;
+
+ if (*ctx->data == '[')
+ return rfc822_parse_domain_literal(ctx, str);
+ else
+ return rfc822_parse_dot_atom(ctx, str);
+}
+
+int rfc822_parse_content_type(struct rfc822_parser_context *ctx, string_t *str)
+{
+ size_t str_pos_0 = str->used;
+ if (rfc822_skip_lwsp(ctx) <= 0)
+ return -1;
+
+ /* get main type, require at least one byte */
+ if (rfc822_parse_mime_token(ctx, str) <= 0 ||
+ str->used == str_pos_0)
+ return -1;
+
+ /* skip over "/" */
+ if (*ctx->data != '/') {
+ str_truncate(str, str_pos_0);
+ return -1;
+ }
+ ctx->data++;
+ if (rfc822_skip_lwsp(ctx) <= 0) {
+ str_truncate(str, str_pos_0);
+ return -1;
+ }
+ str_append_c(str, '/');
+
+ size_t str_pos = str->used;
+ /* get subtype, require at least one byte,
+ and check the next separator to avoid accepting
+ invalid values. */
+ int ret;
+ if ((ret = rfc822_parse_mime_token(ctx, str)) < 0 ||
+ str->used == str_pos ||
+ (ctx->data != ctx->end && *ctx->data != ';')) {
+ str_truncate(str, str_pos_0);
+ return -1;
+ }
+ return ret;
+}
+
+int rfc822_parse_content_param(struct rfc822_parser_context *ctx,
+ const char **key_r, string_t *value)
+{
+ string_t *key;
+ int ret;
+
+ /* .. := *(";" parameter)
+ parameter := attribute "=" value
+ attribute := token
+ value := token / quoted-string
+ */
+ *key_r = NULL;
+ str_truncate(value, 0);
+
+ if (ctx->data >= ctx->end)
+ return 0;
+ if (*ctx->data != ';')
+ return -1;
+ ctx->data++;
+
+ if (rfc822_skip_lwsp(ctx) <= 0)
+ return -1;
+
+ key = t_str_new(64);
+ if (rfc822_parse_mime_token(ctx, key) <= 0)
+ return -1;
+
+ if (*ctx->data != '=')
+ return -1;
+ ctx->data++;
+
+ if ((ret = rfc822_skip_lwsp(ctx)) <= 0) {
+ /* broken / no value */
+ } else if (*ctx->data == '"') {
+ ret = rfc822_parse_quoted_string(ctx, value);
+ } else if (ctx->data < ctx->end && *ctx->data == '=') {
+ /* workaround for broken input:
+ name==?utf-8?b?...?= */
+ while (ctx->data < ctx->end && *ctx->data != ';' &&
+ *ctx->data != ' ' && *ctx->data != '\t' &&
+ *ctx->data != '\r' && *ctx->data != '\n') {
+ str_append_c(value, *ctx->data);
+ ctx->data++;
+ }
+ } else {
+ ret = rfc822_parse_mime_token(ctx, value);
+ }
+
+ *key_r = str_c(key);
+ return ret < 0 ? -1 : 1;
+}
diff --git a/src/lib-mail/rfc822-parser.h b/src/lib-mail/rfc822-parser.h
new file mode 100644
index 0000000..c001f76
--- /dev/null
+++ b/src/lib-mail/rfc822-parser.h
@@ -0,0 +1,71 @@
+#ifndef RFC822_PARSER_H
+#define RFC822_PARSER_H
+
+#include "unichar.h"
+
+/* This can be used as a common NUL replacement character */
+#define RFC822_NUL_REPLACEMENT_STR UNICODE_REPLACEMENT_CHAR_UTF8
+
+struct rfc822_parser_context {
+ const unsigned char *data, *end;
+ string_t *last_comment;
+
+ /* Replace NUL characters with this string */
+ const char *nul_replacement_str;
+};
+
+#define IS_ATEXT(c) \
+ (rfc822_atext_chars[(int)(unsigned char)(c)] != 0)
+#define IS_ATEXT_NON_TSPECIAL(c) \
+ ((rfc822_atext_chars[(int)(unsigned char)(c)] & 3) != 0)
+extern unsigned char rfc822_atext_chars[256];
+
+/* Parse given data using RFC 822 token parser. */
+void rfc822_parser_init(struct rfc822_parser_context *ctx,
+ const unsigned char *data, size_t size,
+ string_t *last_comment) ATTR_NULL(4);
+static inline void rfc822_parser_deinit(struct rfc822_parser_context *ctx)
+{
+ /* make sure the parsing didn't trigger a bug that caused reading
+ past the end pointer. */
+ i_assert(ctx->data <= ctx->end);
+ /* make sure the parser is no longer accessed */
+ ctx->data = ctx->end = NULL;
+}
+
+/* The functions below return 1 = more data available, 0 = no more data
+ available (but a value might have been returned now), -1 = invalid input.
+
+ LWSP is automatically skipped after value, but not before it. So typically
+ you begin with skipping LWSP and then start using the parse functions. */
+
+/* Parse comment. Assumes parser's data points to '(' */
+int rfc822_skip_comment(struct rfc822_parser_context *ctx);
+/* Skip LWSP if there is any */
+int ATTR_NOWARN_UNUSED_RESULT
+rfc822_skip_lwsp(struct rfc822_parser_context *ctx);
+/* Stop at next non-atext char */
+int rfc822_parse_atom(struct rfc822_parser_context *ctx, string_t *str);
+/* Like parse_atom() but don't stop at '.' */
+int rfc822_parse_dot_atom(struct rfc822_parser_context *ctx, string_t *str);
+/* Like parse_dot_atom() but stops for '/', '?' and '='.
+ Also it doesn't allow LWSP around '.' chars. */
+int rfc822_parse_mime_token(struct rfc822_parser_context *ctx, string_t *str);
+/* "quoted string" */
+int rfc822_parse_quoted_string(struct rfc822_parser_context *ctx,
+ string_t *str);
+/* atom or quoted-string */
+int rfc822_parse_phrase(struct rfc822_parser_context *ctx, string_t *str);
+/* dot-atom / domain-literal */
+int rfc822_parse_domain(struct rfc822_parser_context *ctx, string_t *str);
+
+/* Parse Content-Type header's type/subtype. */
+int rfc822_parse_content_type(struct rfc822_parser_context *ctx, string_t *str);
+/* For Content-Type style parameter parsing. Expect ";" key "=" value.
+ value is unescaped if needed. The returned key is allocated from data
+ stack. The value string is truncated for each call. Returns 1 = key/value
+ set, 0 = no more data, -1 = invalid input. */
+int rfc822_parse_content_param(struct rfc822_parser_context *ctx,
+ const char **key_r, string_t *value);
+
+#endif
diff --git a/src/lib-mail/test-istream-attachment.c b/src/lib-mail/test-istream-attachment.c
new file mode 100644
index 0000000..ac3096e
--- /dev/null
+++ b/src/lib-mail/test-istream-attachment.c
@@ -0,0 +1,486 @@
+/* Copyright (c) 2007-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "str.h"
+#include "sha1.h"
+#include "hash-format.h"
+#include "safe-mkstemp.h"
+#include "istream.h"
+#include "istream-crlf.h"
+#include "istream-attachment-extractor.h"
+#include "istream-attachment-connector.h"
+#include "ostream.h"
+#include "test-common.h"
+
+#include <stdio.h>
+#include <unistd.h>
+
+#define BINARY_TEXT_LONG "we have\ra lot \nof \0binary stuff in here\n" \
+"b adjig sadjg jasidgjiaehga3wht8a3w8ghxjc dsgad hasdghsd gasd ds" \
+"jdsoga sjdga0w3tjhawjgsertniq3n5oqerjqw2r89q23h awhrqh835r8a"
+#define BINARY_TEXT_LONG_BASE64 \
+"d2UgaGF2ZQ1hIGxvdCAKb2YgAGJpbmFyeSBzdHVmZiBpbiBoZXJlCmIgYWRqaWcgc2FkamcgamFz\r\n" \
+"aWRnamlhZWhnYTN3aHQ4YTN3OGdoeGpjIGRzZ2FkIGhhc2RnaHNkIGdhc2QgZHNqZHNvZ2Egc2pk\r\n" \
+"Z2EwdzN0amhhd2pnc2VydG5pcTNuNW9xZXJqcXcycjg5cTIzaCBhd2hycWg4MzVyOGE="
+
+#define BINARY_TEXT_SHORT "eh"
+#define BINARY_TEXT_SHORT_BASE64 "ZWg="
+
+static const char mail_input[] =
+"MIME-Version: 1.0\r\n"
+"Content-Type: multipart/alternative;\r\n boundary=\"bound\"\r\n"
+"\r\n"
+"mime header\r\n"
+"\r\n--bound\r\n"
+"Content-Transfer-Encoding: base64\r\n"
+"Content-Type: text/plain\r\n"
+"\r\n"
+BINARY_TEXT_LONG_BASE64
+"\r\n--bound\r\n"
+"Content-Type: text/plain\r\n"
+"Content-Transfer-Encoding: base64\r\n"
+"\r\n"
+BINARY_TEXT_SHORT_BASE64
+"\r\n--bound--\r\n";
+
+static const char mail_output[] =
+"MIME-Version: 1.0\r\n"
+"Content-Type: multipart/alternative;\r\n boundary=\"bound\"\r\n"
+"\r\n"
+"mime header\r\n"
+"\r\n--bound\r\n"
+"Content-Transfer-Encoding: base64\r\n"
+"Content-Type: text/plain\r\n"
+"\r\n"
+"\r\n--bound\r\n"
+"Content-Type: text/plain\r\n"
+"Content-Transfer-Encoding: base64\r\n"
+"\r\n"
+"\r\n--bound--\r\n";
+
+static const char *mail_broken_input_body_prefix =
+"MIME-Version: 1.0\r\n"
+"Content-Type: multipart/alternative;\r\n boundary=\"bound\"\r\n"
+"\r\n"
+"--bound\r\n"
+"Content-Transfer-Encoding: base64\r\n"
+"Content-Type: text/plain\r\n"
+"\r\n";
+
+static const char *mail_broken_input_bodies[] = {
+ /* broken base64 input */
+ "Zm9vCg=\n",
+ "Zm9vCg\n",
+ "Zm9vC\n",
+ /* extra whitespace */
+ "Zm9v\n Zm9v\n",
+ "Zm9v \nZm9v\n",
+ /* mixed LF vs CRLFs */
+ "Zm9vYmFy\r\nZm9vYmFy\n",
+ "Zm9vYmFy\nZm9vYmFy\r\n",
+ /* line length increases */
+ "Zm9v\nZm9vYmFy\n",
+ "Zm9v\nZm9vCg==",
+ "Zm9v\nZm9vYgo="
+};
+
+static const char *mail_nonbroken_input_bodies[] = {
+ /* suffixes with explicit '=' end */
+ "Zm9vCg==",
+ "Zm9vCg==\n",
+ "Zm9vCg==\r\n",
+ "Zm9vCg==\nfoo\n",
+ "Zm9vCg==\r\nfoo\n",
+ "Zm9vCg== \t\t\n\n",
+ /* suffixes with shorter line length */
+ "Zm9vYmFy\nZm9v\n",
+ "Zm9vYmFy\r\nZm9v\r\n",
+ "Zm9vYmFy\nZm9v\nfoo\n",
+ "Zm9vYmFy\r\nZm9v\r\nfoo\n",
+ "Zm9vYmFy\nZm9v\n \t\t\n\n",
+ /* suffixes with empty line */
+ "Zm9v\n\n",
+ "Zm9v\r\n\r\n",
+ "Zm9v\n\nfoo\n"
+ "Zm9v\r\n\nfoo\n"
+ "Zm9v\r\n\r\nfoo\n"
+#if 0
+ /* the whitespace here could be handled as suffixes, but for now
+ they're not: */
+ "Zm9v ",
+ "Zm9v \n"
+#endif
+};
+
+struct attachment {
+ size_t buffer_offset;
+ uoff_t start_offset;
+ uoff_t encoded_size, decoded_size;
+ unsigned int base64_blocks_per_line;
+};
+
+static buffer_t *attachment_data;
+static ARRAY(struct attachment) attachments;
+
+static int test_open_temp_fd(void *context ATTR_UNUSED)
+{
+ string_t *str = t_str_new(128);
+ int fd;
+
+ str_append(str, "/tmp/dovecot-test.");
+ fd = safe_mkstemp(str, 0600, (uid_t)-1, (gid_t)-1);
+ if (fd == -1)
+ i_fatal("safe_mkstemp(%s) failed: %m", str_c(str));
+ i_unlink(str_c(str));
+ return fd;
+}
+
+static int test_open_attachment_ostream(struct istream_attachment_info *info,
+ struct ostream **output_r,
+ const char **error_r ATTR_UNUSED,
+ void *context ATTR_UNUSED)
+{
+ struct attachment *a;
+
+ if (attachment_data == NULL)
+ attachment_data = buffer_create_dynamic(default_pool, 1024);
+ if (!array_is_created(&attachments))
+ i_array_init(&attachments, 8);
+ a = array_append_space(&attachments);
+ a->buffer_offset = attachment_data->used;
+ a->start_offset = info->start_offset;
+ a->encoded_size = info->encoded_size;
+ a->base64_blocks_per_line = info->base64_blocks_per_line;
+ test_assert(strlen(info->hash) == 160/8*2); /* sha1 size */
+
+ *output_r = o_stream_create_buffer(attachment_data);
+ if (o_stream_seek(*output_r, a->buffer_offset) < 0)
+ i_unreached();
+ return 0;
+}
+
+static int
+test_open_attachment_ostream_error(struct istream_attachment_info *info ATTR_UNUSED,
+ struct ostream **output_r ATTR_UNUSED,
+ const char **error_r,
+ void *context ATTR_UNUSED)
+{
+ *error_r = "test open error";
+ return -1;
+}
+
+static int test_close_attachment_ostream(struct ostream *output, bool success,
+ const char **error_r ATTR_UNUSED,
+ void *context ATTR_UNUSED)
+{
+ struct attachment *a;
+
+ i_assert(success);
+
+ a = array_back_modifiable(&attachments);
+ a->decoded_size = output->offset - a->buffer_offset;
+
+ if (o_stream_finish(output) < 0)
+ i_unreached();
+ o_stream_destroy(&output);
+ return 0;
+}
+
+static int
+test_close_attachment_ostream_error(struct ostream *output,
+ bool success, const char **error,
+ void *context ATTR_UNUSED)
+{
+ if (success)
+ *error = "test output error";
+ o_stream_abort(output);
+ o_stream_destroy(&output);
+ return -1;
+}
+
+static struct istream *
+test_build_original_istream(struct istream *base_input, uoff_t msg_size)
+{
+ struct istream_attachment_connector *conn;
+ const unsigned char *data = attachment_data->data;
+ const struct attachment *a;
+ struct istream *input;
+ uoff_t data_size = attachment_data->used;
+ const char *error;
+
+ conn = istream_attachment_connector_begin(base_input, msg_size);
+ array_foreach(&attachments, a) {
+ input = i_stream_create_from_data(data, a->decoded_size);
+ if (istream_attachment_connector_add(conn, input,
+ a->start_offset, a->encoded_size,
+ a->base64_blocks_per_line, TRUE, &error) < 0)
+ i_unreached();
+ i_stream_unref(&input);
+
+ i_assert(a->decoded_size <= data_size);
+ data += a->decoded_size;
+ data_size -= a->decoded_size;
+ }
+ i_assert(data_size == 0);
+ return istream_attachment_connector_finish(&conn);
+}
+
+static void
+get_istream_attachment_settings(struct istream_attachment_settings *set_r)
+{
+ const char *error;
+
+ i_zero(set_r);
+ set_r->min_size = 1;
+ set_r->drain_parent_input = TRUE;
+ set_r->open_temp_fd = test_open_temp_fd;
+ set_r->open_attachment_ostream = test_open_attachment_ostream;
+ set_r->close_attachment_ostream= test_close_attachment_ostream;
+ if (hash_format_init("%{sha1}", &set_r->hash_format, &error) < 0)
+ i_unreached();
+}
+
+static int test_input_stream(struct istream *file_input)
+{
+ struct istream_attachment_settings set;
+ struct istream *input, *input2;
+ const unsigned char *data;
+ size_t size;
+ struct sha1_ctxt hash;
+ uoff_t msg_size, orig_msg_size;
+ buffer_t *base_buf;
+ unsigned char hash_file[SHA1_RESULTLEN], hash_attached[SHA1_RESULTLEN];
+ int ret = 0;
+
+ /* get hash when directly reading input */
+ input = i_stream_create_crlf(file_input);
+ sha1_init(&hash);
+ while (i_stream_read_more(input, &data, &size) > 0) {
+ sha1_loop(&hash, data, size);
+ i_stream_skip(input, size);
+ }
+ sha1_result(&hash, hash_file);
+ msg_size = orig_msg_size = input->v_offset;
+ i_stream_unref(&input);
+
+ /* read through attachment extractor */
+ get_istream_attachment_settings(&set);
+
+ i_stream_seek(file_input, 0);
+ input = i_stream_create_crlf(file_input);
+ input2 = i_stream_create_attachment_extractor(input, &set, NULL);
+ i_stream_unref(&input);
+ base_buf = buffer_create_dynamic(default_pool, 1024);
+ while (i_stream_read_more(input2, &data, &size) > 0) {
+ buffer_append(base_buf, data, size);
+ i_stream_skip(input2, size);
+ }
+ i_stream_unref(&input2);
+
+ /* rebuild the original stream and see if the hash matches */
+ for (unsigned int i = 0; i < 2; i++) {
+ input2 = i_stream_create_from_data(base_buf->data, base_buf->used);
+ input = test_build_original_istream(input2, msg_size);
+ i_stream_unref(&input2);
+
+ sha1_init(&hash);
+ while (i_stream_read_more(input, &data, &size) > 0) {
+ sha1_loop(&hash, data, size);
+ i_stream_skip(input, size);
+ }
+ test_assert_idx(input->eof && input->stream_errno == 0, i);
+ sha1_result(&hash, hash_attached);
+ i_stream_unref(&input);
+
+ if (memcmp(hash_file, hash_attached, SHA1_RESULTLEN) != 0)
+ ret = -1;
+
+ /* try again without knowing the message's size */
+ msg_size = UOFF_T_MAX;
+ }
+
+ /* try with a wrong message size */
+ for (int i = 0; i < 2; i++) {
+ input2 = i_stream_create_from_data(base_buf->data, base_buf->used);
+ input = test_build_original_istream(input2,
+ i == 0 ? orig_msg_size + 1 : orig_msg_size - 1);
+ i_stream_unref(&input2);
+ while (i_stream_read_more(input, &data, &size) > 0)
+ i_stream_skip(input, size);
+ test_assert(input->stream_errno == (i == 0 ? EPIPE : EINVAL));
+ i_stream_unref(&input);
+ }
+
+ buffer_free(&base_buf);
+ buffer_free(&attachment_data);
+ if (array_is_created(&attachments))
+ array_free(&attachments);
+ return ret;
+}
+
+static void test_istream_attachment(void)
+{
+ struct istream_attachment_settings set;
+ struct istream *datainput, *input;
+ const unsigned char *data;
+ size_t i, size;
+ int ret;
+
+ test_begin("istream attachment");
+ datainput = test_istream_create_data(mail_input, sizeof(mail_input));
+ test_istream_set_allow_eof(datainput, FALSE);
+
+ get_istream_attachment_settings(&set);
+ input = i_stream_create_attachment_extractor(datainput, &set, NULL);
+
+ for (i = 1; i <= sizeof(mail_input); i++) {
+ test_istream_set_size(datainput, i);
+ while ((ret = i_stream_read(input)) > 0) ;
+ test_assert(ret == 0);
+ }
+ test_istream_set_allow_eof(datainput, TRUE);
+ while ((ret = i_stream_read(input)) > 0) ;
+ test_assert(ret == -1);
+
+ data = i_stream_get_data(input, &size);
+ test_assert(size == sizeof(mail_output) &&
+ memcmp(data, mail_output, size) == 0);
+
+ data = attachment_data->data;
+ test_assert(attachment_data->used ==
+ sizeof(BINARY_TEXT_LONG)-1 + strlen(BINARY_TEXT_SHORT));
+ test_assert(memcmp(data, BINARY_TEXT_LONG, sizeof(BINARY_TEXT_LONG)-1) == 0);
+ test_assert(memcmp(data + sizeof(BINARY_TEXT_LONG)-1,
+ BINARY_TEXT_SHORT, strlen(BINARY_TEXT_SHORT)) == 0);
+ i_stream_unref(&input);
+ i_stream_unref(&datainput);
+
+ buffer_free(&attachment_data);
+ if (array_is_created(&attachments))
+ array_free(&attachments);
+ test_end();
+}
+
+static bool test_istream_attachment_extractor_one(const char *body, int err_type)
+{
+ const size_t prefix_len = strlen(mail_broken_input_body_prefix);
+ struct istream_attachment_settings set;
+ struct istream *datainput, *input;
+ char *mail_text;
+ const unsigned char *data;
+ size_t size;
+ int ret;
+ bool unchanged;
+
+ mail_text = i_strconcat(mail_broken_input_body_prefix, body, NULL);
+ datainput = test_istream_create_data(mail_text, strlen(mail_text));
+
+ get_istream_attachment_settings(&set);
+ if (err_type == 1)
+ set.open_attachment_ostream = test_open_attachment_ostream_error;
+ else if (err_type == 2)
+ set.close_attachment_ostream = test_close_attachment_ostream_error;
+ input = i_stream_create_attachment_extractor(datainput, &set, NULL);
+
+ while ((ret = i_stream_read(input)) > 0) ;
+ if (err_type != 0) {
+ test_assert(ret == -1 && input->stream_errno == EIO);
+ unchanged = FALSE;
+ goto cleanup;
+ }
+ test_assert(ret == -1 && input->stream_errno == 0);
+
+ data = i_stream_get_data(input, &size);
+ i_assert(size >= prefix_len &&
+ memcmp(data, mail_broken_input_body_prefix, prefix_len) == 0);
+ data += prefix_len;
+ size -= prefix_len;
+
+ i_assert(attachment_data != NULL);
+ unchanged = attachment_data->used <= strlen(body) &&
+ memcmp(attachment_data->data, body, attachment_data->used) == 0 &&
+ strlen(body) - attachment_data->used == size &&
+ memcmp(data, body + attachment_data->used, size) == 0;
+
+cleanup:
+ buffer_free(&attachment_data);
+ if (array_is_created(&attachments))
+ array_free(&attachments);
+
+ i_stream_unref(&input);
+ i_stream_unref(&datainput);
+ i_free(mail_text);
+ return unchanged;
+}
+
+static void test_istream_attachment_extractor(void)
+{
+ unsigned int i;
+
+ test_begin("istream attachment extractor");
+ for (i = 0; i < N_ELEMENTS(mail_broken_input_bodies); i++)
+ test_assert(test_istream_attachment_extractor_one(mail_broken_input_bodies[i], 0));
+ for (i = 0; i < N_ELEMENTS(mail_nonbroken_input_bodies); i++)
+ test_assert(!test_istream_attachment_extractor_one(mail_nonbroken_input_bodies[i], 0));
+ test_end();
+}
+
+static void test_istream_attachment_extractor_error(void)
+{
+ unsigned int i;
+
+ test_begin("istream attachment extractor error");
+ for (int err_type = 1; err_type <= 2; err_type++) {
+ for (i = 0; i < N_ELEMENTS(mail_broken_input_bodies); i++)
+ test_istream_attachment_extractor_one(mail_broken_input_bodies[i], err_type);
+ for (i = 0; i < N_ELEMENTS(mail_nonbroken_input_bodies); i++)
+ test_istream_attachment_extractor_one(mail_nonbroken_input_bodies[i], err_type);
+ }
+ test_end();
+}
+
+static void test_istream_attachment_connector(void)
+{
+ struct istream *input;
+
+ test_begin("istream attachment connector");
+ input = i_stream_create_from_data(mail_input, sizeof(mail_input));
+ test_assert(test_input_stream(input) == 0);
+ i_stream_unref(&input);
+ test_end();
+}
+
+static int test_input_file(const char *path)
+{
+ struct istream *file_input;
+ int ret = 0;
+
+ lib_init();
+
+ file_input = i_stream_create_file(path, 64);
+ if (test_input_stream(file_input) < 0) {
+ fprintf(stderr, "istream-attachment-extractor: mismatch on file %s\n",
+ path);
+ ret = -1;
+ }
+ i_stream_unref(&file_input);
+
+ lib_deinit();
+ return ret;
+}
+
+int main(int argc, char *argv[])
+{
+ static void (*const test_functions[])(void) = {
+ test_istream_attachment,
+ test_istream_attachment_extractor,
+ test_istream_attachment_extractor_error,
+ test_istream_attachment_connector,
+ NULL
+ };
+ if (argc > 1)
+ return test_input_file(argv[1]) < 0 ? 1 : 0;
+ else
+ return test_run(test_functions);
+}
diff --git a/src/lib-mail/test-istream-binary-converter.c b/src/lib-mail/test-istream-binary-converter.c
new file mode 100644
index 0000000..c32dcb0
--- /dev/null
+++ b/src/lib-mail/test-istream-binary-converter.c
@@ -0,0 +1,215 @@
+/* Copyright (c) 2007-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "base64.h"
+#include "buffer.h"
+#include "str.h"
+#include "sha1.h"
+#include "istream.h"
+#include "istream-crlf.h"
+#include "istream-binary-converter.h"
+#include "test-common.h"
+
+#include <stdio.h>
+
+#define BINARY_TEXT_LONG "we have\ra lot \nof \0binary stuff in here\n" \
+"b adjig sadjg jasidgjiaehga3wht8a3w8ghxjc dsgad hasdghsd gasd ds" \
+"jdsoga sjdga0w3tjhawjgsertniq3n5oqerjqw2r89q23h awhrqh835r8a"
+#define BINARY_TEXT_LONG_BASE64 \
+"d2UgaGF2ZQ1hIGxvdCAKb2YgAGJpbmFyeSBzdHVmZiBpbiBoZXJlCmIgYWRqaWcgc2FkamcgamFz\r\n" \
+"aWRnamlhZWhnYTN3aHQ4YTN3OGdoeGpjIGRzZ2FkIGhhc2RnaHNkIGdhc2QgZHNqZHNvZ2Egc2pk\r\n" \
+"Z2EwdzN0amhhd2pnc2VydG5pcTNuNW9xZXJqcXcycjg5cTIzaCBhd2hycWg4MzVyOGE="
+
+#define BINARY_TEXT_SHORT "eh"
+#define BINARY_TEXT_SHORT_BASE64 "ZWg="
+
+static const char mail_input_mime[] =
+"MIME-Version: 1.0\r\n"
+"Content-Type: multipart/alternative;\r\n boundary=\"bound\"\r\n"
+"\r\n"
+"mime header\r\n"
+"\r\n--bound\r\n"
+"Content-Transfer-Encoding: binary\r\n"
+"Content-Type: text/plain\r\n"
+"\r\n"
+BINARY_TEXT_LONG
+"\r\n--bound\r\n"
+"Content-Type: text/plain\r\n"
+"Content-Transfer-Encoding: binary\r\n"
+"\r\n"
+BINARY_TEXT_SHORT
+"\n--bound\r\n"
+"Content-Type: text/plain\r\n"
+"\r\n"
+"hello world\r\n"
+"\r\n--bound--\r\n";
+
+static const char mail_output_mime[] =
+"MIME-Version: 1.0\r\n"
+"Content-Type: multipart/alternative;\r\n boundary=\"bound\"\r\n"
+"\r\n"
+"mime header\r\n"
+"\r\n--bound\r\n"
+"Content-Transfer-Encoding: base64\r\n"
+"Content-Type: text/plain\r\n"
+"\r\n"
+BINARY_TEXT_LONG_BASE64
+"\r\n--bound\r\n"
+"Content-Type: text/plain\r\n"
+"Content-Transfer-Encoding: base64\r\n"
+"\r\n"
+BINARY_TEXT_SHORT_BASE64
+"\n--bound\r\n"
+"Content-Type: text/plain\r\n"
+"\r\n"
+"hello world\r\n"
+"\r\n--bound--\r\n";
+
+static const char mail_input_root_hdr[] =
+"MIME-Version: 1.0\r\n"
+"Content-Transfer-Encoding: binary\r\n"
+"Content-Type: text/plain\r\n"
+"\r\n";
+
+static const char mail_output_root_hdr[] =
+"MIME-Version: 1.0\r\n"
+"Content-Transfer-Encoding: base64\r\n"
+"Content-Type: text/plain\r\n"
+"\r\n";
+
+static const char mail_root_nonbinary[] =
+"MIME-Version: 1.0\r\n"
+"Content-Type: text/plain\r\n"
+"\r\n"
+"hello\n\n";
+
+static void
+test_istream_binary_converter_test(const char *mail_input, unsigned int mail_input_len,
+ const char *mail_output, unsigned int mail_output_len,
+ unsigned int idx)
+{
+ struct istream *datainput, *input;
+ const unsigned char *data;
+ size_t i, size;
+ int ret;
+
+ datainput = test_istream_create_data(mail_input, mail_input_len);
+ test_istream_set_allow_eof(datainput, FALSE);
+ input = i_stream_create_binary_converter(datainput);
+
+ for (i = 1; i <= mail_input_len; i++) {
+ test_istream_set_size(datainput, i);
+ while ((ret = i_stream_read(input)) > 0) ;
+ test_assert_idx(ret == 0, idx);
+ }
+ test_istream_set_allow_eof(datainput, TRUE);
+ while ((ret = i_stream_read(input)) > 0) ;
+ test_assert_idx(ret == -1, idx);
+
+ data = i_stream_get_data(input, &size);
+ test_assert_idx(size == mail_output_len &&
+ memcmp(data, mail_output, size) == 0, idx);
+ i_stream_unref(&input);
+ i_stream_unref(&datainput);
+}
+
+static void test_istream_binary_converter_mime(void)
+{
+ test_begin("istream binary converter in mime parts");
+ test_istream_binary_converter_test(mail_input_mime, sizeof(mail_input_mime)-1,
+ mail_output_mime, sizeof(mail_output_mime)-1, 0);
+ test_end();
+}
+
+static void test_istream_binary_converter_root(void)
+{
+ buffer_t *inbuf = t_buffer_create(512);
+ buffer_t *outbuf = t_buffer_create(512);
+ const char *const suffixes[] = { "\n", "\r\n", "\n\r\n\n\n" };
+ unsigned int i;
+ unsigned int input_hdr_len = sizeof(mail_input_root_hdr)-1;
+
+ test_begin("istream binary converter in root");
+ buffer_append(inbuf, mail_input_root_hdr, input_hdr_len);
+ buffer_append(outbuf, mail_output_root_hdr, sizeof(mail_output_root_hdr)-1);
+ for (i = 0; i < N_ELEMENTS(suffixes); i++) {
+ buffer_set_used_size(inbuf, input_hdr_len);
+ buffer_set_used_size(outbuf, sizeof(mail_output_root_hdr)-1);
+ buffer_append(inbuf, BINARY_TEXT_SHORT, sizeof(BINARY_TEXT_SHORT)-1);
+ buffer_append(inbuf, suffixes[i], strlen(suffixes[i]));
+ base64_encode(CONST_PTR_OFFSET(inbuf->data, input_hdr_len),
+ inbuf->used - input_hdr_len, outbuf);
+ test_istream_binary_converter_test(inbuf->data, inbuf->used,
+ outbuf->data, outbuf->used, i);
+ }
+ test_end();
+}
+
+static void test_istream_binary_converter_root_nonbinary(void)
+{
+ test_begin("istream binary converter in root having non-binary");
+ test_istream_binary_converter_test(mail_root_nonbinary, sizeof(mail_root_nonbinary)-1,
+ mail_root_nonbinary, sizeof(mail_root_nonbinary)-1, 0);
+ test_end();
+}
+
+static int test_input_file(const char *path)
+{
+ struct istream *file_input, *input, *input2;
+ const unsigned char *data;
+ size_t size;
+ struct sha1_ctxt hash;
+ unsigned char hash_file[SHA1_RESULTLEN], hash_converter[SHA1_RESULTLEN];
+ int ret = 0;
+
+ lib_init();
+
+ file_input = i_stream_create_file(path, 64);
+
+ /* get hash when directly reading input */
+ input = i_stream_create_crlf(file_input);
+ sha1_init(&hash);
+ while (i_stream_read_more(input, &data, &size) > 0) {
+ sha1_loop(&hash, data, size);
+ i_stream_skip(input, size);
+ }
+ sha1_result(&hash, hash_file);
+ i_stream_unref(&input);
+
+ /* get hash when going through converter */
+ i_stream_seek(file_input, 0);
+ input = i_stream_create_crlf(file_input);
+ input2 = i_stream_create_binary_converter(input);
+ sha1_init(&hash);
+ while (i_stream_read_more(input2, &data, &size) > 0) {
+ sha1_loop(&hash, data, size);
+ i_stream_skip(input2, size);
+ }
+ sha1_result(&hash, hash_converter);
+ i_stream_unref(&input2);
+ i_stream_unref(&input);
+
+ if (memcmp(hash_file, hash_converter, SHA1_RESULTLEN) != 0) {
+ fprintf(stderr, "istream-binary-converter: mismatch on file %s\n",
+ path);
+ ret = 1;
+ }
+
+ i_stream_unref(&file_input);
+ lib_deinit();
+ return ret;
+}
+
+int main(int argc, char *argv[])
+{
+ static void (*const test_functions[])(void) = {
+ test_istream_binary_converter_mime,
+ test_istream_binary_converter_root,
+ test_istream_binary_converter_root_nonbinary,
+ NULL
+ };
+ if (argc > 1)
+ return test_input_file(argv[1]);
+ else
+ return test_run(test_functions);
+}
diff --git a/src/lib-mail/test-istream-dot.c b/src/lib-mail/test-istream-dot.c
new file mode 100644
index 0000000..b77f364
--- /dev/null
+++ b/src/lib-mail/test-istream-dot.c
@@ -0,0 +1,230 @@
+/* Copyright (c) 2007-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "str.h"
+#include "istream.h"
+#include "istream-dot.h"
+#include "test-common.h"
+
+struct dot_test {
+ const char *input;
+ const char *output;
+ const char *parent_input;
+};
+
+static void test_istream_dot_one(const struct dot_test *test,
+ bool send_last_lf, bool test_bufsize)
+{
+ struct istream *test_input, *input;
+ const unsigned char *data;
+ size_t size;
+ unsigned int i;
+ size_t outsize, input_len, output_len;
+ string_t *str;
+ uoff_t offset;
+ int ret;
+
+ test_input = test_istream_create(test->input);
+ input = i_stream_create_dot(test_input, send_last_lf);
+
+ input_len = strlen(test->input);
+ output_len = strlen(test->output);
+ if (!send_last_lf &&
+ (test->input[input_len-1] == '\n' ||
+ strstr(test->input, "\n.\n") != NULL ||
+ strstr(test->input, "\n.\r\n") != NULL)) {
+ if (output_len > 0 &&
+ test->output[output_len-1] == '\n') {
+ output_len--;
+ if (output_len > 0 &&
+ test->output[output_len-1] == '\r')
+ output_len--;
+ }
+ }
+
+ str = t_str_new(256);
+ if (!test_bufsize) {
+ outsize = 1; i = 0;
+ i_stream_set_max_buffer_size(input, outsize);
+ test_istream_set_size(test_input, 1);
+ while ((ret = i_stream_read(input)) != -1) {
+ switch (ret) {
+ case -2:
+ i_stream_set_max_buffer_size(input, ++outsize);
+ offset = test_input->v_offset;
+ /* seek one byte backwards so stream gets
+ reset */
+ i_stream_seek(test_input, offset - 1);
+ /* go back to original position */
+ test_istream_set_size(test_input, offset);
+ i_stream_skip(test_input, 1);
+ /* and finally allow reading one more byte */
+ test_istream_set_size(test_input, offset + 1);
+ break;
+ case 0:
+ test_istream_set_size(test_input, ++i);
+ break;
+ default:
+ test_assert(ret > 0);
+
+ data = i_stream_get_data(input, &size);
+ str_append_data(str, data, size);
+ i_stream_skip(input, size);
+ }
+ }
+ test_istream_set_size(test_input, input_len);
+ (void)i_stream_read(test_input);
+ } else {
+ test_istream_set_size(test_input, input_len);
+ size = 0;
+ for (i = 1; i < output_len; i++) {
+ i_stream_set_max_buffer_size(input, i);
+ test_assert(i_stream_read(input) == 1);
+ test_assert(i_stream_read(input) == -2);
+ data = i_stream_get_data(input, &size);
+ test_assert(memcmp(data, test->output, size) == 0);
+ }
+ i_stream_set_max_buffer_size(input, i+2);
+ if (size < output_len)
+ test_assert(i_stream_read(input) == 1);
+ test_assert(i_stream_read(input) == -1);
+
+ data = i_stream_get_data(input, &size);
+ if (size > 0)
+ str_append_data(str, data, size);
+ }
+ test_assert(input->stream_errno == 0);
+ test_assert(str_len(str) == output_len);
+ test_assert(memcmp(str_data(str), test->output, output_len) == 0);
+
+ /* read the data after the '.' line and verify it's still there */
+ i_stream_set_max_buffer_size(test_input, SIZE_MAX);
+ (void)i_stream_read(test_input);
+ data = i_stream_get_data(test_input, &size);
+ test_assert(size == strlen(test->parent_input));
+ if (size > 0)
+ test_assert(memcmp(data, test->parent_input, size) == 0);
+
+ i_stream_unref(&test_input);
+ i_stream_unref(&input);
+}
+
+static void test_istream_dot_error(const char *input_str, bool test_bufsize)
+{
+ struct istream *test_input, *input;
+ unsigned int i;
+ size_t outsize, input_len;
+ uoff_t offset;
+ int ret;
+
+ test_input = test_istream_create(input_str);
+ input = i_stream_create_dot(test_input, FALSE);
+
+ input_len = strlen(input_str);
+
+ if (!test_bufsize) {
+ outsize = 1; i = 0;
+ i_stream_set_max_buffer_size(input, outsize);
+ test_istream_set_size(test_input, 1);
+ while ((ret = i_stream_read(input)) != -1) {
+ switch (ret) {
+ case -2:
+ i_stream_set_max_buffer_size(input, ++outsize);
+ offset = test_input->v_offset;
+ /* seek one byte backwards so stream gets
+ reset */
+ i_stream_seek(test_input, offset - 1);
+ /* go back to original position */
+ test_istream_set_size(test_input, offset);
+ i_stream_skip(test_input, 1);
+ /* and finally allow reading one more byte */
+ test_istream_set_size(test_input, offset + 1);
+ break;
+ case 0:
+ test_istream_set_size(test_input, ++i);
+ break;
+ default:
+ test_assert(ret > 0);
+ }
+ }
+ test_istream_set_size(test_input, input_len);
+ (void)i_stream_read(test_input);
+ } else {
+ test_istream_set_size(test_input, input_len);
+ for (i = 1; i <= input_len; i++) {
+ i_stream_set_max_buffer_size(input, i);
+ (void)i_stream_read(input);
+ (void)i_stream_read(input);
+ }
+ i_stream_set_max_buffer_size(input, i+1);
+ (void)i_stream_read(input);
+ }
+ test_assert(input->stream_errno == EPIPE);
+
+ i_stream_unref(&test_input);
+ i_stream_unref(&input);
+}
+
+static void test_istream_dot(void)
+{
+ static struct dot_test tests[] = {
+ { "..foo\n..\n.foo\n.\nfoo", ".foo\n.\nfoo\n", "foo" },
+ { "..foo\r\n..\r\n.foo\r\n.\r\nfoo", ".foo\r\n.\r\nfoo\r\n", "foo" },
+ { "\r.\r\n.\r\n", "\r.\r\n", "" },
+ { "\n\r.\r\r\n.\r\n", "\n\r.\r\r\n", "" },
+ { "\r\n.\rfoo\n.\n", "\r\n\rfoo\n", "" },
+ { "\r\n.\r\n", "\r\n", "" },
+ { "\n.\r\n", "\n", "" },
+ { "\n.\n", "\n", "" },
+ { ".\r\n", "", "" },
+ { ".\n", "", "" }
+ };
+ static const char *error_tests[] = {
+ "",
+ ".",
+ "..",
+ ".\r",
+ ".\rx",
+ "..\r\n",
+ "\r.",
+ "\r.\r",
+ "\r.\rx",
+ "\r.\r\n",
+ "\r.\n",
+ "\r..\n",
+ "\r\n",
+ "\r\n.",
+ "\r\n.\r",
+ "\r\n.\rx",
+ "\r\n.\rx\n",
+ "\r\n..\r\n",
+ "\n",
+ "\n.",
+ "\n.\r",
+ "\n.\rx",
+ "\n..\r\n"
+ };
+ unsigned int i;
+
+ test_begin("dot istream");
+ for (i = 0; i < N_ELEMENTS(tests); i++) {
+ test_istream_dot_one(&tests[i], TRUE, TRUE);
+ test_istream_dot_one(&tests[i], TRUE, FALSE);
+ test_istream_dot_one(&tests[i], FALSE, TRUE);
+ test_istream_dot_one(&tests[i], FALSE, FALSE);
+ }
+ for (i = 0; i < N_ELEMENTS(error_tests); i++) {
+ test_istream_dot_error(error_tests[i], FALSE);
+ test_istream_dot_error(error_tests[i], TRUE);
+ }
+ test_end();
+}
+
+int main(void)
+{
+ static void (*const test_functions[])(void) = {
+ test_istream_dot,
+ NULL
+ };
+ return test_run(test_functions);
+}
diff --git a/src/lib-mail/test-istream-header-filter.c b/src/lib-mail/test-istream-header-filter.c
new file mode 100644
index 0000000..f982c02
--- /dev/null
+++ b/src/lib-mail/test-istream-header-filter.c
@@ -0,0 +1,701 @@
+/* Copyright (c) 2007-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "str.h"
+#include "istream.h"
+#include "message-header-parser.h"
+#include "istream-header-filter.h"
+#include "test-common.h"
+
+struct run_ctx {
+ header_filter_callback *callback;
+ unsigned int callback_call_count;
+ bool null_hdr_seen;
+ bool eoh_seen;
+ bool callback_called;
+};
+
+static void run_callback(struct header_filter_istream *input,
+ struct message_header_line *hdr,
+ bool *matched, struct run_ctx *ctx)
+{
+ i_assert(!ctx->null_hdr_seen);
+
+ ctx->callback_call_count++;
+ if (hdr == NULL)
+ ctx->null_hdr_seen = TRUE;
+ else {
+ i_assert(!ctx->eoh_seen);
+ if (hdr->eoh)
+ ctx->eoh_seen = TRUE;
+ }
+ if (ctx->callback != NULL)
+ ctx->callback(input, hdr, matched, NULL);
+ ctx->callback_called = TRUE;
+}
+
+static inline void
+test_istream_run_prep(struct run_ctx *run_ctx,
+ header_filter_callback *callback)
+{
+ i_zero(run_ctx);
+ run_ctx->callback = callback;
+ run_ctx->null_hdr_seen = FALSE;
+ run_ctx->eoh_seen = FALSE;
+ run_ctx->callback_called = FALSE;
+}
+
+static void
+test_istream_run_check(struct run_ctx *run_ctx,
+ struct istream *filter,
+ const char *output,
+ enum header_filter_flags flags,
+ bool first,
+ size_t *size_r)
+{
+ const unsigned char *data;
+ const struct stat *st;
+
+ if (first)
+ test_assert(run_ctx->null_hdr_seen);
+ else
+ test_assert(run_ctx->null_hdr_seen == run_ctx->callback_called);
+
+ if (first && ((flags & HEADER_FILTER_ADD_MISSING_EOH) != 0))
+ test_assert(run_ctx->eoh_seen);
+
+ data = i_stream_get_data(filter, size_r);
+ test_assert(*size_r == strlen(output) &&
+ memcmp(data, output, *size_r) == 0);
+
+ test_assert(i_stream_stat(filter, TRUE, &st) == 0 &&
+ (uoff_t)st->st_size == *size_r);
+
+ /* make sure buffer doesn't change when returning -1 */
+ i_stream_skip(filter, 1);
+ test_assert(i_stream_read(filter) == -1);
+ test_assert(memcmp(data, output, *size_r) == 0);
+}
+
+static void
+test_istream_run(struct istream *test_istream,
+ unsigned int input_len, const char *output,
+ enum header_filter_flags flags,
+ header_filter_callback *callback)
+{
+ struct run_ctx run_ctx;
+ struct istream *filter;
+ unsigned int i, orig_callback_call_count;
+ size_t size;
+
+ test_istream_run_prep(&run_ctx, callback);
+
+ filter = i_stream_create_header_filter(test_istream, flags, NULL, 0,
+ run_callback, &run_ctx);
+
+ for (i = 1; i < input_len; i++) {
+ test_istream_set_size(test_istream, i);
+ test_assert(i_stream_read(filter) >= 0);
+ }
+ test_istream_set_size(test_istream, input_len);
+ test_assert(i_stream_read(filter) > 0);
+ test_assert(i_stream_read(filter) == -1);
+
+ test_istream_run_check(&run_ctx, filter, output, flags, TRUE, &size);
+ orig_callback_call_count = run_ctx.callback_call_count;
+
+ /* run again to make sure it's still correct the second time */
+ test_istream_run_prep(&run_ctx, callback);
+
+ i_stream_skip(filter, size);
+ i_stream_seek(filter, 0);
+ while (i_stream_read(filter) > 0) ;
+ test_istream_run_check(&run_ctx, filter, output, flags, FALSE, &size);
+ test_assert(run_ctx.callback_call_count == 0 ||
+ run_ctx.callback_call_count == orig_callback_call_count);
+
+ i_stream_unref(&filter);
+}
+
+static void ATTR_NULL(3)
+filter_callback(struct header_filter_istream *input ATTR_UNUSED,
+ struct message_header_line *hdr,
+ bool *matched, void *context ATTR_UNUSED)
+{
+ if (hdr != NULL && (hdr->name_offset == 0 ||
+ strcmp(hdr->name, "X-Drop") == 0)) {
+ /* drop 1) first header, 2) X-Drop header */
+ *matched = TRUE;
+ }
+}
+
+static void test_istream_filter(void)
+{
+ static const char *exclude_headers[] = { "Subject", "To" };
+ const char *input = "From: foo\nFrom: abc\nTo: bar\nSubject: plop\nX-Drop: 1\n\nhello world\n";
+ const char *output = "From: abc\n\nhello world\n";
+ struct istream *istream, *filter, *filter2;
+ unsigned int i;
+ size_t input_len = strlen(input);
+ size_t output_len = strlen(output);
+ const unsigned char *data;
+ const struct stat *st;
+ size_t size;
+
+ test_begin("i_stream_create_header_filter: exclude");
+ istream = test_istream_create(input);
+ filter = i_stream_create_header_filter(istream,
+ HEADER_FILTER_EXCLUDE |
+ HEADER_FILTER_NO_CR,
+ exclude_headers,
+ N_ELEMENTS(exclude_headers),
+ filter_callback, NULL);
+ filter2 = i_stream_create_header_filter(filter,
+ HEADER_FILTER_EXCLUDE |
+ HEADER_FILTER_NO_CR,
+ exclude_headers,
+ N_ELEMENTS(exclude_headers),
+ *null_header_filter_callback,
+ NULL);
+ i_stream_unref(&filter);
+ filter = filter2;
+
+ for (i = 1; i < input_len; i++) {
+ test_istream_set_size(istream, i);
+ test_assert(i_stream_read(filter) >= 0);
+ }
+ test_istream_set_size(istream, input_len);
+ test_assert(i_stream_read(filter) > 0);
+ test_assert(i_stream_read(filter) == -1);
+
+ data = i_stream_get_data(filter, &size);
+ test_assert(size == output_len && memcmp(data, output, size) == 0);
+
+ test_assert(i_stream_stat(filter, TRUE, &st) == 0 &&
+ (uoff_t)st->st_size == size);
+
+ i_stream_skip(filter, size);
+ i_stream_seek(filter, 0);
+ while (i_stream_read(filter) > 0) ;
+ data = i_stream_get_data(filter, &size);
+ test_assert(size == output_len && memcmp(data, output, size) == 0);
+ test_assert(i_stream_stat(filter, TRUE, &st) == 0 &&
+ (uoff_t)st->st_size == size);
+
+ i_stream_unref(&filter);
+ i_stream_unref(&istream);
+
+ test_end();
+}
+
+static void add_random_text(string_t *dest, unsigned int count)
+{
+ unsigned int i;
+
+ for (i = 0; i < count; i++)
+ str_append_c(dest, i_rand_minmax('a', 'z'));
+}
+
+static void ATTR_NULL(3)
+filter2_callback(struct header_filter_istream *input ATTR_UNUSED,
+ struct message_header_line *hdr,
+ bool *matched, bool *null_hdr_seen)
+{
+ if (hdr == NULL)
+ *null_hdr_seen = TRUE;
+ else if (strcmp(hdr->name, "To") == 0)
+ *matched = TRUE;
+}
+
+static void test_istream_filter_large_buffer(void)
+{
+ string_t *input, *output;
+ struct istream *istream, *filter;
+ const struct stat *st;
+ const unsigned char *data;
+ size_t size, prefix_len;
+ const char *p;
+ unsigned int i;
+ bool null_hdr_seen = FALSE;
+
+ test_begin("i_stream_create_header_filter: large buffer");
+
+ input = str_new(default_pool, 1024*128);
+ output = str_new(default_pool, 1024*128);
+ str_append(input, "From: ");
+ add_random_text(input, 1024*31);
+ str_append(input, "\nTo: ");
+ add_random_text(input, 1024*32);
+ str_append(input, "\nSubject: ");
+ add_random_text(input, 1024*34);
+ str_append(input, "\n\nbody\n");
+
+ istream = test_istream_create_data(str_data(input), str_len(input));
+ test_istream_set_max_buffer_size(istream, 8192);
+
+ filter = i_stream_create_header_filter(istream,
+ HEADER_FILTER_EXCLUDE |
+ HEADER_FILTER_NO_CR,
+ NULL, 0,
+ filter2_callback,
+ &null_hdr_seen);
+
+ for (i = 0; i < 2; i++) {
+ for (;;) {
+ ssize_t ret = i_stream_read(filter);
+ i_assert(ret != 0);
+ if (ret == -1)
+ break;
+ if (ret == -2) {
+ unsigned char orig_data[128];
+ size_t orig_data_size;
+
+ data = i_stream_get_data(filter, &size);
+ orig_data_size = I_MIN(sizeof(orig_data), size);
+ memcpy(orig_data, data, orig_data_size);
+
+ /* skip only a bit */
+ size = I_MIN(size, 1);
+ str_append_data(output, data, size);
+ i_stream_skip(filter, size);
+
+ /* do another read */
+ ret = i_stream_read(filter);
+ i_assert(ret == -2 || ret > 0);
+ /* make sure the old data pointer is still
+ usable if -2 is returned */
+ if (ret != -2)
+ data = i_stream_get_data(filter, &size);
+ else {
+ test_assert(memcmp(data, orig_data, orig_data_size) == 0);
+ data = i_stream_get_data(filter, &size);
+ }
+ str_append_data(output, data, size);
+ i_stream_skip(filter, size);
+ }
+ }
+ /* callbacks are called only once */
+ test_assert(null_hdr_seen == (i == 0));
+
+ data = i_stream_get_data(filter, &size);
+ test_assert(size <= 8192);
+ str_append_data(output, data, size);
+
+ p = strstr(str_c(input), "To: ");
+ i_assert(p != NULL);
+ prefix_len = p - str_c(input);
+ test_assert(strncmp(str_c(input), str_c(output), prefix_len) == 0);
+
+ p = strchr(p, '\n');
+ i_assert(p != NULL);
+ test_assert(strcmp(p+1, str_c(output) + prefix_len) == 0);
+
+ test_assert(i_stream_stat(filter, TRUE, &st) == 0 &&
+ (uoff_t)st->st_size == filter->v_offset + size);
+
+ /* seek back and retry once with caching and different
+ buffer size */
+ i_stream_seek(filter, 0);
+ str_truncate(output, 0);
+ test_istream_set_max_buffer_size(istream, 4096);
+ null_hdr_seen = FALSE;
+ }
+
+ str_free(&input);
+ str_free(&output);
+ i_stream_unref(&filter);
+ i_stream_unref(&istream);
+
+ test_end();
+}
+
+static void test_istream_filter_large_buffer2(void)
+{
+ static const char *wanted_headers[] = { "References" };
+ string_t *input, *output;
+ struct istream *istream, *filter;
+ const struct stat *st;
+ const unsigned char *data;
+ size_t size;
+ unsigned int i;
+ int ret;
+
+ test_begin("i_stream_create_header_filter: large buffer2");
+
+ input = str_new(default_pool, 1024*128);
+ output = str_new(default_pool, 1024*128);
+ str_append(input, "References: ");
+ add_random_text(input, 1024*64);
+ str_append(input, "\r\n\r\n");
+
+ istream = test_istream_create_data(str_data(input), str_len(input));
+ test_istream_set_max_buffer_size(istream, 8192);
+
+ filter = i_stream_create_header_filter(istream,
+ HEADER_FILTER_INCLUDE | HEADER_FILTER_HIDE_BODY,
+ wanted_headers, N_ELEMENTS(wanted_headers),
+ *null_header_filter_callback, NULL);
+
+ for (i = 0; i < 2; i++) {
+ while ((ret = i_stream_read_more(filter, &data, &size)) > 0) {
+ str_append_data(output, data, size);
+ i_stream_skip(filter, size);
+ }
+ test_assert(ret == -1);
+ test_assert(filter->stream_errno == 0);
+
+ test_assert(strcmp(str_c(input), str_c(output)) == 0);
+ test_assert(i_stream_stat(filter, TRUE, &st) == 0 &&
+ (uoff_t)st->st_size == filter->v_offset + size);
+
+ /* seek back and retry once with caching and different
+ buffer size */
+ i_stream_seek(filter, 0);
+ str_truncate(output, 0);
+ test_istream_set_max_buffer_size(istream, 4096);
+ }
+
+ str_free(&input);
+ str_free(&output);
+ i_stream_unref(&filter);
+ i_stream_unref(&istream);
+
+ test_end();
+}
+
+static void
+filter3_callback(struct header_filter_istream *input ATTR_UNUSED,
+ struct message_header_line *hdr,
+ bool *matched ATTR_UNUSED, string_t *dest)
+{
+ if (hdr != NULL)
+ message_header_line_write(dest, hdr);
+}
+
+static void test_istream_callbacks(void)
+{
+ string_t *input, *output;
+ const struct stat *st;
+ struct istream *istream, *filter;
+ unsigned int i;
+
+ test_begin("i_stream_create_header_filter: callbacks");
+
+ input = str_new(default_pool, 1024*128);
+ output = str_new(default_pool, 1024*128);
+ str_append(input, "From: first line\n ");
+ add_random_text(input, 1024*31);
+ str_append(input, "\nTo: first line\n\tsecond line\n\t");
+ add_random_text(input, 1024*32);
+ str_append(input, "\n last line\nSubject: ");
+ add_random_text(input, 1024*34);
+ str_append(input, "\n");
+
+ istream = test_istream_create_data(str_data(input), str_len(input));
+ test_istream_set_max_buffer_size(istream, 8192);
+
+ filter = i_stream_create_header_filter(istream,
+ HEADER_FILTER_EXCLUDE |
+ HEADER_FILTER_NO_CR,
+ NULL, 0,
+ filter3_callback,
+ output);
+
+ /* callback should be called exactly once for all the header input */
+ for (i = 0; i < 2; i++) {
+ while (i_stream_read(filter) != -1)
+ i_stream_skip(filter, i_stream_get_data_size(filter));
+ }
+
+ test_assert(i_stream_stat(filter, TRUE, &st) == 0 &&
+ (uoff_t)st->st_size == str_len(output));
+ test_assert(strcmp(str_c(output), str_c(input)) == 0);
+ str_free(&input);
+ str_free(&output);
+ i_stream_unref(&filter);
+ i_stream_unref(&istream);
+
+ test_end();
+}
+
+static void ATTR_NULL(3)
+edit_callback(struct header_filter_istream *input,
+ struct message_header_line *hdr,
+ bool *matched, void *context ATTR_UNUSED)
+{
+ if (hdr == NULL)
+ return;
+ if (hdr->eoh) {
+ /* add a new header */
+ const char *new_hdr = "Added: header\n\n";
+ i_stream_header_filter_add(input, new_hdr, strlen(new_hdr));
+ *matched = TRUE;
+ } else if (strcasecmp(hdr->name, "To") == 0) {
+ /* modify To header */
+ const char *new_to = "To: 123\n";
+ *matched = TRUE;
+ i_stream_header_filter_add(input, new_to, strlen(new_to));
+ }
+}
+
+static void test_istream_edit(void)
+{
+ const char *input = "From: foo\nTo: bar\n\nhello world\n";
+ const char *output = "From: foo\nTo: 123\nAdded: header\n\nhello world\n";
+ struct istream *istream;
+
+ test_begin("i_stream_create_header_filter: edit headers");
+ istream = test_istream_create(input);
+ test_istream_run(istream, strlen(input), output,
+ HEADER_FILTER_EXCLUDE |
+ HEADER_FILTER_NO_CR,
+ edit_callback);
+ i_stream_unref(&istream);
+
+ test_end();
+}
+
+static void test_istream_end_body_with_lf(void)
+{
+ const char *input = "From: foo\n\nhello world";
+ const char *output = "From: foo\n\nhello world\n";
+ const struct stat *st;
+ struct istream *istream, *filter;
+ unsigned int i;
+ size_t input_len = strlen(input);
+ size_t output_len = strlen(output);
+ const unsigned char *data;
+ string_t *str = t_str_new(64);
+ size_t size;
+
+ test_begin("i_stream_create_header_filter: end_body_with_lf");
+ istream = test_istream_create(input);
+ filter = i_stream_create_header_filter(istream,
+ HEADER_FILTER_EXCLUDE |
+ HEADER_FILTER_NO_CR |
+ HEADER_FILTER_END_BODY_WITH_LF,
+ NULL, 0,
+ *null_header_filter_callback,
+ NULL);
+
+ for (i = 1; i < input_len; i++) {
+ test_istream_set_size(istream, i);
+ test_assert(i_stream_read(filter) >= 0);
+ }
+ test_istream_set_size(istream, input_len);
+ test_assert(i_stream_read(filter) > 0);
+ test_assert(i_stream_read(filter) > 0);
+ test_assert(i_stream_read(filter) == -1);
+
+ data = i_stream_get_data(filter, &size);
+ test_assert(size == output_len && memcmp(data, output, size) == 0);
+ test_assert(i_stream_stat(filter, TRUE, &st) == 0 &&
+ (uoff_t)st->st_size == filter->v_offset + size);
+
+ i_stream_skip(filter, size);
+ i_stream_seek(filter, 0);
+ for (i = 1; i < input_len; i++) {
+ test_istream_set_size(istream, i);
+ test_assert(i_stream_read(filter) >= 0);
+
+ data = i_stream_get_data(filter, &size);
+ if (size > 0)
+ str_append_data(str, data, size);
+ i_stream_skip(filter, size);
+ }
+ test_istream_set_size(istream, input_len);
+ test_assert(i_stream_read(filter) == 1);
+ test_assert(i_stream_read(filter) == 1);
+ test_assert(i_stream_read(filter) == -1);
+
+ data = i_stream_get_data(filter, &size);
+ str_append_data(str, data, size);
+ test_assert(strcmp(str_c(str), output) == 0);
+
+ i_stream_unref(&filter);
+ i_stream_unref(&istream);
+
+ test_end();
+}
+
+static void test_istream_add_missing_eoh(void)
+{
+ static const struct {
+ const char *input;
+ const char *output;
+ unsigned int extra;
+ } tests[] = {
+ { "", "\n", 0 },
+ { "From: foo", "From: foo\n\n", 1 },
+ { "From: foo\n", "From: foo\n\n", 1 },
+ { "From: foo\n\n", "From: foo\n\n", 1 },
+ { "From: foo\n\nbar", "From: foo\n\nbar", 0 },
+ { "From: foo\r\n", "From: foo\r\n\r\n", 1 },
+ { "From: foo\r\n\r\n", "From: foo\r\n\r\n", 0 },
+ { "From: foo\r\n\r\nbar", "From: foo\r\n\r\nbar", 0 }
+ };
+ struct istream *istream;
+ unsigned int i;
+
+ test_begin("i_stream_create_header_filter: add missing EOH");
+ for (i = 0; i < N_ELEMENTS(tests); i++) {
+ istream = test_istream_create(tests[i].input);
+ test_istream_run(istream,
+ strlen(tests[i].input) + tests[i].extra,
+ tests[i].output,
+ HEADER_FILTER_EXCLUDE |
+ HEADER_FILTER_CRLF_PRESERVE |
+ HEADER_FILTER_ADD_MISSING_EOH,
+ *null_header_filter_callback);
+ i_stream_unref(&istream);
+ }
+ test_end();
+}
+
+static void test_istream_add_missing_eoh_and_edit(void)
+{
+ static const struct {
+ const char *input;
+ const char *output;
+ } tests[] = {
+ { "From: foo\nTo: bar\n",
+ "From: foo\nTo: 123\nAdded: header\n\n" },
+ { "From: foo\nTo: bar\n\n",
+ "From: foo\nTo: 123\nAdded: header\n\n" },
+ { "From: foo\nTo: bar\n\nbody\n",
+ "From: foo\nTo: 123\nAdded: header\n\nbody\n" },
+ };
+ struct istream *istream;
+ unsigned int i;
+
+ test_begin("i_stream_create_header_filter: add missing EOH and edit headers");
+ for (i = 0; i < N_ELEMENTS(tests); i++) {
+ istream = test_istream_create(tests[i].input);
+ test_istream_run(istream, strlen(tests[i].input), tests[i].output,
+ HEADER_FILTER_EXCLUDE |
+ HEADER_FILTER_ADD_MISSING_EOH |
+ HEADER_FILTER_NO_CR,
+ edit_callback);
+ i_stream_unref(&istream);
+ }
+ test_end();
+}
+
+static void test_istream_hide_body(void)
+{
+ static const struct {
+ const char *input;
+ const char *output;
+ int extra;
+ } tests[] = {
+ { "From: foo", "From: foo", 0 },
+ { "From: foo\n", "From: foo\n", 0 },
+ { "From: foo\n\n", "From: foo\n\n", 1 },
+ { "From: foo\n\nbar", "From: foo\n\n", -2 },
+ { "From: foo\r\n", "From: foo\r\n", 0 },
+ { "From: foo\r\n\r\n", "From: foo\r\n\r\n", 0 },
+ { "From: foo\r\n\r\nbar", "From: foo\r\n\r\n", -3 }
+ };
+ struct istream *istream;
+ unsigned int i;
+
+ test_begin("i_stream_create_header_filter: hide body");
+ for (i = 0; i < N_ELEMENTS(tests); i++) {
+ istream = test_istream_create(tests[i].input);
+ test_istream_run(istream,
+ (int)strlen(tests[i].input) + tests[i].extra,
+ tests[i].output,
+ HEADER_FILTER_EXCLUDE |
+ HEADER_FILTER_CRLF_PRESERVE |
+ HEADER_FILTER_HIDE_BODY,
+ *null_header_filter_callback);
+ i_stream_unref(&istream);
+ }
+ test_end();
+}
+
+static void ATTR_NULL(3)
+strip_eoh_callback(struct header_filter_istream *input ATTR_UNUSED,
+ struct message_header_line *hdr,
+ bool *matched, void *context ATTR_UNUSED)
+{
+ if (hdr != NULL && hdr->eoh)
+ *matched = TRUE;
+}
+
+static void test_istream_strip_eoh(void)
+{
+ const char *input = "From: foo\nTo: bar\n\nhello world\n";
+ const char *output = "From: foo\nTo: bar\nhello world\n";
+ struct istream *istream;
+
+ test_begin("i_stream_create_header_filter: strip_eoh");
+ istream = test_istream_create(input);
+ test_istream_run(istream, strlen(input), output,
+ HEADER_FILTER_EXCLUDE | HEADER_FILTER_NO_CR,
+ strip_eoh_callback);
+ i_stream_unref(&istream);
+
+ test_end();
+}
+
+static void ATTR_NULL(3)
+missing_eoh_callback(struct header_filter_istream *input ATTR_UNUSED,
+ struct message_header_line *hdr,
+ bool *matched ATTR_UNUSED, void *context ATTR_UNUSED)
+{
+ if (hdr == NULL) {
+ const char *new_hdr = "Subject: added\n\n";
+ i_stream_header_filter_add(input, new_hdr, strlen(new_hdr));
+ }
+}
+
+static void test_istream_missing_eoh_callback(void)
+{
+ const char *input = "From: foo\nTo: bar\n";
+ const char *output = "From: foo\nTo: bar\nSubject: added\n\n";
+ struct istream *istream;
+
+ test_begin("i_stream_create_header_filter: add headers when EOH is missing");
+ istream = test_istream_create(input);
+ test_istream_run(istream, strlen(input) + 1, output,
+ HEADER_FILTER_EXCLUDE | HEADER_FILTER_NO_CR,
+ missing_eoh_callback);
+ i_stream_unref(&istream);
+ test_end();
+}
+
+static void test_istream_empty_missing_eoh_callback(void)
+{
+ const char *input = "";
+ const char *output = "Subject: added\n\n";
+ struct istream *istream;
+
+ test_begin("i_stream_create_header_filter: add headers when mail is empty");
+ istream = test_istream_create(input);
+ test_istream_run(istream, strlen(input)+1, output,
+ HEADER_FILTER_EXCLUDE | HEADER_FILTER_NO_CR,
+ missing_eoh_callback);
+ i_stream_unref(&istream);
+ test_end();
+}
+
+int main(void)
+{
+ static void (*const test_functions[])(void) = {
+ test_istream_filter,
+ test_istream_filter_large_buffer,
+ test_istream_filter_large_buffer2,
+ test_istream_callbacks,
+ test_istream_edit,
+ test_istream_add_missing_eoh,
+ test_istream_add_missing_eoh_and_edit,
+ test_istream_end_body_with_lf,
+ test_istream_hide_body,
+ test_istream_strip_eoh,
+ test_istream_missing_eoh_callback,
+ test_istream_empty_missing_eoh_callback,
+ NULL
+ };
+ return test_run(test_functions);
+}
diff --git a/src/lib-mail/test-istream-qp-decoder.c b/src/lib-mail/test-istream-qp-decoder.c
new file mode 100644
index 0000000..cdf5b22
--- /dev/null
+++ b/src/lib-mail/test-istream-qp-decoder.c
@@ -0,0 +1,197 @@
+/* Copyright (c) 2010-2018 Dovecot authors, see the included COPYING file */
+
+#include "test-lib.h"
+#include "str.h"
+#include "istream-private.h"
+#include "istream-qp.h"
+
+static const struct {
+ const char *input;
+ const char *output;
+ int stream_errno;
+ int eof;
+} tests[] = {
+ { "p=C3=A4=C3=A4t=C3=B6s", "p\xC3\xA4\xC3\xA4t\xC3\xB6s", 0 , 0 },
+ { "p=c3=a4=c3=a4t=c3=b6s= \n", "p\xC3\xA4\xC3\xA4t\xC3\xB6s", 0, 0 },
+ { "p=c3=a4= \t \n=c3=\r\n=a4t= \r\n=c3=b6s", "p\xC3\xA4\xC3\xA4t\xC3\xB6s", 0, 1 },
+ { "p=c3=a4= \t \n=c3=\r\n=a4t= \r\n=c3=b6s", "p\xC3\xA4\xC3\xA4t\xC3\xB6s", 0, 2 },
+ { "p=c3=a4= \t \n=c3=\r\n=a4t= \r\n=c3=b6s", "p\xC3\xA4\xC3\xA4t\xC3\xB6s", 0, 3 },
+ { "p=c3=a4= \t \n=c3=\r\n=a4t= \r\n=c3=b6s", "p\xC3\xA4\xC3\xA4t\xC3\xB6s", 0, 4 },
+ { "p=c3=a4= \t \n=c3=\r\n=a4t= \r\n=c3=b6s", "p\xC3\xA4\xC3\xA4t\xC3\xB6s", 0, 5 },
+ { "p=c3=a4= \t \n=c3=\r\n=a4t= \r\n=c3=b6s", "p\xC3\xA4\xC3\xA4t\xC3\xB6s", 0, 7 },
+ { "p=c3", "p\xC3", 0, 2 },
+ { "=0A=0D ", "\n\r", 0, 7 },
+ { "foo_bar", "foo_bar", 0, 0 },
+ { "\n\n", "\r\n\r\n", 0, 0 },
+ { "\r\n\n\n\r\n", "\r\n\r\n\r\n\r\n", 0, 0 },
+ /* Unnecessarily encoded */
+ { "=66=6f=6f=42=61=72", "fooBar", 0, 4 },
+ /* Expected to be encoded but not */
+ { "\xc3\x9c""berm=c3=a4\xc3\x9figer Gebrauch", "\xc3\x9c""berm\xc3\xa4\xc3\x9figer Gebrauch", 0, 9 },
+ /* Decode control characters */
+ { "=0C=07", "\x0C\x07", 0, 0 },
+ /* Data */
+ { "=DE=AD=BE=EF", "\xDE\xAD\xBE\xEF", 0, 0 },
+ /* Non hex data */
+ { "=FJ=X1", "", EINVAL, 0 },
+ /* No content allowed after Soft Line Break */
+ { "=C3=9C = ","\xc3\x9c ", EPIPE, 0 },
+ /* Boundary delimiter */
+ { "=C3=9C=\r\n-------","\xc3\x9c-------", 0, 0 },
+ { "=----------- =C3=9C","", EINVAL, 0 },
+ { "=___________ =C3=9C","", EINVAL, 0 },
+ { "___________ =C3=9C","___________ \xc3\x9c", 0, 0 },
+ { "=2D=2D=2D=2D=2D=2D =C3=9C","------ \xc3\x9c", 0, 0 },
+ { "=FC=83=BF=BF=BF=BF", "\xFC\x83\xBF\xBF\xBF\xBF", 0, 0 },
+ { "=FE=FE=FF=FF", "\xFE\xFE\xFF\xFF", 0, 0 },
+ { "\xFF=C3=9C\xFE\xFF""foobar", "\xFF\xc3\x9c\xFE\xFF""foobar", 0, 0 },
+
+ { "p=c3=a4\rasdf", "p\xC3\xA4", EINVAL, 0 },
+ { "=___________ \xc3\x9c","", EINVAL, 0 },
+ { "p=c", "p", EPIPE, 0 },
+ { "p=A", "p", EPIPE, 0 },
+ { "p=Ax", "p", EINVAL, 0 },
+ { "___________ \xc3\x9c=C3=9","___________ \xc3\x9c\xC3", EPIPE, 0},
+ { "p=c3=a4=c3=a4t=c3=b6s= ", "p\xC3\xA4\xC3\xA4t\xC3\xB6s", EPIPE, 0 },
+ /* Soft Line Break example from the RFC */
+ {
+ "Now's the time =\r\nfor all folk to come=\r\n to the aid of "
+ "their country.", "Now's the time for all folk to come to the"
+ " aid of their country.", 0, 41
+ },
+};
+
+static bool is_hex(char c) {
+ return ((c >= 48 && c <= 57) || (c >= 65 && c <= 70)
+ || (c >= 97 && c <= 102));
+
+}
+
+static unsigned int
+get_encoding_size_diff(const char *qp_input, unsigned int limit)
+{
+ unsigned int encoded_chars = 0;
+ unsigned int soft_line_breaks = 0;
+ for (unsigned int i = 0; i < limit; i++) {
+ char c = qp_input[i];
+ if (c == '=' && i+2 < limit) {
+ if (qp_input[i+1] == '\r' && qp_input[i+2] == '\n') {
+ soft_line_breaks++;
+ i += 2;
+ limit += 3;
+ } else if (is_hex(qp_input[i+1]) && is_hex(qp_input[i+2])) {
+ encoded_chars++;
+ i += 2;
+ limit += 2;
+ }
+ }
+ }
+ return encoded_chars*2 + soft_line_breaks*3;
+}
+
+static void
+decode_test(const char *qp_input, const char *output, int stream_errno,
+ unsigned int buffer_size, unsigned int eof)
+{
+ size_t qp_input_len = strlen(qp_input);
+ struct istream *input_data, *input_data_limited, *input;
+ const unsigned char *data;
+ size_t i, size;
+ string_t *str = t_str_new(32);
+ int ret = 0;
+
+ input_data = test_istream_create_data(qp_input, qp_input_len);
+ test_istream_set_max_buffer_size(input_data, buffer_size);
+ test_istream_set_allow_eof(input_data, FALSE);
+ input = i_stream_create_qp_decoder(input_data);
+
+ for (i = 1; i <= qp_input_len; i++) {
+ test_istream_set_size(input_data, i);
+ while ((ret = i_stream_read_more(input, &data, &size)) > 0) {
+ str_append_data(str, data, size);
+ i_stream_skip(input, size);
+ }
+ if (ret == -1 && stream_errno != 0)
+ break;
+ test_assert(ret == 0);
+ }
+ if (ret == 0) {
+ test_istream_set_allow_eof(input_data, TRUE);
+ while ((ret = i_stream_read_more(input, &data, &size)) > 0) {
+ str_append_data(str, data, size);
+ i_stream_skip(input, size);
+ }
+ }
+ test_assert(ret == -1);
+ test_assert(input->stream_errno == stream_errno);
+
+ if (stream_errno == 0) {
+ /* Test seeking on streams where the testcases do not
+ * expect a specific errno already */
+ uoff_t v_off = input->v_offset;
+ /* Seeking backwards */
+ i_stream_seek(input, 0);
+ test_assert(input->v_offset == 0);
+
+ /* Seeking forward */
+ i_stream_seek(input, v_off+1);
+ test_assert(input->stream_errno == ESPIPE);
+ }
+ /* Compare outputs */
+ test_assert_strcmp(str_c(str), output);
+
+ if (eof > 0) {
+ /* Insert early EOF into input_data */
+ i_stream_seek(input_data, 0);
+ str_truncate(str, 0);
+ input_data_limited = i_stream_create_limit(input_data, eof);
+ test_istream_set_allow_eof(input_data_limited, TRUE);
+ i_stream_unref(&input);
+ input = i_stream_create_qp_decoder(input_data_limited);
+ while ((ret = i_stream_read_more(input, &data, &size)) > 0) {
+ str_append_data(str, data, size);
+ i_stream_skip(input, size);
+ }
+ test_assert(ret == -1);
+ /* If there is no error still assume that the result is valid
+ * till artifical eof. */
+ if (input->stream_errno == 0) {
+ unsigned int encoding_margin =
+ get_encoding_size_diff(qp_input, eof);
+
+ /* Cut the expected output at eof of input*/
+ const char *expected_output =
+ t_strdup_printf("%.*s", eof-encoding_margin,
+ output);
+ test_assert_strcmp(str_c(str), expected_output);
+ }
+ test_assert(input->eof);
+ i_stream_unref(&input_data_limited);
+ }
+
+ i_stream_unref(&input);
+ i_stream_unref(&input_data);
+}
+
+static void test_istream_qp_decoder(void)
+{
+ unsigned int i, j;
+
+ for (i = 0; i < N_ELEMENTS(tests); i++) {
+ test_begin(t_strdup_printf("istream qp decoder %u", i+1));
+ for (j = 1; j < 10; j++) T_BEGIN {
+ decode_test(tests[i].input, tests[i].output,
+ tests[i].stream_errno, j, tests[i].eof);
+ } T_END;
+ test_end();
+ }
+}
+
+int main(void)
+{
+ static void (*const test_functions[])(void) = {
+ test_istream_qp_decoder,
+ NULL
+ };
+ return test_run(test_functions);
+}
diff --git a/src/lib-mail/test-istream-qp-encoder.c b/src/lib-mail/test-istream-qp-encoder.c
new file mode 100644
index 0000000..148fe24
--- /dev/null
+++ b/src/lib-mail/test-istream-qp-encoder.c
@@ -0,0 +1,160 @@
+/* Copyright (c) 2017-2018 Dovecot authors, see the included COPYING file */
+
+#include "test-lib.h"
+#include "str.h"
+#include "istream-private.h"
+#include "istream-qp.h"
+
+#define WHITESPACE10 " \t \t \t"
+#define WHITESPACE70 WHITESPACE10 WHITESPACE10 WHITESPACE10 WHITESPACE10 WHITESPACE10 WHITESPACE10 WHITESPACE10
+
+static const struct {
+ const void *input;
+ const char *output;
+ int stream_errno;
+} tests[] = {
+ { "", "", 0 },
+ { "short test", "short test", 0 },
+ { "C'est une cha\xc3\xaene de test simple", "C'est une cha=C3=AEne de test simple", 0 },
+ {
+ "wrap after 76 characters wrap after 76 characters wrap after 76 characters wrap after 76 characters",
+ "wrap after 76 characters wrap after 76 characters wrap after 76 character=\r\ns wrap after 76 characters",
+ 0
+ },
+ {
+ /* the string is split up to avoid C compilers thinking \x99ed as escape */
+ "P\xc5\x99" "edstavitel\xc3\xa9 francouzsk\xc3\xa9ho lidu, ustanoveni v "
+ "N\xc3\xa1rodn\xc3\xadm shrom\xc3\xa1\xc5\xbe" "d\xc4\x9bn\xc3\xad, domn"
+ "\xc3\xadvaj\xc3\xad" "ce se, \xc5\xbe" "e nev\xc4\x9b" "domost, zapomenut\xc3"
+ "\xad nebo pohrd\xc3\xa1n\xc3\xad lidsk\xc3\xbdmi pr\xc3\xa1vy jsou j"
+ "edin\xc3\xbdmi p\xc5\x99\xc3\xad\xc4\x8dinami ve\xc5\x99" "ejn\xc3\xb"
+ "dch ne\xc5\xa1t\xc4\x9bst\xc3\xad a zkorumpov\xc3\xa1n\xc3\xad vl"
+ "\xc3\xa1" "d, rozhodli se vylo\xc5\xbeit v slavnostn\xc3\xad Deklara"
+ "ci p\xc5\x99irozen\xc3\xa1, nezciziteln\xc3\xa1 a posv\xc3\xa1tn\xc3"
+ "\xa1 pr\xc3\xa1va \xc4\x8dlov\xc4\x9bka za t\xc3\xadm \xc3\xba\xc4"
+ "\x8d" "elem, aby tato Deklarace, neust\xc3\xa1le jsouc p\xc5\x99" "ed o"
+ "\xc4\x8" "dima v\xc5\xa1" "em \xc4\x8dlen\xc5\xafm lidsk\xc3\xa9 spo"
+ "le\xc4\x8dnosti, uv\xc3\xa1" "d\xc4\x9bla jim st\xc3\xa1le na pam\xc4"
+ "\x9b\xc5\xa5 jejich pr\xc3\xa1va a jejich povinnosti; aby \xc4\x8din"
+ "y z\xc3\xa1konod\xc3\xa1rn\xc3\xa9 moci a \xc4\x8diny v\xc3\xbdkonn"
+ "\xc3\xa9 moci mohly b\xc3\xbdt v ka\xc5\xbe" "d\xc3\xa9 chv\xc3\xadli p"
+ "orovn\xc3\xa1v\xc3\xa1ny s \xc3\xba\xc4\x8d" "elem ka\xc5\xbe" "d\xc3"
+ "\xa9 politick\xc3\xa9 instituce a byly v d\xc5\xafsledku toho chov"
+ "\xc3\xa1ny je\xc5\xa1t\xc4\x9b v\xc3\xad" "ce v \xc3\xba" "ct\xc4"
+ "\x9b; aby po\xc5\xbe" "adavky ob\xc4\x8d" "an\xc5\xaf, kdy\xc5\xbe s"
+ "e budou nap\xc5\x99\xc3\xad\xc5\xa1t\xc4\x9b zakl\xc3\xa1" "dat na j"
+ "ednoduch\xc3\xb" "dch a nepop\xc3\xadrateln\xc3\xbd" "ch z\xc3\xa1sa"
+ "d\xc3\xa1" "ch, sm\xc4\x9b\xc5\x99ovaly v\xc5\xb" "edy k zachov\xc3"
+ "\xa1n\xc3\xad \xc3\xbastavy a ku blahu v\xc5\xa1" "ech.",
+ "P=C5=99edstavitel=C3=A9 francouzsk=C3=A9ho lidu, ustanoveni v N=C3=A1rodn=\r\n"
+ "=C3=ADm shrom=C3=A1=C5=BEd=C4=9Bn=C3=AD, domn=C3=ADvaj=C3=ADce se, =C5=BE=\r\n"
+ "e nev=C4=9Bdomost, zapomenut=C3=AD nebo pohrd=C3=A1n=C3=AD lidsk=C3=BDmi=20=\r\n"
+ "pr=C3=A1vy jsou jedin=C3=BDmi p=C5=99=C3=AD=C4=8Dinami ve=C5=99ejn=C3=0Bd=\r\n"
+ "ch ne=C5=A1t=C4=9Bst=C3=AD a zkorumpov=C3=A1n=C3=AD vl=C3=A1d, rozhodli=20=\r\n"
+ "se vylo=C5=BEit v slavnostn=C3=AD Deklaraci p=C5=99irozen=C3=A1, nezcizit=\r\n"
+ "eln=C3=A1 a posv=C3=A1tn=C3=A1 pr=C3=A1va =C4=8Dlov=C4=9Bka za t=C3=ADm=20=\r\n"
+ "=C3=BA=C4=8Delem, aby tato Deklarace, neust=C3=A1le jsouc p=C5=99ed o=C4=\r\n"
+ "=08dima v=C5=A1em =C4=8Dlen=C5=AFm lidsk=C3=A9 spole=C4=8Dnosti, uv=C3=A1=\r\n"
+ "d=C4=9Bla jim st=C3=A1le na pam=C4=9B=C5=A5 jejich pr=C3=A1va a jejich po=\r\n"
+ "vinnosti; aby =C4=8Diny z=C3=A1konod=C3=A1rn=C3=A9 moci a =C4=8Diny v=C3=\r\n"
+ "=BDkonn=C3=A9 moci mohly b=C3=BDt v ka=C5=BEd=C3=A9 chv=C3=ADli porovn=C3=\r\n"
+ "=A1v=C3=A1ny s =C3=BA=C4=8Delem ka=C5=BEd=C3=A9 politick=C3=A9 instituce=20=\r\n"
+ "a byly v d=C5=AFsledku toho chov=C3=A1ny je=C5=A1t=C4=9B v=C3=ADce v =C3=\r\n"
+ "=BAct=C4=9B; aby po=C5=BEadavky ob=C4=8Dan=C5=AF, kdy=C5=BE se budou nap=\r\n"
+ "=C5=99=C3=AD=C5=A1t=C4=9B zakl=C3=A1dat na jednoduch=C3=0Bdch a nepop=C3=\r\n"
+ "=ADrateln=C3=BDch z=C3=A1sad=C3=A1ch, sm=C4=9B=C5=99ovaly v=C5=0Bedy k za=\r\n"
+ "chov=C3=A1n=C3=AD =C3=BAstavy a ku blahu v=C5=A1ech.",
+ 0
+ },
+ /* Test line breaking */
+ { WHITESPACE70"1234567", WHITESPACE70"123=\r\n4567", 0 },
+ { WHITESPACE70" 7", WHITESPACE70" =20=\r\n 7", 0 },
+ { WHITESPACE70""WHITESPACE10"1", WHITESPACE70" =20=\r\n \t \t \t1", 0 },
+
+};
+
+static void
+encode_test(const char *qp_input, const char *output, int stream_errno,
+ unsigned int buffer_size)
+{
+ size_t qp_input_len = strlen(qp_input);
+ struct istream *input_data, *input;
+ const unsigned char *data;
+ size_t i, size;
+ string_t *str = t_str_new(32);
+ int ret = 0;
+
+ input_data = test_istream_create_data(qp_input, qp_input_len);
+ test_istream_set_max_buffer_size(input_data, buffer_size);
+ test_istream_set_allow_eof(input_data, FALSE);
+ input = i_stream_create_qp_encoder(input_data, 0);
+
+ for (i = 1; i <= qp_input_len; i++) {
+ test_istream_set_size(input_data, i);
+ while ((ret = i_stream_read_more(input, &data, &size)) > 0) {
+ str_append_data(str, data, size);
+ i_stream_skip(input, size);
+ }
+ if (ret == -1 && stream_errno != 0)
+ break;
+ test_assert(ret == 0);
+ }
+ if (ret == 0) {
+ test_istream_set_allow_eof(input_data, TRUE);
+ while ((ret = i_stream_read_more(input, &data, &size)) > 0) {
+ str_append_data(str, data, size);
+ i_stream_skip(input, size);
+ }
+ }
+
+ test_assert(ret == -1);
+ test_assert(input->stream_errno == stream_errno);
+ test_assert_strcmp(str_c(str), output);
+
+ if (stream_errno == 0) {
+ /* Test seeking on streams where the testcases do not
+ * expect a specific errno already. */
+ uoff_t v_off = input->v_offset;
+ /* Seeking backwards */
+ i_stream_seek(input, 0);
+ test_assert(input->v_offset == 0);
+
+ /* Seeking forward */
+ i_stream_seek(input, v_off+1);
+ test_assert(input->stream_errno == ESPIPE);
+ }
+
+ i_stream_unref(&input);
+ /* Test closing stream gives expected results */
+ i_stream_seek(input_data, 0);
+ input = i_stream_create_qp_encoder(input_data, 0);
+ i_stream_close(input);
+ test_assert(input->closed);
+ test_assert(i_stream_read_more(input, &data, &size) == -1);
+
+ i_stream_unref(&input);
+ i_stream_unref(&input_data);
+}
+
+static void test_istream_qp_encoder(void)
+{
+ unsigned int i, j;
+
+ for (i = 0; i < N_ELEMENTS(tests); i++) {
+ test_begin(t_strdup_printf("istream qp encoder %u", i+1));
+ for (j = 1; j < 10; j++) T_BEGIN {
+ encode_test(tests[i].input, tests[i].output,
+ tests[i].stream_errno, j);
+ } T_END;
+ test_end();
+ }
+}
+
+int main(void)
+{
+ static void (*const test_functions[])(void) = {
+ test_istream_qp_encoder,
+ NULL
+ };
+ return test_run(test_functions);
+}
diff --git a/src/lib-mail/test-mail-html2text.c b/src/lib-mail/test-mail-html2text.c
new file mode 100644
index 0000000..73e93f7
--- /dev/null
+++ b/src/lib-mail/test-mail-html2text.c
@@ -0,0 +1,120 @@
+/* Copyright (c) 2015-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "str.h"
+#include "istream.h"
+#include "mail-html2text.h"
+#include "test-common.h"
+
+static const struct {
+ const char *input;
+ const char *output;
+} tests[] = {
+ { "&&aaaaaaaaaa", "" },
+
+ { "a&amp;&lt;&clubs;&gt;b",
+ "a&<\xE2\x99\xA3>b" },
+ { "&", "" },
+ { "&amp", "" },
+
+ { "a<style>stylesheet is ignored</style>b",
+ "a b" },
+ { "a<stylea>b</stylea>c",
+ "a b c" },
+ { "a<!--x <p foo=\"bar\">commented tags ignored also</p> y-->b",
+ "ab" },
+ { "a<script>javascript <p>foo</p> ignored</script>b",
+ "a b" },
+ { "a<scripta>b</scripta>c",
+ "a b c" },
+ { "a<blockquote><blockquote>second level</blockquote>ignored</blockquote>b",
+ "a b" },
+ { "a<![CDATA[<style>]] >b</style>]]>c",
+ "a<style>]] >b</style>c" },
+
+ { "a<foo", "a" },
+ { "a<blockquote", "a" },
+ { "a<blockquote>foo</blockquote", "a " },
+ { "a<", "a" },
+ { "a<![CDATA[b", "ab" },
+ { "a<![CDATA[b]]", "ab" },
+ { "a&#228;", "a\xC3\xA4" },
+ { "a&#xe4;", "a\xC3\xA4" },
+ { "&#8364;", "\xE2\x82\xAC" },
+ { "&#deee;", "" }, // invalid codepoint
+};
+
+static const char *test_blockquote_input[] = {
+ "a<blockquote>b<blockquote><blockquote>c</blockquote>d</blockquote>e</blockquote>f",
+ "a&amp;<blockquote>b&amp;<blockquote>&amp;<blockquote>&amp;c</blockquote>d&amp;</blockquote>&amp;e</blockquote>f&amp;",
+ NULL
+};
+
+static const char *test_blockquote_output[] = {
+ "a\n> b\n> \n> c\n> d\n> e\nf",
+ "a&\n> b&\n> &\n> &c\n> d&\n> &e\nf&",
+ NULL
+};
+
+static void test_mail_html2text(void)
+{
+ string_t *str = t_str_new(128);
+ struct mail_html2text *ht;
+ unsigned int i, j;
+
+ test_begin("mail_html2text()");
+ for (i = 0; i < N_ELEMENTS(tests); i++) {
+ ht = mail_html2text_init(MAIL_HTML2TEXT_FLAG_SKIP_QUOTED);
+ for (j = 0; tests[i].input[j] != '\0'; j++) {
+ unsigned char c = tests[i].input[j];
+ mail_html2text_more(ht, &c, 1, str);
+ }
+ test_assert_idx(strcmp(str_c(str), tests[i].output) == 0, i);
+ mail_html2text_deinit(&ht);
+ str_truncate(str, 0);
+ }
+
+ /* test without skipping quoted */
+ for (unsigned int i = 0; test_blockquote_input[i] != NULL; i++) {
+ str_truncate(str, 0);
+ ht = mail_html2text_init(0);
+ mail_html2text_more(ht, (const void *)test_blockquote_input[i],
+ strlen(test_blockquote_input[i]), str);
+ test_assert_idx(strcmp(str_c(str), test_blockquote_output[i]) == 0, i);
+ mail_html2text_deinit(&ht);
+ }
+
+ test_end();
+}
+
+static void test_mail_html2text_random(void)
+{
+ string_t *str = t_str_new(128);
+ struct mail_html2text *ht;
+
+ test_begin("mail_html2text() random");
+ for (unsigned int i = 0; i < 1000; i++) {
+ char valid_chars[] = { '0', 'a', '<', '>', '&', ';', '\\', '\'', '"', '/' };
+ unsigned char s[2];
+
+ ht = mail_html2text_init(0);
+ for (unsigned int i = 0; i < 100; i++) {
+ s[0] = valid_chars[i_rand_limit(N_ELEMENTS(valid_chars))];
+ s[1] = valid_chars[i_rand_limit(N_ELEMENTS(valid_chars))];
+ mail_html2text_more(ht, s, i_rand_minmax(1, 2), str);
+ }
+ mail_html2text_deinit(&ht);
+ str_truncate(str, 0);
+ }
+ test_end();
+}
+
+int main(void)
+{
+ static void (*const test_functions[])(void) = {
+ test_mail_html2text,
+ test_mail_html2text_random,
+ NULL
+ };
+ return test_run(test_functions);
+}
diff --git a/src/lib-mail/test-mail-user-hash.c b/src/lib-mail/test-mail-user-hash.c
new file mode 100644
index 0000000..7e2a496
--- /dev/null
+++ b/src/lib-mail/test-mail-user-hash.c
@@ -0,0 +1,185 @@
+/* Copyright (c) 2020 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "buffer.h"
+#include "str.h"
+#include "mail-user-hash.h"
+#include "test-common.h"
+
+#include "md5.h"
+
+static void test_mail_user_hash(void)
+{
+ struct test_case {
+ const char *username;
+ const char *format;
+ unsigned int hash;
+ } test_cases[] = {
+ {
+ .username = "",
+ .format = "",
+ .hash = 3558706393,
+ },
+ {
+ .username = "testuser",
+ .format = "",
+ .hash = 3558706393,
+ },
+ {
+ .username = "",
+ .format = "%u",
+ .hash = 3558706393,
+ },
+ {
+ .username = "@",
+ .format = "%u",
+ .hash = 1368314517,
+ },
+ {
+ .username = "",
+ .format = "%n@%d",
+ .hash = 1368314517,
+ },
+ {
+ .username = "",
+ .format = "%n",
+ .hash = 3558706393,
+ },
+ {
+ .username = "",
+ .format = "%d",
+ .hash = 3558706393,
+ },
+ {
+ .username = "testuser",
+ .format = "%u",
+ .hash = 1570531526,
+ },
+ {
+ .username = "testuser",
+ .format = "%n",
+ .hash = 1570531526,
+ },
+ {
+ .username = "testuser",
+ .format = "%d",
+ .hash = 3558706393,
+ },
+ {
+ .username = "@domain",
+ .format = "%u",
+ .hash = 3749630072,
+ },
+ {
+ .username = "@domain",
+ .format = "%n@%d",
+ .hash = 3749630072,
+ },
+ {
+ .username = "@domain",
+ .format = "%n",
+ .hash = 3558706393,
+ },
+ {
+ .username = "@domain",
+ .format = "%d",
+ .hash = 2908717800,
+ },
+ {
+ .username = "testuser@domain",
+ .format = "%u",
+ .hash = 3813799143,
+ },
+ {
+ .username = "testuser@domain",
+ .format = "%n@%d",
+ .hash = 3813799143,
+ },
+ {
+ .username = "testuser@domain",
+ .format = "%n",
+ .hash = 1570531526,
+ },
+ {
+ .username = "testuser@domain",
+ .format = "%d",
+ .hash = 2908717800,
+ },
+ {
+ .username = "test@user@domain",
+ .format = "%u",
+ .hash = 2029259821,
+ },
+ {
+ .username = "test@user@domain",
+ .format = "%n@%d",
+ .hash = 2029259821,
+ },
+ {
+ .username = "test@user@domain",
+ .format = "%n",
+ .hash = 160394189,
+ },
+ {
+ .username = "test@user@domain",
+ .format = "%d",
+ .hash = 1841230927,
+ }
+ };
+
+ test_begin("mail_user_hash");
+
+ for (size_t i = 0; i < N_ELEMENTS(test_cases); i++) {
+ const struct test_case *tc = &test_cases[i];
+ const char *error = NULL;
+ unsigned int hash;
+ test_assert_idx(mail_user_hash(tc->username, tc->format, &hash,
+ &error), i);
+ test_assert_idx(error == NULL, i);
+ test_assert_idx(hash == tc->hash, i);
+ }
+
+ test_end();
+}
+
+static void test_mail_user_hash_errors(void)
+{
+ test_begin("mail_user_hash_errors");
+
+ struct test_case {
+ const char *username;
+ const char *format;
+ unsigned int hash;
+ const char *error;
+ } test_cases[] = {
+ {
+ .username = "testuser@domain",
+ .format = "%{invalid}",
+ .hash = 1466562296,
+ .error = "Unknown variable '%invalid'",
+ },
+ };
+
+ for (size_t i = 0; i < N_ELEMENTS(test_cases); i++) {
+ const struct test_case *tc = &test_cases[i];
+ const char *error = NULL;
+ unsigned int hash = 0;
+ test_assert_idx(mail_user_hash(tc->username, tc->format, &hash,
+ &error) == FALSE, i);
+ test_assert_idx(tc->hash == hash, i);
+ test_assert_strcmp_idx(tc->error, error, i);
+ test_assert_idx(tc->hash == hash, i);
+ }
+
+ test_end();
+}
+
+int main(void)
+{
+ static void (*const test_functions[])(void) = {
+ test_mail_user_hash,
+ test_mail_user_hash_errors,
+ NULL
+ };
+ return test_run(test_functions);
+}
diff --git a/src/lib-mail/test-mbox-from.c b/src/lib-mail/test-mbox-from.c
new file mode 100644
index 0000000..2fce324
--- /dev/null
+++ b/src/lib-mail/test-mbox-from.c
@@ -0,0 +1,104 @@
+/* Copyright (c) 2009-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "utc-offset.h"
+#include "mbox-from.h"
+#include "test-common.h"
+
+#include <time.h>
+
+struct test_mbox_from_parse_output {
+ time_t time;
+ int tz_offset;
+ const char *sender;
+ int ret;
+};
+
+static void test_mbox_from_parse(void)
+{
+ static const char *input[] = {
+ "user@domain Thu Nov 29 23:33:09 1973 +0200",
+ "user@domain Thu Nov 29 19:33:09 1973 -0200",
+ "\"user name\"@domain Fri Jan 2 10:13:52 UTC 1970 +0000",
+ "user Fri Jan 2 10:14 1970 +0000",
+ "user Fri, 2 Jan 1970 10:14:00 +0000",
+ "user Fri, 2 Jan 1970 10:14 +0000",
+ " Fri Jan 2 10:14 1970 +0000",
+ "user Fri, 2 Foo 1970 10:14:00",
+ "Fri Jan 2 10:14 1970 +0000",
+ "user Fri Jan x 10:14 1970 +0000",
+ "user Fri Jan 2 0:14 1970 +0000",
+ "user Fri Jan 2 xx:14 1970 +0000",
+ "user Fri Jan 2 10: 1970 +0000",
+ "user Fri Jan 2 10:xx 1970 +0000",
+ "user Fri Jan 2 10:xx +0000",
+ };
+ static struct test_mbox_from_parse_output output[] = {
+ { 123456789, 2*60, "user@domain", 0 },
+ { 123456789, -2*60, "user@domain", 0 },
+ { 123232, 0, "\"user name\"@domain", 0 },
+ { 123240, 0, "user", 0 },
+ { 123240, 0, "user", 0 },
+ { 123240, 0, "user", 0 },
+ { 123240, 0, "", 0 },
+ { 0, 0, NULL, -1 },
+ { 0, 0, NULL, -1 },
+ { 0, 0, NULL, -1 },
+ { 0, 0, NULL, -1 },
+ { 0, 0, NULL, -1 },
+ { 0, 0, NULL, -1 },
+ { 0, 0, NULL, -1 },
+ { 0, 0, NULL, -1 },
+ };
+ unsigned int i, j;
+ size_t len;
+ struct tm *tm;
+ char *sender;
+ bool success;
+ time_t t;
+ int tz, ret;
+
+ for (j = 0; j < 2; j++) {
+ for (i = 0; i < N_ELEMENTS(input); i++) {
+ len = strlen(input[i]) - j*6;
+ ret = mbox_from_parse((const unsigned char *)input[i],
+ len, &t, &tz, &sender);
+ success = (ret < 0 && output[i].ret < 0) ||
+ (ret == output[i].ret && t == output[i].time &&
+ tz == output[i].tz_offset &&
+ strcmp(sender, output[i].sender) == 0);
+ i_free(sender);
+ test_out(t_strdup_printf("mbox_from_parse(%d,%d)", j, i), success);
+
+ /* prepare for testing without timezone */
+ if (output[i].ret == 0) {
+ output[i].time += output[i].tz_offset*60;
+ tm = localtime(&output[i].time);
+ output[i].tz_offset = utc_offset(tm, output[i].time);
+ output[i].time -= output[i].tz_offset*60;
+ }
+ }
+ }
+}
+
+static void test_mbox_from_create(void)
+{
+ time_t t = 1234567890;
+ int tz;
+
+ test_begin("mbox_from_create()");
+ tz = utc_offset(localtime(&t), t) * -60;
+ test_assert(strcmp(mbox_from_create("user", t+tz),
+ "From user Fri Feb 13 23:31:30 2009\n") == 0);
+ test_end();
+}
+
+int main(void)
+{
+ static void (*const test_functions[])(void) = {
+ test_mbox_from_parse,
+ test_mbox_from_create,
+ NULL
+ };
+ return test_run(test_functions);
+}
diff --git a/src/lib-mail/test-message-address.c b/src/lib-mail/test-message-address.c
new file mode 100644
index 0000000..e6204bb
--- /dev/null
+++ b/src/lib-mail/test-message-address.c
@@ -0,0 +1,532 @@
+/* Copyright (c) 2007-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "str.h"
+#include "message-address.h"
+#include "test-common.h"
+
+enum test_message_address {
+ TEST_MESSAGE_ADDRESS_FLAG_SKIP_LIST = BIT(0),
+};
+
+static bool cmp_addr(const struct message_address *a1,
+ const struct message_address *a2)
+{
+ return null_strcmp(a1->name, a2->name) == 0 &&
+ null_strcmp(a1->route, a2->route) == 0 &&
+ null_strcmp(a1->mailbox, a2->mailbox) == 0 &&
+ null_strcmp(a1->domain, a2->domain) == 0 &&
+ a1->invalid_syntax == a2->invalid_syntax;
+}
+
+static const struct message_address *
+test_parse_address(const char *input, bool fill_missing)
+{
+ const enum message_address_parse_flags flags =
+ fill_missing ? MESSAGE_ADDRESS_PARSE_FLAG_FILL_MISSING : 0;
+ /* duplicate the input (without trailing NUL) so valgrind notices
+ if there's any out-of-bounds access */
+ size_t input_len = strlen(input);
+ unsigned char *input_dup = i_memdup(input, input_len);
+ const struct message_address *addr =
+ message_address_parse(pool_datastack_create(),
+ input_dup, input_len, UINT_MAX, flags);
+ i_free(input_dup);
+ return addr;
+}
+
+static void test_message_address(void)
+{
+ static const struct test {
+ const char *input;
+ const char *wanted_output;
+ const char *wanted_filled_output;
+ struct message_address addr;
+ struct message_address filled_addr;
+ enum test_message_address flags;
+ } tests[] = {
+ /* user@domain -> <user@domain> */
+ { "user@domain", "<user@domain>", NULL,
+ { NULL, NULL, NULL, "user", "domain", FALSE },
+ { NULL, NULL, NULL, "user", "domain", FALSE }, 0 },
+ { "\"user\"@domain", "<user@domain>", NULL,
+ { NULL, NULL, NULL, "user", "domain", FALSE },
+ { NULL, NULL, NULL, "user", "domain", FALSE }, 0 },
+ { "\"user name\"@domain", "<\"user name\"@domain>", NULL,
+ { NULL, NULL, NULL, "user name", "domain", FALSE },
+ { NULL, NULL, NULL, "user name", "domain", FALSE }, 0 },
+ { "\"user@na\\\\me\"@domain", "<\"user@na\\\\me\"@domain>", NULL,
+ { NULL, NULL, NULL, "user@na\\me", "domain", FALSE },
+ { NULL, NULL, NULL, "user@na\\me", "domain", FALSE }, 0 },
+ { "\"user\\\"name\"@domain", "<\"user\\\"name\"@domain>", NULL,
+ { NULL, NULL, NULL, "user\"name", "domain", FALSE },
+ { NULL, NULL, NULL, "user\"name", "domain", FALSE }, 0 },
+ { "\"\"@domain", "<\"\"@domain>", NULL,
+ { NULL, NULL, NULL, "", "domain", FALSE },
+ { NULL, NULL, NULL, "", "domain", FALSE }, 0 },
+ { "user", "<user>", "<user@MISSING_DOMAIN>",
+ { NULL, NULL, NULL, "user", "", TRUE },
+ { NULL, NULL, NULL, "user", "MISSING_DOMAIN", TRUE }, 0 },
+ { "@domain", "<\"\"@domain>", "<MISSING_MAILBOX@domain>",
+ { NULL, NULL, NULL, "", "domain", TRUE },
+ { NULL, NULL, NULL, "MISSING_MAILBOX", "domain", TRUE }, 0 },
+
+ /* Display Name -> Display Name */
+ { "Display Name", "\"Display Name\"", "\"Display Name\" <MISSING_MAILBOX@MISSING_DOMAIN>",
+ { NULL, "Display Name", NULL, "", "", TRUE },
+ { NULL, "Display Name", NULL, "MISSING_MAILBOX", "MISSING_DOMAIN", TRUE }, 0 },
+ { "\"Display Name\"", "\"Display Name\"", "\"Display Name\" <MISSING_MAILBOX@MISSING_DOMAIN>",
+ { NULL, "Display Name", NULL, "", "", TRUE },
+ { NULL, "Display Name", NULL, "MISSING_MAILBOX", "MISSING_DOMAIN", TRUE }, 0 },
+ { "Display \"Name\"", "\"Display Name\"", "\"Display Name\" <MISSING_MAILBOX@MISSING_DOMAIN>",
+ { NULL, "Display Name", NULL, "", "", TRUE },
+ { NULL, "Display Name", NULL, "MISSING_MAILBOX", "MISSING_DOMAIN", TRUE }, 0 },
+ { "\"Display\" \"Name\"", "\"Display Name\"", "\"Display Name\" <MISSING_MAILBOX@MISSING_DOMAIN>",
+ { NULL, "Display Name", NULL, "", "", TRUE },
+ { NULL, "Display Name", NULL, "MISSING_MAILBOX", "MISSING_DOMAIN", TRUE }, 0 },
+ { "\"\"", "", "<MISSING_MAILBOX@MISSING_DOMAIN>",
+ { NULL, "", NULL, "", "", TRUE },
+ { NULL, "", NULL, "MISSING_MAILBOX", "MISSING_DOMAIN", TRUE }, 0 },
+
+ /* <user@domain> -> <user@domain> */
+ { "<user@domain>", NULL, NULL,
+ { NULL, NULL, NULL, "user", "domain", FALSE },
+ { NULL, NULL, NULL, "user", "domain", FALSE }, 0 },
+ { "<\"user\"@domain>", "<user@domain>", NULL,
+ { NULL, NULL, NULL, "user", "domain", FALSE },
+ { NULL, NULL, NULL, "user", "domain", FALSE }, 0 },
+ { "<\"user name\"@domain>", NULL, NULL,
+ { NULL, NULL, NULL, "user name", "domain", FALSE },
+ { NULL, NULL, NULL, "user name", "domain", FALSE }, 0 },
+ { "<\"user@na\\\\me\"@domain>", NULL, NULL,
+ { NULL, NULL, NULL, "user@na\\me", "domain", FALSE },
+ { NULL, NULL, NULL, "user@na\\me", "domain", FALSE }, 0 },
+ { "<\"user\\\"name\"@domain>", NULL, NULL,
+ { NULL, NULL, NULL, "user\"name", "domain", FALSE },
+ { NULL, NULL, NULL, "user\"name", "domain", FALSE }, 0 },
+ { "<\"\"@domain>", NULL, NULL,
+ { NULL, NULL, NULL, "", "domain", FALSE },
+ { NULL, NULL, NULL, "", "domain", FALSE }, 0 },
+ { "<user>", NULL, "<user@MISSING_DOMAIN>",
+ { NULL, NULL, NULL, "user", "", TRUE },
+ { NULL, NULL, NULL, "user", "MISSING_DOMAIN", TRUE }, 0 },
+ { "<@route>", "<@route:\"\">", "<INVALID_ROUTE:MISSING_MAILBOX@MISSING_DOMAIN>",
+ { NULL, NULL, "@route", "", "", TRUE },
+ { NULL, NULL, "INVALID_ROUTE", "MISSING_MAILBOX", "MISSING_DOMAIN", TRUE }, 0 },
+
+ /* user@domain (Display Name) -> "Display Name" <user@domain> */
+ { "user@domain (DisplayName)", "DisplayName <user@domain>", NULL,
+ { NULL, "DisplayName", NULL, "user", "domain", FALSE },
+ { NULL, "DisplayName", NULL, "user", "domain", FALSE }, 0 },
+ { "user@domain (Display Name)", "\"Display Name\" <user@domain>", NULL,
+ { NULL, "Display Name", NULL, "user", "domain", FALSE },
+ { NULL, "Display Name", NULL, "user", "domain", FALSE }, 0 },
+ { "user@domain (Display\"Name)", "\"Display\\\"Name\" <user@domain>", NULL,
+ { NULL, "Display\"Name", NULL, "user", "domain", FALSE },
+ { NULL, "Display\"Name", NULL, "user", "domain", FALSE }, 0 },
+ { "user (Display Name)", "\"Display Name\" <user>", "\"Display Name\" <user@MISSING_DOMAIN>",
+ { NULL, "Display Name", NULL, "user", "", TRUE },
+ { NULL, "Display Name", NULL, "user", "MISSING_DOMAIN", TRUE }, 0 },
+ { "@domain (Display Name)", "\"Display Name\" <\"\"@domain>", "\"Display Name\" <MISSING_MAILBOX@domain>",
+ { NULL, "Display Name", NULL, "", "domain", TRUE },
+ { NULL, "Display Name", NULL, "MISSING_MAILBOX", "domain", TRUE }, 0 },
+ { "user@domain ()", "<user@domain>", NULL,
+ { NULL, NULL, NULL, "user", "domain", FALSE },
+ { NULL, NULL, NULL, "user", "domain", FALSE }, 0 },
+
+ /* Display Name <user@domain> -> "Display Name" <user@domain> */
+ { "DisplayName <user@domain>", NULL, NULL,
+ { NULL, "DisplayName", NULL, "user", "domain", FALSE },
+ { NULL, "DisplayName", NULL, "user", "domain", FALSE }, 0 },
+ { "Display Name <user@domain>", "\"Display Name\" <user@domain>", NULL,
+ { NULL, "Display Name", NULL, "user", "domain", FALSE },
+ { NULL, "Display Name", NULL, "user", "domain", FALSE }, 0 },
+ { "\"Display Name\" <user@domain>", NULL, NULL,
+ { NULL, "Display Name", NULL, "user", "domain", FALSE },
+ { NULL, "Display Name", NULL, "user", "domain", FALSE }, 0 },
+ { "\"Display\\\"Name\" <user@domain>", NULL, NULL,
+ { NULL, "Display\"Name", NULL, "user", "domain", FALSE },
+ { NULL, "Display\"Name", NULL, "user", "domain", FALSE }, 0 },
+ { "Display Name <user>", "\"Display Name\" <user>", "\"Display Name\" <user@MISSING_DOMAIN>",
+ { NULL, "Display Name", NULL, "user", "", TRUE },
+ { NULL, "Display Name", NULL, "user", "MISSING_DOMAIN", TRUE }, 0 },
+ { "\"\" <user@domain>", "<user@domain>", NULL,
+ { NULL, NULL, NULL, "user", "domain", FALSE },
+ { NULL, NULL, NULL, "user", "domain", FALSE }, 0 },
+
+ /* <@route:user@domain> -> <@route:user@domain> */
+ { "<@route:user@domain>", NULL, NULL,
+ { NULL, NULL, "@route", "user", "domain", FALSE },
+ { NULL, NULL, "@route", "user", "domain", FALSE }, 0 },
+ { "<@route,@route2:user@domain>", NULL, NULL,
+ { NULL, NULL, "@route,@route2", "user", "domain", FALSE },
+ { NULL, NULL, "@route,@route2", "user", "domain", FALSE }, 0 },
+ { "<@route@route2:user@domain>", "<@route,@route2:user@domain>", NULL,
+ { NULL, NULL, "@route,@route2", "user", "domain", FALSE },
+ { NULL, NULL, "@route,@route2", "user", "domain", FALSE }, 0 },
+ { "<@route@route2:user>", "<@route,@route2:user>", "<@route,@route2:user@MISSING_DOMAIN>",
+ { NULL, NULL, "@route,@route2", "user", "", TRUE },
+ { NULL, NULL, "@route,@route2", "user", "MISSING_DOMAIN", TRUE }, 0 },
+ { "<@route@route2:\"\"@domain>", "<@route,@route2:\"\"@domain>", NULL,
+ { NULL, NULL, "@route,@route2", "", "domain", FALSE },
+ { NULL, NULL, "@route,@route2", "", "domain", FALSE }, 0 },
+
+ /* Display Name <@route:user@domain> ->
+ "Display Name" <@route:user@domain> */
+ { "Display Name <@route:user@domain>", "\"Display Name\" <@route:user@domain>", NULL,
+ { NULL, "Display Name", "@route", "user", "domain", FALSE },
+ { NULL, "Display Name", "@route", "user", "domain", FALSE }, 0 },
+ { "Display Name <@route,@route2:user@domain>", "\"Display Name\" <@route,@route2:user@domain>", NULL,
+ { NULL, "Display Name", "@route,@route2", "user", "domain", FALSE },
+ { NULL, "Display Name", "@route,@route2", "user", "domain", FALSE }, 0 },
+ { "Display Name <@route@route2:user@domain>", "\"Display Name\" <@route,@route2:user@domain>", NULL,
+ { NULL, "Display Name", "@route,@route2", "user", "domain", FALSE },
+ { NULL, "Display Name", "@route,@route2", "user", "domain", FALSE }, 0 },
+ { "Display Name <@route@route2:user>", "\"Display Name\" <@route,@route2:user>", "\"Display Name\" <@route,@route2:user@MISSING_DOMAIN>",
+ { NULL, "Display Name", "@route,@route2", "user", "", TRUE },
+ { NULL, "Display Name", "@route,@route2", "user", "MISSING_DOMAIN", TRUE }, 0 },
+ { "Display Name <@route@route2:\"\"@domain>", "\"Display Name\" <@route,@route2:\"\"@domain>", NULL,
+ { NULL, "Display Name", "@route,@route2", "", "domain", FALSE },
+ { NULL, "Display Name", "@route,@route2", "", "domain", FALSE }, 0 },
+
+ /* other tests: */
+ { "\"foo: <a@b>;,\" <user@domain>", NULL, NULL,
+ { NULL, "foo: <a@b>;,", NULL, "user", "domain", FALSE },
+ { NULL, "foo: <a@b>;,", NULL, "user", "domain", FALSE }, 0 },
+ { "<>", "", "<MISSING_MAILBOX@MISSING_DOMAIN>",
+ { NULL, NULL, NULL, "", "", TRUE },
+ { NULL, NULL, NULL, "MISSING_MAILBOX", "MISSING_DOMAIN", TRUE }, 0 },
+ { "<@>", "", "<INVALID_ROUTE:MISSING_MAILBOX@MISSING_DOMAIN>",
+ { NULL, NULL, NULL, "", "", TRUE },
+ { NULL, NULL, "INVALID_ROUTE", "MISSING_MAILBOX", "MISSING_DOMAIN", TRUE }, 0 },
+
+ /* Test against a out-of-bounds read bug - keep these two tests
+ together in this same order: */
+ { "aaaa@", "<aaaa>", "<aaaa@MISSING_DOMAIN>",
+ { NULL, NULL, NULL, "aaaa", "", TRUE },
+ { NULL, NULL, NULL, "aaaa", "MISSING_DOMAIN", TRUE }, 0 },
+ { "a(aa", "", "<MISSING_MAILBOX@MISSING_DOMAIN>",
+ { NULL, NULL, NULL, "", "", TRUE },
+ { NULL, NULL, NULL, "MISSING_MAILBOX", "MISSING_DOMAIN", TRUE },
+ TEST_MESSAGE_ADDRESS_FLAG_SKIP_LIST },
+ };
+ static struct message_address group_prefix = {
+ NULL, NULL, NULL, "group", NULL, FALSE
+ };
+ static struct message_address group_suffix = {
+ NULL, NULL, NULL, NULL, NULL, FALSE
+ };
+ const struct message_address *addr;
+ string_t *str, *group;
+ const char *wanted_string;
+ unsigned int i;
+
+ test_begin("message address parsing");
+ str = t_str_new(128);
+ group = t_str_new(256);
+
+ for (i = 0; i < N_ELEMENTS(tests)*2; i++) {
+ const struct test *test = &tests[i/2];
+ const struct message_address *test_wanted_addr;
+ bool fill_missing = i%2 != 0;
+
+ test_wanted_addr = !fill_missing ?
+ &test->addr : &test->filled_addr;
+ addr = test_parse_address(test->input, fill_missing);
+ test_assert_idx(addr != NULL && addr->next == NULL &&
+ cmp_addr(addr, test_wanted_addr), i);
+
+ /* test the address alone */
+ str_truncate(str, 0);
+ message_address_write(str, addr);
+ if (fill_missing && test->wanted_filled_output != NULL)
+ wanted_string = test->wanted_filled_output;
+ else if (test->wanted_output != NULL)
+ wanted_string = test->wanted_output;
+ else
+ wanted_string = test->input;
+ test_assert_idx(strcmp(str_c(str), wanted_string) == 0, i);
+
+ if ((test->flags & TEST_MESSAGE_ADDRESS_FLAG_SKIP_LIST) != 0)
+ continue;
+
+ /* test the address as a list of itself */
+ for (unsigned int list_length = 2; list_length <= 5; list_length++) {
+ str_truncate(group, 0);
+ str_append(group, test->input);
+ for (unsigned int j = 1; j < list_length; j++) {
+ if ((j % 2) == 0)
+ str_append(group, ",");
+ else
+ str_append(group, " , \n ");
+ str_append(group, test->input);
+ }
+
+ addr = test_parse_address(str_c(group), fill_missing);
+ for (unsigned int j = 0; j < list_length; j++) {
+ test_assert_idx(addr != NULL &&
+ cmp_addr(addr, test_wanted_addr), i);
+ if (addr != NULL)
+ addr = addr->next;
+ }
+ test_assert_idx(addr == NULL, i);
+ }
+
+ /* test the address as a group of itself */
+ for (unsigned int list_length = 1; list_length <= 5; list_length++) {
+ str_truncate(group, 0);
+ str_printfa(group, "group: %s", test->input);
+ for (unsigned int j = 1; j < list_length; j++) {
+ if ((j % 2) == 0)
+ str_append(group, ",");
+ else
+ str_append(group, " , \n ");
+ str_append(group, test->input);
+ }
+ str_append_c(group, ';');
+
+ addr = test_parse_address(str_c(group), fill_missing);
+ test_assert(addr != NULL && cmp_addr(addr, &group_prefix));
+ addr = addr->next;
+ for (unsigned int j = 0; j < list_length; j++) {
+ test_assert_idx(addr != NULL &&
+ cmp_addr(addr, test_wanted_addr), i);
+ if (addr != NULL)
+ addr = addr->next;
+ }
+ test_assert_idx(addr != NULL && addr->next == NULL &&
+ cmp_addr(addr, &group_suffix), i);
+ }
+ }
+ test_end();
+
+ test_begin("message address parsing with empty group");
+ str_truncate(group, 0);
+ str_append(group, "group:;");
+ addr = test_parse_address(str_c(group), FALSE);
+ str_truncate(str, 0);
+ message_address_write(str, addr);
+ test_assert(addr != NULL && cmp_addr(addr, &group_prefix));
+ addr = addr->next;
+ test_assert(addr != NULL && addr->next == NULL &&
+ cmp_addr(addr, &group_suffix));
+ test_assert(strcmp(str_c(str), "group:;") == 0);
+ test_end();
+
+ test_begin("message address parsing empty string");
+ test_assert(message_address_parse(unsafe_data_stack_pool, &uchar_nul, 0, 10,
+ MESSAGE_ADDRESS_PARSE_FLAG_FILL_MISSING) == NULL);
+ str_truncate(str, 0);
+ message_address_write(str, NULL);
+ test_assert(str_len(str) == 0);
+ test_end();
+}
+
+static void test_message_address_nuls(void)
+{
+ const unsigned char input[] =
+ "\"user\0nuls\\\0-esc\"@[domain\0nuls\\\0-esc] (comment\0nuls\\\0-esc)";
+ const struct message_address output = {
+ NULL, "comment\xEF\xBF\xBDnuls\\\xEF\xBF\xBD-esc", NULL,
+ "user\xEF\xBF\xBDnuls\\\xEF\xBF\xBD-esc",
+ "[domain\xEF\xBF\xBDnuls\\\xEF\xBF\xBD-esc]", FALSE
+ };
+ const struct message_address *addr;
+
+ test_begin("message address parsing with NULs");
+ addr = message_address_parse(pool_datastack_create(),
+ input, sizeof(input)-1, UINT_MAX, 0);
+ test_assert(addr != NULL && cmp_addr(addr, &output));
+ test_end();
+}
+
+static void test_message_address_nuls_display_name(void)
+{
+ const unsigned char input[] =
+ "\"displayname\0nuls\\\0-esc\" <\"user\0nuls\\\0-esc\"@[domain\0nuls\\\0-esc]>";
+ const struct message_address output = {
+ NULL, "displayname\xEF\xBF\xBDnuls\\\xEF\xBF\xBD-esc", NULL,
+ "user\xEF\xBF\xBDnuls\\\xEF\xBF\xBD-esc",
+ "[domain\xEF\xBF\xBDnuls\\\xEF\xBF\xBD-esc]", FALSE
+ };
+ const struct message_address *addr;
+
+ test_begin("message address parsing with NULs in display-name");
+ addr = message_address_parse(pool_datastack_create(),
+ input, sizeof(input)-1, UINT_MAX, 0);
+ test_assert(addr != NULL && cmp_addr(addr, &output));
+ test_end();
+}
+
+static void test_message_address_non_strict_dots(void)
+{
+ const char *const inputs[] = {
+ ".@example.com",
+ "..@example.com",
+ "..foo@example.com",
+ "..foo..@example.com",
+ "..foo..bar..@example.com",
+ };
+ const struct message_address *addr;
+ struct message_address output = {
+ NULL, NULL, NULL, "local-part",
+ "example.com", FALSE
+ };
+
+ test_begin("message address parsing with non-strict dots");
+ for (unsigned int i = 0; i < N_ELEMENTS(inputs); i++) {
+ const unsigned char *addr_input =
+ (const unsigned char *)inputs[i];
+ /* invalid with strict-dots flag */
+ addr = message_address_parse(pool_datastack_create(),
+ addr_input, strlen(inputs[i]), UINT_MAX,
+ MESSAGE_ADDRESS_PARSE_FLAG_STRICT_DOTS);
+ test_assert_idx(addr != NULL && addr->invalid_syntax, i);
+
+ /* valid without the strict-dots flag */
+ addr = message_address_parse(pool_datastack_create(),
+ addr_input, strlen(inputs[i]), UINT_MAX, 0);
+ output.mailbox = t_strcut(inputs[i], '@');
+ test_assert_idx(addr != NULL && cmp_addr(addr, &output), i);
+ }
+ test_end();
+}
+
+static int
+test_parse_path(const char *input, const struct message_address **addr_r)
+{
+ struct message_address *addr;
+ char *input_dup;
+ int ret;
+
+ /* duplicate the input (without trailing NUL) so valgrind notices
+ if there's any out-of-bounds access */
+ size_t input_len = strlen(input);
+ if (input_len > 0)
+ input = input_dup = i_memdup(input, input_len);
+ ret = message_address_parse_path(pool_datastack_create(),
+ (const unsigned char *)input, input_len,
+ &addr);
+ if (input_len > 0)
+ i_free(input_dup);
+ *addr_r = addr;
+ return ret;
+}
+
+static void test_message_address_path(void)
+{
+ static const struct test {
+ const char *input;
+ const char *wanted_output;
+ struct message_address addr;
+ } tests[] = {
+ { "<>", NULL,
+ { NULL, NULL, NULL, NULL, NULL, FALSE } },
+ { " < > ", "<>",
+ { NULL, NULL, NULL, NULL, NULL, FALSE } },
+ { "<user@domain>", NULL,
+ { NULL, NULL, NULL, "user", "domain", FALSE } },
+ { " <user@domain> ", "<user@domain>",
+ { NULL, NULL, NULL, "user", "domain", FALSE } },
+ { "user@domain", "<user@domain>",
+ { NULL, NULL, NULL, "user", "domain", FALSE } },
+ { " user@domain ", "<user@domain>",
+ { NULL, NULL, NULL, "user", "domain", FALSE } },
+ { "<\"user\"@domain>", "<user@domain>",
+ { NULL, NULL, NULL, "user", "domain", FALSE } },
+ { "<\"user name\"@domain>", NULL,
+ { NULL, NULL, NULL, "user name", "domain", FALSE } },
+ { "<\"user@na\\\\me\"@domain>", NULL,
+ { NULL, NULL, NULL, "user@na\\me", "domain", FALSE } },
+ { "<\"user\\\"name\"@domain>", NULL,
+ { NULL, NULL, NULL, "user\"name", "domain", FALSE } },
+ { "<\"\"@domain>", NULL,
+ { NULL, NULL, NULL, "", "domain", FALSE } },
+ { "<@source", "<>",
+ { NULL, NULL, NULL, NULL, NULL, TRUE } },
+ };
+ const struct message_address *addr;
+ string_t *str;
+ const char *wanted_string;
+ unsigned int i;
+
+ test_begin("message address path parsing");
+ str = t_str_new(128);
+
+ for (i = 0; i < N_ELEMENTS(tests); i++) {
+ const struct test *test = &tests[i];
+ const struct message_address *test_wanted_addr;
+ int ret;
+
+ test_wanted_addr = &test->addr;
+ ret = test_parse_path(test->input, &addr);
+ if (addr->invalid_syntax)
+ test_assert_idx(ret == -1, i);
+ else
+ test_assert_idx(ret == 0, i);
+ test_assert_idx(addr != NULL && addr->next == NULL &&
+ cmp_addr(addr, test_wanted_addr), i);
+
+ /* test the address alone */
+ str_truncate(str, 0);
+ message_address_write(str, addr);
+ if (test->wanted_output != NULL)
+ wanted_string = test->wanted_output;
+ else
+ wanted_string = test->input;
+ test_assert_idx(strcmp(str_c(str), wanted_string) == 0, i);
+ }
+ test_end();
+}
+
+static void test_message_address_path_invalid(void)
+{
+ static const char *tests[] = {
+ "",
+ "<",
+ " < ",
+ ">",
+ " > ",
+ "<user@domain",
+ " <user@domain ",
+ "user@domain>",
+ " user@domain> ",
+ "<user>",
+ "<@route@route2:user>",
+ "<@domain>",
+ "@domain",
+ " @domain ",
+ "<user@>",
+ "user@",
+ " user@ ",
+ "<user@domain>bladiebla",
+ "user@domain@"
+ };
+ const struct message_address *addr;
+ unsigned int i;
+
+ test_begin("message address path invalid");
+
+ for (i = 0; i < N_ELEMENTS(tests); i++) {
+ const char *test = tests[i];
+ int ret;
+
+ ret = test_parse_path(test, &addr);
+ test_assert_idx(ret < 0, i);
+ }
+ test_end();
+}
+
+int main(void)
+{
+ static void (*const test_functions[])(void) = {
+ test_message_address,
+ test_message_address_nuls,
+ test_message_address_nuls_display_name,
+ test_message_address_non_strict_dots,
+ test_message_address_path,
+ test_message_address_path_invalid,
+ NULL
+ };
+ return test_run(test_functions);
+}
diff --git a/src/lib-mail/test-message-date.c b/src/lib-mail/test-message-date.c
new file mode 100644
index 0000000..b522ff2
--- /dev/null
+++ b/src/lib-mail/test-message-date.c
@@ -0,0 +1,64 @@
+/* Copyright (c) 2007-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "message-date.h"
+#include "test-common.h"
+
+struct test_message_date {
+ const char *input;
+ time_t time;
+ int tz_offset;
+ bool ret;
+};
+
+static void test_message_date_parse(void)
+{
+ static const struct test_message_date tests[] = {
+#ifdef TIME_T_SIGNED
+ { "Thu, 01 Jan 1970 01:59:59 +0200", -1, 2*60, TRUE },
+ { "Fri, 13 Dec 1901 20:45:53 +0000", -2147483647, 0, TRUE },
+#endif
+#if (TIME_T_MAX_BITS > 32 || !defined(TIME_T_SIGNED))
+ { "Sun, 07 Feb 2106 06:28:15 +0000", 4294967295U, 0, TRUE },
+#endif
+ { "Wed, 07 Nov 2007 01:07:20 +0200", 1194390440, 2*60, TRUE },
+ { "Wed, 07 Nov 2007 01:07:20", 1194397640, 0, TRUE },
+ { "Thu, 01 Jan 1970 02:00:00 +0200", 0, 2*60, TRUE },
+ { "Tue, 19 Jan 2038 03:14:07 +0000", 2147483647, 0, TRUE },
+ { "Tue, 19 Jan 2038", 0, 0, FALSE },
+ /* June leap second */
+ { "Tue, 30 Jun 2015 23:59:59 +0300", 1435697999, 3*60, TRUE },
+ { "Tue, 30 Jun 2015 23:59:60 +0300", 1435697999, 3*60, TRUE },
+ { "Wed, 01 Jul 2015 00:00:00 +0300", 1435698000, 3*60, TRUE },
+ /* Invalid leap second */
+ { "Tue, 24 Jan 2017 15:59:60 +0300", 1485262799, 3*60, TRUE },
+ /* December leap second */
+ { "Sat, 31 Dec 2016 23:59:59 +0200", 1483221599, 2*60, TRUE },
+ { "Sat, 31 Dec 2016 23:59:60 +0200", 1483221599, 2*60, TRUE },
+ { "Sun, 01 Jan 2017 00:00:00 +0200", 1483221600, 2*60, TRUE },
+ };
+ unsigned int i;
+ bool success;
+ time_t t;
+ int tz;
+ bool ret;
+
+ for (i = 0; i < N_ELEMENTS(tests); i++) {
+ const struct test_message_date *test = &tests[i];
+ ret = message_date_parse((const unsigned char *)test->input,
+ strlen(test->input), &t, &tz);
+ success = (!ret && !test->ret) ||
+ (ret == test->ret && t == test->time &&
+ tz == test->tz_offset);
+ test_out(t_strdup_printf("message_date_parse(%d)", i), success);
+ }
+}
+
+int main(void)
+{
+ static void (*const test_functions[])(void) = {
+ test_message_date_parse,
+ NULL
+ };
+ return test_run(test_functions);
+}
diff --git a/src/lib-mail/test-message-decoder.c b/src/lib-mail/test-message-decoder.c
new file mode 100644
index 0000000..edf9210
--- /dev/null
+++ b/src/lib-mail/test-message-decoder.c
@@ -0,0 +1,513 @@
+/* Copyright (c) 2007-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "str.h"
+#include "istream.h"
+#include "charset-utf8.h"
+#include "message-parser.h"
+#include "message-header-decode.h"
+#include "message-decoder.h"
+#include "message-part-data.h"
+#include "test-common.h"
+
+void message_header_decode_utf8(const unsigned char *data, size_t size,
+ buffer_t *dest,
+ normalizer_func_t *normalizer ATTR_UNUSED)
+{
+ buffer_append(dest, data, size);
+}
+
+static void test_message_decoder(void)
+{
+ struct message_decoder_context *ctx;
+ struct message_part part;
+ struct message_header_line hdr;
+ struct message_block input, output;
+
+ test_begin("message decoder");
+
+ i_zero(&part);
+ i_zero(&input);
+ memset(&output, 0xff, sizeof(output));
+ input.part = &part;
+
+ ctx = message_decoder_init(NULL, 0);
+
+ i_zero(&hdr);
+ hdr.name = "Content-Transfer-Encoding";
+ hdr.name_len = strlen(hdr.name);
+ hdr.full_value = (const void *)"quoted-printable";
+ hdr.full_value_len = strlen((const char *)hdr.full_value);
+ input.hdr = &hdr;
+ test_assert(message_decoder_decode_next_block(ctx, &input, &output));
+ test_assert(output.size == 0);
+
+ input.hdr = NULL;
+ test_assert(message_decoder_decode_next_block(ctx, &input, &output));
+
+ input.hdr = NULL;
+ test_assert(message_decoder_decode_next_block(ctx, &input, &output));
+
+ input.data = (const void *)"foo ";
+ input.size = strlen((const char *)input.data);
+ test_assert(message_decoder_decode_next_block(ctx, &input, &output));
+ test_assert(output.size == 3);
+ test_assert(memcmp(output.data, "foo", 3) == 0);
+
+ input.data = (const void *)"bar";
+ input.size = strlen((const char *)input.data);
+ test_assert(message_decoder_decode_next_block(ctx, &input, &output));
+ test_assert(output.size == 14);
+ test_assert(memcmp(output.data, " bar", 14) == 0);
+
+ /* partial text - \xC3\xA4 in quoted-printable. we should get a single
+ UTF-8 letter as result */
+ input.data = (const void *)"="; input.size = 1;
+ test_assert(message_decoder_decode_next_block(ctx, &input, &output));
+ test_assert(output.size == 0);
+ input.data = (const void *)"C"; input.size = 1;
+ test_assert(message_decoder_decode_next_block(ctx, &input, &output));
+ test_assert(output.size == 0);
+ input.data = (const void *)"3"; input.size = 1;
+ test_assert(message_decoder_decode_next_block(ctx, &input, &output));
+ test_assert(output.size == 0);
+ input.data = (const void *)"=A"; input.size = 2;
+ test_assert(message_decoder_decode_next_block(ctx, &input, &output));
+ test_assert(output.size == 0);
+ input.data = (const void *)"4"; input.size = 1;
+ test_assert(message_decoder_decode_next_block(ctx, &input, &output));
+ test_assert(output.size == 2);
+ test_assert(memcmp(output.data, "\xC3\xA4", 2) == 0);
+
+ message_decoder_deinit(&ctx);
+
+ test_end();
+}
+
+static void test_message_decoder_multipart(void)
+{
+ static const char test_message_input[] =
+ "Content-Type: multipart/mixed; boundary=foo\n"
+ "\n"
+ "--foo\n"
+ "Content-Transfer-Encoding: quoted-printable\n"
+ "Content-Type: text/plain; charset=utf-8\n"
+ "\n"
+ "p=C3=A4iv=C3=A4=C3=A4\n"
+ "\n"
+ "--foo\n"
+ "Content-Transfer-Encoding: base64\n"
+ "Content-Type: text/plain; charset=utf-8\n"
+ "\n"
+ "ecO2dMOkIHZhYW4uCg== ignored\n"
+ "--foo\n"
+ "Content-Transfer-Encoding: base64\n"
+ "Content-Type: text/plain; charset=utf-8\n"
+ "\n"
+ "?garbage\n"
+ "--foo--\n";
+ const struct message_parser_settings parser_set = { .flags = 0, };
+ struct message_parser_ctx *parser;
+ struct message_decoder_context *decoder;
+ struct message_part *parts;
+ struct message_block input, output;
+ struct istream *istream;
+ string_t *str_out = t_str_new(20);
+ int ret;
+
+ test_begin("message decoder multipart");
+
+ istream = test_istream_create(test_message_input);
+ parser = message_parser_init(pool_datastack_create(), istream, &parser_set);
+ decoder = message_decoder_init(NULL, 0);
+
+ test_istream_set_allow_eof(istream, FALSE);
+ for (size_t i = 0; i < sizeof(test_message_input); i++) {
+ if (i == sizeof(test_message_input)-1)
+ test_istream_set_allow_eof(istream, TRUE);
+ test_istream_set_size(istream, i);
+ while ((ret = message_parser_parse_next_block(parser, &input)) > 0) {
+ if (message_decoder_decode_next_block(decoder, &input, &output) &&
+ output.hdr == NULL && output.size > 0)
+ str_append_data(str_out, output.data, output.size);
+ }
+ if (i == sizeof(test_message_input)-1)
+ test_assert(ret == -1);
+ else
+ test_assert(ret == 0);
+ }
+ /* NOTE: qp-decoder decoder changes \n into \r\n */
+ test_assert_strcmp(str_c(str_out), "p\xC3\xA4iv\xC3\xA4\xC3\xA4\r\ny\xC3\xB6t\xC3\xA4 vaan.\n");
+
+ message_decoder_deinit(&decoder);
+ message_parser_deinit(&parser, &parts);
+ test_assert(istream->stream_errno == 0);
+ i_stream_unref(&istream);
+ test_end();
+}
+
+static void test_message_decoder_current_content_type(void)
+{
+ struct message_decoder_context *ctx;
+ struct message_part part, part2, part3;
+ struct message_header_line hdr;
+ struct message_block input, output;
+
+ test_begin("message_decoder_current_content_type()");
+
+ i_zero(&part);
+ part2 = part3 = part;
+
+ i_zero(&input);
+ memset(&output, 0xff, sizeof(output));
+ input.part = &part;
+
+ ctx = message_decoder_init(NULL, 0);
+ test_assert(message_decoder_current_content_type(ctx) == NULL);
+
+ /* multipart/mixed */
+ i_zero(&hdr);
+ hdr.name = "Content-Type";
+ hdr.name_len = strlen(hdr.name);
+ hdr.full_value = (const void *)"multipart/mixed; boundary=x";
+ hdr.full_value_len = strlen((const char *)hdr.full_value);
+ input.hdr = &hdr;
+ test_assert(message_decoder_decode_next_block(ctx, &input, &output));
+
+ input.hdr = NULL;
+ test_assert(message_decoder_decode_next_block(ctx, &input, &output));
+ test_assert(strcmp(message_decoder_current_content_type(ctx), "multipart/mixed") == 0);
+
+ /* child 1 */
+ input.part = &part2;
+ hdr.full_value = (const void *)"text/plain";
+ hdr.full_value_len = strlen((const char *)hdr.full_value);
+ input.hdr = &hdr;
+ test_assert(message_decoder_decode_next_block(ctx, &input, &output));
+
+ input.hdr = NULL;
+ test_assert(message_decoder_decode_next_block(ctx, &input, &output));
+ test_assert(strcmp(message_decoder_current_content_type(ctx), "text/plain") == 0);
+
+ /* child 2 */
+ input.part = &part3;
+ hdr.full_value = (const void *)"application/pdf";
+ hdr.full_value_len = strlen((const char *)hdr.full_value);
+ input.hdr = &hdr;
+ test_assert(message_decoder_decode_next_block(ctx, &input, &output));
+
+ input.hdr = NULL;
+ test_assert(message_decoder_decode_next_block(ctx, &input, &output));
+ test_assert(strcmp(message_decoder_current_content_type(ctx), "application/pdf") == 0);
+
+ /* reset */
+ message_decoder_decode_reset(ctx);
+ test_assert(message_decoder_current_content_type(ctx) == NULL);
+
+ message_decoder_deinit(&ctx);
+
+ test_end();
+}
+
+static void test_message_decoder_content_transfer_encoding(void)
+{
+ static const unsigned char test_message_input[] =
+"Content-Type: multipart/mixed; boundary=\"1\"\n"
+"MIME-Version: 1.0\n\n"
+"--1\n"
+"Content-Transfer-Encoding: 7bit\n"
+"Content-Type: text/plain; charset=us-ascii\n\n"
+"Move black king to queen's bishop\n\n"
+"--1\n"
+"Content-Transfer-Encoding:\t\t\t\tbinary\n"
+"Content-Type: text/plain; charset=UTF-8\n\n"
+"Move \xE2\x99\x9A to \xE2\x99\x9B's \xE2\x99\x9D\n\n"
+"--1\n"
+"Content-Transfer-Encoding: 8bit\t\t\t\r\n"
+"Content-Type: text/plain; charset=UTF-8\n\n"
+"Move \xE2\x99\x9A to \xE2\x99\x9B's \xE2\x99\x9D\n\n"
+"--1\n"
+"Content-Transfer-Encoding: quoted-printable \r\n"
+"Content-Type: text/plain; charset=UTF-8\n\n"
+"Move =E2=99=9A to =E2=99=9B's =E2=99=9D\n\n"
+"--1\n"
+"Content-Transfer-Encoding: base64\n"
+"Content-Type: text/plain; charset=UTF-8\n\n"
+"TW92ZSDimZogdG8g4pmbJ3Mg4pmdCg==\n\n"
+"--1--\n";
+
+ static const char test_message_output[] =
+"Move black king to queen's bishop\n"
+"Move \xE2\x99\x9A to \xE2\x99\x9B's \xE2\x99\x9D\n"
+"Move \xE2\x99\x9A to \xE2\x99\x9B's \xE2\x99\x9D\n"
+"Move \xE2\x99\x9A to \xE2\x99\x9B's \xE2\x99\x9D\r\n"
+"Move \xE2\x99\x9A to \xE2\x99\x9B's \xE2\x99\x9D\n";
+
+ test_begin("message decoder content transfer encoding");
+
+ const struct message_parser_settings parser_set = { .flags = 0, };
+ struct message_parser_ctx *parser;
+ struct message_decoder_context *decoder;
+ struct message_part *parts, *part;
+ struct message_block input, output;
+ struct istream *istream;
+ string_t *str_out = t_str_new(20);
+ int ret;
+
+ pool_t pool = pool_alloconly_create("message parser", 10240);
+ istream = test_istream_create_data(test_message_input,
+ sizeof(test_message_input)-1);
+ parser = message_parser_init(pool, istream, &parser_set);
+ decoder = message_decoder_init(NULL, 0);
+
+ while ((ret = message_parser_parse_next_block(parser, &input)) > 0) {
+ message_part_data_parse_from_header(pool, input.part, input.hdr);
+ if (message_decoder_decode_next_block(decoder, &input, &output) &&
+ output.hdr == NULL && output.size > 0)
+ str_append_data(str_out, output.data, output.size);
+ }
+
+ test_assert(ret == -1);
+ test_assert_strcmp(test_message_output, str_c(str_out));
+ message_decoder_deinit(&decoder);
+ message_parser_deinit(&parser, &parts);
+ test_assert(istream->stream_errno == 0);
+
+ /* validate parts */
+
+ part = parts;
+ test_assert(part->children_count == 5);
+ part = part->children;
+ test_assert_strcmp(part->data->content_type, "text");
+ test_assert_strcmp(part->data->content_subtype, "plain");
+ test_assert_strcmp(part->data->content_transfer_encoding, "7bit");
+ test_assert_strcmp(part->data->content_type, "text");
+
+ part = part->next;
+ test_assert_strcmp(part->data->content_transfer_encoding, "binary");
+ test_assert_strcmp(part->data->content_type, "text");
+ test_assert_strcmp(part->data->content_subtype, "plain");
+ part = part->next;
+ test_assert_strcmp(part->data->content_transfer_encoding, "8bit");
+ part = part->next;
+ test_assert_strcmp(part->data->content_transfer_encoding, "quoted-printable");
+ part = part->next;
+ test_assert_strcmp(part->data->content_transfer_encoding, "base64");
+ i_stream_unref(&istream);
+ pool_unref(&pool);
+ test_end();
+}
+
+static void test_message_decoder_invalid_content_transfer_encoding(void)
+{
+ static const unsigned char test_message_input[] =
+ /* all of the child parts have invalid content transfer encoding */
+"Content-Type: multipart/mixed; boundary=\"1\"\n"
+"MIME-Version: 1.0\n\n"
+"--1\n"
+"Content-Transfer-Encoding: 6bit\n"
+"Content-Type: text/plain; charset=UTF-8\n\n"
+"Move black king to queen's bishop\n\n"
+"--1\n"
+"Content-Transfer-Encoding: 7bits\n"
+"Content-Type: text/plain; charset=UTF-8\n\n"
+"Move \xE2\x99\x9A to \xE2\x99\x9B's \xE2\x99\x9D\n\n"
+"--1\n"
+"Content-Transfer-Encoding: 8 bit\n"
+"Content-Type: text/plain; charset=UTF-8\n\n"
+"Move \xE2\x99\x9A to \xE2\x99\x9B's \xE2\x99\x9D\n\n"
+"--1\n"
+"Content-Transfer-Encoding: 7-bit\n"
+"Content-Type: text/plain; charset=UTF-8\n\n"
+"Move \xE2\x99\x9A to \xE2\x99\x9B's \xE2\x99\x9D\n\n"
+"--1\n"
+"Content-Transfer-Encoding: 8-bit\n"
+"Content-Type: text/plain; charset=UTF-8\n\n"
+"Move \xE2\x99\x9A to \xE2\x99\x9B's \xE2\x99\x9D\n\n"
+"--1\n"
+"Content-Transfer-Encoding:\n"
+"Content-Type: text/plain; charset=UTF-8\n\n"
+"Move =E2=99=9A to =E2=99=9B's =E2=99=9D\n\n"
+"--1--\n";
+
+ const char *test_message_output = "";
+
+ test_begin("message decoder content transfer invalid encoding");
+
+ const struct message_parser_settings parser_set = { .flags = 0 };
+ struct message_parser_ctx *parser;
+ struct message_decoder_context *decoder;
+ struct message_part *parts, *part;
+ struct message_block input, output;
+ struct istream *istream;
+ string_t *str_out = t_str_new(20);
+ int ret;
+
+ pool_t pool = pool_alloconly_create("message parser", 10240);
+ istream = test_istream_create_data(test_message_input,
+ sizeof(test_message_input)-1);
+ parser = message_parser_init(pool, istream, &parser_set);
+ decoder = message_decoder_init(NULL, 0);
+
+ while ((ret = message_parser_parse_next_block(parser, &input)) > 0) {
+ message_part_data_parse_from_header(pool, input.part, input.hdr);
+ if (input.hdr != NULL &&
+ strcasecmp(input.hdr->name, "content-transfer-encoding") == 0) {
+ enum message_cte cte = message_decoder_parse_cte(input.hdr);
+ test_assert(cte == MESSAGE_CTE_UNKNOWN);
+ }
+ if (message_decoder_decode_next_block(decoder, &input, &output) &&
+ output.hdr == NULL && output.size > 0)
+ str_append_data(str_out, output.data, output.size);
+ }
+
+ test_assert(ret == -1);
+ test_assert_strcmp(test_message_output, str_c(str_out));
+ message_decoder_deinit(&decoder);
+ message_parser_deinit(&parser, &parts);
+ test_assert(istream->stream_errno == 0);
+
+ part = parts;
+ test_assert(part->children_count == 6);
+ part = part->children;
+ test_assert_strcmp(part->data->content_type, "text");
+ test_assert_strcmp(part->data->content_subtype, "plain");
+ test_assert_strcmp(part->data->content_transfer_encoding, "6bit");
+ test_assert_strcmp(part->data->content_type, "text");
+
+ part = part->next;
+ test_assert_strcmp(part->data->content_transfer_encoding, "7bits");
+ test_assert_strcmp(part->data->content_type, "text");
+ test_assert_strcmp(part->data->content_subtype, "plain");
+ part = part->next;
+ test_assert(part->data->content_transfer_encoding == NULL);
+ part = part->next;
+ test_assert_strcmp(part->data->content_transfer_encoding, "7-bit");
+ part = part->next;
+ test_assert_strcmp(part->data->content_transfer_encoding, "8-bit");
+ part = part->next;
+ test_assert(part->next == NULL);
+ i_stream_unref(&istream);
+ pool_unref(&pool);
+
+#define X10(a) a a a a a a a a a a
+
+#undef TEST_CASE
+#define TEST_CASE(value, result) \
+ { \
+ .hdr = { \
+ .name = "Content-Transfer-Encoding", \
+ .name_len = 25, \
+ .full_value = (const unsigned char*)value, \
+ .full_value_len = sizeof(value)-1, \
+ }, \
+ .cte = result, \
+ }
+
+ const struct {
+ const struct message_header_line hdr;
+ enum message_cte cte;
+ } test_case[] = {
+ TEST_CASE("(binary comment) base64", MESSAGE_CTE_BASE64),
+ TEST_CASE("(\"binary\" ( (comment) test) ) base64", MESSAGE_CTE_BASE64),
+ TEST_CASE("base64 binary", MESSAGE_CTE_UNKNOWN),
+ TEST_CASE("base64\0binary", MESSAGE_CTE_UNKNOWN),
+ TEST_CASE("\0binary", MESSAGE_CTE_UNKNOWN),
+ TEST_CASE("( " X10(X10(X10(X10("a")))) " ) base64", MESSAGE_CTE_BASE64),
+ TEST_CASE("( " X10(X10(X10(X10("a")))) " ) base64 ( " X10(X10(X10(X10("a")))) ")", MESSAGE_CTE_BASE64),
+ TEST_CASE("( base64", MESSAGE_CTE_UNKNOWN),
+ TEST_CASE("base64 (", MESSAGE_CTE_BASE64),
+ TEST_CASE(X10(X10(X10(X10(" ")))) " base64", MESSAGE_CTE_BASE64),
+ TEST_CASE("base64 ; logging-type=\"foobar\"", MESSAGE_CTE_BASE64),
+ };
+
+ for (size_t i = 0; i < N_ELEMENTS(test_case); i++) {
+ test_assert_idx(message_decoder_parse_cte(&test_case[i].hdr) == test_case[i].cte, i);
+ }
+
+ test_end();
+}
+
+static void test_message_decoder_charset(void)
+{
+ /* ensure we decode correctly */
+ static const unsigned char test_message_input[] =
+ /* none of these should work */
+"Content-Type: multipart/mixed; boundary=\"1\"\n"
+"MIME-Version: 1.0\n\n"
+"--1\n"
+"Content-Transfer-Encoding: binary\n"
+"Content-Type: text/plain; charset=utf-16le\n\n"
+"\x54\x00\x65\x00\x73\x00\x74\x00\x20\x00\x6d\x00\x65\x00\x73\x00\x73\x00\x61\x00\x67\x00\x65\x00\n\x00\n"
+"--1\n"
+"Content-Transfer-Encoding: base64\n"
+"Content-Type: text/plain; charset=utf-16be\n\n"
+"AFQAZQBzAHQAIABtAGUAcwBzAGEAZwBlAAo=\n\n"
+"--1\n"
+"Content-Transfer-Encoding: base64\n"
+"Content-Type: text/plain; charset=utf-16le\n\n"
+"VABlAHMAdAAgAG0AZQBzAHMAYQBnAGUACgA=\n\n"
+"--1\n"
+"Content-Transfer-Encoding: base64\n"
+"Content-Type: text/plain; charset=EUC-JP\n\n"
+"odjApLOmv824osDruMCh2Q==\n\n"
+"--1\n"
+"Content-Transfer-Encoding: binary\n"
+"Content-Type: text/plain; charset=UTF-8\n\n"
+"\xad\xad\xad\xad\xad\xad\n"
+"--1--\n";
+
+ static const char *test_message_output =
+"Test message\nTest message\nTest message\n"
+"\xe3\x80\x8e\xe4\xb8\x96\xe7\x95\x8c\xe4\xba\xba"
+"\xe6\xa8\xa9\xe5\xae\xa3\xe8\xa8\x80\xe3\x80\x8f"
+UNICODE_REPLACEMENT_CHAR_UTF8;
+
+ test_begin("message decoder charset");
+
+ const struct message_parser_settings parser_set = { .flags = 0, };
+ struct message_parser_ctx *parser;
+ struct message_decoder_context *decoder;
+ struct message_part *parts;
+ struct message_block input, output;
+ struct istream *istream;
+ string_t *str_out = t_str_new(20);
+ int ret;
+
+ pool_t pool = pool_alloconly_create("message parser", 10240);
+ istream = test_istream_create_data(test_message_input,
+ sizeof(test_message_input)-1);
+ parser = message_parser_init(pool, istream, &parser_set);
+ decoder = message_decoder_init(NULL, 0);
+
+ while ((ret = message_parser_parse_next_block(parser, &input)) > 0) {
+ message_part_data_parse_from_header(pool, input.part, input.hdr);
+ if (message_decoder_decode_next_block(decoder, &input, &output) &&
+ output.hdr == NULL && output.size > 0)
+ str_append_data(str_out, output.data, output.size);
+ }
+
+ test_assert(ret == -1);
+ test_assert_strcmp(test_message_output, str_c(str_out));
+ message_decoder_deinit(&decoder);
+ message_parser_deinit(&parser, &parts);
+ test_assert(istream->stream_errno == 0);
+
+ i_stream_unref(&istream);
+ pool_unref(&pool);
+ test_end();
+}
+
+int main(void)
+{
+ static void (*const test_functions[])(void) = {
+ test_message_decoder,
+ test_message_decoder_multipart,
+ test_message_decoder_current_content_type,
+ test_message_decoder_content_transfer_encoding,
+ test_message_decoder_invalid_content_transfer_encoding,
+ test_message_decoder_charset,
+ NULL
+ };
+ return test_run(test_functions);
+}
diff --git a/src/lib-mail/test-message-header-decode.c b/src/lib-mail/test-message-header-decode.c
new file mode 100644
index 0000000..0221d1a
--- /dev/null
+++ b/src/lib-mail/test-message-header-decode.c
@@ -0,0 +1,216 @@
+/* Copyright (c) 2009-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "buffer.h"
+#include "str.h"
+#include "randgen.h"
+#include "charset-utf8.h"
+#include "message-header-encode.h"
+#include "message-header-decode.h"
+#include "test-common.h"
+
+static void test_message_header_decode(void)
+{
+ static const char *data[] = {
+ " \t=?utf-8?q?=c3=a4?= =?utf-8?q?=c3=a4?= b \t\r\n ", "\xC3\xA4\xC3\xA4 b \t\r\n ",
+ "a =?utf-8?q?=c3=a4?= b", "a \xC3\xA4 b",
+ "a =?utf-8?q?=c3=a4?= b", "a \xC3\xA4 b",
+ "a =?utf-8?q?=c3=a4?=\t\t\r\n =?utf-8?q?=c3=a4?= b", "a \xC3\xA4\xC3\xA4 b",
+ "a =?utf-8?q?=c3=a4?= x =?utf-8?q?=c3=a4?= b", "a \xC3\xA4 x \xC3\xA4 b",
+ "a =?utf-8?b?w6TDpCDDpA==?= b", "a \xC3\xA4\xC3\xA4 \xC3\xA4 b",
+ "=?utf-8?b?w6Qgw6Q=?=", "\xC3\xA4 \xC3\xA4",
+ "a =?utf-8?b?////?= b", "a "UNICODE_REPLACEMENT_CHAR_UTF8" b",
+ "a =?utf-16le?b?UADkAGkAdgDkAOQA?= b", "a P\xC3\xA4iv\xC3\xA4\xC3\xA4 b",
+ "a =?utf-9?b?UMOkaXbDpMOk?= b", "a P\xC3\xA4iv\xC3\xA4\xC3\xA4 b",
+
+ };
+ string_t *dest;
+ unsigned int i;
+
+ test_begin("message header decode");
+
+ dest = t_str_new(256);
+ for (i = 0; i < N_ELEMENTS(data); i += 2) {
+ str_truncate(dest, 0);
+ message_header_decode_utf8((const unsigned char *)data[i],
+ strlen(data[i]), dest, NULL);
+ test_assert_strcmp_idx(str_c(dest), data[i+1], i / 2);
+ }
+ test_end();
+}
+
+static void test_message_header_decode_read_overflow(void)
+{
+ const unsigned char input[] = "=?utf-8?Q?=EF?=";
+ string_t *dest = t_str_new(32);
+
+ test_begin("message header decode read overflow");
+ message_header_decode_utf8(input, sizeof(input)-2, dest, NULL);
+ test_end();
+}
+
+static void check_encoded(string_t *encoded, unsigned int test_idx)
+{
+ const unsigned char *enc = str_data(encoded), *p, *pend;
+ size_t enc_len = str_len(encoded), cur_line_len = 0;
+
+ p = enc;
+ pend = enc + enc_len;
+ while (p < pend) {
+ if (*p == '\r') {
+ p++;
+ continue;
+ }
+ if (*p == '\n') {
+ test_assert_idx(cur_line_len <= 76, test_idx);
+ cur_line_len = 0;
+ p++;
+ continue;
+ }
+ cur_line_len++;
+ test_assert_idx((*p >= 0x20 && *p <= 0x7e) || *p == '\t',
+ test_idx);
+ p++;
+ }
+
+ test_assert_idx(cur_line_len <= 76, test_idx);
+}
+
+static void
+check_encode_decode_result(const unsigned char *inbuf, size_t inbuf_len,
+ string_t *out, unsigned int test_idx)
+{
+ static const unsigned char *rep_char =
+ (const unsigned char *)UNICODE_REPLACEMENT_CHAR_UTF8;
+ static const ptrdiff_t rep_char_len =
+ UNICODE_REPLACEMENT_CHAR_UTF8_LEN;
+ const unsigned char *outbuf = str_data(out);
+ size_t outbuf_len = str_len(out);
+ const unsigned char *pin, *pinend, *pout, *poutend;
+ bool invalid_char = FALSE;
+
+ if (test_has_failed())
+ return;
+
+ pin = inbuf;
+ pinend = inbuf + inbuf_len;
+ pout = outbuf;
+ poutend = outbuf + outbuf_len;
+
+ while (pin < pinend) {
+ unichar_t ch;
+ int nch;
+
+ nch = uni_utf8_get_char_n(pin, pinend - pin, &ch);
+ if (nch <= 0) {
+ /* Invalid character; check proper substitution of
+ replacement character in encoded/decoded output. */
+ pin++;
+ if (!invalid_char) {
+ /* Only one character is substituted for a run
+ of bad stuff. */
+ test_assert_idx(
+ (poutend - pout) >= rep_char_len &&
+ memcmp(pout, rep_char,
+ rep_char_len) == 0, test_idx);
+ pout += rep_char_len;
+ }
+ invalid_char = TRUE;
+ } else {
+ /* Valid character; check matching character bytes. */
+ invalid_char = FALSE;
+ test_assert_idx((pinend - pin) >= nch &&
+ (poutend - pout) >= nch &&
+ memcmp(pin, pout, nch) == 0, test_idx);
+ pin += nch;
+ pout += nch;
+ }
+
+ if (test_has_failed())
+ return;
+ }
+
+ /* Both buffers must have reached the end now. */
+ test_assert_idx(pin == pinend && pout == poutend, test_idx);
+}
+
+static void test_message_header_decode_encode_random(void)
+{
+ string_t *encoded, *decoded;
+ unsigned char buf[1024];
+ unsigned int i, j, buflen;
+
+ test_begin("message header encode & decode randomly (7 bit)");
+
+ encoded = t_str_new(256);
+ decoded = t_str_new(256);
+ for (i = 0; i < 1000; i++) {
+ /* fill only with 7bit data so we don't have to worry about
+ the data being valid UTF-8 */
+ buflen = i_rand_limit(sizeof(buf));
+ for (j = 0; j < buflen; j++)
+ buf[j] = i_rand_limit(128);
+
+ str_truncate(encoded, 0);
+ str_truncate(decoded, 0);
+
+ /* test Q */
+ message_header_encode_q(buf, buflen, encoded, 0);
+ check_encoded(encoded, i);
+ message_header_decode_utf8(encoded->data, encoded->used,
+ decoded, NULL);
+ test_assert_idx(decoded->used == buflen &&
+ memcmp(decoded->data, buf, buflen) == 0, i);
+
+ /* test B */
+ str_truncate(encoded, 0);
+ str_truncate(decoded, 0);
+
+ message_header_encode_b(buf, buflen, encoded, 0);
+ check_encoded(encoded, i);
+ message_header_decode_utf8(encoded->data, encoded->used,
+ decoded, NULL);
+ test_assert_idx(decoded->used == buflen &&
+ memcmp(decoded->data, buf, buflen) == 0, i);
+ }
+ test_end();
+
+ test_begin("message header encode & decode randomly (8 bit)");
+
+ for (i = 0; i < 1000; i++) {
+ buflen = i_rand_limit(sizeof(buf));
+ random_fill(buf, buflen);
+
+ str_truncate(encoded, 0);
+ str_truncate(decoded, 0);
+
+ /* test Q */
+ message_header_encode_q(buf, buflen, encoded, 0);
+ check_encoded(encoded, i);
+ message_header_decode_utf8(encoded->data, encoded->used,
+ decoded, NULL);
+ check_encode_decode_result(buf, buflen, decoded, i);
+
+ /* test B */
+ str_truncate(encoded, 0);
+ str_truncate(decoded, 0);
+
+ message_header_encode_b(buf, buflen, encoded, 0);
+ check_encoded(encoded, i);
+ message_header_decode_utf8(encoded->data, encoded->used,
+ decoded, NULL);
+ check_encode_decode_result(buf, buflen, decoded, i);
+ }
+ test_end();
+}
+
+int main(void)
+{
+ static void (*const test_functions[])(void) = {
+ test_message_header_decode,
+ test_message_header_decode_read_overflow,
+ test_message_header_decode_encode_random,
+ NULL
+ };
+ return test_run(test_functions);
+}
diff --git a/src/lib-mail/test-message-header-encode.c b/src/lib-mail/test-message-header-encode.c
new file mode 100644
index 0000000..b1c5645
--- /dev/null
+++ b/src/lib-mail/test-message-header-encode.c
@@ -0,0 +1,295 @@
+/* Copyright (c) 2009-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "base64.h"
+#include "buffer.h"
+#include "str.h"
+#include "message-header-encode.h"
+#include "test-common.h"
+
+static bool verify_q(const char *str, unsigned int i, bool starts_with_a)
+{
+ unsigned int line_start = i, char_count = 0;
+
+ if (str_begins(str+i, "\n\t")) {
+ i += 2;
+ line_start = i - 1;
+ }
+
+ for (;;) {
+ if (!str_begins(str+i, "=?utf-8?q?"))
+ return FALSE;
+ i += 10;
+
+ if (starts_with_a) {
+ if (str[i] != 'a')
+ return FALSE;
+ starts_with_a = FALSE;
+ i++;
+ }
+ while (!str_begins(str+i, "?=")) {
+ if (!str_begins(str+i, "=C3=A4"))
+ return FALSE;
+ i += 6;
+ char_count++;
+ }
+ i += 2;
+ if (i - line_start > 76)
+ return FALSE;
+
+ if (str[i] == '\0')
+ break;
+ if (!str_begins(str+i, "\n\t"))
+ return FALSE;
+ i += 2;
+ line_start = i - 1;
+ }
+ return char_count == 40;
+}
+
+static void test_message_header_encode_q(void)
+{
+ string_t *input = t_str_new(100);
+ string_t *str = t_str_new(512);
+ unsigned int i, j, skip;
+
+ test_begin("message header encode q");
+
+ str_append_c(input, 'a');
+ for (i = 0; i < 40; i++)
+ str_append(input, "\xC3\xA4");
+ for (i = 0; i < 80; i++) {
+ for (skip = 0; skip < 2; skip++) {
+ str_truncate(str, 0);
+ for (j = 1; j < i; j++)
+ str_append_c(str, 'X');
+ if (i != 0)
+ str_append_c(str, ' ');
+
+ message_header_encode_q(str_data(input) + skip,
+ str_len(input) - skip, str,
+ i == 0 ? 0 : i+1);
+ test_assert(verify_q(str_c(str), i, skip == 0));
+ }
+ }
+ test_end();
+}
+
+static bool verify_b(const char *str, unsigned int i, bool starts_with_a)
+{
+ unsigned int line_start = i, start, j, char_count = 0;
+ char bufdata[1000];
+ buffer_t buf;
+
+ buffer_create_from_data(&buf, bufdata, sizeof(bufdata));
+ if (str_begins(str+i, "\n\t")) {
+ i += 2;
+ line_start = i - 1;
+ }
+
+ for (;;) {
+ if (!str_begins(str+i, "=?utf-8?b?"))
+ return FALSE;
+ i += 10;
+
+ start = i;
+ for (; str[i] != '?'; i++) {
+ if (str[i] == '\0')
+ return FALSE;
+ }
+ buffer_set_used_size(&buf, 0);
+ if (base64_decode(str+start, i-start, NULL, &buf) < 0)
+ return FALSE;
+ i++;
+
+ if (!starts_with_a)
+ j = 0;
+ else {
+ if (bufdata[0] != 'a')
+ return FALSE;
+ starts_with_a = FALSE;
+ j = 1;
+ }
+ for (; j < buf.used; j += 2) {
+ if (bufdata[j] != '\xc3' || bufdata[j+1] != '\xa4')
+ return FALSE;
+ char_count++;
+ }
+ if (j != buf.used)
+ return FALSE;
+
+ if (str[i++] != '=')
+ return FALSE;
+
+ if (i - line_start > 76)
+ return FALSE;
+
+ if (str[i] == '\0')
+ break;
+ if (!str_begins(str+i, "\n\t"))
+ return FALSE;
+ i += 2;
+ line_start = i - 1;
+ }
+ return char_count == 40;
+}
+
+static void test_message_header_encode_b(void)
+{
+ string_t *input = t_str_new(100);
+ string_t *str = t_str_new(512);
+ unsigned int i, j, skip;
+
+ test_begin("message header encode b");
+
+ str_append_c(input, 'a');
+ for (i = 0; i < 40; i++)
+ str_append(input, "\xC3\xA4");
+ for (i = 0; i < 80; i++) {
+ for (skip = 0; skip < 2; skip++) {
+ str_truncate(str, 0);
+ for (j = 1; j < i; j++)
+ str_append_c(str, 'X');
+ if (i != 0)
+ str_append_c(str, ' ');
+
+ message_header_encode_b(str_data(input) + skip,
+ str_len(input) - skip, str,
+ i == 0 ? 0 : i+1);
+ test_assert(verify_b(str_c(str), i, skip == 0));
+ }
+ }
+ test_end();
+}
+
+static void test_message_header_encode(void)
+{
+ const char *data[] = {
+ "a b", "a b",
+ "a bc\xC3\xA4""de f", "a =?utf-8?q?bc=C3=A4de?= f",
+ "a \xC3\xA4\xC3\xA4 \xC3\xA4 b", "a =?utf-8?b?w6TDpCDDpA==?= b",
+ "\xC3\xA4 a \xC3\xA4", "=?utf-8?q?=C3=A4_a_=C3=A4?=",
+ "\xC3\xA4\xC3\xA4 a \xC3\xA4", "=?utf-8?b?w6TDpCBhIMOk?=",
+ "=", "=",
+ "?", "?",
+ "a=?", "a=?",
+ "=?", "=?utf-8?q?=3D=3F?=",
+ "=?x", "=?utf-8?q?=3D=3Fx?=",
+ "a\n=?", "a\n\t=?utf-8?q?=3D=3F?=",
+ "a\t=?", "a\t=?utf-8?q?=3D=3F?=",
+ "a =?", "a =?utf-8?q?=3D=3F?=",
+ "foo\001bar", "=?utf-8?q?foo=01bar?=",
+ "\x01\x02\x03\x04\x05\x06\x07\x08", "=?utf-8?b?AQIDBAUGBwg=?=",
+#define TEXT30 "123456789012345678901234567890"
+ TEXT30 " \xc3\xa4 " TEXT30 "\xc3\xa4 stuff",
+ TEXT30 " =?utf-8?q?=C3=A4_12345678901234567890123456?=\n"
+ "\t=?utf-8?q?7890=C3=A4?= stuff",
+
+ "a\r\n b", "a\r\n b",
+ "a\r\n\tb", "a\r\n\tb",
+ "a\r\nb", "a\r\n\tb",
+ "a\n b", "a\n b",
+ "a\n b", "a\n b",
+ "a\nb", "a\n\tb",
+ "a\r\n", "a",
+ "a\n", "a",
+ "foo\n \001bar", "foo\n =?utf-8?q?=01bar?=",
+ "foo\001\n bar", "=?utf-8?q?foo=01?=\n bar",
+ "\xC3\xA4\xC3\xA4\xC3\xA4\xC3\xA4\xC3\xA4\xC3\xA4\xC3\xA4"
+ "\xC3\xA4\xC3\xA4\xC3\xA4\xC3\xA4\xC3\xA4\xC3\xA4\xC3\xA4",
+ "=?utf-8?b?w6TDpMOkw6TDpMOkw6TDpMOkw6TDpMOkw6TDpA==?=",
+ /* Bad UTF-8 */
+ "foofoo-\x80\x80\x80\x80\x80\x80\x80-barbar",
+ "=?utf-8?q?foofoo-=EF=BF=BD-barbar?=",
+ "foobarfoobar-\x80\x80\x80\x80\x80\x80\x80",
+ "=?utf-8?q?foobarfoobar-=EF=BF=BD?=",
+ "\x80\x80\x80\x80\x80\x80\x80-foobarfoobar",
+ "=?utf-8?q?=EF=BF=BD-foobarfoobar?=",
+ "foofoo-\x80\x80\x80\x80\x80\x80\x80-barbarbarbar-"
+ "\x81\x82\x83\x84\x85\x86\x87-bazbaz",
+ "=?utf-8?q?foofoo-=EF=BF=BD-barbarbarbar-"
+ "=EF=BF=BD-bazbaz?=",
+ "foobarfoobarfoobar-\x80\x80\x80\x80\x80\x80\x80-"
+ "\x81\x82\x83\x84\x85\x86\x87-bazbaz",
+ "=?utf-8?q?foobarfoobarfoobar-=EF=BF=BD-"
+ "=EF=BF=BD-bazbaz?=",
+ "\x80\x80\x80\x80\x80\x80\x80-foobarfoobarfoobar-"
+ "\x81\x82\x83\x84\x85\x86\x87-bazbaz",
+ "=?utf-8?q?=EF=BF=BD-foobarfoobarfoobar-"
+ "=EF=BF=BD-bazbaz?=",
+ "foofoo-\xC3-barbar",
+ "=?utf-8?q?foofoo-=EF=BF=BD-barbar?=",
+ "foobarfoobar-\xC3",
+ "=?utf-8?q?foobarfoobar-=EF=BF=BD?=",
+ "\xC3-foobarfoobar",
+ "=?utf-8?q?=EF=BF=BD-foobarfoobar?=",
+ "f-\x80\x80\x80\x80\x80\x80\x80-b", "=?utf-8?b?Zi3vv70tYg==?=",
+ "fb-\x80\x80\x80\x80\x80\x80\x80", "=?utf-8?b?ZmIt77+9?=",
+ "\x80\x80\x80\x80\x80\x80\x80-fb", "=?utf-8?b?77+9LWZi?=",
+ "ff-\x80\x80\x80\x80\x80\x80\x80-bb-"
+ "\x81\x82\x83\x84\x85\x86\x87-zz",
+ "=?utf-8?b?ZmYt77+9LWJiLe+/vS16eg==?=",
+ "fbfb-\x80\x80\x80\x80\x80\x80\x80-"
+ "\x81\x82\x83\x84\x85\x86\x87-zz",
+ "=?utf-8?b?ZmJmYi3vv70t77+9LXp6?=",
+ "\x80\x80\x80\x80\x80\x80\x80-ff-"
+ "\x81\x82\x83\x84\x85\x86\x87-zz",
+ "=?utf-8?b?77+9LWZmLe+/vS16eg==?=",
+ "\x80\x80\x80\x80\x80\x80\x80\x80\x80\x80\x80\x80\x80\x80",
+ "=?utf-8?b?77+9?=",
+ "-\xC3-\xC3-\xC3-\xC3-\xC3-\xC3-",
+ "=?utf-8?q?-=EF=BF=BD-=EF=BF=BD-"
+ "=EF=BF=BD-=EF=BF=BD-=EF=BF=BD-=EF=BF=BD-?=",
+ "\xC3--\xC3-\xC3-\xC3-\xC3--\xC3",
+ "=?utf-8?q?=EF=BF=BD--=EF=BF=BD-"
+ "=EF=BF=BD-=EF=BF=BD-=EF=BF=BD--=EF=BF=BD?=",
+ "-\xC3\xC3-\xC3\xC3-\xC3\xC3-\xC3\xC3-\xC3\xC3-\xC3\xC3-",
+ "=?utf-8?b?Le+/vS3vv70t77+9Le+/vS3vv70t77+9LQ==?=",
+ "-\xC3\xC3\xC3-\xC3\xC3\xC3-\xC3\xC3\xC3-"
+ "\xC3\xC3\xC3-\xC3\xC3\xC3-\xC3\xC3\xC3-",
+ "=?utf-8?b?Le+/vS3vv70t77+9Le+/vS3vv70t77+9LQ==?=",
+ "\xC3\xC3\xC3\xC3\xC3\xC3\xC3\xC3\xC3\xC3\xC3\xC3\xC3\xC3",
+ "=?utf-8?b?77+9?=",
+ "-\xC3\xA4\xC3\xC3-\xC3\xC3\xA4\xC3-\xC3\xC3\xC3\xA4-"
+ "\xC3\xC3\xC3\xA4-\xC3\xC3\xA4\xC3-\xC3\xA4\xC3\xC3-",
+ "=?utf-8?b?LcOk77+9Le+/vcOk77+9Le+/"
+ "vcOkLe+/vcOkLe+/vcOk77+9LcOk77+9LQ==?=",
+ };
+ string_t *str = t_str_new(128);
+ unsigned int i;
+
+ test_begin("message header encode");
+ for (i = 0; i < N_ELEMENTS(data); i += 2) {
+ str_truncate(str, 0);
+ message_header_encode(data[i], str);
+ test_assert_strcmp(str_c(str), data[i+1]);
+ }
+ test_end();
+}
+
+static void test_message_header_encode_data(void)
+{
+ string_t *str = t_str_new(128);
+ static unsigned char nuls[10] = { 0, };
+
+ test_begin("message header encode data");
+ message_header_encode_data(nuls, 1, str);
+ test_assert_strcmp(str_c(str), "=?utf-8?q?=00?=");
+
+ str_truncate(str, 0);
+ message_header_encode_data(nuls, sizeof(nuls), str);
+ test_assert_strcmp(str_c(str), "=?utf-8?b?AAAAAAAAAAAAAA==?=");
+ test_end();
+}
+
+int main(void)
+{
+ static void (*const test_functions[])(void) = {
+ test_message_header_encode_q,
+ test_message_header_encode_b,
+ test_message_header_encode,
+ test_message_header_encode_data,
+ NULL
+ };
+ return test_run(test_functions);
+}
diff --git a/src/lib-mail/test-message-header-hash.c b/src/lib-mail/test-message-header-hash.c
new file mode 100644
index 0000000..7dd6fd8
--- /dev/null
+++ b/src/lib-mail/test-message-header-hash.c
@@ -0,0 +1,111 @@
+/* Copyright (c) 2016-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "test-common.h"
+#include "md5.h"
+#include "message-header-hash.h"
+
+static const char test_input_with_nuls[] = {
+ "\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f"
+ "\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f"
+ "\x20!?x??yz\x7f\x80\x90\xff-plop\xff"
+};
+
+static const struct {
+ const char *input;
+ unsigned int version;
+ const char *output;
+} tests[] = {
+ { "???hi???", 1, "???hi???" },
+
+ { test_input_with_nuls, 2, "?\t\n? !?x?yz?-plop?" },
+ { "?hi?", 2, "?hi?" },
+ { "\x01hi\x01", 2, "?hi?" },
+ { "???hi???", 2, "?hi?" },
+ { "\x01?hi??\x01", 2, "?hi?" },
+ { "?\t?hi?\t?", 2, "?\t?hi?\t?" },
+ { "\n\nhi\n\n", 2, "\n\nhi\n\n" },
+ { "", 2, "" },
+ { " ", 2, " " },
+ { " ", 2, " " },
+ { "? ? ? hi \x01\x02 \x03 ", 2, "? ? ? hi ? ? " },
+
+ { test_input_with_nuls, 3, "?\t\n?!?x?yz?-plop?" },
+ { "\n\nhi\n\n", 3, "\n\nhi\n\n" },
+ { "", 3, "" },
+ { " ", 3, "" },
+ { " ", 3, "" },
+ { " ? ", 3, "?" },
+ { "? ? ? hi \x01\x02 \x03 ", 3, "???hi??" },
+ { " \t \t", 3, "\t\t" },
+
+ { test_input_with_nuls, 4, "?\n?!?x?yz?-plop?" },
+ { "\n\nhi\n\n", 4, "\n\nhi\n\n" },
+ { "", 4, "" },
+ { " ", 4, "" },
+ { " \t \t", 4, "" },
+ { "foo\t\t", 4, "foo" },
+};
+
+static void test_message_header_hash_more(void)
+{
+ struct message_header_hash_context ctx;
+ struct md5_context md5_ctx;
+ unsigned char md5_input[MD5_RESULTLEN], md5_output[MD5_RESULTLEN];
+
+ test_begin("message_header_hash_more");
+ for (unsigned int i = 0; i < N_ELEMENTS(tests); i++) {
+ const unsigned char *test_input =
+ (const unsigned char *)tests[i].input;
+ size_t input_len = tests[i].input == test_input_with_nuls ?
+ sizeof(test_input_with_nuls)-1 : strlen(tests[i].input);
+ md5_init(&md5_ctx);
+ i_zero(&ctx);
+ message_header_hash_more(&ctx, &hash_method_md5, &md5_ctx,
+ tests[i].version, test_input,
+ input_len);
+ md5_final(&md5_ctx, md5_input);
+
+ md5_init(&md5_ctx);
+ md5_update(&md5_ctx, tests[i].output, strlen(tests[i].output));
+ md5_final(&md5_ctx, md5_output);
+
+ test_assert_idx(memcmp(md5_input, md5_output, MD5_RESULTLEN) == 0, i);
+
+ /* single byte at a time */
+ md5_init(&md5_ctx);
+ i_zero(&ctx);
+ for (unsigned int j = 0; j < input_len; j++) {
+ message_header_hash_more(&ctx, &hash_method_md5,
+ &md5_ctx, tests[i].version,
+ test_input+j, 1);
+ }
+ md5_final(&md5_ctx, md5_input);
+ test_assert_idx(memcmp(md5_input, md5_output, MD5_RESULTLEN) == 0, i);
+
+ /* random number of chars at a time */
+ md5_init(&md5_ctx);
+ i_zero(&ctx);
+ for (unsigned int j = 0; j < input_len; ) {
+ const unsigned char *input_part =
+ (const unsigned char *)tests[i].input + j;
+ unsigned int len = i_rand_minmax(1, input_len - j);
+ message_header_hash_more(&ctx, &hash_method_md5,
+ &md5_ctx, tests[i].version,
+ input_part, len);
+ j += len;
+ }
+ md5_final(&md5_ctx, md5_input);
+ test_assert_idx(memcmp(md5_input, md5_output, MD5_RESULTLEN) == 0, i);
+ }
+ test_end();
+}
+
+int main(void)
+{
+ static void (*const test_functions[])(void) = {
+ test_message_header_hash_more,
+ NULL
+ };
+ return test_run(test_functions);
+}
diff --git a/src/lib-mail/test-message-header-parser.c b/src/lib-mail/test-message-header-parser.c
new file mode 100644
index 0000000..700d341
--- /dev/null
+++ b/src/lib-mail/test-message-header-parser.c
@@ -0,0 +1,479 @@
+/* Copyright (c) 2007-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "str.h"
+#include "strfuncs.h"
+#include "unichar.h"
+#include "istream.h"
+#include "message-size.h"
+#include "message-header-parser.h"
+#include "test-common.h"
+
+#define TEST1_MSG_BODY_LEN 5
+static const char *test1_msg =
+ "h1: v1\n"
+ "h2:\n"
+ " v2\r\n"
+ "h3: \r\n"
+ "\tv3\n"
+ "\tw3\r\n"
+ "h4: \r\n"
+ "\n"
+ " body";
+
+static void
+test_message_header_parser_one(struct message_header_parser_ctx *parser,
+ enum message_header_parser_flags hdr_flags)
+{
+ struct message_header_line *hdr;
+ bool use_full_value;
+
+ use_full_value = hdr_flags != 0;
+
+ test_assert(message_parse_header_next(parser, &hdr) > 0);
+ test_assert(hdr->name_offset == 0);
+ if ((hdr_flags & MESSAGE_HEADER_PARSER_FLAG_SKIP_INITIAL_LWSP) == 0)
+ test_assert(hdr->full_value_offset == 4);
+ else
+ test_assert(hdr->full_value_offset == 5);
+ test_assert(hdr->name_len == 2 && strcmp(hdr->name, "h1") == 0);
+ if ((hdr_flags & MESSAGE_HEADER_PARSER_FLAG_SKIP_INITIAL_LWSP) == 0) {
+ test_assert(hdr->middle_len == 2 && memcmp(hdr->middle, ": ", 2) == 0);
+ test_assert(hdr->value_len == 3 && memcmp(hdr->value, " v1", 3) == 0);
+ } else {
+ test_assert(hdr->middle_len == 3 && memcmp(hdr->middle, ": ", 3) == 0);
+ test_assert(hdr->value_len == 2 && memcmp(hdr->value, "v1", 2) == 0);
+ }
+ test_assert(!hdr->continues && !hdr->continued && !hdr->eoh &&
+ !hdr->no_newline && !hdr->crlf_newline);
+
+ test_assert(message_parse_header_next(parser, &hdr) > 0);
+ test_assert(hdr->name_offset == 8 && hdr->full_value_offset == 11);
+ test_assert(hdr->name_len == 2 && strcmp(hdr->name, "h2") == 0);
+ test_assert(hdr->middle_len == 1 && memcmp(hdr->middle, ":", 1) == 0);
+ test_assert(hdr->value_len == 0);
+ test_assert(hdr->continues && !hdr->continued && !hdr->eoh &&
+ !hdr->no_newline && !hdr->crlf_newline);
+ if (use_full_value) hdr->use_full_value = TRUE;
+
+ test_assert(message_parse_header_next(parser, &hdr) > 0);
+ test_assert(hdr->name_offset == 8 && hdr->full_value_offset == 11);
+ test_assert(hdr->name_len == 2 && strcmp(hdr->name, "h2") == 0);
+ test_assert(hdr->middle_len == 1 && memcmp(hdr->middle, ":", 1) == 0);
+ test_assert(hdr->value_len == 3 && memcmp(hdr->value, " v2", 3) == 0);
+ test_assert(!hdr->continues && hdr->continued && !hdr->eoh &&
+ !hdr->no_newline && hdr->crlf_newline);
+ if ((hdr_flags & MESSAGE_HEADER_PARSER_FLAG_CLEAN_ONELINE) != 0) {
+ test_assert(hdr->full_value_len == 3 &&
+ memcmp(hdr->full_value, " v2", 3) == 0);
+ } else if (use_full_value) {
+ test_assert(hdr->full_value_len == 4 &&
+ memcmp(hdr->full_value, "\n v2", 4) == 0);
+ }
+
+ test_assert(message_parse_header_next(parser, &hdr) > 0);
+ test_assert(hdr->name_offset == 17 && hdr->full_value_offset == 21);
+ test_assert(hdr->name_len == 2 && strcmp(hdr->name, "h3") == 0);
+ test_assert(hdr->middle_len == 2 && memcmp(hdr->middle, ": ", 2) == 0);
+ test_assert(hdr->value_len == 0);
+ test_assert(hdr->continues && !hdr->continued && !hdr->eoh &&
+ !hdr->no_newline && hdr->crlf_newline);
+ if (use_full_value) hdr->use_full_value = TRUE;
+
+ test_assert(message_parse_header_next(parser, &hdr) > 0);
+ test_assert(hdr->name_offset == 17 && hdr->full_value_offset == 21);
+ test_assert(hdr->name_len == 2 && strcmp(hdr->name, "h3") == 0);
+ test_assert(hdr->middle_len == 2 && memcmp(hdr->middle, ": ", 2) == 0);
+ test_assert(hdr->value_len == 3 && memcmp(hdr->value, "\tv3", 3) == 0);
+ test_assert(hdr->continues && hdr->continued && !hdr->eoh &&
+ !hdr->no_newline && !hdr->crlf_newline);
+ if (use_full_value) hdr->use_full_value = TRUE;
+ if ((hdr_flags & MESSAGE_HEADER_PARSER_FLAG_CLEAN_ONELINE) != 0) {
+ test_assert(hdr->full_value_len == 3 &&
+ memcmp(hdr->full_value, " v3", 3) == 0);
+ } else if ((hdr_flags & MESSAGE_HEADER_PARSER_FLAG_DROP_CR) != 0) {
+ test_assert(hdr->full_value_len == 4 &&
+ memcmp(hdr->full_value, "\n\tv3", 4) == 0);
+ } else if (use_full_value) {
+ test_assert(hdr->full_value_len == 5 &&
+ memcmp(hdr->full_value, "\r\n\tv3", 5) == 0);
+ }
+
+ test_assert(message_parse_header_next(parser, &hdr) > 0);
+ test_assert(hdr->name_offset == 17 && hdr->full_value_offset == 21);
+ test_assert(hdr->name_len == 2 && strcmp(hdr->name, "h3") == 0);
+ test_assert(hdr->middle_len == 2 && memcmp(hdr->middle, ": ", 2) == 0);
+ test_assert(hdr->value_len == 3 && memcmp(hdr->value, "\tw3", 3) == 0);
+ test_assert(!hdr->continues && hdr->continued && !hdr->eoh &&
+ !hdr->no_newline && hdr->crlf_newline);
+ if ((hdr_flags & MESSAGE_HEADER_PARSER_FLAG_CLEAN_ONELINE) != 0) {
+ test_assert(hdr->full_value_len == 6 &&
+ memcmp(hdr->full_value, " v3 w3", 6) == 0);
+ } else if ((hdr_flags & MESSAGE_HEADER_PARSER_FLAG_DROP_CR) != 0) {
+ test_assert(hdr->full_value_len == 8 &&
+ memcmp(hdr->full_value, "\n\tv3\n\tw3", 8) == 0);
+ } else if (use_full_value) {
+ test_assert(hdr->full_value_len == 9 &&
+ memcmp(hdr->full_value, "\r\n\tv3\n\tw3", 9) == 0);
+ }
+
+ test_assert(message_parse_header_next(parser, &hdr) > 0);
+ test_assert(hdr->name_offset == 32 && hdr->full_value_offset == 36);
+ test_assert(hdr->name_len == 2 && strcmp(hdr->name, "h4") == 0);
+ test_assert(hdr->middle_len == 2 && memcmp(hdr->middle, ": ", 2) == 0);
+ test_assert(hdr->value_len == 0 && memcmp(hdr->value, "", 0) == 0);
+ test_assert(!hdr->continues && !hdr->continued && !hdr->eoh &&
+ !hdr->no_newline && hdr->crlf_newline);
+ test_assert(hdr->full_value_len == 0 && hdr->full_value != NULL);
+
+ test_assert(message_parse_header_next(parser, &hdr) > 0);
+ test_assert(hdr->name_offset == 38 && hdr->full_value_offset == 38);
+ test_assert(hdr->name_len == 0 && hdr->middle_len == 0 && hdr->value_len == 0);
+ test_assert(!hdr->continues && !hdr->continued && hdr->eoh &&
+ !hdr->no_newline && !hdr->crlf_newline);
+
+ test_assert(message_parse_header_next(parser, &hdr) < 0);
+}
+
+static void test_message_header_parser(void)
+{
+ static enum message_header_parser_flags max_hdr_flags =
+ MESSAGE_HEADER_PARSER_FLAG_SKIP_INITIAL_LWSP |
+ MESSAGE_HEADER_PARSER_FLAG_DROP_CR |
+ MESSAGE_HEADER_PARSER_FLAG_CLEAN_ONELINE;
+ enum message_header_parser_flags hdr_flags;
+ struct message_header_parser_ctx *parser;
+ struct message_size hdr_size, hdr_size2;
+ struct istream *input;
+ bool has_nuls;
+
+ test_begin("message header parser");
+ input = test_istream_create(test1_msg);
+
+ for (hdr_flags = 0; hdr_flags <= max_hdr_flags; hdr_flags++) {
+ i_stream_seek(input, 0);
+ parser = message_parse_header_init(input, &hdr_size, hdr_flags);
+ test_message_header_parser_one(parser, hdr_flags);
+ message_parse_header_deinit(&parser);
+ i_stream_seek(input, 0);
+ message_get_header_size(input, &hdr_size2, &has_nuls);
+ }
+
+ test_assert(!has_nuls);
+ test_assert(hdr_size.physical_size == hdr_size2.physical_size);
+ test_assert(hdr_size.virtual_size == hdr_size2.virtual_size);
+ test_assert(hdr_size.lines == hdr_size2.lines);
+ test_assert(hdr_size.physical_size == strlen(test1_msg)-TEST1_MSG_BODY_LEN);
+ test_assert(hdr_size.virtual_size == strlen(test1_msg) - TEST1_MSG_BODY_LEN + 4);
+
+ i_stream_unref(&input);
+ test_end();
+}
+
+static void hdr_write(string_t *str, struct message_header_line *hdr)
+{
+ if (!hdr->continued) {
+ str_append(str, hdr->name);
+ if (hdr->middle_len > 0)
+ str_append_data(str, hdr->middle, hdr->middle_len);
+ }
+ str_append_data(str, hdr->value, hdr->value_len);
+ if (!hdr->no_newline) {
+ if (hdr->crlf_newline)
+ str_append_c(str, '\r');
+ str_append_c(str, '\n');
+ }
+}
+
+static void test_message_header_parser_partial(void)
+{
+ struct message_header_parser_ctx *parser;
+ struct message_header_line *hdr;
+ struct istream *input;
+ unsigned int i, max = (strlen(test1_msg)-TEST1_MSG_BODY_LEN)*2;
+ string_t *str;
+ int ret;
+
+ test_begin("message header parser partial");
+ input = test_istream_create(test1_msg);
+ test_istream_set_allow_eof(input, FALSE);
+
+ str = t_str_new(max);
+ parser = message_parse_header_init(input, NULL, 0);
+ for (i = 0; i <= max; i++) {
+ test_istream_set_size(input, i/2);
+ while ((ret = message_parse_header_next(parser, &hdr)) > 0)
+ hdr_write(str, hdr);
+ test_assert((ret == 0 && i < max) ||
+ (ret < 0 && i == max));
+ }
+ message_parse_header_deinit(&parser);
+
+ str_append(str, " body");
+ test_assert(strcmp(str_c(str), test1_msg) == 0);
+ i_stream_unref(&input);
+ test_end();
+}
+
+static void
+test_message_header_parser_long_lines_str(const char *str,
+ unsigned int buffer_size,
+ struct message_size *size_r,
+ struct message_size *size_2_r)
+{
+ struct message_header_parser_ctx *parser;
+ struct message_header_line *hdr;
+ struct istream *input;
+ unsigned int i;
+ size_t len = strlen(str);
+ bool has_nuls;
+
+ input = test_istream_create(str);
+ test_istream_set_max_buffer_size(input, buffer_size);
+
+ parser = message_parse_header_init(input, size_r, 0);
+ for (i = 1; i <= len; i++) {
+ test_istream_set_size(input, i);
+ while (message_parse_header_next(parser, &hdr) > 0) ;
+ }
+ message_parse_header_deinit(&parser);
+ i_stream_seek(input, 0);
+ /* Buffer must be +1 for message_get_header_size as it's using
+ i_stream_read_bytes which does not work with lower buffersize
+ because it returns -2 (input buffer full) if 2 bytes are wanted. */
+ test_istream_set_max_buffer_size(input, buffer_size+1);
+ message_get_header_size(input, size_2_r, &has_nuls);
+ i_stream_unref(&input);
+}
+
+#define NAME10 "1234567890"
+#define NAME100 NAME10 NAME10 NAME10 NAME10 NAME10 \
+ NAME10 NAME10 NAME10 NAME10 NAME10
+#define NAME1000 NAME100 NAME100 NAME100 NAME100 NAME100 \
+ NAME100 NAME100 NAME100 NAME100 NAME100
+
+static void test_message_header_parser_long_lines(void)
+{
+ static const char *lf_str = NAME10": 345\n\n";
+ static const char *crlf_str = NAME10": 345\r\n\r\n";
+ static const char *lf_str_vl = NAME1000": Is a long header name\n\n";
+ static const char *crlf_str_vl = NAME1000": Is a long header name\r\n\r\n";
+ static const char *lf_str_ol = NAME1000 \
+ NAME100 ": Is a overlong header name\n\n";
+ static const char *crlf_str_ol = NAME1000 \
+ NAME100 ": Is a overlong header name\r\n\r\n";
+
+ struct message_size hdr_size, hdr_size2;
+ size_t i, len;
+
+ test_begin("message header parser long lines");
+ len = strlen(lf_str);
+ for (i = 2; i < len; i++) {
+ test_message_header_parser_long_lines_str(lf_str, i, &hdr_size, &hdr_size2);
+ test_assert(hdr_size.physical_size == len);
+ test_assert(hdr_size.virtual_size == len + 2);
+ test_assert(hdr_size.virtual_size == hdr_size2.virtual_size);
+ test_assert(hdr_size.physical_size == hdr_size2.physical_size);
+ }
+ len = strlen(crlf_str);
+ for (i = 3; i < len; i++) {
+ test_message_header_parser_long_lines_str(crlf_str, i, &hdr_size, &hdr_size2);
+ test_assert(hdr_size.physical_size == len);
+ test_assert(hdr_size.virtual_size == len);
+ test_assert(hdr_size.virtual_size == hdr_size2.virtual_size);
+ test_assert(hdr_size.physical_size == hdr_size2.physical_size);
+ }
+
+ /* increment these faster, otherwise the test is very slow */
+ len = strlen(lf_str_vl);
+ for (i = 3; i < len; i *= 2) {
+ test_message_header_parser_long_lines_str(lf_str_vl, i, &hdr_size, &hdr_size2);
+ test_assert(hdr_size.physical_size == len);
+ test_assert(hdr_size.virtual_size == len + 2);
+ test_assert(hdr_size.virtual_size == hdr_size2.virtual_size);
+ test_assert(hdr_size.physical_size == hdr_size2.physical_size);
+ }
+ len = strlen(crlf_str_vl);
+ for (i = 3; i < len; i *= 2) {
+ test_message_header_parser_long_lines_str(crlf_str_vl, i, &hdr_size, &hdr_size2);
+ test_assert(hdr_size.physical_size == len);
+ test_assert(hdr_size.virtual_size == len);
+ test_assert(hdr_size.virtual_size == hdr_size2.virtual_size);
+ test_assert(hdr_size.physical_size == hdr_size2.physical_size);
+ }
+
+ /* test that parsing overlength lines work so that name & middle are
+ empty. */
+
+ struct message_header_line *hdr;
+ struct message_header_parser_ctx *ctx;
+ struct istream *input;
+
+ input = test_istream_create(lf_str_ol);
+ ctx = message_parse_header_init(input, NULL, 0);
+
+ test_assert(message_parse_header_next(ctx, &hdr) > 0 &&
+ *hdr->name == '\0' && hdr->middle == uchar_empty_ptr &&
+ hdr->name_len == 0 && hdr->middle_len == 0 &&
+ hdr->value != NULL && hdr->value_len > 0);
+ test_assert(message_parse_header_next(ctx, &hdr) > 0 &&
+ hdr->eoh);
+ message_parse_header_deinit(&ctx);
+ i_stream_unref(&input);
+
+ input = test_istream_create(crlf_str_ol);
+ ctx = message_parse_header_init(input, NULL, 0);
+ test_assert(message_parse_header_next(ctx, &hdr) > 0 &&
+ *hdr->name == '\0' && hdr->middle == uchar_empty_ptr &&
+ hdr->name_len == 0 && hdr->middle_len == 0 &&
+ hdr->value != NULL && hdr->value_len > 0);
+ test_assert(message_parse_header_next(ctx, &hdr) > 0 &&
+ hdr->eoh);
+ message_parse_header_deinit(&ctx);
+ i_stream_unref(&input);
+
+ /* test offset parsing */
+ static const char *data = "h1" NAME1000 NAME100 \
+ ": value1\r\n" \
+ "h2" NAME1000 NAME100 \
+ ": value2\r\n" \
+ "h3" NAME1000 NAME100 \
+ ": value3\r\n\r\n";
+ input = test_istream_create(data);
+ ctx = message_parse_header_init(input, NULL, 0);
+ test_assert(message_parse_header_next(ctx, &hdr) > 0 &&
+ hdr->full_value[0] == 'h' &&
+ hdr->full_value[1] == '1' &&
+ hdr->full_value_offset == 0);
+ test_assert(message_parse_header_next(ctx, &hdr) > 0 &&
+ hdr->full_value[0] == 'h' &&
+ hdr->full_value[1] == '2' &&
+ hdr->full_value_offset == 1112);
+ test_assert(message_parse_header_next(ctx, &hdr) > 0 &&
+ hdr->full_value[0] == 'h' &&
+ hdr->full_value[1] == '3' &&
+ hdr->full_value_offset == 2224);
+ test_assert(message_parse_header_next(ctx, &hdr) > 0 &&
+ hdr->eoh);
+
+ message_parse_header_deinit(&ctx);
+ i_stream_unref(&input);
+
+ test_end();
+}
+
+static void test_message_header_parser_extra_cr_in_eoh(void)
+{
+ static const char *str = "a:b\n\r\r\n";
+ struct message_header_parser_ctx *parser;
+ struct message_header_line *hdr;
+ struct istream *input;
+
+ test_begin("message header parser extra CR in EOH");
+
+ input = test_istream_create(str);
+ parser = message_parse_header_init(input, NULL, 0);
+ test_assert(message_parse_header_next(parser, &hdr) > 0 &&
+ strcmp(hdr->name, "a") == 0);
+ test_assert(message_parse_header_next(parser, &hdr) > 0 &&
+ *hdr->value == '\r' && hdr->value_len == 1 &&
+ hdr->full_value_offset == 4 &&
+ hdr->middle_len == 0 &&
+ hdr->name_len == 0 && !hdr->eoh);
+ test_assert(message_parse_header_next(parser, &hdr) < 0);
+ message_parse_header_deinit(&parser);
+ test_assert(input->stream_errno == 0);
+ i_stream_unref(&input);
+ test_end();
+}
+
+static void test_message_header_parser_no_eoh(void)
+{
+ static const char *str = "a:b\n";
+ struct message_header_parser_ctx *parser;
+ struct message_header_line *hdr;
+ struct istream *input;
+
+ test_begin("message header parser no EOH");
+
+ input = test_istream_create(str);
+ parser = message_parse_header_init(input, NULL, 0);
+ test_assert(message_parse_header_next(parser, &hdr) > 0 &&
+ strcmp(hdr->name, "a") == 0);
+ test_assert_strcmp(message_header_strdup(pool_datastack_create(),
+ hdr->value, hdr->value_len),
+ "b");
+ test_assert(message_parse_header_next(parser, &hdr) < 0);
+ message_parse_header_deinit(&parser);
+ test_assert(input->stream_errno == 0);
+ i_stream_unref(&input);
+ test_end();
+}
+
+static void test_message_header_parser_nul(void)
+{
+ static const unsigned char str[] = "a :\0\0b\n";
+ struct message_header_parser_ctx *parser;
+ struct message_header_line *hdr;
+ struct istream *input;
+
+ test_begin("message header parser NUL");
+
+ input = test_istream_create_data(str, sizeof(str)-1);
+ parser = message_parse_header_init(input, NULL, 0);
+ test_assert(message_parse_header_next(parser, &hdr) > 0 &&
+ strcmp(hdr->name, "a") == 0);
+ test_assert(hdr->value_len >= 3 && memcmp("\0\0b", hdr->value, 3) == 0);
+ test_assert_strcmp(message_header_strdup(pool_datastack_create(),
+ hdr->value, hdr->value_len),
+ UNICODE_REPLACEMENT_CHAR_UTF8 UNICODE_REPLACEMENT_CHAR_UTF8"b");
+ test_assert(message_parse_header_next(parser, &hdr) < 0);
+ message_parse_header_deinit(&parser);
+ test_assert(input->stream_errno == 0);
+ i_stream_unref(&input);
+ test_end();
+}
+
+static void test_message_header_parser_extra_crlf_in_name(void)
+{
+ static const unsigned char str[] = "X-Header\r\n Name: Header Value\n\n";
+ struct message_header_parser_ctx *parser;
+ struct message_header_line *hdr;
+ struct istream *input;
+ test_begin("message header parser CRLF in header name");
+
+ input = test_istream_create_data(str, sizeof(str)-1);
+ parser = message_parse_header_init(input, NULL, 0);
+ hdr = NULL;
+ test_assert(message_parse_header_next(parser, &hdr) > 0 &&
+ *hdr->name == '\0' && hdr->middle == uchar_empty_ptr &&
+ hdr->name_len == 0 && hdr->middle_len == 0 &&
+ hdr->value != NULL && hdr->value_len > 0);
+ test_assert(message_parse_header_next(parser, &hdr) > 0 &&
+ *hdr->name == '\0' && hdr->middle == uchar_empty_ptr &&
+ hdr->name_len == 0 && hdr->middle_len == 0 &&
+ hdr->value != NULL && hdr->value_len > 0 &&
+ hdr->continued);
+ test_assert(message_parse_header_next(parser, &hdr) > 0 &&
+ hdr->eoh);
+
+ message_parse_header_deinit(&parser);
+ i_stream_unref(&input);
+
+ test_end();
+}
+
+int main(void)
+{
+ static void (*const test_functions[])(void) = {
+ test_message_header_parser,
+ test_message_header_parser_partial,
+ test_message_header_parser_long_lines,
+ test_message_header_parser_extra_cr_in_eoh,
+ test_message_header_parser_no_eoh,
+ test_message_header_parser_nul,
+ test_message_header_parser_extra_crlf_in_name,
+ NULL
+ };
+ return test_run(test_functions);
+}
diff --git a/src/lib-mail/test-message-id.c b/src/lib-mail/test-message-id.c
new file mode 100644
index 0000000..d5abf1f
--- /dev/null
+++ b/src/lib-mail/test-message-id.c
@@ -0,0 +1,46 @@
+/* Copyright (c) 2007-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "message-id.h"
+#include "test-common.h"
+
+static void test_message_id_get_next(void)
+{
+ const char *input[] = {
+ "<foo@bar>",
+ "<foo@bar>,skipped,<foo2@bar2>",
+ "(c) < (c) foo (c) @ (c) bar (c) > (c)",
+ "<\"foo 2\"@bar>"
+ };
+ const char *output[] = {
+ "foo@bar", NULL,
+ "foo@bar", "foo2@bar2", NULL,
+ "foo@bar", NULL,
+ "foo 2@bar", NULL
+ };
+ const char *msgid, *next_msgid;
+ unsigned int i, j;
+
+ test_begin("message id parser");
+ for (i = 0, j = 0; i < N_ELEMENTS(input); i++) {
+ msgid = input[i];
+ while ((next_msgid = message_id_get_next(&msgid)) != NULL) {
+ if (output[j] == NULL)
+ break;
+ test_assert(strcmp(output[j], next_msgid) == 0);
+ j++;
+ }
+ test_assert(output[j++] == NULL && next_msgid == NULL);
+ }
+ test_assert(j == N_ELEMENTS(output));
+ test_end();
+}
+
+int main(void)
+{
+ static void (*const test_functions[])(void) = {
+ test_message_id_get_next,
+ NULL
+ };
+ return test_run(test_functions);
+}
diff --git a/src/lib-mail/test-message-parser.c b/src/lib-mail/test-message-parser.c
new file mode 100644
index 0000000..663bfe8
--- /dev/null
+++ b/src/lib-mail/test-message-parser.c
@@ -0,0 +1,1398 @@
+/* Copyright (c) 2007-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "str.h"
+#include "istream.h"
+#include "message-parser.h"
+#include "message-part-data.h"
+#include "message-size.h"
+#include "test-common.h"
+
+static const char test_msg[] =
+"Return-Path: <test@example.org>\n"
+"Subject: Hello world\n"
+"From: Test User <test@example.org>\n"
+"To: Another User <test2@example.org>\n"
+"Message-Id: <1.2.3.4@example>\n"
+"Mime-Version: 1.0\n"
+"Date: Sun, 23 May 2007 04:58:08 +0300\n"
+"Content-Type: multipart/signed; micalg=pgp-sha1;\n"
+" protocol=\"application/pgp-signature\";\n"
+" boundary=\"=-GNQXLhuj24Pl1aCkk4/d\"\n"
+"\n"
+"--=-GNQXLhuj24Pl1aCkk4/d\n"
+"Content-Type: text/plain\n"
+"Content-Transfer-Encoding: quoted-printable\n"
+"\n"
+"There was a day=20\n"
+"a happy=20day\n"
+"\n"
+"--=-GNQXLhuj24Pl1aCkk4/d\n"
+"Content-Type: application/pgp-signature; name=signature.asc\n"
+"\n"
+"-----BEGIN PGP SIGNATURE-----\n"
+"Version: GnuPG v1.2.4 (GNU/Linux)\n"
+"\n"
+"invalid\n"
+"-----END PGP SIGNATURE-----\n"
+"\n"
+"--=-GNQXLhuj24Pl1aCkk4/d--\n"
+"\n"
+"\n";
+#define TEST_MSG_LEN (sizeof(test_msg)-1)
+
+static const struct message_parser_settings set_empty = { .flags = 0 };
+
+static int message_parse_stream(pool_t pool, struct istream *input,
+ const struct message_parser_settings *set,
+ bool parse_data, struct message_part **parts_r)
+{
+ int ret;
+ struct message_parser_ctx *parser;
+ struct message_block block;
+
+ i_zero(&block);
+ parser = message_parser_init(pool, input, set);
+ while ((ret = message_parser_parse_next_block(parser, &block)) > 0)
+ if (parse_data)
+ message_part_data_parse_from_header(pool, block.part,
+ block.hdr);
+ message_parser_deinit(&parser, parts_r);
+ test_assert(input->stream_errno == 0);
+ return ret;
+}
+
+static void test_parsed_parts(struct istream *input, struct message_part *parts)
+{
+ const struct message_parser_settings parser_set = {
+ .flags = MESSAGE_PARSER_FLAG_SKIP_BODY_BLOCK,
+ };
+ struct message_parser_ctx *parser;
+ struct message_block block;
+ struct message_part *parts2;
+ uoff_t i, input_size;
+ const char *error;
+
+ i_stream_seek(input, 0);
+ if (i_stream_get_size(input, TRUE, &input_size) < 0)
+ i_unreached();
+
+ parser = message_parser_init_from_parts(parts, input, &parser_set);
+ for (i = 1; i <= input_size*2+1; i++) {
+ test_istream_set_size(input, i/2);
+ if (i > TEST_MSG_LEN*2)
+ test_istream_set_allow_eof(input, TRUE);
+ while (message_parser_parse_next_block(parser, &block) > 0) ;
+ }
+ test_assert(message_parser_deinit_from_parts(&parser, &parts2, &error) == 0);
+ test_assert(message_part_is_equal(parts, parts2));
+}
+
+static void test_message_parser_small_blocks(void)
+{
+ struct message_parser_ctx *parser;
+ struct istream *input;
+ struct message_part *parts, *parts2;
+ struct message_block block;
+ unsigned int i, end_of_headers_idx;
+ string_t *output;
+ pool_t pool;
+ const char *error;
+ int ret;
+
+ test_begin("message parser in small blocks");
+ pool = pool_alloconly_create("message parser", 10240);
+ input = test_istream_create(test_msg);
+ output = t_str_new(128);
+
+ /* full parsing */
+ const struct message_parser_settings full_parser_set = {
+ .flags = MESSAGE_PARSER_FLAG_INCLUDE_MULTIPART_BLOCKS |
+ MESSAGE_PARSER_FLAG_INCLUDE_BOUNDARIES,
+ };
+ parser = message_parser_init(pool, input, &full_parser_set);
+ while ((ret = message_parser_parse_next_block(parser, &block)) > 0) {
+ if (block.hdr != NULL)
+ message_header_line_write(output, block.hdr);
+ else if (block.size > 0)
+ str_append_data(output, block.data, block.size);
+ }
+
+ test_assert(ret < 0);
+ message_parser_deinit(&parser, &parts);
+ test_assert(input->stream_errno == 0);
+ test_assert(strcmp(test_msg, str_c(output)) == 0);
+
+ /* parsing in small blocks */
+ i_stream_seek(input, 0);
+ test_istream_set_allow_eof(input, FALSE);
+
+ parser = message_parser_init(pool, input, &set_empty);
+ for (i = 1; i <= TEST_MSG_LEN*2+1; i++) {
+ test_istream_set_size(input, i/2);
+ if (i > TEST_MSG_LEN*2)
+ test_istream_set_allow_eof(input, TRUE);
+ while ((ret = message_parser_parse_next_block(parser,
+ &block)) > 0) ;
+ test_assert((ret == 0 && i <= TEST_MSG_LEN*2) ||
+ (ret < 0 && i > TEST_MSG_LEN*2));
+ }
+ message_parser_deinit(&parser, &parts2);
+ test_assert(input->stream_errno == 0);
+ test_assert(message_part_is_equal(parts, parts2));
+
+ /* parsing in small blocks from preparsed parts */
+ i_stream_seek(input, 0);
+ test_istream_set_allow_eof(input, FALSE);
+
+ end_of_headers_idx = (strstr(test_msg, "\n-----") - test_msg);
+ const struct message_parser_settings preparsed_parser_set = {
+ .flags = MESSAGE_PARSER_FLAG_SKIP_BODY_BLOCK,
+ };
+ parser = message_parser_init_from_parts(parts, input,
+ &preparsed_parser_set);
+ for (i = 1; i <= TEST_MSG_LEN*2+1; i++) {
+ test_istream_set_size(input, i/2);
+ if (i > TEST_MSG_LEN*2)
+ test_istream_set_allow_eof(input, TRUE);
+ while ((ret = message_parser_parse_next_block(parser,
+ &block)) > 0) ;
+ test_assert((ret == 0 && i/2 <= end_of_headers_idx) ||
+ (ret < 0 && i/2 > end_of_headers_idx));
+ }
+ test_assert(message_parser_deinit_from_parts(&parser, &parts2, &error) == 0);
+ test_assert(message_part_is_equal(parts, parts2));
+
+ i_stream_unref(&input);
+ pool_unref(&pool);
+ test_end();
+}
+
+static void test_message_parser_stop_early(void)
+{
+ struct istream *input, *input2;
+ struct message_part *parts;
+ unsigned int i;
+ pool_t pool;
+
+ test_begin("message parser in stop early");
+ pool = pool_alloconly_create("message parser", 524288);
+ input = test_istream_create(test_msg);
+
+ test_istream_set_allow_eof(input, FALSE);
+ for (i = 1; i <= TEST_MSG_LEN+1; i++) {
+ i_stream_seek(input, 0);
+ test_istream_set_size(input, i);
+
+ test_assert(message_parse_stream(pool, input, &set_empty, FALSE, &parts) == 0);
+
+ /* test preparsed - first re-parse everything with a stream
+ that sees EOF at this position */
+ input2 = i_stream_create_from_data(test_msg, i);
+ test_assert(message_parse_stream(pool, input2, &set_empty, FALSE, &parts) == -1);
+
+ /* now parse from the parts */
+ i_stream_seek(input2, 0);
+ test_assert(message_parse_stream(pool, input2, &set_empty, FALSE, &parts) == -1);
+
+ i_stream_unref(&input2);
+ }
+
+ i_stream_unref(&input);
+ pool_unref(&pool);
+ test_end();
+}
+
+static void test_message_parser_get_sizes(struct istream *input,
+ struct message_size *body_size_r,
+ struct message_size *header_size_r,
+ bool expect_has_nuls)
+{
+ bool has_nuls;
+ i_zero(body_size_r);
+ i_zero(header_size_r);
+
+ message_get_header_size(input, header_size_r, &has_nuls);
+ test_assert(has_nuls == expect_has_nuls);
+ message_get_body_size(input, body_size_r, &has_nuls);
+ test_assert(has_nuls == expect_has_nuls);
+}
+
+static void test_message_parser_assert_sizes(const struct message_part *part,
+ const struct message_size *body_size,
+ const struct message_size *header_size)
+{
+ test_assert(part->header_size.lines == header_size->lines);
+ test_assert(part->header_size.physical_size == header_size->physical_size);
+ test_assert(part->header_size.virtual_size == header_size->virtual_size);
+ test_assert(part->body_size.lines == body_size->lines);
+ test_assert(part->body_size.physical_size == body_size->physical_size);
+ test_assert(part->body_size.virtual_size == body_size->virtual_size);
+}
+
+static void test_message_parser_truncated_mime_headers(void)
+{
+static const char input_msg[] =
+"Content-Type: multipart/mixed; boundary=\":foo\"\n"
+"\n"
+"--:foo\n"
+"--:foo\n"
+"Content-Type: text/plain\n"
+"--:foo\n"
+"Content-Type: text/plain\r\n"
+"--:foo\n"
+"Content-Type: text/html\n"
+"--:foo--\n";
+ struct istream *input;
+ struct message_part *parts, *part;
+ struct message_size body_size, header_size;
+ pool_t pool;
+
+ test_begin("message parser truncated mime headers");
+ pool = pool_alloconly_create("message parser", 10240);
+ input = test_istream_create(input_msg);
+
+ test_assert(message_parse_stream(pool, input, &set_empty, FALSE, &parts) < 0);
+
+ i_stream_seek(input, 0);
+ test_message_parser_get_sizes(input, &body_size, &header_size, FALSE);
+
+ test_assert((parts->flags & MESSAGE_PART_FLAG_MULTIPART) != 0);
+ test_assert(parts->children_count == 4);
+ test_assert(parts->header_size.lines == 2);
+ test_assert(parts->header_size.physical_size == 48);
+ test_assert(parts->header_size.virtual_size == 48+2);
+ test_assert(parts->body_size.lines == 8);
+ test_assert(parts->body_size.physical_size == 112);
+ test_assert(parts->body_size.virtual_size == 112+7);
+ test_message_parser_assert_sizes(parts, &body_size, &header_size);
+
+ test_assert(parts->children->physical_pos == 55);
+ test_assert(parts->children->header_size.physical_size == 0);
+ test_assert(parts->children->body_size.physical_size == 0);
+ test_assert(parts->children->body_size.lines == 0);
+ test_assert(parts->children->next->physical_pos == 62);
+ test_assert(parts->children->next->header_size.physical_size == 24);
+ test_assert(parts->children->next->header_size.virtual_size == 24);
+ test_assert(parts->children->next->header_size.lines == 0);
+ test_assert(parts->children->next->next->physical_pos == 94);
+ test_assert(parts->children->next->next->header_size.physical_size == 24);
+ test_assert(parts->children->next->next->header_size.virtual_size == 24);
+ test_assert(parts->children->next->next->header_size.lines == 0);
+ test_assert(parts->children->next->next->next->physical_pos == 127);
+ test_assert(parts->children->next->next->next->header_size.physical_size == 23);
+ test_assert(parts->children->next->next->next->header_size.virtual_size == 23);
+ test_assert(parts->children->next->next->next->header_size.lines == 0);
+ for (part = parts->children; part != NULL; part = part->next) {
+ test_assert(part->children_count == 0);
+ test_assert(part->body_size.physical_size == 0);
+ test_assert(part->body_size.virtual_size == 0);
+ }
+ test_assert(parts->children->next->next->next->next == NULL);
+
+ test_parsed_parts(input, parts);
+ i_stream_unref(&input);
+ pool_unref(&pool);
+ test_end();
+}
+
+static void test_message_parser_truncated_mime_headers2(void)
+{
+static const char input_msg[] =
+"Content-Type: multipart/mixed; boundary=\"ab\"\n"
+"\n"
+"--ab\n"
+"Content-Type: multipart/mixed; boundary=\"a\"\n"
+"\n"
+"--ab\n"
+"Content-Type: text/plain\n"
+"\n"
+"--a\n\n";
+ struct istream *input;
+ struct message_part *parts;
+ struct message_size body_size, header_size;
+ pool_t pool;
+
+ test_begin("message parser truncated mime headers 2");
+ pool = pool_alloconly_create("message parser", 10240);
+ input = test_istream_create(input_msg);
+
+ test_assert(message_parse_stream(pool, input, &set_empty, FALSE, &parts) < 0);
+
+ i_stream_seek(input, 0);
+ test_message_parser_get_sizes(input, &body_size, &header_size, FALSE);
+
+ test_assert(parts->flags == (MESSAGE_PART_FLAG_MULTIPART | MESSAGE_PART_FLAG_IS_MIME));
+ test_assert(parts->children_count == 2);
+ test_assert(parts->header_size.lines == 2);
+ test_assert(parts->header_size.physical_size == 46);
+ test_assert(parts->header_size.virtual_size == 46+2);
+ test_assert(parts->body_size.lines == 8);
+ test_assert(parts->body_size.physical_size == 86);
+ test_assert(parts->body_size.virtual_size == 86+8);
+ test_message_parser_assert_sizes(parts, &body_size, &header_size);
+
+ test_assert(parts->children->children_count == 0);
+ test_assert(parts->children->flags == (MESSAGE_PART_FLAG_MULTIPART | MESSAGE_PART_FLAG_IS_MIME));
+ test_assert(parts->children->physical_pos == 51);
+ test_assert(parts->children->header_size.lines == 1);
+ test_assert(parts->children->header_size.physical_size == 44);
+ test_assert(parts->children->header_size.virtual_size == 44+1);
+ test_assert(parts->children->body_size.lines == 0);
+ test_assert(parts->children->body_size.physical_size == 0);
+ test_assert(parts->children->children == NULL);
+
+ test_assert(parts->children->next->children_count == 0);
+ test_assert(parts->children->next->flags == (MESSAGE_PART_FLAG_TEXT | MESSAGE_PART_FLAG_IS_MIME));
+ test_assert(parts->children->next->physical_pos == 101);
+ test_assert(parts->children->next->header_size.lines == 2);
+ test_assert(parts->children->next->header_size.physical_size == 26);
+ test_assert(parts->children->next->header_size.virtual_size == 26+2);
+ test_assert(parts->children->next->body_size.lines == 2);
+ test_assert(parts->children->next->body_size.physical_size == 5);
+ test_assert(parts->children->next->body_size.virtual_size == 5+2);
+ test_assert(parts->children->next->children == NULL);
+
+ test_parsed_parts(input, parts);
+ i_stream_unref(&input);
+ pool_unref(&pool);
+ test_end();
+}
+
+static void test_message_parser_truncated_mime_headers3(void)
+{
+static const char input_msg[] =
+"Content-Type: multipart/mixed; boundary=\"ab\"\n";
+ struct istream *input;
+ struct message_part *parts;
+ pool_t pool;
+
+ test_begin("message parser truncated mime headers 3");
+ pool = pool_alloconly_create("message parser", 10240);
+ input = test_istream_create(input_msg);
+
+ test_assert(message_parse_stream(pool, input, &set_empty, FALSE, &parts) < 0);
+
+ test_assert(parts->children_count == 0);
+ test_assert(parts->flags == (MESSAGE_PART_FLAG_MULTIPART | MESSAGE_PART_FLAG_IS_MIME));
+ test_assert(parts->header_size.lines == 1);
+ test_assert(parts->header_size.physical_size == 45);
+ test_assert(parts->header_size.virtual_size == 45+1);
+ test_assert(parts->body_size.lines == 0);
+ test_assert(parts->body_size.physical_size == 0);
+
+ test_assert(parts->children == NULL);
+
+ test_parsed_parts(input, parts);
+ i_stream_unref(&input);
+ pool_unref(&pool);
+ test_end();
+}
+
+static void test_message_parser_empty_multipart(void)
+{
+static const char input_msg[] =
+"Content-Type: multipart/mixed; boundary=\"ab\"\n"
+"\n"
+"body\n";
+ struct istream *input;
+ struct message_part *parts;
+ pool_t pool;
+
+ test_begin("message parser empty multipart");
+ pool = pool_alloconly_create("message parser", 10240);
+ input = test_istream_create(input_msg);
+
+ test_assert(message_parse_stream(pool, input, &set_empty, FALSE, &parts) < 0);
+
+ test_assert(parts->children_count == 0);
+ test_assert(parts->flags == (MESSAGE_PART_FLAG_MULTIPART | MESSAGE_PART_FLAG_IS_MIME));
+ test_assert(parts->header_size.lines == 2);
+ test_assert(parts->header_size.physical_size == 46);
+ test_assert(parts->header_size.virtual_size == 46+2);
+ test_assert(parts->body_size.lines == 1);
+ test_assert(parts->body_size.physical_size == 5);
+ test_assert(parts->body_size.virtual_size == 5+1);
+
+ test_assert(parts->children == NULL);
+
+ test_parsed_parts(input, parts);
+ i_stream_unref(&input);
+ pool_unref(&pool);
+ test_end();
+}
+
+static void test_message_parser_duplicate_mime_boundary(void)
+{
+static const char input_msg[] =
+"Content-Type: multipart/mixed; boundary=\"a\"\n"
+"\n"
+"--a\n"
+"Content-Type: multipart/mixed; boundary=\"a\"\n"
+"\n"
+"--a\n"
+"Content-Type: text/plain\n"
+"\n"
+"body\n";
+ struct istream *input;
+ struct message_part *parts;
+ struct message_size body_size, header_size;
+ pool_t pool;
+
+ test_begin("message parser duplicate mime boundary");
+ pool = pool_alloconly_create("message parser", 10240);
+ input = test_istream_create(input_msg);
+
+ test_assert(message_parse_stream(pool, input, &set_empty, FALSE, &parts) < 0);
+
+ i_stream_seek(input, 0);
+ test_message_parser_get_sizes(input, &body_size, &header_size, FALSE);
+
+ test_assert(parts->children_count == 2);
+ test_assert(parts->flags == (MESSAGE_PART_FLAG_MULTIPART | MESSAGE_PART_FLAG_IS_MIME));
+ test_assert(parts->header_size.lines == 2);
+ test_assert(parts->header_size.physical_size == 45);
+ test_assert(parts->header_size.virtual_size == 45+2);
+ test_assert(parts->body_size.lines == 7);
+ test_assert(parts->body_size.physical_size == 84);
+ test_assert(parts->body_size.virtual_size == 84+7);
+ test_message_parser_assert_sizes(parts, &body_size, &header_size);
+
+ test_assert(parts->children->children_count == 1);
+ test_assert(parts->children->flags == (MESSAGE_PART_FLAG_MULTIPART | MESSAGE_PART_FLAG_IS_MIME));
+ test_assert(parts->children->physical_pos == 49);
+ test_assert(parts->children->header_size.lines == 2);
+ test_assert(parts->children->header_size.physical_size == 45);
+ test_assert(parts->children->header_size.virtual_size == 45+2);
+ test_assert(parts->children->body_size.lines == 4);
+ test_assert(parts->children->body_size.physical_size == 35);
+ test_assert(parts->children->body_size.virtual_size == 35+4);
+ test_assert(parts->children->children->children_count == 0);
+ test_assert(parts->children->children->flags == (MESSAGE_PART_FLAG_TEXT | MESSAGE_PART_FLAG_IS_MIME));
+ test_assert(parts->children->children->physical_pos == 98);
+ test_assert(parts->children->children->header_size.lines == 2);
+ test_assert(parts->children->children->header_size.physical_size == 26);
+ test_assert(parts->children->children->header_size.virtual_size == 26+2);
+ test_assert(parts->children->children->body_size.lines == 1);
+ test_assert(parts->children->children->body_size.physical_size == 5);
+ test_assert(parts->children->children->body_size.virtual_size == 5+1);
+
+ test_parsed_parts(input, parts);
+ i_stream_unref(&input);
+ pool_unref(&pool);
+ test_end();
+}
+
+static void test_message_parser_garbage_suffix_mime_boundary(void)
+{
+static const char input_msg[] =
+"Content-Type: multipart/mixed; boundary=\"a\"\n"
+"\n"
+"--ab\n"
+"Content-Type: multipart/mixed; boundary=\"a\"\n"
+"\n"
+"--ac\n"
+"Content-Type: text/plain\n"
+"\n"
+"body\n";
+ struct istream *input;
+ struct message_part *parts;
+ struct message_size body_size, header_size;
+ pool_t pool;
+
+ test_begin("message parser garbage suffix mime boundary");
+ pool = pool_alloconly_create("message parser", 10240);
+ input = test_istream_create(input_msg);
+
+ test_assert(message_parse_stream(pool, input, &set_empty, FALSE, &parts) < 0);
+
+ i_stream_seek(input, 0);
+ test_message_parser_get_sizes(input, &body_size, &header_size, FALSE);
+
+ test_assert(parts->children_count == 2);
+ test_assert(parts->flags == (MESSAGE_PART_FLAG_MULTIPART | MESSAGE_PART_FLAG_IS_MIME));
+ test_assert(parts->header_size.lines == 2);
+ test_assert(parts->header_size.physical_size == 45);
+ test_assert(parts->header_size.virtual_size == 45+2);
+ test_assert(parts->body_size.lines == 7);
+ test_assert(parts->body_size.physical_size == 86);
+ test_assert(parts->body_size.virtual_size == 86+7);
+ test_message_parser_assert_sizes(parts, &body_size, &header_size);
+
+ test_assert(parts->children->children_count == 1);
+ test_assert(parts->children->flags == (MESSAGE_PART_FLAG_MULTIPART | MESSAGE_PART_FLAG_IS_MIME));
+ test_assert(parts->children->physical_pos == 50);
+ test_assert(parts->children->header_size.lines == 2);
+ test_assert(parts->children->header_size.physical_size == 45);
+ test_assert(parts->children->header_size.virtual_size == 45+2);
+ test_assert(parts->children->body_size.lines == 4);
+ test_assert(parts->children->body_size.physical_size == 36);
+ test_assert(parts->children->body_size.virtual_size == 36+4);
+ test_assert(parts->children->children->children_count == 0);
+ test_assert(parts->children->children->flags == (MESSAGE_PART_FLAG_TEXT | MESSAGE_PART_FLAG_IS_MIME));
+ test_assert(parts->children->children->physical_pos == 100);
+ test_assert(parts->children->children->header_size.lines == 2);
+ test_assert(parts->children->children->header_size.physical_size == 26);
+ test_assert(parts->children->children->header_size.virtual_size == 26+2);
+ test_assert(parts->children->children->body_size.lines == 1);
+ test_assert(parts->children->children->body_size.physical_size == 5);
+ test_assert(parts->children->children->body_size.virtual_size == 5+1);
+
+ test_parsed_parts(input, parts);
+ i_stream_unref(&input);
+ pool_unref(&pool);
+ test_end();
+}
+
+static void test_message_parser_trailing_dashes(void)
+{
+static const char input_msg[] =
+"Content-Type: multipart/mixed; boundary=\"a--\"\n"
+"\n"
+"--a--\n"
+"Content-Type: multipart/mixed; boundary=\"a----\"\n"
+"\n"
+"--a----\n"
+"Content-Type: text/plain\n"
+"\n"
+"body\n"
+"--a------\n"
+"Content-Type: text/html\n"
+"\n"
+"body2\n"
+"--a----";
+ struct istream *input;
+ struct message_part *parts;
+ pool_t pool;
+
+ test_begin("message parser trailing dashes");
+ pool = pool_alloconly_create("message parser", 10240);
+ input = test_istream_create(input_msg);
+
+ test_assert(message_parse_stream(pool, input, &set_empty, FALSE, &parts) < 0);
+
+ test_assert(parts->children_count == 2);
+ test_assert(parts->children->next == NULL);
+ test_assert(parts->children->children_count == 1);
+ test_assert(parts->children->children->next == NULL);
+ test_assert(parts->children->children->children_count == 0);
+
+ test_parsed_parts(input, parts);
+ i_stream_unref(&input);
+ pool_unref(&pool);
+ test_end();
+}
+
+static void test_message_parser_continuing_mime_boundary(void)
+{
+static const char input_msg[] =
+"Content-Type: multipart/mixed; boundary=\"a\"\n"
+"\n"
+"--a\n"
+"Content-Type: multipart/mixed; boundary=\"ab\"\n"
+"\n"
+"--ab\n"
+"Content-Type: text/plain\n"
+"\n"
+"body\n";
+ struct istream *input;
+ struct message_part *parts;
+ pool_t pool;
+
+ test_begin("message parser continuing mime boundary");
+ pool = pool_alloconly_create("message parser", 10240);
+ input = test_istream_create(input_msg);
+
+ test_assert(message_parse_stream(pool, input, &set_empty, FALSE, &parts) < 0);
+
+ i_stream_seek(input, 0);
+
+ test_assert(parts->children_count == 2);
+ test_assert(parts->flags == (MESSAGE_PART_FLAG_MULTIPART | MESSAGE_PART_FLAG_IS_MIME));
+ test_assert(parts->header_size.lines == 2);
+ test_assert(parts->header_size.physical_size == 45);
+ test_assert(parts->header_size.virtual_size == 45+2);
+ test_assert(parts->body_size.lines == 7);
+ test_assert(parts->body_size.physical_size == 86);
+ test_assert(parts->body_size.virtual_size == 86+7);
+ test_assert(parts->children->children_count == 1);
+ test_assert(parts->children->flags == (MESSAGE_PART_FLAG_MULTIPART | MESSAGE_PART_FLAG_IS_MIME));
+ test_assert(parts->children->physical_pos == 49);
+ test_assert(parts->children->header_size.lines == 2);
+ test_assert(parts->children->header_size.physical_size == 46);
+ test_assert(parts->children->header_size.virtual_size == 46+2);
+ test_assert(parts->children->body_size.lines == 4);
+ test_assert(parts->children->body_size.physical_size == 36);
+ test_assert(parts->children->body_size.virtual_size == 36+4);
+ test_assert(parts->children->children->children_count == 0);
+ test_assert(parts->children->children->flags == (MESSAGE_PART_FLAG_TEXT | MESSAGE_PART_FLAG_IS_MIME));
+ test_assert(parts->children->children->physical_pos == 100);
+ test_assert(parts->children->children->header_size.lines == 2);
+ test_assert(parts->children->children->header_size.physical_size == 26);
+ test_assert(parts->children->children->header_size.virtual_size == 26+2);
+ test_assert(parts->children->children->body_size.lines == 1);
+ test_assert(parts->children->children->body_size.physical_size == 5);
+ test_assert(parts->children->children->body_size.virtual_size == 5+1);
+
+ test_parsed_parts(input, parts);
+ i_stream_unref(&input);
+ pool_unref(&pool);
+ test_end();
+}
+
+static void test_message_parser_continuing_truncated_mime_boundary(void)
+{
+static const char input_msg[] =
+"Content-Type: multipart/mixed; boundary=\"a\"\n"
+"\n"
+"--a\n"
+"Content-Type: multipart/mixed; boundary=\"ab\"\n"
+"MIME-Version: 1.0\n"
+"--ab\n"
+"Content-Type: text/plain\n"
+"\n"
+"--ab--\n"
+"--a--\n\n";
+ struct istream *input;
+ struct message_part *parts, *part;
+ struct message_size body_size, header_size;
+ pool_t pool;
+
+ test_begin("message parser continuing truncated mime boundary");
+ pool = pool_alloconly_create("message parser", 10240);
+ input = test_istream_create(input_msg);
+
+ test_assert(message_parse_stream(pool, input, &set_empty, FALSE, &parts) < 0);
+
+ i_stream_seek(input, 0);
+ test_message_parser_get_sizes(input, &body_size, &header_size, FALSE);
+
+ part = parts;
+ test_assert(part->children_count == 3);
+ test_assert(part->flags == (MESSAGE_PART_FLAG_MULTIPART | MESSAGE_PART_FLAG_IS_MIME));
+ test_assert(part->header_size.lines == 2);
+ test_assert(part->header_size.physical_size == 45);
+ test_assert(part->header_size.virtual_size == 45+2);
+ test_assert(part->body_size.lines == 9);
+ test_assert(part->body_size.physical_size == 112);
+ test_assert(part->body_size.virtual_size == 112+9);
+ test_message_parser_assert_sizes(part, &body_size, &header_size);
+
+ part = parts->children;
+ test_assert(part->children_count == 0);
+ test_assert(part->flags == (MESSAGE_PART_FLAG_MULTIPART | MESSAGE_PART_FLAG_IS_MIME));
+ test_assert(part->physical_pos == 49);
+ test_assert(part->header_size.lines == 1);
+ test_assert(part->header_size.physical_size == 45+17);
+ test_assert(part->header_size.virtual_size == 45+17+1);
+ test_assert(part->body_size.lines == 0);
+ test_assert(part->body_size.physical_size == 0);
+ test_assert(part->children == NULL);
+
+ /* this will not be a child, since the header was truncated. I guess
+ we could make it, but it would complicate the message-parser even
+ more. */
+ part = parts->children->next;
+ test_assert(part->children_count == 0);
+ test_assert(part->flags == (MESSAGE_PART_FLAG_TEXT | MESSAGE_PART_FLAG_IS_MIME));
+ test_assert(part->physical_pos == 117);
+ test_assert(part->header_size.lines == 1);
+ test_assert(part->header_size.physical_size == 25);
+ test_assert(part->header_size.virtual_size == 25+1);
+ test_assert(part->body_size.lines == 0);
+ test_assert(part->body_size.physical_size == 0);
+ test_assert(part->children == NULL);
+
+ part = parts->children->next->next;
+ test_assert(part->children_count == 0);
+ test_assert(part->flags == (MESSAGE_PART_FLAG_TEXT | MESSAGE_PART_FLAG_IS_MIME));
+ test_assert(part->header_size.lines == 0);
+ test_assert(part->header_size.physical_size == 0);
+ test_assert(part->body_size.lines == 0);
+ test_assert(part->body_size.physical_size == 0);
+ test_assert(part->children == NULL);
+ test_assert(part->next == NULL);
+
+ test_parsed_parts(input, parts);
+ i_stream_unref(&input);
+ pool_unref(&pool);
+ test_end();
+}
+
+static void test_message_parser_continuing_mime_boundary_reverse(void)
+{
+static const char input_msg[] =
+"Content-Type: multipart/mixed; boundary=\"ab\"\n"
+"\n"
+"--ab\n"
+"Content-Type: multipart/mixed; boundary=\"a\"\n"
+"\n"
+"--a\n"
+"Content-Type: text/plain\n"
+"\n"
+"body\n"
+"--ab\n"
+"Content-Type: text/html\n"
+"\n"
+"body2\n";
+ struct istream *input;
+ struct message_part *parts;
+ struct message_size body_size, header_size;
+ pool_t pool;
+
+ test_begin("message parser continuing mime boundary reverse");
+ pool = pool_alloconly_create("message parser", 10240);
+ input = test_istream_create(input_msg);
+
+ test_assert(message_parse_stream(pool, input, &set_empty, FALSE, &parts) < 0);
+
+ i_stream_seek(input, 0);
+ test_message_parser_get_sizes(input, &body_size, &header_size, FALSE);
+
+ test_assert(parts->children_count == 3);
+ test_assert(parts->flags == (MESSAGE_PART_FLAG_MULTIPART | MESSAGE_PART_FLAG_IS_MIME));
+ test_assert(parts->header_size.lines == 2);
+ test_assert(parts->header_size.physical_size == 46);
+ test_assert(parts->header_size.virtual_size == 46+2);
+ test_assert(parts->body_size.lines == 11);
+ test_assert(parts->body_size.physical_size == 121);
+ test_assert(parts->body_size.virtual_size == 121+11);
+ test_message_parser_assert_sizes(parts, &body_size, &header_size);
+
+ test_assert(parts->children->children_count == 1);
+ test_assert(parts->children->flags == (MESSAGE_PART_FLAG_MULTIPART | MESSAGE_PART_FLAG_IS_MIME));
+ test_assert(parts->children->physical_pos == 51);
+ test_assert(parts->children->header_size.lines == 2);
+ test_assert(parts->children->header_size.physical_size == 45);
+ test_assert(parts->children->header_size.virtual_size == 45+2);
+ test_assert(parts->children->body_size.lines == 3);
+ test_assert(parts->children->body_size.physical_size == 34);
+ test_assert(parts->children->body_size.virtual_size == 34+3);
+ test_assert(parts->children->children->children_count == 0);
+ test_assert(parts->children->children->flags == (MESSAGE_PART_FLAG_TEXT | MESSAGE_PART_FLAG_IS_MIME));
+ test_assert(parts->children->children->physical_pos == 100);
+ test_assert(parts->children->children->header_size.lines == 2);
+ test_assert(parts->children->children->header_size.physical_size == 26);
+ test_assert(parts->children->children->header_size.virtual_size == 26+2);
+ test_assert(parts->children->children->body_size.lines == 0);
+ test_assert(parts->children->children->body_size.physical_size == 4);
+ test_assert(parts->children->children->body_size.virtual_size == 4);
+ test_assert(parts->children->next->children_count == 0);
+ test_assert(parts->children->next->flags == (MESSAGE_PART_FLAG_TEXT | MESSAGE_PART_FLAG_IS_MIME));
+ test_assert(parts->children->next->physical_pos == 136);
+ test_assert(parts->children->next->header_size.lines == 2);
+ test_assert(parts->children->next->header_size.physical_size == 25);
+ test_assert(parts->children->next->header_size.virtual_size == 25+2);
+ test_assert(parts->children->next->body_size.lines == 1);
+ test_assert(parts->children->next->body_size.physical_size == 6);
+ test_assert(parts->children->next->body_size.virtual_size == 6+1);
+
+ test_parsed_parts(input, parts);
+ i_stream_unref(&input);
+ pool_unref(&pool);
+ test_end();
+}
+
+static void test_message_parser_no_eoh(void)
+{
+ static const char input_msg[] = "a:b\n";
+ struct message_parser_ctx *parser;
+ struct istream *input;
+ struct message_part *parts;
+ struct message_block block;
+ pool_t pool;
+
+ test_begin("message parser no EOH");
+ pool = pool_alloconly_create("message parser", 10240);
+ input = test_istream_create(input_msg);
+
+ parser = message_parser_init(pool, input, &set_empty);
+ test_assert(message_parser_parse_next_block(parser, &block) > 0 &&
+ block.hdr != NULL && strcmp(block.hdr->name, "a") == 0 &&
+ block.hdr->value_len == 1 && block.hdr->value[0] == 'b');
+ test_assert(message_parser_parse_next_block(parser, &block) > 0 &&
+ block.hdr == NULL && block.size == 0);
+ test_assert(message_parser_parse_next_block(parser, &block) < 0);
+ message_parser_deinit(&parser, &parts);
+ test_assert(input->stream_errno == 0);
+
+ test_parsed_parts(input, parts);
+ i_stream_unref(&input);
+ pool_unref(&pool);
+ test_end();
+}
+
+static void test_message_parser_long_mime_boundary(void)
+{
+ /* Close the boundaries in wrong reverse order. But because all
+ boundaries are actually truncated to the same size (..890) it
+ works the same as if all of them were duplicate boundaries. */
+static const char input_msg[] =
+"Content-Type: multipart/mixed; boundary=\"1234567890123456789012345678901234567890123456789012345678901234567890123456789012\"\n"
+"\n"
+"--1234567890123456789012345678901234567890123456789012345678901234567890123456789012\n"
+"Content-Type: multipart/mixed; boundary=\"123456789012345678901234567890123456789012345678901234567890123456789012345678901\"\n"
+"\n"
+"--123456789012345678901234567890123456789012345678901234567890123456789012345678901\n"
+"Content-Type: multipart/mixed; boundary=\"12345678901234567890123456789012345678901234567890123456789012345678901234567890\"\n"
+"\n"
+"--12345678901234567890123456789012345678901234567890123456789012345678901234567890\n"
+"Content-Type: text/plain\n"
+"\n"
+"1\n"
+"--1234567890123456789012345678901234567890123456789012345678901234567890123456789012\n"
+"Content-Type: text/plain\n"
+"\n"
+"22\n"
+"--123456789012345678901234567890123456789012345678901234567890123456789012345678901\n"
+"Content-Type: text/plain\n"
+"\n"
+"333\n"
+"--12345678901234567890123456789012345678901234567890123456789012345678901234567890\n"
+"Content-Type: text/plain\n"
+"\n"
+"4444\n";
+ struct istream *input;
+ struct message_part *parts, *part;
+ struct message_size body_size, header_size;
+ pool_t pool;
+
+ test_begin("message parser long mime boundary");
+ pool = pool_alloconly_create("message parser", 10240);
+ input = test_istream_create(input_msg);
+
+ test_assert(message_parse_stream(pool, input, &set_empty, FALSE, &parts) < 0);
+
+ i_stream_seek(input, 0);
+ test_message_parser_get_sizes(input, &body_size, &header_size, FALSE);
+
+ part = parts;
+ test_assert(part->children_count == 6);
+ test_assert(part->flags == (MESSAGE_PART_FLAG_MULTIPART | MESSAGE_PART_FLAG_IS_MIME));
+ test_assert(part->header_size.lines == 2);
+ test_assert(part->header_size.physical_size == 126);
+ test_assert(part->header_size.virtual_size == 126+2);
+ test_assert(part->body_size.lines == 22);
+ test_assert(part->body_size.physical_size == 871);
+ test_assert(part->body_size.virtual_size == 871+22);
+ test_message_parser_assert_sizes(part, &body_size, &header_size);
+
+ part = parts->children;
+ test_assert(part->children_count == 5);
+ test_assert(part->flags == (MESSAGE_PART_FLAG_MULTIPART | MESSAGE_PART_FLAG_IS_MIME));
+ test_assert(part->header_size.lines == 2);
+ test_assert(part->header_size.physical_size == 125);
+ test_assert(part->header_size.virtual_size == 125+2);
+ test_assert(part->body_size.lines == 19);
+ test_assert(part->body_size.physical_size == 661);
+ test_assert(part->body_size.virtual_size == 661+19);
+
+ part = parts->children->children;
+ test_assert(part->children_count == 4);
+ test_assert(part->flags == (MESSAGE_PART_FLAG_MULTIPART | MESSAGE_PART_FLAG_IS_MIME));
+ test_assert(part->header_size.lines == 2);
+ test_assert(part->header_size.physical_size == 124);
+ test_assert(part->header_size.virtual_size == 124+2);
+ test_assert(part->body_size.lines == 16);
+ test_assert(part->body_size.physical_size == 453);
+ test_assert(part->body_size.virtual_size == 453+16);
+
+ part = parts->children->children->children;
+ for (unsigned int i = 1; i <= 3; i++, part = part->next) {
+ test_assert(part->children_count == 0);
+ test_assert(part->flags == (MESSAGE_PART_FLAG_TEXT | MESSAGE_PART_FLAG_IS_MIME));
+ test_assert(part->header_size.lines == 2);
+ test_assert(part->header_size.physical_size == 26);
+ test_assert(part->header_size.virtual_size == 26+2);
+ test_assert(part->body_size.lines == 0);
+ test_assert(part->body_size.physical_size == i);
+ test_assert(part->body_size.virtual_size == i);
+ }
+
+ test_parsed_parts(input, parts);
+ i_stream_unref(&input);
+ pool_unref(&pool);
+ test_end();
+}
+
+static void test_message_parser_mime_part_nested_limit(void)
+{
+static const char input_msg[] =
+"Content-Type: multipart/mixed; boundary=\"1\"\n"
+"\n"
+"--1\n"
+"Content-Type: multipart/mixed; boundary=\"2\"\n"
+"\n"
+"--2\n"
+"Content-Type: text/plain\n"
+"\n"
+"1\n"
+"--2\n"
+"Content-Type: text/plain\n"
+"\n"
+"22\n"
+"--1\n"
+"Content-Type: text/plain\n"
+"\n"
+"333\n";
+ const struct message_parser_settings parser_set = {
+ .max_nested_mime_parts = 2,
+ };
+ struct istream *input;
+ struct message_part *parts, *part;
+ struct message_size body_size, header_size;
+ pool_t pool;
+
+ test_begin("message parser mime part nested limit");
+ pool = pool_alloconly_create("message parser", 10240);
+ input = test_istream_create(input_msg);
+
+ test_assert(message_parse_stream(pool, input, &parser_set, FALSE, &parts) < 0);
+
+ i_stream_seek(input, 0);
+ test_message_parser_get_sizes(input, &body_size, &header_size, FALSE);
+
+ part = parts;
+ test_assert(part->children_count == 2);
+ test_assert(part->flags == (MESSAGE_PART_FLAG_MULTIPART | MESSAGE_PART_FLAG_IS_MIME));
+ test_assert(part->header_size.lines == 2);
+ test_assert(part->header_size.physical_size == 45);
+ test_assert(part->header_size.virtual_size == 45+2);
+ test_assert(part->body_size.lines == 15);
+ test_assert(part->body_size.physical_size == 148);
+ test_assert(part->body_size.virtual_size == 148+15);
+ test_message_parser_assert_sizes(part, &body_size, &header_size);
+
+ part = parts->children;
+ test_assert(part->children_count == 0);
+ test_assert(part->flags == (MESSAGE_PART_FLAG_IS_MIME |
+ MESSAGE_PART_FLAG_OVERFLOW));
+ test_assert(part->header_size.lines == 2);
+ test_assert(part->header_size.physical_size == 45);
+ test_assert(part->header_size.virtual_size == 45+2);
+ test_assert(part->body_size.lines == 7);
+ test_assert(part->body_size.physical_size == 64);
+ test_assert(part->body_size.virtual_size == 64+7);
+
+ part = parts->children->next;
+ test_assert(part->children_count == 0);
+ test_assert(part->flags == (MESSAGE_PART_FLAG_TEXT | MESSAGE_PART_FLAG_IS_MIME));
+ test_assert(part->header_size.lines == 2);
+ test_assert(part->header_size.physical_size == 26);
+ test_assert(part->header_size.virtual_size == 26+2);
+ test_assert(part->body_size.lines == 1);
+ test_assert(part->body_size.physical_size == 4);
+ test_assert(part->body_size.virtual_size == 4+1);
+
+ test_parsed_parts(input, parts);
+ i_stream_unref(&input);
+ pool_unref(&pool);
+ test_end();
+}
+
+static void test_message_parser_mime_part_nested_limit_rfc822(void)
+{
+static const char input_msg[] =
+"Content-Type: message/rfc822\n"
+"\n"
+"Content-Type: message/rfc822\n"
+"\n"
+"Content-Type: text/plain\n"
+"\n"
+"1\n";
+ const struct message_parser_settings parser_set = {
+ .max_nested_mime_parts = 2,
+ };
+ struct istream *input;
+ struct message_part *parts, *part;
+ struct message_size body_size, header_size;
+ pool_t pool;
+
+ test_begin("message parser mime part nested limit rfc822");
+ pool = pool_alloconly_create("message parser", 10240);
+ input = test_istream_create(input_msg);
+
+ test_assert(message_parse_stream(pool, input, &parser_set, FALSE, &parts) < 0);
+
+ i_stream_seek(input, 0);
+ test_message_parser_get_sizes(input, &body_size, &header_size, FALSE);
+
+ part = parts;
+ test_assert(part->children_count == 1);
+ test_assert(part->flags == (MESSAGE_PART_FLAG_MESSAGE_RFC822 | MESSAGE_PART_FLAG_IS_MIME));
+ test_assert(part->header_size.lines == 2);
+ test_assert(part->header_size.physical_size == 30);
+ test_assert(part->header_size.virtual_size == 30+2);
+ test_assert(part->body_size.lines == 5);
+ test_assert(part->body_size.physical_size == 58);
+ test_assert(part->body_size.virtual_size == 58+5);
+ test_message_parser_assert_sizes(part, &body_size, &header_size);
+
+ part = parts->children;
+ test_assert(part->children_count == 0);
+ test_assert(part->flags == (MESSAGE_PART_FLAG_IS_MIME |
+ MESSAGE_PART_FLAG_OVERFLOW));
+ test_assert(part->header_size.lines == 2);
+ test_assert(part->header_size.physical_size == 30);
+ test_assert(part->header_size.virtual_size == 30+2);
+ test_assert(part->body_size.lines == 3);
+ test_assert(part->body_size.physical_size == 28);
+ test_assert(part->body_size.virtual_size == 28+3);
+
+ test_parsed_parts(input, parts);
+ i_stream_unref(&input);
+ pool_unref(&pool);
+ test_end();
+}
+
+static void test_message_parser_mime_part_limit(void)
+{
+static const char input_msg[] =
+"Content-Type: multipart/mixed; boundary=\"1\"\n"
+"\n"
+"--1\n"
+"Content-Type: multipart/mixed; boundary=\"2\"\n"
+"\n"
+"--2\n"
+"Content-Type: text/plain\n"
+"\n"
+"1\n"
+"--2\n"
+"Content-Type: text/plain\n"
+"\n"
+"22\n"
+"--1\n"
+"Content-Type: text/plain\n"
+"\n"
+"333\n";
+ const struct message_parser_settings parser_set = {
+ .max_total_mime_parts = 4,
+ };
+ struct istream *input;
+ struct message_part *parts, *part;
+ struct message_size body_size, header_size;
+ pool_t pool;
+
+ test_begin("message parser mime part limit");
+ pool = pool_alloconly_create("message parser", 10240);
+ input = test_istream_create(input_msg);
+
+ test_assert(message_parse_stream(pool, input, &parser_set, FALSE, &parts) < 0);
+
+ i_stream_seek(input, 0);
+ test_message_parser_get_sizes(input, &body_size, &header_size, FALSE);
+
+ part = parts;
+ test_assert(part->children_count == 3);
+ test_assert(part->flags == (MESSAGE_PART_FLAG_MULTIPART | MESSAGE_PART_FLAG_IS_MIME));
+ test_assert(part->header_size.lines == 2);
+ test_assert(part->header_size.physical_size == 45);
+ test_assert(part->header_size.virtual_size == 45+2);
+ test_assert(part->body_size.lines == 15);
+ test_assert(part->body_size.physical_size == 148);
+ test_assert(part->body_size.virtual_size == 148+15);
+ test_message_parser_assert_sizes(part, &body_size, &header_size);
+
+ part = parts->children;
+ test_assert(part->children_count == 2);
+ test_assert(part->flags == (MESSAGE_PART_FLAG_MULTIPART | MESSAGE_PART_FLAG_IS_MIME));
+ test_assert(part->header_size.lines == 2);
+ test_assert(part->header_size.physical_size == 45);
+ test_assert(part->header_size.virtual_size == 45+2);
+ test_assert(part->body_size.lines == 12);
+ test_assert(part->body_size.physical_size == 99);
+ test_assert(part->body_size.virtual_size == 99+12);
+
+ part = parts->children->children;
+ test_assert(part->children_count == 0);
+ test_assert(part->flags == (MESSAGE_PART_FLAG_TEXT | MESSAGE_PART_FLAG_IS_MIME));
+ test_assert(part->header_size.lines == 2);
+ test_assert(part->header_size.physical_size == 26);
+ test_assert(part->header_size.virtual_size == 26+2);
+ test_assert(part->body_size.lines == 0);
+ test_assert(part->body_size.physical_size == 1);
+ test_assert(part->body_size.virtual_size == 1);
+
+ part = parts->children->children->next;
+ test_assert(part->children_count == 0);
+ test_assert(part->flags == (MESSAGE_PART_FLAG_TEXT |
+ MESSAGE_PART_FLAG_IS_MIME |
+ MESSAGE_PART_FLAG_OVERFLOW));
+ test_assert(part->header_size.lines == 2);
+ test_assert(part->header_size.physical_size == 26);
+ test_assert(part->header_size.virtual_size == 26+2);
+ test_assert(part->body_size.lines == 5);
+ test_assert(part->body_size.physical_size == 37);
+ test_assert(part->body_size.virtual_size == 37+5);
+
+ test_parsed_parts(input, parts);
+ i_stream_unref(&input);
+ pool_unref(&pool);
+ test_end();
+}
+
+static void test_message_parser_mime_part_limit_rfc822(void)
+{
+static const char input_msg[] =
+"Content-Type: multipart/mixed; boundary=\"1\"\n"
+"\n"
+"--1\n"
+"Content-Type: multipart/mixed; boundary=\"2\"\n"
+"\n"
+"--2\n"
+"Content-Type: message/rfc822\n"
+"\n"
+"Content-Type: text/plain\n"
+"\n"
+"1\n"
+"--2\n"
+"Content-Type: message/rfc822\n"
+"\n"
+"Content-Type: text/plain\n"
+"\n"
+"22\n"
+"--1\n"
+"Content-Type: message/rfc822\n"
+"\n"
+"Content-Type: text/plain\n"
+"\n"
+"333\n";
+ const struct message_parser_settings parser_set = {
+ .max_total_mime_parts = 3,
+ };
+ struct message_parser_ctx *parser;
+ struct istream *input;
+ struct message_part *parts, *part;
+ struct message_block block;
+ pool_t pool;
+ int ret;
+
+ test_begin("message parser mime part limit rfc822");
+ pool = pool_alloconly_create("message parser", 10240);
+ input = test_istream_create(input_msg);
+
+ parser = message_parser_init(pool, input, &parser_set);
+ while ((ret = message_parser_parse_next_block(parser, &block)) > 0) ;
+ test_assert(ret < 0);
+ message_parser_deinit(&parser, &parts);
+
+ part = parts;
+ test_assert(part->children_count == 2);
+ test_assert(part->flags == (MESSAGE_PART_FLAG_MULTIPART | MESSAGE_PART_FLAG_IS_MIME));
+ test_assert(part->header_size.lines == 2);
+ test_assert(part->header_size.physical_size == 45);
+ test_assert(part->header_size.virtual_size == 45+2);
+ test_assert(part->body_size.lines == 21);
+ test_assert(part->body_size.physical_size == 238);
+ test_assert(part->body_size.virtual_size == 238+21);
+
+ part = parts->children;
+ test_assert(part->children_count == 1);
+ test_assert(part->flags == (MESSAGE_PART_FLAG_MULTIPART | MESSAGE_PART_FLAG_IS_MIME));
+ test_assert(part->header_size.lines == 2);
+ test_assert(part->header_size.physical_size == 45);
+ test_assert(part->header_size.virtual_size == 45+2);
+ test_assert(part->body_size.lines == 18);
+ test_assert(part->body_size.physical_size == 189);
+ test_assert(part->body_size.virtual_size == 189+18);
+
+ part = parts->children->children;
+ test_assert(part->children_count == 0);
+ test_assert(part->flags == (MESSAGE_PART_FLAG_IS_MIME |
+ MESSAGE_PART_FLAG_OVERFLOW));
+ test_assert(part->header_size.lines == 2);
+ test_assert(part->header_size.physical_size == 30);
+ test_assert(part->header_size.virtual_size == 30+2);
+ test_assert(part->body_size.lines == 15);
+ test_assert(part->body_size.physical_size == 155);
+ test_assert(part->body_size.virtual_size == 155+15);
+
+ test_parsed_parts(input, parts);
+ i_stream_unref(&input);
+ pool_unref(&pool);
+ test_end();
+}
+
+static void test_message_parser_mime_version(void)
+{
+ test_begin("message parser mime version");
+
+ /* Check that MIME version is accepted. */
+static const char *const input_msgs[] = {
+ /* valid mime header */
+"Content-Type: multipart/mixed; boundary=\"1\"\n"
+"MIME-Version: 1.0\n\n"
+"--1\n"
+"Content-Type: text/plain\n"
+"\n"
+"hello, world\n"
+"--1\n",
+ /* future mime header */
+"Content-Type: multipart/mixed; boundary=\"1\"\n"
+"MIME-Version: 2.0\n\n"
+"--1\n"
+"Content-Type: text/plain\n"
+"\n"
+"hello, world\n"
+"--1\n",
+ /* invalid value in mime header */
+"Content-Type: multipart/mixed; boundary=\"1\"\n"
+"MIME-Version: abc\n\n"
+"--1\n"
+"Content-Type: text/plain\n"
+"\n"
+"hello, world\n"
+"--1\n",
+ /* missing value in mime header */
+"Content-Type: multipart/mixed; boundary=\"1\"\n"
+"MIME-Version:\n\n"
+"--1\n"
+"Content-Type: text/plain\n"
+"\n"
+"hello, world\n"
+"--1\n"
+ };
+
+ const struct message_parser_settings parser_set = {
+ .max_total_mime_parts = 2,
+ .flags = MESSAGE_PARSER_FLAG_MIME_VERSION_STRICT,
+ };
+ struct istream *input;
+ struct message_part *parts, *part;
+ pool_t pool;
+
+ for (size_t i = 0; i < N_ELEMENTS(input_msgs); i++) {
+ ssize_t variance = (ssize_t)strlen(input_msgs[i]) - (ssize_t)strlen(input_msgs[0]);
+ pool = pool_alloconly_create("message parser", 10240);
+ input = test_istream_create(input_msgs[i]);
+
+ test_assert(message_parse_stream(pool, input, &parser_set, TRUE, &parts) < 0);
+ part = parts;
+
+ test_assert_idx(part->children_count == 1, i);
+ test_assert_idx(part->flags == (MESSAGE_PART_FLAG_MULTIPART | MESSAGE_PART_FLAG_IS_MIME), i);
+ test_assert_idx(part->header_size.lines == 3, i);
+ test_assert_idx(part->header_size.physical_size == (size_t)(63 + variance), i);
+ test_assert_idx(part->header_size.virtual_size == (size_t)(66 + variance), i);
+ test_assert_idx(part->body_size.lines == 5, i);
+ test_assert_idx(part->body_size.physical_size == 47, i);
+ test_assert_idx(part->body_size.virtual_size == 52, i);
+ test_assert_strcmp_idx(part->data->content_type, "multipart", i);
+ test_assert_strcmp_idx(part->data->content_subtype, "mixed", i);
+ part = part->children;
+
+ test_assert_idx(part->children_count == 0, i);
+ test_assert_idx(part->flags == (MESSAGE_PART_FLAG_TEXT |
+ MESSAGE_PART_FLAG_IS_MIME |
+ MESSAGE_PART_FLAG_OVERFLOW), i);
+ test_assert_idx(part->header_size.lines == 2, i);
+ test_assert_idx(part->header_size.physical_size == 26, i);
+ test_assert_idx(part->header_size.virtual_size == 28, i);
+ test_assert_idx(part->body_size.lines == 2, i);
+ test_assert_idx(part->body_size.physical_size == 17, i);
+ test_assert_strcmp_idx(part->data->content_type, "text", i);
+ test_assert_strcmp_idx(part->data->content_subtype, "plain", i);
+
+ test_parsed_parts(input, parts);
+ i_stream_unref(&input);
+ pool_unref(&pool);
+ };
+
+ /* test for +10MB header */
+ const size_t test_hdr_size = 10*1024*1024UL;
+ const size_t test_msg_size = test_hdr_size + 1024UL;
+ /* add space for parser */
+ pool = pool_alloconly_create("10mb header", test_msg_size + 10240UL);
+ string_t *buffer = str_new(pool, test_msg_size + 1);
+
+ str_append(buffer, "MIME-Version: ");
+
+ /* @UNSAFE */
+ char *tmp = buffer_append_space_unsafe(buffer, test_hdr_size);
+ memset(tmp, 'a', test_hdr_size);
+
+ str_append_c(buffer, '\n');
+ str_append(buffer, "Content-Type: multipart/mixed; boundary=1\n\n--1--");
+
+ input = test_istream_create_data(buffer->data, buffer->used);
+ test_assert(message_parse_stream(pool, input, &parser_set, TRUE, &parts) < 0);
+
+ i_stream_unref(&input);
+ pool_unref(&pool);
+ test_end();
+}
+
+static void test_message_parser_mime_version_missing(void)
+{
+ test_begin("message parser mime version missing");
+
+static const char input_msg[] =
+"Content-Type: multipart/mixed; boundary=\"1\"\n\n"
+"--1\n"
+"Content-Type: text/plain\n"
+"\n"
+"hello, world\n"
+"--1\n";
+
+ const struct message_parser_settings parser_set = {
+ .max_total_mime_parts = 2,
+ .flags = MESSAGE_PARSER_FLAG_MIME_VERSION_STRICT,
+ };
+ struct istream *input;
+ struct message_part *parts, *part;
+ pool_t pool;
+
+ pool = pool_alloconly_create("message parser", 10240);
+ input = test_istream_create(input_msg);
+
+ test_assert(message_parse_stream(pool, input, &parser_set, TRUE, &parts) < 0);
+ part = parts;
+
+ /* non-MIME message should end up as plain text mail */
+
+ test_assert(part->children_count == 0);
+ test_assert(part->flags == MESSAGE_PART_FLAG_TEXT);
+ test_assert(part->header_size.lines == 2);
+ test_assert(part->header_size.physical_size == 45);
+ test_assert(part->header_size.virtual_size == 47);
+ test_assert(part->body_size.lines == 5);
+ test_assert(part->body_size.physical_size == 47);
+ test_assert(part->body_size.virtual_size == 52);
+ test_assert(part->children == NULL);
+ test_assert(part->data->content_type == NULL);
+ test_assert(part->data->content_subtype == NULL);
+
+ test_parsed_parts(input, parts);
+ i_stream_unref(&input);
+ pool_unref(&pool);
+
+ test_end();
+}
+
+int main(void)
+{
+ static void (*const test_functions[])(void) = {
+ test_message_parser_small_blocks,
+ test_message_parser_stop_early,
+ test_message_parser_truncated_mime_headers,
+ test_message_parser_truncated_mime_headers2,
+ test_message_parser_truncated_mime_headers3,
+ test_message_parser_empty_multipart,
+ test_message_parser_duplicate_mime_boundary,
+ test_message_parser_garbage_suffix_mime_boundary,
+ test_message_parser_trailing_dashes,
+ test_message_parser_continuing_mime_boundary,
+ test_message_parser_continuing_truncated_mime_boundary,
+ test_message_parser_continuing_mime_boundary_reverse,
+ test_message_parser_long_mime_boundary,
+ test_message_parser_no_eoh,
+ test_message_parser_mime_part_nested_limit,
+ test_message_parser_mime_part_nested_limit_rfc822,
+ test_message_parser_mime_part_limit,
+ test_message_parser_mime_part_limit_rfc822,
+ test_message_parser_mime_version,
+ test_message_parser_mime_version_missing,
+ NULL
+ };
+ return test_run(test_functions);
+}
diff --git a/src/lib-mail/test-message-part-serialize.c b/src/lib-mail/test-message-part-serialize.c
new file mode 100644
index 0000000..2e03140
--- /dev/null
+++ b/src/lib-mail/test-message-part-serialize.c
@@ -0,0 +1,266 @@
+/* Copyright (c) 2020 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "str.h"
+#include "istream.h"
+#include "message-parser.h"
+#include "message-part.h"
+#include "message-part-serialize.h"
+#include "test-common.h"
+
+static const struct message_parser_settings set_empty = { .flags = 0 };
+
+static int message_parse_stream(pool_t pool, struct istream *input,
+ const struct message_parser_settings *set,
+ struct message_part **parts_r)
+{
+ int ret;
+ struct message_parser_ctx *parser;
+ struct message_block block;
+
+ i_zero(&block);
+ parser = message_parser_init(pool, input, set);
+ while ((ret = message_parser_parse_next_block(parser, &block)) > 0) ;
+ message_parser_deinit(&parser, parts_r);
+ test_assert(input->stream_errno == 0);
+ return ret;
+}
+
+static void test_parsed_parts(struct istream *input, struct message_part *parts)
+{
+ const struct message_parser_settings parser_set = {
+ .flags = MESSAGE_PARSER_FLAG_SKIP_BODY_BLOCK,
+ };
+ struct message_parser_ctx *parser;
+ struct message_block block;
+ struct message_part *parts2;
+ const char *error;
+
+ i_stream_seek(input, 0);
+
+ parser = message_parser_init_from_parts(parts, input, &parser_set);
+ while (message_parser_parse_next_block(parser, &block) > 0) ;
+ test_assert(message_parser_deinit_from_parts(&parser, &parts2, &error) == 0);
+ test_assert(message_part_is_equal(parts, parts2));
+}
+
+#define TEST_CASE_DATA(x) \
+ { .value = (const unsigned char*)((x)), .value_len = sizeof((x))-1 }
+
+static void test_message_serialize_deserialize(void)
+{
+static struct test_case {
+ struct test_case_data {
+ const unsigned char *value;
+ size_t value_len;
+ } input;
+ int expect_ret;
+} test_cases[] = {
+ {
+ .input = TEST_CASE_DATA("hello, world"),
+ .expect_ret = -1,
+ },
+ {
+ .input = TEST_CASE_DATA(
+"Subject: Hide and seek\n"
+"MIME-Version: 1.0\n"
+"Content-Type: multipart/mixed; boundary=1\n"
+"\n--1\n"
+"Content-Type: multipart/signed; protocol=\"signature/plain\"; migalc=\"pen+paper\"; boundary=2\n"
+"X-Signature-Type: penmanship\n"
+"\n--2\n"
+"Content-Type: multipart/alternative; boundary=3\n"
+"\n--3\n"
+"Content-Type: text/html; charset=us-ascii\n\n"
+"<html><head><title>Search me</title></head><body><p>Don't find me here</p></body></html>\n"
+"\n--3\n"
+"Content-Type: text/plain\n"
+"Content-Transfer-Encoding: binary\n"
+"\n"
+"Search me, and Find me here"
+"\n--3--\n"
+"\n--2\n"
+"Content-Type: signature/plain; charset=us-ascii\n"
+"\n"
+"Signed by undersigned"
+"\n--2--\n"
+"\n--1--"),
+ .expect_ret = -1,
+ },
+ {
+ .input = TEST_CASE_DATA(
+"From: Moderator-Address <moderator>\n" \
+"Content-Type: multipart/digest; boundary=1;\n" \
+"\n\n--1\n" \
+"From: someone-else <someone@else>\n" \
+"Subject: my opinion\n" \
+"\n" \
+"This is my opinion" \
+"\n--1\n\n" \
+"From: another one <another@one>\n" \
+"Subject: i disagree\n" \
+"\n" \
+"Not agreeing one bit!" \
+"\n--1\n\n" \
+"From: attachment <attachment@user>\n" \
+"Subject: funny hat\n" \
+"Content-Type: multipart/mixed; boundary=2\n" \
+"\n--2\n" \
+"Content-Type: text/plain\n"
+"Content-Transfer-Encoding: binary\n"
+"\n" \
+"Lovely attachment for you" \
+"\n--2\n" \
+"Content-Type: application/octet-stream; disposition=attachment; name=\"test.txt\"\n" \
+"Content-Transfer-Encoding: binary\n" \
+"\n" \
+"Foobar" \
+"\n--2--" \
+"\n--1--"),
+ .expect_ret = -1,
+ },
+};
+ test_begin("message part serialize deserialize");
+ for (size_t i = 0; i < N_ELEMENTS(test_cases); i++) {
+ const struct test_case *tc = &test_cases[i];
+ struct message_part *parts;
+ const char *error;
+ pool_t pool = pool_alloconly_create("message parser", 10240);
+ struct istream *is =
+ test_istream_create_data(tc->input.value, tc->input.value_len);
+ test_assert(message_parse_stream(pool, is, &set_empty, &parts) ==
+ tc->expect_ret);
+ buffer_t *dest = buffer_create_dynamic(pool, 256);
+ message_part_serialize(parts, dest);
+ parts = message_part_deserialize(pool, dest->data, dest->used,
+ &error);
+ test_assert(parts != NULL);
+ if (parts != NULL)
+ test_parsed_parts(is, parts);
+ else
+ i_error("message_part_deserialize: %s", error);
+ i_stream_unref(&is);
+ pool_unref(&pool);
+ }
+ test_end();
+}
+
+#define TEST_CASE(data, size, expect_error) \
+ test_assert(message_part_deserialize(pool, (data), (size), &error) == NULL); \
+ test_assert_strcmp(error, (expect_error))
+
+static void test_message_deserialize_errors(void)
+{
+ test_begin("message part deserialize errors");
+ const char *error = NULL;
+ struct message_part part, child1, child2;
+ pool_t pool = pool_datastack_create();
+ buffer_t *dest = buffer_create_dynamic(pool, 256);
+
+ /* empty part */
+ TEST_CASE("", 0, "Not enough data");
+
+ /* truncated part */
+ TEST_CASE("\x08\x00\x00", 3, "Not enough data");
+
+ /* bad sizes */
+ i_zero(&part);
+ part.flags = MESSAGE_PART_FLAG_TEXT;
+ part.header_size.virtual_size = 0;
+ part.header_size.physical_size = 100;
+ message_part_serialize(&part, dest);
+ TEST_CASE(dest->data, dest->used, "header_size.virtual_size too small");
+ buffer_set_used_size(dest, 0);
+
+ i_zero(&part);
+ part.flags = MESSAGE_PART_FLAG_TEXT;
+ part.body_size.virtual_size = 0;
+ part.body_size.physical_size = 100;
+ message_part_serialize(&part, dest);
+ TEST_CASE(dest->data, dest->used, "body_size.virtual_size too small");
+ buffer_set_used_size(dest, 0);
+
+ i_zero(&part);
+ part.flags = MESSAGE_PART_FLAG_MESSAGE_RFC822;
+ message_part_serialize(&part, dest);
+ TEST_CASE(dest->data, dest->used, "message/rfc822 part has no children");
+ buffer_set_used_size(dest, 0);
+
+ i_zero(&part);
+ i_zero(&child1);
+ i_zero(&child2);
+ part.flags = MESSAGE_PART_FLAG_MESSAGE_RFC822;
+ part.children_count = 2;
+ child1.flags = MESSAGE_PART_FLAG_TEXT;
+ child1.parent = &part;
+ part.children = &child1;
+ child2.flags = MESSAGE_PART_FLAG_TEXT;
+ part.children->next = &child2;
+ child2.parent = &part;
+ message_part_serialize(&part, dest);
+ TEST_CASE(dest->data, dest->used, "message/rfc822 part has multiple children");
+ buffer_set_used_size(dest, 0);
+
+ i_zero(&part);
+ i_zero(&child1);
+ part.flags = MESSAGE_PART_FLAG_MULTIPART|MESSAGE_PART_FLAG_IS_MIME;
+ part.children_count = 1;
+ child1.flags = MESSAGE_PART_FLAG_TEXT;
+ child1.parent = &part;
+ part.children = &child1;
+ message_part_serialize(&part, dest);
+ for (size_t i = 0; i < dest->used - 1; i++)
+ TEST_CASE(dest->data, i, "Not enough data");
+ buffer_append_c(dest, '\x00');
+ TEST_CASE(dest->data, dest->used, "Too much data");
+
+ test_end();
+}
+
+static enum fatal_test_state test_message_deserialize_fatals(unsigned int stage)
+{
+ const char *error = NULL;
+ struct message_part part, child1, child2;
+
+ pool_t pool = pool_datastack_create();
+ buffer_t *dest = buffer_create_dynamic(pool, 256);
+
+ switch(stage) {
+ case 0:
+ test_expect_fatal_string("part->children == NULL");
+ test_begin("message deserialize fatals");
+ i_zero(&part);
+ i_zero(&child1);
+ i_zero(&child2);
+ part.flags = MESSAGE_PART_FLAG_MULTIPART|MESSAGE_PART_FLAG_IS_MIME;
+ part.children_count = 1;
+ child1.flags = MESSAGE_PART_FLAG_TEXT;
+ child1.parent = &part;
+ part.children = &child1;
+ child2.parent = &child1;
+ child1.children_count = 1;
+ child1.children = &child2;
+
+ message_part_serialize(&part, dest);
+ TEST_CASE(dest->data, dest->used, "message/rfc822 part has multiple children");
+ buffer_set_used_size(dest, 0);
+ return FATAL_TEST_FAILURE;
+ };
+
+ test_end();
+ return FATAL_TEST_FINISHED;
+}
+
+int main(void)
+{
+ static void (*const test_functions[])(void) = {
+ test_message_serialize_deserialize,
+ test_message_deserialize_errors,
+ NULL
+ };
+ static enum fatal_test_state (*const fatal_functions[])(unsigned int) = {
+ test_message_deserialize_fatals,
+ NULL
+ };
+ return test_run_with_fatals(test_functions, fatal_functions);
+}
diff --git a/src/lib-mail/test-message-part.c b/src/lib-mail/test-message-part.c
new file mode 100644
index 0000000..49cfd65
--- /dev/null
+++ b/src/lib-mail/test-message-part.c
@@ -0,0 +1,117 @@
+/* Copyright (c) 2014-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "istream.h"
+#include "message-parser.h"
+#include "test-common.h"
+
+static const char test_msg[] =
+"From user@domain Fri Feb 22 17:06:23 2008\n"
+"From: user@domain.org\n"
+"Date: Sat, 24 Mar 2007 23:00:00 +0200\n"
+"Mime-Version: 1.0\n"
+"Content-Type: multipart/mixed; boundary=\"foo bar\"\n"
+"\n"
+"Root MIME prologue\n"
+"\n"
+"--foo bar\n"
+"Content-Type: text/x-myown; charset=us-ascii\n"
+"\n"
+"hello\n"
+"\n"
+"--foo bar\n"
+"Content-Type: message/rfc822\n"
+"\n"
+"From: sub@domain.org\n"
+"Date: Sun, 12 Aug 2012 12:34:56 +0300\n"
+"Subject: submsg\n"
+"Content-Type: multipart/alternative; boundary=\"sub1\"\n"
+"\n"
+"Sub MIME prologue\n"
+"--sub1\n"
+"Content-Type: text/html\n"
+"\n"
+"<p>Hello world</p>\n"
+"\n"
+"--sub1\n"
+"Content-Type: multipart/alternative; boundary=\"sub2\"\n"
+"\n"
+"--sub2\n"
+"Content-Type: multipart/alternative; boundary=\"sub3\"\n"
+"\n"
+"--sub3\n"
+"\n"
+"sub3 text\n"
+"--sub3\n"
+"\n"
+"sub3 text2\n"
+"--sub3--\n"
+"\n"
+"sub2 text\n"
+"--sub2\n"
+"\n"
+"sub2 text2\n"
+"--sub1--\n"
+"Sub MIME epilogue\n"
+"\n"
+"--foo bar\n"
+"Content-Type: text/plain\n"
+"\n"
+"Another part\n"
+"--foo bar--\n"
+"Root MIME epilogue\n"
+"\n";
+#define TEST_MSG_LEN (sizeof(test_msg)-1)
+
+static void test_message_part_idx(void)
+{
+ const struct message_parser_settings set = { .flags = 0 };
+ struct message_parser_ctx *parser;
+ struct istream *input;
+ struct message_part *parts, *part, *prev_part;
+ struct message_block block;
+ unsigned int i, prev_idx = 0, part_idx;
+ pool_t pool;
+ int ret;
+
+ test_begin("message part indexes");
+ pool = pool_alloconly_create("message parser", 10240);
+ input = i_stream_create_from_data(test_msg, TEST_MSG_LEN);
+
+ parser = message_parser_init(pool, input, &set);
+ while ((ret = message_parser_parse_next_block(parser, &block)) > 0) {
+ part_idx = message_part_to_idx(block.part);
+ test_assert(part_idx >= prev_idx);
+ prev_idx = part_idx;
+ }
+ test_assert(ret < 0);
+ message_parser_deinit(&parser, &parts);
+ test_assert(input->stream_errno == 0);
+
+ part = message_part_by_idx(parts, 0);
+ test_assert(part == parts);
+ test_assert(message_part_by_idx(parts, 1) == parts->children);
+
+ for (i = 1; i < 11; i++) {
+ prev_part = part;
+ part = message_part_by_idx(parts, i);
+ test_assert(part != NULL);
+ test_assert(part != NULL && message_part_to_idx(part) == i);
+ test_assert(part != NULL && prev_part != NULL &&
+ prev_part->physical_pos < part->physical_pos);
+ }
+ test_assert(message_part_by_idx(parts, i) == NULL);
+
+ i_stream_unref(&input);
+ pool_unref(&pool);
+ test_end();
+}
+
+int main(void)
+{
+ static void (*const test_functions[])(void) = {
+ test_message_part_idx,
+ NULL
+ };
+ return test_run(test_functions);
+}
diff --git a/src/lib-mail/test-message-search.c b/src/lib-mail/test-message-search.c
new file mode 100644
index 0000000..d137a58
--- /dev/null
+++ b/src/lib-mail/test-message-search.c
@@ -0,0 +1,521 @@
+/* Copyright (c) 2016-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "istream.h"
+#include "str.h"
+#include "unichar.h"
+#include "message-parser.h"
+#include "message-search.h"
+#include "test-common.h"
+
+struct test_case_data {
+ const unsigned char *value;
+ size_t value_len;
+};
+
+#define TEST_CASE_DATA(x) \
+ { .value = (const unsigned char*)((x)), .value_len = sizeof((x))-1 }
+#define TEST_CASE_DATA_EMPTY \
+ { .value = NULL, .value_len = 0 }
+#define TEST_CASE_PLAIN_PREAMBLE \
+"Content-Type: text/plain\n" \
+"Content-Transfer-Encoding: binary\n"
+
+struct test_case {
+ struct test_case_data input;
+ const char *search;
+ struct test_case_data output;
+ bool expect_found;
+ bool expect_body;
+ bool expect_header;
+ const char *hdr_name;
+};
+
+static void compare_search_result(const struct test_case *tc,
+ const struct message_block *block,
+ size_t i)
+{
+ if (block->hdr != NULL) {
+ /* found header */
+ test_assert_idx(tc->expect_header == TRUE, i);
+ test_assert_strcmp_idx(tc->hdr_name, block->hdr->name, i);
+ test_assert_idx(block->hdr->full_value != NULL &&
+ tc->output.value != NULL &&
+ tc->output.value_len <= block->hdr->full_value_len &&
+ memcmp(tc->output.value, block->hdr->full_value,
+ tc->output.value_len) == 0, i);
+ } else if (block->data != NULL) {
+ /* found body */
+ test_assert_idx(tc->expect_body == TRUE, i);
+ test_assert_idx(block->data != NULL &&
+ tc->output.value != NULL &&
+ tc->output.value_len <= block->size &&
+ memcmp(tc->output.value, block->data,
+ tc->output.value_len) == 0, i);
+ } else {
+ test_assert_idx(tc->expect_header == FALSE, i);
+ test_assert_idx(tc->expect_body == FALSE, i);
+ }
+}
+
+#define SIGNED_MIME_CORPUS \
+"Subject: Hide and seek\n" \
+"MIME-Version: 1.0\n" \
+"Content-Type: multipart/mixed; boundary=1\n" \
+"\n--1\n" \
+"Content-Type: multipart/signed; protocol=\"signature/plain\"; migalc=\"pen+paper\"; boundary=2\n" \
+"X-Signature-Type: penmanship\n" \
+"\n--2\n" \
+"Content-Type: multipart/alternative; boundary=3\n" \
+"\n--3\n" \
+"Content-Type: text/html; charset=us-ascii\n\n" \
+"<html><head><title>Search me</title></head><body><p>Don't find me here</p></body></html>\n" \
+"\n--3\n" \
+TEST_CASE_PLAIN_PREAMBLE \
+"\n" \
+"Search me, and Find me here" \
+"\n--3--\n" \
+"\n--2\n" \
+"Content-Type: signature/plain; charset=us-ascii\n" \
+"\n" \
+"Signed by undersigned" \
+"\n--2--\n" \
+"\n--1--"
+
+#define PARTIAL_MESSAGE_CORPUS \
+"X-Weird-Header-1: Bar\n" \
+"X-Weird-Header-2: Hello\n" \
+"Message-ID: <c6cceebc-1dcf-11eb-be8c-f7ca132cbfea@example.org>\n" \
+"Content-Type: text/plain; charset=\"us-ascii\"\n" \
+"Content-Transfer-Encoding: base64\n" \
+"\n" \
+"dGhpcyBpcyB0aGUgZmlyc3QgcGFydCBvZiB0aGUgbWVzc2FnZQo="
+
+#define PARTIAL_MIME_CORPUS \
+"Subject: In parts\n" \
+"MIME-Version: 1.0\n" \
+"Content-Type: multipart/mixed; boundary=1\n" \
+"\n--1\n" \
+TEST_CASE_PLAIN_PREAMBLE \
+"\n" \
+"Hi, this is the fancy thing I was talking about!" \
+"\n--1\n" \
+"Content-Type: Message/Partial; number=1; total=5; id=\"heks68ewe@example.org\"\n" \
+"\n" \
+PARTIAL_MESSAGE_CORPUS \
+"\n--1--\n"
+
+#define UT8_CORPUS_CONTENT \
+"\xe4\xba\xba\xe6\xa8\xa9\xe3\x81\xae\xe7\x84\xa1\xe8\xa6\x96\xe5\x8f\x8a"
+
+#define UTF8_CORPUS \
+"Subject: =?UTF-8?B?44GT44KT44Gr44Gh44Gv?=\n" \
+"MIME-Version: 1.0\n" \
+"Content-Type: multipart/mixed; boundary=1;\n" \
+" comment=\"\xe3\x81\x93\xe3\x82\x8c\xe3\x81\xaf\xe5\xa2\x83\xe7\x95\x8c\xe3" \
+ "\x81\xae\xe3\x81\x82\xe3\x82\x8b\xe3\x83\xa1\xe3\x83\x83\xe3\x82" \
+ "\xbb\xe3\x83\xbc\xe3\x82\xb8\xe3\x81\xa7\xe3\x81\x99\"\n" \
+"\n--1\n" \
+TEST_CASE_PLAIN_PREAMBLE \
+"Content-Language: ja\n" \
+"\n" \
+UT8_CORPUS_CONTENT \
+"\n--1--"
+
+#define MULTIPART_DIGEST_CORPUS \
+"From: Moderator-Address <moderator>\n" \
+"Content-Type: multipart/digest; boundary=1;\n" \
+"\n\n--1\n" \
+"From: someone-else <someone@else>\n" \
+"Subject: my opinion\n" \
+"\n" \
+"This is my opinion" \
+"\n--1\n\n" \
+"From: another one <another@one>\n" \
+"Subject: i disagree\n" \
+"\n" \
+"Not agreeing one bit!" \
+"\n--1\n\n" \
+"From: attachment <attachment@user>\n" \
+"Subject: funny hat\n" \
+"Content-Type: multipart/mixed; boundary=2\n" \
+"\n--2\n" \
+TEST_CASE_PLAIN_PREAMBLE \
+"\n" \
+"Lovely attachment for you" \
+"\n--2\n" \
+"Content-Type: application/octet-stream; disposition=attachment; name=\"test.txt\"\n" \
+"Content-Transfer-Encoding: binary\n" \
+"\n" \
+"Foobar" \
+"\n--2--" \
+"\n--1--"
+
+static void test_message_search(void)
+{
+ const struct test_case test_cases[] = {
+ { /* basic test */
+ .input = TEST_CASE_DATA(
+"MIME-Version: 1.0\n"
+TEST_CASE_PLAIN_PREAMBLE
+"\n"
+"Hello, world"),
+ .search = "Hello",
+ .output = TEST_CASE_DATA("Hello, world"),
+ .expect_found = TRUE,
+ .expect_body = TRUE,
+ },
+ { /* look for something that's not found */
+ .input = TEST_CASE_DATA(
+"MIME-Version: 1.0\n"
+TEST_CASE_PLAIN_PREAMBLE
+"\n"
+"Hallo, world"),
+ .search = "Hello",
+ .output = TEST_CASE_DATA_EMPTY,
+ .expect_found = FALSE,
+ },
+ { /* header value search */
+ .input = TEST_CASE_DATA(
+"Subject: Hello, World\n"
+"MIME-Version: 1.0\n"
+TEST_CASE_PLAIN_PREAMBLE
+"\n"
+"Hallo, world"),
+ .search = "Hello",
+ .output = TEST_CASE_DATA("Hello, World"),
+ .expect_found = TRUE,
+ .expect_body = FALSE,
+ .expect_header = TRUE,
+ .hdr_name = "Subject",
+ },
+ { /* header value wrapped in base64 */
+ .input = TEST_CASE_DATA(
+"Subject: =?UTF-8?B?SGVsbG8sIFdvcmxk?=\n"
+"MIME-Version: 1.0\n"
+TEST_CASE_PLAIN_PREAMBLE
+"\n"
+"Hallo, world"),
+ .search = "Hello",
+ .output = TEST_CASE_DATA("Hello, World"),
+ .expect_found = TRUE,
+ .expect_body = FALSE,
+ .expect_header = TRUE,
+ .hdr_name = "Subject",
+ },
+ { /* hidden inside one multipart */
+ .input = TEST_CASE_DATA(
+"Subject: Hide and seek\n"
+"MIME-Version: 1.0\n"
+"CONTENT-TYPE: MULTIPART/MIXED; BOUNDARY=\"A\"\n\n"
+"--A\n"
+TEST_CASE_PLAIN_PREAMBLE
+"\n"
+"Hallo, world"
+"\n--A\n"
+TEST_CASE_PLAIN_PREAMBLE
+"\n"
+"Hullo, world"
+"\n--A\n"
+TEST_CASE_PLAIN_PREAMBLE
+"\n"
+"Hello, world"
+"\n--A--\n"
+),
+ .search = "Hello",
+ .output = TEST_CASE_DATA("Hello, world"),
+ .expect_found = TRUE,
+ .expect_body = TRUE,
+ },
+ { /* same with emoji boundary */
+ .input = TEST_CASE_DATA(
+"Subject: Hide and seek\n"
+"MIME-Version: 1.0\n"
+"CONTENT-TYPE: MULTIPART/MIXED; BOUNDARY=\"\xF0\x9F\x98\x82\"; COMMENT=\"Boundary is U+1F602\"\n\n"
+"--\xF0\x9F\x98\x82\n"
+TEST_CASE_PLAIN_PREAMBLE
+"\n"
+"Face with Tears of Joy"
+"\n--\xF0\x9F\x98\x82\n"
+TEST_CASE_PLAIN_PREAMBLE
+"\n"
+"Emoji"
+"\n--\xF0\x9F\x98\x82--\n"
+),
+ .search = "Emoji",
+ .output = TEST_CASE_DATA("Emoji"),
+ .expect_found = TRUE,
+ .expect_body = TRUE,
+ },
+ { /* Nested body search */
+ .input = TEST_CASE_DATA(SIGNED_MIME_CORPUS),
+ .search = "Find me here",
+ .output = TEST_CASE_DATA("Search me, and Find me here"),
+ .expect_found = TRUE,
+ .expect_body = TRUE,
+ },
+ { /* Nested body search (won't look into signature/plain) */
+ .input = TEST_CASE_DATA(SIGNED_MIME_CORPUS),
+ .search = "undersigned",
+ .output = TEST_CASE_DATA_EMPTY,
+ .expect_found = FALSE,
+ },
+ { /* Nested mime part header search */
+ .input = TEST_CASE_DATA(SIGNED_MIME_CORPUS),
+ .search = "penmanship",
+ .output = TEST_CASE_DATA("penmanship"),
+ .expect_found = TRUE,
+ .expect_body = FALSE,
+ .expect_header = TRUE,
+ .hdr_name = "X-Signature-Type",
+ },
+ { /* Nested mime part header parameter search */
+ .input = TEST_CASE_DATA(SIGNED_MIME_CORPUS),
+ .search = "pen+paper",
+ .output = TEST_CASE_DATA("multipart/signed; protocol=\"signature/plain\"; migalc=\"pen+paper\"; boundary=2"),
+ .expect_found = TRUE,
+ .expect_body = FALSE,
+ .expect_header = TRUE,
+ .hdr_name = "Content-Type",
+ },
+ { /* Partial message - must not parse the content */
+ .input = TEST_CASE_DATA(PARTIAL_MIME_CORPUS),
+ .search = "Bar",
+ .output = TEST_CASE_DATA(PARTIAL_MESSAGE_CORPUS),
+ .expect_found = TRUE,
+ .expect_body = TRUE,
+ },
+ { /* Partial message - must not parse the content */
+ .input = TEST_CASE_DATA(PARTIAL_MIME_CORPUS),
+ .search = "fancy thing",
+ .output = TEST_CASE_DATA("Hi, this is the fancy thing I was talking about!"),
+ .expect_found = TRUE,
+ .expect_body = TRUE,
+ },
+ { /* UTF-8 searches */
+ .input = TEST_CASE_DATA(UTF8_CORPUS),
+ .search = "\xe4\xba\xba\xe6\xa8\xa9",
+ .output = TEST_CASE_DATA(UT8_CORPUS_CONTENT),
+ .expect_found = TRUE,
+ .expect_body = TRUE,
+ },
+ { /* UTF-8 search header */
+ .input = TEST_CASE_DATA(UTF8_CORPUS),
+ .search = "\xe3\x81\x93\xe3\x82\x93",
+ .output = TEST_CASE_DATA("\xe3\x81\x93\xe3\x82\x93\xe3\x81\xab\xe3\x81\xa1\xe3\x81\xaf"),
+ .expect_found = TRUE,
+ .expect_body = FALSE,
+ .expect_header = TRUE,
+ .hdr_name = "Subject",
+ },
+ { /* UTF-8 searches content-type parameter */
+ .input = TEST_CASE_DATA(UTF8_CORPUS),
+ .search = "\xe3\x81\xa7\xe3\x81\x99",
+ .output = TEST_CASE_DATA(
+"multipart/mixed; boundary=1;\n comment=\"\xe3\x81\x93\xe3\x82\x8c\xe3\x81\xaf"
+"\xe5\xa2\x83\xe7\x95\x8c\xe3\x81\xae\xe3\x81\x82\xe3\x82\x8b\xe3\x83\xa1\xe3"
+"\x83\x83\xe3\x82\xbb\xe3\x83\xbc\xe3\x82\xb8\xe3\x81\xa7\xe3\x81\x99\""),
+ .expect_found = TRUE,
+ .expect_body = FALSE,
+ .expect_header = TRUE,
+ .hdr_name = "Content-Type",
+ },
+ {
+ /* Invalid UTF-8 boundary (should not matter) */
+ .input = TEST_CASE_DATA(
+"Content-Type: multipart/mixed; boundary=\"\xff\xff\xff\xff\"\n"
+"\n--\xff\xff\xff\xff\n"
+TEST_CASE_PLAIN_PREAMBLE
+"\n"
+"Can you find me?"
+"\n--\xff\xff\xff\xff--"),
+ .search = "Can you find me?",
+ .output = TEST_CASE_DATA("Can you find me?"),
+ .expect_found = TRUE,
+ .expect_body = TRUE,
+ },
+ {
+ /* Invalid UTF-8 in subject (should work) */
+ .input = TEST_CASE_DATA(
+"Subject: =?UTF-8?B?Um90dGVuIP////8gdGV4dA==?="
+TEST_CASE_PLAIN_PREAMBLE
+"\n"
+"Such horror"),
+ .search = "Rotten",
+ .output = TEST_CASE_DATA("Rotten "UNICODE_REPLACEMENT_CHAR_UTF8" text"),
+ .expect_found = TRUE,
+ .expect_body = FALSE,
+ .expect_header = TRUE,
+ .hdr_name = "Subject",
+ },
+ {
+ /* Invalid UTF-8 in body (should work) */
+ .input = TEST_CASE_DATA(
+"Subject: =?UTF-8?B?Um90dGVuIP////8gdGV4dA==?="
+TEST_CASE_PLAIN_PREAMBLE
+"\n"
+"Such horror \xff\xff\xff\xff"),
+ .search = "Such horror",
+ .output = TEST_CASE_DATA("Such horror "UNICODE_REPLACEMENT_CHAR_UTF8),
+ .expect_found = TRUE,
+ .expect_body = TRUE,
+ },
+ {
+ /* UTF-8 in content-type parameter */
+ .input = TEST_CASE_DATA(
+"Content-Type: multipart/mixed; boundary=1; \xF0\x9F\x98\xAD=\"\xF0\x9F\xA5\xBA U+1F62D=U+1F97A\"\n"
+"\n--1--\n"),
+ .search = "U+1F62D",
+ .output = TEST_CASE_DATA("multipart/mixed; boundary=1; \xF0\x9F\x98\xAD=\"\xF0\x9F\xA5\xBA U+1F62D=U+1F97A\""),
+ .expect_found = TRUE,
+ .expect_body = FALSE,
+ .expect_header = TRUE,
+ .hdr_name = "Content-Type",
+ },
+ {
+ /* Broken UTF-8 in content-type parameter */
+ .input = TEST_CASE_DATA(
+"Content-Type: multipart/mixed; boundary=1;"
+" \xFF\xFF\xFF\xFF=\"\xF0\x9F\xA5\xBA U+1F62D=U+1F97A\"\n"
+"\n--1--\n"),
+ .search = "U+1F62D",
+ .output = TEST_CASE_DATA("multipart/mixed; boundary=1; "UNICODE_REPLACEMENT_CHAR_UTF8"=\"\xF0\x9F\xA5\xBA U+1F62D=U+1F97A\""),
+ .expect_found = TRUE,
+ .expect_body = FALSE,
+ .expect_header = TRUE,
+ .hdr_name = "Content-Type",
+ },
+ { /* Multipart digest */
+ .input = TEST_CASE_DATA(MULTIPART_DIGEST_CORPUS),
+ .search = "Not agreeing",
+ .output = TEST_CASE_DATA("Not agreeing one bit!"),
+ .expect_found = TRUE,
+ .expect_body = TRUE,
+ },
+ { /* Multipart digest header */
+ .input = TEST_CASE_DATA(MULTIPART_DIGEST_CORPUS),
+ .search = "someone-else",
+ .output = TEST_CASE_DATA("someone-else <someone@else>"),
+ .expect_found = TRUE,
+ .expect_body = FALSE,
+ .expect_header = TRUE,
+ .hdr_name = "From",
+ },
+ { /* Multipart digest header parameter */
+ .input = TEST_CASE_DATA(MULTIPART_DIGEST_CORPUS),
+ .search = "test.txt",
+ .output = TEST_CASE_DATA("application/octet-stream; disposition=attachment; name=\"test.txt\""),
+ .expect_found = TRUE,
+ .expect_body = FALSE,
+ .expect_header = TRUE,
+ .hdr_name = "Content-Type",
+ },
+};
+
+ test_begin("message search");
+
+ for (size_t i = 0; i < N_ELEMENTS(test_cases); i++) T_BEGIN {
+ struct message_search_context *sctx;
+ struct message_block raw_block, decoded_block;
+ struct message_part *parts;
+ const char *error;
+ bool found = FALSE;
+ const struct test_case *tc = &test_cases[i];
+ struct message_parser_settings set = {
+ .hdr_flags = MESSAGE_HEADER_PARSER_FLAG_SKIP_INITIAL_LWSP,
+ };
+ pool_t pool = pool_alloconly_create("message parser", 10240);
+ struct istream *is =
+ test_istream_create_data(tc->input.value, tc->input.value_len);
+ struct message_parser_ctx *pctx =
+ message_parser_init(pool, is, &set);
+ int ret;
+ sctx = message_search_init(tc->search, NULL, tc->expect_header ?
+ 0 : MESSAGE_SEARCH_FLAG_SKIP_HEADERS);
+ while ((ret = message_parser_parse_next_block(pctx, &raw_block)) > 0) {
+ if (message_search_more_get_decoded(sctx, &raw_block,
+ &decoded_block)) {
+ found = TRUE;
+ compare_search_result(tc, &decoded_block, i);
+ }
+ }
+ test_assert(ret == -1);
+ test_assert_idx(tc->expect_found == found, i);
+ message_parser_deinit(&pctx, &parts);
+ test_assert(is->stream_errno == 0);
+ i_stream_seek(is, 0);
+ if ((ret = message_search_msg(sctx, is, parts, &error)) < 0) {
+ i_error("Search error: %s", error);
+ } else {
+ test_assert_idx(tc->expect_found == (ret == 1), i);
+ }
+ /* and once more */
+ i_stream_seek(is, 0);
+ if ((ret = message_search_msg(sctx, is, NULL, &error)) < 0) {
+ i_error("Search error: %s", error);
+ } else {
+ test_assert_idx(tc->expect_found == (ret == 1), i);
+ }
+ message_search_deinit(&sctx);
+ test_assert(is->stream_errno == 0);
+ i_stream_unref(&is);
+ pool_unref(&pool);
+ } T_END;
+
+ test_end();
+
+}
+
+static void test_message_search_more_get_decoded(void)
+{
+ const char input[] = "p\xC3\xB6\xC3\xB6";
+ const unsigned char text_plain[] = "text/plain; charset=utf-8";
+ struct message_search_context *ctx1, *ctx2;
+ struct message_block raw_block, decoded_block;
+ struct message_header_line hdr;
+ struct message_part part;
+ unsigned int i;
+
+ test_begin("message_search_more_get_decoded()");
+
+ ctx1 = message_search_init("p\xC3\xA4\xC3\xA4", NULL, 0);
+ ctx2 = message_search_init("p\xC3\xB6\xC3\xB6", NULL, 0);
+
+ i_zero(&raw_block);
+ raw_block.part = &part;
+
+ /* feed the Content-Type header */
+ i_zero(&hdr);
+ hdr.name = "Content-Type"; hdr.name_len = strlen(hdr.name);
+ hdr.value = hdr.full_value = text_plain;
+ hdr.value_len = hdr.full_value_len = sizeof(text_plain)-1;
+ raw_block.hdr = &hdr;
+ test_assert(!message_search_more_get_decoded(ctx1, &raw_block, &decoded_block));
+ test_assert(!message_search_more_decoded(ctx2, &decoded_block));
+
+ /* EOH */
+ raw_block.hdr = NULL;
+ test_assert(!message_search_more_get_decoded(ctx1, &raw_block, &decoded_block));
+ test_assert(!message_search_more_decoded(ctx2, &decoded_block));
+
+ /* body */
+ raw_block.size = 1;
+ for (i = 0; input[i] != '\0'; i++) {
+ raw_block.data = (const void *)&input[i];
+ test_assert(!message_search_more_get_decoded(ctx1, &raw_block, &decoded_block));
+ test_assert(message_search_more_decoded(ctx2, &decoded_block) == (input[i+1] == '\0'));
+ }
+ message_search_deinit(&ctx1);
+ message_search_deinit(&ctx2);
+ test_end();
+}
+
+int main(void)
+{
+ static void (*const test_functions[])(void) = {
+ test_message_search,
+ test_message_search_more_get_decoded,
+ NULL
+ };
+ return test_run(test_functions);
+}
diff --git a/src/lib-mail/test-message-size.c b/src/lib-mail/test-message-size.c
new file mode 100644
index 0000000..4342c27
--- /dev/null
+++ b/src/lib-mail/test-message-size.c
@@ -0,0 +1,159 @@
+/* Copyright (c) 2020 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "istream.h"
+#include "message-size.h"
+#include "test-common.h"
+
+static const char test_msg[] =
+"Return-Path: <test@example.org>\n"
+"Subject: Hello world\n"
+"From: Test User <test@example.org>\n"
+"To: Another User <test2@example.org>\n"
+"Message-Id: <1.2.3.4@example>\n"
+"Mime-Version: 1.0\n"
+"Date: Sun, 23 May 2007 04:58:08 +0300\n"
+"Content-Type: multipart/signed; micalg=pgp-sha1;\n"
+" protocol=\"application/pgp-signature\";\n"
+" boundary=\"=-GNQXLhuj24Pl1aCkk4/d\"\n"
+"\n"
+"--=-GNQXLhuj24Pl1aCkk4/d\n"
+"Content-Type: text/plain\n"
+"Content-Transfer-Encoding: quoted-printable\n"
+"\n"
+"There was a day=20\n"
+"a happy=20day\n"
+"\n"
+"--=-GNQXLhuj24Pl1aCkk4/d\n"
+"Content-Type: application/pgp-signature; name=signature.asc\n"
+"\n"
+"-----BEGIN PGP SIGNATURE-----\n"
+"Version: GnuPG v1.2.4 (GNU/Linux)\n"
+"\n"
+"invalid\n"
+"-----END PGP SIGNATURE-----\n"
+"\n"
+"--=-GNQXLhuj24Pl1aCkk4/d--\n"
+"\n"
+"\n";
+
+static const char test_msg_with_nuls[] =
+"Return-Path: <test@example.org>\n"
+"Subject: Hello world\n"
+"From: Test User <test@example.org>\n"
+"To: Another User <test2@example.org>\n"
+"Message-Id: <1.2.3.4@example>\n"
+"Mime-Version: 1.0\0\n"
+"Date: Sun, 23 May 2007 04:58:08 +0300\n"
+"Content-Type: multipart/signed; micalg=pgp-sha1;\n"
+" protocol=\"application/pgp-signature\";\n"
+" boundary=\"=-GNQXLhuj24Pl1aCkk4/d\"\n"
+"\n"
+"--=-GNQXLhuj24Pl1aCkk4/d\n"
+"\n"
+"Content-Type: text/plain\n"
+"Content-Transfer-Encoding: quoted-printable\n"
+"\n"
+"There was\0 a day=20\n"
+"a happy=20day\n"
+"\n"
+"--=-GNQXLhuj24Pl1aCkk4/d\n"
+"Content-Type: application/pgp-signature; name=signature.asc\n"
+"\n"
+"-----BEGIN PGP SIGNATURE-----\n"
+"Version: GnuPG v1.2.4 (GNU/Linux)\n"
+"\n"
+"inva\0lid\n"
+"-----END PGP SIGNATURE-----\n"
+"\n"
+"--=-GNQXLhuj24Pl1aCkk4/d--\n"
+"\n"
+"\n";
+
+struct test_case {
+ const char *test_name;
+ const char *message;
+ bool has_nuls;
+ unsigned int body_newlines;
+ unsigned int header_newlines;
+ unsigned int message_len;
+ unsigned int header_len;
+};
+static const struct test_case test_cases[] = {
+ {
+ .test_name = "message size",
+ .message = test_msg,
+ .has_nuls = FALSE,
+ .body_newlines = 19,
+ .header_newlines = 11,
+ .message_len = sizeof(test_msg)-1,
+ .header_len = 335,
+ },
+ {
+ .test_name = "message size with nuls",
+ .message = test_msg_with_nuls,
+ .has_nuls = TRUE,
+ .body_newlines = 20,
+ .header_newlines = 11,
+ .message_len = sizeof(test_msg_with_nuls)-1,
+ .header_len = 336,
+ },
+};
+
+static void test_message_size(void)
+{
+ struct istream *input;
+ struct message_size body_size, header_size;
+ bool has_nuls;
+ bool last_cr;
+ unsigned int i;
+
+ for (i = 0; i < N_ELEMENTS(test_cases); i++) {
+ test_begin(test_cases[i].test_name);
+ input = i_stream_create_from_data(test_cases[i].message,
+ test_cases[i].message_len);
+
+ /* Read physical_size */
+ message_get_header_size(input, &header_size, &has_nuls);
+ test_assert_idx(has_nuls == test_cases[i].has_nuls, i);
+ test_assert_idx(input->v_offset == test_cases[i].header_len, i);
+ message_get_body_size(input, &body_size, &has_nuls);
+ test_assert_idx(has_nuls == test_cases[i].has_nuls, i);
+ test_assert_idx(input->v_offset - body_size.physical_size ==
+ test_cases[i].header_len, i);
+ test_assert_idx(body_size.physical_size + header_size.physical_size ==
+ test_cases[i].message_len, i);
+
+ /* Test last_cr handling */
+ i_stream_seek(input, 0);
+ message_skip_virtual(input, 0, &last_cr);
+ test_assert_idx(!last_cr, i);
+ message_skip_virtual(input, header_size.virtual_size-1, &last_cr);
+ test_assert_idx(last_cr, i);
+ message_skip_virtual(input, 2, &last_cr);
+ test_assert_idx(!last_cr, i);
+
+ /* Skipped header size so read body again */
+ message_get_body_size(input, &body_size, &has_nuls);
+ test_assert_idx(has_nuls == test_cases[i].has_nuls, i);
+ test_assert_idx(input->v_offset - body_size.physical_size ==
+ test_cases[i].header_len, i);
+ test_assert_idx(body_size.physical_size + test_cases[i].body_newlines ==
+ body_size.virtual_size, i);
+ test_assert_idx(body_size.virtual_size + header_size.virtual_size -
+ test_cases[i].body_newlines - test_cases[i].header_newlines ==
+ test_cases[i].message_len, i);
+
+ i_stream_unref(&input);
+ test_end();
+ }
+}
+
+int main(void)
+{
+ static void (*const test_functions[])(void) = {
+ test_message_size,
+ NULL
+ };
+ return test_run(test_functions);
+}
diff --git a/src/lib-mail/test-message-snippet.c b/src/lib-mail/test-message-snippet.c
new file mode 100644
index 0000000..bf1a0b3
--- /dev/null
+++ b/src/lib-mail/test-message-snippet.c
@@ -0,0 +1,329 @@
+/* Copyright (c) 2015-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "str.h"
+#include "istream.h"
+#include "unichar.h"
+#include "message-snippet.h"
+#include "test-common.h"
+
+static const struct {
+ const char *input;
+ unsigned int max_snippet_chars;
+ const char *output;
+} tests[] = {
+ { "Content-Type: text/plain\n"
+ "\n"
+ "1234567890 234567890",
+ 12,
+ "1234567890 2" },
+ { "Content-Type: text/plain\n"
+ "\n"
+ "line1\n>quote2\nline2\n",
+ 100,
+ "line1 line2" },
+ { "Content-Type: text/plain\n"
+ "\n"
+ "line1\n>quote2\n> quote3\n > line4\n\n \t\t \nline5\n \t ",
+ 100,
+ "line1 > line4 line5" },
+ { "Content-Type: text/plain; charset=utf-8\n"
+ "\n"
+ "hyv\xC3\xA4\xC3\xA4 p\xC3\xA4iv\xC3\xA4\xC3\xA4",
+ 11,
+ "hyv\xC3\xA4\xC3\xA4 p\xC3\xA4iv\xC3\xA4" },
+ { "Content-Type: text/plain; charset=utf-8\n"
+ "Content-Transfer-Encoding: quoted-printable\n"
+ "\n"
+ "hyv=C3=A4=C3=A4 p=C3=A4iv=C3=A4=C3=A4",
+ 11,
+ "hyv\xC3\xA4\xC3\xA4 p\xC3\xA4iv\xC3\xA4" },
+
+ { "Content-Transfer-Encoding: quoted-printable\n"
+ "Content-Type: text/html;\n"
+ " charset=utf-8\n"
+ "\n"
+ "<html><head><meta http-equiv=3D\"Content-Type\" content=3D\"text/html =\n"
+ "charset=3Dutf-8\"></head><body style=3D\"word-wrap: break-word; =\n"
+ "-webkit-nbsp-mode: space; -webkit-line-break: after-white-space;\" =\n"
+ "class=3D\"\">Hi,<div class=3D\"\"><br class=3D\"\"></div><div class=3D\"\">How =\n"
+ "is it going? <blockquote>quoted text is ignored</blockquote>\n"
+ "&gt; -foo\n"
+ "</div><br =class=3D\"\"></body></html>=\n",
+ 100,
+ "Hi, How is it going?" },
+
+ { "Content-Transfer-Encoding: quoted-printable\n"
+ "Content-Type: application/xhtml+xml;\n"
+ " charset=utf-8\n"
+ "\n"
+ "<html><head><meta http-equiv=3D\"Content-Type\" content=3D\"text/html =\n"
+ "charset=3Dutf-8\"></head><body style=3D\"word-wrap: break-word; =\n"
+ "-webkit-nbsp-mode: space; -webkit-line-break: after-white-space;\" =\n"
+ "class=3D\"\">Hi,<div class=3D\"\"><br class=3D\"\"></div><div class=3D\"\">How =\n"
+ "is it going? <blockquote>quoted text is ignored</blockquote>\n"
+ "&gt; -foo\n"
+ "</div><br =class=3D\"\"></body></html>=\n",
+ 100,
+ "Hi, How is it going?" },
+ { "Content-Type: text/plain\n"
+ "\n"
+ ">quote1\n>quote2\n",
+ 100,
+ ">quote1 quote2" },
+ { "Content-Type: text/plain\n"
+ "\n"
+ ">quote1\n>quote2\nbottom\nposter\n",
+ 100,
+ "bottom poster" },
+ { "Content-Type: text/plain\n"
+ "\n"
+ "top\nposter\n>quote1\n>quote2\n",
+ 100,
+ "top poster" },
+ { "Content-Type: text/plain\n"
+ "\n"
+ ">quoted long text",
+ 7,
+ ">quoted" },
+ { "Content-Type: text/plain\n"
+ "\n"
+ ">quoted long text",
+ 8,
+ ">quoted" },
+ { "Content-Type: text/plain\n"
+ "\n"
+ "whitespace and more",
+ 10,
+ "whitespace" },
+ { "Content-Type: text/plain\n"
+ "\n"
+ "whitespace and more",
+ 11,
+ "whitespace" },
+ { "Content-Type: text/plain; charset=utf-8\n"
+ "\n"
+ "Invalid utf8 \x80\xff\n",
+ 100,
+ "Invalid utf8 "UNICODE_REPLACEMENT_CHAR_UTF8 },
+ { "Content-Type: text/plain; charset=utf-8\n"
+ "\n"
+ "Incomplete utf8 \xC3",
+ 100,
+ "Incomplete utf8" },
+ { "Content-Transfer-Encoding: quoted-printable\n"
+ "Content-Type: text/html;\n"
+ " charset=utf-8\n"
+ "\n"
+ "<html><head><meta http-equiv=3D\"Content-Type\" content=3D\"text/html =\n"
+ "charset=3Dutf-8\"></head><body style=3D\"word-wrap: break-word; =\n"
+ "-webkit-nbsp-mode: space; -webkit-line-break: after-white-space;\" =\n"
+ "class=3D\"\"><div><blockquote>quoted text is included</blockquote>\n"
+ "</div><br =class=3D\"\"></body></html>=\n",
+ 100,
+ ">quoted text is included" },
+ { "Content-Type: text/plain; charset=utf-8\n"
+ "\n"
+ "I think\n",
+ 100,
+ "I think"
+ },
+ { "Content-Type: text/plain; charset=utf-8\n"
+ "\n"
+ " Lorem Ipsum\n",
+ 100,
+ "Lorem Ipsum"
+ },
+ { "Content-Type: text/plain; charset=utf-8\n"
+ "\n"
+ " I think\n",
+ 100,
+ "I think"
+ },
+ { "Content-Type: text/plain; charset=utf-8\n"
+ "\n"
+ " A cat\n",
+ 100,
+ "A cat"
+ },
+ { "Content-Type: text/plain; charset=utf-8\n"
+ "\n"
+ " \n",
+ 100,
+ ""
+ },
+ { "MIME-Version: 1.0\n"
+ "Content-Type: multipart/mixed; boundary=a\n"
+ "\n--a\n"
+ "Content-Transfer-Encoding: 7bit\n"
+ "Content-Type: text/html; charset=utf-8\n\n"
+ "<html><head></head><body><p>part one</p></body></head>\n"
+ "\n--a\n"
+ "Content-Transfer-Encoding: 7bit\n"
+ "Content-Type: text/html; charset=utf-8\n\n"
+ "<html><head></head><body><p>part two</p></body></head>\n"
+ "\n--a--\n",
+ 100,
+ "part one"
+ },
+ { "MIME-Version: 1.0\n"
+ "Content-Type: multipart/alternative; boundary=a\n"
+ "\n--a\n"
+ "Content-Transfer-Encoding: 7bit\n"
+ "Content-Type: text/html; charset=utf-8\n\n"
+ "<html><head></head><body><p>part one</p></body></head>\n"
+ "\n--a\n"
+ "Content-Transfer-Encoding: 7bit\n"
+ "Content-Type: text/plain; charset=utf-8\n\n"
+ "part two\n"
+ "\n--a--\n",
+ 100,
+ "part one"
+ },
+ { "MIME-Version: 1.0\n"
+ "Content-Type: multipart/mixed; boundary=a\n"
+ "\n--a\n"
+ "Content-Transfer-Encoding: 7bit\n"
+ "Content-Type: text/html; charset=utf-8\n\n"
+ "<html><head></head><body><div><p></p><!-- comment --></body></head>\n"
+ "\n--a\n"
+ "Content-Transfer-Encoding: 7bit\n"
+ "Content-Type: text/html; charset=utf-8\n\n"
+ "<html><head></head><body><p>part two</p></body></head>\n"
+ "\n--a--\n",
+ 100,
+ "part two"
+ },
+ { "MIME-Version: 1.0\n"
+ "Content-Type: multipart/alternative; boundary=a\n"
+ "\n--a\n"
+ "Content-Transfer-Encoding: 7bit\n"
+ "Content-Type: text/plain; charset=utf-8\n\n"
+ "> original text\n"
+ "\n--a\n"
+ "Content-Transfer-Encoding: 7bit\n"
+ "Content-Type: text/plain; charset=utf-8\n\n"
+ "part two\n"
+ "\n--a--\n",
+ 100,
+ ">original text"
+ },
+ { "MIME-Version: 1.0\n"
+ "Content-Type: multipart/alternative; boundary=a\n"
+ "\n--a\n"
+ "Content-Transfer-Encoding: 7bit\n"
+ "Content-Type: text/plain; charset=utf-8\n\n"
+ "top poster\n"
+ "\n--a\n"
+ "Content-Transfer-Encoding: 7bit\n"
+ "Content-Type: text/plain; charset=utf-8\n\n"
+ "> original text\n"
+ "\n--a--\n",
+ 100,
+ "top poster"
+ },
+ { "MIME-Version: 1.0\n"
+ "Content-Type: multipart/mixed; boundary=a\n"
+ "\n--a\n"
+ "Content-Transfer-Encoding: 7bit\n"
+ "Content-Type: text/html; charset=utf-8\n\n"
+ "<html><head></head><body><div><p></p><!-- comment --></body></head>\n"
+ "\n--a\n"
+ "Content-Transfer-Encoding: 7bit\n"
+ "Content-Type: text/html; charset=utf-8\n\n"
+ "<html><head></head><body><blockquote><!-- another --></blockquote>\n"
+ "</body></head>\n"
+ "\n--a--\n",
+ 100,
+ ""
+ },
+ { "MIME-Version: 1.0\n"
+ "Content-Type: multipart/mixed; boundary=a\n"
+ "\n--a\n"
+ "Content-Transfer-Encoding: 7bit\n"
+ "Content-Type: text/html; charset=utf-8\n\n"
+ "\n--a\n"
+ "Content-Transfer-Encoding: 7bit\n"
+ "Content-Type: text/html; charset=utf-8\n\n"
+ "</body></head>\n"
+ "\n--a--\n",
+ 100,
+ ""
+ },
+ { "MIME-Version: 1.0\n"
+ "Content-Type: multipart/mixed; boundary=a\n"
+ "\n--a\n"
+ "Content-Transfer-Encoding: base64\n"
+ "Content-Type: application/octet-stream\n\n"
+ "U2hvdWxkIG5vdCBiZSBpbiBzbmlwcGV0\n"
+ "\n--a\n"
+ "Content-Transfer-Encoding: 7bit\n"
+ "Content-Type: text/html; charset=utf-8\n\n"
+ "<html><head></head><body><p>Should be in snippet</p></body></html>\n"
+ "\n--a--\n",
+ 100,
+ "Should be in snippet"
+ },
+ { "MIME-Version: 1.0\n"
+ "Content-Type: multipart/mixed; boundary=a\n"
+ "\n--a\n"
+ "Content-Transfer-Encoding: base64\n"
+ "Content-Type: application/octet-stream\n\n"
+ "U2hvdWxkIG5vdCBiZSBpbiBzbmlwcGV0\n"
+ "\n--a\n"
+ "Content-Transfer-Encoding: base64\n"
+ "Content-Type: TeXT/html; charset=utf-8\n\n"
+ "PGh0bWw+PGhlYWQ+PC9oZWFkPjxib2R5PjxwPlNob3VsZCBiZSBpbiBzbmlwcGV0PC9wPjwvYm9k\n"
+ "eT48L2h0bWw+\n"
+ "\n--a--\n",
+ 100,
+ "Should be in snippet"
+ },
+};
+
+static void test_message_snippet(void)
+{
+ string_t *str = t_str_new(128);
+ struct istream *input;
+ unsigned int i;
+
+ test_begin("message snippet");
+ for (i = 0; i < N_ELEMENTS(tests); i++) {
+ str_truncate(str, 0);
+ input = test_istream_create(tests[i].input);
+ /* Limit the input max buffer size so the parsing uses multiple
+ blocks. 45 = large enough to be able to read the Content-*
+ headers. */
+ test_istream_set_max_buffer_size(input,
+ I_MIN(45, strlen(tests[i].input)));
+ test_assert_idx(message_snippet_generate(input, tests[i].max_snippet_chars, str) == 0, i);
+ test_assert_strcmp_idx(tests[i].output, str_c(str), i);
+ i_stream_destroy(&input);
+ }
+ test_end();
+}
+
+static void test_message_snippet_nuls(void)
+{
+ const char input_text[] = "\nfoo\0bar";
+ string_t *str = t_str_new(128);
+ struct istream *input;
+
+ test_begin("message snippet with NULs");
+
+ input = i_stream_create_from_data(input_text, sizeof(input_text)-1);
+ test_assert(message_snippet_generate(input, 5, str) == 0);
+ test_assert_strcmp(str_c(str), "fooba");
+ i_stream_destroy(&input);
+ test_end();
+}
+
+int main(void)
+{
+ static void (*const test_functions[])(void) = {
+ test_message_snippet,
+ test_message_snippet_nuls,
+ NULL
+ };
+ return test_run(test_functions);
+}
diff --git a/src/lib-mail/test-ostream-dot.c b/src/lib-mail/test-ostream-dot.c
new file mode 100644
index 0000000..9ddb563
--- /dev/null
+++ b/src/lib-mail/test-ostream-dot.c
@@ -0,0 +1,103 @@
+/* Copyright (c) 2013-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "buffer.h"
+#include "str.h"
+#include "istream.h"
+#include "ostream.h"
+#include "ostream-dot.h"
+#include "test-common.h"
+
+struct dot_test {
+ const char *input;
+ const char *output;
+};
+
+static void test_ostream_dot_one(const struct dot_test *test)
+{
+ struct istream *test_input;
+ struct ostream *output, *test_output;
+ buffer_t *output_data;
+ const unsigned char *data;
+ size_t size;
+ ssize_t ret;
+
+ test_input = test_istream_create(test->input);
+ output_data = t_buffer_create(1024);
+ test_output = o_stream_create_buffer(output_data);
+
+ output = o_stream_create_dot(test_output, FALSE);
+
+ while ((ret = i_stream_read(test_input)) > 0 || ret == -2) {
+ data = i_stream_get_data(test_input, &size);
+ ret = o_stream_send(output, data, size);
+ test_assert(ret >= 0);
+ if (ret <= 0)
+ break;
+ i_stream_skip(test_input, ret);
+ }
+
+ test_assert(test_input->eof);
+
+ test_assert(o_stream_finish(output) > 0);
+ test_assert(output->offset == strlen(test->input));
+ test_assert(test_output->offset == strlen(test->output));
+ o_stream_unref(&output);
+ o_stream_unref(&test_output);
+
+ test_assert(strcmp(str_c(output_data), test->output) == 0);
+
+ i_stream_unref(&test_input);
+}
+
+static void test_ostream_dot(void)
+{
+ static struct dot_test tests[] = {
+ { "foo\r\n.\r\n", "foo\r\n..\r\n.\r\n" },
+ { "foo\n.\n", "foo\r\n..\r\n.\r\n" },
+ { ".foo\r\n.\r\nfoo\r\n", "..foo\r\n..\r\nfoo\r\n.\r\n" },
+ { ".foo\n.\nfoo\n", "..foo\r\n..\r\nfoo\r\n.\r\n" },
+ { "\r\n", "\r\n.\r\n" },
+ { "\n", "\r\n.\r\n" },
+ { "", "\r\n.\r\n" },
+ };
+ unsigned int i;
+
+ for (i = 0; i < N_ELEMENTS(tests); i++) {
+ test_begin(t_strdup_printf("dot ostream[%d]:", i));
+ test_ostream_dot_one(&tests[i]);
+ test_end();
+ }
+}
+
+static void test_ostream_dot_parent_almost_full(void)
+{
+ buffer_t *output_data;
+ struct ostream *test_output, *output;
+ ssize_t ret;
+
+ test_begin("dot ostream parent almost full");
+ output_data = t_buffer_create(1024);
+ test_output = test_ostream_create_nonblocking(output_data, 1);
+ test_ostream_set_max_output_size(test_output, 1);
+
+ output = o_stream_create_dot(test_output, FALSE);
+ ret = o_stream_send(output, "a", 1);
+ test_assert(ret == 0);
+ ret = o_stream_send(output, "bc", 2);
+ test_assert(ret == 0);
+ o_stream_unref(&output);
+
+ o_stream_unref(&test_output);
+ test_end();
+}
+
+int main(void)
+{
+ static void (*const test_functions[])(void) = {
+ test_ostream_dot,
+ test_ostream_dot_parent_almost_full,
+ NULL
+ };
+ return test_run(test_functions);
+}
diff --git a/src/lib-mail/test-qp-decoder.c b/src/lib-mail/test-qp-decoder.c
new file mode 100644
index 0000000..9b4d827
--- /dev/null
+++ b/src/lib-mail/test-qp-decoder.c
@@ -0,0 +1,188 @@
+/* Copyright (c) 2007-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "str.h"
+#include "qp-decoder.h"
+#include "test-common.h"
+
+struct test_quoted_printable_decode_data {
+ const char *input;
+ const char *output;
+ size_t error_pos;
+ int ret;
+};
+
+static void test_qp_decoder(void)
+{
+#define WHITESPACE10 " \t \t \t"
+#define WHITESPACE70 WHITESPACE10 WHITESPACE10 WHITESPACE10 WHITESPACE10 WHITESPACE10 WHITESPACE10 WHITESPACE10
+ static struct test_quoted_printable_decode_data tests[] = {
+ { "foo \r\nbar=\n", "foo\r\nbar", 0, 0 },
+ { "foo\t=\nbar", "foo\tbar", 0, 0 },
+ { "foo = \n=01", "foo \001", 0, 0 },
+ { "foo =\t\r\nbar", "foo bar", 0, 0 },
+ { "foo =\r\n=01", "foo \001", 0, 0 },
+ { "foo \nbar=\r\n", "foo\r\nbar", 0, 0 },
+ { "=0A=0D ", "\n\r", 0, 0 },
+ { "foo_bar", "foo_bar", 0, 0 },
+ { "\n\n", "\r\n\r\n", 0, 0 },
+ { "\r\n\n\n\r\n", "\r\n\r\n\r\n\r\n", 0, 0 },
+
+ { "foo=", "foo=", 4, -1 },
+ { "foo= =66", "foo= f", 5, -1 },
+ { "foo= \t", "foo= \t", 6, -1 },
+ { "foo= \r", "foo= \r", 6, -1 },
+ { "foo= \r bar", "foo= \r bar", 6, -1 },
+ { "foo=A", "foo=A", 5, -1 },
+ { "foo=Ax", "foo=Ax", 5, -1 },
+ { "foo=Ax=xy", "foo=Ax=xy", 5, -1 },
+
+ /* above 76 whitespaces is invalid and gets truncated
+ (at 77th whitespace because of the current implementation) */
+ { WHITESPACE70" 7\n", WHITESPACE70" 7\r\n", 0, 0 },
+ { WHITESPACE70" 8\n", WHITESPACE70" 8\r\n", 77, -1 },
+ { WHITESPACE70" 9\n", WHITESPACE70" 9\r\n", 78, -1 },
+ { WHITESPACE70" 0\n", WHITESPACE70" 0\r\n", 79, -1 },
+ /* Expect extra whitespace to be truncated */
+ { WHITESPACE70" 7\n"WHITESPACE10"", WHITESPACE70" 7\r\n", 0, 0 },
+ { WHITESPACE70" 7=\r\n"WHITESPACE10, WHITESPACE70" 7", 0, 0 },
+ /* Unnecessarily encoded */
+ { "=66=6f=6f=42=61=72", "fooBar", 0, 0 },
+ /* Expected to be encoded but not */
+ { "\xc3\x9c""berm=c3=a4\xc3\x9figer Gebrauch", "\xc3\x9c""berm\xc3\xa4\xc3\x9figer Gebrauch", 0, 0 },
+ /* Decode control characters */
+ { "=0C=07", "\x0C\x07", 0, 0 },
+ /* Data */
+ { "=DE=AD=BE=EF", "\xDE\xAD\xBE\xEF", 0, 0 },
+ /* Non hex data */
+ { "=FJ=X1", "=FJ=X1", 2, -1 },
+ /* No content allowed after Soft Line Break */
+ { "=C3=9C = ","\xc3\x9c"" = ", 9, -1 },
+ /* Boundary delimiter */
+ { "=C3=9C=\r\n-------","\xc3\x9c""-------", 0, 0 },
+ { "=----------- =C3=9C","=----------- \xc3\x9c""", 1, -1 },
+ { "=___________ =C3=9C","=___________ \xc3\x9c""", 1, -1 },
+ { "___________ =C3=9C","___________ \xc3\x9c""", 0, 0 },
+ { "=2D=2D=2D=2D=2D=2D =C3=9C","------ \xc3\x9c""", 0, 0 },
+ { "=FC=83=BF=BF=BF=BF", "\xFC\x83\xBF\xBF\xBF\xBF", 0, 0 },
+ { "=FE=FE=FF=FF", "\xFE\xFE\xFF\xFF", 0, 0 },
+ { "\xFF=C3=9C\xFE\xFF""foobar", "\xFF\xc3\x9c""\xFE\xFF""foobar", 0, 0 },
+ /* Unnecessarily encoded and trailing whitespace */
+ {
+ "=66=6f=6f=42=61=72 ",
+ "fooBar", 0, 0
+ },
+ /* Indicate error if encoded line is longer then 76 */
+ {
+ WHITESPACE70" =C3=9C\n",
+ WHITESPACE70" \xc3\x9c""\r\n", 77, -1
+ },
+ /* Soft Line Break example from the RFC */
+ {
+ "Now's the time =\r\nfor all folk to come=\r\n to the"
+ " aid of their country.",
+ "Now's the time for all folk to come to the aid of "
+ "their country.", 0, 0
+ },
+ {
+ "=C3=9Cberm=C3=A4=C3=9Figer Gebrauch",
+ "\xc3\x9c""berm\xc3\xa4\xc3\x9figer Gebrauch", 0, 0
+ },
+ /* Softlinebreak without following content */
+ {
+ "=C3=9Cberm=C3=A4=C3=9Figer Gebrauch=",
+ "\xc3\x9c""berm\xc3\xa4\xc3\x9figer Gebrauch=", 36, -1
+ },
+ /* Lowercase formally illegal but allowed for robustness */
+ {
+ "=c3=9cberm=c3=a4=c3=9figer Gebrauch",
+ "\xc3\x9c""berm\xc3\xa4\xc3\x9figer Gebrauch", 0, 0
+ },
+ /* Control characters in input */
+ {
+ "=c3=9c=10berm=c3=a4=c3=9figer Geb=0Frauch",
+ "\xc3\x9c\x10""berm\xc3\xa4\xc3\x9figer Geb\x0Frauch", 0, 0
+ },
+ /* Trailing whitespace */
+ {
+ "Trailing Whitesp=C3=A4ce =\r\n ",
+ "Trailing Whitesp\xc3\xa4""ce ", 0 ,0
+ },
+ {
+ "Trailing Whitesp=C3=A4ce ",
+ "Trailing Whitesp\xc3\xa4""ce", 0 ,0
+ },
+ {
+ "=54=65=73=74=20=6D=65=73=73=61=67=65",
+ "Test message", 0 , 0
+ },
+ {
+ "=E3=81=93=E3=82=8C=E3=81=AF=E5=A2\r\n=83=E7=95=8C=E3"
+ "=81=AE=E3=81=82=E3=82=8B=E3=83=A1=E3=83=83=E3=82=BB="
+ "E3=83=BC=E3=82=B8=E3=81=A7=E3=81=99",
+ "\xE3\x81\x93\xE3\x82\x8C\xE3\x81\xAF\xE5\xA2\r\n\x83"
+ "\xE7\x95\x8C\xE3\x81\xAE\xE3\x81\x82\xE3\x82\x8B\xE3"
+ "\x83\xA1\xE3\x83\x83\xE3\x82\xBB\xE3\x83\xBC\xE3\x82"
+ "\xB8\xE3\x81\xA7\xE3\x81\x99", 0, 0
+ },
+ {
+ "=E3=81\xc3\xf1=93=E3=82=8\xff""C=E3=81=AF=E5=A2",
+ "\xE3\x81\xc3\xf1\x93\xE3\x82=8\xff""C\xE3\x81\xAF\xE5\xA2",
+ 19, -1
+ },
+ {
+ "\x77Hello\x76=20 \x20 =E3=81\xc3\xf1=93=E3=82",
+ "wHellov \xE3\x81\xc3\xf1\x93\xE3\x82",
+ 0, 0
+ },
+ };
+ string_t *str;
+ unsigned int i, j;
+
+ test_begin("qp-decoder");
+ str = t_str_new(128);
+ for (i = 0; i < N_ELEMENTS(tests); i++) {
+ const char *input = tests[i].input;
+ struct qp_decoder *qp = qp_decoder_init(str);
+ size_t error_pos;
+ const char *error;
+ int ret;
+
+ /* try all at once */
+ ret = qp_decoder_more(qp, (const void *)input, strlen(input),
+ &error_pos, &error);
+ if (qp_decoder_finish(qp, &error) < 0 && ret == 0) {
+ error_pos = strlen(input);
+ ret = -1;
+ }
+ test_assert_idx(ret == tests[i].ret, i);
+ test_assert_idx(ret == 0 || error_pos == tests[i].error_pos, i);
+ test_assert_strcmp_idx(str_c(str), tests[i].output, i);
+
+ /* try in small pieces */
+ str_truncate(str, 0);
+ ret = 0;
+ for (j = 0; input[j] != '\0'; j++) {
+ unsigned char c = (unsigned char)input[j];
+ if (qp_decoder_more(qp, &c, 1, &error_pos, &error) < 0)
+ ret = -1;
+ }
+ if (qp_decoder_finish(qp, &error) < 0)
+ ret = -1;
+ test_assert_idx(ret == tests[i].ret, i);
+ test_assert_strcmp_idx(str_c(str), tests[i].output, i);
+
+ qp_decoder_deinit(&qp);
+ str_truncate(str, 0);
+ }
+ test_end();
+}
+
+int main(void)
+{
+ static void (*const test_functions[])(void) = {
+ test_qp_decoder,
+ NULL
+ };
+ return test_run(test_functions);
+}
diff --git a/src/lib-mail/test-qp-encoder.c b/src/lib-mail/test-qp-encoder.c
new file mode 100644
index 0000000..bb31b1e
--- /dev/null
+++ b/src/lib-mail/test-qp-encoder.c
@@ -0,0 +1,206 @@
+/* Copyright (c) 2017-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "str.h"
+#include "qp-encoder.h"
+#include "test-common.h"
+
+struct test_quoted_printable_encode_data {
+ const void *input;
+ size_t input_len;
+ const char *output;
+ size_t max_line_len;
+};
+
+static void test_qp_encoder(void)
+{
+#define WHITESPACE10 " \t \t \t"
+#define WHITESPACE70 WHITESPACE10 WHITESPACE10 WHITESPACE10 WHITESPACE10 WHITESPACE10 WHITESPACE10 WHITESPACE10
+ static struct test_quoted_printable_encode_data tests[] = {
+ { "", 0, "", 20 },
+ { "a", 1, "a", 20 },
+ { "a b \r c d", 9, "a b =0D c d", 20 },
+ { "a b c d\r", 8, "a b c d=0D", 20 },
+ { "a b \n c d", 9, "a b \r\n c d", 20 },
+ {
+ "test wrap at max 20 characters tab\ttoo", 38,
+ "test wrap at max=20=\r\n20 characters tab=09=\r\ntoo",
+ 20
+ },
+ { "Invalid UTF-8 sequence in \x99", 27, "Invalid UTF-8 sequ=\r\nence in =99", 20 },
+ { "keep CRLF\r\non two lines", 23, "keep CRLF\r\non two lines", 20 },
+ /* Trailing whitespace should be followed by encoded char. */
+ { "Keep trailing whitesp\xC3\xA4""ce ", 26, "Keep trailing whit=\r\nesp=C3=A4ce =", 20 },
+ { "Keep trailing whitesp\xC3\xA4""ce\t", 26, "Keep trailing whitesp=C3=A4ce\t=", 67 },
+ { "Keep trailing whitesp\xC3\xA4""ce ", 26, "Keep trailing whitesp=C3=A4ce =", 67 },
+ { "Keep trailing whitesp\xC3\xA4""ce ", 28, "Keep trailing whitesp=C3=A4ce =", 67 },
+ { "Keep trailing whitesp\xC3\xA4""ce \t ", 28, "Keep trailing whitesp=C3=A4ce \t =", 67 },
+ { "Keep trailing whitesp\xC3\xA4""ce ", 29, "Keep trailing whitesp=C3=A4ce =", 67 },
+ { "Keep trailing whitesp\xC3\xA4""ce ", 30, "Keep trailing whitesp=C3=A4ce =", 67 },
+ { "Keep trailing whitesp\xC3\xA4""ce ", 31, "Keep trailing whitesp=C3=A4ce =", 67 },
+ /* Test line breaking */
+ { WHITESPACE70"1234567", 77, WHITESPACE70"1234=\r\n567", 76 },
+ { WHITESPACE70" 7", 77, WHITESPACE70" =20=\r\n 7", 76 },
+ { WHITESPACE70""WHITESPACE10"1", 81, WHITESPACE70" =20=\r\n\t \t \t1", 76 },
+ /* Control characters */
+ { "\x0C\x07", 2, "=0C=07", 20},
+ /* Data */
+ { "\xDE\xAD\xBE\xEF""deadbeef", 12 ,"=DE=AD=BE=EFdeadbe=\r\nef", 20 },
+ { "\xDE""de""\xAD""ad""\xBE""be""\xEF""ef", 12 ,"=DEde=ADad=BEbe=EF=\r\nef", 20 },
+ /* boundary delimiter */
+ { "___________ \xc3\x9c", 14, "___________ =C3=9C", 20 },
+ { "----------- \xc3\x9c", 14, "----------- =C3=9C", 20 },
+ { "=---------- \xc3\x9c", 14, "=3D---------- =C3=\r\n=9C", 20 },
+ { "=__________ \xc3\x9c", 14, "=3D__________ =C3=\r\n=9C", 20 },
+ /* mixed inputs */
+ { "\xed\xae\x80\xed\xbf\xbf", 6, "=ED=AE=80=ED=BF=BF", 20 },
+ { "f\x6f\x6f""bar\xae\x80\xed\xbf\xbf", 11, "foobar=AE=80=ED=BF=\r\n=BF", 20 },
+ {
+ "\xc3\x9c""ber\x6d\xc3\xa4\xc3\x9f\x69\x67\x0a\xe0\x80\x80 \xf0\x9d\x84\x9e", 21,
+ "=C3=9Cberm=C3=A4=C3=9Fig\r\n=E0=80=80 =F0=9D=84=9E",
+ 76
+ },
+ {
+ "\xc3\x9c""ber\x6d\xc3\xa4\xc3\x9f\x69\x67\x0a\xe0\x80\x80 \xf0\x9d\x84\x9e", 21,
+ "=C3=9Cberm=C3=A4=\r\n=C3=9Fig\r\n=E0=80=80 =F0=9D=\r\n=84=9E",
+ 20
+ },
+ {
+ "\xc3\x9c""ber\x6dä\xc3\x9fi\x0a\xe0g\x80\x80 \xf0\x9d\x84\x9e", 21,
+ "=C3=9Cberm=C3=A4=C3=9Fi\r\n=E0g=80=80 =F0=9D=84=9E",
+ 76
+ },
+ {
+ "\xc3\x9c""ber\x6dä\xc3\xff\x9fi\x0a\xe0g\x80\x80\xfe\xf0\x9d\x84\x9e", 22,
+ "=C3=9Cberm=C3=A4=C3=FF=9Fi\r\n=E0g=80=80=FE=F0=9D=84=9E",
+ 76
+ },
+ };
+ string_t *str;
+ unsigned int i, j;
+
+ test_begin("qp-encoder");
+ str = t_str_new(128);
+ for (i = 0; i < N_ELEMENTS(tests); i++) {
+ const unsigned char *input = tests[i].input;
+ struct qp_encoder *qp = qp_encoder_init(str, tests[i].max_line_len, 0);
+
+ /* try all at once */
+ qp_encoder_more(qp, input, tests[i].input_len);
+ qp_encoder_finish(qp);
+
+ test_assert_strcmp_idx(str_c(str), tests[i].output, i);
+
+ /* try in small pieces */
+ str_truncate(str, 0);
+ for (j = 0; j < tests[i].input_len; j++) {
+ unsigned char c = input[j];
+ qp_encoder_more(qp, &c, 1);
+ }
+ qp_encoder_finish(qp);
+ test_assert_strcmp_idx(str_c(str), tests[i].output, i);
+
+ qp_encoder_deinit(&qp);
+ str_truncate(str, 0);
+ }
+ test_end();
+}
+
+static void test_qp_encoder_binary(void)
+{
+ static struct test_quoted_printable_encode_data tests[] = {
+ { "\0nil\0delimited\0string\0", 22, "=00nil=00delimited=\r\n=00string=00" ,20 },
+ {
+ "\xef\x4e\xc5\xe0\x31\x66\xd7\xef\xae\x12\x7d\x45\x1e\x05\xc7\x2a",
+ 16,
+ "=EFN=C5=E01f=D7=EF=\r\n=AE=12}E=1E=05=C7*",
+ 20
+ },
+ };
+
+ string_t *str;
+ unsigned int i, j;
+
+ test_begin("qp-encoder (binary safe)");
+ str = t_str_new(128);
+ for (i = 0; i < N_ELEMENTS(tests); i++) {
+ const unsigned char *input = tests[i].input;
+ struct qp_encoder *qp = qp_encoder_init(str, tests[i].max_line_len, QP_ENCODER_FLAG_BINARY_DATA);
+
+ /* try all at once */
+ qp_encoder_more(qp, input, tests[i].input_len);
+ qp_encoder_finish(qp);
+
+ test_assert_idx(strcmp(str_c(str), tests[i].output) == 0, i);
+
+ /* try in small pieces */
+ str_truncate(str, 0);
+ for (j = 0; j < tests[i].input_len; j++) {
+ unsigned char c = input[j];
+ qp_encoder_more(qp, &c, 1);
+ }
+ qp_encoder_finish(qp);
+ test_assert_idx(strcmp(str_c(str), tests[i].output) == 0, i);
+
+ qp_encoder_deinit(&qp);
+ str_truncate(str, 0);
+ }
+ test_end();
+}
+
+static void test_qp_encoder_header(void)
+{
+ static struct test_quoted_printable_encode_data tests[] = {
+ { "simple", 6, "=?utf-8?Q?simple?=", 75 },
+ { "J'esuis de paris caf\xc3\xa9", 22, "=?utf-8?Q?J'esuis_de_paris_caf=C3=A9?=", 75 },
+ { "hello_world", 11, "=?utf-8?Q?hello=5Fworld?=", 75 },
+ {
+ "make sure this wraps and that the actual lines are not longer than maximum length including preamble",
+ 100,
+ "=?utf-8?Q?make_sure_this_wraps_and_that_the_actual_lines_are_not_longer_t?=\r\n"
+ " =?utf-8?Q?han_maximum_length_including_preamble?=",
+ 75
+ },
+ };
+
+ string_t *str;
+ unsigned int i, j;
+
+ test_begin("qp-encoder (header format)");
+ str = t_str_new(128);
+ for (i = 0; i < N_ELEMENTS(tests); i++) {
+ const unsigned char *input = tests[i].input;
+ struct qp_encoder *qp = qp_encoder_init(str, tests[i].max_line_len, QP_ENCODER_FLAG_HEADER_FORMAT);
+
+ /* try all at once */
+ qp_encoder_more(qp, input, tests[i].input_len);
+ qp_encoder_finish(qp);
+
+ test_assert_idx(strcmp(str_c(str), tests[i].output) == 0, i);
+
+ /* try in small pieces */
+ str_truncate(str, 0);
+ for (j = 0; j < tests[i].input_len; j++) {
+ unsigned char c = input[j];
+ qp_encoder_more(qp, &c, 1);
+ }
+ qp_encoder_finish(qp);
+ test_assert_idx(strcmp(str_c(str), tests[i].output) == 0, i);
+
+ qp_encoder_deinit(&qp);
+ str_truncate(str, 0);
+ }
+ test_end();
+}
+
+
+int main(void)
+{
+ static void (*const test_functions[])(void) = {
+ test_qp_encoder,
+ test_qp_encoder_binary,
+ test_qp_encoder_header,
+ NULL
+ };
+ return test_run(test_functions);
+}
diff --git a/src/lib-mail/test-quoted-printable.c b/src/lib-mail/test-quoted-printable.c
new file mode 100644
index 0000000..3cce59e
--- /dev/null
+++ b/src/lib-mail/test-quoted-printable.c
@@ -0,0 +1,52 @@
+/* Copyright (c) 2007-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "buffer.h"
+#include "str.h"
+#include "quoted-printable.h"
+#include "test-common.h"
+
+static void test_quoted_printable_q_decode(void)
+{
+ const char *data[] = {
+ "=0A=0D ", "\n\r ",
+ "__foo__bar__", " foo bar ",
+ "foo=", "foo=",
+ "foo=A", "foo=A",
+ "foo=Ax", "foo=Ax",
+ "foo=Ax=xy", "foo=Ax=xy",
+ "=C3=9Cberm=C3=A4=C3=9Figer Gebrauch", "\xc3\x9c""berm\xc3\xa4\xc3\x9figer Gebrauch",
+ /* Lowercase formally illegal but allowed for robustness */
+ "=c3=9cberm=c3=a4=c3=9figer Gebrauch", "\xc3\x9c""berm\xc3\xa4\xc3\x9figer Gebrauch",
+ /* Unnecessarily encoded */
+ "=66=6f=6f=42=61=72", "fooBar",
+ /* Expected to be encoded but not */
+ "\xc3\x9c""berm=c3=a4\xc3\x9figer Gebrauch", "\xc3\x9c""berm\xc3\xa4\xc3\x9figer Gebrauch",
+ /* Decode control characters */
+ "=0C=07", "\x0C\x07",
+ "=DE=AD=BE=EF", "\xDE\xAD\xBE\xEF",
+ /* Non-Hex data */
+ "=FJ=X1", "=FJ=X1",
+ };
+ buffer_t *buf;
+ unsigned int i;
+
+ test_begin("quoted printable q decode");
+ buf = t_buffer_create(128);
+ for (i = 0; i < N_ELEMENTS(data); i += 2) {
+ quoted_printable_q_decode((const void *)data[i], strlen(data[i]),
+ buf);
+ test_assert_strcmp_idx(data[i+1], str_c(buf), i/2);
+ buffer_set_used_size(buf, 0);
+ }
+ test_end();
+}
+
+int main(void)
+{
+ static void (*const test_functions[])(void) = {
+ test_quoted_printable_q_decode,
+ NULL
+ };
+ return test_run(test_functions);
+}
diff --git a/src/lib-mail/test-rfc2231-parser.c b/src/lib-mail/test-rfc2231-parser.c
new file mode 100644
index 0000000..f54910a
--- /dev/null
+++ b/src/lib-mail/test-rfc2231-parser.c
@@ -0,0 +1,51 @@
+/* Copyright (c) 2007-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "rfc822-parser.h"
+#include "rfc2231-parser.h"
+#include "test-common.h"
+
+static void test_rfc2231_parser(void)
+{
+ const unsigned char input[] =
+ "; key4*=us-ascii''foo"
+ "; key*2=ba%"
+ "; key2*0=a"
+ "; key3*0*=us-ascii'en'xyz"
+ "; key*0=\"f\0oo\""
+ "; key2*1*=b%25"
+ "; key3*1=plop%"
+ "; key*1=baz";
+ const char *output[] = {
+ "key",
+ "f\xEF\xBF\xBDoobazba%",
+ "key2*",
+ "''ab%25",
+ "key3*",
+ "us-ascii'en'xyzplop%25",
+ "key4*",
+ "us-ascii''foo",
+ NULL
+ };
+ struct rfc822_parser_context parser;
+ const char *const *result;
+ unsigned int i;
+
+ test_begin("rfc2231 parser");
+ rfc822_parser_init(&parser, input, sizeof(input)-1, NULL);
+ test_assert(rfc2231_parse(&parser, &result) == 0);
+ for (i = 0; output[i] != NULL && result[i] != NULL; i++)
+ test_assert_idx(strcmp(output[i], result[i]) == 0, i);
+ rfc822_parser_deinit(&parser);
+ test_assert(output[i] == NULL && result[i] == NULL);
+ test_end();
+}
+
+int main(void)
+{
+ static void (*const test_functions[])(void) = {
+ test_rfc2231_parser,
+ NULL
+ };
+ return test_run(test_functions);
+}
diff --git a/src/lib-mail/test-rfc822-parser.c b/src/lib-mail/test-rfc822-parser.c
new file mode 100644
index 0000000..a0e7ad0
--- /dev/null
+++ b/src/lib-mail/test-rfc822-parser.c
@@ -0,0 +1,445 @@
+/* Copyright (c) 2015-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "str.h"
+#include "rfc822-parser.h"
+#include "test-common.h"
+
+static void test_rfc822_parse_comment(void)
+{
+ static const struct {
+ const char *input, *output;
+ int ret;
+ } tests[] = {
+ { "(", "", -1 },
+ { "(()", "", -1 },
+
+ { "()", "", 0 },
+ { "(())", "()", 0 },
+ { "(foo ( bar ) baz)", "foo ( bar ) baz", 0 },
+ { "(foo\t\tbar)", "foo\t\tbar", 0 },
+ { "(foo\\(bar)", "foo(bar", 0 },
+ { "(foo\\\\bar)", "foo\\bar", 0 },
+ { "(foo\\\\\\\\)", "foo\\\\", 0 },
+ { "(foo\\)bar)", "foo)bar", 0 },
+ { "(foo\"flop\"\"bar)", "foo\"flop\"\"bar", 0 },
+
+ { "(foo\n bar)", "foo bar", 0 },
+ { "(foo\n\t\t bar)", "foo\t\t bar", 0 },
+ { "(foo\\\n bar)", "foo\\ bar", 0 },
+ { "(foo\\\r\n bar)", "foo\\ bar", 0 },
+ };
+ struct rfc822_parser_context parser, parser2;
+ string_t *str = t_str_new(64);
+ unsigned int i = 0;
+
+ test_begin("rfc822 parse comment");
+ for (i = 0; i < N_ELEMENTS(tests); i++) {
+ rfc822_parser_init(&parser, (const void *)tests[i].input,
+ strlen(tests[i].input), str);
+ rfc822_parser_init(&parser2, (const void *)tests[i].input,
+ strlen(tests[i].input), NULL);
+ test_assert_idx(rfc822_skip_comment(&parser) == tests[i].ret, i);
+ test_assert_idx(rfc822_skip_comment(&parser2) == tests[i].ret, i);
+ test_assert_idx(tests[i].ret < 0 ||
+ strcmp(tests[i].output, str_c(str)) == 0, i);
+ rfc822_parser_deinit(&parser);
+ rfc822_parser_deinit(&parser2);
+ str_truncate(str, 0);
+ }
+ test_end();
+}
+
+static void test_rfc822_parse_comment_nuls(void)
+{
+ const unsigned char input[] = "(\000a\000\000b\\\000c(\000d)\000)";
+ const char output[] = "!a!!b\\!c(!d)!";
+ struct rfc822_parser_context parser;
+ string_t *str = t_str_new(64);
+
+ test_begin("rfc822 parse comment with NULs");
+
+ rfc822_parser_init(&parser, input, sizeof(input)-1, str);
+ test_assert(rfc822_skip_comment(&parser) == 0);
+ /* should be same as input, except the outer () removed */
+ test_assert(str_len(str) == sizeof(input)-1-2 &&
+ memcmp(input+1, str_data(str), str_len(str)) == 0);
+ rfc822_parser_deinit(&parser);
+
+ str_truncate(str, 0);
+ rfc822_parser_init(&parser, input, sizeof(input)-1, str);
+ parser.nul_replacement_str = "!";
+ test_assert(rfc822_skip_comment(&parser) == 0);
+ test_assert(strcmp(str_c(str), output) == 0);
+ rfc822_parser_deinit(&parser);
+
+ test_end();
+}
+
+static void test_rfc822_parse_quoted_string(void)
+{
+ static const struct {
+ const char *input, *output;
+ int ret;
+ } tests[] = {
+ { "\"", "", -1 },
+ { "\"\"", "", 0 },
+ { "\"foo\"", "foo", 0 },
+ { "\"\"foo", "", 1 },
+ { "\"\"\"", "", 1 },
+ { "\"\\\"\"", "\"", 0 },
+ { "\"\\\\\"", "\\", 0 },
+ { "\"\\\\foo\\\\foo\\\\\"", "\\foo\\foo\\", 0 },
+ { "\"foo\n bar\"", "foo bar", 0 },
+ { "\"foo\n\t\t bar\"", "foo\t\t bar", 0 },
+ { "\"foo\\\n bar\"", "foo\\ bar", 0 },
+ { "\"foo\\\r\n bar\"", "foo\\ bar", 0 },
+ };
+ struct rfc822_parser_context parser;
+ string_t *str = t_str_new(64);
+ unsigned int i = 0;
+
+ test_begin("rfc822 parse quoted string");
+ for (i = 0; i < N_ELEMENTS(tests); i++) {
+ rfc822_parser_init(&parser, (const void *)tests[i].input,
+ strlen(tests[i].input), NULL);
+ test_assert_idx(rfc822_parse_quoted_string(&parser, str) == tests[i].ret, i);
+ test_assert_idx(tests[i].ret < 0 ||
+ strcmp(tests[i].output, str_c(str)) == 0, i);
+ rfc822_parser_deinit(&parser);
+ str_truncate(str, 0);
+ }
+ test_end();
+}
+
+static void test_rfc822_parse_dot_atom(void)
+{
+ static const struct {
+ const char *input, *output;
+ int ret;
+ } tests[] = {
+ { "foo", "foo", 0 },
+ { "foo.bar", "foo.bar", 0 },
+ { "foo.bar.baz", "foo.bar.baz", 0 },
+ { "foo . \tbar (comments) . (...) baz\t ", "foo.bar.baz", 0 },
+
+ { ".", "", -1 },
+ { "..", "", -1 },
+ { ".foo", "", -1 },
+ { "foo.", "foo.", -1 },
+ { "foo..bar", "foo.", -1 },
+ { "foo. .bar", "foo.", -1 },
+ { "foo.(middle).bar", "foo.", -1 },
+ { "foo. ", "foo.", -1 },
+ { "foo.\t", "foo.", -1 },
+ { "foo.(ending)", "foo.", -1 },
+ };
+ struct rfc822_parser_context parser;
+ string_t *str = t_str_new(64);
+ string_t *input2 = t_str_new(64);
+ unsigned int i = 0;
+
+ test_begin("rfc822 parse dot-atom");
+ for (i = 0; i < N_ELEMENTS(tests); i++) {
+ rfc822_parser_init(&parser, (const void *)tests[i].input,
+ strlen(tests[i].input), NULL);
+ test_assert_idx(rfc822_parse_dot_atom(&parser, str) == tests[i].ret, i);
+ test_assert_idx(strcmp(tests[i].output, str_c(str)) == 0, i);
+ rfc822_parser_deinit(&parser);
+ str_truncate(str, 0);
+
+ /* same input but with "," appended should return 1 on success,
+ and -1 still on error. */
+ int expected_ret = tests[i].ret == -1 ? -1 : 1;
+ str_append(input2, tests[i].input);
+ str_append_c(input2, ',');
+ rfc822_parser_init(&parser, str_data(input2),
+ str_len(input2), NULL);
+ test_assert_idx(rfc822_parse_dot_atom(&parser, str) == expected_ret, i);
+ test_assert_idx(strcmp(tests[i].output, str_c(str)) == 0, i);
+ rfc822_parser_deinit(&parser);
+
+ str_truncate(str, 0);
+ str_truncate(input2, 0);
+ }
+ test_end();
+}
+
+static void test_rfc822_parse_domain_literal(void)
+{
+ static const struct {
+ const char *input, *output;
+ int ret;
+ } tests[] = {
+ { "@[", "", -1 },
+ { "@[foo", "", -1 },
+ { "@[foo[]", "", -1 },
+ { "@[foo[]]", "", -1 },
+ { "@[]", "[]", 0 },
+ { "@[foo bar]", "[foo bar]", 0 },
+ { "@[foo\n bar]", "[foo bar]", 0 },
+ { "@[foo\n\t\t bar]", "[foo\t\t bar]", 0 },
+ { "@[foo\\\n bar]", "[foo\\ bar]", 0 },
+ };
+ struct rfc822_parser_context parser;
+ string_t *str = t_str_new(64);
+ unsigned int i = 0;
+
+ test_begin("rfc822 parse domain literal");
+ for (i = 0; i < N_ELEMENTS(tests); i++) {
+ rfc822_parser_init(&parser, (const void *)tests[i].input,
+ strlen(tests[i].input), NULL);
+ test_assert_idx(rfc822_parse_domain(&parser, str) == tests[i].ret, i);
+ test_assert_idx(tests[i].ret < 0 ||
+ strcmp(tests[i].output, str_c(str)) == 0, i);
+ rfc822_parser_deinit(&parser);
+ str_truncate(str, 0);
+ }
+ test_end();
+}
+
+#undef TEST_STRING
+#define TEST_STRING(a) .input = (const unsigned char*)a, .input_len = sizeof(a)-1
+
+static void test_rfc822_parse_content_type(void)
+{
+ const struct {
+ const unsigned char *input;
+ size_t input_len;
+ int ret;
+ const char *output;
+ } test_cases[] = {
+ { TEST_STRING(""), -1, "" },
+ { TEST_STRING(";charset=us-ascii"), -1, "" },
+ { TEST_STRING(" ;charset=us-ascii"), -1, "" },
+ { TEST_STRING("/"), -1, "" },
+ { TEST_STRING("/;charset=us-ascii"), -1, "" },
+ { TEST_STRING("/ ;charset=us-ascii"), -1, "" },
+ { TEST_STRING("text/"), -1, "" },
+ { TEST_STRING("text/;charset=us-ascii"), -1, "" },
+ { TEST_STRING("text/ ;charset=us-ascii"), -1, "" },
+ { TEST_STRING("/plain"), -1, "" },
+ { TEST_STRING("/plain;charset=us-ascii"), -1, "" },
+ { TEST_STRING("/plain ;charset=us-ascii"), -1, "" },
+ { TEST_STRING("text/plain"), 0, "text/plain" },
+ { TEST_STRING("text/plain;charset=us-ascii"), 1, "text/plain" },
+ { TEST_STRING("text/plain ;charset=us-ascii"), 1, "text/plain" },
+ { TEST_STRING("text/plain/format"), -1, "" },
+ { TEST_STRING("text/plain/format;charset=us-ascii"), -1, "" },
+ { TEST_STRING("text/plain/format ;charset=us-ascii"), -1, "" },
+ { TEST_STRING("\xe5\x90\xab\xe9\x87\x8f/\xe7\xa8\xae\xe9\xa1\x9e"),
+ 0, "\xe5\x90\xab\xe9\x87\x8f/\xe7\xa8\xae\xe9\xa1\x9e" },
+ { TEST_STRING("\xe5\x90\xab\xe9\x87\x8f/\xe7\xa8\xae\xe9\xa1\x9e;charset=utf-8"),
+ 1, "\xe5\x90\xab\xe9\x87\x8f/\xe7\xa8\xae\xe9\xa1\x9e" },
+ { TEST_STRING("\xe5\x90\xab\xe9\x87\x8f/\xe7\xa8\xae\xe9\xa1\x9e ;charset=utf-8"),
+ 1, "\xe5\x90\xab\xe9\x87\x8f/\xe7\xa8\xae\xe9\xa1\x9e" },
+ { TEST_STRING("application/ld+json"), 0, "application/ld+json" },
+ { TEST_STRING("application/ld+json;charset=us-ascii"),
+ 1, "application/ld+json" },
+ { TEST_STRING("application/ld+json ;charset=us-ascii"),
+ 1, "application/ld+json" },
+ { TEST_STRING("application/x-magic-cap-package-1.0"),
+ 0, "application/x-magic-cap-package-1.0" },
+ { TEST_STRING("application/x-magic-cap-package-1.0;charset=us-ascii"),
+ 1, "application/x-magic-cap-package-1.0" },
+ { TEST_STRING("application/x-magic-cap-package-1.0 ;charset=us-ascii"),
+ 1, "application/x-magic-cap-package-1.0" },
+ { TEST_STRING("application/pro_eng"), 0, "application/pro_eng" },
+ { TEST_STRING("application/pro_eng;charset=us-ascii"),
+ 1, "application/pro_eng" },
+ { TEST_STRING("application/pro_eng ;charset=us-ascii"),
+ 1, "application/pro_eng" },
+ { TEST_STRING("application/wordperfect6.1"),
+ 0, "application/wordperfect6.1" },
+ { TEST_STRING("application/wordperfect6.1;charset=us-ascii"),
+ 1, "application/wordperfect6.1" },
+ { TEST_STRING("application/wordperfect6.1 ;charset=us-ascii"),
+ 1, "application/wordperfect6.1" },
+ { TEST_STRING("application/vnd.openxmlformats-officedocument.wordprocessingml.template"),
+ 0, "application/vnd.openxmlformats-officedocument.wordprocessingml.template" },
+ { TEST_STRING("application/vnd.openxmlformats-officedocument.wordprocessingml.template;charset=us-ascii"),
+ 1, "application/vnd.openxmlformats-officedocument.wordprocessingml.template" },
+ { TEST_STRING("application/vnd.openxmlformats-officedocument.wordprocessingml.template ;charset=us-asii"),
+ 1, "application/vnd.openxmlformats-officedocument.wordprocessingml.template" },
+ { TEST_STRING("(hello) text (plain) / (world) plain (eod)"),
+ 0, "text/plain" },
+ { TEST_STRING("(hello) text (plain) / (world) plain (eod);charset=us-ascii"),
+ 1, "text/plain" },
+ { TEST_STRING("(hello) text (plain) / (world) plain (eod); charset=us-ascii"),
+ 1, "text/plain" },
+ { TEST_STRING("message/rfc822\r\n"), 0, "message/rfc822" },
+ { TEST_STRING(" \t\r message/rfc822 \t\r\n"),
+ 0, "message/rfc822" },
+ { TEST_STRING(" \t\r message/rfc822 \t ;charset=us-ascii\r\n"),
+ 1, "message/rfc822" },
+ { TEST_STRING(" \t\r message/rfc822 \t ; charset=us-ascii\r\n"),
+ 1, "message/rfc822" },
+ { TEST_STRING("test\0/ty\0pe"), -1, "" },
+ };
+
+ for (size_t i = 0; i < N_ELEMENTS(test_cases); i++) T_BEGIN {
+ string_t *value = t_str_new(64);
+ struct rfc822_parser_context parser;
+
+ rfc822_parser_init(&parser, test_cases[i].input,
+ test_cases[i].input_len, NULL);
+ test_assert_idx(rfc822_parse_content_type(&parser, value) ==
+ test_cases[i].ret, i);
+ test_assert_strcmp_idx(test_cases[i].output, str_c(value), i);
+ rfc822_parser_deinit(&parser);
+ } T_END;
+}
+
+static void test_rfc822_parse_content_param(void)
+{
+ const char *input =
+ "; key1=value1#$!%&'*+-.^_`{|}~"
+ "; key2=\" \\\"(),/:;<=>?@[\\\\]\"";
+ const struct {
+ const char *key, *value;
+ } output[] = {
+ { "key1", "value1#$!%&'*+-.^_`{|}~" },
+ { "key2", " \"(),/:;<=>?@[\\]" }
+ };
+ struct rfc822_parser_context parser;
+ const char *key;
+ string_t *value = t_str_new(64);
+ unsigned int i = 0;
+ int ret;
+
+ test_begin("rfc822 parse content param");
+ rfc822_parser_init(&parser, (const void *)input, strlen(input), NULL);
+ while ((ret = rfc822_parse_content_param(&parser, &key, value)) > 0 &&
+ i < N_ELEMENTS(output)) {
+ test_assert_idx(strcmp(output[i].key, key) == 0, i);
+ test_assert_idx(strcmp(output[i].value, str_c(value)) == 0, i);
+ i++;
+ }
+ rfc822_parser_deinit(&parser);
+ test_assert(ret == 0);
+ test_assert(i == N_ELEMENTS(output));
+ test_end();
+}
+
+struct param {
+ const char *key, *value;
+};
+
+static void parse_content_type_param(const void *input, size_t input_len,
+ const char *content_type,
+ const struct param *params, size_t param_count,
+ bool expect_content_type, int expect_ret,
+ int idx)
+{
+ struct rfc822_parser_context parser;
+ const char *key;
+ string_t *value = t_str_new(64);
+ unsigned int i = 0;
+ int ret;
+
+ i_assert(params != NULL || param_count == 0);
+
+ rfc822_parser_init(&parser, input, input_len, NULL);
+ ret = rfc822_parse_content_type(&parser, value);
+ test_assert_idx((expect_content_type && ret == 1) ||
+ (!expect_content_type && ret == -1), idx);
+ test_assert_strcmp_idx(content_type, str_c(value), idx);
+
+ /* parse content type first */
+ while ((ret = rfc822_parse_content_param(&parser, &key, value)) > 0) {
+ if (i < param_count) {
+ test_assert_strcmp_idx(params[i].key, key, idx);
+ test_assert_strcmp_idx(params[i].value, str_c(value), idx);
+ }
+ i++;
+ }
+
+ test_assert_idx(expect_ret == ret, idx);
+ test_assert_idx(i == param_count, idx);
+ rfc822_parser_deinit(&parser);
+}
+
+#undef TEST_STRING
+#define TEST_STRING(a) (a), sizeof((a))-1
+
+#define X10(a) a a a a a a a a a a
+
+static void test_rfc822_parse_content_type_param(void)
+{
+ const char *input =
+ "(hello) text/plain ; (should we skip;comments=\"yes\")"
+ " param=value"
+ " ; param2=value2 (with comments (with comment) with;comment=\"yes\") "
+ " ; param3=\"value3 (with no comment ; or=value)\""
+ " ; param4=\"\xe7\xa8\xae\xe9\xa1\x9e\""
+ " ; \xe5\x90\xab\xe9\x87\x8f=\"\xe7\xa8\xae\xe9\xa1\x9e\""
+ " ; "X10(X10(X10("a")))"="X10(X10(X10("a")))
+ " ; "X10(X10(X10(X10("a"))))"="X10(X10(X10(X10("a"))))
+ " ; (comment) param7 (comment2) = (comment3) value7 (comment4) "
+ ;
+
+ const struct param output[] = {
+ { "param", "value" },
+ { "param2", "value2" },
+ { "param3", "value3 (with no comment ; or=value)" },
+ { "param4", "\xe7\xa8\xae\xe9\xa1\x9e" },
+ { "\xe5\x90\xab\xe9\x87\x8f", "\xe7\xa8\xae\xe9\xa1\x9e" },
+ { X10(X10(X10("a"))), X10(X10(X10("a"))) },
+ { X10(X10(X10(X10("a")))), X10(X10(X10(X10("a")))) },
+ { "param7", "value7" },
+ };
+ const struct param output5[] = {
+ { "charset", "" },
+ };
+
+ test_begin("rfc822 parse content type with params");
+
+ int idx = 0;
+ parse_content_type_param(input, strlen(input), "text/plain",
+ output, N_ELEMENTS(output), TRUE, 0, idx++);
+ parse_content_type_param(TEST_STRING("text/"), "", NULL, 0, FALSE, 0, idx++);
+ parse_content_type_param(
+ TEST_STRING("text/\0plain ;"), "", NULL, 0, FALSE, -1, idx++);
+ parse_content_type_param(
+ TEST_STRING("text/plain\0;charset=us-ascii"), "", NULL, 0, FALSE, -1, idx++);
+ parse_content_type_param(
+ TEST_STRING("text/plain;charset\0=us-ascii"), "text/plain", NULL, 0, TRUE, -1, idx++);
+ parse_content_type_param(
+ TEST_STRING("text/plain;charset="), "text/plain",
+ output5, N_ELEMENTS(output5), TRUE, 0, idx++);
+ parse_content_type_param(
+ TEST_STRING("text/plain ; ; charset=us-ascii"), "text/plain", NULL, 0, TRUE, -1, idx++);
+ /* build a large one */
+ ARRAY(struct param) output2;
+ t_array_init(&output2, 1000);
+ string_t *large = t_str_new(10000);
+ str_append(large, "text/plain");
+ for (unsigned int i = 0; i < 1000; i++) {
+ str_printfa(large, " ; param%u=\"value%u\"", i, i);
+ struct param *param = array_append_space(&output2);
+ param->key = t_strdup_printf("param%u", i);
+ param->value = t_strdup_printf("value%u", i);
+ }
+ parse_content_type_param(large->data, large->used,
+ "text/plain",
+ array_idx(&output2, 0), array_count(&output2),
+ TRUE, 0, idx++);
+ test_end();
+}
+
+int main(void)
+{
+ static void (*const test_functions[])(void) = {
+ test_rfc822_parse_comment,
+ test_rfc822_parse_comment_nuls,
+ test_rfc822_parse_quoted_string,
+ test_rfc822_parse_dot_atom,
+ test_rfc822_parse_domain_literal,
+ test_rfc822_parse_content_type,
+ test_rfc822_parse_content_param,
+ test_rfc822_parse_content_type_param,
+ NULL
+ };
+ return test_run(test_functions);
+}
diff --git a/src/lib-master/Makefile.am b/src/lib-master/Makefile.am
new file mode 100644
index 0000000..8133c05
--- /dev/null
+++ b/src/lib-master/Makefile.am
@@ -0,0 +1,83 @@
+pkgsysconfdir = $(sysconfdir)/dovecot
+
+noinst_LTLIBRARIES = libmaster.la
+
+AM_CPPFLAGS = \
+ -I$(top_srcdir)/src/lib \
+ -I$(top_srcdir)/src/lib-auth \
+ -I$(top_srcdir)/src/lib-dns \
+ -I$(top_srcdir)/src/lib-test \
+ -I$(top_srcdir)/src/lib-settings \
+ -I$(top_srcdir)/src/lib-ssl-iostream \
+ -DPKG_RUNDIR=\""$(rundir)"\" \
+ -DPKG_STATEDIR=\""$(statedir)"\" \
+ -DSYSCONFDIR=\""$(pkgsysconfdir)"\" \
+ -DBINDIR=\""$(bindir)"\"
+
+libmaster_la_SOURCES = \
+ anvil-client.c \
+ ipc-client.c \
+ ipc-server.c \
+ master-auth.c \
+ master-instance.c \
+ master-login.c \
+ master-login-auth.c \
+ master-service.c \
+ master-service-haproxy.c \
+ master-service-settings.c \
+ master-service-settings-cache.c \
+ master-service-ssl.c \
+ master-service-ssl-settings.c \
+ stats-client.c \
+ syslog-util.c
+
+headers = \
+ anvil-client.h \
+ ipc-client.h \
+ ipc-server.h \
+ master-auth.h \
+ master-instance.h \
+ master-interface.h \
+ master-login.h \
+ master-login-auth.h \
+ master-service.h \
+ master-service-private.h \
+ master-service-settings.h \
+ master-service-settings-cache.h \
+ master-service-ssl.h \
+ master-service-ssl-settings.h \
+ service-settings.h \
+ stats-client.h \
+ syslog-util.h
+
+pkginc_libdir=$(pkgincludedir)
+pkginc_lib_HEADERS = $(headers)
+
+test_programs = \
+ test-master-service-settings-cache \
+ test-event-stats
+
+noinst_PROGRAMS = $(test_programs)
+
+test_libs = \
+ ../lib-test/libtest.la \
+ ../lib-dns/libdns.la \
+ ../lib/liblib.la
+
+test_event_stats_libs = \
+ libmaster.la
+
+test_deps = $(noinst_LTLIBRARIES) $(test_libs)
+
+test_master_service_settings_cache_SOURCES = test-master-service-settings-cache.c
+test_master_service_settings_cache_LDADD = master-service-settings-cache.lo ../lib-settings/libsettings.la $(test_libs)
+test_master_service_settings_cache_DEPENDENCIES = $(test_deps) ../lib-settings/libsettings.la
+
+test_event_stats_SOURCES = test-event-stats.c
+test_event_stats_LDADD = $(test_event_stats_libs) $(test_libs)
+test_event_stats_DEPENDENCIES = $(test_deps)
+
+check-local:
+ for bin in $(test_programs); do \
+ if ! $(RUN_TEST) ./$$bin; then exit 1; fi; \
+ done
diff --git a/src/lib-master/Makefile.in b/src/lib-master/Makefile.in
new file mode 100644
index 0000000..e835d76
--- /dev/null
+++ b/src/lib-master/Makefile.in
@@ -0,0 +1,967 @@
+# Makefile.in generated by automake 1.16.1 from Makefile.am.
+# @configure_input@
+
+# Copyright (C) 1994-2018 Free Software Foundation, Inc.
+
+# This Makefile.in is free software; the Free Software Foundation
+# gives unlimited permission to copy and/or distribute it,
+# with or without modifications, as long as this notice is preserved.
+
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY, to the extent permitted by law; without
+# even the implied warranty of MERCHANTABILITY or FITNESS FOR A
+# PARTICULAR PURPOSE.
+
+@SET_MAKE@
+
+
+
+VPATH = @srcdir@
+am__is_gnu_make = { \
+ if test -z '$(MAKELEVEL)'; then \
+ false; \
+ elif test -n '$(MAKE_HOST)'; then \
+ true; \
+ elif test -n '$(MAKE_VERSION)' && test -n '$(CURDIR)'; then \
+ true; \
+ else \
+ false; \
+ fi; \
+}
+am__make_running_with_option = \
+ case $${target_option-} in \
+ ?) ;; \
+ *) echo "am__make_running_with_option: internal error: invalid" \
+ "target option '$${target_option-}' specified" >&2; \
+ exit 1;; \
+ esac; \
+ has_opt=no; \
+ sane_makeflags=$$MAKEFLAGS; \
+ if $(am__is_gnu_make); then \
+ sane_makeflags=$$MFLAGS; \
+ else \
+ case $$MAKEFLAGS in \
+ *\\[\ \ ]*) \
+ bs=\\; \
+ sane_makeflags=`printf '%s\n' "$$MAKEFLAGS" \
+ | sed "s/$$bs$$bs[$$bs $$bs ]*//g"`;; \
+ esac; \
+ fi; \
+ skip_next=no; \
+ strip_trailopt () \
+ { \
+ flg=`printf '%s\n' "$$flg" | sed "s/$$1.*$$//"`; \
+ }; \
+ for flg in $$sane_makeflags; do \
+ test $$skip_next = yes && { skip_next=no; continue; }; \
+ case $$flg in \
+ *=*|--*) continue;; \
+ -*I) strip_trailopt 'I'; skip_next=yes;; \
+ -*I?*) strip_trailopt 'I';; \
+ -*O) strip_trailopt 'O'; skip_next=yes;; \
+ -*O?*) strip_trailopt 'O';; \
+ -*l) strip_trailopt 'l'; skip_next=yes;; \
+ -*l?*) strip_trailopt 'l';; \
+ -[dEDm]) skip_next=yes;; \
+ -[JT]) skip_next=yes;; \
+ esac; \
+ case $$flg in \
+ *$$target_option*) has_opt=yes; break;; \
+ esac; \
+ done; \
+ test $$has_opt = yes
+am__make_dryrun = (target_option=n; $(am__make_running_with_option))
+am__make_keepgoing = (target_option=k; $(am__make_running_with_option))
+pkgdatadir = $(datadir)/@PACKAGE@
+pkgincludedir = $(includedir)/@PACKAGE@
+pkglibdir = $(libdir)/@PACKAGE@
+pkglibexecdir = $(libexecdir)/@PACKAGE@
+am__cd = CDPATH="$${ZSH_VERSION+.}$(PATH_SEPARATOR)" && cd
+install_sh_DATA = $(install_sh) -c -m 644
+install_sh_PROGRAM = $(install_sh) -c
+install_sh_SCRIPT = $(install_sh) -c
+INSTALL_HEADER = $(INSTALL_DATA)
+transform = $(program_transform_name)
+NORMAL_INSTALL = :
+PRE_INSTALL = :
+POST_INSTALL = :
+NORMAL_UNINSTALL = :
+PRE_UNINSTALL = :
+POST_UNINSTALL = :
+build_triplet = @build@
+host_triplet = @host@
+noinst_PROGRAMS = $(am__EXEEXT_1)
+subdir = src/lib-master
+ACLOCAL_M4 = $(top_srcdir)/aclocal.m4
+am__aclocal_m4_deps = $(top_srcdir)/m4/ac_checktype2.m4 \
+ $(top_srcdir)/m4/ac_typeof.m4 $(top_srcdir)/m4/arc4random.m4 \
+ $(top_srcdir)/m4/blockdev.m4 $(top_srcdir)/m4/c99_vsnprintf.m4 \
+ $(top_srcdir)/m4/clock_gettime.m4 $(top_srcdir)/m4/crypt.m4 \
+ $(top_srcdir)/m4/crypt_xpg6.m4 $(top_srcdir)/m4/dbqlk.m4 \
+ $(top_srcdir)/m4/dirent_dtype.m4 $(top_srcdir)/m4/dovecot.m4 \
+ $(top_srcdir)/m4/fd_passing.m4 $(top_srcdir)/m4/fdatasync.m4 \
+ $(top_srcdir)/m4/flexible_array_member.m4 \
+ $(top_srcdir)/m4/glibc.m4 $(top_srcdir)/m4/gmtime_max.m4 \
+ $(top_srcdir)/m4/gmtime_tm_gmtoff.m4 \
+ $(top_srcdir)/m4/ioloop.m4 $(top_srcdir)/m4/iovec.m4 \
+ $(top_srcdir)/m4/ipv6.m4 $(top_srcdir)/m4/libcap.m4 \
+ $(top_srcdir)/m4/libtool.m4 $(top_srcdir)/m4/libwrap.m4 \
+ $(top_srcdir)/m4/linux_mremap.m4 $(top_srcdir)/m4/ltoptions.m4 \
+ $(top_srcdir)/m4/ltsugar.m4 $(top_srcdir)/m4/ltversion.m4 \
+ $(top_srcdir)/m4/lt~obsolete.m4 $(top_srcdir)/m4/mmap_write.m4 \
+ $(top_srcdir)/m4/mntctl.m4 $(top_srcdir)/m4/modules.m4 \
+ $(top_srcdir)/m4/notify.m4 $(top_srcdir)/m4/nsl.m4 \
+ $(top_srcdir)/m4/off_t_max.m4 $(top_srcdir)/m4/pkg.m4 \
+ $(top_srcdir)/m4/pr_set_dumpable.m4 \
+ $(top_srcdir)/m4/q_quotactl.m4 $(top_srcdir)/m4/quota.m4 \
+ $(top_srcdir)/m4/random.m4 $(top_srcdir)/m4/rlimit.m4 \
+ $(top_srcdir)/m4/sendfile.m4 $(top_srcdir)/m4/size_t_signed.m4 \
+ $(top_srcdir)/m4/sockpeercred.m4 $(top_srcdir)/m4/sql.m4 \
+ $(top_srcdir)/m4/ssl.m4 $(top_srcdir)/m4/st_tim.m4 \
+ $(top_srcdir)/m4/static_array.m4 $(top_srcdir)/m4/test_with.m4 \
+ $(top_srcdir)/m4/time_t.m4 $(top_srcdir)/m4/typeof.m4 \
+ $(top_srcdir)/m4/typeof_dev_t.m4 \
+ $(top_srcdir)/m4/uoff_t_max.m4 $(top_srcdir)/m4/vararg.m4 \
+ $(top_srcdir)/m4/want_apparmor.m4 \
+ $(top_srcdir)/m4/want_bsdauth.m4 \
+ $(top_srcdir)/m4/want_bzlib.m4 \
+ $(top_srcdir)/m4/want_cassandra.m4 \
+ $(top_srcdir)/m4/want_cdb.m4 \
+ $(top_srcdir)/m4/want_checkpassword.m4 \
+ $(top_srcdir)/m4/want_clucene.m4 $(top_srcdir)/m4/want_db.m4 \
+ $(top_srcdir)/m4/want_gssapi.m4 $(top_srcdir)/m4/want_icu.m4 \
+ $(top_srcdir)/m4/want_ldap.m4 $(top_srcdir)/m4/want_lua.m4 \
+ $(top_srcdir)/m4/want_lz4.m4 $(top_srcdir)/m4/want_lzma.m4 \
+ $(top_srcdir)/m4/want_mysql.m4 $(top_srcdir)/m4/want_pam.m4 \
+ $(top_srcdir)/m4/want_passwd.m4 $(top_srcdir)/m4/want_pgsql.m4 \
+ $(top_srcdir)/m4/want_prefetch.m4 \
+ $(top_srcdir)/m4/want_shadow.m4 \
+ $(top_srcdir)/m4/want_sodium.m4 $(top_srcdir)/m4/want_solr.m4 \
+ $(top_srcdir)/m4/want_sqlite.m4 \
+ $(top_srcdir)/m4/want_stemmer.m4 \
+ $(top_srcdir)/m4/want_systemd.m4 \
+ $(top_srcdir)/m4/want_textcat.m4 \
+ $(top_srcdir)/m4/want_unwind.m4 $(top_srcdir)/m4/want_zlib.m4 \
+ $(top_srcdir)/m4/want_zstd.m4 $(top_srcdir)/configure.ac
+am__configure_deps = $(am__aclocal_m4_deps) $(CONFIGURE_DEPENDENCIES) \
+ $(ACLOCAL_M4)
+DIST_COMMON = $(srcdir)/Makefile.am $(pkginc_lib_HEADERS) \
+ $(am__DIST_COMMON)
+mkinstalldirs = $(install_sh) -d
+CONFIG_HEADER = $(top_builddir)/config.h
+CONFIG_CLEAN_FILES =
+CONFIG_CLEAN_VPATH_FILES =
+am__EXEEXT_1 = test-master-service-settings-cache$(EXEEXT) \
+ test-event-stats$(EXEEXT)
+PROGRAMS = $(noinst_PROGRAMS)
+LTLIBRARIES = $(noinst_LTLIBRARIES)
+libmaster_la_LIBADD =
+am_libmaster_la_OBJECTS = anvil-client.lo ipc-client.lo ipc-server.lo \
+ master-auth.lo master-instance.lo master-login.lo \
+ master-login-auth.lo master-service.lo \
+ master-service-haproxy.lo master-service-settings.lo \
+ master-service-settings-cache.lo master-service-ssl.lo \
+ master-service-ssl-settings.lo stats-client.lo syslog-util.lo
+libmaster_la_OBJECTS = $(am_libmaster_la_OBJECTS)
+AM_V_lt = $(am__v_lt_@AM_V@)
+am__v_lt_ = $(am__v_lt_@AM_DEFAULT_V@)
+am__v_lt_0 = --silent
+am__v_lt_1 =
+am_test_event_stats_OBJECTS = test-event-stats.$(OBJEXT)
+test_event_stats_OBJECTS = $(am_test_event_stats_OBJECTS)
+am_test_master_service_settings_cache_OBJECTS = \
+ test-master-service-settings-cache.$(OBJEXT)
+test_master_service_settings_cache_OBJECTS = \
+ $(am_test_master_service_settings_cache_OBJECTS)
+AM_V_P = $(am__v_P_@AM_V@)
+am__v_P_ = $(am__v_P_@AM_DEFAULT_V@)
+am__v_P_0 = false
+am__v_P_1 = :
+AM_V_GEN = $(am__v_GEN_@AM_V@)
+am__v_GEN_ = $(am__v_GEN_@AM_DEFAULT_V@)
+am__v_GEN_0 = @echo " GEN " $@;
+am__v_GEN_1 =
+AM_V_at = $(am__v_at_@AM_V@)
+am__v_at_ = $(am__v_at_@AM_DEFAULT_V@)
+am__v_at_0 = @
+am__v_at_1 =
+DEFAULT_INCLUDES = -I.@am__isrc@ -I$(top_builddir)
+depcomp = $(SHELL) $(top_srcdir)/depcomp
+am__maybe_remake_depfiles = depfiles
+am__depfiles_remade = ./$(DEPDIR)/anvil-client.Plo \
+ ./$(DEPDIR)/ipc-client.Plo ./$(DEPDIR)/ipc-server.Plo \
+ ./$(DEPDIR)/master-auth.Plo ./$(DEPDIR)/master-instance.Plo \
+ ./$(DEPDIR)/master-login-auth.Plo ./$(DEPDIR)/master-login.Plo \
+ ./$(DEPDIR)/master-service-haproxy.Plo \
+ ./$(DEPDIR)/master-service-settings-cache.Plo \
+ ./$(DEPDIR)/master-service-settings.Plo \
+ ./$(DEPDIR)/master-service-ssl-settings.Plo \
+ ./$(DEPDIR)/master-service-ssl.Plo \
+ ./$(DEPDIR)/master-service.Plo ./$(DEPDIR)/stats-client.Plo \
+ ./$(DEPDIR)/syslog-util.Plo ./$(DEPDIR)/test-event-stats.Po \
+ ./$(DEPDIR)/test-master-service-settings-cache.Po
+am__mv = mv -f
+COMPILE = $(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) \
+ $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS)
+LTCOMPILE = $(LIBTOOL) $(AM_V_lt) --tag=CC $(AM_LIBTOOLFLAGS) \
+ $(LIBTOOLFLAGS) --mode=compile $(CC) $(DEFS) \
+ $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) $(CPPFLAGS) \
+ $(AM_CFLAGS) $(CFLAGS)
+AM_V_CC = $(am__v_CC_@AM_V@)
+am__v_CC_ = $(am__v_CC_@AM_DEFAULT_V@)
+am__v_CC_0 = @echo " CC " $@;
+am__v_CC_1 =
+CCLD = $(CC)
+LINK = $(LIBTOOL) $(AM_V_lt) --tag=CC $(AM_LIBTOOLFLAGS) \
+ $(LIBTOOLFLAGS) --mode=link $(CCLD) $(AM_CFLAGS) $(CFLAGS) \
+ $(AM_LDFLAGS) $(LDFLAGS) -o $@
+AM_V_CCLD = $(am__v_CCLD_@AM_V@)
+am__v_CCLD_ = $(am__v_CCLD_@AM_DEFAULT_V@)
+am__v_CCLD_0 = @echo " CCLD " $@;
+am__v_CCLD_1 =
+SOURCES = $(libmaster_la_SOURCES) $(test_event_stats_SOURCES) \
+ $(test_master_service_settings_cache_SOURCES)
+DIST_SOURCES = $(libmaster_la_SOURCES) $(test_event_stats_SOURCES) \
+ $(test_master_service_settings_cache_SOURCES)
+am__can_run_installinfo = \
+ case $$AM_UPDATE_INFO_DIR in \
+ n|no|NO) false;; \
+ *) (install-info --version) >/dev/null 2>&1;; \
+ esac
+am__vpath_adj_setup = srcdirstrip=`echo "$(srcdir)" | sed 's|.|.|g'`;
+am__vpath_adj = case $$p in \
+ $(srcdir)/*) f=`echo "$$p" | sed "s|^$$srcdirstrip/||"`;; \
+ *) f=$$p;; \
+ esac;
+am__strip_dir = f=`echo $$p | sed -e 's|^.*/||'`;
+am__install_max = 40
+am__nobase_strip_setup = \
+ srcdirstrip=`echo "$(srcdir)" | sed 's/[].[^$$\\*|]/\\\\&/g'`
+am__nobase_strip = \
+ for p in $$list; do echo "$$p"; done | sed -e "s|$$srcdirstrip/||"
+am__nobase_list = $(am__nobase_strip_setup); \
+ for p in $$list; do echo "$$p $$p"; done | \
+ sed "s| $$srcdirstrip/| |;"' / .*\//!s/ .*/ ./; s,\( .*\)/[^/]*$$,\1,' | \
+ $(AWK) 'BEGIN { files["."] = "" } { files[$$2] = files[$$2] " " $$1; \
+ if (++n[$$2] == $(am__install_max)) \
+ { print $$2, files[$$2]; n[$$2] = 0; files[$$2] = "" } } \
+ END { for (dir in files) print dir, files[dir] }'
+am__base_list = \
+ sed '$$!N;$$!N;$$!N;$$!N;$$!N;$$!N;$$!N;s/\n/ /g' | \
+ sed '$$!N;$$!N;$$!N;$$!N;s/\n/ /g'
+am__uninstall_files_from_dir = { \
+ test -z "$$files" \
+ || { test ! -d "$$dir" && test ! -f "$$dir" && test ! -r "$$dir"; } \
+ || { echo " ( cd '$$dir' && rm -f" $$files ")"; \
+ $(am__cd) "$$dir" && rm -f $$files; }; \
+ }
+am__installdirs = "$(DESTDIR)$(pkginc_libdir)"
+HEADERS = $(pkginc_lib_HEADERS)
+am__tagged_files = $(HEADERS) $(SOURCES) $(TAGS_FILES) $(LISP)
+# Read a list of newline-separated strings from the standard input,
+# and print each of them once, without duplicates. Input order is
+# *not* preserved.
+am__uniquify_input = $(AWK) '\
+ BEGIN { nonempty = 0; } \
+ { items[$$0] = 1; nonempty = 1; } \
+ END { if (nonempty) { for (i in items) print i; }; } \
+'
+# Make sure the list of sources is unique. This is necessary because,
+# e.g., the same source file might be shared among _SOURCES variables
+# for different programs/libraries.
+am__define_uniq_tagged_files = \
+ list='$(am__tagged_files)'; \
+ unique=`for i in $$list; do \
+ if test -f "$$i"; then echo $$i; else echo $(srcdir)/$$i; fi; \
+ done | $(am__uniquify_input)`
+ETAGS = etags
+CTAGS = ctags
+am__DIST_COMMON = $(srcdir)/Makefile.in $(top_srcdir)/depcomp
+DISTFILES = $(DIST_COMMON) $(DIST_SOURCES) $(TEXINFOS) $(EXTRA_DIST)
+ACLOCAL = @ACLOCAL@
+ACLOCAL_AMFLAGS = @ACLOCAL_AMFLAGS@
+AMTAR = @AMTAR@
+AM_DEFAULT_VERBOSITY = @AM_DEFAULT_VERBOSITY@
+APPARMOR_LIBS = @APPARMOR_LIBS@
+AR = @AR@
+AUTH_CFLAGS = @AUTH_CFLAGS@
+AUTH_LIBS = @AUTH_LIBS@
+AUTOCONF = @AUTOCONF@
+AUTOHEADER = @AUTOHEADER@
+AUTOMAKE = @AUTOMAKE@
+AWK = @AWK@
+BINARY_CFLAGS = @BINARY_CFLAGS@
+BINARY_LDFLAGS = @BINARY_LDFLAGS@
+BISON = @BISON@
+CASSANDRA_CFLAGS = @CASSANDRA_CFLAGS@
+CASSANDRA_LIBS = @CASSANDRA_LIBS@
+CC = @CC@
+CCDEPMODE = @CCDEPMODE@
+CDB_LIBS = @CDB_LIBS@
+CFLAGS = @CFLAGS@
+CLUCENE_CFLAGS = @CLUCENE_CFLAGS@
+CLUCENE_LIBS = @CLUCENE_LIBS@
+COMPRESS_LIBS = @COMPRESS_LIBS@
+CPP = @CPP@
+CPPFLAGS = @CPPFLAGS@
+CRYPT_LIBS = @CRYPT_LIBS@
+CXX = @CXX@
+CXXCPP = @CXXCPP@
+CXXDEPMODE = @CXXDEPMODE@
+CXXFLAGS = @CXXFLAGS@
+CYGPATH_W = @CYGPATH_W@
+DEFS = @DEFS@
+DEPDIR = @DEPDIR@
+DICT_LIBS = @DICT_LIBS@
+DLLIB = @DLLIB@
+DLLTOOL = @DLLTOOL@
+DSYMUTIL = @DSYMUTIL@
+DUMPBIN = @DUMPBIN@
+ECHO_C = @ECHO_C@
+ECHO_N = @ECHO_N@
+ECHO_T = @ECHO_T@
+EGREP = @EGREP@
+EXEEXT = @EXEEXT@
+FGREP = @FGREP@
+FLEX = @FLEX@
+FUZZER_CPPFLAGS = @FUZZER_CPPFLAGS@
+FUZZER_LDFLAGS = @FUZZER_LDFLAGS@
+GREP = @GREP@
+INSTALL = @INSTALL@
+INSTALL_DATA = @INSTALL_DATA@
+INSTALL_PROGRAM = @INSTALL_PROGRAM@
+INSTALL_SCRIPT = @INSTALL_SCRIPT@
+INSTALL_STRIP_PROGRAM = @INSTALL_STRIP_PROGRAM@
+KRB5CONFIG = @KRB5CONFIG@
+KRB5_CFLAGS = @KRB5_CFLAGS@
+KRB5_LIBS = @KRB5_LIBS@
+LD = @LD@
+LDAP_LIBS = @LDAP_LIBS@
+LDFLAGS = @LDFLAGS@
+LD_NO_WHOLE_ARCHIVE = @LD_NO_WHOLE_ARCHIVE@
+LD_WHOLE_ARCHIVE = @LD_WHOLE_ARCHIVE@
+LIBCAP = @LIBCAP@
+LIBDOVECOT = @LIBDOVECOT@
+LIBDOVECOT_COMPRESS = @LIBDOVECOT_COMPRESS@
+LIBDOVECOT_DEPS = @LIBDOVECOT_DEPS@
+LIBDOVECOT_DSYNC = @LIBDOVECOT_DSYNC@
+LIBDOVECOT_LA_LIBS = @LIBDOVECOT_LA_LIBS@
+LIBDOVECOT_LDA = @LIBDOVECOT_LDA@
+LIBDOVECOT_LDAP = @LIBDOVECOT_LDAP@
+LIBDOVECOT_LIBFTS = @LIBDOVECOT_LIBFTS@
+LIBDOVECOT_LIBFTS_DEPS = @LIBDOVECOT_LIBFTS_DEPS@
+LIBDOVECOT_LOGIN = @LIBDOVECOT_LOGIN@
+LIBDOVECOT_LUA = @LIBDOVECOT_LUA@
+LIBDOVECOT_LUA_DEPS = @LIBDOVECOT_LUA_DEPS@
+LIBDOVECOT_SQL = @LIBDOVECOT_SQL@
+LIBDOVECOT_STORAGE = @LIBDOVECOT_STORAGE@
+LIBDOVECOT_STORAGE_DEPS = @LIBDOVECOT_STORAGE_DEPS@
+LIBEXTTEXTCAT_CFLAGS = @LIBEXTTEXTCAT_CFLAGS@
+LIBEXTTEXTCAT_LIBS = @LIBEXTTEXTCAT_LIBS@
+LIBICONV = @LIBICONV@
+LIBICU_CFLAGS = @LIBICU_CFLAGS@
+LIBICU_LIBS = @LIBICU_LIBS@
+LIBOBJS = @LIBOBJS@
+LIBS = @LIBS@
+LIBSODIUM_CFLAGS = @LIBSODIUM_CFLAGS@
+LIBSODIUM_LIBS = @LIBSODIUM_LIBS@
+LIBTIRPC_CFLAGS = @LIBTIRPC_CFLAGS@
+LIBTIRPC_LIBS = @LIBTIRPC_LIBS@
+LIBTOOL = @LIBTOOL@
+LIBUNWIND_CFLAGS = @LIBUNWIND_CFLAGS@
+LIBUNWIND_LIBS = @LIBUNWIND_LIBS@
+LIBWRAP_LIBS = @LIBWRAP_LIBS@
+LINKED_STORAGE_LDADD = @LINKED_STORAGE_LDADD@
+LIPO = @LIPO@
+LN_S = @LN_S@
+LTLIBICONV = @LTLIBICONV@
+LTLIBOBJS = @LTLIBOBJS@
+LT_SYS_LIBRARY_PATH = @LT_SYS_LIBRARY_PATH@
+LUA_CFLAGS = @LUA_CFLAGS@
+LUA_LIBS = @LUA_LIBS@
+MAINT = @MAINT@
+MAKEINFO = @MAKEINFO@
+MANIFEST_TOOL = @MANIFEST_TOOL@
+MKDIR_P = @MKDIR_P@
+MODULE_LIBS = @MODULE_LIBS@
+MODULE_SUFFIX = @MODULE_SUFFIX@
+MYSQL_CFLAGS = @MYSQL_CFLAGS@
+MYSQL_CONFIG = @MYSQL_CONFIG@
+MYSQL_LIBS = @MYSQL_LIBS@
+NM = @NM@
+NMEDIT = @NMEDIT@
+NOPLUGIN_LDFLAGS = @NOPLUGIN_LDFLAGS@
+OBJDUMP = @OBJDUMP@
+OBJEXT = @OBJEXT@
+OTOOL = @OTOOL@
+OTOOL64 = @OTOOL64@
+PACKAGE = @PACKAGE@
+PACKAGE_BUGREPORT = @PACKAGE_BUGREPORT@
+PACKAGE_NAME = @PACKAGE_NAME@
+PACKAGE_STRING = @PACKAGE_STRING@
+PACKAGE_TARNAME = @PACKAGE_TARNAME@
+PACKAGE_URL = @PACKAGE_URL@
+PACKAGE_VERSION = @PACKAGE_VERSION@
+PANDOC = @PANDOC@
+PATH_SEPARATOR = @PATH_SEPARATOR@
+PGSQL_CFLAGS = @PGSQL_CFLAGS@
+PGSQL_LIBS = @PGSQL_LIBS@
+PG_CONFIG = @PG_CONFIG@
+PIE_CFLAGS = @PIE_CFLAGS@
+PIE_LDFLAGS = @PIE_LDFLAGS@
+PKG_CONFIG = @PKG_CONFIG@
+PKG_CONFIG_LIBDIR = @PKG_CONFIG_LIBDIR@
+PKG_CONFIG_PATH = @PKG_CONFIG_PATH@
+QUOTA_LIBS = @QUOTA_LIBS@
+RANLIB = @RANLIB@
+RELRO_LDFLAGS = @RELRO_LDFLAGS@
+RPCGEN = @RPCGEN@
+RUN_TEST = @RUN_TEST@
+SED = @SED@
+SETTING_FILES = @SETTING_FILES@
+SET_MAKE = @SET_MAKE@
+SHELL = @SHELL@
+SQLITE_CFLAGS = @SQLITE_CFLAGS@
+SQLITE_LIBS = @SQLITE_LIBS@
+SQL_CFLAGS = @SQL_CFLAGS@
+SQL_LIBS = @SQL_LIBS@
+SSL_CFLAGS = @SSL_CFLAGS@
+SSL_LIBS = @SSL_LIBS@
+STRIP = @STRIP@
+SYSTEMD_CFLAGS = @SYSTEMD_CFLAGS@
+SYSTEMD_LIBS = @SYSTEMD_LIBS@
+VALGRIND = @VALGRIND@
+VERSION = @VERSION@
+ZSTD_CFLAGS = @ZSTD_CFLAGS@
+ZSTD_LIBS = @ZSTD_LIBS@
+abs_builddir = @abs_builddir@
+abs_srcdir = @abs_srcdir@
+abs_top_builddir = @abs_top_builddir@
+abs_top_srcdir = @abs_top_srcdir@
+ac_ct_AR = @ac_ct_AR@
+ac_ct_CC = @ac_ct_CC@
+ac_ct_CXX = @ac_ct_CXX@
+ac_ct_DUMPBIN = @ac_ct_DUMPBIN@
+am__include = @am__include@
+am__leading_dot = @am__leading_dot@
+am__quote = @am__quote@
+am__tar = @am__tar@
+am__untar = @am__untar@
+bindir = @bindir@
+build = @build@
+build_alias = @build_alias@
+build_cpu = @build_cpu@
+build_os = @build_os@
+build_vendor = @build_vendor@
+builddir = @builddir@
+datadir = @datadir@
+datarootdir = @datarootdir@
+dict_drivers = @dict_drivers@
+docdir = @docdir@
+dvidir = @dvidir@
+exec_prefix = @exec_prefix@
+host = @host@
+host_alias = @host_alias@
+host_cpu = @host_cpu@
+host_os = @host_os@
+host_vendor = @host_vendor@
+htmldir = @htmldir@
+includedir = @includedir@
+infodir = @infodir@
+install_sh = @install_sh@
+libdir = @libdir@
+libexecdir = @libexecdir@
+localedir = @localedir@
+localstatedir = @localstatedir@
+mandir = @mandir@
+mkdir_p = @mkdir_p@
+moduledir = @moduledir@
+oldincludedir = @oldincludedir@
+pdfdir = @pdfdir@
+prefix = @prefix@
+program_transform_name = @program_transform_name@
+psdir = @psdir@
+rundir = @rundir@
+runstatedir = @runstatedir@
+sbindir = @sbindir@
+sharedstatedir = @sharedstatedir@
+sql_drivers = @sql_drivers@
+srcdir = @srcdir@
+ssldir = @ssldir@
+statedir = @statedir@
+sysconfdir = @sysconfdir@
+systemdservicetype = @systemdservicetype@
+systemdsystemunitdir = @systemdsystemunitdir@
+target_alias = @target_alias@
+top_build_prefix = @top_build_prefix@
+top_builddir = @top_builddir@
+top_srcdir = @top_srcdir@
+pkgsysconfdir = $(sysconfdir)/dovecot
+noinst_LTLIBRARIES = libmaster.la
+AM_CPPFLAGS = \
+ -I$(top_srcdir)/src/lib \
+ -I$(top_srcdir)/src/lib-auth \
+ -I$(top_srcdir)/src/lib-dns \
+ -I$(top_srcdir)/src/lib-test \
+ -I$(top_srcdir)/src/lib-settings \
+ -I$(top_srcdir)/src/lib-ssl-iostream \
+ -DPKG_RUNDIR=\""$(rundir)"\" \
+ -DPKG_STATEDIR=\""$(statedir)"\" \
+ -DSYSCONFDIR=\""$(pkgsysconfdir)"\" \
+ -DBINDIR=\""$(bindir)"\"
+
+libmaster_la_SOURCES = \
+ anvil-client.c \
+ ipc-client.c \
+ ipc-server.c \
+ master-auth.c \
+ master-instance.c \
+ master-login.c \
+ master-login-auth.c \
+ master-service.c \
+ master-service-haproxy.c \
+ master-service-settings.c \
+ master-service-settings-cache.c \
+ master-service-ssl.c \
+ master-service-ssl-settings.c \
+ stats-client.c \
+ syslog-util.c
+
+headers = \
+ anvil-client.h \
+ ipc-client.h \
+ ipc-server.h \
+ master-auth.h \
+ master-instance.h \
+ master-interface.h \
+ master-login.h \
+ master-login-auth.h \
+ master-service.h \
+ master-service-private.h \
+ master-service-settings.h \
+ master-service-settings-cache.h \
+ master-service-ssl.h \
+ master-service-ssl-settings.h \
+ service-settings.h \
+ stats-client.h \
+ syslog-util.h
+
+pkginc_libdir = $(pkgincludedir)
+pkginc_lib_HEADERS = $(headers)
+test_programs = \
+ test-master-service-settings-cache \
+ test-event-stats
+
+test_libs = \
+ ../lib-test/libtest.la \
+ ../lib-dns/libdns.la \
+ ../lib/liblib.la
+
+test_event_stats_libs = \
+ libmaster.la
+
+test_deps = $(noinst_LTLIBRARIES) $(test_libs)
+test_master_service_settings_cache_SOURCES = test-master-service-settings-cache.c
+test_master_service_settings_cache_LDADD = master-service-settings-cache.lo ../lib-settings/libsettings.la $(test_libs)
+test_master_service_settings_cache_DEPENDENCIES = $(test_deps) ../lib-settings/libsettings.la
+test_event_stats_SOURCES = test-event-stats.c
+test_event_stats_LDADD = $(test_event_stats_libs) $(test_libs)
+test_event_stats_DEPENDENCIES = $(test_deps)
+all: all-am
+
+.SUFFIXES:
+.SUFFIXES: .c .lo .o .obj
+$(srcdir)/Makefile.in: @MAINTAINER_MODE_TRUE@ $(srcdir)/Makefile.am $(am__configure_deps)
+ @for dep in $?; do \
+ case '$(am__configure_deps)' in \
+ *$$dep*) \
+ ( cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh ) \
+ && { if test -f $@; then exit 0; else break; fi; }; \
+ exit 1;; \
+ esac; \
+ done; \
+ echo ' cd $(top_srcdir) && $(AUTOMAKE) --foreign src/lib-master/Makefile'; \
+ $(am__cd) $(top_srcdir) && \
+ $(AUTOMAKE) --foreign src/lib-master/Makefile
+Makefile: $(srcdir)/Makefile.in $(top_builddir)/config.status
+ @case '$?' in \
+ *config.status*) \
+ cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh;; \
+ *) \
+ echo ' cd $(top_builddir) && $(SHELL) ./config.status $(subdir)/$@ $(am__maybe_remake_depfiles)'; \
+ cd $(top_builddir) && $(SHELL) ./config.status $(subdir)/$@ $(am__maybe_remake_depfiles);; \
+ esac;
+
+$(top_builddir)/config.status: $(top_srcdir)/configure $(CONFIG_STATUS_DEPENDENCIES)
+ cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh
+
+$(top_srcdir)/configure: @MAINTAINER_MODE_TRUE@ $(am__configure_deps)
+ cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh
+$(ACLOCAL_M4): @MAINTAINER_MODE_TRUE@ $(am__aclocal_m4_deps)
+ cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh
+$(am__aclocal_m4_deps):
+
+clean-noinstPROGRAMS:
+ @list='$(noinst_PROGRAMS)'; test -n "$$list" || exit 0; \
+ echo " rm -f" $$list; \
+ rm -f $$list || exit $$?; \
+ test -n "$(EXEEXT)" || exit 0; \
+ list=`for p in $$list; do echo "$$p"; done | sed 's/$(EXEEXT)$$//'`; \
+ echo " rm -f" $$list; \
+ rm -f $$list
+
+clean-noinstLTLIBRARIES:
+ -test -z "$(noinst_LTLIBRARIES)" || rm -f $(noinst_LTLIBRARIES)
+ @list='$(noinst_LTLIBRARIES)'; \
+ locs=`for p in $$list; do echo $$p; done | \
+ sed 's|^[^/]*$$|.|; s|/[^/]*$$||; s|$$|/so_locations|' | \
+ sort -u`; \
+ test -z "$$locs" || { \
+ echo rm -f $${locs}; \
+ rm -f $${locs}; \
+ }
+
+libmaster.la: $(libmaster_la_OBJECTS) $(libmaster_la_DEPENDENCIES) $(EXTRA_libmaster_la_DEPENDENCIES)
+ $(AM_V_CCLD)$(LINK) $(libmaster_la_OBJECTS) $(libmaster_la_LIBADD) $(LIBS)
+
+test-event-stats$(EXEEXT): $(test_event_stats_OBJECTS) $(test_event_stats_DEPENDENCIES) $(EXTRA_test_event_stats_DEPENDENCIES)
+ @rm -f test-event-stats$(EXEEXT)
+ $(AM_V_CCLD)$(LINK) $(test_event_stats_OBJECTS) $(test_event_stats_LDADD) $(LIBS)
+
+test-master-service-settings-cache$(EXEEXT): $(test_master_service_settings_cache_OBJECTS) $(test_master_service_settings_cache_DEPENDENCIES) $(EXTRA_test_master_service_settings_cache_DEPENDENCIES)
+ @rm -f test-master-service-settings-cache$(EXEEXT)
+ $(AM_V_CCLD)$(LINK) $(test_master_service_settings_cache_OBJECTS) $(test_master_service_settings_cache_LDADD) $(LIBS)
+
+mostlyclean-compile:
+ -rm -f *.$(OBJEXT)
+
+distclean-compile:
+ -rm -f *.tab.c
+
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/anvil-client.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/ipc-client.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/ipc-server.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/master-auth.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/master-instance.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/master-login-auth.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/master-login.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/master-service-haproxy.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/master-service-settings-cache.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/master-service-settings.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/master-service-ssl-settings.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/master-service-ssl.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/master-service.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/stats-client.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/syslog-util.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/test-event-stats.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/test-master-service-settings-cache.Po@am__quote@ # am--include-marker
+
+$(am__depfiles_remade):
+ @$(MKDIR_P) $(@D)
+ @echo '# dummy' >$@-t && $(am__mv) $@-t $@
+
+am--depfiles: $(am__depfiles_remade)
+
+.c.o:
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(COMPILE) -MT $@ -MD -MP -MF $(DEPDIR)/$*.Tpo -c -o $@ $<
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/$*.Tpo $(DEPDIR)/$*.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='$<' object='$@' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(COMPILE) -c -o $@ $<
+
+.c.obj:
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(COMPILE) -MT $@ -MD -MP -MF $(DEPDIR)/$*.Tpo -c -o $@ `$(CYGPATH_W) '$<'`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/$*.Tpo $(DEPDIR)/$*.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='$<' object='$@' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(COMPILE) -c -o $@ `$(CYGPATH_W) '$<'`
+
+.c.lo:
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(LTCOMPILE) -MT $@ -MD -MP -MF $(DEPDIR)/$*.Tpo -c -o $@ $<
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/$*.Tpo $(DEPDIR)/$*.Plo
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='$<' object='$@' libtool=yes @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(LTCOMPILE) -c -o $@ $<
+
+mostlyclean-libtool:
+ -rm -f *.lo
+
+clean-libtool:
+ -rm -rf .libs _libs
+install-pkginc_libHEADERS: $(pkginc_lib_HEADERS)
+ @$(NORMAL_INSTALL)
+ @list='$(pkginc_lib_HEADERS)'; test -n "$(pkginc_libdir)" || list=; \
+ if test -n "$$list"; then \
+ echo " $(MKDIR_P) '$(DESTDIR)$(pkginc_libdir)'"; \
+ $(MKDIR_P) "$(DESTDIR)$(pkginc_libdir)" || exit 1; \
+ fi; \
+ for p in $$list; do \
+ if test -f "$$p"; then d=; else d="$(srcdir)/"; fi; \
+ echo "$$d$$p"; \
+ done | $(am__base_list) | \
+ while read files; do \
+ echo " $(INSTALL_HEADER) $$files '$(DESTDIR)$(pkginc_libdir)'"; \
+ $(INSTALL_HEADER) $$files "$(DESTDIR)$(pkginc_libdir)" || exit $$?; \
+ done
+
+uninstall-pkginc_libHEADERS:
+ @$(NORMAL_UNINSTALL)
+ @list='$(pkginc_lib_HEADERS)'; test -n "$(pkginc_libdir)" || list=; \
+ files=`for p in $$list; do echo $$p; done | sed -e 's|^.*/||'`; \
+ dir='$(DESTDIR)$(pkginc_libdir)'; $(am__uninstall_files_from_dir)
+
+ID: $(am__tagged_files)
+ $(am__define_uniq_tagged_files); mkid -fID $$unique
+tags: tags-am
+TAGS: tags
+
+tags-am: $(TAGS_DEPENDENCIES) $(am__tagged_files)
+ set x; \
+ here=`pwd`; \
+ $(am__define_uniq_tagged_files); \
+ shift; \
+ if test -z "$(ETAGS_ARGS)$$*$$unique"; then :; else \
+ test -n "$$unique" || unique=$$empty_fix; \
+ if test $$# -gt 0; then \
+ $(ETAGS) $(ETAGSFLAGS) $(AM_ETAGSFLAGS) $(ETAGS_ARGS) \
+ "$$@" $$unique; \
+ else \
+ $(ETAGS) $(ETAGSFLAGS) $(AM_ETAGSFLAGS) $(ETAGS_ARGS) \
+ $$unique; \
+ fi; \
+ fi
+ctags: ctags-am
+
+CTAGS: ctags
+ctags-am: $(TAGS_DEPENDENCIES) $(am__tagged_files)
+ $(am__define_uniq_tagged_files); \
+ test -z "$(CTAGS_ARGS)$$unique" \
+ || $(CTAGS) $(CTAGSFLAGS) $(AM_CTAGSFLAGS) $(CTAGS_ARGS) \
+ $$unique
+
+GTAGS:
+ here=`$(am__cd) $(top_builddir) && pwd` \
+ && $(am__cd) $(top_srcdir) \
+ && gtags -i $(GTAGS_ARGS) "$$here"
+cscopelist: cscopelist-am
+
+cscopelist-am: $(am__tagged_files)
+ list='$(am__tagged_files)'; \
+ case "$(srcdir)" in \
+ [\\/]* | ?:[\\/]*) sdir="$(srcdir)" ;; \
+ *) sdir=$(subdir)/$(srcdir) ;; \
+ esac; \
+ for i in $$list; do \
+ if test -f "$$i"; then \
+ echo "$(subdir)/$$i"; \
+ else \
+ echo "$$sdir/$$i"; \
+ fi; \
+ done >> $(top_builddir)/cscope.files
+
+distclean-tags:
+ -rm -f TAGS ID GTAGS GRTAGS GSYMS GPATH tags
+
+distdir: $(BUILT_SOURCES)
+ $(MAKE) $(AM_MAKEFLAGS) distdir-am
+
+distdir-am: $(DISTFILES)
+ @srcdirstrip=`echo "$(srcdir)" | sed 's/[].[^$$\\*]/\\\\&/g'`; \
+ topsrcdirstrip=`echo "$(top_srcdir)" | sed 's/[].[^$$\\*]/\\\\&/g'`; \
+ list='$(DISTFILES)'; \
+ dist_files=`for file in $$list; do echo $$file; done | \
+ sed -e "s|^$$srcdirstrip/||;t" \
+ -e "s|^$$topsrcdirstrip/|$(top_builddir)/|;t"`; \
+ case $$dist_files in \
+ */*) $(MKDIR_P) `echo "$$dist_files" | \
+ sed '/\//!d;s|^|$(distdir)/|;s,/[^/]*$$,,' | \
+ sort -u` ;; \
+ esac; \
+ for file in $$dist_files; do \
+ if test -f $$file || test -d $$file; then d=.; else d=$(srcdir); fi; \
+ if test -d $$d/$$file; then \
+ dir=`echo "/$$file" | sed -e 's,/[^/]*$$,,'`; \
+ if test -d "$(distdir)/$$file"; then \
+ find "$(distdir)/$$file" -type d ! -perm -700 -exec chmod u+rwx {} \;; \
+ fi; \
+ if test -d $(srcdir)/$$file && test $$d != $(srcdir); then \
+ cp -fpR $(srcdir)/$$file "$(distdir)$$dir" || exit 1; \
+ find "$(distdir)/$$file" -type d ! -perm -700 -exec chmod u+rwx {} \;; \
+ fi; \
+ cp -fpR $$d/$$file "$(distdir)$$dir" || exit 1; \
+ else \
+ test -f "$(distdir)/$$file" \
+ || cp -p $$d/$$file "$(distdir)/$$file" \
+ || exit 1; \
+ fi; \
+ done
+check-am: all-am
+ $(MAKE) $(AM_MAKEFLAGS) check-local
+check: check-am
+all-am: Makefile $(PROGRAMS) $(LTLIBRARIES) $(HEADERS)
+installdirs:
+ for dir in "$(DESTDIR)$(pkginc_libdir)"; do \
+ test -z "$$dir" || $(MKDIR_P) "$$dir"; \
+ done
+install: install-am
+install-exec: install-exec-am
+install-data: install-data-am
+uninstall: uninstall-am
+
+install-am: all-am
+ @$(MAKE) $(AM_MAKEFLAGS) install-exec-am install-data-am
+
+installcheck: installcheck-am
+install-strip:
+ if test -z '$(STRIP)'; then \
+ $(MAKE) $(AM_MAKEFLAGS) INSTALL_PROGRAM="$(INSTALL_STRIP_PROGRAM)" \
+ install_sh_PROGRAM="$(INSTALL_STRIP_PROGRAM)" INSTALL_STRIP_FLAG=-s \
+ install; \
+ else \
+ $(MAKE) $(AM_MAKEFLAGS) INSTALL_PROGRAM="$(INSTALL_STRIP_PROGRAM)" \
+ install_sh_PROGRAM="$(INSTALL_STRIP_PROGRAM)" INSTALL_STRIP_FLAG=-s \
+ "INSTALL_PROGRAM_ENV=STRIPPROG='$(STRIP)'" install; \
+ fi
+mostlyclean-generic:
+
+clean-generic:
+
+distclean-generic:
+ -test -z "$(CONFIG_CLEAN_FILES)" || rm -f $(CONFIG_CLEAN_FILES)
+ -test . = "$(srcdir)" || test -z "$(CONFIG_CLEAN_VPATH_FILES)" || rm -f $(CONFIG_CLEAN_VPATH_FILES)
+
+maintainer-clean-generic:
+ @echo "This command is intended for maintainers to use"
+ @echo "it deletes files that may require special tools to rebuild."
+clean: clean-am
+
+clean-am: clean-generic clean-libtool clean-noinstLTLIBRARIES \
+ clean-noinstPROGRAMS mostlyclean-am
+
+distclean: distclean-am
+ -rm -f ./$(DEPDIR)/anvil-client.Plo
+ -rm -f ./$(DEPDIR)/ipc-client.Plo
+ -rm -f ./$(DEPDIR)/ipc-server.Plo
+ -rm -f ./$(DEPDIR)/master-auth.Plo
+ -rm -f ./$(DEPDIR)/master-instance.Plo
+ -rm -f ./$(DEPDIR)/master-login-auth.Plo
+ -rm -f ./$(DEPDIR)/master-login.Plo
+ -rm -f ./$(DEPDIR)/master-service-haproxy.Plo
+ -rm -f ./$(DEPDIR)/master-service-settings-cache.Plo
+ -rm -f ./$(DEPDIR)/master-service-settings.Plo
+ -rm -f ./$(DEPDIR)/master-service-ssl-settings.Plo
+ -rm -f ./$(DEPDIR)/master-service-ssl.Plo
+ -rm -f ./$(DEPDIR)/master-service.Plo
+ -rm -f ./$(DEPDIR)/stats-client.Plo
+ -rm -f ./$(DEPDIR)/syslog-util.Plo
+ -rm -f ./$(DEPDIR)/test-event-stats.Po
+ -rm -f ./$(DEPDIR)/test-master-service-settings-cache.Po
+ -rm -f Makefile
+distclean-am: clean-am distclean-compile distclean-generic \
+ distclean-tags
+
+dvi: dvi-am
+
+dvi-am:
+
+html: html-am
+
+html-am:
+
+info: info-am
+
+info-am:
+
+install-data-am: install-pkginc_libHEADERS
+
+install-dvi: install-dvi-am
+
+install-dvi-am:
+
+install-exec-am:
+
+install-html: install-html-am
+
+install-html-am:
+
+install-info: install-info-am
+
+install-info-am:
+
+install-man:
+
+install-pdf: install-pdf-am
+
+install-pdf-am:
+
+install-ps: install-ps-am
+
+install-ps-am:
+
+installcheck-am:
+
+maintainer-clean: maintainer-clean-am
+ -rm -f ./$(DEPDIR)/anvil-client.Plo
+ -rm -f ./$(DEPDIR)/ipc-client.Plo
+ -rm -f ./$(DEPDIR)/ipc-server.Plo
+ -rm -f ./$(DEPDIR)/master-auth.Plo
+ -rm -f ./$(DEPDIR)/master-instance.Plo
+ -rm -f ./$(DEPDIR)/master-login-auth.Plo
+ -rm -f ./$(DEPDIR)/master-login.Plo
+ -rm -f ./$(DEPDIR)/master-service-haproxy.Plo
+ -rm -f ./$(DEPDIR)/master-service-settings-cache.Plo
+ -rm -f ./$(DEPDIR)/master-service-settings.Plo
+ -rm -f ./$(DEPDIR)/master-service-ssl-settings.Plo
+ -rm -f ./$(DEPDIR)/master-service-ssl.Plo
+ -rm -f ./$(DEPDIR)/master-service.Plo
+ -rm -f ./$(DEPDIR)/stats-client.Plo
+ -rm -f ./$(DEPDIR)/syslog-util.Plo
+ -rm -f ./$(DEPDIR)/test-event-stats.Po
+ -rm -f ./$(DEPDIR)/test-master-service-settings-cache.Po
+ -rm -f Makefile
+maintainer-clean-am: distclean-am maintainer-clean-generic
+
+mostlyclean: mostlyclean-am
+
+mostlyclean-am: mostlyclean-compile mostlyclean-generic \
+ mostlyclean-libtool
+
+pdf: pdf-am
+
+pdf-am:
+
+ps: ps-am
+
+ps-am:
+
+uninstall-am: uninstall-pkginc_libHEADERS
+
+.MAKE: check-am install-am install-strip
+
+.PHONY: CTAGS GTAGS TAGS all all-am am--depfiles check check-am \
+ check-local clean clean-generic clean-libtool \
+ clean-noinstLTLIBRARIES clean-noinstPROGRAMS cscopelist-am \
+ ctags ctags-am distclean distclean-compile distclean-generic \
+ distclean-libtool distclean-tags distdir dvi dvi-am html \
+ html-am info info-am install install-am install-data \
+ install-data-am install-dvi install-dvi-am install-exec \
+ install-exec-am install-html install-html-am install-info \
+ install-info-am install-man install-pdf install-pdf-am \
+ install-pkginc_libHEADERS install-ps install-ps-am \
+ install-strip installcheck installcheck-am installdirs \
+ maintainer-clean maintainer-clean-generic mostlyclean \
+ mostlyclean-compile mostlyclean-generic mostlyclean-libtool \
+ pdf pdf-am ps ps-am tags tags-am uninstall uninstall-am \
+ uninstall-pkginc_libHEADERS
+
+.PRECIOUS: Makefile
+
+
+check-local:
+ for bin in $(test_programs); do \
+ if ! $(RUN_TEST) ./$$bin; then exit 1; fi; \
+ done
+
+# Tell versions [3.59,3.63) of GNU make to not export all variables.
+# Otherwise a system limit (for SysV at least) may be exceeded.
+.NOEXPORT:
diff --git a/src/lib-master/anvil-client.c b/src/lib-master/anvil-client.c
new file mode 100644
index 0000000..0cda77f
--- /dev/null
+++ b/src/lib-master/anvil-client.c
@@ -0,0 +1,275 @@
+/* Copyright (c) 2009-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "ioloop.h"
+#include "net.h"
+#include "istream.h"
+#include "ostream.h"
+#include "array.h"
+#include "aqueue.h"
+#include "anvil-client.h"
+
+struct anvil_query {
+ anvil_callback_t *callback;
+ void *context;
+};
+
+struct anvil_client {
+ char *path;
+ int fd;
+ struct istream *input;
+ struct ostream *output;
+ struct io *io;
+ struct timeout *to_query;
+
+ struct timeout *to_reconnect;
+ time_t last_reconnect;
+
+ ARRAY(struct anvil_query *) queries_arr;
+ struct aqueue *queries;
+
+ bool (*reconnect_callback)(void);
+ enum anvil_client_flags flags;
+};
+
+#define ANVIL_HANDSHAKE "VERSION\tanvil\t1\t0\n"
+#define ANVIL_INBUF_SIZE 1024
+#define ANVIL_RECONNECT_MIN_SECS 5
+#define ANVIL_QUERY_TIMEOUT_MSECS (1000*5)
+
+static void anvil_client_disconnect(struct anvil_client *client);
+
+struct anvil_client *
+anvil_client_init(const char *path, bool (*reconnect_callback)(void),
+ enum anvil_client_flags flags)
+{
+ struct anvil_client *client;
+
+ client = i_new(struct anvil_client, 1);
+ client->path = i_strdup(path);
+ client->reconnect_callback = reconnect_callback;
+ client->flags = flags;
+ client->fd = -1;
+ i_array_init(&client->queries_arr, 32);
+ client->queries = aqueue_init(&client->queries_arr.arr);
+ return client;
+}
+
+void anvil_client_deinit(struct anvil_client **_client)
+{
+ struct anvil_client *client = *_client;
+
+ *_client = NULL;
+
+ anvil_client_disconnect(client);
+ array_free(&client->queries_arr);
+ aqueue_deinit(&client->queries);
+ i_free(client->path);
+ i_assert(client->to_reconnect == NULL);
+ i_free(client);
+}
+
+static void anvil_reconnect(struct anvil_client *client)
+{
+ anvil_client_disconnect(client);
+ if (client->reconnect_callback != NULL) {
+ if (!client->reconnect_callback()) {
+ /* no reconnection */
+ return;
+ }
+ }
+
+ if (ioloop_time - client->last_reconnect < ANVIL_RECONNECT_MIN_SECS) {
+ if (client->to_reconnect == NULL) {
+ client->to_reconnect =
+ timeout_add(ANVIL_RECONNECT_MIN_SECS*1000,
+ anvil_reconnect, client);
+ }
+ } else {
+ client->last_reconnect = ioloop_time;
+ (void)anvil_client_connect(client, FALSE);
+ }
+}
+
+static void anvil_input(struct anvil_client *client)
+{
+ struct anvil_query *const *queries;
+ struct anvil_query *query;
+ const char *line;
+ unsigned int count;
+
+ queries = array_get(&client->queries_arr, &count);
+ while ((line = i_stream_read_next_line(client->input)) != NULL) {
+ if (aqueue_count(client->queries) == 0) {
+ i_error("anvil: Unexpected input: %s", line);
+ continue;
+ }
+
+ query = queries[aqueue_idx(client->queries, 0)];
+ if (query->callback != NULL) T_BEGIN {
+ query->callback(line, query->context);
+ } T_END;
+ i_free(query);
+ aqueue_delete_tail(client->queries);
+ }
+ if (client->input->stream_errno != 0) {
+ i_error("read(%s) failed: %s", client->path,
+ i_stream_get_error(client->input));
+ anvil_reconnect(client);
+ } else if (client->input->eof) {
+ i_error("read(%s) failed: EOF", client->path);
+ anvil_reconnect(client);
+ } else if (client->to_query != NULL) {
+ if (aqueue_count(client->queries) == 0)
+ timeout_remove(&client->to_query);
+ else
+ timeout_reset(client->to_query);
+ }
+}
+
+int anvil_client_connect(struct anvil_client *client, bool retry)
+{
+ int fd;
+
+ i_assert(client->fd == -1);
+
+ fd = retry ? net_connect_unix_with_retries(client->path, 5000) :
+ net_connect_unix(client->path);
+ if (fd == -1) {
+ if (errno != ENOENT ||
+ (client->flags & ANVIL_CLIENT_FLAG_HIDE_ENOENT) == 0) {
+ i_error("net_connect_unix(%s) failed: %m",
+ client->path);
+ }
+ return -1;
+ }
+
+ timeout_remove(&client->to_reconnect);
+
+ client->fd = fd;
+ client->input = i_stream_create_fd(fd, ANVIL_INBUF_SIZE);
+ client->output = o_stream_create_fd(fd, SIZE_MAX);
+ client->io = io_add(fd, IO_READ, anvil_input, client);
+ if (o_stream_send_str(client->output, ANVIL_HANDSHAKE) < 0) {
+ i_error("write(%s) failed: %s", client->path,
+ o_stream_get_error(client->output));
+ anvil_reconnect(client);
+ return -1;
+ }
+ return 0;
+}
+
+static void anvil_client_cancel_queries(struct anvil_client *client)
+{
+ struct anvil_query *const *queries, *query;
+ unsigned int count;
+
+ queries = array_get(&client->queries_arr, &count);
+ while (aqueue_count(client->queries) > 0) {
+ query = queries[aqueue_idx(client->queries, 0)];
+ if (query->callback != NULL)
+ query->callback(NULL, query->context);
+ i_free(query);
+ aqueue_delete_tail(client->queries);
+ }
+ timeout_remove(&client->to_query);
+}
+
+static void anvil_client_disconnect(struct anvil_client *client)
+{
+ anvil_client_cancel_queries(client);
+ if (client->fd != -1) {
+ io_remove(&client->io);
+ i_stream_destroy(&client->input);
+ o_stream_destroy(&client->output);
+ net_disconnect(client->fd);
+ client->fd = -1;
+ }
+ timeout_remove(&client->to_reconnect);
+}
+
+static void anvil_client_timeout(struct anvil_client *client)
+{
+ i_assert(aqueue_count(client->queries) > 0);
+
+ i_error("%s: Anvil queries timed out after %u secs - aborting queries",
+ client->path, ANVIL_QUERY_TIMEOUT_MSECS/1000);
+ /* perhaps reconnect helps */
+ anvil_reconnect(client);
+}
+
+static int anvil_client_send(struct anvil_client *client, const char *cmd)
+{
+ struct const_iovec iov[2];
+
+ if (client->fd == -1) {
+ if (anvil_client_connect(client, FALSE) < 0)
+ return -1;
+ }
+
+ iov[0].iov_base = cmd;
+ iov[0].iov_len = strlen(cmd);
+ iov[1].iov_base = "\n";
+ iov[1].iov_len = 1;
+ if (o_stream_sendv(client->output, iov, 2) < 0) {
+ i_error("write(%s) failed: %s", client->path,
+ o_stream_get_error(client->output));
+ anvil_reconnect(client);
+ return -1;
+ }
+ return 0;
+}
+
+struct anvil_query *
+anvil_client_query(struct anvil_client *client, const char *query,
+ anvil_callback_t *callback, void *context)
+{
+ struct anvil_query *anvil_query;
+
+ anvil_query = i_new(struct anvil_query, 1);
+ anvil_query->callback = callback;
+ anvil_query->context = context;
+ aqueue_append(client->queries, &anvil_query);
+ if (anvil_client_send(client, query) < 0) {
+ /* connection failure. add a delayed failure callback.
+ the caller may not expect the callback to be called
+ immediately. */
+ timeout_remove(&client->to_query);
+ client->to_query =
+ timeout_add_short(0, anvil_client_cancel_queries, client);
+ } else if (client->to_query == NULL) {
+ client->to_query = timeout_add(ANVIL_QUERY_TIMEOUT_MSECS,
+ anvil_client_timeout, client);
+ }
+ return anvil_query;
+}
+
+void anvil_client_query_abort(struct anvil_client *client,
+ struct anvil_query **_query)
+{
+ struct anvil_query *query = *_query;
+ struct anvil_query *const *queries;
+ unsigned int i, count;
+
+ *_query = NULL;
+
+ count = aqueue_count(client->queries);
+ queries = array_front(&client->queries_arr);
+ for (i = 0; i < count; i++) {
+ if (queries[aqueue_idx(client->queries, i)] == query) {
+ query->callback = NULL;
+ return;
+ }
+ }
+ i_panic("anvil query to be aborted doesn't exist");
+}
+
+void anvil_client_cmd(struct anvil_client *client, const char *cmd)
+{
+ (void)anvil_client_send(client, cmd);
+}
+
+bool anvil_client_is_connected(struct anvil_client *client)
+{
+ return client->fd != -1;
+}
diff --git a/src/lib-master/anvil-client.h b/src/lib-master/anvil-client.h
new file mode 100644
index 0000000..3433041
--- /dev/null
+++ b/src/lib-master/anvil-client.h
@@ -0,0 +1,37 @@
+#ifndef ANVIL_CLIENT_H
+#define ANVIL_CLIENT_H
+
+enum anvil_client_flags {
+ /* if connect() fails with ENOENT, hide the error */
+ ANVIL_CLIENT_FLAG_HIDE_ENOENT = 0x01
+};
+
+/* reply=NULL if query failed */
+typedef void anvil_callback_t(const char *reply, void *context);
+
+/* If reconnect_callback is specified, it's called when connection is lost.
+ If the callback returns FALSE, reconnection isn't attempted. */
+struct anvil_client *
+anvil_client_init(const char *path, bool (*reconnect_callback)(void),
+ enum anvil_client_flags flags) ATTR_NULL(2);
+void anvil_client_deinit(struct anvil_client **client);
+
+/* Connect to anvil. If retry=TRUE, try connecting for a while */
+int anvil_client_connect(struct anvil_client *client, bool retry);
+
+/* Send a query to anvil, expect a one line reply. The returned pointer can be
+ used to abort the query later. It becomes invalid when callback is
+ called (= the callback must not call it). Returns NULL if the query couldn't
+ be sent. */
+struct anvil_query *
+anvil_client_query(struct anvil_client *client, const char *query,
+ anvil_callback_t *callback, void *context);
+void anvil_client_query_abort(struct anvil_client *client,
+ struct anvil_query **query);
+/* Send a command to anvil, don't expect any replies. */
+void anvil_client_cmd(struct anvil_client *client, const char *cmd);
+
+/* Returns TRUE if anvil is connected to. */
+bool anvil_client_is_connected(struct anvil_client *client);
+
+#endif
diff --git a/src/lib-master/ipc-client.c b/src/lib-master/ipc-client.c
new file mode 100644
index 0000000..b734c1b
--- /dev/null
+++ b/src/lib-master/ipc-client.c
@@ -0,0 +1,220 @@
+/* Copyright (c) 2011-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "ioloop.h"
+#include "net.h"
+#include "llist.h"
+#include "istream.h"
+#include "ostream.h"
+#include "hostpid.h"
+#include "master-service.h"
+#include "ipc-client.h"
+
+#include <unistd.h>
+
+struct ipc_client_cmd {
+ struct ipc_client_cmd *prev, *next;
+
+ ipc_client_callback_t *callback;
+ void *context;
+};
+
+struct ipc_client {
+ char *path;
+ ipc_client_callback_t *callback;
+
+ int fd;
+ struct io *io;
+ struct timeout *to_failed;
+ struct istream *input;
+ struct ostream *output;
+ struct ipc_client_cmd *cmds_head, *cmds_tail;
+ unsigned int aborted_cmds_count;
+};
+
+static void ipc_client_disconnect(struct ipc_client *client);
+
+static void ipc_client_input_line(struct ipc_client *client, const char *line)
+{
+ struct ipc_client_cmd *cmd = client->cmds_head;
+ enum ipc_client_cmd_state state;
+ bool disconnect = FALSE;
+
+ if (client->aborted_cmds_count > 0) {
+ /* the command was already aborted */
+ cmd = NULL;
+ } else if (cmd == NULL) {
+ i_error("IPC proxy sent unexpected input: %s", line);
+ return;
+ }
+
+ switch (*line++) {
+ case ':':
+ state = IPC_CLIENT_CMD_STATE_REPLY;
+ break;
+ case '+':
+ state = IPC_CLIENT_CMD_STATE_OK;
+ break;
+ case '-':
+ state = IPC_CLIENT_CMD_STATE_ERROR;
+ break;
+ default:
+ i_error("IPC proxy sent invalid input: %s", line);
+ line = "Invalid input";
+ disconnect = TRUE;
+ state = IPC_CLIENT_CMD_STATE_ERROR;
+ break;
+ }
+
+ if (state != IPC_CLIENT_CMD_STATE_REPLY) {
+ if (cmd != NULL)
+ DLLIST2_REMOVE(&client->cmds_head,
+ &client->cmds_tail, cmd);
+ else
+ client->aborted_cmds_count--;
+ }
+ if (cmd != NULL)
+ cmd->callback(state, line, cmd->context);
+ if (state != IPC_CLIENT_CMD_STATE_REPLY)
+ i_free(cmd);
+ if (disconnect)
+ ipc_client_disconnect(client);
+}
+
+static void ipc_client_input(struct ipc_client *client)
+{
+ const char *line;
+
+ if (i_stream_read(client->input) < 0) {
+ ipc_client_disconnect(client);
+ return;
+ }
+ while ((line = i_stream_next_line(client->input)) != NULL) T_BEGIN {
+ ipc_client_input_line(client, line);
+ } T_END;
+}
+
+static int ipc_client_connect(struct ipc_client *client)
+{
+ if (client->fd != -1)
+ return 0;
+
+ client->fd = net_connect_unix(client->path);
+ if (client->fd == -1) {
+ i_error("connect(%s) failed: %m", client->path);
+ return -1;
+ }
+
+ client->io = io_add(client->fd, IO_READ, ipc_client_input, client);
+ client->input = i_stream_create_fd(client->fd, SIZE_MAX);
+ client->output = o_stream_create_fd(client->fd, SIZE_MAX);
+ o_stream_set_no_error_handling(client->output, TRUE);
+ return 0;
+}
+
+static void ipc_client_abort_commands(struct ipc_client *client,
+ const char *reason)
+{
+ struct ipc_client_cmd *cmd, *next;
+
+ cmd = client->cmds_head;
+ client->cmds_head = client->cmds_tail = NULL;
+ for (; cmd != NULL; cmd = next) {
+ cmd->callback(IPC_CLIENT_CMD_STATE_ERROR, reason, cmd->context);
+ next = cmd->next;
+ i_free(cmd);
+ }
+}
+
+static void ipc_client_disconnect(struct ipc_client *client)
+{
+ timeout_remove(&client->to_failed);
+ ipc_client_abort_commands(client, "Disconnected");
+
+ if (client->fd == -1)
+ return;
+
+ io_remove(&client->io);
+ i_stream_destroy(&client->input);
+ o_stream_destroy(&client->output);
+ if (close(client->fd) < 0)
+ i_error("close(%s) failed: %m", client->path);
+ client->fd = -1;
+}
+
+struct ipc_client *
+ipc_client_init(const char *ipc_socket_path)
+{
+ struct ipc_client *client;
+
+ client = i_new(struct ipc_client, 1);
+ client->path = i_strdup(ipc_socket_path);
+ client->fd = -1;
+ return client;
+}
+
+void ipc_client_deinit(struct ipc_client **_client)
+{
+ struct ipc_client *client = *_client;
+
+ *_client = NULL;
+
+ ipc_client_disconnect(client);
+ i_free(client->path);
+ i_free(client);
+}
+
+static void ipc_client_cmd_connect_failed(struct ipc_client *client)
+{
+ ipc_client_abort_commands(client, "ipc connect failed");
+ timeout_remove(&client->to_failed);
+}
+
+struct ipc_client_cmd *
+ipc_client_cmd(struct ipc_client *client, const char *cmd,
+ ipc_client_callback_t *callback, void *context)
+{
+ struct ipc_client_cmd *ipc_cmd;
+ struct const_iovec iov[2];
+
+ ipc_cmd = i_new(struct ipc_client_cmd, 1);
+ ipc_cmd->callback = callback;
+ ipc_cmd->context = context;
+ DLLIST2_APPEND(&client->cmds_head, &client->cmds_tail, ipc_cmd);
+
+ if (client->to_failed != NULL ||
+ ipc_client_connect(client) < 0) {
+ /* Delay calling the failure callback. Fail all commands until
+ the callback is called. */
+ if (client->to_failed == NULL) {
+ client->to_failed = timeout_add_short(0,
+ ipc_client_cmd_connect_failed, client);
+ }
+ } else {
+ iov[0].iov_base = cmd;
+ iov[0].iov_len = strlen(cmd);
+ iov[1].iov_base = "\n";
+ iov[1].iov_len = 1;
+ o_stream_nsendv(client->output, iov, N_ELEMENTS(iov));
+ }
+ return ipc_cmd;
+}
+
+void ipc_client_cmd_abort(struct ipc_client *client,
+ struct ipc_client_cmd **_cmd)
+{
+ struct ipc_client_cmd *cmd = *_cmd;
+
+ *_cmd = NULL;
+ cmd->callback = NULL;
+ /* Free the command only if it's the oldest. Free also other such
+ commands in case they were aborted earlier. */
+ while (client->cmds_head != NULL &&
+ client->cmds_head->callback == NULL) {
+ struct ipc_client_cmd *head = client->cmds_head;
+
+ client->aborted_cmds_count++;
+ DLLIST2_REMOVE(&client->cmds_head, &client->cmds_tail, head);
+ i_free(head);
+ }
+}
diff --git a/src/lib-master/ipc-client.h b/src/lib-master/ipc-client.h
new file mode 100644
index 0000000..99bce16
--- /dev/null
+++ b/src/lib-master/ipc-client.h
@@ -0,0 +1,24 @@
+#ifndef IPC_CLIENT_H
+#define IPC_CLIENT_H
+
+enum ipc_client_cmd_state {
+ IPC_CLIENT_CMD_STATE_REPLY,
+ IPC_CLIENT_CMD_STATE_OK,
+ IPC_CLIENT_CMD_STATE_ERROR
+};
+
+typedef void ipc_client_callback_t(enum ipc_client_cmd_state state,
+ const char *data, void *context);
+
+struct ipc_client *
+ipc_client_init(const char *ipc_socket_path);
+void ipc_client_deinit(struct ipc_client **client);
+
+struct ipc_client_cmd *
+ipc_client_cmd(struct ipc_client *client, const char *cmd,
+ ipc_client_callback_t *callback, void *context)
+ ATTR_NULL(4);
+void ipc_client_cmd_abort(struct ipc_client *client,
+ struct ipc_client_cmd **cmd);
+
+#endif
diff --git a/src/lib-master/ipc-server.c b/src/lib-master/ipc-server.c
new file mode 100644
index 0000000..8bd8c23
--- /dev/null
+++ b/src/lib-master/ipc-server.c
@@ -0,0 +1,202 @@
+/* Copyright (c) 2011-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "ioloop.h"
+#include "net.h"
+#include "istream.h"
+#include "ostream.h"
+#include "hostpid.h"
+#include "master-service.h"
+#include "ipc-server.h"
+
+#include <unistd.h>
+
+#define IPC_SERVER_RECONNECT_MSECS (10*1000)
+#define IPC_SERVER_PROTOCOL_MAJOR_VERSION 1
+#define IPC_SERVER_PROTOCOL_MINOR_VERSION 0
+#define IPC_SERVER_HANDSHAKE "VERSION\tipc-server\t1\t0\nHANDSHAKE\t%s\t%s\n"
+
+struct ipc_cmd {
+ struct ipc_server *server;
+ unsigned int tag;
+};
+
+struct ipc_server {
+ char *name, *path;
+ ipc_command_callback_t *callback;
+
+ int ipc_cmd_refcount;
+
+ int fd;
+ struct io *io;
+ struct timeout *to;
+ struct istream *input;
+ struct ostream *output;
+
+ bool version_received:1;
+};
+
+static void ipc_server_disconnect(struct ipc_server *server);
+static void ipc_server_connect(struct ipc_server *server);
+
+static void ipc_server_input_line(struct ipc_server *server, char *line)
+{
+ struct ipc_cmd *cmd;
+ unsigned int tag = 0;
+ char *p;
+
+ /* tag cmd */
+ p = strchr(line, '\t');
+ if (p != NULL) {
+ *p++ = '\0';
+ if (str_to_uint(line, &tag) < 0)
+ p = NULL;
+ }
+ if (p == NULL || *p == '\0') {
+ i_error("IPC proxy sent invalid input: %s", line);
+ return;
+ }
+
+ cmd = i_new(struct ipc_cmd, 1);
+ cmd->server = server;
+ cmd->tag = tag;
+
+ server->ipc_cmd_refcount++;
+ T_BEGIN {
+ server->callback(cmd, p);
+ } T_END;
+}
+
+static void ipc_server_input(struct ipc_server *server)
+{
+ char *line;
+
+ if (i_stream_read(server->input) < 0) {
+ ipc_server_disconnect(server);
+ ipc_server_connect(server);
+ return;
+ }
+
+ if (!server->version_received) {
+ if ((line = i_stream_next_line(server->input)) == NULL)
+ return;
+
+ if (!version_string_verify(line, "ipc-proxy",
+ IPC_SERVER_PROTOCOL_MAJOR_VERSION)) {
+ i_error("IPC proxy not compatible with this server "
+ "(mixed old and new binaries?)");
+ ipc_server_disconnect(server);
+ return;
+ }
+ server->version_received = TRUE;
+ }
+
+ while ((line = i_stream_next_line(server->input)) != NULL)
+ ipc_server_input_line(server, line);
+}
+
+static void ipc_server_connect(struct ipc_server *server)
+{
+ i_assert(server->fd == -1);
+
+ timeout_remove(&server->to);
+
+ server->fd = net_connect_unix(server->path);
+ if (server->fd == -1) {
+ i_error("connect(%s) failed: %m", server->path);
+ server->to = timeout_add(IPC_SERVER_RECONNECT_MSECS,
+ ipc_server_connect, server);
+ return;
+ }
+
+ server->io = io_add(server->fd, IO_READ, ipc_server_input, server);
+ server->input = i_stream_create_fd(server->fd, SIZE_MAX);
+ server->output = o_stream_create_fd(server->fd, SIZE_MAX);
+ o_stream_set_no_error_handling(server->output, TRUE);
+ o_stream_nsend_str(server->output,
+ t_strdup_printf(IPC_SERVER_HANDSHAKE, server->name, my_pid));
+ o_stream_cork(server->output);
+}
+
+static void ipc_server_disconnect(struct ipc_server *server)
+{
+ if (server->fd == -1)
+ return;
+
+ io_remove(&server->io);
+ i_stream_destroy(&server->input);
+ o_stream_destroy(&server->output);
+ if (close(server->fd) < 0)
+ i_error("close(%s) failed: %m", server->path);
+ server->fd = -1;
+}
+
+struct ipc_server *
+ipc_server_init(const char *ipc_socket_path, const char *name,
+ ipc_command_callback_t *callback)
+{
+ struct ipc_server *server;
+
+ server = i_new(struct ipc_server, 1);
+ server->name = i_strdup(name);
+ server->path = i_strdup(ipc_socket_path);
+ server->callback = callback;
+ server->fd = -1;
+ ipc_server_connect(server);
+ return server;
+}
+
+void ipc_server_deinit(struct ipc_server **_server)
+{
+ struct ipc_server *server = *_server;
+
+ *_server = NULL;
+
+ i_assert(server->ipc_cmd_refcount == 0);
+
+ ipc_server_disconnect(server);
+ timeout_remove(&server->to);
+ i_free(server->name);
+ i_free(server->path);
+ i_free(server);
+}
+
+void ipc_cmd_send(struct ipc_cmd *cmd, const char *data)
+{
+ o_stream_nsend_str(cmd->server->output,
+ t_strdup_printf("%u\t:%s\n", cmd->tag, data));
+}
+
+static void ipc_cmd_finish(struct ipc_cmd *cmd, const char *line)
+{
+ o_stream_nsend_str(cmd->server->output,
+ t_strdup_printf("%u\t%s\n", cmd->tag, line));
+ o_stream_uncork(cmd->server->output);
+
+ i_assert(cmd->server->ipc_cmd_refcount > 0);
+ cmd->server->ipc_cmd_refcount--;
+ i_free(cmd);
+}
+
+void ipc_cmd_success(struct ipc_cmd **_cmd)
+{
+ ipc_cmd_success_reply(_cmd, "");
+}
+
+void ipc_cmd_success_reply(struct ipc_cmd **_cmd, const char *data)
+{
+ struct ipc_cmd *cmd = *_cmd;
+
+ *_cmd = NULL;
+ ipc_cmd_finish(cmd, t_strconcat("+", data, NULL));
+}
+
+void ipc_cmd_fail(struct ipc_cmd **_cmd, const char *errormsg)
+{
+ struct ipc_cmd *cmd = *_cmd;
+
+ i_assert(errormsg != NULL);
+
+ *_cmd = NULL;
+ ipc_cmd_finish(cmd, t_strconcat("-", errormsg, NULL));
+}
diff --git a/src/lib-master/ipc-server.h b/src/lib-master/ipc-server.h
new file mode 100644
index 0000000..e9e2736
--- /dev/null
+++ b/src/lib-master/ipc-server.h
@@ -0,0 +1,20 @@
+#ifndef IPC_SERVER_H
+#define IPC_SERVER_H
+
+struct ipc_cmd;
+
+/* The callback must eventually free the cmd by calling ip_cmd_success/fail().
+ line is guaranteed to be non-empty. */
+typedef void ipc_command_callback_t(struct ipc_cmd *cmd, const char *line);
+
+struct ipc_server *
+ipc_server_init(const char *ipc_socket_path, const char *name,
+ ipc_command_callback_t *callback);
+void ipc_server_deinit(struct ipc_server **server);
+
+void ipc_cmd_send(struct ipc_cmd *cmd, const char *data);
+void ipc_cmd_success(struct ipc_cmd **cmd);
+void ipc_cmd_success_reply(struct ipc_cmd **cmd, const char *data);
+void ipc_cmd_fail(struct ipc_cmd **cmd, const char *errormsg);
+
+#endif
diff --git a/src/lib-master/master-auth.c b/src/lib-master/master-auth.c
new file mode 100644
index 0000000..36781c4
--- /dev/null
+++ b/src/lib-master/master-auth.c
@@ -0,0 +1,286 @@
+/* Copyright (c) 2005-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "ioloop.h"
+#include "fdpass.h"
+#include "buffer.h"
+#include "hash.h"
+#include "time-util.h"
+#include "master-service-private.h"
+#include "master-auth.h"
+
+#include <unistd.h>
+#include <sys/stat.h>
+
+#define SOCKET_CONNECT_RETRY_MSECS 500
+#define SOCKET_CONNECT_RETRY_WARNING_INTERVAL_SECS 2
+#define MASTER_AUTH_REQUEST_TIMEOUT_MSECS (MASTER_LOGIN_TIMEOUT_SECS/2*1000)
+
+struct master_auth_connection {
+ struct master_auth *auth;
+ unsigned int tag;
+
+ unsigned int client_pid, auth_id;
+ struct ip_addr remote_ip;
+ struct timeval create_time;
+
+ char *path;
+ int fd;
+ struct io *io;
+ struct timeout *to;
+
+ char buf[sizeof(struct master_auth_reply)];
+ unsigned int buf_pos;
+
+ master_auth_callback_t *callback;
+ void *context;
+};
+
+struct master_auth {
+ struct master_service *service;
+ pool_t pool;
+
+ const char *default_path;
+ time_t last_connect_warning;
+
+ unsigned int tag_counter;
+ HASH_TABLE(void *, struct master_auth_connection *) connections;
+};
+
+struct master_auth *
+master_auth_init(struct master_service *service, const char *path)
+{
+ struct master_auth *auth;
+ pool_t pool;
+
+ pool = pool_alloconly_create("master auth", 1024);
+ auth = p_new(pool, struct master_auth, 1);
+ auth->pool = pool;
+ auth->service = service;
+ auth->default_path = p_strdup(pool, path);
+ hash_table_create_direct(&auth->connections, pool, 0);
+ return auth;
+}
+
+static void
+master_auth_connection_deinit(struct master_auth_connection **_conn)
+{
+ struct master_auth_connection *conn = *_conn;
+
+ *_conn = NULL;
+
+ if (conn->tag != 0)
+ hash_table_remove(conn->auth->connections,
+ POINTER_CAST(conn->tag));
+
+ if (conn->callback != NULL)
+ conn->callback(NULL, conn->context);
+
+ timeout_remove(&conn->to);
+ io_remove(&conn->io);
+ if (conn->fd != -1) {
+ if (close(conn->fd) < 0)
+ i_fatal("close(%s) failed: %m", conn->path);
+ conn->fd = -1;
+ }
+ i_free(conn->path);
+ i_free(conn);
+}
+
+static void ATTR_FORMAT(2, 3)
+conn_error(struct master_auth_connection *conn, const char *fmt, ...)
+{
+ va_list args;
+
+ va_start(args, fmt);
+ i_error("master(%s): %s (client-pid=%u, client-id=%u, rip=%s, created %u msecs ago, received %u/%zu bytes)",
+ conn->path, t_strdup_vprintf(fmt, args),
+ conn->client_pid, conn->auth_id, net_ip2addr(&conn->remote_ip),
+ timeval_diff_msecs(&ioloop_timeval, &conn->create_time),
+ conn->buf_pos, sizeof(conn->buf_pos));
+ va_end(args);
+}
+
+void master_auth_deinit(struct master_auth **_auth)
+{
+ struct master_auth *auth = *_auth;
+ struct hash_iterate_context *iter;
+ void *key;
+ struct master_auth_connection *conn;
+
+ *_auth = NULL;
+
+ iter = hash_table_iterate_init(auth->connections);
+ while (hash_table_iterate(iter, auth->connections, &key, &conn)) {
+ conn->tag = 0;
+ master_auth_connection_deinit(&conn);
+ }
+ hash_table_iterate_deinit(&iter);
+ hash_table_destroy(&auth->connections);
+ pool_unref(&auth->pool);
+}
+
+static void master_auth_connection_input(struct master_auth_connection *conn)
+{
+ const struct master_auth_reply *reply;
+ int ret;
+
+ ret = read(conn->fd, conn->buf + conn->buf_pos,
+ sizeof(conn->buf) - conn->buf_pos);
+ if (ret <= 0) {
+ if (ret == 0 || errno == ECONNRESET) {
+ conn_error(conn, "read() failed: Remote closed connection "
+ "(destination service { process_limit } reached?)");
+ } else {
+ if (errno == EAGAIN)
+ return;
+ conn_error(conn, "read() failed: %m");
+ }
+ master_auth_connection_deinit(&conn);
+ return;
+ }
+
+ conn->buf_pos += ret;
+ if (conn->buf_pos < sizeof(conn->buf))
+ return;
+
+ /* reply is now read */
+ reply = (const void *)conn->buf;
+ conn->buf_pos = 0;
+
+ if (conn->tag != reply->tag)
+ conn_error(conn, "Received reply with unknown tag %u", reply->tag);
+ else if (conn->callback == NULL) {
+ /* request aborted */
+ } else {
+ conn->callback(reply, conn->context);
+ conn->callback = NULL;
+ }
+ master_auth_connection_deinit(&conn);
+}
+
+static void master_auth_connection_timeout(struct master_auth_connection *conn)
+{
+ conn_error(conn, "Auth request timed out");
+ master_auth_connection_deinit(&conn);
+}
+
+void master_auth_request_full(struct master_auth *auth,
+ const struct master_auth_request_params *params,
+ master_auth_callback_t *callback, void *context,
+ unsigned int *tag_r)
+{
+ struct master_auth_connection *conn;
+ struct master_auth_request req;
+ buffer_t *buf;
+ struct stat st;
+ ssize_t ret;
+
+ i_assert(params->request.client_pid != 0);
+ i_assert(params->request.auth_pid != 0);
+
+ conn = i_new(struct master_auth_connection, 1);
+ conn->auth = auth;
+ conn->create_time = ioloop_timeval;
+ conn->callback = callback;
+ conn->context = context;
+ conn->path = params->socket_path != NULL ?
+ i_strdup(params->socket_path) : i_strdup(auth->default_path);
+
+ req = params->request;
+ req.tag = ++auth->tag_counter;
+ if (req.tag == 0)
+ req.tag = ++auth->tag_counter;
+
+ conn->client_pid = req.client_pid;
+ conn->auth_id = req.auth_id;
+ conn->remote_ip = req.remote_ip;
+
+ if (fstat(params->client_fd, &st) < 0)
+ i_fatal("fstat(auth dest fd) failed: %m");
+ req.ino = st.st_ino;
+
+ buf = t_buffer_create(sizeof(req) + req.data_size);
+ buffer_append(buf, &req, sizeof(req));
+ buffer_append(buf, params->data, req.data_size);
+
+ conn->fd = net_connect_unix(conn->path);
+ if (conn->fd == -1 && errno == EAGAIN) {
+ /* Couldn't connect to the socket immediately. This will add
+ a delay that causes hangs to the whole process, which won't
+ be obvious unless we log a warning. FIXME: The wait could
+ be asynchronous. */
+ struct timeval start_time;
+
+ io_loop_time_refresh();
+ start_time = ioloop_timeval;
+ conn->fd = net_connect_unix_with_retries(conn->path,
+ SOCKET_CONNECT_RETRY_MSECS);
+ io_loop_time_refresh();
+ if (conn->fd != -1 &&
+ ioloop_time - auth->last_connect_warning >=
+ SOCKET_CONNECT_RETRY_WARNING_INTERVAL_SECS) {
+ i_warning("net_connect_unix(%s) succeeded only after retrying - "
+ "took %lld us", conn->path,
+ timeval_diff_usecs(&ioloop_timeval, &start_time));
+ auth->last_connect_warning = ioloop_time;
+ }
+ }
+ if (conn->fd == -1) {
+ conn_error(conn, "net_connect_unix(%s) failed: %m%s",
+ conn->path, errno != EAGAIN ? "" :
+ " - http://wiki2.dovecot.org/SocketUnavailable");
+ master_auth_connection_deinit(&conn);
+ return;
+ }
+
+ ret = fd_send(conn->fd, params->client_fd, buf->data, buf->used);
+ if (ret < 0) {
+ conn_error(conn, "fd_send(fd=%d) failed: %m",
+ params->client_fd);
+ } else if ((size_t)ret != buf->used) {
+ conn_error(conn, "fd_send() sent only %d of %d bytes",
+ (int)ret, (int)buf->used);
+ ret = -1;
+ }
+ if (ret < 0) {
+ master_auth_connection_deinit(&conn);
+ return;
+ }
+
+ conn->tag = req.tag;
+ conn->to = timeout_add(MASTER_AUTH_REQUEST_TIMEOUT_MSECS,
+ master_auth_connection_timeout, conn);
+ conn->io = io_add(conn->fd, IO_READ,
+ master_auth_connection_input, conn);
+ i_assert(hash_table_lookup(auth->connections, POINTER_CAST(req.tag)) == NULL);
+ hash_table_insert(auth->connections, POINTER_CAST(req.tag), conn);
+ *tag_r = req.tag;
+}
+
+void master_auth_request(struct master_auth *auth, int fd,
+ const struct master_auth_request *request,
+ const unsigned char *data,
+ master_auth_callback_t *callback,
+ void *context, unsigned int *tag_r)
+{
+ struct master_auth_request_params params;
+
+ i_zero(&params);
+ params.client_fd = fd;
+ params.request = *request;
+ params.data = data;
+
+ master_auth_request_full(auth, &params, callback, context, tag_r);
+}
+
+void master_auth_request_abort(struct master_auth *auth, unsigned int tag)
+{
+ struct master_auth_connection *conn;
+
+ conn = hash_table_lookup(auth->connections, POINTER_CAST(tag));
+ if (conn == NULL)
+ i_panic("master_auth_request_abort(): tag %u not found", tag);
+
+ conn->callback = NULL;
+}
diff --git a/src/lib-master/master-auth.h b/src/lib-master/master-auth.h
new file mode 100644
index 0000000..8e0db74
--- /dev/null
+++ b/src/lib-master/master-auth.h
@@ -0,0 +1,112 @@
+#ifndef MASTER_AUTH_H
+#define MASTER_AUTH_H
+
+#include "net.h"
+
+struct master_service;
+
+/* Major version changes are not backwards compatible,
+ minor version numbers can be ignored. */
+#define AUTH_MASTER_PROTOCOL_MAJOR_VERSION 1
+#define AUTH_MASTER_PROTOCOL_MINOR_VERSION 1
+
+/* Authentication client process's cookie size */
+#define MASTER_AUTH_COOKIE_SIZE (128/8)
+
+/* LOGIN_MAX_INBUF_SIZE should be based on this. Keep this large enough so that
+ LOGIN_MAX_INBUF_SIZE will be 1024+2 bytes. This is because IMAP ID command's
+ values may be max. 1024 bytes plus 2 for "" quotes. (Although it could be
+ even double of that when value is full of \" quotes, but for now lets not
+ make it too easy to waste memory..) */
+#define MASTER_AUTH_MAX_DATA_SIZE (1024 + 128 + 64 + 2)
+
+#define MASTER_AUTH_ERRMSG_INTERNAL_FAILURE \
+ "Internal error occurred. Refer to server log for more information."
+
+enum mail_auth_request_flags {
+ /* Connection has TLS compression enabled */
+ MAIL_AUTH_REQUEST_FLAG_TLS_COMPRESSION = BIT(0),
+ /* Connection is secure (SSL or just trusted) */
+ MAIL_AUTH_REQUEST_FLAG_CONN_SECURED = BIT(1),
+ /* Connection is secured using SSL specifically */
+ MAIL_AUTH_REQUEST_FLAG_CONN_SSL_SECURED = BIT(2),
+ /* This login is implicit; no command reply is expected */
+ MAIL_AUTH_REQUEST_FLAG_IMPLICIT = BIT(3),
+};
+
+/* Authentication request. File descriptor may be sent along with the
+ request. */
+struct master_auth_request {
+ /* Request tag. Reply is sent back using same tag. */
+ unsigned int tag;
+
+ /* Authentication process, authentication ID and auth cookie. */
+ pid_t auth_pid;
+ unsigned int auth_id;
+ unsigned int client_pid;
+ uint8_t cookie[MASTER_AUTH_COOKIE_SIZE];
+
+ /* Properties of the connection. The file descriptor
+ itself may be a local socketpair. */
+ struct ip_addr local_ip, remote_ip;
+ in_port_t local_port, remote_port;
+
+ uint32_t flags;
+
+ /* request follows this many bytes of client input */
+ uint32_t data_size;
+ /* inode of the transferred fd. verified just to be sure that the
+ correct fd is mapped to the correct struct. */
+ ino_t ino;
+};
+
+enum master_auth_status {
+ MASTER_AUTH_STATUS_OK,
+ MASTER_AUTH_STATUS_INTERNAL_ERROR
+};
+
+struct master_auth_reply {
+ /* tag=0 are notifications from master */
+ unsigned int tag;
+ enum master_auth_status status;
+ /* PID of the post-login mail process handling this connection */
+ pid_t mail_pid;
+};
+
+struct master_auth_request_params {
+ /* Client fd to transfer to post-login process or -1 if no fd is
+ wanted to be transferred. */
+ int client_fd;
+ /* Override master_auth->default_path if non-NULL */
+ const char *socket_path;
+
+ /* Authentication request that is sent to post-login process.
+ tag is ignored. */
+ struct master_auth_request request;
+ /* Client input of size request.data_size */
+ const unsigned char *data;
+};
+
+/* reply=NULL if the auth lookup was cancelled due to some error */
+typedef void master_auth_callback_t(const struct master_auth_reply *reply,
+ void *context);
+
+struct master_auth *
+master_auth_init(struct master_service *service, const char *path);
+void master_auth_deinit(struct master_auth **auth);
+
+/* Send an authentication request. Returns tag which can be used to abort the
+ request (ie. ignore the reply from master). */
+void master_auth_request_full(struct master_auth *auth,
+ const struct master_auth_request_params *params,
+ master_auth_callback_t *callback, void *context,
+ unsigned int *tag_r);
+/* For backwards compatibility: */
+void master_auth_request(struct master_auth *auth, int fd,
+ const struct master_auth_request *request,
+ const unsigned char *data,
+ master_auth_callback_t *callback,
+ void *context, unsigned int *tag_r);
+void master_auth_request_abort(struct master_auth *auth, unsigned int tag);
+
+#endif
diff --git a/src/lib-master/master-instance.c b/src/lib-master/master-instance.c
new file mode 100644
index 0000000..86fb1c1
--- /dev/null
+++ b/src/lib-master/master-instance.c
@@ -0,0 +1,369 @@
+/* Copyright (c) 2013-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "path-util.h"
+#include "array.h"
+#include "istream.h"
+#include "ostream.h"
+#include "file-dotlock.h"
+#include "str.h"
+#include "strescape.h"
+#include "master-instance.h"
+
+#include <unistd.h>
+#include <fcntl.h>
+
+struct master_instance_list {
+ pool_t pool;
+ const char *path;
+
+ ARRAY(struct master_instance) instances;
+
+ bool locked:1;
+ bool config_paths_changed:1;
+};
+
+struct master_instance_list_iter {
+ struct master_instance_list *list;
+ unsigned int idx;
+};
+
+static const struct dotlock_settings dotlock_set = {
+ .timeout = 10,
+ .stale_timeout = 60,
+ .use_io_notify = TRUE,
+};
+
+struct master_instance_list *master_instance_list_init(const char *path)
+{
+ struct master_instance_list *list;
+ pool_t pool;
+
+ pool = pool_alloconly_create(MEMPOOL_GROWING"master instances", 256);
+ list = p_new(pool, struct master_instance_list, 1);
+ list->pool = pool;
+ list->path = p_strdup(pool, path);
+ p_array_init(&list->instances, pool, 8);
+ return list;
+}
+
+void master_instance_list_deinit(struct master_instance_list **_list)
+{
+ struct master_instance_list *list = *_list;
+
+ *_list = NULL;
+ pool_unref(&list->pool);
+}
+
+static void
+master_instance_update_config_path(struct master_instance_list *list,
+ struct master_instance *inst)
+{
+ const char *path, *config_path, *error;
+
+ /* update instance's config path if it has changed */
+ path = t_strconcat(inst->base_dir, "/"PACKAGE".conf", NULL);
+ if (t_readlink(path, &config_path, &error) < 0) {
+ /* The link may not exist, ignore the error. */
+ if (errno != ENOENT)
+ i_error("t_readlink(%s) failed: %s", path, error);
+ return;
+ }
+ if (null_strcmp(inst->config_path, config_path) != 0) {
+ inst->config_path = p_strdup(list->pool, config_path);
+ list->config_paths_changed = TRUE;
+ }
+}
+
+static int
+master_instance_list_add_line(struct master_instance_list *list,
+ const char *line)
+{
+ struct master_instance *inst;
+ const char *const *args;
+ time_t last_used;
+
+ /* <last used> <name> <base dir> [<config path>] */
+ args = t_strsplit_tabescaped(line);
+ if (str_array_length(args) < 3)
+ return -1;
+ if (str_to_time(args[0], &last_used) < 0)
+ return -1;
+
+ inst = array_append_space(&list->instances);
+ inst->last_used = last_used;
+ inst->name = p_strdup(list->pool, args[1]);
+ inst->base_dir = p_strdup(list->pool, args[2]);
+ inst->config_path = p_strdup_empty(list->pool, args[3]);
+ master_instance_update_config_path(list, inst);
+ return 0;
+}
+
+static int master_instance_list_refresh(struct master_instance_list *list)
+{
+ struct istream *input;
+ const char *line;
+ int fd, ret = 0;
+
+ array_clear(&list->instances);
+
+ fd = open(list->path, O_RDONLY);
+ if (fd == -1) {
+ if (errno == ENOENT)
+ return 0;
+
+ i_error("open(%s) failed: %m", list->path);
+ return -1;
+ }
+ input = i_stream_create_fd_autoclose(&fd, SIZE_MAX);
+ while ((line = i_stream_read_next_line(input)) != NULL) T_BEGIN {
+ if (master_instance_list_add_line(list, line) < 0)
+ i_error("Invalid line in %s: %s", list->path, line);
+ } T_END;
+ if (input->stream_errno != 0) {
+ i_error("read(%s) failed: %s", list->path,
+ i_stream_get_error(input));
+ ret = -1;
+ }
+ i_stream_destroy(&input);
+ return ret;
+}
+
+static int
+master_instance_list_write(struct master_instance_list *list,
+ int fd, const char *path)
+{
+ struct ostream *output;
+ const struct master_instance *inst;
+ string_t *str = t_str_new(128);
+ int ret = 0;
+
+ output = o_stream_create_fd(fd, 0);
+ o_stream_cork(output);
+ array_foreach(&list->instances, inst) {
+ str_truncate(str, 0);
+ str_printfa(str, "%ld\t", (long)inst->last_used);
+ str_append_tabescaped(str, inst->name);
+ str_append_c(str, '\t');
+ str_append_tabescaped(str, inst->base_dir);
+ str_append_c(str, '\t');
+ if (inst->config_path != NULL)
+ str_append_tabescaped(str, inst->config_path);
+ str_append_c(str, '\n');
+ o_stream_nsend(output, str_data(str), str_len(str));
+ }
+ if (o_stream_finish(output) < 0) {
+ i_error("write(%s) failed: %s", path, o_stream_get_error(output));
+ ret = -1;
+ }
+ o_stream_destroy(&output);
+ return ret;
+}
+
+static int master_instance_write_init(struct master_instance_list *list,
+ struct dotlock **dotlock_r)
+{
+ int fd;
+
+ i_assert(!list->locked);
+
+ *dotlock_r = NULL;
+
+ fd = file_dotlock_open_mode(&dotlock_set, list->path, 0, 0644,
+ (uid_t)-1, (gid_t)-1, dotlock_r);
+ if (fd == -1) {
+ i_error("file_dotlock_open(%s) failed: %m", list->path);
+ return -1;
+ }
+ if (master_instance_list_refresh(list) < 0) {
+ file_dotlock_delete(dotlock_r);
+ return -1;
+ }
+ list->locked = TRUE;
+ return fd;
+}
+
+static int master_instance_write_finish(struct master_instance_list *list,
+ int fd, struct dotlock **dotlock)
+{
+ const char *lock_path = file_dotlock_get_lock_path(*dotlock);
+ int ret;
+
+ i_assert(list->locked);
+
+ T_BEGIN {
+ ret = master_instance_list_write(list, fd, lock_path);
+ } T_END;
+
+ list->locked = FALSE;
+ if (ret < 0) {
+ file_dotlock_delete(dotlock);
+ return -1;
+ }
+ if (fdatasync(fd) < 0) {
+ i_error("fdatasync(%s) failed: %m", lock_path);
+ file_dotlock_delete(dotlock);
+ return -1;
+ }
+ list->config_paths_changed = FALSE;
+ return file_dotlock_replace(dotlock, 0);
+}
+
+static struct master_instance *
+master_instance_find(struct master_instance_list *list,
+ const char *base_dir)
+{
+ struct master_instance *inst;
+
+ array_foreach_modifiable(&list->instances, inst) {
+ if (strcmp(inst->base_dir, base_dir) == 0)
+ return inst;
+ }
+ return NULL;
+}
+
+int master_instance_list_update(struct master_instance_list *list,
+ const char *base_dir)
+{
+ struct master_instance *inst;
+ struct dotlock *dotlock;
+ int fd;
+
+ if ((fd = master_instance_write_init(list, &dotlock)) == -1)
+ return -1;
+
+ inst = master_instance_find(list, base_dir);
+ if (inst == NULL) {
+ inst = array_append_space(&list->instances);
+ inst->name = "";
+ inst->base_dir = p_strdup(list->pool, base_dir);
+ }
+ inst->last_used = time(NULL);
+ master_instance_update_config_path(list, inst);
+
+ return master_instance_write_finish(list, fd, &dotlock);
+}
+
+int master_instance_list_set_name(struct master_instance_list *list,
+ const char *base_dir, const char *name)
+{
+ const struct master_instance *orig_inst;
+ struct master_instance *inst;
+ struct dotlock *dotlock;
+ int fd;
+
+ i_assert(*name != '\0');
+
+ if ((fd = master_instance_write_init(list, &dotlock)) == -1)
+ return -1;
+
+ orig_inst = master_instance_list_find_by_name(list, name);
+ if (orig_inst != NULL &&
+ strcmp(orig_inst->base_dir, base_dir) != 0) {
+ /* name already used */
+ file_dotlock_delete(&dotlock);
+ list->locked = FALSE;
+ return 0;
+ }
+
+ inst = master_instance_find(list, base_dir);
+ if (inst == NULL) {
+ inst = array_append_space(&list->instances);
+ inst->base_dir = p_strdup(list->pool, base_dir);
+ }
+ inst->name = p_strdup(list->pool, name);
+ inst->last_used = time(NULL);
+
+ return master_instance_write_finish(list, fd, &dotlock) < 0 ? -1 : 1;
+}
+
+int master_instance_list_remove(struct master_instance_list *list,
+ const char *base_dir)
+{
+ struct dotlock *dotlock;
+ const struct master_instance *instances;
+ unsigned int i, count;
+ int fd;
+
+ if ((fd = master_instance_write_init(list, &dotlock)) == -1)
+ return -1;
+
+ instances = array_get(&list->instances, &count);
+ for (i = 0; i < count; i++) {
+ if (strcmp(instances[i].base_dir, base_dir) == 0) {
+ array_delete(&list->instances, i, 1);
+ break;
+ }
+ }
+
+ if (i == count) {
+ file_dotlock_delete(&dotlock);
+ list->locked = FALSE;
+ return 0;
+ }
+ return master_instance_write_finish(list, fd, &dotlock) < 0 ? -1 : 1;
+}
+
+static int
+master_instance_list_refresh_and_update(struct master_instance_list *list)
+{
+ struct dotlock *dotlock;
+ int fd;
+
+ if (master_instance_list_refresh(list) < 0)
+ return -1;
+ if (list->config_paths_changed && !list->locked) {
+ /* write new config paths */
+ if ((fd = master_instance_write_init(list, &dotlock)) == -1)
+ return -1;
+ if (master_instance_write_finish(list, fd, &dotlock) < 0)
+ return -1;
+ }
+ return 0;
+}
+
+const struct master_instance *
+master_instance_list_find_by_name(struct master_instance_list *list,
+ const char *name)
+{
+ const struct master_instance *inst;
+
+ i_assert(*name != '\0');
+
+ if (array_count(&list->instances) == 0)
+ (void)master_instance_list_refresh_and_update(list);
+
+ array_foreach(&list->instances, inst) {
+ if (strcmp(inst->name, name) == 0)
+ return inst;
+ }
+ return NULL;
+}
+
+struct master_instance_list_iter *
+master_instance_list_iterate_init(struct master_instance_list *list)
+{
+ struct master_instance_list_iter *iter;
+
+ iter = i_new(struct master_instance_list_iter, 1);
+ iter->list = list;
+ (void)master_instance_list_refresh_and_update(list);
+ return iter;
+}
+
+const struct master_instance *
+master_instance_iterate_list_next(struct master_instance_list_iter *iter)
+{
+ if (iter->idx == array_count(&iter->list->instances))
+ return NULL;
+ return array_idx(&iter->list->instances, iter->idx++);
+}
+
+void master_instance_iterate_list_deinit(struct master_instance_list_iter **_iter)
+{
+ struct master_instance_list_iter *iter = *_iter;
+
+ *_iter = NULL;
+
+ i_free(iter);
+}
diff --git a/src/lib-master/master-instance.h b/src/lib-master/master-instance.h
new file mode 100644
index 0000000..2b25978
--- /dev/null
+++ b/src/lib-master/master-instance.h
@@ -0,0 +1,42 @@
+#ifndef MASTER_INSTANCE_H
+#define MASTER_INSTANCE_H
+
+#define MASTER_INSTANCE_FNAME "instances"
+
+struct master_instance_list;
+
+struct master_instance {
+ time_t last_used;
+ const char *name;
+ const char *base_dir;
+ const char *config_path;
+};
+
+struct master_instance_list *master_instance_list_init(const char *path);
+void master_instance_list_deinit(struct master_instance_list **list);
+
+/* Add/update last_used timestamp for an instance. Returns 0 if ok,
+ -1 if I/O error. */
+int master_instance_list_update(struct master_instance_list *list,
+ const char *base_dir);
+/* Set instance's name. Returns 1 if ok, 0 if name was already used for
+ another instance (base_dir) or -1 if I/O error. */
+int master_instance_list_set_name(struct master_instance_list *list,
+ const char *base_dir, const char *name);
+/* Remove instance. Returns 1 if ok, 0 if it didn't exist or -1 if I/O error. */
+int master_instance_list_remove(struct master_instance_list *list,
+ const char *base_dir);
+
+/* Find instance by its name. */
+const struct master_instance *
+master_instance_list_find_by_name(struct master_instance_list *list,
+ const char *name);
+
+/* Iterate through existing instances. */
+struct master_instance_list_iter *
+master_instance_list_iterate_init(struct master_instance_list *list);
+const struct master_instance *
+master_instance_iterate_list_next(struct master_instance_list_iter *iter);
+void master_instance_iterate_list_deinit(struct master_instance_list_iter **iter);
+
+#endif
diff --git a/src/lib-master/master-interface.h b/src/lib-master/master-interface.h
new file mode 100644
index 0000000..e5db7ce
--- /dev/null
+++ b/src/lib-master/master-interface.h
@@ -0,0 +1,117 @@
+#ifndef MASTER_INTERFACE_H
+#define MASTER_INTERFACE_H
+
+/* We are attempting semi-compatibility with Postfix's master process here.
+ Whether this is useful or not remains to be seen. */
+
+/* Child processes should send status updates whenever they accept a new
+ connection (decrease available_count) and when they close existing
+ connection (increase available_count). */
+struct master_status {
+ pid_t pid;
+ /* uid is used to check for old/invalid status messages */
+ unsigned int uid;
+ /* number of new connections process is currently accepting */
+ unsigned int available_count;
+};
+
+/* When connecting to log service, send this handshake first */
+struct log_service_handshake {
+ /* If magic is invalid, assume the data is already what we want
+ to log */
+#define MASTER_LOG_MAGIC 0x02ff03fe
+ unsigned int log_magic;
+
+ /* Add this prefix to each logged line */
+#define MASTER_LOG_PREFIX_NAME "MASTER"
+ unsigned int prefix_len;
+ /* unsigned char prefix[]; */
+};
+
+enum master_login_state {
+ MASTER_LOGIN_STATE_NONFULL = 0,
+ MASTER_LOGIN_STATE_FULL
+};
+
+/* getenv(MASTER_IS_PARENT_ENV) != NULL if process was started by
+ Dovecot master */
+#define MASTER_IS_PARENT_ENV "DOVECOT_CHILD_PROCESS"
+
+/* getenv(MASTER_UID_ENV) provides master_status.uid value */
+#define MASTER_UID_ENV "GENERATION"
+
+/* getenv(MASTER_SERVICE_NAME) provides the service's name */
+#define MASTER_SERVICE_ENV "SERVICE_NAME"
+
+/* getenv(MASTER_CLIENT_LIMIT_ENV) provides maximum
+ master_status.available_count as specified in configuration file */
+#define MASTER_CLIENT_LIMIT_ENV "CLIENT_LIMIT"
+
+/* getenv(MASTER_PROCESS_LIMIT_ENV) specifies how many processes of this type
+ can be created before reaching the limit */
+#define MASTER_PROCESS_LIMIT_ENV "PROCESS_LIMIT"
+
+/* getenv(MASTER_PROCESS_MIN_AVAIL_ENV) specifies how many processes of this
+ type are created at startup and are kept running all the time */
+#define MASTER_PROCESS_MIN_AVAIL_ENV "PROCESS_MIN_AVAIL"
+
+/* getenv(MASTER_SERVICE_COUNT_ENV) specifies how many client connections the
+ process can finish handling before it should kill itself. */
+#define MASTER_SERVICE_COUNT_ENV "SERVICE_COUNT"
+
+/* getenv(MASTER_SERVICE_IDLE_KILL_ENV) specifies service's idle_kill timeout
+ in seconds. */
+#define MASTER_SERVICE_IDLE_KILL_ENV "IDLE_KILL"
+
+/* getenv(MASTER_CONFIG_FILE_ENV) provides path to configuration file/socket */
+#define MASTER_CONFIG_FILE_ENV "CONFIG_FILE"
+
+/* getenv(MASTER_DOVECOT_VERSION_ENV) provides master's version number
+ (unset if version_ignore=yes) */
+#define MASTER_DOVECOT_VERSION_ENV "DOVECOT_VERSION"
+
+/* getenv(MASTER_SSL_KEY_PASSWORD_ENV) returns manually typed SSL key password,
+ if dovecot was started with -p parameter. */
+#define MASTER_SSL_KEY_PASSWORD_ENV "SSL_KEY_PASSWORD"
+
+/* getenv(DOVECOT_PRESERVE_ENVS_ENV) returns a space separated list of
+ environments that should be preserved. */
+#define DOVECOT_PRESERVE_ENVS_ENV "DOVECOT_PRESERVE_ENVS"
+
+/* getenv(DOVECOT_LOG_DEBUG_ENV) returns the global log_debug setting. This can
+ be used to initialize debug logging immediately at startup. */
+#define DOVECOT_LOG_DEBUG_ENV "LOG_DEBUG"
+
+/* getenv(DOVECOT_STATS_WRITER_SOCKET_PATH) returns path to the stats-writer
+ socket. */
+#define DOVECOT_STATS_WRITER_SOCKET_PATH "STATS_WRITER_SOCKET_PATH"
+
+/* Write pipe to anvil. */
+#define MASTER_ANVIL_FD 3
+/* Anvil reads new log fds from this fd */
+#define MASTER_ANVIL_LOG_FDPASS_FD 4
+/* Master's "all processes full" notification fd for login processes */
+#define MASTER_LOGIN_NOTIFY_FD 4
+
+/* Shared pipe to master, used to send master_status reports */
+#define MASTER_STATUS_FD 5
+/* Pipe to master, used to detect when it dies. (MASTER_STATUS_FD would have
+ been fine for this, except it's inefficient in Linux) */
+#define MASTER_DEAD_FD 6
+/* First file descriptor where process is expected to be listening.
+ The file descriptor count is given in -s parameter, defaulting to 1.
+
+ master_status.available_count reports how many accept()s we're still
+ accepting. Once no children are listening, master will do it and create
+ new child processes when needed. */
+#define MASTER_LISTEN_FD_FIRST 7
+
+/* Timeouts: base everything on how long we can wait for login clients. */
+#define MASTER_LOGIN_TIMEOUT_SECS (3*60)
+/* auth server should abort auth requests before that happens */
+#define MASTER_AUTH_SERVER_TIMEOUT_SECS (MASTER_LOGIN_TIMEOUT_SECS - 30)
+/* auth clients should abort auth lookups after server was supposed to have
+ done that */
+#define MASTER_AUTH_LOOKUP_TIMEOUT_SECS (MASTER_AUTH_SERVER_TIMEOUT_SECS + 5)
+
+#endif
diff --git a/src/lib-master/master-login-auth.c b/src/lib-master/master-login-auth.c
new file mode 100644
index 0000000..42de091
--- /dev/null
+++ b/src/lib-master/master-login-auth.c
@@ -0,0 +1,642 @@
+/* Copyright (c) 2009-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "net.h"
+#include "ioloop.h"
+#include "eacces-error.h"
+#include "hostpid.h"
+#include "istream.h"
+#include "ostream.h"
+#include "llist.h"
+#include "hex-binary.h"
+#include "hash.h"
+#include "str.h"
+#include "strescape.h"
+#include "time-util.h"
+#include "connection.h"
+#include "auth-client-private.h"
+#include "master-interface.h"
+#include "master-service.h"
+#include "master-auth.h"
+#include "master-login-auth.h"
+
+
+#define AUTH_MAX_INBUF_SIZE 8192
+
+struct master_login_auth_request {
+ struct master_login_auth_request *prev, *next;
+ struct event *event;
+
+ unsigned int id;
+ struct timeval create_stamp;
+
+ pid_t auth_pid;
+ unsigned int auth_id;
+ unsigned int client_pid;
+ uint8_t cookie[MASTER_AUTH_COOKIE_SIZE];
+
+ master_login_auth_request_callback_t *callback;
+ void *context;
+
+ bool aborted:1;
+};
+
+struct master_login_auth {
+ struct connection conn;
+ struct connection_list *clist;
+ struct event *event;
+ pool_t pool;
+ int refcount;
+
+ const char *auth_socket_path;
+
+ struct timeval connect_time, handshake_time;
+
+ struct timeout *to;
+
+ unsigned int id_counter;
+ HASH_TABLE(void *, struct master_login_auth_request *) requests;
+ /* linked list of requests, ordered by create_stamp */
+ struct master_login_auth_request *request_head, *request_tail;
+
+ pid_t auth_server_pid;
+
+ unsigned int timeout_msecs;
+
+ bool connected:1;
+ bool request_auth_token:1;
+};
+
+static int
+master_login_auth_input_args(struct connection *_conn, const char *const *args);
+static int
+master_login_auth_handshake_line(struct connection *_conn, const char *line);
+static void master_login_auth_destroy(struct connection *_conn);
+
+static void master_login_auth_update_timeout(struct master_login_auth *auth);
+static void master_login_auth_check_spids(struct master_login_auth *auth);
+
+static const struct connection_vfuncs master_login_auth_vfuncs = {
+ .destroy = master_login_auth_destroy,
+ .handshake_line = master_login_auth_handshake_line,
+ .input_args = master_login_auth_input_args,
+};
+
+static const struct connection_settings master_login_auth_set = {
+ .dont_send_version = TRUE,
+ .service_name_in = "auth-master",
+ .service_name_out = "auth-master",
+ .major_version = AUTH_MASTER_PROTOCOL_MAJOR_VERSION,
+ .minor_version = AUTH_MASTER_PROTOCOL_MINOR_VERSION,
+ .unix_client_connect_msecs = 1000,
+ .input_max_size = AUTH_MAX_INBUF_SIZE,
+ .output_max_size = SIZE_MAX,
+ .client = TRUE,
+};
+
+static int
+master_login_auth_connect(struct master_login_auth *auth);
+
+struct master_login_auth *
+master_login_auth_init(const char *auth_socket_path, bool request_auth_token)
+{
+ struct master_login_auth *auth;
+ pool_t pool;
+
+ pool = pool_alloconly_create("master login auth", 1024);
+ auth = p_new(pool, struct master_login_auth, 1);
+ auth->pool = pool;
+ auth->auth_socket_path = p_strdup(pool, auth_socket_path);
+ auth->request_auth_token = request_auth_token;
+ auth->refcount = 1;
+ hash_table_create_direct(&auth->requests, pool, 0);
+ auth->id_counter = i_rand_limit(32767) * 131072U;
+
+ auth->clist = connection_list_init(&master_login_auth_set,
+ &master_login_auth_vfuncs);
+
+ auth->event = event_create(NULL);
+ event_add_category(auth->event, &event_category_auth_client);
+ event_set_append_log_prefix(auth->event, "auth-master: login: ");
+
+ auth->conn.event_parent = auth->event;
+ connection_init_client_unix(auth->clist, &auth->conn,
+ auth->auth_socket_path);
+
+ auth->timeout_msecs = 1000 * MASTER_AUTH_LOOKUP_TIMEOUT_SECS;
+ master_login_auth_connect(auth);
+ return auth;
+}
+
+static void request_failure(struct master_login_auth *auth,
+ struct master_login_auth_request *request,
+ const char *log_reason, const char *client_reason)
+{
+ string_t *str = t_str_new(128);
+
+ str_printfa(str, "auth connected %u msecs ago",
+ timeval_diff_msecs(&ioloop_timeval, &auth->connect_time));
+ if (auth->handshake_time.tv_sec != 0) {
+ str_printfa(str, ", handshake %u msecs ago",
+ timeval_diff_msecs(&ioloop_timeval, &auth->handshake_time));
+ }
+ str_printfa(str, ", request took %u msecs, client-pid=%u client-id=%u",
+ timeval_diff_msecs(&ioloop_timeval, &request->create_stamp),
+ request->client_pid, request->auth_id);
+
+ struct event_passthrough *e =
+ event_create_passthrough(request->event)->
+ set_name("auth_master_client_login_finished");
+ e->add_str("error", log_reason);
+ e_error(e->event(), "Login auth request failed: %s (%s)",
+ log_reason, str_c(str));
+
+ request->callback(NULL, client_reason, request->context);
+}
+
+static void
+request_internal_failure(struct master_login_auth *auth,
+ struct master_login_auth_request *request,
+ const char *reason)
+{
+ request_failure(auth, request, reason, MASTER_AUTH_ERRMSG_INTERNAL_FAILURE);
+}
+
+static void request_free(struct master_login_auth_request **_request)
+{
+ struct master_login_auth_request *request = *_request;
+
+ *_request = NULL;
+
+ event_unref(&request->event);
+ i_free(request);
+}
+
+static void
+master_login_auth_fail(struct master_login_auth *auth,
+ const char *reason) ATTR_NULL(2)
+{
+ struct master_login_auth_request *request;
+
+ if (reason == NULL)
+ reason = "Disconnected from auth server, aborting";
+
+ if (auth->connected)
+ connection_disconnect(&auth->conn);
+ auth->connected = FALSE;
+
+ while (auth->request_head != NULL) {
+ request = auth->request_head;
+ DLLIST2_REMOVE(&auth->request_head,
+ &auth->request_tail, request);
+
+ request_internal_failure(auth, request, reason);
+ request_free(&request);
+ }
+ hash_table_clear(auth->requests, FALSE);
+
+ timeout_remove(&auth->to);
+ i_zero(&auth->connect_time);
+ i_zero(&auth->handshake_time);
+}
+
+void master_login_auth_disconnect(struct master_login_auth *auth)
+{
+ master_login_auth_fail(auth, NULL);
+}
+
+static void master_login_auth_unref(struct master_login_auth **_auth)
+{
+ struct master_login_auth *auth = *_auth;
+ struct connection_list *clist = auth->clist;
+
+ *_auth = NULL;
+
+ i_assert(auth->refcount > 0);
+ if (--auth->refcount > 0)
+ return;
+
+ hash_table_destroy(&auth->requests);
+ connection_deinit(&auth->conn);
+ connection_list_deinit(&clist);
+ event_unref(&auth->event);
+ pool_unref(&auth->pool);
+}
+
+void master_login_auth_deinit(struct master_login_auth **_auth)
+{
+ struct master_login_auth *auth = *_auth;
+
+ *_auth = NULL;
+
+ master_login_auth_disconnect(auth);
+ master_login_auth_unref(&auth);
+}
+
+void master_login_auth_set_timeout(struct master_login_auth *auth,
+ unsigned int msecs)
+{
+ auth->timeout_msecs = msecs;
+}
+
+static void master_login_auth_destroy(struct connection *_conn)
+{
+ struct master_login_auth *auth =
+ container_of(_conn, struct master_login_auth, conn);
+
+ switch (_conn->disconnect_reason) {
+ case CONNECTION_DISCONNECT_HANDSHAKE_FAILED:
+ master_login_auth_fail(auth,
+ "Handshake with auth service failed");
+ break;
+ case CONNECTION_DISCONNECT_BUFFER_FULL:
+ /* buffer full */
+ e_error(auth->event, "Auth server sent us too long line");
+ master_login_auth_fail(auth, NULL);
+ break;
+ default:
+ /* disconnected. stop accepting new connections, because in
+ default configuration we no longer have permissions to
+ connect back to auth-master */
+ master_service_stop_new_connections(master_service);
+ master_login_auth_fail(auth, NULL);
+ }
+}
+
+static unsigned int auth_get_next_timeout_msecs(struct master_login_auth *auth)
+{
+ struct timeval expires;
+ int diff;
+
+ expires = auth->request_head->create_stamp;
+ timeval_add_msecs(&expires, auth->timeout_msecs);
+
+ diff = timeval_diff_msecs(&expires, &ioloop_timeval);
+ return (diff <= 0 ? 0 : (unsigned int)diff);
+}
+
+static void master_login_auth_timeout(struct master_login_auth *auth)
+{
+ struct master_login_auth_request *request;
+ const char *reason;
+
+ while (auth->request_head != NULL &&
+ auth_get_next_timeout_msecs(auth) == 0) {
+ int msecs;
+
+ request = auth->request_head;
+ DLLIST2_REMOVE(&auth->request_head,
+ &auth->request_tail, request);
+ hash_table_remove(auth->requests, POINTER_CAST(request->id));
+
+ msecs = timeval_diff_msecs(&ioloop_timeval,
+ &request->create_stamp);
+ reason = t_strdup_printf(
+ "Auth server request timed out after %u.%03u secs",
+ msecs/1000, msecs%1000);
+ request_internal_failure(auth, request, reason);
+ request_free(&request);
+ }
+ timeout_remove(&auth->to);
+ master_login_auth_update_timeout(auth);
+}
+
+static void master_login_auth_update_timeout(struct master_login_auth *auth)
+{
+ i_assert(auth->to == NULL);
+
+ if (auth->request_head != NULL) {
+ auth->to = timeout_add(auth_get_next_timeout_msecs(auth),
+ master_login_auth_timeout, auth);
+ }
+}
+
+static int
+master_login_auth_handshake_line(struct connection *_conn, const char *line)
+{
+ struct master_login_auth *auth =
+ container_of(_conn, struct master_login_auth, conn);
+ const char *const *tmp;
+ unsigned int major_version, minor_version;
+
+ tmp = t_strsplit_tabescaped(line);
+ if (!auth->conn.version_received && strcmp(tmp[0], "VERSION") == 0 &&
+ tmp[1] != NULL && tmp[2] != NULL) {
+ if (str_to_uint(tmp[1], &major_version) < 0 ||
+ str_to_uint(tmp[2], &minor_version) < 0) {
+ e_error(auth->event,
+ "Auth server sent invalid version line: %s",
+ line);
+ return -1;
+ }
+
+ if (connection_verify_version(_conn, "auth-master",
+ major_version,
+ minor_version) < 0)
+ return -1;
+ return 0;
+ }
+ if (strcmp(tmp[0], "SPID") != 0 ||
+ str_to_pid(tmp[1], &auth->auth_server_pid) < 0) {
+ e_error(auth->event,
+ "Auth server did not send valid SPID: %s", line);
+ return -1;
+ }
+
+ master_login_auth_check_spids(auth);
+ return 1;
+}
+
+static void
+master_login_auth_request_remove(struct master_login_auth *auth,
+ struct master_login_auth_request *request)
+{
+ bool update_timeout;
+
+ update_timeout = request->prev == NULL;
+
+ hash_table_remove(auth->requests, POINTER_CAST(request->id));
+ DLLIST2_REMOVE(&auth->request_head, &auth->request_tail, request);
+
+ if (update_timeout) {
+ timeout_remove(&auth->to);
+ master_login_auth_update_timeout(auth);
+ }
+}
+
+static struct master_login_auth_request *
+master_login_auth_lookup_request(struct master_login_auth *auth,
+ unsigned int id)
+{
+ struct master_login_auth_request *request;
+
+ request = hash_table_lookup(auth->requests, POINTER_CAST(id));
+ if (request == NULL) {
+ e_error(auth->event,
+ "Auth server sent reply with unknown ID %u", id);
+ return NULL;
+ }
+ master_login_auth_request_remove(auth, request);
+ if (request->aborted) {
+ request->callback(NULL, MASTER_AUTH_ERRMSG_INTERNAL_FAILURE,
+ request->context);
+ request_free(&request);
+ return NULL;
+ }
+ return request;
+}
+
+static void
+master_login_auth_input_user(struct master_login_auth *auth, unsigned int id,
+ const char *const *args)
+{
+ struct master_login_auth_request *request;
+
+ /* USER <id> <userid> [..] */
+ request = master_login_auth_lookup_request(auth, id);
+ if (request != NULL) {
+ struct event_passthrough *e =
+ event_create_passthrough(request->event)->
+ set_name("auth_master_client_login_finished");
+ if (args[0] != NULL && *args[0] != '\0')
+ e->add_str("user", args[0]);
+ e_debug(e->event(), "Login auth request successful");
+
+ request->callback(args, NULL, request->context);
+ request_free(&request);
+ }
+}
+
+static void
+master_login_auth_input_notfound(struct master_login_auth *auth,
+ unsigned int id,
+ const char *const *args ATTR_UNUSED)
+{
+ struct master_login_auth_request *request;
+
+ /* NOTFOUND <id> */
+ request = master_login_auth_lookup_request(auth, id);
+ if (request != NULL) {
+ const char *reason = t_strdup_printf(
+ "Authenticated user not found from userdb, "
+ "auth lookup id=%u", id);
+ request_internal_failure(auth, request, reason);
+ request_free(&request);
+ }
+}
+
+static void
+master_login_auth_input_fail(struct master_login_auth *auth, unsigned int id,
+ const char *const *args)
+{
+ struct master_login_auth_request *request;
+ const char *error = NULL;
+ unsigned int i;
+
+ /* FAIL <id> [..] [reason=<error>] [..] */
+ for (i = 0; args[i] != NULL; i++) {
+ if (str_begins(args[i], "reason="))
+ error = args[i] + 7;
+ }
+
+ request = master_login_auth_lookup_request(auth, id);
+ if (request != NULL) {
+ if (error == NULL) {
+ request_internal_failure(auth, request,
+ "Internal auth failure");
+ } else {
+ const char *log_reason = t_strdup_printf(
+ "Internal auth failure: %s", error);
+ request_failure(auth, request, log_reason, error);
+ }
+ request_free(&request);
+ }
+}
+
+static int
+master_login_auth_input_args(struct connection *_conn, const char *const *args)
+{
+ struct master_login_auth *auth =
+ container_of(_conn, struct master_login_auth, conn);
+ unsigned int id;
+
+ if (args[0] != NULL && strcmp(args[0], "CUID") == 0) {
+ e_error(auth->event, "%s is an auth client socket. "
+ "It should be a master socket.",
+ auth->auth_socket_path);
+ return -1;
+ }
+
+ if (args[0] == NULL || args[1] == NULL ||
+ str_to_uint(args[1], &id) < 0) {
+ e_error(auth->event, "BUG: Unexpected input: %s",
+ t_strarray_join(args, "\t"));
+ return -1;
+ }
+
+ auth->refcount++;
+ if (strcmp(args[0], "USER") == 0)
+ master_login_auth_input_user(auth, id, &args[2]);
+ else if (strcmp(args[0], "NOTFOUND") == 0)
+ master_login_auth_input_notfound(auth, id, &args[2]);
+ else if (strcmp(args[0], "FAIL") == 0)
+ master_login_auth_input_fail(auth, id, &args[2]);
+ master_login_auth_unref(&auth);
+
+ return 1;
+}
+
+static int
+master_login_auth_connect(struct master_login_auth *auth)
+{
+ i_assert(!auth->connected);
+
+ if (connection_client_connect(&auth->conn) < 0) {
+ if (errno == EACCES) {
+ e_error(auth->event, "%s",
+ eacces_error_get("connect",
+ auth->auth_socket_path));
+ } else {
+ e_error(auth->event, "connect(%s) failed: %m",
+ auth->auth_socket_path);
+ }
+ return -1;
+ }
+ io_loop_time_refresh();
+ auth->connect_time = ioloop_timeval;
+ auth->connected = TRUE;
+
+ o_stream_nsend_str(auth->conn.output,
+ t_strdup_printf("VERSION\t%u\t%u\n",
+ AUTH_MASTER_PROTOCOL_MAJOR_VERSION,
+ AUTH_MASTER_PROTOCOL_MINOR_VERSION));
+ return 0;
+}
+
+static bool
+auth_request_check_spid(struct master_login_auth *auth,
+ struct master_login_auth_request *req)
+{
+ if (auth->auth_server_pid != req->auth_pid &&
+ auth->conn.handshake_received) {
+ /* auth server was restarted. don't even attempt a login. */
+ e_warning(auth->event,
+ "Auth server restarted (pid %u -> %u), aborting auth",
+ (unsigned int)req->auth_pid,
+ (unsigned int)auth->auth_server_pid);
+ return FALSE;
+ }
+ return TRUE;
+}
+
+static void master_login_auth_check_spids(struct master_login_auth *auth)
+{
+ struct master_login_auth_request *req, *next;
+
+ for (req = auth->request_head; req != NULL; req = next) {
+ next = req->next;
+ if (!auth_request_check_spid(auth, req))
+ req->aborted = TRUE;
+ }
+}
+
+static void
+master_login_auth_send_request(struct master_login_auth *auth,
+ struct master_login_auth_request *req)
+{
+ string_t *str;
+
+ if (!auth_request_check_spid(auth, req)) {
+ master_login_auth_request_remove(auth, req);
+ req->callback(NULL, MASTER_AUTH_ERRMSG_INTERNAL_FAILURE,
+ req->context);
+ request_free(&req);
+ return;
+ }
+
+ str = t_str_new(128);
+ str_printfa(str, "REQUEST\t%u\t%u\t%u\t", req->id,
+ req->client_pid, req->auth_id);
+ binary_to_hex_append(str, req->cookie, sizeof(req->cookie));
+ str_printfa(str, "\tsession_pid=%s", my_pid);
+ if (auth->request_auth_token)
+ str_append(str, "\trequest_auth_token");
+ str_append_c(str, '\n');
+ o_stream_nsend(auth->conn.output, str_data(str), str_len(str));
+}
+
+void master_login_auth_request(struct master_login_auth *auth,
+ const struct master_auth_request *req,
+ master_login_auth_request_callback_t *callback,
+ void *context)
+{
+ struct master_login_auth_request *login_req;
+ unsigned int id;
+
+ if (!auth->connected) {
+ if (master_login_auth_connect(auth) < 0) {
+ /* we couldn't connect to auth now,
+ so we probably can't in future either. */
+ master_service_stop_new_connections(master_service);
+ callback(NULL, MASTER_AUTH_ERRMSG_INTERNAL_FAILURE,
+ context);
+ return;
+ }
+ }
+
+ id = ++auth->id_counter;
+ if (id == 0)
+ id++;
+
+ io_loop_time_refresh();
+ login_req = i_new(struct master_login_auth_request, 1);
+ login_req->create_stamp = ioloop_timeval;
+ login_req->id = id;
+ login_req->auth_pid = req->auth_pid;
+ login_req->client_pid = req->client_pid;
+ login_req->auth_id = req->auth_id;
+ memcpy(login_req->cookie, req->cookie, sizeof(login_req->cookie));
+ login_req->callback = callback;
+ login_req->context = context;
+ i_assert(hash_table_lookup(auth->requests, POINTER_CAST(id)) == NULL);
+ hash_table_insert(auth->requests, POINTER_CAST(id), login_req);
+ DLLIST2_APPEND(&auth->request_head, &auth->request_tail, login_req);
+
+ login_req->event = event_create(auth->event);
+ event_add_int(login_req->event, "id", login_req->id);
+ event_set_append_log_prefix(login_req->event,
+ t_strdup_printf("request [%u]: ",
+ login_req->id));
+
+ if (req->local_ip.family != 0) {
+ event_add_str(login_req->event, "local_ip",
+ net_ip2addr(&req->local_ip));
+ }
+ if (req->local_port != 0) {
+ event_add_int(login_req->event, "local_port",
+ req->local_port);
+ }
+ if (req->remote_ip.family != 0) {
+ event_add_str(login_req->event, "remote_ip",
+ net_ip2addr(&req->remote_ip));
+ }
+ if (req->remote_port != 0) {
+ event_add_int(login_req->event, "remote_port",
+ req->remote_port);
+ }
+
+ struct event_passthrough *e =
+ event_create_passthrough(login_req->event)->
+ set_name("auth_master_client_login_started");
+ e_debug(e->event(), "Started login auth request");
+
+ if (auth->to == NULL)
+ master_login_auth_update_timeout(auth);
+
+ master_login_auth_send_request(auth, login_req);
+}
+
+unsigned int master_login_auth_request_count(struct master_login_auth *auth)
+{
+ return hash_table_count(auth->requests);
+}
diff --git a/src/lib-master/master-login-auth.h b/src/lib-master/master-login-auth.h
new file mode 100644
index 0000000..d08732b
--- /dev/null
+++ b/src/lib-master/master-login-auth.h
@@ -0,0 +1,28 @@
+#ifndef MASTER_LOGIN_AUTH_H
+#define MASTER_LOGIN_AUTH_H
+
+struct master_auth_request;
+
+typedef void
+master_login_auth_request_callback_t(const char *const *auth_args,
+ const char *errormsg, void *context);
+
+struct master_login_auth *
+master_login_auth_init(const char *auth_socket_path, bool request_auth_token);
+void master_login_auth_deinit(struct master_login_auth **auth);
+void master_login_auth_disconnect(struct master_login_auth *auth);
+
+/* Set timeout for requests. */
+void master_login_auth_set_timeout(struct master_login_auth *auth,
+ unsigned int msecs);
+
+/* req has been sent by login process. this function finishes authentication
+ by performing verifying from auth that req is valid and doing the userdb
+ lookup. */
+void master_login_auth_request(struct master_login_auth *auth,
+ const struct master_auth_request *req,
+ master_login_auth_request_callback_t *callback,
+ void *context);
+unsigned int master_login_auth_request_count(struct master_login_auth *auth);
+
+#endif
diff --git a/src/lib-master/master-login.c b/src/lib-master/master-login.c
new file mode 100644
index 0000000..e25ce35
--- /dev/null
+++ b/src/lib-master/master-login.c
@@ -0,0 +1,564 @@
+/* Copyright (c) 2009-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "ioloop.h"
+#include "ostream.h"
+#include "fdpass.h"
+#include "llist.h"
+#include "str.h"
+#include "strescape.h"
+#include "time-util.h"
+#include "master-service-private.h"
+#include "master-login.h"
+#include "master-login-auth.h"
+
+#include <sys/stat.h>
+#include <unistd.h>
+
+#define master_login_conn_is_closed(conn) \
+ ((conn)->fd == -1)
+#define master_login_conn_has_clients(conn) \
+ ((conn)->refcount > 1)
+
+struct master_login_connection {
+ struct master_login_connection *prev, *next;
+
+ struct master_login *login;
+ struct master_login_client *clients;
+ struct timeval create_time;
+ int refcount;
+ int fd;
+ struct io *io;
+ struct ostream *output;
+
+ bool login_success:1;
+};
+
+struct master_login_postlogin {
+ struct master_login_client *client;
+
+ int fd;
+ struct timeval create_time;
+ struct io *io;
+ struct timeout *to;
+ string_t *input;
+ char *username;
+ char *socket_path;
+};
+
+struct master_login {
+ struct master_service *service;
+ master_login_callback_t *callback;
+ master_login_failure_callback_t *failure_callback;
+ struct master_login_connection *conns;
+ struct master_login_auth *auth;
+ char *postlogin_socket_path;
+ unsigned int postlogin_timeout_secs;
+
+ bool stopping:1;
+};
+
+static void master_login_conn_close(struct master_login_connection *conn);
+static void master_login_conn_unref(struct master_login_connection **_conn);
+
+struct master_login *
+master_login_init(struct master_service *service,
+ const struct master_login_settings *set)
+{
+ struct master_login *login;
+
+ i_assert(set->postlogin_socket_path == NULL ||
+ set->postlogin_timeout_secs > 0);
+
+ login = i_new(struct master_login, 1);
+ login->service = service;
+ login->callback = set->callback;
+ login->failure_callback = set->failure_callback;
+ login->auth = master_login_auth_init(set->auth_socket_path,
+ set->request_auth_token);
+ login->postlogin_socket_path = i_strdup(set->postlogin_socket_path);
+ login->postlogin_timeout_secs = set->postlogin_timeout_secs;
+
+ i_assert(service->login == NULL);
+ service->login = login;
+ return login;
+}
+
+void master_login_deinit(struct master_login **_login)
+{
+ struct master_login *login = *_login;
+
+ *_login = NULL;
+
+ i_assert(login->service->login == login);
+ login->service->login = NULL;
+
+ master_login_auth_deinit(&login->auth);
+ while (login->conns != NULL) {
+ struct master_login_connection *conn = login->conns;
+
+ master_login_conn_close(conn);
+ master_login_conn_unref(&conn);
+ }
+ i_free(login->postlogin_socket_path);
+ i_free(login);
+}
+
+static void ATTR_FORMAT(2, 3)
+conn_error(struct master_login_connection *conn, const char *fmt, ...)
+{
+ string_t *str = t_str_new(128);
+ va_list args;
+
+ va_start(args, fmt);
+ str_printfa(str, "connection created %d msecs ago",
+ timeval_diff_msecs(&ioloop_timeval, &conn->create_time));
+ if (conn->clients != NULL) {
+ struct master_login_client *client = conn->clients;
+
+ str_append(str, ", ");
+ if (client->next != NULL)
+ str_printfa(str, "%u clients, first ", conn->refcount-1);
+ str_printfa(str, "client created %d msecs ago: ",
+ timeval_diff_msecs(&ioloop_timeval,
+ &client->create_time));
+ str_printfa(str, "session=%s, rip=%s, auth_pid=%ld, "
+ "client-pid=%u, client-id=%u",
+ client->session_id,
+ net_ip2addr(&client->auth_req.remote_ip),
+ (long)client->auth_req.auth_pid,
+ client->auth_req.client_pid,
+ client->auth_req.auth_id);
+ if (client->postlogin_client != NULL) {
+ struct master_login_postlogin *pl =
+ client->postlogin_client;
+ str_printfa(str, ", post-login script %s started %d msecs ago",
+ pl->socket_path,
+ timeval_diff_msecs(&ioloop_timeval,
+ &pl->create_time));
+ }
+ }
+ i_error("%s (%s)", t_strdup_vprintf(fmt, args), str_c(str));
+ va_end(args);
+}
+
+static int
+master_login_conn_read_request(struct master_login_connection *conn,
+ struct master_auth_request *req_r,
+ unsigned char data[MASTER_AUTH_MAX_DATA_SIZE],
+ int *client_fd_r)
+{
+ struct stat st;
+ ssize_t ret;
+
+ *client_fd_r = -1;
+
+ ret = fd_read(conn->fd, req_r, sizeof(*req_r), client_fd_r);
+ if (ret != sizeof(*req_r)) {
+ if (ret == 0) {
+ /* disconnected */
+ if (master_login_conn_has_clients(conn))
+ conn_error(conn, "Login client disconnected too early");
+ } else if (ret > 0) {
+ /* request wasn't fully read */
+ conn_error(conn, "fd_read() partial input (%d/%d)",
+ (int)ret, (int)sizeof(*req_r));
+ } else {
+ if (errno == EAGAIN)
+ return 0;
+
+ conn_error(conn, "fd_read() failed: %m");
+ }
+ return -1;
+ }
+
+ if (req_r->data_size != 0) {
+ if (req_r->data_size > MASTER_AUTH_MAX_DATA_SIZE) {
+ conn_error(conn, "Too large auth data_size sent");
+ return -1;
+ }
+ /* @UNSAFE */
+ ret = read(conn->fd, data, req_r->data_size);
+ if (ret != (ssize_t)req_r->data_size) {
+ if (ret == 0) {
+ /* disconnected */
+ if (master_login_conn_has_clients(conn)) {
+ conn_error(conn, "Login client disconnected too early "
+ "(while reading data)");
+ }
+ } else if (ret > 0) {
+ /* request wasn't fully read */
+ conn_error(conn, "Data read partially %d/%u",
+ (int)ret, req_r->data_size);
+ } else {
+ conn_error(conn, "read(data) failed: %m");
+ }
+ return -1;
+ }
+ }
+
+ if (*client_fd_r == -1) {
+ conn_error(conn, "Auth request missing a file descriptor");
+ return -1;
+ }
+
+ if (fstat(*client_fd_r, &st) < 0) {
+ conn_error(conn, "fstat(fd_read client) failed: %m");
+ return -1;
+ }
+ if (st.st_ino != req_r->ino) {
+ conn_error(conn, "Auth request inode mismatch: %s != %s",
+ dec2str(st.st_ino), dec2str(req_r->ino));
+ return -1;
+ }
+ return 1;
+}
+
+static void master_login_client_free(struct master_login_client **_client)
+{
+ struct master_login_client *client = *_client;
+
+ *_client = NULL;
+ if (client->fd != -1) {
+ if (close(client->fd) < 0)
+ i_error("close(fd_read client) failed: %m");
+ /* this client failed (login callback wasn't called).
+ reset prefix to default. */
+ i_set_failure_prefix("%s: ", client->conn->login->service->name);
+ }
+
+ /* FIXME: currently we create a separate connection for each request,
+ so close the connection after we're done with this client */
+ if (!master_login_conn_is_closed(client->conn)) {
+ i_assert(client->conn->refcount > 1);
+ client->conn->refcount--;
+ }
+ DLLIST_REMOVE(&client->conn->clients, client);
+ master_login_conn_unref(&client->conn);
+ i_free(client->session_id);
+ i_free(client);
+}
+
+static void master_login_auth_finish(struct master_login_client *client,
+ const char *const *auth_args)
+{
+ struct master_login *login = client->conn->login;
+ struct master_service *service = login->service;
+ bool close_sockets;
+
+ close_sockets = service->master_status.available_count == 0 &&
+ service->service_count_left == 1;
+
+ client->conn->login_success = TRUE;
+ login->callback(client, auth_args[0], auth_args+1);
+
+ if (close_sockets) {
+ /* we're dying as soon as this connection closes. */
+ i_assert(master_login_auth_request_count(login->auth) == 0);
+ master_login_auth_disconnect(login->auth);
+
+ master_service_close_config_fd(service);
+ } else if (login->stopping) {
+ /* try stopping again */
+ master_login_stop(login);
+ }
+
+ client->fd = -1;
+ master_login_client_free(&client);
+}
+
+static void master_login_postlogin_free(struct master_login_postlogin *pl)
+{
+ if (pl->client != NULL) {
+ i_assert(pl->client->postlogin_client == pl);
+ master_login_client_free(&pl->client);
+ }
+ timeout_remove(&pl->to);
+ io_remove(&pl->io);
+ if (close(pl->fd) < 0)
+ i_error("close(postlogin) failed: %m");
+ str_free(&pl->input);
+ i_free(pl->socket_path);
+ i_free(pl->username);
+ i_free(pl);
+}
+
+static void master_login_postlogin_input(struct master_login_postlogin *pl)
+{
+ struct master_login_connection *conn = pl->client->conn;
+ char buf[1024];
+ const char *const *auth_args;
+ size_t len;
+ ssize_t ret;
+ int fd = -1;
+
+ while ((ret = fd_read(pl->fd, buf, sizeof(buf), &fd)) > 0) {
+ if (fd != -1) {
+ /* post-login script replaced fd */
+ if (close(pl->client->fd) < 0)
+ conn_error(conn, "close(client) failed: %m");
+ pl->client->fd = fd;
+ }
+ str_append_data(pl->input, buf, ret);
+ }
+
+ len = str_len(pl->input);
+ if (len > 0 && str_c(pl->input)[len-1] == '\n') {
+ /* finished reading the input */
+ str_truncate(pl->input, len-1);
+ } else {
+ if (ret < 0) {
+ if (errno == EAGAIN)
+ return;
+
+ conn_error(conn, "fd_read(%s) failed: %m", pl->socket_path);
+ } else if (str_len(pl->input) > 0) {
+ conn_error(conn, "fd_read(%s) failed: disconnected",
+ pl->socket_path);
+ } else {
+ conn_error(conn, "Post-login script denied access to user %s",
+ pl->username);
+ }
+ master_login_postlogin_free(pl);
+ return;
+ }
+
+ auth_args = t_strsplit_tabescaped(str_c(pl->input));
+ pl->client->postlogin_client = NULL;
+ master_login_auth_finish(pl->client, auth_args);
+
+ pl->client = NULL;
+ master_login_postlogin_free(pl);
+}
+
+static void master_login_postlogin_timeout(struct master_login_postlogin *pl)
+{
+ conn_error(pl->client->conn,
+ "Timeout waiting for post-login script to finish, aborting");
+
+ master_login_postlogin_free(pl);
+}
+
+static int master_login_postlogin(struct master_login_client *client,
+ const char *const *auth_args,
+ const char *socket_path)
+{
+ struct master_login *login = client->conn->login;
+ struct master_login_postlogin *pl;
+ string_t *str;
+ unsigned int i;
+ int fd;
+ ssize_t ret;
+
+ fd = net_connect_unix_with_retries(socket_path, 1000);
+ if (fd == -1) {
+ conn_error(client->conn, "net_connect_unix(%s) failed: %m%s",
+ socket_path, errno != EAGAIN ? "" :
+ " - http://wiki2.dovecot.org/SocketUnavailable");
+ return -1;
+ }
+
+ str = t_str_new(256);
+ str_printfa(str, "VERSION\tscript-login\t1\t0\n"
+ "%s\t%s", net_ip2addr(&client->auth_req.local_ip),
+ net_ip2addr(&client->auth_req.remote_ip));
+ for (i = 0; auth_args[i] != NULL; i++) {
+ str_append_c(str, '\t');
+ str_append_tabescaped(str, auth_args[i]);
+ }
+ str_append_c(str, '\n');
+ ret = fd_send(fd, client->fd, str_data(str), str_len(str));
+ if (ret != (ssize_t)str_len(str)) {
+ if (ret < 0) {
+ conn_error(client->conn, "write(%s) failed: %m", socket_path);
+ } else {
+ conn_error(client->conn, "write(%s) failed: partial write", socket_path);
+ }
+ i_close_fd(&fd);
+ return -1;
+ }
+ net_set_nonblock(fd, TRUE);
+ io_loop_time_refresh();
+
+ pl = i_new(struct master_login_postlogin, 1);
+ pl->client = client;
+ pl->username = i_strdup(auth_args[0]);
+ pl->socket_path = i_strdup(socket_path);
+ pl->create_time = ioloop_timeval;
+ pl->fd = fd;
+ pl->io = io_add(fd, IO_READ, master_login_postlogin_input, pl);
+ pl->to = timeout_add(login->postlogin_timeout_secs * 1000,
+ master_login_postlogin_timeout, pl);
+ pl->input = str_new(default_pool, 512);
+
+ i_assert(client->postlogin_client == NULL);
+ client->postlogin_client = pl;
+ return 0;
+}
+
+static const char *
+auth_args_find_postlogin_socket(const char *const *auth_args)
+{
+ for (unsigned int i = 0; auth_args[i] != NULL; i++) {
+ if (str_begins(auth_args[i], "postlogin="))
+ return auth_args[i]+10;
+ }
+ return NULL;
+}
+
+static void
+master_login_auth_callback(const char *const *auth_args, const char *errormsg,
+ void *context)
+{
+ struct master_login_client *client = context;
+ struct master_login_connection *conn = client->conn;
+ struct master_auth_reply reply;
+ const char *postlogin_socket_path;
+
+ i_assert(errormsg != NULL || auth_args != NULL);
+
+ i_zero(&reply);
+ reply.tag = client->auth_req.tag;
+ reply.status = errormsg == NULL ? MASTER_AUTH_STATUS_OK :
+ MASTER_AUTH_STATUS_INTERNAL_ERROR;
+ reply.mail_pid = getpid();
+ o_stream_nsend(conn->output, &reply, sizeof(reply));
+
+ if (errormsg != NULL || auth_args[0] == NULL) {
+ if (auth_args != NULL) {
+ i_error("login client: Username missing from auth reply");
+ errormsg = MASTER_AUTH_ERRMSG_INTERNAL_FAILURE;
+ }
+ conn->login->failure_callback(client, errormsg);
+ master_login_client_free(&client);
+ return;
+ }
+ i_set_failure_prefix("%s(%s): ", client->conn->login->service->name,
+ auth_args[0]);
+
+ postlogin_socket_path = auth_args_find_postlogin_socket(auth_args);
+ if (postlogin_socket_path == NULL)
+ postlogin_socket_path = conn->login->postlogin_socket_path;
+
+ if (postlogin_socket_path == NULL)
+ master_login_auth_finish(client, auth_args);
+ else {
+ /* we've sent the reply. the connection is no longer needed,
+ so disconnect it (before login process disconnects us and
+ logs an error) */
+ if (!master_login_conn_is_closed(conn)) {
+ master_login_conn_close(conn);
+ master_login_conn_unref(&conn);
+ }
+
+ /* execute post-login scripts before finishing auth */
+ if (master_login_postlogin(client, auth_args,
+ postlogin_socket_path) < 0)
+ master_login_client_free(&client);
+ }
+}
+
+static void master_login_conn_input(struct master_login_connection *conn)
+{
+ struct master_auth_request req;
+ struct master_login_client *client;
+ struct master_login *login = conn->login;
+ unsigned char data[MASTER_AUTH_MAX_DATA_SIZE];
+ size_t i, session_len = 0;
+ int ret, client_fd;
+
+ ret = master_login_conn_read_request(conn, &req, data, &client_fd);
+ if (ret <= 0) {
+ if (ret < 0) {
+ master_login_conn_close(conn);
+ master_login_conn_unref(&conn);
+ }
+ i_close_fd(&client_fd);
+ return;
+ }
+ fd_close_on_exec(client_fd, TRUE);
+
+ /* extract the session ID from the request data */
+ for (i = 0; i < req.data_size; i++) {
+ if (data[i] == '\0') {
+ session_len = i++;
+ break;
+ }
+ }
+ io_loop_time_refresh();
+
+ /* @UNSAFE: we have a request. do userdb lookup for it. */
+ req.data_size -= i;
+ client = i_malloc(MALLOC_ADD(sizeof(struct master_login_client), req.data_size));
+ client->create_time = ioloop_timeval;
+ client->conn = conn;
+ client->fd = client_fd;
+ client->auth_req = req;
+ client->session_id = i_strndup(data, session_len);
+ memcpy(client->data, data+i, req.data_size);
+ conn->refcount++;
+ DLLIST_PREPEND(&conn->clients, client);
+
+ master_login_auth_request(login->auth, &req,
+ master_login_auth_callback, client);
+}
+
+void master_login_add(struct master_login *login, int fd)
+{
+ struct master_login_connection *conn;
+
+ conn = i_new(struct master_login_connection, 1);
+ conn->refcount = 1;
+ conn->login = login;
+ conn->create_time = ioloop_timeval;
+ conn->fd = fd;
+ conn->io = io_add(conn->fd, IO_READ, master_login_conn_input, conn);
+ conn->output = o_stream_create_fd(fd, SIZE_MAX);
+ o_stream_set_no_error_handling(conn->output, TRUE);
+
+ DLLIST_PREPEND(&login->conns, conn);
+
+ /* NOTE: currently there's a separate connection for each request. */
+}
+
+static void master_login_conn_close(struct master_login_connection *conn)
+{
+ if (master_login_conn_is_closed(conn))
+ return;
+
+ DLLIST_REMOVE(&conn->login->conns, conn);
+
+ io_remove(&conn->io);
+ o_stream_close(conn->output);
+ if (close(conn->fd) < 0)
+ i_error("close(master login) failed: %m");
+ conn->fd = -1;
+}
+
+static void master_login_conn_unref(struct master_login_connection **_conn)
+{
+ struct master_login_connection *conn = *_conn;
+
+ i_assert(conn->refcount > 0);
+
+ if (--conn->refcount > 0)
+ return;
+
+ *_conn = NULL;
+ i_assert(conn->clients == NULL);
+ master_login_conn_close(conn);
+ o_stream_unref(&conn->output);
+
+ if (!conn->login_success)
+ master_service_client_connection_destroyed(conn->login->service);
+ i_free(conn);
+}
+
+void master_login_stop(struct master_login *login)
+{
+ login->stopping = TRUE;
+ if (master_login_auth_request_count(login->auth) == 0) {
+ master_login_auth_disconnect(login->auth);
+ master_service_close_config_fd(login->service);
+ }
+}
diff --git a/src/lib-master/master-login.h b/src/lib-master/master-login.h
new file mode 100644
index 0000000..7029aa9
--- /dev/null
+++ b/src/lib-master/master-login.h
@@ -0,0 +1,50 @@
+#ifndef MASTER_LOGIN_H
+#define MASTER_LOGIN_H
+
+#include "master-auth.h"
+
+#define MASTER_POSTLOGIN_TIMEOUT_DEFAULT 60
+
+struct master_login_client {
+ /* parent connection */
+ struct master_login_connection *conn;
+ /* linked list of all clients within the connection */
+ struct master_login_client *prev, *next;
+ /* non-NULL while running postlogin script */
+ struct master_login_postlogin *postlogin_client;
+
+ int fd;
+ struct timeval create_time;
+
+ struct master_auth_request auth_req;
+ char *session_id;
+ unsigned char data[FLEXIBLE_ARRAY_MEMBER];
+};
+
+typedef void
+master_login_callback_t(const struct master_login_client *client,
+ const char *username, const char *const *extra_fields);
+typedef void
+master_login_failure_callback_t(const struct master_login_client *client,
+ const char *errormsg);
+
+struct master_login_settings {
+ const char *auth_socket_path;
+ const char *postlogin_socket_path;
+ unsigned int postlogin_timeout_secs;
+
+ master_login_callback_t *callback;
+ master_login_failure_callback_t *failure_callback;
+
+ bool request_auth_token:1;
+};
+
+struct master_login *
+master_login_init(struct master_service *service,
+ const struct master_login_settings *set);
+void master_login_deinit(struct master_login **login);
+
+void master_login_add(struct master_login *login, int fd);
+void master_login_stop(struct master_login *login);
+
+#endif
diff --git a/src/lib-master/master-service-haproxy.c b/src/lib-master/master-service-haproxy.c
new file mode 100644
index 0000000..c36935d
--- /dev/null
+++ b/src/lib-master/master-service-haproxy.c
@@ -0,0 +1,689 @@
+/* Copyright (c) 2013-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "llist.h"
+#include "ioloop.h"
+#include "str-sanitize.h"
+#include "master-service-private.h"
+#include "master-service-settings.h"
+
+#define HAPROXY_V1_MAX_HEADER_SIZE (108)
+
+#define PP2_TYPE_ALPN 0x01
+#define PP2_TYPE_AUTHORITY 0x02
+#define PP2_TYPE_CRC32C 0x03
+#define PP2_TYPE_NOOP 0x04
+#define PP2_TYPE_SSL 0x20
+#define PP2_SUBTYPE_SSL_VERSION 0x21
+#define PP2_SUBTYPE_SSL_CN 0x22
+#define PP2_SUBTYPE_SSL_CIPHER 0x23
+#define PP2_SUBTYPE_SSL_SIG_ALG 0x24
+#define PP2_SUBTYPE_SSL_KEY_ALG 0x25
+#define PP2_TYPE_NETNS 0x30
+
+#define PP2_CLIENT_SSL 0x01
+#define PP2_CLIENT_CERT_CONN 0x02
+#define PP2_CLIENT_CERT_SESS 0x04
+
+enum haproxy_version_t {
+ HAPROXY_VERSION_1,
+ HAPROXY_VERSION_2,
+};
+
+enum {
+ HAPROXY_CMD_LOCAL = 0x00,
+ HAPROXY_CMD_PROXY = 0x01
+};
+
+enum {
+ HAPROXY_AF_UNSPEC = 0x00,
+ HAPROXY_AF_INET = 0x01,
+ HAPROXY_AF_INET6 = 0x02,
+ HAPROXY_AF_UNIX = 0x03
+};
+
+enum {
+ HAPROXY_SOCK_UNSPEC = 0x00,
+ HAPROXY_SOCK_STREAM = 0x01,
+ HAPROXY_SOCK_DGRAM = 0x02
+};
+
+static const char haproxy_v2sig[12] =
+ "\x0D\x0A\x0D\x0A\x00\x0D\x0A\x51\x55\x49\x54\x0A";
+
+struct haproxy_header_v2 {
+ uint8_t sig[12];
+ uint8_t ver_cmd;
+ uint8_t fam;
+ uint16_t len;
+};
+
+struct haproxy_data_v2 {
+ union {
+ struct { /* for TCP/UDP over IPv4, len = 12 */
+ uint32_t src_addr;
+ uint32_t dst_addr;
+ uint16_t src_port;
+ uint16_t dst_port;
+ } ip4;
+ struct { /* for TCP/UDP over IPv6, len = 36 */
+ uint8_t src_addr[16];
+ uint8_t dst_addr[16];
+ uint16_t src_port;
+ uint16_t dst_port;
+ } ip6;
+ struct { /* for AF_UNIX sockets, len = 216 */
+ uint8_t src_addr[108];
+ uint8_t dst_addr[108];
+ } unx;
+ } addr;
+};
+
+#define SIZEOF_PP2_TLV (1U+2U)
+struct haproxy_pp2_tlv {
+ uint8_t type;
+ uint16_t len;
+ const unsigned char *data;
+};
+
+#define SIZEOF_PP2_TLV_SSL (1U+4U)
+struct haproxy_pp2_tlv_ssl {
+ uint8_t client;
+ uint32_t verify;
+
+ size_t len;
+ const unsigned char *data;
+};
+
+struct master_service_haproxy_conn {
+ struct master_service_connection conn;
+
+ pool_t pool;
+
+ struct master_service_haproxy_conn *prev, *next;
+
+ struct master_service *service;
+
+ struct io *io;
+ struct timeout *to;
+};
+
+static void
+master_service_haproxy_conn_free(struct master_service_haproxy_conn *hpconn)
+{
+ struct master_service *service = hpconn->service;
+
+ DLLIST_REMOVE(&service->haproxy_conns, hpconn);
+
+ io_remove(&hpconn->io);
+ timeout_remove(&hpconn->to);
+ pool_unref(&hpconn->pool);
+}
+
+static void
+master_service_haproxy_conn_failure(struct master_service_haproxy_conn *hpconn)
+{
+ struct master_service *service = hpconn->service;
+ struct master_service_connection conn = hpconn->conn;
+
+ master_service_haproxy_conn_free(hpconn);
+ master_service_client_connection_handled(service, &conn);
+}
+
+static void
+master_service_haproxy_conn_success(struct master_service_haproxy_conn *hpconn)
+{
+ struct master_service *service = hpconn->service;
+ struct master_service_connection conn = hpconn->conn;
+
+ master_service_haproxy_conn_free(hpconn);
+ master_service_client_connection_callback(service, &conn);
+}
+
+static void
+master_service_haproxy_timeout(struct master_service_haproxy_conn *hpconn)
+{
+ i_error("haproxy: Client timed out (rip=%s)",
+ net_ip2addr(&hpconn->conn.remote_ip));
+ master_service_haproxy_conn_failure(hpconn);
+}
+
+static int
+master_service_haproxy_recv(int fd, void *buf, size_t len, int flags)
+{
+ ssize_t ret;
+
+ do {
+ ret = recv(fd, buf, len, flags);
+ } while (ret < 0 && errno == EINTR);
+
+ if (ret < 0 && errno == EAGAIN)
+ return 0;
+ if (ret <= 0) {
+ if (ret == 0)
+ errno = ECONNRESET;
+ return -1;
+ }
+
+ return ret;
+}
+
+static int get_ssl_tlv(const unsigned char *kvdata, size_t dlen,
+ struct haproxy_pp2_tlv_ssl *kv)
+{
+ if (dlen < SIZEOF_PP2_TLV_SSL)
+ return -1;
+ kv->client = kvdata[0];
+ /* spec does not specify the endianess of this field */
+ kv->verify = cpu32_to_cpu_unaligned(kvdata+1);
+ kv->data = kvdata+SIZEOF_PP2_TLV_SSL;
+ kv->len = dlen - SIZEOF_PP2_TLV_SSL;
+ return 0;
+}
+
+static int get_tlv(const unsigned char *kvdata, size_t dlen,
+ struct haproxy_pp2_tlv *kv)
+{
+ if (dlen < SIZEOF_PP2_TLV)
+ return -1;
+
+ /* spec says
+ uint8_t type
+ uint8_t len_hi
+ uint8_t len_lo
+ so we combine the hi and lo here. */
+ kv->type = kvdata[0];
+ kv->len = (kvdata[1]<<8)+kvdata[2];
+ kv->data = kvdata + SIZEOF_PP2_TLV;
+
+ if (kv->len + SIZEOF_PP2_TLV > dlen)
+ return -1;
+
+ return 0;
+}
+
+static int
+master_service_haproxy_parse_ssl_tlv(struct master_service_haproxy_conn *hpconn,
+ const struct haproxy_pp2_tlv_ssl *ssl_kv,
+ const char **error_r)
+{
+ hpconn->conn.proxy.ssl = (ssl_kv->client & (PP2_CLIENT_SSL)) != 0;
+
+ /* try parse some more */
+ for(size_t i = 0; i < ssl_kv->len;) {
+ struct haproxy_pp2_tlv kv;
+ if (get_tlv(ssl_kv->data + i, ssl_kv->len - i, &kv) < 0) {
+ *error_r = t_strdup_printf("get_tlv(%zu) failed: "
+ "Truncated data", i);
+ return -1;
+ }
+ i += SIZEOF_PP2_TLV + kv.len;
+ switch(kv.type) {
+ /* we don't care about these */
+ case PP2_SUBTYPE_SSL_CIPHER:
+ case PP2_SUBTYPE_SSL_SIG_ALG:
+ case PP2_SUBTYPE_SSL_KEY_ALG:
+ break;
+ case PP2_SUBTYPE_SSL_CN:
+ hpconn->conn.proxy.cert_common_name =
+ p_strndup(hpconn->pool, kv.data, kv.len);
+ break;
+ }
+ }
+ return 0;
+}
+
+static int
+master_service_haproxy_parse_tlv(struct master_service_haproxy_conn *hpconn,
+ const unsigned char *buf, size_t blen,
+ const char **error_r)
+{
+ for(size_t i = 0; i < blen;) {
+ struct haproxy_pp2_tlv kv;
+ struct haproxy_pp2_tlv_ssl ssl_kv;
+
+ if (get_tlv(buf + i, blen - i, &kv) < 0) {
+ *error_r = t_strdup_printf("get_tlv(%zu) failed: "
+ "Truncated data", i);
+ return -1;
+ }
+
+ /* skip unsupported values */
+ switch(kv.type) {
+ case PP2_TYPE_ALPN:
+ hpconn->conn.proxy.alpn_size = kv.len;
+ hpconn->conn.proxy.alpn =
+ p_memdup(hpconn->pool, kv.data, kv.len);
+ break;
+ case PP2_TYPE_AUTHORITY:
+ /* store hostname somewhere */
+ hpconn->conn.proxy.hostname =
+ p_strndup(hpconn->pool, kv.data, kv.len);
+ break;
+ case PP2_TYPE_SSL:
+ if (get_ssl_tlv(kv.data, kv.len, &ssl_kv) < 0) {
+ *error_r = t_strdup_printf("get_ssl_tlv(%zu) failed: "
+ "Truncated data", i);
+ return -1;
+ }
+ if (master_service_haproxy_parse_ssl_tlv(hpconn, &ssl_kv, error_r)<0)
+ return -1;
+ break;
+ }
+ i += SIZEOF_PP2_TLV + kv.len;
+ }
+ return 0;
+}
+
+static int
+master_service_haproxy_read(struct master_service_haproxy_conn *hpconn)
+{
+ /* reasonable max size for haproxy data */
+ unsigned char rbuf[1500];
+ const char *error;
+ static union {
+ unsigned char v1_data[HAPROXY_V1_MAX_HEADER_SIZE];
+ struct {
+ const struct haproxy_header_v2 hdr;
+ const struct haproxy_data_v2 data;
+ } v2;
+ } buf;
+ struct ip_addr *real_remote_ip = &hpconn->conn.remote_ip;
+ int fd = hpconn->conn.fd;
+ struct ip_addr local_ip, remote_ip;
+ in_port_t local_port, remote_port;
+ size_t size,i,want;
+ ssize_t ret;
+ enum haproxy_version_t version;
+
+ /* the protocol specification explicitly states that the protocol header
+ must be sent as one TCP frame, meaning that we will get it in full
+ with the first recv() call.
+ */
+ i_zero(&buf);
+ i_zero(&rbuf);
+
+ /* see if there is a HAPROXY protocol command waiting */
+ if ((ret = master_service_haproxy_recv(fd, &buf, sizeof(buf), MSG_PEEK))<=0) {
+ if (ret < 0)
+ i_info("haproxy: Client disconnected (rip=%s): %m",
+ net_ip2addr(real_remote_ip));
+ return ret;
+ /* see if there is a haproxy command, 8 is used later on as well */
+ } else if (ret >= 8 && memcmp(buf.v1_data, "PROXY", 5) == 0) {
+ /* fine */
+ version = HAPROXY_VERSION_1;
+ } else if ((size_t)ret >= sizeof(buf.v2.hdr) &&
+ memcmp(buf.v2.hdr.sig, haproxy_v2sig, sizeof(haproxy_v2sig)) == 0) {
+ want = ntohs(buf.v2.hdr.len) + sizeof(buf.v2.hdr);
+ if (want > sizeof(rbuf)) {
+ i_error("haproxy: Client disconnected: Too long header (rip=%s)",
+ net_ip2addr(real_remote_ip));
+ return -1;
+ }
+
+ if ((ret = master_service_haproxy_recv(fd, rbuf, want, MSG_WAITALL))<=0) {
+ if (ret < 0)
+ i_info("haproxy: Client disconnected (rip=%s): %m",
+ net_ip2addr(real_remote_ip));
+ return ret;
+ }
+
+ if (ret != (ssize_t)want) {
+ i_info("haproxy: Client disconnected: Failed to read full header (rip=%s)",
+ net_ip2addr(real_remote_ip));
+ return -1;
+ }
+ memcpy(&buf, rbuf, sizeof(buf));
+ version = HAPROXY_VERSION_2;
+ } else {
+ /* it wasn't haproxy data */
+ i_error("haproxy: Client disconnected: "
+ "Failed to read valid HAproxy data (rip=%s)",
+ net_ip2addr(real_remote_ip));
+ return -1;
+ }
+
+ /* don't update true connection data until we succeed */
+ local_ip = hpconn->conn.local_ip;
+ remote_ip = hpconn->conn.remote_ip;
+ local_port = hpconn->conn.local_port;
+ remote_port = hpconn->conn.remote_port;
+
+ /* protocol version 2 */
+ if (version == HAPROXY_VERSION_2) {
+ const struct haproxy_header_v2 *hdr = &buf.v2.hdr;
+ const struct haproxy_data_v2 *data = &buf.v2.data;
+ size_t hdr_len;
+
+ i_assert(ret >= (ssize_t)sizeof(buf.v2.hdr));
+
+ if ((hdr->ver_cmd & 0xf0) != 0x20) {
+ i_error("haproxy: Client disconnected: "
+ "Unsupported protocol version (version=%02x, rip=%s)",
+ (hdr->ver_cmd & 0xf0) >> 4,
+ net_ip2addr(real_remote_ip));
+ return -1;
+ }
+
+ hdr_len = ntohs(hdr->len);
+ size = sizeof(*hdr) + hdr_len;
+ /* keep tab of how much address data there really is because
+ because TLVs begin after that. */
+ i = 0;
+
+ if (ret < (ssize_t)size) {
+ i_error("haproxy(v2): Client disconnected: "
+ "Protocol payload length does not match header "
+ "(got=%zu, expect=%zu, rip=%s)",
+ (size_t)ret, size, net_ip2addr(real_remote_ip));
+ return -1;
+ }
+
+ i += sizeof(*hdr);
+
+ switch (hdr->ver_cmd & 0x0f) {
+ case HAPROXY_CMD_LOCAL:
+ /* keep local connection address for LOCAL */
+ /*i_debug("haproxy(v2): Local connection (rip=%s)",
+ net_ip2addr(real_remote_ip));*/
+ i = size; /* we should skip all the remaining data which can be present in PROXY protocol */
+ break;
+ case HAPROXY_CMD_PROXY:
+ if ((hdr->fam & 0x0f) != HAPROXY_SOCK_STREAM) {
+ /* UDP makes no sense currently */
+ i_error("haproxy(v2): Client disconnected: "
+ "Not using TCP (type=%02x, rip=%s)",
+ (hdr->fam & 0x0f), net_ip2addr(real_remote_ip));
+ return -1;
+ }
+ switch ((hdr->fam & 0xf0) >> 4) {
+ case HAPROXY_AF_INET:
+ /* IPv4 */
+ if (hdr_len < sizeof(data->addr.ip4)) {
+ i_error("haproxy(v2): Client disconnected: "
+ "IPv4 data is incomplete (rip=%s)",
+ net_ip2addr(real_remote_ip));
+ return -1;
+ }
+ local_ip.family = AF_INET;
+ local_ip.u.ip4.s_addr = data->addr.ip4.dst_addr;
+ local_port = ntohs(data->addr.ip4.dst_port);
+ remote_ip.family = AF_INET;
+ remote_ip.u.ip4.s_addr = data->addr.ip4.src_addr;
+ remote_port = ntohs(data->addr.ip4.src_port);
+ i += sizeof(data->addr.ip4);
+ break;
+ case HAPROXY_AF_INET6:
+ /* IPv6 */
+ if (hdr_len < sizeof(data->addr.ip6)) {
+ i_error("haproxy(v2): Client disconnected: "
+ "IPv6 data is incomplete (rip=%s)",
+ net_ip2addr(real_remote_ip));
+ return -1;
+ }
+ local_ip.family = AF_INET6;
+ memcpy(&local_ip.u.ip6.s6_addr, data->addr.ip6.dst_addr, 16);
+ local_port = ntohs(data->addr.ip6.dst_port);
+ remote_ip.family = AF_INET6;
+ memcpy(&remote_ip.u.ip6.s6_addr, data->addr.ip6.src_addr, 16);
+ remote_port = ntohs(data->addr.ip6.src_port);
+ i += sizeof(data->addr.ip6);
+ break;
+ case HAPROXY_AF_UNSPEC:
+ case HAPROXY_AF_UNIX:
+ /* unsupported; ignored */
+ i_error("haproxy(v2): Unsupported address family "
+ "(family=%02x, rip=%s)", (hdr->fam & 0xf0) >> 4,
+ net_ip2addr(real_remote_ip));
+ break;
+ default:
+ /* unsupported; error */
+ i_error("haproxy(v2): Client disconnected: "
+ "Unknown address family "
+ "(family=%02x, rip=%s)", (hdr->fam & 0xf0) >> 4,
+ net_ip2addr(real_remote_ip));
+ return -1;
+ }
+ break;
+ default:
+ i_error("haproxy(v2): Client disconnected: "
+ "Invalid command (cmd=%02x, rip=%s)",
+ (hdr->ver_cmd & 0x0f),
+ net_ip2addr(real_remote_ip));
+ return -1; /* not a supported command */
+ }
+
+ if (i > size) {
+ i_error("haproxy(v2): Client disconnected: "
+ "Invalid header size (size=%zu, tlv offset=%zu)",
+ size, i);
+ return -1; /* not a supported command */
+ }
+ if (master_service_haproxy_parse_tlv(hpconn, rbuf+i, size-i, &error) < 0) {
+ i_error("haproxy(v2): Client disconnected: "
+ "Invalid TLV: %s (cmd=%02x, rip=%s)",
+ error,
+ (hdr->ver_cmd & 0x0f),
+ net_ip2addr(real_remote_ip));
+ return -1;
+ }
+ /* protocol version 1 (soon obsolete) */
+ } else if (version == HAPROXY_VERSION_1) {
+ unsigned char *data = buf.v1_data, *end;
+ const char *const *fields;
+ unsigned int family = 0;
+
+ i_assert(ret >= 8);
+
+ /* find end of header line */
+ end = memchr(data, '\r', ret - 1);
+ if (end == NULL || end[1] != '\n')
+ return -1;
+ *end = '\0';
+ size = end + 2 - data;
+
+ /* magic */
+ fields = t_strsplit((char *)data, " ");
+ i_assert(strcmp(*fields, "PROXY") == 0);
+ fields++;
+
+ /* protocol */
+ if (*fields == NULL) {
+ i_error("haproxy(v1): Client disconnected: "
+ "Field for proxied protocol is missing "
+ "(rip=%s)", net_ip2addr(real_remote_ip));
+ return -1;
+ }
+ if (strcmp(*fields, "TCP4") == 0) {
+ family = AF_INET;
+ } else if (strcmp(*fields, "TCP6") == 0) {
+ family = AF_INET6;
+ } else if (strcmp(*fields, "UNKNOWN") == 0) {
+ family = 0;
+ } else {
+ i_error("haproxy(v1): Client disconnected: "
+ "Unknown proxied protocol "
+ "(protocol=`%s', rip=%s)", str_sanitize(*fields, 64),
+ net_ip2addr(real_remote_ip));
+ return -1;
+ }
+ fields++;
+
+ if (family != 0) {
+ /* remote address */
+ if (*fields == NULL) {
+ i_error("haproxy(v1): Client disconnected: "
+ "Field for proxied remote address is missing "
+ "(rip=%s)", net_ip2addr(real_remote_ip));
+ return -1;
+ }
+ if (net_addr2ip(*fields, &remote_ip) < 0 ||
+ remote_ip.family != family) {
+ i_error("haproxy(v1): Client disconnected: "
+ "Proxied remote address is invalid "
+ "(address=`%s', rip=%s)", str_sanitize(*fields, 64),
+ net_ip2addr(real_remote_ip));
+ return -1;
+ }
+ fields++;
+
+ /* local address */
+ if (*fields == NULL) {
+ i_error("haproxy(v1): Client disconnected: "
+ "Field for proxied local address is missing "
+ "(rip=%s)", net_ip2addr(real_remote_ip));
+ return -1;
+ }
+ if (net_addr2ip(*fields, &local_ip) < 0 ||
+ local_ip.family != family) {
+ i_error("haproxy(v1): Client disconnected: "
+ "Proxied local address is invalid "
+ "(address=`%s', rip=%s)", str_sanitize(*fields, 64),
+ net_ip2addr(real_remote_ip));
+ return -1;
+ }
+ fields++;
+
+ /* remote port */
+ if (*fields == NULL) {
+ i_error("haproxy(v1): Client disconnected: "
+ "Field for proxied local port is missing "
+ "(rip=%s)", net_ip2addr(real_remote_ip));
+ return -1;
+ }
+ if (net_str2port(*fields, &remote_port) < 0) {
+ i_error("haproxy(v1): Client disconnected: "
+ "Proxied remote port is invalid "
+ "(port=`%s', rip=%s)", str_sanitize(*fields, 64),
+ net_ip2addr(real_remote_ip));
+ return -1;
+ }
+ fields++;
+
+ /* local port */
+ if (*fields == NULL) {
+ i_error("haproxy(v1): Client disconnected: "
+ "Field for proxied local port is missing "
+ "(rip=%s)", net_ip2addr(real_remote_ip));
+ return -1;
+ }
+ if (net_str2port(*fields, &local_port) < 0) {
+ i_error("haproxy(v1): Client disconnected: "
+ "Proxied local port is invalid "
+ "(port=`%s', rip=%s)", str_sanitize(*fields, 64),
+ net_ip2addr(real_remote_ip));
+ return -1;
+ }
+ fields++;
+
+ if (*fields != NULL) {
+ i_error("haproxy(v1): Client disconnected: "
+ "Header line has spurius extra field "
+ "(field=`%s', rip=%s)", str_sanitize(*fields, 64),
+ net_ip2addr(real_remote_ip));
+ return -1;
+ }
+ }
+ i_assert(size <= sizeof(buf));
+
+ if ((ret = master_service_haproxy_recv(fd, &buf, size, 0))<=0) {
+ if (ret < 0)
+ i_info("haproxy: Client disconnected (rip=%s): %m",
+ net_ip2addr(real_remote_ip));
+ return ret;
+ } else if (ret != (ssize_t)size) {
+ i_error("haproxy: Client disconnected: "
+ "Failed to read full header (rip=%s)",
+ net_ip2addr(real_remote_ip));
+ return -1;
+ }
+ /* invalid protocol */
+ } else {
+ i_unreached();
+ }
+
+ /* assign data from proxy */
+ hpconn->conn.local_ip = local_ip;
+ hpconn->conn.remote_ip = remote_ip;
+ hpconn->conn.local_port = local_port;
+ hpconn->conn.remote_port = remote_port;
+ hpconn->conn.proxied = TRUE;
+
+ return 1;
+}
+
+static void
+master_service_haproxy_input(struct master_service_haproxy_conn *hpconn)
+{
+ int ret;
+
+ if ((ret = master_service_haproxy_read(hpconn)) <= 0) {
+ if (ret < 0)
+ master_service_haproxy_conn_failure(hpconn);
+ } else {
+ master_service_haproxy_conn_success(hpconn);
+ }
+}
+
+static bool
+master_service_haproxy_conn_is_trusted(struct master_service *service,
+ struct master_service_connection *conn)
+{
+ const char *const *net;
+ struct ip_addr net_ip;
+ unsigned int bits;
+
+ if (service->set->haproxy_trusted_networks == NULL)
+ return FALSE;
+
+ net = t_strsplit_spaces(service->set->haproxy_trusted_networks, ", ");
+ for (; *net != NULL; net++) {
+ if (net_parse_range(*net, &net_ip, &bits) < 0) {
+ i_error("haproxy_trusted_networks: "
+ "Invalid network '%s'", *net);
+ break;
+ }
+
+ if (net_is_in_network(&conn->real_remote_ip, &net_ip, bits))
+ return TRUE;
+ }
+ return FALSE;
+}
+
+void master_service_haproxy_new(struct master_service *service,
+ struct master_service_connection *conn)
+{
+ struct master_service_haproxy_conn *hpconn;
+ pool_t pool;
+
+ if (!master_service_haproxy_conn_is_trusted(service, conn)) {
+ i_warning("haproxy: Client not trusted (rip=%s)",
+ net_ip2addr(&conn->real_remote_ip));
+ master_service_client_connection_handled(service, conn);
+ return;
+ }
+
+ pool = pool_alloconly_create("haproxy connection", 128);
+ hpconn = p_new(pool, struct master_service_haproxy_conn, 1);
+ hpconn->pool = pool;
+ hpconn->conn = *conn;
+ hpconn->service = service;
+ DLLIST_PREPEND(&service->haproxy_conns, hpconn);
+
+ hpconn->io = io_add(conn->fd, IO_READ,
+ master_service_haproxy_input, hpconn);
+ hpconn->to = timeout_add(service->set->haproxy_timeout*1000,
+ master_service_haproxy_timeout, hpconn);
+}
+
+void master_service_haproxy_abort(struct master_service *service)
+{
+ while (service->haproxy_conns != NULL) {
+ int fd = service->haproxy_conns->conn.fd;
+
+ master_service_haproxy_conn_free(service->haproxy_conns);
+ i_close_fd(&fd);
+ }
+}
+
diff --git a/src/lib-master/master-service-private.h b/src/lib-master/master-service-private.h
new file mode 100644
index 0000000..429f2eb
--- /dev/null
+++ b/src/lib-master/master-service-private.h
@@ -0,0 +1,109 @@
+#ifndef MASTER_SERVICE_PRIVATE_H
+#define MASTER_SERVICE_PRIVATE_H
+
+#include "master-interface.h"
+#include "master-service.h"
+
+struct master_service_haproxy_conn;
+
+struct master_service_listener {
+ struct master_service *service;
+ char *name;
+
+ /* settings */
+ bool ssl;
+ bool haproxy;
+
+ /* state */
+ bool closed;
+ int fd;
+ struct io *io;
+};
+
+struct master_service {
+ struct ioloop *ioloop;
+
+ char *name;
+ char *configured_name;
+ char *getopt_str;
+ enum master_service_flags flags;
+
+ int argc;
+ char **argv;
+
+ const char *version_string;
+ char *config_path;
+ ARRAY_TYPE(const_string) config_overrides;
+ int config_fd;
+ int syslog_facility;
+ data_stack_frame_t datastack_frame_id;
+
+ struct master_service_listener *listeners;
+ unsigned int socket_count;
+
+ struct io *io_status_write, *io_status_error;
+ unsigned int service_count_left;
+ unsigned int total_available_count;
+ unsigned int process_limit;
+ unsigned int process_min_avail;
+ unsigned int idle_kill_secs;
+
+ struct master_status master_status;
+ unsigned int last_sent_status_avail_count;
+ time_t last_sent_status_time;
+ struct timeout *to_status;
+
+ bool (*idle_die_callback)(void);
+ void (*die_callback)(void);
+ struct timeout *to_die;
+
+ master_service_avail_overflow_callback_t *avail_overflow_callback;
+ struct timeout *to_overflow_state, *to_overflow_call;
+
+ struct master_login *login;
+
+ master_service_connection_callback_t *callback;
+
+ pool_t set_pool;
+ const struct master_service_settings *set;
+ struct setting_parser_context *set_parser;
+
+ struct ssl_iostream_context *ssl_ctx;
+ time_t ssl_params_last_refresh;
+
+ struct stats_client *stats_client;
+ struct master_service_haproxy_conn *haproxy_conns;
+ struct event_filter *process_shutdown_filter;
+
+ bool killed:1;
+ bool stopping:1;
+ bool keep_environment:1;
+ bool log_directly:1;
+ bool initial_status_sent:1;
+ bool die_with_master:1;
+ bool call_avail_overflow:1;
+ bool config_path_changed_with_param:1;
+ bool want_ssl_server:1;
+ bool ssl_ctx_initialized:1;
+ bool config_path_from_master:1;
+ bool log_initialized:1;
+ bool init_finished:1;
+};
+
+void master_service_io_listeners_add(struct master_service *service);
+void master_status_update(struct master_service *service);
+void master_service_close_config_fd(struct master_service *service);
+
+void master_service_io_listeners_remove(struct master_service *service);
+void master_service_ssl_io_listeners_remove(struct master_service *service);
+
+void master_service_client_connection_handled(struct master_service *service,
+ struct master_service_connection *conn);
+void master_service_client_connection_callback(struct master_service *service,
+ struct master_service_connection *conn);
+
+void master_service_haproxy_new(struct master_service *service,
+ struct master_service_connection *conn);
+void master_service_haproxy_abort(struct master_service *service);
+
+#endif
diff --git a/src/lib-master/master-service-settings-cache.c b/src/lib-master/master-service-settings-cache.c
new file mode 100644
index 0000000..11dd66b
--- /dev/null
+++ b/src/lib-master/master-service-settings-cache.c
@@ -0,0 +1,410 @@
+/* Copyright (c) 2010-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "wildcard-match.h"
+#include "hash.h"
+#include "llist.h"
+#include "settings-parser.h"
+#include "dns-util.h"
+#include "strescape.h"
+#include "master-service-private.h"
+#include "master-service-settings.h"
+#include "master-service-settings-cache.h"
+
+/* we start with just a guess. it's updated later. */
+#define CACHE_INITIAL_ENTRY_POOL_SIZE (1024*16)
+#define CACHE_ADD_ENTRY_POOL_SIZE 1024
+
+struct config_filter {
+ struct config_filter *prev, *next;
+
+ const char *local_name;
+ struct ip_addr local_ip, remote_ip;
+ unsigned int local_bits, remote_bits;
+};
+
+struct settings_entry {
+ struct settings_entry *prev, *next;
+
+ pool_t pool;
+ const char *local_name;
+ struct ip_addr local_ip;
+
+ struct setting_parser_context *parser;
+};
+
+struct master_service_settings_cache {
+ pool_t pool;
+
+ struct master_service *service;
+ const char *module;
+ const char *service_name;
+ size_t max_cache_size;
+
+ /* global settings for this service (after they've been read) */
+ struct setting_parser_context *global_parser;
+
+ /* cache for other settings (local_ip/local_name set) */
+ struct settings_entry *oldest, *newest;
+ /* separate list for entries whose parser=global_parser */
+ struct settings_entry *oldest_global, *newest_global;
+ /* local_name, local_ip => struct settings_entry */
+ HASH_TABLE(char *, struct settings_entry *) local_name_hash;
+ HASH_TABLE(struct ip_addr *, struct settings_entry *) local_ip_hash;
+
+ struct config_filter *filters;
+
+ /* Initial size for new settings entry pools */
+ size_t approx_entry_pool_size;
+ /* number of bytes malloced by cached settings entries
+ (doesn't count memory used by hash table or global sets) */
+ size_t cache_malloc_size;
+
+ bool done_initial_lookup:1;
+ bool service_uses_local:1;
+ bool service_uses_remote:1;
+};
+
+struct master_service_settings_cache *
+master_service_settings_cache_init(struct master_service *service,
+ const char *module, const char *service_name)
+{
+ struct master_service_settings_cache *cache;
+ pool_t pool;
+
+ pool = pool_alloconly_create(MEMPOOL_GROWING"master service settings cache",
+ 1024*12);
+ cache = p_new(pool, struct master_service_settings_cache, 1);
+ cache->pool = pool;
+ cache->service = service;
+ cache->module = p_strdup(pool, module);
+ cache->service_name = p_strdup(pool, service_name);
+ cache->max_cache_size = SIZE_MAX;
+ return cache;
+}
+
+int master_service_settings_cache_init_filter(struct master_service_settings_cache *cache)
+{
+ const char *const *filters;
+ const char *error;
+
+ if (cache->filters != NULL)
+ return 0;
+ if (master_service_settings_get_filters(cache->service, &filters, &error) < 0) {
+ i_error("master-service: cannot get filters: %s", error);
+ return -1;
+ }
+
+ /* parse filters */
+ while(*filters != NULL) {
+ const char *const *keys = t_strsplit_tabescaped(*filters);
+ struct config_filter *filter =
+ p_new(cache->pool, struct config_filter, 1);
+ while(*keys != NULL) {
+ if (str_begins(*keys, "local-net=")) {
+ (void)net_parse_range((*keys)+10,
+ &filter->local_ip, &filter->local_bits);
+ } else if (str_begins(*keys, "remote-net=")) {
+ (void)net_parse_range((*keys)+11,
+ &filter->remote_ip, &filter->remote_bits);
+ } else if (str_begins(*keys, "local-name=")) {
+ filter->local_name = p_strdup(cache->pool, (*keys)+11);
+ }
+ keys++;
+ }
+ DLLIST_PREPEND(&cache->filters, filter);
+ filters++;
+ }
+ return 0;
+}
+
+static bool
+match_local_name(const char *local_name,
+ const char *filter_local_name)
+{
+ /* Handle multiple names separated by spaces in local_name
+ * Ex: local_name "mail.domain.tld domain.tld mx.domain.tld" { ... } */
+ const char *ptr;
+ while((ptr = strchr(filter_local_name, ' ')) != NULL) {
+ if (dns_match_wildcard(local_name,
+ t_strdup_until(filter_local_name, ptr)) == 0)
+ return TRUE;
+ filter_local_name = ptr+1;
+ }
+ return dns_match_wildcard(local_name, filter_local_name) == 0;
+}
+
+/* Remove any elements which there is no filter for */
+static void
+master_service_settings_cache_fix_input(struct master_service_settings_cache *cache,
+ const struct master_service_settings_input *input,
+ struct master_service_settings_input *new_input)
+{
+ bool found_lip, found_rip, found_local_name;
+
+ found_lip = found_rip = found_local_name = FALSE;
+
+ struct config_filter *filter = cache->filters;
+ while(filter != NULL) {
+ if (filter->local_bits > 0 &&
+ net_is_in_network(&input->local_ip, &filter->local_ip,
+ filter->local_bits))
+ found_lip = TRUE;
+ if (filter->remote_bits > 0 &&
+ net_is_in_network(&input->remote_ip, &filter->remote_ip,
+ filter->remote_bits))
+ found_rip = TRUE;
+ if (input->local_name != NULL && filter->local_name != NULL &&
+ match_local_name(input->local_name, filter->local_name))
+ found_local_name = TRUE;
+ filter = filter->next;
+ };
+
+ *new_input = *input;
+
+ if (!found_lip)
+ i_zero(&new_input->local_ip);
+ if (!found_rip)
+ i_zero(&new_input->remote_ip);
+ if (!found_local_name)
+ new_input->local_name = NULL;
+}
+
+
+void master_service_settings_cache_deinit(struct master_service_settings_cache **_cache)
+{
+ struct master_service_settings_cache *cache = *_cache;
+ struct settings_entry *entry, *next;
+
+ /* parsers need to be deinitialized, because they reference the pool */
+ for (entry = cache->oldest_global; entry != NULL; entry = next) {
+ next = entry->next;
+ i_assert(entry->parser == cache->global_parser);
+ pool_unref(&entry->pool);
+ }
+ for (entry = cache->oldest; entry != NULL; entry = next) {
+ next = entry->next;
+ i_assert(entry->parser != cache->global_parser);
+ settings_parser_deinit(&entry->parser);
+ pool_unref(&entry->pool);
+ }
+ hash_table_destroy(&cache->local_name_hash);
+ hash_table_destroy(&cache->local_ip_hash);
+ if (cache->global_parser != NULL)
+ settings_parser_deinit(&cache->global_parser);
+ pool_unref(&cache->pool);
+}
+
+static bool
+cache_can_return_global(struct master_service_settings_cache *cache,
+ const struct master_service_settings_input *input)
+{
+ if (cache->service_uses_local) {
+ if (input->local_name != NULL || input->local_ip.family != 0)
+ return FALSE;
+ }
+ if (cache->service_uses_remote) {
+ if (input->remote_ip.family != 0)
+ return FALSE;
+ }
+ return TRUE;
+}
+
+static bool
+cache_find(struct master_service_settings_cache *cache,
+ const struct master_service_settings_input *input,
+ const struct setting_parser_context **parser_r)
+{
+ struct settings_entry *entry = NULL;
+
+ if (!cache->done_initial_lookup)
+ return FALSE;
+
+ if (cache_can_return_global(cache, input)) {
+ if (cache->global_parser != NULL) {
+ *parser_r = cache->global_parser;
+ return TRUE;
+ }
+ return FALSE;
+ }
+
+ if (cache->service_uses_remote)
+ return FALSE;
+
+ /* see if we have it already in cache. if local_name is specified,
+ don't even try to use local_ip (even though we have it), because
+ there may be different settings specifically for local_name */
+ if (input->local_name != NULL) {
+ if (hash_table_is_created(cache->local_name_hash)) {
+ entry = hash_table_lookup(cache->local_name_hash,
+ input->local_name);
+ }
+ } else if (hash_table_is_created(cache->local_ip_hash) &&
+ input->local_ip.family != 0) {
+ entry = hash_table_lookup(cache->local_ip_hash,
+ &input->local_ip);
+ }
+
+ if (entry != NULL) {
+ if (entry->parser != cache->global_parser) {
+ DLLIST2_REMOVE(&cache->oldest, &cache->newest, entry);
+ DLLIST2_APPEND(&cache->oldest, &cache->newest, entry);
+ }
+ *parser_r = entry->parser;
+ return TRUE;
+ }
+ return FALSE;
+}
+
+static void
+setting_entry_detach(struct master_service_settings_cache *cache,
+ struct settings_entry *entry)
+{
+ DLLIST2_REMOVE(&cache->oldest, &cache->newest, entry);
+ cache->cache_malloc_size -=
+ pool_alloconly_get_total_alloc_size(entry->pool);
+
+ if (entry->local_name != NULL)
+ hash_table_remove(cache->local_name_hash, entry->local_name);
+ else if (entry->local_ip.family != 0)
+ hash_table_remove(cache->local_ip_hash, &entry->local_ip);
+ settings_parser_deinit(&entry->parser);
+}
+
+static struct setting_parser_context *
+cache_add(struct master_service_settings_cache *cache,
+ const struct master_service_settings_input *input,
+ const struct master_service_settings_output *output,
+ struct setting_parser_context *parser)
+{
+ struct settings_entry *entry;
+ pool_t pool;
+ size_t pool_size;
+ char *entry_local_name;
+
+ if (!output->used_local && !output->used_remote) {
+ /* these are same as global settings */
+ if (cache->global_parser == NULL) {
+ cache->global_parser =
+ settings_parser_dup(parser, cache->pool);
+ }
+ }
+ if (cache->service_uses_remote) {
+ /* for now we don't try to handle caching remote IPs */
+ return parser;
+ }
+
+ if (input->local_name == NULL && input->local_ip.family == 0)
+ return parser;
+
+ if (!output->used_local) {
+ /* use global settings, but add local_ip/host to hash tables
+ so we'll find them */
+ pool = pool_alloconly_create("settings global entry", 256);
+ } else if (cache->cache_malloc_size >= cache->max_cache_size) {
+ /* free the oldest and reuse its pool */
+ pool = cache->oldest->pool;
+ setting_entry_detach(cache, cache->oldest);
+ p_clear(pool); /* note: frees also entry */
+ } else {
+ pool_size = cache->approx_entry_pool_size != 0 ?
+ cache->approx_entry_pool_size :
+ CACHE_INITIAL_ENTRY_POOL_SIZE;
+ pool = pool_alloconly_create("settings entry", pool_size);
+ }
+ entry = p_new(pool, struct settings_entry, 1);
+ entry->pool = pool;
+ entry_local_name = p_strdup(pool, input->local_name);
+ entry->local_name = entry_local_name;
+ entry->local_ip = input->local_ip;
+ if (!output->used_local) {
+ entry->parser = cache->global_parser;
+ DLLIST2_APPEND(&cache->oldest_global, &cache->newest_global,
+ entry);
+ } else {
+ entry->parser = settings_parser_dup(parser, entry->pool);
+ DLLIST2_APPEND(&cache->oldest, &cache->newest, entry);
+
+ pool_size = pool_alloconly_get_total_used_size(pool);
+ if (pool_size > cache->approx_entry_pool_size) {
+ cache->approx_entry_pool_size = pool_size +
+ CACHE_ADD_ENTRY_POOL_SIZE;
+ }
+ }
+ cache->cache_malloc_size += pool_alloconly_get_total_alloc_size(pool);
+
+ if (input->local_name != NULL) {
+ if (!hash_table_is_created(cache->local_name_hash)) {
+ hash_table_create(&cache->local_name_hash,
+ cache->pool, 0, str_hash, strcmp);
+ }
+ i_assert(hash_table_lookup(cache->local_name_hash,
+ entry_local_name) == NULL);
+ hash_table_insert(cache->local_name_hash,
+ entry_local_name, entry);
+ } else if (input->local_ip.family != 0) {
+ if (!hash_table_is_created(cache->local_ip_hash)) {
+ hash_table_create(&cache->local_ip_hash, cache->pool, 0,
+ net_ip_hash, net_ip_cmp);
+ }
+ i_assert(hash_table_lookup(cache->local_ip_hash,
+ &entry->local_ip) == NULL);
+ hash_table_insert(cache->local_ip_hash,
+ &entry->local_ip, entry);
+ }
+ return entry->parser;
+}
+
+int master_service_settings_cache_read(struct master_service_settings_cache *cache,
+ const struct master_service_settings_input *input,
+ const struct dynamic_settings_parser *dyn_parsers,
+ const struct setting_parser_context **parser_r,
+ const char **error_r)
+{
+ struct master_service_settings_output output;
+ struct master_service_settings_input new_input;
+ const struct master_service_settings *set;
+
+ i_assert(null_strcmp(input->module, cache->module) == 0);
+ i_assert(null_strcmp(input->service, cache->service_name) == 0);
+
+ if (cache_find(cache, input, parser_r))
+ return 0;
+
+ new_input = *input;
+ if (cache->filters != NULL) {
+ master_service_settings_cache_fix_input(cache, input, &new_input);
+ if (cache_find(cache, &new_input, parser_r))
+ return 0;
+ }
+
+ if (dyn_parsers != NULL) {
+ settings_parser_dyn_update(cache->pool, &new_input.roots,
+ dyn_parsers);
+ }
+ if (master_service_settings_read(cache->service, &new_input,
+ &output, error_r) < 0)
+ return -1;
+
+ if (!cache->done_initial_lookup) {
+ cache->done_initial_lookup = TRUE;
+ cache->service_uses_local = output.service_uses_local;
+ cache->service_uses_remote = output.service_uses_remote;
+
+ set = master_service_settings_get(cache->service);
+ cache->max_cache_size = set->config_cache_size;
+ }
+
+ if (output.used_local && !cache->service_uses_local) {
+ *error_r = "BUG: config unexpectedly returned local settings";
+ return -1;
+ }
+ if (output.used_remote && !cache->service_uses_remote) {
+ *error_r = "BUG: config unexpectedly returned remote settings";
+ return -1;
+ }
+
+ *parser_r = cache_add(cache, &new_input, &output,
+ cache->service->set_parser);
+ return 0;
+}
diff --git a/src/lib-master/master-service-settings-cache.h b/src/lib-master/master-service-settings-cache.h
new file mode 100644
index 0000000..157132e
--- /dev/null
+++ b/src/lib-master/master-service-settings-cache.h
@@ -0,0 +1,16 @@
+#ifndef MASTER_SERVICE_SETTINGS_CACHE_H
+#define MASTER_SERVICE_SETTINGS_CACHE_H
+
+struct master_service_settings_cache *
+master_service_settings_cache_init(struct master_service *service,
+ const char *module,
+ const char *service_name);
+void master_service_settings_cache_deinit(struct master_service_settings_cache **cache);
+int master_service_settings_cache_init_filter(struct master_service_settings_cache *cache);
+int master_service_settings_cache_read(struct master_service_settings_cache *cache,
+ const struct master_service_settings_input *input,
+ const struct dynamic_settings_parser *dyn_parsers,
+ const struct setting_parser_context **parser_r,
+ const char **error_r) ATTR_NULL(3);
+
+#endif
diff --git a/src/lib-master/master-service-settings.c b/src/lib-master/master-service-settings.c
new file mode 100644
index 0000000..f0f0797
--- /dev/null
+++ b/src/lib-master/master-service-settings.c
@@ -0,0 +1,816 @@
+/* Copyright (c) 2005-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "event-filter.h"
+#include "path-util.h"
+#include "istream.h"
+#include "write-full.h"
+#include "str.h"
+#include "strescape.h"
+#include "syslog-util.h"
+#include "eacces-error.h"
+#include "env-util.h"
+#include "execv-const.h"
+#include "settings-parser.h"
+#include "stats-client.h"
+#include "master-service-private.h"
+#include "master-service-ssl-settings.h"
+#include "master-service-settings.h"
+
+#include <stddef.h>
+#include <unistd.h>
+#include <time.h>
+#include <sys/stat.h>
+
+#define DOVECOT_CONFIG_BIN_PATH BINDIR"/doveconf"
+#define DOVECOT_CONFIG_SOCKET_PATH PKG_RUNDIR"/config"
+
+#define CONFIG_READ_TIMEOUT_SECS 10
+#define CONFIG_HANDSHAKE "VERSION\tconfig\t2\t0\n"
+
+#undef DEF
+#define DEF(type, name) \
+ SETTING_DEFINE_STRUCT_##type(#name, name, struct master_service_settings)
+
+static bool
+master_service_settings_check(void *_set, pool_t pool, const char **error_r);
+
+static const struct setting_define master_service_setting_defines[] = {
+ DEF(STR, base_dir),
+ DEF(STR, state_dir),
+ DEF(STR, instance_name),
+ DEF(STR, log_path),
+ DEF(STR, info_log_path),
+ DEF(STR, debug_log_path),
+ DEF(STR, log_timestamp),
+ DEF(STR, log_debug),
+ DEF(STR, log_core_filter),
+ DEF(STR, process_shutdown_filter),
+ DEF(STR, syslog_facility),
+ DEF(STR, import_environment),
+ DEF(STR, stats_writer_socket_path),
+ DEF(SIZE, config_cache_size),
+ DEF(BOOL, version_ignore),
+ DEF(BOOL, shutdown_clients),
+ DEF(BOOL, verbose_proctitle),
+
+ DEF(STR, haproxy_trusted_networks),
+ DEF(TIME, haproxy_timeout),
+
+ SETTING_DEFINE_LIST_END
+};
+
+/* <settings checks> */
+#ifdef HAVE_LIBSYSTEMD
+# define ENV_SYSTEMD " LISTEN_PID LISTEN_FDS NOTIFY_SOCKET"
+#else
+# define ENV_SYSTEMD ""
+#endif
+#ifdef DEBUG
+# define ENV_GDB " GDB DEBUG_SILENT"
+#else
+# define ENV_GDB ""
+#endif
+/* </settings checks> */
+
+static const struct master_service_settings master_service_default_settings = {
+ .base_dir = PKG_RUNDIR,
+ .state_dir = PKG_STATEDIR,
+ .instance_name = PACKAGE,
+ .log_path = "syslog",
+ .info_log_path = "",
+ .debug_log_path = "",
+ .log_timestamp = DEFAULT_FAILURE_STAMP_FORMAT,
+ .log_debug = "",
+ .log_core_filter = "",
+ .process_shutdown_filter = "",
+ .syslog_facility = "mail",
+ .import_environment = "TZ CORE_OUTOFMEM CORE_ERROR" ENV_SYSTEMD ENV_GDB,
+ .stats_writer_socket_path = "stats-writer",
+ .config_cache_size = 1024*1024,
+ .version_ignore = FALSE,
+ .shutdown_clients = TRUE,
+ .verbose_proctitle = FALSE,
+
+ .haproxy_trusted_networks = "",
+ .haproxy_timeout = 3
+};
+
+const struct setting_parser_info master_service_setting_parser_info = {
+ .module_name = "master",
+ .defines = master_service_setting_defines,
+ .defaults = &master_service_default_settings,
+
+ .type_offset = SIZE_MAX,
+ .struct_size = sizeof(struct master_service_settings),
+
+ .parent_offset = SIZE_MAX,
+ .check_func = master_service_settings_check
+};
+
+/* <settings checks> */
+static bool
+setting_filter_parse(const char *set_name, const char *set_value,
+ void (*handle_filter)(struct event_filter *) ATTR_UNUSED,
+ const char **error_r)
+{
+ struct event_filter *filter;
+ const char *error;
+
+ if (set_value[0] == '\0')
+ return TRUE;
+
+ filter = event_filter_create();
+ if (event_filter_parse(set_value, filter, &error) < 0) {
+ *error_r = t_strdup_printf("Invalid %s: %s", set_name, error);
+ event_filter_unref(&filter);
+ return FALSE;
+ }
+#ifndef CONFIG_BINARY
+ handle_filter(filter);
+#endif
+ event_filter_unref(&filter);
+ return TRUE;
+}
+
+static void
+master_service_set_process_shutdown_filter_wrapper(struct event_filter *filter)
+{
+ master_service_set_process_shutdown_filter(master_service, filter);
+}
+
+static bool
+master_service_settings_check(void *_set, pool_t pool ATTR_UNUSED,
+ const char **error_r)
+{
+ struct master_service_settings *set = _set;
+ int facility;
+
+ if (*set->log_path == '\0') {
+ /* default to syslog logging */
+ set->log_path = "syslog";
+ }
+ if (!syslog_facility_find(set->syslog_facility, &facility)) {
+ *error_r = t_strdup_printf("Unknown syslog_facility: %s",
+ set->syslog_facility);
+ return FALSE;
+ }
+
+ if (!setting_filter_parse("log_debug", set->log_debug,
+ event_set_global_debug_log_filter, error_r))
+ return FALSE;
+ if (!setting_filter_parse("log_core_filter", set->log_core_filter,
+ event_set_global_core_log_filter, error_r))
+ return FALSE;
+ if (!setting_filter_parse("process_shutdown_filter",
+ set->process_shutdown_filter,
+ master_service_set_process_shutdown_filter_wrapper,
+ error_r))
+ return FALSE;
+ return TRUE;
+}
+/* </settings checks> */
+
+static void strarr_push(ARRAY_TYPE(const_string) *argv, const char *str)
+{
+ array_push_back(argv, &str);
+}
+
+static void ATTR_NORETURN
+master_service_exec_config(struct master_service *service,
+ const struct master_service_settings_input *input)
+{
+ ARRAY_TYPE(const_string) conf_argv;
+ const char *binary_path = service->argv[0];
+ const char *error = NULL;
+
+ if (!t_binary_abspath(&binary_path, &error)) {
+ i_fatal("t_binary_abspath(%s) failed: %s", binary_path, error);
+ }
+
+ if (!service->keep_environment && !input->preserve_environment) {
+ if (input->preserve_home)
+ master_service_import_environment("HOME");
+ if (input->preserve_user)
+ master_service_import_environment("USER");
+ if ((service->flags & MASTER_SERVICE_FLAG_STANDALONE) != 0)
+ master_service_import_environment("LOG_STDERR_TIMESTAMP");
+
+ /* doveconf empties the environment before exec()ing us back
+ if DOVECOT_PRESERVE_ENVS is set, so make sure it is. */
+ if (getenv(DOVECOT_PRESERVE_ENVS_ENV) == NULL)
+ env_put(DOVECOT_PRESERVE_ENVS_ENV, "");
+ } else {
+ /* make sure doveconf doesn't remove any environment */
+ env_remove(DOVECOT_PRESERVE_ENVS_ENV);
+ }
+ if (input->use_sysexits)
+ env_put("USE_SYSEXITS", "1");
+
+ t_array_init(&conf_argv, 11 + (service->argc + 1) + 1);
+ strarr_push(&conf_argv, DOVECOT_CONFIG_BIN_PATH);
+ if (input->service != NULL) {
+ strarr_push(&conf_argv, "-f");
+ strarr_push(&conf_argv,
+ t_strconcat("service=", input->service, NULL));
+ }
+ strarr_push(&conf_argv, "-c");
+ strarr_push(&conf_argv, service->config_path);
+ if (input->module != NULL) {
+ strarr_push(&conf_argv, "-m");
+ strarr_push(&conf_argv, input->module);
+ }
+ if (input->extra_modules != NULL) {
+ for (unsigned int i = 0; input->extra_modules[i] != NULL; i++) {
+ strarr_push(&conf_argv, "-m");
+ strarr_push(&conf_argv, input->extra_modules[i]);
+ }
+ }
+ if ((service->flags & MASTER_SERVICE_FLAG_DISABLE_SSL_SET) == 0 &&
+ (input->module != NULL || input->extra_modules != NULL)) {
+ strarr_push(&conf_argv, "-m");
+ if (service->want_ssl_server)
+ strarr_push(&conf_argv, "ssl-server");
+ else
+ strarr_push(&conf_argv, "ssl");
+ }
+ if (input->parse_full_config)
+ strarr_push(&conf_argv, "-p");
+
+ strarr_push(&conf_argv, "-e");
+ strarr_push(&conf_argv, binary_path);
+ array_append(&conf_argv, (const char *const *)service->argv + 1,
+ service->argc);
+ array_append_zero(&conf_argv);
+
+ const char *const *argv = array_front(&conf_argv);
+ execv_const(argv[0], argv);
+}
+
+static void
+config_error_update_path_source(struct master_service *service,
+ const struct master_service_settings_input *input,
+ const char **error)
+{
+ if (input->config_path == NULL && service->config_path_from_master) {
+ *error = t_strdup_printf("%s (path is from %s environment)",
+ *error, MASTER_CONFIG_FILE_ENV);
+ }
+}
+
+static void
+config_exec_fallback(struct master_service *service,
+ const struct master_service_settings_input *input,
+ const char **error)
+{
+ const char *path, *stat_error;
+ struct stat st;
+ int saved_errno = errno;
+
+ if (input->never_exec) {
+ *error = t_strdup_printf(
+ "%s - doveconf execution fallback is disabled", *error);
+ return;
+ }
+
+ path = input->config_path != NULL ? input->config_path :
+ master_service_get_config_path(service);
+ if (stat(path, &st) < 0)
+ stat_error = t_strdup_printf("stat(%s) failed: %m", path);
+ else if (S_ISSOCK(st.st_mode))
+ stat_error = t_strdup_printf("%s is a UNIX socket", path);
+ else if (S_ISFIFO(st.st_mode))
+ stat_error = t_strdup_printf("%s is a FIFO", path);
+ else {
+ /* it's a file, not a socket/pipe */
+ master_service_exec_config(service, input);
+ }
+ *error = t_strdup_printf(
+ "%s - Also failed to read config by executing doveconf: %s",
+ *error, stat_error);
+ config_error_update_path_source(service, input, error);
+ errno = saved_errno;
+}
+
+static int
+master_service_open_config(struct master_service *service,
+ const struct master_service_settings_input *input,
+ const char **path_r, const char **error_r)
+{
+ struct stat st;
+ const char *path;
+ int fd;
+
+ *path_r = path = input->config_path != NULL ? input->config_path :
+ master_service_get_config_path(service);
+
+ if (service->config_fd != -1 && input->config_path == NULL &&
+ !service->config_path_changed_with_param) {
+ /* use the already opened config socket */
+ fd = service->config_fd;
+ service->config_fd = -1;
+ return fd;
+ }
+
+ if (!service->config_path_from_master &&
+ !service->config_path_changed_with_param &&
+ input->config_path == NULL) {
+ /* first try to connect to the default config socket.
+ configuration may contain secrets, so in default config
+ this fails because the socket is 0600. it's useful for
+ developers though. :) */
+ fd = net_connect_unix(DOVECOT_CONFIG_SOCKET_PATH);
+ if (fd >= 0) {
+ *path_r = DOVECOT_CONFIG_SOCKET_PATH;
+ net_set_nonblock(fd, FALSE);
+ return fd;
+ }
+ /* fallback to executing doveconf */
+ }
+
+ if (stat(path, &st) < 0) {
+ *error_r = errno == EACCES ? eacces_error_get("stat", path) :
+ t_strdup_printf("stat(%s) failed: %m", path);
+ config_error_update_path_source(service, input, error_r);
+ return -1;
+ }
+
+ if (!S_ISSOCK(st.st_mode) && !S_ISFIFO(st.st_mode)) {
+ /* it's not an UNIX socket, don't even try to connect */
+ fd = -1;
+ errno = ENOTSOCK;
+ } else {
+ fd = net_connect_unix_with_retries(path, 1000);
+ }
+ if (fd < 0) {
+ *error_r = t_strdup_printf("net_connect_unix(%s) failed: %m",
+ path);
+ config_exec_fallback(service, input, error_r);
+ return -1;
+ }
+ net_set_nonblock(fd, FALSE);
+ return fd;
+}
+
+static void
+config_build_request(struct master_service *service, string_t *str,
+ const struct master_service_settings_input *input)
+{
+ str_append(str, "REQ");
+ if (input->module != NULL)
+ str_printfa(str, "\tmodule=%s", input->module);
+ if (input->extra_modules != NULL) {
+ for (unsigned int i = 0; input->extra_modules[i] != NULL; i++)
+ str_printfa(str, "\tmodule=%s", input->extra_modules[i]);
+ }
+ if ((service->flags & MASTER_SERVICE_FLAG_DISABLE_SSL_SET) == 0 &&
+ (input->module != NULL || input->extra_modules != NULL)) {
+ str_printfa(str, "\tmodule=%s",
+ service->want_ssl_server ? "ssl-server" : "ssl");
+ }
+ if (input->no_ssl_ca)
+ str_append(str, "\texclude=ssl_ca\texclude=ssl_verify_client_cert");
+ if (input->service != NULL)
+ str_printfa(str, "\tservice=%s", input->service);
+ if (input->username != NULL)
+ str_printfa(str, "\tuser=%s", input->username);
+ if (input->local_ip.family != 0)
+ str_printfa(str, "\tlip=%s", net_ip2addr(&input->local_ip));
+ if (input->remote_ip.family != 0)
+ str_printfa(str, "\trip=%s", net_ip2addr(&input->remote_ip));
+ if (input->local_name != NULL)
+ str_printfa(str, "\tlname=%s", input->local_name);
+ str_append_c(str, '\n');
+}
+
+static int
+config_send_request(struct master_service *service,
+ const struct master_service_settings_input *input,
+ int fd, const char *path, const char **error_r)
+{
+ int ret;
+
+ T_BEGIN {
+ string_t *str;
+
+ str = t_str_new(128);
+ str_append(str, CONFIG_HANDSHAKE);
+ config_build_request(service, str, input);
+ ret = write_full(fd, str_data(str), str_len(str));
+ } T_END;
+ if (ret < 0) {
+ *error_r = t_strdup_printf("write_full(%s) failed: %m", path);
+ return -1;
+ }
+ return 0;
+}
+
+static int
+config_send_filters_request(int fd, const char *path, const char **error_r)
+{
+ int ret;
+ ret = write_full(fd, CONFIG_HANDSHAKE"FILTERS\n", strlen(CONFIG_HANDSHAKE"FILTERS\n"));
+ if (ret < 0) {
+ *error_r = t_strdup_printf("write_full(%s) failed: %m", path);
+ return -1;
+ }
+ return 0;
+}
+
+static int
+master_service_apply_config_overrides(struct master_service *service,
+ struct setting_parser_context *parser,
+ const char **error_r)
+{
+ const char *const *overrides;
+ unsigned int i, count;
+
+ overrides = array_get(&service->config_overrides, &count);
+ for (i = 0; i < count; i++) {
+ if (settings_parse_line(parser, overrides[i]) < 0) {
+ *error_r = t_strdup_printf(
+ "Invalid -o parameter %s: %s", overrides[i],
+ settings_parser_get_error(parser));
+ return -1;
+ }
+ settings_parse_set_key_expanded(parser, service->set_pool,
+ t_strcut(overrides[i], '='));
+ }
+ return 0;
+}
+
+static int
+config_read_reply_header(struct istream *istream, const char *path, pool_t pool,
+ const struct master_service_settings_input *input,
+ struct master_service_settings_output *output_r,
+ const char **error_r)
+{
+ const char *line;
+ ssize_t ret;
+
+ while ((ret = i_stream_read(istream)) > 0) {
+ line = i_stream_next_line(istream);
+ if (line != NULL)
+ break;
+ }
+ if (ret <= 0) {
+ if (ret == 0)
+ return 1;
+ *error_r = istream->stream_errno != 0 ?
+ t_strdup_printf("read(%s) failed: %s", path,
+ i_stream_get_error(istream)) :
+ t_strdup_printf("read(%s) failed: EOF", path);
+ return -1;
+ }
+
+ T_BEGIN {
+ const char *const *arg = t_strsplit_tabescaped(line);
+ ARRAY_TYPE(const_string) services;
+
+ p_array_init(&services, pool, 8);
+ for (; *arg != NULL; arg++) {
+ if (strcmp(*arg, "service-uses-local") == 0)
+ output_r->service_uses_local = TRUE;
+ else if (strcmp(*arg, "service-uses-remote") == 0)
+ output_r->service_uses_remote = TRUE;
+ if (strcmp(*arg, "used-local") == 0)
+ output_r->used_local = TRUE;
+ else if (strcmp(*arg, "used-remote") == 0)
+ output_r->used_remote = TRUE;
+ else if (str_begins(*arg, "service=")) {
+ const char *name = p_strdup(pool, *arg + 8);
+ array_push_back(&services, &name);
+ }
+ }
+ if (input->service == NULL) {
+ array_append_zero(&services);
+ output_r->specific_services = array_front(&services);
+ }
+ } T_END;
+ return 0;
+}
+
+void master_service_config_socket_try_open(struct master_service *service)
+{
+ struct master_service_settings_input input;
+ const char *path, *error;
+ int fd;
+
+ /* we'll get here before command line parameters have been parsed,
+ so -O, -c and -i parameters haven't been handled yet at this point.
+ this means we could end up opening config socket connection
+ unnecessarily, but this isn't a problem. we'll just have to
+ ignore it later on. (unfortunately there isn't a master_service_*()
+ call where this function would be better called.) */
+ if (getenv("DOVECONF_ENV") != NULL ||
+ (service->flags & MASTER_SERVICE_FLAG_NO_CONFIG_SETTINGS) != 0)
+ return;
+
+ i_zero(&input);
+ input.never_exec = TRUE;
+ fd = master_service_open_config(service, &input, &path, &error);
+ if (fd != -1)
+ service->config_fd = fd;
+}
+
+int master_service_settings_get_filters(struct master_service *service,
+ const char *const **filters,
+ const char **error_r)
+{
+ struct master_service_settings_input input;
+ int fd;
+ bool retry = TRUE;
+ const char *path = NULL;
+ ARRAY_TYPE(const_string) filters_tmp;
+ t_array_init(&filters_tmp, 8);
+ i_zero(&input);
+
+ if (getenv("DOVECONF_ENV") == NULL &&
+ (service->flags & MASTER_SERVICE_FLAG_NO_CONFIG_SETTINGS) == 0) {
+ retry = service->config_fd != -1;
+ for (;;) {
+ fd = master_service_open_config(service, &input, &path, error_r);
+ if (fd == -1) {
+ return -1;
+ }
+ if (config_send_filters_request(fd, path, error_r) == 0)
+ break;
+
+ i_close_fd(&fd);
+ if (!retry)
+ return -1;
+ retry = FALSE;
+ }
+ service->config_fd = fd;
+ struct istream *is = i_stream_create_fd(fd, SIZE_MAX);
+ const char *line;
+ /* try read response */
+ while((line = i_stream_read_next_line(is)) != NULL) {
+ if (*line == '\0')
+ break;
+ if (str_begins(line, "FILTER\t")) {
+ line = t_strdup(line+7);
+ array_push_back(&filters_tmp, &line);
+ }
+ }
+ i_stream_unref(&is);
+ }
+
+ array_append_zero(&filters_tmp);
+ *filters = array_front(&filters_tmp);
+ return 0;
+}
+
+int master_service_settings_read(struct master_service *service,
+ const struct master_service_settings_input *input,
+ struct master_service_settings_output *output_r,
+ const char **error_r)
+{
+ ARRAY(const struct setting_parser_info *) all_roots;
+ const struct setting_parser_info *tmp_root;
+ struct setting_parser_context *parser;
+ struct istream *istream;
+ const char *path = NULL, *error;
+ void **sets;
+ unsigned int i;
+ int ret, fd = -1;
+ time_t now, timeout;
+ bool use_environment, retry;
+
+ i_zero(output_r);
+
+ if (getenv("DOVECONF_ENV") == NULL &&
+ (service->flags & MASTER_SERVICE_FLAG_NO_CONFIG_SETTINGS) == 0) {
+ retry = service->config_fd != -1;
+ for (;;) {
+ fd = master_service_open_config(service, input,
+ &path, error_r);
+ if (fd == -1) {
+ if (errno == EACCES)
+ output_r->permission_denied = TRUE;
+ return -1;
+ }
+
+ if (config_send_request(service, input, fd,
+ path, error_r) == 0)
+ break;
+ i_close_fd(&fd);
+ if (!retry) {
+ config_exec_fallback(service, input, error_r);
+ return -1;
+ }
+ /* config process died, retry connecting */
+ retry = FALSE;
+ }
+ }
+
+ if (service->set_pool != NULL) {
+ if (service->set_parser != NULL)
+ settings_parser_deinit(&service->set_parser);
+ p_clear(service->set_pool);
+ } else {
+ service->set_pool =
+ pool_alloconly_create("master service settings", 16384);
+ }
+
+ p_array_init(&all_roots, service->set_pool, 8);
+ tmp_root = &master_service_setting_parser_info;
+ array_push_back(&all_roots, &tmp_root);
+ tmp_root = &master_service_ssl_setting_parser_info;
+ array_push_back(&all_roots, &tmp_root);
+ if (service->want_ssl_server) {
+ tmp_root = &master_service_ssl_server_setting_parser_info;
+ array_push_back(&all_roots, &tmp_root);
+ }
+ if (input->roots != NULL) {
+ for (i = 0; input->roots[i] != NULL; i++)
+ array_push_back(&all_roots, &input->roots[i]);
+ }
+
+ parser = settings_parser_init_list(service->set_pool,
+ array_front(&all_roots), array_count(&all_roots),
+ SETTINGS_PARSER_FLAG_IGNORE_UNKNOWN_KEYS);
+
+ if (fd != -1) {
+ istream = i_stream_create_fd(fd, SIZE_MAX);
+ now = time(NULL);
+ timeout = now + CONFIG_READ_TIMEOUT_SECS;
+ do {
+ alarm(timeout - now);
+ ret = config_read_reply_header(istream, path,
+ service->set_pool, input,
+ output_r, error_r);
+ if (ret == 0) {
+ ret = settings_parse_stream_read(parser,
+ istream);
+ if (ret < 0)
+ *error_r = t_strdup(
+ settings_parser_get_error(parser));
+ }
+ alarm(0);
+ if (ret <= 0)
+ break;
+
+ /* most likely timed out, but just in case some other
+ signal was delivered early check if we need to
+ continue */
+ now = time(NULL);
+ } while (now < timeout);
+ i_stream_unref(&istream);
+
+ if (ret != 0) {
+ if (ret > 0) {
+ *error_r = t_strdup_printf(
+ "Timeout reading config from %s", path);
+ }
+ i_close_fd(&fd);
+ config_exec_fallback(service, input, error_r);
+ settings_parser_deinit(&parser);
+ return -1;
+ }
+
+ if ((service->flags & MASTER_SERVICE_FLAG_KEEP_CONFIG_OPEN) != 0 &&
+ service->config_fd == -1 && input->config_path == NULL)
+ service->config_fd = fd;
+ else
+ i_close_fd(&fd);
+ use_environment = FALSE;
+ } else {
+ use_environment = TRUE;
+ }
+
+ if (use_environment || service->keep_environment) {
+ if (settings_parse_environ(parser) < 0) {
+ *error_r = t_strdup(settings_parser_get_error(parser));
+ settings_parser_deinit(&parser);
+ return -1;
+ }
+ }
+
+ if (array_is_created(&service->config_overrides)) {
+ if (master_service_apply_config_overrides(service, parser,
+ error_r) < 0) {
+ settings_parser_deinit(&parser);
+ return -1;
+ }
+ }
+
+ if (!settings_parser_check(parser, service->set_pool, &error)) {
+ *error_r = t_strdup_printf("Invalid settings: %s", error);
+ settings_parser_deinit(&parser);
+ return -1;
+ }
+
+ sets = settings_parser_get_list(parser);
+ service->set = sets[0];
+ service->set_parser = parser;
+
+ if (service->set->version_ignore &&
+ (service->flags & MASTER_SERVICE_FLAG_STANDALONE) != 0) {
+ /* running standalone. we want to ignore plugin versions. */
+ service->version_string = NULL;
+ }
+ if ((service->flags & MASTER_SERVICE_FLAG_DONT_SEND_STATS) == 0 &&
+ (service->flags & MASTER_SERVICE_FLAG_STANDALONE) != 0) {
+ /* When running standalone (e.g. doveadm) try to connect to the
+ stats socket, but don't log an error if it's not running.
+ It may be intentional. Non-standalone stats-client
+ initialization was already done earlier. */
+ master_service_init_stats_client(service, TRUE);
+ }
+
+ if (service->set->shutdown_clients)
+ master_service_set_die_with_master(master_service, TRUE);
+
+ /* if we change any settings afterwards, they're in expanded form.
+ especially all settings from userdb are already expanded. */
+ settings_parse_set_expanded(service->set_parser, TRUE);
+ return 0;
+}
+
+int master_service_settings_read_simple(struct master_service *service,
+ const struct setting_parser_info **roots,
+ const char **error_r)
+{
+ struct master_service_settings_input input;
+ struct master_service_settings_output output;
+
+ i_zero(&input);
+ input.roots = roots;
+ input.module = service->name;
+ return master_service_settings_read(service, &input, &output, error_r);
+}
+
+pool_t master_service_settings_detach(struct master_service *service)
+{
+ pool_t pool = service->set_pool;
+
+ settings_parser_deinit(&service->set_parser);
+ service->set_pool = NULL;
+ return pool;
+}
+
+const struct master_service_settings *
+master_service_settings_get(struct master_service *service)
+{
+ void **sets;
+
+ sets = settings_parser_get_list(service->set_parser);
+ return sets[0];
+}
+
+void **master_service_settings_get_others(struct master_service *service)
+{
+ return master_service_settings_parser_get_others(service,
+ service->set_parser);
+}
+
+void **master_service_settings_parser_get_others(struct master_service *service,
+ const struct setting_parser_context *set_parser)
+{
+ return settings_parser_get_list(set_parser) + 2 +
+ (service->want_ssl_server ? 1 : 0);
+}
+
+struct setting_parser_context *
+master_service_get_settings_parser(struct master_service *service)
+{
+ return service->set_parser;
+}
+
+int master_service_set(struct master_service *service, const char *line)
+{
+ return settings_parse_line(service->set_parser, line);
+}
+
+bool master_service_set_has_config_override(struct master_service *service,
+ const char *key)
+{
+ const char *override, *key_root;
+ bool ret;
+
+ if (!array_is_created(&service->config_overrides))
+ return FALSE;
+
+ key_root = settings_parse_unalias(service->set_parser, key);
+ if (key_root == NULL)
+ key_root = key;
+
+ array_foreach_elem(&service->config_overrides, override) {
+ T_BEGIN {
+ const char *okey, *okey_root;
+
+ okey = t_strcut(override, '=');
+ okey_root = settings_parse_unalias(service->set_parser,
+ okey);
+ if (okey_root == NULL)
+ okey_root = okey;
+ ret = strcmp(okey_root, key_root) == 0;
+ } T_END;
+
+ if (ret)
+ return TRUE;
+ }
+ return FALSE;
+}
diff --git a/src/lib-master/master-service-settings.h b/src/lib-master/master-service-settings.h
new file mode 100644
index 0000000..eceec0c
--- /dev/null
+++ b/src/lib-master/master-service-settings.h
@@ -0,0 +1,115 @@
+#ifndef MASTER_SERVICE_SETTINGS_H
+#define MASTER_SERVICE_SETTINGS_H
+
+#include "net.h"
+
+struct setting_parser_info;
+struct setting_parser_context;
+struct master_service;
+
+struct master_service_settings {
+ /* NOTE: log process won't see any new settings unless they're
+ explicitly sent via environment variables by master process. */
+ const char *base_dir;
+ const char *state_dir;
+ const char *instance_name;
+ const char *log_path;
+ const char *info_log_path;
+ const char *debug_log_path;
+ const char *log_timestamp;
+ const char *log_debug;
+ const char *log_core_filter;
+ const char *process_shutdown_filter;
+ const char *syslog_facility;
+ const char *import_environment;
+ const char *stats_writer_socket_path;
+ uoff_t config_cache_size;
+ bool version_ignore;
+ bool shutdown_clients;
+ bool verbose_proctitle;
+
+ const char *haproxy_trusted_networks;
+ unsigned int haproxy_timeout;
+};
+
+struct master_service_settings_input {
+ const struct setting_parser_info *const *roots;
+ const char *config_path;
+ bool preserve_environment;
+ bool preserve_user;
+ bool preserve_home;
+ bool never_exec;
+ bool use_sysexits;
+ bool parse_full_config;
+
+ /* Either/both module and extra_modules can be set. Usually just one
+ is needed, so module is simpler to set. */
+ const char *module;
+ const char *const *extra_modules;
+ const char *service;
+ const char *username;
+ struct ip_addr local_ip, remote_ip;
+ const char *local_name;
+
+ /* A bit of a memory saving kludge: Mail processes (especially imap)
+ shouldn't read ssl_ca setting since it's likely not needed and it
+ can use a lot of memory. */
+ bool no_ssl_ca;
+};
+
+struct master_service_settings_output {
+ /* if service was not given for lookup, this contains names of services
+ that have more specific settings */
+ const char *const *specific_services;
+
+ /* some settings for this service (or if service was not given,
+ all services) contain local/remote ip/host specific settings
+ (but this lookup didn't necessarily return any of them). */
+ bool service_uses_local:1;
+ bool service_uses_remote:1;
+ /* returned settings contain settings specific to given
+ local/remote ip/host */
+ bool used_local:1;
+ bool used_remote:1;
+ /* Config couldn't be read because we don't have enough permissions.
+ The process probably should be restarted and the settings read
+ before dropping privileges. */
+ bool permission_denied:1;
+};
+
+extern const struct setting_parser_info master_service_setting_parser_info;
+
+/* Try to open the config socket if it's going to be needed later by
+ master_service_settings_read*() */
+void master_service_config_socket_try_open(struct master_service *service);
+int master_service_settings_get_filters(struct master_service *service,
+ const char *const **filters,
+ const char **error_r);
+int master_service_settings_read(struct master_service *service,
+ const struct master_service_settings_input *input,
+ struct master_service_settings_output *output_r,
+ const char **error_r);
+int master_service_settings_read_simple(struct master_service *service,
+ const struct setting_parser_info **roots,
+ const char **error_r) ATTR_NULL(2);
+/* destroy settings parser and clear service's set_pool, so that
+ master_service_settings_read*() can be called without freeing memory used
+ by existing settings structures. */
+pool_t master_service_settings_detach(struct master_service *service);
+
+const struct master_service_settings *
+master_service_settings_get(struct master_service *service);
+void **master_service_settings_get_others(struct master_service *service);
+void **master_service_settings_parser_get_others(struct master_service *service,
+ const struct setting_parser_context *set_parser);
+struct setting_parser_context *
+master_service_get_settings_parser(struct master_service *service);
+
+int master_service_set(struct master_service *service, const char *line);
+
+/* Returns TRUE if -o key=value parameter was used. Setting keys in overrides
+ and parameter are unaliased before comparing. */
+bool master_service_set_has_config_override(struct master_service *service,
+ const char *key);
+
+#endif
diff --git a/src/lib-master/master-service-ssl-settings.c b/src/lib-master/master-service-ssl-settings.c
new file mode 100644
index 0000000..5ddf18c
--- /dev/null
+++ b/src/lib-master/master-service-ssl-settings.c
@@ -0,0 +1,275 @@
+/* Copyright (c) 2013-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "settings-parser.h"
+#include "master-service-private.h"
+#include "master-service-ssl-settings.h"
+#include "iostream-ssl.h"
+
+#include <stddef.h>
+
+#undef DEF
+#define DEF(type, name) \
+ SETTING_DEFINE_STRUCT_##type(#name, name, struct master_service_ssl_settings)
+
+static bool
+master_service_ssl_settings_check(void *_set, pool_t pool, const char **error_r);
+
+static const struct setting_define master_service_ssl_setting_defines[] = {
+ DEF(ENUM, ssl),
+ DEF(STR, ssl_ca),
+ DEF(STR, ssl_client_ca_file),
+ DEF(STR, ssl_client_ca_dir),
+ DEF(STR, ssl_client_cert),
+ DEF(STR, ssl_client_key),
+ DEF(STR, ssl_cipher_list),
+ DEF(STR, ssl_cipher_suites),
+ DEF(STR, ssl_curve_list),
+ DEF(STR, ssl_min_protocol),
+ DEF(STR, ssl_cert_username_field),
+ DEF(STR, ssl_crypto_device),
+ DEF(BOOL, ssl_verify_client_cert),
+ DEF(BOOL, ssl_client_require_valid_cert),
+ DEF(BOOL, ssl_require_crl),
+ DEF(BOOL, verbose_ssl),
+ DEF(BOOL, ssl_prefer_server_ciphers),
+ DEF(STR, ssl_options), /* parsed as a string to set bools */
+
+ SETTING_DEFINE_LIST_END
+};
+
+static const struct master_service_ssl_settings master_service_ssl_default_settings = {
+#ifdef HAVE_SSL
+ .ssl = "yes:no:required",
+#else
+ .ssl = "no:yes:required",
+#endif
+ .ssl_ca = "",
+ .ssl_client_ca_file = "",
+ .ssl_client_ca_dir = "",
+ .ssl_client_cert = "",
+ .ssl_client_key = "",
+ .ssl_cipher_list = "ALL:!kRSA:!SRP:!kDHd:!DSS:!aNULL:!eNULL:!EXPORT:!DES:!3DES:!MD5:!PSK:!RC4:!ADH:!LOW@STRENGTH",
+ .ssl_cipher_suites = "", /* Use TLS library provided value */
+ .ssl_curve_list = "",
+ .ssl_min_protocol = "TLSv1.2",
+ .ssl_cert_username_field = "commonName",
+ .ssl_crypto_device = "",
+ .ssl_verify_client_cert = FALSE,
+ .ssl_client_require_valid_cert = TRUE,
+ .ssl_require_crl = TRUE,
+ .verbose_ssl = FALSE,
+ .ssl_prefer_server_ciphers = FALSE,
+ .ssl_options = "",
+};
+
+const struct setting_parser_info master_service_ssl_setting_parser_info = {
+ .module_name = "ssl",
+ .defines = master_service_ssl_setting_defines,
+ .defaults = &master_service_ssl_default_settings,
+
+ .type_offset = SIZE_MAX,
+ .struct_size = sizeof(struct master_service_ssl_settings),
+
+ .parent_offset = SIZE_MAX,
+ .check_func = master_service_ssl_settings_check
+};
+
+#undef DEF
+#define DEF(type, name) \
+ SETTING_DEFINE_STRUCT_##type(#name, name, struct master_service_ssl_server_settings)
+
+static const struct setting_define master_service_ssl_server_setting_defines[] = {
+ DEF(STR, ssl_cert),
+ DEF(STR, ssl_key),
+ DEF(STR, ssl_alt_cert),
+ DEF(STR, ssl_alt_key),
+ DEF(STR, ssl_key_password),
+ DEF(STR, ssl_dh),
+
+ SETTING_DEFINE_LIST_END
+};
+
+static const struct master_service_ssl_server_settings master_service_ssl_server_default_settings = {
+ .ssl_cert = "",
+ .ssl_key = "",
+ .ssl_alt_cert = "",
+ .ssl_alt_key = "",
+ .ssl_key_password = "",
+ .ssl_dh = "",
+};
+
+static const struct setting_parser_info *master_service_ssl_server_setting_dependencies[] = {
+ &master_service_ssl_setting_parser_info,
+ NULL
+};
+
+const struct setting_parser_info master_service_ssl_server_setting_parser_info = {
+ .module_name = "ssl-server",
+ .defines = master_service_ssl_server_setting_defines,
+ .defaults = &master_service_ssl_server_default_settings,
+
+ .type_offset = SIZE_MAX,
+ .struct_size = sizeof(struct master_service_ssl_server_settings),
+
+ .parent_offset = SIZE_MAX,
+ .dependencies = master_service_ssl_server_setting_dependencies,
+};
+
+/* <settings checks> */
+static bool
+master_service_ssl_settings_check(void *_set, pool_t pool ATTR_UNUSED,
+ const char **error_r)
+{
+ struct master_service_ssl_settings *set = _set;
+
+ if (strcmp(set->ssl, "no") == 0) {
+ /* disabled */
+ return TRUE;
+ }
+#ifndef HAVE_SSL
+ *error_r = t_strdup_printf("SSL support not compiled in but ssl=%s",
+ set->ssl);
+ return FALSE;
+#else
+ /* we get called from many different tools, possibly with -O parameter,
+ and few of those tools care about SSL settings. so don't check
+ ssl_cert/ssl_key/etc validity here except in doveconf, because it
+ usually is just an extra annoyance. */
+#ifdef CONFIG_BINARY
+ if (*set->ssl_cert == '\0') {
+ *error_r = "ssl enabled, but ssl_cert not set";
+ return FALSE;
+ }
+ if (*set->ssl_key == '\0') {
+ *error_r = "ssl enabled, but ssl_key not set";
+ return FALSE;
+ }
+#endif
+ if (set->ssl_verify_client_cert && *set->ssl_ca == '\0') {
+ *error_r = "ssl_verify_client_cert set, but ssl_ca not";
+ return FALSE;
+ }
+
+ /* Now explode the ssl_options string into individual flags */
+ /* First set them all to defaults */
+ set->parsed_opts.compression = FALSE;
+ set->parsed_opts.tickets = TRUE;
+
+ /* Then modify anything specified in the string */
+ const char **opts = t_strsplit_spaces(set->ssl_options, ", ");
+ const char *opt;
+ while ((opt = *opts++) != NULL) {
+ if (strcasecmp(opt, "compression") == 0) {
+ set->parsed_opts.compression = TRUE;
+ } else if (strcasecmp(opt, "no_compression") == 0) {
+#ifdef CONFIG_BINARY
+ i_warning("DEPRECATED: no_compression is default, "
+ "so it is redundant in ssl_options");
+#endif
+ } else if (strcasecmp(opt, "no_ticket") == 0) {
+ set->parsed_opts.tickets = FALSE;
+ } else {
+ *error_r = t_strdup_printf("ssl_options: unknown flag: '%s'",
+ opt);
+ return FALSE;
+ }
+ }
+
+#ifndef HAVE_SSL_CTX_SET1_CURVES_LIST
+ if (*set->ssl_curve_list != '\0') {
+ *error_r = "ssl_curve_list is set, but the linked openssl "
+ "version does not support it";
+ return FALSE;
+ }
+#endif
+
+ return TRUE;
+#endif
+}
+/* </settings checks> */
+
+const struct master_service_ssl_settings *
+master_service_ssl_settings_get(struct master_service *service)
+{
+ return master_service_ssl_settings_get_from_parser(service->set_parser);
+}
+
+const struct master_service_ssl_settings *
+master_service_ssl_settings_get_from_parser(struct setting_parser_context *set_parser)
+{
+ void **sets;
+
+ sets = settings_parser_get_list(set_parser);
+ return sets[1];
+}
+
+const struct master_service_ssl_server_settings *
+master_service_ssl_server_settings_get(struct master_service *service)
+{
+ void **sets;
+
+ i_assert(service->want_ssl_server);
+ sets = settings_parser_get_list(service->set_parser);
+ return sets[2];
+}
+
+static void master_service_ssl_common_settings_to_iostream_set(
+ const struct master_service_ssl_settings *ssl_set, pool_t pool,
+ struct ssl_iostream_settings *set_r)
+{
+ i_zero(set_r);
+ set_r->min_protocol = p_strdup(pool, ssl_set->ssl_min_protocol);
+ set_r->cipher_list = p_strdup(pool, ssl_set->ssl_cipher_list);
+ /* leave NULL if empty - let library decide */
+ set_r->ciphersuites = p_strdup_empty(pool, ssl_set->ssl_cipher_suites);
+ /* NOTE: It's a bit questionable whether ssl_ca should be used for
+ clients. But at least for now it's needed for login-proxy. */
+ set_r->ca = p_strdup_empty(pool, ssl_set->ssl_ca);
+
+ set_r->crypto_device = p_strdup(pool, ssl_set->ssl_crypto_device);
+ set_r->cert_username_field = p_strdup(pool, ssl_set->ssl_cert_username_field);
+
+ set_r->verbose = ssl_set->verbose_ssl;
+ set_r->verbose_invalid_cert = ssl_set->verbose_ssl;
+ set_r->skip_crl_check = !ssl_set->ssl_require_crl;
+ set_r->prefer_server_ciphers = ssl_set->ssl_prefer_server_ciphers;
+ set_r->compression = ssl_set->parsed_opts.compression;
+ set_r->tickets = ssl_set->parsed_opts.tickets;
+ set_r->curve_list = p_strdup(pool, ssl_set->ssl_curve_list);
+}
+
+void master_service_ssl_client_settings_to_iostream_set(
+ const struct master_service_ssl_settings *ssl_set, pool_t pool,
+ struct ssl_iostream_settings *set_r)
+{
+ master_service_ssl_common_settings_to_iostream_set(ssl_set, pool, set_r);
+
+ set_r->ca_file = p_strdup_empty(pool, ssl_set->ssl_client_ca_file);
+ set_r->ca_dir = p_strdup_empty(pool, ssl_set->ssl_client_ca_dir);
+ set_r->cert.cert = p_strdup_empty(pool, ssl_set->ssl_client_cert);
+ set_r->cert.key = p_strdup_empty(pool, ssl_set->ssl_client_key);
+ set_r->verify_remote_cert = ssl_set->ssl_client_require_valid_cert;
+ set_r->allow_invalid_cert = !set_r->verify_remote_cert;
+}
+
+void master_service_ssl_server_settings_to_iostream_set(
+ const struct master_service_ssl_settings *ssl_set,
+ const struct master_service_ssl_server_settings *ssl_server_set,
+ pool_t pool, struct ssl_iostream_settings *set_r)
+{
+ master_service_ssl_common_settings_to_iostream_set(ssl_set, pool, set_r);
+
+ set_r->cert.cert = p_strdup(pool, ssl_server_set->ssl_cert);
+ set_r->cert.key = p_strdup(pool, ssl_server_set->ssl_key);
+ set_r->cert.key_password = p_strdup(pool, ssl_server_set->ssl_key_password);
+ if (ssl_server_set->ssl_alt_cert != NULL &&
+ *ssl_server_set->ssl_alt_cert != '\0') {
+ set_r->alt_cert.cert = p_strdup(pool, ssl_server_set->ssl_alt_cert);
+ set_r->alt_cert.key = p_strdup(pool, ssl_server_set->ssl_alt_key);
+ set_r->alt_cert.key_password = p_strdup(pool, ssl_server_set->ssl_key_password);
+ }
+ set_r->dh = p_strdup(pool, ssl_server_set->ssl_dh);
+ set_r->verify_remote_cert = ssl_set->ssl_verify_client_cert;
+ set_r->allow_invalid_cert = !set_r->verify_remote_cert;
+}
diff --git a/src/lib-master/master-service-ssl-settings.h b/src/lib-master/master-service-ssl-settings.h
new file mode 100644
index 0000000..01290ab
--- /dev/null
+++ b/src/lib-master/master-service-ssl-settings.h
@@ -0,0 +1,65 @@
+#ifndef MASTER_SERVICE_SSL_SETTINGS_H
+#define MASTER_SERVICE_SSL_SETTINGS_H
+
+struct master_service;
+struct setting_parser_context;
+struct ssl_iostream_settings;
+
+struct master_service_ssl_settings {
+ const char *ssl;
+ const char *ssl_ca;
+ const char *ssl_client_ca_file;
+ const char *ssl_client_ca_dir;
+ const char *ssl_client_cert;
+ const char *ssl_client_key;
+ const char *ssl_cipher_list;
+ const char *ssl_cipher_suites;
+ const char *ssl_curve_list;
+ const char *ssl_min_protocol;
+ const char *ssl_cert_username_field;
+ const char *ssl_crypto_device;
+ const char *ssl_options;
+
+ bool ssl_verify_client_cert;
+ bool ssl_client_require_valid_cert;
+ bool ssl_require_crl;
+ bool verbose_ssl;
+ bool ssl_prefer_server_ciphers;
+
+ /* These are derived from ssl_options, not set directly */
+ struct {
+ bool compression;
+ bool tickets;
+ } parsed_opts;
+};
+
+struct master_service_ssl_server_settings {
+ const char *ssl_cert;
+ const char *ssl_alt_cert;
+ const char *ssl_key;
+ const char *ssl_alt_key;
+ const char *ssl_key_password;
+ const char *ssl_dh;
+};
+
+extern const struct setting_parser_info master_service_ssl_setting_parser_info;
+extern const struct setting_parser_info master_service_ssl_server_setting_parser_info;
+
+const struct master_service_ssl_settings *
+master_service_ssl_settings_get(struct master_service *service);
+const struct master_service_ssl_settings *
+master_service_ssl_settings_get_from_parser(struct setting_parser_context *set_parser);
+
+const struct master_service_ssl_server_settings *
+master_service_ssl_server_settings_get(struct master_service *service);
+
+/* Provides master service ssl settings to iostream settings */
+void master_service_ssl_client_settings_to_iostream_set(
+ const struct master_service_ssl_settings *ssl_set, pool_t pool,
+ struct ssl_iostream_settings *set_r);
+void master_service_ssl_server_settings_to_iostream_set(
+ const struct master_service_ssl_settings *ssl_set,
+ const struct master_service_ssl_server_settings *ssl_server_set,
+ pool_t pool, struct ssl_iostream_settings *set_r);
+
+#endif
diff --git a/src/lib-master/master-service-ssl.c b/src/lib-master/master-service-ssl.c
new file mode 100644
index 0000000..5d1b515
--- /dev/null
+++ b/src/lib-master/master-service-ssl.c
@@ -0,0 +1,105 @@
+/* Copyright (c) 2013-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "ioloop.h"
+#include "buffer.h"
+#include "iostream-ssl.h"
+#include "master-service-private.h"
+#include "master-service-settings.h"
+#include "master-service-ssl-settings.h"
+#include "master-service-ssl.h"
+
+#include <unistd.h>
+
+int master_service_ssl_init(struct master_service *service,
+ struct istream **input, struct ostream **output,
+ struct ssl_iostream **ssl_iostream_r,
+ const char **error_r)
+{
+ const struct master_service_ssl_settings *set;
+ struct ssl_iostream_settings ssl_set;
+
+ i_assert(service->ssl_ctx_initialized);
+
+ set = master_service_ssl_settings_get(service);
+ if (service->ssl_ctx == NULL) {
+ if (strcmp(set->ssl, "no") == 0)
+ *error_r = "SSL is disabled (ssl=no)";
+ else
+ *error_r = "Failed to initialize SSL context";
+ return -1;
+ }
+
+ i_zero(&ssl_set);
+ ssl_set.verbose = set->verbose_ssl;
+ ssl_set.verify_remote_cert = set->ssl_verify_client_cert;
+ return io_stream_create_ssl_server(service->ssl_ctx, &ssl_set,
+ input, output, ssl_iostream_r, error_r);
+}
+
+bool master_service_ssl_is_enabled(struct master_service *service)
+{
+ return service->ssl_ctx != NULL;
+}
+
+void master_service_ssl_ctx_init(struct master_service *service)
+{
+ const struct master_service_ssl_settings *set;
+ const struct master_service_ssl_server_settings *server_set;
+ struct ssl_iostream_settings ssl_set;
+ const char *error;
+
+ if (service->ssl_ctx_initialized)
+ return;
+ service->ssl_ctx_initialized = TRUE;
+
+ /* must be called after master_service_init_finish() so that if
+ initialization fails we can close the SSL listeners */
+ i_assert(service->listeners != NULL || service->socket_count == 0);
+
+ set = master_service_ssl_settings_get(service);
+ server_set = master_service_ssl_server_settings_get(service);
+ if (strcmp(set->ssl, "no") == 0) {
+ /* SSL disabled, don't use it */
+ return;
+ }
+
+ i_zero(&ssl_set);
+ ssl_set.min_protocol = set->ssl_min_protocol;
+ ssl_set.cipher_list = set->ssl_cipher_list;
+ ssl_set.curve_list = set->ssl_curve_list;
+ ssl_set.ca = set->ssl_ca;
+ ssl_set.cert.cert = server_set->ssl_cert;
+ ssl_set.cert.key = server_set->ssl_key;
+ ssl_set.dh = server_set->ssl_dh;
+ ssl_set.cert.key_password = server_set->ssl_key_password;
+ ssl_set.cert_username_field = set->ssl_cert_username_field;
+ if (server_set->ssl_alt_cert != NULL &&
+ *server_set->ssl_alt_cert != '\0') {
+ ssl_set.alt_cert.cert = server_set->ssl_alt_cert;
+ ssl_set.alt_cert.key = server_set->ssl_alt_key;
+ ssl_set.alt_cert.key_password = server_set->ssl_key_password;
+ }
+ ssl_set.crypto_device = set->ssl_crypto_device;
+ ssl_set.skip_crl_check = !set->ssl_require_crl;
+
+ ssl_set.verbose = set->verbose_ssl;
+ ssl_set.verify_remote_cert = set->ssl_verify_client_cert;
+ ssl_set.prefer_server_ciphers = set->ssl_prefer_server_ciphers;
+ ssl_set.compression = set->parsed_opts.compression;
+
+ if (ssl_iostream_context_init_server(&ssl_set, &service->ssl_ctx,
+ &error) < 0) {
+ i_error("SSL context initialization failed, disabling SSL: %s",
+ error);
+ master_service_ssl_io_listeners_remove(service);
+ return;
+ }
+}
+
+void master_service_ssl_ctx_deinit(struct master_service *service)
+{
+ if (service->ssl_ctx != NULL)
+ ssl_iostream_context_unref(&service->ssl_ctx);
+ service->ssl_ctx_initialized = FALSE;
+}
diff --git a/src/lib-master/master-service-ssl.h b/src/lib-master/master-service-ssl.h
new file mode 100644
index 0000000..3e95145
--- /dev/null
+++ b/src/lib-master/master-service-ssl.h
@@ -0,0 +1,16 @@
+#ifndef MASTER_SERVICE_SSL_H
+#define MASTER_SERVICE_SSL_H
+
+struct ssl_iostream;
+
+int master_service_ssl_init(struct master_service *service,
+ struct istream **input, struct ostream **output,
+ struct ssl_iostream **ssl_iostream_r,
+ const char **error_r);
+
+bool master_service_ssl_is_enabled(struct master_service *service);
+
+void master_service_ssl_ctx_init(struct master_service *service);
+void master_service_ssl_ctx_deinit(struct master_service *service);
+
+#endif
diff --git a/src/lib-master/master-service.c b/src/lib-master/master-service.c
new file mode 100644
index 0000000..a92c0a8
--- /dev/null
+++ b/src/lib-master/master-service.c
@@ -0,0 +1,1548 @@
+/* Copyright (c) 2005-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "lib-signals.h"
+#include "lib-event-private.h"
+#include "event-filter.h"
+#include "ioloop.h"
+#include "hostpid.h"
+#include "path-util.h"
+#include "array.h"
+#include "strescape.h"
+#include "env-util.h"
+#include "home-expand.h"
+#include "process-title.h"
+#include "time-util.h"
+#include "restrict-access.h"
+#include "settings-parser.h"
+#include "syslog-util.h"
+#include "stats-client.h"
+#include "master-instance.h"
+#include "master-login.h"
+#include "master-service-ssl.h"
+#include "master-service-private.h"
+#include "master-service-settings.h"
+#include "iostream-ssl.h"
+
+#include <unistd.h>
+#include <sys/stat.h>
+#include <syslog.h>
+
+#define DEFAULT_CONFIG_FILE_PATH SYSCONFDIR"/dovecot.conf"
+
+/* getenv(MASTER_CONFIG_FILE_ENV) provides path to configuration file/socket */
+#define MASTER_CONFIG_FILE_ENV "CONFIG_FILE"
+
+/* getenv(MASTER_DOVECOT_VERSION_ENV) provides master's version number */
+#define MASTER_DOVECOT_VERSION_ENV "DOVECOT_VERSION"
+
+/* when we're full of connections, how often to check if login state has
+ changed. we normally notice it immediately because of a signal, so this is
+ just a fallback against race conditions. */
+#define MASTER_SERVICE_STATE_CHECK_MSECS 1000
+
+/* If die callback hasn't managed to stop the service for this many seconds,
+ force it. */
+#define MASTER_SERVICE_DIE_TIMEOUT_MSECS (30*1000)
+
+struct master_service *master_service;
+
+static struct event_category master_service_category = {
+ .name = NULL, /* set dynamically later */
+};
+static char *master_service_category_name;
+
+static void master_service_io_listeners_close(struct master_service *service);
+static int master_service_get_login_state(enum master_login_state *state_r);
+static void master_service_refresh_login_state(struct master_service *service);
+static void
+master_status_send(struct master_service *service, bool important_update);
+
+const char *master_service_getopt_string(void)
+{
+ return "c:i:ko:OL";
+}
+
+static void sig_die(const siginfo_t *si, void *context)
+{
+ struct master_service *service = context;
+
+ /* SIGINT comes either from master process or from keyboard. we don't
+ want to log it in either case.*/
+ if (si->si_signo != SIGINT) {
+ i_warning("Killed with signal %d (by pid=%s uid=%s code=%s)",
+ si->si_signo, dec2str(si->si_pid),
+ dec2str(si->si_uid),
+ lib_signal_code_to_str(si->si_signo, si->si_code));
+ } else if ((service->flags & MASTER_SERVICE_FLAG_NO_IDLE_DIE) != 0) {
+ /* never die when idling */
+ return;
+ } else if ((service->flags & MASTER_SERVICE_FLAG_STANDALONE) == 0) {
+ /* SIGINT came from master. die only if we're not handling
+ any clients currently. */
+ if (service->master_status.available_count !=
+ service->total_available_count)
+ return;
+
+ if (service->idle_die_callback != NULL &&
+ !service->idle_die_callback()) {
+ /* we don't want to die - send a notification to master
+ so it doesn't think we're ignoring it completely. */
+ master_status_send(service, FALSE);
+ return;
+ }
+ }
+
+ service->killed = TRUE;
+ io_loop_stop(service->ioloop);
+}
+
+static void sig_close_listeners(const siginfo_t *si ATTR_UNUSED, void *context)
+{
+ struct master_service *service = context;
+
+ /* We're in a signal handler: Close listeners immediately so master
+ can successfully restart. We can safely close only those listeners
+ that don't have an io, but this shouldn't be a big problem. If there
+ is an active io, the service is unlikely to be unresposive for
+ longer periods of time, so the listener gets closed soon enough via
+ master_status_error().
+
+ For extra safety we don't actually close() the fd, but instead
+ replace it with /dev/null. This way it won't be replaced with some
+ other new fd and attempted to be used in unexpected ways. */
+ for (unsigned int i = 0; i < service->socket_count; i++) {
+ if (service->listeners[i].fd != -1 &&
+ service->listeners[i].io == NULL) {
+ if (dup2(dev_null_fd, service->listeners[i].fd) < 0)
+ lib_signals_syscall_error("signal: dup2(/dev/null, listener) failed: ");
+ service->listeners[i].closed = TRUE;
+ }
+ }
+}
+
+static void
+sig_state_changed(const siginfo_t *si ATTR_UNUSED, void *context)
+{
+ struct master_service *service = context;
+
+ master_service_refresh_login_state(service);
+}
+
+static bool
+master_service_event_callback(struct event *event,
+ enum event_callback_type type,
+ struct failure_context *ctx,
+ const char *fmt ATTR_UNUSED,
+ va_list args ATTR_UNUSED)
+{
+ if (type == EVENT_CALLBACK_TYPE_CREATE && event->parent == NULL) {
+ /* Add service:<name> category for all events. It's enough
+ to do it only for root events, because all other events
+ inherit the category from them. */
+ event_add_category(event, &master_service_category);
+ }
+ /* This callback may be called while still in master_service_init().
+ In that case master_service is NULL. */
+ if (type == EVENT_CALLBACK_TYPE_SEND && master_service != NULL &&
+ event_filter_match(master_service->process_shutdown_filter,
+ event, ctx))
+ master_service_stop_new_connections(master_service);
+ return TRUE;
+}
+
+static void master_service_verify_version_string(struct master_service *service)
+{
+ if (service->version_string != NULL &&
+ strcmp(service->version_string, PACKAGE_VERSION) != 0) {
+ i_fatal("Dovecot version mismatch: "
+ "Master is v%s, %s is v"PACKAGE_VERSION" "
+ "(if you don't care, set version_ignore=yes)",
+ service->version_string, service->name);
+ }
+}
+
+static void master_service_init_socket_listeners(struct master_service *service)
+{
+ unsigned int i;
+ const char *value;
+ bool have_ssl_sockets = FALSE;
+
+ if (service->socket_count == 0)
+ return;
+
+ service->listeners =
+ i_new(struct master_service_listener, service->socket_count);
+
+ for (i = 0; i < service->socket_count; i++) {
+ struct master_service_listener *l = &service->listeners[i];
+
+ l->service = service;
+ l->fd = MASTER_LISTEN_FD_FIRST + i;
+
+ value = getenv(t_strdup_printf("SOCKET%u_SETTINGS", i));
+ if (value != NULL) {
+ const char *const *settings =
+ t_strsplit_tabescaped(value);
+
+ if (*settings != NULL) {
+ l->name = i_strdup_empty(*settings);
+ settings++;
+ }
+ while (*settings != NULL) {
+ if (strcmp(*settings, "ssl") == 0) {
+ l->ssl = TRUE;
+ have_ssl_sockets = TRUE;
+ } else if (strcmp(*settings, "haproxy") == 0) {
+ l->haproxy = TRUE;
+ }
+ settings++;
+ }
+ }
+ }
+ service->want_ssl_server = have_ssl_sockets ||
+ (service->flags & MASTER_SERVICE_FLAG_HAVE_STARTTLS) != 0;
+}
+
+struct master_service *
+master_service_init(const char *name, enum master_service_flags flags,
+ int *argc, char **argv[], const char *getopt_str)
+{
+ struct master_service *service;
+ data_stack_frame_t datastack_frame_id = 0;
+ unsigned int count;
+ const char *service_configured_name, *value;
+
+ i_assert(name != NULL);
+
+#ifdef DEBUG
+ if (getenv("GDB") == NULL &&
+ (flags & MASTER_SERVICE_FLAG_STANDALONE) == 0) {
+ value = getenv("SOCKET_COUNT");
+ if (value == NULL || str_to_uint(value, &count) < 0)
+ count = 0;
+ fd_debug_verify_leaks(MASTER_LISTEN_FD_FIRST + count, 1024);
+ }
+#endif
+ if ((flags & MASTER_SERVICE_FLAG_STANDALONE) == 0) {
+ /* make sure we can dump core, at least until
+ privileges are dropped. (i'm not really sure why this
+ is needed, because doing the same just before exec
+ doesn't help, and exec shouldn't affect this with
+ non-setuid/gid binaries..) */
+ restrict_access_allow_coredumps(TRUE);
+ }
+
+ /* NOTE: we start rooted, so keep the code minimal until
+ restrict_access_by_env() is called */
+ lib_init();
+ /* Get the service name from environment. This usually differs from the
+ service name parameter if the executable is used for multiple
+ services. For example "auth" vs "auth-worker". It can also be a
+ service with slightly different settings, like "lmtp" vs
+ "lmtp-no-quota". We don't want to use the configured name as the
+ service's primary name, because that could break some lookups (e.g.
+ auth would suddenly see service=lmtp-no-quota. However, this can be
+ very useful in events to differentiate e.g. auth master and
+ auth-worker events which might otherwise look very similar. It's
+ also useful in log prefixes. */
+ service_configured_name = getenv(MASTER_SERVICE_ENV);
+ if (service_configured_name == NULL)
+ service_configured_name = name;
+ /* Set a logging prefix temporarily. This will be ignored once the log
+ is properly initialized */
+ i_set_failure_prefix("%s(init): ", service_configured_name);
+
+ /* make sure all the data stack allocations during init will be freed
+ before we get to ioloop. the corresponding t_pop() is in
+ master_service_init_finish(). */
+ if ((flags & MASTER_SERVICE_FLAG_NO_INIT_DATASTACK_FRAME) == 0)
+ datastack_frame_id = t_push("master_service_init");
+
+ /* ignore these signals as early as possible */
+ lib_signals_init();
+ lib_signals_ignore(SIGPIPE, TRUE);
+ lib_signals_ignore(SIGALRM, FALSE);
+
+ if (getenv(MASTER_UID_ENV) == NULL)
+ flags |= MASTER_SERVICE_FLAG_STANDALONE;
+
+ process_title_init(*argc, argv);
+
+ /* process_title_init() might destroy all environments.
+ Need to look this up again. */
+ service_configured_name = getenv(MASTER_SERVICE_ENV);
+ if (service_configured_name == NULL)
+ service_configured_name = name;
+
+ service = i_new(struct master_service, 1);
+ service->argc = *argc;
+ service->argv = *argv;
+ service->name = i_strdup(name);
+ service->configured_name = i_strdup(service_configured_name);
+ /* keep getopt_str first in case it contains "+" */
+ service->getopt_str = *getopt_str == '\0' ?
+ i_strdup(master_service_getopt_string()) :
+ i_strconcat(getopt_str, master_service_getopt_string(), NULL);
+ service->flags = flags;
+ service->ioloop = io_loop_create();
+ service->service_count_left = UINT_MAX;
+ service->config_fd = -1;
+ service->datastack_frame_id = datastack_frame_id;
+
+ service->config_path = i_strdup(getenv(MASTER_CONFIG_FILE_ENV));
+ if (service->config_path == NULL)
+ service->config_path = i_strdup(DEFAULT_CONFIG_FILE_PATH);
+ else
+ service->config_path_from_master = TRUE;
+
+ if ((flags & MASTER_SERVICE_FLAG_STANDALONE) == 0) {
+ service->version_string = getenv(MASTER_DOVECOT_VERSION_ENV);
+ service->socket_count = 1;
+ } else {
+ service->version_string = PACKAGE_VERSION;
+ }
+
+ /* listener configuration */
+ value = getenv("SOCKET_COUNT");
+ if (value != NULL && str_to_uint(value, &service->socket_count) < 0)
+ i_fatal("Invalid SOCKET_COUNT environment");
+ T_BEGIN {
+ master_service_init_socket_listeners(service);
+ } T_END;
+
+#ifdef HAVE_SSL
+ /* Load the SSL module if we already know it is necessary. It can also
+ get loaded later on-demand. */
+ if (service->want_ssl_server) {
+ const char *error;
+ if (ssl_module_load(&error) < 0)
+ i_fatal("Cannot load SSL module: %s", error);
+ }
+#endif
+
+ /* set up some kind of logging until we know exactly how and where
+ we want to log */
+ if (getenv("LOG_SERVICE") != NULL)
+ i_set_failure_internal();
+ if (getenv("USER") != NULL) {
+ i_set_failure_prefix("%s(%s): ", service->configured_name,
+ getenv("USER"));
+ } else {
+ i_set_failure_prefix("%s: ", service->configured_name);
+ }
+
+ master_service_category_name =
+ i_strdup_printf("service:%s", service->configured_name);
+ master_service_category.name = master_service_category_name;
+ event_register_callback(master_service_event_callback);
+
+ /* Initialize debug logging */
+ value = getenv(DOVECOT_LOG_DEBUG_ENV);
+ if (value != NULL) {
+ struct event_filter *filter;
+ const char *error;
+ filter = event_filter_create();
+ if (event_filter_parse(value, filter, &error) < 0) {
+ i_error("Invalid "DOVECOT_LOG_DEBUG_ENV" - ignoring: %s",
+ error);
+ } else {
+ event_set_global_debug_log_filter(filter);
+ }
+ event_filter_unref(&filter);
+ }
+
+ if ((flags & MASTER_SERVICE_FLAG_STANDALONE) == 0) {
+ /* initialize master_status structure */
+ value = getenv(MASTER_UID_ENV);
+ if (value == NULL ||
+ str_to_uint(value, &service->master_status.uid) < 0)
+ i_fatal(MASTER_UID_ENV" missing");
+ service->master_status.pid = getpid();
+
+ /* set the default limit */
+ value = getenv(MASTER_CLIENT_LIMIT_ENV);
+ if (value == NULL || str_to_uint(value, &count) < 0 ||
+ count == 0)
+ i_fatal(MASTER_CLIENT_LIMIT_ENV" missing");
+ master_service_set_client_limit(service, count);
+
+ /* seve the process limit */
+ value = getenv(MASTER_PROCESS_LIMIT_ENV);
+ if (value != NULL && str_to_uint(value, &count) == 0 &&
+ count > 0)
+ service->process_limit = count;
+
+ value = getenv(MASTER_PROCESS_MIN_AVAIL_ENV);
+ if (value != NULL && str_to_uint(value, &count) == 0 &&
+ count > 0)
+ service->process_min_avail = count;
+
+ /* set the default service count */
+ value = getenv(MASTER_SERVICE_COUNT_ENV);
+ if (value != NULL && str_to_uint(value, &count) == 0 &&
+ count > 0)
+ master_service_set_service_count(service, count);
+
+ /* set the idle kill timeout */
+ value = getenv(MASTER_SERVICE_IDLE_KILL_ENV);
+ if (value != NULL && str_to_uint(value, &count) == 0)
+ service->idle_kill_secs = count;
+ } else {
+ master_service_set_client_limit(service, 1);
+ master_service_set_service_count(service, 1);
+ }
+ if ((flags & MASTER_SERVICE_FLAG_KEEP_CONFIG_OPEN) != 0) {
+ /* since we're going to keep the config socket open anyway,
+ open it now so we can read settings even after privileges
+ are dropped. */
+ master_service_config_socket_try_open(service);
+ }
+ if ((flags & MASTER_SERVICE_FLAG_DONT_SEND_STATS) == 0) {
+ /* Initialize stats-client early so it can see all events. */
+ value = getenv(DOVECOT_STATS_WRITER_SOCKET_PATH);
+ if (value != NULL && value[0] != '\0')
+ service->stats_client = stats_client_init(value, FALSE);
+ }
+
+ master_service_verify_version_string(service);
+ return service;
+}
+
+int master_getopt(struct master_service *service)
+{
+ int c;
+
+ i_assert(master_getopt_str_is_valid(service->getopt_str));
+
+ while ((c = getopt(service->argc, service->argv,
+ service->getopt_str)) > 0) {
+ if (!master_service_parse_option(service, c, optarg))
+ break;
+ }
+ return c;
+}
+
+bool master_getopt_str_is_valid(const char *str)
+{
+ unsigned int i, j;
+
+ /* make sure there are no duplicates. there are few enough characters
+ that this should be fast enough. */
+ for (i = 0; str[i] != '\0'; i++) {
+ if (str[i] == ':' || str[i] == '+' || str[i] == '-')
+ continue;
+ for (j = i+1; str[j] != '\0'; j++) {
+ if (str[i] == str[j])
+ return FALSE;
+ }
+ }
+ return TRUE;
+}
+
+static bool
+master_service_try_init_log(struct master_service *service,
+ const char *prefix)
+{
+ const char *path, *timestamp;
+
+ if ((service->flags & MASTER_SERVICE_FLAG_STANDALONE) != 0 &&
+ (service->flags & MASTER_SERVICE_FLAG_DONT_LOG_TO_STDERR) == 0) {
+ timestamp = getenv("LOG_STDERR_TIMESTAMP");
+ if (timestamp != NULL)
+ i_set_failure_timestamp_format(timestamp);
+ i_set_failure_file("/dev/stderr", "");
+ return TRUE;
+ }
+
+ if (getenv("LOG_SERVICE") != NULL && !service->log_directly) {
+ /* logging via log service */
+ i_set_failure_internal();
+ i_set_failure_prefix("%s", prefix);
+ return TRUE;
+ }
+
+ if (service->set == NULL) {
+ i_set_failure_file("/dev/stderr", prefix);
+ /* may be called again after we have settings */
+ return FALSE;
+ }
+
+ if (strcmp(service->set->log_path, "syslog") != 0) {
+ /* error logging goes to file or stderr */
+ path = home_expand(service->set->log_path);
+ i_set_failure_file(path, prefix);
+ }
+
+ if (strcmp(service->set->log_path, "syslog") == 0 ||
+ strcmp(service->set->info_log_path, "syslog") == 0 ||
+ strcmp(service->set->debug_log_path, "syslog") == 0) {
+ /* something gets logged to syslog */
+ int facility;
+
+ if (!syslog_facility_find(service->set->syslog_facility,
+ &facility))
+ facility = LOG_MAIL;
+ i_set_failure_syslog(service->set->instance_name, LOG_NDELAY,
+ facility);
+ i_set_failure_prefix("%s", prefix);
+
+ if (strcmp(service->set->log_path, "syslog") != 0) {
+ /* set error handlers back to file */
+ i_set_fatal_handler(default_fatal_handler);
+ i_set_error_handler(default_error_handler);
+ }
+ }
+
+ if (*service->set->info_log_path != '\0' &&
+ strcmp(service->set->info_log_path, "syslog") != 0) {
+ path = home_expand(service->set->info_log_path);
+ if (*path != '\0')
+ i_set_info_file(path);
+ }
+
+ if (*service->set->debug_log_path != '\0' &&
+ strcmp(service->set->debug_log_path, "syslog") != 0) {
+ path = home_expand(service->set->debug_log_path);
+ if (*path != '\0')
+ i_set_debug_file(path);
+ }
+ i_set_failure_timestamp_format(service->set->log_timestamp);
+ return TRUE;
+}
+
+void master_service_init_log(struct master_service *service)
+{
+ master_service_init_log_with_prefix(service, t_strdup_printf(
+ "%s: ", service->configured_name));
+}
+
+void master_service_init_log_with_prefix(struct master_service *service,
+ const char *prefix)
+{
+ if (service->log_initialized) {
+ /* change only the prefix */
+ i_set_failure_prefix("%s", prefix);
+ return;
+ }
+ if (master_service_try_init_log(service, prefix))
+ service->log_initialized = TRUE;
+}
+
+void master_service_init_log_with_pid(struct master_service *service)
+{
+ master_service_init_log_with_prefix(service, t_strdup_printf(
+ "%s(%s): ", service->configured_name, my_pid));
+}
+
+void master_service_init_stats_client(struct master_service *service,
+ bool silent_notfound_errors)
+{
+ if (service->stats_client == NULL &&
+ service->set->stats_writer_socket_path[0] != '\0') T_BEGIN {
+ const char *path = t_strdup_printf("%s/%s",
+ service->set->base_dir,
+ service->set->stats_writer_socket_path);
+ service->stats_client =
+ stats_client_init(path, silent_notfound_errors);
+ } T_END;
+}
+
+void master_service_set_die_with_master(struct master_service *service,
+ bool set)
+{
+ service->die_with_master = set;
+}
+
+void master_service_set_die_callback(struct master_service *service,
+ void (*callback)(void))
+{
+ service->die_callback = callback;
+}
+
+void master_service_set_idle_die_callback(struct master_service *service,
+ bool (*callback)(void))
+{
+ service->idle_die_callback = callback;
+}
+
+static bool get_instance_config(const char *name, const char **config_path_r)
+{
+ struct master_instance_list *list;
+ const struct master_instance *inst;
+ const char *instance_path, *path;
+
+ /* note that we don't have any settings yet. we're just finding out
+ which dovecot.conf we even want to read! so we must use the
+ hardcoded state_dir path. */
+ instance_path = t_strconcat(PKG_STATEDIR"/"MASTER_INSTANCE_FNAME, NULL);
+ list = master_instance_list_init(instance_path);
+ inst = master_instance_list_find_by_name(list, name);
+ if (inst != NULL) {
+ path = t_strdup_printf("%s/dovecot.conf", inst->base_dir);
+ const char *error;
+ if (t_readlink(path, config_path_r, &error) < 0)
+ i_fatal("t_readlink(%s) failed: %s", path, error);
+ }
+ master_instance_list_deinit(&list);
+ return inst != NULL;
+}
+
+bool master_service_parse_option(struct master_service *service,
+ int opt, const char *arg)
+{
+ const char *path;
+
+ switch (opt) {
+ case 'c':
+ i_free(service->config_path);
+ service->config_path = i_strdup(arg);
+ service->config_path_changed_with_param = TRUE;
+ service->config_path_from_master = FALSE;
+ break;
+ case 'i':
+ if (!get_instance_config(arg, &path))
+ i_fatal("Unknown instance name: %s", arg);
+ service->config_path = i_strdup(path);
+ service->config_path_changed_with_param = TRUE;
+ break;
+ case 'k':
+ service->keep_environment = TRUE;
+ break;
+ case 'o':
+ if (!array_is_created(&service->config_overrides))
+ i_array_init(&service->config_overrides, 16);
+ array_push_back(&service->config_overrides, &arg);
+ break;
+ case 'O':
+ service->flags |= MASTER_SERVICE_FLAG_NO_CONFIG_SETTINGS;
+ break;
+ case 'L':
+ service->log_directly = TRUE;
+ break;
+ default:
+ return FALSE;
+ }
+ return TRUE;
+}
+
+static void master_service_error(struct master_service *service)
+{
+ master_service_stop_new_connections(service);
+ if (service->master_status.available_count ==
+ service->total_available_count || service->die_with_master) {
+ if (service->die_callback == NULL)
+ master_service_stop(service);
+ else {
+ service->to_die =
+ timeout_add(MASTER_SERVICE_DIE_TIMEOUT_MSECS,
+ master_service_stop,
+ service);
+ service->die_callback();
+ }
+ }
+}
+
+static void master_status_error(struct master_service *service)
+{
+ /* status fd is a write-only pipe, so if we're here it means the
+ master wants us to die (or died itself). don't die until all
+ service connections are finished. */
+ io_remove(&service->io_status_error);
+
+ /* the log fd may also be closed already, don't die when trying to
+ log later */
+ i_set_failure_ignore_errors(TRUE);
+
+ master_service_error(service);
+}
+
+void master_service_init_finish(struct master_service *service)
+{
+ enum libsig_flags sigint_flags = LIBSIG_FLAG_DELAYED;
+ struct stat st;
+
+ i_assert(!service->init_finished);
+ service->init_finished = TRUE;
+
+ /* From now on we'll abort() if exit() is called unexpectedly. */
+ lib_set_clean_exit(FALSE);
+
+ /* set default signal handlers */
+ if ((service->flags & MASTER_SERVICE_FLAG_STANDALONE) == 0)
+ sigint_flags |= LIBSIG_FLAG_RESTART;
+ lib_signals_set_handler(SIGINT, sigint_flags, sig_die, service);
+ lib_signals_set_handler(SIGTERM, LIBSIG_FLAG_DELAYED, sig_die, service);
+ if ((service->flags & MASTER_SERVICE_FLAG_TRACK_LOGIN_STATE) != 0) {
+ lib_signals_set_handler(SIGUSR1, LIBSIG_FLAGS_SAFE,
+ sig_state_changed, service);
+ }
+
+ if ((service->flags & MASTER_SERVICE_FLAG_STANDALONE) == 0) {
+ if (fstat(MASTER_STATUS_FD, &st) < 0 || !S_ISFIFO(st.st_mode))
+ i_fatal("Must be started by dovecot master process");
+
+ /* start listening errors for status fd, it means master died */
+ service->io_status_error = io_add(MASTER_DEAD_FD, IO_ERROR,
+ master_status_error, service);
+ lib_signals_set_handler(SIGQUIT, 0, sig_close_listeners, service);
+ }
+ master_service_io_listeners_add(service);
+ if (service->want_ssl_server &&
+ (service->flags & MASTER_SERVICE_FLAG_NO_SSL_INIT) == 0)
+ master_service_ssl_ctx_init(service);
+
+ if ((service->flags & MASTER_SERVICE_FLAG_STD_CLIENT) != 0) {
+ /* we already have a connection to be served */
+ service->master_status.available_count--;
+ }
+ master_status_update(service);
+
+ /* close data stack frame opened by master_service_init() */
+ if ((service->flags & MASTER_SERVICE_FLAG_NO_INIT_DATASTACK_FRAME) == 0) {
+ if (!t_pop(&service->datastack_frame_id))
+ i_panic("Leaked t_pop() call");
+ }
+}
+
+static void master_service_import_environment_real(const char *import_environment)
+{
+ const char *const *envs, *key, *value;
+ ARRAY_TYPE(const_string) keys;
+
+ if (*import_environment == '\0')
+ return;
+
+ t_array_init(&keys, 8);
+ /* preserve existing DOVECOT_PRESERVE_ENVS */
+ value = getenv(DOVECOT_PRESERVE_ENVS_ENV);
+ if (value != NULL)
+ array_push_back(&keys, &value);
+#ifdef HAVE_LIBSYSTEMD
+ /* Always import systemd variables, otherwise it is possible to break
+ systemd startup in obscure ways. */
+ value = "NOTIFY_SOCKET LISTEN_FDS LISTEN_PID";
+ array_push_back(&keys, &value);
+#endif
+ /* add new environments */
+ envs = t_strsplit_spaces(import_environment, " ");
+ for (; *envs != NULL; envs++) {
+ value = strchr(*envs, '=');
+ if (value == NULL)
+ key = *envs;
+ else {
+ key = t_strdup_until(*envs, value++);
+ env_put(key, value);
+ }
+ array_push_back(&keys, &key);
+ }
+ array_append_zero(&keys);
+
+ value = t_strarray_join(array_front(&keys), " ");
+ env_put(DOVECOT_PRESERVE_ENVS_ENV, value);
+}
+
+void master_service_import_environment(const char *import_environment)
+{
+ T_BEGIN {
+ master_service_import_environment_real(import_environment);
+ } T_END;
+}
+
+void master_service_env_clean(void)
+{
+ const char *value = getenv(DOVECOT_PRESERVE_ENVS_ENV);
+
+ if (value == NULL || *value == '\0')
+ env_clean();
+ else T_BEGIN {
+ value = t_strconcat(value, " "DOVECOT_PRESERVE_ENVS_ENV, NULL);
+ env_clean_except(t_strsplit_spaces(value, " "));
+ } T_END;
+}
+
+void master_service_set_client_limit(struct master_service *service,
+ unsigned int client_limit)
+{
+ unsigned int used;
+
+ i_assert(service->master_status.available_count ==
+ service->total_available_count);
+
+ used = service->total_available_count -
+ service->master_status.available_count;
+ i_assert(client_limit >= used);
+
+ service->total_available_count = client_limit;
+ service->master_status.available_count = client_limit - used;
+}
+
+unsigned int master_service_get_client_limit(struct master_service *service)
+{
+ return service->total_available_count;
+}
+
+unsigned int master_service_get_process_limit(struct master_service *service)
+{
+ return service->process_limit;
+}
+
+unsigned int master_service_get_process_min_avail(struct master_service *service)
+{
+ return service->process_min_avail;
+}
+
+unsigned int master_service_get_idle_kill_secs(struct master_service *service)
+{
+ return service->idle_kill_secs;
+}
+
+void master_service_set_service_count(struct master_service *service,
+ unsigned int count)
+{
+ unsigned int used;
+
+ used = service->total_available_count -
+ service->master_status.available_count;
+ i_assert(count >= used);
+
+ if (service->total_available_count > count) {
+ service->total_available_count = count;
+ service->master_status.available_count = count - used;
+ }
+ service->service_count_left = count;
+}
+
+unsigned int master_service_get_service_count(struct master_service *service)
+{
+ return service->service_count_left;
+}
+
+unsigned int master_service_get_socket_count(struct master_service *service)
+{
+ return service->socket_count;
+}
+
+const char *master_service_get_socket_name(struct master_service *service,
+ int listen_fd)
+{
+ unsigned int i;
+
+ i_assert(listen_fd >= MASTER_LISTEN_FD_FIRST);
+
+ i = listen_fd - MASTER_LISTEN_FD_FIRST;
+ i_assert(i < service->socket_count);
+ return service->listeners[i].name != NULL ?
+ service->listeners[i].name : "";
+}
+
+void master_service_set_avail_overflow_callback(struct master_service *service,
+ master_service_avail_overflow_callback_t *callback)
+{
+ service->avail_overflow_callback = callback;
+}
+
+const char *master_service_get_config_path(struct master_service *service)
+{
+ return service->config_path;
+}
+
+const char *master_service_get_version_string(struct master_service *service)
+{
+ return service->version_string;
+}
+
+const char *master_service_get_name(struct master_service *service)
+{
+ return service->name;
+}
+
+const char *master_service_get_configured_name(struct master_service *service)
+{
+ return service->configured_name;
+}
+
+void master_service_run(struct master_service *service,
+ master_service_connection_callback_t *callback)
+{
+ service->callback = callback;
+ io_loop_run(service->ioloop);
+ service->callback = NULL;
+}
+
+void master_service_stop(struct master_service *service)
+{
+ io_loop_stop(service->ioloop);
+}
+
+void master_service_stop_new_connections(struct master_service *service)
+{
+ unsigned int current_count;
+
+ if (service->stopping)
+ return;
+
+ service->stopping = TRUE;
+ master_service_io_listeners_remove(service);
+ master_service_io_listeners_close(service);
+
+ /* make sure we stop after servicing current connections */
+ current_count = service->total_available_count -
+ service->master_status.available_count;
+ service->service_count_left = current_count;
+ service->total_available_count = current_count;
+
+ if (current_count == 0)
+ master_service_stop(service);
+ else {
+ /* notify master that we're not accepting any more
+ connections */
+ service->master_status.available_count = 0;
+ master_status_update(service);
+ }
+ if (service->login != NULL)
+ master_login_stop(service->login);
+}
+
+bool master_service_is_killed(struct master_service *service)
+{
+ return service->killed;
+}
+
+bool master_service_is_master_stopped(struct master_service *service)
+{
+ return service->io_status_error == NULL &&
+ (service->flags & MASTER_SERVICE_FLAG_STANDALONE) == 0;
+}
+
+void master_service_anvil_send(struct master_service *service, const char *cmd)
+{
+ ssize_t ret;
+
+ if ((service->flags & MASTER_SERVICE_FLAG_STANDALONE) != 0)
+ return;
+
+ ret = write(MASTER_ANVIL_FD, cmd, strlen(cmd));
+ if (ret < 0) {
+ if (errno == EPIPE) {
+ /* anvil process was probably recreated, don't bother
+ logging an error about losing connection to it */
+ return;
+ }
+ i_error("write(anvil) failed: %m");
+ } else if (ret == 0)
+ i_error("write(anvil) failed: EOF");
+ else {
+ i_assert((size_t)ret == strlen(cmd));
+ }
+}
+
+void master_service_client_connection_created(struct master_service *service)
+{
+ i_assert(service->master_status.available_count > 0);
+ service->master_status.available_count--;
+ master_status_update(service);
+}
+
+static bool master_service_want_listener(struct master_service *service)
+{
+ if (service->master_status.available_count > 0) {
+ /* more concurrent clients can still be added */
+ return TRUE;
+ }
+ if (service->service_count_left == 1) {
+ /* after handling this client, the whole process will stop. */
+ return FALSE;
+ }
+ if (service->avail_overflow_callback != NULL) {
+ /* overflow callback is set. it's possible that the current
+ existing client may be replaced by a new client, which needs
+ the listener to try to accept new connections. */
+ return TRUE;
+ }
+ /* the listener isn't needed until the current client is disconnected */
+ return FALSE;
+}
+
+void master_service_client_connection_handled(struct master_service *service,
+ struct master_service_connection *conn)
+{
+ if (!conn->accepted) {
+ if (close(conn->fd) < 0)
+ i_error("close(service connection) failed: %m");
+ master_service_client_connection_destroyed(service);
+ } else if (conn->fifo) {
+ /* reading FIFOs stays open forever, don't count them
+ as real clients */
+ master_service_client_connection_destroyed(service);
+ }
+ if (!master_service_want_listener(service)) {
+ i_assert(service->listeners != NULL);
+ master_service_io_listeners_remove(service);
+ if (service->service_count_left == 1 &&
+ service->avail_overflow_callback == NULL) {
+ /* we're not going to accept any more connections after
+ this. go ahead and close the connection early. don't
+ do this before calling callback, because it may want
+ to access the listen_fd (e.g. to check socket
+ permissions).
+
+ Don't do this if overflow callback is set, because
+ otherwise it's never called with service_count=1.
+ Actually this isn't important anymore to do with
+ any service, since nowadays master can request the
+ listeners to be closed via SIGQUIT. Still, closing
+ the fd when possible saves a little bit of memory. */
+ master_service_io_listeners_close(service);
+ }
+ }
+}
+
+void master_service_client_connection_callback(struct master_service *service,
+ struct master_service_connection *conn)
+{
+ service->callback(conn);
+
+ master_service_client_connection_handled(service, conn);
+}
+
+void master_service_client_connection_accept(struct master_service_connection *conn)
+{
+ conn->accepted = TRUE;
+}
+
+void master_service_client_connection_destroyed(struct master_service *service)
+{
+ /* we can listen again */
+ master_service_io_listeners_add(service);
+
+ i_assert(service->total_available_count > 0);
+ i_assert(service->service_count_left > 0);
+
+ if (service->service_count_left == service->total_available_count) {
+ service->total_available_count--;
+ service->service_count_left--;
+ } else {
+ if (service->service_count_left != UINT_MAX)
+ service->service_count_left--;
+
+ i_assert(service->master_status.available_count <
+ service->total_available_count);
+ service->master_status.available_count++;
+ }
+
+ if (service->service_count_left == 0) {
+ i_assert(service->master_status.available_count ==
+ service->total_available_count);
+ master_service_stop(service);
+ } else if ((service->io_status_error == NULL ||
+ service->listeners == NULL) &&
+ service->master_status.available_count ==
+ service->total_available_count) {
+ /* we've finished handling all clients, and
+ a) master has closed the connection
+ b) there are no listeners (std-client?) */
+ master_service_stop(service);
+ } else {
+ master_status_update(service);
+ }
+}
+
+static void master_service_set_login_state(struct master_service *service,
+ enum master_login_state state)
+{
+ timeout_remove(&service->to_overflow_state);
+
+ switch (state) {
+ case MASTER_LOGIN_STATE_NONFULL:
+ service->call_avail_overflow = FALSE;
+ if (service->master_status.available_count > 0)
+ return;
+
+ /* some processes should now be able to handle new connections,
+ although we can't. but there may be race conditions, so
+ make sure that we'll check again soon if the state has
+ changed to "full" without our knowledge. */
+ service->to_overflow_state =
+ timeout_add(MASTER_SERVICE_STATE_CHECK_MSECS,
+ master_service_refresh_login_state,
+ service);
+ return;
+ case MASTER_LOGIN_STATE_FULL:
+ /* make sure we're listening for more connections */
+ service->call_avail_overflow = TRUE;
+ master_service_io_listeners_add(service);
+ return;
+ }
+ i_error("Invalid master login state: %d", state);
+}
+
+static int master_service_get_login_state(enum master_login_state *state_r)
+{
+ off_t ret;
+
+ ret = lseek(MASTER_LOGIN_NOTIFY_FD, 0, SEEK_CUR);
+ if (ret < 0) {
+ i_error("lseek(login notify fd) failed: %m");
+ return -1;
+ }
+ *state_r = ret == MASTER_LOGIN_STATE_FULL ?
+ MASTER_LOGIN_STATE_FULL : MASTER_LOGIN_STATE_NONFULL;
+ return 0;
+}
+
+static void master_service_refresh_login_state(struct master_service *service)
+{
+ enum master_login_state state;
+
+ if (master_service_get_login_state(&state) == 0)
+ master_service_set_login_state(service, state);
+}
+
+void master_service_close_config_fd(struct master_service *service)
+{
+ i_close_fd(&service->config_fd);
+}
+
+static void master_service_deinit_real(struct master_service **_service)
+{
+ struct master_service *service = *_service;
+
+ *_service = NULL;
+
+ if (!service->init_finished &&
+ (service->flags & MASTER_SERVICE_FLAG_NO_INIT_DATASTACK_FRAME) == 0) {
+ if (!t_pop(&service->datastack_frame_id))
+ i_panic("Leaked t_pop() call");
+ }
+ master_service_haproxy_abort(service);
+
+ master_service_io_listeners_remove(service);
+ master_service_ssl_ctx_deinit(service);
+
+ if (service->stats_client != NULL)
+ stats_client_deinit(&service->stats_client);
+ master_service_close_config_fd(service);
+ timeout_remove(&service->to_overflow_call);
+ timeout_remove(&service->to_die);
+ timeout_remove(&service->to_overflow_state);
+ timeout_remove(&service->to_status);
+ io_remove(&service->io_status_error);
+ io_remove(&service->io_status_write);
+ if (array_is_created(&service->config_overrides))
+ array_free(&service->config_overrides);
+
+ if (service->set_parser != NULL) {
+ settings_parser_deinit(&service->set_parser);
+ pool_unref(&service->set_pool);
+ }
+ i_free(master_service_category_name);
+ master_service_category.name = NULL;
+ event_unregister_callback(master_service_event_callback);
+ master_service_unset_process_shutdown_filter(service);
+}
+
+static void master_service_free(struct master_service *service)
+{
+ unsigned int i;
+
+ for (i = 0; i < service->socket_count; i++)
+ i_free(service->listeners[i].name);
+ i_free(service->listeners);
+ i_free(service->getopt_str);
+ i_free(service->configured_name);
+ i_free(service->name);
+ i_free(service->config_path);
+ i_free(service);
+}
+
+void master_service_deinit(struct master_service **_service)
+{
+ struct master_service *service = *_service;
+
+ master_service_deinit_real(_service);
+
+ lib_signals_deinit();
+ /* run atexit callbacks before destroying ioloop */
+ lib_atexit_run();
+ io_loop_destroy(&service->ioloop);
+
+ master_service_free(service);
+ lib_deinit();
+}
+
+void master_service_deinit_forked(struct master_service **_service)
+{
+ struct master_service *service = *_service;
+
+ master_service_deinit_real(_service);
+ io_loop_destroy(&service->ioloop);
+
+ master_service_free(service);
+}
+
+static void master_service_overflow(struct master_service *service)
+{
+ enum master_login_state state;
+ struct timeval created;
+
+ timeout_remove(&service->to_overflow_call);
+
+ if (master_service_get_login_state(&state) < 0 ||
+ state != MASTER_LOGIN_STATE_FULL) {
+ /* service is no longer full (or we couldn't check if it is) */
+ return;
+ }
+
+ if (!service->avail_overflow_callback(TRUE, &created)) {
+ /* can't kill the client anymore after all */
+ return;
+ }
+ if (service->master_status.available_count == 0) {
+ /* Client was destroyed, but service_count is now 0.
+ The servive was already stopped, so the process will
+ shutdown and a new process can handle the waiting client
+ connection. */
+ i_assert(service->service_count_left == 0);
+ i_assert(!io_loop_is_running(service->ioloop));
+ return;
+ }
+ master_service_io_listeners_add(service);
+
+ /* The connection is soon accepted by the listener IO callback.
+ Note that this often results in killing two connections, because
+ after the first process has accepted the new client the service is
+ full again. The second process sees this and kills another client.
+ After this the other processes see that the service is no longer
+ full and kill no more clients. */
+}
+
+static unsigned int
+master_service_overflow_timeout_msecs(const struct timeval *created)
+{
+ /* Returns a value between 0..max_wait. The oldest clients return the
+ lowest wait so they get killed before newer clients. For simplicity
+ this code treats all clients older than 10 seconds the same. */
+ const unsigned int max_wait = 100;
+ const int max_since = 10*1000;
+ int created_since = timeval_diff_msecs(&ioloop_timeval, created);
+ unsigned int msecs;
+
+ created_since = I_MAX(created_since, 0);
+ created_since = I_MIN(created_since, max_since);
+
+ msecs = created_since * max_wait / max_since;
+ i_assert(msecs <= max_wait);
+ msecs = max_wait - msecs;
+
+ /* Add some extra randomness, so even if all clients have exactly the
+ same creation time they won't all be killed. */
+ return msecs + i_rand_limit(10);
+}
+
+static bool master_service_full(struct master_service *service)
+{
+ struct timeval created;
+
+ /* This process can't handle any more connections. */
+ if (!service->call_avail_overflow ||
+ service->avail_overflow_callback == NULL)
+ return TRUE;
+
+ /* Master has notified us that all processes are full, and
+ we have the ability to kill old connections. */
+ if (service->total_available_count > 1) {
+ /* This process can still create multiple concurrent
+ clients if we just kill some of the existing ones.
+ Do it immediately. */
+ return !service->avail_overflow_callback(TRUE, &created);
+ }
+
+ /* This process can't create more than a single client. Most likely
+ running with service_count=1. Check the overflow again after a short
+ delay before killing anything. This way only some of the connections
+ get killed instead of all of them. The delay is based on the
+ connection age with a bit of randomness, so the oldest connections
+ should die first, but even if all the connections have time same
+ timestamp they still don't all die at once. */
+ if (!service->avail_overflow_callback(FALSE, &created)) {
+ /* can't kill any clients */
+ return TRUE;
+ }
+ i_assert(service->to_overflow_call == NULL);
+ service->to_overflow_call =
+ timeout_add(master_service_overflow_timeout_msecs(&created),
+ master_service_overflow, service);
+ return TRUE;
+}
+
+static void master_service_listen(struct master_service_listener *l)
+{
+ struct master_service *service = l->service;
+ struct master_service_connection conn;
+
+ if (service->master_status.available_count == 0) {
+ if (master_service_full(service)) {
+ /* Stop the listener until a client has disconnected or
+ overflow callback has killed one. */
+ master_service_io_listeners_remove(service);
+ return;
+ }
+ /* we can accept another client */
+ i_assert(service->master_status.available_count > 0);
+ }
+
+ i_zero(&conn);
+ conn.listen_fd = l->fd;
+ conn.fd = net_accept(l->fd, &conn.remote_ip, &conn.remote_port);
+ if (conn.fd < 0) {
+ struct stat st;
+ int orig_errno = errno;
+
+ if (conn.fd == -1)
+ return;
+
+ if (errno == ENOTSOCK) {
+ /* it's not a socket. should be a fifo. */
+ } else if (errno == EINVAL &&
+ (fstat(l->fd, &st) == 0 && S_ISFIFO(st.st_mode))) {
+ /* BSDI fails accept(fifo) with EINVAL. */
+ } else {
+ errno = orig_errno;
+ i_error("net_accept() failed: %m");
+ /* try again later after one of the existing
+ connections has died */
+ master_service_io_listeners_remove(service);
+ return;
+ }
+ /* use the "listener" as the connection fd and stop the
+ listener. */
+ conn.fd = l->fd;
+ conn.listen_fd = l->fd;
+ conn.fifo = TRUE;
+
+ io_remove(&l->io);
+ l->fd = -1;
+ }
+ conn.ssl = l->ssl;
+ conn.name = master_service_get_socket_name(service, conn.listen_fd);
+
+ (void)net_getsockname(conn.fd, &conn.local_ip, &conn.local_port);
+ conn.real_remote_ip = conn.remote_ip;
+ conn.real_remote_port = conn.remote_port;
+ conn.real_local_ip = conn.local_ip;
+ conn.real_local_port = conn.local_port;
+
+ net_set_nonblock(conn.fd, TRUE);
+
+ master_service_client_connection_created(service);
+ if (l->haproxy)
+ master_service_haproxy_new(service, &conn);
+ else
+ master_service_client_connection_callback(service, &conn);
+}
+
+void master_service_io_listeners_add(struct master_service *service)
+{
+ unsigned int i;
+
+ /* If there's a pending overflow call, remove it now since new
+ clients just became available. */
+ timeout_remove(&service->to_overflow_call);
+
+ if (service->stopping)
+ return;
+
+ for (i = 0; i < service->socket_count; i++) {
+ struct master_service_listener *l = &service->listeners[i];
+
+ if (l->io == NULL && l->fd != -1 && !l->closed) {
+ l->io = io_add(MASTER_LISTEN_FD_FIRST + i, IO_READ,
+ master_service_listen, l);
+ }
+ }
+}
+
+void master_service_io_listeners_remove(struct master_service *service)
+{
+ unsigned int i;
+
+ for (i = 0; i < service->socket_count; i++) {
+ io_remove(&service->listeners[i].io);
+ }
+}
+
+void master_service_ssl_io_listeners_remove(struct master_service *service)
+{
+ unsigned int i;
+
+ for (i = 0; i < service->socket_count; i++) {
+ if (service->listeners[i].io != NULL &&
+ service->listeners[i].ssl)
+ io_remove(&service->listeners[i].io);
+ }
+}
+
+static void master_service_io_listeners_close(struct master_service *service)
+{
+ unsigned int i;
+
+ /* close via listeners. some fds might be pipes that are
+ currently handled as clients. we don't want to close them. */
+ for (i = 0; i < service->socket_count; i++) {
+ if (service->listeners[i].fd != -1) {
+ if (close(service->listeners[i].fd) < 0) {
+ i_error("close(listener %d) failed: %m",
+ service->listeners[i].fd);
+ }
+ service->listeners[i].fd = -1;
+ }
+ }
+}
+
+static bool master_status_update_is_important(struct master_service *service)
+{
+ if (service->master_status.available_count == 0) {
+ /* client_limit reached for this process */
+ return TRUE;
+ }
+ if (service->last_sent_status_avail_count == 0) {
+ /* This process can now handle more clients. This is important
+ to know for master if all the existing processes have
+ avail_count=0 so it doesn't unnecessarily create more
+ processes. */
+ return TRUE;
+ }
+ /* The previous check should have triggered also for the initial
+ status notification. */
+ i_assert(service->initial_status_sent);
+ return FALSE;
+}
+
+static void
+master_status_send(struct master_service *service, bool important_update)
+{
+ ssize_t ret;
+
+ timeout_remove(&service->to_status);
+
+ ret = write(MASTER_STATUS_FD, &service->master_status,
+ sizeof(service->master_status));
+ if (ret == (ssize_t)sizeof(service->master_status)) {
+ /* success */
+ io_remove(&service->io_status_write);
+ service->last_sent_status_time = ioloop_time;
+ service->last_sent_status_avail_count =
+ service->master_status.available_count;
+ service->initial_status_sent = TRUE;
+ } else if (ret >= 0) {
+ /* shouldn't happen? */
+ i_error("write(master_status_fd) returned %d", (int)ret);
+ service->master_status.pid = 0;
+ } else if (errno != EAGAIN) {
+ /* failure */
+ if (errno != EPIPE)
+ i_error("write(master_status_fd) failed: %m");
+ service->master_status.pid = 0;
+ } else if (important_update) {
+ /* reader is busy, but it's important to get this notification
+ through. send it when possible. */
+ if (service->io_status_write == NULL) {
+ service->io_status_write =
+ io_add(MASTER_STATUS_FD, IO_WRITE,
+ master_status_update, service);
+ }
+ }
+}
+
+void master_status_update(struct master_service *service)
+{
+ bool important_update;
+
+ if ((service->flags & MASTER_SERVICE_FLAG_UPDATE_PROCTITLE) != 0 &&
+ service->set != NULL && service->set->verbose_proctitle) T_BEGIN {
+ unsigned int used_count = service->total_available_count -
+ service->master_status.available_count;
+
+ process_title_set(t_strdup_printf("[%u connections]",
+ used_count));
+ } T_END;
+
+ important_update = master_status_update_is_important(service);
+ if (service->master_status.pid == 0 ||
+ service->master_status.available_count ==
+ service->last_sent_status_avail_count) {
+ /* a) closed, b) updating to same state */
+ timeout_remove(&service->to_status);
+ io_remove(&service->io_status_write);
+ return;
+ }
+ if (ioloop_time == service->last_sent_status_time &&
+ !important_update) {
+ /* don't spam master */
+ if (service->to_status != NULL)
+ timeout_reset(service->to_status);
+ else {
+ service->to_status =
+ timeout_add(1000, master_status_update,
+ service);
+ }
+ if (service->io_status_write != NULL)
+ io_remove(&service->io_status_write);
+ return;
+ }
+ master_status_send(service, important_update);
+}
+
+bool version_string_verify(const char *line, const char *service_name,
+ unsigned major_version)
+{
+ unsigned int minor_version;
+
+ return version_string_verify_full(line, service_name,
+ major_version, &minor_version);
+}
+
+bool version_string_verify_full(const char *line, const char *service_name,
+ unsigned major_version,
+ unsigned int *minor_version_r)
+{
+ size_t service_name_len = strlen(service_name);
+ bool ret;
+
+ if (!str_begins(line, "VERSION\t"))
+ return FALSE;
+ line += 8;
+
+ if (strncmp(line, service_name, service_name_len) != 0 ||
+ line[service_name_len] != '\t')
+ return FALSE;
+ line += service_name_len + 1;
+
+ T_BEGIN {
+ const char *p = strchr(line, '\t');
+
+ if (p == NULL)
+ ret = FALSE;
+ else {
+ ret = str_uint_equals(t_strdup_until(line, p),
+ major_version);
+ if (str_to_uint(p+1, minor_version_r) < 0)
+ ret = FALSE;
+ }
+ } T_END;
+ return ret;
+}
+
+void master_service_set_process_shutdown_filter(struct master_service *service,
+ struct event_filter *filter)
+{
+ master_service_unset_process_shutdown_filter(service);
+ service->process_shutdown_filter = filter;
+ event_filter_ref(service->process_shutdown_filter);
+}
+
+void master_service_unset_process_shutdown_filter(struct master_service *service)
+{
+ event_filter_unref(&service->process_shutdown_filter);
+}
diff --git a/src/lib-master/master-service.h b/src/lib-master/master-service.h
new file mode 100644
index 0000000..bd32ad0
--- /dev/null
+++ b/src/lib-master/master-service.h
@@ -0,0 +1,262 @@
+#ifndef MASTER_SERVICE_H
+#define MASTER_SERVICE_H
+
+#include "net.h"
+
+#include <unistd.h> /* for getopt() opt* variables */
+#include <stdio.h> /* for getopt() opt* variables in Solaris */
+
+enum master_service_flags {
+ /* stdin/stdout already contains a client which we want to serve */
+ MASTER_SERVICE_FLAG_STD_CLIENT = 0x01,
+ /* this process is currently running standalone without a master */
+ MASTER_SERVICE_FLAG_STANDALONE = 0x02,
+ /* Log to configured log file instead of stderr. By default when
+ _FLAG_STANDALONE is set, logging is done to stderr. */
+ MASTER_SERVICE_FLAG_DONT_LOG_TO_STDERR = 0x04,
+ /* Service is going to do multiple configuration lookups,
+ keep the connection to config service open. Also opens the config
+ socket before dropping privileges. */
+ MASTER_SERVICE_FLAG_KEEP_CONFIG_OPEN = 0x08,
+ /* Don't read settings, but use whatever is in environment */
+ MASTER_SERVICE_FLAG_NO_CONFIG_SETTINGS = 0x10,
+ /* Use MASTER_LOGIN_NOTIFY_FD to track login overflow state */
+ MASTER_SERVICE_FLAG_TRACK_LOGIN_STATE = 0x40,
+ /* If master sends SIGINT, don't die even if we don't have clients */
+ MASTER_SERVICE_FLAG_NO_IDLE_DIE = 0x80,
+ /* Show number of connections in process title
+ (only if verbose_proctitle setting is enabled) */
+ MASTER_SERVICE_FLAG_UPDATE_PROCTITLE = 0x100,
+ /* Don't read any SSL settings. This is mainly needed to prevent master
+ process from trying to pass through huge list of SSL CA certificates
+ through environment for ssl_ca setting, which could fail. Although
+ the same problem can still happen with standalone doveadm if it
+ reads settings via doveconf instead of config socket. */
+ MASTER_SERVICE_FLAG_DISABLE_SSL_SET = 0x200,
+ /* Don't initialize SSL context automatically. */
+ MASTER_SERVICE_FLAG_NO_SSL_INIT = 0x400,
+ /* Don't create a data stack frame between master_service_init() and
+ master_service_init_finish(). By default this is done to make sure
+ initialization doesn't unnecessarily use up memory in data stack. */
+ MASTER_SERVICE_FLAG_NO_INIT_DATASTACK_FRAME = 0x800,
+ /* Don't connect at startup to the stats process. */
+ MASTER_SERVICE_FLAG_DONT_SEND_STATS = 0x1000,
+ /* Service supports STARTTLS-like feature. SSL server must be
+ initialized even if there are no ssl=yes listeners. */
+ MASTER_SERVICE_FLAG_HAVE_STARTTLS = 0x2000,
+};
+
+struct master_service_connection_proxy {
+ /* only set if ssl is TRUE */
+ const char *hostname;
+ const char *cert_common_name;
+ const unsigned char *alpn;
+ unsigned int alpn_size;
+
+ bool ssl:1;
+ bool ssl_client_cert:1;
+};
+
+struct master_service_connection {
+ /* fd of the new connection. */
+ int fd;
+ /* fd of the socket listener. Same as fd for a FIFO. */
+ int listen_fd;
+ /* listener name as in configuration file, or "" if unnamed. */
+ const char *name;
+
+ /* Original client/server IP/port. Both of these may have been changed
+ by the haproxy protocol. */
+ struct ip_addr remote_ip, local_ip;
+ in_port_t remote_port, local_port;
+
+ /* The real client/server IP/port, unchanged by haproxy protocol. */
+ struct ip_addr real_remote_ip, real_local_ip;
+ in_port_t real_remote_port, real_local_port;
+
+ /* filled if connection is proxied */
+ struct master_service_connection_proxy proxy;
+
+ /* This is a connection proxied wit HAproxy (or similar) */
+ bool proxied:1;
+
+ /* This is a FIFO fd. Only a single "connection" is ever received from
+ a FIFO after the first writer sends something to it. */
+ bool fifo:1;
+ /* Perform immediate SSL handshake for this connection. Currently this
+ needs to be performed explicitly by each service. */
+ bool ssl:1;
+
+ /* Internal: master_service_client_connection_accept() has been
+ called for this connection. */
+ bool accepted:1;
+};
+
+typedef void
+master_service_connection_callback_t(struct master_service_connection *conn);
+
+/* If kill==TRUE, the callback should kill one of the existing connections
+ (likely the oldest). If kill==FALSE, it's just a request to check what is
+ the creation timestamp for the connection to be killed. Returns TRUE if
+ a connection was/could be killed, FALSE if not. */
+typedef bool
+master_service_avail_overflow_callback_t(bool kill, struct timeval *created_r);
+
+extern struct master_service *master_service;
+
+const char *master_service_getopt_string(void);
+
+/* Start service initialization. */
+struct master_service *
+master_service_init(const char *name, enum master_service_flags flags,
+ int *argc, char **argv[], const char *getopt_str);
+/* Call getopt() and handle internal parameters. Return values are the same as
+ getopt()'s. */
+int master_getopt(struct master_service *service);
+/* Returns TRUE if str is a valid getopt_str. Currently this only checks for
+ duplicate args so they aren't accidentally added. */
+bool master_getopt_str_is_valid(const char *str);
+/* Parser command line option. Returns TRUE if processed. */
+bool master_service_parse_option(struct master_service *service,
+ int opt, const char *arg);
+/* Finish service initialization. The caller should drop privileges
+ before calling this. This also notifies the master that the service was
+ successfully started and there shouldn't be any service throttling even if
+ it crashes afterwards, so this should be called after all of the
+ initialization code is finished. */
+void master_service_init_finish(struct master_service *service);
+
+/* import_environment is a space-separated list of environment keys or
+ key=values. The key=values are immediately added to the environment.
+ All the keys are added to DOVECOT_PRESERVE_ENVS environment so they're
+ preserved by master_service_env_clean(). */
+void master_service_import_environment(const char *import_environment);
+/* Clean environment from everything except the ones listed in
+ DOVECOT_PRESERVE_ENVS environment. */
+void master_service_env_clean(void);
+
+/* Initialize logging. Only the first call changes the actual logging
+ functions. The following calls change the log prefix. */
+void master_service_init_log(struct master_service *service);
+/* Initialize/change log prefix to the given log prefix. */
+void master_service_init_log_with_prefix(struct master_service *service,
+ const char *prefix);
+/* Initialize/change log prefix to "configured_name(my_pid): " */
+void master_service_init_log_with_pid(struct master_service *service);
+/* Initialize stats client (if it's not already initialized). This is called
+ automatically if MASTER_SERVICE_FLAG_SEND_STATS is enabled. If
+ silent_notfound_errors is set, connect() errors aren't logged if they're
+ happening because the stats service isn't running. */
+void master_service_init_stats_client(struct master_service *service,
+ bool silent_notfound_errors);
+
+/* If set, die immediately when connection to master is lost.
+ Normally all existing clients are handled first. */
+void master_service_set_die_with_master(struct master_service *service,
+ bool set);
+/* Call the given when master connection dies and die_with_master is TRUE.
+ The callback is expected to shut down the service somewhat soon or it's
+ done forcibly. If NULL, the service is stopped immediately. */
+void master_service_set_die_callback(struct master_service *service,
+ void (*callback)(void));
+/* "idle callback" is called when master thinks we're idling and asks us to
+ die. We'll do it only if the idle callback returns TRUE. This callback isn't
+ even called if the master service code knows that we're handling clients. */
+void master_service_set_idle_die_callback(struct master_service *service,
+ bool (*callback)(void));
+/* Call the given callback when there are no available connections and master
+ has indicated that it can't create any more processes to handle requests. */
+void master_service_set_avail_overflow_callback(struct master_service *service,
+ master_service_avail_overflow_callback_t *callback);
+
+/* Set maximum number of clients we can handle. Default is given by master. */
+void master_service_set_client_limit(struct master_service *service,
+ unsigned int client_limit);
+/* Returns the maximum number of clients we can handle. */
+unsigned int master_service_get_client_limit(struct master_service *service);
+/* Returns how many processes of this type can be created before reaching the
+ limit. */
+unsigned int master_service_get_process_limit(struct master_service *service);
+/* Returns service { process_min_avail } */
+unsigned int master_service_get_process_min_avail(struct master_service *service);
+/* Returns the service's idle_kill timeout in seconds. Normally master handles
+ sending the kill request when the process has no clients, but some services
+ with permanent client connections may need to handle this themselves. */
+unsigned int master_service_get_idle_kill_secs(struct master_service *service);
+
+/* Set maximum number of client connections we will handle before shutting
+ down. */
+void master_service_set_service_count(struct master_service *service,
+ unsigned int count);
+/* Returns the number of client connections we will handle before shutting
+ down. The value is decreased only after connection has been closed. */
+unsigned int master_service_get_service_count(struct master_service *service);
+/* Return the number of listener sockets. */
+unsigned int master_service_get_socket_count(struct master_service *service);
+/* Returns the name of the listener socket, or "" if none is specified. */
+const char *master_service_get_socket_name(struct master_service *service,
+ int listen_fd);
+
+/* Returns configuration file path. */
+const char *master_service_get_config_path(struct master_service *service);
+/* Returns PACKAGE_VERSION or NULL if version_ignore=yes. This function is
+ useful mostly as parameter to module_dir_load(). */
+const char *master_service_get_version_string(struct master_service *service);
+/* Returns name of the service, as given in name parameter to _init(). */
+const char *master_service_get_name(struct master_service *service);
+/* Returns name of the service, as given in the configuration file. For example
+ service name=auth, but configured_name=auth-worker. This is preferred in
+ e.g. log prefixes. */
+const char *master_service_get_configured_name(struct master_service *service);
+
+/* Start the service. Blocks until finished */
+void master_service_run(struct master_service *service,
+ master_service_connection_callback_t *callback)
+ ATTR_NULL(2);
+/* Stop a running service. */
+void master_service_stop(struct master_service *service);
+/* Stop once we're done serving existing new connections, but don't accept
+ any new ones. */
+void master_service_stop_new_connections(struct master_service *service);
+/* Returns TRUE if we've received a SIGINT/SIGTERM and we've decided to stop. */
+bool master_service_is_killed(struct master_service *service);
+/* Returns TRUE if our master process is already stopped. This process may or
+ may not be dying itself. Returns FALSE always if the process was started
+ standalone. */
+bool master_service_is_master_stopped(struct master_service *service);
+
+/* Send command to anvil process, if we have fd to it. */
+void master_service_anvil_send(struct master_service *service, const char *cmd);
+/* Call to accept the client connection. Otherwise the connection is closed. */
+void master_service_client_connection_accept(struct master_service_connection *conn);
+/* Used to create "extra client connections" outside the common accept()
+ method. */
+void master_service_client_connection_created(struct master_service *service);
+/* Call whenever a client connection is destroyed. */
+void master_service_client_connection_destroyed(struct master_service *service);
+
+/* Deinitialize the service. */
+void master_service_deinit(struct master_service **service);
+/* Deinitialize the service for a forked child process. Currently, the only
+ difference with master_service_deinit() is that lib_deinit() and
+ lib_signals_deinit() are not called.
+ */
+void master_service_deinit_forked(struct master_service **_service);
+
+/* Returns TRUE if line contains compatible service name and major version.
+ The line is expected to be in format:
+ VERSION <tab> service_name <tab> major version <tab> minor version */
+bool version_string_verify(const char *line, const char *service_name,
+ unsigned major_version);
+/* Same as version_string_verify(), but return the minor version. */
+bool version_string_verify_full(const char *line, const char *service_name,
+ unsigned major_version,
+ unsigned int *minor_version_r);
+
+/* Sets process shutdown filter */
+void master_service_set_process_shutdown_filter(struct master_service *service,
+ struct event_filter *filter);
+/* Unsets process shutdown filter, if it exists */
+void master_service_unset_process_shutdown_filter(struct master_service *service);
+
+#endif
diff --git a/src/lib-master/service-settings.h b/src/lib-master/service-settings.h
new file mode 100644
index 0000000..c023585
--- /dev/null
+++ b/src/lib-master/service-settings.h
@@ -0,0 +1,82 @@
+#ifndef SERVICE_SETTINGS_H
+#define SERVICE_SETTINGS_H
+
+#include "net.h"
+
+/* <settings checks> */
+enum service_user_default {
+ SERVICE_USER_DEFAULT_NONE = 0,
+ SERVICE_USER_DEFAULT_INTERNAL,
+ SERVICE_USER_DEFAULT_LOGIN
+};
+
+enum service_type {
+ SERVICE_TYPE_UNKNOWN,
+ SERVICE_TYPE_LOG,
+ SERVICE_TYPE_ANVIL,
+ SERVICE_TYPE_CONFIG,
+ SERVICE_TYPE_LOGIN,
+ SERVICE_TYPE_STARTUP,
+ /* Worker processes are intentionally limited to their process_limit,
+ and they can regularly reach it. There shouldn't be unnecessary
+ warnings about temporarily reaching the limit. */
+ SERVICE_TYPE_WORKER,
+};
+/* </settings checks> */
+
+struct file_listener_settings {
+ const char *path;
+ unsigned int mode;
+ const char *user;
+ const char *group;
+};
+ARRAY_DEFINE_TYPE(file_listener_settings, struct file_listener_settings *);
+
+struct inet_listener_settings {
+ const char *name;
+ const char *address;
+ in_port_t port;
+ bool ssl;
+ bool reuse_port;
+ bool haproxy;
+};
+ARRAY_DEFINE_TYPE(inet_listener_settings, struct inet_listener_settings *);
+
+struct service_settings {
+ const char *name;
+ const char *protocol;
+ const char *type;
+ const char *executable;
+ const char *user;
+ const char *group;
+ const char *privileged_group;
+ const char *extra_groups;
+ const char *chroot;
+
+ bool drop_priv_before_exec;
+
+ unsigned int process_min_avail;
+ unsigned int process_limit;
+ unsigned int client_limit;
+ unsigned int service_count;
+ unsigned int idle_kill;
+ uoff_t vsz_limit;
+
+ ARRAY_TYPE(file_listener_settings) unix_listeners;
+ ARRAY_TYPE(file_listener_settings) fifo_listeners;
+ ARRAY_TYPE(inet_listener_settings) inet_listeners;
+
+ /* internal to master: */
+ struct master_settings *master_set;
+ enum service_type parsed_type;
+ enum service_user_default user_default;
+ bool login_dump_core:1;
+
+ /* -- flags that can be set internally -- */
+
+ /* process_limit must not be higher than 1 */
+ bool process_limit_1:1;
+};
+ARRAY_DEFINE_TYPE(service_settings, struct service_settings *);
+
+#endif
diff --git a/src/lib-master/stats-client.c b/src/lib-master/stats-client.c
new file mode 100644
index 0000000..e6b24c7
--- /dev/null
+++ b/src/lib-master/stats-client.c
@@ -0,0 +1,373 @@
+/* Copyright (c) 2017-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "str.h"
+#include "strescape.h"
+#include "ostream.h"
+#include "time-util.h"
+#include "lib-event-private.h"
+#include "event-filter.h"
+#include "connection.h"
+#include "stats-client.h"
+
+#define STATS_CLIENT_TIMEOUT_MSECS (5*1000)
+#define STATS_CLIENT_RECONNECT_INTERVAL_MSECS (10*1000)
+
+struct stats_client {
+ struct connection conn;
+ struct event_filter *filter;
+ struct ioloop *ioloop;
+ struct timeout *to_reconnect;
+ bool handshaked;
+ bool handshake_received_at_least_once;
+ bool silent_notfound_errors;
+};
+
+static struct connection_list *stats_clients;
+
+static void stats_client_connect(struct stats_client *client);
+
+static int
+client_handshake_filter(const char *const *args, struct event_filter **filter_r,
+ const char **error_r)
+{
+ if (strcmp(args[0], "FILTER") != 0) {
+ *error_r = "Expected FILTER";
+ return -1;
+ }
+ if (args[1] == NULL || args[1][0] == '\0') {
+ *filter_r = NULL;
+ return 0;
+ }
+
+ *filter_r = event_filter_create();
+ if (!event_filter_import(*filter_r, t_str_tabunescape(args[1]), error_r)) {
+ event_filter_unref(filter_r);
+ return -1;
+ }
+ return 0;
+}
+
+static int
+stats_client_handshake(struct stats_client *client, const char *const *args)
+{
+ struct event_filter *filter;
+ const char *error;
+
+ if (client_handshake_filter(args, &filter, &error) < 0) {
+ i_error("stats: Received invalid handshake: %s (input: %s)",
+ error, t_strarray_join(args, "\t"));
+ return -1;
+ }
+ client->handshaked = TRUE;
+ client->handshake_received_at_least_once = TRUE;
+ if (client->ioloop != NULL)
+ io_loop_stop(client->ioloop);
+
+ if (filter == NULL)
+ filter = event_filter_create();
+
+ event_filter_unref(&client->filter);
+ client->filter = filter;
+ event_set_global_debug_send_filter(client->filter);
+ return 1;
+}
+
+static int
+stats_client_input_args(struct connection *conn, const char *const *args)
+{
+ struct stats_client *client = (struct stats_client *)conn;
+
+ return stats_client_handshake(client, args);
+
+}
+
+static void stats_client_reconnect(struct stats_client *client)
+{
+ timeout_remove(&client->to_reconnect);
+ stats_client_connect(client);
+}
+
+static void stats_client_destroy(struct connection *conn)
+{
+ struct stats_client *client = (struct stats_client *)conn;
+ struct event *event;
+ unsigned int reconnect_msecs = STATS_CLIENT_RECONNECT_INTERVAL_MSECS;
+
+ /* after reconnection the IDs need to be re-sent */
+ for (event = events_get_head(); event != NULL; event = event->next)
+ event->sent_to_stats_id = 0;
+
+ client->handshaked = FALSE;
+ connection_disconnect(conn);
+ if (client->ioloop != NULL) {
+ /* waiting for stats handshake to finish */
+ io_loop_stop(client->ioloop);
+ } else if (conn->connect_finished.tv_sec != 0) {
+ int msecs_since_connected =
+ timeval_diff_msecs(&ioloop_timeval,
+ &conn->connect_finished);
+ if (msecs_since_connected >= STATS_CLIENT_RECONNECT_INTERVAL_MSECS) {
+ /* reconnect immdiately */
+ reconnect_msecs = 0;
+ } else {
+ /* wait for reconnect interval since we last
+ were connected. */
+ reconnect_msecs = STATS_CLIENT_RECONNECT_INTERVAL_MSECS -
+ msecs_since_connected;
+ }
+ }
+ if (client->to_reconnect == NULL) {
+ client->to_reconnect =
+ timeout_add(reconnect_msecs,
+ stats_client_reconnect, client);
+ }
+}
+
+static const struct connection_settings stats_client_set = {
+ .service_name_in = "stats-server",
+ .service_name_out = "stats-client",
+ .major_version = 4,
+ .minor_version = 0,
+
+ .input_max_size = SIZE_MAX,
+ .output_max_size = SIZE_MAX,
+ .client = TRUE
+};
+
+static const struct connection_vfuncs stats_client_vfuncs = {
+ .destroy = stats_client_destroy,
+ .input_args = stats_client_input_args,
+};
+
+static void
+stats_event_write(struct stats_client *client,
+ struct event *event, struct event *global_event,
+ const struct failure_context *ctx, string_t *str, bool begin)
+{
+ struct event *merged_event;
+ struct event *parent_event;
+ bool update = FALSE, flush_output = FALSE;
+
+ merged_event = begin ? event_ref(event) : event_minimize(event);
+ parent_event = merged_event->parent;
+
+ if (parent_event != NULL) {
+ if (parent_event->sent_to_stats_id !=
+ parent_event->change_id) {
+ stats_event_write(client, parent_event, NULL,
+ ctx, str, TRUE);
+ }
+ i_assert(parent_event->sent_to_stats_id != 0);
+ }
+ if (begin) {
+ i_assert(event == merged_event);
+ update = (event->sent_to_stats_id != 0);
+ const char *cmd = !update ? "BEGIN" : "UPDATE";
+ str_printfa(str, "%s\t%"PRIu64"\t", cmd, event->id);
+ event->sent_to_stats_id = event->change_id;
+ /* Flush the BEGINs early on, because the stats event writing
+ may trigger more events recursively (e.g. data_stack_grow),
+ which may use the BEGIN events as parents. */
+ flush_output = !update;
+ } else {
+ str_printfa(str, "EVENT\t%"PRIu64"\t",
+ global_event == NULL ? 0 : global_event->id);
+ }
+ str_printfa(str, "%"PRIu64"\t",
+ parent_event == NULL ? 0 : parent_event->id);
+ if (!update)
+ str_printfa(str, "%u\t", ctx->type);
+ event_export(merged_event, str);
+ str_append_c(str, '\n');
+ event_unref(&merged_event);
+ if (flush_output) {
+ o_stream_nsend(client->conn.output, str_data(str), str_len(str));
+ str_truncate(str, 0);
+ }
+}
+
+static void
+stats_client_send_event(struct stats_client *client, struct event *event,
+ const struct failure_context *ctx)
+{
+ static int recursion = 0;
+
+ if (!client->handshaked)
+ return;
+
+ if (!event_filter_match(client->filter, event, ctx))
+ return;
+
+ /* Need to send the event for stats and/or export */
+ string_t *str = t_str_new(256);
+
+ if (++recursion == 0)
+ o_stream_cork(client->conn.output);
+ struct event *global_event = event_get_global();
+ if (global_event != NULL)
+ stats_event_write(client, global_event, NULL, ctx, str, TRUE);
+
+ stats_event_write(client, event, global_event, ctx, str, FALSE);
+ o_stream_nsend(client->conn.output, str_data(str), str_len(str));
+
+ i_assert(recursion > 0);
+ if (--recursion == 0)
+ o_stream_uncork(client->conn.output);
+}
+
+static void
+stats_client_free_event(struct stats_client *client, struct event *event)
+{
+ if (event->sent_to_stats_id == 0)
+ return;
+ o_stream_nsend_str(client->conn.output,
+ t_strdup_printf("END\t%"PRIu64"\n", event->id));
+}
+
+static bool
+stats_event_callback(struct event *event, enum event_callback_type type,
+ struct failure_context *ctx,
+ const char *fmt ATTR_UNUSED, va_list args ATTR_UNUSED)
+{
+ if (stats_clients->connections == NULL)
+ return TRUE;
+ struct stats_client *client =
+ (struct stats_client *)stats_clients->connections;
+ if (client->conn.output == NULL)
+ return TRUE;
+
+ switch (type) {
+ case EVENT_CALLBACK_TYPE_CREATE:
+ break;
+ case EVENT_CALLBACK_TYPE_SEND:
+ stats_client_send_event(client, event, ctx);
+ break;
+ case EVENT_CALLBACK_TYPE_FREE:
+ stats_client_free_event(client, event);
+ break;
+ }
+ return TRUE;
+}
+
+static void
+stats_category_append(string_t *str, const struct event_category *category)
+{
+ str_append(str, "CATEGORY\t");
+ str_append_tabescaped(str, category->name);
+ if (category->parent != NULL) {
+ str_append_c(str, '\t');
+ str_append_tabescaped(str, category->parent->name);
+ }
+ str_append_c(str, '\n');
+}
+
+static void stats_category_registered(struct event_category *category)
+{
+ if (stats_clients->connections == NULL)
+ return;
+ struct stats_client *client =
+ (struct stats_client *)stats_clients->connections;
+ if (client->conn.output == NULL)
+ return;
+
+ string_t *str = t_str_new(64);
+ stats_category_append(str, category);
+ o_stream_nsend(client->conn.output, str_data(str), str_len(str));
+}
+
+static void stats_global_init(void)
+{
+ stats_clients = connection_list_init(&stats_client_set,
+ &stats_client_vfuncs);
+ event_register_callback(stats_event_callback);
+ event_category_register_callback(stats_category_registered);
+}
+
+static void stats_global_deinit(void)
+{
+ event_unregister_callback(stats_event_callback);
+ event_category_unregister_callback(stats_category_registered);
+ connection_list_deinit(&stats_clients);
+}
+
+static void stats_client_timeout(struct stats_client *client)
+{
+ e_error(client->conn.event, "Timeout waiting for handshake response");
+ io_loop_stop(client->ioloop);
+}
+
+static void stats_client_wait_handshake(struct stats_client *client)
+{
+ struct ioloop *prev_ioloop = current_ioloop;
+ struct timeout *to;
+
+ i_assert(client->to_reconnect == NULL);
+
+ client->ioloop = io_loop_create();
+ to = timeout_add(STATS_CLIENT_TIMEOUT_MSECS, stats_client_timeout, client);
+ connection_switch_ioloop(&client->conn);
+ io_loop_run(client->ioloop);
+ io_loop_set_current(prev_ioloop);
+ connection_switch_ioloop(&client->conn);
+ if (client->to_reconnect != NULL)
+ client->to_reconnect = io_loop_move_timeout(&client->to_reconnect);
+ io_loop_set_current(client->ioloop);
+ timeout_remove(&to);
+ io_loop_destroy(&client->ioloop);
+}
+
+static void stats_client_send_registered_categories(struct stats_client *client)
+{
+ struct event_category *const *categories;
+ unsigned int i, count;
+
+ string_t *str = t_str_new(64);
+ categories = event_get_registered_categories(&count);
+ for (i = 0; i < count; i++)
+ stats_category_append(str, categories[i]);
+ o_stream_nsend(client->conn.output, str_data(str), str_len(str));
+}
+
+static void stats_client_connect(struct stats_client *client)
+{
+ if (connection_client_connect(&client->conn) == 0) {
+ /* read the handshake so the global debug filter is updated */
+ stats_client_send_registered_categories(client);
+ if (!client->handshake_received_at_least_once)
+ stats_client_wait_handshake(client);
+ } else if (!client->silent_notfound_errors ||
+ (errno != ENOENT && errno != ECONNREFUSED)) {
+ i_error("net_connect_unix(%s) failed: %m", client->conn.name);
+ }
+}
+
+struct stats_client *
+stats_client_init(const char *path, bool silent_notfound_errors)
+{
+ struct stats_client *client;
+
+ if (stats_clients == NULL)
+ stats_global_init();
+
+ client = i_new(struct stats_client, 1);
+ client->silent_notfound_errors = silent_notfound_errors;
+ connection_init_client_unix(stats_clients, &client->conn, path);
+ stats_client_connect(client);
+ return client;
+}
+
+void stats_client_deinit(struct stats_client **_client)
+{
+ struct stats_client *client = *_client;
+
+ *_client = NULL;
+
+ event_filter_unref(&client->filter);
+ connection_deinit(&client->conn);
+ timeout_remove(&client->to_reconnect);
+ i_free(client);
+
+ if (stats_clients->connections == NULL)
+ stats_global_deinit();
+}
diff --git a/src/lib-master/stats-client.h b/src/lib-master/stats-client.h
new file mode 100644
index 0000000..c20bf5d
--- /dev/null
+++ b/src/lib-master/stats-client.h
@@ -0,0 +1,8 @@
+#ifndef STATS_CLIENT_H
+#define STATS_CLIENT_H
+
+struct stats_client *
+stats_client_init(const char *path, bool silent_notfound_errors);
+void stats_client_deinit(struct stats_client **client);
+
+#endif
diff --git a/src/lib-master/syslog-util.c b/src/lib-master/syslog-util.c
new file mode 100644
index 0000000..58827a8
--- /dev/null
+++ b/src/lib-master/syslog-util.c
@@ -0,0 +1,65 @@
+/* Copyright (c) 2005-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "syslog-util.h"
+#include <syslog.h>
+
+struct syslog_facility_list syslog_facilities[] = {
+#ifdef LOG_AUTH
+ { "auth", LOG_AUTH },
+#endif
+#ifdef LOG_AUTHPRIV
+ { "authpriv", LOG_AUTHPRIV },
+#endif
+#ifdef LOG_CRON
+ { "cron", LOG_CRON },
+#endif
+#ifdef LOG_DAEMON
+ { "daemon", LOG_DAEMON },
+#endif
+#ifdef LOG_FTP
+ { "ftp", LOG_FTP },
+#endif
+#ifdef LOG_KERN
+ { "kern", LOG_KERN },
+#endif
+#ifdef LOG_LPR
+ { "lpr", LOG_LPR },
+#endif
+#ifdef LOG_MAIL
+ { "mail", LOG_MAIL },
+#endif
+#ifdef LOG_NEWS
+ { "news", LOG_NEWS },
+#endif
+#ifdef LOG_SYSLOG
+ { "syslog", LOG_SYSLOG },
+#endif
+#ifdef LOG_UUCP
+ { "uucp", LOG_UUCP },
+#endif
+ { "user", LOG_USER },
+ { "local0", LOG_LOCAL0 },
+ { "local1", LOG_LOCAL1 },
+ { "local2", LOG_LOCAL2 },
+ { "local3", LOG_LOCAL3 },
+ { "local4", LOG_LOCAL4 },
+ { "local5", LOG_LOCAL5 },
+ { "local6", LOG_LOCAL6 },
+ { "local7", LOG_LOCAL7 },
+
+ { NULL, 0 }
+};
+
+bool syslog_facility_find(const char *name, int *facility_r)
+{
+ int i;
+
+ for (i = 0; syslog_facilities[i].name != NULL; i++) {
+ if (strcmp(syslog_facilities[i].name, name) == 0) {
+ *facility_r = syslog_facilities[i].facility;
+ return TRUE;
+ }
+ }
+ return FALSE;
+}
diff --git a/src/lib-master/syslog-util.h b/src/lib-master/syslog-util.h
new file mode 100644
index 0000000..68e1250
--- /dev/null
+++ b/src/lib-master/syslog-util.h
@@ -0,0 +1,14 @@
+#ifndef SYSLOG_UTIL_H
+#define SYSLOG_UTIL_H
+
+struct syslog_facility_list {
+ const char *name;
+ int facility;
+};
+
+extern struct syslog_facility_list syslog_facilities[];
+
+/* Returns TRUE if found. */
+bool syslog_facility_find(const char *name, int *facility_r);
+
+#endif
diff --git a/src/lib-master/test-event-stats.c b/src/lib-master/test-event-stats.c
new file mode 100644
index 0000000..565118b
--- /dev/null
+++ b/src/lib-master/test-event-stats.c
@@ -0,0 +1,784 @@
+/* Copyright (c) 2018 Dovecot authors, see the included COPYING file */
+
+#include "test-lib.h"
+#include "lib.h"
+#include "time-util.h"
+#include "lib-event-private.h"
+#include "str.h"
+#include "sleep.h"
+#include "ioloop.h"
+#include "connection.h"
+#include "ostream.h"
+#include "istream.h"
+#include "stats-client.h"
+#include "test-common.h"
+#include <fcntl.h>
+#include <unistd.h>
+#include <signal.h>
+#include <sys/socket.h>
+#include <sys/un.h>
+#include <sys/wait.h>
+
+#define TST_BEGIN(test_name) \
+ test_begin(test_name); \
+ ioloop_timeval.tv_sec = 0; \
+ ioloop_timeval.tv_usec = 0;
+
+#define BASE_DIR "."
+#define SOCK_PATH ".test-temp-stats-event-sock"
+
+#define SOCK_FULL BASE_DIR "/" SOCK_PATH
+
+static struct event_category test_cats[5] = {
+ {.name = "test1"},
+ {.name = "test2"},
+ {.name = "test3"},
+ {.name = "test4"},
+ {.name = "test5"},
+};
+
+static struct event_field test_fields[5] = {
+ {.key = "key1",
+ .value_type = EVENT_FIELD_VALUE_TYPE_STR,
+ .value = {.str = "str1"}},
+
+ {.key = "key2",
+ .value_type = EVENT_FIELD_VALUE_TYPE_INTMAX,
+ .value = {.intmax = 20}},
+
+ {.key = "key3",
+ .value_type = EVENT_FIELD_VALUE_TYPE_TIMEVAL,
+ .value = {.timeval = {.tv_sec = 10}}},
+
+ {.key = "key4",
+ .value = {.str = "str4"}},
+
+ {.key = "key5",
+ .value = {.intmax = 50}},
+};
+
+static void stats_conn_accept(void *context ATTR_UNUSED);
+static void stats_conn_destroy(struct connection *_conn);
+static void stats_conn_input(struct connection *_conn);
+
+static bool compare_test_stats_to(const char *format, ...) ATTR_FORMAT(1, 2);
+
+static struct connection_settings stats_conn_set = {
+ .input_max_size = SIZE_MAX,
+ .output_max_size = SIZE_MAX,
+ .client = FALSE
+};
+
+static const struct connection_vfuncs stats_conn_vfuncs = {
+ .destroy = stats_conn_destroy,
+ .input = stats_conn_input
+};
+
+struct server_connection {
+ struct connection conn;
+
+ pool_t pool;
+ bool handshake_sent:1;
+};
+
+static int stats_sock_fd;
+static struct connection_list *stats_conn_list;
+static struct ioloop *ioloop;
+
+static pid_t stats_pid;
+
+static int run_tests(void);
+static void signal_process(const char *signal_file);
+static void wait_for_signal(const char *signal_file);
+static void kill_stats_child(void);
+
+static const char *stats_ready = ".test-temp-stats-event-stats-ready";
+static const char *test_done = ".test-temp-stats-event-test-done";
+static const char *exit_stats = ".test-temp-stats-event-exit-stats";
+static const char *stats_data_file = ".test-temp-stats-event-test_stats";
+
+static void kill_stats_child(void)
+{
+ i_assert(stats_pid != 0);
+ (void)kill(stats_pid, SIGKILL);
+ (void)waitpid(stats_pid, NULL, 0);
+}
+
+static void stats_proc(void)
+{
+ struct io *io_listen;
+ /* Make sure socket file not existing */
+ i_unlink_if_exists(SOCK_FULL);
+ stats_sock_fd = net_listen_unix(SOCK_FULL, 128);
+ if (stats_sock_fd == -1)
+ i_fatal("listen(%s) failed: %m", SOCK_FULL);
+ ioloop = io_loop_create();
+ io_listen = io_add(stats_sock_fd, IO_READ, stats_conn_accept, NULL);
+ stats_conn_list = connection_list_init(&stats_conn_set,
+ &stats_conn_vfuncs);
+ signal_process(stats_ready);
+ io_loop_run(ioloop);
+ io_remove(&io_listen);
+ connection_list_deinit(&stats_conn_list);
+ io_loop_destroy(&ioloop);
+ i_close_fd(&stats_sock_fd);
+ i_unlink(SOCK_FULL);
+}
+
+static void stats_conn_accept(void *context ATTR_UNUSED)
+{
+ int fd;
+ struct server_connection *conn;
+ pool_t pool;
+ fd = net_accept(stats_sock_fd, NULL, NULL);
+ if (stats_sock_fd == -1)
+ return;
+ if (stats_sock_fd == -2)
+ i_fatal("test stats: accept() failed: %m");
+ net_set_nonblock(fd, TRUE);
+ pool = pool_alloconly_create("stats connection", 512);
+ conn = p_new(pool, struct server_connection, 1);
+ conn->pool = pool;
+ connection_init_server(stats_conn_list,
+ &conn->conn,
+ "stats connection", fd, fd);
+}
+
+static void stats_conn_destroy(struct connection *_conn)
+{
+ struct server_connection *conn =
+ (struct server_connection *)_conn;
+ connection_deinit(&conn->conn);
+ pool_unref(&conn->pool);
+}
+
+static void stats_conn_input(struct connection *_conn)
+{
+ int fd;
+ struct ostream *stats_data_out;
+ struct server_connection *conn = (struct server_connection *)_conn;
+ const char *handshake = "VERSION\tstats-server\t4\t0\n"
+ "FILTER\tcategory=test1 OR category=test2 OR category=test3 OR "
+ "category=test4 OR category=test5\n";
+ const char *line = NULL;
+ if (!conn->handshake_sent) {
+ conn->handshake_sent = TRUE;
+ o_stream_nsend_str(conn->conn.output, handshake);
+ }
+ while (access(exit_stats, F_OK) < 0) {
+ /* Test process haven't signal yet about end of the tests */
+ while (access(test_done, F_OK) < 0 ||
+ ((line=i_stream_read_next_line(conn->conn.input)) != NULL)) {
+ if (line != NULL) {
+ if (str_begins(line, "VERSION"))
+ continue;
+
+ if ((fd=open(stats_data_file, O_WRONLY | O_CREAT | O_APPEND, 0600)) < 0) {
+ i_fatal("failed create stats data file %m");
+ }
+
+ stats_data_out = o_stream_create_fd_autoclose(&fd, SIZE_MAX);
+ o_stream_nsend_str(stats_data_out, line);
+ o_stream_nsend_str(stats_data_out, "\n");
+
+ o_stream_set_no_error_handling(stats_data_out, TRUE);
+ o_stream_unref(&stats_data_out);
+ }
+ i_sleep_msecs(100);
+ }
+ i_unlink(test_done);
+ signal_process(stats_ready);
+ }
+ i_unlink(exit_stats);
+ i_unlink_if_exists(test_done);
+ io_loop_stop(ioloop);
+}
+
+static void wait_for_signal(const char *signal_file)
+{
+ struct timeval start, now;
+ i_gettimeofday(&start);
+ while (access(signal_file, F_OK) < 0) {
+ i_sleep_msecs(10);
+ i_gettimeofday(&now);
+ if (timeval_diff_usecs(&now, &start) > 10000000) {
+ kill_stats_child();
+ i_fatal("wait_for_signal has timed out");
+ }
+ }
+ i_unlink(signal_file);
+}
+
+static void signal_process(const char *signal_file)
+{
+ int fd;
+ if ((fd = open(signal_file, O_CREAT, 0666)) < 0) {
+ if (stats_pid != 0) {
+ kill_stats_child();
+ }
+ i_fatal("Failed to create signal file %s", signal_file);
+ }
+ i_close_fd(&fd);
+}
+
+static bool compare_test_stats_data_line(const char *reference, const char *actual)
+{
+ const char *const *ref_args = t_strsplit(reference, "\t");
+ const char *const *act_args = t_strsplit(actual, "\t");
+ unsigned int max = str_array_length(ref_args);
+
+ /* different lengths imply not equal */
+ if (str_array_length(ref_args) != str_array_length(act_args))
+ return FALSE;
+
+ for (size_t i = 0; i < max; i++) {
+ if (i > 1 && i < 6) continue;
+ if (*(ref_args[i]) == 'l') {
+ i++;
+ continue;
+ }
+ if (strcmp(ref_args[i], act_args[i]) != 0) {
+ return FALSE;
+ }
+ }
+ return TRUE;
+}
+
+static bool compare_test_stats_data_lines(const char *actual, const char *reference)
+{
+ const char *const *lines_ref = t_strsplit(reference, "\n");
+ const char *const *lines_act = t_strsplit(actual, "\n");
+ for(size_t i = 0; *lines_ref != NULL && *lines_act != NULL; i++, lines_ref++, lines_act++) {
+ if (!compare_test_stats_data_line(*lines_ref, *lines_act))
+ return FALSE;
+ }
+ return *lines_ref == *lines_act;
+}
+
+static bool compare_test_stats_to(const char *format, ...)
+{
+ bool res;
+ string_t *reference = t_str_new(1024);
+ struct istream *input;
+ va_list args;
+ va_start (args, format);
+ str_vprintfa (reference, format, args);
+ va_end (args);
+ /* signal stats process to receive and record stats data */
+ signal_process(test_done);
+ /* Wait stats data to be recorded by stats process */
+ wait_for_signal(stats_ready);
+
+ input = i_stream_create_file(stats_data_file, SIZE_MAX);
+ while (i_stream_read(input) > 0) ;
+ if (input->stream_errno != 0) {
+ i_fatal("stats data file read failed: %s",
+ i_stream_get_error(input));
+ res = FALSE;
+ } else {
+ size_t size;
+ const unsigned char *data = i_stream_get_data(input, &size);
+ res = compare_test_stats_data_lines(t_strdup_until(data, data+size), str_c(reference));
+ }
+ i_stream_unref(&input);
+ i_unlink(stats_data_file);
+ return res;
+}
+
+static void test_fail_callback(const struct failure_context *ctx ATTR_UNUSED,
+ const char *format ATTR_UNUSED,
+ va_list args ATTR_UNUSED)
+{
+ /* ignore message, all we need is stats */
+}
+
+static void register_all_categories(void)
+{
+ /* Run this before all the tests,
+ so stats client doesn't send CATEGORY\ttestx anymore,
+ so test will produce stats records independent of test order */
+ struct event *ev;
+ int i;
+ for (i = 0; i < 5; i++) {
+ ev = event_create(NULL);
+ event_add_category(ev, &test_cats[i]);
+ e_info(ev, "message");
+ event_unref(&ev);
+ }
+ signal_process(test_done);
+}
+
+static void test_no_merging1(void)
+{
+ /* NULL parent */
+ int l;
+ TST_BEGIN("no merging parent is NULL");
+ struct event *single_ev = event_create(NULL);
+ event_add_category(single_ev, &test_cats[0]);
+ event_add_str(single_ev, test_fields[0].key, test_fields[0].value.str);
+ e_info(single_ev, "info message");
+ l = __LINE__ - 1;
+ event_unref(&single_ev);
+ test_assert(
+ compare_test_stats_to(
+ "EVENT 0 0 1 0 0"
+ " s"__FILE__" %d"
+ " l0 0 ctest1 Skey1 str1\n", l));
+ test_end();
+}
+
+static void test_no_merging2(void)
+{
+ /* Parent sent to stats */
+ int l;
+ uint64_t id;
+ TST_BEGIN("no merging parent sent to stats");
+ struct event *parent_ev = event_create(NULL);
+ event_add_category(parent_ev, &test_cats[0]);
+ parent_ev->sent_to_stats_id = parent_ev->change_id;
+ id = parent_ev->id;
+ struct event *child_ev = event_create(parent_ev);
+ event_add_category(child_ev, &test_cats[1]);
+ e_info(child_ev, "info message");
+ l = __LINE__ - 1;
+ event_unref(&parent_ev);
+ event_unref(&child_ev);
+ test_assert(
+ compare_test_stats_to(
+ "EVENT 0 %"PRIu64" 1 0 0"
+ " s"__FILE__" %d"
+ " l0 0 ctest2\n"
+ "END 9\n", id, l));
+ test_end();
+}
+
+static void test_no_merging3(void)
+{
+ /* Parent have different timestamp */
+ int l, lp;
+ uint64_t idp;
+ TST_BEGIN("no merging parent timestamp differs");
+ struct event *parent_ev = event_create(NULL);
+ lp = __LINE__ - 1;
+ idp = parent_ev->id;
+ event_add_category(parent_ev, &test_cats[0]);
+ parent_ev->sent_to_stats_id = 0;
+ ioloop_timeval.tv_sec++;
+ struct event *child_ev = event_create(parent_ev);
+ event_add_category(child_ev, &test_cats[1]);
+ e_info(child_ev, "info message");
+ l = __LINE__ - 1;
+ event_unref(&parent_ev);
+ event_unref(&child_ev);
+ test_assert(
+ compare_test_stats_to(
+ "BEGIN %"PRIu64" 0 1 0 0"
+ " s"__FILE__" %d ctest1\n"
+ "EVENT 0 %"PRIu64" 1 1 0"
+ " s"__FILE__" %d"
+ " l1 0 ctest2\n"
+ "END\t%"PRIu64"\n", idp, lp, idp, l, idp));
+ test_end();
+}
+
+static void test_merge_events1(void)
+{
+ int l;
+ TST_BEGIN("merge events parent NULL");
+ struct event *merge_ev1 = event_create(NULL);
+ event_add_category(merge_ev1, &test_cats[0]);
+ event_add_category(merge_ev1, &test_cats[1]);
+ event_add_str(merge_ev1,test_fields[0].key, test_fields[0].value.str);
+ event_add_int(merge_ev1,test_fields[1].key, test_fields[1].value.intmax);
+ struct event *merge_ev2 = event_create(merge_ev1);
+ event_add_category(merge_ev2, &test_cats[2]);
+ event_add_category(merge_ev2, &test_cats[1]);
+ event_add_timeval(merge_ev2,test_fields[2].key,
+ &test_fields[2].value.timeval);
+ event_add_int(merge_ev2,test_fields[1].key, test_fields[1].value.intmax);
+ e_info(merge_ev2, "info message");
+ l = __LINE__ - 1;
+ event_unref(&merge_ev1);
+ event_unref(&merge_ev2);
+ test_assert(
+ compare_test_stats_to(
+ "EVENT 0 0 1 0 0"
+ " s"__FILE__" %d l0 0"
+ " ctest3 ctest2 ctest1 Tkey3"
+ " 10 0 Ikey2 20"
+ " Skey1 str1\n", l));
+ test_end();
+}
+
+static void test_merge_events2(void)
+{
+ int l;
+ uint64_t id;
+ TST_BEGIN("merge events parent sent to stats");
+ struct event *parent_ev = event_create(NULL);
+ event_add_category(parent_ev, &test_cats[3]);
+ parent_ev->sent_to_stats_id = parent_ev->change_id;
+ struct event *merge_ev1 = event_create(parent_ev);
+ event_add_category(merge_ev1, &test_cats[0]);
+ event_add_category(merge_ev1, &test_cats[1]);
+ event_add_str(merge_ev1,test_fields[0].key, test_fields[0].value.str);
+ event_add_int(merge_ev1,test_fields[1].key, test_fields[1].value.intmax);
+ struct event *merge_ev2 = event_create(merge_ev1);
+ event_add_category(merge_ev2, &test_cats[2]);
+ event_add_category(merge_ev2, &test_cats[1]);
+ event_add_timeval(merge_ev2,test_fields[2].key,
+ &test_fields[2].value.timeval);
+ event_add_int(merge_ev2,test_fields[1].key, test_fields[1].value.intmax);
+ e_info(merge_ev2, "info message");
+ l = __LINE__ - 1;
+ id = parent_ev->id;
+ event_unref(&parent_ev);
+ event_unref(&merge_ev1);
+ event_unref(&merge_ev2);
+ test_assert(
+ compare_test_stats_to(
+ "EVENT 0 %"PRIu64" 1 0 0"
+ " s"__FILE__" %d l0 0"
+ " ctest3 ctest2 ctest1 Tkey3"
+ " 10 0 Ikey2 20"
+ " Skey1 str1\n"
+ "END 16\n", id, l));
+ test_end();
+}
+
+static void test_skip_parents(void)
+{
+ int l, lp;
+ uint64_t id;
+ TST_BEGIN("skip empty parents");
+ struct event *parent_to_log = event_create(NULL);
+ lp = __LINE__ - 1;
+ id = parent_to_log->id;
+ event_add_category(parent_to_log, &test_cats[0]);
+ ioloop_timeval.tv_sec++;
+ struct event *empty_parent1 = event_create(parent_to_log);
+ ioloop_timeval.tv_sec++;
+ struct event *empty_parent2 = event_create(empty_parent1);
+ ioloop_timeval.tv_sec++;
+ struct event *child_ev = event_create(empty_parent2);
+ event_add_category(child_ev, &test_cats[1]);
+ e_info(child_ev, "info message");
+ l = __LINE__ - 1;
+ event_unref(&parent_to_log);
+ event_unref(&empty_parent1);
+ event_unref(&empty_parent2);
+ event_unref(&child_ev);
+ test_assert(
+ compare_test_stats_to(
+ "BEGIN %"PRIu64" 0 1 0 0"
+ " s"__FILE__" %d ctest1\n"
+ "EVENT 0 %"PRIu64" 1 3 0 "
+ "s"__FILE__" %d l3 0"
+ " ctest2\nEND\t%"PRIu64"\n", id, lp, id, l, id));
+ test_end();
+}
+
+static void test_merge_events_skip_parents(void)
+{
+ int lp, l;
+ uint64_t id;
+ TST_BEGIN("merge events and skip empty parents");
+ struct event *parent_to_log = event_create(NULL);
+ lp = __LINE__ - 1;
+ id = parent_to_log->id;
+ event_add_category(parent_to_log, &test_cats[0]);
+ ioloop_timeval.tv_sec++;
+ struct event *empty_parent1 = event_create(parent_to_log);
+ ioloop_timeval.tv_sec++;
+ struct event *empty_parent2 = event_create(empty_parent1);
+ ioloop_timeval.tv_sec++;
+ struct event *child1_ev = event_create(empty_parent2);
+ event_add_category(child1_ev, &test_cats[1]);
+ event_add_category(child1_ev, &test_cats[2]);
+ event_add_int(child1_ev,test_fields[1].key, test_fields[1].value.intmax);
+ event_add_str(child1_ev,test_fields[0].key, test_fields[0].value.str);
+ struct event *child2_ev = event_create(empty_parent2);
+ event_add_category(child2_ev, &test_cats[3]);
+ event_add_category(child2_ev, &test_cats[4]);
+ event_add_timeval(child2_ev,test_fields[2].key,
+ &test_fields[2].value.timeval);
+ event_add_str(child2_ev,test_fields[3].key, test_fields[3].value.str);
+ e_info(child2_ev, "info message");
+ l = __LINE__ - 1;
+ event_unref(&parent_to_log);
+ event_unref(&empty_parent1);
+ event_unref(&empty_parent2);
+ event_unref(&child1_ev);
+ event_unref(&child2_ev);
+ test_assert(
+ compare_test_stats_to(
+ "BEGIN %"PRIu64" 0 1 0 0"
+ " s"__FILE__" %d ctest1\n"
+ "EVENT 0 %"PRIu64" 1 3 0 "
+ "s"__FILE__" %d l3 0 "
+ "ctest4 ctest5 Tkey3 10 0 Skey4"
+ " str4\nEND\t%"PRIu64"\n", id, lp, id, l, id));
+ test_end();
+}
+
+static struct event *make_event(struct event *parent,
+ struct event_category *cat,
+ int *line_r, uint64_t *id_r)
+{
+ struct event *event;
+ int line;
+
+ event = event_create(parent);
+ line = __LINE__ -1;
+
+ if (line_r != NULL)
+ *line_r = line;
+ if (id_r != NULL)
+ *id_r = event->id;
+
+ /* something in the test infrastructure assumes that at least one
+ category is always present - make it happy */
+ event_add_category(event, cat);
+
+ /* advance the clock to avoid event sending optimizations */
+ ioloop_timeval.tv_sec++;
+
+ return event;
+}
+
+static void test_parent_update_post_send(void)
+{
+ struct event *a, *b, *c;
+ uint64_t id;
+ int line, line_log1, line_log2;
+
+ TST_BEGIN("parent updated after send");
+
+ a = make_event(NULL, &test_cats[0], &line, &id);
+ b = make_event(a, &test_cats[1], NULL, NULL);
+ c = make_event(b, &test_cats[2], NULL, NULL);
+
+ /* set initial field values */
+ event_add_int(a, "a", 1);
+ event_add_int(b, "b", 2);
+ event_add_int(c, "c", 3);
+
+ /* force 'a' event to be sent */
+ e_info(b, "field 'a' should be 1");
+ line_log1 = __LINE__ - 1;
+
+ event_add_int(a, "a", 1000); /* update parent */
+
+ /* log child, which should re-sent parent */
+ e_info(c, "field 'a' should be 1000");
+ line_log2 = __LINE__ - 1;
+
+ event_unref(&a);
+ event_unref(&b);
+ event_unref(&c);
+
+ /* EVENT <parent> <type> ... */
+ /* BEGIN <id> <parent> <type> ... */
+ /* END <id> */
+ test_assert(
+ compare_test_stats_to(
+ /* first e_info() */
+ "BEGIN %"PRIu64" 0 1 0 0"
+ " s"__FILE__" %d ctest1"
+ " Ia 1\n"
+ "EVENT 0 %"PRIu64" 1 1 0"
+ " s"__FILE__" %d"
+ " l1 0 ctest2" " Ib 2\n"
+ /* second e_info() */
+ "UPDATE %"PRIu64" 0 0 0"
+ " s"__FILE__" %d ctest1"
+ " Ia 1000\n"
+ "BEGIN %"PRIu64" %"PRIu64" 1 0 0"
+ " s"__FILE__" %d"
+ " l0 0 ctest2 Ib 2\n"
+ "EVENT 0 %"PRIu64" 1 1 0"
+ " s"__FILE__" %d"
+ " l1 0 ctest3"
+ " Ic 3\n"
+ "END\t%"PRIu64"\n"
+ "END\t%"PRIu64"\n",
+ id, line, /* BEGIN */
+ id, line_log1, /* EVENT */
+ id, line, /* UPDATE */
+ id + 1, id, line, /* BEGIN */
+ id + 1, line_log2, /* EVENT */
+ id + 1 /* END */,
+ id /* END */));
+
+ test_end();
+}
+
+static void test_large_event_id(void)
+{
+ TST_BEGIN("large event id");
+ int line, line_log1, line_log2, line_log3;
+ struct event *a, *b;
+ uint64_t id;
+
+ a = make_event(NULL, &test_cats[0], &line, &id);
+ a->id += 1000000;
+ id = a->id;
+ a->change_id++;
+ b = make_event(a, &test_cats[1], NULL, NULL);
+
+ ioloop_timeval.tv_sec++;
+ e_info(a, "emit");
+ line_log1 = __LINE__-1;
+ ioloop_timeval.tv_sec++;
+ e_info(b, "emit");
+ line_log2 = __LINE__-1;
+ event_add_int(a, "test1", 1);
+ e_info(b, "emit");
+ line_log3 = __LINE__-1;
+
+ event_unref(&b);
+ event_unref(&a);
+
+ test_assert(
+ compare_test_stats_to(
+ /* first e_info() */
+ "EVENT 0 %"PRIu64" 1 1 0"
+ " s"__FILE__" %d"
+ " l1 0 ctest1\n"
+ "BEGIN %"PRIu64" 0 1 0 0"
+ " s"__FILE__" %d"
+ " l0 0 ctest1\n"
+ "EVENT 0 %"PRIu64" 1 1 0"
+ " s"__FILE__" %d"
+ " l1 0 ctest2\n"
+ "UPDATE %"PRIu64" 0 1 0"
+ " s"__FILE__" %d"
+ " l1 0 ctest1 Itest1 1\n"
+ "EVENT 0 %"PRIu64" 1 1 0"
+ " s"__FILE__" %d"
+ " l1 0 ctest2\n"
+ "END %"PRIu64"\n",
+ (uint64_t)0, line_log1,
+ id, line,
+ id, line_log2,
+ id, line,
+ id, line_log3,
+ id
+ )
+ );
+
+ test_end();
+}
+
+static void test_global_event(void)
+{
+ TST_BEGIN("merge events global");
+ struct event *merge_ev1 = event_create(NULL);
+ event_add_category(merge_ev1, &test_cats[0]);
+ event_add_str(merge_ev1,test_fields[0].key, test_fields[0].value.str);
+ struct event *merge_ev2 = event_create(merge_ev1);
+ event_add_int(merge_ev2,test_fields[1].key, test_fields[1].value.intmax);
+
+ struct event *global_event = event_create(NULL);
+ int global_event_line = __LINE__ - 1;
+ uint64_t global_event_id = global_event->id;
+ event_add_str(global_event, "global", "value");
+ event_push_global(global_event);
+
+ struct timeval tv;
+ event_get_create_time(merge_ev1, &tv);
+
+ e_info(merge_ev2, "info message");
+ int log_line = __LINE__ - 1;
+
+ event_pop_global(global_event);
+ event_unref(&merge_ev1);
+ event_unref(&merge_ev2);
+ event_unref(&global_event);
+
+ test_assert(
+ compare_test_stats_to(
+ "BEGIN\t%"PRIu64"\t0\t1\t0\t0"
+ "\ts"__FILE__"\t%d"
+ "\tSglobal\tvalue\n"
+ "EVENT\t%"PRIu64"\t0\t1\t0\t0"
+ "\ts"__FILE__"\t%d\tl0\t0"
+ "\tctest1\tIkey2\t20\tSkey1\tstr1\n"
+ "END\t%"PRIu64"\n",
+ global_event_id, global_event_line,
+ global_event_id, log_line,
+ global_event_id));
+ test_end();
+}
+
+static int run_tests(void)
+{
+ int ret;
+ void (*const tests[])(void) = {
+ test_no_merging1,
+ test_no_merging2,
+ test_no_merging3,
+ test_merge_events1,
+ test_merge_events2,
+ test_skip_parents,
+ test_merge_events_skip_parents,
+ test_parent_update_post_send,
+ test_large_event_id,
+ test_global_event,
+ NULL
+ };
+ struct ioloop *ioloop = io_loop_create();
+ struct stats_client *stats_client = stats_client_init(SOCK_FULL, FALSE);
+ register_all_categories();
+ wait_for_signal(stats_ready);
+ /* Remove stats data file containing register categories related stuff */
+ i_unlink(stats_data_file);
+ ret = test_run(tests);
+ stats_client_deinit(&stats_client);
+ signal_process(exit_stats);
+ signal_process(test_done);
+ (void)waitpid(stats_pid, NULL, 0);
+ io_loop_destroy(&ioloop);
+ return ret;
+}
+
+static void cleanup_test_stats(void)
+{
+ i_unlink_if_exists(SOCK_FULL);
+ i_unlink_if_exists(stats_data_file);
+ i_unlink_if_exists(test_done);
+ i_unlink_if_exists(exit_stats);
+ i_unlink_if_exists(stats_ready);
+}
+
+static int launch_test_stats(void)
+{
+ int ret;
+
+ /* Make sure files are not existing */
+ cleanup_test_stats();
+
+ if ((stats_pid = fork()) == (pid_t)-1)
+ i_fatal("fork() failed: %m");
+ if (stats_pid == 0) {
+ stats_proc();
+ return 0;
+ }
+ wait_for_signal(stats_ready);
+ ret = run_tests();
+
+ /* Make sure we don't leave anything behind */
+ cleanup_test_stats();
+
+ return ret;
+}
+
+int main(void)
+{
+ int ret;
+ i_set_info_handler(test_fail_callback);
+ lib_init();
+ ret = launch_test_stats();
+ lib_deinit();
+ return ret;
+}
diff --git a/src/lib-master/test-master-service-settings-cache.c b/src/lib-master/test-master-service-settings-cache.c
new file mode 100644
index 0000000..1765839
--- /dev/null
+++ b/src/lib-master/test-master-service-settings-cache.c
@@ -0,0 +1,125 @@
+/* Copyright (c) 2014-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "test-common.h"
+#include "settings-parser.h"
+#include "master-service-private.h"
+#include "master-service-settings.h"
+#include "master-service-settings-cache.h"
+
+
+struct master_service *master_service;
+
+static struct master_service test_master_service;
+static struct master_service_settings set;
+static struct master_service_settings_input input;
+static struct master_service_settings_output output;
+static struct master_service_settings_cache *cache;
+
+struct test_service_settings {
+ const char *foo;
+};
+
+#undef DEF
+#define DEF(type, name) \
+ SETTING_DEFINE_STRUCT_##type(#name, name, struct test_service_settings)
+
+static const struct setting_define test_setting_defines[] = {
+ DEF(STR, foo),
+ SETTING_DEFINE_LIST_END
+};
+
+static const struct test_service_settings test_default_settings = {
+ .foo = ""
+};
+
+static const struct setting_parser_info test_setting_parser_info = {
+ .module_name = "module",
+ .defines = test_setting_defines,
+ .defaults = &test_default_settings,
+
+ .type_offset = SIZE_MAX,
+ .struct_size = sizeof(struct test_service_settings),
+
+ .parent_offset = SIZE_MAX
+};
+
+int master_service_settings_read(struct master_service *service ATTR_UNUSED,
+ const struct master_service_settings_input *input ATTR_UNUSED,
+ struct master_service_settings_output *output_r,
+ const char **error_r ATTR_UNUSED)
+{
+ *output_r = output;
+ return 0;
+}
+
+int master_service_settings_get_filters(struct master_service *service ATTR_UNUSED,
+ const char *const **filters ATTR_UNUSED,
+ const char **error_r ATTR_UNUSED)
+{
+ return -1;
+}
+
+
+const struct master_service_settings *
+master_service_settings_get(struct master_service *service ATTR_UNUSED)
+{
+ return &set;
+}
+
+static void test_master_service_settings_cache_once(void)
+{
+ const struct setting_parser_context *parser;
+ const char *error;
+
+ output.used_local = output.service_uses_local && i_rand_limit(2) != 0;
+ if (output.used_local) {
+ input.local_ip.family = AF_INET;
+ input.local_ip.u.ip4.s_addr = i_rand_minmax(100, 199);
+ }
+ output.used_remote = output.service_uses_remote && i_rand_limit(2) != 0;
+ if (output.used_remote) {
+ input.remote_ip.family = AF_INET;
+ input.remote_ip.u.ip4.s_addr = i_rand_minmax(100, 199);
+ }
+ test_assert(master_service_settings_cache_read(cache, &input, NULL, &parser, &error) == 0);
+}
+
+static void test_master_service_settings_cache(void)
+{
+ int i, j;
+
+ for (i = 1; i < 4; i++) {
+ cache = master_service_settings_cache_init(master_service,
+ "module", "service_name");
+ output.service_uses_local = (i & 1) != 0;
+ output.service_uses_remote = (i & 2) != 0;
+ for (j = 0; j < 1000; j++)
+ test_master_service_settings_cache_once();
+ master_service_settings_cache_deinit(&cache);
+ }
+}
+
+int main(void)
+{
+ static void (*const test_functions[])(void) = {
+ test_master_service_settings_cache,
+ NULL
+ };
+ pool_t pool;
+ int ret;
+
+ i_zero(&input);
+ input.module = "module";
+ input.service = "service_name";
+
+ set.config_cache_size = 1024*4;
+ pool = pool_alloconly_create("set pool", 1024);
+ test_master_service.set_parser =
+ settings_parser_init(pool, &test_setting_parser_info, 0);
+ master_service = &test_master_service;
+ ret = test_run(test_functions);
+ settings_parser_deinit(&test_master_service.set_parser);
+ pool_unref(&pool);
+ return ret;
+}
diff --git a/src/lib-oauth2/Makefile.am b/src/lib-oauth2/Makefile.am
new file mode 100644
index 0000000..458b612
--- /dev/null
+++ b/src/lib-oauth2/Makefile.am
@@ -0,0 +1,71 @@
+AM_CPPFLAGS = \
+ -I$(top_srcdir)/src/lib \
+ -I$(top_srcdir)/src/lib-test \
+ -I$(top_srcdir)/src/lib-http \
+ -I$(top_srcdir)/src/lib-dcrypt \
+ -I$(top_srcdir)/src/lib-dict \
+ -I$(top_srcdir)/src/lib-settings
+
+noinst_LTLIBRARIES=liboauth2.la
+
+pkginc_libdir=$(pkgincludedir)
+pkginc_lib_HEADERS = \
+ oauth2.h
+
+noinst_HEADERS = \
+ oauth2-private.h
+
+liboauth2_la_SOURCES = \
+ oauth2.c \
+ oauth2-request.c \
+ oauth2-jwt.c \
+ oauth2-key-cache.c
+
+test_programs = \
+ test-oauth2-json \
+ test-oauth2-jwt
+
+noinst_PROGRAMS = $(test_programs)
+
+test_libs = \
+ $(noinst_LTLIBRARIES) \
+ ../lib-dcrypt/libdcrypt.la \
+ ../lib-http/libhttp.la \
+ ../lib-dns/libdns.la \
+ ../lib-ssl-iostream/libssl_iostream.la \
+ ../lib-master/libmaster.la \
+ ../lib-auth/libauth.la \
+ ../lib-dict/libdict.la \
+ ../lib-settings/libsettings.la \
+ ../lib-test/libtest.la \
+ ../lib/liblib.la \
+ $(MODULE_LIBS)
+test_deps = \
+ $(noinst_LTLIBRARIES) \
+ ../lib-dcrypt/libdcrypt.la \
+ ../lib-http/libhttp.la \
+ ../lib-dns/libdns.la \
+ ../lib-ssl-iostream/libssl_iostream.la \
+ ../lib-master/libmaster.la \
+ ../lib-auth/libauth.la \
+ ../lib-dict/libdict.la \
+ ../lib-settings/libsettings.la \
+ ../lib-test/libtest.la \
+ ../lib/liblib.la
+
+test_oauth2_json_SOURCES = test-oauth2-json.c
+test_oauth2_json_LDADD = $(test_libs)
+test_oauth2_json_DEPENDENCIES = $(test_deps)
+
+test_oauth2_jwt_SOURCES = test-oauth2-jwt.c
+test_oauth2_jwt_LDADD = $(test_libs)
+if HAVE_WHOLE_ARCHIVE
+test_oauth2_jwt_LDFLAGS = -Wl,$(LD_WHOLE_ARCHIVE),../lib-ssl-iostream/.libs/libssl_iostream.a,$(LD_NO_WHOLE_ARCHIVE)
+endif
+test_oauth2_jwt_DEPENDENCIES = $(test_deps)
+
+check-local:
+ for bin in $(test_programs); do \
+ if ! $(RUN_TEST) ./$$bin; then exit 1; fi; \
+ done
+
diff --git a/src/lib-oauth2/Makefile.in b/src/lib-oauth2/Makefile.in
new file mode 100644
index 0000000..fabc559
--- /dev/null
+++ b/src/lib-oauth2/Makefile.in
@@ -0,0 +1,919 @@
+# Makefile.in generated by automake 1.16.1 from Makefile.am.
+# @configure_input@
+
+# Copyright (C) 1994-2018 Free Software Foundation, Inc.
+
+# This Makefile.in is free software; the Free Software Foundation
+# gives unlimited permission to copy and/or distribute it,
+# with or without modifications, as long as this notice is preserved.
+
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY, to the extent permitted by law; without
+# even the implied warranty of MERCHANTABILITY or FITNESS FOR A
+# PARTICULAR PURPOSE.
+
+@SET_MAKE@
+
+
+
+VPATH = @srcdir@
+am__is_gnu_make = { \
+ if test -z '$(MAKELEVEL)'; then \
+ false; \
+ elif test -n '$(MAKE_HOST)'; then \
+ true; \
+ elif test -n '$(MAKE_VERSION)' && test -n '$(CURDIR)'; then \
+ true; \
+ else \
+ false; \
+ fi; \
+}
+am__make_running_with_option = \
+ case $${target_option-} in \
+ ?) ;; \
+ *) echo "am__make_running_with_option: internal error: invalid" \
+ "target option '$${target_option-}' specified" >&2; \
+ exit 1;; \
+ esac; \
+ has_opt=no; \
+ sane_makeflags=$$MAKEFLAGS; \
+ if $(am__is_gnu_make); then \
+ sane_makeflags=$$MFLAGS; \
+ else \
+ case $$MAKEFLAGS in \
+ *\\[\ \ ]*) \
+ bs=\\; \
+ sane_makeflags=`printf '%s\n' "$$MAKEFLAGS" \
+ | sed "s/$$bs$$bs[$$bs $$bs ]*//g"`;; \
+ esac; \
+ fi; \
+ skip_next=no; \
+ strip_trailopt () \
+ { \
+ flg=`printf '%s\n' "$$flg" | sed "s/$$1.*$$//"`; \
+ }; \
+ for flg in $$sane_makeflags; do \
+ test $$skip_next = yes && { skip_next=no; continue; }; \
+ case $$flg in \
+ *=*|--*) continue;; \
+ -*I) strip_trailopt 'I'; skip_next=yes;; \
+ -*I?*) strip_trailopt 'I';; \
+ -*O) strip_trailopt 'O'; skip_next=yes;; \
+ -*O?*) strip_trailopt 'O';; \
+ -*l) strip_trailopt 'l'; skip_next=yes;; \
+ -*l?*) strip_trailopt 'l';; \
+ -[dEDm]) skip_next=yes;; \
+ -[JT]) skip_next=yes;; \
+ esac; \
+ case $$flg in \
+ *$$target_option*) has_opt=yes; break;; \
+ esac; \
+ done; \
+ test $$has_opt = yes
+am__make_dryrun = (target_option=n; $(am__make_running_with_option))
+am__make_keepgoing = (target_option=k; $(am__make_running_with_option))
+pkgdatadir = $(datadir)/@PACKAGE@
+pkgincludedir = $(includedir)/@PACKAGE@
+pkglibdir = $(libdir)/@PACKAGE@
+pkglibexecdir = $(libexecdir)/@PACKAGE@
+am__cd = CDPATH="$${ZSH_VERSION+.}$(PATH_SEPARATOR)" && cd
+install_sh_DATA = $(install_sh) -c -m 644
+install_sh_PROGRAM = $(install_sh) -c
+install_sh_SCRIPT = $(install_sh) -c
+INSTALL_HEADER = $(INSTALL_DATA)
+transform = $(program_transform_name)
+NORMAL_INSTALL = :
+PRE_INSTALL = :
+POST_INSTALL = :
+NORMAL_UNINSTALL = :
+PRE_UNINSTALL = :
+POST_UNINSTALL = :
+build_triplet = @build@
+host_triplet = @host@
+noinst_PROGRAMS = $(am__EXEEXT_1)
+subdir = src/lib-oauth2
+ACLOCAL_M4 = $(top_srcdir)/aclocal.m4
+am__aclocal_m4_deps = $(top_srcdir)/m4/ac_checktype2.m4 \
+ $(top_srcdir)/m4/ac_typeof.m4 $(top_srcdir)/m4/arc4random.m4 \
+ $(top_srcdir)/m4/blockdev.m4 $(top_srcdir)/m4/c99_vsnprintf.m4 \
+ $(top_srcdir)/m4/clock_gettime.m4 $(top_srcdir)/m4/crypt.m4 \
+ $(top_srcdir)/m4/crypt_xpg6.m4 $(top_srcdir)/m4/dbqlk.m4 \
+ $(top_srcdir)/m4/dirent_dtype.m4 $(top_srcdir)/m4/dovecot.m4 \
+ $(top_srcdir)/m4/fd_passing.m4 $(top_srcdir)/m4/fdatasync.m4 \
+ $(top_srcdir)/m4/flexible_array_member.m4 \
+ $(top_srcdir)/m4/glibc.m4 $(top_srcdir)/m4/gmtime_max.m4 \
+ $(top_srcdir)/m4/gmtime_tm_gmtoff.m4 \
+ $(top_srcdir)/m4/ioloop.m4 $(top_srcdir)/m4/iovec.m4 \
+ $(top_srcdir)/m4/ipv6.m4 $(top_srcdir)/m4/libcap.m4 \
+ $(top_srcdir)/m4/libtool.m4 $(top_srcdir)/m4/libwrap.m4 \
+ $(top_srcdir)/m4/linux_mremap.m4 $(top_srcdir)/m4/ltoptions.m4 \
+ $(top_srcdir)/m4/ltsugar.m4 $(top_srcdir)/m4/ltversion.m4 \
+ $(top_srcdir)/m4/lt~obsolete.m4 $(top_srcdir)/m4/mmap_write.m4 \
+ $(top_srcdir)/m4/mntctl.m4 $(top_srcdir)/m4/modules.m4 \
+ $(top_srcdir)/m4/notify.m4 $(top_srcdir)/m4/nsl.m4 \
+ $(top_srcdir)/m4/off_t_max.m4 $(top_srcdir)/m4/pkg.m4 \
+ $(top_srcdir)/m4/pr_set_dumpable.m4 \
+ $(top_srcdir)/m4/q_quotactl.m4 $(top_srcdir)/m4/quota.m4 \
+ $(top_srcdir)/m4/random.m4 $(top_srcdir)/m4/rlimit.m4 \
+ $(top_srcdir)/m4/sendfile.m4 $(top_srcdir)/m4/size_t_signed.m4 \
+ $(top_srcdir)/m4/sockpeercred.m4 $(top_srcdir)/m4/sql.m4 \
+ $(top_srcdir)/m4/ssl.m4 $(top_srcdir)/m4/st_tim.m4 \
+ $(top_srcdir)/m4/static_array.m4 $(top_srcdir)/m4/test_with.m4 \
+ $(top_srcdir)/m4/time_t.m4 $(top_srcdir)/m4/typeof.m4 \
+ $(top_srcdir)/m4/typeof_dev_t.m4 \
+ $(top_srcdir)/m4/uoff_t_max.m4 $(top_srcdir)/m4/vararg.m4 \
+ $(top_srcdir)/m4/want_apparmor.m4 \
+ $(top_srcdir)/m4/want_bsdauth.m4 \
+ $(top_srcdir)/m4/want_bzlib.m4 \
+ $(top_srcdir)/m4/want_cassandra.m4 \
+ $(top_srcdir)/m4/want_cdb.m4 \
+ $(top_srcdir)/m4/want_checkpassword.m4 \
+ $(top_srcdir)/m4/want_clucene.m4 $(top_srcdir)/m4/want_db.m4 \
+ $(top_srcdir)/m4/want_gssapi.m4 $(top_srcdir)/m4/want_icu.m4 \
+ $(top_srcdir)/m4/want_ldap.m4 $(top_srcdir)/m4/want_lua.m4 \
+ $(top_srcdir)/m4/want_lz4.m4 $(top_srcdir)/m4/want_lzma.m4 \
+ $(top_srcdir)/m4/want_mysql.m4 $(top_srcdir)/m4/want_pam.m4 \
+ $(top_srcdir)/m4/want_passwd.m4 $(top_srcdir)/m4/want_pgsql.m4 \
+ $(top_srcdir)/m4/want_prefetch.m4 \
+ $(top_srcdir)/m4/want_shadow.m4 \
+ $(top_srcdir)/m4/want_sodium.m4 $(top_srcdir)/m4/want_solr.m4 \
+ $(top_srcdir)/m4/want_sqlite.m4 \
+ $(top_srcdir)/m4/want_stemmer.m4 \
+ $(top_srcdir)/m4/want_systemd.m4 \
+ $(top_srcdir)/m4/want_textcat.m4 \
+ $(top_srcdir)/m4/want_unwind.m4 $(top_srcdir)/m4/want_zlib.m4 \
+ $(top_srcdir)/m4/want_zstd.m4 $(top_srcdir)/configure.ac
+am__configure_deps = $(am__aclocal_m4_deps) $(CONFIGURE_DEPENDENCIES) \
+ $(ACLOCAL_M4)
+DIST_COMMON = $(srcdir)/Makefile.am $(noinst_HEADERS) \
+ $(pkginc_lib_HEADERS) $(am__DIST_COMMON)
+mkinstalldirs = $(install_sh) -d
+CONFIG_HEADER = $(top_builddir)/config.h
+CONFIG_CLEAN_FILES =
+CONFIG_CLEAN_VPATH_FILES =
+am__EXEEXT_1 = test-oauth2-json$(EXEEXT) test-oauth2-jwt$(EXEEXT)
+PROGRAMS = $(noinst_PROGRAMS)
+LTLIBRARIES = $(noinst_LTLIBRARIES)
+liboauth2_la_LIBADD =
+am_liboauth2_la_OBJECTS = oauth2.lo oauth2-request.lo oauth2-jwt.lo \
+ oauth2-key-cache.lo
+liboauth2_la_OBJECTS = $(am_liboauth2_la_OBJECTS)
+AM_V_lt = $(am__v_lt_@AM_V@)
+am__v_lt_ = $(am__v_lt_@AM_DEFAULT_V@)
+am__v_lt_0 = --silent
+am__v_lt_1 =
+am_test_oauth2_json_OBJECTS = test-oauth2-json.$(OBJEXT)
+test_oauth2_json_OBJECTS = $(am_test_oauth2_json_OBJECTS)
+am__DEPENDENCIES_1 =
+am__DEPENDENCIES_2 = $(noinst_LTLIBRARIES) ../lib-dcrypt/libdcrypt.la \
+ ../lib-http/libhttp.la ../lib-dns/libdns.la \
+ ../lib-ssl-iostream/libssl_iostream.la \
+ ../lib-master/libmaster.la ../lib-auth/libauth.la \
+ ../lib-dict/libdict.la ../lib-settings/libsettings.la \
+ ../lib-test/libtest.la ../lib/liblib.la $(am__DEPENDENCIES_1)
+am_test_oauth2_jwt_OBJECTS = test-oauth2-jwt.$(OBJEXT)
+test_oauth2_jwt_OBJECTS = $(am_test_oauth2_jwt_OBJECTS)
+test_oauth2_jwt_LINK = $(LIBTOOL) $(AM_V_lt) --tag=CC \
+ $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=link $(CCLD) \
+ $(AM_CFLAGS) $(CFLAGS) $(test_oauth2_jwt_LDFLAGS) $(LDFLAGS) \
+ -o $@
+AM_V_P = $(am__v_P_@AM_V@)
+am__v_P_ = $(am__v_P_@AM_DEFAULT_V@)
+am__v_P_0 = false
+am__v_P_1 = :
+AM_V_GEN = $(am__v_GEN_@AM_V@)
+am__v_GEN_ = $(am__v_GEN_@AM_DEFAULT_V@)
+am__v_GEN_0 = @echo " GEN " $@;
+am__v_GEN_1 =
+AM_V_at = $(am__v_at_@AM_V@)
+am__v_at_ = $(am__v_at_@AM_DEFAULT_V@)
+am__v_at_0 = @
+am__v_at_1 =
+DEFAULT_INCLUDES = -I.@am__isrc@ -I$(top_builddir)
+depcomp = $(SHELL) $(top_srcdir)/depcomp
+am__maybe_remake_depfiles = depfiles
+am__depfiles_remade = ./$(DEPDIR)/oauth2-jwt.Plo \
+ ./$(DEPDIR)/oauth2-key-cache.Plo \
+ ./$(DEPDIR)/oauth2-request.Plo ./$(DEPDIR)/oauth2.Plo \
+ ./$(DEPDIR)/test-oauth2-json.Po ./$(DEPDIR)/test-oauth2-jwt.Po
+am__mv = mv -f
+COMPILE = $(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) \
+ $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS)
+LTCOMPILE = $(LIBTOOL) $(AM_V_lt) --tag=CC $(AM_LIBTOOLFLAGS) \
+ $(LIBTOOLFLAGS) --mode=compile $(CC) $(DEFS) \
+ $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) $(CPPFLAGS) \
+ $(AM_CFLAGS) $(CFLAGS)
+AM_V_CC = $(am__v_CC_@AM_V@)
+am__v_CC_ = $(am__v_CC_@AM_DEFAULT_V@)
+am__v_CC_0 = @echo " CC " $@;
+am__v_CC_1 =
+CCLD = $(CC)
+LINK = $(LIBTOOL) $(AM_V_lt) --tag=CC $(AM_LIBTOOLFLAGS) \
+ $(LIBTOOLFLAGS) --mode=link $(CCLD) $(AM_CFLAGS) $(CFLAGS) \
+ $(AM_LDFLAGS) $(LDFLAGS) -o $@
+AM_V_CCLD = $(am__v_CCLD_@AM_V@)
+am__v_CCLD_ = $(am__v_CCLD_@AM_DEFAULT_V@)
+am__v_CCLD_0 = @echo " CCLD " $@;
+am__v_CCLD_1 =
+SOURCES = $(liboauth2_la_SOURCES) $(test_oauth2_json_SOURCES) \
+ $(test_oauth2_jwt_SOURCES)
+DIST_SOURCES = $(liboauth2_la_SOURCES) $(test_oauth2_json_SOURCES) \
+ $(test_oauth2_jwt_SOURCES)
+am__can_run_installinfo = \
+ case $$AM_UPDATE_INFO_DIR in \
+ n|no|NO) false;; \
+ *) (install-info --version) >/dev/null 2>&1;; \
+ esac
+am__vpath_adj_setup = srcdirstrip=`echo "$(srcdir)" | sed 's|.|.|g'`;
+am__vpath_adj = case $$p in \
+ $(srcdir)/*) f=`echo "$$p" | sed "s|^$$srcdirstrip/||"`;; \
+ *) f=$$p;; \
+ esac;
+am__strip_dir = f=`echo $$p | sed -e 's|^.*/||'`;
+am__install_max = 40
+am__nobase_strip_setup = \
+ srcdirstrip=`echo "$(srcdir)" | sed 's/[].[^$$\\*|]/\\\\&/g'`
+am__nobase_strip = \
+ for p in $$list; do echo "$$p"; done | sed -e "s|$$srcdirstrip/||"
+am__nobase_list = $(am__nobase_strip_setup); \
+ for p in $$list; do echo "$$p $$p"; done | \
+ sed "s| $$srcdirstrip/| |;"' / .*\//!s/ .*/ ./; s,\( .*\)/[^/]*$$,\1,' | \
+ $(AWK) 'BEGIN { files["."] = "" } { files[$$2] = files[$$2] " " $$1; \
+ if (++n[$$2] == $(am__install_max)) \
+ { print $$2, files[$$2]; n[$$2] = 0; files[$$2] = "" } } \
+ END { for (dir in files) print dir, files[dir] }'
+am__base_list = \
+ sed '$$!N;$$!N;$$!N;$$!N;$$!N;$$!N;$$!N;s/\n/ /g' | \
+ sed '$$!N;$$!N;$$!N;$$!N;s/\n/ /g'
+am__uninstall_files_from_dir = { \
+ test -z "$$files" \
+ || { test ! -d "$$dir" && test ! -f "$$dir" && test ! -r "$$dir"; } \
+ || { echo " ( cd '$$dir' && rm -f" $$files ")"; \
+ $(am__cd) "$$dir" && rm -f $$files; }; \
+ }
+am__installdirs = "$(DESTDIR)$(pkginc_libdir)"
+HEADERS = $(noinst_HEADERS) $(pkginc_lib_HEADERS)
+am__tagged_files = $(HEADERS) $(SOURCES) $(TAGS_FILES) $(LISP)
+# Read a list of newline-separated strings from the standard input,
+# and print each of them once, without duplicates. Input order is
+# *not* preserved.
+am__uniquify_input = $(AWK) '\
+ BEGIN { nonempty = 0; } \
+ { items[$$0] = 1; nonempty = 1; } \
+ END { if (nonempty) { for (i in items) print i; }; } \
+'
+# Make sure the list of sources is unique. This is necessary because,
+# e.g., the same source file might be shared among _SOURCES variables
+# for different programs/libraries.
+am__define_uniq_tagged_files = \
+ list='$(am__tagged_files)'; \
+ unique=`for i in $$list; do \
+ if test -f "$$i"; then echo $$i; else echo $(srcdir)/$$i; fi; \
+ done | $(am__uniquify_input)`
+ETAGS = etags
+CTAGS = ctags
+am__DIST_COMMON = $(srcdir)/Makefile.in $(top_srcdir)/depcomp
+DISTFILES = $(DIST_COMMON) $(DIST_SOURCES) $(TEXINFOS) $(EXTRA_DIST)
+ACLOCAL = @ACLOCAL@
+ACLOCAL_AMFLAGS = @ACLOCAL_AMFLAGS@
+AMTAR = @AMTAR@
+AM_DEFAULT_VERBOSITY = @AM_DEFAULT_VERBOSITY@
+APPARMOR_LIBS = @APPARMOR_LIBS@
+AR = @AR@
+AUTH_CFLAGS = @AUTH_CFLAGS@
+AUTH_LIBS = @AUTH_LIBS@
+AUTOCONF = @AUTOCONF@
+AUTOHEADER = @AUTOHEADER@
+AUTOMAKE = @AUTOMAKE@
+AWK = @AWK@
+BINARY_CFLAGS = @BINARY_CFLAGS@
+BINARY_LDFLAGS = @BINARY_LDFLAGS@
+BISON = @BISON@
+CASSANDRA_CFLAGS = @CASSANDRA_CFLAGS@
+CASSANDRA_LIBS = @CASSANDRA_LIBS@
+CC = @CC@
+CCDEPMODE = @CCDEPMODE@
+CDB_LIBS = @CDB_LIBS@
+CFLAGS = @CFLAGS@
+CLUCENE_CFLAGS = @CLUCENE_CFLAGS@
+CLUCENE_LIBS = @CLUCENE_LIBS@
+COMPRESS_LIBS = @COMPRESS_LIBS@
+CPP = @CPP@
+CPPFLAGS = @CPPFLAGS@
+CRYPT_LIBS = @CRYPT_LIBS@
+CXX = @CXX@
+CXXCPP = @CXXCPP@
+CXXDEPMODE = @CXXDEPMODE@
+CXXFLAGS = @CXXFLAGS@
+CYGPATH_W = @CYGPATH_W@
+DEFS = @DEFS@
+DEPDIR = @DEPDIR@
+DICT_LIBS = @DICT_LIBS@
+DLLIB = @DLLIB@
+DLLTOOL = @DLLTOOL@
+DSYMUTIL = @DSYMUTIL@
+DUMPBIN = @DUMPBIN@
+ECHO_C = @ECHO_C@
+ECHO_N = @ECHO_N@
+ECHO_T = @ECHO_T@
+EGREP = @EGREP@
+EXEEXT = @EXEEXT@
+FGREP = @FGREP@
+FLEX = @FLEX@
+FUZZER_CPPFLAGS = @FUZZER_CPPFLAGS@
+FUZZER_LDFLAGS = @FUZZER_LDFLAGS@
+GREP = @GREP@
+INSTALL = @INSTALL@
+INSTALL_DATA = @INSTALL_DATA@
+INSTALL_PROGRAM = @INSTALL_PROGRAM@
+INSTALL_SCRIPT = @INSTALL_SCRIPT@
+INSTALL_STRIP_PROGRAM = @INSTALL_STRIP_PROGRAM@
+KRB5CONFIG = @KRB5CONFIG@
+KRB5_CFLAGS = @KRB5_CFLAGS@
+KRB5_LIBS = @KRB5_LIBS@
+LD = @LD@
+LDAP_LIBS = @LDAP_LIBS@
+LDFLAGS = @LDFLAGS@
+LD_NO_WHOLE_ARCHIVE = @LD_NO_WHOLE_ARCHIVE@
+LD_WHOLE_ARCHIVE = @LD_WHOLE_ARCHIVE@
+LIBCAP = @LIBCAP@
+LIBDOVECOT = @LIBDOVECOT@
+LIBDOVECOT_COMPRESS = @LIBDOVECOT_COMPRESS@
+LIBDOVECOT_DEPS = @LIBDOVECOT_DEPS@
+LIBDOVECOT_DSYNC = @LIBDOVECOT_DSYNC@
+LIBDOVECOT_LA_LIBS = @LIBDOVECOT_LA_LIBS@
+LIBDOVECOT_LDA = @LIBDOVECOT_LDA@
+LIBDOVECOT_LDAP = @LIBDOVECOT_LDAP@
+LIBDOVECOT_LIBFTS = @LIBDOVECOT_LIBFTS@
+LIBDOVECOT_LIBFTS_DEPS = @LIBDOVECOT_LIBFTS_DEPS@
+LIBDOVECOT_LOGIN = @LIBDOVECOT_LOGIN@
+LIBDOVECOT_LUA = @LIBDOVECOT_LUA@
+LIBDOVECOT_LUA_DEPS = @LIBDOVECOT_LUA_DEPS@
+LIBDOVECOT_SQL = @LIBDOVECOT_SQL@
+LIBDOVECOT_STORAGE = @LIBDOVECOT_STORAGE@
+LIBDOVECOT_STORAGE_DEPS = @LIBDOVECOT_STORAGE_DEPS@
+LIBEXTTEXTCAT_CFLAGS = @LIBEXTTEXTCAT_CFLAGS@
+LIBEXTTEXTCAT_LIBS = @LIBEXTTEXTCAT_LIBS@
+LIBICONV = @LIBICONV@
+LIBICU_CFLAGS = @LIBICU_CFLAGS@
+LIBICU_LIBS = @LIBICU_LIBS@
+LIBOBJS = @LIBOBJS@
+LIBS = @LIBS@
+LIBSODIUM_CFLAGS = @LIBSODIUM_CFLAGS@
+LIBSODIUM_LIBS = @LIBSODIUM_LIBS@
+LIBTIRPC_CFLAGS = @LIBTIRPC_CFLAGS@
+LIBTIRPC_LIBS = @LIBTIRPC_LIBS@
+LIBTOOL = @LIBTOOL@
+LIBUNWIND_CFLAGS = @LIBUNWIND_CFLAGS@
+LIBUNWIND_LIBS = @LIBUNWIND_LIBS@
+LIBWRAP_LIBS = @LIBWRAP_LIBS@
+LINKED_STORAGE_LDADD = @LINKED_STORAGE_LDADD@
+LIPO = @LIPO@
+LN_S = @LN_S@
+LTLIBICONV = @LTLIBICONV@
+LTLIBOBJS = @LTLIBOBJS@
+LT_SYS_LIBRARY_PATH = @LT_SYS_LIBRARY_PATH@
+LUA_CFLAGS = @LUA_CFLAGS@
+LUA_LIBS = @LUA_LIBS@
+MAINT = @MAINT@
+MAKEINFO = @MAKEINFO@
+MANIFEST_TOOL = @MANIFEST_TOOL@
+MKDIR_P = @MKDIR_P@
+MODULE_LIBS = @MODULE_LIBS@
+MODULE_SUFFIX = @MODULE_SUFFIX@
+MYSQL_CFLAGS = @MYSQL_CFLAGS@
+MYSQL_CONFIG = @MYSQL_CONFIG@
+MYSQL_LIBS = @MYSQL_LIBS@
+NM = @NM@
+NMEDIT = @NMEDIT@
+NOPLUGIN_LDFLAGS = @NOPLUGIN_LDFLAGS@
+OBJDUMP = @OBJDUMP@
+OBJEXT = @OBJEXT@
+OTOOL = @OTOOL@
+OTOOL64 = @OTOOL64@
+PACKAGE = @PACKAGE@
+PACKAGE_BUGREPORT = @PACKAGE_BUGREPORT@
+PACKAGE_NAME = @PACKAGE_NAME@
+PACKAGE_STRING = @PACKAGE_STRING@
+PACKAGE_TARNAME = @PACKAGE_TARNAME@
+PACKAGE_URL = @PACKAGE_URL@
+PACKAGE_VERSION = @PACKAGE_VERSION@
+PANDOC = @PANDOC@
+PATH_SEPARATOR = @PATH_SEPARATOR@
+PGSQL_CFLAGS = @PGSQL_CFLAGS@
+PGSQL_LIBS = @PGSQL_LIBS@
+PG_CONFIG = @PG_CONFIG@
+PIE_CFLAGS = @PIE_CFLAGS@
+PIE_LDFLAGS = @PIE_LDFLAGS@
+PKG_CONFIG = @PKG_CONFIG@
+PKG_CONFIG_LIBDIR = @PKG_CONFIG_LIBDIR@
+PKG_CONFIG_PATH = @PKG_CONFIG_PATH@
+QUOTA_LIBS = @QUOTA_LIBS@
+RANLIB = @RANLIB@
+RELRO_LDFLAGS = @RELRO_LDFLAGS@
+RPCGEN = @RPCGEN@
+RUN_TEST = @RUN_TEST@
+SED = @SED@
+SETTING_FILES = @SETTING_FILES@
+SET_MAKE = @SET_MAKE@
+SHELL = @SHELL@
+SQLITE_CFLAGS = @SQLITE_CFLAGS@
+SQLITE_LIBS = @SQLITE_LIBS@
+SQL_CFLAGS = @SQL_CFLAGS@
+SQL_LIBS = @SQL_LIBS@
+SSL_CFLAGS = @SSL_CFLAGS@
+SSL_LIBS = @SSL_LIBS@
+STRIP = @STRIP@
+SYSTEMD_CFLAGS = @SYSTEMD_CFLAGS@
+SYSTEMD_LIBS = @SYSTEMD_LIBS@
+VALGRIND = @VALGRIND@
+VERSION = @VERSION@
+ZSTD_CFLAGS = @ZSTD_CFLAGS@
+ZSTD_LIBS = @ZSTD_LIBS@
+abs_builddir = @abs_builddir@
+abs_srcdir = @abs_srcdir@
+abs_top_builddir = @abs_top_builddir@
+abs_top_srcdir = @abs_top_srcdir@
+ac_ct_AR = @ac_ct_AR@
+ac_ct_CC = @ac_ct_CC@
+ac_ct_CXX = @ac_ct_CXX@
+ac_ct_DUMPBIN = @ac_ct_DUMPBIN@
+am__include = @am__include@
+am__leading_dot = @am__leading_dot@
+am__quote = @am__quote@
+am__tar = @am__tar@
+am__untar = @am__untar@
+bindir = @bindir@
+build = @build@
+build_alias = @build_alias@
+build_cpu = @build_cpu@
+build_os = @build_os@
+build_vendor = @build_vendor@
+builddir = @builddir@
+datadir = @datadir@
+datarootdir = @datarootdir@
+dict_drivers = @dict_drivers@
+docdir = @docdir@
+dvidir = @dvidir@
+exec_prefix = @exec_prefix@
+host = @host@
+host_alias = @host_alias@
+host_cpu = @host_cpu@
+host_os = @host_os@
+host_vendor = @host_vendor@
+htmldir = @htmldir@
+includedir = @includedir@
+infodir = @infodir@
+install_sh = @install_sh@
+libdir = @libdir@
+libexecdir = @libexecdir@
+localedir = @localedir@
+localstatedir = @localstatedir@
+mandir = @mandir@
+mkdir_p = @mkdir_p@
+moduledir = @moduledir@
+oldincludedir = @oldincludedir@
+pdfdir = @pdfdir@
+prefix = @prefix@
+program_transform_name = @program_transform_name@
+psdir = @psdir@
+rundir = @rundir@
+runstatedir = @runstatedir@
+sbindir = @sbindir@
+sharedstatedir = @sharedstatedir@
+sql_drivers = @sql_drivers@
+srcdir = @srcdir@
+ssldir = @ssldir@
+statedir = @statedir@
+sysconfdir = @sysconfdir@
+systemdservicetype = @systemdservicetype@
+systemdsystemunitdir = @systemdsystemunitdir@
+target_alias = @target_alias@
+top_build_prefix = @top_build_prefix@
+top_builddir = @top_builddir@
+top_srcdir = @top_srcdir@
+AM_CPPFLAGS = \
+ -I$(top_srcdir)/src/lib \
+ -I$(top_srcdir)/src/lib-test \
+ -I$(top_srcdir)/src/lib-http \
+ -I$(top_srcdir)/src/lib-dcrypt \
+ -I$(top_srcdir)/src/lib-dict \
+ -I$(top_srcdir)/src/lib-settings
+
+noinst_LTLIBRARIES = liboauth2.la
+pkginc_libdir = $(pkgincludedir)
+pkginc_lib_HEADERS = \
+ oauth2.h
+
+noinst_HEADERS = \
+ oauth2-private.h
+
+liboauth2_la_SOURCES = \
+ oauth2.c \
+ oauth2-request.c \
+ oauth2-jwt.c \
+ oauth2-key-cache.c
+
+test_programs = \
+ test-oauth2-json \
+ test-oauth2-jwt
+
+test_libs = \
+ $(noinst_LTLIBRARIES) \
+ ../lib-dcrypt/libdcrypt.la \
+ ../lib-http/libhttp.la \
+ ../lib-dns/libdns.la \
+ ../lib-ssl-iostream/libssl_iostream.la \
+ ../lib-master/libmaster.la \
+ ../lib-auth/libauth.la \
+ ../lib-dict/libdict.la \
+ ../lib-settings/libsettings.la \
+ ../lib-test/libtest.la \
+ ../lib/liblib.la \
+ $(MODULE_LIBS)
+
+test_deps = \
+ $(noinst_LTLIBRARIES) \
+ ../lib-dcrypt/libdcrypt.la \
+ ../lib-http/libhttp.la \
+ ../lib-dns/libdns.la \
+ ../lib-ssl-iostream/libssl_iostream.la \
+ ../lib-master/libmaster.la \
+ ../lib-auth/libauth.la \
+ ../lib-dict/libdict.la \
+ ../lib-settings/libsettings.la \
+ ../lib-test/libtest.la \
+ ../lib/liblib.la
+
+test_oauth2_json_SOURCES = test-oauth2-json.c
+test_oauth2_json_LDADD = $(test_libs)
+test_oauth2_json_DEPENDENCIES = $(test_deps)
+test_oauth2_jwt_SOURCES = test-oauth2-jwt.c
+test_oauth2_jwt_LDADD = $(test_libs)
+@HAVE_WHOLE_ARCHIVE_TRUE@test_oauth2_jwt_LDFLAGS = -Wl,$(LD_WHOLE_ARCHIVE),../lib-ssl-iostream/.libs/libssl_iostream.a,$(LD_NO_WHOLE_ARCHIVE)
+test_oauth2_jwt_DEPENDENCIES = $(test_deps)
+all: all-am
+
+.SUFFIXES:
+.SUFFIXES: .c .lo .o .obj
+$(srcdir)/Makefile.in: @MAINTAINER_MODE_TRUE@ $(srcdir)/Makefile.am $(am__configure_deps)
+ @for dep in $?; do \
+ case '$(am__configure_deps)' in \
+ *$$dep*) \
+ ( cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh ) \
+ && { if test -f $@; then exit 0; else break; fi; }; \
+ exit 1;; \
+ esac; \
+ done; \
+ echo ' cd $(top_srcdir) && $(AUTOMAKE) --foreign src/lib-oauth2/Makefile'; \
+ $(am__cd) $(top_srcdir) && \
+ $(AUTOMAKE) --foreign src/lib-oauth2/Makefile
+Makefile: $(srcdir)/Makefile.in $(top_builddir)/config.status
+ @case '$?' in \
+ *config.status*) \
+ cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh;; \
+ *) \
+ echo ' cd $(top_builddir) && $(SHELL) ./config.status $(subdir)/$@ $(am__maybe_remake_depfiles)'; \
+ cd $(top_builddir) && $(SHELL) ./config.status $(subdir)/$@ $(am__maybe_remake_depfiles);; \
+ esac;
+
+$(top_builddir)/config.status: $(top_srcdir)/configure $(CONFIG_STATUS_DEPENDENCIES)
+ cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh
+
+$(top_srcdir)/configure: @MAINTAINER_MODE_TRUE@ $(am__configure_deps)
+ cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh
+$(ACLOCAL_M4): @MAINTAINER_MODE_TRUE@ $(am__aclocal_m4_deps)
+ cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh
+$(am__aclocal_m4_deps):
+
+clean-noinstPROGRAMS:
+ @list='$(noinst_PROGRAMS)'; test -n "$$list" || exit 0; \
+ echo " rm -f" $$list; \
+ rm -f $$list || exit $$?; \
+ test -n "$(EXEEXT)" || exit 0; \
+ list=`for p in $$list; do echo "$$p"; done | sed 's/$(EXEEXT)$$//'`; \
+ echo " rm -f" $$list; \
+ rm -f $$list
+
+clean-noinstLTLIBRARIES:
+ -test -z "$(noinst_LTLIBRARIES)" || rm -f $(noinst_LTLIBRARIES)
+ @list='$(noinst_LTLIBRARIES)'; \
+ locs=`for p in $$list; do echo $$p; done | \
+ sed 's|^[^/]*$$|.|; s|/[^/]*$$||; s|$$|/so_locations|' | \
+ sort -u`; \
+ test -z "$$locs" || { \
+ echo rm -f $${locs}; \
+ rm -f $${locs}; \
+ }
+
+liboauth2.la: $(liboauth2_la_OBJECTS) $(liboauth2_la_DEPENDENCIES) $(EXTRA_liboauth2_la_DEPENDENCIES)
+ $(AM_V_CCLD)$(LINK) $(liboauth2_la_OBJECTS) $(liboauth2_la_LIBADD) $(LIBS)
+
+test-oauth2-json$(EXEEXT): $(test_oauth2_json_OBJECTS) $(test_oauth2_json_DEPENDENCIES) $(EXTRA_test_oauth2_json_DEPENDENCIES)
+ @rm -f test-oauth2-json$(EXEEXT)
+ $(AM_V_CCLD)$(LINK) $(test_oauth2_json_OBJECTS) $(test_oauth2_json_LDADD) $(LIBS)
+
+test-oauth2-jwt$(EXEEXT): $(test_oauth2_jwt_OBJECTS) $(test_oauth2_jwt_DEPENDENCIES) $(EXTRA_test_oauth2_jwt_DEPENDENCIES)
+ @rm -f test-oauth2-jwt$(EXEEXT)
+ $(AM_V_CCLD)$(test_oauth2_jwt_LINK) $(test_oauth2_jwt_OBJECTS) $(test_oauth2_jwt_LDADD) $(LIBS)
+
+mostlyclean-compile:
+ -rm -f *.$(OBJEXT)
+
+distclean-compile:
+ -rm -f *.tab.c
+
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/oauth2-jwt.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/oauth2-key-cache.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/oauth2-request.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/oauth2.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/test-oauth2-json.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/test-oauth2-jwt.Po@am__quote@ # am--include-marker
+
+$(am__depfiles_remade):
+ @$(MKDIR_P) $(@D)
+ @echo '# dummy' >$@-t && $(am__mv) $@-t $@
+
+am--depfiles: $(am__depfiles_remade)
+
+.c.o:
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(COMPILE) -MT $@ -MD -MP -MF $(DEPDIR)/$*.Tpo -c -o $@ $<
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/$*.Tpo $(DEPDIR)/$*.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='$<' object='$@' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(COMPILE) -c -o $@ $<
+
+.c.obj:
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(COMPILE) -MT $@ -MD -MP -MF $(DEPDIR)/$*.Tpo -c -o $@ `$(CYGPATH_W) '$<'`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/$*.Tpo $(DEPDIR)/$*.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='$<' object='$@' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(COMPILE) -c -o $@ `$(CYGPATH_W) '$<'`
+
+.c.lo:
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(LTCOMPILE) -MT $@ -MD -MP -MF $(DEPDIR)/$*.Tpo -c -o $@ $<
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/$*.Tpo $(DEPDIR)/$*.Plo
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='$<' object='$@' libtool=yes @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(LTCOMPILE) -c -o $@ $<
+
+mostlyclean-libtool:
+ -rm -f *.lo
+
+clean-libtool:
+ -rm -rf .libs _libs
+install-pkginc_libHEADERS: $(pkginc_lib_HEADERS)
+ @$(NORMAL_INSTALL)
+ @list='$(pkginc_lib_HEADERS)'; test -n "$(pkginc_libdir)" || list=; \
+ if test -n "$$list"; then \
+ echo " $(MKDIR_P) '$(DESTDIR)$(pkginc_libdir)'"; \
+ $(MKDIR_P) "$(DESTDIR)$(pkginc_libdir)" || exit 1; \
+ fi; \
+ for p in $$list; do \
+ if test -f "$$p"; then d=; else d="$(srcdir)/"; fi; \
+ echo "$$d$$p"; \
+ done | $(am__base_list) | \
+ while read files; do \
+ echo " $(INSTALL_HEADER) $$files '$(DESTDIR)$(pkginc_libdir)'"; \
+ $(INSTALL_HEADER) $$files "$(DESTDIR)$(pkginc_libdir)" || exit $$?; \
+ done
+
+uninstall-pkginc_libHEADERS:
+ @$(NORMAL_UNINSTALL)
+ @list='$(pkginc_lib_HEADERS)'; test -n "$(pkginc_libdir)" || list=; \
+ files=`for p in $$list; do echo $$p; done | sed -e 's|^.*/||'`; \
+ dir='$(DESTDIR)$(pkginc_libdir)'; $(am__uninstall_files_from_dir)
+
+ID: $(am__tagged_files)
+ $(am__define_uniq_tagged_files); mkid -fID $$unique
+tags: tags-am
+TAGS: tags
+
+tags-am: $(TAGS_DEPENDENCIES) $(am__tagged_files)
+ set x; \
+ here=`pwd`; \
+ $(am__define_uniq_tagged_files); \
+ shift; \
+ if test -z "$(ETAGS_ARGS)$$*$$unique"; then :; else \
+ test -n "$$unique" || unique=$$empty_fix; \
+ if test $$# -gt 0; then \
+ $(ETAGS) $(ETAGSFLAGS) $(AM_ETAGSFLAGS) $(ETAGS_ARGS) \
+ "$$@" $$unique; \
+ else \
+ $(ETAGS) $(ETAGSFLAGS) $(AM_ETAGSFLAGS) $(ETAGS_ARGS) \
+ $$unique; \
+ fi; \
+ fi
+ctags: ctags-am
+
+CTAGS: ctags
+ctags-am: $(TAGS_DEPENDENCIES) $(am__tagged_files)
+ $(am__define_uniq_tagged_files); \
+ test -z "$(CTAGS_ARGS)$$unique" \
+ || $(CTAGS) $(CTAGSFLAGS) $(AM_CTAGSFLAGS) $(CTAGS_ARGS) \
+ $$unique
+
+GTAGS:
+ here=`$(am__cd) $(top_builddir) && pwd` \
+ && $(am__cd) $(top_srcdir) \
+ && gtags -i $(GTAGS_ARGS) "$$here"
+cscopelist: cscopelist-am
+
+cscopelist-am: $(am__tagged_files)
+ list='$(am__tagged_files)'; \
+ case "$(srcdir)" in \
+ [\\/]* | ?:[\\/]*) sdir="$(srcdir)" ;; \
+ *) sdir=$(subdir)/$(srcdir) ;; \
+ esac; \
+ for i in $$list; do \
+ if test -f "$$i"; then \
+ echo "$(subdir)/$$i"; \
+ else \
+ echo "$$sdir/$$i"; \
+ fi; \
+ done >> $(top_builddir)/cscope.files
+
+distclean-tags:
+ -rm -f TAGS ID GTAGS GRTAGS GSYMS GPATH tags
+
+distdir: $(BUILT_SOURCES)
+ $(MAKE) $(AM_MAKEFLAGS) distdir-am
+
+distdir-am: $(DISTFILES)
+ @srcdirstrip=`echo "$(srcdir)" | sed 's/[].[^$$\\*]/\\\\&/g'`; \
+ topsrcdirstrip=`echo "$(top_srcdir)" | sed 's/[].[^$$\\*]/\\\\&/g'`; \
+ list='$(DISTFILES)'; \
+ dist_files=`for file in $$list; do echo $$file; done | \
+ sed -e "s|^$$srcdirstrip/||;t" \
+ -e "s|^$$topsrcdirstrip/|$(top_builddir)/|;t"`; \
+ case $$dist_files in \
+ */*) $(MKDIR_P) `echo "$$dist_files" | \
+ sed '/\//!d;s|^|$(distdir)/|;s,/[^/]*$$,,' | \
+ sort -u` ;; \
+ esac; \
+ for file in $$dist_files; do \
+ if test -f $$file || test -d $$file; then d=.; else d=$(srcdir); fi; \
+ if test -d $$d/$$file; then \
+ dir=`echo "/$$file" | sed -e 's,/[^/]*$$,,'`; \
+ if test -d "$(distdir)/$$file"; then \
+ find "$(distdir)/$$file" -type d ! -perm -700 -exec chmod u+rwx {} \;; \
+ fi; \
+ if test -d $(srcdir)/$$file && test $$d != $(srcdir); then \
+ cp -fpR $(srcdir)/$$file "$(distdir)$$dir" || exit 1; \
+ find "$(distdir)/$$file" -type d ! -perm -700 -exec chmod u+rwx {} \;; \
+ fi; \
+ cp -fpR $$d/$$file "$(distdir)$$dir" || exit 1; \
+ else \
+ test -f "$(distdir)/$$file" \
+ || cp -p $$d/$$file "$(distdir)/$$file" \
+ || exit 1; \
+ fi; \
+ done
+check-am: all-am
+ $(MAKE) $(AM_MAKEFLAGS) check-local
+check: check-am
+all-am: Makefile $(PROGRAMS) $(LTLIBRARIES) $(HEADERS)
+installdirs:
+ for dir in "$(DESTDIR)$(pkginc_libdir)"; do \
+ test -z "$$dir" || $(MKDIR_P) "$$dir"; \
+ done
+install: install-am
+install-exec: install-exec-am
+install-data: install-data-am
+uninstall: uninstall-am
+
+install-am: all-am
+ @$(MAKE) $(AM_MAKEFLAGS) install-exec-am install-data-am
+
+installcheck: installcheck-am
+install-strip:
+ if test -z '$(STRIP)'; then \
+ $(MAKE) $(AM_MAKEFLAGS) INSTALL_PROGRAM="$(INSTALL_STRIP_PROGRAM)" \
+ install_sh_PROGRAM="$(INSTALL_STRIP_PROGRAM)" INSTALL_STRIP_FLAG=-s \
+ install; \
+ else \
+ $(MAKE) $(AM_MAKEFLAGS) INSTALL_PROGRAM="$(INSTALL_STRIP_PROGRAM)" \
+ install_sh_PROGRAM="$(INSTALL_STRIP_PROGRAM)" INSTALL_STRIP_FLAG=-s \
+ "INSTALL_PROGRAM_ENV=STRIPPROG='$(STRIP)'" install; \
+ fi
+mostlyclean-generic:
+
+clean-generic:
+
+distclean-generic:
+ -test -z "$(CONFIG_CLEAN_FILES)" || rm -f $(CONFIG_CLEAN_FILES)
+ -test . = "$(srcdir)" || test -z "$(CONFIG_CLEAN_VPATH_FILES)" || rm -f $(CONFIG_CLEAN_VPATH_FILES)
+
+maintainer-clean-generic:
+ @echo "This command is intended for maintainers to use"
+ @echo "it deletes files that may require special tools to rebuild."
+clean: clean-am
+
+clean-am: clean-generic clean-libtool clean-noinstLTLIBRARIES \
+ clean-noinstPROGRAMS mostlyclean-am
+
+distclean: distclean-am
+ -rm -f ./$(DEPDIR)/oauth2-jwt.Plo
+ -rm -f ./$(DEPDIR)/oauth2-key-cache.Plo
+ -rm -f ./$(DEPDIR)/oauth2-request.Plo
+ -rm -f ./$(DEPDIR)/oauth2.Plo
+ -rm -f ./$(DEPDIR)/test-oauth2-json.Po
+ -rm -f ./$(DEPDIR)/test-oauth2-jwt.Po
+ -rm -f Makefile
+distclean-am: clean-am distclean-compile distclean-generic \
+ distclean-tags
+
+dvi: dvi-am
+
+dvi-am:
+
+html: html-am
+
+html-am:
+
+info: info-am
+
+info-am:
+
+install-data-am: install-pkginc_libHEADERS
+
+install-dvi: install-dvi-am
+
+install-dvi-am:
+
+install-exec-am:
+
+install-html: install-html-am
+
+install-html-am:
+
+install-info: install-info-am
+
+install-info-am:
+
+install-man:
+
+install-pdf: install-pdf-am
+
+install-pdf-am:
+
+install-ps: install-ps-am
+
+install-ps-am:
+
+installcheck-am:
+
+maintainer-clean: maintainer-clean-am
+ -rm -f ./$(DEPDIR)/oauth2-jwt.Plo
+ -rm -f ./$(DEPDIR)/oauth2-key-cache.Plo
+ -rm -f ./$(DEPDIR)/oauth2-request.Plo
+ -rm -f ./$(DEPDIR)/oauth2.Plo
+ -rm -f ./$(DEPDIR)/test-oauth2-json.Po
+ -rm -f ./$(DEPDIR)/test-oauth2-jwt.Po
+ -rm -f Makefile
+maintainer-clean-am: distclean-am maintainer-clean-generic
+
+mostlyclean: mostlyclean-am
+
+mostlyclean-am: mostlyclean-compile mostlyclean-generic \
+ mostlyclean-libtool
+
+pdf: pdf-am
+
+pdf-am:
+
+ps: ps-am
+
+ps-am:
+
+uninstall-am: uninstall-pkginc_libHEADERS
+
+.MAKE: check-am install-am install-strip
+
+.PHONY: CTAGS GTAGS TAGS all all-am am--depfiles check check-am \
+ check-local clean clean-generic clean-libtool \
+ clean-noinstLTLIBRARIES clean-noinstPROGRAMS cscopelist-am \
+ ctags ctags-am distclean distclean-compile distclean-generic \
+ distclean-libtool distclean-tags distdir dvi dvi-am html \
+ html-am info info-am install install-am install-data \
+ install-data-am install-dvi install-dvi-am install-exec \
+ install-exec-am install-html install-html-am install-info \
+ install-info-am install-man install-pdf install-pdf-am \
+ install-pkginc_libHEADERS install-ps install-ps-am \
+ install-strip installcheck installcheck-am installdirs \
+ maintainer-clean maintainer-clean-generic mostlyclean \
+ mostlyclean-compile mostlyclean-generic mostlyclean-libtool \
+ pdf pdf-am ps ps-am tags tags-am uninstall uninstall-am \
+ uninstall-pkginc_libHEADERS
+
+.PRECIOUS: Makefile
+
+
+check-local:
+ for bin in $(test_programs); do \
+ if ! $(RUN_TEST) ./$$bin; then exit 1; fi; \
+ done
+
+# Tell versions [3.59,3.63) of GNU make to not export all variables.
+# Otherwise a system limit (for SysV at least) may be exceeded.
+.NOEXPORT:
diff --git a/src/lib-oauth2/oauth2-jwt.c b/src/lib-oauth2/oauth2-jwt.c
new file mode 100644
index 0000000..ec7ad46
--- /dev/null
+++ b/src/lib-oauth2/oauth2-jwt.c
@@ -0,0 +1,489 @@
+/* Copyright (c) 2020 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "buffer.h"
+#include "str.h"
+#include "hmac.h"
+#include "array.h"
+#include "hash-method.h"
+#include "istream.h"
+#include "iso8601-date.h"
+#include "json-tree.h"
+#include "array.h"
+#include "base64.h"
+#include "str-sanitize.h"
+#include "dcrypt.h"
+#include "var-expand.h"
+#include "oauth2.h"
+#include "oauth2-private.h"
+#include "dict.h"
+
+#include <time.h>
+
+static const char *get_field(const struct json_tree *tree, const char *key)
+{
+ const struct json_tree_node *root = json_tree_root(tree);
+ const struct json_tree_node *value_node = json_tree_find_key(root, key);
+ if (value_node == NULL || value_node->value_type == JSON_TYPE_OBJECT ||
+ value_node->value_type == JSON_TYPE_ARRAY)
+ return NULL;
+ return json_tree_get_value_str(value_node);
+}
+
+static int get_time_field(const struct json_tree *tree, const char *key,
+ int64_t *value_r)
+{
+ time_t tvalue;
+ const char *value = get_field(tree, key);
+ int tz_offset ATTR_UNUSED;
+ if (value == NULL)
+ return 0;
+ if (str_to_int64(value, value_r) == 0) {
+ if (*value_r < 0)
+ return -1;
+ return 1;
+ } else if (iso8601_date_parse((const unsigned char*)value, strlen(value),
+ &tvalue, &tz_offset)) {
+ if (tvalue < 0)
+ return -1;
+ *value_r = tvalue;
+ return 1;
+ }
+ return -1;
+}
+
+/* Escapes '/' and '%' in identifier to %hex */
+static const char *escape_identifier(const char *identifier)
+{
+ size_t pos = strcspn(identifier, "/%");
+ /* nothing to escape */
+ if (identifier[pos] == '\0')
+ return identifier;
+
+ size_t len = strlen(identifier);
+ string_t *new_id = t_str_new(len);
+ str_append_data(new_id, identifier, pos);
+
+ for (size_t i = pos; i < len; i++) {
+ switch (identifier[i]) {
+ case '/':
+ str_append(new_id, "%2f");
+ break;
+ case '%':
+ str_append(new_id, "%25");
+ break;
+ default:
+ str_append_c(new_id, identifier[i]);
+ break;
+ }
+ }
+ return str_c(new_id);
+}
+
+static int
+oauth2_lookup_hmac_key(const struct oauth2_settings *set, const char *azp,
+ const char *alg, const char *key_id,
+ const buffer_t **hmac_key_r, const char **error_r)
+{
+ const char *base64_key;
+ const char *cache_key_id, *lookup_key;
+ int ret;
+
+ cache_key_id = t_strconcat(azp, ".", alg, ".", key_id, NULL);
+ if (oauth2_validation_key_cache_lookup_hmac_key(
+ set->key_cache, cache_key_id, hmac_key_r) == 0)
+ return 0;
+
+
+ /* do a synchronous dict lookup */
+ lookup_key = t_strconcat(DICT_PATH_SHARED, azp, "/", alg, "/", key_id,
+ NULL);
+ struct dict_op_settings dict_set = {
+ .username = NULL,
+ };
+ if ((ret = dict_lookup(set->key_dict, &dict_set, pool_datastack_create(),
+ lookup_key, &base64_key, error_r)) < 0) {
+ return -1;
+ } else if (ret == 0) {
+ *error_r = t_strdup_printf("%s key '%s' not found",
+ alg, key_id);
+ return -1;
+ }
+
+ /* decode key */
+ buffer_t *key = t_base64_decode_str(base64_key);
+ if (key->used == 0) {
+ *error_r = "Invalid base64 encoded key";
+ return -1;
+ }
+ oauth2_validation_key_cache_insert_hmac_key(set->key_cache,
+ cache_key_id, key);
+ *hmac_key_r = key;
+ return 0;
+}
+
+static int
+oauth2_validate_hmac(const struct oauth2_settings *set, const char *azp,
+ const char *alg, const char *key_id,
+ const char *const *blobs, const char **error_r)
+{
+ const struct hash_method *method;
+
+ if (strcmp(alg, "HS256") == 0)
+ method = hash_method_lookup("sha256");
+ else if (strcmp(alg, "HS384") == 0)
+ method = hash_method_lookup("sha384");
+ else if (strcmp(alg, "HS512") == 0)
+ method = hash_method_lookup("sha512");
+ else {
+ *error_r = t_strdup_printf("unsupported algorithm '%s'", alg);
+ return -1;
+ }
+
+ const buffer_t *key;
+ if (oauth2_lookup_hmac_key(set, azp, alg, key_id, &key, error_r) < 0)
+ return -1;
+
+ struct hmac_context ctx;
+ hmac_init(&ctx, key->data, key->used, method);
+ hmac_update(&ctx, blobs[0], strlen(blobs[0]));
+ hmac_update(&ctx, ".", 1);
+ hmac_update(&ctx, blobs[1], strlen(blobs[1]));
+ unsigned char digest[method->digest_size];
+
+ hmac_final(&ctx, digest);
+
+ buffer_t *their_digest =
+ t_base64url_decode_str(BASE64_DECODE_FLAG_NO_PADDING, blobs[2]);
+ if (method->digest_size != their_digest->used ||
+ !mem_equals_timing_safe(digest, their_digest->data,
+ method->digest_size)) {
+ *error_r = "Incorrect JWT signature";
+ return -1;
+ }
+ return 0;
+}
+
+static int
+oauth2_lookup_pubkey(const struct oauth2_settings *set, const char *azp,
+ const char *alg, const char *key_id,
+ struct dcrypt_public_key **key_r, const char **error_r)
+{
+ const char *key_str;
+ const char *cache_key_id, *lookup_key;
+ int ret;
+
+ cache_key_id = t_strconcat(azp, ".", alg, ".", key_id, NULL);
+ if (oauth2_validation_key_cache_lookup_pubkey(
+ set->key_cache, cache_key_id, key_r) == 0)
+ return 0;
+
+ /* do a synchronous dict lookup */
+ lookup_key = t_strconcat(DICT_PATH_SHARED, azp, "/", alg, "/", key_id,
+ NULL);
+ struct dict_op_settings dict_set = {
+ .username = NULL,
+ };
+ if ((ret = dict_lookup(set->key_dict, &dict_set, pool_datastack_create(),
+ lookup_key, &key_str, error_r)) < 0) {
+ return -1;
+ } else if (ret == 0) {
+ *error_r = t_strdup_printf("%s key '%s' not found",
+ alg, key_id);
+ return -1;
+ }
+
+ /* try to load key */
+ struct dcrypt_public_key *pubkey;
+ const char *error;
+ if (!dcrypt_key_load_public(&pubkey, key_str, &error)) {
+ *error_r = t_strdup_printf("Cannot load key: %s", error);
+ return -1;
+ }
+
+ /* cache key */
+ oauth2_validation_key_cache_insert_pubkey(set->key_cache, cache_key_id,
+ pubkey);
+ *key_r = pubkey;
+ return 0;
+}
+
+static int
+oauth2_validate_rsa_ecdsa(const struct oauth2_settings *set,
+ const char *azp, const char *alg, const char *key_id,
+ const char *const *blobs, const char **error_r)
+{
+ const char *method;
+ enum dcrypt_padding padding;
+ enum dcrypt_signature_format sig_format;
+
+ if (!dcrypt_is_initialized()) {
+ *error_r = "No crypto library loaded";
+ return -1;
+ }
+
+ if (str_begins(alg, "RS")) {
+ padding = DCRYPT_PADDING_RSA_PKCS1;
+ sig_format = DCRYPT_SIGNATURE_FORMAT_DSS;
+ } else if (str_begins(alg, "PS")) {
+ padding = DCRYPT_PADDING_RSA_PKCS1_PSS;
+ sig_format = DCRYPT_SIGNATURE_FORMAT_DSS;
+ } else if (str_begins(alg, "ES")) {
+ padding = DCRYPT_PADDING_DEFAULT;
+ sig_format = DCRYPT_SIGNATURE_FORMAT_X962;
+ } else {
+ /* this should be checked by caller */
+ i_unreached();
+ }
+
+ if (strcmp(alg+2, "256") == 0) {
+ method = "sha256";
+ } else if (strcmp(alg+2, "384") == 0) {
+ method = "sha384";
+ } else if (strcmp(alg+2, "512") == 0) {
+ method = "sha512";
+ } else {
+ *error_r = t_strdup_printf("Unsupported algorithm '%s'", alg);
+ return -1;
+ }
+
+ buffer_t *signature =
+ t_base64url_decode_str(BASE64_DECODE_FLAG_NO_PADDING, blobs[2]);
+
+ struct dcrypt_public_key *pubkey;
+ if (oauth2_lookup_pubkey(set, azp, alg, key_id, &pubkey, error_r) < 0)
+ return -1;
+
+ /* data to verify */
+ const char *data = t_strconcat(blobs[0], ".", blobs[1], NULL);
+
+ /* verify signature */
+ bool valid;
+ if (!dcrypt_verify(pubkey, method, sig_format, data, strlen(data),
+ signature->data, signature->used, &valid, padding,
+ error_r)) {
+ valid = FALSE;
+ } else if (!valid) {
+ *error_r = "Bad signature";
+ }
+
+ return valid ? 0 : -1;
+}
+
+static int
+oauth2_validate_signature(const struct oauth2_settings *set, const char *azp,
+ const char *alg, const char *key_id,
+ const char *const *blobs, const char **error_r)
+{
+ if (str_begins(alg, "HS")) {
+ return oauth2_validate_hmac(set, azp, alg, key_id, blobs,
+ error_r);
+ } else if (str_begins(alg, "RS") || str_begins(alg, "PS") ||
+ str_begins(alg, "ES")) {
+ return oauth2_validate_rsa_ecdsa(set, azp, alg, key_id, blobs,
+ error_r);
+ }
+
+ *error_r = t_strdup_printf("Unsupported algorithm '%s'", alg);
+ return -1;
+}
+
+static void
+oauth2_jwt_copy_fields(ARRAY_TYPE(oauth2_field) *fields, struct json_tree *tree)
+{
+ pool_t pool = array_get_pool(fields);
+ ARRAY(const struct json_tree_node*) nodes;
+ const struct json_tree_node *root = json_tree_root(tree);
+
+ t_array_init(&nodes, 1);
+ array_push_back(&nodes, &root);
+
+ while (array_count(&nodes) > 0) {
+ const struct json_tree_node *const *pnode = array_front(&nodes);
+ const struct json_tree_node *node = *pnode;
+ array_pop_front(&nodes);
+ while (node != NULL) {
+ if (node->value_type == JSON_TYPE_OBJECT) {
+ root = node->value.child;
+ array_push_back(&nodes, &root);
+ } else if (node->key != NULL) {
+ struct oauth2_field *field =
+ array_append_space(fields);
+ field->name = p_strdup(pool, node->key);
+ field->value = p_strdup(
+ pool, json_tree_get_value_str(node));
+ }
+ node = node->next;
+ }
+ }
+}
+
+static int
+oauth2_jwt_header_process(struct json_tree *tree, const char **alg_r,
+ const char **kid_r, const char **error_r)
+{
+ const char *typ = get_field(tree, "typ");
+ const char *alg = get_field(tree, "alg");
+ const char *kid = get_field(tree, "kid");
+
+ if (null_strcmp(typ, "JWT") != 0) {
+ *error_r = "Cannot find 'typ' field";
+ return -1;
+ }
+
+ if (alg == NULL) {
+ *error_r = "Cannot find 'alg' field";
+ return -1;
+ }
+
+ /* These are lost when tree is deinitialized.
+ Make sure algorithm is uppercased. */
+ *alg_r = t_str_ucase(alg);
+ *kid_r = t_strdup(kid);
+ return 0;
+}
+
+static int
+oauth2_jwt_body_process(const struct oauth2_settings *set, const char *alg,
+ const char *kid, ARRAY_TYPE(oauth2_field) *fields,
+ struct json_tree *tree, const char *const *blobs,
+ const char **error_r)
+{
+ const char *sub = get_field(tree, "sub");
+
+ int ret;
+ int64_t t0 = time(NULL);
+ /* default IAT and NBF to now */
+ int64_t iat, nbf, exp;
+ int tz_offset ATTR_UNUSED;
+
+ if (sub == NULL) {
+ *error_r = "Missing 'sub' field";
+ return -1;
+ }
+
+ if ((ret = get_time_field(tree, "exp", &exp)) < 1) {
+ *error_r = t_strdup_printf("%s 'exp' field",
+ ret == 0 ? "Missing" : "Malformed");
+ return -1;
+ }
+
+ if ((ret = get_time_field(tree, "nbf", &nbf)) < 0) {
+ *error_r = "Malformed 'nbf' field";
+ return -1;
+ } else if (ret == 0 || nbf == 0)
+ nbf = t0;
+
+ if ((ret = get_time_field(tree, "iat", &iat)) < 0) {
+ *error_r = "Malformed 'iat' field";
+ return -1;
+ } else if (ret == 0 || iat == 0)
+ iat = t0;
+
+ if (nbf > t0) {
+ *error_r = "Token is not valid yet";
+ return -1;
+ }
+ if (iat > t0) {
+ *error_r = "Token is issued in future";
+ return -1;
+ }
+ if (exp < t0) {
+ *error_r = "Token has expired";
+ return -1;
+ }
+
+ /* ensure token dates are not conflicting */
+ if (exp < iat ||
+ exp < nbf) {
+ *error_r = "Token time values are conflicting";
+ return -1;
+ }
+
+ const char *iss = get_field(tree, "iss");
+ if (set->issuers != NULL && *set->issuers != NULL) {
+ if (iss == NULL) {
+ *error_r = "Token is missing 'iss' field";
+ return -1;
+ }
+ if (!str_array_find(set->issuers, iss)) {
+ *error_r = t_strdup_printf("Issuer '%s' is not allowed",
+ str_sanitize_utf8(iss, 128));
+ return -1;
+ }
+ }
+
+ /* see if there is azp */
+ const char *azp = get_field(tree, "azp");
+ if (azp == NULL)
+ azp = "default";
+ else
+ azp = escape_identifier(azp);
+
+ if (oauth2_validate_signature(set, azp, alg, kid, blobs, error_r) < 0)
+ return -1;
+
+ oauth2_jwt_copy_fields(fields, tree);
+ return 0;
+}
+
+int oauth2_try_parse_jwt(const struct oauth2_settings *set,
+ const char *token, ARRAY_TYPE(oauth2_field) *fields,
+ bool *is_jwt_r, const char **error_r)
+{
+ const char *const *blobs = t_strsplit(token, ".");
+ int ret;
+
+ i_assert(set->key_dict != NULL);
+
+ /* we don't know if it's JWT token yet */
+ *is_jwt_r = FALSE;
+
+ if (str_array_length(blobs) != 3) {
+ *error_r = "Not a JWT token";
+ return -1;
+ }
+
+ /* attempt to decode header */
+ buffer_t *header =
+ t_base64url_decode_str(BASE64_DECODE_FLAG_NO_PADDING, blobs[0]);
+
+ if (header->used == 0) {
+ *error_r = "Not a JWT token";
+ return -1;
+ }
+
+ struct json_tree *header_tree;
+ if (oauth2_json_tree_build(header, &header_tree, error_r) < 0)
+ return -1;
+
+ const char *alg, *kid;
+ ret = oauth2_jwt_header_process(header_tree, &alg, &kid, error_r);
+ json_tree_deinit(&header_tree);
+ if (ret < 0)
+ return -1;
+
+ /* it is now assumed to be a JWT token */
+ *is_jwt_r = TRUE;
+
+ if (kid == NULL)
+ kid = "default";
+ else if (*kid == '\0') {
+ *error_r = "'kid' field is empty";
+ return -1;
+ } else {
+ kid = escape_identifier(kid);
+ }
+
+ /* parse body */
+ struct json_tree *body_tree;
+ buffer_t *body =
+ t_base64url_decode_str(BASE64_DECODE_FLAG_NO_PADDING, blobs[1]);
+ if (oauth2_json_tree_build(body, &body_tree, error_r) == -1)
+ return -1;
+ ret = oauth2_jwt_body_process(set, alg, kid, fields, body_tree, blobs,
+ error_r);
+ json_tree_deinit(&body_tree);
+
+ return ret;
+}
diff --git a/src/lib-oauth2/oauth2-key-cache.c b/src/lib-oauth2/oauth2-key-cache.c
new file mode 100644
index 0000000..e85c210
--- /dev/null
+++ b/src/lib-oauth2/oauth2-key-cache.c
@@ -0,0 +1,158 @@
+/* Copyright (c) 2020 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "llist.h"
+#include "buffer.h"
+#include "hash.h"
+#include "dcrypt.h"
+#include "oauth2.h"
+#include "oauth2-private.h"
+
+struct oauth2_key_cache_entry {
+ const char *key_id;
+ struct dcrypt_public_key *pubkey;
+ buffer_t *hmac_key;
+ struct oauth2_key_cache_entry *prev, *next;
+};
+
+HASH_TABLE_DEFINE_TYPE(oauth2_key_cache, const char *,
+ struct oauth2_key_cache_entry *);
+
+struct oauth2_validation_key_cache {
+ pool_t pool;
+ HASH_TABLE_TYPE(oauth2_key_cache) keys;
+ struct oauth2_key_cache_entry *list_start;
+};
+
+struct oauth2_validation_key_cache *oauth2_validation_key_cache_init(void)
+{
+ pool_t pool = pool_alloconly_create(
+ MEMPOOL_GROWING"oauth2 key cache", 128);
+ struct oauth2_validation_key_cache *cache =
+ p_new(pool, struct oauth2_validation_key_cache, 1);
+
+ cache->pool = pool;
+ hash_table_create(&cache->keys, pool, 8, str_hash, strcmp);
+ return cache;
+}
+
+void oauth2_validation_key_cache_deinit(
+ struct oauth2_validation_key_cache **_cache)
+{
+ struct oauth2_validation_key_cache *cache = *_cache;
+ *_cache = NULL;
+ if (cache == NULL)
+ return;
+
+ /* free resources */
+ struct oauth2_key_cache_entry *entry = cache->list_start;
+ while (entry != NULL) {
+ if (entry->pubkey != NULL)
+ dcrypt_key_unref_public(&entry->pubkey);
+ entry = entry->next;
+ }
+ hash_table_destroy(&cache->keys);
+ pool_unref(&cache->pool);
+}
+
+int oauth2_validation_key_cache_lookup_pubkey(
+ struct oauth2_validation_key_cache *cache, const char *key_id,
+ struct dcrypt_public_key **pubkey_r)
+{
+ if (cache == NULL)
+ return -1;
+
+ struct oauth2_key_cache_entry *entry =
+ hash_table_lookup(cache->keys, key_id);
+ if (entry == NULL || entry->pubkey == NULL)
+ return -1;
+
+ *pubkey_r = entry->pubkey;
+ return 0;
+}
+
+int oauth2_validation_key_cache_lookup_hmac_key(
+ struct oauth2_validation_key_cache *cache, const char *key_id,
+ const buffer_t **hmac_key_r)
+{
+ if (cache == NULL)
+ return -1;
+
+ struct oauth2_key_cache_entry *entry =
+ hash_table_lookup(cache->keys, key_id);
+ if (entry == NULL || entry->hmac_key == NULL ||
+ entry->hmac_key->used == 0)
+ return -1;
+
+ *hmac_key_r = entry->hmac_key;
+ return 0;
+}
+
+void oauth2_validation_key_cache_insert_pubkey(
+ struct oauth2_validation_key_cache *cache, const char *key_id,
+ struct dcrypt_public_key *pubkey)
+{
+ if (cache == NULL)
+ return;
+
+ struct oauth2_key_cache_entry *entry =
+ hash_table_lookup(cache->keys, key_id);
+ if (entry != NULL) {
+ dcrypt_key_unref_public(&entry->pubkey);
+ entry->pubkey = pubkey;
+ if (entry->hmac_key != NULL)
+ buffer_set_used_size(entry->hmac_key, 0);
+ return;
+ }
+ entry = p_new(cache->pool, struct oauth2_key_cache_entry, 1);
+ entry->key_id = p_strdup(cache->pool, key_id);
+ entry->pubkey = pubkey;
+ DLLIST_PREPEND(&cache->list_start, entry);
+ hash_table_insert(cache->keys, entry->key_id, entry);
+}
+
+void oauth2_validation_key_cache_insert_hmac_key(
+ struct oauth2_validation_key_cache *cache, const char *key_id,
+ const buffer_t *hmac_key)
+{
+ if (cache == NULL)
+ return;
+
+ struct oauth2_key_cache_entry *entry =
+ hash_table_lookup(cache->keys, key_id);
+ if (entry != NULL) {
+ dcrypt_key_unref_public(&entry->pubkey);
+ if (entry->hmac_key == NULL) {
+ entry->hmac_key = buffer_create_dynamic(
+ cache->pool, hmac_key->used);
+ } else {
+ buffer_set_used_size(entry->hmac_key, 0);
+ }
+ buffer_append(entry->hmac_key, hmac_key->data, hmac_key->used);
+ return;
+ }
+ entry = p_new(cache->pool, struct oauth2_key_cache_entry, 1);
+ entry->key_id = p_strdup(cache->pool, key_id);
+ entry->hmac_key = buffer_create_dynamic(cache->pool, hmac_key->used);
+ buffer_append(entry->hmac_key, hmac_key->data, hmac_key->used);
+ DLLIST_PREPEND(&cache->list_start, entry);
+ hash_table_insert(cache->keys, entry->key_id, entry);
+}
+
+int oauth2_validation_key_cache_evict(struct oauth2_validation_key_cache *cache,
+ const char *key_id)
+{
+ if (cache == NULL)
+ return -1;
+
+ struct oauth2_key_cache_entry *entry =
+ hash_table_lookup(cache->keys, key_id);
+ if (entry == NULL)
+ return -1;
+ if (entry->pubkey != NULL)
+ dcrypt_key_unref_public(&entry->pubkey);
+ DLLIST_REMOVE(&cache->list_start, entry);
+ hash_table_remove(cache->keys, key_id);
+ return 0;
+}
diff --git a/src/lib-oauth2/oauth2-private.h b/src/lib-oauth2/oauth2-private.h
new file mode 100644
index 0000000..038f9e1
--- /dev/null
+++ b/src/lib-oauth2/oauth2-private.h
@@ -0,0 +1,50 @@
+#ifndef OAUTH2_PRIVATE_H
+#define OAUTH2_PRIVATE_H
+
+struct json_tree;
+struct dcrypt_public_key;
+
+struct oauth2_request {
+ pool_t pool;
+
+ const struct oauth2_settings *set;
+ struct http_client_request *req;
+ struct json_parser *parser;
+ struct istream *is;
+ struct io *io;
+
+ const char *delayed_error;
+ struct timeout *to_delayed_error;
+
+ const char *username;
+ const char *key_file_template;
+
+ void (*json_parsed_cb)(struct oauth2_request *, const char *error);
+
+ ARRAY_TYPE(oauth2_field) fields;
+ char *field_name;
+
+ oauth2_request_callback_t *req_callback;
+ void *req_context;
+ /* indicates whether token is valid */
+ unsigned int response_status;
+};
+
+void oauth2_request_parse_json(struct oauth2_request *req);
+int oauth2_json_tree_build(const buffer_t *json, struct json_tree **tree_r,
+ const char **error_r);
+
+int oauth2_validation_key_cache_lookup_pubkey(
+ struct oauth2_validation_key_cache *cache, const char *key_id,
+ struct dcrypt_public_key **pubkey_r);
+int oauth2_validation_key_cache_lookup_hmac_key(
+ struct oauth2_validation_key_cache *cache, const char *key_id,
+ const buffer_t **hmac_key_r);
+void oauth2_validation_key_cache_insert_pubkey(
+ struct oauth2_validation_key_cache *cache, const char *key_id,
+ struct dcrypt_public_key *pubkey);
+void oauth2_validation_key_cache_insert_hmac_key(
+ struct oauth2_validation_key_cache *cache, const char *key_id,
+ const buffer_t *hmac_key);
+
+#endif
diff --git a/src/lib-oauth2/oauth2-request.c b/src/lib-oauth2/oauth2-request.c
new file mode 100644
index 0000000..032eff1
--- /dev/null
+++ b/src/lib-oauth2/oauth2-request.c
@@ -0,0 +1,363 @@
+/* Copyright (c) 2019 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "ioloop.h"
+#include "istream.h"
+#include "str.h"
+#include "http-client.h"
+#include "http-url.h"
+#include "json-parser.h"
+#include "oauth2.h"
+#include "oauth2-private.h"
+
+static void oauth2_request_free(struct oauth2_request *req)
+{
+ timeout_remove(&req->to_delayed_error);
+ pool_unref(&req->pool);
+}
+
+static void
+oauth2_request_callback(struct oauth2_request *req,
+ struct oauth2_request_result *res)
+{
+ i_assert(req->req_callback != NULL);
+ oauth2_request_callback_t *callback = req->req_callback;
+ req->req_callback = NULL;
+ callback(res, req->req_context);
+ oauth2_request_free(req);
+}
+
+static bool
+oauth2_request_field_parse(const struct oauth2_field *field,
+ struct oauth2_request_result *res)
+{
+ if (strcasecmp(field->name, "expires_in") == 0) {
+ uint32_t expires_in = 0;
+ if (str_to_uint32(field->value, &expires_in) < 0) {
+ res->error = t_strdup_printf(
+ "Malformed number '%s' in expires_in",
+ field->value);
+ return FALSE;
+ } else {
+ res->expires_at = ioloop_time + expires_in;
+ }
+ } else if (strcasecmp(field->name, "token_type") == 0) {
+ if (strcasecmp(field->value, "bearer") != 0) {
+ res->error = t_strdup_printf(
+ "Expected Bearer token, got '%s'",
+ field->value);
+ return FALSE;
+ }
+ }
+ return TRUE;
+}
+
+static void
+oauth2_request_continue(struct oauth2_request *req, const char *error)
+{
+ struct oauth2_request_result res;
+ i_zero(&res);
+
+ unsigned int status_hi = req->response_status/100;
+ i_assert(status_hi == 2 || status_hi == 4);
+
+ if (error != NULL)
+ res.error = error;
+ else {
+ const struct oauth2_field *field;
+ /* see if we can figure out when it expires */
+ array_foreach(&req->fields, field) {
+ if (!oauth2_request_field_parse(field, &res))
+ break;
+ }
+ res.valid = (status_hi == 2) && res.error == NULL;
+ }
+
+ res.fields = &req->fields;
+
+ oauth2_request_callback(req, &res);
+}
+
+void oauth2_request_parse_json(struct oauth2_request *req)
+{
+ enum json_type type;
+ const char *token, *error;
+ int ret;
+
+ while((ret = json_parse_next(req->parser, &type, &token)) > 0) {
+ if (req->field_name == NULL) {
+ if (type != JSON_TYPE_OBJECT_KEY) break;
+ /* cannot use t_strdup because we might
+ have to read more */
+ req->field_name = p_strdup(req->pool, token);
+ } else if (type < JSON_TYPE_STRING) {
+ /* this should be last allocation */
+ p_free(req->pool, req->field_name);
+ json_parse_skip(req->parser);
+ } else {
+ if (!array_is_created(&req->fields))
+ p_array_init(&req->fields, req->pool, 4);
+ struct oauth2_field *field =
+ array_append_space(&req->fields);
+ field->name = req->field_name;
+ req->field_name = NULL;
+ field->value = p_strdup(req->pool, token);
+ }
+ }
+
+ /* read more */
+ if (ret == 0) return;
+
+ io_remove(&req->io);
+
+ if (ret > 0) {
+ (void)json_parser_deinit(&req->parser, &error);
+ error = "Invalid response data";
+ } else if (i_stream_read_eof(req->is) &&
+ req->is->v_offset == 0 && req->is->stream_errno == 0) {
+ /* discard error, empty response is OK. */
+ (void)json_parser_deinit(&req->parser, &error);
+ error = NULL;
+ } else if (json_parser_deinit(&req->parser, &error) == 0) {
+ error = NULL;
+ } else {
+ i_assert(error != NULL);
+ }
+
+ i_stream_unref(&req->is);
+
+ req->json_parsed_cb(req, error);
+}
+
+static void
+oauth2_request_response(const struct http_response *response,
+ struct oauth2_request *req)
+{
+ req->response_status = response->status;
+ unsigned int status_hi = req->response_status/100;
+
+ if (status_hi != 2 && status_hi != 4) {
+ /* Unexpected internal error */
+ struct oauth2_request_result res = {
+ .error = http_response_get_message(response),
+ };
+ oauth2_request_callback(req, &res);
+ return;
+ }
+
+ if (response->payload != NULL) {
+ req->is = response->payload;
+ i_stream_ref(req->is);
+ } else {
+ req->is = i_stream_create_from_data("", 0);
+ }
+
+ p_array_init(&req->fields, req->pool, 1);
+ req->parser = json_parser_init(req->is);
+ req->json_parsed_cb = oauth2_request_continue;
+ req->io = io_add_istream(req->is, oauth2_request_parse_json, req);
+ oauth2_request_parse_json(req);
+}
+
+static void
+oauth2_request_set_headers(struct oauth2_request *req,
+ const struct oauth2_request_input *input)
+{
+ if (!req->set->send_auth_headers)
+ return;
+ if (input->service != NULL) {
+ http_client_request_add_header(
+ req->req, "X-Dovecot-Auth-Service", input->service);
+ }
+ if (input->local_ip.family != 0) {
+ const char *addr;
+ if (net_ipport2str(&input->local_ip, input->local_port,
+ &addr) == 0) {
+ http_client_request_add_header(
+ req->req, "X-Dovecot-Auth-Local", addr);
+ }
+ }
+ if (input->remote_ip.family != 0) {
+ const char *addr;
+ if (net_ipport2str(&input->remote_ip, input->remote_port,
+ &addr) == 0) {
+ http_client_request_add_header(
+ req->req, "X-Dovecot-Auth-Remote", addr);
+ }
+ }
+}
+
+static struct oauth2_request *
+oauth2_request_start(const struct oauth2_settings *set,
+ const struct oauth2_request_input *input,
+ oauth2_request_callback_t *callback,
+ void *context,
+ pool_t p,
+ const char *method,
+ const char *url,
+ const string_t *payload,
+ bool add_auth_bearer)
+{
+ i_assert(oauth2_valid_token(input->token));
+
+ pool_t pool = (p == NULL) ?
+ pool_alloconly_create_clean("oauth2 request", 1024) : p;
+ struct oauth2_request *req =
+ p_new(pool, struct oauth2_request, 1);
+
+ req->pool = pool;
+ req->set = set;
+ req->req_callback = callback;
+ req->req_context = context;
+
+ req->req = http_client_request_url_str(req->set->client, method, url,
+ oauth2_request_response, req);
+
+ oauth2_request_set_headers(req, input);
+
+ if (payload != NULL && strcmp(method, "POST") == 0) {
+ struct istream *is = i_stream_create_from_string(payload);
+
+ http_client_request_add_header(
+ req->req, "Content-Type",
+ "application/x-www-form-urlencoded");
+
+ http_client_request_set_payload(req->req, is, FALSE);
+ i_stream_unref(&is);
+ }
+ if (add_auth_bearer &&
+ http_client_request_get_origin_url(req->req)->user == NULL &&
+ set->introspection_mode == INTROSPECTION_MODE_GET_AUTH) {
+ http_client_request_add_header(req->req,
+ "Authorization",
+ t_strdup_printf("Bearer %s",
+ input->token));
+ }
+ http_client_request_set_timeout_msecs(req->req,
+ req->set->timeout_msecs);
+ http_client_request_submit(req->req);
+
+ return req;
+}
+
+#undef oauth2_refresh_start
+struct oauth2_request *
+oauth2_refresh_start(const struct oauth2_settings *set,
+ const struct oauth2_request_input *input,
+ oauth2_request_callback_t *callback, void *context)
+{
+ string_t *payload = t_str_new(128);
+
+ str_append(payload, "client_secret=");
+ http_url_escape_param(payload, set->client_secret);
+ str_append(payload, "&grant_type=refresh_token&refresh_token=");
+ http_url_escape_param(payload, input->token);
+ str_append(payload, "&client_id=");
+ http_url_escape_param(payload, set->client_id);
+
+ return oauth2_request_start(set, input, callback, context, NULL,
+ "POST", set->refresh_url, NULL, FALSE);
+}
+
+#undef oauth2_introspection_start
+struct oauth2_request *
+oauth2_introspection_start(const struct oauth2_settings *set,
+ const struct oauth2_request_input *input,
+ oauth2_request_callback_t *callback, void *context)
+{
+ string_t *enc;
+ const char *url;
+ const char *method;
+ string_t *payload = NULL;
+ pool_t p = NULL;
+
+ switch (set->introspection_mode) {
+ case INTROSPECTION_MODE_GET:
+ enc = t_str_new(64);
+ str_append(enc, set->introspection_url);
+ http_url_escape_param(enc, input->token);
+ str_append(enc, "&client_id=");
+ http_url_escape_param(enc, set->client_id);
+ str_append(enc, "&client_secret=");
+ http_url_escape_param(enc, set->client_secret);
+ url = str_c(enc);
+ method = "GET";
+ break;
+ case INTROSPECTION_MODE_GET_AUTH:
+ url = set->introspection_url;
+ method = "GET";
+ break;
+ case INTROSPECTION_MODE_POST:
+ p = pool_alloconly_create_clean("oauth2 request", 1024);
+ payload = str_new(p, strlen(input->token)+6);
+ str_append(payload, "token=");
+ http_url_escape_param(payload, input->token);
+ str_append(payload, "&client_id=");
+ http_url_escape_param(payload, set->client_id);
+ str_append(payload, "&client_secret=");
+ http_url_escape_param(payload, set->client_secret);
+ url = set->introspection_url;
+ method = "POST";
+ break;
+ default:
+ i_unreached();
+ break;
+ }
+
+ return oauth2_request_start(set, input, callback, context, p,
+ method, url, payload, TRUE);
+}
+
+#undef oauth2_token_validation_start
+struct oauth2_request *
+oauth2_token_validation_start(const struct oauth2_settings *set,
+ const struct oauth2_request_input *input,
+ oauth2_request_callback_t *callback,
+ void *context)
+{
+ string_t *enc = t_str_new(64);
+
+ str_append(enc, set->tokeninfo_url);
+ http_url_escape_param(enc, input->token);
+
+ return oauth2_request_start(set, input, callback, context,
+ NULL, "GET", str_c(enc), NULL, TRUE);
+}
+
+#undef oauth2_passwd_grant_start
+struct oauth2_request *
+oauth2_passwd_grant_start(const struct oauth2_settings *set,
+ const struct oauth2_request_input *input,
+ const char *username, const char *password,
+ oauth2_request_callback_t *callback, void *context)
+{
+ pool_t pool = pool_alloconly_create_clean("oauth2 request", 1024);
+ string_t *payload = str_new(pool, 128);
+
+ /* add token */
+ str_append(payload, "grant_type=password&username=");
+ http_url_escape_param(payload, username);
+ str_append(payload, "&password=");
+ http_url_escape_param(payload, password);
+ str_append(payload, "&client_id=");
+ http_url_escape_param(payload, set->client_id);
+ str_append(payload, "&client_secret=");
+ http_url_escape_param(payload, set->client_secret);
+ if (set->scope[0] != '\0') {
+ str_append(payload, "&scope=");
+ http_url_escape_param(payload, set->scope);
+ }
+
+ return oauth2_request_start(set, input, callback, context,
+ pool, "POST", set->grant_url,
+ payload, FALSE);
+}
+
+void oauth2_request_abort(struct oauth2_request **_req)
+{
+ struct oauth2_request *req = *_req;
+ *_req = NULL;
+
+ http_client_request_abort(&req->req);
+ oauth2_request_free(req);
+}
diff --git a/src/lib-oauth2/oauth2.c b/src/lib-oauth2/oauth2.c
new file mode 100644
index 0000000..b96995e
--- /dev/null
+++ b/src/lib-oauth2/oauth2.c
@@ -0,0 +1,41 @@
+/* Copyright (c) 2017-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "buffer.h"
+#include "istream.h"
+#include "json-tree.h"
+#include "oauth2.h"
+#include "oauth2-private.h"
+
+int oauth2_json_tree_build(const buffer_t *json, struct json_tree **tree_r,
+ const char **error_r)
+{
+ struct istream *is = i_stream_create_from_buffer(json);
+ struct json_parser *parser = json_parser_init(is);
+ struct json_tree *tree = json_tree_init();
+ enum json_type type;
+ const char *value;
+ int ret;
+
+ while ((ret = json_parse_next(parser, &type, &value)) > 0) {
+ /* this is safe to reuse here because it gets rewritten in while
+ loop */
+ ret = json_tree_append(tree, type, value);
+ i_assert(ret == 0);
+ }
+ i_assert(ret != 0);
+ ret = json_parser_deinit(&parser, error_r);
+ i_stream_unref(&is);
+ if (ret != 0)
+ json_tree_deinit(&tree);
+ else
+ *tree_r = tree;
+ return ret;
+}
+
+bool oauth2_valid_token(const char *token)
+{
+ if (token == NULL || *token == '\0' || strpbrk(token, "\r\n") != NULL)
+ return FALSE;
+ return TRUE;
+}
diff --git a/src/lib-oauth2/oauth2.h b/src/lib-oauth2/oauth2.h
new file mode 100644
index 0000000..3d1d3ea
--- /dev/null
+++ b/src/lib-oauth2/oauth2.h
@@ -0,0 +1,149 @@
+#ifndef OAUTH2_H
+#define OAUTH2_H
+
+#include "net.h"
+
+struct dict;
+struct oauth2_request;
+struct oauth2_validation_key_cache;
+
+struct oauth2_field {
+ const char *name;
+ const char *value;
+};
+
+ARRAY_DEFINE_TYPE(oauth2_field, struct oauth2_field);
+
+struct oauth2_settings {
+ struct http_client *client;
+ /* GET tokeninfo from this URL, token is appended to URL
+ http://some.host/path?access_token= */
+ const char *tokeninfo_url;
+ /* POST grant password here, needs user credentials and client_*
+ settings */
+ const char *grant_url;
+ /* GET more information from this URL, uses Bearer authentication */
+ const char *introspection_url;
+ /* POST refresh here, needs refresh token and client_* settings */
+ const char *refresh_url;
+ /* client identificator for oauth2 server */
+ const char *client_id;
+ /* client secret for oauth2 server */
+ const char *client_secret;
+ /* access request scope for oauth2 server (optional) */
+ const char *scope;
+ /* key dict for looking up validation keys */
+ struct dict *key_dict;
+ /* cache for validation keys */
+ struct oauth2_validation_key_cache *key_cache;
+ /* valid issuer names */
+ const char *const *issuers;
+
+ enum {
+ INTROSPECTION_MODE_GET_AUTH,
+ INTROSPECTION_MODE_GET,
+ INTROSPECTION_MODE_POST,
+ INTROSPECTION_MODE_LOCAL,
+ } introspection_mode;
+ unsigned int timeout_msecs;
+ /* Should X-Dovecot-Auth-* headers be sent */
+ bool send_auth_headers;
+ /* Should use grant password mechanism for authentication */
+ bool use_grant_password;
+};
+
+
+struct oauth2_request_result {
+ /* Oauth2 server response fields */
+ ARRAY_TYPE(oauth2_field) *fields;
+ /* Non-NULL if there was an unexpected internal error. */
+ const char *error;
+ /* timestamp token expires at */
+ time_t expires_at;
+ /* User authenticated successfully. Implies that error==NULL. */
+ bool valid:1;
+};
+
+struct oauth2_request_input {
+ const char *token;
+ const char *service;
+ struct ip_addr local_ip, real_local_ip, remote_ip, real_remote_ip;
+ in_port_t local_port, real_local_port, remote_port, real_remote_port;
+};
+
+typedef void
+oauth2_request_callback_t(struct oauth2_request_result*, void*);
+
+bool oauth2_valid_token(const char *token);
+
+struct oauth2_request*
+oauth2_passwd_grant_start(const struct oauth2_settings *set,
+ const struct oauth2_request_input *input,
+ const char *username,
+ const char *password,
+ oauth2_request_callback_t *callback,
+ void *context);
+#define oauth2_passwd_grant_start(set, input, username, password, callback, \
+ context) \
+ oauth2_passwd_grant_start( \
+ set, input - CALLBACK_TYPECHECK( \
+ callback, void(*)(struct oauth2_request_result*, \
+ typeof(context))), \
+ username, password, \
+ (oauth2_request_callback_t*)callback, (void*)context);
+
+struct oauth2_request*
+oauth2_token_validation_start(const struct oauth2_settings *set,
+ const struct oauth2_request_input *input,
+ oauth2_request_callback_t *callback,
+ void *context);
+#define oauth2_token_validation_start(set, input, callback, context) \
+ oauth2_token_validation_start( \
+ set, input - CALLBACK_TYPECHECK( \
+ callback, void(*)(struct oauth2_request_result*, \
+ typeof(context))), \
+ (oauth2_request_callback_t*)callback, (void*)context);
+
+struct oauth2_request*
+oauth2_introspection_start(const struct oauth2_settings *set,
+ const struct oauth2_request_input *input,
+ oauth2_request_callback_t *callback,
+ void *context);
+#define oauth2_introspection_start(set, input, callback, context) \
+ oauth2_introspection_start( \
+ set, input - CALLBACK_TYPECHECK( \
+ callback, void(*)(struct oauth2_request_result*, \
+ typeof(context))), \
+ (oauth2_request_callback_t*)callback, (void*)context);
+
+struct oauth2_request *
+oauth2_refresh_start(const struct oauth2_settings *set,
+ const struct oauth2_request_input *input,
+ oauth2_request_callback_t *callback,
+ void *context);
+#define oauth2_refresh_start(set, input, callback, context) \
+ oauth2_refresh_start( \
+ set, input - CALLBACK_TYPECHECK( \
+ callback, void(*)(struct oauth2_request_result*, \
+ typeof(context))), \
+ (oauth2_request_callback_t*)callback, (void*)context);
+
+/* Abort without calling callback, use this to cancel the request */
+void oauth2_request_abort(struct oauth2_request **);
+
+int oauth2_try_parse_jwt(const struct oauth2_settings *set,
+ const char *token, ARRAY_TYPE(oauth2_field) *fields,
+ bool *is_jwt_r, const char **error_r);
+
+/* Initialize validation key cache */
+struct oauth2_validation_key_cache *oauth2_validation_key_cache_init(void);
+
+/* Evict given key ID from cache, returns 0 on successful eviction */
+int oauth2_validation_key_cache_evict(struct oauth2_validation_key_cache *cache,
+ const char *key_id);
+
+/* Deinitialize validation key cache */
+void oauth2_validation_key_cache_deinit(
+ struct oauth2_validation_key_cache **_cache);
+
+#endif
diff --git a/src/lib-oauth2/test-oauth2-json.c b/src/lib-oauth2/test-oauth2-json.c
new file mode 100644
index 0000000..87caedc
--- /dev/null
+++ b/src/lib-oauth2/test-oauth2-json.c
@@ -0,0 +1,104 @@
+/* Copyright (c) 2015-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "json-parser.h"
+#include "oauth2.h"
+#include "oauth2-private.h"
+#include "test-common.h"
+
+static void
+test_oauth_json_valid_parsed(struct oauth2_request *req ATTR_UNUSED,
+ const char *error)
+{
+ test_assert(error == NULL);
+}
+
+static void test_oauth2_json_valid(void)
+{
+ static const char *test_input =
+ "{\"access_token\":\"9a2dea3c-f8be-4271-b9c8-5b37da4f2f7e\","
+ "\"grant_type\":\"authorization_code\","
+ "\"openid\":\"\","
+ "\"scope\":[\"openid\",\"profile\",\"email\"],"
+ "\"profile\":\"\","
+ "\"realm\":\"/employees\","
+ "\"token_type\":\"Bearer\","
+ "\"expires_in\":2377,"
+ "\"client_id\":\"mosaic\","
+ "\"email\":\"\","
+ "\"extensions\":"
+ "{\"algorithm\":\"cuttlefish\","
+ "\"tentacles\":8"
+ "}"
+ "}";
+ static const struct oauth2_field fields[] = {
+ { .name = "access_token",
+ .value = "9a2dea3c-f8be-4271-b9c8-5b37da4f2f7e" },
+ { .name = "grant_type",
+ .value = "authorization_code" },
+ { .name = "openid",
+ .value = "" },
+ { .name = "profile",
+ .value = "" },
+ { .name = "realm",
+ .value = "/employees" },
+ { .name = "token_type",
+ .value = "Bearer" },
+ { .name = "expires_in",
+ .value = "2377" },
+ { .name = "client_id",
+ .value = "mosaic" },
+ { .name = "email",
+ .value = "" },
+ };
+ static const unsigned int fields_count = N_ELEMENTS(fields);
+ struct oauth2_request *req;
+ const struct oauth2_field *pfields;
+ unsigned int count, i;
+ pool_t pool;
+ size_t pos;
+
+ test_begin("oauth json skip");
+
+ /* Create mock request */
+ pool = pool_alloconly_create_clean("oauth2 json test", 1024);
+ req = p_new(pool, struct oauth2_request, 1);
+ req->pool = pool;
+ p_array_init(&req->fields, req->pool, 1);
+ req->is = test_istream_create_data(test_input, strlen(test_input));
+ req->parser = json_parser_init(req->is);
+ req->json_parsed_cb = test_oauth_json_valid_parsed;
+
+ /* Parse the JSON response */
+ for (pos = 0; pos <= strlen(test_input); pos +=2) {
+ test_istream_set_size(req->is, pos);
+ oauth2_request_parse_json(req);
+ if (req->is == NULL)
+ break;
+ }
+
+ /* Verify the parsed fields */
+ pfields = array_get(&req->fields, &count);
+ test_assert(count == fields_count);
+ if (count > fields_count)
+ count = fields_count;
+ for (i = 0; i < count; i++) {
+ test_assert(strcmp(pfields[i].name, fields[i].name) == 0);
+ test_assert(strcmp(pfields[i].value, fields[i].value) == 0);
+ }
+
+ /* Clean up */
+ pool_unref(&req->pool);
+
+ test_end();
+}
+
+int main(void)
+{
+ static void (*const test_functions[])(void) = {
+ test_oauth2_json_valid,
+ NULL
+ };
+ return test_run(test_functions);
+}
diff --git a/src/lib-oauth2/test-oauth2-jwt.c b/src/lib-oauth2/test-oauth2-jwt.c
new file mode 100644
index 0000000..890712e
--- /dev/null
+++ b/src/lib-oauth2/test-oauth2-jwt.c
@@ -0,0 +1,919 @@
+/* Copyright (c) 2020 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "buffer.h"
+#include "str.h"
+#include "ostream.h"
+#include "hmac.h"
+#include "sha2.h"
+#include "base64.h"
+#include "randgen.h"
+#include "array.h"
+#include "json-parser.h"
+#include "iso8601-date.h"
+#include "oauth2.h"
+#include "oauth2-private.h"
+#include "dcrypt.h"
+#include "dict.h"
+#include "dict-private.h"
+#include "test-common.h"
+#include "unlink-directory.h"
+
+#include <sys/stat.h>
+#include <sys/types.h>
+
+#define base64url_encode_str(str, dest) \
+ base64url_encode(BASE64_ENCODE_FLAG_NO_PADDING, SIZE_MAX, (str), \
+ strlen((str)), (dest))
+
+/**
+ * Test keypair used only for this test.
+ */
+static const char *rsa_public_key =
+"-----BEGIN PUBLIC KEY-----\n"
+"MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAnzyis1ZjfNB0bBgKFMSv\n"
+"vkTtwlvBsaJq7S5wA+kzeVOVpVWwkWdVha4s38XM/pa/yr47av7+z3VTmvDRyAHc\n"
+"aT92whREFpLv9cj5lTeJSibyr/Mrm/YtjCZVWgaOYIhwrXwKLqPr/11inWsAkfIy\n"
+"tvHWTxZYEcXLgAXFuUuaS3uF9gEiNQwzGTU1v0FqkqTBr4B8nW3HCN47XUu0t8Y0\n"
+"e+lf4s4OxQawWD79J9/5d3Ry0vbV3Am1FtGJiJvOwRsIfVChDpYStTcHTCMqtvWb\n"
+"V6L11BWkpzGXSW4Hv43qa+GSYOD2QU68Mb59oSk2OB+BtOLpJofmbGEGgvmwyCI9\n"
+"MwIDAQAB\n"
+"-----END PUBLIC KEY-----";
+static const char *rsa_private_key =
+"-----BEGIN PRIVATE KEY-----\n"
+"MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCfPKKzVmN80HRs\n"
+"GAoUxK++RO3CW8GxomrtLnAD6TN5U5WlVbCRZ1WFrizfxcz+lr/Kvjtq/v7PdVOa\n"
+"8NHIAdxpP3bCFEQWku/1yPmVN4lKJvKv8yub9i2MJlVaBo5giHCtfAouo+v/XWKd\n"
+"awCR8jK28dZPFlgRxcuABcW5S5pLe4X2ASI1DDMZNTW/QWqSpMGvgHydbccI3jtd\n"
+"S7S3xjR76V/izg7FBrBYPv0n3/l3dHLS9tXcCbUW0YmIm87BGwh9UKEOlhK1NwdM\n"
+"Iyq29ZtXovXUFaSnMZdJbge/jepr4ZJg4PZBTrwxvn2hKTY4H4G04ukmh+ZsYQaC\n"
+"+bDIIj0zAgMBAAECggEAKIBGrbCSW2O1yOyQW9nvDUkA5EdsS58Q7US7bvM4iWpu\n"
+"DIBwCXur7/VuKnhn/HUhURLzj/JNozynSChqYyG+CvL+ZLy82LUE3ZIBkSdv/vFL\n"
+"Ft+VvvRtf1EcsmoqenkZl7aN7HD7DJeXBoz5tyVQKuH17WW0fsi9StGtCcUl+H6K\n"
+"zV9Gif0Kj0uLQbCg3THRvKuueBTwCTdjoP0PwaNADgSWb3hJPeLMm/yII4tIMGbO\n"
+"w+xd9wJRl+ZN9nkNtQMxszFGdKjedB6goYLQuP0WRZx+YtykaVJdM75bDUvsQar4\n"
+"9Pc21Fp7UVk/CN11DX/hX3TmTJAUtqYADliVKkTbCQKBgQDLU48tBxm3g1CdDM/P\n"
+"ZIEmpA3Y/m7e9eX7M1Uo/zDh4G/S9a4kkX6GQY2dLFdCtOS8M4hR11Io7MceBKDi\n"
+"djorTZ5zJPQ8+b9Rm+1GlaucGNwRW0cQk2ltT2ksPmJnQn2xvM9T8vE+a4A/YGzw\n"
+"mZOfpoVGykWs/tbSzU2aTaOybQKBgQDIfRf6OmirGPh59l+RSuDkZtISF/51mCV/\n"
+"S1M4DltWDwhjC2Y2T+meIsb/Mjtz4aVNz0EHB8yvn0TMGr94Uwjv4uBdpVSwz+xL\n"
+"hHL7J4rpInH+i0gxa0N+rGwsPwI8wJG95wLY+Kni5KCuXQw55uX1cqnnsahpRZFZ\n"
+"EerBXhjqHwKBgBmEjiaHipm2eEqNjhMoOPFBi59dJ0sCL2/cXGa9yEPA6Cfgv49F\n"
+"V0zAM2azZuwvSbm4+fXTgTMzrDW/PPXPArPmlOk8jQ6OBY3XdOrz48q+b/gZrYyO\n"
+"A6A9ZCSyW6U7+gxxds/BYLeFxF2v21xC2f0iZ/2faykv/oQMUh34en/tAoGACqVZ\n"
+"2JexZyR0TUWf3X80YexzyzIq+OOTWicNzDQ29WLm9xtr2gZ0SUlfd72bGpQoyvDu\n"
+"awkm/UxfwtbIxALkvpg1gcN9s8XWrkviLyPyZF7H3tRWiQlBFEDjnZXa8I7pLkRO\n"
+"Cmdp3fp17cxTEeAI5feovfzZDH39MdWZuZrdh9ECgYBTEv8S7nK8wrxIC390kroV\n"
+"52eBwzckQU2mWa0thUtaGQiU1EYPCSDcjkrLXwB72ft0dW57KyWtvrB6rt1ORgOL\n"
+"eI5hFbwdGQhCHTrAR1vG3SyFPMAm+8JB+sGOD/fvjtZKx//MFNweKFNEF0C/o6Z2\n"
+"FXj90PlgF8sCQut36ZfuIQ==\n"
+"-----END PRIVATE KEY-----";
+
+static buffer_t *hs_sign_key = NULL;
+
+static struct dict *keys_dict = NULL;
+
+static bool skip_dcrypt = FALSE;
+
+static struct oauth2_validation_key_cache *key_cache = NULL;
+
+static int parse_jwt_token(struct oauth2_request *req, const char *token,
+ bool *is_jwt_r, const char **error_r)
+{
+ struct oauth2_settings set;
+
+ i_zero(&set);
+ set.scope = "mail";
+ set.key_dict = keys_dict;
+ set.key_cache = key_cache;
+ i_zero(req);
+ req->pool = pool_datastack_create();
+ req->set = &set;
+ t_array_init(&req->fields, 8);
+ return oauth2_try_parse_jwt(&set, token, &req->fields, is_jwt_r,
+ error_r);
+}
+
+static void test_jwt_token(const char *token)
+{
+ /* then see what the parser likes it */
+ struct oauth2_request req;
+ const char *error = NULL;
+
+ bool is_jwt;
+ test_assert(parse_jwt_token(&req, token, &is_jwt, &error) == 0);
+ test_assert(is_jwt == TRUE);
+ test_assert(error == NULL);
+
+ /* check fields */
+ test_assert(array_is_created(&req.fields));
+ if (array_is_created(&req.fields)) {
+ const struct oauth2_field *field;
+ bool got_sub = FALSE;
+ array_foreach(&req.fields, field) {
+ if (strcmp(field->name, "sub") == 0) {
+ test_assert_strcmp(field->value, "testuser");
+ got_sub = TRUE;
+ }
+ }
+ test_assert(got_sub == TRUE);
+ }
+
+ if (error != NULL)
+ i_error("%s", error);
+}
+
+static buffer_t *create_jwt_token_kid(const char *algo, const char *kid)
+{
+ /* make a token */
+ buffer_t *tokenbuf = t_buffer_create(64);
+
+ /* header */
+ base64url_encode_str(
+ t_strdup_printf(
+ "{\"alg\":\"%s\",\"typ\":\"JWT\",\"kid\":\"%s\"}",
+ algo, kid),
+ tokenbuf);
+ buffer_append(tokenbuf, ".", 1);
+
+ /* body */
+ base64url_encode_str(
+ t_strdup_printf("{\"sub\":\"testuser\","\
+ "\"iat\":%"PRIdTIME_T","
+ "\"exp\":%"PRIdTIME_T"}",
+ time(NULL), time(NULL)+600),
+ tokenbuf);
+ return tokenbuf;
+}
+
+static buffer_t *create_jwt_token(const char *algo)
+{
+ /* make a token */
+ buffer_t *tokenbuf = t_buffer_create(64);
+
+ /* header */
+ base64url_encode_str(
+ t_strdup_printf("{\"alg\":\"%s\",\"typ\":\"JWT\"}", algo),
+ tokenbuf);
+ buffer_append(tokenbuf, ".", 1);
+
+ /* body */
+ base64url_encode_str(
+ t_strdup_printf("{\"sub\":\"testuser\","\
+ "\"iat\":%"PRIdTIME_T","
+ "\"exp\":%"PRIdTIME_T"}",
+ time(NULL), time(NULL)+600),
+ tokenbuf);
+ return tokenbuf;
+}
+
+static void
+append_key_value(string_t *dest, const char *key, const char *value, bool str)
+{
+ str_append_c(dest, '"');
+ json_append_escaped(dest, key);
+ str_append(dest, "\":");
+ if (str)
+ str_append_c(dest, '"');
+ json_append_escaped(dest, value);
+ if (str)
+ str_append_c(dest, '"');
+
+}
+
+#define create_jwt_token_fields(algo, exp, iat, nbf, fields) \
+ create_jwt_token_fields_kid(algo, "default", exp, iat, nbf, fields)
+static buffer_t *
+create_jwt_token_fields_kid(const char *algo, const char *kid, time_t exp, time_t iat,
+ time_t nbf, ARRAY_TYPE(oauth2_field) *fields)
+{
+ const struct oauth2_field *field;
+ buffer_t *tokenbuf = t_buffer_create(64);
+ string_t *hdr = t_str_new(32);
+ str_printfa(hdr, "{\"alg\":\"%s\",\"typ\":\"JWT\"", algo);
+ if (kid != NULL && *kid != '\0') {
+ str_append(hdr, ",\"kid\":\"");
+ json_append_escaped(hdr, kid);
+ str_append_c(hdr, '"');
+ }
+ str_append(hdr, "}");
+ base64url_encode_str(str_c(hdr), tokenbuf);
+ buffer_append(tokenbuf, ".", 1);
+
+ string_t *bodybuf = t_str_new(64);
+ str_append_c(bodybuf, '{');
+ if (exp > 0) {
+ append_key_value(bodybuf, "exp", dec2str(exp), FALSE);
+ }
+ if (iat > 0) {
+ if (exp > 0)
+ str_append_c(bodybuf, ',');
+ append_key_value(bodybuf, "iat", dec2str(iat), FALSE);
+ }
+ if (nbf > 0) {
+ if (exp > 0 || iat > 0)
+ str_append_c(bodybuf, ',');
+ append_key_value(bodybuf, "nbf", dec2str(nbf), FALSE);
+ }
+ array_foreach(fields, field) {
+ if (str_data(bodybuf)[bodybuf->used-1] != '{')
+ str_append_c(bodybuf, ',');
+ append_key_value(bodybuf, field->name, field->value, TRUE);
+ }
+ str_append_c(bodybuf, '}');
+ base64url_encode_str(str_c(bodybuf), tokenbuf);
+
+ return tokenbuf;
+}
+
+#define save_key(algo, key) save_key_to(algo, "default", (key))
+#define save_key_to(algo, name, key) save_key_azp_to(algo, "default", name, (key))
+static void save_key_azp_to(const char *algo, const char *azp,
+ const char *name, const char *keydata)
+{
+ const char *error;
+ struct dict_op_settings set = {
+ .username = "testuser",
+ };
+ struct dict_transaction_context *ctx =
+ dict_transaction_begin(keys_dict, &set);
+ algo = t_str_ucase(algo);
+ dict_set(ctx, t_strconcat(DICT_PATH_SHARED, azp, "/", algo, "/",
+ name, NULL),
+ keydata);
+ if (dict_transaction_commit(&ctx, &error) < 0)
+ i_error("dict_set(%s) failed: %s", name, error);
+}
+
+static void sign_jwt_token_hs256(buffer_t *tokenbuf, buffer_t *key)
+{
+ i_assert(key != NULL);
+ buffer_t *sig = t_hmac_buffer(&hash_method_sha256, key->data, key->used,
+ tokenbuf);
+ buffer_append(tokenbuf, ".", 1);
+ base64url_encode(BASE64_ENCODE_FLAG_NO_PADDING, SIZE_MAX,
+ sig->data, sig->used, tokenbuf);
+}
+
+static void sign_jwt_token_hs384(buffer_t *tokenbuf, buffer_t *key)
+{
+ i_assert(key != NULL);
+ buffer_t *sig = t_hmac_buffer(&hash_method_sha384, key->data, key->used,
+ tokenbuf);
+ buffer_append(tokenbuf, ".", 1);
+ base64url_encode(BASE64_ENCODE_FLAG_NO_PADDING, SIZE_MAX,
+ sig->data, sig->used, tokenbuf);
+}
+
+static void sign_jwt_token_hs512(buffer_t *tokenbuf, buffer_t *key)
+{
+ i_assert(key != NULL);
+ buffer_t *sig = t_hmac_buffer(&hash_method_sha512, key->data, key->used,
+ tokenbuf);
+ buffer_append(tokenbuf, ".", 1);
+ base64url_encode(BASE64_ENCODE_FLAG_NO_PADDING, SIZE_MAX,
+ sig->data, sig->used, tokenbuf);
+}
+
+static void test_jwt_hs_token(void)
+{
+ test_begin("JWT HMAC token");
+
+ buffer_t *sign_key_384 = t_buffer_create(384/8);
+ void *ptr = buffer_append_space_unsafe(sign_key_384, 384/8);
+ random_fill(ptr, 384/8);
+ buffer_t *b64_key = t_base64_encode(0, SIZE_MAX,
+ sign_key_384->data,
+ sign_key_384->used);
+ save_key_to("HS384", "default", str_c(b64_key));
+ buffer_t *sign_key_512 = t_buffer_create(512/8);
+ ptr = buffer_append_space_unsafe(sign_key_512, 512/8);
+ random_fill(ptr, 512/8);
+ b64_key = t_base64_encode(0, SIZE_MAX,
+ sign_key_512->data,
+ sign_key_512->used);
+ save_key_to("HS512", "default", str_c(b64_key));
+ /* make a token */
+ buffer_t *tokenbuf = create_jwt_token("HS256");
+ /* sign it */
+ sign_jwt_token_hs256(tokenbuf, hs_sign_key);
+ test_jwt_token(str_c(tokenbuf));
+
+ tokenbuf = create_jwt_token("HS384");
+ sign_jwt_token_hs384(tokenbuf, sign_key_384);
+ test_jwt_token(str_c(tokenbuf));
+
+ tokenbuf = create_jwt_token("HS512");
+ sign_jwt_token_hs512(tokenbuf, sign_key_512);
+ test_jwt_token(str_c(tokenbuf));
+
+ test_end();
+}
+
+static void test_jwt_token_escape(void)
+{
+ struct test_case {
+ const char *azp;
+ const char *alg;
+ const char *kid;
+ const char *esc_azp;
+ const char *esc_kid;
+ } test_cases[] = {
+ { "", "hs256", "", "default", "default" },
+ { "", "hs256", "test", "default", "test" },
+ { "test", "hs256", "test", "test", "test" },
+ {
+ "http://test.unit/local%key",
+ "hs256",
+ "http://test.unit/local%key",
+ "http:%2f%2ftest.unit%2flocal%25key",
+ "http:%2f%2ftest.unit%2flocal%25key"
+ },
+ { "../", "hs256", "../", "..%2f", "..%2f" },
+ };
+
+ test_begin("JWT token escaping");
+
+ buffer_t *b64_key =
+ t_base64_encode(0, SIZE_MAX, hs_sign_key->data, hs_sign_key->used);
+ ARRAY_TYPE(oauth2_field) fields;
+ t_array_init(&fields, 8);
+
+ for (size_t i = 0; i < N_ELEMENTS(test_cases); i++) {
+ const struct test_case *test_case = &test_cases[i];
+ array_clear(&fields);
+ struct oauth2_field *field = array_append_space(&fields);
+ field->name = "sub";
+ field->value = "testuser";
+ if (*test_case->azp != '\0') {
+ field = array_append_space(&fields);
+ field->name = "azp";
+ field->value = test_case->azp;
+ }
+ if (*test_case->kid != '\0') {
+ field = array_append_space(&fields);
+ field->name = "kid";
+ field->value = test_case->kid;
+ }
+ save_key_azp_to(test_case->alg, test_case->esc_azp, test_case->esc_kid,
+ str_c(b64_key));
+ buffer_t *token = create_jwt_token_fields_kid(test_case->alg,
+ test_case->kid,
+ time(NULL)+500,
+ time(NULL)-500,
+ 0, &fields);
+ sign_jwt_token_hs256(token, hs_sign_key);
+ test_jwt_token(str_c(token));
+ }
+
+ test_end();
+}
+
+static void test_jwt_broken_token(void)
+{
+ struct test_cases {
+ const char *token;
+ bool is_jwt;
+ } test_cases[] = {
+ { /* empty token */
+ .token = "",
+ .is_jwt = FALSE
+ },
+ { /* not base64 */
+ .token = "{\"alg\":\"HS256\":\"typ\":\"JWT\"}",
+ .is_jwt = FALSE
+ },
+ { /* not jwt */
+ .token = "aGVsbG8sIHdvcmxkCg",
+ .is_jwt = FALSE
+ },
+ { /* no alg field */
+ .token = "eyJ0eXAiOiAiSldUIn0",
+ .is_jwt = FALSE
+ },
+ { /* no typ field */
+ .token = "eyJhbGciOiAiSFMyNTYifQ",
+ .is_jwt = FALSE
+ },
+ { /* typ field is wrong */
+ .token = "eyJ0eXAiOiAiand0IiwgImFsZyI6ICJIUzI1NiJ9."
+ "eyJhbGdvIjogIldURiIsICJ0eXAiOiAiSldUIn0."
+ "q2wwwWWJVJxqw-J3uQ0DdlIyWfoZ7Z0QrdzvMW_B-jo",
+ .is_jwt = FALSE
+ },
+ { /* unknown algorithm */
+ .token = "eyJ0eXAiOiAiSldUIiwgImFsZyI6ICJXVEYifQ."
+ "eyJhbGdvIjogIldURiIsICJ0eXAiOiAiSldUIn0."
+ "q2wwwWWJVJxqw-J3uQ0DdlIyWfoZ7Z0QrdzvMW_B-jo",
+ .is_jwt = TRUE
+ },
+ { /* truncated base64 */
+ .token = "yJhbGciOiJIUzI1NiIsInR5",
+ .is_jwt = FALSE
+ },
+ { /* missing body and signature */
+ .token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9",
+ .is_jwt = FALSE
+ },
+ { /* empty body and signature */
+ .token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..",
+ .is_jwt = TRUE
+ },
+ { /* empty signature */
+ .token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9."
+ "eyJleHAiOjE1ODEzMzA3OTN9.",
+ .is_jwt = TRUE
+ },
+ { /* bad signature */
+ .token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9."
+ "eyJleHAiOjE1ODEzMzA3OTN9."
+ "q2wwwWWJVJxqw-J3uQ0DdlIyWfoZ7Z0QrdzvMW_B-jo",
+ .is_jwt = TRUE
+ },
+ };
+
+ test_begin("JWT broken tokens");
+
+ for (size_t i = 0; i < N_ELEMENTS(test_cases); i++) T_BEGIN {
+ struct test_cases *test_case = &test_cases[i];
+ struct oauth2_request req;
+ const char *error = NULL;
+ bool is_jwt;
+ test_assert_idx(parse_jwt_token(&req, test_case->token,
+ &is_jwt, &error) != 0, i);
+ test_assert_idx(test_case->is_jwt == is_jwt, i);
+ test_assert_idx(error != NULL, i);
+ } T_END;
+
+ test_end();
+}
+
+static void test_jwt_bad_valid_token(void)
+{
+ test_begin("JWT bad token tests");
+ time_t now = time(NULL);
+
+ struct test_cases {
+ time_t exp;
+ time_t iat;
+ time_t nbf;
+ const char *key_values[20];
+ const char *error;
+ } test_cases[] =
+ {
+ { /* "empty" token */
+ .exp = 0,
+ .iat = 0,
+ .nbf = 0,
+ .key_values = { NULL },
+ .error = "Missing 'sub' field",
+ },
+ { /* missing sub field */
+ .exp = now+500,
+ .iat = 0,
+ .nbf = 0,
+ .key_values = { NULL },
+ .error = "Missing 'sub' field",
+ },
+ { /* no expiration */
+ .key_values = {
+ "sub", "testuser",
+ NULL
+ },
+ .error = "Missing 'exp' field",
+ },
+ { /* non-ISO date as iat */
+ .exp = now+500,
+ .iat = 0,
+ .nbf = 0,
+ .key_values = { "sub", "testuser", "iat",
+ "1.1.2019 16:00", NULL },
+ .error = "Malformed 'iat' field"
+ },
+ { /* expired token */
+ .exp = now-500,
+ .iat = 0,
+ .nbf = 0,
+ .key_values = { "sub", "testuser", NULL },
+ .error = "Token has expired",
+ },
+ { /* future token */
+ .exp = now+1000,
+ .iat = now+500,
+ .nbf = 0,
+ .key_values = { "sub", "testuser", NULL },
+ .error = "Token is issued in future",
+ },
+ { /* token not valid yet */
+ .exp = now+500,
+ .iat = now,
+ .nbf = now+250,
+ .key_values = { "sub", "testuser", NULL },
+ .error = "Token is not valid yet",
+ },
+ };
+
+ for (size_t i = 0; i < N_ELEMENTS(test_cases); i++) T_BEGIN {
+ const struct test_cases *test_case = &test_cases[i];
+ const char *key = NULL;
+ ARRAY_TYPE(oauth2_field) fields;
+
+ t_array_init(&fields, 8);
+ for (const char *const *value = test_case->key_values;
+ *value != NULL; value++) {
+ if (key == NULL) {
+ key = *value;
+ } else {
+ struct oauth2_field *field =
+ array_append_space(&fields);
+ field->name = key;
+ field->value = *value;
+ key = NULL;
+ }
+ }
+
+ buffer_t *tokenbuf =
+ create_jwt_token_fields("HS256", test_case->exp,
+ test_case->iat, test_case->nbf,
+ &fields);
+ sign_jwt_token_hs256(tokenbuf, hs_sign_key);
+
+ struct oauth2_request req;
+ const char *error = NULL;
+ bool is_jwt;
+
+ test_assert_idx(parse_jwt_token(&req, str_c(tokenbuf),
+ &is_jwt, &error) != 0, i);
+ test_assert_idx(is_jwt == TRUE, i);
+ if (test_case->error != NULL) {
+ test_assert_strcmp(test_case->error, error);
+ }
+ test_assert(error != NULL);
+ } T_END;
+
+ test_end();
+}
+
+static void test_jwt_valid_token(void)
+{
+ test_begin("JWT valid token tests");
+ time_t now = time(NULL);
+
+ struct test_cases {
+ time_t exp;
+ time_t iat;
+ time_t nbf;
+ const char *key_values[20];
+ } test_cases[] = {
+ { /* valid token */
+ .exp = now + 500,
+ .key_values = {
+ "sub", "testuser",
+ NULL
+ },
+ },
+ {
+ .exp = now + 500,
+ .nbf = now - 500,
+ .iat = now - 250,
+ .key_values = {
+ "sub", "testuser",
+ NULL
+ },
+ },
+ { /* token issued in advance */
+ .exp = now + 500,
+ .nbf = now - 500,
+ .iat = now - 3600,
+ .key_values = {
+ "sub", "testuser",
+ NULL,
+ },
+ },
+ };
+
+ for (size_t i = 0; i < N_ELEMENTS(test_cases); i++) T_BEGIN {
+ const struct test_cases *test_case = &test_cases[i];
+ ARRAY_TYPE(oauth2_field) fields;
+
+ t_array_init(&fields, 8);
+ for (unsigned int i = 0; test_case->key_values[i] != NULL; i += 2) {
+ struct oauth2_field *field = array_append_space(&fields);
+ field->name = test_case->key_values[i];
+ field->value = test_case->key_values[i+1];
+ }
+
+ buffer_t *tokenbuf =
+ create_jwt_token_fields("HS256", test_case->exp,
+ test_case->iat, test_case->nbf,
+ &fields);
+ sign_jwt_token_hs256(tokenbuf, hs_sign_key);
+
+ struct oauth2_request req;
+ const char *error = NULL;
+ bool is_jwt;
+
+ test_assert_idx(parse_jwt_token(&req, str_c(tokenbuf),
+ &is_jwt, &error) == 0, i);
+ test_assert_idx(is_jwt == TRUE, i);
+ test_assert_idx(error == NULL, i);
+ if (error != NULL)
+ i_error("JWT validation error: %s", error);
+ } T_END;
+
+ test_end();
+}
+
+static void test_jwt_dates(void)
+{
+ test_begin("JWT Token dates");
+
+ /* simple check to make sure ISO8601 dates work too */
+ ARRAY_TYPE(oauth2_field) fields;
+ t_array_init(&fields, 8);
+ struct oauth2_field *field;
+ struct tm tm_b;
+ struct tm *tm;
+ time_t now = time(NULL);
+ time_t exp = now+500;
+ time_t nbf = now-250;
+ time_t iat = now-500;
+
+ field = array_append_space(&fields);
+ field->name = "sub";
+ field->value = "testuser";
+ field = array_append_space(&fields);
+ field->name = "exp";
+ tm = gmtime_r(&exp, &tm_b);
+ field->value = iso8601_date_create_tm(tm, INT_MAX);
+ field = array_append_space(&fields);
+ field->name = "nbf";
+ tm = gmtime_r(&nbf, &tm_b);
+ field->value = iso8601_date_create_tm(tm, INT_MAX);
+ field = array_append_space(&fields);
+ field->name = "iat";
+ tm = gmtime_r(&iat, &tm_b);
+ field->value = iso8601_date_create_tm(tm, INT_MAX);
+ buffer_t *tokenbuf = create_jwt_token_fields("HS256", 0, 0, 0, &fields);
+ sign_jwt_token_hs256(tokenbuf, hs_sign_key);
+ test_jwt_token(str_c(tokenbuf));
+
+ str_truncate(tokenbuf, 0);
+ base64url_encode_str("{\"alg\":\"HS256\",\"typ\":\"JWT\"}", tokenbuf);
+ str_append_c(tokenbuf, '.');
+ base64url_encode_str(t_strdup_printf("{\"sub\":\"testuser\","
+ "\"exp\":%"PRIdTIME_T","
+ "\"nbf\":0,\"iat\":%"PRIdTIME_T"}",
+ exp, iat),
+ tokenbuf);
+ sign_jwt_token_hs256(tokenbuf, hs_sign_key);
+ test_jwt_token(str_c(tokenbuf));
+
+ test_end();
+}
+
+static void test_jwt_key_files(void)
+{
+ test_begin("JWT key id");
+ /* write HMAC secrets */
+ struct oauth2_request req;
+ bool is_jwt;
+ const char *error = NULL;
+
+ buffer_t *secret = t_buffer_create(32);
+ void *ptr = buffer_append_space_unsafe(secret, 32);
+ random_fill(ptr, 32);
+ buffer_t *b64_key = t_base64_encode(0, SIZE_MAX,
+ secret->data, secret->used);
+ save_key_to("HS256", "first", str_c(b64_key));
+ buffer_t *secret2 = t_buffer_create(32);
+ ptr = buffer_append_space_unsafe(secret2, 32);
+ random_fill(ptr, 32);
+ b64_key = t_base64_encode(0, SIZE_MAX, secret2->data, secret2->used);
+ save_key_to("HS256", "second", str_c(b64_key));
+
+ /* create and sign token */
+ buffer_t *token_1 = create_jwt_token_kid("HS256", "first");
+ buffer_t *token_2 = create_jwt_token_kid("HS256", "second");
+ buffer_t *token_3 = create_jwt_token_kid("HS256", "missing");
+ buffer_t *token_4 = create_jwt_token_kid("HS256", "");
+
+ sign_jwt_token_hs256(token_1, secret);
+ sign_jwt_token_hs256(token_2, secret2);
+ sign_jwt_token_hs256(token_3, secret);
+ sign_jwt_token_hs256(token_4, secret);
+
+ test_jwt_token(str_c(token_1));
+ test_jwt_token(str_c(token_2));
+
+ test_assert(parse_jwt_token(&req, str_c(token_3), &is_jwt, &error) != 0);
+ test_assert(is_jwt == TRUE);
+ test_assert_strcmp(error, "HS256 key 'missing' not found");
+ test_assert(parse_jwt_token(&req, str_c(token_4), &is_jwt, &error) != 0);
+ test_assert(is_jwt == TRUE);
+ test_assert_strcmp(error, "'kid' field is empty");
+
+ test_end();
+}
+
+static void test_jwt_kid_escape(void)
+{
+ test_begin("JWT kid escape");
+ /* save a token */
+ buffer_t *secret = t_buffer_create(32);
+ void *ptr = buffer_append_space_unsafe(secret, 32);
+ random_fill(ptr, 32);
+ buffer_t *b64_key = t_base64_encode(0, SIZE_MAX,
+ secret->data, secret->used);
+ save_key_to("HS256", "hello.world%2f%25", str_c(b64_key));
+ /* make a token */
+ buffer_t *tokenbuf = create_jwt_token_kid("HS256", "hello.world/%");
+ /* sign it */
+ sign_jwt_token_hs256(tokenbuf, secret);
+ test_jwt_token(str_c(tokenbuf));
+ test_end();
+}
+
+static void test_jwt_rs_token(void)
+{
+ const char *error;
+
+ if (skip_dcrypt)
+ return;
+
+ test_begin("JWT RSA token");
+ /* write public key to file */
+ oauth2_validation_key_cache_evict(key_cache, "default");
+ save_key("RS256", rsa_public_key);
+
+ buffer_t *tokenbuf = create_jwt_token("RS256");
+
+ /* sign token */
+ buffer_t *sig = t_buffer_create(64);
+ struct dcrypt_private_key *key;
+ if (!dcrypt_key_load_private(&key, rsa_private_key, NULL, NULL,
+ &error) ||
+ !dcrypt_sign(key, "sha256", DCRYPT_SIGNATURE_FORMAT_DSS,
+ tokenbuf->data, tokenbuf->used, sig,
+ DCRYPT_PADDING_RSA_PKCS1, &error)) {
+ i_error("dcrypt signing failed: %s", error);
+ lib_exit(1);
+ }
+ dcrypt_key_unref_private(&key);
+
+ /* convert to base64 */
+ buffer_append(tokenbuf, ".", 1);
+ base64url_encode(BASE64_ENCODE_FLAG_NO_PADDING, SIZE_MAX,
+ sig->data, sig->used, tokenbuf);
+
+ test_jwt_token(str_c(tokenbuf));
+
+ test_end();
+}
+
+static void test_jwt_ps_token(void)
+{
+ const char *error;
+
+ if (skip_dcrypt)
+ return;
+
+ test_begin("JWT RSAPSS token");
+ /* write public key to file */
+ oauth2_validation_key_cache_evict(key_cache, "default");
+ save_key("PS256", rsa_public_key);
+
+ buffer_t *tokenbuf = create_jwt_token("PS256");
+
+ /* sign token */
+ buffer_t *sig = t_buffer_create(64);
+ struct dcrypt_private_key *key;
+ if (!dcrypt_key_load_private(&key, rsa_private_key, NULL, NULL,
+ &error) ||
+ !dcrypt_sign(key, "sha256", DCRYPT_SIGNATURE_FORMAT_DSS,
+ tokenbuf->data, tokenbuf->used, sig,
+ DCRYPT_PADDING_RSA_PKCS1_PSS, &error)) {
+ i_error("dcrypt signing failed: %s", error);
+ lib_exit(1);
+ }
+ dcrypt_key_unref_private(&key);
+
+ /* convert to base64 */
+ buffer_append(tokenbuf, ".", 1);
+ base64url_encode(BASE64_ENCODE_FLAG_NO_PADDING, SIZE_MAX,
+ sig->data, sig->used, tokenbuf);
+
+ test_jwt_token(str_c(tokenbuf));
+
+ test_end();
+}
+
+static void test_jwt_ec_token(void)
+{
+ const char *error;
+
+ if (skip_dcrypt)
+ return;
+
+ test_begin("JWT ECDSA token");
+ struct dcrypt_keypair pair;
+ i_zero(&pair);
+ if (!dcrypt_keypair_generate(&pair, DCRYPT_KEY_EC, 0,
+ "prime256v1", &error)) {
+ i_error("dcrypt keypair generate failed: %s", error);
+ lib_exit(1);
+ }
+ /* export public key */
+ buffer_t *keybuf = t_buffer_create(256);
+ if (!dcrypt_key_store_public(pair.pub, DCRYPT_FORMAT_PEM, keybuf,
+ &error)) {
+ i_error("dcrypt key store failed: %s", error);
+ lib_exit(1);
+ }
+ oauth2_validation_key_cache_evict(key_cache, "default");
+ save_key("ES256", str_c(keybuf));
+
+ buffer_t *tokenbuf = create_jwt_token("ES256");
+
+ /* sign token */
+ buffer_t *sig = t_buffer_create(64);
+ if (!dcrypt_sign(pair.priv, "sha256", DCRYPT_SIGNATURE_FORMAT_X962,
+ tokenbuf->data, tokenbuf->used, sig,
+ DCRYPT_PADDING_DEFAULT, &error)) {
+ i_error("dcrypt signing failed: %s", error);
+ lib_exit(1);
+ }
+ dcrypt_keypair_unref(&pair);
+
+ /* convert to base64 */
+ buffer_append(tokenbuf, ".", 1);
+ base64url_encode(BASE64_ENCODE_FLAG_NO_PADDING, SIZE_MAX,
+ sig->data, sig->used, tokenbuf);
+ test_jwt_token(str_c(tokenbuf));
+
+ test_end();
+}
+
+static void test_do_init(void)
+{
+ const char *error;
+ struct dcrypt_settings dcrypt_set = {
+ .module_dir = "../lib-dcrypt/.libs",
+ };
+ struct dict_settings dict_set = {
+ .base_dir = ".",
+ };
+
+ i_unlink_if_exists(".keys");
+ dict_driver_register(&dict_driver_file);
+ if (dict_init("file:.keys", &dict_set, &keys_dict, &error) < 0)
+ i_fatal("dict_init(file:.keys): %s", error);
+ if (!dcrypt_initialize(NULL, &dcrypt_set, &error)) {
+ i_error("No functional dcrypt backend found - "
+ "skipping some tests: %s", error);
+ skip_dcrypt = TRUE;
+ }
+ key_cache = oauth2_validation_key_cache_init();
+
+ /* write HMAC secret */
+ hs_sign_key =buffer_create_dynamic(default_pool, 32);
+ void *ptr = buffer_append_space_unsafe(hs_sign_key, 32);
+ random_fill(ptr, 32);
+ buffer_t *b64_key = t_base64_encode(0, SIZE_MAX,
+ hs_sign_key->data,
+ hs_sign_key->used);
+ save_key("HS256", str_c(b64_key));
+}
+
+static void test_do_deinit(void)
+{
+ dict_deinit(&keys_dict);
+ dict_driver_unregister(&dict_driver_file);
+ oauth2_validation_key_cache_deinit(&key_cache);
+ i_unlink(".keys");
+ buffer_free(&hs_sign_key);
+ dcrypt_deinitialize();
+}
+
+int main(void)
+{
+ static void (*test_functions[])(void) = {
+ test_do_init,
+ test_jwt_hs_token,
+ test_jwt_token_escape,
+ test_jwt_valid_token,
+ test_jwt_bad_valid_token,
+ test_jwt_broken_token,
+ test_jwt_dates,
+ test_jwt_key_files,
+ test_jwt_kid_escape,
+ test_jwt_rs_token,
+ test_jwt_ps_token,
+ test_jwt_ec_token,
+ test_do_deinit,
+ NULL
+ };
+ int ret;
+ ret = test_run(test_functions);
+ return ret;
+}
diff --git a/src/lib-old-stats/Makefile.am b/src/lib-old-stats/Makefile.am
new file mode 100644
index 0000000..f97cf62
--- /dev/null
+++ b/src/lib-old-stats/Makefile.am
@@ -0,0 +1,18 @@
+noinst_LTLIBRARIES = libold_stats.la
+
+AM_CPPFLAGS = \
+ -I$(top_srcdir)/src/lib \
+ -I$(top_srcdir)/src/lib-master
+
+libold_stats_la_SOURCES = \
+ stats.c \
+ stats-connection.c \
+ stats-parser.c
+
+headers = \
+ stats.h \
+ stats-connection.h \
+ stats-parser.h
+
+pkginc_libdir = $(pkgincludedir)
+pkginc_lib_HEADERS = $(headers)
diff --git a/src/lib-old-stats/Makefile.in b/src/lib-old-stats/Makefile.in
new file mode 100644
index 0000000..76a4eb4
--- /dev/null
+++ b/src/lib-old-stats/Makefile.in
@@ -0,0 +1,820 @@
+# Makefile.in generated by automake 1.16.1 from Makefile.am.
+# @configure_input@
+
+# Copyright (C) 1994-2018 Free Software Foundation, Inc.
+
+# This Makefile.in is free software; the Free Software Foundation
+# gives unlimited permission to copy and/or distribute it,
+# with or without modifications, as long as this notice is preserved.
+
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY, to the extent permitted by law; without
+# even the implied warranty of MERCHANTABILITY or FITNESS FOR A
+# PARTICULAR PURPOSE.
+
+@SET_MAKE@
+
+
+VPATH = @srcdir@
+am__is_gnu_make = { \
+ if test -z '$(MAKELEVEL)'; then \
+ false; \
+ elif test -n '$(MAKE_HOST)'; then \
+ true; \
+ elif test -n '$(MAKE_VERSION)' && test -n '$(CURDIR)'; then \
+ true; \
+ else \
+ false; \
+ fi; \
+}
+am__make_running_with_option = \
+ case $${target_option-} in \
+ ?) ;; \
+ *) echo "am__make_running_with_option: internal error: invalid" \
+ "target option '$${target_option-}' specified" >&2; \
+ exit 1;; \
+ esac; \
+ has_opt=no; \
+ sane_makeflags=$$MAKEFLAGS; \
+ if $(am__is_gnu_make); then \
+ sane_makeflags=$$MFLAGS; \
+ else \
+ case $$MAKEFLAGS in \
+ *\\[\ \ ]*) \
+ bs=\\; \
+ sane_makeflags=`printf '%s\n' "$$MAKEFLAGS" \
+ | sed "s/$$bs$$bs[$$bs $$bs ]*//g"`;; \
+ esac; \
+ fi; \
+ skip_next=no; \
+ strip_trailopt () \
+ { \
+ flg=`printf '%s\n' "$$flg" | sed "s/$$1.*$$//"`; \
+ }; \
+ for flg in $$sane_makeflags; do \
+ test $$skip_next = yes && { skip_next=no; continue; }; \
+ case $$flg in \
+ *=*|--*) continue;; \
+ -*I) strip_trailopt 'I'; skip_next=yes;; \
+ -*I?*) strip_trailopt 'I';; \
+ -*O) strip_trailopt 'O'; skip_next=yes;; \
+ -*O?*) strip_trailopt 'O';; \
+ -*l) strip_trailopt 'l'; skip_next=yes;; \
+ -*l?*) strip_trailopt 'l';; \
+ -[dEDm]) skip_next=yes;; \
+ -[JT]) skip_next=yes;; \
+ esac; \
+ case $$flg in \
+ *$$target_option*) has_opt=yes; break;; \
+ esac; \
+ done; \
+ test $$has_opt = yes
+am__make_dryrun = (target_option=n; $(am__make_running_with_option))
+am__make_keepgoing = (target_option=k; $(am__make_running_with_option))
+pkgdatadir = $(datadir)/@PACKAGE@
+pkgincludedir = $(includedir)/@PACKAGE@
+pkglibdir = $(libdir)/@PACKAGE@
+pkglibexecdir = $(libexecdir)/@PACKAGE@
+am__cd = CDPATH="$${ZSH_VERSION+.}$(PATH_SEPARATOR)" && cd
+install_sh_DATA = $(install_sh) -c -m 644
+install_sh_PROGRAM = $(install_sh) -c
+install_sh_SCRIPT = $(install_sh) -c
+INSTALL_HEADER = $(INSTALL_DATA)
+transform = $(program_transform_name)
+NORMAL_INSTALL = :
+PRE_INSTALL = :
+POST_INSTALL = :
+NORMAL_UNINSTALL = :
+PRE_UNINSTALL = :
+POST_UNINSTALL = :
+build_triplet = @build@
+host_triplet = @host@
+subdir = src/lib-old-stats
+ACLOCAL_M4 = $(top_srcdir)/aclocal.m4
+am__aclocal_m4_deps = $(top_srcdir)/m4/ac_checktype2.m4 \
+ $(top_srcdir)/m4/ac_typeof.m4 $(top_srcdir)/m4/arc4random.m4 \
+ $(top_srcdir)/m4/blockdev.m4 $(top_srcdir)/m4/c99_vsnprintf.m4 \
+ $(top_srcdir)/m4/clock_gettime.m4 $(top_srcdir)/m4/crypt.m4 \
+ $(top_srcdir)/m4/crypt_xpg6.m4 $(top_srcdir)/m4/dbqlk.m4 \
+ $(top_srcdir)/m4/dirent_dtype.m4 $(top_srcdir)/m4/dovecot.m4 \
+ $(top_srcdir)/m4/fd_passing.m4 $(top_srcdir)/m4/fdatasync.m4 \
+ $(top_srcdir)/m4/flexible_array_member.m4 \
+ $(top_srcdir)/m4/glibc.m4 $(top_srcdir)/m4/gmtime_max.m4 \
+ $(top_srcdir)/m4/gmtime_tm_gmtoff.m4 \
+ $(top_srcdir)/m4/ioloop.m4 $(top_srcdir)/m4/iovec.m4 \
+ $(top_srcdir)/m4/ipv6.m4 $(top_srcdir)/m4/libcap.m4 \
+ $(top_srcdir)/m4/libtool.m4 $(top_srcdir)/m4/libwrap.m4 \
+ $(top_srcdir)/m4/linux_mremap.m4 $(top_srcdir)/m4/ltoptions.m4 \
+ $(top_srcdir)/m4/ltsugar.m4 $(top_srcdir)/m4/ltversion.m4 \
+ $(top_srcdir)/m4/lt~obsolete.m4 $(top_srcdir)/m4/mmap_write.m4 \
+ $(top_srcdir)/m4/mntctl.m4 $(top_srcdir)/m4/modules.m4 \
+ $(top_srcdir)/m4/notify.m4 $(top_srcdir)/m4/nsl.m4 \
+ $(top_srcdir)/m4/off_t_max.m4 $(top_srcdir)/m4/pkg.m4 \
+ $(top_srcdir)/m4/pr_set_dumpable.m4 \
+ $(top_srcdir)/m4/q_quotactl.m4 $(top_srcdir)/m4/quota.m4 \
+ $(top_srcdir)/m4/random.m4 $(top_srcdir)/m4/rlimit.m4 \
+ $(top_srcdir)/m4/sendfile.m4 $(top_srcdir)/m4/size_t_signed.m4 \
+ $(top_srcdir)/m4/sockpeercred.m4 $(top_srcdir)/m4/sql.m4 \
+ $(top_srcdir)/m4/ssl.m4 $(top_srcdir)/m4/st_tim.m4 \
+ $(top_srcdir)/m4/static_array.m4 $(top_srcdir)/m4/test_with.m4 \
+ $(top_srcdir)/m4/time_t.m4 $(top_srcdir)/m4/typeof.m4 \
+ $(top_srcdir)/m4/typeof_dev_t.m4 \
+ $(top_srcdir)/m4/uoff_t_max.m4 $(top_srcdir)/m4/vararg.m4 \
+ $(top_srcdir)/m4/want_apparmor.m4 \
+ $(top_srcdir)/m4/want_bsdauth.m4 \
+ $(top_srcdir)/m4/want_bzlib.m4 \
+ $(top_srcdir)/m4/want_cassandra.m4 \
+ $(top_srcdir)/m4/want_cdb.m4 \
+ $(top_srcdir)/m4/want_checkpassword.m4 \
+ $(top_srcdir)/m4/want_clucene.m4 $(top_srcdir)/m4/want_db.m4 \
+ $(top_srcdir)/m4/want_gssapi.m4 $(top_srcdir)/m4/want_icu.m4 \
+ $(top_srcdir)/m4/want_ldap.m4 $(top_srcdir)/m4/want_lua.m4 \
+ $(top_srcdir)/m4/want_lz4.m4 $(top_srcdir)/m4/want_lzma.m4 \
+ $(top_srcdir)/m4/want_mysql.m4 $(top_srcdir)/m4/want_pam.m4 \
+ $(top_srcdir)/m4/want_passwd.m4 $(top_srcdir)/m4/want_pgsql.m4 \
+ $(top_srcdir)/m4/want_prefetch.m4 \
+ $(top_srcdir)/m4/want_shadow.m4 \
+ $(top_srcdir)/m4/want_sodium.m4 $(top_srcdir)/m4/want_solr.m4 \
+ $(top_srcdir)/m4/want_sqlite.m4 \
+ $(top_srcdir)/m4/want_stemmer.m4 \
+ $(top_srcdir)/m4/want_systemd.m4 \
+ $(top_srcdir)/m4/want_textcat.m4 \
+ $(top_srcdir)/m4/want_unwind.m4 $(top_srcdir)/m4/want_zlib.m4 \
+ $(top_srcdir)/m4/want_zstd.m4 $(top_srcdir)/configure.ac
+am__configure_deps = $(am__aclocal_m4_deps) $(CONFIGURE_DEPENDENCIES) \
+ $(ACLOCAL_M4)
+DIST_COMMON = $(srcdir)/Makefile.am $(pkginc_lib_HEADERS) \
+ $(am__DIST_COMMON)
+mkinstalldirs = $(install_sh) -d
+CONFIG_HEADER = $(top_builddir)/config.h
+CONFIG_CLEAN_FILES =
+CONFIG_CLEAN_VPATH_FILES =
+LTLIBRARIES = $(noinst_LTLIBRARIES)
+libold_stats_la_LIBADD =
+am_libold_stats_la_OBJECTS = stats.lo stats-connection.lo \
+ stats-parser.lo
+libold_stats_la_OBJECTS = $(am_libold_stats_la_OBJECTS)
+AM_V_lt = $(am__v_lt_@AM_V@)
+am__v_lt_ = $(am__v_lt_@AM_DEFAULT_V@)
+am__v_lt_0 = --silent
+am__v_lt_1 =
+AM_V_P = $(am__v_P_@AM_V@)
+am__v_P_ = $(am__v_P_@AM_DEFAULT_V@)
+am__v_P_0 = false
+am__v_P_1 = :
+AM_V_GEN = $(am__v_GEN_@AM_V@)
+am__v_GEN_ = $(am__v_GEN_@AM_DEFAULT_V@)
+am__v_GEN_0 = @echo " GEN " $@;
+am__v_GEN_1 =
+AM_V_at = $(am__v_at_@AM_V@)
+am__v_at_ = $(am__v_at_@AM_DEFAULT_V@)
+am__v_at_0 = @
+am__v_at_1 =
+DEFAULT_INCLUDES = -I.@am__isrc@ -I$(top_builddir)
+depcomp = $(SHELL) $(top_srcdir)/depcomp
+am__maybe_remake_depfiles = depfiles
+am__depfiles_remade = ./$(DEPDIR)/stats-connection.Plo \
+ ./$(DEPDIR)/stats-parser.Plo ./$(DEPDIR)/stats.Plo
+am__mv = mv -f
+COMPILE = $(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) \
+ $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS)
+LTCOMPILE = $(LIBTOOL) $(AM_V_lt) --tag=CC $(AM_LIBTOOLFLAGS) \
+ $(LIBTOOLFLAGS) --mode=compile $(CC) $(DEFS) \
+ $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) $(CPPFLAGS) \
+ $(AM_CFLAGS) $(CFLAGS)
+AM_V_CC = $(am__v_CC_@AM_V@)
+am__v_CC_ = $(am__v_CC_@AM_DEFAULT_V@)
+am__v_CC_0 = @echo " CC " $@;
+am__v_CC_1 =
+CCLD = $(CC)
+LINK = $(LIBTOOL) $(AM_V_lt) --tag=CC $(AM_LIBTOOLFLAGS) \
+ $(LIBTOOLFLAGS) --mode=link $(CCLD) $(AM_CFLAGS) $(CFLAGS) \
+ $(AM_LDFLAGS) $(LDFLAGS) -o $@
+AM_V_CCLD = $(am__v_CCLD_@AM_V@)
+am__v_CCLD_ = $(am__v_CCLD_@AM_DEFAULT_V@)
+am__v_CCLD_0 = @echo " CCLD " $@;
+am__v_CCLD_1 =
+SOURCES = $(libold_stats_la_SOURCES)
+DIST_SOURCES = $(libold_stats_la_SOURCES)
+am__can_run_installinfo = \
+ case $$AM_UPDATE_INFO_DIR in \
+ n|no|NO) false;; \
+ *) (install-info --version) >/dev/null 2>&1;; \
+ esac
+am__vpath_adj_setup = srcdirstrip=`echo "$(srcdir)" | sed 's|.|.|g'`;
+am__vpath_adj = case $$p in \
+ $(srcdir)/*) f=`echo "$$p" | sed "s|^$$srcdirstrip/||"`;; \
+ *) f=$$p;; \
+ esac;
+am__strip_dir = f=`echo $$p | sed -e 's|^.*/||'`;
+am__install_max = 40
+am__nobase_strip_setup = \
+ srcdirstrip=`echo "$(srcdir)" | sed 's/[].[^$$\\*|]/\\\\&/g'`
+am__nobase_strip = \
+ for p in $$list; do echo "$$p"; done | sed -e "s|$$srcdirstrip/||"
+am__nobase_list = $(am__nobase_strip_setup); \
+ for p in $$list; do echo "$$p $$p"; done | \
+ sed "s| $$srcdirstrip/| |;"' / .*\//!s/ .*/ ./; s,\( .*\)/[^/]*$$,\1,' | \
+ $(AWK) 'BEGIN { files["."] = "" } { files[$$2] = files[$$2] " " $$1; \
+ if (++n[$$2] == $(am__install_max)) \
+ { print $$2, files[$$2]; n[$$2] = 0; files[$$2] = "" } } \
+ END { for (dir in files) print dir, files[dir] }'
+am__base_list = \
+ sed '$$!N;$$!N;$$!N;$$!N;$$!N;$$!N;$$!N;s/\n/ /g' | \
+ sed '$$!N;$$!N;$$!N;$$!N;s/\n/ /g'
+am__uninstall_files_from_dir = { \
+ test -z "$$files" \
+ || { test ! -d "$$dir" && test ! -f "$$dir" && test ! -r "$$dir"; } \
+ || { echo " ( cd '$$dir' && rm -f" $$files ")"; \
+ $(am__cd) "$$dir" && rm -f $$files; }; \
+ }
+am__installdirs = "$(DESTDIR)$(pkginc_libdir)"
+HEADERS = $(pkginc_lib_HEADERS)
+am__tagged_files = $(HEADERS) $(SOURCES) $(TAGS_FILES) $(LISP)
+# Read a list of newline-separated strings from the standard input,
+# and print each of them once, without duplicates. Input order is
+# *not* preserved.
+am__uniquify_input = $(AWK) '\
+ BEGIN { nonempty = 0; } \
+ { items[$$0] = 1; nonempty = 1; } \
+ END { if (nonempty) { for (i in items) print i; }; } \
+'
+# Make sure the list of sources is unique. This is necessary because,
+# e.g., the same source file might be shared among _SOURCES variables
+# for different programs/libraries.
+am__define_uniq_tagged_files = \
+ list='$(am__tagged_files)'; \
+ unique=`for i in $$list; do \
+ if test -f "$$i"; then echo $$i; else echo $(srcdir)/$$i; fi; \
+ done | $(am__uniquify_input)`
+ETAGS = etags
+CTAGS = ctags
+am__DIST_COMMON = $(srcdir)/Makefile.in $(top_srcdir)/depcomp
+DISTFILES = $(DIST_COMMON) $(DIST_SOURCES) $(TEXINFOS) $(EXTRA_DIST)
+ACLOCAL = @ACLOCAL@
+ACLOCAL_AMFLAGS = @ACLOCAL_AMFLAGS@
+AMTAR = @AMTAR@
+AM_DEFAULT_VERBOSITY = @AM_DEFAULT_VERBOSITY@
+APPARMOR_LIBS = @APPARMOR_LIBS@
+AR = @AR@
+AUTH_CFLAGS = @AUTH_CFLAGS@
+AUTH_LIBS = @AUTH_LIBS@
+AUTOCONF = @AUTOCONF@
+AUTOHEADER = @AUTOHEADER@
+AUTOMAKE = @AUTOMAKE@
+AWK = @AWK@
+BINARY_CFLAGS = @BINARY_CFLAGS@
+BINARY_LDFLAGS = @BINARY_LDFLAGS@
+BISON = @BISON@
+CASSANDRA_CFLAGS = @CASSANDRA_CFLAGS@
+CASSANDRA_LIBS = @CASSANDRA_LIBS@
+CC = @CC@
+CCDEPMODE = @CCDEPMODE@
+CDB_LIBS = @CDB_LIBS@
+CFLAGS = @CFLAGS@
+CLUCENE_CFLAGS = @CLUCENE_CFLAGS@
+CLUCENE_LIBS = @CLUCENE_LIBS@
+COMPRESS_LIBS = @COMPRESS_LIBS@
+CPP = @CPP@
+CPPFLAGS = @CPPFLAGS@
+CRYPT_LIBS = @CRYPT_LIBS@
+CXX = @CXX@
+CXXCPP = @CXXCPP@
+CXXDEPMODE = @CXXDEPMODE@
+CXXFLAGS = @CXXFLAGS@
+CYGPATH_W = @CYGPATH_W@
+DEFS = @DEFS@
+DEPDIR = @DEPDIR@
+DICT_LIBS = @DICT_LIBS@
+DLLIB = @DLLIB@
+DLLTOOL = @DLLTOOL@
+DSYMUTIL = @DSYMUTIL@
+DUMPBIN = @DUMPBIN@
+ECHO_C = @ECHO_C@
+ECHO_N = @ECHO_N@
+ECHO_T = @ECHO_T@
+EGREP = @EGREP@
+EXEEXT = @EXEEXT@
+FGREP = @FGREP@
+FLEX = @FLEX@
+FUZZER_CPPFLAGS = @FUZZER_CPPFLAGS@
+FUZZER_LDFLAGS = @FUZZER_LDFLAGS@
+GREP = @GREP@
+INSTALL = @INSTALL@
+INSTALL_DATA = @INSTALL_DATA@
+INSTALL_PROGRAM = @INSTALL_PROGRAM@
+INSTALL_SCRIPT = @INSTALL_SCRIPT@
+INSTALL_STRIP_PROGRAM = @INSTALL_STRIP_PROGRAM@
+KRB5CONFIG = @KRB5CONFIG@
+KRB5_CFLAGS = @KRB5_CFLAGS@
+KRB5_LIBS = @KRB5_LIBS@
+LD = @LD@
+LDAP_LIBS = @LDAP_LIBS@
+LDFLAGS = @LDFLAGS@
+LD_NO_WHOLE_ARCHIVE = @LD_NO_WHOLE_ARCHIVE@
+LD_WHOLE_ARCHIVE = @LD_WHOLE_ARCHIVE@
+LIBCAP = @LIBCAP@
+LIBDOVECOT = @LIBDOVECOT@
+LIBDOVECOT_COMPRESS = @LIBDOVECOT_COMPRESS@
+LIBDOVECOT_DEPS = @LIBDOVECOT_DEPS@
+LIBDOVECOT_DSYNC = @LIBDOVECOT_DSYNC@
+LIBDOVECOT_LA_LIBS = @LIBDOVECOT_LA_LIBS@
+LIBDOVECOT_LDA = @LIBDOVECOT_LDA@
+LIBDOVECOT_LDAP = @LIBDOVECOT_LDAP@
+LIBDOVECOT_LIBFTS = @LIBDOVECOT_LIBFTS@
+LIBDOVECOT_LIBFTS_DEPS = @LIBDOVECOT_LIBFTS_DEPS@
+LIBDOVECOT_LOGIN = @LIBDOVECOT_LOGIN@
+LIBDOVECOT_LUA = @LIBDOVECOT_LUA@
+LIBDOVECOT_LUA_DEPS = @LIBDOVECOT_LUA_DEPS@
+LIBDOVECOT_SQL = @LIBDOVECOT_SQL@
+LIBDOVECOT_STORAGE = @LIBDOVECOT_STORAGE@
+LIBDOVECOT_STORAGE_DEPS = @LIBDOVECOT_STORAGE_DEPS@
+LIBEXTTEXTCAT_CFLAGS = @LIBEXTTEXTCAT_CFLAGS@
+LIBEXTTEXTCAT_LIBS = @LIBEXTTEXTCAT_LIBS@
+LIBICONV = @LIBICONV@
+LIBICU_CFLAGS = @LIBICU_CFLAGS@
+LIBICU_LIBS = @LIBICU_LIBS@
+LIBOBJS = @LIBOBJS@
+LIBS = @LIBS@
+LIBSODIUM_CFLAGS = @LIBSODIUM_CFLAGS@
+LIBSODIUM_LIBS = @LIBSODIUM_LIBS@
+LIBTIRPC_CFLAGS = @LIBTIRPC_CFLAGS@
+LIBTIRPC_LIBS = @LIBTIRPC_LIBS@
+LIBTOOL = @LIBTOOL@
+LIBUNWIND_CFLAGS = @LIBUNWIND_CFLAGS@
+LIBUNWIND_LIBS = @LIBUNWIND_LIBS@
+LIBWRAP_LIBS = @LIBWRAP_LIBS@
+LINKED_STORAGE_LDADD = @LINKED_STORAGE_LDADD@
+LIPO = @LIPO@
+LN_S = @LN_S@
+LTLIBICONV = @LTLIBICONV@
+LTLIBOBJS = @LTLIBOBJS@
+LT_SYS_LIBRARY_PATH = @LT_SYS_LIBRARY_PATH@
+LUA_CFLAGS = @LUA_CFLAGS@
+LUA_LIBS = @LUA_LIBS@
+MAINT = @MAINT@
+MAKEINFO = @MAKEINFO@
+MANIFEST_TOOL = @MANIFEST_TOOL@
+MKDIR_P = @MKDIR_P@
+MODULE_LIBS = @MODULE_LIBS@
+MODULE_SUFFIX = @MODULE_SUFFIX@
+MYSQL_CFLAGS = @MYSQL_CFLAGS@
+MYSQL_CONFIG = @MYSQL_CONFIG@
+MYSQL_LIBS = @MYSQL_LIBS@
+NM = @NM@
+NMEDIT = @NMEDIT@
+NOPLUGIN_LDFLAGS = @NOPLUGIN_LDFLAGS@
+OBJDUMP = @OBJDUMP@
+OBJEXT = @OBJEXT@
+OTOOL = @OTOOL@
+OTOOL64 = @OTOOL64@
+PACKAGE = @PACKAGE@
+PACKAGE_BUGREPORT = @PACKAGE_BUGREPORT@
+PACKAGE_NAME = @PACKAGE_NAME@
+PACKAGE_STRING = @PACKAGE_STRING@
+PACKAGE_TARNAME = @PACKAGE_TARNAME@
+PACKAGE_URL = @PACKAGE_URL@
+PACKAGE_VERSION = @PACKAGE_VERSION@
+PANDOC = @PANDOC@
+PATH_SEPARATOR = @PATH_SEPARATOR@
+PGSQL_CFLAGS = @PGSQL_CFLAGS@
+PGSQL_LIBS = @PGSQL_LIBS@
+PG_CONFIG = @PG_CONFIG@
+PIE_CFLAGS = @PIE_CFLAGS@
+PIE_LDFLAGS = @PIE_LDFLAGS@
+PKG_CONFIG = @PKG_CONFIG@
+PKG_CONFIG_LIBDIR = @PKG_CONFIG_LIBDIR@
+PKG_CONFIG_PATH = @PKG_CONFIG_PATH@
+QUOTA_LIBS = @QUOTA_LIBS@
+RANLIB = @RANLIB@
+RELRO_LDFLAGS = @RELRO_LDFLAGS@
+RPCGEN = @RPCGEN@
+RUN_TEST = @RUN_TEST@
+SED = @SED@
+SETTING_FILES = @SETTING_FILES@
+SET_MAKE = @SET_MAKE@
+SHELL = @SHELL@
+SQLITE_CFLAGS = @SQLITE_CFLAGS@
+SQLITE_LIBS = @SQLITE_LIBS@
+SQL_CFLAGS = @SQL_CFLAGS@
+SQL_LIBS = @SQL_LIBS@
+SSL_CFLAGS = @SSL_CFLAGS@
+SSL_LIBS = @SSL_LIBS@
+STRIP = @STRIP@
+SYSTEMD_CFLAGS = @SYSTEMD_CFLAGS@
+SYSTEMD_LIBS = @SYSTEMD_LIBS@
+VALGRIND = @VALGRIND@
+VERSION = @VERSION@
+ZSTD_CFLAGS = @ZSTD_CFLAGS@
+ZSTD_LIBS = @ZSTD_LIBS@
+abs_builddir = @abs_builddir@
+abs_srcdir = @abs_srcdir@
+abs_top_builddir = @abs_top_builddir@
+abs_top_srcdir = @abs_top_srcdir@
+ac_ct_AR = @ac_ct_AR@
+ac_ct_CC = @ac_ct_CC@
+ac_ct_CXX = @ac_ct_CXX@
+ac_ct_DUMPBIN = @ac_ct_DUMPBIN@
+am__include = @am__include@
+am__leading_dot = @am__leading_dot@
+am__quote = @am__quote@
+am__tar = @am__tar@
+am__untar = @am__untar@
+bindir = @bindir@
+build = @build@
+build_alias = @build_alias@
+build_cpu = @build_cpu@
+build_os = @build_os@
+build_vendor = @build_vendor@
+builddir = @builddir@
+datadir = @datadir@
+datarootdir = @datarootdir@
+dict_drivers = @dict_drivers@
+docdir = @docdir@
+dvidir = @dvidir@
+exec_prefix = @exec_prefix@
+host = @host@
+host_alias = @host_alias@
+host_cpu = @host_cpu@
+host_os = @host_os@
+host_vendor = @host_vendor@
+htmldir = @htmldir@
+includedir = @includedir@
+infodir = @infodir@
+install_sh = @install_sh@
+libdir = @libdir@
+libexecdir = @libexecdir@
+localedir = @localedir@
+localstatedir = @localstatedir@
+mandir = @mandir@
+mkdir_p = @mkdir_p@
+moduledir = @moduledir@
+oldincludedir = @oldincludedir@
+pdfdir = @pdfdir@
+prefix = @prefix@
+program_transform_name = @program_transform_name@
+psdir = @psdir@
+rundir = @rundir@
+runstatedir = @runstatedir@
+sbindir = @sbindir@
+sharedstatedir = @sharedstatedir@
+sql_drivers = @sql_drivers@
+srcdir = @srcdir@
+ssldir = @ssldir@
+statedir = @statedir@
+sysconfdir = @sysconfdir@
+systemdservicetype = @systemdservicetype@
+systemdsystemunitdir = @systemdsystemunitdir@
+target_alias = @target_alias@
+top_build_prefix = @top_build_prefix@
+top_builddir = @top_builddir@
+top_srcdir = @top_srcdir@
+noinst_LTLIBRARIES = libold_stats.la
+AM_CPPFLAGS = \
+ -I$(top_srcdir)/src/lib \
+ -I$(top_srcdir)/src/lib-master
+
+libold_stats_la_SOURCES = \
+ stats.c \
+ stats-connection.c \
+ stats-parser.c
+
+headers = \
+ stats.h \
+ stats-connection.h \
+ stats-parser.h
+
+pkginc_libdir = $(pkgincludedir)
+pkginc_lib_HEADERS = $(headers)
+all: all-am
+
+.SUFFIXES:
+.SUFFIXES: .c .lo .o .obj
+$(srcdir)/Makefile.in: @MAINTAINER_MODE_TRUE@ $(srcdir)/Makefile.am $(am__configure_deps)
+ @for dep in $?; do \
+ case '$(am__configure_deps)' in \
+ *$$dep*) \
+ ( cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh ) \
+ && { if test -f $@; then exit 0; else break; fi; }; \
+ exit 1;; \
+ esac; \
+ done; \
+ echo ' cd $(top_srcdir) && $(AUTOMAKE) --foreign src/lib-old-stats/Makefile'; \
+ $(am__cd) $(top_srcdir) && \
+ $(AUTOMAKE) --foreign src/lib-old-stats/Makefile
+Makefile: $(srcdir)/Makefile.in $(top_builddir)/config.status
+ @case '$?' in \
+ *config.status*) \
+ cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh;; \
+ *) \
+ echo ' cd $(top_builddir) && $(SHELL) ./config.status $(subdir)/$@ $(am__maybe_remake_depfiles)'; \
+ cd $(top_builddir) && $(SHELL) ./config.status $(subdir)/$@ $(am__maybe_remake_depfiles);; \
+ esac;
+
+$(top_builddir)/config.status: $(top_srcdir)/configure $(CONFIG_STATUS_DEPENDENCIES)
+ cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh
+
+$(top_srcdir)/configure: @MAINTAINER_MODE_TRUE@ $(am__configure_deps)
+ cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh
+$(ACLOCAL_M4): @MAINTAINER_MODE_TRUE@ $(am__aclocal_m4_deps)
+ cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh
+$(am__aclocal_m4_deps):
+
+clean-noinstLTLIBRARIES:
+ -test -z "$(noinst_LTLIBRARIES)" || rm -f $(noinst_LTLIBRARIES)
+ @list='$(noinst_LTLIBRARIES)'; \
+ locs=`for p in $$list; do echo $$p; done | \
+ sed 's|^[^/]*$$|.|; s|/[^/]*$$||; s|$$|/so_locations|' | \
+ sort -u`; \
+ test -z "$$locs" || { \
+ echo rm -f $${locs}; \
+ rm -f $${locs}; \
+ }
+
+libold_stats.la: $(libold_stats_la_OBJECTS) $(libold_stats_la_DEPENDENCIES) $(EXTRA_libold_stats_la_DEPENDENCIES)
+ $(AM_V_CCLD)$(LINK) $(libold_stats_la_OBJECTS) $(libold_stats_la_LIBADD) $(LIBS)
+
+mostlyclean-compile:
+ -rm -f *.$(OBJEXT)
+
+distclean-compile:
+ -rm -f *.tab.c
+
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/stats-connection.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/stats-parser.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/stats.Plo@am__quote@ # am--include-marker
+
+$(am__depfiles_remade):
+ @$(MKDIR_P) $(@D)
+ @echo '# dummy' >$@-t && $(am__mv) $@-t $@
+
+am--depfiles: $(am__depfiles_remade)
+
+.c.o:
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(COMPILE) -MT $@ -MD -MP -MF $(DEPDIR)/$*.Tpo -c -o $@ $<
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/$*.Tpo $(DEPDIR)/$*.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='$<' object='$@' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(COMPILE) -c -o $@ $<
+
+.c.obj:
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(COMPILE) -MT $@ -MD -MP -MF $(DEPDIR)/$*.Tpo -c -o $@ `$(CYGPATH_W) '$<'`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/$*.Tpo $(DEPDIR)/$*.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='$<' object='$@' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(COMPILE) -c -o $@ `$(CYGPATH_W) '$<'`
+
+.c.lo:
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(LTCOMPILE) -MT $@ -MD -MP -MF $(DEPDIR)/$*.Tpo -c -o $@ $<
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/$*.Tpo $(DEPDIR)/$*.Plo
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='$<' object='$@' libtool=yes @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(LTCOMPILE) -c -o $@ $<
+
+mostlyclean-libtool:
+ -rm -f *.lo
+
+clean-libtool:
+ -rm -rf .libs _libs
+install-pkginc_libHEADERS: $(pkginc_lib_HEADERS)
+ @$(NORMAL_INSTALL)
+ @list='$(pkginc_lib_HEADERS)'; test -n "$(pkginc_libdir)" || list=; \
+ if test -n "$$list"; then \
+ echo " $(MKDIR_P) '$(DESTDIR)$(pkginc_libdir)'"; \
+ $(MKDIR_P) "$(DESTDIR)$(pkginc_libdir)" || exit 1; \
+ fi; \
+ for p in $$list; do \
+ if test -f "$$p"; then d=; else d="$(srcdir)/"; fi; \
+ echo "$$d$$p"; \
+ done | $(am__base_list) | \
+ while read files; do \
+ echo " $(INSTALL_HEADER) $$files '$(DESTDIR)$(pkginc_libdir)'"; \
+ $(INSTALL_HEADER) $$files "$(DESTDIR)$(pkginc_libdir)" || exit $$?; \
+ done
+
+uninstall-pkginc_libHEADERS:
+ @$(NORMAL_UNINSTALL)
+ @list='$(pkginc_lib_HEADERS)'; test -n "$(pkginc_libdir)" || list=; \
+ files=`for p in $$list; do echo $$p; done | sed -e 's|^.*/||'`; \
+ dir='$(DESTDIR)$(pkginc_libdir)'; $(am__uninstall_files_from_dir)
+
+ID: $(am__tagged_files)
+ $(am__define_uniq_tagged_files); mkid -fID $$unique
+tags: tags-am
+TAGS: tags
+
+tags-am: $(TAGS_DEPENDENCIES) $(am__tagged_files)
+ set x; \
+ here=`pwd`; \
+ $(am__define_uniq_tagged_files); \
+ shift; \
+ if test -z "$(ETAGS_ARGS)$$*$$unique"; then :; else \
+ test -n "$$unique" || unique=$$empty_fix; \
+ if test $$# -gt 0; then \
+ $(ETAGS) $(ETAGSFLAGS) $(AM_ETAGSFLAGS) $(ETAGS_ARGS) \
+ "$$@" $$unique; \
+ else \
+ $(ETAGS) $(ETAGSFLAGS) $(AM_ETAGSFLAGS) $(ETAGS_ARGS) \
+ $$unique; \
+ fi; \
+ fi
+ctags: ctags-am
+
+CTAGS: ctags
+ctags-am: $(TAGS_DEPENDENCIES) $(am__tagged_files)
+ $(am__define_uniq_tagged_files); \
+ test -z "$(CTAGS_ARGS)$$unique" \
+ || $(CTAGS) $(CTAGSFLAGS) $(AM_CTAGSFLAGS) $(CTAGS_ARGS) \
+ $$unique
+
+GTAGS:
+ here=`$(am__cd) $(top_builddir) && pwd` \
+ && $(am__cd) $(top_srcdir) \
+ && gtags -i $(GTAGS_ARGS) "$$here"
+cscopelist: cscopelist-am
+
+cscopelist-am: $(am__tagged_files)
+ list='$(am__tagged_files)'; \
+ case "$(srcdir)" in \
+ [\\/]* | ?:[\\/]*) sdir="$(srcdir)" ;; \
+ *) sdir=$(subdir)/$(srcdir) ;; \
+ esac; \
+ for i in $$list; do \
+ if test -f "$$i"; then \
+ echo "$(subdir)/$$i"; \
+ else \
+ echo "$$sdir/$$i"; \
+ fi; \
+ done >> $(top_builddir)/cscope.files
+
+distclean-tags:
+ -rm -f TAGS ID GTAGS GRTAGS GSYMS GPATH tags
+
+distdir: $(BUILT_SOURCES)
+ $(MAKE) $(AM_MAKEFLAGS) distdir-am
+
+distdir-am: $(DISTFILES)
+ @srcdirstrip=`echo "$(srcdir)" | sed 's/[].[^$$\\*]/\\\\&/g'`; \
+ topsrcdirstrip=`echo "$(top_srcdir)" | sed 's/[].[^$$\\*]/\\\\&/g'`; \
+ list='$(DISTFILES)'; \
+ dist_files=`for file in $$list; do echo $$file; done | \
+ sed -e "s|^$$srcdirstrip/||;t" \
+ -e "s|^$$topsrcdirstrip/|$(top_builddir)/|;t"`; \
+ case $$dist_files in \
+ */*) $(MKDIR_P) `echo "$$dist_files" | \
+ sed '/\//!d;s|^|$(distdir)/|;s,/[^/]*$$,,' | \
+ sort -u` ;; \
+ esac; \
+ for file in $$dist_files; do \
+ if test -f $$file || test -d $$file; then d=.; else d=$(srcdir); fi; \
+ if test -d $$d/$$file; then \
+ dir=`echo "/$$file" | sed -e 's,/[^/]*$$,,'`; \
+ if test -d "$(distdir)/$$file"; then \
+ find "$(distdir)/$$file" -type d ! -perm -700 -exec chmod u+rwx {} \;; \
+ fi; \
+ if test -d $(srcdir)/$$file && test $$d != $(srcdir); then \
+ cp -fpR $(srcdir)/$$file "$(distdir)$$dir" || exit 1; \
+ find "$(distdir)/$$file" -type d ! -perm -700 -exec chmod u+rwx {} \;; \
+ fi; \
+ cp -fpR $$d/$$file "$(distdir)$$dir" || exit 1; \
+ else \
+ test -f "$(distdir)/$$file" \
+ || cp -p $$d/$$file "$(distdir)/$$file" \
+ || exit 1; \
+ fi; \
+ done
+check-am: all-am
+check: check-am
+all-am: Makefile $(LTLIBRARIES) $(HEADERS)
+installdirs:
+ for dir in "$(DESTDIR)$(pkginc_libdir)"; do \
+ test -z "$$dir" || $(MKDIR_P) "$$dir"; \
+ done
+install: install-am
+install-exec: install-exec-am
+install-data: install-data-am
+uninstall: uninstall-am
+
+install-am: all-am
+ @$(MAKE) $(AM_MAKEFLAGS) install-exec-am install-data-am
+
+installcheck: installcheck-am
+install-strip:
+ if test -z '$(STRIP)'; then \
+ $(MAKE) $(AM_MAKEFLAGS) INSTALL_PROGRAM="$(INSTALL_STRIP_PROGRAM)" \
+ install_sh_PROGRAM="$(INSTALL_STRIP_PROGRAM)" INSTALL_STRIP_FLAG=-s \
+ install; \
+ else \
+ $(MAKE) $(AM_MAKEFLAGS) INSTALL_PROGRAM="$(INSTALL_STRIP_PROGRAM)" \
+ install_sh_PROGRAM="$(INSTALL_STRIP_PROGRAM)" INSTALL_STRIP_FLAG=-s \
+ "INSTALL_PROGRAM_ENV=STRIPPROG='$(STRIP)'" install; \
+ fi
+mostlyclean-generic:
+
+clean-generic:
+
+distclean-generic:
+ -test -z "$(CONFIG_CLEAN_FILES)" || rm -f $(CONFIG_CLEAN_FILES)
+ -test . = "$(srcdir)" || test -z "$(CONFIG_CLEAN_VPATH_FILES)" || rm -f $(CONFIG_CLEAN_VPATH_FILES)
+
+maintainer-clean-generic:
+ @echo "This command is intended for maintainers to use"
+ @echo "it deletes files that may require special tools to rebuild."
+clean: clean-am
+
+clean-am: clean-generic clean-libtool clean-noinstLTLIBRARIES \
+ mostlyclean-am
+
+distclean: distclean-am
+ -rm -f ./$(DEPDIR)/stats-connection.Plo
+ -rm -f ./$(DEPDIR)/stats-parser.Plo
+ -rm -f ./$(DEPDIR)/stats.Plo
+ -rm -f Makefile
+distclean-am: clean-am distclean-compile distclean-generic \
+ distclean-tags
+
+dvi: dvi-am
+
+dvi-am:
+
+html: html-am
+
+html-am:
+
+info: info-am
+
+info-am:
+
+install-data-am: install-pkginc_libHEADERS
+
+install-dvi: install-dvi-am
+
+install-dvi-am:
+
+install-exec-am:
+
+install-html: install-html-am
+
+install-html-am:
+
+install-info: install-info-am
+
+install-info-am:
+
+install-man:
+
+install-pdf: install-pdf-am
+
+install-pdf-am:
+
+install-ps: install-ps-am
+
+install-ps-am:
+
+installcheck-am:
+
+maintainer-clean: maintainer-clean-am
+ -rm -f ./$(DEPDIR)/stats-connection.Plo
+ -rm -f ./$(DEPDIR)/stats-parser.Plo
+ -rm -f ./$(DEPDIR)/stats.Plo
+ -rm -f Makefile
+maintainer-clean-am: distclean-am maintainer-clean-generic
+
+mostlyclean: mostlyclean-am
+
+mostlyclean-am: mostlyclean-compile mostlyclean-generic \
+ mostlyclean-libtool
+
+pdf: pdf-am
+
+pdf-am:
+
+ps: ps-am
+
+ps-am:
+
+uninstall-am: uninstall-pkginc_libHEADERS
+
+.MAKE: install-am install-strip
+
+.PHONY: CTAGS GTAGS TAGS all all-am am--depfiles check check-am clean \
+ clean-generic clean-libtool clean-noinstLTLIBRARIES \
+ cscopelist-am ctags ctags-am distclean distclean-compile \
+ distclean-generic distclean-libtool distclean-tags distdir dvi \
+ dvi-am html html-am info info-am install install-am \
+ install-data install-data-am install-dvi install-dvi-am \
+ install-exec install-exec-am install-html install-html-am \
+ install-info install-info-am install-man install-pdf \
+ install-pdf-am install-pkginc_libHEADERS install-ps \
+ install-ps-am install-strip installcheck installcheck-am \
+ installdirs maintainer-clean maintainer-clean-generic \
+ mostlyclean mostlyclean-compile mostlyclean-generic \
+ mostlyclean-libtool pdf pdf-am ps ps-am tags tags-am uninstall \
+ uninstall-am uninstall-pkginc_libHEADERS
+
+.PRECIOUS: Makefile
+
+
+# Tell versions [3.59,3.63) of GNU make to not export all variables.
+# Otherwise a system limit (for SysV at least) may be exceeded.
+.NOEXPORT:
diff --git a/src/lib-old-stats/stats-connection.c b/src/lib-old-stats/stats-connection.c
new file mode 100644
index 0000000..24b5e73
--- /dev/null
+++ b/src/lib-old-stats/stats-connection.c
@@ -0,0 +1,119 @@
+/* Copyright (c) 2011-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "ioloop.h"
+#include "str.h"
+#include "master-service.h"
+#include "stats-connection.h"
+
+#include <unistd.h>
+#include <fcntl.h>
+
+#define STATS_EAGAIN_WARN_INTERVAL_SECS 30
+
+struct stats_connection {
+ int refcount;
+
+ int fd;
+ char *path;
+
+ bool open_failed;
+ time_t next_warning_timestamp;
+};
+
+static bool stats_connection_open(struct stats_connection *conn)
+{
+ if (conn->open_failed)
+ return FALSE;
+
+ conn->fd = open(conn->path, O_WRONLY | O_NONBLOCK);
+ if (conn->fd == -1) {
+ i_error("stats: open(%s) failed: %m", conn->path);
+ conn->open_failed = TRUE;
+ return FALSE;
+ }
+ return TRUE;
+}
+
+struct stats_connection *
+stats_connection_create(const char *path)
+{
+ struct stats_connection *conn;
+
+ conn = i_new(struct stats_connection, 1);
+ conn->refcount = 1;
+ conn->path = i_strdup(path);
+ (void)stats_connection_open(conn);
+ return conn;
+}
+
+void stats_connection_ref(struct stats_connection *conn)
+{
+ conn->refcount++;
+}
+
+void stats_connection_unref(struct stats_connection **_conn)
+{
+ struct stats_connection *conn = *_conn;
+
+ i_assert(conn->refcount > 0);
+ if (--conn->refcount > 0)
+ return;
+
+ *_conn = NULL;
+ i_close_fd_path(&conn->fd, conn->path);
+ i_free(conn->path);
+ i_free(conn);
+}
+
+int stats_connection_send(struct stats_connection *conn, const string_t *str)
+{
+ static bool pipe_warned = FALSE;
+ ssize_t ret;
+
+ /* if master process has been stopped (and restarted), don't even try
+ to notify the stats process anymore. even if one exists, it doesn't
+ know about us. */
+ if (master_service_is_master_stopped(master_service))
+ return -1;
+
+ if (conn->fd == -1) {
+ if (!stats_connection_open(conn))
+ return -1;
+ i_assert(conn->fd != -1);
+ }
+
+ if (str_len(str) > PIPE_BUF && !pipe_warned) {
+ i_warning("stats update sent more bytes that PIPE_BUF "
+ "(%zu > %u), this may break statistics",
+ str_len(str), (unsigned int)PIPE_BUF);
+ pipe_warned = TRUE;
+ }
+
+ ret = write(conn->fd, str_data(str), str_len(str));
+ if (ret == (ssize_t)str_len(str)) {
+ /* success */
+ return 0;
+ } else if (ret < 0 && errno == EAGAIN) {
+ /* stats process is busy */
+ if (ioloop_time > conn->next_warning_timestamp) {
+ i_warning("write(%s) failed: %m (stats process is busy)", conn->path);
+ conn->next_warning_timestamp = ioloop_time +
+ STATS_EAGAIN_WARN_INTERVAL_SECS;
+ }
+ return -1;
+ } else {
+ /* error - reconnect */
+ if (ret < 0) {
+ /* don't log EPIPE errors. they can happen when
+ Dovecot is stopped. */
+ if (errno != EPIPE)
+ i_error("write(%s) failed: %m", conn->path);
+ } else if ((size_t)ret != str_len(str))
+ i_error("write(%s) wrote partial update", conn->path);
+ if (close(conn->fd) < 0)
+ i_error("close(%s) failed: %m", conn->path);
+ conn->fd = -1;
+ return -1;
+ }
+}
diff --git a/src/lib-old-stats/stats-connection.h b/src/lib-old-stats/stats-connection.h
new file mode 100644
index 0000000..2295e09
--- /dev/null
+++ b/src/lib-old-stats/stats-connection.h
@@ -0,0 +1,11 @@
+#ifndef STATS_CONNECTION_H
+#define STATS_CONNECTION_H
+
+struct stats_connection *stats_connection_create(const char *path);
+void stats_connection_ref(struct stats_connection *conn);
+void stats_connection_unref(struct stats_connection **conn);
+
+/* Returns 0 on success, -1 on failure. */
+int stats_connection_send(struct stats_connection *conn, const string_t *str);
+
+#endif
diff --git a/src/lib-old-stats/stats-parser.c b/src/lib-old-stats/stats-parser.c
new file mode 100644
index 0000000..da009e9
--- /dev/null
+++ b/src/lib-old-stats/stats-parser.c
@@ -0,0 +1,178 @@
+/* Copyright (c) 2011-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "str.h"
+#include "time-util.h"
+#include "stats-parser.h"
+
+#define USECS_PER_SEC 1000000
+
+static bool stats_diff_timeval(struct timeval *dest,
+ const struct timeval *src1,
+ const struct timeval *src2)
+{
+ long long diff_usecs;
+
+ diff_usecs = timeval_diff_usecs(src2, src1);
+ if (diff_usecs < 0)
+ return FALSE;
+ dest->tv_sec = diff_usecs / USECS_PER_SEC;
+ dest->tv_usec = diff_usecs % USECS_PER_SEC;
+ return TRUE;
+}
+
+static bool
+stats_diff_uint32(uint32_t *dest, const uint32_t *src1, const uint32_t *src2)
+{
+ if (*src1 > *src2)
+ return FALSE;
+ *dest = *src2 - *src1;
+ return TRUE;
+}
+
+static bool
+stats_diff_uint64(uint64_t *dest, const uint64_t *src1, const uint64_t *src2)
+{
+ if (*src1 > *src2)
+ return FALSE;
+ *dest = *src2 - *src1;
+ return TRUE;
+}
+
+bool stats_parser_diff(const struct stats_parser_field *fields,
+ unsigned int fields_count,
+ const struct stats *stats1, const struct stats *stats2,
+ struct stats *diff_stats_r, const char **error_r)
+{
+ unsigned int i;
+
+ for (i = 0; i < fields_count; i++) {
+ unsigned int offset = fields[i].offset;
+ void *dest = PTR_OFFSET(diff_stats_r, offset);
+ const void *src1 = CONST_PTR_OFFSET(stats1, offset);
+ const void *src2 = CONST_PTR_OFFSET(stats2, offset);
+
+ switch (fields[i].type) {
+ case STATS_PARSER_TYPE_UINT:
+ switch (fields[i].size) {
+ case sizeof(uint32_t):
+ if (!stats_diff_uint32(dest, src1, src2)) {
+ *error_r = t_strdup_printf("%s %u < %u",
+ fields[i].name,
+ *(const uint32_t *)src2,
+ *(const uint32_t *)src1);
+ return FALSE;
+ }
+ break;
+ case sizeof(uint64_t):
+ if (!stats_diff_uint64(dest, src1, src2)) {
+ const uint64_t *n1 = src1, *n2 = src2;
+
+ *error_r = t_strdup_printf("%s %"PRIu64" < %"PRIu64,
+ fields[i].name, *n2, *n1);
+ return FALSE;
+ }
+ break;
+ default:
+ i_unreached();
+ }
+ break;
+ case STATS_PARSER_TYPE_TIMEVAL:
+ if (!stats_diff_timeval(dest, src1, src2)) {
+ const struct timeval *tv1 = src1, *tv2 = src2;
+
+ *error_r = t_strdup_printf("%s %ld.%d < %ld.%d",
+ fields[i].name,
+ (long)tv2->tv_sec, (int)tv2->tv_usec,
+ (long)tv1->tv_sec, (int)tv1->tv_usec);
+ return FALSE;
+ }
+ break;
+ }
+ }
+ return TRUE;
+}
+
+static void stats_timeval_add(struct timeval *dest, const struct timeval *src)
+{
+ dest->tv_sec += src->tv_sec;
+ dest->tv_usec += src->tv_usec;
+ if (dest->tv_usec > USECS_PER_SEC) {
+ dest->tv_usec -= USECS_PER_SEC;
+ dest->tv_sec++;
+ }
+}
+
+void stats_parser_add(const struct stats_parser_field *fields,
+ unsigned int fields_count,
+ struct stats *dest, const struct stats *src)
+{
+ unsigned int i;
+
+ for (i = 0; i < fields_count; i++) {
+ unsigned int offset = fields[i].offset;
+ void *f_dest = PTR_OFFSET(dest, offset);
+ const void *f_src = CONST_PTR_OFFSET(src, offset);
+
+ switch (fields[i].type) {
+ case STATS_PARSER_TYPE_UINT:
+ switch (fields[i].size) {
+ case sizeof(uint32_t): {
+ uint32_t *n_dest = f_dest;
+ const uint32_t *n_src = f_src;
+
+ *n_dest += *n_src;
+ break;
+ }
+ case sizeof(uint64_t): {
+ uint64_t *n_dest = f_dest;
+ const uint64_t *n_src = f_src;
+
+ *n_dest += *n_src;
+ break;
+ }
+ default:
+ i_unreached();
+ }
+ break;
+ case STATS_PARSER_TYPE_TIMEVAL:
+ stats_timeval_add(f_dest, f_src);
+ break;
+ }
+ }
+}
+
+void stats_parser_value(string_t *str,
+ const struct stats_parser_field *field,
+ const void *data)
+{
+ const void *ptr = CONST_PTR_OFFSET(data, field->offset);
+
+ switch (field->type) {
+ case STATS_PARSER_TYPE_UINT:
+ switch (field->size) {
+ case sizeof(uint32_t): {
+ const uint32_t *n = ptr;
+
+ str_printfa(str, "%u", *n);
+ break;
+ }
+ case sizeof(uint64_t): {
+ const uint64_t *n = ptr;
+
+ str_printfa(str, "%"PRIu64, *n);
+ break;
+ }
+ default:
+ i_unreached();
+ }
+ break;
+ case STATS_PARSER_TYPE_TIMEVAL: {
+ const struct timeval *tv = ptr;
+
+ str_printfa(str, "%"PRIdTIME_T".%u",
+ tv->tv_sec, (unsigned int)tv->tv_usec);
+ break;
+ }
+ }
+}
diff --git a/src/lib-old-stats/stats-parser.h b/src/lib-old-stats/stats-parser.h
new file mode 100644
index 0000000..ad701bf
--- /dev/null
+++ b/src/lib-old-stats/stats-parser.h
@@ -0,0 +1,29 @@
+#ifndef STATS_PARSER_H
+#define STATS_PARSER_H
+
+struct stats;
+
+enum stats_parser_type {
+ STATS_PARSER_TYPE_UINT,
+ STATS_PARSER_TYPE_TIMEVAL
+};
+
+struct stats_parser_field {
+ const char *name;
+ unsigned int offset;
+ unsigned int size;
+ enum stats_parser_type type;
+};
+
+bool stats_parser_diff(const struct stats_parser_field *fields,
+ unsigned int fields_count,
+ const struct stats *stats1, const struct stats *stats2,
+ struct stats *diff_stats_r, const char **error_r);
+void stats_parser_add(const struct stats_parser_field *fields,
+ unsigned int fields_count,
+ struct stats *dest, const struct stats *src);
+void stats_parser_value(string_t *str,
+ const struct stats_parser_field *field,
+ const void *data);
+
+#endif
diff --git a/src/lib-old-stats/stats.c b/src/lib-old-stats/stats.c
new file mode 100644
index 0000000..284de26
--- /dev/null
+++ b/src/lib-old-stats/stats.c
@@ -0,0 +1,229 @@
+/* Copyright (c) 2015-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "stats.h"
+
+struct stats_item {
+ struct stats_vfuncs v;
+ size_t pos;
+};
+
+static ARRAY(struct stats_item *) stats_items = ARRAY_INIT;
+static unsigned int stats_total_size = 0;
+static bool stats_allocated = FALSE;
+
+struct stats_item *stats_register(const struct stats_vfuncs *vfuncs)
+{
+ struct stats_item *item;
+
+ if (stats_allocated)
+ i_panic("stats_register() called after stats_alloc_size() was already called - this will break existing allocations");
+
+ if (!array_is_created(&stats_items))
+ i_array_init(&stats_items, 8);
+
+ item = i_new(struct stats_item, 1);
+ item->v = *vfuncs;
+ item->pos = stats_total_size;
+ array_push_back(&stats_items, &item);
+
+ stats_total_size += vfuncs->alloc_size();
+ return item;
+}
+
+static bool stats_item_find(struct stats_item *item, unsigned int *idx_r)
+{
+ struct stats_item *const *itemp;
+
+ array_foreach(&stats_items, itemp) {
+ if (*itemp == item) {
+ *idx_r = array_foreach_idx(&stats_items, itemp);
+ return TRUE;
+ }
+ }
+ return FALSE;
+}
+
+static struct stats_item *stats_item_find_by_name(const char *name)
+{
+ struct stats_item *item;
+
+ array_foreach_elem(&stats_items, item) {
+ if (strcmp(item->v.short_name, name) == 0)
+ return item;
+ }
+ return NULL;
+}
+
+void stats_unregister(struct stats_item **_item)
+{
+ struct stats_item *item = *_item;
+ unsigned int idx;
+
+ *_item = NULL;
+
+ if (!stats_item_find(item, &idx))
+ i_unreached();
+ array_delete(&stats_items, idx, 1);
+
+ i_free(item);
+ if (array_count(&stats_items) == 0) {
+ array_free(&stats_items);
+ /* all stats should have been freed by now. allow
+ re-registering and using stats. */
+ stats_allocated = FALSE;
+ }
+}
+
+struct stats *stats_alloc(pool_t pool)
+{
+ return p_malloc(pool, stats_alloc_size());
+}
+
+size_t stats_alloc_size(void)
+{
+ stats_allocated = TRUE;
+ return stats_total_size;
+}
+
+void stats_copy(struct stats *dest, const struct stats *src)
+{
+ memcpy(dest, src, stats_total_size);
+}
+
+unsigned int stats_field_count(void)
+{
+ struct stats_item *item;
+ unsigned int count = 0;
+
+ array_foreach_elem(&stats_items, item)
+ count += item->v.field_count();
+ return count;
+}
+
+const char *stats_field_name(unsigned int n)
+{
+ struct stats_item *item;
+ unsigned int i = 0, count;
+
+ array_foreach_elem(&stats_items, item) {
+ count = item->v.field_count();
+ if (i + count > n)
+ return item->v.field_name(n - i);
+ i += count;
+ }
+ i_unreached();
+}
+
+void stats_field_value(string_t *str, const struct stats *stats,
+ unsigned int n)
+{
+ struct stats_item *item;
+ unsigned int i = 0, count;
+
+ array_foreach_elem(&stats_items, item) {
+ count = item->v.field_count();
+ if (i + count > n) {
+ const void *item_stats =
+ CONST_PTR_OFFSET(stats, item->pos);
+ item->v.field_value(str, item_stats, n - i);
+ return;
+ }
+ i += count;
+ }
+ i_unreached();
+}
+
+bool stats_diff(const struct stats *stats1, const struct stats *stats2,
+ struct stats *diff_stats_r, const char **error_r)
+{
+ struct stats_item *item;
+ bool ret = TRUE;
+
+ array_foreach_elem(&stats_items, item) {
+ if (!item->v.diff(CONST_PTR_OFFSET(stats1, item->pos),
+ CONST_PTR_OFFSET(stats2, item->pos),
+ PTR_OFFSET(diff_stats_r, item->pos),
+ error_r))
+ ret = FALSE;
+ }
+ return ret;
+}
+
+void stats_add(struct stats *dest, const struct stats *src)
+{
+ struct stats_item *item;
+
+ array_foreach_elem(&stats_items, item) {
+ item->v.add(PTR_OFFSET(dest, item->pos),
+ CONST_PTR_OFFSET(src, item->pos));
+ }
+}
+
+bool stats_have_changed(const struct stats *prev, const struct stats *cur)
+{
+ struct stats_item *item;
+
+ array_foreach_elem(&stats_items, item) {
+ if (item->v.have_changed(CONST_PTR_OFFSET(prev, item->pos),
+ CONST_PTR_OFFSET(cur, item->pos)))
+ return TRUE;
+ }
+ return FALSE;
+}
+
+void stats_export(buffer_t *buf, const struct stats *stats)
+{
+ struct stats_item *item;
+
+ array_foreach_elem(&stats_items, item) {
+ buffer_append(buf, item->v.short_name,
+ strlen(item->v.short_name)+1);
+ item->v.export(buf, CONST_PTR_OFFSET(stats, item->pos));
+ }
+}
+
+bool stats_import(const unsigned char *data, size_t size,
+ const struct stats *old_stats, struct stats *stats,
+ const char **error_r)
+{
+ struct stats_item *item;
+ const unsigned char *p;
+ size_t pos;
+
+ memcpy(stats, old_stats, stats_total_size);
+ while (size > 0) {
+ const char *next_name = (const void *)data;
+
+ p = memchr(data, '\0', size);
+ if (p == NULL) {
+ *error_r = "Expected name, but NUL is missing";
+ return FALSE;
+ }
+ item = stats_item_find_by_name(next_name);
+ if (item == NULL) {
+ *error_r = t_strdup_printf("Unknown stats name: '%s'", next_name);
+ return FALSE;
+ }
+ size -= (p+1) - data;
+ data = p+1;
+ if (!item->v.import(data, size, &pos,
+ PTR_OFFSET(stats, item->pos), error_r))
+ return FALSE;
+ i_assert(pos <= size);
+ data += pos;
+ size -= pos;
+ }
+ return TRUE;
+}
+
+void *stats_fill_ptr(struct stats *stats, struct stats_item *item)
+{
+ return PTR_OFFSET(stats, item->pos);
+}
+
+void stats_reset(struct stats *stats)
+{
+ memset(stats, 0, stats_total_size);
+}
diff --git a/src/lib-old-stats/stats.h b/src/lib-old-stats/stats.h
new file mode 100644
index 0000000..0cd0dc3
--- /dev/null
+++ b/src/lib-old-stats/stats.h
@@ -0,0 +1,71 @@
+#ifndef STATS_H
+#define STATS_H
+
+struct stats;
+struct stats_item;
+
+struct stats_vfuncs {
+ const char *short_name;
+
+ size_t (*alloc_size)(void);
+ unsigned int (*field_count)(void);
+ const char *(*field_name)(unsigned int n);
+ void (*field_value)(string_t *str, const struct stats *stats,
+ unsigned int n);
+
+ bool (*diff)(const struct stats *stats1, const struct stats *stats2,
+ struct stats *diff_stats_r, const char **error_r);
+ void (*add)(struct stats *dest, const struct stats *src);
+ bool (*have_changed)(const struct stats *prev, const struct stats *cur);
+
+ void (*export)(buffer_t *buf, const struct stats *stats);
+ bool (*import)(const unsigned char *data, size_t size, size_t *pos_r,
+ struct stats *stats, const char **error_r);
+};
+
+struct stats_item *stats_register(const struct stats_vfuncs *vfuncs);
+void stats_unregister(struct stats_item **item);
+
+/* Allocate struct stats from a given pool. */
+struct stats *stats_alloc(pool_t pool);
+/* Returns the number of bytes allocated to stats. */
+size_t stats_alloc_size(void);
+/* Copy all stats from src to dest. */
+void stats_copy(struct stats *dest, const struct stats *src);
+
+/* Returns the number of stats fields. */
+unsigned int stats_field_count(void);
+/* Returns the name of a stats field (exported to doveadm). */
+const char *stats_field_name(unsigned int n);
+/* Returns the value of a stats field as a string (exported to doveadm). */
+void stats_field_value(string_t *str, const struct stats *stats,
+ unsigned int n);
+
+/* Return diff_stats_r->field = stats2->field - stats1->field.
+ diff1 is supposed to have smaller values than diff2. Returns TRUE if this
+ is so, FALSE if not */
+bool stats_diff(const struct stats *stats1, const struct stats *stats2,
+ struct stats *diff_stats_r, const char **error_r);
+/* dest->field += src->field */
+void stats_add(struct stats *dest, const struct stats *src);
+/* Returns TRUE if any fields have changed in cur since prev in a way that
+ a plugin should send the updated statistics to the stats process. Not all
+ fields necessarily require sending an update. */
+bool stats_have_changed(const struct stats *prev, const struct stats *cur);
+
+/* Export stats into a buffer in binary format. */
+void stats_export(buffer_t *buf, const struct stats *stats);
+/* Import stats from a buffer. The buffer doesn't need to contain an update to
+ all the stats items - old_stats are used for that item in such case.
+ Currently it's not allowed to have unknown items in the buffer. */
+bool stats_import(const unsigned char *data, size_t size,
+ const struct stats *old_stats, struct stats *stats,
+ const char **error_r);
+/* Return a pointer to stats where the specified item starts. The returned
+ pointer can be used to fill up the item-specific stats (up to its
+ alloc_size() number of bytes). */
+void *stats_fill_ptr(struct stats *stats, struct stats_item *item);
+
+void stats_reset(struct stats *stats);
+
+#endif
diff --git a/src/lib-otp/Makefile.am b/src/lib-otp/Makefile.am
new file mode 100644
index 0000000..5b34fba
--- /dev/null
+++ b/src/lib-otp/Makefile.am
@@ -0,0 +1,17 @@
+noinst_LTLIBRARIES = libotp.la
+
+AM_CPPFLAGS = \
+ -I$(top_srcdir)/src/lib
+
+libotp_la_SOURCES = \
+ otp-dictionary.c \
+ otp-hash.c \
+ otp-parity.c \
+ otp-parse.c
+
+noinst_HEADERS = \
+ otp.h \
+ otp-dictionary.h \
+ otp-hash.h \
+ otp-parity.h \
+ otp-parse.h
diff --git a/src/lib-otp/Makefile.in b/src/lib-otp/Makefile.in
new file mode 100644
index 0000000..7f80c7a
--- /dev/null
+++ b/src/lib-otp/Makefile.in
@@ -0,0 +1,771 @@
+# Makefile.in generated by automake 1.16.1 from Makefile.am.
+# @configure_input@
+
+# Copyright (C) 1994-2018 Free Software Foundation, Inc.
+
+# This Makefile.in is free software; the Free Software Foundation
+# gives unlimited permission to copy and/or distribute it,
+# with or without modifications, as long as this notice is preserved.
+
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY, to the extent permitted by law; without
+# even the implied warranty of MERCHANTABILITY or FITNESS FOR A
+# PARTICULAR PURPOSE.
+
+@SET_MAKE@
+
+
+VPATH = @srcdir@
+am__is_gnu_make = { \
+ if test -z '$(MAKELEVEL)'; then \
+ false; \
+ elif test -n '$(MAKE_HOST)'; then \
+ true; \
+ elif test -n '$(MAKE_VERSION)' && test -n '$(CURDIR)'; then \
+ true; \
+ else \
+ false; \
+ fi; \
+}
+am__make_running_with_option = \
+ case $${target_option-} in \
+ ?) ;; \
+ *) echo "am__make_running_with_option: internal error: invalid" \
+ "target option '$${target_option-}' specified" >&2; \
+ exit 1;; \
+ esac; \
+ has_opt=no; \
+ sane_makeflags=$$MAKEFLAGS; \
+ if $(am__is_gnu_make); then \
+ sane_makeflags=$$MFLAGS; \
+ else \
+ case $$MAKEFLAGS in \
+ *\\[\ \ ]*) \
+ bs=\\; \
+ sane_makeflags=`printf '%s\n' "$$MAKEFLAGS" \
+ | sed "s/$$bs$$bs[$$bs $$bs ]*//g"`;; \
+ esac; \
+ fi; \
+ skip_next=no; \
+ strip_trailopt () \
+ { \
+ flg=`printf '%s\n' "$$flg" | sed "s/$$1.*$$//"`; \
+ }; \
+ for flg in $$sane_makeflags; do \
+ test $$skip_next = yes && { skip_next=no; continue; }; \
+ case $$flg in \
+ *=*|--*) continue;; \
+ -*I) strip_trailopt 'I'; skip_next=yes;; \
+ -*I?*) strip_trailopt 'I';; \
+ -*O) strip_trailopt 'O'; skip_next=yes;; \
+ -*O?*) strip_trailopt 'O';; \
+ -*l) strip_trailopt 'l'; skip_next=yes;; \
+ -*l?*) strip_trailopt 'l';; \
+ -[dEDm]) skip_next=yes;; \
+ -[JT]) skip_next=yes;; \
+ esac; \
+ case $$flg in \
+ *$$target_option*) has_opt=yes; break;; \
+ esac; \
+ done; \
+ test $$has_opt = yes
+am__make_dryrun = (target_option=n; $(am__make_running_with_option))
+am__make_keepgoing = (target_option=k; $(am__make_running_with_option))
+pkgdatadir = $(datadir)/@PACKAGE@
+pkgincludedir = $(includedir)/@PACKAGE@
+pkglibdir = $(libdir)/@PACKAGE@
+pkglibexecdir = $(libexecdir)/@PACKAGE@
+am__cd = CDPATH="$${ZSH_VERSION+.}$(PATH_SEPARATOR)" && cd
+install_sh_DATA = $(install_sh) -c -m 644
+install_sh_PROGRAM = $(install_sh) -c
+install_sh_SCRIPT = $(install_sh) -c
+INSTALL_HEADER = $(INSTALL_DATA)
+transform = $(program_transform_name)
+NORMAL_INSTALL = :
+PRE_INSTALL = :
+POST_INSTALL = :
+NORMAL_UNINSTALL = :
+PRE_UNINSTALL = :
+POST_UNINSTALL = :
+build_triplet = @build@
+host_triplet = @host@
+subdir = src/lib-otp
+ACLOCAL_M4 = $(top_srcdir)/aclocal.m4
+am__aclocal_m4_deps = $(top_srcdir)/m4/ac_checktype2.m4 \
+ $(top_srcdir)/m4/ac_typeof.m4 $(top_srcdir)/m4/arc4random.m4 \
+ $(top_srcdir)/m4/blockdev.m4 $(top_srcdir)/m4/c99_vsnprintf.m4 \
+ $(top_srcdir)/m4/clock_gettime.m4 $(top_srcdir)/m4/crypt.m4 \
+ $(top_srcdir)/m4/crypt_xpg6.m4 $(top_srcdir)/m4/dbqlk.m4 \
+ $(top_srcdir)/m4/dirent_dtype.m4 $(top_srcdir)/m4/dovecot.m4 \
+ $(top_srcdir)/m4/fd_passing.m4 $(top_srcdir)/m4/fdatasync.m4 \
+ $(top_srcdir)/m4/flexible_array_member.m4 \
+ $(top_srcdir)/m4/glibc.m4 $(top_srcdir)/m4/gmtime_max.m4 \
+ $(top_srcdir)/m4/gmtime_tm_gmtoff.m4 \
+ $(top_srcdir)/m4/ioloop.m4 $(top_srcdir)/m4/iovec.m4 \
+ $(top_srcdir)/m4/ipv6.m4 $(top_srcdir)/m4/libcap.m4 \
+ $(top_srcdir)/m4/libtool.m4 $(top_srcdir)/m4/libwrap.m4 \
+ $(top_srcdir)/m4/linux_mremap.m4 $(top_srcdir)/m4/ltoptions.m4 \
+ $(top_srcdir)/m4/ltsugar.m4 $(top_srcdir)/m4/ltversion.m4 \
+ $(top_srcdir)/m4/lt~obsolete.m4 $(top_srcdir)/m4/mmap_write.m4 \
+ $(top_srcdir)/m4/mntctl.m4 $(top_srcdir)/m4/modules.m4 \
+ $(top_srcdir)/m4/notify.m4 $(top_srcdir)/m4/nsl.m4 \
+ $(top_srcdir)/m4/off_t_max.m4 $(top_srcdir)/m4/pkg.m4 \
+ $(top_srcdir)/m4/pr_set_dumpable.m4 \
+ $(top_srcdir)/m4/q_quotactl.m4 $(top_srcdir)/m4/quota.m4 \
+ $(top_srcdir)/m4/random.m4 $(top_srcdir)/m4/rlimit.m4 \
+ $(top_srcdir)/m4/sendfile.m4 $(top_srcdir)/m4/size_t_signed.m4 \
+ $(top_srcdir)/m4/sockpeercred.m4 $(top_srcdir)/m4/sql.m4 \
+ $(top_srcdir)/m4/ssl.m4 $(top_srcdir)/m4/st_tim.m4 \
+ $(top_srcdir)/m4/static_array.m4 $(top_srcdir)/m4/test_with.m4 \
+ $(top_srcdir)/m4/time_t.m4 $(top_srcdir)/m4/typeof.m4 \
+ $(top_srcdir)/m4/typeof_dev_t.m4 \
+ $(top_srcdir)/m4/uoff_t_max.m4 $(top_srcdir)/m4/vararg.m4 \
+ $(top_srcdir)/m4/want_apparmor.m4 \
+ $(top_srcdir)/m4/want_bsdauth.m4 \
+ $(top_srcdir)/m4/want_bzlib.m4 \
+ $(top_srcdir)/m4/want_cassandra.m4 \
+ $(top_srcdir)/m4/want_cdb.m4 \
+ $(top_srcdir)/m4/want_checkpassword.m4 \
+ $(top_srcdir)/m4/want_clucene.m4 $(top_srcdir)/m4/want_db.m4 \
+ $(top_srcdir)/m4/want_gssapi.m4 $(top_srcdir)/m4/want_icu.m4 \
+ $(top_srcdir)/m4/want_ldap.m4 $(top_srcdir)/m4/want_lua.m4 \
+ $(top_srcdir)/m4/want_lz4.m4 $(top_srcdir)/m4/want_lzma.m4 \
+ $(top_srcdir)/m4/want_mysql.m4 $(top_srcdir)/m4/want_pam.m4 \
+ $(top_srcdir)/m4/want_passwd.m4 $(top_srcdir)/m4/want_pgsql.m4 \
+ $(top_srcdir)/m4/want_prefetch.m4 \
+ $(top_srcdir)/m4/want_shadow.m4 \
+ $(top_srcdir)/m4/want_sodium.m4 $(top_srcdir)/m4/want_solr.m4 \
+ $(top_srcdir)/m4/want_sqlite.m4 \
+ $(top_srcdir)/m4/want_stemmer.m4 \
+ $(top_srcdir)/m4/want_systemd.m4 \
+ $(top_srcdir)/m4/want_textcat.m4 \
+ $(top_srcdir)/m4/want_unwind.m4 $(top_srcdir)/m4/want_zlib.m4 \
+ $(top_srcdir)/m4/want_zstd.m4 $(top_srcdir)/configure.ac
+am__configure_deps = $(am__aclocal_m4_deps) $(CONFIGURE_DEPENDENCIES) \
+ $(ACLOCAL_M4)
+DIST_COMMON = $(srcdir)/Makefile.am $(noinst_HEADERS) \
+ $(am__DIST_COMMON)
+mkinstalldirs = $(install_sh) -d
+CONFIG_HEADER = $(top_builddir)/config.h
+CONFIG_CLEAN_FILES =
+CONFIG_CLEAN_VPATH_FILES =
+LTLIBRARIES = $(noinst_LTLIBRARIES)
+libotp_la_LIBADD =
+am_libotp_la_OBJECTS = otp-dictionary.lo otp-hash.lo otp-parity.lo \
+ otp-parse.lo
+libotp_la_OBJECTS = $(am_libotp_la_OBJECTS)
+AM_V_lt = $(am__v_lt_@AM_V@)
+am__v_lt_ = $(am__v_lt_@AM_DEFAULT_V@)
+am__v_lt_0 = --silent
+am__v_lt_1 =
+AM_V_P = $(am__v_P_@AM_V@)
+am__v_P_ = $(am__v_P_@AM_DEFAULT_V@)
+am__v_P_0 = false
+am__v_P_1 = :
+AM_V_GEN = $(am__v_GEN_@AM_V@)
+am__v_GEN_ = $(am__v_GEN_@AM_DEFAULT_V@)
+am__v_GEN_0 = @echo " GEN " $@;
+am__v_GEN_1 =
+AM_V_at = $(am__v_at_@AM_V@)
+am__v_at_ = $(am__v_at_@AM_DEFAULT_V@)
+am__v_at_0 = @
+am__v_at_1 =
+DEFAULT_INCLUDES = -I.@am__isrc@ -I$(top_builddir)
+depcomp = $(SHELL) $(top_srcdir)/depcomp
+am__maybe_remake_depfiles = depfiles
+am__depfiles_remade = ./$(DEPDIR)/otp-dictionary.Plo \
+ ./$(DEPDIR)/otp-hash.Plo ./$(DEPDIR)/otp-parity.Plo \
+ ./$(DEPDIR)/otp-parse.Plo
+am__mv = mv -f
+COMPILE = $(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) \
+ $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS)
+LTCOMPILE = $(LIBTOOL) $(AM_V_lt) --tag=CC $(AM_LIBTOOLFLAGS) \
+ $(LIBTOOLFLAGS) --mode=compile $(CC) $(DEFS) \
+ $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) $(CPPFLAGS) \
+ $(AM_CFLAGS) $(CFLAGS)
+AM_V_CC = $(am__v_CC_@AM_V@)
+am__v_CC_ = $(am__v_CC_@AM_DEFAULT_V@)
+am__v_CC_0 = @echo " CC " $@;
+am__v_CC_1 =
+CCLD = $(CC)
+LINK = $(LIBTOOL) $(AM_V_lt) --tag=CC $(AM_LIBTOOLFLAGS) \
+ $(LIBTOOLFLAGS) --mode=link $(CCLD) $(AM_CFLAGS) $(CFLAGS) \
+ $(AM_LDFLAGS) $(LDFLAGS) -o $@
+AM_V_CCLD = $(am__v_CCLD_@AM_V@)
+am__v_CCLD_ = $(am__v_CCLD_@AM_DEFAULT_V@)
+am__v_CCLD_0 = @echo " CCLD " $@;
+am__v_CCLD_1 =
+SOURCES = $(libotp_la_SOURCES)
+DIST_SOURCES = $(libotp_la_SOURCES)
+am__can_run_installinfo = \
+ case $$AM_UPDATE_INFO_DIR in \
+ n|no|NO) false;; \
+ *) (install-info --version) >/dev/null 2>&1;; \
+ esac
+HEADERS = $(noinst_HEADERS)
+am__tagged_files = $(HEADERS) $(SOURCES) $(TAGS_FILES) $(LISP)
+# Read a list of newline-separated strings from the standard input,
+# and print each of them once, without duplicates. Input order is
+# *not* preserved.
+am__uniquify_input = $(AWK) '\
+ BEGIN { nonempty = 0; } \
+ { items[$$0] = 1; nonempty = 1; } \
+ END { if (nonempty) { for (i in items) print i; }; } \
+'
+# Make sure the list of sources is unique. This is necessary because,
+# e.g., the same source file might be shared among _SOURCES variables
+# for different programs/libraries.
+am__define_uniq_tagged_files = \
+ list='$(am__tagged_files)'; \
+ unique=`for i in $$list; do \
+ if test -f "$$i"; then echo $$i; else echo $(srcdir)/$$i; fi; \
+ done | $(am__uniquify_input)`
+ETAGS = etags
+CTAGS = ctags
+am__DIST_COMMON = $(srcdir)/Makefile.in $(top_srcdir)/depcomp
+DISTFILES = $(DIST_COMMON) $(DIST_SOURCES) $(TEXINFOS) $(EXTRA_DIST)
+ACLOCAL = @ACLOCAL@
+ACLOCAL_AMFLAGS = @ACLOCAL_AMFLAGS@
+AMTAR = @AMTAR@
+AM_DEFAULT_VERBOSITY = @AM_DEFAULT_VERBOSITY@
+APPARMOR_LIBS = @APPARMOR_LIBS@
+AR = @AR@
+AUTH_CFLAGS = @AUTH_CFLAGS@
+AUTH_LIBS = @AUTH_LIBS@
+AUTOCONF = @AUTOCONF@
+AUTOHEADER = @AUTOHEADER@
+AUTOMAKE = @AUTOMAKE@
+AWK = @AWK@
+BINARY_CFLAGS = @BINARY_CFLAGS@
+BINARY_LDFLAGS = @BINARY_LDFLAGS@
+BISON = @BISON@
+CASSANDRA_CFLAGS = @CASSANDRA_CFLAGS@
+CASSANDRA_LIBS = @CASSANDRA_LIBS@
+CC = @CC@
+CCDEPMODE = @CCDEPMODE@
+CDB_LIBS = @CDB_LIBS@
+CFLAGS = @CFLAGS@
+CLUCENE_CFLAGS = @CLUCENE_CFLAGS@
+CLUCENE_LIBS = @CLUCENE_LIBS@
+COMPRESS_LIBS = @COMPRESS_LIBS@
+CPP = @CPP@
+CPPFLAGS = @CPPFLAGS@
+CRYPT_LIBS = @CRYPT_LIBS@
+CXX = @CXX@
+CXXCPP = @CXXCPP@
+CXXDEPMODE = @CXXDEPMODE@
+CXXFLAGS = @CXXFLAGS@
+CYGPATH_W = @CYGPATH_W@
+DEFS = @DEFS@
+DEPDIR = @DEPDIR@
+DICT_LIBS = @DICT_LIBS@
+DLLIB = @DLLIB@
+DLLTOOL = @DLLTOOL@
+DSYMUTIL = @DSYMUTIL@
+DUMPBIN = @DUMPBIN@
+ECHO_C = @ECHO_C@
+ECHO_N = @ECHO_N@
+ECHO_T = @ECHO_T@
+EGREP = @EGREP@
+EXEEXT = @EXEEXT@
+FGREP = @FGREP@
+FLEX = @FLEX@
+FUZZER_CPPFLAGS = @FUZZER_CPPFLAGS@
+FUZZER_LDFLAGS = @FUZZER_LDFLAGS@
+GREP = @GREP@
+INSTALL = @INSTALL@
+INSTALL_DATA = @INSTALL_DATA@
+INSTALL_PROGRAM = @INSTALL_PROGRAM@
+INSTALL_SCRIPT = @INSTALL_SCRIPT@
+INSTALL_STRIP_PROGRAM = @INSTALL_STRIP_PROGRAM@
+KRB5CONFIG = @KRB5CONFIG@
+KRB5_CFLAGS = @KRB5_CFLAGS@
+KRB5_LIBS = @KRB5_LIBS@
+LD = @LD@
+LDAP_LIBS = @LDAP_LIBS@
+LDFLAGS = @LDFLAGS@
+LD_NO_WHOLE_ARCHIVE = @LD_NO_WHOLE_ARCHIVE@
+LD_WHOLE_ARCHIVE = @LD_WHOLE_ARCHIVE@
+LIBCAP = @LIBCAP@
+LIBDOVECOT = @LIBDOVECOT@
+LIBDOVECOT_COMPRESS = @LIBDOVECOT_COMPRESS@
+LIBDOVECOT_DEPS = @LIBDOVECOT_DEPS@
+LIBDOVECOT_DSYNC = @LIBDOVECOT_DSYNC@
+LIBDOVECOT_LA_LIBS = @LIBDOVECOT_LA_LIBS@
+LIBDOVECOT_LDA = @LIBDOVECOT_LDA@
+LIBDOVECOT_LDAP = @LIBDOVECOT_LDAP@
+LIBDOVECOT_LIBFTS = @LIBDOVECOT_LIBFTS@
+LIBDOVECOT_LIBFTS_DEPS = @LIBDOVECOT_LIBFTS_DEPS@
+LIBDOVECOT_LOGIN = @LIBDOVECOT_LOGIN@
+LIBDOVECOT_LUA = @LIBDOVECOT_LUA@
+LIBDOVECOT_LUA_DEPS = @LIBDOVECOT_LUA_DEPS@
+LIBDOVECOT_SQL = @LIBDOVECOT_SQL@
+LIBDOVECOT_STORAGE = @LIBDOVECOT_STORAGE@
+LIBDOVECOT_STORAGE_DEPS = @LIBDOVECOT_STORAGE_DEPS@
+LIBEXTTEXTCAT_CFLAGS = @LIBEXTTEXTCAT_CFLAGS@
+LIBEXTTEXTCAT_LIBS = @LIBEXTTEXTCAT_LIBS@
+LIBICONV = @LIBICONV@
+LIBICU_CFLAGS = @LIBICU_CFLAGS@
+LIBICU_LIBS = @LIBICU_LIBS@
+LIBOBJS = @LIBOBJS@
+LIBS = @LIBS@
+LIBSODIUM_CFLAGS = @LIBSODIUM_CFLAGS@
+LIBSODIUM_LIBS = @LIBSODIUM_LIBS@
+LIBTIRPC_CFLAGS = @LIBTIRPC_CFLAGS@
+LIBTIRPC_LIBS = @LIBTIRPC_LIBS@
+LIBTOOL = @LIBTOOL@
+LIBUNWIND_CFLAGS = @LIBUNWIND_CFLAGS@
+LIBUNWIND_LIBS = @LIBUNWIND_LIBS@
+LIBWRAP_LIBS = @LIBWRAP_LIBS@
+LINKED_STORAGE_LDADD = @LINKED_STORAGE_LDADD@
+LIPO = @LIPO@
+LN_S = @LN_S@
+LTLIBICONV = @LTLIBICONV@
+LTLIBOBJS = @LTLIBOBJS@
+LT_SYS_LIBRARY_PATH = @LT_SYS_LIBRARY_PATH@
+LUA_CFLAGS = @LUA_CFLAGS@
+LUA_LIBS = @LUA_LIBS@
+MAINT = @MAINT@
+MAKEINFO = @MAKEINFO@
+MANIFEST_TOOL = @MANIFEST_TOOL@
+MKDIR_P = @MKDIR_P@
+MODULE_LIBS = @MODULE_LIBS@
+MODULE_SUFFIX = @MODULE_SUFFIX@
+MYSQL_CFLAGS = @MYSQL_CFLAGS@
+MYSQL_CONFIG = @MYSQL_CONFIG@
+MYSQL_LIBS = @MYSQL_LIBS@
+NM = @NM@
+NMEDIT = @NMEDIT@
+NOPLUGIN_LDFLAGS = @NOPLUGIN_LDFLAGS@
+OBJDUMP = @OBJDUMP@
+OBJEXT = @OBJEXT@
+OTOOL = @OTOOL@
+OTOOL64 = @OTOOL64@
+PACKAGE = @PACKAGE@
+PACKAGE_BUGREPORT = @PACKAGE_BUGREPORT@
+PACKAGE_NAME = @PACKAGE_NAME@
+PACKAGE_STRING = @PACKAGE_STRING@
+PACKAGE_TARNAME = @PACKAGE_TARNAME@
+PACKAGE_URL = @PACKAGE_URL@
+PACKAGE_VERSION = @PACKAGE_VERSION@
+PANDOC = @PANDOC@
+PATH_SEPARATOR = @PATH_SEPARATOR@
+PGSQL_CFLAGS = @PGSQL_CFLAGS@
+PGSQL_LIBS = @PGSQL_LIBS@
+PG_CONFIG = @PG_CONFIG@
+PIE_CFLAGS = @PIE_CFLAGS@
+PIE_LDFLAGS = @PIE_LDFLAGS@
+PKG_CONFIG = @PKG_CONFIG@
+PKG_CONFIG_LIBDIR = @PKG_CONFIG_LIBDIR@
+PKG_CONFIG_PATH = @PKG_CONFIG_PATH@
+QUOTA_LIBS = @QUOTA_LIBS@
+RANLIB = @RANLIB@
+RELRO_LDFLAGS = @RELRO_LDFLAGS@
+RPCGEN = @RPCGEN@
+RUN_TEST = @RUN_TEST@
+SED = @SED@
+SETTING_FILES = @SETTING_FILES@
+SET_MAKE = @SET_MAKE@
+SHELL = @SHELL@
+SQLITE_CFLAGS = @SQLITE_CFLAGS@
+SQLITE_LIBS = @SQLITE_LIBS@
+SQL_CFLAGS = @SQL_CFLAGS@
+SQL_LIBS = @SQL_LIBS@
+SSL_CFLAGS = @SSL_CFLAGS@
+SSL_LIBS = @SSL_LIBS@
+STRIP = @STRIP@
+SYSTEMD_CFLAGS = @SYSTEMD_CFLAGS@
+SYSTEMD_LIBS = @SYSTEMD_LIBS@
+VALGRIND = @VALGRIND@
+VERSION = @VERSION@
+ZSTD_CFLAGS = @ZSTD_CFLAGS@
+ZSTD_LIBS = @ZSTD_LIBS@
+abs_builddir = @abs_builddir@
+abs_srcdir = @abs_srcdir@
+abs_top_builddir = @abs_top_builddir@
+abs_top_srcdir = @abs_top_srcdir@
+ac_ct_AR = @ac_ct_AR@
+ac_ct_CC = @ac_ct_CC@
+ac_ct_CXX = @ac_ct_CXX@
+ac_ct_DUMPBIN = @ac_ct_DUMPBIN@
+am__include = @am__include@
+am__leading_dot = @am__leading_dot@
+am__quote = @am__quote@
+am__tar = @am__tar@
+am__untar = @am__untar@
+bindir = @bindir@
+build = @build@
+build_alias = @build_alias@
+build_cpu = @build_cpu@
+build_os = @build_os@
+build_vendor = @build_vendor@
+builddir = @builddir@
+datadir = @datadir@
+datarootdir = @datarootdir@
+dict_drivers = @dict_drivers@
+docdir = @docdir@
+dvidir = @dvidir@
+exec_prefix = @exec_prefix@
+host = @host@
+host_alias = @host_alias@
+host_cpu = @host_cpu@
+host_os = @host_os@
+host_vendor = @host_vendor@
+htmldir = @htmldir@
+includedir = @includedir@
+infodir = @infodir@
+install_sh = @install_sh@
+libdir = @libdir@
+libexecdir = @libexecdir@
+localedir = @localedir@
+localstatedir = @localstatedir@
+mandir = @mandir@
+mkdir_p = @mkdir_p@
+moduledir = @moduledir@
+oldincludedir = @oldincludedir@
+pdfdir = @pdfdir@
+prefix = @prefix@
+program_transform_name = @program_transform_name@
+psdir = @psdir@
+rundir = @rundir@
+runstatedir = @runstatedir@
+sbindir = @sbindir@
+sharedstatedir = @sharedstatedir@
+sql_drivers = @sql_drivers@
+srcdir = @srcdir@
+ssldir = @ssldir@
+statedir = @statedir@
+sysconfdir = @sysconfdir@
+systemdservicetype = @systemdservicetype@
+systemdsystemunitdir = @systemdsystemunitdir@
+target_alias = @target_alias@
+top_build_prefix = @top_build_prefix@
+top_builddir = @top_builddir@
+top_srcdir = @top_srcdir@
+noinst_LTLIBRARIES = libotp.la
+AM_CPPFLAGS = \
+ -I$(top_srcdir)/src/lib
+
+libotp_la_SOURCES = \
+ otp-dictionary.c \
+ otp-hash.c \
+ otp-parity.c \
+ otp-parse.c
+
+noinst_HEADERS = \
+ otp.h \
+ otp-dictionary.h \
+ otp-hash.h \
+ otp-parity.h \
+ otp-parse.h
+
+all: all-am
+
+.SUFFIXES:
+.SUFFIXES: .c .lo .o .obj
+$(srcdir)/Makefile.in: @MAINTAINER_MODE_TRUE@ $(srcdir)/Makefile.am $(am__configure_deps)
+ @for dep in $?; do \
+ case '$(am__configure_deps)' in \
+ *$$dep*) \
+ ( cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh ) \
+ && { if test -f $@; then exit 0; else break; fi; }; \
+ exit 1;; \
+ esac; \
+ done; \
+ echo ' cd $(top_srcdir) && $(AUTOMAKE) --foreign src/lib-otp/Makefile'; \
+ $(am__cd) $(top_srcdir) && \
+ $(AUTOMAKE) --foreign src/lib-otp/Makefile
+Makefile: $(srcdir)/Makefile.in $(top_builddir)/config.status
+ @case '$?' in \
+ *config.status*) \
+ cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh;; \
+ *) \
+ echo ' cd $(top_builddir) && $(SHELL) ./config.status $(subdir)/$@ $(am__maybe_remake_depfiles)'; \
+ cd $(top_builddir) && $(SHELL) ./config.status $(subdir)/$@ $(am__maybe_remake_depfiles);; \
+ esac;
+
+$(top_builddir)/config.status: $(top_srcdir)/configure $(CONFIG_STATUS_DEPENDENCIES)
+ cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh
+
+$(top_srcdir)/configure: @MAINTAINER_MODE_TRUE@ $(am__configure_deps)
+ cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh
+$(ACLOCAL_M4): @MAINTAINER_MODE_TRUE@ $(am__aclocal_m4_deps)
+ cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh
+$(am__aclocal_m4_deps):
+
+clean-noinstLTLIBRARIES:
+ -test -z "$(noinst_LTLIBRARIES)" || rm -f $(noinst_LTLIBRARIES)
+ @list='$(noinst_LTLIBRARIES)'; \
+ locs=`for p in $$list; do echo $$p; done | \
+ sed 's|^[^/]*$$|.|; s|/[^/]*$$||; s|$$|/so_locations|' | \
+ sort -u`; \
+ test -z "$$locs" || { \
+ echo rm -f $${locs}; \
+ rm -f $${locs}; \
+ }
+
+libotp.la: $(libotp_la_OBJECTS) $(libotp_la_DEPENDENCIES) $(EXTRA_libotp_la_DEPENDENCIES)
+ $(AM_V_CCLD)$(LINK) $(libotp_la_OBJECTS) $(libotp_la_LIBADD) $(LIBS)
+
+mostlyclean-compile:
+ -rm -f *.$(OBJEXT)
+
+distclean-compile:
+ -rm -f *.tab.c
+
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/otp-dictionary.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/otp-hash.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/otp-parity.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/otp-parse.Plo@am__quote@ # am--include-marker
+
+$(am__depfiles_remade):
+ @$(MKDIR_P) $(@D)
+ @echo '# dummy' >$@-t && $(am__mv) $@-t $@
+
+am--depfiles: $(am__depfiles_remade)
+
+.c.o:
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(COMPILE) -MT $@ -MD -MP -MF $(DEPDIR)/$*.Tpo -c -o $@ $<
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/$*.Tpo $(DEPDIR)/$*.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='$<' object='$@' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(COMPILE) -c -o $@ $<
+
+.c.obj:
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(COMPILE) -MT $@ -MD -MP -MF $(DEPDIR)/$*.Tpo -c -o $@ `$(CYGPATH_W) '$<'`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/$*.Tpo $(DEPDIR)/$*.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='$<' object='$@' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(COMPILE) -c -o $@ `$(CYGPATH_W) '$<'`
+
+.c.lo:
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(LTCOMPILE) -MT $@ -MD -MP -MF $(DEPDIR)/$*.Tpo -c -o $@ $<
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/$*.Tpo $(DEPDIR)/$*.Plo
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='$<' object='$@' libtool=yes @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(LTCOMPILE) -c -o $@ $<
+
+mostlyclean-libtool:
+ -rm -f *.lo
+
+clean-libtool:
+ -rm -rf .libs _libs
+
+ID: $(am__tagged_files)
+ $(am__define_uniq_tagged_files); mkid -fID $$unique
+tags: tags-am
+TAGS: tags
+
+tags-am: $(TAGS_DEPENDENCIES) $(am__tagged_files)
+ set x; \
+ here=`pwd`; \
+ $(am__define_uniq_tagged_files); \
+ shift; \
+ if test -z "$(ETAGS_ARGS)$$*$$unique"; then :; else \
+ test -n "$$unique" || unique=$$empty_fix; \
+ if test $$# -gt 0; then \
+ $(ETAGS) $(ETAGSFLAGS) $(AM_ETAGSFLAGS) $(ETAGS_ARGS) \
+ "$$@" $$unique; \
+ else \
+ $(ETAGS) $(ETAGSFLAGS) $(AM_ETAGSFLAGS) $(ETAGS_ARGS) \
+ $$unique; \
+ fi; \
+ fi
+ctags: ctags-am
+
+CTAGS: ctags
+ctags-am: $(TAGS_DEPENDENCIES) $(am__tagged_files)
+ $(am__define_uniq_tagged_files); \
+ test -z "$(CTAGS_ARGS)$$unique" \
+ || $(CTAGS) $(CTAGSFLAGS) $(AM_CTAGSFLAGS) $(CTAGS_ARGS) \
+ $$unique
+
+GTAGS:
+ here=`$(am__cd) $(top_builddir) && pwd` \
+ && $(am__cd) $(top_srcdir) \
+ && gtags -i $(GTAGS_ARGS) "$$here"
+cscopelist: cscopelist-am
+
+cscopelist-am: $(am__tagged_files)
+ list='$(am__tagged_files)'; \
+ case "$(srcdir)" in \
+ [\\/]* | ?:[\\/]*) sdir="$(srcdir)" ;; \
+ *) sdir=$(subdir)/$(srcdir) ;; \
+ esac; \
+ for i in $$list; do \
+ if test -f "$$i"; then \
+ echo "$(subdir)/$$i"; \
+ else \
+ echo "$$sdir/$$i"; \
+ fi; \
+ done >> $(top_builddir)/cscope.files
+
+distclean-tags:
+ -rm -f TAGS ID GTAGS GRTAGS GSYMS GPATH tags
+
+distdir: $(BUILT_SOURCES)
+ $(MAKE) $(AM_MAKEFLAGS) distdir-am
+
+distdir-am: $(DISTFILES)
+ @srcdirstrip=`echo "$(srcdir)" | sed 's/[].[^$$\\*]/\\\\&/g'`; \
+ topsrcdirstrip=`echo "$(top_srcdir)" | sed 's/[].[^$$\\*]/\\\\&/g'`; \
+ list='$(DISTFILES)'; \
+ dist_files=`for file in $$list; do echo $$file; done | \
+ sed -e "s|^$$srcdirstrip/||;t" \
+ -e "s|^$$topsrcdirstrip/|$(top_builddir)/|;t"`; \
+ case $$dist_files in \
+ */*) $(MKDIR_P) `echo "$$dist_files" | \
+ sed '/\//!d;s|^|$(distdir)/|;s,/[^/]*$$,,' | \
+ sort -u` ;; \
+ esac; \
+ for file in $$dist_files; do \
+ if test -f $$file || test -d $$file; then d=.; else d=$(srcdir); fi; \
+ if test -d $$d/$$file; then \
+ dir=`echo "/$$file" | sed -e 's,/[^/]*$$,,'`; \
+ if test -d "$(distdir)/$$file"; then \
+ find "$(distdir)/$$file" -type d ! -perm -700 -exec chmod u+rwx {} \;; \
+ fi; \
+ if test -d $(srcdir)/$$file && test $$d != $(srcdir); then \
+ cp -fpR $(srcdir)/$$file "$(distdir)$$dir" || exit 1; \
+ find "$(distdir)/$$file" -type d ! -perm -700 -exec chmod u+rwx {} \;; \
+ fi; \
+ cp -fpR $$d/$$file "$(distdir)$$dir" || exit 1; \
+ else \
+ test -f "$(distdir)/$$file" \
+ || cp -p $$d/$$file "$(distdir)/$$file" \
+ || exit 1; \
+ fi; \
+ done
+check-am: all-am
+check: check-am
+all-am: Makefile $(LTLIBRARIES) $(HEADERS)
+installdirs:
+install: install-am
+install-exec: install-exec-am
+install-data: install-data-am
+uninstall: uninstall-am
+
+install-am: all-am
+ @$(MAKE) $(AM_MAKEFLAGS) install-exec-am install-data-am
+
+installcheck: installcheck-am
+install-strip:
+ if test -z '$(STRIP)'; then \
+ $(MAKE) $(AM_MAKEFLAGS) INSTALL_PROGRAM="$(INSTALL_STRIP_PROGRAM)" \
+ install_sh_PROGRAM="$(INSTALL_STRIP_PROGRAM)" INSTALL_STRIP_FLAG=-s \
+ install; \
+ else \
+ $(MAKE) $(AM_MAKEFLAGS) INSTALL_PROGRAM="$(INSTALL_STRIP_PROGRAM)" \
+ install_sh_PROGRAM="$(INSTALL_STRIP_PROGRAM)" INSTALL_STRIP_FLAG=-s \
+ "INSTALL_PROGRAM_ENV=STRIPPROG='$(STRIP)'" install; \
+ fi
+mostlyclean-generic:
+
+clean-generic:
+
+distclean-generic:
+ -test -z "$(CONFIG_CLEAN_FILES)" || rm -f $(CONFIG_CLEAN_FILES)
+ -test . = "$(srcdir)" || test -z "$(CONFIG_CLEAN_VPATH_FILES)" || rm -f $(CONFIG_CLEAN_VPATH_FILES)
+
+maintainer-clean-generic:
+ @echo "This command is intended for maintainers to use"
+ @echo "it deletes files that may require special tools to rebuild."
+clean: clean-am
+
+clean-am: clean-generic clean-libtool clean-noinstLTLIBRARIES \
+ mostlyclean-am
+
+distclean: distclean-am
+ -rm -f ./$(DEPDIR)/otp-dictionary.Plo
+ -rm -f ./$(DEPDIR)/otp-hash.Plo
+ -rm -f ./$(DEPDIR)/otp-parity.Plo
+ -rm -f ./$(DEPDIR)/otp-parse.Plo
+ -rm -f Makefile
+distclean-am: clean-am distclean-compile distclean-generic \
+ distclean-tags
+
+dvi: dvi-am
+
+dvi-am:
+
+html: html-am
+
+html-am:
+
+info: info-am
+
+info-am:
+
+install-data-am:
+
+install-dvi: install-dvi-am
+
+install-dvi-am:
+
+install-exec-am:
+
+install-html: install-html-am
+
+install-html-am:
+
+install-info: install-info-am
+
+install-info-am:
+
+install-man:
+
+install-pdf: install-pdf-am
+
+install-pdf-am:
+
+install-ps: install-ps-am
+
+install-ps-am:
+
+installcheck-am:
+
+maintainer-clean: maintainer-clean-am
+ -rm -f ./$(DEPDIR)/otp-dictionary.Plo
+ -rm -f ./$(DEPDIR)/otp-hash.Plo
+ -rm -f ./$(DEPDIR)/otp-parity.Plo
+ -rm -f ./$(DEPDIR)/otp-parse.Plo
+ -rm -f Makefile
+maintainer-clean-am: distclean-am maintainer-clean-generic
+
+mostlyclean: mostlyclean-am
+
+mostlyclean-am: mostlyclean-compile mostlyclean-generic \
+ mostlyclean-libtool
+
+pdf: pdf-am
+
+pdf-am:
+
+ps: ps-am
+
+ps-am:
+
+uninstall-am:
+
+.MAKE: install-am install-strip
+
+.PHONY: CTAGS GTAGS TAGS all all-am am--depfiles check check-am clean \
+ clean-generic clean-libtool clean-noinstLTLIBRARIES \
+ cscopelist-am ctags ctags-am distclean distclean-compile \
+ distclean-generic distclean-libtool distclean-tags distdir dvi \
+ dvi-am html html-am info info-am install install-am \
+ install-data install-data-am install-dvi install-dvi-am \
+ install-exec install-exec-am install-html install-html-am \
+ install-info install-info-am install-man install-pdf \
+ install-pdf-am install-ps install-ps-am install-strip \
+ installcheck installcheck-am installdirs maintainer-clean \
+ maintainer-clean-generic mostlyclean mostlyclean-compile \
+ mostlyclean-generic mostlyclean-libtool pdf pdf-am ps ps-am \
+ tags tags-am uninstall uninstall-am
+
+.PRECIOUS: Makefile
+
+
+# Tell versions [3.59,3.63) of GNU make to not export all variables.
+# Otherwise a system limit (for SysV at least) may be exceeded.
+.NOEXPORT:
diff --git a/src/lib-otp/otp-dictionary.c b/src/lib-otp/otp-dictionary.c
new file mode 100644
index 0000000..5a13157
--- /dev/null
+++ b/src/lib-otp/otp-dictionary.c
@@ -0,0 +1,589 @@
+/*
+ * OTP standard dictionary.
+ *
+ * Copyright (c) 2006 Andrey Panin <pazke@donpac.ru>
+ *
+ * This software is released under the MIT license.
+ */
+
+#include "lib.h"
+#include "otp.h"
+
+#include <string.h>
+
+struct hint {
+ const short l, u;
+};
+
+static const struct hint hints[] = {
+ { 0, 114 }, /* A */
+ { 114, 292 }, /* B */
+ { 292, 415 }, /* C */
+ { 415, 528 }, /* D */
+ { 528, 575 }, /* E */
+ { 575, 687 }, /* F */
+ { 687, 792 }, /* G */
+ { 792, 922 }, /* H */
+ { 922, 958 }, /* I */
+ { 958, 1014 }, /* J */
+ { 1014, 1055 }, /* K */
+ { 1055, 1189 }, /* L */
+ { 1189, 1315 }, /* M */
+ { 1315, 1383 }, /* N */
+ { 1383, 1446 }, /* O */
+ { 1446, 1482 }, /* P */
+ { 1482, 1486 }, /* Q */
+ { 1486, 1597 }, /* R */
+ { 1597, 1777 }, /* S */
+ { 1777, 1900 }, /* T */
+ { 1900, 1911 }, /* U */
+ { 1911, 1937 }, /* V */
+ { 1937, 2028 }, /* W */
+ { 0, 0 }, /* X */
+ { 2028, 2048 }, /* Y */
+};
+
+struct word {
+ const short value;
+ const char word[4];
+};
+
+static const struct word dictionary[2048] = {
+ { 0, "A" }, { 1, "ABE" }, { 571, "ABED" }, { 572, "ABEL" },
+ { 573, "ABET" }, { 574, "ABLE" }, { 575, "ABUT" }, { 2, "ACE" },
+ { 576, "ACHE" }, { 577, "ACID" }, { 578, "ACME" }, { 579, "ACRE" },
+ { 3, "ACT" }, { 580, "ACTA" }, { 581, "ACTS" }, { 4, "AD" },
+ { 5, "ADA" }, { 582, "ADAM" }, { 6, "ADD" }, { 583, "ADDS" },
+ { 584, "ADEN" }, { 585, "AFAR" }, { 586, "AFRO" }, { 587, "AGEE" },
+ { 7, "AGO" }, { 588, "AHEM" }, { 589, "AHOY" }, { 8, "AID" },
+ { 590, "AIDA" }, { 591, "AIDE" }, { 592, "AIDS" }, { 9, "AIM" },
+ { 10, "AIR" }, { 593, "AIRY" }, { 594, "AJAR" }, { 595, "AKIN" },
+ { 596, "ALAN" }, { 597, "ALEC" }, { 598, "ALGA" }, { 599, "ALIA" },
+ { 11, "ALL" }, { 600, "ALLY" }, { 601, "ALMA" }, { 602, "ALOE" },
+ { 12, "ALP" }, { 603, "ALSO" }, { 604, "ALTO" }, { 605, "ALUM" },
+ { 606, "ALVA" }, { 13, "AM" }, { 607, "AMEN" }, { 608, "AMES" },
+ { 609, "AMID" }, { 610, "AMMO" }, { 611, "AMOK" }, { 612, "AMOS" },
+ { 613, "AMRA" }, { 14, "AMY" }, { 15, "AN" }, { 16, "ANA" },
+ { 17, "AND" }, { 614, "ANDY" }, { 615, "ANEW" }, { 18, "ANN" },
+ { 616, "ANNA" }, { 617, "ANNE" }, { 19, "ANT" }, { 618, "ANTE" },
+ { 619, "ANTI" }, { 20, "ANY" }, { 21, "APE" }, { 22, "APS" },
+ { 23, "APT" }, { 620, "AQUA" }, { 621, "ARAB" }, { 24, "ARC" },
+ { 622, "ARCH" }, { 25, "ARE" }, { 623, "AREA" }, { 624, "ARGO" },
+ { 625, "ARID" }, { 26, "ARK" }, { 27, "ARM" }, { 626, "ARMY" },
+ { 28, "ART" }, { 627, "ARTS" }, { 628, "ARTY" }, { 29, "AS" },
+ { 30, "ASH" }, { 629, "ASIA" }, { 31, "ASK" }, { 630, "ASKS" },
+ { 32, "AT" }, { 33, "ATE" }, { 631, "ATOM" }, { 34, "AUG" },
+ { 35, "AUK" }, { 632, "AUNT" }, { 633, "AURA" }, { 634, "AUTO" },
+ { 36, "AVE" }, { 635, "AVER" }, { 636, "AVID" }, { 637, "AVIS" },
+ { 638, "AVON" }, { 639, "AVOW" }, { 640, "AWAY" }, { 37, "AWE" },
+ { 38, "AWK" }, { 39, "AWL" }, { 40, "AWN" }, { 641, "AWRY" },
+ { 41, "AX" }, { 42, "AYE" }, { 642, "BABE" }, { 643, "BABY" },
+ { 644, "BACH" }, { 645, "BACK" }, { 43, "BAD" }, { 646, "BADE" },
+ { 44, "BAG" }, { 45, "BAH" }, { 647, "BAIL" }, { 648, "BAIT" },
+ { 649, "BAKE" }, { 650, "BALD" }, { 651, "BALE" }, { 652, "BALI" },
+ { 653, "BALK" }, { 654, "BALL" }, { 655, "BALM" }, { 46, "BAM" },
+ { 47, "BAN" }, { 656, "BAND" }, { 657, "BANE" }, { 658, "BANG" },
+ { 659, "BANK" }, { 48, "BAR" }, { 660, "BARB" }, { 661, "BARD" },
+ { 662, "BARE" }, { 663, "BARK" }, { 664, "BARN" }, { 665, "BARR" },
+ { 666, "BASE" }, { 667, "BASH" }, { 668, "BASK" }, { 669, "BASS" },
+ { 49, "BAT" }, { 670, "BATE" }, { 671, "BATH" }, { 672, "BAWD" },
+ { 673, "BAWL" }, { 50, "BAY" }, { 51, "BE" }, { 674, "BEAD" },
+ { 675, "BEAK" }, { 676, "BEAM" }, { 677, "BEAN" }, { 678, "BEAR" },
+ { 679, "BEAT" }, { 680, "BEAU" }, { 681, "BECK" }, { 52, "BED" },
+ { 53, "BEE" }, { 682, "BEEF" }, { 683, "BEEN" }, { 684, "BEER" },
+ { 685, "BEET" }, { 54, "BEG" }, { 686, "BELA" }, { 687, "BELL" },
+ { 688, "BELT" }, { 55, "BEN" }, { 689, "BEND" }, { 690, "BENT" },
+ { 691, "BERG" }, { 692, "BERN" }, { 693, "BERT" }, { 694, "BESS" },
+ { 695, "BEST" }, { 56, "BET" }, { 696, "BETA" }, { 697, "BETH" },
+ { 57, "BEY" }, { 698, "BHOY" }, { 699, "BIAS" }, { 58, "BIB" },
+ { 59, "BID" }, { 700, "BIDE" }, { 701, "BIEN" }, { 60, "BIG" },
+ { 702, "BILE" }, { 703, "BILK" }, { 704, "BILL" }, { 61, "BIN" },
+ { 705, "BIND" }, { 706, "BING" }, { 707, "BIRD" }, { 62, "BIT" },
+ { 708, "BITE" }, { 709, "BITS" }, { 710, "BLAB" }, { 711, "BLAT" },
+ { 712, "BLED" }, { 713, "BLEW" }, { 714, "BLOB" }, { 715, "BLOC" },
+ { 716, "BLOT" }, { 717, "BLOW" }, { 718, "BLUE" }, { 719, "BLUM" },
+ { 720, "BLUR" }, { 721, "BOAR" }, { 722, "BOAT" }, { 63, "BOB" },
+ { 723, "BOCA" }, { 724, "BOCK" }, { 725, "BODE" }, { 726, "BODY" },
+ { 64, "BOG" }, { 727, "BOGY" }, { 728, "BOHR" }, { 729, "BOIL" },
+ { 730, "BOLD" }, { 731, "BOLO" }, { 732, "BOLT" }, { 733, "BOMB" },
+ { 65, "BON" }, { 734, "BONA" }, { 735, "BOND" }, { 736, "BONE" },
+ { 737, "BONG" }, { 738, "BONN" }, { 739, "BONY" }, { 66, "BOO" },
+ { 740, "BOOK" }, { 741, "BOOM" }, { 742, "BOON" }, { 743, "BOOT" },
+ { 67, "BOP" }, { 744, "BORE" }, { 745, "BORG" }, { 746, "BORN" },
+ { 747, "BOSE" }, { 748, "BOSS" }, { 749, "BOTH" }, { 750, "BOUT" },
+ { 68, "BOW" }, { 751, "BOWL" }, { 69, "BOY" }, { 752, "BOYD" },
+ { 753, "BRAD" }, { 754, "BRAE" }, { 755, "BRAG" }, { 756, "BRAN" },
+ { 757, "BRAY" }, { 758, "BRED" }, { 759, "BREW" }, { 760, "BRIG" },
+ { 761, "BRIM" }, { 762, "BROW" }, { 70, "BUB" }, { 763, "BUCK" },
+ { 71, "BUD" }, { 764, "BUDD" }, { 765, "BUFF" }, { 72, "BUG" },
+ { 766, "BULB" }, { 767, "BULK" }, { 768, "BULL" }, { 73, "BUM" },
+ { 74, "BUN" }, { 769, "BUNK" }, { 770, "BUNT" }, { 771, "BUOY" },
+ { 772, "BURG" }, { 773, "BURL" }, { 774, "BURN" }, { 775, "BURR" },
+ { 776, "BURT" }, { 777, "BURY" }, { 75, "BUS" }, { 778, "BUSH" },
+ { 779, "BUSS" }, { 780, "BUST" }, { 781, "BUSY" }, { 76, "BUT" },
+ { 77, "BUY" }, { 78, "BY" }, { 79, "BYE" }, { 782, "BYTE" },
+ { 80, "CAB" }, { 783, "CADY" }, { 784, "CAFE" }, { 785, "CAGE" },
+ { 786, "CAIN" }, { 787, "CAKE" }, { 81, "CAL" }, { 788, "CALF" },
+ { 789, "CALL" }, { 790, "CALM" }, { 82, "CAM" }, { 791, "CAME" },
+ { 83, "CAN" }, { 792, "CANE" }, { 793, "CANT" }, { 84, "CAP" },
+ { 85, "CAR" }, { 794, "CARD" }, { 795, "CARE" }, { 796, "CARL" },
+ { 797, "CARR" }, { 798, "CART" }, { 799, "CASE" }, { 800, "CASH" },
+ { 801, "CASK" }, { 802, "CAST" }, { 86, "CAT" }, { 803, "CAVE" },
+ { 87, "CAW" }, { 804, "CEIL" }, { 805, "CELL" }, { 806, "CENT" },
+ { 807, "CERN" }, { 808, "CHAD" }, { 809, "CHAR" }, { 810, "CHAT" },
+ { 811, "CHAW" }, { 812, "CHEF" }, { 813, "CHEN" }, { 814, "CHEW" },
+ { 815, "CHIC" }, { 816, "CHIN" }, { 817, "CHOU" }, { 818, "CHOW" },
+ { 819, "CHUB" }, { 820, "CHUG" }, { 821, "CHUM" }, { 822, "CITE" },
+ { 823, "CITY" }, { 824, "CLAD" }, { 825, "CLAM" }, { 826, "CLAN" },
+ { 827, "CLAW" }, { 828, "CLAY" }, { 829, "CLOD" }, { 830, "CLOG" },
+ { 831, "CLOT" }, { 832, "CLUB" }, { 833, "CLUE" }, { 834, "COAL" },
+ { 835, "COAT" }, { 836, "COCA" }, { 837, "COCK" }, { 838, "COCO" },
+ { 88, "COD" }, { 839, "CODA" }, { 840, "CODE" }, { 841, "CODY" },
+ { 842, "COED" }, { 89, "COG" }, { 843, "COIL" }, { 844, "COIN" },
+ { 845, "COKE" }, { 90, "COL" }, { 846, "COLA" }, { 847, "COLD" },
+ { 848, "COLT" }, { 849, "COMA" }, { 850, "COMB" }, { 851, "COME" },
+ { 91, "CON" }, { 92, "COO" }, { 852, "COOK" }, { 853, "COOL" },
+ { 854, "COON" }, { 855, "COOT" }, { 93, "COP" }, { 856, "CORD" },
+ { 857, "CORE" }, { 858, "CORK" }, { 859, "CORN" }, { 860, "COST" },
+ { 94, "COT" }, { 861, "COVE" }, { 95, "COW" }, { 862, "COWL" },
+ { 96, "COY" }, { 863, "CRAB" }, { 864, "CRAG" }, { 865, "CRAM" },
+ { 866, "CRAY" }, { 867, "CREW" }, { 868, "CRIB" }, { 869, "CROW" },
+ { 870, "CRUD" }, { 97, "CRY" }, { 98, "CUB" }, { 871, "CUBA" },
+ { 872, "CUBE" }, { 99, "CUE" }, { 873, "CUFF" }, { 874, "CULL" },
+ { 875, "CULT" }, { 876, "CUNY" }, { 100, "CUP" }, { 101, "CUR" },
+ { 877, "CURB" }, { 878, "CURD" }, { 879, "CURE" }, { 880, "CURL" },
+ { 881, "CURT" }, { 102, "CUT" }, { 882, "CUTS" }, { 103, "DAB" },
+ { 104, "DAD" }, { 883, "DADE" }, { 884, "DALE" }, { 105, "DAM" },
+ { 885, "DAME" }, { 106, "DAN" }, { 886, "DANA" }, { 887, "DANE" },
+ { 888, "DANG" }, { 889, "DANK" }, { 107, "DAR" }, { 890, "DARE" },
+ { 891, "DARK" }, { 892, "DARN" }, { 893, "DART" }, { 894, "DASH" },
+ { 895, "DATA" }, { 896, "DATE" }, { 897, "DAVE" }, { 898, "DAVY" },
+ { 899, "DAWN" }, { 108, "DAY" }, { 900, "DAYS" }, { 901, "DEAD" },
+ { 902, "DEAF" }, { 903, "DEAL" }, { 904, "DEAN" }, { 905, "DEAR" },
+ { 906, "DEBT" }, { 907, "DECK" }, { 109, "DEE" }, { 908, "DEED" },
+ { 909, "DEEM" }, { 910, "DEER" }, { 911, "DEFT" }, { 912, "DEFY" },
+ { 110, "DEL" }, { 913, "DELL" }, { 111, "DEN" }, { 914, "DENT" },
+ { 915, "DENY" }, { 112, "DES" }, { 916, "DESK" }, { 113, "DEW" },
+ { 917, "DIAL" }, { 918, "DICE" }, { 114, "DID" }, { 115, "DIE" },
+ { 919, "DIED" }, { 920, "DIET" }, { 116, "DIG" }, { 921, "DIME" },
+ { 117, "DIN" }, { 922, "DINE" }, { 923, "DING" }, { 924, "DINT" },
+ { 118, "DIP" }, { 925, "DIRE" }, { 926, "DIRT" }, { 927, "DISC" },
+ { 928, "DISH" }, { 929, "DISK" }, { 930, "DIVE" }, { 119, "DO" },
+ { 931, "DOCK" }, { 120, "DOE" }, { 932, "DOES" }, { 121, "DOG" },
+ { 933, "DOLE" }, { 934, "DOLL" }, { 935, "DOLT" }, { 936, "DOME" },
+ { 122, "DON" }, { 937, "DONE" }, { 938, "DOOM" }, { 939, "DOOR" },
+ { 940, "DORA" }, { 941, "DOSE" }, { 123, "DOT" }, { 942, "DOTE" },
+ { 943, "DOUG" }, { 944, "DOUR" }, { 945, "DOVE" }, { 124, "DOW" },
+ { 946, "DOWN" }, { 947, "DRAB" }, { 948, "DRAG" }, { 949, "DRAM" },
+ { 950, "DRAW" }, { 951, "DREW" }, { 952, "DRUB" }, { 953, "DRUG" },
+ { 954, "DRUM" }, { 125, "DRY" }, { 955, "DUAL" }, { 126, "DUB" },
+ { 956, "DUCK" }, { 957, "DUCT" }, { 127, "DUD" }, { 128, "DUE" },
+ { 958, "DUEL" }, { 959, "DUET" }, { 129, "DUG" }, { 960, "DUKE" },
+ { 961, "DULL" }, { 962, "DUMB" }, { 130, "DUN" }, { 963, "DUNE" },
+ { 964, "DUNK" }, { 965, "DUSK" }, { 966, "DUST" }, { 967, "DUTY" },
+ { 968, "EACH" }, { 131, "EAR" }, { 969, "EARL" }, { 970, "EARN" },
+ { 971, "EASE" }, { 972, "EAST" }, { 973, "EASY" }, { 132, "EAT" },
+ { 974, "EBEN" }, { 975, "ECHO" }, { 133, "ED" }, { 976, "EDDY" },
+ { 977, "EDEN" }, { 978, "EDGE" }, { 979, "EDGY" }, { 980, "EDIT" },
+ { 981, "EDNA" }, { 134, "EEL" }, { 982, "EGAN" }, { 135, "EGG" },
+ { 136, "EGO" }, { 983, "ELAN" }, { 984, "ELBA" }, { 137, "ELI" },
+ { 138, "ELK" }, { 985, "ELLA" }, { 139, "ELM" }, { 986, "ELSE" },
+ { 140, "ELY" }, { 141, "EM" }, { 987, "EMIL" }, { 988, "EMIT" },
+ { 989, "EMMA" }, { 142, "END" }, { 990, "ENDS" }, { 991, "ERIC" },
+ { 992, "EROS" }, { 143, "EST" }, { 144, "ETC" }, { 145, "EVA" },
+ { 146, "EVE" }, { 993, "EVEN" }, { 994, "EVER" }, { 995, "EVIL" },
+ { 147, "EWE" }, { 148, "EYE" }, { 996, "EYED" }, { 997, "FACE" },
+ { 998, "FACT" }, { 149, "FAD" }, { 999, "FADE" }, { 1000, "FAIL" },
+ { 1001, "FAIN" }, { 1002, "FAIR" }, { 1003, "FAKE" }, { 1004, "FALL" },
+ { 1005, "FAME" }, { 150, "FAN" }, { 1006, "FANG" }, { 151, "FAR" },
+ { 1007, "FARM" }, { 1008, "FAST" }, { 152, "FAT" }, { 1009, "FATE" },
+ { 1010, "FAWN" }, { 153, "FAY" }, { 1011, "FEAR" }, { 1012, "FEAT" },
+ { 154, "FED" }, { 155, "FEE" }, { 1013, "FEED" }, { 1014, "FEEL" },
+ { 1015, "FEET" }, { 1016, "FELL" }, { 1017, "FELT" }, { 1018, "FEND" },
+ { 1019, "FERN" }, { 1020, "FEST" }, { 1021, "FEUD" }, { 156, "FEW" },
+ { 157, "FIB" }, { 1022, "FIEF" }, { 158, "FIG" }, { 1023, "FIGS" },
+ { 1024, "FILE" }, { 1025, "FILL" }, { 1026, "FILM" }, { 159, "FIN" },
+ { 1027, "FIND" }, { 1028, "FINE" }, { 1029, "FINK" }, { 160, "FIR" },
+ { 1030, "FIRE" }, { 1031, "FIRM" }, { 1032, "FISH" }, { 1033, "FISK" },
+ { 1034, "FIST" }, { 161, "FIT" }, { 1035, "FITS" }, { 1036, "FIVE" },
+ { 1037, "FLAG" }, { 1038, "FLAK" }, { 1039, "FLAM" }, { 1040, "FLAT" },
+ { 1041, "FLAW" }, { 1042, "FLEA" }, { 1043, "FLED" }, { 1044, "FLEW" },
+ { 1045, "FLIT" }, { 162, "FLO" }, { 1046, "FLOC" }, { 1047, "FLOG" },
+ { 1048, "FLOW" }, { 1049, "FLUB" }, { 1050, "FLUE" }, { 163, "FLY" },
+ { 1051, "FOAL" }, { 1052, "FOAM" }, { 164, "FOE" }, { 165, "FOG" },
+ { 1053, "FOGY" }, { 1054, "FOIL" }, { 1055, "FOLD" }, { 1056, "FOLK" },
+ { 1057, "FOND" }, { 1058, "FONT" }, { 1059, "FOOD" }, { 1060, "FOOL" },
+ { 1061, "FOOT" }, { 166, "FOR" }, { 1062, "FORD" }, { 1063, "FORE" },
+ { 1064, "FORK" }, { 1065, "FORM" }, { 1066, "FORT" }, { 1067, "FOSS" },
+ { 1068, "FOUL" }, { 1069, "FOUR" }, { 1070, "FOWL" }, { 1071, "FRAU" },
+ { 1072, "FRAY" }, { 1073, "FRED" }, { 1074, "FREE" }, { 1075, "FRET" },
+ { 1076, "FREY" }, { 1077, "FROG" }, { 1078, "FROM" }, { 167, "FRY" },
+ { 1079, "FUEL" }, { 1080, "FULL" }, { 168, "FUM" }, { 1081, "FUME" },
+ { 169, "FUN" }, { 1082, "FUND" }, { 1083, "FUNK" }, { 170, "FUR" },
+ { 1084, "FURY" }, { 1085, "FUSE" }, { 1086, "FUSS" }, { 171, "GAB" },
+ { 172, "GAD" }, { 1087, "GAFF" }, { 173, "GAG" }, { 1088, "GAGE" },
+ { 1089, "GAIL" }, { 1090, "GAIN" }, { 1091, "GAIT" }, { 174, "GAL" },
+ { 1092, "GALA" }, { 1093, "GALE" }, { 1094, "GALL" }, { 1095, "GALT" },
+ { 175, "GAM" }, { 1096, "GAME" }, { 1097, "GANG" }, { 176, "GAP" },
+ { 1098, "GARB" }, { 1099, "GARY" }, { 177, "GAS" }, { 1100, "GASH" },
+ { 1101, "GATE" }, { 1102, "GAUL" }, { 1103, "GAUR" }, { 1104, "GAVE" },
+ { 1105, "GAWK" }, { 178, "GAY" }, { 1106, "GEAR" }, { 179, "GEE" },
+ { 180, "GEL" }, { 1107, "GELD" }, { 181, "GEM" }, { 1108, "GENE" },
+ { 1109, "GENT" }, { 1110, "GERM" }, { 182, "GET" }, { 1111, "GETS" },
+ { 1112, "GIBE" }, { 1113, "GIFT" }, { 183, "GIG" }, { 184, "GIL" },
+ { 1114, "GILD" }, { 1115, "GILL" }, { 1116, "GILT" }, { 185, "GIN" },
+ { 1117, "GINA" }, { 1118, "GIRD" }, { 1119, "GIRL" }, { 1120, "GIST" },
+ { 1121, "GIVE" }, { 1122, "GLAD" }, { 1123, "GLEE" }, { 1124, "GLEN" },
+ { 1125, "GLIB" }, { 1126, "GLOB" }, { 1127, "GLOM" }, { 1128, "GLOW" },
+ { 1129, "GLUE" }, { 1130, "GLUM" }, { 1131, "GLUT" }, { 186, "GO" },
+ { 1132, "GOAD" }, { 1133, "GOAL" }, { 1134, "GOAT" }, { 1135, "GOER" },
+ { 1136, "GOES" }, { 1137, "GOLD" }, { 1138, "GOLF" }, { 1139, "GONE" },
+ { 1140, "GONG" }, { 1141, "GOOD" }, { 1142, "GOOF" }, { 1143, "GORE" },
+ { 1144, "GORY" }, { 1145, "GOSH" }, { 187, "GOT" }, { 1146, "GOUT" },
+ { 1147, "GOWN" }, { 1148, "GRAB" }, { 1149, "GRAD" }, { 1150, "GRAY" },
+ { 1151, "GREG" }, { 1152, "GREW" }, { 1153, "GREY" }, { 1154, "GRID" },
+ { 1155, "GRIM" }, { 1156, "GRIN" }, { 1157, "GRIT" }, { 1158, "GROW" },
+ { 1159, "GRUB" }, { 1160, "GULF" }, { 1161, "GULL" }, { 188, "GUM" },
+ { 189, "GUN" }, { 1162, "GUNK" }, { 1163, "GURU" }, { 190, "GUS" },
+ { 1164, "GUSH" }, { 1165, "GUST" }, { 191, "GUT" }, { 192, "GUY" },
+ { 1166, "GWEN" }, { 1167, "GWYN" }, { 193, "GYM" }, { 194, "GYP" },
+ { 195, "HA" }, { 1168, "HAAG" }, { 1169, "HAAS" }, { 1170, "HACK" },
+ { 196, "HAD" }, { 1171, "HAIL" }, { 1172, "HAIR" }, { 197, "HAL" },
+ { 1173, "HALE" }, { 1174, "HALF" }, { 1175, "HALL" }, { 1176, "HALO" },
+ { 1177, "HALT" }, { 198, "HAM" }, { 199, "HAN" }, { 1178, "HAND" },
+ { 1179, "HANG" }, { 1180, "HANK" }, { 1181, "HANS" }, { 200, "HAP" },
+ { 1182, "HARD" }, { 1183, "HARK" }, { 1184, "HARM" }, { 1185, "HART" },
+ { 201, "HAS" }, { 1186, "HASH" }, { 1187, "HAST" }, { 202, "HAT" },
+ { 1188, "HATE" }, { 1189, "HATH" }, { 1190, "HAUL" }, { 1191, "HAVE" },
+ { 203, "HAW" }, { 1192, "HAWK" }, { 204, "HAY" }, { 1193, "HAYS" },
+ { 205, "HE" }, { 1194, "HEAD" }, { 1195, "HEAL" }, { 1196, "HEAR" },
+ { 1197, "HEAT" }, { 1198, "HEBE" }, { 1199, "HECK" }, { 1200, "HEED" },
+ { 1201, "HEEL" }, { 1202, "HEFT" }, { 1203, "HELD" }, { 1204, "HELL" },
+ { 1205, "HELM" }, { 206, "HEM" }, { 207, "HEN" }, { 208, "HER" },
+ { 1206, "HERB" }, { 1207, "HERD" }, { 1208, "HERE" }, { 1209, "HERO" },
+ { 1210, "HERS" }, { 1211, "HESS" }, { 209, "HEW" }, { 1212, "HEWN" },
+ { 210, "HEY" }, { 211, "HI" }, { 1213, "HICK" }, { 212, "HID" },
+ { 1214, "HIDE" }, { 1215, "HIGH" }, { 1216, "HIKE" }, { 1217, "HILL" },
+ { 1218, "HILT" }, { 213, "HIM" }, { 1219, "HIND" }, { 1220, "HINT" },
+ { 214, "HIP" }, { 1221, "HIRE" }, { 215, "HIS" }, { 1222, "HISS" },
+ { 216, "HIT" }, { 1223, "HIVE" }, { 217, "HO" }, { 218, "HOB" },
+ { 1224, "HOBO" }, { 219, "HOC" }, { 1225, "HOCK" }, { 220, "HOE" },
+ { 1226, "HOFF" }, { 221, "HOG" }, { 1227, "HOLD" }, { 1228, "HOLE" },
+ { 1229, "HOLM" }, { 1230, "HOLT" }, { 1231, "HOME" }, { 1232, "HONE" },
+ { 1233, "HONK" }, { 1234, "HOOD" }, { 1235, "HOOF" }, { 1236, "HOOK" },
+ { 1237, "HOOT" }, { 222, "HOP" }, { 1238, "HORN" }, { 1239, "HOSE" },
+ { 1240, "HOST" }, { 223, "HOT" }, { 1241, "HOUR" }, { 1242, "HOVE" },
+ { 224, "HOW" }, { 1243, "HOWE" }, { 1244, "HOWL" }, { 1245, "HOYT" },
+ { 225, "HUB" }, { 1246, "HUCK" }, { 226, "HUE" }, { 1247, "HUED" },
+ { 1248, "HUFF" }, { 227, "HUG" }, { 1249, "HUGE" }, { 1250, "HUGH" },
+ { 1251, "HUGO" }, { 228, "HUH" }, { 1252, "HULK" }, { 1253, "HULL" },
+ { 229, "HUM" }, { 1254, "HUNK" }, { 1255, "HUNT" }, { 1256, "HURD" },
+ { 1257, "HURL" }, { 1258, "HURT" }, { 1259, "HUSH" }, { 230, "HUT" },
+ { 1260, "HYDE" }, { 1261, "HYMN" }, { 231, "I" }, { 1262, "IBIS" },
+ { 1263, "ICON" }, { 232, "ICY" }, { 233, "IDA" }, { 1264, "IDEA" },
+ { 1265, "IDLE" }, { 234, "IF" }, { 1266, "IFFY" }, { 235, "IKE" },
+ { 236, "ILL" }, { 1267, "INCA" }, { 1268, "INCH" }, { 237, "INK" },
+ { 238, "INN" }, { 1269, "INTO" }, { 239, "IO" }, { 240, "ION" },
+ { 1270, "IONS" }, { 1271, "IOTA" }, { 1272, "IOWA" }, { 241, "IQ" },
+ { 242, "IRA" }, { 243, "IRE" }, { 1273, "IRIS" }, { 244, "IRK" },
+ { 1274, "IRMA" }, { 1275, "IRON" }, { 245, "IS" }, { 1276, "ISLE" },
+ { 246, "IT" }, { 1277, "ITCH" }, { 1278, "ITEM" }, { 247, "ITS" },
+ { 1279, "IVAN" }, { 248, "IVY" }, { 249, "JAB" }, { 1280, "JACK" },
+ { 1281, "JADE" }, { 250, "JAG" }, { 1282, "JAIL" }, { 1283, "JAKE" },
+ { 251, "JAM" }, { 252, "JAN" }, { 1284, "JANE" }, { 253, "JAR" },
+ { 1285, "JAVA" }, { 254, "JAW" }, { 255, "JAY" }, { 1286, "JEAN" },
+ { 1287, "JEFF" }, { 1288, "JERK" }, { 1289, "JESS" }, { 1290, "JEST" },
+ { 256, "JET" }, { 1291, "JIBE" }, { 257, "JIG" }, { 1292, "JILL" },
+ { 1293, "JILT" }, { 258, "JIM" }, { 1294, "JIVE" }, { 259, "JO" },
+ { 1295, "JOAN" }, { 260, "JOB" }, { 1296, "JOBS" }, { 1297, "JOCK" },
+ { 261, "JOE" }, { 1298, "JOEL" }, { 1299, "JOEY" }, { 262, "JOG" },
+ { 1300, "JOHN" }, { 1301, "JOIN" }, { 1302, "JOKE" }, { 1303, "JOLT" },
+ { 263, "JOT" }, { 1304, "JOVE" }, { 264, "JOY" }, { 1305, "JUDD" },
+ { 1306, "JUDE" }, { 1307, "JUDO" }, { 1308, "JUDY" }, { 265, "JUG" },
+ { 1309, "JUJU" }, { 1310, "JUKE" }, { 1311, "JULY" }, { 1312, "JUNE" },
+ { 1313, "JUNK" }, { 1314, "JUNO" }, { 1315, "JURY" }, { 1316, "JUST" },
+ { 266, "JUT" }, { 1317, "JUTE" }, { 1318, "KAHN" }, { 1319, "KALE" },
+ { 1320, "KANE" }, { 1321, "KANT" }, { 1322, "KARL" }, { 1323, "KATE" },
+ { 267, "KAY" }, { 1324, "KEEL" }, { 1325, "KEEN" }, { 268, "KEG" },
+ { 269, "KEN" }, { 1326, "KENO" }, { 1327, "KENT" }, { 1328, "KERN" },
+ { 1329, "KERR" }, { 270, "KEY" }, { 1330, "KEYS" }, { 1331, "KICK" },
+ { 271, "KID" }, { 1332, "KILL" }, { 272, "KIM" }, { 273, "KIN" },
+ { 1333, "KIND" }, { 1334, "KING" }, { 1335, "KIRK" }, { 1336, "KISS" },
+ { 274, "KIT" }, { 1337, "KITE" }, { 1338, "KLAN" }, { 1339, "KNEE" },
+ { 1340, "KNEW" }, { 1341, "KNIT" }, { 1342, "KNOB" }, { 1343, "KNOT" },
+ { 1344, "KNOW" }, { 1345, "KOCH" }, { 1346, "KONG" }, { 1347, "KUDO" },
+ { 1348, "KURD" }, { 1349, "KURT" }, { 1350, "KYLE" }, { 275, "LA" },
+ { 276, "LAB" }, { 277, "LAC" }, { 1351, "LACE" }, { 1352, "LACK" },
+ { 1353, "LACY" }, { 278, "LAD" }, { 1354, "LADY" }, { 279, "LAG" },
+ { 1355, "LAID" }, { 1356, "LAIN" }, { 1357, "LAIR" }, { 1358, "LAKE" },
+ { 280, "LAM" }, { 1359, "LAMB" }, { 1360, "LAME" }, { 1361, "LAND" },
+ { 1362, "LANE" }, { 1363, "LANG" }, { 281, "LAP" }, { 1364, "LARD" },
+ { 1365, "LARK" }, { 1366, "LASS" }, { 1367, "LAST" }, { 1368, "LATE" },
+ { 1369, "LAUD" }, { 1370, "LAVA" }, { 282, "LAW" }, { 1371, "LAWN" },
+ { 1372, "LAWS" }, { 283, "LAY" }, { 1373, "LAYS" }, { 284, "LEA" },
+ { 1374, "LEAD" }, { 1375, "LEAF" }, { 1376, "LEAK" }, { 1377, "LEAN" },
+ { 1378, "LEAR" }, { 285, "LED" }, { 286, "LEE" }, { 1379, "LEEK" },
+ { 1380, "LEER" }, { 1381, "LEFT" }, { 287, "LEG" }, { 288, "LEN" },
+ { 1382, "LEND" }, { 1383, "LENS" }, { 1384, "LENT" }, { 289, "LEO" },
+ { 1385, "LEON" }, { 1386, "LESK" }, { 1387, "LESS" }, { 1388, "LEST" },
+ { 290, "LET" }, { 1389, "LETS" }, { 291, "LEW" }, { 1390, "LIAR" },
+ { 1391, "LICE" }, { 1392, "LICK" }, { 292, "LID" }, { 293, "LIE" },
+ { 1393, "LIED" }, { 1394, "LIEN" }, { 1395, "LIES" }, { 1396, "LIEU" },
+ { 1397, "LIFE" }, { 1398, "LIFT" }, { 1399, "LIKE" }, { 1400, "LILA" },
+ { 1401, "LILT" }, { 1402, "LILY" }, { 1403, "LIMA" }, { 1404, "LIMB" },
+ { 1405, "LIME" }, { 294, "LIN" }, { 1406, "LIND" }, { 1407, "LINE" },
+ { 1408, "LINK" }, { 1409, "LINT" }, { 1410, "LION" }, { 295, "LIP" },
+ { 1411, "LISA" }, { 1412, "LIST" }, { 296, "LIT" }, { 1413, "LIVE" },
+ { 297, "LO" }, { 1414, "LOAD" }, { 1415, "LOAF" }, { 1416, "LOAM" },
+ { 1417, "LOAN" }, { 298, "LOB" }, { 1418, "LOCK" }, { 1419, "LOFT" },
+ { 299, "LOG" }, { 1420, "LOGE" }, { 1421, "LOIS" }, { 1422, "LOLA" },
+ { 1423, "LONE" }, { 1424, "LONG" }, { 1425, "LOOK" }, { 1426, "LOON" },
+ { 1427, "LOOT" }, { 300, "LOP" }, { 1428, "LORD" }, { 1429, "LORE" },
+ { 301, "LOS" }, { 1430, "LOSE" }, { 1431, "LOSS" }, { 1432, "LOST" },
+ { 302, "LOT" }, { 303, "LOU" }, { 1433, "LOUD" }, { 1434, "LOVE" },
+ { 304, "LOW" }, { 1435, "LOWE" }, { 305, "LOY" }, { 1436, "LUCK" },
+ { 1437, "LUCY" }, { 306, "LUG" }, { 1438, "LUGE" }, { 1439, "LUKE" },
+ { 1440, "LULU" }, { 1441, "LUND" }, { 1442, "LUNG" }, { 1443, "LURA" },
+ { 1444, "LURE" }, { 1445, "LURK" }, { 1446, "LUSH" }, { 1447, "LUST" },
+ { 307, "LYE" }, { 1448, "LYLE" }, { 1449, "LYNN" }, { 1450, "LYON" },
+ { 1451, "LYRA" }, { 308, "MA" }, { 309, "MAC" }, { 1452, "MACE" },
+ { 310, "MAD" }, { 1453, "MADE" }, { 311, "MAE" }, { 1454, "MAGI" },
+ { 1455, "MAID" }, { 1456, "MAIL" }, { 1457, "MAIN" }, { 1458, "MAKE" },
+ { 1459, "MALE" }, { 1460, "MALI" }, { 1461, "MALL" }, { 1462, "MALT" },
+ { 312, "MAN" }, { 1463, "MANA" }, { 1464, "MANN" }, { 1465, "MANY" },
+ { 313, "MAO" }, { 314, "MAP" }, { 1466, "MARC" }, { 1467, "MARE" },
+ { 1468, "MARK" }, { 1469, "MARS" }, { 1470, "MART" }, { 1471, "MARY" },
+ { 1472, "MASH" }, { 1473, "MASK" }, { 1474, "MASS" }, { 1475, "MAST" },
+ { 315, "MAT" }, { 1476, "MATE" }, { 1477, "MATH" }, { 1478, "MAUL" },
+ { 316, "MAW" }, { 317, "MAY" }, { 1479, "MAYO" }, { 318, "ME" },
+ { 1480, "MEAD" }, { 1481, "MEAL" }, { 1482, "MEAN" }, { 1483, "MEAT" },
+ { 1484, "MEEK" }, { 1485, "MEET" }, { 319, "MEG" }, { 320, "MEL" },
+ { 1486, "MELD" }, { 1487, "MELT" }, { 1488, "MEMO" }, { 321, "MEN" },
+ { 1489, "MEND" }, { 1490, "MENU" }, { 1491, "MERT" }, { 1492, "MESH" },
+ { 1493, "MESS" }, { 322, "MET" }, { 323, "MEW" }, { 1494, "MICE" },
+ { 324, "MID" }, { 1495, "MIKE" }, { 1496, "MILD" }, { 1497, "MILE" },
+ { 1498, "MILK" }, { 1499, "MILL" }, { 1500, "MILT" }, { 1501, "MIMI" },
+ { 325, "MIN" }, { 1502, "MIND" }, { 1503, "MINE" }, { 1504, "MINI" },
+ { 1505, "MINK" }, { 1506, "MINT" }, { 1507, "MIRE" }, { 1508, "MISS" },
+ { 1509, "MIST" }, { 326, "MIT" }, { 1510, "MITE" }, { 1511, "MITT" },
+ { 1512, "MOAN" }, { 1513, "MOAT" }, { 327, "MOB" }, { 1514, "MOCK" },
+ { 328, "MOD" }, { 1515, "MODE" }, { 329, "MOE" }, { 1516, "MOLD" },
+ { 1517, "MOLE" }, { 1518, "MOLL" }, { 1519, "MOLT" }, { 1520, "MONA" },
+ { 1521, "MONK" }, { 1522, "MONT" }, { 330, "MOO" }, { 1523, "MOOD" },
+ { 1524, "MOON" }, { 1525, "MOOR" }, { 1526, "MOOT" }, { 331, "MOP" },
+ { 1527, "MORE" }, { 1528, "MORN" }, { 1529, "MORT" }, { 332, "MOS" },
+ { 1530, "MOSS" }, { 1531, "MOST" }, { 333, "MOT" }, { 1532, "MOTH" },
+ { 1533, "MOVE" }, { 334, "MOW" }, { 1534, "MUCH" }, { 1535, "MUCK" },
+ { 335, "MUD" }, { 1536, "MUDD" }, { 1537, "MUFF" }, { 336, "MUG" },
+ { 1538, "MULE" }, { 1539, "MULL" }, { 337, "MUM" }, { 1540, "MURK" },
+ { 1541, "MUSH" }, { 1542, "MUST" }, { 1543, "MUTE" }, { 1544, "MUTT" },
+ { 338, "MY" }, { 1545, "MYRA" }, { 1546, "MYTH" }, { 339, "NAB" },
+ { 340, "NAG" }, { 1547, "NAGY" }, { 1548, "NAIL" }, { 1549, "NAIR" },
+ { 1550, "NAME" }, { 341, "NAN" }, { 342, "NAP" }, { 1551, "NARY" },
+ { 1552, "NASH" }, { 343, "NAT" }, { 1553, "NAVE" }, { 1554, "NAVY" },
+ { 344, "NAY" }, { 345, "NE" }, { 1555, "NEAL" }, { 1556, "NEAR" },
+ { 1557, "NEAT" }, { 1558, "NECK" }, { 346, "NED" }, { 347, "NEE" },
+ { 1559, "NEED" }, { 1560, "NEIL" }, { 1561, "NELL" }, { 1562, "NEON" },
+ { 1563, "NERO" }, { 1564, "NESS" }, { 1565, "NEST" }, { 348, "NET" },
+ { 349, "NEW" }, { 1566, "NEWS" }, { 1567, "NEWT" }, { 350, "NIB" },
+ { 1568, "NIBS" }, { 1569, "NICE" }, { 1570, "NICK" }, { 351, "NIL" },
+ { 1571, "NILE" }, { 1572, "NINA" }, { 1573, "NINE" }, { 352, "NIP" },
+ { 353, "NIT" }, { 354, "NO" }, { 1574, "NOAH" }, { 355, "NOB" },
+ { 356, "NOD" }, { 1575, "NODE" }, { 1576, "NOEL" }, { 1577, "NOLL" },
+ { 357, "NON" }, { 1578, "NONE" }, { 1579, "NOOK" }, { 1580, "NOON" },
+ { 358, "NOR" }, { 1581, "NORM" }, { 1582, "NOSE" }, { 359, "NOT" },
+ { 1583, "NOTE" }, { 1584, "NOUN" }, { 360, "NOV" }, { 1585, "NOVA" },
+ { 361, "NOW" }, { 362, "NU" }, { 1586, "NUDE" }, { 1587, "NULL" },
+ { 1588, "NUMB" }, { 363, "NUN" }, { 364, "NUT" }, { 365, "O" },
+ { 366, "OAF" }, { 367, "OAK" }, { 368, "OAR" }, { 369, "OAT" },
+ { 1589, "OATH" }, { 1590, "OBEY" }, { 1591, "OBOE" }, { 370, "ODD" },
+ { 371, "ODE" }, { 1592, "ODIN" }, { 372, "OF" }, { 373, "OFF" },
+ { 374, "OFT" }, { 375, "OH" }, { 1593, "OHIO" }, { 376, "OIL" },
+ { 1594, "OILY" }, { 1595, "OINT" }, { 377, "OK" }, { 1596, "OKAY" },
+ { 1597, "OLAF" }, { 378, "OLD" }, { 1598, "OLDY" }, { 1599, "OLGA" },
+ { 1600, "OLIN" }, { 1601, "OMAN" }, { 1602, "OMEN" }, { 1603, "OMIT" },
+ { 379, "ON" }, { 1604, "ONCE" }, { 380, "ONE" }, { 1605, "ONES" },
+ { 1606, "ONLY" }, { 1607, "ONTO" }, { 1608, "ONUS" }, { 381, "OR" },
+ { 1609, "ORAL" }, { 382, "ORB" }, { 383, "ORE" }, { 1610, "ORGY" },
+ { 384, "ORR" }, { 385, "OS" }, { 1611, "OSLO" }, { 1612, "OTIS" },
+ { 386, "OTT" }, { 1613, "OTTO" }, { 1614, "OUCH" }, { 387, "OUR" },
+ { 1615, "OUST" }, { 388, "OUT" }, { 1616, "OUTS" }, { 389, "OVA" },
+ { 1617, "OVAL" }, { 1618, "OVEN" }, { 1619, "OVER" }, { 390, "OW" },
+ { 391, "OWE" }, { 392, "OWL" }, { 1620, "OWLY" }, { 393, "OWN" },
+ { 1621, "OWNS" }, { 394, "OX" }, { 395, "PA" }, { 396, "PAD" },
+ { 397, "PAL" }, { 398, "PAM" }, { 399, "PAN" }, { 400, "PAP" },
+ { 401, "PAR" }, { 402, "PAT" }, { 403, "PAW" }, { 404, "PAY" },
+ { 405, "PEA" }, { 406, "PEG" }, { 407, "PEN" }, { 408, "PEP" },
+ { 409, "PER" }, { 410, "PET" }, { 411, "PEW" }, { 412, "PHI" },
+ { 413, "PI" }, { 414, "PIE" }, { 415, "PIN" }, { 416, "PIT" },
+ { 417, "PLY" }, { 418, "PO" }, { 419, "POD" }, { 420, "POE" },
+ { 421, "POP" }, { 422, "POT" }, { 423, "POW" }, { 424, "PRO" },
+ { 425, "PRY" }, { 426, "PUB" }, { 427, "PUG" }, { 428, "PUN" },
+ { 429, "PUP" }, { 430, "PUT" }, { 1622, "QUAD" }, { 1623, "QUIT" },
+ { 431, "QUO" }, { 1624, "QUOD" }, { 1625, "RACE" }, { 1626, "RACK" },
+ { 1627, "RACY" }, { 1628, "RAFT" }, { 432, "RAG" }, { 1629, "RAGE" },
+ { 1630, "RAID" }, { 1631, "RAIL" }, { 1632, "RAIN" }, { 1633, "RAKE" },
+ { 433, "RAM" }, { 434, "RAN" }, { 1634, "RANK" }, { 1635, "RANT" },
+ { 435, "RAP" }, { 1636, "RARE" }, { 1637, "RASH" }, { 436, "RAT" },
+ { 1638, "RATE" }, { 1639, "RAVE" }, { 437, "RAW" }, { 438, "RAY" },
+ { 1640, "RAYS" }, { 1641, "READ" }, { 1642, "REAL" }, { 1643, "REAM" },
+ { 1644, "REAR" }, { 439, "REB" }, { 1645, "RECK" }, { 440, "RED" },
+ { 1646, "REED" }, { 1647, "REEF" }, { 1648, "REEK" }, { 1649, "REEL" },
+ { 1650, "REID" }, { 1651, "REIN" }, { 1652, "RENA" }, { 1653, "REND" },
+ { 1654, "RENT" }, { 441, "REP" }, { 1655, "REST" }, { 442, "RET" },
+ { 443, "RIB" }, { 1656, "RICE" }, { 1657, "RICH" }, { 1658, "RICK" },
+ { 444, "RID" }, { 1659, "RIDE" }, { 1660, "RIFT" }, { 445, "RIG" },
+ { 1661, "RILL" }, { 446, "RIM" }, { 1662, "RIME" }, { 1663, "RING" },
+ { 1664, "RINK" }, { 447, "RIO" }, { 448, "RIP" }, { 1665, "RISE" },
+ { 1666, "RISK" }, { 1667, "RITE" }, { 1668, "ROAD" }, { 1669, "ROAM" },
+ { 1670, "ROAR" }, { 449, "ROB" }, { 1671, "ROBE" }, { 1672, "ROCK" },
+ { 450, "ROD" }, { 1673, "RODE" }, { 451, "ROE" }, { 1674, "ROIL" },
+ { 1675, "ROLL" }, { 1676, "ROME" }, { 452, "RON" }, { 1677, "ROOD" },
+ { 1678, "ROOF" }, { 1679, "ROOK" }, { 1680, "ROOM" }, { 1681, "ROOT" },
+ { 1682, "ROSA" }, { 1683, "ROSE" }, { 1684, "ROSS" }, { 1685, "ROSY" },
+ { 453, "ROT" }, { 1686, "ROTH" }, { 1687, "ROUT" }, { 1688, "ROVE" },
+ { 454, "ROW" }, { 1689, "ROWE" }, { 1690, "ROWS" }, { 455, "ROY" },
+ { 456, "RUB" }, { 1691, "RUBE" }, { 1692, "RUBY" }, { 1693, "RUDE" },
+ { 1694, "RUDY" }, { 457, "RUE" }, { 458, "RUG" }, { 1695, "RUIN" },
+ { 1696, "RULE" }, { 459, "RUM" }, { 460, "RUN" }, { 1697, "RUNG" },
+ { 1698, "RUNS" }, { 1699, "RUNT" }, { 1700, "RUSE" }, { 1701, "RUSH" },
+ { 1702, "RUSK" }, { 1703, "RUSS" }, { 1704, "RUST" }, { 1705, "RUTH" },
+ { 461, "RYE" }, { 462, "SAC" }, { 1706, "SACK" }, { 463, "SAD" },
+ { 1707, "SAFE" }, { 464, "SAG" }, { 1708, "SAGE" }, { 1709, "SAID" },
+ { 1710, "SAIL" }, { 465, "SAL" }, { 1711, "SALE" }, { 1712, "SALK" },
+ { 1713, "SALT" }, { 466, "SAM" }, { 1714, "SAME" }, { 467, "SAN" },
+ { 1715, "SAND" }, { 1716, "SANE" }, { 1717, "SANG" }, { 1718, "SANK" },
+ { 468, "SAP" }, { 1719, "SARA" }, { 469, "SAT" }, { 1720, "SAUL" },
+ { 1721, "SAVE" }, { 470, "SAW" }, { 471, "SAY" }, { 1722, "SAYS" },
+ { 1723, "SCAN" }, { 1724, "SCAR" }, { 1725, "SCAT" }, { 1726, "SCOT" },
+ { 472, "SEA" }, { 1727, "SEAL" }, { 1728, "SEAM" }, { 1729, "SEAR" },
+ { 1730, "SEAT" }, { 473, "SEC" }, { 474, "SEE" }, { 1731, "SEED" },
+ { 1732, "SEEK" }, { 1733, "SEEM" }, { 1734, "SEEN" }, { 1735, "SEES" },
+ { 1736, "SELF" }, { 1737, "SELL" }, { 475, "SEN" }, { 1738, "SEND" },
+ { 1739, "SENT" }, { 476, "SET" }, { 1740, "SETS" }, { 477, "SEW" },
+ { 1741, "SEWN" }, { 1742, "SHAG" }, { 1743, "SHAM" }, { 1744, "SHAW" },
+ { 1745, "SHAY" }, { 478, "SHE" }, { 1746, "SHED" }, { 1747, "SHIM" },
+ { 1748, "SHIN" }, { 1749, "SHOD" }, { 1750, "SHOE" }, { 1751, "SHOT" },
+ { 1752, "SHOW" }, { 1753, "SHUN" }, { 1754, "SHUT" }, { 479, "SHY" },
+ { 1755, "SICK" }, { 1756, "SIDE" }, { 1757, "SIFT" }, { 1758, "SIGH" },
+ { 1759, "SIGN" }, { 1760, "SILK" }, { 1761, "SILL" }, { 1762, "SILO" },
+ { 1763, "SILT" }, { 480, "SIN" }, { 1764, "SINE" }, { 1765, "SING" },
+ { 1766, "SINK" }, { 481, "SIP" }, { 482, "SIR" }, { 1767, "SIRE" },
+ { 483, "SIS" }, { 484, "SIT" }, { 1768, "SITE" }, { 1769, "SITS" },
+ { 1770, "SITU" }, { 1771, "SKAT" }, { 1772, "SKEW" }, { 485, "SKI" },
+ { 1773, "SKID" }, { 1774, "SKIM" }, { 1775, "SKIN" }, { 1776, "SKIT" },
+ { 486, "SKY" }, { 1777, "SLAB" }, { 1778, "SLAM" }, { 1779, "SLAT" },
+ { 1780, "SLAY" }, { 1781, "SLED" }, { 1782, "SLEW" }, { 1783, "SLID" },
+ { 1784, "SLIM" }, { 1785, "SLIT" }, { 1786, "SLOB" }, { 1787, "SLOG" },
+ { 1788, "SLOT" }, { 1789, "SLOW" }, { 1790, "SLUG" }, { 1791, "SLUM" },
+ { 1792, "SLUR" }, { 487, "SLY" }, { 1793, "SMOG" }, { 1794, "SMUG" },
+ { 1795, "SNAG" }, { 1796, "SNOB" }, { 1797, "SNOW" }, { 1798, "SNUB" },
+ { 1799, "SNUG" }, { 488, "SO" }, { 1800, "SOAK" }, { 1801, "SOAR" },
+ { 489, "SOB" }, { 1802, "SOCK" }, { 490, "SOD" }, { 1803, "SODA" },
+ { 1804, "SOFA" }, { 1805, "SOFT" }, { 1806, "SOIL" }, { 1807, "SOLD" },
+ { 1808, "SOME" }, { 491, "SON" }, { 1809, "SONG" }, { 1810, "SOON" },
+ { 1811, "SOOT" }, { 492, "SOP" }, { 1812, "SORE" }, { 1813, "SORT" },
+ { 1814, "SOUL" }, { 1815, "SOUR" }, { 493, "SOW" }, { 1816, "SOWN" },
+ { 494, "SOY" }, { 495, "SPA" }, { 496, "SPY" }, { 1817, "STAB" },
+ { 1818, "STAG" }, { 1819, "STAN" }, { 1820, "STAR" }, { 1821, "STAY" },
+ { 1822, "STEM" }, { 1823, "STEW" }, { 1824, "STIR" }, { 1825, "STOW" },
+ { 1826, "STUB" }, { 1827, "STUN" }, { 497, "SUB" }, { 1828, "SUCH" },
+ { 498, "SUD" }, { 1829, "SUDS" }, { 499, "SUE" }, { 1830, "SUIT" },
+ { 1831, "SULK" }, { 500, "SUM" }, { 1832, "SUMS" }, { 501, "SUN" },
+ { 1833, "SUNG" }, { 1834, "SUNK" }, { 502, "SUP" }, { 1835, "SURE" },
+ { 1836, "SURF" }, { 1837, "SWAB" }, { 1838, "SWAG" }, { 1839, "SWAM" },
+ { 1840, "SWAN" }, { 1841, "SWAT" }, { 1842, "SWAY" }, { 1843, "SWIM" },
+ { 1844, "SWUM" }, { 503, "TAB" }, { 1845, "TACK" }, { 1846, "TACT" },
+ { 504, "TAD" }, { 505, "TAG" }, { 1847, "TAIL" }, { 1848, "TAKE" },
+ { 1849, "TALE" }, { 1850, "TALK" }, { 1851, "TALL" }, { 506, "TAN" },
+ { 1852, "TANK" }, { 507, "TAP" }, { 508, "TAR" }, { 1853, "TASK" },
+ { 1854, "TATE" }, { 1855, "TAUT" }, { 509, "TEA" }, { 1856, "TEAL" },
+ { 1857, "TEAM" }, { 1858, "TEAR" }, { 1859, "TECH" }, { 510, "TED" },
+ { 511, "TEE" }, { 1860, "TEEM" }, { 1861, "TEEN" }, { 1862, "TEET" },
+ { 1863, "TELL" }, { 512, "TEN" }, { 1864, "TEND" }, { 1865, "TENT" },
+ { 1866, "TERM" }, { 1867, "TERN" }, { 1868, "TESS" }, { 1869, "TEST" },
+ { 1870, "THAN" }, { 1871, "THAT" }, { 513, "THE" }, { 1872, "THEE" },
+ { 1873, "THEM" }, { 1874, "THEN" }, { 1875, "THEY" }, { 1876, "THIN" },
+ { 1877, "THIS" }, { 1878, "THUD" }, { 1879, "THUG" }, { 514, "THY" },
+ { 515, "TIC" }, { 1880, "TICK" }, { 1881, "TIDE" }, { 1882, "TIDY" },
+ { 516, "TIE" }, { 1883, "TIED" }, { 1884, "TIER" }, { 1885, "TILE" },
+ { 1886, "TILL" }, { 1887, "TILT" }, { 517, "TIM" }, { 1888, "TIME" },
+ { 518, "TIN" }, { 1889, "TINA" }, { 1890, "TINE" }, { 1891, "TINT" },
+ { 1892, "TINY" }, { 519, "TIP" }, { 1893, "TIRE" }, { 520, "TO" },
+ { 1894, "TOAD" }, { 521, "TOE" }, { 522, "TOG" }, { 1895, "TOGO" },
+ { 1896, "TOIL" }, { 1897, "TOLD" }, { 1898, "TOLL" }, { 523, "TOM" },
+ { 524, "TON" }, { 1899, "TONE" }, { 1900, "TONG" }, { 1901, "TONY" },
+ { 525, "TOO" }, { 1902, "TOOK" }, { 1903, "TOOL" }, { 1904, "TOOT" },
+ { 526, "TOP" }, { 1905, "TORE" }, { 1906, "TORN" }, { 1907, "TOTE" },
+ { 1908, "TOUR" }, { 1909, "TOUT" }, { 527, "TOW" }, { 1910, "TOWN" },
+ { 528, "TOY" }, { 1911, "TRAG" }, { 1912, "TRAM" }, { 1913, "TRAY" },
+ { 1914, "TREE" }, { 1915, "TREK" }, { 1916, "TRIG" }, { 1917, "TRIM" },
+ { 1918, "TRIO" }, { 1919, "TROD" }, { 1920, "TROT" }, { 1921, "TROY" },
+ { 1922, "TRUE" }, { 529, "TRY" }, { 530, "TUB" }, { 1923, "TUBA" },
+ { 1924, "TUBE" }, { 1925, "TUCK" }, { 1926, "TUFT" }, { 531, "TUG" },
+ { 532, "TUM" }, { 533, "TUN" }, { 1927, "TUNA" }, { 1928, "TUNE" },
+ { 1929, "TUNG" }, { 1930, "TURF" }, { 1931, "TURN" }, { 1932, "TUSK" },
+ { 1933, "TWIG" }, { 1934, "TWIN" }, { 1935, "TWIT" }, { 534, "TWO" },
+ { 1936, "ULAN" }, { 535, "UN" }, { 1937, "UNIT" }, { 536, "UP" },
+ { 1938, "URGE" }, { 537, "US" }, { 538, "USE" }, { 1939, "USED" },
+ { 1940, "USER" }, { 1941, "USES" }, { 1942, "UTAH" }, { 1943, "VAIL" },
+ { 1944, "VAIN" }, { 1945, "VALE" }, { 539, "VAN" }, { 1946, "VARY" },
+ { 1947, "VASE" }, { 1948, "VAST" }, { 540, "VAT" }, { 1949, "VEAL" },
+ { 1950, "VEDA" }, { 1951, "VEIL" }, { 1952, "VEIN" }, { 1953, "VEND" },
+ { 1954, "VENT" }, { 1955, "VERB" }, { 1956, "VERY" }, { 541, "VET" },
+ { 1957, "VETO" }, { 1958, "VICE" }, { 542, "VIE" }, { 1959, "VIEW" },
+ { 1960, "VINE" }, { 1961, "VISE" }, { 1962, "VOID" }, { 1963, "VOLT" },
+ { 1964, "VOTE" }, { 1965, "WACK" }, { 543, "WAD" }, { 1966, "WADE" },
+ { 544, "WAG" }, { 1967, "WAGE" }, { 1968, "WAIL" }, { 1969, "WAIT" },
+ { 1970, "WAKE" }, { 1971, "WALE" }, { 1972, "WALK" }, { 1973, "WALL" },
+ { 1974, "WALT" }, { 1975, "WAND" }, { 1976, "WANE" }, { 1977, "WANG" },
+ { 1978, "WANT" }, { 545, "WAR" }, { 1979, "WARD" }, { 1980, "WARM" },
+ { 1981, "WARN" }, { 1982, "WART" }, { 546, "WAS" }, { 1983, "WASH" },
+ { 1984, "WAST" }, { 1985, "WATS" }, { 1986, "WATT" }, { 1987, "WAVE" },
+ { 1988, "WAVY" }, { 547, "WAY" }, { 1989, "WAYS" }, { 548, "WE" },
+ { 1990, "WEAK" }, { 1991, "WEAL" }, { 1992, "WEAN" }, { 1993, "WEAR" },
+ { 549, "WEB" }, { 550, "WED" }, { 551, "WEE" }, { 1994, "WEED" },
+ { 1995, "WEEK" }, { 1996, "WEIR" }, { 1997, "WELD" }, { 1998, "WELL" },
+ { 1999, "WELT" }, { 2000, "WENT" }, { 2001, "WERE" }, { 2002, "WERT" },
+ { 2003, "WEST" }, { 552, "WET" }, { 2004, "WHAM" }, { 2005, "WHAT" },
+ { 2006, "WHEE" }, { 2007, "WHEN" }, { 2008, "WHET" }, { 553, "WHO" },
+ { 2009, "WHOA" }, { 2010, "WHOM" }, { 554, "WHY" }, { 2011, "WICK" },
+ { 2012, "WIFE" }, { 2013, "WILD" }, { 2014, "WILL" }, { 555, "WIN" },
+ { 2015, "WIND" }, { 2016, "WINE" }, { 2017, "WING" }, { 2018, "WINK" },
+ { 2019, "WINO" }, { 2020, "WIRE" }, { 2021, "WISE" }, { 2022, "WISH" },
+ { 556, "WIT" }, { 2023, "WITH" }, { 557, "WOK" }, { 2024, "WOLF" },
+ { 558, "WON" }, { 2025, "WONT" }, { 559, "WOO" }, { 2026, "WOOD" },
+ { 2027, "WOOL" }, { 2028, "WORD" }, { 2029, "WORE" }, { 2030, "WORK" },
+ { 2031, "WORM" }, { 2032, "WORN" }, { 2033, "WOVE" }, { 560, "WOW" },
+ { 2034, "WRIT" }, { 561, "WRY" }, { 562, "WU" }, { 2035, "WYNN" },
+ { 2036, "YALE" }, { 563, "YAM" }, { 2037, "YANG" }, { 2038, "YANK" },
+ { 564, "YAP" }, { 2039, "YARD" }, { 2040, "YARN" }, { 565, "YAW" },
+ { 2041, "YAWL" }, { 2042, "YAWN" }, { 566, "YE" }, { 567, "YEA" },
+ { 2043, "YEAH" }, { 2044, "YEAR" }, { 2045, "YELL" }, { 568, "YES" },
+ { 569, "YET" }, { 2046, "YOGA" }, { 2047, "YOKE" }, { 570, "YOU" },
+};
+
+int otp_lookup_word(const char *word)
+{
+ int l, u, idx, c;
+ int first = *word - 'A';
+
+ if ((first < 0) || (first > 'Y' - 'A'))
+ return -1;
+
+ l = hints[first].l;
+ u = hints[first].u;
+ while (l < u) {
+ idx = (l + u) / 2;
+ c = strncmp(word, dictionary[idx].word, 4);
+
+ if (c < 0)
+ u = idx;
+ else if (c > 0)
+ l = idx + 1;
+ else
+ return dictionary[idx].value;
+ }
+
+ return -1;
+}
diff --git a/src/lib-otp/otp-dictionary.h b/src/lib-otp/otp-dictionary.h
new file mode 100644
index 0000000..18e9310
--- /dev/null
+++ b/src/lib-otp/otp-dictionary.h
@@ -0,0 +1,6 @@
+#ifndef OTP_DICTIONARY_H
+#define OTP_DICTIONARY_H
+
+int otp_lookup_word(const char *word);
+
+#endif
diff --git a/src/lib-otp/otp-hash.c b/src/lib-otp/otp-hash.c
new file mode 100644
index 0000000..c1b6f72
--- /dev/null
+++ b/src/lib-otp/otp-hash.c
@@ -0,0 +1,165 @@
+/*
+ * OTP hash generation.
+ *
+ * Copyright (c) 2006 Andrey Panin <pazke@donpac.ru>
+ *
+ * This software is released under the MIT license.
+ */
+
+#include "lib.h"
+#include "md4.h"
+#include "md5.h"
+#include "sha1.h"
+
+#include "otp.h"
+
+struct digest {
+ const char *name;
+ void (*init)(void *ctx);
+ void (*update)(void *ctx, const void *data, const size_t size);
+ void (*final)(void *ctx, void *res);
+ void (*otp_final)(void *ctx, void *res);
+};
+
+struct digest_context {
+ const struct digest *digest;
+ union {
+ struct md4_context md4_ctx;
+ struct md5_context md5_ctx;
+ struct sha1_ctxt sha1_ctx;
+ } ctx;
+};
+
+static void md4_fold(struct md4_context *ctx, void *res)
+{
+ uint32_t tmp[4], *p = res;
+
+ md4_final(ctx, (unsigned char *) tmp);
+
+ *p++ = tmp[0] ^ tmp[2];
+ *p = tmp[1] ^ tmp[3];
+}
+
+static void md5_fold(struct md5_context *ctx, void *res)
+{
+ uint32_t tmp[4], *p = res;
+
+ md5_final(ctx, (unsigned char *) tmp);
+
+ *p++ = tmp[0] ^ tmp[2];
+ *p = tmp[1] ^ tmp[3];
+}
+
+/*
+ * Sometimes I simply can't look at code generated by gcc.
+ */
+static inline uint32_t swab_uint32(uint32_t val)
+{
+#if defined(__GNUC__) && defined(__i386__)
+ asm("xchgb %b0, %h0\n"
+ "rorl $16, %0\n"
+ "xchgb %b0, %h0\n"
+ :"=q" (val)
+ : "0" (val));
+#else
+ val = ((val & 0xff) << 24) | ((val & 0xff00) << 8) |
+ ((val & 0xff0000) >> 8) | ((val >> 24) & 0xff);
+#endif
+ return val;
+}
+
+static void sha1_fold(struct sha1_ctxt *ctx, void *res)
+{
+ uint32_t tmp[5], *p = res;
+
+ sha1_result(ctx, tmp);
+
+ *p++ = swab_uint32(tmp[0] ^ tmp[2] ^ tmp[4]);
+ *p = swab_uint32(tmp[1] ^ tmp[3]);
+}
+
+
+#define F_INIT(name) ((void (*)(void *)) (name))
+#define F_UPDATE(name) ((void (*)(void *, const void *, size_t)) (name))
+#define F_FINAL(name) ((void (*)(void *, void *)) (name))
+#define F_FOLD(name) ((void (*)(void *, void *)) (name))
+
+static const struct digest digests[] = {
+ { "md4", F_INIT(md4_init), F_UPDATE(md4_update), F_FINAL(md4_final), F_FOLD(md4_fold) },
+ { "md5", F_INIT(md5_init), F_UPDATE(md5_update), F_FINAL(md5_final), F_FOLD(md5_fold) },
+ { "sha1", F_INIT(sha1_init), F_UPDATE(sha1_loop), F_FINAL(sha1_result), F_FOLD(sha1_fold) },
+};
+
+#undef F_INIT
+#undef F_UPDATE
+#undef F_FINAL
+#undef F_FOLD
+
+const char *digest_name(unsigned int algo)
+{
+ i_assert(algo < N_ELEMENTS(digests));
+
+ return digests[algo].name;
+}
+
+int digest_find(const char *name)
+{
+ unsigned int i;
+
+ for (i = 0; i < N_ELEMENTS(digests); i++)
+ if (strcmp(name, digests[i].name) == 0)
+ return i;
+
+ return -1;
+}
+
+void digest_init(struct digest_context *ctx, const unsigned int algo)
+{
+ i_assert(algo < N_ELEMENTS(digests));
+
+ ctx->digest = digests + algo;
+ ctx->digest->init(&ctx->ctx);
+}
+
+void digest_update(struct digest_context *ctx, const void *data,
+ const size_t size)
+{
+ ctx->digest->update(&ctx->ctx, data, size);
+}
+
+void digest_final(struct digest_context *ctx, unsigned char *result)
+{
+ ctx->digest->final(&ctx->ctx, result);
+}
+
+void digest_otp_final(struct digest_context *ctx, unsigned char *result)
+{
+ ctx->digest->otp_final(&ctx->ctx, result);
+}
+
+void otp_hash(unsigned int algo, const char *seed, const char *passphrase,
+ unsigned int step, unsigned char *result)
+{
+ struct digest_context ctx;
+
+ digest_init(&ctx, algo);
+ digest_update(&ctx, seed, strlen(seed));
+ digest_update(&ctx, passphrase, strlen(passphrase));
+ digest_otp_final(&ctx, result);
+
+ for (unsigned int i = 0; i < step; i++) {
+ digest_init(&ctx, algo);
+ digest_update(&ctx, result, OTP_HASH_SIZE);
+ digest_otp_final(&ctx, result);
+ }
+}
+
+void otp_next_hash(unsigned int algo, const unsigned char *prev,
+ unsigned char *result)
+{
+ struct digest_context ctx;
+
+ digest_init(&ctx, algo);
+ digest_update(&ctx, prev, OTP_HASH_SIZE);
+ digest_otp_final(&ctx, result);
+}
diff --git a/src/lib-otp/otp-hash.h b/src/lib-otp/otp-hash.h
new file mode 100644
index 0000000..152f28e
--- /dev/null
+++ b/src/lib-otp/otp-hash.h
@@ -0,0 +1,26 @@
+#ifndef OTP_HASH_H
+#define OTP_HASH_H
+
+struct digest_context;
+
+enum {
+ OTP_HASH_MD4,
+ OTP_HASH_MD5,
+ OTP_HASH_SHA1,
+};
+
+int digest_find(const char *name);
+void digest_init(struct digest_context *ctx, const unsigned int algo);
+void digest_update(struct digest_context *ctx, const void *data,
+ const size_t size);
+void digest_final(struct digest_context *ctx, unsigned char *result);
+void digest_otp_final(struct digest_context *ctx, unsigned char *result);
+const char *digest_name(unsigned int algo);
+
+void otp_hash(unsigned int algo, const char *seed, const char *passphrase,
+ unsigned int step, unsigned char *result);
+
+void otp_next_hash(unsigned int algo, const unsigned char *prev,
+ unsigned char *result);
+
+#endif
diff --git a/src/lib-otp/otp-parity.c b/src/lib-otp/otp-parity.c
new file mode 100644
index 0000000..d1ef038
--- /dev/null
+++ b/src/lib-otp/otp-parity.c
@@ -0,0 +1,29 @@
+/*
+ * OTP parity table.
+ *
+ * Copyright (c) 2006 Andrey Panin <pazke@donpac.ru>
+ *
+ * This software is released under the MIT license.
+ */
+
+#include "lib.h"
+#include "otp.h"
+
+const unsigned char parity_table[256] = {
+ 0, 1, 2, 3, 1, 2, 3, 0, 2, 3, 0, 1, 3, 0, 1, 2,
+ 1, 2, 3, 0, 2, 3, 0, 1, 3, 0, 1, 2, 0, 1, 2, 3,
+ 2, 3, 0, 1, 3, 0, 1, 2, 0, 1, 2, 3, 1, 2, 3, 0,
+ 3, 0, 1, 2, 0, 1, 2, 3, 1, 2, 3, 0, 2, 3, 0, 1,
+ 1, 2, 3, 0, 2, 3, 0, 1, 3, 0, 1, 2, 0, 1, 2, 3,
+ 2, 3, 0, 1, 3, 0, 1, 2, 0, 1, 2, 3, 1, 2, 3, 0,
+ 3, 0, 1, 2, 0, 1, 2, 3, 1, 2, 3, 0, 2, 3, 0, 1,
+ 0, 1, 2, 3, 1, 2, 3, 0, 2, 3, 0, 1, 3, 0, 1, 2,
+ 2, 3, 0, 1, 3, 0, 1, 2, 0, 1, 2, 3, 1, 2, 3, 0,
+ 3, 0, 1, 2, 0, 1, 2, 3, 1, 2, 3, 0, 2, 3, 0, 1,
+ 0, 1, 2, 3, 1, 2, 3, 0, 2, 3, 0, 1, 3, 0, 1, 2,
+ 1, 2, 3, 0, 2, 3, 0, 1, 3, 0, 1, 2, 0, 1, 2, 3,
+ 3, 0, 1, 2, 0, 1, 2, 3, 1, 2, 3, 0, 2, 3, 0, 1,
+ 0, 1, 2, 3, 1, 2, 3, 0, 2, 3, 0, 1, 3, 0, 1, 2,
+ 1, 2, 3, 0, 2, 3, 0, 1, 3, 0, 1, 2, 0, 1, 2, 3,
+ 2, 3, 0, 1, 3, 0, 1, 2, 0, 1, 2, 3, 1, 2, 3, 0,
+};
diff --git a/src/lib-otp/otp-parity.h b/src/lib-otp/otp-parity.h
new file mode 100644
index 0000000..86ea18f
--- /dev/null
+++ b/src/lib-otp/otp-parity.h
@@ -0,0 +1,16 @@
+#ifndef OTP_PARITY_H
+#define OTP_PARITY_H
+
+extern const unsigned char parity_table[256];
+
+static inline unsigned int otp_parity(unsigned char *data)
+{
+ unsigned int i, parity = 0;
+
+ for (i = 0; i < OTP_HASH_SIZE; i++)
+ parity += parity_table[*data++];
+
+ return parity & 3;
+}
+
+#endif
diff --git a/src/lib-otp/otp-parse.c b/src/lib-otp/otp-parse.c
new file mode 100644
index 0000000..70b6940
--- /dev/null
+++ b/src/lib-otp/otp-parse.c
@@ -0,0 +1,253 @@
+/*
+ * OTP extended response parser.
+ *
+ * Copyright (c) 2006 Andrey Panin <pazke@donpac.ru>
+ *
+ * This software is released under the MIT license.
+ */
+
+#include "lib.h"
+#include "buffer.h"
+#include "str.h"
+#include "strfuncs.h"
+#include "hex-binary.h"
+
+#include "otp.h"
+
+#include <ctype.h>
+
+#define IS_LWS(c) ((c) == ' ' || (c) == '\t')
+
+static inline const char *otp_skip_lws(const char *data)
+{
+ while (*data != '\0' && IS_LWS(*data))
+ data++;
+ return data;
+}
+
+static inline bool otp_check_tail(const char *data)
+{
+ data = otp_skip_lws(data);
+
+ return *data != 0;
+}
+
+int otp_read_hex(const char *data, const char **endptr, unsigned char *hash)
+{
+ string_t *str;
+ buffer_t buf;
+ unsigned int i = 0;
+
+ if (data == NULL)
+ return -1;
+
+ str = t_str_new(18);
+ buffer_create_from_data(&buf, hash, OTP_HASH_SIZE);
+
+ while (*data != '\0') {
+ char c = *data;
+
+ if (i_isxdigit(c)) {
+ str_append_c(str, c);
+ if (++i == OTP_HASH_SIZE * 2) {
+ data++;
+ break;
+ }
+ } else if (!IS_LWS(c)) {
+ *endptr = data;
+ return -1;
+ }
+ data++;
+ }
+
+ *endptr = data;
+
+ if (i < OTP_HASH_SIZE * 2)
+ return -1;
+
+ return hex_to_binary(str_c(str), &buf);
+}
+
+#define add_word() do { \
+ tmp = otp_lookup_word(str_c(word)); \
+ buffer_append(&buf, &tmp, sizeof(tmp)); \
+ count++; \
+} while (0)
+
+int ATTR_NO_SANITIZE_INTEGER
+otp_read_words(const char *data, const char **endptr, unsigned char *hash)
+{
+ bool space = FALSE;
+ unsigned int len = 0, count = 0;
+ unsigned int parity = 0, bits[OTP_WORDS_NUMBER], tmp;
+ string_t *word;
+ buffer_t buf;
+
+ if (data == NULL)
+ return -1;
+
+ word = t_str_new(8);
+
+ data = otp_skip_lws(data);
+
+ buffer_create_from_data(&buf, bits, sizeof(bits));
+
+ for (; *data != '\0' && (count < OTP_WORDS_NUMBER); data++) {
+ char c = *data;
+
+ if (space) {
+ if (IS_LWS(c))
+ continue;
+ else if (i_isalpha(c)) {
+ str_append_c(word, c);
+ space = FALSE;
+ len = 1;
+ continue;
+ }
+ } else {
+ if (i_isalpha(c)) {
+ if (++len > OTP_MAX_WORD_LEN) {
+ count = 0;
+ break;
+ }
+ str_append_c(word, c);
+ continue;
+ } else if (IS_LWS(c)) {
+ add_word();
+ str_truncate(word, 0);
+ space = TRUE;
+ continue;
+ }
+ }
+ break;
+ }
+
+ if ((str_len(word) > 0) && (count == OTP_WORDS_NUMBER - 1))
+ add_word();
+
+ if (endptr != NULL)
+ *endptr = data;
+
+ if (count < OTP_WORDS_NUMBER)
+ return -1;
+
+ hash[0] = bits[0] >> 3;
+ hash[1] = ((bits[0] & 7) << 5) | (bits[1] >> 6);
+ hash[2] = ((bits[1] & 0x3f) << 2) | (bits[2] >> 9);
+ hash[3] = (bits[2] >> 1) & 0xff;
+ hash[4] = ((bits[2] & 3) << 7) | (bits[3] >> 4);
+ hash[5] = ((bits[3] & 15) << 4) | (bits[4] >> 7);
+ hash[6] = ((bits[4] & 0x7f) << 1) | (bits[5] >> 10);
+ hash[7] = (bits[5] >> 2) & 0xff;
+ parity = bits[5] & 3;
+
+ return otp_parity(hash) != parity ? 1 : 0;
+}
+
+int otp_read_new_params(const char *data, const char **endptr,
+ struct otp_state *state)
+{
+ const char *p, *s;
+ unsigned int i = 0;
+ int algo;
+
+ s = p = data;
+
+ while ((*p != 0) && !IS_LWS(*p)) p++;
+ if (*p == 0)
+ return -1;
+
+ algo = digest_find(t_strdup_until(s, p++));
+ if (algo < 0)
+ return -2;
+ state->algo = algo;
+
+ s = p;
+ if (str_parse_int(s, &state->seq, &p) < 0 || !IS_LWS(*p))
+ return -3;
+ p++;
+
+ while (i_isalnum(*p) && (i < OTP_MAX_SEED_LEN))
+ state->seed[i++] = i_tolower(*p++);
+ state->seed[i] = 0;
+
+ *endptr = p;
+ return 0;
+}
+
+int otp_parse_response(const char *data, unsigned char *hash, bool hex)
+{
+ const char *end;
+ int ret = hex ? otp_read_hex(data, &end, hash) :
+ otp_read_words(data, &end, hash);
+ if (ret < 0)
+ return ret;
+
+ return otp_check_tail(end) ? 1 : 0;
+}
+
+int otp_parse_init_response(const char *data, struct otp_state *new_state,
+ unsigned char *hash, bool hex, const char **error)
+{
+ const char *end;
+ int ret = hex ? otp_read_hex(data, &end, hash) :
+ otp_read_words(data, &end, hash);
+ if (ret < 0) {
+ *error = "invalid current OTP";
+ return ret;
+ }
+
+ end = otp_skip_lws(end);
+ if (*end++ != ':') {
+ *error = "missing colon";
+ return -1;
+ }
+
+ ret = otp_read_new_params(end, &end, new_state);
+ if (ret < 0) {
+ *error = "invalid OTP parameters";
+ return -1;
+ }
+
+ end = otp_skip_lws(end);
+ if (*end++ != ':') {
+ *error = "missing colon";
+ return -1;
+ }
+
+ ret = hex ? otp_read_hex(end, &end, new_state->hash) :
+ otp_read_words(end, &end, new_state->hash);
+ if (ret < 0) {
+ *error = "invalid new OTP";
+ return -1;
+ }
+
+ if (otp_check_tail(end)) {
+ *error = "trailing garbage found";
+ return -1;
+ }
+
+ return 0;
+}
+
+int otp_parse_dbentry(const char *text, struct otp_state *state)
+{
+ const char *end;
+ int ret;
+
+ ret = otp_read_new_params(text, &end, state);
+ if (ret != 0)
+ return ret;
+
+ if (*end++ != ' ')
+ return -1;
+
+ return otp_read_hex(end, &end, state->hash);
+}
+
+const char *otp_print_dbentry(const struct otp_state *state)
+{
+ return t_strdup_printf("%s %d %s %s", digest_name(state->algo),
+ state->seq, state->seed,
+ binary_to_hex(state->hash, 8));
+}
diff --git a/src/lib-otp/otp-parse.h b/src/lib-otp/otp-parse.h
new file mode 100644
index 0000000..527c676
--- /dev/null
+++ b/src/lib-otp/otp-parse.h
@@ -0,0 +1,16 @@
+#ifndef OTP_PARSE_H
+#define OTP_PARSE_H
+
+int otp_read_hex(const char *data, const char **endptr, unsigned char *hash);
+int otp_read_words(const char *data, const char **endptr, unsigned char *hash);
+int otp_read_new_params(const char *data, const char **endptr,
+ struct otp_state *state);
+
+int otp_parse_response(const char *data, unsigned char *hash, bool hex);
+int otp_parse_init_response(const char *data, struct otp_state *new_state,
+ unsigned char *hash, bool hex, const char **error);
+
+int otp_parse_dbentry(const char *text, struct otp_state *state);
+const char *otp_print_dbentry(const struct otp_state *state);
+
+#endif
diff --git a/src/lib-otp/otp.h b/src/lib-otp/otp.h
new file mode 100644
index 0000000..9cd3dfb
--- /dev/null
+++ b/src/lib-otp/otp.h
@@ -0,0 +1,22 @@
+#ifndef OTP_H
+#define OTP_H
+
+#define OTP_MAX_SEED_LEN 16
+#define OTP_MAX_WORD_LEN 4
+#define OTP_WORDS_NUMBER 6
+
+#define OTP_HASH_SIZE 8
+
+struct otp_state {
+ unsigned int algo;
+ int seq;
+ unsigned char hash[OTP_HASH_SIZE];
+ char seed[OTP_MAX_SEED_LEN + 1];
+};
+
+#include "otp-hash.h"
+#include "otp-dictionary.h"
+#include "otp-parity.h"
+#include "otp-parse.h"
+
+#endif
diff --git a/src/lib-program-client/Makefile.am b/src/lib-program-client/Makefile.am
new file mode 100644
index 0000000..4198606
--- /dev/null
+++ b/src/lib-program-client/Makefile.am
@@ -0,0 +1,55 @@
+noinst_LTLIBRARIES = libprogram_client.la
+
+AM_CPPFLAGS = \
+ -I$(top_srcdir)/src/lib \
+ -I$(top_srcdir)/src/lib-test \
+ -I$(top_srcdir)/src/lib-dns \
+ -I$(top_srcdir)/src/lib-mail
+
+libprogram_client_la_SOURCES = \
+ program-client.c \
+ program-client-local.c \
+ program-client-remote.c
+
+headers = \
+ program-client.h
+
+noinst_HEADERS = \
+ program-client-private.h
+
+pkginc_libdir=$(pkgincludedir)
+pkginc_lib_HEADERS = $(headers)
+
+test_programs = \
+ test-program-client-local \
+ test-program-client-unix \
+ test-program-client-net
+
+noinst_PROGRAMS = $(test_programs)
+
+test_libs = \
+ libprogram_client.la \
+ ../lib-dns/libdns.la \
+ ../lib-test/libtest.la \
+ ../lib-mail/libmail.la \
+ ../lib-charset/libcharset.la \
+ ../lib/liblib.la \
+ $(MODULE_LIBS)
+
+test_program_client_local_SOURCE = test-program-client-local.c
+test_program_client_local_LDADD = $(test_libs)
+
+test_program_client_unix_SOURCE = test-program-client-unix.c
+test_program_client_unix_LDADD = $(test_libs)
+
+test_program_client_net_SOURCE = test-program-client-net.c
+test_program_client_net_LDADD = $(test_libs)
+
+check-local:
+ for bin in $(test_programs); do \
+ if test "$$bin" = "test-program-client-local"; then \
+ if ! env NOCHILDREN=yes $(RUN_TEST) ./$$bin; then exit 1; fi; \
+ else \
+ if ! $(RUN_TEST) ./$$bin; then exit 1; fi; \
+ fi \
+ done
diff --git a/src/lib-program-client/Makefile.in b/src/lib-program-client/Makefile.in
new file mode 100644
index 0000000..f899901
--- /dev/null
+++ b/src/lib-program-client/Makefile.in
@@ -0,0 +1,912 @@
+# Makefile.in generated by automake 1.16.1 from Makefile.am.
+# @configure_input@
+
+# Copyright (C) 1994-2018 Free Software Foundation, Inc.
+
+# This Makefile.in is free software; the Free Software Foundation
+# gives unlimited permission to copy and/or distribute it,
+# with or without modifications, as long as this notice is preserved.
+
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY, to the extent permitted by law; without
+# even the implied warranty of MERCHANTABILITY or FITNESS FOR A
+# PARTICULAR PURPOSE.
+
+@SET_MAKE@
+
+
+
+VPATH = @srcdir@
+am__is_gnu_make = { \
+ if test -z '$(MAKELEVEL)'; then \
+ false; \
+ elif test -n '$(MAKE_HOST)'; then \
+ true; \
+ elif test -n '$(MAKE_VERSION)' && test -n '$(CURDIR)'; then \
+ true; \
+ else \
+ false; \
+ fi; \
+}
+am__make_running_with_option = \
+ case $${target_option-} in \
+ ?) ;; \
+ *) echo "am__make_running_with_option: internal error: invalid" \
+ "target option '$${target_option-}' specified" >&2; \
+ exit 1;; \
+ esac; \
+ has_opt=no; \
+ sane_makeflags=$$MAKEFLAGS; \
+ if $(am__is_gnu_make); then \
+ sane_makeflags=$$MFLAGS; \
+ else \
+ case $$MAKEFLAGS in \
+ *\\[\ \ ]*) \
+ bs=\\; \
+ sane_makeflags=`printf '%s\n' "$$MAKEFLAGS" \
+ | sed "s/$$bs$$bs[$$bs $$bs ]*//g"`;; \
+ esac; \
+ fi; \
+ skip_next=no; \
+ strip_trailopt () \
+ { \
+ flg=`printf '%s\n' "$$flg" | sed "s/$$1.*$$//"`; \
+ }; \
+ for flg in $$sane_makeflags; do \
+ test $$skip_next = yes && { skip_next=no; continue; }; \
+ case $$flg in \
+ *=*|--*) continue;; \
+ -*I) strip_trailopt 'I'; skip_next=yes;; \
+ -*I?*) strip_trailopt 'I';; \
+ -*O) strip_trailopt 'O'; skip_next=yes;; \
+ -*O?*) strip_trailopt 'O';; \
+ -*l) strip_trailopt 'l'; skip_next=yes;; \
+ -*l?*) strip_trailopt 'l';; \
+ -[dEDm]) skip_next=yes;; \
+ -[JT]) skip_next=yes;; \
+ esac; \
+ case $$flg in \
+ *$$target_option*) has_opt=yes; break;; \
+ esac; \
+ done; \
+ test $$has_opt = yes
+am__make_dryrun = (target_option=n; $(am__make_running_with_option))
+am__make_keepgoing = (target_option=k; $(am__make_running_with_option))
+pkgdatadir = $(datadir)/@PACKAGE@
+pkgincludedir = $(includedir)/@PACKAGE@
+pkglibdir = $(libdir)/@PACKAGE@
+pkglibexecdir = $(libexecdir)/@PACKAGE@
+am__cd = CDPATH="$${ZSH_VERSION+.}$(PATH_SEPARATOR)" && cd
+install_sh_DATA = $(install_sh) -c -m 644
+install_sh_PROGRAM = $(install_sh) -c
+install_sh_SCRIPT = $(install_sh) -c
+INSTALL_HEADER = $(INSTALL_DATA)
+transform = $(program_transform_name)
+NORMAL_INSTALL = :
+PRE_INSTALL = :
+POST_INSTALL = :
+NORMAL_UNINSTALL = :
+PRE_UNINSTALL = :
+POST_UNINSTALL = :
+build_triplet = @build@
+host_triplet = @host@
+noinst_PROGRAMS = $(am__EXEEXT_1)
+subdir = src/lib-program-client
+ACLOCAL_M4 = $(top_srcdir)/aclocal.m4
+am__aclocal_m4_deps = $(top_srcdir)/m4/ac_checktype2.m4 \
+ $(top_srcdir)/m4/ac_typeof.m4 $(top_srcdir)/m4/arc4random.m4 \
+ $(top_srcdir)/m4/blockdev.m4 $(top_srcdir)/m4/c99_vsnprintf.m4 \
+ $(top_srcdir)/m4/clock_gettime.m4 $(top_srcdir)/m4/crypt.m4 \
+ $(top_srcdir)/m4/crypt_xpg6.m4 $(top_srcdir)/m4/dbqlk.m4 \
+ $(top_srcdir)/m4/dirent_dtype.m4 $(top_srcdir)/m4/dovecot.m4 \
+ $(top_srcdir)/m4/fd_passing.m4 $(top_srcdir)/m4/fdatasync.m4 \
+ $(top_srcdir)/m4/flexible_array_member.m4 \
+ $(top_srcdir)/m4/glibc.m4 $(top_srcdir)/m4/gmtime_max.m4 \
+ $(top_srcdir)/m4/gmtime_tm_gmtoff.m4 \
+ $(top_srcdir)/m4/ioloop.m4 $(top_srcdir)/m4/iovec.m4 \
+ $(top_srcdir)/m4/ipv6.m4 $(top_srcdir)/m4/libcap.m4 \
+ $(top_srcdir)/m4/libtool.m4 $(top_srcdir)/m4/libwrap.m4 \
+ $(top_srcdir)/m4/linux_mremap.m4 $(top_srcdir)/m4/ltoptions.m4 \
+ $(top_srcdir)/m4/ltsugar.m4 $(top_srcdir)/m4/ltversion.m4 \
+ $(top_srcdir)/m4/lt~obsolete.m4 $(top_srcdir)/m4/mmap_write.m4 \
+ $(top_srcdir)/m4/mntctl.m4 $(top_srcdir)/m4/modules.m4 \
+ $(top_srcdir)/m4/notify.m4 $(top_srcdir)/m4/nsl.m4 \
+ $(top_srcdir)/m4/off_t_max.m4 $(top_srcdir)/m4/pkg.m4 \
+ $(top_srcdir)/m4/pr_set_dumpable.m4 \
+ $(top_srcdir)/m4/q_quotactl.m4 $(top_srcdir)/m4/quota.m4 \
+ $(top_srcdir)/m4/random.m4 $(top_srcdir)/m4/rlimit.m4 \
+ $(top_srcdir)/m4/sendfile.m4 $(top_srcdir)/m4/size_t_signed.m4 \
+ $(top_srcdir)/m4/sockpeercred.m4 $(top_srcdir)/m4/sql.m4 \
+ $(top_srcdir)/m4/ssl.m4 $(top_srcdir)/m4/st_tim.m4 \
+ $(top_srcdir)/m4/static_array.m4 $(top_srcdir)/m4/test_with.m4 \
+ $(top_srcdir)/m4/time_t.m4 $(top_srcdir)/m4/typeof.m4 \
+ $(top_srcdir)/m4/typeof_dev_t.m4 \
+ $(top_srcdir)/m4/uoff_t_max.m4 $(top_srcdir)/m4/vararg.m4 \
+ $(top_srcdir)/m4/want_apparmor.m4 \
+ $(top_srcdir)/m4/want_bsdauth.m4 \
+ $(top_srcdir)/m4/want_bzlib.m4 \
+ $(top_srcdir)/m4/want_cassandra.m4 \
+ $(top_srcdir)/m4/want_cdb.m4 \
+ $(top_srcdir)/m4/want_checkpassword.m4 \
+ $(top_srcdir)/m4/want_clucene.m4 $(top_srcdir)/m4/want_db.m4 \
+ $(top_srcdir)/m4/want_gssapi.m4 $(top_srcdir)/m4/want_icu.m4 \
+ $(top_srcdir)/m4/want_ldap.m4 $(top_srcdir)/m4/want_lua.m4 \
+ $(top_srcdir)/m4/want_lz4.m4 $(top_srcdir)/m4/want_lzma.m4 \
+ $(top_srcdir)/m4/want_mysql.m4 $(top_srcdir)/m4/want_pam.m4 \
+ $(top_srcdir)/m4/want_passwd.m4 $(top_srcdir)/m4/want_pgsql.m4 \
+ $(top_srcdir)/m4/want_prefetch.m4 \
+ $(top_srcdir)/m4/want_shadow.m4 \
+ $(top_srcdir)/m4/want_sodium.m4 $(top_srcdir)/m4/want_solr.m4 \
+ $(top_srcdir)/m4/want_sqlite.m4 \
+ $(top_srcdir)/m4/want_stemmer.m4 \
+ $(top_srcdir)/m4/want_systemd.m4 \
+ $(top_srcdir)/m4/want_textcat.m4 \
+ $(top_srcdir)/m4/want_unwind.m4 $(top_srcdir)/m4/want_zlib.m4 \
+ $(top_srcdir)/m4/want_zstd.m4 $(top_srcdir)/configure.ac
+am__configure_deps = $(am__aclocal_m4_deps) $(CONFIGURE_DEPENDENCIES) \
+ $(ACLOCAL_M4)
+DIST_COMMON = $(srcdir)/Makefile.am $(noinst_HEADERS) \
+ $(pkginc_lib_HEADERS) $(am__DIST_COMMON)
+mkinstalldirs = $(install_sh) -d
+CONFIG_HEADER = $(top_builddir)/config.h
+CONFIG_CLEAN_FILES =
+CONFIG_CLEAN_VPATH_FILES =
+am__EXEEXT_1 = test-program-client-local$(EXEEXT) \
+ test-program-client-unix$(EXEEXT) \
+ test-program-client-net$(EXEEXT)
+PROGRAMS = $(noinst_PROGRAMS)
+LTLIBRARIES = $(noinst_LTLIBRARIES)
+libprogram_client_la_LIBADD =
+am_libprogram_client_la_OBJECTS = program-client.lo \
+ program-client-local.lo program-client-remote.lo
+libprogram_client_la_OBJECTS = $(am_libprogram_client_la_OBJECTS)
+AM_V_lt = $(am__v_lt_@AM_V@)
+am__v_lt_ = $(am__v_lt_@AM_DEFAULT_V@)
+am__v_lt_0 = --silent
+am__v_lt_1 =
+test_program_client_local_SOURCES = test-program-client-local.c
+test_program_client_local_OBJECTS = \
+ test-program-client-local.$(OBJEXT)
+am__DEPENDENCIES_1 =
+am__DEPENDENCIES_2 = libprogram_client.la ../lib-dns/libdns.la \
+ ../lib-test/libtest.la ../lib-mail/libmail.la \
+ ../lib-charset/libcharset.la ../lib/liblib.la \
+ $(am__DEPENDENCIES_1)
+test_program_client_local_DEPENDENCIES = $(am__DEPENDENCIES_2)
+test_program_client_net_SOURCES = test-program-client-net.c
+test_program_client_net_OBJECTS = test-program-client-net.$(OBJEXT)
+test_program_client_net_DEPENDENCIES = $(am__DEPENDENCIES_2)
+test_program_client_unix_SOURCES = test-program-client-unix.c
+test_program_client_unix_OBJECTS = test-program-client-unix.$(OBJEXT)
+test_program_client_unix_DEPENDENCIES = $(am__DEPENDENCIES_2)
+AM_V_P = $(am__v_P_@AM_V@)
+am__v_P_ = $(am__v_P_@AM_DEFAULT_V@)
+am__v_P_0 = false
+am__v_P_1 = :
+AM_V_GEN = $(am__v_GEN_@AM_V@)
+am__v_GEN_ = $(am__v_GEN_@AM_DEFAULT_V@)
+am__v_GEN_0 = @echo " GEN " $@;
+am__v_GEN_1 =
+AM_V_at = $(am__v_at_@AM_V@)
+am__v_at_ = $(am__v_at_@AM_DEFAULT_V@)
+am__v_at_0 = @
+am__v_at_1 =
+DEFAULT_INCLUDES = -I.@am__isrc@ -I$(top_builddir)
+depcomp = $(SHELL) $(top_srcdir)/depcomp
+am__maybe_remake_depfiles = depfiles
+am__depfiles_remade = ./$(DEPDIR)/program-client-local.Plo \
+ ./$(DEPDIR)/program-client-remote.Plo \
+ ./$(DEPDIR)/program-client.Plo \
+ ./$(DEPDIR)/test-program-client-local.Po \
+ ./$(DEPDIR)/test-program-client-net.Po \
+ ./$(DEPDIR)/test-program-client-unix.Po
+am__mv = mv -f
+COMPILE = $(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) \
+ $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS)
+LTCOMPILE = $(LIBTOOL) $(AM_V_lt) --tag=CC $(AM_LIBTOOLFLAGS) \
+ $(LIBTOOLFLAGS) --mode=compile $(CC) $(DEFS) \
+ $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) $(CPPFLAGS) \
+ $(AM_CFLAGS) $(CFLAGS)
+AM_V_CC = $(am__v_CC_@AM_V@)
+am__v_CC_ = $(am__v_CC_@AM_DEFAULT_V@)
+am__v_CC_0 = @echo " CC " $@;
+am__v_CC_1 =
+CCLD = $(CC)
+LINK = $(LIBTOOL) $(AM_V_lt) --tag=CC $(AM_LIBTOOLFLAGS) \
+ $(LIBTOOLFLAGS) --mode=link $(CCLD) $(AM_CFLAGS) $(CFLAGS) \
+ $(AM_LDFLAGS) $(LDFLAGS) -o $@
+AM_V_CCLD = $(am__v_CCLD_@AM_V@)
+am__v_CCLD_ = $(am__v_CCLD_@AM_DEFAULT_V@)
+am__v_CCLD_0 = @echo " CCLD " $@;
+am__v_CCLD_1 =
+SOURCES = $(libprogram_client_la_SOURCES) test-program-client-local.c \
+ test-program-client-net.c test-program-client-unix.c
+DIST_SOURCES = $(libprogram_client_la_SOURCES) \
+ test-program-client-local.c test-program-client-net.c \
+ test-program-client-unix.c
+am__can_run_installinfo = \
+ case $$AM_UPDATE_INFO_DIR in \
+ n|no|NO) false;; \
+ *) (install-info --version) >/dev/null 2>&1;; \
+ esac
+am__vpath_adj_setup = srcdirstrip=`echo "$(srcdir)" | sed 's|.|.|g'`;
+am__vpath_adj = case $$p in \
+ $(srcdir)/*) f=`echo "$$p" | sed "s|^$$srcdirstrip/||"`;; \
+ *) f=$$p;; \
+ esac;
+am__strip_dir = f=`echo $$p | sed -e 's|^.*/||'`;
+am__install_max = 40
+am__nobase_strip_setup = \
+ srcdirstrip=`echo "$(srcdir)" | sed 's/[].[^$$\\*|]/\\\\&/g'`
+am__nobase_strip = \
+ for p in $$list; do echo "$$p"; done | sed -e "s|$$srcdirstrip/||"
+am__nobase_list = $(am__nobase_strip_setup); \
+ for p in $$list; do echo "$$p $$p"; done | \
+ sed "s| $$srcdirstrip/| |;"' / .*\//!s/ .*/ ./; s,\( .*\)/[^/]*$$,\1,' | \
+ $(AWK) 'BEGIN { files["."] = "" } { files[$$2] = files[$$2] " " $$1; \
+ if (++n[$$2] == $(am__install_max)) \
+ { print $$2, files[$$2]; n[$$2] = 0; files[$$2] = "" } } \
+ END { for (dir in files) print dir, files[dir] }'
+am__base_list = \
+ sed '$$!N;$$!N;$$!N;$$!N;$$!N;$$!N;$$!N;s/\n/ /g' | \
+ sed '$$!N;$$!N;$$!N;$$!N;s/\n/ /g'
+am__uninstall_files_from_dir = { \
+ test -z "$$files" \
+ || { test ! -d "$$dir" && test ! -f "$$dir" && test ! -r "$$dir"; } \
+ || { echo " ( cd '$$dir' && rm -f" $$files ")"; \
+ $(am__cd) "$$dir" && rm -f $$files; }; \
+ }
+am__installdirs = "$(DESTDIR)$(pkginc_libdir)"
+HEADERS = $(noinst_HEADERS) $(pkginc_lib_HEADERS)
+am__tagged_files = $(HEADERS) $(SOURCES) $(TAGS_FILES) $(LISP)
+# Read a list of newline-separated strings from the standard input,
+# and print each of them once, without duplicates. Input order is
+# *not* preserved.
+am__uniquify_input = $(AWK) '\
+ BEGIN { nonempty = 0; } \
+ { items[$$0] = 1; nonempty = 1; } \
+ END { if (nonempty) { for (i in items) print i; }; } \
+'
+# Make sure the list of sources is unique. This is necessary because,
+# e.g., the same source file might be shared among _SOURCES variables
+# for different programs/libraries.
+am__define_uniq_tagged_files = \
+ list='$(am__tagged_files)'; \
+ unique=`for i in $$list; do \
+ if test -f "$$i"; then echo $$i; else echo $(srcdir)/$$i; fi; \
+ done | $(am__uniquify_input)`
+ETAGS = etags
+CTAGS = ctags
+am__DIST_COMMON = $(srcdir)/Makefile.in $(top_srcdir)/depcomp
+DISTFILES = $(DIST_COMMON) $(DIST_SOURCES) $(TEXINFOS) $(EXTRA_DIST)
+ACLOCAL = @ACLOCAL@
+ACLOCAL_AMFLAGS = @ACLOCAL_AMFLAGS@
+AMTAR = @AMTAR@
+AM_DEFAULT_VERBOSITY = @AM_DEFAULT_VERBOSITY@
+APPARMOR_LIBS = @APPARMOR_LIBS@
+AR = @AR@
+AUTH_CFLAGS = @AUTH_CFLAGS@
+AUTH_LIBS = @AUTH_LIBS@
+AUTOCONF = @AUTOCONF@
+AUTOHEADER = @AUTOHEADER@
+AUTOMAKE = @AUTOMAKE@
+AWK = @AWK@
+BINARY_CFLAGS = @BINARY_CFLAGS@
+BINARY_LDFLAGS = @BINARY_LDFLAGS@
+BISON = @BISON@
+CASSANDRA_CFLAGS = @CASSANDRA_CFLAGS@
+CASSANDRA_LIBS = @CASSANDRA_LIBS@
+CC = @CC@
+CCDEPMODE = @CCDEPMODE@
+CDB_LIBS = @CDB_LIBS@
+CFLAGS = @CFLAGS@
+CLUCENE_CFLAGS = @CLUCENE_CFLAGS@
+CLUCENE_LIBS = @CLUCENE_LIBS@
+COMPRESS_LIBS = @COMPRESS_LIBS@
+CPP = @CPP@
+CPPFLAGS = @CPPFLAGS@
+CRYPT_LIBS = @CRYPT_LIBS@
+CXX = @CXX@
+CXXCPP = @CXXCPP@
+CXXDEPMODE = @CXXDEPMODE@
+CXXFLAGS = @CXXFLAGS@
+CYGPATH_W = @CYGPATH_W@
+DEFS = @DEFS@
+DEPDIR = @DEPDIR@
+DICT_LIBS = @DICT_LIBS@
+DLLIB = @DLLIB@
+DLLTOOL = @DLLTOOL@
+DSYMUTIL = @DSYMUTIL@
+DUMPBIN = @DUMPBIN@
+ECHO_C = @ECHO_C@
+ECHO_N = @ECHO_N@
+ECHO_T = @ECHO_T@
+EGREP = @EGREP@
+EXEEXT = @EXEEXT@
+FGREP = @FGREP@
+FLEX = @FLEX@
+FUZZER_CPPFLAGS = @FUZZER_CPPFLAGS@
+FUZZER_LDFLAGS = @FUZZER_LDFLAGS@
+GREP = @GREP@
+INSTALL = @INSTALL@
+INSTALL_DATA = @INSTALL_DATA@
+INSTALL_PROGRAM = @INSTALL_PROGRAM@
+INSTALL_SCRIPT = @INSTALL_SCRIPT@
+INSTALL_STRIP_PROGRAM = @INSTALL_STRIP_PROGRAM@
+KRB5CONFIG = @KRB5CONFIG@
+KRB5_CFLAGS = @KRB5_CFLAGS@
+KRB5_LIBS = @KRB5_LIBS@
+LD = @LD@
+LDAP_LIBS = @LDAP_LIBS@
+LDFLAGS = @LDFLAGS@
+LD_NO_WHOLE_ARCHIVE = @LD_NO_WHOLE_ARCHIVE@
+LD_WHOLE_ARCHIVE = @LD_WHOLE_ARCHIVE@
+LIBCAP = @LIBCAP@
+LIBDOVECOT = @LIBDOVECOT@
+LIBDOVECOT_COMPRESS = @LIBDOVECOT_COMPRESS@
+LIBDOVECOT_DEPS = @LIBDOVECOT_DEPS@
+LIBDOVECOT_DSYNC = @LIBDOVECOT_DSYNC@
+LIBDOVECOT_LA_LIBS = @LIBDOVECOT_LA_LIBS@
+LIBDOVECOT_LDA = @LIBDOVECOT_LDA@
+LIBDOVECOT_LDAP = @LIBDOVECOT_LDAP@
+LIBDOVECOT_LIBFTS = @LIBDOVECOT_LIBFTS@
+LIBDOVECOT_LIBFTS_DEPS = @LIBDOVECOT_LIBFTS_DEPS@
+LIBDOVECOT_LOGIN = @LIBDOVECOT_LOGIN@
+LIBDOVECOT_LUA = @LIBDOVECOT_LUA@
+LIBDOVECOT_LUA_DEPS = @LIBDOVECOT_LUA_DEPS@
+LIBDOVECOT_SQL = @LIBDOVECOT_SQL@
+LIBDOVECOT_STORAGE = @LIBDOVECOT_STORAGE@
+LIBDOVECOT_STORAGE_DEPS = @LIBDOVECOT_STORAGE_DEPS@
+LIBEXTTEXTCAT_CFLAGS = @LIBEXTTEXTCAT_CFLAGS@
+LIBEXTTEXTCAT_LIBS = @LIBEXTTEXTCAT_LIBS@
+LIBICONV = @LIBICONV@
+LIBICU_CFLAGS = @LIBICU_CFLAGS@
+LIBICU_LIBS = @LIBICU_LIBS@
+LIBOBJS = @LIBOBJS@
+LIBS = @LIBS@
+LIBSODIUM_CFLAGS = @LIBSODIUM_CFLAGS@
+LIBSODIUM_LIBS = @LIBSODIUM_LIBS@
+LIBTIRPC_CFLAGS = @LIBTIRPC_CFLAGS@
+LIBTIRPC_LIBS = @LIBTIRPC_LIBS@
+LIBTOOL = @LIBTOOL@
+LIBUNWIND_CFLAGS = @LIBUNWIND_CFLAGS@
+LIBUNWIND_LIBS = @LIBUNWIND_LIBS@
+LIBWRAP_LIBS = @LIBWRAP_LIBS@
+LINKED_STORAGE_LDADD = @LINKED_STORAGE_LDADD@
+LIPO = @LIPO@
+LN_S = @LN_S@
+LTLIBICONV = @LTLIBICONV@
+LTLIBOBJS = @LTLIBOBJS@
+LT_SYS_LIBRARY_PATH = @LT_SYS_LIBRARY_PATH@
+LUA_CFLAGS = @LUA_CFLAGS@
+LUA_LIBS = @LUA_LIBS@
+MAINT = @MAINT@
+MAKEINFO = @MAKEINFO@
+MANIFEST_TOOL = @MANIFEST_TOOL@
+MKDIR_P = @MKDIR_P@
+MODULE_LIBS = @MODULE_LIBS@
+MODULE_SUFFIX = @MODULE_SUFFIX@
+MYSQL_CFLAGS = @MYSQL_CFLAGS@
+MYSQL_CONFIG = @MYSQL_CONFIG@
+MYSQL_LIBS = @MYSQL_LIBS@
+NM = @NM@
+NMEDIT = @NMEDIT@
+NOPLUGIN_LDFLAGS = @NOPLUGIN_LDFLAGS@
+OBJDUMP = @OBJDUMP@
+OBJEXT = @OBJEXT@
+OTOOL = @OTOOL@
+OTOOL64 = @OTOOL64@
+PACKAGE = @PACKAGE@
+PACKAGE_BUGREPORT = @PACKAGE_BUGREPORT@
+PACKAGE_NAME = @PACKAGE_NAME@
+PACKAGE_STRING = @PACKAGE_STRING@
+PACKAGE_TARNAME = @PACKAGE_TARNAME@
+PACKAGE_URL = @PACKAGE_URL@
+PACKAGE_VERSION = @PACKAGE_VERSION@
+PANDOC = @PANDOC@
+PATH_SEPARATOR = @PATH_SEPARATOR@
+PGSQL_CFLAGS = @PGSQL_CFLAGS@
+PGSQL_LIBS = @PGSQL_LIBS@
+PG_CONFIG = @PG_CONFIG@
+PIE_CFLAGS = @PIE_CFLAGS@
+PIE_LDFLAGS = @PIE_LDFLAGS@
+PKG_CONFIG = @PKG_CONFIG@
+PKG_CONFIG_LIBDIR = @PKG_CONFIG_LIBDIR@
+PKG_CONFIG_PATH = @PKG_CONFIG_PATH@
+QUOTA_LIBS = @QUOTA_LIBS@
+RANLIB = @RANLIB@
+RELRO_LDFLAGS = @RELRO_LDFLAGS@
+RPCGEN = @RPCGEN@
+RUN_TEST = @RUN_TEST@
+SED = @SED@
+SETTING_FILES = @SETTING_FILES@
+SET_MAKE = @SET_MAKE@
+SHELL = @SHELL@
+SQLITE_CFLAGS = @SQLITE_CFLAGS@
+SQLITE_LIBS = @SQLITE_LIBS@
+SQL_CFLAGS = @SQL_CFLAGS@
+SQL_LIBS = @SQL_LIBS@
+SSL_CFLAGS = @SSL_CFLAGS@
+SSL_LIBS = @SSL_LIBS@
+STRIP = @STRIP@
+SYSTEMD_CFLAGS = @SYSTEMD_CFLAGS@
+SYSTEMD_LIBS = @SYSTEMD_LIBS@
+VALGRIND = @VALGRIND@
+VERSION = @VERSION@
+ZSTD_CFLAGS = @ZSTD_CFLAGS@
+ZSTD_LIBS = @ZSTD_LIBS@
+abs_builddir = @abs_builddir@
+abs_srcdir = @abs_srcdir@
+abs_top_builddir = @abs_top_builddir@
+abs_top_srcdir = @abs_top_srcdir@
+ac_ct_AR = @ac_ct_AR@
+ac_ct_CC = @ac_ct_CC@
+ac_ct_CXX = @ac_ct_CXX@
+ac_ct_DUMPBIN = @ac_ct_DUMPBIN@
+am__include = @am__include@
+am__leading_dot = @am__leading_dot@
+am__quote = @am__quote@
+am__tar = @am__tar@
+am__untar = @am__untar@
+bindir = @bindir@
+build = @build@
+build_alias = @build_alias@
+build_cpu = @build_cpu@
+build_os = @build_os@
+build_vendor = @build_vendor@
+builddir = @builddir@
+datadir = @datadir@
+datarootdir = @datarootdir@
+dict_drivers = @dict_drivers@
+docdir = @docdir@
+dvidir = @dvidir@
+exec_prefix = @exec_prefix@
+host = @host@
+host_alias = @host_alias@
+host_cpu = @host_cpu@
+host_os = @host_os@
+host_vendor = @host_vendor@
+htmldir = @htmldir@
+includedir = @includedir@
+infodir = @infodir@
+install_sh = @install_sh@
+libdir = @libdir@
+libexecdir = @libexecdir@
+localedir = @localedir@
+localstatedir = @localstatedir@
+mandir = @mandir@
+mkdir_p = @mkdir_p@
+moduledir = @moduledir@
+oldincludedir = @oldincludedir@
+pdfdir = @pdfdir@
+prefix = @prefix@
+program_transform_name = @program_transform_name@
+psdir = @psdir@
+rundir = @rundir@
+runstatedir = @runstatedir@
+sbindir = @sbindir@
+sharedstatedir = @sharedstatedir@
+sql_drivers = @sql_drivers@
+srcdir = @srcdir@
+ssldir = @ssldir@
+statedir = @statedir@
+sysconfdir = @sysconfdir@
+systemdservicetype = @systemdservicetype@
+systemdsystemunitdir = @systemdsystemunitdir@
+target_alias = @target_alias@
+top_build_prefix = @top_build_prefix@
+top_builddir = @top_builddir@
+top_srcdir = @top_srcdir@
+noinst_LTLIBRARIES = libprogram_client.la
+AM_CPPFLAGS = \
+ -I$(top_srcdir)/src/lib \
+ -I$(top_srcdir)/src/lib-test \
+ -I$(top_srcdir)/src/lib-dns \
+ -I$(top_srcdir)/src/lib-mail
+
+libprogram_client_la_SOURCES = \
+ program-client.c \
+ program-client-local.c \
+ program-client-remote.c
+
+headers = \
+ program-client.h
+
+noinst_HEADERS = \
+ program-client-private.h
+
+pkginc_libdir = $(pkgincludedir)
+pkginc_lib_HEADERS = $(headers)
+test_programs = \
+ test-program-client-local \
+ test-program-client-unix \
+ test-program-client-net
+
+test_libs = \
+ libprogram_client.la \
+ ../lib-dns/libdns.la \
+ ../lib-test/libtest.la \
+ ../lib-mail/libmail.la \
+ ../lib-charset/libcharset.la \
+ ../lib/liblib.la \
+ $(MODULE_LIBS)
+
+test_program_client_local_SOURCE = test-program-client-local.c
+test_program_client_local_LDADD = $(test_libs)
+test_program_client_unix_SOURCE = test-program-client-unix.c
+test_program_client_unix_LDADD = $(test_libs)
+test_program_client_net_SOURCE = test-program-client-net.c
+test_program_client_net_LDADD = $(test_libs)
+all: all-am
+
+.SUFFIXES:
+.SUFFIXES: .c .lo .o .obj
+$(srcdir)/Makefile.in: @MAINTAINER_MODE_TRUE@ $(srcdir)/Makefile.am $(am__configure_deps)
+ @for dep in $?; do \
+ case '$(am__configure_deps)' in \
+ *$$dep*) \
+ ( cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh ) \
+ && { if test -f $@; then exit 0; else break; fi; }; \
+ exit 1;; \
+ esac; \
+ done; \
+ echo ' cd $(top_srcdir) && $(AUTOMAKE) --foreign src/lib-program-client/Makefile'; \
+ $(am__cd) $(top_srcdir) && \
+ $(AUTOMAKE) --foreign src/lib-program-client/Makefile
+Makefile: $(srcdir)/Makefile.in $(top_builddir)/config.status
+ @case '$?' in \
+ *config.status*) \
+ cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh;; \
+ *) \
+ echo ' cd $(top_builddir) && $(SHELL) ./config.status $(subdir)/$@ $(am__maybe_remake_depfiles)'; \
+ cd $(top_builddir) && $(SHELL) ./config.status $(subdir)/$@ $(am__maybe_remake_depfiles);; \
+ esac;
+
+$(top_builddir)/config.status: $(top_srcdir)/configure $(CONFIG_STATUS_DEPENDENCIES)
+ cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh
+
+$(top_srcdir)/configure: @MAINTAINER_MODE_TRUE@ $(am__configure_deps)
+ cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh
+$(ACLOCAL_M4): @MAINTAINER_MODE_TRUE@ $(am__aclocal_m4_deps)
+ cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh
+$(am__aclocal_m4_deps):
+
+clean-noinstPROGRAMS:
+ @list='$(noinst_PROGRAMS)'; test -n "$$list" || exit 0; \
+ echo " rm -f" $$list; \
+ rm -f $$list || exit $$?; \
+ test -n "$(EXEEXT)" || exit 0; \
+ list=`for p in $$list; do echo "$$p"; done | sed 's/$(EXEEXT)$$//'`; \
+ echo " rm -f" $$list; \
+ rm -f $$list
+
+clean-noinstLTLIBRARIES:
+ -test -z "$(noinst_LTLIBRARIES)" || rm -f $(noinst_LTLIBRARIES)
+ @list='$(noinst_LTLIBRARIES)'; \
+ locs=`for p in $$list; do echo $$p; done | \
+ sed 's|^[^/]*$$|.|; s|/[^/]*$$||; s|$$|/so_locations|' | \
+ sort -u`; \
+ test -z "$$locs" || { \
+ echo rm -f $${locs}; \
+ rm -f $${locs}; \
+ }
+
+libprogram_client.la: $(libprogram_client_la_OBJECTS) $(libprogram_client_la_DEPENDENCIES) $(EXTRA_libprogram_client_la_DEPENDENCIES)
+ $(AM_V_CCLD)$(LINK) $(libprogram_client_la_OBJECTS) $(libprogram_client_la_LIBADD) $(LIBS)
+
+test-program-client-local$(EXEEXT): $(test_program_client_local_OBJECTS) $(test_program_client_local_DEPENDENCIES) $(EXTRA_test_program_client_local_DEPENDENCIES)
+ @rm -f test-program-client-local$(EXEEXT)
+ $(AM_V_CCLD)$(LINK) $(test_program_client_local_OBJECTS) $(test_program_client_local_LDADD) $(LIBS)
+
+test-program-client-net$(EXEEXT): $(test_program_client_net_OBJECTS) $(test_program_client_net_DEPENDENCIES) $(EXTRA_test_program_client_net_DEPENDENCIES)
+ @rm -f test-program-client-net$(EXEEXT)
+ $(AM_V_CCLD)$(LINK) $(test_program_client_net_OBJECTS) $(test_program_client_net_LDADD) $(LIBS)
+
+test-program-client-unix$(EXEEXT): $(test_program_client_unix_OBJECTS) $(test_program_client_unix_DEPENDENCIES) $(EXTRA_test_program_client_unix_DEPENDENCIES)
+ @rm -f test-program-client-unix$(EXEEXT)
+ $(AM_V_CCLD)$(LINK) $(test_program_client_unix_OBJECTS) $(test_program_client_unix_LDADD) $(LIBS)
+
+mostlyclean-compile:
+ -rm -f *.$(OBJEXT)
+
+distclean-compile:
+ -rm -f *.tab.c
+
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/program-client-local.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/program-client-remote.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/program-client.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/test-program-client-local.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/test-program-client-net.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/test-program-client-unix.Po@am__quote@ # am--include-marker
+
+$(am__depfiles_remade):
+ @$(MKDIR_P) $(@D)
+ @echo '# dummy' >$@-t && $(am__mv) $@-t $@
+
+am--depfiles: $(am__depfiles_remade)
+
+.c.o:
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(COMPILE) -MT $@ -MD -MP -MF $(DEPDIR)/$*.Tpo -c -o $@ $<
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/$*.Tpo $(DEPDIR)/$*.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='$<' object='$@' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(COMPILE) -c -o $@ $<
+
+.c.obj:
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(COMPILE) -MT $@ -MD -MP -MF $(DEPDIR)/$*.Tpo -c -o $@ `$(CYGPATH_W) '$<'`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/$*.Tpo $(DEPDIR)/$*.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='$<' object='$@' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(COMPILE) -c -o $@ `$(CYGPATH_W) '$<'`
+
+.c.lo:
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(LTCOMPILE) -MT $@ -MD -MP -MF $(DEPDIR)/$*.Tpo -c -o $@ $<
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/$*.Tpo $(DEPDIR)/$*.Plo
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='$<' object='$@' libtool=yes @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(LTCOMPILE) -c -o $@ $<
+
+mostlyclean-libtool:
+ -rm -f *.lo
+
+clean-libtool:
+ -rm -rf .libs _libs
+install-pkginc_libHEADERS: $(pkginc_lib_HEADERS)
+ @$(NORMAL_INSTALL)
+ @list='$(pkginc_lib_HEADERS)'; test -n "$(pkginc_libdir)" || list=; \
+ if test -n "$$list"; then \
+ echo " $(MKDIR_P) '$(DESTDIR)$(pkginc_libdir)'"; \
+ $(MKDIR_P) "$(DESTDIR)$(pkginc_libdir)" || exit 1; \
+ fi; \
+ for p in $$list; do \
+ if test -f "$$p"; then d=; else d="$(srcdir)/"; fi; \
+ echo "$$d$$p"; \
+ done | $(am__base_list) | \
+ while read files; do \
+ echo " $(INSTALL_HEADER) $$files '$(DESTDIR)$(pkginc_libdir)'"; \
+ $(INSTALL_HEADER) $$files "$(DESTDIR)$(pkginc_libdir)" || exit $$?; \
+ done
+
+uninstall-pkginc_libHEADERS:
+ @$(NORMAL_UNINSTALL)
+ @list='$(pkginc_lib_HEADERS)'; test -n "$(pkginc_libdir)" || list=; \
+ files=`for p in $$list; do echo $$p; done | sed -e 's|^.*/||'`; \
+ dir='$(DESTDIR)$(pkginc_libdir)'; $(am__uninstall_files_from_dir)
+
+ID: $(am__tagged_files)
+ $(am__define_uniq_tagged_files); mkid -fID $$unique
+tags: tags-am
+TAGS: tags
+
+tags-am: $(TAGS_DEPENDENCIES) $(am__tagged_files)
+ set x; \
+ here=`pwd`; \
+ $(am__define_uniq_tagged_files); \
+ shift; \
+ if test -z "$(ETAGS_ARGS)$$*$$unique"; then :; else \
+ test -n "$$unique" || unique=$$empty_fix; \
+ if test $$# -gt 0; then \
+ $(ETAGS) $(ETAGSFLAGS) $(AM_ETAGSFLAGS) $(ETAGS_ARGS) \
+ "$$@" $$unique; \
+ else \
+ $(ETAGS) $(ETAGSFLAGS) $(AM_ETAGSFLAGS) $(ETAGS_ARGS) \
+ $$unique; \
+ fi; \
+ fi
+ctags: ctags-am
+
+CTAGS: ctags
+ctags-am: $(TAGS_DEPENDENCIES) $(am__tagged_files)
+ $(am__define_uniq_tagged_files); \
+ test -z "$(CTAGS_ARGS)$$unique" \
+ || $(CTAGS) $(CTAGSFLAGS) $(AM_CTAGSFLAGS) $(CTAGS_ARGS) \
+ $$unique
+
+GTAGS:
+ here=`$(am__cd) $(top_builddir) && pwd` \
+ && $(am__cd) $(top_srcdir) \
+ && gtags -i $(GTAGS_ARGS) "$$here"
+cscopelist: cscopelist-am
+
+cscopelist-am: $(am__tagged_files)
+ list='$(am__tagged_files)'; \
+ case "$(srcdir)" in \
+ [\\/]* | ?:[\\/]*) sdir="$(srcdir)" ;; \
+ *) sdir=$(subdir)/$(srcdir) ;; \
+ esac; \
+ for i in $$list; do \
+ if test -f "$$i"; then \
+ echo "$(subdir)/$$i"; \
+ else \
+ echo "$$sdir/$$i"; \
+ fi; \
+ done >> $(top_builddir)/cscope.files
+
+distclean-tags:
+ -rm -f TAGS ID GTAGS GRTAGS GSYMS GPATH tags
+
+distdir: $(BUILT_SOURCES)
+ $(MAKE) $(AM_MAKEFLAGS) distdir-am
+
+distdir-am: $(DISTFILES)
+ @srcdirstrip=`echo "$(srcdir)" | sed 's/[].[^$$\\*]/\\\\&/g'`; \
+ topsrcdirstrip=`echo "$(top_srcdir)" | sed 's/[].[^$$\\*]/\\\\&/g'`; \
+ list='$(DISTFILES)'; \
+ dist_files=`for file in $$list; do echo $$file; done | \
+ sed -e "s|^$$srcdirstrip/||;t" \
+ -e "s|^$$topsrcdirstrip/|$(top_builddir)/|;t"`; \
+ case $$dist_files in \
+ */*) $(MKDIR_P) `echo "$$dist_files" | \
+ sed '/\//!d;s|^|$(distdir)/|;s,/[^/]*$$,,' | \
+ sort -u` ;; \
+ esac; \
+ for file in $$dist_files; do \
+ if test -f $$file || test -d $$file; then d=.; else d=$(srcdir); fi; \
+ if test -d $$d/$$file; then \
+ dir=`echo "/$$file" | sed -e 's,/[^/]*$$,,'`; \
+ if test -d "$(distdir)/$$file"; then \
+ find "$(distdir)/$$file" -type d ! -perm -700 -exec chmod u+rwx {} \;; \
+ fi; \
+ if test -d $(srcdir)/$$file && test $$d != $(srcdir); then \
+ cp -fpR $(srcdir)/$$file "$(distdir)$$dir" || exit 1; \
+ find "$(distdir)/$$file" -type d ! -perm -700 -exec chmod u+rwx {} \;; \
+ fi; \
+ cp -fpR $$d/$$file "$(distdir)$$dir" || exit 1; \
+ else \
+ test -f "$(distdir)/$$file" \
+ || cp -p $$d/$$file "$(distdir)/$$file" \
+ || exit 1; \
+ fi; \
+ done
+check-am: all-am
+ $(MAKE) $(AM_MAKEFLAGS) check-local
+check: check-am
+all-am: Makefile $(PROGRAMS) $(LTLIBRARIES) $(HEADERS)
+installdirs:
+ for dir in "$(DESTDIR)$(pkginc_libdir)"; do \
+ test -z "$$dir" || $(MKDIR_P) "$$dir"; \
+ done
+install: install-am
+install-exec: install-exec-am
+install-data: install-data-am
+uninstall: uninstall-am
+
+install-am: all-am
+ @$(MAKE) $(AM_MAKEFLAGS) install-exec-am install-data-am
+
+installcheck: installcheck-am
+install-strip:
+ if test -z '$(STRIP)'; then \
+ $(MAKE) $(AM_MAKEFLAGS) INSTALL_PROGRAM="$(INSTALL_STRIP_PROGRAM)" \
+ install_sh_PROGRAM="$(INSTALL_STRIP_PROGRAM)" INSTALL_STRIP_FLAG=-s \
+ install; \
+ else \
+ $(MAKE) $(AM_MAKEFLAGS) INSTALL_PROGRAM="$(INSTALL_STRIP_PROGRAM)" \
+ install_sh_PROGRAM="$(INSTALL_STRIP_PROGRAM)" INSTALL_STRIP_FLAG=-s \
+ "INSTALL_PROGRAM_ENV=STRIPPROG='$(STRIP)'" install; \
+ fi
+mostlyclean-generic:
+
+clean-generic:
+
+distclean-generic:
+ -test -z "$(CONFIG_CLEAN_FILES)" || rm -f $(CONFIG_CLEAN_FILES)
+ -test . = "$(srcdir)" || test -z "$(CONFIG_CLEAN_VPATH_FILES)" || rm -f $(CONFIG_CLEAN_VPATH_FILES)
+
+maintainer-clean-generic:
+ @echo "This command is intended for maintainers to use"
+ @echo "it deletes files that may require special tools to rebuild."
+clean: clean-am
+
+clean-am: clean-generic clean-libtool clean-noinstLTLIBRARIES \
+ clean-noinstPROGRAMS mostlyclean-am
+
+distclean: distclean-am
+ -rm -f ./$(DEPDIR)/program-client-local.Plo
+ -rm -f ./$(DEPDIR)/program-client-remote.Plo
+ -rm -f ./$(DEPDIR)/program-client.Plo
+ -rm -f ./$(DEPDIR)/test-program-client-local.Po
+ -rm -f ./$(DEPDIR)/test-program-client-net.Po
+ -rm -f ./$(DEPDIR)/test-program-client-unix.Po
+ -rm -f Makefile
+distclean-am: clean-am distclean-compile distclean-generic \
+ distclean-tags
+
+dvi: dvi-am
+
+dvi-am:
+
+html: html-am
+
+html-am:
+
+info: info-am
+
+info-am:
+
+install-data-am: install-pkginc_libHEADERS
+
+install-dvi: install-dvi-am
+
+install-dvi-am:
+
+install-exec-am:
+
+install-html: install-html-am
+
+install-html-am:
+
+install-info: install-info-am
+
+install-info-am:
+
+install-man:
+
+install-pdf: install-pdf-am
+
+install-pdf-am:
+
+install-ps: install-ps-am
+
+install-ps-am:
+
+installcheck-am:
+
+maintainer-clean: maintainer-clean-am
+ -rm -f ./$(DEPDIR)/program-client-local.Plo
+ -rm -f ./$(DEPDIR)/program-client-remote.Plo
+ -rm -f ./$(DEPDIR)/program-client.Plo
+ -rm -f ./$(DEPDIR)/test-program-client-local.Po
+ -rm -f ./$(DEPDIR)/test-program-client-net.Po
+ -rm -f ./$(DEPDIR)/test-program-client-unix.Po
+ -rm -f Makefile
+maintainer-clean-am: distclean-am maintainer-clean-generic
+
+mostlyclean: mostlyclean-am
+
+mostlyclean-am: mostlyclean-compile mostlyclean-generic \
+ mostlyclean-libtool
+
+pdf: pdf-am
+
+pdf-am:
+
+ps: ps-am
+
+ps-am:
+
+uninstall-am: uninstall-pkginc_libHEADERS
+
+.MAKE: check-am install-am install-strip
+
+.PHONY: CTAGS GTAGS TAGS all all-am am--depfiles check check-am \
+ check-local clean clean-generic clean-libtool \
+ clean-noinstLTLIBRARIES clean-noinstPROGRAMS cscopelist-am \
+ ctags ctags-am distclean distclean-compile distclean-generic \
+ distclean-libtool distclean-tags distdir dvi dvi-am html \
+ html-am info info-am install install-am install-data \
+ install-data-am install-dvi install-dvi-am install-exec \
+ install-exec-am install-html install-html-am install-info \
+ install-info-am install-man install-pdf install-pdf-am \
+ install-pkginc_libHEADERS install-ps install-ps-am \
+ install-strip installcheck installcheck-am installdirs \
+ maintainer-clean maintainer-clean-generic mostlyclean \
+ mostlyclean-compile mostlyclean-generic mostlyclean-libtool \
+ pdf pdf-am ps ps-am tags tags-am uninstall uninstall-am \
+ uninstall-pkginc_libHEADERS
+
+.PRECIOUS: Makefile
+
+
+check-local:
+ for bin in $(test_programs); do \
+ if test "$$bin" = "test-program-client-local"; then \
+ if ! env NOCHILDREN=yes $(RUN_TEST) ./$$bin; then exit 1; fi; \
+ else \
+ if ! $(RUN_TEST) ./$$bin; then exit 1; fi; \
+ fi \
+ done
+
+# Tell versions [3.59,3.63) of GNU make to not export all variables.
+# Otherwise a system limit (for SysV at least) may be exceeded.
+.NOEXPORT:
diff --git a/src/lib-program-client/program-client-local.c b/src/lib-program-client/program-client-local.c
new file mode 100644
index 0000000..499b2b5
--- /dev/null
+++ b/src/lib-program-client/program-client-local.c
@@ -0,0 +1,556 @@
+/* Copyright (c) 2002-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "lib-signals.h"
+#include "env-util.h"
+#include "execv-const.h"
+#include "array.h"
+#include "net.h"
+#include "istream.h"
+#include "ostream.h"
+#include "restrict-access.h"
+#include "child-wait.h"
+#include "time-util.h"
+#include "program-client-private.h"
+
+#include <sys/types.h>
+#include <sys/socket.h>
+#include <sys/stat.h>
+#include <sys/wait.h>
+#include <unistd.h>
+#include <fcntl.h>
+#include <grp.h>
+
+#define KILL_TIMEOUT 5000
+
+struct program_client_local {
+ struct program_client client;
+
+ struct child_wait *child_wait;
+ struct timeout *to_kill;
+
+ char *bin_path;
+
+ pid_t pid;
+ int status;
+ bool exited:1;
+ bool stopping:1;
+ bool sent_term:1;
+};
+
+static void
+program_client_local_waitchild(const struct child_wait_status *status,
+ struct program_client_local *plclient);
+static void
+program_client_local_disconnect(struct program_client *pclient, bool force);
+static void
+program_client_local_exited(struct program_client_local *plclient);
+
+static void
+exec_child(const char *bin_path, const char *const *args,
+ ARRAY_TYPE(const_string) *envs, int in_fd, int out_fd,
+ int *extra_fds, bool drop_stderr)
+{
+ ARRAY_TYPE(const_string) exec_args;
+
+ /* Setup stdin/stdout */
+
+ if (in_fd < 0)
+ in_fd = dev_null_fd;
+ if (out_fd < 0)
+ out_fd = dev_null_fd;
+
+ if (in_fd != STDIN_FILENO && dup2(in_fd, STDIN_FILENO) < 0)
+ i_fatal("program %s: dup2(stdin) failed: %m", bin_path);
+ if (out_fd != STDOUT_FILENO && dup2(out_fd, STDOUT_FILENO) < 0)
+ i_fatal("program %s: dup2(stdout) failed: %m", bin_path);
+
+ if (in_fd != STDIN_FILENO && in_fd != dev_null_fd && close(in_fd) < 0)
+ i_error("program %s: close(in_fd) failed: %m", bin_path);
+ if (out_fd != STDOUT_FILENO && out_fd != dev_null_fd &&
+ (out_fd != in_fd) && close(out_fd) < 0)
+ i_error("program %s: close(out_fd) failed: %m", bin_path);
+
+ /* Drop stderr if requested */
+ if (drop_stderr) {
+ if (dup2(dev_null_fd, STDERR_FILENO) < 0) {
+ i_fatal("program %s: "
+ "dup2(stderr) failed: %m", bin_path);
+ }
+ }
+
+ /* Setup extra fds */
+ if (extra_fds != NULL) {
+ int *efd;
+ for(efd = extra_fds; *efd != -1; efd += 2) {
+ i_assert(efd[1] != STDIN_FILENO);
+ i_assert(efd[1] != STDOUT_FILENO);
+ i_assert(efd[1] != STDERR_FILENO);
+ if (efd[0] != efd[1]) {
+ if (dup2(efd[0], efd[1]) < 0) {
+ i_fatal("program %s"
+ "dup2(extra_fd=%d) failed: %m",
+ bin_path, efd[1]);
+ }
+ }
+ }
+ for(efd = extra_fds; *efd != -1; efd += 2) {
+ if (efd[0] != efd[1] && efd[0] != STDIN_FILENO &&
+ efd[0] != STDOUT_FILENO &&
+ efd[0] != STDERR_FILENO) {
+ if (close(efd[0]) < 0) {
+ i_error("program %s"
+ "close(extra_fd=%d) failed: %m",
+ bin_path, efd[1]);
+ }
+ }
+ }
+ }
+
+ /* Compose argv */
+
+ t_array_init(&exec_args, 16);
+ array_push_back(&exec_args, &bin_path);
+ if (args != NULL) {
+ for(; *args != NULL; args++)
+ array_push_back(&exec_args, args);
+ }
+ (void) array_append_space(&exec_args);
+
+ /* Setup environment */
+
+ env_clean();
+ if (array_is_created(envs)) {
+ array_append_zero(envs);
+ env_put_array(array_front(envs));
+ }
+
+ /* Execute */
+
+ args = array_front(&exec_args);
+ execvp_const(args[0], args);
+}
+
+static void
+program_client_local_waitchild(const struct child_wait_status *status,
+ struct program_client_local *plclient)
+{
+ struct program_client *pclient = &plclient->client;
+
+ i_assert(plclient->pid == status->pid);
+
+ e_debug(pclient->event, "Child process ended");
+
+ plclient->status = status->status;
+ plclient->exited = TRUE;
+ plclient->pid = -1;
+
+ if (plclient->stopping ||
+ (pclient->fd_in < 0 && pclient->fd_out < 0))
+ program_client_local_exited(plclient);
+}
+
+static int
+program_client_local_connect(struct program_client *pclient)
+{
+ struct program_client_local *plclient =
+ (struct program_client_local *)pclient;
+ int fd_in[2] = { -1, -1 }, fd_out[2] = {-1, -1};
+ struct program_client_extra_fd *efds = NULL;
+ int *parent_extra_fds = NULL, *child_extra_fds = NULL;
+ unsigned int xfd_count = 0, i;
+
+ /* create normal I/O fds */
+ if (pclient->input != NULL) {
+ if (pipe(fd_in) < 0) {
+ e_error(pclient->event, "pipe(in) failed: %m");
+ return -1;
+ }
+ }
+ if (pclient->output != NULL) {
+ if (pipe(fd_out) < 0) {
+ e_error(pclient->event, "pipe(out) failed: %m");
+ return -1;
+ }
+ }
+
+ /* create pipes for additional output through side-channel fds */
+ if (array_is_created(&pclient->extra_fds)) {
+ int extra_fd[2];
+
+ efds = array_get_modifiable(&pclient->extra_fds, &xfd_count);
+ if (xfd_count > 0) {
+ i_assert(xfd_count < INT_MAX);
+ parent_extra_fds = t_new(int, xfd_count);
+ child_extra_fds = t_new(int, xfd_count * 2 + 1);
+ for(i = 0; i < xfd_count; i++) {
+ if (pipe(extra_fd) < 0) {
+ e_error(pclient->event,
+ "pipe(extra=%d) failed: %m",
+ extra_fd[1]);
+ return -1;
+ }
+ parent_extra_fds[i] = extra_fd[0];
+ child_extra_fds[i * 2 + 0] = extra_fd[1];
+ child_extra_fds[i * 2 + 1] = efds[i].child_fd;
+ }
+ child_extra_fds[xfd_count * 2] = -1;
+ }
+ }
+
+ /* fork child */
+ if ((plclient->pid = fork()) == (pid_t)-1) {
+ e_error(pclient->event, "fork() failed: %m");
+
+ /* clean up */
+ if (fd_in[0] >= 0 && close(fd_in[0]) < 0) {
+ e_error(pclient->event,
+ "close(pipe:in:rd) failed: %m");
+ }
+ if (fd_in[1] >= 0 && close(fd_in[1]) < 0) {
+ e_error(pclient->event,
+ "close(pipe:in:wr) failed: %m");
+ }
+ if (fd_out[0] >= 0 && close(fd_out[0]) < 0) {
+ e_error(pclient->event,
+ "close(pipe:out:rd) failed: %m");
+ }
+ if (fd_out[1] >= 0 && close(fd_out[1]) < 0) {
+ e_error(pclient->event,
+ "close(pipe:out:wr) failed: %m");
+ }
+ for(i = 0; i < xfd_count; i++) {
+ if (close(child_extra_fds[i * 2]) < 0) {
+ e_error(pclient->event,
+ "close(pipe:extra=%d:wr) failed: %m",
+ child_extra_fds[i * 2 + 1]);
+ }
+ if (close(parent_extra_fds[i]) < 0) {
+ e_error(pclient->event,
+ "close(pipe:extra=%d:rd) failed: %m",
+ child_extra_fds[i * 2 + 1]);
+ }
+ }
+ return -1;
+ }
+
+ if (plclient->pid == 0) {
+ /* child */
+ if (fd_in[1] >= 0 && close(fd_in[1]) < 0) {
+ e_error(pclient->event,
+ "close(pipe:in:wr) failed: %m");
+ }
+ if (fd_out[0] >= 0 && close(fd_out[0]) < 0) {
+ e_error(pclient->event,
+ "close(pipe:out:rd) failed: %m");
+ }
+ for(i = 0; i < xfd_count; i++) {
+ if (close(parent_extra_fds[i]) < 0) {
+ e_error(pclient->event,
+ "close(pipe:extra=%d:rd) failed: %m",
+ child_extra_fds[i * 2 + 1]);
+ }
+ }
+
+ /* if we want to allow root, then we will not drop
+ root privileges */
+ restrict_access(&pclient->set.restrict_set,
+ (pclient->set.allow_root ?
+ RESTRICT_ACCESS_FLAG_ALLOW_ROOT : 0),
+ pclient->set.home);
+
+ exec_child(plclient->bin_path, pclient->args, &pclient->envs,
+ fd_in[0], fd_out[1], child_extra_fds,
+ pclient->set.drop_stderr);
+ i_unreached();
+ }
+
+ /* parent */
+ e_debug(pclient->event, "Forked child process");
+
+ program_client_set_label(pclient,
+ t_strdup_printf("exec:%s (%d)", plclient->bin_path,
+ plclient->pid));
+
+ if (fd_in[0] >= 0 && close(fd_in[0]) < 0) {
+ e_error(pclient->event, "close(pipe:in:rd) failed: %m");
+ }
+ if (fd_out[1] >= 0 && close(fd_out[1]) < 0) {
+ e_error(pclient->event, "close(pipe:out:wr) failed: %m");
+ }
+ if (fd_in[1] >= 0) {
+ net_set_nonblock(fd_in[1], TRUE);
+ pclient->fd_out = fd_in[1];
+ }
+ if (fd_out[0] >= 0) {
+ net_set_nonblock(fd_out[0], TRUE);
+ pclient->fd_in = fd_out[0];
+ }
+ for(i = 0; i < xfd_count; i++) {
+ if (close(child_extra_fds[i * 2]) < 0) {
+ e_error(pclient->event,
+ "close(pipe:extra=%d:wr) failed: %m",
+ child_extra_fds[i * 2 + 1]);
+ }
+ net_set_nonblock(parent_extra_fds[i], TRUE);
+ efds[i].parent_fd = parent_extra_fds[i];
+ }
+
+ program_client_init_streams(pclient);
+
+ plclient->child_wait =
+ child_wait_new_with_pid(plclient->pid,
+ program_client_local_waitchild,
+ plclient);
+ program_client_connected(pclient);
+ return 0;
+}
+
+static int
+program_client_local_close_output(struct program_client *pclient)
+{
+ int fd_out = pclient->fd_out;
+
+ pclient->fd_out = -1;
+
+ /* Shutdown output; program stdin will get EOF */
+ if (fd_out >= 0 && close(fd_out) < 0) {
+ e_error(pclient->event,
+ "close(fd_out) failed: %m");
+ return -1;
+ }
+ return 1;
+}
+
+static void
+program_client_local_exited(struct program_client_local *plclient)
+{
+ struct program_client *pclient = &plclient->client;
+
+ timeout_remove(&plclient->to_kill);
+ if (plclient->child_wait != NULL)
+ child_wait_free(&plclient->child_wait);
+
+ plclient->exited = TRUE;
+ plclient->pid = -1;
+ /* Evaluate child exit status */
+ pclient->exit_status = PROGRAM_CLIENT_EXIT_STATUS_INTERNAL_FAILURE;
+
+ if (WIFEXITED(plclient->status)) {
+ /* Exited */
+ int exit_code = WEXITSTATUS(plclient->status);
+
+ if (exit_code != 0) {
+ e_info(pclient->event,
+ "Terminated with non-zero exit code %d",
+ exit_code);
+ pclient->exit_status =
+ PROGRAM_CLIENT_EXIT_STATUS_FAILURE;
+ } else {
+ pclient->exit_status =
+ PROGRAM_CLIENT_EXIT_STATUS_SUCCESS;
+ }
+ } else if (WIFSIGNALED(plclient->status)) {
+ /* Killed with a signal */
+ if (plclient->sent_term) {
+ e_error(pclient->event,
+ "Forcibly terminated with signal %d",
+ WTERMSIG(plclient->status));
+ } else {
+ e_error(pclient->event,
+ "Terminated abnormally with signal %d",
+ WTERMSIG(plclient->status));
+ }
+ } else if (WIFSTOPPED(plclient->status)) {
+ /* Stopped */
+ e_error(pclient->event,
+ "Stopped with signal %d",
+ WSTOPSIG(plclient->status));
+ } else {
+ /* Something else */
+ e_error(pclient->event,
+ "Terminated abnormally with status %d",
+ plclient->status);
+ }
+
+ program_client_disconnected(pclient);
+}
+
+static void
+program_client_local_kill_now(struct program_client_local *plclient)
+{
+ struct program_client *pclient = &plclient->client;
+
+ if (plclient->child_wait != NULL) {
+ /* no need for this anymore */
+ child_wait_free(&plclient->child_wait);
+ }
+
+ if (plclient->pid < 0)
+ return;
+
+ e_debug(pclient->event, "Sending SIGKILL signal to program");
+
+ /* kill it brutally now: it should die right away */
+ if (kill(plclient->pid, SIGKILL) < 0) {
+ e_error(pclient->event,
+ "Failed to send SIGKILL signal to program");
+ } else if (waitpid(plclient->pid, &plclient->status, 0) < 0) {
+ e_error(pclient->event, "waitpid(%d) failed: %m",
+ plclient->pid);
+ }
+}
+
+static void
+program_client_local_kill(struct program_client_local *plclient)
+{
+ struct program_client *pclient = &plclient->client;
+
+ /* time to die */
+ timeout_remove(&plclient->to_kill);
+
+ i_assert(plclient->pid != (pid_t)-1);
+
+ if (plclient->client.error == PROGRAM_CLIENT_ERROR_NONE)
+ plclient->client.error = PROGRAM_CLIENT_ERROR_RUN_TIMEOUT;
+
+ if (plclient->sent_term) {
+ /* Timed out again */
+ e_debug(pclient->event,
+ "Program did not die after %d milliseconds",
+ KILL_TIMEOUT);
+
+ program_client_local_kill_now(plclient);
+ program_client_local_exited(plclient);
+ return;
+ }
+
+ e_debug(pclient->event,
+ "Execution timed out after %u milliseconds: "
+ "Sending TERM signal",
+ pclient->set.input_idle_timeout_msecs);
+
+ /* send sigterm, keep on waiting */
+ plclient->sent_term = TRUE;
+
+ /* Kill child gently first */
+ if (kill(plclient->pid, SIGTERM) < 0) {
+ e_error(pclient->event,
+ "Failed to send SIGTERM signal to program");
+ (void)kill(plclient->pid, SIGKILL);
+ program_client_local_exited(plclient);
+ return;
+ }
+
+ i_assert(plclient->child_wait != NULL);
+
+ plclient->to_kill = timeout_add_short(KILL_TIMEOUT,
+ program_client_local_kill, plclient);
+}
+
+static void
+program_client_local_disconnect(struct program_client *pclient, bool force)
+{
+ struct program_client_local *plclient =
+ (struct program_client_local *) pclient;
+ pid_t pid = plclient->pid;
+ unsigned long runtime, timeout = 0;
+
+ if (plclient->exited) {
+ program_client_local_exited(plclient);
+ return;
+ }
+
+ if (plclient->stopping) return;
+ plclient->stopping = TRUE;
+
+ if (pid < 0) {
+ /* program never started */
+ e_debug(pclient->event, "Child process never started");
+ pclient->exit_status = PROGRAM_CLIENT_EXIT_STATUS_FAILURE;
+ program_client_local_exited(plclient);
+ return;
+ }
+
+ /* make sure it hasn't already been reaped */
+ if (waitpid(plclient->pid, &plclient->status, WNOHANG) > 0) {
+ e_debug(pclient->event, "Child process ended");
+ program_client_local_exited(plclient);
+ return;
+ }
+
+ /* Calculate timeout */
+ runtime = timeval_diff_msecs(&ioloop_timeval, &pclient->start_time);
+ if (force || (pclient->set.input_idle_timeout_msecs > 0 &&
+ runtime >= pclient->set.input_idle_timeout_msecs)) {
+ e_debug(pclient->event,
+ "Terminating program immediately");
+
+ program_client_local_kill(plclient);
+ return;
+ }
+
+ if (runtime < pclient->set.input_idle_timeout_msecs)
+ timeout = pclient->set.input_idle_timeout_msecs - runtime;
+
+ e_debug(pclient->event,
+ "Waiting for program to finish after %lu msecs "
+ "(timeout = %lu msecs)", runtime, timeout);
+
+ if (timeout == 0)
+ return;
+
+ plclient->to_kill = timeout_add_short(timeout,
+ program_client_local_kill,
+ plclient);
+}
+
+static void
+program_client_local_destroy(struct program_client *pclient)
+{
+ struct program_client_local *plclient =
+ (struct program_client_local *)pclient;
+
+ timeout_remove(&plclient->to_kill);
+
+ program_client_local_kill_now(plclient);
+ child_wait_deinit();
+}
+
+static void
+program_client_local_switch_ioloop(struct program_client *pclient)
+{
+ struct program_client_local *plclient =
+ (struct program_client_local *)pclient;
+
+ if (plclient->to_kill != NULL)
+ plclient->to_kill = io_loop_move_timeout(&plclient->to_kill);
+ child_wait_switch_ioloop();
+}
+
+struct program_client *
+program_client_local_create(const char *bin_path,
+ const char *const *args,
+ const struct program_client_settings *set)
+{
+ struct program_client_local *plclient;
+ const char *label;
+ pool_t pool;
+
+ label = t_strconcat("exec:", bin_path, NULL);
+
+ pool = pool_alloconly_create("program client local", 1024);
+ plclient = p_new(pool, struct program_client_local, 1);
+ program_client_init(&plclient->client, pool, label, args, set);
+ plclient->client.connect = program_client_local_connect;
+ plclient->client.close_output = program_client_local_close_output;
+ plclient->client.switch_ioloop = program_client_local_switch_ioloop;
+ plclient->client.disconnect = program_client_local_disconnect;
+ plclient->client.destroy = program_client_local_destroy;
+ plclient->bin_path = p_strdup(pool, bin_path);
+ plclient->pid = -1;
+
+ child_wait_init();
+
+ return &plclient->client;
+}
diff --git a/src/lib-program-client/program-client-private.h b/src/lib-program-client/program-client-private.h
new file mode 100644
index 0000000..5f6260a
--- /dev/null
+++ b/src/lib-program-client/program-client-private.h
@@ -0,0 +1,85 @@
+#ifndef PROGRAM_CLIENT_PRIVATE_H
+#define PROGRAM_CLIENT_PRIVATE_H
+
+#include "program-client.h"
+
+enum program_client_error {
+ PROGRAM_CLIENT_ERROR_NONE,
+ PROGRAM_CLIENT_ERROR_CONNECT_TIMEOUT,
+ PROGRAM_CLIENT_ERROR_RUN_TIMEOUT,
+ PROGRAM_CLIENT_ERROR_IO,
+ PROGRAM_CLIENT_ERROR_OTHER
+};
+
+struct program_client_extra_fd {
+ struct program_client *pclient;
+
+ int child_fd, parent_fd;
+ struct istream *input;
+ struct io *io;
+
+ program_client_fd_callback_t *callback;
+ void *context;
+};
+
+struct program_client {
+ pool_t pool;
+ struct program_client_settings set;
+
+ const char **args;
+ ARRAY_TYPE(const_string) envs;
+
+ struct event *event;
+
+ int fd_in, fd_out;
+ struct io *io;
+ struct timeout *to;
+ struct timeval start_time;
+
+ struct istream *input, *program_input, *raw_program_input;
+ struct ostream *output, *program_output, *raw_program_output;
+
+ struct iostream_pump *pump_in, *pump_out;
+
+ ARRAY(struct program_client_extra_fd) extra_fds;
+
+ program_client_callback_t *callback;
+ void *context;
+
+ bool other_error;
+ enum program_client_error error;
+ enum program_client_exit_status exit_status;
+
+ int (*connect) (struct program_client * pclient);
+ int (*close_output) (struct program_client * pclient);
+ void (*switch_ioloop) (struct program_client * pclient);
+ void (*disconnect) (struct program_client * pclient, bool force);
+ void (*destroy) (struct program_client * pclient);
+
+ bool debug:1;
+ bool disconnected:1;
+ bool output_seekable:1;
+ bool destroying:1;
+};
+
+void program_client_set_label(struct program_client *pclient,
+ const char *label);
+
+void program_client_init(struct program_client *pclient, pool_t pool,
+ const char *initial_label,
+ const char *const *args,
+ const struct program_client_settings *set)
+ ATTR_NULL(5);
+
+void program_client_init_streams(struct program_client *pclient);
+
+void program_client_connected(struct program_client *pclient);
+
+void program_client_fail(struct program_client *pclient,
+ enum program_client_error error);
+
+void program_client_program_input(struct program_client *pclient);
+
+void program_client_disconnected(struct program_client *pclient);
+
+#endif
diff --git a/src/lib-program-client/program-client-remote.c b/src/lib-program-client/program-client-remote.c
new file mode 100644
index 0000000..858abe6
--- /dev/null
+++ b/src/lib-program-client/program-client-remote.c
@@ -0,0 +1,702 @@
+/* Copyright (c) 2002-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "ioloop.h"
+#include "str.h"
+#include "strescape.h"
+#include "array.h"
+#include "net.h"
+#include "write-full.h"
+#include "eacces-error.h"
+#include "istream-private.h"
+#include "ostream.h"
+#include "dns-lookup.h"
+#include "program-client-private.h"
+
+#include <unistd.h>
+#include <sys/wait.h>
+#include <sysexits.h>
+
+#define PROGRAM_CLIENT_VERSION_MAJOR "4"
+#define PROGRAM_CLIENT_VERSION_MINOR "0"
+
+#define PROGRAM_CLIENT_VERSION_STRING \
+ "VERSION\tscript\t" \
+ PROGRAM_CLIENT_VERSION_MAJOR "\t" \
+ PROGRAM_CLIENT_VERSION_MINOR "\n"
+
+/*
+ * Script client input stream
+ */
+
+struct program_client_istream {
+ struct istream_private istream;
+
+ struct stat statbuf;
+
+ struct program_client *client;
+
+ bool parsed_result:1;
+};
+
+static void program_client_istream_destroy(struct iostream_private *stream)
+{
+ struct program_client_istream *scstream =
+ (struct program_client_istream *)stream;
+
+ i_stream_unref(&scstream->istream.parent);
+}
+
+static void
+program_client_istream_parse_result(struct program_client_istream *scstream,
+ size_t pos)
+{
+ struct istream_private *stream = &scstream->istream;
+
+ if (scstream->parsed_result)
+ return;
+ scstream->parsed_result = TRUE;
+
+ if (stream->buffer == NULL || pos < 2 ||
+ stream->buffer[pos - 1] != '\n') {
+ if (pos == 0) {
+ e_error(scstream->client->event,
+ "No result code received from remote");
+ } else if (pos < 2) {
+ e_error(scstream->client->event,
+ "Received too short result code from remote");
+ } else {
+ e_error(scstream->client->event,
+ "Missing LF in result code");
+ }
+ scstream->client->exit_status =
+ PROGRAM_CLIENT_EXIT_STATUS_INTERNAL_FAILURE;
+ return;
+ }
+
+ unsigned char rcode = stream->buffer[pos - 2];
+ switch (rcode) {
+ case '+':
+ e_debug(scstream->client->event,
+ "Received '+' result code from remote");
+ scstream->client->exit_status =
+ PROGRAM_CLIENT_EXIT_STATUS_SUCCESS;
+ break;
+ case '-':
+ e_debug(scstream->client->event,
+ "Received '-' result code from remote");
+ scstream->client->exit_status =
+ PROGRAM_CLIENT_EXIT_STATUS_FAILURE;
+ break;
+ default:
+ if (rcode >= 0x20 && rcode < 0x7f) {
+ e_error(scstream->client->event,
+ "Unexpected result code '%c'", rcode);
+ } else {
+ e_error(scstream->client->event,
+ "Unexpected result code 0x%02x", rcode);
+ }
+ scstream->client->exit_status =
+ PROGRAM_CLIENT_EXIT_STATUS_INTERNAL_FAILURE;
+ }
+}
+
+static ssize_t program_client_istream_read(struct istream_private *stream)
+{
+ struct program_client_istream *scstream =
+ (struct program_client_istream *)stream;
+ size_t pos, reserved;
+ ssize_t ret = 0;
+
+ i_stream_skip(stream->parent, stream->skip);
+ stream->skip = 0;
+
+ stream->buffer = i_stream_get_data(stream->parent, &pos);
+
+ if (stream->parent->eof) {
+ /* Check return code at EOF */
+ program_client_istream_parse_result(scstream, pos);
+ }
+
+ reserved = 0;
+ if (stream->buffer != NULL && pos >= 1) {
+ /* Retain/hide potential return code at end of buffer */
+ reserved = (stream->buffer[pos - 1] == '\n' && pos > 1 ? 2 : 1);
+ pos -= reserved;
+ }
+
+ if (stream->parent->eof) {
+ i_assert(scstream->parsed_result);
+ if (pos == 0)
+ i_stream_skip(stream->parent, reserved);
+ stream->istream.eof = TRUE;
+ ret = -1;
+ } else {
+ do {
+ ret = i_stream_read_memarea(stream->parent);
+ stream->istream.stream_errno =
+ stream->parent->stream_errno;
+ stream->buffer =
+ i_stream_get_data(stream->parent, &pos);
+ if (ret == -2) {
+ /* Input buffer full */
+ return -2;
+ }
+ if (ret < 0 && stream->istream.stream_errno != 0)
+ break;
+
+ if (stream->parent->eof) {
+ /* Check return code at EOF */
+ program_client_istream_parse_result(
+ scstream, pos);
+ }
+
+ ssize_t reserve_mod = 0;
+ if (stream->buffer != NULL && pos >= 1) {
+ /* Retain/hide potential return code at end of
+ buffer */
+ size_t old_reserved = reserved;
+
+ reserved = (stream->buffer[pos - 1] == '\n' &&
+ pos > 1 ? 2 : 1);
+ reserve_mod = (ssize_t)reserved - (ssize_t)old_reserved;
+ pos -= reserved;
+ }
+ if (ret == 0) {
+ /* Parent already blocked, but we had to update
+ pos first, to make sure reserved bytes are
+ not visible to application. */
+ break;
+ }
+ if (ret > 0 && ret >= reserve_mod) {
+ /* Subtract additional reserved bytes */
+ ret -= reserve_mod;
+ }
+
+ if (ret <= 0 && stream->parent->eof) {
+ /* Parent EOF and not more data to return;
+ EOF here as well */
+ i_assert(scstream->parsed_result);
+ if (pos == 0)
+ i_stream_skip(stream->parent, reserved);
+ stream->istream.eof = TRUE;
+ ret = -1;
+ }
+ } while (ret == 0);
+ }
+
+ stream->pos = pos;
+
+ i_assert(ret != -1 || stream->istream.eof ||
+ stream->istream.stream_errno != 0);
+ return ret;
+}
+
+static void ATTR_NORETURN
+program_client_istream_sync(struct istream_private *stream ATTR_UNUSED)
+{
+ i_panic("program_client_istream sync() not implemented");
+}
+
+static int
+program_client_istream_stat(struct istream_private *stream, bool exact)
+{
+ struct program_client_istream *scstream =
+ (struct program_client_istream *)stream;
+ const struct stat *st;
+ int ret;
+
+ /* Stat the original stream */
+ ret = i_stream_stat(stream->parent, exact, &st);
+ if (ret < 0 || st->st_size == -1 || !exact)
+ return ret;
+
+ scstream->statbuf = *st;
+ scstream->statbuf.st_size = -1;
+
+ return ret;
+}
+
+static struct istream *
+program_client_istream_create(struct program_client *program_client,
+ struct istream *input)
+{
+ struct program_client_istream *scstream;
+
+ scstream = i_new(struct program_client_istream, 1);
+ scstream->client = program_client;
+
+ scstream->istream.max_buffer_size = input->real_stream->max_buffer_size;
+
+ scstream->istream.iostream.destroy = program_client_istream_destroy;
+ scstream->istream.read = program_client_istream_read;
+ scstream->istream.sync = program_client_istream_sync;
+ scstream->istream.stat = program_client_istream_stat;
+
+ scstream->istream.istream.readable_fd = FALSE;
+ scstream->istream.istream.blocking = input->blocking;
+ scstream->istream.istream.seekable = FALSE;
+
+ i_stream_seek(input, 0);
+
+ return i_stream_create(&scstream->istream, input,
+ i_stream_get_fd(input), 0);
+}
+
+/*
+ * Program client
+ */
+
+struct program_client_remote {
+ struct program_client client;
+
+ const char *address;
+ struct dns_lookup_settings dns_set;
+ struct dns_lookup *lookup;
+ unsigned int ips_count;
+ unsigned int ips_left;
+ struct ip_addr *ips;
+ in_port_t port;
+
+ struct timeout *to_retry;
+
+ bool noreply:1;
+ bool resolved:1;
+ bool have_hostname:1;
+};
+
+static void
+program_client_net_connect_again(struct program_client_remote *prclient);
+
+static void
+program_client_remote_connected(struct program_client_remote *prclient)
+{
+ struct program_client *pclient = &prclient->client;
+ const char **args = pclient->args;
+ string_t *str;
+
+ timeout_remove(&pclient->to);
+ io_remove(&pclient->io);
+ program_client_init_streams(pclient);
+
+ if (!prclient->noreply) {
+ struct istream *is = pclient->raw_program_input;
+
+ pclient->raw_program_input =
+ program_client_istream_create(pclient, is);
+ i_stream_unref(&is);
+ }
+
+ str = t_str_new(1024);
+ str_append(str, PROGRAM_CLIENT_VERSION_STRING);
+ if (array_is_created(&pclient->envs)) {
+ const char *env;
+ array_foreach_elem(&pclient->envs, env) {
+ str_append(str, "env_");
+ str_append_tabescaped(str, env);
+ str_append_c(str, '\n');
+ }
+ }
+ if (prclient->noreply)
+ str_append(str, "noreply\n");
+ else
+ str_append(str, "-\n");
+ if (args != NULL) {
+ for(; *args != NULL; args++) {
+ str_append_tabescaped(str, *args);
+ str_append_c(str, '\n');
+ }
+ }
+ str_append_c(str, '\n');
+
+ if (o_stream_send(pclient->raw_program_output,
+ str_data(str), str_len(str)) < 0) {
+ e_error(pclient->event,
+ "write(%s) failed: %s",
+ o_stream_get_name(pclient->raw_program_output),
+ o_stream_get_error(pclient->raw_program_output));
+ program_client_fail(pclient, PROGRAM_CLIENT_ERROR_IO);
+ return;
+ }
+
+ program_client_connected(pclient);
+}
+
+static int program_client_unix_connect(struct program_client *pclient);
+
+static void
+program_client_unix_reconnect(struct program_client_remote *prclient)
+{
+ (void)program_client_unix_connect(&prclient->client);
+}
+
+static int program_client_unix_connect(struct program_client *pclient)
+{
+ struct program_client_remote *prclient =
+ (struct program_client_remote *)pclient;
+ int fd;
+
+ e_debug(pclient->event, "Trying to connect");
+
+ timeout_remove(&prclient->to_retry);
+
+ if ((fd = net_connect_unix(prclient->address)) < 0) {
+ switch (errno) {
+ case EACCES:
+ e_error(pclient->event, "%s",
+ eacces_error_get("net_connect_unix",
+ prclient->address));
+ return -1;
+ case EAGAIN:
+ prclient->to_retry = timeout_add_short(
+ 100, program_client_unix_reconnect, prclient);
+ return 0;
+ default:
+ e_error(pclient->event,
+ "net_connect_unix(%s) failed: %m",
+ prclient->address);
+ return -1;
+ }
+ }
+
+ pclient->fd_in = (prclient->noreply && pclient->output == NULL ?
+ -1 : fd);
+ pclient->fd_out = fd;
+ pclient->io = io_add(fd, IO_WRITE,
+ program_client_remote_connected, prclient);
+ return 0;
+}
+
+static void
+program_client_net_connect_timeout(struct program_client_remote *prclient)
+{
+ struct program_client *pclient = &prclient->client;
+
+ io_remove(&pclient->io);
+ timeout_remove(&pclient->to);
+
+ e_error(pclient->event, "connect(%s) failed: "
+ "Timeout in %u milliseconds", prclient->address,
+ pclient->set.client_connect_timeout_msecs);
+
+ /* Set error to timeout here */
+ pclient->error = PROGRAM_CLIENT_ERROR_CONNECT_TIMEOUT;
+ i_close_fd(&pclient->fd_out);
+ pclient->fd_in = pclient->fd_out = -1;
+ program_client_net_connect_again(prclient);
+}
+
+/* See if connect succeeded or not, if it did, then proceed normally, otherwise
+ try reconnect to next address.
+ */
+static void program_client_net_connected(struct program_client_remote *prclient)
+{
+ struct program_client *pclient = &prclient->client;
+
+ io_remove(&pclient->io);
+
+ errno = net_geterror(pclient->fd_out);
+ if (errno != 0) {
+ e_error(pclient->event, "connect(%s) failed: %m",
+ prclient->address);
+
+ /* Disconnect and try again */
+ i_close_fd(&pclient->fd_out);
+ pclient->fd_in = pclient->fd_out = -1;
+ program_client_net_connect_again(prclient);
+ } else {
+ pclient->io = io_add(pclient->fd_out, IO_WRITE,
+ program_client_remote_connected, prclient);
+ }
+}
+
+static void
+program_client_net_connect_real(struct program_client_remote *prclient)
+{
+ struct program_client *pclient = &prclient->client;
+ const char *address, *label;
+
+ timeout_remove(&pclient->to);
+ timeout_remove(&prclient->to_retry);
+
+ i_assert(prclient->ips_count > 0);
+
+ if (net_ipport2str(prclient->ips, prclient->port, &address) < 0)
+ i_unreached();
+ label = t_strconcat("tcp:", address, NULL);
+ program_client_set_label(pclient, label);
+
+ e_debug(pclient->event, "Trying to connect (timeout %u msecs)",
+ pclient->set.client_connect_timeout_msecs);
+
+ /* Try to connect */
+ int fd;
+ if ((fd = net_connect_ip(prclient->ips, prclient->port,
+ (prclient->ips->family == AF_INET ?
+ &net_ip4_any : &net_ip6_any))) < 0) {
+ e_error(pclient->event, "connect(%s) failed: %m", address);
+ prclient->to_retry = timeout_add_short(
+ 0, program_client_net_connect_again, prclient);
+ return;
+ }
+
+ pclient->fd_in = (prclient->noreply && pclient->output == NULL ?
+ -1 : fd);
+ pclient->fd_out = fd;
+ pclient->io = io_add(fd, IO_WRITE,
+ program_client_net_connected, prclient);
+
+ if (pclient->set.client_connect_timeout_msecs != 0) {
+ pclient->to = timeout_add(
+ pclient->set.client_connect_timeout_msecs,
+ program_client_net_connect_timeout, prclient);
+ }
+}
+
+static void
+program_client_net_connect_again(struct program_client_remote *prclient)
+{
+ struct program_client *pclient = &prclient->client;
+ enum program_client_error error = pclient->error;
+
+ pclient->error = PROGRAM_CLIENT_ERROR_NONE;
+
+ if (--prclient->ips_left == 0) {
+ if (prclient->ips_count > 1) {
+ e_error(pclient->event,
+ "No IP addresses left to try");
+ }
+ program_client_fail(pclient,
+ (error != PROGRAM_CLIENT_ERROR_NONE ?
+ error : PROGRAM_CLIENT_ERROR_OTHER));
+ return;
+ };
+
+ prclient->ips++;
+ program_client_net_connect_real(prclient);
+}
+
+static void
+program_client_net_connect_resolved(const struct dns_lookup_result *result,
+ struct program_client_remote *prclient)
+{
+ struct program_client *pclient = &prclient->client;
+
+ if (result->ret != 0) {
+ e_error(pclient->event, "Cannot resolve `%s': %s",
+ prclient->address, result->error);
+ program_client_fail(pclient, PROGRAM_CLIENT_ERROR_OTHER);
+ return;
+ }
+
+ e_debug(pclient->event, "DNS lookup successful; got %d IPs",
+ result->ips_count);
+
+ /* Reduce timeout */
+ if (pclient->set.client_connect_timeout_msecs > 0) {
+ if (pclient->set.client_connect_timeout_msecs <=
+ result->msecs) {
+ /* We ran out of time */
+ program_client_fail(
+ pclient, PROGRAM_CLIENT_ERROR_CONNECT_TIMEOUT);
+ return;
+ }
+ pclient->set.client_connect_timeout_msecs -= result->msecs;
+ }
+
+ /* Then connect */
+ prclient->ips_count = result->ips_count;
+ prclient->ips_left = prclient->ips_count;
+ prclient->ips = p_memdup(pclient->pool, result->ips,
+ sizeof(struct ip_addr)*result->ips_count);
+ program_client_net_connect_real(prclient);
+}
+
+static int program_client_net_connect_init(struct program_client *pclient)
+{
+ struct program_client_remote *prclient =
+ (struct program_client_remote *)pclient;
+ struct ip_addr ip;
+
+ if (prclient->ips != NULL) {
+ /* Nothing to do */
+ } else if (net_addr2ip(prclient->address, &ip) == 0) {
+ prclient->resolved = TRUE;
+ prclient->ips = p_new(pclient->pool, struct ip_addr, 1);
+ *prclient->ips = ip;
+ prclient->ips_count = 1;
+ } else {
+ prclient->resolved = FALSE;
+ if (pclient->set.dns_client_socket_path != NULL) {
+ e_debug(pclient->event,
+ "Performing asynchronous DNS lookup");
+ prclient->dns_set.dns_client_socket_path =
+ pclient->set.dns_client_socket_path;
+ prclient->dns_set.timeout_msecs =
+ pclient->set.client_connect_timeout_msecs;
+ prclient->dns_set.event_parent = pclient->event;
+ dns_lookup(prclient->address, &prclient->dns_set,
+ program_client_net_connect_resolved,
+ prclient, &prclient->lookup);
+ return 0;
+ } else {
+ struct ip_addr *ips;
+ unsigned int ips_count;
+ int err;
+
+ /* Guess we do it here then.. */
+ err = net_gethostbyname(prclient->address,
+ &ips, &ips_count);
+ if (err != 0) {
+ e_error(pclient->event,
+ "Cannot resolve `%s': %s",
+ prclient->address,
+ net_gethosterror(err));
+ return -1;
+ }
+ prclient->ips_count = ips_count;
+ prclient->ips = p_memdup(pclient->pool,
+ ips, sizeof(*ips)*ips_count);
+
+ e_debug(pclient->event,
+ "DNS lookup successful; got %d IPs",
+ ips_count);
+ }
+ }
+
+ prclient->ips_left = prclient->ips_count;
+ prclient->to_retry = timeout_add_short(
+ 0, program_client_net_connect_real, prclient);
+ return 0;
+}
+
+static int program_client_remote_close_output(struct program_client *pclient)
+{
+ int fd_out = pclient->fd_out, fd_in = pclient->fd_in;
+
+ pclient->fd_out = -1;
+
+ /* Shutdown output; program stdin will get EOF */
+ if (fd_out >= 0) {
+ if (fd_in >= 0) {
+ if (shutdown(fd_out, SHUT_WR) < 0 &&
+ errno != ENOTCONN) {
+ e_error(pclient->event,
+ "shutdown(fd_out, SHUT_WR) failed: %m");
+ return -1;
+ }
+ } else {
+ i_close_fd(&fd_out);
+ }
+ }
+
+ return 1;
+}
+
+static void
+program_client_remote_disconnect(struct program_client *pclient,
+ bool force ATTR_UNUSED)
+{
+ struct program_client_remote *prclient =
+ (struct program_client_remote *)pclient;
+
+ timeout_remove(&prclient->to_retry);
+
+ program_client_disconnected(pclient);
+}
+
+static void
+program_client_remote_switch_ioloop(struct program_client *pclient)
+{
+ struct program_client_remote *prclient =
+ (struct program_client_remote *)pclient;
+
+ if (prclient->to_retry != NULL)
+ prclient->to_retry = io_loop_move_timeout(&prclient->to_retry);
+ if (prclient->lookup != NULL)
+ dns_lookup_switch_ioloop(prclient->lookup);
+}
+
+struct program_client *
+program_client_unix_create(const char *socket_path, const char *const *args,
+ const struct program_client_settings *set,
+ bool noreply)
+{
+ struct program_client_remote *prclient;
+ const char *label;
+ pool_t pool;
+
+ label = t_strconcat("unix:", socket_path, NULL);
+
+ pool = pool_alloconly_create("program client unix", 1024);
+ prclient = p_new(pool, struct program_client_remote, 1);
+ program_client_init(&prclient->client, pool, label, args, set);
+ prclient->client.connect = program_client_unix_connect;
+ prclient->client.close_output = program_client_remote_close_output;
+ prclient->client.disconnect = program_client_remote_disconnect;
+ prclient->client.switch_ioloop = program_client_remote_switch_ioloop;
+ prclient->address = p_strdup(pool, socket_path);
+ prclient->noreply = noreply;
+
+ return &prclient->client;
+}
+
+struct program_client *
+program_client_net_create(const char *host, in_port_t port,
+ const char *const *args,
+ const struct program_client_settings *set,
+ bool noreply)
+{
+ struct program_client_remote *prclient;
+ const char *label;
+ pool_t pool;
+
+ label = t_strdup_printf("tcp:%s:%u", host, port);
+
+ pool = pool_alloconly_create("program client net", 1024);
+ prclient = p_new(pool, struct program_client_remote, 1);
+ program_client_init(&prclient->client, pool, label, args, set);
+ prclient->client.connect = program_client_net_connect_init;
+ prclient->client.close_output = program_client_remote_close_output;
+ prclient->client.disconnect = program_client_remote_disconnect;
+ prclient->client.set.use_dotstream = TRUE;
+ prclient->address = p_strdup(pool, host);
+ prclient->port = port;
+ prclient->have_hostname = TRUE;
+ prclient->noreply = noreply;
+ return &prclient->client;
+}
+
+struct program_client *
+program_client_net_create_ips(const struct ip_addr *ips, size_t ips_count,
+ in_port_t port,
+ const char *const *args,
+ const struct program_client_settings *set,
+ bool noreply)
+{
+ struct program_client_remote *prclient;
+ const char *label;
+ pool_t pool;
+
+ i_assert(ips != NULL && ips_count > 0);
+
+ if (net_ipport2str(ips, port, &label) < 0)
+ i_unreached();
+ label = t_strconcat("tcp:", label, NULL);
+
+ pool = pool_alloconly_create("program client net", 1024);
+ prclient = p_new(pool, struct program_client_remote, 1);
+ program_client_init(&prclient->client, pool, label, args, set);
+ prclient->client.connect = program_client_net_connect_init;
+ prclient->client.close_output = program_client_remote_close_output;
+ prclient->client.disconnect = program_client_remote_disconnect;
+ prclient->client.switch_ioloop = program_client_remote_switch_ioloop;
+ prclient->client.set.use_dotstream = TRUE;
+ prclient->address = p_strdup(pool, net_ip2addr(ips));
+ prclient->ips = p_memdup(pool, ips,
+ sizeof(struct ip_addr)*ips_count);
+ prclient->ips_count = ips_count;
+ prclient->port = port;
+ prclient->noreply = noreply;
+ return &prclient->client;
+}
diff --git a/src/lib-program-client/program-client.c b/src/lib-program-client/program-client.c
new file mode 100644
index 0000000..c6c6ff6
--- /dev/null
+++ b/src/lib-program-client/program-client.c
@@ -0,0 +1,745 @@
+/* Copyright (c) 2002-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "ioloop.h"
+#include "array.h"
+#include "str.h"
+#include "str-sanitize.h"
+#include "safe-mkstemp.h"
+#include "istream-private.h"
+#include "ostream-dot.h"
+#include "istream-dot.h"
+#include "ostream.h"
+#include "iostream-pump.h"
+#include "iostream-temp.h"
+#include "lib-signals.h"
+
+#include "program-client-private.h"
+
+#include <unistd.h>
+
+#define MAX_OUTPUT_BUFFER_SIZE 16384
+#define MAX_OUTPUT_MEMORY_BUFFER (1024*128)
+
+void program_client_set_label(struct program_client *pclient,
+ const char *label)
+{
+ event_set_append_log_prefix(pclient->event,
+ t_strconcat("program ", label, ": ", NULL));
+}
+
+static void
+program_client_callback(struct program_client *pclient, int result,
+ void *context)
+{
+ program_client_callback_t *callback = pclient->callback;
+
+ pclient->callback = NULL;
+ if (pclient->destroying || callback == NULL)
+ return;
+ callback(result, context);
+}
+
+static void
+program_client_timeout(struct program_client *pclient)
+{
+ e_error(pclient->event,
+ "Execution timed out (> %u msecs)",
+ pclient->set.input_idle_timeout_msecs);
+ program_client_fail(pclient, PROGRAM_CLIENT_ERROR_RUN_TIMEOUT);
+}
+
+static void
+program_client_connect_timeout(struct program_client *pclient)
+{
+ e_error(pclient->event,
+ "Connection timed out (> %u msecs)",
+ pclient->set.client_connect_timeout_msecs);
+ program_client_fail(pclient, PROGRAM_CLIENT_ERROR_CONNECT_TIMEOUT);
+}
+
+static int
+program_client_connect(struct program_client *pclient)
+{
+ e_debug(pclient->event, "Establishing connection");
+
+ if (pclient->set.client_connect_timeout_msecs != 0) {
+ pclient->to = timeout_add(
+ pclient->set.client_connect_timeout_msecs,
+ program_client_connect_timeout, pclient);
+ }
+
+ return pclient->connect(pclient);
+}
+
+static int
+program_client_close_output(struct program_client *pclient)
+{
+ int ret;
+
+ o_stream_destroy(&pclient->program_output);
+ o_stream_destroy(&pclient->raw_program_output);
+ if ((ret = pclient->close_output(pclient)) < 0)
+ return -1;
+
+ return ret;
+}
+
+static void
+program_client_disconnect_extra_fds(struct program_client *pclient)
+{
+ struct program_client_extra_fd *efds;
+ unsigned int i, count;
+
+ if (!array_is_created(&pclient->extra_fds))
+ return;
+
+ efds = array_get_modifiable(&pclient->extra_fds, &count);
+ for(i = 0; i < count; i++) {
+ i_stream_unref(&efds[i].input);
+ io_remove(&efds[i].io);
+ if (efds[i].parent_fd != -1)
+ i_close_fd(&efds[i].parent_fd);
+ }
+
+ array_clear(&pclient->extra_fds);
+}
+
+static void
+program_client_do_disconnect(struct program_client *pclient)
+{
+ i_stream_destroy(&pclient->program_input);
+ o_stream_destroy(&pclient->program_output);
+ i_stream_destroy(&pclient->raw_program_input);
+ o_stream_destroy(&pclient->raw_program_output);
+
+ timeout_remove(&pclient->to);
+ io_remove(&pclient->io);
+ iostream_pump_destroy(&pclient->pump_in);
+ iostream_pump_destroy(&pclient->pump_out);
+
+ if (pclient->fd_out == pclient->fd_in)
+ pclient->fd_in = -1;
+ i_close_fd(&pclient->fd_in);
+ i_close_fd(&pclient->fd_out);
+
+ program_client_disconnect_extra_fds(pclient);
+
+ if (!pclient->disconnected)
+ e_debug(pclient->event, "Disconnected");
+ pclient->disconnected = TRUE;
+}
+
+void program_client_disconnected(struct program_client *pclient)
+{
+ program_client_do_disconnect(pclient);
+
+ if (pclient->other_error &&
+ pclient->error == PROGRAM_CLIENT_ERROR_NONE) {
+ pclient->error = PROGRAM_CLIENT_ERROR_OTHER;
+ }
+
+ program_client_callback(pclient,
+ (pclient->error != PROGRAM_CLIENT_ERROR_NONE ?
+ PROGRAM_CLIENT_EXIT_STATUS_INTERNAL_FAILURE :
+ pclient->exit_status),
+ pclient->context);
+}
+
+static void
+program_client_disconnect(struct program_client *pclient, bool force)
+{
+ if (pclient->disconnected)
+ return;
+
+ program_client_do_disconnect(pclient);
+ pclient->disconnect(pclient, force);
+}
+
+void program_client_fail(struct program_client *pclient,
+ enum program_client_error error)
+{
+ if (pclient->error != PROGRAM_CLIENT_ERROR_NONE)
+ return;
+
+ e_debug(pclient->event, "Failed to run program");
+
+ pclient->error = error;
+ program_client_disconnect(pclient, TRUE);
+}
+
+static bool
+program_client_input_pending(struct program_client *pclient)
+{
+ struct program_client_extra_fd *efds = NULL;
+ unsigned int count, i;
+
+ if (pclient->pump_in != NULL || pclient->pump_out != NULL)
+ return TRUE;
+
+ if (pclient->program_output != NULL &&
+ !pclient->program_output->closed &&
+ o_stream_get_buffer_used_size(pclient->program_output) > 0) {
+ return TRUE;
+ }
+ if (pclient->program_input != NULL &&
+ !pclient->program_input->closed &&
+ i_stream_have_bytes_left(pclient->program_input)) {
+ return TRUE;
+ }
+
+ if (array_is_created(&pclient->extra_fds)) {
+ efds = array_get_modifiable(&pclient->extra_fds, &count);
+ for(i = 0; i < count; i++) {
+ if (efds[i].input != NULL &&
+ !efds[i].input->closed &&
+ i_stream_have_bytes_left(efds[i].input)) {
+ return TRUE;
+ }
+ }
+ }
+
+ return FALSE;
+}
+
+static void
+program_client_output_finished(struct program_client *pclient)
+{
+ e_debug(pclient->event, "Finished input to program");
+
+ /* check whether program i/o is finished */
+ if (!program_client_input_pending(pclient)) {
+ /* finished */
+ program_client_disconnect(pclient, FALSE);
+ /* close output towards program, so that it reads EOF */
+ } else if (program_client_close_output(pclient) < 0) {
+ program_client_fail(pclient,
+ PROGRAM_CLIENT_ERROR_OTHER);
+ }
+}
+
+static int
+program_client_output_finish(struct program_client *pclient)
+{
+ struct ostream *output = pclient->program_output;
+ int ret = 0;
+
+ /* flush the output */
+ if ((ret=o_stream_finish(output)) < 0) {
+ e_error(pclient->event,
+ "write(%s) failed: %s",
+ o_stream_get_name(output),
+ o_stream_get_error(output));
+ program_client_fail(pclient, PROGRAM_CLIENT_ERROR_IO);
+ return -1;
+ }
+ if (ret > 0)
+ program_client_output_finished(pclient);
+ return ret;
+}
+
+static void
+program_client_output_pump_finished(enum iostream_pump_status status,
+ struct program_client *pclient)
+{
+ struct istream *input = pclient->input;
+ struct ostream *output = pclient->program_output;
+
+ i_assert(input != NULL);
+ i_assert(output != NULL);
+
+ switch (status) {
+ case IOSTREAM_PUMP_STATUS_INPUT_EOF:
+ break;
+ case IOSTREAM_PUMP_STATUS_INPUT_ERROR:
+ e_error(pclient->event,
+ "read(%s) failed: %s",
+ i_stream_get_name(input),
+ i_stream_get_error(input));
+ program_client_fail(pclient, PROGRAM_CLIENT_ERROR_IO);
+ return;
+ case IOSTREAM_PUMP_STATUS_OUTPUT_ERROR:
+ e_error(pclient->event,
+ "write(%s) failed: %s",
+ o_stream_get_name(output),
+ o_stream_get_error(output));
+ program_client_fail(pclient, PROGRAM_CLIENT_ERROR_IO);
+ return;
+ }
+
+ iostream_pump_destroy(&pclient->pump_out);
+
+ e_debug(pclient->event, "Finished streaming payload to program");
+
+ o_stream_set_flush_callback(pclient->program_output,
+ program_client_output_finish, pclient);
+ o_stream_set_flush_pending(pclient->program_output, TRUE);
+}
+
+static void
+program_client_input_finished(struct program_client *pclient)
+{
+ e_debug(pclient->event, "Finished output from program");
+
+ /* check whether program i/o is finished */
+ if (program_client_input_pending(pclient))
+ return;
+
+ /* finished */
+ program_client_disconnect(pclient, FALSE);
+}
+
+static void
+program_client_input_finish(struct program_client *pclient)
+{
+ struct istream *input = pclient->program_input;
+ const unsigned char *data;
+ size_t size;
+ int ret;
+
+ /* read (the remainder of) the raw program input */
+ while ((ret=i_stream_read_more(input, &data, &size)) > 0)
+ i_stream_skip(input, size);
+ if (ret == 0)
+ return;
+ if (ret < 0) {
+ if (input->stream_errno != 0) {
+ e_error(pclient->event,
+ "read(%s) failed: %s",
+ i_stream_get_name(input),
+ i_stream_get_error(input));
+ program_client_fail(pclient,
+ PROGRAM_CLIENT_ERROR_IO);
+ return;
+ }
+ }
+
+ if (pclient->program_input != pclient->raw_program_input) {
+ /* return to raw program input */
+ i_stream_unref(&pclient->program_input);
+ pclient->program_input = pclient->raw_program_input;
+ i_stream_ref(pclient->program_input);
+
+ io_remove(&pclient->io);
+ pclient->io = io_add_istream(pclient->program_input,
+ program_client_input_finish,
+ pclient);
+ io_set_pending(pclient->io);
+ }
+
+ program_client_input_finished(pclient);
+}
+
+static void
+program_client_input_pump_finished(enum iostream_pump_status status,
+ struct program_client *pclient)
+{
+ struct istream *input = pclient->program_input;
+ struct ostream *output = pclient->output;
+
+ i_assert(input != NULL);
+ i_assert(output != NULL);
+
+ switch (status) {
+ case IOSTREAM_PUMP_STATUS_INPUT_EOF:
+ break;
+ case IOSTREAM_PUMP_STATUS_INPUT_ERROR:
+ e_error(pclient->event,
+ "read(%s) failed: %s",
+ i_stream_get_name(input),
+ i_stream_get_error(input));
+ program_client_fail(pclient, PROGRAM_CLIENT_ERROR_IO);
+ return;
+ case IOSTREAM_PUMP_STATUS_OUTPUT_ERROR:
+ e_error(pclient->event,
+ "write(%s) failed: %s",
+ o_stream_get_name(output),
+ o_stream_get_error(output));
+ program_client_fail(pclient, PROGRAM_CLIENT_ERROR_IO);
+ return;
+ }
+
+ iostream_pump_destroy(&pclient->pump_in);
+
+ e_debug(pclient->event, "Finished streaming payload from program");
+
+ if (pclient->program_input != pclient->raw_program_input) {
+ /* return to raw program input */
+ i_stream_unref(&pclient->program_input);
+ pclient->program_input = pclient->raw_program_input;
+ i_stream_ref(pclient->program_input);
+ }
+
+ i_assert(pclient->io == NULL);
+ pclient->io = io_add_istream(pclient->program_input,
+ program_client_input_finish, pclient);
+ io_set_pending(pclient->io);
+}
+
+static void
+program_client_extra_fd_input(struct program_client_extra_fd *efd)
+{
+ struct program_client *pclient = efd->pclient;
+
+ i_assert(efd->callback != NULL);
+ efd->callback(efd->context, efd->input);
+
+ if (efd->input->closed || !i_stream_have_bytes_left(efd->input)) {
+ if (!program_client_input_pending(pclient))
+ program_client_disconnect(pclient, FALSE);
+ }
+}
+
+void program_client_connected(struct program_client *pclient)
+{
+ e_debug(pclient->event, "Connected to program");
+
+ /* finish creating program input */
+ if (pclient->raw_program_input != NULL) {
+ struct istream *input = pclient->raw_program_input;
+
+ /* initialize dot input stream if required */
+ if (pclient->set.use_dotstream)
+ input = i_stream_create_dot(input, FALSE);
+ else
+ i_stream_ref(input);
+ pclient->program_input = input;
+ }
+ /* finish creating program output */
+ if (pclient->raw_program_output != NULL) {
+ struct ostream *output = pclient->raw_program_output;
+
+ /* initialize dot output stream if required */
+ if (pclient->set.use_dotstream)
+ output = o_stream_create_dot(output, FALSE);
+ else
+ o_stream_ref(output);
+ pclient->program_output = output;
+ }
+
+ pclient->start_time = ioloop_timeval;
+ timeout_remove(&pclient->to);
+ if (pclient->set.input_idle_timeout_msecs != 0) {
+ pclient->to =
+ timeout_add(pclient->set.input_idle_timeout_msecs,
+ program_client_timeout, pclient);
+ }
+
+ /* run program input */
+ if (pclient->program_input == NULL) {
+ /* nothing */
+ } else if (pclient->output == NULL) {
+ i_assert(pclient->io == NULL);
+ pclient->io = io_add_istream(pclient->program_input,
+ program_client_input_finish,
+ pclient);
+ io_set_pending(pclient->io);
+ } else {
+ pclient->pump_in =
+ iostream_pump_create(pclient->program_input,
+ pclient->output);
+ iostream_pump_set_completion_callback(pclient->pump_in,
+ program_client_input_pump_finished, pclient);
+ iostream_pump_start(pclient->pump_in);
+ }
+
+ /* run program output */
+ if (pclient->program_output == NULL) {
+ /* nothing */
+ } else if (pclient->input == NULL) {
+ o_stream_set_flush_callback(pclient->program_output,
+ program_client_output_finish, pclient);
+ o_stream_set_flush_pending(pclient->program_output, TRUE);
+ } else {
+ pclient->pump_out =
+ iostream_pump_create(pclient->input,
+ pclient->program_output);
+ iostream_pump_set_completion_callback(pclient->pump_out,
+ program_client_output_pump_finished, pclient);
+ iostream_pump_start(pclient->pump_out);
+ }
+}
+
+void program_client_init(struct program_client *pclient, pool_t pool,
+ const char *initial_label, const char *const *args,
+ const struct program_client_settings *set)
+{
+ pclient->pool = pool;
+ if (args != NULL)
+ pclient->args = p_strarray_dup(pool, args);
+ pclient->fd_in = -1;
+ pclient->fd_out = -1;
+
+ if (set == NULL)
+ pclient->event = event_create(NULL);
+ else {
+ pclient->set = *set;
+ pclient->debug = set->debug;
+ pclient->set.dns_client_socket_path =
+ p_strdup(pool, set->dns_client_socket_path);
+ pclient->set.home = p_strdup(pool, set->home);
+
+ pclient->event = event_create(set->event);
+ event_set_forced_debug(pclient->event, set->debug);
+ }
+
+ program_client_set_label(pclient, initial_label);
+
+ e_debug(pclient->event, "Created");
+}
+
+void program_client_set_input(struct program_client *pclient,
+ struct istream *input)
+{
+ i_stream_unref(&pclient->input);
+ if (input != NULL)
+ i_stream_ref(input);
+ pclient->input = input;
+}
+
+void program_client_set_output(struct program_client *pclient,
+ struct ostream *output)
+{
+ o_stream_unref(&pclient->output);
+ if (output != NULL)
+ o_stream_ref(output);
+ pclient->output = output;
+ pclient->output_seekable = FALSE;
+}
+
+void program_client_set_output_seekable(struct program_client *pclient,
+ const char *temp_prefix)
+{
+ o_stream_unref(&pclient->output);
+ pclient->output = iostream_temp_create_sized(temp_prefix, 0,
+ "(program client seekable output)",
+ MAX_OUTPUT_MEMORY_BUFFER);
+ pclient->output_seekable = TRUE;
+}
+
+struct istream *
+program_client_get_output_seekable(struct program_client *pclient)
+{
+ i_assert(pclient->output_seekable);
+ return iostream_temp_finish(&pclient->output, IO_BLOCK_SIZE);
+}
+
+#undef program_client_set_extra_fd
+void program_client_set_extra_fd(struct program_client *pclient, int fd,
+ program_client_fd_callback_t *callback,
+ void *context)
+{
+ struct program_client_extra_fd *efds;
+ struct program_client_extra_fd *efd = NULL;
+ unsigned int i, count;
+ i_assert(fd > 1);
+
+ if (!array_is_created(&pclient->extra_fds))
+ p_array_init(&pclient->extra_fds, pclient->pool, 2);
+
+ efds = array_get_modifiable(&pclient->extra_fds, &count);
+ for(i = 0; i < count; i++) {
+ if (efds[i].child_fd == fd) {
+ efd = &efds[i];
+ break;
+ }
+ }
+
+ if (efd == NULL) {
+ efd = array_append_space(&pclient->extra_fds);
+ efd->pclient = pclient;
+ efd->child_fd = fd;
+ efd->parent_fd = -1;
+ }
+ efd->callback = callback;
+ efd->context = context;
+}
+
+void program_client_set_env(struct program_client *pclient, const char *name,
+ const char *value)
+{
+ const char *env;
+
+ if (!array_is_created(&pclient->envs))
+ p_array_init(&pclient->envs, pclient->pool, 16);
+
+ env = p_strdup_printf(pclient->pool, "%s=%s", name, value);
+ array_push_back(&pclient->envs, &env);
+
+ e_debug(pclient->event, "Pass environment: %s",
+ str_sanitize(env, 256));
+}
+
+void program_client_init_streams(struct program_client *pclient)
+{
+ /* Create streams for normal program I/O */
+ if (pclient->fd_out >= 0) {
+ struct ostream *program_output;
+
+ program_output = o_stream_create_fd(pclient->fd_out,
+ MAX_OUTPUT_BUFFER_SIZE);
+ o_stream_set_name(program_output, "program stdin");
+ o_stream_set_no_error_handling(program_output, TRUE);
+ pclient->raw_program_output = program_output;
+ }
+ if (pclient->fd_in >= 0) {
+ struct istream *program_input;
+
+ program_input = i_stream_create_fd(pclient->fd_in, SIZE_MAX);
+ i_stream_set_name(program_input, "program stdout");
+ pclient->raw_program_input = program_input;
+ }
+
+ /* Create streams for additional output through side-channel fds */
+ if (array_is_created(&pclient->extra_fds)) {
+ struct program_client_extra_fd *efds = NULL;
+ unsigned int count, i;
+
+ efds = array_get_modifiable(&pclient->extra_fds, &count);
+ for(i = 0; i < count; i++) {
+ i_assert(efds[i].parent_fd >= 0);
+ efds[i].input = i_stream_create_fd
+ (efds[i].parent_fd, SIZE_MAX);
+ i_stream_set_name(efds[i].input,
+ t_strdup_printf("program output fd=%d",
+ efds[i].child_fd));
+ efds[i].io = io_add(efds[i].parent_fd, IO_READ,
+ program_client_extra_fd_input,
+ &efds[i]);
+ }
+ }
+}
+
+void program_client_destroy(struct program_client **_pclient)
+{
+ struct program_client *pclient = *_pclient;
+
+ *_pclient = NULL;
+
+ e_debug(pclient->event, "Destroy");
+
+ pclient->destroying = TRUE;
+ pclient->callback = NULL;
+
+ program_client_disconnect(pclient, TRUE);
+
+ i_assert(pclient->callback == NULL);
+
+ i_stream_unref(&pclient->input);
+ o_stream_unref(&pclient->output);
+
+ i_stream_unref(&pclient->program_input);
+ o_stream_unref(&pclient->program_output);
+ i_stream_unref(&pclient->raw_program_input);
+ o_stream_unref(&pclient->raw_program_output);
+
+ if (pclient->destroy != NULL)
+ pclient->destroy(pclient);
+
+ event_unref(&pclient->event);
+
+ pool_unref(&pclient->pool);
+}
+
+void program_client_switch_ioloop(struct program_client *pclient)
+{
+ if (pclient->input != NULL)
+ i_stream_switch_ioloop(pclient->input);
+ if (pclient->program_input != NULL)
+ i_stream_switch_ioloop(pclient->program_input);
+ if (pclient->output != NULL)
+ o_stream_switch_ioloop(pclient->output);
+ if (pclient->program_output != NULL)
+ o_stream_switch_ioloop(pclient->program_output);
+ if (pclient->to != NULL)
+ pclient->to = io_loop_move_timeout(&pclient->to);
+ if (pclient->pump_in != NULL)
+ iostream_pump_switch_ioloop(pclient->pump_in);
+ if (pclient->pump_out != NULL)
+ iostream_pump_switch_ioloop(pclient->pump_out);
+ if (pclient->io != NULL)
+ pclient->io = io_loop_move_io(&pclient->io);
+ pclient->switch_ioloop(pclient);
+}
+
+int program_client_create(const char *uri, const char *const *args,
+ const struct program_client_settings *set,
+ bool noreply, struct program_client **pc_r,
+ const char **error_r)
+{
+ if (str_begins(uri, "exec:")) {
+ *pc_r = program_client_local_create(uri+5, args, set);
+ return 0;
+ } else if (str_begins(uri, "unix:")) {
+ *pc_r = program_client_unix_create(uri+5, args, set, noreply);
+ return 0;
+ } else if (str_begins(uri, "tcp:")) {
+ const char *host;
+ in_port_t port;
+
+ if (net_str2hostport(uri+4, 0, &host, &port) < 0 ||
+ port == 0) {
+ *error_r = t_strdup_printf(
+ "Invalid tcp syntax, "
+ "must be host:port in '%s'", uri+4);
+ return -1;
+ }
+ *pc_r = program_client_net_create(host, port, args, set,
+ noreply);
+ return 0;
+ } else {
+ *error_r = t_strdup_printf(
+ "Unsupported program client scheme '%s'",
+ t_strcut(uri, ':'));
+ return -1;
+ }
+}
+
+static void
+program_client_run_callback(int result, int *context)
+{
+ *context = result;
+ io_loop_stop(current_ioloop);
+}
+
+int program_client_run(struct program_client *pclient)
+{
+ int ret = -2;
+ struct ioloop *prev_ioloop = current_ioloop;
+ struct ioloop *ioloop = io_loop_create();
+
+ program_client_switch_ioloop(pclient);
+
+ program_client_run_async(pclient, program_client_run_callback, &ret);
+
+ if (ret == -2) {
+ io_loop_run(ioloop);
+ }
+
+ io_loop_set_current(prev_ioloop);
+ program_client_switch_ioloop(pclient);
+ io_loop_set_current(ioloop);
+ io_loop_destroy(&ioloop);
+
+ if (pclient->error != PROGRAM_CLIENT_ERROR_NONE)
+ return -1;
+
+ return pclient->exit_status;
+}
+
+#undef program_client_run_async
+void program_client_run_async(struct program_client *pclient,
+ program_client_callback_t *callback,
+ void *context)
+{
+ i_assert(callback != NULL);
+
+ pclient->disconnected = FALSE;
+ pclient->exit_status = PROGRAM_CLIENT_EXIT_STATUS_SUCCESS;
+ pclient->error = PROGRAM_CLIENT_ERROR_NONE;
+
+ pclient->callback = callback;
+ pclient->context = context;
+ if (program_client_connect(pclient) < 0)
+ program_client_fail(pclient, PROGRAM_CLIENT_ERROR_IO);
+}
diff --git a/src/lib-program-client/program-client.h b/src/lib-program-client/program-client.h
new file mode 100644
index 0000000..ce6cdf9
--- /dev/null
+++ b/src/lib-program-client/program-client.h
@@ -0,0 +1,101 @@
+#ifndef PROGRAM_CLIENT_H
+#define PROGRAM_CLIENT_H
+
+#include "restrict-access.h"
+#include "net.h"
+
+struct program_client;
+
+enum program_client_exit_status {
+ PROGRAM_CLIENT_EXIT_STATUS_INTERNAL_FAILURE = -1,
+ PROGRAM_CLIENT_EXIT_STATUS_FAILURE = 0,
+ PROGRAM_CLIENT_EXIT_STATUS_SUCCESS = 1,
+};
+
+struct program_client_settings {
+ unsigned int client_connect_timeout_msecs;
+ unsigned int input_idle_timeout_msecs;
+ /* initialize with
+ restrict_access_init(&set.restrict_set);
+ */
+ struct restrict_access_settings restrict_set;
+ const char *dns_client_socket_path;
+ const char *home;
+
+ /* Event to use for the program client. */
+ struct event *event;
+
+ bool allow_root:1;
+ bool debug:1;
+ bool drop_stderr:1;
+ /* use o_stream_dot, which is mainly useful to make sure that an
+ unexpectedly closed connection doesn't cause the partial input to
+ be accepted as valid and complete program input. This is always
+ enabled for 'net' program clients, which may likely encounter
+ unexpected connection termination. */
+ bool use_dotstream:1;
+};
+
+typedef void program_client_fd_callback_t(void *context, struct istream *input);
+typedef void program_client_callback_t(enum program_client_exit_status status,
+ void *context);
+
+struct program_client *
+program_client_local_create(const char *bin_path, const char *const *args,
+ const struct program_client_settings *set)
+ ATTR_NULL(3);
+struct program_client *
+program_client_unix_create(const char *socket_path, const char *const *args,
+ const struct program_client_settings *set,
+ bool noreply) ATTR_NULL(3);
+struct program_client *
+program_client_net_create(const char *host, in_port_t port,
+ const char *const *args,
+ const struct program_client_settings *set,
+ bool noreply) ATTR_NULL(4);
+struct program_client *
+program_client_net_create_ips(const struct ip_addr *ips, size_t ips_count,
+ in_port_t port, const char *const *args,
+ const struct program_client_settings *set,
+ bool noreply) ATTR_NULL(5);
+int program_client_create(const char *uri, const char *const *args,
+ const struct program_client_settings *set,
+ bool noreply, struct program_client **pc_r,
+ const char **error_r) ATTR_NULL(3);
+
+void program_client_destroy(struct program_client **_pclient);
+
+void program_client_set_input(struct program_client *pclient,
+ struct istream *input);
+void program_client_set_output(struct program_client *pclient,
+ struct ostream *output);
+
+void program_client_set_output_seekable(struct program_client *pclient,
+ const char *temp_prefix);
+struct istream *
+program_client_get_output_seekable(struct program_client *pclient);
+
+void program_client_switch_ioloop(struct program_client *pclient);
+
+/* Program provides side-channel output through an extra fd */
+void program_client_set_extra_fd(struct program_client *pclient, int fd,
+ program_client_fd_callback_t * callback, void *context);
+#define program_client_set_extra_fd(pclient, fd, callback, context) \
+ program_client_set_extra_fd(pclient, fd - \
+ CALLBACK_TYPECHECK(callback, \
+ void (*)(typeof(context), struct istream *input)), \
+ (program_client_fd_callback_t *)callback, context)
+
+void program_client_set_env(struct program_client *pclient,
+ const char *name, const char *value);
+
+enum program_client_exit_status
+program_client_run(struct program_client *pclient);
+void program_client_run_async(struct program_client *pclient,
+ program_client_callback_t *, void*);
+#define program_client_run_async(pclient, callback, context) \
+ program_client_run_async(pclient, (program_client_callback_t*)callback, \
+ 1 ? context : CALLBACK_TYPECHECK(callback, \
+ void (*)(enum program_client_exit_status, typeof(context))))
+
+#endif
diff --git a/src/lib-program-client/test-program-client-local.c b/src/lib-program-client/test-program-client-local.c
new file mode 100644
index 0000000..2040b15
--- /dev/null
+++ b/src/lib-program-client/test-program-client-local.c
@@ -0,0 +1,289 @@
+/* Copyright (c) 2002-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "test-lib.h"
+#include "buffer.h"
+#include "str.h"
+#include "istream.h"
+#include "istream-concat.h"
+#include "ostream.h"
+#include "lib-signals.h"
+#include "program-client.h"
+
+#include <unistd.h>
+
+static const char *pclient_test_io_string =
+ "Lorem ipsum dolor sit amet, consectetur adipiscing elit.\n"
+ "Praesent vehicula ac leo vel placerat. Nullam placerat \n"
+ "volutpat leo, sed ultricies felis pulvinar quis. Nam \n"
+ "tempus, augue ut tempor cursus, neque felis commodo lacus, \n"
+ "sit amet tincidunt arcu justo vel augue. Proin dapibus \n"
+ "vulputate maximus. Mauris congue lacus felis, sed varius \n"
+ "leo finibus sagittis. Cum sociis natoque penatibus et magnis \n"
+ "dis parturient montes, nascetur ridiculus mus. Aliquam \n"
+ "laoreet arcu a hendrerit consequat. Duis vitae erat tellus.";
+
+static struct program_client_settings pc_set = {
+ .client_connect_timeout_msecs = 10000,
+ .input_idle_timeout_msecs = 5000,
+ .debug = FALSE,
+ .restrict_set = {
+ .uid = (uid_t)-1,
+ .gid = (gid_t)-1,
+ },
+ /* we need to permit root when running make check as root */
+ .allow_root = TRUE,
+};
+
+static void test_program_success(void)
+{
+ struct program_client *pc;
+
+ const char *const args[] = {
+ "hello", "world", NULL
+ };
+
+ test_begin("test_program_success");
+
+ pc = program_client_local_create("/bin/echo", args, &pc_set);
+
+ buffer_t *output = buffer_create_dynamic(default_pool, 16);
+ struct ostream *os = test_ostream_create(output);
+ program_client_set_output(pc, os);
+
+ test_assert(program_client_run(pc) == 1);
+ test_assert(strcmp(str_c(output), "hello world\n") == 0);
+
+ program_client_destroy(&pc);
+
+ o_stream_unref(&os);
+ buffer_free(&output);
+
+ test_end();
+}
+
+static void test_program_io_sync(void)
+{
+ struct program_client *pc;
+
+ const char *const args[] = {
+ NULL
+ };
+
+ test_begin("test_program_io (sync)");
+
+ pc = program_client_local_create("/bin/cat", args, &pc_set);
+
+ struct istream *is = test_istream_create(pclient_test_io_string);
+ program_client_set_input(pc, is);
+
+ buffer_t *output = buffer_create_dynamic(default_pool, 16);
+ struct ostream *os = test_ostream_create(output);
+ program_client_set_output(pc, os);
+
+ test_assert(program_client_run(pc) == 1);
+ test_assert(strcmp(str_c(output), pclient_test_io_string) == 0);
+
+ program_client_destroy(&pc);
+
+ i_stream_unref(&is);
+ o_stream_unref(&os);
+ buffer_free(&output);
+
+ test_end();
+}
+
+static void test_program_io_async_callback(int result, int *ret)
+{
+ *ret = result;
+ test_assert(result == 1);
+ io_loop_stop(current_ioloop);
+}
+
+static void test_program_io_async(void)
+{
+ struct ioloop *prev_ioloop, *ioloop;
+ struct program_client *pc;
+ int ret = -2;
+
+ const char *const args[] = {
+ NULL
+ };
+
+ test_begin("test_program_io (async)");
+
+ prev_ioloop = current_ioloop;
+ ioloop = io_loop_create();
+
+ pc = program_client_local_create("/bin/cat", args, &pc_set);
+
+ struct istream *is = test_istream_create(pclient_test_io_string);
+ program_client_set_input(pc, is);
+
+ buffer_t *output = buffer_create_dynamic(default_pool, 16);
+ struct ostream *os = test_ostream_create(output);
+ program_client_set_output(pc, os);
+
+ program_client_run_async(pc, test_program_io_async_callback, &ret);
+
+ if (ret == -2)
+ io_loop_run(ioloop);
+
+ test_assert(strcmp(str_c(output), pclient_test_io_string) == 0);
+
+ program_client_destroy(&pc);
+
+ i_stream_unref(&is);
+ o_stream_unref(&os);
+ buffer_free(&output);
+ io_loop_set_current(prev_ioloop);
+ io_loop_set_current(ioloop);
+ io_loop_destroy(&ioloop);
+
+ test_end();
+}
+
+static void test_program_failure(void)
+{
+ struct program_client *pc;
+
+ const char *const args[] = {
+ NULL
+ };
+
+ test_begin("test_program_failure");
+
+ pc = program_client_local_create("/bin/false", args, &pc_set);
+
+ buffer_t *output = buffer_create_dynamic(default_pool, 16);
+ struct ostream *os = test_ostream_create(output);
+ program_client_set_output(pc, os);
+
+ test_assert(program_client_run(pc) == 0);
+ test_assert(strcmp(str_c(output), "") == 0);
+
+ program_client_destroy(&pc);
+
+ o_stream_unref(&os);
+ buffer_free(&output);
+
+ test_end();
+}
+
+static void test_program_io_big(void)
+{
+ struct program_client *pc;
+
+ /* nasty program that reads data in bits with intermittent delays
+ and then finally reads the rest in one go. */
+ const char *const args[] = {
+ "-c",
+ "(dd bs=10240 count=1; sleep 0.1; "
+ "dd bs=10240 count=1; sleep 0.1; "
+ "dd bs=10240 count=1; sleep 0.1; "
+ "dd bs=10240 count=1; sleep 0.1; "
+ "dd bs=10240 count=1; sleep 0.1; "
+ "dd bs=10240 count=1; sleep 0.1; cat) 2>/dev/null",
+ NULL
+ };
+
+ test_begin("test_program_io (big)");
+
+ pc = program_client_local_create("/bin/sh", args, &pc_set);
+
+ /* make big input with only a small reference string */
+ struct istream *is1 = test_istream_create(pclient_test_io_string);
+ struct istream *in1[11] = {is1, is1, is1, is1, is1,
+ is1, is1, is1, is1, is1, NULL};
+ struct istream *is2 = i_stream_create_concat(in1);
+ struct istream *in2[11] = {is2, is2, is2, is2, is2,
+ is2, is2, is2, is2, is2, NULL};
+ struct istream *is3 = i_stream_create_concat(in2);
+ struct istream *in3[11] = {is3, is3, is3, is3, is3,
+ is3, is3, is3, is3, is3, NULL};
+ struct istream *is = i_stream_create_concat(in3);
+
+ i_stream_unref(&is1);
+ i_stream_unref(&is2);
+ i_stream_unref(&is3);
+
+ program_client_set_input(pc, is);
+
+ buffer_t *output = buffer_create_dynamic(default_pool, 16);
+ struct ostream *os = test_ostream_create(output);
+ program_client_set_output(pc, os);
+
+ test_assert(program_client_run(pc) == 1);
+
+ test_assert(str_len(output) == strlen(pclient_test_io_string)*10*10*10);
+
+ program_client_destroy(&pc);
+
+ i_stream_unref(&is);
+ o_stream_unref(&os);
+ buffer_free(&output);
+
+ test_end();
+}
+
+static void test_program_wait_no_io(void)
+{
+ struct program_client_settings set = pc_set;
+ struct program_client *pc;
+
+ /* nasty program that reads data in bits with intermittent delays
+ and then finally reads the rest in one go. */
+ const char *const args[] = {
+ "-c", "sleep 1",
+ NULL
+ };
+
+ test_begin("test_program_wait (no timeout, no I/O)");
+
+ set.client_connect_timeout_msecs = 0;
+ set.input_idle_timeout_msecs = 0;
+ pc = program_client_local_create("/bin/sh", args, &set);
+
+ test_assert(program_client_run(pc) == 1);
+
+ program_client_destroy(&pc);
+
+ test_end();
+}
+
+int main(int argc, char *argv[])
+{
+ struct ioloop *ioloop;
+ int ret, c;
+
+ void (*tests[])(void) = {
+ test_program_success,
+ test_program_io_sync,
+ test_program_io_async,
+ test_program_io_big,
+ test_program_failure,
+ test_program_wait_no_io,
+ NULL
+ };
+
+ lib_init();
+
+ while ((c = getopt(argc, argv, "D")) > 0) {
+ switch (c) {
+ case 'D':
+ pc_set.debug = TRUE;
+ break;
+ default:
+ i_fatal("Usage: %s [-D]", argv[0]);
+ }
+ }
+
+ ioloop = io_loop_create();
+ lib_signals_init();
+ ret = test_run(tests);
+ lib_signals_deinit();
+ io_loop_destroy(&ioloop);
+
+ lib_deinit();
+ return ret;
+}
diff --git a/src/lib-program-client/test-program-client-net.c b/src/lib-program-client/test-program-client-net.c
new file mode 100644
index 0000000..ed7d68d
--- /dev/null
+++ b/src/lib-program-client/test-program-client-net.c
@@ -0,0 +1,545 @@
+/* Copyright (c) 2002-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "test-lib.h"
+#include "mempool.h"
+#include "buffer.h"
+#include "str.h"
+#include "array.h"
+#include "istream.h"
+#include "ostream.h"
+#include "istream-dot.h"
+#include "ostream-dot.h"
+#include "net.h"
+#include "iostream-temp.h"
+#include "program-client.h"
+
+#include <unistd.h>
+
+static const char *pclient_test_io_string =
+ "Lorem ipsum dolor sit amet, consectetur adipiscing elit.\r\n"
+ "Praesent vehicula ac leo vel placerat. Nullam placerat \r\n"
+ "volutpat leo, sed ultricies felis pulvinar quis. Nam \r\n"
+ "tempus, augue ut tempor cursus, neque felis commodo lacus, \r\n"
+ "sit amet tincidunt arcu justo vel augue. Proin dapibus \r\n"
+ "vulputate maximus. Mauris congue lacus felis, sed varius \r\n"
+ "leo finibus sagittis. Cum sociis natoque penatibus et magnis \r\n"
+ "dis parturient montes, nascetur ridiculus mus. Aliquam \r\n"
+ "laoreet arcu a hendrerit consequat. Duis vitae erat tellus.";
+
+static struct program_client_settings pc_set = {
+ .client_connect_timeout_msecs = 5000,
+ .input_idle_timeout_msecs = 10000,
+ .debug = FALSE,
+};
+
+static struct test_server {
+ struct ioloop *ioloop;
+ struct io *io;
+ struct timeout *to;
+ struct test_client *client;
+ int listen_fd;
+ in_port_t port;
+ unsigned int io_loop_ref;
+} test_globals;
+
+struct test_client {
+ pool_t pool;
+ int fd;
+ struct io *io;
+ struct istream *in;
+ struct ostream *out;
+ struct ostream *os_body;
+ struct istream *is_body;
+ struct istream *body;
+ ARRAY_TYPE(const_string) args;
+ enum {
+ CLIENT_STATE_INIT,
+ CLIENT_STATE_VERSION,
+ CLIENT_STATE_ARGS,
+ CLIENT_STATE_BODY,
+ CLIENT_STATE_FINISH
+ } state;
+};
+
+static void test_program_io_loop_run(void)
+{
+ if (test_globals.io_loop_ref++ == 0)
+ io_loop_run(current_ioloop);
+}
+
+static void test_program_io_loop_stop(void)
+{
+ if (--test_globals.io_loop_ref == 0)
+ io_loop_stop(current_ioloop);
+}
+
+static void test_program_client_destroy(struct test_client **_client)
+{
+ struct test_client *client = *_client;
+ *_client = NULL;
+
+ if (o_stream_finish(client->out) < 0)
+ i_error("output error: %s", o_stream_get_error(client->out));
+
+ io_remove(&client->io);
+ o_stream_unref(&client->out);
+ i_stream_unref(&client->in);
+ o_stream_unref(&client->os_body);
+ i_stream_unref(&client->is_body);
+ i_stream_unref(&client->body);
+ i_close_fd(&client->fd);
+ pool_unref(&client->pool);
+ test_globals.client = NULL;
+ test_program_io_loop_stop();
+}
+
+static int
+test_program_input_handle(struct test_client *client, const char *line)
+{
+ int cmp = -1;
+ const char *arg;
+
+ switch(client->state) {
+ case CLIENT_STATE_INIT:
+ cmp = strncmp(line, "VERSION\tscript\t", 15);
+ test_assert(cmp == 0);
+ if (cmp == 0)
+ client->state = CLIENT_STATE_VERSION;
+ else
+ return -1;
+ break;
+ case CLIENT_STATE_VERSION:
+ if (strcmp(line, "noreply") == 0 ||
+ strcmp(line, "-") == 0)
+ cmp = 0;
+ test_assert(cmp == 0);
+ if (cmp == 0)
+ client->state = CLIENT_STATE_ARGS;
+ else
+ return -1;
+ break;
+ case CLIENT_STATE_ARGS:
+ if (strcmp(line, "") == 0) {
+ array_append_zero(&client->args);
+ client->state = CLIENT_STATE_BODY;
+ return 0;
+ }
+ arg = p_strdup(client->pool, line);
+ array_push_back(&client->args, &arg);
+ break;
+ case CLIENT_STATE_BODY:
+ if (client->os_body == NULL) {
+ client->os_body = iostream_temp_create_named(
+ ".dovecot.test.", 0, "test_program_input body");
+ }
+ if (client->is_body == NULL)
+ client->is_body = i_stream_create_dot(client->in, FALSE);
+ switch (o_stream_send_istream(client->os_body,
+ client->is_body)) {
+ case OSTREAM_SEND_ISTREAM_RESULT_ERROR_OUTPUT:
+ i_panic("Cannot write to ostream-temp: %s",
+ o_stream_get_error(client->os_body));
+ case OSTREAM_SEND_ISTREAM_RESULT_ERROR_INPUT:
+ i_warning("Client stream error: %s",
+ i_stream_get_error(client->is_body));
+ return -1;
+ case OSTREAM_SEND_ISTREAM_RESULT_WAIT_INPUT:
+ break;
+ case OSTREAM_SEND_ISTREAM_RESULT_FINISHED:
+ client->body = iostream_temp_finish(&client->os_body,
+ SIZE_MAX);
+ i_stream_unref(&client->is_body);
+ client->state = CLIENT_STATE_FINISH;
+ return 0;
+ case OSTREAM_SEND_ISTREAM_RESULT_WAIT_OUTPUT:
+ i_panic("Cannot write to ostream-temp");
+ }
+ break;
+ case CLIENT_STATE_FINISH:
+ if (i_stream_read_eof(client->in))
+ return 1;
+ break;
+ }
+ return 0;
+}
+
+static void test_program_end(struct test_client *client)
+{
+ timeout_remove(&test_globals.to);
+ test_program_client_destroy(&client);
+}
+
+static void test_program_run(struct test_client *client)
+{
+ const char *const *args;
+ bool disconnect_later = FALSE;
+ unsigned int count;
+
+ struct ostream *os;
+
+ timeout_remove(&test_globals.to);
+ test_assert(array_is_created(&client->args));
+ if (array_is_created(&client->args)) {
+ args = array_get(&client->args, &count);
+ test_assert(count > 0);
+ if (count >= 2) {
+ if (strcmp(args[0], "test_program_success") == 0) {
+ /* Return hello world */
+ i_assert(count >= 3);
+ o_stream_nsend_str(client->out,
+ t_strdup_printf("%s %s\r\n.\n+\n",
+ args[1], args[2]));
+ } else if (strcmp(args[0], "test_program_io") == 0) {
+ os = o_stream_create_dot(client->out, FALSE);
+ o_stream_nsend_istream(os, client->body);
+ test_assert(o_stream_finish(os) > 0);
+ o_stream_unref(&os);
+ o_stream_nsend_str(client->out, "+\n");
+ } else if (strcmp(args[0],
+ "test_program_failure") == 0) {
+ o_stream_nsend_str(client->out, ".\n-\n");
+ }
+ } else
+ o_stream_nsend_str(client->out, ".\n-\n");
+ if (count >= 3 && strcmp(args[1], "slow_disconnect") == 0)
+ disconnect_later = TRUE;
+ }
+
+ test_assert(o_stream_flush(client->out) > 0);
+
+ if (!disconnect_later)
+ test_program_client_destroy(&client);
+ else {
+ test_globals.to = timeout_add_short(
+ 500, test_program_end, client);
+ }
+}
+
+static void test_program_input(struct test_client *client)
+{
+ const char *line = "";
+ int ret = 0;
+
+ while (ret >= 0) {
+ if (client->state >= CLIENT_STATE_BODY) {
+ ret = test_program_input_handle(client, NULL);
+ break;
+ }
+ while (client->state < CLIENT_STATE_BODY) {
+ line = i_stream_read_next_line(client->in);
+ if (line == NULL) {
+ ret = 0;
+ break;
+ }
+ ret = test_program_input_handle(client, line);
+ if (ret < 0) {
+ i_warning("Client sent invalid line: %s", line);
+ break;
+ }
+ }
+ }
+
+ if (ret < 0 || client->in->stream_errno != 0) {
+ test_program_client_destroy(&client);
+ return;
+ }
+ if (!client->in->eof)
+ return;
+
+ if (client->state < CLIENT_STATE_FINISH)
+ i_warning("Client prematurely disconnected");
+
+ io_remove(&client->io);
+ /* Incur slight delay to check if the connection gets prematurely
+ closed. */
+ test_globals.to = timeout_add_short(100, test_program_run, client);
+}
+
+static void test_program_connected(struct test_server *server)
+{
+ struct test_client *client;
+ int fd;
+
+ i_assert(server->client == NULL);
+ fd = net_accept(server->listen_fd, NULL, NULL); /* makes no sense on net */
+ if (fd < 0)
+ i_fatal("Failed to accept connection: %m");
+
+ net_set_nonblock(fd, TRUE);
+
+ pool_t pool = pool_alloconly_create("test_program client", 1024);
+ client = p_new(pool, struct test_client, 1);
+ client->pool = pool;
+ client->fd = fd;
+ client->in = i_stream_create_fd(fd, SIZE_MAX);
+ client->out = o_stream_create_fd(fd, SIZE_MAX);
+ client->io = io_add_istream(client->in, test_program_input, client);
+ p_array_init(&client->args, client->pool, 2);
+ server->client = client;
+
+ test_program_io_loop_run();
+}
+
+static void test_program_setup(void)
+{
+ struct ip_addr ip;
+
+ test_begin("test_program_setup");
+
+ test_globals.ioloop = io_loop_create();
+ io_loop_set_current(test_globals.ioloop);
+
+ /* Create listener */
+ test_globals.port = 0;
+ test_assert(net_addr2ip("127.0.0.1", &ip) == 0);
+
+ test_globals.listen_fd = net_listen(&ip, &test_globals.port, 1);
+
+ if (test_globals.listen_fd < 0)
+ i_fatal("Cannot create TCP listener: %m");
+
+ test_globals.io = io_add(test_globals.listen_fd, IO_READ,
+ test_program_connected, &test_globals);
+ test_end();
+}
+
+static void test_program_teardown(void)
+{
+ test_begin("test_program_teardown");
+
+ if (test_globals.client != NULL)
+ test_program_client_destroy(&test_globals.client);
+ io_remove(&test_globals.io);
+ i_close_fd(&test_globals.listen_fd);
+ io_loop_destroy(&test_globals.ioloop);
+ test_end();
+}
+
+static void test_program_async_callback(enum program_client_exit_status result,
+ int *ret)
+{
+ *ret = (int)result;
+ test_program_io_loop_stop();
+}
+
+static void test_program_success(void)
+{
+ struct program_client *pc;
+ int ret = -2;
+
+ const char *const args[] = {
+ "test_program_success", "hello", "world", NULL
+ };
+
+ test_begin("test_program_success");
+
+ pc = program_client_net_create("127.0.0.1", test_globals.port, args,
+ &pc_set, FALSE);
+
+ buffer_t *output = buffer_create_dynamic(default_pool, 16);
+ struct ostream *os = test_ostream_create(output);
+ program_client_set_output(pc, os);
+
+ program_client_run_async(pc, test_program_async_callback, &ret);
+
+ if (ret == -2)
+ test_program_io_loop_run();
+
+ test_assert(ret == 1);
+ test_assert(strcmp(str_c(output), "hello world") == 0);
+
+ program_client_destroy(&pc);
+
+ o_stream_unref(&os);
+ buffer_free(&output);
+
+ i_assert(test_globals.client == NULL);
+
+ test_end();
+}
+
+static void test_program_io_common(const char *const *args)
+{
+ struct program_client *pc;
+ int ret = -2;
+
+ pc = program_client_net_create("127.0.0.1", test_globals.port, args,
+ &pc_set, FALSE);
+
+ struct istream *is = test_istream_create(pclient_test_io_string);
+ program_client_set_input(pc, is);
+
+ buffer_t *output = buffer_create_dynamic(default_pool, 16);
+ struct ostream *os = test_ostream_create(output);
+ program_client_set_output(pc, os);
+
+ program_client_run_async(pc, test_program_async_callback, &ret);
+
+ if (ret == -2)
+ test_program_io_loop_run();
+
+ test_assert(ret == 1);
+ test_assert(strcmp(str_c(output), pclient_test_io_string) == 0);
+
+ program_client_destroy(&pc);
+
+ i_stream_unref(&is);
+ o_stream_unref(&os);
+ buffer_free(&output);
+
+ i_assert(test_globals.client == NULL);
+}
+
+static void test_program_io(void)
+{
+ const char *args[3] = {
+ "test_program_io", NULL, NULL
+ };
+
+ test_begin("test_program_io (async)");
+
+ test_program_io_common(args);
+
+ test_end();
+
+ args[1] = "slow_disconnect";
+
+ test_begin("test_program_io (async, slow disconnect)");
+
+ test_program_io_common(args);
+
+ test_end();
+}
+
+static void test_program_failure(void)
+{
+ struct program_client *pc;
+ int ret = -2;
+
+ const char *const args[] = {
+ "test_program_failure", NULL
+ };
+
+ test_begin("test_program_failure");
+
+ pc = program_client_net_create("127.0.0.1", test_globals.port, args,
+ &pc_set, FALSE);
+
+ buffer_t *output = buffer_create_dynamic(default_pool, 16);
+ struct ostream *os = test_ostream_create(output);
+ program_client_set_output(pc, os);
+
+ program_client_run_async(pc, test_program_async_callback, &ret);
+
+ if (ret == -2)
+ test_program_io_loop_run();
+
+ test_assert(ret == 0);
+
+ program_client_destroy(&pc);
+
+ o_stream_unref(&os);
+ buffer_free(&output);
+
+ i_assert(test_globals.client == NULL);
+
+ test_end();
+}
+
+static void test_program_noreply(void)
+{
+ struct program_client *pc;
+ int ret = -2;
+
+ const char *const args[] = {
+ "test_program_success", "hello", "world", NULL
+ };
+
+ test_begin("test_program_noreply");
+
+ pc = program_client_net_create("127.0.0.1", test_globals.port, args,
+ &pc_set, TRUE);
+
+ program_client_run_async(pc, test_program_async_callback, &ret);
+
+ if (ret == -2)
+ test_program_io_loop_run();
+
+ test_assert(ret == 1);
+
+ program_client_destroy(&pc);
+
+ i_assert(test_globals.client == NULL);
+
+ test_end();
+}
+
+static void test_program_refused(void)
+{
+ struct program_client *pc;
+ struct ip_addr ips[4];
+ int ret = -2;
+
+ const char *const args[] = {
+ "test_program_success", "hello", "world", NULL
+ };
+
+ test_begin("test_program_refused");
+
+ if (net_addr2ip("::1", &ips[0]) < 0 ||
+ net_addr2ip("127.0.0.3", &ips[1]) < 0 ||
+ net_addr2ip("127.0.0.2", &ips[2]) < 0 ||
+ net_addr2ip("127.0.0.1", &ips[3]) < 0) {
+ i_fatal("Cannot convert addresses");
+ }
+
+ pc = program_client_net_create_ips(ips, N_ELEMENTS(ips),
+ test_globals.port, args,
+ &pc_set, TRUE);
+
+ test_expect_errors(N_ELEMENTS(ips)-1);
+ program_client_run_async(pc, test_program_async_callback, &ret);
+
+ if (ret == -2)
+ test_program_io_loop_run();
+
+ test_assert(ret == 1);
+
+ program_client_destroy(&pc);
+
+ test_end();
+}
+
+int main(int argc, char *argv[])
+{
+ int ret, c;
+
+ void (*tests[])(void) = {
+ test_program_setup,
+ test_program_success,
+ test_program_io,
+ test_program_failure,
+ test_program_noreply,
+ test_program_refused,
+ test_program_teardown,
+ NULL
+ };
+
+ lib_init();
+
+ while ((c = getopt(argc, argv, "D")) > 0) {
+ switch (c) {
+ case 'D':
+ pc_set.debug = TRUE;
+ break;
+ default:
+ i_fatal("Usage: %s [-D]", argv[0]);
+ }
+ }
+
+ ret = test_run(tests);
+
+ lib_deinit();
+ return ret;
+}
diff --git a/src/lib-program-client/test-program-client-unix.c b/src/lib-program-client/test-program-client-unix.c
new file mode 100644
index 0000000..e524c88
--- /dev/null
+++ b/src/lib-program-client/test-program-client-unix.c
@@ -0,0 +1,441 @@
+/* Copyright (c) 2002-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "test-lib.h"
+#include "mempool.h"
+#include "buffer.h"
+#include "str.h"
+#include "array.h"
+#include "istream.h"
+#include "ostream.h"
+#include "net.h"
+#include "iostream-temp.h"
+#include "program-client.h"
+
+#include <unistd.h>
+
+static const char *TEST_SOCKET = "program-client-test.sock";
+static const char *pclient_test_io_string =
+ "Lorem ipsum dolor sit amet, consectetur adipiscing elit.\n"
+ "Praesent vehicula ac leo vel placerat. Nullam placerat \n"
+ "volutpat leo, sed ultricies felis pulvinar quis. Nam \n"
+ "tempus, augue ut tempor cursus, neque felis commodo lacus, \n"
+ "sit amet tincidunt arcu justo vel augue. Proin dapibus \n"
+ "vulputate maximus. Mauris congue lacus felis, sed varius \n"
+ "leo finibus sagittis. Cum sociis natoque penatibus et magnis \n"
+ "dis parturient montes, nascetur ridiculus mus. Aliquam \n"
+ "laoreet arcu a hendrerit consequat. Duis vitae erat tellus.";
+
+static struct program_client_settings pc_set = {
+ .client_connect_timeout_msecs = 1000,
+ .input_idle_timeout_msecs = 5000,
+ .debug = FALSE,
+};
+
+static struct test_server {
+ struct ioloop *ioloop;
+ struct io *io;
+ struct timeout *to;
+ struct test_client *client;
+ int listen_fd;
+} test_globals;
+
+struct test_client {
+ pool_t pool;
+ int fd;
+ struct io *io;
+ struct istream *in;
+ struct ostream *out;
+ struct ostream *os_body;
+ struct istream *body;
+ ARRAY_TYPE(const_string) args;
+ enum {
+ CLIENT_STATE_INIT,
+ CLIENT_STATE_VERSION,
+ CLIENT_STATE_ARGS,
+ CLIENT_STATE_BODY
+ } state;
+};
+
+static void test_program_client_destroy(struct test_client **_client)
+{
+ struct test_client *client = *_client;
+ *_client = NULL;
+
+ if (o_stream_finish(client->out) < 0)
+ i_error("output error: %s", o_stream_get_error(client->out));
+
+ io_remove(&client->io);
+ o_stream_unref(&client->out);
+ i_stream_unref(&client->in);
+ o_stream_unref(&client->os_body);
+ i_stream_unref(&client->body);
+ i_close_fd(&client->fd);
+ pool_unref(&client->pool);
+ test_globals.client = NULL;
+}
+
+static int
+test_program_input_handle(struct test_client *client, const char *line)
+{
+ int cmp = -1;
+ const char *arg;
+
+ switch(client->state) {
+ case CLIENT_STATE_INIT:
+ cmp = strncmp(line, "VERSION\tscript\t", 15);
+ test_assert(cmp == 0);
+ if (cmp == 0)
+ client->state = CLIENT_STATE_VERSION;
+ else
+ return -1;
+ break;
+ case CLIENT_STATE_VERSION:
+ if (strcmp(line, "noreply") == 0 ||
+ strcmp(line, "-") == 0)
+ cmp = 0;
+ test_assert(cmp == 0);
+ if (cmp == 0)
+ client->state = CLIENT_STATE_ARGS;
+ else
+ return -1;
+ break;
+ case CLIENT_STATE_ARGS:
+ if (strcmp(line, "") == 0) {
+ array_append_zero(&client->args);
+ client->state = CLIENT_STATE_BODY;
+ return 0;
+ }
+ arg = p_strdup(client->pool, line);
+ array_push_back(&client->args, &arg);
+ break;
+ case CLIENT_STATE_BODY:
+ if (client->os_body == NULL) {
+ client->os_body = iostream_temp_create_named(
+ ".dovecot.test.", 0, "test_program_input body");
+ }
+ switch (o_stream_send_istream(client->os_body, client->in)) {
+ case OSTREAM_SEND_ISTREAM_RESULT_ERROR_OUTPUT:
+ i_panic("Cannot write to ostream-temp: %s",
+ o_stream_get_error(client->os_body));
+ case OSTREAM_SEND_ISTREAM_RESULT_ERROR_INPUT:
+ i_warning("Client stream error: %s",
+ i_stream_get_error(client->in));
+ return -1;
+ case OSTREAM_SEND_ISTREAM_RESULT_WAIT_INPUT:
+ i_debug("waiting for input");
+ break;
+ case OSTREAM_SEND_ISTREAM_RESULT_FINISHED:
+ client->body = iostream_temp_finish(&client->os_body,
+ SIZE_MAX);
+ return 1;
+ case OSTREAM_SEND_ISTREAM_RESULT_WAIT_OUTPUT:
+ i_panic("Cannot write to ostream-temp");
+ }
+ break;
+ }
+ return 0;
+}
+
+static void test_program_end(struct test_client *client)
+{
+ timeout_remove(&test_globals.to);
+ test_program_client_destroy(&client);
+}
+
+static void test_program_run(struct test_client *client)
+{
+ const char *const *args;
+ unsigned int count;
+
+ timeout_remove(&test_globals.to);
+
+ args = array_get(&client->args, &count);
+ test_assert(count >= 2);
+ if (strcmp(args[0], "test_program_success") == 0) {
+ /* Return hello world */
+ test_assert(count >= 3);
+ o_stream_nsend_str(client->out, t_strdup_printf("%s %s\n+\n",
+ args[1], args[2]));
+ } else if (strcmp(args[0], "test_program_io") == 0) {
+ o_stream_nsend_istream(client->out, client->body);
+ o_stream_nsend_str(client->out, "+\n");
+ } else if (strcmp(args[0], "test_program_failure") == 0) {
+ o_stream_nsend_str(client->out, "-\n");
+ }
+ if (count < 3 || strcmp(args[1], "slow_disconnect") != 0)
+ test_program_client_destroy(&client);
+ else {
+ test_globals.to = timeout_add_short(
+ 500, test_program_end, client);
+ }
+}
+
+static void test_program_input(struct test_client *client)
+{
+ const char *line = "";
+ int ret = 0;
+
+ while (ret >= 0) {
+ if (client->state == CLIENT_STATE_BODY) {
+ ret = test_program_input_handle(client, NULL);
+ break;
+ }
+ while (client->state < CLIENT_STATE_BODY) {
+ line = i_stream_read_next_line(client->in);
+ if (line == NULL) {
+ ret = 0;
+ break;
+ }
+ ret = test_program_input_handle(client, line);
+ if (ret < 0) {
+ i_warning("Client sent invalid line: %s", line);
+ break;
+ }
+ }
+ }
+
+ if (ret < 0 || client->in->stream_errno != 0) {
+ test_program_client_destroy(&client);
+ return;
+ }
+ if (!client->in->eof)
+ return;
+
+ if (client->state != CLIENT_STATE_BODY)
+ i_warning("Client prematurely disconnected");
+
+ io_remove(&client->io);
+ /* Incur slight delay to check if the connection gets prematurely
+ closed. */
+ test_globals.to = timeout_add_short(100, test_program_run, client);
+}
+
+static void test_program_connected(struct test_server *server)
+{
+ struct test_client *client;
+ int fd;
+
+ i_assert(server->client == NULL);
+ fd = net_accept(server->listen_fd, NULL, NULL); /* makes no sense on unix */
+ if (fd < 0)
+ i_fatal("Failed to accept connection: %m");
+
+ net_set_nonblock(fd, TRUE);
+
+ pool_t pool = pool_alloconly_create("test_program client", 1024);
+ client = p_new(pool, struct test_client, 1);
+ client->pool = pool;
+ client->fd = fd;
+ client->in = i_stream_create_fd(fd, SIZE_MAX);
+ client->out = o_stream_create_fd(fd, SIZE_MAX);
+ client->io = io_add_istream(client->in, test_program_input, client);
+ p_array_init(&client->args, client->pool, 2);
+ server->client = client;
+}
+
+static void test_program_setup(void)
+{
+ test_begin("test_program_setup");
+
+ test_globals.ioloop = io_loop_create();
+ io_loop_set_current(test_globals.ioloop);
+
+ /* Create listener */
+ test_globals.listen_fd = net_listen_unix_unlink_stale(TEST_SOCKET, 100);
+ if (test_globals.listen_fd < 0)
+ i_fatal("Cannot create unix listener: %m");
+
+ test_globals.io = io_add(test_globals.listen_fd, IO_READ,
+ test_program_connected, &test_globals);
+ test_end();
+}
+
+static void test_program_teardown(void)
+{
+ test_begin("test_program_teardown");
+
+ if (test_globals.client != NULL)
+ test_program_client_destroy(&test_globals.client);
+
+ io_remove(&test_globals.io);
+ i_close_fd(&test_globals.listen_fd);
+ io_loop_destroy(&test_globals.ioloop);
+ i_unlink(TEST_SOCKET);
+ test_end();
+}
+
+static void test_program_async_callback(enum program_client_exit_status result,
+ int *ret)
+{
+ *ret = (int)result;
+ io_loop_stop(current_ioloop);
+}
+
+static void test_program_success(void)
+{
+ struct program_client *pc;
+ int ret;
+
+ const char *const args[] = {
+ "test_program_success", "hello", "world", NULL
+ };
+
+ test_begin("test_program_success");
+
+ pc = program_client_unix_create(TEST_SOCKET, args, &pc_set, FALSE);
+
+ buffer_t *output = buffer_create_dynamic(default_pool, 16);
+ struct ostream *os = test_ostream_create(output);
+ program_client_set_output(pc, os);
+
+ program_client_run_async(pc, test_program_async_callback, &ret);
+
+ io_loop_run(current_ioloop);
+
+ test_assert(ret == 1);
+ test_assert(strcmp(str_c(output), "hello world\n") == 0);
+
+ program_client_destroy(&pc);
+
+ o_stream_unref(&os);
+ buffer_free(&output);
+
+ test_end();
+}
+
+static void test_program_io_common(const char *const *args)
+{
+ struct program_client *pc;
+ int ret;
+
+ pc = program_client_unix_create(TEST_SOCKET, args, &pc_set, FALSE);
+
+ struct istream *is = test_istream_create(pclient_test_io_string);
+ program_client_set_input(pc, is);
+
+ buffer_t *output = buffer_create_dynamic(default_pool, 16);
+ struct ostream *os = test_ostream_create(output);
+ program_client_set_output(pc, os);
+
+ program_client_run_async(pc, test_program_async_callback, &ret);
+
+ io_loop_run(current_ioloop);
+
+ test_assert(ret == 1);
+ test_assert(strcmp(str_c(output), pclient_test_io_string) == 0);
+
+ program_client_destroy(&pc);
+
+ i_stream_unref(&is);
+ o_stream_unref(&os);
+ buffer_free(&output);
+}
+
+static void test_program_io(void)
+{
+ const char *args[3] = {
+ "test_program_io", NULL, NULL
+ };
+
+ test_begin("test_program_io (async)");
+
+ test_program_io_common(args);
+
+ test_end();
+
+ args[1] = "slow_disconnect";
+
+ test_begin("test_program_io (async, slow disconnect)");
+
+ test_program_io_common(args);
+
+ test_end();
+}
+
+static void test_program_failure(void)
+{
+ struct program_client *pc;
+ int ret;
+
+ const char *const args[] = {
+ "test_program_failure", NULL
+ };
+
+ test_begin("test_program_failure");
+
+ pc = program_client_unix_create(TEST_SOCKET, args, &pc_set, FALSE);
+
+ buffer_t *output = buffer_create_dynamic(default_pool, 16);
+ struct ostream *os = test_ostream_create(output);
+ program_client_set_output(pc, os);
+
+ program_client_run_async(pc, test_program_async_callback, &ret);
+
+ io_loop_run(current_ioloop);
+
+ test_assert(ret == 0);
+
+ program_client_destroy(&pc);
+
+ o_stream_unref(&os);
+ buffer_free(&output);
+
+ test_end();
+}
+
+static void test_program_noreply(void)
+{
+ struct program_client *pc;
+ int ret;
+
+ const char *const args[] = {
+ "test_program_success", "hello", "world", NULL
+ };
+
+ test_begin("test_program_noreply");
+
+ pc = program_client_unix_create(TEST_SOCKET, args, &pc_set, TRUE);
+
+ program_client_run_async(pc, test_program_async_callback, &ret);
+
+ io_loop_run(current_ioloop);
+
+ test_assert(ret == 1);
+
+ program_client_destroy(&pc);
+
+ test_end();
+}
+
+int main(int argc, char *argv[])
+{
+ int ret, c;
+
+ void (*tests[])(void) = {
+ test_program_setup,
+ test_program_success,
+ test_program_io,
+ test_program_failure,
+ test_program_noreply,
+ test_program_teardown,
+ NULL
+ };
+
+ lib_init();
+
+ while ((c = getopt(argc, argv, "D")) > 0) {
+ switch (c) {
+ case 'D':
+ pc_set.debug = TRUE;
+ break;
+ default:
+ i_fatal("Usage: %s [-D]", argv[0]);
+ }
+ }
+
+ ret = test_run(tests);
+
+ lib_deinit();
+ return ret;
+}
diff --git a/src/lib-sasl/Makefile.am b/src/lib-sasl/Makefile.am
new file mode 100644
index 0000000..5017bb9
--- /dev/null
+++ b/src/lib-sasl/Makefile.am
@@ -0,0 +1,40 @@
+noinst_LTLIBRARIES = libsasl.la
+
+AM_CPPFLAGS = \
+ -I$(top_srcdir)/src/lib \
+ -I$(top_srcdir)/src/lib-test
+
+libsasl_la_SOURCES = \
+ mech-external.c \
+ mech-login.c \
+ mech-plain.c \
+ mech-oauthbearer.c \
+ dsasl-client.c
+
+headers = \
+ dsasl-client.h \
+ dsasl-client-private.h
+
+pkginc_libdir=$(pkgincludedir)
+pkginc_lib_HEADERS = $(headers)
+
+test_programs = \
+ test-sasl-client
+
+noinst_PROGRAMS = $(test_programs)
+
+test_libs = \
+ $(noinst_LTLIBRARIES) \
+ ../lib-test/libtest.la \
+ ../lib/liblib.la
+
+test_deps = $(test_libs)
+
+test_sasl_client_SOURCES = test-sasl-client.c
+test_sasl_client_LDADD = $(test_libs)
+test_sasl_client_DEPENDENCIES = $(test_deps)
+
+check-local:
+ for bin in $(test_programs); do \
+ if ! $(RUN_TEST) ./$$bin; then exit 1; fi; \
+ done
diff --git a/src/lib-sasl/Makefile.in b/src/lib-sasl/Makefile.in
new file mode 100644
index 0000000..55c8891
--- /dev/null
+++ b/src/lib-sasl/Makefile.in
@@ -0,0 +1,870 @@
+# Makefile.in generated by automake 1.16.1 from Makefile.am.
+# @configure_input@
+
+# Copyright (C) 1994-2018 Free Software Foundation, Inc.
+
+# This Makefile.in is free software; the Free Software Foundation
+# gives unlimited permission to copy and/or distribute it,
+# with or without modifications, as long as this notice is preserved.
+
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY, to the extent permitted by law; without
+# even the implied warranty of MERCHANTABILITY or FITNESS FOR A
+# PARTICULAR PURPOSE.
+
+@SET_MAKE@
+
+
+
+VPATH = @srcdir@
+am__is_gnu_make = { \
+ if test -z '$(MAKELEVEL)'; then \
+ false; \
+ elif test -n '$(MAKE_HOST)'; then \
+ true; \
+ elif test -n '$(MAKE_VERSION)' && test -n '$(CURDIR)'; then \
+ true; \
+ else \
+ false; \
+ fi; \
+}
+am__make_running_with_option = \
+ case $${target_option-} in \
+ ?) ;; \
+ *) echo "am__make_running_with_option: internal error: invalid" \
+ "target option '$${target_option-}' specified" >&2; \
+ exit 1;; \
+ esac; \
+ has_opt=no; \
+ sane_makeflags=$$MAKEFLAGS; \
+ if $(am__is_gnu_make); then \
+ sane_makeflags=$$MFLAGS; \
+ else \
+ case $$MAKEFLAGS in \
+ *\\[\ \ ]*) \
+ bs=\\; \
+ sane_makeflags=`printf '%s\n' "$$MAKEFLAGS" \
+ | sed "s/$$bs$$bs[$$bs $$bs ]*//g"`;; \
+ esac; \
+ fi; \
+ skip_next=no; \
+ strip_trailopt () \
+ { \
+ flg=`printf '%s\n' "$$flg" | sed "s/$$1.*$$//"`; \
+ }; \
+ for flg in $$sane_makeflags; do \
+ test $$skip_next = yes && { skip_next=no; continue; }; \
+ case $$flg in \
+ *=*|--*) continue;; \
+ -*I) strip_trailopt 'I'; skip_next=yes;; \
+ -*I?*) strip_trailopt 'I';; \
+ -*O) strip_trailopt 'O'; skip_next=yes;; \
+ -*O?*) strip_trailopt 'O';; \
+ -*l) strip_trailopt 'l'; skip_next=yes;; \
+ -*l?*) strip_trailopt 'l';; \
+ -[dEDm]) skip_next=yes;; \
+ -[JT]) skip_next=yes;; \
+ esac; \
+ case $$flg in \
+ *$$target_option*) has_opt=yes; break;; \
+ esac; \
+ done; \
+ test $$has_opt = yes
+am__make_dryrun = (target_option=n; $(am__make_running_with_option))
+am__make_keepgoing = (target_option=k; $(am__make_running_with_option))
+pkgdatadir = $(datadir)/@PACKAGE@
+pkgincludedir = $(includedir)/@PACKAGE@
+pkglibdir = $(libdir)/@PACKAGE@
+pkglibexecdir = $(libexecdir)/@PACKAGE@
+am__cd = CDPATH="$${ZSH_VERSION+.}$(PATH_SEPARATOR)" && cd
+install_sh_DATA = $(install_sh) -c -m 644
+install_sh_PROGRAM = $(install_sh) -c
+install_sh_SCRIPT = $(install_sh) -c
+INSTALL_HEADER = $(INSTALL_DATA)
+transform = $(program_transform_name)
+NORMAL_INSTALL = :
+PRE_INSTALL = :
+POST_INSTALL = :
+NORMAL_UNINSTALL = :
+PRE_UNINSTALL = :
+POST_UNINSTALL = :
+build_triplet = @build@
+host_triplet = @host@
+noinst_PROGRAMS = $(am__EXEEXT_1)
+subdir = src/lib-sasl
+ACLOCAL_M4 = $(top_srcdir)/aclocal.m4
+am__aclocal_m4_deps = $(top_srcdir)/m4/ac_checktype2.m4 \
+ $(top_srcdir)/m4/ac_typeof.m4 $(top_srcdir)/m4/arc4random.m4 \
+ $(top_srcdir)/m4/blockdev.m4 $(top_srcdir)/m4/c99_vsnprintf.m4 \
+ $(top_srcdir)/m4/clock_gettime.m4 $(top_srcdir)/m4/crypt.m4 \
+ $(top_srcdir)/m4/crypt_xpg6.m4 $(top_srcdir)/m4/dbqlk.m4 \
+ $(top_srcdir)/m4/dirent_dtype.m4 $(top_srcdir)/m4/dovecot.m4 \
+ $(top_srcdir)/m4/fd_passing.m4 $(top_srcdir)/m4/fdatasync.m4 \
+ $(top_srcdir)/m4/flexible_array_member.m4 \
+ $(top_srcdir)/m4/glibc.m4 $(top_srcdir)/m4/gmtime_max.m4 \
+ $(top_srcdir)/m4/gmtime_tm_gmtoff.m4 \
+ $(top_srcdir)/m4/ioloop.m4 $(top_srcdir)/m4/iovec.m4 \
+ $(top_srcdir)/m4/ipv6.m4 $(top_srcdir)/m4/libcap.m4 \
+ $(top_srcdir)/m4/libtool.m4 $(top_srcdir)/m4/libwrap.m4 \
+ $(top_srcdir)/m4/linux_mremap.m4 $(top_srcdir)/m4/ltoptions.m4 \
+ $(top_srcdir)/m4/ltsugar.m4 $(top_srcdir)/m4/ltversion.m4 \
+ $(top_srcdir)/m4/lt~obsolete.m4 $(top_srcdir)/m4/mmap_write.m4 \
+ $(top_srcdir)/m4/mntctl.m4 $(top_srcdir)/m4/modules.m4 \
+ $(top_srcdir)/m4/notify.m4 $(top_srcdir)/m4/nsl.m4 \
+ $(top_srcdir)/m4/off_t_max.m4 $(top_srcdir)/m4/pkg.m4 \
+ $(top_srcdir)/m4/pr_set_dumpable.m4 \
+ $(top_srcdir)/m4/q_quotactl.m4 $(top_srcdir)/m4/quota.m4 \
+ $(top_srcdir)/m4/random.m4 $(top_srcdir)/m4/rlimit.m4 \
+ $(top_srcdir)/m4/sendfile.m4 $(top_srcdir)/m4/size_t_signed.m4 \
+ $(top_srcdir)/m4/sockpeercred.m4 $(top_srcdir)/m4/sql.m4 \
+ $(top_srcdir)/m4/ssl.m4 $(top_srcdir)/m4/st_tim.m4 \
+ $(top_srcdir)/m4/static_array.m4 $(top_srcdir)/m4/test_with.m4 \
+ $(top_srcdir)/m4/time_t.m4 $(top_srcdir)/m4/typeof.m4 \
+ $(top_srcdir)/m4/typeof_dev_t.m4 \
+ $(top_srcdir)/m4/uoff_t_max.m4 $(top_srcdir)/m4/vararg.m4 \
+ $(top_srcdir)/m4/want_apparmor.m4 \
+ $(top_srcdir)/m4/want_bsdauth.m4 \
+ $(top_srcdir)/m4/want_bzlib.m4 \
+ $(top_srcdir)/m4/want_cassandra.m4 \
+ $(top_srcdir)/m4/want_cdb.m4 \
+ $(top_srcdir)/m4/want_checkpassword.m4 \
+ $(top_srcdir)/m4/want_clucene.m4 $(top_srcdir)/m4/want_db.m4 \
+ $(top_srcdir)/m4/want_gssapi.m4 $(top_srcdir)/m4/want_icu.m4 \
+ $(top_srcdir)/m4/want_ldap.m4 $(top_srcdir)/m4/want_lua.m4 \
+ $(top_srcdir)/m4/want_lz4.m4 $(top_srcdir)/m4/want_lzma.m4 \
+ $(top_srcdir)/m4/want_mysql.m4 $(top_srcdir)/m4/want_pam.m4 \
+ $(top_srcdir)/m4/want_passwd.m4 $(top_srcdir)/m4/want_pgsql.m4 \
+ $(top_srcdir)/m4/want_prefetch.m4 \
+ $(top_srcdir)/m4/want_shadow.m4 \
+ $(top_srcdir)/m4/want_sodium.m4 $(top_srcdir)/m4/want_solr.m4 \
+ $(top_srcdir)/m4/want_sqlite.m4 \
+ $(top_srcdir)/m4/want_stemmer.m4 \
+ $(top_srcdir)/m4/want_systemd.m4 \
+ $(top_srcdir)/m4/want_textcat.m4 \
+ $(top_srcdir)/m4/want_unwind.m4 $(top_srcdir)/m4/want_zlib.m4 \
+ $(top_srcdir)/m4/want_zstd.m4 $(top_srcdir)/configure.ac
+am__configure_deps = $(am__aclocal_m4_deps) $(CONFIGURE_DEPENDENCIES) \
+ $(ACLOCAL_M4)
+DIST_COMMON = $(srcdir)/Makefile.am $(pkginc_lib_HEADERS) \
+ $(am__DIST_COMMON)
+mkinstalldirs = $(install_sh) -d
+CONFIG_HEADER = $(top_builddir)/config.h
+CONFIG_CLEAN_FILES =
+CONFIG_CLEAN_VPATH_FILES =
+am__EXEEXT_1 = test-sasl-client$(EXEEXT)
+PROGRAMS = $(noinst_PROGRAMS)
+LTLIBRARIES = $(noinst_LTLIBRARIES)
+libsasl_la_LIBADD =
+am_libsasl_la_OBJECTS = mech-external.lo mech-login.lo mech-plain.lo \
+ mech-oauthbearer.lo dsasl-client.lo
+libsasl_la_OBJECTS = $(am_libsasl_la_OBJECTS)
+AM_V_lt = $(am__v_lt_@AM_V@)
+am__v_lt_ = $(am__v_lt_@AM_DEFAULT_V@)
+am__v_lt_0 = --silent
+am__v_lt_1 =
+am_test_sasl_client_OBJECTS = test-sasl-client.$(OBJEXT)
+test_sasl_client_OBJECTS = $(am_test_sasl_client_OBJECTS)
+AM_V_P = $(am__v_P_@AM_V@)
+am__v_P_ = $(am__v_P_@AM_DEFAULT_V@)
+am__v_P_0 = false
+am__v_P_1 = :
+AM_V_GEN = $(am__v_GEN_@AM_V@)
+am__v_GEN_ = $(am__v_GEN_@AM_DEFAULT_V@)
+am__v_GEN_0 = @echo " GEN " $@;
+am__v_GEN_1 =
+AM_V_at = $(am__v_at_@AM_V@)
+am__v_at_ = $(am__v_at_@AM_DEFAULT_V@)
+am__v_at_0 = @
+am__v_at_1 =
+DEFAULT_INCLUDES = -I.@am__isrc@ -I$(top_builddir)
+depcomp = $(SHELL) $(top_srcdir)/depcomp
+am__maybe_remake_depfiles = depfiles
+am__depfiles_remade = ./$(DEPDIR)/dsasl-client.Plo \
+ ./$(DEPDIR)/mech-external.Plo ./$(DEPDIR)/mech-login.Plo \
+ ./$(DEPDIR)/mech-oauthbearer.Plo ./$(DEPDIR)/mech-plain.Plo \
+ ./$(DEPDIR)/test-sasl-client.Po
+am__mv = mv -f
+COMPILE = $(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) \
+ $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS)
+LTCOMPILE = $(LIBTOOL) $(AM_V_lt) --tag=CC $(AM_LIBTOOLFLAGS) \
+ $(LIBTOOLFLAGS) --mode=compile $(CC) $(DEFS) \
+ $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) $(CPPFLAGS) \
+ $(AM_CFLAGS) $(CFLAGS)
+AM_V_CC = $(am__v_CC_@AM_V@)
+am__v_CC_ = $(am__v_CC_@AM_DEFAULT_V@)
+am__v_CC_0 = @echo " CC " $@;
+am__v_CC_1 =
+CCLD = $(CC)
+LINK = $(LIBTOOL) $(AM_V_lt) --tag=CC $(AM_LIBTOOLFLAGS) \
+ $(LIBTOOLFLAGS) --mode=link $(CCLD) $(AM_CFLAGS) $(CFLAGS) \
+ $(AM_LDFLAGS) $(LDFLAGS) -o $@
+AM_V_CCLD = $(am__v_CCLD_@AM_V@)
+am__v_CCLD_ = $(am__v_CCLD_@AM_DEFAULT_V@)
+am__v_CCLD_0 = @echo " CCLD " $@;
+am__v_CCLD_1 =
+SOURCES = $(libsasl_la_SOURCES) $(test_sasl_client_SOURCES)
+DIST_SOURCES = $(libsasl_la_SOURCES) $(test_sasl_client_SOURCES)
+am__can_run_installinfo = \
+ case $$AM_UPDATE_INFO_DIR in \
+ n|no|NO) false;; \
+ *) (install-info --version) >/dev/null 2>&1;; \
+ esac
+am__vpath_adj_setup = srcdirstrip=`echo "$(srcdir)" | sed 's|.|.|g'`;
+am__vpath_adj = case $$p in \
+ $(srcdir)/*) f=`echo "$$p" | sed "s|^$$srcdirstrip/||"`;; \
+ *) f=$$p;; \
+ esac;
+am__strip_dir = f=`echo $$p | sed -e 's|^.*/||'`;
+am__install_max = 40
+am__nobase_strip_setup = \
+ srcdirstrip=`echo "$(srcdir)" | sed 's/[].[^$$\\*|]/\\\\&/g'`
+am__nobase_strip = \
+ for p in $$list; do echo "$$p"; done | sed -e "s|$$srcdirstrip/||"
+am__nobase_list = $(am__nobase_strip_setup); \
+ for p in $$list; do echo "$$p $$p"; done | \
+ sed "s| $$srcdirstrip/| |;"' / .*\//!s/ .*/ ./; s,\( .*\)/[^/]*$$,\1,' | \
+ $(AWK) 'BEGIN { files["."] = "" } { files[$$2] = files[$$2] " " $$1; \
+ if (++n[$$2] == $(am__install_max)) \
+ { print $$2, files[$$2]; n[$$2] = 0; files[$$2] = "" } } \
+ END { for (dir in files) print dir, files[dir] }'
+am__base_list = \
+ sed '$$!N;$$!N;$$!N;$$!N;$$!N;$$!N;$$!N;s/\n/ /g' | \
+ sed '$$!N;$$!N;$$!N;$$!N;s/\n/ /g'
+am__uninstall_files_from_dir = { \
+ test -z "$$files" \
+ || { test ! -d "$$dir" && test ! -f "$$dir" && test ! -r "$$dir"; } \
+ || { echo " ( cd '$$dir' && rm -f" $$files ")"; \
+ $(am__cd) "$$dir" && rm -f $$files; }; \
+ }
+am__installdirs = "$(DESTDIR)$(pkginc_libdir)"
+HEADERS = $(pkginc_lib_HEADERS)
+am__tagged_files = $(HEADERS) $(SOURCES) $(TAGS_FILES) $(LISP)
+# Read a list of newline-separated strings from the standard input,
+# and print each of them once, without duplicates. Input order is
+# *not* preserved.
+am__uniquify_input = $(AWK) '\
+ BEGIN { nonempty = 0; } \
+ { items[$$0] = 1; nonempty = 1; } \
+ END { if (nonempty) { for (i in items) print i; }; } \
+'
+# Make sure the list of sources is unique. This is necessary because,
+# e.g., the same source file might be shared among _SOURCES variables
+# for different programs/libraries.
+am__define_uniq_tagged_files = \
+ list='$(am__tagged_files)'; \
+ unique=`for i in $$list; do \
+ if test -f "$$i"; then echo $$i; else echo $(srcdir)/$$i; fi; \
+ done | $(am__uniquify_input)`
+ETAGS = etags
+CTAGS = ctags
+am__DIST_COMMON = $(srcdir)/Makefile.in $(top_srcdir)/depcomp
+DISTFILES = $(DIST_COMMON) $(DIST_SOURCES) $(TEXINFOS) $(EXTRA_DIST)
+ACLOCAL = @ACLOCAL@
+ACLOCAL_AMFLAGS = @ACLOCAL_AMFLAGS@
+AMTAR = @AMTAR@
+AM_DEFAULT_VERBOSITY = @AM_DEFAULT_VERBOSITY@
+APPARMOR_LIBS = @APPARMOR_LIBS@
+AR = @AR@
+AUTH_CFLAGS = @AUTH_CFLAGS@
+AUTH_LIBS = @AUTH_LIBS@
+AUTOCONF = @AUTOCONF@
+AUTOHEADER = @AUTOHEADER@
+AUTOMAKE = @AUTOMAKE@
+AWK = @AWK@
+BINARY_CFLAGS = @BINARY_CFLAGS@
+BINARY_LDFLAGS = @BINARY_LDFLAGS@
+BISON = @BISON@
+CASSANDRA_CFLAGS = @CASSANDRA_CFLAGS@
+CASSANDRA_LIBS = @CASSANDRA_LIBS@
+CC = @CC@
+CCDEPMODE = @CCDEPMODE@
+CDB_LIBS = @CDB_LIBS@
+CFLAGS = @CFLAGS@
+CLUCENE_CFLAGS = @CLUCENE_CFLAGS@
+CLUCENE_LIBS = @CLUCENE_LIBS@
+COMPRESS_LIBS = @COMPRESS_LIBS@
+CPP = @CPP@
+CPPFLAGS = @CPPFLAGS@
+CRYPT_LIBS = @CRYPT_LIBS@
+CXX = @CXX@
+CXXCPP = @CXXCPP@
+CXXDEPMODE = @CXXDEPMODE@
+CXXFLAGS = @CXXFLAGS@
+CYGPATH_W = @CYGPATH_W@
+DEFS = @DEFS@
+DEPDIR = @DEPDIR@
+DICT_LIBS = @DICT_LIBS@
+DLLIB = @DLLIB@
+DLLTOOL = @DLLTOOL@
+DSYMUTIL = @DSYMUTIL@
+DUMPBIN = @DUMPBIN@
+ECHO_C = @ECHO_C@
+ECHO_N = @ECHO_N@
+ECHO_T = @ECHO_T@
+EGREP = @EGREP@
+EXEEXT = @EXEEXT@
+FGREP = @FGREP@
+FLEX = @FLEX@
+FUZZER_CPPFLAGS = @FUZZER_CPPFLAGS@
+FUZZER_LDFLAGS = @FUZZER_LDFLAGS@
+GREP = @GREP@
+INSTALL = @INSTALL@
+INSTALL_DATA = @INSTALL_DATA@
+INSTALL_PROGRAM = @INSTALL_PROGRAM@
+INSTALL_SCRIPT = @INSTALL_SCRIPT@
+INSTALL_STRIP_PROGRAM = @INSTALL_STRIP_PROGRAM@
+KRB5CONFIG = @KRB5CONFIG@
+KRB5_CFLAGS = @KRB5_CFLAGS@
+KRB5_LIBS = @KRB5_LIBS@
+LD = @LD@
+LDAP_LIBS = @LDAP_LIBS@
+LDFLAGS = @LDFLAGS@
+LD_NO_WHOLE_ARCHIVE = @LD_NO_WHOLE_ARCHIVE@
+LD_WHOLE_ARCHIVE = @LD_WHOLE_ARCHIVE@
+LIBCAP = @LIBCAP@
+LIBDOVECOT = @LIBDOVECOT@
+LIBDOVECOT_COMPRESS = @LIBDOVECOT_COMPRESS@
+LIBDOVECOT_DEPS = @LIBDOVECOT_DEPS@
+LIBDOVECOT_DSYNC = @LIBDOVECOT_DSYNC@
+LIBDOVECOT_LA_LIBS = @LIBDOVECOT_LA_LIBS@
+LIBDOVECOT_LDA = @LIBDOVECOT_LDA@
+LIBDOVECOT_LDAP = @LIBDOVECOT_LDAP@
+LIBDOVECOT_LIBFTS = @LIBDOVECOT_LIBFTS@
+LIBDOVECOT_LIBFTS_DEPS = @LIBDOVECOT_LIBFTS_DEPS@
+LIBDOVECOT_LOGIN = @LIBDOVECOT_LOGIN@
+LIBDOVECOT_LUA = @LIBDOVECOT_LUA@
+LIBDOVECOT_LUA_DEPS = @LIBDOVECOT_LUA_DEPS@
+LIBDOVECOT_SQL = @LIBDOVECOT_SQL@
+LIBDOVECOT_STORAGE = @LIBDOVECOT_STORAGE@
+LIBDOVECOT_STORAGE_DEPS = @LIBDOVECOT_STORAGE_DEPS@
+LIBEXTTEXTCAT_CFLAGS = @LIBEXTTEXTCAT_CFLAGS@
+LIBEXTTEXTCAT_LIBS = @LIBEXTTEXTCAT_LIBS@
+LIBICONV = @LIBICONV@
+LIBICU_CFLAGS = @LIBICU_CFLAGS@
+LIBICU_LIBS = @LIBICU_LIBS@
+LIBOBJS = @LIBOBJS@
+LIBS = @LIBS@
+LIBSODIUM_CFLAGS = @LIBSODIUM_CFLAGS@
+LIBSODIUM_LIBS = @LIBSODIUM_LIBS@
+LIBTIRPC_CFLAGS = @LIBTIRPC_CFLAGS@
+LIBTIRPC_LIBS = @LIBTIRPC_LIBS@
+LIBTOOL = @LIBTOOL@
+LIBUNWIND_CFLAGS = @LIBUNWIND_CFLAGS@
+LIBUNWIND_LIBS = @LIBUNWIND_LIBS@
+LIBWRAP_LIBS = @LIBWRAP_LIBS@
+LINKED_STORAGE_LDADD = @LINKED_STORAGE_LDADD@
+LIPO = @LIPO@
+LN_S = @LN_S@
+LTLIBICONV = @LTLIBICONV@
+LTLIBOBJS = @LTLIBOBJS@
+LT_SYS_LIBRARY_PATH = @LT_SYS_LIBRARY_PATH@
+LUA_CFLAGS = @LUA_CFLAGS@
+LUA_LIBS = @LUA_LIBS@
+MAINT = @MAINT@
+MAKEINFO = @MAKEINFO@
+MANIFEST_TOOL = @MANIFEST_TOOL@
+MKDIR_P = @MKDIR_P@
+MODULE_LIBS = @MODULE_LIBS@
+MODULE_SUFFIX = @MODULE_SUFFIX@
+MYSQL_CFLAGS = @MYSQL_CFLAGS@
+MYSQL_CONFIG = @MYSQL_CONFIG@
+MYSQL_LIBS = @MYSQL_LIBS@
+NM = @NM@
+NMEDIT = @NMEDIT@
+NOPLUGIN_LDFLAGS = @NOPLUGIN_LDFLAGS@
+OBJDUMP = @OBJDUMP@
+OBJEXT = @OBJEXT@
+OTOOL = @OTOOL@
+OTOOL64 = @OTOOL64@
+PACKAGE = @PACKAGE@
+PACKAGE_BUGREPORT = @PACKAGE_BUGREPORT@
+PACKAGE_NAME = @PACKAGE_NAME@
+PACKAGE_STRING = @PACKAGE_STRING@
+PACKAGE_TARNAME = @PACKAGE_TARNAME@
+PACKAGE_URL = @PACKAGE_URL@
+PACKAGE_VERSION = @PACKAGE_VERSION@
+PANDOC = @PANDOC@
+PATH_SEPARATOR = @PATH_SEPARATOR@
+PGSQL_CFLAGS = @PGSQL_CFLAGS@
+PGSQL_LIBS = @PGSQL_LIBS@
+PG_CONFIG = @PG_CONFIG@
+PIE_CFLAGS = @PIE_CFLAGS@
+PIE_LDFLAGS = @PIE_LDFLAGS@
+PKG_CONFIG = @PKG_CONFIG@
+PKG_CONFIG_LIBDIR = @PKG_CONFIG_LIBDIR@
+PKG_CONFIG_PATH = @PKG_CONFIG_PATH@
+QUOTA_LIBS = @QUOTA_LIBS@
+RANLIB = @RANLIB@
+RELRO_LDFLAGS = @RELRO_LDFLAGS@
+RPCGEN = @RPCGEN@
+RUN_TEST = @RUN_TEST@
+SED = @SED@
+SETTING_FILES = @SETTING_FILES@
+SET_MAKE = @SET_MAKE@
+SHELL = @SHELL@
+SQLITE_CFLAGS = @SQLITE_CFLAGS@
+SQLITE_LIBS = @SQLITE_LIBS@
+SQL_CFLAGS = @SQL_CFLAGS@
+SQL_LIBS = @SQL_LIBS@
+SSL_CFLAGS = @SSL_CFLAGS@
+SSL_LIBS = @SSL_LIBS@
+STRIP = @STRIP@
+SYSTEMD_CFLAGS = @SYSTEMD_CFLAGS@
+SYSTEMD_LIBS = @SYSTEMD_LIBS@
+VALGRIND = @VALGRIND@
+VERSION = @VERSION@
+ZSTD_CFLAGS = @ZSTD_CFLAGS@
+ZSTD_LIBS = @ZSTD_LIBS@
+abs_builddir = @abs_builddir@
+abs_srcdir = @abs_srcdir@
+abs_top_builddir = @abs_top_builddir@
+abs_top_srcdir = @abs_top_srcdir@
+ac_ct_AR = @ac_ct_AR@
+ac_ct_CC = @ac_ct_CC@
+ac_ct_CXX = @ac_ct_CXX@
+ac_ct_DUMPBIN = @ac_ct_DUMPBIN@
+am__include = @am__include@
+am__leading_dot = @am__leading_dot@
+am__quote = @am__quote@
+am__tar = @am__tar@
+am__untar = @am__untar@
+bindir = @bindir@
+build = @build@
+build_alias = @build_alias@
+build_cpu = @build_cpu@
+build_os = @build_os@
+build_vendor = @build_vendor@
+builddir = @builddir@
+datadir = @datadir@
+datarootdir = @datarootdir@
+dict_drivers = @dict_drivers@
+docdir = @docdir@
+dvidir = @dvidir@
+exec_prefix = @exec_prefix@
+host = @host@
+host_alias = @host_alias@
+host_cpu = @host_cpu@
+host_os = @host_os@
+host_vendor = @host_vendor@
+htmldir = @htmldir@
+includedir = @includedir@
+infodir = @infodir@
+install_sh = @install_sh@
+libdir = @libdir@
+libexecdir = @libexecdir@
+localedir = @localedir@
+localstatedir = @localstatedir@
+mandir = @mandir@
+mkdir_p = @mkdir_p@
+moduledir = @moduledir@
+oldincludedir = @oldincludedir@
+pdfdir = @pdfdir@
+prefix = @prefix@
+program_transform_name = @program_transform_name@
+psdir = @psdir@
+rundir = @rundir@
+runstatedir = @runstatedir@
+sbindir = @sbindir@
+sharedstatedir = @sharedstatedir@
+sql_drivers = @sql_drivers@
+srcdir = @srcdir@
+ssldir = @ssldir@
+statedir = @statedir@
+sysconfdir = @sysconfdir@
+systemdservicetype = @systemdservicetype@
+systemdsystemunitdir = @systemdsystemunitdir@
+target_alias = @target_alias@
+top_build_prefix = @top_build_prefix@
+top_builddir = @top_builddir@
+top_srcdir = @top_srcdir@
+noinst_LTLIBRARIES = libsasl.la
+AM_CPPFLAGS = \
+ -I$(top_srcdir)/src/lib \
+ -I$(top_srcdir)/src/lib-test
+
+libsasl_la_SOURCES = \
+ mech-external.c \
+ mech-login.c \
+ mech-plain.c \
+ mech-oauthbearer.c \
+ dsasl-client.c
+
+headers = \
+ dsasl-client.h \
+ dsasl-client-private.h
+
+pkginc_libdir = $(pkgincludedir)
+pkginc_lib_HEADERS = $(headers)
+test_programs = \
+ test-sasl-client
+
+test_libs = \
+ $(noinst_LTLIBRARIES) \
+ ../lib-test/libtest.la \
+ ../lib/liblib.la
+
+test_deps = $(test_libs)
+test_sasl_client_SOURCES = test-sasl-client.c
+test_sasl_client_LDADD = $(test_libs)
+test_sasl_client_DEPENDENCIES = $(test_deps)
+all: all-am
+
+.SUFFIXES:
+.SUFFIXES: .c .lo .o .obj
+$(srcdir)/Makefile.in: @MAINTAINER_MODE_TRUE@ $(srcdir)/Makefile.am $(am__configure_deps)
+ @for dep in $?; do \
+ case '$(am__configure_deps)' in \
+ *$$dep*) \
+ ( cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh ) \
+ && { if test -f $@; then exit 0; else break; fi; }; \
+ exit 1;; \
+ esac; \
+ done; \
+ echo ' cd $(top_srcdir) && $(AUTOMAKE) --foreign src/lib-sasl/Makefile'; \
+ $(am__cd) $(top_srcdir) && \
+ $(AUTOMAKE) --foreign src/lib-sasl/Makefile
+Makefile: $(srcdir)/Makefile.in $(top_builddir)/config.status
+ @case '$?' in \
+ *config.status*) \
+ cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh;; \
+ *) \
+ echo ' cd $(top_builddir) && $(SHELL) ./config.status $(subdir)/$@ $(am__maybe_remake_depfiles)'; \
+ cd $(top_builddir) && $(SHELL) ./config.status $(subdir)/$@ $(am__maybe_remake_depfiles);; \
+ esac;
+
+$(top_builddir)/config.status: $(top_srcdir)/configure $(CONFIG_STATUS_DEPENDENCIES)
+ cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh
+
+$(top_srcdir)/configure: @MAINTAINER_MODE_TRUE@ $(am__configure_deps)
+ cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh
+$(ACLOCAL_M4): @MAINTAINER_MODE_TRUE@ $(am__aclocal_m4_deps)
+ cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh
+$(am__aclocal_m4_deps):
+
+clean-noinstPROGRAMS:
+ @list='$(noinst_PROGRAMS)'; test -n "$$list" || exit 0; \
+ echo " rm -f" $$list; \
+ rm -f $$list || exit $$?; \
+ test -n "$(EXEEXT)" || exit 0; \
+ list=`for p in $$list; do echo "$$p"; done | sed 's/$(EXEEXT)$$//'`; \
+ echo " rm -f" $$list; \
+ rm -f $$list
+
+clean-noinstLTLIBRARIES:
+ -test -z "$(noinst_LTLIBRARIES)" || rm -f $(noinst_LTLIBRARIES)
+ @list='$(noinst_LTLIBRARIES)'; \
+ locs=`for p in $$list; do echo $$p; done | \
+ sed 's|^[^/]*$$|.|; s|/[^/]*$$||; s|$$|/so_locations|' | \
+ sort -u`; \
+ test -z "$$locs" || { \
+ echo rm -f $${locs}; \
+ rm -f $${locs}; \
+ }
+
+libsasl.la: $(libsasl_la_OBJECTS) $(libsasl_la_DEPENDENCIES) $(EXTRA_libsasl_la_DEPENDENCIES)
+ $(AM_V_CCLD)$(LINK) $(libsasl_la_OBJECTS) $(libsasl_la_LIBADD) $(LIBS)
+
+test-sasl-client$(EXEEXT): $(test_sasl_client_OBJECTS) $(test_sasl_client_DEPENDENCIES) $(EXTRA_test_sasl_client_DEPENDENCIES)
+ @rm -f test-sasl-client$(EXEEXT)
+ $(AM_V_CCLD)$(LINK) $(test_sasl_client_OBJECTS) $(test_sasl_client_LDADD) $(LIBS)
+
+mostlyclean-compile:
+ -rm -f *.$(OBJEXT)
+
+distclean-compile:
+ -rm -f *.tab.c
+
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/dsasl-client.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/mech-external.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/mech-login.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/mech-oauthbearer.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/mech-plain.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/test-sasl-client.Po@am__quote@ # am--include-marker
+
+$(am__depfiles_remade):
+ @$(MKDIR_P) $(@D)
+ @echo '# dummy' >$@-t && $(am__mv) $@-t $@
+
+am--depfiles: $(am__depfiles_remade)
+
+.c.o:
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(COMPILE) -MT $@ -MD -MP -MF $(DEPDIR)/$*.Tpo -c -o $@ $<
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/$*.Tpo $(DEPDIR)/$*.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='$<' object='$@' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(COMPILE) -c -o $@ $<
+
+.c.obj:
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(COMPILE) -MT $@ -MD -MP -MF $(DEPDIR)/$*.Tpo -c -o $@ `$(CYGPATH_W) '$<'`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/$*.Tpo $(DEPDIR)/$*.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='$<' object='$@' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(COMPILE) -c -o $@ `$(CYGPATH_W) '$<'`
+
+.c.lo:
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(LTCOMPILE) -MT $@ -MD -MP -MF $(DEPDIR)/$*.Tpo -c -o $@ $<
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/$*.Tpo $(DEPDIR)/$*.Plo
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='$<' object='$@' libtool=yes @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(LTCOMPILE) -c -o $@ $<
+
+mostlyclean-libtool:
+ -rm -f *.lo
+
+clean-libtool:
+ -rm -rf .libs _libs
+install-pkginc_libHEADERS: $(pkginc_lib_HEADERS)
+ @$(NORMAL_INSTALL)
+ @list='$(pkginc_lib_HEADERS)'; test -n "$(pkginc_libdir)" || list=; \
+ if test -n "$$list"; then \
+ echo " $(MKDIR_P) '$(DESTDIR)$(pkginc_libdir)'"; \
+ $(MKDIR_P) "$(DESTDIR)$(pkginc_libdir)" || exit 1; \
+ fi; \
+ for p in $$list; do \
+ if test -f "$$p"; then d=; else d="$(srcdir)/"; fi; \
+ echo "$$d$$p"; \
+ done | $(am__base_list) | \
+ while read files; do \
+ echo " $(INSTALL_HEADER) $$files '$(DESTDIR)$(pkginc_libdir)'"; \
+ $(INSTALL_HEADER) $$files "$(DESTDIR)$(pkginc_libdir)" || exit $$?; \
+ done
+
+uninstall-pkginc_libHEADERS:
+ @$(NORMAL_UNINSTALL)
+ @list='$(pkginc_lib_HEADERS)'; test -n "$(pkginc_libdir)" || list=; \
+ files=`for p in $$list; do echo $$p; done | sed -e 's|^.*/||'`; \
+ dir='$(DESTDIR)$(pkginc_libdir)'; $(am__uninstall_files_from_dir)
+
+ID: $(am__tagged_files)
+ $(am__define_uniq_tagged_files); mkid -fID $$unique
+tags: tags-am
+TAGS: tags
+
+tags-am: $(TAGS_DEPENDENCIES) $(am__tagged_files)
+ set x; \
+ here=`pwd`; \
+ $(am__define_uniq_tagged_files); \
+ shift; \
+ if test -z "$(ETAGS_ARGS)$$*$$unique"; then :; else \
+ test -n "$$unique" || unique=$$empty_fix; \
+ if test $$# -gt 0; then \
+ $(ETAGS) $(ETAGSFLAGS) $(AM_ETAGSFLAGS) $(ETAGS_ARGS) \
+ "$$@" $$unique; \
+ else \
+ $(ETAGS) $(ETAGSFLAGS) $(AM_ETAGSFLAGS) $(ETAGS_ARGS) \
+ $$unique; \
+ fi; \
+ fi
+ctags: ctags-am
+
+CTAGS: ctags
+ctags-am: $(TAGS_DEPENDENCIES) $(am__tagged_files)
+ $(am__define_uniq_tagged_files); \
+ test -z "$(CTAGS_ARGS)$$unique" \
+ || $(CTAGS) $(CTAGSFLAGS) $(AM_CTAGSFLAGS) $(CTAGS_ARGS) \
+ $$unique
+
+GTAGS:
+ here=`$(am__cd) $(top_builddir) && pwd` \
+ && $(am__cd) $(top_srcdir) \
+ && gtags -i $(GTAGS_ARGS) "$$here"
+cscopelist: cscopelist-am
+
+cscopelist-am: $(am__tagged_files)
+ list='$(am__tagged_files)'; \
+ case "$(srcdir)" in \
+ [\\/]* | ?:[\\/]*) sdir="$(srcdir)" ;; \
+ *) sdir=$(subdir)/$(srcdir) ;; \
+ esac; \
+ for i in $$list; do \
+ if test -f "$$i"; then \
+ echo "$(subdir)/$$i"; \
+ else \
+ echo "$$sdir/$$i"; \
+ fi; \
+ done >> $(top_builddir)/cscope.files
+
+distclean-tags:
+ -rm -f TAGS ID GTAGS GRTAGS GSYMS GPATH tags
+
+distdir: $(BUILT_SOURCES)
+ $(MAKE) $(AM_MAKEFLAGS) distdir-am
+
+distdir-am: $(DISTFILES)
+ @srcdirstrip=`echo "$(srcdir)" | sed 's/[].[^$$\\*]/\\\\&/g'`; \
+ topsrcdirstrip=`echo "$(top_srcdir)" | sed 's/[].[^$$\\*]/\\\\&/g'`; \
+ list='$(DISTFILES)'; \
+ dist_files=`for file in $$list; do echo $$file; done | \
+ sed -e "s|^$$srcdirstrip/||;t" \
+ -e "s|^$$topsrcdirstrip/|$(top_builddir)/|;t"`; \
+ case $$dist_files in \
+ */*) $(MKDIR_P) `echo "$$dist_files" | \
+ sed '/\//!d;s|^|$(distdir)/|;s,/[^/]*$$,,' | \
+ sort -u` ;; \
+ esac; \
+ for file in $$dist_files; do \
+ if test -f $$file || test -d $$file; then d=.; else d=$(srcdir); fi; \
+ if test -d $$d/$$file; then \
+ dir=`echo "/$$file" | sed -e 's,/[^/]*$$,,'`; \
+ if test -d "$(distdir)/$$file"; then \
+ find "$(distdir)/$$file" -type d ! -perm -700 -exec chmod u+rwx {} \;; \
+ fi; \
+ if test -d $(srcdir)/$$file && test $$d != $(srcdir); then \
+ cp -fpR $(srcdir)/$$file "$(distdir)$$dir" || exit 1; \
+ find "$(distdir)/$$file" -type d ! -perm -700 -exec chmod u+rwx {} \;; \
+ fi; \
+ cp -fpR $$d/$$file "$(distdir)$$dir" || exit 1; \
+ else \
+ test -f "$(distdir)/$$file" \
+ || cp -p $$d/$$file "$(distdir)/$$file" \
+ || exit 1; \
+ fi; \
+ done
+check-am: all-am
+ $(MAKE) $(AM_MAKEFLAGS) check-local
+check: check-am
+all-am: Makefile $(PROGRAMS) $(LTLIBRARIES) $(HEADERS)
+installdirs:
+ for dir in "$(DESTDIR)$(pkginc_libdir)"; do \
+ test -z "$$dir" || $(MKDIR_P) "$$dir"; \
+ done
+install: install-am
+install-exec: install-exec-am
+install-data: install-data-am
+uninstall: uninstall-am
+
+install-am: all-am
+ @$(MAKE) $(AM_MAKEFLAGS) install-exec-am install-data-am
+
+installcheck: installcheck-am
+install-strip:
+ if test -z '$(STRIP)'; then \
+ $(MAKE) $(AM_MAKEFLAGS) INSTALL_PROGRAM="$(INSTALL_STRIP_PROGRAM)" \
+ install_sh_PROGRAM="$(INSTALL_STRIP_PROGRAM)" INSTALL_STRIP_FLAG=-s \
+ install; \
+ else \
+ $(MAKE) $(AM_MAKEFLAGS) INSTALL_PROGRAM="$(INSTALL_STRIP_PROGRAM)" \
+ install_sh_PROGRAM="$(INSTALL_STRIP_PROGRAM)" INSTALL_STRIP_FLAG=-s \
+ "INSTALL_PROGRAM_ENV=STRIPPROG='$(STRIP)'" install; \
+ fi
+mostlyclean-generic:
+
+clean-generic:
+
+distclean-generic:
+ -test -z "$(CONFIG_CLEAN_FILES)" || rm -f $(CONFIG_CLEAN_FILES)
+ -test . = "$(srcdir)" || test -z "$(CONFIG_CLEAN_VPATH_FILES)" || rm -f $(CONFIG_CLEAN_VPATH_FILES)
+
+maintainer-clean-generic:
+ @echo "This command is intended for maintainers to use"
+ @echo "it deletes files that may require special tools to rebuild."
+clean: clean-am
+
+clean-am: clean-generic clean-libtool clean-noinstLTLIBRARIES \
+ clean-noinstPROGRAMS mostlyclean-am
+
+distclean: distclean-am
+ -rm -f ./$(DEPDIR)/dsasl-client.Plo
+ -rm -f ./$(DEPDIR)/mech-external.Plo
+ -rm -f ./$(DEPDIR)/mech-login.Plo
+ -rm -f ./$(DEPDIR)/mech-oauthbearer.Plo
+ -rm -f ./$(DEPDIR)/mech-plain.Plo
+ -rm -f ./$(DEPDIR)/test-sasl-client.Po
+ -rm -f Makefile
+distclean-am: clean-am distclean-compile distclean-generic \
+ distclean-tags
+
+dvi: dvi-am
+
+dvi-am:
+
+html: html-am
+
+html-am:
+
+info: info-am
+
+info-am:
+
+install-data-am: install-pkginc_libHEADERS
+
+install-dvi: install-dvi-am
+
+install-dvi-am:
+
+install-exec-am:
+
+install-html: install-html-am
+
+install-html-am:
+
+install-info: install-info-am
+
+install-info-am:
+
+install-man:
+
+install-pdf: install-pdf-am
+
+install-pdf-am:
+
+install-ps: install-ps-am
+
+install-ps-am:
+
+installcheck-am:
+
+maintainer-clean: maintainer-clean-am
+ -rm -f ./$(DEPDIR)/dsasl-client.Plo
+ -rm -f ./$(DEPDIR)/mech-external.Plo
+ -rm -f ./$(DEPDIR)/mech-login.Plo
+ -rm -f ./$(DEPDIR)/mech-oauthbearer.Plo
+ -rm -f ./$(DEPDIR)/mech-plain.Plo
+ -rm -f ./$(DEPDIR)/test-sasl-client.Po
+ -rm -f Makefile
+maintainer-clean-am: distclean-am maintainer-clean-generic
+
+mostlyclean: mostlyclean-am
+
+mostlyclean-am: mostlyclean-compile mostlyclean-generic \
+ mostlyclean-libtool
+
+pdf: pdf-am
+
+pdf-am:
+
+ps: ps-am
+
+ps-am:
+
+uninstall-am: uninstall-pkginc_libHEADERS
+
+.MAKE: check-am install-am install-strip
+
+.PHONY: CTAGS GTAGS TAGS all all-am am--depfiles check check-am \
+ check-local clean clean-generic clean-libtool \
+ clean-noinstLTLIBRARIES clean-noinstPROGRAMS cscopelist-am \
+ ctags ctags-am distclean distclean-compile distclean-generic \
+ distclean-libtool distclean-tags distdir dvi dvi-am html \
+ html-am info info-am install install-am install-data \
+ install-data-am install-dvi install-dvi-am install-exec \
+ install-exec-am install-html install-html-am install-info \
+ install-info-am install-man install-pdf install-pdf-am \
+ install-pkginc_libHEADERS install-ps install-ps-am \
+ install-strip installcheck installcheck-am installdirs \
+ maintainer-clean maintainer-clean-generic mostlyclean \
+ mostlyclean-compile mostlyclean-generic mostlyclean-libtool \
+ pdf pdf-am ps ps-am tags tags-am uninstall uninstall-am \
+ uninstall-pkginc_libHEADERS
+
+.PRECIOUS: Makefile
+
+
+check-local:
+ for bin in $(test_programs); do \
+ if ! $(RUN_TEST) ./$$bin; then exit 1; fi; \
+ done
+
+# Tell versions [3.59,3.63) of GNU make to not export all variables.
+# Otherwise a system limit (for SysV at least) may be exceeded.
+.NOEXPORT:
diff --git a/src/lib-sasl/dsasl-client-private.h b/src/lib-sasl/dsasl-client-private.h
new file mode 100644
index 0000000..65b86f7
--- /dev/null
+++ b/src/lib-sasl/dsasl-client-private.h
@@ -0,0 +1,45 @@
+#ifndef DSASL_CLIENT_PRIVATE_H
+#define DSASL_CLIENT_PRIVATE_H
+
+#include "dsasl-client.h"
+
+enum dsasl_mech_security_flags {
+ DSASL_MECH_SEC_ALLOW_NULS = 0x0001,
+};
+
+struct dsasl_client {
+ pool_t pool;
+ struct dsasl_client_settings set;
+ char *password;
+ const struct dsasl_client_mech *mech;
+};
+
+struct dsasl_client_mech {
+ const char *name;
+ size_t struct_size;
+ enum dsasl_mech_security_flags flags;
+
+ int (*input)(struct dsasl_client *client,
+ const unsigned char *input, size_t input_len,
+ const char **error_r);
+ int (*output)(struct dsasl_client *client,
+ const unsigned char **output_r, size_t *output_len_r,
+ const char **error_r);
+ int (*set_parameter)(struct dsasl_client *client,
+ const char *key, const char *value,
+ const char **error_r);
+ int (*get_result)(struct dsasl_client *client,
+ const char *key, const char **value_r,
+ const char **error_r);
+ void (*free)(struct dsasl_client *client);
+};
+
+extern const struct dsasl_client_mech dsasl_client_mech_external;
+extern const struct dsasl_client_mech dsasl_client_mech_login;
+extern const struct dsasl_client_mech dsasl_client_mech_oauthbearer;
+extern const struct dsasl_client_mech dsasl_client_mech_xoauth2;
+
+void dsasl_client_mech_register(const struct dsasl_client_mech *mech);
+void dsasl_client_mech_unregister(const struct dsasl_client_mech *mech);
+
+#endif
diff --git a/src/lib-sasl/dsasl-client.c b/src/lib-sasl/dsasl-client.c
new file mode 100644
index 0000000..bf11d5c
--- /dev/null
+++ b/src/lib-sasl/dsasl-client.c
@@ -0,0 +1,152 @@
+/* Copyright (c) 2013-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "safe-memset.h"
+#include "dsasl-client-private.h"
+
+static int init_refcount = 0;
+static ARRAY(const struct dsasl_client_mech *) dsasl_mechanisms = ARRAY_INIT;
+
+static const struct dsasl_client_mech *
+dsasl_client_mech_find_idx(const char *name, unsigned int *idx_r)
+{
+ const struct dsasl_client_mech *const *mechp;
+
+ array_foreach(&dsasl_mechanisms, mechp) {
+ if (strcasecmp((*mechp)->name, name) == 0) {
+ *idx_r = array_foreach_idx(&dsasl_mechanisms, mechp);
+ return *mechp;
+ }
+ }
+ return NULL;
+}
+
+const struct dsasl_client_mech *dsasl_client_mech_find(const char *name)
+{
+ unsigned int idx;
+
+ return dsasl_client_mech_find_idx(name, &idx);
+}
+
+const char *dsasl_client_mech_get_name(const struct dsasl_client_mech *mech)
+{
+ return mech->name;
+}
+
+void dsasl_client_mech_register(const struct dsasl_client_mech *mech)
+{
+ unsigned int idx;
+
+ if (dsasl_client_mech_find_idx(mech->name, &idx) != NULL) {
+ /* allow plugins to override the default mechanisms */
+ array_delete(&dsasl_mechanisms, idx, 1);
+ }
+ array_push_back(&dsasl_mechanisms, &mech);
+}
+
+void dsasl_client_mech_unregister(const struct dsasl_client_mech *mech)
+{
+ unsigned int idx;
+
+ if (dsasl_client_mech_find_idx(mech->name, &idx) == NULL)
+ i_panic("SASL mechanism not registered: %s", mech->name);
+ array_delete(&dsasl_mechanisms, idx, 1);
+}
+
+struct dsasl_client *dsasl_client_new(const struct dsasl_client_mech *mech,
+ const struct dsasl_client_settings *set)
+{
+ struct dsasl_client *client;
+ pool_t pool = pool_alloconly_create("sasl client", 512);
+
+ client = p_malloc(pool, mech->struct_size);
+ client->pool = pool;
+ client->mech = mech;
+ client->set.authid = p_strdup(pool, set->authid);
+ client->set.authzid = p_strdup(pool, set->authzid);
+ client->password = p_strdup(pool, set->password);
+ client->set.password = client->password;
+ return client;
+}
+
+void dsasl_client_free(struct dsasl_client **_client)
+{
+ struct dsasl_client *client = *_client;
+
+ if (client == NULL)
+ return;
+ *_client = NULL;
+
+ if (client->mech->free != NULL)
+ client->mech->free(client);
+ if (client->password != NULL)
+ safe_memset(client->password, 0, strlen(client->password));
+ pool_unref(&client->pool);
+}
+
+int dsasl_client_input(struct dsasl_client *client,
+ const unsigned char *input, size_t input_len,
+ const char **error_r)
+{
+ if ((client->mech->flags & DSASL_MECH_SEC_ALLOW_NULS) == 0 &&
+ memchr(input, '\0', input_len) != NULL) {
+ *error_r = "Unexpected NUL in input data";
+ return -1;
+ }
+ return client->mech->input(client, input, input_len, error_r);
+}
+
+int dsasl_client_output(struct dsasl_client *client,
+ const unsigned char **output_r, size_t *output_len_r,
+ const char **error_r)
+{
+ return client->mech->output(client, output_r, output_len_r, error_r);
+}
+
+int dsasl_client_set_parameter(struct dsasl_client *client,
+ const char *param, const char *value,
+ const char **error_r)
+{
+ if (client->mech->set_parameter != NULL) {
+ int ret = client->mech->set_parameter(client, param,
+ value, error_r);
+ i_assert(ret >= 0 || *error_r != NULL);
+ return ret;
+ } else
+ return 0;
+}
+
+int dsasl_client_get_result(struct dsasl_client *client,
+ const char *key, const char **value_r,
+ const char **error_r)
+{
+ if (client->mech->get_result != NULL) {
+ int ret =
+ client->mech->get_result(client, key, value_r, error_r);
+ i_assert(ret <= 0 || *value_r != NULL);
+ i_assert(ret >= 0 || *error_r != NULL);
+ return ret;
+ } else
+ return 0;
+}
+
+void dsasl_clients_init(void)
+{
+ if (init_refcount++ > 0)
+ return;
+
+ i_array_init(&dsasl_mechanisms, 8);
+ dsasl_client_mech_register(&dsasl_client_mech_external);
+ dsasl_client_mech_register(&dsasl_client_mech_plain);
+ dsasl_client_mech_register(&dsasl_client_mech_login);
+ dsasl_client_mech_register(&dsasl_client_mech_oauthbearer);
+ dsasl_client_mech_register(&dsasl_client_mech_xoauth2);
+}
+
+void dsasl_clients_deinit(void)
+{
+ if (--init_refcount > 0)
+ return;
+ array_free(&dsasl_mechanisms);
+}
diff --git a/src/lib-sasl/dsasl-client.h b/src/lib-sasl/dsasl-client.h
new file mode 100644
index 0000000..999008d
--- /dev/null
+++ b/src/lib-sasl/dsasl-client.h
@@ -0,0 +1,50 @@
+#ifndef DSASL_CLIENT_H
+#define DSASL_CLIENT_H
+
+struct dsasl_client_settings {
+ /* authentication ID - must be set with most mechanisms */
+ const char *authid;
+ /* authorization ID (who to log in as, if authentication ID is a
+ master user) */
+ const char *authzid;
+ /* password - must be set with most mechanisms */
+ const char *password;
+};
+
+/* PLAIN mechanism always exists and can be accessed directly via this. */
+extern const struct dsasl_client_mech dsasl_client_mech_plain;
+
+const struct dsasl_client_mech *dsasl_client_mech_find(const char *name);
+const char *dsasl_client_mech_get_name(const struct dsasl_client_mech *mech);
+
+struct dsasl_client *dsasl_client_new(const struct dsasl_client_mech *mech,
+ const struct dsasl_client_settings *set);
+void dsasl_client_free(struct dsasl_client **client);
+
+/* Call for server input. */
+int dsasl_client_input(struct dsasl_client *client,
+ const unsigned char *input, size_t input_len,
+ const char **error_r);
+/* Call for getting server output. Also used to get the initial SASL response
+ if supported by the protocol. */
+int dsasl_client_output(struct dsasl_client *client,
+ const unsigned char **output_r, size_t *output_len_r,
+ const char **error_r);
+
+/* Call for setting extra parameters for authentication, these are mechanism
+ dependent. -1 = error, 0 = not found, 1 = ok
+ value can be NULL. */
+int dsasl_client_set_parameter(struct dsasl_client *client,
+ const char *param, const char *value,
+ const char **error_r) ATTR_NULL(3);
+
+/* Call for getting extra result information.
+ -1 = error, 0 = not found, 1 = ok */
+int dsasl_client_get_result(struct dsasl_client *client,
+ const char *key, const char **value_r,
+ const char **error_r);
+
+void dsasl_clients_init(void);
+void dsasl_clients_deinit(void);
+
+#endif
diff --git a/src/lib-sasl/mech-external.c b/src/lib-sasl/mech-external.c
new file mode 100644
index 0000000..7df94b4
--- /dev/null
+++ b/src/lib-sasl/mech-external.c
@@ -0,0 +1,59 @@
+/* Copyright (c) 2016-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "dsasl-client-private.h"
+
+struct external_dsasl_client {
+ struct dsasl_client client;
+ bool output_sent;
+};
+
+static int
+mech_external_input(struct dsasl_client *_client,
+ const unsigned char *input ATTR_UNUSED, size_t input_len,
+ const char **error_r)
+{
+ struct external_dsasl_client *client =
+ (struct external_dsasl_client *)_client;
+
+ if (!client->output_sent) {
+ if (input_len > 0) {
+ *error_r = "Server sent non-empty initial response";
+ return -1;
+ }
+ } else {
+ *error_r = "Server didn't finish authentication";
+ return -1;
+ }
+ return 0;
+}
+
+static int
+mech_external_output(struct dsasl_client *_client,
+ const unsigned char **output_r, size_t *output_len_r,
+ const char **error_r ATTR_UNUSED)
+{
+ struct external_dsasl_client *client =
+ (struct external_dsasl_client *)_client;
+ const char *username;
+
+ if (_client->set.authzid != NULL)
+ username = _client->set.authzid;
+ else if (_client->set.authid != NULL)
+ username = _client->set.authid;
+ else
+ username = "";
+
+ *output_r = (const void *)username;
+ *output_len_r = strlen(username);
+ client->output_sent = TRUE;
+ return 0;
+}
+
+const struct dsasl_client_mech dsasl_client_mech_external = {
+ .name = "EXTERNAL",
+ .struct_size = sizeof(struct external_dsasl_client),
+
+ .input = mech_external_input,
+ .output = mech_external_output
+};
diff --git a/src/lib-sasl/mech-login.c b/src/lib-sasl/mech-login.c
new file mode 100644
index 0000000..83714f1
--- /dev/null
+++ b/src/lib-sasl/mech-login.c
@@ -0,0 +1,75 @@
+/* Copyright (c) 2013-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "str.h"
+#include "dsasl-client-private.h"
+
+enum login_state {
+ STATE_INIT = 0,
+ STATE_USER,
+ STATE_PASS
+};
+
+struct login_dsasl_client {
+ struct dsasl_client client;
+ enum login_state state;
+};
+
+static int
+mech_login_input(struct dsasl_client *_client,
+ const unsigned char *input ATTR_UNUSED,
+ size_t input_len ATTR_UNUSED,
+ const char **error_r)
+{
+ struct login_dsasl_client *client =
+ (struct login_dsasl_client *)_client;
+
+ if (client->state == STATE_PASS) {
+ *error_r = "Server didn't finish authentication";
+ return -1;
+ }
+ client->state++;
+ return 0;
+}
+
+static int
+mech_login_output(struct dsasl_client *_client,
+ const unsigned char **output_r, size_t *output_len_r,
+ const char **error_r)
+{
+ struct login_dsasl_client *client =
+ (struct login_dsasl_client *)_client;
+
+ if (_client->set.authid == NULL) {
+ *error_r = "authid not set";
+ return -1;
+ }
+ if (_client->password == NULL) {
+ *error_r = "password not set";
+ return -1;
+ }
+
+ switch (client->state) {
+ case STATE_INIT:
+ *output_r = uchar_empty_ptr;
+ *output_len_r = 0;
+ return 0;
+ case STATE_USER:
+ *output_r = (const unsigned char *)_client->set.authid;
+ *output_len_r = strlen(_client->set.authid);
+ return 0;
+ case STATE_PASS:
+ *output_r = (const unsigned char *)_client->set.password;
+ *output_len_r = strlen(_client->set.password);
+ return 0;
+ }
+ i_unreached();
+}
+
+const struct dsasl_client_mech dsasl_client_mech_login = {
+ .name = "LOGIN",
+ .struct_size = sizeof(struct login_dsasl_client),
+
+ .input = mech_login_input,
+ .output = mech_login_output
+};
diff --git a/src/lib-sasl/mech-oauthbearer.c b/src/lib-sasl/mech-oauthbearer.c
new file mode 100644
index 0000000..62ec6bc
--- /dev/null
+++ b/src/lib-sasl/mech-oauthbearer.c
@@ -0,0 +1,201 @@
+/* Copyright (c) 2017-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "str.h"
+#include "net.h"
+#include "json-parser.h"
+#include "istream.h"
+#include "dsasl-client-private.h"
+
+struct oauthbearer_dsasl_client {
+ struct dsasl_client client;
+ const char *host;
+ const char *status;
+ in_port_t port;
+ bool output_sent;
+};
+
+static int
+mech_oauthbearer_input(struct dsasl_client *_client,
+ const unsigned char *input, size_t input_len,
+ const char **error_r)
+{
+ struct oauthbearer_dsasl_client *client =
+ (struct oauthbearer_dsasl_client *)_client;
+
+ if (!client->output_sent) {
+ if (input_len > 0) {
+ *error_r = "Server sent non-empty initial response";
+ return -1;
+ }
+ } else {
+ client->status = "";
+ /* if response is empty, authentication has *SUCCEEDED* */
+ if (input_len == 0)
+ return 0;
+
+ /* authentication has failed, try parse status.
+ we are only interested in extracting status if possible
+ so we don't really need to much error handling. */
+ struct istream *is = i_stream_create_from_data(input, input_len);
+ const char *status = NULL, *value;
+ const char *error = NULL;
+ enum json_type jtype;
+ bool found_status = FALSE;
+ struct json_parser *parser = json_parser_init(is);
+ while (json_parse_next(parser, &jtype, &value)>0) {
+ if (found_status && status == NULL) {
+ if (jtype == JSON_TYPE_STRING ||
+ jtype == JSON_TYPE_NUMBER)
+ status = t_strdup(value);
+ break;
+ } else if (jtype == JSON_TYPE_OBJECT_KEY &&
+ strcmp(value, "status") == 0) {
+ found_status = TRUE;
+ } else json_parse_skip_next(parser);
+ }
+
+ /* deinitialize json parser */
+ int ret = json_parser_deinit(&parser, &error);
+ i_stream_unref(&is);
+
+ if (status != NULL)
+ client->status = p_strdup(_client->pool, status);
+ else {
+ ret = -1;
+ if (error == NULL)
+ error = "Status value missing";
+ }
+ if (ret < 0)
+ *error_r = t_strdup_printf("Error parsing JSON reply: %s",
+ error);
+ else
+ *error_r = t_strdup_printf("Failed to authenticate: %s",
+ client->status);
+ return -1;
+ }
+ return 0;
+}
+
+static int
+mech_oauthbearer_output(struct dsasl_client *_client,
+ const unsigned char **output_r, size_t *output_len_r,
+ const char **error_r)
+{
+ struct oauthbearer_dsasl_client *client =
+ (struct oauthbearer_dsasl_client *)_client;
+ string_t *str;
+
+ if (_client->set.authid == NULL) {
+ *error_r = "authid not set";
+ return -1;
+ }
+ if (_client->password == NULL) {
+ *error_r = "password not set";
+ return -1;
+ }
+
+ str = str_new(_client->pool, 64);
+
+ str_printfa(str, "n,a=%s,\x01", _client->set.authid);
+ if (client->host != NULL && *client->host != '\0')
+ str_printfa(str, "host=%s\x01", client->host);
+ if (client->port > 0)
+ str_printfa(str, "port=%u\x01", client->port);
+ str_printfa(str, "auth=Bearer %s\x01", _client->password);
+ str_append_c(str, '\x01');
+
+ *output_r = str_data(str);
+ *output_len_r = str_len(str);
+ client->output_sent = TRUE;
+ return 0;
+}
+
+static int
+mech_xoauth2_output(struct dsasl_client *_client,
+ const unsigned char **output_r, size_t *output_len_r,
+ const char **error_r)
+{
+ struct oauthbearer_dsasl_client *client =
+ (struct oauthbearer_dsasl_client *)_client;
+ string_t *str;
+
+ if (_client->set.authid == NULL) {
+ *error_r = "authid not set";
+ return -1;
+ }
+ if (_client->password == NULL) {
+ *error_r = "password not set";
+ return -1;
+ }
+
+ str = str_new(_client->pool, 64);
+
+ str_printfa(str, "user=%s\x01", _client->set.authid);
+ str_printfa(str, "auth=Bearer %s\x01", _client->password);
+ str_append_c(str, '\x01');
+
+ *output_r = str_data(str);
+ *output_len_r = str_len(str);
+ client->output_sent = TRUE;
+ return 0;
+}
+
+static int
+mech_oauthbearer_set_parameter(struct dsasl_client *_client, const char *key,
+ const char *value, const char **error_r)
+{
+ struct oauthbearer_dsasl_client *client =
+ (struct oauthbearer_dsasl_client *)_client;
+ if (strcmp(key, "host") == 0) {
+ if (value != NULL)
+ client->host = p_strdup(_client->pool, value);
+ else
+ client->host = NULL;
+ return 1;
+ } else if (strcmp(key, "port") == 0) {
+ if (value == NULL) {
+ client->port = 0;
+ } else if (net_str2port(value, &client->port) < 0) {
+ *error_r = "Invalid port value";
+ return -1;
+ }
+ return 1;
+ }
+ return 0;
+}
+
+static int
+mech_oauthbearer_get_result(struct dsasl_client *_client, const char *key,
+ const char **value_r, const char **error_r ATTR_UNUSED)
+{
+ struct oauthbearer_dsasl_client *client =
+ (struct oauthbearer_dsasl_client *)_client;
+ if (strcmp(key, "status") == 0) {
+ /* this is set to value after login attempt */
+ i_assert(client->status != NULL);
+ *value_r = client->status;
+ return 1;
+ }
+ return 0;
+}
+
+const struct dsasl_client_mech dsasl_client_mech_oauthbearer = {
+ .name = "OAUTHBEARER",
+ .struct_size = sizeof(struct oauthbearer_dsasl_client),
+
+ .input = mech_oauthbearer_input,
+ .output = mech_oauthbearer_output,
+ .set_parameter = mech_oauthbearer_set_parameter,
+ .get_result = mech_oauthbearer_get_result,
+};
+
+const struct dsasl_client_mech dsasl_client_mech_xoauth2 = {
+ .name = "XOAUTH2",
+ .struct_size = sizeof(struct oauthbearer_dsasl_client),
+
+ .input = mech_oauthbearer_input,
+ .output = mech_xoauth2_output,
+ .set_parameter = mech_oauthbearer_set_parameter,
+ .get_result = mech_oauthbearer_get_result,
+};
diff --git a/src/lib-sasl/mech-plain.c b/src/lib-sasl/mech-plain.c
new file mode 100644
index 0000000..432fa1f
--- /dev/null
+++ b/src/lib-sasl/mech-plain.c
@@ -0,0 +1,70 @@
+/* Copyright (c) 2013-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "str.h"
+#include "dsasl-client-private.h"
+
+struct plain_dsasl_client {
+ struct dsasl_client client;
+ bool output_sent;
+};
+
+static int
+mech_plain_input(struct dsasl_client *_client,
+ const unsigned char *input ATTR_UNUSED, size_t input_len,
+ const char **error_r)
+{
+ struct plain_dsasl_client *client =
+ (struct plain_dsasl_client *)_client;
+
+ if (!client->output_sent) {
+ if (input_len > 0) {
+ *error_r = "Server sent non-empty initial response";
+ return -1;
+ }
+ } else {
+ *error_r = "Server didn't finish authentication";
+ return -1;
+ }
+ return 0;
+}
+
+static int
+mech_plain_output(struct dsasl_client *_client,
+ const unsigned char **output_r, size_t *output_len_r,
+ const char **error_r)
+{
+ struct plain_dsasl_client *client =
+ (struct plain_dsasl_client *)_client;
+ string_t *str;
+
+ if (_client->set.authid == NULL) {
+ *error_r = "authid not set";
+ return -1;
+ }
+ if (_client->password == NULL) {
+ *error_r = "password not set";
+ return -1;
+ }
+
+ str = str_new(_client->pool, 64);
+ if (_client->set.authzid != NULL)
+ str_append(str, _client->set.authzid);
+ str_append_c(str, '\0');
+ str_append(str, _client->set.authid);
+ str_append_c(str, '\0');
+ str_append(str, _client->password);
+
+ *output_r = str_data(str);
+ *output_len_r = str_len(str);
+ client->output_sent = TRUE;
+ return 0;
+}
+
+const struct dsasl_client_mech dsasl_client_mech_plain = {
+ .name = "PLAIN",
+ .struct_size = sizeof(struct plain_dsasl_client),
+
+ .input = mech_plain_input,
+ .output = mech_plain_output
+};
diff --git a/src/lib-sasl/test-sasl-client.c b/src/lib-sasl/test-sasl-client.c
new file mode 100644
index 0000000..05b65f2
--- /dev/null
+++ b/src/lib-sasl/test-sasl-client.c
@@ -0,0 +1,419 @@
+#include "lib.h"
+#include "str.h"
+#include "strfuncs.h"
+#include "test-common.h"
+#include "dsasl-client.h"
+
+static const struct dsasl_client_settings sasl_empty_set = {
+ .authid = NULL,
+};
+
+static const struct dsasl_client_settings sasl_no_password_set = {
+ .authid = "testuser",
+};
+
+static const struct dsasl_client_settings sasl_set = {
+ .authid = "testuser",
+ .password = "testpassword"
+};
+
+static const struct dsasl_client_settings sasl_master_set = {
+ .authzid = "testuser",
+ .authid = "masteruser",
+ .password = "masterpassword"
+};
+
+static void test_sasl_client_login(void)
+{
+ const char *error;
+ test_begin("sasl client LOGIN");
+ const struct dsasl_client_mech *mech = dsasl_client_mech_find("login");
+ i_assert(mech != NULL);
+ struct dsasl_client *client = dsasl_client_new(mech, &sasl_set);
+ i_assert(client != NULL);
+
+ string_t *input = t_str_new(64);
+ string_t *output_s = t_str_new(64);
+ const unsigned char *output;
+ size_t olen;
+
+ /* parameters shouldn't work (test here for completeness) */
+ test_assert(dsasl_client_set_parameter(client, "parameter", "value", &error) == 0);
+
+ /* Any input is valid */
+ str_append(input, "Username:");
+ test_assert(dsasl_client_input(client, input->data, input->used, &error) == 0);
+ test_assert(dsasl_client_output(client, &output, &olen, &error) == 0);
+ /* see what we got */
+ str_append_data(output_s, output, olen);
+ test_assert_strcmp(str_c(output_s), "testuser");
+
+ str_truncate(input, 0);
+ str_truncate(output_s, 0);
+
+ str_append(input, "Password:");
+ test_assert(dsasl_client_input(client, input->data, input->used, &error) == 0);
+ test_assert(dsasl_client_output(client, &output, &olen, &error) == 0);
+ str_append_data(output_s, output, olen);
+ test_assert_strcmp(str_c(output_s), "testpassword");
+
+ /* there should be no results to collect, we can reuse error here */
+ test_assert(dsasl_client_get_result(client, "parameter", &error, &error) == 0);
+
+ dsasl_client_free(&client);
+ test_assert(client == NULL);
+
+ /* server sends input after password */
+ client = dsasl_client_new(mech, &sasl_set);
+ i_assert(client != NULL);
+ test_assert(dsasl_client_input(client, uchar_empty_ptr, 0, &error) == 0);
+ test_assert(dsasl_client_output(client, &output, &olen, &error) == 0);
+ test_assert(dsasl_client_input(client, uchar_empty_ptr, 0, &error) == 0);
+ test_assert(dsasl_client_output(client, &output, &olen, &error) == 0);
+ test_assert(dsasl_client_input(client, uchar_empty_ptr, 0, &error) == -1);
+ test_assert_strcmp(error, "Server didn't finish authentication");
+
+ dsasl_client_free(&client);
+
+ /* missing username & password */
+ client = dsasl_client_new(mech, &sasl_empty_set);
+ i_assert(client != NULL);
+
+ test_assert(dsasl_client_input(client, uchar_empty_ptr, 0, &error) == 0);
+ test_assert(dsasl_client_output(client, &output, &olen, &error) == -1);
+ test_assert_strcmp(error, "authid not set");
+ dsasl_client_free(&client);
+
+ /* missing password */
+ client = dsasl_client_new(mech, &sasl_no_password_set);
+ i_assert(client != NULL);
+
+ test_assert(dsasl_client_input(client, uchar_empty_ptr, 0, &error) == 0);
+ test_assert(dsasl_client_output(client, &output, &olen, &error) == -1);
+ test_assert_strcmp(error, "password not set");
+ dsasl_client_free(&client);
+
+ /* unexpected NUL byte in input */
+ client = dsasl_client_new(mech, &sasl_set);
+ i_assert(client != NULL);
+
+ str_truncate(input, 0);
+ str_append_data(input, "unexpected\0", 11);
+ test_assert(dsasl_client_input(client, input->data, input->used, &error) == -1);
+ test_assert_strcmp(error, "Unexpected NUL in input data");
+ dsasl_client_free(&client);
+
+ test_end();
+}
+
+static void test_sasl_client_plain(void)
+{
+ const char *error;
+ test_begin("sasl client PLAIN");
+ const struct dsasl_client_mech *mech = dsasl_client_mech_find("plain");
+ i_assert(mech != NULL);
+ struct dsasl_client *client = dsasl_client_new(mech, &sasl_set);
+ i_assert(client != NULL);
+
+ const unsigned char *output;
+ size_t olen;
+
+ test_assert(dsasl_client_input(client, uchar_empty_ptr, 0, &error) == 0);
+ test_assert(dsasl_client_output(client, &output, &olen, &error) == 0);
+ const unsigned char expected[] = "\0testuser\0testpassword";
+ /* there is no NUL byte at the end */
+ test_assert(olen == sizeof(expected) - 1);
+ test_assert(memcmp(output, expected, I_MIN(sizeof(expected) - 1, olen)) == 0);
+
+ dsasl_client_free(&client);
+ test_assert(client == NULL);
+
+ client = dsasl_client_new(mech, &sasl_master_set);
+ i_assert(client != NULL);
+
+ test_assert(dsasl_client_input(client, uchar_empty_ptr, 0, &error) == 0);
+ test_assert(dsasl_client_output(client, &output, &olen, &error) == 0);
+ const unsigned char expected_master[] =
+ "testuser\0masteruser\0masterpassword";
+ /* there is no NUL byte at the end */
+ test_assert(olen == sizeof(expected_master) - 1);
+ test_assert(memcmp(output, expected_master,
+ I_MIN(sizeof(expected_master) - 1, olen)) == 0);
+
+ dsasl_client_free(&client);
+
+ /* unexpected initial response */
+ const unsigned char input[] = "ir";
+ client = dsasl_client_new(mech, &sasl_set);
+ i_assert(client != NULL);
+ test_assert(dsasl_client_input(client, input, sizeof(input)-1, &error) == -1);
+ test_assert_strcmp(error, "Server sent non-empty initial response");
+
+ dsasl_client_free(&client);
+
+ /* server sends input after response */
+ client = dsasl_client_new(mech, &sasl_set);
+ i_assert(client != NULL);
+ test_assert(dsasl_client_input(client, uchar_empty_ptr, 0, &error) == 0);
+ test_assert(dsasl_client_output(client, &output, &olen, &error) == 0);
+ test_assert(dsasl_client_input(client, uchar_empty_ptr, 0, &error) == -1);
+ test_assert_strcmp(error, "Server didn't finish authentication");
+
+ dsasl_client_free(&client);
+
+ /* missing username & password */
+ client = dsasl_client_new(mech, &sasl_empty_set);
+ i_assert(client != NULL);
+
+ test_assert(dsasl_client_input(client, uchar_empty_ptr, 0, &error) == 0);
+ test_assert(dsasl_client_output(client, &output, &olen, &error) == -1);
+ test_assert_strcmp(error, "authid not set");
+ dsasl_client_free(&client);
+
+ /* missing password */
+ client = dsasl_client_new(mech, &sasl_no_password_set);
+ i_assert(client != NULL);
+
+ test_assert(dsasl_client_input(client, uchar_empty_ptr, 0, &error) == 0);
+ test_assert(dsasl_client_output(client, &output, &olen, &error) == -1);
+ test_assert_strcmp(error, "password not set");
+ dsasl_client_free(&client);
+
+ /* unexpected NUL byte in input */
+ client = dsasl_client_new(mech, &sasl_set);
+ i_assert(client != NULL);
+
+ const unsigned char input2[] = "unexpected\0";
+ test_assert(dsasl_client_input(client, input2, sizeof(input2), &error) == -1);
+ test_assert_strcmp(error, "Unexpected NUL in input data");
+ dsasl_client_free(&client);
+
+ test_end();
+}
+
+static void test_sasl_client_external(void)
+{
+ const char *error;
+ test_begin("sasl client EXTERNAL");
+ const struct dsasl_client_mech *mech = dsasl_client_mech_find("external");
+ i_assert(mech != NULL);
+ struct dsasl_client *client = dsasl_client_new(mech, &sasl_set);
+ i_assert(client != NULL);
+
+ const unsigned char *output;
+ size_t olen;
+
+ test_assert(dsasl_client_input(client, uchar_empty_ptr, 0, &error) == 0);
+ test_assert(dsasl_client_output(client, &output, &olen, &error) == 0);
+ const unsigned char expected[] = "testuser";
+ /* there is no NUL byte at the end */
+ test_assert(olen == sizeof(expected) - 1);
+ test_assert(memcmp(output, expected, I_MIN(sizeof(expected) - 1, olen)) == 0);
+
+ dsasl_client_free(&client);
+ test_assert(client == NULL);
+
+ client = dsasl_client_new(mech, &sasl_master_set);
+ i_assert(client != NULL);
+
+ test_assert(dsasl_client_input(client, uchar_empty_ptr, 0, &error) == 0);
+ test_assert(dsasl_client_output(client, &output, &olen, &error) == 0);
+ const unsigned char expected_master[] = "testuser";
+ /* there is no NUL byte at the end */
+ test_assert(olen == sizeof(expected_master) - 1);
+ test_assert(memcmp(output, expected_master,
+ I_MIN(sizeof(expected_master) - 1, olen)) == 0);
+
+ dsasl_client_free(&client);
+
+ /* unexpected NUL byte in input */
+ client = dsasl_client_new(mech, &sasl_set);
+ i_assert(client != NULL);
+
+ const unsigned char input2[] = "unexpected\0";
+ test_assert(dsasl_client_input(client, input2, sizeof(input2), &error) == -1);
+ test_assert_strcmp(error, "Unexpected NUL in input data");
+ dsasl_client_free(&client);
+
+ test_end();
+}
+
+static void test_sasl_client_oauthbearer(void)
+{
+ const char *error;
+ const char *value;
+ test_begin("sasl client OAUTHBEARER");
+ const struct dsasl_client_mech *mech = dsasl_client_mech_find("oauthbearer");
+ i_assert(mech != NULL);
+ struct dsasl_client *client = dsasl_client_new(mech, &sasl_set);
+ i_assert(client != NULL);
+
+ string_t *input = t_str_new(64);
+ const unsigned char *output;
+ size_t olen;
+
+ test_assert(dsasl_client_input(client, uchar_empty_ptr, 0, &error) == 0);
+ test_assert(dsasl_client_output(client, &output, &olen, &error) == 0);
+ test_assert(dsasl_client_input(client, uchar_empty_ptr, 0, &error) == 0);
+
+ const unsigned char expected[] = "n,a=testuser,\1"
+ "auth=Bearer testpassword\1\1";
+ /* there is no NUL byte at the end */
+ test_assert(olen == sizeof(expected) - 1);
+ test_assert(memcmp(output, expected, I_MIN(sizeof(expected) - 1, olen)) == 0);
+ test_assert(dsasl_client_get_result(client, "status", &value, &error) == 1);
+ test_assert_strcmp(value, "");
+
+ dsasl_client_free(&client);
+ test_assert(client == NULL);
+
+ /* with host & port set */
+ client = dsasl_client_new(mech, &sasl_set);
+ i_assert(client != NULL);
+
+ test_assert(dsasl_client_set_parameter(client, "host", "example.com", &error) == 1);
+ test_assert(dsasl_client_set_parameter(client, "port", "imap", &error) == -1);
+ test_assert_strcmp(error, "Invalid port value");
+ test_assert(dsasl_client_set_parameter(client, "port", "143", &error) == 1);
+ test_assert(dsasl_client_set_parameter(client, "unknown", "value", &error) == 0);
+
+ test_assert(dsasl_client_input(client, uchar_empty_ptr, 0, &error) == 0);
+ test_assert(dsasl_client_output(client, &output, &olen, &error) == 0);
+ test_assert(dsasl_client_input(client, uchar_empty_ptr, 0, &error) == 0);
+
+ const unsigned char expected_h_p[] = "n,a=testuser,\1"
+ "host=example.com\1"
+ "port=143\1"
+ "auth=Bearer testpassword\1\1";
+ /* there is no NUL byte at the end */
+ test_assert(olen == sizeof(expected_h_p) - 1);
+ test_assert(memcmp(output, expected_h_p,
+ I_MIN(sizeof(expected_h_p) - 1, olen)) == 0);
+
+ dsasl_client_free(&client);
+ test_assert(client == NULL);
+
+ client = dsasl_client_new(mech, &sasl_set);
+ /* test error response */
+ test_assert(dsasl_client_input(client, uchar_empty_ptr, 0, &error) == 0);
+ test_assert(dsasl_client_output(client, &output, &olen, &error) == 0);
+ test_assert(dsasl_client_input(client, uchar_empty_ptr, 0, &error) == 0);
+ str_append(input, "{\"status\":\"401\",\"schemes\":\"bearer\",\"scope\":\"mail\"}");
+ test_assert(dsasl_client_input(client, input->data, input->used, &error) == -1);
+ test_assert_strcmp(error, "Failed to authenticate: 401");
+ test_assert(dsasl_client_get_result(client, "status", &value, &error) == 1);
+ test_assert_strcmp(value, "401");
+
+ dsasl_client_free(&client);
+
+ /* missing username & password */
+ client = dsasl_client_new(mech, &sasl_empty_set);
+ i_assert(client != NULL);
+
+ test_assert(dsasl_client_input(client, uchar_empty_ptr, 0, &error) == 0);
+ test_assert(dsasl_client_output(client, &output, &olen, &error) == -1);
+ test_assert_strcmp(error, "authid not set");
+ dsasl_client_free(&client);
+
+ /* missing password */
+ client = dsasl_client_new(mech, &sasl_no_password_set);
+ i_assert(client != NULL);
+
+ test_assert(dsasl_client_input(client, uchar_empty_ptr, 0, &error) == 0);
+ test_assert(dsasl_client_output(client, &output, &olen, &error) == -1);
+ test_assert_strcmp(error, "password not set");
+ dsasl_client_free(&client);
+
+ /* unexpected NUL byte in input */
+ client = dsasl_client_new(mech, &sasl_set);
+ i_assert(client != NULL);
+
+ const unsigned char input2[] = "unexpected\0";
+ test_assert(dsasl_client_input(client, input2, sizeof(input2), &error) == -1);
+ test_assert_strcmp(error, "Unexpected NUL in input data");
+ dsasl_client_free(&client);
+
+ test_end();
+}
+
+static void test_sasl_client_xoauth2(void)
+{
+ const char *error;
+ test_begin("sasl client XOAUTH2");
+ const struct dsasl_client_mech *mech = dsasl_client_mech_find("xoauth2");
+ i_assert(mech != NULL);
+ struct dsasl_client *client = dsasl_client_new(mech, &sasl_set);
+ i_assert(client != NULL);
+
+ string_t *input = t_str_new(64);
+ const unsigned char *output;
+ size_t olen;
+
+ test_assert(dsasl_client_input(client, uchar_empty_ptr, 0, &error) == 0);
+ test_assert(dsasl_client_output(client, &output, &olen, &error) == 0);
+ test_assert(dsasl_client_input(client, uchar_empty_ptr, 0, &error) == 0);
+
+ const unsigned char expected[] = "user=testuser\1auth=Bearer testpassword\1\1";
+ /* there is no NUL byte at the end */
+ test_assert(olen == sizeof(expected) - 1);
+ test_assert(memcmp(output, expected, I_MIN(sizeof(expected) - 1, olen)) == 0);
+
+ dsasl_client_free(&client);
+ test_assert(client == NULL);
+
+ client = dsasl_client_new(mech, &sasl_set);
+ /* test error response */
+ test_assert(dsasl_client_input(client, uchar_empty_ptr, 0, &error) == 0);
+ test_assert(dsasl_client_output(client, &output, &olen, &error) == 0);
+ test_assert(dsasl_client_input(client, uchar_empty_ptr, 0, &error) == 0);
+ str_append(input, "{\"status\":\"401\",\"schemes\":\"bearer\",\"scope\":\"mail\"}");
+ test_assert(dsasl_client_input(client, input->data, input->used, &error) == -1);
+ test_assert_strcmp(error, "Failed to authenticate: 401");
+
+ dsasl_client_free(&client);
+
+ /* missing username & password */
+ client = dsasl_client_new(mech, &sasl_empty_set);
+ i_assert(client != NULL);
+
+ test_assert(dsasl_client_input(client, uchar_empty_ptr, 0, &error) == 0);
+ test_assert(dsasl_client_output(client, &output, &olen, &error) == -1);
+ test_assert_strcmp(error, "authid not set");
+ dsasl_client_free(&client);
+
+ /* missing password */
+ client = dsasl_client_new(mech, &sasl_no_password_set);
+ i_assert(client != NULL);
+
+ test_assert(dsasl_client_input(client, uchar_empty_ptr, 0, &error) == 0);
+ test_assert(dsasl_client_output(client, &output, &olen, &error) == -1);
+ test_assert_strcmp(error, "password not set");
+ dsasl_client_free(&client);
+
+ /* unexpected NUL byte in input */
+ client = dsasl_client_new(mech, &sasl_set);
+ i_assert(client != NULL);
+
+ const unsigned char input2[] = "unexpected\0";
+ test_assert(dsasl_client_input(client, input2, sizeof(input2), &error) == -1);
+ test_assert_strcmp(error, "Unexpected NUL in input data");
+ dsasl_client_free(&client);
+
+ test_end();
+}
+
+int main(void)
+{
+ static void (*const test_functions[])(void) = {
+ test_sasl_client_login,
+ test_sasl_client_plain,
+ test_sasl_client_external,
+ test_sasl_client_oauthbearer,
+ test_sasl_client_xoauth2,
+ NULL
+ };
+ dsasl_clients_init();
+ int ret = test_run(test_functions);
+ dsasl_clients_deinit();
+ return ret;
+}
diff --git a/src/lib-settings/Makefile.am b/src/lib-settings/Makefile.am
new file mode 100644
index 0000000..30eeff5
--- /dev/null
+++ b/src/lib-settings/Makefile.am
@@ -0,0 +1,40 @@
+noinst_LTLIBRARIES = libsettings.la
+
+AM_CPPFLAGS = \
+ -I$(top_srcdir)/src/lib \
+ -I$(top_srcdir)/src/lib-test
+
+libsettings_la_SOURCES = \
+ settings.c \
+ settings-parser.c
+
+headers = \
+ settings.h \
+ settings-parser.h
+
+pkginc_libdir=$(pkgincludedir)
+pkginc_lib_HEADERS = $(headers)
+
+test_programs = \
+ test-settings-parser \
+ test-settings
+
+noinst_PROGRAMS = $(test_programs)
+
+test_libs = \
+ libsettings.la \
+ ../lib-test/libtest.la \
+ ../lib/liblib.la
+
+test_settings_parser_SOURCES = test-settings-parser.c
+test_settings_parser_LDADD = $(test_libs)
+test_settings_parser_DEPENDENCIES = $(test_libs)
+
+test_settings_SOURCES = test-settings.c
+test_settings_LDADD = $(test_libs)
+test_settings_DEPENDENCIES = $(test_libs)
+
+check-local:
+ for bin in $(test_programs); do \
+ if ! $(RUN_TEST) ./$$bin; then exit 1; fi; \
+ done
diff --git a/src/lib-settings/Makefile.in b/src/lib-settings/Makefile.in
new file mode 100644
index 0000000..212f721
--- /dev/null
+++ b/src/lib-settings/Makefile.in
@@ -0,0 +1,870 @@
+# Makefile.in generated by automake 1.16.1 from Makefile.am.
+# @configure_input@
+
+# Copyright (C) 1994-2018 Free Software Foundation, Inc.
+
+# This Makefile.in is free software; the Free Software Foundation
+# gives unlimited permission to copy and/or distribute it,
+# with or without modifications, as long as this notice is preserved.
+
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY, to the extent permitted by law; without
+# even the implied warranty of MERCHANTABILITY or FITNESS FOR A
+# PARTICULAR PURPOSE.
+
+@SET_MAKE@
+
+
+
+VPATH = @srcdir@
+am__is_gnu_make = { \
+ if test -z '$(MAKELEVEL)'; then \
+ false; \
+ elif test -n '$(MAKE_HOST)'; then \
+ true; \
+ elif test -n '$(MAKE_VERSION)' && test -n '$(CURDIR)'; then \
+ true; \
+ else \
+ false; \
+ fi; \
+}
+am__make_running_with_option = \
+ case $${target_option-} in \
+ ?) ;; \
+ *) echo "am__make_running_with_option: internal error: invalid" \
+ "target option '$${target_option-}' specified" >&2; \
+ exit 1;; \
+ esac; \
+ has_opt=no; \
+ sane_makeflags=$$MAKEFLAGS; \
+ if $(am__is_gnu_make); then \
+ sane_makeflags=$$MFLAGS; \
+ else \
+ case $$MAKEFLAGS in \
+ *\\[\ \ ]*) \
+ bs=\\; \
+ sane_makeflags=`printf '%s\n' "$$MAKEFLAGS" \
+ | sed "s/$$bs$$bs[$$bs $$bs ]*//g"`;; \
+ esac; \
+ fi; \
+ skip_next=no; \
+ strip_trailopt () \
+ { \
+ flg=`printf '%s\n' "$$flg" | sed "s/$$1.*$$//"`; \
+ }; \
+ for flg in $$sane_makeflags; do \
+ test $$skip_next = yes && { skip_next=no; continue; }; \
+ case $$flg in \
+ *=*|--*) continue;; \
+ -*I) strip_trailopt 'I'; skip_next=yes;; \
+ -*I?*) strip_trailopt 'I';; \
+ -*O) strip_trailopt 'O'; skip_next=yes;; \
+ -*O?*) strip_trailopt 'O';; \
+ -*l) strip_trailopt 'l'; skip_next=yes;; \
+ -*l?*) strip_trailopt 'l';; \
+ -[dEDm]) skip_next=yes;; \
+ -[JT]) skip_next=yes;; \
+ esac; \
+ case $$flg in \
+ *$$target_option*) has_opt=yes; break;; \
+ esac; \
+ done; \
+ test $$has_opt = yes
+am__make_dryrun = (target_option=n; $(am__make_running_with_option))
+am__make_keepgoing = (target_option=k; $(am__make_running_with_option))
+pkgdatadir = $(datadir)/@PACKAGE@
+pkgincludedir = $(includedir)/@PACKAGE@
+pkglibdir = $(libdir)/@PACKAGE@
+pkglibexecdir = $(libexecdir)/@PACKAGE@
+am__cd = CDPATH="$${ZSH_VERSION+.}$(PATH_SEPARATOR)" && cd
+install_sh_DATA = $(install_sh) -c -m 644
+install_sh_PROGRAM = $(install_sh) -c
+install_sh_SCRIPT = $(install_sh) -c
+INSTALL_HEADER = $(INSTALL_DATA)
+transform = $(program_transform_name)
+NORMAL_INSTALL = :
+PRE_INSTALL = :
+POST_INSTALL = :
+NORMAL_UNINSTALL = :
+PRE_UNINSTALL = :
+POST_UNINSTALL = :
+build_triplet = @build@
+host_triplet = @host@
+noinst_PROGRAMS = $(am__EXEEXT_1)
+subdir = src/lib-settings
+ACLOCAL_M4 = $(top_srcdir)/aclocal.m4
+am__aclocal_m4_deps = $(top_srcdir)/m4/ac_checktype2.m4 \
+ $(top_srcdir)/m4/ac_typeof.m4 $(top_srcdir)/m4/arc4random.m4 \
+ $(top_srcdir)/m4/blockdev.m4 $(top_srcdir)/m4/c99_vsnprintf.m4 \
+ $(top_srcdir)/m4/clock_gettime.m4 $(top_srcdir)/m4/crypt.m4 \
+ $(top_srcdir)/m4/crypt_xpg6.m4 $(top_srcdir)/m4/dbqlk.m4 \
+ $(top_srcdir)/m4/dirent_dtype.m4 $(top_srcdir)/m4/dovecot.m4 \
+ $(top_srcdir)/m4/fd_passing.m4 $(top_srcdir)/m4/fdatasync.m4 \
+ $(top_srcdir)/m4/flexible_array_member.m4 \
+ $(top_srcdir)/m4/glibc.m4 $(top_srcdir)/m4/gmtime_max.m4 \
+ $(top_srcdir)/m4/gmtime_tm_gmtoff.m4 \
+ $(top_srcdir)/m4/ioloop.m4 $(top_srcdir)/m4/iovec.m4 \
+ $(top_srcdir)/m4/ipv6.m4 $(top_srcdir)/m4/libcap.m4 \
+ $(top_srcdir)/m4/libtool.m4 $(top_srcdir)/m4/libwrap.m4 \
+ $(top_srcdir)/m4/linux_mremap.m4 $(top_srcdir)/m4/ltoptions.m4 \
+ $(top_srcdir)/m4/ltsugar.m4 $(top_srcdir)/m4/ltversion.m4 \
+ $(top_srcdir)/m4/lt~obsolete.m4 $(top_srcdir)/m4/mmap_write.m4 \
+ $(top_srcdir)/m4/mntctl.m4 $(top_srcdir)/m4/modules.m4 \
+ $(top_srcdir)/m4/notify.m4 $(top_srcdir)/m4/nsl.m4 \
+ $(top_srcdir)/m4/off_t_max.m4 $(top_srcdir)/m4/pkg.m4 \
+ $(top_srcdir)/m4/pr_set_dumpable.m4 \
+ $(top_srcdir)/m4/q_quotactl.m4 $(top_srcdir)/m4/quota.m4 \
+ $(top_srcdir)/m4/random.m4 $(top_srcdir)/m4/rlimit.m4 \
+ $(top_srcdir)/m4/sendfile.m4 $(top_srcdir)/m4/size_t_signed.m4 \
+ $(top_srcdir)/m4/sockpeercred.m4 $(top_srcdir)/m4/sql.m4 \
+ $(top_srcdir)/m4/ssl.m4 $(top_srcdir)/m4/st_tim.m4 \
+ $(top_srcdir)/m4/static_array.m4 $(top_srcdir)/m4/test_with.m4 \
+ $(top_srcdir)/m4/time_t.m4 $(top_srcdir)/m4/typeof.m4 \
+ $(top_srcdir)/m4/typeof_dev_t.m4 \
+ $(top_srcdir)/m4/uoff_t_max.m4 $(top_srcdir)/m4/vararg.m4 \
+ $(top_srcdir)/m4/want_apparmor.m4 \
+ $(top_srcdir)/m4/want_bsdauth.m4 \
+ $(top_srcdir)/m4/want_bzlib.m4 \
+ $(top_srcdir)/m4/want_cassandra.m4 \
+ $(top_srcdir)/m4/want_cdb.m4 \
+ $(top_srcdir)/m4/want_checkpassword.m4 \
+ $(top_srcdir)/m4/want_clucene.m4 $(top_srcdir)/m4/want_db.m4 \
+ $(top_srcdir)/m4/want_gssapi.m4 $(top_srcdir)/m4/want_icu.m4 \
+ $(top_srcdir)/m4/want_ldap.m4 $(top_srcdir)/m4/want_lua.m4 \
+ $(top_srcdir)/m4/want_lz4.m4 $(top_srcdir)/m4/want_lzma.m4 \
+ $(top_srcdir)/m4/want_mysql.m4 $(top_srcdir)/m4/want_pam.m4 \
+ $(top_srcdir)/m4/want_passwd.m4 $(top_srcdir)/m4/want_pgsql.m4 \
+ $(top_srcdir)/m4/want_prefetch.m4 \
+ $(top_srcdir)/m4/want_shadow.m4 \
+ $(top_srcdir)/m4/want_sodium.m4 $(top_srcdir)/m4/want_solr.m4 \
+ $(top_srcdir)/m4/want_sqlite.m4 \
+ $(top_srcdir)/m4/want_stemmer.m4 \
+ $(top_srcdir)/m4/want_systemd.m4 \
+ $(top_srcdir)/m4/want_textcat.m4 \
+ $(top_srcdir)/m4/want_unwind.m4 $(top_srcdir)/m4/want_zlib.m4 \
+ $(top_srcdir)/m4/want_zstd.m4 $(top_srcdir)/configure.ac
+am__configure_deps = $(am__aclocal_m4_deps) $(CONFIGURE_DEPENDENCIES) \
+ $(ACLOCAL_M4)
+DIST_COMMON = $(srcdir)/Makefile.am $(pkginc_lib_HEADERS) \
+ $(am__DIST_COMMON)
+mkinstalldirs = $(install_sh) -d
+CONFIG_HEADER = $(top_builddir)/config.h
+CONFIG_CLEAN_FILES =
+CONFIG_CLEAN_VPATH_FILES =
+am__EXEEXT_1 = test-settings-parser$(EXEEXT) test-settings$(EXEEXT)
+PROGRAMS = $(noinst_PROGRAMS)
+LTLIBRARIES = $(noinst_LTLIBRARIES)
+libsettings_la_LIBADD =
+am_libsettings_la_OBJECTS = settings.lo settings-parser.lo
+libsettings_la_OBJECTS = $(am_libsettings_la_OBJECTS)
+AM_V_lt = $(am__v_lt_@AM_V@)
+am__v_lt_ = $(am__v_lt_@AM_DEFAULT_V@)
+am__v_lt_0 = --silent
+am__v_lt_1 =
+am_test_settings_OBJECTS = test-settings.$(OBJEXT)
+test_settings_OBJECTS = $(am_test_settings_OBJECTS)
+am_test_settings_parser_OBJECTS = test-settings-parser.$(OBJEXT)
+test_settings_parser_OBJECTS = $(am_test_settings_parser_OBJECTS)
+AM_V_P = $(am__v_P_@AM_V@)
+am__v_P_ = $(am__v_P_@AM_DEFAULT_V@)
+am__v_P_0 = false
+am__v_P_1 = :
+AM_V_GEN = $(am__v_GEN_@AM_V@)
+am__v_GEN_ = $(am__v_GEN_@AM_DEFAULT_V@)
+am__v_GEN_0 = @echo " GEN " $@;
+am__v_GEN_1 =
+AM_V_at = $(am__v_at_@AM_V@)
+am__v_at_ = $(am__v_at_@AM_DEFAULT_V@)
+am__v_at_0 = @
+am__v_at_1 =
+DEFAULT_INCLUDES = -I.@am__isrc@ -I$(top_builddir)
+depcomp = $(SHELL) $(top_srcdir)/depcomp
+am__maybe_remake_depfiles = depfiles
+am__depfiles_remade = ./$(DEPDIR)/settings-parser.Plo \
+ ./$(DEPDIR)/settings.Plo ./$(DEPDIR)/test-settings-parser.Po \
+ ./$(DEPDIR)/test-settings.Po
+am__mv = mv -f
+COMPILE = $(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) \
+ $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS)
+LTCOMPILE = $(LIBTOOL) $(AM_V_lt) --tag=CC $(AM_LIBTOOLFLAGS) \
+ $(LIBTOOLFLAGS) --mode=compile $(CC) $(DEFS) \
+ $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) $(CPPFLAGS) \
+ $(AM_CFLAGS) $(CFLAGS)
+AM_V_CC = $(am__v_CC_@AM_V@)
+am__v_CC_ = $(am__v_CC_@AM_DEFAULT_V@)
+am__v_CC_0 = @echo " CC " $@;
+am__v_CC_1 =
+CCLD = $(CC)
+LINK = $(LIBTOOL) $(AM_V_lt) --tag=CC $(AM_LIBTOOLFLAGS) \
+ $(LIBTOOLFLAGS) --mode=link $(CCLD) $(AM_CFLAGS) $(CFLAGS) \
+ $(AM_LDFLAGS) $(LDFLAGS) -o $@
+AM_V_CCLD = $(am__v_CCLD_@AM_V@)
+am__v_CCLD_ = $(am__v_CCLD_@AM_DEFAULT_V@)
+am__v_CCLD_0 = @echo " CCLD " $@;
+am__v_CCLD_1 =
+SOURCES = $(libsettings_la_SOURCES) $(test_settings_SOURCES) \
+ $(test_settings_parser_SOURCES)
+DIST_SOURCES = $(libsettings_la_SOURCES) $(test_settings_SOURCES) \
+ $(test_settings_parser_SOURCES)
+am__can_run_installinfo = \
+ case $$AM_UPDATE_INFO_DIR in \
+ n|no|NO) false;; \
+ *) (install-info --version) >/dev/null 2>&1;; \
+ esac
+am__vpath_adj_setup = srcdirstrip=`echo "$(srcdir)" | sed 's|.|.|g'`;
+am__vpath_adj = case $$p in \
+ $(srcdir)/*) f=`echo "$$p" | sed "s|^$$srcdirstrip/||"`;; \
+ *) f=$$p;; \
+ esac;
+am__strip_dir = f=`echo $$p | sed -e 's|^.*/||'`;
+am__install_max = 40
+am__nobase_strip_setup = \
+ srcdirstrip=`echo "$(srcdir)" | sed 's/[].[^$$\\*|]/\\\\&/g'`
+am__nobase_strip = \
+ for p in $$list; do echo "$$p"; done | sed -e "s|$$srcdirstrip/||"
+am__nobase_list = $(am__nobase_strip_setup); \
+ for p in $$list; do echo "$$p $$p"; done | \
+ sed "s| $$srcdirstrip/| |;"' / .*\//!s/ .*/ ./; s,\( .*\)/[^/]*$$,\1,' | \
+ $(AWK) 'BEGIN { files["."] = "" } { files[$$2] = files[$$2] " " $$1; \
+ if (++n[$$2] == $(am__install_max)) \
+ { print $$2, files[$$2]; n[$$2] = 0; files[$$2] = "" } } \
+ END { for (dir in files) print dir, files[dir] }'
+am__base_list = \
+ sed '$$!N;$$!N;$$!N;$$!N;$$!N;$$!N;$$!N;s/\n/ /g' | \
+ sed '$$!N;$$!N;$$!N;$$!N;s/\n/ /g'
+am__uninstall_files_from_dir = { \
+ test -z "$$files" \
+ || { test ! -d "$$dir" && test ! -f "$$dir" && test ! -r "$$dir"; } \
+ || { echo " ( cd '$$dir' && rm -f" $$files ")"; \
+ $(am__cd) "$$dir" && rm -f $$files; }; \
+ }
+am__installdirs = "$(DESTDIR)$(pkginc_libdir)"
+HEADERS = $(pkginc_lib_HEADERS)
+am__tagged_files = $(HEADERS) $(SOURCES) $(TAGS_FILES) $(LISP)
+# Read a list of newline-separated strings from the standard input,
+# and print each of them once, without duplicates. Input order is
+# *not* preserved.
+am__uniquify_input = $(AWK) '\
+ BEGIN { nonempty = 0; } \
+ { items[$$0] = 1; nonempty = 1; } \
+ END { if (nonempty) { for (i in items) print i; }; } \
+'
+# Make sure the list of sources is unique. This is necessary because,
+# e.g., the same source file might be shared among _SOURCES variables
+# for different programs/libraries.
+am__define_uniq_tagged_files = \
+ list='$(am__tagged_files)'; \
+ unique=`for i in $$list; do \
+ if test -f "$$i"; then echo $$i; else echo $(srcdir)/$$i; fi; \
+ done | $(am__uniquify_input)`
+ETAGS = etags
+CTAGS = ctags
+am__DIST_COMMON = $(srcdir)/Makefile.in $(top_srcdir)/depcomp
+DISTFILES = $(DIST_COMMON) $(DIST_SOURCES) $(TEXINFOS) $(EXTRA_DIST)
+ACLOCAL = @ACLOCAL@
+ACLOCAL_AMFLAGS = @ACLOCAL_AMFLAGS@
+AMTAR = @AMTAR@
+AM_DEFAULT_VERBOSITY = @AM_DEFAULT_VERBOSITY@
+APPARMOR_LIBS = @APPARMOR_LIBS@
+AR = @AR@
+AUTH_CFLAGS = @AUTH_CFLAGS@
+AUTH_LIBS = @AUTH_LIBS@
+AUTOCONF = @AUTOCONF@
+AUTOHEADER = @AUTOHEADER@
+AUTOMAKE = @AUTOMAKE@
+AWK = @AWK@
+BINARY_CFLAGS = @BINARY_CFLAGS@
+BINARY_LDFLAGS = @BINARY_LDFLAGS@
+BISON = @BISON@
+CASSANDRA_CFLAGS = @CASSANDRA_CFLAGS@
+CASSANDRA_LIBS = @CASSANDRA_LIBS@
+CC = @CC@
+CCDEPMODE = @CCDEPMODE@
+CDB_LIBS = @CDB_LIBS@
+CFLAGS = @CFLAGS@
+CLUCENE_CFLAGS = @CLUCENE_CFLAGS@
+CLUCENE_LIBS = @CLUCENE_LIBS@
+COMPRESS_LIBS = @COMPRESS_LIBS@
+CPP = @CPP@
+CPPFLAGS = @CPPFLAGS@
+CRYPT_LIBS = @CRYPT_LIBS@
+CXX = @CXX@
+CXXCPP = @CXXCPP@
+CXXDEPMODE = @CXXDEPMODE@
+CXXFLAGS = @CXXFLAGS@
+CYGPATH_W = @CYGPATH_W@
+DEFS = @DEFS@
+DEPDIR = @DEPDIR@
+DICT_LIBS = @DICT_LIBS@
+DLLIB = @DLLIB@
+DLLTOOL = @DLLTOOL@
+DSYMUTIL = @DSYMUTIL@
+DUMPBIN = @DUMPBIN@
+ECHO_C = @ECHO_C@
+ECHO_N = @ECHO_N@
+ECHO_T = @ECHO_T@
+EGREP = @EGREP@
+EXEEXT = @EXEEXT@
+FGREP = @FGREP@
+FLEX = @FLEX@
+FUZZER_CPPFLAGS = @FUZZER_CPPFLAGS@
+FUZZER_LDFLAGS = @FUZZER_LDFLAGS@
+GREP = @GREP@
+INSTALL = @INSTALL@
+INSTALL_DATA = @INSTALL_DATA@
+INSTALL_PROGRAM = @INSTALL_PROGRAM@
+INSTALL_SCRIPT = @INSTALL_SCRIPT@
+INSTALL_STRIP_PROGRAM = @INSTALL_STRIP_PROGRAM@
+KRB5CONFIG = @KRB5CONFIG@
+KRB5_CFLAGS = @KRB5_CFLAGS@
+KRB5_LIBS = @KRB5_LIBS@
+LD = @LD@
+LDAP_LIBS = @LDAP_LIBS@
+LDFLAGS = @LDFLAGS@
+LD_NO_WHOLE_ARCHIVE = @LD_NO_WHOLE_ARCHIVE@
+LD_WHOLE_ARCHIVE = @LD_WHOLE_ARCHIVE@
+LIBCAP = @LIBCAP@
+LIBDOVECOT = @LIBDOVECOT@
+LIBDOVECOT_COMPRESS = @LIBDOVECOT_COMPRESS@
+LIBDOVECOT_DEPS = @LIBDOVECOT_DEPS@
+LIBDOVECOT_DSYNC = @LIBDOVECOT_DSYNC@
+LIBDOVECOT_LA_LIBS = @LIBDOVECOT_LA_LIBS@
+LIBDOVECOT_LDA = @LIBDOVECOT_LDA@
+LIBDOVECOT_LDAP = @LIBDOVECOT_LDAP@
+LIBDOVECOT_LIBFTS = @LIBDOVECOT_LIBFTS@
+LIBDOVECOT_LIBFTS_DEPS = @LIBDOVECOT_LIBFTS_DEPS@
+LIBDOVECOT_LOGIN = @LIBDOVECOT_LOGIN@
+LIBDOVECOT_LUA = @LIBDOVECOT_LUA@
+LIBDOVECOT_LUA_DEPS = @LIBDOVECOT_LUA_DEPS@
+LIBDOVECOT_SQL = @LIBDOVECOT_SQL@
+LIBDOVECOT_STORAGE = @LIBDOVECOT_STORAGE@
+LIBDOVECOT_STORAGE_DEPS = @LIBDOVECOT_STORAGE_DEPS@
+LIBEXTTEXTCAT_CFLAGS = @LIBEXTTEXTCAT_CFLAGS@
+LIBEXTTEXTCAT_LIBS = @LIBEXTTEXTCAT_LIBS@
+LIBICONV = @LIBICONV@
+LIBICU_CFLAGS = @LIBICU_CFLAGS@
+LIBICU_LIBS = @LIBICU_LIBS@
+LIBOBJS = @LIBOBJS@
+LIBS = @LIBS@
+LIBSODIUM_CFLAGS = @LIBSODIUM_CFLAGS@
+LIBSODIUM_LIBS = @LIBSODIUM_LIBS@
+LIBTIRPC_CFLAGS = @LIBTIRPC_CFLAGS@
+LIBTIRPC_LIBS = @LIBTIRPC_LIBS@
+LIBTOOL = @LIBTOOL@
+LIBUNWIND_CFLAGS = @LIBUNWIND_CFLAGS@
+LIBUNWIND_LIBS = @LIBUNWIND_LIBS@
+LIBWRAP_LIBS = @LIBWRAP_LIBS@
+LINKED_STORAGE_LDADD = @LINKED_STORAGE_LDADD@
+LIPO = @LIPO@
+LN_S = @LN_S@
+LTLIBICONV = @LTLIBICONV@
+LTLIBOBJS = @LTLIBOBJS@
+LT_SYS_LIBRARY_PATH = @LT_SYS_LIBRARY_PATH@
+LUA_CFLAGS = @LUA_CFLAGS@
+LUA_LIBS = @LUA_LIBS@
+MAINT = @MAINT@
+MAKEINFO = @MAKEINFO@
+MANIFEST_TOOL = @MANIFEST_TOOL@
+MKDIR_P = @MKDIR_P@
+MODULE_LIBS = @MODULE_LIBS@
+MODULE_SUFFIX = @MODULE_SUFFIX@
+MYSQL_CFLAGS = @MYSQL_CFLAGS@
+MYSQL_CONFIG = @MYSQL_CONFIG@
+MYSQL_LIBS = @MYSQL_LIBS@
+NM = @NM@
+NMEDIT = @NMEDIT@
+NOPLUGIN_LDFLAGS = @NOPLUGIN_LDFLAGS@
+OBJDUMP = @OBJDUMP@
+OBJEXT = @OBJEXT@
+OTOOL = @OTOOL@
+OTOOL64 = @OTOOL64@
+PACKAGE = @PACKAGE@
+PACKAGE_BUGREPORT = @PACKAGE_BUGREPORT@
+PACKAGE_NAME = @PACKAGE_NAME@
+PACKAGE_STRING = @PACKAGE_STRING@
+PACKAGE_TARNAME = @PACKAGE_TARNAME@
+PACKAGE_URL = @PACKAGE_URL@
+PACKAGE_VERSION = @PACKAGE_VERSION@
+PANDOC = @PANDOC@
+PATH_SEPARATOR = @PATH_SEPARATOR@
+PGSQL_CFLAGS = @PGSQL_CFLAGS@
+PGSQL_LIBS = @PGSQL_LIBS@
+PG_CONFIG = @PG_CONFIG@
+PIE_CFLAGS = @PIE_CFLAGS@
+PIE_LDFLAGS = @PIE_LDFLAGS@
+PKG_CONFIG = @PKG_CONFIG@
+PKG_CONFIG_LIBDIR = @PKG_CONFIG_LIBDIR@
+PKG_CONFIG_PATH = @PKG_CONFIG_PATH@
+QUOTA_LIBS = @QUOTA_LIBS@
+RANLIB = @RANLIB@
+RELRO_LDFLAGS = @RELRO_LDFLAGS@
+RPCGEN = @RPCGEN@
+RUN_TEST = @RUN_TEST@
+SED = @SED@
+SETTING_FILES = @SETTING_FILES@
+SET_MAKE = @SET_MAKE@
+SHELL = @SHELL@
+SQLITE_CFLAGS = @SQLITE_CFLAGS@
+SQLITE_LIBS = @SQLITE_LIBS@
+SQL_CFLAGS = @SQL_CFLAGS@
+SQL_LIBS = @SQL_LIBS@
+SSL_CFLAGS = @SSL_CFLAGS@
+SSL_LIBS = @SSL_LIBS@
+STRIP = @STRIP@
+SYSTEMD_CFLAGS = @SYSTEMD_CFLAGS@
+SYSTEMD_LIBS = @SYSTEMD_LIBS@
+VALGRIND = @VALGRIND@
+VERSION = @VERSION@
+ZSTD_CFLAGS = @ZSTD_CFLAGS@
+ZSTD_LIBS = @ZSTD_LIBS@
+abs_builddir = @abs_builddir@
+abs_srcdir = @abs_srcdir@
+abs_top_builddir = @abs_top_builddir@
+abs_top_srcdir = @abs_top_srcdir@
+ac_ct_AR = @ac_ct_AR@
+ac_ct_CC = @ac_ct_CC@
+ac_ct_CXX = @ac_ct_CXX@
+ac_ct_DUMPBIN = @ac_ct_DUMPBIN@
+am__include = @am__include@
+am__leading_dot = @am__leading_dot@
+am__quote = @am__quote@
+am__tar = @am__tar@
+am__untar = @am__untar@
+bindir = @bindir@
+build = @build@
+build_alias = @build_alias@
+build_cpu = @build_cpu@
+build_os = @build_os@
+build_vendor = @build_vendor@
+builddir = @builddir@
+datadir = @datadir@
+datarootdir = @datarootdir@
+dict_drivers = @dict_drivers@
+docdir = @docdir@
+dvidir = @dvidir@
+exec_prefix = @exec_prefix@
+host = @host@
+host_alias = @host_alias@
+host_cpu = @host_cpu@
+host_os = @host_os@
+host_vendor = @host_vendor@
+htmldir = @htmldir@
+includedir = @includedir@
+infodir = @infodir@
+install_sh = @install_sh@
+libdir = @libdir@
+libexecdir = @libexecdir@
+localedir = @localedir@
+localstatedir = @localstatedir@
+mandir = @mandir@
+mkdir_p = @mkdir_p@
+moduledir = @moduledir@
+oldincludedir = @oldincludedir@
+pdfdir = @pdfdir@
+prefix = @prefix@
+program_transform_name = @program_transform_name@
+psdir = @psdir@
+rundir = @rundir@
+runstatedir = @runstatedir@
+sbindir = @sbindir@
+sharedstatedir = @sharedstatedir@
+sql_drivers = @sql_drivers@
+srcdir = @srcdir@
+ssldir = @ssldir@
+statedir = @statedir@
+sysconfdir = @sysconfdir@
+systemdservicetype = @systemdservicetype@
+systemdsystemunitdir = @systemdsystemunitdir@
+target_alias = @target_alias@
+top_build_prefix = @top_build_prefix@
+top_builddir = @top_builddir@
+top_srcdir = @top_srcdir@
+noinst_LTLIBRARIES = libsettings.la
+AM_CPPFLAGS = \
+ -I$(top_srcdir)/src/lib \
+ -I$(top_srcdir)/src/lib-test
+
+libsettings_la_SOURCES = \
+ settings.c \
+ settings-parser.c
+
+headers = \
+ settings.h \
+ settings-parser.h
+
+pkginc_libdir = $(pkgincludedir)
+pkginc_lib_HEADERS = $(headers)
+test_programs = \
+ test-settings-parser \
+ test-settings
+
+test_libs = \
+ libsettings.la \
+ ../lib-test/libtest.la \
+ ../lib/liblib.la
+
+test_settings_parser_SOURCES = test-settings-parser.c
+test_settings_parser_LDADD = $(test_libs)
+test_settings_parser_DEPENDENCIES = $(test_libs)
+test_settings_SOURCES = test-settings.c
+test_settings_LDADD = $(test_libs)
+test_settings_DEPENDENCIES = $(test_libs)
+all: all-am
+
+.SUFFIXES:
+.SUFFIXES: .c .lo .o .obj
+$(srcdir)/Makefile.in: @MAINTAINER_MODE_TRUE@ $(srcdir)/Makefile.am $(am__configure_deps)
+ @for dep in $?; do \
+ case '$(am__configure_deps)' in \
+ *$$dep*) \
+ ( cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh ) \
+ && { if test -f $@; then exit 0; else break; fi; }; \
+ exit 1;; \
+ esac; \
+ done; \
+ echo ' cd $(top_srcdir) && $(AUTOMAKE) --foreign src/lib-settings/Makefile'; \
+ $(am__cd) $(top_srcdir) && \
+ $(AUTOMAKE) --foreign src/lib-settings/Makefile
+Makefile: $(srcdir)/Makefile.in $(top_builddir)/config.status
+ @case '$?' in \
+ *config.status*) \
+ cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh;; \
+ *) \
+ echo ' cd $(top_builddir) && $(SHELL) ./config.status $(subdir)/$@ $(am__maybe_remake_depfiles)'; \
+ cd $(top_builddir) && $(SHELL) ./config.status $(subdir)/$@ $(am__maybe_remake_depfiles);; \
+ esac;
+
+$(top_builddir)/config.status: $(top_srcdir)/configure $(CONFIG_STATUS_DEPENDENCIES)
+ cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh
+
+$(top_srcdir)/configure: @MAINTAINER_MODE_TRUE@ $(am__configure_deps)
+ cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh
+$(ACLOCAL_M4): @MAINTAINER_MODE_TRUE@ $(am__aclocal_m4_deps)
+ cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh
+$(am__aclocal_m4_deps):
+
+clean-noinstPROGRAMS:
+ @list='$(noinst_PROGRAMS)'; test -n "$$list" || exit 0; \
+ echo " rm -f" $$list; \
+ rm -f $$list || exit $$?; \
+ test -n "$(EXEEXT)" || exit 0; \
+ list=`for p in $$list; do echo "$$p"; done | sed 's/$(EXEEXT)$$//'`; \
+ echo " rm -f" $$list; \
+ rm -f $$list
+
+clean-noinstLTLIBRARIES:
+ -test -z "$(noinst_LTLIBRARIES)" || rm -f $(noinst_LTLIBRARIES)
+ @list='$(noinst_LTLIBRARIES)'; \
+ locs=`for p in $$list; do echo $$p; done | \
+ sed 's|^[^/]*$$|.|; s|/[^/]*$$||; s|$$|/so_locations|' | \
+ sort -u`; \
+ test -z "$$locs" || { \
+ echo rm -f $${locs}; \
+ rm -f $${locs}; \
+ }
+
+libsettings.la: $(libsettings_la_OBJECTS) $(libsettings_la_DEPENDENCIES) $(EXTRA_libsettings_la_DEPENDENCIES)
+ $(AM_V_CCLD)$(LINK) $(libsettings_la_OBJECTS) $(libsettings_la_LIBADD) $(LIBS)
+
+test-settings$(EXEEXT): $(test_settings_OBJECTS) $(test_settings_DEPENDENCIES) $(EXTRA_test_settings_DEPENDENCIES)
+ @rm -f test-settings$(EXEEXT)
+ $(AM_V_CCLD)$(LINK) $(test_settings_OBJECTS) $(test_settings_LDADD) $(LIBS)
+
+test-settings-parser$(EXEEXT): $(test_settings_parser_OBJECTS) $(test_settings_parser_DEPENDENCIES) $(EXTRA_test_settings_parser_DEPENDENCIES)
+ @rm -f test-settings-parser$(EXEEXT)
+ $(AM_V_CCLD)$(LINK) $(test_settings_parser_OBJECTS) $(test_settings_parser_LDADD) $(LIBS)
+
+mostlyclean-compile:
+ -rm -f *.$(OBJEXT)
+
+distclean-compile:
+ -rm -f *.tab.c
+
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/settings-parser.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/settings.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/test-settings-parser.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/test-settings.Po@am__quote@ # am--include-marker
+
+$(am__depfiles_remade):
+ @$(MKDIR_P) $(@D)
+ @echo '# dummy' >$@-t && $(am__mv) $@-t $@
+
+am--depfiles: $(am__depfiles_remade)
+
+.c.o:
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(COMPILE) -MT $@ -MD -MP -MF $(DEPDIR)/$*.Tpo -c -o $@ $<
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/$*.Tpo $(DEPDIR)/$*.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='$<' object='$@' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(COMPILE) -c -o $@ $<
+
+.c.obj:
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(COMPILE) -MT $@ -MD -MP -MF $(DEPDIR)/$*.Tpo -c -o $@ `$(CYGPATH_W) '$<'`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/$*.Tpo $(DEPDIR)/$*.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='$<' object='$@' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(COMPILE) -c -o $@ `$(CYGPATH_W) '$<'`
+
+.c.lo:
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(LTCOMPILE) -MT $@ -MD -MP -MF $(DEPDIR)/$*.Tpo -c -o $@ $<
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/$*.Tpo $(DEPDIR)/$*.Plo
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='$<' object='$@' libtool=yes @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(LTCOMPILE) -c -o $@ $<
+
+mostlyclean-libtool:
+ -rm -f *.lo
+
+clean-libtool:
+ -rm -rf .libs _libs
+install-pkginc_libHEADERS: $(pkginc_lib_HEADERS)
+ @$(NORMAL_INSTALL)
+ @list='$(pkginc_lib_HEADERS)'; test -n "$(pkginc_libdir)" || list=; \
+ if test -n "$$list"; then \
+ echo " $(MKDIR_P) '$(DESTDIR)$(pkginc_libdir)'"; \
+ $(MKDIR_P) "$(DESTDIR)$(pkginc_libdir)" || exit 1; \
+ fi; \
+ for p in $$list; do \
+ if test -f "$$p"; then d=; else d="$(srcdir)/"; fi; \
+ echo "$$d$$p"; \
+ done | $(am__base_list) | \
+ while read files; do \
+ echo " $(INSTALL_HEADER) $$files '$(DESTDIR)$(pkginc_libdir)'"; \
+ $(INSTALL_HEADER) $$files "$(DESTDIR)$(pkginc_libdir)" || exit $$?; \
+ done
+
+uninstall-pkginc_libHEADERS:
+ @$(NORMAL_UNINSTALL)
+ @list='$(pkginc_lib_HEADERS)'; test -n "$(pkginc_libdir)" || list=; \
+ files=`for p in $$list; do echo $$p; done | sed -e 's|^.*/||'`; \
+ dir='$(DESTDIR)$(pkginc_libdir)'; $(am__uninstall_files_from_dir)
+
+ID: $(am__tagged_files)
+ $(am__define_uniq_tagged_files); mkid -fID $$unique
+tags: tags-am
+TAGS: tags
+
+tags-am: $(TAGS_DEPENDENCIES) $(am__tagged_files)
+ set x; \
+ here=`pwd`; \
+ $(am__define_uniq_tagged_files); \
+ shift; \
+ if test -z "$(ETAGS_ARGS)$$*$$unique"; then :; else \
+ test -n "$$unique" || unique=$$empty_fix; \
+ if test $$# -gt 0; then \
+ $(ETAGS) $(ETAGSFLAGS) $(AM_ETAGSFLAGS) $(ETAGS_ARGS) \
+ "$$@" $$unique; \
+ else \
+ $(ETAGS) $(ETAGSFLAGS) $(AM_ETAGSFLAGS) $(ETAGS_ARGS) \
+ $$unique; \
+ fi; \
+ fi
+ctags: ctags-am
+
+CTAGS: ctags
+ctags-am: $(TAGS_DEPENDENCIES) $(am__tagged_files)
+ $(am__define_uniq_tagged_files); \
+ test -z "$(CTAGS_ARGS)$$unique" \
+ || $(CTAGS) $(CTAGSFLAGS) $(AM_CTAGSFLAGS) $(CTAGS_ARGS) \
+ $$unique
+
+GTAGS:
+ here=`$(am__cd) $(top_builddir) && pwd` \
+ && $(am__cd) $(top_srcdir) \
+ && gtags -i $(GTAGS_ARGS) "$$here"
+cscopelist: cscopelist-am
+
+cscopelist-am: $(am__tagged_files)
+ list='$(am__tagged_files)'; \
+ case "$(srcdir)" in \
+ [\\/]* | ?:[\\/]*) sdir="$(srcdir)" ;; \
+ *) sdir=$(subdir)/$(srcdir) ;; \
+ esac; \
+ for i in $$list; do \
+ if test -f "$$i"; then \
+ echo "$(subdir)/$$i"; \
+ else \
+ echo "$$sdir/$$i"; \
+ fi; \
+ done >> $(top_builddir)/cscope.files
+
+distclean-tags:
+ -rm -f TAGS ID GTAGS GRTAGS GSYMS GPATH tags
+
+distdir: $(BUILT_SOURCES)
+ $(MAKE) $(AM_MAKEFLAGS) distdir-am
+
+distdir-am: $(DISTFILES)
+ @srcdirstrip=`echo "$(srcdir)" | sed 's/[].[^$$\\*]/\\\\&/g'`; \
+ topsrcdirstrip=`echo "$(top_srcdir)" | sed 's/[].[^$$\\*]/\\\\&/g'`; \
+ list='$(DISTFILES)'; \
+ dist_files=`for file in $$list; do echo $$file; done | \
+ sed -e "s|^$$srcdirstrip/||;t" \
+ -e "s|^$$topsrcdirstrip/|$(top_builddir)/|;t"`; \
+ case $$dist_files in \
+ */*) $(MKDIR_P) `echo "$$dist_files" | \
+ sed '/\//!d;s|^|$(distdir)/|;s,/[^/]*$$,,' | \
+ sort -u` ;; \
+ esac; \
+ for file in $$dist_files; do \
+ if test -f $$file || test -d $$file; then d=.; else d=$(srcdir); fi; \
+ if test -d $$d/$$file; then \
+ dir=`echo "/$$file" | sed -e 's,/[^/]*$$,,'`; \
+ if test -d "$(distdir)/$$file"; then \
+ find "$(distdir)/$$file" -type d ! -perm -700 -exec chmod u+rwx {} \;; \
+ fi; \
+ if test -d $(srcdir)/$$file && test $$d != $(srcdir); then \
+ cp -fpR $(srcdir)/$$file "$(distdir)$$dir" || exit 1; \
+ find "$(distdir)/$$file" -type d ! -perm -700 -exec chmod u+rwx {} \;; \
+ fi; \
+ cp -fpR $$d/$$file "$(distdir)$$dir" || exit 1; \
+ else \
+ test -f "$(distdir)/$$file" \
+ || cp -p $$d/$$file "$(distdir)/$$file" \
+ || exit 1; \
+ fi; \
+ done
+check-am: all-am
+ $(MAKE) $(AM_MAKEFLAGS) check-local
+check: check-am
+all-am: Makefile $(PROGRAMS) $(LTLIBRARIES) $(HEADERS)
+installdirs:
+ for dir in "$(DESTDIR)$(pkginc_libdir)"; do \
+ test -z "$$dir" || $(MKDIR_P) "$$dir"; \
+ done
+install: install-am
+install-exec: install-exec-am
+install-data: install-data-am
+uninstall: uninstall-am
+
+install-am: all-am
+ @$(MAKE) $(AM_MAKEFLAGS) install-exec-am install-data-am
+
+installcheck: installcheck-am
+install-strip:
+ if test -z '$(STRIP)'; then \
+ $(MAKE) $(AM_MAKEFLAGS) INSTALL_PROGRAM="$(INSTALL_STRIP_PROGRAM)" \
+ install_sh_PROGRAM="$(INSTALL_STRIP_PROGRAM)" INSTALL_STRIP_FLAG=-s \
+ install; \
+ else \
+ $(MAKE) $(AM_MAKEFLAGS) INSTALL_PROGRAM="$(INSTALL_STRIP_PROGRAM)" \
+ install_sh_PROGRAM="$(INSTALL_STRIP_PROGRAM)" INSTALL_STRIP_FLAG=-s \
+ "INSTALL_PROGRAM_ENV=STRIPPROG='$(STRIP)'" install; \
+ fi
+mostlyclean-generic:
+
+clean-generic:
+
+distclean-generic:
+ -test -z "$(CONFIG_CLEAN_FILES)" || rm -f $(CONFIG_CLEAN_FILES)
+ -test . = "$(srcdir)" || test -z "$(CONFIG_CLEAN_VPATH_FILES)" || rm -f $(CONFIG_CLEAN_VPATH_FILES)
+
+maintainer-clean-generic:
+ @echo "This command is intended for maintainers to use"
+ @echo "it deletes files that may require special tools to rebuild."
+clean: clean-am
+
+clean-am: clean-generic clean-libtool clean-noinstLTLIBRARIES \
+ clean-noinstPROGRAMS mostlyclean-am
+
+distclean: distclean-am
+ -rm -f ./$(DEPDIR)/settings-parser.Plo
+ -rm -f ./$(DEPDIR)/settings.Plo
+ -rm -f ./$(DEPDIR)/test-settings-parser.Po
+ -rm -f ./$(DEPDIR)/test-settings.Po
+ -rm -f Makefile
+distclean-am: clean-am distclean-compile distclean-generic \
+ distclean-tags
+
+dvi: dvi-am
+
+dvi-am:
+
+html: html-am
+
+html-am:
+
+info: info-am
+
+info-am:
+
+install-data-am: install-pkginc_libHEADERS
+
+install-dvi: install-dvi-am
+
+install-dvi-am:
+
+install-exec-am:
+
+install-html: install-html-am
+
+install-html-am:
+
+install-info: install-info-am
+
+install-info-am:
+
+install-man:
+
+install-pdf: install-pdf-am
+
+install-pdf-am:
+
+install-ps: install-ps-am
+
+install-ps-am:
+
+installcheck-am:
+
+maintainer-clean: maintainer-clean-am
+ -rm -f ./$(DEPDIR)/settings-parser.Plo
+ -rm -f ./$(DEPDIR)/settings.Plo
+ -rm -f ./$(DEPDIR)/test-settings-parser.Po
+ -rm -f ./$(DEPDIR)/test-settings.Po
+ -rm -f Makefile
+maintainer-clean-am: distclean-am maintainer-clean-generic
+
+mostlyclean: mostlyclean-am
+
+mostlyclean-am: mostlyclean-compile mostlyclean-generic \
+ mostlyclean-libtool
+
+pdf: pdf-am
+
+pdf-am:
+
+ps: ps-am
+
+ps-am:
+
+uninstall-am: uninstall-pkginc_libHEADERS
+
+.MAKE: check-am install-am install-strip
+
+.PHONY: CTAGS GTAGS TAGS all all-am am--depfiles check check-am \
+ check-local clean clean-generic clean-libtool \
+ clean-noinstLTLIBRARIES clean-noinstPROGRAMS cscopelist-am \
+ ctags ctags-am distclean distclean-compile distclean-generic \
+ distclean-libtool distclean-tags distdir dvi dvi-am html \
+ html-am info info-am install install-am install-data \
+ install-data-am install-dvi install-dvi-am install-exec \
+ install-exec-am install-html install-html-am install-info \
+ install-info-am install-man install-pdf install-pdf-am \
+ install-pkginc_libHEADERS install-ps install-ps-am \
+ install-strip installcheck installcheck-am installdirs \
+ maintainer-clean maintainer-clean-generic mostlyclean \
+ mostlyclean-compile mostlyclean-generic mostlyclean-libtool \
+ pdf pdf-am ps ps-am tags tags-am uninstall uninstall-am \
+ uninstall-pkginc_libHEADERS
+
+.PRECIOUS: Makefile
+
+
+check-local:
+ for bin in $(test_programs); do \
+ if ! $(RUN_TEST) ./$$bin; then exit 1; fi; \
+ done
+
+# Tell versions [3.59,3.63) of GNU make to not export all variables.
+# Otherwise a system limit (for SysV at least) may be exceeded.
+.NOEXPORT:
diff --git a/src/lib-settings/settings-parser.c b/src/lib-settings/settings-parser.c
new file mode 100644
index 0000000..7b63b30
--- /dev/null
+++ b/src/lib-settings/settings-parser.c
@@ -0,0 +1,2226 @@
+/* Copyright (c) 2002-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "hash.h"
+#include "net.h"
+#include "istream.h"
+#include "env-util.h"
+#include "execv-const.h"
+#include "str.h"
+#include "strescape.h"
+#include "var-expand.h"
+#include "settings-parser.h"
+
+#include <stdio.h>
+#include <unistd.h>
+#include <ctype.h>
+#include <fcntl.h>
+#include <sys/stat.h>
+#include <sys/wait.h>
+
+#define IS_WHITE(c) ((c) == ' ' || (c) == '\t')
+
+struct setting_link {
+ struct setting_link *parent;
+ const struct setting_parser_info *info;
+
+ const char *full_key;
+
+ /* Points to array inside parent->set_struct.
+ SET_DEFLIST : array of set_structs
+ SET_STRLIST : array of const_strings */
+ ARRAY_TYPE(void_array) *array;
+ /* Pointer to structure containing the values */
+ void *set_struct;
+ /* Pointer to structure containing non-zero values for settings that
+ have been changed. */
+ void *change_struct;
+ /* SET_DEFLIST: array of change_structs */
+ ARRAY_TYPE(void_array) *change_array;
+};
+
+struct setting_parser_context {
+ pool_t set_pool, parser_pool;
+ enum settings_parser_flags flags;
+ bool str_vars_are_expanded;
+
+ struct setting_link *roots;
+ unsigned int root_count;
+ HASH_TABLE(char *, struct setting_link *) links;
+
+ unsigned int linenum;
+ const char *error;
+ const struct setting_parser_info *prev_info;
+};
+
+static const struct setting_parser_info strlist_info = {
+ .module_name = NULL,
+ .defines = NULL,
+ .defaults = NULL,
+
+ .type_offset = SIZE_MAX,
+ .struct_size = 0,
+
+ .parent_offset = SIZE_MAX
+};
+
+HASH_TABLE_DEFINE_TYPE(setting_link, struct setting_link *,
+ struct setting_link *);
+
+static void
+setting_parser_copy_defaults(struct setting_parser_context *ctx,
+ const struct setting_parser_info *info,
+ struct setting_link *link);
+static int
+settings_apply(struct setting_link *dest_link,
+ const struct setting_link *src_link,
+ pool_t pool, const char **conflict_key_r);
+
+struct setting_parser_context *
+settings_parser_init(pool_t set_pool, const struct setting_parser_info *root,
+ enum settings_parser_flags flags)
+{
+ return settings_parser_init_list(set_pool, &root, 1, flags);
+}
+
+static void
+copy_unique_defaults(struct setting_parser_context *ctx,
+ const struct setting_define *def,
+ struct setting_link *link)
+{
+ ARRAY_TYPE(void_array) *arr =
+ STRUCT_MEMBER_P(link->set_struct, def->offset);
+ ARRAY_TYPE(void_array) *carr = NULL;
+ struct setting_link *new_link;
+ struct setting_parser_info info;
+ const char *const *keyp, *key, *prefix;
+ void *const *children;
+ void *new_set, *new_changes = NULL;
+ char *full_key;
+ unsigned int i, count;
+
+ if (!array_is_created(arr))
+ return;
+
+ children = array_get(arr, &count);
+ if (link->change_struct != NULL) {
+ carr = STRUCT_MEMBER_P(link->change_struct, def->offset);
+ i_assert(!array_is_created(carr));
+ p_array_init(carr, ctx->set_pool, count + 4);
+ }
+ p_array_init(arr, ctx->set_pool, count + 4);
+
+ i_zero(&info);
+ info = *def->list_info;
+
+ for (i = 0; i < count; i++) T_BEGIN {
+ new_set = p_malloc(ctx->set_pool, info.struct_size);
+ array_push_back(arr, &new_set);
+
+ if (link->change_struct != NULL) {
+ i_assert(carr != NULL);
+ new_changes = p_malloc(ctx->set_pool, info.struct_size);
+ array_push_back(carr, &new_changes);
+ }
+
+ keyp = CONST_PTR_OFFSET(children[i], info.type_offset);
+ key = settings_section_escape(*keyp);
+
+ new_link = p_new(ctx->set_pool, struct setting_link, 1);
+ prefix = link->full_key == NULL ?
+ t_strconcat(def->key, SETTINGS_SEPARATOR_S, NULL) :
+ t_strconcat(link->full_key, SETTINGS_SEPARATOR_S,
+ def->key, SETTINGS_SEPARATOR_S,NULL);
+ full_key = p_strconcat(ctx->set_pool, prefix, key, NULL);
+ new_link->full_key = full_key;
+ new_link->parent = link;
+ new_link->info = def->list_info;
+ new_link->array = arr;
+ new_link->change_array = carr;
+ new_link->set_struct = new_set;
+ new_link->change_struct = new_changes;
+ i_assert(hash_table_lookup(ctx->links, full_key) == NULL);
+ hash_table_insert(ctx->links, full_key, new_link);
+
+ info.defaults = children[i];
+ setting_parser_copy_defaults(ctx, &info, new_link);
+ } T_END;
+}
+
+static void
+setting_parser_copy_defaults(struct setting_parser_context *ctx,
+ const struct setting_parser_info *info,
+ struct setting_link *link)
+{
+ const struct setting_define *def;
+ const char *p, **strp;
+
+ if (info->defaults == NULL)
+ return;
+
+ memcpy(link->set_struct, info->defaults, info->struct_size);
+ for (def = info->defines; def->key != NULL; def++) {
+ switch (def->type) {
+ case SET_ENUM: {
+ /* fix enums by dropping everything after the
+ first ':' */
+ strp = STRUCT_MEMBER_P(link->set_struct, def->offset);
+ p = strchr(*strp, ':');
+ if (p != NULL)
+ *strp = p_strdup_until(ctx->set_pool, *strp, p);
+ break;
+ }
+ case SET_STR_VARS: {
+ /* insert the unexpanded-character */
+ strp = STRUCT_MEMBER_P(link->set_struct, def->offset);
+ if (*strp != NULL) {
+ *strp = p_strconcat(ctx->set_pool,
+ SETTING_STRVAR_UNEXPANDED,
+ *strp, NULL);
+ }
+ break;
+ }
+ case SET_DEFLIST_UNIQUE:
+ copy_unique_defaults(ctx, def, link);
+ break;
+ default:
+ break;
+ }
+ }
+}
+
+struct setting_parser_context *
+settings_parser_init_list(pool_t set_pool,
+ const struct setting_parser_info *const *roots,
+ unsigned int count, enum settings_parser_flags flags)
+{
+ struct setting_parser_context *ctx;
+ unsigned int i;
+ pool_t parser_pool;
+
+ i_assert(count > 0);
+
+ parser_pool = pool_alloconly_create(MEMPOOL_GROWING"settings parser",
+ 1024);
+ ctx = p_new(parser_pool, struct setting_parser_context, 1);
+ ctx->set_pool = set_pool;
+ ctx->parser_pool = parser_pool;
+ ctx->flags = flags;
+ /* use case-insensitive comparisons. this is mainly because settings
+ may go through environment variables where their keys get
+ uppercased. of course the alternative would be to not uppercase
+ environment. probably doesn't make much difference which way is
+ chosen. */
+ hash_table_create(&ctx->links, ctx->parser_pool, 0,
+ strcase_hash, strcasecmp);
+
+ ctx->root_count = count;
+ ctx->roots = p_new(ctx->parser_pool, struct setting_link, count);
+ for (i = 0; i < count; i++) {
+ ctx->roots[i].info = roots[i];
+ if (roots[i]->struct_size == 0)
+ continue;
+
+ ctx->roots[i].set_struct =
+ p_malloc(ctx->set_pool, roots[i]->struct_size);
+ if ((flags & SETTINGS_PARSER_FLAG_TRACK_CHANGES) != 0) {
+ ctx->roots[i].change_struct =
+ p_malloc(ctx->set_pool, roots[i]->struct_size);
+ }
+ setting_parser_copy_defaults(ctx, roots[i], &ctx->roots[i]);
+ }
+
+ pool_ref(ctx->set_pool);
+ return ctx;
+}
+
+void settings_parser_deinit(struct setting_parser_context **_ctx)
+{
+ struct setting_parser_context *ctx = *_ctx;
+
+ *_ctx = NULL;
+ hash_table_destroy(&ctx->links);
+ pool_unref(&ctx->set_pool);
+ pool_unref(&ctx->parser_pool);
+}
+
+void *settings_parser_get(struct setting_parser_context *ctx)
+{
+ i_assert(ctx->root_count == 1);
+
+ return ctx->roots[0].set_struct;
+}
+
+void **settings_parser_get_list(const struct setting_parser_context *ctx)
+{
+ unsigned int i;
+ void **sets;
+
+ sets = t_new(void *, ctx->root_count + 1);
+ for (i = 0; i < ctx->root_count; i++)
+ sets[i] = ctx->roots[i].set_struct;
+ return sets;
+}
+
+void *settings_parser_get_changes(struct setting_parser_context *ctx)
+{
+ i_assert(ctx->root_count == 1);
+
+ return ctx->roots[0].change_struct;
+}
+
+const struct setting_parser_info *const *
+settings_parser_get_roots(const struct setting_parser_context *ctx)
+{
+ const struct setting_parser_info **infos;
+ unsigned int i;
+
+ infos = t_new(const struct setting_parser_info *, ctx->root_count + 1);
+ for (i = 0; i < ctx->root_count; i++)
+ infos[i] = ctx->roots[i].info;
+ return infos;
+}
+
+const char *settings_parser_get_error(struct setting_parser_context *ctx)
+{
+ return ctx->error;
+}
+
+static const struct setting_define *
+setting_define_find(const struct setting_parser_info *info, const char *key)
+{
+ const struct setting_define *list;
+
+ for (list = info->defines; list->key != NULL; list++) {
+ if (strcmp(list->key, key) == 0)
+ return list;
+ }
+ return NULL;
+}
+
+static int
+get_bool(struct setting_parser_context *ctx, const char *value, bool *result_r)
+{
+ /* FIXME: eventually we'd want to support only yes/no */
+ if (strcasecmp(value, "yes") == 0 ||
+ strcasecmp(value, "y") == 0 || strcmp(value, "1") == 0)
+ *result_r = TRUE;
+ else if (strcasecmp(value, "no") == 0)
+ *result_r = FALSE;
+ else {
+ ctx->error = p_strdup_printf(ctx->parser_pool,
+ "Invalid boolean value: %s (use yes or no)", value);
+ return -1;
+ }
+
+ return 0;
+}
+
+static int
+get_uint(struct setting_parser_context *ctx, const char *value,
+ unsigned int *result_r)
+{
+ if (str_to_uint(value, result_r) < 0) {
+ ctx->error = p_strdup_printf(ctx->parser_pool,
+ "Invalid number %s: %s", value,
+ str_num_error(value));
+ return -1;
+ }
+ return 0;
+}
+
+static int
+get_octal(struct setting_parser_context *ctx, const char *value,
+ unsigned int *result_r)
+{
+ unsigned long long octal;
+
+ if (*value != '0')
+ return get_uint(ctx, value, result_r);
+
+ if (str_to_ullong_oct(value, &octal) < 0) {
+ ctx->error = p_strconcat(ctx->parser_pool, "Invalid number: ",
+ value, NULL);
+ return -1;
+ }
+ *result_r = (unsigned int)octal;
+ return 0;
+}
+
+static int settings_get_time_full(const char *str, unsigned int *interval_r,
+ bool milliseconds, const char **error_r)
+{
+ uintmax_t num, multiply = milliseconds ? 1000 : 1;
+ const char *p;
+
+ if (str_parse_uintmax(str, &num, &p) < 0) {
+ *error_r = t_strconcat("Invalid time interval: ", str, NULL);
+ return -1;
+ }
+ while (*p == ' ') p++;
+ if (*p == '\0' && num != 0) {
+ *error_r = t_strdup_printf("Time interval '%s' is missing units "
+ "(add e.g. 's' for seconds)", str);
+ return -1;
+ }
+ switch (i_toupper(*p)) {
+ case 'S':
+ multiply *= 1;
+ if (strncasecmp(p, "secs", strlen(p)) == 0 ||
+ strncasecmp(p, "seconds", strlen(p)) == 0)
+ p = "";
+ break;
+ case 'M':
+ multiply *= 60;
+ if (strncasecmp(p, "mins", strlen(p)) == 0 ||
+ strncasecmp(p, "minutes", strlen(p)) == 0)
+ p = "";
+ else if (strncasecmp(p, "msecs", strlen(p)) == 0 ||
+ strncasecmp(p, "mseconds", strlen(p)) == 0 ||
+ strncasecmp(p, "millisecs", strlen(p)) == 0 ||
+ strncasecmp(p, "milliseconds", strlen(p)) == 0) {
+ if (milliseconds || (num % 1000) == 0) {
+ if (!milliseconds) {
+ /* allow ms also for seconds, as long
+ as it's divisible by seconds */
+ num /= 1000;
+ }
+ multiply = 1;
+ p = "";
+ break;
+ }
+ *error_r = t_strdup_printf(
+ "Milliseconds not supported for this setting: %s", str);
+ return -1;
+ }
+ break;
+ case 'H':
+ multiply *= 60*60;
+ if (strncasecmp(p, "hours", strlen(p)) == 0)
+ p = "";
+ break;
+ case 'D':
+ multiply *= 60*60*24;
+ if (strncasecmp(p, "days", strlen(p)) == 0)
+ p = "";
+ break;
+ case 'W':
+ multiply *= 60*60*24*7;
+ if (strncasecmp(p, "weeks", strlen(p)) == 0)
+ p = "";
+ break;
+ }
+
+ if (*p != '\0') {
+ *error_r = t_strconcat("Invalid time interval: ", str, NULL);
+ return -1;
+ }
+ if (num > UINT_MAX / multiply) {
+ *error_r = t_strconcat("Time interval is too large: ",
+ str, NULL);
+ return -1;
+ }
+ *interval_r = num * multiply;
+ return 0;
+}
+
+int settings_get_time(const char *str, unsigned int *secs_r,
+ const char **error_r)
+{
+ return settings_get_time_full(str, secs_r, FALSE, error_r);
+}
+
+int settings_get_time_msecs(const char *str, unsigned int *msecs_r,
+ const char **error_r)
+{
+ return settings_get_time_full(str, msecs_r, TRUE, error_r);
+}
+
+int settings_get_size(const char *str, uoff_t *bytes_r,
+ const char **error_r)
+{
+ uintmax_t num, multiply = 1;
+ const char *p;
+
+ if (str_parse_uintmax(str, &num, &p) < 0) {
+ *error_r = t_strconcat("Invalid size: ", str, NULL);
+ return -1;
+ }
+ while (*p == ' ') p++;
+ switch (i_toupper(*p)) {
+ case 'B':
+ multiply = 1;
+ p += 1;
+ break;
+ case 'K':
+ multiply = 1024;
+ p += 1;
+ break;
+ case 'M':
+ multiply = 1024*1024;
+ p += 1;
+ break;
+ case 'G':
+ multiply = 1024*1024*1024;
+ p += 1;
+ break;
+ case 'T':
+ multiply = 1024ULL*1024*1024*1024;
+ p += 1;
+ break;
+ }
+
+ if (multiply > 1) {
+ /* Allow: k, ki, kiB */
+ if (i_toupper(*p) == 'I')
+ p++;
+ if (i_toupper(*p) == 'B')
+ p++;
+ }
+ if (*p != '\0') {
+ *error_r = t_strconcat("Invalid size: ", str, NULL);
+ return -1;
+ }
+ if (num > (UOFF_T_MAX) / multiply) {
+ *error_r = t_strconcat("Size is too large: ", str, NULL);
+ return -1;
+ }
+ *bytes_r = num * multiply;
+ return 0;
+}
+
+static int get_enum(struct setting_parser_context *ctx, const char *value,
+ char **result_r, const char *allowed_values)
+{
+ const char *p;
+
+ while (allowed_values != NULL) {
+ p = strchr(allowed_values, ':');
+ if (p == NULL) {
+ if (strcmp(allowed_values, value) == 0)
+ break;
+
+ ctx->error = p_strconcat(ctx->parser_pool,
+ "Invalid value: ",
+ value, NULL);
+ return -1;
+ }
+
+ if (strncmp(allowed_values, value, p - allowed_values) == 0 &&
+ value[p - allowed_values] == '\0')
+ break;
+
+ allowed_values = p + 1;
+ }
+
+ *result_r = p_strdup(ctx->set_pool, value);
+ return 0;
+}
+
+static void
+setting_link_init_set_struct(struct setting_parser_context *ctx,
+ struct setting_link *link)
+{
+ void *ptr;
+
+ link->set_struct = p_malloc(ctx->set_pool, link->info->struct_size);
+ if ((ctx->flags & SETTINGS_PARSER_FLAG_TRACK_CHANGES) != 0) {
+ link->change_struct =
+ p_malloc(ctx->set_pool, link->info->struct_size);
+ array_push_back(link->change_array, &link->change_struct);
+ }
+
+ setting_parser_copy_defaults(ctx, link->info, link);
+ array_push_back(link->array, &link->set_struct);
+
+ if (link->info->parent_offset != SIZE_MAX && link->parent != NULL) {
+ ptr = STRUCT_MEMBER_P(link->set_struct,
+ link->info->parent_offset);
+ *((void **)ptr) = link->parent->set_struct;
+ }
+}
+
+static int ATTR_NULL(2)
+setting_link_add(struct setting_parser_context *ctx,
+ const struct setting_define *def,
+ const struct setting_link *link_copy, char *key)
+{
+ struct setting_link *link;
+
+ link = hash_table_lookup(ctx->links, key);
+ if (link != NULL) {
+ if (link->parent == link_copy->parent &&
+ link->info == link_copy->info &&
+ (def == NULL || def->type == SET_DEFLIST_UNIQUE))
+ return 0;
+ ctx->error = p_strconcat(ctx->parser_pool, key,
+ " already exists", NULL);
+ return -1;
+ }
+
+ link = p_new(ctx->parser_pool, struct setting_link, 1);
+ *link = *link_copy;
+ link->full_key = key;
+ i_assert(hash_table_lookup(ctx->links, key) == NULL);
+ hash_table_insert(ctx->links, key, link);
+
+ if (link->info->struct_size != 0)
+ setting_link_init_set_struct(ctx, link);
+ return 0;
+}
+
+static int ATTR_NULL(3, 8)
+get_deflist(struct setting_parser_context *ctx, struct setting_link *parent,
+ const struct setting_define *def,
+ const struct setting_parser_info *info,
+ const char *key, const char *value, ARRAY_TYPE(void_array) *result,
+ ARRAY_TYPE(void_array) *change_result)
+{
+ struct setting_link new_link;
+ const char *const *list;
+ char *full_key;
+
+ i_assert(info->defines != NULL || info == &strlist_info);
+
+ if (!array_is_created(result))
+ p_array_init(result, ctx->set_pool, 5);
+ if (change_result != NULL && !array_is_created(change_result))
+ p_array_init(change_result, ctx->set_pool, 5);
+
+ i_zero(&new_link);
+ new_link.parent = parent;
+ new_link.info = info;
+ new_link.array = result;
+ new_link.change_array = change_result;
+
+ if (info == &strlist_info) {
+ /* there are no sections below strlist, so allow referencing it
+ without the key (e.g. plugin/foo instead of plugin/0/foo) */
+ full_key = p_strdup(ctx->parser_pool, key);
+ if (setting_link_add(ctx, def, &new_link, full_key) < 0)
+ return -1;
+ }
+
+ list = t_strsplit(value, ",\t ");
+ for (; *list != NULL; list++) {
+ if (**list == '\0')
+ continue;
+
+ full_key = p_strconcat(ctx->parser_pool, key,
+ SETTINGS_SEPARATOR_S, *list, NULL);
+ if (setting_link_add(ctx, def, &new_link, full_key) < 0)
+ return -1;
+ }
+ return 0;
+}
+
+static int
+get_in_port_zero(struct setting_parser_context *ctx, const char *value,
+ in_port_t *result_r)
+{
+ if (net_str2port_zero(value, result_r) < 0) {
+ ctx->error = p_strdup_printf(ctx->parser_pool,
+ "Invalid port number %s", value);
+ return -1;
+ }
+ return 0;
+}
+
+static int
+settings_parse(struct setting_parser_context *ctx, struct setting_link *link,
+ const struct setting_define *def,
+ const char *key, const char *value)
+{
+ void *ptr, *change_ptr;
+ const void *ptr2;
+ const char *error;
+
+ while (def->type == SET_ALIAS) {
+ i_assert(def != link->info->defines);
+ def--;
+ }
+
+ ctx->prev_info = link->info;
+
+ if (link->set_struct == NULL)
+ setting_link_init_set_struct(ctx, link);
+
+ change_ptr = link->change_struct == NULL ? NULL :
+ STRUCT_MEMBER_P(link->change_struct, def->offset);
+
+ ptr = STRUCT_MEMBER_P(link->set_struct, def->offset);
+ switch (def->type) {
+ case SET_BOOL:
+ if (get_bool(ctx, value, (bool *)ptr) < 0)
+ return -1;
+ break;
+ case SET_UINT:
+ if (get_uint(ctx, value, (unsigned int *)ptr) < 0)
+ return -1;
+ break;
+ case SET_UINT_OCT:
+ if (get_octal(ctx, value, (unsigned int *)ptr) < 0)
+ return -1;
+ break;
+ case SET_TIME:
+ if (settings_get_time(value, (unsigned int *)ptr, &error) < 0) {
+ ctx->error = p_strdup(ctx->parser_pool, error);
+ return -1;
+ }
+ break;
+ case SET_TIME_MSECS:
+ if (settings_get_time_msecs(value, (unsigned int *)ptr, &error) < 0) {
+ ctx->error = p_strdup(ctx->parser_pool, error);
+ return -1;
+ }
+ break;
+ case SET_SIZE:
+ if (settings_get_size(value, (uoff_t *)ptr, &error) < 0) {
+ ctx->error = p_strdup(ctx->parser_pool, error);
+ return -1;
+ }
+ break;
+ case SET_IN_PORT:
+ if (get_in_port_zero(ctx, value, (in_port_t *)ptr) < 0)
+ return -1;
+ break;
+ case SET_STR:
+ *((char **)ptr) = p_strdup(ctx->set_pool, value);
+ break;
+ case SET_STR_VARS:
+ *((char **)ptr) = p_strconcat(ctx->set_pool,
+ ctx->str_vars_are_expanded ?
+ SETTING_STRVAR_EXPANDED :
+ SETTING_STRVAR_UNEXPANDED,
+ value, NULL);
+ break;
+ case SET_ENUM:
+ /* get the available values from default string */
+ i_assert(link->info->defaults != NULL);
+ ptr2 = CONST_STRUCT_MEMBER_P(link->info->defaults, def->offset);
+ if (get_enum(ctx, value, (char **)ptr,
+ *(const char *const *)ptr2) < 0)
+ return -1;
+ break;
+ case SET_DEFLIST:
+ case SET_DEFLIST_UNIQUE:
+ ctx->prev_info = def->list_info;
+ return get_deflist(ctx, link, def, def->list_info,
+ key, value, (ARRAY_TYPE(void_array) *)ptr,
+ (ARRAY_TYPE(void_array) *)change_ptr);
+ case SET_STRLIST: {
+ ctx->prev_info = &strlist_info;
+ if (get_deflist(ctx, link, NULL, &strlist_info, key, value,
+ (ARRAY_TYPE(void_array) *)ptr, NULL) < 0)
+ return -1;
+ break;
+ }
+ case SET_ALIAS:
+ i_unreached();
+ break;
+ }
+
+ if (change_ptr != NULL)
+ *((char *)change_ptr) = 1;
+ return 0;
+}
+
+static bool
+settings_find_key_nth(struct setting_parser_context *ctx, const char *key,
+ unsigned int *n, const struct setting_define **def_r,
+ struct setting_link **link_r)
+{
+ const struct setting_define *def;
+ struct setting_link *link;
+ const char *end, *parent_key;
+ unsigned int i;
+
+ /* try to find from roots */
+ for (i = *n; i < ctx->root_count; i++) {
+ def = setting_define_find(ctx->roots[i].info, key);
+ if (def != NULL) {
+ *n = i + 1;
+ *def_r = def;
+ *link_r = &ctx->roots[i];
+ return TRUE;
+ }
+ }
+ if (*n > ctx->root_count)
+ return FALSE;
+ *n += 1;
+
+ /* try to find from links */
+ end = strrchr(key, SETTINGS_SEPARATOR);
+ if (end == NULL)
+ return FALSE;
+
+ parent_key = t_strdup_until(key, end);
+ link = hash_table_lookup(ctx->links, parent_key);
+ if (link == NULL) {
+ /* maybe this is the first strlist value */
+ unsigned int parent_n = 0;
+ const struct setting_define *parent_def;
+ struct setting_link *parent_link;
+
+ if (!settings_find_key_nth(ctx, parent_key, &parent_n,
+ &parent_def, &parent_link))
+ return FALSE;
+ if (parent_def == NULL) {
+ /* we'll get here with e.g. "plugin/a/b=val".
+ not sure if we should ever do anything here.. */
+ if (parent_link->full_key == NULL ||
+ strcmp(parent_link->full_key, parent_key) != 0)
+ return FALSE;
+ } else {
+ if (parent_def->type != SET_STRLIST)
+ return FALSE;
+ }
+
+ /* setting parent_key=0 adds it to links list */
+ if (settings_parse_keyvalue(ctx, parent_key, "0") <= 0)
+ return FALSE;
+
+ link = hash_table_lookup(ctx->links, parent_key);
+ i_assert(link != NULL);
+ }
+
+ *link_r = link;
+ if (link->info == &strlist_info) {
+ *def_r = NULL;
+ return TRUE;
+ } else {
+ *def_r = setting_define_find(link->info, end + 1);
+ return *def_r != NULL;
+ }
+}
+
+static bool
+settings_find_key(struct setting_parser_context *ctx, const char *key,
+ const struct setting_define **def_r,
+ struct setting_link **link_r)
+{
+ unsigned int n = 0;
+
+ return settings_find_key_nth(ctx, key, &n, def_r, link_r);
+}
+
+static void
+settings_parse_strlist(struct setting_parser_context *ctx,
+ struct setting_link *link,
+ const char *key, const char *value)
+{
+ void *const *items;
+ void *vkey, *vvalue;
+ unsigned int i, count;
+
+ key = strrchr(key, SETTINGS_SEPARATOR) + 1;
+ vvalue = p_strdup(ctx->set_pool, value);
+
+ /* replace if it already exists */
+ items = array_get(link->array, &count);
+ for (i = 0; i < count; i += 2) {
+ if (strcmp(items[i], key) == 0) {
+ array_idx_set(link->array, i + 1, &vvalue);
+ return;
+ }
+ }
+
+ vkey = p_strdup(ctx->set_pool, key);
+ array_push_back(link->array, &vkey);
+ array_push_back(link->array, &vvalue);
+}
+
+int settings_parse_keyvalue(struct setting_parser_context *ctx,
+ const char *key, const char *value)
+{
+ const struct setting_define *def;
+ struct setting_link *link;
+ unsigned int n = 0;
+
+ ctx->error = NULL;
+ ctx->prev_info = NULL;
+
+ if (!settings_find_key_nth(ctx, key, &n, &def, &link)) {
+ ctx->error = p_strconcat(ctx->parser_pool,
+ "Unknown setting: ", key, NULL);
+ return 0;
+ }
+
+ do {
+ if (def == NULL) {
+ i_assert(link->info == &strlist_info);
+ settings_parse_strlist(ctx, link, key, value);
+ return 1;
+ }
+
+ if (settings_parse(ctx, link, def, key, value) < 0)
+ return -1;
+ /* there may be more instances of the setting */
+ } while (settings_find_key_nth(ctx, key, &n, &def, &link));
+ return 1;
+}
+
+bool settings_parse_is_valid_key(struct setting_parser_context *ctx,
+ const char *key)
+{
+ const struct setting_define *def;
+ struct setting_link *link;
+
+ return settings_find_key(ctx, key, &def, &link);
+}
+
+const char *settings_parse_unalias(struct setting_parser_context *ctx,
+ const char *key)
+{
+ const struct setting_define *def;
+ struct setting_link *link;
+
+ if (!settings_find_key(ctx, key, &def, &link))
+ return NULL;
+ if (def == NULL) {
+ /* strlist */
+ i_assert(link->info == &strlist_info);
+ return key;
+ }
+
+ while (def->type == SET_ALIAS) {
+ i_assert(def != link->info->defines);
+ def--;
+ }
+ return def->key;
+}
+
+const void *
+settings_parse_get_value(struct setting_parser_context *ctx,
+ const char *key, enum setting_type *type_r)
+{
+ const struct setting_define *def;
+ struct setting_link *link;
+
+ if (!settings_find_key(ctx, key, &def, &link))
+ return NULL;
+ if (link->set_struct == NULL || def == NULL)
+ return NULL;
+
+ *type_r = def->type;
+ return STRUCT_MEMBER_P(link->set_struct, def->offset);
+}
+
+bool settings_parse_is_changed(struct setting_parser_context *ctx,
+ const char *key)
+{
+ const struct setting_define *def;
+ struct setting_link *link;
+ const unsigned char *p;
+
+ if (!settings_find_key(ctx, key, &def, &link))
+ return FALSE;
+ if (link->change_struct == NULL || def == NULL)
+ return FALSE;
+
+ p = STRUCT_MEMBER_P(link->change_struct, def->offset);
+ return *p != 0;
+}
+
+int settings_parse_line(struct setting_parser_context *ctx, const char *line)
+{
+ const char *key, *value;
+ int ret;
+
+ key = line;
+ value = strchr(line, '=');
+ if (value == NULL) {
+ ctx->error = "Missing '='";
+ return -1;
+ }
+
+ if (key == value) {
+ ctx->error = "Missing key name ('=' at the beginning of line)";
+ return -1;
+ }
+
+ T_BEGIN {
+ key = t_strdup_until(key, value);
+ ret = settings_parse_keyvalue(ctx, key, value + 1);
+ } T_END;
+ return ret;
+}
+
+const struct setting_parser_info *
+settings_parse_get_prev_info(struct setting_parser_context *ctx)
+{
+ return ctx->prev_info;
+}
+
+static const char *settings_translate_lf(const char *value)
+{
+ char *dest, *p;
+
+ if (strchr(value, SETTING_STREAM_LF_CHAR[0]) == NULL)
+ return value;
+
+ dest = t_strdup_noconst(value);
+ for (p = dest; *p != '\0'; p++) {
+ if (*p == SETTING_STREAM_LF_CHAR[0])
+ *p = '\n';
+ }
+ return dest;
+}
+
+int settings_parse_stream(struct setting_parser_context *ctx,
+ struct istream *input)
+{
+ bool ignore_unknown_keys =
+ (ctx->flags & SETTINGS_PARSER_FLAG_IGNORE_UNKNOWN_KEYS) != 0;
+ const char *line;
+ int ret;
+
+ while ((line = i_stream_next_line(input)) != NULL) {
+ if (*line == '\0') {
+ /* empty line finishes it */
+ return 0;
+ }
+ ctx->linenum++;
+ if (ctx->linenum == 1 && str_begins(line, "ERROR ")) {
+ ctx->error = p_strdup(ctx->parser_pool, line + 6);
+ return -1;
+ }
+
+ T_BEGIN {
+ line = settings_translate_lf(line);
+ ret = settings_parse_line(ctx, line);
+ } T_END;
+
+ if (ret < 0 || (ret == 0 && !ignore_unknown_keys)) {
+ ctx->error = p_strdup_printf(ctx->parser_pool,
+ "Line %u: %s", ctx->linenum, ctx->error);
+ return -1;
+ }
+ }
+ return 1;
+}
+
+int settings_parse_stream_read(struct setting_parser_context *ctx,
+ struct istream *input)
+{
+ int ret;
+
+ do {
+ if ((ret = settings_parse_stream(ctx, input)) < 0)
+ return -1;
+ if (ret == 0) {
+ /* empty line read */
+ return 0;
+ }
+ } while ((ret = i_stream_read(input)) > 0);
+
+ switch (ret) {
+ case -1:
+ if (ctx->error != NULL)
+ break;
+ if (input->stream_errno != 0) {
+ ctx->error = p_strdup_printf(ctx->parser_pool,
+ "read(%s) failed: %s", i_stream_get_name(input),
+ i_stream_get_error(input));
+ } else if (input->v_offset == 0) {
+ ctx->error = p_strdup_printf(ctx->parser_pool,
+ "read(%s) disconnected before receiving any data",
+ i_stream_get_name(input));
+ } else {
+ ctx->error = p_strdup_printf(ctx->parser_pool,
+ "read(%s) disconnected before receiving "
+ "end-of-settings line",
+ i_stream_get_name(input));
+ }
+ break;
+ case -2:
+ ctx->error = p_strdup_printf(ctx->parser_pool,
+ "Line %u: line too long",
+ ctx->linenum);
+ break;
+ case 0:
+ /* blocks */
+ return 1;
+ default:
+ i_unreached();
+ }
+ return -1;
+}
+
+int settings_parse_file(struct setting_parser_context *ctx,
+ const char *path, size_t max_line_length)
+{
+ struct istream *input;
+ int fd, ret;
+
+ fd = open(path, O_RDONLY);
+ if (fd < 0) {
+ ctx->error = p_strdup_printf(ctx->parser_pool,
+ "open(%s) failed: %m", path);
+ return -1;
+ }
+
+ input = i_stream_create_fd_autoclose(&fd, max_line_length);
+ i_stream_set_name(input, path);
+ ret = settings_parse_stream_read(ctx, input);
+ i_stream_unref(&input);
+
+ return ret;
+}
+
+static int environ_cmp(char *const *s1, char *const *s2)
+{
+ return -strcmp(*s1, *s2);
+}
+
+int settings_parse_environ(struct setting_parser_context *ctx)
+{
+ char **environ = *env_get_environ_p();
+ ARRAY_TYPE(string) sorted_envs_arr;
+ const char *key, *value;
+ char *const *sorted_envs;
+ unsigned int i, count;
+ int ret = 0;
+
+ if (environ == NULL)
+ return 0;
+
+ /* sort the settings first. this is necessary for putenv()
+ implementations (e.g. valgrind) which change the order of strings
+ in environ[] */
+ i_array_init(&sorted_envs_arr, 128);
+ for (i = 0; environ[i] != NULL; i++)
+ array_push_back(&sorted_envs_arr, &environ[i]);
+ array_sort(&sorted_envs_arr, environ_cmp);
+ sorted_envs = array_get(&sorted_envs_arr, &count);
+
+ for (i = 0; i < count && ret == 0; i++) {
+ value = strchr(sorted_envs[i], '=');
+ if (value != NULL) T_BEGIN {
+ key = t_strdup_until(sorted_envs[i], value++);
+ key = t_str_lcase(key);
+ if (settings_parse_keyvalue(ctx, key, value) < 0) {
+ ctx->error = p_strdup_printf(ctx->parser_pool,
+ "Invalid setting %s: %s",
+ key, ctx->error);
+ ret = -1;
+ }
+ } T_END;
+ }
+ array_free(&sorted_envs_arr);
+ return ret;
+}
+
+int settings_parse_exec(struct setting_parser_context *ctx,
+ const char *bin_path, const char *config_path,
+ const char *service)
+{
+ struct istream *input;
+ pid_t pid;
+ int ret, fd[2], status;
+
+ if (pipe(fd) < 0) {
+ i_error("pipe() failed: %m");
+ return -1;
+ }
+
+ pid = fork();
+ if (pid == (pid_t)-1) {
+ i_error("fork() failed: %m");
+ i_close_fd(&fd[0]);
+ i_close_fd(&fd[1]);
+ return -1;
+ }
+ if (pid == 0) {
+ /* child */
+ static const char *argv[] = {
+ NULL,
+ "-c", NULL,
+ "-p", NULL,
+ NULL
+ };
+ argv[0] = bin_path;
+ argv[2] = config_path;
+ argv[4] = service;
+ i_close_fd(&fd[0]);
+ if (dup2(fd[1], STDOUT_FILENO) < 0)
+ i_fatal("dup2() failed: %m");
+
+ execv_const(argv[0], argv);
+ }
+ i_close_fd(&fd[1]);
+
+ input = i_stream_create_fd_autoclose(&fd[0], SIZE_MAX);
+ i_stream_set_name(input, bin_path);
+ ret = settings_parse_stream_read(ctx, input);
+ i_stream_destroy(&input);
+
+ if (waitpid(pid, &status, 0) < 0) {
+ i_error("waitpid() failed: %m");
+ ret = -1;
+ } else if (status != 0) {
+ i_error("%s returned failure: %d", bin_path, status);
+ ret = -1;
+ }
+ return ret;
+}
+
+static bool
+settings_check_dynamic(const struct setting_parser_info *info, pool_t pool,
+ void *set, const char **error_r)
+{
+ unsigned int i;
+
+ if (info->dynamic_parsers == NULL)
+ return TRUE;
+
+ for (i = 0; info->dynamic_parsers[i].name != NULL; i++) {
+ struct dynamic_settings_parser *dyn = &info->dynamic_parsers[i];
+
+ if (!settings_check(dyn->info, pool,
+ PTR_OFFSET(set, dyn->struct_offset),
+ error_r))
+ return FALSE;
+ }
+ return TRUE;
+}
+
+bool settings_check(const struct setting_parser_info *info, pool_t pool,
+ void *set, const char **error_r)
+{
+ const struct setting_define *def;
+ const ARRAY_TYPE(void_array) *val;
+ void *const *children;
+ unsigned int i, count;
+ bool valid;
+
+ if (info->check_func != NULL) {
+ T_BEGIN {
+ valid = info->check_func(set, pool, error_r);
+ } T_END_PASS_STR_IF(!valid, error_r);
+ if (!valid)
+ return FALSE;
+ }
+
+ for (def = info->defines; def->key != NULL; def++) {
+ if (!SETTING_TYPE_IS_DEFLIST(def->type))
+ continue;
+
+ val = CONST_PTR_OFFSET(set, def->offset);
+ if (!array_is_created(val))
+ continue;
+
+ children = array_get(val, &count);
+ for (i = 0; i < count; i++) {
+ if (!settings_check(def->list_info, pool,
+ children[i], error_r))
+ return FALSE;
+ }
+ }
+ return settings_check_dynamic(info, pool, set, error_r);
+}
+
+bool settings_parser_check(struct setting_parser_context *ctx, pool_t pool,
+ const char **error_r)
+{
+ unsigned int i;
+
+ for (i = 0; i < ctx->root_count; i++) {
+ if (!settings_check(ctx->roots[i].info, pool,
+ ctx->roots[i].set_struct, error_r))
+ return FALSE;
+ }
+ return TRUE;
+}
+
+void settings_parse_set_expanded(struct setting_parser_context *ctx,
+ bool is_expanded)
+{
+ ctx->str_vars_are_expanded = is_expanded;
+}
+
+void settings_parse_set_key_expanded(struct setting_parser_context *ctx,
+ pool_t pool, const char *key)
+{
+ const struct setting_define *def;
+ struct setting_link *link;
+ const char **val;
+
+ if (!settings_find_key(ctx, key, &def, &link))
+ return;
+ if (def == NULL) {
+ /* parent is strlist, no expansion needed */
+ i_assert(link->info == &strlist_info);
+ return;
+ }
+
+ val = PTR_OFFSET(link->set_struct, def->offset);
+ if (def->type == SET_STR_VARS && *val != NULL) {
+ i_assert(**val == SETTING_STRVAR_UNEXPANDED[0] ||
+ **val == SETTING_STRVAR_EXPANDED[0]);
+ *val = p_strconcat(pool, SETTING_STRVAR_EXPANDED,
+ *val + 1, NULL);
+ }
+}
+
+void settings_parse_set_keys_expanded(struct setting_parser_context *ctx,
+ pool_t pool, const char *const *keys)
+{
+ for (; *keys != NULL; keys++)
+ settings_parse_set_key_expanded(ctx, pool, *keys);
+}
+
+static int ATTR_NULL(3, 4, 5)
+settings_var_expand_info(const struct setting_parser_info *info, void *set,
+ pool_t pool,
+ const struct var_expand_table *table,
+ const struct var_expand_func_table *func_table,
+ void *func_context, string_t *str,
+ const char **error_r)
+{
+ const struct setting_define *def;
+ void *value, *const *children;
+ const char *error;
+ unsigned int i, count;
+ int ret, final_ret = 1;
+
+ for (def = info->defines; def->key != NULL; def++) {
+ value = PTR_OFFSET(set, def->offset);
+ switch (def->type) {
+ case SET_BOOL:
+ case SET_UINT:
+ case SET_UINT_OCT:
+ case SET_TIME:
+ case SET_TIME_MSECS:
+ case SET_SIZE:
+ case SET_IN_PORT:
+ case SET_STR:
+ case SET_ENUM:
+ case SET_STRLIST:
+ case SET_ALIAS:
+ break;
+ case SET_STR_VARS: {
+ const char **val = value;
+
+ if (*val == NULL)
+ break;
+
+ if (table == NULL) {
+ i_assert(**val == SETTING_STRVAR_EXPANDED[0] ||
+ **val == SETTING_STRVAR_UNEXPANDED[0]);
+ *val += 1;
+ } else if (**val == SETTING_STRVAR_UNEXPANDED[0]) {
+ str_truncate(str, 0);
+ ret = var_expand_with_funcs(str, *val + 1, table,
+ func_table, func_context,
+ &error);
+ if (final_ret > ret) {
+ final_ret = ret;
+ *error_r = t_strdup_printf(
+ "%s: %s", def->key, error);
+ }
+ *val = p_strdup(pool, str_c(str));
+ } else {
+ i_assert(**val == SETTING_STRVAR_EXPANDED[0]);
+ *val += 1;
+ }
+ break;
+ }
+ case SET_DEFLIST:
+ case SET_DEFLIST_UNIQUE: {
+ const ARRAY_TYPE(void_array) *val = value;
+
+ if (!array_is_created(val))
+ break;
+
+ children = array_get(val, &count);
+ for (i = 0; i < count; i++) {
+ ret = settings_var_expand_info(def->list_info,
+ children[i], pool, table, func_table,
+ func_context, str, &error);
+ if (final_ret > ret) {
+ final_ret = ret;
+ *error_r = error;
+ }
+ }
+ break;
+ }
+ }
+ }
+
+ if (final_ret <= 0)
+ return final_ret;
+
+ if (info->expand_check_func != NULL) {
+ if (!info->expand_check_func(set, pool, error_r))
+ return -1;
+ }
+ if (info->dynamic_parsers != NULL) {
+ for (i = 0; info->dynamic_parsers[i].name != NULL; i++) {
+ struct dynamic_settings_parser *dyn = &info->dynamic_parsers[i];
+ const struct setting_parser_info *dinfo = dyn->info;
+ void *dset = PTR_OFFSET(set, dyn->struct_offset);
+
+ if (dinfo->expand_check_func != NULL) {
+ if (!dinfo->expand_check_func(dset, pool, error_r))
+ return -1;
+ }
+ }
+ }
+
+ return final_ret;
+}
+
+int settings_var_expand(const struct setting_parser_info *info,
+ void *set, pool_t pool,
+ const struct var_expand_table *table,
+ const char **error_r)
+{
+ return settings_var_expand_with_funcs(info, set, pool, table,
+ NULL, NULL, error_r);
+}
+
+int settings_var_expand_with_funcs(const struct setting_parser_info *info,
+ void *set, pool_t pool,
+ const struct var_expand_table *table,
+ const struct var_expand_func_table *func_table,
+ void *func_context, const char **error_r)
+{
+ int ret;
+
+ T_BEGIN {
+ string_t *str = t_str_new(256);
+
+ ret = settings_var_expand_info(info, set, pool, table,
+ func_table, func_context, str,
+ error_r);
+ } T_END_PASS_STR_IF(ret <= 0, error_r);
+ return ret;
+}
+
+void settings_parse_var_skip(struct setting_parser_context *ctx)
+{
+ unsigned int i;
+ const char *error;
+
+ for (i = 0; i < ctx->root_count; i++) {
+ (void)settings_var_expand_info(ctx->roots[i].info,
+ ctx->roots[i].set_struct,
+ NULL, NULL, NULL, NULL, NULL,
+ &error);
+ }
+}
+
+bool settings_vars_have_key(const struct setting_parser_info *info, void *set,
+ char var_key, const char *long_var_key,
+ const char **key_r, const char **value_r)
+{
+ const struct setting_define *def;
+ const void *value;
+ void *const *children;
+ unsigned int i, count;
+
+ for (def = info->defines; def->key != NULL; def++) {
+ value = CONST_PTR_OFFSET(set, def->offset);
+ switch (def->type) {
+ case SET_BOOL:
+ case SET_UINT:
+ case SET_UINT_OCT:
+ case SET_TIME:
+ case SET_TIME_MSECS:
+ case SET_SIZE:
+ case SET_IN_PORT:
+ case SET_STR:
+ case SET_ENUM:
+ case SET_STRLIST:
+ case SET_ALIAS:
+ break;
+ case SET_STR_VARS: {
+ const char *const *val = value;
+
+ if (*val == NULL)
+ break;
+
+ if (**val == SETTING_STRVAR_UNEXPANDED[0]) {
+ if (var_has_key(*val + 1, var_key,
+ long_var_key)) {
+ *key_r = def->key;
+ *value_r = *val + 1;
+ return TRUE;
+ }
+ } else {
+ i_assert(**val == SETTING_STRVAR_EXPANDED[0]);
+ }
+ break;
+ }
+ case SET_DEFLIST:
+ case SET_DEFLIST_UNIQUE: {
+ const ARRAY_TYPE(void_array) *val = value;
+
+ if (!array_is_created(val))
+ break;
+
+ children = array_get(val, &count);
+ for (i = 0; i < count; i++) {
+ if (settings_vars_have_key(def->list_info,
+ children[i], var_key,
+ long_var_key,
+ key_r, value_r))
+ return TRUE;
+ }
+ break;
+ }
+ }
+ }
+ return FALSE;
+}
+
+static void settings_set_parent(const struct setting_parser_info *info,
+ void *child, void *parent)
+{
+ void **ptr;
+
+ if (info->parent_offset == SIZE_MAX)
+ return;
+
+ ptr = PTR_OFFSET(child, info->parent_offset);
+ *ptr = parent;
+}
+
+static bool
+setting_copy(enum setting_type type, const void *src, void *dest, pool_t pool,
+ bool keep_values)
+{
+ switch (type) {
+ case SET_BOOL: {
+ const bool *src_bool = src;
+ bool *dest_bool = dest;
+
+ *dest_bool = *src_bool;
+ break;
+ }
+ case SET_UINT:
+ case SET_UINT_OCT:
+ case SET_TIME:
+ case SET_TIME_MSECS: {
+ const unsigned int *src_uint = src;
+ unsigned int *dest_uint = dest;
+
+ *dest_uint = *src_uint;
+ break;
+ }
+ case SET_SIZE: {
+ const uoff_t *src_size = src;
+ uoff_t *dest_size = dest;
+
+ *dest_size = *src_size;
+ break;
+ }
+ case SET_IN_PORT: {
+ const in_port_t *src_size = src;
+ in_port_t *dest_size = dest;
+
+ *dest_size = *src_size;
+ break;
+ }
+ case SET_STR_VARS:
+ case SET_STR:
+ case SET_ENUM: {
+ const char *const *src_str = src;
+ const char **dest_str = dest;
+
+ if (keep_values)
+ *dest_str = *src_str;
+ else
+ *dest_str = p_strdup(pool, *src_str);
+ break;
+ }
+ case SET_DEFLIST:
+ case SET_DEFLIST_UNIQUE:
+ return FALSE;
+ case SET_STRLIST: {
+ const ARRAY_TYPE(const_string) *src_arr = src;
+ ARRAY_TYPE(const_string) *dest_arr = dest;
+ const char *const *strings, *const *dest_strings, *dup;
+ unsigned int i, j, count, dest_count;
+
+ if (!array_is_created(src_arr))
+ break;
+
+ strings = array_get(src_arr, &count);
+ i_assert(count % 2 == 0);
+ if (!array_is_created(dest_arr))
+ p_array_init(dest_arr, pool, count);
+ dest_count = array_count(dest_arr);
+ i_assert(dest_count % 2 == 0);
+ for (i = 0; i < count; i += 2) {
+ if (dest_count > 0) {
+ dest_strings = array_front(dest_arr);
+ for (j = 0; j < dest_count; j += 2) {
+ if (strcmp(strings[i], dest_strings[j]) == 0)
+ break;
+ }
+ if (j < dest_count)
+ continue;
+ }
+ dup = keep_values ? strings[i] : p_strdup(pool, strings[i]);
+ array_push_back(dest_arr, &dup);
+ dup = keep_values ? strings[i+1] : p_strdup(pool, strings[i+1]);
+ array_push_back(dest_arr, &dup);
+ }
+ break;
+ }
+ case SET_ALIAS:
+ break;
+ }
+ return TRUE;
+}
+
+static void *settings_dup_full(const struct setting_parser_info *info,
+ const void *set, pool_t pool, bool keep_values)
+{
+ const struct setting_define *def;
+ const void *src;
+ void *dest_set, *dest, *const *children;
+ unsigned int i, count;
+
+ if (info->struct_size == 0)
+ return NULL;
+
+ /* don't just copy everything from set to dest_set. it may contain
+ some non-setting fields allocated from the original pool. */
+ dest_set = p_malloc(pool, info->struct_size);
+ for (def = info->defines; def->key != NULL; def++) {
+ src = CONST_PTR_OFFSET(set, def->offset);
+ dest = PTR_OFFSET(dest_set, def->offset);
+
+ if (!setting_copy(def->type, src, dest, pool, keep_values)) {
+ const ARRAY_TYPE(void_array) *src_arr = src;
+ ARRAY_TYPE(void_array) *dest_arr = dest;
+ void *child_set;
+
+ if (!array_is_created(src_arr))
+ continue;
+
+ children = array_get(src_arr, &count);
+ p_array_init(dest_arr, pool, count);
+ for (i = 0; i < count; i++) {
+ child_set = settings_dup_full(def->list_info,
+ children[i], pool,
+ keep_values);
+ array_push_back(dest_arr, &child_set);
+ settings_set_parent(def->list_info, child_set,
+ dest_set);
+ }
+ }
+ }
+ return dest_set;
+}
+
+void *settings_dup(const struct setting_parser_info *info,
+ const void *set, pool_t pool)
+{
+ return settings_dup_full(info, set, pool, FALSE);
+}
+
+void *settings_dup_with_pointers(const struct setting_parser_info *info,
+ const void *set, pool_t pool)
+{
+ return settings_dup_full(info, set, pool, TRUE);
+}
+
+static void *
+settings_changes_dup(const struct setting_parser_info *info,
+ const void *change_set, pool_t pool)
+{
+ const struct setting_define *def;
+ const void *src;
+ void *dest_set, *dest, *const *children;
+ unsigned int i, count;
+
+ if (change_set == NULL || info->struct_size == 0)
+ return NULL;
+
+ dest_set = p_malloc(pool, info->struct_size);
+ for (def = info->defines; def->key != NULL; def++) {
+ src = CONST_PTR_OFFSET(change_set, def->offset);
+ dest = PTR_OFFSET(dest_set, def->offset);
+
+ switch (def->type) {
+ case SET_BOOL:
+ case SET_UINT:
+ case SET_UINT_OCT:
+ case SET_TIME:
+ case SET_TIME_MSECS:
+ case SET_SIZE:
+ case SET_IN_PORT:
+ case SET_STR_VARS:
+ case SET_STR:
+ case SET_ENUM:
+ case SET_STRLIST:
+ *((char *)dest) = *((const char *)src);
+ break;
+ case SET_DEFLIST:
+ case SET_DEFLIST_UNIQUE: {
+ const ARRAY_TYPE(void_array) *src_arr = src;
+ ARRAY_TYPE(void_array) *dest_arr = dest;
+ void *child_set;
+
+ if (!array_is_created(src_arr))
+ break;
+
+ children = array_get(src_arr, &count);
+ p_array_init(dest_arr, pool, count);
+ for (i = 0; i < count; i++) {
+ child_set = settings_changes_dup(def->list_info,
+ children[i],
+ pool);
+ array_push_back(dest_arr, &child_set);
+ }
+ break;
+ }
+ case SET_ALIAS:
+ break;
+ }
+ }
+ return dest_set;
+}
+
+static void
+info_update_real(pool_t pool, struct setting_parser_info *parent,
+ const struct dynamic_settings_parser *parsers)
+{
+ /* @UNSAFE */
+ ARRAY(struct setting_define) defines;
+ ARRAY_TYPE(dynamic_settings_parser) dynamic_parsers;
+ struct dynamic_settings_parser new_parser;
+ const struct setting_define *cur_defines;
+ struct setting_define *new_defines, new_define;
+ void *parent_defaults;
+ unsigned int i, j;
+ size_t offset, new_struct_size;
+
+ t_array_init(&defines, 128);
+ /* add existing defines */
+ for (j = 0; parent->defines[j].key != NULL; j++)
+ array_push_back(&defines, &parent->defines[j]);
+ new_struct_size = MEM_ALIGN(parent->struct_size);
+
+ /* add new dynamic defines */
+ for (i = 0; parsers[i].name != NULL; i++) {
+ i_assert(parsers[i].info->parent == parent);
+ cur_defines = parsers[i].info->defines;
+ for (j = 0; cur_defines[j].key != NULL; j++) {
+ new_define = cur_defines[j];
+ new_define.offset += new_struct_size;
+ array_push_back(&defines, &new_define);
+ }
+ new_struct_size += MEM_ALIGN(parsers[i].info->struct_size);
+ }
+ new_defines = p_new(pool, struct setting_define,
+ array_count(&defines) + 1);
+ memcpy(new_defines, array_front(&defines),
+ sizeof(*parent->defines) * array_count(&defines));
+ parent->defines = new_defines;
+
+ /* update defaults */
+ parent_defaults = p_malloc(pool, new_struct_size);
+ memcpy(parent_defaults, parent->defaults, parent->struct_size);
+ offset = MEM_ALIGN(parent->struct_size);
+ for (i = 0; parsers[i].name != NULL; i++) {
+ memcpy(PTR_OFFSET(parent_defaults, offset),
+ parsers[i].info->defaults, parsers[i].info->struct_size);
+ offset += MEM_ALIGN(parsers[i].info->struct_size);
+ }
+ parent->defaults = parent_defaults;
+
+ /* update dynamic parsers list */
+ t_array_init(&dynamic_parsers, 32);
+ if (parent->dynamic_parsers != NULL) {
+ for (i = 0; parent->dynamic_parsers[i].name != NULL; i++) {
+ array_push_back(&dynamic_parsers,
+ &parent->dynamic_parsers[i]);
+ }
+ }
+ offset = MEM_ALIGN(parent->struct_size);
+ for (i = 0; parsers[i].name != NULL; i++) {
+ new_parser = parsers[i];
+ new_parser.name = p_strdup(pool, new_parser.name);
+ new_parser.struct_offset = offset;
+ array_push_back(&dynamic_parsers, &new_parser);
+ offset += MEM_ALIGN(parsers[i].info->struct_size);
+ }
+ parent->dynamic_parsers =
+ p_new(pool, struct dynamic_settings_parser,
+ array_count(&dynamic_parsers) + 1);
+ memcpy(parent->dynamic_parsers, array_front(&dynamic_parsers),
+ sizeof(*parent->dynamic_parsers) *
+ array_count(&dynamic_parsers));
+ parent->struct_size = new_struct_size;
+}
+
+void settings_parser_info_update(pool_t pool,
+ struct setting_parser_info *parent,
+ const struct dynamic_settings_parser *parsers)
+{
+ if (parsers[0].name != NULL) T_BEGIN {
+ info_update_real(pool, parent, parsers);
+ } T_END;
+}
+
+static void
+settings_parser_update_children_parent(struct setting_parser_info *parent,
+ pool_t pool)
+{
+ struct setting_define *new_defs;
+ struct setting_parser_info *new_info;
+ unsigned int i, count;
+
+ for (count = 0; parent->defines[count].key != NULL; count++) ;
+
+ new_defs = p_new(pool, struct setting_define, count + 1);
+ memcpy(new_defs, parent->defines, sizeof(*new_defs) * count);
+ parent->defines = new_defs;
+
+ for (i = 0; i < count; i++) {
+ if (new_defs[i].list_info == NULL ||
+ new_defs[i].list_info->parent == NULL)
+ continue;
+
+ new_info = p_new(pool, struct setting_parser_info, 1);
+ *new_info = *new_defs[i].list_info;
+ new_info->parent = parent;
+ new_defs[i].list_info = new_info;
+ }
+}
+
+void settings_parser_dyn_update(pool_t pool,
+ const struct setting_parser_info *const **_roots,
+ const struct dynamic_settings_parser *dyn_parsers)
+{
+ const struct setting_parser_info *const *roots = *_roots;
+ const struct setting_parser_info *old_parent, **new_roots;
+ struct setting_parser_info *new_parent, *new_info;
+ struct dynamic_settings_parser *new_dyn_parsers;
+ unsigned int i, count;
+
+ /* settings_parser_info_update() modifies the parent structure.
+ since we may be using the same structure later, we want it to be
+ in its original state, so we'll have to copy all structures. */
+ old_parent = dyn_parsers[0].info->parent;
+ new_parent = p_new(pool, struct setting_parser_info, 1);
+ *new_parent = *old_parent;
+ settings_parser_update_children_parent(new_parent, pool);
+
+ /* update root */
+ for (count = 0; roots[count] != NULL; count++) ;
+ new_roots = p_new(pool, const struct setting_parser_info *, count + 1);
+ for (i = 0; i < count; i++) {
+ if (roots[i] == old_parent)
+ new_roots[i] = new_parent;
+ else
+ new_roots[i] = roots[i];
+ }
+ *_roots = new_roots;
+
+ /* update parent in dyn_parsers */
+ for (count = 0; dyn_parsers[count].name != NULL; count++) ;
+ new_dyn_parsers = p_new(pool, struct dynamic_settings_parser, count + 1);
+ for (i = 0; i < count; i++) {
+ new_dyn_parsers[i] = dyn_parsers[i];
+
+ new_info = p_new(pool, struct setting_parser_info, 1);
+ *new_info = *dyn_parsers[i].info;
+ new_info->parent = new_parent;
+ new_dyn_parsers[i].info = new_info;
+ }
+
+ settings_parser_info_update(pool, new_parent, new_dyn_parsers);
+}
+
+const void *settings_find_dynamic(const struct setting_parser_info *info,
+ const void *base_set, const char *name)
+{
+ unsigned int i;
+
+ if (info->dynamic_parsers == NULL)
+ return NULL;
+
+ for (i = 0; info->dynamic_parsers[i].name != NULL; i++) {
+ if (strcmp(info->dynamic_parsers[i].name, name) == 0) {
+ return CONST_PTR_OFFSET(base_set,
+ info->dynamic_parsers[i].struct_offset);
+ }
+ }
+ return NULL;
+}
+
+static struct setting_link *
+settings_link_get_new(struct setting_parser_context *new_ctx,
+ HASH_TABLE_TYPE(setting_link) links,
+ struct setting_link *old_link)
+{
+ struct setting_link *new_link;
+ void *const *old_sets, **new_sets;
+ unsigned int i, count, count2;
+ size_t diff;
+
+ new_link = hash_table_lookup(links, old_link);
+ if (new_link != NULL)
+ return new_link;
+
+ i_assert(old_link->parent != NULL);
+ i_assert(old_link->array != NULL);
+
+ new_link = p_new(new_ctx->parser_pool, struct setting_link, 1);
+ new_link->info = old_link->info;
+ new_link->parent = settings_link_get_new(new_ctx, links,
+ old_link->parent);
+
+ /* find the array from parent struct */
+ diff = (char *)old_link->array - (char *)old_link->parent->set_struct;
+ i_assert(diff + sizeof(*old_link->array) <= old_link->parent->info->struct_size);
+ new_link->array = PTR_OFFSET(new_link->parent->set_struct, diff);
+
+ if (old_link->set_struct != NULL) {
+ /* find our struct from array */
+ old_sets = array_get(old_link->array, &count);
+ new_sets = array_get_modifiable(new_link->array, &count2);
+ i_assert(count == count2);
+ for (i = 0; i < count; i++) {
+ if (old_sets[i] == old_link->set_struct) {
+ new_link->set_struct = new_sets[i];
+ break;
+ }
+ }
+ i_assert(i < count);
+ }
+ i_assert(hash_table_lookup(links, old_link) == NULL);
+ hash_table_insert(links, old_link, new_link);
+ return new_link;
+}
+
+struct setting_parser_context *
+settings_parser_dup(const struct setting_parser_context *old_ctx,
+ pool_t new_pool)
+{
+ struct setting_parser_context *new_ctx;
+ struct hash_iterate_context *iter;
+ HASH_TABLE_TYPE(setting_link) links;
+ struct setting_link *new_link, *value;
+ char *key;
+ unsigned int i;
+ pool_t parser_pool;
+ bool keep_values;
+
+ /* if source and destination pools are the same, there's no need to
+ duplicate values */
+ keep_values = new_pool == old_ctx->set_pool;
+
+ pool_ref(new_pool);
+ parser_pool = pool_alloconly_create(MEMPOOL_GROWING"dup settings parser",
+ 1024);
+ new_ctx = p_new(parser_pool, struct setting_parser_context, 1);
+ new_ctx->set_pool = new_pool;
+ new_ctx->parser_pool = parser_pool;
+ new_ctx->flags = old_ctx->flags;
+ new_ctx->str_vars_are_expanded = old_ctx->str_vars_are_expanded;
+ new_ctx->linenum = old_ctx->linenum;
+ new_ctx->error = p_strdup(new_ctx->parser_pool, old_ctx->error);
+ new_ctx->prev_info = old_ctx->prev_info;
+
+ hash_table_create_direct(&links, new_ctx->parser_pool, 0);
+
+ new_ctx->root_count = old_ctx->root_count;
+ new_ctx->roots = p_new(new_ctx->parser_pool, struct setting_link,
+ new_ctx->root_count);
+ for (i = 0; i < new_ctx->root_count; i++) {
+ i_assert(old_ctx->roots[i].parent == NULL);
+ i_assert(old_ctx->roots[i].array == NULL);
+
+ new_ctx->roots[i].info = old_ctx->roots[i].info;
+ new_ctx->roots[i].set_struct =
+ settings_dup_full(old_ctx->roots[i].info,
+ old_ctx->roots[i].set_struct,
+ new_ctx->set_pool, keep_values);
+ new_ctx->roots[i].change_struct =
+ settings_changes_dup(old_ctx->roots[i].info,
+ old_ctx->roots[i].change_struct,
+ new_ctx->set_pool);
+ hash_table_insert(links, &old_ctx->roots[i],
+ &new_ctx->roots[i]);
+ }
+
+ hash_table_create(&new_ctx->links, new_ctx->parser_pool, 0,
+ strcase_hash, strcasecmp);
+
+ iter = hash_table_iterate_init(old_ctx->links);
+ while (hash_table_iterate(iter, old_ctx->links, &key, &value)) {
+ new_link = settings_link_get_new(new_ctx, links, value);
+ key = p_strdup(new_ctx->parser_pool, key);
+ hash_table_insert(new_ctx->links, key, new_link);
+ }
+ hash_table_iterate_deinit(&iter);
+ hash_table_destroy(&links);
+ return new_ctx;
+}
+
+static void *
+settings_changes_init(const struct setting_parser_info *info,
+ const void *change_set, pool_t pool)
+{
+ const struct setting_define *def;
+ const ARRAY_TYPE(void_array) *src_arr;
+ ARRAY_TYPE(void_array) *dest_arr;
+ void *dest_set, *set, *const *children;
+ unsigned int i, count;
+
+ if (info->struct_size == 0)
+ return NULL;
+
+ dest_set = p_malloc(pool, info->struct_size);
+ for (def = info->defines; def->key != NULL; def++) {
+ if (!SETTING_TYPE_IS_DEFLIST(def->type))
+ continue;
+
+ src_arr = CONST_PTR_OFFSET(change_set, def->offset);
+ dest_arr = PTR_OFFSET(dest_set, def->offset);
+
+ if (array_is_created(src_arr)) {
+ children = array_get(src_arr, &count);
+ i_assert(!array_is_created(dest_arr));
+ p_array_init(dest_arr, pool, count);
+ for (i = 0; i < count; i++) {
+ set = settings_changes_init(def->list_info,
+ children[i], pool);
+ array_push_back(dest_arr, &set);
+ }
+ }
+ }
+ return dest_set;
+}
+
+static void settings_copy_deflist(const struct setting_define *def,
+ const struct setting_link *src_link,
+ struct setting_link *dest_link,
+ pool_t pool)
+{
+ const ARRAY_TYPE(void_array) *src_arr;
+ ARRAY_TYPE(void_array) *dest_arr;
+ void *const *children, *child_set;
+ unsigned int i, count;
+
+ src_arr = CONST_PTR_OFFSET(src_link->set_struct, def->offset);
+ dest_arr = PTR_OFFSET(dest_link->set_struct, def->offset);
+
+ if (!array_is_created(src_arr))
+ return;
+
+ children = array_get(src_arr, &count);
+ if (!array_is_created(dest_arr))
+ p_array_init(dest_arr, pool, count);
+ for (i = 0; i < count; i++) {
+ child_set = settings_dup(def->list_info, children[i], pool);
+ array_push_back(dest_arr, &child_set);
+ settings_set_parent(def->list_info, child_set,
+ dest_link->set_struct);
+ }
+
+ /* copy changes */
+ dest_arr = PTR_OFFSET(dest_link->change_struct, def->offset);
+ if (!array_is_created(dest_arr))
+ p_array_init(dest_arr, pool, count);
+ for (i = 0; i < count; i++) {
+ child_set = settings_changes_init(def->list_info,
+ children[i], pool);
+ array_push_back(dest_arr, &child_set);
+ }
+}
+
+static int
+settings_copy_deflist_unique(const struct setting_define *def,
+ const struct setting_link *src_link,
+ struct setting_link *dest_link,
+ pool_t pool, const char **conflict_key_r)
+{
+ struct setting_link child_dest_link, child_src_link;
+ const ARRAY_TYPE(void_array) *src_arr, *src_carr;
+ ARRAY_TYPE(void_array) *dest_arr, *dest_carr;
+ void *const *src_children, *const *src_cchildren;
+ void *const *dest_children, *const *dest_cchildren, *child_set;
+ const char *const *src_namep, *const *dest_namep;
+ unsigned int i, j, src_count, dest_count, ccount;
+ unsigned int type_offset;
+
+ i_assert(def->list_info->type_offset != SIZE_MAX);
+
+ src_arr = CONST_PTR_OFFSET(src_link->set_struct, def->offset);
+ src_carr = CONST_PTR_OFFSET(src_link->change_struct, def->offset);
+ dest_arr = PTR_OFFSET(dest_link->set_struct, def->offset);
+ dest_carr = PTR_OFFSET(dest_link->change_struct, def->offset);
+
+ if (!array_is_created(src_arr))
+ return 0;
+ type_offset = def->list_info->type_offset;
+
+ i_zero(&child_dest_link);
+ i_zero(&child_src_link);
+
+ child_dest_link.info = child_src_link.info = def->list_info;
+
+ src_children = array_get(src_arr, &src_count);
+ src_cchildren = array_get(src_carr, &ccount);
+ i_assert(src_count == ccount);
+ if (!array_is_created(dest_arr)) {
+ p_array_init(dest_arr, pool, src_count);
+ p_array_init(dest_carr, pool, src_count);
+ }
+ for (i = 0; i < src_count; i++) {
+ src_namep = CONST_PTR_OFFSET(src_children[i], type_offset);
+ dest_children = array_get(dest_arr, &dest_count);
+ dest_cchildren = array_get(dest_carr, &ccount);
+ i_assert(dest_count == ccount);
+ for (j = 0; j < dest_count; j++) {
+ dest_namep = CONST_PTR_OFFSET(dest_children[j],
+ type_offset);
+ if (strcmp(*src_namep, *dest_namep) == 0)
+ break;
+ }
+
+ if (j < dest_count && **src_namep != '\0') {
+ /* merge */
+ child_src_link.set_struct = src_children[i];
+ child_src_link.change_struct = src_cchildren[i];
+ child_dest_link.set_struct = dest_children[j];
+ child_dest_link.change_struct = dest_cchildren[j];
+ if (settings_apply(&child_dest_link, &child_src_link,
+ pool, conflict_key_r) < 0)
+ return -1;
+ } else {
+ /* append */
+ child_set = settings_dup(def->list_info,
+ src_children[i], pool);
+ array_push_back(dest_arr, &child_set);
+ settings_set_parent(def->list_info, child_set,
+ dest_link->set_struct);
+
+ child_set = settings_changes_init(def->list_info,
+ src_cchildren[i],
+ pool);
+ array_push_back(dest_carr, &child_set);
+ }
+ }
+ return 0;
+}
+
+static int
+settings_apply(struct setting_link *dest_link,
+ const struct setting_link *src_link,
+ pool_t pool, const char **conflict_key_r)
+{
+ const struct setting_define *def;
+ const void *src, *csrc;
+ void *dest, *cdest;
+
+ for (def = dest_link->info->defines; def->key != NULL; def++) {
+ csrc = CONST_PTR_OFFSET(src_link->change_struct, def->offset);
+ cdest = PTR_OFFSET(dest_link->change_struct, def->offset);
+
+ if (def->type == SET_DEFLIST || def->type == SET_STRLIST) {
+ /* just add the new values */
+ } else if (def->type == SET_DEFLIST_UNIQUE) {
+ /* merge sections */
+ } else if (*((const char *)csrc) == 0) {
+ /* unchanged */
+ continue;
+ } else if (def->type == SET_ALIAS) {
+ /* ignore aliases */
+ continue;
+ } else if (*((const char *)cdest) != 0) {
+ /* conflict */
+ if (conflict_key_r != NULL) {
+ *conflict_key_r = def->key;
+ return -1;
+ }
+ continue;
+ } else {
+ *((char *)cdest) = 1;
+ }
+
+ /* found a changed setting */
+ src = CONST_PTR_OFFSET(src_link->set_struct, def->offset);
+ dest = PTR_OFFSET(dest_link->set_struct, def->offset);
+
+ if (setting_copy(def->type, src, dest, pool, FALSE)) {
+ /* non-list */
+ } else if (def->type == SET_DEFLIST) {
+ settings_copy_deflist(def, src_link, dest_link, pool);
+ } else {
+ i_assert(def->type == SET_DEFLIST_UNIQUE);
+ if (settings_copy_deflist_unique(def, src_link,
+ dest_link, pool,
+ conflict_key_r) < 0)
+ return -1;
+ }
+ }
+ return 0;
+}
+
+int settings_parser_apply_changes(struct setting_parser_context *dest,
+ const struct setting_parser_context *src,
+ pool_t pool, const char **conflict_key_r)
+{
+ unsigned int i;
+
+ i_assert(src->root_count == dest->root_count);
+ for (i = 0; i < dest->root_count; i++) {
+ i_assert(src->roots[i].info == dest->roots[i].info);
+ if (settings_apply(&dest->roots[i], &src->roots[i], pool,
+ conflict_key_r) < 0)
+ return -1;
+ }
+ return 0;
+}
+
+const char *settings_section_escape(const char *name)
+{
+#define CHAR_NEED_ESCAPE(c) \
+ ((c) == '=' || (c) == SETTINGS_SEPARATOR || (c) == '\\' || (c) == ' ' || (c) == ',')
+ string_t *str;
+ unsigned int i;
+
+ for (i = 0; name[i] != '\0'; i++) {
+ if (CHAR_NEED_ESCAPE(name[i]))
+ break;
+ }
+ if (name[i] == '\0')
+ return name;
+
+ str = t_str_new(i + strlen(name+i) + 8);
+ str_append_data(str, name, i);
+ for (; name[i] != '\0'; i++) {
+ switch (name[i]) {
+ case '=':
+ str_append(str, "\\e");
+ break;
+ case SETTINGS_SEPARATOR:
+ str_append(str, "\\s");
+ break;
+ case '\\':
+ str_append(str, "\\\\");
+ break;
+ case ' ':
+ str_append(str, "\\_");
+ break;
+ case ',':
+ str_append(str, "\\+");
+ break;
+ default:
+ str_append_c(str, name[i]);
+ break;
+ }
+ }
+ return str_c(str);
+}
+
diff --git a/src/lib-settings/settings-parser.h b/src/lib-settings/settings-parser.h
new file mode 100644
index 0000000..d7ffcb5
--- /dev/null
+++ b/src/lib-settings/settings-parser.h
@@ -0,0 +1,281 @@
+#ifndef SETTINGS_PARSER_H
+#define SETTINGS_PARSER_H
+
+struct var_expand_table;
+struct var_expand_func_table;
+
+#define SETTINGS_SEPARATOR '/'
+#define SETTINGS_SEPARATOR_S "/"
+
+/* STR_VARS pointer begins with either of these initially. Before actually
+ using the variables all variables in all unexpanded strings need to be
+ expanded. Afterwards the string pointers should be increased to skip
+ the initial '1' so it'll be easy to use them. */
+#define SETTING_STRVAR_UNEXPANDED "0"
+#define SETTING_STRVAR_EXPANDED "1"
+
+/* When parsing streams, this character is translated to LF. */
+#define SETTING_STREAM_LF_CHAR "\003"
+
+enum setting_type {
+ SET_BOOL,
+ SET_UINT,
+ SET_UINT_OCT,
+ SET_TIME,
+ SET_TIME_MSECS,
+ SET_SIZE,
+ SET_IN_PORT, /* internet port */
+ SET_STR,
+ SET_STR_VARS, /* string with %variables */
+ SET_ENUM,
+ SET_DEFLIST, /* of type array_t */
+ SET_DEFLIST_UNIQUE,
+ SET_STRLIST, /* of type ARRAY_TYPE(const_string) */
+ SET_ALIAS /* alias name for above setting definition */
+};
+enum setting_flags {
+ SET_FLAG_HIDDEN = BIT(0),
+};
+#define SETTING_TYPE_IS_DEFLIST(type) \
+ ((type) == SET_DEFLIST || (type) == SET_DEFLIST_UNIQUE)
+
+#define SETTING_DEFINE_LIST_END { 0, 0, NULL, 0, NULL }
+
+struct setting_define {
+ enum setting_type type;
+ enum setting_flags flags;
+ const char *key;
+
+ size_t offset;
+ const struct setting_parser_info *list_info;
+};
+
+#define SETTING_DEFINE_STRUCT_TYPE(_enum_type, _flags, _c_type, _key, _name, _struct_name) \
+ { .type = (_enum_type) + COMPILE_ERROR_IF_TYPES_NOT_COMPATIBLE( \
+ ((_struct_name *)0)->_name, _c_type), \
+ .flags = _flags, .key = _key, \
+ .offset = offsetof(_struct_name, _name) }
+
+#define SETTING_DEFINE_STRUCT_BOOL(key, name, struct_name) \
+ SETTING_DEFINE_STRUCT_TYPE(SET_BOOL, 0, bool, key, name, struct_name)
+#define SETTING_DEFINE_STRUCT_UINT(key, name, struct_name) \
+ SETTING_DEFINE_STRUCT_TYPE(SET_UINT, 0, unsigned int, key, name, struct_name)
+#define SETTING_DEFINE_STRUCT_UINT_OCT(key, name, struct_name) \
+ SETTING_DEFINE_STRUCT_TYPE(SET_UINT_OCT, 0, unsigned int, key, name, struct_name)
+#define SETTING_DEFINE_STRUCT_TIME(key, name, struct_name) \
+ SETTING_DEFINE_STRUCT_TYPE(SET_TIME, 0, unsigned int, key, name, struct_name)
+#define SETTING_DEFINE_STRUCT_TIME_MSECS(key, name, struct_name) \
+ SETTING_DEFINE_STRUCT_TYPE(SET_TIME_MSECS, 0, unsigned int, key, name, struct_name)
+#define SETTING_DEFINE_STRUCT_SIZE(key, name, struct_name) \
+ SETTING_DEFINE_STRUCT_TYPE(SET_SIZE, 0, uoff_t, key, name, struct_name)
+#define SETTING_DEFINE_STRUCT_IN_PORT(key, name, struct_name) \
+ SETTING_DEFINE_STRUCT_TYPE(SET_IN_PORT, 0, in_port_t, key, name, struct_name)
+#define SETTING_DEFINE_STRUCT_STR(key, name, struct_name) \
+ SETTING_DEFINE_STRUCT_TYPE(SET_STR, 0, const char *, key, name, struct_name)
+#define SETTING_DEFINE_STRUCT_STR_VARS(key, name, struct_name) \
+ SETTING_DEFINE_STRUCT_TYPE(SET_STR_VARS, 0, const char *, key, name, struct_name)
+#define SETTING_DEFINE_STRUCT_ENUM(key, name, struct_name) \
+ SETTING_DEFINE_STRUCT_TYPE(SET_ENUM, 0, const char *, key, name, struct_name)
+
+#define SETTING_DEFINE_STRUCT_BOOL_HIDDEN(key, name, struct_name) \
+ SETTING_DEFINE_STRUCT_TYPE(SET_BOOL, SET_FLAG_HIDDEN, bool, key, name, struct_name)
+#define SETTING_DEFINE_STRUCT_UINT_HIDDEN(key, name, struct_name) \
+ SETTING_DEFINE_STRUCT_TYPE(SET_UINT, SET_FLAG_HIDDEN, unsigned int, key, name, struct_name)
+#define SETTING_DEFINE_STRUCT_UINT_OCT_HIDDEN(key, name, struct_name) \
+ SETTING_DEFINE_STRUCT_TYPE(SET_UINT_OCT, SET_FLAG_HIDDEN, unsigned int, key, name, struct_name)
+#define SETTING_DEFINE_STRUCT_TIME_HIDDEN(key, name, struct_name) \
+ SETTING_DEFINE_STRUCT_TYPE(SET_TIME, SET_FLAG_HIDDEN, unsigned int, key, name, struct_name)
+#define SETTING_DEFINE_STRUCT_TIME_MSECS_HIDDEN(key, name, struct_name) \
+ SETTING_DEFINE_STRUCT_TYPE(SET_TIME_MSECS, SET_FLAG_HIDDEN, unsigned int, key, name, struct_name)
+#define SETTING_DEFINE_STRUCT_SIZE_HIDDEN(key, name, struct_name) \
+ SETTING_DEFINE_STRUCT_TYPE(SET_SIZE, SET_FLAG_HIDDEN, uoff_t, key, name, struct_name)
+#define SETTING_DEFINE_STRUCT_IN_PORT_HIDDEN(key, name, struct_name) \
+ SETTING_DEFINE_STRUCT_TYPE(SET_IN_PORT, SET_FLAG_HIDDEN, in_port_t, key, name, struct_name)
+#define SETTING_DEFINE_STRUCT_STR_HIDDEN(key, name, struct_name) \
+ SETTING_DEFINE_STRUCT_TYPE(SET_STR, SET_FLAG_HIDDEN, const char *, key, name, struct_name)
+#define SETTING_DEFINE_STRUCT_STR_VARS_HIDDEN(key, name, struct_name) \
+ SETTING_DEFINE_STRUCT_TYPE(SET_STR_VARS, SET_FLAG_HIDDEN, const char *, key, name, struct_name)
+#define SETTING_DEFINE_STRUCT_ENUM_HIDDEN(key, name, struct_name) \
+ SETTING_DEFINE_STRUCT_TYPE(SET_ENUM, SET_FLAG_HIDDEN, const char *, key, name, struct_name)
+
+struct setting_parser_info {
+ const char *module_name;
+ const struct setting_define *defines;
+ const void *defaults;
+
+ size_t type_offset;
+ size_t struct_size;
+
+ size_t parent_offset;
+ const struct setting_parser_info *parent;
+
+ bool (*check_func)(void *set, pool_t pool, const char **error_r);
+ bool (*expand_check_func)(void *set, pool_t pool, const char **error_r);
+ const struct setting_parser_info *const *dependencies;
+ struct dynamic_settings_parser *dynamic_parsers;
+
+};
+ARRAY_DEFINE_TYPE(setting_parser_info, struct setting_parser_info);
+
+/* name=NULL-terminated list of parsers. These follow the static settings.
+ After this list follows the actual settings. */
+struct dynamic_settings_parser {
+ const char *name;
+ const struct setting_parser_info *info;
+ size_t struct_offset;
+};
+ARRAY_DEFINE_TYPE(dynamic_settings_parser, struct dynamic_settings_parser);
+
+enum settings_parser_flags {
+ SETTINGS_PARSER_FLAG_IGNORE_UNKNOWN_KEYS = 0x01,
+ SETTINGS_PARSER_FLAG_TRACK_CHANGES = 0x02
+};
+
+struct setting_parser_context;
+
+struct setting_parser_context *
+settings_parser_init(pool_t set_pool, const struct setting_parser_info *root,
+ enum settings_parser_flags flags);
+struct setting_parser_context *
+settings_parser_init_list(pool_t set_pool,
+ const struct setting_parser_info *const *roots,
+ unsigned int count, enum settings_parser_flags flags);
+void settings_parser_deinit(struct setting_parser_context **ctx);
+
+/* Return pointer to root setting structure. */
+void *settings_parser_get(struct setting_parser_context *ctx);
+/* If there are multiple roots, return a NULL-terminated list to all of
+ their settings. */
+void **settings_parser_get_list(const struct setting_parser_context *ctx);
+/* Like settings_parser_get(), but return change struct. */
+void *settings_parser_get_changes(struct setting_parser_context *ctx);
+/* Returns the setting parser's roots (same as given to init()). */
+const struct setting_parser_info *const *
+settings_parser_get_roots(const struct setting_parser_context *ctx);
+
+/* Return the last error. */
+const char *settings_parser_get_error(struct setting_parser_context *ctx);
+/* Return the parser info used for the previously parsed line. */
+const struct setting_parser_info *
+settings_parse_get_prev_info(struct setting_parser_context *ctx);
+
+/* Returns TRUE if the given key is a valid setting. */
+bool settings_parse_is_valid_key(struct setting_parser_context *ctx,
+ const char *key);
+/* If key is an alias, return the primary key name. If key exists, return key
+ itself. If key doesn't exist, return NULL. */
+const char *settings_parse_unalias(struct setting_parser_context *ctx,
+ const char *key);
+/* Returns pointer to value for a key, or NULL if not found. */
+const void *
+settings_parse_get_value(struct setting_parser_context *ctx,
+ const char *key, enum setting_type *type_r);
+/* Returns TRUE if setting has been changed by this parser. */
+bool settings_parse_is_changed(struct setting_parser_context *ctx,
+ const char *key);
+/* Parse a single line. Returns 1 if OK, 0 if key is unknown, -1 if error. */
+int settings_parse_line(struct setting_parser_context *ctx, const char *line);
+/* Parse key/value pair. Returns 1 if OK, 0 if key is unknown, -1 if error. */
+int settings_parse_keyvalue(struct setting_parser_context *ctx,
+ const char *key, const char *value);
+/* Parse data already read in input stream. */
+int settings_parse_stream(struct setting_parser_context *ctx,
+ struct istream *input);
+/* Read data from input stream and parser it. returns -1 = error,
+ 0 = done, 1 = not finished yet (stream is non-blocking) */
+int settings_parse_stream_read(struct setting_parser_context *ctx,
+ struct istream *input);
+/* Open file and parse it. */
+int settings_parse_file(struct setting_parser_context *ctx,
+ const char *path, size_t max_line_length);
+int settings_parse_environ(struct setting_parser_context *ctx);
+/* Execute the given binary and wait for it to return the configuration. */
+int settings_parse_exec(struct setting_parser_context *ctx,
+ const char *bin_path, const char *config_path,
+ const char *service);
+/* Call all check_func()s to see if currently parsed settings are valid. */
+bool settings_parser_check(struct setting_parser_context *ctx, pool_t pool,
+ const char **error_r);
+bool settings_check(const struct setting_parser_info *info, pool_t pool,
+ void *set, const char **error_r);
+
+/* While parsing values, specifies if STR_VARS strings are already expanded. */
+void settings_parse_set_expanded(struct setting_parser_context *ctx,
+ bool is_expanded);
+/* Mark all the parsed settings with given keys as being already expanded. */
+void settings_parse_set_key_expanded(struct setting_parser_context *ctx,
+ pool_t pool, const char *key);
+void settings_parse_set_keys_expanded(struct setting_parser_context *ctx,
+ pool_t pool, const char *const *keys);
+/* Update variable string pointers to skip over the '1' or '0'.
+ This is mainly useful when you want to run settings_parser_check() without
+ actually knowing what the variables are. */
+void settings_parse_var_skip(struct setting_parser_context *ctx);
+/* Expand all unexpanded variables using the given table. Update the string
+ pointers so that they can be used without skipping over the '1'.
+ Returns the same as var_expand(). */
+int settings_var_expand(const struct setting_parser_info *info,
+ void *set, pool_t pool,
+ const struct var_expand_table *table,
+ const char **error_r);
+int settings_var_expand_with_funcs(const struct setting_parser_info *info,
+ void *set, pool_t pool,
+ const struct var_expand_table *table,
+ const struct var_expand_func_table *func_table,
+ void *func_context, const char **error_r);
+/* Go through all the settings and return the first one that has an unexpanded
+ setting containing the given %key. */
+bool settings_vars_have_key(const struct setting_parser_info *info, void *set,
+ char var_key, const char *long_var_key,
+ const char **key_r, const char **value_r);
+/* Duplicate the entire settings structure. */
+void *settings_dup(const struct setting_parser_info *info,
+ const void *set, pool_t pool);
+/* Same as settings_dup(), but assume that the old pointers can still be safely
+ used. This saves memory since strings don't have to be duplicated. */
+void *settings_dup_with_pointers(const struct setting_parser_info *info,
+ const void *set, pool_t pool);
+/* Duplicate the entire setting parser. */
+struct setting_parser_context *
+settings_parser_dup(const struct setting_parser_context *old_ctx,
+ pool_t new_pool);
+
+/* parsers is a name=NULL -terminated list. The parsers are appended as
+ dynamic_settings_list structures to their parent. All must have the same
+ parent. The new structures are allocated from the given pool. */
+void settings_parser_info_update(pool_t pool,
+ struct setting_parser_info *parent,
+ const struct dynamic_settings_parser *parsers);
+void settings_parser_dyn_update(pool_t pool,
+ const struct setting_parser_info *const **roots,
+ const struct dynamic_settings_parser *dyn_parsers);
+
+/* Return pointer to beginning of settings for given name, or NULL if there is
+ no such registered name. */
+const void *settings_find_dynamic(const struct setting_parser_info *info,
+ const void *base_set, const char *name);
+
+/* Copy changed settings from src to dest. If conflict_key_r is not NULL and
+ both src and dest have changed the same setting, return -1 and set the
+ key name. If it's NULL, the old setting is kept.
+
+ KLUDGE: For SET_STRLIST types if both source and destination have identical
+ keys, the duplicates in the source side are ignored. This is required to
+ make the current config code work correctly. */
+int settings_parser_apply_changes(struct setting_parser_context *dest,
+ const struct setting_parser_context *src,
+ pool_t pool, const char **conflict_key_r);
+
+/* Return section name escaped */
+const char *settings_section_escape(const char *name);
+/* Parse time interval string, return as seconds. */
+int settings_get_time(const char *str, unsigned int *secs_r,
+ const char **error_r);
+/* Parse time interval string, return as milliseconds. */
+int settings_get_time_msecs(const char *str, unsigned int *msecs_r,
+ const char **error_r);
+/* Parse size string, return as bytes. */
+int settings_get_size(const char *str, uoff_t *bytes_r,
+ const char **error_r);
+
+#endif
diff --git a/src/lib-settings/settings.c b/src/lib-settings/settings.c
new file mode 100644
index 0000000..228fab7
--- /dev/null
+++ b/src/lib-settings/settings.c
@@ -0,0 +1,434 @@
+/* Copyright (c) 2002-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "str.h"
+#include "istream.h"
+#include "strescape.h"
+#include "settings.h"
+
+#include <stdio.h>
+#include <fcntl.h>
+#ifdef HAVE_GLOB_H
+# include <glob.h>
+#endif
+
+#ifndef GLOB_BRACE
+# define GLOB_BRACE 0
+#endif
+
+#define SECTION_ERRORMSG "%s (section changed in %s at line %d)"
+
+struct input_stack {
+ struct input_stack *prev;
+
+ struct istream *input;
+ const char *path;
+ unsigned int linenum;
+};
+
+settings_section_callback_t *null_settings_section_callback = NULL;
+
+static const char *get_bool(const char *value, bool *result)
+{
+ if (strcasecmp(value, "yes") == 0)
+ *result = TRUE;
+ else if (strcasecmp(value, "no") == 0)
+ *result = FALSE;
+ else
+ return t_strconcat("Invalid boolean: ", value, NULL);
+
+ return NULL;
+}
+
+static const char *get_uint(const char *value, unsigned int *result)
+{
+ int num;
+
+ if (sscanf(value, "%i", &num) != 1 || num < 0)
+ return t_strconcat("Invalid number: ", value, NULL);
+ *result = num;
+ return NULL;
+}
+
+#define IS_WHITE(c) ((c) == ' ' || (c) == '\t')
+
+static const char *expand_environment_vars(const char *value)
+{
+ const char *pvalue = value, *p;
+
+ /* Fast path when there are no candidates */
+ if ((pvalue = strchr(pvalue, '$')) == NULL)
+ return value;
+
+ string_t *expanded_value = t_str_new(strlen(value));
+ str_append_data(expanded_value, value, pvalue - value);
+
+ while (pvalue != NULL && (p = strchr(pvalue, '$')) != NULL) {
+ const char *var_end;
+ str_append_data(expanded_value, pvalue, p - pvalue);
+ if ((p == value || IS_WHITE(p[-1])) &&
+ str_begins(p, "$ENV:")) {
+ const char *var_name, *envval;
+ var_end = strchr(p, ' ');
+ if (var_end == NULL)
+ var_name = p + 5;
+ else
+ var_name = t_strdup_until(p + 5, var_end);
+ if ((envval = getenv(var_name)) != NULL)
+ str_append(expanded_value, envval);
+ } else {
+ str_append_c(expanded_value, '$');
+ var_end = p + 1;
+ }
+ pvalue = var_end;
+ }
+
+ if (pvalue != NULL)
+ str_append(expanded_value, pvalue);
+
+ return str_c(expanded_value);
+}
+
+const char *
+parse_setting_from_defs(pool_t pool, const struct setting_def *defs, void *base,
+ const char *key, const char *value)
+{
+ const struct setting_def *def;
+
+ for (def = defs; def->name != NULL; def++) {
+ if (strcmp(def->name, key) == 0) {
+ void *ptr = STRUCT_MEMBER_P(base, def->offset);
+
+ switch (def->type) {
+ case SET_STR:
+ *((char **)ptr) = p_strdup(pool, value);
+ return NULL;
+ case SET_INT:
+ /* use %i so we can handle eg. 0600
+ as octal value with umasks */
+ return get_uint(value, (unsigned int *) ptr);
+ case SET_BOOL:
+ return get_bool(value, (bool *) ptr);
+ }
+ }
+ }
+
+ return t_strconcat("Unknown setting: ", key, NULL);
+}
+
+static const char *
+fix_relative_path(const char *path, struct input_stack *input)
+{
+ const char *p;
+
+ if (*path == '/')
+ return path;
+
+ p = strrchr(input->path, '/');
+ if (p == NULL)
+ return path;
+
+ return t_strconcat(t_strdup_until(input->path, p+1), path, NULL);
+}
+
+static int settings_add_include(const char *path, struct input_stack **inputp,
+ bool ignore_errors, const char **error_r)
+{
+ struct input_stack *tmp, *new_input;
+ int fd;
+
+ for (tmp = *inputp; tmp != NULL; tmp = tmp->prev) {
+ if (strcmp(tmp->path, path) == 0)
+ break;
+ }
+ if (tmp != NULL) {
+ *error_r = t_strdup_printf("Recursive include file: %s", path);
+ return -1;
+ }
+
+ if ((fd = open(path, O_RDONLY)) == -1) {
+ if (ignore_errors)
+ return 0;
+
+ *error_r = t_strdup_printf("Couldn't open include file %s: %m",
+ path);
+ return -1;
+ }
+
+ new_input = t_new(struct input_stack, 1);
+ new_input->prev = *inputp;
+ new_input->path = t_strdup(path);
+ new_input->input = i_stream_create_fd_autoclose(&fd, SIZE_MAX);
+ i_stream_set_return_partial_line(new_input->input, TRUE);
+ *inputp = new_input;
+ return 0;
+}
+
+static int
+settings_include(const char *pattern, struct input_stack **inputp,
+ bool ignore_errors, const char **error_r)
+{
+#ifdef HAVE_GLOB
+ glob_t globbers;
+ unsigned int i;
+
+ switch (glob(pattern, GLOB_BRACE, NULL, &globbers)) {
+ case 0:
+ break;
+ case GLOB_NOSPACE:
+ *error_r = "glob() failed: Not enough memory";
+ return -1;
+ case GLOB_ABORTED:
+ *error_r = "glob() failed: Read error";
+ return -1;
+ case GLOB_NOMATCH:
+ if (ignore_errors)
+ return 0;
+ *error_r = "No matches";
+ return -1;
+ default:
+ *error_r = "glob() failed: Unknown error";
+ return -1;
+ }
+
+ /* iterate through the different files matching the globbing */
+ for (i = 0; i < globbers.gl_pathc; i++) {
+ if (settings_add_include(globbers.gl_pathv[i], inputp,
+ ignore_errors, error_r) < 0)
+ return -1;
+ }
+ globfree(&globbers);
+ return 0;
+#else
+ return settings_add_include(pattern, inputp, ignore_errors, error_r);
+#endif
+}
+
+bool settings_read_i(const char *path, const char *section,
+ settings_callback_t *callback,
+ settings_section_callback_t *sect_callback, void *context,
+ const char **error_r)
+{
+ /* pretty horrible code, but v2.0 will have this rewritten anyway.. */
+ struct input_stack root, *input;
+ const char *errormsg, *next_section, *name, *last_section_path = NULL;
+ char *line, *key, *p, quote;
+ string_t *full_line;
+ size_t len;
+ int fd, last_section_line = 0, skip, sections, root_section;
+
+ fd = open(path, O_RDONLY);
+ if (fd < 0) {
+ *error_r = t_strdup_printf(
+ "Can't open configuration file %s: %m", path);
+ return FALSE;
+ }
+
+ if (section == NULL) {
+ skip = 0;
+ next_section = NULL;
+ } else {
+ skip = 1;
+ next_section = t_strcut(section, '/');
+ }
+
+ i_zero(&root);
+ root.path = path;
+ input = &root;
+
+ full_line = t_str_new(512);
+ sections = 0; root_section = 0; errormsg = NULL;
+ input->input = i_stream_create_fd_autoclose(&fd, SIZE_MAX);
+ i_stream_set_return_partial_line(input->input, TRUE);
+prevfile:
+ while ((line = i_stream_read_next_line(input->input)) != NULL) {
+ input->linenum++;
+
+ /* @UNSAFE: line is modified */
+
+ /* skip whitespace */
+ while (IS_WHITE(*line))
+ line++;
+
+ /* ignore comments or empty lines */
+ if (*line == '#' || *line == '\0')
+ continue;
+
+ /* strip away comments. pretty kludgy way really.. */
+ for (p = line; *p != '\0'; p++) {
+ if (*p == '\'' || *p == '"') {
+ quote = *p;
+ for (p++; *p != quote && *p != '\0'; p++) {
+ if (*p == '\\' && p[1] != '\0')
+ p++;
+ }
+ if (*p == '\0')
+ break;
+ } else if (*p == '#') {
+ if (!IS_WHITE(p[-1])) {
+ i_warning("Configuration file %s line %u: "
+ "Ambiguous '#' character in line, treating it as comment. "
+ "Add a space before it to remove this warning.",
+ input->path, input->linenum);
+ }
+ *p = '\0';
+ break;
+ }
+ }
+
+ /* remove whitespace from end of line */
+ len = strlen(line);
+ while (IS_WHITE(line[len-1]))
+ len--;
+ line[len] = '\0';
+
+ if (len > 0 && line[len-1] == '\\') {
+ /* continues in next line */
+ len--;
+ while (IS_WHITE(line[len-1]))
+ len--;
+ str_append_data(full_line, line, len);
+ str_append_c(full_line, ' ');
+ continue;
+ }
+ if (str_len(full_line) > 0) {
+ str_append(full_line, line);
+ line = str_c_modifiable(full_line);
+ }
+
+ bool quoted = FALSE;
+ /* a) key = value
+ b) section_type [section_name] {
+ c) } */
+ key = line;
+ while (!IS_WHITE(*line) && *line != '\0' && *line != '=')
+ line++;
+ if (IS_WHITE(*line)) {
+ *line++ = '\0';
+ while (IS_WHITE(*line)) line++;
+ }
+
+ if (strcmp(key, "!include_try") == 0 ||
+ strcmp(key, "!include") == 0) {
+ if (settings_include(fix_relative_path(line, input),
+ &input,
+ strcmp(key, "!include_try") == 0,
+ &errormsg) == 0)
+ goto prevfile;
+ } else if (*line == '=') {
+ /* a) */
+ *line++ = '\0';
+ while (IS_WHITE(*line)) line++;
+
+ len = strlen(line);
+ if (len > 0 &&
+ ((*line == '"' && line[len-1] == '"') ||
+ (*line == '\'' && line[len-1] == '\''))) {
+ line[len-1] = '\0';
+ line = str_unescape(line+1);
+ quoted = TRUE;
+ }
+
+ /* @UNSAFE: Cast to modifiable datastack value,
+ but it will not be actually modified after this. */
+ if (!quoted)
+ line = (char *)expand_environment_vars(line);
+
+ errormsg = skip > 0 ? NULL :
+ callback(key, line, context);
+ } else if (strcmp(key, "}") != 0 || *line != '\0') {
+ /* b) + errors */
+ line[-1] = '\0';
+
+ if (*line == '{')
+ name = "";
+ else {
+ name = line;
+ while (!IS_WHITE(*line) && *line != '\0')
+ line++;
+
+ if (*line != '\0') {
+ *line++ = '\0';
+ while (IS_WHITE(*line))
+ line++;
+ }
+ }
+
+ if (*line != '{')
+ errormsg = "Expecting '='";
+ else {
+ sections++;
+ if (next_section != NULL &&
+ strcmp(next_section, name) == 0) {
+ section += strlen(next_section);
+ if (*section == '\0') {
+ skip = 0;
+ next_section = NULL;
+ root_section = sections;
+ } else {
+ i_assert(*section == '/');
+ section++;
+ next_section =
+ t_strcut(section, '/');
+ }
+ }
+
+ if (skip > 0)
+ skip++;
+ else {
+ skip = sect_callback == NULL ? 1 :
+ !sect_callback(key, name,
+ context,
+ &errormsg);
+ if (errormsg != NULL &&
+ last_section_line != 0) {
+ errormsg = t_strdup_printf(
+ SECTION_ERRORMSG,
+ errormsg,
+ last_section_path,
+ last_section_line);
+ }
+ }
+ last_section_path = input->path;
+ last_section_line = input->linenum;
+ }
+ } else {
+ /* c) */
+ if (sections == 0)
+ errormsg = "Unexpected '}'";
+ else {
+ if (skip > 0)
+ skip--;
+ else {
+ i_assert(sect_callback != NULL);
+ sect_callback(NULL, NULL, context,
+ &errormsg);
+ if (root_section == sections &&
+ errormsg == NULL) {
+ /* we found the section,
+ now quit */
+ break;
+ }
+ }
+ last_section_path = input->path;
+ last_section_line = input->linenum;
+ sections--;
+ }
+ }
+
+ if (errormsg != NULL) {
+ *error_r = t_strdup_printf(
+ "Error in configuration file %s line %d: %s",
+ input->path, input->linenum, errormsg);
+ break;
+ }
+ str_truncate(full_line, 0);
+ }
+
+ i_stream_destroy(&input->input);
+ input = input->prev;
+ if (line == NULL && input != NULL)
+ goto prevfile;
+
+ return errormsg == NULL;
+}
diff --git a/src/lib-settings/settings.h b/src/lib-settings/settings.h
new file mode 100644
index 0000000..888dfb0
--- /dev/null
+++ b/src/lib-settings/settings.h
@@ -0,0 +1,73 @@
+#ifndef SETTINGS_H
+#define SETTINGS_H
+
+/*
+ * Note:
+ *
+ * The definitions in this file are used for parsing of external config
+ * files and *not* for parsing of dovecot.conf. Unfortunately, the types
+ * here (e.g., enum settings_type) collide with those in settings-parser.h.
+ *
+ * We should remove the need for this file in v3.0.
+ */
+
+enum setting_type {
+ SET_STR,
+ SET_INT,
+ SET_BOOL
+};
+
+struct setting_def {
+ enum setting_type type;
+ const char *name;
+ size_t offset;
+};
+
+#define DEF_STRUCT_STR(name, struct_name) \
+ { SET_STR + COMPILE_ERROR_IF_TYPES_NOT_COMPATIBLE( \
+ ((struct struct_name *)0)->name, const char *), \
+ #name, offsetof(struct struct_name, name) }
+#define DEF_STRUCT_INT(name, struct_name) \
+ { SET_INT + COMPILE_ERROR_IF_TYPES_NOT_COMPATIBLE( \
+ ((struct struct_name *)0)->name, unsigned int), \
+ #name, offsetof(struct struct_name, name) }
+#define DEF_STRUCT_BOOL(name, struct_name) \
+ { SET_BOOL + COMPILE_ERROR_IF_TYPES_NOT_COMPATIBLE( \
+ ((struct struct_name *)0)->name, bool), \
+ #name, offsetof(struct struct_name, name) }
+
+/* Return error message. When closing section, key = NULL, value = NULL. */
+typedef const char *settings_callback_t(const char *key, const char *value,
+ void *context);
+
+/* Return TRUE if we want to go inside the section */
+typedef bool settings_section_callback_t(const char *type, const char *name,
+ void *context, const char **errormsg);
+
+extern settings_section_callback_t *null_settings_section_callback;
+
+const char *
+parse_setting_from_defs(pool_t pool, const struct setting_def *defs, void *base,
+ const char *key, const char *value);
+
+bool settings_read_i(const char *path, const char *section,
+ settings_callback_t *callback,
+ settings_section_callback_t *sect_callback, void *context,
+ const char **error_r)
+ ATTR_NULL(2, 4, 5);
+#define settings_read(path, section, callback, sect_callback, context, error_r) \
+ settings_read_i(path - \
+ CALLBACK_TYPECHECK(callback, const char *(*)( \
+ const char *, const char *, typeof(context))) - \
+ CALLBACK_TYPECHECK(sect_callback, bool (*)( \
+ const char *, const char *, typeof(context), \
+ const char **)), \
+ section, (settings_callback_t *)callback, \
+ (settings_section_callback_t *)sect_callback, context, error_r)
+#define settings_read_nosection(path, callback, context, error_r) \
+ settings_read_i(path - \
+ CALLBACK_TYPECHECK(callback, const char *(*)( \
+ const char *, const char *, typeof(context))), \
+ NULL, (settings_callback_t *)callback, NULL, context, error_r)
+
+#endif
diff --git a/src/lib-settings/test-settings-parser.c b/src/lib-settings/test-settings-parser.c
new file mode 100644
index 0000000..83aefba
--- /dev/null
+++ b/src/lib-settings/test-settings-parser.c
@@ -0,0 +1,340 @@
+/* Copyright (c) 2009-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "net.h"
+#include "var-expand.h"
+#include "settings-parser.h"
+#include "istream.h"
+#include "test-common.h"
+
+static const char *test_settings_blobs[] =
+{
+/* Blob 0 */
+ "bool_true=yes\n"
+ "bool_false=no\n"
+ "uint=15\n"
+ "uint_oct=0700\n"
+ "secs=5s\n"
+ "msecs=5ms\n"
+ "size=1k\n"
+ "port=2205\n"
+ "str=test string\n"
+ "expand_str=test %{string}\n"
+ "strlist=\n"
+ "strlist/x=a\n"
+ "strlist/y=b\n"
+ "strlist/z=c\n"
+ "\n",
+};
+
+
+static void test_settings_get_time(void)
+{
+ static const struct {
+ const char *input;
+ unsigned int output;
+ } tests[] = {
+ { "0", 0 },
+
+ { "59s", 59 },
+ { "59 s", 59 },
+ { "59se", 59 },
+ { "59sec", 59 },
+ { "59secs", 59 },
+ { "59seco", 59 },
+ { "59secon", 59 },
+ { "59second", 59 },
+ { "59seconds", 59 },
+ { "123456 seconds", 123456 },
+
+ { "123m", 123*60 },
+ { "123 m", 123*60 },
+ { "123 mi", 123*60 },
+ { "123 min", 123*60 },
+ { "123 mins", 123*60 },
+ { "123 minu", 123*60 },
+ { "123 minut", 123*60 },
+ { "123 minute", 123*60 },
+ { "123 minutes", 123*60 },
+
+ { "123h", 123*60*60 },
+ { "123 h", 123*60*60 },
+ { "123 ho", 123*60*60 },
+ { "123 hou", 123*60*60 },
+ { "123 hour", 123*60*60 },
+ { "123 hours", 123*60*60 },
+
+ { "12d", 12*60*60*24 },
+ { "12 d", 12*60*60*24 },
+ { "12 da", 12*60*60*24 },
+ { "12 day", 12*60*60*24 },
+ { "12 days", 12*60*60*24 },
+
+ { "3w", 3*60*60*24*7 },
+ { "3 w", 3*60*60*24*7 },
+ { "3 we", 3*60*60*24*7 },
+ { "3 wee", 3*60*60*24*7 },
+ { "3 week", 3*60*60*24*7 },
+ { "3 weeks", 3*60*60*24*7 },
+
+ { "1000ms", 1 },
+ { "50000ms", 50 },
+ };
+ struct {
+ const char *input;
+ unsigned int output;
+ } msecs_tests[] = {
+ { "0ms", 0 },
+ { "1ms", 1 },
+ { "123456ms", 123456 },
+ { "123456 ms", 123456 },
+ { "123456mse", 123456 },
+ { "123456msec", 123456 },
+ { "123456msecs", 123456 },
+ { "123456mseco", 123456 },
+ { "123456msecon", 123456 },
+ { "123456msecond", 123456 },
+ { "123456mseconds", 123456 },
+ { "123456mil", 123456 },
+ { "123456mill", 123456 },
+ { "123456milli", 123456 },
+ { "123456millis", 123456 },
+ { "123456millisec", 123456 },
+ { "123456millisecs", 123456 },
+ { "123456milliseco", 123456 },
+ { "123456millisecon", 123456 },
+ { "123456millisecond", 123456 },
+ { "123456milliseconds", 123456 },
+ { "4294967295 ms", 4294967295 },
+ };
+ const char *secs_errors[] = {
+ "-1",
+ "1",
+ /* wrong spellings: */
+ "1ss",
+ "1secss",
+ "1secondss",
+ "1ma",
+ "1minsa",
+ "1hu",
+ "1hoursa",
+ "1dd",
+ "1days?",
+ "1wa",
+ "1weeksb",
+
+ /* milliseconds: */
+ "1ms",
+ "999ms",
+ "1001ms",
+ /* overflows: */
+ "7102 w",
+ "4294967296 s",
+ };
+ const char *msecs_errors[] = {
+ "-1",
+ "1",
+ /* wrong spellings: */
+ "1mis",
+ "1mss",
+ /* overflows: */
+ "8 w",
+ "4294967296 ms",
+ };
+ unsigned int i, secs, msecs;
+ const char *error;
+
+ test_begin("settings_get_time()");
+ for (i = 0; i < N_ELEMENTS(tests); i++) {
+ test_assert_idx(settings_get_time(tests[i].input, &secs, &error) == 0, i);
+ test_assert_idx(secs == tests[i].output, i);
+
+ test_assert_idx(settings_get_time_msecs(tests[i].input, &msecs, &error) == 0, i);
+ test_assert_idx(msecs == tests[i].output*1000, i);
+ }
+ for (i = 0; i < N_ELEMENTS(msecs_tests); i++) {
+ test_assert_idx(settings_get_time_msecs(msecs_tests[i].input, &msecs, &error) == 0, i);
+ test_assert_idx(msecs == msecs_tests[i].output, i);
+ }
+ for (i = 0; i < N_ELEMENTS(secs_errors); i++)
+ test_assert_idx(settings_get_time(secs_errors[i], &secs, &error) < 0, i);
+ for (i = 0; i < N_ELEMENTS(msecs_errors); i++)
+ test_assert_idx(settings_get_time_msecs(msecs_errors[i], &msecs, &error) < 0, i);
+ test_end();
+}
+
+static void test_settings_get_size(void)
+{
+ test_begin("settings_get_size()");
+
+ static const struct {
+ const char *input;
+ uoff_t output;
+ } tests[] = {
+ { "0", 0 },
+ { "0000", 0 },
+ { "1b", 1 },
+ { "1B", 1 },
+ { "1 b", 1 },
+ { "1k", 1024 },
+ { "1K", 1024 },
+ { "1 k", 1024 },
+ { "1m", 1024*1024 },
+ { "1M", 1024*1024 },
+ { "1 m", 1024*1024 },
+ { "1g", 1024*1024*1024ULL },
+ { "1G", 1024*1024*1024ULL },
+ { "1 g", 1024*1024*1024ULL },
+ { "1t", 1024*1024*1024*1024ULL },
+ { "1T", 1024*1024*1024*1024ULL },
+ { "1 t", 1024*1024*1024*1024ULL },
+ };
+
+ const char *size_errors[] = {
+ "-1",
+ "one",
+ "",
+ "340282366920938463463374607431768211456",
+ "2^32",
+ "2**32",
+ "1e10",
+ "1 byte",
+ };
+
+ size_t i;
+ uoff_t size;
+ const char *error;
+
+ for (i = 0; i < N_ELEMENTS(tests); i++) {
+ error = NULL;
+ test_assert_idx(settings_get_size(tests[i].input, &size, &error) == 0, i);
+ test_assert_idx(size == tests[i].output, i);
+ test_assert(error == NULL);
+ }
+ for (i = 0; i < N_ELEMENTS(size_errors); i++) {
+ error = NULL;
+ test_assert_idx(settings_get_size(size_errors[i], &size, &error) < 0, i);
+ test_assert(error != NULL);
+ };
+
+ test_end();
+}
+
+static void test_settings_parser_get(void)
+{
+ struct test_settings {
+ bool bool_true;
+ bool bool_false;
+ unsigned int uint;
+ unsigned int uint_oct;
+ unsigned int secs;
+ unsigned int msecs;
+ uoff_t size;
+ in_port_t port;
+ const char *str;
+ const char *expand_str;
+ ARRAY_TYPE(const_string) strlist;
+ } test_defaults = {
+ FALSE, /* for negation test */
+ TRUE,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ "",
+ "",
+ ARRAY_INIT,
+ };
+ const struct setting_define defs[] = {
+ SETTING_DEFINE_STRUCT_BOOL("bool_true", bool_true, struct test_settings),
+ SETTING_DEFINE_STRUCT_BOOL("bool_false", bool_false, struct test_settings),
+ SETTING_DEFINE_STRUCT_UINT("uint", uint, struct test_settings),
+ { .type = SET_UINT_OCT, .key = "uint_oct",
+ offsetof(struct test_settings, uint_oct), NULL },
+ SETTING_DEFINE_STRUCT_TIME("secs", secs, struct test_settings),
+ SETTING_DEFINE_STRUCT_TIME_MSECS("msecs", msecs, struct test_settings),
+ SETTING_DEFINE_STRUCT_SIZE("size", size, struct test_settings),
+ SETTING_DEFINE_STRUCT_IN_PORT("port", port, struct test_settings),
+ SETTING_DEFINE_STRUCT_STR("str", str, struct test_settings),
+ { .type = SET_STR_VARS, .key = "expand_str",
+ offsetof(struct test_settings, expand_str), NULL },
+ { .type = SET_STRLIST, .key = "strlist",
+ offsetof(struct test_settings, strlist), NULL },
+ SETTING_DEFINE_LIST_END
+ };
+ const struct setting_parser_info root = {
+ .module_name = "test",
+ .defines = defs,
+ .defaults = &test_defaults,
+
+ .type_offset = SIZE_MAX,
+ .struct_size = sizeof(struct test_settings),
+
+ .parent_offset = SIZE_MAX,
+ };
+
+ test_begin("settings_parser_get");
+
+ pool_t pool = pool_alloconly_create("settings parser", 1024);
+ struct setting_parser_context *ctx =
+ settings_parser_init(pool, &root, 0);
+ struct istream *is = test_istream_create(test_settings_blobs[0]);
+ const char *error = NULL;
+ int ret;
+ while((ret = settings_parse_stream_read(ctx, is)) > 0);
+ test_assert(ret == 0);
+ if (ret < 0)
+ i_error("settings_parse_stream failed: %s",
+ settings_parser_get_error(ctx));
+ i_stream_unref(&is);
+ test_assert(settings_parser_check(ctx, pool, NULL));
+
+ /* check what we got */
+ struct test_settings *settings = settings_parser_get(ctx);
+ test_assert(settings != NULL);
+
+ test_assert(settings->bool_true == TRUE);
+ test_assert(settings->bool_false == FALSE);
+ test_assert(settings->uint == 15);
+ test_assert(settings->uint_oct == 0700);
+ test_assert(settings->secs == 5);
+ test_assert(settings->msecs == 5);
+ test_assert(settings->size == 1024);
+ test_assert(settings->port == 2205);
+ test_assert_strcmp(settings->str, "test string");
+ test_assert_strcmp(settings->expand_str, "0test %{string}");
+
+ test_assert(array_count(&settings->strlist) == 6);
+ test_assert_strcmp(t_array_const_string_join(&settings->strlist, ";"),
+ "x;a;y;b;z;c");
+
+ const struct var_expand_table table[] = {
+ {'\0', "value", "string"},
+ {'\0', NULL, NULL}
+ };
+
+ /* expand settings */
+ test_assert(settings_var_expand(&root, settings, pool, table, &error) == 1 &&
+ error == NULL);
+
+ /* check that the setting got expanded */
+ test_assert_strcmp(settings->expand_str, "test value");
+
+ settings_parser_deinit(&ctx);
+ pool_unref(&pool);
+ test_end();
+}
+
+int main(void)
+{
+ static void (*const test_functions[])(void) = {
+ test_settings_get_time,
+ test_settings_get_size,
+ test_settings_parser_get,
+ NULL
+ };
+ return test_run(test_functions);
+}
diff --git a/src/lib-settings/test-settings.c b/src/lib-settings/test-settings.c
new file mode 100644
index 0000000..4ad2a6b
--- /dev/null
+++ b/src/lib-settings/test-settings.c
@@ -0,0 +1,165 @@
+/* Copyright (c) 2021 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "net.h"
+#include "settings.h"
+#include "istream.h"
+#include "ostream.h"
+#include "test-common.h"
+
+#define TEST_SETTING_FILE ".test_settings.conf"
+
+static const char *config_contents =
+"# this is a comment\n"
+"str = value\n"
+"str2 = some other value # and this should be ignored\n"
+"str3 = $ENV:test\n"
+"str4 = $ENV:test %{second}\n"
+"str5 = Hello $ENV:test\n"
+"str6 = foo$ENV:test bar\n"
+"str7 = \"this is $ENV:test string literal\"\n"
+"str8 = \\$ENV:test escaped\n"
+"str9 = $ENV:FOO$ENV:FOO bar\n"
+"str10 = \\$escape \\escape \\\"escape\\\"\n"
+"str11 = 'this is $ENV:test string literal'\n"
+"str12 = $ENV:test $ENV:test\n"
+"b_true = yes\n"
+"b_false = no\n"
+"number = 1234\n";
+
+struct test_settings {
+ const char *str;
+ const char *str2;
+ const char *str3;
+ const char *str4;
+ const char *str5;
+ const char *str6;
+ const char *str7;
+ const char *str8;
+ const char *str9;
+ const char *str10;
+ const char *str11;
+ const char *str12;
+
+ bool b_true;
+ bool b_false;
+ unsigned int number;
+};
+
+#undef DEF_STR
+#undef DEF_BOOL
+#undef DEF_INT
+
+#define DEF_STR(name) DEF_STRUCT_STR(name, test_settings)
+#define DEF_BOOL(name) DEF_STRUCT_BOOL(name, test_settings)
+#define DEF_INT(name) DEF_STRUCT_INT(name, test_settings)
+
+static struct setting_def setting_defs[] = {
+ DEF_STR(str),
+ DEF_STR(str2),
+ DEF_STR(str3),
+ DEF_STR(str4),
+ DEF_STR(str5),
+ DEF_STR(str6),
+ DEF_STR(str7),
+ DEF_STR(str8),
+ DEF_STR(str9),
+ DEF_STR(str10),
+ DEF_STR(str11),
+ DEF_STR(str12),
+ DEF_BOOL(b_true),
+ DEF_BOOL(b_false),
+ DEF_INT(number),
+ { 0, NULL, 0 }
+};
+
+static struct test_settings default_settings = {
+ .str = "",
+ .str2 = "",
+ .str3 = "",
+ .str4 = "",
+ .str5 = "",
+ .str6 = "",
+ .str7 = "",
+ .str8 = "",
+ .str9 = "",
+ .str10 = "",
+ .str11 = "",
+ .str12 = "",
+
+ .b_true = FALSE,
+ .b_false = TRUE,
+ .number = 0,
+};
+
+struct test_settings_context {
+ pool_t pool;
+ struct test_settings set;
+};
+
+static const char *parse_setting(const char *key, const char *value,
+ struct test_settings_context *ctx)
+{
+ return parse_setting_from_defs(ctx->pool, setting_defs,
+ &ctx->set, key, value);
+}
+
+static void test_settings_read_nosection(void)
+{
+ test_begin("settings_read_nosection");
+
+ const char *error = NULL;
+ /* write a simple config file */
+ struct ostream *os = o_stream_create_file(TEST_SETTING_FILE, 0, 0600, 0);
+ o_stream_nsend_str(os, config_contents);
+ test_assert(o_stream_finish(os) == 1);
+ o_stream_unref(&os);
+
+ putenv("test=first");
+ putenv("FOO$ENV:FOO=works");
+ /* try parse it */
+ pool_t pool = pool_alloconly_create("test settings", 1024);
+ struct test_settings_context *ctx =
+ p_new(pool, struct test_settings_context, 1);
+ ctx->pool = pool;
+ ctx->set = default_settings;
+
+ test_assert(settings_read_nosection(TEST_SETTING_FILE, parse_setting,
+ ctx, &error));
+ test_assert(error == NULL);
+ if (error != NULL)
+ i_error("%s", error);
+
+ /* see what we got */
+ test_assert_strcmp(ctx->set.str, "value");
+ test_assert_strcmp(ctx->set.str2, "some other value");
+ test_assert_strcmp(ctx->set.str3, "first");
+ test_assert_strcmp(ctx->set.str4, "first %{second}");
+ test_assert_strcmp(ctx->set.str5, "Hello first");
+ test_assert_strcmp(ctx->set.str6, "foo$ENV:test bar");
+ test_assert_strcmp(ctx->set.str7, "this is $ENV:test string literal");
+ test_assert_strcmp(ctx->set.str8, "\\$ENV:test escaped");
+ test_assert_strcmp(ctx->set.str9, "works bar");
+ test_assert_strcmp(ctx->set.str10, "\\$escape \\escape \\\"escape\\\"");
+ test_assert_strcmp(ctx->set.str11, "this is $ENV:test string literal");
+ test_assert_strcmp(ctx->set.str12, "first first");
+
+ test_assert(ctx->set.b_true == TRUE);
+ test_assert(ctx->set.b_false == FALSE);
+ test_assert(ctx->set.number == 1234);
+
+ pool_unref(&pool);
+
+ i_unlink_if_exists(TEST_SETTING_FILE);
+ test_end();
+}
+
+int main(void)
+{
+ static void (*const test_functions[])(void) = {
+ test_settings_read_nosection,
+ NULL
+ };
+ return test_run(test_functions);
+}
diff --git a/src/lib-smtp/Makefile.am b/src/lib-smtp/Makefile.am
new file mode 100644
index 0000000..a16dc68
--- /dev/null
+++ b/src/lib-smtp/Makefile.am
@@ -0,0 +1,192 @@
+noinst_LTLIBRARIES = libsmtp.la
+
+AM_CPPFLAGS = \
+ -I$(top_srcdir)/src/lib \
+ -I$(top_srcdir)/src/lib-test \
+ -I$(top_srcdir)/src/lib-settings \
+ -I$(top_srcdir)/src/lib-master \
+ -I$(top_srcdir)/src/lib-sasl \
+ -I$(top_srcdir)/src/lib-ssl-iostream \
+ -I$(top_srcdir)/src/lib-dns \
+ -I$(top_srcdir)/src/lib-program-client \
+ -I$(top_srcdir)/src/lib-mail \
+ -DTEST_BIN_DIR=\"$(abs_srcdir)/test-bin\"
+
+smtp_server_cmds = \
+ smtp-server-cmd-helo.c \
+ smtp-server-cmd-starttls.c \
+ smtp-server-cmd-auth.c \
+ smtp-server-cmd-mail.c \
+ smtp-server-cmd-rcpt.c \
+ smtp-server-cmd-data.c \
+ smtp-server-cmd-rset.c \
+ smtp-server-cmd-noop.c \
+ smtp-server-cmd-quit.c \
+ smtp-server-cmd-vrfy.c \
+ smtp-server-cmd-xclient.c
+
+libsmtp_la_SOURCES = \
+ smtp-parser.c \
+ smtp-syntax.c \
+ smtp-address.c \
+ smtp-common.c \
+ smtp-params.c \
+ smtp-reply.c \
+ smtp-reply-parser.c \
+ smtp-command-parser.c \
+ smtp-client-command.c \
+ smtp-client-transaction.c \
+ smtp-client-connection.c \
+ smtp-client.c \
+ $(smtp_server_cmds) \
+ smtp-server-reply.c \
+ smtp-server-command.c \
+ smtp-server-recipient.c \
+ smtp-server-transaction.c \
+ smtp-server-connection.c \
+ smtp-server.c \
+ smtp-submit-settings.c \
+ smtp-submit.c
+
+headers = \
+ smtp-parser.h \
+ smtp-syntax.h \
+ smtp-address.h \
+ smtp-common.h \
+ smtp-params.h \
+ smtp-reply.h \
+ smtp-reply-parser.h \
+ smtp-command.h \
+ smtp-command-parser.h \
+ smtp-client-command.h \
+ smtp-client-transaction.h \
+ smtp-client-connection.h \
+ smtp-client-private.h \
+ smtp-client.h \
+ smtp-server-private.h \
+ smtp-server.h \
+ smtp-submit-settings.h \
+ smtp-submit.h
+
+pkginc_libdir=$(pkgincludedir)
+pkginc_lib_HEADERS = $(headers)
+
+test_programs = \
+ test-smtp-syntax \
+ test-smtp-address \
+ test-smtp-params \
+ test-smtp-reply \
+ test-smtp-command-parser \
+ test-smtp-payload \
+ test-smtp-submit \
+ test-smtp-client-errors \
+ test-smtp-server-errors
+
+test_nocheck_programs =
+
+fuzz_programs =
+
+if USE_FUZZER
+fuzz_programs += \
+ fuzz-smtp-server
+endif
+
+noinst_PROGRAMS = $(fuzz_programs) $(test_programs) $(test_nocheck_programs)
+
+EXTRA_DIST = \
+ test-bin/sendmail-exit-1.sh \
+ test-bin/sendmail-success.sh
+
+test_libs = \
+ $(noinst_LTLIBRARIES) \
+ ../lib-program-client/libprogram_client.la \
+ ../lib-dns/libdns.la \
+ ../lib-mail/libmail.la \
+ ../lib-charset/libcharset.la \
+ ../lib-master/libmaster.la \
+ ../lib-auth/libauth.la \
+ ../lib-ssl-iostream/libssl_iostream.la \
+ ../lib-settings/libsettings.la \
+ ../lib-sasl/libsasl.la \
+ ../lib-test/libtest.la \
+ ../lib/liblib.la \
+ $(MODULE_LIBS)
+
+test_deps = \
+ $(noinst_LTLIBRARIES) \
+ ../lib-program-client/libprogram_client.la \
+ ../lib-dns/libdns.la \
+ ../lib-mail/libmail.la \
+ ../lib-charset/libcharset.la \
+ ../lib-master/libmaster.la \
+ ../lib-auth/libauth.la \
+ ../lib-ssl-iostream/libssl_iostream.la \
+ ../lib-settings/libsettings.la \
+ ../lib-sasl/libsasl.la \
+ ../lib-test/libtest.la \
+ ../lib/liblib.la
+
+test_libs_ssl=
+if BUILD_OPENSSL
+test_libs_ssl += ../lib-ssl-iostream/libssl_iostream_openssl.la
+endif
+
+test_smtp_syntax_SOURCES = test-smtp-syntax.c
+test_smtp_syntax_LDADD = $(test_libs)
+test_smtp_syntax_DEPENDENCIES = $(test_deps)
+
+test_smtp_address_SOURCES = test-smtp-address.c
+test_smtp_address_LDFLAGS = -export-dynamic
+test_smtp_address_LDADD = $(test_libs)
+test_smtp_address_DEPENDENCIES = $(test_deps)
+
+test_smtp_params_SOURCES = test-smtp-params.c
+test_smtp_params_LDFLAGS = -export-dynamic
+test_smtp_params_LDADD = $(test_libs)
+test_smtp_params_DEPENDENCIES = $(test_deps)
+
+test_smtp_reply_SOURCES = test-smtp-reply.c
+test_smtp_reply_LDFLAGS = -export-dynamic
+test_smtp_reply_LDADD = $(test_libs)
+test_smtp_reply_DEPENDENCIES = $(test_deps)
+
+test_smtp_command_parser_SOURCES = test-smtp-command-parser.c
+test_smtp_command_parser_LDFLAGS = -export-dynamic
+test_smtp_command_parser_LDADD = $(test_libs)
+test_smtp_command_parser_DEPENDENCIES = $(test_deps)
+
+test_smtp_payload_SOURCES = test-smtp-payload.c
+test_smtp_payload_LDFLAGS = -export-dynamic
+test_smtp_payload_LDADD = $(test_libs) $(test_libs_ssl)
+test_smtp_payload_DEPENDENCIES = $(test_deps)
+
+test_smtp_submit_SOURCES = test-smtp-submit.c
+test_smtp_submit_LDFLAGS = -export-dynamic
+test_smtp_submit_LDADD = $(test_libs)
+test_smtp_submit_DEPENDENCIES = $(test_deps)
+
+test_smtp_client_errors_SOURCES = test-smtp-client-errors.c
+test_smtp_client_errors_LDFLAGS = -export-dynamic
+test_smtp_client_errors_LDADD = $(test_libs) $(test_libs_ssl)
+test_smtp_client_errors_DEPENDENCIES = $(test_deps)
+
+test_smtp_server_errors_SOURCES = test-smtp-server-errors.c
+test_smtp_server_errors_LDFLAGS = -export-dynamic
+test_smtp_server_errors_LDADD = $(test_libs)
+test_smtp_server_errors_DEPENDENCIES = $(test_deps)
+
+nodist_EXTRA_fuzz_smtp_server_SOURCES = force-cxx-linking.cxx
+fuzz_smtp_server_CPPFLAGS = $(FUZZER_CPPFLAGS)
+fuzz_smtp_server_LDFLAGS = $(FUZZER_LDFLAGS)
+fuzz_smtp_server_SOURCES = fuzz-smtp-server.c
+fuzz_smtp_server_LDADD = $(test_libs)
+fuzz_smtp_server_DEPENDENCIES = $(test_deps)
+
+check-local:
+ for bin in $(test_programs); do \
+ if test "$$bin" = "test-smtp-submit"; then \
+ if ! env NOCHILDREN=yes $(RUN_TEST) ./$$bin; then exit 1; fi; \
+ else \
+ if ! $(RUN_TEST) ./$$bin; then exit 1; fi; \
+ fi \
+ done
diff --git a/src/lib-smtp/Makefile.in b/src/lib-smtp/Makefile.in
new file mode 100644
index 0000000..8548807
--- /dev/null
+++ b/src/lib-smtp/Makefile.in
@@ -0,0 +1,1356 @@
+# Makefile.in generated by automake 1.16.1 from Makefile.am.
+# @configure_input@
+
+# Copyright (C) 1994-2018 Free Software Foundation, Inc.
+
+# This Makefile.in is free software; the Free Software Foundation
+# gives unlimited permission to copy and/or distribute it,
+# with or without modifications, as long as this notice is preserved.
+
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY, to the extent permitted by law; without
+# even the implied warranty of MERCHANTABILITY or FITNESS FOR A
+# PARTICULAR PURPOSE.
+
+@SET_MAKE@
+
+
+
+VPATH = @srcdir@
+am__is_gnu_make = { \
+ if test -z '$(MAKELEVEL)'; then \
+ false; \
+ elif test -n '$(MAKE_HOST)'; then \
+ true; \
+ elif test -n '$(MAKE_VERSION)' && test -n '$(CURDIR)'; then \
+ true; \
+ else \
+ false; \
+ fi; \
+}
+am__make_running_with_option = \
+ case $${target_option-} in \
+ ?) ;; \
+ *) echo "am__make_running_with_option: internal error: invalid" \
+ "target option '$${target_option-}' specified" >&2; \
+ exit 1;; \
+ esac; \
+ has_opt=no; \
+ sane_makeflags=$$MAKEFLAGS; \
+ if $(am__is_gnu_make); then \
+ sane_makeflags=$$MFLAGS; \
+ else \
+ case $$MAKEFLAGS in \
+ *\\[\ \ ]*) \
+ bs=\\; \
+ sane_makeflags=`printf '%s\n' "$$MAKEFLAGS" \
+ | sed "s/$$bs$$bs[$$bs $$bs ]*//g"`;; \
+ esac; \
+ fi; \
+ skip_next=no; \
+ strip_trailopt () \
+ { \
+ flg=`printf '%s\n' "$$flg" | sed "s/$$1.*$$//"`; \
+ }; \
+ for flg in $$sane_makeflags; do \
+ test $$skip_next = yes && { skip_next=no; continue; }; \
+ case $$flg in \
+ *=*|--*) continue;; \
+ -*I) strip_trailopt 'I'; skip_next=yes;; \
+ -*I?*) strip_trailopt 'I';; \
+ -*O) strip_trailopt 'O'; skip_next=yes;; \
+ -*O?*) strip_trailopt 'O';; \
+ -*l) strip_trailopt 'l'; skip_next=yes;; \
+ -*l?*) strip_trailopt 'l';; \
+ -[dEDm]) skip_next=yes;; \
+ -[JT]) skip_next=yes;; \
+ esac; \
+ case $$flg in \
+ *$$target_option*) has_opt=yes; break;; \
+ esac; \
+ done; \
+ test $$has_opt = yes
+am__make_dryrun = (target_option=n; $(am__make_running_with_option))
+am__make_keepgoing = (target_option=k; $(am__make_running_with_option))
+pkgdatadir = $(datadir)/@PACKAGE@
+pkgincludedir = $(includedir)/@PACKAGE@
+pkglibdir = $(libdir)/@PACKAGE@
+pkglibexecdir = $(libexecdir)/@PACKAGE@
+am__cd = CDPATH="$${ZSH_VERSION+.}$(PATH_SEPARATOR)" && cd
+install_sh_DATA = $(install_sh) -c -m 644
+install_sh_PROGRAM = $(install_sh) -c
+install_sh_SCRIPT = $(install_sh) -c
+INSTALL_HEADER = $(INSTALL_DATA)
+transform = $(program_transform_name)
+NORMAL_INSTALL = :
+PRE_INSTALL = :
+POST_INSTALL = :
+NORMAL_UNINSTALL = :
+PRE_UNINSTALL = :
+POST_UNINSTALL = :
+build_triplet = @build@
+host_triplet = @host@
+@USE_FUZZER_TRUE@am__append_1 = \
+@USE_FUZZER_TRUE@ fuzz-smtp-server
+
+noinst_PROGRAMS = $(am__EXEEXT_2) $(am__EXEEXT_3) $(am__EXEEXT_4)
+@BUILD_OPENSSL_TRUE@am__append_2 = ../lib-ssl-iostream/libssl_iostream_openssl.la
+subdir = src/lib-smtp
+ACLOCAL_M4 = $(top_srcdir)/aclocal.m4
+am__aclocal_m4_deps = $(top_srcdir)/m4/ac_checktype2.m4 \
+ $(top_srcdir)/m4/ac_typeof.m4 $(top_srcdir)/m4/arc4random.m4 \
+ $(top_srcdir)/m4/blockdev.m4 $(top_srcdir)/m4/c99_vsnprintf.m4 \
+ $(top_srcdir)/m4/clock_gettime.m4 $(top_srcdir)/m4/crypt.m4 \
+ $(top_srcdir)/m4/crypt_xpg6.m4 $(top_srcdir)/m4/dbqlk.m4 \
+ $(top_srcdir)/m4/dirent_dtype.m4 $(top_srcdir)/m4/dovecot.m4 \
+ $(top_srcdir)/m4/fd_passing.m4 $(top_srcdir)/m4/fdatasync.m4 \
+ $(top_srcdir)/m4/flexible_array_member.m4 \
+ $(top_srcdir)/m4/glibc.m4 $(top_srcdir)/m4/gmtime_max.m4 \
+ $(top_srcdir)/m4/gmtime_tm_gmtoff.m4 \
+ $(top_srcdir)/m4/ioloop.m4 $(top_srcdir)/m4/iovec.m4 \
+ $(top_srcdir)/m4/ipv6.m4 $(top_srcdir)/m4/libcap.m4 \
+ $(top_srcdir)/m4/libtool.m4 $(top_srcdir)/m4/libwrap.m4 \
+ $(top_srcdir)/m4/linux_mremap.m4 $(top_srcdir)/m4/ltoptions.m4 \
+ $(top_srcdir)/m4/ltsugar.m4 $(top_srcdir)/m4/ltversion.m4 \
+ $(top_srcdir)/m4/lt~obsolete.m4 $(top_srcdir)/m4/mmap_write.m4 \
+ $(top_srcdir)/m4/mntctl.m4 $(top_srcdir)/m4/modules.m4 \
+ $(top_srcdir)/m4/notify.m4 $(top_srcdir)/m4/nsl.m4 \
+ $(top_srcdir)/m4/off_t_max.m4 $(top_srcdir)/m4/pkg.m4 \
+ $(top_srcdir)/m4/pr_set_dumpable.m4 \
+ $(top_srcdir)/m4/q_quotactl.m4 $(top_srcdir)/m4/quota.m4 \
+ $(top_srcdir)/m4/random.m4 $(top_srcdir)/m4/rlimit.m4 \
+ $(top_srcdir)/m4/sendfile.m4 $(top_srcdir)/m4/size_t_signed.m4 \
+ $(top_srcdir)/m4/sockpeercred.m4 $(top_srcdir)/m4/sql.m4 \
+ $(top_srcdir)/m4/ssl.m4 $(top_srcdir)/m4/st_tim.m4 \
+ $(top_srcdir)/m4/static_array.m4 $(top_srcdir)/m4/test_with.m4 \
+ $(top_srcdir)/m4/time_t.m4 $(top_srcdir)/m4/typeof.m4 \
+ $(top_srcdir)/m4/typeof_dev_t.m4 \
+ $(top_srcdir)/m4/uoff_t_max.m4 $(top_srcdir)/m4/vararg.m4 \
+ $(top_srcdir)/m4/want_apparmor.m4 \
+ $(top_srcdir)/m4/want_bsdauth.m4 \
+ $(top_srcdir)/m4/want_bzlib.m4 \
+ $(top_srcdir)/m4/want_cassandra.m4 \
+ $(top_srcdir)/m4/want_cdb.m4 \
+ $(top_srcdir)/m4/want_checkpassword.m4 \
+ $(top_srcdir)/m4/want_clucene.m4 $(top_srcdir)/m4/want_db.m4 \
+ $(top_srcdir)/m4/want_gssapi.m4 $(top_srcdir)/m4/want_icu.m4 \
+ $(top_srcdir)/m4/want_ldap.m4 $(top_srcdir)/m4/want_lua.m4 \
+ $(top_srcdir)/m4/want_lz4.m4 $(top_srcdir)/m4/want_lzma.m4 \
+ $(top_srcdir)/m4/want_mysql.m4 $(top_srcdir)/m4/want_pam.m4 \
+ $(top_srcdir)/m4/want_passwd.m4 $(top_srcdir)/m4/want_pgsql.m4 \
+ $(top_srcdir)/m4/want_prefetch.m4 \
+ $(top_srcdir)/m4/want_shadow.m4 \
+ $(top_srcdir)/m4/want_sodium.m4 $(top_srcdir)/m4/want_solr.m4 \
+ $(top_srcdir)/m4/want_sqlite.m4 \
+ $(top_srcdir)/m4/want_stemmer.m4 \
+ $(top_srcdir)/m4/want_systemd.m4 \
+ $(top_srcdir)/m4/want_textcat.m4 \
+ $(top_srcdir)/m4/want_unwind.m4 $(top_srcdir)/m4/want_zlib.m4 \
+ $(top_srcdir)/m4/want_zstd.m4 $(top_srcdir)/configure.ac
+am__configure_deps = $(am__aclocal_m4_deps) $(CONFIGURE_DEPENDENCIES) \
+ $(ACLOCAL_M4)
+DIST_COMMON = $(srcdir)/Makefile.am $(pkginc_lib_HEADERS) \
+ $(am__DIST_COMMON)
+mkinstalldirs = $(install_sh) -d
+CONFIG_HEADER = $(top_builddir)/config.h
+CONFIG_CLEAN_FILES =
+CONFIG_CLEAN_VPATH_FILES =
+@USE_FUZZER_TRUE@am__EXEEXT_1 = fuzz-smtp-server$(EXEEXT)
+am__EXEEXT_2 = $(am__EXEEXT_1)
+am__EXEEXT_3 = test-smtp-syntax$(EXEEXT) test-smtp-address$(EXEEXT) \
+ test-smtp-params$(EXEEXT) test-smtp-reply$(EXEEXT) \
+ test-smtp-command-parser$(EXEEXT) test-smtp-payload$(EXEEXT) \
+ test-smtp-submit$(EXEEXT) test-smtp-client-errors$(EXEEXT) \
+ test-smtp-server-errors$(EXEEXT)
+am__EXEEXT_4 =
+PROGRAMS = $(noinst_PROGRAMS)
+LTLIBRARIES = $(noinst_LTLIBRARIES)
+libsmtp_la_LIBADD =
+am__objects_1 = smtp-server-cmd-helo.lo smtp-server-cmd-starttls.lo \
+ smtp-server-cmd-auth.lo smtp-server-cmd-mail.lo \
+ smtp-server-cmd-rcpt.lo smtp-server-cmd-data.lo \
+ smtp-server-cmd-rset.lo smtp-server-cmd-noop.lo \
+ smtp-server-cmd-quit.lo smtp-server-cmd-vrfy.lo \
+ smtp-server-cmd-xclient.lo
+am_libsmtp_la_OBJECTS = smtp-parser.lo smtp-syntax.lo smtp-address.lo \
+ smtp-common.lo smtp-params.lo smtp-reply.lo \
+ smtp-reply-parser.lo smtp-command-parser.lo \
+ smtp-client-command.lo smtp-client-transaction.lo \
+ smtp-client-connection.lo smtp-client.lo $(am__objects_1) \
+ smtp-server-reply.lo smtp-server-command.lo \
+ smtp-server-recipient.lo smtp-server-transaction.lo \
+ smtp-server-connection.lo smtp-server.lo \
+ smtp-submit-settings.lo smtp-submit.lo
+libsmtp_la_OBJECTS = $(am_libsmtp_la_OBJECTS)
+AM_V_lt = $(am__v_lt_@AM_V@)
+am__v_lt_ = $(am__v_lt_@AM_DEFAULT_V@)
+am__v_lt_0 = --silent
+am__v_lt_1 =
+am_fuzz_smtp_server_OBJECTS = \
+ fuzz_smtp_server-fuzz-smtp-server.$(OBJEXT)
+fuzz_smtp_server_OBJECTS = $(am_fuzz_smtp_server_OBJECTS)
+am__DEPENDENCIES_1 =
+am__DEPENDENCIES_2 = $(noinst_LTLIBRARIES) \
+ ../lib-program-client/libprogram_client.la \
+ ../lib-dns/libdns.la ../lib-mail/libmail.la \
+ ../lib-charset/libcharset.la ../lib-master/libmaster.la \
+ ../lib-auth/libauth.la ../lib-ssl-iostream/libssl_iostream.la \
+ ../lib-settings/libsettings.la ../lib-sasl/libsasl.la \
+ ../lib-test/libtest.la ../lib/liblib.la $(am__DEPENDENCIES_1)
+fuzz_smtp_server_LINK = $(LIBTOOL) $(AM_V_lt) --tag=CXX \
+ $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=link $(CXXLD) \
+ $(AM_CXXFLAGS) $(CXXFLAGS) $(fuzz_smtp_server_LDFLAGS) \
+ $(LDFLAGS) -o $@
+am_test_smtp_address_OBJECTS = test-smtp-address.$(OBJEXT)
+test_smtp_address_OBJECTS = $(am_test_smtp_address_OBJECTS)
+test_smtp_address_LINK = $(LIBTOOL) $(AM_V_lt) --tag=CC \
+ $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=link $(CCLD) \
+ $(AM_CFLAGS) $(CFLAGS) $(test_smtp_address_LDFLAGS) $(LDFLAGS) \
+ -o $@
+am_test_smtp_client_errors_OBJECTS = \
+ test-smtp-client-errors.$(OBJEXT)
+test_smtp_client_errors_OBJECTS = \
+ $(am_test_smtp_client_errors_OBJECTS)
+test_smtp_client_errors_LINK = $(LIBTOOL) $(AM_V_lt) --tag=CC \
+ $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=link $(CCLD) \
+ $(AM_CFLAGS) $(CFLAGS) $(test_smtp_client_errors_LDFLAGS) \
+ $(LDFLAGS) -o $@
+am_test_smtp_command_parser_OBJECTS = \
+ test-smtp-command-parser.$(OBJEXT)
+test_smtp_command_parser_OBJECTS = \
+ $(am_test_smtp_command_parser_OBJECTS)
+test_smtp_command_parser_LINK = $(LIBTOOL) $(AM_V_lt) --tag=CC \
+ $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=link $(CCLD) \
+ $(AM_CFLAGS) $(CFLAGS) $(test_smtp_command_parser_LDFLAGS) \
+ $(LDFLAGS) -o $@
+am_test_smtp_params_OBJECTS = test-smtp-params.$(OBJEXT)
+test_smtp_params_OBJECTS = $(am_test_smtp_params_OBJECTS)
+test_smtp_params_LINK = $(LIBTOOL) $(AM_V_lt) --tag=CC \
+ $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=link $(CCLD) \
+ $(AM_CFLAGS) $(CFLAGS) $(test_smtp_params_LDFLAGS) $(LDFLAGS) \
+ -o $@
+am_test_smtp_payload_OBJECTS = test-smtp-payload.$(OBJEXT)
+test_smtp_payload_OBJECTS = $(am_test_smtp_payload_OBJECTS)
+test_smtp_payload_LINK = $(LIBTOOL) $(AM_V_lt) --tag=CC \
+ $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=link $(CCLD) \
+ $(AM_CFLAGS) $(CFLAGS) $(test_smtp_payload_LDFLAGS) $(LDFLAGS) \
+ -o $@
+am_test_smtp_reply_OBJECTS = test-smtp-reply.$(OBJEXT)
+test_smtp_reply_OBJECTS = $(am_test_smtp_reply_OBJECTS)
+test_smtp_reply_LINK = $(LIBTOOL) $(AM_V_lt) --tag=CC \
+ $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=link $(CCLD) \
+ $(AM_CFLAGS) $(CFLAGS) $(test_smtp_reply_LDFLAGS) $(LDFLAGS) \
+ -o $@
+am_test_smtp_server_errors_OBJECTS = \
+ test-smtp-server-errors.$(OBJEXT)
+test_smtp_server_errors_OBJECTS = \
+ $(am_test_smtp_server_errors_OBJECTS)
+test_smtp_server_errors_LINK = $(LIBTOOL) $(AM_V_lt) --tag=CC \
+ $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=link $(CCLD) \
+ $(AM_CFLAGS) $(CFLAGS) $(test_smtp_server_errors_LDFLAGS) \
+ $(LDFLAGS) -o $@
+am_test_smtp_submit_OBJECTS = test-smtp-submit.$(OBJEXT)
+test_smtp_submit_OBJECTS = $(am_test_smtp_submit_OBJECTS)
+test_smtp_submit_LINK = $(LIBTOOL) $(AM_V_lt) --tag=CC \
+ $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=link $(CCLD) \
+ $(AM_CFLAGS) $(CFLAGS) $(test_smtp_submit_LDFLAGS) $(LDFLAGS) \
+ -o $@
+am_test_smtp_syntax_OBJECTS = test-smtp-syntax.$(OBJEXT)
+test_smtp_syntax_OBJECTS = $(am_test_smtp_syntax_OBJECTS)
+AM_V_P = $(am__v_P_@AM_V@)
+am__v_P_ = $(am__v_P_@AM_DEFAULT_V@)
+am__v_P_0 = false
+am__v_P_1 = :
+AM_V_GEN = $(am__v_GEN_@AM_V@)
+am__v_GEN_ = $(am__v_GEN_@AM_DEFAULT_V@)
+am__v_GEN_0 = @echo " GEN " $@;
+am__v_GEN_1 =
+AM_V_at = $(am__v_at_@AM_V@)
+am__v_at_ = $(am__v_at_@AM_DEFAULT_V@)
+am__v_at_0 = @
+am__v_at_1 =
+DEFAULT_INCLUDES = -I.@am__isrc@ -I$(top_builddir)
+depcomp = $(SHELL) $(top_srcdir)/depcomp
+am__maybe_remake_depfiles = depfiles
+am__depfiles_remade = \
+ ./$(DEPDIR)/fuzz_smtp_server-force-cxx-linking.Po \
+ ./$(DEPDIR)/fuzz_smtp_server-fuzz-smtp-server.Po \
+ ./$(DEPDIR)/smtp-address.Plo \
+ ./$(DEPDIR)/smtp-client-command.Plo \
+ ./$(DEPDIR)/smtp-client-connection.Plo \
+ ./$(DEPDIR)/smtp-client-transaction.Plo \
+ ./$(DEPDIR)/smtp-client.Plo \
+ ./$(DEPDIR)/smtp-command-parser.Plo \
+ ./$(DEPDIR)/smtp-common.Plo ./$(DEPDIR)/smtp-params.Plo \
+ ./$(DEPDIR)/smtp-parser.Plo ./$(DEPDIR)/smtp-reply-parser.Plo \
+ ./$(DEPDIR)/smtp-reply.Plo \
+ ./$(DEPDIR)/smtp-server-cmd-auth.Plo \
+ ./$(DEPDIR)/smtp-server-cmd-data.Plo \
+ ./$(DEPDIR)/smtp-server-cmd-helo.Plo \
+ ./$(DEPDIR)/smtp-server-cmd-mail.Plo \
+ ./$(DEPDIR)/smtp-server-cmd-noop.Plo \
+ ./$(DEPDIR)/smtp-server-cmd-quit.Plo \
+ ./$(DEPDIR)/smtp-server-cmd-rcpt.Plo \
+ ./$(DEPDIR)/smtp-server-cmd-rset.Plo \
+ ./$(DEPDIR)/smtp-server-cmd-starttls.Plo \
+ ./$(DEPDIR)/smtp-server-cmd-vrfy.Plo \
+ ./$(DEPDIR)/smtp-server-cmd-xclient.Plo \
+ ./$(DEPDIR)/smtp-server-command.Plo \
+ ./$(DEPDIR)/smtp-server-connection.Plo \
+ ./$(DEPDIR)/smtp-server-recipient.Plo \
+ ./$(DEPDIR)/smtp-server-reply.Plo \
+ ./$(DEPDIR)/smtp-server-transaction.Plo \
+ ./$(DEPDIR)/smtp-server.Plo \
+ ./$(DEPDIR)/smtp-submit-settings.Plo \
+ ./$(DEPDIR)/smtp-submit.Plo ./$(DEPDIR)/smtp-syntax.Plo \
+ ./$(DEPDIR)/test-smtp-address.Po \
+ ./$(DEPDIR)/test-smtp-client-errors.Po \
+ ./$(DEPDIR)/test-smtp-command-parser.Po \
+ ./$(DEPDIR)/test-smtp-params.Po \
+ ./$(DEPDIR)/test-smtp-payload.Po \
+ ./$(DEPDIR)/test-smtp-reply.Po \
+ ./$(DEPDIR)/test-smtp-server-errors.Po \
+ ./$(DEPDIR)/test-smtp-submit.Po \
+ ./$(DEPDIR)/test-smtp-syntax.Po
+am__mv = mv -f
+COMPILE = $(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) \
+ $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS)
+LTCOMPILE = $(LIBTOOL) $(AM_V_lt) --tag=CC $(AM_LIBTOOLFLAGS) \
+ $(LIBTOOLFLAGS) --mode=compile $(CC) $(DEFS) \
+ $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) $(CPPFLAGS) \
+ $(AM_CFLAGS) $(CFLAGS)
+AM_V_CC = $(am__v_CC_@AM_V@)
+am__v_CC_ = $(am__v_CC_@AM_DEFAULT_V@)
+am__v_CC_0 = @echo " CC " $@;
+am__v_CC_1 =
+CCLD = $(CC)
+LINK = $(LIBTOOL) $(AM_V_lt) --tag=CC $(AM_LIBTOOLFLAGS) \
+ $(LIBTOOLFLAGS) --mode=link $(CCLD) $(AM_CFLAGS) $(CFLAGS) \
+ $(AM_LDFLAGS) $(LDFLAGS) -o $@
+AM_V_CCLD = $(am__v_CCLD_@AM_V@)
+am__v_CCLD_ = $(am__v_CCLD_@AM_DEFAULT_V@)
+am__v_CCLD_0 = @echo " CCLD " $@;
+am__v_CCLD_1 =
+CXXCOMPILE = $(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) \
+ $(AM_CPPFLAGS) $(CPPFLAGS) $(AM_CXXFLAGS) $(CXXFLAGS)
+LTCXXCOMPILE = $(LIBTOOL) $(AM_V_lt) --tag=CXX $(AM_LIBTOOLFLAGS) \
+ $(LIBTOOLFLAGS) --mode=compile $(CXX) $(DEFS) \
+ $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) $(CPPFLAGS) \
+ $(AM_CXXFLAGS) $(CXXFLAGS)
+AM_V_CXX = $(am__v_CXX_@AM_V@)
+am__v_CXX_ = $(am__v_CXX_@AM_DEFAULT_V@)
+am__v_CXX_0 = @echo " CXX " $@;
+am__v_CXX_1 =
+CXXLD = $(CXX)
+CXXLINK = $(LIBTOOL) $(AM_V_lt) --tag=CXX $(AM_LIBTOOLFLAGS) \
+ $(LIBTOOLFLAGS) --mode=link $(CXXLD) $(AM_CXXFLAGS) \
+ $(CXXFLAGS) $(AM_LDFLAGS) $(LDFLAGS) -o $@
+AM_V_CXXLD = $(am__v_CXXLD_@AM_V@)
+am__v_CXXLD_ = $(am__v_CXXLD_@AM_DEFAULT_V@)
+am__v_CXXLD_0 = @echo " CXXLD " $@;
+am__v_CXXLD_1 =
+SOURCES = $(libsmtp_la_SOURCES) $(fuzz_smtp_server_SOURCES) \
+ $(nodist_EXTRA_fuzz_smtp_server_SOURCES) \
+ $(test_smtp_address_SOURCES) \
+ $(test_smtp_client_errors_SOURCES) \
+ $(test_smtp_command_parser_SOURCES) \
+ $(test_smtp_params_SOURCES) $(test_smtp_payload_SOURCES) \
+ $(test_smtp_reply_SOURCES) $(test_smtp_server_errors_SOURCES) \
+ $(test_smtp_submit_SOURCES) $(test_smtp_syntax_SOURCES)
+DIST_SOURCES = $(libsmtp_la_SOURCES) $(fuzz_smtp_server_SOURCES) \
+ $(test_smtp_address_SOURCES) \
+ $(test_smtp_client_errors_SOURCES) \
+ $(test_smtp_command_parser_SOURCES) \
+ $(test_smtp_params_SOURCES) $(test_smtp_payload_SOURCES) \
+ $(test_smtp_reply_SOURCES) $(test_smtp_server_errors_SOURCES) \
+ $(test_smtp_submit_SOURCES) $(test_smtp_syntax_SOURCES)
+am__can_run_installinfo = \
+ case $$AM_UPDATE_INFO_DIR in \
+ n|no|NO) false;; \
+ *) (install-info --version) >/dev/null 2>&1;; \
+ esac
+am__vpath_adj_setup = srcdirstrip=`echo "$(srcdir)" | sed 's|.|.|g'`;
+am__vpath_adj = case $$p in \
+ $(srcdir)/*) f=`echo "$$p" | sed "s|^$$srcdirstrip/||"`;; \
+ *) f=$$p;; \
+ esac;
+am__strip_dir = f=`echo $$p | sed -e 's|^.*/||'`;
+am__install_max = 40
+am__nobase_strip_setup = \
+ srcdirstrip=`echo "$(srcdir)" | sed 's/[].[^$$\\*|]/\\\\&/g'`
+am__nobase_strip = \
+ for p in $$list; do echo "$$p"; done | sed -e "s|$$srcdirstrip/||"
+am__nobase_list = $(am__nobase_strip_setup); \
+ for p in $$list; do echo "$$p $$p"; done | \
+ sed "s| $$srcdirstrip/| |;"' / .*\//!s/ .*/ ./; s,\( .*\)/[^/]*$$,\1,' | \
+ $(AWK) 'BEGIN { files["."] = "" } { files[$$2] = files[$$2] " " $$1; \
+ if (++n[$$2] == $(am__install_max)) \
+ { print $$2, files[$$2]; n[$$2] = 0; files[$$2] = "" } } \
+ END { for (dir in files) print dir, files[dir] }'
+am__base_list = \
+ sed '$$!N;$$!N;$$!N;$$!N;$$!N;$$!N;$$!N;s/\n/ /g' | \
+ sed '$$!N;$$!N;$$!N;$$!N;s/\n/ /g'
+am__uninstall_files_from_dir = { \
+ test -z "$$files" \
+ || { test ! -d "$$dir" && test ! -f "$$dir" && test ! -r "$$dir"; } \
+ || { echo " ( cd '$$dir' && rm -f" $$files ")"; \
+ $(am__cd) "$$dir" && rm -f $$files; }; \
+ }
+am__installdirs = "$(DESTDIR)$(pkginc_libdir)"
+HEADERS = $(pkginc_lib_HEADERS)
+am__tagged_files = $(HEADERS) $(SOURCES) $(TAGS_FILES) $(LISP)
+# Read a list of newline-separated strings from the standard input,
+# and print each of them once, without duplicates. Input order is
+# *not* preserved.
+am__uniquify_input = $(AWK) '\
+ BEGIN { nonempty = 0; } \
+ { items[$$0] = 1; nonempty = 1; } \
+ END { if (nonempty) { for (i in items) print i; }; } \
+'
+# Make sure the list of sources is unique. This is necessary because,
+# e.g., the same source file might be shared among _SOURCES variables
+# for different programs/libraries.
+am__define_uniq_tagged_files = \
+ list='$(am__tagged_files)'; \
+ unique=`for i in $$list; do \
+ if test -f "$$i"; then echo $$i; else echo $(srcdir)/$$i; fi; \
+ done | $(am__uniquify_input)`
+ETAGS = etags
+CTAGS = ctags
+am__DIST_COMMON = $(srcdir)/Makefile.in $(top_srcdir)/depcomp
+DISTFILES = $(DIST_COMMON) $(DIST_SOURCES) $(TEXINFOS) $(EXTRA_DIST)
+ACLOCAL = @ACLOCAL@
+ACLOCAL_AMFLAGS = @ACLOCAL_AMFLAGS@
+AMTAR = @AMTAR@
+AM_DEFAULT_VERBOSITY = @AM_DEFAULT_VERBOSITY@
+APPARMOR_LIBS = @APPARMOR_LIBS@
+AR = @AR@
+AUTH_CFLAGS = @AUTH_CFLAGS@
+AUTH_LIBS = @AUTH_LIBS@
+AUTOCONF = @AUTOCONF@
+AUTOHEADER = @AUTOHEADER@
+AUTOMAKE = @AUTOMAKE@
+AWK = @AWK@
+BINARY_CFLAGS = @BINARY_CFLAGS@
+BINARY_LDFLAGS = @BINARY_LDFLAGS@
+BISON = @BISON@
+CASSANDRA_CFLAGS = @CASSANDRA_CFLAGS@
+CASSANDRA_LIBS = @CASSANDRA_LIBS@
+CC = @CC@
+CCDEPMODE = @CCDEPMODE@
+CDB_LIBS = @CDB_LIBS@
+CFLAGS = @CFLAGS@
+CLUCENE_CFLAGS = @CLUCENE_CFLAGS@
+CLUCENE_LIBS = @CLUCENE_LIBS@
+COMPRESS_LIBS = @COMPRESS_LIBS@
+CPP = @CPP@
+CPPFLAGS = @CPPFLAGS@
+CRYPT_LIBS = @CRYPT_LIBS@
+CXX = @CXX@
+CXXCPP = @CXXCPP@
+CXXDEPMODE = @CXXDEPMODE@
+CXXFLAGS = @CXXFLAGS@
+CYGPATH_W = @CYGPATH_W@
+DEFS = @DEFS@
+DEPDIR = @DEPDIR@
+DICT_LIBS = @DICT_LIBS@
+DLLIB = @DLLIB@
+DLLTOOL = @DLLTOOL@
+DSYMUTIL = @DSYMUTIL@
+DUMPBIN = @DUMPBIN@
+ECHO_C = @ECHO_C@
+ECHO_N = @ECHO_N@
+ECHO_T = @ECHO_T@
+EGREP = @EGREP@
+EXEEXT = @EXEEXT@
+FGREP = @FGREP@
+FLEX = @FLEX@
+FUZZER_CPPFLAGS = @FUZZER_CPPFLAGS@
+FUZZER_LDFLAGS = @FUZZER_LDFLAGS@
+GREP = @GREP@
+INSTALL = @INSTALL@
+INSTALL_DATA = @INSTALL_DATA@
+INSTALL_PROGRAM = @INSTALL_PROGRAM@
+INSTALL_SCRIPT = @INSTALL_SCRIPT@
+INSTALL_STRIP_PROGRAM = @INSTALL_STRIP_PROGRAM@
+KRB5CONFIG = @KRB5CONFIG@
+KRB5_CFLAGS = @KRB5_CFLAGS@
+KRB5_LIBS = @KRB5_LIBS@
+LD = @LD@
+LDAP_LIBS = @LDAP_LIBS@
+LDFLAGS = @LDFLAGS@
+LD_NO_WHOLE_ARCHIVE = @LD_NO_WHOLE_ARCHIVE@
+LD_WHOLE_ARCHIVE = @LD_WHOLE_ARCHIVE@
+LIBCAP = @LIBCAP@
+LIBDOVECOT = @LIBDOVECOT@
+LIBDOVECOT_COMPRESS = @LIBDOVECOT_COMPRESS@
+LIBDOVECOT_DEPS = @LIBDOVECOT_DEPS@
+LIBDOVECOT_DSYNC = @LIBDOVECOT_DSYNC@
+LIBDOVECOT_LA_LIBS = @LIBDOVECOT_LA_LIBS@
+LIBDOVECOT_LDA = @LIBDOVECOT_LDA@
+LIBDOVECOT_LDAP = @LIBDOVECOT_LDAP@
+LIBDOVECOT_LIBFTS = @LIBDOVECOT_LIBFTS@
+LIBDOVECOT_LIBFTS_DEPS = @LIBDOVECOT_LIBFTS_DEPS@
+LIBDOVECOT_LOGIN = @LIBDOVECOT_LOGIN@
+LIBDOVECOT_LUA = @LIBDOVECOT_LUA@
+LIBDOVECOT_LUA_DEPS = @LIBDOVECOT_LUA_DEPS@
+LIBDOVECOT_SQL = @LIBDOVECOT_SQL@
+LIBDOVECOT_STORAGE = @LIBDOVECOT_STORAGE@
+LIBDOVECOT_STORAGE_DEPS = @LIBDOVECOT_STORAGE_DEPS@
+LIBEXTTEXTCAT_CFLAGS = @LIBEXTTEXTCAT_CFLAGS@
+LIBEXTTEXTCAT_LIBS = @LIBEXTTEXTCAT_LIBS@
+LIBICONV = @LIBICONV@
+LIBICU_CFLAGS = @LIBICU_CFLAGS@
+LIBICU_LIBS = @LIBICU_LIBS@
+LIBOBJS = @LIBOBJS@
+LIBS = @LIBS@
+LIBSODIUM_CFLAGS = @LIBSODIUM_CFLAGS@
+LIBSODIUM_LIBS = @LIBSODIUM_LIBS@
+LIBTIRPC_CFLAGS = @LIBTIRPC_CFLAGS@
+LIBTIRPC_LIBS = @LIBTIRPC_LIBS@
+LIBTOOL = @LIBTOOL@
+LIBUNWIND_CFLAGS = @LIBUNWIND_CFLAGS@
+LIBUNWIND_LIBS = @LIBUNWIND_LIBS@
+LIBWRAP_LIBS = @LIBWRAP_LIBS@
+LINKED_STORAGE_LDADD = @LINKED_STORAGE_LDADD@
+LIPO = @LIPO@
+LN_S = @LN_S@
+LTLIBICONV = @LTLIBICONV@
+LTLIBOBJS = @LTLIBOBJS@
+LT_SYS_LIBRARY_PATH = @LT_SYS_LIBRARY_PATH@
+LUA_CFLAGS = @LUA_CFLAGS@
+LUA_LIBS = @LUA_LIBS@
+MAINT = @MAINT@
+MAKEINFO = @MAKEINFO@
+MANIFEST_TOOL = @MANIFEST_TOOL@
+MKDIR_P = @MKDIR_P@
+MODULE_LIBS = @MODULE_LIBS@
+MODULE_SUFFIX = @MODULE_SUFFIX@
+MYSQL_CFLAGS = @MYSQL_CFLAGS@
+MYSQL_CONFIG = @MYSQL_CONFIG@
+MYSQL_LIBS = @MYSQL_LIBS@
+NM = @NM@
+NMEDIT = @NMEDIT@
+NOPLUGIN_LDFLAGS = @NOPLUGIN_LDFLAGS@
+OBJDUMP = @OBJDUMP@
+OBJEXT = @OBJEXT@
+OTOOL = @OTOOL@
+OTOOL64 = @OTOOL64@
+PACKAGE = @PACKAGE@
+PACKAGE_BUGREPORT = @PACKAGE_BUGREPORT@
+PACKAGE_NAME = @PACKAGE_NAME@
+PACKAGE_STRING = @PACKAGE_STRING@
+PACKAGE_TARNAME = @PACKAGE_TARNAME@
+PACKAGE_URL = @PACKAGE_URL@
+PACKAGE_VERSION = @PACKAGE_VERSION@
+PANDOC = @PANDOC@
+PATH_SEPARATOR = @PATH_SEPARATOR@
+PGSQL_CFLAGS = @PGSQL_CFLAGS@
+PGSQL_LIBS = @PGSQL_LIBS@
+PG_CONFIG = @PG_CONFIG@
+PIE_CFLAGS = @PIE_CFLAGS@
+PIE_LDFLAGS = @PIE_LDFLAGS@
+PKG_CONFIG = @PKG_CONFIG@
+PKG_CONFIG_LIBDIR = @PKG_CONFIG_LIBDIR@
+PKG_CONFIG_PATH = @PKG_CONFIG_PATH@
+QUOTA_LIBS = @QUOTA_LIBS@
+RANLIB = @RANLIB@
+RELRO_LDFLAGS = @RELRO_LDFLAGS@
+RPCGEN = @RPCGEN@
+RUN_TEST = @RUN_TEST@
+SED = @SED@
+SETTING_FILES = @SETTING_FILES@
+SET_MAKE = @SET_MAKE@
+SHELL = @SHELL@
+SQLITE_CFLAGS = @SQLITE_CFLAGS@
+SQLITE_LIBS = @SQLITE_LIBS@
+SQL_CFLAGS = @SQL_CFLAGS@
+SQL_LIBS = @SQL_LIBS@
+SSL_CFLAGS = @SSL_CFLAGS@
+SSL_LIBS = @SSL_LIBS@
+STRIP = @STRIP@
+SYSTEMD_CFLAGS = @SYSTEMD_CFLAGS@
+SYSTEMD_LIBS = @SYSTEMD_LIBS@
+VALGRIND = @VALGRIND@
+VERSION = @VERSION@
+ZSTD_CFLAGS = @ZSTD_CFLAGS@
+ZSTD_LIBS = @ZSTD_LIBS@
+abs_builddir = @abs_builddir@
+abs_srcdir = @abs_srcdir@
+abs_top_builddir = @abs_top_builddir@
+abs_top_srcdir = @abs_top_srcdir@
+ac_ct_AR = @ac_ct_AR@
+ac_ct_CC = @ac_ct_CC@
+ac_ct_CXX = @ac_ct_CXX@
+ac_ct_DUMPBIN = @ac_ct_DUMPBIN@
+am__include = @am__include@
+am__leading_dot = @am__leading_dot@
+am__quote = @am__quote@
+am__tar = @am__tar@
+am__untar = @am__untar@
+bindir = @bindir@
+build = @build@
+build_alias = @build_alias@
+build_cpu = @build_cpu@
+build_os = @build_os@
+build_vendor = @build_vendor@
+builddir = @builddir@
+datadir = @datadir@
+datarootdir = @datarootdir@
+dict_drivers = @dict_drivers@
+docdir = @docdir@
+dvidir = @dvidir@
+exec_prefix = @exec_prefix@
+host = @host@
+host_alias = @host_alias@
+host_cpu = @host_cpu@
+host_os = @host_os@
+host_vendor = @host_vendor@
+htmldir = @htmldir@
+includedir = @includedir@
+infodir = @infodir@
+install_sh = @install_sh@
+libdir = @libdir@
+libexecdir = @libexecdir@
+localedir = @localedir@
+localstatedir = @localstatedir@
+mandir = @mandir@
+mkdir_p = @mkdir_p@
+moduledir = @moduledir@
+oldincludedir = @oldincludedir@
+pdfdir = @pdfdir@
+prefix = @prefix@
+program_transform_name = @program_transform_name@
+psdir = @psdir@
+rundir = @rundir@
+runstatedir = @runstatedir@
+sbindir = @sbindir@
+sharedstatedir = @sharedstatedir@
+sql_drivers = @sql_drivers@
+srcdir = @srcdir@
+ssldir = @ssldir@
+statedir = @statedir@
+sysconfdir = @sysconfdir@
+systemdservicetype = @systemdservicetype@
+systemdsystemunitdir = @systemdsystemunitdir@
+target_alias = @target_alias@
+top_build_prefix = @top_build_prefix@
+top_builddir = @top_builddir@
+top_srcdir = @top_srcdir@
+noinst_LTLIBRARIES = libsmtp.la
+AM_CPPFLAGS = \
+ -I$(top_srcdir)/src/lib \
+ -I$(top_srcdir)/src/lib-test \
+ -I$(top_srcdir)/src/lib-settings \
+ -I$(top_srcdir)/src/lib-master \
+ -I$(top_srcdir)/src/lib-sasl \
+ -I$(top_srcdir)/src/lib-ssl-iostream \
+ -I$(top_srcdir)/src/lib-dns \
+ -I$(top_srcdir)/src/lib-program-client \
+ -I$(top_srcdir)/src/lib-mail \
+ -DTEST_BIN_DIR=\"$(abs_srcdir)/test-bin\"
+
+smtp_server_cmds = \
+ smtp-server-cmd-helo.c \
+ smtp-server-cmd-starttls.c \
+ smtp-server-cmd-auth.c \
+ smtp-server-cmd-mail.c \
+ smtp-server-cmd-rcpt.c \
+ smtp-server-cmd-data.c \
+ smtp-server-cmd-rset.c \
+ smtp-server-cmd-noop.c \
+ smtp-server-cmd-quit.c \
+ smtp-server-cmd-vrfy.c \
+ smtp-server-cmd-xclient.c
+
+libsmtp_la_SOURCES = \
+ smtp-parser.c \
+ smtp-syntax.c \
+ smtp-address.c \
+ smtp-common.c \
+ smtp-params.c \
+ smtp-reply.c \
+ smtp-reply-parser.c \
+ smtp-command-parser.c \
+ smtp-client-command.c \
+ smtp-client-transaction.c \
+ smtp-client-connection.c \
+ smtp-client.c \
+ $(smtp_server_cmds) \
+ smtp-server-reply.c \
+ smtp-server-command.c \
+ smtp-server-recipient.c \
+ smtp-server-transaction.c \
+ smtp-server-connection.c \
+ smtp-server.c \
+ smtp-submit-settings.c \
+ smtp-submit.c
+
+headers = \
+ smtp-parser.h \
+ smtp-syntax.h \
+ smtp-address.h \
+ smtp-common.h \
+ smtp-params.h \
+ smtp-reply.h \
+ smtp-reply-parser.h \
+ smtp-command.h \
+ smtp-command-parser.h \
+ smtp-client-command.h \
+ smtp-client-transaction.h \
+ smtp-client-connection.h \
+ smtp-client-private.h \
+ smtp-client.h \
+ smtp-server-private.h \
+ smtp-server.h \
+ smtp-submit-settings.h \
+ smtp-submit.h
+
+pkginc_libdir = $(pkgincludedir)
+pkginc_lib_HEADERS = $(headers)
+test_programs = \
+ test-smtp-syntax \
+ test-smtp-address \
+ test-smtp-params \
+ test-smtp-reply \
+ test-smtp-command-parser \
+ test-smtp-payload \
+ test-smtp-submit \
+ test-smtp-client-errors \
+ test-smtp-server-errors
+
+test_nocheck_programs =
+fuzz_programs = $(am__append_1)
+EXTRA_DIST = \
+ test-bin/sendmail-exit-1.sh \
+ test-bin/sendmail-success.sh
+
+test_libs = \
+ $(noinst_LTLIBRARIES) \
+ ../lib-program-client/libprogram_client.la \
+ ../lib-dns/libdns.la \
+ ../lib-mail/libmail.la \
+ ../lib-charset/libcharset.la \
+ ../lib-master/libmaster.la \
+ ../lib-auth/libauth.la \
+ ../lib-ssl-iostream/libssl_iostream.la \
+ ../lib-settings/libsettings.la \
+ ../lib-sasl/libsasl.la \
+ ../lib-test/libtest.la \
+ ../lib/liblib.la \
+ $(MODULE_LIBS)
+
+test_deps = \
+ $(noinst_LTLIBRARIES) \
+ ../lib-program-client/libprogram_client.la \
+ ../lib-dns/libdns.la \
+ ../lib-mail/libmail.la \
+ ../lib-charset/libcharset.la \
+ ../lib-master/libmaster.la \
+ ../lib-auth/libauth.la \
+ ../lib-ssl-iostream/libssl_iostream.la \
+ ../lib-settings/libsettings.la \
+ ../lib-sasl/libsasl.la \
+ ../lib-test/libtest.la \
+ ../lib/liblib.la
+
+test_libs_ssl = $(am__append_2)
+test_smtp_syntax_SOURCES = test-smtp-syntax.c
+test_smtp_syntax_LDADD = $(test_libs)
+test_smtp_syntax_DEPENDENCIES = $(test_deps)
+test_smtp_address_SOURCES = test-smtp-address.c
+test_smtp_address_LDFLAGS = -export-dynamic
+test_smtp_address_LDADD = $(test_libs)
+test_smtp_address_DEPENDENCIES = $(test_deps)
+test_smtp_params_SOURCES = test-smtp-params.c
+test_smtp_params_LDFLAGS = -export-dynamic
+test_smtp_params_LDADD = $(test_libs)
+test_smtp_params_DEPENDENCIES = $(test_deps)
+test_smtp_reply_SOURCES = test-smtp-reply.c
+test_smtp_reply_LDFLAGS = -export-dynamic
+test_smtp_reply_LDADD = $(test_libs)
+test_smtp_reply_DEPENDENCIES = $(test_deps)
+test_smtp_command_parser_SOURCES = test-smtp-command-parser.c
+test_smtp_command_parser_LDFLAGS = -export-dynamic
+test_smtp_command_parser_LDADD = $(test_libs)
+test_smtp_command_parser_DEPENDENCIES = $(test_deps)
+test_smtp_payload_SOURCES = test-smtp-payload.c
+test_smtp_payload_LDFLAGS = -export-dynamic
+test_smtp_payload_LDADD = $(test_libs) $(test_libs_ssl)
+test_smtp_payload_DEPENDENCIES = $(test_deps)
+test_smtp_submit_SOURCES = test-smtp-submit.c
+test_smtp_submit_LDFLAGS = -export-dynamic
+test_smtp_submit_LDADD = $(test_libs)
+test_smtp_submit_DEPENDENCIES = $(test_deps)
+test_smtp_client_errors_SOURCES = test-smtp-client-errors.c
+test_smtp_client_errors_LDFLAGS = -export-dynamic
+test_smtp_client_errors_LDADD = $(test_libs) $(test_libs_ssl)
+test_smtp_client_errors_DEPENDENCIES = $(test_deps)
+test_smtp_server_errors_SOURCES = test-smtp-server-errors.c
+test_smtp_server_errors_LDFLAGS = -export-dynamic
+test_smtp_server_errors_LDADD = $(test_libs)
+test_smtp_server_errors_DEPENDENCIES = $(test_deps)
+nodist_EXTRA_fuzz_smtp_server_SOURCES = force-cxx-linking.cxx
+fuzz_smtp_server_CPPFLAGS = $(FUZZER_CPPFLAGS)
+fuzz_smtp_server_LDFLAGS = $(FUZZER_LDFLAGS)
+fuzz_smtp_server_SOURCES = fuzz-smtp-server.c
+fuzz_smtp_server_LDADD = $(test_libs)
+fuzz_smtp_server_DEPENDENCIES = $(test_deps)
+all: all-am
+
+.SUFFIXES:
+.SUFFIXES: .c .cxx .lo .o .obj
+$(srcdir)/Makefile.in: @MAINTAINER_MODE_TRUE@ $(srcdir)/Makefile.am $(am__configure_deps)
+ @for dep in $?; do \
+ case '$(am__configure_deps)' in \
+ *$$dep*) \
+ ( cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh ) \
+ && { if test -f $@; then exit 0; else break; fi; }; \
+ exit 1;; \
+ esac; \
+ done; \
+ echo ' cd $(top_srcdir) && $(AUTOMAKE) --foreign src/lib-smtp/Makefile'; \
+ $(am__cd) $(top_srcdir) && \
+ $(AUTOMAKE) --foreign src/lib-smtp/Makefile
+Makefile: $(srcdir)/Makefile.in $(top_builddir)/config.status
+ @case '$?' in \
+ *config.status*) \
+ cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh;; \
+ *) \
+ echo ' cd $(top_builddir) && $(SHELL) ./config.status $(subdir)/$@ $(am__maybe_remake_depfiles)'; \
+ cd $(top_builddir) && $(SHELL) ./config.status $(subdir)/$@ $(am__maybe_remake_depfiles);; \
+ esac;
+
+$(top_builddir)/config.status: $(top_srcdir)/configure $(CONFIG_STATUS_DEPENDENCIES)
+ cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh
+
+$(top_srcdir)/configure: @MAINTAINER_MODE_TRUE@ $(am__configure_deps)
+ cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh
+$(ACLOCAL_M4): @MAINTAINER_MODE_TRUE@ $(am__aclocal_m4_deps)
+ cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh
+$(am__aclocal_m4_deps):
+
+clean-noinstPROGRAMS:
+ @list='$(noinst_PROGRAMS)'; test -n "$$list" || exit 0; \
+ echo " rm -f" $$list; \
+ rm -f $$list || exit $$?; \
+ test -n "$(EXEEXT)" || exit 0; \
+ list=`for p in $$list; do echo "$$p"; done | sed 's/$(EXEEXT)$$//'`; \
+ echo " rm -f" $$list; \
+ rm -f $$list
+
+clean-noinstLTLIBRARIES:
+ -test -z "$(noinst_LTLIBRARIES)" || rm -f $(noinst_LTLIBRARIES)
+ @list='$(noinst_LTLIBRARIES)'; \
+ locs=`for p in $$list; do echo $$p; done | \
+ sed 's|^[^/]*$$|.|; s|/[^/]*$$||; s|$$|/so_locations|' | \
+ sort -u`; \
+ test -z "$$locs" || { \
+ echo rm -f $${locs}; \
+ rm -f $${locs}; \
+ }
+
+libsmtp.la: $(libsmtp_la_OBJECTS) $(libsmtp_la_DEPENDENCIES) $(EXTRA_libsmtp_la_DEPENDENCIES)
+ $(AM_V_CCLD)$(LINK) $(libsmtp_la_OBJECTS) $(libsmtp_la_LIBADD) $(LIBS)
+
+fuzz-smtp-server$(EXEEXT): $(fuzz_smtp_server_OBJECTS) $(fuzz_smtp_server_DEPENDENCIES) $(EXTRA_fuzz_smtp_server_DEPENDENCIES)
+ @rm -f fuzz-smtp-server$(EXEEXT)
+ $(AM_V_CXXLD)$(fuzz_smtp_server_LINK) $(fuzz_smtp_server_OBJECTS) $(fuzz_smtp_server_LDADD) $(LIBS)
+
+test-smtp-address$(EXEEXT): $(test_smtp_address_OBJECTS) $(test_smtp_address_DEPENDENCIES) $(EXTRA_test_smtp_address_DEPENDENCIES)
+ @rm -f test-smtp-address$(EXEEXT)
+ $(AM_V_CCLD)$(test_smtp_address_LINK) $(test_smtp_address_OBJECTS) $(test_smtp_address_LDADD) $(LIBS)
+
+test-smtp-client-errors$(EXEEXT): $(test_smtp_client_errors_OBJECTS) $(test_smtp_client_errors_DEPENDENCIES) $(EXTRA_test_smtp_client_errors_DEPENDENCIES)
+ @rm -f test-smtp-client-errors$(EXEEXT)
+ $(AM_V_CCLD)$(test_smtp_client_errors_LINK) $(test_smtp_client_errors_OBJECTS) $(test_smtp_client_errors_LDADD) $(LIBS)
+
+test-smtp-command-parser$(EXEEXT): $(test_smtp_command_parser_OBJECTS) $(test_smtp_command_parser_DEPENDENCIES) $(EXTRA_test_smtp_command_parser_DEPENDENCIES)
+ @rm -f test-smtp-command-parser$(EXEEXT)
+ $(AM_V_CCLD)$(test_smtp_command_parser_LINK) $(test_smtp_command_parser_OBJECTS) $(test_smtp_command_parser_LDADD) $(LIBS)
+
+test-smtp-params$(EXEEXT): $(test_smtp_params_OBJECTS) $(test_smtp_params_DEPENDENCIES) $(EXTRA_test_smtp_params_DEPENDENCIES)
+ @rm -f test-smtp-params$(EXEEXT)
+ $(AM_V_CCLD)$(test_smtp_params_LINK) $(test_smtp_params_OBJECTS) $(test_smtp_params_LDADD) $(LIBS)
+
+test-smtp-payload$(EXEEXT): $(test_smtp_payload_OBJECTS) $(test_smtp_payload_DEPENDENCIES) $(EXTRA_test_smtp_payload_DEPENDENCIES)
+ @rm -f test-smtp-payload$(EXEEXT)
+ $(AM_V_CCLD)$(test_smtp_payload_LINK) $(test_smtp_payload_OBJECTS) $(test_smtp_payload_LDADD) $(LIBS)
+
+test-smtp-reply$(EXEEXT): $(test_smtp_reply_OBJECTS) $(test_smtp_reply_DEPENDENCIES) $(EXTRA_test_smtp_reply_DEPENDENCIES)
+ @rm -f test-smtp-reply$(EXEEXT)
+ $(AM_V_CCLD)$(test_smtp_reply_LINK) $(test_smtp_reply_OBJECTS) $(test_smtp_reply_LDADD) $(LIBS)
+
+test-smtp-server-errors$(EXEEXT): $(test_smtp_server_errors_OBJECTS) $(test_smtp_server_errors_DEPENDENCIES) $(EXTRA_test_smtp_server_errors_DEPENDENCIES)
+ @rm -f test-smtp-server-errors$(EXEEXT)
+ $(AM_V_CCLD)$(test_smtp_server_errors_LINK) $(test_smtp_server_errors_OBJECTS) $(test_smtp_server_errors_LDADD) $(LIBS)
+
+test-smtp-submit$(EXEEXT): $(test_smtp_submit_OBJECTS) $(test_smtp_submit_DEPENDENCIES) $(EXTRA_test_smtp_submit_DEPENDENCIES)
+ @rm -f test-smtp-submit$(EXEEXT)
+ $(AM_V_CCLD)$(test_smtp_submit_LINK) $(test_smtp_submit_OBJECTS) $(test_smtp_submit_LDADD) $(LIBS)
+
+test-smtp-syntax$(EXEEXT): $(test_smtp_syntax_OBJECTS) $(test_smtp_syntax_DEPENDENCIES) $(EXTRA_test_smtp_syntax_DEPENDENCIES)
+ @rm -f test-smtp-syntax$(EXEEXT)
+ $(AM_V_CCLD)$(LINK) $(test_smtp_syntax_OBJECTS) $(test_smtp_syntax_LDADD) $(LIBS)
+
+mostlyclean-compile:
+ -rm -f *.$(OBJEXT)
+
+distclean-compile:
+ -rm -f *.tab.c
+
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/fuzz_smtp_server-force-cxx-linking.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/fuzz_smtp_server-fuzz-smtp-server.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/smtp-address.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/smtp-client-command.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/smtp-client-connection.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/smtp-client-transaction.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/smtp-client.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/smtp-command-parser.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/smtp-common.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/smtp-params.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/smtp-parser.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/smtp-reply-parser.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/smtp-reply.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/smtp-server-cmd-auth.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/smtp-server-cmd-data.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/smtp-server-cmd-helo.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/smtp-server-cmd-mail.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/smtp-server-cmd-noop.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/smtp-server-cmd-quit.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/smtp-server-cmd-rcpt.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/smtp-server-cmd-rset.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/smtp-server-cmd-starttls.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/smtp-server-cmd-vrfy.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/smtp-server-cmd-xclient.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/smtp-server-command.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/smtp-server-connection.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/smtp-server-recipient.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/smtp-server-reply.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/smtp-server-transaction.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/smtp-server.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/smtp-submit-settings.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/smtp-submit.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/smtp-syntax.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/test-smtp-address.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/test-smtp-client-errors.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/test-smtp-command-parser.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/test-smtp-params.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/test-smtp-payload.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/test-smtp-reply.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/test-smtp-server-errors.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/test-smtp-submit.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/test-smtp-syntax.Po@am__quote@ # am--include-marker
+
+$(am__depfiles_remade):
+ @$(MKDIR_P) $(@D)
+ @echo '# dummy' >$@-t && $(am__mv) $@-t $@
+
+am--depfiles: $(am__depfiles_remade)
+
+.c.o:
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(COMPILE) -MT $@ -MD -MP -MF $(DEPDIR)/$*.Tpo -c -o $@ $<
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/$*.Tpo $(DEPDIR)/$*.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='$<' object='$@' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(COMPILE) -c -o $@ $<
+
+.c.obj:
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(COMPILE) -MT $@ -MD -MP -MF $(DEPDIR)/$*.Tpo -c -o $@ `$(CYGPATH_W) '$<'`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/$*.Tpo $(DEPDIR)/$*.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='$<' object='$@' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(COMPILE) -c -o $@ `$(CYGPATH_W) '$<'`
+
+.c.lo:
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(LTCOMPILE) -MT $@ -MD -MP -MF $(DEPDIR)/$*.Tpo -c -o $@ $<
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/$*.Tpo $(DEPDIR)/$*.Plo
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='$<' object='$@' libtool=yes @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(LTCOMPILE) -c -o $@ $<
+
+fuzz_smtp_server-fuzz-smtp-server.o: fuzz-smtp-server.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(fuzz_smtp_server_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT fuzz_smtp_server-fuzz-smtp-server.o -MD -MP -MF $(DEPDIR)/fuzz_smtp_server-fuzz-smtp-server.Tpo -c -o fuzz_smtp_server-fuzz-smtp-server.o `test -f 'fuzz-smtp-server.c' || echo '$(srcdir)/'`fuzz-smtp-server.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/fuzz_smtp_server-fuzz-smtp-server.Tpo $(DEPDIR)/fuzz_smtp_server-fuzz-smtp-server.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='fuzz-smtp-server.c' object='fuzz_smtp_server-fuzz-smtp-server.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(fuzz_smtp_server_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o fuzz_smtp_server-fuzz-smtp-server.o `test -f 'fuzz-smtp-server.c' || echo '$(srcdir)/'`fuzz-smtp-server.c
+
+fuzz_smtp_server-fuzz-smtp-server.obj: fuzz-smtp-server.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(fuzz_smtp_server_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT fuzz_smtp_server-fuzz-smtp-server.obj -MD -MP -MF $(DEPDIR)/fuzz_smtp_server-fuzz-smtp-server.Tpo -c -o fuzz_smtp_server-fuzz-smtp-server.obj `if test -f 'fuzz-smtp-server.c'; then $(CYGPATH_W) 'fuzz-smtp-server.c'; else $(CYGPATH_W) '$(srcdir)/fuzz-smtp-server.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/fuzz_smtp_server-fuzz-smtp-server.Tpo $(DEPDIR)/fuzz_smtp_server-fuzz-smtp-server.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='fuzz-smtp-server.c' object='fuzz_smtp_server-fuzz-smtp-server.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(fuzz_smtp_server_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o fuzz_smtp_server-fuzz-smtp-server.obj `if test -f 'fuzz-smtp-server.c'; then $(CYGPATH_W) 'fuzz-smtp-server.c'; else $(CYGPATH_W) '$(srcdir)/fuzz-smtp-server.c'; fi`
+
+.cxx.o:
+@am__fastdepCXX_TRUE@ $(AM_V_CXX)$(CXXCOMPILE) -MT $@ -MD -MP -MF $(DEPDIR)/$*.Tpo -c -o $@ $<
+@am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/$*.Tpo $(DEPDIR)/$*.Po
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ $(AM_V_CXX)source='$<' object='$@' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(CXXCOMPILE) -c -o $@ $<
+
+.cxx.obj:
+@am__fastdepCXX_TRUE@ $(AM_V_CXX)$(CXXCOMPILE) -MT $@ -MD -MP -MF $(DEPDIR)/$*.Tpo -c -o $@ `$(CYGPATH_W) '$<'`
+@am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/$*.Tpo $(DEPDIR)/$*.Po
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ $(AM_V_CXX)source='$<' object='$@' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(CXXCOMPILE) -c -o $@ `$(CYGPATH_W) '$<'`
+
+.cxx.lo:
+@am__fastdepCXX_TRUE@ $(AM_V_CXX)$(LTCXXCOMPILE) -MT $@ -MD -MP -MF $(DEPDIR)/$*.Tpo -c -o $@ $<
+@am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/$*.Tpo $(DEPDIR)/$*.Plo
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ $(AM_V_CXX)source='$<' object='$@' libtool=yes @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(LTCXXCOMPILE) -c -o $@ $<
+
+fuzz_smtp_server-force-cxx-linking.o: force-cxx-linking.cxx
+@am__fastdepCXX_TRUE@ $(AM_V_CXX)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(fuzz_smtp_server_CPPFLAGS) $(CPPFLAGS) $(AM_CXXFLAGS) $(CXXFLAGS) -MT fuzz_smtp_server-force-cxx-linking.o -MD -MP -MF $(DEPDIR)/fuzz_smtp_server-force-cxx-linking.Tpo -c -o fuzz_smtp_server-force-cxx-linking.o `test -f 'force-cxx-linking.cxx' || echo '$(srcdir)/'`force-cxx-linking.cxx
+@am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/fuzz_smtp_server-force-cxx-linking.Tpo $(DEPDIR)/fuzz_smtp_server-force-cxx-linking.Po
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ $(AM_V_CXX)source='force-cxx-linking.cxx' object='fuzz_smtp_server-force-cxx-linking.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(fuzz_smtp_server_CPPFLAGS) $(CPPFLAGS) $(AM_CXXFLAGS) $(CXXFLAGS) -c -o fuzz_smtp_server-force-cxx-linking.o `test -f 'force-cxx-linking.cxx' || echo '$(srcdir)/'`force-cxx-linking.cxx
+
+fuzz_smtp_server-force-cxx-linking.obj: force-cxx-linking.cxx
+@am__fastdepCXX_TRUE@ $(AM_V_CXX)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(fuzz_smtp_server_CPPFLAGS) $(CPPFLAGS) $(AM_CXXFLAGS) $(CXXFLAGS) -MT fuzz_smtp_server-force-cxx-linking.obj -MD -MP -MF $(DEPDIR)/fuzz_smtp_server-force-cxx-linking.Tpo -c -o fuzz_smtp_server-force-cxx-linking.obj `if test -f 'force-cxx-linking.cxx'; then $(CYGPATH_W) 'force-cxx-linking.cxx'; else $(CYGPATH_W) '$(srcdir)/force-cxx-linking.cxx'; fi`
+@am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/fuzz_smtp_server-force-cxx-linking.Tpo $(DEPDIR)/fuzz_smtp_server-force-cxx-linking.Po
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ $(AM_V_CXX)source='force-cxx-linking.cxx' object='fuzz_smtp_server-force-cxx-linking.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(fuzz_smtp_server_CPPFLAGS) $(CPPFLAGS) $(AM_CXXFLAGS) $(CXXFLAGS) -c -o fuzz_smtp_server-force-cxx-linking.obj `if test -f 'force-cxx-linking.cxx'; then $(CYGPATH_W) 'force-cxx-linking.cxx'; else $(CYGPATH_W) '$(srcdir)/force-cxx-linking.cxx'; fi`
+
+mostlyclean-libtool:
+ -rm -f *.lo
+
+clean-libtool:
+ -rm -rf .libs _libs
+install-pkginc_libHEADERS: $(pkginc_lib_HEADERS)
+ @$(NORMAL_INSTALL)
+ @list='$(pkginc_lib_HEADERS)'; test -n "$(pkginc_libdir)" || list=; \
+ if test -n "$$list"; then \
+ echo " $(MKDIR_P) '$(DESTDIR)$(pkginc_libdir)'"; \
+ $(MKDIR_P) "$(DESTDIR)$(pkginc_libdir)" || exit 1; \
+ fi; \
+ for p in $$list; do \
+ if test -f "$$p"; then d=; else d="$(srcdir)/"; fi; \
+ echo "$$d$$p"; \
+ done | $(am__base_list) | \
+ while read files; do \
+ echo " $(INSTALL_HEADER) $$files '$(DESTDIR)$(pkginc_libdir)'"; \
+ $(INSTALL_HEADER) $$files "$(DESTDIR)$(pkginc_libdir)" || exit $$?; \
+ done
+
+uninstall-pkginc_libHEADERS:
+ @$(NORMAL_UNINSTALL)
+ @list='$(pkginc_lib_HEADERS)'; test -n "$(pkginc_libdir)" || list=; \
+ files=`for p in $$list; do echo $$p; done | sed -e 's|^.*/||'`; \
+ dir='$(DESTDIR)$(pkginc_libdir)'; $(am__uninstall_files_from_dir)
+
+ID: $(am__tagged_files)
+ $(am__define_uniq_tagged_files); mkid -fID $$unique
+tags: tags-am
+TAGS: tags
+
+tags-am: $(TAGS_DEPENDENCIES) $(am__tagged_files)
+ set x; \
+ here=`pwd`; \
+ $(am__define_uniq_tagged_files); \
+ shift; \
+ if test -z "$(ETAGS_ARGS)$$*$$unique"; then :; else \
+ test -n "$$unique" || unique=$$empty_fix; \
+ if test $$# -gt 0; then \
+ $(ETAGS) $(ETAGSFLAGS) $(AM_ETAGSFLAGS) $(ETAGS_ARGS) \
+ "$$@" $$unique; \
+ else \
+ $(ETAGS) $(ETAGSFLAGS) $(AM_ETAGSFLAGS) $(ETAGS_ARGS) \
+ $$unique; \
+ fi; \
+ fi
+ctags: ctags-am
+
+CTAGS: ctags
+ctags-am: $(TAGS_DEPENDENCIES) $(am__tagged_files)
+ $(am__define_uniq_tagged_files); \
+ test -z "$(CTAGS_ARGS)$$unique" \
+ || $(CTAGS) $(CTAGSFLAGS) $(AM_CTAGSFLAGS) $(CTAGS_ARGS) \
+ $$unique
+
+GTAGS:
+ here=`$(am__cd) $(top_builddir) && pwd` \
+ && $(am__cd) $(top_srcdir) \
+ && gtags -i $(GTAGS_ARGS) "$$here"
+cscopelist: cscopelist-am
+
+cscopelist-am: $(am__tagged_files)
+ list='$(am__tagged_files)'; \
+ case "$(srcdir)" in \
+ [\\/]* | ?:[\\/]*) sdir="$(srcdir)" ;; \
+ *) sdir=$(subdir)/$(srcdir) ;; \
+ esac; \
+ for i in $$list; do \
+ if test -f "$$i"; then \
+ echo "$(subdir)/$$i"; \
+ else \
+ echo "$$sdir/$$i"; \
+ fi; \
+ done >> $(top_builddir)/cscope.files
+
+distclean-tags:
+ -rm -f TAGS ID GTAGS GRTAGS GSYMS GPATH tags
+
+distdir: $(BUILT_SOURCES)
+ $(MAKE) $(AM_MAKEFLAGS) distdir-am
+
+distdir-am: $(DISTFILES)
+ @srcdirstrip=`echo "$(srcdir)" | sed 's/[].[^$$\\*]/\\\\&/g'`; \
+ topsrcdirstrip=`echo "$(top_srcdir)" | sed 's/[].[^$$\\*]/\\\\&/g'`; \
+ list='$(DISTFILES)'; \
+ dist_files=`for file in $$list; do echo $$file; done | \
+ sed -e "s|^$$srcdirstrip/||;t" \
+ -e "s|^$$topsrcdirstrip/|$(top_builddir)/|;t"`; \
+ case $$dist_files in \
+ */*) $(MKDIR_P) `echo "$$dist_files" | \
+ sed '/\//!d;s|^|$(distdir)/|;s,/[^/]*$$,,' | \
+ sort -u` ;; \
+ esac; \
+ for file in $$dist_files; do \
+ if test -f $$file || test -d $$file; then d=.; else d=$(srcdir); fi; \
+ if test -d $$d/$$file; then \
+ dir=`echo "/$$file" | sed -e 's,/[^/]*$$,,'`; \
+ if test -d "$(distdir)/$$file"; then \
+ find "$(distdir)/$$file" -type d ! -perm -700 -exec chmod u+rwx {} \;; \
+ fi; \
+ if test -d $(srcdir)/$$file && test $$d != $(srcdir); then \
+ cp -fpR $(srcdir)/$$file "$(distdir)$$dir" || exit 1; \
+ find "$(distdir)/$$file" -type d ! -perm -700 -exec chmod u+rwx {} \;; \
+ fi; \
+ cp -fpR $$d/$$file "$(distdir)$$dir" || exit 1; \
+ else \
+ test -f "$(distdir)/$$file" \
+ || cp -p $$d/$$file "$(distdir)/$$file" \
+ || exit 1; \
+ fi; \
+ done
+check-am: all-am
+ $(MAKE) $(AM_MAKEFLAGS) check-local
+check: check-am
+all-am: Makefile $(PROGRAMS) $(LTLIBRARIES) $(HEADERS)
+installdirs:
+ for dir in "$(DESTDIR)$(pkginc_libdir)"; do \
+ test -z "$$dir" || $(MKDIR_P) "$$dir"; \
+ done
+install: install-am
+install-exec: install-exec-am
+install-data: install-data-am
+uninstall: uninstall-am
+
+install-am: all-am
+ @$(MAKE) $(AM_MAKEFLAGS) install-exec-am install-data-am
+
+installcheck: installcheck-am
+install-strip:
+ if test -z '$(STRIP)'; then \
+ $(MAKE) $(AM_MAKEFLAGS) INSTALL_PROGRAM="$(INSTALL_STRIP_PROGRAM)" \
+ install_sh_PROGRAM="$(INSTALL_STRIP_PROGRAM)" INSTALL_STRIP_FLAG=-s \
+ install; \
+ else \
+ $(MAKE) $(AM_MAKEFLAGS) INSTALL_PROGRAM="$(INSTALL_STRIP_PROGRAM)" \
+ install_sh_PROGRAM="$(INSTALL_STRIP_PROGRAM)" INSTALL_STRIP_FLAG=-s \
+ "INSTALL_PROGRAM_ENV=STRIPPROG='$(STRIP)'" install; \
+ fi
+mostlyclean-generic:
+
+clean-generic:
+
+distclean-generic:
+ -test -z "$(CONFIG_CLEAN_FILES)" || rm -f $(CONFIG_CLEAN_FILES)
+ -test . = "$(srcdir)" || test -z "$(CONFIG_CLEAN_VPATH_FILES)" || rm -f $(CONFIG_CLEAN_VPATH_FILES)
+
+maintainer-clean-generic:
+ @echo "This command is intended for maintainers to use"
+ @echo "it deletes files that may require special tools to rebuild."
+clean: clean-am
+
+clean-am: clean-generic clean-libtool clean-noinstLTLIBRARIES \
+ clean-noinstPROGRAMS mostlyclean-am
+
+distclean: distclean-am
+ -rm -f ./$(DEPDIR)/fuzz_smtp_server-force-cxx-linking.Po
+ -rm -f ./$(DEPDIR)/fuzz_smtp_server-fuzz-smtp-server.Po
+ -rm -f ./$(DEPDIR)/smtp-address.Plo
+ -rm -f ./$(DEPDIR)/smtp-client-command.Plo
+ -rm -f ./$(DEPDIR)/smtp-client-connection.Plo
+ -rm -f ./$(DEPDIR)/smtp-client-transaction.Plo
+ -rm -f ./$(DEPDIR)/smtp-client.Plo
+ -rm -f ./$(DEPDIR)/smtp-command-parser.Plo
+ -rm -f ./$(DEPDIR)/smtp-common.Plo
+ -rm -f ./$(DEPDIR)/smtp-params.Plo
+ -rm -f ./$(DEPDIR)/smtp-parser.Plo
+ -rm -f ./$(DEPDIR)/smtp-reply-parser.Plo
+ -rm -f ./$(DEPDIR)/smtp-reply.Plo
+ -rm -f ./$(DEPDIR)/smtp-server-cmd-auth.Plo
+ -rm -f ./$(DEPDIR)/smtp-server-cmd-data.Plo
+ -rm -f ./$(DEPDIR)/smtp-server-cmd-helo.Plo
+ -rm -f ./$(DEPDIR)/smtp-server-cmd-mail.Plo
+ -rm -f ./$(DEPDIR)/smtp-server-cmd-noop.Plo
+ -rm -f ./$(DEPDIR)/smtp-server-cmd-quit.Plo
+ -rm -f ./$(DEPDIR)/smtp-server-cmd-rcpt.Plo
+ -rm -f ./$(DEPDIR)/smtp-server-cmd-rset.Plo
+ -rm -f ./$(DEPDIR)/smtp-server-cmd-starttls.Plo
+ -rm -f ./$(DEPDIR)/smtp-server-cmd-vrfy.Plo
+ -rm -f ./$(DEPDIR)/smtp-server-cmd-xclient.Plo
+ -rm -f ./$(DEPDIR)/smtp-server-command.Plo
+ -rm -f ./$(DEPDIR)/smtp-server-connection.Plo
+ -rm -f ./$(DEPDIR)/smtp-server-recipient.Plo
+ -rm -f ./$(DEPDIR)/smtp-server-reply.Plo
+ -rm -f ./$(DEPDIR)/smtp-server-transaction.Plo
+ -rm -f ./$(DEPDIR)/smtp-server.Plo
+ -rm -f ./$(DEPDIR)/smtp-submit-settings.Plo
+ -rm -f ./$(DEPDIR)/smtp-submit.Plo
+ -rm -f ./$(DEPDIR)/smtp-syntax.Plo
+ -rm -f ./$(DEPDIR)/test-smtp-address.Po
+ -rm -f ./$(DEPDIR)/test-smtp-client-errors.Po
+ -rm -f ./$(DEPDIR)/test-smtp-command-parser.Po
+ -rm -f ./$(DEPDIR)/test-smtp-params.Po
+ -rm -f ./$(DEPDIR)/test-smtp-payload.Po
+ -rm -f ./$(DEPDIR)/test-smtp-reply.Po
+ -rm -f ./$(DEPDIR)/test-smtp-server-errors.Po
+ -rm -f ./$(DEPDIR)/test-smtp-submit.Po
+ -rm -f ./$(DEPDIR)/test-smtp-syntax.Po
+ -rm -f Makefile
+distclean-am: clean-am distclean-compile distclean-generic \
+ distclean-tags
+
+dvi: dvi-am
+
+dvi-am:
+
+html: html-am
+
+html-am:
+
+info: info-am
+
+info-am:
+
+install-data-am: install-pkginc_libHEADERS
+
+install-dvi: install-dvi-am
+
+install-dvi-am:
+
+install-exec-am:
+
+install-html: install-html-am
+
+install-html-am:
+
+install-info: install-info-am
+
+install-info-am:
+
+install-man:
+
+install-pdf: install-pdf-am
+
+install-pdf-am:
+
+install-ps: install-ps-am
+
+install-ps-am:
+
+installcheck-am:
+
+maintainer-clean: maintainer-clean-am
+ -rm -f ./$(DEPDIR)/fuzz_smtp_server-force-cxx-linking.Po
+ -rm -f ./$(DEPDIR)/fuzz_smtp_server-fuzz-smtp-server.Po
+ -rm -f ./$(DEPDIR)/smtp-address.Plo
+ -rm -f ./$(DEPDIR)/smtp-client-command.Plo
+ -rm -f ./$(DEPDIR)/smtp-client-connection.Plo
+ -rm -f ./$(DEPDIR)/smtp-client-transaction.Plo
+ -rm -f ./$(DEPDIR)/smtp-client.Plo
+ -rm -f ./$(DEPDIR)/smtp-command-parser.Plo
+ -rm -f ./$(DEPDIR)/smtp-common.Plo
+ -rm -f ./$(DEPDIR)/smtp-params.Plo
+ -rm -f ./$(DEPDIR)/smtp-parser.Plo
+ -rm -f ./$(DEPDIR)/smtp-reply-parser.Plo
+ -rm -f ./$(DEPDIR)/smtp-reply.Plo
+ -rm -f ./$(DEPDIR)/smtp-server-cmd-auth.Plo
+ -rm -f ./$(DEPDIR)/smtp-server-cmd-data.Plo
+ -rm -f ./$(DEPDIR)/smtp-server-cmd-helo.Plo
+ -rm -f ./$(DEPDIR)/smtp-server-cmd-mail.Plo
+ -rm -f ./$(DEPDIR)/smtp-server-cmd-noop.Plo
+ -rm -f ./$(DEPDIR)/smtp-server-cmd-quit.Plo
+ -rm -f ./$(DEPDIR)/smtp-server-cmd-rcpt.Plo
+ -rm -f ./$(DEPDIR)/smtp-server-cmd-rset.Plo
+ -rm -f ./$(DEPDIR)/smtp-server-cmd-starttls.Plo
+ -rm -f ./$(DEPDIR)/smtp-server-cmd-vrfy.Plo
+ -rm -f ./$(DEPDIR)/smtp-server-cmd-xclient.Plo
+ -rm -f ./$(DEPDIR)/smtp-server-command.Plo
+ -rm -f ./$(DEPDIR)/smtp-server-connection.Plo
+ -rm -f ./$(DEPDIR)/smtp-server-recipient.Plo
+ -rm -f ./$(DEPDIR)/smtp-server-reply.Plo
+ -rm -f ./$(DEPDIR)/smtp-server-transaction.Plo
+ -rm -f ./$(DEPDIR)/smtp-server.Plo
+ -rm -f ./$(DEPDIR)/smtp-submit-settings.Plo
+ -rm -f ./$(DEPDIR)/smtp-submit.Plo
+ -rm -f ./$(DEPDIR)/smtp-syntax.Plo
+ -rm -f ./$(DEPDIR)/test-smtp-address.Po
+ -rm -f ./$(DEPDIR)/test-smtp-client-errors.Po
+ -rm -f ./$(DEPDIR)/test-smtp-command-parser.Po
+ -rm -f ./$(DEPDIR)/test-smtp-params.Po
+ -rm -f ./$(DEPDIR)/test-smtp-payload.Po
+ -rm -f ./$(DEPDIR)/test-smtp-reply.Po
+ -rm -f ./$(DEPDIR)/test-smtp-server-errors.Po
+ -rm -f ./$(DEPDIR)/test-smtp-submit.Po
+ -rm -f ./$(DEPDIR)/test-smtp-syntax.Po
+ -rm -f Makefile
+maintainer-clean-am: distclean-am maintainer-clean-generic
+
+mostlyclean: mostlyclean-am
+
+mostlyclean-am: mostlyclean-compile mostlyclean-generic \
+ mostlyclean-libtool
+
+pdf: pdf-am
+
+pdf-am:
+
+ps: ps-am
+
+ps-am:
+
+uninstall-am: uninstall-pkginc_libHEADERS
+
+.MAKE: check-am install-am install-strip
+
+.PHONY: CTAGS GTAGS TAGS all all-am am--depfiles check check-am \
+ check-local clean clean-generic clean-libtool \
+ clean-noinstLTLIBRARIES clean-noinstPROGRAMS cscopelist-am \
+ ctags ctags-am distclean distclean-compile distclean-generic \
+ distclean-libtool distclean-tags distdir dvi dvi-am html \
+ html-am info info-am install install-am install-data \
+ install-data-am install-dvi install-dvi-am install-exec \
+ install-exec-am install-html install-html-am install-info \
+ install-info-am install-man install-pdf install-pdf-am \
+ install-pkginc_libHEADERS install-ps install-ps-am \
+ install-strip installcheck installcheck-am installdirs \
+ maintainer-clean maintainer-clean-generic mostlyclean \
+ mostlyclean-compile mostlyclean-generic mostlyclean-libtool \
+ pdf pdf-am ps ps-am tags tags-am uninstall uninstall-am \
+ uninstall-pkginc_libHEADERS
+
+.PRECIOUS: Makefile
+
+
+check-local:
+ for bin in $(test_programs); do \
+ if test "$$bin" = "test-smtp-submit"; then \
+ if ! env NOCHILDREN=yes $(RUN_TEST) ./$$bin; then exit 1; fi; \
+ else \
+ if ! $(RUN_TEST) ./$$bin; then exit 1; fi; \
+ fi \
+ done
+
+# Tell versions [3.59,3.63) of GNU make to not export all variables.
+# Otherwise a system limit (for SysV at least) may be exceeded.
+.NOEXPORT:
diff --git a/src/lib-smtp/fuzz-smtp-server.c b/src/lib-smtp/fuzz-smtp-server.c
new file mode 100644
index 0000000..4b5d21d
--- /dev/null
+++ b/src/lib-smtp/fuzz-smtp-server.c
@@ -0,0 +1,102 @@
+/* Copyright (c) 2020 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "fuzzer.h"
+#include "istream.h"
+#include "ioloop.h"
+#include "smtp-server.h"
+
+static struct {
+ struct istream *data_input;
+} state = {
+ .data_input = NULL,
+};
+
+static int
+server_cmd_rcpt(void *conn_ctx ATTR_UNUSED,
+ struct smtp_server_cmd_ctx *cmd ATTR_UNUSED,
+ struct smtp_server_recipient *rcpt ATTR_UNUSED)
+{
+ return 1;
+}
+
+static int
+server_cmd_data_continue(void *conn_ctx ATTR_UNUSED,
+ struct smtp_server_cmd_ctx *cmd,
+ struct smtp_server_transaction *trans ATTR_UNUSED)
+{
+ struct istream *data_input = state.data_input;
+ const unsigned char *data;
+ size_t size;
+ ssize_t ret;
+
+ while ((ret = i_stream_read(data_input)) > 0 || ret == -2) {
+ data = i_stream_get_data(data_input, &size);
+ i_stream_skip(data_input, size);
+ if (!smtp_server_cmd_data_check_size(cmd))
+ return -1;
+ }
+
+ if (ret == 0)
+ return 0;
+ if (ret < 0 && data_input->stream_errno != 0) {
+ /* Client probably disconnected */
+ return -1;
+ }
+
+ smtp_server_reply_all(cmd, 250, "2.0.0", "Accepted");
+ return 1;
+}
+
+static int
+server_cmd_data_begin(void *conn_ctx ATTR_UNUSED,
+ struct smtp_server_cmd_ctx *cmd ATTR_UNUSED,
+ struct smtp_server_transaction *trans ATTR_UNUSED,
+ struct istream *data_input)
+{
+ state.data_input = data_input;
+ return 0;
+}
+
+static void server_connection_free(void *context)
+{
+ struct fuzzer_context *ctx = context;
+ io_loop_stop(ctx->ioloop);
+}
+
+static void test_server_continue(struct fuzzer_context *ctx)
+{
+ //instead of simple io_loop_stop so as to free input io
+ io_loop_stop_delayed(ctx->ioloop);
+}
+
+FUZZ_BEGIN_FD
+{
+ struct smtp_server_connection *conn;
+ struct smtp_server_settings smtp_server_set = {
+ .max_client_idle_time_msecs = 500,
+ .max_pipelined_commands = 16,
+ .auth_optional = TRUE,
+ };
+ struct smtp_server_callbacks server_callbacks = {
+ .conn_cmd_rcpt = server_cmd_rcpt,
+ .conn_cmd_data_begin = server_cmd_data_begin,
+ .conn_cmd_data_continue = server_cmd_data_continue,
+ .conn_free = server_connection_free,
+ };
+ struct smtp_server *smtp_server = NULL;
+ struct timeout *to;
+
+ to = timeout_add_short(10, test_server_continue, &fuzz_ctx);
+ smtp_server = smtp_server_init(&smtp_server_set);
+
+ conn = smtp_server_connection_create(smtp_server, fuzz_ctx.fd, fuzz_ctx.fd, NULL, 0,
+ FALSE, NULL, &server_callbacks, &fuzz_ctx);
+ smtp_server_connection_start(conn);
+
+ io_loop_run(fuzz_ctx.ioloop);
+
+ smtp_server_deinit(&smtp_server);
+ timeout_remove(&to);
+}
+FUZZ_END
diff --git a/src/lib-smtp/smtp-address.c b/src/lib-smtp/smtp-address.c
new file mode 100644
index 0000000..eac25e1
--- /dev/null
+++ b/src/lib-smtp/smtp-address.c
@@ -0,0 +1,958 @@
+/* Copyright (c) 2013-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "str.h"
+#include "message-address.h"
+#include "smtp-parser.h"
+#include "smtp-address.h"
+
+/* From RFC 5321:
+
+ Reverse-path = Path / "<>"
+ Forward-path = Path
+
+ Path = "<" [ A-d-l ":" ] Mailbox ">"
+ A-d-l = At-domain *( "," At-domain )
+ ; Note that this form, the so-called "source
+ ; route", MUST BE accepted, SHOULD NOT be
+ ; generated, and SHOULD be ignored.
+ At-domain = "@" Domain
+
+ Domain = sub-domain *("." sub-domain)
+
+ sub-domain = Let-dig [Ldh-str]
+ Let-dig = ALPHA / DIGIT
+ Ldh-str = *( ALPHA / DIGIT / "-" ) Let-dig
+
+ address-literal = "[" ( IPv4-address-literal /
+ IPv6-address-literal /
+ General-address-literal ) "]"
+ ; See Section 4.1.3
+
+ Mailbox = Local-part "@" ( Domain / address-literal )
+
+ Local-part = Dot-string / Quoted-string
+ ; MAY be case-sensitive
+ Dot-string = Atom *("." Atom)
+ Atom = 1*atext
+ */
+
+/*
+ * SMTP address parsing
+ */
+
+struct smtp_address_parser {
+ struct smtp_parser parser;
+
+ struct smtp_address address;
+ const unsigned char *address_end;
+
+ bool parse:1;
+ bool path:1;
+ bool parsed_any:1;
+ bool totally_broken:1;
+};
+
+static int
+smtp_parser_parse_dot_string(struct smtp_parser *parser, const char **value_r)
+{
+ const unsigned char *pbegin = parser->cur;
+
+ /* Dot-string = Atom *("." Atom)
+ */
+
+ /* NOTE: this deviates from Dot-String syntax to allow some Japanese
+ mail addresses with dots at non-standard places to be accepted. */
+
+ if (parser->cur >= parser->end ||
+ (!smtp_char_is_atext(*parser->cur) && *parser->cur != '.'))
+ return 0;
+ parser->cur++;
+
+ while (parser->cur < parser->end &&
+ (smtp_char_is_atext(*parser->cur) || *parser->cur == '.'))
+ parser->cur++;
+
+ if (value_r != NULL)
+ *value_r = t_strndup(pbegin, parser->cur - pbegin);
+ return 1;
+}
+
+static int
+smtp_parse_localpart(struct smtp_parser *parser, const char **localpart_r)
+{
+ int ret;
+
+ if ((ret = smtp_parser_parse_quoted_string(parser, localpart_r)) != 0)
+ return ret;
+
+ return smtp_parser_parse_dot_string(parser, localpart_r);
+}
+
+static int
+smtp_address_parser_find_end(struct smtp_address_parser *aparser,
+ enum smtp_address_parse_flags flags)
+{
+ struct smtp_parser *parser = &aparser->parser;
+ const char *begin = (const char *)parser->begin, *end;
+ const char **address_p = NULL;
+
+ if (aparser->address_end != NULL)
+ return 0;
+
+ if (aparser->parse &&
+ HAS_ALL_BITS(flags, SMTP_ADDRESS_PARSE_FLAG_PRESERVE_RAW))
+ address_p = &aparser->address.raw;
+ if (smtp_address_parse_any(begin, address_p, &end) < 0) {
+ parser->error = "Invalid character";
+ aparser->totally_broken = TRUE;
+ return -1;
+ }
+ aparser->parsed_any = TRUE;
+ aparser->address_end = (const unsigned char *)end;
+ if (aparser->path) {
+ i_assert(aparser->address_end > parser->begin);
+ aparser->address_end--;
+ }
+ return 0;
+}
+
+static int
+smtp_parse_mailbox(struct smtp_address_parser *aparser,
+ enum smtp_address_parse_flags flags)
+{
+ struct smtp_parser *parser = &aparser->parser;
+ const char **value = NULL;
+ const unsigned char *p, *dp;
+ int ret;
+
+ /* Mailbox = Local-part "@" ( Domain / address-literal )
+ */
+
+ value = (aparser->parse ? &aparser->address.localpart : NULL);
+ if ((flags & SMTP_ADDRESS_PARSE_FLAG_STRICT) != 0 ||
+ (flags & SMTP_ADDRESS_PARSE_FLAG_ALLOW_BAD_LOCALPART) == 0 ||
+ aparser->path || *parser->cur == '\"') {
+ if ((ret = smtp_parse_localpart(parser, value)) <= 0)
+ return ret;
+ } else {
+ /* find the end of the address */
+ if (smtp_address_parser_find_end(aparser, flags) < 0)
+ return -1;
+ /* use the right-most '@' as separator */
+ dp = aparser->address_end - 1;
+ while (dp > parser->cur && *dp != '@')
+ dp--;
+ if (dp == parser->cur)
+ dp = aparser->address_end;
+ /* check whether the resulting localpart could be encoded as
+ quoted string */
+ for (p = parser->cur; p < dp; p++) {
+ if (!smtp_char_is_qtext(*p) &&
+ !smtp_char_is_qpair(*p)) {
+ parser->error =
+ "Invalid character in localpart";
+ return -1;
+ }
+ }
+ if (aparser->parse) {
+ aparser->address.localpart =
+ p_strdup_until(parser->pool, parser->cur, dp);
+ }
+ parser->cur = dp;
+ }
+
+ if ((parser->cur >= parser->end || *parser->cur != '@') &&
+ (flags & SMTP_ADDRESS_PARSE_FLAG_ALLOW_LOCALPART) == 0) {
+ if (parser->cur >= parser->end ||
+ (aparser->path && *parser->cur == '>'))
+ parser->error = "Missing domain";
+ else
+ parser->error = "Invalid character in localpart";
+ return -1;
+ }
+
+ if (parser->cur >= parser->end || *parser->cur != '@')
+ return 1;
+ parser->cur++;
+
+ value = (aparser->parse ? &aparser->address.domain : NULL);
+ if ((ret = smtp_parser_parse_domain(parser, value)) == 0 &&
+ (ret = smtp_parser_parse_address_literal(
+ parser, value, NULL)) == 0) {
+ if (parser->cur >= parser->end ||
+ (aparser->path && *parser->cur == '>')) {
+ parser->error = "Missing domain after '@'";
+ return -1;
+ } else {
+ parser->error = "Invalid domain";
+ return -1;
+ }
+ }
+ return ret;
+}
+
+static int smtp_parse_source_route(struct smtp_parser *parser)
+{
+ /* Source-route = [ A-d-l ":" ]
+ A-d-l = At-domain *( "," At-domain )
+ At-domain = "@" Domain
+ */
+
+ /* "@" Domain */
+ if (parser->cur >= parser->end || *parser->cur != '@')
+ return 0;
+ parser->cur++;
+
+ for (;;) {
+ /* Domain */
+ if (smtp_parser_parse_domain(parser, NULL) <= 0) {
+ parser->error =
+ "Missing domain after '@' in source route";
+ return -1;
+ }
+
+ /* *( "," At-domain ) */
+ if (parser->cur >= parser->end || *parser->cur != ',')
+ break;
+ parser->cur++;
+
+ /* "@" Domain */
+ if (parser->cur >= parser->end || *parser->cur != '@') {
+ parser->error = "Missing '@' after ',' in source route";
+ return -1;
+ }
+ parser->cur++;
+ }
+
+ /* ":" */
+ if (parser->cur >= parser->end || *parser->cur != ':') {
+ parser->error = "Missing ':' at end of source route";
+ return -1;
+ }
+ parser->cur++;
+ return 1;
+}
+
+static int
+smtp_parse_path(struct smtp_address_parser *aparser,
+ enum smtp_address_parse_flags flags)
+{
+ struct smtp_parser *parser = &aparser->parser;
+ int ret, sret = 0;
+
+ /* Path = "<" [ A-d-l ":" ] Mailbox ">"
+ */
+
+ /* "<" */
+ if (parser->cur < parser->end && *parser->cur == '<') {
+ aparser->path = TRUE;
+ parser->cur++;
+ } else if ((flags & SMTP_ADDRESS_PARSE_FLAG_BRACKETS_OPTIONAL) == 0) {
+ return 0;
+ }
+
+ /* [ A-d-l ":" ] */
+ if (aparser->path && (sret = smtp_parse_source_route(parser)) < 0)
+ return -1;
+
+ /* Mailbox */
+ if ((ret = smtp_parse_mailbox(aparser, flags)) < 0)
+ return -1;
+ if (ret == 0) {
+ if (parser->cur < parser->end && *parser->cur == '>') {
+ if (sret > 0) {
+ parser->error =
+ "Path only consists of source route";
+ return -1;
+ }
+ if ((flags & SMTP_ADDRESS_PARSE_FLAG_ALLOW_EMPTY)
+ == 0) {
+ parser->error = "Null path not allowed";
+ return -1;
+ }
+ } else {
+ parser->error = "Invalid character in localpart";
+ return -1;
+ }
+ }
+
+ /* ">" */
+ if (aparser->path) {
+ if (parser->cur >= parser->end || *parser->cur != '>') {
+ parser->error = "Missing '>' at end of path";
+ return -1;
+ }
+ parser->cur++;
+ } else if (parser->cur < parser->end && *parser->cur == '>') {
+ parser->error = "Unmatched '>' at end of path";
+ return -1;
+ }
+ return 1;
+}
+
+int smtp_address_parse_mailbox(pool_t pool, const char *mailbox,
+ enum smtp_address_parse_flags flags,
+ struct smtp_address **address_r,
+ const char **error_r)
+{
+ struct smtp_address_parser aparser;
+ int ret;
+
+ if (address_r != NULL)
+ *address_r = NULL;
+ if (error_r != NULL)
+ *error_r = NULL;
+
+ if (error_r != NULL)
+ *error_r = NULL;
+
+ if ((mailbox == NULL || *mailbox == '\0')) {
+ if ((flags & SMTP_ADDRESS_PARSE_FLAG_ALLOW_EMPTY) == 0) {
+ if (error_r != NULL)
+ *error_r = "Mailbox is empty string";
+ return -1;
+ }
+
+ if (address_r != NULL)
+ *address_r = p_new(pool, struct smtp_address, 1);
+ return 0;
+ }
+
+ i_zero(&aparser);
+ smtp_parser_init(&aparser.parser, pool_datastack_create(), mailbox);
+ aparser.address_end = aparser.parser.end;
+ aparser.parse = (address_r != NULL);
+
+ if ((ret = smtp_parse_mailbox(&aparser, flags)) <= 0) {
+ if (error_r != NULL) {
+ *error_r = (ret < 0 ? aparser.parser.error :
+ "Invalid character in localpart");
+ }
+ return -1;
+ }
+ if (aparser.parser.cur != aparser.parser.end) {
+ if (error_r != NULL)
+ *error_r = "Invalid character in mailbox";
+ return -1;
+ }
+
+ if (address_r != NULL)
+ *address_r = smtp_address_clone(pool, &aparser.address);
+ return 0;
+}
+
+static int
+smtp_address_parse_path_broken(struct smtp_address_parser *aparser,
+ enum smtp_address_parse_flags flags,
+ const char **endp_r) ATTR_NULL(3)
+{
+ struct smtp_parser *parser = &aparser->parser;
+ const char *begin = (const char *)parser->begin, *end;
+ const char *raw = aparser->address.raw;
+ const char **address_p = NULL;
+
+ i_zero(&aparser->address);
+ aparser->address.raw = raw;
+
+ if (aparser->totally_broken ||
+ HAS_NO_BITS(flags, SMTP_ADDRESS_PARSE_FLAG_IGNORE_BROKEN))
+ return -1;
+ if (*begin != '<' &&
+ HAS_NO_BITS(flags, SMTP_ADDRESS_PARSE_FLAG_BRACKETS_OPTIONAL)) {
+ /* brackets missing; totally broken */
+ return -1;
+ }
+ i_assert(aparser->parse);
+ if (aparser->parsed_any) {
+ if (endp_r != NULL)
+ *endp_r = (const char *)aparser->address_end;
+ return 0;
+ }
+
+ if (HAS_ALL_BITS(flags, SMTP_ADDRESS_PARSE_FLAG_PRESERVE_RAW))
+ address_p = &aparser->address.raw;
+ if (smtp_address_parse_any(begin, address_p, &end) < 0) {
+ /* totally broken */
+ return -1;
+ }
+ if (endp_r != NULL)
+ *endp_r = end;
+ return 0;
+}
+
+int smtp_address_parse_path_full(pool_t pool, const char *path,
+ enum smtp_address_parse_flags flags,
+ struct smtp_address **address_r,
+ const char **error_r, const char **endp_r)
+{
+ struct smtp_address_parser aparser;
+ int ret;
+
+ if (address_r != NULL)
+ *address_r = NULL;
+ if (error_r != NULL)
+ *error_r = NULL;
+ if (endp_r != NULL)
+ *endp_r = NULL;
+
+ if (path == NULL || *path == '\0') {
+ if ((flags & SMTP_ADDRESS_PARSE_FLAG_ALLOW_EMPTY) == 0 ||
+ (flags & SMTP_ADDRESS_PARSE_FLAG_BRACKETS_OPTIONAL) == 0) {
+ if (error_r != NULL)
+ *error_r = "Path is empty string";
+ return -1;
+ }
+ if (address_r != NULL)
+ *address_r = p_new(pool, struct smtp_address, 1);
+ if (endp_r != NULL)
+ *endp_r = path;
+ return 0;
+ }
+
+ i_zero(&aparser);
+ smtp_parser_init(&aparser.parser, pool_datastack_create(), path);
+ aparser.address_end = (endp_r != NULL ? NULL : aparser.parser.end);
+ aparser.parse = (address_r != NULL);
+
+ if ((ret = smtp_parse_path(&aparser, flags)) <= 0) {
+ if (error_r != NULL) {
+ *error_r = (ret < 0 ? aparser.parser.error :
+ "Missing '<' at beginning of path");
+ }
+ ret = -1;
+ } else if (endp_r != NULL) {
+ if (aparser.parser.cur == aparser.parser.end ||
+ *aparser.parser.cur == ' ' ||
+ HAS_NO_BITS(flags, SMTP_ADDRESS_PARSE_FLAG_IGNORE_BROKEN)) {
+ *endp_r = (const char *)aparser.parser.cur;
+ ret = 0;
+ } else {
+ if (error_r != NULL)
+ *error_r = "Invalid character in path";
+ ret = -1;
+ }
+ } else if (aparser.parser.cur == aparser.parser.end) {
+ ret = 0;
+ } else {
+ if (error_r != NULL)
+ *error_r = "Invalid character in path";
+ ret = -1;
+ }
+
+ if (ret < 0) {
+ /* normal parsing failed */
+ if (smtp_address_parse_path_broken(&aparser, flags,
+ endp_r) < 0) {
+ /* failed to parse it as a broken address as well */
+ return -1;
+ }
+ /* broken address */
+ } else if (HAS_ALL_BITS(flags, SMTP_ADDRESS_PARSE_FLAG_PRESERVE_RAW) &&
+ aparser.address.localpart != NULL) {
+ if (aparser.path &&
+ ((const unsigned char *)(path + 1) < aparser.parser.cur)) {
+ aparser.address.raw = t_strdup_until(
+ path + 1, aparser.parser.cur - 1);
+ } else {
+ aparser.address.raw = t_strdup_until(
+ path, aparser.parser.cur);
+ }
+ }
+
+ if (address_r != NULL)
+ *address_r = smtp_address_clone(pool, &aparser.address);
+ return ret;
+}
+
+int smtp_address_parse_path(pool_t pool, const char *path,
+ enum smtp_address_parse_flags flags,
+ struct smtp_address **address_r,
+ const char **error_r)
+{
+ return smtp_address_parse_path_full(pool, path, flags,
+ address_r, error_r, NULL);
+}
+
+int smtp_address_parse_username(pool_t pool, const char *username,
+ struct smtp_address **address_r,
+ const char **error_r)
+{
+ enum smtp_address_parse_flags flags =
+ SMTP_ADDRESS_PARSE_FLAG_ALLOW_LOCALPART |
+ SMTP_ADDRESS_PARSE_FLAG_ALLOW_BAD_LOCALPART;
+
+ struct smtp_address_parser aparser;
+ int ret;
+
+ if (address_r != NULL)
+ *address_r = NULL;
+ if (error_r != NULL)
+ *error_r = NULL;
+
+ if ((username == NULL || *username == '\0')) {
+ if (error_r != NULL)
+ *error_r = "Username is empty string";
+ return -1;
+ }
+
+ i_zero(&aparser);
+ smtp_parser_init(&aparser.parser, pool_datastack_create(), username);
+ aparser.address_end = aparser.parser.end;
+ aparser.parse = (address_r != NULL);
+
+ if ((ret = smtp_parse_mailbox(&aparser, flags)) <= 0) {
+ if (error_r != NULL) {
+ *error_r = (ret < 0 ? aparser.parser.error :
+ "Invalid character in user name");
+ }
+ return -1;
+ }
+ if (aparser.parser.cur != aparser.parser.end) {
+ if (error_r != NULL)
+ *error_r = "Invalid character in user name";
+ return -1;
+ }
+
+ if (address_r != NULL)
+ *address_r = smtp_address_clone(pool, &aparser.address);
+ return 0;
+}
+
+void smtp_address_detail_parse(pool_t pool, const char *delimiters,
+ struct smtp_address *address,
+ const char **username_r, char *delim_r,
+ const char **detail_r)
+{
+ const char *localpart;
+ const char *user, *p;
+ size_t idx;
+
+ i_assert(!smtp_address_isnull(address));
+
+ localpart = address->localpart;
+ user = localpart;
+ *detail_r = "";
+ *delim_r = '\0';
+
+ /* first character that matches the recipient_delimiter */
+ idx = strcspn(localpart, delimiters);
+ p = (localpart[idx] != '\0' ? &localpart[idx] : NULL);
+
+ if (p != NULL) {
+ *delim_r = *p;
+ /* user+detail */
+ user = p_strdup_until(pool, localpart, p);
+ *detail_r = p+1;
+ }
+
+ if (address->domain == NULL || *address->domain == '\0')
+ *username_r = user;
+ else if (strchr(user, '@') == NULL) {
+ /* username is just glued to the domain... no SMTP escaping */
+ *username_r = p_strconcat(pool, user, "@", address->domain,
+ NULL);
+ } else {
+ struct smtp_address uaddr;
+
+ /* username contains '@'; apply escaping */
+ smtp_address_init(&uaddr, user, address->domain);
+ if (pool->datastack_pool)
+ *username_r = smtp_address_encode(&uaddr);
+ else {
+ *username_r =
+ p_strdup(pool, smtp_address_encode(&uaddr));
+ }
+ }
+}
+
+void smtp_address_detail_parse_temp(const char *delimiters,
+ struct smtp_address *address,
+ const char **username_r, char *delim_r,
+ const char **detail_r)
+{
+ smtp_address_detail_parse(pool_datastack_create(), delimiters,
+ address, username_r, delim_r, detail_r);
+}
+
+int smtp_address_parse_any(const char *in, const char **address_r,
+ const char **endp_r)
+{
+ const unsigned char *p, *pend, *poffset;
+ bool path = FALSE;
+ bool quoted = FALSE;
+
+ if (endp_r != NULL)
+ *endp_r = in;
+
+ poffset = p = (const unsigned char *)in;
+ pend = p + strlen(in);
+ if (*p == '<') {
+ path = TRUE;
+ p++;
+ poffset = p;
+ }
+ if (*p == '"') {
+ quoted = TRUE;
+ p++;
+ }
+
+ while (p < pend) {
+ if (quoted && *p == '\\') {
+ p++;
+ if (p == pend || *p < 0x20)
+ return -1;
+ p++;
+ if (p == pend)
+ break;
+ }
+ switch (*p) {
+ case '"':
+ quoted = FALSE;
+ break;
+ case ' ':
+ if (!quoted) {
+ if (path)
+ return -1;
+ if (address_r != NULL)
+ *address_r = t_strdup_until(poffset, p);
+ if (endp_r != NULL)
+ *endp_r = (const char *)p;
+ return 0;
+ }
+ break;
+ case '>':
+ if (!quoted) {
+ if (address_r != NULL)
+ *address_r = t_strdup_until(poffset, p);
+ if (endp_r != NULL)
+ *endp_r = (const char *)(p + 1);
+ return 0;
+ }
+ break;
+ default:
+ if (*p < 0x20)
+ return -1;
+ break;
+ }
+ p++;
+ }
+ if (quoted || path)
+ return -1;
+ if (address_r != NULL)
+ *address_r = t_strdup_until(poffset, p);
+ if (endp_r != NULL)
+ *endp_r = (const char *)p;
+ return 0;
+}
+
+/*
+ * SMTP address construction
+ */
+
+void smtp_address_write(string_t *out, const struct smtp_address *address)
+ ATTR_NULL(2)
+{
+ bool quoted = FALSE;
+ const unsigned char *p, *pend, *pblock;
+ size_t begin;
+
+ if (smtp_address_isnull(address))
+ return;
+ begin = str_len(out);
+
+ /* encode localpart */
+ p = (const unsigned char *)address->localpart;
+ pend = p + strlen(address->localpart);
+ pblock = p;
+ while (p < pend) {
+ while (p < pend && smtp_char_is_atext(*p))
+ p++;
+
+ if (!quoted && p < pend && (*p != '.' || p == pblock)) {
+ quoted = TRUE;
+ str_insert(out, begin, "\"");
+ }
+
+ str_append_data(out, pblock, p - pblock);
+ if (p >= pend)
+ break;
+
+ if (!quoted) {
+ str_append_c(out, '.');
+ } else {
+ i_assert(smtp_char_is_qpair(*p));
+ if (!smtp_char_is_qtext(*p))
+ str_append_c(out, '\\');
+ str_append_c(out, *p);
+ }
+
+ p++;
+ pblock = p;
+ }
+
+ if (p == pblock && !quoted) {
+ quoted = TRUE;
+ str_insert(out, begin, "\"");
+ }
+
+ if (quoted)
+ str_append_c(out, '\"');
+
+ if (address->domain == NULL || *address->domain == '\0')
+ return;
+
+ str_append_c(out, '@');
+ str_append(out, address->domain);
+}
+
+void smtp_address_write_path(string_t *out, const struct smtp_address *address)
+{
+ str_append_c(out, '<');
+ smtp_address_write(out, address);
+ str_append_c(out, '>');
+}
+
+const char *smtp_address_encode(const struct smtp_address *address)
+{
+ string_t *str = t_str_new(256);
+ smtp_address_write(str, address);
+ return str_c(str);
+}
+
+const char *smtp_address_encode_path(const struct smtp_address *address)
+{
+ string_t *str = t_str_new(256);
+ smtp_address_write_path(str, address);
+ return str_c(str);
+}
+
+const char *smtp_address_encode_raw(const struct smtp_address *address)
+{
+ if (address != NULL && address->raw != NULL && *address->raw != '\0')
+ return address->raw;
+
+ return smtp_address_encode(address);
+}
+
+const char *smtp_address_encode_raw_path(const struct smtp_address *address)
+{
+ if (address != NULL && address->raw != NULL && *address->raw != '\0')
+ return t_strconcat("<", address->raw, ">", NULL);
+
+ return smtp_address_encode_path(address);
+}
+
+/*
+ * SMTP address manipulation
+ */
+
+void smtp_address_init(struct smtp_address *address,
+ const char *localpart, const char *domain)
+{
+ i_zero(address);
+ if (localpart == NULL || *localpart == '\0')
+ return;
+
+ address->localpart = localpart;
+ if (domain != NULL && *domain != '\0')
+ address->domain = domain;
+}
+
+int smtp_address_init_from_msg(struct smtp_address *address,
+ const struct message_address *msg_addr)
+{
+ const unsigned char *p;
+
+ i_zero(address);
+ if (msg_addr->mailbox == NULL || *msg_addr->mailbox == '\0')
+ return 0;
+
+ /* The message_address_parse() function allows UTF-8 codepoints in
+ the localpart. For SMTP addresses that is not an option, so we
+ need to check this upon conversion. */
+ for (p = (const unsigned char *)msg_addr->mailbox; *p != '\0'; p++) {
+ if (!smtp_char_is_qpair(*p))
+ return -1;
+ }
+
+ address->localpart = msg_addr->mailbox;
+ if (msg_addr->domain != NULL && *msg_addr->domain != '\0')
+ address->domain = msg_addr->domain;
+ return 0;
+}
+
+struct smtp_address *
+smtp_address_clone(pool_t pool, const struct smtp_address *src)
+{
+ struct smtp_address *new;
+ size_t size, lpsize = 0, dsize = 0, rsize = 0;
+ char *data, *localpart = NULL, *domain = NULL, *raw = NULL;
+
+ if (src == NULL)
+ return NULL;
+
+ /* @UNSAFE */
+
+ size = sizeof(struct smtp_address);
+ if (!smtp_address_isnull(src)) {
+ lpsize = strlen(src->localpart) + 1;
+ size = MALLOC_ADD(size, lpsize);
+ }
+ if (src->domain != NULL && *src->domain != '\0') {
+ dsize = strlen(src->domain) + 1;
+ size = MALLOC_ADD(size, dsize);
+ }
+ if (src->raw != NULL && *src->raw != '\0') {
+ rsize = strlen(src->raw) + 1;
+ size = MALLOC_ADD(size, rsize);
+ }
+
+ data = p_malloc(pool, size);
+ new = (struct smtp_address *)data;
+ if (lpsize > 0) {
+ localpart = PTR_OFFSET(data, sizeof(*new));
+ memcpy(localpart, src->localpart, lpsize);
+ }
+ if (dsize > 0) {
+ domain = PTR_OFFSET(data, sizeof(*new) + lpsize);
+ memcpy(domain, src->domain, dsize);
+ }
+ if (rsize > 0) {
+ raw = PTR_OFFSET(data, sizeof(*new) + lpsize + dsize);
+ memcpy(raw, src->raw, rsize);
+ }
+ new->localpart = localpart;
+ new->domain = domain;
+ new->raw = raw;
+
+ return new;
+}
+
+struct smtp_address *
+smtp_address_create(pool_t pool, const char *localpart, const char *domain)
+{
+ struct smtp_address addr;
+
+ smtp_address_init(&addr, localpart, domain);
+ return smtp_address_clone(pool, &addr);
+}
+
+
+int smtp_address_create_from_msg(pool_t pool,
+ const struct message_address *msg_addr,
+ struct smtp_address **address_r)
+{
+ struct smtp_address addr;
+
+ if (smtp_address_init_from_msg(&addr, msg_addr) < 0) {
+ *address_r = NULL;
+ return -1;
+ }
+ *address_r = smtp_address_clone(pool, &addr);
+ return 0;
+}
+
+struct smtp_address *smtp_address_clone_temp(const struct smtp_address *src)
+{
+ struct smtp_address *new;
+
+ if (src == NULL)
+ return NULL;
+
+ new = t_new(struct smtp_address, 1);
+ new->localpart = t_strdup_empty(src->localpart);
+ new->domain = t_strdup_empty(src->domain);
+ new->raw = t_strdup_empty(src->raw);
+ return new;
+}
+
+struct smtp_address *
+smtp_address_create_temp(const char *localpart, const char *domain)
+{
+ struct smtp_address addr;
+
+ smtp_address_init(&addr, localpart, domain);
+ return smtp_address_clone_temp(&addr);
+}
+
+int smtp_address_create_from_msg_temp(const struct message_address *msg_addr,
+ struct smtp_address **address_r)
+{
+ struct smtp_address addr;
+
+ if (smtp_address_init_from_msg(&addr, msg_addr) < 0) {
+ *address_r = NULL;
+ return -1;
+ }
+ *address_r = smtp_address_clone_temp(&addr);
+ return 0;
+}
+
+struct smtp_address *
+smtp_address_add_detail(pool_t pool, const struct smtp_address *address,
+ const char *detail, char delim_c)
+{
+ struct smtp_address *new_addr;
+ const char delim[] = {delim_c, '\0'};
+
+ i_assert(!smtp_address_isnull(address));
+
+ new_addr = p_new(pool, struct smtp_address, 1);
+ new_addr->localpart = p_strconcat(pool, address->localpart, delim,
+ detail, NULL);
+ new_addr->domain = p_strdup_empty(pool, address->domain);
+
+ return new_addr;
+}
+
+struct smtp_address *
+smtp_address_add_detail_temp(const struct smtp_address *address,
+ const char *detail, char delim_c)
+{
+ struct smtp_address *new_addr;
+ const char delim[] = {delim_c, '\0'};
+
+ i_assert(!smtp_address_isnull(address));
+
+ new_addr = t_new(struct smtp_address, 1);
+ new_addr->localpart = t_strconcat(address->localpart, delim, detail,
+ NULL);
+ new_addr->domain = t_strdup_empty(address->domain);
+
+ return new_addr;
+}
+
+int smtp_address_cmp(const struct smtp_address *address1,
+ const struct smtp_address *address2)
+{
+ bool null1, null2;
+ int ret;
+
+ null1 = smtp_address_isnull(address1);
+ null2 = smtp_address_isnull(address2);
+ if (null1)
+ return (null2 ? 0 : -1);
+ else if (null2)
+ return 1;
+ if ((ret = null_strcasecmp(address1->domain, address2->domain)) != 0)
+ return ret;
+ return null_strcmp(address1->localpart, address2->localpart);
+}
+
+int smtp_address_cmp_icase(const struct smtp_address *address1,
+ const struct smtp_address *address2)
+{
+ bool null1, null2;
+ int ret;
+
+ null1 = smtp_address_isnull(address1);
+ null2 = smtp_address_isnull(address2);
+ if (null1)
+ return (null2 ? 0 : -1);
+ else if (null2)
+ return 1;
+ if ((ret = null_strcasecmp(address1->domain, address2->domain)) != 0)
+ return ret;
+ return null_strcasecmp(address1->localpart, address2->localpart);
+}
diff --git a/src/lib-smtp/smtp-address.h b/src/lib-smtp/smtp-address.h
new file mode 100644
index 0000000..326a673
--- /dev/null
+++ b/src/lib-smtp/smtp-address.h
@@ -0,0 +1,214 @@
+#ifndef SMTP_ADDRESS_H
+#define SMTP_ADDRESS_H
+
+#include "array-decl.h"
+
+struct message_address;
+
+enum smtp_address_parse_flags {
+ /* Strictly enforce the RFC 5321 syntax */
+ SMTP_ADDRESS_PARSE_FLAG_STRICT = BIT(0),
+ /* Allow an empty/NULL address */
+ SMTP_ADDRESS_PARSE_FLAG_ALLOW_EMPTY = BIT(1),
+ /* Allow an address without a domain part */
+ SMTP_ADDRESS_PARSE_FLAG_ALLOW_LOCALPART = BIT(2),
+ /* Allow omission of the <...> brackets in a path. This flag is only
+ relevant for smtp_address_parse_path(). */
+ SMTP_ADDRESS_PARSE_FLAG_BRACKETS_OPTIONAL = BIT(3),
+ /* Allow localpart to have all kinds of bad unquoted characters by
+ parsing the last '@' in the string directly as the localpart/domain
+ separator. Addresses starting with `<' or `"' are parsed as normal.
+ The address is rejected when the resulting localpart and domain
+ cannot be used to construct a valid RFC 5321 address.
+ */
+ SMTP_ADDRESS_PARSE_FLAG_ALLOW_BAD_LOCALPART = BIT(4),
+ /* Store an unparsed copy of the address in the `raw' field of struct
+ smtp_address. When combined with SMTP_ADDRESS_PARSE_FLAG_SKIP_BROKEN,
+ the broken address will be stored there. This flag is only relevant
+ for smtp_address_parse_path(). */
+ SMTP_ADDRESS_PARSE_FLAG_PRESERVE_RAW = BIT(5),
+ /* Try to skip over a broken address to allow working around syntax
+ errors in e.g. the sender address for the MAIL command. This flag is
+ only relevant for smtp_address_parse_path*(). The parser will return
+ failure, but it will return a broken address which is be equivalent
+ to <>. The raw broken address string is available in the address->raw
+ field. When the broken address contains control characters or is
+ badly delimited, parsing will still fail completely. */
+ SMTP_ADDRESS_PARSE_FLAG_IGNORE_BROKEN = BIT(6),
+};
+
+struct smtp_address {
+ /* Localpart */
+ const char *localpart;
+ /* Domain */
+ const char *domain;
+ /* Raw, unparsed address. If localpart == NULL, the value of this field
+ is syntactically invalid and MUST NOT be used for any purposes that
+ may be visible to external systems. It can be e.g. used for logging.
+ This is always in mailbox format, meaning that there are no
+ surrounding '<' and '>'.
+ */
+ const char *raw;
+};
+
+ARRAY_DEFINE_TYPE(smtp_address, struct smtp_address *);
+ARRAY_DEFINE_TYPE(smtp_address_const, const struct smtp_address *);
+
+/*
+ * SMTP address parsing
+ */
+
+
+/* Parse the RFC 5321 address from the provided mailbox string. Returns 0 when
+ the address was parsed successfully and -1 upon error. The address is
+ returned in address_r. When address_r is NULL, the provided string will be
+ verified for validity as a mailbox only. */
+int smtp_address_parse_mailbox(pool_t pool, const char *mailbox,
+ enum smtp_address_parse_flags flags,
+ struct smtp_address **address_r,
+ const char **error_r) ATTR_NULL(4, 5);
+/* Parse the RFC 5321 address from the provided path string. Returns 0 when
+ the address was parsed successfully and -1 upon error. The address is
+ returned in address_r. When address_r is NULL, the provided string will be
+ verified for validity as a path only. The endp_r parameter is used to
+ return a pointer to the end of the path string, so that the caller can
+ continue parsing from there. When the SMTP_ADDRESS_PARSE_FLAG_IGNORE_BROKEN
+ flag is set, a broken address will be returned, even when the return value
+ is -1 (see above). If it is totally broken, *endp_r will be then be NULL.
+ */
+int smtp_address_parse_path_full(pool_t pool, const char *path,
+ enum smtp_address_parse_flags flags,
+ struct smtp_address **address_r,
+ const char **error_r, const char **endp_r)
+ ATTR_NULL(4, 5, 6);
+/* Parse the RFC 5321 address from the provided path string. Returns 0 when
+ the address was parsed successfully and -1 upon error. The address is
+ returned in address_r. When address_r is NULL, the provided string will be
+ verified for validity as a path only. When the
+ SMTP_ADDRESS_PARSE_FLAG_IGNORE_BROKEN flag is set, a broken address will be
+ returned, even when the return value is -1 (see above). */
+int smtp_address_parse_path(pool_t pool, const char *path,
+ enum smtp_address_parse_flags flags,
+ struct smtp_address **address_r,
+ const char **error_r) ATTR_NULL(4, 5);
+/* Parse the RFC 5321 address from the provided username string. A username
+ string is not strictly parsed as an RFC 5321 mailbox; it allows a more
+ lenient syntax. If the address obtained from splitting the string at the last
+ `@' can be encoded back into a valid RFC 5321 mailbox string, parsing the
+ username will succeeded. Returns 0 when the address was parsed successfully
+ and -1 upon error. The address is returned in address_r. When address_r is
+ NULL, the provided string will be verified for validity as a username only.
+ */
+int smtp_address_parse_username(pool_t pool, const char *username,
+ struct smtp_address **address_r,
+ const char **error_r) ATTR_NULL(3, 4);
+
+/* Parse address+detail@domain into address@domain and detail
+ using given delimiters. Returns used delimiter. */
+void smtp_address_detail_parse(pool_t pool, const char *delimiters,
+ struct smtp_address *address,
+ const char **username_r, char *delim_r,
+ const char **detail_r);
+void smtp_address_detail_parse_temp(const char *delimiters,
+ struct smtp_address *address,
+ const char **username_r, char *delim_r,
+ const char **detail_r);
+
+/* Parse any (possibly broken) address on the input to the best of our ability
+ until end of input or unquoted ` '. Things that are truly evil (unending
+ quoted string, control characters and a path without a closing '>') will
+ still fail and return -1. If the parse was successful, it will return 0.
+ The parsed address string is returned in address_r. Any outer < and > are
+ omitted in the parsed address. The endp_r parameter is used to return a
+ pointer to the end of the path string, so that the caller can continue
+ parsing from there.*/
+int smtp_address_parse_any(const char *in, const char **address_r,
+ const char **endp_r) ATTR_NULL(2, 3);
+
+/*
+ * SMTP address construction
+ */
+
+void smtp_address_write(string_t *out, const struct smtp_address *address)
+ ATTR_NULL(2);
+void smtp_address_write_path(string_t *out, const struct smtp_address *address)
+ ATTR_NULL(2);
+
+const char *smtp_address_encode(const struct smtp_address *address)
+ ATTR_NULL(1);
+const char *smtp_address_encode_path(const struct smtp_address *address)
+ ATTR_NULL(1);
+
+const char *
+smtp_address_encode_raw(const struct smtp_address *address) ATTR_NULL(1);
+const char *
+smtp_address_encode_raw_path(const struct smtp_address *address) ATTR_NULL(1);
+
+/*
+ * SMTP address manipulation
+ */
+
+void smtp_address_init(struct smtp_address *address,
+ const char *localpart, const char *domain)
+ ATTR_NULL(2,3);
+int smtp_address_init_from_msg(struct smtp_address *address,
+ const struct message_address *msg_addr);
+
+struct smtp_address *
+smtp_address_clone(pool_t pool, const struct smtp_address *address)
+ ATTR_NULL(2);
+struct smtp_address *
+smtp_address_create(pool_t pool, const char *localpart, const char *domain)
+ ATTR_NULL(2, 3);
+int smtp_address_create_from_msg(pool_t pool,
+ const struct message_address *msg_addr,
+ struct smtp_address **address_r);
+
+struct smtp_address *
+smtp_address_clone_temp(const struct smtp_address *address) ATTR_NULL(1);
+struct smtp_address *
+smtp_address_create_temp(const char *localpart, const char *domain)
+ ATTR_NULL(2, 3);
+int smtp_address_create_from_msg_temp(const struct message_address *msg_addr,
+ struct smtp_address **address_r);
+
+struct smtp_address *
+smtp_address_add_detail(pool_t pool, const struct smtp_address *address,
+ const char *detail, char delim_c);
+struct smtp_address *
+smtp_address_add_detail_temp(const struct smtp_address *address,
+ const char *detail, char delim_c);
+
+int smtp_address_cmp(const struct smtp_address *address1,
+ const struct smtp_address *address2) ATTR_NULL(1, 2);
+int smtp_address_cmp_icase(const struct smtp_address *address1,
+ const struct smtp_address *address2) ATTR_NULL(1, 2);
+
+static inline bool ATTR_NULL(1, 2)
+smtp_address_equals(const struct smtp_address *address1,
+ const struct smtp_address *address2)
+{
+ return (smtp_address_cmp(address1, address2) == 0);
+}
+static inline bool ATTR_NULL(1, 2)
+smtp_address_equals_icase(const struct smtp_address *address1,
+ const struct smtp_address *address2)
+{
+ return (smtp_address_cmp_icase(address1, address2) == 0);
+}
+
+static inline bool ATTR_NULL(1) ATTR_PURE
+smtp_address_isnull(const struct smtp_address *address)
+{
+ return (address == NULL || address->localpart == NULL);
+}
+
+static inline bool ATTR_NULL(1) ATTR_PURE
+smtp_address_is_broken(const struct smtp_address *address)
+{
+ return (address != NULL &&
+ smtp_address_isnull(address) &&
+ (address->raw != NULL && *address->raw != '\0'));
+}
+
+#endif
diff --git a/src/lib-smtp/smtp-client-command.c b/src/lib-smtp/smtp-client-command.c
new file mode 100644
index 0000000..9eaf76d
--- /dev/null
+++ b/src/lib-smtp/smtp-client-command.c
@@ -0,0 +1,1580 @@
+/* Copyright (c) 2013-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "buffer.h"
+#include "array.h"
+#include "str.h"
+#include "str-sanitize.h"
+#include "llist.h"
+#include "istream.h"
+#include "ostream.h"
+#include "ostream-dot.h"
+#include "smtp-common.h"
+#include "smtp-syntax.h"
+#include "smtp-params.h"
+#include "smtp-client-private.h"
+
+static const char *
+smtp_client_command_get_name(struct smtp_client_command *cmd)
+{
+ const unsigned char *p, *pend;
+
+ if (cmd->name != NULL)
+ return cmd->name;
+
+ if (cmd->plug)
+ return NULL;
+ if (cmd->data == NULL || cmd->data->used == 0)
+ return NULL;
+
+ p = cmd->data->data;
+ pend = p + cmd->data->used;
+ for (;p < pend; p++) {
+ if (*p == ' ' || *p == '\r' || *p == '\n')
+ break;
+ }
+ cmd->name = p_strdup(cmd->pool,
+ t_str_ucase(t_strdup_until(cmd->data->data, p)));
+ return cmd->name;
+}
+
+static const char *
+smtp_client_command_get_label(struct smtp_client_command *cmd)
+{
+ if (cmd->plug)
+ return "[plug]";
+ if (cmd->data == NULL || cmd->data->used == 0) {
+ if (!cmd->has_stream)
+ return "[empty]";
+ return "[data]";
+ }
+ return smtp_client_command_get_name(cmd);
+}
+
+static void smtp_client_command_update_event(struct smtp_client_command *cmd)
+{
+ const char *label = smtp_client_command_get_label(cmd);
+
+ event_add_str(cmd->event, "cmd_name",
+ smtp_client_command_get_name(cmd));
+ event_set_append_log_prefix(
+ cmd->event, t_strdup_printf("command %s: ",
+ str_sanitize(label, 128)));
+}
+
+static struct smtp_client_command *
+smtp_client_command_create(struct smtp_client_connection *conn,
+ enum smtp_client_command_flags flags,
+ smtp_client_command_callback_t *callback,
+ void *context)
+{
+ struct smtp_client_command *cmd;
+ pool_t pool;
+
+ pool = pool_alloconly_create("smtp client command", 2048);
+ cmd = p_new(pool, struct smtp_client_command, 1);
+ cmd->pool = pool;
+ cmd->refcount = 1;
+ cmd->conn = conn;
+ cmd->flags = flags;
+ cmd->replies_expected = 1;
+ cmd->callback = callback;
+ cmd->context = context;
+ cmd->event = event_create(conn->event);
+ smtp_client_command_update_event(cmd);
+ return cmd;
+}
+
+#undef smtp_client_command_new
+struct smtp_client_command *
+smtp_client_command_new(struct smtp_client_connection *conn,
+ enum smtp_client_command_flags flags,
+ smtp_client_command_callback_t *callback,
+ void *context)
+{
+ i_assert(callback != NULL);
+ return smtp_client_command_create(conn, flags, callback, context);
+}
+
+struct smtp_client_command *
+smtp_client_command_plug(struct smtp_client_connection *conn,
+ struct smtp_client_command *after)
+{
+ struct smtp_client_command *cmd;
+
+ cmd = smtp_client_command_create(conn, 0, NULL, NULL);
+ cmd->plug = TRUE;
+ smtp_client_command_submit_after(cmd, after);
+ return cmd;
+}
+
+void smtp_client_command_ref(struct smtp_client_command *cmd)
+{
+ cmd->refcount++;
+}
+
+bool smtp_client_command_unref(struct smtp_client_command **_cmd)
+{
+ struct smtp_client_command *cmd = *_cmd;
+
+ *_cmd = NULL;
+
+ if (cmd == NULL)
+ return FALSE;
+
+ struct smtp_client_connection *conn = cmd->conn;
+
+ i_assert(cmd->refcount > 0);
+ if (--cmd->refcount > 0)
+ return TRUE;
+
+ e_debug(cmd->event, "Destroy (%u commands pending, %u commands queued)",
+ conn->cmd_wait_list_count, conn->cmd_send_queue_count);
+
+ i_assert(cmd->state >= SMTP_CLIENT_COMMAND_STATE_FINISHED);
+ i_assert(cmd != conn->cmd_streaming);
+
+ i_stream_unref(&cmd->stream);
+ event_unref(&cmd->event);
+ pool_unref(&cmd->pool);
+
+ return FALSE;
+}
+
+bool smtp_client_command_name_equals(struct smtp_client_command *cmd,
+ const char *name)
+{
+ const unsigned char *data;
+ size_t name_len, data_len;
+
+ if (cmd->data == NULL)
+ return FALSE;
+
+ name_len = strlen(name);
+ data = cmd->data->data;
+ data_len = cmd->data->used;
+
+ if (data_len < name_len || i_memcasecmp(data, name, name_len) != 0)
+ return FALSE;
+ return (data_len == name_len ||
+ data[name_len] == ' ' || data[name_len] == '\r');
+}
+
+void smtp_client_command_lock(struct smtp_client_command *cmd)
+{
+ if (cmd->plug)
+ return;
+ cmd->locked = TRUE;
+}
+
+void smtp_client_command_unlock(struct smtp_client_command *cmd)
+{
+ if (cmd->plug)
+ return;
+ if (cmd->locked) {
+ cmd->locked = FALSE;
+ if (!cmd->conn->corked)
+ smtp_client_connection_trigger_output(cmd->conn);
+ }
+}
+
+void smtp_client_command_abort(struct smtp_client_command **_cmd)
+{
+ struct smtp_client_command *cmd = *_cmd;
+
+ if (cmd == NULL)
+ return;
+ *_cmd = NULL;
+
+ struct smtp_client_connection *conn = cmd->conn;
+ enum smtp_client_command_state state = cmd->state;
+ bool disconnected =
+ (conn->state == SMTP_CLIENT_CONNECTION_STATE_DISCONNECTED);
+ bool was_locked =
+ (state >= SMTP_CLIENT_COMMAND_STATE_SUBMITTED) &&
+ (cmd->locked || cmd->plug);
+ bool was_sent =
+ (!disconnected && state > SMTP_CLIENT_COMMAND_STATE_SUBMITTED &&
+ state < SMTP_CLIENT_COMMAND_STATE_FINISHED);
+
+ smtp_client_command_drop_callback(cmd);
+
+ if ((!disconnected && !cmd->plug && cmd->aborting) ||
+ state >= SMTP_CLIENT_COMMAND_STATE_FINISHED)
+ return;
+
+ struct event_passthrough *e = event_create_passthrough(cmd->event);
+ if (!cmd->event_finished) {
+ struct smtp_reply failure;
+
+ smtp_reply_init(&failure,
+ SMTP_CLIENT_COMMAND_ERROR_ABORTED, "Aborted");
+ failure.enhanced_code = SMTP_REPLY_ENH_CODE(9, 0, 0);
+
+ e->set_name("smtp_client_command_finished");
+ smtp_reply_add_to_event(&failure, e);
+ cmd->event_finished = TRUE;
+ }
+ e_debug(e->event(), "Aborted%s",
+ (was_sent ? " (already sent)" : ""));
+
+ if (!was_sent) {
+ cmd->state = SMTP_CLIENT_COMMAND_STATE_ABORTED;
+ } else {
+ i_assert(state < SMTP_CLIENT_COMMAND_STATE_FINISHED);
+ cmd->aborting = TRUE;
+ }
+ cmd->locked = FALSE;
+
+ i_assert(!cmd->plug || state <= SMTP_CLIENT_COMMAND_STATE_SUBMITTED);
+
+ switch (state) {
+ case SMTP_CLIENT_COMMAND_STATE_NEW:
+ if (cmd->delaying_failure) {
+ DLLIST_REMOVE(&conn->cmd_fail_list, cmd);
+ if (conn->cmd_fail_list == NULL)
+ timeout_remove(&conn->to_cmd_fail);
+ }
+ break;
+ case SMTP_CLIENT_COMMAND_STATE_SENDING:
+ if (!disconnected) {
+ /* It is being sent; cannot truly abort it now */
+ break;
+ }
+ /* fall through */
+ case SMTP_CLIENT_COMMAND_STATE_SUBMITTED:
+ /* Not yet sent */
+ e_debug(cmd->event, "Removed from send queue");
+ i_assert(conn->cmd_send_queue_count > 0);
+ DLLIST2_REMOVE(&conn->cmd_send_queue_head,
+ &conn->cmd_send_queue_tail, cmd);
+ i_assert(conn->cmd_send_queue_count > 1 ||
+ (cmd->prev == NULL && cmd->next == NULL));
+ conn->cmd_send_queue_count--;
+ break;
+ case SMTP_CLIENT_COMMAND_STATE_WAITING:
+ if (!disconnected) {
+ /* We're expecting a reply; cannot truly abort it now */
+ break;
+ }
+ e_debug(cmd->event, "Removed from wait list");
+ i_assert(conn->cmd_wait_list_count > 0);
+ DLLIST2_REMOVE(&conn->cmd_wait_list_head,
+ &conn->cmd_wait_list_tail, cmd);
+ conn->cmd_wait_list_count--;
+ break;
+ default:
+ i_unreached();
+ }
+
+ if (cmd->abort_callback != NULL) {
+ cmd->abort_callback(cmd->abort_context);
+ cmd->abort_callback = NULL;
+ }
+
+ if (disconnected || cmd->plug ||
+ state <= SMTP_CLIENT_COMMAND_STATE_SUBMITTED) {
+ /* Can only destroy it when it is not pending */
+ smtp_client_command_unref(&cmd);
+ }
+
+ if (!disconnected && was_locked && !conn->corked)
+ smtp_client_connection_trigger_output(conn);
+}
+
+void smtp_client_command_drop_callback(struct smtp_client_command *cmd)
+{
+ cmd->callback = NULL;
+ cmd->context = NULL;
+}
+
+void smtp_client_command_fail_reply(struct smtp_client_command **_cmd,
+ const struct smtp_reply *reply)
+{
+ struct smtp_client_command *cmd = *_cmd, *tmp_cmd;
+
+ if (cmd == NULL)
+ return;
+ *_cmd = NULL;
+
+ struct smtp_client_connection *conn = cmd->conn;
+ enum smtp_client_command_state state = cmd->state;
+ smtp_client_command_callback_t *callback = cmd->callback;
+
+ if (state >= SMTP_CLIENT_COMMAND_STATE_FINISHED)
+ return;
+
+ if (cmd->delay_failure) {
+ i_assert(cmd->delayed_failure == NULL);
+ i_assert(state < SMTP_CLIENT_COMMAND_STATE_SUBMITTED);
+
+ e_debug(cmd->event, "Fail (delay)");
+
+ cmd->delayed_failure = smtp_reply_clone(cmd->pool, reply);
+ cmd->delaying_failure = TRUE;
+ if (conn->to_cmd_fail == NULL) {
+ conn->to_cmd_fail = timeout_add_short(
+ 0, smtp_client_commands_fail_delayed, conn);
+ }
+ DLLIST_PREPEND(&conn->cmd_fail_list, cmd);
+ return;
+ }
+
+ cmd->callback = NULL;
+
+ smtp_client_connection_ref(conn);
+ smtp_client_command_ref(cmd);
+
+ if (!cmd->aborting) {
+ cmd->failed = TRUE;
+
+ struct event_passthrough *e =
+ event_create_passthrough(cmd->event);
+ if (!cmd->event_finished) {
+ e->set_name("smtp_client_command_finished");
+ smtp_reply_add_to_event(reply, e);
+ cmd->event_finished = TRUE;
+ }
+ e_debug(e->event(), "Failed: %s", smtp_reply_log(reply));
+
+ if (callback != NULL)
+ (void)callback(reply, cmd->context);
+ }
+
+ tmp_cmd = cmd;
+ smtp_client_command_abort(&tmp_cmd);
+
+ smtp_client_command_unref(&cmd);
+ smtp_client_connection_unref(&conn);
+}
+
+void smtp_client_command_fail(struct smtp_client_command **_cmd,
+ unsigned int status, const char *error)
+{
+ struct smtp_reply reply;
+ const char *text_lines[] = {error, NULL};
+
+ i_zero(&reply);
+ reply.status = status;
+ reply.text_lines = text_lines;
+ reply.enhanced_code.x = 9;
+
+ smtp_client_command_fail_reply(_cmd, &reply);
+}
+
+static void smtp_client_command_fail_delayed(struct smtp_client_command **_cmd)
+{
+ struct smtp_client_command *cmd = *_cmd;
+
+ e_debug(cmd->event, "Fail delayed");
+
+ i_assert(!cmd->delay_failure);
+ i_assert(cmd->state < SMTP_CLIENT_COMMAND_STATE_FINISHED);
+ smtp_client_command_fail_reply(_cmd, cmd->delayed_failure);
+}
+
+void smtp_client_commands_list_abort(struct smtp_client_command *cmds_list,
+ unsigned int cmds_list_count)
+{
+ struct smtp_client_command *cmd;
+ ARRAY(struct smtp_client_command *) cmds_arr;
+ struct smtp_client_command **cmds;
+ unsigned int count, i;
+
+ if (cmds_list == NULL)
+ return;
+ i_assert(cmds_list_count > 0);
+
+ /* Copy the array and reference the commands to be robust against more
+ than one command disappearing from the list */
+ t_array_init(&cmds_arr, cmds_list_count);
+ for (cmd = cmds_list; cmd != NULL; cmd = cmd->next) {
+ smtp_client_command_ref(cmd);
+ array_push_back(&cmds_arr, &cmd);
+ }
+
+ cmds = array_get_modifiable(&cmds_arr, &count);
+ for (i = 0; i < count; i++) {
+ cmd = cmds[i];
+ /* Fail the reply */
+ smtp_client_command_abort(&cmds[i]);
+ /* Drop our reference */
+ smtp_client_command_unref(&cmd);
+ }
+}
+
+void smtp_client_commands_list_fail_reply(struct smtp_client_command *cmds_list,
+ unsigned int cmds_list_count,
+ const struct smtp_reply *reply)
+{
+ struct smtp_client_command *cmd;
+ ARRAY(struct smtp_client_command *) cmds_arr;
+ struct smtp_client_command **cmds;
+ unsigned int count, i;
+
+ if (cmds_list == NULL)
+ return;
+ i_assert(cmds_list_count > 0);
+
+ /* Copy the array and reference the commands to be robust against more
+ than one command disappearing from the list */
+ t_array_init(&cmds_arr, cmds_list_count);
+ for (cmd = cmds_list; cmd != NULL; cmd = cmd->next) {
+ smtp_client_command_ref(cmd);
+ array_push_back(&cmds_arr, &cmd);
+ }
+
+ cmds = array_get_modifiable(&cmds_arr, &count);
+ for (i = 0; i < count; i++) {
+ cmd = cmds[i];
+ /* Fail the reply */
+ smtp_client_command_fail_reply(&cmds[i], reply);
+ /* Drop our reference */
+ smtp_client_command_unref(&cmd);
+ }
+}
+
+void smtp_client_commands_abort_delayed(struct smtp_client_connection *conn)
+{
+ struct smtp_client_command *cmd;
+
+ timeout_remove(&conn->to_cmd_fail);
+
+ cmd = conn->cmd_fail_list;
+ conn->cmd_fail_list = NULL;
+ while (cmd != NULL) {
+ struct smtp_client_command *cmd_next = cmd->next;
+
+ cmd->delaying_failure = FALSE;
+ smtp_client_command_abort(&cmd);
+ cmd = cmd_next;
+ }
+}
+
+void smtp_client_commands_fail_delayed(struct smtp_client_connection *conn)
+{
+ struct smtp_client_command *cmd;
+
+ timeout_remove(&conn->to_cmd_fail);
+
+ cmd = conn->cmd_fail_list;
+ conn->cmd_fail_list = NULL;
+ while (cmd != NULL) {
+ struct smtp_client_command *cmd_next = cmd->next;
+
+ cmd->delaying_failure = FALSE;
+ smtp_client_command_fail_delayed(&cmd);
+ cmd = cmd_next;
+ }
+}
+
+void smtp_client_command_set_abort_callback(struct smtp_client_command *cmd,
+ void (*callback)(void *context),
+ void *context)
+{
+ cmd->abort_callback = callback;
+ cmd->abort_context = context;
+}
+
+void smtp_client_command_set_sent_callback(struct smtp_client_command *cmd,
+ void (*callback)(void *context),
+ void *context)
+{
+ cmd->sent_callback = callback;
+ cmd->sent_context = context;
+}
+
+void smtp_client_command_set_replies(struct smtp_client_command *cmd,
+ unsigned int replies)
+{
+ i_assert(cmd->replies_expected == 1 ||
+ cmd->replies_expected == replies);
+ i_assert(replies > 0);
+ i_assert(cmd->replies_seen <= 1);
+ cmd->replies_expected = replies;
+}
+
+static void smtp_client_command_sent(struct smtp_client_command *cmd)
+{
+ struct event_passthrough *e;
+
+ e = event_create_passthrough(cmd->event)->
+ set_name("smtp_client_command_sent");
+
+ if (cmd->data == NULL)
+ e_debug(e->event(), "Sent");
+ else {
+ i_assert(str_len(cmd->data) > 2);
+ str_truncate(cmd->data, str_len(cmd->data)-2);
+ e_debug(e->event(), "Sent: %s", str_c(cmd->data));
+ }
+
+ if (smtp_client_command_name_equals(cmd, "QUIT"))
+ cmd->conn->sent_quit = TRUE;
+
+ if (cmd->sent_callback != NULL) {
+ cmd->sent_callback(cmd->sent_context);
+ cmd->sent_callback = NULL;
+ }
+}
+
+static int
+smtp_client_command_finish_dot_stream(struct smtp_client_command *cmd)
+{
+ struct smtp_client_connection *conn = cmd->conn;
+ int ret;
+
+ i_assert(cmd->stream_dot);
+ i_assert(conn->dot_output != NULL);
+
+ /* This concludes the dot stream with CRLF.CRLF */
+ if ((ret = o_stream_finish(conn->dot_output)) < 0) {
+ o_stream_unref(&conn->dot_output);
+ smtp_client_connection_handle_output_error(conn);
+ return -1;
+ }
+ if (ret == 0)
+ return 0;
+ o_stream_unref(&conn->dot_output);
+ return 1;
+}
+
+static void smtp_client_command_payload_input(struct smtp_client_command *cmd)
+{
+ struct smtp_client_connection *conn = cmd->conn;
+
+ io_remove(&conn->io_cmd_payload);
+
+ smtp_client_connection_trigger_output(conn);
+}
+
+static int smtp_client_command_send_stream(struct smtp_client_command *cmd)
+{
+ struct smtp_client_connection *conn = cmd->conn;
+ struct istream *stream = cmd->stream;
+ struct ostream *output = conn->conn.output;
+ enum ostream_send_istream_result res;
+ int ret;
+
+ io_remove(&conn->io_cmd_payload);
+
+ if (cmd->stream_finished) {
+ if ((ret = smtp_client_command_finish_dot_stream(cmd)) <= 0)
+ return ret;
+ /* Done sending payload */
+ e_debug(cmd->event, "Finished sending payload");
+ i_stream_unref(&cmd->stream);
+ return 1;
+ }
+ if (cmd->stream_dot) {
+ if (conn->dot_output == NULL)
+ conn->dot_output = o_stream_create_dot(output, FALSE);
+ output = conn->dot_output;
+ }
+
+ /* We're sending the stream now */
+ o_stream_set_max_buffer_size(output, IO_BLOCK_SIZE);
+ res = o_stream_send_istream(output, stream);
+ o_stream_set_max_buffer_size(output, SIZE_MAX);
+
+ switch (res) {
+ case OSTREAM_SEND_ISTREAM_RESULT_FINISHED:
+ i_assert(cmd->stream_size == 0 ||
+ stream->v_offset == cmd->stream_size);
+ /* Finished with the stream */
+ e_debug(cmd->event, "Finished reading payload stream");
+ cmd->stream_finished = TRUE;
+ if (cmd->stream_dot) {
+ ret = smtp_client_command_finish_dot_stream(cmd);
+ if (ret <= 0)
+ return ret;
+ }
+ /* Done sending payload */
+ e_debug(cmd->event, "Finished sending payload");
+ i_stream_unref(&cmd->stream);
+ return 1;
+ case OSTREAM_SEND_ISTREAM_RESULT_WAIT_INPUT:
+ /* Input is blocking (client needs to act; disable timeout) */
+ conn->io_cmd_payload = io_add_istream(
+ stream, smtp_client_command_payload_input, cmd);
+ return 0;
+ case OSTREAM_SEND_ISTREAM_RESULT_WAIT_OUTPUT:
+ e_debug(cmd->event, "Partially sent payload");
+ i_assert(cmd->stream_size == 0 ||
+ stream->v_offset < cmd->stream_size);
+ return 0;
+ case OSTREAM_SEND_ISTREAM_RESULT_ERROR_INPUT:
+ /* The provided payload stream is broken;
+ fail this command separately */
+ smtp_client_command_fail(
+ &cmd, SMTP_CLIENT_COMMAND_ERROR_BROKEN_PAYLOAD,
+ "Broken payload stream");
+ /* We're in the middle of sending a command, so the connection
+ will also have to be aborted */
+ o_stream_unref(&conn->dot_output);
+ smtp_client_connection_fail(
+ conn, SMTP_CLIENT_COMMAND_ERROR_CONNECTION_LOST,
+ t_strdup_printf("read(%s) failed: %s",
+ i_stream_get_name(stream),
+ i_stream_get_error(stream)),
+ "Broken payload stream");
+ return -1;
+ case OSTREAM_SEND_ISTREAM_RESULT_ERROR_OUTPUT:
+ /* Normal connection failure */
+ o_stream_unref(&conn->dot_output);
+ smtp_client_connection_handle_output_error(conn);
+ return -1;
+ }
+ i_unreached();
+}
+
+static int smtp_client_command_send_line(struct smtp_client_command *cmd)
+{
+ struct smtp_client_connection *conn = cmd->conn;
+ const char *data;
+ size_t size;
+ ssize_t sent;
+
+ if (cmd->data == NULL)
+ return 1;
+
+ while (cmd->send_pos < cmd->data->used) {
+ data = CONST_PTR_OFFSET(cmd->data->data, cmd->send_pos);
+ size = cmd->data->used - cmd->send_pos;
+
+ sent = o_stream_send(conn->conn.output, data, size);
+ if (sent <= 0) {
+ if (sent < 0) {
+ smtp_client_connection_handle_output_error(conn);
+ return -1;
+ }
+ e_debug(cmd->event, "Blocked while sending");
+ return 0;
+ }
+ cmd->send_pos += sent;
+ }
+
+ i_assert(cmd->send_pos == cmd->data->used);
+ return 1;
+}
+
+static bool
+smtp_client_command_pipeline_is_open(struct smtp_client_connection *conn)
+{
+ struct smtp_client_command *cmd = conn->cmd_send_queue_head;
+
+ if (cmd == NULL)
+ return TRUE;
+
+ if (cmd->plug) {
+ e_debug(cmd->event, "Pipeline is plugged");
+ return FALSE;
+ }
+
+ if (conn->state < SMTP_CLIENT_CONNECTION_STATE_READY &&
+ (cmd->flags & SMTP_CLIENT_COMMAND_FLAG_PRELOGIN) == 0) {
+ /* Wait until we're fully connected */
+ e_debug(cmd->event, "Connection not ready [state=%s]",
+ smtp_client_connection_state_names[conn->state]);
+ return FALSE;
+ }
+
+ cmd = conn->cmd_wait_list_head;
+ if (cmd != NULL &&
+ (conn->caps.standard & SMTP_CAPABILITY_PIPELINING) == 0) {
+ /* Cannot pipeline; wait for reply */
+ e_debug(cmd->event, "Pipeline occupied");
+ return FALSE;
+ }
+ while (cmd != NULL) {
+ if ((conn->caps.standard & SMTP_CAPABILITY_PIPELINING) == 0 ||
+ (cmd->flags & SMTP_CLIENT_COMMAND_FLAG_PIPELINE) == 0 ||
+ cmd->locked) {
+ /* Cannot pipeline with previous command;
+ wait for reply */
+ e_debug(cmd->event, "Pipeline blocked");
+ return FALSE;
+ }
+ cmd = cmd->next;
+ }
+
+ return TRUE;
+}
+
+static void smtp_cient_command_wait(struct smtp_client_command *cmd)
+{
+ struct smtp_client_connection *conn = cmd->conn;
+
+ /* Move command to wait list. */
+ i_assert(conn->cmd_send_queue_count > 0);
+ i_assert(conn->cmd_send_queue_count > 1 ||
+ (cmd->prev == NULL && cmd->next == NULL));
+ DLLIST2_REMOVE(&conn->cmd_send_queue_head,
+ &conn->cmd_send_queue_tail, cmd);
+ conn->cmd_send_queue_count--;
+ DLLIST2_APPEND(&conn->cmd_wait_list_head,
+ &conn->cmd_wait_list_tail, cmd);
+ conn->cmd_wait_list_count++;
+}
+
+static int smtp_client_command_do_send_more(struct smtp_client_connection *conn)
+{
+ struct smtp_client_command *cmd;
+ int ret;
+
+ if (conn->cmd_streaming != NULL) {
+ cmd = conn->cmd_streaming;
+ i_assert(cmd->stream != NULL);
+ } else {
+ /* Check whether we can send anything */
+ cmd = conn->cmd_send_queue_head;
+ if (cmd == NULL)
+ return 0;
+ if (!smtp_client_command_pipeline_is_open(conn))
+ return 0;
+
+ cmd->state = SMTP_CLIENT_COMMAND_STATE_SENDING;
+ conn->sending_command = TRUE;
+
+ if ((ret = smtp_client_command_send_line(cmd)) <= 0)
+ return ret;
+
+ /* Command line sent. move command to wait list. */
+ smtp_cient_command_wait(cmd);
+ cmd->state = SMTP_CLIENT_COMMAND_STATE_WAITING;
+ }
+
+ if (cmd->stream != NULL &&
+ (ret = smtp_client_command_send_stream(cmd)) <= 0) {
+ if (ret < 0)
+ return -1;
+ e_debug(cmd->event, "Blocked while sending payload");
+ if (conn->cmd_streaming != cmd) {
+ i_assert(conn->cmd_streaming == NULL);
+ conn->cmd_streaming = cmd;
+ smtp_client_command_ref(cmd);
+ }
+ return 0;
+ }
+
+ conn->sending_command = FALSE;
+ if (conn->cmd_streaming != cmd ||
+ smtp_client_command_unref(&conn->cmd_streaming))
+ smtp_client_command_sent(cmd);
+ return 1;
+}
+
+int smtp_client_command_send_more(struct smtp_client_connection *conn)
+{
+ int ret;
+
+ while ((ret = smtp_client_command_do_send_more(conn)) > 0);
+ if (ret < 0)
+ return -1;
+
+ smtp_client_connection_update_cmd_timeout(conn);
+ return ret;
+}
+
+static void
+smtp_client_command_disconnected(struct smtp_client_connection *conn)
+{
+ smtp_client_connection_fail(
+ conn, SMTP_CLIENT_COMMAND_ERROR_CONNECTION_LOST,
+ NULL, "Disconnected");
+}
+
+static void
+smtp_client_command_insert_prioritized(struct smtp_client_command *cmd,
+ enum smtp_client_command_flags flag)
+{
+ struct smtp_client_connection *conn = cmd->conn;
+ struct smtp_client_command *cmd_cur, *cmd_prev;
+
+ cmd_cur = conn->cmd_send_queue_head;
+ if (cmd_cur == NULL || (cmd_cur->flags & flag) == 0) {
+ DLLIST2_PREPEND(&conn->cmd_send_queue_head,
+ &conn->cmd_send_queue_tail, cmd);
+ conn->cmd_send_queue_count++;
+ } else {
+ cmd_prev = cmd_cur;
+ cmd_cur = cmd_cur->next;
+ while (cmd_cur != NULL && (cmd_cur->flags & flag) != 0) {
+ cmd_prev = cmd_cur;
+ cmd_cur = cmd_cur->next;
+ }
+ DLLIST2_INSERT_AFTER(&conn->cmd_send_queue_head,
+ &conn->cmd_send_queue_tail, cmd_prev, cmd);
+ conn->cmd_send_queue_count++;
+ }
+}
+
+void smtp_client_command_submit_after(struct smtp_client_command *cmd,
+ struct smtp_client_command *after)
+{
+ struct smtp_client_connection *conn = cmd->conn;
+ struct event_passthrough *e;
+
+ i_assert(after == NULL || cmd->conn == after->conn);
+
+ smtp_client_command_update_event(cmd);
+ e = event_create_passthrough(cmd->event)->
+ set_name("smtp_client_command_started");
+
+ cmd->state = SMTP_CLIENT_COMMAND_STATE_SUBMITTED;
+
+ if (smtp_client_command_name_equals(cmd, "EHLO"))
+ cmd->ehlo = TRUE;
+
+ if (conn->state == SMTP_CLIENT_CONNECTION_STATE_DISCONNECTED) {
+ /* Add commands to send queue for delayed failure reply from
+ ioloop */
+ DLLIST2_APPEND(&conn->cmd_send_queue_head,
+ &conn->cmd_send_queue_tail, cmd);
+ conn->cmd_send_queue_count++;
+ if (conn->to_commands == NULL) {
+ conn->to_commands = timeout_add_short(
+ 0, smtp_client_command_disconnected, conn);
+ }
+ e_debug(e->event(), "Submitted, but disconnected");
+ return;
+ }
+
+ if (cmd->data != NULL)
+ str_append(cmd->data, "\r\n");
+
+ if ((cmd->flags & SMTP_CLIENT_COMMAND_FLAG_PRELOGIN) != 0 &&
+ conn->state < SMTP_CLIENT_CONNECTION_STATE_READY) {
+ /* Pre-login commands get inserted before everything else */
+ smtp_client_command_insert_prioritized(
+ cmd, SMTP_CLIENT_COMMAND_FLAG_PRELOGIN);
+ if (!conn->corked)
+ smtp_client_connection_trigger_output(conn);
+ e_debug(e->event(), "Submitted with priority");
+ return;
+ }
+
+ if (after != NULL) {
+ if (after->state >= SMTP_CLIENT_COMMAND_STATE_WAITING) {
+ /* Not in the send queue anymore; just prepend */
+ DLLIST2_PREPEND(&conn->cmd_send_queue_head,
+ &conn->cmd_send_queue_tail, cmd);
+ conn->cmd_send_queue_count++;
+ } else {
+ /* Insert after indicated command */
+ DLLIST2_INSERT_AFTER(&conn->cmd_send_queue_head,
+ &conn->cmd_send_queue_tail,
+ after, cmd);
+ conn->cmd_send_queue_count++;
+ }
+ } else if ((cmd->flags & SMTP_CLIENT_COMMAND_FLAG_PRIORITY) != 0) {
+ /* Insert at beginning of queue for priority commands */
+ smtp_client_command_insert_prioritized(
+ cmd, SMTP_CLIENT_COMMAND_FLAG_PRIORITY);
+ } else {
+ /* Just append at end of queue */
+ DLLIST2_APPEND(&conn->cmd_send_queue_head,
+ &conn->cmd_send_queue_tail, cmd);
+ conn->cmd_send_queue_count++;
+ }
+
+ if (conn->state >= SMTP_CLIENT_CONNECTION_STATE_READY)
+ smtp_client_connection_start_cmd_timeout(conn);
+
+ if (!conn->corked)
+ smtp_client_connection_trigger_output(conn);
+ e_debug(e->event(), "Submitted");
+}
+
+void smtp_client_command_submit(struct smtp_client_command *cmd)
+{
+ smtp_client_command_submit_after(cmd, NULL);
+}
+
+void smtp_client_command_set_flags(struct smtp_client_command *cmd,
+ enum smtp_client_command_flags flags)
+{
+ cmd->flags = flags;
+}
+
+void smtp_client_command_write(struct smtp_client_command *cmd,
+ const char *cmd_str)
+{
+ unsigned int len = strlen(cmd_str);
+
+ i_assert(cmd->state < SMTP_CLIENT_COMMAND_STATE_SUBMITTED);
+ if (cmd->data == NULL)
+ cmd->data = str_new(cmd->pool, len + 2);
+ str_append(cmd->data, cmd_str);
+}
+
+void smtp_client_command_printf(struct smtp_client_command *cmd,
+ const char *cmd_fmt, ...)
+{
+ va_list args;
+
+ va_start(args, cmd_fmt);
+ smtp_client_command_vprintf(cmd, cmd_fmt, args);
+ va_end(args);
+}
+
+void smtp_client_command_vprintf(struct smtp_client_command *cmd,
+ const char *cmd_fmt, va_list args)
+{
+ if (cmd->data == NULL)
+ cmd->data = str_new(cmd->pool, 128);
+ str_vprintfa(cmd->data, cmd_fmt, args);
+}
+
+void smtp_client_command_set_stream(struct smtp_client_command *cmd,
+ struct istream *input, bool dot)
+{
+ int ret;
+
+ cmd->stream = input;
+ i_stream_ref(input);
+
+ if ((ret = i_stream_get_size(input, TRUE, &cmd->stream_size)) <= 0) {
+ if (ret < 0) {
+ e_error(cmd->event, "i_stream_get_size(%s) failed: %s",
+ i_stream_get_name(input),
+ i_stream_get_error(input));
+ }
+ /* Size must be known if stream is to be sent in chunks */
+ i_assert(dot);
+ cmd->stream_size = 0;
+ }
+
+ cmd->stream_dot = dot;
+ cmd->has_stream = TRUE;
+}
+
+int smtp_client_command_input_reply(struct smtp_client_command *cmd,
+ const struct smtp_reply *reply)
+{
+ struct smtp_client_connection *conn = cmd->conn;
+ smtp_client_command_callback_t *callback = cmd->callback;
+ void *context = cmd->context;
+ bool finished;
+
+ i_assert(cmd->replies_seen < cmd->replies_expected);
+ finished = (++cmd->replies_seen == cmd->replies_expected);
+
+ /* Finish command event at final reply or first failure */
+ struct event_passthrough *e = event_create_passthrough(cmd->event);
+ if (!cmd->event_finished &&
+ (finished || !smtp_reply_is_success(reply))) {
+ e->set_name("smtp_client_command_finished");
+ smtp_reply_add_to_event(reply, e);
+ cmd->event_finished = TRUE;
+ }
+ e_debug(e->event(), "Got reply (%u/%u): %s "
+ "(%u commands pending, %u commands queued)",
+ cmd->replies_seen, cmd->replies_expected,
+ smtp_reply_log(reply), conn->cmd_wait_list_count,
+ conn->cmd_send_queue_count);
+
+ if (finished) {
+ i_assert(conn->cmd_wait_list_count > 0);
+ DLLIST2_REMOVE(&conn->cmd_wait_list_head,
+ &conn->cmd_wait_list_tail, cmd);
+ conn->cmd_wait_list_count--;
+ if (cmd->aborting)
+ cmd->state = SMTP_CLIENT_COMMAND_STATE_ABORTED;
+ else if (cmd->state != SMTP_CLIENT_COMMAND_STATE_ABORTED)
+ cmd->state = SMTP_CLIENT_COMMAND_STATE_FINISHED;
+
+ smtp_client_connection_update_cmd_timeout(conn);
+ smtp_client_command_drop_callback(cmd);
+ }
+
+ if (!cmd->aborting && callback != NULL)
+ callback(reply, context);
+
+ if (finished) {
+ smtp_client_command_unref(&cmd);
+ smtp_client_connection_trigger_output(conn);
+ }
+ return 1;
+}
+
+enum smtp_client_command_state
+smtp_client_command_get_state(struct smtp_client_command *cmd)
+{
+ return cmd->state;
+}
+
+/*
+ * Standard commands
+ */
+
+/* NOTE: Pipelining is only enabled for certain commands:
+
+ From RFC 2920, Section 3.1:
+
+ Once the client SMTP has confirmed that support exists for the
+ pipelining extension, the client SMTP may then elect to transmit
+ groups of SMTP commands in batches without waiting for a response to
+ each individual command. In particular, the commands RSET, MAIL FROM,
+ SEND FROM, SOML FROM, SAML FROM, and RCPT TO can all appear anywhere
+ in a pipelined command group. The EHLO, DATA, VRFY, EXPN, TURN,
+ QUIT, and NOOP commands can only appear as the last command in a
+ group since their success or failure produces a change of state which
+ the client SMTP must accommodate. (NOOP is included in this group so
+ it can be used as a synchronization point.)
+
+ Additional commands added by other SMTP extensions may only appear as
+ the last command in a group unless otherwise specified by the
+ extensions that define the commands.
+ */
+
+/* NOOP */
+
+#undef smtp_client_command_noop_submit_after
+struct smtp_client_command *
+smtp_client_command_noop_submit_after(struct smtp_client_connection *conn,
+ enum smtp_client_command_flags flags,
+ struct smtp_client_command *after,
+ smtp_client_command_callback_t *callback,
+ void *context)
+{
+ struct smtp_client_command *cmd;
+
+ cmd = smtp_client_command_new(conn, flags, callback, context);
+ smtp_client_command_write(cmd, "NOOP");
+ smtp_client_command_submit_after(cmd, after);
+ return cmd;
+}
+
+#undef smtp_client_command_noop_submit
+struct smtp_client_command *
+smtp_client_command_noop_submit(struct smtp_client_connection *conn,
+ enum smtp_client_command_flags flags,
+ smtp_client_command_callback_t *callback,
+ void *context)
+{
+ return smtp_client_command_noop_submit_after(conn, flags, NULL,
+ callback, context);
+}
+
+/* VRFY */
+
+#undef smtp_client_command_vrfy_submit_after
+struct smtp_client_command *
+smtp_client_command_vrfy_submit_after(struct smtp_client_connection *conn,
+ enum smtp_client_command_flags flags,
+ struct smtp_client_command *after,
+ const char *param,
+ smtp_client_command_callback_t *callback,
+ void *context)
+{
+ struct smtp_client_command *cmd;
+
+ cmd = smtp_client_command_new(conn, flags, callback, context);
+ smtp_client_command_write(cmd, "VRFY ");
+ smtp_string_write(cmd->data, param);
+ smtp_client_command_submit_after(cmd, after);
+ return cmd;
+}
+
+#undef smtp_client_command_vrfy_submit
+struct smtp_client_command *
+smtp_client_command_vrfy_submit(struct smtp_client_connection *conn,
+ enum smtp_client_command_flags flags,
+ const char *param,
+ smtp_client_command_callback_t *callback,
+ void *context)
+{
+ return smtp_client_command_vrfy_submit_after(conn, flags, NULL, param,
+ callback, context);
+}
+
+/* RSET */
+
+#undef smtp_client_command_rset_submit_after
+struct smtp_client_command *
+smtp_client_command_rset_submit_after(struct smtp_client_connection *conn,
+ enum smtp_client_command_flags flags,
+ struct smtp_client_command *after,
+ smtp_client_command_callback_t *callback,
+ void *context)
+{
+ struct smtp_client_command *cmd;
+
+ cmd = smtp_client_command_new(conn,
+ flags | SMTP_CLIENT_COMMAND_FLAG_PIPELINE,
+ callback, context);
+ smtp_client_command_write(cmd, "RSET");
+ smtp_client_command_submit_after(cmd, after);
+ return cmd;
+}
+
+#undef smtp_client_command_rset_submit
+struct smtp_client_command *
+smtp_client_command_rset_submit(struct smtp_client_connection *conn,
+ enum smtp_client_command_flags flags,
+ smtp_client_command_callback_t *callback,
+ void *context)
+{
+ return smtp_client_command_rset_submit_after(conn, flags, NULL,
+ callback, context);
+}
+
+/* MAIL FROM: */
+
+#undef smtp_client_command_mail_submit_after
+struct smtp_client_command *
+smtp_client_command_mail_submit_after(struct smtp_client_connection *conn,
+ enum smtp_client_command_flags flags,
+ struct smtp_client_command *after,
+ const struct smtp_address *from,
+ const struct smtp_params_mail *params,
+ smtp_client_command_callback_t *callback,
+ void *context)
+{
+ struct smtp_client_command *cmd;
+
+ smtp_client_connection_send_xclient(conn);
+
+ flags |= SMTP_CLIENT_COMMAND_FLAG_PIPELINE;
+ cmd = smtp_client_command_new(conn, flags, callback, context);
+ if (!conn->set.mail_send_broken_path || !smtp_address_is_broken(from)) {
+ /* Compose MAIL command with normalized path. */
+ smtp_client_command_printf(cmd, "MAIL FROM:<%s>",
+ smtp_address_encode(from));
+ } else {
+ /* Compose MAIL command with broken path (for proxy). */
+ smtp_client_command_printf(cmd, "MAIL FROM:<%s>",
+ smtp_address_encode_raw(from));
+ }
+ if (params != NULL) {
+ size_t orig_len = str_len(cmd->data);
+ const char *const *extensions = NULL;
+
+ if (array_is_created(&conn->caps.mail_param_extensions)) {
+ extensions =
+ array_front(&conn->caps.mail_param_extensions);
+ }
+
+ str_append_c(cmd->data, ' ');
+ smtp_params_mail_write(cmd->data, conn->caps.standard,
+ extensions, params);
+ if (str_len(cmd->data) == orig_len + 1)
+ str_truncate(cmd->data, orig_len);
+ }
+ smtp_client_command_submit_after(cmd, after);
+ return cmd;
+}
+
+#undef smtp_client_command_mail_submit
+struct smtp_client_command *
+smtp_client_command_mail_submit(struct smtp_client_connection *conn,
+ enum smtp_client_command_flags flags,
+ const struct smtp_address *from,
+ const struct smtp_params_mail *params,
+ smtp_client_command_callback_t *callback,
+ void *context)
+{
+ return smtp_client_command_mail_submit_after(conn, flags, NULL,
+ from, params,
+ callback, context);
+}
+
+/* RCPT TO: */
+
+#undef smtp_client_command_rcpt_submit_after
+struct smtp_client_command *
+smtp_client_command_rcpt_submit_after(struct smtp_client_connection *conn,
+ enum smtp_client_command_flags flags,
+ struct smtp_client_command *after,
+ const struct smtp_address *to,
+ const struct smtp_params_rcpt *params,
+ smtp_client_command_callback_t *callback,
+ void *context)
+{
+ struct smtp_client_command *cmd;
+
+ flags |= SMTP_CLIENT_COMMAND_FLAG_PIPELINE;
+ cmd = smtp_client_command_new(conn, flags, callback, context);
+ smtp_client_command_printf(cmd, "RCPT TO:<%s>",
+ smtp_address_encode(to));
+ if (params != NULL) {
+ size_t orig_len = str_len(cmd->data);
+ const char *const *extensions = NULL;
+
+ if (array_is_created(&conn->caps.rcpt_param_extensions)) {
+ extensions =
+ array_front(&conn->caps.rcpt_param_extensions);
+ }
+
+ str_append_c(cmd->data, ' ');
+ smtp_params_rcpt_write(cmd->data, conn->caps.standard,
+ extensions, params);
+ if (str_len(cmd->data) == orig_len + 1)
+ str_truncate(cmd->data, orig_len);
+ }
+ smtp_client_command_submit_after(cmd, after);
+ return cmd;
+}
+
+#undef smtp_client_command_rcpt_submit
+struct smtp_client_command *
+smtp_client_command_rcpt_submit(struct smtp_client_connection *conn,
+ enum smtp_client_command_flags flags,
+ const struct smtp_address *from,
+ const struct smtp_params_rcpt *params,
+ smtp_client_command_callback_t *callback,
+ void *context)
+{
+ return smtp_client_command_rcpt_submit_after(conn, flags, NULL, from,
+ params, callback, context);
+}
+
+/* DATA or BDAT */
+
+struct _cmd_data_context {
+ struct smtp_client_connection *conn;
+ pool_t pool;
+
+ struct smtp_client_command *cmd_data, *cmd_first;
+ ARRAY(struct smtp_client_command *) cmds;
+
+ struct istream *data;
+ uoff_t data_offset, data_left;
+};
+
+static void
+_cmd_bdat_send_chunks(struct _cmd_data_context *ctx,
+ struct smtp_client_command *after);
+
+static void _cmd_data_context_free(struct _cmd_data_context *ctx)
+{
+ if (ctx->cmd_data != NULL) {
+ /* Abort the main (possibly unsubmitted) data command */
+ smtp_client_command_set_abort_callback(ctx->cmd_data,
+ NULL, NULL);
+ ctx->cmd_data = NULL;
+ }
+ i_stream_unref(&ctx->data);
+}
+
+static void _cmd_data_abort(struct _cmd_data_context *ctx)
+{
+ struct smtp_client_command **cmds;
+ unsigned int count, i;
+
+ /* Drop all pending commands */
+ cmds = array_get_modifiable(&ctx->cmds, &count);
+ for (i = 0; i < count; i++) {
+ smtp_client_command_set_abort_callback(cmds[i], NULL, NULL);
+ smtp_client_command_abort(&cmds[i]);
+ }
+}
+
+static void _cmd_data_abort_cb(void *context)
+{
+ struct _cmd_data_context *ctx = (struct _cmd_data_context *)context;
+
+ /* The main (possibly unsubmitted) data command got aborted */
+ _cmd_data_abort(ctx);
+ _cmd_data_context_free(ctx);
+}
+
+static void
+_cmd_data_error(struct _cmd_data_context *ctx, const struct smtp_reply *reply)
+{
+ struct smtp_client_command *cmd = ctx->cmd_data;
+
+ if (cmd != NULL) {
+ /* Fail the main (possibly unsubmitted) data command so that
+ the caller gets notified */
+ smtp_client_command_fail_reply(&cmd, reply);
+ }
+}
+
+static void _cmd_data_cb(const struct smtp_reply *reply, void *context)
+{
+ struct _cmd_data_context *ctx = (struct _cmd_data_context *)context;
+ struct smtp_client_command *const *cmds, *cmd;
+ unsigned int count;
+
+ /* Got DATA reply; one command must be pending */
+ cmds = array_get(&ctx->cmds, &count);
+ i_assert(count > 0);
+
+ if (reply->status == 354) {
+ /* Submit second stage: which is a command with only a stream */
+ cmd = ctx->cmd_data;
+ smtp_client_command_submit_after(cmd, cmds[0]);
+
+ /* Nothing else to do, so drop the context already */
+ _cmd_data_context_free(ctx);
+ } else {
+ /* Error */
+ _cmd_data_error(ctx, reply);
+ }
+}
+
+static void _cmd_bdat_cb(const struct smtp_reply *reply, void *context)
+{
+ struct _cmd_data_context *ctx = (struct _cmd_data_context *)context;
+
+ /* Got BDAT reply, so there must be ones pending */
+ i_assert(array_count(&ctx->cmds) > 0);
+
+ if ((reply->status / 100) != 2) {
+ /* Error */
+ _cmd_data_error(ctx, reply);
+ return;
+ }
+
+ /* Drop the command from the list */
+ array_pop_front(&ctx->cmds);
+
+ /* Send more BDAT commands if necessary */
+ (void)_cmd_bdat_send_chunks(ctx, NULL);
+
+ if (array_count(&ctx->cmds) == 0) {
+ /* All of the BDAT commands finished already */
+ _cmd_data_context_free(ctx);
+ }
+}
+
+static void _cmd_bdat_sent_cb(void *context)
+{
+ struct _cmd_data_context *ctx = (struct _cmd_data_context *)context;
+
+ /* Send more BDAT commands if possible */
+ (void)_cmd_bdat_send_chunks(ctx, NULL);
+}
+
+static int
+_cmd_bdat_read_data(struct _cmd_data_context *ctx, size_t *data_size_r)
+{
+ int ret;
+
+ while ((ret = i_stream_read(ctx->data)) > 0);
+
+ if (ret < 0) {
+ if (ret != -2 && ctx->data->stream_errno != 0) {
+ e_error(ctx->cmd_data->event,
+ "Failed to read DATA stream: %s",
+ i_stream_get_error(ctx->data));
+ smtp_client_command_fail(
+ &ctx->cmd_data,
+ SMTP_CLIENT_COMMAND_ERROR_BROKEN_PAYLOAD,
+ "Broken payload stream");
+ return -1;
+ }
+ }
+
+ *data_size_r = i_stream_get_data_size(ctx->data);
+ return 0;
+}
+
+static void
+_cmd_bdat_send_chunks(struct _cmd_data_context *ctx,
+ struct smtp_client_command *after)
+{
+ struct smtp_client_connection *conn = ctx->conn;
+ const struct smtp_client_settings *set = &conn->set;
+ struct smtp_client_command *const *cmds, *cmd, *cmd_prev;
+ unsigned int count;
+ struct istream *chunk;
+ size_t data_size, max_chunk_size;
+
+ if (smtp_client_command_get_state(ctx->cmd_data) >=
+ SMTP_CLIENT_COMMAND_STATE_SUBMITTED) {
+ /* Finished or aborted */
+ return;
+ }
+
+ /* Pipeline management: determine where to submit the next command */
+ cmds = array_get(&ctx->cmds, &count);
+ cmd_prev = NULL;
+ if (after != NULL) {
+ i_assert(count == 0);
+ cmd_prev = after;
+ } else if (count > 0) {
+ cmd_prev = cmds[count-1];
+ smtp_client_command_unlock(cmd_prev);
+ }
+
+ data_size = ctx->data_left;
+ if (data_size > 0) {
+ max_chunk_size = set->max_data_chunk_size;
+ } else {
+ if (ctx->data->v_offset < ctx->data_offset) {
+ /* Previous BDAT command not completely sent */
+ return;
+ }
+ max_chunk_size = i_stream_get_max_buffer_size(ctx->data);
+ if (set->max_data_chunk_size < max_chunk_size)
+ max_chunk_size = set->max_data_chunk_size;
+ if (_cmd_bdat_read_data(ctx, &data_size) < 0)
+ return;
+ }
+
+ /* Keep sending more chunks until pipeline is filled to the limit */
+ cmd = NULL;
+ while (data_size > max_chunk_size ||
+ (data_size == max_chunk_size && !ctx->data->eof)) {
+ enum smtp_client_command_flags flags = ctx->cmd_data->flags;
+ size_t size = (data_size > set->max_data_chunk_size ?
+ set->max_data_chunk_size : data_size);
+ chunk = i_stream_create_range(ctx->data, ctx->data_offset,
+ size);
+
+ flags |= SMTP_CLIENT_COMMAND_FLAG_PIPELINE;
+ cmd = smtp_client_command_new(conn, flags, _cmd_bdat_cb, ctx);
+ smtp_client_command_set_abort_callback(cmd, _cmd_data_abort_cb,
+ ctx);
+ smtp_client_command_set_stream(cmd, chunk, FALSE);
+ i_stream_unref(&chunk);
+ smtp_client_command_printf(cmd, "BDAT %"PRIuUOFF_T,
+ (uoff_t)size);
+ smtp_client_command_submit_after(cmd, cmd_prev);
+ array_push_back(&ctx->cmds, &cmd);
+
+ ctx->data_offset += size;
+ data_size -= size;
+
+ if (array_count(&ctx->cmds) >= set->max_data_chunk_pipeline) {
+ /* Pipeline full */
+ if (ctx->data_left != 0) {
+ /* Data stream size known:
+ record where we left off */
+ ctx->data_left = data_size;
+ }
+ smtp_client_command_lock(cmd);
+ return;
+ }
+
+ cmd_prev = cmd;
+ }
+
+ if (ctx->data_left != 0) {
+ /* Data stream size known: record where we left off */
+ ctx->data_left = data_size;
+ } else if (!ctx->data->eof) {
+ /* More to read */
+ if (cmd != NULL) {
+ smtp_client_command_set_sent_callback(
+ cmd, _cmd_bdat_sent_cb, ctx);
+ }
+ return;
+ }
+
+ /* The last chunk, which may actually be empty */
+ chunk = i_stream_create_range(ctx->data, ctx->data_offset, data_size);
+
+ /* Submit final command */
+ cmd = ctx->cmd_data;
+ smtp_client_command_set_stream(cmd, chunk, FALSE);
+ i_stream_unref(&chunk);
+ smtp_client_command_printf(cmd, "BDAT %zu LAST", data_size);
+ smtp_client_command_submit_after(cmd, cmd_prev);
+
+ if (array_count(&ctx->cmds) == 0) {
+ /* All of the previous BDAT commands got replies already */
+ _cmd_data_context_free(ctx);
+ }
+}
+
+#undef smtp_client_command_data_submit_after
+struct smtp_client_command *
+smtp_client_command_data_submit_after(struct smtp_client_connection *conn,
+ enum smtp_client_command_flags flags,
+ struct smtp_client_command *after,
+ struct istream *data,
+ smtp_client_command_callback_t *callback,
+ void *context)
+{
+ const struct smtp_client_settings *set = &conn->set;
+ struct _cmd_data_context *ctx;
+ struct smtp_client_command *cmd, *cmd_data;
+
+ /* Create the final command early for reference by the caller;
+ it will not be submitted for now. The DATA command is handled in
+ two stages (== command submissions), the BDAT command in one or more.
+ */
+ cmd = cmd_data = smtp_client_command_create(conn, flags,
+ callback, context);
+
+ /* Protect against race conditions */
+ cmd_data->delay_failure = TRUE;
+
+ /* Create context in the final command's pool */
+ ctx = p_new(cmd->pool, struct _cmd_data_context, 1);
+ ctx->conn = conn;
+ ctx->pool = cmd->pool;
+ ctx->cmd_data = cmd;
+
+ /* Capture abort event with our context */
+ smtp_client_command_set_abort_callback(cmd, _cmd_data_abort_cb, ctx);
+
+ ctx->data = data;
+ i_stream_ref(data);
+
+ if ((conn->caps.standard & SMTP_CAPABILITY_CHUNKING) == 0) {
+ /* DATA */
+ p_array_init(&ctx->cmds, ctx->pool, 1);
+
+ /* Data stream is sent in one go in the second stage. Since the
+ data is sent in a '<CRLF>.<CRLF>'-terminated stream, it size
+ is not relevant here. */
+ smtp_client_command_set_stream(cmd, ctx->data, TRUE);
+
+ /* Submit the initial DATA command */
+ cmd = smtp_client_command_new(conn, flags, _cmd_data_cb, ctx);
+ smtp_client_command_set_abort_callback(cmd, _cmd_data_abort_cb,
+ ctx);
+ smtp_client_command_write(cmd, "DATA");
+ smtp_client_command_submit_after(cmd, after);
+ array_push_back(&ctx->cmds, &cmd);
+ } else {
+ /* BDAT */
+ p_array_init(&ctx->cmds, ctx->pool,
+ conn->set.max_data_chunk_pipeline);
+
+ /* The data stream is sent in multiple chunks. Either the size
+ of the data stream is known or it is not. These cases are
+ handled a little differently. */
+ if (i_stream_get_size(data, TRUE, &ctx->data_left) > 0) {
+ /* Size is known */
+ i_assert(ctx->data_left >= data->v_offset);
+ ctx->data_left -= data->v_offset;
+ } else {
+ /* Size is unknown */
+ ctx->data_left = 0;
+
+ /* Make sure we can send chunks of sufficient size by
+ making the data stream buffer size limit at least
+ equally large. */
+ if (i_stream_get_max_buffer_size(ctx->data) <
+ set->max_data_chunk_size) {
+ i_stream_set_max_buffer_size(
+ ctx->data, set->max_data_chunk_size);
+ }
+ }
+
+ /* Send the first BDAT command(s) */
+ ctx->data_offset = data->v_offset;
+ _cmd_bdat_send_chunks(ctx, after);
+ }
+
+ cmd_data->delay_failure = FALSE;
+ return cmd_data;
+}
+
+#undef smtp_client_command_data_submit
+struct smtp_client_command *
+smtp_client_command_data_submit(struct smtp_client_connection *conn,
+ enum smtp_client_command_flags flags,
+ struct istream *data,
+ smtp_client_command_callback_t *callback,
+ void *context)
+{
+ return smtp_client_command_data_submit_after(conn, flags, NULL, data,
+ callback, context);
+}
diff --git a/src/lib-smtp/smtp-client-command.h b/src/lib-smtp/smtp-client-command.h
new file mode 100644
index 0000000..70dba92
--- /dev/null
+++ b/src/lib-smtp/smtp-client-command.h
@@ -0,0 +1,274 @@
+#ifndef SMTP_CLIENT_COMMAND
+#define SMTP_CLIENT_COMMAND
+
+struct smtp_reply;
+struct smtp_params_mail;
+struct smtp_params_rcpt;
+struct smtp_client_command;
+struct smtp_client_connection;
+
+enum smtp_client_command_state {
+ SMTP_CLIENT_COMMAND_STATE_NEW = 0,
+ SMTP_CLIENT_COMMAND_STATE_SUBMITTED,
+ SMTP_CLIENT_COMMAND_STATE_SENDING,
+ SMTP_CLIENT_COMMAND_STATE_WAITING,
+ SMTP_CLIENT_COMMAND_STATE_FINISHED,
+ SMTP_CLIENT_COMMAND_STATE_ABORTED
+};
+
+enum smtp_client_command_flags {
+ /* The command is sent to server before login (or is the login
+ command itself). Non-prelogin commands will be queued until login
+ is successful. */
+ SMTP_CLIENT_COMMAND_FLAG_PRELOGIN = 0x01,
+ /* This command may be positioned anywhere in a PIPELINING group. */
+ SMTP_CLIENT_COMMAND_FLAG_PIPELINE = 0x02,
+ /* This command has priority and needs to be inserted before anything
+ else. This is e.g. used to make sure that the initial handshake
+ commands are sent before any other command that may already be
+ submitted to the connection. */
+ SMTP_CLIENT_COMMAND_FLAG_PRIORITY = 0x04
+};
+
+/* Called when reply is received for command. */
+typedef void smtp_client_command_callback_t(const struct smtp_reply *reply,
+ void *context);
+
+struct smtp_client_command *
+smtp_client_command_new(struct smtp_client_connection *conn,
+ enum smtp_client_command_flags flags,
+ smtp_client_command_callback_t *callback,
+ void *context);
+#define smtp_client_command_new(conn, flags, callback, context) \
+ smtp_client_command_new(conn, flags - \
+ CALLBACK_TYPECHECK(callback, void (*)( \
+ const struct smtp_reply *reply, typeof(context))), \
+ (smtp_client_command_callback_t *)callback, context)
+
+/* Create a plug command, which is a dummy command that blocks the send queue.
+ This is used by transactions to prevent subsequently submitted
+ transactions from messing up the command sequence while the present
+ transaction is still submitting commands. The plug command is aborted once
+ the send queue is to be released. */
+struct smtp_client_command *
+smtp_client_command_plug(struct smtp_client_connection *conn,
+ struct smtp_client_command *after);
+
+void smtp_client_command_ref(struct smtp_client_command *cmd);
+bool smtp_client_command_unref(struct smtp_client_command **_cmd)
+ ATTR_NOWARN_UNUSED_RESULT;
+
+bool smtp_client_command_name_equals(struct smtp_client_command *cmd,
+ const char *name);
+
+/* Lock the command; no commands after this one will be sent until this one
+ finishes */
+void smtp_client_command_lock(struct smtp_client_command *cmd);
+void smtp_client_command_unlock(struct smtp_client_command *cmd);
+
+void smtp_client_command_set_flags(struct smtp_client_command *cmd,
+ enum smtp_client_command_flags flags);
+void smtp_client_command_set_stream(struct smtp_client_command *cmd,
+ struct istream *input, bool dot);
+
+void smtp_client_command_write(struct smtp_client_command *cmd,
+ const char *cmd_str);
+void smtp_client_command_printf(struct smtp_client_command *cmd,
+ const char *cmd_fmt, ...) ATTR_FORMAT(2, 3);
+void smtp_client_command_vprintf(struct smtp_client_command *cmd,
+ const char *cmd_fmt, va_list args)
+ ATTR_FORMAT(2, 0);
+
+void smtp_client_command_submit_after(struct smtp_client_command *cmd,
+ struct smtp_client_command *after);
+void smtp_client_command_submit(struct smtp_client_command *cmd);
+
+void smtp_client_command_abort(struct smtp_client_command **_cmd);
+void smtp_client_command_set_abort_callback(struct smtp_client_command *cmd,
+ void (*callback)(void *context),
+ void *context);
+
+void smtp_client_command_set_sent_callback(struct smtp_client_command *cmd,
+ void (*callback)(void *context),
+ void *context);
+
+void smtp_client_command_set_replies(struct smtp_client_command *cmd,
+ unsigned int replies);
+
+enum smtp_client_command_state
+smtp_client_command_get_state(struct smtp_client_command *cmd) ATTR_PURE;
+
+
+/*
+ * Standard commands
+ */
+
+/* Send NOOP */
+struct smtp_client_command *
+smtp_client_command_noop_submit_after(struct smtp_client_connection *conn,
+ enum smtp_client_command_flags flags,
+ struct smtp_client_command *after,
+ smtp_client_command_callback_t *callback,
+ void *context);
+#define smtp_client_command_noop_submit_after(conn, flags, after, \
+ callback, context) \
+ smtp_client_command_noop_submit_after(conn, flags - \
+ CALLBACK_TYPECHECK(callback, void (*)( \
+ const struct smtp_reply *reply, typeof(context))), \
+ after, (smtp_client_command_callback_t *)callback, context)
+struct smtp_client_command *
+smtp_client_command_noop_submit(struct smtp_client_connection *conn,
+ enum smtp_client_command_flags flags,
+ smtp_client_command_callback_t *callback,
+ void *context);
+#define smtp_client_command_noop_submit(conn, flags, callback, context) \
+ smtp_client_command_noop_submit(conn, flags - \
+ CALLBACK_TYPECHECK(callback, void (*)( \
+ const struct smtp_reply *reply, typeof(context))), \
+ (smtp_client_command_callback_t *)callback, context)
+
+/* Send VRFY <param> */
+struct smtp_client_command *
+smtp_client_command_vrfy_submit_after(struct smtp_client_connection *conn,
+ enum smtp_client_command_flags flags,
+ struct smtp_client_command *after,
+ const char *param,
+ smtp_client_command_callback_t *callback,
+ void *context);
+#define smtp_client_command_vrfy_submit_after(conn, flags, after, param, \
+ callback, context) \
+ smtp_client_command_vrfy_submit_after(conn, flags - \
+ CALLBACK_TYPECHECK(callback, void (*)( \
+ const struct smtp_reply *reply, typeof(context))), \
+ after, param, \
+ (smtp_client_command_callback_t *)callback, context)
+struct smtp_client_command *
+smtp_client_command_vrfy_submit(struct smtp_client_connection *conn,
+ enum smtp_client_command_flags flags,
+ const char *param,
+ smtp_client_command_callback_t *callback,
+ void *context);
+#define smtp_client_command_vrfy_submit(conn, flags, param, callback, context) \
+ smtp_client_command_vrfy_submit(conn, flags - \
+ CALLBACK_TYPECHECK(callback, void (*)( \
+ const struct smtp_reply *reply, typeof(context))), \
+ param, (smtp_client_command_callback_t *)callback, context)
+
+/* Send RSET */
+struct smtp_client_command *
+smtp_client_command_rset_submit_after(struct smtp_client_connection *conn,
+ enum smtp_client_command_flags flags,
+ struct smtp_client_command *after,
+ smtp_client_command_callback_t *callback,
+ void *context);
+#define smtp_client_command_rset_submit_after(conn, flags, after, \
+ callback, context) \
+ smtp_client_command_rset_submit_after(conn, flags - \
+ CALLBACK_TYPECHECK(callback, void (*)( \
+ const struct smtp_reply *reply, typeof(context))), \
+ after, (smtp_client_command_callback_t *)callback, context)
+struct smtp_client_command *
+smtp_client_command_rset_submit(struct smtp_client_connection *conn,
+ enum smtp_client_command_flags flags,
+ smtp_client_command_callback_t *callback,
+ void *context);
+#define smtp_client_command_rset_submit(conn, flags, callback, context) \
+ smtp_client_command_rset_submit(conn, flags - \
+ CALLBACK_TYPECHECK(callback, void (*)( \
+ const struct smtp_reply *reply, typeof(context))), \
+ (smtp_client_command_callback_t *)callback, context)
+
+/* Send MAIL FROM:<address> <params...> */
+struct smtp_client_command *
+smtp_client_command_mail_submit_after(struct smtp_client_connection *conn,
+ enum smtp_client_command_flags flags,
+ struct smtp_client_command *after,
+ const struct smtp_address *from,
+ const struct smtp_params_mail *params,
+ smtp_client_command_callback_t *callback,
+ void *context);
+#define smtp_client_command_mail_submit_after(conn, flags, after, \
+ address, params, \
+ callback, context) \
+ smtp_client_command_mail_submit_after(conn, flags - \
+ CALLBACK_TYPECHECK(callback, void (*)( \
+ const struct smtp_reply *reply, typeof(context))), \
+ after, address, params, \
+ (smtp_client_command_callback_t *)callback, context)
+struct smtp_client_command *
+smtp_client_command_mail_submit(struct smtp_client_connection *conn,
+ enum smtp_client_command_flags flags,
+ const struct smtp_address *from,
+ const struct smtp_params_mail *params,
+ smtp_client_command_callback_t *callback,
+ void *context);
+#define smtp_client_command_mail_submit(conn, flags, address, params, \
+ callback, context) \
+ smtp_client_command_mail_submit(conn, flags - \
+ CALLBACK_TYPECHECK(callback, void (*)( \
+ const struct smtp_reply *reply, typeof(context))), \
+ address, params, \
+ (smtp_client_command_callback_t *)callback, context)
+
+/* send RCPT TO:<address> parameters */
+struct smtp_client_command *
+smtp_client_command_rcpt_submit_after(struct smtp_client_connection *conn,
+ enum smtp_client_command_flags flags,
+ struct smtp_client_command *after,
+ const struct smtp_address *to,
+ const struct smtp_params_rcpt *params,
+ smtp_client_command_callback_t *callback,
+ void *context);
+#define smtp_client_command_rcpt_submit_after(conn, flags, after, to, params, \
+ callback, context) \
+ smtp_client_command_rcpt_submit_after(conn, flags - \
+ CALLBACK_TYPECHECK(callback, void (*)( \
+ const struct smtp_reply *reply, typeof(context))), \
+ after, to, params, \
+ (smtp_client_command_callback_t *)callback, context)
+struct smtp_client_command *
+smtp_client_command_rcpt_submit(struct smtp_client_connection *conn,
+ enum smtp_client_command_flags flags,
+ const struct smtp_address *to,
+ const struct smtp_params_rcpt *params,
+ smtp_client_command_callback_t *callback,
+ void *context);
+#define smtp_client_command_rcpt_submit(conn, flags, to, params, \
+ callback, context) \
+ smtp_client_command_rcpt_submit(conn, flags - \
+ CALLBACK_TYPECHECK(callback, void (*)( \
+ const struct smtp_reply *reply, typeof(context))), \
+ to, params, \
+ (smtp_client_command_callback_t *)callback, context)
+
+/* Send message data using DATA or BDAT (preferred if supported).
+ This handles the DATA 354 response implicitly. Making sure that the data has
+ CRLF line endings consistently is the responsibility of the caller.
+ */
+struct smtp_client_command *
+smtp_client_command_data_submit_after(struct smtp_client_connection *conn,
+ enum smtp_client_command_flags flags,
+ struct smtp_client_command *after,
+ struct istream *data,
+ smtp_client_command_callback_t *callback,
+ void *context);
+#define smtp_client_command_data_submit_after(conn, flags, after, data, \
+ callback, context) \
+ smtp_client_command_data_submit_after(conn, flags - \
+ CALLBACK_TYPECHECK(callback, void (*)( \
+ const struct smtp_reply *reply, typeof(context))), \
+ after, data, \
+ (smtp_client_command_callback_t *)callback, context)
+struct smtp_client_command *
+smtp_client_command_data_submit(struct smtp_client_connection *conn,
+ enum smtp_client_command_flags flags,
+ struct istream *data,
+ smtp_client_command_callback_t *callback,
+ void *context);
+#define smtp_client_command_data_submit(conn, flags, data, callback, context) \
+ smtp_client_command_data_submit(conn, flags - \
+ CALLBACK_TYPECHECK(callback, void (*)( \
+ const struct smtp_reply *reply, typeof(context))), \
+ data, (smtp_client_command_callback_t *)callback, context)
+
+#endif
diff --git a/src/lib-smtp/smtp-client-connection.c b/src/lib-smtp/smtp-client-connection.c
new file mode 100644
index 0000000..47862ad
--- /dev/null
+++ b/src/lib-smtp/smtp-client-connection.c
@@ -0,0 +1,2522 @@
+/* Copyright (c) 2013-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "llist.h"
+#include "array.h"
+#include "safe-memset.h"
+#include "ioloop.h"
+#include "net.h"
+#include "base64.h"
+#include "istream.h"
+#include "ostream.h"
+#include "ostream-dot.h"
+#include "iostream-rawlog.h"
+#include "iostream-ssl.h"
+#include "str.h"
+#include "dsasl-client.h"
+#include "dns-lookup.h"
+#include "smtp-syntax.h"
+#include "smtp-reply-parser.h"
+#include "smtp-client-private.h"
+
+#include <ctype.h>
+
+#define SMTP_CLIENT_ERROR_TEXT_CONNECT_FAILED \
+ "Failed to connect to remote server"
+
+const char *const smtp_client_connection_state_names[] = {
+ "disconnected",
+ "connecting",
+ "handshaking",
+ "authenticating",
+ "ready",
+ "transaction"
+};
+
+static int
+smtp_client_connection_ssl_init(struct smtp_client_connection *conn,
+ const char **error_r);
+static void
+smtp_client_connection_handshake(struct smtp_client_connection *conn);
+static void
+smtp_client_connection_established(struct smtp_client_connection *conn);
+static void
+smtp_client_connection_start_transaction(struct smtp_client_connection *conn);
+static void
+smtp_client_connection_connect_next_ip(struct smtp_client_connection *conn);
+static bool
+smtp_client_connection_last_ip(struct smtp_client_connection *conn);
+
+/*
+ * Capabilities
+ */
+
+enum smtp_capability
+smtp_client_connection_get_capabilities(struct smtp_client_connection *conn)
+{
+ return conn->caps.standard;
+}
+
+uoff_t smtp_client_connection_get_size_capability(
+ struct smtp_client_connection *conn)
+{
+ return conn->caps.size;
+}
+
+static const struct smtp_client_capability_extra *
+smtp_client_connection_find_extra_capability(
+ struct smtp_client_connection *conn, const char *cap_name)
+{
+ const struct smtp_client_capability_extra *cap;
+
+ if (!array_is_created(&conn->extra_capabilities))
+ return NULL;
+ array_foreach(&conn->extra_capabilities, cap) {
+ if (strcasecmp(cap->name, cap_name) == 0)
+ return cap;
+ }
+ return NULL;
+}
+
+void smtp_client_connection_accept_extra_capability(
+ struct smtp_client_connection *conn,
+ const struct smtp_client_capability_extra *cap)
+{
+ i_assert(smtp_client_connection_find_extra_capability(conn, cap->name)
+ == NULL);
+
+ if (!array_is_created(&conn->extra_capabilities))
+ p_array_init(&conn->extra_capabilities, conn->pool, 8);
+
+ struct smtp_client_capability_extra cap_new = {
+ .name = p_strdup(conn->pool, cap->name),
+ };
+
+ if (cap->mail_param_extensions != NULL) {
+ cap_new.mail_param_extensions =
+ p_strarray_dup(conn->pool, cap->mail_param_extensions);
+ }
+ if (cap->rcpt_param_extensions != NULL) {
+ cap_new.rcpt_param_extensions =
+ p_strarray_dup(conn->pool, cap->rcpt_param_extensions);
+ }
+
+ array_push_back(&conn->extra_capabilities, &cap_new);
+}
+
+const struct smtp_capability_extra *
+smtp_client_connection_get_extra_capability(struct smtp_client_connection *conn,
+ const char *name)
+{
+ const struct smtp_capability_extra *cap;
+
+ if (!array_is_created(&conn->caps.extra))
+ return NULL;
+
+ array_foreach(&conn->caps.extra, cap) {
+ if (strcasecmp(cap->name, name) == 0)
+ return cap;
+ }
+
+ return NULL;
+}
+
+/*
+ *
+ */
+
+static void
+smtp_client_connection_commands_abort(struct smtp_client_connection *conn)
+{
+ smtp_client_commands_list_abort(conn->cmd_wait_list_head,
+ conn->cmd_wait_list_count);
+ smtp_client_commands_list_abort(conn->cmd_send_queue_head,
+ conn->cmd_send_queue_count);
+ smtp_client_commands_abort_delayed(conn);
+}
+
+static void
+smtp_client_connection_commands_fail_reply(struct smtp_client_connection *conn,
+ const struct smtp_reply *reply)
+{
+ smtp_client_commands_list_fail_reply(conn->cmd_wait_list_head,
+ conn->cmd_wait_list_count, reply);
+ smtp_client_commands_list_fail_reply(conn->cmd_send_queue_head,
+ conn->cmd_send_queue_count, reply);
+ smtp_client_commands_fail_delayed(conn);
+}
+
+static void
+smtp_client_connection_commands_fail(struct smtp_client_connection *conn,
+ unsigned int status, const char *error)
+{
+ struct smtp_reply reply;
+
+ smtp_reply_init(&reply, status, error);
+ reply.enhanced_code.x = 9;
+
+ smtp_client_connection_commands_fail_reply(conn, &reply);
+}
+
+static void
+smtp_client_connection_transactions_abort(struct smtp_client_connection *conn)
+{
+ struct smtp_client_transaction *trans;
+
+ trans = conn->transactions_head;
+ while (trans != NULL) {
+ struct smtp_client_transaction *trans_next = trans->next;
+ smtp_client_transaction_abort(trans);
+ trans = trans_next;
+ }
+}
+
+static void
+smtp_client_connection_transactions_fail_reply(
+ struct smtp_client_connection *conn, const struct smtp_reply *reply)
+{
+ struct smtp_client_transaction *trans;
+
+ trans = conn->transactions_head;
+ while (trans != NULL) {
+ struct smtp_client_transaction *trans_next = trans->next;
+ smtp_client_transaction_connection_result(trans, reply);
+ trans = trans_next;
+ }
+}
+
+static void
+smtp_client_connection_transactions_fail(struct smtp_client_connection *conn,
+ unsigned int status, const char *error)
+{
+ struct smtp_reply reply;
+
+ smtp_reply_init(&reply, status, error);
+ reply.enhanced_code.x = 9;
+
+ smtp_client_connection_transactions_fail_reply(conn, &reply);
+}
+
+static void
+smtp_client_connection_transactions_drop(struct smtp_client_connection *conn)
+{
+ struct smtp_client_transaction *trans;
+
+ trans = conn->transactions_head;
+ while (trans != NULL) {
+ struct smtp_client_transaction *trans_next = trans->next;
+ smtp_client_transaction_connection_destroyed(trans);
+ trans = trans_next;
+ }
+}
+
+static void
+smtp_client_connection_login_callback(struct smtp_client_connection *conn,
+ const struct smtp_reply *reply)
+{
+ const struct smtp_client_login_callback *cb;
+ ARRAY(struct smtp_client_login_callback) login_cbs;
+
+ if (conn->state_data.login_reply == NULL) {
+ conn->state_data.login_reply =
+ smtp_reply_clone(conn->state_pool, reply);
+ }
+
+ if (!array_is_created(&conn->login_callbacks) ||
+ array_count(&conn->login_callbacks) == 0)
+ return;
+
+ t_array_init(&login_cbs, array_count(&conn->login_callbacks));
+ array_copy(&login_cbs.arr, 0, &conn->login_callbacks.arr, 0,
+ array_count(&conn->login_callbacks));
+ array_foreach(&login_cbs, cb) {
+ i_assert(cb->callback != NULL);
+ if (conn->closed)
+ break;
+ if (cb->callback != NULL)
+ cb->callback(reply, cb->context);
+ }
+ array_clear(&conn->login_callbacks);
+}
+
+static void
+smtp_client_connection_login_fail(struct smtp_client_connection *conn,
+ unsigned int status, const char *error)
+{
+ struct smtp_reply reply;
+
+ smtp_reply_init(&reply, status, error);
+ reply.enhanced_code.x = 9;
+
+ smtp_client_connection_login_callback(conn, &reply);
+}
+
+static void
+smtp_client_connection_set_state(struct smtp_client_connection *conn,
+ enum smtp_client_connection_state state)
+{
+ conn->state = state;
+}
+
+void smtp_client_connection_cork(struct smtp_client_connection *conn)
+{
+ conn->corked = TRUE;
+ if (conn->conn.output != NULL)
+ o_stream_cork(conn->conn.output);
+}
+
+void smtp_client_connection_uncork(struct smtp_client_connection *conn)
+{
+ conn->corked = FALSE;
+ if (conn->conn.output != NULL) {
+ if (o_stream_uncork_flush(conn->conn.output) < 0) {
+ smtp_client_connection_handle_output_error(conn);
+ return;
+ }
+ smtp_client_connection_trigger_output(conn);
+ }
+}
+
+enum smtp_client_connection_state
+smtp_client_connection_get_state(struct smtp_client_connection *conn)
+{
+ return conn->state;
+}
+
+static void
+smtp_client_command_timeout(struct smtp_client_connection *conn)
+{
+ smtp_client_connection_ref(conn);
+
+ smtp_client_connection_fail(conn, SMTP_CLIENT_COMMAND_ERROR_TIMED_OUT,
+ "Command timed out, disconnecting",
+ "Command timed out");
+ smtp_client_connection_unref(&conn);
+}
+
+void smtp_client_connection_start_cmd_timeout(
+ struct smtp_client_connection *conn)
+{
+ unsigned int msecs = conn->set.command_timeout_msecs;
+
+ if (conn->state < SMTP_CLIENT_CONNECTION_STATE_READY) {
+ /* pre-login uses connect timeout */
+ return;
+ }
+ if (msecs == 0) {
+ /* no timeout configured */
+ timeout_remove(&conn->to_commands);
+ return;
+ }
+ if (conn->cmd_wait_list_head == NULL && !conn->sending_command) {
+ /* no commands pending */
+ timeout_remove(&conn->to_commands);
+ return;
+ }
+
+ e_debug(conn->event, "Start timeout");
+ if (conn->to_commands == NULL) {
+ conn->to_commands = timeout_add(
+ msecs, smtp_client_command_timeout, conn);
+ }
+}
+
+void smtp_client_connection_update_cmd_timeout(
+ struct smtp_client_connection *conn)
+{
+ unsigned int msecs = conn->set.command_timeout_msecs;
+
+ if (conn->state < SMTP_CLIENT_CONNECTION_STATE_READY) {
+ /* pre-login uses connect timeout */
+ return;
+ }
+ if (msecs == 0) {
+ /* no timeout configured */
+ timeout_remove(&conn->to_commands);
+ return;
+ }
+
+ if (conn->cmd_wait_list_head == NULL && !conn->sending_command) {
+ if (conn->to_commands != NULL) {
+ e_debug(conn->event,
+ "No commands pending; stop timeout");
+ }
+ timeout_remove(&conn->to_commands);
+ } else if (conn->to_commands != NULL) {
+ e_debug(conn->event, "Reset timeout");
+ timeout_reset(conn->to_commands);
+ } else {
+ smtp_client_connection_start_cmd_timeout(conn);
+ }
+}
+
+static void
+smtp_client_connection_fail_reply(struct smtp_client_connection *conn,
+ const struct smtp_reply *reply)
+{
+ e_debug(conn->event, "Connection failed: %s", smtp_reply_log(reply));
+
+ smtp_client_connection_ref(conn);
+ conn->failing = TRUE;
+
+ smtp_client_connection_disconnect(conn);
+ smtp_client_connection_login_callback(conn, reply);
+
+ smtp_client_connection_transactions_fail_reply(conn, reply);
+ smtp_client_connection_commands_fail_reply(conn, reply);
+
+ conn->failing = FALSE;
+ smtp_client_connection_unref(&conn);
+}
+
+void smtp_client_connection_fail(struct smtp_client_connection *conn,
+ unsigned int status, const char *error,
+ const char *user_error)
+{
+ struct smtp_reply reply;
+ const char *text_lines[2];
+
+ if (error != NULL)
+ e_error(conn->event, "%s", error);
+
+ i_zero(&text_lines);
+ i_assert(user_error != NULL);
+ if (conn->set.verbose_user_errors && error != NULL)
+ text_lines[0] = error;
+ else
+ text_lines[0] = user_error;
+
+ timeout_remove(&conn->to_connect);
+
+ if (status == SMTP_CLIENT_COMMAND_ERROR_CONNECT_FAILED &&
+ !smtp_client_connection_last_ip(conn)) {
+ conn->to_connect = timeout_add_short(
+ 0, smtp_client_connection_connect_next_ip, conn);
+ return;
+ }
+
+ i_zero(&reply);
+ reply.status = status;
+ reply.text_lines = text_lines;
+ reply.enhanced_code.x = 9;
+
+ smtp_client_connection_fail_reply(conn, &reply);
+}
+
+static void
+smtp_client_connection_fail_equal(struct smtp_client_connection *conn,
+ unsigned int status, const char *error)
+{
+ smtp_client_connection_fail(conn, status, error, error);
+}
+
+static void
+smtp_client_connection_lost(struct smtp_client_connection *conn,
+ const char *error, const char *user_error)
+{
+ if (error != NULL)
+ error = t_strdup_printf("Connection lost: %s", error);
+
+ if (user_error == NULL)
+ user_error = "Lost connection to remote server";
+ else {
+ user_error = t_strdup_printf(
+ "Lost connection to remote server: %s",
+ user_error);
+ }
+
+ if (conn->ssl_iostream != NULL) {
+ const char *sslerr =
+ ssl_iostream_get_last_error(conn->ssl_iostream);
+
+ if (error != NULL && sslerr != NULL) {
+ error = t_strdup_printf("%s (last SSL error: %s)",
+ error, sslerr);
+ } else if (sslerr != NULL) {
+ error = t_strdup_printf(
+ "Connection lost (last SSL error: %s)", sslerr);
+ }
+ if (ssl_iostream_has_handshake_failed(conn->ssl_iostream)) {
+ /* This isn't really a "connection lost", but that we
+ don't trust the remote's SSL certificate. */
+ i_assert(error != NULL);
+ smtp_client_connection_fail(
+ conn, SMTP_CLIENT_COMMAND_ERROR_CONNECT_FAILED,
+ error, user_error);
+ return;
+ }
+ }
+
+ smtp_client_connection_fail(
+ conn, SMTP_CLIENT_COMMAND_ERROR_CONNECTION_LOST,
+ error, user_error);
+}
+
+void smtp_client_connection_handle_output_error(
+ struct smtp_client_connection *conn)
+{
+ struct ostream *output = conn->conn.output;
+
+ if (output->stream_errno != EPIPE &&
+ output->stream_errno != ECONNRESET) {
+ smtp_client_connection_lost(
+ conn,
+ t_strdup_printf("write(%s) failed: %s",
+ o_stream_get_name(conn->conn.output),
+ o_stream_get_error(conn->conn.output)),
+ "Write failure");
+ } else {
+ smtp_client_connection_lost(
+ conn, "Remote disconnected while writing output",
+ "Remote closed connection unexpectedly");
+ }
+}
+
+static void
+stmp_client_connection_ready(struct smtp_client_connection *conn,
+ const struct smtp_reply *reply)
+{
+ timeout_remove(&conn->to_connect);
+
+ smtp_client_connection_set_state(
+ conn, SMTP_CLIENT_CONNECTION_STATE_READY);
+ conn->reset_needed = FALSE;
+
+ e_debug(conn->event, "Connection ready");
+
+ smtp_client_connection_login_callback(conn, reply);
+
+ smtp_client_connection_update_cmd_timeout(conn);
+
+ smtp_client_connection_start_transaction(conn);
+}
+
+static void
+smtp_client_connection_xclient_cb(const struct smtp_reply *reply,
+ struct smtp_client_connection *conn)
+{
+ e_debug(conn->event, "Received XCLIENT handshake reply: %s",
+ smtp_reply_log(reply));
+
+ i_assert(conn->xclient_replies_expected > 0);
+
+ if (reply->status == 421) {
+ smtp_client_connection_fail_reply(conn, reply);
+ return;
+ }
+ if (conn->state == SMTP_CLIENT_CONNECTION_STATE_DISCONNECTED)
+ return;
+
+ if (conn->to_connect != NULL)
+ timeout_reset(conn->to_connect);
+ if (--conn->xclient_replies_expected == 0)
+ smtp_client_connection_handshake(conn);
+}
+
+static void
+smtp_client_connection_xclient_submit(struct smtp_client_connection *conn,
+ const char *cmdstr)
+{
+ struct smtp_client_command *cmd;
+ enum smtp_client_command_flags flags;
+
+ e_debug(conn->event, "Sending XCLIENT handshake");
+
+ flags = SMTP_CLIENT_COMMAND_FLAG_PRELOGIN |
+ SMTP_CLIENT_COMMAND_FLAG_PRIORITY;
+
+ cmd = smtp_client_command_new(conn, flags,
+ smtp_client_connection_xclient_cb, conn);
+ smtp_client_command_write(cmd, cmdstr);
+ smtp_client_command_submit(cmd);
+
+ conn->xclient_replies_expected++;
+}
+
+static void
+smtp_client_connection_xclient_add(struct smtp_client_connection *conn,
+ string_t *str, size_t offset,
+ const char *field, const char *value)
+{
+ size_t prev_offset = str_len(str);
+ const char *new_field;
+
+ i_assert(prev_offset >= offset);
+
+ str_append_c(str, ' ');
+ str_append(str, field);
+ str_append_c(str, '=');
+ smtp_xtext_encode_cstr(str, value);
+
+ if (prev_offset == offset ||
+ str_len(str) <= SMTP_BASE_LINE_LENGTH_LIMIT)
+ return;
+
+ /* preserve field we just added */
+ new_field = t_strdup(str_c(str) + prev_offset);
+
+ /* revert to previous position */
+ str_truncate(str, prev_offset);
+
+ /* send XCLIENT command */
+ smtp_client_connection_xclient_submit(conn, str_c(str));
+
+ /* start next XCLIENT command with new field */
+ str_truncate(str, offset);
+ str_append(str, new_field);
+}
+
+static void ATTR_FORMAT(5, 6)
+smtp_client_connection_xclient_addf(struct smtp_client_connection *conn,
+ string_t *str, size_t offset,
+ const char *field, const char *format, ...)
+{
+ va_list args;
+
+ va_start(args, format);
+ smtp_client_connection_xclient_add(conn, str, offset, field,
+ t_strdup_vprintf(format, args));
+ va_end(args);
+}
+
+void smtp_client_connection_send_xclient(struct smtp_client_connection *conn)
+{
+ const struct smtp_proxy_data *xclient = &conn->set.proxy_data;
+ const char **xclient_args = conn->caps.xclient_args;
+ size_t offset;
+ string_t *str;
+
+ if (!conn->set.peer_trusted)
+ return;
+ if (conn->xclient_sent)
+ return;
+ if ((conn->caps.standard & SMTP_CAPABILITY_XCLIENT) == 0 ||
+ conn->caps.xclient_args == NULL)
+ return;
+
+ i_assert(conn->xclient_replies_expected == 0);
+
+ /* http://www.postfix.org/XCLIENT_README.html:
+
+ The client must not send XCLIENT commands that exceed the 512
+ character limit for SMTP commands. To avoid exceeding the limit the
+ client should send the information in multiple XCLIENT commands; for
+ example, send NAME and ADDR last, after HELO and PROTO. Once ADDR is
+ sent, the client is usually no longer authorized to send XCLIENT
+ commands.
+ */
+
+ str = t_str_new(64);
+ str_append(str, "XCLIENT");
+ offset = str_len(str);
+
+ /* HELO */
+ if (xclient->helo != NULL &&
+ str_array_icase_find(xclient_args, "HELO")) {
+ smtp_client_connection_xclient_add(conn, str, offset,
+ "HELO", xclient->helo);
+ }
+
+ /* PROTO */
+ if (str_array_icase_find(xclient_args, "PROTO")) {
+ switch (xclient->proto) {
+ case SMTP_PROXY_PROTOCOL_SMTP:
+ smtp_client_connection_xclient_add(conn, str, offset,
+ "PROTO", "SMTP");
+ break;
+ case SMTP_PROXY_PROTOCOL_ESMTP:
+ smtp_client_connection_xclient_add(conn, str, offset,
+ "PROTO", "ESMTP");
+ break;
+ case SMTP_PROXY_PROTOCOL_LMTP:
+ smtp_client_connection_xclient_add(conn, str, offset,
+ "PROTO", "LMTP");
+ break;
+ default:
+ break;
+ }
+ }
+
+ /* LOGIN */
+ if (xclient->login != NULL &&
+ str_array_icase_find(xclient_args, "LOGIN")) {
+ smtp_client_connection_xclient_add(conn, str, offset,
+ "LOGIN", xclient->login);
+ }
+
+ /* SESSION */
+ if (xclient->session != NULL &&
+ str_array_icase_find(xclient_args, "SESSION")) {
+ smtp_client_connection_xclient_add(conn, str, offset,
+ "SESSION", xclient->session);
+ }
+
+ /* TTL */
+ if (xclient->ttl_plus_1 > 0 &&
+ str_array_icase_find(xclient_args, "TTL")) {
+ smtp_client_connection_xclient_addf(conn, str, offset,
+ "TTL", "%u",
+ xclient->ttl_plus_1-1);
+ }
+
+ /* TIMEOUT */
+ if (xclient->timeout_secs > 0 &&
+ str_array_icase_find(xclient_args, "TIMEOUT")) {
+ smtp_client_connection_xclient_addf(conn, str, offset,
+ "TIMEOUT", "%u",
+ xclient->timeout_secs);
+ }
+
+ /* PORT */
+ if (xclient->source_port != 0 &&
+ str_array_icase_find(xclient_args, "PORT")) {
+ smtp_client_connection_xclient_addf(conn, str, offset,
+ "PORT", "%u",
+ xclient->source_port);
+ }
+
+ /* ADDR */
+ if (xclient->source_ip.family != 0 &&
+ str_array_icase_find(xclient_args, "ADDR")) {
+ const char *addr = net_ip2addr(&xclient->source_ip);
+
+ /* Older versions of Dovecot LMTP don't quite follow Postfix'
+ specification of the XCLIENT command regarding IPv6
+ addresses: the "IPV6:" prefix is omitted. For now, we
+ maintain this deviation for LMTP. Newer versions of Dovecot
+ LMTP can work with or without the prefix. */
+ if (conn->protocol != SMTP_PROTOCOL_LMTP &&
+ xclient->source_ip.family == AF_INET6)
+ addr = t_strconcat("IPV6:", addr, NULL);
+ smtp_client_connection_xclient_add(conn, str, offset,
+ "ADDR", addr);
+ }
+
+ /* final XCLIENT command */
+ if (str_len(str) > offset)
+ smtp_client_connection_xclient_submit(conn, str_c(str));
+
+ conn->xclient_sent = TRUE;
+}
+
+static void
+smtp_client_connection_clear_password(struct smtp_client_connection *conn)
+{
+ if (conn->set.remember_password)
+ return;
+ if (conn->password == NULL)
+ return;
+ safe_memset(conn->password, 0, strlen(conn->password));
+ conn->set.password = NULL;
+ conn->password = NULL;
+}
+
+static void
+smtp_client_connection_auth_deinit(struct smtp_client_connection *conn)
+{
+ dsasl_client_free(&conn->sasl_client);
+ i_free(conn->sasl_ir);
+}
+
+static void
+smtp_client_connection_auth_cb(const struct smtp_reply *reply,
+ struct smtp_client_connection *conn)
+{
+ struct smtp_client_command *cmd, *cmd_auth = conn->cmd_auth;
+ const char *error;
+
+ conn->cmd_auth = NULL;
+ i_assert(cmd_auth != NULL);
+
+ if (reply->status == 334) {
+ const unsigned char *sasl_output;
+ size_t sasl_output_len, input_len;
+ buffer_t *buf;
+
+ if (reply->text_lines[1] != NULL) {
+ error = t_strdup_printf(
+ "Authentication failed: "
+ "Server returned multi-line reply: %s",
+ smtp_reply_log(reply));
+ smtp_client_connection_fail(
+ conn, SMTP_CLIENT_COMMAND_ERROR_AUTH_FAILED,
+ error, "Authentication protocol error");
+ return;
+ }
+ if (conn->sasl_ir != NULL) {
+ if (*reply->text_lines[0] == '\0') {
+ /* Send initial response */
+ cmd = smtp_client_command_new(
+ conn, SMTP_CLIENT_COMMAND_FLAG_PRELOGIN,
+ smtp_client_connection_auth_cb, conn);
+ smtp_client_command_write(cmd, conn->sasl_ir);
+ smtp_client_command_submit_after(cmd, cmd_auth);
+ conn->cmd_auth = cmd;
+ i_free(conn->sasl_ir);
+ return;
+ }
+ error = t_strdup_printf(
+ "Authentication failed: "
+ "Server sent unexpected server-first challenge: %s",
+ smtp_reply_log(reply));
+ smtp_client_connection_fail(
+ conn, SMTP_CLIENT_COMMAND_ERROR_AUTH_FAILED,
+ error, "Authentication protocol error");
+ return;
+ }
+
+ input_len = strlen(reply->text_lines[0]);
+ buf = buffer_create_dynamic(pool_datastack_create(),
+ MAX_BASE64_DECODED_SIZE(input_len));
+ if (base64_decode(reply->text_lines[0], input_len,
+ NULL, buf) < 0) {
+ error = t_strdup_printf(
+ "Authentication failed: "
+ "Server sent non-base64 input for AUTH: %s",
+ reply->text_lines[0]);
+ } else if (dsasl_client_input(conn->sasl_client,
+ buf->data, buf->used,
+ &error) < 0) {
+ error = t_strdup_printf("Authentication failed: %s",
+ error);
+ } else if (dsasl_client_output(conn->sasl_client, &sasl_output,
+ &sasl_output_len, &error) < 0) {
+ error = t_strdup_printf("Authentication failed: %s",
+ error);
+ } else {
+ string_t *smtp_output = t_str_new(
+ MAX_BASE64_ENCODED_SIZE(sasl_output_len) + 2);
+ base64_encode(sasl_output, sasl_output_len,
+ smtp_output);
+ cmd = smtp_client_command_new(
+ conn, SMTP_CLIENT_COMMAND_FLAG_PRELOGIN,
+ smtp_client_connection_auth_cb, conn);
+ smtp_client_command_write(cmd, conn->sasl_ir);
+ smtp_client_command_submit_after(cmd, cmd_auth);
+ conn->cmd_auth = cmd;
+ return;
+ }
+
+ smtp_client_connection_fail(
+ conn, SMTP_CLIENT_COMMAND_ERROR_AUTH_FAILED,
+ error, "Authentication failed");
+ return;
+ }
+
+ if ((reply->status / 100) != 2) {
+ e_error(conn->event, "Authentication failed: %s",
+ smtp_reply_log(reply));
+ smtp_client_connection_fail_reply(conn, reply);
+ return;
+ }
+
+ smtp_client_connection_clear_password(conn);
+ smtp_client_connection_auth_deinit(conn);
+
+ e_debug(conn->event, "Authenticated successfully");
+
+ if (conn->to_connect != NULL)
+ timeout_reset(conn->to_connect);
+ conn->authenticated = TRUE;
+ smtp_client_connection_handshake(conn);
+}
+
+static int
+smtp_client_connection_get_sasl_mech(struct smtp_client_connection *conn,
+ const struct dsasl_client_mech **mech_r,
+ const char **error_r)
+{
+ const struct smtp_client_settings *set = &conn->set;
+ const char *const *mechanisms;
+
+ if (set->sasl_mech != NULL) {
+ const char *mech = dsasl_client_mech_get_name(set->sasl_mech);
+
+ if (!str_array_icase_find(conn->caps.auth_mechanisms, mech)) {
+ *error_r = t_strdup_printf(
+ "Server doesn't support `%s' SASL mechanism",
+ mech);
+ return -1;
+ }
+ *mech_r = set->sasl_mech;
+ return 0;
+ }
+ if (set->sasl_mechanisms == NULL ||
+ set->sasl_mechanisms[0] == '\0') {
+ *mech_r = &dsasl_client_mech_plain;
+ return 0;
+ }
+
+ /* find one of the specified SASL mechanisms */
+ mechanisms = t_strsplit_spaces(set->sasl_mechanisms, ", ");
+ for (; *mechanisms != NULL; mechanisms++) {
+ if (str_array_icase_find(conn->caps.auth_mechanisms,
+ *mechanisms)) {
+ *mech_r = dsasl_client_mech_find(*mechanisms);
+ if (*mech_r != NULL)
+ return 0;
+
+ *error_r = t_strdup_printf(
+ "Support for SASL mechanism `%s' is missing",
+ *mechanisms);
+ return -1;
+ }
+ }
+ *error_r = t_strdup_printf(
+ "Server doesn't support any of "
+ "the requested SASL mechanisms: %s", set->sasl_mechanisms);
+ return -1;
+}
+
+static bool
+smtp_client_connection_authenticate(struct smtp_client_connection *conn)
+{
+ const struct smtp_client_settings *set = &conn->set;
+ struct dsasl_client_settings sasl_set;
+ const struct dsasl_client_mech *sasl_mech = NULL;
+ struct smtp_client_command *cmd;
+ const unsigned char *sasl_output;
+ size_t sasl_output_len;
+ string_t *sasl_output_base64;
+ const char *error;
+
+ if (set->username == NULL && set->sasl_mech == NULL) {
+ if (!conn->set.xclient_defer)
+ smtp_client_connection_send_xclient(conn);
+ return (conn->xclient_replies_expected == 0);
+ }
+
+ smtp_client_connection_send_xclient(conn);
+ if (conn->xclient_replies_expected > 0)
+ return FALSE;
+ if (conn->authenticated)
+ return TRUE;
+
+ if ((conn->caps.standard & SMTP_CAPABILITY_AUTH) == 0) {
+ smtp_client_connection_fail(
+ conn, SMTP_CLIENT_COMMAND_ERROR_AUTH_FAILED,
+ NULL, "Authentication not supported");
+ return FALSE;
+ }
+
+ if (set->master_user != NULL) {
+ e_debug(conn->event, "Authenticating as %s for user %s",
+ set->master_user, set->username);
+ } else if (set->username == NULL) {
+ e_debug(conn->event, "Authenticating");
+ } else {
+ e_debug(conn->event, "Authenticating as %s", set->username);
+ }
+
+ if (smtp_client_connection_get_sasl_mech(conn, &sasl_mech,
+ &error) < 0) {
+ error = t_strdup_printf("Authentication failed: %s", error);
+ smtp_client_connection_fail(
+ conn, SMTP_CLIENT_COMMAND_ERROR_AUTH_FAILED,
+ error, "Server authentication mechanisms incompatible");
+ return FALSE;
+ }
+
+ i_zero(&sasl_set);
+ if (set->master_user == NULL)
+ sasl_set.authid = set->username;
+ else {
+ sasl_set.authid = set->master_user;
+ sasl_set.authzid = set->username;
+ }
+ sasl_set.password = set->password;
+
+ conn->sasl_client = dsasl_client_new(sasl_mech, &sasl_set);
+
+ if (dsasl_client_output(conn->sasl_client, &sasl_output,
+ &sasl_output_len, &error) < 0) {
+ error = t_strdup_printf(
+ "Failed to create initial %s SASL reply: %s",
+ dsasl_client_mech_get_name(sasl_mech), error);
+ smtp_client_connection_fail(
+ conn, SMTP_CLIENT_COMMAND_ERROR_AUTH_FAILED,
+ error, "Internal authentication failure");
+ return FALSE;
+ }
+
+ sasl_output_base64 = t_str_new(
+ MAX_BASE64_ENCODED_SIZE(sasl_output_len));
+ base64_encode(sasl_output, sasl_output_len, sasl_output_base64);
+
+ /* RFC 4954, Section 4:
+
+ Note that the AUTH command is still subject to the line length
+ limitations defined in [SMTP]. If use of the initial response
+ argument would cause the AUTH command to exceed this length, the
+ client MUST NOT use the initial response parameter (and instead
+ proceed as defined in Section 5.1 of [SASL]).
+
+ If the client is transmitting an initial response of zero length, it
+ MUST instead transmit the response as a single equals sign ("=").
+ This indicates that the response is present, but contains no data.
+ */
+
+ const char *init_resp = "";
+ const char *mech_name = dsasl_client_mech_get_name(sasl_mech);
+
+ i_assert(conn->sasl_ir == NULL);
+ if (str_len(sasl_output_base64) == 0)
+ init_resp = "=";
+ else if ((5 + strlen(mech_name) + 1 + str_len(sasl_output_base64)) >
+ SMTP_BASE_LINE_LENGTH_LIMIT)
+ conn->sasl_ir = i_strdup(str_c(sasl_output_base64));
+ else
+ init_resp = str_c(sasl_output_base64);
+
+ cmd = smtp_client_command_new(conn, SMTP_CLIENT_COMMAND_FLAG_PRELOGIN,
+ smtp_client_connection_auth_cb, conn);
+ if (*init_resp == '\0')
+ smtp_client_command_printf(cmd, "AUTH %s", mech_name);
+ else {
+ smtp_client_command_printf(cmd, "AUTH %s %s",
+ mech_name, init_resp);
+ }
+ smtp_client_command_submit(cmd);
+ conn->cmd_auth = cmd;
+
+ smtp_client_connection_set_state(
+ conn, SMTP_CLIENT_CONNECTION_STATE_AUTHENTICATING);
+ return FALSE;
+}
+
+static void
+smtp_client_connection_starttls_cb(const struct smtp_reply *reply,
+ struct smtp_client_connection *conn)
+{
+ const char *error;
+
+ e_debug(conn->event, "Received STARTTLS reply: %s",
+ smtp_reply_log(reply));
+
+ if ((reply->status / 100) != 2) {
+ smtp_client_connection_fail_reply(conn, reply);
+ return;
+ }
+
+ if (smtp_client_connection_ssl_init(conn, &error) < 0) {
+ smtp_client_connection_fail(
+ conn, SMTP_CLIENT_COMMAND_ERROR_CONNECT_FAILED,
+ error, SMTP_CLIENT_ERROR_TEXT_CONNECT_FAILED);
+ } else {
+ if (conn->to_connect != NULL)
+ timeout_reset(conn->to_connect);
+ smtp_client_connection_handshake(conn);
+ }
+}
+
+static bool smtp_client_connection_starttls(struct smtp_client_connection *conn)
+{
+ struct smtp_client_command *cmd;
+
+ if (conn->ssl_mode == SMTP_CLIENT_SSL_MODE_STARTTLS &&
+ conn->ssl_iostream == NULL) {
+ if ((conn->caps.standard & SMTP_CAPABILITY_STARTTLS) == 0) {
+ smtp_client_connection_fail(
+ conn, SMTP_CLIENT_COMMAND_ERROR_CONNECT_FAILED,
+ "Requested STARTTLS, "
+ "but server doesn't support it",
+ "STARTTLS not supported");
+ return FALSE;
+ }
+
+ e_debug(conn->event, "Starting TLS");
+
+ cmd = smtp_client_command_new(
+ conn, SMTP_CLIENT_COMMAND_FLAG_PRELOGIN,
+ smtp_client_connection_starttls_cb, conn);
+ smtp_client_command_write(cmd, "STARTTLS");
+ smtp_client_command_submit(cmd);
+ return FALSE;
+ }
+
+ return smtp_client_connection_authenticate(conn);
+}
+
+static void
+smtp_client_connection_record_param_extensions(
+ struct smtp_client_connection *conn, ARRAY_TYPE(const_string) *arr,
+ const char *const *extensions)
+{
+ pool_t pool = conn->cap_pool;
+
+ if (extensions == NULL || *extensions == NULL)
+ return;
+
+ if (!array_is_created(arr))
+ p_array_init(arr, pool, 4);
+ else {
+ const char *const *end;
+
+ /* Drop end marker */
+ i_assert(array_count(arr) > 0);
+ end = array_back(arr);
+ i_assert(*end == NULL);
+ array_pop_back(arr);
+ }
+
+ const char *const *new_p;
+ for (new_p = extensions; *new_p != NULL; new_p++) {
+ /* Drop duplicates */
+ if (array_lsearch(arr, new_p, i_strcasecmp_p) != NULL)
+ continue;
+
+ array_push_back(arr, new_p);
+ }
+
+ /* Add new end marker */
+ array_append_zero(arr);
+}
+
+static void
+smtp_client_connection_record_extra_capability(
+ struct smtp_client_connection *conn, const char *cap_name,
+ const char *const *params)
+{
+ const struct smtp_client_capability_extra *ccap_extra;
+ struct smtp_capability_extra cap_extra;
+ pool_t pool = conn->cap_pool;
+
+ ccap_extra = smtp_client_connection_find_extra_capability(
+ conn, cap_name);
+ if (ccap_extra == NULL)
+ return;
+ if (smtp_client_connection_get_extra_capability(conn, cap_name) != NULL)
+ return;
+
+ if (!array_is_created(&conn->caps.extra))
+ p_array_init(&conn->caps.extra, pool, 4);
+
+ i_zero(&cap_extra);
+ cap_extra.name = p_strdup(pool, ccap_extra->name);
+ cap_extra.params = p_strarray_dup(pool, params);
+
+ array_push_back(&conn->caps.extra, &cap_extra);
+
+ smtp_client_connection_record_param_extensions(
+ conn, &conn->caps.mail_param_extensions,
+ ccap_extra->mail_param_extensions);
+ smtp_client_connection_record_param_extensions(
+ conn, &conn->caps.rcpt_param_extensions,
+ ccap_extra->rcpt_param_extensions);
+}
+
+static void
+smtp_client_connection_handshake_cb(const struct smtp_reply *reply,
+ struct smtp_client_connection *conn)
+{
+ const char *const *lines;
+
+ e_debug(conn->event, "Received handshake reply");
+
+ /* check reply status */
+ if ((reply->status / 100) != 2) {
+ /* RFC 5321, Section 3.2:
+ For a particular connection attempt, if the server returns a
+ "command not recognized" response to EHLO, the client SHOULD
+ be able to fall back and send HELO. */
+ if (conn->protocol == SMTP_PROTOCOL_SMTP && !conn->old_smtp &&
+ (reply->status == 500 || reply->status == 502)) {
+ /* try HELO */
+ conn->old_smtp = TRUE;
+ smtp_client_connection_handshake(conn);
+ return;
+ }
+ /* failed */
+ smtp_client_connection_fail_reply(conn, reply);
+ return;
+ }
+
+ /* reset capabilities */
+ p_clear(conn->cap_pool);
+ i_zero(&conn->caps);
+ conn->caps.standard = conn->set.forced_capabilities;
+
+ lines = reply->text_lines;
+ if (*lines == NULL) {
+ smtp_client_connection_fail_equal(
+ conn, SMTP_CLIENT_COMMAND_ERROR_BAD_REPLY,
+ "Invalid handshake reply");
+ return;
+ }
+
+ /* greeting line */
+ lines++;
+
+ /* capability lines */
+ while (*lines != NULL) {
+ enum smtp_capability cap;
+ const char *const *params;
+ const char *cap_name, *error;
+
+ if (smtp_ehlo_line_parse(*lines, &cap_name, &params,
+ &error) <= 0) {
+ e_warning(conn->event,
+ "Received invalid EHLO response line: %s",
+ error);
+ lines++;
+ continue;
+ }
+
+ cap = smtp_capability_find_by_name(cap_name);
+ switch (cap) {
+ case SMTP_CAPABILITY_AUTH:
+ conn->caps.auth_mechanisms =
+ p_strarray_dup(conn->cap_pool, params);
+ break;
+ case SMTP_CAPABILITY_SIZE:
+ if (params == NULL || *params == NULL)
+ break;
+ if (str_to_uoff(*params, &conn->caps.size) < 0) {
+ e_warning(conn->event,
+ "Received invalid SIZE capability "
+ "in EHLO response line");
+ cap = SMTP_CAPABILITY_NONE;
+ }
+ break;
+ case SMTP_CAPABILITY_XCLIENT:
+ conn->caps.xclient_args =
+ p_strarray_dup(conn->cap_pool, params);
+ break;
+ case SMTP_CAPABILITY_NONE:
+ smtp_client_connection_record_extra_capability(
+ conn, cap_name, params);
+ break;
+ default:
+ break;
+ }
+
+ conn->caps.standard |= cap;
+ lines++;
+ }
+
+ e_debug(conn->event, "Received server capabilities");
+
+ if (conn->to_connect != NULL)
+ timeout_reset(conn->to_connect);
+ if (smtp_client_connection_starttls(conn)) {
+ stmp_client_connection_ready(conn, reply);
+ }
+}
+
+static void
+smtp_client_connection_handshake(struct smtp_client_connection *conn)
+{
+ struct smtp_client_command *cmd;
+ enum smtp_client_command_flags flags;
+ const char *command;
+
+ flags = SMTP_CLIENT_COMMAND_FLAG_PRELOGIN |
+ SMTP_CLIENT_COMMAND_FLAG_PRIORITY;
+
+ switch (conn->protocol) {
+ case SMTP_PROTOCOL_SMTP:
+ command = (conn->old_smtp ? "HELO" : "EHLO");
+ break;
+ case SMTP_PROTOCOL_LMTP:
+ command = "LHLO";
+ break;
+ default:
+ i_unreached();
+ }
+
+ e_debug(conn->event, "Sending %s handshake", command);
+
+ cmd = smtp_client_command_new(
+ conn, flags, smtp_client_connection_handshake_cb, conn);
+ smtp_client_command_write(cmd, command);
+ smtp_client_command_write(cmd, " ");
+ smtp_client_command_write(cmd, conn->set.my_hostname);
+ smtp_client_command_submit(cmd);
+ smtp_client_connection_set_state(
+ conn, SMTP_CLIENT_CONNECTION_STATE_HANDSHAKING);
+}
+
+static int
+smtp_client_connection_input_reply(struct smtp_client_connection *conn,
+ const struct smtp_reply *reply)
+{
+ int ret;
+
+ /* initial greeting? */
+ if (conn->state == SMTP_CLIENT_CONNECTION_STATE_CONNECTING) {
+ e_debug(conn->event, "Received greeting from server: %s",
+ smtp_reply_log(reply));
+ if (reply->status != 220) {
+ if (smtp_reply_is_success(reply)) {
+ smtp_client_connection_fail_equal(
+ conn,
+ SMTP_CLIENT_COMMAND_ERROR_BAD_REPLY,
+ "Received inappropriate greeting");
+ } else {
+ smtp_client_connection_fail_reply(conn, reply);
+ }
+ return -1;
+ }
+ smtp_client_connection_handshake(conn);
+ return 1;
+ }
+
+ if (reply->status == SMTP_CLIENT_COMMAND_ERROR_CONNECTION_CLOSED) {
+ smtp_client_connection_fail_reply(conn, reply);
+ return -1;
+ }
+
+ /* unexpected reply? */
+ if (conn->cmd_wait_list_head == NULL) {
+ smtp_client_connection_fail(
+ conn, SMTP_CLIENT_COMMAND_ERROR_BAD_REPLY,
+ t_strdup_printf("Unexpected reply: %s",
+ smtp_reply_log(reply)),
+ "Got unexpected reply");
+ return -1;
+ }
+
+ /* replied early? */
+ if (conn->cmd_wait_list_head == conn->cmd_streaming &&
+ !conn->cmd_wait_list_head->stream_finished) {
+ if (!smtp_reply_is_success(reply)) {
+ e_debug(conn->event, "Early reply: %s",
+ smtp_reply_log(reply));
+ } else {
+ smtp_client_connection_fail(
+ conn, SMTP_CLIENT_COMMAND_ERROR_BAD_REPLY,
+ t_strdup_printf(
+ "Got early success reply: %s",
+ smtp_reply_log(reply)),
+ "Got early success reply");
+ return -1;
+ }
+ }
+
+ /* command reply */
+ ret = smtp_client_command_input_reply(conn->cmd_wait_list_head, reply);
+
+ if (conn->state == SMTP_CLIENT_CONNECTION_STATE_DISCONNECTED ||
+ conn->conn.output == NULL)
+ return -1;
+ return ret;
+}
+
+static void smtp_client_connection_input(struct connection *_conn)
+{
+ struct smtp_client_connection *conn =
+ (struct smtp_client_connection *)_conn;
+ bool enhanced_codes = ((conn->caps.standard &
+ SMTP_CAPABILITY_ENHANCEDSTATUSCODES) != 0);
+ struct smtp_reply *reply;
+ const char *error = NULL;
+ int ret;
+
+ if (conn->ssl_iostream != NULL &&
+ !ssl_iostream_is_handshaked(conn->ssl_iostream)) {
+ /* finish SSL negotiation by reading from input stream */
+ while ((ret = i_stream_read(conn->conn.input)) > 0 ||
+ ret == -2) {
+ if (ssl_iostream_is_handshaked(conn->ssl_iostream))
+ break;
+ }
+ if (ret < 0) {
+ /* failed somehow */
+ i_assert(ret != -2);
+ error = t_strdup_printf(
+ "SSL handshaking with %s failed: "
+ "read(%s) failed: %s", _conn->name,
+ i_stream_get_name(conn->conn.input),
+ i_stream_get_error(conn->conn.input));
+ smtp_client_connection_fail(
+ conn, SMTP_CLIENT_COMMAND_ERROR_CONNECT_FAILED,
+ error, SMTP_CLIENT_ERROR_TEXT_CONNECT_FAILED);
+ return;
+ }
+
+ if (!ssl_iostream_is_handshaked(conn->ssl_iostream)) {
+ /* not finished */
+ i_assert(ret == 0);
+ return;
+ }
+
+ if (conn->to_connect != NULL)
+ timeout_reset(conn->to_connect);
+ }
+
+ if (!conn->connect_succeeded) {
+ /* just got ready for SMTP handshake */
+ smtp_client_connection_established(conn);
+ }
+
+ smtp_client_connection_ref(conn);
+ o_stream_cork(conn->conn.output);
+ for (;;) {
+ if (conn->cmd_wait_list_head != NULL &&
+ conn->cmd_wait_list_head->ehlo) {
+ if ((ret = smtp_reply_parse_ehlo(conn->reply_parser,
+ &reply, &error)) <= 0)
+ break;
+ } else {
+ if ((ret = smtp_reply_parse_next(conn->reply_parser,
+ enhanced_codes,
+ &reply, &error)) <= 0)
+ break;
+ }
+
+ T_BEGIN {
+ ret = smtp_client_connection_input_reply(conn, reply);
+ } T_END;
+ if (ret < 0) {
+ if (conn->conn.output != NULL && !conn->corked)
+ o_stream_uncork(conn->conn.output);
+ smtp_client_connection_unref(&conn);
+ return;
+ }
+ }
+
+ if (ret < 0 || (ret == 0 && conn->conn.input->eof)) {
+ if (conn->conn.input->stream_errno == ENOBUFS) {
+ smtp_client_connection_fail_equal(
+ conn, SMTP_CLIENT_COMMAND_ERROR_BAD_REPLY,
+ "Command reply line too long");
+ } else if (conn->conn.input->stream_errno != 0) {
+ smtp_client_connection_lost(
+ conn,
+ t_strdup_printf(
+ "read(%s) failed: %s",
+ i_stream_get_name(conn->conn.input),
+ i_stream_get_error(conn->conn.input)),
+ "Read failure");
+ } else if (!i_stream_have_bytes_left(conn->conn.input)) {
+ if (conn->sent_quit) {
+ smtp_client_connection_lost(
+ conn, NULL,
+ "Remote closed connection");
+ } else {
+ smtp_client_connection_lost(
+ conn, NULL,
+ "Remote closed connection unexpectedly");
+ }
+ } else {
+ i_assert(error != NULL);
+ error = t_strdup_printf("Invalid command reply: %s",
+ error);
+ smtp_client_connection_fail_equal(
+ conn, SMTP_CLIENT_COMMAND_ERROR_BAD_REPLY,
+ error);
+ }
+ }
+ if (ret >= 0 && conn->conn.output != NULL && !conn->corked) {
+ if (o_stream_uncork_flush(conn->conn.output) < 0)
+ smtp_client_connection_handle_output_error(conn);
+ }
+ smtp_client_connection_unref(&conn);
+}
+
+static int smtp_client_connection_output(struct smtp_client_connection *conn)
+{
+ int ret;
+
+ if (conn->to_connect != NULL)
+ timeout_reset(conn->to_connect);
+
+ ret = o_stream_flush(conn->conn.output);
+ if (ret <= 0) {
+ if (ret < 0)
+ smtp_client_connection_handle_output_error(conn);
+ return ret;
+ }
+
+ smtp_client_connection_ref(conn);
+ o_stream_cork(conn->conn.output);
+ if (smtp_client_command_send_more(conn) < 0)
+ ret = -1;
+ if (ret >= 0 && conn->conn.output != NULL && !conn->corked) {
+ if (o_stream_uncork_flush(conn->conn.output) < 0)
+ smtp_client_connection_handle_output_error(conn);
+ }
+ smtp_client_connection_unref(&conn);
+ return ret;
+}
+
+void smtp_client_connection_trigger_output(struct smtp_client_connection *conn)
+{
+ if (conn->conn.output != NULL)
+ o_stream_set_flush_pending(conn->conn.output, TRUE);
+}
+
+static void smtp_client_connection_destroy(struct connection *_conn)
+{
+ struct smtp_client_connection *conn =
+ (struct smtp_client_connection *)_conn;
+ const char *error;
+
+ switch (_conn->disconnect_reason) {
+ case CONNECTION_DISCONNECT_NOT:
+ break;
+ case CONNECTION_DISCONNECT_DEINIT:
+ e_debug(conn->event, "Connection deinit");
+ smtp_client_connection_close(&conn);
+ break;
+ case CONNECTION_DISCONNECT_CONNECT_TIMEOUT:
+ error = t_strdup_printf(
+ "connect(%s) failed: Connection timed out",
+ _conn->name);
+ smtp_client_connection_fail(
+ conn, SMTP_CLIENT_COMMAND_ERROR_CONNECT_FAILED,
+ error, "Connect timed out");
+ break;
+ default:
+ case CONNECTION_DISCONNECT_CONN_CLOSED:
+ if (conn->connect_failed) {
+ smtp_client_connection_fail(conn,
+ SMTP_CLIENT_COMMAND_ERROR_CONNECT_FAILED,
+ NULL, SMTP_CLIENT_ERROR_TEXT_CONNECT_FAILED);
+ break;
+ }
+ if (_conn->input != NULL && _conn->input->stream_errno != 0) {
+ smtp_client_connection_lost(
+ conn,
+ t_strdup_printf("read(%s) failed: %s",
+ i_stream_get_name(conn->conn.input),
+ i_stream_get_error(conn->conn.input)),
+ "Read failure");
+ break;
+ }
+ smtp_client_connection_lost(
+ conn, "Remote disconnected",
+ "Remote closed connection unexpectedly");
+ break;
+ }
+}
+
+static void
+smtp_client_connection_established(struct smtp_client_connection *conn)
+{
+ i_assert(!conn->connect_succeeded);
+ conn->connect_succeeded = TRUE;
+
+ if (conn->to_connect != NULL)
+ timeout_reset(conn->to_connect);
+
+ /* set flush callback */
+ o_stream_set_flush_callback(conn->conn.output,
+ smtp_client_connection_output, conn);
+}
+
+static int
+smtp_client_connection_ssl_handshaked(const char **error_r, void *context)
+{
+ struct smtp_client_connection *conn = context;
+ const char *error, *host = conn->host;
+
+ if (ssl_iostream_check_cert_validity(conn->ssl_iostream,
+ host, &error) == 0) {
+ e_debug(conn->event, "SSL handshake successful");
+ } else if (conn->set.ssl->allow_invalid_cert) {
+ e_debug(conn->event, "SSL handshake successful, "
+ "ignoring invalid certificate: %s", error);
+ } else {
+ *error_r = error;
+ return -1;
+ }
+ return 0;
+}
+
+static void
+smtp_client_connection_streams_changed(struct smtp_client_connection *conn)
+{
+ struct stat st;
+
+ if (conn->set.rawlog_dir != NULL &&
+ stat(conn->set.rawlog_dir, &st) == 0) {
+ iostream_rawlog_create(conn->set.rawlog_dir,
+ &conn->conn.input, &conn->conn.output);
+ }
+
+ if (conn->reply_parser == NULL) {
+ conn->reply_parser = smtp_reply_parser_init(
+ conn->conn.input, conn->set.max_reply_size);
+ } else {
+ smtp_reply_parser_set_stream(conn->reply_parser,
+ conn->conn.input);
+ }
+
+ connection_streams_changed(&conn->conn);
+}
+
+static int
+smtp_client_connection_init_ssl_ctx(struct smtp_client_connection *conn,
+ const char **error_r)
+{
+ struct smtp_client *client = conn->client;
+ const char *error;
+
+ if (conn->ssl_ctx != NULL)
+ return 0;
+
+ if (conn->set.ssl == client->set.ssl) {
+ if (smtp_client_init_ssl_ctx(client, error_r) < 0)
+ return -1;
+ conn->ssl_ctx = client->ssl_ctx;
+ ssl_iostream_context_ref(conn->ssl_ctx);
+ return 0;
+ }
+
+ if (conn->set.ssl == NULL) {
+ *error_r =
+ "Requested SSL connection, but no SSL settings given";
+ return -1;
+ }
+ if (ssl_iostream_client_context_cache_get(conn->set.ssl, &conn->ssl_ctx,
+ &error) < 0) {
+ *error_r = t_strdup_printf(
+ "Couldn't initialize SSL context: %s", error);
+ return -1;
+ }
+ return 0;
+}
+
+static int
+smtp_client_connection_ssl_init(struct smtp_client_connection *conn,
+ const char **error_r)
+{
+ const char *error;
+
+ if (smtp_client_connection_init_ssl_ctx(conn, &error) < 0) {
+ *error_r = t_strdup_printf(
+ "Failed to initialize SSL: %s", error);
+ return -1;
+ }
+
+ e_debug(conn->event, "Starting SSL handshake");
+
+ if (conn->raw_input != conn->conn.input) {
+ /* recreate rawlog after STARTTLS */
+ i_stream_ref(conn->raw_input);
+ o_stream_ref(conn->raw_output);
+ i_stream_destroy(&conn->conn.input);
+ o_stream_destroy(&conn->conn.output);
+ conn->conn.input = conn->raw_input;
+ conn->conn.output = conn->raw_output;
+ }
+
+ connection_input_halt(&conn->conn);
+ if (io_stream_create_ssl_client(
+ conn->ssl_ctx, conn->host, conn->set.ssl,
+ &conn->conn.input, &conn->conn.output,
+ &conn->ssl_iostream, &error) < 0) {
+ *error_r = t_strdup_printf(
+ "Couldn't initialize SSL client for %s: %s",
+ conn->conn.name, error);
+ return -1;
+ }
+ connection_input_resume(&conn->conn);
+ smtp_client_connection_streams_changed(conn);
+
+ ssl_iostream_set_handshake_callback(
+ conn->ssl_iostream, smtp_client_connection_ssl_handshaked,
+ conn);
+ if (ssl_iostream_handshake(conn->ssl_iostream) < 0) {
+ *error_r = t_strdup_printf(
+ "SSL handshake to %s failed: %s", conn->conn.name,
+ ssl_iostream_get_last_error(conn->ssl_iostream));
+ return -1;
+ }
+
+ if (ssl_iostream_is_handshaked(conn->ssl_iostream) &&
+ !conn->connect_succeeded) {
+ smtp_client_connection_established(conn);
+ } else {
+ /* wait for handshake to complete; connection input handler
+ does the rest by reading from the input stream */
+ o_stream_set_flush_callback(
+ conn->conn.output, smtp_client_connection_output, conn);
+ }
+ return 0;
+}
+
+static void
+smtp_client_connection_connected(struct connection *_conn, bool success)
+{
+ struct smtp_client_connection *conn =
+ (struct smtp_client_connection *)_conn;
+ const struct smtp_client_settings *set = &conn->set;
+ const char *error;
+
+ if (!success) {
+ e_error(conn->event, "connect(%s) failed: %m", _conn->name);
+ conn->connect_failed = TRUE;
+ return;
+ }
+
+ if (conn->set.debug) {
+ struct ip_addr local_ip;
+ in_port_t local_port;
+ int ret;
+
+ ret = net_getsockname(_conn->fd_in, &local_ip, &local_port);
+ i_assert(ret == 0);
+ e_debug(conn->event, "Connected to server (from %s:%u)",
+ net_ip2addr(&local_ip), local_port);
+ }
+
+ (void)net_set_tcp_nodelay(_conn->fd_out, TRUE);
+ if (set->socket_send_buffer_size > 0 &&
+ net_set_send_buffer_size(_conn->fd_out,
+ set->socket_send_buffer_size) < 0) {
+ e_error(conn->event,
+ "net_set_send_buffer_size(%zu) failed: %m",
+ set->socket_send_buffer_size);
+ }
+ if (set->socket_recv_buffer_size > 0 &&
+ net_set_recv_buffer_size(_conn->fd_in,
+ set->socket_recv_buffer_size) < 0) {
+ e_error(conn->event,
+ "net_set_recv_buffer_size(%zu) failed: %m",
+ set->socket_recv_buffer_size);
+ }
+
+ conn->raw_input = conn->conn.input;
+ conn->raw_output = conn->conn.output;
+ smtp_client_connection_streams_changed(conn);
+
+ if (conn->ssl_mode == SMTP_CLIENT_SSL_MODE_IMMEDIATE) {
+ if (smtp_client_connection_ssl_init(conn, &error) < 0) {
+ error = t_strdup_printf("connect(%s) failed: %s",
+ _conn->name, error);
+ smtp_client_connection_fail(
+ conn, SMTP_CLIENT_COMMAND_ERROR_CONNECT_FAILED,
+ error, SMTP_CLIENT_ERROR_TEXT_CONNECT_FAILED);
+ }
+ } else {
+ smtp_client_connection_established(conn);
+ smtp_client_connection_input(_conn);
+ }
+}
+
+static void
+smtp_client_connection_connect_timeout(struct smtp_client_connection *conn)
+{
+ switch (conn->state) {
+ case SMTP_CLIENT_CONNECTION_STATE_CONNECTING:
+ smtp_client_connection_fail(
+ conn, SMTP_CLIENT_COMMAND_ERROR_CONNECT_FAILED,
+ t_strdup_printf(
+ "Connection timed out after %u seconds",
+ conn->set.connect_timeout_msecs/1000),
+ "Connect timed out");
+ break;
+ case SMTP_CLIENT_CONNECTION_STATE_HANDSHAKING:
+ smtp_client_connection_fail(
+ conn, SMTP_CLIENT_COMMAND_ERROR_CONNECT_FAILED,
+ t_strdup_printf(
+ "SMTP handshake timed out after %u seconds",
+ conn->set.connect_timeout_msecs/1000),
+ "Handshake timed out");
+ break;
+ case SMTP_CLIENT_CONNECTION_STATE_AUTHENTICATING:
+ smtp_client_connection_fail(
+ conn, SMTP_CLIENT_COMMAND_ERROR_AUTH_FAILED,
+ t_strdup_printf(
+ "Authentication timed out after %u seconds",
+ conn->set.connect_timeout_msecs/1000),
+ "Authentication timed out");
+ break;
+ default:
+ i_unreached();
+ }
+}
+
+static void
+smtp_client_connection_delayed_connect_error(
+ struct smtp_client_connection *conn)
+{
+ e_debug(conn->event, "Delayed connect error");
+
+ timeout_remove(&conn->to_connect);
+ errno = conn->connect_errno;
+ smtp_client_connection_connected(&conn->conn, FALSE);
+ smtp_client_connection_fail(
+ conn, SMTP_CLIENT_COMMAND_ERROR_CONNECT_FAILED,
+ NULL, SMTP_CLIENT_ERROR_TEXT_CONNECT_FAILED);
+}
+
+static void
+smtp_client_connection_do_connect(struct smtp_client_connection *conn)
+{
+ unsigned int msecs;
+
+ if (conn->closed || conn->failing)
+ return;
+
+ /* Clear state data */
+ i_zero(&conn->state_data);
+ p_clear(conn->state_pool);
+
+ if (connection_client_connect(&conn->conn) < 0) {
+ conn->connect_errno = errno;
+ e_debug(conn->event, "Connect failed: %m");
+ conn->to_connect = timeout_add_short(
+ 0, smtp_client_connection_delayed_connect_error, conn);
+ return;
+ }
+
+ /* don't use connection.h timeout because we want this timeout
+ to include also the SSL handshake */
+ msecs = conn->set.connect_timeout_msecs;
+ if (msecs == 0)
+ msecs = conn->set.command_timeout_msecs;
+ i_assert(conn->to_connect == NULL);
+ if (msecs > 0) {
+ conn->to_connect = timeout_add(
+ msecs, smtp_client_connection_connect_timeout, conn);
+ }
+}
+
+static bool smtp_client_connection_last_ip(struct smtp_client_connection *conn)
+{
+ i_assert(conn->prev_connect_idx < conn->ips_count);
+ return (conn->prev_connect_idx + 1) % conn->ips_count == 0;
+}
+
+static void
+smtp_client_connection_connect_next_ip(struct smtp_client_connection *conn)
+{
+ const struct ip_addr *ip, *my_ip = &conn->set.my_ip;
+
+ timeout_remove(&conn->to_connect);
+
+ conn->prev_connect_idx = (conn->prev_connect_idx+1) % conn->ips_count;
+ ip = &conn->ips[conn->prev_connect_idx];
+
+ if (my_ip->family != 0) {
+ e_debug(conn->event, "Connecting to %s:%u (from %s)",
+ net_ip2addr(ip), conn->port, net_ip2addr(my_ip));
+ } else {
+ e_debug(conn->event, "Connecting to %s:%u",
+ net_ip2addr(ip), conn->port);
+ }
+
+ connection_init_client_ip_from(conn->client->conn_list, &conn->conn,
+ (conn->host_is_ip ? NULL : conn->host),
+ ip, conn->port, my_ip);
+
+ smtp_client_connection_do_connect(conn);
+}
+
+static void
+smtp_client_connection_connect_unix(struct smtp_client_connection *conn)
+{
+ timeout_remove(&conn->to_connect);
+
+ e_debug(conn->event, "Connecting to socket %s", conn->path);
+
+ connection_init_client_unix(conn->client->conn_list, &conn->conn,
+ conn->path);
+
+ smtp_client_connection_do_connect(conn);
+}
+
+static void
+smtp_client_connection_delayed_host_lookup_failure(
+ struct smtp_client_connection *conn)
+{
+ e_debug(conn->event, "Delayed host lookup failure");
+
+ i_assert(conn->to_connect != NULL);
+ timeout_remove(&conn->to_connect);
+ smtp_client_connection_fail(
+ conn, SMTP_CLIENT_COMMAND_ERROR_HOST_LOOKUP_FAILED,
+ NULL, "Failed to lookup remote server");
+}
+
+static void
+smtp_client_connection_dns_callback(const struct dns_lookup_result *result,
+ struct smtp_client_connection *conn)
+{
+ conn->dns_lookup = NULL;
+
+ if (result->ret != 0) {
+ e_error(conn->event, "dns_lookup(%s) failed: %s",
+ conn->host, result->error);
+ timeout_remove(&conn->to_connect);
+ conn->to_connect = timeout_add_short(
+ 0, smtp_client_connection_delayed_host_lookup_failure,
+ conn);
+ return;
+ }
+
+ e_debug(conn->event, "DNS lookup successful; got %d IPs",
+ result->ips_count);
+
+ i_assert(result->ips_count > 0);
+ conn->ips_count = result->ips_count;
+ conn->ips = i_new(struct ip_addr, conn->ips_count);
+ memcpy(conn->ips, result->ips, sizeof(*conn->ips) * conn->ips_count);
+ conn->prev_connect_idx = conn->ips_count - 1;
+
+ smtp_client_connection_connect_next_ip(conn);
+}
+
+static void
+smtp_client_connection_lookup_ip(struct smtp_client_connection *conn)
+{
+ struct dns_lookup_settings dns_set;
+ struct ip_addr ip, *ips;
+ unsigned int ips_count;
+ int ret;
+
+ if (conn->ips_count != 0)
+ return;
+
+ e_debug(conn->event, "Looking up IP address");
+
+ if (net_addr2ip(conn->host, &ip) == 0) {
+ /* IP address */
+ conn->ips_count = 1;
+ conn->ips = i_new(struct ip_addr, conn->ips_count);
+ conn->ips[0] = ip;
+ conn->host_is_ip = TRUE;
+ } else if (conn->set.dns_client != NULL) {
+ e_debug(conn->event, "Performing asynchronous DNS lookup");
+ (void)dns_client_lookup(
+ conn->set.dns_client, conn->host,
+ smtp_client_connection_dns_callback, conn,
+ &conn->dns_lookup);
+ } else if (conn->set.dns_client_socket_path != NULL) {
+ i_zero(&dns_set);
+ dns_set.dns_client_socket_path =
+ conn->set.dns_client_socket_path;
+ dns_set.timeout_msecs = conn->set.connect_timeout_msecs;
+ dns_set.event_parent = conn->event;
+ e_debug(conn->event, "Performing asynchronous DNS lookup");
+ (void)dns_lookup(conn->host, &dns_set,
+ smtp_client_connection_dns_callback, conn,
+ &conn->dns_lookup);
+ } else {
+ /* no dns-conn, use blocking lookup */
+ ret = net_gethostbyname(conn->host, &ips, &ips_count);
+ if (ret != 0) {
+ e_error(conn->event, "net_gethostbyname(%s) failed: %s",
+ conn->host, net_gethosterror(ret));
+ timeout_remove(&conn->to_connect);
+ conn->to_connect = timeout_add_short(
+ 0,
+ smtp_client_connection_delayed_host_lookup_failure,
+ conn);
+ return;
+ }
+
+ e_debug(conn->event, "DNS lookup successful; got %d IPs",
+ ips_count);
+
+ conn->ips_count = ips_count;
+ conn->ips = i_new(struct ip_addr, ips_count);
+ memcpy(conn->ips, ips, ips_count * sizeof(*ips));
+ }
+}
+
+static void
+smtp_client_connection_already_connected(struct smtp_client_connection *conn)
+{
+ i_assert(conn->state_data.login_reply != NULL);
+
+ timeout_remove(&conn->to_connect);
+
+ e_debug(conn->event, "Already connected");
+
+ smtp_client_connection_login_callback(
+ conn, conn->state_data.login_reply);
+}
+
+static void
+smtp_client_connection_connect_more(struct smtp_client_connection *conn)
+{
+ if (!array_is_created(&conn->login_callbacks) ||
+ array_count(&conn->login_callbacks) == 0) {
+ /* No login callbacks required */
+ return;
+ }
+ if (conn->state < SMTP_CLIENT_CONNECTION_STATE_READY) {
+ /* Login callbacks will be called once the connection succeeds
+ or fails. */
+ return;
+ }
+
+ if (array_count(&conn->login_callbacks) > 1) {
+ /* Another login callback is already pending */
+ i_assert(conn->to_connect != NULL);
+ return;
+ }
+
+ /* Schedule immediate login callback */
+ i_assert(conn->to_connect == NULL);
+ conn->to_connect = timeout_add(
+ 0, smtp_client_connection_already_connected, conn);
+}
+
+void smtp_client_connection_connect(
+ struct smtp_client_connection *conn,
+ smtp_client_command_callback_t login_callback, void *login_context)
+{
+ struct smtp_client_login_callback *login_cb;
+
+ if (conn->closed)
+ return;
+
+ if (login_callback != NULL) {
+ if (!array_is_created(&conn->login_callbacks))
+ i_array_init(&conn->login_callbacks, 4);
+
+ login_cb = array_append_space(&conn->login_callbacks);
+ login_cb->callback = login_callback;
+ login_cb->context = login_context;
+ }
+
+ if (conn->state != SMTP_CLIENT_CONNECTION_STATE_DISCONNECTED) {
+ /* Already connecting or connected */
+ smtp_client_connection_connect_more(conn);
+ return;
+ }
+ if (conn->failing)
+ return;
+
+ e_debug(conn->event, "Disconnected");
+
+ conn->xclient_replies_expected = 0;
+ conn->authenticated = FALSE;
+ conn->xclient_sent = FALSE;
+ conn->connect_failed = FALSE;
+ conn->connect_succeeded = FALSE;
+ conn->handshake_failed = FALSE;
+ conn->sent_quit = FALSE;
+ conn->reset_needed = FALSE;
+
+ smtp_client_connection_set_state(
+ conn, SMTP_CLIENT_CONNECTION_STATE_CONNECTING);
+
+ if (conn->path == NULL) {
+ smtp_client_connection_lookup_ip(conn);
+ if (conn->ips_count == 0)
+ return;
+
+ /* always work asynchronously */
+ timeout_remove(&conn->to_connect);
+ conn->to_connect = timeout_add(
+ 0, smtp_client_connection_connect_next_ip, conn);
+ } else {
+ /* always work asynchronously */
+ timeout_remove(&conn->to_connect);
+ conn->to_connect = timeout_add(
+ 0, smtp_client_connection_connect_unix, conn);
+ }
+}
+
+static const struct connection_settings smtp_client_connection_set = {
+ .input_max_size = SIZE_MAX,
+ .output_max_size = SIZE_MAX,
+ .client = TRUE,
+ .delayed_unix_client_connected_callback = TRUE,
+ .log_connection_id = TRUE,
+};
+
+static const struct connection_vfuncs smtp_client_connection_vfuncs = {
+ .destroy = smtp_client_connection_destroy,
+ .input = smtp_client_connection_input,
+ .client_connected = smtp_client_connection_connected
+};
+
+struct connection_list *smtp_client_connection_list_init(void)
+{
+ return connection_list_init(&smtp_client_connection_set,
+ &smtp_client_connection_vfuncs);
+}
+
+void smtp_client_connection_disconnect(struct smtp_client_connection *conn)
+{
+ if (conn->state == SMTP_CLIENT_CONNECTION_STATE_DISCONNECTED)
+ return;
+
+ e_debug(conn->event, "Disconnected");
+
+ smtp_client_connection_clear_password(conn);
+
+ if (conn->conn.output != NULL && !conn->sent_quit &&
+ !conn->sending_command) {
+ /* Close the connection gracefully if possible */
+ o_stream_nsend_str(conn->conn.output, "QUIT\r\n");
+ o_stream_uncork(conn->conn.output);
+ }
+
+ if (conn->dns_lookup != NULL)
+ dns_lookup_abort(&conn->dns_lookup);
+ io_remove(&conn->io_cmd_payload);
+ timeout_remove(&conn->to_connect);
+ timeout_remove(&conn->to_trans);
+ timeout_remove(&conn->to_commands);
+ timeout_remove(&conn->to_cmd_fail);
+
+ ssl_iostream_destroy(&conn->ssl_iostream);
+ if (conn->ssl_ctx != NULL)
+ ssl_iostream_context_unref(&conn->ssl_ctx);
+ smtp_client_connection_auth_deinit(conn);
+
+ o_stream_destroy(&conn->dot_output);
+
+ connection_disconnect(&conn->conn);
+
+ smtp_client_connection_set_state(
+ conn, SMTP_CLIENT_CONNECTION_STATE_DISCONNECTED);
+
+ if (!conn->failing) {
+ smtp_client_connection_login_fail(
+ conn, SMTP_CLIENT_COMMAND_ERROR_ABORTED,
+ "Disconnected from server");
+ smtp_client_connection_transactions_fail(
+ conn, SMTP_CLIENT_COMMAND_ERROR_ABORTED,
+ "Disconnected from server");
+ smtp_client_connection_commands_fail(
+ conn, SMTP_CLIENT_COMMAND_ERROR_ABORTED,
+ "Disconnected from server");
+ }
+ smtp_client_command_unref(&conn->cmd_streaming);
+}
+
+static struct smtp_client_connection *
+smtp_client_connection_do_create(struct smtp_client *client, const char *name,
+ enum smtp_protocol protocol,
+ const struct smtp_client_settings *set)
+{
+ struct smtp_client_connection *conn;
+ struct event *conn_event;
+ pool_t pool;
+
+ pool = pool_alloconly_create("smtp client connection", 2048);
+ conn = p_new(pool, struct smtp_client_connection, 1);
+ conn->refcount = 1;
+ conn->pool = pool;
+
+ conn->client = client;
+ conn->protocol = protocol;
+
+ conn->set = client->set;
+ if (set != NULL) {
+ if (set->my_ip.family != 0)
+ conn->set.my_ip = set->my_ip;
+ if (set->my_hostname != NULL && *set->my_hostname != '\0') {
+ conn->set.my_hostname =
+ p_strdup(pool, set->my_hostname);
+ }
+
+ conn->set.forced_capabilities |= set->forced_capabilities;
+ if (set->extra_capabilities != NULL) {
+ conn->set.extra_capabilities =
+ p_strarray_dup(pool, set->extra_capabilities);
+ }
+
+ if (set->rawlog_dir != NULL && *set->rawlog_dir != '\0') {
+ conn->set.rawlog_dir =
+ p_strdup_empty(pool, set->rawlog_dir);
+ }
+
+ if (set->ssl != NULL) {
+ conn->set.ssl =
+ ssl_iostream_settings_dup(pool, set->ssl);
+ }
+
+ if (set->master_user != NULL && *set->master_user != '\0') {
+ conn->set.master_user =
+ p_strdup_empty(pool, set->master_user);
+ }
+ if (set->username != NULL && *set->username != '\0') {
+ conn->set.username =
+ p_strdup_empty(pool, set->username);
+ }
+ if (set->password != NULL && *set->password != '\0') {
+ conn->password = p_strdup(pool, set->password);
+ conn->set.password = conn->password;
+ }
+ if (set->sasl_mech != NULL)
+ conn->set.sasl_mech = set->sasl_mech;
+ else if (set->sasl_mechanisms != NULL &&
+ *set->sasl_mechanisms != '\0') {
+ conn->set.sasl_mechanisms =
+ p_strdup(pool, set->sasl_mechanisms);
+ }
+ conn->set.remember_password = set->remember_password;
+
+ if (set->command_timeout_msecs > 0) {
+ conn->set.command_timeout_msecs =
+ set->command_timeout_msecs;
+ }
+ if (set->connect_timeout_msecs > 0) {
+ conn->set.connect_timeout_msecs =
+ set->connect_timeout_msecs;
+ }
+ if (set->max_reply_size > 0)
+ conn->set.max_reply_size = set->max_reply_size;
+ if (set->max_data_chunk_size > 0) {
+ conn->set.max_data_chunk_size =
+ set->max_data_chunk_size;
+ }
+ if (set->max_data_chunk_pipeline > 0) {
+ conn->set.max_data_chunk_pipeline =
+ set->max_data_chunk_pipeline;
+ }
+
+ if (set->socket_send_buffer_size > 0) {
+ conn->set.socket_send_buffer_size =
+ set->socket_send_buffer_size;
+ }
+ if (set->socket_recv_buffer_size > 0) {
+ conn->set.socket_recv_buffer_size =
+ set->socket_recv_buffer_size;
+ }
+ conn->set.debug = conn->set.debug || set->debug;
+
+ smtp_proxy_data_merge(conn->pool, &conn->set.proxy_data,
+ &set->proxy_data);
+ conn->set.xclient_defer = set->xclient_defer;
+ conn->set.peer_trusted = set->peer_trusted;
+
+ conn->set.mail_send_broken_path = set->mail_send_broken_path;
+
+ conn->set.verbose_user_errors =
+ conn->set.verbose_user_errors ||
+ set->verbose_user_errors;
+ }
+
+ if (set != NULL && set->extra_capabilities != NULL) {
+ const char *const *extp;
+
+ p_array_init(&conn->extra_capabilities, pool,
+ str_array_length(set->extra_capabilities) + 8);
+ for (extp = set->extra_capabilities; *extp != NULL; extp++) {
+ struct smtp_client_capability_extra cap = {
+ .name = p_strdup(pool, *extp),
+ };
+
+ array_push_back(&conn->extra_capabilities, &cap);
+ }
+ }
+
+ i_assert(conn->set.my_hostname != NULL &&
+ *conn->set.my_hostname != '\0');
+
+ conn->caps.standard = conn->set.forced_capabilities;
+ conn->cap_pool = pool_alloconly_create(
+ "smtp client connection capabilities", 128);
+ conn->state_pool = pool_alloconly_create(
+ "smtp client connection state", 256);
+
+ if (set != NULL && set->event_parent != NULL)
+ conn_event = event_create(set->event_parent);
+ else
+ conn_event = event_create(client->event);
+ event_set_append_log_prefix(
+ conn_event,
+ t_strdup_printf("%s-client: ",
+ smtp_protocol_name(conn->protocol)));
+ event_add_str(conn_event, "protocol",
+ smtp_protocol_name(conn->protocol));
+ event_set_forced_debug(conn_event, (set != NULL && set->debug));
+
+ conn->conn.event_parent = conn_event;
+ connection_init(conn->client->conn_list, &conn->conn, name);
+ conn->event = conn->conn.event;
+ event_unref(&conn_event);
+
+ return conn;
+}
+
+struct smtp_client_connection *
+smtp_client_connection_create(struct smtp_client *client,
+ enum smtp_protocol protocol,
+ const char *host, in_port_t port,
+ enum smtp_client_connection_ssl_mode ssl_mode,
+ const struct smtp_client_settings *set)
+{
+ struct smtp_client_connection *conn;
+ const char *name = t_strdup_printf("%s:%u", host, port);
+
+ conn = smtp_client_connection_do_create(client, name, protocol, set);
+ conn->host = p_strdup(conn->pool, host);
+ conn->port = port;
+ conn->ssl_mode = ssl_mode;
+
+ event_add_str(conn->event, "host", host);
+
+ e_debug(conn->event, "Connection created");
+
+ return conn;
+}
+
+struct smtp_client_connection *
+smtp_client_connection_create_ip(struct smtp_client *client,
+ enum smtp_protocol protocol,
+ const struct ip_addr *ip, in_port_t port,
+ const char *hostname,
+ enum smtp_client_connection_ssl_mode ssl_mode,
+ const struct smtp_client_settings *set)
+{
+ struct smtp_client_connection *conn;
+ bool host_is_ip = FALSE;
+
+ if (hostname == NULL) {
+ hostname = net_ip2addr(ip);
+ host_is_ip = TRUE;
+ }
+
+ conn = smtp_client_connection_create(client, protocol, hostname, port,
+ ssl_mode, set);
+ conn->ips_count = 1;
+ conn->ips = i_new(struct ip_addr, conn->ips_count);
+ conn->ips[0] = *ip;
+ conn->host_is_ip = host_is_ip;
+ return conn;
+}
+
+struct smtp_client_connection *
+smtp_client_connection_create_unix(struct smtp_client *client,
+ enum smtp_protocol protocol,
+ const char *path,
+ const struct smtp_client_settings *set)
+{
+ struct smtp_client_connection *conn;
+ const char *name = t_strconcat("unix:", path, NULL);
+
+ conn = smtp_client_connection_do_create(client, name, protocol, set);
+ conn->path = p_strdup(conn->pool, path);
+
+ e_debug(conn->event, "Connection created");
+
+ return conn;
+}
+
+void smtp_client_connection_ref(struct smtp_client_connection *conn)
+{
+ i_assert(conn->refcount >= 0);
+ conn->refcount++;
+}
+
+void smtp_client_connection_unref(struct smtp_client_connection **_conn)
+{
+ struct smtp_client_connection *conn = *_conn;
+
+ *_conn = NULL;
+
+ i_assert(conn->refcount > 0);
+ if (--conn->refcount > 0)
+ return;
+ if (conn->destroying)
+ return;
+
+ conn->destroying = TRUE;
+
+ smtp_client_connection_clear_password(conn);
+ smtp_client_connection_disconnect(conn);
+
+ /* could have been created while already disconnected */
+ timeout_remove(&conn->to_commands);
+ timeout_remove(&conn->to_cmd_fail);
+
+ e_debug(conn->event, "Destroy");
+
+ if (conn->reply_parser != NULL)
+ smtp_reply_parser_deinit(&conn->reply_parser);
+
+ smtp_client_connection_login_fail(
+ conn, SMTP_CLIENT_COMMAND_ERROR_ABORTED,
+ "Connection destroy");
+ smtp_client_connection_transactions_fail(
+ conn, SMTP_CLIENT_COMMAND_ERROR_ABORTED,
+ "Connection destroy");
+ smtp_client_connection_commands_fail(
+ conn, SMTP_CLIENT_COMMAND_ERROR_ABORTED,
+ "Connection destroy");
+ smtp_client_connection_transactions_drop(conn);
+
+ connection_deinit(&conn->conn);
+
+ i_free(conn->ips);
+ array_free(&conn->login_callbacks);
+ pool_unref(&conn->cap_pool);
+ pool_unref(&conn->state_pool);
+ pool_unref(&conn->pool);
+}
+
+void smtp_client_connection_close(struct smtp_client_connection **_conn)
+{
+ struct smtp_client_connection *conn = *_conn;
+
+ *_conn = NULL;
+
+ if (conn->closed)
+ return;
+ conn->closed = TRUE;
+
+ smtp_client_connection_transactions_abort(conn);
+ smtp_client_connection_commands_abort(conn);
+ smtp_client_connection_disconnect(conn);
+
+ /* could have been created while already disconnected */
+ timeout_remove(&conn->to_commands);
+ timeout_remove(&conn->to_cmd_fail);
+
+ smtp_client_connection_unref(&conn);
+}
+
+void smtp_client_connection_update_proxy_data(
+ struct smtp_client_connection *conn,
+ const struct smtp_proxy_data *proxy_data)
+{
+ if (conn->xclient_sent)
+ return;
+
+ smtp_proxy_data_merge(conn->pool, &conn->set.proxy_data, proxy_data);
+}
+
+void smtp_client_connection_switch_ioloop(struct smtp_client_connection *conn)
+{
+ struct smtp_client_transaction *trans;
+
+ if (conn->io_cmd_payload != NULL)
+ conn->io_cmd_payload = io_loop_move_io(&conn->io_cmd_payload);
+ if (conn->to_connect != NULL)
+ conn->to_connect = io_loop_move_timeout(&conn->to_connect);
+ if (conn->to_trans != NULL)
+ conn->to_trans = io_loop_move_timeout(&conn->to_trans);
+ if (conn->to_commands != NULL)
+ conn->to_commands = io_loop_move_timeout(&conn->to_commands);
+ if (conn->to_cmd_fail != NULL)
+ conn->to_cmd_fail = io_loop_move_timeout(&conn->to_cmd_fail);
+ connection_switch_ioloop(&conn->conn);
+
+ trans = conn->transactions_head;
+ while (trans != NULL) {
+ smtp_client_transaction_switch_ioloop(trans);
+ trans = trans->next;
+ }
+}
+
+static void
+smtp_client_connection_rset_dummy_cb(
+ const struct smtp_reply *reply ATTR_UNUSED,
+ struct smtp_client_connection *conn ATTR_UNUSED)
+{
+ /* nothing */
+}
+
+static void
+smtp_client_connection_reset(struct smtp_client_connection *conn)
+{
+ e_debug(conn->event, "Submitting RSET command");
+
+ conn->reset_needed = FALSE;
+
+ (void)smtp_client_command_rset_submit(
+ conn, SMTP_CLIENT_COMMAND_FLAG_PRIORITY,
+ smtp_client_connection_rset_dummy_cb, conn);
+}
+
+static void
+smtp_client_connection_do_start_transaction(struct smtp_client_connection *conn)
+{
+ struct smtp_reply reply;
+
+ timeout_remove(&conn->to_trans);
+
+ if (conn->state != SMTP_CLIENT_CONNECTION_STATE_TRANSACTION)
+ return;
+ if (conn->transactions_head == NULL) {
+ smtp_client_connection_set_state(
+ conn, SMTP_CLIENT_CONNECTION_STATE_READY);
+ return;
+ }
+
+ if (conn->reset_needed)
+ smtp_client_connection_reset(conn);
+
+ e_debug(conn->event, "Start next transaction");
+
+ smtp_reply_init(&reply, 200, "Connection ready");
+ smtp_client_transaction_connection_result(
+ conn->transactions_head, &reply);
+}
+
+static void
+smtp_client_connection_start_transaction(struct smtp_client_connection *conn)
+{
+ if (conn->state != SMTP_CLIENT_CONNECTION_STATE_READY)
+ return;
+ if (conn->transactions_head == NULL)
+ return;
+ if (conn->to_trans != NULL)
+ return;
+
+ smtp_client_connection_set_state(
+ conn, SMTP_CLIENT_CONNECTION_STATE_TRANSACTION);
+ conn->to_trans = timeout_add_short(
+ 0, smtp_client_connection_do_start_transaction, conn);
+}
+
+void smtp_client_connection_add_transaction(
+ struct smtp_client_connection *conn,
+ struct smtp_client_transaction *trans)
+{
+ e_debug(conn->event, "Add transaction");
+
+ DLLIST2_APPEND(&conn->transactions_head, &conn->transactions_tail,
+ trans);
+
+ smtp_client_connection_connect(conn, NULL, NULL);
+ smtp_client_connection_start_transaction(conn);
+}
+
+void smtp_client_connection_abort_transaction(
+ struct smtp_client_connection *conn,
+ struct smtp_client_transaction *trans)
+{
+ bool was_first = (trans == conn->transactions_head);
+
+ e_debug(conn->event, "Abort transaction");
+
+ DLLIST2_REMOVE(&conn->transactions_head, &conn->transactions_tail,
+ trans);
+
+ if (!was_first)
+ return;
+ i_assert(conn->state != SMTP_CLIENT_CONNECTION_STATE_READY);
+ if (conn->state != SMTP_CLIENT_CONNECTION_STATE_TRANSACTION)
+ return;
+
+ /* transaction messed up; protocol state needs to be reset for
+ next transaction */
+ conn->reset_needed = TRUE;
+
+ smtp_client_connection_set_state(
+ conn, SMTP_CLIENT_CONNECTION_STATE_READY);
+ smtp_client_connection_start_transaction(conn);
+}
+
+void smtp_client_connection_next_transaction(
+ struct smtp_client_connection *conn,
+ struct smtp_client_transaction *trans)
+{
+ e_debug(conn->event, "Initiate next transaction");
+
+ i_assert(trans == conn->transactions_head);
+
+ DLLIST2_REMOVE(&conn->transactions_head, &conn->transactions_tail,
+ trans);
+
+ i_assert(conn->state != SMTP_CLIENT_CONNECTION_STATE_READY);
+ if (conn->state != SMTP_CLIENT_CONNECTION_STATE_TRANSACTION)
+ return;
+
+ smtp_client_connection_set_state(
+ conn, SMTP_CLIENT_CONNECTION_STATE_READY);
+ smtp_client_connection_start_transaction(conn);
+}
diff --git a/src/lib-smtp/smtp-client-connection.h b/src/lib-smtp/smtp-client-connection.h
new file mode 100644
index 0000000..b406e2d
--- /dev/null
+++ b/src/lib-smtp/smtp-client-connection.h
@@ -0,0 +1,93 @@
+#ifndef SMTP_CLIENT_CONNECTION_H
+#define SMTP_CLIENT_CONNECTION_H
+
+#include "net.h"
+#include "smtp-common.h"
+
+#include "smtp-client-command.h"
+
+enum smtp_capability;
+
+struct smtp_reply;
+struct smtp_client;
+struct smtp_client_capability_extra;
+struct smtp_client_settings;
+struct smtp_client_command;
+
+enum smtp_client_connection_ssl_mode {
+ SMTP_CLIENT_SSL_MODE_NONE = 0,
+ SMTP_CLIENT_SSL_MODE_IMMEDIATE,
+ SMTP_CLIENT_SSL_MODE_STARTTLS
+};
+
+enum smtp_client_connection_state {
+ /* No connection */
+ SMTP_CLIENT_CONNECTION_STATE_DISCONNECTED = 0,
+ /* Trying to connect */
+ SMTP_CLIENT_CONNECTION_STATE_CONNECTING,
+ /* Connected, performing handshake */
+ SMTP_CLIENT_CONNECTION_STATE_HANDSHAKING,
+ /* Handshake ready, trying to authenticate */
+ SMTP_CLIENT_CONNECTION_STATE_AUTHENTICATING,
+ /* Authenticated, ready to accept commands */
+ SMTP_CLIENT_CONNECTION_STATE_READY,
+ /* Involved in active transaction */
+ SMTP_CLIENT_CONNECTION_STATE_TRANSACTION
+};
+extern const char *const smtp_client_connection_state_names[];
+
+struct smtp_client_connection *
+smtp_client_connection_create(struct smtp_client *client,
+ enum smtp_protocol protocol,
+ const char *host, in_port_t port,
+ enum smtp_client_connection_ssl_mode ssl_mode,
+ const struct smtp_client_settings *set)
+ ATTR_NULL(6);
+struct smtp_client_connection *
+smtp_client_connection_create_ip(struct smtp_client *client,
+ enum smtp_protocol protocol,
+ const struct ip_addr *ip, in_port_t port,
+ const char *hostname,
+ enum smtp_client_connection_ssl_mode ssl_mode,
+ const struct smtp_client_settings *set)
+ ATTR_NULL(5, 7);
+struct smtp_client_connection *
+smtp_client_connection_create_unix(struct smtp_client *client,
+ enum smtp_protocol protocol,
+ const char *path,
+ const struct smtp_client_settings *set)
+ ATTR_NULL(4);
+
+void smtp_client_connection_ref(struct smtp_client_connection *conn);
+void smtp_client_connection_unref(struct smtp_client_connection **_conn);
+void smtp_client_connection_close(struct smtp_client_connection **_conn);
+
+void smtp_client_connection_update_proxy_data(
+ struct smtp_client_connection *conn,
+ const struct smtp_proxy_data *proxy_data);
+
+void smtp_client_connection_cork(struct smtp_client_connection *conn);
+void smtp_client_connection_uncork(struct smtp_client_connection *conn);
+
+void smtp_client_connection_connect(
+ struct smtp_client_connection *conn,
+ smtp_client_command_callback_t login_callback, void *login_context);
+void smtp_client_connection_disconnect(struct smtp_client_connection *conn);
+
+void smtp_client_connection_switch_ioloop(struct smtp_client_connection *conn);
+
+enum smtp_capability
+smtp_client_connection_get_capabilities(struct smtp_client_connection *conn);
+uoff_t smtp_client_connection_get_size_capability(
+ struct smtp_client_connection *conn);
+void smtp_client_connection_accept_extra_capability(
+ struct smtp_client_connection *conn,
+ const struct smtp_client_capability_extra *cap);
+const struct smtp_capability_extra *
+smtp_client_connection_get_extra_capability(struct smtp_client_connection *conn,
+ const char *name);
+
+enum smtp_client_connection_state
+smtp_client_connection_get_state(struct smtp_client_connection *conn);
+
+#endif
diff --git a/src/lib-smtp/smtp-client-private.h b/src/lib-smtp/smtp-client-private.h
new file mode 100644
index 0000000..40f50f9
--- /dev/null
+++ b/src/lib-smtp/smtp-client-private.h
@@ -0,0 +1,340 @@
+#ifndef SMTP_CLIENT_PRIVATE_H
+#define SMTP_CLIENT_PRIVATE_H
+
+#include "connection.h"
+
+#include "smtp-common.h"
+#include "smtp-params.h"
+#include "smtp-client.h"
+#include "smtp-client-command.h"
+#include "smtp-client-transaction.h"
+#include "smtp-client-connection.h"
+
+#define SMTP_CLIENT_DATA_CHUNK_SIZE IO_BLOCK_SIZE
+
+struct smtp_client_command {
+ pool_t pool;
+ int refcount;
+ struct event *event;
+
+ struct smtp_client_command *prev, *next;
+
+ buffer_t *data;
+ unsigned int send_pos;
+ const char *name;
+
+ enum smtp_client_command_flags flags;
+
+ struct smtp_client_connection *conn;
+ enum smtp_client_command_state state;
+ unsigned int replies_expected;
+ unsigned int replies_seen;
+
+ struct istream *stream;
+ uoff_t stream_size;
+
+ struct smtp_reply *delayed_failure;
+
+ smtp_client_command_callback_t *callback;
+ void *context;
+
+ void (*abort_callback)(void *context);
+ void *abort_context;
+
+ void (*sent_callback)(void *context);
+ void *sent_context;
+
+ bool has_stream:1;
+ bool stream_dot:1;
+ bool stream_finished:1;
+ bool ehlo:1;
+ bool locked:1;
+ bool plug:1;
+ bool failed:1;
+ bool aborting:1;
+ bool delay_failure:1;
+ bool delaying_failure:1;
+ bool event_finished:1;
+};
+
+struct smtp_client_transaction_mail {
+ pool_t pool;
+ struct smtp_client_transaction *trans;
+
+ struct smtp_client_transaction_mail *prev, *next;
+
+ struct smtp_address *mail_from;
+ struct smtp_params_mail mail_params;
+
+ smtp_client_command_callback_t *mail_callback;
+ void *context;
+
+ struct smtp_client_command *cmd_mail_from;
+};
+
+struct smtp_client_transaction_rcpt {
+ pool_t pool;
+ struct smtp_client_transaction *trans;
+ struct event *event;
+
+ struct smtp_client_transaction_rcpt *prev, *next;
+
+ struct smtp_address *rcpt_to;
+ struct smtp_params_rcpt rcpt_params;
+
+ smtp_client_command_callback_t *rcpt_callback;
+ void *context;
+
+ smtp_client_command_callback_t *data_callback;
+ void *data_context;
+
+ struct smtp_client_command *cmd_rcpt_to;
+
+ bool external_pool:1;
+ bool queued:1;
+ bool finished:1;
+};
+
+struct smtp_client_transaction {
+ pool_t pool;
+ int refcount;
+ struct event *event;
+
+ struct smtp_client_transaction *prev, *next;
+
+ struct smtp_client_connection *conn;
+ enum smtp_client_transaction_flags flags;
+
+ enum smtp_client_transaction_state state;
+ struct smtp_client_command *cmd_data, *cmd_rset;
+ struct smtp_client_command *cmd_plug, *cmd_last;
+ struct smtp_reply *failure, *mail_failure, *data_failure;
+
+ struct smtp_client_transaction_mail *mail_head, *mail_tail;
+ struct smtp_client_transaction_mail *mail_send;
+
+ struct smtp_client_transaction_rcpt *rcpts_queue_head, *rcpts_queue_tail;
+ struct smtp_client_transaction_rcpt *rcpts_send;
+ struct smtp_client_transaction_rcpt *rcpts_head, *rcpts_tail;
+ struct smtp_client_transaction_rcpt *rcpts_data;
+ unsigned int rcpts_queue_count;
+ unsigned int rcpts_count;
+
+ unsigned int rcpts_total;
+ unsigned int rcpts_aborted;
+ unsigned int rcpts_denied;
+ unsigned int rcpts_failed;
+ unsigned int rcpts_succeeded;
+
+ struct istream *data_input;
+ smtp_client_command_callback_t *data_callback;
+ void *data_context;
+
+ smtp_client_command_callback_t *reset_callback;
+ void *reset_context;
+
+ smtp_client_transaction_callback_t *callback;
+ void *context;
+
+ struct smtp_client_transaction_times times;
+
+ unsigned int finish_timeout_msecs;
+ struct timeout *to_finish, *to_send;
+
+ bool immediate:1;
+ bool sender_accepted:1;
+ bool data_provided:1;
+ bool reset:1;
+ bool finished:1;
+ bool submitting:1;
+ bool failing:1;
+ bool submitted_data:1;
+};
+
+struct smtp_client_login_callback {
+ smtp_client_command_callback_t *callback;
+ void *context;
+};
+
+struct smtp_client_connection {
+ struct connection conn;
+ pool_t pool;
+ int refcount;
+ struct event *event;
+
+ struct smtp_client *client;
+
+ enum smtp_protocol protocol;
+ const char *path, *host;
+ in_port_t port;
+ enum smtp_client_connection_ssl_mode ssl_mode;
+
+ int connect_errno;
+
+ struct smtp_client_settings set;
+ char *password;
+ ARRAY(struct smtp_client_capability_extra) extra_capabilities;
+
+ pool_t cap_pool;
+ struct {
+ enum smtp_capability standard;
+ ARRAY(struct smtp_capability_extra) extra;
+ const char **auth_mechanisms;
+ const char **xclient_args;
+ uoff_t size;
+
+ /* Lists of custom MAIL/RCPT parameters supported by peer. These
+ arrays always end in NULL pointer once created. */
+ ARRAY_TYPE(const_string) mail_param_extensions;
+ ARRAY_TYPE(const_string) rcpt_param_extensions;
+ } caps;
+
+ struct smtp_reply_parser *reply_parser;
+ struct smtp_reply reply;
+ unsigned int xclient_replies_expected;
+
+ struct dns_lookup *dns_lookup;
+
+ struct dsasl_client *sasl_client;
+ char *sasl_ir;
+ struct smtp_client_command *cmd_auth;
+
+ struct timeout *to_connect, *to_trans, *to_commands, *to_cmd_fail;
+ struct io *io_cmd_payload;
+
+ struct istream *raw_input;
+ struct ostream *raw_output, *dot_output;
+
+ struct ssl_iostream_context *ssl_ctx;
+ struct ssl_iostream *ssl_iostream;
+
+ enum smtp_client_connection_state state;
+ pool_t state_pool;
+ struct {
+ struct smtp_reply *login_reply;
+ } state_data;
+
+ ARRAY(struct smtp_client_login_callback) login_callbacks;
+
+ /* commands pending in queue to be sent */
+ struct smtp_client_command *cmd_send_queue_head, *cmd_send_queue_tail;
+ unsigned int cmd_send_queue_count;
+ /* commands that have been (mostly) sent, waiting for response */
+ struct smtp_client_command *cmd_wait_list_head, *cmd_wait_list_tail;
+ unsigned int cmd_wait_list_count;
+ /* commands that have failed before submission */
+ struct smtp_client_command *cmd_fail_list;
+ /* command sending data stream */
+ struct smtp_client_command *cmd_streaming;
+
+ /* active transactions */
+ struct smtp_client_transaction *transactions_head, *transactions_tail;
+
+ unsigned int ips_count, prev_connect_idx;
+ struct ip_addr *ips;
+
+ bool host_is_ip:1;
+ bool old_smtp:1;
+ bool authenticated:1;
+ bool xclient_sent:1;
+ bool connect_failed:1;
+ bool connect_succeeded:1;
+ bool handshake_failed:1;
+ bool corked:1;
+ bool sent_quit:1;
+ bool sending_command:1;
+ bool reset_needed:1;
+ bool failing:1;
+ bool destroying:1;
+ bool closed:1;
+};
+
+struct smtp_client {
+ pool_t pool;
+
+ struct smtp_client_settings set;
+
+ struct event *event;
+ struct ioloop *ioloop;
+ struct ssl_iostream_context *ssl_ctx;
+
+ struct connection_list *conn_list;
+};
+
+/*
+ * Command
+ */
+
+void smtp_client_command_free(struct smtp_client_command *cmd);
+int smtp_client_command_send_more(struct smtp_client_connection *conn);
+int smtp_client_command_input_reply(struct smtp_client_command *cmd,
+ const struct smtp_reply *reply);
+
+void smtp_client_command_drop_callback(struct smtp_client_command *cmd);
+
+void smtp_client_command_fail(struct smtp_client_command **_cmd,
+ unsigned int status, const char *error);
+void smtp_client_command_fail_reply(struct smtp_client_command **_cmd,
+ const struct smtp_reply *reply);
+
+void smtp_client_commands_list_abort(struct smtp_client_command *cmds_list,
+ unsigned int cmds_list_count);
+void smtp_client_commands_list_fail_reply(
+ struct smtp_client_command *cmds_list, unsigned int cmds_list_count,
+ const struct smtp_reply *reply);
+
+void smtp_client_commands_abort_delayed(struct smtp_client_connection *conn);
+void smtp_client_commands_fail_delayed(struct smtp_client_connection *conn);
+
+/*
+ * Transaction
+ */
+
+void smtp_client_transaction_connection_result(
+ struct smtp_client_transaction *trans,
+ const struct smtp_reply *reply);
+void smtp_client_transaction_connection_destroyed(
+ struct smtp_client_transaction *trans);
+
+void smtp_client_transaction_switch_ioloop(
+ struct smtp_client_transaction *trans);
+
+/*
+ * Connection
+ */
+
+struct connection_list *smtp_client_connection_list_init(void);
+
+void smtp_client_connection_send_xclient(struct smtp_client_connection *conn);
+
+void smtp_client_connection_fail(struct smtp_client_connection *conn,
+ unsigned int status, const char *error,
+ const char *user_error) ATTR_NULL(3);
+
+void smtp_client_connection_handle_output_error(
+ struct smtp_client_connection *conn);
+void smtp_client_connection_trigger_output(
+ struct smtp_client_connection *conn);
+
+void smtp_client_connection_start_cmd_timeout(
+ struct smtp_client_connection *conn);
+void smtp_client_connection_update_cmd_timeout(
+ struct smtp_client_connection *conn);
+
+void smtp_client_connection_add_transaction(
+ struct smtp_client_connection *conn,
+ struct smtp_client_transaction *trans);
+void smtp_client_connection_abort_transaction(
+ struct smtp_client_connection *conn,
+ struct smtp_client_transaction *trans);
+void smtp_client_connection_next_transaction(
+ struct smtp_client_connection *conn,
+ struct smtp_client_transaction *trans);
+
+/*
+ * Client
+ */
+
+int smtp_client_init_ssl_ctx(struct smtp_client *client, const char **error_r);
+
+#endif
diff --git a/src/lib-smtp/smtp-client-transaction.c b/src/lib-smtp/smtp-client-transaction.c
new file mode 100644
index 0000000..0112021
--- /dev/null
+++ b/src/lib-smtp/smtp-client-transaction.c
@@ -0,0 +1,1656 @@
+/* Copyright (c) 2009-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "llist.h"
+#include "ioloop.h"
+#include "net.h"
+#include "istream.h"
+#include "istream-crlf.h"
+#include "ostream.h"
+#include "str.h"
+#include "str-sanitize.h"
+#include "dns-lookup.h"
+
+#include "smtp-common.h"
+#include "smtp-address.h"
+#include "smtp-params.h"
+#include "smtp-client-private.h"
+#include "smtp-client-command.h"
+#include "smtp-client-transaction.h"
+
+#include <ctype.h>
+
+const char *const smtp_client_transaction_state_names[] = {
+ "new",
+ "pending",
+ "mail_from",
+ "rcpt_to",
+ "data",
+ "reset",
+ "finished",
+ "aborted"
+};
+
+static void
+smtp_client_transaction_submit_more(struct smtp_client_transaction *trans);
+static void
+smtp_client_transaction_submit(struct smtp_client_transaction *trans,
+ bool start);
+
+static void
+smtp_client_transaction_try_complete(struct smtp_client_transaction *trans);
+
+static void
+smtp_client_transaction_send_data(struct smtp_client_transaction *trans);
+static void
+smtp_client_transaction_send_reset(struct smtp_client_transaction *trans);
+
+/*
+ * Sender
+ */
+
+static struct smtp_client_transaction_mail *
+smtp_client_transaction_mail_new(struct smtp_client_transaction *trans,
+ const struct smtp_address *mail_from,
+ const struct smtp_params_mail *mail_params)
+{
+ struct smtp_client_transaction_mail *mail;
+ pool_t pool;
+
+ pool = pool_alloconly_create("smtp transaction mail", 512);
+ mail = p_new(pool, struct smtp_client_transaction_mail, 1);
+ mail->pool = pool;
+ mail->trans = trans;
+ mail->mail_from = smtp_address_clone(pool, mail_from);
+ smtp_params_mail_copy(pool, &mail->mail_params, mail_params);
+
+ DLLIST2_APPEND(&trans->mail_head, &trans->mail_tail, mail);
+ if (trans->mail_send == NULL)
+ trans->mail_send = mail;
+
+ return mail;
+}
+
+static void
+smtp_client_transaction_mail_free(struct smtp_client_transaction_mail **_mail)
+{
+ struct smtp_client_transaction_mail *mail = *_mail;
+
+ if (mail == NULL)
+ return;
+ *_mail = NULL;
+
+ struct smtp_client_transaction *trans = mail->trans;
+
+ if (mail->cmd_mail_from != NULL)
+ smtp_client_command_abort(&mail->cmd_mail_from);
+ DLLIST2_REMOVE(&trans->mail_head, &trans->mail_tail, mail);
+ pool_unref(&mail->pool);
+}
+
+static void
+smtp_client_transaction_mail_replied(
+ struct smtp_client_transaction_mail **_mail,
+ const struct smtp_reply *reply)
+{
+ struct smtp_client_transaction_mail *mail = *_mail;
+
+ if (mail == NULL)
+ return;
+ *_mail = NULL;
+
+ smtp_client_command_callback_t *mail_callback = mail->mail_callback;
+ void *context = mail->context;
+
+ mail->mail_callback = NULL;
+ smtp_client_transaction_mail_free(&mail);
+
+ /* Call the callback */
+ if (mail_callback != NULL)
+ mail_callback(reply, context);
+}
+
+void smtp_client_transaction_mail_abort(
+ struct smtp_client_transaction_mail **_mail)
+{
+ struct smtp_client_transaction_mail *mail = *_mail;
+
+ if (mail == NULL)
+ return;
+ *_mail = NULL;
+
+ struct smtp_client_transaction *trans = mail->trans;
+
+ i_assert(trans->state <= SMTP_CLIENT_TRANSACTION_STATE_MAIL_FROM ||
+ trans->state == SMTP_CLIENT_TRANSACTION_STATE_ABORTED);
+
+ smtp_client_transaction_mail_free(&mail);
+}
+
+static void
+smtp_client_transaction_mail_fail_reply(
+ struct smtp_client_transaction_mail **_mail,
+ const struct smtp_reply *reply)
+{
+ struct smtp_client_transaction_mail *mail = *_mail;
+
+ if (mail == NULL)
+ return;
+ *_mail = NULL;
+
+ smtp_client_command_callback_t *callback = mail->mail_callback;
+ void *context = mail->context;
+
+ mail->mail_callback = NULL;
+ smtp_client_transaction_mail_free(&mail);
+
+ if (callback != NULL)
+ callback(reply, context);
+}
+
+/*
+ * Recipient
+ */
+
+static void
+smtp_client_transaction_rcpt_update_event(
+ struct smtp_client_transaction_rcpt *rcpt)
+{
+ const char *to = smtp_address_encode(rcpt->rcpt_to);
+
+ event_set_append_log_prefix(rcpt->event,
+ t_strdup_printf("rcpt <%s>: ",
+ str_sanitize(to, 128)));
+ event_add_str(rcpt->event, "rcpt_to", to);
+ smtp_params_rcpt_add_to_event(&rcpt->rcpt_params, rcpt->event);
+}
+
+static struct smtp_client_transaction_rcpt *
+smtp_client_transaction_rcpt_new(struct smtp_client_transaction *trans,
+ pool_t pool,
+ const struct smtp_address *rcpt_to,
+ const struct smtp_params_rcpt *rcpt_params)
+{
+ struct smtp_client_transaction_rcpt *rcpt;
+
+ pool_ref(pool);
+
+ rcpt = p_new(pool, struct smtp_client_transaction_rcpt, 1);
+ rcpt->pool = pool;
+ rcpt->trans = trans;
+ rcpt->rcpt_to = smtp_address_clone(pool, rcpt_to);
+ smtp_params_rcpt_copy(pool, &rcpt->rcpt_params, rcpt_params);
+
+ DLLIST2_APPEND(&trans->rcpts_queue_head, &trans->rcpts_queue_tail,
+ rcpt);
+ trans->rcpts_queue_count++;
+ rcpt->queued = TRUE;
+ if (trans->rcpts_send == NULL)
+ trans->rcpts_send = rcpt;
+
+ rcpt->event = event_create(trans->event);
+ smtp_client_transaction_rcpt_update_event(rcpt);
+
+ trans->rcpts_total++;
+
+ return rcpt;
+}
+
+static void
+smtp_client_transaction_rcpt_free(
+ struct smtp_client_transaction_rcpt **_rcpt)
+{
+ struct smtp_client_transaction_rcpt *rcpt = *_rcpt;
+
+ if (rcpt == NULL)
+ return;
+ *_rcpt = NULL;
+
+ struct smtp_client_transaction *trans = rcpt->trans;
+
+ smtp_client_command_abort(&rcpt->cmd_rcpt_to);
+ if (trans->rcpts_send == rcpt)
+ trans->rcpts_send = rcpt->next;
+ if (trans->rcpts_data == rcpt)
+ trans->rcpts_data = rcpt->next;
+ if (rcpt->queued) {
+ DLLIST2_REMOVE(&trans->rcpts_queue_head,
+ &trans->rcpts_queue_tail, rcpt);
+ trans->rcpts_queue_count--;
+ } else {
+ DLLIST2_REMOVE(&trans->rcpts_head,
+ &trans->rcpts_tail, rcpt);
+ trans->rcpts_count--;
+ }
+
+ if (!rcpt->finished) {
+ struct smtp_reply failure;
+
+ trans->rcpts_aborted++;
+
+ smtp_reply_init(&failure,
+ SMTP_CLIENT_COMMAND_ERROR_ABORTED, "Aborted");
+ failure.enhanced_code = SMTP_REPLY_ENH_CODE(9, 0, 0);
+
+ struct event_passthrough *e =
+ event_create_passthrough(rcpt->event)->
+ set_name("smtp_client_transaction_rcpt_finished");
+ smtp_reply_add_to_event(&failure, e);
+ e_debug(e->event(), "Aborted");
+ }
+
+ event_unref(&rcpt->event);
+
+ if (rcpt->queued || rcpt->external_pool) {
+ i_assert(rcpt->pool != NULL);
+ pool_unref(&rcpt->pool);
+ }
+}
+
+static void
+smtp_client_transaction_rcpt_approved(
+ struct smtp_client_transaction_rcpt **_rcpt)
+{
+ struct smtp_client_transaction_rcpt *prcpt = *_rcpt;
+
+ i_assert(prcpt != NULL);
+
+ struct smtp_client_transaction *trans = prcpt->trans;
+ struct smtp_client_transaction_rcpt *rcpt;
+ pool_t pool;
+
+ i_assert(prcpt->queued);
+
+ if (prcpt->external_pool) {
+ /* Allocated externally; just remove it from the queue */
+ prcpt->queued = FALSE;
+ if (trans->rcpts_send == prcpt)
+ trans->rcpts_send = prcpt->next;
+ DLLIST2_REMOVE(&trans->rcpts_queue_head,
+ &trans->rcpts_queue_tail, prcpt);
+ trans->rcpts_queue_count--;
+
+ rcpt = prcpt;
+ } else {
+ /* Move to transaction pool */
+ pool = trans->pool;
+ rcpt = p_new(pool, struct smtp_client_transaction_rcpt, 1);
+ rcpt->trans = trans;
+ rcpt->rcpt_to = smtp_address_clone(pool, prcpt->rcpt_to);
+ smtp_params_rcpt_copy(pool, &rcpt->rcpt_params,
+ &prcpt->rcpt_params);
+ rcpt->data_callback = prcpt->data_callback;
+ rcpt->data_context = prcpt->data_context;
+
+ rcpt->event = prcpt->event;
+ event_ref(rcpt->event);
+
+ /* Free the old object, thereby removing it from the queue */
+ smtp_client_transaction_rcpt_free(&prcpt);
+ }
+
+ /* Recipient is approved */
+ DLLIST2_APPEND(&trans->rcpts_head, &trans->rcpts_tail, rcpt);
+ trans->rcpts_count++;
+ if (trans->rcpts_data == NULL)
+ trans->rcpts_data = trans->rcpts_head;
+
+ *_rcpt = rcpt;
+}
+
+static void
+smtp_client_transaction_rcpt_denied(
+ struct smtp_client_transaction_rcpt **_rcpt,
+ const struct smtp_reply *reply)
+{
+ struct smtp_client_transaction_rcpt *prcpt = *_rcpt;
+
+ *_rcpt = NULL;
+ i_assert(prcpt != NULL);
+
+ struct smtp_client_transaction *trans = prcpt->trans;
+
+ trans->rcpts_denied++;
+ trans->rcpts_failed++;
+
+ struct event_passthrough *e =
+ event_create_passthrough(prcpt->event)->
+ set_name("smtp_client_transaction_rcpt_finished");
+ smtp_reply_add_to_event(reply, e);
+ e_debug(e->event(), "Denied");
+
+ /* Not pending anymore */
+ smtp_client_transaction_rcpt_free(&prcpt);
+}
+
+static void
+smtp_client_transaction_rcpt_replied(
+ struct smtp_client_transaction_rcpt **_rcpt,
+ const struct smtp_reply *reply)
+{
+ struct smtp_client_transaction_rcpt *rcpt = *_rcpt;
+
+ *_rcpt = NULL;
+ if (rcpt == NULL)
+ return;
+
+ bool success = smtp_reply_is_success(reply);
+ smtp_client_command_callback_t *rcpt_callback = rcpt->rcpt_callback;
+ void *context = rcpt->context;
+
+ rcpt->rcpt_callback = NULL;
+
+ if (rcpt->finished)
+ return;
+ rcpt->finished = !success;
+
+ if (success)
+ smtp_client_transaction_rcpt_approved(&rcpt);
+ else
+ smtp_client_transaction_rcpt_denied(&rcpt, reply);
+
+ /* Call the callback */
+ if (rcpt_callback != NULL)
+ rcpt_callback(reply, context);
+}
+
+void smtp_client_transaction_rcpt_abort(
+ struct smtp_client_transaction_rcpt **_rcpt)
+{
+ struct smtp_client_transaction_rcpt *rcpt = *_rcpt;
+
+ if (rcpt == NULL)
+ return;
+ *_rcpt = NULL;
+
+ struct smtp_client_transaction *trans = rcpt->trans;
+
+ i_assert(rcpt->queued || rcpt->external_pool);
+
+ i_assert(trans->state <= SMTP_CLIENT_TRANSACTION_STATE_RCPT_TO ||
+ trans->state == SMTP_CLIENT_TRANSACTION_STATE_ABORTED);
+
+ smtp_client_transaction_rcpt_free(&rcpt);
+}
+
+static void
+smtp_client_transaction_rcpt_fail_reply(
+ struct smtp_client_transaction_rcpt **_rcpt,
+ const struct smtp_reply *reply)
+{
+ struct smtp_client_transaction_rcpt *rcpt = *_rcpt;
+
+ if (rcpt == NULL)
+ return;
+ *_rcpt = NULL;
+
+ struct smtp_client_transaction *trans = rcpt->trans;
+ smtp_client_command_callback_t *callback;
+ void *context;
+
+ if (rcpt->finished)
+ return;
+ rcpt->finished = TRUE;
+
+ trans->rcpts_failed++;
+
+ if (rcpt->queued) {
+ callback = rcpt->rcpt_callback;
+ context = rcpt->context;
+ } else {
+ callback = rcpt->data_callback;
+ context = rcpt->data_context;
+ }
+ rcpt->rcpt_callback = NULL;
+ rcpt->data_callback = NULL;
+
+ struct event_passthrough *e =
+ event_create_passthrough(rcpt->event)->
+ set_name("smtp_client_transaction_rcpt_finished");
+ smtp_reply_add_to_event(reply, e);
+ e_debug(e->event(), "Failed");
+
+ smtp_client_transaction_rcpt_free(&rcpt);
+
+ if (callback != NULL)
+ callback(reply, context);
+}
+
+static void
+smtp_client_transaction_rcpt_finished(struct smtp_client_transaction_rcpt *rcpt,
+ const struct smtp_reply *reply)
+{
+ struct smtp_client_transaction *trans = rcpt->trans;
+
+ i_assert(!rcpt->finished);
+ rcpt->finished = TRUE;
+
+ if (smtp_reply_is_success(reply))
+ trans->rcpts_succeeded++;
+ else
+ trans->rcpts_failed++;
+
+ struct event_passthrough *e =
+ event_create_passthrough(rcpt->event)->
+ set_name("smtp_client_transaction_rcpt_finished");
+ smtp_reply_add_to_event(reply, e);
+ e_debug(e->event(), "Finished");
+
+ if (rcpt->data_callback != NULL)
+ rcpt->data_callback(reply, rcpt->data_context);
+ rcpt->data_callback = NULL;
+}
+
+#undef smtp_client_transaction_rcpt_set_data_callback
+void smtp_client_transaction_rcpt_set_data_callback(
+ struct smtp_client_transaction_rcpt *rcpt,
+ smtp_client_command_callback_t *callback, void *context)
+{
+ i_assert(!rcpt->finished);
+
+ rcpt->data_callback = callback;
+ rcpt->data_context = context;
+}
+
+/*
+ * Transaction
+ */
+
+static void
+smtp_client_transaction_update_event(struct smtp_client_transaction *trans)
+{
+ event_set_append_log_prefix(trans->event, "transaction: ");
+}
+
+static struct event_passthrough *
+smtp_client_transaction_result_event(struct smtp_client_transaction *trans,
+ const struct smtp_reply *reply)
+{
+ struct event_passthrough *e;
+ unsigned int rcpts_aborted = (trans->rcpts_aborted +
+ trans->rcpts_queue_count);
+
+ e = event_create_passthrough(trans->event)->
+ set_name("smtp_client_transaction_finished")->
+ add_int("recipients", trans->rcpts_total)->
+ add_int("recipients_aborted", rcpts_aborted)->
+ add_int("recipients_denied", trans->rcpts_denied)->
+ add_int("recipients_failed", trans->rcpts_failed)->
+ add_int("recipients_succeeded", trans->rcpts_succeeded);
+
+ smtp_reply_add_to_event(reply, e);
+ if (trans->reset)
+ e->add_str("is_reset", "yes");
+ return e;
+}
+
+#undef smtp_client_transaction_create_empty
+struct smtp_client_transaction *
+smtp_client_transaction_create_empty(
+ struct smtp_client_connection *conn,
+ enum smtp_client_transaction_flags flags,
+ smtp_client_transaction_callback_t *callback, void *context)
+{
+ struct smtp_client_transaction *trans;
+ pool_t pool;
+
+ if (conn->protocol == SMTP_PROTOCOL_LMTP)
+ flags |= SMTP_CLIENT_TRANSACTION_FLAG_REPLY_PER_RCPT;
+
+ pool = pool_alloconly_create("smtp transaction", 4096);
+ trans = p_new(pool, struct smtp_client_transaction, 1);
+ trans->refcount = 1;
+ trans->pool = pool;
+ trans->flags = flags;
+ trans->callback = callback;
+ trans->context = context;
+
+ trans->event = event_create(conn->event);
+ smtp_client_transaction_update_event(trans);
+
+ trans->conn = conn;
+ smtp_client_connection_ref(conn);
+
+ e_debug(trans->event, "Created");
+
+ return trans;
+}
+
+#undef smtp_client_transaction_create
+struct smtp_client_transaction *
+smtp_client_transaction_create(struct smtp_client_connection *conn,
+ const struct smtp_address *mail_from,
+ const struct smtp_params_mail *mail_params,
+ enum smtp_client_transaction_flags flags,
+ smtp_client_transaction_callback_t *callback,
+ void *context)
+{
+ struct smtp_client_transaction *trans;
+
+ trans = smtp_client_transaction_create_empty(conn, flags,
+ callback, context);
+ (void)smtp_client_transaction_mail_new(trans, mail_from, mail_params);
+ return trans;
+}
+
+static void
+smtp_client_transaction_finish(struct smtp_client_transaction *trans,
+ const struct smtp_reply *final_reply)
+{
+ struct smtp_client_connection *conn = trans->conn;
+
+ if (trans->state >= SMTP_CLIENT_TRANSACTION_STATE_FINISHED)
+ return;
+
+ timeout_remove(&trans->to_finish);
+
+ struct event_passthrough *e =
+ smtp_client_transaction_result_event(trans, final_reply);
+ e_debug(e->event(), "Finished");
+
+ io_loop_time_refresh();
+ trans->times.finished = ioloop_timeval;
+
+ i_assert(trans->to_send == NULL);
+
+ trans->state = SMTP_CLIENT_TRANSACTION_STATE_FINISHED;
+ i_assert(trans->callback != NULL);
+ trans->callback(trans->context);
+
+ if (!trans->submitted_data)
+ smtp_client_connection_abort_transaction(conn, trans);
+
+ smtp_client_transaction_unref(&trans);
+}
+
+void smtp_client_transaction_abort(struct smtp_client_transaction *trans)
+{
+ struct smtp_client_connection *conn = trans->conn;
+
+ if (trans->failing) {
+ e_debug(trans->event, "Abort (already failing)");
+ return;
+ }
+
+ e_debug(trans->event, "Abort");
+
+ /* Clean up */
+ i_stream_unref(&trans->data_input);
+ timeout_remove(&trans->to_send);
+ timeout_remove(&trans->to_finish);
+
+ trans->cmd_last = NULL;
+
+ /* Abort any pending commands */
+ while (trans->mail_head != NULL) {
+ struct smtp_client_transaction_mail *mail = trans->mail_head;
+
+ smtp_client_transaction_mail_free(&mail);
+ }
+ while (trans->rcpts_queue_count > 0) {
+ struct smtp_client_transaction_rcpt *rcpt =
+ trans->rcpts_queue_head;
+
+ smtp_client_transaction_rcpt_free(&rcpt);
+ }
+ if (trans->cmd_data != NULL)
+ smtp_client_command_abort(&trans->cmd_data);
+ if (trans->cmd_rset != NULL)
+ smtp_client_command_abort(&trans->cmd_rset);
+ if (trans->cmd_plug != NULL)
+ smtp_client_command_abort(&trans->cmd_plug);
+ trans->cmd_data = NULL;
+ trans->cmd_rset = NULL;
+ trans->cmd_plug = NULL;
+
+ smtp_client_connection_abort_transaction(conn, trans);
+
+ /* Abort if not finished */
+ if (trans->state < SMTP_CLIENT_TRANSACTION_STATE_FINISHED) {
+ struct event_passthrough *e;
+
+ if (trans->failure != NULL) {
+ e = smtp_client_transaction_result_event(
+ trans, trans->failure);
+ e_debug(e->event(), "Failed");
+ } else {
+ struct smtp_reply failure;
+
+ smtp_reply_init(&failure,
+ SMTP_CLIENT_COMMAND_ERROR_ABORTED,
+ "Aborted");
+ failure.enhanced_code = SMTP_REPLY_ENH_CODE(9, 0, 0);
+
+ e = smtp_client_transaction_result_event(
+ trans, &failure);
+ e_debug(e->event(), "Aborted");
+ }
+
+ trans->state = SMTP_CLIENT_TRANSACTION_STATE_ABORTED;
+ i_assert(trans->callback != NULL);
+ trans->callback(trans->context);
+
+ smtp_client_transaction_unref(&trans);
+ }
+}
+
+void smtp_client_transaction_ref(struct smtp_client_transaction *trans)
+{
+ trans->refcount++;
+}
+
+void smtp_client_transaction_unref(struct smtp_client_transaction **_trans)
+{
+ struct smtp_client_transaction *trans = *_trans;
+ struct smtp_client_connection *conn;
+
+ *_trans = NULL;
+
+ if (trans == NULL)
+ return;
+ conn = trans->conn;
+
+ i_assert(trans->refcount > 0);
+ if (--trans->refcount > 0)
+ return;
+
+ e_debug(trans->event, "Destroy");
+
+ i_stream_unref(&trans->data_input);
+ smtp_client_transaction_abort(trans);
+
+ while (trans->rcpts_count > 0) {
+ struct smtp_client_transaction_rcpt *rcpt =
+ trans->rcpts_head;
+ smtp_client_transaction_rcpt_free(&rcpt);
+ }
+
+ i_assert(trans->state >= SMTP_CLIENT_TRANSACTION_STATE_FINISHED);
+ event_unref(&trans->event);
+ pool_unref(&trans->pool);
+
+ smtp_client_connection_unref(&conn);
+}
+
+void smtp_client_transaction_destroy(struct smtp_client_transaction **_trans)
+{
+ struct smtp_client_transaction *trans = *_trans;
+ struct smtp_client_transaction_mail *mail;
+ struct smtp_client_transaction_rcpt *rcpt;
+
+ *_trans = NULL;
+
+ if (trans == NULL)
+ return;
+
+ smtp_client_transaction_ref(trans);
+ smtp_client_transaction_abort(trans);
+
+ /* Make sure this transaction doesn't produce any more callbacks.
+ We cannot fully abort (destroy) these commands, as this may be
+ called from a callback. */
+ for (mail = trans->mail_head; mail != NULL; mail = mail->next) {
+ if (mail->cmd_mail_from != NULL)
+ smtp_client_command_drop_callback(mail->cmd_mail_from);
+ }
+ for (rcpt = trans->rcpts_queue_head; rcpt != NULL; rcpt = rcpt->next) {
+ if (rcpt->cmd_rcpt_to != NULL)
+ smtp_client_command_drop_callback(rcpt->cmd_rcpt_to);
+ }
+ if (trans->cmd_data != NULL)
+ smtp_client_command_drop_callback(trans->cmd_data);
+ if (trans->cmd_rset != NULL)
+ smtp_client_command_drop_callback(trans->cmd_rset);
+ if (trans->cmd_plug != NULL)
+ smtp_client_command_abort(&trans->cmd_plug);
+
+ /* Free any approved recipients early */
+ while (trans->rcpts_count > 0) {
+ struct smtp_client_transaction_rcpt *rcpt =
+ trans->rcpts_head;
+ smtp_client_transaction_rcpt_free(&rcpt);
+ }
+
+ if (trans->state < SMTP_CLIENT_TRANSACTION_STATE_FINISHED) {
+ struct smtp_client_transaction *trans_tmp = trans;
+
+ trans->state = SMTP_CLIENT_TRANSACTION_STATE_ABORTED;
+ smtp_client_transaction_unref(&trans_tmp);
+ }
+
+ smtp_client_transaction_unref(&trans);
+}
+
+void smtp_client_transaction_fail_reply(struct smtp_client_transaction *trans,
+ const struct smtp_reply *reply)
+{
+ struct smtp_client_transaction_rcpt *rcpt, *rcpt_next;
+
+ if (reply == NULL)
+ reply = trans->failure;
+ i_assert(reply != NULL);
+
+ if (trans->failing) {
+ e_debug(trans->event, "Already failing: %s",
+ smtp_reply_log(reply));
+ return;
+ }
+ trans->failing = TRUE;
+
+ e_debug(trans->event, "Returning failure: %s", smtp_reply_log(reply));
+
+ /* Hold a reference to prevent early destruction in a callback */
+ smtp_client_transaction_ref(trans);
+
+ trans->cmd_last = NULL;
+
+ timeout_remove(&trans->to_send);
+
+ /* MAIL */
+ while (trans->mail_head != NULL) {
+ struct smtp_client_transaction_mail *mail = trans->mail_head;
+
+ smtp_client_transaction_mail_fail_reply(&mail, reply);
+ }
+
+ /* RCPT */
+ rcpt = trans->rcpts_queue_head;
+ while (rcpt != NULL) {
+ struct smtp_client_command *cmd = rcpt->cmd_rcpt_to;
+
+ rcpt_next = rcpt->next;
+
+ rcpt->cmd_rcpt_to = NULL;
+ if (cmd != NULL)
+ smtp_client_command_fail_reply(&cmd, reply);
+ else
+ smtp_client_transaction_rcpt_fail_reply(&rcpt, reply);
+
+ rcpt = rcpt_next;
+ }
+
+ /* DATA / RSET */
+ if (!trans->data_provided && !trans->reset) {
+ /* None of smtp_client_transaction_send() and
+ smtp_client_transaction_reset() was called so far
+ */
+ } else if (trans->cmd_data != NULL) {
+ /* The DATA command is still pending; handle the failure by
+ failing the DATA command. */
+ smtp_client_command_fail_reply(&trans->cmd_data, reply);
+ } else if (trans->cmd_rset != NULL) {
+ /* The RSET command is still pending; handle the failure by
+ failing the RSET command. */
+ smtp_client_command_fail_reply(&trans->cmd_rset, reply);
+ } else {
+ i_assert(!trans->reset);
+
+ /* The DATA command was not sent yet; call all DATA callbacks
+ for the recipients that were previously accepted. */
+ rcpt = trans->rcpts_data;
+ while (rcpt != NULL) {
+ rcpt_next = rcpt->next;
+ smtp_client_transaction_rcpt_fail_reply(&rcpt, reply);
+ rcpt = rcpt_next;
+ }
+ if (trans->data_callback != NULL)
+ trans->data_callback(reply, trans->data_context);
+ trans->data_callback = NULL;
+ }
+
+ /* Plug */
+ if (trans->failure == NULL)
+ trans->failure = smtp_reply_clone(trans->pool, reply);
+ if (trans->cmd_plug != NULL)
+ smtp_client_command_abort(&trans->cmd_plug);
+ trans->cmd_plug = NULL;
+
+ trans->failing = FALSE;
+
+ if (trans->data_provided || trans->reset) {
+ /* Abort the transaction only if smtp_client_transaction_send()
+ or smtp_client_transaction_reset() was called (and if it is
+ not aborted already) */
+ smtp_client_transaction_abort(trans);
+ }
+
+ /* Drop reference held earlier in this function */
+ smtp_client_transaction_unref(&trans);
+}
+
+void smtp_client_transaction_fail(struct smtp_client_transaction *trans,
+ unsigned int status, const char *error)
+{
+ struct smtp_reply reply;
+
+ smtp_reply_init(&reply, status, error);
+ smtp_client_transaction_fail_reply(trans, &reply);
+}
+
+void smtp_client_transaction_set_event(struct smtp_client_transaction *trans,
+ struct event *event)
+{
+ i_assert(trans->conn != NULL);
+ event_unref(&trans->event);
+ trans->event = event_create(event);
+ event_set_forced_debug(trans->event, trans->conn->set.debug);
+ smtp_client_transaction_update_event(trans);
+}
+
+static void
+smtp_client_transaction_timeout(struct smtp_client_transaction *trans)
+{
+ struct smtp_reply reply;
+
+ smtp_reply_printf(
+ &reply, 451, "Remote server not answering "
+ "(transaction timed out while %s)",
+ smtp_client_transaction_get_state_destription(trans));
+ reply.enhanced_code = SMTP_REPLY_ENH_CODE(4, 4, 0);
+
+ smtp_client_transaction_fail_reply(trans, &reply);
+}
+
+void smtp_client_transaction_set_timeout(struct smtp_client_transaction *trans,
+ unsigned int timeout_msecs)
+{
+ i_assert(trans->state < SMTP_CLIENT_TRANSACTION_STATE_FINISHED);
+
+ trans->finish_timeout_msecs = timeout_msecs;
+
+ if (trans->data_input != NULL && timeout_msecs > 0) {
+ /* Adjust timeout if it is already started */
+ timeout_remove(&trans->to_finish);
+ trans->to_finish = timeout_add(trans->finish_timeout_msecs,
+ smtp_client_transaction_timeout,
+ trans);
+ }
+}
+
+static void
+smtp_client_transaction_mail_cb(const struct smtp_reply *reply,
+ struct smtp_client_transaction *trans)
+{
+ struct smtp_client_transaction_mail *mail = trans->mail_head;
+ bool success = smtp_reply_is_success(reply);
+
+ e_debug(trans->event, "Got MAIL reply: %s", smtp_reply_log(reply));
+
+ i_assert(mail != NULL);
+ i_assert(trans->conn != NULL);
+
+ if (success) {
+ if (trans->sender_accepted) {
+ smtp_client_transaction_fail(
+ trans, SMTP_CLIENT_COMMAND_ERROR_BAD_REPLY,
+ "Server accepted more than a single MAIL command.");
+ return;
+ }
+ trans->mail_failure = NULL;
+ trans->sender_accepted = TRUE;
+ }
+
+ /* Plug command line pipeline if no RCPT commands are yet issued */
+ if (!trans->immediate && mail->next == NULL &&
+ mail->cmd_mail_from == trans->cmd_last) {
+ trans->cmd_plug = trans->cmd_last =
+ smtp_client_command_plug(trans->conn, trans->cmd_last);
+ }
+ mail->cmd_mail_from = NULL;
+
+ if (trans->rcpts_queue_count > 0)
+ trans->state = SMTP_CLIENT_TRANSACTION_STATE_RCPT_TO;
+ else if (trans->reset)
+ trans->state = SMTP_CLIENT_TRANSACTION_STATE_RESET;
+
+ {
+ enum smtp_client_transaction_state state;
+ struct smtp_client_transaction *tmp_trans = trans;
+
+ smtp_client_transaction_ref(tmp_trans);
+
+ smtp_client_transaction_mail_replied(&mail, reply);
+
+ state = trans->state;
+ smtp_client_transaction_unref(&tmp_trans);
+ if (state >= SMTP_CLIENT_TRANSACTION_STATE_FINISHED)
+ return;
+ }
+
+ if (!trans->sender_accepted && trans->mail_head != NULL) {
+ /* Update transaction with next MAIL command candidate */
+ mail = trans->mail_head;
+ event_add_str(trans->event, "mail_from",
+ smtp_address_encode(mail->mail_from));
+ smtp_params_mail_add_to_event(&mail->mail_params,
+ trans->event);
+ }
+
+ if (!success && !trans->sender_accepted) {
+ if (trans->state > SMTP_CLIENT_TRANSACTION_STATE_MAIL_FROM)
+ smtp_client_transaction_fail_reply(trans, reply);
+ else if (trans->mail_failure == NULL) {
+ trans->mail_failure =
+ smtp_reply_clone(trans->pool, reply);
+ }
+ }
+}
+
+#undef smtp_client_transaction_add_mail
+struct smtp_client_transaction_mail *
+smtp_client_transaction_add_mail(struct smtp_client_transaction *trans,
+ const struct smtp_address *mail_from,
+ const struct smtp_params_mail *mail_params,
+ smtp_client_command_callback_t *mail_callback,
+ void *context)
+{
+ struct smtp_client_transaction_mail *mail;
+
+ e_debug(trans->event, "Add MAIL command");
+
+ i_assert(!trans->data_provided);
+ i_assert(!trans->reset);
+
+ i_assert(trans->state < SMTP_CLIENT_TRANSACTION_STATE_RCPT_TO);
+
+ mail = smtp_client_transaction_mail_new(trans, mail_from, mail_params);
+ mail->mail_callback = mail_callback;
+ mail->context = context;
+
+ smtp_client_transaction_submit(trans, FALSE);
+
+ return mail;
+}
+
+static void
+smtp_client_transaction_connection_ready(struct smtp_client_transaction *trans)
+{
+ if (trans->state != SMTP_CLIENT_TRANSACTION_STATE_PENDING)
+ return;
+
+ e_debug(trans->event, "Connecton is ready for transaction");
+
+ trans->state = SMTP_CLIENT_TRANSACTION_STATE_MAIL_FROM;
+
+ smtp_client_transaction_submit_more(trans);
+}
+
+#undef smtp_client_transaction_start
+void smtp_client_transaction_start(
+ struct smtp_client_transaction *trans,
+ smtp_client_command_callback_t *mail_callback, void *context)
+{
+ struct smtp_client_connection *conn = trans->conn;
+ struct smtp_client_transaction_mail *mail = trans->mail_head;
+
+ i_assert(trans->state == SMTP_CLIENT_TRANSACTION_STATE_NEW);
+ i_assert(trans->conn != NULL);
+
+ i_assert(mail != NULL);
+ event_add_str(trans->event, "mail_from",
+ smtp_address_encode(mail->mail_from));
+ event_add_str(trans->event, "mail_from_raw",
+ smtp_address_encode_raw(mail->mail_from));
+ smtp_params_mail_add_to_event(&mail->mail_params,
+ trans->event);
+
+ struct event_passthrough *e =
+ event_create_passthrough(trans->event)->
+ set_name("smtp_client_transaction_started");
+ e_debug(e->event(), "Start");
+
+ io_loop_time_refresh();
+ trans->times.started = ioloop_timeval;
+
+ i_assert(mail->mail_callback == NULL);
+
+ mail->mail_callback = mail_callback;
+ mail->context = context;
+
+ trans->state = SMTP_CLIENT_TRANSACTION_STATE_PENDING;
+
+ smtp_client_connection_add_transaction(conn, trans);
+
+ if (trans->immediate &&
+ conn->state == SMTP_CLIENT_CONNECTION_STATE_READY) {
+ trans->state = SMTP_CLIENT_TRANSACTION_STATE_MAIL_FROM;
+
+ if (!trans->submitting)
+ smtp_client_transaction_submit_more(trans);
+ } else if (trans->cmd_last == NULL) {
+ trans->cmd_plug = trans->cmd_last =
+ smtp_client_command_plug(trans->conn, NULL);
+ }
+}
+
+#undef smtp_client_transaction_start_empty
+void smtp_client_transaction_start_empty(
+ struct smtp_client_transaction *trans,
+ const struct smtp_address *mail_from,
+ const struct smtp_params_mail *mail_params,
+ smtp_client_command_callback_t *mail_callback, void *context)
+{
+ i_assert(trans->mail_head == NULL);
+
+ (void)smtp_client_transaction_mail_new(trans, mail_from, mail_params);
+
+ smtp_client_transaction_start(trans, mail_callback, context);
+}
+
+static void
+smtp_client_transaction_rcpt_cb(const struct smtp_reply *reply,
+ struct smtp_client_transaction_rcpt *rcpt)
+{
+ struct smtp_client_transaction *trans = rcpt->trans;
+
+ i_assert(trans->conn != NULL);
+
+ e_debug(trans->event, "Got RCPT reply: %s", smtp_reply_log(reply));
+
+ /* Plug command line pipeline if DATA command is not yet issued */
+ if (!trans->immediate && !trans->reset &&
+ rcpt->cmd_rcpt_to == trans->cmd_last && trans->cmd_data == NULL) {
+ trans->cmd_plug = trans->cmd_last =
+ smtp_client_command_plug(trans->conn, trans->cmd_last);
+ }
+ rcpt->cmd_rcpt_to = NULL;
+
+ {
+ enum smtp_client_transaction_state state;
+ struct smtp_client_transaction *tmp_trans = trans;
+
+ smtp_client_transaction_ref(tmp_trans);
+
+ smtp_client_transaction_rcpt_replied(&rcpt, reply);
+
+ state = trans->state;
+ smtp_client_transaction_unref(&tmp_trans);
+ if (state >= SMTP_CLIENT_TRANSACTION_STATE_FINISHED)
+ return;
+ }
+
+ smtp_client_transaction_try_complete(trans);
+}
+
+#undef smtp_client_transaction_add_rcpt
+struct smtp_client_transaction_rcpt *
+smtp_client_transaction_add_rcpt(struct smtp_client_transaction *trans,
+ const struct smtp_address *rcpt_to,
+ const struct smtp_params_rcpt *rcpt_params,
+ smtp_client_command_callback_t *rcpt_callback,
+ smtp_client_command_callback_t *data_callback,
+ void *context)
+{
+ struct smtp_client_transaction_rcpt *rcpt;
+ pool_t pool;
+
+ e_debug(trans->event, "Add recipient");
+
+ i_assert(!trans->data_provided);
+ i_assert(!trans->reset);
+
+ i_assert(trans->state < SMTP_CLIENT_TRANSACTION_STATE_FINISHED);
+
+ if (trans->mail_head == NULL &&
+ trans->state == SMTP_CLIENT_TRANSACTION_STATE_MAIL_FROM)
+ trans->state = SMTP_CLIENT_TRANSACTION_STATE_RCPT_TO;
+
+ pool = pool_alloconly_create("smtp transaction rcpt", 512);
+ rcpt = smtp_client_transaction_rcpt_new(trans, pool,
+ rcpt_to, rcpt_params);
+ pool_unref(&pool);
+
+ rcpt->rcpt_callback = rcpt_callback;
+ rcpt->context = context;
+
+ rcpt->data_callback = data_callback;
+ rcpt->data_context = context;
+
+ smtp_client_transaction_submit(trans, FALSE);
+
+ return rcpt;
+}
+
+#undef smtp_client_transaction_add_pool_rcpt
+struct smtp_client_transaction_rcpt *
+smtp_client_transaction_add_pool_rcpt(
+ struct smtp_client_transaction *trans, pool_t pool,
+ const struct smtp_address *rcpt_to,
+ const struct smtp_params_rcpt *rcpt_params,
+ smtp_client_command_callback_t *rcpt_callback, void *context)
+{
+ struct smtp_client_transaction_rcpt *rcpt;
+
+ e_debug(trans->event, "Add recipient (external pool)");
+
+ i_assert(!trans->data_provided);
+ i_assert(!trans->reset);
+
+ i_assert(trans->state < SMTP_CLIENT_TRANSACTION_STATE_FINISHED);
+
+ if (trans->mail_head == NULL &&
+ trans->state == SMTP_CLIENT_TRANSACTION_STATE_MAIL_FROM)
+ trans->state = SMTP_CLIENT_TRANSACTION_STATE_RCPT_TO;
+
+ rcpt = smtp_client_transaction_rcpt_new(trans, pool,
+ rcpt_to, rcpt_params);
+ rcpt->rcpt_callback = rcpt_callback;
+ rcpt->context = context;
+ rcpt->external_pool = TRUE;
+
+ smtp_client_transaction_submit(trans, FALSE);
+
+ return rcpt;
+}
+
+static void
+smtp_client_transaction_data_cb(const struct smtp_reply *reply,
+ struct smtp_client_transaction *trans)
+{
+ bool reply_per_rcpt = HAS_ALL_BITS(
+ trans->flags, SMTP_CLIENT_TRANSACTION_FLAG_REPLY_PER_RCPT);
+
+ i_assert(!trans->reset);
+
+ smtp_client_transaction_ref(trans);
+
+ if (trans->data_input != NULL) {
+ event_add_int(trans->event, "data_sent",
+ trans->data_input->v_offset);
+ i_stream_unref(&trans->data_input);
+ }
+
+ if (reply_per_rcpt &&
+ trans->cmd_data != NULL && /* NULL when failed early */
+ trans->rcpts_data == NULL && trans->rcpts_count > 0) {
+ smtp_client_command_set_replies(trans->cmd_data,
+ trans->rcpts_count);
+ }
+ while (trans->rcpts_data != NULL) {
+ struct smtp_client_transaction_rcpt *rcpt = trans->rcpts_data;
+
+ trans->rcpts_data = trans->rcpts_data->next;
+ smtp_client_transaction_rcpt_finished(rcpt, reply);
+ if (HAS_ALL_BITS(trans->flags,
+ SMTP_CLIENT_TRANSACTION_FLAG_REPLY_PER_RCPT))
+ break;
+ }
+
+ if (reply_per_rcpt && trans->rcpts_count > 1 &&
+ !smtp_reply_is_success(reply) && trans->data_failure == NULL)
+ trans->data_failure = smtp_reply_clone(trans->pool, reply);
+ if (trans->rcpts_data != NULL) {
+ smtp_client_transaction_unref(&trans);
+ return;
+ }
+
+ trans->cmd_data = NULL;
+
+ if (trans->data_callback != NULL)
+ trans->data_callback(reply, trans->data_context);
+ trans->data_callback = NULL;
+
+ /* finished */
+ smtp_client_transaction_finish(
+ trans, (trans->data_failure == NULL ?
+ reply : trans->data_failure));
+
+ smtp_client_transaction_unref(&trans);
+}
+
+static void
+smtp_client_transaction_send_data(struct smtp_client_transaction *trans)
+{
+ struct smtp_reply failure;
+
+ i_assert(!trans->reset);
+ i_assert(trans->data_input != NULL);
+
+ e_debug(trans->event, "Sending data");
+
+ timeout_remove(&trans->to_send);
+
+ i_zero(&failure);
+ if (trans->failure != NULL) {
+ smtp_client_transaction_fail_reply(trans, trans->failure);
+ failure = *trans->failure;
+ i_assert(failure.status != 0);
+ } else if ((trans->rcpts_count + trans->rcpts_queue_count) == 0) {
+ e_debug(trans->event, "No valid recipients");
+ if (trans->failure != NULL)
+ failure = *trans->failure;
+ else {
+ smtp_reply_init(&failure, 554, "No valid recipients");
+ failure.enhanced_code = SMTP_REPLY_ENH_CODE(5, 5, 0);
+ }
+ i_assert(failure.status != 0);
+ } else {
+ i_assert(trans->conn != NULL);
+
+ trans->cmd_data = smtp_client_command_data_submit_after(
+ trans->conn, 0, trans->cmd_last, trans->data_input,
+ smtp_client_transaction_data_cb, trans);
+ trans->submitted_data = TRUE;
+
+ if (trans->cmd_last != NULL)
+ smtp_client_command_unlock(trans->cmd_last);
+
+ smtp_client_transaction_try_complete(trans);
+ }
+
+ if (trans->cmd_plug != NULL)
+ smtp_client_command_abort(&trans->cmd_plug);
+ trans->cmd_last = NULL;
+
+ if (failure.status != 0)
+ smtp_client_transaction_finish(trans, &failure);
+}
+
+#undef smtp_client_transaction_send
+void smtp_client_transaction_send(
+ struct smtp_client_transaction *trans, struct istream *data_input,
+ smtp_client_command_callback_t *data_callback, void *data_context)
+{
+ i_assert(trans->state < SMTP_CLIENT_TRANSACTION_STATE_FINISHED);
+ i_assert(!trans->data_provided);
+ i_assert(!trans->reset);
+
+ if (trans->rcpts_queue_count == 0)
+ e_debug(trans->event, "Got all RCPT replies");
+
+ e_debug(trans->event, "Send");
+
+ trans->data_provided = TRUE;
+
+ i_assert(trans->data_input == NULL);
+ trans->data_input = i_stream_create_crlf(data_input);
+
+ trans->data_callback = data_callback;
+ trans->data_context = data_context;
+
+ if (trans->finish_timeout_msecs > 0) {
+ i_assert(trans->to_finish == NULL);
+ trans->to_finish = timeout_add(trans->finish_timeout_msecs,
+ smtp_client_transaction_timeout,
+ trans);
+ }
+
+ smtp_client_transaction_submit(trans, TRUE);
+}
+
+static void
+smtp_client_transaction_rset_cb(const struct smtp_reply *reply,
+ struct smtp_client_transaction *trans)
+{
+ smtp_client_transaction_ref(trans);
+
+ trans->cmd_rset = NULL;
+
+ if (trans->reset_callback != NULL)
+ trans->reset_callback(reply, trans->reset_context);
+ trans->reset_callback = NULL;
+
+ /* Finished */
+ smtp_client_transaction_finish(trans, reply);
+
+ smtp_client_transaction_unref(&trans);
+}
+
+static void
+smtp_client_transaction_send_reset(struct smtp_client_transaction *trans)
+{
+ struct smtp_reply failure;
+
+ i_assert(trans->reset);
+
+ e_debug(trans->event, "Sending reset");
+
+ timeout_remove(&trans->to_send);
+
+ i_zero(&failure);
+ if (trans->failure != NULL) {
+ smtp_client_transaction_fail_reply(trans, trans->failure);
+ failure = *trans->failure;
+ i_assert(failure.status != 0);
+ } else {
+ i_assert(trans->conn != NULL);
+
+ trans->cmd_rset = smtp_client_command_rset_submit_after(
+ trans->conn, 0, trans->cmd_last,
+ smtp_client_transaction_rset_cb, trans);
+
+ if (trans->cmd_last != NULL)
+ smtp_client_command_unlock(trans->cmd_last);
+
+ smtp_client_transaction_try_complete(trans);
+ }
+
+ if (trans->cmd_plug != NULL)
+ smtp_client_command_abort(&trans->cmd_plug);
+ trans->cmd_last = NULL;
+
+ if (failure.status != 0)
+ smtp_client_transaction_finish(trans, &failure);
+}
+
+#undef smtp_client_transaction_reset
+void smtp_client_transaction_reset(
+ struct smtp_client_transaction *trans,
+ smtp_client_command_callback_t *reset_callback, void *reset_context)
+{
+ i_assert(trans->state < SMTP_CLIENT_TRANSACTION_STATE_FINISHED);
+ i_assert(!trans->data_provided);
+ i_assert(!trans->reset);
+
+ e_debug(trans->event, "Reset");
+
+ trans->reset = TRUE;
+
+ trans->reset_callback = reset_callback;
+ trans->reset_context = reset_context;
+
+ if (trans->finish_timeout_msecs > 0) {
+ i_assert(trans->to_finish == NULL);
+ trans->to_finish = timeout_add(trans->finish_timeout_msecs,
+ smtp_client_transaction_timeout,
+ trans);
+ }
+
+ smtp_client_transaction_submit(trans, TRUE);
+}
+
+static void
+smtp_client_transaction_do_submit_more(struct smtp_client_transaction *trans)
+{
+ timeout_remove(&trans->to_send);
+
+ /* Check whether we already failed */
+ if (trans->failure == NULL &&
+ trans->state > SMTP_CLIENT_TRANSACTION_STATE_MAIL_FROM)
+ trans->failure = trans->mail_failure;
+ if (trans->failure != NULL) {
+ smtp_client_transaction_fail_reply(trans, trans->failure);
+ return;
+ }
+
+ i_assert(trans->conn != NULL);
+
+ /* Make sure transaction is started */
+ if (trans->state == SMTP_CLIENT_TRANSACTION_STATE_NEW) {
+ enum smtp_client_transaction_state state;
+ struct smtp_client_transaction *tmp_trans = trans;
+
+ smtp_client_transaction_ref(tmp_trans);
+ smtp_client_transaction_start(tmp_trans, NULL, NULL);
+ state = trans->state;
+ smtp_client_transaction_unref(&tmp_trans);
+ if (state >= SMTP_CLIENT_TRANSACTION_STATE_FINISHED)
+ return;
+ }
+
+ if (trans->state <= SMTP_CLIENT_TRANSACTION_STATE_PENDING) {
+ if (trans->cmd_last == NULL) {
+ trans->cmd_plug = trans->cmd_last =
+ smtp_client_command_plug(trans->conn, NULL);
+ }
+ return;
+ }
+
+ /* MAIL */
+ if (trans->mail_send != NULL) {
+ e_debug(trans->event, "Sending MAIL command");
+
+ i_assert(trans->state ==
+ SMTP_CLIENT_TRANSACTION_STATE_MAIL_FROM);
+
+ if (trans->cmd_last != NULL)
+ smtp_client_command_unlock(trans->cmd_last);
+
+ while (trans->mail_send != NULL) {
+ struct smtp_client_transaction_mail *mail =
+ trans->mail_send;
+
+ trans->mail_send = trans->mail_send->next;
+ mail->cmd_mail_from = trans->cmd_last =
+ smtp_client_command_mail_submit_after(
+ trans->conn, 0, trans->cmd_last,
+ mail->mail_from, &mail->mail_params,
+ smtp_client_transaction_mail_cb, trans);
+ }
+ } else if (trans->immediate)
+ trans->cmd_last = NULL;
+
+ /* RCPT */
+ if (trans->rcpts_send != NULL) {
+ e_debug(trans->event, "Sending recipients");
+
+ if (trans->cmd_last != NULL)
+ smtp_client_command_unlock(trans->cmd_last);
+
+ while (trans->rcpts_send != NULL) {
+ struct smtp_client_transaction_rcpt *rcpt =
+ trans->rcpts_send;
+
+ trans->rcpts_send = trans->rcpts_send->next;
+ rcpt->cmd_rcpt_to = trans->cmd_last =
+ smtp_client_command_rcpt_submit_after(
+ trans->conn, 0, trans->cmd_last,
+ rcpt->rcpt_to, &rcpt->rcpt_params,
+ smtp_client_transaction_rcpt_cb, rcpt);
+ }
+ }
+
+ if (trans->cmd_plug != NULL &&
+ (trans->immediate || trans->cmd_last != trans->cmd_plug))
+ smtp_client_command_abort(&trans->cmd_plug);
+ if (trans->cmd_last != NULL && !trans->immediate)
+ smtp_client_command_lock(trans->cmd_last);
+
+ /* DATA / RSET */
+ if (trans->reset) {
+ smtp_client_transaction_send_reset(trans);
+ } else if (trans->data_input != NULL) {
+ smtp_client_transaction_send_data(trans);
+ }
+}
+
+static void
+smtp_client_transaction_submit_more(struct smtp_client_transaction *trans)
+{
+ smtp_client_transaction_ref(trans);
+ trans->submitting = TRUE;
+ smtp_client_transaction_do_submit_more(trans);
+ trans->submitting = FALSE;
+ smtp_client_transaction_unref(&trans);
+}
+
+static void
+smtp_client_transaction_submit(struct smtp_client_transaction *trans,
+ bool start)
+{
+ if (trans->failure == NULL && !start &&
+ trans->state <= SMTP_CLIENT_TRANSACTION_STATE_PENDING) {
+ /* Cannot submit commands at this time */
+ return;
+ }
+
+ if (trans->immediate) {
+ /* Submit immediately if not failed already: avoid calling
+ failure callbacks directly (which is the first thing
+ smtp_client_transaction_submit_more() would do). */
+ if (trans->failure == NULL &&
+ trans->state > SMTP_CLIENT_TRANSACTION_STATE_MAIL_FROM)
+ trans->failure = trans->mail_failure;
+ if (trans->failure == NULL) {
+ smtp_client_transaction_submit_more(trans);
+ return;
+ }
+ }
+
+ if (trans->to_send != NULL) {
+ /* Already scheduled command submission */
+ return;
+ }
+
+ trans->to_send = timeout_add_short(0,
+ smtp_client_transaction_submit_more, trans);
+}
+
+static void
+smtp_client_transaction_try_complete(struct smtp_client_transaction *trans)
+{
+ i_assert(trans->conn != NULL);
+
+ if (trans->rcpts_queue_count > 0) {
+ /* Not all RCPT replies have come in yet */
+ e_debug(trans->event, "RCPT replies are still pending (%u/%u)",
+ trans->rcpts_queue_count,
+ (trans->rcpts_queue_count + trans->rcpts_count));
+ return;
+ }
+ if (!trans->data_provided && !trans->reset) {
+ /* Still waiting for application to issue either
+ smtp_client_transaction_send() or
+ smtp_client_transaction_reset() */
+ e_debug(trans->event, "Transaction is not yet complete");
+ return;
+ }
+
+ if (trans->state == SMTP_CLIENT_TRANSACTION_STATE_RCPT_TO) {
+ /* Completed at this instance */
+ e_debug(trans->event,
+ "Got all RCPT replies and transaction is complete");
+ }
+
+ if (trans->reset) {
+ /* Entering reset state */
+ trans->state = SMTP_CLIENT_TRANSACTION_STATE_RESET;
+
+ if (trans->cmd_rset == NULL)
+ return;
+ } else {
+ /* Entering data state */
+ trans->state = SMTP_CLIENT_TRANSACTION_STATE_DATA;
+
+ if (trans->rcpts_count == 0) {
+ /* abort transaction if all recipients failed */
+ smtp_client_transaction_abort(trans);
+ return;
+ }
+
+ if (trans->cmd_data == NULL)
+ return;
+
+ if (HAS_ALL_BITS(trans->flags,
+ SMTP_CLIENT_TRANSACTION_FLAG_REPLY_PER_RCPT)) {
+ smtp_client_command_set_replies(trans->cmd_data,
+ trans->rcpts_count);
+ }
+ }
+
+ /* Got replies for all recipients and submitted our last command;
+ the next transaction can submit its commands now. */
+ smtp_client_connection_next_transaction(trans->conn, trans);
+}
+
+void smtp_client_transaction_set_immediate(
+ struct smtp_client_transaction *trans, bool immediate)
+{
+ trans->immediate = immediate;
+}
+
+void smtp_client_transaction_connection_result(
+ struct smtp_client_transaction *trans,
+ const struct smtp_reply *reply)
+{
+ if (!smtp_reply_is_success(reply)) {
+ if (trans->state <= SMTP_CLIENT_TRANSACTION_STATE_PENDING) {
+ e_debug(trans->event, "Failed to connect: %s",
+ smtp_reply_log(reply));
+ } else {
+ e_debug(trans->event, "Connection lost: %s",
+ smtp_reply_log(reply));
+ }
+ smtp_client_transaction_fail_reply(trans, reply);
+ return;
+ }
+
+ smtp_client_transaction_connection_ready(trans);
+}
+
+void smtp_client_transaction_connection_destroyed(
+ struct smtp_client_transaction *trans)
+{
+ i_assert(trans->failure != NULL);
+ smtp_client_connection_unref(&trans->conn);
+}
+
+const struct smtp_client_transaction_times *
+smtp_client_transaction_get_times(struct smtp_client_transaction *trans)
+{
+ return &trans->times;
+}
+
+enum smtp_client_transaction_state
+smtp_client_transaction_get_state(struct smtp_client_transaction *trans)
+{
+ return trans->state;
+}
+
+const char *
+smtp_client_transaction_get_state_name(struct smtp_client_transaction *trans)
+{
+ i_assert(trans->state >= SMTP_CLIENT_TRANSACTION_STATE_NEW &&
+ trans->state <= SMTP_CLIENT_TRANSACTION_STATE_ABORTED);
+ return smtp_client_transaction_state_names[trans->state];
+}
+
+const char *
+smtp_client_transaction_get_state_destription(
+ struct smtp_client_transaction *trans)
+{
+ enum smtp_client_connection_state conn_state;
+
+ switch (trans->state) {
+ case SMTP_CLIENT_TRANSACTION_STATE_NEW:
+ break;
+ case SMTP_CLIENT_TRANSACTION_STATE_PENDING:
+ i_assert(trans->conn != NULL);
+ conn_state = smtp_client_connection_get_state(trans->conn);
+ switch (conn_state) {
+ case SMTP_CLIENT_CONNECTION_STATE_CONNECTING:
+ case SMTP_CLIENT_CONNECTION_STATE_HANDSHAKING:
+ case SMTP_CLIENT_CONNECTION_STATE_AUTHENTICATING:
+ return smtp_client_connection_state_names[conn_state];
+ case SMTP_CLIENT_CONNECTION_STATE_TRANSACTION:
+ return "waiting for connection";
+ case SMTP_CLIENT_CONNECTION_STATE_DISCONNECTED:
+ case SMTP_CLIENT_CONNECTION_STATE_READY:
+ default:
+ break;
+ }
+ break;
+ case SMTP_CLIENT_TRANSACTION_STATE_MAIL_FROM:
+ return "waiting for reply to MAIL FROM";
+ case SMTP_CLIENT_TRANSACTION_STATE_RCPT_TO:
+ return "waiting for reply to RCPT TO";
+ case SMTP_CLIENT_TRANSACTION_STATE_DATA:
+ return "waiting for reply to DATA";
+ case SMTP_CLIENT_TRANSACTION_STATE_RESET:
+ return "waiting for reply to RESET";
+ case SMTP_CLIENT_TRANSACTION_STATE_FINISHED:
+ return "finished";
+ case SMTP_CLIENT_TRANSACTION_STATE_ABORTED:
+ return "aborted";
+ }
+ i_unreached();
+}
+
+void smtp_client_transaction_switch_ioloop(
+ struct smtp_client_transaction *trans)
+{
+ if (trans->to_send != NULL)
+ trans->to_send = io_loop_move_timeout(&trans->to_send);
+ if (trans->to_finish != NULL)
+ trans->to_finish = io_loop_move_timeout(&trans->to_finish);
+}
diff --git a/src/lib-smtp/smtp-client-transaction.h b/src/lib-smtp/smtp-client-transaction.h
new file mode 100644
index 0000000..d0cc58b
--- /dev/null
+++ b/src/lib-smtp/smtp-client-transaction.h
@@ -0,0 +1,259 @@
+#ifndef SMTP_CLIENT_TRANSACTION_H
+#define SMTP_CLIENT_TRANSACTION_H
+
+#include "net.h"
+#include "istream.h"
+
+struct smtp_address;
+struct smtp_client_transaction;
+struct smtp_client_transaction_mail;
+struct smtp_client_transaction_rcpt;
+
+enum smtp_client_transaction_flags {
+ SMTP_CLIENT_TRANSACTION_FLAG_REPLY_PER_RCPT = BIT(0),
+};
+
+enum smtp_client_transaction_state {
+ SMTP_CLIENT_TRANSACTION_STATE_NEW = 0,
+ SMTP_CLIENT_TRANSACTION_STATE_PENDING,
+ SMTP_CLIENT_TRANSACTION_STATE_MAIL_FROM,
+ SMTP_CLIENT_TRANSACTION_STATE_RCPT_TO,
+ SMTP_CLIENT_TRANSACTION_STATE_DATA,
+ SMTP_CLIENT_TRANSACTION_STATE_RESET,
+ SMTP_CLIENT_TRANSACTION_STATE_FINISHED,
+ SMTP_CLIENT_TRANSACTION_STATE_ABORTED
+ /* NOTE: keep synced with smtp_client_transaction_state_names[] */
+};
+extern const char *const smtp_client_transaction_state_names[];
+
+struct smtp_client_transaction_times {
+ struct timeval started;
+ struct timeval finished;
+};
+
+/* Called when the transaction is finished, either because the MAIL FROM
+ failed, all RCPT TOs failed or because all DATA replies have been
+ received. */
+typedef void
+smtp_client_transaction_callback_t(void *context);
+
+/* Create an empty transaction (i.e. even without the parameters for the
+ MAIL FROM command) */
+struct smtp_client_transaction *
+smtp_client_transaction_create_empty(
+ struct smtp_client_connection *conn,
+ enum smtp_client_transaction_flags flags,
+ smtp_client_transaction_callback_t *callback, void *context)
+ ATTR_NULL(4);
+#define smtp_client_transaction_create_empty(conn, flags, callback, context) \
+ smtp_client_transaction_create_empty(conn, flags - \
+ CALLBACK_TYPECHECK(callback, void (*)(typeof(context))), \
+ (smtp_client_transaction_callback_t *)callback, context)
+/* Create a new transaction, including the parameters for the MAIL FROM
+ command */
+struct smtp_client_transaction *
+smtp_client_transaction_create(struct smtp_client_connection *conn,
+ const struct smtp_address *mail_from,
+ const struct smtp_params_mail *mail_params,
+ enum smtp_client_transaction_flags flags,
+ smtp_client_transaction_callback_t *callback, void *context)
+ ATTR_NULL(2, 3, 6);
+#define smtp_client_transaction_create(conn, \
+ mail_from, mail_params, flags, callback, context) \
+ smtp_client_transaction_create(conn, mail_from, mail_params, flags - \
+ CALLBACK_TYPECHECK(callback, void (*)(typeof(context))), \
+ (smtp_client_transaction_callback_t *)callback, context)
+
+void smtp_client_transaction_ref(struct smtp_client_transaction *trans);
+void smtp_client_transaction_unref(struct smtp_client_transaction **_trans);
+void smtp_client_transaction_destroy(struct smtp_client_transaction **trans);
+
+void smtp_client_transaction_abort(struct smtp_client_transaction *trans);
+void smtp_client_transaction_fail_reply(struct smtp_client_transaction *trans,
+ const struct smtp_reply *reply);
+void smtp_client_transaction_fail(struct smtp_client_transaction *trans,
+ unsigned int status, const char *error);
+
+void smtp_client_transaction_set_event(struct smtp_client_transaction *trans,
+ struct event *event);
+void smtp_client_transaction_set_timeout(struct smtp_client_transaction *trans,
+ unsigned int timeout_msecs);
+
+/* Start the transaction with a MAIL command. The mail_from_callback is
+ called once the server replies to the MAIL FROM command. Calling this
+ function is not mandatory; it is called implicitly by
+ smtp_client_transaction_send() if the transaction wasn't already started.
+ */
+void smtp_client_transaction_start(struct smtp_client_transaction *trans,
+ smtp_client_command_callback_t *mail_callback, void *context);
+#define smtp_client_transaction_start(trans, mail_callback, context) \
+ smtp_client_transaction_start(trans, \
+ (smtp_client_command_callback_t *)mail_callback, TRUE ? context : \
+ CALLBACK_TYPECHECK(mail_callback, void (*)( \
+ const struct smtp_reply *reply, typeof(context))))
+/* Start the transaction with a MAIL command. This function allows providing the
+ parameters for the MAIL FROM command for when the transaction was created
+ empty. The mail_from_callback is called once the server replies to the MAIL
+ FROM command. Calling this function is not mandatory; it is called implicitly
+ by smtp_client_transaction_send() if the transaction wasn't already started.
+ In that case, the NULL sender ("<>") will be used when the transaction was
+ created empty.
+ */
+void smtp_client_transaction_start_empty(
+ struct smtp_client_transaction *trans,
+ const struct smtp_address *mail_from,
+ const struct smtp_params_mail *mail_params,
+ smtp_client_command_callback_t *mail_callback, void *context);
+#define smtp_client_transaction_start_empty(trans, mail_from, mail_params, \
+ mail_callback, context) \
+ smtp_client_transaction_start_empty(trans, mail_from, mail_params, \
+ (smtp_client_command_callback_t *)mail_callback, TRUE ? context : \
+ CALLBACK_TYPECHECK(mail_callback, void (*)( \
+ const struct smtp_reply *reply, typeof(context))))
+
+/* Add an extra pipelined MAIL command to the transaction. The mail_callback is
+ called once the server replies to the MAIL command. This is usually only
+ useful for forwarding pipelined SMTP transactions, which can involve more
+ than a single MAIL command (e.g. to have an implicit fallback sender address
+ in the pipeline when the first one fails). Of course, only one MAIL command
+ will succeed and therefore error replies for the others will not abort the
+ transaction. This function returns a struct that can be used to abort the
+ MAIL command prematurely (see below). */
+struct smtp_client_transaction_mail *
+smtp_client_transaction_add_mail(struct smtp_client_transaction *trans,
+ const struct smtp_address *mail_from,
+ const struct smtp_params_mail *mail_params,
+ smtp_client_command_callback_t *mail_callback,
+ void *context)
+ ATTR_NOWARN_UNUSED_RESULT ATTR_NULL(3,5);
+#define smtp_client_transaction_add_mail(trans, \
+ mail_from, mail_params, mail_callback, context) \
+ smtp_client_transaction_add_mail(trans, mail_from - \
+ CALLBACK_TYPECHECK(mail_callback, void (*)( \
+ const struct smtp_reply *reply, typeof(context))), \
+ mail_params, \
+ (smtp_client_command_callback_t *)mail_callback, context)
+/* Abort the MAIL command prematurely. This function must not be called after
+ the mail_callback from smtp_client_transaction_add_mail() is called. */
+void smtp_client_transaction_mail_abort(
+ struct smtp_client_transaction_mail **_mail);
+
+/* Add recipient to the transaction with a RCPT TO command. The
+ rcpt_to_callback is called once the server replies to the RCPT TO command.
+ If RCPT TO succeeded, the data_callback is called once the server replies
+ to the DATA command. The data_callback will not be called until
+ smtp_client_transaction_send() is called for the transaction (see below).
+ Until that time, any failure is remembered. This function returns a struct
+ that can be used to abort the RCPT command prematurely (see below). This
+ struct must not be used after the rcpt_callback is called. */
+struct smtp_client_transaction_rcpt *
+smtp_client_transaction_add_rcpt(struct smtp_client_transaction *trans,
+ const struct smtp_address *rcpt_to,
+ const struct smtp_params_rcpt *rcpt_params,
+ smtp_client_command_callback_t *rcpt_callback,
+ smtp_client_command_callback_t *data_callback,
+ void *context)
+ ATTR_NOWARN_UNUSED_RESULT ATTR_NULL(3,5,6);
+#define smtp_client_transaction_add_rcpt(trans, \
+ rcpt_to, rcpt_params, rcpt_callback, data_callback, context) \
+ smtp_client_transaction_add_rcpt(trans, rcpt_to - \
+ CALLBACK_TYPECHECK(rcpt_callback, void (*)( \
+ const struct smtp_reply *reply, typeof(context))) - \
+ CALLBACK_TYPECHECK(data_callback, void (*)( \
+ const struct smtp_reply *reply, typeof(context))), \
+ rcpt_params, \
+ (smtp_client_command_callback_t *)rcpt_callback, \
+ (smtp_client_command_callback_t *)data_callback, context)
+/* Add recipient to the transaction with a RCPT TO command. The
+ rcpt_to_callback is called once the server replies to the RCPT TO command.
+ This function returns a struct that can be used to abort the RCPT command
+ prematurely (see below). This struct is allocated on the provided pool (the
+ pool is referenced) and remains valid until the destruction of the
+ transaction.
+ */
+struct smtp_client_transaction_rcpt *
+smtp_client_transaction_add_pool_rcpt(
+ struct smtp_client_transaction *trans, pool_t pool,
+ const struct smtp_address *rcpt_to,
+ const struct smtp_params_rcpt *rcpt_params,
+ smtp_client_command_callback_t *rcpt_callback, void *context)
+ ATTR_NOWARN_UNUSED_RESULT ATTR_NULL(4,6,7);
+#define smtp_client_transaction_add_pool_rcpt(trans, pool, \
+ rcpt_to, rcpt_params, rcpt_callback, context) \
+ smtp_client_transaction_add_pool_rcpt(trans, pool, rcpt_to - \
+ CALLBACK_TYPECHECK(rcpt_callback, void (*)( \
+ const struct smtp_reply *reply, typeof(context))), \
+ rcpt_params, \
+ (smtp_client_command_callback_t *)rcpt_callback, context)
+/* Abort the RCPT command prematurely. This function must not be called after
+ the rcpt_callback from smtp_client_transaction_add_rcpt() is called. */
+void smtp_client_transaction_rcpt_abort(
+ struct smtp_client_transaction_rcpt **_rcpt);
+/* Set the DATA callback for this recipient. If RCPT TO succeeded, the callback
+ is called once the server replies to the DATA command. Until that time, any
+ failure is remembered. The callback will not be called until
+ smtp_client_transaction_send() is called for the transaction (see below). */
+void smtp_client_transaction_rcpt_set_data_callback(
+ struct smtp_client_transaction_rcpt *rcpt,
+ smtp_client_command_callback_t *callback, void *context)
+ ATTR_NULL(3);
+#define smtp_client_transaction_rcpt_set_data_callback(trans, \
+ callback, context) \
+ smtp_client_transaction_rcpt_set_data_callback(trans, \
+ (smtp_client_command_callback_t *)callback, \
+ (TRUE ? context : \
+ CALLBACK_TYPECHECK(callback, void (*)( \
+ const struct smtp_reply *reply, typeof(context)))))
+
+/* Start sending input stream as DATA. This completes the transaction, which
+ means that any pending failures that got recorded before this function was
+ called will be triggered now. If any RCPT TO succeeded, the provided
+ data_callback is called once the server replies to the DATA command. This
+ callback is mainly useful for SMTP, for LMTP it will only yield the reply for
+ the last recipient. This function starts the transaction implicitly. */
+void smtp_client_transaction_send(
+ struct smtp_client_transaction *trans, struct istream *data_input,
+ smtp_client_command_callback_t *data_callback, void *data_context);
+#define smtp_client_transaction_send(trans, \
+ data_input, data_callback, data_context) \
+ smtp_client_transaction_send(trans, data_input - \
+ CALLBACK_TYPECHECK(data_callback, void (*)( \
+ const struct smtp_reply *reply, typeof(data_context))), \
+ (smtp_client_command_callback_t *)data_callback, data_context)
+
+/* Gracefully reset the transaction by sending the RSET command and waiting for
+ the response. This does not try to abort pending MAIL and RCPT commands,
+ allowing the transaction to be evaluated without proceeding with the DATA
+ command. */
+void smtp_client_transaction_reset(
+ struct smtp_client_transaction *trans,
+ smtp_client_command_callback_t *reset_callback, void *reset_context);
+#define smtp_client_transaction_reset(trans, reset_callback, reset_context) \
+ smtp_client_transaction_reset(trans, \
+ (smtp_client_command_callback_t *)reset_callback, \
+ TRUE ? reset_context : \
+ CALLBACK_TYPECHECK(reset_callback, void (*)( \
+ const struct smtp_reply *reply, typeof(reset_context))))
+
+/* Enables mode in which all commands are submitted immediately and (non-
+ transaction) commands can be interleaved. This is mainly important for
+ relaying SMTP in realtime. */
+void smtp_client_transaction_set_immediate(
+ struct smtp_client_transaction *trans, bool immediate);
+
+/* Return transaction statistics. */
+const struct smtp_client_transaction_times *
+smtp_client_transaction_get_times(struct smtp_client_transaction *trans);
+
+/* Return transaction state */
+enum smtp_client_transaction_state
+smtp_client_transaction_get_state(struct smtp_client_transaction *trans)
+ ATTR_PURE;
+const char *
+smtp_client_transaction_get_state_name(struct smtp_client_transaction *trans)
+ ATTR_PURE;
+const char *
+smtp_client_transaction_get_state_destription(
+ struct smtp_client_transaction *trans);
+
+#endif
diff --git a/src/lib-smtp/smtp-client.c b/src/lib-smtp/smtp-client.c
new file mode 100644
index 0000000..653dd0e
--- /dev/null
+++ b/src/lib-smtp/smtp-client.c
@@ -0,0 +1,142 @@
+/* Copyright (c) 2013-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "net.h"
+#include "str.h"
+#include "hash.h"
+#include "array.h"
+#include "ioloop.h"
+#include "istream.h"
+#include "ostream.h"
+#include "connection.h"
+#include "dns-lookup.h"
+#include "iostream-rawlog.h"
+#include "iostream-ssl.h"
+
+#include "smtp-client-private.h"
+
+#define SMTP_DEFAULT_PORT 80
+#define SSMTP_DEFAULT_PORT 465
+
+static struct event_category event_category_smtp_client = {
+ .name = "smtp-client"
+};
+
+/*
+ * Client
+ */
+
+struct smtp_client *smtp_client_init(const struct smtp_client_settings *set)
+{
+ struct smtp_client *client;
+ pool_t pool;
+
+ pool = pool_alloconly_create("smtp client", 1024);
+ client = p_new(pool, struct smtp_client, 1);
+ client->pool = pool;
+
+ client->set.my_ip = set->my_ip;
+ client->set.my_hostname = p_strdup(pool, set->my_hostname);
+
+ client->set.forced_capabilities = set->forced_capabilities;
+ if (set->extra_capabilities != NULL) {
+ client->set.extra_capabilities =
+ p_strarray_dup(pool, set->extra_capabilities);
+ }
+
+ client->set.dns_client = set->dns_client;
+ client->set.dns_client_socket_path =
+ p_strdup(pool, set->dns_client_socket_path);
+ client->set.rawlog_dir = p_strdup_empty(pool, set->rawlog_dir);
+
+ if (set->ssl != NULL) {
+ client->set.ssl =
+ ssl_iostream_settings_dup(client->pool, set->ssl);
+ }
+
+ client->set.master_user = p_strdup_empty(pool, set->master_user);
+ client->set.username = p_strdup_empty(pool, set->username);
+ client->set.sasl_mech = set->sasl_mech;
+ if (set->sasl_mech == NULL) {
+ client->set.sasl_mechanisms =
+ p_strdup(pool, set->sasl_mechanisms);
+ }
+
+ client->set.connect_timeout_msecs = set->connect_timeout_msecs != 0 ?
+ set->connect_timeout_msecs :
+ SMTP_DEFAULT_CONNECT_TIMEOUT_MSECS;
+ client->set.command_timeout_msecs = set->command_timeout_msecs != 0 ?
+ set->command_timeout_msecs :
+ SMTP_DEFAULT_COMMAND_TIMEOUT_MSECS;
+ client->set.max_reply_size = set->max_reply_size != 0 ?
+ set->max_reply_size : SMTP_DEFAULT_MAX_REPLY_SIZE;
+ client->set.max_data_chunk_size = set->max_data_chunk_size != 0 ?
+ set->max_data_chunk_size : SMTP_DEFAULT_MAX_DATA_CHUNK_SIZE;
+ client->set.max_data_chunk_pipeline = set->max_data_chunk_pipeline != 0 ?
+ set->max_data_chunk_pipeline : SMTP_DEFAULT_MAX_DATA_CHUNK_PIPELINE;
+
+ client->set.socket_send_buffer_size = set->socket_send_buffer_size;
+ client->set.socket_recv_buffer_size = set->socket_recv_buffer_size;
+ client->set.debug = set->debug;
+ client->set.verbose_user_errors = set->verbose_user_errors;
+
+ smtp_proxy_data_merge(pool, &client->set.proxy_data, &set->proxy_data);
+
+ client->conn_list = smtp_client_connection_list_init();
+
+ /* There is no event log prefix added here, since the client itself does
+ not log anything and the prefix is protocol-dependent. */
+ client->event = event_create(set->event_parent);
+ event_add_category(client->event, &event_category_smtp_client);
+ event_set_forced_debug(client->event, set->debug);
+
+ return client;
+}
+
+void smtp_client_deinit(struct smtp_client **_client)
+{
+ struct smtp_client *client = *_client;
+
+ connection_list_deinit(&client->conn_list);
+
+ if (client->ssl_ctx != NULL)
+ ssl_iostream_context_unref(&client->ssl_ctx);
+ event_unref(&client->event);
+ pool_unref(&client->pool);
+ *_client = NULL;
+}
+
+void smtp_client_switch_ioloop(struct smtp_client *client)
+{
+ struct connection *_conn = client->conn_list->connections;
+
+ /* move connections */
+ for (; _conn != NULL; _conn = _conn->next) {
+ struct smtp_client_connection *conn =
+ (struct smtp_client_connection *)_conn;
+
+ smtp_client_connection_switch_ioloop(conn);
+ }
+}
+
+int smtp_client_init_ssl_ctx(struct smtp_client *client, const char **error_r)
+{
+ const char *error;
+
+ if (client->ssl_ctx != NULL)
+ return 0;
+
+ if (client->set.ssl == NULL) {
+ *error_r = "Requested SSL connection, but no SSL settings given";
+ return -1;
+ }
+ if (ssl_iostream_client_context_cache_get(client->set.ssl,
+ &client->ssl_ctx, &error) < 0) {
+ *error_r = t_strdup_printf("Couldn't initialize SSL context: %s",
+ error);
+ return -1;
+ }
+ return 0;
+}
+
+// FIXME: Implement smtp_client_run()
diff --git a/src/lib-smtp/smtp-client.h b/src/lib-smtp/smtp-client.h
new file mode 100644
index 0000000..75608e9
--- /dev/null
+++ b/src/lib-smtp/smtp-client.h
@@ -0,0 +1,128 @@
+#ifndef SMTP_CLIENT_H
+#define SMTP_CLIENT_H
+
+#include "net.h"
+#include "smtp-common.h"
+#include "smtp-address.h"
+#include "smtp-reply.h"
+
+struct smtp_client;
+struct smtp_client_request;
+
+#define SMTP_DEFAULT_CONNECT_TIMEOUT_MSECS (1000*30)
+#define SMTP_DEFAULT_COMMAND_TIMEOUT_MSECS (1000*60*5)
+#define SMTP_DEFAULT_MAX_REPLY_SIZE (SIZE_MAX)
+#define SMTP_DEFAULT_MAX_DATA_CHUNK_SIZE NET_BLOCK_SIZE
+#define SMTP_DEFAULT_MAX_DATA_CHUNK_PIPELINE 4
+
+enum smtp_client_command_error {
+ /* Server closed the connection */
+ SMTP_CLIENT_COMMAND_ERROR_CONNECTION_CLOSED = 421,
+ /* The command was aborted */
+ SMTP_CLIENT_COMMAND_ERROR_ABORTED = 9000,
+ /* DNS lookup failed */
+ SMTP_CLIENT_COMMAND_ERROR_HOST_LOOKUP_FAILED,
+ /* Failed to establish the connection */
+ SMTP_CLIENT_COMMAND_ERROR_CONNECT_FAILED,
+ /* Failed to authenticate using the provided credentials */
+ SMTP_CLIENT_COMMAND_ERROR_AUTH_FAILED,
+ /* Lost the connection after initially succeeded */
+ SMTP_CLIENT_COMMAND_ERROR_CONNECTION_LOST,
+ /* Got an invalid reply from the server */
+ SMTP_CLIENT_COMMAND_ERROR_BAD_REPLY,
+ /* We sent a command with a payload stream that broke while reading
+ from it */
+ SMTP_CLIENT_COMMAND_ERROR_BROKEN_PAYLOAD,
+ /* The server failed to respond before the command timed out */
+ SMTP_CLIENT_COMMAND_ERROR_TIMED_OUT
+};
+
+struct smtp_client_capability_extra {
+ const char *name;
+
+ /* Send these additional custom MAIL parameters if available. */
+ const char *const *mail_param_extensions;
+ /* Send these additional custom RCPT parameters if available. */
+ const char *const *rcpt_param_extensions;
+};
+
+struct smtp_client_settings {
+ struct ip_addr my_ip;
+ const char *my_hostname;
+ const char *temp_path_prefix;
+
+ /* Capabilities that are assumed to be enabled no matter whether the
+ server indicates support. */
+ enum smtp_capability forced_capabilities;
+ /* Record these extra capabilities if returned in the EHLO response */
+ const char *const *extra_capabilities;
+
+ struct dns_client *dns_client;
+ const char *dns_client_socket_path;
+
+ const struct ssl_iostream_settings *ssl;
+
+ const char *master_user;
+ const char *username;
+ const char *password;
+ const struct dsasl_client_mech *sasl_mech;
+ /* Space-separated list of SASL mechanisms to try (in the specified
+ order). The default is to use only SASL PLAIN. */
+ const char *sasl_mechanisms;
+
+ const char *rawlog_dir;
+
+ /* Timeout for SMTP commands. Reset every time more data is being
+ sent or received.
+ (default = unlimited) */
+ unsigned int command_timeout_msecs;
+ /* Timeout for loggging in
+ (default = cmd_timeout_msecs) */
+ unsigned int connect_timeout_msecs;
+
+ /* Max total size of reply */
+ size_t max_reply_size;
+
+ /* Maximum BDAT chunk size for the CHUNKING capability */
+ uoff_t max_data_chunk_size;
+ /* Maximum pipelined BDAT commands */
+ unsigned int max_data_chunk_pipeline;
+
+ /* if remote server supports XCLIENT capability,
+ send this data */
+ struct smtp_proxy_data proxy_data;
+
+ /* the kernel send/receive buffer sizes used for the connection sockets.
+ Configuring this is mainly useful for the test suite. The kernel
+ defaults are used when these settings are 0. */
+ size_t socket_send_buffer_size;
+ size_t socket_recv_buffer_size;
+
+ /* Event to use as the parent for the smtp client/connection event. For
+ specific transactions this can be overridden with
+ smtp_client_transaction_set_event(). */
+ struct event *event_parent;
+
+ /* enable logging debug messages */
+ bool debug;
+ /* peer is trusted, so e.g. attempt sending XCLIENT data */
+ bool peer_trusted;
+ /* defer sending XCLIENT command until authentication or first mail
+ transaction. */
+ bool xclient_defer;
+ /* don't clear password after first successful authentication */
+ bool remember_password;
+ /* sending even broken MAIL command path (otherwise a broken address
+ is sent as <>) */
+ bool mail_send_broken_path;
+ /* Yield verbose user-visible errors for commands and connections that
+ failed locally. */
+ bool verbose_user_errors;
+};
+
+struct smtp_client *smtp_client_init(const struct smtp_client_settings *set);
+void smtp_client_deinit(struct smtp_client **_client);
+
+void smtp_client_switch_ioloop(struct smtp_client *client);
+
+#endif
diff --git a/src/lib-smtp/smtp-command-parser.c b/src/lib-smtp/smtp-command-parser.c
new file mode 100644
index 0000000..f50abc9
--- /dev/null
+++ b/src/lib-smtp/smtp-command-parser.c
@@ -0,0 +1,613 @@
+/* Copyright (c) 2013-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "buffer.h"
+#include "unichar.h"
+#include "istream.h"
+#include "istream-failure-at.h"
+#include "istream-sized.h"
+#include "istream-dot.h"
+
+#include "smtp-parser.h"
+#include "smtp-command-parser.h"
+
+#include <ctype.h>
+
+#define SMTP_COMMAND_PARSER_MAX_COMMAND_LENGTH 32
+
+enum smtp_command_parser_state {
+ SMTP_COMMAND_PARSE_STATE_INIT = 0,
+ SMTP_COMMAND_PARSE_STATE_SKIP_LINE,
+ SMTP_COMMAND_PARSE_STATE_COMMAND,
+ SMTP_COMMAND_PARSE_STATE_SP,
+ SMTP_COMMAND_PARSE_STATE_PARAMETERS,
+ SMTP_COMMAND_PARSE_STATE_CR,
+ SMTP_COMMAND_PARSE_STATE_LF,
+ SMTP_COMMAND_PARSE_STATE_ERROR,
+};
+
+struct smtp_command_parser_state_data {
+ enum smtp_command_parser_state state;
+
+ char *cmd_name;
+ char *cmd_params;
+
+ size_t poff;
+};
+
+struct smtp_command_parser {
+ struct istream *input;
+
+ struct smtp_command_limits limits;
+
+ const unsigned char *cur, *end;
+ buffer_t *line_buffer;
+ struct istream *data;
+
+ struct smtp_command_parser_state_data state;
+
+ enum smtp_command_parse_error error_code;
+ char *error;
+
+ bool auth_response:1;
+};
+
+static inline void ATTR_FORMAT(3, 4)
+smtp_command_parser_error(struct smtp_command_parser *parser,
+ enum smtp_command_parse_error code,
+ const char *format, ...)
+{
+ va_list args;
+
+ parser->state.state = SMTP_COMMAND_PARSE_STATE_ERROR;
+
+ i_free(parser->error);
+ parser->error_code = code;
+
+ va_start(args, format);
+ parser->error = i_strdup_vprintf(format, args);
+ va_end(args);
+}
+
+struct smtp_command_parser *
+smtp_command_parser_init(struct istream *input,
+ const struct smtp_command_limits *limits)
+{
+ struct smtp_command_parser *parser;
+
+ parser = i_new(struct smtp_command_parser, 1);
+ parser->input = input;
+ i_stream_ref(input);
+
+ if (limits != NULL)
+ parser->limits = *limits;
+ if (parser->limits.max_parameters_size == 0) {
+ parser->limits.max_parameters_size =
+ SMTP_COMMAND_DEFAULT_MAX_PARAMETERS_SIZE;
+ }
+ if (parser->limits.max_auth_size == 0) {
+ parser->limits.max_auth_size =
+ SMTP_COMMAND_DEFAULT_MAX_AUTH_SIZE;
+ }
+ if (parser->limits.max_data_size == 0) {
+ parser->limits.max_data_size =
+ SMTP_COMMAND_DEFAULT_MAX_DATA_SIZE;
+ }
+
+ return parser;
+}
+
+void smtp_command_parser_deinit(struct smtp_command_parser **_parser)
+{
+ struct smtp_command_parser *parser = *_parser;
+
+ i_stream_unref(&parser->data);
+ buffer_free(&parser->line_buffer);
+ i_free(parser->state.cmd_name);
+ i_free(parser->state.cmd_params);
+ i_free(parser->error);
+ i_stream_unref(&parser->input);
+ i_free(parser);
+ *_parser = NULL;
+}
+
+static void smtp_command_parser_restart(struct smtp_command_parser *parser)
+{
+ buffer_free(&parser->line_buffer);
+ i_free(parser->state.cmd_name);
+ i_free(parser->state.cmd_params);
+
+ i_zero(&parser->state);
+}
+
+void smtp_command_parser_set_stream(struct smtp_command_parser *parser,
+ struct istream *input)
+{
+ i_stream_unref(&parser->input);
+ if (input != NULL) {
+ parser->input = input;
+ i_stream_ref(parser->input);
+ }
+}
+
+static inline const char *_chr_sanitize(unsigned char c)
+{
+ if (c >= 0x20 && c < 0x7F)
+ return t_strdup_printf("`%c'", c);
+ if (c == 0x0a)
+ return "<LF>";
+ if (c == 0x0d)
+ return "<CR>";
+ return t_strdup_printf("<0x%02x>", c);
+}
+
+static int smtp_command_parse_identifier(struct smtp_command_parser *parser)
+{
+ const unsigned char *p;
+
+ /* The commands themselves are alphabetic characters.
+ */
+ p = parser->cur + parser->state.poff;
+ i_assert(p <= parser->end);
+ while (p < parser->end && i_isalpha(*p))
+ p++;
+ if ((p - parser->cur) > SMTP_COMMAND_PARSER_MAX_COMMAND_LENGTH) {
+ smtp_command_parser_error(
+ parser, SMTP_COMMAND_PARSE_ERROR_BAD_COMMAND,
+ "Command name is too long");
+ return -1;
+ }
+ parser->state.poff = p - parser->cur;
+ if (p == parser->end)
+ return 0;
+ parser->state.cmd_name = i_strdup_until(parser->cur, p);
+ parser->cur = p;
+ parser->state.poff = 0;
+ return 1;
+}
+
+static int smtp_command_parse_parameters(struct smtp_command_parser *parser)
+{
+ const unsigned char *p, *mp;
+ size_t max_size = (parser->auth_response ?
+ parser->limits.max_auth_size :
+ parser->limits.max_parameters_size);
+ size_t buf_size = (parser->line_buffer == NULL ?
+ 0 : parser->line_buffer->used);
+ int nch = 1;
+
+ i_assert(max_size == 0 || buf_size <= max_size);
+ if (max_size > 0 && buf_size == max_size) {
+ smtp_command_parser_error(
+ parser, SMTP_COMMAND_PARSE_ERROR_LINE_TOO_LONG,
+ "%s line is too long",
+ (parser->auth_response ? "AUTH response" : "Command"));
+ return -1;
+ }
+
+ /* We assume parameters to match textstr (HT, SP, Printable US-ASCII).
+ For command parameters, we also accept valid UTF-8 characters.
+ */
+ p = parser->cur + parser->state.poff;
+ while (p < parser->end) {
+ unichar_t ch;
+
+ if (parser->auth_response)
+ ch = *p;
+ else {
+ nch = uni_utf8_get_char_n(p, (size_t)(parser->end - p),
+ &ch);
+ }
+ if (nch == 0)
+ break;
+ if (nch < 0) {
+ smtp_command_parser_error(
+ parser, SMTP_COMMAND_PARSE_ERROR_BAD_COMMAND,
+ "Invalid UTF-8 character in command parameters");
+ return -1;
+ }
+ if (nch == 1 && !smtp_char_is_textstr((unsigned char)ch))
+ break;
+ p += nch;
+ }
+ if (max_size > 0 && (size_t)(p - parser->cur) > (max_size - buf_size)) {
+ smtp_command_parser_error(
+ parser, SMTP_COMMAND_PARSE_ERROR_LINE_TOO_LONG,
+ "%s line is too long",
+ (parser->auth_response ? "AUTH response" : "Command"));
+ return -1;
+ }
+ parser->state.poff = p - parser->cur;
+ if (p == parser->end || nch == 0) {
+ /* Parsed up to end of what is currently buffered in the input
+ stream. */
+ unsigned int ch_size = (p == parser->end ?
+ 0 : uni_utf8_char_bytes(*p));
+ size_t max_input = i_stream_get_max_buffer_size(parser->input);
+
+ /* Move parsed data to parser's line buffer if the input stream
+ buffer is full. This can happen when the parser's limits
+ exceed the input stream max buffer size. */
+ if ((parser->state.poff + ch_size) >= max_input) {
+ if (parser->line_buffer == NULL) {
+ buf_size = (max_input < SIZE_MAX / 2 ?
+ max_input * 2 : SIZE_MAX);
+ buf_size = I_MAX(buf_size, 2048);
+ buf_size = I_MIN(buf_size, max_size);
+
+ parser->line_buffer = buffer_create_dynamic(
+ default_pool, buf_size);
+ }
+ buffer_append(parser->line_buffer, parser->cur,
+ (p - parser->cur));
+
+ parser->cur = p;
+ parser->state.poff = 0;
+ }
+ return 0;
+ }
+
+ /* In the interest of improved interoperability, SMTP receivers SHOULD
+ tolerate trailing white space before the terminating <CRLF>.
+
+ WSP = SP / HTAB ; white space
+
+ --> Trim the end of the buffer
+ */
+ mp = p;
+ if (mp > parser->cur) {
+ while (mp > parser->cur && (*(mp-1) == ' ' || *(mp-1) == '\t'))
+ mp--;
+ }
+
+ if (!parser->auth_response && mp > parser->cur && *parser->cur == ' ') {
+ smtp_command_parser_error(
+ parser, SMTP_COMMAND_PARSE_ERROR_BAD_COMMAND,
+ "Duplicate space after command name");
+ return -1;
+ }
+
+ if (parser->line_buffer == NULL) {
+ /* Buffered only in input stream */
+ parser->state.cmd_params = i_strdup_until(parser->cur, mp);
+ } else {
+ /* Buffered also in the parser */
+ buffer_append(parser->line_buffer, parser->cur,
+ (mp - parser->cur));
+ parser->state.cmd_params =
+ buffer_free_without_data(&parser->line_buffer);
+ }
+ parser->cur = p;
+ parser->state.poff = 0;
+ return 1;
+}
+
+static int smtp_command_parse_line(struct smtp_command_parser *parser)
+{
+ int ret;
+
+ /* RFC 5321, Section 4.1.1:
+
+ SMTP commands are character strings terminated by <CRLF>. The
+ commands themselves are alphabetic characters terminated by <SP> if
+ parameters follow and <CRLF> otherwise. (In the interest of improved
+ interoperability, SMTP receivers SHOULD tolerate trailing white space
+ before the terminating <CRLF>.)
+ */
+ for (;;) {
+ switch (parser->state.state) {
+ case SMTP_COMMAND_PARSE_STATE_INIT:
+ smtp_command_parser_restart(parser);
+ if (parser->auth_response) {
+ /* Parse AUTH response as bare parameters */
+ parser->state.state =
+ SMTP_COMMAND_PARSE_STATE_PARAMETERS;
+ } else {
+ parser->state.state =
+ SMTP_COMMAND_PARSE_STATE_COMMAND;
+ }
+ if (parser->cur == parser->end)
+ return 0;
+ if (parser->auth_response)
+ break;
+ /* fall through */
+ case SMTP_COMMAND_PARSE_STATE_COMMAND:
+ ret = smtp_command_parse_identifier(parser);
+ if (ret <= 0)
+ return ret;
+ parser->state.state = SMTP_COMMAND_PARSE_STATE_SP;
+ if (parser->cur == parser->end)
+ return 0;
+ /* fall through */
+ case SMTP_COMMAND_PARSE_STATE_SP:
+ if (*parser->cur == '\r') {
+ parser->state.state =
+ SMTP_COMMAND_PARSE_STATE_CR;
+ break;
+ } else if (*parser->cur == '\n') {
+ parser->state.state =
+ SMTP_COMMAND_PARSE_STATE_LF;
+ break;
+ } else if (*parser->cur != ' ') {
+ smtp_command_parser_error(parser,
+ SMTP_COMMAND_PARSE_ERROR_BAD_COMMAND,
+ "Unexpected character %s in command name",
+ _chr_sanitize(*parser->cur));
+ return -1;
+ }
+ parser->cur++;
+ parser->state.state =
+ SMTP_COMMAND_PARSE_STATE_PARAMETERS;
+ if (parser->cur >= parser->end)
+ return 0;
+ /* fall through */
+ case SMTP_COMMAND_PARSE_STATE_PARAMETERS:
+ ret = smtp_command_parse_parameters(parser);
+ if (ret <= 0)
+ return ret;
+ parser->state.state = SMTP_COMMAND_PARSE_STATE_CR;
+ if (parser->cur == parser->end)
+ return 0;
+ /* fall through */
+ case SMTP_COMMAND_PARSE_STATE_CR:
+ if (*parser->cur == '\r') {
+ parser->cur++;
+ } else if (*parser->cur != '\n') {
+ smtp_command_parser_error(parser,
+ SMTP_COMMAND_PARSE_ERROR_BAD_COMMAND,
+ "Unexpected character %s in %s",
+ _chr_sanitize(*parser->cur),
+ (parser->auth_response ?
+ "AUTH response" :
+ "command parameters"));
+ return -1;
+ }
+ parser->state.state = SMTP_COMMAND_PARSE_STATE_LF;
+ if (parser->cur == parser->end)
+ return 0;
+ /* fall through */
+ case SMTP_COMMAND_PARSE_STATE_LF:
+ if (*parser->cur != '\n') {
+ smtp_command_parser_error(parser,
+ SMTP_COMMAND_PARSE_ERROR_BAD_COMMAND,
+ "Expected LF after CR at end of %s, "
+ "but found %s",
+ (parser->auth_response ?
+ "AUTH response" : "command"),
+ _chr_sanitize(*parser->cur));
+ return -1;
+ }
+ parser->cur++;
+ parser->state.state = SMTP_COMMAND_PARSE_STATE_INIT;
+ return 1;
+ case SMTP_COMMAND_PARSE_STATE_ERROR:
+ /* Skip until end of line */
+ while (parser->cur < parser->end &&
+ *parser->cur != '\n')
+ parser->cur++;
+ if (parser->cur == parser->end)
+ return 0;
+ parser->cur++;
+ parser->state.state = SMTP_COMMAND_PARSE_STATE_INIT;
+ break;
+ default:
+ i_unreached();
+ }
+ }
+
+ i_unreached();
+ return -1;
+}
+
+static int smtp_command_parse(struct smtp_command_parser *parser)
+{
+ const unsigned char *begin;
+ size_t size, old_bytes = 0;
+ int ret;
+
+ while ((ret = i_stream_read_data(parser->input, &begin, &size,
+ old_bytes)) > 0) {
+ parser->cur = begin;
+ parser->end = parser->cur + size;
+
+ ret = smtp_command_parse_line(parser);
+ i_stream_skip(parser->input, parser->cur - begin);
+ if (ret != 0)
+ return ret;
+ old_bytes = i_stream_get_data_size(parser->input);
+ }
+ i_assert(ret != -2);
+
+ if (ret < 0) {
+ i_assert(parser->input->eof);
+ if (parser->input->stream_errno == 0) {
+ if (parser->state.state ==
+ SMTP_COMMAND_PARSE_STATE_INIT)
+ ret = -2;
+ smtp_command_parser_error(
+ parser, SMTP_COMMAND_PARSE_ERROR_BROKEN_COMMAND,
+ "Premature end of input");
+ } else {
+ smtp_command_parser_error(
+ parser, SMTP_COMMAND_PARSE_ERROR_BROKEN_STREAM,
+ "%s", i_stream_get_disconnect_reason(parser->input));
+ }
+ }
+ return ret;
+}
+
+bool smtp_command_parser_pending_data(struct smtp_command_parser *parser)
+{
+ if (parser->data == NULL)
+ return FALSE;
+ return i_stream_have_bytes_left(parser->data);
+}
+
+static int smtp_command_parse_finish_data(struct smtp_command_parser *parser)
+{
+ const unsigned char *data;
+ size_t size;
+ int ret;
+
+ parser->error_code = SMTP_COMMAND_PARSE_ERROR_NONE;
+ parser->error = NULL;
+
+ if (parser->data == NULL)
+ return 1;
+ if (parser->data->eof) {
+ i_stream_unref(&parser->data);
+ return 1;
+ }
+
+ while ((ret = i_stream_read_data(parser->data, &data, &size, 0)) > 0)
+ i_stream_skip(parser->data, size);
+ if (ret == 0 || parser->data->stream_errno != 0) {
+ switch (parser->data->stream_errno) {
+ case 0:
+ return 0;
+ case EMSGSIZE:
+ smtp_command_parser_error(
+ parser, SMTP_COMMAND_PARSE_ERROR_DATA_TOO_LARGE,
+ "Command data too large");
+ break;
+ default:
+ smtp_command_parser_error(
+ parser, SMTP_COMMAND_PARSE_ERROR_BROKEN_STREAM,
+ "%s", i_stream_get_disconnect_reason(parser->data));
+ }
+ return -1;
+ }
+ i_stream_unref(&parser->data);
+ return 1;
+}
+
+int smtp_command_parse_next(struct smtp_command_parser *parser,
+ const char **cmd_name_r, const char **cmd_params_r,
+ enum smtp_command_parse_error *error_code_r,
+ const char **error_r)
+{
+ int ret;
+
+ i_assert(!parser->auth_response ||
+ parser->state.state == SMTP_COMMAND_PARSE_STATE_INIT ||
+ parser->state.state == SMTP_COMMAND_PARSE_STATE_ERROR);
+ parser->auth_response = FALSE;
+
+ *error_code_r = parser->error_code = SMTP_COMMAND_PARSE_ERROR_NONE;
+ *error_r = NULL;
+
+ i_free_and_null(parser->error);
+
+ /* Make sure we finished streaming payload from previous command
+ before we continue. */
+ ret = smtp_command_parse_finish_data(parser);
+ if (ret <= 0) {
+ if (ret < 0) {
+ *error_code_r = parser->error_code;
+ *error_r = parser->error;
+ }
+ return ret;
+ }
+
+ ret = smtp_command_parse(parser);
+ if (ret <= 0) {
+ if (ret < 0) {
+ *error_code_r = parser->error_code;
+ *error_r = parser->error;
+ parser->state.state = SMTP_COMMAND_PARSE_STATE_ERROR;
+ }
+ return ret;
+ }
+
+ i_assert(parser->state.state == SMTP_COMMAND_PARSE_STATE_INIT);
+ *cmd_name_r = parser->state.cmd_name;
+ *cmd_params_r = (parser->state.cmd_params == NULL ?
+ "" : parser->state.cmd_params);
+ return 1;
+}
+
+struct istream *
+smtp_command_parse_data_with_size(struct smtp_command_parser *parser,
+ uoff_t size)
+{
+ i_assert(parser->data == NULL);
+ if (size > parser->limits.max_data_size) {
+ /* Not supposed to happen; command should check size */
+ parser->data = i_stream_create_error_str(EMSGSIZE,
+ "Command data size exceeds maximum "
+ "(%"PRIuUOFF_T" > %"PRIuUOFF_T")",
+ size, parser->limits.max_data_size);
+ } else {
+ // FIXME: Make exact_size stream type
+ struct istream *limit_input =
+ i_stream_create_limit(parser->input, size);
+ parser->data = i_stream_create_min_sized(limit_input, size);
+ i_stream_unref(&limit_input);
+ }
+ i_stream_ref(parser->data);
+ return parser->data;
+}
+
+struct istream *
+smtp_command_parse_data_with_dot(struct smtp_command_parser *parser)
+{
+ struct istream *data;
+ i_assert(parser->data == NULL);
+
+ data = i_stream_create_dot(parser->input, TRUE);
+ if (parser->limits.max_data_size != UOFF_T_MAX) {
+ parser->data = i_stream_create_failure_at(
+ data, parser->limits.max_data_size, EMSGSIZE,
+ t_strdup_printf("Command data size exceeds maximum "
+ "(> %"PRIuUOFF_T")",
+ parser->limits.max_data_size));
+ i_stream_unref(&data);
+ } else {
+ parser->data = data;
+ }
+ i_stream_ref(parser->data);
+ return parser->data;
+}
+
+int smtp_command_parse_auth_response(
+ struct smtp_command_parser *parser, const char **line_r,
+ enum smtp_command_parse_error *error_code_r, const char **error_r)
+{
+ int ret;
+
+ i_assert(parser->auth_response ||
+ parser->state.state == SMTP_COMMAND_PARSE_STATE_INIT ||
+ parser->state.state == SMTP_COMMAND_PARSE_STATE_ERROR);
+ parser->auth_response = TRUE;
+
+ *error_code_r = parser->error_code = SMTP_COMMAND_PARSE_ERROR_NONE;
+ *error_r = NULL;
+
+ i_free_and_null(parser->error);
+
+ /* Make sure we finished streaming payload from previous command
+ before we continue. */
+ ret = smtp_command_parse_finish_data(parser);
+ if (ret <= 0) {
+ if (ret < 0) {
+ *error_code_r = parser->error_code;
+ *error_r = parser->error;
+ }
+ return ret;
+ }
+
+ ret = smtp_command_parse(parser);
+ if (ret <= 0) {
+ if (ret < 0) {
+ *error_code_r = parser->error_code;
+ *error_r = parser->error;
+ parser->state.state = SMTP_COMMAND_PARSE_STATE_ERROR;
+ }
+ return ret;
+ }
+
+ i_assert(parser->state.state == SMTP_COMMAND_PARSE_STATE_INIT);
+ *line_r = parser->state.cmd_params;
+ parser->auth_response = FALSE;
+ return 1;
+}
diff --git a/src/lib-smtp/smtp-command-parser.h b/src/lib-smtp/smtp-command-parser.h
new file mode 100644
index 0000000..c9b56a1
--- /dev/null
+++ b/src/lib-smtp/smtp-command-parser.h
@@ -0,0 +1,51 @@
+#ifndef SMTP_COMMAND_PARSER_H
+#define SMTP_COMMAND_PARSER_H
+
+#include "smtp-command.h"
+
+/* FIXME: drop unused */
+enum smtp_command_parse_error {
+ /* No error */
+ SMTP_COMMAND_PARSE_ERROR_NONE = 0,
+ /* Stream error */
+ SMTP_COMMAND_PARSE_ERROR_BROKEN_STREAM,
+ /* Unrecoverable generic error */
+ SMTP_COMMAND_PARSE_ERROR_BROKEN_COMMAND,
+ /* Recoverable generic error */
+ SMTP_COMMAND_PARSE_ERROR_BAD_COMMAND,
+ /* Stream error */
+ SMTP_COMMAND_PARSE_ERROR_LINE_TOO_LONG,
+ /* Data too large (fatal) */
+ SMTP_COMMAND_PARSE_ERROR_DATA_TOO_LARGE
+};
+
+struct smtp_command_parser *
+smtp_command_parser_init(struct istream *input,
+ const struct smtp_command_limits *limits) ATTR_NULL(2);
+void smtp_command_parser_deinit(struct smtp_command_parser **_parser);
+
+void smtp_command_parser_set_stream(struct smtp_command_parser *parser,
+ struct istream *input);
+
+/* Returns 1 if a command was returned, 0 if more data is needed, -1 on error,
+ -2 if disconnected in SMTP_COMMAND_PARSE_STATE_INIT state. -2 is mainly for
+ unit tests - it can normally be treated the same as -1. */
+int smtp_command_parse_next(struct smtp_command_parser *parser,
+ const char **cmd_name_r, const char **cmd_params_r,
+ enum smtp_command_parse_error *error_code_r,
+ const char **error_r);
+
+struct istream *
+smtp_command_parse_data_with_size(struct smtp_command_parser *parser,
+ uoff_t size);
+struct istream *
+smtp_command_parse_data_with_dot(struct smtp_command_parser *parser);
+bool smtp_command_parser_pending_data(struct smtp_command_parser *parser);
+
+/* Returns the same as smtp_command_parse_next() */
+int smtp_command_parse_auth_response(struct smtp_command_parser *parser,
+ const char **line_r,
+ enum smtp_command_parse_error *error_code_r,
+ const char **error_r);
+
+#endif
diff --git a/src/lib-smtp/smtp-command.h b/src/lib-smtp/smtp-command.h
new file mode 100644
index 0000000..94f2251
--- /dev/null
+++ b/src/lib-smtp/smtp-command.h
@@ -0,0 +1,38 @@
+#ifndef SMTP_COMMAND_H
+#define SMTP_COMMAND_H
+
+#define SMTP_COMMAND_DEFAULT_MAX_PARAMETERS_SIZE 4*1024
+#define SMTP_COMMAND_DEFAULT_MAX_AUTH_SIZE 8*1024
+#define SMTP_COMMAND_DEFAULT_MAX_DATA_SIZE 40*1024*1024
+
+struct smtp_command_limits {
+ /* Maximum size of command parameters, starting after first space */
+ size_t max_parameters_size;
+ /* Maximum size of authentication response */
+ size_t max_auth_size;
+ /* Absolute maximum size of command data, beyond which the parser yields
+ a fatal error; i.e. closing the connection in the server. This should
+ be higher than a normal message size limit, which would return a
+ normal informative error. The limit here just serves to protect
+ against abuse. */
+ uoff_t max_data_size;
+};
+
+struct smtp_command {
+ const char *name;
+ const char *parameters;
+};
+
+static inline void
+smtp_command_limits_merge(struct smtp_command_limits *limits,
+ const struct smtp_command_limits *new_limits)
+{
+ if (new_limits->max_parameters_size > 0)
+ limits->max_parameters_size = new_limits->max_parameters_size;
+ if (new_limits->max_auth_size > 0)
+ limits->max_auth_size = new_limits->max_auth_size;
+ if (new_limits->max_data_size > 0)
+ limits->max_data_size = new_limits->max_data_size;
+}
+
+#endif
diff --git a/src/lib-smtp/smtp-common.c b/src/lib-smtp/smtp-common.c
new file mode 100644
index 0000000..1771169
--- /dev/null
+++ b/src/lib-smtp/smtp-common.c
@@ -0,0 +1,91 @@
+/* Copyright (c) 2013-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "smtp-common.h"
+
+/*
+ * Capabilities
+ */
+
+const struct smtp_capability_name smtp_capability_names[] = {
+ { "AUTH", SMTP_CAPABILITY_AUTH },
+ { "STARTTLS", SMTP_CAPABILITY_STARTTLS },
+ { "PIPELINING", SMTP_CAPABILITY_PIPELINING },
+ { "SIZE", SMTP_CAPABILITY_SIZE },
+ { "ENHANCEDSTATUSCODES", SMTP_CAPABILITY_ENHANCEDSTATUSCODES },
+ { "8BITMIME", SMTP_CAPABILITY_8BITMIME },
+ { "CHUNKING", SMTP_CAPABILITY_CHUNKING },
+ { "BINARYMIME", SMTP_CAPABILITY_BINARYMIME },
+ { "BURL", SMTP_CAPABILITY_BURL },
+ { "DSN", SMTP_CAPABILITY_DSN },
+ { "VRFY", SMTP_CAPABILITY_VRFY },
+ { "ETRN", SMTP_CAPABILITY_ETRN },
+ { "XCLIENT", SMTP_CAPABILITY_XCLIENT },
+ { NULL, 0 }
+};
+
+enum smtp_capability smtp_capability_find_by_name(const char *cap_name)
+{
+ const struct smtp_capability_name *cap;
+ unsigned int i;
+
+ for (i = 0; smtp_capability_names[i].name != NULL; i++) {
+ cap = &smtp_capability_names[i];
+
+ if (strcasecmp(cap_name, cap->name) == 0)
+ return cap->capability;
+ }
+
+ return SMTP_CAPABILITY_NONE;
+}
+
+/*
+ * SMTP proxy data
+ */
+
+static void
+smtp_proxy_data_merge_extra_fields(pool_t pool, struct smtp_proxy_data *dst,
+ const struct smtp_proxy_data *src)
+{
+ const struct smtp_proxy_data_field *sefields;
+ struct smtp_proxy_data_field *defields;
+ unsigned int i;
+
+ if (src->extra_fields_count == 0)
+ return;
+
+ sefields = src->extra_fields;
+ defields = p_new(pool, struct smtp_proxy_data_field,
+ src->extra_fields_count);
+ for (i = 0; i < src->extra_fields_count; i++) {
+ defields[i].name = p_strdup(pool, sefields[i].name);
+ defields[i].value = p_strdup(pool, sefields[i].value);
+ }
+
+ dst->extra_fields = defields;
+ dst->extra_fields_count = src->extra_fields_count;
+}
+
+void smtp_proxy_data_merge(pool_t pool, struct smtp_proxy_data *dst,
+ const struct smtp_proxy_data *src)
+{
+ if (src->proto != SMTP_PROXY_PROTOCOL_UNKNOWN)
+ dst->proto = src->proto;
+ if (src->source_ip.family != 0) {
+ dst->source_ip = src->source_ip;
+ if (src->source_port != 0)
+ dst->source_port = src->source_port;
+ }
+ if (src->helo != NULL && *src->helo != '\0')
+ dst->helo = p_strdup(pool, src->helo);
+ if (src->login != NULL && *src->login != '\0')
+ dst->login = p_strdup(pool, src->login);
+ if (src->session != NULL && *src->session != '\0')
+ dst->session = p_strdup(pool, src->session);
+ if (src->ttl_plus_1 > 0)
+ dst->ttl_plus_1 = src->ttl_plus_1;
+ if (src->timeout_secs > 0)
+ dst->timeout_secs = src->timeout_secs;
+
+ smtp_proxy_data_merge_extra_fields(pool, dst, src);
+};
diff --git a/src/lib-smtp/smtp-common.h b/src/lib-smtp/smtp-common.h
new file mode 100644
index 0000000..ec95dc0
--- /dev/null
+++ b/src/lib-smtp/smtp-common.h
@@ -0,0 +1,119 @@
+#ifndef SMTP_COMMON_H
+#define SMTP_COMMON_H
+
+#include "net.h"
+
+/*
+ * Limits
+ */
+
+#define SMTP_BASE_LINE_LENGTH_LIMIT (512 - 2)
+
+/*
+ * SMTP protocols
+ */
+
+enum smtp_protocol {
+ SMTP_PROTOCOL_SMTP = 0,
+ SMTP_PROTOCOL_LMTP
+};
+
+static inline const char *
+smtp_protocol_name(enum smtp_protocol proto)
+{
+ switch (proto) {
+ case SMTP_PROTOCOL_SMTP:
+ return "smtp";
+ case SMTP_PROTOCOL_LMTP:
+ return "lmtp";
+ default:
+ break;
+ }
+ i_unreached();
+}
+
+/* SMTP capabilities */
+
+enum smtp_capability {
+ SMTP_CAPABILITY_NONE = 0,
+
+ SMTP_CAPABILITY_AUTH = BIT(0),
+ SMTP_CAPABILITY_STARTTLS = BIT(1),
+ SMTP_CAPABILITY_PIPELINING = BIT(2),
+ SMTP_CAPABILITY_SIZE = BIT(3),
+ SMTP_CAPABILITY_ENHANCEDSTATUSCODES = BIT(4),
+ SMTP_CAPABILITY_8BITMIME = BIT(5),
+ SMTP_CAPABILITY_CHUNKING = BIT(6),
+ SMTP_CAPABILITY_BINARYMIME = BIT(7),
+ SMTP_CAPABILITY_BURL = BIT(8),
+ SMTP_CAPABILITY_DSN = BIT(9),
+ SMTP_CAPABILITY_VRFY = BIT(10),
+ SMTP_CAPABILITY_ETRN = BIT(11),
+ SMTP_CAPABILITY_XCLIENT = BIT(12),
+
+ SMTP_CAPABILITY__ORCPT = BIT(24),
+};
+
+struct smtp_capability_name {
+ const char *name;
+ enum smtp_capability capability;
+};
+
+struct smtp_capability_extra {
+ const char *name;
+ const char *const *params;
+};
+
+extern const struct smtp_capability_name smtp_capability_names[];
+
+enum smtp_capability smtp_capability_find_by_name(const char *cap_name);
+
+/*
+ * SMTP proxy data
+ */
+
+enum smtp_proxy_protocol {
+ SMTP_PROXY_PROTOCOL_UNKNOWN = 0,
+ SMTP_PROXY_PROTOCOL_SMTP,
+ SMTP_PROXY_PROTOCOL_ESMTP,
+ SMTP_PROXY_PROTOCOL_LMTP
+};
+
+struct smtp_proxy_data_field {
+ const char *name;
+ const char *value;
+};
+ARRAY_DEFINE_TYPE(smtp_proxy_data_field, struct smtp_proxy_data_field);
+
+struct smtp_proxy_data {
+ /* PROTO */
+ enum smtp_proxy_protocol proto;
+ /* ADDR */
+ struct ip_addr source_ip;
+ /* PORT */
+ in_port_t source_port;
+ /* HELO, LOGIN */
+ const char *helo, *login;
+ /* SESSION */
+ const char *session;
+
+ /* TTL: send as this -1, so the default 0 means "don't send it" */
+ unsigned int ttl_plus_1;
+ /* TIMEOUT: remote is notified that the connection is going to be closed
+ after this many seconds, so it should try to keep lock waits and such
+ lower than this. */
+ unsigned int timeout_secs;
+
+ /* additional fields */
+ const struct smtp_proxy_data_field *extra_fields;
+ unsigned int extra_fields_count;
+};
+
+/*
+ * SMTP proxy data
+ */
+
+void smtp_proxy_data_merge(pool_t pool, struct smtp_proxy_data *dst,
+ const struct smtp_proxy_data *src);
+
+#endif
diff --git a/src/lib-smtp/smtp-params.c b/src/lib-smtp/smtp-params.c
new file mode 100644
index 0000000..3994ce5
--- /dev/null
+++ b/src/lib-smtp/smtp-params.c
@@ -0,0 +1,1375 @@
+/* Copyright (c) 2013-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "str.h"
+#include "array.h"
+#include "message-address.h"
+#include "smtp-common.h"
+#include "smtp-parser.h"
+#include "smtp-syntax.h"
+#include "smtp-address.h"
+
+#include "smtp-params.h"
+
+#include <ctype.h>
+
+/*
+ * Common
+ */
+
+/* parse */
+
+static int
+smtp_param_do_parse(struct smtp_parser *parser, struct smtp_param *param_r)
+{
+ const unsigned char *pbegin = parser->cur;
+
+ /* esmtp-param = esmtp-keyword ["=" esmtp-value]
+ esmtp-keyword = (ALPHA / DIGIT) *(ALPHA / DIGIT / "-")
+ esmtp-value = 1*(%d33-60 / %d62-126)
+ ; any CHAR excluding "=", SP, and control
+ ; characters. If this string is an email address,
+ ; i.e., a Mailbox, then the "xtext" syntax [32]
+ ; SHOULD be used.
+ */
+
+ if (parser->cur >= parser->end || !i_isalnum(*parser->cur)) {
+ parser->error = "Unexpected character in parameter keyword";
+ return -1;
+ }
+ parser->cur++;
+
+ while (parser->cur < parser->end &&
+ (i_isalnum(*parser->cur) || *parser->cur == '-'))
+ parser->cur++;
+ param_r->keyword = t_strndup(pbegin, parser->cur - pbegin);
+
+ if (parser->cur >= parser->end) {
+ param_r->value = NULL;
+ return 1;
+ }
+ if (*parser->cur != '=') {
+ parser->error = "Unexpected character in parameter keyword";
+ return -1;
+ }
+ parser->cur++;
+
+ pbegin = parser->cur;
+ while (parser->cur < parser->end &&
+ smtp_char_is_esmtp_value(*parser->cur))
+ parser->cur++;
+
+ if (parser->cur < parser->end) {
+ parser->error = "Unexpected character in parameter value";
+ return -1;
+ }
+ param_r->value = t_strndup(pbegin, parser->cur - pbegin);
+ return 1;
+}
+
+int smtp_param_parse(pool_t pool, const char *text,
+ struct smtp_param *param_r, const char **error_r)
+{
+ struct smtp_parser parser;
+
+ i_zero(param_r);
+
+ if (text == NULL || *text == '\0') {
+ if (error_r != NULL)
+ *error_r = "Parameter is empty";
+ return -1;
+ }
+
+ smtp_parser_init(&parser, pool, text);
+
+ if (smtp_param_do_parse(&parser, param_r) <= 0) {
+ if (error_r != NULL)
+ *error_r = parser.error;
+ return -1;
+ }
+ return 1;
+}
+
+/* manipulate */
+
+void smtp_params_copy(pool_t pool, ARRAY_TYPE(smtp_param) *dst,
+ const ARRAY_TYPE(smtp_param) *src)
+{
+ const struct smtp_param *param;
+
+ if (!array_is_created(src))
+ return;
+
+ p_array_init(dst, pool, array_count(src));
+ array_foreach(src, param) {
+ struct smtp_param param_new;
+
+ param_new.keyword = p_strdup(pool, param->keyword);
+ param_new.value = p_strdup(pool, param->value);
+ array_push_back(dst, &param_new);
+ }
+}
+
+void smtp_params_add_one(ARRAY_TYPE(smtp_param) *params, pool_t pool,
+ const char *keyword, const char *value)
+{
+ struct smtp_param param;
+
+ if (!array_is_created(params))
+ p_array_init(params, pool, 4);
+
+ i_zero(&param);
+ param.keyword = p_strdup(pool, keyword);
+ param.value = p_strdup(pool, value);
+ array_push_back(params, &param);
+}
+
+void smtp_params_add_encoded(ARRAY_TYPE(smtp_param) *params, pool_t pool,
+ const char *keyword, const unsigned char *value,
+ size_t value_len)
+{
+ string_t *value_enc = t_str_new(value_len * 2);
+
+ smtp_xtext_encode(value_enc, value, value_len);
+ smtp_params_add_one(params, pool, keyword, str_c(value_enc));
+}
+
+bool smtp_params_drop_one(ARRAY_TYPE(smtp_param) *params, const char *keyword,
+ const char **value_r)
+{
+ const struct smtp_param *param;
+
+ if (!array_is_created(params))
+ return FALSE;
+
+ array_foreach(params, param) {
+ if (strcasecmp(param->keyword, keyword) == 0) {
+ if (value_r != NULL)
+ *value_r = param->value;
+ array_delete(params,
+ array_foreach_idx(params, param), 1);
+ return TRUE;
+ }
+ }
+ return FALSE;
+}
+
+/* write */
+
+static bool smtp_param_value_valid(const char *value)
+{
+ const char *p = value;
+
+ while (*p != '\0' && smtp_char_is_esmtp_value(*p))
+ p++;
+ return (*p == '\0');
+}
+
+void smtp_param_write(string_t *out, const struct smtp_param *param)
+{
+ str_append(out, t_str_ucase(param->keyword));
+ if (param->value != NULL) {
+ i_assert(smtp_param_value_valid(param->value));
+ str_append_c(out, '=');
+ str_append(out, param->value);
+ }
+}
+
+static void
+smtp_params_write(string_t *buffer, const char *const *param_keywords,
+ const ARRAY_TYPE(smtp_param) *params) ATTR_NULL(2)
+{
+ const struct smtp_param *param;
+
+ if (param_keywords == NULL || *param_keywords == NULL)
+ return;
+ if (!array_is_created(params))
+ return;
+
+ array_foreach(params, param) {
+ if (str_array_icase_find(param_keywords, param->keyword))
+ smtp_param_write(buffer, param);
+ str_append_c(buffer, ' ');
+ }
+}
+
+/* evaluate */
+
+const struct smtp_param *
+smtp_params_get_param(const ARRAY_TYPE(smtp_param) *params,
+ const char *keyword)
+{
+ const struct smtp_param *param;
+
+ if (!array_is_created(params))
+ return NULL;
+
+ array_foreach(params, param) {
+ if (strcasecmp(param->keyword, keyword) == 0)
+ return param;
+ }
+ return NULL;
+}
+
+int smtp_params_decode_param(const ARRAY_TYPE(smtp_param) *params,
+ const char *keyword, string_t **value_r,
+ bool allow_nul, const char **error_r)
+{
+ const struct smtp_param *param;
+
+ param = smtp_params_get_param(params, keyword);
+ if (param == NULL)
+ return 0;
+
+ *value_r = t_str_new(strlen(param->value) * 2);
+ if (smtp_xtext_decode(*value_r, param->value, allow_nul, error_r) <= 0)
+ return -1;
+ return 1;
+}
+
+bool smtp_params_equal(const ARRAY_TYPE(smtp_param) *params1,
+ const ARRAY_TYPE(smtp_param) *params2)
+{
+ const struct smtp_param *param1, *param2;
+
+ if (!array_is_created(params1) || array_count(params1) == 0) {
+ return (!array_is_created(params2) ||
+ array_count(params2) == 0);
+ }
+ if (!array_is_created(params2) || array_count(params2) == 0)
+ return FALSE;
+
+ if (array_count(params1) != array_count(params2))
+ return FALSE;
+
+ array_foreach(params1, param1) {
+ param2 = smtp_params_get_param(params2, param1->keyword);
+ if (param2 == NULL)
+ return FALSE;
+ if (null_strcmp(param1->value, param2->value) != 0)
+ return FALSE;
+ }
+ return TRUE;
+}
+
+/*
+ * MAIL parameters
+ */
+
+/* parse */
+
+struct smtp_params_mail_parser {
+ pool_t pool;
+ struct smtp_params_mail *params;
+ enum smtp_capability caps;
+
+ enum smtp_param_parse_error error_code;
+ const char *error;
+};
+
+static int
+smtp_params_mail_parse_auth(struct smtp_params_mail_parser *pmparser,
+ const char *xtext)
+{
+ struct smtp_params_mail *params = pmparser->params;
+ struct smtp_address *auth_addr;
+ const char *value, *error;
+
+ /* AUTH=: RFC 4954, Section 5
+
+ We ignore this parameter, but we do check it for validity
+ */
+
+ /* cannot specify this multiple times */
+ if (params->auth != NULL) {
+ pmparser->error = "Duplicate AUTH= parameter";
+ pmparser->error_code = SMTP_PARAM_PARSE_ERROR_BAD_SYNTAX;
+ return -1;
+ }
+ /* value required */
+ if (xtext == NULL) {
+ pmparser->error = "Missing AUTH= parameter value";
+ pmparser->error_code = SMTP_PARAM_PARSE_ERROR_BAD_SYNTAX;
+ return -1;
+ }
+ if (smtp_xtext_parse(xtext, &value, &error) < 0) {
+ pmparser->error = t_strdup_printf(
+ "Invalid AUTH= parameter value: %s", error);
+ pmparser->error_code = SMTP_PARAM_PARSE_ERROR_BAD_SYNTAX;
+ return -1;
+ }
+ if (strcmp(value, "<>") == 0) {
+ params->auth = p_new(pmparser->pool, struct smtp_address, 1);
+ } else if (smtp_address_parse_mailbox(
+ pmparser->pool, value,
+ SMTP_ADDRESS_PARSE_FLAG_ALLOW_LOCALPART,
+ &auth_addr, &error) < 0) {
+ pmparser->error = t_strdup_printf(
+ "Invalid AUTH= address value: %s", error);
+ pmparser->error_code = SMTP_PARAM_PARSE_ERROR_BAD_SYNTAX;
+ return -1;
+ } else {
+ params->auth = auth_addr;
+ }
+ /* ignore, our own AUTH data is added below */
+ return 0;
+}
+
+static int
+smtp_params_mail_parse_body(struct smtp_params_mail_parser *pmparser,
+ const char *value, const char *const *extensions)
+{
+ struct smtp_params_mail *params = pmparser->params;
+ enum smtp_capability caps = pmparser->caps;
+
+ /* BODY=<type>: RFC 6152 */
+
+ /* cannot specify this multiple times */
+ if (params->body.type != SMTP_PARAM_MAIL_BODY_TYPE_UNSPECIFIED) {
+ pmparser->error = "Duplicate BODY= parameter";
+ pmparser->error_code = SMTP_PARAM_PARSE_ERROR_BAD_SYNTAX;
+ return -1;
+ }
+ /* value required */
+ if (value == NULL) {
+ pmparser->error = "Missing BODY= parameter value";
+ pmparser->error_code = SMTP_PARAM_PARSE_ERROR_BAD_SYNTAX;
+ return -1;
+ }
+
+ value = t_str_ucase(value);
+ params->body.ext = NULL;
+ /* =7BIT: RFC 6152 */
+ if (strcmp(value, "7BIT") == 0) {
+ params->body.type = SMTP_PARAM_MAIL_BODY_TYPE_7BIT;
+ /* =8BITMIME: RFC 6152 */
+ } else if ((caps & SMTP_CAPABILITY_8BITMIME) != 0 &&
+ strcmp(value, "8BITMIME") == 0) {
+ params->body.type = SMTP_PARAM_MAIL_BODY_TYPE_8BITMIME;
+ /* =BINARYMIME: RFC 3030 */
+ } else if ((caps & SMTP_CAPABILITY_BINARYMIME) != 0 &&
+ (caps & SMTP_CAPABILITY_CHUNKING) != 0 &&
+ strcmp(value, "BINARYMIME") == 0) {
+ params->body.type = SMTP_PARAM_MAIL_BODY_TYPE_BINARYMIME;
+ /* =?? */
+ } else if (extensions != NULL &&
+ str_array_icase_find(extensions, value)) {
+ params->body.type = SMTP_PARAM_MAIL_BODY_TYPE_EXTENSION;
+ params->body.ext = p_strdup(pmparser->pool, value);
+ } else {
+ pmparser->error = "Unsupported mail BODY type";
+ pmparser->error_code = SMTP_PARAM_PARSE_ERROR_NOT_SUPPORTED;
+ return -1;
+ }
+ return 0;
+}
+
+static int
+smtp_params_mail_parse_envid(struct smtp_params_mail_parser *pmparser,
+ const char *xtext)
+{
+ struct smtp_params_mail *params = pmparser->params;
+ const unsigned char *p, *pend;
+ const char *envid, *error;
+
+ /* ENVID=<envid>: RFC 3461 */
+
+ /* cannot specify this multiple times */
+ if (params->envid != NULL) {
+ pmparser->error = "Duplicate ENVID= parameter";
+ pmparser->error_code = SMTP_PARAM_PARSE_ERROR_BAD_SYNTAX;
+ return -1;
+ }
+ /* value required */
+ if (xtext == NULL) {
+ pmparser->error = "Missing ENVID= parameter value";
+ pmparser->error_code = SMTP_PARAM_PARSE_ERROR_BAD_SYNTAX;
+ return -1;
+ }
+ /* check xtext */
+ if (smtp_xtext_parse(xtext, &envid, &error) < 0) {
+ pmparser->error = t_strdup_printf(
+ "Invalid ENVID= parameter value: %s", error);
+ pmparser->error_code = SMTP_PARAM_PARSE_ERROR_BAD_SYNTAX;
+ return -1;
+ }
+ /* RFC 3461, Section 4.4:
+
+ Due to limitations in the Delivery Status Notification format, the
+ value of the ENVID parameter prior to encoding as "xtext" MUST
+ consist entirely of printable (graphic and white space) characters
+ from the US-ASCII repertoire.
+ */
+ p = (const unsigned char *)envid;
+ pend = p + strlen(envid);
+ while (p < pend && smtp_char_is_textstr(*p))
+ p++;
+ if (p < pend) {
+ pmparser->error =
+ "Invalid ENVID= parameter value: "
+ "Contains non-printable characters";
+ pmparser->error_code = SMTP_PARAM_PARSE_ERROR_BAD_SYNTAX;
+ return -1;
+ }
+ params->envid = p_strdup(pmparser->pool, envid);
+ return 0;
+}
+
+static int
+smtp_params_mail_parse_ret(struct smtp_params_mail_parser *pmparser,
+ const char *value)
+{
+ struct smtp_params_mail *params = pmparser->params;
+
+ /* RET=<keyword>: RFC 3461 */
+
+ /* cannot specify this multiple times */
+ if (params->ret != SMTP_PARAM_MAIL_RET_UNSPECIFIED) {
+ pmparser->error = "Duplicate RET= parameter";
+ pmparser->error_code = SMTP_PARAM_PARSE_ERROR_BAD_SYNTAX;
+ return -1;
+ }
+ /* value required */
+ if (value == NULL) {
+ pmparser->error = "Missing RET= parameter value";
+ pmparser->error_code = SMTP_PARAM_PARSE_ERROR_BAD_SYNTAX;
+ return -1;
+ }
+
+ value = t_str_ucase(value);
+ /* =FULL */
+ if (strcmp(value, "FULL") == 0) {
+ params->ret = SMTP_PARAM_MAIL_RET_FULL;
+ /* =HDRS */
+ } else if (strcmp(value, "HDRS") == 0) {
+ params->ret = SMTP_PARAM_MAIL_RET_HDRS;
+ } else {
+ pmparser->error = "Unsupported RET= parameter keyword";
+ pmparser->error_code = SMTP_PARAM_PARSE_ERROR_NOT_SUPPORTED;
+ return -1;
+ }
+ return 0;
+}
+
+static int
+smtp_params_mail_parse_size(struct smtp_params_mail_parser *pmparser,
+ const char *value)
+{
+ struct smtp_params_mail *params = pmparser->params;
+
+ /* SIZE=<size-value>: RFC 1870 */
+
+ /* cannot specify this multiple times */
+ if (params->size != 0) {
+ pmparser->error = "Duplicate SIZE= parameter";
+ pmparser->error_code = SMTP_PARAM_PARSE_ERROR_BAD_SYNTAX;
+ return -1;
+ }
+ /* value required */
+ if (value == NULL) {
+ pmparser->error = "Missing SIZE= parameter value";
+ pmparser->error_code = SMTP_PARAM_PARSE_ERROR_BAD_SYNTAX;
+ return -1;
+ }
+
+ /* size-value ::= 1*20DIGIT */
+ if (str_to_uoff(value, &params->size) < 0) {
+ pmparser->error = "Unsupported SIZE parameter value";
+ pmparser->error_code = SMTP_PARAM_PARSE_ERROR_NOT_SUPPORTED;
+ return -1;
+ }
+ return 0;
+}
+
+int smtp_params_mail_parse(pool_t pool, const char *args,
+ enum smtp_capability caps,
+ const char *const *extensions,
+ const char *const *body_extensions,
+ struct smtp_params_mail *params_r,
+ enum smtp_param_parse_error *error_code_r,
+ const char **error_r)
+{
+ struct smtp_params_mail_parser pmparser;
+ struct smtp_param param;
+ const char *const *argv;
+ const char *error;
+ int ret = 0;
+
+ i_zero(params_r);
+
+ i_zero(&pmparser);
+ pmparser.pool = pool;
+ pmparser.params = params_r;
+ pmparser.caps = caps;
+
+ argv = t_strsplit(args, " ");
+ for (; *argv != NULL; argv++) {
+ if (smtp_param_parse(pool_datastack_create(), *argv,
+ &param, &error) < 0) {
+ *error_r = t_strdup_printf(
+ "Invalid MAIL parameter: %s", error);
+ *error_code_r = SMTP_PARAM_PARSE_ERROR_BAD_SYNTAX;
+ return -1;
+ }
+
+ /* parse known parameters */
+ param.keyword = t_str_ucase(param.keyword);
+ if ((caps & SMTP_CAPABILITY_AUTH) != 0 &&
+ strcmp(param.keyword, "AUTH") == 0) {
+ if (smtp_params_mail_parse_auth(
+ &pmparser, param.value) < 0) {
+ ret = -1;
+ break;
+ }
+ } else if (strcmp(param.keyword, "BODY") == 0) {
+ if (smtp_params_mail_parse_body(&pmparser, param.value,
+ body_extensions) < 0) {
+ ret = -1;
+ break;
+ }
+ } else if ((caps & SMTP_CAPABILITY_DSN) != 0 &&
+ strcmp(param.keyword, "ENVID") == 0) {
+ if (smtp_params_mail_parse_envid(&pmparser,
+ param.value) < 0) {
+ ret = -1;
+ break;
+ }
+ } else if ((caps & SMTP_CAPABILITY_DSN) != 0 &&
+ strcmp(param.keyword, "RET") == 0) {
+ if (smtp_params_mail_parse_ret(&pmparser,
+ param.value) < 0) {
+ ret = -1;
+ break;
+ }
+ } else if ((caps & SMTP_CAPABILITY_SIZE) != 0 &&
+ strcmp(param.keyword, "SIZE") == 0) {
+ if (smtp_params_mail_parse_size(&pmparser,
+ param.value) < 0) {
+ ret = -1;
+ break;
+ }
+ } else if (extensions != NULL &&
+ str_array_icase_find(extensions, param.keyword)) {
+ /* add the rest to ext_param for specific
+ applications */
+ smtp_params_mail_add_extra(params_r, pool,
+ param.keyword, param.value);
+ } else {
+ /* RFC 5321, Section 4.1.1.11:
+ If the server SMTP does not recognize or cannot
+ implement one or more of the parameters associated
+ with a particular MAIL FROM or RCPT TO command, it
+ will return code 555. */
+ *error_r = "Unsupported parameters";
+ *error_code_r = SMTP_PARAM_PARSE_ERROR_NOT_SUPPORTED;
+ return -1;
+ }
+ }
+
+ if (ret < 0) {
+ *error_r = pmparser.error;
+ *error_code_r = pmparser.error_code;
+ }
+ return ret;
+}
+
+/* manipulate */
+
+void smtp_params_mail_copy(pool_t pool, struct smtp_params_mail *dst,
+ const struct smtp_params_mail *src)
+{
+ i_zero(dst);
+
+ if (src == NULL)
+ return;
+
+ dst->auth = smtp_address_clone(pool, src->auth);
+ dst->body.type = src->body.type;
+ dst->body.ext = p_strdup(pool, src->body.ext);
+ dst->envid = p_strdup(pool, src->envid);
+ dst->ret = src->ret;
+ dst->size = src->size;
+
+ smtp_params_copy(pool, &dst->extra_params, &src->extra_params);
+}
+
+void smtp_params_mail_add_extra(struct smtp_params_mail *params, pool_t pool,
+ const char *keyword, const char *value)
+{
+ smtp_params_add_one(&params->extra_params, pool, keyword, value);
+}
+
+void smtp_params_mail_encode_extra(struct smtp_params_mail *params, pool_t pool,
+ const char *keyword,
+ const unsigned char *value,
+ size_t value_len)
+{
+ smtp_params_add_encoded(&params->extra_params, pool,
+ keyword, value, value_len);
+}
+
+bool smtp_params_mail_drop_extra(struct smtp_params_mail *params,
+ const char *keyword, const char **value_r)
+{
+ return smtp_params_drop_one(&params->extra_params, keyword, value_r);
+}
+
+/* write */
+
+static void
+smtp_params_mail_write_auth(string_t *buffer, enum smtp_capability caps,
+ const struct smtp_params_mail *params)
+{
+ /* add AUTH= parameter */
+ string_t *auth_addr;
+
+ if (params->auth == NULL)
+ return;
+ if ((caps & SMTP_CAPABILITY_AUTH) == 0)
+ return;
+
+ auth_addr = t_str_new(256);
+
+ if (params->auth->localpart == NULL)
+ str_append(auth_addr, "<>");
+ else
+ smtp_address_write(auth_addr, params->auth);
+ str_append(buffer, "AUTH=");
+ smtp_xtext_encode(buffer, str_data(auth_addr), str_len(auth_addr));
+ str_append_c(buffer, ' ');
+}
+
+static void
+smtp_params_mail_write_body(string_t *buffer, enum smtp_capability caps,
+ const struct smtp_params_mail *params)
+{
+ /* BODY=<type>: RFC 6152 */
+ /* =7BIT: RFC 6152 */
+ switch (params->body.type) {
+ case SMTP_PARAM_MAIL_BODY_TYPE_UNSPECIFIED:
+ break;
+ case SMTP_PARAM_MAIL_BODY_TYPE_7BIT:
+ str_append(buffer, "BODY=7BIT ");
+ break;
+ /* =8BITMIME: RFC 6152 */
+ case SMTP_PARAM_MAIL_BODY_TYPE_8BITMIME:
+ i_assert((caps & SMTP_CAPABILITY_8BITMIME) != 0);
+ str_append(buffer, "BODY=8BITMIME ");
+ break;
+ /* =BINARYMIME: RFC 3030 */
+ case SMTP_PARAM_MAIL_BODY_TYPE_BINARYMIME:
+ i_assert((caps & SMTP_CAPABILITY_BINARYMIME) != 0 &&
+ (caps & SMTP_CAPABILITY_CHUNKING) != 0);
+ str_append(buffer, "BODY=BINARYMIME ");
+ break;
+ case SMTP_PARAM_MAIL_BODY_TYPE_EXTENSION:
+ str_append(buffer, "BODY=");
+ str_append(buffer, params->body.ext);
+ str_append_c(buffer, ' ');
+ break;
+ default:
+ i_unreached();
+ }
+}
+
+static void
+smtp_params_mail_write_envid(string_t *buffer, enum smtp_capability caps,
+ const struct smtp_params_mail *params)
+{
+ const char *envid = params->envid;
+
+ /* ENVID=<envid>: RFC 3461 */
+
+ if (envid == NULL)
+ return;
+ if ((caps & SMTP_CAPABILITY_DSN) == 0)
+ return;
+
+ str_append(buffer, "ENVID=");
+ smtp_xtext_encode(buffer, (const unsigned char *)envid, strlen(envid));
+ str_append_c(buffer, ' ');
+}
+
+static void
+smtp_params_mail_write_ret(string_t *buffer, enum smtp_capability caps,
+ const struct smtp_params_mail *params)
+{
+ if ((caps & SMTP_CAPABILITY_DSN) == 0)
+ return;
+ /* RET=<keyword>: RFC 3461 */
+ switch (params->ret) {
+ case SMTP_PARAM_MAIL_RET_UNSPECIFIED:
+ break;
+ case SMTP_PARAM_MAIL_RET_HDRS:
+ str_append(buffer, "RET=HDRS ");
+ break;
+ case SMTP_PARAM_MAIL_RET_FULL:
+ str_append(buffer, "RET=FULL ");
+ break;
+ default:
+ i_unreached();
+ }
+}
+
+static void
+smtp_params_mail_write_size(string_t *buffer, enum smtp_capability caps,
+ const struct smtp_params_mail *params)
+{
+ /* SIZE=<size-value>: RFC 1870 */
+
+ if (params->size == 0)
+ return;
+ if ((caps & SMTP_CAPABILITY_SIZE) == 0)
+ return;
+
+ /* proxy the SIZE parameter (account for additional size) */
+ str_printfa(buffer, "SIZE=%"PRIuUOFF_T" ", params->size);
+}
+
+void smtp_params_mail_write(string_t *buffer, enum smtp_capability caps,
+ const char *const *extra_params,
+ const struct smtp_params_mail *params)
+{
+ size_t init_len = str_len(buffer);
+
+ smtp_params_mail_write_auth(buffer, caps, params);
+ smtp_params_mail_write_body(buffer, caps, params);
+ smtp_params_mail_write_envid(buffer, caps, params);
+ smtp_params_mail_write_ret(buffer, caps, params);
+ smtp_params_mail_write_size(buffer, caps, params);
+
+ smtp_params_write(buffer, extra_params, &params->extra_params);
+
+ if (str_len(buffer) > init_len)
+ str_truncate(buffer, str_len(buffer)-1);
+}
+
+/* evaluate */
+
+const struct smtp_param *
+smtp_params_mail_get_extra(const struct smtp_params_mail *params,
+ const char *keyword)
+{
+ return smtp_params_get_param(&params->extra_params, keyword);
+}
+
+int smtp_params_mail_decode_extra(const struct smtp_params_mail *params,
+ const char *keyword, string_t **value_r,
+ bool allow_nul, const char **error_r)
+{
+ return smtp_params_decode_param(&params->extra_params,
+ keyword, value_r, allow_nul, error_r);
+}
+
+/* events */
+
+static void
+smtp_params_mail_add_auth_to_event(const struct smtp_params_mail *params,
+ struct event *event)
+{
+ /* AUTH: RFC 4954 */
+ if (params->auth == NULL)
+ return;
+
+ event_add_str(event, "mail_param_auth",
+ smtp_address_encode(params->auth));
+}
+
+static void
+smtp_params_mail_add_body_to_event(const struct smtp_params_mail *params,
+ struct event *event)
+{
+ /* BODY: RFC 6152 */
+ switch (params->body.type) {
+ case SMTP_PARAM_MAIL_BODY_TYPE_UNSPECIFIED:
+ break;
+ case SMTP_PARAM_MAIL_BODY_TYPE_7BIT:
+ event_add_str(event, "mail_param_body", "7BIT");
+ break;
+ case SMTP_PARAM_MAIL_BODY_TYPE_8BITMIME:
+ event_add_str(event, "mail_param_body", "8BITMIME");
+ break;
+ case SMTP_PARAM_MAIL_BODY_TYPE_BINARYMIME:
+ event_add_str(event, "mail_param_body", "BINARYMIME");
+ break;
+ case SMTP_PARAM_MAIL_BODY_TYPE_EXTENSION:
+ event_add_str(event, "mail_param_body", params->body.ext);
+ break;
+ default:
+ i_unreached();
+ }
+}
+
+static void
+smtp_params_mail_add_envid_to_event(const struct smtp_params_mail *params,
+ struct event *event)
+{
+ /* ENVID: RFC 3461, Section 4.4 */
+ if (params->envid == NULL)
+ return;
+
+ event_add_str(event, "mail_param_envid", params->envid);
+}
+
+static void
+smtp_params_mail_add_ret_to_event(const struct smtp_params_mail *params,
+ struct event *event)
+{
+ /* RET: RFC 3461, Section 4.3 */
+ switch (params->ret) {
+ case SMTP_PARAM_MAIL_RET_UNSPECIFIED:
+ break;
+ case SMTP_PARAM_MAIL_RET_HDRS:
+ event_add_str(event, "mail_param_ret", "HDRS");
+ break;
+ case SMTP_PARAM_MAIL_RET_FULL:
+ event_add_str(event, "mail_param_ret", "FULL");
+ break;
+ default:
+ i_unreached();
+ }
+}
+
+static void
+smtp_params_mail_add_size_to_event(const struct smtp_params_mail *params,
+ struct event *event)
+{
+ /* SIZE: RFC 1870 */
+ if (params->size == 0)
+ return;
+
+ event_add_int(event, "mail_param_size", params->size);
+}
+
+void smtp_params_mail_add_to_event(const struct smtp_params_mail *params,
+ struct event *event)
+{
+ smtp_params_mail_add_auth_to_event(params, event);
+ smtp_params_mail_add_body_to_event(params, event);
+ smtp_params_mail_add_envid_to_event(params, event);
+ smtp_params_mail_add_ret_to_event(params, event);
+ smtp_params_mail_add_size_to_event(params, event);
+}
+
+/*
+ * RCPT parameters
+ */
+
+/* parse */
+
+struct smtp_params_rcpt_parser {
+ pool_t pool;
+ struct smtp_params_rcpt *params;
+ enum smtp_param_rcpt_parse_flags flags;
+ enum smtp_capability caps;
+
+ enum smtp_param_parse_error error_code;
+ const char *error;
+};
+
+static int
+smtp_params_rcpt_parse_notify(struct smtp_params_rcpt_parser *prparser,
+ const char *value)
+{
+ struct smtp_params_rcpt *params = prparser->params;
+ const char *const *list;
+ bool valid, unsupported;
+
+ /* NOTIFY=<type>: RFC 3461
+
+ notify-esmtp-value = "NEVER" / 1#notify-list-element
+ notify-list-element = "SUCCESS" / "FAILURE" / "DELAY"
+
+ We check and normalize this parameter.
+ */
+
+ /* cannot specify this multiple times */
+ if (params->notify != SMTP_PARAM_RCPT_NOTIFY_UNSPECIFIED) {
+ prparser->error = "Duplicate NOTIFY= parameter";
+ prparser->error_code = SMTP_PARAM_PARSE_ERROR_BAD_SYNTAX;
+ return -1;
+ }
+ /* value required */
+ if (value == NULL) {
+ prparser->error = "Missing NOTIFY= parameter value";
+ prparser->error_code = SMTP_PARAM_PARSE_ERROR_BAD_SYNTAX;
+ return -1;
+ }
+
+ valid = TRUE;
+ unsupported = FALSE;
+ list = t_strsplit(value, ","); /* RFC 822, Section 2.7 */
+ while (*list != NULL) {
+ if (**list != '\0') {
+ /* NEVER */
+ if (strcasecmp(*list, "NEVER") == 0) {
+ if (params->notify != SMTP_PARAM_RCPT_NOTIFY_UNSPECIFIED)
+ valid = FALSE;
+ params->notify = SMTP_PARAM_RCPT_NOTIFY_NEVER;
+ /* SUCCESS */
+ } else if (strcasecmp(*list, "SUCCESS") == 0) {
+ if ((params->notify & SMTP_PARAM_RCPT_NOTIFY_NEVER) != 0)
+ valid = FALSE;
+ params->notify |= SMTP_PARAM_RCPT_NOTIFY_SUCCESS;
+ /* FAILURE */
+ } else if (strcasecmp(*list, "FAILURE") == 0) {
+ if ((params->notify & SMTP_PARAM_RCPT_NOTIFY_NEVER) != 0)
+ valid = FALSE;
+ params->notify |= SMTP_PARAM_RCPT_NOTIFY_FAILURE;
+ /* DELAY */
+ } else if (strcasecmp(*list, "DELAY") == 0) {
+ if ((params->notify & SMTP_PARAM_RCPT_NOTIFY_NEVER) != 0)
+ valid = FALSE;
+ params->notify |= SMTP_PARAM_RCPT_NOTIFY_DELAY;
+ } else {
+ unsupported = TRUE;
+ }
+ }
+ list++;
+ }
+
+ if (!valid || unsupported ||
+ params->notify == SMTP_PARAM_RCPT_NOTIFY_UNSPECIFIED) {
+ prparser->error = "Invalid NOTIFY= parameter value";
+ prparser->error_code = ((valid && unsupported) ?
+ SMTP_PARAM_PARSE_ERROR_NOT_SUPPORTED :
+ SMTP_PARAM_PARSE_ERROR_BAD_SYNTAX);
+ return -1;
+ }
+ return 0;
+}
+
+static int
+smtp_params_rcpt_parse_orcpt_rfc822(struct smtp_params_rcpt_parser *prparser,
+ const char *addr_str, pool_t pool,
+ const struct smtp_address **addr_r)
+{
+ struct message_address *rfc822_addr;
+ struct smtp_address *addr;
+
+ rfc822_addr = message_address_parse(pool_datastack_create(),
+ (const unsigned char *)addr_str,
+ strlen(addr_str), 2, 0);
+ if (rfc822_addr == NULL || rfc822_addr->next != NULL)
+ return -1;
+ if (rfc822_addr->invalid_syntax) {
+ if (HAS_NO_BITS(prparser->flags,
+ SMTP_PARAM_RCPT_FLAG_ORCPT_ALLOW_LOCALPART) ||
+ rfc822_addr->mailbox == NULL ||
+ *rfc822_addr->mailbox == '\0')
+ return -1;
+ rfc822_addr->invalid_syntax = FALSE;
+ }
+ if (smtp_address_create_from_msg(pool, rfc822_addr, &addr) < 0)
+ return -1;
+ *addr_r = addr;
+ return 0;
+}
+
+static int
+smtp_params_rcpt_parse_orcpt(struct smtp_params_rcpt_parser *prparser,
+ const char *value)
+{
+ struct smtp_params_rcpt *params = prparser->params;
+ struct smtp_parser parser;
+ const unsigned char *p, *pend;
+ string_t *address;
+ const char *addr_type;
+ int ret;
+
+ /* ORCPT=<address>: RFC 3461
+
+ orcpt-parameter = "ORCPT=" original-recipient-address
+ original-recipient-address = addr-type ";" xtext
+ addr-type = atom
+
+ We check and normalize this parameter.
+ */
+
+ /* cannot specify this multiple times */
+ if (params->orcpt.addr_type != NULL) {
+ prparser->error = "Duplicate ORCPT= parameter";
+ prparser->error_code = SMTP_PARAM_PARSE_ERROR_BAD_SYNTAX;
+ return -1;
+ }
+ /* value required */
+ if (value == NULL) {
+ prparser->error = "Missing ORCPT= parameter value";
+ prparser->error_code = SMTP_PARAM_PARSE_ERROR_BAD_SYNTAX;
+ return -1;
+ }
+
+ /* check addr-type */
+ smtp_parser_init(&parser, pool_datastack_create(), value);
+ if (smtp_parser_parse_atom(&parser, &addr_type) <= 0 ||
+ parser.cur >= parser.end || *parser.cur != ';') {
+ prparser->error = "Invalid addr-type for ORCPT= parameter";
+ prparser->error_code = SMTP_PARAM_PARSE_ERROR_BAD_SYNTAX;
+ return -1;
+ }
+ params->orcpt.addr_type = p_strdup(prparser->pool, addr_type);
+ parser.cur++;
+
+ /* check xtext */
+ address = t_str_new(256);
+ if ((ret=smtp_parser_parse_xtext(&parser, address)) <= 0 ||
+ parser.cur < parser.end) {
+ if (ret < 0) {
+ prparser->error = t_strdup_printf(
+ "Invalid ORCPT= parameter: %s",
+ parser.error);
+ prparser->error_code =
+ SMTP_PARAM_PARSE_ERROR_BAD_SYNTAX;
+ } else if (parser.cur < parser.end) {
+ prparser->error = "Invalid ORCPT= parameter: "
+ "Invalid character in xtext";
+ prparser->error_code =
+ SMTP_PARAM_PARSE_ERROR_BAD_SYNTAX;
+ } else {
+ prparser->error = "Invalid ORCPT= parameter: "
+ "Empty address value";
+ prparser->error_code =
+ SMTP_PARAM_PARSE_ERROR_BAD_SYNTAX;
+ }
+ return -1;
+ }
+
+ /* RFC 3461, Section 4.2:
+
+ Due to limitations in the Delivery Status Notification format, the
+ value of the original recipient address prior to encoding as "xtext"
+ MUST consist entirely of printable (graphic and white space)
+ characters from the US-ASCII repertoire.
+ */
+ p = str_data(address);
+ pend = p + str_len(address);
+ while (p < pend && smtp_char_is_textstr(*p))
+ p++;
+ if (p < pend) {
+ prparser->error =
+ "Invalid ORCPT= address value: "
+ "Contains non-printable characters";
+ prparser->error_code = SMTP_PARAM_PARSE_ERROR_BAD_SYNTAX;
+ return -1;
+ }
+
+ params->orcpt.addr_raw = p_strdup(prparser->pool, str_c(address));
+
+ if (strcasecmp(params->orcpt.addr_type, "rfc822") == 0) {
+ if (smtp_params_rcpt_parse_orcpt_rfc822(
+ prparser, params->orcpt.addr_raw, prparser->pool,
+ &params->orcpt.addr) < 0) {
+ prparser->error = "Invalid ORCPT= address value: "
+ "Invalid RFC822 address";
+ prparser->error_code =
+ SMTP_PARAM_PARSE_ERROR_BAD_SYNTAX;
+ return -1;
+ }
+ }
+ return 0;
+}
+
+int smtp_params_rcpt_parse(pool_t pool, const char *args,
+ enum smtp_param_rcpt_parse_flags flags,
+ enum smtp_capability caps,
+ const char *const *extensions,
+ struct smtp_params_rcpt *params_r,
+ enum smtp_param_parse_error *error_code_r,
+ const char **error_r)
+{
+ struct smtp_params_rcpt_parser prparser;
+ struct smtp_param param;
+ const char *const *argv;
+ const char *error;
+ int ret = 0;
+
+ i_zero(params_r);
+
+ i_zero(&prparser);
+ prparser.pool = pool;
+ prparser.params = params_r;
+ prparser.flags = flags;
+ prparser.caps = caps;
+
+ argv = t_strsplit(args, " ");
+ for (; *argv != NULL; argv++) {
+ if (smtp_param_parse(pool_datastack_create(), *argv,
+ &param, &error) < 0) {
+ *error_r = t_strdup_printf(
+ "Invalid RCPT parameter: %s", error);
+ *error_code_r = SMTP_PARAM_PARSE_ERROR_BAD_SYNTAX;
+ return -1;
+ }
+
+ /* parse known parameters */
+ param.keyword = t_str_ucase(param.keyword);
+ if ((caps & SMTP_CAPABILITY_DSN) != 0 &&
+ strcmp(param.keyword, "NOTIFY") == 0) {
+ if (smtp_params_rcpt_parse_notify
+ (&prparser, param.value) < 0) {
+ ret = -1;
+ break;
+ }
+ } else if (((caps & SMTP_CAPABILITY_DSN) != 0 ||
+ (caps & SMTP_CAPABILITY__ORCPT) != 0) &&
+ strcmp(param.keyword, "ORCPT") == 0) {
+ if (smtp_params_rcpt_parse_orcpt
+ (&prparser, param.value) < 0) {
+ ret = -1;
+ break;
+ }
+ } else if (extensions != NULL &&
+ str_array_icase_find(extensions, param.keyword)) {
+ /* add the rest to ext_param for specific applications
+ */
+ smtp_params_rcpt_add_extra(params_r, pool,
+ param.keyword, param.value);
+ } else {
+ /* RFC 5321, Section 4.1.1.11:
+ If the server SMTP does not recognize or cannot
+ implement one or more of the parameters associated
+ with a particular MAIL FROM or RCPT TO command, it
+ will return code 555. */
+ *error_r = "Unsupported parameters";
+ *error_code_r = SMTP_PARAM_PARSE_ERROR_NOT_SUPPORTED;
+ return -1;
+ }
+ }
+
+ if (ret < 0) {
+ *error_r = prparser.error;
+ *error_code_r = prparser.error_code;
+ }
+ return ret;
+}
+
+/* manipulate */
+
+void smtp_params_rcpt_copy(pool_t pool, struct smtp_params_rcpt *dst,
+ const struct smtp_params_rcpt *src)
+{
+ i_zero(dst);
+
+ if (src == NULL)
+ return;
+
+ dst->notify = src->notify;
+ dst->orcpt.addr_type = p_strdup(pool, src->orcpt.addr_type);
+ dst->orcpt.addr_raw = p_strdup(pool, src->orcpt.addr_raw);
+ dst->orcpt.addr = smtp_address_clone(pool, src->orcpt.addr);
+
+ smtp_params_copy(pool, &dst->extra_params, &src->extra_params);
+}
+
+void smtp_params_rcpt_add_extra(struct smtp_params_rcpt *params, pool_t pool,
+ const char *keyword, const char *value)
+{
+ smtp_params_add_one(&params->extra_params, pool, keyword, value);
+}
+
+void smtp_params_rcpt_encode_extra(struct smtp_params_rcpt *params, pool_t pool,
+ const char *keyword,
+ const unsigned char *value,
+ size_t value_len)
+{
+ smtp_params_add_encoded(&params->extra_params, pool,
+ keyword, value, value_len);
+}
+
+bool smtp_params_rcpt_drop_extra(struct smtp_params_rcpt *params,
+ const char *keyword, const char **value_r)
+{
+ return smtp_params_drop_one(&params->extra_params, keyword, value_r);
+}
+
+void smtp_params_rcpt_set_orcpt(struct smtp_params_rcpt *params, pool_t pool,
+ struct smtp_address *rcpt)
+{
+ params->orcpt.addr_type = "rfc822";
+ params->orcpt.addr = smtp_address_clone(pool, rcpt);
+ params->orcpt.addr_raw = p_strdup(pool, smtp_address_encode(rcpt));
+}
+
+/* write */
+
+static void
+smtp_params_rcpt_write_notify(string_t *buffer, enum smtp_capability caps,
+ const struct smtp_params_rcpt *params)
+{
+ if (params->notify == SMTP_PARAM_RCPT_NOTIFY_UNSPECIFIED)
+ return;
+ if ((caps & SMTP_CAPABILITY_DSN) == 0)
+ return;
+
+ /* NOTIFY=<type>: RFC 3461
+
+ notify-esmtp-value = "NEVER" / 1#notify-list-element
+ notify-list-element = "SUCCESS" / "FAILURE" / "DELAY"
+ */
+
+ str_append(buffer, "NOTIFY=");
+ if ((params->notify & SMTP_PARAM_RCPT_NOTIFY_NEVER) != 0) {
+ i_assert(params->notify == SMTP_PARAM_RCPT_NOTIFY_NEVER);
+ str_append(buffer, "NEVER");
+ } else {
+ bool comma = FALSE;
+ if ((params->notify & SMTP_PARAM_RCPT_NOTIFY_SUCCESS) != 0) {
+ str_append(buffer, "SUCCESS");
+ comma = TRUE;
+ }
+ if ((params->notify & SMTP_PARAM_RCPT_NOTIFY_FAILURE) != 0) {
+ if (comma)
+ str_append_c(buffer, ',');
+ str_append(buffer, "FAILURE");
+ comma = TRUE;
+ }
+ if ((params->notify & SMTP_PARAM_RCPT_NOTIFY_DELAY) != 0) {
+ if (comma)
+ str_append_c(buffer, ',');
+ str_append(buffer, "DELAY");
+ }
+ }
+ str_append_c(buffer, ' ');
+}
+
+static void
+smtp_params_rcpt_write_orcpt(string_t *buffer, enum smtp_capability caps,
+ const struct smtp_params_rcpt *params)
+{
+ if (!smtp_params_rcpt_has_orcpt(params))
+ return;
+ if ((caps & SMTP_CAPABILITY_DSN) == 0 &&
+ (caps & SMTP_CAPABILITY__ORCPT) == 0)
+ return;
+
+ /* ORCPT=<address>: RFC 3461 */
+
+ str_printfa(buffer, "ORCPT=%s;", params->orcpt.addr_type);
+ if (strcasecmp(params->orcpt.addr_type, "rfc822") == 0) {
+ smtp_xtext_encode_cstr(
+ buffer, smtp_address_encode(params->orcpt.addr));
+ } else {
+ i_assert(params->orcpt.addr_raw != NULL);
+ smtp_xtext_encode_cstr(buffer, params->orcpt.addr_raw);
+ }
+ str_append_c(buffer, ' ');
+}
+
+void smtp_params_rcpt_write(string_t *buffer, enum smtp_capability caps,
+ const char *const *extra_params,
+ const struct smtp_params_rcpt *params)
+{
+ size_t init_len = str_len(buffer);
+
+ smtp_params_rcpt_write_notify(buffer, caps, params);
+ smtp_params_rcpt_write_orcpt(buffer, caps, params);
+
+ smtp_params_write(buffer, extra_params, &params->extra_params);
+
+ if (str_len(buffer) > init_len)
+ str_truncate(buffer, str_len(buffer)-1);
+}
+
+/* evaluate */
+
+const struct smtp_param *
+smtp_params_rcpt_get_extra(const struct smtp_params_rcpt *params,
+ const char *keyword)
+{
+ return smtp_params_get_param(&params->extra_params, keyword);
+}
+
+int smtp_params_rcpt_decode_extra(const struct smtp_params_rcpt *params,
+ const char *keyword, string_t **value_r,
+ bool allow_nul, const char **error_r)
+{
+ return smtp_params_decode_param(&params->extra_params,
+ keyword, value_r, allow_nul, error_r);
+}
+
+bool smtp_params_rcpt_equal(const struct smtp_params_rcpt *params1,
+ const struct smtp_params_rcpt *params2)
+{
+ if (params1 == NULL || params2 == NULL)
+ return (params1 == params2);
+
+ /* NOTIFY: RFC 3461, Section 4.1 */
+ if (params1->notify != params2->notify)
+ return FALSE;
+
+ /* ORCPT: RFC 3461, Section 4.2 */
+ if (null_strcasecmp(params1->orcpt.addr_type,
+ params2->orcpt.addr_type) != 0)
+ return FALSE;
+ if (null_strcasecmp(params1->orcpt.addr_type, "rfc822") == 0) {
+ if (!smtp_address_equals(params1->orcpt.addr,
+ params2->orcpt.addr))
+ return FALSE;
+ } else {
+ if (null_strcmp(params1->orcpt.addr_raw,
+ params2->orcpt.addr_raw) != 0)
+ return FALSE;
+ }
+
+ /* extra parameters */
+ return smtp_params_equal(&params1->extra_params,
+ &params2->extra_params);
+}
+
+/* events */
+
+static void
+smtp_params_rcpt_add_notify_to_event(const struct smtp_params_rcpt *params,
+ struct event *event)
+{
+ /* NOTIFY: RFC 3461, Section 4.1 */
+ if (params->notify == SMTP_PARAM_RCPT_NOTIFY_UNSPECIFIED)
+ return;
+ if ((params->notify & SMTP_PARAM_RCPT_NOTIFY_NEVER) != 0) {
+ i_assert(params->notify ==
+ SMTP_PARAM_RCPT_NOTIFY_NEVER);
+ event_add_str(event, "rcpt_param_notify", "NEVER");
+ } else {
+ string_t *str = t_str_new(32);
+ if ((params->notify & SMTP_PARAM_RCPT_NOTIFY_SUCCESS) != 0)
+ str_append(str, "SUCCESS");
+ if ((params->notify & SMTP_PARAM_RCPT_NOTIFY_FAILURE) != 0) {
+ if (str_len(str) > 0)
+ str_append_c(str, ',');
+ str_append(str, "FAILURE");
+ }
+ if ((params->notify & SMTP_PARAM_RCPT_NOTIFY_DELAY) != 0) {
+ if (str_len(str) > 0)
+ str_append_c(str, ',');
+ str_append(str, "DELAY");
+ }
+ event_add_str(event, "rcpt_param_notify", str_c(str));
+ }
+}
+
+static void
+smtp_params_rcpt_add_orcpt_to_event(const struct smtp_params_rcpt *params,
+ struct event *event)
+{
+ /* ORCPT: RFC 3461, Section 4.2 */
+ if (params->orcpt.addr_type == NULL)
+ return;
+
+ event_add_str(event, "rcpt_param_orcpt_type",
+ params->orcpt.addr_type);
+ if (strcasecmp(params->orcpt.addr_type, "rfc822") == 0) {
+ event_add_str(event, "rcpt_param_orcpt",
+ smtp_address_encode(params->orcpt.addr));
+ } else {
+ i_assert(params->orcpt.addr_raw != NULL);
+ event_add_str(event, "rcpt_param_orcpt",
+ params->orcpt.addr_raw);
+ }
+}
+
+void smtp_params_rcpt_add_to_event(const struct smtp_params_rcpt *params,
+ struct event *event)
+{
+ smtp_params_rcpt_add_notify_to_event(params, event);
+ smtp_params_rcpt_add_orcpt_to_event(params, event);
+}
diff --git a/src/lib-smtp/smtp-params.h b/src/lib-smtp/smtp-params.h
new file mode 100644
index 0000000..34568ce
--- /dev/null
+++ b/src/lib-smtp/smtp-params.h
@@ -0,0 +1,233 @@
+#ifndef SMTP_PARAMS_H
+#define SMTP_PARAMS_H
+
+#include "array-decl.h"
+
+#include "smtp-common.h"
+
+struct smtp_param;
+
+ARRAY_DEFINE_TYPE(smtp_param, struct smtp_param);
+
+enum smtp_param_mail_body_type {
+ SMTP_PARAM_MAIL_BODY_TYPE_UNSPECIFIED = 0,
+ SMTP_PARAM_MAIL_BODY_TYPE_7BIT,
+ SMTP_PARAM_MAIL_BODY_TYPE_8BITMIME,
+ SMTP_PARAM_MAIL_BODY_TYPE_BINARYMIME,
+ SMTP_PARAM_MAIL_BODY_TYPE_EXTENSION
+};
+
+enum smtp_param_mail_ret {
+ SMTP_PARAM_MAIL_RET_UNSPECIFIED = 0,
+ SMTP_PARAM_MAIL_RET_HDRS,
+ SMTP_PARAM_MAIL_RET_FULL,
+};
+
+enum smtp_param_rcpt_notify {
+ SMTP_PARAM_RCPT_NOTIFY_UNSPECIFIED = 0x00,
+ SMTP_PARAM_RCPT_NOTIFY_SUCCESS = 0x01,
+ SMTP_PARAM_RCPT_NOTIFY_FAILURE = 0x02,
+ SMTP_PARAM_RCPT_NOTIFY_DELAY = 0x04,
+ SMTP_PARAM_RCPT_NOTIFY_NEVER = 0x80
+};
+
+struct smtp_param {
+ const char *keyword;
+ const char *value;
+};
+
+struct smtp_params_mail {
+ /* AUTH: RFC 4954 */
+ const struct smtp_address *auth;
+ /* BODY: RFC 6152 */
+ struct {
+ enum smtp_param_mail_body_type type;
+ const char *ext;
+ } body;
+ /* ENVID: RFC 3461, Section 4.4 */
+ const char *envid;
+ /* RET: RFC 3461, Section 4.3 */
+ enum smtp_param_mail_ret ret;
+ /* SIZE: RFC 1870 */
+ uoff_t size;
+ /* extra parameters */
+ ARRAY_TYPE(smtp_param) extra_params;
+};
+
+struct smtp_params_rcpt {
+ /* ORCPT: RFC 3461, Section 4.2 */
+ struct {
+ const char *addr_type;
+ /* addr_type=rfc822 */
+ const struct smtp_address *addr;
+ /* raw value */
+ const char *addr_raw;
+ } orcpt;
+ /* NOTIFY: RFC 3461, Section 4.1 */
+ enum smtp_param_rcpt_notify notify;
+ /* extra parameters */
+ ARRAY_TYPE(smtp_param) extra_params;
+};
+
+enum smtp_param_parse_error {
+ SMTP_PARAM_PARSE_ERROR_BAD_SYNTAX = 0,
+ SMTP_PARAM_PARSE_ERROR_NOT_SUPPORTED
+};
+
+/*
+ * Common
+ */
+
+/* parse */
+
+int smtp_param_parse(pool_t pool, const char *text,
+ struct smtp_param *param_r, const char **error_r);
+
+/* manipulate */
+
+void smtp_params_copy(pool_t pool, ARRAY_TYPE(smtp_param) *dst,
+ const ARRAY_TYPE(smtp_param) *src) ATTR_NULL(3);
+
+void smtp_params_add_one(ARRAY_TYPE(smtp_param) *params, pool_t pool,
+ const char *keyword, const char *value);
+void smtp_params_add_encoded(ARRAY_TYPE(smtp_param) *params, pool_t pool,
+ const char *keyword, const unsigned char *value,
+ size_t value_len);
+
+bool smtp_params_drop_one(ARRAY_TYPE(smtp_param) *params, const char *keyword,
+ const char **value_r);
+
+/* write */
+
+void smtp_param_write(string_t *out, const struct smtp_param *param);
+
+/* evaluate */
+
+const struct smtp_param *
+smtp_params_get_param(const ARRAY_TYPE(smtp_param) *params,
+ const char *keyword);
+int smtp_params_decode_param(const ARRAY_TYPE(smtp_param) *params,
+ const char *keyword, string_t **value_r,
+ bool allow_nul, const char **error_r);
+
+bool smtp_params_equal(const ARRAY_TYPE(smtp_param) *params1,
+ const ARRAY_TYPE(smtp_param) *params2);
+
+/*
+ * MAIL parameters
+ */
+
+/* parse */
+
+int smtp_params_mail_parse(pool_t pool, const char *args,
+ enum smtp_capability caps,
+ const char *const *param_extensions,
+ const char *const *body_param_extensions,
+ struct smtp_params_mail *params_r,
+ enum smtp_param_parse_error *error_code_r,
+ const char **error_r) ATTR_NULL(4, 5);
+
+/* manipulate */
+
+void smtp_params_mail_copy(pool_t pool, struct smtp_params_mail *dst,
+ const struct smtp_params_mail *src);
+
+void smtp_params_mail_add_extra(struct smtp_params_mail *params, pool_t pool,
+ const char *keyword, const char *value)
+ ATTR_NULL(4);
+void smtp_params_mail_encode_extra(struct smtp_params_mail *params, pool_t pool,
+ const char *keyword,
+ const unsigned char *value,
+ size_t value_len);
+bool smtp_params_mail_drop_extra(struct smtp_params_mail *params,
+ const char *keyword, const char **value_r)
+ ATTR_NULL(3);
+
+/* write */
+
+void smtp_params_mail_write(string_t *buffer, enum smtp_capability caps,
+ const char *const *extra_params,
+ const struct smtp_params_mail *params) ATTR_NULL(3);
+
+/* evaluate */
+
+const struct smtp_param *
+smtp_params_mail_get_extra(const struct smtp_params_mail *params,
+ const char *keyword);
+int smtp_params_mail_decode_extra(const struct smtp_params_mail *params,
+ const char *keyword, string_t **value_r,
+ bool allow_nul, const char **error_r);
+
+/* events */
+
+void smtp_params_mail_add_to_event(const struct smtp_params_mail *params,
+ struct event *event);
+
+/*
+ * RCPT parameters
+ */
+
+/* parse */
+
+enum smtp_param_rcpt_parse_flags {
+ /* Allow address values without a domain part */
+ SMTP_PARAM_RCPT_FLAG_ORCPT_ALLOW_LOCALPART = BIT(0),
+};
+
+int smtp_params_rcpt_parse(pool_t pool, const char *args,
+ enum smtp_param_rcpt_parse_flags flags,
+ enum smtp_capability caps,
+ const char *const *param_extensions,
+ struct smtp_params_rcpt *params_r,
+ enum smtp_param_parse_error *error_code_r,
+ const char **error_r) ATTR_NULL(4);
+
+/* manipulate */
+
+void smtp_params_rcpt_copy(pool_t pool, struct smtp_params_rcpt *dst,
+ const struct smtp_params_rcpt *src) ATTR_NULL(3);
+
+void smtp_params_rcpt_add_extra(struct smtp_params_rcpt *params, pool_t pool,
+ const char *keyword, const char *value)
+ ATTR_NULL(4);
+void smtp_params_rcpt_encode_extra(struct smtp_params_rcpt *params, pool_t pool,
+ const char *keyword,
+ const unsigned char *value,
+ size_t value_len);
+bool smtp_params_rcpt_drop_extra(struct smtp_params_rcpt *params,
+ const char *keyword, const char **value_r)
+ ATTR_NULL(3);
+
+void smtp_params_rcpt_set_orcpt(struct smtp_params_rcpt *params, pool_t pool,
+ struct smtp_address *rcpt);
+
+/* write */
+
+void smtp_params_rcpt_write(string_t *buffer, enum smtp_capability caps,
+ const char *const *extra_params,
+ const struct smtp_params_rcpt *params) ATTR_NULL(3);
+
+/* evaluate */
+
+const struct smtp_param *
+smtp_params_rcpt_get_extra(const struct smtp_params_rcpt *params,
+ const char *keyword);
+int smtp_params_rcpt_decode_extra(const struct smtp_params_rcpt *params,
+ const char *keyword, string_t **value_r,
+ bool allow_nul, const char **error_r);
+
+bool smtp_params_rcpt_equal(const struct smtp_params_rcpt *params1,
+ const struct smtp_params_rcpt *params2);
+
+static inline bool
+smtp_params_rcpt_has_orcpt(const struct smtp_params_rcpt *params)
+{
+ return (params->orcpt.addr_type != NULL);
+}
+
+/* events */
+
+void smtp_params_rcpt_add_to_event(const struct smtp_params_rcpt *params,
+ struct event *event);
+
+#endif
diff --git a/src/lib-smtp/smtp-parser.c b/src/lib-smtp/smtp-parser.c
new file mode 100644
index 0000000..5672ff0
--- /dev/null
+++ b/src/lib-smtp/smtp-parser.c
@@ -0,0 +1,587 @@
+/* Copyright (c) 2013-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "net.h"
+#include "str.h"
+#include "strescape.h"
+
+#include "smtp-parser.h"
+
+#include <ctype.h>
+
+/* Character definitions from RFC 5321/5322:
+
+ textstring = 1*(%d09 / %d32-126) ; HT, SP, Printable US-ASCII
+ = 1*(%x09 / %x20-7e)
+ ehlo-param = 1*(%d33-126)
+ = 1*(%x21-7e)
+ ehlo-greet = 1*(%d0-9 / %d11-12 / %d14-127)
+ = 1*(%x00-09 / %x0b-0c / %x0e-7f)
+ qtext = %d32-33 / %d35-91 / %d93-126
+ = %x20-21 / %x23-5B / %x5d-7e
+ quoted-pair = %d92 %d32-126
+ = %x5c %x20-7e
+ atext = ALPHA / DIGIT / ; Printable US-ASCII
+ "!" / "#" / ; characters not including
+ "$" / "%" / ; specials. Used for atoms.
+ "&" / "'" /
+ "*" / "+" /
+ "-" / "/" /
+ "=" / "?" /
+ "^" / "_" /
+ "`" / "{" /
+ "|" / "}" /
+ "~"
+ = %x21 / %x23-27 / %x2a-2b / %x2d / %x2f-39 / %x3d /
+ %d3f / %x41-5a / %x5e-7e /
+ esmtp-value = 1*(%d33-60 / %d62-126)
+ = 1*(%x21-3c / %x3e-7e)
+ dcontent = %d33-90 / ; Printable US-ASCII
+ %d94-126 ; excl. "[", "\", "]"
+ = %x21-5a / %x5e-7e
+ xchar = any ASCII CHAR between "!" (33) and "~" (126) inclusive,
+ except for "+" and "=". [RFC 3461]
+ = %x21-2a / %2c-3c / %x3e-7e
+
+ Bit mappings (FIXME: rearrange):
+
+ (1<<0) => %x21-2a / %2c-3c / %x3e-7e (xtext)
+ (1<<1) => %x21 / %x23-27 / %x2a-2b / %x2d / %x2f-39 / %x3d /
+ %d3f / %x41-5a / %x5e-7e /
+ (1<<2) => %x28-29 / %x2c / %x2e / %x3a-3c / %x3e / %x40
+ (1<<8) => %x00-09 / %x0b-0c / %x0e-20 / %x7f
+ (1<<5) => %x09 / %5b-5d
+ (1<<4) => %x5b / %x5d
+ (1<<3) => %x20
+ (1<<9) => %x22
+ (1<<6) => %x2b
+ (1<<7) => %x3d
+ */
+
+/* xtext */
+const uint16_t smtp_xtext_char_mask = (1<<0);
+/* atext */
+const uint16_t smtp_atext_char_mask = (1<<1);
+/* dcontent */
+const uint16_t smtp_dcontent_char_mask = (1<<1)|(1<<2)|(1<<9);
+/* qtext */
+const uint16_t smtp_qtext_char_mask = (1<<1)|(1<<2)|(1<<3)|(1<<4);
+/* textstring */
+const uint16_t smtp_textstr_char_mask = (1<<1)|(1<<2)|(1<<9)|(1<<3)|(1<<5);
+/* esmtp-value */
+const uint16_t smtp_esmtp_value_char_mask = (1<<0)|(1<<6);
+/* ehlo-param */
+const uint16_t smtp_ehlo_param_char_mask = (1<<0)|(1<<6)|(1<<7);
+/* ehlo-greet */
+const uint16_t smtp_ehlo_greet_char_mask = (1<<0)|(1<<6)|(1<<7)|(1<<8);
+/* quoted-pair */
+const uint16_t smtp_qpair_char_mask = (1<<0)|(1<<3)|(1<<6)|(1<<7);
+
+const uint16_t smtp_char_lookup[256] = {
+ 0x100, 0x100, 0x100, 0x100, 0x100, 0x100, 0x100, 0x100, // 00
+ 0x100, 0x120, 0x000, 0x100, 0x100, 0x000, 0x100, 0x100, // 08
+ 0x100, 0x100, 0x100, 0x100, 0x100, 0x100, 0x100, 0x100, // 10
+ 0x100, 0x100, 0x100, 0x100, 0x100, 0x100, 0x100, 0x100, // 18
+ 0x108, 0x003, 0x201, 0x003, 0x003, 0x003, 0x003, 0x003, // 20
+ 0x005, 0x005, 0x003, 0x042, 0x005, 0x003, 0x005, 0x003, // 28
+ 0x003, 0x003, 0x003, 0x003, 0x003, 0x003, 0x003, 0x003, // 30
+ 0x003, 0x003, 0x005, 0x005, 0x005, 0x082, 0x005, 0x003, // 38
+ 0x005, 0x003, 0x003, 0x003, 0x003, 0x003, 0x003, 0x003, // 40
+ 0x003, 0x003, 0x003, 0x003, 0x003, 0x003, 0x003, 0x003, // 48
+ 0x003, 0x003, 0x003, 0x003, 0x003, 0x003, 0x003, 0x003, // 50
+ 0x003, 0x003, 0x003, 0x031, 0x021, 0x031, 0x003, 0x003, // 58
+ 0x003, 0x003, 0x003, 0x003, 0x003, 0x003, 0x003, 0x003, // 60
+ 0x003, 0x003, 0x003, 0x003, 0x003, 0x003, 0x003, 0x003, // 68
+ 0x003, 0x003, 0x003, 0x003, 0x003, 0x003, 0x003, 0x003, // 70
+ 0x003, 0x003, 0x003, 0x003, 0x003, 0x003, 0x003, 0x100, // 78
+
+ 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, // 80
+ 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, // 88
+ 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, // 90
+ 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, // 98
+ 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, // a0
+ 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, // a8
+ 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, // b0
+ 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, // b8
+ 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, // c0
+ 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, // c8
+ 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, // d0
+ 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, // d8
+ 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, // e0
+ 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, // e8
+ 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, // f0
+ 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, // f8
+};
+
+/*
+ * Parser
+ */
+
+void smtp_parser_init(struct smtp_parser *parser,
+ pool_t pool, const char *data)
+{
+ parser->pool = pool;
+ parser->begin = parser->cur = (const unsigned char *)data;
+ parser->end = parser->begin + strlen(data);
+ parser->error = NULL;
+}
+
+/*
+ * Common syntax
+ */
+
+static int
+smtp_parser_parse_ldh_str(struct smtp_parser *parser,
+ string_t *out)
+{
+ const unsigned char *pbegin = parser->cur, *palnum;
+
+ /* Ldh-str = *( ALPHA / DIGIT / "-" ) Let-dig
+ Let-dig = ALPHA / DIGIT
+ */
+
+ /* Ldh-str */
+ palnum = NULL;
+ while (parser->cur < parser->end) {
+ if (i_isalnum(*parser->cur))
+ palnum = parser->cur;
+ else if (*parser->cur != '-')
+ break;
+ parser->cur++;
+ }
+ if (parser->cur == pbegin || palnum == NULL) {
+ parser->cur = pbegin;
+ return 0;
+ }
+
+ parser->cur = palnum+1;
+ if (out != NULL)
+ str_append_data(out, pbegin, parser->cur - pbegin);
+ return 1;
+}
+
+int smtp_parser_parse_domain(struct smtp_parser *parser,
+ const char **value_r)
+{
+ string_t *value = NULL;
+
+ /* Domain = sub-domain *("." sub-domain)
+ sub-domain = Let-dig [Ldh-str]
+ Let-dig = ALPHA / DIGIT
+ Ldh-str = *( ALPHA / DIGIT / "-" ) Let-dig
+
+ NOTE: A more generic syntax is accepted to be lenient towards
+ systems that don't adhere to the standards. It allows
+ '-' and '_' to occur anywhere in a sub-domain.
+ */
+
+ /* Let-dig (first) (nope) */
+ if (parser->cur >= parser->end ||
+ (!i_isalnum(*parser->cur) && *parser->cur != '-' &&
+ *parser->cur != '_'))
+ return 0;
+
+ if (value_r != NULL)
+ value = t_str_new(256);
+
+ for (;;) {
+ /* Let-dig (nope) */
+ if (parser->cur >= parser->end || *parser->cur == '.') {
+ parser->error = "Empty sub-domain";
+ return -1;
+ }
+ if (!i_isalnum(*parser->cur) && *parser->cur != '-' &&
+ *parser->cur != '_') {
+ parser->error = "Invalid character in domain";
+ return -1;
+ }
+ if (value_r != NULL)
+ str_append_c(value, *parser->cur);
+ parser->cur++;
+
+ /* Ldh-str (nope) */
+ while (parser->cur < parser->end) {
+ if (!i_isalnum(*parser->cur) && *parser->cur != '-' &&
+ *parser->cur != '_')
+ break;
+
+ if (value_r != NULL)
+ str_append_c(value, *parser->cur);
+ parser->cur++;
+ }
+
+ /* *("." sub-domain) */
+ if (parser->cur >= parser->end || *parser->cur != '.')
+ break;
+
+ if (value_r != NULL)
+ str_append_c(value, '.');
+ parser->cur++;
+ }
+
+ if (value_r != NULL)
+ *value_r = str_c(value);
+ return 1;
+}
+
+static int
+smtp_parser_parse_snum(struct smtp_parser *parser, string_t *literal,
+ uint8_t *octet_r)
+{
+ const unsigned char *pbegin = parser->cur;
+ uint8_t octet = 0;
+
+ /* Snum = 1*3DIGIT
+ ; representing a decimal integer
+ ; value in the range 0 through 255
+ */
+
+ if (*parser->cur < '0' || *parser->cur > '9')
+ return 0;
+ do {
+ if (octet >= ((uint8_t)-1 / 10)) {
+ if (octet > (uint8_t)-1 / 10)
+ return -1;
+ if ((uint8_t)(*parser->cur - '0') > ((uint8_t)-1 % 10))
+ return -1;
+ }
+ octet = octet * 10 + (*parser->cur - '0');
+ parser->cur++;
+ } while (*parser->cur >= '0' && *parser->cur <= '9');
+
+ if (literal != NULL)
+ str_append_data(literal, pbegin, parser->cur - pbegin);
+ *octet_r = octet;
+ return 1;
+}
+
+static int
+smtp_parser_parse_ipv4_address(struct smtp_parser *parser,
+ string_t *literal, struct in_addr *ip4_r)
+{
+ uint8_t octet;
+ uint32_t ip = 0;
+ int ret;
+ int i;
+
+ /* IPv4-address-literal = Snum 3("." Snum) */
+ if ((ret = smtp_parser_parse_snum(parser, literal, &octet)) <= 0)
+ return ret;
+ ip = octet;
+
+ for (i = 0; i < 3 && parser->cur < parser->end; i++) {
+ if (*parser->cur != '.')
+ return -1;
+
+ if (literal != NULL)
+ str_append_c(literal, '.');
+ parser->cur++;
+
+ if (smtp_parser_parse_snum(parser, literal, &octet) <= 0)
+ return -1;
+ ip = (ip << 8) + octet;
+ }
+
+ if (ip4_r != NULL)
+ ip4_r->s_addr = htonl(ip);
+ return 1;
+}
+
+int smtp_parser_parse_address_literal(struct smtp_parser *parser,
+ const char **value_r, struct ip_addr *ip_r)
+{
+ const unsigned char *pblock;
+ struct in_addr ip4;
+ struct in6_addr ip6;
+ bool ipv6 = FALSE;
+ string_t *value = NULL, *tagbuf;
+ int ret;
+
+ /* address-literal = "[" ( IPv4-address-literal /
+ IPv6-address-literal /
+ General-address-literal ) "]"
+ ; See Section 4.1.3
+
+ IPv6-address-literal = "IPv6:" IPv6-addr
+ General-address-literal = Standardized-tag ":" 1*dcontent
+ Standardized-tag = Ldh-str
+ ; Standardized-tag MUST be specified in a
+ ; Standards-Track RFC and registered with
+ ; IANA
+ dcontent = %d33-90 / ; Printable US-ASCII
+ %d94-126 ; excl. "[", "\", "]"
+ */
+
+ /* "[" */
+ if (parser->cur >= parser->end || *parser->cur != '[')
+ return 0;
+ parser->cur++;
+
+ if (value_r != NULL) {
+ value = t_str_new(128);
+ str_append_c(value, '[');
+ }
+ if (ip_r != NULL)
+ i_zero(ip_r);
+
+ /* IPv4-address-literal / ... */
+ i_zero(&ip4);
+ if ((ret=smtp_parser_parse_ipv4_address(parser, value, &ip4)) != 0) {
+ if (ret < 0) {
+ parser->error = "Invalid IPv4 address literal";
+ return -1;
+ }
+ if (ip_r != NULL) {
+ ip_r->family = AF_INET;
+ ip_r->u.ip4 = ip4;
+ }
+
+ /* ... / IPv6-address-literal / General-address-literal */
+ } else {
+ /* IPv6-address-literal = "IPv6:" IPv6-addr
+ General-address-literal = Standardized-tag ":" 1*dcontent
+ Standardized-tag = Ldh-str
+ */
+ if (value_r != NULL) {
+ tagbuf = value;
+ } else {
+ tagbuf = t_str_new(16);
+ str_append_c(tagbuf, '[');
+ }
+ if (smtp_parser_parse_ldh_str(parser, tagbuf) <= 0 ||
+ parser->cur >= parser->end || *parser->cur != ':') {
+ parser->error = "Invalid address literal";
+ return -1;
+ }
+ if (strcasecmp(str_c(tagbuf)+1, "IPv6") == 0)
+ ipv6 = TRUE;
+ else if (value_r == NULL) {
+ parser->error = t_strdup_printf(
+ "Unsupported %s address literal",
+ str_c(tagbuf)+1);
+ return -1;
+ }
+ parser->cur++;
+ if (value_r != NULL)
+ str_append_c(value, ':');
+
+ /* 1*dcontent */
+ pblock = parser->cur;
+ while (parser->cur < parser->end &&
+ smtp_char_is_dcontent(*parser->cur))
+ parser->cur++;
+
+ if (parser->cur == pblock) {
+ parser->error = "Empty address literal";
+ return -1;
+ }
+ if (value_r != NULL)
+ str_append_data(value, pblock, parser->cur - pblock);
+
+ if (ipv6) {
+ i_zero(&ip6);
+ if (inet_pton(AF_INET6, t_strndup(pblock,
+ parser->cur - pblock), &ip6) <= 0) {
+ parser->error = "Invalid IPv6 address literal";
+ return -1;
+ }
+ if (ip_r != NULL) {
+ ip_r->family = AF_INET6;
+ ip_r->u.ip6 = ip6;
+ }
+ }
+ }
+
+ /* ']' */
+ if (parser->cur >= parser->end) {
+ parser->error = "Missing ']' at end of address literal";
+ return -1;
+ } else if (*parser->cur != ']') {
+ parser->error = "Invalid character in address literal";
+ return -1;
+ }
+
+ parser->cur++;
+ if (value_r != NULL) {
+ str_append_c(value, ']');
+ *value_r = str_c(value);
+ }
+ return 1;
+}
+
+int smtp_parser_parse_quoted_string(struct smtp_parser *parser,
+ const char **value_r)
+{
+ string_t *value = NULL;
+ const unsigned char *pbegin;
+
+ /* Quoted-string = DQUOTE *QcontentSMTP DQUOTE
+ QcontentSMTP = qtextSMTP / quoted-pairSMTP
+ quoted-pairSMTP = %d92 %d32-126
+ ; i.e., backslash followed by any ASCII
+ ; graphic (including itself) or SPace
+ qtextSMTP = %d32-33 / %d35-91 / %d93-126
+ ; i.e., within a quoted string, any
+ ; ASCII graphic or space is permitted
+ ; without blackslash-quoting except
+ ; double-quote and the backslash itself.
+ */
+
+ /* DQUOTE */
+ if (parser->cur >= parser->end || *parser->cur != '"')
+ return 0;
+ parser->cur++;
+
+ if (value_r != NULL)
+ value = t_str_new(256);
+
+ /* *QcontentSMTP */
+ while (parser->cur < parser->end) {
+ pbegin = parser->cur;
+ while (parser->cur < parser->end &&
+ smtp_char_is_qtext(*parser->cur)) {
+ /* qtextSMTP */
+ parser->cur++;
+ }
+
+ if (value_r != NULL)
+ str_append_data(value, pbegin, parser->cur - pbegin);
+
+ if (parser->cur >= parser->end || *parser->cur != '\\')
+ break;
+ parser->cur++;
+
+ /* quoted-pairSMTP */
+ if (parser->cur >= parser->end ||
+ !smtp_char_is_qpair(*parser->cur)) {
+ parser->error =
+ "Invalid character after '\\' in quoted string";
+ return -1;
+ }
+
+ if (value_r != NULL)
+ str_append_c(value, *parser->cur);
+ parser->cur++;
+ }
+
+ /* DQUOTE */
+ if (parser->cur >= parser->end) {
+ parser->error = "Premature end of quoted string";
+ return -1;
+ }
+ if (*parser->cur != '"') {
+ parser->error = "Invalid character in quoted string";
+ return -1;
+ }
+ parser->cur++;
+ if (value_r != NULL)
+ *value_r = str_c(value);
+ return 1;
+}
+
+static int
+smtp_parser_skip_atom(struct smtp_parser *parser)
+{
+ /* Atom = 1*atext */
+
+ if (parser->cur >= parser->end || !smtp_char_is_atext(*parser->cur))
+ return 0;
+ parser->cur++;
+
+ while (parser->cur < parser->end && smtp_char_is_atext(*parser->cur))
+ parser->cur++;
+ return 1;
+}
+
+int smtp_parser_parse_atom(struct smtp_parser *parser,
+ const char **value_r)
+{
+ const unsigned char *pbegin = parser->cur;
+ int ret;
+
+ if ((ret=smtp_parser_skip_atom(parser)) <= 0)
+ return ret;
+
+ if (value_r != NULL)
+ *value_r = t_strndup(pbegin, parser->cur - pbegin);
+ return 1;
+}
+
+int smtp_parser_parse_string(struct smtp_parser *parser,
+ const char **value_r)
+{
+ int ret;
+
+ /* String = Atom / Quoted-string */
+
+ if ((ret=smtp_parser_parse_quoted_string(parser, value_r)) != 0)
+ return ret;
+ return smtp_parser_parse_atom(parser, value_r);
+}
+
+static bool
+smtp_parse_xtext_hexdigit(const unsigned char digit,
+ unsigned char *hexvalue)
+{
+ switch (digit) {
+ case '0': case '1': case '2': case '3': case '4':
+ case '5': case '6': case '7': case '8': case '9':
+ *hexvalue = (*hexvalue) << 4;
+ *hexvalue += digit - '0';
+ break;
+ case 'A': case 'B': case 'C': case 'D': case 'E': case 'F':
+ *hexvalue = (*hexvalue) << 4;
+ *hexvalue += digit - 'A' + 10;
+ break;
+ default:
+ return FALSE;
+ }
+ return TRUE;
+}
+
+int smtp_parser_parse_xtext(struct smtp_parser *parser,
+ string_t *out)
+{
+ unsigned char hexchar;
+
+ /* xtext = *( xchar / hexchar )
+ xchar = any ASCII CHAR between "!" (33) and "~" (126) inclusive,
+ except for "+" and "=".
+ hexchar = ASCII "+" immediately followed by two upper case
+ hexadecimal digits
+ */
+ if (parser->cur >= parser->end ||
+ (!smtp_char_is_xtext(*parser->cur) && *parser->cur != '+'))
+ return 0;
+
+ while (parser->cur < parser->end) {
+ const unsigned char *pbegin = parser->cur;
+
+ while (parser->cur < parser->end &&
+ smtp_char_is_xtext(*parser->cur))
+ parser->cur++;
+
+ if (out != NULL)
+ str_append_data(out, pbegin, parser->cur - pbegin);
+
+ if (parser->cur >= parser->end || *parser->cur != '+')
+ break;
+ parser->cur++;
+
+ hexchar = 0;
+ if (smtp_parse_xtext_hexdigit(*parser->cur, &hexchar)) {
+ parser->cur++;
+ if (smtp_parse_xtext_hexdigit(*parser->cur, &hexchar)) {
+ parser->cur++;
+ if (out != NULL)
+ str_append_c(out, hexchar);
+ continue;
+ }
+ }
+
+ parser->error = "Invalid hexchar after '+' in xtext";
+ return -1;
+ }
+
+ return 1;
+}
diff --git a/src/lib-smtp/smtp-parser.h b/src/lib-smtp/smtp-parser.h
new file mode 100644
index 0000000..9b76d2c
--- /dev/null
+++ b/src/lib-smtp/smtp-parser.h
@@ -0,0 +1,89 @@
+#ifndef SMTP_PARSER_H
+#define SMTP_PARSER_H
+
+/*
+ * Character definitions
+ */
+
+extern const uint16_t smtp_xtext_char_mask;
+extern const uint16_t smtp_atext_char_mask;
+extern const uint16_t smtp_dcontent_char_mask;
+extern const uint16_t smtp_qtext_char_mask;
+extern const uint16_t smtp_textstr_char_mask;
+extern const uint16_t smtp_esmtp_value_char_mask;
+extern const uint16_t smtp_ehlo_param_char_mask;
+extern const uint16_t smtp_ehlo_greet_char_mask;
+extern const uint16_t smtp_qpair_char_mask;
+
+extern const uint16_t smtp_char_lookup[256];
+
+static inline bool
+smtp_char_is_xtext(unsigned char ch) {
+ return (smtp_char_lookup[ch] & smtp_xtext_char_mask) != 0;
+}
+static inline bool
+smtp_char_is_atext(unsigned char ch) {
+ return (smtp_char_lookup[ch] & smtp_atext_char_mask) != 0;
+}
+static inline bool
+smtp_char_is_dcontent(unsigned char ch) {
+ return (smtp_char_lookup[ch] & smtp_dcontent_char_mask) != 0;
+}
+static inline bool
+smtp_char_is_qtext(unsigned char ch) {
+ return (smtp_char_lookup[ch] & smtp_qtext_char_mask) != 0;
+}
+static inline bool
+smtp_char_is_textstr(unsigned char ch) {
+ return (smtp_char_lookup[ch] & smtp_textstr_char_mask) != 0;
+}
+static inline bool
+smtp_char_is_esmtp_value(unsigned char ch) {
+ return (smtp_char_lookup[ch] & smtp_esmtp_value_char_mask) != 0;
+}
+static inline bool
+smtp_char_is_ehlo_param(unsigned char ch) {
+ return (smtp_char_lookup[ch] & smtp_ehlo_param_char_mask) != 0;
+}
+static inline bool
+smtp_char_is_ehlo_greet(unsigned char ch) {
+ return (smtp_char_lookup[ch] & smtp_ehlo_greet_char_mask) != 0;
+}
+static inline bool
+smtp_char_is_qpair(unsigned char ch) {
+ return (smtp_char_lookup[ch] & smtp_qpair_char_mask) != 0;
+}
+
+/*
+ * SMTP parser
+ */
+
+struct smtp_parser {
+ pool_t pool;
+ const char *error;
+
+ const unsigned char *begin, *cur, *end;
+};
+
+void smtp_parser_init(struct smtp_parser *parser,
+ pool_t pool, const char *data);
+string_t *smtp_parser_get_tmpbuf(struct smtp_parser *parser, size_t size);
+
+/*
+ * Common syntax
+ */
+
+int smtp_parser_parse_domain(struct smtp_parser *parser,
+ const char **value_r);
+int smtp_parser_parse_address_literal(struct smtp_parser *parser,
+ const char **value_r, struct ip_addr *ip_r);
+int smtp_parser_parse_atom(struct smtp_parser *parser,
+ const char **value_r);
+int smtp_parser_parse_quoted_string(struct smtp_parser *parser,
+ const char **value_r);
+int smtp_parser_parse_string(struct smtp_parser *parser,
+ const char **value_r);
+int smtp_parser_parse_xtext(struct smtp_parser *parser,
+ string_t *out);
+
+#endif
diff --git a/src/lib-smtp/smtp-reply-parser.c b/src/lib-smtp/smtp-reply-parser.c
new file mode 100644
index 0000000..34dc27d
--- /dev/null
+++ b/src/lib-smtp/smtp-reply-parser.c
@@ -0,0 +1,657 @@
+/* Copyright (c) 2013-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "str.h"
+#include "strfuncs.h"
+#include "istream.h"
+#include "smtp-parser.h"
+
+#include "smtp-reply-parser.h"
+
+#include <ctype.h>
+
+/* From RFC 5321:
+
+ Reply-line = *( Reply-code "-" [ textstring ] CRLF )
+ Reply-code [ SP textstring ] CRLF
+ Reply-code = %x32-35 %x30-35 %x30-39
+ textstring = 1*(%d09 / %d32-126) ; HT, SP, Printable US-ASCII
+
+ Greeting = ( "220 " (Domain / address-literal)
+ [ SP textstring ] CRLF ) /
+ ( "220-" (Domain / address-literal)
+ [ SP textstring ] CRLF
+ *( "220-" [ textstring ] CRLF )
+ "220" [ SP textstring ] CRLF )
+
+ ehlo-ok-rsp = ( "250" SP Domain [ SP ehlo-greet ] CRLF )
+ / ( "250-" Domain [ SP ehlo-greet ] CRLF
+ *( "250-" ehlo-line CRLF )
+ "250" SP ehlo-line CRLF )
+ ehlo-greet = 1*(%d0-9 / %d11-12 / %d14-127)
+ ; string of any characters other than CR or LF
+ ehlo-line = ehlo-keyword *( SP ehlo-param )
+ ehlo-keyword = (ALPHA / DIGIT) *(ALPHA / DIGIT / "-")
+ ; additional syntax of ehlo-params depends on
+ ; ehlo-keyword
+ ehlo-param = 1*(%d33-126)
+ ; any CHAR excluding <SP> and all
+ ; control characters (US-ASCII 0-31 and 127
+ ; inclusive)
+
+ From RFC 2034:
+
+ status-code ::= class "." subject "." detail
+ class ::= "2" / "4" / "5"
+ subject ::= 1*3digit
+ detail ::= 1*3digit
+ */
+
+enum smtp_reply_parser_state {
+ SMTP_REPLY_PARSE_STATE_INIT = 0,
+ SMTP_REPLY_PARSE_STATE_CODE,
+ SMTP_REPLY_PARSE_STATE_SEP,
+ SMTP_REPLY_PARSE_STATE_TEXT,
+ SMTP_REPLY_PARSE_STATE_EHLO_SPACE,
+ SMTP_REPLY_PARSE_STATE_EHLO_GREET,
+ SMTP_REPLY_PARSE_STATE_CR,
+ SMTP_REPLY_PARSE_STATE_CRLF,
+ SMTP_REPLY_PARSE_STATE_LF
+};
+
+struct smtp_reply_parser_state_data {
+ enum smtp_reply_parser_state state;
+ unsigned int line;
+
+ struct smtp_reply *reply;
+ ARRAY_TYPE(const_string) reply_lines;
+ size_t reply_size;
+
+ bool last_line:1;
+};
+
+struct smtp_reply_parser {
+ struct istream *input;
+
+ size_t max_reply_size;
+
+ const unsigned char *begin, *cur, *end;
+
+ string_t *strbuf;
+
+ struct smtp_reply_parser_state_data state;
+ pool_t reply_pool;
+
+ char *error;
+
+ bool enhanced_codes:1;
+ bool ehlo:1;
+};
+
+bool smtp_reply_parse_enhanced_code(const char *text,
+ struct smtp_reply_enhanced_code *enh_code_r,
+ const char **pos_r)
+{
+ const char *p = text;
+ unsigned int digits, x, y, z;
+
+ i_zero(enh_code_r);
+
+ /* status-code ::= class "." subject "." detail
+ class ::= "2" / "4" / "5"
+ subject ::= 1*3digit
+ detail ::= 1*3digit
+ */
+
+ /* class */
+ if (p[1] != '.' || (p[0] != '2' && p[0] != '4' && p[0] != '5'))
+ return FALSE;
+ x = p[0] - '0';
+ p += 2;
+
+ /* subject */
+ digits = 0;
+ y = 0;
+ while (*p != '\0' && i_isdigit(*p) && digits++ < 3) {
+ y = y*10 + (*p - '0');
+ p++;
+ }
+ if (digits == 0 || *p != '.')
+ return FALSE;
+ p++;
+
+ /* detail */
+ digits = 0;
+ z = 0;
+ while (*p != '\0' && i_isdigit(*p) && digits++ < 3) {
+ z = z*10 + (*p - '0');
+ p++;
+ }
+ if (digits == 0 || (pos_r == NULL && *p != '\0'))
+ return FALSE;
+
+ if (pos_r != NULL) {
+ /* code is syntactically valid; strip code from textstring */
+ *pos_r = p;
+ }
+
+ enh_code_r->x = x;
+ enh_code_r->y = y;
+ enh_code_r->z = z;
+ return TRUE;
+}
+
+static inline void ATTR_FORMAT(2, 3)
+smtp_reply_parser_error(struct smtp_reply_parser *parser,
+ const char *format, ...)
+{
+ va_list args;
+
+ i_free(parser->error);
+
+ va_start(args, format);
+ parser->error = i_strdup_vprintf(format, args);
+ va_end(args);
+}
+
+struct smtp_reply_parser *
+smtp_reply_parser_init(struct istream *input, size_t max_reply_size)
+{
+ struct smtp_reply_parser *parser;
+
+ parser = i_new(struct smtp_reply_parser, 1);
+ parser->max_reply_size =
+ (max_reply_size > 0 ? max_reply_size : SIZE_MAX);
+ parser->input = input;
+ i_stream_ref(input);
+ parser->strbuf = str_new(default_pool, 128);
+ return parser;
+}
+
+void smtp_reply_parser_deinit(struct smtp_reply_parser **_parser)
+{
+ struct smtp_reply_parser *parser = *_parser;
+
+ *_parser = NULL;
+
+ str_free(&parser->strbuf);
+ pool_unref(&parser->reply_pool);
+ i_stream_unref(&parser->input);
+ i_free(parser->error);
+ i_free(parser);
+}
+
+void smtp_reply_parser_set_stream(struct smtp_reply_parser *parser,
+ struct istream *input)
+{
+ i_stream_unref(&parser->input);
+ if (input != NULL) {
+ parser->input = input;
+ i_stream_ref(parser->input);
+ }
+}
+
+static void
+smtp_reply_parser_restart(struct smtp_reply_parser *parser)
+{
+ str_truncate(parser->strbuf, 0);
+ pool_unref(&parser->reply_pool);
+ i_zero(&parser->state);
+
+ parser->reply_pool = pool_alloconly_create("smtp_reply", 1024);
+ parser->state.reply = p_new(parser->reply_pool, struct smtp_reply, 1);
+ p_array_init(&parser->state.reply_lines, parser->reply_pool, 8);
+
+}
+
+static int smtp_reply_parse_code
+(struct smtp_reply_parser *parser, unsigned int *code_r)
+{
+ const unsigned char *first = parser->cur;
+ const unsigned char *p;
+
+ /* Reply-code = %x32-35 %x30-35 %x30-39
+ */
+ while (parser->cur < parser->end && i_isdigit(*parser->cur))
+ parser->cur++;
+
+ if (str_len(parser->strbuf) + (parser->cur-first) > 3)
+ return -1;
+
+ str_append_data(parser->strbuf, first, parser->cur - first);
+ if (parser->cur == parser->end)
+ return 0;
+ if (str_len(parser->strbuf) != 3)
+ return -1;
+ p = str_data(parser->strbuf);
+ if (p[0] < '2' || p[0] > '5' || p[1] > '5')
+ return -1;
+ *code_r = (p[0] - '0')*100 + (p[1] - '0')*10 + (p[2] - '0');
+ str_truncate(parser->strbuf, 0);
+ return 1;
+}
+
+static int smtp_reply_parse_textstring(struct smtp_reply_parser *parser)
+{
+ const unsigned char *first = parser->cur;
+
+ /* textstring = 1*(%d09 / %d32-126) ; HT, SP, Printable US-ASCII
+ */
+ while (parser->cur < parser->end && smtp_char_is_textstr(*parser->cur))
+ parser->cur++;
+
+ if (((parser->cur-first) + parser->state.reply_size +
+ str_len(parser->strbuf)) > parser->max_reply_size) {
+ smtp_reply_parser_error(parser,
+ "Reply exceeds size limit");
+ return -1;
+ }
+
+ str_append_data(parser->strbuf, first, parser->cur - first);
+ if (parser->cur == parser->end)
+ return 0;
+ return 1;
+}
+
+static int smtp_reply_parse_ehlo_domain(struct smtp_reply_parser *parser)
+{
+ const unsigned char *first = parser->cur;
+
+ /* Domain [ SP ...
+ */
+ while (parser->cur < parser->end && *parser->cur != ' ' &&
+ smtp_char_is_textstr(*parser->cur))
+ parser->cur++;
+
+ if (((parser->cur-first) + parser->state.reply_size +
+ str_len(parser->strbuf)) > parser->max_reply_size) {
+ smtp_reply_parser_error(parser,
+ "Reply exceeds size limit");
+ return -1;
+ }
+ str_append_data(parser->strbuf, first, parser->cur - first);
+ if (parser->cur == parser->end)
+ return 0;
+ return 1;
+}
+
+static int smtp_reply_parse_ehlo_greet(struct smtp_reply_parser *parser)
+{
+ const unsigned char *first = parser->cur;
+
+ /* ehlo-greet = 1*(%d0-9 / %d11-12 / %d14-127)
+ *
+ * The greet is not supposed to be empty, but we don't really care
+ */
+
+ if (parser->cur == parser->end)
+ return 0;
+ if (smtp_char_is_ehlo_greet(*parser->cur)) {
+ for (;;) {
+ while (parser->cur < parser->end &&
+ smtp_char_is_textstr(*parser->cur))
+ parser->cur++;
+
+ if (((parser->cur-first) + parser->state.reply_size +
+ str_len(parser->strbuf)) >
+ parser->max_reply_size) {
+ smtp_reply_parser_error(parser,
+ "Reply exceeds size limit");
+ return -1;
+ }
+
+ /* sanitize bad characters */
+ str_append_data(parser->strbuf,
+ first, parser->cur - first);
+
+ if (parser->cur == parser->end)
+ return 0;
+ if (!smtp_char_is_ehlo_greet(*parser->cur))
+ break;
+ str_append_c(parser->strbuf, ' ');
+ parser->cur++;
+ first = parser->cur;
+ }
+ }
+ return 1;
+}
+
+static inline const char *_chr_sanitize(unsigned char c)
+{
+ if (c >= 0x20 && c < 0x7F)
+ return t_strdup_printf("'%c'", c);
+ return t_strdup_printf("0x%02x", c);
+}
+
+static void
+smtp_reply_parser_parse_enhanced_code(const char *text,
+ struct smtp_reply_parser *parser,
+ const char **pos_r)
+{
+ struct smtp_reply_enhanced_code code;
+ struct smtp_reply_enhanced_code *cur_code =
+ &parser->state.reply->enhanced_code;
+
+ if (cur_code->x == 9)
+ return; /* failed on earlier line */
+
+ if (!smtp_reply_parse_enhanced_code(text, &code, pos_r)) {
+ /* failed to parse an enhanced code */
+ i_zero(cur_code);
+ cur_code->x = 9;
+ return;
+ }
+
+ if (**pos_r != ' ' && **pos_r != '\r' && **pos_r != '\n')
+ return;
+ (*pos_r)++;
+
+ /* check for match with status */
+ if (code.x != parser->state.reply->status / 100) {
+ /* ignore code */
+ return;
+ }
+
+ /* check for code consistency */
+ if (parser->state.line > 0 &&
+ (cur_code->x != code.x || cur_code->y != code.y ||
+ cur_code->z != code.z)) {
+ /* ignore code */
+ return;
+ }
+
+ *cur_code = code;
+}
+
+static void smtp_reply_parser_finish_line(struct smtp_reply_parser *parser)
+{
+ const char *text = str_c(parser->strbuf);
+
+ if (parser->enhanced_codes && str_len(parser->strbuf) > 5)
+ smtp_reply_parser_parse_enhanced_code(text, parser, &text);
+
+ parser->state.line++;
+ parser->state.reply_size += str_len(parser->strbuf);
+ text = p_strdup(parser->reply_pool, text);
+ array_push_back(&parser->state.reply_lines, &text);
+ str_truncate(parser->strbuf, 0);
+}
+
+static int smtp_reply_parse_more(struct smtp_reply_parser *parser)
+{
+ unsigned int status;
+ int ret;
+
+ /*
+ Reply-line = *( Reply-code "-" [ textstring ] CRLF )
+ Reply-code [ SP textstring ] CRLF
+ Reply-code = %x32-35 %x30-35 %x30-39
+
+ ehlo-ok-rsp = ( "250" SP Domain [ SP ehlo-greet ] CRLF )
+ / ( "250-" Domain [ SP ehlo-greet ] CRLF
+ *( "250-" ehlo-line CRLF )
+ "250" SP ehlo-line CRLF )
+ */
+
+ for (;;) {
+ switch (parser->state.state) {
+ case SMTP_REPLY_PARSE_STATE_INIT:
+ smtp_reply_parser_restart(parser);
+ parser->state.state = SMTP_REPLY_PARSE_STATE_CODE;
+ /* fall through */
+ /* Reply-code */
+ case SMTP_REPLY_PARSE_STATE_CODE:
+ if ((ret=smtp_reply_parse_code(parser, &status)) <= 0) {
+ if (ret < 0) {
+ smtp_reply_parser_error(parser,
+ "Invalid status code in reply");
+ }
+ return ret;
+ }
+ if (parser->state.line == 0) {
+ parser->state.reply->status = status;
+ } else if (status != parser->state.reply->status) {
+ smtp_reply_parser_error(parser,
+ "Inconsistent status codes in reply");
+ return -1;
+ }
+ parser->state.state = SMTP_REPLY_PARSE_STATE_SEP;
+ if (parser->cur == parser->end)
+ return 0;
+ /* fall through */
+ /* "-" / SP / CRLF */
+ case SMTP_REPLY_PARSE_STATE_SEP:
+ switch (*parser->cur) {
+ /* "-" [ textstring ] CRLF */
+ case '-':
+ parser->cur++;
+ parser->state.last_line = FALSE;
+ parser->state.state =
+ SMTP_REPLY_PARSE_STATE_TEXT;
+ break;
+ /* SP [ textstring ] CRLF ; allow missing text */
+ case ' ':
+ parser->cur++;
+ parser->state.state =
+ SMTP_REPLY_PARSE_STATE_TEXT;
+ parser->state.last_line = TRUE;
+ break;
+ /* CRLF */
+ case '\r':
+ case '\n':
+ parser->state.last_line = TRUE;
+ parser->state.state = SMTP_REPLY_PARSE_STATE_CR;
+ break;
+ default:
+ smtp_reply_parser_error(parser,
+ "Encountered unexpected %s after reply status code",
+ _chr_sanitize(*parser->cur));
+ return -1;
+ }
+ if (parser->state.state != SMTP_REPLY_PARSE_STATE_TEXT)
+ break;
+ /* fall through */
+ /* textstring / (Domain [ SP ehlo-greet ]) */
+ case SMTP_REPLY_PARSE_STATE_TEXT:
+ if (parser->ehlo &&
+ parser->state.reply->status == 250 &&
+ parser->state.line == 0) {
+ /* handle first line of EHLO success response
+ differently because it can contain control
+ characters (WHY??!) */
+ if ((ret=smtp_reply_parse_ehlo_domain(parser)) <= 0)
+ return ret;
+ parser->state.state =
+ SMTP_REPLY_PARSE_STATE_EHLO_SPACE;
+ if (parser->cur == parser->end)
+ return 0;
+ break;
+ }
+ if ((ret=smtp_reply_parse_textstring(parser)) <= 0)
+ return ret;
+ parser->state.state = SMTP_REPLY_PARSE_STATE_CR;
+ if (parser->cur == parser->end)
+ return 0;
+ /* fall through */
+ /* CR */
+ case SMTP_REPLY_PARSE_STATE_CR:
+ if (*parser->cur == '\r') {
+ parser->cur++;
+ parser->state.state =
+ SMTP_REPLY_PARSE_STATE_CRLF;
+ } else {
+ parser->state.state =
+ SMTP_REPLY_PARSE_STATE_LF;
+ }
+ if (parser->cur == parser->end)
+ return 0;
+ /* fall through */
+ /* CRLF / LF */
+ case SMTP_REPLY_PARSE_STATE_CRLF:
+ case SMTP_REPLY_PARSE_STATE_LF:
+ if (*parser->cur != '\n') {
+ if (parser->state.state ==
+ SMTP_REPLY_PARSE_STATE_CRLF) {
+ smtp_reply_parser_error(parser,
+ "Encountered stray CR in reply text");
+ } else {
+ smtp_reply_parser_error(parser,
+ "Encountered stray %s in reply text",
+ _chr_sanitize(*parser->cur));
+ }
+ return -1;
+ }
+ parser->cur++;
+ smtp_reply_parser_finish_line(parser);
+ if (parser->state.last_line) {
+ parser->state.state =
+ SMTP_REPLY_PARSE_STATE_INIT;
+ return 1;
+ }
+ parser->state.state = SMTP_REPLY_PARSE_STATE_CODE;
+ break;
+ /* SP ehlo-greet */
+ case SMTP_REPLY_PARSE_STATE_EHLO_SPACE:
+ if (*parser->cur != ' ') {
+ parser->state.state = SMTP_REPLY_PARSE_STATE_CR;
+ break;
+ }
+ parser->cur++;
+ str_append_c(parser->strbuf, ' ');
+ parser->state.state = SMTP_REPLY_PARSE_STATE_EHLO_GREET;
+ if (parser->cur == parser->end)
+ return 0;
+ /* fall through */
+ /* ehlo-greet */
+ case SMTP_REPLY_PARSE_STATE_EHLO_GREET:
+ if ((ret=smtp_reply_parse_ehlo_greet(parser)) <= 0)
+ return ret;
+ parser->state.state = SMTP_REPLY_PARSE_STATE_CR;
+ if (parser->cur == parser->end)
+ return 0;
+ break;
+ default:
+ i_unreached();
+ }
+ }
+
+ i_unreached();
+ return -1;
+}
+
+static int smtp_reply_parse(struct smtp_reply_parser *parser)
+{
+ size_t size;
+ int ret;
+
+ while ((ret = i_stream_read_more(parser->input,
+ &parser->begin, &size)) > 0) {
+ parser->cur = parser->begin;
+ parser->end = parser->cur + size;
+
+ if ((ret = smtp_reply_parse_more(parser)) < 0)
+ return -1;
+
+ i_stream_skip(parser->input, parser->cur - parser->begin);
+ if (ret > 0)
+ return 1;
+ }
+
+ i_assert(ret != -2);
+ if (ret < 0) {
+ i_assert(parser->input->eof);
+ if (parser->input->stream_errno == 0) {
+ if (parser->state.state == SMTP_REPLY_PARSE_STATE_INIT)
+ return 0;
+ smtp_reply_parser_error(parser,
+ "Premature end of input");
+ } else {
+ smtp_reply_parser_error(parser,
+ "Stream error: %s",
+ i_stream_get_error(parser->input));
+ }
+ }
+ return ret;
+}
+
+int smtp_reply_parse_next(struct smtp_reply_parser *parser,
+ bool enhanced_codes, struct smtp_reply **reply_r,
+ const char **error_r)
+{
+ int ret;
+
+ i_assert(parser->state.state == SMTP_REPLY_PARSE_STATE_INIT ||
+ (parser->enhanced_codes == enhanced_codes && !parser->ehlo));
+
+ parser->enhanced_codes = enhanced_codes;
+ parser->ehlo = FALSE;
+
+ i_free_and_null(parser->error);
+
+ /*
+ Reply-line = *( Reply-code "-" [ textstring ] CRLF )
+ Reply-code [ SP textstring ] CRLF
+ Reply-code = %x32-35 %x30-35 %x30-39
+ textstring = 1*(%d09 / %d32-126) ; HT, SP, Printable US-ASCII
+
+ Greeting is not handled specially here.
+ */
+ if ((ret=smtp_reply_parse(parser)) <= 0) {
+ *error_r = parser->error;
+ return ret;
+ }
+
+ i_assert(array_count(&parser->state.reply_lines) > 0);
+ array_append_zero(&parser->state.reply_lines);
+
+ parser->state.state = SMTP_REPLY_PARSE_STATE_INIT;
+ parser->state.reply->text_lines =
+ array_front(&parser->state.reply_lines);
+ *reply_r = parser->state.reply;
+ return 1;
+}
+
+int smtp_reply_parse_ehlo(struct smtp_reply_parser *parser,
+ struct smtp_reply **reply_r, const char **error_r)
+{
+ int ret;
+
+ i_assert(parser->state.state == SMTP_REPLY_PARSE_STATE_INIT ||
+ (!parser->enhanced_codes && parser->ehlo));
+
+ parser->enhanced_codes = FALSE;
+ parser->ehlo = TRUE;
+
+ i_free_and_null(parser->error);
+
+ /*
+ ehlo-ok-rsp = ( "250" SP Domain [ SP ehlo-greet ] CRLF )
+ / ( "250-" Domain [ SP ehlo-greet ] CRLF
+ *( "250-" ehlo-line CRLF )
+ "250" SP ehlo-line CRLF )
+ ehlo-greet = 1*(%d0-9 / %d11-12 / %d14-127)
+ ; string of any characters other than CR or LF
+ ehlo-line = ehlo-keyword *( SP ehlo-param )
+ ehlo-keyword = (ALPHA / DIGIT) *(ALPHA / DIGIT / "-")
+ ; additional syntax of ehlo-params depends on
+ ; ehlo-keyword
+ ehlo-param = 1*(%d33-126)
+ ; any CHAR excluding <SP> and all
+ ; control characters (US-ASCII 0-31 and 127
+ ; inclusive)
+ */
+ if ((ret=smtp_reply_parse(parser)) <= 0) {
+ *error_r = parser->error;
+ return ret;
+ }
+
+ i_assert(array_count(&parser->state.reply_lines) > 0);
+ array_append_zero(&parser->state.reply_lines);
+
+ parser->state.state = SMTP_REPLY_PARSE_STATE_INIT;
+ parser->state.reply->text_lines =
+ array_front(&parser->state.reply_lines);
+ *reply_r = parser->state.reply;
+ return 1;
+}
diff --git a/src/lib-smtp/smtp-reply-parser.h b/src/lib-smtp/smtp-reply-parser.h
new file mode 100644
index 0000000..4ffb32a
--- /dev/null
+++ b/src/lib-smtp/smtp-reply-parser.h
@@ -0,0 +1,25 @@
+#ifndef SMTP_REPLY_PARSER_H
+#define SMTP_REPLY_PARSER_H
+
+#include "smtp-reply.h"
+
+struct smtp_reply_parser;
+
+bool smtp_reply_parse_enhanced_code(const char *text,
+ struct smtp_reply_enhanced_code *enh_code_r,
+ const char **pos_r) ATTR_NULL(3);
+
+struct smtp_reply_parser *
+smtp_reply_parser_init(struct istream *input, size_t max_reply_size);
+void smtp_reply_parser_deinit(struct smtp_reply_parser **_parser);
+
+void smtp_reply_parser_set_stream(struct smtp_reply_parser *parser,
+ struct istream *input);
+
+int smtp_reply_parse_next(struct smtp_reply_parser *parser,
+ bool enhanced_codes, struct smtp_reply **reply_r,
+ const char **error_r);
+int smtp_reply_parse_ehlo(struct smtp_reply_parser *parser,
+ struct smtp_reply **reply_r, const char **error_r);
+
+#endif
diff --git a/src/lib-smtp/smtp-reply.c b/src/lib-smtp/smtp-reply.c
new file mode 100644
index 0000000..3ee27da
--- /dev/null
+++ b/src/lib-smtp/smtp-reply.c
@@ -0,0 +1,186 @@
+/* Copyright (c) 2013-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "str.h"
+#include "strfuncs.h"
+#include "smtp-reply.h"
+
+void smtp_reply_init(struct smtp_reply *reply, unsigned int status,
+ const char *text)
+{
+ const char **text_lines = t_new(const char *, 2);
+
+ text_lines[0] = text;
+ text_lines[1] = NULL;
+
+ i_zero(reply);
+ reply->status = status;
+ reply->text_lines = text_lines;
+}
+
+void smtp_reply_printf(struct smtp_reply *reply, unsigned int status,
+ const char *format, ...)
+{
+ va_list args;
+
+ va_start(args, format);
+ smtp_reply_init(reply, status, t_strdup_vprintf(format, args));
+ va_end(args);
+}
+
+const char *
+smtp_reply_get_enh_code(const struct smtp_reply *reply)
+{
+ if (reply->enhanced_code.x < 2)
+ return NULL;
+ if (reply->enhanced_code.x >= 6)
+ return NULL;
+
+ return t_strdup_printf("%u.%u.%u",
+ reply->enhanced_code.x, reply->enhanced_code.y, reply->enhanced_code.z);
+}
+
+const char *const *
+smtp_reply_get_text_lines_omit_prefix(const struct smtp_reply *reply)
+{
+ unsigned int lines_count, i;
+ const char **lines;
+ const char *p;
+
+ if ((p=strchr(reply->text_lines[0], ' ')) == NULL)
+ return reply->text_lines;
+
+ lines_count = str_array_length(reply->text_lines);
+ lines = t_new(const char *, lines_count + 1);
+
+ lines[0] = p + 1;
+ for (i = 1; i < lines_count; i++)
+ lines[i] = reply->text_lines[i];
+
+ return lines;
+}
+
+void
+smtp_reply_write(string_t *out, const struct smtp_reply *reply)
+{
+ const char *prefix, *enh_code;
+ const char *const *lines;
+
+ i_assert(reply->status < 560);
+ i_assert(reply->enhanced_code.x < 6);
+
+ prefix = t_strdup_printf("%03u", reply->status);
+ enh_code = smtp_reply_get_enh_code(reply);
+
+ if (reply->text_lines == NULL || *reply->text_lines == NULL) {
+ str_append(out, prefix);
+ if (enh_code != NULL) {
+ str_append_c(out, ' ');
+ str_append(out, enh_code);
+ }
+ str_append(out, " \r\n");
+ return;
+ }
+
+ lines = reply->text_lines;
+ while (*lines != NULL) {
+ str_append(out, prefix);
+ if (*(lines+1) == NULL)
+ str_append_c(out, ' ');
+ else
+ str_append_c(out, '-');
+ if (enh_code != NULL) {
+ str_append(out, enh_code);
+ str_append_c(out, ' ');
+ }
+ str_append(out, *lines);
+ str_append(out, "\r\n");
+ lines++;
+ }
+}
+
+static void
+smtp_reply_write_message_one_line(string_t *out, const struct smtp_reply *reply)
+{
+ const char *const *lines;
+
+ lines = reply->text_lines;
+ while (*lines != NULL) {
+ if (str_len(out) > 0)
+ str_append_c(out, ' ');
+ str_append(out, *lines);
+ lines++;
+ }
+}
+
+void smtp_reply_write_one_line(string_t *out, const struct smtp_reply *reply)
+{
+ const char *enh_code = smtp_reply_get_enh_code(reply);
+
+ i_assert(reply->status < 560);
+ i_assert(reply->enhanced_code.x < 6);
+
+ str_printfa(out, "%03u", reply->status);
+ if (enh_code != NULL) {
+ str_append_c(out, ' ');
+ str_append(out, enh_code);
+ }
+
+ smtp_reply_write_message_one_line(out, reply);
+}
+
+const char *smtp_reply_log(const struct smtp_reply *reply)
+{
+ string_t *msg = t_str_new(256);
+
+ if (smtp_reply_is_remote(reply)) {
+ const char *enh_code = smtp_reply_get_enh_code(reply);
+
+ str_printfa(msg, "%03u", reply->status);
+ if (enh_code != NULL) {
+ str_append_c(msg, ' ');
+ str_append(msg, enh_code);
+ }
+ }
+
+ smtp_reply_write_message_one_line(msg, reply);
+ return str_c(msg);
+}
+
+const char *smtp_reply_get_message(const struct smtp_reply *reply)
+{
+ string_t *msg = t_str_new(256);
+
+ smtp_reply_write_message_one_line(msg, reply);
+ return str_c(msg);
+}
+
+void smtp_reply_copy(pool_t pool, struct smtp_reply *dst,
+ const struct smtp_reply *src)
+{
+ *dst = *src;
+ dst->text_lines = p_strarray_dup(pool, src->text_lines);
+}
+
+struct smtp_reply *smtp_reply_clone(pool_t pool,
+ const struct smtp_reply *src)
+{
+ struct smtp_reply *dst;
+
+ dst = p_new(pool, struct smtp_reply, 1);
+ smtp_reply_copy(pool, dst, src);
+
+ return dst;
+}
+
+void smtp_reply_add_to_event(const struct smtp_reply *reply,
+ struct event_passthrough *e)
+{
+ const char *enh_code = smtp_reply_get_enh_code(reply);
+
+ e->add_int("status_code", reply->status);
+ e->add_str("enhanced_code", enh_code);
+ if (!smtp_reply_is_success(reply))
+ e->add_str("error", smtp_reply_get_message(reply));
+}
diff --git a/src/lib-smtp/smtp-reply.h b/src/lib-smtp/smtp-reply.h
new file mode 100644
index 0000000..713ce1f
--- /dev/null
+++ b/src/lib-smtp/smtp-reply.h
@@ -0,0 +1,82 @@
+#ifndef SMTP_REPLY_H
+#define SMTP_REPLY_H
+
+struct smtp_reply_enhanced_code {
+ /* x:class, y:subject, z:detail;
+ x==0 means no enhanced code present
+ x==9 means invalid/missing enhanced code in reply
+ */
+ unsigned int x, y, z;
+};
+
+struct smtp_reply {
+ unsigned int status;
+
+ struct smtp_reply_enhanced_code enhanced_code;
+
+ const char *const *text_lines;
+};
+
+#define SMTP_REPLY_ENH_CODE(x, y, z) \
+ (const struct smtp_reply_enhanced_code){(x), (y), (z)}
+#define SMTP_REPLY_ENH_CODE_NONE SMTP_REPLY_ENH_CODE(0, 0, 0)
+
+static inline bool
+smtp_reply_has_enhanced_code(const struct smtp_reply *reply)
+{
+ return reply->enhanced_code.x > 1 && reply->enhanced_code.x < 6;
+}
+
+static inline bool
+smtp_reply_is_success(const struct smtp_reply *reply)
+{
+ return ((reply->status / 100) == 2);
+}
+
+static inline bool
+smtp_reply_is_remote(const struct smtp_reply *reply)
+{
+ return (reply->status >= 200 && reply->status < 560);
+}
+
+static inline bool
+smtp_reply_is_temp_fail(const struct smtp_reply *reply)
+{
+ return ((reply->status / 100) == 4);
+}
+
+void smtp_reply_init(struct smtp_reply *reply, unsigned int status,
+ const char *text);
+void smtp_reply_printf(struct smtp_reply *reply, unsigned int status,
+ const char *format, ...) ATTR_FORMAT(3, 4);
+
+const char *
+smtp_reply_get_enh_code(const struct smtp_reply *reply);
+const char *const *
+smtp_reply_get_text_lines_omit_prefix(const struct smtp_reply *reply);
+
+/* Write the SMTP reply as a sequence of lines according to the SMTP syntax,
+ each terminated by CRLF. */
+void smtp_reply_write(string_t *out, const struct smtp_reply *reply);
+/* Write the SMTP reply as a single line without CRLF, even when it consists
+ of multiple lines. This function cannot be used with internal client error
+ replies (status code >= 560). */
+void smtp_reply_write_one_line(string_t *out, const struct smtp_reply *reply);
+/* Create a log line from the SMTP reply. This also properly handles internal
+ client error replies (status_code >= 560). */
+const char *smtp_reply_log(const struct smtp_reply *reply);
+/* Returns the message of the reply as a single line without status codes and
+ without CRLF.
+ */
+const char *smtp_reply_get_message(const struct smtp_reply *reply);
+
+void smtp_reply_copy(pool_t pool, struct smtp_reply *dst,
+ const struct smtp_reply *src);
+struct smtp_reply *smtp_reply_clone(pool_t pool,
+ const struct smtp_reply *src);
+
+/* Set standard reply fields in provided pass-through event */
+void smtp_reply_add_to_event(const struct smtp_reply *reply,
+ struct event_passthrough *e);
+
+#endif
diff --git a/src/lib-smtp/smtp-server-cmd-auth.c b/src/lib-smtp/smtp-server-cmd-auth.c
new file mode 100644
index 0000000..841e186
--- /dev/null
+++ b/src/lib-smtp/smtp-server-cmd-auth.c
@@ -0,0 +1,242 @@
+/* Copyright (c) 2013-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "istream.h"
+#include "smtp-syntax.h"
+#include "smtp-command-parser.h"
+
+#include "smtp-server-private.h"
+
+/* AUTH command (RFC 4954) */
+
+
+static bool
+cmd_auth_check_state(struct smtp_server_cmd_ctx *cmd)
+{
+ struct smtp_server_connection *conn = cmd->conn;
+
+ /* RFC 4954, Section 4:
+ After an AUTH command has been successfully completed, no more
+ AUTH commands may be issued in the same session. After a
+ successful AUTH command completes, a server MUST reject any
+ further AUTH commands with a 503 reply. */
+ if (conn->authenticated) {
+ smtp_server_reply(cmd,
+ 503, "5.5.0", "Already authenticated");
+ return FALSE;
+ }
+
+ /* RFC 4954, Section 4:
+ The AUTH command is not permitted during a mail transaction.
+ An AUTH command issued during a mail transaction MUST be
+ rejected with a 503 reply. */
+ if (conn->state.trans != NULL) {
+ smtp_server_reply(cmd, 503, "5.5.0",
+ "Authentication not permitted during a mail transaction");
+ return FALSE;
+ }
+ return TRUE;
+}
+
+void smtp_server_cmd_auth_success(struct smtp_server_cmd_ctx *cmd,
+ const char *username, const char *success_msg)
+{
+ cmd->conn->username = i_strdup(username);
+
+ smtp_server_reply(cmd, 235, "2.7.0", "%s",
+ (success_msg == NULL ? "Logged in." : success_msg));
+}
+
+static void
+cmd_auth_completed(struct smtp_server_cmd_ctx *cmd,
+ struct smtp_server_cmd_auth *data ATTR_UNUSED)
+{
+ struct smtp_server_connection *conn = cmd->conn;
+ struct smtp_server_command *command = cmd->cmd;
+
+ i_assert(smtp_server_command_is_replied(command));
+ if (smtp_server_command_replied_success(command)) {
+ /* only one valid success status for AUTH command */
+ i_assert(smtp_server_command_reply_status_equals(command, 235));
+ conn->authenticated = TRUE;
+ }
+}
+
+static void cmd_auth_input(struct smtp_server_cmd_ctx *cmd)
+{
+ struct smtp_server_connection *conn = cmd->conn;
+ const struct smtp_server_callbacks *callbacks = conn->callbacks;
+ struct smtp_server_command *command = cmd->cmd;
+ enum smtp_command_parse_error error_code;
+ const char *auth_response, *error;
+ int ret;
+
+ /* parse response */
+ if ((ret=smtp_command_parse_auth_response(conn->smtp_parser,
+ &auth_response, &error_code, &error)) <= 0) {
+ /* check for disconnect */
+ if (conn->conn.input->eof) {
+ smtp_server_connection_close(&conn,
+ i_stream_get_disconnect_reason(conn->conn.input));
+ return;
+ }
+ /* handle syntax error */
+ if (ret < 0) {
+ e_debug(conn->event,
+ "Client sent invalid AUTH response: %s", error);
+
+ smtp_server_command_input_lock(cmd);
+ switch (error_code) {
+ case SMTP_COMMAND_PARSE_ERROR_BROKEN_COMMAND:
+ conn->input_broken = TRUE;
+ /* fall through */
+ case SMTP_COMMAND_PARSE_ERROR_BAD_COMMAND:
+ smtp_server_reply(cmd, 500, "5.5.2",
+ "Invalid AUTH response syntax");
+ break;
+ case SMTP_COMMAND_PARSE_ERROR_LINE_TOO_LONG:
+ smtp_server_reply(cmd, 500, "5.5.2",
+ "Line too long");
+ break;
+ default:
+ i_unreached();
+ }
+ }
+ if (conn->input_broken || conn->closing)
+ smtp_server_connection_input_halt(conn);
+ return;
+ }
+
+ e_debug(conn->event, "Received AUTH response: %s", auth_response);
+
+ smtp_server_command_input_lock(cmd);
+
+ /* continue authentication */
+ smtp_server_command_ref(command);
+ i_assert(callbacks != NULL &&
+ callbacks->conn_cmd_auth_continue != NULL);
+ if ((ret=callbacks->conn_cmd_auth_continue(conn->context,
+ cmd, auth_response)) <= 0) {
+ /* command is waiting for external event or it failed */
+ i_assert(ret == 0 || smtp_server_command_is_replied(command));
+ smtp_server_command_unref(&command);
+ return;
+ }
+ if (!smtp_server_command_is_replied(command)) {
+ /* set generic AUTH success reply if none is provided */
+ smtp_server_reply(cmd, 235, "2.7.0", "Logged in.");
+ }
+ conn->authenticated = TRUE;
+ smtp_server_command_unref(&command);
+}
+
+void smtp_server_cmd_auth_send_challenge(struct smtp_server_cmd_ctx *cmd,
+ const char *challenge)
+{
+ struct smtp_server_connection *conn = cmd->conn;
+ struct smtp_server_command *command = cmd->cmd;
+
+ i_assert(command->prev == NULL &&
+ command->reg->func == smtp_server_cmd_auth);
+
+ smtp_server_connection_reply_immediate(conn, 334, "%s", challenge);
+ smtp_server_connection_timeout_reset(conn);
+
+ /* start AUTH-specific input handling */
+ smtp_server_command_input_capture(cmd, cmd_auth_input);
+}
+
+static void
+cmd_auth_start(struct smtp_server_cmd_ctx *cmd,
+ struct smtp_server_cmd_auth *data)
+{
+ struct smtp_server_connection *conn = cmd->conn;
+ struct smtp_server_command *command = cmd->cmd;
+ const struct smtp_server_callbacks *callbacks = conn->callbacks;
+ int ret;
+
+ /* all preceeding commands have finished and now the transaction state
+ is clear. This provides the opportunity to re-check the protocol
+ state */
+ if (!cmd_auth_check_state(cmd))
+ return;
+
+ /* advance state */
+ smtp_server_connection_set_state(conn, SMTP_SERVER_STATE_AUTH, NULL);
+
+ smtp_server_command_ref(command);
+ i_assert(callbacks != NULL && callbacks->conn_cmd_auth != NULL);
+
+ /* specific implementation of AUTH command */
+ ret = callbacks->conn_cmd_auth(conn->context, cmd, data);
+ i_assert(ret == 0 || smtp_server_command_is_replied(command));
+
+ if (ret == 0)
+ smtp_server_connection_timeout_stop(conn);
+
+ smtp_server_command_unref(&command);
+ return;
+}
+
+void smtp_server_cmd_auth(struct smtp_server_cmd_ctx *cmd,
+ const char *params)
+{
+ struct smtp_server_connection *conn = cmd->conn;
+ struct smtp_server_command *command = cmd->cmd;
+ struct smtp_server_cmd_auth *auth_data;
+ const char *sasl_mech, *initial_response;
+ const char *const *argv;
+ int ret = 1;
+
+ if ((conn->set.capabilities & SMTP_CAPABILITY_AUTH) == 0) {
+ smtp_server_reply(cmd,
+ 502, "5.5.1", "Unsupported command");
+ return;
+ }
+
+ /* RFC 4954, Section 8:
+
+ auth-command = "AUTH" SP sasl-mech [SP initial-response]
+ *(CRLF [base64]) [CRLF cancel-response]
+ CRLF
+ ;; <sasl-mech> is defined in [SASL]
+
+ initial-response = base64 / "="
+ */
+ argv = t_strsplit(params, " ");
+ initial_response = sasl_mech = NULL;
+ if (argv[0] == NULL) {
+ smtp_server_reply(cmd,
+ 501, "5.5.4", "Missing SASL mechanism parameter");
+ ret = -1;
+ } else {
+ sasl_mech = argv[0];
+
+ if (argv[1] != NULL) {
+ if (argv[2] != NULL) {
+ smtp_server_reply(cmd,
+ 501, "5.5.4", "Invalid parameters");
+ ret = -1;
+ } else {
+ initial_response = argv[1];
+ }
+ }
+ }
+ if (ret < 0)
+ return;
+
+ /* check protocol state */
+ if (!cmd_auth_check_state(cmd))
+ return;
+
+ smtp_server_command_input_lock(cmd);
+
+ auth_data = p_new(cmd->pool, struct smtp_server_cmd_auth, 1);
+ auth_data->sasl_mech = p_strdup(cmd->pool, sasl_mech);
+ auth_data->initial_response = p_strdup(cmd->pool, initial_response);
+
+ smtp_server_command_add_hook(command, SMTP_SERVER_COMMAND_HOOK_NEXT,
+ cmd_auth_start, auth_data);
+ smtp_server_command_add_hook(command, SMTP_SERVER_COMMAND_HOOK_COMPLETED,
+ cmd_auth_completed, auth_data);
+}
diff --git a/src/lib-smtp/smtp-server-cmd-data.c b/src/lib-smtp/smtp-server-cmd-data.c
new file mode 100644
index 0000000..7f33fdf
--- /dev/null
+++ b/src/lib-smtp/smtp-server-cmd-data.c
@@ -0,0 +1,679 @@
+/* Copyright (c) 2013-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "str.h"
+#include "array.h"
+#include "istream.h"
+#include "istream-chain.h"
+
+#include "smtp-command-parser.h"
+#include "smtp-server-private.h"
+
+/* DATA/BDAT/B... commands */
+
+struct cmd_data_context {
+ struct istream *main_input;
+ struct istream *chunk_input;
+ uoff_t chunk_size;
+
+ bool chunking:1;
+ bool client_input:1;
+ bool chunk_first:1;
+ bool chunk_last:1;
+};
+
+static void
+smtp_server_cmd_data_size_limit_exceeded(struct smtp_server_cmd_ctx *cmd)
+{
+ struct smtp_server_command *command = cmd->cmd;
+
+ smtp_server_command_fail(command, 552, "5.2.3",
+ "Message size exceeds administrative limit");
+}
+
+bool smtp_server_cmd_data_check_size(struct smtp_server_cmd_ctx *cmd)
+{
+ struct smtp_server_connection *conn = cmd->conn;
+ const struct smtp_server_settings *set = &conn->set;
+
+ i_assert(conn->state.state == SMTP_SERVER_STATE_DATA);
+
+ if (conn->state.data_input == NULL)
+ return TRUE;
+ if (set->max_message_size == 0)
+ return TRUE;
+ if (conn->state.data_input->v_offset <= set->max_message_size)
+ return TRUE;
+
+ smtp_server_cmd_data_size_limit_exceeded(cmd);
+ return FALSE;
+}
+
+bool smtp_server_connection_data_check_state(struct smtp_server_cmd_ctx *cmd)
+{
+ struct smtp_server_connection *conn = cmd->conn;
+ struct smtp_server_command *command = cmd->cmd;
+ struct cmd_data_context *data_cmd = command->data;
+
+ if (conn->state.data_chunks > 0 && conn->state.data_failed) {
+ // FIXME: should it even reply anything? RFC is unclear.
+ smtp_server_command_fail(command, 503, "5.5.0",
+ "Previous data chunk failed, issue RSET first");
+ return FALSE;
+ }
+
+ /* check valid MAIL */
+ if (conn->state.trans == NULL
+ && conn->state.pending_mail_cmds == 0) {
+ smtp_server_command_fail(command,
+ 503, "5.5.0", "MAIL needed first");
+ return FALSE;
+ }
+ if (conn->state.trans != NULL &&
+ (conn->state.trans->params.body.type ==
+ SMTP_PARAM_MAIL_BODY_TYPE_BINARYMIME) &&
+ !data_cmd->chunking) {
+ /* RFC 3030, Section 3:
+ BINARYMIME cannot be used with the DATA command. If a DATA
+ command is issued after a MAIL command containing the
+ body-value of "BINARYMIME", a 503 "Bad sequence of commands"
+ response MUST be sent. The resulting state from this error
+ condition is indeterminate and the transaction MUST be reset
+ with the RSET command. */
+ smtp_server_command_fail(command,
+ 503, "5.5.0", "DATA cannot be used with BINARYMIME");
+ return FALSE;
+ }
+
+ /* Can only decide whether we have valid recipients once there are no
+ pending RCPT commands */
+ if (conn->state.pending_rcpt_cmds > 0)
+ return TRUE;
+
+ /* special handling for LMTP */
+ if (conn->set.protocol == SMTP_PROTOCOL_LMTP) {
+ /* check valid RCPT (at least one) */
+ if (conn->state.trans == NULL ||
+ !smtp_server_transaction_has_rcpt(conn->state.trans)) {
+ if (data_cmd->chunk_size > 0 && data_cmd->chunk_last) {
+ /* RFC 2033, Section 4.3:
+ If there were no previously successful RCPT
+ commands in the mail transaction, then the
+ BDAT LAST command returns zero replies.
+ */
+ smtp_server_command_abort(&command);
+ } else {
+ /* RFC 2033, Section 4.2:
+ The additional restriction is that when there
+ have been no successful RCPT commands in the
+ mail transaction, the DATA command MUST fail
+ with a 503 reply code.
+ */
+ smtp_server_command_fail(command,
+ 503, "5.5.0", "No valid recipients");
+ }
+ return FALSE;
+ }
+
+ } else {
+ /* check valid RCPT (at least one) */
+ if (conn->state.trans == NULL ||
+ !smtp_server_transaction_has_rcpt(conn->state.trans)) {
+ smtp_server_command_fail(command,
+ 554, "5.5.0", "No valid recipients");
+ return FALSE;
+ }
+ }
+ return TRUE;
+}
+
+static void
+cmd_data_destroy(struct smtp_server_cmd_ctx *cmd,
+ struct cmd_data_context *data_cmd)
+{
+ struct smtp_server_connection *conn = cmd->conn;
+ struct smtp_server_command *command = cmd->cmd;
+
+ i_assert(data_cmd != NULL);
+
+ if (data_cmd->main_input == conn->state.data_input &&
+ (data_cmd->chunk_last ||
+ !smtp_server_command_replied_success(command))) {
+ /* clean up */
+ i_stream_destroy(&conn->state.data_input);
+ i_stream_destroy(&conn->state.data_chain_input);
+ conn->state.data_chain = NULL;
+ }
+
+ i_stream_unref(&data_cmd->chunk_input);
+}
+
+static void
+cmd_data_replied_one(struct smtp_server_cmd_ctx *cmd,
+ struct cmd_data_context *data_cmd ATTR_UNUSED)
+{
+ struct smtp_server_connection *conn = cmd->conn;
+ struct smtp_server_transaction *trans = conn->state.trans;
+ struct smtp_server_recipient *rcpt;
+
+ if (trans == NULL || !array_is_created(&trans->rcpt_to))
+ return;
+
+ array_foreach_elem(&trans->rcpt_to, rcpt)
+ smtp_server_recipient_data_replied(rcpt);
+}
+
+static void
+cmd_data_replied(struct smtp_server_cmd_ctx *cmd,
+ struct cmd_data_context *data_cmd ATTR_UNUSED)
+{
+ struct smtp_server_connection *conn = cmd->conn;
+ struct smtp_server_command *command = cmd->cmd;
+
+ i_assert(conn->state.pending_data_cmds > 0);
+ conn->state.pending_data_cmds--;
+
+ smtp_server_command_input_lock(cmd);
+ if (!smtp_server_command_replied_success(command))
+ smtp_server_command_input_unlock(cmd);
+}
+
+static void
+cmd_data_completed(struct smtp_server_cmd_ctx *cmd,
+ struct cmd_data_context *data_cmd)
+{
+ struct smtp_server_connection *conn = cmd->conn;
+
+ i_assert(data_cmd != NULL);
+ i_stream_unref(&data_cmd->chunk_input);
+
+ i_assert(conn->state.trans != NULL);
+ smtp_server_transaction_finished(conn->state.trans, cmd);
+
+ /* reset state */
+ smtp_server_connection_reset_state(conn);
+}
+
+static void
+cmd_data_chunk_replied(struct smtp_server_cmd_ctx *cmd,
+ struct cmd_data_context *data_cmd)
+{
+ struct smtp_server_connection *conn = cmd->conn;
+ struct smtp_server_command *command = cmd->cmd;
+
+ i_assert(data_cmd != NULL);
+
+ i_assert(conn->state.pending_data_cmds > 0);
+ conn->state.pending_data_cmds--;
+
+ i_assert(smtp_server_command_is_replied(command));
+ if (!smtp_server_command_replied_success(command) &&
+ conn->state.pending_data_cmds == 0)
+ conn->state.data_failed = TRUE;
+}
+
+static void
+cmd_data_chunk_completed(struct smtp_server_cmd_ctx *cmd,
+ struct cmd_data_context *data_cmd ATTR_UNUSED)
+{
+ struct smtp_server_connection *conn = cmd->conn;
+ struct smtp_server_command *command = cmd->cmd;
+
+ if (!smtp_server_command_replied_success(command))
+ conn->state.data_failed = TRUE;
+}
+
+static void
+cmd_data_chunk_finish(struct smtp_server_cmd_ctx *cmd)
+{
+ struct smtp_server_command *command = cmd->cmd;
+ struct cmd_data_context *data_cmd = command->data;
+
+ smtp_server_command_input_lock(cmd);
+ i_stream_unref(&data_cmd->chunk_input);
+
+ /* re-check transaction state (for BDAT/B... command) */
+ if (!smtp_server_connection_data_check_state(cmd))
+ return;
+
+ smtp_server_reply(cmd, 250, "2.0.0",
+ "Added %"PRIuUOFF_T" octets", data_cmd->chunk_size);
+}
+
+static void cmd_data_input_error(struct smtp_server_cmd_ctx *cmd)
+{
+ struct smtp_server_connection *conn = cmd->conn;
+ struct smtp_server_command *command = cmd->cmd;
+ struct cmd_data_context *data_cmd = command->data;
+ struct istream *data_input = conn->state.data_input;
+ const char *error;
+
+ conn->state.data_failed = TRUE;
+
+ if (!data_cmd->client_input) {
+ if (!smtp_server_command_is_replied(command)) {
+ smtp_server_command_fail(command,
+ 400, "4.0.0", "Failed to add data");
+ }
+ return;
+ }
+
+ error = i_stream_get_disconnect_reason(data_input);
+ e_debug(conn->event, "Connection lost during data transfer: %s", error);
+ smtp_server_connection_close(&conn, error);
+}
+
+static int cmd_data_do_handle_input(struct smtp_server_cmd_ctx *cmd)
+{
+ struct smtp_server_connection *conn = cmd->conn;
+ const struct smtp_server_callbacks *callbacks = conn->callbacks;
+ struct smtp_server_command *command = cmd->cmd;
+ struct cmd_data_context *data_cmd = command->data;
+ int ret;
+
+ i_assert(data_cmd != NULL);
+
+ i_assert(callbacks != NULL &&
+ callbacks->conn_cmd_data_continue != NULL);
+ struct event_reason *reason =
+ smtp_server_connection_reason_begin(conn, "cmd_data");
+ ret = callbacks->conn_cmd_data_continue(conn->context,
+ cmd, conn->state.trans);
+ event_reason_end(&reason);
+ if (ret >= 0) {
+ if (!smtp_server_cmd_data_check_size(cmd)) {
+ return -1;
+ } else if (!i_stream_have_bytes_left(conn->state.data_input)) {
+ e_debug(cmd->event, "End of data");
+ smtp_server_transaction_received(
+ conn->state.trans,
+ conn->state.data_input->v_offset);
+ smtp_server_command_input_lock(cmd);
+ smtp_server_connection_timeout_stop(conn);
+ } else if (!data_cmd->chunk_last &&
+ !i_stream_have_bytes_left(data_cmd->chunk_input)) {
+ e_debug(cmd->event, "End of chunk");
+ cmd_data_chunk_finish(cmd);
+ } else if (i_stream_get_data_size(
+ conn->state.data_input) > 0) {
+ e_debug(cmd->event, "Not all client data read");
+ smtp_server_connection_timeout_stop(cmd->conn);
+ } else {
+ smtp_server_connection_timeout_start(cmd->conn);
+ }
+ } else {
+ if (conn->state.data_input->stream_errno != 0) {
+ cmd_data_input_error(cmd);
+ return -1;
+ }
+ /* command is waiting for external event or it failed */
+ i_assert(smtp_server_command_is_replied(command));
+ }
+
+ return 1;
+}
+
+static int cmd_data_handle_input(struct smtp_server_cmd_ctx *cmd)
+{
+ struct smtp_server_connection *conn = cmd->conn;
+ struct smtp_server_command *command = cmd->cmd;
+ int ret;
+
+ if (!smtp_server_cmd_data_check_size(cmd))
+ return -1;
+
+ smtp_server_connection_ref(conn);
+ smtp_server_command_ref(command);
+
+ /* continue reading from client */
+ ret = cmd_data_do_handle_input(cmd);
+
+ smtp_server_command_unref(&command);
+ smtp_server_connection_unref(&conn);
+
+ return ret;
+}
+
+static void cmd_data_input(struct smtp_server_cmd_ctx *cmd)
+{
+ smtp_server_connection_timeout_reset(cmd->conn);
+ (void)cmd_data_handle_input(cmd);
+}
+
+static void
+cmd_data_next(struct smtp_server_cmd_ctx *cmd,
+ struct cmd_data_context *data_cmd)
+{
+ struct smtp_server_connection *conn = cmd->conn;
+ struct smtp_server_transaction *trans = conn->state.trans;
+ const struct smtp_server_callbacks *callbacks = conn->callbacks;
+ struct smtp_server_command *command = cmd->cmd;
+
+ /* this command is next to send a reply */
+
+ i_assert(data_cmd != NULL);
+ i_assert(trans != NULL);
+
+ /* DATA command stops the pipeline, so if it is next to reply, nothing
+ else can be pending. */
+ i_assert(conn->state.pending_mail_cmds == 0 &&
+ conn->state.pending_rcpt_cmds == 0);
+
+ e_debug(cmd->event, "Command is next to be replied");
+
+ smtp_server_transaction_data_command(trans, cmd);
+
+ /* check whether we have had successful mail and rcpt commands */
+ if (!smtp_server_connection_data_check_state(cmd))
+ return;
+
+ if (data_cmd->chunk_last) {
+ /* LMTP 'DATA' and 'BDAT LAST' commands need to send more than
+ one reply per recipient */
+ if (HAS_ALL_BITS(trans->flags,
+ SMTP_SERVER_TRANSACTION_FLAG_REPLY_PER_RCPT)) {
+ smtp_server_command_set_reply_count(command,
+ array_count(&trans->rcpt_to));
+ }
+ }
+
+ smtp_server_connection_set_state(conn, SMTP_SERVER_STATE_DATA, NULL);
+
+ /* chain data streams in the correct order */
+ if (conn->state.data_chain != NULL) {
+ i_assert(data_cmd->chunk_input != NULL);
+ i_stream_chain_append(conn->state.data_chain,
+ data_cmd->chunk_input);
+ if (data_cmd->chunk_last) {
+ e_debug(cmd->event, "Seen the last chunk");
+ i_stream_chain_append_eof(conn->state.data_chain);
+ }
+ }
+
+ if (data_cmd->chunk_first) {
+ struct smtp_server_command *cmd_temp = command;
+
+ e_debug(cmd->event, "First chunk");
+
+ smtp_server_command_ref(cmd_temp);
+ i_assert(callbacks != NULL &&
+ callbacks->conn_cmd_data_begin != NULL);
+ i_assert(conn->state.data_input != NULL);
+ struct event_reason *reason =
+ smtp_server_connection_reason_begin(conn, "cmd_data");
+ int ret = callbacks->conn_cmd_data_begin(conn->context,
+ cmd, conn->state.trans, conn->state.data_input);
+ event_reason_end(&reason);
+ if (ret < 0) {
+ i_assert(smtp_server_command_is_replied(cmd_temp));
+ /* command failed */
+ smtp_server_command_unref(&cmd_temp);
+ return;
+ }
+ if (!smtp_server_command_unref(&cmd_temp))
+ return;
+ }
+
+ if (smtp_server_command_is_replied(command)) {
+ smtp_server_command_input_unlock(cmd);
+ } else {
+ if (data_cmd->client_input) {
+ /* using input from client connection;
+ capture I/O event */
+ smtp_server_connection_timeout_start(conn);
+ smtp_server_command_input_capture(cmd, cmd_data_input);
+ }
+
+ (void)cmd_data_handle_input(cmd);
+ }
+}
+
+static void
+cmd_data_start_input(struct smtp_server_cmd_ctx *cmd,
+ struct cmd_data_context *data_cmd, struct istream *input)
+{
+ struct smtp_server_connection *conn = cmd->conn;
+ struct smtp_server_command *command = cmd->cmd;
+
+ i_assert(data_cmd != NULL);
+
+ if (input != NULL) {
+ i_assert(conn->state.data_input == NULL);
+ conn->state.data_input = input;
+ i_stream_ref(input);
+ }
+ data_cmd->main_input = conn->state.data_input;
+
+ if (data_cmd->client_input)
+ smtp_server_command_input_lock(cmd);
+
+ if (data_cmd->chunk_last) {
+ smtp_server_command_add_hook(
+ command, SMTP_SERVER_COMMAND_HOOK_COMPLETED,
+ cmd_data_completed, data_cmd);
+ } else {
+ smtp_server_command_add_hook(
+ command, SMTP_SERVER_COMMAND_HOOK_COMPLETED,
+ cmd_data_chunk_completed, data_cmd);
+ }
+
+ if (conn->state.pending_mail_cmds == 0 &&
+ conn->state.pending_rcpt_cmds == 0) {
+ cmd_data_next(cmd, data_cmd);
+ } else {
+ smtp_server_command_add_hook(
+ command, SMTP_SERVER_COMMAND_HOOK_NEXT,
+ cmd_data_next, data_cmd);
+ }
+}
+
+/* DATA command */
+
+static void
+cmd_data_start(struct smtp_server_cmd_ctx *cmd,
+ struct cmd_data_context *data_cmd)
+{
+ struct smtp_server_connection *conn = cmd->conn;
+ struct smtp_server_transaction *trans = conn->state.trans;
+ struct istream *dot_input;
+
+ /* called when all previous commands were finished */
+ i_assert(conn->state.pending_mail_cmds == 0 &&
+ conn->state.pending_rcpt_cmds == 0);
+
+ if (trans != NULL)
+ smtp_server_transaction_data_command(trans, cmd);
+
+ /* check whether we have had successful mail and rcpt commands */
+ if (!smtp_server_connection_data_check_state(cmd))
+ return;
+
+ /* don't allow classic DATA when CHUNKING sequence was started before */
+ if (conn->state.data_chunks > 0) {
+ smtp_server_command_fail(cmd->cmd,
+ 503, "5.5.0", "Bad sequence of commands");
+ return;
+ }
+
+ smtp_server_connection_set_state(conn, SMTP_SERVER_STATE_DATA, NULL);
+
+ /* confirm initial success to client */
+ smtp_server_connection_reply_immediate(conn, 354, "OK");
+
+ /* start reading message data from client */
+ dot_input = smtp_command_parse_data_with_dot(conn->smtp_parser);
+ cmd_data_start_input(cmd, data_cmd, dot_input);
+ i_stream_unref(&dot_input);
+}
+
+void smtp_server_cmd_data(struct smtp_server_cmd_ctx *cmd,
+ const char *params)
+{
+ struct smtp_server_connection *conn = cmd->conn;
+ struct smtp_server_command *command = cmd->cmd;
+ struct cmd_data_context *data_cmd;
+
+ /* data = "DATA" CRLF */
+ if (*params != '\0') {
+ smtp_server_reply(cmd,
+ 501, "5.5.4", "Invalid parameters");
+ return;
+ }
+
+ smtp_server_command_input_lock(cmd);
+
+ data_cmd = p_new(cmd->pool, struct cmd_data_context, 1);
+ data_cmd->chunk_first = TRUE;
+ data_cmd->chunk_last = TRUE;
+ data_cmd->client_input = TRUE;
+ command->data = data_cmd;
+
+ smtp_server_command_add_hook(command, SMTP_SERVER_COMMAND_HOOK_NEXT,
+ cmd_data_start, data_cmd);
+ smtp_server_command_add_hook(command, SMTP_SERVER_COMMAND_HOOK_REPLIED_ONE,
+ cmd_data_replied_one, data_cmd);
+ smtp_server_command_add_hook(command, SMTP_SERVER_COMMAND_HOOK_REPLIED,
+ cmd_data_replied, data_cmd);
+ smtp_server_command_add_hook(command, SMTP_SERVER_COMMAND_HOOK_DESTROY,
+ cmd_data_destroy, data_cmd);
+
+ conn->state.pending_data_cmds++;
+}
+
+/* BDAT/B... commands */
+
+void smtp_server_connection_data_chunk_init(struct smtp_server_cmd_ctx *cmd)
+{
+ struct smtp_server_connection *conn = cmd->conn;
+ struct smtp_server_command *command = cmd->cmd;
+ struct cmd_data_context *data_cmd;
+
+ data_cmd = p_new(cmd->pool, struct cmd_data_context, 1);
+ data_cmd->chunking = TRUE;
+ data_cmd->chunk_first = (conn->state.data_chunks++ == 0);
+ command->data = data_cmd;
+
+ smtp_server_command_add_hook(command, SMTP_SERVER_COMMAND_HOOK_REPLIED,
+ cmd_data_chunk_replied, data_cmd);
+ smtp_server_command_add_hook(command, SMTP_SERVER_COMMAND_HOOK_DESTROY,
+ cmd_data_destroy, data_cmd);
+
+ conn->state.pending_data_cmds++;
+
+ if (!conn->state.data_failed && conn->state.data_chain == NULL) {
+ i_assert(data_cmd->chunk_first);
+ i_assert(conn->state.data_chain_input == NULL);
+ conn->state.data_chain_input =
+ i_stream_create_chain(&conn->state.data_chain,
+ IO_BLOCK_SIZE);
+ }
+}
+
+int smtp_server_connection_data_chunk_add(struct smtp_server_cmd_ctx *cmd,
+ struct istream *chunk, uoff_t chunk_size, bool chunk_last,
+ bool client_input)
+{
+ struct smtp_server_connection *conn = cmd->conn;
+ struct smtp_server_transaction *trans = conn->state.trans;
+ const struct smtp_server_settings *set = &conn->set;
+ struct smtp_server_command *command = cmd->cmd;
+ struct cmd_data_context *data_cmd = command->data;
+ uoff_t new_size;
+
+ i_assert(data_cmd != NULL);
+
+ if (trans != NULL)
+ smtp_server_transaction_data_command(trans, cmd);
+
+ if (!smtp_server_connection_data_check_state(cmd))
+ return -1;
+
+ /* check message size increase early */
+ new_size = conn->state.data_size + chunk_size;
+ if (new_size < conn->state.data_size ||
+ (set->max_message_size > 0 && new_size > set->max_message_size)) {
+ smtp_server_cmd_data_size_limit_exceeded(cmd);
+ return -1;
+ }
+ conn->state.data_size = new_size;
+
+ if (chunk_last) {
+ smtp_server_command_remove_hook(
+ command, SMTP_SERVER_COMMAND_HOOK_REPLIED,
+ cmd_data_chunk_replied);
+ smtp_server_command_add_hook(
+ command, SMTP_SERVER_COMMAND_HOOK_REPLIED,
+ cmd_data_replied, data_cmd);
+ }
+
+ data_cmd->chunk_input = chunk;
+ data_cmd->chunk_size = chunk_size;
+ data_cmd->chunk_last = chunk_last;
+ data_cmd->client_input = client_input;
+ i_stream_ref(chunk);
+
+ cmd_data_start_input(cmd, data_cmd, conn->state.data_chain_input);
+ i_stream_unref(&conn->state.data_chain_input);
+ return 0;
+}
+
+/* BDAT command */
+
+void smtp_server_cmd_bdat(struct smtp_server_cmd_ctx *cmd,
+ const char *params)
+{
+ struct smtp_server_connection *conn = cmd->conn;
+ struct istream *input = NULL;
+ uoff_t size = 0;
+ const char *const *argv;
+ bool chunk_last = FALSE;
+ int ret = 1;
+
+ if ((conn->set.capabilities & SMTP_CAPABILITY_CHUNKING) == 0) {
+ smtp_server_reply(cmd,
+ 502, "5.5.1", "Unsupported command");
+ return;
+ }
+
+ smtp_server_connection_data_chunk_init(cmd);
+
+ /* bdat-cmd = "BDAT" SP chunk-size [ SP end-marker ] CR LF
+ chunk-size = 1*DIGIT
+ end-marker = "LAST"
+ */
+ argv = t_strsplit(params, " ");
+ if (argv[0] == NULL || str_to_uoff(argv[0], &size) < 0) {
+ smtp_server_reply(cmd,
+ 501, "5.5.4", "Invalid chunk size parameter");
+ size = 0;
+ ret = -1;
+ } else if (argv[1] != NULL) {
+ if (argv[2] != NULL) {
+ smtp_server_reply(cmd,
+ 501, "5.5.4", "Invalid parameters");
+ ret = -1;
+ } else if (strcasecmp(argv[1], "LAST") != 0) {
+ smtp_server_reply(cmd,
+ 501, "5.5.4", "Invalid end marker parameter");
+ ret = -1;
+ } else {
+ chunk_last = TRUE;
+ }
+ }
+
+ if (ret > 0 || (size > 0 && !conn->disconnected)) {
+ /* Read/skip data even in case of error, as long as size is
+ known and connection is still usable. */
+ input = smtp_command_parse_data_with_size(conn->smtp_parser,
+ size);
+ }
+
+ if (ret < 0) {
+ i_stream_unref(&input);
+ return;
+ }
+
+ (void)smtp_server_connection_data_chunk_add(cmd,
+ input, size, chunk_last, TRUE);
+ i_stream_unref(&input);
+}
diff --git a/src/lib-smtp/smtp-server-cmd-helo.c b/src/lib-smtp/smtp-server-cmd-helo.c
new file mode 100644
index 0000000..2f03ceb
--- /dev/null
+++ b/src/lib-smtp/smtp-server-cmd-helo.c
@@ -0,0 +1,196 @@
+/* Copyright (c) 2013-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "str.h"
+#include "array.h"
+#include "smtp-syntax.h"
+
+#include "smtp-server-private.h"
+
+/* EHLO, HELO commands */
+
+static void
+cmd_helo_completed(struct smtp_server_cmd_ctx *cmd,
+ struct smtp_server_cmd_helo *data)
+{
+ struct smtp_server_connection *conn = cmd->conn;
+ struct smtp_server_command *command = cmd->cmd;
+
+ i_assert(smtp_server_command_is_replied(command));
+ if (!smtp_server_command_replied_success(command)) {
+ /* Failure */
+ return;
+ }
+
+ if (conn->pending_helo == &data->helo)
+ conn->pending_helo = NULL;
+
+ /* Success */
+ smtp_server_connection_reset_state(conn);
+
+ i_free(conn->helo_domain);
+ conn->helo_domain = i_strdup(data->helo.domain);
+ conn->helo.domain = conn->helo_domain;
+ conn->helo.domain_valid = data->helo.domain_valid;
+ conn->helo.old_smtp = data->helo.old_smtp;
+}
+
+static void
+cmd_helo_next(struct smtp_server_cmd_ctx *cmd,
+ struct smtp_server_cmd_helo *data)
+{
+ struct smtp_server_connection *conn = cmd->conn;
+
+ if (null_strcmp(conn->helo.domain, data->helo.domain) != 0 ||
+ conn->helo.old_smtp != data->helo.old_smtp)
+ data->changed = TRUE; /* Definitive assessment */
+}
+
+static void
+smtp_server_cmd_helo_run(struct smtp_server_cmd_ctx *cmd, const char *params,
+ bool old_smtp)
+{
+ struct smtp_server_connection *conn = cmd->conn;
+ const struct smtp_server_callbacks *callbacks = conn->callbacks;
+ struct smtp_server_cmd_helo *helo_data;
+ struct smtp_server_command *command = cmd->cmd;
+ bool first = (conn->pending_helo == NULL && conn->helo.domain == NULL);
+ const char *domain = NULL;
+ int ret;
+
+ /* Parse domain argument */
+
+ if (*params == '\0') {
+ smtp_server_reply(cmd, 501, "", "Missing hostname");
+ return;
+ }
+ ret = smtp_helo_domain_parse(params, !old_smtp, &domain);
+
+ smtp_server_command_pipeline_block(cmd);
+ if (conn->state.state == SMTP_SERVER_STATE_GREETING) {
+ smtp_server_connection_set_state(conn, SMTP_SERVER_STATE_HELO,
+ NULL);
+ }
+
+ helo_data = p_new(cmd->pool, struct smtp_server_cmd_helo, 1);
+ helo_data->helo.domain = p_strdup(cmd->pool, domain);
+ helo_data->helo.domain_valid = ( ret >= 0 );
+ helo_data->helo.old_smtp = old_smtp;
+ helo_data->first = first;
+ command->data = helo_data;
+
+ if (null_strcmp(conn->helo.domain, domain) != 0 ||
+ conn->helo.old_smtp != old_smtp)
+ helo_data->changed = TRUE; /* Preliminary assessment */
+
+ if (conn->pending_helo == NULL)
+ conn->pending_helo = &helo_data->helo;
+
+ smtp_server_command_add_hook(
+ command, SMTP_SERVER_COMMAND_HOOK_NEXT,
+ cmd_helo_next, helo_data);
+ smtp_server_command_add_hook(
+ command, SMTP_SERVER_COMMAND_HOOK_COMPLETED,
+ cmd_helo_completed, helo_data);
+
+ smtp_server_command_ref(command);
+ if (callbacks != NULL && callbacks->conn_cmd_helo != NULL) {
+ /* Specific implementation of EHLO command */
+ ret = callbacks->conn_cmd_helo(conn->context, cmd, helo_data);
+ if (ret <= 0) {
+ i_assert(ret == 0 ||
+ smtp_server_command_is_replied(command));
+ /* Command is waiting for external event or it failed */
+ smtp_server_command_unref(&command);
+ return;
+ }
+ }
+
+ if (!smtp_server_command_is_replied(command)) {
+ /* Submit default EHLO reply if none is provided */
+ smtp_server_cmd_ehlo_reply_default(cmd);
+ }
+ smtp_server_command_unref(&command);
+}
+
+void smtp_server_cmd_ehlo(struct smtp_server_cmd_ctx *cmd, const char *params)
+{
+ /* ehlo = "EHLO" SP ( Domain / address-literal ) CRLF */
+
+ smtp_server_cmd_helo_run(cmd, params, FALSE);
+}
+
+void smtp_server_cmd_helo(struct smtp_server_cmd_ctx *cmd, const char *params)
+{
+ /* helo = "HELO" SP Domain CRLF */
+
+ smtp_server_cmd_helo_run(cmd, params, TRUE);
+}
+
+struct smtp_server_reply *
+smtp_server_cmd_ehlo_reply_create(struct smtp_server_cmd_ctx *cmd)
+{
+ static struct {
+ const char *name;
+ void (*add)(struct smtp_server_reply *reply);
+ } standard_caps[] = {
+ /* Sorted alphabetically */
+ { "8BITMIME", smtp_server_reply_ehlo_add_8bitmime },
+ { "BINARYMIME", smtp_server_reply_ehlo_add_binarymime },
+ { "CHUNKING", smtp_server_reply_ehlo_add_chunking },
+ { "DSN", smtp_server_reply_ehlo_add_dsn },
+ { "ENHANCEDSTATUSCODES",
+ smtp_server_reply_ehlo_add_enhancedstatuscodes },
+ { "PIPELINING", smtp_server_reply_ehlo_add_pipelining },
+ { "SIZE", smtp_server_reply_ehlo_add_size },
+ { "STARTTLS", smtp_server_reply_ehlo_add_starttls },
+ { "VRFY", smtp_server_reply_ehlo_add_vrfy },
+ { "XCLIENT", smtp_server_reply_ehlo_add_xclient }
+ };
+ const unsigned int standard_caps_count = N_ELEMENTS(standard_caps);
+ struct smtp_server_connection *conn = cmd->conn;
+ struct smtp_server_command *command = cmd->cmd;
+ struct smtp_server_cmd_helo *helo_data = command->data;
+ const struct smtp_capability_extra *extra_caps = NULL;
+ unsigned int extra_caps_count, i, j;
+ struct smtp_server_reply *reply;
+
+ reply = smtp_server_reply_create_ehlo(cmd->cmd);
+
+ if (helo_data->helo.old_smtp) {
+ i_assert(cmd->cmd->reg->func == smtp_server_cmd_helo);
+ return reply;
+ }
+ i_assert(cmd->cmd->reg->func == smtp_server_cmd_ehlo);
+
+ extra_caps_count = 0;
+ if (array_is_created(&conn->extra_capabilities)) {
+ extra_caps = array_get(&conn->extra_capabilities,
+ &extra_caps_count);
+ }
+
+ i = j = 0;
+ while (i < standard_caps_count || j < extra_caps_count) {
+ if (i < standard_caps_count &&
+ (j >= extra_caps_count ||
+ strcasecmp(standard_caps[i].name,
+ extra_caps[j].name) < 0)) {
+ standard_caps[i].add(reply);
+ i++;
+ } else {
+ smtp_server_reply_ehlo_add_params(
+ reply, extra_caps[j].name,
+ extra_caps[j].params);
+ j++;
+ }
+ }
+ return reply;
+}
+
+void smtp_server_cmd_ehlo_reply_default(struct smtp_server_cmd_ctx *cmd)
+{
+ struct smtp_server_reply *reply;
+
+ reply = smtp_server_cmd_ehlo_reply_create(cmd);
+ smtp_server_reply_submit(reply);
+}
diff --git a/src/lib-smtp/smtp-server-cmd-mail.c b/src/lib-smtp/smtp-server-cmd-mail.c
new file mode 100644
index 0000000..82b8e81
--- /dev/null
+++ b/src/lib-smtp/smtp-server-cmd-mail.c
@@ -0,0 +1,216 @@
+/* Copyright (c) 2013-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "ioloop.h"
+#include "smtp-parser.h"
+#include "smtp-syntax.h"
+#include "smtp-address.h"
+
+#include "smtp-server-private.h"
+
+/* MAIL command */
+
+static bool cmd_mail_check_state(struct smtp_server_cmd_ctx *cmd)
+{
+ struct smtp_server_connection *conn = cmd->conn;
+ struct smtp_server_command *command = cmd->cmd;
+
+ if (smtp_server_command_is_replied(command) &&
+ !smtp_server_command_replied_success(command) &&
+ !smtp_server_command_reply_is_forwarded(command))
+ return FALSE;
+
+ if (conn->state.trans != NULL) {
+ smtp_server_reply(cmd, 503, "5.5.0", "MAIL already given");
+ return FALSE;
+ }
+ return TRUE;
+}
+
+static void
+cmd_mail_completed(struct smtp_server_cmd_ctx *cmd,
+ struct smtp_server_cmd_mail *data)
+{
+ struct smtp_server_connection *conn = cmd->conn;
+ struct smtp_server_command *command = cmd->cmd;
+
+ i_assert(conn->state.pending_mail_cmds > 0);
+ conn->state.pending_mail_cmds--;
+
+ i_assert(smtp_server_command_is_replied(command));
+ i_assert(conn->state.state == SMTP_SERVER_STATE_MAIL_FROM ||
+ !smtp_server_command_replied_success(command));
+
+ if (!smtp_server_command_replied_success(command)) {
+ /* Failure */
+ return;
+ }
+
+ /* Success */
+ conn->state.trans = smtp_server_transaction_create(conn, data);
+}
+
+static void
+cmd_mail_recheck(struct smtp_server_cmd_ctx *cmd,
+ struct smtp_server_cmd_mail *data)
+{
+ struct smtp_server_connection *conn = cmd->conn;
+
+ /* All preceeding commands have finished and now the transaction state
+ is clear. This provides the opportunity to re-check the transaction
+ state */
+ if (!cmd_mail_check_state(cmd))
+ return;
+
+ /* Advance state */
+ smtp_server_connection_set_state(conn, SMTP_SERVER_STATE_MAIL_FROM,
+ smtp_address_encode(data->path));
+}
+
+void smtp_server_cmd_mail(struct smtp_server_cmd_ctx *cmd,
+ const char *params)
+{
+ struct smtp_server_connection *conn = cmd->conn;
+ const struct smtp_server_settings *set = &conn->set;
+ enum smtp_capability caps = set->capabilities;
+ const struct smtp_server_callbacks *callbacks = conn->callbacks;
+ struct smtp_server_command *command = cmd->cmd;
+ struct smtp_server_cmd_mail *mail_data;
+ enum smtp_address_parse_flags path_parse_flags;
+ const char *const *param_extensions = NULL;
+ struct smtp_address *path = NULL;
+ enum smtp_param_parse_error pperror;
+ const char *error;
+ int ret;
+
+ /* mail = "MAIL FROM:" Reverse-path [SP Mail-parameters] CRLF
+ Reverse-path = Path / "<>"
+ */
+
+ /* Check transaction state as far as possible */
+ if (!cmd_mail_check_state(cmd))
+ return;
+
+ /* Reverse-path */
+ if (params == NULL || strncasecmp(params, "FROM:", 5) != 0) {
+ smtp_server_reply(cmd, 501, "5.5.4", "Invalid parameters");
+ return;
+ }
+ if (params[5] != ' ' && params[5] != '\t') {
+ params += 5;
+ } else if ((set->workarounds &
+ SMTP_SERVER_WORKAROUND_WHITESPACE_BEFORE_PATH) != 0) {
+ params += 5;
+ while (*params == ' ' || *params == '\t')
+ params++;
+ } else {
+ smtp_server_reply(cmd, 501, "5.5.4",
+ "Invalid FROM: "
+ "Unexpected whitespace before path");
+ return;
+ }
+ path_parse_flags =
+ SMTP_ADDRESS_PARSE_FLAG_ALLOW_EMPTY |
+ SMTP_ADDRESS_PARSE_FLAG_PRESERVE_RAW;
+ if (*params != '\0' &&
+ (set->workarounds & SMTP_SERVER_WORKAROUND_MAILBOX_FOR_PATH) != 0)
+ path_parse_flags |= SMTP_ADDRESS_PARSE_FLAG_BRACKETS_OPTIONAL;
+ if (set->mail_path_allow_broken) {
+ path_parse_flags |=
+ SMTP_ADDRESS_PARSE_FLAG_ALLOW_BAD_LOCALPART |
+ SMTP_ADDRESS_PARSE_FLAG_IGNORE_BROKEN;
+ }
+ ret = smtp_address_parse_path_full(pool_datastack_create(), params,
+ path_parse_flags, &path, &error,
+ &params);
+ if (ret < 0 && !smtp_address_is_broken(path)) {
+ smtp_server_reply(cmd, 501, "5.5.4",
+ "Invalid FROM: %s", error);
+ return;
+ }
+ if (*params == ' ')
+ params++;
+ else if (*params != '\0') {
+ smtp_server_reply(
+ cmd, 501, "5.5.4",
+ "Invalid FROM: Invalid character in path");
+ return;
+ }
+ if (ret < 0) {
+ i_assert(set->mail_path_allow_broken);
+ e_debug(conn->event, "Invalid FROM: %s "
+ "(proceeding with <> as sender)", error);
+ }
+
+ mail_data = p_new(cmd->pool, struct smtp_server_cmd_mail, 1);
+
+ if (conn->set.protocol == SMTP_PROTOCOL_LMTP)
+ mail_data->flags |= SMTP_SERVER_TRANSACTION_FLAG_REPLY_PER_RCPT;
+
+ /* [SP Mail-parameters] */
+ if (array_is_created(&conn->mail_param_extensions))
+ param_extensions = array_front(&conn->mail_param_extensions);
+ if (smtp_params_mail_parse(cmd->pool, params, caps, param_extensions,
+ NULL, &mail_data->params, &pperror,
+ &error) < 0) {
+ switch (pperror) {
+ case SMTP_PARAM_PARSE_ERROR_BAD_SYNTAX:
+ smtp_server_reply(cmd, 501, "5.5.4", "%s", error);
+ break;
+ case SMTP_PARAM_PARSE_ERROR_NOT_SUPPORTED:
+ smtp_server_reply(cmd, 555, "5.5.4", "%s", error);
+ break;
+ default:
+ i_unreached();
+ }
+ return;
+ }
+
+ if ((caps & SMTP_CAPABILITY_SIZE) != 0 && set->max_message_size > 0 &&
+ mail_data->params.size > set->max_message_size) {
+ smtp_server_reply(cmd, 552, "5.2.3",
+ "Message size exceeds administrative limit");
+ return;
+ }
+
+ mail_data->path = smtp_address_clone(cmd->pool, path);
+ mail_data->timestamp = ioloop_timeval;
+
+ smtp_server_command_add_hook(command, SMTP_SERVER_COMMAND_HOOK_NEXT,
+ cmd_mail_recheck, mail_data);
+ smtp_server_command_add_hook(command, SMTP_SERVER_COMMAND_HOOK_COMPLETED,
+ cmd_mail_completed, mail_data);
+
+ smtp_server_connection_set_state(conn, SMTP_SERVER_STATE_MAIL_FROM,
+ smtp_address_encode(mail_data->path));
+ conn->state.pending_mail_cmds++;
+
+ smtp_server_command_ref(command);
+ if (callbacks != NULL && callbacks->conn_cmd_mail != NULL) {
+ /* Specific implementation of MAIL command */
+ struct event_reason *reason =
+ smtp_server_connection_reason_begin(conn, "cmd_mail");
+ ret = callbacks->conn_cmd_mail(conn->context, cmd, mail_data);
+ event_reason_end(&reason);
+ if (ret <= 0) {
+ i_assert(ret == 0 ||
+ smtp_server_command_is_replied(command));
+ /* Command is waiting for external event or it failed */
+ smtp_server_command_unref(&command);
+ return;
+ }
+ }
+ if (!smtp_server_command_is_replied(command)) {
+ /* Set generic MAIL success reply if none is provided */
+ smtp_server_cmd_mail_reply_success(cmd);
+ }
+ smtp_server_command_unref(&command);
+}
+
+void smtp_server_cmd_mail_reply_success(struct smtp_server_cmd_ctx *cmd)
+{
+ i_assert(cmd->cmd->reg->func == smtp_server_cmd_mail);
+
+ smtp_server_reply(cmd, 250, "2.1.0", "OK");
+}
diff --git a/src/lib-smtp/smtp-server-cmd-noop.c b/src/lib-smtp/smtp-server-cmd-noop.c
new file mode 100644
index 0000000..86e426f
--- /dev/null
+++ b/src/lib-smtp/smtp-server-cmd-noop.c
@@ -0,0 +1,52 @@
+/* Copyright (c) 2013-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "smtp-syntax.h"
+
+#include "smtp-server-private.h"
+
+/* NOOP command */
+
+void smtp_server_cmd_noop(struct smtp_server_cmd_ctx *cmd,
+ const char *params)
+{
+ struct smtp_server_connection *conn = cmd->conn;
+ struct smtp_server_command *command = cmd->cmd;
+ const struct smtp_server_callbacks *callbacks = conn->callbacks;
+ const char *param, *error;
+ int ret;
+
+ /* "NOOP" [ SP String ] CRLF */
+ ret = smtp_string_parse(params, &param, &error);
+ if (ret < 0) {
+ smtp_server_reply(cmd, 501, "5.5.4",
+ "Invalid string parameter: %s",
+ error);
+ return;
+ }
+
+ smtp_server_command_input_lock(cmd);
+
+ smtp_server_command_ref(command);
+ if (callbacks != NULL && callbacks->conn_cmd_noop != NULL) {
+ /* specific implementation of NOOP command */
+ ret = callbacks->conn_cmd_noop(conn->context, cmd);
+ if (ret <= 0) {
+ i_assert(ret == 0 ||
+ smtp_server_command_is_replied(command));
+ /* command is waiting for external event or it failed */
+ smtp_server_command_unref(&command);
+ return;
+ }
+ }
+ if (!smtp_server_command_is_replied(command))
+ smtp_server_cmd_noop_reply_success(cmd);
+ smtp_server_command_unref(&command);
+}
+
+void smtp_server_cmd_noop_reply_success(struct smtp_server_cmd_ctx *cmd)
+{
+ i_assert(cmd->cmd->reg->func == smtp_server_cmd_noop);
+
+ smtp_server_reply(cmd, 250, "2.0.0", "OK");
+}
diff --git a/src/lib-smtp/smtp-server-cmd-quit.c b/src/lib-smtp/smtp-server-cmd-quit.c
new file mode 100644
index 0000000..04764fd
--- /dev/null
+++ b/src/lib-smtp/smtp-server-cmd-quit.c
@@ -0,0 +1,42 @@
+/* Copyright (c) 2013-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+
+#include "smtp-server-private.h"
+
+/* QUIT command */
+
+void smtp_server_cmd_quit(struct smtp_server_cmd_ctx *cmd,
+ const char *params)
+{
+ struct smtp_server_connection *conn = cmd->conn;
+ struct smtp_server_command *command = cmd->cmd;
+ const struct smtp_server_callbacks *callbacks = conn->callbacks;
+ int ret;
+
+ /* "QUIT" CRLF */
+ if (*params != '\0') {
+ smtp_server_reply(cmd,
+ 501, "5.5.4", "Invalid parameters");
+ return;
+ }
+
+ smtp_server_connection_input_halt(conn);
+
+ smtp_server_command_ref(command);
+ if (callbacks != NULL && callbacks->conn_cmd_quit != NULL) {
+ /* specific implementation of QUIT command */
+ if ((ret = callbacks->conn_cmd_quit(conn->context, cmd)) <= 0) {
+ i_assert(ret == 0 ||
+ smtp_server_command_is_replied(command));
+ /* command is waiting for external event or it failed */
+ smtp_server_command_unref(&command);
+ return;
+ }
+ }
+ if (!smtp_server_command_is_replied(command)) {
+ /* set generic QUIT success reply if none is provided */
+ smtp_server_reply_quit(cmd);
+ }
+ smtp_server_command_unref(&command);
+}
diff --git a/src/lib-smtp/smtp-server-cmd-rcpt.c b/src/lib-smtp/smtp-server-cmd-rcpt.c
new file mode 100644
index 0000000..85e9a93
--- /dev/null
+++ b/src/lib-smtp/smtp-server-cmd-rcpt.c
@@ -0,0 +1,243 @@
+/* Copyright (c) 2013-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "str.h"
+#include "array.h"
+#include "smtp-parser.h"
+#include "smtp-address.h"
+#include "smtp-reply.h"
+#include "smtp-syntax.h"
+
+#include "smtp-server-private.h"
+
+/* RCPT command */
+
+struct smtp_server_cmd_rcpt {
+ struct smtp_server_recipient *rcpt;
+};
+
+static void
+cmd_rcpt_destroy(struct smtp_server_cmd_ctx *cmd ATTR_UNUSED,
+ struct smtp_server_cmd_rcpt *data)
+{
+ smtp_server_recipient_destroy(&data->rcpt);
+}
+
+static bool
+cmd_rcpt_check_state(struct smtp_server_cmd_ctx *cmd, bool next_to_reply)
+{
+ struct smtp_server_connection *conn = cmd->conn;
+ struct smtp_server_command *command = cmd->cmd;
+ struct smtp_server_transaction *trans = conn->state.trans;
+
+ if (smtp_server_command_is_replied(command) &&
+ !smtp_server_command_replied_success(command) &&
+ !smtp_server_command_reply_is_forwarded(command))
+ return FALSE;
+
+ if (trans == NULL &&
+ (conn->state.pending_mail_cmds == 0 || next_to_reply)) {
+ smtp_server_reply(cmd,
+ 503, "5.5.0", "MAIL needed first");
+ return FALSE;
+ }
+ if (conn->set.max_recipients > 0 && trans != NULL &&
+ smtp_server_transaction_rcpt_count(trans) >=
+ conn->set.max_recipients) {
+ smtp_server_reply(cmd,
+ 451, "4.5.3", "Too many recipients");
+ return FALSE;
+ }
+
+ return TRUE;
+}
+
+static void
+cmd_rcpt_completed(struct smtp_server_cmd_ctx *cmd,
+ struct smtp_server_cmd_rcpt *data)
+{
+ struct smtp_server_connection *conn = cmd->conn;
+ struct smtp_server_command *command = cmd->cmd;
+ struct smtp_server_recipient *rcpt = data->rcpt;
+
+ i_assert(conn->state.pending_rcpt_cmds > 0);
+ conn->state.pending_rcpt_cmds--;
+
+ i_assert(smtp_server_command_is_replied(command));
+ i_assert(conn->state.state == SMTP_SERVER_STATE_RCPT_TO ||
+ !smtp_server_command_replied_success(command));
+
+ if (!smtp_server_command_replied_success(command)) {
+ /* Failure */
+ conn->state.denied_rcpt_cmds++;
+ smtp_server_recipient_denied(
+ rcpt, smtp_server_command_get_reply(cmd->cmd, 0));
+ return;
+ }
+
+ /* Success */
+ data->rcpt = NULL; /* clear to prevent destruction */
+ (void)smtp_server_recipient_approved(&rcpt);
+}
+
+static void
+cmd_rcpt_recheck(struct smtp_server_cmd_ctx *cmd,
+ struct smtp_server_cmd_rcpt *data ATTR_UNUSED)
+{
+ struct smtp_server_connection *conn = cmd->conn;
+
+ /* All preceeding commands have finished and now the transaction state
+ is clear. This provides the opportunity to re-check the transaction
+ state and abort the pending proxied mail command if it is bound to
+ fail */
+ if (!cmd_rcpt_check_state(cmd, TRUE))
+ return;
+
+ /* Advance state */
+ smtp_server_connection_set_state(conn, SMTP_SERVER_STATE_RCPT_TO,
+ smtp_address_encode(data->rcpt->path));
+}
+
+void smtp_server_cmd_rcpt(struct smtp_server_cmd_ctx *cmd,
+ const char *params)
+{
+ struct smtp_server_connection *conn = cmd->conn;
+ const struct smtp_server_settings *set = &conn->set;
+ enum smtp_capability caps = set->capabilities;
+ const struct smtp_server_callbacks *callbacks = conn->callbacks;
+ struct smtp_server_command *command = cmd->cmd;
+ struct smtp_server_cmd_rcpt *rcpt_data;
+ struct smtp_server_recipient *rcpt;
+ enum smtp_address_parse_flags path_parse_flags;
+ enum smtp_param_rcpt_parse_flags param_parse_flags;
+ const char *const *param_extensions = NULL;
+ struct smtp_address *path;
+ struct smtp_params_rcpt rcpt_params;
+ enum smtp_param_parse_error pperror;
+ const char *error;
+ int ret;
+
+ /* rcpt = "RCPT TO:" ( "<Postmaster@" Domain ">" /
+ "<Postmaster>" / Forward-path ) [SP Rcpt-parameters] CRLF
+ Forward-path = Path
+ */
+
+ /* Check transaction state as far as possible */
+ if (!cmd_rcpt_check_state(cmd, FALSE))
+ return;
+
+ /* ( "<Postmaster@" Domain ">" / "<Postmaster>" / Forward-path ) */
+ if (params == NULL || strncasecmp(params, "TO:", 3) != 0) {
+ smtp_server_reply(cmd,
+ 501, "5.5.4", "Invalid parameters");
+ return;
+ }
+ if (params[3] != ' ' && params[3] != '\t') {
+ params += 3;
+ } else if ((set->workarounds &
+ SMTP_SERVER_WORKAROUND_WHITESPACE_BEFORE_PATH) != 0) {
+ params += 3;
+ while (*params == ' ' || *params == '\t')
+ params++;
+ } else {
+ smtp_server_reply(cmd, 501, "5.5.4",
+ "Invalid TO: "
+ "Unexpected whitespace before path");
+ return;
+ }
+ path_parse_flags = SMTP_ADDRESS_PARSE_FLAG_ALLOW_LOCALPART;
+ if ((set->workarounds & SMTP_SERVER_WORKAROUND_MAILBOX_FOR_PATH) != 0)
+ path_parse_flags |= SMTP_ADDRESS_PARSE_FLAG_BRACKETS_OPTIONAL;
+ if (smtp_address_parse_path_full(pool_datastack_create(), params,
+ path_parse_flags, &path, &error,
+ &params) < 0) {
+ smtp_server_reply(cmd,
+ 501, "5.5.4", "Invalid TO: %s", error);
+ return;
+ }
+ if (*params == ' ')
+ params++;
+ else if (*params != '\0') {
+ smtp_server_reply(cmd, 501, "5.5.4",
+ "Invalid TO: Invalid character in path");
+ return;
+ }
+ if (path->domain == NULL && !conn->set.rcpt_domain_optional &&
+ strcasecmp(path->localpart, "postmaster") != 0) {
+ smtp_server_reply(cmd,
+ 501, "5.5.4", "Invalid TO: Missing domain");
+ return;
+ }
+
+ /* [SP Rcpt-parameters] */
+ param_parse_flags = 0;
+ if (conn->set.rcpt_domain_optional)
+ param_parse_flags |= SMTP_PARAM_RCPT_FLAG_ORCPT_ALLOW_LOCALPART;
+ if (array_is_created(&conn->rcpt_param_extensions))
+ param_extensions = array_front(&conn->rcpt_param_extensions);
+ if (smtp_params_rcpt_parse(pool_datastack_create(), params,
+ param_parse_flags, caps, param_extensions,
+ &rcpt_params, &pperror, &error) < 0) {
+ switch (pperror) {
+ case SMTP_PARAM_PARSE_ERROR_BAD_SYNTAX:
+ smtp_server_reply(cmd,
+ 501, "5.5.4", "%s", error);
+ break;
+ case SMTP_PARAM_PARSE_ERROR_NOT_SUPPORTED:
+ smtp_server_reply(cmd,
+ 555, "5.5.4", "%s", error);
+ break;
+ default:
+ i_unreached();
+ }
+ return;
+ }
+
+ rcpt = smtp_server_recipient_create(cmd, path, &rcpt_params);
+
+ rcpt_data = p_new(cmd->pool, struct smtp_server_cmd_rcpt, 1);
+ rcpt_data->rcpt = rcpt;
+ command->data = rcpt_data;
+
+ smtp_server_command_add_hook(command, SMTP_SERVER_COMMAND_HOOK_NEXT,
+ cmd_rcpt_recheck, rcpt_data);
+ smtp_server_command_add_hook(command, SMTP_SERVER_COMMAND_HOOK_COMPLETED,
+ cmd_rcpt_completed, rcpt_data);
+ smtp_server_command_add_hook(command, SMTP_SERVER_COMMAND_HOOK_DESTROY,
+ cmd_rcpt_destroy, rcpt_data);
+
+ conn->state.pending_rcpt_cmds++;
+
+ smtp_server_command_ref(command);
+ i_assert(callbacks != NULL && callbacks->conn_cmd_rcpt != NULL);
+
+ struct event_reason *reason =
+ smtp_server_connection_reason_begin(conn, "cmd_rcpt");
+ ret = callbacks->conn_cmd_rcpt(conn->context, cmd, rcpt);
+ event_reason_end(&reason);
+ if (ret <= 0) {
+ i_assert(ret == 0 || smtp_server_command_is_replied(command));
+ /* Command is waiting for external event or it failed */
+ smtp_server_command_unref(&command);
+ return;
+ }
+ if (!smtp_server_command_is_replied(command)) {
+ /* Set generic RCPT success reply if none is provided */
+ smtp_server_cmd_rcpt_reply_success(cmd);
+ }
+ smtp_server_command_unref(&command);
+}
+
+bool smtp_server_command_is_rcpt(struct smtp_server_cmd_ctx *cmd)
+{
+ return (cmd->cmd->reg->func == smtp_server_cmd_rcpt);
+}
+
+void smtp_server_cmd_rcpt_reply_success(struct smtp_server_cmd_ctx *cmd)
+{
+ struct smtp_server_cmd_rcpt *rcpt_data = cmd->cmd->data;
+
+ i_assert(smtp_server_command_is_rcpt(cmd));
+
+ smtp_server_recipient_reply(rcpt_data->rcpt, 250, "2.1.5", "OK");
+}
diff --git a/src/lib-smtp/smtp-server-cmd-rset.c b/src/lib-smtp/smtp-server-cmd-rset.c
new file mode 100644
index 0000000..b07478b
--- /dev/null
+++ b/src/lib-smtp/smtp-server-cmd-rset.c
@@ -0,0 +1,70 @@
+/* Copyright (c) 2013-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+
+#include "smtp-server-private.h"
+
+/* RSET command */
+
+static void
+cmd_rset_completed(struct smtp_server_cmd_ctx *cmd, void *context ATTR_UNUSED)
+{
+ struct smtp_server_connection *conn = cmd->conn;
+ struct smtp_server_command *command = cmd->cmd;
+
+ i_assert(smtp_server_command_is_replied(command));
+ if (!smtp_server_command_replied_success(command)) {
+ /* failure */
+ return;
+ }
+
+ /* success */
+ if (conn->state.trans != NULL)
+ smtp_server_transaction_reset(conn->state.trans);
+ smtp_server_connection_reset_state(conn);
+}
+
+void smtp_server_cmd_rset(struct smtp_server_cmd_ctx *cmd,
+ const char *params)
+{
+ struct smtp_server_connection *conn = cmd->conn;
+ const struct smtp_server_callbacks *callbacks = conn->callbacks;
+ struct smtp_server_command *command = cmd->cmd;
+ int ret;
+
+ /* rset = "RSET" CRLF */
+ if (*params != '\0') {
+ smtp_server_reply(cmd,
+ 501, "5.5.4", "Invalid parameters");
+ return;
+ }
+
+ smtp_server_command_pipeline_block(cmd);
+ smtp_server_command_add_hook(command, SMTP_SERVER_COMMAND_HOOK_COMPLETED,
+ cmd_rset_completed, NULL);
+
+ smtp_server_command_ref(command);
+ if (callbacks != NULL && callbacks->conn_cmd_rset != NULL) {
+ /* specific implementation of RSET command */
+ if ((ret=callbacks->conn_cmd_rset(conn->context, cmd)) <= 0) {
+ i_assert(ret == 0 ||
+ smtp_server_command_is_replied(command));
+ /* command is waiting for external event or it failed */
+ smtp_server_command_unref(&command);
+ return;
+ }
+ }
+
+ if (!smtp_server_command_is_replied(command)) {
+ /* set generic RSET success reply if none is provided */
+ smtp_server_cmd_rset_reply_success(cmd);
+ }
+ smtp_server_command_unref(&command);;
+}
+
+void smtp_server_cmd_rset_reply_success(struct smtp_server_cmd_ctx *cmd)
+{
+ i_assert(cmd->cmd->reg->func == smtp_server_cmd_rset);
+
+ smtp_server_reply(cmd, 250, "2.0.0", "OK");
+}
diff --git a/src/lib-smtp/smtp-server-cmd-starttls.c b/src/lib-smtp/smtp-server-cmd-starttls.c
new file mode 100644
index 0000000..de53b39
--- /dev/null
+++ b/src/lib-smtp/smtp-server-cmd-starttls.c
@@ -0,0 +1,179 @@
+/* Copyright (c) 2013-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "istream.h"
+#include "ostream.h"
+#include "iostream-ssl.h"
+#include "master-service.h"
+#include "master-service-ssl.h"
+#include "smtp-syntax.h"
+
+#include "smtp-server-private.h"
+
+/* STARTTLS command (RFC 3207) */
+
+static int cmd_starttls_start(struct smtp_server_connection *conn)
+{
+ const struct smtp_server_callbacks *callbacks = conn->callbacks;
+
+ e_debug(conn->event, "Starting TLS");
+
+ if (callbacks != NULL && callbacks->conn_start_tls != NULL) {
+ struct smtp_server_connection *tmp_conn = conn;
+ struct istream *input = conn->conn.input;
+ struct ostream *output = conn->conn.output;
+ int ret;
+
+ smtp_server_connection_ref(tmp_conn);
+ ret = callbacks->conn_start_tls(tmp_conn->context,
+ &input, &output);
+ if (!smtp_server_connection_unref(&tmp_conn) || ret < 0)
+ return -1;
+
+ smtp_server_connection_set_ssl_streams(conn, input, output);
+ } else if (smtp_server_connection_ssl_init(conn) < 0) {
+ smtp_server_connection_close(&conn,
+ "SSL Initialization failed");
+ return -1;
+ }
+
+ /* The command queue must be empty at this point. If anything were to be
+ queued somehow, this connection is vulnerable to STARTTLS command
+ insertion.
+ */
+ i_assert(conn->command_queue_count == 0 &&
+ conn->command_queue_head == NULL);
+
+ /* RFC 3207, Section 4.2:
+
+ Upon completion of the TLS handshake, the SMTP protocol is reset to
+ the initial state (the state in SMTP after a server issues a 220
+ service ready greeting). The server MUST discard any knowledge
+ obtained from the client, such as the argument to the EHLO command,
+ which was not obtained from the TLS negotiation itself.
+ */
+ smtp_server_connection_clear(conn);
+ smtp_server_connection_input_unlock(conn);
+
+ return 0;
+}
+
+static int cmd_starttls_output(struct smtp_server_connection *conn)
+{
+ int ret;
+
+ if ((ret=smtp_server_connection_flush(conn)) < 0)
+ return 1;
+
+ if (ret > 0) {
+ o_stream_unset_flush_callback(conn->conn.output);
+ if (cmd_starttls_start(conn) < 0)
+ return -1;
+ }
+ return 1;
+}
+
+static void
+cmd_starttls_destroy(struct smtp_server_cmd_ctx *cmd, void *context ATTR_UNUSED)
+{
+ struct smtp_server_connection *conn = cmd->conn;
+ struct smtp_server_command *command = cmd->cmd;
+ int ret;
+
+ if (conn->conn.output == NULL)
+ return;
+
+ if (smtp_server_command_replied_success(command)) {
+ /* only one valid success status for STARTTLS command */
+ i_assert(smtp_server_command_reply_status_equals(command, 220));
+
+ /* uncork */
+ o_stream_uncork(conn->conn.output);
+
+ /* flush */
+ if ((ret=smtp_server_connection_flush(conn)) < 0) {
+ return;
+ } else if (ret == 0) {
+ /* the buffer has to be flushed */
+ i_assert(!conn->conn.output->closed);
+ o_stream_set_flush_callback(conn->conn.output,
+ cmd_starttls_output,
+ conn);
+ o_stream_set_flush_pending(conn->conn.output, TRUE);
+ } else {
+ cmd_starttls_start(conn);
+ }
+ }
+}
+
+static void
+cmd_starttls_next(struct smtp_server_cmd_ctx *cmd, void *context ATTR_UNUSED)
+{
+ struct smtp_server_connection *conn = cmd->conn;
+ struct smtp_server_command *command = cmd->cmd;
+ const struct smtp_server_callbacks *callbacks = conn->callbacks;
+ int ret;
+
+ /* The command queue can only contain the STARTTLS command at this
+ point. If anything beyond the STARTTLS were queued somehow, this
+ connection is vulnerable to STARTTLS command insertion.
+ */
+ i_assert(conn->command_queue_count == 1 &&
+ conn->command_queue_tail == command);
+
+ smtp_server_connection_set_state(conn, SMTP_SERVER_STATE_STARTTLS,
+ NULL);
+
+ smtp_server_command_ref(command);
+ if (callbacks != NULL && callbacks->conn_cmd_starttls != NULL)
+ ret = callbacks->conn_cmd_starttls(conn->context, cmd);
+ else
+ ret = 1;
+
+ smtp_server_command_add_hook(command, SMTP_SERVER_COMMAND_HOOK_DESTROY,
+ cmd_starttls_destroy, NULL);
+
+ if (ret <= 0) {
+ i_assert(ret == 0 || smtp_server_command_is_replied(command));
+ /* command is waiting for external event or it failed */
+ smtp_server_command_unref(&command);
+ return;
+ }
+ if (!smtp_server_command_is_replied(command)) {
+ smtp_server_reply(cmd,
+ 220, "2.0.0", "Begin TLS negotiation now.");
+ }
+ smtp_server_command_unref(&command);
+}
+
+void smtp_server_cmd_starttls(struct smtp_server_cmd_ctx *cmd,
+ const char *params)
+{
+ struct smtp_server_connection *conn = cmd->conn;
+ struct smtp_server_command *command = cmd->cmd;
+ enum smtp_capability capabilities = conn->set.capabilities;
+
+ if (conn->ssl_secured) {
+ i_assert((capabilities & SMTP_CAPABILITY_STARTTLS) == 0);
+ smtp_server_reply(cmd,
+ 502, "5.5.1", "TLS is already active.");
+ return;
+ } else if ((capabilities & SMTP_CAPABILITY_STARTTLS) == 0) {
+ smtp_server_reply(cmd,
+ 502, "5.5.1", "TLS support is not enabled.");
+ return;
+ }
+
+ /* "STARTTLS" CRLF */
+ if (*params != '\0') {
+ smtp_server_reply(cmd,
+ 501, "5.5.4", "Invalid parameters");
+ return;
+ }
+
+ smtp_server_command_input_lock(cmd);
+ smtp_server_connection_input_lock(conn);
+
+ smtp_server_command_add_hook(command, SMTP_SERVER_COMMAND_HOOK_NEXT,
+ cmd_starttls_next, NULL);
+}
diff --git a/src/lib-smtp/smtp-server-cmd-vrfy.c b/src/lib-smtp/smtp-server-cmd-vrfy.c
new file mode 100644
index 0000000..6eb2ae8
--- /dev/null
+++ b/src/lib-smtp/smtp-server-cmd-vrfy.c
@@ -0,0 +1,73 @@
+/* Copyright (c) 2013-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "smtp-syntax.h"
+
+#include "smtp-server-private.h"
+
+/* VRFY command */
+
+void smtp_server_cmd_vrfy(struct smtp_server_cmd_ctx *cmd,
+ const char *params)
+{
+ struct smtp_server_connection *conn = cmd->conn;
+ struct smtp_server_command *command = cmd->cmd;
+ const struct smtp_server_callbacks *callbacks = conn->callbacks;
+ const char *param, *error;
+ int ret;
+
+ /* vrfy = "VRFY" SP String CRLF */
+ ret = smtp_string_parse(params, &param, &error);
+ if (ret < 0) {
+ smtp_server_reply(cmd, 501, "5.5.4",
+ "Invalid string parameter: %s", error);
+ return;
+ } else if (ret == 0) {
+ smtp_server_reply(cmd, 501, "5.5.4", "Invalid parameters");
+ return;
+ }
+
+ smtp_server_command_ref(command);
+ if (callbacks != NULL && callbacks->conn_cmd_vrfy != NULL) {
+ /* specific implementation of VRFY command */
+ ret = callbacks->conn_cmd_vrfy(conn->context, cmd, param);
+ if (ret <= 0) {
+ i_assert(ret == 0 ||
+ smtp_server_command_is_replied(command));
+ /* command is waiting for external event or it failed */
+ smtp_server_command_unref(&command);
+ return;
+ }
+ }
+
+ /* RFC 5321, Section 3.5.3:
+
+ A server MUST NOT return a 250 code in response to a VRFY or EXPN
+ command unless it has actually verified the address. In particular,
+ a server MUST NOT return 250 if all it has done is to verify that the
+ syntax given is valid. In that case, 502 (Command not implemented)
+ or 500 (Syntax error, command unrecognized) SHOULD be returned. As
+ stated elsewhere, implementation (in the sense of actually validating
+ addresses and returning information) of VRFY and EXPN are strongly
+ recommended. Hence, implementations that return 500 or 502 for VRFY
+ are not in full compliance with this specification.
+
+ There may be circumstances where an address appears to be valid but
+ cannot reasonably be verified in real time, particularly when a
+ server is acting as a mail exchanger for another server or domain.
+ "Apparent validity", in this case, would normally involve at least
+ syntax checking and might involve verification that any domains
+ specified were ones to which the host expected to be able to relay
+ mail. In these situations, reply code 252 SHOULD be returned.
+ */
+ if (!smtp_server_command_is_replied(command))
+ smtp_server_cmd_vrfy_reply_default(cmd);
+ smtp_server_command_unref(&command);
+}
+
+void smtp_server_cmd_vrfy_reply_default(struct smtp_server_cmd_ctx *cmd)
+{
+ i_assert(cmd->cmd->reg->func == smtp_server_cmd_vrfy);
+
+ smtp_server_reply(cmd, 252, "2.3.3", "Try RCPT instead");
+}
diff --git a/src/lib-smtp/smtp-server-cmd-xclient.c b/src/lib-smtp/smtp-server-cmd-xclient.c
new file mode 100644
index 0000000..d926b45
--- /dev/null
+++ b/src/lib-smtp/smtp-server-cmd-xclient.c
@@ -0,0 +1,234 @@
+/* Copyright (c) 2013-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "istream.h"
+#include "smtp-syntax.h"
+#include "smtp-reply.h"
+
+#include "smtp-server-private.h"
+
+/* XCLIENT command (http://www.postfix.org/XCLIENT_README.html) */
+
+static bool
+cmd_xclient_check_state(struct smtp_server_cmd_ctx *cmd)
+{
+ struct smtp_server_connection *conn = cmd->conn;
+
+ /* http://www.postfix.org/XCLIENT_README.html:
+
+ The XCLIENT command may be sent at any time, except in the middle
+ of a mail delivery transaction (i.e. between MAIL and DOT, or MAIL
+ and RSET). */
+ if (conn->state.trans != NULL) {
+ smtp_server_reply(cmd, 503, "5.5.0",
+ "XCLIENT not permitted during a mail transaction");
+ return FALSE;
+ }
+ return TRUE;
+}
+
+static void
+cmd_xclient_completed(struct smtp_server_cmd_ctx *cmd,
+ struct smtp_proxy_data *proxy_data)
+{
+ struct smtp_server_connection *conn = cmd->conn;
+ struct smtp_server_command *command = cmd->cmd;
+
+ i_assert(smtp_server_command_is_replied(command));
+ if (!smtp_server_command_replied_success(command)) {
+ /* failure */
+ return;
+ }
+
+ /* success */
+ smtp_server_connection_reset_state(conn);
+ smtp_server_connection_set_proxy_data(conn, proxy_data);
+}
+
+static void
+cmd_xclient_recheck(struct smtp_server_cmd_ctx *cmd,
+ struct smtp_proxy_data *proxy_data ATTR_UNUSED)
+{
+ struct smtp_server_connection *conn = cmd->conn;
+
+ /* all preceeding commands have finished and now the transaction state is
+ clear. This provides the opportunity to re-check the protocol state */
+ if (!cmd_xclient_check_state(cmd))
+ return;
+ smtp_server_connection_set_state(conn, SMTP_SERVER_STATE_XCLIENT, NULL);
+
+ /* succes; send greeting */
+ smtp_server_reply(cmd, 220, NULL, "%s %s",
+ conn->set.hostname, conn->set.login_greeting);
+ return;
+}
+
+static void
+smtp_server_cmd_xclient_extra_field(struct smtp_server_connection *conn,
+ pool_t pool, const struct smtp_param *param,
+ ARRAY_TYPE(smtp_proxy_data_field) *fields)
+{
+ struct smtp_proxy_data_field *field;
+
+ if (conn->set.xclient_extensions == NULL ||
+ !str_array_icase_find(conn->set.xclient_extensions, param->keyword))
+ return;
+
+ if (!array_is_created(fields))
+ p_array_init(fields, pool, 8);
+ field = array_append_space(fields);
+ field->name = p_strdup(pool, param->keyword);
+ field->value = p_strdup(pool, param->value);
+}
+
+void smtp_server_cmd_xclient(struct smtp_server_cmd_ctx *cmd,
+ const char *params)
+{
+ struct smtp_server_connection *conn = cmd->conn;
+ struct smtp_server_command *command = cmd->cmd;
+ const struct smtp_server_callbacks *callbacks = conn->callbacks;
+ struct smtp_proxy_data *proxy_data;
+ ARRAY_TYPE(smtp_proxy_data_field) extra_fields = ARRAY_INIT;
+ const char *const *argv;
+
+ /* xclient-command = XCLIENT 1*( SP attribute-name"="attribute-value )
+ attribute-name = ( NAME | ADDR | PORT | PROTO | HELO | LOGIN )
+ attribute-value = xtext
+ */
+
+ if ((conn->set.capabilities & SMTP_CAPABILITY_XCLIENT) == 0) {
+ smtp_server_reply(cmd,
+ 502, "5.5.1", "Unsupported command");
+ return;
+ }
+
+ /* check transaction state as far as possible */
+ if (!cmd_xclient_check_state(cmd))
+ return;
+
+ /* check whether client is trusted */
+ if (!smtp_server_connection_is_trusted(conn)) {
+ smtp_server_reply(cmd, 550, "5.7.14",
+ "You are not from trusted IP");
+ return;
+ }
+
+ proxy_data = p_new(cmd->pool, struct smtp_proxy_data, 1);
+
+ argv = t_strsplit(params, " ");
+ for (; *argv != NULL; argv++) {
+ struct smtp_param param;
+ const char *error;
+
+ if (smtp_param_parse(pool_datastack_create(), *argv,
+ &param, &error) < 0) {
+ smtp_server_reply(cmd, 501, "5.5.4",
+ "Invalid parameter: %s", error);
+ return;
+ }
+
+ param.keyword = t_str_ucase(param.keyword);
+
+ if (smtp_xtext_parse(param.value, &param.value, &error) < 0) {
+ smtp_server_reply(cmd, 501, "5.5.4",
+ "Invalid %s parameter: %s",
+ param.keyword, error);
+ return;
+ }
+
+ if (strcmp(param.keyword, "ADDR") == 0) {
+ bool ipv6 = FALSE;
+ if (strcasecmp(param.value, "[UNAVAILABLE]") == 0)
+ continue;
+ if (strncasecmp(param.value, "IPV6:", 5) == 0) {
+ ipv6 = TRUE;
+ param.value += 5;
+ }
+ if (net_addr2ip(param.value, &proxy_data->source_ip) < 0 ||
+ (ipv6 && proxy_data->source_ip.family != AF_INET6)) {
+ smtp_server_reply(cmd, 501, "5.5.4",
+ "Invalid ADDR parameter");
+ return;
+ }
+ } else if (strcmp(param.keyword, "HELO") == 0) {
+ if (strcasecmp(param.value, "[UNAVAILABLE]") == 0)
+ continue;
+ if (smtp_helo_domain_parse
+ (param.value, TRUE, &proxy_data->helo) >= 0)
+ proxy_data->helo =
+ p_strdup(cmd->pool, proxy_data->helo);
+ } else if (strcmp(param.keyword, "LOGIN") == 0) {
+ if (strcasecmp(param.value, "[UNAVAILABLE]") == 0)
+ continue;
+ proxy_data->login = p_strdup(cmd->pool, param.value);
+ } else if (strcmp(param.keyword, "PORT") == 0) {
+ if (strcasecmp(param.value, "[UNAVAILABLE]") == 0)
+ continue;
+ if (net_str2port(param.value, &proxy_data->source_port) < 0) {
+ smtp_server_reply(cmd, 501, "5.5.4",
+ "Invalid PORT parameter");
+ return;
+ }
+ } else if (strcmp(param.keyword, "PROTO") == 0) {
+ param.value = t_str_ucase(param.value);
+ if (strcmp(param.value, "SMTP") == 0)
+ proxy_data->proto = SMTP_PROXY_PROTOCOL_SMTP;
+ else if (strcmp(param.value, "ESMTP") == 0)
+ proxy_data->proto = SMTP_PROXY_PROTOCOL_ESMTP;
+ else if (strcmp(param.value, "LMTP") == 0)
+ proxy_data->proto = SMTP_PROXY_PROTOCOL_LMTP;
+ else {
+ smtp_server_reply(cmd, 501, "5.5.4",
+ "Invalid PROTO parameter");
+ return;
+ }
+ } else if (strcmp(param.keyword, "SESSION") == 0) {
+ if (strcasecmp(param.value, "[UNAVAILABLE]") == 0)
+ continue;
+ proxy_data->session = p_strdup(cmd->pool, param.value);
+ } else if (strcmp(param.keyword, "TIMEOUT") == 0) {
+ if (str_to_uint(param.value,
+ &proxy_data->timeout_secs) < 0) {
+ smtp_server_reply(cmd, 501, "5.5.4",
+ "Invalid TIMEOUT parameter");
+ return;
+ }
+ } else if (strcmp(param.keyword, "TTL") == 0) {
+ if (str_to_uint(param.value,
+ &proxy_data->ttl_plus_1) < 0) {
+ smtp_server_reply(cmd, 501, "5.5.4",
+ "Invalid TTL parameter");
+ return;
+ }
+ proxy_data->ttl_plus_1++;
+ } else {
+ smtp_server_cmd_xclient_extra_field(conn,
+ cmd->pool, &param, &extra_fields);
+ }
+ }
+
+ if (array_is_created(&extra_fields)) {
+ proxy_data->extra_fields = array_get(&extra_fields,
+ &proxy_data->extra_fields_count);
+ }
+
+ smtp_server_command_input_lock(cmd);
+
+ smtp_server_command_add_hook(command, SMTP_SERVER_COMMAND_HOOK_NEXT,
+ cmd_xclient_recheck, proxy_data);
+ smtp_server_command_add_hook(command, SMTP_SERVER_COMMAND_HOOK_COMPLETED,
+ cmd_xclient_completed, proxy_data);
+
+ if (conn->state.state == SMTP_SERVER_STATE_GREETING) {
+ smtp_server_connection_set_state(
+ conn, SMTP_SERVER_STATE_XCLIENT, NULL);
+ }
+
+ smtp_server_command_ref(command);
+ if (callbacks != NULL && callbacks->conn_cmd_xclient != NULL) {
+ /* specific implementation of XCLIENT command */
+ callbacks->conn_cmd_xclient(conn->context, cmd, proxy_data);
+ }
+ smtp_server_command_unref(&command);
+}
diff --git a/src/lib-smtp/smtp-server-command.c b/src/lib-smtp/smtp-server-command.c
new file mode 100644
index 0000000..992a345
--- /dev/null
+++ b/src/lib-smtp/smtp-server-command.c
@@ -0,0 +1,901 @@
+/* Copyright (c) 2013-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "llist.h"
+#include "array.h"
+
+#include "smtp-reply.h"
+#include "smtp-server-private.h"
+
+/*
+ * Command registry
+ */
+
+void smtp_server_command_register(struct smtp_server *server, const char *name,
+ smtp_server_cmd_start_func_t *func,
+ enum smtp_server_command_flags flags)
+{
+ struct smtp_server_command_reg cmd;
+
+ i_zero(&cmd);
+ cmd.name = name;
+ cmd.func = func;
+ cmd.flags = flags;
+ array_push_back(&server->commands_reg, &cmd);
+
+ server->commands_unsorted = TRUE;
+}
+
+static bool ATTR_NOWARN_UNUSED_RESULT
+smtp_server_command_do_unregister(struct smtp_server *server,
+ const char *name)
+{
+ const struct smtp_server_command_reg *cmd;
+ unsigned int i, count;
+
+ cmd = array_get(&server->commands_reg, &count);
+ for (i = 0; i < count; i++) {
+ if (strcasecmp(cmd[i].name, name) == 0) {
+ array_delete(&server->commands_reg, i, 1);
+ return TRUE;
+ }
+ }
+
+ return FALSE;
+}
+
+void smtp_server_command_unregister(struct smtp_server *server,
+ const char *name)
+{
+ if (smtp_server_command_do_unregister(server, name))
+ return;
+ i_panic("smtp-server: Trying to unregister unknown command '%s'", name);
+}
+
+void smtp_server_command_override(struct smtp_server *server, const char *name,
+ smtp_server_cmd_start_func_t *func,
+ enum smtp_server_command_flags flags)
+{
+ smtp_server_command_do_unregister(server, name);
+ smtp_server_command_register(server, name, func, flags);
+}
+
+static int
+smtp_server_command_cmp(const struct smtp_server_command_reg *c1,
+ const struct smtp_server_command_reg *c2)
+{
+ return strcasecmp(c1->name, c2->name);
+}
+
+static int
+smtp_server_command_bsearch(const char *name,
+ const struct smtp_server_command_reg *cmd)
+{
+ return strcasecmp(name, cmd->name);
+}
+
+static struct smtp_server_command_reg *
+smtp_server_command_find(struct smtp_server *server, const char *name)
+{
+ if (server->commands_unsorted) {
+ array_sort(&server->commands_reg, smtp_server_command_cmp);
+ server->commands_unsorted = FALSE;
+ }
+
+ return array_bsearch(&server->commands_reg,
+ name, smtp_server_command_bsearch);
+}
+
+void smtp_server_commands_init(struct smtp_server *server)
+{
+ p_array_init(&server->commands_reg, server->pool, 16);
+
+ switch (server->set.protocol) {
+ case SMTP_PROTOCOL_SMTP:
+ smtp_server_command_register(
+ server, "EHLO", smtp_server_cmd_ehlo,
+ SMTP_SERVER_CMD_FLAG_PRETLS |
+ SMTP_SERVER_CMD_FLAG_PREAUTH);
+ smtp_server_command_register(
+ server, "HELO", smtp_server_cmd_helo,
+ SMTP_SERVER_CMD_FLAG_PREAUTH);
+ break;
+ case SMTP_PROTOCOL_LMTP:
+ smtp_server_command_register(
+ server, "LHLO", smtp_server_cmd_ehlo,
+ SMTP_SERVER_CMD_FLAG_PRETLS |
+ SMTP_SERVER_CMD_FLAG_PREAUTH);
+ break;
+ }
+
+ smtp_server_command_register(
+ server, "AUTH", smtp_server_cmd_auth,
+ SMTP_SERVER_CMD_FLAG_PREAUTH);
+ smtp_server_command_register(
+ server, "STARTTLS", smtp_server_cmd_starttls,
+ SMTP_SERVER_CMD_FLAG_PRETLS | SMTP_SERVER_CMD_FLAG_PREAUTH);
+ smtp_server_command_register(
+ server, "MAIL", smtp_server_cmd_mail, 0);
+ smtp_server_command_register(server, "RCPT", smtp_server_cmd_rcpt, 0);
+ smtp_server_command_register(server, "DATA", smtp_server_cmd_data, 0);
+ smtp_server_command_register(server, "BDAT", smtp_server_cmd_bdat, 0);
+ smtp_server_command_register(
+ server, "RSET", smtp_server_cmd_rset,
+ SMTP_SERVER_CMD_FLAG_PREAUTH);
+ smtp_server_command_register(server, "VRFY", smtp_server_cmd_vrfy, 0);
+ smtp_server_command_register(
+ server, "NOOP", smtp_server_cmd_noop,
+ SMTP_SERVER_CMD_FLAG_PRETLS | SMTP_SERVER_CMD_FLAG_PREAUTH);
+ smtp_server_command_register(
+ server, "QUIT", smtp_server_cmd_quit,
+ SMTP_SERVER_CMD_FLAG_PRETLS | SMTP_SERVER_CMD_FLAG_PREAUTH);
+
+ smtp_server_command_register(
+ server, "XCLIENT", smtp_server_cmd_xclient,
+ SMTP_SERVER_CMD_FLAG_PREAUTH);
+}
+
+/*
+ *
+ */
+
+static void smtp_server_command_update_event(struct smtp_server_command *cmd)
+{
+ struct event *event = cmd->context.event;
+ const char *label = (cmd->context.name == NULL ?
+ "[unknown]" :
+ t_str_ucase(cmd->context.name));
+
+ if (cmd->reg != NULL)
+ event_add_str(event, "cmd_name", cmd->reg->name);
+ else
+ event_add_str(event, "cmd_name", "unknown");
+ event_add_str(event, "cmd_input_name", cmd->context.name);
+ event_set_append_log_prefix(event,
+ t_strdup_printf("command %s: ", label));
+}
+
+static struct smtp_server_command *
+smtp_server_command_alloc(struct smtp_server_connection *conn)
+{
+ struct smtp_server_command *cmd;
+ pool_t pool;
+
+ pool = pool_alloconly_create("smtp_server_command", 1024);
+ cmd = p_new(pool, struct smtp_server_command, 1);
+ cmd->context.pool = pool;
+ cmd->context.cmd = cmd;
+ cmd->context.event = event_create(conn->event);
+ cmd->refcount = 1;
+ cmd->context.conn = conn;
+ cmd->context.server = conn->server;
+ cmd->replies_expected = 1;
+
+ DLLIST2_APPEND(&conn->command_queue_head,
+ &conn->command_queue_tail, cmd);
+ conn->command_queue_count++;
+
+ return cmd;
+}
+
+struct smtp_server_command *
+smtp_server_command_new_invalid(struct smtp_server_connection *conn)
+{
+ struct smtp_server_command *cmd;
+
+ cmd = smtp_server_command_alloc(conn);
+ smtp_server_command_update_event(cmd);
+
+ e_debug(cmd->context.event, "Invalid command");
+
+ return cmd;
+}
+
+struct smtp_server_command *
+smtp_server_command_new(struct smtp_server_connection *conn,
+ const char *name)
+{
+ struct smtp_server *server = conn->server;
+ struct smtp_server_command *cmd;
+
+ cmd = smtp_server_command_alloc(conn);
+ cmd->context.name = p_strdup(cmd->context.pool, name);
+ cmd->reg = smtp_server_command_find(server, name);
+
+ smtp_server_command_update_event(cmd);
+
+ e_debug(cmd->context.event, "New command");
+
+ return cmd;
+}
+
+void smtp_server_command_execute(struct smtp_server_command *cmd,
+ const char *params)
+{
+ struct smtp_server_connection *conn = cmd->context.conn;
+
+ event_add_str(cmd->context.event, "cmd_args", params);
+ event_add_str(cmd->context.event, "cmd_human_args", params);
+
+ struct event_passthrough *e =
+ event_create_passthrough(cmd->context.event)->
+ set_name("smtp_server_command_started");
+ e_debug(e->event(), "Execute command");
+
+ if (cmd->reg == NULL) {
+ /* RFC 5321, Section 4.2.4: Reply Code 502
+
+ Questions have been raised as to when reply code 502 (Command
+ not implemented) SHOULD be returned in preference to other
+ codes. 502 SHOULD be used when the command is actually
+ recognized by the SMTP server, but not implemented. If the
+ command is not recognized, code 500 SHOULD be returned.
+ */
+ smtp_server_command_fail(cmd,
+ 500, "5.5.1", "Unknown command");
+
+ } else if (!conn->ssl_secured && conn->set.tls_required &&
+ (cmd->reg->flags & SMTP_SERVER_CMD_FLAG_PRETLS) == 0) {
+ /* RFC 3207, Section 4:
+
+ A SMTP server that is not publicly referenced may choose to
+ require that the client perform a TLS negotiation before
+ accepting any commands. In this case, the server SHOULD
+ return the reply code:
+
+ 530 Must issue a STARTTLS command first
+
+ to every command other than NOOP, EHLO, STARTTLS, or QUIT. If
+ the client and server are using the ENHANCEDSTATUSCODES ESMTP
+ extension [RFC2034], the status code to be returned SHOULD be
+ 5.7.0.
+ */
+ smtp_server_command_fail(cmd,
+ 530, "5.7.0", "TLS required.");
+
+ } else if (!conn->authenticated && !conn->set.auth_optional &&
+ (cmd->reg->flags & SMTP_SERVER_CMD_FLAG_PREAUTH) == 0) {
+ /* RFC 4954, Section 6: Status Codes
+
+ 530 5.7.0 Authentication required
+
+ This response SHOULD be returned by any command other than
+ AUTH, EHLO, HELO, NOOP, RSET, or QUIT when server policy
+ requires authentication in order to perform the requested
+ action and authentication is not currently in force.
+ */
+ smtp_server_command_fail(cmd,
+ 530, "5.7.0", "Authentication required.");
+
+ } else {
+ struct smtp_server_command *tmp_cmd = cmd;
+
+ i_assert(cmd->reg->func != NULL);
+ smtp_server_command_ref(tmp_cmd);
+ cmd->reg->func(&tmp_cmd->context, params);
+ if (tmp_cmd->state == SMTP_SERVER_COMMAND_STATE_NEW)
+ tmp_cmd->state = SMTP_SERVER_COMMAND_STATE_PROCESSING;
+ if (!smtp_server_command_unref(&tmp_cmd))
+ cmd = NULL;
+ }
+}
+
+void smtp_server_command_ref(struct smtp_server_command *cmd)
+{
+ if (cmd->destroying)
+ return;
+ cmd->refcount++;
+}
+
+bool smtp_server_command_unref(struct smtp_server_command **_cmd)
+{
+ struct smtp_server_command *cmd = *_cmd;
+ struct smtp_server_connection *conn = cmd->context.conn;
+
+ *_cmd = NULL;
+
+ if (cmd->destroying)
+ return FALSE;
+
+ i_assert(cmd->refcount > 0);
+ if (--cmd->refcount > 0)
+ return TRUE;
+ cmd->destroying = TRUE;
+
+ if (cmd->state >= SMTP_SERVER_COMMAND_STATE_FINISHED) {
+ e_debug(cmd->context.event, "Destroy");
+ } else {
+ struct event_passthrough *e =
+ event_create_passthrough(cmd->context.event)->
+ set_name("smtp_server_command_finished");
+ e->add_int("status_code", 9000);
+ e->add_str("enhanced_code", "9.0.0");
+ e->add_str("error", "Aborted");
+ e_debug(e->event(), "Destroy");
+
+ cmd->state = SMTP_SERVER_COMMAND_STATE_ABORTED;
+ DLLIST2_REMOVE(&conn->command_queue_head,
+ &conn->command_queue_tail, cmd);
+ conn->command_queue_count--;
+ }
+
+ /* Execute hooks */
+ if (!smtp_server_command_call_hooks(
+ &cmd, SMTP_SERVER_COMMAND_HOOK_DESTROY, TRUE))
+ i_unreached();
+
+ smtp_server_command_pipeline_unblock(&cmd->context);
+
+ smtp_server_reply_free(cmd);
+ event_unref(&cmd->context.event);
+ pool_unref(&cmd->context.pool);
+ return FALSE;
+}
+
+void smtp_server_command_abort(struct smtp_server_command **_cmd)
+{
+ struct smtp_server_command *cmd = *_cmd;
+ struct smtp_server_connection *conn = cmd->context.conn;
+
+ /* Preemptively remove command from queue (references may still exist)
+ */
+ if (cmd->state >= SMTP_SERVER_COMMAND_STATE_FINISHED) {
+ e_debug(cmd->context.event, "Abort");
+ } else {
+ struct event_passthrough *e =
+ event_create_passthrough(cmd->context.event)->
+ set_name("smtp_server_command_finished");
+ e->add_int("status_code", 9000);
+ e->add_str("enhanced_code", "9.0.0");
+ e->add_str("error", "Aborted");
+ e_debug(e->event(), "Abort");
+
+ cmd->state = SMTP_SERVER_COMMAND_STATE_ABORTED;
+ DLLIST2_REMOVE(&conn->command_queue_head,
+ &conn->command_queue_tail, cmd);
+ conn->command_queue_count--;
+ }
+ smtp_server_reply_free(cmd);
+
+ smtp_server_command_pipeline_unblock(&cmd->context);
+ smtp_server_command_unref(_cmd);
+}
+
+#undef smtp_server_command_add_hook
+void smtp_server_command_add_hook(struct smtp_server_command *cmd,
+ enum smtp_server_command_hook_type type,
+ smtp_server_cmd_func_t func,
+ void *context)
+{
+ struct smtp_server_command_hook *hook;
+
+ i_assert(func != NULL);
+
+ hook = cmd->hooks_head;
+ while (hook != NULL) {
+ /* No double registrations */
+ i_assert(hook->type != type || hook->func != func);
+
+ hook = hook->next;
+ }
+
+ hook = p_new(cmd->context.pool, struct smtp_server_command_hook, 1);
+ hook->type = type;
+ hook->func = func;
+ hook->context = context;
+
+ DLLIST2_APPEND(&cmd->hooks_head, &cmd->hooks_tail, hook);
+}
+
+#undef smtp_server_command_remove_hook
+void smtp_server_command_remove_hook(struct smtp_server_command *cmd,
+ enum smtp_server_command_hook_type type,
+ smtp_server_cmd_func_t *func)
+{
+ struct smtp_server_command_hook *hook;
+ bool found = FALSE;
+
+ hook = cmd->hooks_head;
+ while (hook != NULL) {
+ struct smtp_server_command_hook *hook_next = hook->next;
+
+ if (hook->type == type && hook->func == func) {
+ DLLIST2_REMOVE(&cmd->hooks_head, &cmd->hooks_tail,
+ hook);
+ found = TRUE;
+ break;
+ }
+
+ hook = hook_next;
+ }
+ i_assert(found);
+}
+
+bool smtp_server_command_call_hooks(struct smtp_server_command **_cmd,
+ enum smtp_server_command_hook_type type,
+ bool remove)
+{
+ struct smtp_server_command *cmd = *_cmd;
+ struct smtp_server_command_hook *hook;
+
+ if (type != SMTP_SERVER_COMMAND_HOOK_DESTROY) {
+ if (cmd->state >= SMTP_SERVER_COMMAND_STATE_FINISHED)
+ return FALSE;
+ smtp_server_command_ref(cmd);
+ }
+
+ hook = cmd->hooks_head;
+ while (hook != NULL) {
+ struct smtp_server_command_hook *hook_next = hook->next;
+
+ if (hook->type == type) {
+ if (remove) {
+ DLLIST2_REMOVE(&cmd->hooks_head,
+ &cmd->hooks_tail, hook);
+ }
+ hook->func(&cmd->context, hook->context);
+ }
+
+ hook = hook_next;
+ }
+
+ if (type != SMTP_SERVER_COMMAND_HOOK_DESTROY) {
+ if (!smtp_server_command_unref(&cmd)) {
+ *_cmd = NULL;
+ return FALSE;
+ }
+ }
+ return TRUE;
+}
+
+void smtp_server_command_remove_hooks(struct smtp_server_command *cmd,
+ enum smtp_server_command_hook_type type)
+{
+ struct smtp_server_command_hook *hook;
+
+ hook = cmd->hooks_head;
+ while (hook != NULL) {
+ struct smtp_server_command_hook *hook_next = hook->next;
+
+ if (hook->type == type) {
+ DLLIST2_REMOVE(&cmd->hooks_head, &cmd->hooks_tail,
+ hook);
+ }
+
+ hook = hook_next;
+ }
+}
+
+void smtp_server_command_set_reply_count(struct smtp_server_command *cmd,
+ unsigned int count)
+{
+ i_assert(count > 0);
+ i_assert(!array_is_created(&cmd->replies));
+ cmd->replies_expected = count;
+}
+
+unsigned int
+smtp_server_command_get_reply_count(struct smtp_server_command *cmd)
+{
+ i_assert(cmd->replies_expected > 0);
+ return cmd->replies_expected;
+}
+
+bool smtp_server_command_next_to_reply(struct smtp_server_command **_cmd)
+{
+ struct smtp_server_command *cmd = *_cmd;
+
+ e_debug(cmd->context.event, "Next to reply");
+
+ if (!smtp_server_command_call_hooks(
+ _cmd, SMTP_SERVER_COMMAND_HOOK_NEXT, TRUE))
+ return FALSE;
+
+ smtp_server_command_remove_hooks(cmd, SMTP_SERVER_COMMAND_HOOK_NEXT);
+ return TRUE;
+}
+
+void smtp_server_command_ready_to_reply(struct smtp_server_command *cmd)
+{
+ cmd->state = SMTP_SERVER_COMMAND_STATE_READY_TO_REPLY;
+ e_debug(cmd->context.event, "Ready to reply");
+ smtp_server_connection_trigger_output(cmd->context.conn);
+}
+
+static bool
+smtp_server_command_replied(struct smtp_server_command **_cmd)
+{
+ struct smtp_server_command *cmd = *_cmd;
+
+ if (cmd->replies_submitted < cmd->replies_expected) {
+ e_debug(cmd->context.event, "Replied (one)");
+
+ return smtp_server_command_call_hooks(
+ _cmd, SMTP_SERVER_COMMAND_HOOK_REPLIED_ONE, FALSE);
+ }
+
+ e_debug(cmd->context.event, "Replied");
+
+ return (smtp_server_command_call_hooks(
+ _cmd, SMTP_SERVER_COMMAND_HOOK_REPLIED_ONE, TRUE) &&
+ smtp_server_command_call_hooks(
+ _cmd, SMTP_SERVER_COMMAND_HOOK_REPLIED, TRUE));
+}
+
+bool smtp_server_command_completed(struct smtp_server_command **_cmd)
+{
+ struct smtp_server_command *cmd = *_cmd;
+
+ if (cmd->replies_submitted < cmd->replies_expected)
+ return TRUE;
+
+ e_debug(cmd->context.event, "Completed");
+
+ if (cmd->pipeline_blocked)
+ smtp_server_command_pipeline_unblock(&cmd->context);
+
+ return smtp_server_command_call_hooks(
+ _cmd, SMTP_SERVER_COMMAND_HOOK_COMPLETED, TRUE);
+}
+
+static bool
+smtp_server_command_handle_reply(struct smtp_server_command *cmd)
+{
+ struct smtp_server_connection *conn = cmd->context.conn;
+
+ smtp_server_connection_ref(conn);
+
+ if (!smtp_server_command_replied(&cmd))
+ return smtp_server_connection_unref(&conn);
+
+ if (cmd->input_locked)
+ smtp_server_command_input_unlock(&cmd->context);
+
+ /* Submit reply */
+ switch (cmd->state) {
+ case SMTP_SERVER_COMMAND_STATE_NEW:
+ case SMTP_SERVER_COMMAND_STATE_PROCESSING:
+ if (!smtp_server_command_is_complete(cmd)) {
+ e_debug(cmd->context.event, "Not ready to reply");
+ cmd->state = SMTP_SERVER_COMMAND_STATE_SUBMITTED_REPLY;
+ break;
+ }
+ smtp_server_command_ready_to_reply(cmd);
+ break;
+ case SMTP_SERVER_COMMAND_STATE_READY_TO_REPLY:
+ case SMTP_SERVER_COMMAND_STATE_ABORTED:
+ break;
+ default:
+ i_unreached();
+ }
+
+ return smtp_server_connection_unref(&conn);
+}
+
+void smtp_server_command_submit_reply(struct smtp_server_command *cmd)
+{
+ struct smtp_server_connection *conn = cmd->context.conn;
+ unsigned int i, submitted;
+ bool is_bad = FALSE;
+
+ i_assert(conn != NULL && array_is_created(&cmd->replies));
+
+ submitted = 0;
+ for (i = 0; i < cmd->replies_expected; i++) {
+ const struct smtp_server_reply *reply =
+ array_idx(&cmd->replies, i);
+ if (!reply->submitted)
+ continue;
+ submitted++;
+
+ i_assert(reply->content != NULL);
+ switch (reply->content->status) {
+ case 500:
+ case 501:
+ case 503:
+ is_bad = TRUE;
+ break;
+ }
+ }
+
+ i_assert(submitted == cmd->replies_submitted);
+
+ /* Limit number of consecutive bad commands */
+ if (is_bad)
+ conn->bad_counter++;
+ else if (cmd->replies_submitted == cmd->replies_expected)
+ conn->bad_counter = 0;
+
+ if (!smtp_server_command_handle_reply(cmd))
+ return;
+
+ if (conn != NULL && conn->bad_counter > conn->set.max_bad_commands) {
+ smtp_server_connection_terminate(&conn,
+ "4.7.0", "Too many invalid commands.");
+ return;
+ }
+}
+
+bool smtp_server_command_is_replied(struct smtp_server_command *cmd)
+{
+ unsigned int i;
+
+ if (!array_is_created(&cmd->replies))
+ return FALSE;
+
+ for (i = 0; i < cmd->replies_expected; i++) {
+ const struct smtp_server_reply *reply =
+ array_idx(&cmd->replies, i);
+ if (!reply->submitted)
+ return FALSE;
+ }
+
+ return TRUE;
+}
+
+bool smtp_server_command_reply_is_forwarded(struct smtp_server_command *cmd)
+{
+ unsigned int i;
+
+ if (!array_is_created(&cmd->replies))
+ return FALSE;
+
+ for (i = 0; i < cmd->replies_expected; i++) {
+ const struct smtp_server_reply *reply =
+ array_idx(&cmd->replies, i);
+ if (!reply->submitted)
+ return FALSE;
+ if (reply->forwarded)
+ return TRUE;
+ }
+
+ return FALSE;
+}
+
+struct smtp_server_reply *
+smtp_server_command_get_reply(struct smtp_server_command *cmd,
+ unsigned int idx)
+{
+ struct smtp_server_reply *reply;
+
+ i_assert(idx < cmd->replies_expected);
+
+ if (!array_is_created(&cmd->replies))
+ return NULL;
+
+ reply = array_idx_get_space(&cmd->replies, idx);
+ if (!reply->submitted)
+ return NULL;
+ return reply;
+}
+
+bool smtp_server_command_reply_status_equals(struct smtp_server_command *cmd,
+ unsigned int status)
+{
+ struct smtp_server_reply *reply;
+
+ i_assert(cmd->replies_expected == 1);
+ reply = smtp_server_command_get_reply(cmd, 0);
+
+ return (reply->content != NULL && reply->content->status == status);
+}
+
+bool smtp_server_command_replied_success(struct smtp_server_command *cmd)
+{
+ bool success = FALSE;
+ unsigned int i;
+
+ if (!array_is_created(&cmd->replies))
+ return FALSE;
+
+ for (i = 0; i < cmd->replies_expected; i++) {
+ const struct smtp_server_reply *reply =
+ array_idx(&cmd->replies, i);
+ if (!reply->submitted)
+ return FALSE;
+ if (smtp_server_reply_is_success(reply))
+ success = TRUE;
+ }
+
+ return success;
+}
+
+static int
+smtp_server_command_send_more_replies(struct smtp_server_command *cmd)
+{
+ unsigned int i;
+ int ret = 1;
+
+ smtp_server_command_ref(cmd);
+
+ // FIXME: handle LMTP DATA command with enormous number of recipients;
+ // i.e. don't keep filling output stream with replies indefinitely.
+ for (i = 0; i < cmd->replies_expected; i++) {
+ struct smtp_server_reply *reply;
+
+ reply = array_idx_modifiable(&cmd->replies, i);
+
+ if (!reply->submitted) {
+ i_assert(!reply->sent);
+ ret = 0;
+ break;
+ }
+ if (smtp_server_reply_send(reply) < 0) {
+ ret = -1;
+ break;
+ }
+ }
+
+ if (!smtp_server_command_unref(&cmd))
+ return -1;
+ return ret;
+}
+
+bool smtp_server_command_send_replies(struct smtp_server_command *cmd)
+{
+ int ret;
+
+ if (!smtp_server_command_next_to_reply(&cmd))
+ return FALSE;
+ if (cmd->state < SMTP_SERVER_COMMAND_STATE_READY_TO_REPLY)
+ return FALSE;
+
+ i_assert(cmd->state == SMTP_SERVER_COMMAND_STATE_READY_TO_REPLY &&
+ array_is_created(&cmd->replies));
+
+ if (!smtp_server_command_completed(&cmd))
+ return TRUE;
+
+ /* Send command replies */
+ ret = smtp_server_command_send_more_replies(cmd);
+ if (ret < 0)
+ return FALSE;
+ if (ret == 0) {
+ cmd->state = SMTP_SERVER_COMMAND_STATE_PROCESSING;
+ return FALSE;
+ }
+
+ smtp_server_command_finished(cmd);
+ return TRUE;
+}
+
+void smtp_server_command_finished(struct smtp_server_command *cmd)
+{
+ struct smtp_server_connection *conn = cmd->context.conn;
+ struct smtp_server_reply *reply;
+
+ i_assert(cmd->state < SMTP_SERVER_COMMAND_STATE_FINISHED);
+ cmd->state = SMTP_SERVER_COMMAND_STATE_FINISHED;
+
+ DLLIST2_REMOVE(&conn->command_queue_head,
+ &conn->command_queue_tail, cmd);
+ conn->command_queue_count--;
+ conn->stats.reply_count++;
+
+ i_assert(array_is_created(&cmd->replies));
+ reply = array_front_modifiable(&cmd->replies);
+ i_assert(reply->content != NULL);
+
+ struct event_passthrough *e =
+ event_create_passthrough(cmd->context.event)->
+ set_name("smtp_server_command_finished");
+ smtp_server_reply_add_to_event(reply, e);
+ e_debug(e->event(), "Finished");
+
+ if (reply->content->status == 221 || reply->content->status == 421) {
+ i_assert(cmd->replies_expected == 1);
+ if (reply->content->status == 421) {
+ smtp_server_connection_close(&conn, t_strdup_printf(
+ "Server closed the connection: %s",
+ smtp_server_reply_get_one_line(reply)));
+
+ } else if (conn->set.auth_optional || conn->authenticated) {
+ smtp_server_connection_close(&conn, "Logged out");
+ } else {
+ smtp_server_connection_close(&conn,
+ "Aborted login by logging out");
+ }
+ smtp_server_command_unref(&cmd);
+ return;
+ }
+ if (cmd->input_locked)
+ smtp_server_command_input_unlock(&cmd->context);
+ if (cmd->pipeline_blocked)
+ smtp_server_command_pipeline_unblock(&cmd->context);
+
+ smtp_server_command_unref(&cmd);
+ smtp_server_connection_trigger_output(conn);
+}
+
+void smtp_server_command_fail(struct smtp_server_command *cmd,
+ unsigned int status, const char *enh_code,
+ const char *fmt, ...)
+{
+ unsigned int i;
+ va_list args;
+
+ i_assert(status / 100 > 2);
+
+ va_start(args, fmt);
+ if (cmd->replies_expected == 1) {
+ smtp_server_reply_indexv(&cmd->context, 0,
+ status, enh_code, fmt, args);
+ } else for (i = 0; i < cmd->replies_expected; i++) {
+ bool sent = FALSE;
+
+ if (array_is_created(&cmd->replies)) {
+ const struct smtp_server_reply *reply =
+ array_idx(&cmd->replies, i);
+ sent = reply->sent;
+ }
+
+ /* Send the same reply for all */
+ if (!sent) {
+ va_list args_copy;
+ VA_COPY(args_copy, args);
+ smtp_server_reply_indexv(&cmd->context, i,
+ status, enh_code, fmt, args_copy);
+ va_end(args_copy);
+ }
+ }
+ va_end(args);
+}
+
+void smtp_server_command_input_lock(struct smtp_server_cmd_ctx *cmd)
+{
+ struct smtp_server_command *command = cmd->cmd;
+ struct smtp_server_connection *conn = cmd->conn;
+
+ command->input_locked = TRUE;
+ smtp_server_connection_input_halt(conn);
+}
+
+void smtp_server_command_input_unlock(struct smtp_server_cmd_ctx *cmd)
+{
+ struct smtp_server_command *command = cmd->cmd;
+ struct smtp_server_connection *conn = cmd->conn;
+
+ command->input_locked = FALSE;
+ if (command->input_captured) {
+ command->input_captured = FALSE;
+ smtp_server_connection_input_halt(conn);
+ }
+ smtp_server_connection_input_resume(conn);
+}
+
+void smtp_server_command_input_capture(
+ struct smtp_server_cmd_ctx *cmd,
+ smtp_server_cmd_input_callback_t *callback)
+{
+ struct smtp_server_command *command = cmd->cmd;
+ struct smtp_server_connection *conn = cmd->conn;
+
+ smtp_server_connection_input_capture(conn, *callback, cmd);
+ command->input_locked = TRUE;
+ command->input_captured = TRUE;
+}
+
+void smtp_server_command_pipeline_block(struct smtp_server_cmd_ctx *cmd)
+{
+ struct smtp_server_command *command = cmd->cmd;
+ struct smtp_server_connection *conn = cmd->conn;
+
+ e_debug(cmd->event, "Pipeline blocked");
+
+ command->pipeline_blocked = TRUE;
+ smtp_server_connection_input_lock(conn);
+}
+
+void smtp_server_command_pipeline_unblock(struct smtp_server_cmd_ctx *cmd)
+{
+ struct smtp_server_command *command = cmd->cmd;
+ struct smtp_server_connection *conn = cmd->conn;
+
+ if (!command->pipeline_blocked)
+ return;
+
+ command->pipeline_blocked = FALSE;
+ smtp_server_connection_input_unlock(conn);
+
+ e_debug(cmd->event, "Pipeline unblocked");
+}
diff --git a/src/lib-smtp/smtp-server-connection.c b/src/lib-smtp/smtp-server-connection.c
new file mode 100644
index 0000000..f301e82
--- /dev/null
+++ b/src/lib-smtp/smtp-server-connection.c
@@ -0,0 +1,1648 @@
+/* Copyright (c) 2013-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "llist.h"
+#include "array.h"
+#include "str.h"
+#include "guid.h"
+#include "base64.h"
+#include "ioloop.h"
+#include "istream.h"
+#include "ostream.h"
+#include "iostream.h"
+#include "connection.h"
+#include "iostream-rawlog.h"
+#include "iostream-ssl.h"
+#include "master-service.h"
+#include "master-service-ssl.h"
+
+#include "smtp-syntax.h"
+#include "smtp-reply-parser.h"
+#include "smtp-command-parser.h"
+#include "smtp-server-private.h"
+
+const char *const smtp_server_state_names[] = {
+ "GREETING",
+ "XCLIENT",
+ "HELO",
+ "STARTTLS",
+ "AUTH",
+ "READY",
+ "MAIL FROM",
+ "RCPT TO",
+ "DATA"
+};
+
+/*
+ * Connection
+ */
+
+static void smtp_server_connection_input(struct connection *_conn);
+static int smtp_server_connection_output(struct smtp_server_connection *conn);
+static void
+smtp_server_connection_disconnect(struct smtp_server_connection *conn,
+ const char *reason) ATTR_NULL(2);
+
+static void
+smtp_server_connection_update_stats(struct smtp_server_connection *conn)
+{
+ if (conn->conn.input != NULL)
+ conn->stats.input = conn->conn.input->v_offset;
+ if (conn->conn.output != NULL)
+ conn->stats.output = conn->conn.output->offset;
+}
+
+const struct smtp_server_stats *
+smtp_server_connection_get_stats(struct smtp_server_connection *conn)
+{
+ smtp_server_connection_update_stats(conn);
+ return &conn->stats;
+}
+
+static bool
+smtp_server_connection_check_pipeline(struct smtp_server_connection *conn)
+{
+ unsigned int pipeline = conn->command_queue_count;
+
+ if (conn->command_queue_tail != NULL) {
+ i_assert(pipeline > 0);
+ if (conn->command_queue_tail->state ==
+ SMTP_SERVER_COMMAND_STATE_SUBMITTED_REPLY)
+ pipeline--;
+ }
+
+ if (pipeline >= conn->set.max_pipelined_commands) {
+ e_debug(conn->event, "Command pipeline is full "
+ "(pipelined commands %u > limit %u)",
+ pipeline, conn->set.max_pipelined_commands);
+ return FALSE;
+ }
+ return TRUE;
+}
+
+void smtp_server_connection_input_halt(struct smtp_server_connection *conn)
+{
+ connection_input_halt(&conn->conn);
+}
+
+void smtp_server_connection_input_resume(struct smtp_server_connection *conn)
+{
+ struct smtp_server_command *cmd;
+ bool cmd_locked = FALSE;
+
+ if (conn->conn.io == NULL) {
+ /* Only resume when we actually can */
+ if (conn->input_locked || conn->input_broken ||
+ conn->disconnected)
+ return;
+ if (!smtp_server_connection_check_pipeline(conn))
+ return;
+
+ /* Is queued command still blocking input? */
+ cmd = conn->command_queue_head;
+ while (cmd != NULL) {
+ if (cmd->input_locked || cmd->pipeline_blocked) {
+ cmd_locked = TRUE;
+ break;
+ }
+ cmd = cmd->next;
+ }
+ if (cmd_locked)
+ return;
+
+ /* Restore input handler */
+ connection_input_resume(&conn->conn);
+ }
+
+ if (conn->conn.io != NULL &&
+ i_stream_have_bytes_left(conn->conn.input)) {
+ io_set_pending(conn->conn.io);
+ }
+}
+
+void smtp_server_connection_input_lock(struct smtp_server_connection *conn)
+{
+ conn->input_locked = TRUE;
+ smtp_server_connection_input_halt(conn);
+}
+
+void smtp_server_connection_input_unlock(struct smtp_server_connection *conn)
+{
+ conn->input_locked = FALSE;
+ smtp_server_connection_input_resume(conn);
+}
+
+#undef smtp_server_connection_input_capture
+void smtp_server_connection_input_capture(struct smtp_server_connection *conn,
+ smtp_server_input_callback_t *callback, void *context)
+{
+ i_assert(!conn->input_broken && !conn->disconnected);
+ connection_input_halt(&conn->conn);
+ conn->conn.io = io_add_istream(conn->conn.input, *callback, context);
+}
+
+static void
+smtp_server_connection_update_rawlog(struct smtp_server_connection *conn)
+{
+ struct stat st;
+
+ if (conn->set.rawlog_dir == NULL)
+ return;
+
+ if (!conn->rawlog_checked) {
+ conn->rawlog_checked = TRUE;
+ if (stat(conn->set.rawlog_dir, &st) == 0)
+ conn->rawlog_enabled = TRUE;
+ }
+ if (conn->rawlog_enabled) {
+ iostream_rawlog_create(conn->set.rawlog_dir,
+ &conn->conn.input, &conn->conn.output);
+ }
+}
+
+static void
+smtp_server_connection_streams_changed(struct smtp_server_connection *conn)
+{
+ smtp_server_connection_update_rawlog(conn);
+ smtp_command_parser_set_stream(conn->smtp_parser, conn->conn.input);
+
+ o_stream_set_flush_callback(conn->conn.output,
+ smtp_server_connection_output, conn);
+ o_stream_set_flush_pending(conn->conn.output, TRUE);
+}
+
+void smtp_server_connection_set_streams(struct smtp_server_connection *conn,
+ struct istream *input,
+ struct ostream *output)
+{
+ struct istream *old_input = conn->conn.input;
+ struct ostream *old_output = conn->conn.output;
+
+ i_assert(conn->created_from_streams);
+
+ conn->conn.input = input;
+ i_stream_ref(conn->conn.input);
+
+ conn->conn.output = output;
+ o_stream_ref(conn->conn.output);
+ o_stream_set_no_error_handling(conn->conn.output, TRUE);
+
+ i_stream_unref(&old_input);
+ o_stream_unref(&old_output);
+
+ smtp_server_connection_streams_changed(conn);
+}
+
+void smtp_server_connection_set_ssl_streams(struct smtp_server_connection *conn,
+ struct istream *input,
+ struct ostream *output)
+{
+ conn->ssl_secured = TRUE;
+ conn->set.capabilities &= ENUM_NEGATE(SMTP_CAPABILITY_STARTTLS);
+
+ smtp_server_connection_set_streams(conn, input, output);
+}
+
+static void
+smtp_server_connection_idle_timeout(struct smtp_server_connection *conn)
+{
+ smtp_server_connection_terminate(
+ &conn, "4.4.2", "Disconnected for inactivity");
+}
+
+void smtp_server_connection_timeout_stop(struct smtp_server_connection *conn)
+{
+ if (conn->to_idle != NULL) {
+ e_debug(conn->event, "Timeout stop");
+
+ timeout_remove(&conn->to_idle);
+ }
+}
+
+void smtp_server_connection_timeout_start(struct smtp_server_connection *conn)
+{
+ if (conn->disconnected)
+ return;
+
+ if (conn->to_idle == NULL &&
+ conn->set.max_client_idle_time_msecs > 0) {
+ e_debug(conn->event, "Timeout start");
+
+ conn->to_idle = timeout_add(
+ conn->set.max_client_idle_time_msecs,
+ smtp_server_connection_idle_timeout, conn);
+ }
+}
+
+void smtp_server_connection_timeout_reset(struct smtp_server_connection *conn)
+{
+ if (conn->to_idle != NULL)
+ timeout_reset(conn->to_idle);
+}
+
+static void
+smtp_server_connection_timeout_update(struct smtp_server_connection *conn)
+{
+ struct smtp_server_command *cmd = conn->command_queue_head;
+
+ if (cmd == NULL) {
+ smtp_server_connection_timeout_start(conn);
+ return;
+ }
+
+ switch (cmd->state) {
+ case SMTP_SERVER_COMMAND_STATE_NEW:
+ smtp_server_connection_timeout_start(conn);
+ break;
+ case SMTP_SERVER_COMMAND_STATE_PROCESSING:
+ if (cmd->input_captured) {
+ /* Command updates timeout internally */
+ return;
+ }
+ smtp_server_connection_timeout_stop(conn);
+ break;
+ case SMTP_SERVER_COMMAND_STATE_SUBMITTED_REPLY:
+ case SMTP_SERVER_COMMAND_STATE_READY_TO_REPLY:
+ smtp_server_connection_timeout_stop(conn);
+ break;
+ case SMTP_SERVER_COMMAND_STATE_FINISHED:
+ case SMTP_SERVER_COMMAND_STATE_ABORTED:
+ i_unreached();
+ }
+}
+
+static void smtp_server_connection_ready(struct smtp_server_connection *conn)
+{
+ conn->raw_input = conn->conn.input;
+ conn->raw_output = conn->conn.output;
+
+ smtp_server_connection_update_rawlog(conn);
+
+ conn->smtp_parser = smtp_command_parser_init(conn->conn.input,
+ &conn->set.command_limits);
+ o_stream_set_flush_callback(conn->conn.output,
+ smtp_server_connection_output, conn);
+
+ o_stream_cork(conn->conn.output);
+ if (conn->set.no_greeting) {
+ /* Don't send greeting or login reply. */
+ } else if (conn->authenticated) {
+ /* RFC 4954, Section 4:
+ Should the client successfully complete the exchange, the
+ SMTP server issues a 235 reply. */
+ smtp_server_connection_send_line(
+ conn, "235 2.7.0 Logged in.");
+ } else {
+ smtp_server_connection_send_line(
+ conn, "220 %s %s", conn->set.hostname,
+ conn->set.login_greeting);
+ }
+ if (!conn->corked)
+ o_stream_uncork(conn->conn.output);
+}
+
+static void smtp_server_connection_destroy(struct connection *_conn)
+{
+ struct smtp_server_connection *conn =
+ (struct smtp_server_connection *)_conn;
+
+ smtp_server_connection_disconnect(conn, NULL);
+ smtp_server_connection_unref(&conn);
+}
+
+static bool
+smtp_server_connection_handle_command(struct smtp_server_connection *conn,
+ const char *cmd_name, const char *cmd_params)
+{
+ struct smtp_server_connection *tmp_conn = conn;
+ struct smtp_server_command *cmd;
+ bool finished;
+
+ cmd = smtp_server_command_new(tmp_conn, cmd_name);
+
+ smtp_server_command_ref(cmd);
+
+ smtp_server_connection_ref(tmp_conn);
+ smtp_server_command_execute(cmd, cmd_params);
+ if (!smtp_server_connection_unref(&tmp_conn)) {
+ /* The command start callback managed to get this connection
+ destroyed */
+ smtp_server_command_unref(&cmd);
+ return FALSE;
+ }
+
+ if (conn->command_queue_head == cmd)
+ (void)smtp_server_command_next_to_reply(&cmd);
+
+ smtp_server_connection_timeout_update(conn);
+
+ finished = !cmd->input_locked;
+ return (!smtp_server_command_unref(&cmd) || finished);
+}
+
+static int
+smtp_server_connection_init_ssl_ctx(struct smtp_server_connection *conn,
+ const char **error_r)
+{
+ struct smtp_server *server = conn->server;
+ const char *error;
+
+ if (conn->ssl_ctx != NULL || conn->set.ssl == NULL)
+ return 0;
+ if (conn->set.ssl == server->set.ssl) {
+ if (smtp_server_init_ssl_ctx(server, error_r) < 0)
+ return -1;
+ conn->ssl_ctx = server->ssl_ctx;
+ ssl_iostream_context_ref(conn->ssl_ctx);
+ return 0;
+ }
+
+ if (ssl_iostream_server_context_cache_get(conn->set.ssl, &conn->ssl_ctx,
+ &error) < 0) {
+ *error_r = t_strdup_printf(
+ "Couldn't initialize SSL context: %s", error);
+ return -1;
+ }
+ return 0;
+}
+
+int smtp_server_connection_ssl_init(struct smtp_server_connection *conn)
+{
+ const char *error;
+ int ret;
+
+ if (smtp_server_connection_init_ssl_ctx(conn, &error) < 0) {
+ e_error(conn->event, "Couldn't initialize SSL: %s", error);
+ return -1;
+ }
+
+ e_debug(conn->event, "Starting SSL handshake");
+
+ if (conn->raw_input != conn->conn.input) {
+ /* Recreate rawlog after STARTTLS */
+ i_stream_ref(conn->raw_input);
+ o_stream_ref(conn->raw_output);
+ i_stream_destroy(&conn->conn.input);
+ o_stream_destroy(&conn->conn.output);
+ conn->conn.input = conn->raw_input;
+ conn->conn.output = conn->raw_output;
+ }
+
+ smtp_server_connection_input_halt(conn);
+ if (conn->ssl_ctx == NULL) {
+ ret = master_service_ssl_init(
+ master_service, &conn->conn.input, &conn->conn.output,
+ &conn->ssl_iostream, &error);
+ } else {
+ ret = io_stream_create_ssl_server(
+ conn->ssl_ctx, conn->set.ssl,
+ &conn->conn.input, &conn->conn.output,
+ &conn->ssl_iostream, &error);
+ }
+ if (ret < 0) {
+ e_error(conn->event,
+ "Couldn't initialize SSL server for %s: %s",
+ conn->conn.name, error);
+ return -1;
+ }
+ smtp_server_connection_input_resume(conn);
+
+ conn->ssl_secured = TRUE;
+ conn->set.capabilities &= ENUM_NEGATE(SMTP_CAPABILITY_STARTTLS);
+
+ if (conn->ssl_start)
+ smtp_server_connection_ready(conn);
+ else
+ smtp_server_connection_streams_changed(conn);
+ return 0;
+}
+
+static void
+smtp_server_connection_handle_input(struct smtp_server_connection *conn)
+{
+ struct smtp_server_command *pending_command;
+ enum smtp_command_parse_error error_code;
+ const char *cmd_name, *cmd_params, *error;
+ int ret;
+
+ /* Check whether we are continuing a command */
+ pending_command = NULL;
+ if (conn->command_queue_tail != NULL) {
+ pending_command =
+ ((conn->command_queue_tail->state ==
+ SMTP_SERVER_COMMAND_STATE_SUBMITTED_REPLY) ?
+ conn->command_queue_tail : NULL);
+ }
+
+ smtp_server_connection_timeout_reset(conn);
+
+ /* Parse commands */
+ ret = 1;
+ while (!conn->closing && !conn->input_locked && ret != 0) {
+ while ((ret = smtp_command_parse_next(
+ conn->smtp_parser, &cmd_name, &cmd_params,
+ &error_code, &error)) > 0) {
+
+ if (pending_command != NULL) {
+ /* Previous command is now fully read and ready
+ to reply */
+ smtp_server_command_ready_to_reply(pending_command);
+ pending_command = NULL;
+ }
+
+ e_debug(conn->event, "Received new command: %s %s",
+ cmd_name, cmd_params);
+
+ conn->stats.command_count++;
+
+ /* Handle command (cmd may be destroyed after this) */
+ if (!smtp_server_connection_handle_command(conn,
+ cmd_name, cmd_params))
+ return;
+
+ if (conn->disconnected)
+ return;
+ /* Last command locked the input; stop trying to read
+ more. */
+ if (conn->input_locked)
+ break;
+ /* Client indicated it will close after this command;
+ stop trying to read more. */
+ if (conn->closing)
+ break;
+
+ if (!smtp_server_connection_check_pipeline(conn)) {
+ smtp_server_connection_input_halt(conn);
+ return;
+ }
+
+ if (conn->command_queue_tail != NULL) {
+ pending_command =
+ ((conn->command_queue_tail->state ==
+ SMTP_SERVER_COMMAND_STATE_SUBMITTED_REPLY) ?
+ conn->command_queue_tail : NULL);
+ }
+ }
+
+ if (ret < 0 && conn->conn.input->eof) {
+ const char *error =
+ i_stream_get_disconnect_reason(conn->conn.input);
+ e_debug(conn->event, "Remote closed connection: %s",
+ error);
+
+ if (conn->command_queue_head == NULL ||
+ conn->command_queue_head->state <
+ SMTP_SERVER_COMMAND_STATE_SUBMITTED_REPLY) {
+ /* No pending commands or unfinished
+ command; close */
+ smtp_server_connection_close(&conn, error);
+ } else {
+ /* A command is still processing;
+ only drop input io for now */
+ conn->input_broken = TRUE;
+ smtp_server_connection_input_halt(conn);
+ }
+ return;
+ }
+
+ if (ret < 0) {
+ struct smtp_server_command *cmd;
+
+ e_debug(conn->event,
+ "Client sent invalid command: %s", error);
+
+ switch (error_code) {
+ case SMTP_COMMAND_PARSE_ERROR_BROKEN_COMMAND:
+ conn->input_broken = TRUE;
+ /* fall through */
+ case SMTP_COMMAND_PARSE_ERROR_BAD_COMMAND:
+ cmd = smtp_server_command_new_invalid(conn);
+ smtp_server_command_fail(
+ cmd, 500, "5.5.2",
+ "Invalid command syntax");
+ break;
+ case SMTP_COMMAND_PARSE_ERROR_LINE_TOO_LONG:
+ cmd = smtp_server_command_new_invalid(conn);
+ smtp_server_command_fail(
+ cmd, 500, "5.5.2", "Line too long");
+ break;
+ case SMTP_COMMAND_PARSE_ERROR_DATA_TOO_LARGE:
+ /* Command data size exceeds the absolute limit;
+ i.e. beyond which we don't even want to skip
+ data anymore. The command error is usually
+ already submitted by the application and sent
+ to the client. */
+ smtp_server_connection_close(&conn,
+ "Command data size exceeds absolute limit");
+ return;
+ case SMTP_COMMAND_PARSE_ERROR_BROKEN_STREAM:
+ smtp_server_connection_close(&conn, error);
+ return;
+ default:
+ i_unreached();
+ }
+ }
+
+ if (conn->disconnected)
+ return;
+ if (conn->input_broken || conn->closing) {
+ smtp_server_connection_input_halt(conn);
+ return;
+ }
+
+ if (ret == 0 && pending_command != NULL &&
+ !smtp_command_parser_pending_data(conn->smtp_parser)) {
+ /* Previous command is now fully read and ready to
+ reply */
+ smtp_server_command_ready_to_reply(pending_command);
+ }
+ }
+}
+
+static void smtp_server_connection_input(struct connection *_conn)
+{
+ struct smtp_server_connection *conn =
+ (struct smtp_server_connection *)_conn;
+
+ i_assert(!conn->input_broken);
+
+ if (conn->handling_input)
+ return;
+
+ smtp_server_connection_timeout_reset(conn);
+
+ if (conn->ssl_start && conn->ssl_iostream == NULL) {
+ if (smtp_server_connection_ssl_init(conn) < 0) {
+ smtp_server_connection_close(&conn,
+ "SSL Initialization failed");
+ return;
+ }
+ if (conn->halted) {
+ smtp_server_connection_input_lock(conn);
+ return;
+ }
+ }
+ i_assert(!conn->halted);
+
+
+ if (!smtp_server_connection_check_pipeline(conn)) {
+ smtp_server_connection_input_halt(conn);
+ return;
+ }
+
+ smtp_server_connection_ref(conn);
+ conn->handling_input = TRUE;
+ if (conn->callbacks != NULL &&
+ conn->callbacks->conn_cmd_input_pre != NULL)
+ conn->callbacks->conn_cmd_input_pre(conn->context);
+ smtp_server_connection_handle_input(conn);
+ if (conn->callbacks != NULL &&
+ conn->callbacks->conn_cmd_input_post != NULL)
+ conn->callbacks->conn_cmd_input_post(conn->context);
+ conn->handling_input = FALSE;
+ smtp_server_connection_unref(&conn);
+}
+
+bool smtp_server_connection_pending_command_data(
+ struct smtp_server_connection *conn)
+{
+ if (conn->smtp_parser == NULL)
+ return FALSE;
+ return smtp_command_parser_pending_data(conn->smtp_parser);
+}
+
+/*
+ * Command reply handling
+ */
+
+void smtp_server_connection_handle_output_error(
+ struct smtp_server_connection *conn)
+{
+ smtp_server_connection_close(&conn,
+ o_stream_get_disconnect_reason(conn->conn.output));
+}
+
+static bool
+smtp_server_connection_next_reply(struct smtp_server_connection *conn)
+{
+ struct smtp_server_command *cmd;
+
+ cmd = conn->command_queue_head;
+ if (cmd == NULL) {
+ /* No commands pending */
+ e_debug(conn->event, "No more commands pending");
+ return FALSE;
+ }
+
+ return smtp_server_command_send_replies(cmd);
+}
+
+void smtp_server_connection_cork(struct smtp_server_connection *conn)
+{
+ conn->corked = TRUE;
+ if (conn->conn.output != NULL)
+ o_stream_cork(conn->conn.output);
+}
+
+void smtp_server_connection_uncork(struct smtp_server_connection *conn)
+{
+ conn->corked = FALSE;
+ if (conn->conn.output != NULL) {
+ if (o_stream_uncork_flush(conn->conn.output) < 0) {
+ smtp_server_connection_handle_output_error(conn);
+ return;
+ }
+ smtp_server_connection_trigger_output(conn);
+ }
+}
+
+static void
+smtp_server_connection_send_replies(struct smtp_server_connection *conn)
+{
+ /* Send more replies until no more replies remain, the output
+ blocks again, or the connection is closed */
+ while (!conn->disconnected && smtp_server_connection_next_reply(conn));
+
+ smtp_server_connection_timeout_update(conn);
+
+ /* Accept more commands if possible */
+ smtp_server_connection_input_resume(conn);
+}
+
+int smtp_server_connection_flush(struct smtp_server_connection *conn)
+{
+ struct ostream *output = conn->conn.output;
+ int ret;
+
+ if ((ret = o_stream_flush(output)) <= 0) {
+ if (ret < 0)
+ smtp_server_connection_handle_output_error(conn);
+ return ret;
+ }
+ return 1;
+}
+
+static int smtp_server_connection_output(struct smtp_server_connection *conn)
+{
+ int ret;
+
+ e_debug(conn->event, "Sending replies");
+
+ smtp_server_connection_ref(conn);
+ o_stream_cork(conn->conn.output);
+ ret = smtp_server_connection_flush(conn);
+ if (ret > 0) {
+ smtp_server_connection_timeout_reset(conn);
+ smtp_server_connection_send_replies(conn);
+ }
+ if (ret >= 0 && !conn->corked && conn->conn.output != NULL)
+ ret = o_stream_uncork_flush(conn->conn.output);
+ if (conn->conn.output != NULL && conn->conn.output->closed) {
+ smtp_server_connection_handle_output_error(conn);
+ ret = -1;
+ }
+ smtp_server_connection_unref(&conn);
+ return ret;
+}
+
+void smtp_server_connection_trigger_output(struct smtp_server_connection *conn)
+{
+ if (conn->conn.output != NULL) {
+ e_debug(conn->event, "Trigger output");
+ o_stream_set_flush_pending(conn->conn.output, TRUE);
+ }
+}
+
+/*
+ *
+ */
+
+static struct connection_settings smtp_server_connection_set = {
+ .input_max_size = SIZE_MAX,
+ .output_max_size = SIZE_MAX,
+ .client = FALSE,
+ .log_connection_id = TRUE,
+};
+
+static const struct connection_vfuncs smtp_server_connection_vfuncs = {
+ .destroy = smtp_server_connection_destroy,
+ .input = smtp_server_connection_input,
+};
+
+struct connection_list *smtp_server_connection_list_init(void)
+{
+ return connection_list_init(&smtp_server_connection_set,
+ &smtp_server_connection_vfuncs);
+}
+
+static struct event *
+smtp_server_connection_event_create(struct smtp_server *server,
+ const struct smtp_server_settings *set)
+{
+ struct event *conn_event;
+
+ if (set != NULL && set->event_parent != NULL) {
+ conn_event = event_create(set->event_parent);
+ smtp_server_event_init(server, conn_event);
+ } else
+ conn_event = event_create(server->event);
+ event_set_append_log_prefix(conn_event, t_strdup_printf(
+ "%s-server: ", smtp_protocol_name(server->set.protocol)));
+ event_set_forced_debug(conn_event, (set != NULL && set->debug));
+
+ return conn_event;
+}
+
+static void
+smtp_server_connection_update_event(struct smtp_server_connection *conn)
+{
+ event_add_str(conn->event, "connection_id", conn->session_id);
+ event_add_str(conn->event, "session", conn->session_id);
+}
+
+static void
+smtp_server_connection_init_session(struct smtp_server_connection *conn)
+{
+ guid_128_t guid;
+ string_t *session_id;
+
+ session_id = t_str_new(30);
+ guid_128_generate(guid);
+ base64_encode(guid, sizeof(guid), session_id);
+
+ /* drop trailing "==" */
+ i_assert(str_c(session_id)[str_len(session_id)-2] == '=');
+ str_truncate(session_id, str_len(session_id)-2);
+
+ conn->session_id = i_strdup(str_c(session_id));
+}
+
+static struct smtp_server_connection * ATTR_NULL(5, 6)
+smtp_server_connection_alloc(struct smtp_server *server,
+ const struct smtp_server_settings *set,
+ int fd_in, int fd_out,
+ const struct smtp_server_callbacks *callbacks,
+ void *context)
+{
+ struct smtp_server_connection *conn;
+ pool_t pool;
+
+ pool = pool_alloconly_create("smtp server", 1024);
+ conn = p_new(pool, struct smtp_server_connection, 1);
+ conn->pool = pool;
+ conn->refcount = 1;
+ conn->server = server;
+ conn->callbacks = callbacks;
+ conn->context = context;
+
+ /* Merge settings with global server settings */
+ conn->set = server->set;
+ if (set != NULL) {
+ conn->set.protocol = server->set.protocol;
+ if (set->rawlog_dir != NULL && *set->rawlog_dir != '\0')
+ conn->set.rawlog_dir = p_strdup(pool, set->rawlog_dir);
+
+ if (set->ssl != NULL)
+ conn->set.ssl = ssl_iostream_settings_dup(pool, set->ssl);
+
+ if (set->hostname != NULL && *set->hostname != '\0')
+ conn->set.hostname = p_strdup(pool, set->hostname);
+ if (set->login_greeting != NULL &&
+ *set->login_greeting != '\0') {
+ conn->set.login_greeting =
+ p_strdup(pool, set->login_greeting);
+ }
+ if (set->capabilities != 0)
+ conn->set.capabilities = set->capabilities;
+ conn->set.workarounds |= set->workarounds;
+
+ if (set->max_client_idle_time_msecs > 0) {
+ conn->set.max_client_idle_time_msecs =
+ set->max_client_idle_time_msecs;
+ }
+ if (set->max_pipelined_commands > 0) {
+ conn->set.max_pipelined_commands =
+ set->max_pipelined_commands;
+ }
+ if (set->max_bad_commands > 0) {
+ conn->set.max_bad_commands = set->max_bad_commands;
+ }
+ if (set->max_recipients > 0)
+ conn->set.max_recipients = set->max_recipients;
+ smtp_command_limits_merge(&conn->set.command_limits,
+ &set->command_limits);
+
+ conn->set.max_message_size = set->max_message_size;
+ if (set->max_message_size == 0 ||
+ set->max_message_size == UOFF_T_MAX) {
+ conn->set.command_limits.max_data_size = UOFF_T_MAX;
+ } else if (conn->set.command_limits.max_data_size != 0) {
+ /* Explicit limit given */
+ } else if (set->max_message_size >
+ (UOFF_T_MAX - SMTP_SERVER_DEFAULT_MAX_SIZE_EXCESS_LIMIT)) {
+ /* Very high limit */
+ conn->set.command_limits.max_data_size = UOFF_T_MAX;
+ } else {
+ /* Absolute maximum before connection is closed in DATA
+ command */
+ conn->set.command_limits.max_data_size =
+ set->max_message_size +
+ SMTP_SERVER_DEFAULT_MAX_SIZE_EXCESS_LIMIT;
+ }
+
+ if (set->mail_param_extensions != NULL) {
+ conn->set.mail_param_extensions =
+ p_strarray_dup(pool, set->mail_param_extensions);
+ }
+ if (set->rcpt_param_extensions != NULL) {
+ conn->set.rcpt_param_extensions =
+ p_strarray_dup(pool, set->rcpt_param_extensions);
+ }
+ if (set->xclient_extensions != NULL) {
+ conn->set.xclient_extensions =
+ p_strarray_dup(pool, set->xclient_extensions);
+ }
+
+ if (set->socket_send_buffer_size > 0) {
+ conn->set.socket_send_buffer_size =
+ set->socket_send_buffer_size;
+ }
+ if (set->socket_recv_buffer_size > 0) {
+ conn->set.socket_recv_buffer_size =
+ set->socket_recv_buffer_size;
+ }
+
+ conn->set.tls_required =
+ conn->set.tls_required || set->tls_required;
+ conn->set.auth_optional =
+ conn->set.auth_optional || set->auth_optional;
+ conn->set.mail_path_allow_broken =
+ conn->set.mail_path_allow_broken ||
+ set->mail_path_allow_broken;
+ conn->set.rcpt_domain_optional =
+ conn->set.rcpt_domain_optional ||
+ set->rcpt_domain_optional;
+ conn->set.no_greeting =
+ conn->set.no_greeting || set->no_greeting;
+ conn->set.debug = conn->set.debug || set->debug;
+ }
+
+ if (set != NULL && set->mail_param_extensions != NULL) {
+ const char *const *extp;
+
+ p_array_init(&conn->mail_param_extensions, pool,
+ str_array_length(set->mail_param_extensions) + 8);
+ for (extp = set->mail_param_extensions; *extp != NULL; extp++) {
+ const char *ext = p_strdup(pool, *extp);
+ array_push_back(&conn->mail_param_extensions, &ext);
+ }
+ array_append_zero(&conn->mail_param_extensions);
+ }
+ if (set != NULL && set->rcpt_param_extensions != NULL) {
+ const char *const *extp;
+
+ p_array_init(&conn->rcpt_param_extensions, pool,
+ str_array_length(set->rcpt_param_extensions) + 8);
+ for (extp = set->rcpt_param_extensions; *extp != NULL; extp++) {
+ const char *ext = p_strdup(pool, *extp);
+ array_push_back(&conn->rcpt_param_extensions, &ext);
+ }
+ array_append_zero(&conn->rcpt_param_extensions);
+ }
+
+ net_set_nonblock(fd_in, TRUE);
+ if (fd_in != fd_out)
+ net_set_nonblock(fd_out, TRUE);
+ (void)net_set_tcp_nodelay(fd_out, TRUE);
+
+ set = &conn->set;
+ if (set->socket_send_buffer_size > 0 &&
+ net_set_send_buffer_size(fd_out,
+ set->socket_send_buffer_size) < 0) {
+ e_error(conn->event,
+ "net_set_send_buffer_size(%zu) failed: %m",
+ set->socket_send_buffer_size);
+ }
+ if (set->socket_recv_buffer_size > 0 &&
+ net_set_recv_buffer_size(fd_in,
+ set->socket_recv_buffer_size) < 0) {
+ e_error(conn->event,
+ "net_set_recv_buffer_size(%zu) failed: %m",
+ set->socket_recv_buffer_size);
+ }
+
+ smtp_server_connection_init_session(conn);
+
+ return conn;
+}
+
+struct smtp_server_connection *
+smtp_server_connection_create(
+ struct smtp_server *server, int fd_in, int fd_out,
+ const struct ip_addr *remote_ip, in_port_t remote_port,
+ bool ssl_start, const struct smtp_server_settings *set,
+ const struct smtp_server_callbacks *callbacks, void *context)
+{
+ struct smtp_server_connection *conn;
+ struct event *conn_event;
+
+ conn = smtp_server_connection_alloc(server, set, fd_in, fd_out,
+ callbacks, context);
+ conn_event = smtp_server_connection_event_create(server, set);
+ conn->conn.event_parent = conn_event;
+ connection_init_server_ip(server->conn_list, &conn->conn, NULL,
+ fd_in, fd_out, remote_ip, remote_port);
+ conn->event = conn->conn.event;
+ smtp_server_connection_update_event(conn);
+ event_unref(&conn_event);
+
+ conn->ssl_start = ssl_start;
+ if (ssl_start)
+ conn->set.capabilities &= ENUM_NEGATE(SMTP_CAPABILITY_STARTTLS);
+
+ /* Halt input until started */
+ smtp_server_connection_halt(conn);
+
+ e_debug(conn->event, "Connection created");
+
+ return conn;
+}
+
+struct smtp_server_connection *
+smtp_server_connection_create_from_streams(
+ struct smtp_server *server,
+ struct istream *input, struct ostream *output,
+ const struct ip_addr *remote_ip, in_port_t remote_port,
+ const struct smtp_server_settings *set,
+ const struct smtp_server_callbacks *callbacks, void *context)
+{
+ struct smtp_server_connection *conn;
+ struct event *conn_event;
+ int fd_in, fd_out;
+
+ fd_in = i_stream_get_fd(input);
+ fd_out = o_stream_get_fd(output);
+ i_assert(fd_in >= 0);
+ i_assert(fd_out >= 0);
+
+ conn = smtp_server_connection_alloc(server, set, fd_in, fd_out,
+ callbacks, context);
+ if (remote_ip != NULL && remote_ip->family != 0)
+ conn->conn.remote_ip = *remote_ip;
+ if (remote_port != 0)
+ conn->conn.remote_port = remote_port;
+ conn_event = smtp_server_connection_event_create(server, set);
+ conn->conn.event_parent = conn_event;
+ connection_init_from_streams(server->conn_list, &conn->conn, NULL,
+ input, output);
+ conn->created_from_streams = TRUE;
+ conn->event = conn->conn.event;
+ smtp_server_connection_update_event(conn);
+ event_unref(&conn_event);
+
+ /* Halt input until started */
+ smtp_server_connection_halt(conn);
+
+ e_debug(conn->event, "Connection created");
+
+ return conn;
+}
+
+void smtp_server_connection_ref(struct smtp_server_connection *conn)
+{
+ conn->refcount++;
+}
+
+static const char *
+smtp_server_connection_get_disconnect_reason(
+ struct smtp_server_connection *conn)
+{
+ const char *err;
+
+ if (conn->ssl_iostream != NULL &&
+ !ssl_iostream_is_handshaked(conn->ssl_iostream)) {
+ err = ssl_iostream_get_last_error(conn->ssl_iostream);
+ if (err != NULL) {
+ return t_strdup_printf(
+ "TLS handshaking failed: %s", err);
+ }
+ }
+
+ return io_stream_get_disconnect_reason(conn->conn.input,
+ conn->conn.output);
+}
+
+static void
+smtp_server_connection_disconnect(struct smtp_server_connection *conn,
+ const char *reason)
+{
+ struct smtp_server_command *cmd, *cmd_next;
+
+ if (conn->disconnected)
+ return;
+ conn->disconnected = TRUE;
+
+ if (reason == NULL)
+ reason = smtp_server_connection_get_disconnect_reason(conn);
+ else
+ reason = t_str_oneline(reason);
+
+ cmd = conn->command_queue_head;
+ if (cmd != NULL && cmd->reg != NULL) {
+ /* Unfinished command - include it in the reason string */
+ reason = t_strdup_printf("%s (unfinished %s command)",
+ reason, cmd->reg->name);
+ }
+ if (!conn->set.no_state_in_reason) {
+ reason = t_strdup_printf("%s (state=%s)", reason,
+ smtp_server_state_names[conn->state.state]);
+ }
+
+ e_debug(conn->event, "Disconnected: %s", reason);
+
+ /* Preserve statistics */
+ smtp_server_connection_update_stats(conn);
+
+ /* Drop transaction */
+ smtp_server_connection_reset_state(conn);
+
+ /* Clear command queue */
+ cmd = conn->command_queue_head;
+ while (cmd != NULL) {
+ cmd_next = cmd->next;
+ smtp_server_command_abort(&cmd);
+ cmd = cmd_next;
+ }
+
+ smtp_server_connection_timeout_stop(conn);
+ if (conn->conn.output != NULL)
+ o_stream_uncork(conn->conn.output);
+ if (conn->smtp_parser != NULL)
+ smtp_command_parser_deinit(&conn->smtp_parser);
+ ssl_iostream_destroy(&conn->ssl_iostream);
+ if (conn->ssl_ctx != NULL)
+ ssl_iostream_context_unref(&conn->ssl_ctx);
+
+ if (conn->callbacks != NULL &&
+ conn->callbacks->conn_disconnect != NULL) {
+ /* The callback may close the fd, so remove IO before that */
+ io_remove(&conn->conn.io);
+ conn->callbacks->conn_disconnect(conn->context, reason);
+ }
+
+ if (!conn->created_from_streams)
+ connection_disconnect(&conn->conn);
+ else {
+ conn->conn.fd_in = conn->conn.fd_out = -1;
+ io_remove(&conn->conn.io);
+ i_stream_unref(&conn->conn.input);
+ o_stream_unref(&conn->conn.output);
+ }
+}
+
+bool smtp_server_connection_unref(struct smtp_server_connection **_conn)
+{
+ struct smtp_server_connection *conn = *_conn;
+
+ *_conn = NULL;
+
+ i_assert(conn->refcount > 0);
+ if (--conn->refcount > 0)
+ return TRUE;
+
+ smtp_server_connection_disconnect(conn, NULL);
+
+ e_debug(conn->event, "Connection destroy");
+
+ if (conn->callbacks != NULL && conn->callbacks->conn_free != NULL)
+ conn->callbacks->conn_free(conn->context);
+
+ connection_deinit(&conn->conn);
+
+ i_free(conn->proxy_helo);
+ i_free(conn->helo_domain);
+ i_free(conn->username);
+ i_free(conn->session_id);
+ event_unref(&conn->next_trans_event);
+ pool_unref(&conn->pool);
+ return FALSE;
+}
+
+void smtp_server_connection_send_line(struct smtp_server_connection *conn,
+ const char *fmt, ...)
+{
+ va_list args;
+
+ va_start(args, fmt);
+
+ T_BEGIN {
+ string_t *str;
+
+ str = t_str_new(256);
+ str_vprintfa(str, fmt, args);
+
+ e_debug(conn->event, "Sent: %s", str_c(str));
+
+ str_append(str, "\r\n");
+ o_stream_nsend(conn->conn.output, str_data(str), str_len(str));
+ } T_END;
+ va_end(args);
+}
+
+void smtp_server_connection_reply_lines(struct smtp_server_connection *conn,
+ unsigned int status,
+ const char *enh_code,
+ const char *const *text_lines)
+{
+ struct smtp_reply reply;
+
+ i_zero(&reply);
+ reply.status = status;
+ reply.text_lines = text_lines;
+
+ if (!smtp_reply_parse_enhanced_code(
+ enh_code, &reply.enhanced_code, NULL))
+ reply.enhanced_code = SMTP_REPLY_ENH_CODE(status / 100, 0, 0);
+
+ T_BEGIN {
+ string_t *str;
+
+ e_debug(conn->event, "Sent: %s", smtp_reply_log(&reply));
+
+ str = t_str_new(256);
+ smtp_reply_write(str, &reply);
+ o_stream_nsend(conn->conn.output, str_data(str), str_len(str));
+ } T_END;
+}
+
+void smtp_server_connection_reply_immediate(
+ struct smtp_server_connection *conn,
+ unsigned int status, const char *fmt, ...)
+{
+ va_list args;
+
+ va_start(args, fmt);
+ T_BEGIN {
+ string_t *str;
+
+ str = t_str_new(256);
+ str_printfa(str, "%03u ", status);
+ str_vprintfa(str, fmt, args);
+
+ e_debug(conn->event, "Sent: %s", str_c(str));
+
+ str_append(str, "\r\n");
+ o_stream_nsend(conn->conn.output, str_data(str), str_len(str));
+ } T_END;
+ va_end(args);
+
+ /* Send immediately */
+ if (o_stream_is_corked(conn->conn.output)) {
+ o_stream_uncork(conn->conn.output);
+ o_stream_cork(conn->conn.output);
+ }
+}
+
+void smtp_server_connection_login(struct smtp_server_connection *conn,
+ const char *username, const char *helo,
+ const unsigned char *pdata,
+ unsigned int pdata_len, bool ssl_secured)
+{
+ i_assert(!conn->started);
+
+ conn->set.capabilities &= ENUM_NEGATE(SMTP_CAPABILITY_STARTTLS);
+ i_free(conn->username);
+ conn->username = i_strdup(username);
+ if (helo != NULL && *helo != '\0') {
+ i_free(conn->helo_domain);
+ conn->helo_domain = i_strdup(helo);
+ conn->helo.domain = conn->helo_domain;
+ conn->helo.domain_valid = TRUE;
+ }
+ conn->authenticated = TRUE;
+ conn->ssl_secured = ssl_secured;
+
+ if (pdata_len > 0) {
+ if (!i_stream_add_data(conn->conn.input, pdata, pdata_len))
+ i_panic("Couldn't add client input to stream");
+ }
+}
+
+void smtp_server_connection_start_pending(struct smtp_server_connection *conn)
+{
+ i_assert(!conn->started);
+ conn->started = TRUE;
+
+ conn->raw_input = conn->conn.input;
+ conn->raw_output = conn->conn.output;
+
+ if (!conn->ssl_start)
+ smtp_server_connection_ready(conn);
+ else if (conn->ssl_iostream == NULL)
+ smtp_server_connection_input_unlock(conn);
+}
+
+void smtp_server_connection_start(struct smtp_server_connection *conn)
+{
+ smtp_server_connection_start_pending(conn);
+ smtp_server_connection_resume(conn);
+}
+
+void smtp_server_connection_abort(struct smtp_server_connection **_conn,
+ unsigned int status, const char *enh_code,
+ const char *reason)
+{
+ struct smtp_server_connection *conn = *_conn;
+ const char **reason_lines;
+
+ if (conn == NULL)
+ return;
+ *_conn = NULL;
+
+ i_assert(!conn->started);
+ conn->started = TRUE;
+
+ if (conn->authenticated) {
+ reason_lines = t_strsplit_spaces(reason, "\r\n");
+ smtp_server_connection_reply_lines(
+ conn, status, enh_code, reason_lines);
+ smtp_server_connection_terminate(
+ &conn, "4.3.2", "Shutting down due to fatal error");
+ } else {
+ smtp_server_connection_terminate(&conn, enh_code, reason);
+ }
+}
+
+void smtp_server_connection_halt(struct smtp_server_connection *conn)
+{
+ conn->halted = TRUE;
+ smtp_server_connection_timeout_stop(conn);
+ if (!conn->started || !conn->ssl_start || conn->ssl_iostream != NULL)
+ smtp_server_connection_input_lock(conn);
+}
+
+void smtp_server_connection_resume(struct smtp_server_connection *conn)
+{
+ smtp_server_connection_input_unlock(conn);
+ smtp_server_connection_timeout_update(conn);
+ conn->halted = FALSE;
+}
+
+void smtp_server_connection_close(struct smtp_server_connection **_conn,
+ const char *reason)
+{
+ struct smtp_server_connection *conn = *_conn;
+
+ *_conn = NULL;
+
+ if (conn->closed)
+ return;
+ conn->closed = TRUE;
+
+ smtp_server_connection_disconnect(conn, reason);
+ smtp_server_connection_unref(&conn);
+}
+
+void smtp_server_connection_terminate(struct smtp_server_connection **_conn,
+ const char *enh_code, const char *reason)
+{
+ struct smtp_server_connection *conn = *_conn;
+ const char **reason_lines;
+
+ *_conn = NULL;
+
+ if (conn->closed)
+ return;
+
+ i_assert(enh_code[0] == '4' && enh_code[1] == '.');
+
+ T_BEGIN {
+ /* Add hostname prefix */
+ reason_lines = t_strsplit_spaces(reason, "\r\n");
+ reason_lines[0] = t_strconcat(conn->set.hostname, " ",
+ reason_lines[0], NULL);
+
+ smtp_server_connection_reply_lines(conn, 421, enh_code,
+ reason_lines);
+
+ smtp_server_connection_close(&conn, reason);
+ } T_END;
+}
+
+struct smtp_server_helo_data *
+smtp_server_connection_get_helo_data(struct smtp_server_connection *conn)
+{
+ return &conn->helo;
+}
+
+enum smtp_server_state
+smtp_server_connection_get_state(struct smtp_server_connection *conn,
+ const char **args_r)
+{
+ if (args_r != NULL)
+ *args_r = conn->state.args;
+ return conn->state.state;
+}
+
+void smtp_server_connection_set_state(struct smtp_server_connection *conn,
+ enum smtp_server_state state,
+ const char *args)
+{
+ bool changed = FALSE;
+
+ if (conn->state.state != state) {
+ conn->state.state = state;
+ changed = TRUE;
+ }
+ if (null_strcmp(args, conn->state.args) != 0) {
+ i_free(conn->state.args);
+ conn->state.args = i_strdup(args);
+ changed = TRUE;
+ }
+
+ if (changed && conn->callbacks != NULL &&
+ conn->callbacks->conn_state_changed != NULL)
+ conn->callbacks->conn_state_changed(conn->context, state, args);
+}
+
+const char *
+smtp_server_connection_get_security_string(struct smtp_server_connection *conn)
+{
+ if (conn->ssl_iostream == NULL)
+ return NULL;
+ return ssl_iostream_get_security_string(conn->ssl_iostream);
+}
+
+void smtp_server_connection_reset_state(struct smtp_server_connection *conn)
+{
+ e_debug(conn->event, "Connection state reset");
+
+ i_free(conn->state.args);
+
+ if (conn->state.trans != NULL)
+ smtp_server_transaction_free(&conn->state.trans);
+
+ /* RFC 3030, Section 2:
+ The RSET command, when issued after the first BDAT and before the
+ BDAT LAST, clears all segments sent during that transaction and resets
+ the session.
+ */
+ i_stream_destroy(&conn->state.data_input);
+ i_stream_destroy(&conn->state.data_chain_input);
+ conn->state.data_chain = NULL;
+
+ /* Reset state */
+ i_zero(&conn->state);
+ smtp_server_connection_set_state(conn, SMTP_SERVER_STATE_READY, NULL);
+}
+
+void smtp_server_connection_clear(struct smtp_server_connection *conn)
+{
+ e_debug(conn->event, "Connection clear");
+
+ i_free(conn->helo_domain);
+ i_zero(&conn->helo);
+ smtp_server_connection_reset_state(conn);
+}
+
+void smtp_server_connection_set_capabilities(
+ struct smtp_server_connection *conn, enum smtp_capability capabilities)
+{
+ conn->set.capabilities = capabilities;
+}
+
+void smtp_server_connection_add_extra_capability(
+ struct smtp_server_connection *conn,
+ const struct smtp_capability_extra *cap)
+{
+ const struct smtp_capability_extra *cap_idx;
+ struct smtp_capability_extra cap_new;
+ unsigned int insert_idx;
+ pool_t pool = conn->pool;
+
+ /* Avoid committing protocol errors */
+ i_assert(smtp_ehlo_keyword_is_valid(cap->name));
+ i_assert(smtp_ehlo_params_are_valid(cap->params));
+
+ /* Cannot override standard capabiltiies */
+ i_assert(smtp_capability_find_by_name(cap->name)
+ == SMTP_CAPABILITY_NONE);
+
+ if (!array_is_created(&conn->extra_capabilities))
+ p_array_init(&conn->extra_capabilities, pool, 4);
+
+ /* Keep array sorted */
+ insert_idx = array_count(&conn->extra_capabilities);
+ array_foreach(&conn->extra_capabilities, cap_idx) {
+ int cmp = strcasecmp(cap_idx->name, cap->name);
+
+ /* Prohibit duplicates */
+ i_assert(cmp != 0);
+
+ if (cmp > 0) {
+ insert_idx = array_foreach_idx(
+ &conn->extra_capabilities, cap_idx);
+ break;
+ }
+ }
+
+ i_zero(&cap_new);
+ cap_new.name = p_strdup(pool, cap->name);
+ if (cap->params != NULL)
+ cap_new.params = p_strarray_dup(pool, cap->params);
+
+ array_insert(&conn->extra_capabilities, insert_idx, &cap_new, 1);
+}
+
+void *smtp_server_connection_get_context(struct smtp_server_connection *conn)
+{
+ return conn->context;
+}
+
+bool smtp_server_connection_is_ssl_secured(struct smtp_server_connection *conn)
+{
+ return conn->ssl_secured;
+}
+
+bool smtp_server_connection_is_trusted(struct smtp_server_connection *conn)
+{
+ if (conn->callbacks == NULL || conn->callbacks->conn_is_trusted == NULL)
+ return FALSE;
+ return conn->callbacks->conn_is_trusted(conn->context);
+}
+
+enum smtp_protocol
+smtp_server_connection_get_protocol(struct smtp_server_connection *conn)
+{
+ return conn->set.protocol;
+}
+
+const char *
+smtp_server_connection_get_protocol_name(struct smtp_server_connection *conn)
+{
+ string_t *pname = t_str_new(16);
+
+ switch (conn->set.protocol) {
+ case SMTP_PROTOCOL_SMTP:
+ if (conn->helo.old_smtp)
+ str_append(pname, "SMTP");
+ else
+ str_append(pname, "ESMTP");
+ break;
+ case SMTP_PROTOCOL_LMTP:
+ str_append(pname, "LMTP");
+ break;
+ default:
+ i_unreached();
+ }
+ if (conn->ssl_secured)
+ str_append_c(pname, 'S');
+ if (conn->authenticated)
+ str_append_c(pname, 'A');
+ return str_c(pname);
+}
+
+struct smtp_server_transaction *
+smtp_server_connection_get_transaction(struct smtp_server_connection *conn)
+{
+ return conn->state.trans;
+}
+
+const char *
+smtp_server_connection_get_transaction_id(struct smtp_server_connection *conn)
+{
+ if (conn->state.trans == NULL)
+ return NULL;
+ return conn->state.trans->id;
+}
+
+void smtp_server_connection_get_proxy_data(struct smtp_server_connection *conn,
+ struct smtp_proxy_data *proxy_data)
+{
+ i_zero(proxy_data);
+ proxy_data->source_ip = conn->conn.remote_ip;
+ proxy_data->source_port = conn->conn.remote_port;
+ if (conn->proxy_helo != NULL)
+ proxy_data->helo = conn->proxy_helo;
+ else if (conn->helo.domain_valid)
+ proxy_data->helo = conn->helo.domain;
+ proxy_data->login = conn->username;
+ proxy_data->session = conn->session_id;
+
+ if (conn->proxy_proto != SMTP_PROXY_PROTOCOL_UNKNOWN)
+ proxy_data->proto = conn->proxy_proto;
+ else if (conn->set.protocol == SMTP_PROTOCOL_LMTP)
+ proxy_data->proto = SMTP_PROXY_PROTOCOL_LMTP;
+ else if (conn->helo.old_smtp)
+ proxy_data->proto = SMTP_PROXY_PROTOCOL_SMTP;
+ else
+ proxy_data->proto = SMTP_PROXY_PROTOCOL_ESMTP;
+
+ proxy_data->ttl_plus_1 = conn->proxy_ttl_plus_1;
+ proxy_data->timeout_secs = conn->proxy_timeout_secs;
+}
+
+void smtp_server_connection_set_proxy_data(
+ struct smtp_server_connection *conn,
+ const struct smtp_proxy_data *proxy_data)
+{
+ if (proxy_data->source_ip.family != 0)
+ conn->conn.remote_ip = proxy_data->source_ip;
+ if (proxy_data->source_port != 0)
+ conn->conn.remote_port = proxy_data->source_port;
+ if (proxy_data->helo != NULL) {
+ i_free(conn->helo_domain);
+ conn->helo_domain = i_strdup(proxy_data->helo);
+ conn->helo.domain = conn->helo_domain;
+ conn->helo.domain_valid = TRUE;
+ if (conn->helo.domain_valid) {
+ i_free(conn->proxy_helo);
+ conn->proxy_helo = i_strdup(proxy_data->helo);
+ }
+ }
+ if (proxy_data->login != NULL) {
+ i_free(conn->username);
+ conn->username = i_strdup(proxy_data->login);
+ }
+ if (proxy_data->proto != SMTP_PROXY_PROTOCOL_UNKNOWN)
+ conn->proxy_proto = proxy_data->proto;
+ if (proxy_data->session != NULL &&
+ strcmp(proxy_data->session, conn->session_id) != 0) {
+ e_debug(conn->event, "Updated session ID from %s to %s",
+ conn->session_id, proxy_data->session);
+ i_free(conn->session_id);
+ conn->session_id = i_strdup(proxy_data->session);
+ }
+
+ if (proxy_data->ttl_plus_1 > 0)
+ conn->proxy_ttl_plus_1 = proxy_data->ttl_plus_1;
+ if (conn->proxy_timeout_secs > 0)
+ conn->proxy_timeout_secs = proxy_data->timeout_secs;
+
+ connection_update_properties(&conn->conn);
+ smtp_server_connection_update_event(conn);
+
+ if (conn->callbacks != NULL &&
+ conn->callbacks->conn_proxy_data_updated != NULL) {
+ struct smtp_proxy_data full_data;
+
+ smtp_server_connection_get_proxy_data(conn, &full_data);
+
+ conn->callbacks->
+ conn_proxy_data_updated(conn->context, &full_data);
+ }
+}
+
+void smtp_server_connection_register_mail_param(
+ struct smtp_server_connection *conn, const char *param)
+{
+ param = p_strdup(conn->pool, param);
+
+ if (!array_is_created(&conn->mail_param_extensions)) {
+ p_array_init(&conn->mail_param_extensions, conn->pool, 8);
+ array_push_back(&conn->mail_param_extensions, &param);
+ } else {
+ unsigned int count = array_count(&conn->mail_param_extensions);
+
+ i_assert(count > 0);
+ array_idx_set(&conn->mail_param_extensions,
+ count - 1, &param);
+ }
+ array_append_zero(&conn->mail_param_extensions);
+}
+
+void smtp_server_connection_register_rcpt_param(
+ struct smtp_server_connection *conn, const char *param)
+{
+ param = p_strdup(conn->pool, param);
+
+ if (!array_is_created(&conn->rcpt_param_extensions)) {
+ p_array_init(&conn->rcpt_param_extensions, conn->pool, 8);
+ array_push_back(&conn->rcpt_param_extensions, &param);
+ } else {
+ unsigned int count = array_count(&conn->rcpt_param_extensions);
+
+ i_assert(count > 0);
+ array_idx_set(&conn->rcpt_param_extensions,
+ count - 1, &param);
+ }
+ array_append_zero(&conn->rcpt_param_extensions);
+}
+
+void smtp_server_connection_switch_ioloop(struct smtp_server_connection *conn)
+{
+ if (conn->to_idle != NULL)
+ conn->to_idle = io_loop_move_timeout(&conn->to_idle);
+ connection_switch_ioloop(&conn->conn);
+}
+
+struct event_reason *
+smtp_server_connection_reason_begin(struct smtp_server_connection *conn,
+ const char *name)
+{
+ if (conn->set.reason_code_module == NULL)
+ return NULL;
+ const char *reason_code =
+ event_reason_code(conn->set.reason_code_module, name);
+ return event_reason_begin(reason_code);
+}
diff --git a/src/lib-smtp/smtp-server-private.h b/src/lib-smtp/smtp-server-private.h
new file mode 100644
index 0000000..7a92336
--- /dev/null
+++ b/src/lib-smtp/smtp-server-private.h
@@ -0,0 +1,404 @@
+#ifndef SMTP_SERVER_PRIVATE_H
+#define SMTP_SERVER_PRIVATE_H
+
+#include "connection.h"
+
+#include "smtp-server.h"
+
+#define SMTP_SERVER_COMMAND_POOL_MAX (8 * 1024)
+
+#define SMTP_SERVER_DEFAULT_MAX_COMMAND_LINE (4 * 1024)
+#define SMTP_SERVER_DEFAULT_MAX_BAD_COMMANDS 10
+#define SMTP_SERVER_DEFAULT_MAX_SIZE_EXCESS_LIMIT (1024*1024)
+
+#define SMTP_SERVER_DEFAULT_CAPABILITIES \
+ (SMTP_CAPABILITY_SIZE | SMTP_CAPABILITY_ENHANCEDSTATUSCODES | \
+ SMTP_CAPABILITY_8BITMIME | SMTP_CAPABILITY_CHUNKING)
+
+struct smtp_server_cmd_hook;
+struct smtp_server_reply;
+struct smtp_server_command;
+struct smtp_server_connection;
+
+ARRAY_DEFINE_TYPE(smtp_server_reply, struct smtp_server_reply);
+ARRAY_DEFINE_TYPE(smtp_server_cmd_hook, struct smtp_server_cmd_hook);
+
+enum smtp_server_command_state {
+ /* New command; callback to command start handler executing. */
+ SMTP_SERVER_COMMAND_STATE_NEW = 0,
+ /* This command is being processed; command data is fully read, but no
+ reply is yet submitted */
+ SMTP_SERVER_COMMAND_STATE_PROCESSING,
+ /* A reply is submitted for this command. If not all command data was
+ read by the handler, it is first skipped on the input. If this is a
+ multi-reply command (LMTP->DATA), not all replies may be submitted
+ yet. */
+ SMTP_SERVER_COMMAND_STATE_SUBMITTED_REPLY,
+ /* Request is ready for sending reply; a reply is submitted and the
+ command payload is fully read. If this is a multi-reply command
+ (LMTP->DATA), not all replies may be submitted yet. In that case the
+ command state goes back to PROCESSING once the all submitted replies
+ are sent. */
+ SMTP_SERVER_COMMAND_STATE_READY_TO_REPLY,
+ /* The reply for the command is sent */
+ SMTP_SERVER_COMMAND_STATE_FINISHED,
+ /* Request is aborted; still lingering due to references */
+ SMTP_SERVER_COMMAND_STATE_ABORTED
+};
+
+struct smtp_server_command_hook {
+ enum smtp_server_command_hook_type type;
+ struct smtp_server_command_hook *prev, *next;
+
+ smtp_server_cmd_func_t *func;
+ void *context;
+};
+
+struct smtp_server_recipient_hook {
+ enum smtp_server_recipient_hook_type type;
+ struct smtp_server_recipient_hook *prev, *next;
+
+ smtp_server_rcpt_func_t *func;
+ void *context;
+};
+
+struct smtp_server_reply_content {
+ unsigned int status;
+ const char *enhanced_code;
+ const char *status_prefix;
+
+ string_t *text;
+ size_t last_line;
+};
+
+struct smtp_server_reply {
+ struct smtp_server_command *command;
+ unsigned int index;
+ struct event *event;
+
+ /* Replies may share content */
+ struct smtp_server_reply_content *content;
+
+ bool submitted:1;
+ bool sent:1;
+ bool forwarded:1;
+};
+
+struct smtp_server_command_reg {
+ const char *name;
+ enum smtp_server_command_flags flags;
+ smtp_server_cmd_start_func_t *func;
+};
+
+struct smtp_server_command {
+ struct smtp_server_cmd_ctx context;
+ const struct smtp_server_command_reg *reg;
+ int refcount;
+
+ enum smtp_server_command_state state;
+
+ struct smtp_server_command *prev, *next;
+
+ struct smtp_server_command_hook *hooks_head, *hooks_tail;
+ void *data;
+
+ ARRAY_TYPE(smtp_server_reply) replies;
+ unsigned int replies_expected;
+ unsigned int replies_submitted;
+
+ bool input_locked:1;
+ bool input_captured:1;
+ bool pipeline_blocked:1;
+ bool reply_early:1;
+ bool destroying:1;
+};
+
+struct smtp_server_recipient_private {
+ struct smtp_server_recipient rcpt;
+ int refcount;
+
+ struct smtp_server_recipient_hook *hooks_head, *hooks_tail;
+
+ bool destroying:1;
+};
+
+struct smtp_server_state_data {
+ enum smtp_server_state state;
+ char *args;
+ time_t timestamp;
+
+ unsigned int pending_mail_cmds;
+ unsigned int pending_rcpt_cmds, denied_rcpt_cmds;
+ unsigned int pending_data_cmds;
+
+ struct smtp_server_transaction *trans;
+ struct istream *data_input, *data_chain_input;
+ struct istream_chain *data_chain;
+ unsigned int data_chunks;
+ uoff_t data_size;
+
+ bool data_failed:1;
+};
+
+struct smtp_server_connection {
+ struct connection conn;
+ struct smtp_server *server;
+ pool_t pool;
+ int refcount;
+ struct event *event, *next_trans_event;
+
+ struct smtp_server_settings set;
+
+ ARRAY(struct smtp_capability_extra) extra_capabilities;
+ ARRAY_TYPE(const_string) mail_param_extensions; /* NULL-terminated */
+ ARRAY_TYPE(const_string) rcpt_param_extensions; /* NULL-terminated */
+
+ const struct smtp_server_callbacks *callbacks;
+ void *context;
+
+ enum smtp_proxy_protocol proxy_proto;
+ unsigned int proxy_ttl_plus_1;
+ unsigned int proxy_timeout_secs;
+ char *proxy_helo;
+
+ struct smtp_server_helo_data helo, *pending_helo;
+ char *helo_domain, *username;
+
+ char *session_id;
+ unsigned int transaction_seq;
+
+ struct timeout *to_idle;
+ struct istream *raw_input;
+ struct ostream *raw_output;
+ struct ssl_iostream_context *ssl_ctx;
+ struct ssl_iostream *ssl_iostream;
+ struct smtp_command_parser *smtp_parser;
+
+ struct smtp_server_command *command_queue_head, *command_queue_tail;
+ unsigned int command_queue_count;
+ unsigned int bad_counter;
+
+ struct smtp_server_state_data state;
+
+ struct smtp_server_stats stats;
+
+ bool started:1;
+ bool halted:1;
+ bool ssl_start:1;
+ bool ssl_secured:1;
+ bool authenticated:1;
+ bool created_from_streams:1;
+ bool corked:1;
+ bool disconnected:1;
+ bool closing:1;
+ bool closed:1;
+ bool input_broken:1;
+ bool input_locked:1;
+ bool handling_input:1;
+ bool rawlog_checked:1;
+ bool rawlog_enabled:1;
+};
+
+struct smtp_server {
+ pool_t pool;
+
+ struct smtp_server_settings set;
+
+ struct event *event;
+ struct ssl_iostream_context *ssl_ctx;
+
+ ARRAY(struct smtp_server_command_reg) commands_reg;
+
+ struct connection_list *conn_list;
+
+ bool commands_unsorted:1;
+};
+
+bool smtp_server_connection_pending_command_data(
+ struct smtp_server_connection *conn);
+
+/*
+ * Reply
+ */
+
+void smtp_server_reply_free(struct smtp_server_command *cmd);
+
+int smtp_server_reply_send(struct smtp_server_reply *resp);
+
+const char *
+smtp_server_reply_get_one_line(const struct smtp_server_reply *reply);
+const char *
+smtp_server_reply_get_message(const struct smtp_server_reply *reply);
+
+void smtp_server_reply_add_to_event(const struct smtp_server_reply *reply,
+ struct event_passthrough *e);
+
+/*
+ * Command
+ */
+
+void smtp_server_commands_init(struct smtp_server *server);
+
+void smtp_server_command_debug(struct smtp_server_cmd_ctx *cmd,
+ const char *format, ...) ATTR_FORMAT(2, 3);
+
+struct smtp_server_command *
+smtp_server_command_new_invalid(struct smtp_server_connection *conn);
+struct smtp_server_command *
+smtp_server_command_new(struct smtp_server_connection *conn, const char *name);
+
+void smtp_server_command_execute(struct smtp_server_command *cmd,
+ const char *params);
+
+void smtp_server_command_ref(struct smtp_server_command *cmd);
+bool smtp_server_command_unref(struct smtp_server_command **_cmd);
+void smtp_server_command_abort(struct smtp_server_command **_cmd);
+
+bool smtp_server_command_call_hooks(struct smtp_server_command **_cmd,
+ enum smtp_server_command_hook_type type,
+ bool remove);
+void smtp_server_command_remove_hooks(struct smtp_server_command *cmd,
+ enum smtp_server_command_hook_type type);
+
+void smtp_server_command_submit_reply(struct smtp_server_command *cmd);
+
+int smtp_server_connection_flush(struct smtp_server_connection *conn);
+
+void smtp_server_command_ready_to_reply(struct smtp_server_command *cmd);
+bool smtp_server_command_send_replies(struct smtp_server_command *cmd);
+void smtp_server_command_finished(struct smtp_server_command *cmd);
+
+bool smtp_server_command_next_to_reply(struct smtp_server_command **_cmd);
+bool smtp_server_command_completed(struct smtp_server_command **_cmd);
+
+static inline bool
+smtp_server_command_is_complete(struct smtp_server_command *cmd)
+{
+ struct smtp_server_connection *conn = cmd->context.conn;
+
+ return (conn->input_broken || (cmd->next != NULL) || cmd->reply_early ||
+ !smtp_server_connection_pending_command_data(conn));
+}
+
+/*
+ * Connection
+ */
+
+typedef void smtp_server_input_callback_t(void *context);
+
+void smtp_server_connection_debug(struct smtp_server_connection *conn,
+ const char *format, ...) ATTR_FORMAT(2, 3);
+
+struct connection_list *smtp_server_connection_list_init(void);
+
+struct event_reason *
+smtp_server_connection_reason_begin(struct smtp_server_connection *conn,
+ const char *name);
+
+void smtp_server_connection_switch_ioloop(struct smtp_server_connection *conn);
+
+void smtp_server_connection_handle_output_error(
+ struct smtp_server_connection *conn);
+void smtp_server_connection_trigger_output(struct smtp_server_connection *conn);
+bool smtp_server_connection_pending_payload(struct smtp_server_connection *conn);
+
+void smtp_server_connection_cork(struct smtp_server_connection *conn);
+void smtp_server_connection_uncork(struct smtp_server_connection *conn);
+
+void smtp_server_connection_input_halt(struct smtp_server_connection *conn);
+void smtp_server_connection_input_resume(struct smtp_server_connection *conn);
+void smtp_server_connection_input_capture(
+ struct smtp_server_connection *conn,
+ smtp_server_input_callback_t *callback, void *context);
+#define smtp_server_connection_input_capture(conn, callback, context) \
+ smtp_server_connection_input_capture(conn - \
+ CALLBACK_TYPECHECK(callback, void (*)(typeof(context))), \
+ (smtp_server_input_callback_t *)callback, context)
+
+void smtp_server_connection_timeout_stop(struct smtp_server_connection *conn);
+void smtp_server_connection_timeout_start(struct smtp_server_connection *conn);
+void smtp_server_connection_timeout_reset(struct smtp_server_connection *conn);
+
+void smtp_server_connection_send_line(struct smtp_server_connection *conn,
+ const char *fmt, ...) ATTR_FORMAT(2, 3);
+void smtp_server_connection_reply_lines(struct smtp_server_connection *conn,
+ unsigned int status,
+ const char *enh_code,
+ const char *const *text_lines);
+void smtp_server_connection_reply_immediate(
+ struct smtp_server_connection *conn, unsigned int status,
+ const char *fmt, ...) ATTR_FORMAT(3, 4);
+
+void smtp_server_connection_reset_state(struct smtp_server_connection *conn);
+void smtp_server_connection_set_state(struct smtp_server_connection *conn,
+ enum smtp_server_state state,
+ const char *args) ATTR_NULL(3);
+
+int smtp_server_connection_ssl_init(struct smtp_server_connection *conn);
+
+void smtp_server_connection_clear(struct smtp_server_connection *conn);
+
+struct smtp_server_transaction *
+smtp_server_connection_get_transaction(struct smtp_server_connection *conn);
+
+/*
+ * Recipient
+ */
+
+struct smtp_server_recipient *
+smtp_server_recipient_create(struct smtp_server_cmd_ctx *cmd,
+ const struct smtp_address *rcpt_to,
+ const struct smtp_params_rcpt *params);
+void smtp_server_recipient_ref(struct smtp_server_recipient *rcpt);
+bool smtp_server_recipient_unref(struct smtp_server_recipient **_rcpt);
+void smtp_server_recipient_destroy(struct smtp_server_recipient **_rcpt);
+
+bool smtp_server_recipient_approved(struct smtp_server_recipient **_rcpt);
+void smtp_server_recipient_denied(struct smtp_server_recipient *rcpt,
+ const struct smtp_server_reply *reply);
+
+void smtp_server_recipient_data_command(struct smtp_server_recipient *rcpt,
+ struct smtp_server_cmd_ctx *cmd);
+void smtp_server_recipient_data_replied(struct smtp_server_recipient *rcpt);
+
+void smtp_server_recipient_reset(struct smtp_server_recipient *rcpt);
+void smtp_server_recipient_finished(struct smtp_server_recipient *rcpt,
+ const struct smtp_server_reply *reply);
+
+bool smtp_server_recipient_call_hooks(
+ struct smtp_server_recipient **_rcpt,
+ enum smtp_server_recipient_hook_type type);
+
+/*
+ * Transaction
+ */
+
+struct smtp_server_transaction *
+smtp_server_transaction_create(struct smtp_server_connection *conn,
+ const struct smtp_server_cmd_mail *mail_data);
+void smtp_server_transaction_free(struct smtp_server_transaction **_trans);
+
+void smtp_server_transaction_add_rcpt(struct smtp_server_transaction *trans,
+ struct smtp_server_recipient *rcpt);
+bool smtp_server_transaction_has_rcpt(struct smtp_server_transaction *trans);
+unsigned int
+smtp_server_transaction_rcpt_count(struct smtp_server_transaction *trans);
+
+void smtp_server_transaction_data_command(struct smtp_server_transaction *trans,
+ struct smtp_server_cmd_ctx *cmd);
+
+void smtp_server_transaction_received(struct smtp_server_transaction *trans,
+ uoff_t data_size);
+
+void smtp_server_transaction_reset(struct smtp_server_transaction *trans);
+void smtp_server_transaction_finished(struct smtp_server_transaction *trans,
+ struct smtp_server_cmd_ctx *cmd);
+
+/*
+ * Server
+ */
+
+void smtp_server_event_init(struct smtp_server *server, struct event *event);
+int smtp_server_init_ssl_ctx(struct smtp_server *server, const char **error_r);
+
+#endif
diff --git a/src/lib-smtp/smtp-server-recipient.c b/src/lib-smtp/smtp-server-recipient.c
new file mode 100644
index 0000000..fbae663
--- /dev/null
+++ b/src/lib-smtp/smtp-server-recipient.c
@@ -0,0 +1,361 @@
+/* Copyright (c) 2013-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "llist.h"
+#include "str-sanitize.h"
+#include "smtp-address.h"
+#include "smtp-reply.h"
+
+#include "smtp-server-private.h"
+
+static void
+smtp_server_recipient_update_event(struct smtp_server_recipient_private *prcpt)
+{
+ struct event *event = prcpt->rcpt.event;
+ const char *path = smtp_address_encode(prcpt->rcpt.path);
+
+ event_add_str(event, "rcpt_to", path);
+ smtp_params_rcpt_add_to_event(&prcpt->rcpt.params, event);
+ event_set_append_log_prefix(
+ event, t_strdup_printf("rcpt %s: ", str_sanitize(path, 128)));
+}
+
+static void
+smtp_server_recipient_create_event(struct smtp_server_recipient_private *prcpt)
+{
+ struct smtp_server_recipient *rcpt = &prcpt->rcpt;
+ struct smtp_server_connection *conn = rcpt->conn;
+
+ if (rcpt->event != NULL)
+ return;
+
+ if (conn->state.trans == NULL) {
+ /* Create event for the transaction early. */
+ if (conn->next_trans_event == NULL) {
+ conn->next_trans_event = event_create(conn->event);
+ event_set_append_log_prefix(conn->next_trans_event,
+ "trans: ");
+ }
+ rcpt->event = event_create(conn->next_trans_event);
+ } else {
+ /* Use existing transaction event. */
+ rcpt->event = event_create(conn->state.trans->event);
+ }
+ /* Drop transaction log prefix so that the connection event prefix
+ remains. */
+ event_drop_parent_log_prefixes(rcpt->event, 1);
+
+ smtp_server_recipient_update_event(prcpt);
+}
+
+struct smtp_server_recipient *
+smtp_server_recipient_create(struct smtp_server_cmd_ctx *cmd,
+ const struct smtp_address *rcpt_to,
+ const struct smtp_params_rcpt *params)
+{
+ struct smtp_server_recipient_private *prcpt;
+ pool_t pool;
+
+ pool = pool_alloconly_create("smtp server recipient", 512);
+ prcpt = p_new(pool, struct smtp_server_recipient_private, 1);
+ prcpt->refcount = 1;
+ prcpt->rcpt.pool = pool;
+ prcpt->rcpt.conn = cmd->conn;
+ prcpt->rcpt.cmd = cmd;
+ prcpt->rcpt.path = smtp_address_clone(pool, rcpt_to);
+ smtp_params_rcpt_copy(pool, &prcpt->rcpt.params, params);
+
+ smtp_server_recipient_create_event(prcpt);
+
+ return &prcpt->rcpt;
+}
+
+void smtp_server_recipient_ref(struct smtp_server_recipient *rcpt)
+{
+ struct smtp_server_recipient_private *prcpt =
+ (struct smtp_server_recipient_private *)rcpt;
+
+ if (prcpt->destroying)
+ return;
+ i_assert(prcpt->refcount > 0);
+ prcpt->refcount++;
+}
+
+bool smtp_server_recipient_unref(struct smtp_server_recipient **_rcpt)
+{
+ struct smtp_server_recipient *rcpt = *_rcpt;
+ struct smtp_server_recipient_private *prcpt =
+ (struct smtp_server_recipient_private *)rcpt;
+
+ *_rcpt = NULL;
+
+ if (rcpt == NULL)
+ return FALSE;
+ if (prcpt->destroying)
+ return FALSE;
+
+ i_assert(prcpt->refcount > 0);
+ if (--prcpt->refcount > 0)
+ return TRUE;
+ prcpt->destroying = TRUE;
+
+ if (!smtp_server_recipient_call_hooks(
+ &rcpt, SMTP_SERVER_RECIPIENT_HOOK_DESTROY))
+ i_unreached();
+
+ if (!rcpt->finished) {
+ smtp_server_recipient_create_event(prcpt);
+
+ struct event_passthrough *e =
+ event_create_passthrough(rcpt->event)->
+ set_name("smtp_server_transaction_rcpt_finished");
+ e->add_int("status_code", 9000);
+ e->add_str("enhanced_code", "9.0.0");
+ e->add_str("error", "Aborted");
+
+ e_debug(e->event(), "Aborted");
+ }
+
+ event_unref(&rcpt->event);
+ pool_unref(&rcpt->pool);
+ return FALSE;
+}
+
+void smtp_server_recipient_destroy(struct smtp_server_recipient **_rcpt)
+{
+ smtp_server_recipient_unref(_rcpt);
+}
+
+const struct smtp_address *
+smtp_server_recipient_get_original(struct smtp_server_recipient *rcpt)
+{
+ if (rcpt->params.orcpt.addr == NULL)
+ return rcpt->path;
+ return rcpt->params.orcpt.addr;
+}
+
+bool smtp_server_recipient_approved(struct smtp_server_recipient **_rcpt)
+{
+ struct smtp_server_recipient *rcpt = *_rcpt;
+ struct smtp_server_transaction *trans = rcpt->conn->state.trans;
+
+ i_assert(trans != NULL);
+ i_assert(rcpt->event != NULL);
+
+ e_debug(rcpt->event, "Approved");
+
+ rcpt->cmd = NULL;
+ smtp_server_transaction_add_rcpt(trans, rcpt);
+
+ return smtp_server_recipient_call_hooks(
+ _rcpt, SMTP_SERVER_RECIPIENT_HOOK_APPROVED);
+}
+
+void smtp_server_recipient_denied(struct smtp_server_recipient *rcpt,
+ const struct smtp_server_reply *reply)
+{
+ i_assert(!rcpt->finished);
+ i_assert(rcpt->event != NULL);
+
+ rcpt->finished = TRUE;
+
+ struct event_passthrough *e =
+ event_create_passthrough(rcpt->event)->
+ set_name("smtp_server_transaction_rcpt_finished");
+ smtp_server_reply_add_to_event(reply, e);
+
+ e_debug(e->event(), "Denied");
+}
+
+void smtp_server_recipient_data_command(struct smtp_server_recipient *rcpt,
+ struct smtp_server_cmd_ctx *cmd)
+{
+ rcpt->cmd = cmd;
+}
+
+void smtp_server_recipient_data_replied(struct smtp_server_recipient *rcpt)
+{
+ if (rcpt->replied)
+ return;
+ if (smtp_server_recipient_get_reply(rcpt) == NULL)
+ return;
+ rcpt->replied = TRUE;
+ if (!smtp_server_recipient_call_hooks(
+ &rcpt, SMTP_SERVER_RECIPIENT_HOOK_DATA_REPLIED)) {
+ /* Nothing to do */
+ }
+}
+
+struct smtp_server_reply *
+smtp_server_recipient_get_reply(struct smtp_server_recipient *rcpt)
+{
+ if (!HAS_ALL_BITS(rcpt->trans->flags,
+ SMTP_SERVER_TRANSACTION_FLAG_REPLY_PER_RCPT))
+ return smtp_server_command_get_reply(rcpt->cmd->cmd, 0);
+ return smtp_server_command_get_reply(rcpt->cmd->cmd, rcpt->index);
+}
+
+bool smtp_server_recipient_is_replied(struct smtp_server_recipient *rcpt)
+{
+ i_assert(rcpt->cmd != NULL);
+
+ return smtp_server_command_is_replied(rcpt->cmd->cmd);
+}
+
+void smtp_server_recipient_replyv(struct smtp_server_recipient *rcpt,
+ unsigned int status, const char *enh_code,
+ const char *fmt, va_list args)
+{
+ i_assert(rcpt->cmd != NULL);
+
+ if (smtp_server_command_is_rcpt(rcpt->cmd) && (status / 100) == 2) {
+ smtp_server_reply_indexv(rcpt->cmd, rcpt->index,
+ status, enh_code, fmt, args);
+ return;
+ }
+
+ smtp_server_reply_index(rcpt->cmd, rcpt->index, status, enh_code,
+ "<%s> %s", smtp_address_encode(rcpt->path),
+ t_strdup_vprintf(fmt, args));
+}
+
+void smtp_server_recipient_reply(struct smtp_server_recipient *rcpt,
+ unsigned int status, const char *enh_code,
+ const char *fmt, ...)
+{
+ va_list args;
+
+ va_start(args, fmt);
+ smtp_server_recipient_replyv(rcpt, status, enh_code, fmt, args);
+ va_end(args);
+}
+
+void smtp_server_recipient_reply_forward(struct smtp_server_recipient *rcpt,
+ const struct smtp_reply *from)
+{
+ bool add_path = (!smtp_server_command_is_rcpt(rcpt->cmd) ||
+ !smtp_reply_is_success(from));
+ struct smtp_server_reply *reply;
+
+ reply = smtp_server_reply_create_forward(rcpt->cmd->cmd, rcpt->index,
+ from);
+ smtp_server_reply_replace_path(reply, rcpt->path, add_path);
+ smtp_server_reply_submit(reply);
+}
+
+void smtp_server_recipient_reset(struct smtp_server_recipient *rcpt)
+{
+ i_assert(!rcpt->finished);
+ rcpt->finished = TRUE;
+
+ struct event_passthrough *e =
+ event_create_passthrough(rcpt->event)->
+ set_name("smtp_server_transaction_rcpt_finished");
+ e->add_int("status_code", 9000);
+ e->add_str("enhanced_code", "9.0.0");
+ e->add_str("error", "Reset");
+
+ e_debug(e->event(), "Reset");
+}
+
+void smtp_server_recipient_finished(struct smtp_server_recipient *rcpt,
+ const struct smtp_server_reply *reply)
+{
+ i_assert(!rcpt->finished);
+ rcpt->finished = TRUE;
+
+ struct event_passthrough *e =
+ event_create_passthrough(rcpt->event)->
+ set_name("smtp_server_transaction_rcpt_finished");
+ smtp_server_reply_add_to_event(reply, e);
+
+ e_debug(e->event(), "Finished");
+}
+
+#undef smtp_server_recipient_add_hook
+void smtp_server_recipient_add_hook(struct smtp_server_recipient *rcpt,
+ enum smtp_server_recipient_hook_type type,
+ smtp_server_rcpt_func_t func, void *context)
+{
+ struct smtp_server_recipient_private *prcpt =
+ (struct smtp_server_recipient_private *)rcpt;
+ struct smtp_server_recipient_hook *hook;
+
+ i_assert(func != NULL);
+
+ hook = prcpt->hooks_head;
+ while (hook != NULL) {
+ /* No double registrations */
+ i_assert(hook->type != type || hook->func != func);
+
+ hook = hook->next;
+ }
+
+ hook = p_new(rcpt->pool, struct smtp_server_recipient_hook, 1);
+ hook->type = type;
+ hook->func = func;
+ hook->context = context;
+
+ DLLIST2_APPEND(&prcpt->hooks_head, &prcpt->hooks_tail, hook);
+}
+
+#undef smtp_server_recipient_remove_hook
+void smtp_server_recipient_remove_hook(
+ struct smtp_server_recipient *rcpt,
+ enum smtp_server_recipient_hook_type type,
+ smtp_server_rcpt_func_t *func)
+{
+ struct smtp_server_recipient_private *prcpt =
+ (struct smtp_server_recipient_private *)rcpt;
+ struct smtp_server_recipient_hook *hook;
+ bool found = FALSE;
+
+ hook = prcpt->hooks_head;
+ while (hook != NULL) {
+ struct smtp_server_recipient_hook *hook_next = hook->next;
+
+ if (hook->type == type && hook->func == func) {
+ DLLIST2_REMOVE(&prcpt->hooks_head, &prcpt->hooks_tail,
+ hook);
+ found = TRUE;
+ break;
+ }
+
+ hook = hook_next;
+ }
+ i_assert(found);
+}
+
+bool smtp_server_recipient_call_hooks(
+ struct smtp_server_recipient **_rcpt,
+ enum smtp_server_recipient_hook_type type)
+{
+ struct smtp_server_recipient *rcpt = *_rcpt;
+ struct smtp_server_recipient_private *prcpt =
+ (struct smtp_server_recipient_private *)rcpt;
+ struct smtp_server_recipient_hook *hook;
+
+ if (type != SMTP_SERVER_RECIPIENT_HOOK_DESTROY)
+ smtp_server_recipient_ref(rcpt);
+
+ hook = prcpt->hooks_head;
+ while (hook != NULL) {
+ struct smtp_server_recipient_hook *hook_next = hook->next;
+
+ if (hook->type == type) {
+ DLLIST2_REMOVE(&prcpt->hooks_head, &prcpt->hooks_tail,
+ hook);
+ hook->func(rcpt, hook->context);
+ }
+
+ hook = hook_next;
+ }
+
+ if (type != SMTP_SERVER_RECIPIENT_HOOK_DESTROY) {
+ if (!smtp_server_recipient_unref(&rcpt)) {
+ *_rcpt = NULL;
+ return FALSE;
+ }
+ }
+ return TRUE;
+}
diff --git a/src/lib-smtp/smtp-server-reply.c b/src/lib-smtp/smtp-server-reply.c
new file mode 100644
index 0000000..60342db
--- /dev/null
+++ b/src/lib-smtp/smtp-server-reply.c
@@ -0,0 +1,876 @@
+/* Copyright (c) 2013-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "str.h"
+#include "array.h"
+#include "istream.h"
+#include "ostream.h"
+#include "smtp-address.h"
+#include "smtp-reply.h"
+
+#include "smtp-server-private.h"
+
+/*
+ * Reply
+ */
+
+static void smtp_server_reply_destroy(struct smtp_server_reply *reply)
+{
+ if (reply->command == NULL)
+ return;
+
+ if (reply->event != NULL) {
+ e_debug(reply->event, "Destroy");
+ event_unref(&reply->event);
+ }
+
+ if (reply->content == NULL)
+ return;
+ str_free(&reply->content->text);
+}
+
+static void smtp_server_reply_clear(struct smtp_server_reply *reply)
+{
+ smtp_server_reply_destroy(reply);
+ if (reply->submitted) {
+ i_assert(reply->command->replies_submitted > 0);
+ reply->command->replies_submitted--;
+ }
+ reply->submitted = FALSE;
+ reply->forwarded = FALSE;
+}
+
+static void smtp_server_reply_update_event(struct smtp_server_reply *reply)
+{
+ struct smtp_server_command *command = reply->command;
+
+ event_add_int(reply->event, "index", reply->index);
+ event_add_int(reply->event, "status", reply->content->status);
+
+ if (command->replies_expected > 1) {
+ event_set_append_log_prefix(reply->event,
+ t_strdup_printf("%u reply [%u/%u]: ",
+ reply->content->status,
+ reply->index+1,
+ command->replies_expected));
+ } else {
+ event_set_append_log_prefix(reply->event,
+ t_strdup_printf("%u reply: ",
+ reply->content->status));
+ }
+}
+
+static struct smtp_server_reply *
+smtp_server_reply_alloc(struct smtp_server_command *cmd, unsigned int index)
+{
+ struct smtp_server_reply *reply;
+ pool_t pool = cmd->context.pool;
+
+ if (array_is_created(&cmd->replies)) {
+ reply = array_idx_modifiable(&cmd->replies, index);
+ /* get rid of any existing reply */
+ i_assert(!reply->sent);
+ smtp_server_reply_clear(reply);
+ } else {
+ p_array_init(&cmd->replies, pool, cmd->replies_expected);
+ array_idx_clear(&cmd->replies, cmd->replies_expected - 1);
+ reply = array_idx_modifiable(&cmd->replies, index);
+ }
+ reply->event = event_create(cmd->context.event);
+
+ return reply;
+}
+
+static void
+smtp_server_reply_update_prefix(struct smtp_server_reply *reply,
+ unsigned int status, const char *enh_code)
+{
+ pool_t pool = reply->command->context.pool;
+ string_t *textbuf, *new_text;
+ const char *new_prefix, *text, *p;
+ size_t text_len, prefix_len, line_len;
+
+ if (enh_code == NULL || *enh_code == '\0') {
+ new_prefix = p_strdup_printf(pool, "%03u-", status);
+ } else {
+ new_prefix = p_strdup_printf(pool, "%03u-%s ",
+ status, enh_code);
+ }
+
+ i_assert(reply->content != NULL);
+ textbuf = reply->content->text;
+
+ if (textbuf == NULL || str_len(textbuf) == 0) {
+ reply->content->status_prefix = new_prefix;
+ return;
+ }
+ new_text = str_new(default_pool, 256);
+
+ prefix_len = strlen(reply->content->status_prefix);
+ text = str_c(textbuf);
+ text_len = str_len(textbuf);
+
+ i_assert(text_len > prefix_len);
+ text_len -= prefix_len;
+ text += prefix_len;
+
+ for (;;) {
+ reply->content->last_line = str_len(new_text);
+
+ p = strchr(text, '\n');
+ i_assert(p != NULL && p > text && *(p-1) == '\r');
+ p++;
+
+ str_append(new_text, new_prefix);
+ str_append_data(new_text, text, p - text);
+
+ line_len = (size_t)(p - text);
+ i_assert(text_len >= line_len);
+ text_len -= line_len;
+ text = p;
+
+ if (text_len <= prefix_len)
+ break;
+
+ text_len -= prefix_len;
+ text += prefix_len;
+ }
+
+ str_free(&textbuf);
+ reply->content->text = new_text;
+ reply->content->status_prefix = new_prefix;
+}
+
+void smtp_server_reply_set_status(struct smtp_server_reply *reply,
+ unsigned int status, const char *enh_code)
+{
+ pool_t pool = reply->command->context.pool;
+
+ /* RFC 5321, Section 4.2:
+
+ In the absence of extensions negotiated with the client, SMTP servers
+ MUST NOT send reply codes whose first digits are other than 2, 3, 4,
+ or 5. Clients that receive such out-of-range codes SHOULD normally
+ treat them as fatal errors and terminate the mail transaction.
+ */
+ i_assert(status >= 200 && status < 560);
+
+ /* RFC 2034, Section 4:
+
+ All status codes returned by the server must agree with the primary
+ response code, that is, a 2xx response must incorporate a 2.X.X code,
+ a 4xx response must incorporate a 4.X.X code, and a 5xx response must
+ incorporate a 5.X.X code.
+ */
+ i_assert(enh_code == NULL || *enh_code == '\0' ||
+ ((unsigned int)(enh_code[0] - '0') == (status / 100)
+ && enh_code[1] == '.'));
+
+ if (reply->content->status == status &&
+ null_strcmp(reply->content->enhanced_code, enh_code) == 0)
+ return;
+
+ smtp_server_reply_update_prefix(reply, status, enh_code);
+ reply->content->status = status;
+ reply->content->enhanced_code = p_strdup(pool, enh_code);
+}
+
+unsigned int smtp_server_reply_get_status(struct smtp_server_reply *reply,
+ const char **enh_code_r)
+{
+ if (enh_code_r != NULL)
+ *enh_code_r = reply->content->enhanced_code;
+ return reply->content->status;
+}
+
+struct smtp_server_reply *
+smtp_server_reply_create_index(struct smtp_server_command *cmd,
+ unsigned int index, unsigned int status,
+ const char *enh_code)
+{
+ struct smtp_server_reply *reply;
+ pool_t pool = cmd->context.pool;
+
+ i_assert(cmd->replies_expected > 0);
+ i_assert(index < cmd->replies_expected);
+
+ reply = smtp_server_reply_alloc(cmd, index);
+ reply->index = index;
+ reply->command = cmd;
+
+ if (reply->content == NULL)
+ reply->content = p_new(pool, struct smtp_server_reply_content, 1);
+ smtp_server_reply_set_status(reply, status, enh_code);
+ reply->content->text = str_new(default_pool, 256);
+
+ smtp_server_reply_update_event(reply);
+
+ return reply;
+}
+
+struct smtp_server_reply *
+smtp_server_reply_create(struct smtp_server_command *cmd,
+ unsigned int status, const char *enh_code)
+{
+ return smtp_server_reply_create_index(cmd, 0, status, enh_code);
+}
+
+struct smtp_server_reply *
+smtp_server_reply_create_forward(struct smtp_server_command *cmd,
+ unsigned int index, const struct smtp_reply *from)
+{
+ struct smtp_server_reply *reply;
+ string_t *textbuf;
+ char *text;
+ size_t last_line, i;
+
+ reply = smtp_server_reply_create_index(cmd, index,
+ from->status, smtp_reply_get_enh_code(from));
+ smtp_reply_write(reply->content->text, from);
+
+ i_assert(reply->content != NULL);
+ textbuf = reply->content->text;
+ text = str_c_modifiable(textbuf);
+
+ /* Find the last line */
+ reply->content->last_line = last_line = 0;
+ for (i = 0; i < str_len(textbuf); i++) {
+ if (text[i] == '\n') {
+ reply->content->last_line = last_line;
+ last_line = i + 1;
+ }
+ }
+
+ /* Make this reply suitable for further amendment with
+ smtp_server_reply_add_text() */
+ if ((reply->content->last_line + 3) < str_len(textbuf)) {
+ i_assert(text[reply->content->last_line + 3] == ' ');
+ text[reply->content->last_line + 3] = '-';
+ } else {
+ str_append_c(textbuf, '-');
+ }
+
+ reply->forwarded = TRUE;
+
+ return reply;
+}
+
+void smtp_server_reply_free(struct smtp_server_command *cmd)
+{
+ unsigned int i;
+
+ if (!array_is_created(&cmd->replies))
+ return;
+
+ for (i = 0; i < cmd->replies_expected; i++) {
+ struct smtp_server_reply *reply =
+ array_idx_modifiable(&cmd->replies, i);
+ smtp_server_reply_destroy(reply);
+ }
+}
+
+void smtp_server_reply_add_text(struct smtp_server_reply *reply,
+ const char *text)
+{
+ string_t *textbuf = reply->content->text;
+
+ i_assert(!reply->submitted);
+
+ if (*text == '\0')
+ return;
+
+ do {
+ const char *p;
+
+ reply->content->last_line = str_len(textbuf);
+
+ p = strchr(text, '\n');
+ str_append(textbuf, reply->content->status_prefix);
+ if (p == NULL) {
+ str_append(textbuf, text);
+ text = NULL;
+ } else {
+ if (p > text && *(p-1) == '\r')
+ str_append_data(textbuf, text, p - text - 1);
+ else
+ str_append_data(textbuf, text, p - text);
+ text = p + 1;
+ }
+ str_append(textbuf, "\r\n");
+ } while (text != NULL && *text != '\0');
+}
+
+static size_t
+smtp_server_reply_get_path_len(struct smtp_server_reply *reply)
+{
+ size_t prefix_len = strlen(reply->content->status_prefix);
+ size_t text_len = str_len(reply->content->text), line_len, path_len;
+ const char *text = str_c(reply->content->text);
+ const char *text_end = text + text_len, *line_end;
+
+ i_assert(prefix_len <= text_len);
+
+ line_end = strchr(text, '\r');
+ if (line_end == NULL) {
+ line_end = text_end;
+ line_len = text_len;
+ } else {
+ i_assert(line_end + 1 < text_end);
+ i_assert(*(line_end + 1) == '\n');
+ line_len = line_end - text;
+ }
+
+ if (prefix_len == line_len || text[prefix_len] != '<') {
+ path_len = 0;
+ } else {
+ const char *path_begin = &text[prefix_len], *path_end;
+
+ path_end = strchr(path_begin, '>');
+ if (path_end == NULL || path_end > line_end)
+ path_len = 0;
+ else {
+ i_assert(path_end < line_end);
+ path_end++;
+ path_len = path_end - path_begin;
+ if (path_end < line_end && *path_end != ' ')
+ path_len = 0;
+ }
+ }
+
+ i_assert(prefix_len + path_len <= text_len);
+ return path_len;
+}
+
+void smtp_server_reply_prepend_text(struct smtp_server_reply *reply,
+ const char *text_prefix)
+{
+ const char *text = str_c(reply->content->text);
+ size_t tlen = str_len(reply->content->text), offset;
+
+ i_assert(!reply->sent);
+ i_assert(reply->content != NULL);
+ i_assert(reply->content->text != NULL);
+
+ offset = strlen(reply->content->status_prefix) +
+ smtp_server_reply_get_path_len(reply);
+ i_assert(offset < tlen);
+ if (text[offset] == ' ')
+ offset++;
+
+ str_insert(reply->content->text, offset, text_prefix);
+
+ if (reply->content->last_line > 0)
+ reply->content->last_line += strlen(text_prefix);
+}
+
+void smtp_server_reply_replace_path(struct smtp_server_reply *reply,
+ struct smtp_address *path, bool add)
+{
+ size_t prefix_len, path_len;
+ const char *path_text;
+
+ i_assert(!reply->sent);
+ i_assert(reply->content != NULL);
+ i_assert(reply->content->text != NULL);
+
+ prefix_len = strlen(reply->content->status_prefix);
+ path_len = smtp_server_reply_get_path_len(reply);
+
+ if (path_len > 0) {
+ path_text = smtp_address_encode_path(path);
+ str_replace(reply->content->text, prefix_len, path_len,
+ path_text);
+ } else if (add) {
+ path_text = t_strdup_printf(
+ "<%s> ", smtp_address_encode(path));
+ str_insert(reply->content->text, prefix_len, path_text);
+ }
+}
+
+void smtp_server_reply_submit(struct smtp_server_reply *reply)
+{
+ i_assert(!reply->submitted);
+ i_assert(reply->content != NULL);
+ i_assert(str_len(reply->content->text) >= 5);
+ e_debug(reply->event, "Submitted");
+
+ reply->command->replies_submitted++;
+ reply->submitted = TRUE;
+ smtp_server_command_submit_reply(reply->command);
+}
+
+void smtp_server_reply_submit_duplicate(struct smtp_server_cmd_ctx *_cmd,
+ unsigned int index,
+ unsigned int from_index)
+{
+ struct smtp_server_command *cmd = _cmd->cmd;
+ struct smtp_server_reply *reply, *from_reply;
+
+ i_assert(cmd->replies_expected > 0);
+ i_assert(index < cmd->replies_expected);
+ i_assert(from_index < cmd->replies_expected);
+ i_assert(array_is_created(&cmd->replies));
+
+ from_reply = array_idx_modifiable(&cmd->replies, from_index);
+ i_assert(from_reply->content != NULL);
+ i_assert(from_reply->submitted);
+
+ reply = smtp_server_reply_alloc(cmd, index);
+ reply->index = index;
+ reply->command = cmd;
+ reply->content = from_reply->content;
+ smtp_server_reply_update_event(reply);
+
+ smtp_server_reply_submit(reply);
+}
+
+void smtp_server_reply_indexv(struct smtp_server_cmd_ctx *_cmd,
+ unsigned int index, unsigned int status, const char *enh_code,
+ const char *fmt, va_list args)
+{
+ struct smtp_server_command *cmd = _cmd->cmd;
+ struct smtp_server_reply *reply;
+
+ reply = smtp_server_reply_create_index(cmd, index, status, enh_code);
+ smtp_server_reply_add_text(reply, t_strdup_vprintf(fmt, args));
+ smtp_server_reply_submit(reply);
+}
+
+void smtp_server_reply(struct smtp_server_cmd_ctx *_cmd,
+ unsigned int status, const char *enh_code, const char *fmt, ...)
+{
+ struct smtp_server_command *cmd = _cmd->cmd;
+ va_list args;
+
+ i_assert(cmd->replies_expected <= 1);
+
+ va_start(args, fmt);
+ smtp_server_reply_indexv(_cmd, 0, status, enh_code, fmt, args);
+ va_end(args);
+}
+
+void smtp_server_reply_index(struct smtp_server_cmd_ctx *_cmd,
+ unsigned int index, unsigned int status, const char *enh_code,
+ const char *fmt, ...)
+{
+ va_list args;
+
+ va_start(args, fmt);
+ smtp_server_reply_indexv(_cmd, index, status, enh_code, fmt, args);
+ va_end(args);
+}
+
+void smtp_server_reply_index_forward(struct smtp_server_cmd_ctx *cmd,
+ unsigned int index, const struct smtp_reply *from)
+{
+ smtp_server_reply_submit(
+ smtp_server_reply_create_forward(cmd->cmd, index, from));
+}
+
+void smtp_server_reply_forward(struct smtp_server_cmd_ctx *_cmd,
+ const struct smtp_reply *from)
+{
+ struct smtp_server_command *cmd = _cmd->cmd;
+
+ i_assert(cmd->replies_expected <= 1);
+
+ smtp_server_reply_submit(
+ smtp_server_reply_create_forward(cmd, 0, from));
+}
+
+static void ATTR_FORMAT(4, 0)
+smtp_server_reply_allv(struct smtp_server_cmd_ctx *_cmd,
+ unsigned int status, const char *enh_code,
+ const char *fmt, va_list args)
+{
+ struct smtp_server_command *cmd = _cmd->cmd;
+ struct smtp_server_reply *reply;
+ const char *text;
+ unsigned int first, i = 0;
+
+ /* find the first unsent reply */
+ if (array_is_created(&cmd->replies)) {
+ for (; i < cmd->replies_expected; i++) {
+ struct smtp_server_reply *reply =
+ array_idx_modifiable(&cmd->replies, i);
+ if (!reply->sent)
+ break;
+ }
+ i_assert (i < cmd->replies_expected);
+ }
+ first = i++;
+
+ /* compose the reply text */
+ text = t_strdup_vprintf(fmt, args);
+
+ /* submit the first remaining reply */
+ reply = smtp_server_reply_create_index(cmd, first, status, enh_code);
+ smtp_server_reply_add_text(reply, text);
+ smtp_server_reply_submit(reply);
+
+ /* duplicate the rest from it */
+ for (; i < cmd->replies_expected; i++)
+ smtp_server_reply_submit_duplicate(_cmd, i, first);
+}
+
+void smtp_server_reply_all(struct smtp_server_cmd_ctx *_cmd,
+ unsigned int status, const char *enh_code,
+ const char *fmt, ...)
+{
+ va_list args;
+
+ va_start(args, fmt);
+ smtp_server_reply_allv(_cmd, status, enh_code, fmt, args);
+ va_end(args);
+}
+
+void smtp_server_reply_early(struct smtp_server_cmd_ctx *_cmd,
+ unsigned int status, const char *enh_code,
+ const char *fmt, ...)
+{
+ va_list args;
+
+ _cmd->cmd->reply_early = TRUE;
+
+ va_start(args, fmt);
+ smtp_server_reply_allv(_cmd, status, enh_code, fmt, args);
+ va_end(args);
+}
+
+void smtp_server_reply_quit(struct smtp_server_cmd_ctx *_cmd)
+{
+ struct smtp_server_command *cmd = _cmd->cmd;
+ struct smtp_server_reply *reply;
+
+ reply = smtp_server_reply_create(cmd, 221, "2.0.0");
+ smtp_server_reply_add_text(reply, "Bye");
+ smtp_server_reply_submit(reply);
+}
+
+static void
+smtp_server_reply_write_one_line(const struct smtp_server_reply *reply,
+ string_t *str, bool skip_status)
+{
+ string_t *textbuf;
+ const char *text, *p;
+ size_t text_len, prefix_len, line_len;
+
+ i_assert(reply->content != NULL);
+ textbuf = reply->content->text;
+ i_assert(str_len(textbuf) > 0);
+
+ prefix_len = strlen(reply->content->status_prefix);
+ text = str_c(textbuf);
+ text_len = str_len(textbuf);
+
+ if (skip_status) {
+ i_assert(text_len > prefix_len);
+ text_len -= prefix_len;
+ text += prefix_len;
+ }
+
+ for (;;) {
+ p = strchr(text, '\n');
+ i_assert(p != NULL && p > text && *(p-1) == '\r');
+ str_append_data(str, text, p - text - 1);
+ line_len = (size_t)(p - text) + 1;
+ i_assert(text_len >= line_len);
+ text_len -= line_len;
+ text = p + 1;
+
+ if (text_len <= prefix_len)
+ break;
+
+ text_len -= prefix_len;
+ text += prefix_len;
+ str_append_c(str, ' ');
+ }
+}
+
+const char *
+smtp_server_reply_get_one_line(const struct smtp_server_reply *reply)
+{
+ string_t *str = t_str_new(256);
+
+ smtp_server_reply_write_one_line(reply, str, FALSE);
+ return str_c(str);
+}
+
+const char *
+smtp_server_reply_get_message(const struct smtp_server_reply *reply)
+{
+ string_t *str = t_str_new(256);
+
+ smtp_server_reply_write_one_line(reply, str, TRUE);
+ return str_c(str);
+}
+
+static int smtp_server_reply_send_real(struct smtp_server_reply *reply)
+{
+ struct smtp_server_command *cmd = reply->command;
+ struct smtp_server_connection *conn = cmd->context.conn;
+ struct ostream *output = conn->conn.output;
+ string_t *textbuf;
+ char *text;
+ int ret = 0;
+
+ i_assert(reply->content != NULL);
+ textbuf = reply->content->text;
+ i_assert(str_len(textbuf) > 0);
+
+ /* substitute '-' with ' ' in last line */
+ text = str_c_modifiable(textbuf);
+ text = text + reply->content->last_line + 3;
+ if (text[0] != ' ') {
+ i_assert(text[0] == '-');
+ text[0] = ' ';
+ }
+
+ if (o_stream_send(output, str_data(textbuf), str_len(textbuf)) < 0) {
+ e_debug(reply->event, "Send failed: %s",
+ o_stream_get_disconnect_reason(output));
+ smtp_server_connection_handle_output_error(conn);
+ return -1;
+ }
+
+ e_debug(reply->event, "Sent: %s",
+ smtp_server_reply_get_one_line(reply));
+ return ret;
+}
+
+int smtp_server_reply_send(struct smtp_server_reply *reply)
+{
+ int ret;
+
+ if (reply->sent)
+ return 0;
+
+ T_BEGIN {
+ ret = smtp_server_reply_send_real(reply);
+ } T_END;
+
+ reply->sent = TRUE;
+ return ret;
+}
+
+bool smtp_server_reply_is_success(const struct smtp_server_reply *reply)
+{
+ i_assert(reply->content != NULL);
+ return (reply->content->status / 100 == 2);
+}
+
+void smtp_server_reply_add_to_event(const struct smtp_server_reply *reply,
+ struct event_passthrough *e)
+{
+ i_assert(reply->content != NULL);
+ e->add_int("status_code", reply->content->status);
+ if (reply->content->enhanced_code != NULL &&
+ reply->content->enhanced_code[0] != '\0')
+ e->add_str("enhanced_code", reply->content->enhanced_code);
+ if (!smtp_server_reply_is_success(reply))
+ e->add_str("error", smtp_server_reply_get_message(reply));
+}
+
+/*
+ * EHLO reply
+ */
+
+struct smtp_server_reply *
+smtp_server_reply_create_ehlo(struct smtp_server_command *cmd)
+{
+ struct smtp_server_connection *conn = cmd->context.conn;
+ struct smtp_server_reply *reply;
+ string_t *textbuf;
+
+ reply = smtp_server_reply_create(cmd, 250, "");
+ textbuf = reply->content->text;
+ str_append(textbuf, reply->content->status_prefix);
+ str_append(textbuf, conn->set.hostname);
+ str_append(textbuf, "\r\n");
+
+ return reply;
+}
+
+void smtp_server_reply_ehlo_add(struct smtp_server_reply *reply,
+ const char *keyword)
+{
+ string_t *textbuf;
+
+ i_assert(!reply->submitted);
+ i_assert(reply->content != NULL);
+ textbuf = reply->content->text;
+
+ reply->content->last_line = str_len(textbuf);
+ str_append(textbuf, reply->content->status_prefix);
+ str_append(textbuf, keyword);
+ str_append(textbuf, "\r\n");
+}
+
+void smtp_server_reply_ehlo_add_param(struct smtp_server_reply *reply,
+ const char *keyword, const char *param_fmt, ...)
+{
+ va_list args;
+ string_t *textbuf;
+
+ i_assert(!reply->submitted);
+ i_assert(reply->content != NULL);
+ textbuf = reply->content->text;
+
+ reply->content->last_line = str_len(textbuf);
+ str_append(textbuf, reply->content->status_prefix);
+ str_append(textbuf, keyword);
+ if (*param_fmt != '\0') {
+ va_start(args, param_fmt);
+ str_append_c(textbuf, ' ');
+ str_vprintfa(textbuf, param_fmt, args);
+ va_end(args);
+ }
+ str_append(textbuf, "\r\n");
+}
+
+void smtp_server_reply_ehlo_add_params(struct smtp_server_reply *reply,
+ const char *keyword,
+ const char *const *params)
+{
+ string_t *textbuf;
+
+ i_assert(!reply->submitted);
+ i_assert(reply->content != NULL);
+ textbuf = reply->content->text;
+
+ reply->content->last_line = str_len(textbuf);
+ str_append(textbuf, reply->content->status_prefix);
+ str_append(textbuf, keyword);
+ if (params != NULL) {
+ while (*params != NULL) {
+ str_append_c(textbuf, ' ');
+ str_append(textbuf, *params);
+ params++;
+ }
+ }
+ str_append(textbuf, "\r\n");
+}
+
+void smtp_server_reply_ehlo_add_8bitmime(struct smtp_server_reply *reply)
+{
+ struct smtp_server_cmd_ctx *cmd = &reply->command->context;
+ struct smtp_server_connection *conn = cmd->conn;
+ enum smtp_capability caps = conn->set.capabilities;
+
+ if ((caps & SMTP_CAPABILITY_8BITMIME) == 0)
+ return;
+ smtp_server_reply_ehlo_add(reply, "8BITMIME");
+}
+
+void smtp_server_reply_ehlo_add_binarymime(struct smtp_server_reply *reply)
+{
+ struct smtp_server_cmd_ctx *cmd = &reply->command->context;
+ struct smtp_server_connection *conn = cmd->conn;
+ enum smtp_capability caps = conn->set.capabilities;
+
+ if ((caps & SMTP_CAPABILITY_BINARYMIME) == 0 ||
+ (caps & SMTP_CAPABILITY_CHUNKING) == 0)
+ return;
+ smtp_server_reply_ehlo_add(reply, "BINARYMIME");
+}
+
+void smtp_server_reply_ehlo_add_chunking(struct smtp_server_reply *reply)
+{
+ struct smtp_server_cmd_ctx *cmd = &reply->command->context;
+ struct smtp_server_connection *conn = cmd->conn;
+ enum smtp_capability caps = conn->set.capabilities;
+
+ if ((caps & SMTP_CAPABILITY_CHUNKING) == 0)
+ return;
+ smtp_server_reply_ehlo_add(reply, "CHUNKING");
+}
+
+void smtp_server_reply_ehlo_add_dsn(struct smtp_server_reply *reply)
+{
+ struct smtp_server_cmd_ctx *cmd = &reply->command->context;
+ struct smtp_server_connection *conn = cmd->conn;
+ enum smtp_capability caps = conn->set.capabilities;
+
+ if ((caps & SMTP_CAPABILITY_DSN) == 0)
+ return;
+ smtp_server_reply_ehlo_add(reply, "DSN");
+}
+
+void smtp_server_reply_ehlo_add_enhancedstatuscodes(
+ struct smtp_server_reply *reply)
+{
+ struct smtp_server_cmd_ctx *cmd = &reply->command->context;
+ struct smtp_server_connection *conn = cmd->conn;
+ enum smtp_capability caps = conn->set.capabilities;
+
+ if ((caps & SMTP_CAPABILITY_ENHANCEDSTATUSCODES) == 0)
+ return;
+ smtp_server_reply_ehlo_add(reply, "ENHANCEDSTATUSCODES");
+}
+
+void smtp_server_reply_ehlo_add_pipelining(struct smtp_server_reply *reply)
+{
+ smtp_server_reply_ehlo_add(reply, "PIPELINING");
+}
+
+void smtp_server_reply_ehlo_add_size(struct smtp_server_reply *reply)
+{
+ struct smtp_server_cmd_ctx *cmd = &reply->command->context;
+ struct smtp_server_connection *conn = cmd->conn;
+ enum smtp_capability caps = conn->set.capabilities;
+ uoff_t cap_size = conn->set.max_message_size;
+
+ if ((caps & SMTP_CAPABILITY_SIZE) == 0)
+ return;
+
+ if (cap_size > 0 && cap_size != UOFF_T_MAX) {
+ smtp_server_reply_ehlo_add_param(reply,
+ "SIZE", "%"PRIuUOFF_T, cap_size);
+ } else {
+ smtp_server_reply_ehlo_add(reply, "SIZE");
+ }
+}
+
+void smtp_server_reply_ehlo_add_starttls(struct smtp_server_reply *reply)
+{
+ struct smtp_server_cmd_ctx *cmd = &reply->command->context;
+ struct smtp_server_connection *conn = cmd->conn;
+ enum smtp_capability caps = conn->set.capabilities;
+
+ if ((caps & SMTP_CAPABILITY_STARTTLS) == 0)
+ return;
+ smtp_server_reply_ehlo_add(reply, "STARTTLS");
+}
+
+void smtp_server_reply_ehlo_add_vrfy(struct smtp_server_reply *reply)
+{
+ struct smtp_server_cmd_ctx *cmd = &reply->command->context;
+ struct smtp_server_connection *conn = cmd->conn;
+ enum smtp_capability caps = conn->set.capabilities;
+
+ if ((caps & SMTP_CAPABILITY_VRFY) == 0)
+ return;
+ smtp_server_reply_ehlo_add(reply, "VRFY");
+}
+
+void smtp_server_reply_ehlo_add_xclient(struct smtp_server_reply *reply)
+{
+ static const char *base_fields =
+ "ADDR PORT PROTO HELO LOGIN SESSION TTL TIMEOUT";
+ struct smtp_server_cmd_ctx *cmd = &reply->command->context;
+ struct smtp_server_connection *conn = cmd->conn;
+
+ if (!smtp_server_connection_is_trusted(conn))
+ return;
+ if (conn->set.xclient_extensions == NULL ||
+ *conn->set.xclient_extensions == NULL) {
+ smtp_server_reply_ehlo_add_param(reply, "XCLIENT", "%s",
+ base_fields);
+ return;
+ }
+
+ smtp_server_reply_ehlo_add_param(reply, "XCLIENT", "%s",
+ t_strconcat(base_fields, " ",
+ t_strarray_join(conn->set.xclient_extensions, " "),
+ NULL));
+}
diff --git a/src/lib-smtp/smtp-server-transaction.c b/src/lib-smtp/smtp-server-transaction.c
new file mode 100644
index 0000000..7d9b839
--- /dev/null
+++ b/src/lib-smtp/smtp-server-transaction.c
@@ -0,0 +1,361 @@
+/* Copyright (c) 2013-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "str.h"
+#include "strfuncs.h"
+#include "message-date.h"
+#include "smtp-address.h"
+#include "smtp-params.h"
+
+#include "smtp-server-private.h"
+
+static void
+smtp_server_transaction_update_event(struct smtp_server_transaction *trans)
+{
+ struct event *event = trans->event;
+
+ event_add_str(event, "transaction_id", trans->id);
+ event_add_str(event, "session", trans->id);
+ event_add_str(event, "mail_from",
+ smtp_address_encode(trans->mail_from));
+ event_add_str(event, "mail_from_raw",
+ smtp_address_encode_raw(trans->mail_from));
+ smtp_params_mail_add_to_event(&trans->params, event);
+ event_set_append_log_prefix(event,
+ t_strdup_printf("trans <%s>: ", trans->id));
+}
+
+struct smtp_server_transaction *
+smtp_server_transaction_create(struct smtp_server_connection *conn,
+ const struct smtp_server_cmd_mail *mail_data)
+{
+ struct smtp_server_transaction *trans;
+ pool_t pool;
+
+ /* create new transaction */
+ pool = pool_alloconly_create("smtp server transaction", 4096);
+ trans = p_new(pool, struct smtp_server_transaction, 1);
+ trans->pool = pool;
+ trans->conn = conn;
+
+ /* generate transaction ID */
+ if (conn->transaction_seq++ == 0)
+ trans->id = conn->session_id;
+ else {
+ trans->id = p_strdup_printf(pool, "%s:T%u", conn->session_id,
+ conn->transaction_seq);
+ }
+
+ trans->flags = mail_data->flags;
+ trans->mail_from = smtp_address_clone(trans->pool, mail_data->path);
+ smtp_params_mail_copy(pool, &trans->params, &mail_data->params);
+ trans->timestamp = mail_data->timestamp;
+
+ if (conn->next_trans_event == NULL)
+ trans->event = event_create(conn->event);
+ else {
+ trans->event = conn->next_trans_event;
+ conn->next_trans_event = NULL;
+ }
+ smtp_server_transaction_update_event(trans);
+
+ struct event_passthrough *e =
+ event_create_passthrough(trans->event)->
+ set_name("smtp_server_transaction_started");
+
+ e_debug(e->event(), "Start");
+
+ if (conn->callbacks != NULL &&
+ conn->callbacks->conn_trans_start != NULL)
+ conn->callbacks->conn_trans_start(conn->context, trans);
+
+ return trans;
+}
+
+void smtp_server_transaction_free(struct smtp_server_transaction **_trans)
+{
+ struct smtp_server_transaction *trans = *_trans;
+ struct smtp_server_connection *conn = trans->conn;
+ struct smtp_server_recipient **rcpts;
+ unsigned int rcpts_total, rcpts_aborted, rcpts_failed;
+ unsigned int rcpts_count, i;
+
+ *_trans = NULL;
+
+ if (conn->callbacks != NULL &&
+ conn->callbacks->conn_trans_free != NULL)
+ conn->callbacks->conn_trans_free(conn->context, trans);
+
+ rcpts_count = 0;
+ if (array_is_created(&trans->rcpt_to))
+ rcpts = array_get_modifiable(&trans->rcpt_to, &rcpts_count);
+
+ rcpts_aborted = rcpts_count + conn->state.pending_rcpt_cmds;
+ rcpts_failed = conn->state.denied_rcpt_cmds;
+ rcpts_total = rcpts_aborted + rcpts_failed;
+
+ for (i = 0; i < rcpts_count; i++)
+ smtp_server_recipient_destroy(&rcpts[i]);
+
+ if (!trans->finished) {
+ struct event_passthrough *e =
+ event_create_passthrough(trans->event)->
+ set_name("smtp_server_transaction_finished")->
+ add_int("recipients", rcpts_total)->
+ add_int("recipients_denied", rcpts_failed)->
+ add_int("recipients_aborted", rcpts_aborted)->
+ add_int("recipients_failed", rcpts_failed)->
+ add_int("recipients_succeeded", 0);
+ e->add_int("status_code", 9000);
+ e->add_str("enhanced_code", "9.0.0");
+ e->add_str("error", "Aborted");
+
+ e_debug(e->event(), "Aborted");
+ }
+
+ event_unref(&trans->event);
+ pool_unref(&trans->pool);
+}
+
+struct smtp_server_recipient *
+smtp_server_transaction_find_rcpt_duplicate(
+ struct smtp_server_transaction *trans,
+ struct smtp_server_recipient *rcpt)
+{
+ struct smtp_server_recipient *drcpt;
+
+ i_assert(array_is_created(&trans->rcpt_to));
+ array_foreach_elem(&trans->rcpt_to, drcpt) {
+ if (drcpt == rcpt)
+ continue;
+ if (smtp_address_equals(drcpt->path, rcpt->path) &&
+ smtp_params_rcpt_equal(&drcpt->params, &rcpt->params))
+ return drcpt;
+ }
+ return NULL;
+}
+
+void smtp_server_transaction_add_rcpt(struct smtp_server_transaction *trans,
+ struct smtp_server_recipient *rcpt)
+{
+ if (!array_is_created(&trans->rcpt_to))
+ p_array_init(&trans->rcpt_to, trans->pool, 8);
+
+ rcpt->trans = trans;
+ rcpt->index = array_count(&trans->rcpt_to);
+
+ array_push_back(&trans->rcpt_to, &rcpt);
+}
+
+bool smtp_server_transaction_has_rcpt(struct smtp_server_transaction *trans)
+{
+ return (array_is_created(&trans->rcpt_to) &&
+ array_count(&trans->rcpt_to) > 0);
+}
+
+unsigned int
+smtp_server_transaction_rcpt_count(struct smtp_server_transaction *trans)
+{
+ if (!array_is_created(&trans->rcpt_to))
+ return 0;
+ return array_count(&trans->rcpt_to);
+}
+
+void smtp_server_transaction_data_command(struct smtp_server_transaction *trans,
+ struct smtp_server_cmd_ctx *cmd)
+{
+ struct smtp_server_recipient *rcpt;
+
+ trans->cmd = cmd;
+
+ if (!array_is_created(&trans->rcpt_to))
+ return;
+ array_foreach_elem(&trans->rcpt_to, rcpt)
+ smtp_server_recipient_data_command(rcpt, cmd);
+}
+
+void smtp_server_transaction_received(struct smtp_server_transaction *trans,
+ uoff_t data_size)
+{
+ event_add_int(trans->event, "data_size", data_size);
+}
+
+void smtp_server_transaction_reset(struct smtp_server_transaction *trans)
+{
+ struct smtp_server_connection *conn = trans->conn;
+ struct smtp_server_recipient *const *rcpts = NULL;
+ unsigned int rcpts_total, rcpts_failed, rcpts_aborted;
+ unsigned int rcpts_count, i;
+
+ i_assert(!trans->finished);
+ trans->finished = TRUE;
+
+ rcpts_count = 0;
+ if (array_is_created(&trans->rcpt_to))
+ rcpts = array_get(&trans->rcpt_to, &rcpts_count);
+
+ rcpts_aborted = rcpts_count + conn->state.pending_rcpt_cmds;
+ rcpts_failed = conn->state.denied_rcpt_cmds;
+ rcpts_total = rcpts_aborted + rcpts_failed;
+
+ for (i = 0; i < rcpts_count; i++)
+ smtp_server_recipient_reset(rcpts[i]);
+
+ struct event_passthrough *e =
+ event_create_passthrough(trans->event)->
+ set_name("smtp_server_transaction_finished")->
+ add_int("recipients", rcpts_total)->
+ add_int("recipients_denied", rcpts_failed)->
+ add_int("recipients_aborted", rcpts_aborted)->
+ add_int("recipients_failed", rcpts_failed)->
+ add_int("recipients_succeeded", 0)->
+ add_str("is_reset", "yes");
+ e_debug(e->event(), "Finished");
+}
+
+void smtp_server_transaction_finished(struct smtp_server_transaction *trans,
+ struct smtp_server_cmd_ctx *cmd)
+{
+ struct smtp_server_connection *conn = trans->conn;
+ struct smtp_server_recipient *const *rcpts = NULL;
+ const struct smtp_server_reply *trans_reply = NULL;
+ unsigned int rcpts_total, rcpts_denied, rcpts_failed, rcpts_succeeded;
+ unsigned int rcpts_count, i;
+
+ i_assert(conn->state.pending_rcpt_cmds == 0);
+ i_assert(!trans->finished);
+ trans->finished = TRUE;
+
+ rcpts_count = 0;
+ if (array_is_created(&trans->rcpt_to))
+ rcpts = array_get(&trans->rcpt_to, &rcpts_count);
+
+ rcpts_succeeded = 0;
+ rcpts_denied = conn->state.denied_rcpt_cmds;
+ rcpts_failed = conn->state.denied_rcpt_cmds;
+ rcpts_total = rcpts_count + conn->state.denied_rcpt_cmds;
+ for (i = 0; i < rcpts_count; i++) {
+ struct smtp_server_reply *reply;
+
+ if ((trans->flags &
+ SMTP_SERVER_TRANSACTION_FLAG_REPLY_PER_RCPT) != 0)
+ reply = smtp_server_command_get_reply(cmd->cmd, i);
+ else
+ reply = smtp_server_command_get_reply(cmd->cmd, 0);
+ smtp_server_recipient_finished(rcpts[i], reply);
+
+ if (smtp_server_reply_is_success(reply))
+ rcpts_succeeded++;
+ else {
+ rcpts_failed++;
+ if (trans_reply == NULL)
+ trans_reply = reply;
+ }
+ }
+
+ if (trans_reply == NULL) {
+ /* record first success reply in transaction */
+ trans_reply = smtp_server_command_get_reply(cmd->cmd, 0);
+ }
+
+ struct event_passthrough *e =
+ event_create_passthrough(trans->event)->
+ set_name("smtp_server_transaction_finished")->
+ add_int("recipients", rcpts_total)->
+ add_int("recipients_denied", rcpts_denied)->
+ add_int("recipients_aborted", 0)->
+ add_int("recipients_failed", rcpts_failed)->
+ add_int("recipients_succeeded", rcpts_succeeded);
+ smtp_server_reply_add_to_event(trans_reply, e);
+
+ e_debug(e->event(), "Finished");
+}
+
+void smtp_server_transaction_fail_data(struct smtp_server_transaction *trans,
+ struct smtp_server_cmd_ctx *data_cmd,
+ unsigned int status, const char *enh_code,
+ const char *fmt, va_list args)
+{
+ struct smtp_server_recipient *const *rcpts;
+ const char *msg;
+ unsigned int count, i;
+
+ msg = t_strdup_vprintf(fmt, args);
+ rcpts = array_get(&trans->rcpt_to, &count);
+ for (i = 0; i < count; i++) {
+ smtp_server_reply_index(data_cmd, i,
+ status, enh_code, "<%s> %s",
+ smtp_address_encode(rcpts[i]->path), msg);
+ }
+}
+
+void smtp_server_transaction_write_trace_record(
+ string_t *str, struct smtp_server_transaction *trans,
+ enum smtp_server_trace_rcpt_to_address rcpt_to_address)
+{
+ struct smtp_server_connection *conn = trans->conn;
+ const struct smtp_server_helo_data *helo_data = &conn->helo;
+ const char *host, *secstr, *rcpt_to = NULL;
+
+ if (array_count(&trans->rcpt_to) == 1) {
+ struct smtp_server_recipient *const *rcpts =
+ array_front(&trans->rcpt_to);
+
+ switch (rcpt_to_address) {
+ case SMTP_SERVER_TRACE_RCPT_TO_ADDRESS_NONE:
+ break;
+ case SMTP_SERVER_TRACE_RCPT_TO_ADDRESS_FINAL:
+ rcpt_to = smtp_address_encode(rcpts[0]->path);
+ break;
+ case SMTP_SERVER_TRACE_RCPT_TO_ADDRESS_ORIGINAL:
+ rcpt_to = smtp_address_encode(
+ smtp_server_recipient_get_original(rcpts[0]));
+ break;
+ }
+ }
+
+ /* from */
+ str_append(str, "Received: from ");
+ if (helo_data->domain_valid)
+ str_append(str, helo_data->domain);
+ else
+ str_append(str, "unknown");
+ host = "";
+ if (conn->conn.remote_ip.family != 0)
+ host = net_ip2addr(&conn->conn.remote_ip);
+ if (host[0] != '\0') {
+ str_append(str, " ([");
+ str_append(str, host);
+ str_append(str, "])");
+ }
+ /* (using) */
+ secstr = smtp_server_connection_get_security_string(conn);
+ if (secstr != NULL) {
+ str_append(str, "\r\n\t(using ");
+ str_append(str, secstr);
+ str_append(str, ")");
+ }
+ /* by, with */
+ str_append(str, "\r\n\tby ");
+ str_append(str, conn->set.hostname);
+ str_append(str, " with ");
+ str_append(str, smtp_server_connection_get_protocol_name(conn));
+ /* id */
+ str_append(str, "\r\n\tid ");
+ str_append(str, trans->id);
+ /* (envelope-from) */
+ str_append(str, "\r\n\t(envelope-from <");
+ smtp_address_write(str, trans->mail_from);
+ str_append(str, ">)");
+ /* for */
+ if (rcpt_to != NULL) {
+ str_append(str, "\r\n\tfor <");
+ str_append(str, rcpt_to);
+ str_append(str, ">");
+ }
+ str_append(str, "; ");
+ /* date */
+ str_append(str, message_date_create(trans->timestamp.tv_sec));
+ str_printfa(str, "\r\n");
+}
diff --git a/src/lib-smtp/smtp-server.c b/src/lib-smtp/smtp-server.c
new file mode 100644
index 0000000..e0afde3
--- /dev/null
+++ b/src/lib-smtp/smtp-server.c
@@ -0,0 +1,153 @@
+/* Copyright (c) 2013-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "net.h"
+#include "str.h"
+#include "hash.h"
+#include "hostpid.h"
+#include "array.h"
+#include "ioloop.h"
+#include "istream.h"
+#include "ostream.h"
+#include "connection.h"
+#include "dns-lookup.h"
+#include "iostream-rawlog.h"
+#include "iostream-ssl.h"
+
+#include "smtp-server-private.h"
+
+static struct event_category event_category_smtp_server = {
+ .name = "smtp-server"
+};
+
+/*
+ * Server
+ */
+
+struct smtp_server *smtp_server_init(const struct smtp_server_settings *set)
+{
+ struct smtp_server *server;
+ pool_t pool;
+
+ pool = pool_alloconly_create("smtp server", 1024);
+ server = p_new(pool, struct smtp_server, 1);
+ server->pool = pool;
+ server->set.protocol = set->protocol;
+ server->set.reason_code_module =
+ p_strdup(pool, set->reason_code_module);
+ server->set.rawlog_dir = p_strdup_empty(pool, set->rawlog_dir);
+
+ if (set->ssl != NULL) {
+ server->set.ssl =
+ ssl_iostream_settings_dup(server->pool, set->ssl);
+ }
+
+ if (set->hostname != NULL && *set->hostname != '\0')
+ server->set.hostname = p_strdup(pool, set->hostname);
+ else
+ server->set.hostname = p_strdup(pool, my_hostdomain());
+ if (set->login_greeting != NULL && *set->login_greeting != '\0')
+ server->set.login_greeting = p_strdup(pool, set->login_greeting);
+ else
+ server->set.login_greeting = PACKAGE_NAME" ready.";
+ if (set->capabilities == 0) {
+ server->set.capabilities = SMTP_SERVER_DEFAULT_CAPABILITIES;
+ } else {
+ server->set.capabilities = set->capabilities;
+ }
+ server->set.workarounds = set->workarounds;
+ server->set.max_client_idle_time_msecs = set->max_client_idle_time_msecs;
+ server->set.max_pipelined_commands = (set->max_pipelined_commands > 0 ?
+ set->max_pipelined_commands : 1);
+ server->set.max_bad_commands = (set->max_bad_commands > 0 ?
+ set->max_bad_commands : SMTP_SERVER_DEFAULT_MAX_BAD_COMMANDS);
+ server->set.max_recipients = set->max_recipients;
+ server->set.command_limits = set->command_limits;
+ server->set.max_message_size = set->max_message_size;
+
+ if (set->mail_param_extensions != NULL) {
+ server->set.mail_param_extensions =
+ p_strarray_dup(pool, set->mail_param_extensions);
+ }
+ if (set->rcpt_param_extensions != NULL) {
+ server->set.rcpt_param_extensions =
+ p_strarray_dup(pool, set->rcpt_param_extensions);
+ }
+ if (set->xclient_extensions != NULL) {
+ server->set.xclient_extensions =
+ p_strarray_dup(pool, set->xclient_extensions);
+ }
+
+ server->set.socket_send_buffer_size = set->socket_send_buffer_size;
+ server->set.socket_recv_buffer_size = set->socket_recv_buffer_size;
+
+ server->set.tls_required = set->tls_required;
+ server->set.auth_optional = set->auth_optional;
+ server->set.rcpt_domain_optional = set->rcpt_domain_optional;
+ server->set.mail_path_allow_broken = set->mail_path_allow_broken;
+ server->set.no_greeting = set->no_greeting;
+ server->set.debug = set->debug;
+ server->set.no_state_in_reason = set->no_state_in_reason;
+
+ /* There is no event log prefix added here, since the server itself does
+ not log anything. */
+ server->event = event_create(set->event_parent);
+ smtp_server_event_init(server, server->event);
+ event_set_forced_debug(server->event, set->debug);
+
+ server->conn_list = smtp_server_connection_list_init();
+ smtp_server_commands_init(server);
+ return server;
+}
+
+void smtp_server_event_init(struct smtp_server *server, struct event *event)
+{
+ event_add_category(event, &event_category_smtp_server);
+ event_add_str(event, "protocol",
+ smtp_protocol_name(server->set.protocol));
+}
+
+void smtp_server_deinit(struct smtp_server **_server)
+{
+ struct smtp_server *server = *_server;
+
+ connection_list_deinit(&server->conn_list);
+
+ if (server->ssl_ctx != NULL)
+ ssl_iostream_context_unref(&server->ssl_ctx);
+ event_unref(&server->event);
+ pool_unref(&server->pool);
+ *_server = NULL;
+}
+
+void smtp_server_switch_ioloop(struct smtp_server *server)
+{
+ struct connection *_conn = server->conn_list->connections;
+
+ /* move connections */
+ /* FIXME: we wouldn't necessarily need to switch all of them
+ immediately, only those that have commands now. but also connections
+ that get new commands before ioloop is switched again.. */
+ for (; _conn != NULL; _conn = _conn->next) {
+ struct smtp_server_connection *conn =
+ (struct smtp_server_connection *)_conn;
+
+ smtp_server_connection_switch_ioloop(conn);
+ }
+}
+
+int smtp_server_init_ssl_ctx(struct smtp_server *server, const char **error_r)
+{
+ const char *error;
+
+ if (server->ssl_ctx != NULL || server->set.ssl == NULL)
+ return 0;
+
+ if (ssl_iostream_server_context_cache_get(server->set.ssl,
+ &server->ssl_ctx, &error) < 0) {
+ *error_r = t_strdup_printf("Couldn't initialize SSL context: %s",
+ error);
+ return -1;
+ }
+ return 0;
+}
diff --git a/src/lib-smtp/smtp-server.h b/src/lib-smtp/smtp-server.h
new file mode 100644
index 0000000..5c13491
--- /dev/null
+++ b/src/lib-smtp/smtp-server.h
@@ -0,0 +1,802 @@
+#ifndef SMTP_SERVER_H
+#define SMTP_SERVER_H
+
+#include "smtp-common.h"
+#include "smtp-command.h"
+#include "smtp-params.h"
+
+struct smtp_address;
+struct smtp_reply;
+struct smtp_command;
+
+struct smtp_server_helo_data;
+
+struct smtp_server_esmtp_param;
+struct smtp_server_cmd_ehlo;
+struct smtp_server_cmd_mail;
+struct smtp_server_cmd_ctx;
+struct smtp_server_command;
+struct smtp_server_reply;
+struct smtp_server_recipient;
+struct smtp_server_transaction;
+
+struct smtp_server;
+
+/*
+ * Types
+ */
+
+enum smtp_server_state {
+ SMTP_SERVER_STATE_GREETING = 0,
+ SMTP_SERVER_STATE_XCLIENT,
+ SMTP_SERVER_STATE_HELO,
+ SMTP_SERVER_STATE_STARTTLS,
+ SMTP_SERVER_STATE_AUTH,
+ SMTP_SERVER_STATE_READY,
+ SMTP_SERVER_STATE_MAIL_FROM,
+ SMTP_SERVER_STATE_RCPT_TO,
+ SMTP_SERVER_STATE_DATA,
+};
+extern const char *const smtp_server_state_names[];
+
+struct smtp_server_helo_data {
+ const char *domain;
+
+ bool domain_valid:1; /* Valid domain/literal specified */
+ bool old_smtp:1; /* Client sent HELO rather than EHLO */
+};
+
+/*
+ * Recipient
+ */
+
+enum smtp_server_recipient_hook_type {
+ /* approved: the server is about to approve this recipient by sending
+ a success reply to the RCPT command. */
+ SMTP_SERVER_RECIPIENT_HOOK_APPROVED,
+ /* data_replied: the DATA command is replied for this recipient */
+ SMTP_SERVER_RECIPIENT_HOOK_DATA_REPLIED,
+ /* destroy: recipient is about to be destroyed. */
+ SMTP_SERVER_RECIPIENT_HOOK_DESTROY
+};
+
+typedef void smtp_server_rcpt_func_t(struct smtp_server_recipient *rcpt,
+ void *context);
+
+struct smtp_server_recipient {
+ pool_t pool;
+ struct smtp_server_connection *conn;
+ struct smtp_server_transaction *trans;
+ struct event *event;
+
+ struct smtp_address *path;
+ struct smtp_params_rcpt params;
+
+ /* The associated RCPT or DATA command (whichever applies). This is NULL
+ when no command is active. */
+ struct smtp_server_cmd_ctx *cmd;
+
+ /* The index in the list of approved recipients */
+ unsigned int index;
+
+ void *context;
+
+ bool replied:1;
+ bool finished:1;
+};
+ARRAY_DEFINE_TYPE(smtp_server_recipient, struct smtp_server_recipient *);
+
+/* Returns the original recipient path if available. Otherwise, it returns the
+ final path. */
+const struct smtp_address *
+smtp_server_recipient_get_original(struct smtp_server_recipient *rcpt);
+
+struct smtp_server_reply *
+smtp_server_recipient_get_reply(struct smtp_server_recipient *rcpt);
+bool smtp_server_recipient_is_replied(struct smtp_server_recipient *rcpt);
+void smtp_server_recipient_replyv(struct smtp_server_recipient *rcpt,
+ unsigned int status, const char *enh_code,
+ const char *fmt, va_list args)
+ ATTR_FORMAT(4, 0);
+void smtp_server_recipient_reply(struct smtp_server_recipient *rcpt,
+ unsigned int status, const char *enh_code,
+ const char *fmt, ...) ATTR_FORMAT(4, 5);
+void smtp_server_recipient_reply_forward(struct smtp_server_recipient *rcpt,
+ const struct smtp_reply *from);
+
+/* Hooks */
+
+void smtp_server_recipient_add_hook(struct smtp_server_recipient *rcpt,
+ enum smtp_server_recipient_hook_type type,
+ smtp_server_rcpt_func_t func,
+ void *context);
+#define smtp_server_recipient_add_hook(_rcpt, _type, _func, _context) \
+ smtp_server_recipient_add_hook((_rcpt), (_type) - \
+ CALLBACK_TYPECHECK(_func, void (*)( \
+ struct smtp_server_recipient *, typeof(_context))), \
+ (smtp_server_rcpt_func_t *)(_func), (_context))
+void smtp_server_recipient_remove_hook(
+ struct smtp_server_recipient *rcpt,
+ enum smtp_server_recipient_hook_type type,
+ smtp_server_rcpt_func_t *func);
+#define smtp_server_recipient_remove_hook(_rcpt, _type, _func) \
+ smtp_server_recipient_remove_hook((_rcpt), (_type), \
+ (smtp_server_rcpt_func_t *)(_func));
+
+/*
+ * Transaction
+ */
+
+enum smtp_server_trace_rcpt_to_address {
+ /* Don't add recipient address to trace header. */
+ SMTP_SERVER_TRACE_RCPT_TO_ADDRESS_NONE,
+ /* Add final recipient address to trace header. */
+ SMTP_SERVER_TRACE_RCPT_TO_ADDRESS_FINAL,
+ /* Add original recipient address to trace header. */
+ SMTP_SERVER_TRACE_RCPT_TO_ADDRESS_ORIGINAL,
+};
+
+enum smtp_server_transaction_flags {
+ SMTP_SERVER_TRANSACTION_FLAG_REPLY_PER_RCPT = BIT(0),
+};
+
+struct smtp_server_transaction {
+ pool_t pool;
+ struct smtp_server_connection *conn;
+ struct event *event;
+ const char *id;
+ struct timeval timestamp;
+
+ enum smtp_server_transaction_flags flags;
+
+ struct smtp_address *mail_from;
+ struct smtp_params_mail params;
+ ARRAY_TYPE(smtp_server_recipient) rcpt_to;
+
+ /* The associated DATA command. This is NULL until the last DATA/BDAT
+ command is issued.
+ */
+ struct smtp_server_cmd_ctx *cmd;
+
+ void *context;
+
+ bool finished:1;
+};
+
+struct smtp_server_recipient *
+smtp_server_transaction_find_rcpt_duplicate(
+ struct smtp_server_transaction *trans,
+ struct smtp_server_recipient *rcpt);
+
+void smtp_server_transaction_fail_data(struct smtp_server_transaction *trans,
+ struct smtp_server_cmd_ctx *data_cmd,
+ unsigned int status,
+ const char *enh_code,
+ const char *fmt, va_list args)
+ ATTR_FORMAT(5, 0);
+
+void smtp_server_transaction_write_trace_record(
+ string_t *str, struct smtp_server_transaction *trans,
+ enum smtp_server_trace_rcpt_to_address rcpt_to_address);
+
+/*
+ * Callbacks
+ */
+
+struct smtp_server_cmd_helo {
+ struct smtp_server_helo_data helo;
+
+ bool first:1; /* This is the first */
+ bool changed:1; /* This EHLO/HELO/LHLO is the first or different
+ from a previous one */
+};
+
+struct smtp_server_cmd_mail {
+ struct smtp_address *path;
+ struct smtp_params_mail params;
+
+ struct timeval timestamp;
+
+ enum smtp_server_transaction_flags flags;
+};
+
+struct smtp_server_cmd_auth {
+ const char *sasl_mech;
+ const char *initial_response;
+};
+
+struct smtp_server_callbacks {
+ /* Command callbacks:
+
+ These are used to override/implement the behavior of the various core
+ SMTP commands. Commands are handled asynchronously, which means that
+ the command is not necessarily finished when the callback ends. A
+ command is finished either when 1 is returned or a reply is submitted
+ for it. When a callback returns 0, the command implementation is
+ waiting for an external event and when it returns -1 an error
+ occurred. When 1 is returned, a default success reply is set if no
+ reply was submitted. Not submitting an error reply when -1 is
+ returned causes an assert fail. Except for RCPT and DATA, all these
+ callbacks are optional to implement; appropriate default behavior is
+ provided.
+
+ The SMTP server API takes care of transaction state checking.
+ However, until all previous commands are handled, a transaction
+ command cannot rely on the transaction state being final. Use
+ cmd->hook_next to get notified when all previous commands are
+ finished and the current command is next in line to reply.
+
+ If the implementation does not need asynchronous behavior, set
+ max_pipelined_commands=1 and don't return 0 from any command handler.
+ */
+
+ /* HELO/EHLO/LHLO */
+ int (*conn_cmd_helo)(void *conn_ctx, struct smtp_server_cmd_ctx *cmd,
+ struct smtp_server_cmd_helo *data);
+ /* STARTTLS */
+ int (*conn_cmd_starttls)(void *conn_ctx,
+ struct smtp_server_cmd_ctx *cmd);
+ /* AUTH */
+ int (*conn_cmd_auth)(void *conn_ctx, struct smtp_server_cmd_ctx *cmd,
+ struct smtp_server_cmd_auth *data);
+ int (*conn_cmd_auth_continue)(void *conn_ctx,
+ struct smtp_server_cmd_ctx *cmd,
+ const char *response);
+ /* MAIL */
+ int (*conn_cmd_mail)(void *conn_ctx, struct smtp_server_cmd_ctx *cmd,
+ struct smtp_server_cmd_mail *data);
+ /* RCPT */
+ int (*conn_cmd_rcpt)(void *conn_ctx, struct smtp_server_cmd_ctx *cmd,
+ struct smtp_server_recipient *rcpt);
+ /* RSET */
+ int (*conn_cmd_rset)(void *conn_ctx, struct smtp_server_cmd_ctx *cmd);
+ /* DATA */
+ int (*conn_cmd_data_begin)(void *conn_ctx,
+ struct smtp_server_cmd_ctx *cmd,
+ struct smtp_server_transaction *trans,
+ struct istream *data_input);
+ int (*conn_cmd_data_continue)(void *conn_ctx,
+ struct smtp_server_cmd_ctx *cmd,
+ struct smtp_server_transaction *trans);
+ /* VRFY */
+ int (*conn_cmd_vrfy)(void *conn_ctx, struct smtp_server_cmd_ctx *cmd,
+ const char *param);
+ /* NOOP */
+ int (*conn_cmd_noop)(void *conn_ctx, struct smtp_server_cmd_ctx *cmd);
+ /* QUIT */
+ int (*conn_cmd_quit)(void *conn_ctx, struct smtp_server_cmd_ctx *cmd);
+ /* XCLIENT */
+ void (*conn_cmd_xclient)(void *conn_ctx,
+ struct smtp_server_cmd_ctx *cmd,
+ struct smtp_proxy_data *data);
+
+ /* Command input callbacks:
+
+ These can be used to do stuff before and after a pipelined group of
+ commands is read.
+ */
+ void (*conn_cmd_input_pre)(void *context);
+ void (*conn_cmd_input_post)(void *context);
+
+ /* Transaction events */
+ void (*conn_trans_start)(void *context,
+ struct smtp_server_transaction *trans);
+ void (*conn_trans_free)(void *context,
+ struct smtp_server_transaction *trans);
+
+ /* Protocol state events */
+ void (*conn_state_changed)(void *context,
+ enum smtp_server_state new_state,
+ const char *new_args) ATTR_NULL(3);
+
+ /* Proxy data */
+ void (*conn_proxy_data_updated)(void *conn_ctx,
+ const struct smtp_proxy_data *data);
+
+ /* Connection */
+ int (*conn_start_tls)(void *conn_ctx,
+ struct istream **input, struct ostream **output);
+ /* Connection is disconnected. This is always called before
+ conn_free(). */
+ void (*conn_disconnect)(void *context, const char *reason);
+ /* The last reference to connection is dropped, causing the connection
+ to be freed. */
+ void (*conn_free)(void *context);
+
+ /* Security */
+ bool (*conn_is_trusted)(void *context);
+};
+
+/*
+ * Server
+ */
+
+enum smtp_server_workarounds {
+ SMTP_SERVER_WORKAROUND_WHITESPACE_BEFORE_PATH = BIT(0),
+ SMTP_SERVER_WORKAROUND_MAILBOX_FOR_PATH = BIT(1)
+};
+
+struct smtp_server_settings {
+ /* The protocol we are serving */
+ enum smtp_protocol protocol;
+ /* Standard capabilities supported by the server */
+ enum smtp_capability capabilities;
+ /* Enabled workarounds for client protocol deviations */
+ enum smtp_server_workarounds workarounds;
+
+ /* Module name for event reason codes. */
+ const char *reason_code_module;
+ /* Our hostname as presented to the client */
+ const char *hostname;
+ /* The message sent in the SMTP server greeting */
+ const char *login_greeting;
+ /* The directory that - if it exists and is accessible - is used to
+ write raw protocol logs for debugging */
+ const char *rawlog_dir;
+
+ /* SSL settings; if NULL, master_service_ssl_init() is used instead */
+ const struct ssl_iostream_settings *ssl;
+
+ /* The maximum time in milliseconds a client is allowed to be idle
+ before it is disconnected. */
+ unsigned int max_client_idle_time_msecs;
+
+ /* Maximum number of commands in pipeline per connection (default = 1)
+ */
+ unsigned int max_pipelined_commands;
+
+ /* Maximum number of sequential bad commands */
+ unsigned int max_bad_commands;
+
+ /* Maximum number of recipients in a transaction
+ (0 means unlimited, which is the default) */
+ unsigned int max_recipients;
+
+ /* Command limits */
+ struct smtp_command_limits command_limits;
+
+ /* Message size limit */
+ uoff_t max_message_size;
+
+ /* Accept these additional custom MAIL parameters */
+ const char *const *mail_param_extensions;
+ /* Accept these additional custom RCPT parameters */
+ const char *const *rcpt_param_extensions;
+ /* Accept these additional custom XCLIENT fields */
+ const char *const *xclient_extensions;
+
+ /* The kernel send/receive buffer sizes used for the connection sockets.
+ Configuring this is mainly useful for the test suite. The kernel
+ defaults are used when these settings are 0. */
+ size_t socket_send_buffer_size;
+ size_t socket_recv_buffer_size;
+
+ /* Event to use for the smtp server. */
+ struct event *event_parent;
+
+ /* Enable logging debug messages */
+ bool debug:1;
+ /* Authentication is not required for this service */
+ bool auth_optional:1;
+ /* TLS security is required for this service */
+ bool tls_required:1;
+ /* The path provided to the MAIL command does not need to be valid. A
+ completely invalid path will parse as <>. Paths that can still be
+ fixed by splitting it on the last `@' yielding a usable localpart and
+ domain, will be parsed as such. There are limits though; when the
+ path is badly delimited or contains control characters, the MAIL
+ command will still fail. The unparsed broken address will be
+ available in the `raw' field of struct smtp_address for logging etc.
+ */
+ bool mail_path_allow_broken:1;
+ /* The path provided to the RCPT command does not need to have the
+ domain part. */
+ bool rcpt_domain_optional:1;
+ /* Don't include "(state=%s)" in the disconnection reason string. */
+ bool no_state_in_reason:1;
+ /* Don't send a greeting or login success message to the client upon
+ connection start. */
+ bool no_greeting:1;
+};
+
+struct smtp_server_stats {
+ unsigned int command_count, reply_count;
+ uoff_t input, output;
+};
+
+/*
+ * Server
+ */
+
+struct smtp_server *smtp_server_init(const struct smtp_server_settings *set);
+void smtp_server_deinit(struct smtp_server **_server);
+
+void smtp_server_switch_ioloop(struct smtp_server *server);
+
+/*
+ * Connection
+ */
+
+/* Create connection. It is still inactive and needs to be started with
+ one of the functions below. */
+struct smtp_server_connection *
+smtp_server_connection_create(
+ struct smtp_server *server, int fd_in, int fd_out,
+ const struct ip_addr *remote_ip, in_port_t remote_port, bool ssl_start,
+ const struct smtp_server_settings *set,
+ const struct smtp_server_callbacks *callbacks, void *context)
+ ATTR_NULL(4, 6, 8);
+struct smtp_server_connection *
+smtp_server_connection_create_from_streams(
+ struct smtp_server *server,
+ struct istream *input, struct ostream *output,
+ const struct ip_addr *remote_ip, in_port_t remote_port,
+ const struct smtp_server_settings *set,
+ const struct smtp_server_callbacks *callbacks, void *context)
+ ATTR_NULL(4, 6, 8);
+
+void smtp_server_connection_ref(struct smtp_server_connection *conn);
+bool smtp_server_connection_unref(struct smtp_server_connection **_conn);
+
+/* Initialize the connection with state and data from login service */
+void smtp_server_connection_login(struct smtp_server_connection *conn,
+ const char *username, const char *helo,
+ const unsigned char *pdata,
+ unsigned int pdata_len, bool ssl_secured);
+
+/* Start the connection. Establishes SSL layer immediately if instructed,
+ and sends the greeting once the connection is ready for commands. */
+void smtp_server_connection_start(struct smtp_server_connection *conn);
+/* Start the connection, but only establish SSL layer and send greeting;
+ handling command input is held off until smtp_server_connection_resume() is
+ called. */
+void smtp_server_connection_start_pending(struct smtp_server_connection *conn);
+/* Abort the connection prematurely (before it is started). */
+void smtp_server_connection_abort(struct smtp_server_connection **_conn,
+ unsigned int status, const char *enh_code,
+ const char *reason);
+
+/* Halt connection command input and idle timeout entirely. */
+void smtp_server_connection_halt(struct smtp_server_connection *conn);
+/* Resume connection command input and idle timeout. */
+void smtp_server_connection_resume(struct smtp_server_connection *conn);
+
+void smtp_server_connection_input_lock(struct smtp_server_connection *conn);
+void smtp_server_connection_input_unlock(struct smtp_server_connection *conn);
+
+void smtp_server_connection_set_streams(struct smtp_server_connection *conn,
+ struct istream *input,
+ struct ostream *output);
+void smtp_server_connection_set_ssl_streams(struct smtp_server_connection *conn,
+ struct istream *input,
+ struct ostream *output);
+
+void smtp_server_connection_close(struct smtp_server_connection **_conn,
+ const char *reason) ATTR_NULL(2);
+void smtp_server_connection_terminate(struct smtp_server_connection **_conn,
+ const char *enh_code, const char *reason)
+ ATTR_NULL(3);
+
+bool smtp_server_connection_data_check_state(struct smtp_server_cmd_ctx *cmd);
+void smtp_server_connection_data_chunk_init(struct smtp_server_cmd_ctx *cmd);
+int smtp_server_connection_data_chunk_add(struct smtp_server_cmd_ctx *cmd,
+ struct istream *chunk,
+ uoff_t chunk_size, bool chunk_last,
+ bool client_input);
+
+enum smtp_server_state
+smtp_server_connection_get_state(struct smtp_server_connection *conn,
+ const char **args_r) ATTR_NULL(2);
+const char *
+smtp_server_connection_get_security_string(struct smtp_server_connection *conn);
+struct smtp_server_transaction *
+smtp_server_connection_get_transaction(struct smtp_server_connection *conn);
+const char *
+smtp_server_connection_get_transaction_id(struct smtp_server_connection *conn);
+const struct smtp_server_stats *
+smtp_server_connection_get_stats(struct smtp_server_connection *conn);
+void *smtp_server_connection_get_context(struct smtp_server_connection *conn)
+ ATTR_PURE;
+enum smtp_protocol
+smtp_server_connection_get_protocol(struct smtp_server_connection *conn)
+ ATTR_PURE;
+const char *
+smtp_server_connection_get_protocol_name(struct smtp_server_connection *conn);
+struct smtp_server_helo_data *
+smtp_server_connection_get_helo_data(struct smtp_server_connection *conn);
+
+void smtp_server_connection_get_proxy_data(struct smtp_server_connection *conn,
+ struct smtp_proxy_data *proxy_data);
+void smtp_server_connection_set_proxy_data(
+ struct smtp_server_connection *conn,
+ const struct smtp_proxy_data *proxy_data);
+
+void smtp_server_connection_set_capabilities(
+ struct smtp_server_connection *conn, enum smtp_capability capabilities);
+void smtp_server_connection_add_extra_capability(
+ struct smtp_server_connection *conn,
+ const struct smtp_capability_extra *cap);
+
+void smtp_server_connection_register_mail_param(
+ struct smtp_server_connection *conn, const char *param);
+void smtp_server_connection_register_rcpt_param(
+ struct smtp_server_connection *conn, const char *param);
+
+bool smtp_server_connection_is_ssl_secured(struct smtp_server_connection *conn);
+bool smtp_server_connection_is_trusted(struct smtp_server_connection *conn);
+
+/*
+ * Command
+ */
+
+enum smtp_server_command_flags {
+ SMTP_SERVER_CMD_FLAG_PRETLS = BIT(0),
+ SMTP_SERVER_CMD_FLAG_PREAUTH = BIT(1)
+};
+
+enum smtp_server_command_hook_type {
+ /* next: command is next to reply but has not submittted all replies
+ yet. */
+ SMTP_SERVER_COMMAND_HOOK_NEXT,
+ /* replied_one: command has submitted one reply. */
+ SMTP_SERVER_COMMAND_HOOK_REPLIED_ONE,
+ /* replied: command has submitted all replies. */
+ SMTP_SERVER_COMMAND_HOOK_REPLIED,
+ /* completed: server is about to send last replies for this command. */
+ SMTP_SERVER_COMMAND_HOOK_COMPLETED,
+ /* destroy: command is about to be destroyed. */
+ SMTP_SERVER_COMMAND_HOOK_DESTROY
+};
+
+/* Commands are handled asynchronously, which means that the command is not
+ necessary finished when the start function ends. A command is finished
+ when a reply is submitted for it. Several command hooks are available to
+ get notified about events in the command's life cycle.
+ */
+
+typedef void smtp_server_cmd_input_callback_t(struct smtp_server_cmd_ctx *cmd);
+typedef void smtp_server_cmd_start_func_t(struct smtp_server_cmd_ctx *cmd,
+ const char *params);
+typedef void smtp_server_cmd_func_t(struct smtp_server_cmd_ctx *cmd,
+ void *context);
+
+struct smtp_server_cmd_ctx {
+ pool_t pool;
+ struct event *event;
+ const char *name;
+
+ struct smtp_server *server;
+ struct smtp_server_connection *conn;
+ struct smtp_server_command *cmd;
+};
+
+/* Hooks:
+
+ */
+
+void smtp_server_command_add_hook(struct smtp_server_command *cmd,
+ enum smtp_server_command_hook_type type,
+ smtp_server_cmd_func_t func,
+ void *context);
+#define smtp_server_command_add_hook(_cmd, _type, _func, _context) \
+ smtp_server_command_add_hook((_cmd), (_type) - \
+ CALLBACK_TYPECHECK(_func, void (*)( \
+ struct smtp_server_cmd_ctx *, typeof(_context))), \
+ (smtp_server_cmd_func_t *)(_func), (_context))
+void smtp_server_command_remove_hook(struct smtp_server_command *cmd,
+ enum smtp_server_command_hook_type type,
+ smtp_server_cmd_func_t *func);
+#define smtp_server_command_remove_hook(_cmd, _type, _func) \
+ smtp_server_command_remove_hook((_cmd), (_type), \
+ (smtp_server_cmd_func_t *)(_func));
+
+/* The core SMTP commands are pre-registered. Special connection callbacks are
+ provided for the core SMTP commands. Only use this command registration API
+ when custom/extension SMTP commands are required. It is also possible to
+ completely override the default implementations.
+ */
+void smtp_server_command_register(struct smtp_server *server, const char *name,
+ smtp_server_cmd_start_func_t *func,
+ enum smtp_server_command_flags);
+void smtp_server_command_unregister(struct smtp_server *server,
+ const char *name);
+void smtp_server_command_override(struct smtp_server *server, const char *name,
+ smtp_server_cmd_start_func_t *func,
+ enum smtp_server_command_flags flags);
+
+void smtp_server_command_set_reply_count(struct smtp_server_command *cmd,
+ unsigned int count);
+unsigned int
+smtp_server_command_get_reply_count(struct smtp_server_command *cmd);
+
+void smtp_server_command_fail(struct smtp_server_command *cmd,
+ unsigned int status, const char *enh_code,
+ const char *fmt, ...) ATTR_FORMAT(4, 5);
+
+struct smtp_server_reply *
+smtp_server_command_get_reply(struct smtp_server_command *cmd,
+ unsigned int idx);
+bool smtp_server_command_reply_status_equals(struct smtp_server_command *cmd,
+ unsigned int status);
+bool smtp_server_command_is_replied(struct smtp_server_command *cmd);
+bool smtp_server_command_reply_is_forwarded(struct smtp_server_command *cmd);
+bool smtp_server_command_replied_success(struct smtp_server_command *cmd);
+
+void smtp_server_command_input_lock(struct smtp_server_cmd_ctx *cmd);
+void smtp_server_command_input_unlock(struct smtp_server_cmd_ctx *cmd);
+void smtp_server_command_input_capture(
+ struct smtp_server_cmd_ctx *cmd,
+ smtp_server_cmd_input_callback_t *callback);
+
+void smtp_server_command_pipeline_block(struct smtp_server_cmd_ctx *cmd);
+void smtp_server_command_pipeline_unblock(struct smtp_server_cmd_ctx *cmd);
+
+/* EHLO */
+
+void smtp_server_cmd_ehlo(struct smtp_server_cmd_ctx *cmd, const char *params);
+void smtp_server_cmd_helo(struct smtp_server_cmd_ctx *cmd, const char *params);
+
+struct smtp_server_reply *
+smtp_server_cmd_ehlo_reply_create(struct smtp_server_cmd_ctx *cmd);
+void smtp_server_cmd_ehlo_reply_default(struct smtp_server_cmd_ctx *cmd);
+
+/* STARTTLS */
+
+void smtp_server_cmd_starttls(struct smtp_server_cmd_ctx *cmd,
+ const char *params);
+
+/* AUTH */
+
+void smtp_server_cmd_auth(struct smtp_server_cmd_ctx *cmd, const char *params);
+
+void smtp_server_cmd_auth_send_challenge(struct smtp_server_cmd_ctx *cmd,
+ const char *challenge);
+void smtp_server_cmd_auth_success(struct smtp_server_cmd_ctx *cmd,
+ const char *username, const char *success_msg)
+ ATTR_NULL(3);
+
+/* MAIL */
+
+void smtp_server_cmd_mail(struct smtp_server_cmd_ctx *cmd, const char *params);
+
+void smtp_server_cmd_mail_reply_success(struct smtp_server_cmd_ctx *cmd);
+
+/* RCPT */
+
+void smtp_server_cmd_rcpt(struct smtp_server_cmd_ctx *cmd, const char *params);
+
+bool smtp_server_command_is_rcpt(struct smtp_server_cmd_ctx *cmd);
+void smtp_server_cmd_rcpt_reply_success(struct smtp_server_cmd_ctx *cmd);
+
+/* RSET */
+
+void smtp_server_cmd_rset(struct smtp_server_cmd_ctx *cmd, const char *params);
+
+void smtp_server_cmd_rset_reply_success(struct smtp_server_cmd_ctx *cmd);
+
+/* DATA */
+
+void smtp_server_cmd_data(struct smtp_server_cmd_ctx *cmd, const char *params);
+void smtp_server_cmd_bdat(struct smtp_server_cmd_ctx *cmd, const char *params);
+
+bool smtp_server_cmd_data_check_size(struct smtp_server_cmd_ctx *cmd);
+
+/* VRFY */
+
+void smtp_server_cmd_vrfy(struct smtp_server_cmd_ctx *cmd, const char *params);
+
+void smtp_server_cmd_vrfy_reply_default(struct smtp_server_cmd_ctx *cmd);
+
+/* NOOP */
+
+void smtp_server_cmd_noop(struct smtp_server_cmd_ctx *cmd, const char *params);
+
+void smtp_server_cmd_noop_reply_success(struct smtp_server_cmd_ctx *cmd);
+
+/* QUIT */
+
+void smtp_server_cmd_quit(struct smtp_server_cmd_ctx *cmd, const char *params);
+
+/* XCLIENT */
+
+void smtp_server_cmd_xclient(struct smtp_server_cmd_ctx *cmd,
+ const char *params);
+
+/*
+ * Reply
+ */
+
+struct smtp_server_reply *
+smtp_server_reply_create_index(struct smtp_server_command *cmd,
+ unsigned int index, unsigned int status,
+ const char *enh_code) ATTR_NULL(3);
+struct smtp_server_reply *
+smtp_server_reply_create(struct smtp_server_command *cmd, unsigned int status,
+ const char *enh_code) ATTR_NULL(3);
+struct smtp_server_reply *
+smtp_server_reply_create_forward(struct smtp_server_command *cmd,
+ unsigned int index,
+ const struct smtp_reply *from);
+
+void smtp_server_reply_set_status(struct smtp_server_reply *reply,
+ unsigned int status, const char *enh_code)
+ ATTR_NULL(3);
+unsigned int smtp_server_reply_get_status(struct smtp_server_reply *reply,
+ const char **enh_code_r) ATTR_NULL(3);
+
+void smtp_server_reply_add_text(struct smtp_server_reply *reply,
+ const char *line);
+void smtp_server_reply_prepend_text(struct smtp_server_reply *reply,
+ const char *text_prefix);
+void smtp_server_reply_replace_path(struct smtp_server_reply *reply,
+ struct smtp_address *path, bool add);
+
+void smtp_server_reply_submit(struct smtp_server_reply *reply);
+void smtp_server_reply_submit_duplicate(struct smtp_server_cmd_ctx *_cmd,
+ unsigned int index,
+ unsigned int from_index);
+
+/* Submit a reply for the command at the specified index (> 0 only if more than
+ a single reply is expected). */
+void smtp_server_reply_indexv(struct smtp_server_cmd_ctx *_cmd,
+ unsigned int index, unsigned int status,
+ const char *enh_code,
+ const char *fmt, va_list args) ATTR_FORMAT(5, 0);
+void smtp_server_reply_index(struct smtp_server_cmd_ctx *_cmd,
+ unsigned int index, unsigned int status,
+ const char *enh_code, const char *fmt, ...)
+ ATTR_FORMAT(5, 6);
+/* Submit the reply for the specified command. */
+void smtp_server_reply(struct smtp_server_cmd_ctx *_cmd, unsigned int status,
+ const char *enh_code, const char *fmt, ...)
+ ATTR_FORMAT(4, 5);
+/* Forward a reply for the command at the specified index (> 0 only if more
+ than a single reply is expected). */
+void smtp_server_reply_index_forward(struct smtp_server_cmd_ctx *cmd,
+ unsigned int index,
+ const struct smtp_reply *from);
+/* Forward the reply for the specified command. */
+void smtp_server_reply_forward(struct smtp_server_cmd_ctx *cmd,
+ const struct smtp_reply *from);
+/* Submit the same message for all expected replies for this command. */
+void smtp_server_reply_all(struct smtp_server_cmd_ctx *_cmd,
+ unsigned int status, const char *enh_code,
+ const char *fmt, ...) ATTR_FORMAT(4, 5);
+/* Submit and send the same message for all expected replies for this command
+ early; i.e., no matter whether all command data is received completely. */
+void smtp_server_reply_early(struct smtp_server_cmd_ctx *_cmd,
+ unsigned int status, const char *enh_code,
+ const char *fmt, ...) ATTR_FORMAT(4, 5);
+
+/* Reply the command with a 221 bye message */
+void smtp_server_reply_quit(struct smtp_server_cmd_ctx *_cmd);
+
+bool smtp_server_reply_is_success(const struct smtp_server_reply *reply);
+
+/* EHLO */
+
+struct smtp_server_reply *
+smtp_server_reply_create_ehlo(struct smtp_server_command *cmd);
+void smtp_server_reply_ehlo_add(struct smtp_server_reply *reply,
+ const char *keyword);
+void smtp_server_reply_ehlo_add_param(struct smtp_server_reply *reply,
+ const char *keyword,
+ const char *param_fmt, ...)
+ ATTR_FORMAT(3, 4);
+void smtp_server_reply_ehlo_add_params(struct smtp_server_reply *reply,
+ const char *keyword,
+ const char *const *params) ATTR_NULL(3);
+
+void smtp_server_reply_ehlo_add_8bitmime(struct smtp_server_reply *reply);
+void smtp_server_reply_ehlo_add_binarymime(struct smtp_server_reply *reply);
+void smtp_server_reply_ehlo_add_chunking(struct smtp_server_reply *reply);
+void smtp_server_reply_ehlo_add_dsn(struct smtp_server_reply *reply);
+void smtp_server_reply_ehlo_add_enhancedstatuscodes(
+ struct smtp_server_reply *reply);
+void smtp_server_reply_ehlo_add_pipelining(struct smtp_server_reply *reply);
+void smtp_server_reply_ehlo_add_size(struct smtp_server_reply *reply);
+void smtp_server_reply_ehlo_add_starttls(struct smtp_server_reply *reply);
+void smtp_server_reply_ehlo_add_vrfy(struct smtp_server_reply *reply);
+void smtp_server_reply_ehlo_add_xclient(struct smtp_server_reply *reply);
+
+#endif
diff --git a/src/lib-smtp/smtp-submit-settings.c b/src/lib-smtp/smtp-submit-settings.c
new file mode 100644
index 0000000..cfe4afd
--- /dev/null
+++ b/src/lib-smtp/smtp-submit-settings.c
@@ -0,0 +1,69 @@
+/* Copyright (c) 2017-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "hostpid.h"
+#include "settings-parser.h"
+#include "smtp-submit-settings.h"
+
+#include <stddef.h>
+
+static bool smtp_submit_settings_check(void *_set, pool_t pool, const char **error_r);
+
+#undef DEF
+#undef DEFLIST
+#define DEF(type, name) \
+ SETTING_DEFINE_STRUCT_##type(#name, name, struct smtp_submit_settings)
+#define DEFLIST(field, name, defines) \
+ { .type = SET_DEFLIST, .key = name, \
+ .offset = offsetof(struct smtp_submit_settings, field), \
+ .list_info = defines }
+
+static const struct setting_define smtp_submit_setting_defines[] = {
+ DEF(STR, hostname),
+ DEF(BOOL, mail_debug),
+
+ DEF(STR_VARS, submission_host),
+ DEF(STR_VARS, sendmail_path),
+ DEF(TIME, submission_timeout),
+
+ DEF(ENUM, submission_ssl),
+
+ SETTING_DEFINE_LIST_END
+};
+
+static const struct smtp_submit_settings smtp_submit_default_settings = {
+ .hostname = "",
+ .mail_debug = FALSE,
+
+ .submission_host = "",
+ .sendmail_path = "/usr/sbin/sendmail",
+ .submission_timeout = 30,
+
+ .submission_ssl = "no:smtps:submissions:starttls",
+};
+
+const struct setting_parser_info smtp_submit_setting_parser_info = {
+ .module_name = "smtp-submit",
+ .defines = smtp_submit_setting_defines,
+ .defaults = &smtp_submit_default_settings,
+
+ .type_offset = SIZE_MAX,
+ .struct_size = sizeof(struct smtp_submit_settings),
+
+ .parent_offset = SIZE_MAX,
+
+#ifndef CONFIG_BINARY
+ .check_func = smtp_submit_settings_check,
+#endif
+};
+
+static bool
+smtp_submit_settings_check(void *_set, pool_t pool,
+ const char **error_r ATTR_UNUSED)
+{
+ struct smtp_submit_settings *set = _set;
+
+ if (*set->hostname == '\0')
+ set->hostname = p_strdup(pool, my_hostdomain());
+ return TRUE;
+}
diff --git a/src/lib-smtp/smtp-submit-settings.h b/src/lib-smtp/smtp-submit-settings.h
new file mode 100644
index 0000000..08c4243
--- /dev/null
+++ b/src/lib-smtp/smtp-submit-settings.h
@@ -0,0 +1,17 @@
+#ifndef SMTP_SUBMIT_SETTINGS_H
+#define SMTP_SUBMIT_SETTINGS_H
+
+struct smtp_submit_settings {
+ const char *hostname;
+ bool mail_debug;
+
+ const char *submission_host;
+ const char *sendmail_path;
+ unsigned int submission_timeout;
+
+ const char *submission_ssl;
+};
+
+extern const struct setting_parser_info smtp_submit_setting_parser_info;
+
+#endif
diff --git a/src/lib-smtp/smtp-submit.c b/src/lib-smtp/smtp-submit.c
new file mode 100644
index 0000000..0328b95
--- /dev/null
+++ b/src/lib-smtp/smtp-submit.c
@@ -0,0 +1,505 @@
+/* Copyright (c) 2006-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "ioloop.h"
+#include "array.h"
+#include "str.h"
+#include "istream.h"
+#include "ostream.h"
+#include "iostream-temp.h"
+#include "iostream-ssl.h"
+#include "master-service.h"
+#include "program-client.h"
+#include "smtp-client.h"
+#include "smtp-client-connection.h"
+#include "smtp-client-transaction.h"
+#include "smtp-submit.h"
+
+#include <unistd.h>
+#include <sys/wait.h>
+#include <sysexits.h>
+#include <signal.h>
+
+#define DEFAULT_SUBMISSION_PORT 25
+
+static struct event_category event_category_smtp_submit = {
+ .name = "smtp-submit"
+};
+
+struct smtp_submit_session {
+ pool_t pool;
+ struct smtp_submit_settings set;
+ struct ssl_iostream_settings ssl_set;
+ struct event *event;
+ bool allow_root:1;
+};
+
+struct smtp_submit {
+ pool_t pool;
+
+ struct smtp_submit_session *session;
+ struct event *event;
+
+ struct ostream *output;
+ struct istream *input;
+
+ struct smtp_address *mail_from;
+ ARRAY_TYPE(smtp_address) rcpt_to;
+
+ struct timeout *to_error;
+ int status;
+ const char *error;
+
+ struct program_client *prg_client;
+ struct smtp_client *smtp_client;
+ struct smtp_client_transaction *smtp_trans;
+
+ smtp_submit_callback_t *callback;
+ void *context;
+
+ bool simple:1;
+};
+
+struct smtp_submit_session *
+smtp_submit_session_init(const struct smtp_submit_input *input,
+ const struct smtp_submit_settings *set)
+{
+ struct smtp_submit_session *session;
+ pool_t pool;
+
+ pool = pool_alloconly_create("smtp submit session", 128);
+ session = p_new(pool, struct smtp_submit_session, 1);
+ session->pool = pool;
+
+ session->set = *set;
+ session->set.hostname =
+ p_strdup_empty(pool, set->hostname);
+ session->set.submission_host =
+ p_strdup_empty(pool, set->submission_host);
+ session->set.sendmail_path =
+ p_strdup_empty(pool, set->sendmail_path);
+ session->set.submission_ssl =
+ p_strdup_empty(pool, set->submission_ssl);
+
+ if (input->ssl != NULL) {
+ ssl_iostream_settings_init_from(pool, &session->ssl_set,
+ input->ssl);
+ }
+ session->allow_root = input->allow_root;
+
+ session->event = event_create(input->event_parent);
+ event_add_category(session->event, &event_category_smtp_submit);
+
+ return session;
+}
+
+void smtp_submit_session_deinit(struct smtp_submit_session **_session)
+{
+ struct smtp_submit_session *session = *_session;
+
+ *_session = NULL;
+
+ event_unref(&session->event);
+ pool_unref(&session->pool);
+}
+
+struct smtp_submit *
+smtp_submit_init(struct smtp_submit_session *session,
+ const struct smtp_address *mail_from)
+{
+ struct smtp_submit *subm;
+ pool_t pool;
+
+ pool = pool_alloconly_create("smtp submit", 256);
+ subm = p_new(pool, struct smtp_submit, 1);
+ subm->session = session;
+ subm->pool = pool;
+
+ subm->mail_from = smtp_address_clone(pool, mail_from);;
+ p_array_init(&subm->rcpt_to, pool, 2);
+
+ subm->event = event_create(session->event);
+ event_add_str(subm->event, "mail_from",
+ smtp_address_encode(subm->mail_from));
+
+ return subm;
+}
+
+struct smtp_submit *
+smtp_submit_init_simple(const struct smtp_submit_input *input,
+ const struct smtp_submit_settings *set,
+ const struct smtp_address *mail_from)
+{
+ struct smtp_submit_session *session;
+ struct smtp_submit *subm;
+
+ session = smtp_submit_session_init(input, set);
+ subm = smtp_submit_init(session, mail_from);
+ subm->simple = TRUE;
+ return subm;
+}
+
+void smtp_submit_deinit(struct smtp_submit **_subm)
+{
+ struct smtp_submit *subm = *_subm;
+
+ *_subm = NULL;
+
+ if (subm->output != NULL)
+ o_stream_destroy(&subm->output);
+ if (subm->input != NULL)
+ i_stream_destroy(&subm->input);
+
+ if (subm->prg_client != NULL)
+ program_client_destroy(&subm->prg_client);
+ if (subm->smtp_trans != NULL)
+ smtp_client_transaction_destroy(&subm->smtp_trans);
+ if (subm->smtp_client != NULL)
+ smtp_client_deinit(&subm->smtp_client);
+
+ timeout_remove(&subm->to_error);
+
+ if (subm->simple)
+ smtp_submit_session_deinit(&subm->session);
+ event_unref(&subm->event);
+ pool_unref(&subm->pool);
+}
+
+void smtp_submit_add_rcpt(struct smtp_submit *subm,
+ const struct smtp_address *rcpt_to)
+{
+ struct smtp_address *rcpt;
+
+ i_assert(subm->output == NULL);
+ i_assert(!smtp_address_isnull(rcpt_to));
+
+ rcpt = smtp_address_clone(subm->pool, rcpt_to);
+ array_push_back(&subm->rcpt_to, &rcpt);
+}
+
+struct ostream *smtp_submit_send(struct smtp_submit *subm)
+{
+ i_assert(subm->output == NULL);
+ i_assert(array_count(&subm->rcpt_to) > 0);
+
+ event_add_int(subm->event, "recipients", array_count(&subm->rcpt_to));
+
+ subm->output = iostream_temp_create
+ (t_strconcat("/tmp/dovecot.",
+ master_service_get_name(master_service), NULL), 0);
+ o_stream_set_no_error_handling(subm->output, TRUE);
+ return subm->output;
+}
+
+static void
+smtp_submit_callback(struct smtp_submit *subm, int status,
+ const char *error)
+{
+ struct smtp_submit_result result;
+ smtp_submit_callback_t *callback;
+
+ timeout_remove(&subm->to_error);
+
+ struct event_passthrough *e =
+ event_create_passthrough(subm->event)->
+ set_name("smtp_submit_finished");
+ if (status > 0)
+ e_debug(e->event(), "Sent message successfully");
+ else {
+ e->add_str("error", error);
+ e_debug(e->event(), "Failed to send message: %s", error);
+ }
+
+ i_zero(&result);
+ result.status = status;
+ result.error = error;
+
+ callback = subm->callback;
+ subm->callback = NULL;
+ callback(&result, subm->context);
+}
+
+static void
+smtp_submit_delayed_error_callback(struct smtp_submit *subm)
+{
+ smtp_submit_callback(subm, -1, subm->error);
+}
+
+static void
+smtp_submit_delayed_error(struct smtp_submit *subm,
+ const char *error)
+{
+ subm->status = -1;
+ subm->error = p_strdup(subm->pool, error);
+ subm->to_error = timeout_add_short(0,
+ smtp_submit_delayed_error_callback, subm);
+}
+
+static void
+smtp_submit_error(struct smtp_submit *subm,
+ int status, const char *error)
+{
+ const struct smtp_submit_settings *set = &subm->session->set;
+ i_assert(status <= 0);
+ if (subm->error != NULL)
+ return;
+
+ subm->status = status;
+ subm->error = p_strdup_printf(subm->pool,
+ "smtp(%s): %s",
+ set->submission_host, error);
+}
+
+static void
+smtp_submit_success(struct smtp_submit *subm)
+{
+ if (subm->error != NULL)
+ return;
+ subm->status = 1;
+}
+
+static void
+smtp_submit_send_host_finished(struct smtp_submit *subm)
+{
+ i_assert(subm->status > 0 || subm->error != NULL);
+ smtp_submit_callback(subm, subm->status, subm->error);
+ subm->smtp_trans = NULL;
+}
+
+static bool
+reply_is_temp_fail(const struct smtp_reply *reply)
+{
+ return (smtp_reply_is_temp_fail(reply) ||
+ !smtp_reply_is_remote(reply));
+}
+
+static void
+rcpt_to_callback(const struct smtp_reply *reply,
+ struct smtp_submit *subm)
+{
+ if (!smtp_reply_is_success(reply)) {
+ smtp_submit_error(subm,
+ (reply_is_temp_fail(reply) ? -1 : 0),
+ t_strdup_printf("RCPT TO failed: %s",
+ smtp_reply_log(reply)));
+ }
+}
+
+static void
+data_callback(const struct smtp_reply *reply,
+ struct smtp_submit *subm)
+{
+ if (!smtp_reply_is_success(reply)) {
+ smtp_submit_error(subm,
+ (reply_is_temp_fail(reply) ? -1 : 0),
+ t_strdup_printf("DATA failed: %s",
+ smtp_reply_log(reply)));
+ return;
+ }
+
+ smtp_submit_success(subm);
+}
+
+static void
+data_dummy_callback(const struct smtp_reply *reply ATTR_UNUSED,
+ struct smtp_submit *subm ATTR_UNUSED)
+{
+ /* nothing */
+}
+
+static void
+smtp_submit_send_host(struct smtp_submit *subm)
+{
+ const struct smtp_submit_settings *set = &subm->session->set;
+ struct smtp_client_settings smtp_set;
+ struct smtp_client *smtp_client;
+ struct smtp_client_connection *smtp_conn;
+ struct smtp_client_transaction *smtp_trans;
+ enum smtp_client_connection_ssl_mode ssl_mode;
+ struct smtp_address *rcpt;
+ const char *host;
+ in_port_t port;
+
+ if (net_str2hostport(set->submission_host,
+ DEFAULT_SUBMISSION_PORT, &host, &port) < 0) {
+ smtp_submit_delayed_error(subm, t_strdup_printf(
+ "Invalid submission_host: %s", host));
+ return;
+ }
+
+ i_zero(&smtp_set);
+ smtp_set.my_hostname = set->hostname;
+ smtp_set.connect_timeout_msecs = set->submission_timeout*1000;
+ smtp_set.command_timeout_msecs = set->submission_timeout*1000;
+ smtp_set.debug = set->mail_debug;
+ smtp_set.ssl = &subm->session->ssl_set;
+ smtp_set.event_parent = subm->event;
+
+ ssl_mode = SMTP_CLIENT_SSL_MODE_NONE;
+ if (set->submission_ssl != NULL) {
+ if (strcasecmp(set->submission_ssl, "smtps") == 0 ||
+ strcasecmp(set->submission_ssl, "submissions") == 0)
+ ssl_mode = SMTP_CLIENT_SSL_MODE_IMMEDIATE;
+ else if (strcasecmp(set->submission_ssl, "starttls") == 0)
+ ssl_mode = SMTP_CLIENT_SSL_MODE_STARTTLS;
+ }
+
+ smtp_client = smtp_client_init(&smtp_set);
+ smtp_conn = smtp_client_connection_create(smtp_client,
+ SMTP_PROTOCOL_SMTP, host, port, ssl_mode, NULL);
+
+ smtp_trans = smtp_client_transaction_create(smtp_conn,
+ subm->mail_from, NULL, 0, smtp_submit_send_host_finished, subm);
+ smtp_client_connection_unref(&smtp_conn);
+
+ array_foreach_elem(&subm->rcpt_to, rcpt) {
+ smtp_client_transaction_add_rcpt(smtp_trans,
+ rcpt, NULL, rcpt_to_callback, data_dummy_callback, subm);
+ }
+
+ subm->smtp_client = smtp_client;
+ subm->smtp_trans = smtp_trans;
+
+ smtp_client_transaction_send
+ (smtp_trans, subm->input, data_callback, subm);
+ i_stream_unref(&subm->input);
+}
+
+static void
+smtp_submit_sendmail_callback(enum program_client_exit_status status,
+ struct smtp_submit *subm)
+{
+ if (status == PROGRAM_CLIENT_EXIT_STATUS_INTERNAL_FAILURE) {
+ smtp_submit_callback(subm, -1,
+ "Failed to execute sendmail");
+ return;
+ }
+ if (status == PROGRAM_CLIENT_EXIT_STATUS_FAILURE) {
+ smtp_submit_callback(subm, -1,
+ "Sendmail program returned error");
+ return;
+ }
+
+ smtp_submit_callback(subm, 1, NULL);
+}
+
+static void
+smtp_submit_send_sendmail(struct smtp_submit *subm)
+{
+ const struct smtp_submit_settings *set = &subm->session->set;
+ const char *const *sendmail_args, *sendmail_bin, *str;
+ ARRAY_TYPE(const_string) args;
+ struct smtp_address *rcpt;
+ unsigned int i;
+ struct program_client_settings pc_set;
+ struct program_client *pc;
+
+ sendmail_args = t_strsplit(set->sendmail_path, " ");
+ t_array_init(&args, 16);
+ i_assert(sendmail_args[0] != NULL);
+ sendmail_bin = sendmail_args[0];
+ for (i = 1; sendmail_args[i] != NULL; i++)
+ array_push_back(&args, &sendmail_args[i]);
+
+ str = "-i"; array_push_back(&args, &str); /* ignore dots */
+ str = "-f"; array_push_back(&args, &str);
+ str = !smtp_address_isnull(subm->mail_from) ?
+ smtp_address_encode(subm->mail_from) : "<>";
+ array_push_back(&args, &str);
+
+ str = "--"; array_push_back(&args, &str);
+ array_foreach_elem(&subm->rcpt_to, rcpt) {
+ const char *rcpt_encoded = smtp_address_encode(rcpt);
+ array_push_back(&args, &rcpt_encoded);
+ }
+ array_append_zero(&args);
+
+ i_zero(&pc_set);
+ pc_set.client_connect_timeout_msecs = set->submission_timeout * 1000;
+ pc_set.input_idle_timeout_msecs = set->submission_timeout * 1000;
+ pc_set.debug = set->mail_debug;
+ pc_set.event = subm->event;
+ pc_set.allow_root = subm->session->allow_root;
+ restrict_access_init(&pc_set.restrict_set);
+
+ pc = program_client_local_create
+ (sendmail_bin, array_front(&args), &pc_set);
+
+ program_client_set_input(pc, subm->input);
+ i_stream_unref(&subm->input);
+
+ subm->prg_client = pc;
+
+ program_client_run_async(pc, smtp_submit_sendmail_callback, subm);
+}
+
+struct smtp_submit_run_context {
+ int status;
+ char *error;
+};
+
+static void
+smtp_submit_run_callback(const struct smtp_submit_result *result,
+ struct smtp_submit_run_context *rctx)
+{
+ rctx->error = i_strdup(result->error);
+ rctx->status = result->status;
+ io_loop_stop(current_ioloop);
+}
+
+int smtp_submit_run(struct smtp_submit *subm,
+ const char **error_r)
+{
+ struct smtp_submit_run_context rctx;
+ struct ioloop *ioloop;
+
+ ioloop = io_loop_create();
+ io_loop_set_running(ioloop);
+
+ i_zero(&rctx);
+ smtp_submit_run_async(subm,
+ smtp_submit_run_callback, &rctx);
+
+ if (io_loop_is_running(ioloop))
+ io_loop_run(ioloop);
+
+ io_loop_destroy(&ioloop);
+
+ if (rctx.error == NULL)
+ *error_r = NULL;
+ else {
+ *error_r = t_strdup(rctx.error);
+ i_free(rctx.error);
+ }
+
+ return rctx.status;
+}
+
+#undef smtp_submit_run_async
+void smtp_submit_run_async(struct smtp_submit *subm,
+ smtp_submit_callback_t *callback, void *context)
+{
+ const struct smtp_submit_settings *set = &subm->session->set;
+ uoff_t data_size;
+
+ subm->callback = callback;
+ subm->context = context;
+
+ /* the mail has been written to a file. now actually send it. */
+ subm->input = iostream_temp_finish
+ (&subm->output, IO_BLOCK_SIZE);
+
+ if (i_stream_get_size(subm->input, TRUE, &data_size) > 0)
+ event_add_int(subm->event, "data_size", data_size);
+
+ struct event_passthrough *e =
+ event_create_passthrough(subm->event)->
+ set_name("smtp_submit_started");
+ e_debug(e->event(), "Started sending message");
+
+ if (set->submission_host != NULL) {
+ smtp_submit_send_host(subm);
+ } else {
+ smtp_submit_send_sendmail(subm);
+ }
+}
diff --git a/src/lib-smtp/smtp-submit.h b/src/lib-smtp/smtp-submit.h
new file mode 100644
index 0000000..6a00f56
--- /dev/null
+++ b/src/lib-smtp/smtp-submit.h
@@ -0,0 +1,73 @@
+#ifndef SMTP_SUBMIT_H
+#define SMTP_SUBMIT_H
+
+#include "smtp-submit-settings.h"
+
+struct ssl_iostream_settings;
+struct smtp_address;
+struct smtp_submit_settings;
+struct smtp_submit_session;
+struct smtp_submit;
+
+struct smtp_submit_input {
+ /* SSL settings */
+ const struct ssl_iostream_settings *ssl;
+
+ /* Event to use as parent for the submit event */
+ struct event *event_parent;
+
+ /* Allow running sendmail as root */
+ bool allow_root:1;
+};
+
+struct smtp_submit_result {
+ /* 1 on success,
+ 0 on permanent failure (e.g. invalid destination),
+ -1 on temporary failure */
+ int status;
+
+ const char *error;
+};
+
+typedef void
+smtp_submit_callback_t(const struct smtp_submit_result *result,
+ void *context);
+
+/* Use submit session to reuse resources (e.g. SMTP connections) between
+ submissions (FIXME: actually implement this) */
+struct smtp_submit_session *
+smtp_submit_session_init(const struct smtp_submit_input *input,
+ const struct smtp_submit_settings *set);
+void smtp_submit_session_deinit(struct smtp_submit_session **_session);
+
+struct smtp_submit *
+smtp_submit_init(struct smtp_submit_session *session,
+ const struct smtp_address *mail_from);
+struct smtp_submit *
+smtp_submit_init_simple(const struct smtp_submit_input *input,
+ const struct smtp_submit_settings *set,
+ const struct smtp_address *mail_from) ATTR_NULL(2);
+void smtp_submit_deinit(struct smtp_submit **_submit);
+
+/* Add a new recipient */
+void smtp_submit_add_rcpt(struct smtp_submit *subm,
+ const struct smtp_address *rcpt_to);
+/* Get an output stream where the message can be written to. The recipients
+ must already be added before calling this. */
+struct ostream *smtp_submit_send(struct smtp_submit *subm);
+
+/* Submit the message. Callback is called once the message submission
+ finishes. */
+void smtp_submit_run_async(struct smtp_submit *subm,
+ smtp_submit_callback_t *callback, void *context);
+#define smtp_submit_run_async(subm, callback, context) \
+ smtp_submit_run_async(subm, \
+ (smtp_submit_callback_t*)callback, \
+ (char*)context - CALLBACK_TYPECHECK(callback, \
+ void (*)(const struct smtp_submit_result *result, typeof(context))))
+
+/* Returns 1 on success, 0 on permanent failure (e.g. invalid destination),
+ -1 on temporary failure. */
+int smtp_submit_run(struct smtp_submit *subm, const char **error_r);
+
+#endif
diff --git a/src/lib-smtp/smtp-syntax.c b/src/lib-smtp/smtp-syntax.c
new file mode 100644
index 0000000..dc7b744
--- /dev/null
+++ b/src/lib-smtp/smtp-syntax.c
@@ -0,0 +1,327 @@
+/* Copyright (c) 2013-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "str.h"
+#include "array.h"
+#include "smtp-parser.h"
+
+#include "smtp-syntax.h"
+
+#include <ctype.h>
+
+/*
+ * String
+ */
+
+int smtp_string_parse(const char *string, const char **value_r,
+ const char **error_r)
+{
+ struct smtp_parser parser;
+
+ *value_r = NULL;
+ *error_r = NULL;
+
+ if (string == NULL || *string == '\0') {
+ *value_r = "";
+ return 0;
+ }
+
+ smtp_parser_init(&parser, pool_datastack_create(), string);
+
+ if (smtp_parser_parse_string(&parser, value_r) < 0) {
+ *error_r = parser.error;
+ return -1;
+ }
+ if (parser.cur < parser.end) {
+ *error_r = "Invalid character in string";
+ return -1;
+ }
+ return 1;
+}
+
+void smtp_string_write(string_t *out, const char *value)
+{
+ bool quoted = FALSE;
+ const unsigned char *p, *pend, *pblock;
+ size_t begin = str_len(out);
+
+ if (value == NULL)
+ return;
+ p = (const unsigned char *)value;
+ pend = p + strlen(value);
+ while (p < pend) {
+ pblock = p;
+ while (p < pend && smtp_char_is_atext(*p))
+ p++;
+
+ if (!quoted && p < pend) {
+ quoted = TRUE;
+ str_insert(out, begin, "\"");
+ }
+
+ str_append_data(out, pblock, p-pblock);
+ if (p >= pend)
+ break;
+
+ i_assert(quoted);
+ i_assert(smtp_char_is_qpair(*p));
+
+ if (!smtp_char_is_qtext(*p))
+ str_append_c(out, '\\');
+ str_append_c(out, *p);
+
+ p++;
+ }
+
+ if (quoted)
+ str_append_c(out, '\"');
+}
+
+/*
+ * Xtext encoding
+ */
+
+int smtp_xtext_decode(string_t *out, const char *xtext, bool allow_nul,
+ const char **error_r)
+{
+ struct smtp_parser parser;
+
+ if (xtext == NULL || *xtext == '\0')
+ return 1;
+
+ smtp_parser_init(&parser, pool_datastack_create(), xtext);
+
+ if (smtp_parser_parse_xtext(&parser, out) < 0) {
+ *error_r = parser.error;
+ return -1;
+ }
+ if (parser.cur < parser.end) {
+ *error_r = "Invalid character in xtext";
+ return -1;
+ }
+ if (!allow_nul && strlen(str_c(out)) != str_len(out)) {
+ *error_r = "Encountered NUL character in xtext";
+ return -1;
+ }
+ return 1;
+}
+
+int smtp_xtext_parse(const char *xtext, const char **value_r,
+ const char **error_r)
+{
+ string_t *value;
+ int ret;
+
+ *value_r = NULL;
+ *error_r = NULL;
+
+ if (xtext == NULL || *xtext == '\0') {
+ *value_r = "";
+ return 1;
+ }
+
+ value = t_str_new(256);
+ ret = smtp_xtext_decode(value, xtext, FALSE, error_r);
+ if (ret <= 0)
+ return ret;
+
+ *value_r = str_c(value);
+ return 1;
+}
+
+void smtp_xtext_encode(string_t *out, const unsigned char *data, size_t size)
+{
+ const unsigned char *p, *pbegin, *pend;
+
+ p = data;
+ pend = p + size;
+ while (p < pend) {
+ pbegin = p;
+ while (p < pend && smtp_char_is_xtext(*p))
+ p++;
+
+ str_append_data(out, pbegin, p-pbegin);
+ if (p >= pend)
+ break;
+
+ str_printfa(out, "+%02X", (unsigned int)*p);
+ p++;
+ }
+}
+
+/*
+ * HELO domain
+ */
+
+int smtp_helo_domain_parse(const char *helo, bool allow_literal,
+ const char **domain_r)
+{
+ struct smtp_parser parser;
+ int ret;
+
+ smtp_parser_init(&parser, pool_datastack_create(), helo);
+
+ ret = smtp_parser_parse_domain(&parser, domain_r);
+ if (ret == 0) {
+ if (allow_literal) {
+ ret = smtp_parser_parse_address_literal(
+ &parser, domain_r, NULL);
+ }
+ }
+
+ if (ret <= 0 || (parser.cur < parser.end && *parser.cur != ' '))
+ return -1;
+ return 0;
+}
+
+/*
+ * EHLO reply
+ */
+
+bool smtp_ehlo_keyword_is_valid(const char *keyword)
+{
+ const char *p;
+
+ for (p = keyword; *p != '\0'; p++) {
+ if (!i_isalnum(*p))
+ return FALSE;
+ }
+ return TRUE;
+}
+
+bool smtp_ehlo_param_is_valid(const char *param)
+{
+ const char *p;
+
+ for (p = param; *p != '\0'; p++) {
+ if (!smtp_char_is_ehlo_param(*p))
+ return FALSE;
+ }
+ return TRUE;
+}
+
+bool smtp_ehlo_params_are_valid(const char *const *params)
+{
+ if (params == NULL)
+ return TRUE;
+
+ while (*params != NULL) {
+ if (!smtp_ehlo_param_is_valid(*params))
+ return FALSE;
+ params++;
+ }
+
+ return TRUE;
+}
+
+bool smtp_ehlo_params_str_is_valid(const char *params)
+{
+ const char *p;
+ bool space = FALSE;
+
+ for (p = params; *p != '\0'; p++) {
+ if (*p == ' ') {
+ if (space)
+ return FALSE;
+ space = TRUE;
+ continue;
+ }
+ space = FALSE;
+
+ if (!smtp_char_is_ehlo_param(*p))
+ return FALSE;
+ }
+ return TRUE;
+}
+
+static int
+smtp_parse_ehlo_line(struct smtp_parser *parser, const char **key_r,
+ const char *const **params_r)
+{
+ const unsigned char *pbegin = parser->cur;
+ ARRAY_TYPE(const_string) params = ARRAY_INIT;
+ const char *param;
+
+ /* ehlo-line = ehlo-keyword *( SP ehlo-param )
+ ehlo-keyword = (ALPHA / DIGIT) *(ALPHA / DIGIT / "-")
+ ; additional syntax of ehlo-params depends on
+ ; ehlo-keyword
+ ehlo-param = 1*(%d33-126)
+ ; any CHAR excluding <SP> and all
+ ; control characters (US-ASCII 0-31 and 127
+ ; inclusive)
+ */
+
+ if (parser->cur >= parser->end || !i_isalnum(*parser->cur)) {
+ parser->error = "Unexpected character in EHLO keyword";
+ return -1;
+ }
+ parser->cur++;
+
+ while (parser->cur < parser->end &&
+ (i_isalnum(*parser->cur) || *parser->cur == '-'))
+ parser->cur++;
+
+ *key_r = p_strdup_until(parser->pool, pbegin, parser->cur);
+
+ if (parser->cur >= parser->end) {
+ *params_r = p_new(parser->pool, const char *, 1);
+ return 1;
+ }
+ if (*parser->cur != ' ') {
+ parser->error = "Unexpected character in EHLO keyword";
+ return -1;
+ }
+ parser->cur++;
+
+ pbegin = parser->cur;
+ p_array_init(&params, parser->pool, 32);
+ while (parser->cur < parser->end) {
+ if (*parser->cur == ' ') {
+ if (parser->cur+1 >= parser->end ||
+ *(parser->cur+1) == ' ') {
+ parser->error =
+ "Missing EHLO parameter after ' '";
+ return -1;
+ }
+ param = p_strdup_until(parser->pool, pbegin,
+ parser->cur);
+ array_push_back(&params, &param);
+ pbegin = parser->cur + 1;
+ } else if (!smtp_char_is_ehlo_param(*parser->cur)) {
+ parser->error =
+ "Unexpected character in EHLO parameter";
+ return -1;
+ }
+ parser->cur++;
+ }
+
+ param = p_strdup_until(parser->pool, pbegin, parser->cur);
+ array_push_back(&params, &param);
+ array_append_zero(&params);
+ *params_r = array_front(&params);
+ return 1;
+}
+
+int smtp_ehlo_line_parse(const char *ehlo_line, const char **key_r,
+ const char *const **params_r, const char **error_r)
+{
+ struct smtp_parser parser;
+
+ *key_r = NULL;
+ *params_r = NULL;
+ *error_r = NULL;
+
+ if (ehlo_line == NULL || *ehlo_line == '\0') {
+ *error_r = "Parameter is empty";
+ return -1;
+ }
+
+ smtp_parser_init(&parser, pool_datastack_create(), ehlo_line);
+
+ if (smtp_parse_ehlo_line(&parser, key_r, params_r) <= 0) {
+ *error_r = parser.error;
+ return -1;
+ }
+ return 1;
+}
diff --git a/src/lib-smtp/smtp-syntax.h b/src/lib-smtp/smtp-syntax.h
new file mode 100644
index 0000000..f9f960c
--- /dev/null
+++ b/src/lib-smtp/smtp-syntax.h
@@ -0,0 +1,49 @@
+#ifndef SMTP_SYNTAX_H
+#define SMTP_SYNTAX_H
+
+struct smtp_parser;
+
+/*
+ * String
+ */
+
+int smtp_string_parse(const char *string, const char **value_r,
+ const char **error_r);
+void smtp_string_write(string_t *out, const char *value);
+
+/*
+ * Xtext encoding
+ */
+
+int smtp_xtext_decode(string_t *out, const char *xtext, bool allow_nul,
+ const char **error_r);
+int smtp_xtext_parse(const char *xtext, const char **value_r,
+ const char **error_r);
+
+void smtp_xtext_encode(string_t *out, const unsigned char *data, size_t size);
+static inline void
+smtp_xtext_encode_cstr(string_t *out, const char *data)
+{
+ smtp_xtext_encode(out, (const unsigned char *)data, strlen(data));
+}
+
+/*
+ * HELO domain
+ */
+
+int smtp_helo_domain_parse(const char *helo, bool allow_literal,
+ const char **domain_r);
+
+/*
+ * EHLO reply
+ */
+
+bool smtp_ehlo_keyword_is_valid(const char *keyword);
+bool smtp_ehlo_param_is_valid(const char *param);
+bool smtp_ehlo_params_are_valid(const char *const *params) ATTR_NULL(1);
+bool smtp_ehlo_params_str_is_valid(const char *params);
+
+int smtp_ehlo_line_parse(const char *ehlo_line, const char **key_r,
+ const char *const **params_r, const char **error_r);
+
+#endif
diff --git a/src/lib-smtp/test-bin/sendmail-exit-1.sh b/src/lib-smtp/test-bin/sendmail-exit-1.sh
new file mode 100755
index 0000000..11b5422
--- /dev/null
+++ b/src/lib-smtp/test-bin/sendmail-exit-1.sh
@@ -0,0 +1,4 @@
+#!/bin/sh
+
+cat > /dev/null
+exit 1;
diff --git a/src/lib-smtp/test-bin/sendmail-success.sh b/src/lib-smtp/test-bin/sendmail-success.sh
new file mode 100755
index 0000000..bb09b2b
--- /dev/null
+++ b/src/lib-smtp/test-bin/sendmail-success.sh
@@ -0,0 +1,4 @@
+#!/bin/sh
+
+cat > $1
+exit 0;
diff --git a/src/lib-smtp/test-smtp-address.c b/src/lib-smtp/test-smtp-address.c
new file mode 100644
index 0000000..f16ac4c
--- /dev/null
+++ b/src/lib-smtp/test-smtp-address.c
@@ -0,0 +1,1382 @@
+/* Copyright (c) 2013-2018 Dovecot authors, see the included COPYING file */
+
+#include "test-lib.h"
+#include "str.h"
+#include "str-sanitize.h"
+#include "test-common.h"
+#include "smtp-address.h"
+
+/*
+ * Valid mailbox parse tests
+ */
+
+struct valid_mailbox_parse_test {
+ const char *input, *output;
+ enum smtp_address_parse_flags flags;
+
+ struct smtp_address address;
+};
+
+static const struct valid_mailbox_parse_test
+valid_mailbox_parse_tests[] = {
+ {
+ .input = "",
+ .flags = SMTP_ADDRESS_PARSE_FLAG_ALLOW_EMPTY,
+ .address = { .localpart = NULL, .domain = NULL },
+ },
+ {
+ .input = "user",
+ .flags = SMTP_ADDRESS_PARSE_FLAG_ALLOW_LOCALPART,
+ .address = { .localpart = "user", .domain = NULL },
+ },
+ {
+ .input = "user@domain.tld",
+ .address = { .localpart = "user", .domain = "domain.tld" },
+ },
+ {
+ .input = "1234567890@domain.tld",
+ .address = {
+ .localpart = "1234567890",
+ .domain = "domain.tld" },
+ },
+ {
+ .input = "_______@domain.tld",
+ .address = {
+ .localpart = "_______",
+ .domain = "domain.tld" },
+ },
+ {
+ .input = "firstname.lastname@domain.tld",
+ .address = {
+ .localpart = "firstname.lastname",
+ .domain = "domain.tld" },
+ },
+ {
+ .input = "firstname+lastname@domain.tld",
+ .address = {
+ .localpart = "firstname+lastname",
+ .domain = "domain.tld" },
+ },
+ {
+ .input = "firstname-lastname@domain.tld",
+ .address = {
+ .localpart = "firstname-lastname",
+ .domain = "domain.tld" },
+ },
+ {
+ .input = "\"user\"@domain.tld",
+ .address = { .localpart = "user", .domain = "domain.tld" },
+ .output = "user@domain.tld"
+ },
+ {
+ .input = "\"user@frop\"@domain.tld",
+ .address = { .localpart = "user@frop", .domain = "domain.tld" },
+ .output = "\"user@frop\"@domain.tld"
+ },
+ {
+ .input = "user@127.0.0.1",
+ .address = { .localpart = "user", .domain = "127.0.0.1" },
+ },
+ {
+ .input = "user@[127.0.0.1]",
+ .address = { .localpart = "user", .domain = "[127.0.0.1]" },
+ },
+ {
+ .input = "user@[IPv6:::1]",
+ .address = { .localpart = "user", .domain = "[IPv6:::1]" },
+ },
+ {
+ .input = "user@[IPv6:::127.0.0.1]",
+ .address = { .localpart = "user", .domain = "[IPv6:::127.0.0.1]" },
+ /* Japanese deviations */
+ },
+ {
+ .input = "email@-example.com",
+ .address = { .localpart = "email", .domain = "-example.com" },
+ },
+ {
+ .input = ".email@example.com",
+ .output = "\".email\"@example.com",
+ .address = { .localpart = ".email", .domain = "example.com" },
+ },
+ {
+ .input = "email.@example.com",
+ .output = "\"email.\"@example.com",
+ .address = { .localpart = "email.", .domain = "example.com" },
+ },
+ {
+ .input = "email..email@example.com",
+ .output = "\"email..email\"@example.com",
+ .address = { .localpart = "email..email", .domain = "example.com" },
+ },
+ {
+ .input = "Abc..123@example.com",
+ .output = "\"Abc..123\"@example.com",
+ .address = { .localpart = "Abc..123", .domain = "example.com" },
+ },
+ {
+ .input = "Abc..@example.com",
+ .output = "\"Abc..\"@example.com",
+ .address = { .localpart = "Abc..", .domain = "example.com" },
+ },
+};
+
+unsigned int valid_mailbox_parse_test_count =
+ N_ELEMENTS(valid_mailbox_parse_tests);
+
+static void
+test_smtp_mailbox_equal(const struct smtp_address *test,
+ const struct smtp_address *parsed)
+{
+ if (parsed->localpart == NULL) {
+ test_out("address->localpart = (null)",
+ (parsed->localpart == test->localpart));
+ } else {
+ test_out(t_strdup_printf("address->localpart = \"%s\"",
+ parsed->localpart),
+ null_strcmp(parsed->localpart, test->localpart) == 0);
+ }
+ if (parsed->domain == NULL) {
+ test_out(t_strdup_printf("address->domain = (null)"),
+ (parsed->domain == test->domain));
+ } else {
+ test_out(t_strdup_printf("address->domain = \"%s\"",
+ parsed->domain),
+ null_strcmp(parsed->domain, test->domain) == 0);
+ }
+}
+
+static void test_smtp_mailbox_parse_valid(void)
+{
+ unsigned int i;
+
+ for (i = 0; i < valid_mailbox_parse_test_count; i++) T_BEGIN {
+ const struct valid_mailbox_parse_test *test;
+ struct smtp_address *address;
+ const char *error = NULL, *output, *encoded;
+ int ret;
+
+ test = &valid_mailbox_parse_tests[i];
+ ret = smtp_address_parse_mailbox(pool_datastack_create(),
+ test->input, test->flags,
+ &address, &error);
+
+ test_begin(t_strdup_printf("smtp mailbox valid [%d]", i));
+ test_out_reason(t_strdup_printf("parse(\"%s\")", test->input),
+ ret == 0, error);
+
+ if (!test_has_failed()) {
+ test_smtp_mailbox_equal(&test->address, address);
+
+ encoded = smtp_address_encode(address);
+ output = (test->output == NULL ?
+ test->input : test->output);
+ test_out(t_strdup_printf("encode() = \"%s\"", encoded),
+ strcmp(encoded, output) == 0);
+ }
+ test_end();
+ } T_END;
+}
+
+/*
+ * Valid path parse tests
+ */
+
+struct valid_path_parse_test {
+ const char *input, *output;
+ enum smtp_address_parse_flags flags;
+
+ struct smtp_address address;
+};
+
+static const struct valid_path_parse_test
+valid_path_parse_tests[] = {
+ {
+ .input = "<>",
+ .flags = SMTP_ADDRESS_PARSE_FLAG_ALLOW_EMPTY,
+ .address = { .localpart = NULL, .domain = NULL }
+ },
+ {
+ .input = "<user>",
+ .flags = SMTP_ADDRESS_PARSE_FLAG_ALLOW_LOCALPART,
+ .address = { .localpart = "user", .domain = NULL }
+ },
+ {
+ .input = "<user@domain.tld>",
+ .address = { .localpart = "user", .domain = "domain.tld" }
+ },
+ {
+ .input = "<@otherdomain.tld,@yetanotherdomain.tld:user@domain.tld>",
+ .address = { .localpart = "user", .domain = "domain.tld" },
+ .output = "<user@domain.tld>"
+ },
+ {
+ .input = "user@domain.tld",
+ .flags = SMTP_ADDRESS_PARSE_FLAG_BRACKETS_OPTIONAL,
+ .address = { .localpart = "user", .domain = "domain.tld" },
+ .output = "<user@domain.tld>"
+ },
+ /* Raw */
+ {
+ .input = "<>",
+ .flags = SMTP_ADDRESS_PARSE_FLAG_ALLOW_EMPTY |
+ SMTP_ADDRESS_PARSE_FLAG_PRESERVE_RAW,
+ .address = { .localpart = NULL, .domain = NULL, .raw = NULL }
+ },
+ {
+ .input = "<user>",
+ .flags = SMTP_ADDRESS_PARSE_FLAG_ALLOW_LOCALPART |
+ SMTP_ADDRESS_PARSE_FLAG_PRESERVE_RAW,
+ .address = { .localpart = "user", .domain = NULL,
+ .raw = "user" }
+ },
+ {
+ .input = "<user@domain.tld>",
+ .flags = SMTP_ADDRESS_PARSE_FLAG_PRESERVE_RAW,
+ .address = { .localpart = "user", .domain = "domain.tld",
+ .raw = "user@domain.tld" }
+ },
+ {
+ .input = "<@otherdomain.tld,@yetanotherdomain.tld:user@domain.tld>",
+ .flags = SMTP_ADDRESS_PARSE_FLAG_PRESERVE_RAW,
+ .address = { .localpart = "user", .domain = "domain.tld",
+ .raw = "@otherdomain.tld,@yetanotherdomain.tld:"
+ "user@domain.tld" },
+ .output = "<user@domain.tld>"
+ },
+ {
+ .input = "user@domain.tld",
+ .flags = SMTP_ADDRESS_PARSE_FLAG_BRACKETS_OPTIONAL |
+ SMTP_ADDRESS_PARSE_FLAG_PRESERVE_RAW,
+ .address = { .localpart = "user", .domain = "domain.tld",
+ .raw = "user@domain.tld"},
+ .output = "<user@domain.tld>"
+ },
+ /* Broken */
+ {
+ .input = "<>",
+ .flags = SMTP_ADDRESS_PARSE_FLAG_ALLOW_EMPTY |
+ SMTP_ADDRESS_PARSE_FLAG_PRESERVE_RAW |
+ SMTP_ADDRESS_PARSE_FLAG_IGNORE_BROKEN,
+ .address = { .localpart = NULL, .domain = NULL, .raw = NULL }
+ },
+ {
+ .input = "<user>",
+ .flags = SMTP_ADDRESS_PARSE_FLAG_ALLOW_LOCALPART |
+ SMTP_ADDRESS_PARSE_FLAG_PRESERVE_RAW |
+ SMTP_ADDRESS_PARSE_FLAG_IGNORE_BROKEN,
+ .address = { .localpart = "user", .domain = NULL,
+ .raw = "user" }
+ },
+ {
+ .input = "<user@domain.tld>",
+ .flags = SMTP_ADDRESS_PARSE_FLAG_PRESERVE_RAW |
+ SMTP_ADDRESS_PARSE_FLAG_IGNORE_BROKEN,
+ .address = { .localpart = "user", .domain = "domain.tld",
+ .raw = "user@domain.tld" }
+ },
+ {
+ .input = "<@otherdomain.tld,@yetanotherdomain.tld:user@domain.tld>",
+ .flags = SMTP_ADDRESS_PARSE_FLAG_PRESERVE_RAW |
+ SMTP_ADDRESS_PARSE_FLAG_IGNORE_BROKEN,
+ .address = { .localpart = "user", .domain = "domain.tld",
+ .raw = "@otherdomain.tld,@yetanotherdomain.tld:"
+ "user@domain.tld" },
+ .output = "<user@domain.tld>"
+ },
+ {
+ .input = "user@domain.tld",
+ .flags = SMTP_ADDRESS_PARSE_FLAG_BRACKETS_OPTIONAL |
+ SMTP_ADDRESS_PARSE_FLAG_PRESERVE_RAW |
+ SMTP_ADDRESS_PARSE_FLAG_IGNORE_BROKEN,
+ .address = { .localpart = "user", .domain = "domain.tld",
+ .raw = "user@domain.tld"},
+ .output = "<user@domain.tld>"
+ },
+ {
+ .input = "u\"ser",
+ .flags = SMTP_ADDRESS_PARSE_FLAG_BRACKETS_OPTIONAL |
+ SMTP_ADDRESS_PARSE_FLAG_ALLOW_LOCALPART |
+ SMTP_ADDRESS_PARSE_FLAG_PRESERVE_RAW |
+ SMTP_ADDRESS_PARSE_FLAG_IGNORE_BROKEN,
+ .address = { .localpart = NULL, .domain = NULL,
+ .raw = "u\"ser" },
+ .output = "<>",
+ },
+ {
+ .input = "user\"@domain.tld",
+ .flags = SMTP_ADDRESS_PARSE_FLAG_BRACKETS_OPTIONAL |
+ SMTP_ADDRESS_PARSE_FLAG_PRESERVE_RAW |
+ SMTP_ADDRESS_PARSE_FLAG_IGNORE_BROKEN,
+ .address = { .localpart = NULL, .domain = NULL,
+ .raw = "user\"@domain.tld" },
+ .output = "<>",
+ },
+ {
+ .input = "<u\"ser>",
+ .flags = SMTP_ADDRESS_PARSE_FLAG_ALLOW_LOCALPART |
+ SMTP_ADDRESS_PARSE_FLAG_PRESERVE_RAW |
+ SMTP_ADDRESS_PARSE_FLAG_IGNORE_BROKEN,
+ .address = { .localpart = NULL, .domain = NULL,
+ .raw = "u\"ser" },
+ .output = "<>",
+ },
+ {
+ .input = "<user\"@domain.tld>",
+ .flags = SMTP_ADDRESS_PARSE_FLAG_PRESERVE_RAW |
+ SMTP_ADDRESS_PARSE_FLAG_IGNORE_BROKEN,
+ .address = { .localpart = NULL, .domain = NULL,
+ .raw = "user\"@domain.tld" },
+ .output = "<>",
+ },
+ {
+ .input = "bla$die%bla@die&bla",
+ .flags = SMTP_ADDRESS_PARSE_FLAG_BRACKETS_OPTIONAL |
+ SMTP_ADDRESS_PARSE_FLAG_PRESERVE_RAW |
+ SMTP_ADDRESS_PARSE_FLAG_IGNORE_BROKEN,
+ .address = { .localpart = NULL, .domain = NULL,
+ .raw = "bla$die%bla@die&bla" },
+ .output = "<>",
+ },
+ {
+ .input = "/@)$@)BLAARGH!@#$$",
+ .flags = SMTP_ADDRESS_PARSE_FLAG_BRACKETS_OPTIONAL |
+ SMTP_ADDRESS_PARSE_FLAG_PRESERVE_RAW |
+ SMTP_ADDRESS_PARSE_FLAG_IGNORE_BROKEN,
+ .address = { .localpart = NULL, .domain = NULL,
+ .raw = "/@)$@)BLAARGH!@#$$" },
+ .output = "<>",
+ },
+ {
+ .input = "</@)$@)BLAARGH!@#$$>",
+ .flags = SMTP_ADDRESS_PARSE_FLAG_PRESERVE_RAW |
+ SMTP_ADDRESS_PARSE_FLAG_IGNORE_BROKEN,
+ .address = { .localpart = NULL, .domain = NULL,
+ .raw = "/@)$@)BLAARGH!@#$$" },
+ .output = "<>",
+ },
+ {
+ .input = "/@)$@)BLAARGH!@#$$",
+ .flags = SMTP_ADDRESS_PARSE_FLAG_PRESERVE_RAW |
+ SMTP_ADDRESS_PARSE_FLAG_IGNORE_BROKEN |
+ SMTP_ADDRESS_PARSE_FLAG_ALLOW_BAD_LOCALPART |
+ SMTP_ADDRESS_PARSE_FLAG_BRACKETS_OPTIONAL,
+ .address = { .localpart = NULL, .domain = NULL,
+ .raw = "/@)$@)BLAARGH!@#$$" },
+ .output = "<>",
+ },
+ {
+ .input = "</@)$@)BLAARGH!@#$$>",
+ .flags = SMTP_ADDRESS_PARSE_FLAG_PRESERVE_RAW |
+ SMTP_ADDRESS_PARSE_FLAG_IGNORE_BROKEN |
+ SMTP_ADDRESS_PARSE_FLAG_ALLOW_BAD_LOCALPART |
+ SMTP_ADDRESS_PARSE_FLAG_BRACKETS_OPTIONAL,
+ .address = { .localpart = NULL, .domain = NULL,
+ .raw = "/@)$@)BLAARGH!@#$$" },
+ .output = "<>",
+ },
+ {
+ .input = "f\xc3\xb6\xc3\xa4@\xc3\xb6\xc3\xa4",
+ .flags = SMTP_ADDRESS_PARSE_FLAG_BRACKETS_OPTIONAL |
+ SMTP_ADDRESS_PARSE_FLAG_PRESERVE_RAW |
+ SMTP_ADDRESS_PARSE_FLAG_IGNORE_BROKEN |
+ SMTP_ADDRESS_PARSE_FLAG_ALLOW_BAD_LOCALPART |
+ SMTP_ADDRESS_PARSE_FLAG_BRACKETS_OPTIONAL,
+ .address = { .localpart = NULL, .domain = NULL,
+ .raw = "f\xc3\xb6\xc3\xa4@\xc3\xb6\xc3\xa4" },
+ .output = "<>",
+ },
+ {
+ .input = "<f\xc3\xb6\xc3\xa4@\xc3\xb6\xc3\xa4>",
+ .flags = SMTP_ADDRESS_PARSE_FLAG_PRESERVE_RAW |
+ SMTP_ADDRESS_PARSE_FLAG_IGNORE_BROKEN |
+ SMTP_ADDRESS_PARSE_FLAG_ALLOW_BAD_LOCALPART |
+ SMTP_ADDRESS_PARSE_FLAG_BRACKETS_OPTIONAL,
+ .address = { .localpart = NULL, .domain = NULL,
+ .raw = "f\xc3\xb6\xc3\xa4@\xc3\xb6\xc3\xa4" },
+ .output = "<>",
+ },
+};
+
+unsigned int valid_path_parse_test_count =
+ N_ELEMENTS(valid_path_parse_tests);
+
+static void
+test_smtp_path_equal(const struct smtp_address *test,
+ const struct smtp_address *parsed)
+{
+ if (smtp_address_isnull(parsed) || smtp_address_isnull(test)) {
+ test_out("address = <>",
+ (smtp_address_isnull(parsed) &&
+ smtp_address_isnull(test)));
+ } else {
+ test_out(t_strdup_printf("address->localpart = \"%s\"",
+ parsed->localpart),
+ null_strcmp(parsed->localpart, test->localpart) == 0);
+ }
+ if (smtp_address_isnull(parsed)) {
+ /* nothing */
+ } else if (parsed->domain == NULL) {
+ test_out("address->domain = (null)",
+ (parsed->domain == test->domain));
+ } else {
+ test_out(t_strdup_printf("address->domain = \"%s\"",
+ parsed->domain),
+ null_strcmp(parsed->domain, test->domain) == 0);
+ }
+ if (parsed == NULL) {
+ test_out_quiet(t_strdup_printf("address = (null)"),
+ (test->raw == NULL));
+ } else if (parsed->raw == NULL) {
+ test_out_quiet(t_strdup_printf("address->raw = (null)"),
+ (parsed->raw == test->raw));
+ } else {
+ test_out_quiet(t_strdup_printf("address->raw = \"%s\"",
+ parsed->raw),
+ null_strcmp(parsed->raw, test->raw) == 0);
+ }
+}
+
+static void test_smtp_path_parse_valid(void)
+{
+ unsigned int i;
+
+ for (i = 0; i < valid_path_parse_test_count; i++) T_BEGIN {
+ const struct valid_path_parse_test *test;
+ bool ignore_broken;
+ struct smtp_address *address;
+ const char *error = NULL, *output, *encoded;
+ int ret;
+
+ test = &valid_path_parse_tests[i];
+ ignore_broken = HAS_ALL_BITS(
+ test->flags, SMTP_ADDRESS_PARSE_FLAG_IGNORE_BROKEN);
+ ret = smtp_address_parse_path(pool_datastack_create(),
+ test->input, test->flags,
+ &address, &error);
+
+ test_begin(t_strdup_printf("smtp path valid [%d]", i));
+ test_out_reason(t_strdup_printf("parse(\"%s\")", test->input),
+ (ret == 0 || ignore_broken), error);
+
+ if (!test_has_failed()) {
+ test_smtp_path_equal(&test->address, address);
+
+ encoded = smtp_address_encode_path(address);
+ output = (test->output == NULL ?
+ test->input : test->output);
+ test_out(t_strdup_printf("encode() = \"%s\"", encoded),
+ strcmp(encoded, output) == 0);
+ }
+ test_end();
+ } T_END;
+}
+
+/*
+ * Valid username parse tests
+ */
+
+struct valid_username_parse_test {
+ const char *input, *output;
+
+ struct smtp_address address;
+};
+
+static const struct valid_username_parse_test
+valid_username_parse_tests[] = {
+ {
+ .input = "user",
+ .address = {
+ .localpart = "user",
+ .domain = NULL },
+ },
+ {
+ .input = "user@domain.tld",
+ .address = {
+ .localpart = "user",
+ .domain = "domain.tld" },
+ },
+ {
+ .input = "user@domain.tld",
+ .address = {
+ .localpart = "user",
+ .domain = "domain.tld" },
+ },
+ {
+ .input = "1234567890@domain.tld",
+ .address = {
+ .localpart = "1234567890",
+ .domain = "domain.tld" },
+ },
+ {
+ .input = "_______@domain.tld",
+ .address = {
+ .localpart = "_______",
+ .domain = "domain.tld" },
+ },
+ {
+ .input = "firstname.lastname@domain.tld",
+ .address = {
+ .localpart = "firstname.lastname",
+ .domain = "domain.tld" },
+ },
+ {
+ .input = "firstname+lastname@domain.tld",
+ .address = {
+ .localpart = "firstname+lastname",
+ .domain = "domain.tld" },
+ },
+ {
+ .input = "firstname-lastname@domain.tld",
+ .address = {
+ .localpart = "firstname-lastname",
+ .domain = "domain.tld" },
+ },
+ {
+ .input = "\"user\"@domain.tld",
+ .address = { .localpart = "user", .domain = "domain.tld" },
+ .output = "user@domain.tld"
+ },
+ {
+ .input = "\"user@frop\"@domain.tld",
+ .address = { .localpart = "user@frop", .domain = "domain.tld" },
+ .output = "\"user@frop\"@domain.tld"
+ },
+ {
+ .input = "user@frop@domain.tld",
+ .address = { .localpart = "user@frop", .domain = "domain.tld" },
+ .output = "\"user@frop\"@domain.tld"
+ },
+ {
+ .input = "user frop@domain.tld",
+ .address = { .localpart = "user frop", .domain = "domain.tld" },
+ .output = "\"user frop\"@domain.tld"
+ },
+ {
+ .input = "user\"frop@domain.tld",
+ .address = { .localpart = "user\"frop", .domain = "domain.tld" },
+ .output = "\"user\\\"frop\"@domain.tld"
+ },
+ {
+ .input = "user\\frop@domain.tld",
+ .address = { .localpart = "user\\frop", .domain = "domain.tld" },
+ .output = "\"user\\\\frop\"@domain.tld"
+ },
+ {
+ .input = "user@127.0.0.1",
+ .address = { .localpart = "user", .domain = "127.0.0.1" },
+ },
+ {
+ .input = "user@[127.0.0.1]",
+ .address = { .localpart = "user", .domain = "[127.0.0.1]" },
+ },
+ {
+ .input = "user@[IPv6:::1]",
+ .address = { .localpart = "user", .domain = "[IPv6:::1]" },
+ },
+ {
+ .input = "user@[IPv6:::127.0.0.1]",
+ .address = { .localpart = "user", .domain = "[IPv6:::127.0.0.1]" },
+ },
+};
+
+unsigned int valid_username_parse_test_count =
+ N_ELEMENTS(valid_username_parse_tests);
+
+static void test_smtp_username_parse_valid(void)
+{
+ unsigned int i;
+
+ for (i = 0; i < valid_username_parse_test_count; i++) T_BEGIN {
+ const struct valid_username_parse_test *test;
+ struct smtp_address *address;
+ const char *error = NULL, *output, *encoded;
+ int ret;
+
+ test = &valid_username_parse_tests[i];
+ ret = smtp_address_parse_username(pool_datastack_create(),
+ test->input,
+ &address, &error);
+
+ test_begin(t_strdup_printf("smtp username valid [%d]", i));
+ test_out_reason(t_strdup_printf("parse(\"%s\")", test->input),
+ ret == 0, error);
+
+ if (!test_has_failed()) {
+ test_smtp_path_equal(&test->address, address);
+
+ encoded = smtp_address_encode(address);
+ output = (test->output == NULL ?
+ test->input : test->output);
+ test_out(t_strdup_printf("encode() = \"%s\"", encoded),
+ strcmp(encoded, output) == 0);
+ }
+ test_end();
+ } T_END;
+}
+
+/*
+ * Invalid mailbox parse tests
+ */
+
+struct invalid_mailbox_parse_test {
+ const char *input;
+ enum smtp_address_parse_flags flags;
+};
+
+static const struct invalid_mailbox_parse_test
+invalid_mailbox_parse_tests[] = {
+ {
+ .input = "",
+ },
+ {
+ .input = "user",
+ },
+ {
+ .input = "\"user@domain.tld",
+ },
+ {
+ .input = "us\"er@domain.tld",
+ },
+ {
+ .input = "user@frop@domain.tld",
+ },
+ {
+ .input = "user@.tld",
+ },
+ {
+ .input = "user@a$.tld",
+ },
+ {
+ .input = "user@a..tld",
+ },
+ {
+ .input = "user@[]",
+ },
+ {
+ .input = "user@[",
+ },
+ {
+ .input = "user@[AA]",
+ },
+ {
+ .input = "user@[AA",
+ },
+ {
+ .input = "user@[127.0.0]",
+ },
+ {
+ .input = "user@[256.256.256.256]",
+ },
+ {
+ .input = "user@[127.0.0.1",
+ },
+ {
+ .input = "user@[::1]",
+ },
+ {
+ .input = "user@[IPv6:flierp]",
+ },
+ {
+ .input = "user@[IPv6:aa:bb::cc::dd]",
+ },
+ {
+ .input = "user@[IPv6::1]",
+ },
+ {
+ .input = "user@[IPv6:::1",
+ },
+ {
+ .input = "user@[Gen:]",
+ },
+ {
+ .input = "user@[Gen:Hopsa",
+ },
+ {
+ .input = "user@[Gen-:Hopsa]",
+ },
+ {
+ .input = "#@%^%#$@#$@#.com",
+ },
+ {
+ .input = "@example.com",
+ },
+ {
+ .input = "Eric Mail <email@example.com>",
+ },
+ {
+ .input = "email.example.com",
+ },
+ {
+ .input = "email@example@example.com",
+ },
+ {
+ .input = "あいうえお@example.com",
+ },
+ {
+ .input = "email@example.com (Eric Mail)",
+ },
+ {
+ .input = "email@example..com",
+#if 0 /* These deviations are allowed (maybe implement strict mode) */
+ },
+ {
+ .input = "email@-example.com",
+ },
+ {
+ .input = ".email@example.com",
+ },
+ {
+ .input = "email.@example.com",
+ },
+ {
+ .input = "email..email@example.com",
+ },
+ {
+ .input = "Abc..123@example.com"
+#endif
+ },
+};
+
+unsigned int invalid_mailbox_parse_test_count =
+ N_ELEMENTS(invalid_mailbox_parse_tests);
+
+static void test_smtp_mailbox_parse_invalid(void)
+{
+ unsigned int i;
+
+ for (i = 0; i < invalid_mailbox_parse_test_count; i++) T_BEGIN {
+ const struct invalid_mailbox_parse_test *test;
+ struct smtp_address *address;
+ const char *error = NULL;
+ int ret;
+
+ test = &invalid_mailbox_parse_tests[i];
+ ret = smtp_address_parse_mailbox(pool_datastack_create(),
+ test->input, test->flags,
+ &address, &error);
+
+ test_begin(t_strdup_printf("smtp mailbox invalid [%d]", i));
+ test_out_reason(t_strdup_printf("parse(\"%s\")", test->input),
+ ret < 0, error);
+ test_end();
+ } T_END;
+}
+
+/*
+ * Invalid path parse tests
+ */
+
+struct invalid_path_parse_test {
+ const char *input;
+ enum smtp_address_parse_flags flags;
+};
+
+static const struct invalid_path_parse_test
+invalid_path_parse_tests[] = {
+ {
+ .input = "",
+ .flags = SMTP_ADDRESS_PARSE_FLAG_BRACKETS_OPTIONAL
+ },
+ {
+ .input = "user",
+ .flags = SMTP_ADDRESS_PARSE_FLAG_BRACKETS_OPTIONAL
+ },
+ {
+ .input = "\"user@domain.tld",
+ .flags = SMTP_ADDRESS_PARSE_FLAG_BRACKETS_OPTIONAL
+ },
+ {
+ .input = "us\"er@domain.tld",
+ .flags = SMTP_ADDRESS_PARSE_FLAG_BRACKETS_OPTIONAL
+ },
+ {
+ .input = "user@frop@domain.tld",
+ .flags = SMTP_ADDRESS_PARSE_FLAG_BRACKETS_OPTIONAL
+ },
+ {
+ .input = "user@.tld",
+ .flags = SMTP_ADDRESS_PARSE_FLAG_BRACKETS_OPTIONAL
+ },
+ {
+ .input = "user@a$.tld",
+ .flags = SMTP_ADDRESS_PARSE_FLAG_BRACKETS_OPTIONAL
+ },
+ {
+ .input = "user@a..tld",
+ .flags = SMTP_ADDRESS_PARSE_FLAG_BRACKETS_OPTIONAL
+ },
+ {
+ .input = "user@[]",
+ .flags = SMTP_ADDRESS_PARSE_FLAG_BRACKETS_OPTIONAL
+ },
+ {
+ .input = "user@[",
+ .flags = SMTP_ADDRESS_PARSE_FLAG_BRACKETS_OPTIONAL
+ },
+ {
+ .input = "user@[AA]",
+ .flags = SMTP_ADDRESS_PARSE_FLAG_BRACKETS_OPTIONAL
+ },
+ {
+ .input = "user@[AA",
+ .flags = SMTP_ADDRESS_PARSE_FLAG_BRACKETS_OPTIONAL
+ },
+ {
+ .input = "user@[127.0.0]",
+ .flags = SMTP_ADDRESS_PARSE_FLAG_BRACKETS_OPTIONAL
+ },
+ {
+ .input = "user@[256.256.256.256]",
+ .flags = SMTP_ADDRESS_PARSE_FLAG_BRACKETS_OPTIONAL
+ },
+ {
+ .input = "user@[127.0.0.1",
+ .flags = SMTP_ADDRESS_PARSE_FLAG_BRACKETS_OPTIONAL
+ },
+ {
+ .input = "user@[::1]",
+ .flags = SMTP_ADDRESS_PARSE_FLAG_BRACKETS_OPTIONAL
+ },
+ {
+ .input = "user@[IPv6:flierp]",
+ .flags = SMTP_ADDRESS_PARSE_FLAG_BRACKETS_OPTIONAL
+ },
+ {
+ .input = "user@[IPv6:aa:bb::cc::dd]",
+ .flags = SMTP_ADDRESS_PARSE_FLAG_BRACKETS_OPTIONAL
+ },
+ {
+ .input = "user@[IPv6::1]",
+ .flags = SMTP_ADDRESS_PARSE_FLAG_BRACKETS_OPTIONAL
+ },
+ {
+ .input = "user@[IPv6:::1",
+ .flags = SMTP_ADDRESS_PARSE_FLAG_BRACKETS_OPTIONAL
+ },
+ {
+ .input = "user@[Gen:]",
+ .flags = SMTP_ADDRESS_PARSE_FLAG_BRACKETS_OPTIONAL
+ },
+ {
+ .input = "user@[Gen:Hopsa",
+ .flags = SMTP_ADDRESS_PARSE_FLAG_BRACKETS_OPTIONAL
+ },
+ {
+ .input = "user@[Gen-:Hopsa]",
+ .flags = SMTP_ADDRESS_PARSE_FLAG_BRACKETS_OPTIONAL
+ },
+ {
+ .input = "#@%^%#$@#$@#.com",
+ .flags = SMTP_ADDRESS_PARSE_FLAG_BRACKETS_OPTIONAL
+ },
+ {
+ .input = "@example.com",
+ .flags = SMTP_ADDRESS_PARSE_FLAG_BRACKETS_OPTIONAL
+ },
+ {
+ .input = "Eric Mail <email@example.com>",
+ .flags = SMTP_ADDRESS_PARSE_FLAG_BRACKETS_OPTIONAL
+ },
+ {
+ .input = "email.example.com",
+ .flags = SMTP_ADDRESS_PARSE_FLAG_BRACKETS_OPTIONAL
+ },
+ {
+ .input = "email@example@example.com",
+ .flags = SMTP_ADDRESS_PARSE_FLAG_BRACKETS_OPTIONAL
+ },
+ {
+ .input = "あいうえお@example.com",
+ .flags = SMTP_ADDRESS_PARSE_FLAG_BRACKETS_OPTIONAL
+ },
+ {
+ .input = "email@example.com (Eric Mail)",
+ .flags = SMTP_ADDRESS_PARSE_FLAG_BRACKETS_OPTIONAL
+ },
+ {
+ .input = "email@example..com",
+ .flags = SMTP_ADDRESS_PARSE_FLAG_BRACKETS_OPTIONAL
+ },
+ {
+ .input = "@otherdomain.tld,@yetanotherdomain.tld:user@domain.tld",
+ .flags = SMTP_ADDRESS_PARSE_FLAG_BRACKETS_OPTIONAL
+ },
+ {
+ .input = "user@domain.tld",
+ .flags = SMTP_ADDRESS_PARSE_FLAG_IGNORE_BROKEN,
+ },
+ {
+ .input = "<>",
+ },
+ {
+ .input = "<user>",
+ },
+ {
+ .input = "<\"user@domain.tld>",
+ },
+ {
+ .input = "<us\"er@domain.tld>",
+ },
+ {
+ .input = "<user@frop@domain.tld>",
+ },
+ {
+ .input = "<user@.tld>",
+ },
+ {
+ .input = "<user@a$.tld>",
+ },
+ {
+ .input = "<user@a..tld>",
+ },
+ {
+ .input = "<user@[]>",
+ },
+ {
+ .input = "<user@[>",
+ },
+ {
+ .input = "<user@[AA]>",
+ },
+ {
+ .input = "<user@[AA>",
+ },
+ {
+ .input = "<user@[127.0.0]>",
+ },
+ {
+ .input = "<user@[256.256.256.256]>",
+ },
+ {
+ .input = "<user@[127.0.0.1>",
+ },
+ {
+ .input = "<user@[::1]>",
+ },
+ {
+ .input = "<user@[IPv6:flierp]>",
+ },
+ {
+ .input = "<user@[IPv6:aa:bb::cc::dd]>",
+ },
+ {
+ .input = "<user@[IPv6::1]>",
+ },
+ {
+ .input = "<user@[IPv6:::1>",
+ },
+ {
+ .input = "<user@[Gen:]>",
+ },
+ {
+ .input = "<user@[Gen:Hopsa>",
+ },
+ {
+ .input = "<user@[Gen-:Hopsa]>",
+ },
+ {
+ .input = "<#@%^%#$@#$@#.com>",
+ },
+ {
+ .input = "<@example.com>",
+ },
+ {
+ .input = "Eric Mail <email@example.com>",
+ },
+ {
+ .input = "<email.example.com>",
+ },
+ {
+ .input = "<email@example@example.com>",
+ },
+ {
+ .input = "<あいうえお@example.com>",
+ },
+ {
+ .input = "<email@example.com> (Eric Mail)",
+ },
+ {
+ .input = "<email@example..com>",
+ },
+ {
+ .input = "<email@example.com",
+ },
+ {
+ .input = "email@example.com>",
+ },
+ {
+ .input = "email@example.com>",
+ .flags = SMTP_ADDRESS_PARSE_FLAG_BRACKETS_OPTIONAL
+ },
+ {
+ .input = "<",
+ .flags = SMTP_ADDRESS_PARSE_FLAG_ALLOW_EMPTY,
+ },
+ {
+ .input = "<user",
+ .flags = SMTP_ADDRESS_PARSE_FLAG_ALLOW_LOCALPART,
+ },
+ {
+ .input = "<@otherdomain.tld,@yetanotherdomain.tld.user@domain.tld>",
+ },
+ {
+ .input = "<@###domain.tld,@yetanotherdomain.tld.user@domain.tld>",
+ },
+ {
+ .input = "f\xc3\xb6\xc3\xa4@\xc3\xb6\xc3\xa4",
+ .flags = SMTP_ADDRESS_PARSE_FLAG_IGNORE_BROKEN,
+ }
+};
+
+unsigned int invalid_path_parse_test_count =
+ N_ELEMENTS(invalid_path_parse_tests);
+
+static void test_smtp_path_parse_invalid(void)
+{
+ unsigned int i;
+
+ for (i = 0; i < invalid_path_parse_test_count; i++) T_BEGIN {
+ const struct invalid_path_parse_test *test;
+ struct smtp_address *address;
+ const char *error = NULL;
+ int ret;
+
+ test = &invalid_path_parse_tests[i];
+ ret = smtp_address_parse_path(pool_datastack_create(),
+ test->input, test->flags,
+ &address, &error);
+
+ test_begin(t_strdup_printf("smtp path invalid [%d]", i));
+ test_out_reason(t_strdup_printf("parse(\"%s\")", test->input),
+ (ret < 0 && !smtp_address_is_broken(address)),
+ error);
+ test_end();
+ } T_END;
+}
+
+/*
+ * Invalid username parse tests
+ */
+
+struct invalid_username_parse_test {
+ const char *input;
+ enum smtp_address_parse_flags flags;
+};
+
+static const struct invalid_username_parse_test
+invalid_username_parse_tests[] = {
+ {
+ .input = "frop@$%^$%^.tld",
+ },
+ {
+ .input = "fr\top@domain.tld",
+ }
+};
+
+unsigned int invalid_username_parse_test_count =
+ N_ELEMENTS(invalid_username_parse_tests);
+
+static void test_smtp_username_parse_invalid(void)
+{
+ unsigned int i;
+
+ for (i = 0; i < invalid_username_parse_test_count; i++) T_BEGIN {
+ const struct invalid_username_parse_test *test;
+ struct smtp_address *address;
+ const char *error = NULL;
+ int ret;
+
+ test = &invalid_username_parse_tests[i];
+ ret = smtp_address_parse_username(pool_datastack_create(),
+ test->input,
+ &address, &error);
+
+ test_begin(t_strdup_printf("smtp username invalid [%d]", i));
+ test_out_reason(t_strdup_printf("parse(\"%s\")", test->input),
+ ret < 0, error);
+ test_end();
+ } T_END;
+}
+
+/*
+ * Address detail parsing
+ */
+
+struct address_detail_parse_test {
+ const char *delimiters;
+ const char *address;
+ const char *username;
+ const char *detail;
+ char delim;
+};
+
+static const struct address_detail_parse_test
+address_detail_parse_tests[] = {
+ { "", "test", "test", "", '\0' },
+ { "", "test+address", "test+address", "", '\0' },
+ { "", "\"test:address\"", "test:address", "", '\0' },
+ { "", "\"test-address:another+delim\"", "test-address:another+delim",
+ "", '\0' },
+ { "", "test@domain", "test@domain", "", '\0' },
+ { "", "test+address@domain", "test+address@domain", "", '\0' },
+ { "", "\"test:address\"@domain", "test:address@domain", "", '\0' },
+ { "", "\"test-address:another+delim\"@domain",
+ "test-address:another+delim@domain", "", '\0' },
+
+ { "+-:", "test", "test", "", '\0' },
+ { "+-:", "test+address", "test", "address", '+' },
+ { "+-:", "\"test:address\"", "test", "address", ':' },
+ { "+-:", "\"test-address:another+delim\"",
+ "test", "address:another+delim", '-' },
+ { "+-:", "test@domain", "test@domain", "", '\0' },
+ { "+-:", "test+address@domain", "test@domain", "address", '+' },
+ { "+-:", "\"test:address\"@domain", "test@domain", "address", ':' },
+ { "+-:", "\"test-address:another+delim\"@domain", "test@domain",
+ "address:another+delim", '-' },
+};
+
+unsigned int addresss_detail_parse_test_count =
+ N_ELEMENTS(address_detail_parse_tests);
+
+static void test_smtp_address_detail_parse(void)
+{
+ unsigned int i;
+
+
+ for (i = 0; i < N_ELEMENTS(address_detail_parse_tests); i++) T_BEGIN {
+ const struct address_detail_parse_test *test =
+ &address_detail_parse_tests[i];
+ struct smtp_address *address;
+ const char *username, *detail, *error;
+ char delim;
+ int ret;
+
+ test_begin(t_strdup_printf(
+ "smtp address detail parsing [%d]", i));
+
+ ret = smtp_address_parse_path(
+ pool_datastack_create(), test->address,
+ SMTP_ADDRESS_PARSE_FLAG_ALLOW_LOCALPART |
+ SMTP_ADDRESS_PARSE_FLAG_BRACKETS_OPTIONAL,
+ &address, &error);
+ test_out_reason("address parse", ret == 0, error);
+
+ if (!test_has_failed()) {
+ smtp_address_detail_parse_temp(test->delimiters,
+ address, &username,
+ &delim, &detail);
+ test_assert(strcmp(username, test->username) == 0);
+ test_assert(strcmp(detail, test->detail) == 0);
+ test_assert(delim == test->delim);
+ }
+
+ test_end();
+ } T_END;
+}
+
+/*
+ * Skip address tests
+ */
+
+struct any_address_parse_test {
+ const char *input;
+ const char *address;
+ size_t pos;
+ int ret;
+};
+
+static const struct any_address_parse_test
+any_address_parse_tests[] = {
+ {
+ .input = "",
+ .address = "",
+ .pos = 0,
+ .ret = 0,
+ },
+ {
+ .input = " ",
+ .address = "",
+ .pos = 0,
+ .ret = 0,
+ },
+ {
+ .input = "frop@example.com",
+ .address = "frop@example.com",
+ .pos = 16,
+ .ret = 0,
+ },
+ {
+ .input = "frop@example.com ",
+ .address = "frop@example.com",
+ .pos = 16,
+ .ret = 0,
+ },
+ {
+ .input = "<frop@example.com>",
+ .address = "frop@example.com",
+ .pos = 18,
+ .ret = 0,
+ },
+ {
+ .input = "<frop@example.com> ",
+ .address = "frop@example.com",
+ .pos = 18,
+ .ret = 0,
+ },
+ {
+ .input = "<frop@example.com",
+ .pos = 0,
+ .ret = -1,
+ },
+ {
+ .input = "<frop@example.com ",
+ .pos = 0,
+ .ret = -1,
+ },
+ {
+ .input = "fr\"op@example.com",
+ .address = "fr\"op@example.com",
+ .pos = 17,
+ .ret = 0,
+ },
+ {
+ .input = "fr\"op@example.com ",
+ .address = "fr\"op@example.com",
+ .pos = 17,
+ .ret = 0,
+ },
+ {
+ .input = "fr<op@example.com",
+ .address = "fr<op@example.com",
+ .pos = 17,
+ .ret = 0,
+ },
+ {
+ .input = "fr<op@example.com ",
+ .address = "fr<op@example.com",
+ .pos = 17,
+ .ret = 0,
+ },
+ {
+ .input = "\"frop\"@example.com",
+ .address = "\"frop\"@example.com",
+ .pos = 18,
+ .ret = 0,
+ },
+ {
+ .input = "\"frop\"@example.com ",
+ .address = "\"frop\"@example.com",
+ .pos = 18,
+ .ret = 0,
+ },
+ {
+ .input = "\"frop\\\"@example.com",
+ .pos = 0,
+ .ret = -1,
+ },
+ {
+ .input = "\"frop\\\"@example.com ",
+ .pos = 0,
+ .ret = -1,
+ },
+ {
+ .input = "<\"fr>op\"@example.com>",
+ .address = "\"fr>op\"@example.com",
+ .pos = 21,
+ .ret = 0,
+ },
+ {
+ .input = "<\"fr>op\"@example.com> ",
+ .address = "\"fr>op\"@example.com",
+ .pos = 21,
+ .ret = 0,
+ },
+ {
+ .input = "<\"fr>op\"@example.com",
+ .pos = 0,
+ .ret = -1,
+ },
+ {
+ .input = "<\"fr>op\"@example.com ",
+ .pos = 0,
+ .ret = -1,
+ },
+ {
+ .input = "<\"frop\">",
+ .address = "\"frop\"",
+ .pos = 8,
+ .ret = 0,
+ },
+ {
+ .input = "<\"frop\"> ",
+ .address = "\"frop\"",
+ .pos = 8,
+ .ret = 0,
+ },
+ {
+ .input = "<\"frop\"",
+ .pos = 0,
+ .ret = -1,
+ },
+ {
+ .input = "<\"frop\" ",
+ .pos = 0,
+ .ret = -1,
+ },
+ {
+ .input = "\"frop\\\" ",
+ .pos = 0,
+ .ret = -1,
+ },
+ {
+ .input = "\"frop\\\"",
+ .pos = 0,
+ .ret = -1,
+ },
+};
+
+unsigned int any_address_parse_tests_count =
+ N_ELEMENTS(any_address_parse_tests);
+
+static void test_smtp_parse_any_address(void)
+{
+ unsigned int i;
+
+ for (i = 0; i < any_address_parse_tests_count; i++) T_BEGIN {
+ const struct any_address_parse_test *test;
+ const char *address = NULL, *pos = NULL;
+ int ret;
+
+ test = &any_address_parse_tests[i];
+ ret = smtp_address_parse_any(test->input, &address, &pos);
+
+ test_begin(t_strdup_printf("smtp parse any [%d]", i));
+ test_out_quiet(t_strdup_printf("parse(\"%s\")",
+ str_sanitize(test->input, 256)),
+ (ret == test->ret) &&
+ ((size_t)(pos - test->input) == test->pos) &&
+ (null_strcmp(test->address, address ) == 0));
+ test_end();
+ } T_END;
+}
+
+/*
+ * Tests
+ */
+
+int main(void)
+{
+ static void (*test_functions[])(void) = {
+ test_smtp_mailbox_parse_valid,
+ test_smtp_path_parse_valid,
+ test_smtp_username_parse_valid,
+ test_smtp_mailbox_parse_invalid,
+ test_smtp_path_parse_invalid,
+ test_smtp_username_parse_invalid,
+ test_smtp_address_detail_parse,
+ test_smtp_parse_any_address,
+ NULL
+ };
+ return test_run(test_functions);
+}
diff --git a/src/lib-smtp/test-smtp-client-errors.c b/src/lib-smtp/test-smtp-client-errors.c
new file mode 100644
index 0000000..673ed04
--- /dev/null
+++ b/src/lib-smtp/test-smtp-client-errors.c
@@ -0,0 +1,4374 @@
+/* Copyright (c) 2016-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "str.h"
+#include "hostpid.h"
+#include "ioloop.h"
+#include "istream.h"
+#include "istream-dot.h"
+#include "istream-chain.h"
+#include "istream-failure-at.h"
+#include "ostream.h"
+#include "iostream-ssl.h"
+#include "iostream-ssl-test.h"
+#ifdef HAVE_OPENSSL
+# include "iostream-openssl.h"
+#endif
+#include "time-util.h"
+#include "sleep.h"
+#include "connection.h"
+#include "test-common.h"
+#include "test-subprocess.h"
+#include "smtp-client.h"
+#include "smtp-client-connection.h"
+#include "smtp-client-transaction.h"
+
+#include <unistd.h>
+
+#define CLIENT_PROGRESS_TIMEOUT 10
+#define SERVER_KILL_TIMEOUT_SECS 20
+
+static void main_deinit(void);
+
+/*
+ * Types
+ */
+
+enum server_connection_state {
+ SERVER_CONNECTION_STATE_EHLO = 0,
+ SERVER_CONNECTION_STATE_MAIL_FROM,
+ SERVER_CONNECTION_STATE_RCPT_TO,
+ SERVER_CONNECTION_STATE_DATA,
+ SERVER_CONNECTION_STATE_FINISH
+};
+
+struct server_connection {
+ struct connection conn;
+ void *context;
+
+ struct ssl_iostream *ssl_iostream;
+
+ enum server_connection_state state;
+ char *file_path;
+ struct istream *dot_input;
+
+ pool_t pool;
+
+ bool version_sent:1;
+};
+
+typedef void (*test_server_init_t)(unsigned int index);
+typedef bool
+(*test_client_init_t)(const struct smtp_client_settings *client_set);
+typedef void (*test_dns_init_t)(void);
+
+/*
+ * State
+ */
+
+/* common */
+static struct ip_addr bind_ip;
+static in_port_t *bind_ports = 0;
+static struct ioloop *ioloop;
+static bool debug = FALSE;
+
+/* server */
+static struct io *io_listen;
+static int fd_listen = -1;
+static struct connection_list *server_conn_list;
+static unsigned int server_index;
+struct ssl_iostream_context *server_ssl_ctx = NULL;
+bool test_server_ssl = FALSE;
+static void (*test_server_input)(struct server_connection *conn);
+static int
+(*test_server_input_line)(struct server_connection *conn, const char *line);
+static int
+(*test_server_input_data)(struct server_connection *conn,
+ const unsigned char *data, size_t size);
+static int (*test_server_init)(struct server_connection *conn);
+static void (*test_server_deinit)(struct server_connection *conn);
+
+/* client */
+static struct timeout *to_client_progress = NULL;
+static struct smtp_client *smtp_client = NULL;
+
+/*
+ * Forward declarations
+ */
+
+/* server */
+static void test_server_run(unsigned int index);
+static void server_connection_deinit(struct server_connection **_conn);
+
+/* client */
+static void test_client_defaults(struct smtp_client_settings *smtp_set);
+static void test_client_deinit(void);
+
+/* test*/
+static void
+test_run_client_server(const struct smtp_client_settings *client_set,
+ test_client_init_t client_test,
+ test_server_init_t server_test,
+ unsigned int server_tests_count,
+ test_dns_init_t dns_test) ATTR_NULL(3);
+
+/*
+ * Unconfigured SSL
+ */
+
+/* server */
+
+static void
+test_server_unconfigured_ssl_input(struct server_connection *conn ATTR_UNUSED)
+{
+ /* nothing */
+}
+
+static void test_server_unconfigured_ssl(unsigned int index)
+{
+ i_sleep_intr_secs(100);
+ test_server_input = test_server_unconfigured_ssl_input;
+ test_server_run(index);
+}
+
+/* client */
+
+struct _unconfigured_ssl {
+ unsigned int count;
+};
+
+static void
+test_client_unconfigured_ssl_reply(const struct smtp_reply *reply,
+ void *context)
+{
+ struct _unconfigured_ssl *ctx = (struct _unconfigured_ssl *)context;
+
+ if (debug)
+ i_debug("REPLY: %s", smtp_reply_log(reply));
+
+ test_assert(reply->status == SMTP_CLIENT_COMMAND_ERROR_CONNECT_FAILED);
+
+ if (--ctx->count == 0) {
+ i_free(ctx);
+ io_loop_stop(ioloop);
+ }
+}
+
+static bool
+test_client_unconfigured_ssl(const struct smtp_client_settings *client_set)
+{
+ struct smtp_client_connection *sconn;
+ struct _unconfigured_ssl *ctx;
+
+ test_expect_errors(2);
+
+ ctx = i_new(struct _unconfigured_ssl, 1);
+ ctx->count = 2;
+
+ smtp_client = smtp_client_init(client_set);
+
+ sconn = smtp_client_connection_create(
+ smtp_client, SMTP_PROTOCOL_SMTP, "127.0.0.1", bind_ports[0],
+ SMTP_CLIENT_SSL_MODE_IMMEDIATE, NULL);
+ smtp_client_connection_connect(sconn,
+ test_client_unconfigured_ssl_reply, ctx);
+
+ sconn = smtp_client_connection_create(
+ smtp_client, SMTP_PROTOCOL_SMTP, "127.0.0.1", bind_ports[0],
+ SMTP_CLIENT_SSL_MODE_IMMEDIATE, NULL);
+ smtp_client_connection_connect(
+ sconn, test_client_unconfigured_ssl_reply, ctx);
+
+ return TRUE;
+}
+
+/* test */
+
+static void test_unconfigured_ssl(void)
+{
+ struct smtp_client_settings smtp_client_set;
+
+ test_client_defaults(&smtp_client_set);
+
+ test_begin("unconfigured ssl");
+ test_run_client_server(&smtp_client_set,
+ test_client_unconfigured_ssl,
+ test_server_unconfigured_ssl, 1, NULL);
+ test_end();
+}
+
+/*
+ * Unconfigured SSL abort
+ */
+
+/* server */
+
+static void
+test_server_unconfigured_ssl_abort_input(
+ struct server_connection *conn ATTR_UNUSED)
+{
+ /* nothing */
+}
+
+static void test_server_unconfigured_ssl_abort(unsigned int index)
+{
+ i_sleep_intr_secs(100);
+ test_server_input = test_server_unconfigured_ssl_abort_input;
+ test_server_run(index);
+}
+
+/* client */
+
+struct _unconfigured_ssl_abort {
+ unsigned int count;
+};
+
+static void
+test_client_unconfigured_ssl_abort_reply1(
+ const struct smtp_reply *reply,
+ struct _unconfigured_ssl_abort *ctx ATTR_UNUSED)
+{
+ if (debug)
+ i_debug("REPLY: %s", smtp_reply_log(reply));
+
+ test_out_quiet("inappropriate callback", FALSE);
+}
+
+static void
+test_client_unconfigured_ssl_abort_reply2(const struct smtp_reply *reply,
+ struct _unconfigured_ssl_abort *ctx)
+{
+ if (debug)
+ i_debug("REPLY: %s", smtp_reply_log(reply));
+
+ test_assert(reply->status == SMTP_CLIENT_COMMAND_ERROR_CONNECT_FAILED);
+
+ i_free(ctx);
+ io_loop_stop(ioloop);
+}
+
+static bool
+test_client_unconfigured_ssl_abort(
+ const struct smtp_client_settings *client_set)
+{
+ struct smtp_client_connection *sconn;
+ struct smtp_client_command *scmd;
+ struct _unconfigured_ssl_abort *ctx;
+
+ test_expect_errors(2);
+
+ ctx = i_new(struct _unconfigured_ssl_abort, 1);
+ ctx->count = 1;
+
+ smtp_client = smtp_client_init(client_set);
+
+ sconn = smtp_client_connection_create(
+ smtp_client, SMTP_PROTOCOL_SMTP, "127.0.0.1", bind_ports[0],
+ SMTP_CLIENT_SSL_MODE_IMMEDIATE, NULL);
+ smtp_client_connection_connect(sconn, NULL, NULL);
+ scmd = smtp_client_command_new(
+ sconn, 0, test_client_unconfigured_ssl_abort_reply1, ctx);
+ smtp_client_command_write(scmd, "FROP");
+ smtp_client_command_submit(scmd);
+ smtp_client_command_abort(&scmd);
+
+ sconn = smtp_client_connection_create(
+ smtp_client, SMTP_PROTOCOL_SMTP, "127.0.0.1", bind_ports[0],
+ SMTP_CLIENT_SSL_MODE_IMMEDIATE, NULL);
+ smtp_client_connection_connect(sconn, NULL, NULL);
+ scmd = smtp_client_command_new(
+ sconn, 0, test_client_unconfigured_ssl_abort_reply2, ctx);
+ smtp_client_command_write(scmd, "FROP");
+ smtp_client_command_submit(scmd);
+
+ return TRUE;
+}
+
+/* test */
+
+static void test_unconfigured_ssl_abort(void)
+{
+ struct smtp_client_settings smtp_client_set;
+
+ test_client_defaults(&smtp_client_set);
+
+ test_begin("unconfigured ssl abort");
+ test_run_client_server(&smtp_client_set,
+ test_client_unconfigured_ssl_abort,
+ test_server_unconfigured_ssl_abort, 1, NULL);
+ test_end();
+}
+
+/*
+ * Host lookup failed
+ */
+
+/* client */
+
+struct _host_lookup_failed {
+ unsigned int count;
+};
+
+static void
+test_client_host_lookup_failed_reply(const struct smtp_reply *reply,
+ struct _host_lookup_failed *ctx)
+{
+ if (debug)
+ i_debug("REPLY: %s", smtp_reply_log(reply));
+
+ test_assert(reply->status ==
+ SMTP_CLIENT_COMMAND_ERROR_HOST_LOOKUP_FAILED);
+
+ if (--ctx->count == 0) {
+ i_free(ctx);
+ io_loop_stop(ioloop);
+ }
+}
+
+static bool
+test_client_host_lookup_failed(const struct smtp_client_settings *client_set)
+{
+ struct smtp_client_connection *sconn;
+ struct smtp_client_command *scmd;
+ struct _host_lookup_failed *ctx;
+
+ test_expect_errors(2);
+
+ ctx = i_new(struct _host_lookup_failed, 1);
+ ctx->count = 2;
+
+ smtp_client = smtp_client_init(client_set);
+
+ sconn = smtp_client_connection_create(
+ smtp_client, SMTP_PROTOCOL_SMTP, "host.in-addr.arpa", 465,
+ SMTP_CLIENT_SSL_MODE_IMMEDIATE, NULL);
+ smtp_client_connection_connect(sconn, NULL, NULL);
+ scmd = smtp_client_command_new(
+ sconn, 0, test_client_host_lookup_failed_reply, ctx);
+ smtp_client_command_write(scmd, "FROP");
+ smtp_client_command_submit(scmd);
+
+ sconn = smtp_client_connection_create(
+ smtp_client, SMTP_PROTOCOL_SMTP, "host.in-addr.arpa", 465,
+ SMTP_CLIENT_SSL_MODE_IMMEDIATE, NULL);
+ smtp_client_connection_connect(sconn, NULL, NULL);
+ scmd = smtp_client_command_new(
+ sconn, 0, test_client_host_lookup_failed_reply, ctx);
+ smtp_client_command_write(scmd, "FROP");
+ smtp_client_command_submit(scmd);
+
+ return TRUE;
+}
+
+/* test */
+
+static void test_host_lookup_failed(void)
+{
+ struct smtp_client_settings smtp_client_set;
+
+ test_client_defaults(&smtp_client_set);
+
+ test_begin("host lookup failed");
+ test_run_client_server(&smtp_client_set,
+ test_client_host_lookup_failed, NULL, 0, NULL);
+ test_end();
+}
+
+/*
+ * Connection refused
+ */
+
+/* server */
+
+static void test_server_connection_refused(unsigned int index ATTR_UNUSED)
+{
+ i_close_fd(&fd_listen);
+}
+
+/* client */
+
+struct _connection_refused {
+ unsigned int count;
+};
+
+static void
+test_client_connection_refused_reply(const struct smtp_reply *reply,
+ struct _connection_refused *ctx)
+{
+ if (debug)
+ i_debug("REPLY: %s", smtp_reply_log(reply));
+
+ test_assert(reply->status == SMTP_CLIENT_COMMAND_ERROR_CONNECT_FAILED);
+
+ if (--ctx->count == 0) {
+ i_free(ctx);
+ io_loop_stop(ioloop);
+ }
+}
+
+static bool
+test_client_connection_refused(const struct smtp_client_settings *client_set)
+{
+ struct smtp_client_connection *sconn;
+ struct smtp_client_command *scmd;
+ struct _connection_refused *ctx;
+
+ test_expect_errors(2);
+
+ ctx = i_new(struct _connection_refused, 1);
+ ctx->count = 2;
+
+ smtp_client = smtp_client_init(client_set);
+
+ sconn = smtp_client_connection_create(
+ smtp_client, SMTP_PROTOCOL_SMTP,
+ net_ip2addr(&bind_ip), bind_ports[0],
+ SMTP_CLIENT_SSL_MODE_NONE, NULL);
+ smtp_client_connection_connect(sconn, NULL, NULL);
+ scmd = smtp_client_command_new(
+ sconn, 0, test_client_connection_refused_reply, ctx);
+ smtp_client_command_write(scmd, "FROP");
+ smtp_client_command_submit(scmd);
+
+ sconn = smtp_client_connection_create(
+ smtp_client, SMTP_PROTOCOL_SMTP,
+ net_ip2addr(&bind_ip), bind_ports[0],
+ SMTP_CLIENT_SSL_MODE_NONE, NULL);
+ smtp_client_connection_connect(sconn, NULL, NULL);
+ scmd = smtp_client_command_new(
+ sconn, 0, test_client_connection_refused_reply, ctx);
+ smtp_client_command_write(scmd, "FROP");
+ smtp_client_command_submit(scmd);
+
+ return TRUE;
+}
+
+/* test */
+
+static void test_connection_refused(void)
+{
+ struct smtp_client_settings smtp_client_set;
+
+ test_client_defaults(&smtp_client_set);
+
+ test_begin("connection refused");
+ test_run_client_server(&smtp_client_set,
+ test_client_connection_refused,
+ test_server_connection_refused, 1, NULL);
+ test_end();
+}
+
+/*
+ * Connection lost prematurely
+ */
+
+/* server */
+
+static void
+test_connection_lost_prematurely_input(struct server_connection *conn)
+{
+ const char *line;
+
+ line = i_stream_read_next_line(conn->conn.input);
+ if (line == NULL) {
+ if (conn->conn.input->eof ||
+ conn->conn.input->stream_errno != 0) {
+ server_connection_deinit(&conn);
+ }
+ return;
+ }
+ server_connection_deinit(&conn);
+}
+
+static int test_connection_lost_prematurely_init(struct server_connection *conn)
+{
+ o_stream_nsend_str(conn->conn.output,
+ "220 testserver ESMTP Testfix (Frop/GNU)\r\n");
+ return 1;
+}
+
+static void test_server_connection_lost_prematurely(unsigned int index)
+{
+ test_server_init = test_connection_lost_prematurely_init;
+ test_server_input = test_connection_lost_prematurely_input;
+ test_server_run(index);
+}
+
+/* client */
+
+struct _connection_lost_prematurely {
+ unsigned int count;
+};
+
+static void
+test_client_connection_lost_prematurely_reply(
+ const struct smtp_reply *reply,
+ struct _connection_lost_prematurely *ctx)
+{
+ if (debug)
+ i_debug("REPLY: %s", smtp_reply_log(reply));
+
+ test_assert(reply->status == SMTP_CLIENT_COMMAND_ERROR_CONNECTION_LOST);
+
+ if (--ctx->count == 0) {
+ i_free(ctx);
+ io_loop_stop(ioloop);
+ }
+}
+
+static bool
+test_client_connection_lost_prematurely(
+ const struct smtp_client_settings *client_set)
+{
+ struct smtp_client_connection *sconn;
+ struct smtp_client_command *scmd;
+ struct _connection_lost_prematurely *ctx;
+
+ ctx = i_new(struct _connection_lost_prematurely, 1);
+ ctx->count = 2;
+
+ smtp_client = smtp_client_init(client_set);
+
+ sconn = smtp_client_connection_create(
+ smtp_client, SMTP_PROTOCOL_SMTP,
+ net_ip2addr(&bind_ip), bind_ports[0],
+ SMTP_CLIENT_SSL_MODE_NONE, NULL);
+ smtp_client_connection_connect(sconn, NULL, NULL);
+ scmd = smtp_client_command_new(
+ sconn, 0, test_client_connection_lost_prematurely_reply, ctx);
+ smtp_client_command_write(scmd, "FROP");
+ smtp_client_command_submit(scmd);
+
+ sconn = smtp_client_connection_create(
+ smtp_client, SMTP_PROTOCOL_SMTP,
+ net_ip2addr(&bind_ip), bind_ports[0],
+ SMTP_CLIENT_SSL_MODE_NONE, NULL);
+ smtp_client_connection_connect(sconn, NULL, NULL);
+ scmd = smtp_client_command_new(
+ sconn, 0, test_client_connection_lost_prematurely_reply, ctx);
+ smtp_client_command_write(scmd, "FROP");
+ smtp_client_command_submit(scmd);
+
+ return TRUE;
+}
+
+/* test */
+
+static void test_connection_lost_prematurely(void)
+{
+ struct smtp_client_settings smtp_client_set;
+
+ test_client_defaults(&smtp_client_set);
+
+ test_begin("connection lost prematurely");
+ test_run_client_server(&smtp_client_set,
+ test_client_connection_lost_prematurely,
+ test_server_connection_lost_prematurely,
+ 1, NULL);
+ test_end();
+}
+
+/*
+ * Connection timed out
+ */
+
+/* server */
+
+static void test_server_connection_timed_out(unsigned int index ATTR_UNUSED)
+{
+ i_sleep_intr_secs(10);
+}
+
+/* client */
+
+struct _connection_timed_out {
+ unsigned int count;
+};
+
+static void
+test_client_connection_timed_out_reply(const struct smtp_reply *reply,
+ struct _connection_timed_out *ctx)
+{
+ if (debug)
+ i_debug("REPLY: %s", smtp_reply_log(reply));
+
+ test_assert(reply->status == SMTP_CLIENT_COMMAND_ERROR_CONNECT_FAILED);
+
+ if (--ctx->count == 0) {
+ i_free(ctx);
+ io_loop_stop(ioloop);
+ }
+}
+
+static bool
+test_client_connection_timed_out(const struct smtp_client_settings *client_set)
+{
+ struct smtp_client_connection *sconn;
+ struct smtp_client_command *scmd;
+ struct _connection_timed_out *ctx;
+
+ test_expect_errors(2);
+
+ ctx = i_new(struct _connection_timed_out, 1);
+ ctx->count = 2;
+
+ smtp_client = smtp_client_init(client_set);
+
+ sconn = smtp_client_connection_create(
+ smtp_client, SMTP_PROTOCOL_SMTP,
+ net_ip2addr(&bind_ip), bind_ports[0],
+ SMTP_CLIENT_SSL_MODE_NONE, NULL);
+ smtp_client_connection_connect(sconn, NULL, NULL);
+ scmd = smtp_client_command_new(
+ sconn, 0, test_client_connection_timed_out_reply, ctx);
+ smtp_client_command_write(scmd, "FROP");
+ smtp_client_command_submit(scmd);
+
+ sconn = smtp_client_connection_create(
+ smtp_client, SMTP_PROTOCOL_SMTP,
+ net_ip2addr(&bind_ip), bind_ports[0],
+ SMTP_CLIENT_SSL_MODE_NONE, NULL);
+ smtp_client_connection_connect(sconn, NULL, NULL);
+ scmd = smtp_client_command_new(
+ sconn, 0, test_client_connection_timed_out_reply, ctx);
+ smtp_client_command_write(scmd, "FROP");
+ smtp_client_command_submit(scmd);
+
+ return TRUE;
+}
+
+/* test */
+
+static void test_connection_timed_out(void)
+{
+ struct smtp_client_settings smtp_client_set;
+
+ test_client_defaults(&smtp_client_set);
+ smtp_client_set.connect_timeout_msecs = 1000;
+
+ test_begin("connection timed out");
+ test_run_client_server(&smtp_client_set,
+ test_client_connection_timed_out,
+ test_server_connection_timed_out, 1, NULL);
+ test_end();
+}
+
+/*
+ * Broken payload
+ */
+
+/* server */
+
+static int
+test_broken_payload_input_line(struct server_connection *conn ATTR_UNUSED,
+ const char *line ATTR_UNUSED)
+{
+ return 0;
+}
+
+static void test_server_broken_payload(unsigned int index)
+{
+ test_server_input_line = test_broken_payload_input_line;
+ test_server_run(index);
+}
+
+static int
+test_broken_payload_chunking_input_line(struct server_connection *conn,
+ const char *line ATTR_UNUSED)
+{
+ if (conn->state == SERVER_CONNECTION_STATE_EHLO) {
+ o_stream_nsend_str(conn->conn.output,
+ "250-testserver\r\n"
+ "250-PIPELINING\r\n"
+ "250-CHUNKING\r\n"
+ "250-ENHANCEDSTATUSCODES\r\n"
+ "250 DSN\r\n");
+ return 1;
+ }
+ return 0;
+}
+
+static void test_server_broken_payload_chunking(unsigned int index)
+{
+ test_server_input_line = test_broken_payload_chunking_input_line;
+ test_server_run(index);
+}
+
+/* client */
+
+static void
+test_client_broken_payload_rcpt_to_cb(const struct smtp_reply *reply,
+ void *context ATTR_UNUSED)
+{
+ test_assert(smtp_reply_is_success(reply));
+}
+
+static void
+test_client_broken_payload_rcpt_data_cb(const struct smtp_reply *reply,
+ void *context ATTR_UNUSED)
+{
+ if (debug)
+ i_debug("REPLY: %s", smtp_reply_log(reply));
+
+ test_assert(reply->status == SMTP_CLIENT_COMMAND_ERROR_BROKEN_PAYLOAD);
+}
+
+static void
+test_client_broken_payload_data_cb(const struct smtp_reply *reply,
+ void *context ATTR_UNUSED)
+{
+ if (debug)
+ i_debug("REPLY: %s", smtp_reply_log(reply));
+
+ test_assert(reply->status == SMTP_CLIENT_COMMAND_ERROR_BROKEN_PAYLOAD);
+}
+
+static void test_client_broken_payload_finished(void *context ATTR_UNUSED)
+{
+ io_loop_stop(ioloop);
+}
+
+static bool
+test_client_broken_payload(const struct smtp_client_settings *client_set)
+{
+ struct smtp_client_connection *sconn;
+ struct smtp_client_transaction *strans;
+ struct istream *input;
+
+ test_expect_errors(2);
+
+ input = i_stream_create_error_str(EIO, "Moehahahaha!!");
+ i_stream_set_name(input, "PURE EVIL");
+
+ smtp_client = smtp_client_init(client_set);
+
+ sconn = smtp_client_connection_create(
+ smtp_client, SMTP_PROTOCOL_SMTP,
+ net_ip2addr(&bind_ip), bind_ports[0],
+ SMTP_CLIENT_SSL_MODE_NONE, NULL);
+ strans = smtp_client_transaction_create(
+ sconn, &((struct smtp_address){
+ .localpart = "sender",
+ .domain = "example.com"}), NULL, 0,
+ test_client_broken_payload_finished, NULL);
+ smtp_client_connection_unref(&sconn);
+
+ smtp_client_transaction_add_rcpt(
+ strans, &((struct smtp_address){
+ .localpart = "rcpt",
+ .domain = "example.com"}), NULL,
+ test_client_broken_payload_rcpt_to_cb,
+ test_client_broken_payload_rcpt_data_cb, NULL);
+ smtp_client_transaction_send(
+ strans, input, test_client_broken_payload_data_cb, NULL);
+ i_stream_unref(&input);
+
+ return TRUE;
+}
+
+static bool
+test_client_broken_payload_later(const struct smtp_client_settings *client_set)
+{
+ static const char *message =
+ "From: lucifer@example.com\r\n"
+ "To: lostsoul@example.com\r\n"
+ "Subject: Moehahaha!\r\n"
+ "\r\n"
+ "Moehahahahahahahahahahahahahahahahahahahahahaha!!\r\n"
+ "Moehahahahahahahahahahahahahahahahahahahahahaha!!\r\n"
+ "Moehahahahahahahahahahahahahahahahahahahahahaha!!\r\n"
+ "Moehahahahahahahahahahahahahahahahahahahahahaha!!\r\n"
+ "Moehahahahahahahahahahahahahahahahahahahahahaha!!\r\n"
+ "Moehahahahahahahahahahahahahahahahahahahahahaha!!\r\n"
+ "Moehahahahahahahahahahahahahahahahahahahahahaha!!\r\n"
+ "Moehahahahahahahahahahahahahahahahahahahahahaha!!\r\n"
+ "Moehahahahahahahahahahahahahahahahahahahahahaha!!\r\n"
+ "Moehahahahahahahahahahahahahahahahahahahahahaha!!\r\n"
+ "Moehahahahahahahahahahahahahahahahahahahahahaha!!\r\n"
+ "Moehahahahahahahahahahahahahahahahahahahahahaha!!\r\n"
+ "Moehahahahahahahahahahahahahahahahahahahahahaha!!\r\n"
+ "Moehahahahahahahahahahahahahahahahahahahahahaha!!\r\n"
+ "Moehahahahahahahahahahahahahahahahahahahahahaha!!\r\n"
+ "Moehahahahahahahahahahahahahahahahahahahahahaha!!\r\n"
+ "Moehahahahahahahahahahahahahahahahahahahahahaha!!\r\n";
+ struct smtp_client_connection *sconn;
+ struct smtp_client_transaction *strans;
+ struct istream *input, *msg_input;
+
+ test_expect_errors(1);
+
+ msg_input = i_stream_create_from_data(message, strlen(message));
+ input = i_stream_create_failure_at(msg_input, 666,
+ EIO, "Moehahahaha!!");
+ i_stream_unref(&msg_input);
+ i_stream_set_name(input, "PURE EVIL");
+
+ smtp_client = smtp_client_init(client_set);
+
+ sconn = smtp_client_connection_create(
+ smtp_client, SMTP_PROTOCOL_SMTP,
+ net_ip2addr(&bind_ip), bind_ports[0],
+ SMTP_CLIENT_SSL_MODE_NONE, NULL);
+ strans = smtp_client_transaction_create(
+ sconn, &((struct smtp_address){
+ .localpart = "sender",
+ .domain = "example.com"}), NULL, 0,
+ test_client_broken_payload_finished, NULL);
+ smtp_client_connection_unref(&sconn);
+
+ smtp_client_transaction_add_rcpt(
+ strans, &((struct smtp_address){
+ .localpart = "rcpt",
+ .domain = "example.com"}), NULL,
+ test_client_broken_payload_rcpt_to_cb,
+ test_client_broken_payload_rcpt_data_cb, NULL);
+ smtp_client_transaction_send(
+ strans, input, test_client_broken_payload_data_cb, NULL);
+ i_stream_unref(&input);
+
+ return TRUE;
+}
+
+/* test */
+
+static void test_broken_payload(void)
+{
+ struct smtp_client_settings smtp_client_set;
+
+ test_client_defaults(&smtp_client_set);
+ smtp_client_set.connect_timeout_msecs = 1000;
+
+ test_begin("broken payload");
+ test_run_client_server(&smtp_client_set,
+ test_client_broken_payload,
+ test_server_broken_payload, 1, NULL);
+ test_end();
+
+ test_begin("broken payload (later)");
+ test_run_client_server(&smtp_client_set,
+ test_client_broken_payload_later,
+ test_server_broken_payload, 1, NULL);
+ test_end();
+
+ test_begin("broken payload (later, chunking)");
+ test_run_client_server(&smtp_client_set,
+ test_client_broken_payload_later,
+ test_server_broken_payload_chunking, 1, NULL);
+ test_end();
+}
+
+/*
+ * Connection lost
+ */
+
+/* server */
+
+static int
+test_connection_lost_input_line(struct server_connection *conn,
+ const char *line ATTR_UNUSED)
+{
+ switch (conn->state) {
+ case SERVER_CONNECTION_STATE_EHLO:
+ if (server_index == 0) {
+ conn->state = SERVER_CONNECTION_STATE_MAIL_FROM;
+ i_sleep_intr_secs(1);
+ server_connection_deinit(&conn);
+ return -1;
+ }
+ break;
+ case SERVER_CONNECTION_STATE_MAIL_FROM:
+ if (server_index == 1) {
+ conn->state = SERVER_CONNECTION_STATE_RCPT_TO;
+ i_sleep_intr_secs(1);
+ server_connection_deinit(&conn);
+ return -1;
+ }
+ break;
+ case SERVER_CONNECTION_STATE_RCPT_TO:
+ if (server_index == 2) {
+ conn->state = SERVER_CONNECTION_STATE_DATA;
+ i_sleep_intr_secs(1);
+ server_connection_deinit(&conn);
+ return -1;
+ }
+ break;
+ case SERVER_CONNECTION_STATE_DATA:
+ if (server_index == 3) {
+ conn->state = SERVER_CONNECTION_STATE_FINISH;
+ i_sleep_intr_secs(1);
+ server_connection_deinit(&conn);
+ return -1;
+ }
+ break;
+ case SERVER_CONNECTION_STATE_FINISH:
+ break;
+ }
+ return 0;
+}
+
+static int
+test_connection_lost_input_data(struct server_connection *conn,
+ const unsigned char *data ATTR_UNUSED,
+ size_t size ATTR_UNUSED)
+{
+ i_sleep_intr_secs(1);
+ server_connection_deinit(&conn);
+ return -1;
+}
+
+static void test_server_connection_lost(unsigned int index)
+{
+ test_server_input_line = test_connection_lost_input_line;
+ test_server_input_data = test_connection_lost_input_data;
+ test_server_run(index);
+}
+
+/* client */
+
+struct _connection_lost {
+ unsigned int count;
+};
+
+struct _connection_lost_peer {
+ struct _connection_lost *context;
+ unsigned int index;
+};
+
+static void
+test_client_connection_lost_rcpt_to_cb(const struct smtp_reply *reply,
+ struct _connection_lost_peer *pctx)
+{
+ if (debug) {
+ i_debug("RCPT TO REPLY[%u]: %s",
+ pctx->index, smtp_reply_log(reply));
+ }
+
+ switch (pctx->index) {
+ case 0:
+ test_assert(reply->status ==
+ SMTP_CLIENT_COMMAND_ERROR_CONNECTION_LOST);
+ break;
+ case 1:
+ test_assert(reply->status ==
+ SMTP_CLIENT_COMMAND_ERROR_CONNECTION_LOST);
+ break;
+ case 2:
+ test_assert(reply->status ==
+ SMTP_CLIENT_COMMAND_ERROR_CONNECTION_LOST);
+ break;
+ case 3:
+ test_assert(smtp_reply_is_success(reply));
+ break;
+ }
+}
+
+static void
+test_client_connection_lost_rcpt_data_cb(const struct smtp_reply *reply,
+ struct _connection_lost_peer *pctx)
+{
+ if (debug) {
+ i_debug("RCPT DATA REPLY[%u]: %s",
+ pctx->index, smtp_reply_log(reply));
+ }
+
+ switch (pctx->index) {
+ case 0:
+ test_assert(FALSE);
+ break;
+ case 1:
+ test_assert(FALSE);
+ break;
+ case 2:
+ test_assert(FALSE);
+ break;
+ case 3:
+ test_assert(reply->status ==
+ SMTP_CLIENT_COMMAND_ERROR_CONNECTION_LOST);
+ break;
+ }
+}
+
+static void
+test_client_connection_lost_data_cb(const struct smtp_reply *reply,
+ struct _connection_lost_peer *pctx)
+{
+ if (debug) {
+ i_debug("DATA REPLY[%u]: %s",
+ pctx->index, smtp_reply_log(reply));
+ }
+
+ test_assert(reply->status == SMTP_CLIENT_COMMAND_ERROR_CONNECTION_LOST);
+}
+
+static void
+test_client_connection_lost_finished(struct _connection_lost_peer *pctx)
+{
+ struct _connection_lost *ctx = pctx->context;
+
+ if (debug)
+ i_debug("FINISHED[%u]", pctx->index);
+ if (--ctx->count == 0) {
+ i_free(ctx);
+ io_loop_stop(ioloop);
+ }
+ i_free(pctx);
+}
+
+static void
+test_client_connection_lost_submit(struct _connection_lost *ctx,
+ unsigned int index)
+{
+ static const char *message =
+ "From: stephan@example.com\r\n"
+ "To: timo@example.com\r\n"
+ "Subject: Frop!\r\n"
+ "\r\n"
+ "Frop!\r\n";
+ struct _connection_lost_peer *pctx;
+ struct smtp_client_connection *sconn;
+ struct smtp_client_transaction *strans;
+ struct istream *input;
+
+ pctx = i_new(struct _connection_lost_peer, 1);
+ pctx->context = ctx;
+ pctx->index = index;
+
+ input = i_stream_create_from_data(message, strlen(message));
+ i_stream_set_name(input, "message");
+
+ sconn = smtp_client_connection_create(
+ smtp_client, SMTP_PROTOCOL_SMTP,
+ net_ip2addr(&bind_ip), bind_ports[index],
+ SMTP_CLIENT_SSL_MODE_NONE, NULL);
+ strans = smtp_client_transaction_create(
+ sconn, &((struct smtp_address){
+ .localpart = "sender",
+ .domain = "example.com"}), NULL, 0,
+ test_client_connection_lost_finished, pctx);
+ smtp_client_connection_unref(&sconn);
+
+ smtp_client_transaction_add_rcpt(
+ strans, &((struct smtp_address){
+ .localpart = "rcpt",
+ .domain = "example.com"}), NULL,
+ test_client_connection_lost_rcpt_to_cb,
+ test_client_connection_lost_rcpt_data_cb, pctx);
+ smtp_client_transaction_send(
+ strans, input, test_client_connection_lost_data_cb, pctx);
+ i_stream_unref(&input);
+}
+
+static bool
+test_client_connection_lost(const struct smtp_client_settings *client_set)
+{
+ struct _connection_lost *ctx;
+ unsigned int i;
+
+ ctx = i_new(struct _connection_lost, 1);
+ ctx->count = 5;
+
+ smtp_client = smtp_client_init(client_set);
+
+ for (i = 0; i < ctx->count; i++)
+ test_client_connection_lost_submit(ctx, i);
+
+ return TRUE;
+}
+
+/* test */
+
+static void test_connection_lost(void)
+{
+ struct smtp_client_settings smtp_client_set;
+
+ test_client_defaults(&smtp_client_set);
+
+ test_begin("connection lost");
+ test_run_client_server(&smtp_client_set,
+ test_client_connection_lost,
+ test_server_connection_lost, 5, NULL);
+ test_end();
+}
+
+/*
+ * Unexpected reply
+ */
+
+/* server */
+
+static int
+test_unexpected_reply_init(struct server_connection *conn)
+{
+ if (server_index == 5) {
+ o_stream_nsend_str(conn->conn.output, "220 testserver "
+ "ESMTP Testfix (Debian/GNU)\r\n");
+ o_stream_nsend_str(conn->conn.output, "421 testserver "
+ "Server shutting down for maintenance\r\n");
+ i_sleep_intr_secs(4);
+ server_connection_deinit(&conn);
+ return 1;
+ }
+ return 0;
+}
+
+static int
+test_unexpected_reply_input_line(struct server_connection *conn,
+ const char *line ATTR_UNUSED)
+{
+ switch (conn->state) {
+ case SERVER_CONNECTION_STATE_EHLO:
+ if (server_index == 4) {
+ o_stream_nsend_str(conn->conn.output,
+ "250-testserver\r\n"
+ "250-PIPELINING\r\n"
+ "250-ENHANCEDSTATUSCODES\r\n"
+ "250 DSN\r\n");
+ o_stream_nsend_str(
+ conn->conn.output, "421 testserver "
+ "Server shutting down for maintenance\r\n");
+ i_sleep_intr_secs(4);
+ server_connection_deinit(&conn);
+ return -1;
+ }
+ break;
+ case SERVER_CONNECTION_STATE_MAIL_FROM:
+ if (server_index == 3) {
+ o_stream_nsend_str(conn->conn.output,
+ "250 2.1.0 Ok\r\n");
+ o_stream_nsend_str(
+ conn->conn.output, "421 testserver "
+ "Server shutting down for maintenance\r\n");
+ i_sleep_intr_secs(4);
+ server_connection_deinit(&conn);
+ return -1;
+ }
+ break;
+ case SERVER_CONNECTION_STATE_RCPT_TO:
+ if (server_index == 2) {
+ o_stream_nsend_str(conn->conn.output,
+ "250 2.1.5 Ok\r\n");
+ o_stream_nsend_str(
+ conn->conn.output, "421 testserver "
+ "Server shutting down for maintenance\r\n");
+ i_sleep_intr_secs(4);
+ server_connection_deinit(&conn);
+ return -1;
+ }
+ break;
+ case SERVER_CONNECTION_STATE_DATA:
+ if (server_index == 1) {
+ o_stream_nsend_str(
+ conn->conn.output,
+ "354 End data with <CR><LF>.<CR><LF>\r\n");
+ o_stream_nsend_str(
+ conn->conn.output, "421 testserver "
+ "Server shutting down for maintenance\r\n");
+ i_sleep_intr_secs(4);
+ server_connection_deinit(&conn);
+ return -1;
+ }
+ break;
+ case SERVER_CONNECTION_STATE_FINISH:
+ break;
+ }
+ return 0;
+}
+
+static void test_server_unexpected_reply(unsigned int index)
+{
+ test_server_init = test_unexpected_reply_init;
+ test_server_input_line = test_unexpected_reply_input_line;
+ test_server_run(index);
+}
+
+/* client */
+
+struct _unexpected_reply {
+ unsigned int count;
+};
+
+struct _unexpected_reply_peer {
+ struct _unexpected_reply *context;
+ unsigned int index;
+
+ struct smtp_client_connection *conn;
+ struct smtp_client_transaction *trans;
+ struct timeout *to;
+
+ bool login_callback:1;
+ bool mail_from_callback:1;
+ bool rcpt_to_callback:1;
+ bool rcpt_data_callback:1;
+ bool data_callback:1;
+};
+
+static void
+test_client_unexpected_reply_login_cb(const struct smtp_reply *reply,
+ void *context)
+{
+ struct _unexpected_reply_peer *pctx =
+ (struct _unexpected_reply_peer *)context;
+
+ pctx->login_callback = TRUE;
+
+ if (debug) {
+ i_debug("LOGIN REPLY[%u]: %s",
+ pctx->index, smtp_reply_log(reply));
+ }
+
+ switch (pctx->index) {
+ case 0: case 1: case 2: case 3: case 4:
+ test_assert(smtp_reply_is_success(reply));
+ break;
+ case 5:
+ test_assert(reply->status == 421);
+ break;
+ }
+}
+
+static void
+test_client_unexpected_reply_mail_from_cb(const struct smtp_reply *reply,
+ struct _unexpected_reply_peer *pctx)
+{
+ if (debug) {
+ i_debug("MAIL FROM REPLY[%u]: %s",
+ pctx->index, smtp_reply_log(reply));
+ }
+
+ pctx->mail_from_callback = TRUE;
+
+ switch (pctx->index) {
+ case 0: case 1: case 2: case 3:
+ test_assert(smtp_reply_is_success(reply));
+ break;
+ case 4: case 5:
+ test_assert(reply->status == 421);
+ break;
+ }
+}
+
+static void
+test_client_unexpected_reply_rcpt_to_cb(const struct smtp_reply *reply,
+ struct _unexpected_reply_peer *pctx)
+{
+ if (debug) {
+ i_debug("RCPT TO REPLY[%u]: %s",
+ pctx->index, smtp_reply_log(reply));
+ }
+
+ pctx->rcpt_to_callback = TRUE;
+
+ switch (pctx->index) {
+ case 0: case 1: case 2:
+ test_assert(smtp_reply_is_success(reply));
+ break;
+ case 3: case 4: case 5:
+ test_assert(reply->status == 421);
+ break;
+ }
+}
+
+static void
+test_client_unexpected_reply_rcpt_data_cb(const struct smtp_reply *reply,
+ struct _unexpected_reply_peer *pctx)
+{
+ if (debug) {
+ i_debug("RCPT DATA REPLY[%u]: %s",
+ pctx->index, smtp_reply_log(reply));
+ }
+
+ pctx->rcpt_data_callback = TRUE;
+
+ switch (pctx->index) {
+ case 0:
+ test_assert(smtp_reply_is_success(reply));
+ break;
+ case 1: case 2:
+ test_assert(reply->status == 421);
+ break;
+ case 3: case 4: case 5:
+ i_unreached();
+ }
+}
+
+static void
+test_client_unexpected_reply_data_cb(const struct smtp_reply *reply,
+ struct _unexpected_reply_peer *pctx)
+{
+ if (debug) {
+ i_debug("DATA REPLY[%u]: %s",
+ pctx->index, smtp_reply_log(reply));
+ }
+
+ pctx->data_callback = TRUE;
+
+ switch (pctx->index) {
+ case 0:
+ test_assert(smtp_reply_is_success(reply));
+ break;
+ case 1: case 2: case 3: case 4: case 5:
+ test_assert(reply->status == 421);
+ break;
+ }
+}
+
+static void
+test_client_unexpected_reply_finished(struct _unexpected_reply_peer *pctx)
+{
+ struct _unexpected_reply *ctx = pctx->context;
+
+ if (debug)
+ i_debug("FINISHED[%u]", pctx->index);
+ if (--ctx->count == 0) {
+ i_free(ctx);
+ io_loop_stop(ioloop);
+ }
+
+ switch (pctx->index) {
+ case 0: case 1: case 2:
+ test_assert(pctx->mail_from_callback);
+ test_assert(pctx->rcpt_to_callback);
+ test_assert(pctx->rcpt_data_callback);
+ test_assert(pctx->data_callback);
+ break;
+ case 3: case 4: case 5:
+ test_assert(pctx->mail_from_callback);
+ test_assert(pctx->rcpt_to_callback);
+ test_assert(!pctx->rcpt_data_callback);
+ test_assert(pctx->data_callback);
+ break;
+ }
+
+ pctx->trans = NULL;
+ timeout_remove(&pctx->to);
+ i_free(pctx);
+}
+
+static void
+test_client_unexpected_reply_submit2(struct _unexpected_reply_peer *pctx)
+{
+ struct smtp_client_transaction *strans = pctx->trans;
+ static const char *message =
+ "From: stephan@example.com\r\n"
+ "To: timo@example.com\r\n"
+ "Subject: Frop!\r\n"
+ "\r\n"
+ "Frop!\r\n";
+ struct istream *input;
+
+ timeout_remove(&pctx->to);
+
+ input = i_stream_create_from_data(message, strlen(message));
+ i_stream_set_name(input, "message");
+
+ smtp_client_transaction_send(
+ strans, input, test_client_unexpected_reply_data_cb, pctx);
+ i_stream_unref(&input);
+}
+
+static void
+test_client_unexpected_reply_submit1(struct _unexpected_reply_peer *pctx)
+{
+ timeout_remove(&pctx->to);
+
+ smtp_client_transaction_add_rcpt(
+ pctx->trans, &((struct smtp_address){
+ .localpart = "rcpt",
+ .domain = "example.com"}), NULL,
+ test_client_unexpected_reply_rcpt_to_cb,
+ test_client_unexpected_reply_rcpt_data_cb, pctx);
+
+ pctx->to = timeout_add_short(
+ 500, test_client_unexpected_reply_submit2, pctx);
+}
+
+static void
+test_client_unexpected_reply_submit(struct _unexpected_reply *ctx,
+ unsigned int index)
+{
+ struct _unexpected_reply_peer *pctx;
+
+ pctx = i_new(struct _unexpected_reply_peer, 1);
+ pctx->context = ctx;
+ pctx->index = index;
+
+ pctx->conn = smtp_client_connection_create(
+ smtp_client, SMTP_PROTOCOL_SMTP,
+ net_ip2addr(&bind_ip), bind_ports[index],
+ SMTP_CLIENT_SSL_MODE_NONE, NULL);
+ pctx->trans = smtp_client_transaction_create(
+ pctx->conn, &((struct smtp_address){
+ .localpart = "sender",
+ .domain = "example.com"}), NULL, 0,
+ test_client_unexpected_reply_finished, pctx);
+ smtp_client_connection_connect(
+ pctx->conn, test_client_unexpected_reply_login_cb,
+ (void *)pctx);
+ smtp_client_transaction_start(
+ pctx->trans, test_client_unexpected_reply_mail_from_cb, pctx);
+ smtp_client_connection_unref(&pctx->conn);
+
+ pctx->to = timeout_add_short(
+ 500, test_client_unexpected_reply_submit1, pctx);
+}
+
+static bool
+test_client_unexpected_reply(const struct smtp_client_settings *client_set)
+{
+ struct _unexpected_reply *ctx;
+ unsigned int i;
+
+ ctx = i_new(struct _unexpected_reply, 1);
+ ctx->count = 6;
+
+ smtp_client = smtp_client_init(client_set);
+
+ for (i = 0; i < ctx->count; i++)
+ test_client_unexpected_reply_submit(ctx, i);
+
+ return TRUE;
+}
+
+/* test */
+
+static void test_unexpected_reply(void)
+{
+ struct smtp_client_settings smtp_client_set;
+
+ test_client_defaults(&smtp_client_set);
+
+ test_begin("unexpected reply");
+ test_run_client_server(&smtp_client_set,
+ test_client_unexpected_reply,
+ test_server_unexpected_reply, 6, NULL);
+ test_end();
+}
+
+/*
+ * Partial reply
+ */
+
+/* server */
+
+static int
+test_partial_reply_input_line(struct server_connection *conn,
+ const char *line ATTR_UNUSED)
+{
+ if (conn->state == SERVER_CONNECTION_STATE_EHLO)
+ return 0;
+ o_stream_nsend_str(conn->conn.output,
+ "500 Command not");
+ server_connection_deinit(&conn);
+ return -1;
+}
+
+static void test_server_partial_reply(unsigned int index)
+{
+ test_server_input_line = test_partial_reply_input_line;
+ test_server_run(index);
+}
+
+/* client */
+
+struct _partial_reply {
+ unsigned int count;
+};
+
+static void
+test_client_partial_reply_reply(const struct smtp_reply *reply,
+ struct _partial_reply *ctx)
+{
+ if (debug)
+ i_debug("REPLY: %s", smtp_reply_log(reply));
+
+ test_assert(reply->status == SMTP_CLIENT_COMMAND_ERROR_CONNECTION_LOST);
+
+ if (--ctx->count == 0) {
+ i_free(ctx);
+ io_loop_stop(ioloop);
+ }
+}
+
+static bool
+test_client_partial_reply(const struct smtp_client_settings *client_set)
+{
+ struct smtp_client_connection *sconn;
+ struct smtp_client_command *scmd;
+ struct _partial_reply *ctx;
+
+ ctx = i_new(struct _partial_reply, 1);
+ ctx->count = 2;
+
+ smtp_client = smtp_client_init(client_set);
+
+ sconn = smtp_client_connection_create(
+ smtp_client, SMTP_PROTOCOL_SMTP,
+ net_ip2addr(&bind_ip), bind_ports[0],
+ SMTP_CLIENT_SSL_MODE_NONE, NULL);
+ smtp_client_connection_connect(sconn, NULL, NULL);
+ scmd = smtp_client_command_new(
+ sconn, 0, test_client_partial_reply_reply, ctx);
+ smtp_client_command_write(scmd, "FROP");
+ smtp_client_command_submit(scmd);
+
+ sconn = smtp_client_connection_create(
+ smtp_client, SMTP_PROTOCOL_SMTP,
+ net_ip2addr(&bind_ip), bind_ports[0],
+ SMTP_CLIENT_SSL_MODE_NONE, NULL);
+ smtp_client_connection_connect(sconn, NULL, NULL);
+ scmd = smtp_client_command_new(
+ sconn, 0, test_client_partial_reply_reply, ctx);
+ smtp_client_command_write(scmd, "FROP");
+ smtp_client_command_submit(scmd);
+
+ return TRUE;
+}
+
+/* test */
+
+static void test_partial_reply(void)
+{
+ struct smtp_client_settings smtp_client_set;
+
+ test_client_defaults(&smtp_client_set);
+
+ test_begin("partial reply");
+ test_run_client_server(&smtp_client_set,
+ test_client_partial_reply,
+ test_server_partial_reply, 1, NULL);
+ test_end();
+}
+
+/*
+ * Premature reply
+ */
+
+/* server */
+
+static int
+test_premature_reply_init(struct server_connection *conn)
+{
+ if (server_index == 5) {
+ o_stream_nsend_str(
+ conn->conn.output,
+ "220 testserver ESMTP Testfix (Debian/GNU)\r\n"
+ "250-testserver\r\n"
+ "250-PIPELINING\r\n"
+ "250-ENHANCEDSTATUSCODES\r\n"
+ "250 DSN\r\n");
+ i_sleep_intr_secs(4);
+ server_connection_deinit(&conn);
+ return 1;
+ }
+ return 0;
+}
+
+static int
+test_premature_reply_input_line(struct server_connection *conn, const char *line)
+{
+ if (debug)
+ i_debug("[%u] GOT LINE: %s", server_index, line);
+ switch (conn->state) {
+ case SERVER_CONNECTION_STATE_EHLO:
+ if (debug)
+ i_debug("[%u] EHLO", server_index);
+ if (server_index == 4) {
+ o_stream_nsend_str(conn->conn.output,
+ "250-testserver\r\n"
+ "250-PIPELINING\r\n"
+ "250-ENHANCEDSTATUSCODES\r\n"
+ "250 DSN\r\n"
+ "250 2.1.0 Ok\r\n");
+ conn->state = SERVER_CONNECTION_STATE_MAIL_FROM;
+ return 1;
+ }
+ break;
+ case SERVER_CONNECTION_STATE_MAIL_FROM:
+ if (server_index == 4) {
+ conn->state = SERVER_CONNECTION_STATE_RCPT_TO;
+ return 1;
+ }
+ if (server_index == 3) {
+ o_stream_nsend_str(conn->conn.output,
+ "250 2.1.0 Ok\r\n"
+ "250 2.1.5 Ok\r\n");
+ i_sleep_intr_secs(4);
+ server_connection_deinit(&conn);
+ return -1;
+ }
+ break;
+ case SERVER_CONNECTION_STATE_RCPT_TO:
+ if (server_index == 2) {
+ o_stream_nsend_str(
+ conn->conn.output,
+ "250 2.1.5 Ok\r\n"
+ "354 End data with <CR><LF>.<CR><LF>\r\n");
+ i_sleep_intr_secs(4);
+ server_connection_deinit(&conn);
+ return -1;
+ }
+ break;
+ case SERVER_CONNECTION_STATE_DATA:
+ if (server_index == 1) {
+ o_stream_nsend_str(
+ conn->conn.output,
+ "354 End data with <CR><LF>.<CR><LF>\r\n"
+ "250 2.0.0 Ok: queued as 35424ed4af24\r\n");
+ i_sleep_intr_secs(4);
+ server_connection_deinit(&conn);
+ return -1;
+ }
+ break;
+ case SERVER_CONNECTION_STATE_FINISH:
+ break;
+ }
+ return 0;
+}
+
+static void test_server_premature_reply(unsigned int index)
+{
+ test_server_init = test_premature_reply_init;
+ test_server_input_line = test_premature_reply_input_line;
+ test_server_run(index);
+}
+
+/* client */
+
+struct _premature_reply {
+ unsigned int count;
+};
+
+struct _premature_reply_peer {
+ struct _premature_reply *context;
+ unsigned int index;
+
+ struct smtp_client_connection *conn;
+ struct smtp_client_transaction *trans;
+ struct timeout *to;
+
+ bool login_callback:1;
+ bool mail_from_callback:1;
+ bool rcpt_to_callback:1;
+ bool rcpt_data_callback:1;
+ bool data_callback:1;
+};
+
+static void
+test_client_premature_reply_login_cb(const struct smtp_reply *reply,
+ void *context)
+{
+ struct _premature_reply_peer *pctx =
+ (struct _premature_reply_peer *)context;
+
+ pctx->login_callback = TRUE;
+
+ if (debug) {
+ i_debug("LOGIN REPLY[%u]: %s", pctx->index,
+ smtp_reply_log(reply));
+ }
+
+ switch (pctx->index) {
+ case 0: case 1: case 2: case 3: case 4:
+ test_assert(smtp_reply_is_success(reply));
+ break;
+ case 5:
+ test_assert(reply->status ==
+ SMTP_CLIENT_COMMAND_ERROR_BAD_REPLY);
+ /* Don't bother continueing with this test. Second try after
+ smtp_client_transaction_start() will have the same result. */
+ smtp_client_transaction_abort(pctx->trans);
+ break;
+ }
+}
+
+static void
+test_client_premature_reply_mail_from_cb(const struct smtp_reply *reply,
+ struct _premature_reply_peer *pctx)
+{
+ if (debug) {
+ i_debug("MAIL FROM REPLY[%u]: %s",
+ pctx->index, smtp_reply_log(reply));
+ }
+
+ pctx->mail_from_callback = TRUE;
+
+ switch (pctx->index) {
+ case 0: case 1: case 2: case 3:
+ test_assert(smtp_reply_is_success(reply));
+ break;
+ case 4: case 5:
+ test_assert(reply->status ==
+ SMTP_CLIENT_COMMAND_ERROR_BAD_REPLY);
+ break;
+ }
+}
+
+static void
+test_client_premature_reply_rcpt_to_cb(const struct smtp_reply *reply,
+ struct _premature_reply_peer *pctx)
+{
+ if (debug) {
+ i_debug("RCPT TO REPLY[%u]: %s",
+ pctx->index, smtp_reply_log(reply));
+ }
+
+ pctx->rcpt_to_callback = TRUE;
+
+ switch (pctx->index) {
+ case 0: case 1: case 2:
+ test_assert(smtp_reply_is_success(reply));
+ break;
+ case 3: case 4: case 5:
+ test_assert(reply->status ==
+ SMTP_CLIENT_COMMAND_ERROR_BAD_REPLY);
+ break;
+ }
+}
+
+static void
+test_client_premature_reply_rcpt_data_cb(const struct smtp_reply *reply,
+ struct _premature_reply_peer *pctx)
+{
+ if (debug) {
+ i_debug("RCPT DATA REPLY[%u]: %s",
+ pctx->index, smtp_reply_log(reply));
+ }
+
+ pctx->rcpt_data_callback = TRUE;
+
+ switch (pctx->index) {
+ case 0:
+ test_assert(smtp_reply_is_success(reply));
+ break;
+ case 1: case 2:
+ test_assert(reply->status ==
+ SMTP_CLIENT_COMMAND_ERROR_BAD_REPLY);
+ break;
+ case 3: case 4: case 5:
+ i_unreached();
+ }
+}
+
+static void
+test_client_premature_reply_data_cb(const struct smtp_reply *reply,
+ struct _premature_reply_peer *pctx)
+{
+ if (debug) {
+ i_debug("DATA REPLY[%u]: %s",
+ pctx->index, smtp_reply_log(reply));
+ }
+
+ pctx->data_callback = TRUE;
+
+ switch (pctx->index) {
+ case 0:
+ test_assert(smtp_reply_is_success(reply));
+ break;
+ case 1: case 2: case 3: case 4: case 5:
+ test_assert(reply->status ==
+ SMTP_CLIENT_COMMAND_ERROR_BAD_REPLY);
+ break;
+ }
+}
+
+static void
+test_client_premature_reply_finished(struct _premature_reply_peer *pctx)
+{
+ struct _premature_reply *ctx = pctx->context;
+
+ if (debug)
+ i_debug("FINISHED[%u]", pctx->index);
+ if (--ctx->count == 0) {
+ i_free(ctx);
+ io_loop_stop(ioloop);
+ }
+
+ switch (pctx->index) {
+ case 0: case 1: case 2:
+ test_assert(pctx->mail_from_callback);
+ test_assert(pctx->rcpt_to_callback);
+ test_assert(pctx->rcpt_data_callback);
+ test_assert(pctx->data_callback);
+ break;
+ case 3: case 4:
+ test_assert(pctx->mail_from_callback);
+ test_assert(pctx->rcpt_to_callback);
+ test_assert(!pctx->rcpt_data_callback);
+ test_assert(pctx->data_callback);
+ break;
+ case 5:
+ test_assert(!pctx->mail_from_callback);
+ test_assert(!pctx->rcpt_to_callback);
+ test_assert(!pctx->rcpt_data_callback);
+ test_assert(!pctx->data_callback);
+ }
+
+ pctx->trans = NULL;
+ timeout_remove(&pctx->to);
+ i_free(pctx);
+}
+
+static void
+test_client_premature_reply_submit3(struct _premature_reply_peer *pctx)
+{
+ struct smtp_client_transaction *strans = pctx->trans;
+ static const char *message =
+ "From: stephan@example.com\r\n"
+ "To: timo@example.com\r\n"
+ "Subject: Frop!\r\n"
+ "\r\n"
+ "Frop!\r\n";
+ struct istream *input;
+
+ timeout_remove(&pctx->to);
+
+ if (debug)
+ i_debug("SUBMIT3[%u]", pctx->index);
+
+ input = i_stream_create_from_data(message, strlen(message));
+ i_stream_set_name(input, "message");
+
+ smtp_client_transaction_send(
+ strans, input, test_client_premature_reply_data_cb, pctx);
+ i_stream_unref(&input);
+}
+
+static void
+test_client_premature_reply_submit2(struct _premature_reply_peer *pctx)
+{
+ timeout_remove(&pctx->to);
+
+ if (debug)
+ i_debug("SUBMIT2[%u]", pctx->index);
+
+ smtp_client_transaction_add_rcpt(
+ pctx->trans, &((struct smtp_address){
+ .localpart = "rcpt",
+ .domain = "example.com"}), NULL,
+ test_client_premature_reply_rcpt_to_cb,
+ test_client_premature_reply_rcpt_data_cb, pctx);
+
+ pctx->to = timeout_add_short(
+ 500, test_client_premature_reply_submit3, pctx);
+}
+
+static void
+test_client_premature_reply_submit1(struct _premature_reply_peer *pctx)
+{
+ timeout_remove(&pctx->to);
+
+ if (debug)
+ i_debug("SUBMIT1[%u]", pctx->index);
+
+ smtp_client_transaction_start(
+ pctx->trans, test_client_premature_reply_mail_from_cb, pctx);
+
+ pctx->to = timeout_add_short(
+ 500, test_client_premature_reply_submit2, pctx);
+}
+
+static void
+test_client_premature_reply_submit(struct _premature_reply *ctx,
+ unsigned int index)
+{
+ struct _premature_reply_peer *pctx;
+ struct smtp_client_connection *conn;
+
+ pctx = i_new(struct _premature_reply_peer, 1);
+ pctx->context = ctx;
+ pctx->index = index;
+
+ pctx->conn = conn = smtp_client_connection_create(
+ smtp_client, SMTP_PROTOCOL_SMTP,
+ net_ip2addr(&bind_ip), bind_ports[index],
+ SMTP_CLIENT_SSL_MODE_NONE, NULL);
+ pctx->trans = smtp_client_transaction_create(
+ conn, &((struct smtp_address){
+ .localpart = "sender",
+ .domain = "example.com"}), NULL, 0,
+ test_client_premature_reply_finished, pctx);
+ smtp_client_connection_connect(
+ conn, test_client_premature_reply_login_cb, (void *)pctx);
+ smtp_client_connection_unref(&conn);
+
+ pctx->to = timeout_add_short(
+ 500, test_client_premature_reply_submit1, pctx);
+}
+
+static bool
+test_client_premature_reply(const struct smtp_client_settings *client_set)
+{
+ struct _premature_reply *ctx;
+ unsigned int i;
+
+ test_expect_errors(6);
+
+ ctx = i_new(struct _premature_reply, 1);
+ ctx->count = 6;
+
+ smtp_client = smtp_client_init(client_set);
+
+ for (i = 0; i < ctx->count; i++)
+ test_client_premature_reply_submit(ctx, i);
+
+ return TRUE;
+}
+
+/* test */
+
+static void test_premature_reply(void)
+{
+ struct smtp_client_settings smtp_client_set;
+
+ test_client_defaults(&smtp_client_set);
+
+ test_begin("premature reply");
+ test_run_client_server(&smtp_client_set,
+ test_client_premature_reply,
+ test_server_premature_reply, 6, NULL);
+ test_end();
+}
+
+/*
+ * Early data reply
+ */
+
+/* server */
+
+static int
+test_early_data_reply_input_line(struct server_connection *conn ATTR_UNUSED,
+ const char *line)
+{
+ if (debug)
+ i_debug("[%u] GOT LINE: %s", server_index, line);
+
+ switch (conn->state) {
+ case SERVER_CONNECTION_STATE_DATA:
+ break;
+ default:
+ return 0;
+ }
+
+ if ((uintptr_t)conn->context == 0) {
+ if (debug)
+ i_debug("[%u] REPLIED 354", server_index);
+ o_stream_nsend_str(conn->conn.output,
+ "354 End data with <CR><LF>.<CR><LF>\r\n");
+ conn->context = (void*)1;
+ return 1;
+ }
+
+ if (server_index == 2 && strcmp(line, ".") == 0) {
+ if (debug)
+ i_debug("[%u] FINISHED TRANSACTION",
+ server_index);
+ o_stream_nsend_str(conn->conn.output,
+ "250 2.0.0 Ok: queued as 73BDE342129\r\n");
+ return 1;
+ }
+
+ if ((uintptr_t)conn->context == 5 && server_index < 2) {
+ if (debug)
+ i_debug("[%u] FINISHED TRANSACTION EARLY",
+ server_index);
+
+ if (server_index == 0) {
+ o_stream_nsend_str(
+ conn->conn.output,
+ "250 2.0.0 Ok: queued as 73BDE342129\r\n");
+ } else {
+ o_stream_nsend_str(
+ conn->conn.output,
+ "452 4.3.1 Mail system full\r\n");
+ }
+ }
+ if ((uintptr_t)conn->context > 5) {
+ o_stream_nsend_str(conn->conn.output,
+ "250 2.0.0 OK\r\n");
+ return 1;
+ }
+ conn->context = (void*)(((uintptr_t)conn->context) + 1);
+ return 1;
+}
+
+static void test_server_early_data_reply(unsigned int index)
+{
+ test_server_input_line = test_early_data_reply_input_line;
+ test_server_run(index);
+}
+
+/* client */
+
+struct _early_data_reply {
+ unsigned int count;
+};
+
+struct _early_data_reply_peer {
+ struct _early_data_reply *context;
+ unsigned int index;
+
+ struct ostream *output;
+
+ struct smtp_client_connection *conn;
+ struct smtp_client_transaction *trans;
+ struct timeout *to;
+
+ bool data_callback:1;
+};
+
+static void
+test_client_early_data_reply_submit1(struct _early_data_reply_peer *pctx);
+
+static void
+test_client_early_data_reply_login_cb(const struct smtp_reply *reply,
+ void *context)
+{
+ struct _early_data_reply_peer *pctx = context;
+
+ if (debug) {
+ i_debug("LOGIN REPLY[%u]: %s",
+ pctx->index, smtp_reply_log(reply));
+ }
+
+ test_assert(smtp_reply_is_success(reply));
+}
+
+static void
+test_client_early_data_reply_mail_from_cb(const struct smtp_reply *reply,
+ struct _early_data_reply_peer *pctx)
+{
+ if (debug) {
+ i_debug("MAIL FROM REPLY[%u]: %s",
+ pctx->index, smtp_reply_log(reply));
+ }
+
+ test_assert(smtp_reply_is_success(reply));
+}
+
+static void
+test_client_early_data_reply_rcpt_to_cb(const struct smtp_reply *reply,
+ struct _early_data_reply_peer *pctx)
+{
+ if (debug) {
+ i_debug("RCPT TO REPLY[%u]: %s",
+ pctx->index, smtp_reply_log(reply));
+ }
+
+ test_assert(smtp_reply_is_success(reply));
+
+ pctx->to = timeout_add_short(
+ 1000, test_client_early_data_reply_submit1, pctx);
+}
+
+static void
+test_client_early_data_reply_rcpt_data_cb(const struct smtp_reply *reply,
+ struct _early_data_reply_peer *pctx)
+{
+ if (debug) {
+ i_debug("RCPT DATA REPLY[%u]: %s",
+ pctx->index, smtp_reply_log(reply));
+ }
+
+ switch (pctx->index) {
+ case 0:
+ test_assert(reply->status ==
+ SMTP_CLIENT_COMMAND_ERROR_BAD_REPLY);
+ break;
+ case 1:
+ test_assert(reply->status == 452);
+ break;
+ case 2:
+ test_assert(smtp_reply_is_success(reply));
+ break;
+ }
+}
+
+static void
+test_client_early_data_reply_data_cb(const struct smtp_reply *reply,
+ struct _early_data_reply_peer *pctx)
+{
+ if (debug) {
+ i_debug("DATA REPLY[%u]: %s",
+ pctx->index, smtp_reply_log(reply));
+ }
+
+ pctx->data_callback = TRUE;
+
+ switch (pctx->index) {
+ case 0:
+ test_assert(reply->status ==
+ SMTP_CLIENT_COMMAND_ERROR_BAD_REPLY);
+ break;
+ case 1:
+ test_assert(reply->status == 452);
+ break;
+ case 2:
+ test_assert(smtp_reply_is_success(reply));
+ break;
+ }
+}
+
+static void
+test_client_early_data_reply_noop_cb(const struct smtp_reply *reply,
+ struct _early_data_reply_peer *pctx)
+{
+ struct _early_data_reply *ctx = pctx->context;
+
+ if (debug) {
+ i_debug("NOOP REPLY[%u]: %s",
+ pctx->index, smtp_reply_log(reply));
+ }
+
+ switch (pctx->index) {
+ case 0:
+ test_assert(reply->status ==
+ SMTP_CLIENT_COMMAND_ERROR_BAD_REPLY);
+ break;
+ case 1:
+ case 2:
+ test_assert(smtp_reply_is_success(reply));
+ break;
+ }
+
+ if (--ctx->count == 0) {
+ i_free(ctx);
+ io_loop_stop(ioloop);
+ }
+
+ test_assert(pctx->data_callback);
+
+ pctx->trans = NULL;
+ timeout_remove(&pctx->to);
+ o_stream_destroy(&pctx->output);
+ smtp_client_connection_unref(&pctx->conn);
+ i_free(pctx);
+}
+
+static void
+test_client_early_data_reply_finished(struct _early_data_reply_peer *pctx)
+{
+ if (debug)
+ i_debug("FINISHED[%u]", pctx->index);
+
+ /* Send NOOP command to check that connection is still viable.
+ */
+ smtp_client_command_noop_submit(
+ pctx->conn, 0,
+ test_client_early_data_reply_noop_cb, pctx);
+}
+
+static void
+test_client_early_data_reply_submit1(struct _early_data_reply_peer *pctx)
+{
+ if (debug)
+ i_debug("FINISH DATA[%u]", pctx->index);
+
+ timeout_remove(&pctx->to);
+
+ if (o_stream_finish(pctx->output) < 0) {
+ i_error("Failed to finish output: %s",
+ o_stream_get_error(pctx->output));
+ }
+ o_stream_destroy(&pctx->output);
+}
+
+static void
+test_client_early_data_reply_submit(struct _early_data_reply *ctx,
+ unsigned int index)
+{
+ struct _early_data_reply_peer *pctx;
+ struct smtp_client_connection *conn;
+ static const char *message =
+ "From: stephan@example.com\r\n"
+ "To: timo@example.com\r\n"
+ "Subject: Frop!\r\n"
+ "\r\n"
+ "Frop!\r\n";
+ int pipefd[2];
+ struct istream *input;
+
+ pctx = i_new(struct _early_data_reply_peer, 1);
+ pctx->context = ctx;
+ pctx->index = index;
+
+ if (pipe(pipefd) < 0)
+ i_fatal("Failed to create pipe: %m");
+
+ fd_set_nonblock(pipefd[0], TRUE);
+ fd_set_nonblock(pipefd[1], TRUE);
+
+ input = i_stream_create_fd_autoclose(&pipefd[0], 1024);
+ pctx->output = o_stream_create_fd_autoclose(&pipefd[1], 1024);
+
+ pctx->conn = conn = smtp_client_connection_create(
+ smtp_client, SMTP_PROTOCOL_SMTP,
+ net_ip2addr(&bind_ip), bind_ports[index],
+ SMTP_CLIENT_SSL_MODE_NONE, NULL);
+ smtp_client_connection_connect(conn,
+ test_client_early_data_reply_login_cb, (void *)pctx);
+
+ pctx->trans = smtp_client_transaction_create(
+ conn, &((struct smtp_address){
+ .localpart = "sender",
+ .domain = "example.com"}), NULL, 0,
+ test_client_early_data_reply_finished, pctx);
+ smtp_client_transaction_add_rcpt(
+ pctx->trans, &((struct smtp_address){
+ .localpart = "rcpt",
+ .domain = "example.com"}), NULL,
+ test_client_early_data_reply_rcpt_to_cb,
+ test_client_early_data_reply_rcpt_data_cb, pctx);
+ smtp_client_transaction_start(pctx->trans,
+ test_client_early_data_reply_mail_from_cb, pctx);
+
+ smtp_client_transaction_send(
+ pctx->trans, input, test_client_early_data_reply_data_cb, pctx);
+ i_stream_unref(&input);
+
+ o_stream_nsend(pctx->output, message, strlen(message));
+}
+
+static bool
+test_client_early_data_reply(const struct smtp_client_settings *client_set)
+{
+ struct _early_data_reply *ctx;
+ unsigned int i;
+
+ test_expect_errors(2);
+
+ ctx = i_new(struct _early_data_reply, 1);
+ ctx->count = 3;
+
+ smtp_client = smtp_client_init(client_set);
+
+ for (i = 0; i < ctx->count; i++)
+ test_client_early_data_reply_submit(ctx, i);
+
+ return TRUE;
+}
+
+/* test */
+
+static void test_early_data_reply(void)
+{
+ struct smtp_client_settings smtp_client_set;
+
+ test_client_defaults(&smtp_client_set);
+
+ test_begin("early data reply");
+ test_run_client_server(&smtp_client_set,
+ test_client_early_data_reply,
+ test_server_early_data_reply, 3, NULL);
+ test_end();
+}
+
+/*
+ * Bad reply
+ */
+
+/* server */
+
+static int
+test_bad_reply_input_line(struct server_connection *conn,
+ const char *line ATTR_UNUSED)
+{
+ if (conn->state == SERVER_CONNECTION_STATE_EHLO)
+ return 0;
+ o_stream_nsend_str(conn->conn.output,
+ "666 Really bad reply\r\n");
+ server_connection_deinit(&conn);
+ return -1;
+}
+
+static void test_server_bad_reply(unsigned int index)
+{
+ test_server_input_line = test_bad_reply_input_line;
+ test_server_run(index);
+}
+
+/* client */
+
+struct _bad_reply {
+ unsigned int count;
+};
+
+static void
+test_client_bad_reply_reply(const struct smtp_reply *reply,
+ struct _bad_reply *ctx)
+{
+ if (debug)
+ i_debug("REPLY: %s", smtp_reply_log(reply));
+
+ test_assert(reply->status == SMTP_CLIENT_COMMAND_ERROR_BAD_REPLY);
+
+ if (--ctx->count == 0) {
+ i_free(ctx);
+ io_loop_stop(ioloop);
+ }
+}
+
+static bool
+test_client_bad_reply(
+ const struct smtp_client_settings *client_set)
+{
+ struct smtp_client_connection *sconn;
+ struct smtp_client_command *scmd;
+ struct _bad_reply *ctx;
+
+ test_expect_errors(2);
+
+ ctx = i_new(struct _bad_reply, 1);
+ ctx->count = 2;
+
+ smtp_client = smtp_client_init(client_set);
+
+ sconn = smtp_client_connection_create(
+ smtp_client, SMTP_PROTOCOL_SMTP,
+ net_ip2addr(&bind_ip), bind_ports[0],
+ SMTP_CLIENT_SSL_MODE_NONE, NULL);
+ smtp_client_connection_connect(sconn, NULL, NULL);
+ scmd = smtp_client_command_new(
+ sconn, 0, test_client_bad_reply_reply, ctx);
+ smtp_client_command_write(scmd, "FROP");
+ smtp_client_command_submit(scmd);
+
+ sconn = smtp_client_connection_create(
+ smtp_client, SMTP_PROTOCOL_SMTP,
+ net_ip2addr(&bind_ip), bind_ports[0],
+ SMTP_CLIENT_SSL_MODE_NONE, NULL);
+ smtp_client_connection_connect(sconn, NULL, NULL);
+ scmd = smtp_client_command_new(
+ sconn, 0, test_client_bad_reply_reply, ctx);
+ smtp_client_command_write(scmd, "FROP");
+ smtp_client_command_submit(scmd);
+
+ return TRUE;
+}
+
+/* test */
+
+static void test_bad_reply(void)
+{
+ struct smtp_client_settings smtp_client_set;
+
+ test_client_defaults(&smtp_client_set);
+
+ test_begin("bad reply");
+ test_run_client_server(&smtp_client_set,
+ test_client_bad_reply,
+ test_server_bad_reply, 1, NULL);
+ test_end();
+}
+
+/*
+ * Bad greeting
+ */
+
+/* server */
+
+static int test_bad_greeting_init(struct server_connection *conn)
+{
+ switch (server_index) {
+ case 0:
+ o_stream_nsend_str(conn->conn.output,
+ "666 Mouhahahaha!!\r\n");
+ break;
+ case 1:
+ o_stream_nsend_str(conn->conn.output,
+ "446 Not right now, sorry.\r\n");
+ break;
+ case 2:
+ o_stream_nsend_str(conn->conn.output,
+ "233 Gimme all your mail, NOW!!\r\n");
+ break;
+ }
+ server_connection_deinit(&conn);
+ return -1;
+}
+
+static void test_server_bad_greeting(unsigned int index)
+{
+ test_server_init = test_bad_greeting_init;
+ test_server_run(index);
+}
+
+/* client */
+
+struct _bad_greeting {
+ unsigned int count;
+};
+
+struct _bad_greeting_peer {
+ struct _bad_greeting *context;
+ unsigned int index;
+};
+
+static void
+test_client_bad_greeting_reply(const struct smtp_reply *reply,
+ struct _bad_greeting_peer *pctx)
+{
+ if (debug)
+ i_debug("REPLY: %s", smtp_reply_log(reply));
+
+ switch (pctx->index) {
+ case 0:
+ test_assert(reply->status ==
+ SMTP_CLIENT_COMMAND_ERROR_BAD_REPLY);
+ break;
+ case 1:
+ test_assert(reply->status == 446);
+ break;
+ case 2:
+ test_assert(reply->status ==
+ SMTP_CLIENT_COMMAND_ERROR_BAD_REPLY);
+ break;
+ }
+
+ if (--pctx->context->count == 0) {
+ i_free(pctx->context);
+ io_loop_stop(ioloop);
+ }
+ i_free(pctx);
+}
+
+static void
+test_client_bad_greeting_submit(struct _bad_greeting *ctx, unsigned int index)
+{
+ struct smtp_client_connection *sconn;
+ struct smtp_client_command *scmd;
+ struct _bad_greeting_peer *pctx;
+
+ pctx = i_new(struct _bad_greeting_peer, 1);
+ pctx->context = ctx;
+ pctx->index = index;
+
+ sconn = smtp_client_connection_create(
+ smtp_client, SMTP_PROTOCOL_SMTP,
+ net_ip2addr(&bind_ip), bind_ports[index],
+ SMTP_CLIENT_SSL_MODE_NONE, NULL);
+ smtp_client_connection_connect(sconn, NULL, NULL);
+ scmd = smtp_client_command_new(
+ sconn, 0, test_client_bad_greeting_reply, pctx);
+ smtp_client_command_write(scmd, "FROP");
+ smtp_client_command_submit(scmd);
+}
+
+static bool
+test_client_bad_greeting(const struct smtp_client_settings *client_set)
+{
+ struct _bad_greeting *ctx;
+
+ test_expect_errors(2);
+
+ ctx = i_new(struct _bad_greeting, 1);
+ ctx->count = 3;
+
+ smtp_client = smtp_client_init(client_set);
+
+ test_client_bad_greeting_submit(ctx, 0);
+ test_client_bad_greeting_submit(ctx, 1);
+ test_client_bad_greeting_submit(ctx, 2);
+ return TRUE;
+}
+
+/* test */
+
+static void test_bad_greeting(void)
+{
+ struct smtp_client_settings smtp_client_set;
+
+ test_client_defaults(&smtp_client_set);
+
+ test_begin("bad greeting");
+ test_run_client_server(&smtp_client_set,
+ test_client_bad_greeting,
+ test_server_bad_greeting, 3, NULL);
+ test_end();
+}
+
+/*
+ * Command timeout
+ */
+
+/* server */
+
+static int
+test_command_timed_out_input_line(struct server_connection *conn,
+ const char *line ATTR_UNUSED)
+{
+ if (conn->state == SERVER_CONNECTION_STATE_EHLO)
+ return 0;
+ i_sleep_intr_secs(10);
+ server_connection_deinit(&conn);
+ return -1;
+}
+
+static void test_server_command_timed_out(unsigned int index)
+{
+ test_server_input_line = test_command_timed_out_input_line;
+ test_server_run(index);
+}
+
+/* client */
+
+struct _command_timed_out {
+ unsigned int count;
+};
+
+static void
+test_client_command_timed_out_reply(const struct smtp_reply *reply,
+ struct _command_timed_out *ctx)
+{
+ if (debug)
+ i_debug("REPLY: %s", smtp_reply_log(reply));
+
+ test_assert(reply->status == SMTP_CLIENT_COMMAND_ERROR_TIMED_OUT);
+
+ if (--ctx->count == 0) {
+ i_free(ctx);
+ io_loop_stop(ioloop);
+ }
+}
+
+static bool
+test_client_command_timed_out(const struct smtp_client_settings *client_set)
+{
+ struct smtp_client_connection *sconn;
+ struct smtp_client_command *scmd;
+ struct _command_timed_out *ctx;
+
+ test_expect_errors(1);
+
+ ctx = i_new(struct _command_timed_out, 1);
+ ctx->count = 1;
+
+ smtp_client = smtp_client_init(client_set);
+
+ sconn = smtp_client_connection_create(
+ smtp_client, SMTP_PROTOCOL_SMTP,
+ net_ip2addr(&bind_ip), bind_ports[0],
+ SMTP_CLIENT_SSL_MODE_NONE, NULL);
+ smtp_client_connection_connect(sconn, NULL, NULL);
+ scmd = smtp_client_command_new(
+ sconn, 0, test_client_command_timed_out_reply, ctx);
+ smtp_client_command_write(scmd, "FROP");
+ smtp_client_command_submit(scmd);
+
+ return TRUE;
+}
+
+/* test */
+
+static void test_command_timed_out(void)
+{
+ struct smtp_client_settings smtp_client_set;
+
+ test_client_defaults(&smtp_client_set);
+ smtp_client_set.command_timeout_msecs = 1000;
+
+ test_begin("command timed out");
+ test_run_client_server(&smtp_client_set,
+ test_client_command_timed_out,
+ test_server_command_timed_out, 1, NULL);
+ test_end();
+}
+
+/*
+ * Command aborted early
+ */
+
+/* server */
+
+static int
+test_command_aborted_early_input_line(struct server_connection *conn,
+ const char *line ATTR_UNUSED)
+{
+ if (conn->state == SERVER_CONNECTION_STATE_EHLO)
+ return 0;
+
+ i_sleep_intr_secs(1);
+ o_stream_nsend_str(conn->conn.output, "200 OK\r\n");
+ server_connection_deinit(&conn);
+ return -1;
+}
+
+static void test_server_command_aborted_early(unsigned int index)
+{
+ test_server_input_line = test_command_aborted_early_input_line;
+ test_server_run(index);
+}
+
+/* client */
+
+struct _command_aborted_early {
+ struct smtp_client_command *cmd;
+ struct timeout *to;
+};
+
+static void
+test_client_command_aborted_early_reply(
+ const struct smtp_reply *reply,
+ struct _command_aborted_early *ctx ATTR_UNUSED)
+{
+ if (debug)
+ i_debug("REPLY: %s", smtp_reply_log(reply));
+
+ /* abort does not trigger callback */
+ test_assert(FALSE);
+}
+
+static void
+test_client_command_aborted_early_timeout(struct _command_aborted_early *ctx)
+{
+ timeout_remove(&ctx->to);
+
+ if (ctx->cmd != NULL) {
+ if (debug)
+ i_debug("ABORT");
+
+ /* abort early */
+ smtp_client_command_abort(&ctx->cmd);
+
+ /* wait a little for server to actually respond to an
+ already aborted request */
+ ctx->to = timeout_add_short(
+ 1000, test_client_command_aborted_early_timeout, ctx);
+ } else {
+ if (debug)
+ i_debug("FINISHED");
+
+ /* all done */
+ i_free(ctx);
+ io_loop_stop(ioloop);
+ }
+}
+
+static bool
+test_client_command_aborted_early(const struct smtp_client_settings *client_set)
+{
+ struct smtp_client_connection *sconn;
+ struct _command_aborted_early *ctx;
+
+ ctx = i_new(struct _command_aborted_early, 1);
+
+ smtp_client = smtp_client_init(client_set);
+
+ sconn = smtp_client_connection_create(smtp_client,
+ SMTP_PROTOCOL_SMTP, net_ip2addr(&bind_ip), bind_ports[0],
+ SMTP_CLIENT_SSL_MODE_NONE, NULL);
+ smtp_client_connection_connect(sconn, NULL, NULL);
+ ctx->cmd = smtp_client_command_new(sconn, 0,
+ test_client_command_aborted_early_reply, ctx);
+ smtp_client_command_write(ctx->cmd, "FROP");
+ smtp_client_command_submit(ctx->cmd);
+
+ ctx->to = timeout_add_short(500,
+ test_client_command_aborted_early_timeout, ctx);
+
+ return TRUE;
+}
+
+/* test */
+
+static void test_command_aborted_early(void)
+{
+ struct smtp_client_settings smtp_client_set;
+
+ test_client_defaults(&smtp_client_set);
+
+ test_begin("command aborted early");
+ test_run_client_server(&smtp_client_set,
+ test_client_command_aborted_early,
+ test_server_command_aborted_early, 1, NULL);
+ test_end();
+}
+
+/*
+ * Client deinit early
+ */
+
+/* server */
+
+static int
+test_client_deinit_early_input_line(struct server_connection *conn,
+ const char *line ATTR_UNUSED)
+{
+ if (conn->state == SERVER_CONNECTION_STATE_EHLO)
+ return 0;
+
+ i_sleep_intr_secs(1);
+ o_stream_nsend_str(conn->conn.output, "200 OK\r\n");
+ server_connection_deinit(&conn);
+ return -1;
+}
+
+static void test_server_client_deinit_early(unsigned int index)
+{
+ test_server_input_line = test_client_deinit_early_input_line;
+ test_server_run(index);
+}
+
+/* client */
+
+struct _client_deinit_early {
+ struct smtp_client_command *cmd;
+ struct timeout *to;
+};
+
+static void
+test_client_client_deinit_early_reply(
+ const struct smtp_reply *reply,
+ struct _client_deinit_early *ctx ATTR_UNUSED)
+{
+ if (debug)
+ i_debug("REPLY: %s", smtp_reply_log(reply));
+
+ /* abort does not trigger callback */
+ test_assert(FALSE);
+}
+
+static void
+test_client_client_deinit_early_timeout(struct _client_deinit_early *ctx)
+{
+ timeout_remove(&ctx->to);
+
+ /* deinit early */
+ smtp_client_deinit(&smtp_client);
+
+ /* all done */
+ i_free(ctx);
+ io_loop_stop(ioloop);
+}
+
+static bool
+test_client_client_deinit_early(const struct smtp_client_settings *client_set)
+{
+ struct smtp_client_connection *sconn;
+ struct _client_deinit_early *ctx;
+
+ ctx = i_new(struct _client_deinit_early, 1);
+
+ smtp_client = smtp_client_init(client_set);
+
+ sconn = smtp_client_connection_create(
+ smtp_client, SMTP_PROTOCOL_SMTP,
+ net_ip2addr(&bind_ip), bind_ports[0],
+ SMTP_CLIENT_SSL_MODE_NONE, NULL);
+ smtp_client_connection_connect(sconn, NULL, NULL);
+ ctx->cmd = smtp_client_command_new(
+ sconn, 0, test_client_client_deinit_early_reply, ctx);
+ smtp_client_command_write(ctx->cmd, "FROP");
+ smtp_client_command_submit(ctx->cmd);
+
+ ctx->to = timeout_add_short(
+ 500, test_client_client_deinit_early_timeout, ctx);
+
+ return TRUE;
+}
+
+/* test */
+
+static void test_client_deinit_early(void)
+{
+ struct smtp_client_settings smtp_client_set;
+
+ test_client_defaults(&smtp_client_set);
+
+
+ test_begin("client deinit early");
+ test_run_client_server(&smtp_client_set,
+ test_client_client_deinit_early,
+ test_server_client_deinit_early, 1, NULL);
+ test_end();
+}
+
+/*
+ * DNS service failure
+ */
+
+/* client */
+
+struct _dns_service_failure {
+ unsigned int count;
+};
+
+static void
+test_client_dns_service_failure_reply(const struct smtp_reply *reply,
+ struct _dns_service_failure *ctx)
+{
+ if (debug)
+ i_debug("REPLY: %s", smtp_reply_log(reply));
+
+ test_assert(reply->status ==
+ SMTP_CLIENT_COMMAND_ERROR_HOST_LOOKUP_FAILED);
+
+ if (--ctx->count == 0) {
+ i_free(ctx);
+ io_loop_stop(ioloop);
+ }
+}
+
+static bool
+test_client_dns_service_failure(const struct smtp_client_settings *client_set)
+{
+ struct smtp_client_connection *sconn;
+ struct smtp_client_command *scmd;
+ struct _dns_service_failure *ctx;
+
+ test_expect_errors(2);
+
+ ctx = i_new(struct _dns_service_failure, 1);
+ ctx->count = 2;
+
+ smtp_client = smtp_client_init(client_set);
+
+ sconn = smtp_client_connection_create(
+ smtp_client, SMTP_PROTOCOL_SMTP, "host.in-addr.arpa", 465,
+ SMTP_CLIENT_SSL_MODE_IMMEDIATE, NULL);
+ smtp_client_connection_connect(sconn, NULL, NULL);
+ scmd = smtp_client_command_new(
+ sconn, 0, test_client_dns_service_failure_reply, ctx);
+ smtp_client_command_write(scmd, "FROP");
+ smtp_client_command_submit(scmd);
+
+ sconn = smtp_client_connection_create(
+ smtp_client, SMTP_PROTOCOL_SMTP, "host.in-addr.arpa", 465,
+ SMTP_CLIENT_SSL_MODE_IMMEDIATE, NULL);
+ smtp_client_connection_connect(sconn, NULL, NULL);
+ scmd = smtp_client_command_new(
+ sconn, 0, test_client_dns_service_failure_reply, ctx);
+ smtp_client_command_write(scmd, "FROP");
+ smtp_client_command_submit(scmd);
+
+ return TRUE;
+}
+
+/* test */
+
+static void test_dns_service_failure(void)
+{
+ struct smtp_client_settings smtp_client_set;
+
+ test_client_defaults(&smtp_client_set);
+ smtp_client_set.dns_client_socket_path = "./frop";
+
+ test_begin("dns service failure");
+ test_run_client_server(&smtp_client_set,
+ test_client_dns_service_failure, NULL, 0, NULL);
+ test_end();
+}
+
+/*
+ * DNS timeout
+ */
+
+/* dns */
+
+static void test_dns_timeout_input(struct server_connection *conn ATTR_UNUSED)
+{
+ /* hang */
+ i_sleep_intr_secs(100);
+
+ io_loop_stop(current_ioloop);
+ io_remove(&io_listen);
+ i_close_fd(&fd_listen);
+ server_connection_deinit(&conn);
+}
+
+static void test_dns_dns_timeout(void)
+{
+ test_server_input = test_dns_timeout_input;
+ test_server_run(0);
+}
+
+/* client */
+
+struct _dns_timeout {
+ unsigned int count;
+};
+
+static void
+test_client_dns_timeout_reply(const struct smtp_reply *reply,
+ struct _dns_timeout *ctx)
+{
+ if (debug)
+ i_debug("REPLY: %s", smtp_reply_log(reply));
+
+ test_assert(reply->status ==
+ SMTP_CLIENT_COMMAND_ERROR_HOST_LOOKUP_FAILED);
+
+ if (--ctx->count == 0) {
+ i_free(ctx);
+ io_loop_stop(ioloop);
+ }
+}
+
+static bool
+test_client_dns_timeout(const struct smtp_client_settings *client_set)
+{
+ struct smtp_client_connection *sconn;
+ struct smtp_client_command *scmd;
+ struct _dns_timeout *ctx;
+
+ test_expect_errors(2);
+
+ ctx = i_new(struct _dns_timeout, 1);
+ ctx->count = 2;
+
+ smtp_client = smtp_client_init(client_set);
+
+ sconn = smtp_client_connection_create(
+ smtp_client, SMTP_PROTOCOL_SMTP, "example.com", 465,
+ SMTP_CLIENT_SSL_MODE_IMMEDIATE, NULL);
+ smtp_client_connection_connect(sconn, NULL, NULL);
+ scmd = smtp_client_command_new(
+ sconn, 0, test_client_dns_timeout_reply, ctx);
+ smtp_client_command_write(scmd, "FROP");
+ smtp_client_command_submit(scmd);
+
+ sconn = smtp_client_connection_create(
+ smtp_client, SMTP_PROTOCOL_SMTP, "example.com", 465,
+ SMTP_CLIENT_SSL_MODE_IMMEDIATE, NULL);
+ smtp_client_connection_connect(sconn, NULL, NULL);
+ scmd = smtp_client_command_new(
+ sconn, 0, test_client_dns_timeout_reply, ctx);
+ smtp_client_command_write(scmd, "FROP");
+ smtp_client_command_submit(scmd);
+
+ return TRUE;
+}
+
+/* test */
+
+static void test_dns_timeout(void)
+{
+ struct smtp_client_settings smtp_client_set;
+
+ test_client_defaults(&smtp_client_set);
+ smtp_client_set.connect_timeout_msecs = 2000;
+ smtp_client_set.dns_client_socket_path = "./dns-test";
+
+ test_begin("dns timeout");
+ test_run_client_server(&smtp_client_set,
+ test_client_dns_timeout, NULL, 0,
+ test_dns_dns_timeout);
+ test_end();
+}
+
+/*
+ * DNS lookup failure
+ */
+
+/* dns */
+
+static void
+test_dns_lookup_failure_input(struct server_connection *conn)
+{
+ o_stream_nsend_str(
+ conn->conn.output,
+ t_strdup_printf("VERSION\tdns\t1\t0\n%d\tFAIL\n", EAI_FAIL));
+ server_connection_deinit(&conn);
+}
+
+static void test_dns_dns_lookup_failure(void)
+{
+ test_server_input = test_dns_lookup_failure_input;
+ test_server_run(0);
+}
+
+/* client */
+
+struct _dns_lookup_failure {
+ unsigned int count;
+};
+
+static void
+test_client_dns_lookup_failure_reply(const struct smtp_reply *reply,
+ struct _dns_lookup_failure *ctx)
+{
+ if (debug)
+ i_debug("REPLY: %s", smtp_reply_log(reply));
+
+ test_assert(reply->status ==
+ SMTP_CLIENT_COMMAND_ERROR_HOST_LOOKUP_FAILED);
+
+ if (--ctx->count == 0) {
+ i_free(ctx);
+ io_loop_stop(ioloop);
+ }
+}
+
+static bool
+test_client_dns_lookup_failure(const struct smtp_client_settings *client_set)
+{
+ struct smtp_client_connection *sconn;
+ struct smtp_client_command *scmd;
+ struct _dns_lookup_failure *ctx;
+
+ test_expect_errors(2);
+
+ ctx = i_new(struct _dns_lookup_failure, 1);
+ ctx->count = 2;
+
+ smtp_client = smtp_client_init(client_set);
+
+ sconn = smtp_client_connection_create(
+ smtp_client, SMTP_PROTOCOL_SMTP, "example.com", 465,
+ SMTP_CLIENT_SSL_MODE_IMMEDIATE, NULL);
+ smtp_client_connection_connect(sconn, NULL, NULL);
+ scmd = smtp_client_command_new(
+ sconn, 0, test_client_dns_lookup_failure_reply, ctx);
+ smtp_client_command_write(scmd, "FROP");
+ smtp_client_command_submit(scmd);
+
+ sconn = smtp_client_connection_create(
+ smtp_client, SMTP_PROTOCOL_SMTP, "example.com", 465,
+ SMTP_CLIENT_SSL_MODE_IMMEDIATE, NULL);
+ smtp_client_connection_connect(sconn, NULL, NULL);
+ scmd = smtp_client_command_new(
+ sconn, 0, test_client_dns_lookup_failure_reply, ctx);
+ smtp_client_command_write(scmd, "FROP");
+ smtp_client_command_submit(scmd);
+
+ return TRUE;
+}
+
+/* test */
+
+static void test_dns_lookup_failure(void)
+{
+ struct smtp_client_settings smtp_client_set;
+
+ test_client_defaults(&smtp_client_set);
+ smtp_client_set.dns_client_socket_path = "./dns-test";
+
+ test_begin("dns lookup failure");
+ test_run_client_server(&smtp_client_set,
+ test_client_dns_lookup_failure, NULL, 0,
+ test_dns_dns_lookup_failure);
+ test_end();
+}
+
+/*
+ * Authentication failed
+ */
+
+/* server */
+
+static int
+test_authentication_input_line(struct server_connection *conn,
+ const char *line ATTR_UNUSED)
+{
+ switch (conn->state) {
+ case SERVER_CONNECTION_STATE_EHLO:
+ if (server_index > 0) {
+ o_stream_nsend_str(
+ conn->conn.output,
+ "250-testserver\r\n"
+ "250-PIPELINING\r\n"
+ "250-ENHANCEDSTATUSCODES\r\n"
+ "250-AUTH PLAIN\r\n"
+ "250 DSN\r\n");
+ conn->state = SERVER_CONNECTION_STATE_MAIL_FROM;
+ return 1;
+ }
+ break;
+ case SERVER_CONNECTION_STATE_MAIL_FROM:
+ switch (server_index ) {
+ case 1:
+ o_stream_nsend_str(
+ conn->conn.output,
+ "535 5.7.8 "
+ "Authentication credentials invalid\r\n");
+ i_sleep_intr_secs(10);
+ server_connection_deinit(&conn);
+ return -1;
+ case 3: case 5:
+ if (str_begins(line, "AUTH ")) {
+ o_stream_nsend_str(conn->conn.output,
+ "334 \r\n");
+ return 1;
+ }
+ if (str_begins(line, "EHLO ")) {
+ o_stream_nsend_str(conn->conn.output,
+ "250-testserver\r\n"
+ "250-PIPELINING\r\n"
+ "250-ENHANCEDSTATUSCODES\r\n"
+ "250-AUTH PLAIN\r\n"
+ "250 DSN\r\n");
+ return 1;
+ }
+ if (!str_begins(line, "MAIL ")) {
+ o_stream_nsend_str(
+ conn->conn.output, "235 2.7.0 "
+ "Authentication successful\r\n");
+ return 1;
+ }
+ }
+ break;
+ default:
+ break;
+ }
+ return 0;
+}
+
+static void test_server_authentication(unsigned int index)
+{
+ test_server_input_line = test_authentication_input_line;
+ test_server_run(index);
+}
+
+/* client */
+
+struct _authentication {
+ unsigned int count;
+};
+
+struct _authentication_peer {
+ struct _authentication *context;
+ unsigned int index;
+
+ struct smtp_client_connection *conn;
+ struct smtp_client_transaction *trans;
+};
+
+static void
+test_client_authentication_login_cb(const struct smtp_reply *reply,
+ void *context)
+{
+ struct _authentication_peer *pctx =
+ (struct _authentication_peer *)context;
+
+ if (debug) {
+ i_debug("LOGIN REPLY[%u]: %s",
+ pctx->index, smtp_reply_log(reply));
+ }
+
+ switch (pctx->index) {
+ case 0:
+ test_assert(reply->status ==
+ SMTP_CLIENT_COMMAND_ERROR_AUTH_FAILED);
+ break;
+ case 1:
+ test_assert(reply->status == 535);
+ break;
+ case 2: case 3: case 4: case 5:
+ test_assert(reply->status == 250);
+ break;
+ }
+}
+
+static void
+test_client_authentication_mail_from_cb(
+ const struct smtp_reply *reply,
+ struct _authentication_peer *pctx)
+{
+ if (debug) {
+ i_debug("MAIL FROM REPLY[%u]: %s",
+ pctx->index, smtp_reply_log(reply));
+ }
+
+ switch (pctx->index) {
+ case 0:
+ test_assert(reply->status ==
+ SMTP_CLIENT_COMMAND_ERROR_AUTH_FAILED);
+ break;
+ case 1:
+ test_assert(reply->status == 535);
+ break;
+ case 2: case 3: case 4: case 5:
+ test_assert(reply->status == 250);
+ break;
+ }
+}
+
+static void
+test_client_authentication_rcpt_to_cb(
+ const struct smtp_reply *reply,
+ struct _authentication_peer *pctx)
+{
+ if (debug) {
+ i_debug("RCPT TO REPLY[%u]: %s",
+ pctx->index, smtp_reply_log(reply));
+ }
+
+ switch (pctx->index) {
+ case 0:
+ test_assert(reply->status ==
+ SMTP_CLIENT_COMMAND_ERROR_AUTH_FAILED);
+ break;
+ case 1:
+ test_assert(reply->status == 535);
+ break;
+ case 2: case 3: case 4: case 5:
+ test_assert(reply->status == 250);
+ break;
+ }
+}
+
+static void
+test_client_authentication_rcpt_data_cb(
+ const struct smtp_reply *reply,
+ struct _authentication_peer *pctx)
+{
+ if (debug) {
+ i_debug("RCPT DATA REPLY[%u]: %s",
+ pctx->index, smtp_reply_log(reply));
+ }
+
+
+ switch (pctx->index) {
+ case 0:
+ case 1:
+ test_assert(FALSE);
+ break;
+ case 2: case 3: case 4: case 5:
+ test_assert(TRUE);
+ break;
+ }
+}
+
+static void
+test_client_authentication_data_cb(
+ const struct smtp_reply *reply,
+ struct _authentication_peer *pctx)
+{
+ if (debug) {
+ i_debug("DATA REPLY[%u]: %s",
+ pctx->index, smtp_reply_log(reply));
+ }
+
+ switch (pctx->index) {
+ case 0:
+ test_assert(reply->status ==
+ SMTP_CLIENT_COMMAND_ERROR_AUTH_FAILED);
+ break;
+ case 1:
+ test_assert(reply->status == 535);
+ break;
+ case 2: case 3: case 4: case 5:
+ test_assert(reply->status == 250);
+ break;
+ }
+}
+
+static void
+test_client_authentication_finished(
+ struct _authentication_peer *pctx)
+{
+ struct _authentication *ctx = pctx->context;
+
+ if (debug)
+ i_debug("FINISHED[%u]", pctx->index);
+ if (--ctx->count == 0) {
+ i_free(ctx);
+ io_loop_stop(ioloop);
+ }
+
+ pctx->trans = NULL;
+ i_free(pctx);
+}
+
+static void
+test_client_authentication_submit(struct _authentication *ctx,
+ unsigned int index)
+{
+ struct _authentication_peer *pctx;
+ struct smtp_client_settings smtp_set;
+ static const char *message =
+ "From: stephan@example.com\r\n"
+ "To: timo@example.com\r\n"
+ "Subject: Frop!\r\n"
+ "\r\n"
+ "Frop!\r\n";
+ struct istream *input;
+
+ pctx = i_new(struct _authentication_peer, 1);
+ pctx->context = ctx;
+ pctx->index = index;
+
+ i_zero(&smtp_set);
+ smtp_set.username = "peter.wolfsen";
+
+ switch (index) {
+ case 3: /* Much too large for initial response */
+ smtp_set.password =
+ "0123456789abcdef0123456789abcdef"
+ "0123456789abcdef0123456789abcdef"
+ "0123456789abcdef0123456789abcdef"
+ "0123456789abcdef0123456789abcdef"
+ "0123456789abcdef0123456789abcdef"
+ "0123456789abcdef0123456789abcdef"
+ "0123456789abcdef0123456789abcdef"
+ "0123456789abcdef0123456789abcdef"
+ "0123456789abcdef0123456789abcdef"
+ "0123456789abcdef0123456789abcdef"
+ "0123456789abcdef0123456789abcdef"
+ "0123456789abcdef0123456789abcdef"
+ "0123456789abcdef0123456789abcdef"
+ "0123456789abcdef0123456789abcdef"
+ "0123456789abcdef0123456789abcdef"
+ "0123456789abcdef0123456789abcdef"
+ "0123456789abcdef0123456789abcdef";
+ break;
+ case 4: /* Just small enough for initial response */
+ smtp_set.password =
+ "0123456789abcdef0123456789abcdef"
+ "0123456789abcdef0123456789abcdef"
+ "0123456789abcdef0123456789abcdef"
+ "0123456789abcdef0123456789abcdef"
+ "0123456789abcdef0123456789abcdef"
+ "0123456789abcdef0123456789abcdef"
+ "0123456789abcdef0123456789abcdef"
+ "0123456789abcdef0123456789abcdef"
+ "0123456789abcdef0123456789abcdef"
+ "0123456789abcdef0123456789abcdef"
+ "0123456789abcdef0123456789abcdef"
+ "01234";
+ break;
+ case 5: /* Just too large for initial response */
+ smtp_set.password =
+ "0123456789abcdef0123456789abcdef"
+ "0123456789abcdef0123456789abcdef"
+ "0123456789abcdef0123456789abcdef"
+ "0123456789abcdef0123456789abcdef"
+ "0123456789abcdef0123456789abcdef"
+ "0123456789abcdef0123456789abcdef"
+ "0123456789abcdef0123456789abcdef"
+ "0123456789abcdef0123456789abcdef"
+ "0123456789abcdef0123456789abcdef"
+ "0123456789abcdef0123456789abcdef"
+ "0123456789abcdef0123456789abcdef"
+ "012345";
+ break;
+ default:
+ smtp_set.password = "crybaby";
+ break;
+ }
+
+ pctx->conn = smtp_client_connection_create(
+ smtp_client, SMTP_PROTOCOL_SMTP,
+ net_ip2addr(&bind_ip), bind_ports[index],
+ SMTP_CLIENT_SSL_MODE_NONE, &smtp_set);
+ pctx->trans = smtp_client_transaction_create(
+ pctx->conn, &((struct smtp_address){
+ .localpart = "sender",
+ .domain = "example.com"}), NULL, 0,
+ test_client_authentication_finished, pctx);
+ smtp_client_connection_connect(
+ pctx->conn, test_client_authentication_login_cb,
+ (void *)pctx);
+ smtp_client_transaction_start(
+ pctx->trans, test_client_authentication_mail_from_cb,
+ pctx);
+ smtp_client_connection_unref(&pctx->conn);
+
+ smtp_client_transaction_add_rcpt(
+ pctx->trans, &((struct smtp_address){
+ .localpart = "rcpt",
+ .domain = "example.com"}), NULL,
+ test_client_authentication_rcpt_to_cb,
+ test_client_authentication_rcpt_data_cb, pctx);
+
+ input = i_stream_create_from_data(message, strlen(message));
+ i_stream_set_name(input, "message");
+
+ smtp_client_transaction_send(
+ pctx->trans, input,
+ test_client_authentication_data_cb, pctx);
+ i_stream_unref(&input);
+}
+
+static bool
+test_client_authentication(const struct smtp_client_settings *client_set)
+{
+ struct _authentication *ctx;
+ unsigned int i;
+
+ test_expect_errors(2);
+
+ ctx = i_new(struct _authentication, 1);
+ ctx->count = 6;
+
+ smtp_client = smtp_client_init(client_set);
+
+ for (i = 0; i < ctx->count; i++)
+ test_client_authentication_submit(ctx, i);
+
+ return TRUE;
+}
+
+/* test */
+
+static void test_authentication(void)
+{
+ struct smtp_client_settings smtp_client_set;
+
+ test_client_defaults(&smtp_client_set);
+
+ test_begin("authentication");
+ test_run_client_server(&smtp_client_set,
+ test_client_authentication,
+ test_server_authentication, 6, NULL);
+ test_end();
+}
+
+/*
+ * Transaction timeout
+ */
+
+/* server */
+
+static int
+test_transaction_timeout_input_line(struct server_connection *conn,
+ const char *line ATTR_UNUSED)
+{
+ switch (conn->state) {
+ case SERVER_CONNECTION_STATE_EHLO:
+ break;
+ case SERVER_CONNECTION_STATE_MAIL_FROM:
+ if (server_index == 0)
+ i_sleep_intr_secs(20);
+ break;
+ case SERVER_CONNECTION_STATE_RCPT_TO:
+ if (server_index == 1)
+ i_sleep_intr_secs(20);
+ break;
+ case SERVER_CONNECTION_STATE_DATA:
+ if (server_index == 2)
+ i_sleep_intr_secs(20);
+ break;
+ case SERVER_CONNECTION_STATE_FINISH:
+ break;
+ }
+ return 0;
+}
+
+static void test_server_transaction_timeout(unsigned int index)
+{
+ test_expect_errors(1);
+ test_server_input_line = test_transaction_timeout_input_line;
+ test_server_run(index);
+}
+
+/* client */
+
+struct _transaction_timeout {
+ unsigned int count;
+};
+
+struct _transaction_timeout_peer {
+ struct _transaction_timeout *context;
+ unsigned int index;
+
+ struct smtp_client_connection *conn;
+ struct smtp_client_transaction *trans;
+ struct timeout *to;
+
+ bool login_callback:1;
+ bool mail_from_callback:1;
+ bool rcpt_to_callback:1;
+ bool rcpt_data_callback:1;
+ bool data_callback:1;
+};
+
+static void
+test_client_transaction_timeout_mail_from_cb(
+ const struct smtp_reply *reply,
+ struct _transaction_timeout_peer *pctx)
+{
+ if (debug) {
+ i_debug("MAIL FROM REPLY[%u]: %s",
+ pctx->index, smtp_reply_log(reply));
+ }
+
+ pctx->mail_from_callback = TRUE;
+
+ switch (pctx->index) {
+ case 0:
+ test_assert(reply->status == 451);
+ break;
+ case 1: case 2: case 3:
+ test_assert(smtp_reply_is_success(reply));
+ break;
+ }
+}
+
+static void
+test_client_transaction_timeout_rcpt_to_cb(
+ const struct smtp_reply *reply, struct _transaction_timeout_peer *pctx)
+{
+ if (debug) {
+ i_debug("RCPT TO REPLY[%u]: %s",
+ pctx->index, smtp_reply_log(reply));
+ }
+
+ pctx->rcpt_to_callback = TRUE;
+
+ switch (pctx->index) {
+ case 0: case 1:
+ test_assert(reply->status == 451);
+ break;
+ case 2: case 3:
+ test_assert(smtp_reply_is_success(reply));
+ break;
+ }
+}
+
+static void
+test_client_transaction_timeout_rcpt_data_cb(
+ const struct smtp_reply *reply, struct _transaction_timeout_peer *pctx)
+{
+ if (debug) {
+ i_debug("RCPT DATA REPLY[%u]: %s",
+ pctx->index, smtp_reply_log(reply));
+ }
+
+ pctx->rcpt_data_callback = TRUE;
+
+ switch (pctx->index) {
+ case 0: case 1:
+ i_unreached();
+ case 2:
+ test_assert(reply->status == 451);
+ break;
+ case 3:
+ test_assert(smtp_reply_is_success(reply));
+ break;
+ }
+}
+
+static void
+test_client_transaction_timeout_data_cb(const struct smtp_reply *reply,
+ struct _transaction_timeout_peer *pctx)
+{
+ if (debug) {
+ i_debug("DATA REPLY[%u]: %s",
+ pctx->index, smtp_reply_log(reply));
+ }
+
+ pctx->data_callback = TRUE;
+
+ switch (pctx->index) {
+ case 0: case 1: case 2:
+ test_assert(reply->status == 451);
+ break;
+ case 3:
+ test_assert(smtp_reply_is_success(reply));
+ break;
+ }
+}
+
+static void
+test_client_transaction_timeout_finished(struct _transaction_timeout_peer *pctx)
+{
+ struct _transaction_timeout *ctx = pctx->context;
+
+ if (debug)
+ i_debug("FINISHED[%u]", pctx->index);
+ if (--ctx->count == 0) {
+ i_free(ctx);
+ io_loop_stop(ioloop);
+ }
+
+ switch (pctx->index) {
+ case 0: case 1:
+ test_assert(pctx->mail_from_callback);
+ test_assert(pctx->rcpt_to_callback);
+ test_assert(!pctx->rcpt_data_callback);
+ test_assert(pctx->data_callback);
+ break;
+ case 2: case 3:
+ test_assert(pctx->mail_from_callback);
+ test_assert(pctx->rcpt_to_callback);
+ test_assert(pctx->rcpt_data_callback);
+ test_assert(pctx->data_callback);
+ break;
+ }
+
+ pctx->trans = NULL;
+ timeout_remove(&pctx->to);
+ i_free(pctx);
+}
+
+static void
+test_client_transaction_timeout_submit2(struct _transaction_timeout_peer *pctx)
+{
+ struct smtp_client_transaction *strans = pctx->trans;
+ static const char *message =
+ "From: stephan@example.com\r\n"
+ "To: timo@example.com\r\n"
+ "Subject: Frop!\r\n"
+ "\r\n"
+ "Frop!\r\n";
+ struct istream *input;
+
+ timeout_remove(&pctx->to);
+
+ input = i_stream_create_from_data(message, strlen(message));
+ i_stream_set_name(input, "message");
+
+ smtp_client_transaction_send(
+ strans, input, test_client_transaction_timeout_data_cb, pctx);
+ i_stream_unref(&input);
+}
+
+static void
+test_client_transaction_timeout_submit1(struct _transaction_timeout_peer *pctx)
+{
+ timeout_remove(&pctx->to);
+
+ smtp_client_transaction_add_rcpt(
+ pctx->trans, &((struct smtp_address){
+ .localpart = "rcpt",
+ .domain = "example.com"}), NULL,
+ test_client_transaction_timeout_rcpt_to_cb,
+ test_client_transaction_timeout_rcpt_data_cb, pctx);
+
+ pctx->to = timeout_add_short(
+ 500, test_client_transaction_timeout_submit2, pctx);
+}
+
+static void
+test_client_transaction_timeout_submit(struct _transaction_timeout *ctx,
+ unsigned int index)
+{
+ struct _transaction_timeout_peer *pctx;
+
+ pctx = i_new(struct _transaction_timeout_peer, 1);
+ pctx->context = ctx;
+ pctx->index = index;
+
+ pctx->conn = smtp_client_connection_create(
+ smtp_client, SMTP_PROTOCOL_SMTP,
+ net_ip2addr(&bind_ip), bind_ports[index],
+ SMTP_CLIENT_SSL_MODE_NONE, NULL);
+ pctx->trans = smtp_client_transaction_create(
+ pctx->conn, &((struct smtp_address){
+ .localpart = "sender",
+ .domain = "example.com"}), NULL, 0,
+ test_client_transaction_timeout_finished, pctx);
+ smtp_client_transaction_set_timeout(pctx->trans, 1000);
+ smtp_client_transaction_start(
+ pctx->trans, test_client_transaction_timeout_mail_from_cb,
+ pctx);
+ smtp_client_connection_unref(&pctx->conn);
+
+ pctx->to = timeout_add_short(
+ 500, test_client_transaction_timeout_submit1, pctx);
+}
+
+static bool
+test_client_transaction_timeout(const struct smtp_client_settings *client_set)
+{
+ struct _transaction_timeout *ctx;
+ unsigned int i;
+
+ ctx = i_new(struct _transaction_timeout, 1);
+ ctx->count = 4;
+
+ smtp_client = smtp_client_init(client_set);
+
+ for (i = 0; i < ctx->count; i++)
+ test_client_transaction_timeout_submit(ctx, i);
+
+ return TRUE;
+}
+
+/* test */
+
+static void test_transaction_timeout(void)
+{
+ struct smtp_client_settings smtp_client_set;
+
+ test_client_defaults(&smtp_client_set);
+
+ test_begin("transaction timeout");
+ test_run_client_server(&smtp_client_set,
+ test_client_transaction_timeout,
+ test_server_transaction_timeout, 6, NULL);
+ test_end();
+}
+
+/*
+ * Invalid SSL certificate
+ */
+
+#ifdef HAVE_OPENSSL
+
+/* dns */
+
+static void
+test_dns_invalid_ssl_certificate_input(struct server_connection *conn)
+{
+ const char *line;
+
+ if (!conn->version_sent) {
+ conn->version_sent = TRUE;
+ o_stream_nsend_str(conn->conn.output, "VERSION\tdns\t1\t0\n");
+ }
+
+
+ while ((line = i_stream_read_next_line(conn->conn.input)) != NULL) {
+ if (debug)
+ i_debug("DNS REQUEST: %s", line);
+
+ o_stream_nsend_str(conn->conn.output,
+ t_strdup_printf("0\t%s\n",
+ net_ip2addr(&bind_ip)));
+ }
+}
+
+static void test_dns_invalid_ssl_certificate(void)
+{
+ test_server_input = test_dns_invalid_ssl_certificate_input;
+ test_server_run(0);
+}
+
+/* server */
+
+static void
+test_invalid_ssl_certificate_input(struct server_connection *conn)
+{
+ const char *line;
+
+ line = i_stream_read_next_line(conn->conn.input);
+ if (line == NULL) {
+ if (conn->conn.input->eof ||
+ conn->conn.input->stream_errno != 0)
+ server_connection_deinit(&conn);
+ return;
+ }
+ server_connection_deinit(&conn);
+}
+
+static int
+test_invalid_ssl_certificate_init(struct server_connection *conn)
+{
+ sleep(1);
+ o_stream_nsend_str(conn->conn.output,
+ "220 testserver ESMTP Testfix (Frop/GNU)\r\n");
+ return 1;
+}
+
+static void test_server_invalid_ssl_certificate(unsigned int index)
+{
+ test_server_ssl = TRUE;
+ test_server_init = test_invalid_ssl_certificate_init;
+ test_server_input = test_invalid_ssl_certificate_input;
+ test_server_run(index);
+}
+
+/* client */
+
+struct _invalid_ssl_certificate {
+ unsigned int count;
+};
+
+static void
+test_client_invalid_ssl_certificate_reply(const struct smtp_reply *reply,
+ struct _invalid_ssl_certificate *ctx)
+{
+ if (debug)
+ i_debug("REPLY: %s", smtp_reply_log(reply));
+
+ test_assert(reply->status == SMTP_CLIENT_COMMAND_ERROR_CONNECT_FAILED);
+
+ if (--ctx->count == 0) {
+ i_free(ctx);
+ io_loop_stop(ioloop);
+ }
+}
+
+static bool
+test_client_invalid_ssl_certificate(
+ const struct smtp_client_settings *client_set)
+{
+ struct smtp_client_connection *sconn;
+ struct smtp_client_command *scmd;
+ struct _invalid_ssl_certificate *ctx;
+
+ test_expect_errors(2);
+
+ ctx = i_new(struct _invalid_ssl_certificate, 1);
+ ctx->count = 2;
+
+ smtp_client = smtp_client_init(client_set);
+
+ sconn = smtp_client_connection_create(
+ smtp_client, SMTP_PROTOCOL_SMTP, "example.com", bind_ports[0],
+ SMTP_CLIENT_SSL_MODE_IMMEDIATE, NULL);
+ smtp_client_connection_connect(sconn, NULL, NULL);
+ scmd = smtp_client_command_new(
+ sconn, 0, test_client_invalid_ssl_certificate_reply, ctx);
+ smtp_client_command_write(scmd, "FROP");
+ smtp_client_command_submit(scmd);
+
+ sconn = smtp_client_connection_create(
+ smtp_client, SMTP_PROTOCOL_SMTP, "example.com", bind_ports[0],
+ SMTP_CLIENT_SSL_MODE_IMMEDIATE, NULL);
+ smtp_client_connection_connect(sconn, NULL, NULL);
+ scmd = smtp_client_command_new(
+ sconn, 0, test_client_invalid_ssl_certificate_reply, ctx);
+ smtp_client_command_write(scmd, "FROP");
+ smtp_client_command_submit(scmd);
+
+ return TRUE;
+}
+
+/* test */
+
+static void test_invalid_ssl_certificate(void)
+{
+ struct smtp_client_settings smtp_client_set;
+ struct ssl_iostream_settings ssl_set;
+
+ /* ssl settings */
+ ssl_iostream_test_settings_client(&ssl_set);
+ ssl_set.verbose = debug;
+
+ test_client_defaults(&smtp_client_set);
+ smtp_client_set.dns_client_socket_path = "./dns-test";
+ smtp_client_set.ssl = &ssl_set;
+
+ test_begin("invalid ssl certificate");
+ test_run_client_server(&smtp_client_set,
+ test_client_invalid_ssl_certificate,
+ test_server_invalid_ssl_certificate, 1,
+ test_dns_invalid_ssl_certificate);
+ test_end();
+}
+
+#endif
+
+/*
+ * All tests
+ */
+
+static void (*const test_functions[])(void) = {
+ test_unconfigured_ssl,
+ test_unconfigured_ssl_abort,
+ test_host_lookup_failed,
+ test_connection_refused,
+ test_connection_lost_prematurely,
+ test_connection_timed_out,
+ test_broken_payload,
+ test_connection_lost,
+ test_unexpected_reply,
+ test_premature_reply,
+ test_early_data_reply,
+ test_partial_reply,
+ test_bad_reply,
+ test_bad_greeting,
+ test_command_timed_out,
+ test_command_aborted_early,
+ test_client_deinit_early,
+ test_dns_service_failure,
+ test_dns_timeout,
+ test_dns_lookup_failure,
+ test_authentication,
+ test_transaction_timeout,
+#ifdef HAVE_OPENSSL
+ test_invalid_ssl_certificate,
+#endif
+ NULL
+};
+
+/*
+ * Test client
+ */
+
+static void test_client_defaults(struct smtp_client_settings *smtp_set)
+{
+ /* client settings */
+ i_zero(smtp_set);
+ smtp_set->my_hostname = "frop.example.com";
+ smtp_set->debug = debug;
+}
+
+static void test_client_progress_timeout(void *context ATTR_UNUSED)
+{
+ /* Terminate test due to lack of progress */
+ test_assert(FALSE);
+ timeout_remove(&to_client_progress);
+ io_loop_stop(current_ioloop);
+}
+
+static bool
+test_client_init(test_client_init_t client_test,
+ const struct smtp_client_settings *client_set)
+{
+ i_assert(client_test != NULL);
+ if (!client_test(client_set))
+ return FALSE;
+
+ to_client_progress = timeout_add(CLIENT_PROGRESS_TIMEOUT*1000,
+ test_client_progress_timeout, NULL);
+
+ return TRUE;
+}
+
+static void test_client_deinit(void)
+{
+ timeout_remove(&to_client_progress);
+
+ if (smtp_client != NULL)
+ smtp_client_deinit(&smtp_client);
+}
+
+static void
+test_client_run(test_client_init_t client_test,
+ const struct smtp_client_settings *client_set)
+{
+ if (test_client_init(client_test, client_set))
+ io_loop_run(ioloop);
+ test_client_deinit();
+}
+
+/*
+ * Test server
+ */
+
+/* client connection */
+
+static int
+server_connection_init_ssl(struct server_connection *conn)
+{
+ struct ssl_iostream_settings ssl_set;
+ const char *error;
+
+ if (!test_server_ssl)
+ return 0;
+
+ connection_input_halt(&conn->conn);
+
+ ssl_iostream_test_settings_server(&ssl_set);
+ ssl_set.verbose = debug;
+
+ if (server_ssl_ctx == NULL &&
+ ssl_iostream_context_init_server(&ssl_set, &server_ssl_ctx,
+ &error) < 0) {
+ i_error("SSL context initialization failed: %s", error);
+ return -1;
+ }
+
+ if (io_stream_create_ssl_server(server_ssl_ctx, &ssl_set,
+ &conn->conn.input, &conn->conn.output,
+ &conn->ssl_iostream, &error) < 0) {
+ i_error("SSL init failed: %s", error);
+ return -1;
+ }
+ if (ssl_iostream_handshake(conn->ssl_iostream) < 0) {
+ i_error("SSL handshake failed: %s",
+ ssl_iostream_get_last_error(conn->ssl_iostream));
+ return -1;
+ }
+
+ connection_input_resume(&conn->conn);
+ return 0;
+}
+
+static void
+server_connection_input(struct connection *_conn)
+{
+ struct server_connection *conn = (struct server_connection *)_conn;
+ const char *line;
+ int ret;
+
+ if (test_server_input != NULL) {
+ test_server_input(conn);
+ return;
+ }
+
+ for (;;) {
+ if (conn->state == SERVER_CONNECTION_STATE_FINISH) {
+ const unsigned char *data;
+ size_t size;
+ int ret;
+
+ if (conn->dot_input == NULL) {
+ conn->dot_input = i_stream_create_dot(
+ conn->conn.input, TRUE);
+ }
+ while ((ret = i_stream_read_more(conn->dot_input,
+ &data, &size)) > 0) {
+ if (test_server_input_data != NULL) {
+ if (test_server_input_data(
+ conn, data, size) < 0)
+ return;
+ }
+ i_stream_skip(conn->dot_input, size);
+ }
+
+ if (ret == 0)
+ return;
+ if (conn->dot_input->stream_errno != 0) {
+ if (debug) {
+ i_debug("Failed to read message payload: %s",
+ i_stream_get_error(conn->dot_input));
+ }
+ server_connection_deinit(&conn);
+ return;
+ }
+
+ o_stream_nsend_str(
+ conn->conn.output,
+ "250 2.0.0 Ok: queued as 73BDE342129\r\n");
+ conn->state = SERVER_CONNECTION_STATE_MAIL_FROM;
+ continue;
+ }
+
+ line = i_stream_read_next_line(conn->conn.input);
+ if (line == NULL) {
+ if (conn->conn.input->eof ||
+ conn->conn.input->stream_errno != 0)
+ server_connection_deinit(&conn);
+ return;
+ }
+
+ if (test_server_input_line != NULL) {
+ if ((ret = test_server_input_line(conn, line)) < 0)
+ return;
+ if (ret > 0)
+ continue;
+ }
+
+ switch (conn->state) {
+ case SERVER_CONNECTION_STATE_EHLO:
+ o_stream_nsend_str(conn->conn.output,
+ "250-testserver\r\n"
+ "250-PIPELINING\r\n"
+ "250-ENHANCEDSTATUSCODES\r\n"
+ "250 DSN\r\n");
+ conn->state = SERVER_CONNECTION_STATE_MAIL_FROM;
+ return;
+ case SERVER_CONNECTION_STATE_MAIL_FROM:
+ if (str_begins(line, "AUTH ")) {
+ o_stream_nsend_str(
+ conn->conn.output, "235 2.7.0 "
+ "Authentication successful\r\n");
+ continue;
+ }
+ if (str_begins(line, "EHLO ")) {
+ o_stream_nsend_str(conn->conn.output,
+ "250-testserver\r\n"
+ "250-PIPELINING\r\n"
+ "250-ENHANCEDSTATUSCODES\r\n"
+ "250-AUTH PLAIN\r\n"
+ "250 DSN\r\n");
+ continue;
+ }
+ o_stream_nsend_str(conn->conn.output,
+ "250 2.1.0 Ok\r\n");
+ conn->state = SERVER_CONNECTION_STATE_RCPT_TO;
+ continue;
+ case SERVER_CONNECTION_STATE_RCPT_TO:
+ o_stream_nsend_str(conn->conn.output,
+ "250 2.1.5 Ok\r\n");
+ conn->state = SERVER_CONNECTION_STATE_DATA;
+ continue;
+ case SERVER_CONNECTION_STATE_DATA:
+ o_stream_nsend_str(
+ conn->conn.output,
+ "354 End data with <CR><LF>.<CR><LF>\r\n");
+ conn->state = SERVER_CONNECTION_STATE_FINISH;
+ continue;
+ case SERVER_CONNECTION_STATE_FINISH:
+ break;
+ }
+ i_unreached();
+ }
+}
+
+static void server_connection_init(int fd)
+{
+ struct server_connection *conn;
+ pool_t pool;
+
+ net_set_nonblock(fd, TRUE);
+
+ pool = pool_alloconly_create("server connection", 256);
+ conn = p_new(pool, struct server_connection, 1);
+ conn->pool = pool;
+
+ connection_init_server(server_conn_list, &conn->conn,
+ "server connection", fd, fd);
+
+ if (server_connection_init_ssl(conn) < 0) {
+ server_connection_deinit(&conn);
+ return;
+ }
+
+ if (test_server_init != NULL) {
+ if (test_server_init(conn) != 0)
+ return;
+ }
+
+ if (test_server_input == NULL) {
+ o_stream_nsend_str(
+ conn->conn.output,
+ "220 testserver ESMTP Testfix (Debian/GNU)\r\n");
+ }
+}
+
+static void server_connection_deinit(struct server_connection **_conn)
+{
+ struct server_connection *conn = *_conn;
+
+ *_conn = NULL;
+
+ if (test_server_deinit != NULL)
+ test_server_deinit(conn);
+
+ i_stream_unref(&conn->dot_input);
+
+ ssl_iostream_destroy(&conn->ssl_iostream);
+ connection_deinit(&conn->conn);
+ pool_unref(&conn->pool);
+}
+
+static void server_connection_destroy(struct connection *_conn)
+{
+ struct server_connection *conn =
+ (struct server_connection *)_conn;
+
+ server_connection_deinit(&conn);
+}
+
+static void server_connection_accept(void *context ATTR_UNUSED)
+{
+ int fd;
+
+ /* accept new client */
+ fd = net_accept(fd_listen, NULL, NULL);
+ if (fd == -1)
+ return;
+ if (fd == -2)
+ i_fatal("test server: accept() failed: %m");
+
+ server_connection_init(fd);
+}
+
+/* */
+
+static struct connection_settings server_connection_set = {
+ .input_max_size = SIZE_MAX,
+ .output_max_size = SIZE_MAX,
+ .client = FALSE,
+};
+
+static const struct connection_vfuncs server_connection_vfuncs = {
+ .destroy = server_connection_destroy,
+ .input = server_connection_input,
+};
+
+static void test_server_run(unsigned int index)
+{
+ server_index = index;
+
+ /* open server socket */
+ io_listen = io_add(fd_listen, IO_READ, server_connection_accept, NULL);
+
+ server_conn_list = connection_list_init(&server_connection_set,
+ &server_connection_vfuncs);
+
+ io_loop_run(ioloop);
+
+ /* close server socket */
+ io_remove(&io_listen);
+
+ connection_list_deinit(&server_conn_list);
+
+ if (server_ssl_ctx != NULL)
+ ssl_iostream_context_unref(&server_ssl_ctx);
+}
+
+/*
+ * Tests
+ */
+
+struct test_server_data {
+ unsigned int index;
+ test_server_init_t server_test;
+};
+
+static int test_open_server_fd(in_port_t *bind_port)
+{
+ int fd = net_listen(&bind_ip, bind_port, 128);
+ if (debug)
+ i_debug("server listening on %u", *bind_port);
+ if (fd == -1) {
+ i_fatal("listen(%s:%u) failed: %m",
+ net_ip2addr(&bind_ip), *bind_port);
+ }
+ return fd;
+}
+
+static int test_run_server(struct test_server_data *data)
+{
+ i_set_failure_prefix("SERVER[%u]: ", data->index + 1);
+
+ if (debug)
+ i_debug("PID=%s", my_pid);
+
+ server_ssl_ctx = NULL;
+
+ ioloop = io_loop_create();
+ data->server_test(data->index);
+ io_loop_destroy(&ioloop);
+
+ if (debug)
+ i_debug("Terminated");
+
+ i_close_fd(&fd_listen);
+ i_free(bind_ports);
+ main_deinit();
+ return 0;
+}
+
+static int test_run_dns(test_dns_init_t dns_test)
+{
+ i_set_failure_prefix("DNS: ");
+
+ if (debug)
+ i_debug("PID=%s", my_pid);
+
+ ioloop = io_loop_create();
+ dns_test();
+ io_loop_destroy(&ioloop);
+
+ if (debug)
+ i_debug("Terminated");
+
+ i_close_fd(&fd_listen);
+ i_free(bind_ports);
+ main_deinit();
+ return 0;
+}
+
+static void
+test_run_client(const struct smtp_client_settings *client_set,
+ test_client_init_t client_test)
+{
+ i_set_failure_prefix("CLIENT: ");
+
+ if (debug)
+ i_debug("PID=%s", my_pid);
+
+ i_sleep_msecs(100); /* wait a little for server setup */
+
+ ioloop = io_loop_create();
+ test_client_run(client_test, client_set);
+ io_loop_destroy(&ioloop);
+
+ if (debug)
+ i_debug("Terminated");
+}
+
+static void
+test_run_client_server(const struct smtp_client_settings *client_set,
+ test_client_init_t client_test,
+ test_server_init_t server_test,
+ unsigned int server_tests_count,
+ test_dns_init_t dns_test)
+{
+ unsigned int i;
+
+ if (server_tests_count > 0) {
+ int fds[server_tests_count];
+
+ bind_ports = i_new(in_port_t, server_tests_count);
+ for (i = 0; i < server_tests_count; i++)
+ fds[i] = test_open_server_fd(&bind_ports[i]);
+
+ for (i = 0; i < server_tests_count; i++) {
+ struct test_server_data data;
+
+ i_zero(&data);
+ data.index = i;
+ data.server_test = server_test;
+
+ /* Fork server */
+ fd_listen = fds[i];
+ test_subprocess_fork(test_run_server, &data, FALSE);
+ i_close_fd(&fd_listen);
+ }
+ }
+
+ if (dns_test != NULL) {
+ int fd;
+
+ i_unlink_if_exists("./dns-test");
+ fd = net_listen_unix("./dns-test", 128);
+ if (fd == -1) {
+ i_fatal("listen(./dns-test) failed: %m");
+ }
+
+ /* Fork DNS service */
+ fd_listen = fd;
+ test_subprocess_fork(test_run_dns, dns_test, FALSE);
+ i_close_fd(&fd_listen);
+ }
+
+ /* Run client */
+ test_run_client(client_set, client_test);
+
+ i_unset_failure_prefix();
+ test_subprocess_kill_all(SERVER_KILL_TIMEOUT_SECS);
+ i_free(bind_ports);
+
+ i_unlink_if_exists("./dns-test");
+}
+
+/*
+ * Main
+ */
+
+static void main_init(void)
+{
+#ifdef HAVE_OPENSSL
+ ssl_iostream_openssl_init();
+#endif
+}
+
+static void main_deinit(void)
+{
+ ssl_iostream_context_cache_free();
+#ifdef HAVE_OPENSSL
+ ssl_iostream_openssl_deinit();
+#endif
+}
+
+int main(int argc, char *argv[])
+{
+ int c;
+ int ret;
+
+ lib_init();
+ main_init();
+
+ while ((c = getopt(argc, argv, "D")) > 0) {
+ switch (c) {
+ case 'D':
+ debug = TRUE;
+ break;
+ default:
+ i_fatal("Usage: %s [-D]", argv[0]);
+ }
+ }
+
+ test_subprocesses_init(debug);
+
+ /* listen on localhost */
+ i_zero(&bind_ip);
+ bind_ip.family = AF_INET;
+ bind_ip.u.ip4.s_addr = htonl(INADDR_LOOPBACK);
+
+ ret = test_run(test_functions);
+
+ test_subprocesses_deinit();
+ main_deinit();
+ lib_deinit();
+
+ return ret;
+}
diff --git a/src/lib-smtp/test-smtp-command-parser.c b/src/lib-smtp/test-smtp-command-parser.c
new file mode 100644
index 0000000..9c53c18
--- /dev/null
+++ b/src/lib-smtp/test-smtp-command-parser.c
@@ -0,0 +1,643 @@
+/* Copyright (c) 2017-2018 Dovecot authors, see the included COPYING file */
+
+#include "test-lib.h"
+#include "array.h"
+#include "buffer.h"
+#include "str.h"
+#include "str-sanitize.h"
+#include "istream.h"
+#include "ostream.h"
+#include "test-common.h"
+#include "smtp-command-parser.h"
+
+#include <time.h>
+
+/*
+ * Valid command tests
+ */
+
+struct smtp_command_parse_valid_test {
+ const char *command;
+
+ struct smtp_command_limits limits;
+
+ const char *cmd_name;
+ const char *cmd_params;
+};
+
+static const struct smtp_command_parse_valid_test
+valid_command_parse_tests[] = {
+ {
+ .command = "RSET\r\n",
+ .cmd_name = "RSET",
+ .cmd_params = "",
+ },
+ {
+ .command = "RSET \r\n",
+ .cmd_name = "RSET",
+ .cmd_params = "",
+ },
+ {
+ .command = "EHLO example.com\r\n",
+ .cmd_name = "EHLO",
+ .cmd_params = "example.com",
+ },
+ {
+ .command = "EHLO example.com \r\n",
+ .cmd_name = "EHLO",
+ .cmd_params = "example.com",
+ },
+ {
+ .command = "MAIL FROM:<sender@example.com> ENVID=frop\r\n",
+ .cmd_name = "MAIL",
+ .cmd_params = "FROM:<sender@example.com> ENVID=frop",
+ },
+ {
+ .command = "VRFY \"Sherlock Holmes\"\r\n",
+ .cmd_name = "VRFY",
+ .cmd_params = "\"Sherlock Holmes\"",
+ },
+ {
+ .command = "RCPT TO:<recipient@example.com> NOTIFY=NEVER\r\n",
+ .limits = { .max_parameters_size = 39 },
+ .cmd_name = "RCPT",
+ .cmd_params = "TO:<recipient@example.com> NOTIFY=NEVER",
+ },
+ {
+ .command = "MAIL FROM:<f\xc3\xb6\xc3\xa4@\xc3\xb6\xc3\xa4>\r\n",
+ .cmd_name = "MAIL",
+ .cmd_params = "FROM:<f\xc3\xb6\xc3\xa4@\xc3\xb6\xc3\xa4>",
+ },
+};
+
+unsigned int valid_command_parse_test_count =
+ N_ELEMENTS(valid_command_parse_tests);
+
+static void
+test_smtp_command_parse_valid_check(
+ const struct smtp_command_parse_valid_test *test,
+ const char *cmd_name, const char *cmd_params)
+{
+ test_out(t_strdup_printf("command name = `%s'", test->cmd_name),
+ null_strcmp(cmd_name, test->cmd_name) == 0);
+ test_out(t_strdup_printf("command params = `%s'",
+ str_sanitize(test->cmd_params, 24)),
+ null_strcmp(cmd_params, test->cmd_params) == 0);
+}
+
+static void test_smtp_command_parse_valid(void)
+{
+ unsigned int i;
+
+ for (i = 0; i < valid_command_parse_test_count; i++) T_BEGIN {
+ struct istream *input;
+ const struct smtp_command_parse_valid_test *test;
+ struct smtp_command_parser *parser;
+ const char *command_text, *cmd_name, *cmd_params, *error;
+ enum smtp_command_parse_error error_code;
+ unsigned int pos, command_text_len;
+ int ret;
+
+ test_begin(t_strdup_printf("smtp command valid [%d]", i));
+
+ cmd_name = cmd_params = error = NULL;
+
+ test = &valid_command_parse_tests[i];
+ command_text = test->command;
+ command_text_len = strlen(command_text);
+
+ /* Fully buffered input */
+ input = i_stream_create_from_data(command_text,
+ command_text_len);
+ parser = smtp_command_parser_init(input, &test->limits);
+
+ while ((ret = smtp_command_parse_next(
+ parser, &cmd_name, &cmd_params,
+ &error_code, &error)) > 0);
+
+ test_out_reason("parse success [buffer]", ret == -2,
+ (ret == -2 ? NULL : error));
+ if (ret == 0) {
+ /* Verify last command only */
+ test_smtp_command_parse_valid_check(
+ test, cmd_name, cmd_params);
+ }
+
+ smtp_command_parser_deinit(&parser);
+ i_stream_unref(&input);
+
+ error = NULL;
+ error_code = SMTP_COMMAND_PARSE_ERROR_NONE;
+ ret = 0;
+
+ /* Trickle stream */
+ input = test_istream_create_data(command_text,
+ command_text_len);
+ parser = smtp_command_parser_init(input, &test->limits);
+
+ for (pos = 0; pos <= command_text_len && ret == 0; pos++) {
+ test_istream_set_size(input, pos);
+ ret = smtp_command_parse_next(
+ parser, &cmd_name, &cmd_params,
+ &error_code, &error);
+ }
+ test_istream_set_size(input, command_text_len);
+ if (ret >= 0) {
+ while ((ret = smtp_command_parse_next(
+ parser, &cmd_name, &cmd_params,
+ &error_code, &error)) > 0);
+ }
+
+ test_out_reason("parse success [stream]", ret == -2,
+ (ret == -2 ? NULL : error));
+ if (ret == 0) {
+ /* Verify last command only */
+ test_smtp_command_parse_valid_check(
+ test, cmd_name, cmd_params);
+ }
+
+ smtp_command_parser_deinit(&parser);
+ i_stream_unref(&input);
+
+ test_end();
+ } T_END;
+}
+
+/*
+ * Invalid command tests
+ */
+
+struct smtp_command_parse_invalid_test {
+ const char *command;
+
+ struct smtp_command_limits limits;
+
+ enum smtp_command_parse_error error_code;
+};
+
+static const struct smtp_command_parse_invalid_test
+ invalid_command_parse_tests[] = {
+ {
+ .command = "B52\r\n",
+ .error_code = SMTP_COMMAND_PARSE_ERROR_BAD_COMMAND,
+ },
+ {
+ .command = "BELL\x08\r\n",
+ .error_code = SMTP_COMMAND_PARSE_ERROR_BAD_COMMAND,
+ },
+ {
+ .command = "EHLO example.com\r\n",
+ .error_code = SMTP_COMMAND_PARSE_ERROR_BAD_COMMAND,
+ },
+ {
+ .command = "NOOP \"\x01\x02\x03\"\r\n",
+ .error_code = SMTP_COMMAND_PARSE_ERROR_BAD_COMMAND,
+ },
+ {
+ .command = "RSET\rQUIT\r\n",
+ .error_code = SMTP_COMMAND_PARSE_ERROR_BAD_COMMAND,
+ },
+ {
+ .command = "INSANELYREDICULOUSLYLONGCOMMANDNAME\r\n",
+ .error_code = SMTP_COMMAND_PARSE_ERROR_BAD_COMMAND,
+ },
+ {
+ .command = "RCPT TO:<recipient@example.com> NOTIFY=NEVER\r\n",
+ .limits = { .max_parameters_size = 38 },
+ .error_code = SMTP_COMMAND_PARSE_ERROR_LINE_TOO_LONG,
+ },
+ {
+ .command = "MAIL FROM:<f\xc3\xb6\xc3\xa4@\xc3\xb6\xc3>\r\n",
+ .error_code = SMTP_COMMAND_PARSE_ERROR_BAD_COMMAND,
+ },
+ {
+ .command = "MAIL FROM:f\xc3\xb6\xc3\xa4@\xc3\xb6\xc3\r\n",
+ .error_code = SMTP_COMMAND_PARSE_ERROR_BAD_COMMAND,
+ },
+ {
+ .command = "MAIL FROM:f\xc3\xb6\xc3\xa4@\xc3\xb6\xc3",
+ .error_code = SMTP_COMMAND_PARSE_ERROR_BROKEN_COMMAND,
+ },
+ {
+ .command = "FROP \xF1",
+ .error_code = SMTP_COMMAND_PARSE_ERROR_BROKEN_COMMAND,
+ },
+ {
+ .command = "FROP \xF1\x80",
+ .error_code = SMTP_COMMAND_PARSE_ERROR_BROKEN_COMMAND,
+ },
+ {
+ .command = "FROP \xF1\x80\x80",
+ .error_code = SMTP_COMMAND_PARSE_ERROR_BROKEN_COMMAND,
+ },
+ {
+ .command = "FROP \xF1\x80\x80\x80",
+ .error_code = SMTP_COMMAND_PARSE_ERROR_BROKEN_COMMAND,
+ },
+};
+
+unsigned int invalid_command_parse_test_count =
+ N_ELEMENTS(invalid_command_parse_tests);
+
+static void test_smtp_command_parse_invalid(void)
+{
+ unsigned int i;
+
+ for (i = 0; i < invalid_command_parse_test_count; i++) T_BEGIN {
+ const struct smtp_command_parse_invalid_test *test;
+ struct istream *input;
+ struct smtp_command_parser *parser;
+ const char *command_text, *cmd_name, *cmd_params, *error;
+ enum smtp_command_parse_error error_code;
+ unsigned int pos, command_text_len;
+ int ret;
+
+ test_begin(t_strdup_printf("smtp command invalid [%d]", i));
+
+ test = &invalid_command_parse_tests[i];
+ command_text = test->command;
+ command_text_len = strlen(command_text);
+
+ /* Fully buffered input */
+ input = i_stream_create_from_data(command_text,
+ command_text_len);
+ parser = smtp_command_parser_init(input, &test->limits);
+
+ while ((ret = smtp_command_parse_next(
+ parser, &cmd_name, &cmd_params,
+ &error_code, &error)) > 0);
+
+ test_out_reason(t_strdup_printf("parse(\"%s\") [buffer]",
+ str_sanitize(command_text, 28)),
+ ret == -1, error);
+ test_out_quiet("error code", error_code == test->error_code);
+
+ smtp_command_parser_deinit(&parser);
+ i_stream_unref(&input);
+
+ error = NULL;
+ error_code = SMTP_COMMAND_PARSE_ERROR_NONE;
+ ret = 0;
+
+ /* Trickle stream */
+ input = test_istream_create_data(command_text,
+ command_text_len);
+ parser = smtp_command_parser_init(input, &test->limits);
+
+ for (pos = 0; pos <= command_text_len && ret == 0; pos++) {
+ test_istream_set_size(input, pos);
+ ret = smtp_command_parse_next(
+ parser, &cmd_name, &cmd_params,
+ &error_code, &error);
+ }
+ test_istream_set_size(input, command_text_len);
+ if (ret >= 0) {
+ while ((ret = smtp_command_parse_next(
+ parser, &cmd_name, &cmd_params,
+ &error_code, &error)) > 0);
+ }
+
+ test_out_reason(t_strdup_printf("parse(\"%s\") [stream]",
+ str_sanitize(command_text, 28)),
+ ret == -1, error);
+ test_out_quiet("error code", error_code == test->error_code);
+
+ smtp_command_parser_deinit(&parser);
+ i_stream_unref(&input);
+
+ test_end();
+ } T_END;
+}
+
+/*
+ * Valid auth response tests
+ */
+
+struct smtp_auth_response_parse_valid_test {
+ const char *auth_response;
+
+ struct smtp_command_limits limits;
+
+ const char *line;
+};
+
+static const struct smtp_auth_response_parse_valid_test
+valid_auth_response_parse_tests[] = {
+ {
+ .auth_response = "U3R1cGlkIEJhc2U2NCB0ZXN0\r\n",
+ .line = "U3R1cGlkIEJhc2U2NCB0ZXN0",
+ },
+ {
+ .auth_response = "U3R1cGlkIEJhc2U2NCB0ZXN0 \r\n",
+ .line = "U3R1cGlkIEJhc2U2NCB0ZXN0",
+ },
+ {
+ .auth_response =
+ "U3R1cGlkIHZlcnkgdmVyeSB2ZXJ5IHZlcnkgdmVyeS"
+ "B2ZXJ5IHZlcnkgdmVyeSBsb25nIEJhc2U2NCB0ZXN0\r\n",
+ .limits = { .max_auth_size = 84 },
+ .line = "U3R1cGlkIHZlcnkgdmVyeSB2ZXJ5IHZlcnkgdmVyeS"
+ "B2ZXJ5IHZlcnkgdmVyeSBsb25nIEJhc2U2NCB0ZXN0",
+ },
+ {
+ .auth_response =
+ "dXNlcj10ZXN0dXNlcjEBYXV0aD1CZWFyZXIgZXlKaG"
+ "JHY2lPaUpTVXpJMU5pSXNJblI1Y0NJZ09pQWlTbGRV"
+ "SWl3aWEybGtJaUE2SUNKdVRIRlVlRnBXWVhKSlgwWn"
+ "dSa0Z3Umt3MloyUnhiak4xV1VSS2R6WnNWVjlMYVZo"
+ "a2JWazJialpSSW4wLmV5SmxlSEFpT2pFMk16UTJNem"
+ "MyTlRFc0ltbGhkQ0k2TVRZek5EWXpOek0xTVN3aWFu"
+ "UnBJam9pT1RFM1lUYzFaalF0WTJZME9DMDBOVEEyTF"
+ "RnNVpXSXRNRE13WldaaU5tSTVOMlZrSWl3aWFYTnpJ"
+ "am9pYUhSMGNEb3ZMekU1TWk0eE5qZ3VNUzR5TVRveE"
+ "9EQTRNQzloZFhSb0wzSmxZV3h0Y3k5eVpXeDBaWE4w"
+ "SWl3aVlYVmtJam9pWVdOamIzVnVkQ0lzSW5OMVlpST"
+ "ZJamhsWVRRME1UWTNMVGN6TTJVdE5EVTBZeTFpT0dJ"
+ "MUxXTmpabVl3WkRnek1URTVaQ0lzSW5SNWNDSTZJa0"
+ "psWVhKbGNpSXNJbUY2Y0NJNkltUnZkbVZqYjNRaUxD"
+ "SnpaWE56YVc5dVgzTjBZWFJsSWpvaU1tTTNPVEUzWl"
+ "dJdE16QTFOUzAwTkRZeExXSXdZell0WTJVeFlUbGlN"
+ "VEZoTWpReklpd2lZV055SWpvaU1TSXNJbkpsWVd4dF"
+ "gyRmpZMlZ6Y3lJNmV5SnliMnhsY3lJNld5SnZabVpz"
+ "YVc1bFgyRmpZMlZ6Y3lJc0luVnRZVjloZFhSb2IzSn"
+ "BlbUYwYVc5dUlsMTlMQ0p5WlhOdmRYSmpaVjloWTJO"
+ "bGMzTWlPbnNpWVdOamIzVnVkQ0k2ZXlKeWIyeGxjeU"
+ "k2V3lKdFlXNWhaMlV0WVdOamIzVnVkQ0lzSW0xaGJt"
+ "Rm5aUzFoWTJOdmRXNTBMV3hwYm10eklpd2lkbWxsZH"
+ "kxd2NtOW1hV3hsSWwxOWZTd2ljMk52Y0dVaU9pSndj"
+ "bTltYVd4bElHVnRZV2xzSWl3aVpXMWhhV3hmZG1WeW"
+ "FXWnBaV1FpT21aaGJITmxMQ0p1WVcxbElqb2lkR1Z6"
+ "ZEhWelpYSXhJRUYxZEc5SFpXNWxjbUYwWldRaUxDSn"
+ "djbVZtWlhKeVpXUmZkWE5sY201aGJXVWlPaUowWlhO"
+ "MGRYTmxjakVpTENKbmFYWmxibDl1WVcxbElqb2lkR1"
+ "Z6ZEhWelpYSXhJaXdpWm1GdGFXeDVYMjVoYldVaU9p"
+ "SkJkWFJ2UjJWdVpYSmhkR1ZrSWl3aVpXMWhhV3dpT2"
+ "lKMFpYTjBkWE5sY2pGQWJYbGtiMjFoYVc0dWIzZ2lm"
+ "US5ta2JGSURpT0FhbENCcVMwODRhVHJURjBIdDk1c1"
+ "Z4cGlSbTFqZnhJd0JiN1hMM2gzWUJkdXVrVXlZdDJq"
+ "X1pqUFlhMDhDcVVYNWFrLVBOSjdSVWRTUXNmUlgwM1"
+ "ZicXA4MHFZZjNGYzJpcDR0YmhHLXFEV0R6NzdhZDhW"
+ "cEFNei16YWlSamZCclZ2R3hBT3ZsZnFDVWhaZTJDR3"
+ "ZqWjZ1Q3RKTlFaS0dyazZHOXoxX2pqekZkTjBXWjUx"
+ "bEZsUS1JdE5LREpoTjNIekJ5SW93M19qQU9kWEI0R0"
+ "w4R3JHM1hqU09rSFVRam5GTEQwQUF1QXY4SkxmTXY1"
+ "NGc1a2tKaklxRFgxZlgyWVo0Y2JQOWV3TUp6UV84ZW"
+ "dLeW5TVV9XSk8xRU9Qa1NVZjlMX19RX3FwY0dNbzFt"
+ "TkxuTURKUlU2dmZFY3JrM2k0cVNzMXRPdHdLaHcBAQ"
+ "==\r\n",
+ .line =
+ "dXNlcj10ZXN0dXNlcjEBYXV0aD1CZWFyZXIgZXlKaG"
+ "JHY2lPaUpTVXpJMU5pSXNJblI1Y0NJZ09pQWlTbGRV"
+ "SWl3aWEybGtJaUE2SUNKdVRIRlVlRnBXWVhKSlgwWn"
+ "dSa0Z3Umt3MloyUnhiak4xV1VSS2R6WnNWVjlMYVZo"
+ "a2JWazJialpSSW4wLmV5SmxlSEFpT2pFMk16UTJNem"
+ "MyTlRFc0ltbGhkQ0k2TVRZek5EWXpOek0xTVN3aWFu"
+ "UnBJam9pT1RFM1lUYzFaalF0WTJZME9DMDBOVEEyTF"
+ "RnNVpXSXRNRE13WldaaU5tSTVOMlZrSWl3aWFYTnpJ"
+ "am9pYUhSMGNEb3ZMekU1TWk0eE5qZ3VNUzR5TVRveE"
+ "9EQTRNQzloZFhSb0wzSmxZV3h0Y3k5eVpXeDBaWE4w"
+ "SWl3aVlYVmtJam9pWVdOamIzVnVkQ0lzSW5OMVlpST"
+ "ZJamhsWVRRME1UWTNMVGN6TTJVdE5EVTBZeTFpT0dJ"
+ "MUxXTmpabVl3WkRnek1URTVaQ0lzSW5SNWNDSTZJa0"
+ "psWVhKbGNpSXNJbUY2Y0NJNkltUnZkbVZqYjNRaUxD"
+ "SnpaWE56YVc5dVgzTjBZWFJsSWpvaU1tTTNPVEUzWl"
+ "dJdE16QTFOUzAwTkRZeExXSXdZell0WTJVeFlUbGlN"
+ "VEZoTWpReklpd2lZV055SWpvaU1TSXNJbkpsWVd4dF"
+ "gyRmpZMlZ6Y3lJNmV5SnliMnhsY3lJNld5SnZabVpz"
+ "YVc1bFgyRmpZMlZ6Y3lJc0luVnRZVjloZFhSb2IzSn"
+ "BlbUYwYVc5dUlsMTlMQ0p5WlhOdmRYSmpaVjloWTJO"
+ "bGMzTWlPbnNpWVdOamIzVnVkQ0k2ZXlKeWIyeGxjeU"
+ "k2V3lKdFlXNWhaMlV0WVdOamIzVnVkQ0lzSW0xaGJt"
+ "Rm5aUzFoWTJOdmRXNTBMV3hwYm10eklpd2lkbWxsZH"
+ "kxd2NtOW1hV3hsSWwxOWZTd2ljMk52Y0dVaU9pSndj"
+ "bTltYVd4bElHVnRZV2xzSWl3aVpXMWhhV3hmZG1WeW"
+ "FXWnBaV1FpT21aaGJITmxMQ0p1WVcxbElqb2lkR1Z6"
+ "ZEhWelpYSXhJRUYxZEc5SFpXNWxjbUYwWldRaUxDSn"
+ "djbVZtWlhKeVpXUmZkWE5sY201aGJXVWlPaUowWlhO"
+ "MGRYTmxjakVpTENKbmFYWmxibDl1WVcxbElqb2lkR1"
+ "Z6ZEhWelpYSXhJaXdpWm1GdGFXeDVYMjVoYldVaU9p"
+ "SkJkWFJ2UjJWdVpYSmhkR1ZrSWl3aVpXMWhhV3dpT2"
+ "lKMFpYTjBkWE5sY2pGQWJYbGtiMjFoYVc0dWIzZ2lm"
+ "US5ta2JGSURpT0FhbENCcVMwODRhVHJURjBIdDk1c1"
+ "Z4cGlSbTFqZnhJd0JiN1hMM2gzWUJkdXVrVXlZdDJq"
+ "X1pqUFlhMDhDcVVYNWFrLVBOSjdSVWRTUXNmUlgwM1"
+ "ZicXA4MHFZZjNGYzJpcDR0YmhHLXFEV0R6NzdhZDhW"
+ "cEFNei16YWlSamZCclZ2R3hBT3ZsZnFDVWhaZTJDR3"
+ "ZqWjZ1Q3RKTlFaS0dyazZHOXoxX2pqekZkTjBXWjUx"
+ "bEZsUS1JdE5LREpoTjNIekJ5SW93M19qQU9kWEI0R0"
+ "w4R3JHM1hqU09rSFVRam5GTEQwQUF1QXY4SkxmTXY1"
+ "NGc1a2tKaklxRFgxZlgyWVo0Y2JQOWV3TUp6UV84ZW"
+ "dLeW5TVV9XSk8xRU9Qa1NVZjlMX19RX3FwY0dNbzFt"
+ "TkxuTURKUlU2dmZFY3JrM2k0cVNzMXRPdHdLaHcBAQ",
+ },
+};
+
+unsigned int valid_auth_response_parse_test_count =
+ N_ELEMENTS(valid_auth_response_parse_tests);
+
+static void
+test_smtp_auth_response_parse_valid_check(
+ const struct smtp_auth_response_parse_valid_test *test,
+ const char *line)
+{
+ test_out(t_strdup_printf("line = `%s'",
+ str_sanitize(test->line, 24)),
+ null_strcmp(line, test->line) == 0);
+}
+
+static void test_smtp_auth_response_parse_valid(void)
+{
+ unsigned int i;
+
+ for (i = 0; i < valid_auth_response_parse_test_count; i++) T_BEGIN {
+ struct istream *input;
+ const struct smtp_auth_response_parse_valid_test *test;
+ struct smtp_command_parser *parser;
+ const char *response_text, *line, *error;
+ enum smtp_command_parse_error error_code;
+ unsigned int pos, response_text_len;
+ int ret;
+
+ test_begin(t_strdup_printf("smtp auth_response valid [%d]", i));
+
+ line = error = NULL;
+
+ test = &valid_auth_response_parse_tests[i];
+ response_text = test->auth_response;
+ response_text_len = strlen(response_text);
+
+ /* Fully buffered input */
+ input = i_stream_create_from_data(response_text,
+ response_text_len);
+ parser = smtp_command_parser_init(input, &test->limits);
+
+ while ((ret = smtp_command_parse_auth_response(
+ parser, &line, &error_code, &error)) > 0);
+
+ test_out_reason("parse success [buffer]", ret == -2,
+ (ret == -2 ? NULL : error));
+ if (ret == 0) {
+ /* Verify last reponse only */
+ test_smtp_auth_response_parse_valid_check(test, line);
+ }
+
+ smtp_command_parser_deinit(&parser);
+ i_stream_unref(&input);
+
+ error = NULL;
+ error_code = SMTP_COMMAND_PARSE_ERROR_NONE;
+ ret = 0;
+
+ /* Trickle stream */
+ input = test_istream_create_data(response_text,
+ response_text_len);
+ parser = smtp_command_parser_init(input, &test->limits);
+
+ for (pos = 0; pos <= response_text_len && ret == 0; pos++) {
+ test_istream_set_size(input, pos);
+ ret = smtp_command_parse_auth_response(
+ parser, &line, &error_code, &error);
+ }
+ test_istream_set_size(input, response_text_len);
+ if (ret >= 0) {
+ while ((ret = smtp_command_parse_auth_response(
+ parser, &line, &error_code, &error)) > 0);
+ }
+
+ test_out_reason("parse success [stream]", ret == -2,
+ (ret == -2 ? NULL : error));
+ if (ret == 0) {
+ /* Verify last reponse only */
+ test_smtp_auth_response_parse_valid_check(test, line);
+ }
+
+ smtp_command_parser_deinit(&parser);
+ i_stream_unref(&input);
+
+ test_end();
+ } T_END;
+}
+
+/*
+ * Invalid auth response tests
+ */
+
+struct smtp_auth_response_parse_invalid_test {
+ const char *auth_response;
+
+ struct smtp_command_limits limits;
+
+ enum smtp_command_parse_error error_code;
+};
+
+static const struct smtp_auth_response_parse_invalid_test
+ invalid_auth_response_parse_tests[] = {
+ {
+ .auth_response = "\x01\x02\x03\r\n",
+ .error_code = SMTP_COMMAND_PARSE_ERROR_BAD_COMMAND,
+ },
+ {
+ .auth_response = "U3R1cGlkIEJhc\r2U2NCB0ZXN0\r\n",
+ .error_code = SMTP_COMMAND_PARSE_ERROR_BAD_COMMAND,
+ },
+ {
+ .auth_response =
+ "U3R1cGlkIHZlcnkgdmVyeSB2ZXJ5IHZlcnkgdmVyeS"
+ "B2ZXJ5IHZlcnkgdmVyeSBsb25nIEJhc2U2NCB0ZXN0\r\n",
+ .limits = { .max_auth_size = 83 },
+ .error_code = SMTP_COMMAND_PARSE_ERROR_LINE_TOO_LONG,
+ },
+ {
+ .auth_response = "\xc3\xb6\xc3\xa4\xc3\xb6\xc3\xa4\r\n",
+ .error_code = SMTP_COMMAND_PARSE_ERROR_BAD_COMMAND,
+ },
+};
+
+unsigned int invalid_auth_response_parse_test_count =
+ N_ELEMENTS(invalid_auth_response_parse_tests);
+
+static void test_smtp_auth_response_parse_invalid(void)
+{
+ unsigned int i;
+
+ for (i = 0; i < invalid_auth_response_parse_test_count; i++) T_BEGIN {
+ const struct smtp_auth_response_parse_invalid_test *test;
+ struct istream *input;
+ struct smtp_command_parser *parser;
+ const char *response_text, *line, *error;
+ enum smtp_command_parse_error error_code;
+ unsigned int pos, response_text_len;
+ int ret;
+
+ test_begin(
+ t_strdup_printf("smtp auth response invalid [%d]", i));
+
+ test = &invalid_auth_response_parse_tests[i];
+ response_text = test->auth_response;
+ response_text_len = strlen(response_text);
+
+ /* Fully buffered input */
+ input = i_stream_create_from_data(response_text,
+ strlen(response_text));
+ parser = smtp_command_parser_init(input, &test->limits);
+
+ while ((ret = smtp_command_parse_auth_response(
+ parser, &line, &error_code, &error)) > 0);
+
+ test_out_reason(t_strdup_printf("parse(\"%s\") [buffer]",
+ str_sanitize(response_text,
+ 28)),
+ ret == -1, error);
+ test_out_quiet("error code", error_code == test->error_code);
+
+ smtp_command_parser_deinit(&parser);
+ i_stream_unref(&input);
+
+ error = NULL;
+ error_code = SMTP_COMMAND_PARSE_ERROR_NONE;
+ ret = 0;
+
+ /* Trickle stream */
+ input = test_istream_create_data(response_text,
+ response_text_len);
+ parser = smtp_command_parser_init(input, &test->limits);
+
+ for (pos = 0; pos <= response_text_len && ret == 0; pos++) {
+ test_istream_set_size(input, pos);
+ ret = smtp_command_parse_auth_response(
+ parser, &line, &error_code, &error);
+ }
+ test_istream_set_size(input, response_text_len);
+ if (ret >= 0) {
+ while ((ret = smtp_command_parse_auth_response(
+ parser, &line, &error_code, &error)) > 0);
+ }
+
+ test_out_reason(t_strdup_printf("parse(\"%s\") [stream]",
+ str_sanitize(response_text,
+ 28)),
+ ret == -1, error);
+ test_out_quiet("error code", error_code == test->error_code);
+
+ smtp_command_parser_deinit(&parser);
+ i_stream_unref(&input);
+
+ test_end();
+ } T_END;
+}
+
+/*
+ * Tests
+ */
+
+int main(void)
+{
+ static void (*test_functions[])(void) = {
+ test_smtp_command_parse_valid,
+ test_smtp_command_parse_invalid,
+ test_smtp_auth_response_parse_valid,
+ test_smtp_auth_response_parse_invalid,
+ NULL
+ };
+ return test_run(test_functions);
+}
diff --git a/src/lib-smtp/test-smtp-params.c b/src/lib-smtp/test-smtp-params.c
new file mode 100644
index 0000000..2847441
--- /dev/null
+++ b/src/lib-smtp/test-smtp-params.c
@@ -0,0 +1,894 @@
+/* Copyright (c) 2013-2018 Dovecot authors, see the included COPYING file */
+
+#include "test-lib.h"
+#include "str.h"
+#include "array.h"
+#include "test-common.h"
+#include "smtp-common.h"
+#include "smtp-address.h"
+#include "smtp-params.h"
+
+static const char *test_extensions[] = { "FROP", "FRUP", NULL };
+
+static struct smtp_address test_address1 =
+ { .localpart = NULL, .domain = NULL };
+static struct smtp_address test_address2 =
+ { .localpart = "user+detail", .domain = NULL };
+static struct smtp_address test_address3 =
+ { .localpart = "e=mc2", .domain = "example.com" };
+
+static struct smtp_param test_params1[] = {
+ { .keyword = "FROP", .value = "friep" }
+};
+static struct smtp_param test_params2[] = {
+ { .keyword = "FROP", .value = "friep" },
+ { .keyword = "FRUP", .value = "frml" }
+};
+
+static struct buffer test_params_buffer1 = {
+ .data = (void*)&test_params1,
+ .used = sizeof(test_params1)
+};
+static struct buffer test_params_buffer2 = {
+ .data = (void*)&test_params2,
+ .used = sizeof(test_params2)
+};
+
+/* Valid mail params tests */
+
+struct valid_mail_params_parse_test {
+ const char *input, *output;
+
+ enum smtp_capability caps;
+ const char *const *extensions;
+ const char *const *body_extensions;
+
+ struct smtp_params_mail params;
+};
+
+static const struct valid_mail_params_parse_test
+valid_mail_params_parse_tests[] = {
+ /* AUTH */
+ {
+ .input = "AUTH=<>",
+ .caps = SMTP_CAPABILITY_AUTH,
+ .params = {
+ .auth = &test_address1
+ }
+ },
+ {
+ .input = "AUTH=user+2Bdetail",
+ .caps = SMTP_CAPABILITY_AUTH,
+ .params = {
+ .auth = &test_address2
+ }
+ },
+ {
+ .input = "AUTH=e+3Dmc2@example.com",
+ .caps = SMTP_CAPABILITY_AUTH,
+ .params = {
+ .auth = &test_address3
+ }
+ },
+ /* BODY */
+ {
+ .input = "",
+ .caps = SMTP_CAPABILITY_8BITMIME,
+ .params = {
+ .body = {
+ .type = SMTP_PARAM_MAIL_BODY_TYPE_UNSPECIFIED,
+ }
+ }
+ },
+ {
+ .input = "BODY=7BIT",
+ .caps = SMTP_CAPABILITY_8BITMIME,
+ .params = {
+ .body = {
+ .type = SMTP_PARAM_MAIL_BODY_TYPE_7BIT,
+ }
+ }
+ },
+ {
+ .input = "BODY=8BITMIME",
+ .caps = SMTP_CAPABILITY_8BITMIME,
+ .params = {
+ .body = {
+ .type = SMTP_PARAM_MAIL_BODY_TYPE_8BITMIME,
+ }
+ }
+ },
+ {
+ .input = "BODY=BINARYMIME",
+ .caps = SMTP_CAPABILITY_8BITMIME |
+ SMTP_CAPABILITY_BINARYMIME |
+ SMTP_CAPABILITY_CHUNKING,
+ .params = {
+ .body = {
+ .type = SMTP_PARAM_MAIL_BODY_TYPE_BINARYMIME,
+ }
+ }
+ },
+ {
+ .input = "BODY=FROP",
+ .caps = SMTP_CAPABILITY_8BITMIME |
+ SMTP_CAPABILITY_BINARYMIME |
+ SMTP_CAPABILITY_CHUNKING,
+ .body_extensions = test_extensions,
+ .params = {
+ .body = {
+ .type = SMTP_PARAM_MAIL_BODY_TYPE_EXTENSION,
+ .ext = "FROP"
+ }
+ }
+ },
+ /* ENVID */
+ {
+ .input = "",
+ .caps = SMTP_CAPABILITY_DSN,
+ .params = {
+ .envid = NULL,
+ }
+ },
+ {
+ .input = "ENVID=",
+ .caps = SMTP_CAPABILITY_DSN,
+ .params = {
+ .envid = "",
+ }
+ },
+ {
+ .input = "ENVID=AABBCCDD",
+ .caps = SMTP_CAPABILITY_DSN,
+ .params = {
+ .envid = "AABBCCDD",
+ }
+ },
+ {
+ .input = "ENVID=AA+2BBB+3DCC+2BDD",
+ .caps = SMTP_CAPABILITY_DSN,
+ .params = {
+ .envid = "AA+BB=CC+DD",
+ }
+ },
+ /* RET */
+ {
+ .input = "",
+ .caps = SMTP_CAPABILITY_DSN,
+ .params = {
+ .ret = SMTP_PARAM_MAIL_RET_UNSPECIFIED,
+ }
+ },
+ {
+ .input = "RET=HDRS",
+ .caps = SMTP_CAPABILITY_DSN,
+ .params = {
+ .ret = SMTP_PARAM_MAIL_RET_HDRS,
+ }
+ },
+ {
+ .input = "RET=FULL",
+ .caps = SMTP_CAPABILITY_DSN,
+ .params = {
+ .ret = SMTP_PARAM_MAIL_RET_FULL,
+ }
+ },
+ /* SIZE */
+ {
+ .input = "",
+ .caps = SMTP_CAPABILITY_SIZE,
+ .params = {
+ .size = 0
+ }
+ },
+ {
+ .input = "SIZE=267914296",
+ .caps = SMTP_CAPABILITY_SIZE,
+ .params = {
+ .size = 267914296
+ }
+ },
+ /* <extensions> */
+ {
+ .input = "FROP=friep",
+ .caps = SMTP_CAPABILITY_SIZE,
+ .extensions = test_extensions,
+ .params = {
+ .extra_params = {
+ .arr = {
+ .buffer = &test_params_buffer1,
+ .element_size = sizeof(struct smtp_param)
+ }
+ }
+ }
+ },
+ {
+ .input = "FROP=friep FRUP=frml",
+ .extensions = test_extensions,
+ .params = {
+ .extra_params = {
+ .arr = {
+ .buffer = &test_params_buffer2,
+ .element_size = sizeof(struct smtp_param)
+ }
+ }
+ }
+ }
+};
+
+unsigned int valid_mail_params_parse_test_count =
+ N_ELEMENTS(valid_mail_params_parse_tests);
+
+static void
+test_smtp_mail_params_auth(const struct smtp_params_mail *test,
+ const struct smtp_params_mail *parsed)
+{
+ if (parsed->auth->localpart == NULL ||
+ test->auth->localpart == NULL) {
+ test_out(t_strdup_printf("params.auth->localpart = %s",
+ parsed->auth->localpart),
+ (parsed->auth->localpart == test->auth->localpart));
+ } else {
+ test_out(t_strdup_printf("params.auth->localpart = \"%s\"",
+ parsed->auth->localpart),
+ strcmp(parsed->auth->localpart,
+ test->auth->localpart) == 0);
+ }
+ if (parsed->auth->domain == NULL ||
+ test->auth->domain == NULL) {
+ test_out(t_strdup_printf("params.auth->domain = %s",
+ parsed->auth->domain),
+ (parsed->auth->domain == test->auth->domain));
+ } else {
+ test_out(t_strdup_printf("params.auth->domain = \"%s\"",
+ parsed->auth->domain),
+ strcmp(parsed->auth->domain,
+ test->auth->domain) == 0);
+ }
+}
+
+static void
+test_smtp_mail_params_body(const struct smtp_params_mail *test,
+ const struct smtp_params_mail *parsed)
+{
+ const char *type_name = NULL;
+
+ switch (parsed->body.type) {
+ case SMTP_PARAM_MAIL_BODY_TYPE_UNSPECIFIED:
+ type_name = "<UNSPECIFIED>";
+ break;
+ case SMTP_PARAM_MAIL_BODY_TYPE_7BIT:
+ type_name = "7BIT";
+ break;
+ case SMTP_PARAM_MAIL_BODY_TYPE_8BITMIME:
+ type_name = "8BITMIME";
+ break;
+ case SMTP_PARAM_MAIL_BODY_TYPE_BINARYMIME:
+ type_name = "BINARYMIME";
+ break;
+ case SMTP_PARAM_MAIL_BODY_TYPE_EXTENSION:
+ type_name = parsed->body.ext;
+ break;
+ default:
+ i_unreached();
+ }
+
+ test_out(t_strdup_printf("params.body.type = %s", type_name),
+ (parsed->body.type == test->body.type &&
+ (parsed->body.type != SMTP_PARAM_MAIL_BODY_TYPE_EXTENSION ||
+ (parsed->body.ext != NULL &&
+ strcmp(parsed->body.ext, test->body.ext) == 0))));
+}
+
+static void
+test_smtp_mail_params_envid(const struct smtp_params_mail *test,
+ const struct smtp_params_mail *parsed)
+{
+ if (parsed->envid == NULL || test->envid == NULL) {
+ test_out(t_strdup_printf("params.auth->localpart = %s",
+ parsed->envid),
+ (parsed->envid == test->envid));
+ } else {
+ test_out(t_strdup_printf("params.auth->localpart = \"%s\"",
+ parsed->envid),
+ strcmp(parsed->envid, test->envid) == 0);
+ }
+}
+
+static void
+test_smtp_mail_params_ret(const struct smtp_params_mail *test,
+ const struct smtp_params_mail *parsed)
+{
+ const char *ret_name = NULL;
+
+ switch (parsed->ret) {
+ case SMTP_PARAM_MAIL_RET_UNSPECIFIED:
+ ret_name = "<UNSPECIFIED>";
+ break;
+ case SMTP_PARAM_MAIL_RET_HDRS:
+ ret_name = "HDRS";
+ break;
+ case SMTP_PARAM_MAIL_RET_FULL:
+ ret_name = "FULL";
+ break;
+ default:
+ i_unreached();
+ }
+
+ test_out(t_strdup_printf("params.ret = %s", ret_name),
+ parsed->ret == test->ret);
+}
+
+static void
+test_smtp_mail_params_size(const struct smtp_params_mail *test,
+ const struct smtp_params_mail *parsed)
+{
+ test_out(t_strdup_printf("params.size = %"PRIuUOFF_T, parsed->size),
+ parsed->size == test->size);
+}
+
+static void
+test_smtp_mail_params_extensions(const struct smtp_params_mail *test,
+ const struct smtp_params_mail *parsed)
+{
+ const struct smtp_param *tparam, *pparam;
+ unsigned int i;
+
+ if (!array_is_created(&test->extra_params) ||
+ array_count(&test->extra_params) == 0) {
+ test_out(t_strdup_printf("params.extra_params.count = %u",
+ (!array_is_created(&parsed->extra_params) ?
+ 0 : array_count(&parsed->extra_params))),
+ (!array_is_created(&parsed->extra_params) ||
+ array_count(&parsed->extra_params) == 0));
+ return;
+ }
+
+ if (!array_is_created(&parsed->extra_params) ||
+ array_count(&parsed->extra_params) == 0) {
+ test_out("params.extra_params.count = 0", FALSE);
+ return;
+ }
+
+ if (array_count(&test->extra_params) !=
+ array_count(&parsed->extra_params)) {
+ test_out(t_strdup_printf("params.extra_params.count = %u",
+ (!array_is_created(&parsed->extra_params) ?
+ 0 : array_count(&parsed->extra_params))), FALSE);
+ return;
+ }
+
+ for (i = 0; i < array_count(&test->extra_params); i++) {
+ tparam = array_idx(&test->extra_params, i);
+ pparam = array_idx(&parsed->extra_params, i);
+
+ test_out(t_strdup_printf("params.extra_params[%u] = [\"%s\"=\"%s\"]",
+ i, pparam->keyword, pparam->value),
+ strcmp(pparam->keyword, tparam->keyword) == 0 &&
+ ((pparam->value == NULL && tparam->value == NULL) ||
+ (pparam->value != NULL && tparam->value != NULL &&
+ strcmp(pparam->value, tparam->value) == 0)));
+ }
+}
+
+static void test_smtp_mail_params_parse_valid(void)
+{
+ unsigned int i;
+
+ for (i = 0; i < valid_mail_params_parse_test_count; i++) T_BEGIN {
+ const struct valid_mail_params_parse_test *test;
+ struct smtp_params_mail params;
+ enum smtp_param_parse_error error_code;
+ const char *error = NULL, *output;
+ int ret;
+
+ test = &valid_mail_params_parse_tests[i];
+ ret = smtp_params_mail_parse(pool_datastack_create(),
+ test->input, test->caps, test->extensions,
+ test->body_extensions, &params, &error_code, &error);
+
+ test_begin(t_strdup_printf("smtp mail params valid [%d]", i));
+ test_out_reason(t_strdup_printf("parse(\"%s\")", test->input),
+ ret >= 0, error);
+
+ if (ret >= 0) {
+ string_t *encoded;
+
+ /* AUTH */
+ if ((test->caps & SMTP_CAPABILITY_AUTH) != 0)
+ test_smtp_mail_params_auth(&test->params, &params);
+ /* BODY */
+ if ((test->caps & SMTP_CAPABILITY_8BITMIME) != 0 ||
+ (test->caps & SMTP_CAPABILITY_BINARYMIME) != 0)
+ test_smtp_mail_params_body(&test->params, &params);
+ /* ENVID */
+ if ((test->caps & SMTP_CAPABILITY_DSN) != 0)
+ test_smtp_mail_params_envid(&test->params, &params);
+ /* RET */
+ if ((test->caps & SMTP_CAPABILITY_DSN) != 0)
+ test_smtp_mail_params_ret(&test->params, &params);
+ /* SIZE */
+ if ((test->caps & SMTP_CAPABILITY_SIZE) != 0)
+ test_smtp_mail_params_size(&test->params, &params);
+ /* <extensions> */
+ if (test->extensions != NULL)
+ test_smtp_mail_params_extensions(&test->params, &params);
+
+ encoded = t_str_new(256);
+ smtp_params_mail_write(encoded, test->caps,
+ test->extensions, &params);
+
+ output = (test->output == NULL ? test->input : test->output);
+ test_out(t_strdup_printf("encode() = \"%s\"",
+ str_c(encoded)),
+ strcmp(str_c(encoded), output) == 0);
+ }
+ test_end();
+ } T_END;
+}
+
+/* Invalid mail params tests */
+
+struct invalid_mail_params_parse_test {
+ const char *input;
+
+ enum smtp_capability caps;
+ const char *const *extensions;
+};
+
+static const struct invalid_mail_params_parse_test
+invalid_mail_params_parse_tests[] = {
+ /* AUTH */
+ {
+ .input = "AUTH=<>",
+ },
+ {
+ .input = "AUTH=++",
+ .caps = SMTP_CAPABILITY_AUTH
+ },
+ /* BODY */
+ {
+ .input = "BODY=8BITMIME",
+ },
+ {
+ .input = "BODY=BINARYMIME",
+ },
+ {
+ .input = "BODY=BINARYMIME",
+ .caps = SMTP_CAPABILITY_BINARYMIME
+ },
+ {
+ .input = "BODY=FROP",
+ .caps = SMTP_CAPABILITY_8BITMIME
+ },
+ /* ENVID */
+ {
+ .input = "ENVID=AABBCC",
+ },
+ {
+ .input = "ENVID=++",
+ .caps = SMTP_CAPABILITY_DSN
+ },
+ /* RET */
+ {
+ .input = "RET=FULL",
+ },
+ {
+ .input = "RET=HDR",
+ },
+ {
+ .input = "RET=FROP",
+ .caps = SMTP_CAPABILITY_DSN
+ },
+ /* SIZE */
+ {
+ .input = "SIZE=13",
+ },
+ {
+ .input = "SIZE=ABC",
+ .caps = SMTP_CAPABILITY_SIZE
+ }
+};
+
+unsigned int invalid_mail_params_parse_test_count =
+ N_ELEMENTS(invalid_mail_params_parse_tests);
+
+static void test_smtp_mail_params_parse_invalid(void)
+{
+ unsigned int i;
+
+ for (i = 0; i < invalid_mail_params_parse_test_count; i++) T_BEGIN {
+ const struct invalid_mail_params_parse_test *test;
+ struct smtp_params_mail params;
+ enum smtp_param_parse_error error_code;
+ const char *error = NULL;
+ int ret;
+
+ test = &invalid_mail_params_parse_tests[i];
+ ret = smtp_params_mail_parse(pool_datastack_create(),
+ test->input, test->caps,
+ test->extensions, NULL,
+ &params, &error_code, &error);
+
+ test_begin(t_strdup_printf("smtp mail params invalid [%d]", i));
+ test_out_reason(t_strdup_printf("parse(\"%s\")", test->input),
+ ret < 0, error);
+ test_end();
+ } T_END;
+}
+
+/* Valid rcpt params tests */
+
+struct valid_rcpt_params_parse_test {
+ const char *input, *output;
+
+ enum smtp_param_rcpt_parse_flags flags;
+ enum smtp_capability caps;
+ const char *const *extensions;
+
+ struct smtp_params_rcpt params;
+};
+
+static const struct valid_rcpt_params_parse_test
+valid_rcpt_params_parse_tests[] = {
+ /* ORCPT */
+ {
+ .input = "ORCPT=rfc822;e+3Dmc2@example.com",
+ .caps = SMTP_CAPABILITY_DSN,
+ .params = {
+ .orcpt = {
+ .addr = &test_address3
+ }
+ }
+ },
+ {
+ .input = "ORCPT=rfc822;<e+3Dmc2@example.com>",
+ .output = "ORCPT=rfc822;e+3Dmc2@example.com",
+ .caps = SMTP_CAPABILITY_DSN,
+ .params = {
+ .orcpt = {
+ .addr = &test_address3
+ }
+ }
+ },
+ {
+ .input = "ORCPT=rfc822;user+2Bdetail",
+ .flags = SMTP_PARAM_RCPT_FLAG_ORCPT_ALLOW_LOCALPART,
+ .caps = SMTP_CAPABILITY_DSN,
+ .params = {
+ .orcpt = {
+ .addr = &test_address2
+ }
+ }
+ },
+ {
+ .input = "ORCPT=rfc822;<user+2Bdetail>",
+ .output = "ORCPT=rfc822;user+2Bdetail",
+ .flags = SMTP_PARAM_RCPT_FLAG_ORCPT_ALLOW_LOCALPART,
+ .caps = SMTP_CAPABILITY_DSN,
+ .params = {
+ .orcpt = {
+ .addr = &test_address2
+ }
+ }
+ },
+ /* NOTIFY */
+ {
+ .input = "",
+ .caps = SMTP_CAPABILITY_DSN,
+ .params = {
+ .notify = SMTP_PARAM_RCPT_NOTIFY_UNSPECIFIED,
+ }
+ },
+ {
+ .input = "NOTIFY=SUCCESS",
+ .caps = SMTP_CAPABILITY_DSN,
+ .params = {
+ .notify = SMTP_PARAM_RCPT_NOTIFY_SUCCESS,
+ }
+ },
+ {
+ .input = "NOTIFY=FAILURE",
+ .caps = SMTP_CAPABILITY_DSN,
+ .params = {
+ .notify = SMTP_PARAM_RCPT_NOTIFY_FAILURE,
+ }
+ },
+ {
+ .input = "NOTIFY=DELAY",
+ .caps = SMTP_CAPABILITY_DSN,
+ .params = {
+ .notify = SMTP_PARAM_RCPT_NOTIFY_DELAY,
+ }
+ },
+ {
+ .input = "NOTIFY=NEVER",
+ .caps = SMTP_CAPABILITY_DSN,
+ .params = {
+ .notify = SMTP_PARAM_RCPT_NOTIFY_NEVER,
+ }
+ },
+ {
+ .input = "NOTIFY=SUCCESS,FAILURE,DELAY",
+ .caps = SMTP_CAPABILITY_DSN,
+ .params = {
+ .notify = SMTP_PARAM_RCPT_NOTIFY_SUCCESS |
+ SMTP_PARAM_RCPT_NOTIFY_FAILURE |
+ SMTP_PARAM_RCPT_NOTIFY_DELAY,
+ }
+ },
+ /* <extensions> */
+ {
+ .input = "FROP=friep",
+ .caps = SMTP_CAPABILITY_SIZE,
+ .extensions = test_extensions,
+ .params = {
+ .extra_params = {
+ .arr = {
+ .buffer = &test_params_buffer1,
+ .element_size = sizeof(struct smtp_param)
+ }
+ }
+ }
+ },
+ {
+ .input = "FROP=friep FRUP=frml",
+ .extensions = test_extensions,
+ .params = {
+ .extra_params = {
+ .arr = {
+ .buffer = &test_params_buffer2,
+ .element_size = sizeof(struct smtp_param)
+ }
+ }
+ }
+ }
+};
+
+unsigned int valid_rcpt_params_parse_test_count =
+ N_ELEMENTS(valid_rcpt_params_parse_tests);
+
+static void
+test_smtp_rcpt_params_orcpt(const struct smtp_params_rcpt *test,
+ const struct smtp_params_rcpt *parsed)
+{
+ if (parsed->orcpt.addr == NULL) {
+ test_out("params.orcpt.addr = NULL",
+ test->orcpt.addr == NULL);
+ return;
+ }
+
+ if (parsed->orcpt.addr->localpart == NULL ||
+ test->orcpt.addr->localpart == NULL) {
+ test_out(t_strdup_printf("params.orcpt.addr->localpart = %s",
+ parsed->orcpt.addr->localpart),
+ (parsed->orcpt.addr->localpart ==
+ test->orcpt.addr->localpart));
+ } else {
+ test_out(t_strdup_printf("params.orcpt.addr->localpart = \"%s\"",
+ parsed->orcpt.addr->localpart),
+ strcmp(parsed->orcpt.addr->localpart,
+ test->orcpt.addr->localpart) == 0);
+ }
+ if (parsed->orcpt.addr->domain == NULL ||
+ test->orcpt.addr->domain == NULL) {
+ test_out(t_strdup_printf("params.orcpt.addr->domain = %s",
+ parsed->orcpt.addr->domain),
+ (parsed->orcpt.addr->domain ==
+ test->orcpt.addr->domain));
+ } else {
+ test_out(t_strdup_printf("params.orcpt.addr->domain = \"%s\"",
+ parsed->orcpt.addr->domain),
+ strcmp(parsed->orcpt.addr->domain,
+ test->orcpt.addr->domain) == 0);
+ }
+}
+
+
+static void
+test_smtp_rcpt_params_notify(const struct smtp_params_rcpt *test,
+ const struct smtp_params_rcpt *parsed)
+{
+ string_t *notify_name;
+
+ notify_name = t_str_new(64);
+ if (parsed->notify == 0) {
+ str_append(notify_name, "<UNSPECIFIED>");
+ } else if ((parsed->notify & SMTP_PARAM_RCPT_NOTIFY_NEVER) != 0) {
+ i_assert((parsed->notify & SMTP_PARAM_RCPT_NOTIFY_SUCCESS) == 0);
+ i_assert((parsed->notify & SMTP_PARAM_RCPT_NOTIFY_FAILURE) == 0);
+ i_assert((parsed->notify & SMTP_PARAM_RCPT_NOTIFY_DELAY) == 0);
+ str_append(notify_name, "NEVER");
+ } else {
+ if ((parsed->notify & SMTP_PARAM_RCPT_NOTIFY_SUCCESS) != 0)
+ str_append(notify_name, "SUCCESS");
+ if ((parsed->notify & SMTP_PARAM_RCPT_NOTIFY_FAILURE) != 0) {
+ if (str_len(notify_name) > 0)
+ str_append_c(notify_name, ',');
+ str_append(notify_name, "FAILURE");
+ }
+ if ((parsed->notify & SMTP_PARAM_RCPT_NOTIFY_DELAY) != 0) {
+ if (str_len(notify_name) > 0)
+ str_append_c(notify_name, ',');
+ str_append(notify_name, "DELAY");
+ }
+ }
+
+ test_out(t_strdup_printf("params.notify = %s", str_c(notify_name)),
+ parsed->notify == test->notify);
+}
+
+static void
+test_smtp_rcpt_params_extensions(const struct smtp_params_rcpt *test,
+ const struct smtp_params_rcpt *parsed)
+{
+ const struct smtp_param *tparam, *pparam;
+ unsigned int i;
+
+ if (!array_is_created(&test->extra_params) ||
+ array_count(&test->extra_params) == 0) {
+ test_out(t_strdup_printf("params.extra_params.count = %u",
+ (!array_is_created(&parsed->extra_params) ?
+ 0 : array_count(&parsed->extra_params))),
+ (!array_is_created(&parsed->extra_params) ||
+ array_count(&parsed->extra_params) == 0));
+ return;
+ }
+
+ if (!array_is_created(&parsed->extra_params) ||
+ array_count(&parsed->extra_params) == 0) {
+ test_out("params.extra_params.count = 0", FALSE);
+ return;
+ }
+
+ if (array_count(&test->extra_params) !=
+ array_count(&parsed->extra_params)) {
+ test_out(t_strdup_printf("params.extra_params.count = %u",
+ (!array_is_created(&parsed->extra_params) ? 0 :
+ array_count(&parsed->extra_params))), FALSE);
+ return;
+ }
+
+ for (i = 0; i < array_count(&test->extra_params); i++) {
+ tparam = array_idx(&test->extra_params, i);
+ pparam = array_idx(&parsed->extra_params, i);
+
+ test_out(t_strdup_printf("params.extra_params[%u] = [\"%s\"=\"%s\"]",
+ i, pparam->keyword, pparam->value),
+ strcmp(pparam->keyword, tparam->keyword) == 0 &&
+ ((pparam->value == NULL && tparam->value == NULL) ||
+ (pparam->value != NULL && tparam->value != NULL &&
+ strcmp(pparam->value, tparam->value) == 0)));
+ }
+}
+
+static void test_smtp_rcpt_params_parse_valid(void)
+{
+ unsigned int i;
+
+ for (i = 0; i < valid_rcpt_params_parse_test_count; i++) T_BEGIN {
+ const struct valid_rcpt_params_parse_test *test;
+ struct smtp_params_rcpt params;
+ enum smtp_param_parse_error error_code;
+ const char *error = NULL, *output;
+ int ret;
+
+ test = &valid_rcpt_params_parse_tests[i];
+ ret = smtp_params_rcpt_parse(pool_datastack_create(),
+ test->input, test->flags,
+ test->caps, test->extensions,
+ &params, &error_code, &error);
+
+ test_begin(t_strdup_printf("smtp rcpt params valid [%d]", i));
+ test_out_reason(t_strdup_printf("parse(\"%s\")",
+ test->input), ret >= 0, error);
+
+ if (ret >= 0) {
+ string_t *encoded;
+
+ /* ORCPT */
+ if ((test->caps & SMTP_CAPABILITY_DSN) != 0)
+ test_smtp_rcpt_params_orcpt(&test->params, &params);
+ /* NOTIFY */
+ if ((test->caps & SMTP_CAPABILITY_DSN) != 0)
+ test_smtp_rcpt_params_notify(&test->params, &params);
+ /* <extensions> */
+ if (test->extensions != NULL)
+ test_smtp_rcpt_params_extensions(&test->params, &params);
+
+ encoded = t_str_new(256);
+ smtp_params_rcpt_write(encoded, test->caps,
+ test->extensions, &params);
+
+ output = (test->output == NULL ? test->input : test->output);
+ test_out(t_strdup_printf("encode() = \"%s\"",
+ str_c(encoded)),
+ strcmp(str_c(encoded), output) == 0);
+ }
+ test_end();
+ } T_END;
+}
+
+/* Invalid rcpt params tests */
+
+struct invalid_rcpt_params_parse_test {
+ const char *input;
+
+ enum smtp_param_rcpt_parse_flags flags;
+ enum smtp_capability caps;
+ const char *const *extensions;
+};
+
+static const struct invalid_rcpt_params_parse_test
+invalid_rcpt_params_parse_tests[] = {
+ /* DSN */
+ {
+ .input = "ORCPT=rfc822;frop@example.com",
+ },
+ {
+ .input = "ORCPT=rfc822;<>",
+ .caps = SMTP_CAPABILITY_DSN
+ },
+ {
+ .input = "ORCPT=rfc822;",
+ .caps = SMTP_CAPABILITY_DSN
+ },
+ {
+ .input = "ORCPT=++",
+ .caps = SMTP_CAPABILITY_DSN
+ },
+ {
+ .input = "ORCPT=rfc822;++",
+ .caps = SMTP_CAPABILITY_DSN
+ },
+ {
+ .input = "NOTIFY=SUCCESS",
+ },
+ {
+ .input = "NOTIFY=FROP",
+ .caps = SMTP_CAPABILITY_DSN
+ },
+ {
+ .input = "NOTIFY=NEVER,SUCCESS",
+ .caps = SMTP_CAPABILITY_DSN
+ }
+};
+
+unsigned int invalid_rcpt_params_parse_test_count =
+ N_ELEMENTS(invalid_rcpt_params_parse_tests);
+
+static void test_smtp_rcpt_params_parse_invalid(void)
+{
+ unsigned int i;
+
+ for (i = 0; i < invalid_rcpt_params_parse_test_count; i++) T_BEGIN {
+ const struct invalid_rcpt_params_parse_test *test;
+ struct smtp_params_rcpt params;
+ enum smtp_param_parse_error error_code;
+ const char *error = NULL;
+ int ret;
+
+ test = &invalid_rcpt_params_parse_tests[i];
+ ret = smtp_params_rcpt_parse(pool_datastack_create(),
+ test->input, test->flags,
+ test->caps, test->extensions,
+ &params, &error_code, &error);
+
+ test_begin(t_strdup_printf("smtp rcpt params invalid [%d]", i));
+ test_out_reason(t_strdup_printf("parse(\"%s\")", test->input),
+ ret < 0, error);
+ test_end();
+ } T_END;
+}
+
+int main(void)
+{
+ static void (*test_functions[])(void) = {
+ test_smtp_mail_params_parse_valid,
+ test_smtp_mail_params_parse_invalid,
+ test_smtp_rcpt_params_parse_valid,
+ test_smtp_rcpt_params_parse_invalid,
+ NULL
+ };
+ return test_run(test_functions);
+}
diff --git a/src/lib-smtp/test-smtp-payload.c b/src/lib-smtp/test-smtp-payload.c
new file mode 100644
index 0000000..7363736
--- /dev/null
+++ b/src/lib-smtp/test-smtp-payload.c
@@ -0,0 +1,1148 @@
+/* Copyright (c) 2013-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "str.h"
+#include "llist.h"
+#include "array.h"
+#include "path-util.h"
+#include "hostpid.h"
+#include "ioloop.h"
+#include "istream.h"
+#include "ostream.h"
+#include "istream-base64.h"
+#include "istream-crlf.h"
+#include "iostream-temp.h"
+#include "iostream-ssl.h"
+#include "iostream-ssl-test.h"
+#ifdef HAVE_OPENSSL
+#include "iostream-openssl.h"
+#endif
+#include "connection.h"
+#include "test-common.h"
+#include "test-subprocess.h"
+#include "smtp-server.h"
+#include "smtp-client.h"
+#include "smtp-client-connection.h"
+#include "smtp-client-transaction.h"
+
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <fcntl.h>
+#include <unistd.h>
+#include <dirent.h>
+
+#define CLIENT_PROGRESS_TIMEOUT 60
+#define SERVER_KILL_TIMEOUT_SECS 20
+#define MAX_PARALLEL_PENDING 200
+
+static bool debug = FALSE;
+static bool small_socket_buffers = FALSE;
+static const char *failure = NULL;
+
+enum test_ssl_mode {
+ TEST_SSL_MODE_NONE = 0,
+ TEST_SSL_MODE_IMMEDIATE,
+ TEST_SSL_MODE_STARTTLS
+};
+
+static unsigned int test_max_pending = 1;
+static bool test_unknown_size = FALSE;
+static enum test_ssl_mode test_ssl_mode = TEST_SSL_MODE_NONE;
+
+static struct ip_addr bind_ip;
+static in_port_t bind_port = 0;
+static int fd_listen = -1;
+
+static void main_deinit(void);
+
+/*
+ * Test files
+ */
+
+static ARRAY_TYPE(const_string) files;
+static pool_t files_pool;
+
+static void test_files_read_dir(const char *path)
+{
+ DIR *dirp;
+
+ /* open the directory */
+ if ((dirp = opendir(path)) == NULL) {
+ if (errno == ENOENT || errno == EACCES)
+ return;
+ i_fatal("test files: "
+ "failed to open directory %s: %m", path);
+ }
+
+ /* read entries */
+ for (;;) {
+ const char *file;
+ struct dirent *dp;
+ struct stat st;
+#if 0
+ if (array_count(&files) > 10)
+ break;
+#endif
+ errno = 0;
+ if ((dp = readdir(dirp)) == NULL)
+ break;
+ if (*dp->d_name == '.')
+ continue;
+
+ file = t_abspath_to(dp->d_name, path);
+ if (stat(file, &st) == 0) {
+ if (S_ISREG(st.st_mode)) {
+ file += 2; /* skip "./" */
+ file = p_strdup(files_pool, file);
+ array_push_back(&files, &file);
+ } else if (S_ISDIR(st.st_mode)) {
+ test_files_read_dir(file);
+ }
+ }
+ }
+
+ if (errno != 0)
+ i_fatal("test files: "
+ "failed to read directory %s: %m", path);
+
+ /* Close the directory */
+ if (closedir(dirp) < 0)
+ i_error("test files: "
+ "failed to close directory %s: %m", path);
+}
+
+static void test_files_init(void)
+{
+ /* initialize file array */
+ files_pool = pool_alloconly_create(
+ MEMPOOL_GROWING"smtp_server_request", 4096);
+ p_array_init(&files, files_pool, 512);
+
+ /* obtain all filenames */
+ test_files_read_dir(".");
+}
+
+static void test_files_deinit(void)
+{
+ pool_unref(&files_pool);
+}
+
+static struct istream *test_file_open(const char *path)
+{
+ struct istream *file;
+ int fd;
+
+ fd = open(path, O_RDONLY);
+ if (fd < 0) {
+ if (errno != ENOENT && errno != EACCES) {
+ i_fatal("test files: "
+ "open(%s) failed: %m", path);
+ }
+ if (debug) {
+ i_debug("test files: "
+ "open(%s) failed: %m", path);
+ }
+ return NULL;
+ }
+
+ file = i_stream_create_fd_autoclose(&fd, 40960);
+ i_stream_set_name(file, path);
+ return file;
+}
+
+/*
+ * Test server
+ */
+
+struct client {
+ pool_t pool;
+ struct client *prev, *next;
+
+ struct smtp_server_connection *smtp_conn;
+};
+
+struct client_transaction {
+ struct client *client;
+ struct smtp_server_cmd_ctx *data_cmd;
+ struct smtp_server_transaction *trans;
+
+ const char *path;
+
+ struct istream *payload, *file;
+};
+
+static struct smtp_server *smtp_server;
+
+static struct io *io_listen;
+static struct client *clients;
+
+static int
+client_transaction_read_more(struct client_transaction *ctrans)
+{
+ struct istream *payload = ctrans->payload;
+ const unsigned char *pdata, *fdata;
+ size_t psize, fsize, pleft;
+ off_t ret;
+
+ if (debug) {
+ i_debug("test server: read more payload for [%s]",
+ ctrans->path);
+ }
+
+ /* read payload */
+ while ((ret = i_stream_read_more(payload, &pdata, &psize)) > 0) {
+ if (debug) {
+ i_debug("test server: "
+ "got data for [%s] (size=%d)",
+ ctrans->path, (int)psize);
+ }
+ /* compare with file on disk */
+ pleft = psize;
+ while ((ret = i_stream_read_more(ctrans->file,
+ &fdata, &fsize)) > 0 &&
+ pleft > 0) {
+ fsize = (fsize > pleft ? pleft : fsize);
+ if (memcmp(pdata, fdata, fsize) != 0) {
+ i_fatal("test server: "
+ "received data does not match file [%s] "
+ "(%"PRIuUOFF_T":%"PRIuUOFF_T")",
+ ctrans->path, payload->v_offset,
+ ctrans->file->v_offset);
+ }
+ i_stream_skip(ctrans->file, fsize);
+ pleft -= fsize;
+ pdata += fsize;
+ }
+ if (ret < 0 && ctrans->file->stream_errno != 0) {
+ i_fatal("test server: "
+ "failed to read file: %s",
+ i_stream_get_error(ctrans->file));
+ }
+ i_stream_skip(payload, psize);
+ }
+
+ if (ret == 0) {
+ if (debug) {
+ i_debug("test server: "
+ "need more data for [%s]",
+ ctrans->path);
+ }
+ /* we will be called again for this request */
+ return 0;
+ }
+
+ (void)i_stream_read(ctrans->file);
+ if (payload->stream_errno != 0) {
+ i_fatal("test server: "
+ "failed to read transaction payload: %s",
+ i_stream_get_error(payload));
+ }
+ if (i_stream_have_bytes_left(ctrans->file)) {
+ if (i_stream_read_more(ctrans->file, &fdata, &fsize) <= 0)
+ fsize = 0;
+ i_fatal("test server: "
+ "payload ended prematurely "
+ "(at least %zu bytes left)", fsize);
+ }
+
+ if (debug) {
+ i_debug("test server: "
+ "finished transaction for [%s]",
+ ctrans->path);
+ }
+
+ /* dereference payload stream; finishes the request */
+ i_stream_unref(&payload);
+ ctrans->payload = NULL;
+ i_stream_unref(&ctrans->file);
+
+ /* finished */
+ smtp_server_reply_all(ctrans->data_cmd, 250, "2.0.0", "OK");
+ return 1;
+}
+
+static void
+client_transaction_handle_payload(struct client_transaction *ctrans,
+ const char *path, struct istream *data_input)
+{
+ struct smtp_server_transaction *trans = ctrans->trans;
+ struct istream *fstream;
+
+ ctrans->path = p_strdup(trans->pool, path);
+
+ if (debug) {
+ i_debug("test server: got transaction for: %s",
+ path);
+ }
+
+ fstream = test_file_open(path);
+ if (fstream == NULL)
+ i_fatal("test server: failed to open: %s", path);
+
+ i_stream_ref(data_input);
+ ctrans->payload = data_input;
+ i_assert(ctrans->payload != NULL);
+
+ ctrans->file = i_stream_create_base64_encoder(fstream, 80, TRUE),
+ i_stream_unref(&fstream);
+
+ (void)client_transaction_read_more(ctrans);
+}
+
+/* transaction */
+
+static struct client_transaction *
+client_transaction_init(struct client *client,
+ struct smtp_server_cmd_ctx *data_cmd,
+ struct smtp_server_transaction *trans)
+{
+ struct client_transaction *ctrans;
+ pool_t pool = trans->pool;
+
+ ctrans = p_new(pool, struct client_transaction, 1);
+ ctrans->client = client;
+ ctrans->trans = trans;
+ ctrans->data_cmd = data_cmd;
+
+ return ctrans;
+}
+
+static void client_transaction_deinit(struct client_transaction **_ctrans)
+{
+ struct client_transaction *ctrans = *_ctrans;
+
+ *_ctrans = NULL;
+
+ i_stream_unref(&ctrans->payload);
+ i_stream_unref(&ctrans->file);
+}
+
+static void
+test_server_conn_trans_free(void *context ATTR_UNUSED,
+ struct smtp_server_transaction *trans)
+{
+ struct client_transaction *ctrans =
+ (struct client_transaction *)trans->context;
+ client_transaction_deinit(&ctrans);
+}
+
+static int
+test_server_conn_cmd_rcpt(void *conn_ctx ATTR_UNUSED,
+ struct smtp_server_cmd_ctx *cmd ATTR_UNUSED,
+ struct smtp_server_recipient *rcpt)
+{
+ if (debug) {
+ i_debug("test server: RCPT TO:%s",
+ smtp_address_encode(rcpt->path));
+ }
+
+ return 1;
+}
+
+static int
+test_server_conn_cmd_data_begin(void *conn_ctx, struct smtp_server_cmd_ctx *cmd,
+ struct smtp_server_transaction *trans,
+ struct istream *data_input)
+{
+ struct client *client = (struct client *)conn_ctx;
+ const char *fpath = trans->params.envid;
+ struct client_transaction *ctrans;
+
+ i_assert(fpath != NULL);
+
+ if (debug)
+ i_debug("test server: DATA (file path = %s)", fpath);
+
+ ctrans = client_transaction_init(client, cmd, trans);
+ client_transaction_handle_payload(ctrans, fpath, data_input);
+ trans->context = ctrans;
+ return 0;
+}
+
+static int
+test_server_conn_cmd_data_continue(void *conn_ctx ATTR_UNUSED,
+ struct smtp_server_cmd_ctx *cmd,
+ struct smtp_server_transaction *trans)
+{
+ struct client_transaction *ctrans =
+ (struct client_transaction *)trans->context;
+
+ if (debug)
+ i_debug("test server: DATA continue");
+
+ ctrans->data_cmd = cmd;
+
+ return client_transaction_read_more(ctrans);
+}
+
+/* client connection */
+
+static void test_server_connection_free(void *context);
+
+static const struct smtp_server_callbacks server_callbacks =
+{
+ .conn_cmd_rcpt = test_server_conn_cmd_rcpt,
+ .conn_cmd_data_begin = test_server_conn_cmd_data_begin,
+ .conn_cmd_data_continue = test_server_conn_cmd_data_continue,
+
+ .conn_trans_free = test_server_conn_trans_free,
+
+ .conn_free = test_server_connection_free,
+};
+
+static void client_init(int fd)
+{
+ struct client *client;
+ pool_t pool;
+
+ net_set_nonblock(fd, TRUE);
+
+ pool = pool_alloconly_create("client", 256);
+ client = p_new(pool, struct client, 1);
+ client->pool = pool;
+
+ client->smtp_conn = smtp_server_connection_create(
+ smtp_server, fd, fd, NULL, 0,
+ (test_ssl_mode == TEST_SSL_MODE_IMMEDIATE),
+ NULL, &server_callbacks, client);
+ smtp_server_connection_start(client->smtp_conn);
+ DLLIST_PREPEND(&clients, client);
+}
+
+static void client_deinit(struct client **_client)
+{
+ struct client *client = *_client;
+
+ *_client = NULL;
+
+ DLLIST_REMOVE(&clients, client);
+
+ if (client->smtp_conn != NULL) {
+ smtp_server_connection_terminate(&client->smtp_conn,
+ NULL, "deinit");
+ }
+ pool_unref(&client->pool);
+}
+
+static void test_server_connection_free(void *context)
+{
+ struct client *client = context;
+
+ client->smtp_conn = NULL;
+ client_deinit(&client);
+}
+
+static void client_accept(void *context ATTR_UNUSED)
+{
+ int fd;
+
+ for (;;) {
+ /* accept new client */
+ if ((fd = net_accept(fd_listen, NULL, NULL)) < 0) {
+ if (errno == EAGAIN)
+ break;
+ if (errno == ECONNABORTED)
+ continue;
+ i_fatal("test server: accept() failed: %m");
+ }
+
+ client_init(fd);
+ }
+}
+
+/* */
+
+static void test_server_init(const struct smtp_server_settings *server_set)
+{
+ /* open server socket */
+ io_listen = io_add(fd_listen, IO_READ, client_accept, NULL);
+
+ smtp_server = smtp_server_init(server_set);
+}
+
+static void test_server_deinit(void)
+{
+ /* close server socket */
+ io_remove(&io_listen);
+
+ /* deinitialize */
+ smtp_server_deinit(&smtp_server);
+}
+
+/*
+ * Test client
+ */
+
+struct test_client_connection {
+ struct smtp_client_connection *conn;
+ struct smtp_client_transaction *trans;
+};
+
+struct test_client_transaction {
+ struct test_client_transaction *prev, *next;
+ struct test_client_connection *conn;
+
+ struct io *io;
+ struct istream *file;
+ unsigned int files_idx;
+};
+
+static struct test_client_connection test_conns[MAX_PARALLEL_PENDING];
+static struct smtp_client *smtp_client;
+static enum smtp_protocol client_protocol;
+static struct test_client_transaction *client_requests;
+static unsigned int client_files_first, client_files_last;
+static struct timeout *client_to = NULL;
+struct timeout *to_client_progress = NULL;
+
+static struct test_client_connection *test_client_connection_get(void)
+{
+ unsigned int i;
+ enum smtp_client_connection_ssl_mode ssl_mode;
+
+ for (i = 0; i < MAX_PARALLEL_PENDING; i++) {
+ if (test_conns[i].trans == NULL)
+ break;
+ }
+
+ i_assert(i < MAX_PARALLEL_PENDING);
+
+ switch (test_ssl_mode) {
+ case TEST_SSL_MODE_NONE:
+ default:
+ ssl_mode = SMTP_CLIENT_SSL_MODE_NONE;
+ break;
+ case TEST_SSL_MODE_IMMEDIATE:
+ ssl_mode = SMTP_CLIENT_SSL_MODE_IMMEDIATE;
+ break;
+ case TEST_SSL_MODE_STARTTLS:
+ ssl_mode = SMTP_CLIENT_SSL_MODE_STARTTLS;
+ break;
+ }
+
+ if (test_conns[i].conn == NULL) {
+ test_conns[i].conn = smtp_client_connection_create(
+ smtp_client, client_protocol,
+ net_ip2addr(&bind_ip), bind_port,
+ ssl_mode, NULL);
+ }
+ return &test_conns[i];
+}
+
+static struct test_client_transaction *test_client_transaction_new(void)
+{
+ struct test_client_transaction *tctrans;
+
+ tctrans = i_new(struct test_client_transaction, 1);
+ DLLIST_PREPEND(&client_requests, tctrans);
+
+ return tctrans;
+}
+
+static void
+test_client_transaction_destroy(struct test_client_transaction *tctrans)
+{
+ smtp_client_transaction_destroy(&tctrans->conn->trans);
+ io_remove(&tctrans->io);
+ i_stream_unref(&tctrans->file);
+
+ DLLIST_REMOVE(&client_requests, tctrans);
+ i_free(tctrans);
+}
+
+static void test_client_continue(void *dummy);
+
+static void test_client_finished(unsigned int files_idx)
+{
+ const char **paths;
+ unsigned int count;
+
+ if (debug) {
+ i_debug("test client: "
+ "finished [%u]", files_idx);
+ }
+
+ paths = array_get_modifiable(&files, &count);
+ i_assert(files_idx < count);
+ i_assert(client_files_first < count);
+ i_assert(paths[files_idx] != NULL);
+
+ paths[files_idx] = NULL;
+ if (client_to == NULL)
+ client_to = timeout_add_short(0, test_client_continue, NULL);
+}
+
+static void
+test_client_transaction_finish(struct test_client_transaction *tctrans)
+{
+ tctrans->conn->trans = NULL;
+ if (io_loop_is_running(current_ioloop))
+ test_client_finished(tctrans->files_idx);
+ test_client_transaction_destroy(tctrans);
+}
+
+static void
+test_client_transaction_rcpt(const struct smtp_reply *reply,
+ struct test_client_transaction *tctrans)
+{
+ const char **paths;
+ const char *path;
+ unsigned int count;
+
+ if (to_client_progress != NULL)
+ timeout_reset(to_client_progress);
+
+ paths = array_get_modifiable(&files, &count);
+ i_assert(tctrans->files_idx < count);
+ i_assert(client_files_first < count);
+ path = paths[tctrans->files_idx];
+ i_assert(path != NULL);
+
+ if (reply->status / 100 != 2) {
+ i_fatal("test client: "
+ "SMTP RCPT for %s failed: %s",
+ path, smtp_reply_log(reply));
+ }
+}
+
+static void
+test_client_transaction_rcpt_data(const struct smtp_reply *reply ATTR_UNUSED,
+ struct test_client_transaction *tctrans)
+{
+ const char **paths;
+ const char *path;
+ unsigned int count;
+
+ if (to_client_progress != NULL)
+ timeout_reset(to_client_progress);
+
+ paths = array_get_modifiable(&files, &count);
+ i_assert(tctrans->files_idx < count);
+ i_assert(client_files_first < count);
+ path = paths[tctrans->files_idx];
+ i_assert(path != NULL);
+
+ if (reply->status / 100 != 2) {
+ i_fatal("test client: "
+ "SMTP DATA for %s failed: %s",
+ path, smtp_reply_log(reply));
+ }
+}
+
+static void
+test_client_transaction_data(const struct smtp_reply *reply,
+ struct test_client_transaction *tctrans)
+{
+ const char **paths;
+ const char *path;
+ unsigned int count;
+
+ if (to_client_progress != NULL)
+ timeout_reset(to_client_progress);
+
+ if (debug) {
+ i_debug("test client: "
+ "got response for DATA [%u]",
+ tctrans->files_idx);
+ }
+
+ paths = array_get_modifiable(&files, &count);
+ i_assert(tctrans->files_idx < count);
+ i_assert(client_files_first < count);
+ path = paths[tctrans->files_idx];
+ i_assert(path != NULL);
+
+ if (debug) {
+ i_debug("test client: "
+ "path for [%u]: %s",
+ tctrans->files_idx, path);
+ }
+
+ if (reply->status / 100 != 2) {
+ i_fatal("test client: "
+ "SMTP transaction for %s failed: %s",
+ path, smtp_reply_log(reply));
+ }
+}
+
+static void test_client_continue(void *dummy ATTR_UNUSED)
+{
+ struct test_client_transaction *tctrans;
+ struct smtp_params_mail mail_params;
+ const char **paths;
+ unsigned int count, pending_count, i;
+
+ if (debug)
+ i_debug("test client: continue");
+
+ timeout_remove(&client_to);
+ if (to_client_progress != NULL)
+ timeout_reset(to_client_progress);
+
+ paths = array_get_modifiable(&files, &count);
+
+ i_assert(client_files_first <= count);
+ i_assert(client_files_last <= count);
+
+ i_assert(client_files_first <= client_files_last);
+ for (; (client_files_first < client_files_last &&
+ paths[client_files_first] == NULL); client_files_first++);
+
+ pending_count = 0;
+ for (i = client_files_first; i < client_files_last; i++) {
+ if (paths[i] != NULL)
+ pending_count++;
+ }
+
+ if (debug) {
+ i_debug("test client: finished until [%u/%u]; "
+ "sending until [%u/%u] (%u pending)",
+ client_files_first-1, count,
+ client_files_last, count, pending_count);
+ }
+
+ if (debug && client_files_first < count) {
+ const char *path = paths[client_files_first];
+ i_debug("test client: "
+ "next blocking: %s [%d]",
+ (path == NULL ? "none" : path),
+ client_files_first);
+ }
+
+ if (client_files_first >= count) {
+ io_loop_stop(current_ioloop);
+ return;
+ }
+
+ for (; client_files_last < count && pending_count < test_max_pending;
+ client_files_last++, pending_count++) {
+ struct istream *fstream, *payload;
+ const char *path = paths[client_files_last];
+ unsigned int r, rcpts;
+
+ fstream = test_file_open(path);
+ if (fstream == NULL) {
+ paths[client_files_last] = NULL;
+ if (debug) {
+ i_debug("test client: "
+ "skipping %s [%u]",
+ path, client_files_last);
+ }
+ if (client_to == NULL) {
+ client_to = timeout_add_short(
+ 0, test_client_continue, NULL);
+ }
+ continue;
+ }
+
+ if (debug) {
+ i_debug("test client: "
+ "retrieving %s [%u]",
+ path, client_files_last);
+ }
+
+ tctrans = test_client_transaction_new();
+ tctrans->files_idx = client_files_last;
+ tctrans->conn = test_client_connection_get();
+
+ i_zero(&mail_params);
+ mail_params.envid = path;
+
+ tctrans->conn->trans = smtp_client_transaction_create(
+ tctrans->conn->conn,
+ &((struct smtp_address){.localpart = "user",
+ .domain = "example.com"}),
+ &mail_params, 0,
+ test_client_transaction_finish, tctrans);
+
+ rcpts = tctrans->files_idx % 10 + 1;
+ for (r = 1; r <= rcpts; r++) {
+ smtp_client_transaction_add_rcpt(
+ tctrans->conn->trans,
+ smtp_address_create_temp(
+ t_strdup_printf("rcpt%u", r),
+ "example.com"), NULL,
+ test_client_transaction_rcpt,
+ test_client_transaction_rcpt_data, tctrans);
+ }
+
+ if (!test_unknown_size) {
+ payload = i_stream_create_base64_encoder(
+ fstream, 80, TRUE);
+ } else {
+ struct istream *b64_stream =
+ i_stream_create_base64_encoder(
+ fstream, 80, FALSE);
+ payload = i_stream_create_crlf(b64_stream);
+ i_stream_unref(&b64_stream);
+ }
+
+ if (debug) {
+ uoff_t raw_size = UOFF_T_MAX, b64_size = UOFF_T_MAX;
+
+ (void)i_stream_get_size(fstream, TRUE, &raw_size);
+ (void)i_stream_get_size(payload, TRUE, &b64_size);
+ i_debug("test client: "
+ "sending %"PRIuUOFF_T"/%"PRIuUOFF_T" bytes payload %s [%u]",
+ raw_size, b64_size, path, client_files_last);
+ }
+
+ smtp_client_transaction_send(tctrans->conn->trans, payload,
+ test_client_transaction_data,
+ tctrans);
+
+ i_stream_unref(&payload);
+ i_stream_unref(&fstream);
+ }
+}
+
+static void test_client_progress_timeout(void *context ATTR_UNUSED)
+{
+ /* Terminate test due to lack of progress */
+ failure = "Test is hanging";
+ timeout_remove(&to_client_progress);
+ io_loop_stop(current_ioloop);
+}
+
+static void
+test_client(enum smtp_protocol protocol,
+ const struct smtp_client_settings *client_set)
+{
+ client_protocol = protocol;
+
+ if (!small_socket_buffers) {
+ to_client_progress = timeout_add(
+ CLIENT_PROGRESS_TIMEOUT*1000,
+ test_client_progress_timeout, NULL);
+ }
+
+ /* create client */
+ smtp_client = smtp_client_init(client_set);
+
+ /* start querying server */
+ client_files_first = client_files_last = 0;
+ test_client_continue(NULL);
+}
+
+static void test_client_init(void)
+{
+ i_zero(&test_conns);
+}
+
+static void test_client_deinit(void)
+{
+ timeout_remove(&client_to);
+ timeout_remove(&to_client_progress);
+ smtp_client_deinit(&smtp_client);
+
+ i_zero(&test_conns);
+}
+
+/*
+ * Tests
+ */
+
+struct test_server_data {
+ const struct smtp_server_settings *set;
+};
+
+static void test_open_server_fd(void)
+{
+ if (fd_listen != -1)
+ i_close_fd(&fd_listen);
+ fd_listen = net_listen(&bind_ip, &bind_port, 128);
+ if (fd_listen == -1) {
+ i_fatal("listen(%s:%u) failed: %m",
+ net_ip2addr(&bind_ip), bind_port);
+ }
+ net_set_nonblock(fd_listen, TRUE);
+}
+
+static int test_run_server(struct test_server_data *data)
+{
+ const struct smtp_server_settings *server_set = data->set;
+ struct ioloop *ioloop;
+
+ i_set_failure_prefix("SERVER: ");
+
+ if (debug)
+ i_debug("PID=%s", my_pid);
+
+ ioloop = io_loop_create();
+ test_server_init(server_set);
+ io_loop_run(ioloop);
+ test_server_deinit();
+ io_loop_destroy(&ioloop);
+
+ if (debug)
+ i_debug("Terminated");
+
+ i_close_fd(&fd_listen);
+ test_files_deinit();
+ main_deinit();
+ return 0;
+}
+
+static void
+test_run_client(
+ enum smtp_protocol protocol, struct smtp_client_settings *client_set,
+ void (*client_init)(enum smtp_protocol protocol,
+ const struct smtp_client_settings *client_set))
+{
+ struct ioloop *ioloop;
+
+ i_set_failure_prefix("CLIENT: ");
+
+ if (debug)
+ i_debug("client: PID=%s", my_pid);
+
+ ioloop = io_loop_create();
+ test_client_init();
+ client_init(protocol, client_set);
+ io_loop_run(ioloop);
+ test_client_deinit();
+ io_loop_destroy(&ioloop);
+
+ if (debug)
+ i_debug("Terminated");
+}
+
+static void
+test_run_client_server(
+ enum smtp_protocol protocol,
+ struct smtp_client_settings *client_set,
+ struct smtp_server_settings *server_set,
+ void (*client_init)(enum smtp_protocol protocol,
+ const struct smtp_client_settings *client_set))
+{
+ struct test_server_data data;
+
+ if (test_ssl_mode == TEST_SSL_MODE_STARTTLS)
+ server_set->capabilities |= SMTP_CAPABILITY_STARTTLS;
+
+ failure = NULL;
+
+ i_zero(&data);
+ data.set = server_set;
+
+ test_files_init();
+
+ /* Fork server */
+ test_open_server_fd();
+ test_subprocess_fork(test_run_server, &data, FALSE);
+ i_close_fd(&fd_listen);
+
+ /* Run client */
+ test_run_client(protocol, client_set, client_init);
+
+ i_unset_failure_prefix();
+ bind_port = 0;
+ test_subprocess_kill_all(SERVER_KILL_TIMEOUT_SECS);
+ test_files_deinit();
+}
+
+static void
+test_run_scenarios(
+ enum smtp_protocol protocol,
+ enum smtp_capability capabilities,
+ void (*client_init)(enum smtp_protocol protocol,
+ const struct smtp_client_settings *client_set))
+{
+ struct smtp_server_settings smtp_server_set;
+ struct smtp_client_settings smtp_client_set;
+ struct ssl_iostream_settings ssl_server_set, ssl_client_set;
+
+ /* ssl settings */
+ ssl_iostream_test_settings_server(&ssl_server_set);
+ ssl_server_set.verbose = debug;
+ ssl_iostream_test_settings_client(&ssl_client_set);
+ ssl_client_set.verbose = debug;
+
+ /* server settings */
+ i_zero(&smtp_server_set);
+ smtp_server_set.protocol = protocol;
+ smtp_server_set.capabilities = capabilities;
+ smtp_server_set.hostname = "localhost";
+ smtp_server_set.max_client_idle_time_msecs =
+ CLIENT_PROGRESS_TIMEOUT*1000;
+ smtp_server_set.max_pipelined_commands = 1;
+ smtp_server_set.auth_optional = TRUE;
+ smtp_server_set.ssl = &ssl_server_set;
+ smtp_server_set.debug = debug;
+
+ /* client settings */
+ i_zero(&smtp_client_set);
+ smtp_client_set.my_hostname = "localhost";
+ smtp_client_set.temp_path_prefix = "/tmp";
+ smtp_client_set.command_timeout_msecs = CLIENT_PROGRESS_TIMEOUT*1000;
+ smtp_client_set.connect_timeout_msecs = CLIENT_PROGRESS_TIMEOUT*1000;
+ smtp_client_set.ssl = &ssl_client_set;
+ smtp_client_set.debug = debug;
+
+ if (small_socket_buffers) {
+ smtp_client_set.socket_send_buffer_size = 4096;
+ smtp_client_set.socket_recv_buffer_size = 4096;
+ smtp_client_set.command_timeout_msecs = 20*60*1000;
+ smtp_client_set.connect_timeout_msecs = 20*60*1000;
+ smtp_server_set.socket_send_buffer_size = 4096;
+ smtp_server_set.socket_recv_buffer_size = 4096;
+ }
+
+ test_max_pending = 1;
+ test_unknown_size = FALSE;
+ test_ssl_mode = TEST_SSL_MODE_NONE;
+ test_run_client_server(protocol, &smtp_client_set, &smtp_server_set,
+ client_init);
+
+ test_out_reason("sequential", (failure == NULL), failure);
+
+ test_max_pending = MAX_PARALLEL_PENDING;
+ test_unknown_size = FALSE;
+ test_ssl_mode = TEST_SSL_MODE_NONE;
+ test_run_client_server(protocol, &smtp_client_set, &smtp_server_set,
+ client_init);
+
+ test_out_reason("parallel", (failure == NULL), failure);
+
+ smtp_server_set.max_pipelined_commands = 5;
+ smtp_server_set.capabilities |= SMTP_CAPABILITY_PIPELINING;
+ test_max_pending = MAX_PARALLEL_PENDING;
+ test_unknown_size = FALSE;
+ test_ssl_mode = TEST_SSL_MODE_NONE;
+ test_run_client_server(protocol, &smtp_client_set, &smtp_server_set,
+ client_init);
+
+ test_out_reason("parallel pipelining", (failure == NULL), failure);
+
+ smtp_server_set.max_pipelined_commands = 5;
+ smtp_server_set.capabilities |= SMTP_CAPABILITY_PIPELINING;
+ test_max_pending = MAX_PARALLEL_PENDING;
+ test_unknown_size = TRUE;
+ test_ssl_mode = TEST_SSL_MODE_NONE;
+ test_run_client_server(protocol, &smtp_client_set, &smtp_server_set,
+ client_init);
+
+ test_out_reason("unknown payload size", (failure == NULL), failure);
+
+#ifdef HAVE_OPENSSL
+ smtp_server_set.max_pipelined_commands = 5;
+ smtp_server_set.capabilities |= SMTP_CAPABILITY_PIPELINING;
+ test_max_pending = MAX_PARALLEL_PENDING;
+ test_unknown_size = FALSE;
+ test_ssl_mode = TEST_SSL_MODE_IMMEDIATE;
+ test_run_client_server(protocol, &smtp_client_set, &smtp_server_set,
+ client_init);
+
+ test_out_reason("parallel pipelining ssl",
+ (failure == NULL), failure);
+
+ smtp_server_set.max_pipelined_commands = 5;
+ smtp_server_set.capabilities |= SMTP_CAPABILITY_PIPELINING;
+ test_max_pending = MAX_PARALLEL_PENDING;
+ test_unknown_size = FALSE;
+ test_ssl_mode = TEST_SSL_MODE_STARTTLS;
+ test_run_client_server(protocol, &smtp_client_set, &smtp_server_set,
+ client_init);
+
+ test_out_reason("parallel pipelining startls",
+ (failure == NULL), failure);
+#endif
+}
+
+static void test_smtp_normal(void)
+{
+ test_begin("smtp payload - normal");
+ test_run_scenarios(SMTP_PROTOCOL_SMTP,
+ SMTP_CAPABILITY_DSN, test_client);
+ test_end();
+}
+
+static void test_smtp_chunking(void)
+{
+ test_begin("smtp payload - chunking");
+ test_run_scenarios(SMTP_PROTOCOL_SMTP,
+ SMTP_CAPABILITY_DSN | SMTP_CAPABILITY_CHUNKING,
+ test_client);
+ test_end();
+}
+
+static void test_lmtp_normal(void)
+{
+ test_begin("lmtp payload - normal");
+ test_run_scenarios(SMTP_PROTOCOL_LMTP,
+ SMTP_CAPABILITY_DSN, test_client);
+ test_end();
+}
+
+static void test_lmtp_chunking(void)
+{
+ test_begin("lmtp payload - chunking");
+ test_run_scenarios(SMTP_PROTOCOL_LMTP,
+ SMTP_CAPABILITY_DSN | SMTP_CAPABILITY_CHUNKING,
+ test_client);
+ test_end();
+}
+
+static void (*const test_functions[])(void) = {
+ test_smtp_normal,
+ test_smtp_chunking,
+ test_lmtp_normal,
+ test_lmtp_chunking,
+ NULL
+};
+
+/*
+ * Main
+ */
+
+static void main_init(void)
+{
+#ifdef HAVE_OPENSSL
+ ssl_iostream_openssl_init();
+#endif
+}
+
+static void main_deinit(void)
+{
+ ssl_iostream_context_cache_free();
+#ifdef HAVE_OPENSSL
+ ssl_iostream_openssl_deinit();
+#endif
+}
+
+int main(int argc, char *argv[])
+{
+ int c;
+ int ret;
+
+ lib_init();
+ main_init();
+
+ while ((c = getopt(argc, argv, "DS")) > 0) {
+ switch (c) {
+ case 'D':
+ debug = TRUE;
+ break;
+ case 'S':
+ small_socket_buffers = TRUE;
+ break;
+ default:
+ i_fatal("Usage: %s [-D][-S]", argv[0]);
+ }
+ }
+
+ test_subprocesses_init(debug);
+
+ /* listen on localhost */
+ i_zero(&bind_ip);
+ bind_ip.family = AF_INET;
+ bind_ip.u.ip4.s_addr = htonl(INADDR_LOOPBACK);
+
+ ret = test_run(test_functions);
+
+ test_subprocesses_deinit();
+ main_deinit();
+ lib_deinit();
+
+ return ret;
+}
diff --git a/src/lib-smtp/test-smtp-reply.c b/src/lib-smtp/test-smtp-reply.c
new file mode 100644
index 0000000..69ded10
--- /dev/null
+++ b/src/lib-smtp/test-smtp-reply.c
@@ -0,0 +1,279 @@
+/* Copyright (c) 2013-2018 Dovecot authors, see the included COPYING file */
+
+#include "test-lib.h"
+#include "array.h"
+#include "buffer.h"
+#include "str.h"
+#include "str-sanitize.h"
+#include "istream.h"
+#include "ostream.h"
+#include "test-common.h"
+#include "smtp-reply-parser.h"
+
+#include <time.h>
+
+struct smtp_reply_parse_valid_test {
+ const char *input, *output;
+ unsigned int status;
+ bool ehlo;
+ size_t max_size;
+ struct {
+ unsigned int x, y, z;
+ } enhanced_code;
+ const char *const *text_lines;
+};
+
+/* Valid reply tests */
+
+static const struct smtp_reply_parse_valid_test
+valid_reply_parse_tests[] = {
+ {
+ .input = "220\r\n",
+ .output = "220 \r\n",
+ .status = 220,
+ .text_lines = (const char *[]){ "", NULL }
+ },{
+ .input = "220 \r\n",
+ .status = 220,
+ .text_lines = (const char *[]){ "", NULL }
+ },{
+ .input = "220 OK\r\n",
+ .status = 220,
+ .text_lines = (const char *[]){ "OK", NULL }
+ },{
+ .input = "550 Requested action not taken: mailbox unavailable\r\n",
+ .status = 550,
+ .text_lines = (const char *[])
+ { "Requested action not taken: mailbox unavailable", NULL }
+ },{
+ .input =
+ "250-smtp.example.com Hello client.example.org [10.0.0.1]\r\n"
+ "250-SIZE 52428800\r\n"
+ "250-PIPELINING\r\n"
+ "250-STARTTLS\r\n"
+ "250 HELP\r\n",
+ .ehlo = TRUE,
+ .status = 250,
+ .text_lines = (const char *[]) {
+ "smtp.example.com Hello client.example.org [10.0.0.1]",
+ "SIZE 52428800",
+ "PIPELINING",
+ "STARTTLS",
+ "HELP",
+ NULL
+ }
+ },{
+ .input =
+ "250-smtp.example.com We got some nice '\x03' and '\x04'\r\n"
+ "250 HELP\r\n",
+ .output =
+ "250-smtp.example.com We got some nice ' ' and ' '\r\n"
+ "250 HELP\r\n",
+ .ehlo = TRUE,
+ .status = 250,
+ .text_lines = (const char *[]) {
+ "smtp.example.com We got some nice ' ' and ' '",
+ "HELP",
+ NULL
+ }
+ },{
+ .input =
+ "250 smtp.example.com We got some nice '\x08'\r\n",
+ .output =
+ "250 smtp.example.com We got some nice ' '\r\n",
+ .ehlo = TRUE,
+ .status = 250,
+ .text_lines = (const char *[]) {
+ "smtp.example.com We got some nice ' '",
+ NULL
+ }
+ },{
+ .input = "250 2.1.0 Originator <frop@example.com> ok\r\n",
+ .status = 250,
+ .enhanced_code = { 2, 1, 0 },
+ .text_lines = (const char *[]){
+ "Originator <frop@example.com> ok", NULL
+ }
+ },{
+ .input =
+ "551-5.7.1 Forwarding to remote hosts disabled\r\n"
+ "551 5.7.1 Select another host to act as your forwarder\r\n",
+ .status = 551,
+ .enhanced_code = { 5, 7, 1 },
+ .text_lines = (const char *[]) {
+ "Forwarding to remote hosts disabled",
+ "Select another host to act as your forwarder",
+ NULL
+ }
+ }
+};
+
+unsigned int valid_reply_parse_test_count =
+ N_ELEMENTS(valid_reply_parse_tests);
+
+static void test_smtp_reply_parse_valid(void)
+{
+ unsigned int i;
+
+ for (i = 0; i < valid_reply_parse_test_count; i++) T_BEGIN {
+ struct istream *input;
+ const struct smtp_reply_parse_valid_test *test;
+ struct smtp_reply_parser *parser;
+ struct smtp_reply *reply;
+ const char *error;
+ int ret;
+
+ test = &valid_reply_parse_tests[i];
+ input = i_stream_create_from_data(test->input,
+ strlen(test->input));
+ parser = smtp_reply_parser_init(input, test->max_size);
+ i_stream_unref(&input);
+
+ test_begin(t_strdup_printf("smtp reply valid [%d]", i));
+
+ if (test->ehlo) {
+ while ((ret=smtp_reply_parse_ehlo
+ (parser, &reply, &error)) > 0) {
+ }
+ } else {
+ while ((ret=smtp_reply_parse_next
+ (parser, test->enhanced_code.x > 0, &reply, &error)) > 0) {
+ }
+ }
+
+ test_out_reason("parse success", ret == 0, error);
+
+ if (ret == 0) {
+ const char *output;
+ string_t *encoded;
+
+ /* verify last response only */
+ test_out(t_strdup_printf("reply->status = %d", test->status),
+ reply->status == test->status);
+ if (test->enhanced_code.x > 0) {
+ test_out(t_strdup_printf("reply->enhanced_code = %d.%d.%d",
+ test->enhanced_code.x, test->enhanced_code.y, test->enhanced_code.z),
+ (reply->enhanced_code.x == test->enhanced_code.x &&
+ reply->enhanced_code.y == test->enhanced_code.y &&
+ reply->enhanced_code.z == test->enhanced_code.z));
+ }
+ if (test->text_lines != NULL) {
+ const char *const *line = test->text_lines;
+ const char *const *reply_line = reply->text_lines;
+ unsigned int index = 0;
+
+ while (*line != NULL) {
+ if (*reply_line == NULL) {
+ test_out(
+ t_strdup_printf("reply->text_lines[%d] = NULL", index),
+ FALSE);
+ break;
+ }
+ test_out(t_strdup_printf(
+ "reply->text_lines[%d] = \"%s\"", index, *reply_line),
+ strcmp(*line, *reply_line) == 0);
+ line++;
+ reply_line++;
+ index++;
+ }
+ } else {
+ test_out("reply->text_lines = NULL", reply->text_lines == NULL);
+ }
+
+ encoded = t_str_new(512);
+ smtp_reply_write(encoded, reply);
+
+ output = (test->output == NULL ? test->input : test->output);
+ test_out("write() = input",
+ strcmp(str_c(encoded), output) == 0);
+ }
+ test_end();
+ smtp_reply_parser_deinit(&parser);
+ } T_END;
+}
+
+struct smtp_reply_parse_invalid_test {
+ const char *reply;
+ bool ehlo;
+ size_t max_size;
+};
+
+static const struct smtp_reply_parse_invalid_test
+ invalid_reply_parse_tests[] = {
+ {
+ .reply = "22X OK\r\n"
+ },{
+ .reply = "220OK\r\n"
+ },{
+ .reply =
+ "200-This is\r\n"
+ "250 inconsistent.\r\n"
+ },{
+ .reply = "400 This \r is wrong\r\n"
+ },{
+ .reply = "500 This is \x03 worse\r\n"
+ },{
+ .reply = "699 Obscure\r\n"
+ },{
+ .reply = "100 Invalid\r\n"
+ },{
+ .reply = "400 Interrupted\r"
+ },{
+ .reply = "251 example.com We got '\x04'\r\n",
+ .ehlo = TRUE
+ },{
+ .reply =
+ "250-example.com Hello\r\n"
+ "250 We got some '\x08' for you\r\n",
+ .ehlo = TRUE
+ },{
+ .reply =
+ "556-This is a very long reply\r\n"
+ "556 that exceeds the very low limit.\r\n",
+ .max_size = 50
+ }
+};
+
+unsigned int invalid_reply_parse_test_count =
+ N_ELEMENTS(invalid_reply_parse_tests);
+
+static void test_smtp_reply_parse_invalid(void)
+{
+ unsigned int i;
+
+ for (i = 0; i < invalid_reply_parse_test_count; i++) T_BEGIN {
+ const struct smtp_reply_parse_invalid_test *test;
+ struct istream *input;
+ struct smtp_reply_parser *parser;
+ struct smtp_reply *reply;
+ const char *reply_text, *error;
+ int ret;
+
+ test = &invalid_reply_parse_tests[i];
+ reply_text = test->reply;
+ input = i_stream_create_from_data(reply_text, strlen(reply_text));
+ parser = smtp_reply_parser_init(input, test->max_size);
+ i_stream_unref(&input);
+
+ test_begin(t_strdup_printf("smtp reply invalid [%d]", i));
+
+ if (test->ehlo)
+ while ((ret=smtp_reply_parse_ehlo(parser, &reply, &error)) > 0);
+ else
+ while ((ret=smtp_reply_parse_next(parser, FALSE, &reply, &error)) > 0);
+ test_out_reason(t_strdup_printf("parse(\"%s\")",
+ str_sanitize(reply_text, 80)), ret < 0, error);
+ test_end();
+ smtp_reply_parser_deinit(&parser);
+ } T_END;
+}
+
+int main(void)
+{
+ static void (*test_functions[])(void) = {
+ test_smtp_reply_parse_valid,
+ test_smtp_reply_parse_invalid,
+ NULL
+ };
+ return test_run(test_functions);
+}
diff --git a/src/lib-smtp/test-smtp-server-errors.c b/src/lib-smtp/test-smtp-server-errors.c
new file mode 100644
index 0000000..d3e528c
--- /dev/null
+++ b/src/lib-smtp/test-smtp-server-errors.c
@@ -0,0 +1,3883 @@
+/* Copyright (c) 2017-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "str.h"
+#include "array.h"
+#include "hostpid.h"
+#include "ioloop.h"
+#include "istream.h"
+#include "ostream.h"
+#include "time-util.h"
+#include "sleep.h"
+#include "connection.h"
+#include "test-common.h"
+#include "test-subprocess.h"
+#include "smtp-address.h"
+#include "smtp-reply-parser.h"
+#include "smtp-server.h"
+
+#include <unistd.h>
+
+#define SERVER_MAX_TIMEOUT_MSECS 10*1000
+#define CLIENT_KILL_TIMEOUT_SECS 20
+
+static void main_deinit(void);
+
+/*
+ * Types
+ */
+
+struct server_connection {
+ void *context;
+};
+
+struct client_connection {
+ struct connection conn;
+ void *context;
+
+ pool_t pool;
+};
+
+typedef void
+(*test_server_init_t)(const struct smtp_server_settings *server_set);
+typedef void (*test_client_init_t)(unsigned int index);
+
+/*
+ * State
+ */
+
+/* common */
+static struct ip_addr bind_ip;
+static in_port_t bind_port = 0;
+static struct ioloop *ioloop;
+static bool debug = FALSE;
+
+/* server */
+static struct smtp_server *smtp_server = NULL;
+static struct io *io_listen;
+static int fd_listen = -1;
+static size_t server_io_buffer_size = 0;
+static struct smtp_server_callbacks server_callbacks;
+static unsigned int server_pending;
+
+/* client */
+static struct connection_list *client_conn_list;
+static unsigned int client_index;
+static void (*test_client_connected)(struct client_connection *conn);
+static void (*test_client_input)(struct client_connection *conn);
+static void (*test_client_deinit)(struct client_connection *conn);
+
+/*
+ * Forward declarations
+ */
+
+/* server */
+static void test_server_defaults(struct smtp_server_settings *smtp_set);
+static void test_server_run(const struct smtp_server_settings *smtp_set);
+
+/* client */
+static void test_client_run(unsigned int index);
+
+/* test*/
+static void
+test_run_client_server(const struct smtp_server_settings *server_set,
+ test_server_init_t server_test,
+ test_client_init_t client_test,
+ unsigned int client_tests_count) ATTR_NULL(3);
+
+/*
+ * Slow server
+ */
+
+/* client */
+
+static void test_slow_server_input(struct client_connection *conn ATTR_UNUSED)
+{
+ /* do nothing */
+ sleep(10);
+}
+
+static void test_slow_server_connected(struct client_connection *conn)
+{
+ if (debug)
+ i_debug("CONNECTED");
+
+ o_stream_nsend_str(conn->conn.output,
+ "EHLO frop\r\n");
+}
+
+static void test_client_slow_server(unsigned int index)
+{
+ test_client_input = test_slow_server_input;
+ test_client_connected = test_slow_server_connected;
+ test_client_run(index);
+}
+
+/* server */
+
+struct _slow_server {
+ struct smtp_server_cmd_ctx *cmd;
+ struct timeout *to_delay;
+ bool serviced:1;
+};
+
+static void
+test_server_slow_server_destroyed(struct smtp_server_cmd_ctx *cmd ATTR_UNUSED,
+ struct _slow_server *ctx)
+{
+ test_assert(ctx->serviced);
+ timeout_remove(&ctx->to_delay);
+ i_free(ctx);
+ io_loop_stop(ioloop);
+}
+
+static void test_server_slow_server_delayed(struct _slow_server *ctx)
+{
+ struct smtp_server_reply *reply;
+ struct smtp_server_cmd_ctx *cmd = ctx->cmd;
+
+ reply = smtp_server_reply_create_ehlo(cmd->cmd);
+ smtp_server_reply_ehlo_add(reply, "FROP");
+
+ smtp_server_reply_submit(reply);
+ ctx->serviced = TRUE;
+}
+
+static int
+test_server_slow_server_cmd_helo(void *conn_ctx ATTR_UNUSED,
+ struct smtp_server_cmd_ctx *cmd,
+ struct smtp_server_cmd_helo *data ATTR_UNUSED)
+{
+ struct _slow_server *ctx;
+
+ if (debug)
+ i_debug("HELO");
+
+ ctx = i_new(struct _slow_server, 1);
+ ctx->cmd = cmd;
+
+ smtp_server_command_add_hook(cmd->cmd,
+ SMTP_SERVER_COMMAND_HOOK_DESTROY,
+ test_server_slow_server_destroyed, ctx);
+
+ ctx->to_delay = timeout_add(4000, test_server_slow_server_delayed, ctx);
+
+ return 0;
+}
+
+static void
+test_server_slow_server(const struct smtp_server_settings *server_set)
+{
+ server_callbacks.conn_cmd_helo = test_server_slow_server_cmd_helo;
+ test_server_run(server_set);
+}
+
+/* test */
+
+static void test_slow_server(void)
+{
+ struct smtp_server_settings smtp_server_set;
+
+ test_server_defaults(&smtp_server_set);
+ smtp_server_set.max_client_idle_time_msecs = 1000;
+
+ test_begin("slow server");
+ test_run_client_server(&smtp_server_set,
+ test_server_slow_server,
+ test_client_slow_server, 1);
+ test_end();
+}
+
+/*
+ * Slow client
+ */
+
+/* client */
+
+static void test_slow_client_input(struct client_connection *conn ATTR_UNUSED)
+{
+ /* nothing */
+}
+
+static void test_slow_client_connected(struct client_connection *conn)
+{
+ if (debug)
+ i_debug("CONNECTED");
+
+ o_stream_nsend_str(conn->conn.output, "EHLO frop\r\n");
+}
+
+static void test_client_slow_client(unsigned int index)
+{
+ test_client_input = test_slow_client_input;
+ test_client_connected = test_slow_client_connected;
+ test_client_run(index);
+}
+
+/* server */
+
+struct _slow_client {
+ struct smtp_server_cmd_ctx *cmd;
+ struct timeout *to_delay;
+ struct timeout *to_disconnect;
+ bool serviced:1;
+};
+
+static void test_server_slow_client_disconnect_timeout(struct _slow_client *ctx)
+{
+ test_assert(FALSE);
+
+ timeout_remove(&ctx->to_disconnect);
+ i_free(ctx);
+ io_loop_stop(ioloop);
+}
+
+static void
+test_server_slow_client_disconnect(void *conn_ctx, const char *reason)
+{
+ struct server_connection *conn = (struct server_connection *)conn_ctx;
+ struct _slow_client *ctx = (struct _slow_client *)conn->context;
+
+ if (debug)
+ i_debug("DISCONNECTED: %s", reason);
+
+ timeout_remove(&ctx->to_disconnect);
+ i_free(ctx);
+ io_loop_stop(ioloop);
+}
+
+static void
+test_server_slow_client_cmd_destroyed(
+ struct smtp_server_cmd_ctx *cmd ATTR_UNUSED, struct _slow_client *ctx)
+{
+ test_assert(ctx->serviced);
+ timeout_remove(&ctx->to_delay);
+}
+
+static void test_server_slow_client_delayed(struct _slow_client *ctx)
+{
+ struct smtp_server_reply *reply;
+ struct smtp_server_cmd_ctx *cmd = ctx->cmd;
+
+ timeout_remove(&ctx->to_delay);
+
+ reply = smtp_server_reply_create_ehlo(cmd->cmd);
+ smtp_server_reply_ehlo_add(reply, "FROP");
+
+ ctx->to_disconnect = timeout_add(
+ 2000, test_server_slow_client_disconnect_timeout, ctx);
+
+ smtp_server_reply_submit(reply);
+ ctx->serviced = TRUE;
+}
+
+static int
+test_server_slow_client_cmd_helo(void *conn_ctx,
+ struct smtp_server_cmd_ctx *cmd,
+ struct smtp_server_cmd_helo *data ATTR_UNUSED)
+{
+ struct server_connection *conn = (struct server_connection *)conn_ctx;
+ struct _slow_client *ctx;
+
+ if (debug)
+ i_debug("HELO");
+
+ ctx = i_new(struct _slow_client, 1);
+ ctx->cmd = cmd;
+
+ conn->context = ctx;
+
+ smtp_server_command_add_hook(cmd->cmd,
+ SMTP_SERVER_COMMAND_HOOK_DESTROY,
+ test_server_slow_client_cmd_destroyed,
+ ctx);
+
+ ctx->to_delay = timeout_add_short(500,
+ test_server_slow_client_delayed, ctx);
+
+ return 0;
+}
+
+static void
+test_server_slow_client(const struct smtp_server_settings *server_set)
+{
+ server_callbacks.conn_disconnect = test_server_slow_client_disconnect;
+ server_callbacks.conn_cmd_helo = test_server_slow_client_cmd_helo;
+ test_server_run(server_set);
+}
+
+/* test */
+
+static void test_slow_client(void)
+{
+ struct smtp_server_settings smtp_server_set;
+
+ test_server_defaults(&smtp_server_set);
+ smtp_server_set.max_client_idle_time_msecs = 1000;
+
+ test_begin("slow client");
+ test_run_client_server(&smtp_server_set,
+ test_server_slow_client,
+ test_client_slow_client, 1);
+ test_end();
+}
+
+/*
+ * Hanging command payload
+ */
+
+/* client */
+
+static void
+test_hanging_command_payload_connected(struct client_connection *conn)
+{
+ o_stream_nsend_str(conn->conn.output,
+ "EHLO frop\r\n"
+ "MAIL FROM:<hangman@example.com>\r\n"
+ "RCPT TO:<jerry@example.com>\r\n"
+ "DATA\r\n"
+ "To be continued... or not");
+}
+
+static void test_client_hanging_command_payload(unsigned int index)
+{
+ test_client_connected = test_hanging_command_payload_connected;
+ test_client_run(index);
+}
+
+/* server */
+
+struct _hanging_command_payload {
+ struct istream *payload_input;
+ struct io *io;
+
+ bool serviced:1;
+};
+
+static void
+test_server_hanging_command_payload_trans_free(
+ void *conn_ctx ATTR_UNUSED, struct smtp_server_transaction *trans)
+{
+ struct _hanging_command_payload *ctx =
+ (struct _hanging_command_payload *)trans->context;
+
+ test_assert(!ctx->serviced);
+ i_free(ctx);
+ io_loop_stop(ioloop);
+}
+
+static int
+test_server_hanging_command_payload_rcpt(
+ void *conn_ctx ATTR_UNUSED, struct smtp_server_cmd_ctx *cmd ATTR_UNUSED,
+ struct smtp_server_recipient *rcpt)
+{
+ if (debug) {
+ i_debug("RCPT TO:%s",
+ smtp_address_encode(rcpt->path));
+ }
+
+ return 1;
+}
+
+static int
+test_server_hanging_command_payload_data_begin(
+ void *conn_ctx ATTR_UNUSED, struct smtp_server_cmd_ctx *cmd ATTR_UNUSED,
+ struct smtp_server_transaction *trans, struct istream *data_input)
+{
+ struct _hanging_command_payload *ctx;
+
+ if (debug)
+ i_debug("DATA");
+
+ ctx = i_new(struct _hanging_command_payload, 1);
+ trans->context = ctx;
+
+ ctx->payload_input = data_input;
+ return 0;
+}
+
+static int
+test_server_hanging_command_payload_data_continue(
+ void *conn_ctx ATTR_UNUSED, struct smtp_server_cmd_ctx *cmd,
+ struct smtp_server_transaction *trans)
+{
+ struct _hanging_command_payload *ctx =
+ (struct _hanging_command_payload *)trans->context;
+ const unsigned char *data;
+ size_t size;
+ int ret;
+
+
+ if (debug)
+ i_debug("DATA continue");
+
+ while ((ret = i_stream_read_data(ctx->payload_input,
+ &data, &size, 0)) > 0)
+ i_stream_skip(ctx->payload_input, size);
+
+ if (ret == 0)
+ return 0;
+ if (ctx->payload_input->stream_errno != 0) {
+ i_error("failed to read DATA payload: %s",
+ i_stream_get_error(ctx->payload_input));
+ return -1;
+ }
+
+ i_assert(ctx->payload_input->eof);
+
+ smtp_server_reply(cmd, 250, "2.0.0", "OK");
+ ctx->serviced = TRUE;
+
+ i_stream_unref(&ctx->payload_input);
+ return 1;
+}
+
+static void
+test_server_hanging_command_payload(
+ const struct smtp_server_settings *server_set)
+{
+ server_callbacks.conn_trans_free =
+ test_server_hanging_command_payload_trans_free;
+
+ server_callbacks.conn_cmd_rcpt =
+ test_server_hanging_command_payload_rcpt;
+ server_callbacks.conn_cmd_data_begin =
+ test_server_hanging_command_payload_data_begin;
+ server_callbacks.conn_cmd_data_continue =
+ test_server_hanging_command_payload_data_continue;
+ test_server_run(server_set);
+}
+
+/* test */
+
+static void test_hanging_command_payload(void)
+{
+ struct smtp_server_settings smtp_server_set;
+
+ test_server_defaults(&smtp_server_set);
+ smtp_server_set.max_client_idle_time_msecs = 1000;
+
+ test_begin("hanging command payload");
+ test_run_client_server(&smtp_server_set,
+ test_server_hanging_command_payload,
+ test_client_hanging_command_payload, 1);
+ test_end();
+}
+
+/*
+ * Bad command
+ */
+
+/* client */
+
+static void
+test_bad_command_connected(struct client_connection *conn)
+{
+ o_stream_nsend_str(conn->conn.output, "EHLO\tfrop\r\n");
+}
+
+static void test_client_bad_command(unsigned int index)
+{
+ test_client_connected = test_bad_command_connected;
+ test_client_run(index);
+}
+
+/* server */
+
+struct _bad_command {
+ struct istream *payload_input;
+ struct io *io;
+
+ bool serviced:1;
+};
+
+static void
+test_server_bad_command_disconnect(void *context ATTR_UNUSED,
+ const char *reason)
+{
+ if (debug)
+ i_debug("Disconnect: %s", reason);
+ io_loop_stop(ioloop);
+}
+
+static int
+test_server_bad_command_helo(void *conn_ctx ATTR_UNUSED,
+ struct smtp_server_cmd_ctx *cmd ATTR_UNUSED,
+ struct smtp_server_cmd_helo *data ATTR_UNUSED)
+{
+ test_assert(FALSE);
+ return 1;
+}
+
+static int
+test_server_bad_command_rcpt(void *conn_ctx ATTR_UNUSED,
+ struct smtp_server_cmd_ctx *cmd ATTR_UNUSED,
+ struct smtp_server_recipient *rcpt ATTR_UNUSED)
+{
+ return 1;
+}
+
+static int
+test_server_bad_command_data_begin(
+ void *conn_ctx ATTR_UNUSED, struct smtp_server_cmd_ctx *cmd,
+ struct smtp_server_transaction *trans ATTR_UNUSED,
+ struct istream *data_input ATTR_UNUSED)
+{
+ smtp_server_reply(cmd, 250, "2.0.0", "OK");
+ return 1;
+}
+
+static void
+test_server_bad_command(const struct smtp_server_settings *server_set)
+{
+ server_callbacks.conn_disconnect =
+ test_server_bad_command_disconnect;
+
+ server_callbacks.conn_cmd_helo =
+ test_server_bad_command_helo;
+ server_callbacks.conn_cmd_rcpt =
+ test_server_bad_command_rcpt;
+ server_callbacks.conn_cmd_data_begin =
+ test_server_bad_command_data_begin;
+ test_server_run(server_set);
+}
+
+/* test */
+
+static void test_bad_command(void)
+{
+ struct smtp_server_settings smtp_server_set;
+
+ test_server_defaults(&smtp_server_set);
+ smtp_server_set.max_client_idle_time_msecs = 1000;
+
+ test_begin("bad command");
+ test_run_client_server(&smtp_server_set,
+ test_server_bad_command,
+ test_client_bad_command, 1);
+ test_end();
+}
+
+/*
+ * Many bad commands
+ */
+
+/* client */
+
+struct _many_bad_commands_client {
+ struct smtp_reply_parser *parser;
+ unsigned int reply;
+ bool replied:1;
+};
+
+static void
+test_many_bad_commands_client_input(struct client_connection *conn)
+{
+ struct _many_bad_commands_client *ctx = conn->context;
+ struct smtp_reply *reply;
+ const char *error;
+ int ret;
+
+ while ((ret=smtp_reply_parse_next(ctx->parser, FALSE,
+ &reply, &error)) > 0) {
+ if (debug)
+ i_debug("REPLY #%u: %s", ctx->reply, smtp_reply_log(reply));
+
+ switch (ctx->reply++) {
+ /* greeting */
+ case 0:
+ i_assert(reply->status == 220);
+ break;
+ /* bad command reply */
+ case 1: case 2: case 3: case 4: case 5:
+ case 6: case 7: case 8: case 9: case 10:
+ i_assert(reply->status == 500);
+ break;
+ case 11:
+ i_assert(reply->status == 421);
+ ctx->replied = TRUE;
+ io_loop_stop(ioloop);
+ connection_disconnect(&conn->conn);
+ return;
+ default:
+ i_unreached();
+ }
+ }
+
+ i_assert(ret >= 0);
+}
+
+static void
+test_many_bad_commands_client_connected(struct client_connection *conn)
+{
+ struct _many_bad_commands_client *ctx;
+
+ ctx = p_new(conn->pool, struct _many_bad_commands_client, 1);
+ ctx->parser = smtp_reply_parser_init(conn->conn.input, SIZE_MAX);
+ conn->context = ctx;
+
+ switch (client_index) {
+ case 0:
+ o_stream_nsend_str(conn->conn.output,
+ "\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n");
+ break;
+ case 1:
+ o_stream_nsend_str(conn->conn.output,
+ "a\r\nb\r\nc\r\nd\r\ne\r\nf\r\ng\r\nh\r\n"
+ "i\r\nj\r\nk\r\nl\r\nm\r\nn\r\no\r\np\r\n");
+ break;
+ default:
+ i_unreached();
+ }
+}
+
+static void
+test_many_bad_commands_client_deinit(struct client_connection *conn)
+{
+ struct _many_bad_commands_client *ctx = conn->context;
+
+ i_assert(ctx->replied);
+ smtp_reply_parser_deinit(&ctx->parser);
+}
+
+static void test_client_many_bad_commands(unsigned int index)
+{
+ test_client_input = test_many_bad_commands_client_input;
+ test_client_connected = test_many_bad_commands_client_connected;
+ test_client_deinit = test_many_bad_commands_client_deinit;
+ test_client_run(index);
+}
+
+/* server */
+
+static void
+test_server_many_bad_commands_disconnect(void *context ATTR_UNUSED,
+ const char *reason)
+{
+ struct server_connection *sconn = context;
+
+ if (debug)
+ i_debug("Disconnect: %s", reason);
+
+ sconn->context = POINTER_CAST(POINTER_CAST_TO(sconn->context,
+ unsigned int) + 1);
+
+ if (POINTER_CAST_TO(sconn->context, unsigned int) == 2)
+ io_loop_stop(ioloop);
+}
+
+static int
+test_server_many_bad_commands_helo(
+ void *conn_ctx ATTR_UNUSED, struct smtp_server_cmd_ctx *cmd ATTR_UNUSED,
+ struct smtp_server_cmd_helo *data ATTR_UNUSED)
+{
+ test_assert(FALSE);
+ return 1;
+}
+
+static int
+test_server_many_bad_commands_rcpt(
+ void *conn_ctx ATTR_UNUSED, struct smtp_server_cmd_ctx *cmd ATTR_UNUSED,
+ struct smtp_server_recipient *rcpt ATTR_UNUSED)
+{
+ test_assert(FALSE);
+ return 1;
+}
+
+static int
+test_server_many_bad_commands_data_begin(
+ void *conn_ctx ATTR_UNUSED, struct smtp_server_cmd_ctx *cmd ATTR_UNUSED,
+ struct smtp_server_transaction *trans ATTR_UNUSED,
+ struct istream *data_input ATTR_UNUSED)
+{
+ test_assert(FALSE);
+ return 1;
+}
+
+static void test_server_many_bad_commands
+(const struct smtp_server_settings *server_set)
+{
+ server_callbacks.conn_disconnect =
+ test_server_many_bad_commands_disconnect;
+
+ server_callbacks.conn_cmd_helo =
+ test_server_many_bad_commands_helo;
+ server_callbacks.conn_cmd_rcpt =
+ test_server_many_bad_commands_rcpt;
+ server_callbacks.conn_cmd_data_begin =
+ test_server_many_bad_commands_data_begin;
+ test_server_run(server_set);
+}
+
+/* test */
+
+static void test_many_bad_commands(void)
+{
+ struct smtp_server_settings smtp_server_set;
+
+ test_server_defaults(&smtp_server_set);
+ smtp_server_set.max_client_idle_time_msecs = 1000;
+ smtp_server_set.max_bad_commands = 10;
+
+ test_begin("many bad commands");
+ test_run_client_server(&smtp_server_set,
+ test_server_many_bad_commands,
+ test_client_many_bad_commands, 2);
+ test_end();
+}
+
+/*
+ * Long command
+ */
+
+/* client */
+
+static void test_long_command_connected(struct client_connection *conn)
+{
+ o_stream_nsend_str(
+ conn->conn.output,
+ "EHLO some.very.very.very.very.very.long.domain\r\n");
+}
+
+static void test_client_long_command(unsigned int index)
+{
+ test_client_connected = test_long_command_connected;
+ test_client_run(index);
+}
+
+/* server */
+
+struct _long_command {
+ struct istream *payload_input;
+ struct io *io;
+
+ bool serviced:1;
+};
+
+static void
+test_server_long_command_disconnect(void *context ATTR_UNUSED,
+ const char *reason)
+{
+ if (debug)
+ i_debug("Disconnect: %s", reason);
+ io_loop_stop(ioloop);
+}
+
+static int
+test_server_long_command_helo(void *conn_ctx ATTR_UNUSED,
+ struct smtp_server_cmd_ctx *cmd ATTR_UNUSED,
+ struct smtp_server_cmd_helo *data ATTR_UNUSED)
+{
+ test_assert(FALSE);
+ return 1;
+}
+
+static int
+test_server_long_command_rcpt(void *conn_ctx ATTR_UNUSED,
+ struct smtp_server_cmd_ctx *cmd ATTR_UNUSED,
+ struct smtp_server_recipient *rcpt ATTR_UNUSED)
+{
+ return 1;
+}
+
+static int
+test_server_long_command_data_begin(
+ void *conn_ctx ATTR_UNUSED, struct smtp_server_cmd_ctx *cmd,
+ struct smtp_server_transaction *trans ATTR_UNUSED,
+ struct istream *data_input ATTR_UNUSED)
+{
+ smtp_server_reply(cmd, 250, "2.0.0", "OK");
+ return 1;
+}
+
+static void
+test_server_long_command(const struct smtp_server_settings *server_set)
+{
+ server_callbacks.conn_disconnect =
+ test_server_long_command_disconnect;
+
+ server_callbacks.conn_cmd_helo =
+ test_server_long_command_helo;
+ server_callbacks.conn_cmd_rcpt =
+ test_server_long_command_rcpt;
+ server_callbacks.conn_cmd_data_begin =
+ test_server_long_command_data_begin;
+ test_server_run(server_set);
+}
+
+/* test */
+
+static void test_long_command(void)
+{
+ struct smtp_server_settings smtp_server_set;
+
+ test_server_defaults(&smtp_server_set);
+ smtp_server_set.max_client_idle_time_msecs = 1000;
+ smtp_server_set.command_limits.max_parameters_size = 32;
+
+ test_begin("long command");
+ test_run_client_server(&smtp_server_set,
+ test_server_long_command,
+ test_client_long_command, 1);
+ test_end();
+}
+
+/*
+ * Long auth line
+ */
+
+/* client */
+
+#define _LONG_AUTH_LINE_DATA \
+ "dXNlcj10ZXN0dXNlcjEBYXV0aD1CZWFyZXIgZXlKaGJHY2lPaUpTVXpJMU5pSXNJ" \
+ "blI1Y0NJZ09pQWlTbGRVSWl3aWEybGtJaUE2SUNKdVRIRlVlRnBXWVhKSlgwWndS" \
+ "a0Z3Umt3MloyUnhiak4xV1VSS2R6WnNWVjlMYVZoa2JWazJialpSSW4wLmV5Smxl" \
+ "SEFpT2pFMk16UTJNemMyTlRFc0ltbGhkQ0k2TVRZek5EWXpOek0xTVN3aWFuUnBJ" \
+ "am9pT1RFM1lUYzFaalF0WTJZME9DMDBOVEEyTFRnNVpXSXRNRE13WldaaU5tSTVO" \
+ "MlZrSWl3aWFYTnpJam9pYUhSMGNEb3ZMekU1TWk0eE5qZ3VNUzR5TVRveE9EQTRN" \
+ "QzloZFhSb0wzSmxZV3h0Y3k5eVpXeDBaWE4wSWl3aVlYVmtJam9pWVdOamIzVnVk" \
+ "Q0lzSW5OMVlpSTZJamhsWVRRME1UWTNMVGN6TTJVdE5EVTBZeTFpT0dJMUxXTmpa" \
+ "bVl3WkRnek1URTVaQ0lzSW5SNWNDSTZJa0psWVhKbGNpSXNJbUY2Y0NJNkltUnZk" \
+ "bVZqYjNRaUxDSnpaWE56YVc5dVgzTjBZWFJsSWpvaU1tTTNPVEUzWldJdE16QTFO" \
+ "UzAwTkRZeExXSXdZell0WTJVeFlUbGlNVEZoTWpReklpd2lZV055SWpvaU1TSXNJ" \
+ "bkpsWVd4dFgyRmpZMlZ6Y3lJNmV5SnliMnhsY3lJNld5SnZabVpzYVc1bFgyRmpZ" \
+ "MlZ6Y3lJc0luVnRZVjloZFhSb2IzSnBlbUYwYVc5dUlsMTlMQ0p5WlhOdmRYSmpa" \
+ "VjloWTJObGMzTWlPbnNpWVdOamIzVnVkQ0k2ZXlKeWIyeGxjeUk2V3lKdFlXNWha" \
+ "MlV0WVdOamIzVnVkQ0lzSW0xaGJtRm5aUzFoWTJOdmRXNTBMV3hwYm10eklpd2lk" \
+ "bWxsZHkxd2NtOW1hV3hsSWwxOWZTd2ljMk52Y0dVaU9pSndjbTltYVd4bElHVnRZ" \
+ "V2xzSWl3aVpXMWhhV3hmZG1WeWFXWnBaV1FpT21aaGJITmxMQ0p1WVcxbElqb2lk" \
+ "R1Z6ZEhWelpYSXhJRUYxZEc5SFpXNWxjbUYwWldRaUxDSndjbVZtWlhKeVpXUmZk" \
+ "WE5sY201aGJXVWlPaUowWlhOMGRYTmxjakVpTENKbmFYWmxibDl1WVcxbElqb2lk" \
+ "R1Z6ZEhWelpYSXhJaXdpWm1GdGFXeDVYMjVoYldVaU9pSkJkWFJ2UjJWdVpYSmhk" \
+ "R1ZrSWl3aVpXMWhhV3dpT2lKMFpYTjBkWE5sY2pGQWJYbGtiMjFoYVc0dWIzZ2lm" \
+ "US5ta2JGSURpT0FhbENCcVMwODRhVHJURjBIdDk1c1Z4cGlSbTFqZnhJd0JiN1hM" \
+ "M2gzWUJkdXVrVXlZdDJqX1pqUFlhMDhDcVVYNWFrLVBOSjdSVWRTUXNmUlgwM1Zi" \
+ "cXA4MHFZZjNGYzJpcDR0YmhHLXFEV0R6NzdhZDhWcEFNei16YWlSamZCclZ2R3hB" \
+ "T3ZsZnFDVWhaZTJDR3ZqWjZ1Q3RKTlFaS0dyazZHOXoxX2pqekZkTjBXWjUxbEZs" \
+ "US1JdE5LREpoTjNIekJ5SW93M19qQU9kWEI0R0w4R3JHM1hqU09rSFVRam5GTEQw" \
+ "QUF1QXY4SkxmTXY1NGc1a2tKaklxRFgxZlgyWVo0Y2JQOWV3TUp6UV84ZWdLeW5T" \
+ "VV9XSk8xRU9Qa1NVZjlMX19RX3FwY0dNbzFtTkxuTURKUlU2dmZFY3JrM2k0cVNz" \
+ "MXRPdHdLaHcBAQ"
+
+struct _long_auth_line_client {
+ struct smtp_reply_parser *parser;
+ unsigned int reply;
+
+ bool replied:1;
+};
+
+static void test_long_auth_line_client_input(struct client_connection *conn)
+{
+ struct _long_auth_line_client *ctx = conn->context;
+ struct smtp_reply *reply;
+ const char *error;
+ int ret;
+
+ while ((ret = smtp_reply_parse_next(ctx->parser, FALSE,
+ &reply, &error)) > 0) {
+ if (debug)
+ i_debug("REPLY: %s", smtp_reply_log(reply));
+
+ switch (ctx->reply++) {
+ case 0: /* greeting */
+ i_assert(reply->status == 220);
+ break;
+ case 1: /* EHLO reply */
+ i_assert(reply->status == 250);
+ break;
+ case 2: /* AUTH continue */
+ i_assert(reply->status == 334);
+ break;
+ case 3: /* AUTH reply */
+ switch (client_index) {
+ case 0:
+ i_assert(reply->status == 235);
+ break;
+ case 1:
+ i_assert(reply->status == 235);
+ break;
+ case 2:
+ i_assert(reply->status == 500);
+ ctx->replied = TRUE;
+ io_loop_stop(ioloop);
+ connection_disconnect(&conn->conn);
+ return;
+ default:
+ i_unreached();
+ }
+ break;
+ case 4: /* MAIL reply */
+ i_assert(client_index < 2);
+ i_assert(reply->status == 250);
+ break;
+ case 5: /* RCPT reply */
+ i_assert(client_index < 2);
+ i_assert(reply->status == 250);
+ break;
+ case 6: /* DATA initial reply */
+ i_assert(client_index < 2);
+ i_assert(reply->status == 354);
+ break;
+ case 7: /* DATA reply */
+ i_assert(client_index < 2);
+ i_assert(reply->status == 250);
+ break;
+ case 8: /* QUIT reply */
+ i_assert(client_index < 2);
+ i_assert(reply->status == 221);
+ ctx->replied = TRUE;
+ io_loop_stop(ioloop);
+ connection_disconnect(&conn->conn);
+ return;
+ default:
+ i_unreached();
+ }
+ }
+
+ i_assert(ret >= 0);
+}
+
+static void test_long_auth_line_client_connected(struct client_connection *conn)
+{
+ struct _long_auth_line_client *ctx;
+ unsigned int i;
+
+ ctx = p_new(conn->pool, struct _long_auth_line_client, 1);
+ ctx->parser = smtp_reply_parser_init(conn->conn.input, SIZE_MAX);
+ conn->context = ctx;
+
+ o_stream_nsend_str(
+ conn->conn.output,
+ "EHLO frop\r\n"
+ "AUTH XOAUTH2\r\n");
+ for (i = 0; i < (client_index > 1 ? 6 : 1); i++)
+ o_stream_nsend_str(conn->conn.output, _LONG_AUTH_LINE_DATA);
+ o_stream_nsend_str(
+ conn->conn.output,
+ "==");
+ if (client_index == 1) {
+ o_stream_nsend_str(
+ conn->conn.output,
+ " ");
+ }
+ o_stream_nsend_str(
+ conn->conn.output,
+ "\r\n"
+ "MAIL FROM:<user@example.com>\r\n"
+ "RCPT TO:<user@example.com>\r\n"
+ "DATA\r\n"
+ "frop\r\n"
+ ".\r\n"
+ "QUIT\r\n");
+}
+
+static void test_long_auth_line_client_deinit(struct client_connection *conn)
+{
+ struct _long_auth_line_client *ctx = conn->context;
+
+ i_assert(ctx->replied);
+ smtp_reply_parser_deinit(&ctx->parser);
+}
+
+static void test_client_long_auth_line(unsigned int index)
+{
+ test_client_input = test_long_auth_line_client_input;
+ test_client_connected = test_long_auth_line_client_connected;
+ test_client_deinit = test_long_auth_line_client_deinit;
+ test_client_run(index);
+}
+
+/* server */
+
+struct _long_auth_line {
+ struct istream *payload_input;
+};
+
+static void
+test_server_long_auth_line_disconnect(void *context ATTR_UNUSED,
+ const char *reason)
+{
+ if (debug)
+ i_debug("Disconnect: %s", reason);
+}
+
+static int
+test_server_long_auth_line_helo(void *conn_ctx ATTR_UNUSED,
+ struct smtp_server_cmd_ctx *cmd ATTR_UNUSED,
+ struct smtp_server_cmd_helo *data ATTR_UNUSED)
+{
+ return 1;
+}
+
+static int
+test_server_long_auth_line_auth(void *conn_ctx ATTR_UNUSED,
+ struct smtp_server_cmd_ctx *cmd,
+ struct smtp_server_cmd_auth *data ATTR_UNUSED)
+{
+ smtp_server_cmd_auth_send_challenge(cmd, "");
+ return 0;
+}
+
+static int
+test_server_long_auth_line_auth_continue(void *conn_ctx ATTR_UNUSED,
+ struct smtp_server_cmd_ctx *cmd,
+ const char *response)
+{
+ if (strcmp(response, _LONG_AUTH_LINE_DATA"==") == 0)
+ smtp_server_cmd_auth_success(cmd, "user", NULL);
+ else {
+ smtp_server_reply(cmd, 535, "5.7.8",
+ "Authentication credentials invalid");
+ }
+ return 1;
+}
+
+static int
+test_server_long_auth_line_rcpt(void *conn_ctx ATTR_UNUSED,
+ struct smtp_server_cmd_ctx *cmd ATTR_UNUSED,
+ struct smtp_server_recipient *rcpt ATTR_UNUSED)
+{
+ if (debug)
+ i_debug("RCPT");
+ return 1;
+}
+
+static int
+test_server_long_auth_line_data_begin(void *conn_ctx ATTR_UNUSED,
+ struct smtp_server_cmd_ctx *cmd ATTR_UNUSED,
+ struct smtp_server_transaction *trans,
+ struct istream *data_input)
+{
+ struct _long_auth_line *ctx;
+
+ if (debug)
+ i_debug("DATA");
+
+ ctx = p_new(trans->pool, struct _long_auth_line, 1);
+ trans->context = ctx;
+
+ ctx->payload_input = data_input;
+ return 0;
+}
+
+static int
+test_server_long_auth_line_data_continue(void *conn_ctx ATTR_UNUSED,
+ struct smtp_server_cmd_ctx *cmd,
+ struct smtp_server_transaction *trans)
+{
+ struct _long_auth_line *ctx = (struct _long_auth_line *)trans->context;
+ struct istream *data_input = ctx->payload_input;
+ size_t size;
+ ssize_t ret;
+
+ if (debug)
+ i_debug("DATA continue");
+
+ while ((ret = i_stream_read(data_input)) > 0 || ret == -2) {
+ (void)i_stream_get_data(data_input, &size);
+ i_stream_skip(data_input, size);
+ if (!smtp_server_cmd_data_check_size(cmd))
+ return -1;
+ }
+
+ if (ret == 0)
+ return 0;
+ if (ret < 0 && data_input->stream_errno != 0) {
+ /* Client probably disconnected */
+ return -1;
+ }
+
+ smtp_server_reply_all(cmd, 250, "2.0.0", "Accepted");
+ return 1;
+}
+
+static void
+test_server_long_auth_line(const struct smtp_server_settings *server_set)
+{
+ server_callbacks.conn_disconnect =
+ test_server_long_auth_line_disconnect;
+
+ server_callbacks.conn_cmd_helo =
+ test_server_long_auth_line_helo;
+ server_callbacks.conn_cmd_auth =
+ test_server_long_auth_line_auth;
+ server_callbacks.conn_cmd_auth_continue =
+ test_server_long_auth_line_auth_continue;
+ server_callbacks.conn_cmd_rcpt =
+ test_server_long_auth_line_rcpt;
+ server_callbacks.conn_cmd_data_begin =
+ test_server_long_auth_line_data_begin;
+ server_callbacks.conn_cmd_data_continue =
+ test_server_long_auth_line_data_continue;
+ test_server_run(server_set);
+}
+
+static void
+test_server_long_auth_line_small_buf(
+ const struct smtp_server_settings *server_set)
+{
+ server_io_buffer_size = 1024;
+
+ server_callbacks.conn_disconnect =
+ test_server_long_auth_line_disconnect;
+
+ server_callbacks.conn_cmd_helo =
+ test_server_long_auth_line_helo;
+ server_callbacks.conn_cmd_auth =
+ test_server_long_auth_line_auth;
+ server_callbacks.conn_cmd_auth_continue =
+ test_server_long_auth_line_auth_continue;
+ server_callbacks.conn_cmd_rcpt =
+ test_server_long_auth_line_rcpt;
+ server_callbacks.conn_cmd_data_begin =
+ test_server_long_auth_line_data_begin;
+ server_callbacks.conn_cmd_data_continue =
+ test_server_long_auth_line_data_continue;
+ test_server_run(server_set);
+}
+
+/* test */
+
+static void test_long_auth_line(void)
+{
+ struct smtp_server_settings smtp_server_set;
+
+ test_server_defaults(&smtp_server_set);
+ smtp_server_set.capabilities = SMTP_CAPABILITY_AUTH;
+ smtp_server_set.max_client_idle_time_msecs = 1000;
+
+ test_begin("long auth line");
+ test_run_client_server(&smtp_server_set,
+ test_server_long_auth_line,
+ test_client_long_auth_line, 3);
+ test_end();
+}
+
+static void test_long_auth_line_small_buf(void)
+{
+ struct smtp_server_settings smtp_server_set;
+
+ test_server_defaults(&smtp_server_set);
+ smtp_server_set.capabilities = SMTP_CAPABILITY_AUTH;
+ smtp_server_set.max_client_idle_time_msecs = 1000;
+
+ test_begin("long auth line (small i/o buffers)");
+ test_run_client_server(&smtp_server_set,
+ test_server_long_auth_line_small_buf,
+ test_client_long_auth_line, 3);
+ test_end();
+}
+
+
+/*
+ * Big data
+ */
+
+/* client */
+
+static void
+test_big_data_connected(struct client_connection *conn)
+{
+ o_stream_nsend_str(conn->conn.output,
+ "EHLO frop\r\n"
+ "MAIL FROM:<sender@example.com>\r\n"
+ "RCPT TO:<recipient@example.com>\r\n"
+ "DATA\r\n"
+ "0123456789ABCDEF0123456789ABCDEF\r\n"
+ "0123456789ABCDEF0123456789ABCDEF\r\n"
+ "0123456789ABCDEF0123456789ABCDEF\r\n"
+ "0123456789ABCDEF0123456789ABCDEF\r\n"
+ "0123456789ABCDEF0123456789ABCDEF\r\n"
+ "0123456789ABCDEF0123456789ABCDEF\r\n"
+ "0123456789ABCDEF0123456789ABCDEF\r\n"
+ ".\r\n");
+}
+
+static void test_client_big_data(unsigned int index)
+{
+ test_client_connected = test_big_data_connected;
+ test_client_run(index);
+}
+
+/* server */
+
+struct _big_data {
+ struct istream *payload_input;
+ struct io *io;
+};
+
+static void
+test_server_big_data_trans_free(void *conn_ctx ATTR_UNUSED,
+ struct smtp_server_transaction *trans)
+{
+ struct _big_data *ctx = (struct _big_data *)trans->context;
+
+ i_free(ctx);
+ io_loop_stop(ioloop);
+}
+
+static int
+test_server_big_data_rcpt(void *conn_ctx ATTR_UNUSED,
+ struct smtp_server_cmd_ctx *cmd ATTR_UNUSED,
+ struct smtp_server_recipient *rcpt)
+{
+ if (debug) {
+ i_debug("RCPT TO:%s",
+ smtp_address_encode(rcpt->path));
+ }
+ return 1;
+}
+
+static int
+test_server_big_data_data_begin(void *conn_ctx ATTR_UNUSED,
+ struct smtp_server_cmd_ctx *cmd ATTR_UNUSED,
+ struct smtp_server_transaction *trans,
+ struct istream *data_input)
+{
+ struct _big_data *ctx;
+
+ if (debug)
+ i_debug("DATA");
+
+ ctx = i_new(struct _big_data, 1);
+ trans->context = ctx;
+
+ ctx->payload_input = data_input;
+ return 0;
+}
+
+static int
+test_server_big_data_data_continue(void *conn_ctx ATTR_UNUSED,
+ struct smtp_server_cmd_ctx *cmd,
+ struct smtp_server_transaction *trans)
+{
+ static const size_t max_size = 32;
+ struct _big_data *ctx = (struct _big_data *)trans->context;
+ const unsigned char *data;
+ size_t size;
+ int ret;
+
+
+ if (debug)
+ i_debug("DATA continue");
+
+ while (ctx->payload_input->v_offset < max_size &&
+ (ret = i_stream_read_data(ctx->payload_input,
+ &data, &size, 0)) > 0) {
+ if (ctx->payload_input->v_offset + size > max_size) {
+ if (ctx->payload_input->v_offset >= max_size)
+ size = 0;
+ else
+ size = max_size - ctx->payload_input->v_offset;
+ }
+ i_stream_skip(ctx->payload_input, size);
+
+ if (ctx->payload_input->v_offset >= max_size)
+ break;
+ }
+
+ if (ctx->payload_input->v_offset >= max_size) {
+ smtp_server_reply_early(cmd, 552, "5.3.4",
+ "Message too big for system");
+ return -1;
+ }
+
+ if (ret == 0)
+ return 0;
+
+ test_assert(FALSE);
+ return 1;
+}
+
+static void test_server_big_data(const struct smtp_server_settings *server_set)
+{
+ server_callbacks.conn_trans_free =
+ test_server_big_data_trans_free;
+
+ server_callbacks.conn_cmd_rcpt =
+ test_server_big_data_rcpt;
+ server_callbacks.conn_cmd_data_begin =
+ test_server_big_data_data_begin;
+ server_callbacks.conn_cmd_data_continue =
+ test_server_big_data_data_continue;
+ test_server_run(server_set);
+}
+
+/* test */
+
+static void test_big_data(void)
+{
+ struct smtp_server_settings smtp_server_set;
+
+ test_server_defaults(&smtp_server_set);
+ smtp_server_set.max_client_idle_time_msecs = 1000;
+ smtp_server_set.command_limits.max_data_size = 64;
+
+ test_begin("big_data");
+ test_run_client_server(&smtp_server_set,
+ test_server_big_data,
+ test_client_big_data, 1);
+ test_end();
+}
+
+/*
+ * Bad HELO
+ */
+
+/* client */
+
+struct _bad_helo_client {
+ struct smtp_reply_parser *parser;
+ unsigned int reply;
+
+ bool replied:1;
+};
+
+static void test_bad_helo_client_input(struct client_connection *conn)
+{
+ struct _bad_helo_client *ctx = conn->context;
+ struct smtp_reply *reply;
+ const char *error;
+ int ret;
+
+ for (;;) {
+ if (ctx->reply != 1 ||
+ client_index == 0 || client_index == 2) {
+ ret = smtp_reply_parse_next(ctx->parser, FALSE, &reply,
+ &error);
+ } else {
+ ret = smtp_reply_parse_ehlo(ctx->parser, &reply,
+ &error);
+ }
+ if (ret <= 0)
+ break;
+
+ if (debug)
+ i_debug("REPLY: %s", smtp_reply_log(reply));
+
+ switch (ctx->reply++) {
+ case 0: /* greeting */
+ i_assert(reply->status == 220);
+ break;
+ case 1: /* bad command reply */
+ switch (client_index) {
+ case 0: case 1:
+ i_assert(reply->status == 501);
+ break;
+ case 2: case 3:
+ i_assert(reply->status == 250);
+ break;
+ default:
+ i_unreached();
+ }
+ if (debug)
+ i_debug("REPLIED");
+ ctx->replied = TRUE;
+ io_loop_stop(ioloop);
+ connection_disconnect(&conn->conn);
+ return;
+ default:
+ i_unreached();
+ }
+ }
+
+ i_assert(ret >= 0);
+}
+
+static void test_bad_helo_client_connected(struct client_connection *conn)
+{
+ struct _bad_helo_client *ctx;
+
+ ctx = p_new(conn->pool, struct _bad_helo_client, 1);
+ ctx->parser = smtp_reply_parser_init(conn->conn.input, SIZE_MAX);
+ conn->context = ctx;
+
+ switch (client_index) {
+ case 0:
+ o_stream_nsend_str(conn->conn.output, "HELO\r\n");
+ break;
+ case 1:
+ o_stream_nsend_str(conn->conn.output, "EHLO\r\n");
+ break;
+ case 2:
+ o_stream_nsend_str(conn->conn.output, "HELO frop\r\n");
+ break;
+ case 3:
+ o_stream_nsend_str(conn->conn.output, "EHLO frop\r\n");
+ break;
+ default:
+ i_unreached();
+ }
+}
+
+static void test_bad_helo_client_deinit(struct client_connection *conn)
+{
+ struct _bad_helo_client *ctx = conn->context;
+
+ i_assert(ctx->replied);
+ smtp_reply_parser_deinit(&ctx->parser);
+}
+
+static void test_client_bad_helo(unsigned int index)
+{
+ test_client_input = test_bad_helo_client_input;
+ test_client_connected = test_bad_helo_client_connected;
+ test_client_deinit = test_bad_helo_client_deinit;
+ test_client_run(index);
+}
+
+/* server */
+
+struct _bad_helo {
+ struct istream *payload_input;
+ struct io *io;
+
+ bool serviced:1;
+};
+
+static void
+test_server_bad_helo_disconnect(void *context ATTR_UNUSED, const char *reason)
+{
+ if (debug)
+ i_debug("Disconnect: %s", reason);
+}
+
+static int
+test_server_bad_helo_helo(void *conn_ctx ATTR_UNUSED,
+ struct smtp_server_cmd_ctx *cmd ATTR_UNUSED,
+ struct smtp_server_cmd_helo *data ATTR_UNUSED)
+{
+ return 1;
+}
+
+static int
+test_server_bad_helo_rcpt(void *conn_ctx ATTR_UNUSED,
+ struct smtp_server_cmd_ctx *cmd ATTR_UNUSED,
+ struct smtp_server_recipient *rcpt ATTR_UNUSED)
+{
+ return 1;
+}
+
+static int
+test_server_bad_helo_data_begin(
+ void *conn_ctx ATTR_UNUSED, struct smtp_server_cmd_ctx *cmd,
+ struct smtp_server_transaction *trans ATTR_UNUSED,
+ struct istream *data_input ATTR_UNUSED)
+{
+ smtp_server_reply(cmd, 250, "2.0.0", "OK");
+ return 1;
+}
+
+static void test_server_bad_helo(const struct smtp_server_settings *server_set)
+{
+ server_callbacks.conn_disconnect =
+ test_server_bad_helo_disconnect;
+
+ server_callbacks.conn_cmd_helo =
+ test_server_bad_helo_helo;
+ server_callbacks.conn_cmd_rcpt =
+ test_server_bad_helo_rcpt;
+ server_callbacks.conn_cmd_data_begin =
+ test_server_bad_helo_data_begin;
+ test_server_run(server_set);
+}
+
+/* test */
+
+static void test_bad_helo(void)
+{
+ struct smtp_server_settings smtp_server_set;
+
+ test_server_defaults(&smtp_server_set);
+ smtp_server_set.max_client_idle_time_msecs = 1000;
+
+ test_begin("bad HELO");
+ test_run_client_server(&smtp_server_set,
+ test_server_bad_helo,
+ test_client_bad_helo, 4);
+ test_end();
+}
+
+/*
+ * Bad MAIL
+ */
+
+/* client */
+
+struct _bad_mail_client {
+ struct smtp_reply_parser *parser;
+ unsigned int reply;
+
+ bool replied:1;
+};
+
+static void test_bad_mail_client_input(struct client_connection *conn)
+{
+ struct _bad_mail_client *ctx = conn->context;
+ struct smtp_reply *reply;
+ const char *error;
+ int ret;
+
+ while ((ret = smtp_reply_parse_next(ctx->parser, FALSE,
+ &reply, &error)) > 0) {
+ if (debug)
+ i_debug("REPLY: %s", smtp_reply_log(reply));
+
+ switch (ctx->reply++) {
+ case 0: /* greeting */
+ i_assert(reply->status == 220);
+ break;
+ case 1: /* bad command reply */
+ switch (client_index) {
+ case 0: case 1: case 2: case 3: case 4: case 5: case 6:
+ i_assert(reply->status == 501);
+ break;
+ case 7: case 8:
+ i_assert(reply->status == 250);
+ break;
+ default:
+ i_unreached();
+ }
+ ctx->replied = TRUE;
+ io_loop_stop(ioloop);
+ connection_disconnect(&conn->conn);
+ return;
+ default:
+ i_unreached();
+ }
+ }
+
+ i_assert(ret >= 0);
+}
+
+static void test_bad_mail_client_connected(struct client_connection *conn)
+{
+ struct _bad_mail_client *ctx;
+
+ ctx = p_new(conn->pool, struct _bad_mail_client, 1);
+ ctx->parser = smtp_reply_parser_init(conn->conn.input, SIZE_MAX);
+ conn->context = ctx;
+
+ switch (client_index) {
+ case 0:
+ o_stream_nsend_str(
+ conn->conn.output,
+ "MAIL FROM: <hendrik@example.com>\r\n");
+ break;
+ case 1:
+ o_stream_nsend_str(
+ conn->conn.output,
+ "MAIL FROM:hendrik@example.com\r\n");
+ break;
+ case 2:
+ o_stream_nsend_str(
+ conn->conn.output,
+ "MAIL FROM: hendrik@example.com\r\n");
+ break;
+ case 3:
+ o_stream_nsend_str(
+ conn->conn.output,
+ "MAIL FROM:\r\n");
+ break;
+ case 4:
+ o_stream_nsend_str(
+ conn->conn.output,
+ "MAIL FROM: \r\n");
+ break;
+ case 5:
+ o_stream_nsend_str(
+ conn->conn.output,
+ "MAIL FROM: BODY=7BIT\r\n");
+ break;
+ case 6:
+ o_stream_nsend_str(
+ conn->conn.output,
+ "MAIL FROM: <>\r\n");
+ break;
+ case 7:
+ o_stream_nsend_str(
+ conn->conn.output,
+ "MAIL FROM:<hendrik@example.com>\r\n");
+ break;
+ case 8:
+ o_stream_nsend_str(
+ conn->conn.output,
+ "MAIL FROM:<>\r\n");
+ break;
+ default:
+ i_unreached();
+ }
+}
+
+static void test_bad_mail_client_deinit(struct client_connection *conn)
+{
+ struct _bad_mail_client *ctx = conn->context;
+
+ i_assert(ctx->replied);
+ smtp_reply_parser_deinit(&ctx->parser);
+}
+
+static void test_client_bad_mail(unsigned int index)
+{
+ test_client_input = test_bad_mail_client_input;
+ test_client_connected = test_bad_mail_client_connected;
+ test_client_deinit = test_bad_mail_client_deinit;
+ test_client_run(index);
+}
+
+/* server */
+
+static void
+test_server_bad_mail_disconnect(void *context ATTR_UNUSED, const char *reason)
+{
+ if (debug)
+ i_debug("Disconnect: %s", reason);
+}
+
+static int
+test_server_bad_mail_rcpt(void *conn_ctx ATTR_UNUSED,
+ struct smtp_server_cmd_ctx *cmd ATTR_UNUSED,
+ struct smtp_server_recipient *rcpt ATTR_UNUSED)
+{
+ test_assert(FALSE);
+ return 1;
+}
+
+static int
+test_server_bad_mail_data_begin(
+ void *conn_ctx ATTR_UNUSED, struct smtp_server_cmd_ctx *cmd ATTR_UNUSED,
+ struct smtp_server_transaction *trans ATTR_UNUSED,
+ struct istream *data_input ATTR_UNUSED)
+{
+ test_assert(FALSE);
+ return 1;
+}
+
+static void test_server_bad_mail(const struct smtp_server_settings *server_set)
+{
+ server_callbacks.conn_disconnect =
+ test_server_bad_mail_disconnect;
+
+ server_callbacks.conn_cmd_rcpt =
+ test_server_bad_mail_rcpt;
+ server_callbacks.conn_cmd_data_begin =
+ test_server_bad_mail_data_begin;
+ test_server_run(server_set);
+}
+
+/* test */
+
+static void test_bad_mail(void)
+{
+ struct smtp_server_settings smtp_server_set;
+
+ test_server_defaults(&smtp_server_set);
+ smtp_server_set.max_client_idle_time_msecs = 1000;
+
+ test_begin("bad MAIL");
+ test_run_client_server(&smtp_server_set,
+ test_server_bad_mail,
+ test_client_bad_mail, 9);
+ test_end();
+}
+
+/*
+ * Bad RCPT
+ */
+
+/* client */
+
+struct _bad_rcpt_client {
+ struct smtp_reply_parser *parser;
+ unsigned int reply;
+
+ bool replied:1;
+};
+
+static void
+test_bad_rcpt_client_input(struct client_connection *conn)
+{
+ struct _bad_rcpt_client *ctx = conn->context;
+ struct smtp_reply *reply;
+ const char *error;
+ int ret;
+
+ while ((ret = smtp_reply_parse_next(ctx->parser, FALSE,
+ &reply, &error)) > 0) {
+ if (debug)
+ i_debug("REPLY: %s", smtp_reply_log(reply));
+
+ switch (ctx->reply++) {
+ case 0: /* greeting */
+ i_assert(reply->status == 220);
+ break;
+ case 1: /* MAIL FROM */
+ i_assert(reply->status == 250);
+ break;
+ case 2: /* bad command reply */
+ switch (client_index) {
+ case 0: case 1: case 2: case 3: case 4: case 5:
+ i_assert(reply->status == 501);
+ break;
+ case 6:
+ i_assert(reply->status == 250);
+ break;
+ default:
+ i_unreached();
+ }
+ ctx->replied = TRUE;
+ io_loop_stop(ioloop);
+ connection_disconnect(&conn->conn);
+ return;
+ default:
+ i_unreached();
+ }
+ }
+
+ i_assert(ret >= 0);
+}
+
+static void test_bad_rcpt_client_connected(struct client_connection *conn)
+{
+ struct _bad_rcpt_client *ctx;
+
+ ctx = p_new(conn->pool, struct _bad_rcpt_client, 1);
+ ctx->parser = smtp_reply_parser_init(conn->conn.input, SIZE_MAX);
+ conn->context = ctx;
+
+ switch (client_index) {
+ case 0:
+ o_stream_nsend_str(
+ conn->conn.output,
+ "MAIL FROM:<hendrik@example.com>\r\n"
+ "RCPT TO: <harrie@example.com>\r\n");
+ break;
+ case 1:
+ o_stream_nsend_str(
+ conn->conn.output,
+ "MAIL FROM:<hendrik@example.com>\r\n"
+ "RCPT TO:harrie@example.com\r\n");
+ break;
+ case 2:
+ o_stream_nsend_str(
+ conn->conn.output,
+ "MAIL FROM:<hendrik@example.com>\r\n"
+ "RCPT TO: harrie@example.com\r\n");
+ break;
+ case 3:
+ o_stream_nsend_str(
+ conn->conn.output,
+ "MAIL FROM:<hendrik@example.com>\r\n"
+ "RCPT TO:\r\n");
+ break;
+ case 4:
+ o_stream_nsend_str(
+ conn->conn.output,
+ "MAIL FROM:<hendrik@example.com>\r\n"
+ "RCPT TO: \r\n");
+ break;
+ case 5:
+ o_stream_nsend_str(
+ conn->conn.output,
+ "MAIL FROM:<hendrik@example.com>\r\n"
+ "RCPT TO: NOTIFY=NEVER\r\n");
+ break;
+ case 6:
+ o_stream_nsend_str(
+ conn->conn.output,
+ "MAIL FROM:<hendrik@example.com>\r\n"
+ "RCPT TO:<harrie@example.com>\r\n");
+ break;
+ default:
+ i_unreached();
+ }
+}
+
+static void test_bad_rcpt_client_deinit(struct client_connection *conn)
+{
+ struct _bad_rcpt_client *ctx = conn->context;
+
+ i_assert(ctx->replied);
+ smtp_reply_parser_deinit(&ctx->parser);
+}
+
+static void test_client_bad_rcpt(unsigned int index)
+{
+ test_client_input = test_bad_rcpt_client_input;
+ test_client_connected = test_bad_rcpt_client_connected;
+ test_client_deinit = test_bad_rcpt_client_deinit;
+ test_client_run(index);
+}
+
+/* server */
+
+static void
+test_server_bad_rcpt_disconnect(void *context ATTR_UNUSED, const char *reason)
+{
+ if (debug)
+ i_debug("Disconnect: %s", reason);
+}
+
+static int
+test_server_bad_rcpt_rcpt(void *conn_ctx ATTR_UNUSED,
+ struct smtp_server_cmd_ctx *cmd ATTR_UNUSED,
+ struct smtp_server_recipient *rcpt ATTR_UNUSED)
+{
+ return 1;
+}
+
+static int
+test_server_bad_rcpt_data_begin(
+ void *conn_ctx ATTR_UNUSED, struct smtp_server_cmd_ctx *cmd ATTR_UNUSED,
+ struct smtp_server_transaction *trans ATTR_UNUSED,
+ struct istream *data_input ATTR_UNUSED)
+{
+ test_assert(FALSE);
+ return 1;
+}
+
+static void test_server_bad_rcpt(const struct smtp_server_settings *server_set)
+{
+ server_callbacks.conn_disconnect =
+ test_server_bad_rcpt_disconnect;
+
+ server_callbacks.conn_cmd_rcpt =
+ test_server_bad_rcpt_rcpt;
+ server_callbacks.conn_cmd_data_begin =
+ test_server_bad_rcpt_data_begin;
+ test_server_run(server_set);
+}
+
+/* test */
+
+static void test_bad_rcpt(void)
+{
+ struct smtp_server_settings smtp_server_set;
+
+ test_server_defaults(&smtp_server_set);
+ smtp_server_set.max_client_idle_time_msecs = 1000;
+
+ test_begin("bad RCPT");
+ test_run_client_server(&smtp_server_set,
+ test_server_bad_rcpt,
+ test_client_bad_rcpt, 7);
+ test_end();
+}
+
+/*
+ * Bad VRFY
+ */
+
+/* client */
+
+struct _bad_vrfy_client {
+ struct smtp_reply_parser *parser;
+ unsigned int reply;
+
+ bool replied:1;
+};
+
+static void
+test_bad_vrfy_client_input(struct client_connection *conn)
+{
+ struct _bad_vrfy_client *ctx = conn->context;
+ struct smtp_reply *reply;
+ const char *error;
+ int ret;
+
+ while ((ret = smtp_reply_parse_next(ctx->parser, FALSE,
+ &reply, &error)) > 0) {
+ if (debug)
+ i_debug("REPLY: %s", smtp_reply_log(reply));
+
+ switch (ctx->reply++) {
+ case 0: /* greeting */
+ i_assert(reply->status == 220);
+ break;
+ case 1: /* bad command reply */
+ switch (client_index) {
+ case 0: case 1: case 2:
+ i_assert(reply->status == 501);
+ break;
+ case 3:
+ i_assert(smtp_reply_is_success(reply));
+ break;
+ default:
+ i_unreached();
+ }
+ ctx->replied = TRUE;
+ io_loop_stop(ioloop);
+ connection_disconnect(&conn->conn);
+ return;
+ default:
+ i_unreached();
+ }
+ }
+
+ i_assert(ret == 0);
+}
+
+static void
+test_bad_vrfy_client_connected(struct client_connection *conn)
+{
+ struct _bad_vrfy_client *ctx;
+
+ ctx = p_new(conn->pool, struct _bad_vrfy_client, 1);
+ ctx->parser = smtp_reply_parser_init(conn->conn.input, SIZE_MAX);
+ conn->context = ctx;
+
+ switch (client_index) {
+ case 0:
+ o_stream_nsend_str(conn->conn.output,
+ "VRFY\r\n");
+ break;
+ case 1:
+ o_stream_nsend_str(conn->conn.output,
+ "VRFY \"hendrik\r\n");
+ break;
+ case 2:
+ o_stream_nsend_str(conn->conn.output,
+ "VRFY hen\"drik\r\n");
+ break;
+ case 3:
+ o_stream_nsend_str(conn->conn.output,
+ "VRFY \"hendrik\"\r\n");
+ break;
+ default:
+ i_unreached();
+ }
+}
+
+static void
+test_bad_vrfy_client_deinit(struct client_connection *conn)
+{
+ struct _bad_vrfy_client *ctx = conn->context;
+
+ i_assert(ctx->replied);
+ smtp_reply_parser_deinit(&ctx->parser);
+}
+
+static void test_client_bad_vrfy(unsigned int index)
+{
+ test_client_input = test_bad_vrfy_client_input;
+ test_client_connected = test_bad_vrfy_client_connected;
+ test_client_deinit = test_bad_vrfy_client_deinit;
+ test_client_run(index);
+}
+
+/* server */
+
+static void
+test_server_bad_vrfy_disconnect(void *context ATTR_UNUSED, const char *reason)
+{
+ if (debug)
+ i_debug("Disconnect: %s", reason);
+}
+
+static int
+test_server_bad_vrfy_rcpt(void *conn_ctx ATTR_UNUSED,
+ struct smtp_server_cmd_ctx *cmd ATTR_UNUSED,
+ struct smtp_server_recipient *rcpt ATTR_UNUSED)
+{
+ test_assert(FALSE);
+ return 1;
+}
+
+static int
+test_server_bad_vrfy_data_begin(
+ void *conn_ctx ATTR_UNUSED, struct smtp_server_cmd_ctx *cmd ATTR_UNUSED,
+ struct smtp_server_transaction *trans ATTR_UNUSED,
+ struct istream *data_input ATTR_UNUSED)
+{
+ test_assert(FALSE);
+ return 1;
+}
+
+static void
+test_server_bad_vrfy(const struct smtp_server_settings *server_set)
+{
+ server_callbacks.conn_disconnect = test_server_bad_vrfy_disconnect;
+
+ server_callbacks.conn_cmd_rcpt = test_server_bad_vrfy_rcpt;
+ server_callbacks.conn_cmd_data_begin = test_server_bad_vrfy_data_begin;
+ test_server_run(server_set);
+}
+
+/* test */
+
+static void test_bad_vrfy(void)
+{
+ struct smtp_server_settings smtp_server_set;
+
+ test_server_defaults(&smtp_server_set);
+ smtp_server_set.max_client_idle_time_msecs = 1000;
+
+ test_begin("bad VRFY");
+ test_run_client_server(&smtp_server_set,
+ test_server_bad_vrfy,
+ test_client_bad_vrfy, 4);
+ test_end();
+}
+
+/*
+ * Bad NOOP
+ */
+
+/* client */
+
+struct _bad_noop_client {
+ struct smtp_reply_parser *parser;
+ unsigned int reply;
+
+ bool replied:1;
+};
+
+static void
+test_bad_noop_client_input(struct client_connection *conn)
+{
+ struct _bad_noop_client *ctx = conn->context;
+ struct smtp_reply *reply;
+ const char *error;
+ int ret;
+
+ while ((ret = smtp_reply_parse_next(ctx->parser, FALSE,
+ &reply, &error)) > 0) {
+ if (debug)
+ i_debug("REPLY: %s", smtp_reply_log(reply));
+
+ switch (ctx->reply++) {
+ case 0: /* greeting */
+ i_assert(reply->status == 220);
+ break;
+ case 1: /* bad command reply */
+ switch (client_index) {
+ case 1: case 2:
+ i_assert(reply->status == 501);
+ break;
+ case 0: case 3:
+ i_assert(smtp_reply_is_success(reply));
+ break;
+ default:
+ i_unreached();
+ }
+ ctx->replied = TRUE;
+ io_loop_stop(ioloop);
+ connection_disconnect(&conn->conn);
+ return;
+ default:
+ i_unreached();
+ }
+ }
+
+ i_assert(ret == 0);
+}
+
+static void
+test_bad_noop_client_connected(struct client_connection *conn)
+{
+ struct _bad_noop_client *ctx;
+
+ ctx = p_new(conn->pool, struct _bad_noop_client, 1);
+ ctx->parser = smtp_reply_parser_init(conn->conn.input, SIZE_MAX);
+ conn->context = ctx;
+
+ switch (client_index) {
+ case 0:
+ o_stream_nsend_str(conn->conn.output,
+ "NOOP\r\n");
+ break;
+ case 1:
+ o_stream_nsend_str(conn->conn.output,
+ "NOOP \"frop\r\n");
+ break;
+ case 2:
+ o_stream_nsend_str(conn->conn.output,
+ "NOOP fr\"op\r\n");
+ break;
+ case 3:
+ o_stream_nsend_str(conn->conn.output,
+ "NOOP \"frop\"\r\n");
+ break;
+ default:
+ i_unreached();
+ }
+}
+
+static void
+test_bad_noop_client_deinit(struct client_connection *conn)
+{
+ struct _bad_noop_client *ctx = conn->context;
+
+ i_assert(ctx->replied);
+ smtp_reply_parser_deinit(&ctx->parser);
+}
+
+static void test_client_bad_noop(unsigned int index)
+{
+ test_client_input = test_bad_noop_client_input;
+ test_client_connected = test_bad_noop_client_connected;
+ test_client_deinit = test_bad_noop_client_deinit;
+ test_client_run(index);
+}
+
+/* server */
+
+static void
+test_server_bad_noop_disconnect(void *context ATTR_UNUSED, const char *reason)
+{
+ if (debug)
+ i_debug("Disconnect: %s", reason);
+}
+
+static int
+test_server_bad_noop_rcpt(void *conn_ctx ATTR_UNUSED,
+ struct smtp_server_cmd_ctx *cmd ATTR_UNUSED,
+ struct smtp_server_recipient *rcpt ATTR_UNUSED)
+{
+ test_assert(FALSE);
+ return 1;
+}
+
+static int
+test_server_bad_noop_data_begin(
+ void *conn_ctx ATTR_UNUSED, struct smtp_server_cmd_ctx *cmd ATTR_UNUSED,
+ struct smtp_server_transaction *trans ATTR_UNUSED,
+ struct istream *data_input ATTR_UNUSED)
+{
+ test_assert(FALSE);
+ return 1;
+}
+
+static void
+test_server_bad_noop(const struct smtp_server_settings *server_set)
+{
+ server_callbacks.conn_disconnect = test_server_bad_noop_disconnect;
+
+ server_callbacks.conn_cmd_rcpt = test_server_bad_noop_rcpt;
+ server_callbacks.conn_cmd_data_begin = test_server_bad_noop_data_begin;
+ test_server_run(server_set);
+}
+
+/* test */
+
+static void test_bad_noop(void)
+{
+ struct smtp_server_settings smtp_server_set;
+
+ test_server_defaults(&smtp_server_set);
+ smtp_server_set.max_client_idle_time_msecs = 1000;
+
+ test_begin("bad NOOP");
+ test_run_client_server(&smtp_server_set,
+ test_server_bad_noop,
+ test_client_bad_noop, 4);
+ test_end();
+}
+
+/*
+ * MAIL workarounds
+ */
+
+/* client */
+
+struct _mail_workarounds_client {
+ struct smtp_reply_parser *parser;
+ unsigned int reply;
+
+ bool replied:1;
+};
+
+static void test_mail_workarounds_client_input(struct client_connection *conn)
+{
+ struct _mail_workarounds_client *ctx = conn->context;
+ struct smtp_reply *reply;
+ const char *error;
+ int ret;
+
+ while ((ret = smtp_reply_parse_next(ctx->parser, FALSE,
+ &reply, &error)) > 0) {
+ if (debug)
+ i_debug("REPLY: %s", smtp_reply_log(reply));
+
+ switch (ctx->reply++) {
+ case 0: /* greeting */
+ i_assert(reply->status == 220);
+ break;
+ case 1: /* bad command reply */
+ switch (client_index) {
+ case 5: case 6: case 7:
+ i_assert(reply->status == 501);
+ break;
+ case 0: case 1: case 2: case 3: case 4: case 8:
+ case 9: case 10:
+ i_assert(reply->status == 250);
+ break;
+ default:
+ i_unreached();
+ }
+ ctx->replied = TRUE;
+ io_loop_stop(ioloop);
+ connection_disconnect(&conn->conn);
+ return;
+ default:
+ i_unreached();
+ }
+ }
+
+ i_assert(ret >= 0);
+}
+
+static void
+test_mail_workarounds_client_connected(struct client_connection *conn)
+{
+ struct _mail_workarounds_client *ctx;
+
+ ctx = p_new(conn->pool, struct _mail_workarounds_client, 1);
+ ctx->parser = smtp_reply_parser_init(conn->conn.input, SIZE_MAX);
+ conn->context = ctx;
+
+ switch (client_index) {
+ case 0:
+ o_stream_nsend_str(
+ conn->conn.output,
+ "MAIL FROM: <hendrik@example.com>\r\n");
+ break;
+ case 1:
+ o_stream_nsend_str(
+ conn->conn.output,
+ "MAIL FROM:\t<hendrik@example.com>\r\n");
+ break;
+ case 2:
+ o_stream_nsend_str(
+ conn->conn.output,
+ "MAIL FROM:\t <hendrik@example.com>\r\n");
+ break;
+ case 3:
+ o_stream_nsend_str(
+ conn->conn.output,
+ "MAIL FROM:hendrik@example.com\r\n");
+ break;
+ case 4:
+ o_stream_nsend_str(
+ conn->conn.output,
+ "MAIL FROM: hendrik@example.com\r\n");
+ break;
+ case 5:
+ o_stream_nsend_str(
+ conn->conn.output,
+ "MAIL FROM:\r\n");
+ break;
+ case 6:
+ o_stream_nsend_str(
+ conn->conn.output,
+ "MAIL FROM: \r\n");
+ break;
+ case 7:
+ o_stream_nsend_str(
+ conn->conn.output,
+ "MAIL FROM: BODY=7BIT\r\n");
+ break;
+ case 8:
+ o_stream_nsend_str(
+ conn->conn.output,
+ "MAIL FROM: <>\r\n");
+ break;
+ case 9:
+ o_stream_nsend_str(
+ conn->conn.output,
+ "MAIL FROM:<hendrik@example.com>\r\n");
+ break;
+ case 10:
+ o_stream_nsend_str(
+ conn->conn.output,
+ "MAIL FROM:<>\r\n");
+ break;
+ default:
+ i_unreached();
+ }
+}
+
+static void
+test_mail_workarounds_client_deinit(struct client_connection *conn)
+{
+ struct _mail_workarounds_client *ctx = conn->context;
+
+ i_assert(ctx->replied);
+ smtp_reply_parser_deinit(&ctx->parser);
+}
+
+static void test_client_mail_workarounds(unsigned int index)
+{
+ test_client_input = test_mail_workarounds_client_input;
+ test_client_connected = test_mail_workarounds_client_connected;
+ test_client_deinit = test_mail_workarounds_client_deinit;
+ test_client_run(index);
+}
+
+/* server */
+
+static void
+test_server_mail_workarounds_disconnect(void *context ATTR_UNUSED,
+ const char *reason)
+{
+ if (debug)
+ i_debug("Disconnect: %s", reason);
+}
+
+static int
+test_server_mail_workarounds_rcpt(
+ void *conn_ctx ATTR_UNUSED, struct smtp_server_cmd_ctx *cmd ATTR_UNUSED,
+ struct smtp_server_recipient *rcpt ATTR_UNUSED)
+{
+ test_assert(FALSE);
+ return 1;
+}
+
+static int
+test_server_mail_workarounds_data_begin(
+ void *conn_ctx ATTR_UNUSED, struct smtp_server_cmd_ctx *cmd ATTR_UNUSED,
+ struct smtp_server_transaction *trans ATTR_UNUSED,
+ struct istream *data_input ATTR_UNUSED)
+{
+ test_assert(FALSE);
+ return 1;
+}
+
+static void
+test_server_mail_workarounds(const struct smtp_server_settings *server_set)
+{
+ server_callbacks.conn_disconnect =
+ test_server_mail_workarounds_disconnect;
+
+ server_callbacks.conn_cmd_rcpt =
+ test_server_mail_workarounds_rcpt;
+ server_callbacks.conn_cmd_data_begin =
+ test_server_mail_workarounds_data_begin;
+ test_server_run(server_set);
+}
+
+/* test */
+
+static void test_mail_workarounds(void)
+{
+ struct smtp_server_settings smtp_server_set;
+
+ test_server_defaults(&smtp_server_set);
+ smtp_server_set.workarounds =
+ SMTP_SERVER_WORKAROUND_WHITESPACE_BEFORE_PATH |
+ SMTP_SERVER_WORKAROUND_MAILBOX_FOR_PATH;
+ smtp_server_set.max_client_idle_time_msecs = 1000;
+
+ test_begin("MAIL workarounds");
+ test_run_client_server(&smtp_server_set,
+ test_server_mail_workarounds,
+ test_client_mail_workarounds, 11);
+ test_end();
+}
+
+/*
+ * RCPT workarounds
+ */
+
+/* client */
+
+struct _rcpt_workarounds_client {
+ struct smtp_reply_parser *parser;
+ unsigned int reply;
+
+ bool replied:1;
+};
+
+static void test_rcpt_workarounds_client_input(struct client_connection *conn)
+{
+ struct _rcpt_workarounds_client *ctx = conn->context;
+ struct smtp_reply *reply;
+ const char *error;
+ int ret;
+
+ while ((ret = smtp_reply_parse_next(ctx->parser, FALSE,
+ &reply, &error)) > 0) {
+ if (debug)
+ i_debug("REPLY: %s", smtp_reply_log(reply));
+
+ switch (ctx->reply++) {
+ case 0: /* greeting */
+ i_assert(reply->status == 220);
+ break;
+ case 1: /* MAIL FROM */
+ i_assert(reply->status == 250);
+ break;
+ case 2: /* bad command reply */
+ switch (client_index) {
+ case 5: case 6: case 7:
+ i_assert(reply->status == 501);
+ break;
+ case 0: case 1: case 2: case 3: case 4: case 8:
+ i_assert(reply->status == 250);
+ break;
+ default:
+ i_unreached();
+ }
+ ctx->replied = TRUE;
+ io_loop_stop(ioloop);
+ connection_disconnect(&conn->conn);
+ return;
+ default:
+ i_unreached();
+ }
+ }
+
+ i_assert(ret >= 0);
+}
+
+static void
+test_rcpt_workarounds_client_connected(struct client_connection *conn)
+{
+ struct _rcpt_workarounds_client *ctx;
+
+ ctx = p_new(conn->pool, struct _rcpt_workarounds_client, 1);
+ ctx->parser = smtp_reply_parser_init(conn->conn.input, SIZE_MAX);
+ conn->context = ctx;
+
+ switch (client_index) {
+ case 0:
+ o_stream_nsend_str(
+ conn->conn.output,
+ "MAIL FROM:<hendrik@example.com>\r\n"
+ "RCPT TO: <harrie@example.com>\r\n");
+ break;
+ case 1:
+ o_stream_nsend_str(
+ conn->conn.output,
+ "MAIL FROM:<hendrik@example.com>\r\n"
+ "RCPT TO:\t<harrie@example.com>\r\n");
+ break;
+ case 2:
+ o_stream_nsend_str(
+ conn->conn.output,
+ "MAIL FROM:<hendrik@example.com>\r\n"
+ "RCPT TO:\t <harrie@example.com>\r\n");
+ break;
+ case 3:
+ o_stream_nsend_str(
+ conn->conn.output,
+ "MAIL FROM:<hendrik@example.com>\r\n"
+ "RCPT TO:harrie@example.com\r\n");
+ break;
+ case 4:
+ o_stream_nsend_str(
+ conn->conn.output,
+ "MAIL FROM:<hendrik@example.com>\r\n"
+ "RCPT TO: harrie@example.com\r\n");
+ break;
+ case 5:
+ o_stream_nsend_str(
+ conn->conn.output,
+ "MAIL FROM:<hendrik@example.com>\r\n"
+ "RCPT TO:\r\n");
+ break;
+ case 6:
+ o_stream_nsend_str(
+ conn->conn.output,
+ "MAIL FROM:<hendrik@example.com>\r\n"
+ "RCPT TO: \r\n");
+ break;
+ case 7:
+ o_stream_nsend_str(
+ conn->conn.output,
+ "MAIL FROM:<hendrik@example.com>\r\n"
+ "RCPT TO: NOTIFY=NEVER\r\n");
+ break;
+ case 8:
+ o_stream_nsend_str(
+ conn->conn.output,
+ "MAIL FROM:<hendrik@example.com>\r\n"
+ "RCPT TO:<harrie@example.com>\r\n");
+ break;
+ default:
+ i_unreached();
+ }
+}
+
+static void test_rcpt_workarounds_client_deinit(struct client_connection *conn)
+{
+ struct _rcpt_workarounds_client *ctx = conn->context;
+
+ i_assert(ctx->replied);
+ smtp_reply_parser_deinit(&ctx->parser);
+}
+
+static void test_client_rcpt_workarounds(unsigned int index)
+{
+ test_client_input = test_rcpt_workarounds_client_input;
+ test_client_connected = test_rcpt_workarounds_client_connected;
+ test_client_deinit = test_rcpt_workarounds_client_deinit;
+ test_client_run(index);
+}
+
+/* server */
+
+static void
+test_server_rcpt_workarounds_disconnect(void *context ATTR_UNUSED,
+ const char *reason)
+{
+ if (debug)
+ i_debug("Disconnect: %s", reason);
+}
+
+static int
+test_server_rcpt_workarounds_rcpt(
+ void *conn_ctx ATTR_UNUSED, struct smtp_server_cmd_ctx *cmd ATTR_UNUSED,
+ struct smtp_server_recipient *rcpt ATTR_UNUSED)
+{
+ return 1;
+}
+
+static int
+test_server_rcpt_workarounds_data_begin(
+ void *conn_ctx ATTR_UNUSED, struct smtp_server_cmd_ctx *cmd ATTR_UNUSED,
+ struct smtp_server_transaction *trans ATTR_UNUSED,
+ struct istream *data_input ATTR_UNUSED)
+{
+ test_assert(FALSE);
+ return 1;
+}
+
+static void
+test_server_rcpt_workarounds(const struct smtp_server_settings *server_set)
+{
+ server_callbacks.conn_disconnect =
+ test_server_rcpt_workarounds_disconnect;
+
+ server_callbacks.conn_cmd_rcpt =
+ test_server_rcpt_workarounds_rcpt;
+ server_callbacks.conn_cmd_data_begin =
+ test_server_rcpt_workarounds_data_begin;
+ test_server_run(server_set);
+}
+
+/* test */
+
+static void test_rcpt_workarounds(void)
+{
+ struct smtp_server_settings smtp_server_set;
+
+ test_server_defaults(&smtp_server_set);
+ smtp_server_set.workarounds =
+ SMTP_SERVER_WORKAROUND_WHITESPACE_BEFORE_PATH |
+ SMTP_SERVER_WORKAROUND_MAILBOX_FOR_PATH;
+ smtp_server_set.max_client_idle_time_msecs = 1000;
+
+ test_begin("RCPT workarounds");
+ test_run_client_server(&smtp_server_set,
+ test_server_rcpt_workarounds,
+ test_client_rcpt_workarounds, 9);
+ test_end();
+}
+
+/*
+ * Too many recipients
+ */
+
+/* client */
+
+static void test_too_many_recipients_connected(struct client_connection *conn)
+{
+ o_stream_nsend_str(conn->conn.output,
+ "EHLO frop\r\n"
+ "MAIL FROM:<sender@example.com>\r\n"
+ "RCPT TO:<recipient1@example.com>\r\n"
+ "RCPT TO:<recipient2@example.com>\r\n"
+ "RCPT TO:<recipient3@example.com>\r\n"
+ "RCPT TO:<recipient4@example.com>\r\n"
+ "RCPT TO:<recipient5@example.com>\r\n"
+ "RCPT TO:<recipient6@example.com>\r\n"
+ "RCPT TO:<recipient7@example.com>\r\n"
+ "RCPT TO:<recipient8@example.com>\r\n"
+ "RCPT TO:<recipient9@example.com>\r\n"
+ "RCPT TO:<recipient10@example.com>\r\n"
+ "RCPT TO:<recipient11@example.com>\r\n"
+ "DATA\r\n"
+ "0123456789ABCDEF0123456789ABCDEF\r\n"
+ "0123456789ABCDEF0123456789ABCDEF\r\n"
+ "0123456789ABCDEF0123456789ABCDEF\r\n"
+ "0123456789ABCDEF0123456789ABCDEF\r\n"
+ "0123456789ABCDEF0123456789ABCDEF\r\n"
+ "0123456789ABCDEF0123456789ABCDEF\r\n"
+ "0123456789ABCDEF0123456789ABCDEF\r\n"
+ ".\r\n");
+}
+
+static void test_client_too_many_recipients(unsigned int index)
+{
+ test_client_connected = test_too_many_recipients_connected;
+ test_client_run(index);
+}
+
+/* server */
+
+static void
+test_server_too_many_recipients_trans_free(
+ void *conn_ctx ATTR_UNUSED,
+ struct smtp_server_transaction *trans ATTR_UNUSED)
+{
+ io_loop_stop(ioloop);
+}
+
+static int
+test_server_too_many_recipients_rcpt(
+ void *conn_ctx ATTR_UNUSED, struct smtp_server_cmd_ctx *cmd ATTR_UNUSED,
+ struct smtp_server_recipient *rcpt)
+{
+ if (debug) {
+ i_debug("RCPT TO:%s",
+ smtp_address_encode(rcpt->path));
+ }
+ return 1;
+}
+
+static int
+test_server_too_many_recipients_data_begin(
+ void *conn_ctx ATTR_UNUSED, struct smtp_server_cmd_ctx *cmd,
+ struct smtp_server_transaction *trans,
+ struct istream *data_input ATTR_UNUSED)
+{
+ test_assert(array_count(&trans->rcpt_to) == 10);
+
+ smtp_server_reply(cmd, 250, "2.0.0", "OK");
+ return 1;
+}
+
+static void
+test_server_too_many_recipients(const struct smtp_server_settings *server_set)
+{
+ server_callbacks.conn_trans_free =
+ test_server_too_many_recipients_trans_free;
+ server_callbacks.conn_cmd_rcpt =
+ test_server_too_many_recipients_rcpt;
+ server_callbacks.conn_cmd_data_begin =
+ test_server_too_many_recipients_data_begin;
+ test_server_run(server_set);
+}
+
+/* test */
+
+static void test_too_many_recipients(void)
+{
+ struct smtp_server_settings smtp_server_set;
+
+ test_server_defaults(&smtp_server_set);
+ smtp_server_set.max_client_idle_time_msecs = 1000;
+ smtp_server_set.max_recipients = 10;
+
+ test_begin("too many recipients");
+ test_run_client_server(&smtp_server_set,
+ test_server_too_many_recipients,
+ test_client_too_many_recipients, 1);
+ test_end();
+}
+
+/*
+ * DATA without MAIL
+ */
+
+/* client */
+
+static void test_data_no_mail_connected(struct client_connection *conn)
+{
+ o_stream_nsend_str(conn->conn.output,
+ "EHLO frop\r\n"
+ "DATA\r\n"
+ ".\r\n"
+ "RSET\r\n");
+}
+
+static void test_client_data_no_mail(unsigned int index)
+{
+ test_client_connected = test_data_no_mail_connected;
+ test_client_run(index);
+}
+
+/* server */
+
+static int
+test_server_data_no_mail_rcpt(void *conn_ctx ATTR_UNUSED,
+ struct smtp_server_cmd_ctx *cmd ATTR_UNUSED,
+ struct smtp_server_recipient *rcpt ATTR_UNUSED)
+{
+ /* not supposed to get here */
+ i_assert(FALSE);
+ return 1;
+}
+
+static int
+test_server_data_no_mail_data_begin(
+ void *conn_ctx ATTR_UNUSED, struct smtp_server_cmd_ctx *cmd ATTR_UNUSED,
+ struct smtp_server_transaction *trans ATTR_UNUSED,
+ struct istream *data_input ATTR_UNUSED)
+{
+ /* not supposed to get here */
+ i_assert(FALSE);
+ return 1;
+}
+
+static int
+test_server_data_no_mail_rset(void *conn_ctx ATTR_UNUSED,
+ struct smtp_server_cmd_ctx *cmd ATTR_UNUSED)
+{
+ io_loop_stop(ioloop);
+ return 1;
+}
+
+static void
+test_server_data_no_mail(const struct smtp_server_settings *server_set)
+{
+ server_callbacks.conn_cmd_rcpt =
+ test_server_data_no_mail_rcpt;
+ server_callbacks.conn_cmd_data_begin =
+ test_server_data_no_mail_data_begin;
+ server_callbacks.conn_cmd_rset =
+ test_server_data_no_mail_rset;
+ test_server_run(server_set);
+}
+
+/* test */
+
+static void test_data_no_mail(void)
+{
+ struct smtp_server_settings smtp_server_set;
+
+ test_server_defaults(&smtp_server_set);
+ smtp_server_set.capabilities =
+ SMTP_CAPABILITY_BINARYMIME | SMTP_CAPABILITY_CHUNKING;
+ smtp_server_set.max_client_idle_time_msecs = 1000;
+ smtp_server_set.max_recipients = 10;
+
+ test_begin("DATA without MAIL");
+ test_run_client_server(&smtp_server_set,
+ test_server_data_no_mail,
+ test_client_data_no_mail, 1);
+ test_end();
+}
+
+/*
+ * DATA without RCPT
+ */
+
+/* client */
+
+static void test_data_no_rcpt_connected(struct client_connection *conn)
+{
+ o_stream_nsend_str(conn->conn.output,
+ "EHLO frop\r\n"
+ "MAIL FROM:<sender@example.com>\r\n"
+ "DATA\r\n"
+ ".\r\n"
+ "RSET\r\n");
+}
+
+static void test_client_data_no_rcpt(unsigned int index)
+{
+ test_client_connected = test_data_no_rcpt_connected;
+ test_client_run(index);
+}
+
+/* server */
+
+static void
+test_server_data_no_rcpt_trans_free(
+ void *conn_ctx ATTR_UNUSED,
+ struct smtp_server_transaction *trans ATTR_UNUSED)
+{
+ io_loop_stop(ioloop);
+}
+
+static int
+test_server_data_no_rcpt_rcpt(
+ void *conn_ctx ATTR_UNUSED, struct smtp_server_cmd_ctx *cmd ATTR_UNUSED,
+ struct smtp_server_recipient *rcpt ATTR_UNUSED)
+{
+ /* not supposed to get here */
+ i_assert(FALSE);
+ return 1;
+}
+
+static int
+test_server_data_no_rcpt_data_begin(
+ void *conn_ctx ATTR_UNUSED, struct smtp_server_cmd_ctx *cmd ATTR_UNUSED,
+ struct smtp_server_transaction *trans ATTR_UNUSED,
+ struct istream *data_input ATTR_UNUSED)
+{
+ /* not supposed to get here */
+ i_assert(FALSE);
+ return 1;
+}
+
+static void
+test_server_data_no_rcpt(const struct smtp_server_settings *server_set)
+{
+ server_callbacks.conn_trans_free =
+ test_server_data_no_rcpt_trans_free;
+ server_callbacks.conn_cmd_rcpt =
+ test_server_data_no_rcpt_rcpt;
+ server_callbacks.conn_cmd_data_begin =
+ test_server_data_no_rcpt_data_begin;
+ test_server_run(server_set);
+}
+
+/* test */
+
+static void test_data_no_rcpt(void)
+{
+ struct smtp_server_settings smtp_server_set;
+
+ test_server_defaults(&smtp_server_set);
+ smtp_server_set.capabilities =
+ SMTP_CAPABILITY_BINARYMIME | SMTP_CAPABILITY_CHUNKING;
+ smtp_server_set.max_client_idle_time_msecs = 1000;
+ smtp_server_set.max_recipients = 10;
+
+ test_begin("DATA without RCPT");
+ test_run_client_server(&smtp_server_set,
+ test_server_data_no_rcpt,
+ test_client_data_no_rcpt, 1);
+ test_end();
+}
+
+/*
+ * Bad pipelined DATA
+ */
+
+/* client */
+
+static void test_bad_pipelined_data_connected(struct client_connection *conn)
+{
+ o_stream_nsend_str(conn->conn.output,
+ "MAIL FROM:<senderp@example.com>\r\n"
+ "RCPT TO:<<recipient1@example.com>\r\n"
+ "DATA\r\n"
+ "FROP!\r\n"
+ "DATA\r\n"
+ "FROP!\r\n"
+ ".\r\n"
+ "QUIT\r\n");
+}
+
+static void test_client_bad_pipelined_data(unsigned int index)
+{
+ test_client_connected = test_bad_pipelined_data_connected;
+ test_client_run(index);
+}
+
+/* server */
+
+static void
+test_server_bad_pipelined_data_trans_free(
+ void *conn_ctx ATTR_UNUSED,
+ struct smtp_server_transaction *trans ATTR_UNUSED)
+{
+ io_loop_stop(ioloop);
+}
+
+static int
+test_server_bad_pipelined_data_rcpt(
+ void *conn_ctx ATTR_UNUSED, struct smtp_server_cmd_ctx *cmd ATTR_UNUSED,
+ struct smtp_server_recipient *rcpt ATTR_UNUSED)
+{
+ /* not supposed to get here */
+ i_assert(FALSE);
+ return 1;
+}
+
+static int
+test_server_bad_pipelined_data_data_begin(
+ void *conn_ctx ATTR_UNUSED, struct smtp_server_cmd_ctx *cmd ATTR_UNUSED,
+ struct smtp_server_transaction *trans ATTR_UNUSED,
+ struct istream *data_input ATTR_UNUSED)
+{
+ /* not supposed to get here */
+ i_assert(FALSE);
+ return 1;
+}
+
+static void
+test_server_bad_pipelined_data(const struct smtp_server_settings *server_set)
+{
+ server_callbacks.conn_trans_free =
+ test_server_bad_pipelined_data_trans_free;
+ server_callbacks.conn_cmd_rcpt =
+ test_server_bad_pipelined_data_rcpt;
+ server_callbacks.conn_cmd_data_begin =
+ test_server_bad_pipelined_data_data_begin;
+ test_server_run(server_set);
+}
+
+/* test */
+
+static void test_bad_pipelined_data(void)
+{
+ struct smtp_server_settings smtp_server_set;
+
+ test_server_defaults(&smtp_server_set);
+ smtp_server_set.capabilities =
+ SMTP_CAPABILITY_BINARYMIME | SMTP_CAPABILITY_CHUNKING;
+ smtp_server_set.max_client_idle_time_msecs = 1000;
+ smtp_server_set.max_recipients = 10;
+ smtp_server_set.max_pipelined_commands = 16;
+
+ test_begin("Bad pipelined DATA");
+ test_run_client_server(&smtp_server_set,
+ test_server_bad_pipelined_data,
+ test_client_bad_pipelined_data, 1);
+ test_end();
+}
+
+/*
+ * Bad pipelined DATA #2
+ */
+
+/* client */
+
+static void test_bad_pipelined_data2_connected(struct client_connection *conn)
+{
+ o_stream_nsend_str(conn->conn.output,
+ "MAIL FROM:<frop@example.com>\r\n"
+ "DATA\r\n"
+ "DATA\r\n"
+ "RCPT TO:<frop@example.com>\r\n"
+ "BDAT 0\r\n");
+}
+
+static void test_client_bad_pipelined_data2(unsigned int index)
+{
+ test_client_connected = test_bad_pipelined_data2_connected;
+ test_client_run(index);
+}
+
+/* server */
+
+struct _bad_pipelined_data2 {
+ struct istream *payload_input;
+ struct io *io;
+};
+
+static void
+test_server_bad_pipelined_data2_trans_free(
+ void *conn_ctx ATTR_UNUSED, struct smtp_server_transaction *trans)
+{
+ struct _bad_pipelined_data2 *ctx = trans->context;
+
+ i_free(ctx);
+ io_loop_stop(ioloop);
+}
+
+static int
+test_server_bad_pipelined_data2_rcpt(
+ void *conn_ctx ATTR_UNUSED, struct smtp_server_cmd_ctx *cmd ATTR_UNUSED,
+ struct smtp_server_recipient *rcpt)
+{
+ if (debug) {
+ i_debug("RCPT TO:%s",
+ smtp_address_encode(rcpt->path));
+ }
+ return 1;
+}
+
+static int
+test_server_bad_pipelined_data2_data_begin(
+ void *conn_ctx ATTR_UNUSED, struct smtp_server_cmd_ctx *cmd ATTR_UNUSED,
+ struct smtp_server_transaction *trans, struct istream *data_input)
+{
+ struct _bad_pipelined_data2 *ctx;
+
+ if (debug)
+ i_debug("DATA");
+
+ ctx = i_new(struct _bad_pipelined_data2, 1);
+ trans->context = ctx;
+
+ ctx->payload_input = data_input;
+ return 0;
+}
+
+static int
+test_server_bad_pipelined_data2_data_continue(
+ void *conn_ctx ATTR_UNUSED, struct smtp_server_cmd_ctx *cmd,
+ struct smtp_server_transaction *trans ATTR_UNUSED)
+{
+ struct _bad_pipelined_data2 *ctx = trans->context;
+ size_t size;
+ ssize_t ret;
+
+ while ((ret = i_stream_read(ctx->payload_input)) > 0 || ret == -2) {
+ size = i_stream_get_data_size(ctx->payload_input);
+ i_stream_skip(ctx->payload_input, size);
+ }
+
+ if (ret == 0)
+ return 0;
+ if (ret < 0 && ctx->payload_input->stream_errno != 0) {
+ /* Client probably disconnected */
+ return -1;
+ }
+
+ smtp_server_reply_all(cmd, 250, "2.0.0", "Accepted");
+ return 1;
+}
+
+static void
+test_server_bad_pipelined_data2(const struct smtp_server_settings *server_set)
+{
+ server_callbacks.conn_trans_free =
+ test_server_bad_pipelined_data2_trans_free;
+ server_callbacks.conn_cmd_rcpt =
+ test_server_bad_pipelined_data2_rcpt;
+ server_callbacks.conn_cmd_data_begin =
+ test_server_bad_pipelined_data2_data_begin;
+ server_callbacks.conn_cmd_data_continue =
+ test_server_bad_pipelined_data2_data_continue;
+ test_server_run(server_set);
+}
+
+/* test */
+
+static void test_bad_pipelined_data2(void)
+{
+ struct smtp_server_settings smtp_server_set;
+
+ test_server_defaults(&smtp_server_set);
+ smtp_server_set.capabilities =
+ SMTP_CAPABILITY_BINARYMIME | SMTP_CAPABILITY_CHUNKING;
+ smtp_server_set.max_client_idle_time_msecs = 1000;
+ smtp_server_set.max_recipients = 10;
+ smtp_server_set.max_pipelined_commands = 16;
+
+ test_begin("Bad pipelined DATA #2");
+ test_run_client_server(&smtp_server_set,
+ test_server_bad_pipelined_data2,
+ test_client_bad_pipelined_data2, 1);
+ test_end();
+}
+
+/*
+ * DATA with BINARYMIME
+ */
+
+/* client */
+
+static void test_data_binarymime_connected(struct client_connection *conn)
+{
+ o_stream_nsend_str(conn->conn.output,
+ "EHLO frop\r\n"
+ "MAIL FROM:<sender@example.com> BODY=BINARYMIME\r\n"
+ "RCPT TO:<recipient1@example.com>\r\n"
+ "DATA\r\n"
+ ".\r\n"
+ "RSET\r\n");
+}
+
+static void test_client_data_binarymime(unsigned int index)
+{
+ test_client_connected = test_data_binarymime_connected;
+ test_client_run(index);
+}
+
+/* server */
+
+static void
+test_server_data_binarymime_trans_free(
+ void *conn_ctx ATTR_UNUSED,
+ struct smtp_server_transaction *trans ATTR_UNUSED)
+{
+ io_loop_stop(ioloop);
+}
+
+static int
+test_server_data_binarymime_rcpt(
+ void *conn_ctx ATTR_UNUSED, struct smtp_server_cmd_ctx *cmd ATTR_UNUSED,
+ struct smtp_server_recipient *rcpt)
+{
+ if (debug) {
+ i_debug("RCPT TO:%s",
+ smtp_address_encode(rcpt->path));
+ }
+ return 1;
+}
+
+static int
+test_server_data_binarymime_data_begin(
+ void *conn_ctx ATTR_UNUSED, struct smtp_server_cmd_ctx *cmd ATTR_UNUSED,
+ struct smtp_server_transaction *trans ATTR_UNUSED,
+ struct istream *data_input ATTR_UNUSED)
+{
+ /* not supposed to get here */
+ i_assert(FALSE);
+ return 1;
+}
+
+static void
+test_server_data_binarymime(const struct smtp_server_settings *server_set)
+{
+ server_callbacks.conn_trans_free =
+ test_server_data_binarymime_trans_free;
+ server_callbacks.conn_cmd_rcpt =
+ test_server_data_binarymime_rcpt;
+ server_callbacks.conn_cmd_data_begin =
+ test_server_data_binarymime_data_begin;
+ test_server_run(server_set);
+}
+
+/* test */
+
+static void test_data_binarymime(void)
+{
+ struct smtp_server_settings smtp_server_set;
+
+ test_server_defaults(&smtp_server_set);
+ smtp_server_set.capabilities =
+ SMTP_CAPABILITY_BINARYMIME | SMTP_CAPABILITY_CHUNKING;
+ smtp_server_set.max_client_idle_time_msecs = 1000;
+ smtp_server_set.max_recipients = 10;
+
+ test_begin("DATA with BINARYMIME");
+ test_run_client_server(&smtp_server_set,
+ test_server_data_binarymime,
+ test_client_data_binarymime, 1);
+ test_end();
+}
+
+/*
+ * MAIL broken path
+ */
+
+/* client */
+
+struct _mail_broken_path_client {
+ struct smtp_reply_parser *parser;
+ unsigned int reply;
+
+ bool replied:1;
+};
+
+static void
+test_mail_broken_path_client_input(struct client_connection *conn)
+{
+ struct _mail_broken_path_client *ctx = conn->context;
+ struct smtp_reply *reply;
+ const char *error;
+ int ret;
+
+ while ((ret = smtp_reply_parse_next(ctx->parser, FALSE,
+ &reply, &error)) > 0) {
+ if (debug)
+ i_debug("REPLY: %s", smtp_reply_log(reply));
+
+ switch (ctx->reply++) {
+ case 0: /* greeting */
+ i_assert(reply->status == 220);
+ break;
+ case 1: /* bad command reply */
+ switch (client_index) {
+ case 0: case 1: case 2: case 3: case 4: case 5:
+ case 6: case 7: case 8: case 11: case 14: case 16:
+ i_assert(reply->status == 501);
+ break;
+ case 9: case 10: case 12: case 13: case 15: case 17:
+ i_assert(reply->status == 250);
+ break;
+ default:
+ i_info("STATUS: %u", reply->status);
+ i_unreached();
+ }
+ ctx->replied = TRUE;
+ io_loop_stop(ioloop);
+ connection_disconnect(&conn->conn);
+ return;
+ default:
+ i_unreached();
+ }
+ }
+
+ i_assert(ret >= 0);
+}
+
+static void
+test_mail_broken_path_client_connected(struct client_connection *conn)
+{
+ struct _mail_broken_path_client *ctx;
+
+ ctx = p_new(conn->pool, struct _mail_broken_path_client, 1);
+ ctx->parser = smtp_reply_parser_init(conn->conn.input, SIZE_MAX);
+ conn->context = ctx;
+
+ switch (client_index) {
+ case 0:
+ o_stream_nsend_str(
+ conn->conn.output,
+ "MAIL FROM: <hendrik@example.com>\r\n");
+ break;
+ case 1:
+ o_stream_nsend_str(
+ conn->conn.output,
+ "MAIL FROM:\t<hendrik@example.com>\r\n");
+ break;
+ case 2:
+ o_stream_nsend_str(
+ conn->conn.output,
+ "MAIL FROM:\t <hendrik@example.com>\r\n");
+ break;
+ case 3:
+ o_stream_nsend_str(
+ conn->conn.output,
+ "MAIL FROM:hendrik@example.com\r\n");
+ break;
+ case 4:
+ o_stream_nsend_str(
+ conn->conn.output,
+ "MAIL FROM: hendrik@example.com\r\n");
+ break;
+ case 5:
+ o_stream_nsend_str(
+ conn->conn.output,
+ "MAIL FROM:\r\n");
+ break;
+ case 6:
+ o_stream_nsend_str(
+ conn->conn.output,
+ "MAIL FROM: \r\n");
+ break;
+ case 7:
+ o_stream_nsend_str(
+ conn->conn.output,
+ "MAIL FROM: BODY=7BIT\r\n");
+ break;
+ case 8:
+ o_stream_nsend_str(
+ conn->conn.output,
+ "MAIL FROM: <>\r\n");
+ break;
+ case 9:
+ o_stream_nsend_str(
+ conn->conn.output,
+ "MAIL FROM:<hendrik@example.com>\r\n");
+ break;
+ case 10:
+ o_stream_nsend_str(
+ conn->conn.output,
+ "MAIL FROM:<>\r\n");
+ break;
+ case 11:
+ o_stream_nsend_str(
+ conn->conn.output,
+ "MAIL FROM:bla$die%bla@die&bla\r\n");
+ break;
+ case 12:
+ o_stream_nsend_str(
+ conn->conn.output,
+ "MAIL FROM:<u\"ser>\r\n");
+ break;
+ case 13:
+ o_stream_nsend_str(
+ conn->conn.output,
+ "MAIL FROM:<u\"ser@domain.tld>\r\n");
+ break;
+ case 14:
+ o_stream_nsend_str(
+ conn->conn.output,
+ "MAIL FROM:/@)$@)BLAARGH!@#$$\r\n");
+ break;
+ case 15:
+ o_stream_nsend_str(
+ conn->conn.output,
+ "MAIL FROM:</@)$@)BLAARGH!@#$$>\r\n");
+ break;
+ case 16:
+ o_stream_nsend_str(
+ conn->conn.output,
+ "MAIL FROM:f\xc3\xb6\xc3\xa4@\xc3\xb6\xc3\xa4\r\n");
+ break;
+ case 17:
+ o_stream_nsend_str(
+ conn->conn.output,
+ "MAIL FROM:<f\xc3\xb6\xc3\xa4@\xc3\xb6\xc3\xa4>\r\n");
+ break;
+ default:
+ i_unreached();
+ }
+}
+
+static void test_mail_broken_path_client_deinit(struct client_connection *conn)
+{
+ struct _mail_broken_path_client *ctx = conn->context;
+
+ i_assert(ctx->replied);
+ smtp_reply_parser_deinit(&ctx->parser);
+}
+
+static void test_client_mail_broken_path(unsigned int index)
+{
+ test_client_input = test_mail_broken_path_client_input;
+ test_client_connected = test_mail_broken_path_client_connected;
+ test_client_deinit = test_mail_broken_path_client_deinit;
+ test_client_run(index);
+}
+
+/* server */
+
+static void
+test_server_mail_broken_path_disconnect(void *context ATTR_UNUSED,
+ const char *reason)
+{
+ if (debug)
+ i_debug("Disconnect: %s", reason);
+}
+
+static int
+test_server_mail_broken_path_rcpt(
+ void *conn_ctx ATTR_UNUSED, struct smtp_server_cmd_ctx *cmd ATTR_UNUSED,
+ struct smtp_server_recipient *rcpt ATTR_UNUSED)
+{
+ test_assert(FALSE);
+ return 1;
+}
+
+static int
+test_server_mail_broken_path_data_begin(
+ void *conn_ctx ATTR_UNUSED, struct smtp_server_cmd_ctx *cmd ATTR_UNUSED,
+ struct smtp_server_transaction *trans ATTR_UNUSED,
+ struct istream *data_input ATTR_UNUSED)
+{
+ test_assert(FALSE);
+ return 1;
+}
+
+static void
+test_server_mail_broken_path(const struct smtp_server_settings *server_set)
+{
+ server_callbacks.conn_disconnect =
+ test_server_mail_broken_path_disconnect;
+
+ server_callbacks.conn_cmd_rcpt =
+ test_server_mail_broken_path_rcpt;
+ server_callbacks.conn_cmd_data_begin =
+ test_server_mail_broken_path_data_begin;
+ test_server_run(server_set);
+}
+
+/* test */
+
+static void test_mail_broken_path(void)
+{
+ struct smtp_server_settings smtp_server_set;
+
+ test_server_defaults(&smtp_server_set);
+ smtp_server_set.mail_path_allow_broken = TRUE;
+ smtp_server_set.max_client_idle_time_msecs = 1000;
+
+ test_begin("MAIL broken path");
+ test_run_client_server(&smtp_server_set,
+ test_server_mail_broken_path,
+ test_client_mail_broken_path, 18);
+ test_end();
+}
+
+/*
+ * Bad pipelined MAIL
+ */
+
+/* client */
+
+static void test_bad_pipelined_mail_connected(struct client_connection *conn)
+{
+ o_stream_nsend_str(conn->conn.output,
+ "MAIL FROM:<user1@example.com>\r\n"
+ "RCPT TO:<user2@example.com>\r\n"
+ "RCPT TO:<user3@example.com>\r\n"
+ "MAIL FROM:<user4@example.com>\r\n"
+ "DATA\r\n"
+ "FROP!\r\n"
+ ".\r\n"
+ "QUIT\r\n");
+}
+
+static void test_client_bad_pipelined_mail(unsigned int index)
+{
+ test_client_connected = test_bad_pipelined_mail_connected;
+ test_client_run(index);
+}
+
+/* server */
+
+struct _bad_pipelined_mail {
+ struct istream *payload_input;
+ struct io *io;
+};
+
+static void
+test_server_bad_pipelined_mail_trans_free(
+ void *conn_ctx ATTR_UNUSED, struct smtp_server_transaction *trans)
+{
+ struct _bad_pipelined_mail *ctx = trans->context;
+
+ i_free(ctx);
+ io_loop_stop(ioloop);
+}
+
+static int
+test_server_bad_pipelined_mail_rcpt(
+ void *conn_ctx ATTR_UNUSED, struct smtp_server_cmd_ctx *cmd ATTR_UNUSED,
+ struct smtp_server_recipient *rcpt)
+{
+ if (debug) {
+ i_debug("RCPT TO:%s",
+ smtp_address_encode(rcpt->path));
+ }
+ return 1;
+}
+
+static int
+test_server_bad_pipelined_mail_data_begin(
+ void *conn_ctx ATTR_UNUSED, struct smtp_server_cmd_ctx *cmd ATTR_UNUSED,
+ struct smtp_server_transaction *trans, struct istream *data_input)
+{
+ struct _bad_pipelined_mail *ctx;
+
+ if (debug)
+ i_debug("DATA");
+
+ ctx = i_new(struct _bad_pipelined_mail, 1);
+ trans->context = ctx;
+
+ ctx->payload_input = data_input;
+ return 0;
+}
+
+static int
+test_server_bad_pipelined_mail_data_continue(
+ void *conn_ctx ATTR_UNUSED, struct smtp_server_cmd_ctx *cmd,
+ struct smtp_server_transaction *trans ATTR_UNUSED)
+{
+ struct _bad_pipelined_mail *ctx = trans->context;
+ size_t size;
+ ssize_t ret;
+
+ while ((ret = i_stream_read(ctx->payload_input)) > 0 || ret == -2) {
+ size = i_stream_get_data_size(ctx->payload_input);
+ i_stream_skip(ctx->payload_input, size);
+ }
+
+ if (ret == 0)
+ return 0;
+ if (ret < 0 && ctx->payload_input->stream_errno != 0) {
+ /* Client probably disconnected */
+ return -1;
+ }
+
+ smtp_server_reply_all(cmd, 250, "2.0.0", "Accepted");
+ return 1;
+}
+
+static void
+test_server_bad_pipelined_mail(const struct smtp_server_settings *server_set)
+{
+ server_callbacks.conn_trans_free =
+ test_server_bad_pipelined_mail_trans_free;
+ server_callbacks.conn_cmd_rcpt =
+ test_server_bad_pipelined_mail_rcpt;
+ server_callbacks.conn_cmd_data_begin =
+ test_server_bad_pipelined_mail_data_begin;
+ server_callbacks.conn_cmd_data_continue =
+ test_server_bad_pipelined_mail_data_continue;
+ test_server_run(server_set);
+}
+
+/* test */
+
+static void test_bad_pipelined_mail(void)
+{
+ struct smtp_server_settings smtp_server_set;
+
+ test_server_defaults(&smtp_server_set);
+ smtp_server_set.capabilities =
+ SMTP_CAPABILITY_BINARYMIME | SMTP_CAPABILITY_CHUNKING;
+ smtp_server_set.max_client_idle_time_msecs = 1000;
+ smtp_server_set.max_recipients = 10;
+ smtp_server_set.max_pipelined_commands = 16;
+
+ test_begin("Bad pipelined MAIL");
+ test_run_client_server(&smtp_server_set,
+ test_server_bad_pipelined_mail,
+ test_client_bad_pipelined_mail, 1);
+ test_end();
+}
+
+/*
+ * All tests
+ */
+
+static void (*const test_functions[])(void) = {
+ test_slow_server,
+ test_slow_client,
+ test_hanging_command_payload,
+ test_bad_command,
+ test_many_bad_commands,
+ test_long_command,
+ test_long_auth_line,
+ test_long_auth_line_small_buf,
+ test_big_data,
+ test_bad_helo,
+ test_bad_mail,
+ test_bad_rcpt,
+ test_bad_vrfy,
+ test_bad_noop,
+ test_mail_workarounds,
+ test_rcpt_workarounds,
+ test_too_many_recipients,
+ test_data_no_mail,
+ test_data_no_rcpt,
+ test_bad_pipelined_data,
+ test_bad_pipelined_data2,
+ test_data_binarymime,
+ test_mail_broken_path,
+ test_bad_pipelined_mail,
+ NULL
+};
+
+/*
+ * Test client
+ */
+
+/* client connection */
+
+static void client_connection_input(struct connection *_conn)
+{
+ struct client_connection *conn = (struct client_connection *)_conn;
+
+ if (test_client_input != NULL)
+ test_client_input(conn);
+}
+
+static void client_connection_connected(struct connection *_conn, bool success)
+{
+ struct client_connection *conn = (struct client_connection *)_conn;
+
+ if (debug)
+ i_debug("Client connected");
+
+ if (success && test_client_connected != NULL)
+ test_client_connected(conn);
+}
+
+static void client_connection_init(const struct ip_addr *ip, in_port_t port)
+{
+ struct client_connection *conn;
+ pool_t pool;
+
+ pool = pool_alloconly_create("client connection", 256);
+ conn = p_new(pool, struct client_connection, 1);
+ conn->pool = pool;
+
+ connection_init_client_ip(client_conn_list, &conn->conn, NULL,
+ ip, port);
+ (void)connection_client_connect(&conn->conn);
+}
+
+static void client_connection_deinit(struct client_connection **_conn)
+{
+ struct client_connection *conn = *_conn;
+
+ *_conn = NULL;
+
+ if (test_client_deinit != NULL)
+ test_client_deinit(conn);
+ connection_deinit(&conn->conn);
+ pool_unref(&conn->pool);
+}
+
+static void client_connection_destroy(struct connection *_conn)
+{
+ struct client_connection *conn = (struct client_connection *)_conn;
+
+ client_connection_deinit(&conn);
+}
+
+/* */
+
+static struct connection_settings client_connection_set = {
+ .input_max_size = SIZE_MAX,
+ .output_max_size = SIZE_MAX,
+ .client = TRUE
+};
+
+static const struct connection_vfuncs client_connection_vfuncs = {
+ .destroy = client_connection_destroy,
+ .client_connected = client_connection_connected,
+ .input = client_connection_input
+};
+
+static void test_client_run(unsigned int index)
+{
+ client_index = index;
+
+ if (debug)
+ i_debug("client connecting to %u", bind_port);
+
+ client_conn_list = connection_list_init(&client_connection_set,
+ &client_connection_vfuncs);
+
+ client_connection_init(&bind_ip, bind_port);
+
+ io_loop_run(ioloop);
+
+ /* close server socket */
+ io_remove(&io_listen);
+
+ connection_list_deinit(&client_conn_list);
+}
+
+/*
+ * Test server
+ */
+
+static void test_server_defaults(struct smtp_server_settings *smtp_set)
+{
+ /* server settings */
+ i_zero(smtp_set);
+ smtp_set->max_client_idle_time_msecs = 5*1000;
+ smtp_set->max_pipelined_commands = 1;
+ smtp_set->auth_optional = TRUE;
+ smtp_set->debug = debug;
+}
+
+/* client connection */
+
+static void server_connection_free(void *context)
+{
+ struct server_connection *sconn = (struct server_connection *)context;
+
+ if (debug)
+ i_debug("Connection freed");
+
+ if (--server_pending == 0)
+ io_loop_stop(ioloop);
+ i_free(sconn);
+}
+
+static void server_connection_accept(void *context ATTR_UNUSED)
+{
+ struct smtp_server_connection *conn;
+ struct server_connection *sconn;
+ int fd;
+
+ /* accept new client */
+ fd = net_accept(fd_listen, NULL, NULL);
+ if (fd == -1)
+ return;
+ if (fd == -2) {
+ i_fatal("test server: accept() failed: %m");
+ }
+
+ if (debug)
+ i_debug("Accepted connection");
+
+ net_set_nonblock(fd, TRUE);
+
+ sconn = i_new(struct server_connection, 1);
+
+ server_callbacks.conn_free = server_connection_free;
+
+ if (server_io_buffer_size == 0) {
+ conn = smtp_server_connection_create(smtp_server, fd, fd,
+ NULL, 0, FALSE, NULL,
+ &server_callbacks, sconn);
+ } else {
+ struct istream *input;
+ struct ostream *output;
+
+ input = i_stream_create_fd(fd, server_io_buffer_size);
+ output = o_stream_create_fd(fd, server_io_buffer_size);
+ o_stream_set_no_error_handling(output, TRUE);
+
+ conn = smtp_server_connection_create_from_streams(
+ smtp_server, input, output, NULL, 0, NULL,
+ &server_callbacks, sconn);
+
+ i_stream_unref(&input);
+ o_stream_unref(&output);
+ }
+ smtp_server_connection_start(conn);
+}
+
+/* */
+
+static void test_server_timeout(void *context ATTR_UNUSED)
+{
+ i_fatal("Server timed out");
+}
+
+static void test_server_run(const struct smtp_server_settings *smtp_set)
+{
+ struct timeout *to;
+
+ to = timeout_add(SERVER_MAX_TIMEOUT_MSECS,
+ test_server_timeout, NULL);
+
+ /* open server socket */
+ io_listen = io_add(fd_listen, IO_READ, server_connection_accept, NULL);
+
+ smtp_server = smtp_server_init(smtp_set);
+
+ io_loop_run(ioloop);
+
+ if (debug)
+ i_debug("Server finished");
+
+ /* close server socket */
+ io_remove(&io_listen);
+ timeout_remove(&to);
+
+ smtp_server_deinit(&smtp_server);
+}
+
+/*
+ * Tests
+ */
+
+struct test_client_data {
+ unsigned int index;
+ test_client_init_t client_test;
+};
+
+static int test_open_server_fd(void)
+{
+ int fd = net_listen(&bind_ip, &bind_port, 128);
+ if (debug)
+ i_debug("server listening on %u", bind_port);
+ if (fd == -1) {
+ i_fatal("listen(%s:%u) failed: %m",
+ net_ip2addr(&bind_ip), bind_port);
+ }
+ return fd;
+}
+
+static int test_run_client(struct test_client_data *data)
+{
+ i_set_failure_prefix("CLIENT[%u]: ", data->index + 1);
+
+ if (debug)
+ i_debug("PID=%s", my_pid);
+
+ /* wait a little for server setup */
+ i_sleep_msecs(100);
+
+ ioloop = io_loop_create();
+ data->client_test(data->index);
+ io_loop_destroy(&ioloop);
+
+ if (debug)
+ i_debug("Terminated");
+
+ main_deinit();
+ return 0;
+}
+
+static void
+test_run_server(const struct smtp_server_settings *server_set,
+ test_server_init_t server_test,
+ unsigned int client_tests_count)
+{
+ i_set_failure_prefix("SERVER: ");
+
+ if (debug)
+ i_debug("PID=%s", my_pid);
+
+ i_zero(&server_callbacks);
+
+ server_pending = client_tests_count;
+ ioloop = io_loop_create();
+ server_test(server_set);
+ io_loop_destroy(&ioloop);
+
+ if (debug)
+ i_debug("Terminated");
+}
+
+static void
+test_run_client_server(const struct smtp_server_settings *server_set,
+ test_server_init_t server_test,
+ test_client_init_t client_test,
+ unsigned int client_tests_count)
+{
+ unsigned int i;
+
+ server_io_buffer_size = 0;
+
+ fd_listen = test_open_server_fd();
+
+ for (i = 0; i < client_tests_count; i++) {
+ struct test_client_data data;
+
+ i_zero(&data);
+ data.index = i;
+ data.client_test = client_test;
+
+ /* Fork client */
+ test_subprocess_fork(test_run_client, &data, FALSE);
+
+ }
+
+ /* Run server */
+ test_run_server(server_set, server_test, client_tests_count);
+
+ i_unset_failure_prefix();
+ i_close_fd(&fd_listen);
+ test_subprocess_kill_all(CLIENT_KILL_TIMEOUT_SECS);
+}
+
+/*
+ * Main
+ */
+
+static void main_init(void)
+{
+ /* nothing yet */
+}
+
+static void main_deinit(void)
+{
+ /* nothing yet; also called from sub-processes */
+}
+
+int main(int argc, char *argv[])
+{
+ int c;
+ int ret;
+
+ lib_init();
+ main_init();
+
+ while ((c = getopt(argc, argv, "D")) > 0) {
+ switch (c) {
+ case 'D':
+ debug = TRUE;
+ break;
+ default:
+ i_fatal("Usage: %s [-D]", argv[0]);
+ }
+ }
+
+ test_subprocesses_init(debug);
+
+ /* listen on localhost */
+ i_zero(&bind_ip);
+ bind_ip.family = AF_INET;
+ bind_ip.u.ip4.s_addr = htonl(INADDR_LOOPBACK);
+
+ ret = test_run(test_functions);
+
+ test_subprocesses_deinit();
+ main_deinit();
+ lib_deinit();
+
+ return ret;
+}
diff --git a/src/lib-smtp/test-smtp-submit.c b/src/lib-smtp/test-smtp-submit.c
new file mode 100644
index 0000000..f82dc4d
--- /dev/null
+++ b/src/lib-smtp/test-smtp-submit.c
@@ -0,0 +1,2199 @@
+/* Copyright (c) 2016-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "str.h"
+#include "hostpid.h"
+#include "ioloop.h"
+#include "istream.h"
+#include "istream-chain.h"
+#include "ostream.h"
+#include "time-util.h"
+#include "sleep.h"
+#include "unlink-directory.h"
+#include "write-full.h"
+#include "connection.h"
+#include "master-service.h"
+#include "istream-dot.h"
+#include "test-common.h"
+#include "test-subprocess.h"
+
+#include "smtp-address.h"
+#include "smtp-submit.h"
+
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <fcntl.h>
+
+#define SERVER_KILL_TIMEOUT_SECS 20
+
+static void main_deinit(void);
+
+static const char *test_message1 =
+ "Subject: Test message\r\n"
+ "To: rcpt@example.com\r\n"
+ "From: sender@example.com\r\n"
+ "\r\n"
+ "Test message\r\n";
+static const char *test_message2 =
+ "Subject: Test message\r\n"
+ "To: rcpt@example.com\r\n"
+ "From: sender@example.com\r\n"
+ "\r\n"
+ "Test message Test message Test message Test message Test message\r\n"
+ "Test message Test message Test message Test message Test message\r\n"
+ "Test message Test message Test message Test message Test message\r\n"
+ "Test message Test message Test message Test message Test message\r\n"
+ "Test message Test message Test message Test message Test message\r\n"
+ "Test message Test message Test message Test message Test message\r\n";
+
+/*
+ * Types
+ */
+
+struct server_connection {
+ struct connection conn;
+
+ void *context;
+
+ pool_t pool;
+};
+
+typedef void (*test_server_init_t)(unsigned int index);
+typedef bool
+(*test_client_init_t)(const struct smtp_submit_settings *submit_set);
+
+/*
+ * State
+ */
+
+/* common */
+static struct ip_addr bind_ip;
+static in_port_t *bind_ports = NULL;
+static struct ioloop *ioloop;
+static bool debug = FALSE;
+static char *tmp_dir = NULL;
+
+/* server */
+static struct io *io_listen;
+static int fd_listen = -1;
+static in_port_t server_port = 0;
+static struct connection_list *server_conn_list;
+static unsigned int server_index;
+static void (*test_server_input)(struct server_connection *conn);
+static void (*test_server_init)(struct server_connection *conn);
+static void (*test_server_deinit)(struct server_connection *conn);
+
+/* client */
+
+/*
+ * Forward declarations
+ */
+
+/* server */
+static void test_server_run(unsigned int index);
+static void server_connection_deinit(struct server_connection **_conn);
+
+/* client */
+static void
+test_client_defaults(struct smtp_submit_settings *smtp_set);
+static void test_client_deinit(void);
+
+static int
+test_client_smtp_send_simple(const struct smtp_submit_settings *smtp_set,
+ const char *message, const char *host,
+ const char **error_r);
+static int
+test_client_smtp_send_simple_port(const struct smtp_submit_settings *smtp_set,
+ const char *message, unsigned int port,
+ const char **error_r);
+
+/* test*/
+static const char *test_tmp_dir_get(void);
+
+static void test_message_delivery(const char *message, const char *file);
+
+static void
+test_run_client_server(const struct smtp_submit_settings *submit_set,
+ test_client_init_t client_test,
+ test_server_init_t server_test,
+ unsigned int server_tests_count) ATTR_NULL(3);
+
+/*
+ * Host lookup failed
+ */
+
+/* client */
+
+static bool
+test_client_host_lookup_failed(const struct smtp_submit_settings *submit_set)
+{
+ const char *error = NULL;
+ int ret;
+
+ ret = test_client_smtp_send_simple(submit_set, test_message1,
+ "host.invalid", &error);
+ test_out_reason("run (ret < 0)", ret < 0, error);
+
+ return FALSE;
+}
+
+/* test */
+
+static void test_host_lookup_failed(void)
+{
+ struct smtp_submit_settings smtp_submit_set;
+
+ test_client_defaults(&smtp_submit_set);
+ smtp_submit_set.submission_timeout = 5;
+
+ test_begin("host lookup failed");
+ test_expect_errors(1);
+ test_run_client_server(&smtp_submit_set,
+ test_client_host_lookup_failed, NULL, 0);
+ test_end();
+}
+
+/*
+ * Connection refused
+ */
+
+/* server */
+
+static void test_server_connection_refused(unsigned int index ATTR_UNUSED)
+{
+ i_close_fd(&fd_listen);
+}
+
+/* client */
+
+static bool
+test_client_connection_refused(const struct smtp_submit_settings *submit_set)
+{
+ const char *error = NULL;
+ int ret;
+
+ ret = test_client_smtp_send_simple_port(submit_set, test_message1,
+ bind_ports[0], &error);
+ test_out_reason("run (ret < 0)", ret < 0, error);
+
+ return FALSE;
+}
+
+/* test */
+
+static void test_connection_refused(void)
+{
+ struct smtp_submit_settings smtp_submit_set;
+
+ test_client_defaults(&smtp_submit_set);
+ smtp_submit_set.submission_timeout = 5;
+
+ test_begin("connection refused");
+ test_expect_errors(1);
+ test_run_client_server(&smtp_submit_set,
+ test_client_connection_refused,
+ test_server_connection_refused, 1);
+ test_end();
+}
+
+/*
+ * Connection timed out
+ */
+
+/* server */
+
+static void test_connection_timed_out_input(struct server_connection *conn)
+{
+ i_sleep_intr_secs(10);
+ server_connection_deinit(&conn);
+}
+
+static void test_server_connection_timed_out(unsigned int index)
+{
+ test_server_input = test_connection_timed_out_input;
+ test_server_run(index);
+}
+/* client */
+
+static bool
+test_client_connection_timed_out(const struct smtp_submit_settings *submit_set)
+{
+ time_t time;
+ const char *error = NULL;
+ int ret;
+
+ io_loop_time_refresh();
+ time = ioloop_time;
+ ret = test_client_smtp_send_simple_port(submit_set, test_message1,
+ bind_ports[0], &error);
+ test_out_reason("run (ret < 0)", ret < 0, error);
+
+ io_loop_time_refresh();
+ test_out("timeout", (ioloop_time - time) < 5);
+
+ return FALSE;
+}
+
+/* test */
+
+static void test_connection_timed_out(void)
+{
+ struct smtp_submit_settings smtp_submit_set;
+
+ test_client_defaults(&smtp_submit_set);
+ smtp_submit_set.submission_timeout = 1;
+
+ test_begin("connection timed out");
+ test_expect_errors(1);
+ test_run_client_server(&smtp_submit_set,
+ test_client_connection_timed_out,
+ test_server_connection_timed_out, 1);
+ test_end();
+}
+
+/*
+ * Bad greeting
+ */
+
+/* server */
+
+static void test_bad_greeting_input(struct server_connection *conn)
+{
+ server_connection_deinit(&conn);
+}
+
+static void test_bad_greeting_init(struct server_connection *conn)
+{
+ o_stream_nsend_str(conn->conn.output,
+ "554 No SMTP service here.\r\n");
+}
+
+static void test_server_bad_greeting(unsigned int index)
+{
+ test_server_init = test_bad_greeting_init;
+ test_server_input = test_bad_greeting_input;
+ test_server_run(index);
+}
+
+/* client */
+
+static bool
+test_client_bad_greeting(const struct smtp_submit_settings *submit_set)
+{
+ const char *error = NULL;
+ int ret;
+
+ ret = test_client_smtp_send_simple_port(submit_set, test_message1,
+ bind_ports[0], &error);
+ test_out_reason("run (ret == 0)", ret == 0, error);
+
+ return FALSE;
+}
+
+/* test */
+
+static void test_bad_greeting(void)
+{
+ struct smtp_submit_settings smtp_submit_set;
+
+ test_client_defaults(&smtp_submit_set);
+ smtp_submit_set.submission_timeout = 5;
+
+ test_begin("bad greeting");
+ test_expect_errors(0);
+ test_run_client_server(&smtp_submit_set,
+ test_client_bad_greeting,
+ test_server_bad_greeting, 1);
+ test_end();
+}
+
+/*
+ * Denied HELO
+ */
+
+/* server */
+
+static void test_denied_helo_input(struct server_connection *conn)
+{
+ const char *line;
+
+ line = i_stream_read_next_line(conn->conn.input);
+ if (line == NULL) {
+ if (conn->conn.input->eof)
+ server_connection_deinit(&conn);
+ return;
+ }
+ o_stream_nsend_str(conn->conn.output,
+ "550 Command rejected for testing reasons\r\n");
+ server_connection_deinit(&conn);
+}
+
+static void test_denied_helo_init(struct server_connection *conn)
+{
+ o_stream_nsend_str(conn->conn.output,
+ "220 testserver ESMTP Testfix (Debian/GNU)\r\n");
+}
+
+static void test_server_denied_helo(unsigned int index)
+{
+ test_server_init = test_denied_helo_init;
+ test_server_input = test_denied_helo_input;
+ test_server_run(index);
+}
+
+/* client */
+
+static bool
+test_client_denied_helo(const struct smtp_submit_settings *submit_set)
+{
+ const char *error = NULL;
+ int ret;
+
+ ret = test_client_smtp_send_simple_port(submit_set,
+ test_message1, bind_ports[0], &error);
+ test_out_reason("run (ret == 0)", ret == 0, error);
+
+ return FALSE;
+}
+
+/* test */
+
+static void test_denied_helo(void)
+{
+ struct smtp_submit_settings smtp_submit_set;
+
+ test_client_defaults(&smtp_submit_set);
+ smtp_submit_set.submission_timeout = 5;
+
+ test_begin("denied helo");
+ test_expect_errors(0);
+ test_run_client_server(&smtp_submit_set,
+ test_client_denied_helo,
+ test_server_denied_helo, 1);
+ test_end();
+}
+
+/*
+ * Disconnect HELO
+ */
+
+/* server */
+
+static void test_disconnect_helo_input(struct server_connection *conn)
+{
+ const char *line;
+
+ line = i_stream_read_next_line(conn->conn.input);
+ if (line == NULL) {
+ if (conn->conn.input->eof)
+ server_connection_deinit(&conn);
+ return;
+ }
+ server_connection_deinit(&conn);
+}
+
+static void test_disconnect_helo_init(struct server_connection *conn)
+{
+ o_stream_nsend_str(conn->conn.output,
+ "220 testserver ESMTP Testfix (Debian/GNU)\r\n");
+}
+
+static void test_server_disconnect_helo(unsigned int index)
+{
+ test_server_init = test_disconnect_helo_init;
+ test_server_input = test_disconnect_helo_input;
+ test_server_run(index);
+}
+
+/* client */
+
+static bool
+test_client_disconnect_helo(const struct smtp_submit_settings *submit_set)
+{
+ const char *error = NULL;
+ int ret;
+
+ ret = test_client_smtp_send_simple_port(submit_set, test_message1,
+ bind_ports[0], &error);
+ test_out_reason("run (ret < 0)", ret < 0, error);
+
+ return FALSE;
+}
+
+/* test */
+
+static void test_disconnect_helo(void)
+{
+ struct smtp_submit_settings smtp_submit_set;
+
+ test_client_defaults(&smtp_submit_set);
+ smtp_submit_set.submission_timeout = 5;
+
+ test_begin("disconnect helo");
+ test_expect_errors(0);
+ test_run_client_server(&smtp_submit_set,
+ test_client_disconnect_helo,
+ test_server_disconnect_helo, 1);
+ test_end();
+}
+
+/*
+ * Denied MAIL
+ */
+
+/* server */
+
+enum _denied_mail_state {
+ DENIED_MAIL_STATE_EHLO = 0,
+ DENIED_MAIL_STATE_MAIL_FROM
+};
+
+struct _denied_mail_server {
+ enum _denied_mail_state state;
+};
+
+static void test_denied_mail_input(struct server_connection *conn)
+{
+ struct _denied_mail_server *ctx;
+ const char *line;
+
+ if (conn->context == NULL) {
+ ctx = p_new(conn->pool, struct _denied_mail_server, 1);
+ conn->context = (void*)ctx;
+ } else {
+ ctx = (struct _denied_mail_server *)conn->context;
+ }
+
+ for (;;) {
+ line = i_stream_read_next_line(conn->conn.input);
+ if (line == NULL) {
+ if (conn->conn.input->eof)
+ server_connection_deinit(&conn);
+ return;
+ }
+
+ switch (ctx->state) {
+ case DENIED_MAIL_STATE_EHLO:
+ o_stream_nsend_str(conn->conn.output,
+ "250-testserver\r\n"
+ "250-PIPELINING\r\n"
+ "250-ENHANCEDSTATUSCODES\r\n"
+ "250-8BITMIME\r\n"
+ "250 DSN\r\n");
+ ctx->state = DENIED_MAIL_STATE_MAIL_FROM;
+ return;
+ case DENIED_MAIL_STATE_MAIL_FROM:
+ o_stream_nsend_str(
+ conn->conn.output,"453 4.3.2 "
+ "Incapable of accepting messages at this time.\r\n");
+ server_connection_deinit(&conn);
+ return;
+ }
+ i_unreached();
+ }
+}
+
+static void test_denied_mail_init(struct server_connection *conn)
+{
+ o_stream_nsend_str(conn->conn.output,
+ "220 testserver ESMTP Testfix (Debian/GNU)\r\n");
+}
+
+static void test_server_denied_mail(unsigned int index)
+{
+ test_server_init = test_denied_mail_init;
+ test_server_input = test_denied_mail_input;
+ test_server_run(index);
+}
+
+/* client */
+
+static bool
+test_client_denied_mail(const struct smtp_submit_settings *submit_set)
+{
+ const char *error = NULL;
+ int ret;
+
+ ret = test_client_smtp_send_simple_port(submit_set, test_message1,
+ bind_ports[0], &error);
+ test_out_reason("run (ret < 0)", ret < 0, error);
+
+ return FALSE;
+}
+
+/* test */
+
+static void test_denied_mail(void)
+{
+ struct smtp_submit_settings smtp_submit_set;
+
+ test_client_defaults(&smtp_submit_set);
+ smtp_submit_set.submission_timeout = 5;
+
+ test_begin("denied mail");
+ test_expect_errors(0);
+ test_run_client_server(&smtp_submit_set,
+ test_client_denied_mail,
+ test_server_denied_mail, 1);
+ test_end();
+}
+
+/*
+ * Denied RCPT
+ */
+
+/* server */
+
+enum _denied_rcpt_state {
+ DENIED_RCPT_STATE_EHLO = 0,
+ DENIED_RCPT_STATE_MAIL_FROM,
+ DENIED_RCPT_STATE_RCPT_TO
+};
+
+struct _denied_rcpt_server {
+ enum _denied_rcpt_state state;
+};
+
+static void test_denied_rcpt_input(struct server_connection *conn)
+{
+ struct _denied_rcpt_server *ctx;
+ const char *line;
+
+ if (conn->context == NULL) {
+ ctx = p_new(conn->pool, struct _denied_rcpt_server, 1);
+ conn->context = (void*)ctx;
+ } else {
+ ctx = (struct _denied_rcpt_server *)conn->context;
+ }
+
+ for (;;) {
+ line = i_stream_read_next_line(conn->conn.input);
+ if (line == NULL) {
+ if (conn->conn.input->eof)
+ server_connection_deinit(&conn);
+ return;
+ }
+
+ switch (ctx->state) {
+ case DENIED_RCPT_STATE_EHLO:
+ o_stream_nsend_str(conn->conn.output,
+ "250-testserver\r\n"
+ "250-PIPELINING\r\n"
+ "250-ENHANCEDSTATUSCODES\r\n"
+ "250-8BITMIME\r\n"
+ "250 DSN\r\n");
+ ctx->state = DENIED_RCPT_STATE_MAIL_FROM;
+ return;
+ case DENIED_RCPT_STATE_MAIL_FROM:
+ o_stream_nsend_str(conn->conn.output,
+ "250 2.1.0 Ok\r\n");
+ ctx->state = DENIED_RCPT_STATE_RCPT_TO;
+ continue;
+ case DENIED_RCPT_STATE_RCPT_TO:
+ o_stream_nsend_str(
+ conn->conn.output, "550 5.4.3 "
+ "Directory server failure\r\n");
+ server_connection_deinit(&conn);
+ return;
+ }
+ i_unreached();
+ }
+}
+
+static void test_denied_rcpt_init(struct server_connection *conn)
+{
+ o_stream_nsend_str(
+ conn->conn.output,
+ "220 testserver ESMTP Testfix (Debian/GNU)\r\n");
+}
+
+static void test_server_denied_rcpt(unsigned int index)
+{
+ test_server_init = test_denied_rcpt_init;
+ test_server_input = test_denied_rcpt_input;
+ test_server_run(index);
+}
+
+/* client */
+
+static bool
+test_client_denied_rcpt(const struct smtp_submit_settings *submit_set)
+{
+ const char *error = NULL;
+ int ret;
+
+ ret = test_client_smtp_send_simple_port(submit_set, test_message1,
+ bind_ports[0], &error);
+ test_out_reason("run (ret == 0)", ret == 0, error);
+
+ return FALSE;
+}
+
+/* test */
+
+static void test_denied_rcpt(void)
+{
+ struct smtp_submit_settings smtp_submit_set;
+
+ test_client_defaults(&smtp_submit_set);
+ smtp_submit_set.submission_timeout = 5;
+
+ test_begin("denied rcpt");
+ test_expect_errors(0);
+ test_run_client_server(&smtp_submit_set,
+ test_client_denied_rcpt,
+ test_server_denied_rcpt, 1);
+ test_end();
+}
+
+/*
+ * Denied second RCPT
+ */
+
+/* server */
+
+enum _denied_second_rcpt_state {
+ DENIED_SECOND_RCPT_STATE_EHLO = 0,
+ DENIED_SECOND_RCPT_STATE_MAIL_FROM,
+ DENIED_SECOND_RCPT_STATE_RCPT_TO,
+ DENIED_SECOND_RCPT_STATE_RCPT_TO2
+};
+
+struct _denied_second_rcpt_server {
+ enum _denied_second_rcpt_state state;
+};
+
+static void test_denied_second_rcpt_input(struct server_connection *conn)
+{
+ struct _denied_second_rcpt_server *ctx;
+ const char *line;
+
+ if (conn->context == NULL) {
+ ctx = p_new(conn->pool, struct _denied_second_rcpt_server, 1);
+ conn->context = (void*)ctx;
+ } else {
+ ctx = (struct _denied_second_rcpt_server *)conn->context;
+ }
+
+ for (;;) {
+ line = i_stream_read_next_line(conn->conn.input);
+ if (line == NULL) {
+ if (conn->conn.input->eof)
+ server_connection_deinit(&conn);
+ return;
+ }
+
+ switch (ctx->state) {
+ case DENIED_SECOND_RCPT_STATE_EHLO:
+ o_stream_nsend_str(conn->conn.output,
+ "250-testserver\r\n"
+ "250-PIPELINING\r\n"
+ "250-ENHANCEDSTATUSCODES\r\n"
+ "250-8BITMIME\r\n"
+ "250 DSN\r\n");
+ ctx->state = DENIED_SECOND_RCPT_STATE_MAIL_FROM;
+ return;
+ case DENIED_SECOND_RCPT_STATE_MAIL_FROM:
+ o_stream_nsend_str(conn->conn.output,
+ "250 2.1.0 Ok\r\n");
+ ctx->state = DENIED_SECOND_RCPT_STATE_RCPT_TO;
+ continue;
+ case DENIED_SECOND_RCPT_STATE_RCPT_TO:
+ o_stream_nsend_str(conn->conn.output,
+ "250 2.1.5 Ok\r\n");
+ ctx->state = DENIED_SECOND_RCPT_STATE_RCPT_TO2;
+ continue;
+ case DENIED_SECOND_RCPT_STATE_RCPT_TO2:
+ o_stream_nsend_str(conn->conn.output, "550 5.4.3 "
+ "Directory server failure\r\n");
+ server_connection_deinit(&conn);
+ return;
+ }
+ i_unreached();
+ }
+}
+
+static void test_denied_second_rcpt_init(struct server_connection *conn)
+{
+ o_stream_nsend_str(
+ conn->conn.output,
+ "220 testserver ESMTP Testfix (Debian/GNU)\r\n");
+}
+
+static void test_server_denied_second_rcpt(unsigned int index)
+{
+ test_server_init = test_denied_second_rcpt_init;
+ test_server_input = test_denied_second_rcpt_input;
+ test_server_run(index);
+}
+
+/* client */
+
+static void test_smtp_submit_input_init(struct smtp_submit_input *smtp_input_r)
+{
+ i_zero(smtp_input_r);
+ smtp_input_r->allow_root = TRUE;
+}
+
+static bool
+test_client_denied_second_rcpt(const struct smtp_submit_settings *submit_set)
+{
+ struct smtp_submit *smtp_submit;
+ struct smtp_submit_input smtp_input;
+ struct smtp_submit_settings smtp_submit_set;
+ struct ostream *output;
+ const char *error = NULL;
+ int ret;
+
+ smtp_submit_set = *submit_set;
+ smtp_submit_set.submission_host =
+ t_strdup_printf("127.0.0.1:%u", bind_ports[0]);
+ smtp_submit_set.submission_timeout = 1000;
+
+ test_smtp_submit_input_init(&smtp_input);
+ smtp_submit = smtp_submit_init_simple(
+ &smtp_input, &smtp_submit_set, &((struct smtp_address){
+ .localpart = "sender",
+ .domain = "example.com"}));
+
+ smtp_submit_add_rcpt(smtp_submit, &((struct smtp_address){
+ .localpart = "rcpt",
+ .domain = "example.com"}));
+ smtp_submit_add_rcpt(smtp_submit, &((struct smtp_address){
+ .localpart = "rcpt2",
+ .domain = "example.com"}));
+ output = smtp_submit_send(smtp_submit);
+ o_stream_nsend_str(output, test_message1);
+
+ ret = smtp_submit_run(smtp_submit, &error);
+ test_out_reason("run (ret == 0)", ret == 0, error);
+
+ smtp_submit_deinit(&smtp_submit);
+
+ return FALSE;
+}
+
+/* test */
+
+static void test_denied_second_rcpt(void)
+{
+ struct smtp_submit_settings smtp_submit_set;
+
+ test_client_defaults(&smtp_submit_set);
+
+ test_begin("denied second rcpt");
+ test_expect_errors(0);
+ test_run_client_server(&smtp_submit_set,
+ test_client_denied_second_rcpt,
+ test_server_denied_second_rcpt, 1);
+ test_end();
+}
+
+/*
+ * Denied DATA
+ */
+
+/* server */
+
+enum _denied_data_state {
+ DENIED_DATA_STATE_EHLO = 0,
+ DENIED_DATA_STATE_MAIL_FROM,
+ DENIED_DATA_STATE_RCPT_TO,
+ DENIED_DATA_STATE_DATA
+};
+
+struct _denied_data_server {
+ enum _denied_data_state state;
+};
+
+static void test_denied_data_input(struct server_connection *conn)
+{
+ struct _denied_data_server *ctx;
+ const char *line;
+
+ if (conn->context == NULL) {
+ ctx = p_new(conn->pool, struct _denied_data_server, 1);
+ conn->context = (void*)ctx;
+ } else {
+ ctx = (struct _denied_data_server *)conn->context;
+ }
+
+ for (;;) {
+ line = i_stream_read_next_line(conn->conn.input);
+ if (line == NULL) {
+ if (conn->conn.input->eof)
+ server_connection_deinit(&conn);
+ return;
+ }
+
+ switch (ctx->state) {
+ case DENIED_DATA_STATE_EHLO:
+ o_stream_nsend_str(conn->conn.output,
+ "250-testserver\r\n"
+ "250-PIPELINING\r\n"
+ "250-ENHANCEDSTATUSCODES\r\n"
+ "250-8BITMIME\r\n"
+ "250 DSN\r\n");
+ ctx->state = DENIED_DATA_STATE_MAIL_FROM;
+ return;
+ case DENIED_DATA_STATE_MAIL_FROM:
+ o_stream_nsend_str(conn->conn.output,
+ "250 2.1.0 Ok\r\n");
+ ctx->state = DENIED_DATA_STATE_RCPT_TO;
+ continue;
+ case DENIED_DATA_STATE_RCPT_TO:
+ o_stream_nsend_str(conn->conn.output,
+ "250 2.1.5 Ok\r\n");
+ ctx->state = DENIED_DATA_STATE_DATA;
+ continue;
+ case DENIED_DATA_STATE_DATA:
+ o_stream_nsend_str(conn->conn.output, "500 5.0.0 "
+ "Unacceptable recipients\r\n");
+ server_connection_deinit(&conn);
+ return;
+ }
+ i_unreached();
+ }
+}
+
+static void test_denied_data_init(struct server_connection *conn)
+{
+ o_stream_nsend_str(
+ conn->conn.output,
+ "220 testserver ESMTP Testfix (Debian/GNU)\r\n");
+}
+
+static void test_server_denied_data(unsigned int index)
+{
+ test_server_init = test_denied_data_init;
+ test_server_input = test_denied_data_input;
+ test_server_run(index);
+}
+
+/* client */
+
+static bool
+test_client_denied_data(const struct smtp_submit_settings *submit_set)
+{
+ const char *error = NULL;
+ int ret;
+
+ ret = test_client_smtp_send_simple_port(submit_set, test_message1,
+ bind_ports[0], &error);
+ test_out_reason("run (ret == 0)", ret == 0, error);
+
+ return FALSE;
+}
+
+/* test */
+
+static void test_denied_data(void)
+{
+ struct smtp_submit_settings smtp_submit_set;
+
+ test_client_defaults(&smtp_submit_set);
+ smtp_submit_set.submission_timeout = 5;
+
+ test_begin("denied data");
+ test_expect_errors(0);
+ test_run_client_server(&smtp_submit_set,
+ test_client_denied_data,
+ test_server_denied_data, 1);
+ test_end();
+}
+
+/*
+ * Data failure
+ */
+
+/* server */
+
+enum _data_failure_state {
+ DATA_FAILURE_STATE_EHLO = 0,
+ DATA_FAILURE_STATE_MAIL_FROM,
+ DATA_FAILURE_STATE_RCPT_TO,
+ DATA_FAILURE_STATE_DATA,
+ DATA_FAILURE_STATE_FINISH
+};
+
+struct _data_failure_server {
+ enum _data_failure_state state;
+};
+
+static void test_data_failure_input(struct server_connection *conn)
+{
+ struct _data_failure_server *ctx;
+ const char *line;
+
+ if (conn->context == NULL) {
+ ctx = p_new(conn->pool, struct _data_failure_server, 1);
+ conn->context = (void*)ctx;
+ } else {
+ ctx = (struct _data_failure_server *)conn->context;
+ }
+
+ for (;;) {
+ line = i_stream_read_next_line(conn->conn.input);
+ if (line == NULL) {
+ if (conn->conn.input->eof)
+ server_connection_deinit(&conn);
+ return;
+ }
+
+ switch (ctx->state) {
+ case DATA_FAILURE_STATE_EHLO:
+ o_stream_nsend_str(conn->conn.output,
+ "250-testserver\r\n"
+ "250-PIPELINING\r\n"
+ "250-ENHANCEDSTATUSCODES\r\n"
+ "250-8BITMIME\r\n"
+ "250 DSN\r\n");
+ ctx->state = DATA_FAILURE_STATE_MAIL_FROM;
+ return;
+ case DATA_FAILURE_STATE_MAIL_FROM:
+ o_stream_nsend_str(conn->conn.output,
+ "250 2.1.0 Ok\r\n");
+ ctx->state = DATA_FAILURE_STATE_RCPT_TO;
+ continue;
+ case DATA_FAILURE_STATE_RCPT_TO:
+ o_stream_nsend_str(conn->conn.output,
+ "250 2.1.5 Ok\r\n");
+ ctx->state = DATA_FAILURE_STATE_DATA;
+ continue;
+ case DATA_FAILURE_STATE_DATA:
+ o_stream_nsend_str(
+ conn->conn.output,
+ "354 End data with <CR><LF>.<CR><LF>\r\n");
+ ctx->state = DATA_FAILURE_STATE_FINISH;
+ continue;
+ case DATA_FAILURE_STATE_FINISH:
+ if (strcmp(line, ".") == 0) {
+ o_stream_nsend_str(
+ conn->conn.output, "552 5.2.3 "
+ "Message length exceeds administrative limit\r\n");
+ server_connection_deinit(&conn);
+ return;
+ }
+ continue;
+ }
+ i_unreached();
+ }
+}
+
+static void test_data_failure_init(struct server_connection *conn)
+{
+ o_stream_nsend_str(
+ conn->conn.output,
+ "220 testserver ESMTP Testfix (Debian/GNU)\r\n");
+}
+
+static void test_server_data_failure(unsigned int index)
+{
+ test_server_init = test_data_failure_init;
+ test_server_input = test_data_failure_input;
+ test_server_run(index);
+}
+
+/* client */
+
+static bool
+test_client_data_failure(const struct smtp_submit_settings *submit_set)
+{
+ const char *error = NULL;
+ int ret;
+
+ ret = test_client_smtp_send_simple_port(submit_set, test_message1,
+ bind_ports[0], &error);
+ test_out_reason("run (ret == 0)", ret == 0, error);
+
+ return FALSE;
+}
+
+/* test */
+
+static void test_data_failure(void)
+{
+ struct smtp_submit_settings smtp_submit_set;
+
+ test_client_defaults(&smtp_submit_set);
+ smtp_submit_set.submission_timeout = 5;
+
+ test_begin("data failure");
+ test_expect_errors(0);
+ test_run_client_server(&smtp_submit_set,
+ test_client_data_failure,
+ test_server_data_failure, 1);
+ test_end();
+}
+
+/*
+ * Data disconnect
+ */
+
+/* server */
+
+enum _data_disconnect_state {
+ DATA_DISCONNECT_STATE_EHLO = 0,
+ DATA_DISCONNECT_STATE_MAIL_FROM,
+ DATA_DISCONNECT_STATE_RCPT_TO,
+ DATA_DISCONNECT_STATE_DATA,
+ DATA_DISCONNECT_STATE_FINISH
+};
+
+struct _data_disconnect_server {
+ enum _data_disconnect_state state;
+};
+
+static void test_data_disconnect_input(struct server_connection *conn)
+{
+ struct _data_disconnect_server *ctx;
+ const char *line;
+
+ if (conn->context == NULL) {
+ ctx = p_new(conn->pool, struct _data_disconnect_server, 1);
+ conn->context = (void*)ctx;
+ } else {
+ ctx = (struct _data_disconnect_server *)conn->context;
+ }
+
+ for (;;) {
+ line = i_stream_read_next_line(conn->conn.input);
+ if (line == NULL) {
+ if (conn->conn.input->eof)
+ server_connection_deinit(&conn);
+ return;
+ }
+
+ switch (ctx->state) {
+ case DATA_DISCONNECT_STATE_EHLO:
+ o_stream_nsend_str(conn->conn.output,
+ "250-testserver\r\n"
+ "250-PIPELINING\r\n"
+ "250-ENHANCEDSTATUSCODES\r\n"
+ "250-8BITMIME\r\n"
+ "250 DSN\r\n");
+ ctx->state = DATA_DISCONNECT_STATE_MAIL_FROM;
+ return;
+ case DATA_DISCONNECT_STATE_MAIL_FROM:
+ o_stream_nsend_str(conn->conn.output,
+ "250 2.1.0 Ok\r\n");
+ ctx->state = DATA_DISCONNECT_STATE_RCPT_TO;
+ continue;
+ case DATA_DISCONNECT_STATE_RCPT_TO:
+ o_stream_nsend_str(conn->conn.output,
+ "250 2.1.5 Ok\r\n");
+ ctx->state = DATA_DISCONNECT_STATE_DATA;
+ continue;
+ case DATA_DISCONNECT_STATE_DATA:
+ o_stream_nsend_str(
+ conn->conn.output,
+ "354 End data with <CR><LF>.<CR><LF>\r\n");
+ ctx->state = DATA_DISCONNECT_STATE_FINISH;
+ continue;
+ case DATA_DISCONNECT_STATE_FINISH:
+ server_connection_deinit(&conn);
+ return;
+ }
+ i_unreached();
+ }
+}
+
+static void test_data_disconnect_init(struct server_connection *conn)
+{
+ o_stream_nsend_str(
+ conn->conn.output,
+ "220 testserver ESMTP Testfix (Debian/GNU)\r\n");
+}
+
+static void test_server_data_disconnect(unsigned int index)
+{
+ test_server_init = test_data_disconnect_init;
+ test_server_input = test_data_disconnect_input;
+ test_server_run(index);
+}
+
+/* client */
+
+static bool
+test_client_data_disconnect(const struct smtp_submit_settings *submit_set)
+{
+ const char *error = NULL;
+ int ret;
+
+ ret = test_client_smtp_send_simple_port(submit_set, test_message1,
+ bind_ports[0], &error);
+ test_out_reason("run (ret < 0)", ret < 0, error);
+
+ return FALSE;
+}
+
+/* test */
+
+static void test_data_disconnect(void)
+{
+ struct smtp_submit_settings smtp_submit_set;
+
+ test_client_defaults(&smtp_submit_set);
+ smtp_submit_set.submission_timeout = 5;
+
+ test_begin("data disconnect");
+ test_expect_errors(0);
+ test_run_client_server(&smtp_submit_set,
+ test_client_data_disconnect,
+ test_server_data_disconnect, 1);
+ test_end();
+}
+
+/*
+ * Data timout
+ */
+
+/* server */
+
+enum _data_timout_state {
+ DATA_TIMEOUT_STATE_EHLO = 0,
+ DATA_TIMEOUT_STATE_MAIL_FROM,
+ DATA_TIMEOUT_STATE_RCPT_TO,
+ DATA_TIMEOUT_STATE_DATA,
+ DATA_TIMEOUT_STATE_FINISH
+};
+
+struct _data_timout_server {
+ enum _data_timout_state state;
+};
+
+static void test_data_timout_input(struct server_connection *conn)
+{
+ struct _data_timout_server *ctx;
+ const char *line;
+
+ if (conn->context == NULL) {
+ ctx = p_new(conn->pool, struct _data_timout_server, 1);
+ conn->context = (void*)ctx;
+ } else {
+ ctx = (struct _data_timout_server *)conn->context;
+ }
+
+ for (;;) {
+ line = i_stream_read_next_line(conn->conn.input);
+ if (line == NULL) {
+ if (conn->conn.input->eof)
+ server_connection_deinit(&conn);
+ return;
+ }
+
+ switch (ctx->state) {
+ case DATA_TIMEOUT_STATE_EHLO:
+ o_stream_nsend_str(conn->conn.output,
+ "250-testserver\r\n"
+ "250-PIPELINING\r\n"
+ "250-ENHANCEDSTATUSCODES\r\n"
+ "250-8BITMIME\r\n"
+ "250 DSN\r\n");
+ ctx->state = DATA_TIMEOUT_STATE_MAIL_FROM;
+ return;
+ case DATA_TIMEOUT_STATE_MAIL_FROM:
+ o_stream_nsend_str(conn->conn.output,
+ "250 2.1.0 Ok\r\n");
+ ctx->state = DATA_TIMEOUT_STATE_RCPT_TO;
+ continue;
+ case DATA_TIMEOUT_STATE_RCPT_TO:
+ o_stream_nsend_str(conn->conn.output,
+ "250 2.1.5 Ok\r\n");
+ ctx->state = DATA_TIMEOUT_STATE_DATA;
+ continue;
+ case DATA_TIMEOUT_STATE_DATA:
+ o_stream_nsend_str(
+ conn->conn.output,
+ "354 End data with <CR><LF>.<CR><LF>\r\n");
+ ctx->state = DATA_TIMEOUT_STATE_FINISH;
+ continue;
+ case DATA_TIMEOUT_STATE_FINISH:
+ i_sleep_intr_secs(10);
+ server_connection_deinit(&conn);
+ return;
+ }
+ i_unreached();
+ }
+}
+
+static void test_data_timout_init(struct server_connection *conn)
+{
+ o_stream_nsend_str(
+ conn->conn.output,
+ "220 testserver ESMTP Testfix (Debian/GNU)\r\n");
+}
+
+static void test_server_data_timout(unsigned int index)
+{
+ test_server_init = test_data_timout_init;
+ test_server_input = test_data_timout_input;
+ test_server_run(index);
+}
+
+/* client */
+
+static bool
+test_client_data_timout(const struct smtp_submit_settings *submit_set)
+{
+ time_t time;
+ const char *error = NULL;
+ int ret;
+
+ io_loop_time_refresh();
+ time = ioloop_time;
+ ret = test_client_smtp_send_simple_port(submit_set, test_message1,
+ bind_ports[0], &error);
+ test_out_reason("run (ret < 0)", ret < 0, error);
+
+ io_loop_time_refresh();
+ test_out("timeout", (ioloop_time - time) < 5);
+
+ return FALSE;
+}
+
+/* test */
+
+static void test_data_timeout(void)
+{
+ struct smtp_submit_settings smtp_submit_set;
+
+ test_client_defaults(&smtp_submit_set);
+ smtp_submit_set.submission_timeout = 2;
+
+ test_begin("data timeout");
+ test_expect_errors(1);
+ test_run_client_server(&smtp_submit_set,
+ test_client_data_timout,
+ test_server_data_timout, 1);
+ test_end();
+}
+
+/*
+ * Successful delivery
+ */
+
+/* server */
+
+enum _successful_delivery_state {
+ SUCCESSFUL_DELIVERY_STATE_EHLO = 0,
+ SUCCESSFUL_DELIVERY_STATE_MAIL_FROM,
+ SUCCESSFUL_DELIVERY_STATE_RCPT_TO,
+ SUCCESSFUL_DELIVERY_STATE_DATA,
+ SUCCESSFUL_DELIVERY_STATE_FINISH
+};
+
+struct _successful_delivery_server {
+ enum _successful_delivery_state state;
+
+ char *file_path;
+ struct istream *dot_input;
+ struct ostream *file;
+};
+
+static void test_successful_delivery_input(struct server_connection *conn)
+{
+ struct _successful_delivery_server *ctx;
+ const char *line;
+
+ if (conn->context == NULL) {
+ ctx = p_new(conn->pool, struct _successful_delivery_server, 1);
+ conn->context = (void*)ctx;
+ } else {
+ ctx = (struct _successful_delivery_server *)conn->context;
+ }
+
+ // FIXME: take structure from test-smtp-client-errors
+
+ for (;;) {
+ if (ctx->state == SUCCESSFUL_DELIVERY_STATE_FINISH) {
+ enum ostream_send_istream_result res;
+
+ if (ctx->dot_input == NULL) {
+ int fd;
+
+ ctx->dot_input =
+ i_stream_create_dot(conn->conn.input, TRUE);
+ ctx->file_path = p_strdup_printf(
+ conn->pool, "%s/message-%u.eml",
+ test_tmp_dir_get(), server_port);
+
+ if ((fd = open(ctx->file_path, O_WRONLY | O_CREAT,
+ 0600)) < 0) {
+ i_fatal("failed create tmp file for message: "
+ "open(%s) failed: %m",
+ ctx->file_path);
+ }
+ ctx->file = o_stream_create_fd_autoclose(
+ &fd, IO_BLOCK_SIZE);
+ }
+
+ res = o_stream_send_istream(ctx->file, ctx->dot_input);
+ switch (res) {
+ case OSTREAM_SEND_ISTREAM_RESULT_FINISHED:
+ break;
+ case OSTREAM_SEND_ISTREAM_RESULT_WAIT_INPUT:
+ case OSTREAM_SEND_ISTREAM_RESULT_WAIT_OUTPUT:
+ return;
+ case OSTREAM_SEND_ISTREAM_RESULT_ERROR_INPUT:
+ i_error("test server: "
+ "Failed to read all message payload [%s]",
+ ctx->file_path);
+ server_connection_deinit(&conn);
+ return;
+ case OSTREAM_SEND_ISTREAM_RESULT_ERROR_OUTPUT:
+ i_error("test server: "
+ "Failed to write all message payload [%s]",
+ ctx->file_path);
+ server_connection_deinit(&conn);
+ return;
+ }
+
+ o_stream_nsend_str(
+ conn->conn.output,
+ "250 2.0.0 Ok: queued as 73BDE342129\r\n");
+ ctx->state = SUCCESSFUL_DELIVERY_STATE_MAIL_FROM;
+ continue;
+ }
+
+ line = i_stream_read_next_line(conn->conn.input);
+ if (line == NULL) {
+ if (conn->conn.input->eof)
+ server_connection_deinit(&conn);
+ return;
+ }
+
+ switch (ctx->state) {
+ case SUCCESSFUL_DELIVERY_STATE_EHLO:
+ o_stream_nsend_str(conn->conn.output,
+ "250-testserver\r\n"
+ "250-PIPELINING\r\n"
+ "250-ENHANCEDSTATUSCODES\r\n"
+ "250-8BITMIME\r\n"
+ "250 DSN\r\n");
+ ctx->state = SUCCESSFUL_DELIVERY_STATE_MAIL_FROM;
+ return;
+ case SUCCESSFUL_DELIVERY_STATE_MAIL_FROM:
+ o_stream_nsend_str(conn->conn.output,
+ "250 2.1.0 Ok\r\n");
+ ctx->state = SUCCESSFUL_DELIVERY_STATE_RCPT_TO;
+ continue;
+ case SUCCESSFUL_DELIVERY_STATE_RCPT_TO:
+ o_stream_nsend_str(conn->conn.output,
+ "250 2.1.5 Ok\r\n");
+ ctx->state = SUCCESSFUL_DELIVERY_STATE_DATA;
+ continue;
+ case SUCCESSFUL_DELIVERY_STATE_DATA:
+ o_stream_nsend_str(
+ conn->conn.output,
+ "354 End data with <CR><LF>.<CR><LF>\r\n");
+ ctx->state = SUCCESSFUL_DELIVERY_STATE_FINISH;
+ continue;
+ case SUCCESSFUL_DELIVERY_STATE_FINISH:
+ break;
+ }
+ i_unreached();
+ }
+}
+
+static void test_successful_delivery_init(struct server_connection *conn)
+{
+ o_stream_nsend_str(conn->conn.output,
+ "220 testserver ESMTP Testfix (Debian/GNU)\r\n");
+}
+
+static void test_successful_delivery_deinit(struct server_connection *conn)
+{
+ struct _successful_delivery_server *ctx =
+ (struct _successful_delivery_server *)conn->context;
+
+ i_stream_unref(&ctx->dot_input);
+ o_stream_unref(&ctx->file);
+}
+
+static void test_server_successful_delivery(unsigned int index)
+{
+ test_server_init = test_successful_delivery_init;
+ test_server_input = test_successful_delivery_input;
+ test_server_deinit = test_successful_delivery_deinit;
+ test_server_run(index);
+}
+
+/* client */
+
+static bool
+test_client_successful_delivery(const struct smtp_submit_settings *submit_set)
+{
+ const char *error = NULL;
+ int ret;
+
+ /* send the message */
+ ret = test_client_smtp_send_simple_port(submit_set, test_message1,
+ bind_ports[0], &error);
+ test_out_reason("run (ret > 0)", ret > 0, error);
+
+ /* verify delivery */
+ test_message_delivery(test_message1,
+ t_strdup_printf("%s/message-%u.eml",
+ test_tmp_dir_get(),
+ bind_ports[0]));
+
+ return FALSE;
+}
+
+struct _parallel_delivery_client {
+ unsigned int count;
+};
+
+static void
+test_client_parallel_delivery_callback(const struct smtp_submit_result *result,
+ struct _parallel_delivery_client *ctx)
+{
+ if (result->status <= 0)
+ i_error("Submit failed: %s", result->error);
+
+ if (--ctx->count == 0)
+ io_loop_stop(current_ioloop);
+}
+
+static bool
+test_client_parallel_delivery(const struct smtp_submit_settings *submit_set)
+{
+ struct smtp_submit_input smtp_input;
+ struct smtp_submit_settings smtp_submit_set;
+ struct _parallel_delivery_client *ctx;
+ struct smtp_submit *smtp_submit1, *smtp_submit2;
+ struct ostream *output;
+ struct ioloop *ioloop;
+
+ ioloop = io_loop_create();
+
+ ctx = i_new(struct _parallel_delivery_client, 1);
+ ctx->count = 2;
+
+ smtp_submit_set = *submit_set;
+ smtp_submit_set.submission_timeout = 5;
+
+ /* submit 1 */
+ test_smtp_submit_input_init(&smtp_input);
+ smtp_submit_set.submission_host =
+ t_strdup_printf("127.0.0.1:%u", bind_ports[0]);
+ smtp_submit1 = smtp_submit_init_simple(
+ &smtp_input, &smtp_submit_set, &((struct smtp_address){
+ .localpart = "sender",
+ .domain = "example.com"}));
+
+ smtp_submit_add_rcpt(
+ smtp_submit1, &((struct smtp_address){
+ .localpart = "rcpt",
+ .domain = "example.com"}));
+ output = smtp_submit_send(smtp_submit1);
+ o_stream_nsend_str(output, test_message1);
+
+ smtp_submit_run_async(
+ smtp_submit1, test_client_parallel_delivery_callback, ctx);
+
+ /* submit 2 */
+ test_smtp_submit_input_init(&smtp_input);
+ smtp_submit_set.submission_host =
+ t_strdup_printf("127.0.0.1:%u", bind_ports[1]);
+ smtp_submit2 = smtp_submit_init_simple(
+ &smtp_input, &smtp_submit_set, &((struct smtp_address){
+ .localpart = "sender",
+ .domain = "example.com"}));
+
+ smtp_submit_add_rcpt(
+ smtp_submit2, &((struct smtp_address){
+ .localpart = "rcpt",
+ .domain = "example.com"}));
+ output = smtp_submit_send(smtp_submit2);
+ o_stream_nsend_str(output, test_message2);
+
+ smtp_submit_run_async(
+ smtp_submit2, test_client_parallel_delivery_callback, ctx);
+
+ io_loop_run(ioloop);
+
+ smtp_submit_deinit(&smtp_submit1);
+ smtp_submit_deinit(&smtp_submit2);
+ io_loop_destroy(&ioloop);
+
+ /* verify delivery */
+ test_message_delivery(test_message1,
+ t_strdup_printf("%s/message-%u.eml",
+ test_tmp_dir_get(),
+ bind_ports[0]));
+ test_message_delivery(test_message2,
+ t_strdup_printf("%s/message-%u.eml",
+ test_tmp_dir_get(),
+ bind_ports[1]));
+ i_free(ctx);
+
+ return FALSE;
+}
+
+/* test */
+
+static void test_successful_delivery(void)
+{
+ struct smtp_submit_settings smtp_submit_set;
+
+ test_client_defaults(&smtp_submit_set);
+
+ test_begin("successful delivery");
+ test_expect_errors(0);
+ test_run_client_server(&smtp_submit_set,
+ test_client_successful_delivery,
+ test_server_successful_delivery, 1);
+ test_end();
+
+ test_begin("parallel delivery");
+ test_expect_errors(0);
+ test_run_client_server(&smtp_submit_set,
+ test_client_parallel_delivery,
+ test_server_successful_delivery, 2);
+ test_end();
+}
+
+/*
+ * Failed sendmail
+ */
+
+/* client */
+
+static bool
+test_client_failed_sendmail(const struct smtp_submit_settings *submit_set)
+{
+ struct smtp_submit_settings smtp_submit_set;
+ struct smtp_submit_input smtp_input;
+ struct smtp_submit *smtp_submit;
+ struct ostream *output;
+ const char *sendmail_path, *error = NULL;
+ int ret;
+
+ sendmail_path = TEST_BIN_DIR"/sendmail-exit-1.sh";
+
+ smtp_submit_set = *submit_set;
+ smtp_submit_set.sendmail_path = sendmail_path;
+ smtp_submit_set.submission_timeout = 5;
+
+ test_smtp_submit_input_init(&smtp_input);
+ smtp_submit = smtp_submit_init_simple(
+ &smtp_input, &smtp_submit_set, &((struct smtp_address){
+ .localpart = "sender",
+ .domain = "example.com"}));
+
+ smtp_submit_add_rcpt(smtp_submit, &((struct smtp_address){
+ .localpart = "rcpt",
+ .domain = "example.com"}));
+ output = smtp_submit_send(smtp_submit);
+ o_stream_nsend_str(output, test_message1);
+
+ ret = smtp_submit_run(smtp_submit, &error);
+ test_out_reason("run (ret < 0)", ret < 0, error);
+
+ smtp_submit_deinit(&smtp_submit);
+
+ return FALSE;
+}
+
+/* test */
+
+static void test_failed_sendmail(void)
+{
+ struct smtp_submit_settings smtp_submit_set;
+
+ test_client_defaults(&smtp_submit_set);
+
+ test_begin("failed sendmail");
+ test_expect_errors(0);
+ test_run_client_server(&smtp_submit_set,
+ test_client_failed_sendmail, NULL, 0);
+ test_end();
+}
+
+/*
+ * Successful sendmail
+ */
+
+/* client */
+
+static bool
+test_client_successful_sendmail(const struct smtp_submit_settings *submit_set)
+{
+ struct smtp_submit_input smtp_input;
+ struct smtp_submit_settings smtp_submit_set;
+ struct smtp_submit *smtp_submit;
+ struct ostream *output;
+ const char *sendmail_path, *msg_path, *error = NULL;
+ int ret;
+
+ msg_path = t_strdup_printf("%s/message.eml", test_tmp_dir_get());
+
+ sendmail_path = t_strdup_printf(
+ TEST_BIN_DIR"/sendmail-success.sh %s", msg_path);
+
+ smtp_submit_set = *submit_set;
+ smtp_submit_set.sendmail_path = sendmail_path;
+ smtp_submit_set.submission_timeout = 5;
+
+ test_smtp_submit_input_init(&smtp_input);
+ smtp_submit = smtp_submit_init_simple(
+ &smtp_input, &smtp_submit_set, &((struct smtp_address){
+ .localpart = "sender",
+ .domain = "example.com"}));
+
+ smtp_submit_add_rcpt(smtp_submit, &((struct smtp_address){
+ .localpart = "rcpt",
+ .domain = "example.com"}));
+ output = smtp_submit_send(smtp_submit);
+ o_stream_nsend_str(output, test_message1);
+
+ ret = smtp_submit_run(smtp_submit, &error);
+ test_out_reason("run (ret > 0)", ret > 0, error);
+
+ smtp_submit_deinit(&smtp_submit);
+
+ /* verify delivery */
+ test_message_delivery(test_message1, msg_path);
+
+ return FALSE;
+}
+
+/* test */
+
+static void test_successful_sendmail(void)
+{
+ struct smtp_submit_settings smtp_submit_set;
+
+ test_client_defaults(&smtp_submit_set);
+
+ test_begin("successful sendmail");
+ test_expect_errors(0);
+ test_run_client_server(&smtp_submit_set,
+ test_client_successful_sendmail, NULL, 0);
+ test_end();
+}
+
+/*
+ * Parallel sendmail
+ */
+
+/* client */
+
+struct _parallel_sendmail_client {
+ unsigned int count;
+};
+
+static void
+test_client_parallel_sendmail_callback(const struct smtp_submit_result *result,
+ struct _parallel_sendmail_client *ctx)
+{
+ if (result->status <= 0)
+ i_error("Submit failed: %s", result->error);
+
+ if (--ctx->count == 0)
+ io_loop_stop(current_ioloop);
+}
+
+static bool
+test_client_parallel_sendmail(const struct smtp_submit_settings *submit_set)
+{
+ struct smtp_submit_input smtp_input;
+ struct smtp_submit_settings smtp_submit_set;
+ struct _parallel_sendmail_client *ctx;
+ struct smtp_submit *smtp_submit1, *smtp_submit2;
+ struct ostream *output;
+ const char *sendmail_path1, *sendmail_path2;
+ const char *msg_path1, *msg_path2;
+ struct ioloop *ioloop;
+
+ ctx = i_new(struct _parallel_sendmail_client, 1);
+ ctx->count = 2;
+
+ ioloop = io_loop_create();
+
+ msg_path1 = t_strdup_printf("%s/message1.eml", test_tmp_dir_get());
+ msg_path2 = t_strdup_printf("%s/message2.eml", test_tmp_dir_get());
+
+ sendmail_path1 = t_strdup_printf(
+ TEST_BIN_DIR"/sendmail-success.sh %s", msg_path1);
+ sendmail_path2 = t_strdup_printf(
+ TEST_BIN_DIR"/sendmail-success.sh %s", msg_path2);
+
+ smtp_submit_set = *submit_set;
+ smtp_submit_set.submission_timeout = 5;
+
+ /* submit 1 */
+ test_smtp_submit_input_init(&smtp_input);
+ smtp_submit_set.sendmail_path = sendmail_path1;
+ smtp_submit1 = smtp_submit_init_simple(
+ &smtp_input, &smtp_submit_set, &((struct smtp_address){
+ .localpart = "sender",
+ .domain = "example.com"}));
+
+ smtp_submit_add_rcpt(
+ smtp_submit1, &((struct smtp_address){
+ .localpart = "rcpt",
+ .domain = "example.com"}));
+ output = smtp_submit_send(smtp_submit1);
+ o_stream_nsend_str(output, test_message1);
+
+ smtp_submit_run_async(
+ smtp_submit1, test_client_parallel_sendmail_callback, ctx);
+
+ /* submit 2 */
+ test_smtp_submit_input_init(&smtp_input);
+ smtp_submit_set.sendmail_path = sendmail_path2;
+ smtp_submit2 = smtp_submit_init_simple(
+ &smtp_input, &smtp_submit_set, &((struct smtp_address){
+ .localpart = "sender",
+ .domain = "example.com"}));
+
+ smtp_submit_add_rcpt(
+ smtp_submit2, &((struct smtp_address){
+ .localpart = "rcpt",
+ .domain = "example.com"}));
+ output = smtp_submit_send(smtp_submit2);
+ o_stream_nsend_str(output, test_message2);
+
+ smtp_submit_run_async(
+ smtp_submit2, test_client_parallel_sendmail_callback, ctx);
+
+ io_loop_run(ioloop);
+
+ smtp_submit_deinit(&smtp_submit1);
+ smtp_submit_deinit(&smtp_submit2);
+ io_loop_destroy(&ioloop);
+
+ /* verify delivery */
+ test_message_delivery(test_message1, msg_path1);
+ test_message_delivery(test_message2, msg_path2);
+
+ i_free(ctx);
+
+ return FALSE;
+}
+
+/* test */
+
+static void test_parallel_sendmail(void)
+{
+ struct smtp_submit_settings smtp_submit_set;
+
+ test_client_defaults(&smtp_submit_set);
+
+ test_begin("parallel sendmail");
+ test_expect_errors(0);
+ test_run_client_server(&smtp_submit_set,
+ test_client_parallel_sendmail, NULL, 0);
+ test_end();
+}
+
+/*
+ * All tests
+ */
+
+static void (*const test_functions[])(void) = {
+ test_host_lookup_failed,
+ test_connection_refused,
+ test_connection_timed_out,
+ test_bad_greeting,
+ test_denied_helo,
+ test_disconnect_helo,
+ test_denied_mail,
+ test_denied_rcpt,
+ test_denied_second_rcpt,
+ test_denied_data,
+ test_data_failure,
+ test_data_disconnect,
+ test_data_timeout,
+ test_successful_delivery,
+ test_failed_sendmail,
+ test_successful_sendmail,
+ test_parallel_sendmail,
+ NULL
+};
+
+/*
+ * Test client
+ */
+
+static void test_client_defaults(struct smtp_submit_settings *smtp_set)
+{
+ i_zero(smtp_set);
+ smtp_set->hostname = "test";
+ smtp_set->submission_host = "";
+ smtp_set->sendmail_path = "/bin/false";
+ smtp_set->mail_debug = debug;
+}
+
+static void test_client_deinit(void)
+{
+}
+
+static int
+test_client_smtp_send_simple(const struct smtp_submit_settings *smtp_set,
+ const char *message, const char *host,
+ const char **error_r)
+{
+ struct smtp_submit_input smtp_input;
+ struct smtp_submit_settings smtp_submit_set;
+ struct smtp_submit *smtp_submit;
+ struct ostream *output;
+ int ret;
+
+ /* send the message */
+ smtp_submit_set = *smtp_set;
+ smtp_submit_set.submission_host = host,
+
+ i_zero(&smtp_input);
+ smtp_submit = smtp_submit_init_simple(
+ &smtp_input, &smtp_submit_set, &((struct smtp_address){
+ .localpart = "sender",
+ .domain = "example.com"}));
+
+ smtp_submit_add_rcpt(
+ smtp_submit, &((struct smtp_address){
+ .localpart = "rcpt",
+ .domain = "example.com"}));
+ output = smtp_submit_send(smtp_submit);
+ o_stream_nsend_str(output, message);
+
+ ret = smtp_submit_run(smtp_submit, error_r);
+
+ smtp_submit_deinit(&smtp_submit);
+
+ return ret;
+}
+
+static int
+test_client_smtp_send_simple_port(const struct smtp_submit_settings *smtp_set,
+ const char *message, unsigned int port,
+ const char **error_r)
+{
+ const char *host = t_strdup_printf("127.0.0.1:%u", port);
+
+ return test_client_smtp_send_simple(smtp_set, message, host, error_r);
+}
+
+/*
+ * Test server
+ */
+
+/* client connection */
+
+static void server_connection_input(struct connection *_conn)
+{
+ struct server_connection *conn = (struct server_connection *)_conn;
+
+ test_server_input(conn);
+}
+
+static void server_connection_init(int fd)
+{
+ struct server_connection *conn;
+ pool_t pool;
+
+ net_set_nonblock(fd, TRUE);
+
+ pool = pool_alloconly_create("server connection", 256);
+ conn = p_new(pool, struct server_connection, 1);
+ conn->pool = pool;
+
+ connection_init_server(server_conn_list, &conn->conn,
+ "server connection", fd, fd);
+
+ if (test_server_init != NULL)
+ test_server_init(conn);
+}
+
+static void server_connection_deinit(struct server_connection **_conn)
+{
+ struct server_connection *conn = *_conn;
+
+ *_conn = NULL;
+
+ if (test_server_deinit != NULL)
+ test_server_deinit(conn);
+
+ connection_deinit(&conn->conn);
+ pool_unref(&conn->pool);
+}
+
+static void server_connection_destroy(struct connection *_conn)
+{
+ struct server_connection *conn =
+ (struct server_connection *)_conn;
+
+ server_connection_deinit(&conn);
+}
+
+static void server_connection_accept(void *context ATTR_UNUSED)
+{
+ int fd;
+
+ /* accept new client */
+ fd = net_accept(fd_listen, NULL, NULL);
+ if (fd == -1)
+ return;
+ if (fd == -2) {
+ i_fatal("test server: accept() failed: %m");
+ }
+
+ server_connection_init(fd);
+}
+
+/* */
+
+static struct connection_settings server_connection_set = {
+ .input_max_size = SIZE_MAX,
+ .output_max_size = SIZE_MAX,
+ .client = FALSE
+};
+
+static const struct connection_vfuncs server_connection_vfuncs = {
+ .destroy = server_connection_destroy,
+ .input = server_connection_input
+};
+
+static void test_server_run(unsigned int index)
+{
+ server_index = index;
+
+ /* open server socket */
+ io_listen = io_add(fd_listen,
+ IO_READ, server_connection_accept, NULL);
+
+ server_conn_list = connection_list_init(&server_connection_set,
+ &server_connection_vfuncs);
+
+ io_loop_run(ioloop);
+
+ /* close server socket */
+ io_remove(&io_listen);
+
+ connection_list_deinit(&server_conn_list);
+}
+
+/*
+ * Tests
+ */
+
+struct test_server_data {
+ unsigned int index;
+ test_server_init_t server_test;
+};
+
+static int test_open_server_fd(in_port_t *bind_port)
+{
+ int fd = net_listen(&bind_ip, bind_port, 128);
+ if (debug)
+ i_debug("server listening on %u", *bind_port);
+ if (fd == -1) {
+ i_fatal("listen(%s:%u) failed: %m",
+ net_ip2addr(&bind_ip), *bind_port);
+ }
+ return fd;
+}
+
+static void test_tmp_dir_init(void)
+{
+ tmp_dir = i_strdup_printf("/tmp/dovecot-test-smtp-client.%s.%s",
+ dec2str(time(NULL)), dec2str(getpid()));
+}
+
+static const char *test_tmp_dir_get(void)
+{
+ if (mkdir(tmp_dir, 0700) < 0 && errno != EEXIST) {
+ i_fatal("failed to create temporary directory `%s': %m",
+ tmp_dir);
+ }
+ return tmp_dir;
+}
+
+static void test_tmp_dir_deinit(void)
+{
+ const char *error;
+
+ if (unlink_directory(tmp_dir, UNLINK_DIRECTORY_FLAG_RMDIR,
+ &error) < 0) {
+ i_warning("failed to remove temporary directory `%s': %s.",
+ tmp_dir, error);
+ }
+
+ i_free(tmp_dir);
+}
+
+static void test_message_delivery(const char *message, const char *file)
+{
+ struct istream *input;
+ const unsigned char *data;
+ size_t size, msize;
+ int ret;
+
+ msize = strlen(message);
+
+ input = i_stream_create_file(file, SIZE_MAX);
+ while ((ret = i_stream_read_more(input, &data, &size)) > 0) {
+ const unsigned char *mdata;
+
+ test_assert(input->v_offset < (uoff_t)msize &&
+ (input->v_offset + (uoff_t)size) <= (uoff_t)msize);
+ if (test_has_failed())
+ break;
+ mdata = (const unsigned char *)message + input->v_offset;
+ test_assert(memcmp(data, mdata, size) == 0);
+ if (test_has_failed())
+ break;
+ i_stream_skip(input, size);
+ }
+
+ test_out_reason("delivery", ret < 0 &&
+ input->stream_errno == 0 &&
+ input->eof &&
+ input->v_offset == (uoff_t)msize,
+ (input->stream_errno == 0 ?
+ NULL : i_stream_get_error(input)));
+ i_stream_unref(&input);
+}
+
+static int test_run_server(struct test_server_data *data)
+{
+ server_port = bind_ports[data->index];
+
+ main_deinit();
+ master_service_deinit_forked(&master_service);
+
+ i_set_failure_prefix("SERVER[%u]: ", data->index + 1);
+
+ if (debug)
+ i_debug("PID=%s", my_pid);
+
+ ioloop = io_loop_create();
+ data->server_test(data->index);
+ io_loop_destroy(&ioloop);
+
+ if (debug)
+ i_debug("Terminated");
+
+ i_close_fd(&fd_listen);
+ i_free(bind_ports);
+ test_tmp_dir_deinit();
+ return 0;
+}
+
+static void
+test_run_client(const struct smtp_submit_settings *submit_set,
+ test_client_init_t client_test)
+{
+ i_set_failure_prefix("CLIENT: ");
+
+ if (debug)
+ i_debug("PID=%s", my_pid);
+
+ server_port = 0;
+ i_sleep_msecs(100); /* wait a little for server setup */
+
+ ioloop = io_loop_create();
+ if (client_test(submit_set))
+ io_loop_run(ioloop);
+ test_client_deinit();
+ io_loop_destroy(&ioloop);
+
+ if (debug)
+ i_debug("Terminated");
+}
+
+static void
+test_run_client_server(const struct smtp_submit_settings *submit_set,
+ test_client_init_t client_test,
+ test_server_init_t server_test,
+ unsigned int server_tests_count)
+{
+ unsigned int i;
+
+ test_tmp_dir_init();
+
+ if (server_tests_count > 0) {
+ int fds[server_tests_count];
+
+ bind_ports = i_new(in_port_t, server_tests_count);
+ for (i = 0; i < server_tests_count; i++)
+ fds[i] = test_open_server_fd(&bind_ports[i]);
+
+ for (i = 0; i < server_tests_count; i++) {
+ struct test_server_data data;
+
+ i_zero(&data);
+ data.index = i;
+ data.server_test = server_test;
+
+ /* Fork server */
+ fd_listen = fds[i];
+ test_subprocess_fork(test_run_server, &data, FALSE);
+ i_close_fd(&fd_listen);
+ }
+ }
+
+ /* Run client */
+ test_run_client(submit_set, client_test);
+
+ i_unset_failure_prefix();
+ test_subprocess_kill_all(SERVER_KILL_TIMEOUT_SECS);
+ i_free(bind_ports);
+ test_tmp_dir_deinit();
+}
+
+/*
+ * Main
+ */
+
+static void main_init(void)
+{
+ /* nothing yet */
+}
+
+static void main_deinit(void)
+{
+ /* nothing yet; also called from sub-processes */
+}
+
+int main(int argc, char *argv[])
+{
+ const enum master_service_flags service_flags =
+ MASTER_SERVICE_FLAG_STANDALONE |
+ MASTER_SERVICE_FLAG_DONT_SEND_STATS;
+ int c;
+ int ret;
+
+ master_service = master_service_init("test-smtp-submit", service_flags,
+ &argc, &argv, "D");
+ main_init();
+
+ while ((c = master_getopt(master_service)) > 0) {
+ switch (c) {
+ case 'D':
+ debug = TRUE;
+ break;
+ default:
+ i_fatal("Usage: %s [-D]", argv[0]);
+ }
+ }
+
+ master_service_init_finish(master_service);
+ test_subprocesses_init(debug);
+
+ /* listen on localhost */
+ i_zero(&bind_ip);
+ bind_ip.family = AF_INET;
+ bind_ip.u.ip4.s_addr = htonl(INADDR_LOOPBACK);
+
+ ret = test_run(test_functions);
+
+ test_subprocesses_deinit();
+ main_deinit();
+ master_service_deinit(&master_service);
+
+ return ret;
+}
diff --git a/src/lib-smtp/test-smtp-syntax.c b/src/lib-smtp/test-smtp-syntax.c
new file mode 100644
index 0000000..735cd01
--- /dev/null
+++ b/src/lib-smtp/test-smtp-syntax.c
@@ -0,0 +1,150 @@
+/* Copyright (c) 2020 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "str.h"
+#include "str-sanitize.h"
+#include "test-common.h"
+#include "smtp-syntax.h"
+
+/*
+ * Valid string parse tests
+ */
+
+struct valid_string_parse_test {
+ const char *input, *parsed, *output;
+};
+
+static const struct valid_string_parse_test
+valid_string_parse_tests[] = {
+ {
+ .input = "",
+ .parsed = "",
+ },
+ {
+ .input = "atom",
+ .parsed = "atom",
+ },
+ {
+ .input = "abcdefghijklmnopqrstuvwxyz"
+ "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
+ "0123456789!#$%&'*+-/=?^_`{|}~",
+ .parsed = "abcdefghijklmnopqrstuvwxyz"
+ "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
+ "0123456789!#$%&'*+-/=?^_`{|}~",
+ },
+ {
+ .input = "\"quoted-string\"",
+ .parsed = "quoted-string",
+ .output = "quoted-string",
+ },
+ {
+ .input = "\"quoted \\\"string\\\"\"",
+ .parsed = "quoted \"string\"",
+ },
+ {
+ .input = "\"quoted \\\\string\\\\\"",
+ .parsed = "quoted \\string\\",
+ },
+};
+
+static const unsigned int valid_string_parse_test_count =
+ N_ELEMENTS(valid_string_parse_tests);
+
+static void test_smtp_string_parse_valid(void)
+{
+ unsigned int i;
+
+ for (i = 0; i < valid_string_parse_test_count; i++) T_BEGIN {
+ const struct valid_string_parse_test *test =
+ &valid_string_parse_tests[i];
+ const char *parsed, *error = NULL;
+ int ret;
+
+ ret = smtp_string_parse(test->input, &parsed, &error);
+
+ test_begin(t_strdup_printf("smtp string valid [%d]", i));
+ test_out_reason(t_strdup_printf("parse(\"%s\")", test->input),
+ ret >= 0, error);
+ test_assert(ret != 0 || *test->input == '\0');
+
+ if (!test_has_failed()) {
+ string_t *encoded;
+ const char *output;
+
+ test_out(t_strdup_printf("parsed = \"%s\"", parsed),
+ null_strcmp(parsed, test->parsed) == 0);
+
+ encoded = t_str_new(255);
+ smtp_string_write(encoded, parsed);
+ output = (test->output == NULL ?
+ test->input : test->output);
+ test_out(t_strdup_printf("write() = \"%s\"",
+ str_c(encoded)),
+ strcmp(str_c(encoded), output) == 0);
+ }
+ test_end();
+ } T_END;
+}
+
+/*
+ * Invalid string parse tests
+ */
+
+struct invalid_string_parse_test {
+ const char *input;
+};
+
+static const struct invalid_string_parse_test
+invalid_string_parse_tests[] = {
+ {
+ .input = " ",
+ },
+ {
+ .input = "\\",
+ },
+ {
+ .input = "\"",
+ },
+ {
+ .input = "\"aa",
+ },
+ {
+ .input = "aa\"",
+ },
+};
+
+static const unsigned int invalid_string_parse_test_count =
+ N_ELEMENTS(invalid_string_parse_tests);
+
+static void test_smtp_string_parse_invalid(void)
+{
+ unsigned int i;
+
+ for (i = 0; i < invalid_string_parse_test_count; i++) T_BEGIN {
+ const struct invalid_string_parse_test *test =
+ &invalid_string_parse_tests[i];
+ const char *parsed, *error;
+ int ret;
+
+ ret = smtp_string_parse(test->input, &parsed, &error);
+
+ test_begin(t_strdup_printf("smtp string invalid [%d]", i));
+ test_out_reason(t_strdup_printf("parse(\"%s\")", test->input),
+ ret < 0, error);
+ test_end();
+ } T_END;
+}
+
+/*
+ * Tests
+ */
+
+int main(void)
+{
+ static void (*test_functions[])(void) = {
+ test_smtp_string_parse_valid,
+ test_smtp_string_parse_invalid,
+ NULL
+ };
+ return test_run(test_functions);
+}
diff --git a/src/lib-sql/Makefile.am b/src/lib-sql/Makefile.am
new file mode 100644
index 0000000..89c3d2d
--- /dev/null
+++ b/src/lib-sql/Makefile.am
@@ -0,0 +1,144 @@
+noinst_LTLIBRARIES = libsql.la libdriver_test.la
+
+SQL_DRIVER_PLUGINS =
+
+# automake seems to force making this unconditional..
+NOPLUGIN_LDFLAGS =
+
+if SQL_PLUGINS
+if BUILD_MYSQL
+MYSQL_LIB = libdriver_mysql.la
+SQL_DRIVER_PLUGINS += mysql
+endif
+if BUILD_PGSQL
+PGSQL_LIB = libdriver_pgsql.la
+SQL_DRIVER_PLUGINS += pgsql
+endif
+if BUILD_SQLITE
+SQLITE_LIB = libdriver_sqlite.la
+SQL_DRIVER_PLUGINS += sqlite
+endif
+if BUILD_CASSANDRA
+CASSANDRA_LIB = libdriver_cassandra.la
+SQL_DRIVER_PLUGINS += cassandra
+endif
+
+sql_module_LTLIBRARIES = \
+ $(MYSQL_LIB) \
+ $(PGSQL_LIB) \
+ $(SQLITE_LIB) \
+ $(CASSANDRA_LIB)
+
+sql_moduledir = $(moduledir)
+endif
+
+sql_drivers = @sql_drivers@
+
+AM_CPPFLAGS = \
+ -I$(top_srcdir)/src/lib \
+ -I$(top_srcdir)/src/lib-settings \
+ $(SQL_CFLAGS)
+
+dist_sources = \
+ sql-api.c \
+ sql-db-cache.c
+
+if ! SQL_PLUGINS
+driver_sources = \
+ driver-mysql.c \
+ driver-pgsql.c \
+ driver-sqlite.c \
+ driver-cassandra.c
+endif
+
+libsql_la_SOURCES = \
+ $(dist_sources) \
+ $(driver_sources) \
+ driver-sqlpool.c
+libsql_la_LIBADD = $(SQL_LIBS)
+
+nodist_libsql_la_SOURCES = sql-drivers-register.c
+
+deplibs = \
+ ../lib-dovecot/libdovecot.la
+
+if SQL_PLUGINS
+libdriver_mysql_la_LDFLAGS = -module -avoid-version
+libdriver_mysql_la_LIBADD = $(MYSQL_LIBS)
+libdriver_mysql_la_CPPFLAGS = $(AM_CPPFLAGS) $(MYSQL_CFLAGS)
+libdriver_mysql_la_SOURCES = driver-mysql.c
+
+libdriver_pgsql_la_LDFLAGS = -module -avoid-version
+libdriver_pgsql_la_LIBADD = $(PGSQL_LIBS)
+libdriver_pgsql_la_CPPFLAGS = $(AM_CPPFLAGS) $(PGSQL_CFLAGS)
+libdriver_pgsql_la_SOURCES = driver-pgsql.c
+
+libdriver_sqlite_la_LDFLAGS = -module -avoid-version
+libdriver_sqlite_la_LIBADD = $(SQLITE_LIBS)
+libdriver_sqlite_la_CPPFLAGS = $(AM_CPPFLAGS) $(SQLITE_CFLAGS)
+libdriver_sqlite_la_SOURCES = driver-sqlite.c
+
+libdriver_cassandra_la_LDFLAGS = -module -avoid-version
+libdriver_cassandra_la_LIBADD = $(CASSANDRA_LIBS)
+libdriver_cassandra_la_CPPFLAGS = $(AM_CPPFLAGS) $(CASSANDRA_CFLAGS)
+libdriver_cassandra_la_SOURCES = driver-cassandra.c
+else
+endif
+
+libdriver_test_la_LDFLAGS = -avoid-version
+libdriver_test_la_CPPFLAGS = $(AM_CPPFLAGS) \
+ -I$(top_srcdir)/src/lib-test
+libdriver_test_la_SOURCES = driver-test.c
+
+noinst_HEADERS = driver-test.h
+
+pkglib_LTLIBRARIES = libdovecot-sql.la
+libdovecot_sql_la_SOURCES =
+libdovecot_sql_la_LIBADD = libsql.la $(deplibs)
+libdovecot_sql_la_DEPENDENCIES = libsql.la
+libdovecot_sql_la_LDFLAGS = -export-dynamic
+
+headers = \
+ sql-api.h \
+ sql-api-private.h \
+ sql-db-cache.h
+
+pkginc_libdir=$(pkgincludedir)
+pkginc_lib_HEADERS = $(headers)
+
+sql-drivers-register.c: Makefile
+ rm -f $@
+ echo '/* this file automatically generated by Makefile */' >$@
+ echo '#include "lib.h"' >>$@
+ echo '#include "sql-api.h"' >>$@
+if ! SQL_PLUGINS
+ for i in $(sql_drivers) null; do \
+ if [ "$${i}" != "null" ]; then \
+ echo "extern struct sql_db driver_$${i}_db;" >>$@ ; \
+ fi; \
+ done
+endif
+ echo 'void sql_drivers_register_all(void) {' >>$@
+if ! SQL_PLUGINS
+ for i in $(sql_drivers) null; do \
+ if [ "$${i}" != "null" ]; then \
+ echo "sql_driver_register(&driver_$${i}_db);" >>$@ ; \
+ fi; \
+ done
+endif
+ echo '}' >>$@
+
+if SQL_PLUGINS
+install-exec-local:
+ for d in auth dict; do \
+ $(mkdir_p) $(DESTDIR)$(moduledir)/$$d; \
+ for driver in $(SQL_DRIVER_PLUGINS); do \
+ rm -f $(DESTDIR)$(moduledir)/$$d/libdriver_$$driver.so; \
+ $(LN_S) ../libdriver_$$driver.so $(DESTDIR)$(moduledir)/$$d; \
+ done; \
+ done
+endif
+
+
+distclean-generic:
+ rm -f Makefile sql-drivers-register.c
diff --git a/src/lib-sql/Makefile.in b/src/lib-sql/Makefile.in
new file mode 100644
index 0000000..e807466
--- /dev/null
+++ b/src/lib-sql/Makefile.in
@@ -0,0 +1,1159 @@
+# Makefile.in generated by automake 1.16.1 from Makefile.am.
+# @configure_input@
+
+# Copyright (C) 1994-2018 Free Software Foundation, Inc.
+
+# This Makefile.in is free software; the Free Software Foundation
+# gives unlimited permission to copy and/or distribute it,
+# with or without modifications, as long as this notice is preserved.
+
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY, to the extent permitted by law; without
+# even the implied warranty of MERCHANTABILITY or FITNESS FOR A
+# PARTICULAR PURPOSE.
+
+@SET_MAKE@
+
+
+VPATH = @srcdir@
+am__is_gnu_make = { \
+ if test -z '$(MAKELEVEL)'; then \
+ false; \
+ elif test -n '$(MAKE_HOST)'; then \
+ true; \
+ elif test -n '$(MAKE_VERSION)' && test -n '$(CURDIR)'; then \
+ true; \
+ else \
+ false; \
+ fi; \
+}
+am__make_running_with_option = \
+ case $${target_option-} in \
+ ?) ;; \
+ *) echo "am__make_running_with_option: internal error: invalid" \
+ "target option '$${target_option-}' specified" >&2; \
+ exit 1;; \
+ esac; \
+ has_opt=no; \
+ sane_makeflags=$$MAKEFLAGS; \
+ if $(am__is_gnu_make); then \
+ sane_makeflags=$$MFLAGS; \
+ else \
+ case $$MAKEFLAGS in \
+ *\\[\ \ ]*) \
+ bs=\\; \
+ sane_makeflags=`printf '%s\n' "$$MAKEFLAGS" \
+ | sed "s/$$bs$$bs[$$bs $$bs ]*//g"`;; \
+ esac; \
+ fi; \
+ skip_next=no; \
+ strip_trailopt () \
+ { \
+ flg=`printf '%s\n' "$$flg" | sed "s/$$1.*$$//"`; \
+ }; \
+ for flg in $$sane_makeflags; do \
+ test $$skip_next = yes && { skip_next=no; continue; }; \
+ case $$flg in \
+ *=*|--*) continue;; \
+ -*I) strip_trailopt 'I'; skip_next=yes;; \
+ -*I?*) strip_trailopt 'I';; \
+ -*O) strip_trailopt 'O'; skip_next=yes;; \
+ -*O?*) strip_trailopt 'O';; \
+ -*l) strip_trailopt 'l'; skip_next=yes;; \
+ -*l?*) strip_trailopt 'l';; \
+ -[dEDm]) skip_next=yes;; \
+ -[JT]) skip_next=yes;; \
+ esac; \
+ case $$flg in \
+ *$$target_option*) has_opt=yes; break;; \
+ esac; \
+ done; \
+ test $$has_opt = yes
+am__make_dryrun = (target_option=n; $(am__make_running_with_option))
+am__make_keepgoing = (target_option=k; $(am__make_running_with_option))
+pkgdatadir = $(datadir)/@PACKAGE@
+pkgincludedir = $(includedir)/@PACKAGE@
+pkglibdir = $(libdir)/@PACKAGE@
+pkglibexecdir = $(libexecdir)/@PACKAGE@
+am__cd = CDPATH="$${ZSH_VERSION+.}$(PATH_SEPARATOR)" && cd
+install_sh_DATA = $(install_sh) -c -m 644
+install_sh_PROGRAM = $(install_sh) -c
+install_sh_SCRIPT = $(install_sh) -c
+INSTALL_HEADER = $(INSTALL_DATA)
+transform = $(program_transform_name)
+NORMAL_INSTALL = :
+PRE_INSTALL = :
+POST_INSTALL = :
+NORMAL_UNINSTALL = :
+PRE_UNINSTALL = :
+POST_UNINSTALL = :
+build_triplet = @build@
+host_triplet = @host@
+@BUILD_MYSQL_TRUE@@SQL_PLUGINS_TRUE@am__append_1 = mysql
+@BUILD_PGSQL_TRUE@@SQL_PLUGINS_TRUE@am__append_2 = pgsql
+@BUILD_SQLITE_TRUE@@SQL_PLUGINS_TRUE@am__append_3 = sqlite
+@BUILD_CASSANDRA_TRUE@@SQL_PLUGINS_TRUE@am__append_4 = cassandra
+subdir = src/lib-sql
+ACLOCAL_M4 = $(top_srcdir)/aclocal.m4
+am__aclocal_m4_deps = $(top_srcdir)/m4/ac_checktype2.m4 \
+ $(top_srcdir)/m4/ac_typeof.m4 $(top_srcdir)/m4/arc4random.m4 \
+ $(top_srcdir)/m4/blockdev.m4 $(top_srcdir)/m4/c99_vsnprintf.m4 \
+ $(top_srcdir)/m4/clock_gettime.m4 $(top_srcdir)/m4/crypt.m4 \
+ $(top_srcdir)/m4/crypt_xpg6.m4 $(top_srcdir)/m4/dbqlk.m4 \
+ $(top_srcdir)/m4/dirent_dtype.m4 $(top_srcdir)/m4/dovecot.m4 \
+ $(top_srcdir)/m4/fd_passing.m4 $(top_srcdir)/m4/fdatasync.m4 \
+ $(top_srcdir)/m4/flexible_array_member.m4 \
+ $(top_srcdir)/m4/glibc.m4 $(top_srcdir)/m4/gmtime_max.m4 \
+ $(top_srcdir)/m4/gmtime_tm_gmtoff.m4 \
+ $(top_srcdir)/m4/ioloop.m4 $(top_srcdir)/m4/iovec.m4 \
+ $(top_srcdir)/m4/ipv6.m4 $(top_srcdir)/m4/libcap.m4 \
+ $(top_srcdir)/m4/libtool.m4 $(top_srcdir)/m4/libwrap.m4 \
+ $(top_srcdir)/m4/linux_mremap.m4 $(top_srcdir)/m4/ltoptions.m4 \
+ $(top_srcdir)/m4/ltsugar.m4 $(top_srcdir)/m4/ltversion.m4 \
+ $(top_srcdir)/m4/lt~obsolete.m4 $(top_srcdir)/m4/mmap_write.m4 \
+ $(top_srcdir)/m4/mntctl.m4 $(top_srcdir)/m4/modules.m4 \
+ $(top_srcdir)/m4/notify.m4 $(top_srcdir)/m4/nsl.m4 \
+ $(top_srcdir)/m4/off_t_max.m4 $(top_srcdir)/m4/pkg.m4 \
+ $(top_srcdir)/m4/pr_set_dumpable.m4 \
+ $(top_srcdir)/m4/q_quotactl.m4 $(top_srcdir)/m4/quota.m4 \
+ $(top_srcdir)/m4/random.m4 $(top_srcdir)/m4/rlimit.m4 \
+ $(top_srcdir)/m4/sendfile.m4 $(top_srcdir)/m4/size_t_signed.m4 \
+ $(top_srcdir)/m4/sockpeercred.m4 $(top_srcdir)/m4/sql.m4 \
+ $(top_srcdir)/m4/ssl.m4 $(top_srcdir)/m4/st_tim.m4 \
+ $(top_srcdir)/m4/static_array.m4 $(top_srcdir)/m4/test_with.m4 \
+ $(top_srcdir)/m4/time_t.m4 $(top_srcdir)/m4/typeof.m4 \
+ $(top_srcdir)/m4/typeof_dev_t.m4 \
+ $(top_srcdir)/m4/uoff_t_max.m4 $(top_srcdir)/m4/vararg.m4 \
+ $(top_srcdir)/m4/want_apparmor.m4 \
+ $(top_srcdir)/m4/want_bsdauth.m4 \
+ $(top_srcdir)/m4/want_bzlib.m4 \
+ $(top_srcdir)/m4/want_cassandra.m4 \
+ $(top_srcdir)/m4/want_cdb.m4 \
+ $(top_srcdir)/m4/want_checkpassword.m4 \
+ $(top_srcdir)/m4/want_clucene.m4 $(top_srcdir)/m4/want_db.m4 \
+ $(top_srcdir)/m4/want_gssapi.m4 $(top_srcdir)/m4/want_icu.m4 \
+ $(top_srcdir)/m4/want_ldap.m4 $(top_srcdir)/m4/want_lua.m4 \
+ $(top_srcdir)/m4/want_lz4.m4 $(top_srcdir)/m4/want_lzma.m4 \
+ $(top_srcdir)/m4/want_mysql.m4 $(top_srcdir)/m4/want_pam.m4 \
+ $(top_srcdir)/m4/want_passwd.m4 $(top_srcdir)/m4/want_pgsql.m4 \
+ $(top_srcdir)/m4/want_prefetch.m4 \
+ $(top_srcdir)/m4/want_shadow.m4 \
+ $(top_srcdir)/m4/want_sodium.m4 $(top_srcdir)/m4/want_solr.m4 \
+ $(top_srcdir)/m4/want_sqlite.m4 \
+ $(top_srcdir)/m4/want_stemmer.m4 \
+ $(top_srcdir)/m4/want_systemd.m4 \
+ $(top_srcdir)/m4/want_textcat.m4 \
+ $(top_srcdir)/m4/want_unwind.m4 $(top_srcdir)/m4/want_zlib.m4 \
+ $(top_srcdir)/m4/want_zstd.m4 $(top_srcdir)/configure.ac
+am__configure_deps = $(am__aclocal_m4_deps) $(CONFIGURE_DEPENDENCIES) \
+ $(ACLOCAL_M4)
+DIST_COMMON = $(srcdir)/Makefile.am $(noinst_HEADERS) \
+ $(pkginc_lib_HEADERS) $(am__DIST_COMMON)
+mkinstalldirs = $(install_sh) -d
+CONFIG_HEADER = $(top_builddir)/config.h
+CONFIG_CLEAN_FILES =
+CONFIG_CLEAN_VPATH_FILES =
+am__vpath_adj_setup = srcdirstrip=`echo "$(srcdir)" | sed 's|.|.|g'`;
+am__vpath_adj = case $$p in \
+ $(srcdir)/*) f=`echo "$$p" | sed "s|^$$srcdirstrip/||"`;; \
+ *) f=$$p;; \
+ esac;
+am__strip_dir = f=`echo $$p | sed -e 's|^.*/||'`;
+am__install_max = 40
+am__nobase_strip_setup = \
+ srcdirstrip=`echo "$(srcdir)" | sed 's/[].[^$$\\*|]/\\\\&/g'`
+am__nobase_strip = \
+ for p in $$list; do echo "$$p"; done | sed -e "s|$$srcdirstrip/||"
+am__nobase_list = $(am__nobase_strip_setup); \
+ for p in $$list; do echo "$$p $$p"; done | \
+ sed "s| $$srcdirstrip/| |;"' / .*\//!s/ .*/ ./; s,\( .*\)/[^/]*$$,\1,' | \
+ $(AWK) 'BEGIN { files["."] = "" } { files[$$2] = files[$$2] " " $$1; \
+ if (++n[$$2] == $(am__install_max)) \
+ { print $$2, files[$$2]; n[$$2] = 0; files[$$2] = "" } } \
+ END { for (dir in files) print dir, files[dir] }'
+am__base_list = \
+ sed '$$!N;$$!N;$$!N;$$!N;$$!N;$$!N;$$!N;s/\n/ /g' | \
+ sed '$$!N;$$!N;$$!N;$$!N;s/\n/ /g'
+am__uninstall_files_from_dir = { \
+ test -z "$$files" \
+ || { test ! -d "$$dir" && test ! -f "$$dir" && test ! -r "$$dir"; } \
+ || { echo " ( cd '$$dir' && rm -f" $$files ")"; \
+ $(am__cd) "$$dir" && rm -f $$files; }; \
+ }
+am__installdirs = "$(DESTDIR)$(pkglibdir)" \
+ "$(DESTDIR)$(sql_moduledir)" "$(DESTDIR)$(pkginc_libdir)"
+LTLIBRARIES = $(noinst_LTLIBRARIES) $(pkglib_LTLIBRARIES) \
+ $(sql_module_LTLIBRARIES)
+am_libdovecot_sql_la_OBJECTS =
+libdovecot_sql_la_OBJECTS = $(am_libdovecot_sql_la_OBJECTS)
+AM_V_lt = $(am__v_lt_@AM_V@)
+am__v_lt_ = $(am__v_lt_@AM_DEFAULT_V@)
+am__v_lt_0 = --silent
+am__v_lt_1 =
+libdovecot_sql_la_LINK = $(LIBTOOL) $(AM_V_lt) --tag=CC \
+ $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=link $(CCLD) \
+ $(AM_CFLAGS) $(CFLAGS) $(libdovecot_sql_la_LDFLAGS) $(LDFLAGS) \
+ -o $@
+am__DEPENDENCIES_1 =
+@SQL_PLUGINS_TRUE@libdriver_cassandra_la_DEPENDENCIES = \
+@SQL_PLUGINS_TRUE@ $(am__DEPENDENCIES_1)
+am__libdriver_cassandra_la_SOURCES_DIST = driver-cassandra.c
+@SQL_PLUGINS_TRUE@am_libdriver_cassandra_la_OBJECTS = \
+@SQL_PLUGINS_TRUE@ libdriver_cassandra_la-driver-cassandra.lo
+libdriver_cassandra_la_OBJECTS = $(am_libdriver_cassandra_la_OBJECTS)
+libdriver_cassandra_la_LINK = $(LIBTOOL) $(AM_V_lt) --tag=CC \
+ $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=link $(CCLD) \
+ $(AM_CFLAGS) $(CFLAGS) $(libdriver_cassandra_la_LDFLAGS) \
+ $(LDFLAGS) -o $@
+@BUILD_CASSANDRA_TRUE@@SQL_PLUGINS_TRUE@am_libdriver_cassandra_la_rpath = \
+@BUILD_CASSANDRA_TRUE@@SQL_PLUGINS_TRUE@ -rpath \
+@BUILD_CASSANDRA_TRUE@@SQL_PLUGINS_TRUE@ $(sql_moduledir)
+@SQL_PLUGINS_TRUE@libdriver_mysql_la_DEPENDENCIES = \
+@SQL_PLUGINS_TRUE@ $(am__DEPENDENCIES_1)
+am__libdriver_mysql_la_SOURCES_DIST = driver-mysql.c
+@SQL_PLUGINS_TRUE@am_libdriver_mysql_la_OBJECTS = \
+@SQL_PLUGINS_TRUE@ libdriver_mysql_la-driver-mysql.lo
+libdriver_mysql_la_OBJECTS = $(am_libdriver_mysql_la_OBJECTS)
+libdriver_mysql_la_LINK = $(LIBTOOL) $(AM_V_lt) --tag=CC \
+ $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=link $(CCLD) \
+ $(AM_CFLAGS) $(CFLAGS) $(libdriver_mysql_la_LDFLAGS) \
+ $(LDFLAGS) -o $@
+@BUILD_MYSQL_TRUE@@SQL_PLUGINS_TRUE@am_libdriver_mysql_la_rpath = \
+@BUILD_MYSQL_TRUE@@SQL_PLUGINS_TRUE@ -rpath $(sql_moduledir)
+@SQL_PLUGINS_TRUE@libdriver_pgsql_la_DEPENDENCIES = \
+@SQL_PLUGINS_TRUE@ $(am__DEPENDENCIES_1)
+am__libdriver_pgsql_la_SOURCES_DIST = driver-pgsql.c
+@SQL_PLUGINS_TRUE@am_libdriver_pgsql_la_OBJECTS = \
+@SQL_PLUGINS_TRUE@ libdriver_pgsql_la-driver-pgsql.lo
+libdriver_pgsql_la_OBJECTS = $(am_libdriver_pgsql_la_OBJECTS)
+libdriver_pgsql_la_LINK = $(LIBTOOL) $(AM_V_lt) --tag=CC \
+ $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=link $(CCLD) \
+ $(AM_CFLAGS) $(CFLAGS) $(libdriver_pgsql_la_LDFLAGS) \
+ $(LDFLAGS) -o $@
+@BUILD_PGSQL_TRUE@@SQL_PLUGINS_TRUE@am_libdriver_pgsql_la_rpath = \
+@BUILD_PGSQL_TRUE@@SQL_PLUGINS_TRUE@ -rpath $(sql_moduledir)
+@SQL_PLUGINS_TRUE@libdriver_sqlite_la_DEPENDENCIES = \
+@SQL_PLUGINS_TRUE@ $(am__DEPENDENCIES_1)
+am__libdriver_sqlite_la_SOURCES_DIST = driver-sqlite.c
+@SQL_PLUGINS_TRUE@am_libdriver_sqlite_la_OBJECTS = \
+@SQL_PLUGINS_TRUE@ libdriver_sqlite_la-driver-sqlite.lo
+libdriver_sqlite_la_OBJECTS = $(am_libdriver_sqlite_la_OBJECTS)
+libdriver_sqlite_la_LINK = $(LIBTOOL) $(AM_V_lt) --tag=CC \
+ $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=link $(CCLD) \
+ $(AM_CFLAGS) $(CFLAGS) $(libdriver_sqlite_la_LDFLAGS) \
+ $(LDFLAGS) -o $@
+@BUILD_SQLITE_TRUE@@SQL_PLUGINS_TRUE@am_libdriver_sqlite_la_rpath = \
+@BUILD_SQLITE_TRUE@@SQL_PLUGINS_TRUE@ -rpath $(sql_moduledir)
+libdriver_test_la_LIBADD =
+am_libdriver_test_la_OBJECTS = libdriver_test_la-driver-test.lo
+libdriver_test_la_OBJECTS = $(am_libdriver_test_la_OBJECTS)
+libdriver_test_la_LINK = $(LIBTOOL) $(AM_V_lt) --tag=CC \
+ $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=link $(CCLD) \
+ $(AM_CFLAGS) $(CFLAGS) $(libdriver_test_la_LDFLAGS) $(LDFLAGS) \
+ -o $@
+libsql_la_DEPENDENCIES = $(am__DEPENDENCIES_1)
+am__libsql_la_SOURCES_DIST = sql-api.c sql-db-cache.c driver-mysql.c \
+ driver-pgsql.c driver-sqlite.c driver-cassandra.c \
+ driver-sqlpool.c
+am__objects_1 = sql-api.lo sql-db-cache.lo
+@SQL_PLUGINS_FALSE@am__objects_2 = driver-mysql.lo driver-pgsql.lo \
+@SQL_PLUGINS_FALSE@ driver-sqlite.lo driver-cassandra.lo
+am_libsql_la_OBJECTS = $(am__objects_1) $(am__objects_2) \
+ driver-sqlpool.lo
+nodist_libsql_la_OBJECTS = sql-drivers-register.lo
+libsql_la_OBJECTS = $(am_libsql_la_OBJECTS) \
+ $(nodist_libsql_la_OBJECTS)
+AM_V_P = $(am__v_P_@AM_V@)
+am__v_P_ = $(am__v_P_@AM_DEFAULT_V@)
+am__v_P_0 = false
+am__v_P_1 = :
+AM_V_GEN = $(am__v_GEN_@AM_V@)
+am__v_GEN_ = $(am__v_GEN_@AM_DEFAULT_V@)
+am__v_GEN_0 = @echo " GEN " $@;
+am__v_GEN_1 =
+AM_V_at = $(am__v_at_@AM_V@)
+am__v_at_ = $(am__v_at_@AM_DEFAULT_V@)
+am__v_at_0 = @
+am__v_at_1 =
+DEFAULT_INCLUDES = -I.@am__isrc@ -I$(top_builddir)
+depcomp = $(SHELL) $(top_srcdir)/depcomp
+am__maybe_remake_depfiles = depfiles
+am__depfiles_remade = ./$(DEPDIR)/driver-cassandra.Plo \
+ ./$(DEPDIR)/driver-mysql.Plo ./$(DEPDIR)/driver-pgsql.Plo \
+ ./$(DEPDIR)/driver-sqlite.Plo ./$(DEPDIR)/driver-sqlpool.Plo \
+ ./$(DEPDIR)/libdriver_cassandra_la-driver-cassandra.Plo \
+ ./$(DEPDIR)/libdriver_mysql_la-driver-mysql.Plo \
+ ./$(DEPDIR)/libdriver_pgsql_la-driver-pgsql.Plo \
+ ./$(DEPDIR)/libdriver_sqlite_la-driver-sqlite.Plo \
+ ./$(DEPDIR)/libdriver_test_la-driver-test.Plo \
+ ./$(DEPDIR)/sql-api.Plo ./$(DEPDIR)/sql-db-cache.Plo \
+ ./$(DEPDIR)/sql-drivers-register.Plo
+am__mv = mv -f
+COMPILE = $(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) \
+ $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS)
+LTCOMPILE = $(LIBTOOL) $(AM_V_lt) --tag=CC $(AM_LIBTOOLFLAGS) \
+ $(LIBTOOLFLAGS) --mode=compile $(CC) $(DEFS) \
+ $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) $(CPPFLAGS) \
+ $(AM_CFLAGS) $(CFLAGS)
+AM_V_CC = $(am__v_CC_@AM_V@)
+am__v_CC_ = $(am__v_CC_@AM_DEFAULT_V@)
+am__v_CC_0 = @echo " CC " $@;
+am__v_CC_1 =
+CCLD = $(CC)
+LINK = $(LIBTOOL) $(AM_V_lt) --tag=CC $(AM_LIBTOOLFLAGS) \
+ $(LIBTOOLFLAGS) --mode=link $(CCLD) $(AM_CFLAGS) $(CFLAGS) \
+ $(AM_LDFLAGS) $(LDFLAGS) -o $@
+AM_V_CCLD = $(am__v_CCLD_@AM_V@)
+am__v_CCLD_ = $(am__v_CCLD_@AM_DEFAULT_V@)
+am__v_CCLD_0 = @echo " CCLD " $@;
+am__v_CCLD_1 =
+SOURCES = $(libdovecot_sql_la_SOURCES) \
+ $(libdriver_cassandra_la_SOURCES) \
+ $(libdriver_mysql_la_SOURCES) $(libdriver_pgsql_la_SOURCES) \
+ $(libdriver_sqlite_la_SOURCES) $(libdriver_test_la_SOURCES) \
+ $(libsql_la_SOURCES) $(nodist_libsql_la_SOURCES)
+DIST_SOURCES = $(libdovecot_sql_la_SOURCES) \
+ $(am__libdriver_cassandra_la_SOURCES_DIST) \
+ $(am__libdriver_mysql_la_SOURCES_DIST) \
+ $(am__libdriver_pgsql_la_SOURCES_DIST) \
+ $(am__libdriver_sqlite_la_SOURCES_DIST) \
+ $(libdriver_test_la_SOURCES) $(am__libsql_la_SOURCES_DIST)
+am__can_run_installinfo = \
+ case $$AM_UPDATE_INFO_DIR in \
+ n|no|NO) false;; \
+ *) (install-info --version) >/dev/null 2>&1;; \
+ esac
+HEADERS = $(noinst_HEADERS) $(pkginc_lib_HEADERS)
+am__tagged_files = $(HEADERS) $(SOURCES) $(TAGS_FILES) $(LISP)
+# Read a list of newline-separated strings from the standard input,
+# and print each of them once, without duplicates. Input order is
+# *not* preserved.
+am__uniquify_input = $(AWK) '\
+ BEGIN { nonempty = 0; } \
+ { items[$$0] = 1; nonempty = 1; } \
+ END { if (nonempty) { for (i in items) print i; }; } \
+'
+# Make sure the list of sources is unique. This is necessary because,
+# e.g., the same source file might be shared among _SOURCES variables
+# for different programs/libraries.
+am__define_uniq_tagged_files = \
+ list='$(am__tagged_files)'; \
+ unique=`for i in $$list; do \
+ if test -f "$$i"; then echo $$i; else echo $(srcdir)/$$i; fi; \
+ done | $(am__uniquify_input)`
+ETAGS = etags
+CTAGS = ctags
+am__DIST_COMMON = $(srcdir)/Makefile.in $(top_srcdir)/depcomp
+DISTFILES = $(DIST_COMMON) $(DIST_SOURCES) $(TEXINFOS) $(EXTRA_DIST)
+ACLOCAL = @ACLOCAL@
+ACLOCAL_AMFLAGS = @ACLOCAL_AMFLAGS@
+AMTAR = @AMTAR@
+AM_DEFAULT_VERBOSITY = @AM_DEFAULT_VERBOSITY@
+APPARMOR_LIBS = @APPARMOR_LIBS@
+AR = @AR@
+AUTH_CFLAGS = @AUTH_CFLAGS@
+AUTH_LIBS = @AUTH_LIBS@
+AUTOCONF = @AUTOCONF@
+AUTOHEADER = @AUTOHEADER@
+AUTOMAKE = @AUTOMAKE@
+AWK = @AWK@
+BINARY_CFLAGS = @BINARY_CFLAGS@
+BINARY_LDFLAGS = @BINARY_LDFLAGS@
+BISON = @BISON@
+CASSANDRA_CFLAGS = @CASSANDRA_CFLAGS@
+CASSANDRA_LIBS = @CASSANDRA_LIBS@
+CC = @CC@
+CCDEPMODE = @CCDEPMODE@
+CDB_LIBS = @CDB_LIBS@
+CFLAGS = @CFLAGS@
+CLUCENE_CFLAGS = @CLUCENE_CFLAGS@
+CLUCENE_LIBS = @CLUCENE_LIBS@
+COMPRESS_LIBS = @COMPRESS_LIBS@
+CPP = @CPP@
+CPPFLAGS = @CPPFLAGS@
+CRYPT_LIBS = @CRYPT_LIBS@
+CXX = @CXX@
+CXXCPP = @CXXCPP@
+CXXDEPMODE = @CXXDEPMODE@
+CXXFLAGS = @CXXFLAGS@
+CYGPATH_W = @CYGPATH_W@
+DEFS = @DEFS@
+DEPDIR = @DEPDIR@
+DICT_LIBS = @DICT_LIBS@
+DLLIB = @DLLIB@
+DLLTOOL = @DLLTOOL@
+DSYMUTIL = @DSYMUTIL@
+DUMPBIN = @DUMPBIN@
+ECHO_C = @ECHO_C@
+ECHO_N = @ECHO_N@
+ECHO_T = @ECHO_T@
+EGREP = @EGREP@
+EXEEXT = @EXEEXT@
+FGREP = @FGREP@
+FLEX = @FLEX@
+FUZZER_CPPFLAGS = @FUZZER_CPPFLAGS@
+FUZZER_LDFLAGS = @FUZZER_LDFLAGS@
+GREP = @GREP@
+INSTALL = @INSTALL@
+INSTALL_DATA = @INSTALL_DATA@
+INSTALL_PROGRAM = @INSTALL_PROGRAM@
+INSTALL_SCRIPT = @INSTALL_SCRIPT@
+INSTALL_STRIP_PROGRAM = @INSTALL_STRIP_PROGRAM@
+KRB5CONFIG = @KRB5CONFIG@
+KRB5_CFLAGS = @KRB5_CFLAGS@
+KRB5_LIBS = @KRB5_LIBS@
+LD = @LD@
+LDAP_LIBS = @LDAP_LIBS@
+LDFLAGS = @LDFLAGS@
+LD_NO_WHOLE_ARCHIVE = @LD_NO_WHOLE_ARCHIVE@
+LD_WHOLE_ARCHIVE = @LD_WHOLE_ARCHIVE@
+LIBCAP = @LIBCAP@
+LIBDOVECOT = @LIBDOVECOT@
+LIBDOVECOT_COMPRESS = @LIBDOVECOT_COMPRESS@
+LIBDOVECOT_DEPS = @LIBDOVECOT_DEPS@
+LIBDOVECOT_DSYNC = @LIBDOVECOT_DSYNC@
+LIBDOVECOT_LA_LIBS = @LIBDOVECOT_LA_LIBS@
+LIBDOVECOT_LDA = @LIBDOVECOT_LDA@
+LIBDOVECOT_LDAP = @LIBDOVECOT_LDAP@
+LIBDOVECOT_LIBFTS = @LIBDOVECOT_LIBFTS@
+LIBDOVECOT_LIBFTS_DEPS = @LIBDOVECOT_LIBFTS_DEPS@
+LIBDOVECOT_LOGIN = @LIBDOVECOT_LOGIN@
+LIBDOVECOT_LUA = @LIBDOVECOT_LUA@
+LIBDOVECOT_LUA_DEPS = @LIBDOVECOT_LUA_DEPS@
+LIBDOVECOT_SQL = @LIBDOVECOT_SQL@
+LIBDOVECOT_STORAGE = @LIBDOVECOT_STORAGE@
+LIBDOVECOT_STORAGE_DEPS = @LIBDOVECOT_STORAGE_DEPS@
+LIBEXTTEXTCAT_CFLAGS = @LIBEXTTEXTCAT_CFLAGS@
+LIBEXTTEXTCAT_LIBS = @LIBEXTTEXTCAT_LIBS@
+LIBICONV = @LIBICONV@
+LIBICU_CFLAGS = @LIBICU_CFLAGS@
+LIBICU_LIBS = @LIBICU_LIBS@
+LIBOBJS = @LIBOBJS@
+LIBS = @LIBS@
+LIBSODIUM_CFLAGS = @LIBSODIUM_CFLAGS@
+LIBSODIUM_LIBS = @LIBSODIUM_LIBS@
+LIBTIRPC_CFLAGS = @LIBTIRPC_CFLAGS@
+LIBTIRPC_LIBS = @LIBTIRPC_LIBS@
+LIBTOOL = @LIBTOOL@
+LIBUNWIND_CFLAGS = @LIBUNWIND_CFLAGS@
+LIBUNWIND_LIBS = @LIBUNWIND_LIBS@
+LIBWRAP_LIBS = @LIBWRAP_LIBS@
+LINKED_STORAGE_LDADD = @LINKED_STORAGE_LDADD@
+LIPO = @LIPO@
+LN_S = @LN_S@
+LTLIBICONV = @LTLIBICONV@
+LTLIBOBJS = @LTLIBOBJS@
+LT_SYS_LIBRARY_PATH = @LT_SYS_LIBRARY_PATH@
+LUA_CFLAGS = @LUA_CFLAGS@
+LUA_LIBS = @LUA_LIBS@
+MAINT = @MAINT@
+MAKEINFO = @MAKEINFO@
+MANIFEST_TOOL = @MANIFEST_TOOL@
+MKDIR_P = @MKDIR_P@
+MODULE_LIBS = @MODULE_LIBS@
+MODULE_SUFFIX = @MODULE_SUFFIX@
+MYSQL_CFLAGS = @MYSQL_CFLAGS@
+MYSQL_CONFIG = @MYSQL_CONFIG@
+MYSQL_LIBS = @MYSQL_LIBS@
+NM = @NM@
+NMEDIT = @NMEDIT@
+
+# automake seems to force making this unconditional..
+NOPLUGIN_LDFLAGS =
+OBJDUMP = @OBJDUMP@
+OBJEXT = @OBJEXT@
+OTOOL = @OTOOL@
+OTOOL64 = @OTOOL64@
+PACKAGE = @PACKAGE@
+PACKAGE_BUGREPORT = @PACKAGE_BUGREPORT@
+PACKAGE_NAME = @PACKAGE_NAME@
+PACKAGE_STRING = @PACKAGE_STRING@
+PACKAGE_TARNAME = @PACKAGE_TARNAME@
+PACKAGE_URL = @PACKAGE_URL@
+PACKAGE_VERSION = @PACKAGE_VERSION@
+PANDOC = @PANDOC@
+PATH_SEPARATOR = @PATH_SEPARATOR@
+PGSQL_CFLAGS = @PGSQL_CFLAGS@
+PGSQL_LIBS = @PGSQL_LIBS@
+PG_CONFIG = @PG_CONFIG@
+PIE_CFLAGS = @PIE_CFLAGS@
+PIE_LDFLAGS = @PIE_LDFLAGS@
+PKG_CONFIG = @PKG_CONFIG@
+PKG_CONFIG_LIBDIR = @PKG_CONFIG_LIBDIR@
+PKG_CONFIG_PATH = @PKG_CONFIG_PATH@
+QUOTA_LIBS = @QUOTA_LIBS@
+RANLIB = @RANLIB@
+RELRO_LDFLAGS = @RELRO_LDFLAGS@
+RPCGEN = @RPCGEN@
+RUN_TEST = @RUN_TEST@
+SED = @SED@
+SETTING_FILES = @SETTING_FILES@
+SET_MAKE = @SET_MAKE@
+SHELL = @SHELL@
+SQLITE_CFLAGS = @SQLITE_CFLAGS@
+SQLITE_LIBS = @SQLITE_LIBS@
+SQL_CFLAGS = @SQL_CFLAGS@
+SQL_LIBS = @SQL_LIBS@
+SSL_CFLAGS = @SSL_CFLAGS@
+SSL_LIBS = @SSL_LIBS@
+STRIP = @STRIP@
+SYSTEMD_CFLAGS = @SYSTEMD_CFLAGS@
+SYSTEMD_LIBS = @SYSTEMD_LIBS@
+VALGRIND = @VALGRIND@
+VERSION = @VERSION@
+ZSTD_CFLAGS = @ZSTD_CFLAGS@
+ZSTD_LIBS = @ZSTD_LIBS@
+abs_builddir = @abs_builddir@
+abs_srcdir = @abs_srcdir@
+abs_top_builddir = @abs_top_builddir@
+abs_top_srcdir = @abs_top_srcdir@
+ac_ct_AR = @ac_ct_AR@
+ac_ct_CC = @ac_ct_CC@
+ac_ct_CXX = @ac_ct_CXX@
+ac_ct_DUMPBIN = @ac_ct_DUMPBIN@
+am__include = @am__include@
+am__leading_dot = @am__leading_dot@
+am__quote = @am__quote@
+am__tar = @am__tar@
+am__untar = @am__untar@
+bindir = @bindir@
+build = @build@
+build_alias = @build_alias@
+build_cpu = @build_cpu@
+build_os = @build_os@
+build_vendor = @build_vendor@
+builddir = @builddir@
+datadir = @datadir@
+datarootdir = @datarootdir@
+dict_drivers = @dict_drivers@
+docdir = @docdir@
+dvidir = @dvidir@
+exec_prefix = @exec_prefix@
+host = @host@
+host_alias = @host_alias@
+host_cpu = @host_cpu@
+host_os = @host_os@
+host_vendor = @host_vendor@
+htmldir = @htmldir@
+includedir = @includedir@
+infodir = @infodir@
+install_sh = @install_sh@
+libdir = @libdir@
+libexecdir = @libexecdir@
+localedir = @localedir@
+localstatedir = @localstatedir@
+mandir = @mandir@
+mkdir_p = @mkdir_p@
+moduledir = @moduledir@
+oldincludedir = @oldincludedir@
+pdfdir = @pdfdir@
+prefix = @prefix@
+program_transform_name = @program_transform_name@
+psdir = @psdir@
+rundir = @rundir@
+runstatedir = @runstatedir@
+sbindir = @sbindir@
+sharedstatedir = @sharedstatedir@
+sql_drivers = @sql_drivers@
+srcdir = @srcdir@
+ssldir = @ssldir@
+statedir = @statedir@
+sysconfdir = @sysconfdir@
+systemdservicetype = @systemdservicetype@
+systemdsystemunitdir = @systemdsystemunitdir@
+target_alias = @target_alias@
+top_build_prefix = @top_build_prefix@
+top_builddir = @top_builddir@
+top_srcdir = @top_srcdir@
+noinst_LTLIBRARIES = libsql.la libdriver_test.la
+SQL_DRIVER_PLUGINS = $(am__append_1) $(am__append_2) $(am__append_3) \
+ $(am__append_4)
+@BUILD_MYSQL_TRUE@@SQL_PLUGINS_TRUE@MYSQL_LIB = libdriver_mysql.la
+@BUILD_PGSQL_TRUE@@SQL_PLUGINS_TRUE@PGSQL_LIB = libdriver_pgsql.la
+@BUILD_SQLITE_TRUE@@SQL_PLUGINS_TRUE@SQLITE_LIB = libdriver_sqlite.la
+@BUILD_CASSANDRA_TRUE@@SQL_PLUGINS_TRUE@CASSANDRA_LIB = libdriver_cassandra.la
+@SQL_PLUGINS_TRUE@sql_module_LTLIBRARIES = \
+@SQL_PLUGINS_TRUE@ $(MYSQL_LIB) \
+@SQL_PLUGINS_TRUE@ $(PGSQL_LIB) \
+@SQL_PLUGINS_TRUE@ $(SQLITE_LIB) \
+@SQL_PLUGINS_TRUE@ $(CASSANDRA_LIB)
+
+@SQL_PLUGINS_TRUE@sql_moduledir = $(moduledir)
+AM_CPPFLAGS = \
+ -I$(top_srcdir)/src/lib \
+ -I$(top_srcdir)/src/lib-settings \
+ $(SQL_CFLAGS)
+
+dist_sources = \
+ sql-api.c \
+ sql-db-cache.c
+
+@SQL_PLUGINS_FALSE@driver_sources = \
+@SQL_PLUGINS_FALSE@ driver-mysql.c \
+@SQL_PLUGINS_FALSE@ driver-pgsql.c \
+@SQL_PLUGINS_FALSE@ driver-sqlite.c \
+@SQL_PLUGINS_FALSE@ driver-cassandra.c
+
+libsql_la_SOURCES = \
+ $(dist_sources) \
+ $(driver_sources) \
+ driver-sqlpool.c
+
+libsql_la_LIBADD = $(SQL_LIBS)
+nodist_libsql_la_SOURCES = sql-drivers-register.c
+deplibs = \
+ ../lib-dovecot/libdovecot.la
+
+@SQL_PLUGINS_TRUE@libdriver_mysql_la_LDFLAGS = -module -avoid-version
+@SQL_PLUGINS_TRUE@libdriver_mysql_la_LIBADD = $(MYSQL_LIBS)
+@SQL_PLUGINS_TRUE@libdriver_mysql_la_CPPFLAGS = $(AM_CPPFLAGS) $(MYSQL_CFLAGS)
+@SQL_PLUGINS_TRUE@libdriver_mysql_la_SOURCES = driver-mysql.c
+@SQL_PLUGINS_TRUE@libdriver_pgsql_la_LDFLAGS = -module -avoid-version
+@SQL_PLUGINS_TRUE@libdriver_pgsql_la_LIBADD = $(PGSQL_LIBS)
+@SQL_PLUGINS_TRUE@libdriver_pgsql_la_CPPFLAGS = $(AM_CPPFLAGS) $(PGSQL_CFLAGS)
+@SQL_PLUGINS_TRUE@libdriver_pgsql_la_SOURCES = driver-pgsql.c
+@SQL_PLUGINS_TRUE@libdriver_sqlite_la_LDFLAGS = -module -avoid-version
+@SQL_PLUGINS_TRUE@libdriver_sqlite_la_LIBADD = $(SQLITE_LIBS)
+@SQL_PLUGINS_TRUE@libdriver_sqlite_la_CPPFLAGS = $(AM_CPPFLAGS) $(SQLITE_CFLAGS)
+@SQL_PLUGINS_TRUE@libdriver_sqlite_la_SOURCES = driver-sqlite.c
+@SQL_PLUGINS_TRUE@libdriver_cassandra_la_LDFLAGS = -module -avoid-version
+@SQL_PLUGINS_TRUE@libdriver_cassandra_la_LIBADD = $(CASSANDRA_LIBS)
+@SQL_PLUGINS_TRUE@libdriver_cassandra_la_CPPFLAGS = $(AM_CPPFLAGS) $(CASSANDRA_CFLAGS)
+@SQL_PLUGINS_TRUE@libdriver_cassandra_la_SOURCES = driver-cassandra.c
+libdriver_test_la_LDFLAGS = -avoid-version
+libdriver_test_la_CPPFLAGS = $(AM_CPPFLAGS) \
+ -I$(top_srcdir)/src/lib-test
+
+libdriver_test_la_SOURCES = driver-test.c
+noinst_HEADERS = driver-test.h
+pkglib_LTLIBRARIES = libdovecot-sql.la
+libdovecot_sql_la_SOURCES =
+libdovecot_sql_la_LIBADD = libsql.la $(deplibs)
+libdovecot_sql_la_DEPENDENCIES = libsql.la
+libdovecot_sql_la_LDFLAGS = -export-dynamic
+headers = \
+ sql-api.h \
+ sql-api-private.h \
+ sql-db-cache.h
+
+pkginc_libdir = $(pkgincludedir)
+pkginc_lib_HEADERS = $(headers)
+all: all-am
+
+.SUFFIXES:
+.SUFFIXES: .c .lo .o .obj
+$(srcdir)/Makefile.in: @MAINTAINER_MODE_TRUE@ $(srcdir)/Makefile.am $(am__configure_deps)
+ @for dep in $?; do \
+ case '$(am__configure_deps)' in \
+ *$$dep*) \
+ ( cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh ) \
+ && { if test -f $@; then exit 0; else break; fi; }; \
+ exit 1;; \
+ esac; \
+ done; \
+ echo ' cd $(top_srcdir) && $(AUTOMAKE) --foreign src/lib-sql/Makefile'; \
+ $(am__cd) $(top_srcdir) && \
+ $(AUTOMAKE) --foreign src/lib-sql/Makefile
+Makefile: $(srcdir)/Makefile.in $(top_builddir)/config.status
+ @case '$?' in \
+ *config.status*) \
+ cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh;; \
+ *) \
+ echo ' cd $(top_builddir) && $(SHELL) ./config.status $(subdir)/$@ $(am__maybe_remake_depfiles)'; \
+ cd $(top_builddir) && $(SHELL) ./config.status $(subdir)/$@ $(am__maybe_remake_depfiles);; \
+ esac;
+
+$(top_builddir)/config.status: $(top_srcdir)/configure $(CONFIG_STATUS_DEPENDENCIES)
+ cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh
+
+$(top_srcdir)/configure: @MAINTAINER_MODE_TRUE@ $(am__configure_deps)
+ cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh
+$(ACLOCAL_M4): @MAINTAINER_MODE_TRUE@ $(am__aclocal_m4_deps)
+ cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh
+$(am__aclocal_m4_deps):
+
+clean-noinstLTLIBRARIES:
+ -test -z "$(noinst_LTLIBRARIES)" || rm -f $(noinst_LTLIBRARIES)
+ @list='$(noinst_LTLIBRARIES)'; \
+ locs=`for p in $$list; do echo $$p; done | \
+ sed 's|^[^/]*$$|.|; s|/[^/]*$$||; s|$$|/so_locations|' | \
+ sort -u`; \
+ test -z "$$locs" || { \
+ echo rm -f $${locs}; \
+ rm -f $${locs}; \
+ }
+
+install-pkglibLTLIBRARIES: $(pkglib_LTLIBRARIES)
+ @$(NORMAL_INSTALL)
+ @list='$(pkglib_LTLIBRARIES)'; test -n "$(pkglibdir)" || list=; \
+ list2=; for p in $$list; do \
+ if test -f $$p; then \
+ list2="$$list2 $$p"; \
+ else :; fi; \
+ done; \
+ test -z "$$list2" || { \
+ echo " $(MKDIR_P) '$(DESTDIR)$(pkglibdir)'"; \
+ $(MKDIR_P) "$(DESTDIR)$(pkglibdir)" || exit 1; \
+ echo " $(LIBTOOL) $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=install $(INSTALL) $(INSTALL_STRIP_FLAG) $$list2 '$(DESTDIR)$(pkglibdir)'"; \
+ $(LIBTOOL) $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=install $(INSTALL) $(INSTALL_STRIP_FLAG) $$list2 "$(DESTDIR)$(pkglibdir)"; \
+ }
+
+uninstall-pkglibLTLIBRARIES:
+ @$(NORMAL_UNINSTALL)
+ @list='$(pkglib_LTLIBRARIES)'; test -n "$(pkglibdir)" || list=; \
+ for p in $$list; do \
+ $(am__strip_dir) \
+ echo " $(LIBTOOL) $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=uninstall rm -f '$(DESTDIR)$(pkglibdir)/$$f'"; \
+ $(LIBTOOL) $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=uninstall rm -f "$(DESTDIR)$(pkglibdir)/$$f"; \
+ done
+
+clean-pkglibLTLIBRARIES:
+ -test -z "$(pkglib_LTLIBRARIES)" || rm -f $(pkglib_LTLIBRARIES)
+ @list='$(pkglib_LTLIBRARIES)'; \
+ locs=`for p in $$list; do echo $$p; done | \
+ sed 's|^[^/]*$$|.|; s|/[^/]*$$||; s|$$|/so_locations|' | \
+ sort -u`; \
+ test -z "$$locs" || { \
+ echo rm -f $${locs}; \
+ rm -f $${locs}; \
+ }
+
+install-sql_moduleLTLIBRARIES: $(sql_module_LTLIBRARIES)
+ @$(NORMAL_INSTALL)
+ @list='$(sql_module_LTLIBRARIES)'; test -n "$(sql_moduledir)" || list=; \
+ list2=; for p in $$list; do \
+ if test -f $$p; then \
+ list2="$$list2 $$p"; \
+ else :; fi; \
+ done; \
+ test -z "$$list2" || { \
+ echo " $(MKDIR_P) '$(DESTDIR)$(sql_moduledir)'"; \
+ $(MKDIR_P) "$(DESTDIR)$(sql_moduledir)" || exit 1; \
+ echo " $(LIBTOOL) $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=install $(INSTALL) $(INSTALL_STRIP_FLAG) $$list2 '$(DESTDIR)$(sql_moduledir)'"; \
+ $(LIBTOOL) $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=install $(INSTALL) $(INSTALL_STRIP_FLAG) $$list2 "$(DESTDIR)$(sql_moduledir)"; \
+ }
+
+uninstall-sql_moduleLTLIBRARIES:
+ @$(NORMAL_UNINSTALL)
+ @list='$(sql_module_LTLIBRARIES)'; test -n "$(sql_moduledir)" || list=; \
+ for p in $$list; do \
+ $(am__strip_dir) \
+ echo " $(LIBTOOL) $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=uninstall rm -f '$(DESTDIR)$(sql_moduledir)/$$f'"; \
+ $(LIBTOOL) $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=uninstall rm -f "$(DESTDIR)$(sql_moduledir)/$$f"; \
+ done
+
+clean-sql_moduleLTLIBRARIES:
+ -test -z "$(sql_module_LTLIBRARIES)" || rm -f $(sql_module_LTLIBRARIES)
+ @list='$(sql_module_LTLIBRARIES)'; \
+ locs=`for p in $$list; do echo $$p; done | \
+ sed 's|^[^/]*$$|.|; s|/[^/]*$$||; s|$$|/so_locations|' | \
+ sort -u`; \
+ test -z "$$locs" || { \
+ echo rm -f $${locs}; \
+ rm -f $${locs}; \
+ }
+
+libdovecot-sql.la: $(libdovecot_sql_la_OBJECTS) $(libdovecot_sql_la_DEPENDENCIES) $(EXTRA_libdovecot_sql_la_DEPENDENCIES)
+ $(AM_V_CCLD)$(libdovecot_sql_la_LINK) -rpath $(pkglibdir) $(libdovecot_sql_la_OBJECTS) $(libdovecot_sql_la_LIBADD) $(LIBS)
+
+libdriver_cassandra.la: $(libdriver_cassandra_la_OBJECTS) $(libdriver_cassandra_la_DEPENDENCIES) $(EXTRA_libdriver_cassandra_la_DEPENDENCIES)
+ $(AM_V_CCLD)$(libdriver_cassandra_la_LINK) $(am_libdriver_cassandra_la_rpath) $(libdriver_cassandra_la_OBJECTS) $(libdriver_cassandra_la_LIBADD) $(LIBS)
+
+libdriver_mysql.la: $(libdriver_mysql_la_OBJECTS) $(libdriver_mysql_la_DEPENDENCIES) $(EXTRA_libdriver_mysql_la_DEPENDENCIES)
+ $(AM_V_CCLD)$(libdriver_mysql_la_LINK) $(am_libdriver_mysql_la_rpath) $(libdriver_mysql_la_OBJECTS) $(libdriver_mysql_la_LIBADD) $(LIBS)
+
+libdriver_pgsql.la: $(libdriver_pgsql_la_OBJECTS) $(libdriver_pgsql_la_DEPENDENCIES) $(EXTRA_libdriver_pgsql_la_DEPENDENCIES)
+ $(AM_V_CCLD)$(libdriver_pgsql_la_LINK) $(am_libdriver_pgsql_la_rpath) $(libdriver_pgsql_la_OBJECTS) $(libdriver_pgsql_la_LIBADD) $(LIBS)
+
+libdriver_sqlite.la: $(libdriver_sqlite_la_OBJECTS) $(libdriver_sqlite_la_DEPENDENCIES) $(EXTRA_libdriver_sqlite_la_DEPENDENCIES)
+ $(AM_V_CCLD)$(libdriver_sqlite_la_LINK) $(am_libdriver_sqlite_la_rpath) $(libdriver_sqlite_la_OBJECTS) $(libdriver_sqlite_la_LIBADD) $(LIBS)
+
+libdriver_test.la: $(libdriver_test_la_OBJECTS) $(libdriver_test_la_DEPENDENCIES) $(EXTRA_libdriver_test_la_DEPENDENCIES)
+ $(AM_V_CCLD)$(libdriver_test_la_LINK) $(libdriver_test_la_OBJECTS) $(libdriver_test_la_LIBADD) $(LIBS)
+
+libsql.la: $(libsql_la_OBJECTS) $(libsql_la_DEPENDENCIES) $(EXTRA_libsql_la_DEPENDENCIES)
+ $(AM_V_CCLD)$(LINK) $(libsql_la_OBJECTS) $(libsql_la_LIBADD) $(LIBS)
+
+mostlyclean-compile:
+ -rm -f *.$(OBJEXT)
+
+distclean-compile:
+ -rm -f *.tab.c
+
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/driver-cassandra.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/driver-mysql.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/driver-pgsql.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/driver-sqlite.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/driver-sqlpool.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/libdriver_cassandra_la-driver-cassandra.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/libdriver_mysql_la-driver-mysql.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/libdriver_pgsql_la-driver-pgsql.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/libdriver_sqlite_la-driver-sqlite.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/libdriver_test_la-driver-test.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/sql-api.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/sql-db-cache.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/sql-drivers-register.Plo@am__quote@ # am--include-marker
+
+$(am__depfiles_remade):
+ @$(MKDIR_P) $(@D)
+ @echo '# dummy' >$@-t && $(am__mv) $@-t $@
+
+am--depfiles: $(am__depfiles_remade)
+
+.c.o:
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(COMPILE) -MT $@ -MD -MP -MF $(DEPDIR)/$*.Tpo -c -o $@ $<
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/$*.Tpo $(DEPDIR)/$*.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='$<' object='$@' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(COMPILE) -c -o $@ $<
+
+.c.obj:
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(COMPILE) -MT $@ -MD -MP -MF $(DEPDIR)/$*.Tpo -c -o $@ `$(CYGPATH_W) '$<'`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/$*.Tpo $(DEPDIR)/$*.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='$<' object='$@' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(COMPILE) -c -o $@ `$(CYGPATH_W) '$<'`
+
+.c.lo:
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(LTCOMPILE) -MT $@ -MD -MP -MF $(DEPDIR)/$*.Tpo -c -o $@ $<
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/$*.Tpo $(DEPDIR)/$*.Plo
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='$<' object='$@' libtool=yes @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(LTCOMPILE) -c -o $@ $<
+
+libdriver_cassandra_la-driver-cassandra.lo: driver-cassandra.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(LIBTOOL) $(AM_V_lt) --tag=CC $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=compile $(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdriver_cassandra_la_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT libdriver_cassandra_la-driver-cassandra.lo -MD -MP -MF $(DEPDIR)/libdriver_cassandra_la-driver-cassandra.Tpo -c -o libdriver_cassandra_la-driver-cassandra.lo `test -f 'driver-cassandra.c' || echo '$(srcdir)/'`driver-cassandra.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/libdriver_cassandra_la-driver-cassandra.Tpo $(DEPDIR)/libdriver_cassandra_la-driver-cassandra.Plo
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='driver-cassandra.c' object='libdriver_cassandra_la-driver-cassandra.lo' libtool=yes @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(LIBTOOL) $(AM_V_lt) --tag=CC $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=compile $(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdriver_cassandra_la_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o libdriver_cassandra_la-driver-cassandra.lo `test -f 'driver-cassandra.c' || echo '$(srcdir)/'`driver-cassandra.c
+
+libdriver_mysql_la-driver-mysql.lo: driver-mysql.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(LIBTOOL) $(AM_V_lt) --tag=CC $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=compile $(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdriver_mysql_la_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT libdriver_mysql_la-driver-mysql.lo -MD -MP -MF $(DEPDIR)/libdriver_mysql_la-driver-mysql.Tpo -c -o libdriver_mysql_la-driver-mysql.lo `test -f 'driver-mysql.c' || echo '$(srcdir)/'`driver-mysql.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/libdriver_mysql_la-driver-mysql.Tpo $(DEPDIR)/libdriver_mysql_la-driver-mysql.Plo
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='driver-mysql.c' object='libdriver_mysql_la-driver-mysql.lo' libtool=yes @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(LIBTOOL) $(AM_V_lt) --tag=CC $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=compile $(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdriver_mysql_la_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o libdriver_mysql_la-driver-mysql.lo `test -f 'driver-mysql.c' || echo '$(srcdir)/'`driver-mysql.c
+
+libdriver_pgsql_la-driver-pgsql.lo: driver-pgsql.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(LIBTOOL) $(AM_V_lt) --tag=CC $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=compile $(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdriver_pgsql_la_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT libdriver_pgsql_la-driver-pgsql.lo -MD -MP -MF $(DEPDIR)/libdriver_pgsql_la-driver-pgsql.Tpo -c -o libdriver_pgsql_la-driver-pgsql.lo `test -f 'driver-pgsql.c' || echo '$(srcdir)/'`driver-pgsql.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/libdriver_pgsql_la-driver-pgsql.Tpo $(DEPDIR)/libdriver_pgsql_la-driver-pgsql.Plo
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='driver-pgsql.c' object='libdriver_pgsql_la-driver-pgsql.lo' libtool=yes @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(LIBTOOL) $(AM_V_lt) --tag=CC $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=compile $(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdriver_pgsql_la_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o libdriver_pgsql_la-driver-pgsql.lo `test -f 'driver-pgsql.c' || echo '$(srcdir)/'`driver-pgsql.c
+
+libdriver_sqlite_la-driver-sqlite.lo: driver-sqlite.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(LIBTOOL) $(AM_V_lt) --tag=CC $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=compile $(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdriver_sqlite_la_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT libdriver_sqlite_la-driver-sqlite.lo -MD -MP -MF $(DEPDIR)/libdriver_sqlite_la-driver-sqlite.Tpo -c -o libdriver_sqlite_la-driver-sqlite.lo `test -f 'driver-sqlite.c' || echo '$(srcdir)/'`driver-sqlite.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/libdriver_sqlite_la-driver-sqlite.Tpo $(DEPDIR)/libdriver_sqlite_la-driver-sqlite.Plo
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='driver-sqlite.c' object='libdriver_sqlite_la-driver-sqlite.lo' libtool=yes @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(LIBTOOL) $(AM_V_lt) --tag=CC $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=compile $(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdriver_sqlite_la_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o libdriver_sqlite_la-driver-sqlite.lo `test -f 'driver-sqlite.c' || echo '$(srcdir)/'`driver-sqlite.c
+
+libdriver_test_la-driver-test.lo: driver-test.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(LIBTOOL) $(AM_V_lt) --tag=CC $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=compile $(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdriver_test_la_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT libdriver_test_la-driver-test.lo -MD -MP -MF $(DEPDIR)/libdriver_test_la-driver-test.Tpo -c -o libdriver_test_la-driver-test.lo `test -f 'driver-test.c' || echo '$(srcdir)/'`driver-test.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/libdriver_test_la-driver-test.Tpo $(DEPDIR)/libdriver_test_la-driver-test.Plo
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='driver-test.c' object='libdriver_test_la-driver-test.lo' libtool=yes @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(LIBTOOL) $(AM_V_lt) --tag=CC $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=compile $(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdriver_test_la_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o libdriver_test_la-driver-test.lo `test -f 'driver-test.c' || echo '$(srcdir)/'`driver-test.c
+
+mostlyclean-libtool:
+ -rm -f *.lo
+
+clean-libtool:
+ -rm -rf .libs _libs
+install-pkginc_libHEADERS: $(pkginc_lib_HEADERS)
+ @$(NORMAL_INSTALL)
+ @list='$(pkginc_lib_HEADERS)'; test -n "$(pkginc_libdir)" || list=; \
+ if test -n "$$list"; then \
+ echo " $(MKDIR_P) '$(DESTDIR)$(pkginc_libdir)'"; \
+ $(MKDIR_P) "$(DESTDIR)$(pkginc_libdir)" || exit 1; \
+ fi; \
+ for p in $$list; do \
+ if test -f "$$p"; then d=; else d="$(srcdir)/"; fi; \
+ echo "$$d$$p"; \
+ done | $(am__base_list) | \
+ while read files; do \
+ echo " $(INSTALL_HEADER) $$files '$(DESTDIR)$(pkginc_libdir)'"; \
+ $(INSTALL_HEADER) $$files "$(DESTDIR)$(pkginc_libdir)" || exit $$?; \
+ done
+
+uninstall-pkginc_libHEADERS:
+ @$(NORMAL_UNINSTALL)
+ @list='$(pkginc_lib_HEADERS)'; test -n "$(pkginc_libdir)" || list=; \
+ files=`for p in $$list; do echo $$p; done | sed -e 's|^.*/||'`; \
+ dir='$(DESTDIR)$(pkginc_libdir)'; $(am__uninstall_files_from_dir)
+
+ID: $(am__tagged_files)
+ $(am__define_uniq_tagged_files); mkid -fID $$unique
+tags: tags-am
+TAGS: tags
+
+tags-am: $(TAGS_DEPENDENCIES) $(am__tagged_files)
+ set x; \
+ here=`pwd`; \
+ $(am__define_uniq_tagged_files); \
+ shift; \
+ if test -z "$(ETAGS_ARGS)$$*$$unique"; then :; else \
+ test -n "$$unique" || unique=$$empty_fix; \
+ if test $$# -gt 0; then \
+ $(ETAGS) $(ETAGSFLAGS) $(AM_ETAGSFLAGS) $(ETAGS_ARGS) \
+ "$$@" $$unique; \
+ else \
+ $(ETAGS) $(ETAGSFLAGS) $(AM_ETAGSFLAGS) $(ETAGS_ARGS) \
+ $$unique; \
+ fi; \
+ fi
+ctags: ctags-am
+
+CTAGS: ctags
+ctags-am: $(TAGS_DEPENDENCIES) $(am__tagged_files)
+ $(am__define_uniq_tagged_files); \
+ test -z "$(CTAGS_ARGS)$$unique" \
+ || $(CTAGS) $(CTAGSFLAGS) $(AM_CTAGSFLAGS) $(CTAGS_ARGS) \
+ $$unique
+
+GTAGS:
+ here=`$(am__cd) $(top_builddir) && pwd` \
+ && $(am__cd) $(top_srcdir) \
+ && gtags -i $(GTAGS_ARGS) "$$here"
+cscopelist: cscopelist-am
+
+cscopelist-am: $(am__tagged_files)
+ list='$(am__tagged_files)'; \
+ case "$(srcdir)" in \
+ [\\/]* | ?:[\\/]*) sdir="$(srcdir)" ;; \
+ *) sdir=$(subdir)/$(srcdir) ;; \
+ esac; \
+ for i in $$list; do \
+ if test -f "$$i"; then \
+ echo "$(subdir)/$$i"; \
+ else \
+ echo "$$sdir/$$i"; \
+ fi; \
+ done >> $(top_builddir)/cscope.files
+
+distclean-tags:
+ -rm -f TAGS ID GTAGS GRTAGS GSYMS GPATH tags
+
+distdir: $(BUILT_SOURCES)
+ $(MAKE) $(AM_MAKEFLAGS) distdir-am
+
+distdir-am: $(DISTFILES)
+ @srcdirstrip=`echo "$(srcdir)" | sed 's/[].[^$$\\*]/\\\\&/g'`; \
+ topsrcdirstrip=`echo "$(top_srcdir)" | sed 's/[].[^$$\\*]/\\\\&/g'`; \
+ list='$(DISTFILES)'; \
+ dist_files=`for file in $$list; do echo $$file; done | \
+ sed -e "s|^$$srcdirstrip/||;t" \
+ -e "s|^$$topsrcdirstrip/|$(top_builddir)/|;t"`; \
+ case $$dist_files in \
+ */*) $(MKDIR_P) `echo "$$dist_files" | \
+ sed '/\//!d;s|^|$(distdir)/|;s,/[^/]*$$,,' | \
+ sort -u` ;; \
+ esac; \
+ for file in $$dist_files; do \
+ if test -f $$file || test -d $$file; then d=.; else d=$(srcdir); fi; \
+ if test -d $$d/$$file; then \
+ dir=`echo "/$$file" | sed -e 's,/[^/]*$$,,'`; \
+ if test -d "$(distdir)/$$file"; then \
+ find "$(distdir)/$$file" -type d ! -perm -700 -exec chmod u+rwx {} \;; \
+ fi; \
+ if test -d $(srcdir)/$$file && test $$d != $(srcdir); then \
+ cp -fpR $(srcdir)/$$file "$(distdir)$$dir" || exit 1; \
+ find "$(distdir)/$$file" -type d ! -perm -700 -exec chmod u+rwx {} \;; \
+ fi; \
+ cp -fpR $$d/$$file "$(distdir)$$dir" || exit 1; \
+ else \
+ test -f "$(distdir)/$$file" \
+ || cp -p $$d/$$file "$(distdir)/$$file" \
+ || exit 1; \
+ fi; \
+ done
+check-am: all-am
+check: check-am
+all-am: Makefile $(LTLIBRARIES) $(HEADERS)
+installdirs:
+ for dir in "$(DESTDIR)$(pkglibdir)" "$(DESTDIR)$(sql_moduledir)" "$(DESTDIR)$(pkginc_libdir)"; do \
+ test -z "$$dir" || $(MKDIR_P) "$$dir"; \
+ done
+install: install-am
+install-exec: install-exec-am
+install-data: install-data-am
+uninstall: uninstall-am
+
+install-am: all-am
+ @$(MAKE) $(AM_MAKEFLAGS) install-exec-am install-data-am
+
+installcheck: installcheck-am
+install-strip:
+ if test -z '$(STRIP)'; then \
+ $(MAKE) $(AM_MAKEFLAGS) INSTALL_PROGRAM="$(INSTALL_STRIP_PROGRAM)" \
+ install_sh_PROGRAM="$(INSTALL_STRIP_PROGRAM)" INSTALL_STRIP_FLAG=-s \
+ install; \
+ else \
+ $(MAKE) $(AM_MAKEFLAGS) INSTALL_PROGRAM="$(INSTALL_STRIP_PROGRAM)" \
+ install_sh_PROGRAM="$(INSTALL_STRIP_PROGRAM)" INSTALL_STRIP_FLAG=-s \
+ "INSTALL_PROGRAM_ENV=STRIPPROG='$(STRIP)'" install; \
+ fi
+mostlyclean-generic:
+
+clean-generic:
+
+maintainer-clean-generic:
+ @echo "This command is intended for maintainers to use"
+ @echo "it deletes files that may require special tools to rebuild."
+@SQL_PLUGINS_FALSE@install-exec-local:
+clean: clean-am
+
+clean-am: clean-generic clean-libtool clean-noinstLTLIBRARIES \
+ clean-pkglibLTLIBRARIES clean-sql_moduleLTLIBRARIES \
+ mostlyclean-am
+
+distclean: distclean-am
+ -rm -f ./$(DEPDIR)/driver-cassandra.Plo
+ -rm -f ./$(DEPDIR)/driver-mysql.Plo
+ -rm -f ./$(DEPDIR)/driver-pgsql.Plo
+ -rm -f ./$(DEPDIR)/driver-sqlite.Plo
+ -rm -f ./$(DEPDIR)/driver-sqlpool.Plo
+ -rm -f ./$(DEPDIR)/libdriver_cassandra_la-driver-cassandra.Plo
+ -rm -f ./$(DEPDIR)/libdriver_mysql_la-driver-mysql.Plo
+ -rm -f ./$(DEPDIR)/libdriver_pgsql_la-driver-pgsql.Plo
+ -rm -f ./$(DEPDIR)/libdriver_sqlite_la-driver-sqlite.Plo
+ -rm -f ./$(DEPDIR)/libdriver_test_la-driver-test.Plo
+ -rm -f ./$(DEPDIR)/sql-api.Plo
+ -rm -f ./$(DEPDIR)/sql-db-cache.Plo
+ -rm -f ./$(DEPDIR)/sql-drivers-register.Plo
+ -rm -f Makefile
+distclean-am: clean-am distclean-compile distclean-generic \
+ distclean-tags
+
+dvi: dvi-am
+
+dvi-am:
+
+html: html-am
+
+html-am:
+
+info: info-am
+
+info-am:
+
+install-data-am: install-pkginc_libHEADERS \
+ install-sql_moduleLTLIBRARIES
+
+install-dvi: install-dvi-am
+
+install-dvi-am:
+
+install-exec-am: install-exec-local install-pkglibLTLIBRARIES
+
+install-html: install-html-am
+
+install-html-am:
+
+install-info: install-info-am
+
+install-info-am:
+
+install-man:
+
+install-pdf: install-pdf-am
+
+install-pdf-am:
+
+install-ps: install-ps-am
+
+install-ps-am:
+
+installcheck-am:
+
+maintainer-clean: maintainer-clean-am
+ -rm -f ./$(DEPDIR)/driver-cassandra.Plo
+ -rm -f ./$(DEPDIR)/driver-mysql.Plo
+ -rm -f ./$(DEPDIR)/driver-pgsql.Plo
+ -rm -f ./$(DEPDIR)/driver-sqlite.Plo
+ -rm -f ./$(DEPDIR)/driver-sqlpool.Plo
+ -rm -f ./$(DEPDIR)/libdriver_cassandra_la-driver-cassandra.Plo
+ -rm -f ./$(DEPDIR)/libdriver_mysql_la-driver-mysql.Plo
+ -rm -f ./$(DEPDIR)/libdriver_pgsql_la-driver-pgsql.Plo
+ -rm -f ./$(DEPDIR)/libdriver_sqlite_la-driver-sqlite.Plo
+ -rm -f ./$(DEPDIR)/libdriver_test_la-driver-test.Plo
+ -rm -f ./$(DEPDIR)/sql-api.Plo
+ -rm -f ./$(DEPDIR)/sql-db-cache.Plo
+ -rm -f ./$(DEPDIR)/sql-drivers-register.Plo
+ -rm -f Makefile
+maintainer-clean-am: distclean-am maintainer-clean-generic
+
+mostlyclean: mostlyclean-am
+
+mostlyclean-am: mostlyclean-compile mostlyclean-generic \
+ mostlyclean-libtool
+
+pdf: pdf-am
+
+pdf-am:
+
+ps: ps-am
+
+ps-am:
+
+uninstall-am: uninstall-pkginc_libHEADERS uninstall-pkglibLTLIBRARIES \
+ uninstall-sql_moduleLTLIBRARIES
+
+.MAKE: install-am install-strip
+
+.PHONY: CTAGS GTAGS TAGS all all-am am--depfiles check check-am clean \
+ clean-generic clean-libtool clean-noinstLTLIBRARIES \
+ clean-pkglibLTLIBRARIES clean-sql_moduleLTLIBRARIES \
+ cscopelist-am ctags ctags-am distclean distclean-compile \
+ distclean-generic distclean-libtool distclean-tags distdir dvi \
+ dvi-am html html-am info info-am install install-am \
+ install-data install-data-am install-dvi install-dvi-am \
+ install-exec install-exec-am install-exec-local install-html \
+ install-html-am install-info install-info-am install-man \
+ install-pdf install-pdf-am install-pkginc_libHEADERS \
+ install-pkglibLTLIBRARIES install-ps install-ps-am \
+ install-sql_moduleLTLIBRARIES install-strip installcheck \
+ installcheck-am installdirs maintainer-clean \
+ maintainer-clean-generic mostlyclean mostlyclean-compile \
+ mostlyclean-generic mostlyclean-libtool pdf pdf-am ps ps-am \
+ tags tags-am uninstall uninstall-am \
+ uninstall-pkginc_libHEADERS uninstall-pkglibLTLIBRARIES \
+ uninstall-sql_moduleLTLIBRARIES
+
+.PRECIOUS: Makefile
+
+
+sql-drivers-register.c: Makefile
+ rm -f $@
+ echo '/* this file automatically generated by Makefile */' >$@
+ echo '#include "lib.h"' >>$@
+ echo '#include "sql-api.h"' >>$@
+@SQL_PLUGINS_FALSE@ for i in $(sql_drivers) null; do \
+@SQL_PLUGINS_FALSE@ if [ "$${i}" != "null" ]; then \
+@SQL_PLUGINS_FALSE@ echo "extern struct sql_db driver_$${i}_db;" >>$@ ; \
+@SQL_PLUGINS_FALSE@ fi; \
+@SQL_PLUGINS_FALSE@ done
+ echo 'void sql_drivers_register_all(void) {' >>$@
+@SQL_PLUGINS_FALSE@ for i in $(sql_drivers) null; do \
+@SQL_PLUGINS_FALSE@ if [ "$${i}" != "null" ]; then \
+@SQL_PLUGINS_FALSE@ echo "sql_driver_register(&driver_$${i}_db);" >>$@ ; \
+@SQL_PLUGINS_FALSE@ fi; \
+@SQL_PLUGINS_FALSE@ done
+ echo '}' >>$@
+
+@SQL_PLUGINS_TRUE@install-exec-local:
+@SQL_PLUGINS_TRUE@ for d in auth dict; do \
+@SQL_PLUGINS_TRUE@ $(mkdir_p) $(DESTDIR)$(moduledir)/$$d; \
+@SQL_PLUGINS_TRUE@ for driver in $(SQL_DRIVER_PLUGINS); do \
+@SQL_PLUGINS_TRUE@ rm -f $(DESTDIR)$(moduledir)/$$d/libdriver_$$driver.so; \
+@SQL_PLUGINS_TRUE@ $(LN_S) ../libdriver_$$driver.so $(DESTDIR)$(moduledir)/$$d; \
+@SQL_PLUGINS_TRUE@ done; \
+@SQL_PLUGINS_TRUE@ done
+
+distclean-generic:
+ rm -f Makefile sql-drivers-register.c
+
+# Tell versions [3.59,3.63) of GNU make to not export all variables.
+# Otherwise a system limit (for SysV at least) may be exceeded.
+.NOEXPORT:
diff --git a/src/lib-sql/driver-cassandra.c b/src/lib-sql/driver-cassandra.c
new file mode 100644
index 0000000..4e66809
--- /dev/null
+++ b/src/lib-sql/driver-cassandra.c
@@ -0,0 +1,2579 @@
+/* Copyright (c) 2015-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "istream.h"
+#include "array.h"
+#include "hostpid.h"
+#include "hex-binary.h"
+#include "str.h"
+#include "ioloop.h"
+#include "net.h"
+#include "write-full.h"
+#include "time-util.h"
+#include "var-expand.h"
+#include "safe-memset.h"
+#include "settings-parser.h"
+#include "sql-api-private.h"
+
+#ifdef BUILD_CASSANDRA
+#include <stdio.h>
+#include <fcntl.h>
+#include <unistd.h>
+#include <cassandra.h>
+#include <pthread.h>
+
+#define IS_CONNECTED(db) \
+ ((db)->api.state != SQL_DB_STATE_DISCONNECTED && \
+ (db)->api.state != SQL_DB_STATE_CONNECTING)
+
+#define CASSANDRA_FALLBACK_WARN_INTERVAL_SECS 60
+#define CASSANDRA_FALLBACK_FIRST_RETRY_MSECS 50
+#define CASSANDRA_FALLBACK_MAX_RETRY_MSECS (1000*60)
+
+#define CASS_QUERY_DEFAULT_WARN_TIMEOUT_MSECS (5*1000)
+
+typedef void driver_cassandra_callback_t(CassFuture *future, void *context);
+
+enum cassandra_counter_type {
+ CASSANDRA_COUNTER_TYPE_QUERY_SENT,
+ CASSANDRA_COUNTER_TYPE_QUERY_RECV_OK,
+ CASSANDRA_COUNTER_TYPE_QUERY_RECV_ERR_NO_HOSTS,
+ CASSANDRA_COUNTER_TYPE_QUERY_RECV_ERR_QUEUE_FULL,
+ CASSANDRA_COUNTER_TYPE_QUERY_RECV_ERR_CLIENT_TIMEOUT,
+ CASSANDRA_COUNTER_TYPE_QUERY_RECV_ERR_SERVER_TIMEOUT,
+ CASSANDRA_COUNTER_TYPE_QUERY_RECV_ERR_SERVER_UNAVAILABLE,
+ CASSANDRA_COUNTER_TYPE_QUERY_RECV_ERR_OTHER,
+ CASSANDRA_COUNTER_TYPE_QUERY_SLOW,
+
+ CASSANDRA_COUNTER_COUNT
+};
+static const char *counter_names[CASSANDRA_COUNTER_COUNT] = {
+ "sent",
+ "recv_ok",
+ "recv_err_no_hosts",
+ "recv_err_queue_full",
+ "recv_err_client_timeout",
+ "recv_err_server_timeout",
+ "recv_err_server_unavailable",
+ "recv_err_other",
+ "slow",
+};
+
+enum cassandra_query_type {
+ CASSANDRA_QUERY_TYPE_READ,
+ CASSANDRA_QUERY_TYPE_READ_MORE,
+ CASSANDRA_QUERY_TYPE_WRITE,
+ CASSANDRA_QUERY_TYPE_DELETE,
+
+ CASSANDRA_QUERY_TYPE_COUNT
+};
+
+static const char *cassandra_query_type_names[CASSANDRA_QUERY_TYPE_COUNT] = {
+ "read", "read-more", "write", "delete"
+};
+
+struct cassandra_callback {
+ unsigned int id;
+ struct timeout *to;
+ CassFuture *future;
+ struct cassandra_db *db;
+ driver_cassandra_callback_t *callback;
+ void *context;
+};
+
+struct cassandra_db {
+ struct sql_db api;
+
+ char *hosts, *keyspace, *user, *password;
+ CassConsistency read_consistency, write_consistency, delete_consistency;
+ CassConsistency read_fallback_consistency, write_fallback_consistency;
+ CassConsistency delete_fallback_consistency;
+ CassLogLevel log_level;
+ bool debug_queries;
+ bool latency_aware_routing;
+ bool init_ssl;
+ unsigned int protocol_version;
+ unsigned int num_threads;
+ unsigned int connect_timeout_msecs, request_timeout_msecs;
+ unsigned int warn_timeout_msecs;
+ unsigned int heartbeat_interval_secs, idle_timeout_secs;
+ unsigned int execution_retry_interval_msecs, execution_retry_times;
+ unsigned int page_size;
+ in_port_t port;
+
+ CassCluster *cluster;
+ CassSession *session;
+ CassTimestampGen *timestamp_gen;
+ CassSsl *ssl;
+
+ int fd_pipe[2];
+ struct io *io_pipe;
+ ARRAY(struct cassandra_sql_prepared_statement *) pending_prepares;
+ ARRAY(struct cassandra_callback *) callbacks;
+ ARRAY(struct cassandra_result *) results;
+ unsigned int callback_ids;
+
+ char *metrics_path;
+ char *ssl_ca_file;
+ char *ssl_cert_file;
+ char *ssl_private_key_file;
+ char *ssl_private_key_password;
+ CassSslVerifyFlags ssl_verify_flags;
+
+ struct timeout *to_metrics;
+ uint64_t counters[CASSANDRA_COUNTER_COUNT];
+
+ struct timeval primary_query_last_sent[CASSANDRA_QUERY_TYPE_COUNT];
+ time_t last_fallback_warning[CASSANDRA_QUERY_TYPE_COUNT];
+ unsigned int fallback_failures[CASSANDRA_QUERY_TYPE_COUNT];
+
+ /* for synchronous queries: */
+ struct ioloop *ioloop, *orig_ioloop;
+ struct sql_result *sync_result;
+
+ char *error;
+};
+
+struct cassandra_result {
+ struct sql_result api;
+ CassStatement *statement;
+ const CassResult *result;
+ CassIterator *iterator;
+ char *query;
+ char *error;
+ CassConsistency consistency, fallback_consistency;
+ enum cassandra_query_type query_type;
+ struct timeval page0_start_time, start_time, finish_time;
+ unsigned int row_count, total_row_count, page_num;
+ cass_int64_t timestamp;
+
+ pool_t row_pool;
+ ARRAY_TYPE(const_string) fields;
+ ARRAY(size_t) field_sizes;
+
+ sql_query_callback_t *callback;
+ void *context;
+
+ bool is_prepared:1;
+ bool query_sent:1;
+ bool finished:1;
+ bool paging_continues:1;
+};
+
+struct cassandra_transaction_context {
+ struct sql_transaction_context ctx;
+ int refcount;
+
+ sql_commit_callback_t *callback;
+ void *context;
+
+ struct cassandra_sql_statement *stmt;
+ char *query;
+ cass_int64_t query_timestamp;
+ char *error;
+
+ bool begin_succeeded:1;
+ bool begin_failed:1;
+ bool failed:1;
+};
+
+struct cassandra_sql_arg {
+ unsigned int column_idx;
+
+ char *value_str;
+ const unsigned char *value_binary;
+ size_t value_binary_size;
+ int64_t value_int64;
+};
+
+struct cassandra_sql_statement {
+ struct sql_statement stmt;
+
+ struct cassandra_sql_prepared_statement *prep;
+ CassStatement *cass_stmt;
+
+ ARRAY(struct cassandra_sql_arg) pending_args;
+ cass_int64_t timestamp;
+
+ struct cassandra_result *result;
+};
+
+struct cassandra_sql_prepared_statement {
+ struct sql_prepared_statement prep_stmt;
+
+ /* NULL, until the prepare is asynchronously finished */
+ const CassPrepared *prepared;
+ /* statements waiting for prepare to finish */
+ ARRAY(struct cassandra_sql_statement *) pending_statements;
+ /* an error here will cause the prepare to be retried on the next
+ execution attempt. */
+ char *error;
+
+ bool pending;
+};
+
+extern const struct sql_db driver_cassandra_db;
+extern const struct sql_result driver_cassandra_result;
+
+static struct {
+ CassConsistency consistency;
+ const char *name;
+} cass_consistency_names[] = {
+ { CASS_CONSISTENCY_ANY, "any" },
+ { CASS_CONSISTENCY_ONE, "one" },
+ { CASS_CONSISTENCY_TWO, "two" },
+ { CASS_CONSISTENCY_THREE, "three" },
+ { CASS_CONSISTENCY_QUORUM, "quorum" },
+ { CASS_CONSISTENCY_ALL, "all" },
+ { CASS_CONSISTENCY_LOCAL_QUORUM, "local-quorum" },
+ { CASS_CONSISTENCY_EACH_QUORUM, "each-quorum" },
+ { CASS_CONSISTENCY_SERIAL, "serial" },
+ { CASS_CONSISTENCY_LOCAL_SERIAL, "local-serial" },
+ { CASS_CONSISTENCY_LOCAL_ONE, "local-one" }
+};
+
+static struct {
+ CassLogLevel log_level;
+ const char *name;
+} cass_log_level_names[] = {
+ { CASS_LOG_CRITICAL, "critical" },
+ { CASS_LOG_ERROR, "error" },
+ { CASS_LOG_WARN, "warn" },
+ { CASS_LOG_INFO, "info" },
+ { CASS_LOG_DEBUG, "debug" },
+ { CASS_LOG_TRACE, "trace" }
+};
+
+static struct event_category event_category_cassandra = {
+ .parent = &event_category_sql,
+ .name = "cassandra"
+};
+
+static pthread_t main_thread_id;
+static bool main_thread_id_set;
+
+static void driver_cassandra_prepare_pending(struct cassandra_db *db);
+static void
+prepare_finish_pending_statements(struct cassandra_sql_prepared_statement *prep_stmt);
+static void driver_cassandra_result_send_query(struct cassandra_result *result);
+static void driver_cassandra_send_queries(struct cassandra_db *db);
+static void result_finish(struct cassandra_result *result);
+
+static void log_one_line(const CassLogMessage *message,
+ enum log_type log_type, const char *log_level_str,
+ const char *text, size_t text_len)
+{
+ /* NOTE: We may not be in the main thread. We can't use the
+ standard Dovecot functions that may use data stack. That's why
+ we can't use i_log_type() in here, but have to re-implement the
+ internal logging protocol. Otherwise preserve Cassandra's own
+ logging format. */
+ fprintf(stderr, "\001%c%s %u.%03u %s(%s:%d:%s): %.*s\n",
+ log_type+1, my_pid,
+ (unsigned int)(message->time_ms / 1000),
+ (unsigned int)(message->time_ms % 1000),
+ log_level_str,
+ message->file, message->line, message->function,
+ (int)text_len, text);
+}
+
+static void
+driver_cassandra_log_handler(const CassLogMessage* message,
+ void *data ATTR_UNUSED)
+{
+ enum log_type log_type = LOG_TYPE_ERROR;
+ const char *log_level_str = "";
+
+ switch (message->severity) {
+ case CASS_LOG_DISABLED:
+ case CASS_LOG_LAST_ENTRY:
+ i_unreached();
+ case CASS_LOG_CRITICAL:
+ log_type = LOG_TYPE_PANIC;
+ break;
+ case CASS_LOG_ERROR:
+ log_type = LOG_TYPE_ERROR;
+ break;
+ case CASS_LOG_WARN:
+ log_type = LOG_TYPE_WARNING;
+ break;
+ case CASS_LOG_INFO:
+ log_type = LOG_TYPE_INFO;
+ break;
+ case CASS_LOG_TRACE:
+ log_level_str = "[TRACE] ";
+ /* fall through */
+ case CASS_LOG_DEBUG:
+ log_type = LOG_TYPE_DEBUG;
+ break;
+ }
+
+ /* Log message may contain LFs, so log each line separately. */
+ const char *p, *line = message->message;
+ while ((p = strchr(line, '\n')) != NULL) {
+ log_one_line(message, log_type, log_level_str, line, p - line);
+ line = p+1;
+ }
+ log_one_line(message, log_type, log_level_str, line, strlen(line));
+}
+
+static void driver_cassandra_init_log(void)
+{
+ failure_callback_t *fatal_callback, *error_callback;
+ failure_callback_t *info_callback, *debug_callback;
+
+ i_get_failure_handlers(&fatal_callback, &error_callback,
+ &info_callback, &debug_callback);
+ if (i_failure_handler_is_internal(debug_callback)) {
+ /* Using internal logging protocol. Use it ourself to set log
+ levels correctly. */
+ cass_log_set_callback(driver_cassandra_log_handler, NULL);
+ }
+}
+
+static int consistency_parse(const char *str, CassConsistency *consistency_r)
+{
+ unsigned int i;
+
+ for (i = 0; i < N_ELEMENTS(cass_consistency_names); i++) {
+ if (strcmp(cass_consistency_names[i].name, str) == 0) {
+ *consistency_r = cass_consistency_names[i].consistency;
+ return 0;
+ }
+ }
+ return -1;
+}
+
+static int log_level_parse(const char *str, CassLogLevel *log_level_r)
+{
+ unsigned int i;
+
+ for (i = 0; i < N_ELEMENTS(cass_log_level_names); i++) {
+ if (strcmp(cass_log_level_names[i].name, str) == 0) {
+ *log_level_r = cass_log_level_names[i].log_level;
+ return 0;
+ }
+ }
+ return -1;
+}
+
+static void driver_cassandra_set_state(struct cassandra_db *db,
+ enum sql_db_state state)
+{
+ /* switch back to original ioloop in case the caller wants to
+ add/remove timeouts */
+ if (db->ioloop != NULL)
+ io_loop_set_current(db->orig_ioloop);
+ sql_db_set_state(&db->api, state);
+ if (db->ioloop != NULL)
+ io_loop_set_current(db->ioloop);
+}
+
+static void driver_cassandra_close(struct cassandra_db *db, const char *error)
+{
+ struct cassandra_sql_prepared_statement *prep_stmt;
+ struct cassandra_result *const *resultp;
+
+ io_remove(&db->io_pipe);
+ if (db->fd_pipe[0] != -1) {
+ i_close_fd(&db->fd_pipe[0]);
+ i_close_fd(&db->fd_pipe[1]);
+ }
+ driver_cassandra_set_state(db, SQL_DB_STATE_DISCONNECTED);
+
+ array_foreach_elem(&db->pending_prepares, prep_stmt) {
+ prep_stmt->pending = FALSE;
+ prep_stmt->error = i_strdup(error);
+ prepare_finish_pending_statements(prep_stmt);
+ }
+ array_clear(&db->pending_prepares);
+
+ while (array_count(&db->results) > 0) {
+ resultp = array_front(&db->results);
+ if ((*resultp)->error == NULL)
+ (*resultp)->error = i_strdup(error);
+ result_finish(*resultp);
+ }
+
+ if (db->ioloop != NULL) {
+ /* running a sync query, stop it */
+ io_loop_stop(db->ioloop);
+ }
+}
+
+static void driver_cassandra_log_error(struct cassandra_db *db,
+ CassFuture *future, const char *str)
+{
+ const char *message;
+ size_t size;
+
+ cass_future_error_message(future, &message, &size);
+ e_error(db->api.event, "%s: %.*s", str, (int)size, message);
+}
+
+static struct cassandra_callback *
+cassandra_callback_detach(struct cassandra_db *db, unsigned int id)
+{
+ struct cassandra_callback *cb, *const *cbp;
+
+ /* usually there are only a few callbacks, so don't bother with using
+ a hash table */
+ array_foreach(&db->callbacks, cbp) {
+ cb = *cbp;
+ if (cb->id == id) {
+ array_delete(&db->callbacks,
+ array_foreach_idx(&db->callbacks, cbp), 1);
+ return cb;
+ }
+ }
+ return NULL;
+}
+
+static void cassandra_callback_run(struct cassandra_callback *cb)
+{
+ timeout_remove(&cb->to);
+ cb->callback(cb->future, cb->context);
+ cass_future_free(cb->future);
+ i_free(cb);
+}
+
+static void driver_cassandra_future_callback(CassFuture *future ATTR_UNUSED,
+ void *context)
+{
+ struct cassandra_callback *cb = context;
+
+ if (pthread_equal(pthread_self(), main_thread_id) != 0) {
+ /* called immediately from the main thread. */
+ cassandra_callback_detach(cb->db, cb->id);
+ cb->to = timeout_add_short(0, cassandra_callback_run, cb);
+ return;
+ }
+
+ /* this isn't the main thread - communicate with main thread by
+ writing the callback id to the pipe. note that we must not use
+ almost any dovecot functions here because most of them are using
+ data-stack, which isn't thread-safe. especially don't use
+ i_error() here. */
+ if (write_full(cb->db->fd_pipe[1], &cb->id, sizeof(cb->id)) < 0) {
+ const char *str = t_strdup_printf(
+ "cassandra: write(pipe) failed: %s\n",
+ strerror(errno));
+ (void)write_full(STDERR_FILENO, str, strlen(str));
+ }
+}
+
+static void driver_cassandra_input_id(struct cassandra_db *db, unsigned int id)
+{
+ struct cassandra_callback *cb;
+
+ cb = cassandra_callback_detach(db, id);
+ if (cb == NULL)
+ i_panic("cassandra: Received unknown ID %u", id);
+ cassandra_callback_run(cb);
+}
+
+static void driver_cassandra_input(struct cassandra_db *db)
+{
+ unsigned int ids[1024];
+ ssize_t ret;
+
+ ret = read(db->fd_pipe[0], ids, sizeof(ids));
+ if (ret < 0)
+ e_error(db->api.event, "read(pipe) failed: %m");
+ else if (ret == 0)
+ e_error(db->api.event, "read(pipe) failed: EOF");
+ else if (ret % sizeof(ids[0]) != 0)
+ e_error(db->api.event, "read(pipe) returned wrong amount of data");
+ else {
+ /* success */
+ unsigned int i, count = ret / sizeof(ids[0]);
+
+ for (i = 0; i < count &&
+ db->api.state != SQL_DB_STATE_DISCONNECTED; i++)
+ driver_cassandra_input_id(db, ids[i]);
+ return;
+ }
+ driver_cassandra_close(db, "IPC pipe closed");
+}
+
+static void
+driver_cassandra_set_callback(CassFuture *future, struct cassandra_db *db,
+ driver_cassandra_callback_t *callback,
+ void *context)
+{
+ struct cassandra_callback *cb;
+
+ i_assert(callback != NULL);
+
+ cb = i_new(struct cassandra_callback, 1);
+ cb->future = future;
+ cb->callback = callback;
+ cb->context = context;
+ cb->db = db;
+
+ array_push_back(&db->callbacks, &cb);
+ cb->id = ++db->callback_ids;
+ if (cb->id == 0)
+ cb->id = ++db->callback_ids;
+
+ /* NOTE: The callback may be called immediately by this same thread.
+ This is checked within the callback. It may also be called at any
+ time after this call by another thread. So we must not access "cb"
+ again after this call. */
+ cass_future_set_callback(future, driver_cassandra_future_callback, cb);
+}
+
+static void connect_callback(CassFuture *future, void *context)
+{
+ struct cassandra_db *db = context;
+
+ if (cass_future_error_code(future) != CASS_OK) {
+ driver_cassandra_log_error(db, future,
+ "Couldn't connect to Cassandra");
+ driver_cassandra_close(db, "Couldn't connect to Cassandra");
+ return;
+ }
+ driver_cassandra_set_state(db, SQL_DB_STATE_IDLE);
+ if (db->ioloop != NULL) {
+ /* driver_cassandra_sync_init() waiting for connection to
+ finish */
+ io_loop_stop(db->ioloop);
+ }
+ driver_cassandra_prepare_pending(db);
+ driver_cassandra_send_queries(db);
+}
+
+static int driver_cassandra_connect(struct sql_db *_db)
+{
+ struct cassandra_db *db = (struct cassandra_db *)_db;
+ CassFuture *future;
+
+ i_assert(db->api.state == SQL_DB_STATE_DISCONNECTED);
+
+ if (pipe(db->fd_pipe) < 0) {
+ e_error(_db->event, "pipe() failed: %m");
+ return -1;
+ }
+ db->io_pipe = io_add(db->fd_pipe[0], IO_READ,
+ driver_cassandra_input, db);
+ driver_cassandra_set_state(db, SQL_DB_STATE_CONNECTING);
+
+ future = cass_session_connect_keyspace(db->session, db->cluster,
+ db->keyspace);
+ driver_cassandra_set_callback(future, db, connect_callback, db);
+ return 0;
+}
+
+static void driver_cassandra_disconnect(struct sql_db *_db)
+{
+ struct cassandra_db *db = (struct cassandra_db *)_db;
+
+ driver_cassandra_close(db, "Disconnected");
+}
+
+static const char *
+driver_cassandra_escape_string(struct sql_db *db ATTR_UNUSED,
+ const char *string)
+{
+ string_t *escaped;
+ unsigned int i;
+
+ if (strchr(string, '\'') == NULL)
+ return string;
+ escaped = t_str_new(strlen(string)+10);
+ for (i = 0; string[i] != '\0'; i++) {
+ if (string[i] == '\'')
+ str_append_c(escaped, '\'');
+ str_append_c(escaped, string[i]);
+ }
+ return str_c(escaped);
+}
+
+static int driver_cassandra_parse_connect_string(struct cassandra_db *db,
+ const char *connect_string,
+ const char **error_r)
+{
+ const char *const *args, *key, *value, *error;
+ string_t *hosts = t_str_new(64);
+ bool read_fallback_set = FALSE, write_fallback_set = FALSE;
+ bool delete_fallback_set = FALSE;
+
+ db->log_level = CASS_LOG_WARN;
+ db->read_consistency = CASS_CONSISTENCY_LOCAL_QUORUM;
+ db->write_consistency = CASS_CONSISTENCY_LOCAL_QUORUM;
+ db->delete_consistency = CASS_CONSISTENCY_LOCAL_QUORUM;
+ db->connect_timeout_msecs = SQL_CONNECT_TIMEOUT_SECS*1000;
+ db->request_timeout_msecs = SQL_QUERY_TIMEOUT_SECS*1000;
+ db->warn_timeout_msecs = CASS_QUERY_DEFAULT_WARN_TIMEOUT_MSECS;
+
+ args = t_strsplit_spaces(connect_string, " ");
+ for (; *args != NULL; args++) {
+ value = strchr(*args, '=');
+ if (value == NULL) {
+ *error_r = t_strdup_printf(
+ "Missing value in connect string: %s", *args);
+ return -1;
+ }
+ key = t_strdup_until(*args, value++);
+
+ if (str_begins(key, "ssl_"))
+ db->init_ssl = TRUE;
+
+ if (strcmp(key, "host") == 0) {
+ if (str_len(hosts) > 0)
+ str_append_c(hosts, ',');
+ str_append(hosts, value);
+ } else if (strcmp(key, "port") == 0) {
+ if (net_str2port(value, &db->port) < 0) {
+ *error_r = t_strdup_printf(
+ "Invalid port: %s", value);
+ return -1;
+ }
+ } else if (strcmp(key, "dbname") == 0 ||
+ strcmp(key, "keyspace") == 0) {
+ i_free(db->keyspace);
+ db->keyspace = i_strdup(value);
+ } else if (strcmp(key, "user") == 0) {
+ i_free(db->user);
+ db->user = i_strdup(value);
+ } else if (strcmp(key, "password") == 0) {
+ i_free(db->password);
+ db->password = i_strdup(value);
+ } else if (strcmp(key, "read_consistency") == 0) {
+ if (consistency_parse(value, &db->read_consistency) < 0) {
+ *error_r = t_strdup_printf(
+ "Unknown read_consistency: %s", value);
+ return -1;
+ }
+ } else if (strcmp(key, "read_fallback_consistency") == 0) {
+ if (consistency_parse(value, &db->read_fallback_consistency) < 0) {
+ *error_r = t_strdup_printf(
+ "Unknown read_fallback_consistency: %s", value);
+ return -1;
+ }
+ read_fallback_set = TRUE;
+ } else if (strcmp(key, "write_consistency") == 0) {
+ if (consistency_parse(value,
+ &db->write_consistency) < 0) {
+ *error_r = t_strdup_printf(
+ "Unknown write_consistency: %s", value);
+ return -1;
+ }
+ } else if (strcmp(key, "write_fallback_consistency") == 0) {
+ if (consistency_parse(value,
+ &db->write_fallback_consistency) < 0) {
+ *error_r = t_strdup_printf(
+ "Unknown write_fallback_consistency: %s",
+ value);
+ return -1;
+ }
+ write_fallback_set = TRUE;
+ } else if (strcmp(key, "delete_consistency") == 0) {
+ if (consistency_parse(value,
+ &db->delete_consistency) < 0) {
+ *error_r = t_strdup_printf(
+ "Unknown delete_consistency: %s", value);
+ return -1;
+ }
+ } else if (strcmp(key, "delete_fallback_consistency") == 0) {
+ if (consistency_parse(value,
+ &db->delete_fallback_consistency) < 0) {
+ *error_r = t_strdup_printf(
+ "Unknown delete_fallback_consistency: %s",
+ value);
+ return -1;
+ }
+ delete_fallback_set = TRUE;
+ } else if (strcmp(key, "log_level") == 0) {
+ if (log_level_parse(value, &db->log_level) < 0) {
+ *error_r = t_strdup_printf(
+ "Unknown log_level: %s", value);
+ return -1;
+ }
+ } else if (strcmp(key, "debug_queries") == 0) {
+ db->debug_queries = TRUE;
+ } else if (strcmp(key, "latency_aware_routing") == 0) {
+ db->latency_aware_routing = TRUE;
+ } else if (strcmp(key, "version") == 0) {
+ if (str_to_uint(value, &db->protocol_version) < 0) {
+ *error_r = t_strdup_printf(
+ "Invalid version: %s", value);
+ return -1;
+ }
+ } else if (strcmp(key, "num_threads") == 0) {
+ if (str_to_uint(value, &db->num_threads) < 0) {
+ *error_r = t_strdup_printf(
+ "Invalid num_threads: %s", value);
+ return -1;
+ }
+ } else if (strcmp(key, "heartbeat_interval") == 0) {
+ if (settings_get_time(value, &db->heartbeat_interval_secs,
+ &error) < 0) {
+ *error_r = t_strdup_printf(
+ "Invalid heartbeat_interval '%s': %s",
+ value, error);
+ return -1;
+ }
+ } else if (strcmp(key, "idle_timeout") == 0) {
+ if (settings_get_time(value, &db->idle_timeout_secs,
+ &error) < 0) {
+ *error_r = t_strdup_printf(
+ "Invalid idle_timeout '%s': %s",
+ value, error);
+ return -1;
+ }
+ } else if (strcmp(key, "connect_timeout") == 0) {
+ if (settings_get_time_msecs(value,
+ &db->connect_timeout_msecs,
+ &error) < 0) {
+ *error_r = t_strdup_printf(
+ "Invalid connect_timeout '%s': %s",
+ value, error);
+ return -1;
+ }
+ } else if (strcmp(key, "request_timeout") == 0) {
+ if (settings_get_time_msecs(value,
+ &db->request_timeout_msecs,
+ &error) < 0) {
+ *error_r = t_strdup_printf(
+ "Invalid request_timeout '%s': %s",
+ value, error);
+ return -1;
+ }
+ } else if (strcmp(key, "warn_timeout") == 0) {
+ if (settings_get_time_msecs(value,
+ &db->warn_timeout_msecs,
+ &error) < 0) {
+ *error_r = t_strdup_printf(
+ "Invalid warn_timeout '%s': %s",
+ value, error);
+ return -1;
+ }
+ } else if (strcmp(key, "metrics") == 0) {
+ i_free(db->metrics_path);
+ db->metrics_path = i_strdup(value);
+ } else if (strcmp(key, "execution_retry_interval") == 0) {
+ if (settings_get_time_msecs(value,
+ &db->execution_retry_interval_msecs,
+ &error) < 0) {
+ *error_r = t_strdup_printf(
+ "Invalid execution_retry_interval '%s': %s",
+ value, error);
+ return -1;
+ }
+#ifndef HAVE_CASSANDRA_SPECULATIVE_POLICY
+ *error_r = t_strdup_printf(
+ "This cassandra version does not support execution_retry_interval");
+ return -1;
+#endif
+ } else if (strcmp(key, "execution_retry_times") == 0) {
+ if (str_to_uint(value, &db->execution_retry_times) < 0) {
+ *error_r = t_strdup_printf(
+ "Invalid execution_retry_times %s",
+ value);
+ return -1;
+ }
+#ifndef HAVE_CASSANDRA_SPECULATIVE_POLICY
+ *error_r = t_strdup_printf(
+ "This cassandra version does not support execution_retry_times");
+ return -1;
+#endif
+ } else if (strcmp(key, "page_size") == 0) {
+ if (str_to_uint(value, &db->page_size) < 0) {
+ *error_r = t_strdup_printf(
+ "Invalid page_size: %s",
+ value);
+ return -1;
+ }
+ } else if (strcmp(key, "ssl_ca") == 0) {
+ db->ssl_ca_file = i_strdup(value);
+ } else if (strcmp(key, "ssl_cert_file") == 0) {
+ db->ssl_cert_file = i_strdup(value);
+ } else if (strcmp(key, "ssl_private_key_file") == 0) {
+ db->ssl_private_key_file = i_strdup(value);
+ } else if (strcmp(key, "ssl_private_key_password") == 0) {
+ db->ssl_private_key_password = i_strdup(value);
+ } else if (strcmp(key, "ssl_verify") == 0) {
+ if (strcmp(value, "none") == 0) {
+ db->ssl_verify_flags = CASS_SSL_VERIFY_NONE;
+ } else if (strcmp(value, "cert") == 0) {
+ db->ssl_verify_flags = CASS_SSL_VERIFY_PEER_CERT;
+ } else if (strcmp(value, "cert-ip") == 0) {
+ db->ssl_verify_flags =
+ CASS_SSL_VERIFY_PEER_CERT |
+ CASS_SSL_VERIFY_PEER_IDENTITY;
+#if HAVE_DECL_CASS_SSL_VERIFY_PEER_IDENTITY_DNS == 1
+ } else if (strcmp(value, "cert-dns") == 0) {
+ db->ssl_verify_flags =
+ CASS_SSL_VERIFY_PEER_CERT |
+ CASS_SSL_VERIFY_PEER_IDENTITY_DNS;
+#endif
+ } else {
+ *error_r = t_strdup_printf(
+ "Unsupported ssl_verify flags: '%s'",
+ value);
+ return -1;
+ }
+ } else {
+ *error_r = t_strdup_printf(
+ "Unknown connect string: %s", key);
+ return -1;
+ }
+ }
+
+ if (!read_fallback_set)
+ db->read_fallback_consistency = db->read_consistency;
+ if (!write_fallback_set)
+ db->write_fallback_consistency = db->write_consistency;
+ if (!delete_fallback_set)
+ db->delete_fallback_consistency = db->delete_consistency;
+
+ if (str_len(hosts) == 0) {
+ *error_r = t_strdup_printf("No hosts given in connect string");
+ return -1;
+ }
+ if (db->keyspace == NULL) {
+ *error_r = t_strdup_printf("No dbname given in connect string");
+ return -1;
+ }
+
+ if ((db->ssl_cert_file != NULL && db->ssl_private_key_file == NULL) ||
+ (db->ssl_cert_file == NULL && db->ssl_private_key_file != NULL)) {
+ *error_r = "ssl_cert_file and ssl_private_key_file need to be both set";
+ return -1;
+ }
+
+ db->hosts = i_strdup(str_c(hosts));
+ return 0;
+}
+
+static void
+driver_cassandra_get_metrics_json(struct cassandra_db *db, string_t *dest)
+{
+#define ADD_UINT64(_struct, _field) \
+ str_printfa(dest, "\""#_field"\": %llu,", \
+ (unsigned long long)metrics._struct._field);
+#define ADD_DOUBLE(_struct, _field) \
+ str_printfa(dest, "\""#_field"\": %02lf,", metrics._struct._field);
+ CassMetrics metrics;
+
+ cass_session_get_metrics(db->session, &metrics);
+ str_append(dest, "{ \"requests\": {");
+ ADD_UINT64(requests, min);
+ ADD_UINT64(requests, max);
+ ADD_UINT64(requests, mean);
+ ADD_UINT64(requests, stddev);
+ ADD_UINT64(requests, median);
+ ADD_UINT64(requests, percentile_75th);
+ ADD_UINT64(requests, percentile_95th);
+ ADD_UINT64(requests, percentile_98th);
+ ADD_UINT64(requests, percentile_99th);
+ ADD_UINT64(requests, percentile_999th);
+ ADD_DOUBLE(requests, mean_rate);
+ ADD_DOUBLE(requests, one_minute_rate);
+ ADD_DOUBLE(requests, five_minute_rate);
+ ADD_DOUBLE(requests, fifteen_minute_rate);
+ str_truncate(dest, str_len(dest)-1);
+
+ str_append(dest, "}, \"stats\": {");
+ ADD_UINT64(stats, total_connections);
+ ADD_UINT64(stats, available_connections);
+ ADD_UINT64(stats, exceeded_pending_requests_water_mark);
+ ADD_UINT64(stats, exceeded_write_bytes_water_mark);
+ str_truncate(dest, str_len(dest)-1);
+
+ str_append(dest, "}, \"errors\": {");
+ ADD_UINT64(errors, connection_timeouts);
+ ADD_UINT64(errors, pending_request_timeouts);
+ ADD_UINT64(errors, request_timeouts);
+ str_truncate(dest, str_len(dest)-1);
+
+ str_append(dest, "}, \"queries\": {");
+ for (unsigned int i = 0; i < CASSANDRA_COUNTER_COUNT; i++) {
+ str_printfa(dest, "\"%s\": %"PRIu64",", counter_names[i],
+ db->counters[i]);
+ }
+ str_truncate(dest, str_len(dest)-1);
+ str_append(dest, "}}");
+}
+
+static void driver_cassandra_metrics_write(struct cassandra_db *db)
+{
+ struct var_expand_table tab[] = {
+ { '\0', NULL, NULL }
+ };
+ string_t *path = t_str_new(64);
+ string_t *data;
+ const char *error;
+ int fd;
+
+ if (var_expand(path, db->metrics_path, tab, &error) <= 0) {
+ e_error(db->api.event, "Failed to expand metrics_path=%s: %s",
+ db->metrics_path, error);
+ return;
+ }
+
+ fd = open(str_c(path), O_WRONLY | O_CREAT | O_TRUNC | O_NONBLOCK, 0600);
+ if (fd == -1) {
+ e_error(db->api.event, "creat(%s) failed: %m", str_c(path));
+ return;
+ }
+ data = t_str_new(1024);
+ driver_cassandra_get_metrics_json(db, data);
+ if (write_full(fd, str_data(data), str_len(data)) < 0)
+ e_error(db->api.event, "write(%s) failed: %m", str_c(path));
+ i_close_fd(&fd);
+}
+
+static void driver_cassandra_free(struct cassandra_db **_db)
+{
+ struct cassandra_db *db = *_db;
+ *_db = NULL;
+
+ event_unref(&db->api.event);
+ i_free(db->metrics_path);
+ i_free(db->hosts);
+ i_free(db->error);
+ i_free(db->keyspace);
+ i_free(db->user);
+ i_free(db->password);
+ i_free(db->ssl_ca_file);
+ i_free(db->ssl_cert_file);
+ i_free(db->ssl_private_key_file);
+ i_free_and_null(db->ssl_private_key_password);
+ array_free(&db->api.module_contexts);
+ if (db->ssl != NULL)
+ cass_ssl_free(db->ssl);
+ i_free(db);
+}
+
+static int driver_cassandra_init_ssl(struct cassandra_db *db, const char **error_r)
+{
+ buffer_t *buf = t_buffer_create(512);
+ CassError c_err;
+
+ db->ssl = cass_ssl_new();
+ i_assert(db->ssl != NULL);
+
+ if (db->ssl_ca_file != NULL) {
+ if (buffer_append_full_file(buf, db->ssl_ca_file, SIZE_MAX,
+ error_r) < 0)
+ return -1;
+ if ((c_err = cass_ssl_add_trusted_cert(db->ssl, str_c(buf))) != CASS_OK) {
+ *error_r = cass_error_desc(c_err);
+ return -1;
+ }
+ }
+
+ if (db->ssl_private_key_file != NULL && db->ssl_cert_file != NULL) {
+ buffer_set_used_size(buf, 0);
+ if (buffer_append_full_file(buf, db->ssl_private_key_file,
+ SIZE_MAX, error_r) < 0)
+ return -1;
+ c_err = cass_ssl_set_private_key(db->ssl, str_c(buf),
+ db->ssl_private_key_password);
+ safe_memset(buffer_get_modifiable_data(buf, NULL), 0, buf->used);
+ if (c_err != CASS_OK) {
+ *error_r = cass_error_desc(c_err);
+ return -1;
+ }
+
+ buffer_set_used_size(buf, 0);
+ if (buffer_append_full_file(buf, db->ssl_cert_file, SIZE_MAX, error_r) < 0)
+ return -1;
+ if ((c_err = cass_ssl_set_cert(db->ssl, str_c(buf))) != CASS_OK) {
+ *error_r = cass_error_desc(c_err);
+ return -1;
+ }
+ }
+
+ cass_ssl_set_verify_flags(db->ssl, db->ssl_verify_flags);
+
+ return 0;
+}
+
+static int driver_cassandra_init_full_v(const struct sql_settings *set,
+ struct sql_db **db_r,
+ const char **error_r)
+{
+ struct cassandra_db *db;
+ int ret;
+
+ db = i_new(struct cassandra_db, 1);
+ db->api = driver_cassandra_db;
+ db->fd_pipe[0] = db->fd_pipe[1] = -1;
+ db->api.event = event_create(set->event_parent);
+ event_add_category(db->api.event, &event_category_cassandra);
+ event_set_append_log_prefix(db->api.event, "cassandra: ");
+
+ T_BEGIN {
+ ret = driver_cassandra_parse_connect_string(db,
+ set->connect_string, error_r);
+ } T_END_PASS_STR_IF(ret < 0, error_r);
+
+ if (ret < 0) {
+ driver_cassandra_free(&db);
+ return -1;
+ }
+
+ if (db->init_ssl && driver_cassandra_init_ssl(db, error_r) < 0) {
+ driver_cassandra_free(&db);
+ return -1;
+ }
+
+ driver_cassandra_init_log();
+ cass_log_set_level(db->log_level);
+ if (db->log_level >= CASS_LOG_DEBUG)
+ event_set_forced_debug(db->api.event, TRUE);
+
+ if (db->protocol_version > 0 && db->protocol_version < 4) {
+ /* binding with column indexes requires v4 */
+ db->api.v.prepared_statement_init = NULL;
+ db->api.v.prepared_statement_deinit = NULL;
+ db->api.v.statement_init_prepared = NULL;
+ }
+
+ db->timestamp_gen = cass_timestamp_gen_monotonic_new();
+ db->cluster = cass_cluster_new();
+
+#ifdef HAVE_CASS_CLUSTER_SET_USE_HOSTNAME_RESOLUTION
+ if ((db->ssl_verify_flags & CASS_SSL_VERIFY_PEER_IDENTITY_DNS) != 0) {
+ CassError c_err;
+ if ((c_err = cass_cluster_set_use_hostname_resolution(
+ db->cluster, cass_true)) != CASS_OK) {
+ *error_r = cass_error_desc(c_err);
+ driver_cassandra_free(&db);
+ return -1;
+ }
+ }
+#endif
+ cass_cluster_set_ssl(db->cluster, db->ssl);
+ cass_cluster_set_timestamp_gen(db->cluster, db->timestamp_gen);
+ cass_cluster_set_connect_timeout(db->cluster, db->connect_timeout_msecs);
+ cass_cluster_set_request_timeout(db->cluster, db->request_timeout_msecs);
+ cass_cluster_set_contact_points(db->cluster, db->hosts);
+ if (db->user != NULL && db->password != NULL)
+ cass_cluster_set_credentials(db->cluster, db->user, db->password);
+ if (db->port != 0)
+ cass_cluster_set_port(db->cluster, db->port);
+ if (db->protocol_version != 0)
+ cass_cluster_set_protocol_version(db->cluster, db->protocol_version);
+ if (db->num_threads != 0)
+ cass_cluster_set_num_threads_io(db->cluster, db->num_threads);
+ if (db->latency_aware_routing)
+ cass_cluster_set_latency_aware_routing(db->cluster, cass_true);
+ if (db->heartbeat_interval_secs != 0)
+ cass_cluster_set_connection_heartbeat_interval(db->cluster,
+ db->heartbeat_interval_secs);
+ if (db->idle_timeout_secs != 0)
+ cass_cluster_set_connection_idle_timeout(db->cluster,
+ db->idle_timeout_secs);
+#ifdef HAVE_CASSANDRA_SPECULATIVE_POLICY
+ if (db->execution_retry_times > 0 && db->execution_retry_interval_msecs > 0)
+ cass_cluster_set_constant_speculative_execution_policy(
+ db->cluster, db->execution_retry_interval_msecs,
+ db->execution_retry_times);
+#endif
+ if (db->ssl != NULL) {
+ e_debug(db->api.event, "Enabling TLS for cluster");
+ cass_cluster_set_ssl(db->cluster, db->ssl);
+ }
+ db->session = cass_session_new();
+ if (db->metrics_path != NULL)
+ db->to_metrics = timeout_add(1000, driver_cassandra_metrics_write,
+ db);
+ i_array_init(&db->results, 16);
+ i_array_init(&db->callbacks, 16);
+ i_array_init(&db->pending_prepares, 16);
+ if (!main_thread_id_set) {
+ main_thread_id = pthread_self();
+ main_thread_id_set = TRUE;
+ }
+
+ *db_r = &db->api;
+ return 0;
+}
+
+static void driver_cassandra_deinit_v(struct sql_db *_db)
+{
+ struct cassandra_db *db = (struct cassandra_db *)_db;
+
+ driver_cassandra_close(db, "Deinitialized");
+
+ i_assert(array_count(&db->callbacks) == 0);
+ array_free(&db->callbacks);
+ i_assert(array_count(&db->results) == 0);
+ array_free(&db->results);
+ i_assert(array_count(&db->pending_prepares) == 0);
+ array_free(&db->pending_prepares);
+
+ cass_session_free(db->session);
+ cass_cluster_free(db->cluster);
+ cass_timestamp_gen_free(db->timestamp_gen);
+ timeout_remove(&db->to_metrics);
+ sql_connection_log_finished(_db);
+ driver_cassandra_free(&db);
+}
+
+static void driver_cassandra_result_unlink(struct cassandra_db *db,
+ struct cassandra_result *result)
+{
+ struct cassandra_result *const *results;
+ unsigned int i, count;
+
+ results = array_get(&db->results, &count);
+ for (i = 0; i < count; i++) {
+ if (results[i] == result) {
+ array_delete(&db->results, i, 1);
+ return;
+ }
+ }
+ i_unreached();
+}
+
+static void driver_cassandra_log_result(struct cassandra_result *result,
+ bool all_pages, long long reply_usecs)
+{
+ struct cassandra_db *db = (struct cassandra_db *)result->api.db;
+ struct timeval now;
+ unsigned int row_count;
+
+ i_gettimeofday(&now);
+
+ string_t *str = t_str_new(128);
+ str_printfa(str, "Finished %squery '%s' (",
+ result->is_prepared ? "prepared " : "", result->query);
+ if (result->timestamp != 0)
+ str_printfa(str, "timestamp=%"PRId64", ", result->timestamp);
+ if (all_pages) {
+ str_printfa(str, "%u pages in total, ", result->page_num);
+ row_count = result->total_row_count;
+ } else {
+ if (result->page_num > 0 || result->paging_continues)
+ str_printfa(str, "page %u, ", result->page_num);
+ row_count = result->row_count;
+ }
+ str_printfa(str, "%u rows, %lld+%lld us): %s", row_count, reply_usecs,
+ timeval_diff_usecs(&now, &result->finish_time),
+ result->error != NULL ? result->error : "success");
+
+ struct event_passthrough *e =
+ sql_query_finished_event(&db->api, result->api.event,
+ result->query, result->error == NULL,
+ NULL);
+ if (result->error != NULL)
+ e->add_str("error", result->error);
+
+ struct event *event = e->event();
+ if (db->debug_queries)
+ event_set_forced_debug(event, TRUE);
+ if (reply_usecs/1000 >= db->warn_timeout_msecs) {
+ db->counters[CASSANDRA_COUNTER_TYPE_QUERY_SLOW]++;
+ e_warning(event, "%s", str_c(str));
+ } else {
+ e_debug(event, "%s", str_c(str));
+ }
+}
+
+static void driver_cassandra_result_free(struct sql_result *_result)
+{
+ struct cassandra_db *db = (struct cassandra_db *)_result->db;
+ struct cassandra_result *result = (struct cassandra_result *)_result;
+ long long reply_usecs;
+
+ i_assert(!result->api.callback);
+ i_assert(result->callback == NULL);
+
+ if (_result == db->sync_result)
+ db->sync_result = NULL;
+
+ reply_usecs = timeval_diff_usecs(&result->finish_time,
+ &result->start_time);
+ driver_cassandra_log_result(result, FALSE, reply_usecs);
+
+ if (result->page_num > 0 && !result->paging_continues) {
+ /* Multi-page query finishes now. Log a debug/warning summary
+ message about it separate from the per-page messages. */
+ reply_usecs = timeval_diff_usecs(&result->finish_time,
+ &result->page0_start_time);
+ driver_cassandra_log_result(result, TRUE, reply_usecs);
+ }
+
+ if (result->result != NULL)
+ cass_result_free(result->result);
+ if (result->iterator != NULL)
+ cass_iterator_free(result->iterator);
+ if (result->statement != NULL)
+ cass_statement_free(result->statement);
+ pool_unref(&result->row_pool);
+ event_unref(&result->api.event);
+ i_free(result->query);
+ i_free(result->error);
+ i_free(result);
+}
+
+static void result_finish(struct cassandra_result *result)
+{
+ struct cassandra_db *db = (struct cassandra_db *)result->api.db;
+ bool free_result = TRUE;
+
+ result->finished = TRUE;
+ result->finish_time = ioloop_timeval;
+ driver_cassandra_result_unlink(db, result);
+
+ i_assert((result->error != NULL) == (result->iterator == NULL));
+
+ result->api.callback = TRUE;
+ T_BEGIN {
+ result->callback(&result->api, result->context);
+ } T_END;
+ result->api.callback = FALSE;
+
+ free_result = db->sync_result != &result->api;
+ if (db->ioloop != NULL)
+ io_loop_stop(db->ioloop);
+
+ i_assert(!free_result || result->api.refcount > 0);
+ result->callback = NULL;
+ if (free_result)
+ sql_result_unref(&result->api);
+}
+
+static void query_resend_with_fallback(struct cassandra_result *result)
+{
+ struct cassandra_db *db = (struct cassandra_db *)result->api.db;
+ time_t last_warning =
+ ioloop_time - db->last_fallback_warning[result->query_type];
+
+ if (last_warning >= CASSANDRA_FALLBACK_WARN_INTERVAL_SECS) {
+ e_warning(db->api.event,
+ "%s - retrying future %s queries with consistency %s (instead of %s)",
+ result->error, cassandra_query_type_names[result->query_type],
+ cass_consistency_string(result->fallback_consistency),
+ cass_consistency_string(result->consistency));
+ db->last_fallback_warning[result->query_type] = ioloop_time;
+ }
+ i_free_and_null(result->error);
+ db->fallback_failures[result->query_type]++;
+
+ result->consistency = result->fallback_consistency;
+ driver_cassandra_result_send_query(result);
+}
+
+static void counters_inc_error(struct cassandra_db *db, CassError error)
+{
+ switch (error) {
+ case CASS_ERROR_LIB_NO_HOSTS_AVAILABLE:
+ db->counters[CASSANDRA_COUNTER_TYPE_QUERY_RECV_ERR_NO_HOSTS]++;
+ break;
+ case CASS_ERROR_LIB_REQUEST_QUEUE_FULL:
+ db->counters[CASSANDRA_COUNTER_TYPE_QUERY_RECV_ERR_QUEUE_FULL]++;
+ break;
+ case CASS_ERROR_LIB_REQUEST_TIMED_OUT:
+ db->counters[CASSANDRA_COUNTER_TYPE_QUERY_RECV_ERR_CLIENT_TIMEOUT]++;
+ break;
+ case CASS_ERROR_SERVER_WRITE_TIMEOUT:
+ db->counters[CASSANDRA_COUNTER_TYPE_QUERY_RECV_ERR_SERVER_TIMEOUT]++;
+ break;
+ case CASS_ERROR_SERVER_UNAVAILABLE:
+ db->counters[CASSANDRA_COUNTER_TYPE_QUERY_RECV_ERR_SERVER_UNAVAILABLE]++;
+ break;
+ default:
+ db->counters[CASSANDRA_COUNTER_TYPE_QUERY_RECV_ERR_OTHER]++;
+ break;
+ }
+}
+
+static bool query_error_want_fallback(CassError error)
+{
+ switch (error) {
+ case CASS_ERROR_LIB_WRITE_ERROR:
+ case CASS_ERROR_LIB_REQUEST_TIMED_OUT:
+ /* Communication problems on client side. Maybe it will work
+ with fallback consistency? */
+ return TRUE;
+ case CASS_ERROR_LIB_NO_HOSTS_AVAILABLE:
+ /* The client library couldn't connect to enough Cassandra
+ nodes. The error message text is the same as for
+ CASS_ERROR_SERVER_UNAVAILABLE. */
+ return TRUE;
+ case CASS_ERROR_SERVER_SERVER_ERROR:
+ case CASS_ERROR_SERVER_OVERLOADED:
+ case CASS_ERROR_SERVER_IS_BOOTSTRAPPING:
+ case CASS_ERROR_SERVER_READ_TIMEOUT:
+ case CASS_ERROR_SERVER_READ_FAILURE:
+ case CASS_ERROR_SERVER_WRITE_FAILURE:
+ /* Servers are having trouble. Maybe with fallback consistency
+ we can reach non-troubled servers? */
+ return TRUE;
+ case CASS_ERROR_SERVER_UNAVAILABLE:
+ /* Cassandra server knows that there aren't enough nodes
+ available. "All hosts in current policy attempted and were
+ either unavailable or failed". */
+ return TRUE;
+ case CASS_ERROR_SERVER_WRITE_TIMEOUT:
+ /* Cassandra server couldn't reach all the needed nodes.
+ This may be because it hasn't yet detected that the servers
+ are down, or because the servers are just too busy. We'll
+ try the fallback consistency to avoid unnecessary temporary
+ errors. */
+ return TRUE;
+ default:
+ return FALSE;
+ }
+}
+
+static enum sql_result_error_type
+driver_cassandra_error_is_uncertain(CassError error)
+{
+ switch (error) {
+ case CASS_ERROR_SERVER_WRITE_FAILURE:
+ /* This happens when some of the replicas that were contacted
+ * by the coordinator replied with an error. */
+ case CASS_ERROR_SERVER_WRITE_TIMEOUT:
+ /* A Cassandra timeout during a write query. */
+ case CASS_ERROR_SERVER_UNAVAILABLE:
+ /* The coordinator knows there are not enough replicas alive
+ * to perform a query with the requested consistency level. */
+ case CASS_ERROR_LIB_REQUEST_TIMED_OUT:
+ /* A request sent from the driver has timed out. */
+ case CASS_ERROR_LIB_WRITE_ERROR:
+ /* A write error occured. */
+ return SQL_RESULT_ERROR_TYPE_WRITE_UNCERTAIN;
+ default:
+ return SQL_RESULT_ERROR_TYPE_UNKNOWN;
+ }
+}
+
+static void query_callback(CassFuture *future, void *context)
+{
+ struct cassandra_result *result = context;
+ struct cassandra_db *db = (struct cassandra_db *)result->api.db;
+ CassError error = cass_future_error_code(future);
+
+ if (error != CASS_OK) {
+ const char *errmsg;
+ size_t errsize;
+ int msecs;
+
+ cass_future_error_message(future, &errmsg, &errsize);
+ i_free(result->error);
+
+ msecs = timeval_diff_msecs(&ioloop_timeval, &result->start_time);
+ counters_inc_error(db, error);
+ /* Timeouts bring uncertainty whether the query succeeded or
+ not. Also _SERVER_UNAVAILABLE could have actually written
+ enough copies of the data for the query to succeed. */
+ result->api.error_type = driver_cassandra_error_is_uncertain(error);
+ result->error = i_strdup_printf(
+ "Query '%s' failed: %.*s (in %u.%03u secs%s)",
+ result->query, (int)errsize, errmsg, msecs/1000, msecs%1000,
+ result->page_num == 0 ?
+ "" :
+ t_strdup_printf(", page %u", result->page_num));
+
+ if (query_error_want_fallback(error) &&
+ result->fallback_consistency != result->consistency) {
+ /* retry with fallback consistency */
+ query_resend_with_fallback(result);
+ return;
+ }
+ result_finish(result);
+ return;
+ }
+ db->counters[CASSANDRA_COUNTER_TYPE_QUERY_RECV_OK]++;
+
+ if (result->fallback_consistency != result->consistency) {
+ /* non-fallback query finished successfully. if there had been
+ any fallbacks, reset them. */
+ db->fallback_failures[result->query_type] = 0;
+ }
+
+ result->result = cass_future_get_result(future);
+ result->iterator = cass_iterator_from_result(result->result);
+ result_finish(result);
+}
+
+static void driver_cassandra_init_statement(struct cassandra_result *result)
+{
+ struct cassandra_db *db = (struct cassandra_db *)result->api.db;
+
+ cass_statement_set_consistency(result->statement, result->consistency);
+
+#ifdef HAVE_CASSANDRA_SPECULATIVE_POLICY
+ cass_statement_set_is_idempotent(result->statement, cass_true);
+#endif
+ if (db->page_size > 0)
+ cass_statement_set_paging_size(result->statement, db->page_size);
+}
+
+static void driver_cassandra_result_send_query(struct cassandra_result *result)
+{
+ struct cassandra_db *db = (struct cassandra_db *)result->api.db;
+ CassFuture *future;
+
+ i_assert(result->statement != NULL);
+
+ db->counters[CASSANDRA_COUNTER_TYPE_QUERY_SENT]++;
+ if (result->query_type != CASSANDRA_QUERY_TYPE_READ_MORE)
+ driver_cassandra_init_statement(result);
+
+ future = cass_session_execute(db->session, result->statement);
+ driver_cassandra_set_callback(future, db, query_callback, result);
+}
+
+static bool
+driver_cassandra_want_fallback_query(struct cassandra_result *result)
+{
+ struct cassandra_db *db = (struct cassandra_db *)result->api.db;
+ unsigned int failure_count = db->fallback_failures[result->query_type];
+ unsigned int i, msecs = CASSANDRA_FALLBACK_FIRST_RETRY_MSECS;
+ struct timeval tv;
+
+ if (failure_count == 0)
+ return FALSE;
+ /* double the retries every time. */
+ for (i = 1; i < failure_count; i++) {
+ msecs *= 2;
+ if (msecs >= CASSANDRA_FALLBACK_MAX_RETRY_MSECS) {
+ msecs = CASSANDRA_FALLBACK_MAX_RETRY_MSECS;
+ break;
+ }
+ }
+ /* If last primary query sent timestamp + msecs is older than current
+ time, we need to retry the primary query. Note that this practically
+ prevents multiple primary queries from being attempted
+ simultaneously, because the caller updates primary_query_last_sent
+ immediately when returning.
+
+ The only time when multiple primary queries can be running in
+ parallel is when the earlier query is being slow and hasn't finished
+ early enough. This could even be a wanted feature, since while the
+ first query might have to wait for a timeout, Cassandra could have
+ been fixed in the meantime and the second query finishes
+ successfully. */
+ tv = db->primary_query_last_sent[result->query_type];
+ timeval_add_msecs(&tv, msecs);
+ return timeval_cmp(&ioloop_timeval, &tv) < 0;
+}
+
+static int driver_cassandra_send_query(struct cassandra_result *result)
+{
+ struct cassandra_db *db = (struct cassandra_db *)result->api.db;
+ int ret;
+
+ if (!SQL_DB_IS_READY(&db->api)) {
+ if ((ret = sql_connect(&db->api)) <= 0) {
+ if (ret < 0)
+ driver_cassandra_close(db,
+ "Couldn't connect to Cassandra");
+ return ret;
+ }
+ }
+
+ if (result->page0_start_time.tv_sec == 0)
+ result->page0_start_time = ioloop_timeval;
+ result->start_time = ioloop_timeval;
+ result->row_pool = pool_alloconly_create("cassandra result", 512);
+ switch (result->query_type) {
+ case CASSANDRA_QUERY_TYPE_READ:
+ result->consistency = db->read_consistency;
+ result->fallback_consistency = db->read_fallback_consistency;
+ break;
+ case CASSANDRA_QUERY_TYPE_READ_MORE:
+ /* consistency is already set and we don't want to fallback
+ at this point anymore. */
+ result->fallback_consistency = result->consistency;
+ break;
+ case CASSANDRA_QUERY_TYPE_WRITE:
+ result->consistency = db->write_consistency;
+ result->fallback_consistency = db->write_fallback_consistency;
+ break;
+ case CASSANDRA_QUERY_TYPE_DELETE:
+ result->consistency = db->delete_consistency;
+ result->fallback_consistency = db->delete_fallback_consistency;
+ break;
+ case CASSANDRA_QUERY_TYPE_COUNT:
+ i_unreached();
+ }
+
+ if (driver_cassandra_want_fallback_query(result))
+ result->consistency = result->fallback_consistency;
+ else
+ db->primary_query_last_sent[result->query_type] = ioloop_timeval;
+
+ driver_cassandra_result_send_query(result);
+ result->query_sent = TRUE;
+ return 1;
+}
+
+static void driver_cassandra_send_queries(struct cassandra_db *db)
+{
+ struct cassandra_result *const *results;
+ unsigned int i, count;
+
+ results = array_get(&db->results, &count);
+ for (i = 0; i < count; i++) {
+ if (!results[i]->query_sent && results[i]->statement != NULL) {
+ if (driver_cassandra_send_query(results[i]) <= 0)
+ break;
+ }
+ }
+}
+
+static void exec_callback(struct sql_result *_result ATTR_UNUSED,
+ void *context ATTR_UNUSED)
+{
+}
+
+static struct cassandra_result *
+driver_cassandra_query_init(struct cassandra_db *db, const char *query,
+ enum cassandra_query_type query_type,
+ bool is_prepared,
+ sql_query_callback_t *callback, void *context)
+{
+ struct cassandra_result *result;
+
+ result = i_new(struct cassandra_result, 1);
+ result->api = driver_cassandra_result;
+ result->api.db = &db->api;
+ result->api.refcount = 1;
+ result->callback = callback;
+ result->context = context;
+ result->query_type = query_type;
+ result->query = i_strdup(query);
+ result->is_prepared = is_prepared;
+ result->api.event = event_create(db->api.event);
+ array_push_back(&db->results, &result);
+ return result;
+}
+
+static void
+driver_cassandra_query_full(struct sql_db *_db, const char *query,
+ enum cassandra_query_type query_type,
+ sql_query_callback_t *callback, void *context)
+{
+ struct cassandra_db *db = (struct cassandra_db *)_db;
+ struct cassandra_result *result;
+
+ result = driver_cassandra_query_init(db, query, query_type, FALSE,
+ callback, context);
+ result->statement = cass_statement_new(query, 0);
+ (void)driver_cassandra_send_query(result);
+}
+
+static void driver_cassandra_exec(struct sql_db *db, const char *query)
+{
+ driver_cassandra_query_full(db, query, CASSANDRA_QUERY_TYPE_WRITE,
+ exec_callback, NULL);
+}
+
+static void driver_cassandra_query(struct sql_db *db, const char *query,
+ sql_query_callback_t *callback, void *context)
+{
+ driver_cassandra_query_full(db, query, CASSANDRA_QUERY_TYPE_READ,
+ callback, context);
+}
+
+static void cassandra_query_s_callback(struct sql_result *result, void *context)
+{
+ struct cassandra_db *db = context;
+
+ db->sync_result = result;
+}
+
+static void driver_cassandra_sync_init(struct cassandra_db *db)
+{
+ if (sql_connect(&db->api) < 0)
+ return;
+ db->orig_ioloop = current_ioloop;
+ db->ioloop = io_loop_create();
+ if (IS_CONNECTED(db))
+ return;
+ i_assert(db->api.state == SQL_DB_STATE_CONNECTING);
+
+ db->io_pipe = io_loop_move_io(&db->io_pipe);
+ /* wait for connecting to finish */
+ io_loop_run(db->ioloop);
+}
+
+static void driver_cassandra_sync_deinit(struct cassandra_db *db)
+{
+ if (db->orig_ioloop == NULL)
+ return;
+ if (db->io_pipe != NULL) {
+ io_loop_set_current(db->orig_ioloop);
+ db->io_pipe = io_loop_move_io(&db->io_pipe);
+ io_loop_set_current(db->ioloop);
+ }
+ io_loop_destroy(&db->ioloop);
+}
+
+static struct sql_result *
+driver_cassandra_sync_query(struct cassandra_db *db, const char *query,
+ enum cassandra_query_type query_type)
+{
+ struct sql_result *result;
+
+ i_assert(db->sync_result == NULL);
+
+ switch (db->api.state) {
+ case SQL_DB_STATE_CONNECTING:
+ case SQL_DB_STATE_BUSY:
+ i_unreached();
+ case SQL_DB_STATE_DISCONNECTED:
+ sql_not_connected_result.refcount++;
+ return &sql_not_connected_result;
+ case SQL_DB_STATE_IDLE:
+ break;
+ }
+
+ driver_cassandra_query_full(&db->api, query, query_type,
+ cassandra_query_s_callback, db);
+ if (db->sync_result == NULL) {
+ db->io_pipe = io_loop_move_io(&db->io_pipe);
+ io_loop_run(db->ioloop);
+ }
+
+ result = db->sync_result;
+ if (result == &sql_not_connected_result) {
+ /* we don't end up in cassandra's free function, so sync_result
+ won't be set to NULL if we don't do it here. */
+ db->sync_result = NULL;
+ } else if (result == NULL) {
+ result = &sql_not_connected_result;
+ result->refcount++;
+ }
+ return result;
+}
+
+static struct sql_result *
+driver_cassandra_query_s(struct sql_db *_db, const char *query)
+{
+ struct cassandra_db *db = (struct cassandra_db *)_db;
+ struct sql_result *result;
+
+ driver_cassandra_sync_init(db);
+ result = driver_cassandra_sync_query(db, query,
+ CASSANDRA_QUERY_TYPE_READ);
+ driver_cassandra_sync_deinit(db);
+ return result;
+}
+
+static int
+driver_cassandra_get_value(struct cassandra_result *result,
+ const CassValue *value, const char **str_r,
+ size_t *len_r)
+{
+ const unsigned char *output;
+ void *output_dup;
+ size_t output_size;
+ CassError rc;
+ const char *type;
+
+ if (cass_value_is_null(value) != 0) {
+ *str_r = NULL;
+ *len_r = 0;
+ return 0;
+ }
+
+ switch (cass_data_type_type(cass_value_data_type(value))) {
+ case CASS_VALUE_TYPE_INT: {
+ cass_int32_t num;
+
+ rc = cass_value_get_int32(value, &num);
+ if (rc == CASS_OK) {
+ const char *str = t_strdup_printf("%d", num);
+ output_size = strlen(str);
+ output = (const void *)str;
+ }
+ type = "int32";
+ break;
+ }
+ case CASS_VALUE_TYPE_TIMESTAMP:
+ case CASS_VALUE_TYPE_BIGINT: {
+ cass_int64_t num;
+
+ rc = cass_value_get_int64(value, &num);
+ if (rc == CASS_OK) {
+ const char *str = t_strdup_printf("%lld", (long long)num);
+ output_size = strlen(str);
+ output = (const void *)str;
+ }
+ type = "int64";
+ break;
+ }
+ default:
+ rc = cass_value_get_bytes(value, &output, &output_size);
+ type = "bytes";
+ break;
+ }
+ if (rc != CASS_OK) {
+ i_free(result->error);
+ result->error = i_strdup_printf("Couldn't get value as %s: %s",
+ type, cass_error_desc(rc));
+ return -1;
+ }
+ output_dup = p_malloc(result->row_pool, output_size + 1);
+ memcpy(output_dup, output, output_size);
+ *str_r = output_dup;
+ *len_r = output_size;
+ return 0;
+}
+
+static int driver_cassandra_result_next_page(struct cassandra_result *result)
+{
+ struct cassandra_db *db = (struct cassandra_db *)result->api.db;
+
+ if (db->page_size == 0) {
+ /* no paging */
+ return 0;
+ }
+ if (cass_result_has_more_pages(result->result) == cass_false)
+ return 0;
+
+ /* callers that don't support sql_query_more() will still get a useful
+ error message. */
+ i_free(result->error);
+ result->error = i_strdup(
+ "Paged query has more results, but not supported by the caller");
+ return SQL_RESULT_NEXT_MORE;
+}
+
+static int driver_cassandra_result_next_row(struct sql_result *_result)
+{
+ struct cassandra_result *result = (struct cassandra_result *)_result;
+ const CassRow *row;
+ const CassValue *value;
+ const char *str;
+ size_t size;
+ unsigned int i;
+ int ret = 1;
+
+ if (result->iterator == NULL)
+ return -1;
+
+ if (cass_iterator_next(result->iterator) == 0)
+ return driver_cassandra_result_next_page(result);
+ result->row_count++;
+ result->total_row_count++;
+
+ p_clear(result->row_pool);
+ p_array_init(&result->fields, result->row_pool, 8);
+ p_array_init(&result->field_sizes, result->row_pool, 8);
+
+ row = cass_iterator_get_row(result->iterator);
+ for (i = 0; (value = cass_row_get_column(row, i)) != NULL; i++) {
+ if (driver_cassandra_get_value(result, value, &str, &size) < 0) {
+ ret = -1;
+ break;
+ }
+ array_push_back(&result->fields, &str);
+ array_push_back(&result->field_sizes, &size);
+ }
+ return ret;
+}
+
+static void
+driver_cassandra_result_more(struct sql_result **_result, bool async,
+ sql_query_callback_t *callback, void *context)
+{
+ struct cassandra_db *db = (struct cassandra_db *)(*_result)->db;
+ struct cassandra_result *new_result;
+ struct cassandra_result *old_result =
+ (struct cassandra_result *)*_result;
+
+ /* Initialize the next page as a new sql_result */
+ new_result = driver_cassandra_query_init(db, old_result->query,
+ CASSANDRA_QUERY_TYPE_READ_MORE,
+ old_result->is_prepared,
+ callback, context);
+
+ /* Preserve the statement and update its paging state */
+ new_result->statement = old_result->statement;
+ old_result->statement = NULL;
+ cass_statement_set_paging_state(new_result->statement,
+ old_result->result);
+ old_result->paging_continues = TRUE;
+ /* The caller did support paging. Clear out the "...not supported by
+ the caller" error text, so it won't be in the debug log output. */
+ i_free_and_null(old_result->error);
+
+ new_result->timestamp = old_result->timestamp;
+ new_result->consistency = old_result->consistency;
+ new_result->page_num = old_result->page_num + 1;
+ new_result->page0_start_time = old_result->page0_start_time;
+ new_result->total_row_count = old_result->total_row_count;
+
+ sql_result_unref(*_result);
+ *_result = NULL;
+
+ if (async)
+ (void)driver_cassandra_send_query(new_result);
+ else {
+ i_assert(db->api.state == SQL_DB_STATE_IDLE);
+ driver_cassandra_sync_init(db);
+ (void)driver_cassandra_send_query(new_result);
+ if (new_result->result == NULL) {
+ db->io_pipe = io_loop_move_io(&db->io_pipe);
+ io_loop_run(db->ioloop);
+ }
+ driver_cassandra_sync_deinit(db);
+
+ callback(&new_result->api, context);
+ }
+}
+
+static unsigned int
+driver_cassandra_result_get_fields_count(struct sql_result *_result)
+{
+ struct cassandra_result *result = (struct cassandra_result *)_result;
+
+ return array_count(&result->fields);
+}
+
+static const char *
+driver_cassandra_result_get_field_name(struct sql_result *_result ATTR_UNUSED,
+ unsigned int idx ATTR_UNUSED)
+{
+ i_unreached();
+}
+
+static int
+driver_cassandra_result_find_field(struct sql_result *_result ATTR_UNUSED,
+ const char *field_name ATTR_UNUSED)
+{
+ i_unreached();
+}
+
+static const char *
+driver_cassandra_result_get_field_value(struct sql_result *_result,
+ unsigned int idx)
+{
+ struct cassandra_result *result = (struct cassandra_result *)_result;
+
+ return array_idx_elem(&result->fields, idx);
+}
+
+static const unsigned char *
+driver_cassandra_result_get_field_value_binary(struct sql_result *_result ATTR_UNUSED,
+ unsigned int idx ATTR_UNUSED,
+ size_t *size_r ATTR_UNUSED)
+{
+ struct cassandra_result *result = (struct cassandra_result *)_result;
+ const char *str;
+ const size_t *sizep;
+
+ str = array_idx_elem(&result->fields, idx);
+ sizep = array_idx(&result->field_sizes, idx);
+ *size_r = *sizep;
+ return (const void *)str;
+}
+
+static const char *
+driver_cassandra_result_find_field_value(struct sql_result *result ATTR_UNUSED,
+ const char *field_name ATTR_UNUSED)
+{
+ i_unreached();
+}
+
+static const char *const *
+driver_cassandra_result_get_values(struct sql_result *_result)
+{
+ struct cassandra_result *result = (struct cassandra_result *)_result;
+
+ return array_front(&result->fields);
+}
+
+static const char *driver_cassandra_result_get_error(struct sql_result *_result)
+{
+ struct cassandra_result *result = (struct cassandra_result *)_result;
+
+ if (result->error != NULL)
+ return result->error;
+ return "FIXME";
+}
+
+static struct sql_transaction_context *
+driver_cassandra_transaction_begin(struct sql_db *db)
+{
+ struct cassandra_transaction_context *ctx;
+
+ ctx = i_new(struct cassandra_transaction_context, 1);
+ ctx->ctx.db = db;
+ ctx->ctx.event = event_create(db->event);
+ ctx->refcount = 1;
+ return &ctx->ctx;
+}
+
+static void
+driver_cassandra_transaction_unref(struct cassandra_transaction_context **_ctx)
+{
+ struct cassandra_transaction_context *ctx = *_ctx;
+
+ *_ctx = NULL;
+ i_assert(ctx->refcount > 0);
+ if (--ctx->refcount > 0)
+ return;
+
+ event_unref(&ctx->ctx.event);
+ i_free(ctx->query);
+ i_free(ctx->error);
+ i_free(ctx);
+}
+
+static void
+transaction_set_failed(struct cassandra_transaction_context *ctx,
+ const char *error)
+{
+ if (ctx->failed) {
+ i_assert(ctx->error != NULL);
+ } else {
+ i_assert(ctx->error == NULL);
+ ctx->failed = TRUE;
+ ctx->error = i_strdup(error);
+ }
+}
+
+static void
+transaction_commit_callback(struct sql_result *result, void *context)
+{
+ struct cassandra_transaction_context *ctx = context;
+ struct sql_commit_result commit_result;
+
+ i_zero(&commit_result);
+ if (sql_result_next_row(result) < 0) {
+ commit_result.error = sql_result_get_error(result);
+ commit_result.error_type = sql_result_get_error_type(result);
+ e_debug(sql_transaction_finished_event(&ctx->ctx)->
+ add_str("error", commit_result.error)->event(),
+ "Transaction failed");
+ } else {
+ e_debug(sql_transaction_finished_event(&ctx->ctx)->event(),
+ "Transaction committed");
+ }
+ ctx->callback(&commit_result, ctx->context);
+ driver_cassandra_transaction_unref(&ctx);
+}
+
+static void
+driver_cassandra_transaction_commit(struct sql_transaction_context *_ctx,
+ sql_commit_callback_t *callback, void *context)
+{
+ struct cassandra_transaction_context *ctx =
+ (struct cassandra_transaction_context *)_ctx;
+ struct cassandra_db *db = (struct cassandra_db *)_ctx->db;
+ enum cassandra_query_type query_type;
+ struct sql_commit_result result;
+
+ i_zero(&result);
+ ctx->callback = callback;
+ ctx->context = context;
+
+ if (ctx->failed || (ctx->query == NULL && ctx->stmt == NULL)) {
+ if (ctx->failed)
+ result.error = ctx->error;
+
+ e_debug(sql_transaction_finished_event(_ctx)->
+ add_str("error", "Rolled back")->event(),
+ "Transaction rolled back");
+ callback(&result, context);
+ driver_cassandra_transaction_unref(&ctx);
+ return;
+ }
+
+ /* just a single query, send it */
+ const char *query = ctx->query != NULL ?
+ ctx->query : sql_statement_get_query(&ctx->stmt->stmt);
+ if (strncasecmp(query, "DELETE ", 7) == 0)
+ query_type = CASSANDRA_QUERY_TYPE_DELETE;
+ else
+ query_type = CASSANDRA_QUERY_TYPE_WRITE;
+
+ if (ctx->query != NULL) {
+ struct cassandra_result *cass_result;
+
+ cass_result = driver_cassandra_query_init(db, query, query_type,
+ FALSE, transaction_commit_callback, ctx);
+ cass_result->statement = cass_statement_new(query, 0);
+ if (ctx->query_timestamp != 0) {
+ cass_result->timestamp = ctx->query_timestamp;
+ cass_statement_set_timestamp(cass_result->statement,
+ ctx->query_timestamp);
+ }
+ (void)driver_cassandra_send_query(cass_result);
+ } else {
+ ctx->stmt->result =
+ driver_cassandra_query_init(db, query, query_type, TRUE,
+ transaction_commit_callback, ctx);
+ if (ctx->stmt->cass_stmt == NULL) {
+ /* wait for prepare to finish */
+ } else {
+ ctx->stmt->result->statement = ctx->stmt->cass_stmt;
+ ctx->stmt->result->timestamp = ctx->stmt->timestamp;
+ (void)driver_cassandra_send_query(ctx->stmt->result);
+ pool_unref(&ctx->stmt->stmt.pool);
+ }
+ }
+}
+
+static void
+driver_cassandra_try_commit_s(struct cassandra_transaction_context *ctx)
+{
+ struct sql_transaction_context *_ctx = &ctx->ctx;
+ struct cassandra_db *db = (struct cassandra_db *)_ctx->db;
+ struct sql_result *result = NULL;
+ enum cassandra_query_type query_type;
+
+ /* just a single query, send it */
+ if (strncasecmp(ctx->query, "DELETE ", 7) == 0)
+ query_type = CASSANDRA_QUERY_TYPE_DELETE;
+ else
+ query_type = CASSANDRA_QUERY_TYPE_WRITE;
+ driver_cassandra_sync_init(db);
+ result = driver_cassandra_sync_query(db, ctx->query, query_type);
+ driver_cassandra_sync_deinit(db);
+
+ if (sql_result_next_row(result) < 0)
+ transaction_set_failed(ctx, sql_result_get_error(result));
+ sql_result_unref(result);
+}
+
+static int
+driver_cassandra_transaction_commit_s(struct sql_transaction_context *_ctx,
+ const char **error_r)
+{
+ struct cassandra_transaction_context *ctx =
+ (struct cassandra_transaction_context *)_ctx;
+
+ if (ctx->stmt != NULL) {
+ /* nothing should be using this - don't bother implementing */
+ i_panic("cassandra: sql_transaction_commit_s() not supported for prepared statements");
+ }
+
+ if (ctx->query != NULL && !ctx->failed)
+ driver_cassandra_try_commit_s(ctx);
+ *error_r = t_strdup(ctx->error);
+
+ i_assert(ctx->refcount == 1);
+ i_assert((*error_r != NULL) == ctx->failed);
+ driver_cassandra_transaction_unref(&ctx);
+ return *error_r == NULL ? 0 : -1;
+}
+
+static void
+driver_cassandra_transaction_rollback(struct sql_transaction_context *_ctx)
+{
+ struct cassandra_transaction_context *ctx =
+ (struct cassandra_transaction_context *)_ctx;
+
+ i_assert(ctx->refcount == 1);
+ driver_cassandra_transaction_unref(&ctx);
+}
+
+static void
+driver_cassandra_update(struct sql_transaction_context *_ctx, const char *query,
+ unsigned int *affected_rows)
+{
+ struct cassandra_transaction_context *ctx =
+ (struct cassandra_transaction_context *)_ctx;
+
+ i_assert(affected_rows == NULL);
+
+ if (ctx->query != NULL || ctx->stmt != NULL) {
+ transaction_set_failed(ctx, "Multiple changes in transaction not supported");
+ return;
+ }
+ ctx->query = i_strdup(query);
+}
+
+static const char *
+driver_cassandra_escape_blob(struct sql_db *_db ATTR_UNUSED,
+ const unsigned char *data, size_t size)
+{
+ string_t *str = t_str_new(128);
+
+ str_append(str, "0x");
+ binary_to_hex_append(str, data, size);
+ return str_c(str);
+}
+
+static CassError
+driver_cassandra_bind_int(struct cassandra_sql_statement *stmt,
+ unsigned int column_idx, int64_t value)
+{
+ const CassDataType *data_type;
+ CassValueType value_type;
+
+ i_assert(stmt->prep != NULL);
+
+ /* statements require exactly correct value type */
+ data_type = cass_prepared_parameter_data_type(stmt->prep->prepared,
+ column_idx);
+ value_type = cass_data_type_type(data_type);
+
+ switch (value_type) {
+ case CASS_VALUE_TYPE_INT:
+ if (value < INT32_MIN || value > INT32_MAX)
+ return CASS_ERROR_LIB_INVALID_VALUE_TYPE;
+ return cass_statement_bind_int32(stmt->cass_stmt, column_idx,
+ value);
+ case CASS_VALUE_TYPE_TIMESTAMP:
+ case CASS_VALUE_TYPE_BIGINT:
+ return cass_statement_bind_int64(stmt->cass_stmt, column_idx,
+ value);
+ case CASS_VALUE_TYPE_SMALL_INT:
+ if (value < INT16_MIN || value > INT16_MAX)
+ return CASS_ERROR_LIB_INVALID_VALUE_TYPE;
+ return cass_statement_bind_int16(stmt->cass_stmt, column_idx,
+ value);
+ case CASS_VALUE_TYPE_TINY_INT:
+ if (value < INT8_MIN || value > INT8_MAX)
+ return CASS_ERROR_LIB_INVALID_VALUE_TYPE;
+ return cass_statement_bind_int8(stmt->cass_stmt, column_idx,
+ value);
+ default:
+ return CASS_ERROR_LIB_INVALID_VALUE_TYPE;
+ }
+}
+
+static void prepare_finish_arg(struct cassandra_sql_statement *stmt,
+ const struct cassandra_sql_arg *arg)
+{
+ CassError rc;
+
+ if (arg->value_str != NULL) {
+ rc = cass_statement_bind_string(stmt->cass_stmt, arg->column_idx,
+ arg->value_str);
+ } else if (arg->value_binary != NULL) {
+ rc = cass_statement_bind_bytes(stmt->cass_stmt, arg->column_idx,
+ arg->value_binary,
+ arg->value_binary_size);
+ } else {
+ rc = driver_cassandra_bind_int(stmt, arg->column_idx,
+ arg->value_int64);
+ }
+ if (rc != CASS_OK) {
+ e_error(stmt->stmt.db->event,
+ "Statement '%s': Failed to bind column %u: %s",
+ stmt->stmt.query_template, arg->column_idx,
+ cass_error_desc(rc));
+ }
+}
+
+static void prepare_finish_statement(struct cassandra_sql_statement *stmt)
+{
+ const struct cassandra_sql_arg *arg;
+
+ if (stmt->prep->prepared == NULL) {
+ i_assert(stmt->prep->error != NULL);
+
+ if (stmt->result != NULL) {
+ stmt->result->error = i_strdup(stmt->prep->error);
+ result_finish(stmt->result);
+ }
+ pool_unref(&stmt->stmt.pool);
+ return;
+ }
+ stmt->cass_stmt = cass_prepared_bind(stmt->prep->prepared);
+
+ if (stmt->timestamp != 0)
+ cass_statement_set_timestamp(stmt->cass_stmt, stmt->timestamp);
+
+ if (array_is_created(&stmt->pending_args)) {
+ array_foreach(&stmt->pending_args, arg)
+ prepare_finish_arg(stmt, arg);
+ }
+ if (stmt->result != NULL) {
+ stmt->result->statement = stmt->cass_stmt;
+ stmt->result->timestamp = stmt->timestamp;
+ (void)driver_cassandra_send_query(stmt->result);
+ pool_unref(&stmt->stmt.pool);
+ }
+}
+
+static void
+prepare_finish_pending_statements(struct cassandra_sql_prepared_statement *prep_stmt)
+{
+ struct cassandra_sql_statement *stmt;
+
+ array_foreach_elem(&prep_stmt->pending_statements, stmt)
+ prepare_finish_statement(stmt);
+ array_clear(&prep_stmt->pending_statements);
+}
+
+static void prepare_callback(CassFuture *future, void *context)
+{
+ struct cassandra_sql_prepared_statement *prep_stmt = context;
+ CassError error = cass_future_error_code(future);
+
+ if (error != CASS_OK) {
+ const char *errmsg;
+ size_t errsize;
+
+ cass_future_error_message(future, &errmsg, &errsize);
+ i_free(prep_stmt->error);
+ prep_stmt->error = i_strndup(errmsg, errsize);
+ } else {
+ prep_stmt->prepared = cass_future_get_prepared(future);
+ }
+
+ prepare_finish_pending_statements(prep_stmt);
+}
+
+static void prepare_start(struct cassandra_sql_prepared_statement *prep_stmt)
+{
+ struct cassandra_db *db = (struct cassandra_db *)prep_stmt->prep_stmt.db;
+ CassFuture *future;
+
+ if (!SQL_DB_IS_READY(&db->api)) {
+ if (!prep_stmt->pending) {
+ prep_stmt->pending = TRUE;
+ array_push_back(&db->pending_prepares, &prep_stmt);
+
+ if (sql_connect(&db->api) < 0)
+ i_unreached();
+ }
+ return;
+ }
+
+ /* clear the current error in case we're retrying */
+ i_free_and_null(prep_stmt->error);
+
+ future = cass_session_prepare(db->session,
+ prep_stmt->prep_stmt.query_template);
+ driver_cassandra_set_callback(future, db, prepare_callback, prep_stmt);
+}
+
+static void driver_cassandra_prepare_pending(struct cassandra_db *db)
+{
+ struct cassandra_sql_prepared_statement *prep_stmt;
+
+ i_assert(SQL_DB_IS_READY(&db->api));
+
+ array_foreach_elem(&db->pending_prepares, prep_stmt) {
+ prep_stmt->pending = FALSE;
+ prepare_start(prep_stmt);
+ }
+ array_clear(&db->pending_prepares);
+}
+
+static struct sql_prepared_statement *
+driver_cassandra_prepared_statement_init(struct sql_db *db,
+ const char *query_template)
+{
+ struct cassandra_sql_prepared_statement *prep_stmt =
+ i_new(struct cassandra_sql_prepared_statement, 1);
+ prep_stmt->prep_stmt.db = db;
+ prep_stmt->prep_stmt.refcount = 1;
+ prep_stmt->prep_stmt.query_template = i_strdup(query_template);
+ i_array_init(&prep_stmt->pending_statements, 4);
+ prepare_start(prep_stmt);
+ return &prep_stmt->prep_stmt;
+}
+
+static void
+driver_cassandra_prepared_statement_deinit(struct sql_prepared_statement *_prep_stmt)
+{
+ struct cassandra_sql_prepared_statement *prep_stmt =
+ (struct cassandra_sql_prepared_statement *)_prep_stmt;
+
+ i_assert(array_count(&prep_stmt->pending_statements) == 0);
+ if (prep_stmt->prepared != NULL)
+ cass_prepared_free(prep_stmt->prepared);
+ array_free(&prep_stmt->pending_statements);
+ i_free(prep_stmt->error);
+ i_free(prep_stmt->prep_stmt.query_template);
+ i_free(prep_stmt);
+}
+
+static struct sql_statement *
+driver_cassandra_statement_init(struct sql_db *db ATTR_UNUSED,
+ const char *query_template ATTR_UNUSED)
+{
+ pool_t pool = pool_alloconly_create("cassandra sql statement", 1024);
+ struct cassandra_sql_statement *stmt =
+ p_new(pool, struct cassandra_sql_statement, 1);
+ stmt->stmt.pool = pool;
+ return &stmt->stmt;
+}
+
+static struct sql_statement *
+driver_cassandra_statement_init_prepared(struct sql_prepared_statement *_prep_stmt)
+{
+ struct cassandra_sql_prepared_statement *prep_stmt =
+ (struct cassandra_sql_prepared_statement *)_prep_stmt;
+ pool_t pool = pool_alloconly_create("cassandra prepared sql statement", 1024);
+ struct cassandra_sql_statement *stmt =
+ p_new(pool, struct cassandra_sql_statement, 1);
+
+ stmt->stmt.pool = pool;
+ stmt->stmt.query_template =
+ p_strdup(stmt->stmt.pool, prep_stmt->prep_stmt.query_template);
+ stmt->prep = prep_stmt;
+
+ if (prep_stmt->prepared != NULL) {
+ /* statement is already prepared. we can use it immediately. */
+ stmt->cass_stmt = cass_prepared_bind(prep_stmt->prepared);
+ } else {
+ if (prep_stmt->error != NULL)
+ prepare_start(prep_stmt);
+ /* need to wait until prepare is finished */
+ array_push_back(&prep_stmt->pending_statements, &stmt);
+ }
+ return &stmt->stmt;
+}
+
+static void
+driver_cassandra_statement_abort(struct sql_statement *_stmt)
+{
+ struct cassandra_sql_statement *stmt =
+ (struct cassandra_sql_statement *)_stmt;
+
+ if (stmt->cass_stmt != NULL)
+ cass_statement_free(stmt->cass_stmt);
+}
+
+static void
+driver_cassandra_statement_set_timestamp(struct sql_statement *_stmt,
+ const struct timespec *ts)
+{
+ struct cassandra_sql_statement *stmt =
+ (struct cassandra_sql_statement *)_stmt;
+ cass_int64_t ts_usecs =
+ (cass_int64_t)ts->tv_sec * 1000000ULL +
+ ts->tv_nsec / 1000;
+
+ i_assert(stmt->result == NULL);
+
+ if (stmt->cass_stmt != NULL)
+ cass_statement_set_timestamp(stmt->cass_stmt, ts_usecs);
+ stmt->timestamp = ts_usecs;
+}
+
+static struct cassandra_sql_arg *
+driver_cassandra_add_pending_arg(struct cassandra_sql_statement *stmt,
+ unsigned int column_idx)
+{
+ struct cassandra_sql_arg *arg;
+
+ if (!array_is_created(&stmt->pending_args))
+ p_array_init(&stmt->pending_args, stmt->stmt.pool, 8);
+ arg = array_append_space(&stmt->pending_args);
+ arg->column_idx = column_idx;
+ return arg;
+}
+
+static void
+driver_cassandra_statement_bind_str(struct sql_statement *_stmt,
+ unsigned int column_idx,
+ const char *value)
+{
+ struct cassandra_sql_statement *stmt =
+ (struct cassandra_sql_statement *)_stmt;
+ if (stmt->cass_stmt != NULL)
+ cass_statement_bind_string(stmt->cass_stmt, column_idx, value);
+ else if (stmt->prep != NULL) {
+ struct cassandra_sql_arg *arg =
+ driver_cassandra_add_pending_arg(stmt, column_idx);
+ arg->value_str = p_strdup(_stmt->pool, value);
+ }
+}
+
+static void
+driver_cassandra_statement_bind_binary(struct sql_statement *_stmt,
+ unsigned int column_idx,
+ const void *value, size_t value_size)
+{
+ struct cassandra_sql_statement *stmt =
+ (struct cassandra_sql_statement *)_stmt;
+
+ if (stmt->cass_stmt != NULL) {
+ cass_statement_bind_bytes(stmt->cass_stmt, column_idx,
+ value, value_size);
+ } else if (stmt->prep != NULL) {
+ struct cassandra_sql_arg *arg =
+ driver_cassandra_add_pending_arg(stmt, column_idx);
+ arg->value_binary = value_size == 0 ? &uchar_nul :
+ p_memdup(_stmt->pool, value, value_size);
+ arg->value_binary_size = value_size;
+ }
+}
+
+static void
+driver_cassandra_statement_bind_int64(struct sql_statement *_stmt,
+ unsigned int column_idx, int64_t value)
+{
+ struct cassandra_sql_statement *stmt =
+ (struct cassandra_sql_statement *)_stmt;
+
+ if (stmt->cass_stmt != NULL)
+ driver_cassandra_bind_int(stmt, column_idx, value);
+ else if (stmt->prep != NULL) {
+ struct cassandra_sql_arg *arg =
+ driver_cassandra_add_pending_arg(stmt, column_idx);
+ arg->value_int64 = value;
+ }
+}
+
+static void
+driver_cassandra_statement_query(struct sql_statement *_stmt,
+ sql_query_callback_t *callback, void *context)
+{
+ struct cassandra_sql_statement *stmt =
+ (struct cassandra_sql_statement *)_stmt;
+ struct cassandra_db *db = (struct cassandra_db *)_stmt->db;
+ const char *query = sql_statement_get_query(_stmt);
+ bool is_prepared = stmt->cass_stmt != NULL || stmt->prep != NULL;
+
+ stmt->result = driver_cassandra_query_init(db, query,
+ CASSANDRA_QUERY_TYPE_READ,
+ is_prepared,
+ callback, context);
+ if (stmt->cass_stmt != NULL) {
+ stmt->result->statement = stmt->cass_stmt;
+ stmt->result->timestamp = stmt->timestamp;
+ } else if (stmt->prep != NULL) {
+ /* wait for prepare to finish */
+ return;
+ } else {
+ stmt->result->statement = cass_statement_new(query, 0);
+ stmt->result->timestamp = stmt->timestamp;
+ if (stmt->timestamp != 0) {
+ cass_statement_set_timestamp(stmt->result->statement,
+ stmt->timestamp);
+ }
+ }
+ (void)driver_cassandra_send_query(stmt->result);
+ pool_unref(&_stmt->pool);
+}
+
+static struct sql_result *
+driver_cassandra_statement_query_s(struct sql_statement *_stmt ATTR_UNUSED)
+{
+ i_panic("cassandra: sql_statement_query_s() not supported");
+}
+
+static void
+driver_cassandra_update_stmt(struct sql_transaction_context *_ctx,
+ struct sql_statement *_stmt,
+ unsigned int *affected_rows)
+{
+ struct cassandra_transaction_context *ctx =
+ (struct cassandra_transaction_context *)_ctx;
+ struct cassandra_sql_statement *stmt =
+ (struct cassandra_sql_statement *)_stmt;
+
+ i_assert(affected_rows == NULL);
+
+ if (ctx->query != NULL || ctx->stmt != NULL) {
+ transaction_set_failed(ctx,
+ "Multiple changes in transaction not supported");
+ return;
+ }
+ if (stmt->prep != NULL)
+ ctx->stmt = stmt;
+ else {
+ ctx->query = i_strdup(sql_statement_get_query(_stmt));
+ ctx->query_timestamp = stmt->timestamp;
+ pool_unref(&_stmt->pool);
+ }
+}
+
+static bool driver_cassandra_have_work(struct cassandra_db *db)
+{
+ return array_not_empty(&db->pending_prepares) ||
+ array_not_empty(&db->callbacks) ||
+ array_not_empty(&db->results);
+}
+
+static void driver_cassandra_wait(struct sql_db *_db)
+{
+ struct cassandra_db *db = (struct cassandra_db *)_db;
+
+ if (!driver_cassandra_have_work(db))
+ return;
+
+ struct ioloop *prev_ioloop = current_ioloop;
+ db->ioloop = io_loop_create();
+ db->io_pipe = io_loop_move_io(&db->io_pipe);
+ while (driver_cassandra_have_work(db))
+ io_loop_run(db->ioloop);
+
+ io_loop_set_current(prev_ioloop);
+ db->io_pipe = io_loop_move_io(&db->io_pipe);
+ io_loop_set_current(db->ioloop);
+ io_loop_destroy(&db->ioloop);
+}
+
+const struct sql_db driver_cassandra_db = {
+ .name = "cassandra",
+ .flags = SQL_DB_FLAG_PREP_STATEMENTS,
+
+ .v = {
+ .init_full = driver_cassandra_init_full_v,
+ .deinit = driver_cassandra_deinit_v,
+ .connect = driver_cassandra_connect,
+ .disconnect = driver_cassandra_disconnect,
+ .escape_string = driver_cassandra_escape_string,
+ .exec = driver_cassandra_exec,
+ .query = driver_cassandra_query,
+ .query_s = driver_cassandra_query_s,
+ .wait = driver_cassandra_wait,
+
+ .transaction_begin = driver_cassandra_transaction_begin,
+ .transaction_commit = driver_cassandra_transaction_commit,
+ .transaction_commit_s = driver_cassandra_transaction_commit_s,
+ .transaction_rollback = driver_cassandra_transaction_rollback,
+
+ .update = driver_cassandra_update,
+
+ .escape_blob = driver_cassandra_escape_blob,
+
+ .prepared_statement_init = driver_cassandra_prepared_statement_init,
+ .prepared_statement_deinit = driver_cassandra_prepared_statement_deinit,
+ .statement_init = driver_cassandra_statement_init,
+ .statement_init_prepared = driver_cassandra_statement_init_prepared,
+ .statement_abort = driver_cassandra_statement_abort,
+ .statement_set_timestamp = driver_cassandra_statement_set_timestamp,
+ .statement_bind_str = driver_cassandra_statement_bind_str,
+ .statement_bind_binary = driver_cassandra_statement_bind_binary,
+ .statement_bind_int64 = driver_cassandra_statement_bind_int64,
+ .statement_query = driver_cassandra_statement_query,
+ .statement_query_s = driver_cassandra_statement_query_s,
+ .update_stmt = driver_cassandra_update_stmt,
+ }
+};
+
+const struct sql_result driver_cassandra_result = {
+ .v = {
+ driver_cassandra_result_free,
+ driver_cassandra_result_next_row,
+ driver_cassandra_result_get_fields_count,
+ driver_cassandra_result_get_field_name,
+ driver_cassandra_result_find_field,
+ driver_cassandra_result_get_field_value,
+ driver_cassandra_result_get_field_value_binary,
+ driver_cassandra_result_find_field_value,
+ driver_cassandra_result_get_values,
+ driver_cassandra_result_get_error,
+ driver_cassandra_result_more,
+ }
+};
+
+const char *driver_cassandra_version = DOVECOT_ABI_VERSION;
+
+void driver_cassandra_init(void);
+void driver_cassandra_deinit(void);
+
+void driver_cassandra_init(void)
+{
+ sql_driver_register(&driver_cassandra_db);
+}
+
+void driver_cassandra_deinit(void)
+{
+ sql_driver_unregister(&driver_cassandra_db);
+}
+
+#endif
diff --git a/src/lib-sql/driver-mysql.c b/src/lib-sql/driver-mysql.c
new file mode 100644
index 0000000..b8308cc
--- /dev/null
+++ b/src/lib-sql/driver-mysql.c
@@ -0,0 +1,835 @@
+/* Copyright (c) 2003-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "ioloop.h"
+#include "array.h"
+#include "hex-binary.h"
+#include "str.h"
+#include "net.h"
+#include "time-util.h"
+#include "sql-api-private.h"
+
+#ifdef BUILD_MYSQL
+#include <unistd.h>
+#include <time.h>
+#ifdef HAVE_ATTR_NULL
+/* ugly way to tell clang that mysql.h is a system header and we don't want
+ to enable nonnull attributes for it by default.. */
+# 4 "driver-mysql.c" 3
+#endif
+#include <mysql.h>
+#ifdef HAVE_ATTR_NULL
+# 4 "driver-mysql.c" 3
+# line 20
+#endif
+#include <errmsg.h>
+
+#define MYSQL_DEFAULT_READ_TIMEOUT_SECS 30
+#define MYSQL_DEFAULT_WRITE_TIMEOUT_SECS 30
+
+struct mysql_db {
+ struct sql_db api;
+
+ pool_t pool;
+ const char *user, *password, *dbname, *host, *unix_socket;
+ const char *ssl_cert, *ssl_key, *ssl_ca, *ssl_ca_path, *ssl_cipher;
+ int ssl_verify_server_cert;
+ const char *option_file, *option_group;
+ in_port_t port;
+ unsigned int client_flags;
+ unsigned int connect_timeout, read_timeout, write_timeout;
+ time_t last_success;
+
+ MYSQL *mysql;
+ unsigned int next_query_connection;
+
+ bool ssl_set:1;
+};
+
+struct mysql_result {
+ struct sql_result api;
+
+ MYSQL_RES *result;
+ MYSQL_ROW row;
+
+ MYSQL_FIELD *fields;
+ unsigned int fields_count;
+
+ my_ulonglong affected_rows;
+};
+
+struct mysql_transaction_context {
+ struct sql_transaction_context ctx;
+
+ pool_t query_pool;
+ const char *error;
+
+ bool failed:1;
+ bool committed:1;
+ bool commit_started:1;
+};
+
+extern const struct sql_db driver_mysql_db;
+extern const struct sql_result driver_mysql_result;
+extern const struct sql_result driver_mysql_error_result;
+
+static struct event_category event_category_mysql = {
+ .parent = &event_category_sql,
+ .name = "mysql"
+};
+
+static int driver_mysql_connect(struct sql_db *_db)
+{
+ struct mysql_db *db = (struct mysql_db *)_db;
+ const char *unix_socket, *host;
+ unsigned long client_flags = db->client_flags;
+ unsigned int secs_used;
+ time_t start_time;
+ bool failed;
+
+ i_assert(db->api.state == SQL_DB_STATE_DISCONNECTED);
+
+ sql_db_set_state(&db->api, SQL_DB_STATE_CONNECTING);
+
+ if (mysql_init(db->mysql) == NULL)
+ i_fatal("mysql_init() failed");
+
+ if (db->host == NULL) {
+ /* assume option_file overrides the host, or if not we'll just
+ connect to localhost */
+ unix_socket = NULL;
+ host = NULL;
+ } else if (*db->host == '/') {
+ unix_socket = db->host;
+ host = NULL;
+ } else {
+ unix_socket = NULL;
+ host = db->host;
+ }
+
+ if (db->option_file != NULL) {
+ mysql_options(db->mysql, MYSQL_READ_DEFAULT_FILE,
+ db->option_file);
+ }
+
+ if (db->host != NULL)
+ event_set_append_log_prefix(_db->event, t_strdup_printf("mysql(%s): ", db->host));
+
+ e_debug(_db->event, "Connecting");
+
+ mysql_options(db->mysql, MYSQL_OPT_CONNECT_TIMEOUT, &db->connect_timeout);
+ mysql_options(db->mysql, MYSQL_OPT_READ_TIMEOUT, &db->read_timeout);
+ mysql_options(db->mysql, MYSQL_OPT_WRITE_TIMEOUT, &db->write_timeout);
+ mysql_options(db->mysql, MYSQL_READ_DEFAULT_GROUP,
+ db->option_group != NULL ? db->option_group : "client");
+
+ if (!db->ssl_set && (db->ssl_ca != NULL || db->ssl_ca_path != NULL)) {
+#ifdef HAVE_MYSQL_SSL
+ mysql_ssl_set(db->mysql, db->ssl_key, db->ssl_cert,
+ db->ssl_ca, db->ssl_ca_path
+#ifdef HAVE_MYSQL_SSL_CIPHER
+ , db->ssl_cipher
+#endif
+ );
+#ifdef HAVE_MYSQL_SSL_VERIFY_SERVER_CERT
+ mysql_options(db->mysql, MYSQL_OPT_SSL_VERIFY_SERVER_CERT,
+ (void *)&db->ssl_verify_server_cert);
+#endif
+ db->ssl_set = TRUE;
+#else
+ i_fatal("mysql: SSL support not compiled in "
+ "(remove ssl_ca and ssl_ca_path settings)");
+#endif
+ }
+
+#ifdef CLIENT_MULTI_RESULTS
+ client_flags |= CLIENT_MULTI_RESULTS;
+#endif
+ /* CLIENT_MULTI_RESULTS allows the use of stored procedures */
+ start_time = time(NULL);
+ failed = mysql_real_connect(db->mysql, host, db->user, db->password,
+ db->dbname, db->port, unix_socket,
+ client_flags) == NULL;
+ secs_used = time(NULL) - start_time;
+ if (failed) {
+ /* connecting could have taken a while. make sure that any
+ timeouts that get added soon will get a refreshed
+ timestamp. */
+ io_loop_time_refresh();
+
+ if (db->api.connect_delay < secs_used)
+ db->api.connect_delay = secs_used;
+ sql_db_set_state(&db->api, SQL_DB_STATE_DISCONNECTED);
+ e_error(_db->event, "Connect failed to database (%s): %s - "
+ "waiting for %u seconds before retry",
+ db->dbname, mysql_error(db->mysql), db->api.connect_delay);
+ return -1;
+ } else {
+ db->last_success = ioloop_time;
+ sql_db_set_state(&db->api, SQL_DB_STATE_IDLE);
+ return 1;
+ }
+}
+
+static void driver_mysql_disconnect(struct sql_db *_db)
+{
+ struct mysql_db *db = (struct mysql_db *)_db;
+ if (db->mysql != NULL)
+ mysql_close(db->mysql);
+ db->mysql = NULL;
+}
+
+static int driver_mysql_parse_connect_string(struct mysql_db *db,
+ const char *connect_string,
+ const char **error_r)
+{
+ const char *const *args, *name, *value;
+ const char **field;
+
+ db->ssl_cipher = "HIGH";
+ db->ssl_verify_server_cert = 1;
+ db->connect_timeout = SQL_CONNECT_TIMEOUT_SECS;
+ db->read_timeout = MYSQL_DEFAULT_READ_TIMEOUT_SECS;
+ db->write_timeout = MYSQL_DEFAULT_WRITE_TIMEOUT_SECS;
+
+ args = t_strsplit_spaces(connect_string, " ");
+ for (; *args != NULL; args++) {
+ value = strchr(*args, '=');
+ if (value == NULL) {
+ *error_r = t_strdup_printf("Missing value in connect string: %s",
+ *args);
+ return -1;
+ }
+ name = t_strdup_until(*args, value);
+ value++;
+
+ field = NULL;
+ if (strcmp(name, "host") == 0 ||
+ strcmp(name, "hostaddr") == 0)
+ field = &db->host;
+ else if (strcmp(name, "user") == 0)
+ field = &db->user;
+ else if (strcmp(name, "password") == 0)
+ field = &db->password;
+ else if (strcmp(name, "dbname") == 0)
+ field = &db->dbname;
+ else if (strcmp(name, "port") == 0) {
+ if (net_str2port(value, &db->port) < 0) {
+ *error_r = t_strdup_printf("Invalid port number: %s", value);
+ return -1;
+ }
+ } else if (strcmp(name, "client_flags") == 0) {
+ if (str_to_uint(value, &db->client_flags) < 0) {
+ *error_r = t_strdup_printf("Invalid client flags: %s", value);
+ return -1;
+ }
+ } else if (strcmp(name, "connect_timeout") == 0) {
+ if (str_to_uint(value, &db->connect_timeout) < 0) {
+ *error_r = t_strdup_printf("Invalid read_timeout: %s", value);
+ return -1;
+ }
+ } else if (strcmp(name, "read_timeout") == 0) {
+ if (str_to_uint(value, &db->read_timeout) < 0) {
+ *error_r = t_strdup_printf("Invalid read_timeout: %s", value);
+ return -1;
+ }
+ } else if (strcmp(name, "write_timeout") == 0) {
+ if (str_to_uint(value, &db->write_timeout) < 0) {
+ *error_r = t_strdup_printf("Invalid read_timeout: %s", value);
+ return -1;
+ }
+ } else if (strcmp(name, "ssl_cert") == 0)
+ field = &db->ssl_cert;
+ else if (strcmp(name, "ssl_key") == 0)
+ field = &db->ssl_key;
+ else if (strcmp(name, "ssl_ca") == 0)
+ field = &db->ssl_ca;
+ else if (strcmp(name, "ssl_ca_path") == 0)
+ field = &db->ssl_ca_path;
+ else if (strcmp(name, "ssl_cipher") == 0)
+ field = &db->ssl_cipher;
+ else if (strcmp(name, "ssl_verify_server_cert") == 0) {
+ if (strcmp(value, "yes") == 0)
+ db->ssl_verify_server_cert = 1;
+ else if (strcmp(value, "no") == 0)
+ db->ssl_verify_server_cert = 0;
+ else {
+ *error_r = t_strdup_printf("Invalid boolean: %s", value);
+ return -1;
+ }
+ } else if (strcmp(name, "option_file") == 0)
+ field = &db->option_file;
+ else if (strcmp(name, "option_group") == 0)
+ field = &db->option_group;
+ else {
+ *error_r = t_strdup_printf("Unknown connect string: %s", name);
+ return -1;
+ }
+ if (field != NULL)
+ *field = p_strdup(db->pool, value);
+ }
+
+ if (db->host == NULL && db->option_file == NULL) {
+ *error_r = "No hosts given in connect string";
+ return -1;
+ }
+ db->mysql = p_new(db->pool, MYSQL, 1);
+ return 0;
+}
+
+static int driver_mysql_init_full_v(const struct sql_settings *set,
+ struct sql_db **db_r, const char **error_r)
+{
+ struct mysql_db *db;
+ const char *error = NULL;
+ pool_t pool;
+ int ret;
+
+ pool = pool_alloconly_create("mysql driver", 1024);
+ db = p_new(pool, struct mysql_db, 1);
+ db->pool = pool;
+ db->api = driver_mysql_db;
+ db->api.event = event_create(set->event_parent);
+ event_add_category(db->api.event, &event_category_mysql);
+ event_set_append_log_prefix(db->api.event, "mysql: ");
+ T_BEGIN {
+ ret = driver_mysql_parse_connect_string(db, set->connect_string, &error);
+ error = p_strdup(db->pool, error);
+ } T_END;
+
+ if (ret < 0) {
+ *error_r = t_strdup(error);
+ pool_unref(&db->pool);
+ return ret;
+ }
+
+ *db_r = &db->api;
+ return 0;
+}
+
+static void driver_mysql_deinit_v(struct sql_db *_db)
+{
+ struct mysql_db *db = (struct mysql_db *)_db;
+
+ _db->no_reconnect = TRUE;
+ sql_db_set_state(&db->api, SQL_DB_STATE_DISCONNECTED);
+
+ if (db->mysql != NULL)
+ mysql_close(db->mysql);
+ db->mysql = NULL;
+
+ sql_connection_log_finished(_db);
+ event_unref(&_db->event);
+ array_free(&_db->module_contexts);
+ pool_unref(&db->pool);
+}
+
+static int driver_mysql_do_query(struct mysql_db *db, const char *query,
+ struct event *event)
+{
+ int ret, diff;
+ struct event_passthrough *e;
+
+ ret = mysql_query(db->mysql, query);
+ io_loop_time_refresh();
+ e = sql_query_finished_event(&db->api, event, query, ret == 0, &diff);
+
+ if (ret != 0) {
+ e->add_int("error_code", mysql_errno(db->mysql));
+ e->add_str("error", mysql_error(db->mysql));
+ e_debug(e->event(), SQL_QUERY_FINISHED_FMT": %s", query,
+ diff, mysql_error(db->mysql));
+ } else
+ e_debug(e->event(), SQL_QUERY_FINISHED_FMT, query, diff);
+
+ if (ret == 0)
+ return 0;
+
+ /* failed */
+ switch (mysql_errno(db->mysql)) {
+ case CR_SERVER_GONE_ERROR:
+ case CR_SERVER_LOST:
+ sql_db_set_state(&db->api, SQL_DB_STATE_DISCONNECTED);
+ break;
+ default:
+ break;
+ }
+ return -1;
+}
+
+static const char *
+driver_mysql_escape_string(struct sql_db *_db, const char *string)
+{
+ struct mysql_db *db = (struct mysql_db *)_db;
+ size_t len = strlen(string);
+ char *to;
+
+ if (_db->state == SQL_DB_STATE_DISCONNECTED) {
+ /* try connecting */
+ (void)sql_connect(&db->api);
+ }
+
+ if (db->mysql == NULL) {
+ /* FIXME: we don't have a valid connection, so fallback
+ to using default escaping. the next query will most
+ likely fail anyway so it shouldn't matter that much
+ what we return here.. Anyway, this API needs
+ changing so that the escaping function could already
+ fail the query reliably. */
+ to = t_buffer_get(len * 2 + 1);
+ len = mysql_escape_string(to, string, len);
+ t_buffer_alloc(len + 1);
+ return to;
+ }
+
+ to = t_buffer_get(len * 2 + 1);
+ len = mysql_real_escape_string(db->mysql, to, string, len);
+ t_buffer_alloc(len + 1);
+ return to;
+}
+
+static void driver_mysql_exec(struct sql_db *_db, const char *query)
+{
+ struct mysql_db *db = (struct mysql_db *)_db;
+ struct event *event = event_create(_db->event);
+
+ (void)driver_mysql_do_query(db, query, event);
+
+ event_unref(&event);
+}
+
+static void driver_mysql_query(struct sql_db *db, const char *query,
+ sql_query_callback_t *callback, void *context)
+{
+ struct sql_result *result;
+
+ result = sql_query_s(db, query);
+ result->callback = TRUE;
+ callback(result, context);
+ result->callback = FALSE;
+ sql_result_unref(result);
+}
+
+static struct sql_result *
+driver_mysql_query_s(struct sql_db *_db, const char *query)
+{
+ struct mysql_db *db = (struct mysql_db *)_db;
+ struct mysql_result *result;
+ struct event *event;
+ int ret;
+
+ result = i_new(struct mysql_result, 1);
+ result->api = driver_mysql_result;
+ event = event_create(_db->event);
+
+ if (driver_mysql_do_query(db, query, event) < 0)
+ result->api = driver_mysql_error_result;
+ else {
+ /* query ok */
+ result->affected_rows = mysql_affected_rows(db->mysql);
+ result->result = mysql_store_result(db->mysql);
+#ifdef CLIENT_MULTI_RESULTS
+ /* Because we've enabled CLIENT_MULTI_RESULTS, we need to read
+ (ignore) extra results - there should not be any.
+ ret is: -1 = done, >0 = error, 0 = more results. */
+ while ((ret = mysql_next_result(db->mysql)) == 0) ;
+#else
+ ret = -1;
+#endif
+
+ if (ret < 0 &&
+ (result->result != NULL || mysql_errno(db->mysql) == 0)) {
+ /* ok */
+ } else {
+ /* failed */
+ if (result->result != NULL)
+ mysql_free_result(result->result);
+ result->api = driver_mysql_error_result;
+ }
+ }
+
+ result->api.db = _db;
+ result->api.refcount = 1;
+ result->api.event = event;
+ return &result->api;
+}
+
+static void driver_mysql_result_free(struct sql_result *_result)
+{
+ struct mysql_result *result = (struct mysql_result *)_result;
+
+ i_assert(_result != &sql_not_connected_result);
+ if (_result->callback)
+ return;
+
+ if (result->result != NULL)
+ mysql_free_result(result->result);
+ event_unref(&_result->event);
+ i_free(result);
+}
+
+static int driver_mysql_result_next_row(struct sql_result *_result)
+{
+ struct mysql_result *result = (struct mysql_result *)_result;
+ struct mysql_db *db = (struct mysql_db *)_result->db;
+ int ret;
+
+ if (result->result == NULL) {
+ /* no results */
+ return 0;
+ }
+
+ result->row = mysql_fetch_row(result->result);
+ if (result->row != NULL)
+ ret = 1;
+ else {
+ if (mysql_errno(db->mysql) != 0)
+ return -1;
+ ret = 0;
+ }
+ db->last_success = ioloop_time;
+ return ret;
+}
+
+static void driver_mysql_result_fetch_fields(struct mysql_result *result)
+{
+ if (result->fields != NULL)
+ return;
+
+ result->fields_count = mysql_num_fields(result->result);
+ result->fields = mysql_fetch_fields(result->result);
+}
+
+static unsigned int
+driver_mysql_result_get_fields_count(struct sql_result *_result)
+{
+ struct mysql_result *result = (struct mysql_result *)_result;
+
+ driver_mysql_result_fetch_fields(result);
+ return result->fields_count;
+}
+
+static const char *
+driver_mysql_result_get_field_name(struct sql_result *_result, unsigned int idx)
+{
+ struct mysql_result *result = (struct mysql_result *)_result;
+
+ driver_mysql_result_fetch_fields(result);
+ i_assert(idx < result->fields_count);
+ return result->fields[idx].name;
+}
+
+static int driver_mysql_result_find_field(struct sql_result *_result,
+ const char *field_name)
+{
+ struct mysql_result *result = (struct mysql_result *)_result;
+ unsigned int i;
+
+ driver_mysql_result_fetch_fields(result);
+ for (i = 0; i < result->fields_count; i++) {
+ if (strcmp(result->fields[i].name, field_name) == 0)
+ return i;
+ }
+ return -1;
+}
+
+static const char *
+driver_mysql_result_get_field_value(struct sql_result *_result,
+ unsigned int idx)
+{
+ struct mysql_result *result = (struct mysql_result *)_result;
+
+ return (const char *)result->row[idx];
+}
+
+static const unsigned char *
+driver_mysql_result_get_field_value_binary(struct sql_result *_result,
+ unsigned int idx, size_t *size_r)
+{
+ struct mysql_result *result = (struct mysql_result *)_result;
+ unsigned long *lengths;
+
+ lengths = mysql_fetch_lengths(result->result);
+
+ *size_r = lengths[idx];
+ return (const void *)result->row[idx];
+}
+
+static const char *
+driver_mysql_result_find_field_value(struct sql_result *result,
+ const char *field_name)
+{
+ int idx;
+
+ idx = driver_mysql_result_find_field(result, field_name);
+ if (idx < 0)
+ return NULL;
+ return driver_mysql_result_get_field_value(result, idx);
+}
+
+static const char *const *
+driver_mysql_result_get_values(struct sql_result *_result)
+{
+ struct mysql_result *result = (struct mysql_result *)_result;
+
+ return (const char *const *)result->row;
+}
+
+static const char *driver_mysql_result_get_error(struct sql_result *_result)
+{
+ struct mysql_db *db = (struct mysql_db *)_result->db;
+ const char *errstr;
+ unsigned int idle_time;
+ int err;
+
+ err = mysql_errno(db->mysql);
+ errstr = mysql_error(db->mysql);
+ if ((err == CR_SERVER_GONE_ERROR || err == CR_SERVER_LOST) &&
+ db->last_success != 0) {
+ idle_time = ioloop_time - db->last_success;
+ errstr = t_strdup_printf("%s (idled for %u secs)",
+ errstr, idle_time);
+ }
+ return errstr;
+}
+
+static struct sql_transaction_context *
+driver_mysql_transaction_begin(struct sql_db *db)
+{
+ struct mysql_transaction_context *ctx;
+
+ ctx = i_new(struct mysql_transaction_context, 1);
+ ctx->ctx.db = db;
+ ctx->query_pool = pool_alloconly_create("mysql transaction", 1024);
+ ctx->ctx.event = event_create(db->event);
+ return &ctx->ctx;
+}
+
+static void
+driver_mysql_transaction_commit(struct sql_transaction_context *ctx,
+ sql_commit_callback_t *callback, void *context)
+{
+ struct sql_commit_result result;
+ const char *error;
+
+ i_zero(&result);
+ if (sql_transaction_commit_s(&ctx, &error) < 0)
+ result.error = error;
+ callback(&result, context);
+}
+
+static int ATTR_NULL(3)
+transaction_send_query(struct mysql_transaction_context *ctx, const char *query,
+ unsigned int *affected_rows_r)
+{
+ struct sql_result *_result;
+ int ret = 0;
+
+ if (ctx->failed)
+ return -1;
+
+ _result = sql_query_s(ctx->ctx.db, query);
+ if (sql_result_next_row(_result) < 0) {
+ ctx->error = sql_result_get_error(_result);
+ ctx->failed = TRUE;
+ ret = -1;
+ } else if (affected_rows_r != NULL) {
+ struct mysql_result *result = (struct mysql_result *)_result;
+
+ i_assert(result->affected_rows != (my_ulonglong)-1);
+ *affected_rows_r = result->affected_rows;
+ }
+ sql_result_unref(_result);
+ return ret;
+}
+
+static int driver_mysql_try_commit_s(struct mysql_transaction_context *ctx)
+{
+ struct sql_transaction_context *_ctx = &ctx->ctx;
+ bool multi = _ctx->head != NULL && _ctx->head->next != NULL;
+
+ /* wrap in BEGIN/COMMIT only if transaction has mutiple statements. */
+ if (multi && transaction_send_query(ctx, "BEGIN", NULL) < 0) {
+ if (_ctx->db->state != SQL_DB_STATE_DISCONNECTED)
+ return -1;
+ /* we got disconnected, retry */
+ return 0;
+ } else if (multi) {
+ ctx->commit_started = TRUE;
+ }
+
+ while (_ctx->head != NULL) {
+ if (transaction_send_query(ctx, _ctx->head->query,
+ _ctx->head->affected_rows) < 0)
+ return -1;
+ _ctx->head = _ctx->head->next;
+ }
+ if (multi && transaction_send_query(ctx, "COMMIT", NULL) < 0)
+ return -1;
+ return 1;
+}
+
+static int
+driver_mysql_transaction_commit_s(struct sql_transaction_context *_ctx,
+ const char **error_r)
+{
+ struct mysql_transaction_context *ctx =
+ (struct mysql_transaction_context *)_ctx;
+ struct mysql_db *db = (struct mysql_db *)_ctx->db;
+ int ret = 1;
+
+ *error_r = NULL;
+
+ if (_ctx->head != NULL) {
+ ret = driver_mysql_try_commit_s(ctx);
+ *error_r = t_strdup(ctx->error);
+ if (ret == 0) {
+ e_info(db->api.event, "Disconnected from database, "
+ "retrying commit");
+ if (sql_connect(_ctx->db) >= 0) {
+ ctx->failed = FALSE;
+ ret = driver_mysql_try_commit_s(ctx);
+ }
+ }
+ }
+
+ if (ret > 0)
+ ctx->committed = TRUE;
+
+ sql_transaction_rollback(&_ctx);
+ return ret <= 0 ? -1 : 0;
+}
+
+static void
+driver_mysql_transaction_rollback(struct sql_transaction_context *_ctx)
+{
+ struct mysql_transaction_context *ctx =
+ (struct mysql_transaction_context *)_ctx;
+
+ if (ctx->failed) {
+ bool rolledback = FALSE;
+ const char *orig_error = t_strdup(ctx->error);
+ if (ctx->commit_started) {
+ /* reset failed flag so ROLLBACK is actually sent.
+ otherwise, transaction_send_query() will return
+ without trying to send the query. */
+ ctx->failed = FALSE;
+ if (transaction_send_query(ctx, "ROLLBACK", NULL) < 0)
+ e_debug(event_create_passthrough(_ctx->event)->
+ add_str("error", ctx->error)->event(),
+ "Rollback failed: %s", ctx->error);
+ else
+ rolledback = TRUE;
+ }
+ e_debug(sql_transaction_finished_event(_ctx)->
+ add_str("error", orig_error)->event(),
+ "Transaction failed: %s%s", orig_error,
+ rolledback ? " - Rolled back" : "");
+ } else if (ctx->committed)
+ e_debug(sql_transaction_finished_event(_ctx)->event(),
+ "Transaction committed");
+ else
+ e_debug(sql_transaction_finished_event(_ctx)->
+ add_str("error", "Rolled back")->event(),
+ "Transaction rolled back");
+
+ event_unref(&ctx->ctx.event);
+ pool_unref(&ctx->query_pool);
+ i_free(ctx);
+}
+
+static void
+driver_mysql_update(struct sql_transaction_context *_ctx, const char *query,
+ unsigned int *affected_rows)
+{
+ struct mysql_transaction_context *ctx =
+ (struct mysql_transaction_context *)_ctx;
+
+ sql_transaction_add_query(&ctx->ctx, ctx->query_pool,
+ query, affected_rows);
+}
+
+static const char *
+driver_mysql_escape_blob(struct sql_db *_db ATTR_UNUSED,
+ const unsigned char *data, size_t size)
+{
+ string_t *str = t_str_new(128);
+
+ str_append(str, "X'");
+ binary_to_hex_append(str, data, size);
+ str_append_c(str, '\'');
+ return str_c(str);
+}
+
+const struct sql_db driver_mysql_db = {
+ .name = "mysql",
+ .flags = SQL_DB_FLAG_BLOCKING | SQL_DB_FLAG_POOLED |
+ SQL_DB_FLAG_ON_DUPLICATE_KEY,
+
+ .v = {
+ .init_full = driver_mysql_init_full_v,
+ .deinit = driver_mysql_deinit_v,
+ .connect = driver_mysql_connect,
+ .disconnect = driver_mysql_disconnect,
+ .escape_string = driver_mysql_escape_string,
+ .exec = driver_mysql_exec,
+ .query = driver_mysql_query,
+ .query_s = driver_mysql_query_s,
+
+ .transaction_begin = driver_mysql_transaction_begin,
+ .transaction_commit = driver_mysql_transaction_commit,
+ .transaction_commit_s = driver_mysql_transaction_commit_s,
+ .transaction_rollback = driver_mysql_transaction_rollback,
+
+ .update = driver_mysql_update,
+
+ .escape_blob = driver_mysql_escape_blob,
+ }
+};
+
+const struct sql_result driver_mysql_result = {
+ .v = {
+ .free = driver_mysql_result_free,
+ .next_row = driver_mysql_result_next_row,
+ .get_fields_count = driver_mysql_result_get_fields_count,
+ .get_field_name = driver_mysql_result_get_field_name,
+ .find_field = driver_mysql_result_find_field,
+ .get_field_value = driver_mysql_result_get_field_value,
+ .get_field_value_binary = driver_mysql_result_get_field_value_binary,
+ .find_field_value = driver_mysql_result_find_field_value,
+ .get_values = driver_mysql_result_get_values,
+ .get_error = driver_mysql_result_get_error,
+ }
+};
+
+static int
+driver_mysql_result_error_next_row(struct sql_result *result ATTR_UNUSED)
+{
+ return -1;
+}
+
+const struct sql_result driver_mysql_error_result = {
+ .v = {
+ .free = driver_mysql_result_free,
+ .next_row = driver_mysql_result_error_next_row,
+ .get_error = driver_mysql_result_get_error,
+ },
+ .failed_try_retry = TRUE
+};
+
+const char *driver_mysql_version = DOVECOT_ABI_VERSION;
+
+void driver_mysql_init(void);
+void driver_mysql_deinit(void);
+
+void driver_mysql_init(void)
+{
+ sql_driver_register(&driver_mysql_db);
+}
+
+void driver_mysql_deinit(void)
+{
+ sql_driver_unregister(&driver_mysql_db);
+}
+
+#endif
diff --git a/src/lib-sql/driver-pgsql.c b/src/lib-sql/driver-pgsql.c
new file mode 100644
index 0000000..e4b0c63
--- /dev/null
+++ b/src/lib-sql/driver-pgsql.c
@@ -0,0 +1,1344 @@
+/* Copyright (c) 2004-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "ioloop.h"
+#include "hex-binary.h"
+#include "str.h"
+#include "time-util.h"
+#include "sql-api-private.h"
+#include "llist.h"
+
+#ifdef BUILD_PGSQL
+#include <libpq-fe.h>
+
+#define PGSQL_DNS_WARN_MSECS 500
+
+struct pgsql_db {
+ struct sql_db api;
+
+ pool_t pool;
+ char *connect_string;
+ char *host;
+ PGconn *pg;
+
+ struct io *io;
+ struct timeout *to_connect;
+ enum io_condition io_dir;
+
+ struct pgsql_result *pending_results;
+ struct pgsql_result *cur_result;
+ struct ioloop *ioloop, *orig_ioloop;
+ struct sql_result *sync_result;
+
+ bool (*next_callback)(void *);
+ void *next_context;
+
+ char *error;
+ const char *connect_state;
+
+ bool fatal_error:1;
+};
+
+struct pgsql_binary_value {
+ unsigned char *value;
+ size_t size;
+};
+
+struct pgsql_result {
+ struct sql_result api;
+
+ struct pgsql_result *prev, *next;
+
+ PGresult *pgres;
+ struct timeout *to;
+
+ unsigned int rownum, rows;
+ unsigned int fields_count;
+ const char **fields;
+ const char **values;
+ char *query;
+
+ ARRAY(struct pgsql_binary_value) binary_values;
+
+ sql_query_callback_t *callback;
+ void *context;
+
+ bool timeout:1;
+};
+
+struct pgsql_transaction_context {
+ struct sql_transaction_context ctx;
+ int refcount;
+
+ sql_commit_callback_t *callback;
+ void *context;
+
+ pool_t query_pool;
+ const char *error;
+
+ bool failed:1;
+};
+
+extern const struct sql_db driver_pgsql_db;
+extern const struct sql_result driver_pgsql_result;
+
+static void result_finish(struct pgsql_result *result);
+static void
+transaction_update_callback(struct sql_result *result,
+ struct sql_transaction_query *query);
+
+static struct event_category event_category_pgsql = {
+ .parent = &event_category_sql,
+ .name = "pgsql"
+};
+
+static void driver_pgsql_set_state(struct pgsql_db *db, enum sql_db_state state)
+{
+ i_assert(state == SQL_DB_STATE_BUSY || db->cur_result == NULL);
+
+ /* switch back to original ioloop in case the caller wants to
+ add/remove timeouts */
+ if (db->ioloop != NULL)
+ io_loop_set_current(db->orig_ioloop);
+ sql_db_set_state(&db->api, state);
+ if (db->ioloop != NULL)
+ io_loop_set_current(db->ioloop);
+}
+
+static bool driver_pgsql_next_callback(struct pgsql_db *db)
+{
+ bool (*next_callback)(void *) = db->next_callback;
+ void *next_context = db->next_context;
+
+ if (next_callback == NULL)
+ return FALSE;
+
+ db->next_callback = NULL;
+ db->next_context = NULL;
+ return next_callback(next_context);
+}
+
+static void driver_pgsql_stop_io(struct pgsql_db *db)
+{
+ if (db->io != NULL) {
+ io_remove(&db->io);
+ db->io_dir = 0;
+ }
+}
+
+static void driver_pgsql_close(struct pgsql_db *db)
+{
+ db->io_dir = 0;
+ db->fatal_error = FALSE;
+
+ driver_pgsql_stop_io(db);
+
+ PQfinish(db->pg);
+ db->pg = NULL;
+
+ timeout_remove(&db->to_connect);
+
+ driver_pgsql_set_state(db, SQL_DB_STATE_DISCONNECTED);
+
+ if (db->ioloop != NULL) {
+ /* running a sync query, stop it */
+ io_loop_stop(db->ioloop);
+ }
+ driver_pgsql_next_callback(db);
+}
+
+static const char *last_error(struct pgsql_db *db)
+{
+ const char *msg;
+ size_t len;
+
+ msg = PQerrorMessage(db->pg);
+ if (msg == NULL)
+ return "(no error set)";
+
+ /* Error message should contain trailing \n, we don't want it */
+ len = strlen(msg);
+ return len == 0 || msg[len-1] != '\n' ? msg :
+ t_strndup(msg, len-1);
+}
+
+static void connect_callback(struct pgsql_db *db)
+{
+ enum io_condition io_dir = 0;
+ int ret;
+
+ driver_pgsql_stop_io(db);
+
+ while ((ret = PQconnectPoll(db->pg)) == PGRES_POLLING_ACTIVE)
+ ;
+
+ switch (ret) {
+ case PGRES_POLLING_READING:
+ db->connect_state = "wait for input";
+ io_dir = IO_READ;
+ break;
+ case PGRES_POLLING_WRITING:
+ db->connect_state = "wait for output";
+ io_dir = IO_WRITE;
+ break;
+ case PGRES_POLLING_OK:
+ break;
+ case PGRES_POLLING_FAILED:
+ e_error(db->api.event, "Connect failed to database %s: %s (state: %s)",
+ PQdb(db->pg), last_error(db), db->connect_state);
+ driver_pgsql_close(db);
+ return;
+ }
+
+ if (io_dir != 0) {
+ db->io = io_add(PQsocket(db->pg), io_dir, connect_callback, db);
+ db->io_dir = io_dir;
+ }
+
+ if (io_dir == 0) {
+ db->connect_state = "connected";
+ timeout_remove(&db->to_connect);
+ if (PQserverVersion(db->pg) >= 90500) {
+ /* v9.5+ */
+ db->api.flags |= SQL_DB_FLAG_ON_CONFLICT_DO;
+ }
+ driver_pgsql_set_state(db, SQL_DB_STATE_IDLE);
+ if (db->ioloop != NULL) {
+ /* driver_pgsql_sync_init() waiting for connection to
+ finish */
+ io_loop_stop(db->ioloop);
+ }
+ }
+}
+
+static void driver_pgsql_connect_timeout(struct pgsql_db *db)
+{
+ unsigned int secs = ioloop_time - db->api.last_connect_try;
+
+ e_error(db->api.event, "Connect failed: Timeout after %u seconds (state: %s)",
+ secs, db->connect_state);
+ driver_pgsql_close(db);
+}
+
+static int driver_pgsql_connect(struct sql_db *_db)
+{
+ struct pgsql_db *db = (struct pgsql_db *)_db;
+ struct timeval tv_start;
+ int msecs;
+
+ i_assert(db->api.state == SQL_DB_STATE_DISCONNECTED);
+
+ io_loop_time_refresh();
+ tv_start = ioloop_timeval;
+
+ db->pg = PQconnectStart(db->connect_string);
+ if (db->pg == NULL) {
+ i_fatal("pgsql: PQconnectStart() failed (out of memory)");
+ }
+
+ if (PQstatus(db->pg) == CONNECTION_BAD) {
+ e_error(_db->event, "Connect failed to database %s: %s",
+ PQdb(db->pg), last_error(db));
+ driver_pgsql_close(db);
+ return -1;
+ }
+ /* PQconnectStart() blocks on host name resolving. Log a warning if
+ it takes too long. Also don't include time spent on that in the
+ connect timeout (by refreshing ioloop time). */
+ io_loop_time_refresh();
+ msecs = timeval_diff_msecs(&ioloop_timeval, &tv_start);
+ if (msecs > PGSQL_DNS_WARN_MSECS) {
+ e_warning(_db->event, "DNS lookup took %d.%03d s",
+ msecs/1000, msecs % 1000);
+ }
+
+ /* nonblocking connecting begins. */
+ if (PQsetnonblocking(db->pg, 1) < 0)
+ e_error(_db->event, "PQsetnonblocking() failed");
+ i_assert(db->to_connect == NULL);
+ db->to_connect = timeout_add(SQL_CONNECT_TIMEOUT_SECS * 1000,
+ driver_pgsql_connect_timeout, db);
+ db->connect_state = "connecting";
+ db->io = io_add(PQsocket(db->pg), IO_WRITE, connect_callback, db);
+ db->io_dir = IO_WRITE;
+ driver_pgsql_set_state(db, SQL_DB_STATE_CONNECTING);
+ return 0;
+}
+
+static void driver_pgsql_disconnect(struct sql_db *_db)
+{
+ struct pgsql_db *db = (struct pgsql_db *)_db;
+
+ if (db->cur_result != NULL && db->cur_result->to != NULL) {
+ driver_pgsql_stop_io(db);
+ result_finish(db->cur_result);
+ }
+
+ _db->no_reconnect = TRUE;
+ driver_pgsql_close(db);
+ _db->no_reconnect = FALSE;
+}
+
+static void driver_pgsql_free(struct pgsql_db **_db)
+{
+ struct pgsql_db *db = *_db;
+ *_db = NULL;
+
+ event_unref(&db->api.event);
+ i_free(db->connect_string);
+ i_free(db->host);
+ i_free(db->error);
+ array_free(&db->api.module_contexts);
+ i_free(db);
+}
+
+static enum sql_db_flags driver_pgsql_get_flags(struct sql_db *db)
+{
+ switch (db->state) {
+ case SQL_DB_STATE_DISCONNECTED:
+ if (sql_connect(db) < 0)
+ break;
+ /* fall through */
+ case SQL_DB_STATE_CONNECTING:
+ /* Wait for connection to finish, so we can get the flags
+ reliably. */
+ sql_wait(db);
+ break;
+ case SQL_DB_STATE_IDLE:
+ case SQL_DB_STATE_BUSY:
+ break;
+ }
+ return db->flags;
+}
+
+static int driver_pgsql_init_full_v(const struct sql_settings *set,
+ struct sql_db **db_r, const char **error_r ATTR_UNUSED)
+{
+ struct pgsql_db *db;
+
+ db = i_new(struct pgsql_db, 1);
+ db->connect_string = i_strdup(set->connect_string);
+ db->api = driver_pgsql_db;
+ db->api.event = event_create(set->event_parent);
+ event_add_category(db->api.event, &event_category_pgsql);
+
+ /* NOTE: Connection string will be parsed by pgsql itself
+ We only pick the host part here */
+ T_BEGIN {
+ const char *const *arg = t_strsplit(db->connect_string, " ");
+
+ for (; *arg != NULL; arg++) {
+ if (str_begins(*arg, "host="))
+ db->host = i_strdup(*arg + 5);
+
+ }
+ } T_END;
+
+ event_set_append_log_prefix(db->api.event, t_strdup_printf("pgsql(%s): ", db->host));
+
+ *db_r = &db->api;
+ return 0;
+}
+
+static void driver_pgsql_deinit_v(struct sql_db *_db)
+{
+ struct pgsql_db *db = (struct pgsql_db *)_db;
+
+ driver_pgsql_disconnect(_db);
+ driver_pgsql_free(&db);
+}
+
+static void driver_pgsql_set_idle(struct pgsql_db *db)
+{
+ i_assert(db->api.state == SQL_DB_STATE_BUSY);
+
+ if (db->fatal_error)
+ driver_pgsql_close(db);
+ else if (!driver_pgsql_next_callback(db))
+ driver_pgsql_set_state(db, SQL_DB_STATE_IDLE);
+}
+
+static void consume_results(struct pgsql_db *db)
+{
+ PGresult *pgres;
+
+ driver_pgsql_stop_io(db);
+
+ while (PQconsumeInput(db->pg) != 0) {
+ if (PQisBusy(db->pg) != 0) {
+ db->io = io_add(PQsocket(db->pg), IO_READ,
+ consume_results, db);
+ db->io_dir = IO_READ;
+ return;
+ }
+
+ pgres = PQgetResult(db->pg);
+ if (pgres == NULL)
+ break;
+ PQclear(pgres);
+ }
+
+ if (PQstatus(db->pg) == CONNECTION_BAD)
+ driver_pgsql_close(db);
+ else
+ driver_pgsql_set_idle(db);
+}
+
+static void driver_pgsql_result_free(struct sql_result *_result)
+{
+ struct pgsql_db *db = (struct pgsql_db *)_result->db;
+ struct pgsql_result *result = (struct pgsql_result *)_result;
+ bool success;
+
+ i_assert(!result->api.callback);
+ i_assert(db->cur_result == result);
+ i_assert(result->callback == NULL);
+
+ if (_result == db->sync_result)
+ db->sync_result = NULL;
+ db->cur_result = NULL;
+
+ success = result->pgres != NULL && !db->fatal_error;
+ if (result->pgres != NULL) {
+ PQclear(result->pgres);
+ result->pgres = NULL;
+ }
+
+ if (success) {
+ /* we'll have to read the rest of the results as well */
+ i_assert(db->io == NULL);
+ consume_results(db);
+ } else {
+ driver_pgsql_set_idle(db);
+ }
+
+ if (array_is_created(&result->binary_values)) {
+ struct pgsql_binary_value *value;
+
+ array_foreach_modifiable(&result->binary_values, value)
+ PQfreemem(value->value);
+ array_free(&result->binary_values);
+ }
+
+ event_unref(&result->api.event);
+ i_free(result->query);
+ i_free(result->fields);
+ i_free(result->values);
+ i_free(result);
+}
+
+static void result_finish(struct pgsql_result *result)
+{
+ struct pgsql_db *db = (struct pgsql_db *)result->api.db;
+ bool free_result = TRUE;
+ int duration;
+
+ i_assert(db->io == NULL);
+ timeout_remove(&result->to);
+ DLLIST_REMOVE(&db->pending_results, result);
+
+ /* if connection to server was lost, we don't yet see that the
+ connection is bad. we only see the fatal error, so assume it also
+ means disconnection. */
+ if (PQstatus(db->pg) == CONNECTION_BAD || result->pgres == NULL ||
+ PQresultStatus(result->pgres) == PGRES_FATAL_ERROR)
+ db->fatal_error = TRUE;
+
+ if (db->fatal_error) {
+ result->api.failed = TRUE;
+ result->api.failed_try_retry = TRUE;
+ }
+
+ /* emit event */
+ if (result->api.failed) {
+ const char *error = result->timeout ? "Timed out" : last_error(db);
+ struct event_passthrough *e =
+ sql_query_finished_event(&db->api, result->api.event,
+ result->query, TRUE, &duration);
+ e->add_str("error", error);
+ e_debug(e->event(), SQL_QUERY_FINISHED_FMT": %s", result->query,
+ duration, error);
+ } else {
+ e_debug(sql_query_finished_event(&db->api, result->api.event,
+ result->query, FALSE, &duration)->
+ event(),
+ SQL_QUERY_FINISHED_FMT, result->query, duration);
+ }
+ result->api.callback = TRUE;
+ T_BEGIN {
+ if (result->callback != NULL)
+ result->callback(&result->api, result->context);
+ } T_END;
+ result->api.callback = FALSE;
+
+ free_result = db->sync_result != &result->api;
+ if (db->ioloop != NULL)
+ io_loop_stop(db->ioloop);
+
+ i_assert(!free_result || result->api.refcount > 0);
+ result->callback = NULL;
+ if (free_result)
+ sql_result_unref(&result->api);
+}
+
+static void get_result(struct pgsql_result *result)
+{
+ struct pgsql_db *db = (struct pgsql_db *)result->api.db;
+
+ driver_pgsql_stop_io(db);
+
+ if (PQconsumeInput(db->pg) == 0) {
+ result_finish(result);
+ return;
+ }
+
+ if (PQisBusy(db->pg) != 0) {
+ db->io = io_add(PQsocket(db->pg), IO_READ,
+ get_result, result);
+ db->io_dir = IO_READ;
+ return;
+ }
+
+ result->pgres = PQgetResult(db->pg);
+ result_finish(result);
+}
+
+static void flush_callback(struct pgsql_result *result)
+{
+ struct pgsql_db *db = (struct pgsql_db *)result->api.db;
+ int ret;
+
+ driver_pgsql_stop_io(db);
+
+ ret = PQflush(db->pg);
+ if (ret > 0) {
+ db->io = io_add(PQsocket(db->pg), IO_WRITE,
+ flush_callback, result);
+ db->io_dir = IO_WRITE;
+ return;
+ }
+
+ if (ret < 0) {
+ result_finish(result);
+ } else {
+ /* all flushed */
+ get_result(result);
+ }
+}
+
+static void query_timeout(struct pgsql_result *result)
+{
+ struct pgsql_db *db = (struct pgsql_db *)result->api.db;
+
+ driver_pgsql_stop_io(db);
+
+ result->timeout = TRUE;
+ result_finish(result);
+}
+
+static void do_query(struct pgsql_result *result, const char *query)
+{
+ struct pgsql_db *db = (struct pgsql_db *)result->api.db;
+ int ret;
+
+ i_assert(SQL_DB_IS_READY(&db->api));
+ i_assert(db->cur_result == NULL);
+ i_assert(db->io == NULL);
+
+ driver_pgsql_set_state(db, SQL_DB_STATE_BUSY);
+ db->cur_result = result;
+ DLLIST_PREPEND(&db->pending_results, result);
+ result->to = timeout_add(SQL_QUERY_TIMEOUT_SECS * 1000,
+ query_timeout, result);
+ result->query = i_strdup(query);
+
+ if (PQsendQuery(db->pg, query) == 0 ||
+ (ret = PQflush(db->pg)) < 0) {
+ /* failed to send query */
+ result_finish(result);
+ return;
+ }
+
+ if (ret > 0) {
+ /* write blocks */
+ db->io = io_add(PQsocket(db->pg), IO_WRITE,
+ flush_callback, result);
+ db->io_dir = IO_WRITE;
+ } else {
+ get_result(result);
+ }
+}
+
+static const char *
+driver_pgsql_escape_string(struct sql_db *_db, const char *string)
+{
+ struct pgsql_db *db = (struct pgsql_db *)_db;
+ size_t len = strlen(string);
+ char *to;
+
+#ifdef HAVE_PQESCAPE_STRING_CONN
+ if (db->api.state == SQL_DB_STATE_DISCONNECTED) {
+ /* try connecting again */
+ (void)sql_connect(&db->api);
+ }
+ if (db->api.state != SQL_DB_STATE_DISCONNECTED) {
+ int error;
+
+ to = t_buffer_get(len * 2 + 1);
+ len = PQescapeStringConn(db->pg, to, string, len, &error);
+ } else
+#endif
+ {
+ to = t_buffer_get(len * 2 + 1);
+ len = PQescapeString(to, string, len);
+ }
+ t_buffer_alloc(len + 1);
+ return to;
+}
+
+static void exec_callback(struct sql_result *_result,
+ void *context ATTR_UNUSED)
+{
+ struct pgsql_result *result = (struct pgsql_result*)_result;
+ result_finish(result);
+}
+
+static void driver_pgsql_exec(struct sql_db *db, const char *query)
+{
+ struct pgsql_result *result;
+
+ result = i_new(struct pgsql_result, 1);
+ result->api = driver_pgsql_result;
+ result->api.db = db;
+ result->api.refcount = 1;
+ result->api.event = event_create(db->event);
+ result->callback = exec_callback;
+ do_query(result, query);
+}
+
+static void driver_pgsql_query(struct sql_db *db, const char *query,
+ sql_query_callback_t *callback, void *context)
+{
+ struct pgsql_result *result;
+
+ result = i_new(struct pgsql_result, 1);
+ result->api = driver_pgsql_result;
+ result->api.db = db;
+ result->api.refcount = 1;
+ result->api.event = event_create(db->event);
+ result->callback = callback;
+ result->context = context;
+ do_query(result, query);
+}
+
+static void pgsql_query_s_callback(struct sql_result *result, void *context)
+{
+ struct pgsql_db *db = context;
+
+ db->sync_result = result;
+}
+
+static void driver_pgsql_sync_init(struct pgsql_db *db)
+{
+ bool add_to_connect;
+
+ db->orig_ioloop = current_ioloop;
+ if (db->io == NULL) {
+ db->ioloop = io_loop_create();
+ return;
+ }
+
+ i_assert(db->api.state == SQL_DB_STATE_CONNECTING);
+
+ /* have to move our existing I/O and timeout handlers to new I/O loop */
+ io_remove(&db->io);
+
+ add_to_connect = (db->to_connect != NULL);
+ timeout_remove(&db->to_connect);
+
+ db->ioloop = io_loop_create();
+ if (add_to_connect) {
+ db->to_connect = timeout_add(SQL_CONNECT_TIMEOUT_SECS * 1000,
+ driver_pgsql_connect_timeout, db);
+ }
+ db->io = io_add(PQsocket(db->pg), db->io_dir, connect_callback, db);
+ /* wait for connecting to finish */
+ io_loop_run(db->ioloop);
+}
+
+static void driver_pgsql_sync_deinit(struct pgsql_db *db)
+{
+ io_loop_destroy(&db->ioloop);
+}
+
+static struct sql_result *
+driver_pgsql_sync_query(struct pgsql_db *db, const char *query)
+{
+ struct sql_result *result;
+
+ i_assert(db->sync_result == NULL);
+
+ switch (db->api.state) {
+ case SQL_DB_STATE_CONNECTING:
+ case SQL_DB_STATE_BUSY:
+ i_unreached();
+ case SQL_DB_STATE_DISCONNECTED:
+ sql_not_connected_result.refcount++;
+ return &sql_not_connected_result;
+ case SQL_DB_STATE_IDLE:
+ break;
+ }
+
+ driver_pgsql_query(&db->api, query, pgsql_query_s_callback, db);
+ if (db->sync_result == NULL)
+ io_loop_run(db->ioloop);
+
+ i_assert(db->io == NULL);
+
+ result = db->sync_result;
+ if (result == &sql_not_connected_result) {
+ /* we don't end up in pgsql's free function, so sync_result
+ won't be set to NULL if we don't do it here. */
+ db->sync_result = NULL;
+ } else if (result == NULL) {
+ result = &sql_not_connected_result;
+ result->refcount++;
+ }
+
+ i_assert(db->io == NULL);
+ return result;
+}
+
+static struct sql_result *
+driver_pgsql_query_s(struct sql_db *_db, const char *query)
+{
+ struct pgsql_db *db = (struct pgsql_db *)_db;
+ struct sql_result *result;
+
+ driver_pgsql_sync_init(db);
+ result = driver_pgsql_sync_query(db, query);
+ driver_pgsql_sync_deinit(db);
+ return result;
+}
+
+static int driver_pgsql_result_next_row(struct sql_result *_result)
+{
+ struct pgsql_result *result = (struct pgsql_result *)_result;
+ struct pgsql_db *db = (struct pgsql_db *)_result->db;
+
+ if (result->rows != 0) {
+ /* second time we're here */
+ if (++result->rownum < result->rows)
+ return 1;
+
+ /* end of this packet. see if there's more. FIXME: this may
+ block, but the current API doesn't provide a non-blocking
+ way to do this.. */
+ PQclear(result->pgres);
+ result->pgres = PQgetResult(db->pg);
+ if (result->pgres == NULL)
+ return 0;
+ }
+
+ if (result->pgres == NULL) {
+ _result->failed = TRUE;
+ return -1;
+ }
+
+ switch (PQresultStatus(result->pgres)) {
+ case PGRES_COMMAND_OK:
+ /* no rows returned */
+ return 0;
+ case PGRES_TUPLES_OK:
+ result->rows = PQntuples(result->pgres);
+ return result->rows > 0 ? 1 : 0;
+ case PGRES_EMPTY_QUERY:
+ case PGRES_NONFATAL_ERROR:
+ /* nonfatal error */
+ _result->failed = TRUE;
+ return -1;
+ default:
+ /* treat as fatal error */
+ _result->failed = TRUE;
+ db->fatal_error = TRUE;
+ return -1;
+ }
+}
+
+static void driver_pgsql_result_fetch_fields(struct pgsql_result *result)
+{
+ unsigned int i;
+
+ if (result->fields != NULL)
+ return;
+
+ /* @UNSAFE */
+ result->fields_count = PQnfields(result->pgres);
+ result->fields = i_new(const char *, result->fields_count);
+ for (i = 0; i < result->fields_count; i++)
+ result->fields[i] = PQfname(result->pgres, i);
+}
+
+static unsigned int
+driver_pgsql_result_get_fields_count(struct sql_result *_result)
+{
+ struct pgsql_result *result = (struct pgsql_result *)_result;
+
+ driver_pgsql_result_fetch_fields(result);
+ return result->fields_count;
+}
+
+static const char *
+driver_pgsql_result_get_field_name(struct sql_result *_result, unsigned int idx)
+{
+ struct pgsql_result *result = (struct pgsql_result *)_result;
+
+ driver_pgsql_result_fetch_fields(result);
+ i_assert(idx < result->fields_count);
+ return result->fields[idx];
+}
+
+static int driver_pgsql_result_find_field(struct sql_result *_result,
+ const char *field_name)
+{
+ struct pgsql_result *result = (struct pgsql_result *)_result;
+ unsigned int i;
+
+ driver_pgsql_result_fetch_fields(result);
+ for (i = 0; i < result->fields_count; i++) {
+ if (strcmp(result->fields[i], field_name) == 0)
+ return i;
+ }
+ return -1;
+}
+
+static const char *
+driver_pgsql_result_get_field_value(struct sql_result *_result,
+ unsigned int idx)
+{
+ struct pgsql_result *result = (struct pgsql_result *)_result;
+
+ if (PQgetisnull(result->pgres, result->rownum, idx) != 0)
+ return NULL;
+
+ return PQgetvalue(result->pgres, result->rownum, idx);
+}
+
+static const unsigned char *
+driver_pgsql_result_get_field_value_binary(struct sql_result *_result,
+ unsigned int idx, size_t *size_r)
+{
+ struct pgsql_result *result = (struct pgsql_result *)_result;
+ const char *value;
+ struct pgsql_binary_value *binary_value;
+
+ if (PQgetisnull(result->pgres, result->rownum, idx) != 0) {
+ *size_r = 0;
+ return NULL;
+ }
+
+ value = PQgetvalue(result->pgres, result->rownum, idx);
+
+ if (!array_is_created(&result->binary_values))
+ i_array_init(&result->binary_values, idx + 1);
+
+ binary_value = array_idx_get_space(&result->binary_values, idx);
+ if (binary_value->value == NULL) {
+ binary_value->value =
+ PQunescapeBytea((const unsigned char *)value,
+ &binary_value->size);
+ }
+
+ *size_r = binary_value->size;
+ return binary_value->value;
+}
+
+static const char *
+driver_pgsql_result_find_field_value(struct sql_result *result,
+ const char *field_name)
+{
+ int idx;
+
+ idx = driver_pgsql_result_find_field(result, field_name);
+ if (idx < 0)
+ return NULL;
+ return driver_pgsql_result_get_field_value(result, idx);
+}
+
+static const char *const *
+driver_pgsql_result_get_values(struct sql_result *_result)
+{
+ struct pgsql_result *result = (struct pgsql_result *)_result;
+ unsigned int i;
+
+ if (result->values == NULL) {
+ driver_pgsql_result_fetch_fields(result);
+ result->values = i_new(const char *, result->fields_count);
+ }
+
+ /* @UNSAFE */
+ for (i = 0; i < result->fields_count; i++) {
+ result->values[i] =
+ driver_pgsql_result_get_field_value(_result, i);
+ }
+
+ return result->values;
+}
+
+static const char *driver_pgsql_result_get_error(struct sql_result *_result)
+{
+ struct pgsql_result *result = (struct pgsql_result *)_result;
+ struct pgsql_db *db = (struct pgsql_db *)_result->db;
+ const char *msg;
+ size_t len;
+
+ i_free_and_null(db->error);
+
+ if (result->timeout) {
+ db->error = i_strdup("Query timed out");
+ } else if (result->pgres == NULL) {
+ /* connection error */
+ db->error = i_strdup(last_error(db));
+ } else {
+ msg = PQresultErrorMessage(result->pgres);
+ if (msg == NULL)
+ return "(no error set)";
+
+ /* Error message should contain trailing \n, we don't want it */
+ len = strlen(msg);
+ db->error = len == 0 || msg[len-1] != '\n' ?
+ i_strdup(msg) : i_strndup(msg, len-1);
+ }
+ return db->error;
+}
+
+static struct sql_transaction_context *
+driver_pgsql_transaction_begin(struct sql_db *db)
+{
+ struct pgsql_transaction_context *ctx;
+
+ ctx = i_new(struct pgsql_transaction_context, 1);
+ ctx->ctx.db = db;
+ ctx->ctx.event = event_create(db->event);
+ /* we need to be able to handle multiple open transactions, so at least
+ for now just keep them in memory until commit time. */
+ ctx->query_pool = pool_alloconly_create("pgsql transaction", 1024);
+ return &ctx->ctx;
+}
+
+static void
+driver_pgsql_transaction_free(struct pgsql_transaction_context *ctx)
+{
+ pool_unref(&ctx->query_pool);
+ event_unref(&ctx->ctx.event);
+ i_free(ctx);
+}
+
+static void
+transaction_commit_callback(struct sql_result *result,
+ struct pgsql_transaction_context *ctx)
+{
+ struct sql_commit_result commit_result;
+
+ i_zero(&commit_result);
+ if (sql_result_next_row(result) < 0) {
+ commit_result.error = sql_result_get_error(result);
+ commit_result.error_type = sql_result_get_error_type(result);
+ }
+ ctx->callback(&commit_result, ctx->context);
+ driver_pgsql_transaction_free(ctx);
+}
+
+static bool transaction_send_next(void *context)
+{
+ struct pgsql_transaction_context *ctx = context;
+
+ i_assert(!ctx->failed);
+
+ if (ctx->ctx.db->state == SQL_DB_STATE_BUSY) {
+ /* kludgy.. */
+ ctx->ctx.db->state = SQL_DB_STATE_IDLE;
+ } else if (!SQL_DB_IS_READY(ctx->ctx.db)) {
+ struct sql_commit_result commit_result = {
+ .error = "Not connected"
+ };
+ ctx->callback(&commit_result, ctx->context);
+ return FALSE;
+ }
+
+ if (ctx->ctx.head != NULL) {
+ struct sql_transaction_query *query = ctx->ctx.head;
+
+ ctx->ctx.head = ctx->ctx.head->next;
+ sql_query(ctx->ctx.db, query->query,
+ transaction_update_callback, query);
+ } else {
+ sql_query(ctx->ctx.db, "COMMIT",
+ transaction_commit_callback, ctx);
+ }
+ return TRUE;
+}
+
+static void
+transaction_commit_error_callback(struct pgsql_transaction_context *ctx,
+ struct sql_result *result)
+{
+ struct sql_commit_result commit_result;
+
+ i_zero(&commit_result);
+ commit_result.error = sql_result_get_error(result);
+ commit_result.error_type = sql_result_get_error_type(result);
+ e_debug(sql_transaction_finished_event(&ctx->ctx)->
+ add_str("error", commit_result.error)->event(),
+ "Transaction failed: %s", commit_result.error);
+ ctx->callback(&commit_result, ctx->context);
+}
+
+static void
+transaction_begin_callback(struct sql_result *result,
+ struct pgsql_transaction_context *ctx)
+{
+ struct pgsql_db *db = (struct pgsql_db *)result->db;
+
+ i_assert(result->db == ctx->ctx.db);
+
+ if (sql_result_next_row(result) < 0) {
+ transaction_commit_error_callback(ctx, result);
+ driver_pgsql_transaction_free(ctx);
+ return;
+ }
+ i_assert(db->next_callback == NULL);
+ db->next_callback = transaction_send_next;
+ db->next_context = ctx;
+}
+
+static void
+transaction_update_callback(struct sql_result *result,
+ struct sql_transaction_query *query)
+{
+ struct pgsql_transaction_context *ctx =
+ (struct pgsql_transaction_context *)query->trans;
+ struct pgsql_db *db = (struct pgsql_db *)result->db;
+
+ if (sql_result_next_row(result) < 0) {
+ transaction_commit_error_callback(ctx, result);
+ driver_pgsql_transaction_free(ctx);
+ return;
+ }
+
+ if (query->affected_rows != NULL) {
+ struct pgsql_result *pg_result = (struct pgsql_result *)result;
+
+ if (str_to_uint(PQcmdTuples(pg_result->pgres),
+ query->affected_rows) < 0)
+ i_unreached();
+ }
+ i_assert(db->next_callback == NULL);
+ db->next_callback = transaction_send_next;
+ db->next_context = ctx;
+}
+
+static void
+transaction_trans_query_callback(struct sql_result *result,
+ struct sql_transaction_query *query)
+{
+ struct pgsql_transaction_context *ctx =
+ (struct pgsql_transaction_context *)query->trans;
+ struct sql_commit_result commit_result;
+
+ if (sql_result_next_row(result) < 0) {
+ transaction_commit_error_callback(ctx, result);
+ driver_pgsql_transaction_free(ctx);
+ return;
+ }
+
+ if (query->affected_rows != NULL) {
+ struct pgsql_result *pg_result = (struct pgsql_result *)result;
+
+ if (str_to_uint(PQcmdTuples(pg_result->pgres),
+ query->affected_rows) < 0)
+ i_unreached();
+ }
+ e_debug(sql_transaction_finished_event(&ctx->ctx)->event(),
+ "Transaction committed");
+ i_zero(&commit_result);
+ ctx->callback(&commit_result, ctx->context);
+ driver_pgsql_transaction_free(ctx);
+}
+
+static void
+driver_pgsql_transaction_commit(struct sql_transaction_context *_ctx,
+ sql_commit_callback_t *callback, void *context)
+{
+ struct pgsql_transaction_context *ctx =
+ (struct pgsql_transaction_context *)_ctx;
+ struct sql_commit_result result;
+
+ i_zero(&result);
+ ctx->callback = callback;
+ ctx->context = context;
+
+ if (ctx->failed || _ctx->head == NULL) {
+ if (ctx->failed) {
+ result.error = ctx->error;
+ e_debug(sql_transaction_finished_event(_ctx)->
+ add_str("error", ctx->error)->event(),
+ "Transaction failed: %s", ctx->error);
+ } else {
+ e_debug(sql_transaction_finished_event(_ctx)->event(),
+ "Transaction committed");
+ }
+ callback(&result, context);
+ driver_pgsql_transaction_free(ctx);
+ } else if (_ctx->head->next == NULL) {
+ /* just a single query, send it */
+ sql_query(_ctx->db, _ctx->head->query,
+ transaction_trans_query_callback, _ctx->head);
+ } else {
+ /* multiple queries, use a transaction */
+ i_assert(_ctx->db->v.query == driver_pgsql_query);
+ sql_query(_ctx->db, "BEGIN", transaction_begin_callback, ctx);
+ }
+}
+
+static void
+commit_multi_fail(struct pgsql_transaction_context *ctx,
+ struct sql_result *result, const char *query)
+{
+ ctx->failed = TRUE;
+ ctx->error = t_strdup_printf("%s (query: %s)",
+ sql_result_get_error(result), query);
+ sql_result_unref(result);
+}
+
+static struct sql_result *
+driver_pgsql_transaction_commit_multi(struct pgsql_transaction_context *ctx)
+{
+ struct pgsql_db *db = (struct pgsql_db *)ctx->ctx.db;
+ struct sql_result *result;
+ struct sql_transaction_query *query;
+
+ result = driver_pgsql_sync_query(db, "BEGIN");
+ if (sql_result_next_row(result) < 0) {
+ commit_multi_fail(ctx, result, "BEGIN");
+ return NULL;
+ }
+ sql_result_unref(result);
+
+ /* send queries */
+ for (query = ctx->ctx.head; query != NULL; query = query->next) {
+ result = driver_pgsql_sync_query(db, query->query);
+ if (sql_result_next_row(result) < 0) {
+ commit_multi_fail(ctx, result, query->query);
+ break;
+ }
+ if (query->affected_rows != NULL) {
+ struct pgsql_result *pg_result =
+ (struct pgsql_result *)result;
+
+ if (str_to_uint(PQcmdTuples(pg_result->pgres),
+ query->affected_rows) < 0)
+ i_unreached();
+ }
+ sql_result_unref(result);
+ }
+
+ return driver_pgsql_sync_query(db, ctx->failed ?
+ "ROLLBACK" : "COMMIT");
+}
+
+static void
+driver_pgsql_try_commit_s(struct pgsql_transaction_context *ctx,
+ const char **error_r)
+{
+ struct sql_transaction_context *_ctx = &ctx->ctx;
+ struct pgsql_db *db = (struct pgsql_db *)_ctx->db;
+ struct sql_transaction_query *single_query = NULL;
+ struct sql_result *result;
+
+ if (_ctx->head->next == NULL) {
+ /* just a single query, send it */
+ single_query = _ctx->head;
+ result = sql_query_s(_ctx->db, single_query->query);
+ } else {
+ /* multiple queries, use a transaction */
+ driver_pgsql_sync_init(db);
+ result = driver_pgsql_transaction_commit_multi(ctx);
+ driver_pgsql_sync_deinit(db);
+ }
+
+ if (ctx->failed) {
+ i_assert(ctx->error != NULL);
+ e_debug(sql_transaction_finished_event(_ctx)->
+ add_str("error", ctx->error)->event(),
+ "Transaction failed: %s", ctx->error);
+ *error_r = ctx->error;
+ } else if (result != NULL) {
+ if (sql_result_next_row(result) < 0)
+ *error_r = sql_result_get_error(result);
+ else if (single_query != NULL &&
+ single_query->affected_rows != NULL) {
+ struct pgsql_result *pg_result =
+ (struct pgsql_result *)result;
+
+ if (str_to_uint(PQcmdTuples(pg_result->pgres),
+ single_query->affected_rows) < 0)
+ i_unreached();
+ }
+ }
+
+ if (!ctx->failed) {
+ e_debug(sql_transaction_finished_event(_ctx)->event(),
+ "Transaction committed");
+ }
+
+ if (result != NULL)
+ sql_result_unref(result);
+}
+
+static int
+driver_pgsql_transaction_commit_s(struct sql_transaction_context *_ctx,
+ const char **error_r)
+{
+ struct pgsql_transaction_context *ctx =
+ (struct pgsql_transaction_context *)_ctx;
+ struct pgsql_db *db = (struct pgsql_db *)_ctx->db;
+
+ *error_r = NULL;
+
+ if (_ctx->head != NULL) {
+ driver_pgsql_try_commit_s(ctx, error_r);
+ if (_ctx->db->state == SQL_DB_STATE_DISCONNECTED) {
+ *error_r = t_strdup(*error_r);
+ e_info(db->api.event, "Disconnected from database, "
+ "retrying commit");
+ if (sql_connect(_ctx->db) >= 0) {
+ ctx->failed = FALSE;
+ *error_r = NULL;
+ driver_pgsql_try_commit_s(ctx, error_r);
+ }
+ }
+ }
+
+ driver_pgsql_transaction_free(ctx);
+ return *error_r == NULL ? 0 : -1;
+}
+
+static void
+driver_pgsql_transaction_rollback(struct sql_transaction_context *_ctx)
+{
+ struct pgsql_transaction_context *ctx =
+ (struct pgsql_transaction_context *)_ctx;
+ e_debug(sql_transaction_finished_event(_ctx)->
+ add_str("error", "Rolled back")->event(),
+ "Transaction rolled back");
+
+ driver_pgsql_transaction_free(ctx);
+}
+
+static void
+driver_pgsql_update(struct sql_transaction_context *_ctx, const char *query,
+ unsigned int *affected_rows)
+{
+ struct pgsql_transaction_context *ctx =
+ (struct pgsql_transaction_context *)_ctx;
+
+ sql_transaction_add_query(_ctx, ctx->query_pool, query, affected_rows);
+}
+
+static const char *
+driver_pgsql_escape_blob(struct sql_db *_db ATTR_UNUSED,
+ const unsigned char *data, size_t size)
+{
+ string_t *str = t_str_new(128);
+
+ str_append(str, "E'\\\\x");
+ binary_to_hex_append(str, data, size);
+ str_append_c(str, '\'');
+ return str_c(str);
+}
+
+static bool driver_pgsql_have_work(struct pgsql_db *db)
+{
+ return db->next_callback != NULL || db->pending_results != NULL ||
+ db->api.state == SQL_DB_STATE_CONNECTING;
+}
+
+static void driver_pgsql_wait(struct sql_db *_db)
+{
+ struct pgsql_db *db = (struct pgsql_db *)_db;
+
+ if (!driver_pgsql_have_work(db))
+ return;
+
+ struct ioloop *prev_ioloop = current_ioloop;
+ db->ioloop = io_loop_create();
+ db->io = io_loop_move_io(&db->io);
+ while (driver_pgsql_have_work(db))
+ io_loop_run(db->ioloop);
+
+ io_loop_set_current(prev_ioloop);
+ db->io = io_loop_move_io(&db->io);
+ io_loop_set_current(db->ioloop);
+ io_loop_destroy(&db->ioloop);
+}
+
+const struct sql_db driver_pgsql_db = {
+ .name = "pgsql",
+ .flags = SQL_DB_FLAG_POOLED,
+
+ .v = {
+ .get_flags = driver_pgsql_get_flags,
+ .init_full = driver_pgsql_init_full_v,
+ .deinit = driver_pgsql_deinit_v,
+ .connect = driver_pgsql_connect,
+ .disconnect = driver_pgsql_disconnect,
+ .escape_string = driver_pgsql_escape_string,
+ .exec = driver_pgsql_exec,
+ .query = driver_pgsql_query,
+ .query_s = driver_pgsql_query_s,
+ .wait = driver_pgsql_wait,
+
+ .transaction_begin = driver_pgsql_transaction_begin,
+ .transaction_commit = driver_pgsql_transaction_commit,
+ .transaction_commit_s = driver_pgsql_transaction_commit_s,
+ .transaction_rollback = driver_pgsql_transaction_rollback,
+
+ .update = driver_pgsql_update,
+
+ .escape_blob = driver_pgsql_escape_blob,
+ }
+};
+
+const struct sql_result driver_pgsql_result = {
+ .v = {
+ .free = driver_pgsql_result_free,
+ .next_row = driver_pgsql_result_next_row,
+ .get_fields_count = driver_pgsql_result_get_fields_count,
+ .get_field_name = driver_pgsql_result_get_field_name,
+ .find_field = driver_pgsql_result_find_field,
+ .get_field_value = driver_pgsql_result_get_field_value,
+ .get_field_value_binary = driver_pgsql_result_get_field_value_binary,
+ .find_field_value = driver_pgsql_result_find_field_value,
+ .get_values = driver_pgsql_result_get_values,
+ .get_error = driver_pgsql_result_get_error,
+ }
+};
+
+const char *driver_pgsql_version = DOVECOT_ABI_VERSION;
+
+void driver_pgsql_init(void);
+void driver_pgsql_deinit(void);
+
+void driver_pgsql_init(void)
+{
+ sql_driver_register(&driver_pgsql_db);
+}
+
+void driver_pgsql_deinit(void)
+{
+ sql_driver_unregister(&driver_pgsql_db);
+}
+
+#endif
diff --git a/src/lib-sql/driver-sqlite.c b/src/lib-sql/driver-sqlite.c
new file mode 100644
index 0000000..e05ed18
--- /dev/null
+++ b/src/lib-sql/driver-sqlite.c
@@ -0,0 +1,555 @@
+/* Copyright (c) 2006-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "ioloop.h"
+#include "str.h"
+#include "hex-binary.h"
+#include "sql-api-private.h"
+
+#ifdef BUILD_SQLITE
+#include <sqlite3.h>
+
+/* retry time if db is busy (in ms) */
+static const int sqlite_busy_timeout = 1000;
+
+struct sqlite_db {
+ struct sql_db api;
+
+ pool_t pool;
+ const char *dbfile;
+ sqlite3 *sqlite;
+ bool connected:1;
+ int rc;
+};
+
+struct sqlite_result {
+ struct sql_result api;
+ sqlite3_stmt *stmt;
+ unsigned int cols;
+ const char **row;
+};
+
+struct sqlite_transaction_context {
+ struct sql_transaction_context ctx;
+ bool failed:1;
+};
+
+extern const struct sql_db driver_sqlite_db;
+extern const struct sql_result driver_sqlite_result;
+extern const struct sql_result driver_sqlite_error_result;
+
+static struct event_category event_category_sqlite = {
+ .parent = &event_category_sql,
+ .name = "sqlite"
+};
+
+static int driver_sqlite_connect(struct sql_db *_db)
+{
+ struct sqlite_db *db = (struct sqlite_db *)_db;
+
+ if (db->connected)
+ return 1;
+
+ db->rc = sqlite3_open(db->dbfile, &db->sqlite);
+
+ if (db->rc == SQLITE_OK) {
+ db->connected = TRUE;
+ sqlite3_busy_timeout(db->sqlite, sqlite_busy_timeout);
+ return 1;
+ } else {
+ e_error(_db->event, "open(%s) failed: %s", db->dbfile,
+ sqlite3_errmsg(db->sqlite));
+ sqlite3_close(db->sqlite);
+ db->sqlite = NULL;
+ return -1;
+ }
+}
+
+static void driver_sqlite_disconnect(struct sql_db *_db)
+{
+ struct sqlite_db *db = (struct sqlite_db *)_db;
+
+ sqlite3_close(db->sqlite);
+ db->sqlite = NULL;
+}
+
+static int driver_sqlite_init_full_v(const struct sql_settings *set, struct sql_db **db_r,
+ const char **error_r ATTR_UNUSED)
+{
+ struct sqlite_db *db;
+ pool_t pool;
+
+ pool = pool_alloconly_create("sqlite driver", 512);
+ db = p_new(pool, struct sqlite_db, 1);
+ db->pool = pool;
+ db->api = driver_sqlite_db;
+ db->dbfile = p_strdup(db->pool, set->connect_string);
+ db->connected = FALSE;
+ db->api.event = event_create(set->event_parent);
+ event_add_category(db->api.event, &event_category_sqlite);
+ event_set_append_log_prefix(db->api.event, "sqlite: ");
+
+ *db_r = &db->api;
+ return 0;
+}
+
+static void driver_sqlite_deinit_v(struct sql_db *_db)
+{
+ struct sqlite_db *db = (struct sqlite_db *)_db;
+
+ _db->no_reconnect = TRUE;
+ sql_db_set_state(&db->api, SQL_DB_STATE_DISCONNECTED);
+
+ sqlite3_close(db->sqlite);
+ sql_connection_log_finished(_db);
+ event_unref(&_db->event);
+ array_free(&_db->module_contexts);
+ pool_unref(&db->pool);
+}
+
+static const char *
+driver_sqlite_escape_string(struct sql_db *_db ATTR_UNUSED,
+ const char *string)
+{
+ const char *p;
+ char *dest, *destbegin;
+
+ /* find the first ' */
+ for (p = string; *p != '\''; p++) {
+ if (*p == '\0')
+ return t_strdup_noconst(string);
+ }
+
+ /* @UNSAFE: escape ' with '' */
+ dest = destbegin = t_buffer_get((p - string) + strlen(string) * 2 + 1);
+
+ memcpy(dest, string, p - string);
+ dest += p - string;
+
+ for (; *p != '\0'; p++) {
+ *dest++ = *p;
+ if (*p == '\'')
+ *dest++ = *p;
+ }
+ *dest++ = '\0';
+ t_buffer_alloc(dest - destbegin);
+
+ return destbegin;
+}
+
+static void driver_sqlite_result_log(const struct sql_result *result, const char *query)
+{
+ struct sqlite_db *db = (struct sqlite_db *)result->db;
+ bool success = db->connected && db->rc == SQLITE_OK;
+ int duration;
+ const char *suffix = "";
+ struct event_passthrough *e =
+ sql_query_finished_event(&db->api, result->event, query, success,
+ &duration);
+ io_loop_time_refresh();
+
+ if (!db->connected) {
+ suffix = ": Cannot connect to database";
+ e->add_str("error", "Cannot connect to database");
+ } else if (db->rc != SQLITE_OK) {
+ suffix = t_strdup_printf(": %s (%d)", sqlite3_errmsg(db->sqlite),
+ db->rc);
+ e->add_str("error", sqlite3_errmsg(db->sqlite));
+ e->add_int("error_code", db->rc);
+ }
+
+ e_debug(e->event(), SQL_QUERY_FINISHED_FMT"%s", query, duration, suffix);
+}
+
+static void driver_sqlite_exec(struct sql_db *_db, const char *query)
+{
+ struct sqlite_db *db = (struct sqlite_db *)_db;
+ struct sql_result result;
+
+ i_zero(&result);
+ result.db = _db;
+ result.event = event_create(_db->event);
+
+ /* Other drivers do not include time spent connecting
+ but this simplifies error logging, so we include
+ it here. */
+ if (driver_sqlite_connect(_db) < 0) {
+ driver_sqlite_result_log(&result, query);
+ } else {
+ db->rc = sqlite3_exec(db->sqlite, query, NULL, NULL, NULL);
+ driver_sqlite_result_log(&result, query);
+ }
+
+ event_unref(&result.event);
+}
+
+static void driver_sqlite_query(struct sql_db *db, const char *query,
+ sql_query_callback_t *callback, void *context)
+{
+ struct sql_result *result;
+
+ result = sql_query_s(db, query);
+ result->callback = TRUE;
+ callback(result, context);
+ result->callback = FALSE;
+ sql_result_unref(result);
+}
+
+static struct sql_result *
+driver_sqlite_query_s(struct sql_db *_db, const char *query)
+{
+ struct sqlite_db *db = (struct sqlite_db *)_db;
+ struct sqlite_result *result;
+ struct event *event;
+
+ result = i_new(struct sqlite_result, 1);
+ result->api.db = _db;
+ /* Temporarily store the event since result->api gets
+ * overwritten later here and we need to reset it. */
+ event = event_create(_db->event);
+ result->api.event = event;
+
+ if (driver_sqlite_connect(_db) < 0) {
+ driver_sqlite_result_log(&result->api, query);
+ result->api = driver_sqlite_error_result;
+ result->stmt = NULL;
+ result->cols = 0;
+ } else {
+ db->rc = sqlite3_prepare(db->sqlite, query, -1, &result->stmt, NULL);
+ driver_sqlite_result_log(&result->api, query);
+ if (db->rc == SQLITE_OK) {
+ result->api = driver_sqlite_result;
+ result->cols = sqlite3_column_count(result->stmt);
+ result->row = i_new(const char *, result->cols);
+ } else {
+ result->api = driver_sqlite_error_result;
+ result->stmt = NULL;
+ result->cols = 0;
+ }
+ }
+
+ result->api.db = _db;
+ result->api.refcount = 1;
+ result->api.event = event;
+ return &result->api;
+}
+
+static void driver_sqlite_result_free(struct sql_result *_result)
+{
+ struct sqlite_result *result = (struct sqlite_result *)_result;
+ struct sqlite_db *db = (struct sqlite_db *) result->api.db;
+ int rc;
+
+ if (_result->callback)
+ return;
+
+ if (result->stmt != NULL) {
+ if ((rc = sqlite3_finalize(result->stmt)) != SQLITE_OK) {
+ e_warning(_result->event, "finalize failed: %s (%d)",
+ sqlite3_errmsg(db->sqlite), rc);
+ }
+ i_free(result->row);
+ }
+ event_unref(&result->api.event);
+ i_free(result);
+}
+
+static int driver_sqlite_result_next_row(struct sql_result *_result)
+{
+ struct sqlite_result *result = (struct sqlite_result *)_result;
+
+ switch (sqlite3_step(result->stmt)) {
+ case SQLITE_ROW:
+ return 1;
+ case SQLITE_DONE:
+ return 0;
+ default:
+ return -1;
+ }
+}
+
+static unsigned int
+driver_sqlite_result_get_fields_count(struct sql_result *_result)
+{
+ struct sqlite_result *result = (struct sqlite_result *)_result;
+
+ return result->cols;
+}
+
+static const char *
+driver_sqlite_result_get_field_name(struct sql_result *_result,
+ unsigned int idx)
+{
+ struct sqlite_result *result = (struct sqlite_result *)_result;
+
+ return sqlite3_column_name(result->stmt, idx);
+}
+
+static int driver_sqlite_result_find_field(struct sql_result *_result,
+ const char *field_name)
+{
+ struct sqlite_result *result = (struct sqlite_result *)_result;
+ unsigned int i;
+
+ for (i = 0; i < result->cols; ++i) {
+ const char *col = sqlite3_column_name(result->stmt, i);
+
+ if (strcmp(col, field_name) == 0)
+ return i;
+ }
+
+ return -1;
+}
+
+static const char *
+driver_sqlite_result_get_field_value(struct sql_result *_result,
+ unsigned int idx)
+{
+ struct sqlite_result *result = (struct sqlite_result *)_result;
+
+ return (const char*)sqlite3_column_text(result->stmt, idx);
+}
+
+static const unsigned char *
+driver_sqlite_result_get_field_value_binary(struct sql_result *_result,
+ unsigned int idx, size_t *size_r)
+{
+ struct sqlite_result *result = (struct sqlite_result *)_result;
+
+ *size_r = sqlite3_column_bytes(result->stmt, idx);
+ return sqlite3_column_blob(result->stmt, idx);
+}
+
+static const char *
+driver_sqlite_result_find_field_value(struct sql_result *result,
+ const char *field_name)
+{
+ int idx;
+
+ idx = driver_sqlite_result_find_field(result, field_name);
+ if (idx < 0)
+ return NULL;
+ return driver_sqlite_result_get_field_value(result, idx);
+}
+
+static const char *const *
+driver_sqlite_result_get_values(struct sql_result *_result)
+{
+ struct sqlite_result *result = (struct sqlite_result *)_result;
+ unsigned int i;
+
+ for (i = 0; i < result->cols; ++i) {
+ result->row[i] =
+ driver_sqlite_result_get_field_value(_result, i);
+ }
+
+ return (const char *const *)result->row;
+}
+
+static const char *driver_sqlite_result_get_error(struct sql_result *_result)
+{
+ struct sqlite_result *result = (struct sqlite_result *)_result;
+ struct sqlite_db *db = (struct sqlite_db *)result->api.db;
+
+ if (db->connected)
+ return sqlite3_errmsg(db->sqlite);
+ else
+ return "Cannot connect to database";
+}
+
+static struct sql_transaction_context *
+driver_sqlite_transaction_begin(struct sql_db *_db)
+{
+ struct sqlite_transaction_context *ctx;
+ struct sqlite_db *db = (struct sqlite_db *)_db;
+
+ ctx = i_new(struct sqlite_transaction_context, 1);
+ ctx->ctx.db = _db;
+ ctx->ctx.event = event_create(_db->event);
+
+ sql_exec(_db, "BEGIN TRANSACTION");
+ if (db->rc != SQLITE_OK)
+ ctx->failed = TRUE;
+
+ return &ctx->ctx;
+}
+
+static void
+driver_sqlite_transaction_rollback(struct sql_transaction_context *_ctx)
+{
+ struct sqlite_transaction_context *ctx =
+ (struct sqlite_transaction_context *)_ctx;
+
+ if (!ctx->failed) {
+ e_debug(sql_transaction_finished_event(_ctx)->
+ add_str("error", "Rolled back")->event(),
+ "Transaction rolled back");
+ }
+ sql_exec(_ctx->db, "ROLLBACK");
+ event_unref(&_ctx->event);
+ i_free(ctx);
+}
+
+static void
+driver_sqlite_transaction_commit(struct sql_transaction_context *_ctx,
+ sql_commit_callback_t *callback, void *context)
+{
+ struct sqlite_transaction_context *ctx =
+ (struct sqlite_transaction_context *)_ctx;
+ struct sqlite_db *db = (struct sqlite_db *)ctx->ctx.db;
+ struct sql_commit_result commit_result;
+
+ if (!ctx->failed) {
+ sql_exec(_ctx->db, "COMMIT");
+ if (db->rc != SQLITE_OK)
+ ctx->failed = TRUE;
+ }
+
+ i_zero(&commit_result);
+ if (ctx->failed) {
+ commit_result.error = sqlite3_errmsg(db->sqlite);
+ callback(&commit_result, context);
+ e_debug(sql_transaction_finished_event(_ctx)->
+ add_str("error", commit_result.error)->event(),
+ "Transaction failed");
+ /* From SQLite manual: It is recommended that applications
+ respond to the errors listed above by explicitly issuing a
+ ROLLBACK command. If the transaction has already been rolled
+ back automatically by the error response, then the ROLLBACK
+ command will fail with an error, but no harm is caused by
+ this. */
+ driver_sqlite_transaction_rollback(_ctx);
+ } else {
+ e_debug(sql_transaction_finished_event(_ctx)->event(),
+ "Transaction committed");
+ callback(&commit_result, context);
+ event_unref(&_ctx->event);
+ i_free(ctx);
+ }
+}
+
+static int
+driver_sqlite_transaction_commit_s(struct sql_transaction_context *_ctx,
+ const char **error_r)
+{
+ struct sqlite_transaction_context *ctx =
+ (struct sqlite_transaction_context *)_ctx;
+ struct sqlite_db *db = (struct sqlite_db *) ctx->ctx.db;
+
+ if (ctx->failed) {
+ /* also does i_free(ctx) */
+ driver_sqlite_transaction_rollback(_ctx);
+ return -1;
+ }
+
+ sql_exec(_ctx->db, "COMMIT");
+ *error_r = sqlite3_errmsg(db->sqlite);
+ i_free(ctx);
+ return 0;
+}
+
+static void
+driver_sqlite_update(struct sql_transaction_context *_ctx, const char *query,
+ unsigned int *affected_rows)
+{
+ struct sqlite_transaction_context *ctx =
+ (struct sqlite_transaction_context *)_ctx;
+ struct sqlite_db *db = (struct sqlite_db *)ctx->ctx.db;
+
+ if (ctx->failed)
+ return;
+
+ sql_exec(_ctx->db, query);
+ if (db->rc != SQLITE_OK)
+ ctx->failed = TRUE;
+ else if (affected_rows != NULL)
+ *affected_rows = sqlite3_changes(db->sqlite);
+}
+
+static const char *
+driver_sqlite_escape_blob(struct sql_db *_db ATTR_UNUSED,
+ const unsigned char *data, size_t size)
+{
+ string_t *str = t_str_new(128);
+
+ str_append(str, "x'");
+ binary_to_hex_append(str, data, size);
+ str_append_c(str, '\'');
+ return str_c(str);
+}
+
+const struct sql_db driver_sqlite_db = {
+ .name = "sqlite",
+ .flags =
+#if SQLITE_VERSION_NUMBER >= 3024000
+ SQL_DB_FLAG_ON_CONFLICT_DO |
+#endif
+ SQL_DB_FLAG_BLOCKING,
+
+ .v = {
+ .init_full = driver_sqlite_init_full_v,
+ .deinit = driver_sqlite_deinit_v,
+ .connect = driver_sqlite_connect,
+ .disconnect = driver_sqlite_disconnect,
+ .escape_string = driver_sqlite_escape_string,
+ .exec = driver_sqlite_exec,
+ .query = driver_sqlite_query,
+ .query_s = driver_sqlite_query_s,
+
+ .transaction_begin = driver_sqlite_transaction_begin,
+ .transaction_commit = driver_sqlite_transaction_commit,
+ .transaction_commit_s = driver_sqlite_transaction_commit_s,
+ .transaction_rollback = driver_sqlite_transaction_rollback,
+
+ .update = driver_sqlite_update,
+
+ .escape_blob = driver_sqlite_escape_blob,
+ }
+};
+
+const struct sql_result driver_sqlite_result = {
+ .v = {
+ .free = driver_sqlite_result_free,
+ .next_row = driver_sqlite_result_next_row,
+ .get_fields_count = driver_sqlite_result_get_fields_count,
+ .get_field_name = driver_sqlite_result_get_field_name,
+ .find_field = driver_sqlite_result_find_field,
+ .get_field_value = driver_sqlite_result_get_field_value,
+ .get_field_value_binary = driver_sqlite_result_get_field_value_binary,
+ .find_field_value = driver_sqlite_result_find_field_value,
+ .get_values = driver_sqlite_result_get_values,
+ .get_error = driver_sqlite_result_get_error,
+ }
+};
+
+static int
+driver_sqlite_result_error_next_row(struct sql_result *result ATTR_UNUSED)
+{
+ return -1;
+}
+
+const struct sql_result driver_sqlite_error_result = {
+ .v = {
+ .free = driver_sqlite_result_free,
+ .next_row = driver_sqlite_result_error_next_row,
+ .get_error = driver_sqlite_result_get_error,
+ }
+};
+
+const char *driver_sqlite_version = DOVECOT_ABI_VERSION;
+
+void driver_sqlite_init(void);
+void driver_sqlite_deinit(void);
+
+void driver_sqlite_init(void)
+{
+ sql_driver_register(&driver_sqlite_db);
+}
+
+void driver_sqlite_deinit(void)
+{
+ sql_driver_unregister(&driver_sqlite_db);
+}
+
+#endif
diff --git a/src/lib-sql/driver-sqlpool.c b/src/lib-sql/driver-sqlpool.c
new file mode 100644
index 0000000..553b2a0
--- /dev/null
+++ b/src/lib-sql/driver-sqlpool.c
@@ -0,0 +1,934 @@
+/* Copyright (c) 2010-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "llist.h"
+#include "ioloop.h"
+#include "sql-api-private.h"
+
+#include <time.h>
+
+#define QUERY_TIMEOUT_SECS 6
+
+/* sqlpool events are separate from category:sql, because
+ they are usually not very interesting, and would only
+ make logging too noisy. They can be enabled explicitly.
+*/
+static struct event_category event_category_sqlpool = {
+ .name = "sqlpool",
+};
+
+struct sqlpool_host {
+ char *connect_string;
+
+ unsigned int connection_count;
+};
+
+struct sqlpool_connection {
+ struct sql_db *db;
+ unsigned int host_idx;
+};
+
+struct sqlpool_db {
+ struct sql_db api;
+
+ pool_t pool;
+ const struct sql_db *driver;
+ unsigned int connection_limit;
+
+ ARRAY(struct sqlpool_host) hosts;
+ /* all connections from all hosts */
+ ARRAY(struct sqlpool_connection) all_connections;
+ /* index of last connection in all_connections that was used to
+ send a query. */
+ unsigned int last_query_conn_idx;
+
+ /* queued requests */
+ struct sqlpool_request *requests_head, *requests_tail;
+ struct timeout *request_to;
+};
+
+struct sqlpool_request {
+ struct sqlpool_request *prev, *next;
+
+ struct sqlpool_db *db;
+ time_t created;
+
+ unsigned int host_idx;
+ unsigned int retry_count;
+
+ struct event *event;
+
+ /* requests are a) queries */
+ char *query;
+ sql_query_callback_t *callback;
+ void *context;
+
+ /* b) transaction waiters */
+ struct sqlpool_transaction_context *trans;
+};
+
+struct sqlpool_transaction_context {
+ struct sql_transaction_context ctx;
+
+ sql_commit_callback_t *callback;
+ void *context;
+
+ pool_t query_pool;
+ struct sqlpool_request *commit_request;
+};
+
+extern struct sql_db driver_sqlpool_db;
+
+static struct sqlpool_connection *
+sqlpool_add_connection(struct sqlpool_db *db, struct sqlpool_host *host,
+ unsigned int host_idx);
+static void
+driver_sqlpool_query_callback(struct sql_result *result,
+ struct sqlpool_request *request);
+static void
+driver_sqlpool_commit_callback(const struct sql_commit_result *result,
+ struct sqlpool_transaction_context *ctx);
+static void driver_sqlpool_deinit(struct sql_db *_db);
+
+static struct sqlpool_request * ATTR_NULL(2)
+sqlpool_request_new(struct sqlpool_db *db, const char *query)
+{
+ struct sqlpool_request *request;
+
+ request = i_new(struct sqlpool_request, 1);
+ request->db = db;
+ request->created = time(NULL);
+ request->query = i_strdup(query);
+ request->event = event_create(db->api.event);
+ return request;
+}
+
+static void
+sqlpool_request_free(struct sqlpool_request **_request)
+{
+ struct sqlpool_request *request = *_request;
+
+ *_request = NULL;
+
+ i_assert(request->prev == NULL && request->next == NULL);
+ event_unref(&request->event);
+ i_free(request->query);
+ i_free(request);
+}
+
+static void
+sqlpool_request_abort(struct sqlpool_request **_request)
+{
+ struct sqlpool_request *request = *_request;
+
+ *_request = NULL;
+
+ if (request->callback != NULL)
+ request->callback(&sql_not_connected_result, request->context);
+
+ i_assert(request->prev != NULL ||
+ request->db->requests_head == request);
+ DLLIST2_REMOVE(&request->db->requests_head,
+ &request->db->requests_tail, request);
+ sqlpool_request_free(&request);
+}
+
+static struct sql_transaction_context *
+driver_sqlpool_new_conn_trans(struct sqlpool_transaction_context *trans,
+ struct sql_db *conndb)
+{
+ struct sql_transaction_context *conn_trans;
+ struct sql_transaction_query *query;
+
+ conn_trans = sql_transaction_begin(conndb);
+ /* backend will use our queries list (we might still append more
+ queries to the list) */
+ conn_trans->head = trans->ctx.head;
+ conn_trans->tail = trans->ctx.tail;
+ for (query = conn_trans->head; query != NULL; query = query->next)
+ query->trans = conn_trans;
+ return conn_trans;
+}
+
+static void
+sqlpool_request_handle_transaction(struct sql_db *conndb,
+ struct sqlpool_transaction_context *trans)
+{
+ struct sql_transaction_context *conn_trans;
+
+ sqlpool_request_free(&trans->commit_request);
+ conn_trans = driver_sqlpool_new_conn_trans(trans, conndb);
+ sql_transaction_commit(&conn_trans,
+ driver_sqlpool_commit_callback, trans);
+}
+
+static void
+sqlpool_request_send_next(struct sqlpool_db *db, struct sql_db *conndb)
+{
+ struct sqlpool_request *request;
+
+ if (db->requests_head == NULL || !SQL_DB_IS_READY(conndb))
+ return;
+
+ request = db->requests_head;
+ DLLIST2_REMOVE(&db->requests_head, &db->requests_tail, request);
+ timeout_reset(db->request_to);
+
+ if (request->query != NULL) {
+ sql_query(conndb, request->query,
+ driver_sqlpool_query_callback, request);
+ } else if (request->trans != NULL) {
+ sqlpool_request_handle_transaction(conndb, request->trans);
+ } else {
+ i_unreached();
+ }
+}
+
+static void sqlpool_reconnect(struct sql_db *conndb)
+{
+ timeout_remove(&conndb->to_reconnect);
+ (void)sql_connect(conndb);
+}
+
+static struct sqlpool_host *
+sqlpool_find_host_with_least_connections(struct sqlpool_db *db,
+ unsigned int *host_idx_r)
+{
+ struct sqlpool_host *hosts, *min = NULL;
+ unsigned int i, count;
+
+ hosts = array_get_modifiable(&db->hosts, &count);
+ i_assert(count > 0);
+
+ min = &hosts[0];
+ *host_idx_r = 0;
+
+ for (i = 1; i < count; i++) {
+ if (min->connection_count > hosts[i].connection_count) {
+ min = &hosts[i];
+ *host_idx_r = i;
+ }
+ }
+ return min;
+}
+
+static bool sqlpool_have_successful_connections(struct sqlpool_db *db)
+{
+ const struct sqlpool_connection *conn;
+
+ array_foreach(&db->all_connections, conn) {
+ if (conn->db->state >= SQL_DB_STATE_IDLE)
+ return TRUE;
+ }
+ return FALSE;
+}
+
+static void
+sqlpool_handle_connect_failed(struct sqlpool_db *db, struct sql_db *conndb)
+{
+ struct sqlpool_host *host;
+ unsigned int host_idx;
+
+ if (conndb->connect_failure_count > 0) {
+ /* increase delay between reconnections to this
+ server */
+ conndb->connect_delay *= 5;
+ if (conndb->connect_delay > SQL_CONNECT_MAX_DELAY)
+ conndb->connect_delay = SQL_CONNECT_MAX_DELAY;
+ }
+ conndb->connect_failure_count++;
+
+ /* reconnect after the delay */
+ timeout_remove(&conndb->to_reconnect);
+ conndb->to_reconnect = timeout_add(conndb->connect_delay * 1000,
+ sqlpool_reconnect, conndb);
+
+ /* if we have zero successful hosts and there still are hosts
+ without connections, connect to one of them. */
+ if (!sqlpool_have_successful_connections(db)) {
+ host = sqlpool_find_host_with_least_connections(db, &host_idx);
+ if (host->connection_count == 0)
+ (void)sqlpool_add_connection(db, host, host_idx);
+ }
+}
+
+static void
+sqlpool_state_changed(struct sql_db *conndb, enum sql_db_state prev_state,
+ void *context)
+{
+ struct sqlpool_db *db = context;
+
+ if (conndb->state == SQL_DB_STATE_IDLE) {
+ conndb->connect_failure_count = 0;
+ conndb->connect_delay = SQL_CONNECT_MIN_DELAY;
+ sqlpool_request_send_next(db, conndb);
+ }
+
+ if (prev_state == SQL_DB_STATE_CONNECTING &&
+ conndb->state == SQL_DB_STATE_DISCONNECTED &&
+ !conndb->no_reconnect)
+ sqlpool_handle_connect_failed(db, conndb);
+}
+
+static struct sqlpool_connection *
+sqlpool_add_connection(struct sqlpool_db *db, struct sqlpool_host *host,
+ unsigned int host_idx)
+{
+ struct sql_db *conndb;
+ struct sqlpool_connection *conn;
+ const char *error;
+ int ret = 0;
+
+ host->connection_count++;
+
+ e_debug(db->api.event, "Creating new connection");
+
+ if (db->driver->v.init_full == NULL) {
+ conndb = db->driver->v.init(host->connect_string);
+ } else {
+ struct sql_settings set = {
+ .connect_string = host->connect_string,
+ .event_parent = event_get_parent(db->api.event),
+ };
+ ret = db->driver->v.init_full(&set, &conndb, &error);
+ }
+ if (ret < 0)
+ i_fatal("sqlpool: %s", error);
+
+ sql_init_common(conndb);
+
+ conndb->state_change_callback = sqlpool_state_changed;
+ conndb->state_change_context = db;
+ conndb->connect_delay = SQL_CONNECT_MIN_DELAY;
+
+ conn = array_append_space(&db->all_connections);
+ conn->host_idx = host_idx;
+ conn->db = conndb;
+ return conn;
+}
+
+static struct sqlpool_connection *
+sqlpool_add_new_connection(struct sqlpool_db *db)
+{
+ struct sqlpool_host *host;
+ unsigned int host_idx;
+
+ host = sqlpool_find_host_with_least_connections(db, &host_idx);
+ if (host->connection_count >= db->connection_limit)
+ return NULL;
+ else
+ return sqlpool_add_connection(db, host, host_idx);
+}
+
+static const struct sqlpool_connection *
+sqlpool_find_available_connection(struct sqlpool_db *db,
+ unsigned int unwanted_host_idx,
+ bool *all_disconnected_r)
+{
+ const struct sqlpool_connection *conns;
+ unsigned int i, count;
+
+ *all_disconnected_r = TRUE;
+
+ conns = array_get(&db->all_connections, &count);
+ for (i = 0; i < count; i++) {
+ unsigned int idx = (i + db->last_query_conn_idx + 1) % count;
+ struct sql_db *conndb = conns[idx].db;
+
+ if (conns[idx].host_idx == unwanted_host_idx)
+ continue;
+
+ if (!SQL_DB_IS_READY(conndb) && conndb->to_reconnect == NULL) {
+ /* see if we could reconnect to it immediately */
+ (void)sql_connect(conndb);
+ }
+ if (SQL_DB_IS_READY(conndb)) {
+ db->last_query_conn_idx = idx;
+ *all_disconnected_r = FALSE;
+ return &conns[idx];
+ }
+ if (conndb->state != SQL_DB_STATE_DISCONNECTED)
+ *all_disconnected_r = FALSE;
+ }
+ return NULL;
+}
+
+static bool
+driver_sqlpool_get_connection(struct sqlpool_db *db,
+ unsigned int unwanted_host_idx,
+ const struct sqlpool_connection **conn_r)
+{
+ const struct sqlpool_connection *conn, *conns;
+ unsigned int i, count;
+ bool all_disconnected;
+
+ conn = sqlpool_find_available_connection(db, unwanted_host_idx,
+ &all_disconnected);
+ if (conn == NULL && unwanted_host_idx != UINT_MAX) {
+ /* maybe there are no wanted hosts. use any of them. */
+ conn = sqlpool_find_available_connection(db, UINT_MAX,
+ &all_disconnected);
+ }
+ if (conn == NULL && all_disconnected) {
+ /* no connected connections. connect_delays may have gotten too
+ high, reset all of them to see if some are still alive. */
+ conns = array_get(&db->all_connections, &count);
+ for (i = 0; i < count; i++) {
+ struct sql_db *conndb = conns[i].db;
+
+ if (conndb->connect_delay > SQL_CONNECT_RESET_DELAY)
+ conndb->connect_delay = SQL_CONNECT_RESET_DELAY;
+ }
+ conn = sqlpool_find_available_connection(db, UINT_MAX,
+ &all_disconnected);
+ }
+ if (conn == NULL) {
+ /* still nothing. try creating new connections */
+ conn = sqlpool_add_new_connection(db);
+ if (conn != NULL)
+ (void)sql_connect(conn->db);
+ if (conn == NULL || !SQL_DB_IS_READY(conn->db))
+ return FALSE;
+ }
+ *conn_r = conn;
+ return TRUE;
+}
+
+static bool
+driver_sqlpool_get_sync_connection(struct sqlpool_db *db,
+ const struct sqlpool_connection **conn_r)
+{
+ const struct sqlpool_connection *conns;
+ unsigned int i, count;
+
+ if (driver_sqlpool_get_connection(db, UINT_MAX, conn_r))
+ return TRUE;
+
+ /* no idling connections, but maybe we can find one that's trying to
+ connect to server, and we can use it once it's finished */
+ conns = array_get(&db->all_connections, &count);
+ for (i = 0; i < count; i++) {
+ if (conns[i].db->state == SQL_DB_STATE_CONNECTING) {
+ *conn_r = &conns[i];
+ return TRUE;
+ }
+ }
+ return FALSE;
+}
+
+static bool
+driver_sqlpool_get_connected_flags(struct sqlpool_db *db,
+ enum sql_db_flags *flags_r)
+{
+ const struct sqlpool_connection *conn;
+
+ array_foreach(&db->all_connections, conn) {
+ if (conn->db->state > SQL_DB_STATE_CONNECTING) {
+ *flags_r = sql_get_flags(conn->db);
+ return TRUE;
+ }
+ }
+ return FALSE;
+}
+
+static enum sql_db_flags driver_sqlpool_get_flags(struct sql_db *_db)
+{
+ struct sqlpool_db *db = (struct sqlpool_db *)_db;
+ const struct sqlpool_connection *conn;
+ enum sql_db_flags flags;
+
+ /* try to use a connected db */
+ if (driver_sqlpool_get_connected_flags(db, &flags))
+ return flags;
+
+ if (!driver_sqlpool_get_sync_connection(db, &conn)) {
+ /* Failed to connect to database. Just use the first
+ connection. */
+ conn = array_idx(&db->all_connections, 0);
+ }
+ return sql_get_flags(conn->db);
+}
+
+static int
+driver_sqlpool_parse_hosts(struct sqlpool_db *db, const char *connect_string,
+ const char **error_r)
+{
+ const char *const *args, *key, *value, *hostname;
+ struct sqlpool_host *host;
+ ARRAY_TYPE(const_string) hostnames, connect_args;
+
+ t_array_init(&hostnames, 8);
+ t_array_init(&connect_args, 32);
+
+ /* connect string is a space separated list. it may contain
+ backend-specific strings which we'll pass as-is. we'll only care
+ about our own settings, plus the host settings. */
+ args = t_strsplit_spaces(connect_string, " ");
+ for (; *args != NULL; args++) {
+ value = strchr(*args, '=');
+ if (value == NULL) {
+ key = *args;
+ value = "";
+ } else {
+ key = t_strdup_until(*args, value);
+ value++;
+ }
+
+ if (strcmp(key, "maxconns") == 0) {
+ if (str_to_uint(value, &db->connection_limit) < 0) {
+ *error_r = t_strdup_printf("Invalid value for maxconns: %s",
+ value);
+ return -1;
+ }
+ } else if (strcmp(key, "host") == 0) {
+ array_push_back(&hostnames, &value);
+ } else {
+ array_push_back(&connect_args, args);
+ }
+ }
+
+ /* build a new connect string without our settings or hosts */
+ array_append_zero(&connect_args);
+ connect_string = t_strarray_join(array_front(&connect_args), " ");
+
+ if (array_count(&hostnames) == 0) {
+ /* no hosts specified. create a default one. */
+ host = array_append_space(&db->hosts);
+ host->connect_string = i_strdup(connect_string);
+ } else {
+ if (*connect_string == '\0')
+ connect_string = NULL;
+
+ array_foreach_elem(&hostnames, hostname) {
+ host = array_append_space(&db->hosts);
+ host->connect_string =
+ i_strconcat("host=", hostname, " ",
+ connect_string, NULL);
+ }
+ }
+
+ if (db->connection_limit == 0)
+ db->connection_limit = SQL_DEFAULT_CONNECTION_LIMIT;
+ return 0;
+}
+
+static void sqlpool_add_all_once(struct sqlpool_db *db)
+{
+ struct sqlpool_host *host;
+ unsigned int host_idx;
+
+ for (;;) {
+ host = sqlpool_find_host_with_least_connections(db, &host_idx);
+ if (host->connection_count > 0)
+ break;
+ (void)sqlpool_add_connection(db, host, host_idx);
+ }
+}
+
+int driver_sqlpool_init_full(const struct sql_settings *set, const struct sql_db *driver,
+ struct sql_db **db_r, const char **error_r)
+{
+ struct sqlpool_db *db;
+ int ret;
+
+ db = i_new(struct sqlpool_db, 1);
+ db->driver = driver;
+ db->api = driver_sqlpool_db;
+ db->api.flags = driver->flags;
+ db->api.event = event_create(set->event_parent);
+ event_add_category(db->api.event, &event_category_sqlpool);
+ event_set_append_log_prefix(db->api.event,
+ t_strdup_printf("sqlpool(%s): ", driver->name));
+ i_array_init(&db->hosts, 8);
+
+ T_BEGIN {
+ ret = driver_sqlpool_parse_hosts(db, set->connect_string,
+ error_r);
+ } T_END_PASS_STR_IF(ret < 0, error_r);
+
+ if (ret < 0) {
+ driver_sqlpool_deinit(&db->api);
+ return ret;
+ }
+ i_array_init(&db->all_connections, 16);
+ /* connect to all databases so we can do load balancing immediately */
+ sqlpool_add_all_once(db);
+
+ *db_r = &db->api;
+ return 0;
+}
+
+static void driver_sqlpool_abort_requests(struct sqlpool_db *db)
+{
+ while (db->requests_head != NULL) {
+ struct sqlpool_request *request = db->requests_head;
+
+ sqlpool_request_abort(&request);
+ }
+ timeout_remove(&db->request_to);
+}
+
+static void driver_sqlpool_deinit(struct sql_db *_db)
+{
+ struct sqlpool_db *db = (struct sqlpool_db *)_db;
+ struct sqlpool_host *host;
+ struct sqlpool_connection *conn;
+
+ array_foreach_modifiable(&db->all_connections, conn)
+ sql_unref(&conn->db);
+ array_clear(&db->all_connections);
+
+ driver_sqlpool_abort_requests(db);
+
+ array_foreach_modifiable(&db->hosts, host)
+ i_free(host->connect_string);
+
+ i_assert(array_count(&db->all_connections) == 0);
+ array_free(&db->hosts);
+ array_free(&db->all_connections);
+ array_free(&_db->module_contexts);
+ event_unref(&_db->event);
+ i_free(db);
+}
+
+static int driver_sqlpool_connect(struct sql_db *_db)
+{
+ struct sqlpool_db *db = (struct sqlpool_db *)_db;
+ const struct sqlpool_connection *conn;
+ int ret = -1, ret2;
+
+ array_foreach(&db->all_connections, conn) {
+ ret2 = conn->db->to_reconnect != NULL ? -1 :
+ sql_connect(conn->db);
+ if (ret2 > 0)
+ ret = 1;
+ else if (ret2 == 0 && ret < 0)
+ ret = 0;
+ }
+ return ret;
+}
+
+static void driver_sqlpool_disconnect(struct sql_db *_db)
+{
+ struct sqlpool_db *db = (struct sqlpool_db *)_db;
+ const struct sqlpool_connection *conn;
+
+ array_foreach(&db->all_connections, conn)
+ sql_disconnect(conn->db);
+ driver_sqlpool_abort_requests(db);
+}
+
+static const char *
+driver_sqlpool_escape_string(struct sql_db *_db, const char *string)
+{
+ struct sqlpool_db *db = (struct sqlpool_db *)_db;
+ const struct sqlpool_connection *conns;
+ unsigned int i, count;
+
+ /* use the first ready connection */
+ conns = array_get(&db->all_connections, &count);
+ for (i = 0; i < count; i++) {
+ if (SQL_DB_IS_READY(conns[i].db))
+ return sql_escape_string(conns[i].db, string);
+ }
+ /* no ready connections. just use the first one (we're guaranteed
+ to always have one) */
+ return sql_escape_string(conns[0].db, string);
+}
+
+static void driver_sqlpool_timeout(struct sqlpool_db *db)
+{
+ int duration;
+
+ while (db->requests_head != NULL) {
+ struct sqlpool_request *request = db->requests_head;
+
+ if (request->created + SQL_QUERY_TIMEOUT_SECS > ioloop_time)
+ break;
+
+
+ if (request->query != NULL) {
+ e_error(sql_query_finished_event(&db->api, request->event,
+ request->query, FALSE,
+ &duration)->
+ add_str("error", "Query timed out")->
+ event(),
+ SQL_QUERY_FINISHED_FMT": Query timed out "
+ "(no free connections for %u secs)",
+ request->query, duration,
+ (unsigned int)(ioloop_time - request->created));
+ } else {
+ e_error(event_create_passthrough(request->event)->
+ add_str("error", "Timed out")->
+ set_name(SQL_TRANSACTION_FINISHED)->event(),
+ "Transaction timed out "
+ "(no free connections for %u secs)",
+ (unsigned int)(ioloop_time - request->created));
+ }
+ sqlpool_request_abort(&request);
+ }
+
+ if (db->requests_head == NULL)
+ timeout_remove(&db->request_to);
+}
+
+static void
+driver_sqlpool_prepend_request(struct sqlpool_db *db,
+ struct sqlpool_request *request)
+{
+ DLLIST2_PREPEND(&db->requests_head, &db->requests_tail, request);
+ if (db->request_to == NULL) {
+ db->request_to = timeout_add(SQL_QUERY_TIMEOUT_SECS * 1000,
+ driver_sqlpool_timeout, db);
+ }
+}
+
+static void
+driver_sqlpool_append_request(struct sqlpool_db *db,
+ struct sqlpool_request *request)
+{
+ DLLIST2_APPEND(&db->requests_head, &db->requests_tail, request);
+ if (db->request_to == NULL) {
+ db->request_to = timeout_add(SQL_QUERY_TIMEOUT_SECS * 1000,
+ driver_sqlpool_timeout, db);
+ }
+}
+
+static void
+driver_sqlpool_query_callback(struct sql_result *result,
+ struct sqlpool_request *request)
+{
+ struct sqlpool_db *db = request->db;
+ const struct sqlpool_connection *conn = NULL;
+ struct sql_db *conndb;
+
+ if (result->failed_try_retry &&
+ request->retry_count < array_count(&db->hosts)) {
+ e_warning(db->api.event, "Query failed, retrying: %s",
+ sql_result_get_error(result));
+ request->retry_count++;
+ driver_sqlpool_prepend_request(db, request);
+
+ if (driver_sqlpool_get_connection(request->db,
+ request->host_idx, &conn)) {
+ request->host_idx = conn->host_idx;
+ sqlpool_request_send_next(db, conn->db);
+ }
+ } else {
+ if (result->failed) {
+ e_error(db->api.event, "Query failed, aborting: %s",
+ request->query);
+ }
+ conndb = result->db;
+
+ if (request->callback != NULL)
+ request->callback(result, request->context);
+ sqlpool_request_free(&request);
+
+ sqlpool_request_send_next(db, conndb);
+ }
+}
+
+static void ATTR_NULL(3, 4)
+driver_sqlpool_query(struct sql_db *_db, const char *query,
+ sql_query_callback_t *callback, void *context)
+{
+ struct sqlpool_db *db = (struct sqlpool_db *)_db;
+ struct sqlpool_request *request;
+ const struct sqlpool_connection *conn;
+
+ request = sqlpool_request_new(db, query);
+ request->callback = callback;
+ request->context = context;
+
+ if (!driver_sqlpool_get_connection(db, UINT_MAX, &conn))
+ driver_sqlpool_append_request(db, request);
+ else {
+ request->host_idx = conn->host_idx;
+ sql_query(conn->db, query, driver_sqlpool_query_callback,
+ request);
+ }
+}
+
+static void driver_sqlpool_exec(struct sql_db *_db, const char *query)
+{
+ driver_sqlpool_query(_db, query, NULL, NULL);
+}
+
+static struct sql_result *
+driver_sqlpool_query_s(struct sql_db *_db, const char *query)
+{
+ struct sqlpool_db *db = (struct sqlpool_db *)_db;
+ const struct sqlpool_connection *conn;
+ struct sql_result *result;
+
+ if (!driver_sqlpool_get_sync_connection(db, &conn)) {
+ sql_not_connected_result.refcount++;
+ return &sql_not_connected_result;
+ }
+
+ result = sql_query_s(conn->db, query);
+ if (result->failed_try_retry) {
+ if (!driver_sqlpool_get_sync_connection(db, &conn))
+ return result;
+
+ sql_result_unref(result);
+ result = sql_query_s(conn->db, query);
+ }
+ return result;
+}
+
+static struct sql_transaction_context *
+driver_sqlpool_transaction_begin(struct sql_db *_db)
+{
+ struct sqlpool_transaction_context *ctx;
+
+ ctx = i_new(struct sqlpool_transaction_context, 1);
+ ctx->ctx.db = _db;
+
+ /* queue changes until commit. even if we did have a free connection
+ now, don't use it or multiple open transactions could tie up all
+ connections. */
+ ctx->query_pool = pool_alloconly_create("sqlpool transaction", 1024);
+ return &ctx->ctx;
+}
+
+static void
+driver_sqlpool_transaction_free(struct sqlpool_transaction_context *ctx)
+{
+ if (ctx->commit_request != NULL)
+ sqlpool_request_abort(&ctx->commit_request);
+ pool_unref(&ctx->query_pool);
+ i_free(ctx);
+}
+
+static void
+driver_sqlpool_commit_callback(const struct sql_commit_result *result,
+ struct sqlpool_transaction_context *ctx)
+{
+ ctx->callback(result, ctx->context);
+ driver_sqlpool_transaction_free(ctx);
+}
+
+static void
+driver_sqlpool_transaction_commit(struct sql_transaction_context *_ctx,
+ sql_commit_callback_t *callback,
+ void *context)
+{
+ struct sqlpool_transaction_context *ctx =
+ (struct sqlpool_transaction_context *)_ctx;
+ struct sqlpool_db *db = (struct sqlpool_db *)_ctx->db;
+ const struct sqlpool_connection *conn;
+
+ ctx->callback = callback;
+ ctx->context = context;
+
+ ctx->commit_request = sqlpool_request_new(db, NULL);
+ ctx->commit_request->trans = ctx;
+
+ if (driver_sqlpool_get_connection(db, UINT_MAX, &conn))
+ sqlpool_request_handle_transaction(conn->db, ctx);
+ else
+ driver_sqlpool_append_request(db, ctx->commit_request);
+}
+
+static int
+driver_sqlpool_transaction_commit_s(struct sql_transaction_context *_ctx,
+ const char **error_r)
+{
+ struct sqlpool_transaction_context *ctx =
+ (struct sqlpool_transaction_context *)_ctx;
+ struct sqlpool_db *db = (struct sqlpool_db *)_ctx->db;
+ const struct sqlpool_connection *conn;
+ struct sql_transaction_context *conn_trans;
+ int ret;
+
+ *error_r = NULL;
+
+ if (!driver_sqlpool_get_sync_connection(db, &conn)) {
+ *error_r = SQL_ERRSTR_NOT_CONNECTED;
+ driver_sqlpool_transaction_free(ctx);
+ return -1;
+ }
+
+ conn_trans = driver_sqlpool_new_conn_trans(ctx, conn->db);
+ ret = sql_transaction_commit_s(&conn_trans, error_r);
+ driver_sqlpool_transaction_free(ctx);
+ return ret;
+}
+
+static void
+driver_sqlpool_transaction_rollback(struct sql_transaction_context *_ctx)
+{
+ struct sqlpool_transaction_context *ctx =
+ (struct sqlpool_transaction_context *)_ctx;
+
+ driver_sqlpool_transaction_free(ctx);
+}
+
+static void
+driver_sqlpool_update(struct sql_transaction_context *_ctx, const char *query,
+ unsigned int *affected_rows)
+{
+ struct sqlpool_transaction_context *ctx =
+ (struct sqlpool_transaction_context *)_ctx;
+
+ /* we didn't get a connection for transaction immediately.
+ queue updates until commit transfers all of these */
+ sql_transaction_add_query(&ctx->ctx, ctx->query_pool,
+ query, affected_rows);
+}
+
+static const char *
+driver_sqlpool_escape_blob(struct sql_db *_db,
+ const unsigned char *data, size_t size)
+{
+ struct sqlpool_db *db = (struct sqlpool_db *)_db;
+ const struct sqlpool_connection *conns;
+ unsigned int i, count;
+
+ /* use the first ready connection */
+ conns = array_get(&db->all_connections, &count);
+ for (i = 0; i < count; i++) {
+ if (SQL_DB_IS_READY(conns[i].db))
+ return sql_escape_blob(conns[i].db, data, size);
+ }
+ /* no ready connections. just use the first one (we're guaranteed
+ to always have one) */
+ return sql_escape_blob(conns[0].db, data, size);
+}
+
+static void driver_sqlpool_wait(struct sql_db *_db)
+{
+ struct sqlpool_db *db = (struct sqlpool_db *)_db;
+ const struct sqlpool_connection *conn;
+
+ array_foreach(&db->all_connections, conn)
+ sql_wait(conn->db);
+}
+
+struct sql_db driver_sqlpool_db = {
+ "",
+
+ .v = {
+ .get_flags = driver_sqlpool_get_flags,
+ .deinit = driver_sqlpool_deinit,
+ .connect = driver_sqlpool_connect,
+ .disconnect = driver_sqlpool_disconnect,
+ .escape_string = driver_sqlpool_escape_string,
+ .exec = driver_sqlpool_exec,
+ .query = driver_sqlpool_query,
+ .query_s = driver_sqlpool_query_s,
+ .wait = driver_sqlpool_wait,
+
+ .transaction_begin = driver_sqlpool_transaction_begin,
+ .transaction_commit = driver_sqlpool_transaction_commit,
+ .transaction_commit_s = driver_sqlpool_transaction_commit_s,
+ .transaction_rollback = driver_sqlpool_transaction_rollback,
+
+ .update = driver_sqlpool_update,
+
+ .escape_blob = driver_sqlpool_escape_blob,
+ }
+};
diff --git a/src/lib-sql/driver-test.c b/src/lib-sql/driver-test.c
new file mode 100644
index 0000000..9d4db76
--- /dev/null
+++ b/src/lib-sql/driver-test.c
@@ -0,0 +1,514 @@
+/* Copyright (c) 2017-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "test-lib.h"
+#include "str.h"
+#include "buffer.h"
+#include "sql-api-private.h"
+#include "driver-test.h"
+#include "array.h"
+#include "hex-binary.h"
+
+struct test_sql_db {
+ struct sql_db api;
+
+ pool_t pool;
+ ARRAY(struct test_driver_result) expected;
+ const char *error;
+ bool failed:1;
+};
+
+struct test_sql_result {
+ struct sql_result api;
+ struct test_driver_result *result;
+ const char *error;
+};
+
+static struct sql_db *driver_test_mysql_init(const char *connect_string);
+static struct sql_db *driver_test_cassandra_init(const char *connect_string);
+static struct sql_db *driver_test_sqlite_init(const char *connect_string);
+static void driver_test_deinit(struct sql_db *_db);
+static int driver_test_connect(struct sql_db *_db);
+static void driver_test_disconnect(struct sql_db *_db);
+static const char *
+driver_test_mysql_escape_string(struct sql_db *_db, const char *string);
+static const char *
+driver_test_escape_string(struct sql_db *_db, const char *string);
+static void driver_test_exec(struct sql_db *_db, const char *query);
+static void driver_test_query(struct sql_db *_db, const char *query,
+ sql_query_callback_t *callback, void *context);
+static struct sql_result *
+driver_test_query_s(struct sql_db *_db, const char *query);
+static struct sql_transaction_context *
+driver_test_transaction_begin(struct sql_db *_db);
+static void driver_test_transaction_commit(struct sql_transaction_context *ctx,
+ sql_commit_callback_t *callback,
+ void *context);
+static int
+driver_test_transaction_commit_s(struct sql_transaction_context *ctx,
+ const char **error_r);
+static void
+driver_test_transaction_rollback(struct sql_transaction_context *ctx);
+static void
+driver_test_update(struct sql_transaction_context *ctx, const char *query,
+ unsigned int *affected_rows);
+static const char *
+driver_test_mysql_escape_blob(struct sql_db *_db, const unsigned char *data,
+ size_t size);
+static const char *
+driver_test_escape_blob(struct sql_db *_db, const unsigned char *data,
+ size_t size);
+
+static void driver_test_result_free(struct sql_result *result);
+static int driver_test_result_next_row(struct sql_result *result);
+
+static unsigned int
+driver_test_result_get_fields_count(struct sql_result *result);
+static const char *
+driver_test_result_get_field_name(struct sql_result *result, unsigned int idx);
+static int
+driver_test_result_find_field(struct sql_result *result, const char *field_name);
+
+static const char *
+driver_test_result_get_field_value(struct sql_result *result, unsigned int idx);
+static const unsigned char *
+driver_test_result_get_field_value_binary(struct sql_result *result,
+ unsigned int idx, size_t *size_r);
+static const char *
+driver_test_result_find_field_value(struct sql_result *result,
+ const char *field_name);
+static const char *const *
+driver_test_result_get_values(struct sql_result *result);
+
+const char *driver_test_result_get_error(struct sql_result *result);
+
+
+const struct sql_db driver_test_mysql_db = {
+ .name = "mysql",
+ .flags = SQL_DB_FLAG_BLOCKING | SQL_DB_FLAG_ON_DUPLICATE_KEY,
+
+ .v = {
+ .init = driver_test_mysql_init,
+ .deinit = driver_test_deinit,
+ .connect = driver_test_connect,
+ .disconnect = driver_test_disconnect,
+ .escape_string = driver_test_mysql_escape_string,
+ .exec = driver_test_exec,
+ .query = driver_test_query,
+ .query_s = driver_test_query_s,
+
+ .transaction_begin = driver_test_transaction_begin,
+ .transaction_commit = driver_test_transaction_commit,
+ .transaction_commit_s = driver_test_transaction_commit_s,
+ .transaction_rollback = driver_test_transaction_rollback,
+ .update = driver_test_update,
+
+ .escape_blob = driver_test_mysql_escape_blob,
+ }
+};
+
+const struct sql_db driver_test_cassandra_db = {
+ .name = "cassandra",
+
+ .v = {
+ .init = driver_test_cassandra_init,
+ .deinit = driver_test_deinit,
+ .connect = driver_test_connect,
+ .disconnect = driver_test_disconnect,
+ .escape_string = driver_test_escape_string,
+ .exec = driver_test_exec,
+ .query = driver_test_query,
+ .query_s = driver_test_query_s,
+
+ .transaction_begin = driver_test_transaction_begin,
+ .transaction_commit = driver_test_transaction_commit,
+ .transaction_commit_s = driver_test_transaction_commit_s,
+ .transaction_rollback = driver_test_transaction_rollback,
+ .update = driver_test_update,
+
+ .escape_blob = driver_test_escape_blob,
+ }
+};
+
+const struct sql_db driver_test_sqlite_db = {
+ .name = "sqlite",
+ .flags = SQL_DB_FLAG_ON_CONFLICT_DO | SQL_DB_FLAG_BLOCKING,
+
+ .v = {
+ .init = driver_test_sqlite_init,
+ .deinit = driver_test_deinit,
+ .connect = driver_test_connect,
+ .disconnect = driver_test_disconnect,
+ .escape_string = driver_test_escape_string,
+ .exec = driver_test_exec,
+ .query = driver_test_query,
+ .query_s = driver_test_query_s,
+
+ .transaction_begin = driver_test_transaction_begin,
+ .transaction_commit = driver_test_transaction_commit,
+ .transaction_commit_s = driver_test_transaction_commit_s,
+ .transaction_rollback = driver_test_transaction_rollback,
+ .update = driver_test_update,
+
+ .escape_blob = driver_test_escape_blob,
+ }
+};
+
+
+const struct sql_result driver_test_result = {
+ .v = {
+ .free = driver_test_result_free,
+ .next_row = driver_test_result_next_row,
+ .get_fields_count = driver_test_result_get_fields_count,
+ .get_field_name = driver_test_result_get_field_name,
+ .find_field = driver_test_result_find_field,
+ .get_field_value = driver_test_result_get_field_value,
+ .get_field_value_binary = driver_test_result_get_field_value_binary,
+ .find_field_value = driver_test_result_find_field_value,
+ .get_values = driver_test_result_get_values,
+ .get_error = driver_test_result_get_error,
+ }
+};
+
+void sql_driver_test_register(void)
+{
+ sql_driver_register(&driver_test_mysql_db);
+ sql_driver_register(&driver_test_cassandra_db);
+ sql_driver_register(&driver_test_sqlite_db);
+}
+
+void sql_driver_test_unregister(void)
+{
+ sql_driver_unregister(&driver_test_mysql_db);
+ sql_driver_unregister(&driver_test_cassandra_db);
+ sql_driver_unregister(&driver_test_sqlite_db);
+}
+
+static struct sql_db *driver_test_init(const struct sql_db *driver,
+ const char *connect_string ATTR_UNUSED)
+{
+ pool_t pool = pool_alloconly_create(MEMPOOL_GROWING" test sql driver", 2048);
+ struct test_sql_db *ret = p_new(pool, struct test_sql_db, 1);
+ ret->pool = pool;
+ ret->api = *driver;
+ p_array_init(&ret->expected, pool, 8);
+ return &ret->api;
+}
+
+static struct sql_db *driver_test_mysql_init(const char *connect_string)
+{
+ return driver_test_init(&driver_test_mysql_db, connect_string);
+}
+
+static struct sql_db *driver_test_cassandra_init(const char *connect_string)
+{
+ return driver_test_init(&driver_test_cassandra_db, connect_string);
+}
+
+static struct sql_db *driver_test_sqlite_init(const char *connect_string)
+{
+ return driver_test_init(&driver_test_sqlite_db, connect_string);
+}
+
+static void driver_test_deinit(struct sql_db *_db ATTR_UNUSED)
+{
+ struct test_sql_db *db = (struct test_sql_db*)_db;
+ array_free(&_db->module_contexts);
+ pool_unref(&db->pool);
+}
+
+static int driver_test_connect(struct sql_db *_db ATTR_UNUSED)
+{
+ /* nix */
+ return 0;
+}
+
+static void driver_test_disconnect(struct sql_db *_db ATTR_UNUSED)
+{ }
+
+static const char *
+driver_test_mysql_escape_string(struct sql_db *_db ATTR_UNUSED,
+ const char *string)
+{
+ string_t *esc = t_str_new(strlen(string));
+ for(const char *ptr = string; *ptr != '\0'; ptr++) {
+ if (*ptr == '\n' || *ptr == '\r' || *ptr == '\\' ||
+ *ptr == '\'' || *ptr == '\"' || *ptr == '\x1a')
+ str_append_c(esc, '\\');
+ str_append_c(esc, *ptr);
+ }
+ return str_c(esc);
+}
+
+static const char *
+driver_test_escape_string(struct sql_db *_db ATTR_UNUSED, const char *string)
+{
+ return string;
+}
+
+static void driver_test_exec(struct sql_db *_db, const char *query)
+{
+ struct test_sql_db *db = (struct test_sql_db*)_db;
+ struct test_driver_result *result =
+ array_front_modifiable(&db->expected);
+ i_assert(result->cur < result->nqueries);
+
+/* i_debug("DUMMY EXECUTE: %s", query);
+ i_debug("DUMMY EXPECT : %s", result->queries[result->cur]); */
+
+ test_assert_strcmp(result->queries[result->cur], query);
+
+ if (strcmp(result->queries[result->cur], query) != 0) {
+ db->error = "Invalid query";
+ db->failed = TRUE;
+ }
+
+ result->cur++;
+}
+
+static void
+driver_test_query(struct sql_db *_db, const char *query,
+ sql_query_callback_t *callback, void *context)
+{
+ struct sql_result *result = driver_test_query_s(_db, query);
+ if (callback != NULL)
+ callback(result, context);
+}
+
+static struct sql_result *
+driver_test_query_s(struct sql_db *_db, const char *query)
+{
+ struct test_sql_db *db = (struct test_sql_db*)_db;
+ struct test_driver_result *result =
+ array_front_modifiable(&db->expected);
+ struct test_sql_result *res = i_new(struct test_sql_result, 1);
+
+ driver_test_exec(_db, query);
+
+ if (db->failed) {
+ res->api.failed = TRUE;
+ }
+
+ res->api.v = driver_test_result.v;
+ res->api.db = _db;
+ if (result->result != NULL) {
+ res->result = i_new(struct test_driver_result, 1);
+ memcpy(res->result, result, sizeof(*result));
+ }
+ res->api.refcount = 1;
+
+ /* drop it from array if it's used up */
+ if (result->cur == result->nqueries)
+ array_pop_front(&db->expected);
+
+ return &res->api;
+}
+
+static struct sql_transaction_context *
+driver_test_transaction_begin(struct sql_db *_db)
+{
+ struct sql_transaction_context *ctx =
+ i_new(struct sql_transaction_context, 1);
+ ctx->db = _db;
+ return ctx;
+}
+
+static void
+driver_test_transaction_commit(struct sql_transaction_context *ctx,
+ sql_commit_callback_t *callback, void *context)
+{
+ struct sql_commit_result res;
+ res.error_type = driver_test_transaction_commit_s(ctx, &res.error);
+ callback(&res, context);
+}
+
+static int
+driver_test_transaction_commit_s(struct sql_transaction_context *ctx,
+ const char **error_r)
+{
+ struct test_sql_db *db = (struct test_sql_db*)ctx->db;
+ int ret = 0;
+
+ if (db->error != NULL) {
+ *error_r = db->error;
+ ret = -1;
+ }
+ i_free(ctx);
+ db->error = NULL;
+ db->failed = FALSE;
+
+ return ret;
+}
+
+static void
+driver_test_transaction_rollback(struct sql_transaction_context *ctx)
+{
+ struct test_sql_db *db = (struct test_sql_db*)ctx->db;
+ i_free(ctx);
+ db->error = NULL;
+ db->failed = FALSE;
+}
+
+static void
+driver_test_update(struct sql_transaction_context *ctx, const char *query,
+ unsigned int *affected_rows)
+{
+ struct test_sql_db *db= (struct test_sql_db*)ctx->db;
+ struct test_driver_result *result =
+ array_front_modifiable(&db->expected);
+ driver_test_exec(ctx->db, query);
+
+ if (affected_rows != NULL)
+ *affected_rows = result->affected_rows;
+
+ /* drop it from array if it's used up */
+ if (result->cur == result->nqueries)
+ array_pop_front(&db->expected);
+}
+
+static const char *
+driver_test_mysql_escape_blob(struct sql_db *_db ATTR_UNUSED,
+ const unsigned char *data, size_t size)
+{
+ return t_strdup_printf("X'%s'", binary_to_hex(data,size));
+}
+
+static const char *
+driver_test_escape_blob(struct sql_db *_db ATTR_UNUSED,
+ const unsigned char *data, size_t size)
+{
+ return t_strdup_printf("X'%s'", binary_to_hex(data,size));
+}
+
+static void driver_test_result_free(struct sql_result *result)
+{
+ struct test_sql_result *tsr =
+ (struct test_sql_result *)result;
+ if (tsr->result != NULL)
+ i_free(tsr->result);
+ i_free(result);
+}
+
+static int driver_test_result_next_row(struct sql_result *result)
+{
+ struct test_sql_result *tsr =
+ (struct test_sql_result *)result;
+ struct test_driver_result *r = tsr->result;
+
+ if (r == NULL) return 0;
+
+ struct test_driver_result_set *rs =
+ &(r->result[r->cur-1]);
+ if (rs->cur <= rs->rows) {
+ rs->cur++;
+ }
+
+ return rs->cur <= rs->rows ? 1 : 0;
+}
+
+static unsigned int
+driver_test_result_get_fields_count(struct sql_result *result)
+{
+ struct test_sql_result *tsr =
+ (struct test_sql_result *)result;
+ struct test_driver_result *r = tsr->result;
+ struct test_driver_result_set *rs =
+ &(r->result[r->cur-1]);
+ return rs->cols;
+}
+
+static const char *
+driver_test_result_get_field_name(struct sql_result *result, unsigned int idx)
+{
+ struct test_sql_result *tsr =
+ (struct test_sql_result *)result;
+ struct test_driver_result *r = tsr->result;
+ struct test_driver_result_set *rs =
+ &(r->result[r->cur-1]);
+ i_assert(idx < rs->cols);
+ return rs->col_names[idx];
+}
+
+static int
+driver_test_result_find_field(struct sql_result *result, const char *field_name)
+{
+ struct test_sql_result *tsr =
+ (struct test_sql_result *)result;
+ struct test_driver_result *r = tsr->result;
+ struct test_driver_result_set *rs =
+ &(r->result[r->cur-1]);
+ for(size_t i = 0; i < rs->cols; i++) {
+ if (strcmp(field_name, rs->col_names[i])==0)
+ return i;
+ }
+ return -1;
+}
+
+static const char *
+driver_test_result_get_field_value(struct sql_result *result, unsigned int idx)
+{
+ struct test_sql_result *tsr =
+ (struct test_sql_result *)result;
+ struct test_driver_result *r = tsr->result;
+ struct test_driver_result_set *rs =
+ &(r->result[r->cur-1]);
+
+ i_assert(idx < rs->cols);
+ i_assert(rs->cur <= rs->rows);
+
+ return rs->row_data[rs->cur-1][idx];
+}
+static const unsigned char *
+driver_test_result_get_field_value_binary(struct sql_result *result,
+ unsigned int idx, size_t *size_r)
+{
+ buffer_t *buf = t_buffer_create(64);
+ const char *value = driver_test_result_get_field_value(result, idx);
+ /* expect it hex encoded */
+ if (hex_to_binary(value, buf) < 0) {
+ *size_r = 0;
+ return NULL;
+ }
+ *size_r = buf->used;
+ return buf->data;
+}
+static const char *
+driver_test_result_find_field_value(struct sql_result *result,
+ const char *field_name)
+{
+ int idx = driver_test_result_find_field(result, field_name);
+ if (idx < 0) return NULL;
+ return driver_test_result_get_field_value(result, idx);
+}
+static const char *const *
+driver_test_result_get_values(struct sql_result *result)
+{
+ struct test_sql_result *tsr =
+ (struct test_sql_result *)result;
+ struct test_driver_result *r = tsr->result;
+ struct test_driver_result_set *rs =
+ &(r->result[r->cur-1]);
+ i_assert(rs->cur <= rs->rows);
+ return rs->row_data[rs->cur-1];
+}
+
+const char *driver_test_result_get_error(struct sql_result *result)
+{
+ struct test_sql_result *tsr =
+ (struct test_sql_result *)result;
+ return tsr->error;
+}
+
+
+void sql_driver_test_add_expected_result(struct sql_db *_db,
+ const struct test_driver_result *result)
+{
+ struct test_sql_db *db = (struct test_sql_db*)_db;
+ array_push_back(&db->expected, result);
+}
+
+void sql_driver_test_clear_expected_results(struct sql_db *_db)
+{
+ struct test_sql_db *db = (struct test_sql_db*)_db;
+ array_clear(&db->expected);
+}
diff --git a/src/lib-sql/driver-test.h b/src/lib-sql/driver-test.h
new file mode 100644
index 0000000..49915ad
--- /dev/null
+++ b/src/lib-sql/driver-test.h
@@ -0,0 +1,28 @@
+#ifndef DRIVER_TEST_H
+#define DRIVER_TEST_H 1
+
+struct test_driver_result_set {
+ size_t rows, cols, cur;
+ const char *const *col_names;
+ const char ***row_data;
+};
+
+struct test_driver_result {
+ /* expected queries */
+ size_t nqueries;
+ size_t cur;
+ unsigned int affected_rows;
+ const char *const *queries;
+
+ /* test result, rows and columns */
+ struct test_driver_result_set *result;
+};
+
+void sql_driver_test_register(void);
+void sql_driver_test_unregister(void);
+
+void sql_driver_test_add_expected_result(struct sql_db *_db,
+ const struct test_driver_result *result);
+void sql_driver_test_clear_expected_results(struct sql_db *_db);
+
+#endif
diff --git a/src/lib-sql/sql-api-private.h b/src/lib-sql/sql-api-private.h
new file mode 100644
index 0000000..192afb6
--- /dev/null
+++ b/src/lib-sql/sql-api-private.h
@@ -0,0 +1,251 @@
+#ifndef SQL_API_PRIVATE_H
+#define SQL_API_PRIVATE_H
+
+#include "sql-api.h"
+#include "module-context.h"
+
+enum sql_db_state {
+ /* not connected to database */
+ SQL_DB_STATE_DISCONNECTED,
+ /* waiting for connection attempt to succeed or fail */
+ SQL_DB_STATE_CONNECTING,
+ /* connected, allowing more queries */
+ SQL_DB_STATE_IDLE,
+ /* connected, no more queries allowed */
+ SQL_DB_STATE_BUSY
+};
+
+/* Minimum delay between reconnecting to same server */
+#define SQL_CONNECT_MIN_DELAY 1
+/* Maximum time to avoiding reconnecting to same server */
+#define SQL_CONNECT_MAX_DELAY (60*30)
+/* If no servers are connected but a query is requested, try reconnecting to
+ next server which has been disconnected longer than this (with a single
+ server setup this is really the "max delay" and the SQL_CONNECT_MAX_DELAY
+ is never used). */
+#define SQL_CONNECT_RESET_DELAY 15
+/* Abort connect() if it can't connect within this time. */
+#define SQL_CONNECT_TIMEOUT_SECS 5
+/* Abort queries after this many seconds */
+#define SQL_QUERY_TIMEOUT_SECS 60
+/* Default max. number of connections to create per host */
+#define SQL_DEFAULT_CONNECTION_LIMIT 5
+
+#define SQL_DB_IS_READY(db) \
+ ((db)->state == SQL_DB_STATE_IDLE)
+#define SQL_ERRSTR_NOT_CONNECTED "Not connected to database"
+
+/* What is considered slow query */
+#define SQL_SLOW_QUERY_MSEC 1000
+
+#define SQL_QUERY_FINISHED "sql_query_finished"
+#define SQL_CONNECTION_FINISHED "sql_connection_finished"
+#define SQL_TRANSACTION_FINISHED "sql_transaction_finished"
+
+#define SQL_QUERY_FINISHED_FMT "Finished query '%s' in %u msecs"
+
+struct sql_db_module_register {
+ unsigned int id;
+};
+
+union sql_db_module_context {
+ struct sql_db_module_register *reg;
+};
+
+extern struct sql_db_module_register sql_db_module_register;
+
+extern struct event_category event_category_sql;
+
+struct sql_transaction_query {
+ struct sql_transaction_query *next;
+ struct sql_transaction_context *trans;
+
+ const char *query;
+ unsigned int *affected_rows;
+};
+
+struct sql_db_vfuncs {
+ struct sql_db *(*init)(const char *connect_string);
+ int (*init_full)(const struct sql_settings *set, struct sql_db **db_r,
+ const char **error);
+ void (*deinit)(struct sql_db *db);
+ void (*unref)(struct sql_db *db);
+ void (*wait) (struct sql_db *db);
+
+ enum sql_db_flags (*get_flags)(struct sql_db *db);
+
+ int (*connect)(struct sql_db *db);
+ void (*disconnect)(struct sql_db *db);
+ const char *(*escape_string)(struct sql_db *db, const char *string);
+
+ void (*exec)(struct sql_db *db, const char *query);
+ void (*query)(struct sql_db *db, const char *query,
+ sql_query_callback_t *callback, void *context);
+ struct sql_result *(*query_s)(struct sql_db *db, const char *query);
+
+ struct sql_transaction_context *(*transaction_begin)(struct sql_db *db);
+ void (*transaction_commit)(struct sql_transaction_context *ctx,
+ sql_commit_callback_t *callback,
+ void *context);
+ int (*transaction_commit_s)(struct sql_transaction_context *ctx,
+ const char **error_r);
+ void (*transaction_rollback)(struct sql_transaction_context *ctx);
+
+ void (*update)(struct sql_transaction_context *ctx, const char *query,
+ unsigned int *affected_rows);
+ const char *(*escape_blob)(struct sql_db *db,
+ const unsigned char *data, size_t size);
+
+ struct sql_prepared_statement *
+ (*prepared_statement_init)(struct sql_db *db,
+ const char *query_template);
+ void (*prepared_statement_deinit)(struct sql_prepared_statement *prep_stmt);
+
+
+ struct sql_statement *
+ (*statement_init)(struct sql_db *db, const char *query_template);
+ struct sql_statement *
+ (*statement_init_prepared)(struct sql_prepared_statement *prep_stmt);
+ void (*statement_abort)(struct sql_statement *stmt);
+ void (*statement_set_timestamp)(struct sql_statement *stmt,
+ const struct timespec *ts);
+ void (*statement_bind_str)(struct sql_statement *stmt,
+ unsigned int column_idx, const char *value);
+ void (*statement_bind_binary)(struct sql_statement *stmt,
+ unsigned int column_idx, const void *value,
+ size_t value_size);
+ void (*statement_bind_int64)(struct sql_statement *stmt,
+ unsigned int column_idx, int64_t value);
+ void (*statement_query)(struct sql_statement *stmt,
+ sql_query_callback_t *callback, void *context);
+ struct sql_result *(*statement_query_s)(struct sql_statement *stmt);
+ void (*update_stmt)(struct sql_transaction_context *ctx,
+ struct sql_statement *stmt,
+ unsigned int *affected_rows);
+};
+
+struct sql_db {
+ const char *name;
+ enum sql_db_flags flags;
+ int refcount;
+
+ struct sql_db_vfuncs v;
+ ARRAY(union sql_db_module_context *) module_contexts;
+
+ void (*state_change_callback)(struct sql_db *db,
+ enum sql_db_state prev_state,
+ void *context);
+ void *state_change_context;
+
+ struct event *event;
+ HASH_TABLE(char *, struct sql_prepared_statement *) prepared_stmt_hash;
+
+ enum sql_db_state state;
+ /* last time we started connecting to this server
+ (which may or may not have succeeded) */
+ time_t last_connect_try;
+ unsigned int connect_delay;
+ unsigned int connect_failure_count;
+ struct timeout *to_reconnect;
+
+ uint64_t succeeded_queries;
+ uint64_t failed_queries;
+ /* includes both succeeded and failed */
+ uint64_t slow_queries;
+
+ bool no_reconnect:1;
+};
+
+struct sql_result_vfuncs {
+ void (*free)(struct sql_result *result);
+ int (*next_row)(struct sql_result *result);
+
+ unsigned int (*get_fields_count)(struct sql_result *result);
+ const char *(*get_field_name)(struct sql_result *result,
+ unsigned int idx);
+ int (*find_field)(struct sql_result *result, const char *field_name);
+
+ const char *(*get_field_value)(struct sql_result *result,
+ unsigned int idx);
+ const unsigned char *
+ (*get_field_value_binary)(struct sql_result *result,
+ unsigned int idx,
+ size_t *size_r);
+ const char *(*find_field_value)(struct sql_result *result,
+ const char *field_name);
+ const char *const *(*get_values)(struct sql_result *result);
+
+ const char *(*get_error)(struct sql_result *result);
+ void (*more)(struct sql_result **result, bool async,
+ sql_query_callback_t *callback, void *context);
+};
+
+struct sql_prepared_statement {
+ struct sql_db *db;
+ int refcount;
+ char *query_template;
+};
+
+struct sql_statement {
+ struct sql_db *db;
+
+ pool_t pool;
+ const char *query_template;
+ ARRAY_TYPE(const_string) args;
+};
+
+struct sql_field_map {
+ enum sql_field_type type;
+ size_t offset;
+};
+
+struct sql_result {
+ struct sql_result_vfuncs v;
+ int refcount;
+
+ struct sql_db *db;
+ const struct sql_field_def *fields;
+
+ unsigned int map_size;
+ struct sql_field_map *map;
+ void *fetch_dest;
+ struct event *event;
+ size_t fetch_dest_size;
+ enum sql_result_error_type error_type;
+
+ bool failed:1;
+ bool failed_try_retry:1;
+ bool callback:1;
+};
+
+struct sql_transaction_context {
+ struct sql_db *db;
+ struct event *event;
+
+ /* commit() must use this query list if head is non-NULL. */
+ struct sql_transaction_query *head, *tail;
+};
+
+ARRAY_DEFINE_TYPE(sql_drivers, const struct sql_db *);
+
+extern ARRAY_TYPE(sql_drivers) sql_drivers;
+extern struct sql_result sql_not_connected_result;
+
+void sql_init_common(struct sql_db *db);
+struct sql_db *
+driver_sqlpool_init(const char *connect_string, const struct sql_db *driver);
+int driver_sqlpool_init_full(const struct sql_settings *set, const struct sql_db *driver,
+ struct sql_db **db_r, const char **error_r);
+
+void sql_db_set_state(struct sql_db *db, enum sql_db_state state);
+
+void sql_transaction_add_query(struct sql_transaction_context *ctx, pool_t pool,
+ const char *query, unsigned int *affected_rows);
+const char *sql_statement_get_query(struct sql_statement *stmt);
+
+void sql_connection_log_finished(struct sql_db *db);
+struct event_passthrough *
+sql_query_finished_event(struct sql_db *db, struct event *event, const char *query,
+ bool success, int *duration_r);
+struct event_passthrough *sql_transaction_finished_event(struct sql_transaction_context *ctx);
+#endif
diff --git a/src/lib-sql/sql-api.c b/src/lib-sql/sql-api.c
new file mode 100644
index 0000000..919513b
--- /dev/null
+++ b/src/lib-sql/sql-api.c
@@ -0,0 +1,833 @@
+/* Copyright (c) 2004-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "ioloop.h"
+#include "hash.h"
+#include "str.h"
+#include "time-util.h"
+#include "sql-api-private.h"
+
+#include <time.h>
+
+struct event_category event_category_sql = {
+ .name = "sql",
+};
+
+struct sql_db_module_register sql_db_module_register = { 0 };
+ARRAY_TYPE(sql_drivers) sql_drivers;
+
+void sql_drivers_init(void)
+{
+ i_array_init(&sql_drivers, 8);
+}
+
+void sql_drivers_deinit(void)
+{
+ array_free(&sql_drivers);
+}
+
+static const struct sql_db *sql_driver_lookup(const char *name)
+{
+ const struct sql_db *const *drivers;
+ unsigned int i, count;
+
+ drivers = array_get(&sql_drivers, &count);
+ for (i = 0; i < count; i++) {
+ if (strcmp(drivers[i]->name, name) == 0)
+ return drivers[i];
+ }
+ return NULL;
+}
+
+void sql_driver_register(const struct sql_db *driver)
+{
+ if (sql_driver_lookup(driver->name) != NULL) {
+ i_fatal("sql_driver_register(%s): Already registered",
+ driver->name);
+ }
+ array_push_back(&sql_drivers, &driver);
+}
+
+void sql_driver_unregister(const struct sql_db *driver)
+{
+ const struct sql_db *const *drivers;
+ unsigned int i, count;
+
+ drivers = array_get(&sql_drivers, &count);
+ for (i = 0; i < count; i++) {
+ if (drivers[i] == driver) {
+ array_delete(&sql_drivers, i, 1);
+ break;
+ }
+ }
+}
+
+struct sql_db *sql_init(const char *db_driver, const char *connect_string)
+{
+ const char *error;
+ struct sql_db *db;
+ struct sql_settings set = {
+ .driver = db_driver,
+ .connect_string = connect_string,
+ };
+
+ if (sql_init_full(&set, &db, &error) < 0)
+ i_fatal("%s", error);
+ return db;
+}
+
+int sql_init_full(const struct sql_settings *set, struct sql_db **db_r,
+ const char **error_r)
+{
+ const struct sql_db *driver;
+ struct sql_db *db;
+ int ret = 0;
+
+ i_assert(set->connect_string != NULL);
+
+ driver = sql_driver_lookup(set->driver);
+ if (driver == NULL) {
+ *error_r = t_strdup_printf("Unknown database driver '%s'", set->driver);
+ return -1;
+ }
+
+ if ((driver->flags & SQL_DB_FLAG_POOLED) == 0) {
+ if (driver->v.init_full == NULL) {
+ db = driver->v.init(set->connect_string);
+ } else
+ ret = driver->v.init_full(set, &db, error_r);
+ } else
+ ret = driver_sqlpool_init_full(set, driver, &db, error_r);
+
+ if (ret < 0)
+ return -1;
+
+ sql_init_common(db);
+ *db_r = db;
+ return 0;
+}
+
+void sql_init_common(struct sql_db *db)
+{
+ db->refcount = 1;
+ i_array_init(&db->module_contexts, 5);
+ hash_table_create(&db->prepared_stmt_hash, default_pool, 0,
+ str_hash, strcmp);
+}
+
+void sql_ref(struct sql_db *db)
+{
+ i_assert(db->refcount > 0);
+ db->refcount++;
+}
+
+static void
+default_sql_prepared_statement_deinit(struct sql_prepared_statement *prep_stmt)
+{
+ i_free(prep_stmt->query_template);
+ i_free(prep_stmt);
+}
+
+static void sql_prepared_statements_free(struct sql_db *db)
+{
+ struct hash_iterate_context *iter;
+ struct sql_prepared_statement *prep_stmt;
+ char *query;
+
+ iter = hash_table_iterate_init(db->prepared_stmt_hash);
+ while (hash_table_iterate(iter, db->prepared_stmt_hash, &query, &prep_stmt)) {
+ i_assert(prep_stmt->refcount == 0);
+ if (prep_stmt->db->v.prepared_statement_deinit != NULL)
+ prep_stmt->db->v.prepared_statement_deinit(prep_stmt);
+ else
+ default_sql_prepared_statement_deinit(prep_stmt);
+ }
+ hash_table_iterate_deinit(&iter);
+ hash_table_clear(db->prepared_stmt_hash, TRUE);
+}
+
+void sql_unref(struct sql_db **_db)
+{
+ struct sql_db *db = *_db;
+
+ *_db = NULL;
+
+ i_assert(db->refcount > 0);
+ if (db->v.unref != NULL)
+ db->v.unref(db);
+ if (--db->refcount > 0)
+ return;
+
+ timeout_remove(&db->to_reconnect);
+ sql_prepared_statements_free(db);
+ hash_table_destroy(&db->prepared_stmt_hash);
+ db->v.deinit(db);
+}
+
+enum sql_db_flags sql_get_flags(struct sql_db *db)
+{
+ if (db->v.get_flags != NULL)
+ return db->v.get_flags(db);
+ else
+ return db->flags;
+}
+
+int sql_connect(struct sql_db *db)
+{
+ time_t now;
+
+ switch (db->state) {
+ case SQL_DB_STATE_DISCONNECTED:
+ break;
+ case SQL_DB_STATE_CONNECTING:
+ return 0;
+ default:
+ return 1;
+ }
+
+ /* don't try reconnecting more than once a second */
+ now = time(NULL);
+ if (db->last_connect_try + (time_t)db->connect_delay > now)
+ return -1;
+ db->last_connect_try = now;
+
+ return db->v.connect(db);
+}
+
+void sql_disconnect(struct sql_db *db)
+{
+ timeout_remove(&db->to_reconnect);
+ db->v.disconnect(db);
+}
+
+const char *sql_escape_string(struct sql_db *db, const char *string)
+{
+ return db->v.escape_string(db, string);
+}
+
+const char *sql_escape_blob(struct sql_db *db,
+ const unsigned char *data, size_t size)
+{
+ return db->v.escape_blob(db, data, size);
+}
+
+void sql_exec(struct sql_db *db, const char *query)
+{
+ db->v.exec(db, query);
+}
+
+#undef sql_query
+void sql_query(struct sql_db *db, const char *query,
+ sql_query_callback_t *callback, void *context)
+{
+ db->v.query(db, query, callback, context);
+}
+
+struct sql_result *sql_query_s(struct sql_db *db, const char *query)
+{
+ return db->v.query_s(db, query);
+}
+
+static struct sql_prepared_statement *
+default_sql_prepared_statement_init(struct sql_db *db,
+ const char *query_template)
+{
+ struct sql_prepared_statement *prep_stmt;
+
+ prep_stmt = i_new(struct sql_prepared_statement, 1);
+ prep_stmt->db = db;
+ prep_stmt->refcount = 1;
+ prep_stmt->query_template = i_strdup(query_template);
+ return prep_stmt;
+}
+
+static struct sql_statement *
+default_sql_statement_init_prepared(struct sql_prepared_statement *stmt)
+{
+ return sql_statement_init(stmt->db, stmt->query_template);
+}
+
+const char *sql_statement_get_query(struct sql_statement *stmt)
+{
+ string_t *query = t_str_new(128);
+ const char *const *args;
+ unsigned int i, args_count, arg_pos = 0;
+
+ args = array_get(&stmt->args, &args_count);
+
+ for (i = 0; stmt->query_template[i] != '\0'; i++) {
+ if (stmt->query_template[i] == '?') {
+ if (arg_pos >= args_count ||
+ args[arg_pos] == NULL) {
+ i_panic("lib-sql: Missing bind for arg #%u in statement: %s",
+ arg_pos, stmt->query_template);
+ }
+ str_append(query, args[arg_pos++]);
+ } else {
+ str_append_c(query, stmt->query_template[i]);
+ }
+ }
+ if (arg_pos != args_count) {
+ i_panic("lib-sql: Too many bind args (%u) for statement: %s",
+ args_count, stmt->query_template);
+ }
+ return str_c(query);
+}
+
+static void
+default_sql_statement_query(struct sql_statement *stmt,
+ sql_query_callback_t *callback, void *context)
+{
+ sql_query(stmt->db, sql_statement_get_query(stmt),
+ callback, context);
+ pool_unref(&stmt->pool);
+}
+
+static struct sql_result *
+default_sql_statement_query_s(struct sql_statement *stmt)
+{
+ struct sql_result *result =
+ sql_query_s(stmt->db, sql_statement_get_query(stmt));
+ pool_unref(&stmt->pool);
+ return result;
+}
+
+static void default_sql_update_stmt(struct sql_transaction_context *ctx,
+ struct sql_statement *stmt,
+ unsigned int *affected_rows)
+{
+ ctx->db->v.update(ctx, sql_statement_get_query(stmt),
+ affected_rows);
+ pool_unref(&stmt->pool);
+}
+
+struct sql_prepared_statement *
+sql_prepared_statement_init(struct sql_db *db, const char *query_template)
+{
+ struct sql_prepared_statement *stmt;
+
+ stmt = hash_table_lookup(db->prepared_stmt_hash, query_template);
+ if (stmt != NULL) {
+ stmt->refcount++;
+ return stmt;
+ }
+
+ if (db->v.prepared_statement_init != NULL)
+ stmt = db->v.prepared_statement_init(db, query_template);
+ else
+ stmt = default_sql_prepared_statement_init(db, query_template);
+
+ hash_table_insert(db->prepared_stmt_hash, stmt->query_template, stmt);
+ return stmt;
+}
+
+void sql_prepared_statement_unref(struct sql_prepared_statement **_prep_stmt)
+{
+ struct sql_prepared_statement *prep_stmt = *_prep_stmt;
+
+ *_prep_stmt = NULL;
+
+ i_assert(prep_stmt->refcount > 0);
+ prep_stmt->refcount--;
+}
+
+static void
+sql_statement_init_fields(struct sql_statement *stmt, struct sql_db *db)
+{
+ stmt->db = db;
+ p_array_init(&stmt->args, stmt->pool, 8);
+}
+
+struct sql_statement *
+sql_statement_init(struct sql_db *db, const char *query_template)
+{
+ struct sql_statement *stmt;
+
+ if (db->v.statement_init != NULL)
+ stmt = db->v.statement_init(db, query_template);
+ else {
+ pool_t pool = pool_alloconly_create("sql statement", 1024);
+ stmt = p_new(pool, struct sql_statement, 1);
+ stmt->pool = pool;
+ }
+ stmt->query_template = p_strdup(stmt->pool, query_template);
+ sql_statement_init_fields(stmt, db);
+ return stmt;
+}
+
+struct sql_statement *
+sql_statement_init_prepared(struct sql_prepared_statement *prep_stmt)
+{
+ struct sql_statement *stmt;
+
+ if (prep_stmt->db->v.statement_init_prepared == NULL)
+ return default_sql_statement_init_prepared(prep_stmt);
+
+ stmt = prep_stmt->db->v.statement_init_prepared(prep_stmt);
+ sql_statement_init_fields(stmt, prep_stmt->db);
+ return stmt;
+}
+
+void sql_statement_abort(struct sql_statement **_stmt)
+{
+ struct sql_statement *stmt = *_stmt;
+
+ *_stmt = NULL;
+ if (stmt->db->v.statement_abort != NULL)
+ stmt->db->v.statement_abort(stmt);
+ pool_unref(&stmt->pool);
+}
+
+void sql_statement_set_timestamp(struct sql_statement *stmt,
+ const struct timespec *ts)
+{
+ if (stmt->db->v.statement_set_timestamp != NULL)
+ stmt->db->v.statement_set_timestamp(stmt, ts);
+}
+
+void sql_statement_bind_str(struct sql_statement *stmt,
+ unsigned int column_idx, const char *value)
+{
+ const char *escaped_value =
+ p_strdup_printf(stmt->pool, "'%s'",
+ sql_escape_string(stmt->db, value));
+ array_idx_set(&stmt->args, column_idx, &escaped_value);
+
+ if (stmt->db->v.statement_bind_str != NULL)
+ stmt->db->v.statement_bind_str(stmt, column_idx, value);
+}
+
+void sql_statement_bind_binary(struct sql_statement *stmt,
+ unsigned int column_idx, const void *value,
+ size_t value_size)
+{
+ const char *value_str =
+ p_strdup_printf(stmt->pool, "%s",
+ sql_escape_blob(stmt->db, value, value_size));
+ array_idx_set(&stmt->args, column_idx, &value_str);
+
+ if (stmt->db->v.statement_bind_binary != NULL) {
+ stmt->db->v.statement_bind_binary(stmt, column_idx,
+ value, value_size);
+ }
+}
+
+void sql_statement_bind_int64(struct sql_statement *stmt,
+ unsigned int column_idx, int64_t value)
+{
+ const char *value_str = p_strdup_printf(stmt->pool, "%"PRId64, value);
+ array_idx_set(&stmt->args, column_idx, &value_str);
+
+ if (stmt->db->v.statement_bind_int64 != NULL)
+ stmt->db->v.statement_bind_int64(stmt, column_idx, value);
+}
+
+#undef sql_statement_query
+void sql_statement_query(struct sql_statement **_stmt,
+ sql_query_callback_t *callback, void *context)
+{
+ struct sql_statement *stmt = *_stmt;
+
+ *_stmt = NULL;
+ if (stmt->db->v.statement_query != NULL)
+ stmt->db->v.statement_query(stmt, callback, context);
+ else
+ default_sql_statement_query(stmt, callback, context);
+}
+
+struct sql_result *sql_statement_query_s(struct sql_statement **_stmt)
+{
+ struct sql_statement *stmt = *_stmt;
+
+ *_stmt = NULL;
+ if (stmt->db->v.statement_query_s != NULL)
+ return stmt->db->v.statement_query_s(stmt);
+ else
+ return default_sql_statement_query_s(stmt);
+}
+
+void sql_result_ref(struct sql_result *result)
+{
+ result->refcount++;
+}
+
+void sql_result_unref(struct sql_result *result)
+{
+ i_assert(result->refcount > 0);
+ if (--result->refcount > 0)
+ return;
+
+ i_free(result->map);
+ result->v.free(result);
+}
+
+static const struct sql_field_def *
+sql_field_def_find(const struct sql_field_def *fields, const char *name)
+{
+ unsigned int i;
+
+ for (i = 0; fields[i].name != NULL; i++) {
+ if (strcasecmp(fields[i].name, name) == 0)
+ return &fields[i];
+ }
+ return NULL;
+}
+
+static void
+sql_result_build_map(struct sql_result *result,
+ const struct sql_field_def *fields, size_t dest_size)
+{
+ const struct sql_field_def *def;
+ const char *name;
+ unsigned int i, count, field_size = 0;
+
+ count = sql_result_get_fields_count(result);
+
+ result->map_size = count;
+ result->map = i_new(struct sql_field_map, result->map_size);
+ for (i = 0; i < count; i++) {
+ name = sql_result_get_field_name(result, i);
+ def = sql_field_def_find(fields, name);
+ if (def != NULL) {
+ result->map[i].type = def->type;
+ result->map[i].offset = def->offset;
+ switch (def->type) {
+ case SQL_TYPE_STR:
+ field_size = sizeof(const char *);
+ break;
+ case SQL_TYPE_UINT:
+ field_size = sizeof(unsigned int);
+ break;
+ case SQL_TYPE_ULLONG:
+ field_size = sizeof(unsigned long long);
+ break;
+ case SQL_TYPE_BOOL:
+ field_size = sizeof(bool);
+ break;
+ }
+ i_assert(def->offset + field_size <= dest_size);
+ } else {
+ result->map[i].offset = SIZE_MAX;
+ }
+ }
+}
+
+void sql_result_setup_fetch(struct sql_result *result,
+ const struct sql_field_def *fields,
+ void *dest, size_t dest_size)
+{
+ if (result->map == NULL)
+ sql_result_build_map(result, fields, dest_size);
+ result->fetch_dest = dest;
+ result->fetch_dest_size = dest_size;
+}
+
+static void sql_result_fetch(struct sql_result *result)
+{
+ unsigned int i, count;
+ const char *value;
+ void *ptr;
+
+ memset(result->fetch_dest, 0, result->fetch_dest_size);
+ count = result->map_size;
+ for (i = 0; i < count; i++) {
+ if (result->map[i].offset == SIZE_MAX)
+ continue;
+
+ value = sql_result_get_field_value(result, i);
+ ptr = STRUCT_MEMBER_P(result->fetch_dest,
+ result->map[i].offset);
+
+ switch (result->map[i].type) {
+ case SQL_TYPE_STR: {
+ *((const char **)ptr) = value;
+ break;
+ }
+ case SQL_TYPE_UINT: {
+ if (value != NULL &&
+ str_to_uint(value, (unsigned int *)ptr) < 0)
+ i_error("sql: Value not uint: %s", value);
+ break;
+ }
+ case SQL_TYPE_ULLONG: {
+ if (value != NULL &&
+ str_to_ullong(value, (unsigned long long *)ptr) < 0)
+ i_error("sql: Value not ullong: %s", value);
+ break;
+ }
+ case SQL_TYPE_BOOL: {
+ if (value != NULL && (*value == 't' || *value == '1'))
+ *((bool *)ptr) = TRUE;
+ break;
+ }
+ }
+ }
+}
+
+int sql_result_next_row(struct sql_result *result)
+{
+ int ret;
+
+ if ((ret = result->v.next_row(result)) <= 0)
+ return ret;
+
+ if (result->fetch_dest != NULL)
+ sql_result_fetch(result);
+ return 1;
+}
+
+#undef sql_result_more
+void sql_result_more(struct sql_result **result,
+ sql_query_callback_t *callback, void *context)
+{
+ i_assert((*result)->v.more != NULL);
+
+ (*result)->v.more(result, TRUE, callback, context);
+}
+
+static void
+sql_result_more_sync_callback(struct sql_result *result, void *context)
+{
+ struct sql_result **dest_result = context;
+
+ *dest_result = result;
+}
+
+void sql_result_more_s(struct sql_result **result)
+{
+ i_assert((*result)->v.more != NULL);
+
+ (*result)->v.more(result, FALSE, sql_result_more_sync_callback, result);
+ /* the callback must have been called */
+ i_assert(*result != NULL);
+}
+
+unsigned int sql_result_get_fields_count(struct sql_result *result)
+{
+ return result->v.get_fields_count(result);
+}
+
+const char *sql_result_get_field_name(struct sql_result *result,
+ unsigned int idx)
+{
+ return result->v.get_field_name(result, idx);
+}
+
+int sql_result_find_field(struct sql_result *result, const char *field_name)
+{
+ return result->v.find_field(result, field_name);
+}
+
+const char *sql_result_get_field_value(struct sql_result *result,
+ unsigned int idx)
+{
+ return result->v.get_field_value(result, idx);
+}
+
+const unsigned char *
+sql_result_get_field_value_binary(struct sql_result *result,
+ unsigned int idx, size_t *size_r)
+{
+ return result->v.get_field_value_binary(result, idx, size_r);
+}
+
+const char *sql_result_find_field_value(struct sql_result *result,
+ const char *field_name)
+{
+ return result->v.find_field_value(result, field_name);
+}
+
+const char *const *sql_result_get_values(struct sql_result *result)
+{
+ return result->v.get_values(result);
+}
+
+const char *sql_result_get_error(struct sql_result *result)
+{
+ return result->v.get_error(result);
+}
+
+enum sql_result_error_type sql_result_get_error_type(struct sql_result *result)
+{
+ return result->error_type;
+}
+
+static void
+sql_result_not_connected_free(struct sql_result *result ATTR_UNUSED)
+{
+}
+
+static int
+sql_result_not_connected_next_row(struct sql_result *result ATTR_UNUSED)
+{
+ return -1;
+}
+
+static const char *
+sql_result_not_connected_get_error(struct sql_result *result ATTR_UNUSED)
+{
+ return SQL_ERRSTR_NOT_CONNECTED;
+}
+
+struct sql_transaction_context *sql_transaction_begin(struct sql_db *db)
+{
+ return db->v.transaction_begin(db);
+}
+
+#undef sql_transaction_commit
+void sql_transaction_commit(struct sql_transaction_context **_ctx,
+ sql_commit_callback_t *callback, void *context)
+{
+ struct sql_transaction_context *ctx = *_ctx;
+
+ *_ctx = NULL;
+ ctx->db->v.transaction_commit(ctx, callback, context);
+}
+
+int sql_transaction_commit_s(struct sql_transaction_context **_ctx,
+ const char **error_r)
+{
+ struct sql_transaction_context *ctx = *_ctx;
+
+ *_ctx = NULL;
+ return ctx->db->v.transaction_commit_s(ctx, error_r);
+}
+
+void sql_transaction_rollback(struct sql_transaction_context **_ctx)
+{
+ struct sql_transaction_context *ctx = *_ctx;
+
+ *_ctx = NULL;
+ ctx->db->v.transaction_rollback(ctx);
+}
+
+void sql_update(struct sql_transaction_context *ctx, const char *query)
+{
+ ctx->db->v.update(ctx, query, NULL);
+}
+
+void sql_update_stmt(struct sql_transaction_context *ctx,
+ struct sql_statement **_stmt)
+{
+ struct sql_statement *stmt = *_stmt;
+
+ *_stmt = NULL;
+ if (ctx->db->v.update_stmt != NULL)
+ ctx->db->v.update_stmt(ctx, stmt, NULL);
+ else
+ default_sql_update_stmt(ctx, stmt, NULL);
+}
+
+void sql_update_get_rows(struct sql_transaction_context *ctx, const char *query,
+ unsigned int *affected_rows)
+{
+ ctx->db->v.update(ctx, query, affected_rows);
+}
+
+void sql_update_stmt_get_rows(struct sql_transaction_context *ctx,
+ struct sql_statement **_stmt,
+ unsigned int *affected_rows)
+{
+ struct sql_statement *stmt = *_stmt;
+
+ *_stmt = NULL;
+ if (ctx->db->v.update_stmt != NULL)
+ ctx->db->v.update_stmt(ctx, stmt, affected_rows);
+ else
+ default_sql_update_stmt(ctx, stmt, affected_rows);
+}
+
+void sql_db_set_state(struct sql_db *db, enum sql_db_state state)
+{
+ enum sql_db_state old_state = db->state;
+
+ if (db->state == state)
+ return;
+
+ db->state = state;
+ if (db->state_change_callback != NULL) {
+ db->state_change_callback(db, old_state,
+ db->state_change_context);
+ }
+}
+
+void sql_transaction_add_query(struct sql_transaction_context *ctx, pool_t pool,
+ const char *query, unsigned int *affected_rows)
+{
+ struct sql_transaction_query *tquery;
+
+ tquery = p_new(pool, struct sql_transaction_query, 1);
+ tquery->trans = ctx;
+ tquery->query = p_strdup(pool, query);
+ tquery->affected_rows = affected_rows;
+
+ if (ctx->head == NULL)
+ ctx->head = tquery;
+ else
+ ctx->tail->next = tquery;
+ ctx->tail = tquery;
+}
+
+void sql_connection_log_finished(struct sql_db *db)
+{
+ struct event_passthrough *e = event_create_passthrough(db->event)->
+ set_name(SQL_CONNECTION_FINISHED);
+ e_debug(e->event(),
+ "Connection finished (queries=%"PRIu64", slow queries=%"PRIu64")",
+ db->succeeded_queries + db->failed_queries,
+ db->slow_queries);
+}
+
+struct event_passthrough *
+sql_query_finished_event(struct sql_db *db, struct event *event, const char *query,
+ bool success, int *duration_r)
+{
+ int diff;
+ struct timeval tv;
+ event_get_create_time(event, &tv);
+ struct event_passthrough *e = event_create_passthrough(event)->
+ set_name(SQL_QUERY_FINISHED)->
+ add_str("query_first_word", t_strcut(query, ' '));
+ diff = timeval_diff_msecs(&ioloop_timeval, &tv);
+
+ if (!success) {
+ db->failed_queries++;
+ } else {
+ db->succeeded_queries++;
+ }
+
+ if (diff >= SQL_SLOW_QUERY_MSEC) {
+ e->add_str("slow_query", "y");
+ db->slow_queries++;
+ }
+
+ if (duration_r != NULL)
+ *duration_r = diff;
+
+ return e;
+}
+
+struct event_passthrough *sql_transaction_finished_event(struct sql_transaction_context *ctx)
+{
+ return event_create_passthrough(ctx->event)->
+ set_name(SQL_TRANSACTION_FINISHED);
+}
+
+void sql_wait(struct sql_db *db)
+{
+ if (db->v.wait != NULL)
+ db->v.wait(db);
+}
+
+
+struct sql_result sql_not_connected_result = {
+ .v = {
+ sql_result_not_connected_free,
+ sql_result_not_connected_next_row,
+ NULL, NULL, NULL, NULL, NULL, NULL, NULL,
+ sql_result_not_connected_get_error,
+ NULL,
+ },
+ .failed_try_retry = TRUE
+};
diff --git a/src/lib-sql/sql-api.h b/src/lib-sql/sql-api.h
new file mode 100644
index 0000000..7936b98
--- /dev/null
+++ b/src/lib-sql/sql-api.h
@@ -0,0 +1,249 @@
+#ifndef SQL_API_H
+#define SQL_API_H
+
+struct timespec;
+
+/* This SQL API is designed to work asynchronously. The underlying drivers
+ however may not. */
+
+enum sql_db_flags {
+ /* Set if queries are not executed asynchronously */
+ SQL_DB_FLAG_BLOCKING = 0x01,
+ /* Set if database wants to use connection pooling */
+ SQL_DB_FLAG_POOLED = 0x02,
+ /* Prepared statements are supported by the database. If they aren't,
+ the functions can still be used, but they're just internally
+ convered into regular statements. */
+ SQL_DB_FLAG_PREP_STATEMENTS = 0x04,
+ /* Database supports INSERT .. ON DUPLICATE KEY syntax. */
+ SQL_DB_FLAG_ON_DUPLICATE_KEY = 0x08,
+ /* Database supports INSERT .. ON CONFLICT DO UPDATE syntax. */
+ SQL_DB_FLAG_ON_CONFLICT_DO = 0x10,
+};
+
+enum sql_field_type {
+ SQL_TYPE_STR,
+ SQL_TYPE_UINT,
+ SQL_TYPE_ULLONG,
+ SQL_TYPE_BOOL
+};
+
+struct sql_field_def {
+ enum sql_field_type type;
+ const char *name;
+ size_t offset;
+};
+
+enum sql_result_error_type {
+ SQL_RESULT_ERROR_TYPE_UNKNOWN = 0,
+ /* It's unknown whether write succeeded or not. This could be due to
+ a timeout or a disconnection from server. */
+ SQL_RESULT_ERROR_TYPE_WRITE_UNCERTAIN
+};
+
+enum sql_result_next {
+ /* Row was returned */
+ SQL_RESULT_NEXT_OK = 1,
+ /* There are no more rows */
+ SQL_RESULT_NEXT_LAST = 0,
+ /* Error occurred - see sql_result_get_error*() */
+ SQL_RESULT_NEXT_ERROR = -1,
+ /* There are more results - call sql_result_more() */
+ SQL_RESULT_NEXT_MORE = -99
+};
+
+#define SQL_DEF_STRUCT(name, struct_name, type, c_type) \
+ { (type) + COMPILE_ERROR_IF_TYPES_NOT_COMPATIBLE( \
+ ((struct struct_name *)0)->name, c_type), \
+ #name, offsetof(struct struct_name, name) }
+
+#define SQL_DEF_STRUCT_STR(name, struct_name) \
+ SQL_DEF_STRUCT(name, struct_name, SQL_TYPE_STR, const char *)
+#define SQL_DEF_STRUCT_UINT(name, struct_name) \
+ SQL_DEF_STRUCT(name, struct_name, SQL_TYPE_UINT, unsigned int)
+#define SQL_DEF_STRUCT_ULLONG(name, struct_name) \
+ SQL_DEF_STRUCT(name, struct_name, SQL_TYPE_ULLONG, unsigned long long)
+#define SQL_DEF_STRUCT_BOOL(name, struct_name) \
+ SQL_DEF_STRUCT(name, struct_name, SQL_TYPE_BOOL, bool)
+
+struct sql_db;
+struct sql_result;
+
+struct sql_commit_result {
+ const char *error;
+ enum sql_result_error_type error_type;
+};
+
+struct sql_settings {
+ const char *driver;
+ const char *connect_string;
+ struct event *event_parent;
+};
+
+typedef void sql_query_callback_t(struct sql_result *result, void *context);
+typedef void sql_commit_callback_t(const struct sql_commit_result *result, void *context);
+
+void sql_drivers_init(void);
+void sql_drivers_deinit(void);
+
+/* register all built-in SQL drivers */
+void sql_drivers_register_all(void);
+
+void sql_driver_register(const struct sql_db *driver);
+void sql_driver_unregister(const struct sql_db *driver);
+
+/* Initialize database connections. db_driver is the database driver name,
+ eg. "mysql" or "pgsql". connect_string is driver-specific. */
+struct sql_db *sql_init(const char *db_driver, const char *connect_string);
+int sql_init_full(const struct sql_settings *set, struct sql_db **db_r,
+ const char **error_r);
+
+void sql_ref(struct sql_db *db);
+void sql_unref(struct sql_db **db);
+
+/* Returns SQL database state flags. */
+enum sql_db_flags sql_get_flags(struct sql_db *db);
+
+/* Explicitly connect to the database. It's not required to call this function
+ though. Returns -1 if we're not connected, 0 if we started connecting or
+ 1 if we are fully connected now. */
+int sql_connect(struct sql_db *db);
+/* Explicitly disconnect from database and abort pending auth requests. */
+void sql_disconnect(struct sql_db *db);
+
+/* Escape the given string if needed and return it. */
+const char *sql_escape_string(struct sql_db *db, const char *string);
+/* Escape the given data as a string. */
+const char *sql_escape_blob(struct sql_db *db,
+ const unsigned char *data, size_t size);
+
+/* Execute SQL query without waiting for results. */
+void sql_exec(struct sql_db *db, const char *query);
+/* Execute SQL query and return result in callback. If fields list is given,
+ the returned fields are validated to be of correct type, and you can use
+ sql_result_next_row_get() */
+void sql_query(struct sql_db *db, const char *query,
+ sql_query_callback_t *callback, void *context);
+#define sql_query(db, query, callback, context) \
+ sql_query(db, query - \
+ CALLBACK_TYPECHECK(callback, void (*)( \
+ struct sql_result *, typeof(context))), \
+ (sql_query_callback_t *)callback, context)
+/* Execute blocking SQL query and return result. */
+struct sql_result *sql_query_s(struct sql_db *db, const char *query);
+
+struct sql_prepared_statement *
+sql_prepared_statement_init(struct sql_db *db, const char *query_template);
+void sql_prepared_statement_unref(struct sql_prepared_statement **prep_stmt);
+
+struct sql_statement *
+sql_statement_init(struct sql_db *db, const char *query_template);
+struct sql_statement *
+sql_statement_init_prepared(struct sql_prepared_statement *prep_stmt);
+void sql_statement_abort(struct sql_statement **stmt);
+void sql_statement_set_timestamp(struct sql_statement *stmt,
+ const struct timespec *ts);
+void sql_statement_bind_str(struct sql_statement *stmt,
+ unsigned int column_idx, const char *value);
+void sql_statement_bind_binary(struct sql_statement *stmt,
+ unsigned int column_idx, const void *value,
+ size_t value_size);
+void sql_statement_bind_int64(struct sql_statement *stmt,
+ unsigned int column_idx, int64_t value);
+void sql_statement_query(struct sql_statement **stmt,
+ sql_query_callback_t *callback, void *context);
+#define sql_statement_query(stmt, callback, context) \
+ sql_statement_query(stmt, \
+ (sql_query_callback_t *)callback, TRUE ? context : \
+ CALLBACK_TYPECHECK(callback, void (*)( \
+ struct sql_result *, typeof(context))))
+struct sql_result *sql_statement_query_s(struct sql_statement **stmt);
+
+void sql_result_setup_fetch(struct sql_result *result,
+ const struct sql_field_def *fields,
+ void *dest, size_t dest_size);
+
+/* Go to next row. See enum sql_result_next. */
+int sql_result_next_row(struct sql_result *result);
+
+/* If sql_result_next_row() returned SQL_RESULT_NEXT_MORE, this can be called
+ to continue returning more results. The result is freed with this call, so
+ it must not be accesed anymore until the callback is finished. */
+void sql_result_more(struct sql_result **result,
+ sql_query_callback_t *callback, void *context);
+#define sql_result_more(result, callback, context) \
+ sql_result_more(result - \
+ CALLBACK_TYPECHECK(callback, void (*)( \
+ struct sql_result *, typeof(context))), \
+ (sql_query_callback_t *)callback, context)
+/* Synchronous version of sql_result_more(). The result will be replaced with
+ the new result. */
+void sql_result_more_s(struct sql_result **result);
+
+void sql_result_ref(struct sql_result *result);
+/* Needs to be called only with sql_query_s() or when result has been
+ explicitly referenced. */
+void sql_result_unref(struct sql_result *result);
+
+/* Return number of fields in result. */
+unsigned int sql_result_get_fields_count(struct sql_result *result);
+/* Return name of the given field index. */
+const char *sql_result_get_field_name(struct sql_result *result,
+ unsigned int idx);
+/* Return field index for given name, or -1 if not found. */
+int sql_result_find_field(struct sql_result *result, const char *field_name);
+
+/* Returns value of given field as string. Note that it can be NULL. */
+const char *sql_result_get_field_value(struct sql_result *result,
+ unsigned int idx);
+/* Returns a binary value. Note that a NULL is returned as NULL with size=0,
+ while empty string returns non-NULL with size=0. */
+const unsigned char *
+sql_result_get_field_value_binary(struct sql_result *result,
+ unsigned int idx, size_t *size_r);
+/* Find the field and return its value. NULL return value can mean that either
+ the field didn't exist or that its value is NULL. */
+const char *sql_result_find_field_value(struct sql_result *result,
+ const char *field_name);
+/* Return all values of current row. Note that this array is not
+ NULL-terminated - you must use sql_result_get_fields_count() to find out
+ the array's length. It's also possible that some of the values inside the
+ array are NULL. */
+const char *const *sql_result_get_values(struct sql_result *result);
+
+/* Return last error message in result. */
+const char *sql_result_get_error(struct sql_result *result);
+enum sql_result_error_type sql_result_get_error_type(struct sql_result *result);
+
+/* Begin a new transaction. Currently you're limited to only one open
+ transaction at a time. */
+struct sql_transaction_context *sql_transaction_begin(struct sql_db *db);
+/* Commit transaction. */
+void sql_transaction_commit(struct sql_transaction_context **ctx,
+ sql_commit_callback_t *callback, void *context);
+#define sql_transaction_commit(ctx, callback, context) \
+ sql_transaction_commit(ctx - \
+ CALLBACK_TYPECHECK(callback, void (*)( \
+ const struct sql_commit_result *, typeof(context))), \
+ (sql_commit_callback_t *)callback, context)
+/* Synchronous commit. Returns 0 if ok, -1 if error. */
+int sql_transaction_commit_s(struct sql_transaction_context **ctx,
+ const char **error_r);
+void sql_transaction_rollback(struct sql_transaction_context **ctx);
+
+/* Execute query in given transaction. */
+void sql_update(struct sql_transaction_context *ctx, const char *query);
+void sql_update_stmt(struct sql_transaction_context *ctx,
+ struct sql_statement **stmt);
+/* Save the number of rows updated by this query. The value is set before
+ commit callback is called. */
+void sql_update_get_rows(struct sql_transaction_context *ctx, const char *query,
+ unsigned int *affected_rows);
+void sql_update_stmt_get_rows(struct sql_transaction_context *ctx,
+ struct sql_statement **stmt,
+ unsigned int *affected_rows);
+
+/* Wait for SQL query results. */
+void sql_wait(struct sql_db *db);
+
+#endif
diff --git a/src/lib-sql/sql-db-cache.c b/src/lib-sql/sql-db-cache.c
new file mode 100644
index 0000000..b2fb9fb
--- /dev/null
+++ b/src/lib-sql/sql-db-cache.c
@@ -0,0 +1,156 @@
+/* Copyright (c) 2004-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "hash.h"
+#include "sql-api-private.h"
+#include "sql-db-cache.h"
+
+#define SQL_DB_CACHE_CONTEXT(obj) \
+ MODULE_CONTEXT_REQUIRE(obj, sql_db_cache_module)
+
+struct sql_db_cache_context {
+ union sql_db_module_context module_ctx;
+ struct sql_db *prev, *next; /* These are set while refcount=0 */
+
+ struct sql_db_cache *cache;
+ int refcount;
+ char *key;
+ void (*orig_deinit)(struct sql_db *db);
+};
+
+struct sql_db_cache {
+ HASH_TABLE(char *, struct sql_db *) dbs;
+ unsigned int unused_count, max_unused_connections;
+ struct sql_db *unused_tail, *unused_head;
+};
+
+static MODULE_CONTEXT_DEFINE_INIT(sql_db_cache_module, &sql_db_module_register);
+
+static void sql_db_cache_db_unref(struct sql_db *db)
+{
+ struct sql_db_cache_context *ctx = SQL_DB_CACHE_CONTEXT(db);
+ struct sql_db_cache_context *head_ctx;
+
+ if (--ctx->refcount > 0)
+ return;
+
+ i_assert(db->refcount == 2);
+
+ ctx->cache->unused_count++;
+ if (ctx->cache->unused_tail == NULL)
+ ctx->cache->unused_tail = db;
+ else {
+ head_ctx = SQL_DB_CACHE_CONTEXT(ctx->cache->unused_head);
+ head_ctx->next = db;
+ }
+ ctx->prev = ctx->cache->unused_head;
+ ctx->cache->unused_head = db;
+}
+
+static void sql_db_cache_unlink(struct sql_db_cache_context *ctx)
+{
+ struct sql_db_cache_context *prev_ctx, *next_ctx;
+
+ i_assert(ctx->refcount == 0);
+
+ if (ctx->prev == NULL)
+ ctx->cache->unused_tail = ctx->next;
+ else {
+ prev_ctx = SQL_DB_CACHE_CONTEXT(ctx->prev);
+ prev_ctx->next = ctx->next;
+ }
+ if (ctx->next == NULL)
+ ctx->cache->unused_head = ctx->prev;
+ else {
+ next_ctx = SQL_DB_CACHE_CONTEXT(ctx->next);
+ next_ctx->prev = ctx->prev;
+ }
+ ctx->cache->unused_count--;
+}
+
+static void sql_db_cache_free_tail(struct sql_db_cache *cache)
+{
+ struct sql_db *db;
+ struct sql_db_cache_context *ctx;
+
+ db = cache->unused_tail;
+ i_assert(db->refcount == 1);
+
+ ctx = SQL_DB_CACHE_CONTEXT(db);
+ sql_db_cache_unlink(ctx);
+ hash_table_remove(cache->dbs, ctx->key);
+
+ i_free(ctx->key);
+ i_free(ctx);
+
+ db->v.unref = NULL;
+ sql_unref(&db);
+}
+
+static void sql_db_cache_drop_oldest(struct sql_db_cache *cache)
+{
+ while (cache->unused_count >= cache->max_unused_connections)
+ sql_db_cache_free_tail(cache);
+}
+
+int sql_db_cache_new(struct sql_db_cache *cache, const struct sql_settings *set,
+ struct sql_db **db_r, const char **error_r)
+{
+ struct sql_db_cache_context *ctx;
+ struct sql_db *db;
+ char *key;
+
+ key = i_strdup_printf("%s\t%s", set->driver, set->connect_string);
+ db = hash_table_lookup(cache->dbs, key);
+ if (db != NULL) {
+ ctx = SQL_DB_CACHE_CONTEXT(db);
+ if (ctx->refcount == 0) {
+ sql_db_cache_unlink(ctx);
+ ctx->prev = ctx->next = NULL;
+ }
+ i_free(key);
+ } else {
+ sql_db_cache_drop_oldest(cache);
+
+ if (sql_init_full(set, &db, error_r) < 0) {
+ i_free(key);
+ return -1;
+ }
+
+ ctx = i_new(struct sql_db_cache_context, 1);
+ ctx->cache = cache;
+ ctx->key = key;
+ ctx->orig_deinit = db->v.deinit;
+ db->v.unref = sql_db_cache_db_unref;
+
+ MODULE_CONTEXT_SET(db, sql_db_cache_module, ctx);
+ hash_table_insert(cache->dbs, ctx->key, db);
+ }
+
+ ctx->refcount++;
+ sql_ref(db);
+ *db_r = db;
+ return 0;
+}
+
+struct sql_db_cache *sql_db_cache_init(unsigned int max_unused_connections)
+{
+ struct sql_db_cache *cache;
+
+ cache = i_new(struct sql_db_cache, 1);
+ hash_table_create(&cache->dbs, default_pool, 0, str_hash, strcmp);
+ cache->max_unused_connections = max_unused_connections;
+ return cache;
+}
+
+void sql_db_cache_deinit(struct sql_db_cache **_cache)
+{
+ struct sql_db_cache *cache = *_cache;
+
+ *_cache = NULL;
+ while (cache->unused_tail != NULL)
+ sql_db_cache_free_tail(cache);
+ hash_table_destroy(&cache->dbs);
+ i_free(cache);
+}
diff --git a/src/lib-sql/sql-db-cache.h b/src/lib-sql/sql-db-cache.h
new file mode 100644
index 0000000..1517f5f
--- /dev/null
+++ b/src/lib-sql/sql-db-cache.h
@@ -0,0 +1,13 @@
+#ifndef SQL_DB_CACHE_H
+#define SQL_DB_CACHE_H
+
+struct sql_db_cache;
+
+/* Like sql_init(), but use a connection pool. */
+int sql_db_cache_new(struct sql_db_cache *cache, const struct sql_settings *set,
+ struct sql_db **db_r, const char **error_r);
+
+struct sql_db_cache *sql_db_cache_init(unsigned int max_unused_connections);
+void sql_db_cache_deinit(struct sql_db_cache **cache);
+
+#endif
diff --git a/src/lib-ssl-iostream/Makefile.am b/src/lib-ssl-iostream/Makefile.am
new file mode 100644
index 0000000..5aaea5d
--- /dev/null
+++ b/src/lib-ssl-iostream/Makefile.am
@@ -0,0 +1,61 @@
+noinst_LTLIBRARIES = libssl_iostream.la
+
+NOPLUGIN_LDFLAGS =
+
+AM_CPPFLAGS = \
+ -I$(top_srcdir)/src/lib \
+ -I$(top_srcdir)/src/lib-test \
+ -DMODULE_DIR=\""$(moduledir)"\"
+
+if BUILD_OPENSSL
+module_LTLIBRARIES = libssl_iostream_openssl.la
+
+libssl_iostream_openssl_la_LDFLAGS = -module -avoid-version
+libssl_iostream_openssl_la_LIBADD = $(SSL_LIBS)
+libssl_iostream_openssl_la_SOURCES = \
+ dovecot-openssl-common.c \
+ iostream-openssl.c \
+ iostream-openssl-common.c \
+ iostream-openssl-context.c \
+ istream-openssl.c \
+ ostream-openssl.c
+endif
+
+libssl_iostream_la_SOURCES = \
+ iostream-ssl.c \
+ iostream-ssl-context-cache.c \
+ iostream-ssl-test.c
+
+noinst_HEADERS = \
+ dovecot-openssl-common.h
+
+headers = \
+ iostream-openssl.h \
+ iostream-ssl.h \
+ iostream-ssl-private.h \
+ iostream-ssl-test.h
+
+pkginc_libdir=$(pkgincludedir)
+pkginc_lib_HEADERS = $(headers)
+
+if BUILD_OPENSSL
+test_libs = \
+ $(module_LTLIBRARIES) \
+ $(noinst_LTLIBRARIES) \
+ ../lib-test/libtest.la \
+ ../lib/liblib.la
+
+test_iostream_ssl_SOURCES = test-iostream-ssl.c
+test_iostream_ssl_LDADD = $(test_libs) $(SSL_LIBS) $(DLLIB)
+test_iostream_ssl_DEPENDENCIES = $(test_libs)
+
+test_programs = \
+ test-iostream-ssl
+
+noinst_PROGRAMS = $(test_programs)
+
+check-local:
+ for bin in $(test_programs); do \
+ if ! $(RUN_TEST) ./$$bin; then exit 1; fi; \
+ done
+endif
diff --git a/src/lib-ssl-iostream/Makefile.in b/src/lib-ssl-iostream/Makefile.in
new file mode 100644
index 0000000..3fff60f
--- /dev/null
+++ b/src/lib-ssl-iostream/Makefile.in
@@ -0,0 +1,970 @@
+# Makefile.in generated by automake 1.16.1 from Makefile.am.
+# @configure_input@
+
+# Copyright (C) 1994-2018 Free Software Foundation, Inc.
+
+# This Makefile.in is free software; the Free Software Foundation
+# gives unlimited permission to copy and/or distribute it,
+# with or without modifications, as long as this notice is preserved.
+
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY, to the extent permitted by law; without
+# even the implied warranty of MERCHANTABILITY or FITNESS FOR A
+# PARTICULAR PURPOSE.
+
+@SET_MAKE@
+
+
+
+VPATH = @srcdir@
+am__is_gnu_make = { \
+ if test -z '$(MAKELEVEL)'; then \
+ false; \
+ elif test -n '$(MAKE_HOST)'; then \
+ true; \
+ elif test -n '$(MAKE_VERSION)' && test -n '$(CURDIR)'; then \
+ true; \
+ else \
+ false; \
+ fi; \
+}
+am__make_running_with_option = \
+ case $${target_option-} in \
+ ?) ;; \
+ *) echo "am__make_running_with_option: internal error: invalid" \
+ "target option '$${target_option-}' specified" >&2; \
+ exit 1;; \
+ esac; \
+ has_opt=no; \
+ sane_makeflags=$$MAKEFLAGS; \
+ if $(am__is_gnu_make); then \
+ sane_makeflags=$$MFLAGS; \
+ else \
+ case $$MAKEFLAGS in \
+ *\\[\ \ ]*) \
+ bs=\\; \
+ sane_makeflags=`printf '%s\n' "$$MAKEFLAGS" \
+ | sed "s/$$bs$$bs[$$bs $$bs ]*//g"`;; \
+ esac; \
+ fi; \
+ skip_next=no; \
+ strip_trailopt () \
+ { \
+ flg=`printf '%s\n' "$$flg" | sed "s/$$1.*$$//"`; \
+ }; \
+ for flg in $$sane_makeflags; do \
+ test $$skip_next = yes && { skip_next=no; continue; }; \
+ case $$flg in \
+ *=*|--*) continue;; \
+ -*I) strip_trailopt 'I'; skip_next=yes;; \
+ -*I?*) strip_trailopt 'I';; \
+ -*O) strip_trailopt 'O'; skip_next=yes;; \
+ -*O?*) strip_trailopt 'O';; \
+ -*l) strip_trailopt 'l'; skip_next=yes;; \
+ -*l?*) strip_trailopt 'l';; \
+ -[dEDm]) skip_next=yes;; \
+ -[JT]) skip_next=yes;; \
+ esac; \
+ case $$flg in \
+ *$$target_option*) has_opt=yes; break;; \
+ esac; \
+ done; \
+ test $$has_opt = yes
+am__make_dryrun = (target_option=n; $(am__make_running_with_option))
+am__make_keepgoing = (target_option=k; $(am__make_running_with_option))
+pkgdatadir = $(datadir)/@PACKAGE@
+pkgincludedir = $(includedir)/@PACKAGE@
+pkglibdir = $(libdir)/@PACKAGE@
+pkglibexecdir = $(libexecdir)/@PACKAGE@
+am__cd = CDPATH="$${ZSH_VERSION+.}$(PATH_SEPARATOR)" && cd
+install_sh_DATA = $(install_sh) -c -m 644
+install_sh_PROGRAM = $(install_sh) -c
+install_sh_SCRIPT = $(install_sh) -c
+INSTALL_HEADER = $(INSTALL_DATA)
+transform = $(program_transform_name)
+NORMAL_INSTALL = :
+PRE_INSTALL = :
+POST_INSTALL = :
+NORMAL_UNINSTALL = :
+PRE_UNINSTALL = :
+POST_UNINSTALL = :
+build_triplet = @build@
+host_triplet = @host@
+@BUILD_OPENSSL_TRUE@noinst_PROGRAMS = $(am__EXEEXT_1)
+subdir = src/lib-ssl-iostream
+ACLOCAL_M4 = $(top_srcdir)/aclocal.m4
+am__aclocal_m4_deps = $(top_srcdir)/m4/ac_checktype2.m4 \
+ $(top_srcdir)/m4/ac_typeof.m4 $(top_srcdir)/m4/arc4random.m4 \
+ $(top_srcdir)/m4/blockdev.m4 $(top_srcdir)/m4/c99_vsnprintf.m4 \
+ $(top_srcdir)/m4/clock_gettime.m4 $(top_srcdir)/m4/crypt.m4 \
+ $(top_srcdir)/m4/crypt_xpg6.m4 $(top_srcdir)/m4/dbqlk.m4 \
+ $(top_srcdir)/m4/dirent_dtype.m4 $(top_srcdir)/m4/dovecot.m4 \
+ $(top_srcdir)/m4/fd_passing.m4 $(top_srcdir)/m4/fdatasync.m4 \
+ $(top_srcdir)/m4/flexible_array_member.m4 \
+ $(top_srcdir)/m4/glibc.m4 $(top_srcdir)/m4/gmtime_max.m4 \
+ $(top_srcdir)/m4/gmtime_tm_gmtoff.m4 \
+ $(top_srcdir)/m4/ioloop.m4 $(top_srcdir)/m4/iovec.m4 \
+ $(top_srcdir)/m4/ipv6.m4 $(top_srcdir)/m4/libcap.m4 \
+ $(top_srcdir)/m4/libtool.m4 $(top_srcdir)/m4/libwrap.m4 \
+ $(top_srcdir)/m4/linux_mremap.m4 $(top_srcdir)/m4/ltoptions.m4 \
+ $(top_srcdir)/m4/ltsugar.m4 $(top_srcdir)/m4/ltversion.m4 \
+ $(top_srcdir)/m4/lt~obsolete.m4 $(top_srcdir)/m4/mmap_write.m4 \
+ $(top_srcdir)/m4/mntctl.m4 $(top_srcdir)/m4/modules.m4 \
+ $(top_srcdir)/m4/notify.m4 $(top_srcdir)/m4/nsl.m4 \
+ $(top_srcdir)/m4/off_t_max.m4 $(top_srcdir)/m4/pkg.m4 \
+ $(top_srcdir)/m4/pr_set_dumpable.m4 \
+ $(top_srcdir)/m4/q_quotactl.m4 $(top_srcdir)/m4/quota.m4 \
+ $(top_srcdir)/m4/random.m4 $(top_srcdir)/m4/rlimit.m4 \
+ $(top_srcdir)/m4/sendfile.m4 $(top_srcdir)/m4/size_t_signed.m4 \
+ $(top_srcdir)/m4/sockpeercred.m4 $(top_srcdir)/m4/sql.m4 \
+ $(top_srcdir)/m4/ssl.m4 $(top_srcdir)/m4/st_tim.m4 \
+ $(top_srcdir)/m4/static_array.m4 $(top_srcdir)/m4/test_with.m4 \
+ $(top_srcdir)/m4/time_t.m4 $(top_srcdir)/m4/typeof.m4 \
+ $(top_srcdir)/m4/typeof_dev_t.m4 \
+ $(top_srcdir)/m4/uoff_t_max.m4 $(top_srcdir)/m4/vararg.m4 \
+ $(top_srcdir)/m4/want_apparmor.m4 \
+ $(top_srcdir)/m4/want_bsdauth.m4 \
+ $(top_srcdir)/m4/want_bzlib.m4 \
+ $(top_srcdir)/m4/want_cassandra.m4 \
+ $(top_srcdir)/m4/want_cdb.m4 \
+ $(top_srcdir)/m4/want_checkpassword.m4 \
+ $(top_srcdir)/m4/want_clucene.m4 $(top_srcdir)/m4/want_db.m4 \
+ $(top_srcdir)/m4/want_gssapi.m4 $(top_srcdir)/m4/want_icu.m4 \
+ $(top_srcdir)/m4/want_ldap.m4 $(top_srcdir)/m4/want_lua.m4 \
+ $(top_srcdir)/m4/want_lz4.m4 $(top_srcdir)/m4/want_lzma.m4 \
+ $(top_srcdir)/m4/want_mysql.m4 $(top_srcdir)/m4/want_pam.m4 \
+ $(top_srcdir)/m4/want_passwd.m4 $(top_srcdir)/m4/want_pgsql.m4 \
+ $(top_srcdir)/m4/want_prefetch.m4 \
+ $(top_srcdir)/m4/want_shadow.m4 \
+ $(top_srcdir)/m4/want_sodium.m4 $(top_srcdir)/m4/want_solr.m4 \
+ $(top_srcdir)/m4/want_sqlite.m4 \
+ $(top_srcdir)/m4/want_stemmer.m4 \
+ $(top_srcdir)/m4/want_systemd.m4 \
+ $(top_srcdir)/m4/want_textcat.m4 \
+ $(top_srcdir)/m4/want_unwind.m4 $(top_srcdir)/m4/want_zlib.m4 \
+ $(top_srcdir)/m4/want_zstd.m4 $(top_srcdir)/configure.ac
+am__configure_deps = $(am__aclocal_m4_deps) $(CONFIGURE_DEPENDENCIES) \
+ $(ACLOCAL_M4)
+DIST_COMMON = $(srcdir)/Makefile.am $(noinst_HEADERS) \
+ $(pkginc_lib_HEADERS) $(am__DIST_COMMON)
+mkinstalldirs = $(install_sh) -d
+CONFIG_HEADER = $(top_builddir)/config.h
+CONFIG_CLEAN_FILES =
+CONFIG_CLEAN_VPATH_FILES =
+@BUILD_OPENSSL_TRUE@am__EXEEXT_1 = test-iostream-ssl$(EXEEXT)
+PROGRAMS = $(noinst_PROGRAMS)
+am__vpath_adj_setup = srcdirstrip=`echo "$(srcdir)" | sed 's|.|.|g'`;
+am__vpath_adj = case $$p in \
+ $(srcdir)/*) f=`echo "$$p" | sed "s|^$$srcdirstrip/||"`;; \
+ *) f=$$p;; \
+ esac;
+am__strip_dir = f=`echo $$p | sed -e 's|^.*/||'`;
+am__install_max = 40
+am__nobase_strip_setup = \
+ srcdirstrip=`echo "$(srcdir)" | sed 's/[].[^$$\\*|]/\\\\&/g'`
+am__nobase_strip = \
+ for p in $$list; do echo "$$p"; done | sed -e "s|$$srcdirstrip/||"
+am__nobase_list = $(am__nobase_strip_setup); \
+ for p in $$list; do echo "$$p $$p"; done | \
+ sed "s| $$srcdirstrip/| |;"' / .*\//!s/ .*/ ./; s,\( .*\)/[^/]*$$,\1,' | \
+ $(AWK) 'BEGIN { files["."] = "" } { files[$$2] = files[$$2] " " $$1; \
+ if (++n[$$2] == $(am__install_max)) \
+ { print $$2, files[$$2]; n[$$2] = 0; files[$$2] = "" } } \
+ END { for (dir in files) print dir, files[dir] }'
+am__base_list = \
+ sed '$$!N;$$!N;$$!N;$$!N;$$!N;$$!N;$$!N;s/\n/ /g' | \
+ sed '$$!N;$$!N;$$!N;$$!N;s/\n/ /g'
+am__uninstall_files_from_dir = { \
+ test -z "$$files" \
+ || { test ! -d "$$dir" && test ! -f "$$dir" && test ! -r "$$dir"; } \
+ || { echo " ( cd '$$dir' && rm -f" $$files ")"; \
+ $(am__cd) "$$dir" && rm -f $$files; }; \
+ }
+am__installdirs = "$(DESTDIR)$(moduledir)" \
+ "$(DESTDIR)$(pkginc_libdir)"
+LTLIBRARIES = $(module_LTLIBRARIES) $(noinst_LTLIBRARIES)
+libssl_iostream_la_LIBADD =
+am_libssl_iostream_la_OBJECTS = iostream-ssl.lo \
+ iostream-ssl-context-cache.lo iostream-ssl-test.lo
+libssl_iostream_la_OBJECTS = $(am_libssl_iostream_la_OBJECTS)
+AM_V_lt = $(am__v_lt_@AM_V@)
+am__v_lt_ = $(am__v_lt_@AM_DEFAULT_V@)
+am__v_lt_0 = --silent
+am__v_lt_1 =
+am__DEPENDENCIES_1 =
+@BUILD_OPENSSL_TRUE@libssl_iostream_openssl_la_DEPENDENCIES = \
+@BUILD_OPENSSL_TRUE@ $(am__DEPENDENCIES_1)
+am__libssl_iostream_openssl_la_SOURCES_DIST = \
+ dovecot-openssl-common.c iostream-openssl.c \
+ iostream-openssl-common.c iostream-openssl-context.c \
+ istream-openssl.c ostream-openssl.c
+@BUILD_OPENSSL_TRUE@am_libssl_iostream_openssl_la_OBJECTS = \
+@BUILD_OPENSSL_TRUE@ dovecot-openssl-common.lo \
+@BUILD_OPENSSL_TRUE@ iostream-openssl.lo \
+@BUILD_OPENSSL_TRUE@ iostream-openssl-common.lo \
+@BUILD_OPENSSL_TRUE@ iostream-openssl-context.lo \
+@BUILD_OPENSSL_TRUE@ istream-openssl.lo ostream-openssl.lo
+libssl_iostream_openssl_la_OBJECTS = \
+ $(am_libssl_iostream_openssl_la_OBJECTS)
+libssl_iostream_openssl_la_LINK = $(LIBTOOL) $(AM_V_lt) --tag=CC \
+ $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=link $(CCLD) \
+ $(AM_CFLAGS) $(CFLAGS) $(libssl_iostream_openssl_la_LDFLAGS) \
+ $(LDFLAGS) -o $@
+@BUILD_OPENSSL_TRUE@am_libssl_iostream_openssl_la_rpath = -rpath \
+@BUILD_OPENSSL_TRUE@ $(moduledir)
+am__test_iostream_ssl_SOURCES_DIST = test-iostream-ssl.c
+@BUILD_OPENSSL_TRUE@am_test_iostream_ssl_OBJECTS = \
+@BUILD_OPENSSL_TRUE@ test-iostream-ssl.$(OBJEXT)
+test_iostream_ssl_OBJECTS = $(am_test_iostream_ssl_OBJECTS)
+AM_V_P = $(am__v_P_@AM_V@)
+am__v_P_ = $(am__v_P_@AM_DEFAULT_V@)
+am__v_P_0 = false
+am__v_P_1 = :
+AM_V_GEN = $(am__v_GEN_@AM_V@)
+am__v_GEN_ = $(am__v_GEN_@AM_DEFAULT_V@)
+am__v_GEN_0 = @echo " GEN " $@;
+am__v_GEN_1 =
+AM_V_at = $(am__v_at_@AM_V@)
+am__v_at_ = $(am__v_at_@AM_DEFAULT_V@)
+am__v_at_0 = @
+am__v_at_1 =
+DEFAULT_INCLUDES = -I.@am__isrc@ -I$(top_builddir)
+depcomp = $(SHELL) $(top_srcdir)/depcomp
+am__maybe_remake_depfiles = depfiles
+am__depfiles_remade = ./$(DEPDIR)/dovecot-openssl-common.Plo \
+ ./$(DEPDIR)/iostream-openssl-common.Plo \
+ ./$(DEPDIR)/iostream-openssl-context.Plo \
+ ./$(DEPDIR)/iostream-openssl.Plo \
+ ./$(DEPDIR)/iostream-ssl-context-cache.Plo \
+ ./$(DEPDIR)/iostream-ssl-test.Plo ./$(DEPDIR)/iostream-ssl.Plo \
+ ./$(DEPDIR)/istream-openssl.Plo \
+ ./$(DEPDIR)/ostream-openssl.Plo \
+ ./$(DEPDIR)/test-iostream-ssl.Po
+am__mv = mv -f
+COMPILE = $(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) \
+ $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS)
+LTCOMPILE = $(LIBTOOL) $(AM_V_lt) --tag=CC $(AM_LIBTOOLFLAGS) \
+ $(LIBTOOLFLAGS) --mode=compile $(CC) $(DEFS) \
+ $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) $(CPPFLAGS) \
+ $(AM_CFLAGS) $(CFLAGS)
+AM_V_CC = $(am__v_CC_@AM_V@)
+am__v_CC_ = $(am__v_CC_@AM_DEFAULT_V@)
+am__v_CC_0 = @echo " CC " $@;
+am__v_CC_1 =
+CCLD = $(CC)
+LINK = $(LIBTOOL) $(AM_V_lt) --tag=CC $(AM_LIBTOOLFLAGS) \
+ $(LIBTOOLFLAGS) --mode=link $(CCLD) $(AM_CFLAGS) $(CFLAGS) \
+ $(AM_LDFLAGS) $(LDFLAGS) -o $@
+AM_V_CCLD = $(am__v_CCLD_@AM_V@)
+am__v_CCLD_ = $(am__v_CCLD_@AM_DEFAULT_V@)
+am__v_CCLD_0 = @echo " CCLD " $@;
+am__v_CCLD_1 =
+SOURCES = $(libssl_iostream_la_SOURCES) \
+ $(libssl_iostream_openssl_la_SOURCES) \
+ $(test_iostream_ssl_SOURCES)
+DIST_SOURCES = $(libssl_iostream_la_SOURCES) \
+ $(am__libssl_iostream_openssl_la_SOURCES_DIST) \
+ $(am__test_iostream_ssl_SOURCES_DIST)
+am__can_run_installinfo = \
+ case $$AM_UPDATE_INFO_DIR in \
+ n|no|NO) false;; \
+ *) (install-info --version) >/dev/null 2>&1;; \
+ esac
+HEADERS = $(noinst_HEADERS) $(pkginc_lib_HEADERS)
+am__tagged_files = $(HEADERS) $(SOURCES) $(TAGS_FILES) $(LISP)
+# Read a list of newline-separated strings from the standard input,
+# and print each of them once, without duplicates. Input order is
+# *not* preserved.
+am__uniquify_input = $(AWK) '\
+ BEGIN { nonempty = 0; } \
+ { items[$$0] = 1; nonempty = 1; } \
+ END { if (nonempty) { for (i in items) print i; }; } \
+'
+# Make sure the list of sources is unique. This is necessary because,
+# e.g., the same source file might be shared among _SOURCES variables
+# for different programs/libraries.
+am__define_uniq_tagged_files = \
+ list='$(am__tagged_files)'; \
+ unique=`for i in $$list; do \
+ if test -f "$$i"; then echo $$i; else echo $(srcdir)/$$i; fi; \
+ done | $(am__uniquify_input)`
+ETAGS = etags
+CTAGS = ctags
+am__DIST_COMMON = $(srcdir)/Makefile.in $(top_srcdir)/depcomp
+DISTFILES = $(DIST_COMMON) $(DIST_SOURCES) $(TEXINFOS) $(EXTRA_DIST)
+ACLOCAL = @ACLOCAL@
+ACLOCAL_AMFLAGS = @ACLOCAL_AMFLAGS@
+AMTAR = @AMTAR@
+AM_DEFAULT_VERBOSITY = @AM_DEFAULT_VERBOSITY@
+APPARMOR_LIBS = @APPARMOR_LIBS@
+AR = @AR@
+AUTH_CFLAGS = @AUTH_CFLAGS@
+AUTH_LIBS = @AUTH_LIBS@
+AUTOCONF = @AUTOCONF@
+AUTOHEADER = @AUTOHEADER@
+AUTOMAKE = @AUTOMAKE@
+AWK = @AWK@
+BINARY_CFLAGS = @BINARY_CFLAGS@
+BINARY_LDFLAGS = @BINARY_LDFLAGS@
+BISON = @BISON@
+CASSANDRA_CFLAGS = @CASSANDRA_CFLAGS@
+CASSANDRA_LIBS = @CASSANDRA_LIBS@
+CC = @CC@
+CCDEPMODE = @CCDEPMODE@
+CDB_LIBS = @CDB_LIBS@
+CFLAGS = @CFLAGS@
+CLUCENE_CFLAGS = @CLUCENE_CFLAGS@
+CLUCENE_LIBS = @CLUCENE_LIBS@
+COMPRESS_LIBS = @COMPRESS_LIBS@
+CPP = @CPP@
+CPPFLAGS = @CPPFLAGS@
+CRYPT_LIBS = @CRYPT_LIBS@
+CXX = @CXX@
+CXXCPP = @CXXCPP@
+CXXDEPMODE = @CXXDEPMODE@
+CXXFLAGS = @CXXFLAGS@
+CYGPATH_W = @CYGPATH_W@
+DEFS = @DEFS@
+DEPDIR = @DEPDIR@
+DICT_LIBS = @DICT_LIBS@
+DLLIB = @DLLIB@
+DLLTOOL = @DLLTOOL@
+DSYMUTIL = @DSYMUTIL@
+DUMPBIN = @DUMPBIN@
+ECHO_C = @ECHO_C@
+ECHO_N = @ECHO_N@
+ECHO_T = @ECHO_T@
+EGREP = @EGREP@
+EXEEXT = @EXEEXT@
+FGREP = @FGREP@
+FLEX = @FLEX@
+FUZZER_CPPFLAGS = @FUZZER_CPPFLAGS@
+FUZZER_LDFLAGS = @FUZZER_LDFLAGS@
+GREP = @GREP@
+INSTALL = @INSTALL@
+INSTALL_DATA = @INSTALL_DATA@
+INSTALL_PROGRAM = @INSTALL_PROGRAM@
+INSTALL_SCRIPT = @INSTALL_SCRIPT@
+INSTALL_STRIP_PROGRAM = @INSTALL_STRIP_PROGRAM@
+KRB5CONFIG = @KRB5CONFIG@
+KRB5_CFLAGS = @KRB5_CFLAGS@
+KRB5_LIBS = @KRB5_LIBS@
+LD = @LD@
+LDAP_LIBS = @LDAP_LIBS@
+LDFLAGS = @LDFLAGS@
+LD_NO_WHOLE_ARCHIVE = @LD_NO_WHOLE_ARCHIVE@
+LD_WHOLE_ARCHIVE = @LD_WHOLE_ARCHIVE@
+LIBCAP = @LIBCAP@
+LIBDOVECOT = @LIBDOVECOT@
+LIBDOVECOT_COMPRESS = @LIBDOVECOT_COMPRESS@
+LIBDOVECOT_DEPS = @LIBDOVECOT_DEPS@
+LIBDOVECOT_DSYNC = @LIBDOVECOT_DSYNC@
+LIBDOVECOT_LA_LIBS = @LIBDOVECOT_LA_LIBS@
+LIBDOVECOT_LDA = @LIBDOVECOT_LDA@
+LIBDOVECOT_LDAP = @LIBDOVECOT_LDAP@
+LIBDOVECOT_LIBFTS = @LIBDOVECOT_LIBFTS@
+LIBDOVECOT_LIBFTS_DEPS = @LIBDOVECOT_LIBFTS_DEPS@
+LIBDOVECOT_LOGIN = @LIBDOVECOT_LOGIN@
+LIBDOVECOT_LUA = @LIBDOVECOT_LUA@
+LIBDOVECOT_LUA_DEPS = @LIBDOVECOT_LUA_DEPS@
+LIBDOVECOT_SQL = @LIBDOVECOT_SQL@
+LIBDOVECOT_STORAGE = @LIBDOVECOT_STORAGE@
+LIBDOVECOT_STORAGE_DEPS = @LIBDOVECOT_STORAGE_DEPS@
+LIBEXTTEXTCAT_CFLAGS = @LIBEXTTEXTCAT_CFLAGS@
+LIBEXTTEXTCAT_LIBS = @LIBEXTTEXTCAT_LIBS@
+LIBICONV = @LIBICONV@
+LIBICU_CFLAGS = @LIBICU_CFLAGS@
+LIBICU_LIBS = @LIBICU_LIBS@
+LIBOBJS = @LIBOBJS@
+LIBS = @LIBS@
+LIBSODIUM_CFLAGS = @LIBSODIUM_CFLAGS@
+LIBSODIUM_LIBS = @LIBSODIUM_LIBS@
+LIBTIRPC_CFLAGS = @LIBTIRPC_CFLAGS@
+LIBTIRPC_LIBS = @LIBTIRPC_LIBS@
+LIBTOOL = @LIBTOOL@
+LIBUNWIND_CFLAGS = @LIBUNWIND_CFLAGS@
+LIBUNWIND_LIBS = @LIBUNWIND_LIBS@
+LIBWRAP_LIBS = @LIBWRAP_LIBS@
+LINKED_STORAGE_LDADD = @LINKED_STORAGE_LDADD@
+LIPO = @LIPO@
+LN_S = @LN_S@
+LTLIBICONV = @LTLIBICONV@
+LTLIBOBJS = @LTLIBOBJS@
+LT_SYS_LIBRARY_PATH = @LT_SYS_LIBRARY_PATH@
+LUA_CFLAGS = @LUA_CFLAGS@
+LUA_LIBS = @LUA_LIBS@
+MAINT = @MAINT@
+MAKEINFO = @MAKEINFO@
+MANIFEST_TOOL = @MANIFEST_TOOL@
+MKDIR_P = @MKDIR_P@
+MODULE_LIBS = @MODULE_LIBS@
+MODULE_SUFFIX = @MODULE_SUFFIX@
+MYSQL_CFLAGS = @MYSQL_CFLAGS@
+MYSQL_CONFIG = @MYSQL_CONFIG@
+MYSQL_LIBS = @MYSQL_LIBS@
+NM = @NM@
+NMEDIT = @NMEDIT@
+NOPLUGIN_LDFLAGS =
+OBJDUMP = @OBJDUMP@
+OBJEXT = @OBJEXT@
+OTOOL = @OTOOL@
+OTOOL64 = @OTOOL64@
+PACKAGE = @PACKAGE@
+PACKAGE_BUGREPORT = @PACKAGE_BUGREPORT@
+PACKAGE_NAME = @PACKAGE_NAME@
+PACKAGE_STRING = @PACKAGE_STRING@
+PACKAGE_TARNAME = @PACKAGE_TARNAME@
+PACKAGE_URL = @PACKAGE_URL@
+PACKAGE_VERSION = @PACKAGE_VERSION@
+PANDOC = @PANDOC@
+PATH_SEPARATOR = @PATH_SEPARATOR@
+PGSQL_CFLAGS = @PGSQL_CFLAGS@
+PGSQL_LIBS = @PGSQL_LIBS@
+PG_CONFIG = @PG_CONFIG@
+PIE_CFLAGS = @PIE_CFLAGS@
+PIE_LDFLAGS = @PIE_LDFLAGS@
+PKG_CONFIG = @PKG_CONFIG@
+PKG_CONFIG_LIBDIR = @PKG_CONFIG_LIBDIR@
+PKG_CONFIG_PATH = @PKG_CONFIG_PATH@
+QUOTA_LIBS = @QUOTA_LIBS@
+RANLIB = @RANLIB@
+RELRO_LDFLAGS = @RELRO_LDFLAGS@
+RPCGEN = @RPCGEN@
+RUN_TEST = @RUN_TEST@
+SED = @SED@
+SETTING_FILES = @SETTING_FILES@
+SET_MAKE = @SET_MAKE@
+SHELL = @SHELL@
+SQLITE_CFLAGS = @SQLITE_CFLAGS@
+SQLITE_LIBS = @SQLITE_LIBS@
+SQL_CFLAGS = @SQL_CFLAGS@
+SQL_LIBS = @SQL_LIBS@
+SSL_CFLAGS = @SSL_CFLAGS@
+SSL_LIBS = @SSL_LIBS@
+STRIP = @STRIP@
+SYSTEMD_CFLAGS = @SYSTEMD_CFLAGS@
+SYSTEMD_LIBS = @SYSTEMD_LIBS@
+VALGRIND = @VALGRIND@
+VERSION = @VERSION@
+ZSTD_CFLAGS = @ZSTD_CFLAGS@
+ZSTD_LIBS = @ZSTD_LIBS@
+abs_builddir = @abs_builddir@
+abs_srcdir = @abs_srcdir@
+abs_top_builddir = @abs_top_builddir@
+abs_top_srcdir = @abs_top_srcdir@
+ac_ct_AR = @ac_ct_AR@
+ac_ct_CC = @ac_ct_CC@
+ac_ct_CXX = @ac_ct_CXX@
+ac_ct_DUMPBIN = @ac_ct_DUMPBIN@
+am__include = @am__include@
+am__leading_dot = @am__leading_dot@
+am__quote = @am__quote@
+am__tar = @am__tar@
+am__untar = @am__untar@
+bindir = @bindir@
+build = @build@
+build_alias = @build_alias@
+build_cpu = @build_cpu@
+build_os = @build_os@
+build_vendor = @build_vendor@
+builddir = @builddir@
+datadir = @datadir@
+datarootdir = @datarootdir@
+dict_drivers = @dict_drivers@
+docdir = @docdir@
+dvidir = @dvidir@
+exec_prefix = @exec_prefix@
+host = @host@
+host_alias = @host_alias@
+host_cpu = @host_cpu@
+host_os = @host_os@
+host_vendor = @host_vendor@
+htmldir = @htmldir@
+includedir = @includedir@
+infodir = @infodir@
+install_sh = @install_sh@
+libdir = @libdir@
+libexecdir = @libexecdir@
+localedir = @localedir@
+localstatedir = @localstatedir@
+mandir = @mandir@
+mkdir_p = @mkdir_p@
+moduledir = @moduledir@
+oldincludedir = @oldincludedir@
+pdfdir = @pdfdir@
+prefix = @prefix@
+program_transform_name = @program_transform_name@
+psdir = @psdir@
+rundir = @rundir@
+runstatedir = @runstatedir@
+sbindir = @sbindir@
+sharedstatedir = @sharedstatedir@
+sql_drivers = @sql_drivers@
+srcdir = @srcdir@
+ssldir = @ssldir@
+statedir = @statedir@
+sysconfdir = @sysconfdir@
+systemdservicetype = @systemdservicetype@
+systemdsystemunitdir = @systemdsystemunitdir@
+target_alias = @target_alias@
+top_build_prefix = @top_build_prefix@
+top_builddir = @top_builddir@
+top_srcdir = @top_srcdir@
+noinst_LTLIBRARIES = libssl_iostream.la
+AM_CPPFLAGS = \
+ -I$(top_srcdir)/src/lib \
+ -I$(top_srcdir)/src/lib-test \
+ -DMODULE_DIR=\""$(moduledir)"\"
+
+@BUILD_OPENSSL_TRUE@module_LTLIBRARIES = libssl_iostream_openssl.la
+@BUILD_OPENSSL_TRUE@libssl_iostream_openssl_la_LDFLAGS = -module -avoid-version
+@BUILD_OPENSSL_TRUE@libssl_iostream_openssl_la_LIBADD = $(SSL_LIBS)
+@BUILD_OPENSSL_TRUE@libssl_iostream_openssl_la_SOURCES = \
+@BUILD_OPENSSL_TRUE@ dovecot-openssl-common.c \
+@BUILD_OPENSSL_TRUE@ iostream-openssl.c \
+@BUILD_OPENSSL_TRUE@ iostream-openssl-common.c \
+@BUILD_OPENSSL_TRUE@ iostream-openssl-context.c \
+@BUILD_OPENSSL_TRUE@ istream-openssl.c \
+@BUILD_OPENSSL_TRUE@ ostream-openssl.c
+
+libssl_iostream_la_SOURCES = \
+ iostream-ssl.c \
+ iostream-ssl-context-cache.c \
+ iostream-ssl-test.c
+
+noinst_HEADERS = \
+ dovecot-openssl-common.h
+
+headers = \
+ iostream-openssl.h \
+ iostream-ssl.h \
+ iostream-ssl-private.h \
+ iostream-ssl-test.h
+
+pkginc_libdir = $(pkgincludedir)
+pkginc_lib_HEADERS = $(headers)
+@BUILD_OPENSSL_TRUE@test_libs = \
+@BUILD_OPENSSL_TRUE@ $(module_LTLIBRARIES) \
+@BUILD_OPENSSL_TRUE@ $(noinst_LTLIBRARIES) \
+@BUILD_OPENSSL_TRUE@ ../lib-test/libtest.la \
+@BUILD_OPENSSL_TRUE@ ../lib/liblib.la
+
+@BUILD_OPENSSL_TRUE@test_iostream_ssl_SOURCES = test-iostream-ssl.c
+@BUILD_OPENSSL_TRUE@test_iostream_ssl_LDADD = $(test_libs) $(SSL_LIBS) $(DLLIB)
+@BUILD_OPENSSL_TRUE@test_iostream_ssl_DEPENDENCIES = $(test_libs)
+@BUILD_OPENSSL_TRUE@test_programs = \
+@BUILD_OPENSSL_TRUE@ test-iostream-ssl
+
+all: all-am
+
+.SUFFIXES:
+.SUFFIXES: .c .lo .o .obj
+$(srcdir)/Makefile.in: @MAINTAINER_MODE_TRUE@ $(srcdir)/Makefile.am $(am__configure_deps)
+ @for dep in $?; do \
+ case '$(am__configure_deps)' in \
+ *$$dep*) \
+ ( cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh ) \
+ && { if test -f $@; then exit 0; else break; fi; }; \
+ exit 1;; \
+ esac; \
+ done; \
+ echo ' cd $(top_srcdir) && $(AUTOMAKE) --foreign src/lib-ssl-iostream/Makefile'; \
+ $(am__cd) $(top_srcdir) && \
+ $(AUTOMAKE) --foreign src/lib-ssl-iostream/Makefile
+Makefile: $(srcdir)/Makefile.in $(top_builddir)/config.status
+ @case '$?' in \
+ *config.status*) \
+ cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh;; \
+ *) \
+ echo ' cd $(top_builddir) && $(SHELL) ./config.status $(subdir)/$@ $(am__maybe_remake_depfiles)'; \
+ cd $(top_builddir) && $(SHELL) ./config.status $(subdir)/$@ $(am__maybe_remake_depfiles);; \
+ esac;
+
+$(top_builddir)/config.status: $(top_srcdir)/configure $(CONFIG_STATUS_DEPENDENCIES)
+ cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh
+
+$(top_srcdir)/configure: @MAINTAINER_MODE_TRUE@ $(am__configure_deps)
+ cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh
+$(ACLOCAL_M4): @MAINTAINER_MODE_TRUE@ $(am__aclocal_m4_deps)
+ cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh
+$(am__aclocal_m4_deps):
+
+clean-noinstPROGRAMS:
+ @list='$(noinst_PROGRAMS)'; test -n "$$list" || exit 0; \
+ echo " rm -f" $$list; \
+ rm -f $$list || exit $$?; \
+ test -n "$(EXEEXT)" || exit 0; \
+ list=`for p in $$list; do echo "$$p"; done | sed 's/$(EXEEXT)$$//'`; \
+ echo " rm -f" $$list; \
+ rm -f $$list
+
+install-moduleLTLIBRARIES: $(module_LTLIBRARIES)
+ @$(NORMAL_INSTALL)
+ @list='$(module_LTLIBRARIES)'; test -n "$(moduledir)" || list=; \
+ list2=; for p in $$list; do \
+ if test -f $$p; then \
+ list2="$$list2 $$p"; \
+ else :; fi; \
+ done; \
+ test -z "$$list2" || { \
+ echo " $(MKDIR_P) '$(DESTDIR)$(moduledir)'"; \
+ $(MKDIR_P) "$(DESTDIR)$(moduledir)" || exit 1; \
+ echo " $(LIBTOOL) $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=install $(INSTALL) $(INSTALL_STRIP_FLAG) $$list2 '$(DESTDIR)$(moduledir)'"; \
+ $(LIBTOOL) $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=install $(INSTALL) $(INSTALL_STRIP_FLAG) $$list2 "$(DESTDIR)$(moduledir)"; \
+ }
+
+uninstall-moduleLTLIBRARIES:
+ @$(NORMAL_UNINSTALL)
+ @list='$(module_LTLIBRARIES)'; test -n "$(moduledir)" || list=; \
+ for p in $$list; do \
+ $(am__strip_dir) \
+ echo " $(LIBTOOL) $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=uninstall rm -f '$(DESTDIR)$(moduledir)/$$f'"; \
+ $(LIBTOOL) $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=uninstall rm -f "$(DESTDIR)$(moduledir)/$$f"; \
+ done
+
+clean-moduleLTLIBRARIES:
+ -test -z "$(module_LTLIBRARIES)" || rm -f $(module_LTLIBRARIES)
+ @list='$(module_LTLIBRARIES)'; \
+ locs=`for p in $$list; do echo $$p; done | \
+ sed 's|^[^/]*$$|.|; s|/[^/]*$$||; s|$$|/so_locations|' | \
+ sort -u`; \
+ test -z "$$locs" || { \
+ echo rm -f $${locs}; \
+ rm -f $${locs}; \
+ }
+
+clean-noinstLTLIBRARIES:
+ -test -z "$(noinst_LTLIBRARIES)" || rm -f $(noinst_LTLIBRARIES)
+ @list='$(noinst_LTLIBRARIES)'; \
+ locs=`for p in $$list; do echo $$p; done | \
+ sed 's|^[^/]*$$|.|; s|/[^/]*$$||; s|$$|/so_locations|' | \
+ sort -u`; \
+ test -z "$$locs" || { \
+ echo rm -f $${locs}; \
+ rm -f $${locs}; \
+ }
+
+libssl_iostream.la: $(libssl_iostream_la_OBJECTS) $(libssl_iostream_la_DEPENDENCIES) $(EXTRA_libssl_iostream_la_DEPENDENCIES)
+ $(AM_V_CCLD)$(LINK) $(libssl_iostream_la_OBJECTS) $(libssl_iostream_la_LIBADD) $(LIBS)
+
+libssl_iostream_openssl.la: $(libssl_iostream_openssl_la_OBJECTS) $(libssl_iostream_openssl_la_DEPENDENCIES) $(EXTRA_libssl_iostream_openssl_la_DEPENDENCIES)
+ $(AM_V_CCLD)$(libssl_iostream_openssl_la_LINK) $(am_libssl_iostream_openssl_la_rpath) $(libssl_iostream_openssl_la_OBJECTS) $(libssl_iostream_openssl_la_LIBADD) $(LIBS)
+
+test-iostream-ssl$(EXEEXT): $(test_iostream_ssl_OBJECTS) $(test_iostream_ssl_DEPENDENCIES) $(EXTRA_test_iostream_ssl_DEPENDENCIES)
+ @rm -f test-iostream-ssl$(EXEEXT)
+ $(AM_V_CCLD)$(LINK) $(test_iostream_ssl_OBJECTS) $(test_iostream_ssl_LDADD) $(LIBS)
+
+mostlyclean-compile:
+ -rm -f *.$(OBJEXT)
+
+distclean-compile:
+ -rm -f *.tab.c
+
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/dovecot-openssl-common.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/iostream-openssl-common.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/iostream-openssl-context.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/iostream-openssl.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/iostream-ssl-context-cache.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/iostream-ssl-test.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/iostream-ssl.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/istream-openssl.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/ostream-openssl.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/test-iostream-ssl.Po@am__quote@ # am--include-marker
+
+$(am__depfiles_remade):
+ @$(MKDIR_P) $(@D)
+ @echo '# dummy' >$@-t && $(am__mv) $@-t $@
+
+am--depfiles: $(am__depfiles_remade)
+
+.c.o:
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(COMPILE) -MT $@ -MD -MP -MF $(DEPDIR)/$*.Tpo -c -o $@ $<
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/$*.Tpo $(DEPDIR)/$*.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='$<' object='$@' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(COMPILE) -c -o $@ $<
+
+.c.obj:
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(COMPILE) -MT $@ -MD -MP -MF $(DEPDIR)/$*.Tpo -c -o $@ `$(CYGPATH_W) '$<'`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/$*.Tpo $(DEPDIR)/$*.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='$<' object='$@' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(COMPILE) -c -o $@ `$(CYGPATH_W) '$<'`
+
+.c.lo:
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(LTCOMPILE) -MT $@ -MD -MP -MF $(DEPDIR)/$*.Tpo -c -o $@ $<
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/$*.Tpo $(DEPDIR)/$*.Plo
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='$<' object='$@' libtool=yes @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(LTCOMPILE) -c -o $@ $<
+
+mostlyclean-libtool:
+ -rm -f *.lo
+
+clean-libtool:
+ -rm -rf .libs _libs
+install-pkginc_libHEADERS: $(pkginc_lib_HEADERS)
+ @$(NORMAL_INSTALL)
+ @list='$(pkginc_lib_HEADERS)'; test -n "$(pkginc_libdir)" || list=; \
+ if test -n "$$list"; then \
+ echo " $(MKDIR_P) '$(DESTDIR)$(pkginc_libdir)'"; \
+ $(MKDIR_P) "$(DESTDIR)$(pkginc_libdir)" || exit 1; \
+ fi; \
+ for p in $$list; do \
+ if test -f "$$p"; then d=; else d="$(srcdir)/"; fi; \
+ echo "$$d$$p"; \
+ done | $(am__base_list) | \
+ while read files; do \
+ echo " $(INSTALL_HEADER) $$files '$(DESTDIR)$(pkginc_libdir)'"; \
+ $(INSTALL_HEADER) $$files "$(DESTDIR)$(pkginc_libdir)" || exit $$?; \
+ done
+
+uninstall-pkginc_libHEADERS:
+ @$(NORMAL_UNINSTALL)
+ @list='$(pkginc_lib_HEADERS)'; test -n "$(pkginc_libdir)" || list=; \
+ files=`for p in $$list; do echo $$p; done | sed -e 's|^.*/||'`; \
+ dir='$(DESTDIR)$(pkginc_libdir)'; $(am__uninstall_files_from_dir)
+
+ID: $(am__tagged_files)
+ $(am__define_uniq_tagged_files); mkid -fID $$unique
+tags: tags-am
+TAGS: tags
+
+tags-am: $(TAGS_DEPENDENCIES) $(am__tagged_files)
+ set x; \
+ here=`pwd`; \
+ $(am__define_uniq_tagged_files); \
+ shift; \
+ if test -z "$(ETAGS_ARGS)$$*$$unique"; then :; else \
+ test -n "$$unique" || unique=$$empty_fix; \
+ if test $$# -gt 0; then \
+ $(ETAGS) $(ETAGSFLAGS) $(AM_ETAGSFLAGS) $(ETAGS_ARGS) \
+ "$$@" $$unique; \
+ else \
+ $(ETAGS) $(ETAGSFLAGS) $(AM_ETAGSFLAGS) $(ETAGS_ARGS) \
+ $$unique; \
+ fi; \
+ fi
+ctags: ctags-am
+
+CTAGS: ctags
+ctags-am: $(TAGS_DEPENDENCIES) $(am__tagged_files)
+ $(am__define_uniq_tagged_files); \
+ test -z "$(CTAGS_ARGS)$$unique" \
+ || $(CTAGS) $(CTAGSFLAGS) $(AM_CTAGSFLAGS) $(CTAGS_ARGS) \
+ $$unique
+
+GTAGS:
+ here=`$(am__cd) $(top_builddir) && pwd` \
+ && $(am__cd) $(top_srcdir) \
+ && gtags -i $(GTAGS_ARGS) "$$here"
+cscopelist: cscopelist-am
+
+cscopelist-am: $(am__tagged_files)
+ list='$(am__tagged_files)'; \
+ case "$(srcdir)" in \
+ [\\/]* | ?:[\\/]*) sdir="$(srcdir)" ;; \
+ *) sdir=$(subdir)/$(srcdir) ;; \
+ esac; \
+ for i in $$list; do \
+ if test -f "$$i"; then \
+ echo "$(subdir)/$$i"; \
+ else \
+ echo "$$sdir/$$i"; \
+ fi; \
+ done >> $(top_builddir)/cscope.files
+
+distclean-tags:
+ -rm -f TAGS ID GTAGS GRTAGS GSYMS GPATH tags
+
+distdir: $(BUILT_SOURCES)
+ $(MAKE) $(AM_MAKEFLAGS) distdir-am
+
+distdir-am: $(DISTFILES)
+ @srcdirstrip=`echo "$(srcdir)" | sed 's/[].[^$$\\*]/\\\\&/g'`; \
+ topsrcdirstrip=`echo "$(top_srcdir)" | sed 's/[].[^$$\\*]/\\\\&/g'`; \
+ list='$(DISTFILES)'; \
+ dist_files=`for file in $$list; do echo $$file; done | \
+ sed -e "s|^$$srcdirstrip/||;t" \
+ -e "s|^$$topsrcdirstrip/|$(top_builddir)/|;t"`; \
+ case $$dist_files in \
+ */*) $(MKDIR_P) `echo "$$dist_files" | \
+ sed '/\//!d;s|^|$(distdir)/|;s,/[^/]*$$,,' | \
+ sort -u` ;; \
+ esac; \
+ for file in $$dist_files; do \
+ if test -f $$file || test -d $$file; then d=.; else d=$(srcdir); fi; \
+ if test -d $$d/$$file; then \
+ dir=`echo "/$$file" | sed -e 's,/[^/]*$$,,'`; \
+ if test -d "$(distdir)/$$file"; then \
+ find "$(distdir)/$$file" -type d ! -perm -700 -exec chmod u+rwx {} \;; \
+ fi; \
+ if test -d $(srcdir)/$$file && test $$d != $(srcdir); then \
+ cp -fpR $(srcdir)/$$file "$(distdir)$$dir" || exit 1; \
+ find "$(distdir)/$$file" -type d ! -perm -700 -exec chmod u+rwx {} \;; \
+ fi; \
+ cp -fpR $$d/$$file "$(distdir)$$dir" || exit 1; \
+ else \
+ test -f "$(distdir)/$$file" \
+ || cp -p $$d/$$file "$(distdir)/$$file" \
+ || exit 1; \
+ fi; \
+ done
+@BUILD_OPENSSL_FALSE@check-local:
+check-am: all-am
+ $(MAKE) $(AM_MAKEFLAGS) check-local
+check: check-am
+all-am: Makefile $(PROGRAMS) $(LTLIBRARIES) $(HEADERS)
+installdirs:
+ for dir in "$(DESTDIR)$(moduledir)" "$(DESTDIR)$(pkginc_libdir)"; do \
+ test -z "$$dir" || $(MKDIR_P) "$$dir"; \
+ done
+install: install-am
+install-exec: install-exec-am
+install-data: install-data-am
+uninstall: uninstall-am
+
+install-am: all-am
+ @$(MAKE) $(AM_MAKEFLAGS) install-exec-am install-data-am
+
+installcheck: installcheck-am
+install-strip:
+ if test -z '$(STRIP)'; then \
+ $(MAKE) $(AM_MAKEFLAGS) INSTALL_PROGRAM="$(INSTALL_STRIP_PROGRAM)" \
+ install_sh_PROGRAM="$(INSTALL_STRIP_PROGRAM)" INSTALL_STRIP_FLAG=-s \
+ install; \
+ else \
+ $(MAKE) $(AM_MAKEFLAGS) INSTALL_PROGRAM="$(INSTALL_STRIP_PROGRAM)" \
+ install_sh_PROGRAM="$(INSTALL_STRIP_PROGRAM)" INSTALL_STRIP_FLAG=-s \
+ "INSTALL_PROGRAM_ENV=STRIPPROG='$(STRIP)'" install; \
+ fi
+mostlyclean-generic:
+
+clean-generic:
+
+distclean-generic:
+ -test -z "$(CONFIG_CLEAN_FILES)" || rm -f $(CONFIG_CLEAN_FILES)
+ -test . = "$(srcdir)" || test -z "$(CONFIG_CLEAN_VPATH_FILES)" || rm -f $(CONFIG_CLEAN_VPATH_FILES)
+
+maintainer-clean-generic:
+ @echo "This command is intended for maintainers to use"
+ @echo "it deletes files that may require special tools to rebuild."
+clean: clean-am
+
+clean-am: clean-generic clean-libtool clean-moduleLTLIBRARIES \
+ clean-noinstLTLIBRARIES clean-noinstPROGRAMS mostlyclean-am
+
+distclean: distclean-am
+ -rm -f ./$(DEPDIR)/dovecot-openssl-common.Plo
+ -rm -f ./$(DEPDIR)/iostream-openssl-common.Plo
+ -rm -f ./$(DEPDIR)/iostream-openssl-context.Plo
+ -rm -f ./$(DEPDIR)/iostream-openssl.Plo
+ -rm -f ./$(DEPDIR)/iostream-ssl-context-cache.Plo
+ -rm -f ./$(DEPDIR)/iostream-ssl-test.Plo
+ -rm -f ./$(DEPDIR)/iostream-ssl.Plo
+ -rm -f ./$(DEPDIR)/istream-openssl.Plo
+ -rm -f ./$(DEPDIR)/ostream-openssl.Plo
+ -rm -f ./$(DEPDIR)/test-iostream-ssl.Po
+ -rm -f Makefile
+distclean-am: clean-am distclean-compile distclean-generic \
+ distclean-tags
+
+dvi: dvi-am
+
+dvi-am:
+
+html: html-am
+
+html-am:
+
+info: info-am
+
+info-am:
+
+install-data-am: install-moduleLTLIBRARIES install-pkginc_libHEADERS
+
+install-dvi: install-dvi-am
+
+install-dvi-am:
+
+install-exec-am:
+
+install-html: install-html-am
+
+install-html-am:
+
+install-info: install-info-am
+
+install-info-am:
+
+install-man:
+
+install-pdf: install-pdf-am
+
+install-pdf-am:
+
+install-ps: install-ps-am
+
+install-ps-am:
+
+installcheck-am:
+
+maintainer-clean: maintainer-clean-am
+ -rm -f ./$(DEPDIR)/dovecot-openssl-common.Plo
+ -rm -f ./$(DEPDIR)/iostream-openssl-common.Plo
+ -rm -f ./$(DEPDIR)/iostream-openssl-context.Plo
+ -rm -f ./$(DEPDIR)/iostream-openssl.Plo
+ -rm -f ./$(DEPDIR)/iostream-ssl-context-cache.Plo
+ -rm -f ./$(DEPDIR)/iostream-ssl-test.Plo
+ -rm -f ./$(DEPDIR)/iostream-ssl.Plo
+ -rm -f ./$(DEPDIR)/istream-openssl.Plo
+ -rm -f ./$(DEPDIR)/ostream-openssl.Plo
+ -rm -f ./$(DEPDIR)/test-iostream-ssl.Po
+ -rm -f Makefile
+maintainer-clean-am: distclean-am maintainer-clean-generic
+
+mostlyclean: mostlyclean-am
+
+mostlyclean-am: mostlyclean-compile mostlyclean-generic \
+ mostlyclean-libtool
+
+pdf: pdf-am
+
+pdf-am:
+
+ps: ps-am
+
+ps-am:
+
+uninstall-am: uninstall-moduleLTLIBRARIES uninstall-pkginc_libHEADERS
+
+.MAKE: check-am install-am install-strip
+
+.PHONY: CTAGS GTAGS TAGS all all-am am--depfiles check check-am \
+ check-local clean clean-generic clean-libtool \
+ clean-moduleLTLIBRARIES clean-noinstLTLIBRARIES \
+ clean-noinstPROGRAMS cscopelist-am ctags ctags-am distclean \
+ distclean-compile distclean-generic distclean-libtool \
+ distclean-tags distdir dvi dvi-am html html-am info info-am \
+ install install-am install-data install-data-am install-dvi \
+ install-dvi-am install-exec install-exec-am install-html \
+ install-html-am install-info install-info-am install-man \
+ install-moduleLTLIBRARIES install-pdf install-pdf-am \
+ install-pkginc_libHEADERS install-ps install-ps-am \
+ install-strip installcheck installcheck-am installdirs \
+ maintainer-clean maintainer-clean-generic mostlyclean \
+ mostlyclean-compile mostlyclean-generic mostlyclean-libtool \
+ pdf pdf-am ps ps-am tags tags-am uninstall uninstall-am \
+ uninstall-moduleLTLIBRARIES uninstall-pkginc_libHEADERS
+
+.PRECIOUS: Makefile
+
+
+@BUILD_OPENSSL_TRUE@check-local:
+@BUILD_OPENSSL_TRUE@ for bin in $(test_programs); do \
+@BUILD_OPENSSL_TRUE@ if ! $(RUN_TEST) ./$$bin; then exit 1; fi; \
+@BUILD_OPENSSL_TRUE@ done
+
+# Tell versions [3.59,3.63) of GNU make to not export all variables.
+# Otherwise a system limit (for SysV at least) may be exceeded.
+.NOEXPORT:
diff --git a/src/lib-ssl-iostream/dovecot-openssl-common.c b/src/lib-ssl-iostream/dovecot-openssl-common.c
new file mode 100644
index 0000000..76f98bc
--- /dev/null
+++ b/src/lib-ssl-iostream/dovecot-openssl-common.c
@@ -0,0 +1,132 @@
+/* Copyright (c) 2016-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "randgen.h"
+#include "dovecot-openssl-common.h"
+
+#include <openssl/ssl.h>
+#include <openssl/engine.h>
+#include <openssl/rand.h>
+
+static int openssl_init_refcount = 0;
+static ENGINE *dovecot_openssl_engine;
+
+#ifdef HAVE_SSL_NEW_MEM_FUNCS
+static void *dovecot_openssl_malloc(size_t size, const char *u0 ATTR_UNUSED, int u1 ATTR_UNUSED)
+#else
+static void *dovecot_openssl_malloc(size_t size)
+#endif
+{
+ /* this may be performance critical, so don't use
+ i_malloc() or calloc() */
+ void *mem = malloc(size);
+ if (mem == NULL) {
+ i_fatal_status(FATAL_OUTOFMEM,
+ "OpenSSL: malloc(%zu): Out of memory", size);
+ }
+ return mem;
+}
+
+#ifdef HAVE_SSL_NEW_MEM_FUNCS
+static void *dovecot_openssl_realloc(void *ptr, size_t size, const char *u0 ATTR_UNUSED, int u1 ATTR_UNUSED)
+#else
+static void *dovecot_openssl_realloc(void *ptr, size_t size)
+#endif
+{
+ void *mem = realloc(ptr, size);
+ if (mem == NULL) {
+ i_fatal_status(FATAL_OUTOFMEM,
+ "OpenSSL: realloc(%zu): Out of memory", size);
+ }
+ return mem;
+}
+
+#ifdef HAVE_SSL_NEW_MEM_FUNCS
+static void dovecot_openssl_free(void *ptr, const char *u0 ATTR_UNUSED, int u1 ATTR_UNUSED)
+#else
+static void dovecot_openssl_free(void *ptr)
+#endif
+{
+ free(ptr);
+}
+
+void dovecot_openssl_common_global_ref(void)
+{
+ if (openssl_init_refcount++ > 0)
+ return;
+
+ /* use our own memory allocation functions that will die instead of
+ returning NULL. this avoids random failures on out-of-memory
+ conditions. */
+ if (CRYPTO_set_mem_functions(dovecot_openssl_malloc,
+ dovecot_openssl_realloc, dovecot_openssl_free) == 0) {
+ /*i_warning("CRYPTO_set_mem_functions() was called too late");*/
+ }
+
+ SSL_library_init();
+ SSL_load_error_strings();
+ OpenSSL_add_all_algorithms();
+}
+
+bool dovecot_openssl_common_global_unref(void)
+{
+ i_assert(openssl_init_refcount > 0);
+
+ if (--openssl_init_refcount > 0)
+ return TRUE;
+
+ if (dovecot_openssl_engine != NULL) {
+ ENGINE_finish(dovecot_openssl_engine);
+ dovecot_openssl_engine = NULL;
+ }
+ /* OBJ_cleanup() is called automatically by EVP_cleanup() in
+ newer versions. Doesn't hurt to call it anyway. */
+ OBJ_cleanup();
+#ifdef HAVE_SSL_COMP_FREE_COMPRESSION_METHODS
+ SSL_COMP_free_compression_methods();
+#endif
+ ENGINE_cleanup();
+ EVP_cleanup();
+ CRYPTO_cleanup_all_ex_data();
+#ifdef HAVE_OPENSSL_AUTO_THREAD_DEINIT
+ /* no cleanup needed */
+#elif defined(HAVE_OPENSSL_ERR_REMOVE_THREAD_STATE)
+ /* This was marked as deprecated in v1.1. */
+ ERR_remove_thread_state(NULL);
+#else
+ /* This was deprecated by ERR_remove_thread_state(NULL) in v1.0.0. */
+ ERR_remove_state(0);
+#endif
+ ERR_free_strings();
+#ifdef HAVE_OPENSSL_CLEANUP
+ OPENSSL_cleanup();
+#endif
+ return FALSE;
+}
+
+int dovecot_openssl_common_global_set_engine(const char *engine,
+ const char **error_r)
+{
+ if (dovecot_openssl_engine != NULL)
+ return 1;
+
+ ENGINE_load_builtin_engines();
+ dovecot_openssl_engine = ENGINE_by_id(engine);
+ if (dovecot_openssl_engine == NULL) {
+ *error_r = t_strdup_printf("Unknown engine '%s'", engine);
+ return 0;
+ }
+ if (ENGINE_init(dovecot_openssl_engine) == 0) {
+ *error_r = t_strdup_printf("ENGINE_init(%s) failed", engine);
+ ENGINE_free(dovecot_openssl_engine);
+ dovecot_openssl_engine = NULL;
+ return -1;
+ }
+ if (ENGINE_set_default(dovecot_openssl_engine, ENGINE_METHOD_ALL) == 0) {
+ *error_r = t_strdup_printf("ENGINE_set_default(%s) failed", engine);
+ ENGINE_free(dovecot_openssl_engine);
+ dovecot_openssl_engine = NULL;
+ return -1;
+ }
+ return 1;
+}
diff --git a/src/lib-ssl-iostream/dovecot-openssl-common.h b/src/lib-ssl-iostream/dovecot-openssl-common.h
new file mode 100644
index 0000000..31854d3
--- /dev/null
+++ b/src/lib-ssl-iostream/dovecot-openssl-common.h
@@ -0,0 +1,16 @@
+#ifndef DOVECOT_OPENSSL_COMMON_H
+#define DOVECOT_OPENSSL_COMMON_H
+
+/* Initialize OpenSSL if this is the first instance.
+ Increase initialization reference count. */
+void dovecot_openssl_common_global_ref(void);
+/* Deinitialize OpenSSL if this is the last instance. Returns TRUE if there
+ are more instances left. */
+bool dovecot_openssl_common_global_unref(void);
+
+/* Set OpenSSL engine if it's not already set. Returns 1 on success, 0 if engine
+ is unknown, -1 on other error. error_r is set on 0/-1. */
+int dovecot_openssl_common_global_set_engine(const char *engine,
+ const char **error_r);
+
+#endif
diff --git a/src/lib-ssl-iostream/iostream-openssl-common.c b/src/lib-ssl-iostream/iostream-openssl-common.c
new file mode 100644
index 0000000..04dc5ea
--- /dev/null
+++ b/src/lib-ssl-iostream/iostream-openssl-common.c
@@ -0,0 +1,343 @@
+/* Copyright (c) 2009-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "net.h"
+#include "str.h"
+#include "iostream-openssl.h"
+
+#include <openssl/x509v3.h>
+#include <openssl/err.h>
+#include <arpa/inet.h>
+
+/* openssl_min_protocol_to_options() scans this array for name and returns
+ version and opt. opt is used with SSL_set_options() and version is used with
+ SSL_set_min_proto_version(). Using either method should enable the same
+ SSL protocol versions. */
+static const struct {
+ const char *name;
+ int version;
+ long opt;
+} protocol_versions[] = {
+#ifdef TLS_ANY_VERSION
+ { "ANY", TLS_ANY_VERSION, 0 },
+#else
+ { "ANY", SSL3_VERSION, 0 },
+#endif
+ { SSL_TXT_SSLV3, SSL3_VERSION, 0 },
+ { SSL_TXT_TLSV1, TLS1_VERSION, SSL_OP_NO_SSLv3 },
+ { SSL_TXT_TLSV1_1, TLS1_1_VERSION, SSL_OP_NO_SSLv3 | SSL_OP_NO_TLSv1 },
+ { SSL_TXT_TLSV1_2, TLS1_2_VERSION,
+ SSL_OP_NO_SSLv3 | SSL_OP_NO_TLSv1 | SSL_OP_NO_TLSv1_1 },
+#if defined(TLS1_3_VERSION)
+ { "TLSv1.3", TLS1_3_VERSION,
+ SSL_OP_NO_SSLv3 | SSL_OP_NO_TLSv1 | SSL_OP_NO_TLSv1_1 |
+ SSL_OP_NO_TLSv1_2 },
+#endif
+ /* Use latest protocol version. If this is used on some
+ ancient system which does not support ssl_min_protocol,
+ ensure only TLSv1.2 is supported. */
+#ifdef TLS_MAX_VERSION
+ { "LATEST", TLS_MAX_VERSION,
+#else
+ { "LATEST", 0,
+#endif
+ SSL_OP_NO_SSLv3 | SSL_OP_NO_TLSv1 | SSL_OP_NO_TLSv1_1 },
+};
+int openssl_min_protocol_to_options(const char *min_protocol, long *opt_r,
+ int *version_r)
+{
+ unsigned i = 0;
+ for (; i < N_ELEMENTS(protocol_versions); i++) {
+ if (strcasecmp(protocol_versions[i].name, min_protocol) == 0)
+ break;
+ }
+ if (i >= N_ELEMENTS(protocol_versions))
+ return -1;
+
+ if (opt_r != NULL)
+ *opt_r = protocol_versions[i].opt;
+ if (version_r != NULL)
+ *version_r = protocol_versions[i].version;
+ return 0;
+}
+
+#if !defined(HAVE_X509_CHECK_HOST) || !defined(HAVE_X509_CHECK_IP_ASC)
+static const char *asn1_string_to_c(ASN1_STRING *asn_str)
+{
+ const char *cstr;
+ unsigned int len;
+
+ len = ASN1_STRING_length(asn_str);
+ cstr = t_strndup(ASN1_STRING_get0_data(asn_str), len);
+ if (strlen(cstr) != len) {
+ /* NULs in the name - could be some MITM attack.
+ never allow. */
+ return "";
+ }
+ return cstr;
+}
+
+static const char *get_general_dns_name(const GENERAL_NAME *name)
+{
+ if (ASN1_STRING_type(name->d.ia5) != V_ASN1_IA5STRING)
+ return "";
+
+ return asn1_string_to_c(name->d.ia5);
+}
+
+static int get_general_ip_addr(const GENERAL_NAME *name, struct ip_addr *ip_r)
+{
+ if (ASN1_STRING_type(name->d.ip) != V_ASN1_OCTET_STRING)
+ return 0;
+ const unsigned char *data = ASN1_STRING_get0_data(name->d.ip);
+
+ if (name->d.ip->length == sizeof(ip_r->u.ip4.s_addr)) {
+ ip_r->family = AF_INET;
+ memcpy(&ip_r->u.ip4.s_addr, data, sizeof(ip_r->u.ip4.s_addr));
+ } else if (name->d.ip->length == sizeof(ip_r->u.ip6.s6_addr)) {
+ ip_r->family = AF_INET6;
+ memcpy(ip_r->u.ip6.s6_addr, data, sizeof(ip_r->u.ip6.s6_addr));
+ } else
+ return -1;
+ return 0;
+}
+
+static const char *get_cname(X509 *cert)
+{
+ X509_NAME *name;
+ X509_NAME_ENTRY *entry;
+ ASN1_STRING *str;
+ int cn_idx;
+
+ name = X509_get_subject_name(cert);
+ if (name == NULL)
+ return "";
+ cn_idx = X509_NAME_get_index_by_NID(name, NID_commonName, -1);
+ if (cn_idx == -1)
+ return "";
+ entry = X509_NAME_get_entry(name, cn_idx);
+ i_assert(entry != NULL);
+ str = X509_NAME_ENTRY_get_data(entry);
+ i_assert(str != NULL);
+ return asn1_string_to_c(str);
+}
+
+static bool openssl_hostname_equals(const char *ssl_name, const char *host)
+{
+ const char *p;
+
+ if (strcasecmp(ssl_name, host) == 0)
+ return TRUE;
+
+ /* check for *.example.com wildcard */
+ if (ssl_name[0] != '*' || ssl_name[1] != '.')
+ return FALSE;
+ p = strchr(host, '.');
+ return p != NULL && strcasecmp(ssl_name+2, p+1) == 0;
+}
+#endif
+
+bool openssl_cert_match_name(SSL *ssl, const char *verify_name,
+ const char **reason_r)
+{
+ X509 *cert;
+ bool ret;
+
+ *reason_r = NULL;
+
+ cert = SSL_get_peer_certificate(ssl);
+ i_assert(cert != NULL);
+
+#if defined(HAVE_X509_CHECK_HOST) && defined(HAVE_X509_CHECK_IP_ASC)
+ char *peername;
+ int check_res;
+
+ /* First check DNS name agains CommonName or SubjectAltNames.
+ If failed, check IP addresses. */
+ if ((check_res = X509_check_host(cert, verify_name, strlen(verify_name),
+ X509_CHECK_FLAG_NO_PARTIAL_WILDCARDS,
+ &peername)) == 1) {
+ *reason_r = t_strdup_printf("Matched to %s", peername);
+ free(peername);
+ ret = TRUE;
+ } else if (check_res == 0 &&
+ (check_res = X509_check_ip_asc(cert, verify_name, 0)) == 1) {
+ *reason_r = t_strdup_printf("Matched to IP address %s", verify_name);
+ ret = TRUE;
+ } else if (check_res == 0) {
+ *reason_r = "did not match to any IP or DNS fields";
+ ret = FALSE;
+ } else {
+ *reason_r = "Malformed input";
+ ret = FALSE;
+ }
+#else
+ STACK_OF(GENERAL_NAME) *gnames;
+ const GENERAL_NAME *gn;
+ struct ip_addr ip;
+ const char *dnsname;
+ bool dns_names = FALSE;
+ unsigned int i, count;
+
+ /* verify against SubjectAltNames */
+ gnames = X509_get_ext_d2i(cert, NID_subject_alt_name, NULL, NULL);
+ count = gnames == NULL ? 0 : sk_GENERAL_NAME_num(gnames);
+
+ i_zero(&ip);
+ /* try to convert verify_name to IP */
+ if (inet_pton(AF_INET6, verify_name, &ip.u.ip6) == 1)
+ ip.family = AF_INET6;
+ else if (inet_pton(AF_INET, verify_name, &ip.u.ip4) == 1)
+ ip.family = AF_INET;
+ else
+ i_zero(&ip);
+
+ for (i = 0; i < count; i++) {
+ gn = sk_GENERAL_NAME_value(gnames, i);
+
+ if (gn->type == GEN_DNS) {
+ dns_names = TRUE;
+ dnsname = get_general_dns_name(gn);
+ if (openssl_hostname_equals(dnsname, verify_name)) {
+ *reason_r = t_strdup_printf(
+ "Matches DNS name in SubjectAltNames: %s", dnsname);
+ break;
+ }
+ } else if (gn->type == GEN_IPADD) {
+ struct ip_addr ip_2;
+ i_zero(&ip_2);
+ dns_names = TRUE;
+ if (get_general_ip_addr(gn, &ip_2) == 0 &&
+ net_ip_compare(&ip, &ip_2)) {
+ *reason_r = t_strdup_printf(
+ "Matches IP in SubjectAltNames: %s", net_ip2addr(&ip_2));
+ break;
+ }
+ }
+ }
+ sk_GENERAL_NAME_pop_free(gnames, GENERAL_NAME_free);
+
+ /* verify against CommonName only when there wasn't any DNS
+ SubjectAltNames */
+ if (dns_names) {
+ i_assert(*reason_r != NULL || i == count);
+ if (i == count) {
+ *reason_r = t_strdup_printf(
+ "No match to %u SubjectAltNames",
+ count);
+ ret = FALSE;
+ } else {
+ ret = TRUE;
+ }
+ } else {
+ const char *cname = get_cname(cert);
+
+ if (openssl_hostname_equals(cname, verify_name)) {
+ ret = TRUE;
+ *reason_r = t_strdup_printf(
+ "Matches to CommonName: %s", cname);
+ } else {
+ *reason_r = t_strdup_printf(
+ "No match to CommonName=%s or %u SubjectAltNames",
+ cname, count);
+ ret = FALSE;
+ }
+ }
+#endif
+ X509_free(cert);
+ return ret;
+}
+
+static const char *ssl_err2str(unsigned long err, const char *data, int flags)
+{
+ const char *ret;
+ char *buf;
+ size_t err_size = 256;
+
+ buf = t_malloc0(err_size);
+ ERR_error_string_n(err, buf, err_size-1);
+ ret = buf;
+
+ if ((flags & ERR_TXT_STRING) != 0)
+ ret = t_strdup_printf("%s: %s", buf, data);
+ return ret;
+}
+
+const char *openssl_iostream_error(void)
+{
+ string_t *errstr = NULL;
+ unsigned long err;
+ const char *data, *final_error;
+ int flags;
+
+ while ((err = ERR_get_error_line_data(NULL, NULL, &data, &flags)) != 0) {
+ if (ERR_GET_REASON(err) == ERR_R_MALLOC_FAILURE)
+ i_fatal_status(FATAL_OUTOFMEM, "OpenSSL malloc() failed");
+ if (ERR_peek_error() == 0)
+ break;
+ if (errstr == NULL)
+ errstr = t_str_new(128);
+ else
+ str_append(errstr, ", ");
+ str_append(errstr, ssl_err2str(err, data, flags));
+ }
+ if (err == 0) {
+ if (errno != 0)
+ final_error = strerror(errno);
+ else
+ final_error = "Unknown error";
+ } else {
+ final_error = ssl_err2str(err, data, flags);
+ }
+ if (errstr == NULL)
+ return final_error;
+ else {
+ str_printfa(errstr, ", %s", final_error);
+ return str_c(errstr);
+ }
+}
+
+const char *openssl_iostream_key_load_error(void)
+{
+ unsigned long err = ERR_peek_error();
+
+ if (ERR_GET_LIB(err) == ERR_LIB_X509 &&
+ ERR_GET_REASON(err) == X509_R_KEY_VALUES_MISMATCH)
+ return "Key is for a different cert than ssl_cert";
+ else
+ return openssl_iostream_error();
+}
+
+static bool is_pem_key(const char *cert)
+{
+ return strstr(cert, "PRIVATE KEY---") != NULL;
+}
+
+const char *
+openssl_iostream_use_certificate_error(const char *cert, const char *set_name)
+{
+ unsigned long err;
+
+ if (cert[0] == '\0')
+ return "The certificate is empty";
+
+ err = ERR_peek_error();
+ if (ERR_GET_LIB(err) != ERR_LIB_PEM ||
+ ERR_GET_REASON(err) != PEM_R_NO_START_LINE)
+ return openssl_iostream_error();
+ else if (is_pem_key(cert)) {
+ return "The file contains a private key "
+ "(you've mixed ssl_cert and ssl_key settings)";
+ } else if (set_name != NULL && strchr(cert, '\n') == NULL) {
+ return t_strdup_printf("There is no valid PEM certificate. "
+ "(You probably forgot '<' from %s=<%s)", set_name, cert);
+ } else {
+ return "There is no valid PEM certificate.";
+ }
+}
+
+void openssl_iostream_clear_errors(void)
+{
+ while (ERR_get_error() != 0)
+ ;
+}
diff --git a/src/lib-ssl-iostream/iostream-openssl-context.c b/src/lib-ssl-iostream/iostream-openssl-context.c
new file mode 100644
index 0000000..fe9b059
--- /dev/null
+++ b/src/lib-ssl-iostream/iostream-openssl-context.c
@@ -0,0 +1,755 @@
+/* Copyright (c) 2009-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "safe-memset.h"
+#include "iostream-openssl.h"
+#include "dovecot-openssl-common.h"
+
+#include <openssl/crypto.h>
+#include <openssl/rsa.h>
+#include <openssl/dh.h>
+#include <openssl/bn.h>
+#include <openssl/x509.h>
+#include <openssl/pem.h>
+#include <openssl/ssl.h>
+#include <openssl/err.h>
+
+#if !defined(OPENSSL_NO_ECDH) && OPENSSL_VERSION_NUMBER >= 0x10000000L
+# define HAVE_ECDH
+#endif
+
+struct ssl_iostream_password_context {
+ const char *password;
+ const char *error;
+};
+
+static bool ssl_global_initialized = FALSE;
+int dovecot_ssl_extdata_index;
+
+static RSA *ssl_gen_rsa_key(SSL *ssl ATTR_UNUSED,
+ int is_export ATTR_UNUSED, int keylength)
+{
+#ifdef HAVE_RSA_GENERATE_KEY_EX
+ BIGNUM *bn = BN_new();
+ RSA *rsa = RSA_new();
+
+ if (bn != NULL && BN_set_word(bn, RSA_F4) != 0 &&
+ RSA_generate_key_ex(rsa, keylength, bn, NULL) != 0) {
+ BN_free(bn);
+ return rsa;
+ }
+
+ if (bn != NULL)
+ BN_free(bn);
+ if (rsa != NULL)
+ RSA_free(rsa);
+ return NULL;
+#else
+ return RSA_generate_key(keylength, RSA_F4, NULL, NULL);
+#endif
+}
+
+static DH *ssl_tmp_dh_callback(SSL *ssl ATTR_UNUSED,
+ int is_export ATTR_UNUSED, int keylength ATTR_UNUSED)
+{
+ i_error("Diffie-Hellman key exchange requested, "
+ "but no DH parameters provided. Set ssl_dh=</path/to/dh.pem");
+ return NULL;
+}
+
+static int
+pem_password_callback(char *buf, int size, int rwflag ATTR_UNUSED,
+ void *userdata)
+{
+ struct ssl_iostream_password_context *ctx = userdata;
+
+ if (ctx->password == NULL) {
+ ctx->error = "SSL private key file is password protected, "
+ "but password isn't given";
+ return 0;
+ }
+
+ if (i_strocpy(buf, ctx->password, size) < 0) {
+ ctx->error = "SSL private key password is too long";
+ return 0;
+ }
+ return strlen(buf);
+}
+
+int openssl_iostream_load_key(const struct ssl_iostream_cert *set,
+ const char *set_name,
+ EVP_PKEY **pkey_r, const char **error_r)
+{
+ struct ssl_iostream_password_context ctx;
+ EVP_PKEY *pkey;
+ BIO *bio;
+ char *key;
+
+ key = t_strdup_noconst(set->key);
+ bio = BIO_new_mem_buf(key, strlen(key));
+ if (bio == NULL) {
+ *error_r = t_strdup_printf("BIO_new_mem_buf() failed: %s",
+ openssl_iostream_error());
+ safe_memset(key, 0, strlen(key));
+ return -1;
+ }
+
+ ctx.password = set->key_password;
+ ctx.error = NULL;
+
+ pkey = PEM_read_bio_PrivateKey(bio, NULL, pem_password_callback, &ctx);
+ if (pkey == NULL && ctx.error == NULL) {
+ ctx.error = t_strdup_printf(
+ "Couldn't parse private SSL key (%s setting)%s: %s",
+ set_name,
+ ctx.password != NULL ?
+ " (maybe ssl_key_password is wrong?)" :
+ "",
+ openssl_iostream_error());
+ }
+ BIO_free(bio);
+
+ safe_memset(key, 0, strlen(key));
+ *pkey_r = pkey;
+ *error_r = ctx.error;
+ return pkey == NULL ? -1 : 0;
+}
+
+static
+int openssl_iostream_load_dh(const struct ssl_iostream_settings *set,
+ DH **dh_r, const char **error_r)
+{
+ DH *dh;
+ BIO *bio;
+ char *dhvalue;
+
+ dhvalue = t_strdup_noconst(set->dh);
+ bio = BIO_new_mem_buf(dhvalue, strlen(dhvalue));
+
+ if (bio == NULL) {
+ *error_r = t_strdup_printf("BIO_new_mem_buf() failed: %s",
+ openssl_iostream_error());
+ return -1;
+ }
+
+ dh = NULL;
+ dh = PEM_read_bio_DHparams(bio, &dh, NULL, NULL);
+
+ if (dh == NULL) {
+ *error_r = t_strdup_printf("Couldn't parse DH parameters: %s",
+ openssl_iostream_error());
+ }
+ BIO_free(bio);
+ *dh_r = dh;
+ return dh == NULL ? -1 : 0;
+}
+
+static int
+ssl_iostream_ctx_use_key(struct ssl_iostream_context *ctx, const char *set_name,
+ const struct ssl_iostream_cert *set,
+ const char **error_r)
+{
+ EVP_PKEY *pkey;
+ int ret = 0;
+
+ if (openssl_iostream_load_key(set, set_name, &pkey, error_r) < 0)
+ return -1;
+ if (SSL_CTX_use_PrivateKey(ctx->ssl_ctx, pkey) == 0) {
+ *error_r = t_strdup_printf(
+ "Can't load SSL private key (%s setting): %s",
+ set_name, openssl_iostream_key_load_error());
+ ret = -1;
+ }
+ EVP_PKEY_free(pkey);
+ return ret;
+}
+
+static int
+ssl_iostream_ctx_use_dh(struct ssl_iostream_context *ctx,
+ const struct ssl_iostream_settings *set,
+ const char **error_r)
+{
+ DH *dh;
+ int ret = 0;
+ if (*set->dh == '\0') {
+ return 0;
+ }
+ if (openssl_iostream_load_dh(set, &dh, error_r) < 0)
+ return -1;
+ if (SSL_CTX_set_tmp_dh(ctx->ssl_ctx, dh) == 0) {
+ *error_r = t_strdup_printf(
+ "Can't load DH parameters (ssl_dh setting): %s",
+ openssl_iostream_key_load_error());
+ ret = -1;
+ }
+ DH_free(dh);
+ return ret;
+}
+
+static int ssl_ctx_use_certificate_chain(SSL_CTX *ctx, const char *cert)
+{
+ /* mostly just copy&pasted from SSL_CTX_use_certificate_chain_file() */
+ BIO *in;
+ X509 *x;
+ int ret = 0;
+
+ in = BIO_new_mem_buf(t_strdup_noconst(cert), strlen(cert));
+ if (in == NULL)
+ i_fatal("BIO_new_mem_buf() failed");
+
+ x = PEM_read_bio_X509(in, NULL, NULL, NULL);
+ if (x == NULL)
+ goto end;
+
+ ret = SSL_CTX_use_certificate(ctx, x);
+ if (ERR_peek_error() != 0)
+ ret = 0;
+
+ if (ret != 0) {
+#ifdef HAVE_SSL_CTX_SET_CURRENT_CERT
+ SSL_CTX_select_current_cert(ctx, x);
+#endif
+ /* If we could set up our certificate, now proceed to
+ * the CA certificates.
+ */
+ X509 *ca;
+ int r;
+ unsigned long err;
+
+ while ((ca = PEM_read_bio_X509(in,NULL,NULL,NULL)) != NULL) {
+#ifdef HAVE_SSL_CTX_ADD0_CHAIN_CERT
+ r = SSL_CTX_add0_chain_cert(ctx, ca);
+#else
+ r = SSL_CTX_add_extra_chain_cert(ctx, ca);
+#endif
+ if (r == 0) {
+ X509_free(ca);
+ ret = 0;
+ goto end;
+ }
+ }
+ /* When the while loop ends, it's usually just EOF. */
+ err = ERR_peek_last_error();
+ if (ERR_GET_LIB(err) == ERR_LIB_PEM && ERR_GET_REASON(err) == PEM_R_NO_START_LINE)
+ ERR_clear_error();
+ else
+ ret = 0; /* some real error */
+ }
+
+end:
+ if (x != NULL) X509_free(x);
+ BIO_free(in);
+#ifdef HAVE_SSL_CTX_SET_CURRENT_CERT
+ SSL_CTX_set_current_cert(ctx, SSL_CERT_SET_FIRST);
+#endif
+ return ret;
+}
+
+static int load_ca(X509_STORE *store, const char *ca,
+ STACK_OF(X509_NAME) **xnames_r)
+{
+ /* mostly just copy&pasted from X509_load_cert_crl_file() */
+ STACK_OF(X509_INFO) *inf;
+ STACK_OF(X509_NAME) *xnames;
+ X509_INFO *itmp;
+ X509_NAME *xname;
+ BIO *bio;
+ int i;
+
+ bio = BIO_new_mem_buf(t_strdup_noconst(ca), strlen(ca));
+ if (bio == NULL)
+ i_fatal("BIO_new_mem_buf() failed");
+ inf = PEM_X509_INFO_read_bio(bio, NULL, NULL, NULL);
+ BIO_free(bio);
+
+ if (inf == NULL)
+ return -1;
+
+ xnames = sk_X509_NAME_new_null();
+ if (xnames == NULL)
+ i_fatal("sk_X509_NAME_new_null() failed");
+ for(i = 0; i < sk_X509_INFO_num(inf); i++) {
+ itmp = sk_X509_INFO_value(inf, i);
+ if(itmp->x509 != NULL) {
+ X509_STORE_add_cert(store, itmp->x509);
+ xname = X509_get_subject_name(itmp->x509);
+ if (xname != NULL)
+ xname = X509_NAME_dup(xname);
+ if (xname != NULL)
+ sk_X509_NAME_push(xnames, xname);
+ }
+ if(itmp->crl != NULL)
+ X509_STORE_add_crl(store, itmp->crl);
+ }
+ sk_X509_INFO_pop_free(inf, X509_INFO_free);
+ *xnames_r = xnames;
+ return 0;
+}
+
+static int
+load_ca_locations(struct ssl_iostream_context *ctx, const char *ca_file,
+ const char *ca_dir, const char **error_r)
+{
+ if (SSL_CTX_load_verify_locations(ctx->ssl_ctx, ca_file, ca_dir) != 0)
+ return 0;
+
+ if (ca_dir == NULL) {
+ *error_r = t_strdup_printf(
+ "Can't load CA certs from %s "
+ "(ssl_client_ca_file setting): %s",
+ ca_file, openssl_iostream_error());
+ } else if (ca_file == NULL) {
+ *error_r = t_strdup_printf(
+ "Can't load CA certs from directory %s "
+ "(ssl_client_ca_dir setting): %s",
+ ca_dir, openssl_iostream_error());
+ } else {
+ *error_r = t_strdup_printf(
+ "Can't load CA certs from file %s and directory %s "
+ "(ssl_client_ca_* settings): %s",
+ ca_file, ca_dir, openssl_iostream_error());
+ }
+ return -1;
+}
+
+static void
+ssl_iostream_ctx_verify_remote_cert(struct ssl_iostream_context *ctx,
+ STACK_OF(X509_NAME) *ca_names)
+{
+#if OPENSSL_VERSION_NUMBER >= 0x00907000L
+ if (!ctx->set.skip_crl_check) {
+ X509_STORE *store;
+
+ store = SSL_CTX_get_cert_store(ctx->ssl_ctx);
+ X509_STORE_set_flags(store, X509_V_FLAG_CRL_CHECK |
+ X509_V_FLAG_CRL_CHECK_ALL);
+ }
+#endif
+
+ SSL_CTX_set_client_CA_list(ctx->ssl_ctx, ca_names);
+}
+
+#ifdef HAVE_SSL_GET_SERVERNAME
+static int ssl_servername_callback(SSL *ssl, int *al ATTR_UNUSED,
+ void *context ATTR_UNUSED)
+{
+ struct ssl_iostream *ssl_io;
+ const char *host, *error;
+
+ ssl_io = SSL_get_ex_data(ssl, dovecot_ssl_extdata_index);
+ host = SSL_get_servername(ssl, TLSEXT_NAMETYPE_host_name);
+ if (SSL_get_servername_type(ssl) != -1) {
+ i_free(ssl_io->sni_host);
+ ssl_io->sni_host = i_strdup(host);
+ } else if (ssl_io->verbose) {
+ i_debug("SSL_get_servername() failed");
+ }
+
+ if (ssl_io->sni_callback != NULL) {
+ if (ssl_io->sni_callback(ssl_io->sni_host, &error,
+ ssl_io->sni_context) < 0) {
+ openssl_iostream_set_error(ssl_io, error);
+ return SSL_TLSEXT_ERR_ALERT_FATAL;
+ }
+ }
+ return SSL_TLSEXT_ERR_OK;
+}
+#endif
+
+static int
+ssl_iostream_context_load_ca(struct ssl_iostream_context *ctx,
+ const struct ssl_iostream_settings *set,
+ const char **error_r)
+{
+ X509_STORE *store;
+ STACK_OF(X509_NAME) *xnames = NULL;
+ const char *ca_file, *ca_dir;
+ bool have_ca = FALSE;
+
+ if (set->ca != NULL) {
+ store = SSL_CTX_get_cert_store(ctx->ssl_ctx);
+ if (load_ca(store, set->ca, &xnames) < 0) {
+ *error_r = t_strdup_printf("Couldn't parse ssl_ca: %s",
+ openssl_iostream_error());
+ return -1;
+ }
+ ssl_iostream_ctx_verify_remote_cert(ctx, xnames);
+ have_ca = TRUE;
+ }
+ ca_file = set->ca_file == NULL || *set->ca_file == '\0' ?
+ NULL : set->ca_file;
+ ca_dir = set->ca_dir == NULL || *set->ca_dir == '\0' ?
+ NULL : set->ca_dir;
+ if (ca_file != NULL || ca_dir != NULL) {
+ if (load_ca_locations(ctx, ca_file, ca_dir, error_r) < 0)
+ return -1;
+ have_ca = TRUE;
+ }
+ if (!have_ca && ctx->client_ctx) {
+ if (SSL_CTX_set_default_verify_paths(ctx->ssl_ctx) != 1) {
+ *error_r = t_strdup_printf(
+ "Can't load default CA locations: %s (ssl_client_ca_* settings missing)",
+ openssl_iostream_error());
+ return -1;
+ }
+ } else if (!have_ca) {
+ *error_r = "Can't verify remote client certs without CA (ssl_ca setting)";
+ return -1;
+ }
+ return 0;
+}
+
+static int
+ssl_iostream_context_set(struct ssl_iostream_context *ctx,
+ const struct ssl_iostream_settings *set,
+ const char **error_r)
+{
+ ssl_iostream_settings_init_from(ctx->pool, &ctx->set, set);
+ if (set->cipher_list != NULL &&
+ SSL_CTX_set_cipher_list(ctx->ssl_ctx, set->cipher_list) == 0) {
+ *error_r = t_strdup_printf(
+ "Can't set cipher list to '%s' (ssl_cipher_list setting): %s",
+ set->cipher_list, openssl_iostream_error());
+ return -1;
+ }
+#ifdef HAVE_SSL_CTX_SET1_CURVES_LIST
+ if (set->curve_list != NULL && strlen(set->curve_list) > 0 &&
+ SSL_CTX_set1_curves_list(ctx->ssl_ctx, set->curve_list) == 0) {
+ *error_r = t_strdup_printf(
+ "Can't set curve list to '%s' (ssl_curve_list setting)",
+ set->curve_list);
+ return -1;
+ }
+#endif
+#ifdef HAVE_SSL_CTX_SET_CIPHERSUITES
+ if (set->ciphersuites != NULL &&
+ SSL_CTX_set_ciphersuites(ctx->ssl_ctx, set->ciphersuites) == 0) {
+ *error_r = t_strdup_printf("Can't set ciphersuites to '%s': %s",
+ set->ciphersuites, openssl_iostream_error());
+ return -1;
+ }
+#endif
+ if (set->prefer_server_ciphers) {
+ SSL_CTX_set_options(ctx->ssl_ctx,
+ SSL_OP_CIPHER_SERVER_PREFERENCE);
+ }
+ if (ctx->set.min_protocol != NULL) {
+ long opts;
+ int min_protocol;
+ if (openssl_min_protocol_to_options(ctx->set.min_protocol,
+ &opts, &min_protocol) < 0) {
+ *error_r = t_strdup_printf(
+ "Unknown ssl_min_protocol setting '%s'",
+ set->min_protocol);
+ return -1;
+ }
+#ifdef HAVE_SSL_CTX_SET_MIN_PROTO_VERSION
+ SSL_CTX_set_min_proto_version(ctx->ssl_ctx, min_protocol);
+#else
+ SSL_CTX_set_options(ctx->ssl_ctx, opts);
+#endif
+ }
+
+ if (set->cert.cert != NULL &&
+ ssl_ctx_use_certificate_chain(ctx->ssl_ctx, set->cert.cert) == 0) {
+ *error_r = t_strdup_printf(
+ "Can't load SSL certificate (ssl_cert setting): %s",
+ openssl_iostream_use_certificate_error(set->cert.cert, NULL));
+ return -1;
+ }
+ if (set->cert.key != NULL) {
+ if (ssl_iostream_ctx_use_key(ctx, "ssl_key", &set->cert, error_r) < 0)
+ return -1;
+ }
+ if (set->alt_cert.cert != NULL &&
+ ssl_ctx_use_certificate_chain(ctx->ssl_ctx, set->alt_cert.cert) == 0) {
+ *error_r = t_strdup_printf(
+ "Can't load alternative SSL certificate "
+ "(ssl_alt_cert setting): %s",
+ openssl_iostream_use_certificate_error(set->alt_cert.cert, NULL));
+ return -1;
+ }
+ if (set->alt_cert.key != NULL) {
+ if (ssl_iostream_ctx_use_key(ctx, "ssl_alt_key", &set->alt_cert, error_r) < 0)
+ return -1;
+ }
+
+ if (set->dh != NULL) {
+ if (ssl_iostream_ctx_use_dh(ctx, set, error_r) < 0)
+ return -1;
+ }
+
+ /* set trusted CA certs */
+ if (set->verify_remote_cert) {
+ if (ssl_iostream_context_load_ca(ctx, set, error_r) < 0)
+ return -1;
+ }
+
+ if (set->cert_username_field != NULL) {
+ ctx->username_nid = OBJ_txt2nid(set->cert_username_field);
+ if (ctx->username_nid == NID_undef) {
+ *error_r = t_strdup_printf(
+ "Invalid cert_username_field: %s",
+ set->cert_username_field);
+ return -1;
+ }
+ }
+#ifdef HAVE_SSL_GET_SERVERNAME
+ if (!ctx->client_ctx) {
+ if (SSL_CTX_set_tlsext_servername_callback(ctx->ssl_ctx,
+ ssl_servername_callback) != 1) {
+ if (set->verbose)
+ i_debug("OpenSSL library doesn't support SNI");
+ }
+ }
+#endif
+ return 0;
+}
+
+#if defined(HAVE_ECDH) && !defined(SSL_CTX_set_ecdh_auto)
+static int
+ssl_proxy_ctx_get_pkey_ec_curve_name(const struct ssl_iostream_settings *set,
+ int *nid_r, const char **error_r)
+{
+ int nid = 0;
+ EVP_PKEY *pkey;
+ EC_KEY *eckey;
+ const EC_GROUP *ecgrp;
+
+ if (set->cert.key != NULL) {
+ if (openssl_iostream_load_key(&set->cert, "ssl_key", &pkey, error_r) < 0)
+ return -1;
+
+ if ((eckey = EVP_PKEY_get1_EC_KEY(pkey)) != NULL &&
+ (ecgrp = EC_KEY_get0_group(eckey)) != NULL)
+ nid = EC_GROUP_get_curve_name(ecgrp);
+ else {
+ /* clear errors added by the above calls */
+ openssl_iostream_clear_errors();
+ }
+ EVP_PKEY_free(pkey);
+ }
+ if (nid == 0 && set->alt_cert.key != NULL) {
+ if (openssl_iostream_load_key(&set->alt_cert, "ssl_alt_key", &pkey, error_r) < 0)
+ return -1;
+
+ if ((eckey = EVP_PKEY_get1_EC_KEY(pkey)) != NULL &&
+ (ecgrp = EC_KEY_get0_group(eckey)) != NULL)
+ nid = EC_GROUP_get_curve_name(ecgrp);
+ else {
+ /* clear errors added by the above calls */
+ openssl_iostream_clear_errors();
+ }
+ EVP_PKEY_free(pkey);
+ }
+
+ *nid_r = nid;
+ return 0;
+}
+#endif
+
+static int
+ssl_proxy_ctx_set_crypto_params(SSL_CTX *ssl_ctx,
+ const struct ssl_iostream_settings *set,
+ const char **error_r ATTR_UNUSED)
+{
+#if defined(HAVE_ECDH) && !defined(SSL_CTX_set_ecdh_auto)
+ EC_KEY *ecdh;
+ int nid;
+ const char *curve_name;
+#endif
+ if (SSL_CTX_need_tmp_RSA(ssl_ctx) != 0)
+ SSL_CTX_set_tmp_rsa_callback(ssl_ctx, ssl_gen_rsa_key);
+ if (set->dh == NULL || *set->dh == '\0')
+ SSL_CTX_set_tmp_dh_callback(ssl_ctx, ssl_tmp_dh_callback);
+#ifdef HAVE_ECDH
+ /* In the non-recommended situation where ECDH cipher suites are being
+ used instead of ECDHE, do not reuse the same ECDH key pair for
+ different sessions. This option improves forward secrecy. */
+ SSL_CTX_set_options(ssl_ctx, SSL_OP_SINGLE_ECDH_USE);
+#ifdef SSL_CTX_set_ecdh_auto
+ /* OpenSSL >= 1.0.2 automatically handles ECDH temporary key parameter
+ selection. The return value of this function changes is changed to
+ bool in OpenSSL 1.1 and is int in OpenSSL 1.0.2+ */
+ if ((long)(SSL_CTX_set_ecdh_auto(ssl_ctx, 1)) == 0) {
+ /* shouldn't happen */
+ *error_r = t_strdup_printf("SSL_CTX_set_ecdh_auto() failed: %s",
+ openssl_iostream_error());
+ return -1;
+ }
+#else
+ /* For OpenSSL < 1.0.2, ECDH temporary key parameter selection must be
+ performed manually. Attempt to select the same curve as that used
+ in the server's private EC key file. Otherwise fall back to the
+ NIST P-384 (secp384r1) curve to be compliant with RFC 6460 when
+ AES-256 TLS cipher suites are in use. This fall back option does
+ however make Dovecot non-compliant with RFC 6460 which requires
+ curve NIST P-256 (prime256v1) be used when AES-128 TLS cipher
+ suites are in use. At least the non-compliance is in the form of
+ providing too much security rather than too little. */
+ if (ssl_proxy_ctx_get_pkey_ec_curve_name(set, &nid, error_r) < 0)
+ return -1;
+ ecdh = EC_KEY_new_by_curve_name(nid);
+ if (ecdh == NULL) {
+ /* Fall back option */
+ nid = NID_secp384r1;
+ ecdh = EC_KEY_new_by_curve_name(nid);
+ }
+ if ((curve_name = OBJ_nid2sn(nid)) != NULL && set->verbose) {
+ i_debug("SSL: elliptic curve %s will be used for ECDH and"
+ " ECDHE key exchanges", curve_name);
+ }
+ if (ecdh != NULL) {
+ SSL_CTX_set_tmp_ecdh(ssl_ctx, ecdh);
+ EC_KEY_free(ecdh);
+ }
+#endif
+#endif
+#ifdef SSL_OP_SINGLE_DH_USE
+ /* Improves forward secrecy with DH parameters, especially if the
+ parameters used aren't strong primes. See OpenSSL manual. */
+ SSL_CTX_set_options(ssl_ctx, SSL_OP_SINGLE_DH_USE);
+#endif
+ return 0;
+}
+
+static int
+ssl_iostream_context_init_common(struct ssl_iostream_context *ctx,
+ const struct ssl_iostream_settings *set,
+ const char **error_r)
+{
+ unsigned long ssl_ops = SSL_OP_NO_SSLv2 |
+ (SSL_OP_ALL & ~SSL_OP_DONT_INSERT_EMPTY_FRAGMENTS);
+
+ ctx->pool = pool_alloconly_create("ssl iostream context", 4096);
+
+ /* enable all SSL workarounds, except empty fragments as it
+ makes SSL more vulnerable against attacks */
+#ifdef SSL_OP_NO_COMPRESSION
+ if (!set->compression)
+ ssl_ops |= SSL_OP_NO_COMPRESSION;
+#endif
+#ifdef SSL_OP_NO_TICKET
+ if (!set->tickets)
+ ssl_ops |= SSL_OP_NO_TICKET;
+#endif
+ SSL_CTX_set_options(ctx->ssl_ctx, ssl_ops);
+#ifdef SSL_MODE_RELEASE_BUFFERS
+ SSL_CTX_set_mode(ctx->ssl_ctx, SSL_MODE_RELEASE_BUFFERS);
+#endif
+#ifdef SSL_MODE_ENABLE_PARTIAL_WRITE
+ SSL_CTX_set_mode(ctx->ssl_ctx, SSL_MODE_ENABLE_PARTIAL_WRITE);
+#endif
+#ifdef SSL_MODE_ACCEPT_MOVING_WRITE_BUFFER
+ SSL_CTX_set_mode(ctx->ssl_ctx, SSL_MODE_ACCEPT_MOVING_WRITE_BUFFER);
+#endif
+ if (ssl_proxy_ctx_set_crypto_params(ctx->ssl_ctx, set, error_r) < 0)
+ return -1;
+
+ return ssl_iostream_context_set(ctx, set, error_r);
+}
+
+int openssl_iostream_context_init_client(const struct ssl_iostream_settings *set,
+ struct ssl_iostream_context **ctx_r,
+ const char **error_r)
+{
+ struct ssl_iostream_context *ctx;
+ SSL_CTX *ssl_ctx;
+
+ if ((ssl_ctx = SSL_CTX_new(SSLv23_client_method())) == NULL) {
+ *error_r = t_strdup_printf("SSL_CTX_new() failed: %s",
+ openssl_iostream_error());
+ return -1;
+ }
+ SSL_CTX_set_mode(ssl_ctx, SSL_MODE_ENABLE_PARTIAL_WRITE);
+
+ ctx = i_new(struct ssl_iostream_context, 1);
+ ctx->refcount = 1;
+ ctx->ssl_ctx = ssl_ctx;
+ ctx->client_ctx = TRUE;
+ if (ssl_iostream_context_init_common(ctx, set, error_r) < 0) {
+ ssl_iostream_context_unref(&ctx);
+ return -1;
+ }
+ *ctx_r = ctx;
+ return 0;
+}
+
+int openssl_iostream_context_init_server(const struct ssl_iostream_settings *set,
+ struct ssl_iostream_context **ctx_r,
+ const char **error_r)
+{
+ struct ssl_iostream_context *ctx;
+ SSL_CTX *ssl_ctx;
+
+ if ((ssl_ctx = SSL_CTX_new(SSLv23_server_method())) == NULL) {
+ *error_r = t_strdup_printf("SSL_CTX_new() failed: %s",
+ openssl_iostream_error());
+ return -1;
+ }
+
+ ctx = i_new(struct ssl_iostream_context, 1);
+ ctx->refcount = 1;
+ ctx->ssl_ctx = ssl_ctx;
+ if (ssl_iostream_context_init_common(ctx, set, error_r) < 0) {
+ ssl_iostream_context_unref(&ctx);
+ return -1;
+ }
+ *ctx_r = ctx;
+ return 0;
+}
+
+void openssl_iostream_context_ref(struct ssl_iostream_context *ctx)
+{
+ i_assert(ctx->refcount > 0);
+ ctx->refcount++;
+}
+
+void openssl_iostream_context_unref(struct ssl_iostream_context *ctx)
+{
+ i_assert(ctx->refcount > 0);
+ if (--ctx->refcount > 0)
+ return;
+
+ SSL_CTX_free(ctx->ssl_ctx);
+ pool_unref(&ctx->pool);
+ i_free(ctx);
+}
+
+void openssl_iostream_global_deinit(void)
+{
+ if (!ssl_global_initialized)
+ return;
+ dovecot_openssl_common_global_unref();
+}
+
+int openssl_iostream_global_init(const struct ssl_iostream_settings *set,
+ const char **error_r)
+{
+ static char dovecot[] = "dovecot";
+ const char *error;
+
+ if (ssl_global_initialized)
+ return 0;
+
+ ssl_global_initialized = TRUE;
+ dovecot_openssl_common_global_ref();
+
+ dovecot_ssl_extdata_index =
+ SSL_get_ex_new_index(0, dovecot, NULL, NULL, NULL);
+
+ if (set->crypto_device != NULL && *set->crypto_device != '\0') {
+ switch (dovecot_openssl_common_global_set_engine(set->crypto_device, &error)) {
+ case 0:
+ error = t_strdup_printf(
+ "Unknown ssl_crypto_device: %s",
+ set->crypto_device);
+ /* fall through */
+ case -1:
+ *error_r = error;
+ /* we'll deinit at exit in any case */
+ return -1;
+ }
+ }
+ return 0;
+}
diff --git a/src/lib-ssl-iostream/iostream-openssl.c b/src/lib-ssl-iostream/iostream-openssl.c
new file mode 100644
index 0000000..6920c53
--- /dev/null
+++ b/src/lib-ssl-iostream/iostream-openssl.c
@@ -0,0 +1,946 @@
+/* Copyright (c) 2009-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "istream-private.h"
+#include "ostream-private.h"
+#include "iostream-openssl.h"
+
+#include <openssl/rand.h>
+#include <openssl/err.h>
+
+static void openssl_iostream_free(struct ssl_iostream *ssl_io);
+
+void openssl_iostream_set_error(struct ssl_iostream *ssl_io, const char *str)
+{
+ char *new_str;
+
+ /* i_debug() may sometimes be overriden, making it write to this very
+ same SSL stream, in which case the provided str may be invalidated
+ before it is even used. Therefore, we duplicate it immediately. */
+ new_str = i_strdup(str);
+
+ if (ssl_io->verbose) {
+ /* This error should normally be logged by lib-ssl-iostream's
+ caller. But if verbose=TRUE, log it here as well to make
+ sure that the error is always logged. */
+ i_debug("%sSSL error: %s", ssl_io->log_prefix, new_str);
+ }
+ i_free(ssl_io->last_error);
+ ssl_io->last_error = new_str;
+}
+
+static void openssl_info_callback(const SSL *ssl, int where, int ret)
+{
+ struct ssl_iostream *ssl_io;
+
+ ssl_io = SSL_get_ex_data(ssl, dovecot_ssl_extdata_index);
+ if ((where & SSL_CB_ALERT) != 0) {
+ switch (ret & 0xff) {
+ case SSL_AD_CLOSE_NOTIFY:
+ i_debug("%sSSL alert: %s",
+ ssl_io->log_prefix,
+ SSL_alert_desc_string_long(ret));
+ break;
+ default:
+ i_debug("%sSSL alert: where=0x%x, ret=%d: %s %s",
+ ssl_io->log_prefix, where, ret,
+ SSL_alert_type_string_long(ret),
+ SSL_alert_desc_string_long(ret));
+ break;
+ }
+ } else if (ret == 0) {
+ i_debug("%sSSL failed: where=0x%x: %s",
+ ssl_io->log_prefix, where, SSL_state_string_long(ssl));
+ } else {
+ i_debug("%sSSL: where=0x%x, ret=%d: %s",
+ ssl_io->log_prefix, where, ret,
+ SSL_state_string_long(ssl));
+ }
+}
+
+static int
+openssl_iostream_use_certificate(struct ssl_iostream *ssl_io, const char *cert,
+ const char **error_r)
+{
+ BIO *in;
+ X509 *x;
+ int ret = 0;
+
+ in = BIO_new_mem_buf(t_strdup_noconst(cert), strlen(cert));
+ if (in == NULL) {
+ *error_r = t_strdup_printf("BIO_new_mem_buf() failed: %s",
+ openssl_iostream_error());
+ return -1;
+ }
+
+ x = PEM_read_bio_X509(in, NULL, NULL, NULL);
+ if (x != NULL) {
+ ret = SSL_use_certificate(ssl_io->ssl, x);
+ if (ERR_peek_error() != 0)
+ ret = 0;
+ X509_free(x);
+ }
+ BIO_free(in);
+
+ if (ret == 0) {
+ *error_r = t_strdup_printf("Can't load ssl_cert: %s",
+ openssl_iostream_use_certificate_error(cert, NULL));
+ return -1;
+ }
+ return 0;
+}
+
+static int
+openssl_iostream_use_key(struct ssl_iostream *ssl_io, const char *set_name,
+ const struct ssl_iostream_cert *set,
+ const char **error_r)
+{
+ EVP_PKEY *pkey;
+ int ret = 0;
+
+ if (openssl_iostream_load_key(set, set_name, &pkey, error_r) < 0)
+ return -1;
+ if (SSL_use_PrivateKey(ssl_io->ssl, pkey) != 1) {
+ *error_r = t_strdup_printf(
+ "Can't load SSL private key (%s setting): %s",
+ set_name, openssl_iostream_key_load_error());
+ ret = -1;
+ }
+ EVP_PKEY_free(pkey);
+ return ret;
+}
+
+static int
+openssl_iostream_verify_client_cert(int preverify_ok, X509_STORE_CTX *ctx)
+{
+ int ssl_extidx = SSL_get_ex_data_X509_STORE_CTX_idx();
+ SSL *ssl;
+ struct ssl_iostream *ssl_io;
+ char certname[1024];
+ X509_NAME *subject;
+
+ ssl = X509_STORE_CTX_get_ex_data(ctx, ssl_extidx);
+ ssl_io = SSL_get_ex_data(ssl, dovecot_ssl_extdata_index);
+ ssl_io->cert_received = TRUE;
+
+ subject = X509_get_subject_name(X509_STORE_CTX_get_current_cert(ctx));
+ if (subject == NULL ||
+ X509_NAME_oneline(subject, certname, sizeof(certname)) == NULL)
+ certname[0] = '\0';
+ else
+ certname[sizeof(certname)-1] = '\0'; /* just in case.. */
+ if (preverify_ok == 0) {
+ openssl_iostream_set_error(ssl_io, t_strdup_printf(
+ "Received invalid SSL certificate: %s: %s (check %s)",
+ X509_verify_cert_error_string(X509_STORE_CTX_get_error(ctx)), certname,
+ ssl_io->ctx->client_ctx ?
+ "ssl_client_ca_* settings?" :
+ "ssl_ca setting?"));
+ if (ssl_io->verbose_invalid_cert)
+ i_info("%s", ssl_io->last_error);
+ } else if (ssl_io->verbose) {
+ i_info("Received valid SSL certificate: %s", certname);
+ }
+ if (preverify_ok == 0) {
+ ssl_io->cert_broken = TRUE;
+ if (!ssl_io->allow_invalid_cert) {
+ ssl_io->handshake_failed = TRUE;
+ return 0;
+ }
+ }
+ return 1;
+}
+
+static int
+openssl_iostream_set(struct ssl_iostream *ssl_io,
+ const struct ssl_iostream_settings *set,
+ const char **error_r)
+{
+ const struct ssl_iostream_settings *ctx_set = &ssl_io->ctx->set;
+ int verify_flags;
+
+ if (set->verbose)
+ SSL_set_info_callback(ssl_io->ssl, openssl_info_callback);
+
+ if (set->cipher_list != NULL &&
+ strcmp(ctx_set->cipher_list, set->cipher_list) != 0) {
+ if (SSL_set_cipher_list(ssl_io->ssl, set->cipher_list) == 0) {
+ *error_r = t_strdup_printf(
+ "Can't set cipher list to '%s': %s",
+ set->cipher_list, openssl_iostream_error());
+ return -1;
+ }
+ }
+#ifdef HAVE_SSL_CTX_SET1_CURVES_LIST
+ if (set->curve_list != NULL && strlen(set->curve_list) > 0 &&
+ (ctx_set->curve_list == NULL || strcmp(ctx_set->curve_list, set->curve_list) != 0)) {
+ if (SSL_set1_curves_list(ssl_io->ssl, set->curve_list) == 0) {
+ *error_r = t_strdup_printf(
+ "Failed to set curve list to '%s'",
+ set->curve_list);
+ return -1;
+ }
+ }
+#endif
+#ifdef HAVE_SSL_CTX_SET_CIPHERSUITES
+ if (set->ciphersuites != NULL &&
+ strcmp(ctx_set->ciphersuites, set->ciphersuites) != 0) {
+ if (SSL_set_ciphersuites(ssl_io->ssl, set->ciphersuites) == 0) {
+ *error_r = t_strdup_printf(
+ "Can't set ciphersuites to '%s': %s",
+ set->ciphersuites, openssl_iostream_error());
+ return -1;
+ }
+ }
+#endif
+ if (set->prefer_server_ciphers)
+ SSL_set_options(ssl_io->ssl, SSL_OP_CIPHER_SERVER_PREFERENCE);
+ if (set->min_protocol != NULL) {
+#if defined(HAVE_SSL_CLEAR_OPTIONS)
+ SSL_clear_options(ssl_io->ssl, OPENSSL_ALL_PROTOCOL_OPTIONS);
+#endif
+ long opts;
+ int min_protocol;
+ if (openssl_min_protocol_to_options(set->min_protocol, &opts,
+ &min_protocol) < 0) {
+ *error_r = t_strdup_printf(
+ "Unknown ssl_min_protocol setting '%s'",
+ set->min_protocol);
+ return -1;
+ }
+#ifdef HAVE_SSL_CTX_SET_MIN_PROTO_VERSION
+ SSL_set_min_proto_version(ssl_io->ssl, min_protocol);
+#else
+ SSL_set_options(ssl_io->ssl, opts);
+#endif
+ }
+
+ if (set->cert.cert != NULL && strcmp(ctx_set->cert.cert, set->cert.cert) != 0) {
+ if (openssl_iostream_use_certificate(ssl_io, set->cert.cert, error_r) < 0)
+ return -1;
+ }
+ if (set->cert.key != NULL && strcmp(ctx_set->cert.key, set->cert.key) != 0) {
+ if (openssl_iostream_use_key(ssl_io, "ssl_key", &set->cert, error_r) < 0)
+ return -1;
+ }
+ if (set->alt_cert.cert != NULL && strcmp(ctx_set->alt_cert.cert, set->alt_cert.cert) != 0) {
+ if (openssl_iostream_use_certificate(ssl_io, set->alt_cert.cert, error_r) < 0)
+ return -1;
+ }
+ if (set->alt_cert.key != NULL && strcmp(ctx_set->alt_cert.key, set->alt_cert.key) != 0) {
+ if (openssl_iostream_use_key(ssl_io, "ssl_alt_key", &set->alt_cert, error_r) < 0)
+ return -1;
+ }
+ if (set->verify_remote_cert) {
+ if (ssl_io->ctx->client_ctx)
+ verify_flags = SSL_VERIFY_NONE;
+ else
+ verify_flags = SSL_VERIFY_PEER | SSL_VERIFY_CLIENT_ONCE;
+ SSL_set_verify(ssl_io->ssl, verify_flags,
+ openssl_iostream_verify_client_cert);
+ }
+
+ if (set->cert_username_field != NULL) {
+ ssl_io->username_nid = OBJ_txt2nid(set->cert_username_field);
+ if (ssl_io->username_nid == NID_undef) {
+ *error_r = t_strdup_printf(
+ "Invalid cert_username_field: %s",
+ set->cert_username_field);
+ return -1;
+ }
+ } else {
+ ssl_io->username_nid = ssl_io->ctx->username_nid;
+ }
+
+ ssl_io->verbose = set->verbose;
+ ssl_io->verbose_invalid_cert = set->verbose_invalid_cert || set->verbose;
+ ssl_io->allow_invalid_cert = set->allow_invalid_cert;
+ return 0;
+}
+
+static int
+openssl_iostream_create(struct ssl_iostream_context *ctx, const char *host,
+ const struct ssl_iostream_settings *set,
+ struct istream **input, struct ostream **output,
+ struct ssl_iostream **iostream_r,
+ const char **error_r)
+{
+ struct ssl_iostream *ssl_io;
+ SSL *ssl;
+ BIO *bio_int, *bio_ext;
+
+ /* Don't allow an existing io_add_istream() to be use on the input.
+ It would seem to work, but it would also cause hangs. */
+ i_assert(i_stream_get_root_io(*input)->real_stream->io == NULL);
+
+ ssl = SSL_new(ctx->ssl_ctx);
+ if (ssl == NULL) {
+ *error_r = t_strdup_printf("SSL_new() failed: %s",
+ openssl_iostream_error());
+ return -1;
+ }
+
+ /* BIO pairs use default buffer sizes (17 kB in OpenSSL 0.9.8e).
+ Each of the BIOs have one "write buffer". BIO_write() copies data
+ to them, while BIO_read() reads from the other BIO's write buffer
+ into the given buffer. The bio_int is used by OpenSSL and bio_ext
+ is used by this library. */
+ if (BIO_new_bio_pair(&bio_int, 0, &bio_ext, 0) != 1) {
+ *error_r = t_strdup_printf("BIO_new_bio_pair() failed: %s",
+ openssl_iostream_error());
+ SSL_free(ssl);
+ return -1;
+ }
+
+ ssl_io = i_new(struct ssl_iostream, 1);
+ ssl_io->refcount = 1;
+ ssl_io->ctx = ctx;
+ ssl_iostream_context_ref(ssl_io->ctx);
+ ssl_io->ssl = ssl;
+ ssl_io->bio_ext = bio_ext;
+ ssl_io->plain_input = *input;
+ ssl_io->plain_output = *output;
+ ssl_io->connected_host = i_strdup(host);
+ ssl_io->log_prefix = host == NULL ? i_strdup("") :
+ i_strdup_printf("%s: ", host);
+ /* bio_int will be freed by SSL_free() */
+ SSL_set_bio(ssl_io->ssl, bio_int, bio_int);
+ SSL_set_ex_data(ssl_io->ssl, dovecot_ssl_extdata_index, ssl_io);
+#ifdef HAVE_SSL_GET_SERVERNAME
+ SSL_set_tlsext_host_name(ssl_io->ssl, host);
+#endif
+
+ if (openssl_iostream_set(ssl_io, set, error_r) < 0) {
+ openssl_iostream_free(ssl_io);
+ return -1;
+ }
+
+ o_stream_uncork(ssl_io->plain_output);
+
+ *input = openssl_i_stream_create_ssl(ssl_io);
+ ssl_io->ssl_input = *input;
+
+ *output = openssl_o_stream_create_ssl(ssl_io);
+ i_stream_set_name(*input, t_strconcat("SSL ",
+ i_stream_get_name(ssl_io->plain_input), NULL));
+ o_stream_set_name(*output, t_strconcat("SSL ",
+ o_stream_get_name(ssl_io->plain_output), NULL));
+
+ if (ssl_io->plain_output->real_stream->error_handling_disabled)
+ o_stream_set_no_error_handling(*output, TRUE);
+
+ ssl_io->ssl_output = *output;
+ *iostream_r = ssl_io;
+ return 0;
+}
+
+static void openssl_iostream_free(struct ssl_iostream *ssl_io)
+{
+ ssl_iostream_context_unref(&ssl_io->ctx);
+ o_stream_unref(&ssl_io->plain_output);
+ i_stream_unref(&ssl_io->plain_input);
+ BIO_free(ssl_io->bio_ext);
+ SSL_free(ssl_io->ssl);
+ i_free(ssl_io->plain_stream_errstr);
+ i_free(ssl_io->last_error);
+ i_free(ssl_io->connected_host);
+ i_free(ssl_io->sni_host);
+ i_free(ssl_io->log_prefix);
+ i_free(ssl_io);
+}
+
+static void openssl_iostream_unref(struct ssl_iostream *ssl_io)
+{
+ i_assert(ssl_io->refcount > 0);
+ if (--ssl_io->refcount > 0)
+ return;
+
+ openssl_iostream_free(ssl_io);
+}
+
+void openssl_iostream_shutdown(struct ssl_iostream *ssl_io)
+{
+ if (ssl_io->destroyed)
+ return;
+
+ i_assert(ssl_io->ssl_input != NULL);
+ i_assert(ssl_io->ssl_output != NULL);
+
+ ssl_io->destroyed = TRUE;
+ if (ssl_io->handshaked && SSL_shutdown(ssl_io->ssl) != 1) {
+ /* if bidirectional shutdown fails we need to clear
+ the error queue */
+ openssl_iostream_clear_errors();
+ }
+ if (ssl_io->handshaked) {
+ (void)openssl_iostream_bio_sync(ssl_io,
+ OPENSSL_IOSTREAM_SYNC_TYPE_WRITE);
+ }
+ (void)o_stream_flush(ssl_io->plain_output);
+ /* close the plain i/o streams, because their fd may be closed soon,
+ but we may still keep this ssl-iostream referenced until later. */
+ i_stream_close(ssl_io->plain_input);
+ o_stream_close(ssl_io->plain_output);
+}
+
+static void openssl_iostream_destroy(struct ssl_iostream *ssl_io)
+{
+ openssl_iostream_shutdown(ssl_io);
+ ssl_iostream_unref(&ssl_io);
+}
+
+static int openssl_iostream_bio_output_real(struct ssl_iostream *ssl_io)
+{
+ size_t bytes, max_bytes = 0;
+ ssize_t sent;
+ unsigned char buffer[IO_BLOCK_SIZE];
+ int result = 0;
+ int ret;
+
+ o_stream_cork(ssl_io->plain_output);
+ while ((bytes = BIO_ctrl_pending(ssl_io->bio_ext)) > 0) {
+ /* bytes contains how many SSL encrypted bytes we should be
+ sending out */
+ max_bytes = o_stream_get_buffer_avail_size(ssl_io->plain_output);
+ if (bytes > max_bytes) {
+ if (max_bytes == 0) {
+ /* wait until output buffer clears */
+ break;
+ }
+ bytes = max_bytes;
+ }
+ if (bytes > sizeof(buffer))
+ bytes = sizeof(buffer);
+
+ /* BIO_read() is guaranteed to return all the bytes that
+ BIO_ctrl_pending() returned */
+ ret = BIO_read(ssl_io->bio_ext, buffer, bytes);
+ i_assert(ret == (int)bytes);
+
+ /* we limited number of read bytes to plain_output's
+ available size. this send() is guaranteed to either
+ fully succeed or completely fail due to some error. */
+ sent = o_stream_send(ssl_io->plain_output, buffer, bytes);
+ if (sent < 0) {
+ o_stream_uncork(ssl_io->plain_output);
+ return -1;
+ }
+ i_assert(sent == (ssize_t)bytes);
+ result = 1;
+ }
+
+ ret = o_stream_uncork_flush(ssl_io->plain_output);
+ if (ret < 0)
+ return -1;
+ if (ret == 0 || (bytes > 0 && max_bytes == 0))
+ o_stream_set_flush_pending(ssl_io->plain_output, TRUE);
+
+ return result;
+}
+
+static int openssl_iostream_bio_output(struct ssl_iostream *ssl_io)
+{
+ int ret;
+
+ ret = openssl_iostream_bio_output_real(ssl_io);
+ if (ret < 0) {
+ i_assert(ssl_io->plain_output->stream_errno != 0);
+ i_free(ssl_io->plain_stream_errstr);
+ ssl_io->plain_stream_errstr =
+ i_strdup(o_stream_get_error(ssl_io->plain_output));
+ ssl_io->plain_stream_errno =
+ ssl_io->plain_output->stream_errno;
+ ssl_io->closed = TRUE;
+ }
+ return ret;
+}
+
+static ssize_t
+openssl_iostream_read_more(struct ssl_iostream *ssl_io,
+ enum openssl_iostream_sync_type type, size_t wanted,
+ const unsigned char **data_r, size_t *size_r)
+{
+ *data_r = i_stream_get_data(ssl_io->plain_input, size_r);
+ if (*size_r > 0)
+ return 0;
+
+ if (type == OPENSSL_IOSTREAM_SYNC_TYPE_CONTINUE_READ) {
+ /* only the first i_stream_read() call attempts to read more
+ input. the following reads will just process the buffered
+ data. */
+ return 0;
+ }
+
+ if (i_stream_read_limited(ssl_io->plain_input, data_r, size_r,
+ wanted) < 0)
+ return -1;
+ return 0;
+}
+
+static int
+openssl_iostream_bio_input(struct ssl_iostream *ssl_io,
+ enum openssl_iostream_sync_type type)
+{
+ const unsigned char *data;
+ size_t bytes, size;
+ int ret;
+ bool bytes_read = FALSE;
+
+ while ((bytes = BIO_ctrl_get_write_guarantee(ssl_io->bio_ext)) > 0) {
+ /* bytes contains how many bytes we can write to bio_ext */
+ ret = openssl_iostream_read_more(ssl_io, type, bytes,
+ &data, &size);
+ if (ret == -1 && size == 0 && !bytes_read) {
+ if (ssl_io->plain_input->stream_errno != 0) {
+ i_free(ssl_io->plain_stream_errstr);
+ ssl_io->plain_stream_errstr =
+ i_strdup(i_stream_get_error(ssl_io->plain_input));
+ ssl_io->plain_stream_errno =
+ ssl_io->plain_input->stream_errno;
+ }
+ ssl_io->closed = TRUE;
+ return -1;
+ }
+ if (size == 0) {
+ /* wait for more input */
+ break;
+ }
+ if (size > bytes)
+ size = bytes;
+
+ ret = BIO_write(ssl_io->bio_ext, data, size);
+ i_assert(ret == (ssize_t)size);
+
+ i_stream_skip(ssl_io->plain_input, size);
+ bytes_read = TRUE;
+ }
+ if (bytes == 0 && !bytes_read && ssl_io->want_read) {
+ /* shouldn't happen */
+ i_error("SSL BIO buffer size too small");
+ i_free(ssl_io->plain_stream_errstr);
+ ssl_io->plain_stream_errstr =
+ i_strdup("SSL BIO buffer size too small");
+ ssl_io->plain_stream_errno = EINVAL;
+ ssl_io->closed = TRUE;
+ return -1;
+ }
+ if (bytes_read) {
+ if (ssl_io->ostream_flush_waiting_input) {
+ ssl_io->ostream_flush_waiting_input = FALSE;
+ o_stream_set_flush_pending(ssl_io->plain_output, TRUE);
+ }
+ }
+ if (bytes_read || i_stream_get_data_size(ssl_io->plain_input) > 0) {
+ if (i_stream_get_data_size(ssl_io->plain_input) > 0 ||
+ (type != OPENSSL_IOSTREAM_SYNC_TYPE_FIRST_READ &&
+ type != OPENSSL_IOSTREAM_SYNC_TYPE_CONTINUE_READ))
+ i_stream_set_input_pending(ssl_io->ssl_input, TRUE);
+ ssl_io->want_read = FALSE;
+ }
+ return (bytes_read ? 1 : 0);
+}
+
+int openssl_iostream_bio_sync(struct ssl_iostream *ssl_io,
+ enum openssl_iostream_sync_type type)
+{
+ int ret;
+
+ i_assert(type != OPENSSL_IOSTREAM_SYNC_TYPE_NONE);
+
+ ret = openssl_iostream_bio_output(ssl_io);
+ if (ret >= 0 && openssl_iostream_bio_input(ssl_io, type) > 0)
+ ret = 1;
+ return ret;
+}
+
+static void openssl_iostream_closed(struct ssl_iostream *ssl_io)
+{
+ if (ssl_io->plain_stream_errno != 0) {
+ i_assert(ssl_io->plain_stream_errstr != NULL);
+ openssl_iostream_set_error(ssl_io, ssl_io->plain_stream_errstr);
+ errno = ssl_io->plain_stream_errno;
+ } else {
+ openssl_iostream_set_error(ssl_io, "Connection closed");
+ errno = EPIPE;
+ }
+}
+
+int openssl_iostream_handle_error(struct ssl_iostream *ssl_io, int ret,
+ enum openssl_iostream_sync_type type,
+ const char *func_name)
+{
+ const char *errstr = NULL;
+ int err;
+
+ err = SSL_get_error(ssl_io->ssl, ret);
+ switch (err) {
+ case SSL_ERROR_WANT_WRITE:
+ if (type != OPENSSL_IOSTREAM_SYNC_TYPE_NONE &&
+ openssl_iostream_bio_sync(ssl_io, type) == 0) {
+ if (type != OPENSSL_IOSTREAM_SYNC_TYPE_WRITE)
+ i_panic("SSL ostream buffer size not unlimited");
+ return 0;
+ }
+ if (ssl_io->closed) {
+ openssl_iostream_closed(ssl_io);
+ return -1;
+ }
+ if (type == OPENSSL_IOSTREAM_SYNC_TYPE_NONE)
+ return 0;
+ return 1;
+ case SSL_ERROR_WANT_READ:
+ ssl_io->want_read = TRUE;
+ if (type != OPENSSL_IOSTREAM_SYNC_TYPE_NONE)
+ (void)openssl_iostream_bio_sync(ssl_io, type);
+ if (ssl_io->closed) {
+ openssl_iostream_closed(ssl_io);
+ return -1;
+ }
+ if (type == OPENSSL_IOSTREAM_SYNC_TYPE_NONE)
+ return 0;
+ return ssl_io->want_read ? 0 : 1;
+ case SSL_ERROR_SYSCALL:
+ /* eat up the error queue */
+ if (ERR_peek_error() != 0) {
+ errstr = openssl_iostream_error();
+ errno = EINVAL;
+ } else if (ret == 0) {
+ /* EOF. */
+ errno = EPIPE;
+ errstr = "Disconnected";
+ break;
+ } else if (errno != 0) {
+ errstr = strerror(errno);
+ } else {
+ /* Seen this at least with v1.1.0l SSL_accept() */
+ errstr = "OpenSSL BUG: errno=0";
+ errno = EINVAL;
+ }
+ errstr = t_strdup_printf("%s syscall failed: %s",
+ func_name, errstr);
+ break;
+ case SSL_ERROR_ZERO_RETURN:
+ /* clean connection closing */
+ errno = EPIPE;
+ if (ssl_io->handshaked)
+ i_free_and_null(ssl_io->last_error);
+ else if (ssl_io->last_error == NULL) {
+ errstr = "SSL connection closed during handshake";
+ break;
+ }
+ return -1;
+ case SSL_ERROR_SSL:
+ errstr = t_strdup_printf("%s failed: %s",
+ func_name, openssl_iostream_error());
+ errno = EINVAL;
+ break;
+ default:
+ errstr = t_strdup_printf("%s failed: unknown failure %d (%s)",
+ func_name, err,
+ openssl_iostream_error());
+ errno = EINVAL;
+ break;
+ }
+
+ openssl_iostream_set_error(ssl_io, errstr);
+ return -1;
+}
+
+static bool
+openssl_iostream_cert_match_name(struct ssl_iostream *ssl_io,
+ const char *verify_name, const char **reason_r)
+{
+ if (!ssl_iostream_has_valid_client_cert(ssl_io)) {
+ *reason_r = "Invalid certificate";
+ return FALSE;
+ }
+
+ return openssl_cert_match_name(ssl_io->ssl, verify_name, reason_r);
+}
+
+static int openssl_iostream_handshake(struct ssl_iostream *ssl_io)
+{
+ const char *reason, *error = NULL;
+ int ret;
+
+ i_assert(!ssl_io->handshaked);
+
+ /* we are being destroyed, so do not do any more handshaking */
+ if (ssl_io->destroyed)
+ return 0;
+
+ if (ssl_io->ctx->client_ctx) {
+ while ((ret = SSL_connect(ssl_io->ssl)) <= 0) {
+ ret = openssl_iostream_handle_error(ssl_io, ret,
+ OPENSSL_IOSTREAM_SYNC_TYPE_HANDSHAKE, "SSL_connect()");
+ if (ret <= 0)
+ return ret;
+ }
+ } else {
+ while ((ret = SSL_accept(ssl_io->ssl)) <= 0) {
+ ret = openssl_iostream_handle_error(ssl_io, ret,
+ OPENSSL_IOSTREAM_SYNC_TYPE_HANDSHAKE, "SSL_accept()");
+ if (ret <= 0)
+ return ret;
+ }
+ }
+ /* handshake finished */
+ (void)openssl_iostream_bio_sync(ssl_io, OPENSSL_IOSTREAM_SYNC_TYPE_HANDSHAKE);
+
+ if (ssl_io->handshake_callback != NULL) {
+ if (ssl_io->handshake_callback(&error, ssl_io->handshake_context) < 0) {
+ i_assert(error != NULL);
+ openssl_iostream_set_error(ssl_io, error);
+ ssl_io->handshake_failed = TRUE;
+ }
+ } else if (ssl_io->connected_host != NULL && !ssl_io->handshake_failed &&
+ !ssl_io->allow_invalid_cert) {
+ if (ssl_iostream_check_cert_validity(ssl_io, ssl_io->connected_host, &reason) < 0) {
+ openssl_iostream_set_error(ssl_io, reason);
+ ssl_io->handshake_failed = TRUE;
+ }
+ }
+ if (ssl_io->handshake_failed) {
+ i_stream_close(ssl_io->plain_input);
+ o_stream_close(ssl_io->plain_output);
+ errno = EINVAL;
+ return -1;
+ }
+ i_free_and_null(ssl_io->last_error);
+ ssl_io->handshaked = TRUE;
+
+ if (ssl_io->ssl_output != NULL)
+ (void)o_stream_flush(ssl_io->ssl_output);
+ return 1;
+}
+
+static void
+openssl_iostream_set_handshake_callback(struct ssl_iostream *ssl_io,
+ ssl_iostream_handshake_callback_t *callback,
+ void *context)
+{
+ ssl_io->handshake_callback = callback;
+ ssl_io->handshake_context = context;
+}
+
+static void
+openssl_iostream_set_sni_callback(struct ssl_iostream *ssl_io,
+ ssl_iostream_sni_callback_t *callback,
+ void *context)
+{
+ ssl_io->sni_callback = callback;
+ ssl_io->sni_context = context;
+}
+
+static void
+openssl_iostream_change_context(struct ssl_iostream *ssl_io,
+ struct ssl_iostream_context *ctx)
+{
+ if (ctx != ssl_io->ctx) {
+ SSL_set_SSL_CTX(ssl_io->ssl, ctx->ssl_ctx);
+ ssl_iostream_context_ref(ctx);
+ ssl_iostream_context_unref(&ssl_io->ctx);
+ ssl_io->ctx = ctx;
+ }
+}
+
+static void openssl_iostream_set_log_prefix(struct ssl_iostream *ssl_io,
+ const char *prefix)
+{
+ i_free(ssl_io->log_prefix);
+ ssl_io->log_prefix = i_strdup(prefix);
+}
+
+static bool openssl_iostream_is_handshaked(const struct ssl_iostream *ssl_io)
+{
+ return ssl_io->handshaked;
+}
+
+static bool
+openssl_iostream_has_handshake_failed(const struct ssl_iostream *ssl_io)
+{
+ return ssl_io->handshake_failed;
+}
+
+static bool
+openssl_iostream_has_valid_client_cert(const struct ssl_iostream *ssl_io)
+{
+ return ssl_io->cert_received && !ssl_io->cert_broken;
+}
+
+static bool
+openssl_iostream_has_broken_client_cert(struct ssl_iostream *ssl_io)
+{
+ return ssl_io->cert_received && ssl_io->cert_broken;
+}
+
+static const char *
+openssl_iostream_get_peer_name(struct ssl_iostream *ssl_io)
+{
+ X509 *x509;
+ char *name;
+ int len;
+
+ if (!ssl_iostream_has_valid_client_cert(ssl_io))
+ return NULL;
+
+ x509 = SSL_get_peer_certificate(ssl_io->ssl);
+ i_assert(x509 != NULL);
+
+ len = X509_NAME_get_text_by_NID(X509_get_subject_name(x509),
+ ssl_io->username_nid, NULL, 0);
+ if (len < 0)
+ name = "";
+ else {
+ name = t_malloc0(len + 1);
+ if (X509_NAME_get_text_by_NID(X509_get_subject_name(x509),
+ ssl_io->username_nid,
+ name, len + 1) < 0)
+ name = "";
+ else if (strlen(name) != (size_t)len) {
+ /* NUL characters in name. Someone's trying to fake
+ being another user? Don't allow it. */
+ name = "";
+ }
+ }
+ X509_free(x509);
+
+ return *name == '\0' ? NULL : name;
+}
+
+static const char *openssl_iostream_get_server_name(struct ssl_iostream *ssl_io)
+{
+ return ssl_io->sni_host;
+}
+
+static const char *
+openssl_iostream_get_compression(struct ssl_iostream *ssl_io)
+{
+#if defined(HAVE_SSL_COMPRESSION) && !defined(OPENSSL_NO_COMP)
+ const COMP_METHOD *comp;
+
+ comp = SSL_get_current_compression(ssl_io->ssl);
+ return comp == NULL ? NULL : SSL_COMP_get_name(comp);
+#else
+ return NULL;
+#endif
+}
+
+static const char *
+openssl_iostream_get_security_string(struct ssl_iostream *ssl_io)
+{
+ const SSL_CIPHER *cipher;
+#if defined(HAVE_SSL_COMPRESSION) && !defined(OPENSSL_NO_COMP)
+ const COMP_METHOD *comp;
+#endif
+ const char *comp_str;
+ int bits, alg_bits;
+
+ if (!ssl_io->handshaked)
+ return "";
+
+ cipher = SSL_get_current_cipher(ssl_io->ssl);
+ bits = SSL_CIPHER_get_bits(cipher, &alg_bits);
+#if defined(HAVE_SSL_COMPRESSION) && !defined(OPENSSL_NO_COMP)
+ comp = SSL_get_current_compression(ssl_io->ssl);
+ comp_str = comp == NULL ? "" :
+ t_strconcat(" ", SSL_COMP_get_name(comp), NULL);
+#else
+ comp_str = "";
+#endif
+ return t_strdup_printf("%s with cipher %s (%d/%d bits)%s",
+ SSL_get_version(ssl_io->ssl),
+ SSL_CIPHER_get_name(cipher),
+ bits, alg_bits, comp_str);
+}
+
+static const char *
+openssl_iostream_get_last_error(struct ssl_iostream *ssl_io)
+{
+ return ssl_io->last_error;
+}
+
+static const char *
+openssl_iostream_get_cipher(struct ssl_iostream *ssl_io, unsigned int *bits_r)
+{
+ if (!ssl_io->handshaked)
+ return NULL;
+
+ const SSL_CIPHER *cipher = SSL_get_current_cipher(ssl_io->ssl);
+ *bits_r = SSL_CIPHER_get_bits(cipher, NULL);
+ return SSL_CIPHER_get_name(cipher);
+}
+
+static const char *
+openssl_iostream_get_pfs(struct ssl_iostream *ssl_io)
+{
+ if (!ssl_io->handshaked)
+ return NULL;
+
+ const SSL_CIPHER *cipher = SSL_get_current_cipher(ssl_io->ssl);
+#if defined(HAVE_SSL_CIPHER_get_kx_nid)
+ int nid = SSL_CIPHER_get_kx_nid(cipher);
+ return OBJ_nid2sn(nid);
+#else
+ char buf[128];
+ const char *desc, *ptr;
+ if ((desc = SSL_CIPHER_description(cipher, buf, sizeof(buf)))==NULL ||
+ (ptr = strstr(desc, "Kx=")) == NULL)
+ return "";
+ return t_strcut(ptr+3, ' ');
+#endif
+}
+
+static const char *
+openssl_iostream_get_protocol_name(struct ssl_iostream *ssl_io)
+{
+ if (!ssl_io->handshaked)
+ return NULL;
+ return SSL_get_version(ssl_io->ssl);
+}
+
+
+static const struct iostream_ssl_vfuncs ssl_vfuncs = {
+ .global_init = openssl_iostream_global_init,
+ .context_init_client = openssl_iostream_context_init_client,
+ .context_init_server = openssl_iostream_context_init_server,
+ .context_ref = openssl_iostream_context_ref,
+ .context_unref = openssl_iostream_context_unref,
+
+ .create = openssl_iostream_create,
+ .unref = openssl_iostream_unref,
+ .destroy = openssl_iostream_destroy,
+
+ .handshake = openssl_iostream_handshake,
+ .set_handshake_callback = openssl_iostream_set_handshake_callback,
+ .set_sni_callback = openssl_iostream_set_sni_callback,
+ .change_context = openssl_iostream_change_context,
+
+ .set_log_prefix = openssl_iostream_set_log_prefix,
+ .is_handshaked = openssl_iostream_is_handshaked,
+ .has_handshake_failed = openssl_iostream_has_handshake_failed,
+ .has_valid_client_cert = openssl_iostream_has_valid_client_cert,
+ .has_broken_client_cert = openssl_iostream_has_broken_client_cert,
+ .cert_match_name = openssl_iostream_cert_match_name,
+ .get_peer_name = openssl_iostream_get_peer_name,
+ .get_server_name = openssl_iostream_get_server_name,
+ .get_compression = openssl_iostream_get_compression,
+ .get_security_string = openssl_iostream_get_security_string,
+ .get_last_error = openssl_iostream_get_last_error,
+ .get_cipher = openssl_iostream_get_cipher,
+ .get_pfs = openssl_iostream_get_pfs,
+ .get_protocol_name = openssl_iostream_get_protocol_name,
+};
+
+void ssl_iostream_openssl_init(void)
+{
+ unsigned char buf;
+ if (RAND_bytes(&buf, 1) < 1)
+ i_fatal("OpenSSL RNG failed to initialize");
+ iostream_ssl_module_init(&ssl_vfuncs);
+}
+
+void ssl_iostream_openssl_deinit(void)
+{
+ openssl_iostream_global_deinit();
+}
diff --git a/src/lib-ssl-iostream/iostream-openssl.h b/src/lib-ssl-iostream/iostream-openssl.h
new file mode 100644
index 0000000..4449668
--- /dev/null
+++ b/src/lib-ssl-iostream/iostream-openssl.h
@@ -0,0 +1,129 @@
+#ifndef IOSTREAM_OPENSSL_H
+#define IOSTREAM_OPENSSL_H
+
+#include "iostream-ssl-private.h"
+
+#include <openssl/ssl.h>
+
+#ifndef HAVE_ASN1_STRING_GET0_DATA
+# define ASN1_STRING_get0_data(str) ASN1_STRING_data(str)
+#endif
+enum openssl_iostream_sync_type {
+ OPENSSL_IOSTREAM_SYNC_TYPE_NONE,
+ OPENSSL_IOSTREAM_SYNC_TYPE_FIRST_READ,
+ OPENSSL_IOSTREAM_SYNC_TYPE_CONTINUE_READ,
+ OPENSSL_IOSTREAM_SYNC_TYPE_WRITE,
+ OPENSSL_IOSTREAM_SYNC_TYPE_HANDSHAKE
+};
+
+struct ssl_iostream_context {
+ int refcount;
+ SSL_CTX *ssl_ctx;
+
+ pool_t pool;
+ struct ssl_iostream_settings set;
+
+ int username_nid;
+
+ bool client_ctx:1;
+};
+
+struct ssl_iostream {
+ int refcount;
+ struct ssl_iostream_context *ctx;
+
+ SSL *ssl;
+ BIO *bio_ext;
+
+ struct istream *plain_input;
+ struct ostream *plain_output;
+ struct istream *ssl_input;
+ struct ostream *ssl_output;
+
+ /* SSL clients: host where we connected to */
+ char *connected_host;
+ /* SSL servers: host requested by the client via SNI */
+ char *sni_host;
+ char *last_error;
+ char *log_prefix;
+ char *plain_stream_errstr;
+ int plain_stream_errno;
+
+ /* copied settings */
+ bool verbose, verbose_invalid_cert, allow_invalid_cert;
+ int username_nid;
+
+ ssl_iostream_handshake_callback_t *handshake_callback;
+ void *handshake_context;
+
+ ssl_iostream_sni_callback_t *sni_callback;
+ void *sni_context;
+
+ bool handshaked:1;
+ bool handshake_failed:1;
+ bool cert_received:1;
+ bool cert_broken:1;
+ bool want_read:1;
+ bool ostream_flush_waiting_input:1;
+ bool closed:1;
+ bool destroyed:1;
+};
+
+extern int dovecot_ssl_extdata_index;
+
+struct istream *openssl_i_stream_create_ssl(struct ssl_iostream *ssl_io);
+struct ostream *openssl_o_stream_create_ssl(struct ssl_iostream *ssl_io);
+
+int openssl_iostream_global_init(const struct ssl_iostream_settings *set,
+ const char **error_r);
+
+int openssl_iostream_context_init_client(const struct ssl_iostream_settings *set,
+ struct ssl_iostream_context **ctx_r,
+ const char **error_r);
+int openssl_iostream_context_init_server(const struct ssl_iostream_settings *set,
+ struct ssl_iostream_context **ctx_r,
+ const char **error_r);
+void openssl_iostream_context_ref(struct ssl_iostream_context *ctx);
+void openssl_iostream_context_unref(struct ssl_iostream_context *ctx);
+void openssl_iostream_global_deinit(void);
+
+int openssl_iostream_load_key(const struct ssl_iostream_cert *set,
+ const char *set_name,
+ EVP_PKEY **pkey_r, const char **error_r);
+bool openssl_cert_match_name(SSL *ssl, const char *verify_name,
+ const char **reason_r);
+#define OPENSSL_ALL_PROTOCOL_OPTIONS \
+ (SSL_OP_NO_SSLv2 | SSL_OP_NO_SSLv3 | SSL_OP_NO_TLSv1)
+/* opt_r is used with SSL_set_options() and version_r is used with
+ SSL_set_min_proto_version(). Using either method should enable the same SSL
+ protocol versions. */
+int openssl_min_protocol_to_options(const char *min_protocol, long *opt_r,
+ int *version_r) ATTR_NULL(2,3);
+
+/* Sync plain_input/plain_output streams with BIOs. Returns 1 if at least
+ one byte was read/written, 0 if nothing was written, and -1 if an error
+ occurred. */
+int openssl_iostream_bio_sync(struct ssl_iostream *ssl_io,
+ enum openssl_iostream_sync_type type);
+
+/* Returns 1 if the operation should be retried (we read/wrote more data),
+ 0 if the operation should retried later once more data has been
+ read/written, -1 if a fatal error occurred (errno is set). */
+int openssl_iostream_handle_error(struct ssl_iostream *ssl_io, int ret,
+ enum openssl_iostream_sync_type type,
+ const char *func_name);
+
+/* Perform clean shutdown for the connection. */
+void openssl_iostream_shutdown(struct ssl_iostream *ssl_io);
+
+void openssl_iostream_set_error(struct ssl_iostream *ssl_io, const char *str);
+const char *openssl_iostream_error(void);
+const char *openssl_iostream_key_load_error(void);
+const char *
+openssl_iostream_use_certificate_error(const char *cert, const char *set_name);
+void openssl_iostream_clear_errors(void);
+
+void ssl_iostream_openssl_init(void);
+void ssl_iostream_openssl_deinit(void);
+
+#endif
diff --git a/src/lib-ssl-iostream/iostream-ssl-context-cache.c b/src/lib-ssl-iostream/iostream-ssl-context-cache.c
new file mode 100644
index 0000000..a7245ce
--- /dev/null
+++ b/src/lib-ssl-iostream/iostream-ssl-context-cache.c
@@ -0,0 +1,129 @@
+/* Copyright (c) 2017-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "hash.h"
+#include "iostream-ssl-private.h"
+
+struct ssl_iostream_context_cache {
+ bool server;
+ struct ssl_iostream_settings set;
+};
+
+static pool_t ssl_iostream_contexts_pool;
+static HASH_TABLE(struct ssl_iostream_context_cache *,
+ struct ssl_iostream_context *) ssl_iostream_contexts;
+
+static unsigned int
+ssl_iostream_context_cache_hash(const struct ssl_iostream_context_cache *cache)
+{
+ unsigned int n, i, g, h = 0;
+ const char *const cert[] = { cache->set.cert.cert, cache->set.alt_cert.cert };
+
+ /* checking for different certs is typically good enough,
+ and it should be enough to check only the first few bytes (after the
+ "BEGIN CERTIFICATE" line). */
+ for (n = 0; n < N_ELEMENTS(cert); n++) {
+ if (cert[n] == NULL)
+ continue;
+
+ for (i = 0; i < 64 && cert[n][i] != '\0'; i++) {
+ h = (h << 4) + cert[n][i];
+ if ((g = h & 0xf0000000UL) != 0) {
+ h = h ^ (g >> 24);
+ h = h ^ g;
+ }
+ }
+ }
+ return h ^ (cache->server ? 1 : 0);
+}
+
+static int
+ssl_iostream_context_cache_cmp(const struct ssl_iostream_context_cache *c1,
+ const struct ssl_iostream_context_cache *c2)
+{
+ if (c1->server != c2->server)
+ return -1;
+ return ssl_iostream_settings_equals(&c1->set, &c2->set) ? 0 : -1;
+}
+
+static int
+ssl_iostream_context_cache_get(const struct ssl_iostream_settings *set,
+ bool server,
+ struct ssl_iostream_context **ctx_r,
+ const char **error_r)
+{
+ struct ssl_iostream_context *ctx;
+ struct ssl_iostream_context_cache *cache;
+ struct ssl_iostream_context_cache lookup = {
+ .server = server,
+ .set = *set,
+ };
+
+ if (ssl_iostream_contexts_pool == NULL) {
+ ssl_iostream_contexts_pool =
+ pool_alloconly_create(MEMPOOL_GROWING"ssl iostream context cache", 1024);
+ hash_table_create(&ssl_iostream_contexts,
+ ssl_iostream_contexts_pool, 0,
+ ssl_iostream_context_cache_hash,
+ ssl_iostream_context_cache_cmp);
+ }
+ ssl_iostream_settings_drop_stream_only(&lookup.set);
+
+ ctx = hash_table_lookup(ssl_iostream_contexts, &lookup);
+ if (ctx != NULL) {
+ ssl_iostream_context_ref(ctx);
+ *ctx_r = ctx;
+ return 0;
+ }
+
+ /* add to cache */
+ if (server) {
+ if (ssl_iostream_context_init_server(&lookup.set, &ctx, error_r) < 0)
+ return -1;
+ } else {
+ if (ssl_iostream_context_init_client(&lookup.set, &ctx, error_r) < 0)
+ return -1;
+ }
+
+ cache = p_new(ssl_iostream_contexts_pool,
+ struct ssl_iostream_context_cache, 1);
+ cache->server = server;
+ ssl_iostream_settings_init_from(ssl_iostream_contexts_pool,
+ &cache->set, &lookup.set);
+ hash_table_insert(ssl_iostream_contexts, cache, ctx);
+
+ ssl_iostream_context_ref(ctx);
+ *ctx_r = ctx;
+ return 0;
+}
+
+int ssl_iostream_client_context_cache_get(const struct ssl_iostream_settings *set,
+ struct ssl_iostream_context **ctx_r,
+ const char **error_r)
+{
+ return ssl_iostream_context_cache_get(set, FALSE, ctx_r, error_r);
+}
+
+int ssl_iostream_server_context_cache_get(const struct ssl_iostream_settings *set,
+ struct ssl_iostream_context **ctx_r,
+ const char **error_r)
+{
+ return ssl_iostream_context_cache_get(set, TRUE, ctx_r, error_r);
+}
+
+void ssl_iostream_context_cache_free(void)
+{
+ struct hash_iterate_context *iter;
+ struct ssl_iostream_context_cache *lookup;
+ struct ssl_iostream_context *ctx;
+
+ if (ssl_iostream_contexts_pool == NULL)
+ return;
+
+ iter = hash_table_iterate_init(ssl_iostream_contexts);
+ while (hash_table_iterate(iter, ssl_iostream_contexts, &lookup, &ctx))
+ ssl_iostream_context_unref(&ctx);
+ hash_table_iterate_deinit(&iter);
+ hash_table_destroy(&ssl_iostream_contexts);
+ pool_unref(&ssl_iostream_contexts_pool);
+}
diff --git a/src/lib-ssl-iostream/iostream-ssl-private.h b/src/lib-ssl-iostream/iostream-ssl-private.h
new file mode 100644
index 0000000..c0f4a3a
--- /dev/null
+++ b/src/lib-ssl-iostream/iostream-ssl-private.h
@@ -0,0 +1,64 @@
+#ifndef IOSTREAM_SSL_PRIVATE_H
+#define IOSTREAM_SSL_PRIVATE_H
+
+#include "iostream-ssl.h"
+
+struct iostream_ssl_vfuncs {
+ int (*global_init)(const struct ssl_iostream_settings *set,
+ const char **error_r);
+ int (*context_init_client)(const struct ssl_iostream_settings *set,
+ struct ssl_iostream_context **ctx_r,
+ const char **error_r);
+ int (*context_init_server)(const struct ssl_iostream_settings *set,
+ struct ssl_iostream_context **ctx_r,
+ const char **error_r);
+ void (*context_ref)(struct ssl_iostream_context *ctx);
+ void (*context_unref)(struct ssl_iostream_context *ctx);
+
+ int (*create)(struct ssl_iostream_context *ctx, const char *host,
+ const struct ssl_iostream_settings *set,
+ struct istream **input, struct ostream **output,
+ struct ssl_iostream **iostream_r, const char **error_r);
+ void (*unref)(struct ssl_iostream *ssl_io);
+ void (*destroy)(struct ssl_iostream *ssl_io);
+
+ int (*handshake)(struct ssl_iostream *ssl_io);
+ void (*set_handshake_callback)(struct ssl_iostream *ssl_io,
+ ssl_iostream_handshake_callback_t *callback,
+ void *context);
+ void (*set_sni_callback)(struct ssl_iostream *ssl_io,
+ ssl_iostream_sni_callback_t *callback,
+ void *context);
+ void (*change_context)(struct ssl_iostream *ssl_io,
+ struct ssl_iostream_context *ctx);
+
+ void (*set_log_prefix)(struct ssl_iostream *ssl_io, const char *prefix);
+ bool (*is_handshaked)(const struct ssl_iostream *ssl_io);
+ bool (*has_handshake_failed)(const struct ssl_iostream *ssl_io);
+ bool (*has_valid_client_cert)(const struct ssl_iostream *ssl_io);
+ bool (*has_broken_client_cert)(struct ssl_iostream *ssl_io);
+ bool (*cert_match_name)(struct ssl_iostream *ssl_io, const char *name,
+ const char **reason_r);
+ const char *(*get_peer_name)(struct ssl_iostream *ssl_io);
+ const char *(*get_server_name)(struct ssl_iostream *ssl_io);
+ const char *(*get_compression)(struct ssl_iostream *ssl_io);
+ const char *(*get_security_string)(struct ssl_iostream *ssl_io);
+ const char *(*get_last_error)(struct ssl_iostream *ssl_io);
+ const char *(*get_cipher)(struct ssl_iostream *ssl_io, unsigned int *bits_r);
+ const char *(*get_pfs)(struct ssl_iostream *ssl_io);
+ const char *(*get_protocol_name)(struct ssl_iostream *ssl_io);
+};
+
+void iostream_ssl_module_init(const struct iostream_ssl_vfuncs *vfuncs);
+
+/* Returns TRUE if both settings are equal. Note that NULL and "" aren't
+ treated equal. */
+bool ssl_iostream_settings_equals(const struct ssl_iostream_settings *set1,
+ const struct ssl_iostream_settings *set2);
+/* Clear out all stream-only settings, so only settings useful for a context
+ are left. */
+void ssl_iostream_settings_drop_stream_only(struct ssl_iostream_settings *set);
+
+void ssl_iostream_unref(struct ssl_iostream **ssl_io);
+
+#endif
diff --git a/src/lib-ssl-iostream/iostream-ssl-test.c b/src/lib-ssl-iostream/iostream-ssl-test.c
new file mode 100644
index 0000000..50d0aab
--- /dev/null
+++ b/src/lib-ssl-iostream/iostream-ssl-test.c
@@ -0,0 +1,158 @@
+/* Copyright (c) 2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+
+#include "iostream-ssl.h"
+#include "iostream-ssl-test.h"
+
+static const char *test_ca_cert =
+ "-----BEGIN CERTIFICATE-----\n"
+ "MIIF4TCCA8mgAwIBAgICEAAwDQYJKoZIhvcNAQELBQAwgYQxCzAJBgNVBAYTAk5M\n"
+ "MRMwEQYDVQQIDApHZWxkZXJsYW5kMRIwEAYDVQQHDAlCYXJuZXZlbGQxGjAYBgNV\n"
+ "BAoMEUNoaWNrZW4gQ29vcCBCLlYuMR4wHAYDVQQLDBVDZXJ0aWZpY2F0ZSBBdXRo\n"
+ "b3JpdHkxEDAOBgNVBAMMB1Jvb3QgQ0EwIBcNMTgwMjA4MjEyODE2WhgPMjExODAx\n"
+ "MTUyMTI4MTZaMHgxCzAJBgNVBAYTAk5MMRMwEQYDVQQIDApHZWxkZXJsYW5kMRow\n"
+ "GAYDVQQKDBFDaGlja2VuIENvb3AgQi5WLjEeMBwGA1UECwwVQ2VydGlmaWNhdGUg\n"
+ "QXV0aG9yaXR5MRgwFgYDVQQDDA9JbnRlcm1lZGlhdGUgQ0EwggIiMA0GCSqGSIb3\n"
+ "DQEBAQUAA4ICDwAwggIKAoICAQDgwuwUQ387ALkBO2YAvLiOi0rhQMis+TY34tpN\n"
+ "96Xx9Jaa8gdiAW9y3l8hGFm1+Ens5ZukwMysUoP7rrI5s0XOgCTChzrB4dEnbWHj\n"
+ "2YUYUBVLTLqZ4PTbY6xyrjYHKol1govkU+wclmyeI+Os946U0HFubg+KuXGZ2oLM\n"
+ "iYAmur/oxickEwJX932KhzQS4xdT5o38cVv470ot6eNEAiZcufP/gBSjAyUd8Wge\n"
+ "bwpW64fE/0LyCXYZrK5LWG1dMPC8MpETb8uLAB33r6q3yLTcEWg79bes7SgNrQdx\n"
+ "ncUXBoh8YSJvniZQ6OhwENPGTNhZWzgltDZHASyKXY2ojV70D8iiy/uB+owPSTla\n"
+ "txnu7z8B4kVCBWhCUizk7upjZNA0aFutjEHyYLtxqbTon+iLYm7M4iaga23YBdMU\n"
+ "1QVtulmUY6dcjTJ8GG3uo+qglPKuSodLSb23ovxAdVdIF+BNukd18ZhEIAe08hbw\n"
+ "YBHUYsKNkTMYcwxSgK3yQ4tQw0Cky4wAdsDv1XBK0LZ8+wnuWjnrnO/TRvgLWRU4\n"
+ "qI36OEMk9T0bxi+UwP3mzu78OoMCCdf67ccZ2/zFfHg+dqTBc9zV0sYJ/RQvmEN1\n"
+ "KDgqJAhz+VkDzTBiQYxoztTgBv9yxYufFvwZX4uhsvtMXUuRfvoVwK16vPXnMHwF\n"
+ "muIvwwIDAQABo2YwZDAdBgNVHQ4EFgQUWv/zcVnDWf53C2iN9f6uyUmhp1EwHwYD\n"
+ "VR0jBBgwFoAUsSOnSayEpzvbN21MEQGEyVKx+kIwEgYDVR0TAQH/BAgwBgEB/wIB\n"
+ "ADAOBgNVHQ8BAf8EBAMCAYYwDQYJKoZIhvcNAQELBQADggIBAJUlgJIHnnclDQMn\n"
+ "PSmruCl7bCRvLZYQtSiIv3/K4kPXJMYOvULoXGLA0+CdHoo06k8/TLk7gF1gNCPr\n"
+ "23z2+SCIuvVrVlveWyqD684yQ3UYeWoJnOv90F19267uELrWX4UMVE+z1r7iULyw\n"
+ "cxQokw6EGjw7xHiVETvNlHlmA0/8IuZv89CTztOP54NAXqu8WeloL/ipEhgj1HRx\n"
+ "TkuDf6SlTQ+mXKbiVJFiEA6rOsYFGGWE/SDNbLfx60OoHz0rQ95zw0rv/wPs1Oai\n"
+ "x71Ccuz/i2iI7ItXjQcUEcXvIDFJEMNXDaccIVmk1uda0Cm92W/sGc4vG2LUwEpl\n"
+ "LRj/x4Q39WaZTCrZzd6p2+6tYA45VnoblZ0enYU5XcQzryR/GC7VWFH1OvzOcSG5\n"
+ "NhpKIiWZvuMG5ilXyw7yh7cnWiPvGp8zCO7w1IOyk8sETQBstxuiALeEdrgdz6R5\n"
+ "jV5oIqsCmsIihfRgNufx/SImTJvue4uYgrKa4jo1tw+CFkEWPd7zXjferkxyU8C5\n"
+ "Y+Fr3yMuis5O4qa5mb94r0AQhc8MbCAuInSqNGX0Iu/UTg6Z+56omA2CnKGt6Rwd\n"
+ "LxLo2vhT9gTF88QwTMBPlhVjBbjTRhmY+mHv9gh3GczQ/i5VRXyYQH4h7EBtKFFI\n"
+ "t4mBWMavY+hS/zVkufYzUcUR7D1P\n"
+ "-----END CERTIFICATE-----\n"
+ "-----BEGIN CERTIFICATE-----\n"
+ "MIIF8jCCA9qgAwIBAgIJAN0zFa9E/xyxMA0GCSqGSIb3DQEBCwUAMIGEMQswCQYD\n"
+ "VQQGEwJOTDETMBEGA1UECAwKR2VsZGVybGFuZDESMBAGA1UEBwwJQmFybmV2ZWxk\n"
+ "MRowGAYDVQQKDBFDaGlja2VuIENvb3AgQi5WLjEeMBwGA1UECwwVQ2VydGlmaWNh\n"
+ "dGUgQXV0aG9yaXR5MRAwDgYDVQQDDAdSb290IENBMCAXDTE4MDIwODIxMjA1NloY\n"
+ "DzIyMTcxMjIyMjEyMDU2WjCBhDELMAkGA1UEBhMCTkwxEzARBgNVBAgMCkdlbGRl\n"
+ "cmxhbmQxEjAQBgNVBAcMCUJhcm5ldmVsZDEaMBgGA1UECgwRQ2hpY2tlbiBDb29w\n"
+ "IEIuVi4xHjAcBgNVBAsMFUNlcnRpZmljYXRlIEF1dGhvcml0eTEQMA4GA1UEAwwH\n"
+ "Um9vdCBDQTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAKMvVLNtj1C+\n"
+ "ZQ4ypNIA5+zB8oseN65k8VqpyhcPAQv0M/HvOB8jXzWH4v1VfKOYkTxQXqu4v0RP\n"
+ "6k0awe/6FH0GDlYhKUuPNDH2djlGOVbq+qBdSXpC3UjEcksxIuigCmzdkuytnlhW\n"
+ "qQYnLVl6kXwYtzjWsetTZBGseCpSYBWnvdtG/MGQNozi03VsGFkj/fbwuLK7ZHVp\n"
+ "64QLk9j3IPZRPHUaFlnT+v2ySMjO8OncsZ/fMZ/nxmt8GJ/68cMy9czydauz2KZs\n"
+ "pQEFS6s/HCmRXT1VQZ7zw5V/PBnF7ecveTaQtxJoNO4Pr7sh77El/ChUxN1Acw4N\n"
+ "2UH/06k6xnirLsvJonCRbVX3bxPBoDzGHjPqb7r0AKD0WBvrzgjeooSjobEtcIvA\n"
+ "LntiGKp4KtvWKcANPWkutH9X71U7M773oMmrz5fWvz9yv3wVuyblZSaMBwrV16GX\n"
+ "mcym6KF+Oj7j86jNq4wxNtjiQVV0QZcBijtnWpHaD5EMhI/TZvLK9oCFyAL92Wzi\n"
+ "t95r8g3D/8ue0CHqB/EpodH88MdVwr7sgxLQ40KibpErOXb93CJnq/7MVMO/EzTj\n"
+ "4XiGGUOo0elLqEPjzBO6AiGEgXAE2iNoghX79cbMQFtk9sK7XdMVLoXwBvt+Naaz\n"
+ "w96+7R+rZ4SsfrtlrP7xoCPJXbeQ4YI9AgMBAAGjYzBhMB0GA1UdDgQWBBSxI6dJ\n"
+ "rISnO9s3bUwRAYTJUrH6QjAfBgNVHSMEGDAWgBSxI6dJrISnO9s3bUwRAYTJUrH6\n"
+ "QjAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBhjANBgkqhkiG9w0BAQsF\n"
+ "AAOCAgEAQ30mub/GBwThYX3h9p01kQh+/eXXQ47h1xG3B870EwJ2y36wlGbAhZmE\n"
+ "7o1kDZwhNMR4NxT2PSWQHn8m6RiuSGAG9DU2q55tPEZg2DqkQmoCFvV3n2MVIAwL\n"
+ "rZ8c9EaoM5RkeeDBmuVo8H1aCvd5oLJ5j64z6wkgsSRwVXkxQLOAEdmRSVHN/c/6\n"
+ "QEdg0Uh5wkeC7R5wiwQUEkhLie+XwUPG7dIJHWp9g5oVO7IN+KWBLWiqbAJhVFhF\n"
+ "evOSqGDRV/Q2kfwSqDRrokk7CaE8KO/i+AUTF4TFQc/ewCLSeBSkvV7ORXBbe7ob\n"
+ "ShGViL7WEngpGAVoDZEsSViXQ36a5zxCvYcGjHcsKUITPMiD55x0aKNWjc0XfEg4\n"
+ "JtWvYWwygxTcefbs9pxHrmEnyCPpyDB8cPj866JAeaEAhxhDtSqaBE/ek576aJ+Z\n"
+ "ZaGjBQDDhRndLhTPAx1EXB8jgl/yjD+KMUqHs39UowKH25iBxMiW3R1XDIyYFGyi\n"
+ "+UFP+5NgokW/z6JfpUYd4W3jRcareS10UrLQC8tk4vvixk+1MuNKmzBy2eRITYZz\n"
+ "KiYX6NTvbvRt6XsKil8ypHKvWH+i2Cn3JrnTaCzJ4y66lnbRs4/ZnRceqRz35i39\n"
+ "rNT5Ier3SjmyIulxnmoYXHInIcS0TSV1+byyaTUCHKHLx12RxAM=\n"
+ "-----END CERTIFICATE-----\n";
+
+static const char *test_server_cert =
+ "-----BEGIN CERTIFICATE-----\n"
+ "MIIF/jCCA+agAwIBAgICEAMwDQYJKoZIhvcNAQELBQAweDELMAkGA1UEBhMCTkwx\n"
+ "EzARBgNVBAgMCkdlbGRlcmxhbmQxGjAYBgNVBAoMEUNoaWNrZW4gQ29vcCBCLlYu\n"
+ "MR4wHAYDVQQLDBVDZXJ0aWZpY2F0ZSBBdXRob3JpdHkxGDAWBgNVBAMMD0ludGVy\n"
+ "bWVkaWF0ZSBDQTAgFw0xOTAyMTkxMDI2MzdaGA8yMTIxMTAyMjEwMjYzN1owgbUx\n"
+ "CzAJBgNVBAYTAk5MMRMwEQYDVQQIDApHZWxkZXJsYW5kMRIwEAYDVQQHDAlCYXJu\n"
+ "ZXZlbGQxGjAYBgNVBAoMEUNoaWNrZW4gQ29vcCBCLlYuMSIwIAYDVQQLDBlDaGlj\n"
+ "a2VuIENvb3AgV2ViIFNlcnZpY2VzMRIwEAYDVQQDDAkxMjcuMC4wLjExKTAnBgkq\n"
+ "hkiG9w0BCQEWGmhlbm5pZUBjaGlja2VuY29vcC5leGFtcGxlMIIBIjANBgkqhkiG\n"
+ "9w0BAQEFAAOCAQ8AMIIBCgKCAQEA3mNxJKdYUP88HHAPP8FWSDYKofxg2ECJsBQP\n"
+ "cIEvhuDsOpU+kBp3CbVFL+it4kNq7KvYvQyGvag8JA3RFrvsj3/nFM99lPm6RP1y\n"
+ "h9hNYVWRHHWKyOTdwqhk9bOrLk8j8fxw4NfQ/dkURYLJ0OtVTtJELJsxif5BfIAP\n"
+ "ypHayfnFoNa3rFG3uGULzJlb5JOHJHr7vxOTrRUZGYFZeBFacKF14CHYMUkpr9dv\n"
+ "BLaGqKFF0Lx2TU0tzRIXGu2AIFydcM0eGhCFLOV8YqxwT0R+XyNoNG9jRVSewWx0\n"
+ "r7rAF/93vARdRQVDmVoLmxrUMldYN7mQ9MsuGkMBfecLO4HqrQIDAQABo4IBUDCC\n"
+ "AUwwCQYDVR0TBAIwADARBglghkgBhvhCAQEEBAMCBkAwMwYJYIZIAYb4QgENBCYW\n"
+ "JE9wZW5TU0wgR2VuZXJhdGVkIFNlcnZlciBDZXJ0aWZpY2F0ZTAdBgNVHQ4EFgQU\n"
+ "Ez4hL16BDA+GMrQs+g3/1Do4Vr4wgbIGA1UdIwSBqjCBp4AUWv/zcVnDWf53C2iN\n"
+ "9f6uyUmhp1GhgYqkgYcwgYQxCzAJBgNVBAYTAk5MMRMwEQYDVQQIDApHZWxkZXJs\n"
+ "YW5kMRIwEAYDVQQHDAlCYXJuZXZlbGQxGjAYBgNVBAoMEUNoaWNrZW4gQ29vcCBC\n"
+ "LlYuMR4wHAYDVQQLDBVDZXJ0aWZpY2F0ZSBBdXRob3JpdHkxEDAOBgNVBAMMB1Jv\n"
+ "b3QgQ0GCAhAAMA4GA1UdDwEB/wQEAwIFoDATBgNVHSUEDDAKBggrBgEFBQcDATAN\n"
+ "BgkqhkiG9w0BAQsFAAOCAgEAVbs9RDJiVvGAW2cQv5OvghvMECF2Lh0pxsyOeywz\n"
+ "AQBBEaz85ZTRjtQjTWxpJwgwnIFEmfMEpuZoDoffCWDKUNkt24DD9TN8kriHplPK\n"
+ "5P3qZsnuV/E6CiYxBNpYhEle001XO+sU3yjAMeynPZWSZVz1JnjAdh1+opVd62O8\n"
+ "RX7twWdaSUBdsw7JJ2tdtTPeYkyRqDbfQWZSbA4/3VOwLuOs38zgy5ZqHy+YF/MM\n"
+ "48D1cOI0K1qpJE4qYLzcoULSnJ6C8KsS9LsspiLSDGdEvFY0DnCOa/POG6WiuEGk\n"
+ "JGRD+bDbEPZmNU0OLWjzM3vC2qxjF2PC8Q4chhkm4s4cAT7DMf+nnwqTPGK1U3N3\n"
+ "S5xhHrQ1GMHDhrqmKIrBOxdc8Hg7CwCU+C7TXxIPuQlbdStqMW/lVfbRNKsm/Hr6\n"
+ "smOC/hTxuned5DxRkDTCKNof3bZt8k9dqFLjqj14txmITq7MLhJLcsXgECWe7HmX\n"
+ "imxYJzLHjJU6QoDqGXHEMNrIBp46aIOcTjJP6tfosgAzplj+o00xdpMH9Q9hG602\n"
+ "lpgS9JfemZI68jPTZeGie9wXilPp4kE4qmlxhKSsjh3RVHo6ju7uKI5vCBkk15C6\n"
+ "lQMIGAmkRJ5vkcpUWnO6dKCSQdru/gKn3X4JVAHwm4178gqfRkofT28xAZ+vZoFj\n"
+ "L7M=\n"
+ "-----END CERTIFICATE-----\n";
+
+static const char *test_server_key =
+ "-----BEGIN RSA PRIVATE KEY-----\n"
+ "MIIEowIBAAKCAQEA3mNxJKdYUP88HHAPP8FWSDYKofxg2ECJsBQPcIEvhuDsOpU+\n"
+ "kBp3CbVFL+it4kNq7KvYvQyGvag8JA3RFrvsj3/nFM99lPm6RP1yh9hNYVWRHHWK\n"
+ "yOTdwqhk9bOrLk8j8fxw4NfQ/dkURYLJ0OtVTtJELJsxif5BfIAPypHayfnFoNa3\n"
+ "rFG3uGULzJlb5JOHJHr7vxOTrRUZGYFZeBFacKF14CHYMUkpr9dvBLaGqKFF0Lx2\n"
+ "TU0tzRIXGu2AIFydcM0eGhCFLOV8YqxwT0R+XyNoNG9jRVSewWx0r7rAF/93vARd\n"
+ "RQVDmVoLmxrUMldYN7mQ9MsuGkMBfecLO4HqrQIDAQABAoIBAQCGV4Az6ju5wlXn\n"
+ "v/IWS4751Fub+z/tox8KFTQ2fHPfgORzh1Dh8HrUjIKdLGxOcPeYvT8TBQwoagba\n"
+ "qNYUa7W+Aj/wHF/6rNlPb+POGGa2U+BzVrZeIZOtUdibbMwOD5ThS+RMj1Ma5hYO\n"
+ "37FW2bMRCIhSgfXtLIEW2q2va2jF9VI+c7F9dL01COG65nwUSJaRSLPrK5WbVNZE\n"
+ "yNm2st88iiu3TtPJGty+wmFgyV0PjAYYqxVEPs6qcmYMuHrfKlrntDfYai6x7fb+\n"
+ "RDfHhmqmn2oV/LGl7YtH+jBML9WG+Rfw+M7pYsd3ffRjG4aRxSTotu0QZfjduP0a\n"
+ "NZL1C3OBAoGBAPrzC4X2/fPGfyHksg+ecPS2Pvnyr+L9Gtg2Dg9wAONbmYqGKYxa\n"
+ "7DUBr6WWbOy9qJKk1CHR2Gn5FK3KbHG1PhLiOuG00kjm6imSCFTpJ3sBXUCl9qMk\n"
+ "FJnBifT4//INLFIcLjFlygR20hHgqyLU8P2kKzM+7iPaF1GYdILYHnExAoGBAOLd\n"
+ "PmKFcwuD4VfzTwK/Va0L8FFi3Oo7O4OE+9PyfTCJ8fH9PW03e4DiddjM0djDovWW\n"
+ "sm1ySVTfHjE0rsnaMGngPvqhG5T6IMUtFowEa2VwqwfFrPzK4PUYeRV1G+rTL1ZX\n"
+ "RGV1nPxCRXog91gzDCuyYB7jblrjG+DP+ZfGI5I9AoGAJc8Mg2iNJndXnDGqqjPC\n"
+ "7PuwTVRFL7vWmZC7WZQUbizU20wPYngocmwInLgnPRvuE/oFg/rr0juW5ABFinQ2\n"
+ "H/45xNvLevRff1fjLXfbXOr9s8nNeRLsj6XbNS920G8vqEdaplKhtz53s/3Xiu3u\n"
+ "SSi84YGvu3MWZFLF6xjIrWECgYAI2tXahpbs9iLPigGle85eSL8CjjdNNS6nfYNO\n"
+ "zIIyaM/2wAmrv6SkbTJoWeY+7bPong8s0m8mTucgyIuh+VA2cbhDlBI9iF3LFG1y\n"
+ "3aFLflBOp1qPK2QIbQIc4ktKqR+J4TIcO7D676NClxLQcH2jHv09d2cRSRgHeFan\n"
+ "o+YziQKBgEtqnknKgedinexHheBhE/fo49bHwNYZBfA5ZKqQDguNBafU1BUOmPEO\n"
+ "eNb8S5cJDMR7zKeZJ9dHzf4j78sITigwxt8+Ee6VY92U/uTFkPKZNLpAr3OfDSdh\n"
+ "z+M/zgztKqdrSKhr64g/3Dbbe+XqdeGe8MIx+P+QN+SrJNNaNZ1r\n"
+ "-----END RSA PRIVATE KEY-----\n";
+
+void ssl_iostream_test_settings_server(struct ssl_iostream_settings *test_set)
+{
+ i_zero(test_set);
+ test_set->ca = test_ca_cert;
+ test_set->cert.cert = test_server_cert;
+ test_set->cert.key = test_server_key;
+ test_set->skip_crl_check = TRUE;
+}
+
+void ssl_iostream_test_settings_client(struct ssl_iostream_settings *test_set)
+{
+ i_zero(test_set);
+ test_set->ca = test_ca_cert;
+ test_set->skip_crl_check = TRUE;
+}
diff --git a/src/lib-ssl-iostream/iostream-ssl-test.h b/src/lib-ssl-iostream/iostream-ssl-test.h
new file mode 100644
index 0000000..82c5811
--- /dev/null
+++ b/src/lib-ssl-iostream/iostream-ssl-test.h
@@ -0,0 +1,9 @@
+#ifndef IOSTREAM_SSL_TEST_H
+#define IOSTREAM_SSL_TEST_H
+
+struct ssl_iostream_settings;
+
+void ssl_iostream_test_settings_server(struct ssl_iostream_settings *test_set);
+void ssl_iostream_test_settings_client(struct ssl_iostream_settings *test_set);
+
+#endif
diff --git a/src/lib-ssl-iostream/iostream-ssl.c b/src/lib-ssl-iostream/iostream-ssl.c
new file mode 100644
index 0000000..430e69d
--- /dev/null
+++ b/src/lib-ssl-iostream/iostream-ssl.c
@@ -0,0 +1,351 @@
+/* Copyright (c) 2009-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "module-dir.h"
+#include "iostream-ssl-private.h"
+
+#define OFFSET(name) offsetof(struct ssl_iostream_settings, name)
+static const size_t ssl_iostream_settings_string_offsets[] = {
+ OFFSET(min_protocol),
+ OFFSET(cipher_list),
+ OFFSET(ciphersuites),
+ OFFSET(curve_list),
+ OFFSET(ca),
+ OFFSET(ca_file),
+ OFFSET(ca_dir),
+ OFFSET(cert.cert),
+ OFFSET(cert.key),
+ OFFSET(cert.key_password),
+ OFFSET(alt_cert.cert),
+ OFFSET(alt_cert.key),
+ OFFSET(alt_cert.key_password),
+ OFFSET(dh),
+ OFFSET(cert_username_field),
+ OFFSET(crypto_device),
+};
+
+static bool ssl_module_loaded = FALSE;
+#ifdef HAVE_SSL
+static struct module *ssl_module = NULL;
+#endif
+static const struct iostream_ssl_vfuncs *ssl_vfuncs = NULL;
+
+#ifdef HAVE_SSL
+static void ssl_module_unload(void)
+{
+ ssl_iostream_context_cache_free();
+ module_dir_unload(&ssl_module);
+}
+#endif
+
+void iostream_ssl_module_init(const struct iostream_ssl_vfuncs *vfuncs)
+{
+ ssl_vfuncs = vfuncs;
+ ssl_module_loaded = TRUE;
+}
+
+int ssl_module_load(const char **error_r)
+{
+#ifdef HAVE_SSL
+ const char *plugin_name = "ssl_iostream_openssl";
+ struct module_dir_load_settings mod_set;
+
+ i_zero(&mod_set);
+ mod_set.abi_version = DOVECOT_ABI_VERSION;
+ mod_set.setting_name = "<built-in lib-ssl-iostream lookup>";
+ mod_set.require_init_funcs = TRUE;
+ ssl_module = module_dir_load(MODULE_DIR, plugin_name, &mod_set);
+ if (module_dir_try_load_missing(&ssl_module, MODULE_DIR, plugin_name,
+ &mod_set, error_r) < 0)
+ return -1;
+ module_dir_init(ssl_module);
+ if (!ssl_module_loaded) {
+ *error_r = t_strdup_printf(
+ "%s didn't call iostream_ssl_module_init() - SSL not initialized",
+ plugin_name);
+ module_dir_unload(&ssl_module);
+ return -1;
+ }
+
+ /* Destroy SSL module after (most of) the others. Especially lib-fs
+ backends may still want to access SSL module in their own
+ atexit-callbacks. */
+ lib_atexit_priority(ssl_module_unload, LIB_ATEXIT_PRIORITY_LOW);
+ return 0;
+#else
+ *error_r = "SSL support not compiled in";
+ return -1;
+#endif
+}
+
+int io_stream_ssl_global_init(const struct ssl_iostream_settings *set,
+ const char **error_r)
+{
+ return ssl_vfuncs->global_init(set, error_r);
+}
+
+int ssl_iostream_context_init_client(const struct ssl_iostream_settings *set,
+ struct ssl_iostream_context **ctx_r,
+ const char **error_r)
+{
+ struct ssl_iostream_settings set_copy = *set;
+
+ /* ensure this is set to TRUE */
+ set_copy.verify_remote_cert = TRUE;
+
+ if (!ssl_module_loaded) {
+ if (ssl_module_load(error_r) < 0)
+ return -1;
+ }
+ if (io_stream_ssl_global_init(&set_copy, error_r) < 0)
+ return -1;
+ return ssl_vfuncs->context_init_client(&set_copy, ctx_r, error_r);
+}
+
+int ssl_iostream_context_init_server(const struct ssl_iostream_settings *set,
+ struct ssl_iostream_context **ctx_r,
+ const char **error_r)
+{
+ if (!ssl_module_loaded) {
+ if (ssl_module_load(error_r) < 0)
+ return -1;
+ }
+ if (io_stream_ssl_global_init(set, error_r) < 0)
+ return -1;
+ return ssl_vfuncs->context_init_server(set, ctx_r, error_r);
+}
+
+void ssl_iostream_context_ref(struct ssl_iostream_context *ctx)
+{
+ ssl_vfuncs->context_ref(ctx);
+}
+
+void ssl_iostream_context_unref(struct ssl_iostream_context **_ctx)
+{
+ struct ssl_iostream_context *ctx = *_ctx;
+
+ if (*_ctx == NULL)
+ return;
+ *_ctx = NULL;
+ ssl_vfuncs->context_unref(ctx);
+}
+
+int io_stream_create_ssl_client(struct ssl_iostream_context *ctx, const char *host,
+ const struct ssl_iostream_settings *set,
+ struct istream **input, struct ostream **output,
+ struct ssl_iostream **iostream_r,
+ const char **error_r)
+{
+ struct ssl_iostream_settings set_copy = *set;
+ set_copy.verify_remote_cert = TRUE;
+ return ssl_vfuncs->create(ctx, host, &set_copy, input, output,
+ iostream_r, error_r);
+}
+
+int io_stream_create_ssl_server(struct ssl_iostream_context *ctx,
+ const struct ssl_iostream_settings *set,
+ struct istream **input, struct ostream **output,
+ struct ssl_iostream **iostream_r,
+ const char **error_r)
+{
+ return ssl_vfuncs->create(ctx, NULL, set, input, output,
+ iostream_r, error_r);
+}
+
+void ssl_iostream_unref(struct ssl_iostream **_ssl_io)
+{
+ struct ssl_iostream *ssl_io = *_ssl_io;
+
+ *_ssl_io = NULL;
+ ssl_vfuncs->unref(ssl_io);
+}
+
+void ssl_iostream_destroy(struct ssl_iostream **_ssl_io)
+{
+ struct ssl_iostream *ssl_io;
+
+ if (_ssl_io == NULL || *_ssl_io == NULL)
+ return;
+
+ ssl_io = *_ssl_io;
+ *_ssl_io = NULL;
+ ssl_vfuncs->destroy(ssl_io);
+}
+
+void ssl_iostream_set_log_prefix(struct ssl_iostream *ssl_io,
+ const char *prefix)
+{
+ ssl_vfuncs->set_log_prefix(ssl_io, prefix);
+}
+
+int ssl_iostream_handshake(struct ssl_iostream *ssl_io)
+{
+ return ssl_vfuncs->handshake(ssl_io);
+}
+
+void ssl_iostream_set_handshake_callback(struct ssl_iostream *ssl_io,
+ ssl_iostream_handshake_callback_t *callback,
+ void *context)
+{
+ ssl_vfuncs->set_handshake_callback(ssl_io, callback, context);
+}
+
+void ssl_iostream_set_sni_callback(struct ssl_iostream *ssl_io,
+ ssl_iostream_sni_callback_t *callback,
+ void *context)
+{
+ ssl_vfuncs->set_sni_callback(ssl_io, callback, context);
+}
+
+void ssl_iostream_change_context(struct ssl_iostream *ssl_io,
+ struct ssl_iostream_context *ctx)
+{
+ ssl_vfuncs->change_context(ssl_io, ctx);
+}
+
+bool ssl_iostream_is_handshaked(const struct ssl_iostream *ssl_io)
+{
+ return ssl_vfuncs->is_handshaked(ssl_io);
+}
+
+bool ssl_iostream_has_handshake_failed(const struct ssl_iostream *ssl_io)
+{
+ return ssl_vfuncs->has_handshake_failed(ssl_io);
+}
+
+bool ssl_iostream_has_valid_client_cert(const struct ssl_iostream *ssl_io)
+{
+ return ssl_vfuncs->has_valid_client_cert(ssl_io);
+}
+
+bool ssl_iostream_has_broken_client_cert(struct ssl_iostream *ssl_io)
+{
+ return ssl_vfuncs->has_broken_client_cert(ssl_io);
+}
+
+bool ssl_iostream_cert_match_name(struct ssl_iostream *ssl_io, const char *name,
+ const char **reason_r)
+{
+ return ssl_vfuncs->cert_match_name(ssl_io, name, reason_r);
+}
+
+int ssl_iostream_check_cert_validity(struct ssl_iostream *ssl_io,
+ const char *host, const char **error_r)
+{
+ const char *reason;
+
+ if (!ssl_iostream_has_valid_client_cert(ssl_io)) {
+ if (!ssl_iostream_has_broken_client_cert(ssl_io))
+ *error_r = "SSL certificate not received";
+ else {
+ *error_r = t_strdup(ssl_iostream_get_last_error(ssl_io));
+ if (*error_r == NULL)
+ *error_r = "Received invalid SSL certificate";
+ }
+ return -1;
+ } else if (!ssl_iostream_cert_match_name(ssl_io, host, &reason)) {
+ *error_r = t_strdup_printf(
+ "SSL certificate doesn't match expected host name %s: %s",
+ host, reason);
+ return -1;
+ }
+ return 0;
+}
+
+const char *ssl_iostream_get_peer_name(struct ssl_iostream *ssl_io)
+{
+ return ssl_vfuncs->get_peer_name(ssl_io);
+}
+
+const char *ssl_iostream_get_server_name(struct ssl_iostream *ssl_io)
+{
+ return ssl_vfuncs->get_server_name(ssl_io);
+}
+
+const char *ssl_iostream_get_compression(struct ssl_iostream *ssl_io)
+{
+ return ssl_vfuncs->get_compression(ssl_io);
+}
+
+const char *ssl_iostream_get_security_string(struct ssl_iostream *ssl_io)
+{
+ return ssl_vfuncs->get_security_string(ssl_io);
+}
+
+const char *ssl_iostream_get_last_error(struct ssl_iostream *ssl_io)
+{
+ return ssl_vfuncs->get_last_error(ssl_io);
+}
+
+struct ssl_iostream_settings *ssl_iostream_settings_dup(pool_t pool,
+ const struct ssl_iostream_settings *old_set)
+{
+ struct ssl_iostream_settings *new_set;
+
+ new_set = p_new(pool, struct ssl_iostream_settings, 1);
+ ssl_iostream_settings_init_from(pool, new_set, old_set);
+ return new_set;
+}
+
+void ssl_iostream_settings_init_from(pool_t pool,
+ struct ssl_iostream_settings *dest,
+ const struct ssl_iostream_settings *src)
+{
+ unsigned int i;
+
+ *dest = *src;
+ for (i = 0; i < N_ELEMENTS(ssl_iostream_settings_string_offsets); i++) {
+ const size_t offset = ssl_iostream_settings_string_offsets[i];
+ const char *const *src_str = CONST_PTR_OFFSET(src, offset);
+ const char **dest_str = PTR_OFFSET(dest, offset);
+ *dest_str = p_strdup(pool, *src_str);
+ }
+}
+
+bool ssl_iostream_settings_equals(const struct ssl_iostream_settings *set1,
+ const struct ssl_iostream_settings *set2)
+{
+ struct ssl_iostream_settings set1_nonstr, set2_nonstr;
+ unsigned int i;
+
+ set1_nonstr = *set1;
+ set2_nonstr = *set2;
+ for (i = 0; i < N_ELEMENTS(ssl_iostream_settings_string_offsets); i++) {
+ const size_t offset = ssl_iostream_settings_string_offsets[i];
+ const char **str1 = PTR_OFFSET(&set1_nonstr, offset);
+ const char **str2 = PTR_OFFSET(&set2_nonstr, offset);
+
+ if (null_strcmp(*str1, *str2) != 0)
+ return FALSE;
+
+ /* clear away the string pointer from the settings struct */
+ *str1 = NULL;
+ *str2 = NULL;
+ }
+ /* The set*_nonstr no longer have any pointers, so we can compare them
+ directly. */
+ return memcmp(&set1_nonstr, &set2_nonstr, sizeof(set1_nonstr)) == 0;
+}
+
+void ssl_iostream_settings_drop_stream_only(struct ssl_iostream_settings *set)
+{
+ set->verbose = FALSE;
+ set->verbose_invalid_cert = FALSE;
+ set->allow_invalid_cert = FALSE;
+}
+
+const char *ssl_iostream_get_cipher(struct ssl_iostream *ssl_io,
+ unsigned int *bits_r)
+{
+ return ssl_vfuncs->get_cipher(ssl_io, bits_r);
+}
+
+const char *ssl_iostream_get_pfs(struct ssl_iostream *ssl_io)
+{
+ return ssl_vfuncs->get_pfs(ssl_io);
+}
+
+const char *ssl_iostream_get_protocol_name(struct ssl_iostream *ssl_io)
+{
+ return ssl_vfuncs->get_protocol_name(ssl_io);
+}
diff --git a/src/lib-ssl-iostream/iostream-ssl.h b/src/lib-ssl-iostream/iostream-ssl.h
new file mode 100644
index 0000000..3224486
--- /dev/null
+++ b/src/lib-ssl-iostream/iostream-ssl.h
@@ -0,0 +1,175 @@
+#ifndef IOSTREAM_SSL_H
+#define IOSTREAM_SSL_H
+
+struct ssl_iostream;
+struct ssl_iostream_context;
+
+struct ssl_iostream_cert {
+ const char *cert;
+ const char *key;
+ const char *key_password;
+};
+
+struct ssl_iostream_settings {
+ /* NOTE: when updating, remember to update:
+ ssl_iostream_settings_string_offsets[],
+ ssl_iostream_settings_drop_stream_only() */
+ const char *min_protocol; /* both */
+ const char *cipher_list; /* both */
+ const char *ciphersuites; /* both, TLSv1.3 only */
+ const char *curve_list; /* both */
+ const char *ca, *ca_file, *ca_dir; /* context-only */
+ /* alternative cert is for providing certificate using
+ different key algorithm */
+ struct ssl_iostream_cert cert; /* both */
+ struct ssl_iostream_cert alt_cert; /* both */
+ const char *dh; /* context-only */
+ const char *cert_username_field; /* both */
+ const char *crypto_device; /* context-only */
+
+ bool verbose, verbose_invalid_cert; /* stream-only */
+ bool skip_crl_check; /* context-only */
+ bool verify_remote_cert; /* neither/both */
+ bool allow_invalid_cert; /* stream-only */
+ bool prefer_server_ciphers; /* both */
+ bool compression; /* context-only */
+ bool tickets; /* context-only */
+};
+
+/* Load SSL module */
+int ssl_module_load(const char **error_r);
+
+/* Returns 0 if ok, -1 and sets error_r if failed. The returned error string
+ becomes available via ssl_iostream_get_last_error(). The callback most
+ likely should be calling ssl_iostream_check_cert_validity(). */
+typedef int
+ssl_iostream_handshake_callback_t(const char **error_r, void *context);
+/* Called when TLS SNI becomes available. */
+typedef int ssl_iostream_sni_callback_t(const char *name, const char **error_r,
+ void *context);
+
+/* Explicitly initialize SSL library globally. This is also done automatically
+ when the first SSL connection is created, but it may be useful to call it
+ earlier in case of chrooting. After the initialization is successful, any
+ further calls will just be ignored. Returns 0 on success, -1 on error. */
+int io_stream_ssl_global_init(const struct ssl_iostream_settings *set,
+ const char **error_r);
+
+int io_stream_create_ssl_client(struct ssl_iostream_context *ctx, const char *host,
+ const struct ssl_iostream_settings *set,
+ struct istream **input, struct ostream **output,
+ struct ssl_iostream **iostream_r,
+ const char **error_r);
+int io_stream_create_ssl_server(struct ssl_iostream_context *ctx,
+ const struct ssl_iostream_settings *set,
+ struct istream **input, struct ostream **output,
+ struct ssl_iostream **iostream_r,
+ const char **error_r);
+/* Shutdown SSL connection and unreference ssl iostream.
+ The returned input and output streams must also be unreferenced. */
+void ssl_iostream_destroy(struct ssl_iostream **ssl_io);
+
+/* If verbose logging is enabled, use the specified log prefix */
+void ssl_iostream_set_log_prefix(struct ssl_iostream *ssl_io,
+ const char *prefix);
+
+int ssl_iostream_handshake(struct ssl_iostream *ssl_io);
+/* Call the given callback when SSL handshake finishes. The callback must
+ verify whether the certificate and its hostname is valid. If there is no
+ callback, the default is to use ssl_iostream_check_cert_validity() with the
+ same host as given to io_stream_create_ssl_client()
+
+ Before the callback is called, certificate is only checked for issuer
+ and validity period. You should call ssl_iostream_check_cert_validity()
+ in your callback.
+*/
+void ssl_iostream_set_handshake_callback(struct ssl_iostream *ssl_io,
+ ssl_iostream_handshake_callback_t *callback,
+ void *context);
+/* Call the given callback when client sends SNI. The callback can change the
+ ssl_iostream's context (with different certificates) by using
+ ssl_iostream_change_context(). */
+void ssl_iostream_set_sni_callback(struct ssl_iostream *ssl_io,
+ ssl_iostream_sni_callback_t *callback,
+ void *context);
+void ssl_iostream_change_context(struct ssl_iostream *ssl_io,
+ struct ssl_iostream_context *ctx);
+
+bool ssl_iostream_is_handshaked(const struct ssl_iostream *ssl_io);
+/* Returns TRUE if the remote cert is invalid, or handshake callback returned
+ failure. */
+bool ssl_iostream_has_handshake_failed(const struct ssl_iostream *ssl_io);
+bool ssl_iostream_has_valid_client_cert(const struct ssl_iostream *ssl_io);
+bool ssl_iostream_has_broken_client_cert(struct ssl_iostream *ssl_io);
+/* Checks certificate validity based, also performs name checking. Called by
+ default in handshake, unless handshake callback is set with
+ ssl_iostream_check_cert_validity().
+
+ Host should be set as the name you want to validate the certificate name(s)
+ against. Usually this is the host name you connected to.
+
+ This function is same as calling ssl_iostream_has_valid_client_cert()
+ and ssl_iostream_cert_match_name().
+ */
+int ssl_iostream_check_cert_validity(struct ssl_iostream *ssl_io,
+ const char *host, const char **error_r);
+/* Returns TRUE if the given name matches the SSL stream's certificate.
+ The returned reason is a human-readable string explaining what exactly
+ matched the name, or why nothing matched. Note that this function works
+ only if the certificate was valid - using it when certificate is invalid
+ will always return FALSE before even checking the hostname. */
+bool ssl_iostream_cert_match_name(struct ssl_iostream *ssl_io, const char *name,
+ const char **reason_r);
+const char *ssl_iostream_get_peer_name(struct ssl_iostream *ssl_io);
+const char *ssl_iostream_get_compression(struct ssl_iostream *ssl_io);
+const char *ssl_iostream_get_server_name(struct ssl_iostream *ssl_io);
+const char *ssl_iostream_get_security_string(struct ssl_iostream *ssl_io);
+/* Returns SSL context's current used cipher algorithm. Returns NULL
+ if SSL handshake has not been performed.
+
+ This returns values like 'AESGCM'
+*/
+const char *ssl_iostream_get_cipher(struct ssl_iostream *ssl_io,
+ unsigned int *bits_r);
+/* Returns currently used forward secrecy algorithm, if available.
+ Returns NULL if handshake not done yet, empty string if missing.
+
+ This returns values like 'DH', 'ECDH' etc..
+*/
+const char *ssl_iostream_get_pfs(struct ssl_iostream *ssl_io);
+/* Returns currently used SSL protocol name. Returns NULL if handshake
+ has not yet been made.
+
+ This returns values like SSLv3, TLSv1, TLSv1.1, TLSv1.2
+*/
+const char *ssl_iostream_get_protocol_name(struct ssl_iostream *ssl_io);
+
+const char *ssl_iostream_get_last_error(struct ssl_iostream *ssl_io);
+
+int ssl_iostream_context_init_client(const struct ssl_iostream_settings *set,
+ struct ssl_iostream_context **ctx_r,
+ const char **error_r);
+int ssl_iostream_context_init_server(const struct ssl_iostream_settings *set,
+ struct ssl_iostream_context **ctx_r,
+ const char **error_r);
+void ssl_iostream_context_ref(struct ssl_iostream_context *ctx);
+void ssl_iostream_context_unref(struct ssl_iostream_context **ctx);
+
+struct ssl_iostream_settings *ssl_iostream_settings_dup(pool_t pool,
+ const struct ssl_iostream_settings *old_set);
+void ssl_iostream_settings_init_from(pool_t pool,
+ struct ssl_iostream_settings *dest,
+ const struct ssl_iostream_settings *src);
+
+/* Persistent cache of ssl_iostream_contexts. The context is permanently stored
+ until ssl_iostream_context_cache_free() is called. The returned context
+ must be unreferenced by the caller. */
+int ssl_iostream_client_context_cache_get(const struct ssl_iostream_settings *set,
+ struct ssl_iostream_context **ctx_r,
+ const char **error_r);
+int ssl_iostream_server_context_cache_get(const struct ssl_iostream_settings *set,
+ struct ssl_iostream_context **ctx_r,
+ const char **error_r);
+void ssl_iostream_context_cache_free(void);
+
+#endif
diff --git a/src/lib-ssl-iostream/istream-openssl.c b/src/lib-ssl-iostream/istream-openssl.c
new file mode 100644
index 0000000..f0446d9
--- /dev/null
+++ b/src/lib-ssl-iostream/istream-openssl.c
@@ -0,0 +1,130 @@
+/* Copyright (c) 2009-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "istream-private.h"
+#include "iostream-openssl.h"
+
+struct ssl_istream {
+ struct istream_private istream;
+ struct ssl_iostream *ssl_io;
+ bool seen_eof;
+};
+
+static void i_stream_ssl_close(struct iostream_private *stream,
+ bool close_parent)
+{
+ struct ssl_istream *sstream = (struct ssl_istream *)stream;
+
+ if (close_parent)
+ i_stream_close(sstream->ssl_io->plain_input);
+}
+
+static void i_stream_ssl_destroy(struct iostream_private *stream)
+{
+ struct ssl_istream *sstream = (struct ssl_istream *)stream;
+
+ openssl_iostream_shutdown(sstream->ssl_io);
+ i_stream_free_buffer(&sstream->istream);
+ sstream->ssl_io->ssl_input = NULL;
+ ssl_iostream_unref(&sstream->ssl_io);
+}
+
+static ssize_t i_stream_ssl_read(struct istream_private *stream)
+{
+ struct ssl_istream *sstream = (struct ssl_istream *)stream;
+ struct ssl_iostream *ssl_io = sstream->ssl_io;
+ size_t size;
+ ssize_t ret, total_ret;
+
+ if (sstream->seen_eof) {
+ stream->istream.eof = TRUE;
+ return -1;
+ }
+
+ if (!ssl_io->handshaked) {
+ if ((ret = ssl_iostream_handshake(ssl_io)) <= 0) {
+ if (ret < 0) {
+ /* handshake failed */
+ i_assert(errno != 0);
+ io_stream_set_error(&stream->iostream,
+ "%s", ssl_io->last_error);
+ stream->istream.stream_errno = errno;
+ }
+ return ret;
+ }
+ }
+ if (openssl_iostream_bio_sync(ssl_io,
+ OPENSSL_IOSTREAM_SYNC_TYPE_HANDSHAKE) < 0) {
+ i_assert(ssl_io->plain_stream_errno != 0 &&
+ ssl_io->plain_stream_errstr != NULL);
+ io_stream_set_error(&stream->iostream,
+ "%s", ssl_io->plain_stream_errstr);
+ stream->istream.stream_errno = ssl_io->plain_stream_errno;
+ return -1;
+ }
+
+ total_ret = 0;
+ for (;;) {
+ int pending = SSL_pending(ssl_io->ssl);
+
+ /* Allocate buffer space if needed. */
+ i_assert(stream->buffer_size >= stream->pos);
+ size = stream->buffer_size - stream->pos;
+ if ((pending > 0 || size == 0) &&
+ !i_stream_try_alloc(stream, I_MAX(pending, 1), &size)) {
+ if (total_ret > 0)
+ break;
+ return -2;
+ }
+
+ ret = SSL_read(ssl_io->ssl, stream->w_buffer + stream->pos, size);
+ if (ret <= 0) {
+ /* failed to read anything */
+ ret = openssl_iostream_handle_error(ssl_io, ret,
+ (total_ret == 0 ?
+ OPENSSL_IOSTREAM_SYNC_TYPE_CONTINUE_READ :
+ OPENSSL_IOSTREAM_SYNC_TYPE_NONE), "SSL_read");
+ if (ret <= 0) {
+ if (ret == 0)
+ break;
+ if (ssl_io->last_error != NULL) {
+ io_stream_set_error(&stream->iostream,
+ "%s", ssl_io->last_error);
+ }
+ if (errno != EPIPE)
+ stream->istream.stream_errno = errno;
+ stream->istream.eof = TRUE;
+ sstream->seen_eof = TRUE;
+ if (total_ret > 0)
+ break;
+ return -1;
+ }
+ /* we did some BIO I/O, try reading again */
+ continue;
+ }
+ stream->pos += ret;
+ total_ret += ret;
+ }
+ if (SSL_pending(ssl_io->ssl) > 0)
+ i_stream_set_input_pending(ssl_io->ssl_input, TRUE);
+ return total_ret;
+}
+
+struct istream *openssl_i_stream_create_ssl(struct ssl_iostream *ssl_io)
+{
+ struct ssl_istream *sstream;
+
+ ssl_io->refcount++;
+
+ sstream = i_new(struct ssl_istream, 1);
+ sstream->ssl_io = ssl_io;
+ sstream->istream.iostream.close = i_stream_ssl_close;
+ sstream->istream.iostream.destroy = i_stream_ssl_destroy;
+ sstream->istream.max_buffer_size =
+ ssl_io->plain_input->real_stream->max_buffer_size;
+ sstream->istream.read = i_stream_ssl_read;
+
+ sstream->istream.istream.readable_fd = FALSE;
+ return i_stream_create(&sstream->istream, NULL,
+ i_stream_get_fd(ssl_io->plain_input), 0);
+}
diff --git a/src/lib-ssl-iostream/ostream-openssl.c b/src/lib-ssl-iostream/ostream-openssl.c
new file mode 100644
index 0000000..8434a9b
--- /dev/null
+++ b/src/lib-ssl-iostream/ostream-openssl.c
@@ -0,0 +1,339 @@
+/* Copyright (c) 2009-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "buffer.h"
+#include "istream.h"
+#include "ostream-private.h"
+#include "iostream-openssl.h"
+
+struct ssl_ostream {
+ struct ostream_private ostream;
+ struct ssl_iostream *ssl_io;
+ buffer_t *buffer;
+
+ bool shutdown:1;
+};
+
+static void
+o_stream_ssl_close(struct iostream_private *stream, bool close_parent)
+{
+ struct ssl_ostream *sstream = (struct ssl_ostream *)stream;
+
+ if (close_parent)
+ o_stream_close(sstream->ssl_io->plain_output);
+}
+
+static void o_stream_ssl_destroy(struct iostream_private *stream)
+{
+ struct ssl_ostream *sstream = (struct ssl_ostream *)stream;
+ struct istream *ssl_input = sstream->ssl_io->ssl_input;
+
+ openssl_iostream_shutdown(sstream->ssl_io);
+ sstream->ssl_io->ssl_output = NULL;
+ i_stream_unref(&ssl_input);
+ ssl_iostream_unref(&sstream->ssl_io);
+ buffer_free(&sstream->buffer);
+}
+
+static size_t get_buffer_avail_size(const struct ssl_ostream *sstream)
+{
+ if (sstream->ostream.max_buffer_size == 0) {
+ if (sstream->buffer == NULL)
+ return 0;
+ /* we're requested to use whatever space is available in
+ the buffer */
+ return buffer_get_writable_size(sstream->buffer) - sstream->buffer->used;
+ } else {
+ size_t buffer_used = (sstream->buffer == NULL ? 0 :
+ sstream->buffer->used);
+ return sstream->ostream.max_buffer_size > buffer_used ?
+ sstream->ostream.max_buffer_size - buffer_used : 0;
+ }
+}
+
+static size_t
+o_stream_ssl_buffer(struct ssl_ostream *sstream, const struct const_iovec *iov,
+ unsigned int iov_count, size_t bytes_sent)
+{
+ size_t avail, skip_left, size;
+ unsigned int i;
+
+ if (sstream->buffer == NULL)
+ sstream->buffer = buffer_create_dynamic(default_pool,
+ I_MIN(IO_BLOCK_SIZE, sstream->ostream.max_buffer_size));
+
+ skip_left = bytes_sent;
+ for (i = 0; i < iov_count; i++) {
+ if (skip_left < iov[i].iov_len)
+ break;
+ skip_left -= iov[i].iov_len;
+ }
+
+ avail = get_buffer_avail_size(sstream);
+ if (i < iov_count && skip_left > 0) {
+ size = I_MIN(iov[i].iov_len - skip_left, avail);
+ buffer_append(sstream->buffer,
+ CONST_PTR_OFFSET(iov[i].iov_base, skip_left),
+ size);
+ bytes_sent += size;
+ avail -= size;
+ if (size != iov[i].iov_len)
+ i = iov_count;
+ }
+ if (avail > 0)
+ o_stream_set_flush_pending(sstream->ssl_io->plain_output, TRUE);
+
+ for (; i < iov_count; i++) {
+ size = I_MIN(iov[i].iov_len, avail);
+ buffer_append(sstream->buffer, iov[i].iov_base, size);
+ bytes_sent += size;
+ avail -= size;
+
+ if (size != iov[i].iov_len)
+ break;
+ }
+
+ sstream->ostream.ostream.offset += bytes_sent;
+ return bytes_sent;
+}
+
+static int o_stream_ssl_flush_buffer(struct ssl_ostream *sstream)
+{
+ struct ssl_iostream *ssl_io = sstream->ssl_io;
+ size_t pos = 0;
+ int ret = 1;
+
+ i_assert(!sstream->shutdown);
+
+ while (pos < sstream->buffer->used) {
+ /* we're writing plaintext data to OpenSSL, which it encrypts
+ and writes to bio_int's buffer. ssl_iostream_bio_sync()
+ reads it from there and adds to plain_output stream. */
+ ret = SSL_write(ssl_io->ssl,
+ CONST_PTR_OFFSET(sstream->buffer->data, pos),
+ sstream->buffer->used - pos);
+ if (ret <= 0) {
+ ret = openssl_iostream_handle_error(
+ ssl_io, ret, OPENSSL_IOSTREAM_SYNC_TYPE_WRITE,
+ "SSL_write");
+ if (ret < 0) {
+ io_stream_set_error(
+ &sstream->ostream.iostream,
+ "%s", ssl_io->last_error);
+ sstream->ostream.ostream.stream_errno = errno;
+ break;
+ }
+ if (ret == 0)
+ break;
+ } else {
+ pos += ret;
+ ret = openssl_iostream_bio_sync(
+ ssl_io, OPENSSL_IOSTREAM_SYNC_TYPE_WRITE);
+ if (ret < 0) {
+ i_assert(ssl_io->plain_stream_errstr != NULL &&
+ ssl_io->plain_stream_errno != 0);
+ io_stream_set_error(
+ &sstream->ostream.iostream,
+ "%s", ssl_io->plain_stream_errstr);
+ sstream->ostream.ostream.stream_errno =
+ ssl_io->plain_stream_errno;
+ break;
+ }
+ }
+ }
+ buffer_delete(sstream->buffer, 0, pos);
+ return ret <= 0 ? ret : 1;
+}
+
+static int o_stream_ssl_flush(struct ostream_private *stream)
+{
+ struct ssl_ostream *sstream = (struct ssl_ostream *)stream;
+ struct ssl_iostream *ssl_io = sstream->ssl_io;
+ struct ostream *plain_output = ssl_io->plain_output;
+ int ret = 1;
+
+ if (!ssl_io->handshaked) {
+ if ((ret = ssl_iostream_handshake(ssl_io)) < 0) {
+ /* handshake failed */
+ i_assert(errno != 0);
+ io_stream_set_error(&stream->iostream,
+ "%s", ssl_io->last_error);
+ stream->ostream.stream_errno = errno;
+ return ret;
+ }
+ }
+ if (ret > 0 &&
+ openssl_iostream_bio_sync(
+ ssl_io, OPENSSL_IOSTREAM_SYNC_TYPE_HANDSHAKE) < 0) {
+ i_assert(ssl_io->plain_stream_errno != 0 &&
+ ssl_io->plain_stream_errstr != NULL);
+ io_stream_set_error(&stream->iostream,
+ "%s", ssl_io->plain_stream_errstr);
+ stream->ostream.stream_errno = ssl_io->plain_stream_errno;
+ return -1;
+ }
+
+ if (ret > 0 && sstream->buffer != NULL && sstream->buffer->used > 0) {
+ /* we can try to send some of our buffered data */
+ ret = o_stream_ssl_flush_buffer(sstream);
+ }
+
+ /* Stream is finished; shutdown the SSL write direction once our buffer
+ is empty. */
+ if (stream->finished && !sstream->shutdown && ret >= 0 &&
+ (sstream->buffer == NULL || sstream->buffer->used == 0)) {
+ sstream->shutdown = TRUE;
+ if (SSL_shutdown(ssl_io->ssl) < 0) {
+ io_stream_set_error(
+ &sstream->ostream.iostream, "%s",
+ t_strdup_printf("SSL_shutdown() failed: %s",
+ openssl_iostream_error()));
+ sstream->ostream.ostream.stream_errno = EIO;
+ ret = -1;
+ }
+ }
+
+ if (ret == 0 && ssl_io->want_read) {
+ /* we need to read more data until we can continue. */
+ o_stream_set_flush_pending(plain_output, FALSE);
+ ssl_io->ostream_flush_waiting_input = TRUE;
+ ret = 1;
+ }
+
+ if (ret <= 0)
+ return ret;
+
+ /* return 1 only when the output buffer is empty, which is what the
+ caller expects. */
+ return o_stream_get_buffer_used_size(plain_output) == 0 ? 1 : 0;
+}
+
+static ssize_t
+o_stream_ssl_sendv(struct ostream_private *stream,
+ const struct const_iovec *iov, unsigned int iov_count)
+{
+ struct ssl_ostream *sstream = (struct ssl_ostream *)stream;
+ size_t bytes_sent = 0;
+
+ i_assert(!sstream->shutdown);
+
+ bytes_sent = o_stream_ssl_buffer(sstream, iov, iov_count, bytes_sent);
+ if (sstream->ssl_io->handshaked &&
+ sstream->buffer->used == bytes_sent) {
+ /* buffer was empty before calling this. try to write it
+ immediately. */
+ if (o_stream_ssl_flush_buffer(sstream) < 0)
+ return -1;
+ }
+ return bytes_sent;
+}
+
+static void o_stream_ssl_switch_ioloop_to(struct ostream_private *stream,
+ struct ioloop *ioloop)
+{
+ struct ssl_ostream *sstream = (struct ssl_ostream *)stream;
+
+ o_stream_switch_ioloop_to(sstream->ssl_io->plain_output, ioloop);
+}
+
+static int plain_flush_callback(struct ssl_ostream *sstream)
+{
+ struct ostream *ostream = &sstream->ostream.ostream;
+ int ret, ret2;
+
+ /* try to actually flush the pending data */
+ if ((ret = o_stream_flush(sstream->ssl_io->plain_output)) < 0)
+ return -1;
+
+ /* we may be able to copy more data, try it */
+ o_stream_ref(ostream);
+ if (sstream->ostream.callback != NULL)
+ ret2 = sstream->ostream.callback(sstream->ostream.context);
+ else
+ ret2 = o_stream_flush(&sstream->ostream.ostream);
+ if (ret2 == 0)
+ o_stream_set_flush_pending(sstream->ssl_io->plain_output, TRUE);
+ o_stream_unref(&ostream);
+ if (ret2 < 0)
+ return -1;
+ return ret > 0 && ret2 > 0 ? 1 : 0;
+}
+
+static size_t
+o_stream_ssl_get_buffer_used_size(const struct ostream_private *stream)
+{
+ const struct ssl_ostream *sstream = (const struct ssl_ostream *)stream;
+ BIO *bio = SSL_get_wbio(sstream->ssl_io->ssl);
+ size_t wbuf_avail = BIO_ctrl_get_write_guarantee(bio);
+ size_t wbuf_total_size = BIO_get_write_buf_size(bio, 0);
+ size_t buffer_used = (sstream->buffer == NULL ? 0 :
+ sstream->buffer->used);
+ i_assert(wbuf_avail <= wbuf_total_size);
+ return buffer_used + (wbuf_total_size - wbuf_avail) +
+ o_stream_get_buffer_used_size(sstream->ssl_io->plain_output);
+}
+
+static size_t
+o_stream_ssl_get_buffer_avail_size(const struct ostream_private *stream)
+{
+ const struct ssl_ostream *sstream = (const struct ssl_ostream *)stream;
+
+ return get_buffer_avail_size(sstream);
+}
+
+static void
+o_stream_ssl_flush_pending(struct ostream_private *_stream, bool set)
+{
+ struct ssl_ostream *sstream = (struct ssl_ostream *)_stream;
+
+ o_stream_set_flush_pending(sstream->ssl_io->plain_output, set);
+}
+
+static void o_stream_ssl_set_max_buffer_size(struct iostream_private *_stream,
+ size_t max_size)
+{
+ struct ssl_ostream *sstream = (struct ssl_ostream *)_stream;
+
+ sstream->ostream.max_buffer_size = max_size;
+ o_stream_set_max_buffer_size(sstream->ssl_io->plain_output, max_size);
+}
+
+struct ostream *openssl_o_stream_create_ssl(struct ssl_iostream *ssl_io)
+{
+ struct ssl_ostream *sstream;
+
+ ssl_io->refcount++;
+
+ /* When ostream is destroyed, it's flushed. With iostream-ssl the
+ flushing requires both istream and ostream to be available. The
+ istream is referenced here to make sure it's not destroyed before
+ the ostream. */
+ i_assert(ssl_io->ssl_input != NULL);
+ i_stream_ref(ssl_io->ssl_input);
+
+ sstream = i_new(struct ssl_ostream, 1);
+ sstream->ssl_io = ssl_io;
+ sstream->ostream.max_buffer_size =
+ ssl_io->plain_output->real_stream->max_buffer_size;
+ sstream->ostream.iostream.close = o_stream_ssl_close;
+ sstream->ostream.iostream.destroy = o_stream_ssl_destroy;
+ sstream->ostream.sendv = o_stream_ssl_sendv;
+ sstream->ostream.flush = o_stream_ssl_flush;
+ sstream->ostream.switch_ioloop_to = o_stream_ssl_switch_ioloop_to;
+
+ sstream->ostream.get_buffer_used_size =
+ o_stream_ssl_get_buffer_used_size;
+ sstream->ostream.get_buffer_avail_size =
+ o_stream_ssl_get_buffer_avail_size;
+ sstream->ostream.flush_pending = o_stream_ssl_flush_pending;
+ sstream->ostream.iostream.set_max_buffer_size =
+ o_stream_ssl_set_max_buffer_size;
+
+ sstream->ostream.callback = ssl_io->plain_output->real_stream->callback;
+ sstream->ostream.context = ssl_io->plain_output->real_stream->context;
+ o_stream_set_flush_callback(ssl_io->plain_output,
+ plain_flush_callback, sstream);
+
+ return o_stream_create(&sstream->ostream, NULL,
+ o_stream_get_fd(ssl_io->plain_output));
+}
diff --git a/src/lib-ssl-iostream/test-iostream-ssl.c b/src/lib-ssl-iostream/test-iostream-ssl.c
new file mode 100644
index 0000000..b2533ac
--- /dev/null
+++ b/src/lib-ssl-iostream/test-iostream-ssl.c
@@ -0,0 +1,559 @@
+/* Copyright (c) 2018 Dovecot authors, see the included COPYING file */
+
+#include "test-lib.h"
+#include "buffer.h"
+#include "randgen.h"
+#include "istream.h"
+#include "ostream.h"
+#include "iostream-openssl.h"
+#include "iostream-ssl.h"
+#include "iostream-ssl-test.h"
+
+#include <sys/socket.h>
+
+#define MAX_SENT_BYTES 10000
+
+struct test_endpoint {
+ pool_t pool;
+ int fd;
+ const char *hostname;
+ const struct ssl_iostream_settings *set;
+ struct ssl_iostream_context *ctx;
+ struct ssl_iostream *iostream;
+ struct istream *input;
+ struct ostream *output;
+ struct io *io;
+ buffer_t *last_write;
+ ssize_t sent;
+ bool client;
+ bool failed;
+
+ struct test_endpoint *other;
+
+ bool finished:1;
+};
+
+static void send_output(struct test_endpoint *ep)
+{
+ ssize_t amt = i_rand_limit(10)+1;
+ char data[amt];
+ random_fill(data, amt);
+ buffer_append(ep->other->last_write, data, amt);
+ test_assert(o_stream_send(ep->output, data, amt) == amt);
+ ep->sent += amt;
+}
+
+static int flush_output(struct test_endpoint *ep, bool finish)
+{
+ int ret = (finish ?
+ o_stream_finish(ep->output) : o_stream_flush(ep->output));
+ test_assert(ret >= 0);
+
+ if (ret > 0) {
+ if (finish)
+ ep->finished = TRUE;
+ if (ep->other->finished)
+ io_loop_stop(current_ioloop);
+ }
+ return ret;
+}
+
+static void handshake_input_callback(struct test_endpoint *ep)
+{
+ if (ep->failed)
+ return;
+ if (ssl_iostream_is_handshaked(ep->iostream)) {
+ if (ssl_iostream_is_handshaked(ep->other->iostream))
+ io_loop_stop(current_ioloop);
+ return;
+ }
+ if (ssl_iostream_handshake(ep->iostream) < 0) {
+ ep->failed = TRUE;
+ io_loop_stop(current_ioloop);
+ }
+}
+
+static int bufsize_flush_callback(struct test_endpoint *ep)
+{
+ io_loop_stop(current_ioloop);
+ return flush_output(ep, FALSE);
+}
+
+static int bufsize_finish_callback(struct test_endpoint *ep)
+{
+ return flush_output(ep, TRUE);
+}
+
+static void bufsize_input_callback(struct test_endpoint *ep)
+{
+ const unsigned char *data;
+ size_t size, wanted = i_rand_limit(512);
+
+ io_loop_stop(current_ioloop);
+ if (wanted == 0)
+ return;
+
+ test_assert(i_stream_read_bytes(ep->input, &data, &size, wanted) > -1);
+ i_stream_skip(ep->input, I_MIN(size, wanted));
+}
+
+static void bufsize_discard_callback(struct test_endpoint *ep)
+{
+ const unsigned char *data;
+ size_t size;
+
+ test_assert(i_stream_read_bytes(ep->input, &data, &size, 1) > -1 ||
+ ep->input->stream_errno == 0);
+ i_stream_skip(ep->input, size);
+}
+
+static int small_packets_flush_callback(struct test_endpoint *ep)
+{
+ return flush_output(ep, FALSE);
+}
+
+static void small_packets_input_callback(struct test_endpoint *ep)
+{
+ const unsigned char *data;
+ size_t size, wanted = i_rand_limit(10);
+ int ret;
+
+ if (wanted == 0) {
+ i_stream_set_input_pending(ep->input, TRUE);
+ return;
+ }
+
+ size = 0;
+ test_assert((ret = i_stream_read_bytes(ep->input, &data, &size, wanted)) > -1 ||
+ ep->input->stream_errno == 0);
+
+ if (size > wanted)
+ i_stream_set_input_pending(ep->input, TRUE);
+
+ size = I_MIN(size, wanted);
+
+ i_stream_skip(ep->input, size);
+ if (size > 0) {
+ test_assert(ep->last_write->used >= size);
+ if (ep->last_write->used >= size) {
+ test_assert(memcmp(ep->last_write->data, data, size) == 0);
+ /* remove the data that was wanted */
+ buffer_delete(ep->last_write, 0, size);
+ }
+ }
+
+ if (ep->sent > MAX_SENT_BYTES)
+ (void)flush_output(ep, TRUE);
+ else
+ send_output(ep);
+}
+
+static struct test_endpoint *
+create_test_endpoint(int fd, const struct ssl_iostream_settings *set)
+{
+ pool_t pool = pool_alloconly_create("ssl endpoint", 2048);
+ struct test_endpoint *ep = p_new(pool, struct test_endpoint, 1);
+ ep->pool = pool;
+ ep->fd = fd;
+ ep->input = i_stream_create_fd(ep->fd, 512);
+ ep->output = o_stream_create_fd(ep->fd, 1024);
+ o_stream_uncork(ep->output);
+ ep->set = ssl_iostream_settings_dup(pool, set);
+ ep->last_write = buffer_create_dynamic(pool, 1024);
+ return ep;
+}
+
+static void destroy_test_endpoint(struct test_endpoint **_ep)
+{
+ struct test_endpoint *ep = *_ep;
+ _ep = NULL;
+
+ io_remove(&ep->io);
+
+ i_stream_unref(&ep->input);
+ o_stream_unref(&ep->output);
+ ssl_iostream_destroy(&ep->iostream);
+ i_close_fd(&ep->fd);
+ if (ep->ctx != NULL)
+ ssl_iostream_context_unref(&ep->ctx);
+ pool_unref(&ep->pool);
+}
+
+static int test_iostream_ssl_handshake_real(struct ssl_iostream_settings *server_set,
+ struct ssl_iostream_settings *client_set,
+ const char *hostname)
+{
+ const char *error;
+ struct test_endpoint *server, *client;
+ int fd[2], ret = 0;
+
+ if (socketpair(AF_UNIX, SOCK_STREAM, 0, fd) < 0)
+ i_fatal("socketpair() failed: %m");
+ fd_set_nonblock(fd[0], TRUE);
+ fd_set_nonblock(fd[1], TRUE);
+
+ server = create_test_endpoint(fd[0], server_set);
+ client = create_test_endpoint(fd[1], client_set);
+ client->hostname = hostname;
+ client->client = TRUE;
+
+ server->other = client;
+ client->other = server;
+
+ if (ssl_iostream_context_init_server(server->set, &server->ctx,
+ &error) < 0) {
+ i_error("server: %s", error);
+ destroy_test_endpoint(&client);
+ destroy_test_endpoint(&server);
+ return -1;
+ }
+ if (ssl_iostream_context_init_client(client->set, &client->ctx,
+ &error) < 0) {
+ i_error("client: %s", error);
+ destroy_test_endpoint(&client);
+ destroy_test_endpoint(&server);
+ return -1;
+ }
+
+ if (io_stream_create_ssl_server(server->ctx, server->set,
+ &server->input, &server->output,
+ &server->iostream, &error) != 0) {
+ ret = -1;
+ }
+
+ if (io_stream_create_ssl_client(client->ctx, client->hostname, client->set,
+ &client->input, &client->output,
+ &client->iostream, &error) != 0) {
+ ret = -1;
+ }
+
+ client->io = io_add_istream(client->input, handshake_input_callback, client);
+ server->io = io_add_istream(server->input, handshake_input_callback, server);
+
+ if (ssl_iostream_handshake(client->iostream) < 0)
+ return -1;
+
+ io_loop_run(current_ioloop);
+
+ if (client->failed || server->failed)
+ ret = -1;
+
+ if (ssl_iostream_has_handshake_failed(client->iostream)) {
+ i_error("client: %s", ssl_iostream_get_last_error(client->iostream));
+ ret = -1;
+ } else if (ssl_iostream_has_handshake_failed(server->iostream)) {
+ i_error("server: %s", ssl_iostream_get_last_error(server->iostream));
+ ret = -1;
+ /* check hostname */
+ } else if (client->hostname != NULL &&
+ !client->set->allow_invalid_cert &&
+ ssl_iostream_check_cert_validity(client->iostream, client->hostname,
+ &error) != 0) {
+ i_error("client(%s): %s", client->hostname, error);
+ ret = -1;
+ /* client cert */
+ } else if (server->set->verify_remote_cert &&
+ ssl_iostream_check_cert_validity(server->iostream, NULL, &error) != 0) {
+ i_error("server: %s", error);
+ ret = -1;
+ }
+
+ i_stream_unref(&server->input);
+ o_stream_unref(&server->output);
+ i_stream_unref(&client->input);
+ o_stream_unref(&client->output);
+
+ destroy_test_endpoint(&client);
+ destroy_test_endpoint(&server);
+
+ return ret;
+}
+
+static void test_iostream_ssl_handshake(void)
+{
+ struct ssl_iostream_settings server_set, client_set;
+ struct ioloop *ioloop;
+ int idx = 0;
+
+ test_begin("ssl: handshake");
+
+ ioloop = io_loop_create();
+
+ /* allow invalid cert, connect to localhost */
+ ssl_iostream_test_settings_server(&server_set);
+ ssl_iostream_test_settings_client(&client_set);
+ client_set.allow_invalid_cert = TRUE;
+ test_assert_idx(test_iostream_ssl_handshake_real(&server_set, &client_set,
+ "localhost") == 0, idx);
+ idx++;
+
+ /* allow invalid cert, connect to failhost */
+ ssl_iostream_test_settings_server(&server_set);
+ ssl_iostream_test_settings_client(&client_set);
+ client_set.allow_invalid_cert = TRUE;
+ test_assert_idx(test_iostream_ssl_handshake_real(&server_set, &client_set,
+ "failhost") == 0, idx);
+ idx++;
+
+ /* verify remote cert */
+ ssl_iostream_test_settings_server(&server_set);
+ ssl_iostream_test_settings_client(&client_set);
+ client_set.verify_remote_cert = TRUE;
+ test_assert_idx(test_iostream_ssl_handshake_real(&server_set, &client_set,
+ "127.0.0.1") == 0, idx);
+ idx++;
+
+ /* verify remote cert, missing hostname */
+ ssl_iostream_test_settings_server(&server_set);
+ ssl_iostream_test_settings_client(&client_set);
+ client_set.verify_remote_cert = TRUE;
+ test_expect_error_string("client: SSL certificate doesn't "
+ "match expected host name failhost");
+ test_assert_idx(test_iostream_ssl_handshake_real(&server_set, &client_set,
+ "failhost") != 0, idx);
+ idx++;
+
+ /* verify remote cert, missing CA */
+ ssl_iostream_test_settings_server(&server_set);
+ ssl_iostream_test_settings_client(&client_set);
+ client_set.verify_remote_cert = TRUE;
+ client_set.ca = NULL;
+ test_expect_error_string("client: Received invalid SSL certificate");
+ test_assert_idx(test_iostream_ssl_handshake_real(&server_set, &client_set,
+ "127.0.0.1") != 0, idx);
+ idx++;
+
+ /* verify remote cert, require CRL */
+ ssl_iostream_test_settings_server(&server_set);
+ ssl_iostream_test_settings_client(&client_set);
+ client_set.verify_remote_cert = TRUE;
+ client_set.skip_crl_check = FALSE;
+ test_expect_error_string("client: Received invalid SSL certificate");
+ test_assert_idx(test_iostream_ssl_handshake_real(&server_set, &client_set,
+ "127.0.0.1") != 0, idx);
+ idx++;
+
+ /* missing server credentials */
+ ssl_iostream_test_settings_server(&server_set);
+ server_set.cert.key = NULL;
+ ssl_iostream_test_settings_client(&client_set);
+ client_set.verify_remote_cert = TRUE;
+ test_expect_error_string("client(failhost): SSL certificate not received");
+ test_assert_idx(test_iostream_ssl_handshake_real(&server_set, &client_set,
+ "failhost") != 0, idx);
+ idx++;
+ ssl_iostream_test_settings_server(&server_set);
+ server_set.cert.cert = NULL;
+ ssl_iostream_test_settings_client(&client_set);
+ client_set.verify_remote_cert = TRUE;
+ test_expect_error_string("client(failhost): SSL certificate not received");
+ test_assert_idx(test_iostream_ssl_handshake_real(&server_set, &client_set,
+ "failhost") != 0, idx);
+ idx++;
+
+ /* invalid client credentials: missing credentials */
+ ssl_iostream_test_settings_server(&server_set);
+ ssl_iostream_test_settings_client(&client_set);
+ client_set.verify_remote_cert = TRUE;
+ server_set.verify_remote_cert = TRUE;
+ server_set.ca = client_set.ca;
+ test_expect_error_string("server: SSL certificate not received");
+ test_assert_idx(test_iostream_ssl_handshake_real(&server_set, &client_set,
+ "127.0.0.1") != 0, idx);
+ idx++;
+
+ /* invalid client credentials: incorrect extended usage */
+ ssl_iostream_test_settings_server(&server_set);
+ ssl_iostream_test_settings_client(&client_set);
+ client_set.verify_remote_cert = TRUE;
+ server_set.verify_remote_cert = TRUE;
+ server_set.ca = client_set.ca;
+ client_set.cert = server_set.cert;
+ test_expect_error_string("server: SSL_accept() failed: error:");
+ test_assert_idx(test_iostream_ssl_handshake_real(&server_set, &client_set,
+ "127.0.0.1") != 0, idx);
+ idx++;
+
+ io_loop_destroy(&ioloop);
+
+ test_end();
+}
+
+static void test_iostream_ssl_get_buffer_avail_size(void)
+{
+ struct ssl_iostream_settings set;
+ struct test_endpoint *server, *client;
+ struct ioloop *ioloop;
+ int fd[2];
+ const char *error;
+
+ test_begin("ssl: o_stream_get_buffer_avail_size");
+
+ if (socketpair(AF_UNIX, SOCK_STREAM, 0, fd) < 0)
+ i_fatal("socketpair() failed: %m");
+ fd_set_nonblock(fd[0], TRUE);
+ fd_set_nonblock(fd[1], TRUE);
+
+ ioloop = io_loop_create();
+
+ ssl_iostream_test_settings_server(&set);
+ server = create_test_endpoint(fd[0], &set);
+ ssl_iostream_test_settings_client(&set);
+ set.allow_invalid_cert = TRUE;
+ client = create_test_endpoint(fd[1], &set);
+ client->client = TRUE;
+
+ client->other = server;
+ server->other = client;
+
+ test_assert(ssl_iostream_context_init_server(server->set, &server->ctx,
+ &error) == 0);
+ test_assert(ssl_iostream_context_init_client(client->set, &client->ctx,
+ &error) == 0);
+
+ test_assert(io_stream_create_ssl_server(server->ctx, server->set,
+ &server->input, &server->output,
+ &server->iostream, &error) == 0);
+ test_assert(io_stream_create_ssl_client(client->ctx, "localhost", client->set,
+ &client->input, &client->output,
+ &client->iostream, &error) == 0);
+
+ o_stream_set_flush_callback(server->output, bufsize_flush_callback, server);
+ o_stream_set_flush_callback(client->output, bufsize_flush_callback, client);
+
+ server->io = io_add_istream(server->input, bufsize_input_callback, server);
+ client->io = io_add_istream(client->input, bufsize_input_callback, client);
+
+ test_assert(ssl_iostream_handshake(client->iostream) == 0);
+ test_assert(ssl_iostream_handshake(server->iostream) == 0);
+
+ for (unsigned int i = 0; i < 100000 && !test_has_failed(); i++) {
+ size_t avail = o_stream_get_buffer_avail_size(server->output);
+ if (avail > 0) {
+ void *buf = i_malloc(avail);
+ random_fill(buf, avail);
+ test_assert(o_stream_send(server->output, buf, avail) ==
+ (ssize_t)avail);
+ i_free(buf);
+ }
+ avail = o_stream_get_buffer_avail_size(client->output);
+ if (avail > 0) {
+ void *buf = i_malloc(avail);
+ random_fill(buf, avail);
+ test_assert(o_stream_send(client->output, buf, avail) ==
+ (ssize_t)avail);
+ i_free(buf);
+ }
+ io_loop_run(ioloop);
+ }
+
+ io_remove(&server->io);
+ io_remove(&client->io);
+ o_stream_set_flush_callback(server->output, bufsize_finish_callback, server);
+ o_stream_set_flush_callback(client->output, bufsize_finish_callback, client);
+ server->io = io_add_istream(server->input, bufsize_discard_callback, server);
+ client->io = io_add_istream(client->input, bufsize_discard_callback, client);
+ o_stream_set_flush_pending(server->output, TRUE);
+ o_stream_set_flush_pending(client->output, TRUE);
+ io_loop_run(ioloop);
+
+ i_stream_unref(&server->input);
+ o_stream_unref(&server->output);
+ i_stream_unref(&client->input);
+ o_stream_unref(&client->output);
+
+ destroy_test_endpoint(&client);
+ destroy_test_endpoint(&server);
+
+ io_loop_destroy(&ioloop);
+
+ test_end();
+}
+
+static void test_iostream_ssl_small_packets(void)
+{
+ struct ssl_iostream_settings set;
+ struct test_endpoint *server, *client;
+ struct ioloop *ioloop;
+ int fd[2];
+ const char *error;
+
+ test_begin("ssl: small packets");
+
+ if (socketpair(AF_UNIX, SOCK_STREAM, 0, fd) < 0)
+ i_fatal("socketpair() failed: %m");
+ fd_set_nonblock(fd[0], TRUE);
+ fd_set_nonblock(fd[1], TRUE);
+
+ ioloop = io_loop_create();
+
+ ssl_iostream_test_settings_server(&set);
+ server = create_test_endpoint(fd[0], &set);
+ ssl_iostream_test_settings_client(&set);
+ set.allow_invalid_cert = TRUE;
+ client = create_test_endpoint(fd[1], &set);
+ client->client = TRUE;
+
+ test_assert(ssl_iostream_context_init_server(server->set, &server->ctx,
+ &error) == 0);
+ test_assert(ssl_iostream_context_init_client(client->set, &client->ctx,
+ &error) == 0);
+
+ client->other = server;
+ server->other = client;
+
+ test_assert(io_stream_create_ssl_server(server->ctx, server->set,
+ &server->input, &server->output,
+ &server->iostream, &error) == 0);
+ test_assert(io_stream_create_ssl_client(client->ctx, "localhost", client->set,
+ &client->input, &client->output,
+ &client->iostream, &error) == 0);
+
+ o_stream_set_flush_callback(server->output, small_packets_flush_callback,
+ server);
+ o_stream_set_flush_callback(client->output, small_packets_flush_callback,
+ client);
+
+ server->io = io_add_istream(server->input, small_packets_input_callback,
+ server);
+ client->io = io_add_istream(client->input, small_packets_input_callback,
+ client);
+
+ test_assert(ssl_iostream_handshake(client->iostream) == 0);
+ test_assert(ssl_iostream_handshake(server->iostream) == 0);
+
+ struct timeout *to = timeout_add(5000, io_loop_stop, ioloop);
+
+ io_loop_run(ioloop);
+
+ timeout_remove(&to);
+
+ test_assert(server->sent > MAX_SENT_BYTES ||
+ client->sent > MAX_SENT_BYTES);
+
+ i_stream_unref(&server->input);
+ o_stream_unref(&server->output);
+ i_stream_unref(&client->input);
+ o_stream_unref(&client->output);
+
+ destroy_test_endpoint(&server);
+ destroy_test_endpoint(&client);
+
+ io_loop_destroy(&ioloop);
+
+ test_end();
+}
+
+int main(void)
+{
+ static void (*const test_functions[])(void) = {
+ test_iostream_ssl_handshake,
+ test_iostream_ssl_get_buffer_avail_size,
+ test_iostream_ssl_small_packets,
+ NULL
+ };
+ ssl_iostream_openssl_init();
+ int ret = test_run(test_functions);
+ ssl_iostream_openssl_deinit();
+ return ret;
+}
diff --git a/src/lib-storage/Makefile.am b/src/lib-storage/Makefile.am
new file mode 100644
index 0000000..ddf4c9a
--- /dev/null
+++ b/src/lib-storage/Makefile.am
@@ -0,0 +1,210 @@
+SUBDIRS = list index
+
+noinst_LTLIBRARIES = libstorage.la
+
+AM_CPPFLAGS = \
+ -I$(top_srcdir)/src/lib \
+ -I$(top_srcdir)/src/lib-test \
+ -I$(top_srcdir)/src/lib-auth \
+ -I$(top_srcdir)/src/lib-dict \
+ -I$(top_srcdir)/src/lib-sasl \
+ -I$(top_srcdir)/src/lib-ssl-iostream \
+ -I$(top_srcdir)/src/lib-fs \
+ -I$(top_srcdir)/src/lib-master \
+ -I$(top_srcdir)/src/lib-settings \
+ -I$(top_srcdir)/src/lib-charset \
+ -I$(top_srcdir)/src/lib-mail \
+ -I$(top_srcdir)/src/lib-smtp \
+ -I$(top_srcdir)/src/lib-imap \
+ -I$(top_srcdir)/src/lib-index \
+ -DPKG_RUNDIR=\""$(rundir)"\" \
+ -DMODULEDIR=\""$(moduledir)"\"
+
+libstorage_la_SOURCES = \
+ fail-mail-storage.c \
+ fail-mailbox.c \
+ fail-mail.c \
+ mail.c \
+ mail-autoexpunge.c \
+ mail-copy.c \
+ mail-duplicate.c \
+ mail-error.c \
+ mail-namespace.c \
+ mail-search.c \
+ mail-search-args-cmdline.c \
+ mail-search-args-imap.c \
+ mail-search-args-simplify.c \
+ mail-search-build.c \
+ mail-search-mime.c \
+ mail-search-mime-build.c \
+ mail-search-mime-register.c \
+ mail-search-parser.c \
+ mail-search-parser-imap.c \
+ mail-search-parser-cmdline.c \
+ mail-search-register.c \
+ mail-search-register-human.c \
+ mail-search-register-imap.c \
+ mail-storage.c \
+ mail-storage-hooks.c \
+ mail-storage-register.c \
+ mail-storage-service.c \
+ mail-storage-settings.c \
+ mail-thread.c \
+ mail-user.c \
+ mailbox-attribute.c \
+ mailbox-attribute-internal.c \
+ mailbox-get.c \
+ mailbox-guid-cache.c \
+ mailbox-header.c \
+ mailbox-keywords.c \
+ mailbox-list.c \
+ mailbox-list-notify.c \
+ mailbox-list-register.c \
+ mailbox-match-plugin.c \
+ mailbox-recent-flags.c \
+ mailbox-search-result.c \
+ mailbox-tree.c \
+ mailbox-uidvalidity.c \
+ mailbox-watch.c \
+ test-mail-storage-common.c
+
+headers = \
+ fail-mail-storage.h \
+ mail-autoexpunge.h \
+ mail-copy.h \
+ mail-duplicate.h \
+ mail-error.h \
+ mail-namespace.h \
+ mail-search.h \
+ mail-search-build.h \
+ mail-search-mime.h \
+ mail-search-mime-build.h \
+ mail-search-mime-register.h \
+ mail-search-register.h \
+ mail-thread.h \
+ mail-storage.h \
+ mail-search-parser.h \
+ mail-search-parser-private.h \
+ mail-storage-private.h \
+ mail-storage-hooks.h \
+ mail-storage-service.h \
+ mail-storage-settings.h \
+ mail-user.h \
+ mailbox-attribute.h \
+ mailbox-attribute-internal.h \
+ mailbox-attribute-private.h \
+ mailbox-guid-cache.h \
+ mailbox-list.h \
+ mailbox-list-iter.h \
+ mailbox-list-private.h \
+ mailbox-list-notify.h \
+ mailbox-match-plugin.h \
+ mailbox-recent-flags.h \
+ mailbox-search-result-private.h \
+ mailbox-tree.h \
+ mailbox-uidvalidity.h \
+ mailbox-watch.h \
+ test-mail-storage-common.h
+
+shlibs = \
+ index/shared/libstorage_shared.la \
+ index/dbox-multi/libstorage_dbox_multi.la \
+ index/dbox-single/libstorage_dbox_single.la \
+ index/dbox-common/libstorage_dbox_common.la \
+ index/maildir/libstorage_maildir.la \
+ index/mbox/libstorage_mbox.la \
+ index/imapc/libstorage_imapc.la \
+ ../lib-imap-client/libimap_client.la \
+ index/pop3c/libstorage_pop3c.la \
+ index/raw/libstorage_raw.la \
+ list/libstorage_list.la \
+ index/libstorage_index.la \
+ ../lib-index/libindex.la \
+ ../lib-imap-storage/libimap-storage.la
+
+libstorage_la_LIBADD = $(shlibs)
+libstorage_la_DEPENDENCIES = $(shlibs)
+
+pkglib_LTLIBRARIES = libdovecot-storage.la
+libdovecot_storage_la_SOURCES =
+libdovecot_storage_la_LIBADD = \
+ libstorage.la \
+ ../lib-dovecot/libdovecot.la \
+ $(LINKED_STORAGE_LDADD)
+libdovecot_storage_la_DEPENDENCIES = \
+ libstorage.la \
+ ../lib-dovecot/libdovecot.la \
+ $(LIBDOVECOT_DEPS)
+libdovecot_storage_la_LDFLAGS = -export-dynamic
+
+if HAVE_LUA
+pkglib_LTLIBRARIES += libdovecot-storage-lua.la
+libdovecot_storage_lua_la_SOURCES = \
+ mail-lua.c \
+ mail-user-lua.c \
+ mail-storage-lua.c \
+ mailbox-lua.c \
+ mailbox-attribute-lua.c
+libdovecot_storage_lua_la_CPPFLAGS = \
+ $(AM_CPPFLAGS) \
+ $(LUA_CFLAGS) \
+ -I$(top_srcdir)/src/lib-lua
+libdovecot_storage_lua_la_LIBADD = \
+ libdovecot-storage.la \
+ ../lib-lua/libdovecot-lua.la
+libdovecot_storage_lua_la_DEPENDENCIES = \
+ libdovecot-storage.la \
+ ../lib-lua/libdovecot-lua.la
+libdovecot_storage_lua_la_LDFLAGS = -export-dynamic
+
+headers += \
+ mail-storage-lua.h \
+ mail-storage-lua-private.h
+endif
+
+test_programs = \
+ test-mail-search-args-imap \
+ test-mail-search-args-simplify \
+ test-mail \
+ test-mail-storage \
+ test-mailbox-get \
+ test-mailbox-list
+
+noinst_PROGRAMS = $(test_programs)
+
+test_libs = \
+ $(top_builddir)/src/lib-test/libtest.la \
+ $(top_builddir)/src/lib/liblib.la
+
+test_mail_search_args_imap_SOURCES = test-mail-search-args-imap.c
+test_mail_search_args_imap_LDADD = libstorage.la $(LIBDOVECOT)
+test_mail_search_args_imap_DEPENDENCIES = libstorage.la $(LIBDOVECOT_DEPS)
+
+test_mail_search_args_simplify_SOURCES = test-mail-search-args-simplify.c
+test_mail_search_args_simplify_LDADD = libstorage.la $(LIBDOVECOT)
+test_mail_search_args_simplify_DEPENDENCIES = libstorage.la $(LIBDOVECOT_DEPS)
+
+test_mailbox_get_SOURCES = test-mailbox-get.c
+test_mailbox_get_LDADD = mailbox-get.lo $(test_libs)
+test_mailbox_get_DEPENDENCIES = $(noinst_LTLIBRARIES) $(test_libs)
+
+test_mail_SOURCES = test-mail.c
+test_mail_LDADD = libstorage.la $(LIBDOVECOT)
+test_mail_DEPENDENCIES = libstorage.la $(LIBDOVECOT_DEPS)
+
+test_mail_storage_SOURCES = test-mail-storage.c
+test_mail_storage_LDADD = libstorage.la $(LIBDOVECOT)
+test_mail_storage_DEPENDENCIES = libstorage.la $(LIBDOVECOT_DEPS)
+
+test_mailbox_list_SOURCES = test-mailbox-list.c
+test_mailbox_list_LDADD = libstorage.la $(LIBDOVECOT)
+test_mailbox_list_DEPENDENCIES = libstorage.la $(LIBDOVECOT_DEPS)
+
+check-local:
+ for bin in $(test_programs); do \
+ if ! $(RUN_TEST) ./$$bin; then exit 1; fi; \
+ done
+
+pkginc_libdir=$(pkgincludedir)
+pkginc_lib_HEADERS = $(headers)
+noinst_HEADERS = $(test_headers)
diff --git a/src/lib-storage/Makefile.in b/src/lib-storage/Makefile.in
new file mode 100644
index 0000000..8669715
--- /dev/null
+++ b/src/lib-storage/Makefile.in
@@ -0,0 +1,1509 @@
+# Makefile.in generated by automake 1.16.1 from Makefile.am.
+# @configure_input@
+
+# Copyright (C) 1994-2018 Free Software Foundation, Inc.
+
+# This Makefile.in is free software; the Free Software Foundation
+# gives unlimited permission to copy and/or distribute it,
+# with or without modifications, as long as this notice is preserved.
+
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY, to the extent permitted by law; without
+# even the implied warranty of MERCHANTABILITY or FITNESS FOR A
+# PARTICULAR PURPOSE.
+
+@SET_MAKE@
+
+
+
+VPATH = @srcdir@
+am__is_gnu_make = { \
+ if test -z '$(MAKELEVEL)'; then \
+ false; \
+ elif test -n '$(MAKE_HOST)'; then \
+ true; \
+ elif test -n '$(MAKE_VERSION)' && test -n '$(CURDIR)'; then \
+ true; \
+ else \
+ false; \
+ fi; \
+}
+am__make_running_with_option = \
+ case $${target_option-} in \
+ ?) ;; \
+ *) echo "am__make_running_with_option: internal error: invalid" \
+ "target option '$${target_option-}' specified" >&2; \
+ exit 1;; \
+ esac; \
+ has_opt=no; \
+ sane_makeflags=$$MAKEFLAGS; \
+ if $(am__is_gnu_make); then \
+ sane_makeflags=$$MFLAGS; \
+ else \
+ case $$MAKEFLAGS in \
+ *\\[\ \ ]*) \
+ bs=\\; \
+ sane_makeflags=`printf '%s\n' "$$MAKEFLAGS" \
+ | sed "s/$$bs$$bs[$$bs $$bs ]*//g"`;; \
+ esac; \
+ fi; \
+ skip_next=no; \
+ strip_trailopt () \
+ { \
+ flg=`printf '%s\n' "$$flg" | sed "s/$$1.*$$//"`; \
+ }; \
+ for flg in $$sane_makeflags; do \
+ test $$skip_next = yes && { skip_next=no; continue; }; \
+ case $$flg in \
+ *=*|--*) continue;; \
+ -*I) strip_trailopt 'I'; skip_next=yes;; \
+ -*I?*) strip_trailopt 'I';; \
+ -*O) strip_trailopt 'O'; skip_next=yes;; \
+ -*O?*) strip_trailopt 'O';; \
+ -*l) strip_trailopt 'l'; skip_next=yes;; \
+ -*l?*) strip_trailopt 'l';; \
+ -[dEDm]) skip_next=yes;; \
+ -[JT]) skip_next=yes;; \
+ esac; \
+ case $$flg in \
+ *$$target_option*) has_opt=yes; break;; \
+ esac; \
+ done; \
+ test $$has_opt = yes
+am__make_dryrun = (target_option=n; $(am__make_running_with_option))
+am__make_keepgoing = (target_option=k; $(am__make_running_with_option))
+pkgdatadir = $(datadir)/@PACKAGE@
+pkgincludedir = $(includedir)/@PACKAGE@
+pkglibdir = $(libdir)/@PACKAGE@
+pkglibexecdir = $(libexecdir)/@PACKAGE@
+am__cd = CDPATH="$${ZSH_VERSION+.}$(PATH_SEPARATOR)" && cd
+install_sh_DATA = $(install_sh) -c -m 644
+install_sh_PROGRAM = $(install_sh) -c
+install_sh_SCRIPT = $(install_sh) -c
+INSTALL_HEADER = $(INSTALL_DATA)
+transform = $(program_transform_name)
+NORMAL_INSTALL = :
+PRE_INSTALL = :
+POST_INSTALL = :
+NORMAL_UNINSTALL = :
+PRE_UNINSTALL = :
+POST_UNINSTALL = :
+build_triplet = @build@
+host_triplet = @host@
+@HAVE_LUA_TRUE@am__append_1 = libdovecot-storage-lua.la
+@HAVE_LUA_TRUE@am__append_2 = \
+@HAVE_LUA_TRUE@ mail-storage-lua.h \
+@HAVE_LUA_TRUE@ mail-storage-lua-private.h
+
+noinst_PROGRAMS = $(am__EXEEXT_1)
+subdir = src/lib-storage
+ACLOCAL_M4 = $(top_srcdir)/aclocal.m4
+am__aclocal_m4_deps = $(top_srcdir)/m4/ac_checktype2.m4 \
+ $(top_srcdir)/m4/ac_typeof.m4 $(top_srcdir)/m4/arc4random.m4 \
+ $(top_srcdir)/m4/blockdev.m4 $(top_srcdir)/m4/c99_vsnprintf.m4 \
+ $(top_srcdir)/m4/clock_gettime.m4 $(top_srcdir)/m4/crypt.m4 \
+ $(top_srcdir)/m4/crypt_xpg6.m4 $(top_srcdir)/m4/dbqlk.m4 \
+ $(top_srcdir)/m4/dirent_dtype.m4 $(top_srcdir)/m4/dovecot.m4 \
+ $(top_srcdir)/m4/fd_passing.m4 $(top_srcdir)/m4/fdatasync.m4 \
+ $(top_srcdir)/m4/flexible_array_member.m4 \
+ $(top_srcdir)/m4/glibc.m4 $(top_srcdir)/m4/gmtime_max.m4 \
+ $(top_srcdir)/m4/gmtime_tm_gmtoff.m4 \
+ $(top_srcdir)/m4/ioloop.m4 $(top_srcdir)/m4/iovec.m4 \
+ $(top_srcdir)/m4/ipv6.m4 $(top_srcdir)/m4/libcap.m4 \
+ $(top_srcdir)/m4/libtool.m4 $(top_srcdir)/m4/libwrap.m4 \
+ $(top_srcdir)/m4/linux_mremap.m4 $(top_srcdir)/m4/ltoptions.m4 \
+ $(top_srcdir)/m4/ltsugar.m4 $(top_srcdir)/m4/ltversion.m4 \
+ $(top_srcdir)/m4/lt~obsolete.m4 $(top_srcdir)/m4/mmap_write.m4 \
+ $(top_srcdir)/m4/mntctl.m4 $(top_srcdir)/m4/modules.m4 \
+ $(top_srcdir)/m4/notify.m4 $(top_srcdir)/m4/nsl.m4 \
+ $(top_srcdir)/m4/off_t_max.m4 $(top_srcdir)/m4/pkg.m4 \
+ $(top_srcdir)/m4/pr_set_dumpable.m4 \
+ $(top_srcdir)/m4/q_quotactl.m4 $(top_srcdir)/m4/quota.m4 \
+ $(top_srcdir)/m4/random.m4 $(top_srcdir)/m4/rlimit.m4 \
+ $(top_srcdir)/m4/sendfile.m4 $(top_srcdir)/m4/size_t_signed.m4 \
+ $(top_srcdir)/m4/sockpeercred.m4 $(top_srcdir)/m4/sql.m4 \
+ $(top_srcdir)/m4/ssl.m4 $(top_srcdir)/m4/st_tim.m4 \
+ $(top_srcdir)/m4/static_array.m4 $(top_srcdir)/m4/test_with.m4 \
+ $(top_srcdir)/m4/time_t.m4 $(top_srcdir)/m4/typeof.m4 \
+ $(top_srcdir)/m4/typeof_dev_t.m4 \
+ $(top_srcdir)/m4/uoff_t_max.m4 $(top_srcdir)/m4/vararg.m4 \
+ $(top_srcdir)/m4/want_apparmor.m4 \
+ $(top_srcdir)/m4/want_bsdauth.m4 \
+ $(top_srcdir)/m4/want_bzlib.m4 \
+ $(top_srcdir)/m4/want_cassandra.m4 \
+ $(top_srcdir)/m4/want_cdb.m4 \
+ $(top_srcdir)/m4/want_checkpassword.m4 \
+ $(top_srcdir)/m4/want_clucene.m4 $(top_srcdir)/m4/want_db.m4 \
+ $(top_srcdir)/m4/want_gssapi.m4 $(top_srcdir)/m4/want_icu.m4 \
+ $(top_srcdir)/m4/want_ldap.m4 $(top_srcdir)/m4/want_lua.m4 \
+ $(top_srcdir)/m4/want_lz4.m4 $(top_srcdir)/m4/want_lzma.m4 \
+ $(top_srcdir)/m4/want_mysql.m4 $(top_srcdir)/m4/want_pam.m4 \
+ $(top_srcdir)/m4/want_passwd.m4 $(top_srcdir)/m4/want_pgsql.m4 \
+ $(top_srcdir)/m4/want_prefetch.m4 \
+ $(top_srcdir)/m4/want_shadow.m4 \
+ $(top_srcdir)/m4/want_sodium.m4 $(top_srcdir)/m4/want_solr.m4 \
+ $(top_srcdir)/m4/want_sqlite.m4 \
+ $(top_srcdir)/m4/want_stemmer.m4 \
+ $(top_srcdir)/m4/want_systemd.m4 \
+ $(top_srcdir)/m4/want_textcat.m4 \
+ $(top_srcdir)/m4/want_unwind.m4 $(top_srcdir)/m4/want_zlib.m4 \
+ $(top_srcdir)/m4/want_zstd.m4 $(top_srcdir)/configure.ac
+am__configure_deps = $(am__aclocal_m4_deps) $(CONFIGURE_DEPENDENCIES) \
+ $(ACLOCAL_M4)
+DIST_COMMON = $(srcdir)/Makefile.am $(noinst_HEADERS) \
+ $(am__pkginc_lib_HEADERS_DIST) $(am__DIST_COMMON)
+mkinstalldirs = $(install_sh) -d
+CONFIG_HEADER = $(top_builddir)/config.h
+CONFIG_CLEAN_FILES =
+CONFIG_CLEAN_VPATH_FILES =
+am__EXEEXT_1 = test-mail-search-args-imap$(EXEEXT) \
+ test-mail-search-args-simplify$(EXEEXT) test-mail$(EXEEXT) \
+ test-mail-storage$(EXEEXT) test-mailbox-get$(EXEEXT) \
+ test-mailbox-list$(EXEEXT)
+PROGRAMS = $(noinst_PROGRAMS)
+am__vpath_adj_setup = srcdirstrip=`echo "$(srcdir)" | sed 's|.|.|g'`;
+am__vpath_adj = case $$p in \
+ $(srcdir)/*) f=`echo "$$p" | sed "s|^$$srcdirstrip/||"`;; \
+ *) f=$$p;; \
+ esac;
+am__strip_dir = f=`echo $$p | sed -e 's|^.*/||'`;
+am__install_max = 40
+am__nobase_strip_setup = \
+ srcdirstrip=`echo "$(srcdir)" | sed 's/[].[^$$\\*|]/\\\\&/g'`
+am__nobase_strip = \
+ for p in $$list; do echo "$$p"; done | sed -e "s|$$srcdirstrip/||"
+am__nobase_list = $(am__nobase_strip_setup); \
+ for p in $$list; do echo "$$p $$p"; done | \
+ sed "s| $$srcdirstrip/| |;"' / .*\//!s/ .*/ ./; s,\( .*\)/[^/]*$$,\1,' | \
+ $(AWK) 'BEGIN { files["."] = "" } { files[$$2] = files[$$2] " " $$1; \
+ if (++n[$$2] == $(am__install_max)) \
+ { print $$2, files[$$2]; n[$$2] = 0; files[$$2] = "" } } \
+ END { for (dir in files) print dir, files[dir] }'
+am__base_list = \
+ sed '$$!N;$$!N;$$!N;$$!N;$$!N;$$!N;$$!N;s/\n/ /g' | \
+ sed '$$!N;$$!N;$$!N;$$!N;s/\n/ /g'
+am__uninstall_files_from_dir = { \
+ test -z "$$files" \
+ || { test ! -d "$$dir" && test ! -f "$$dir" && test ! -r "$$dir"; } \
+ || { echo " ( cd '$$dir' && rm -f" $$files ")"; \
+ $(am__cd) "$$dir" && rm -f $$files; }; \
+ }
+am__installdirs = "$(DESTDIR)$(pkglibdir)" \
+ "$(DESTDIR)$(pkginc_libdir)"
+LTLIBRARIES = $(noinst_LTLIBRARIES) $(pkglib_LTLIBRARIES)
+am__libdovecot_storage_lua_la_SOURCES_DIST = mail-lua.c \
+ mail-user-lua.c mail-storage-lua.c mailbox-lua.c \
+ mailbox-attribute-lua.c
+@HAVE_LUA_TRUE@am_libdovecot_storage_lua_la_OBJECTS = \
+@HAVE_LUA_TRUE@ libdovecot_storage_lua_la-mail-lua.lo \
+@HAVE_LUA_TRUE@ libdovecot_storage_lua_la-mail-user-lua.lo \
+@HAVE_LUA_TRUE@ libdovecot_storage_lua_la-mail-storage-lua.lo \
+@HAVE_LUA_TRUE@ libdovecot_storage_lua_la-mailbox-lua.lo \
+@HAVE_LUA_TRUE@ libdovecot_storage_lua_la-mailbox-attribute-lua.lo
+libdovecot_storage_lua_la_OBJECTS = \
+ $(am_libdovecot_storage_lua_la_OBJECTS)
+AM_V_lt = $(am__v_lt_@AM_V@)
+am__v_lt_ = $(am__v_lt_@AM_DEFAULT_V@)
+am__v_lt_0 = --silent
+am__v_lt_1 =
+libdovecot_storage_lua_la_LINK = $(LIBTOOL) $(AM_V_lt) --tag=CC \
+ $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=link $(CCLD) \
+ $(AM_CFLAGS) $(CFLAGS) $(libdovecot_storage_lua_la_LDFLAGS) \
+ $(LDFLAGS) -o $@
+@HAVE_LUA_TRUE@am_libdovecot_storage_lua_la_rpath = -rpath \
+@HAVE_LUA_TRUE@ $(pkglibdir)
+am__DEPENDENCIES_1 =
+am_libdovecot_storage_la_OBJECTS =
+libdovecot_storage_la_OBJECTS = $(am_libdovecot_storage_la_OBJECTS)
+libdovecot_storage_la_LINK = $(LIBTOOL) $(AM_V_lt) --tag=CC \
+ $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=link $(CCLD) \
+ $(AM_CFLAGS) $(CFLAGS) $(libdovecot_storage_la_LDFLAGS) \
+ $(LDFLAGS) -o $@
+am_libstorage_la_OBJECTS = fail-mail-storage.lo fail-mailbox.lo \
+ fail-mail.lo mail.lo mail-autoexpunge.lo mail-copy.lo \
+ mail-duplicate.lo mail-error.lo mail-namespace.lo \
+ mail-search.lo mail-search-args-cmdline.lo \
+ mail-search-args-imap.lo mail-search-args-simplify.lo \
+ mail-search-build.lo mail-search-mime.lo \
+ mail-search-mime-build.lo mail-search-mime-register.lo \
+ mail-search-parser.lo mail-search-parser-imap.lo \
+ mail-search-parser-cmdline.lo mail-search-register.lo \
+ mail-search-register-human.lo mail-search-register-imap.lo \
+ mail-storage.lo mail-storage-hooks.lo mail-storage-register.lo \
+ mail-storage-service.lo mail-storage-settings.lo \
+ mail-thread.lo mail-user.lo mailbox-attribute.lo \
+ mailbox-attribute-internal.lo mailbox-get.lo \
+ mailbox-guid-cache.lo mailbox-header.lo mailbox-keywords.lo \
+ mailbox-list.lo mailbox-list-notify.lo \
+ mailbox-list-register.lo mailbox-match-plugin.lo \
+ mailbox-recent-flags.lo mailbox-search-result.lo \
+ mailbox-tree.lo mailbox-uidvalidity.lo mailbox-watch.lo \
+ test-mail-storage-common.lo
+libstorage_la_OBJECTS = $(am_libstorage_la_OBJECTS)
+am_test_mail_OBJECTS = test-mail.$(OBJEXT)
+test_mail_OBJECTS = $(am_test_mail_OBJECTS)
+am_test_mail_search_args_imap_OBJECTS = \
+ test-mail-search-args-imap.$(OBJEXT)
+test_mail_search_args_imap_OBJECTS = \
+ $(am_test_mail_search_args_imap_OBJECTS)
+am_test_mail_search_args_simplify_OBJECTS = \
+ test-mail-search-args-simplify.$(OBJEXT)
+test_mail_search_args_simplify_OBJECTS = \
+ $(am_test_mail_search_args_simplify_OBJECTS)
+am_test_mail_storage_OBJECTS = test-mail-storage.$(OBJEXT)
+test_mail_storage_OBJECTS = $(am_test_mail_storage_OBJECTS)
+am_test_mailbox_get_OBJECTS = test-mailbox-get.$(OBJEXT)
+test_mailbox_get_OBJECTS = $(am_test_mailbox_get_OBJECTS)
+am_test_mailbox_list_OBJECTS = test-mailbox-list.$(OBJEXT)
+test_mailbox_list_OBJECTS = $(am_test_mailbox_list_OBJECTS)
+AM_V_P = $(am__v_P_@AM_V@)
+am__v_P_ = $(am__v_P_@AM_DEFAULT_V@)
+am__v_P_0 = false
+am__v_P_1 = :
+AM_V_GEN = $(am__v_GEN_@AM_V@)
+am__v_GEN_ = $(am__v_GEN_@AM_DEFAULT_V@)
+am__v_GEN_0 = @echo " GEN " $@;
+am__v_GEN_1 =
+AM_V_at = $(am__v_at_@AM_V@)
+am__v_at_ = $(am__v_at_@AM_DEFAULT_V@)
+am__v_at_0 = @
+am__v_at_1 =
+DEFAULT_INCLUDES = -I.@am__isrc@ -I$(top_builddir)
+depcomp = $(SHELL) $(top_srcdir)/depcomp
+am__maybe_remake_depfiles = depfiles
+am__depfiles_remade = ./$(DEPDIR)/fail-mail-storage.Plo \
+ ./$(DEPDIR)/fail-mail.Plo ./$(DEPDIR)/fail-mailbox.Plo \
+ ./$(DEPDIR)/libdovecot_storage_lua_la-mail-lua.Plo \
+ ./$(DEPDIR)/libdovecot_storage_lua_la-mail-storage-lua.Plo \
+ ./$(DEPDIR)/libdovecot_storage_lua_la-mail-user-lua.Plo \
+ ./$(DEPDIR)/libdovecot_storage_lua_la-mailbox-attribute-lua.Plo \
+ ./$(DEPDIR)/libdovecot_storage_lua_la-mailbox-lua.Plo \
+ ./$(DEPDIR)/mail-autoexpunge.Plo ./$(DEPDIR)/mail-copy.Plo \
+ ./$(DEPDIR)/mail-duplicate.Plo ./$(DEPDIR)/mail-error.Plo \
+ ./$(DEPDIR)/mail-namespace.Plo \
+ ./$(DEPDIR)/mail-search-args-cmdline.Plo \
+ ./$(DEPDIR)/mail-search-args-imap.Plo \
+ ./$(DEPDIR)/mail-search-args-simplify.Plo \
+ ./$(DEPDIR)/mail-search-build.Plo \
+ ./$(DEPDIR)/mail-search-mime-build.Plo \
+ ./$(DEPDIR)/mail-search-mime-register.Plo \
+ ./$(DEPDIR)/mail-search-mime.Plo \
+ ./$(DEPDIR)/mail-search-parser-cmdline.Plo \
+ ./$(DEPDIR)/mail-search-parser-imap.Plo \
+ ./$(DEPDIR)/mail-search-parser.Plo \
+ ./$(DEPDIR)/mail-search-register-human.Plo \
+ ./$(DEPDIR)/mail-search-register-imap.Plo \
+ ./$(DEPDIR)/mail-search-register.Plo \
+ ./$(DEPDIR)/mail-search.Plo ./$(DEPDIR)/mail-storage-hooks.Plo \
+ ./$(DEPDIR)/mail-storage-register.Plo \
+ ./$(DEPDIR)/mail-storage-service.Plo \
+ ./$(DEPDIR)/mail-storage-settings.Plo \
+ ./$(DEPDIR)/mail-storage.Plo ./$(DEPDIR)/mail-thread.Plo \
+ ./$(DEPDIR)/mail-user.Plo ./$(DEPDIR)/mail.Plo \
+ ./$(DEPDIR)/mailbox-attribute-internal.Plo \
+ ./$(DEPDIR)/mailbox-attribute.Plo ./$(DEPDIR)/mailbox-get.Plo \
+ ./$(DEPDIR)/mailbox-guid-cache.Plo \
+ ./$(DEPDIR)/mailbox-header.Plo \
+ ./$(DEPDIR)/mailbox-keywords.Plo \
+ ./$(DEPDIR)/mailbox-list-notify.Plo \
+ ./$(DEPDIR)/mailbox-list-register.Plo \
+ ./$(DEPDIR)/mailbox-list.Plo \
+ ./$(DEPDIR)/mailbox-match-plugin.Plo \
+ ./$(DEPDIR)/mailbox-recent-flags.Plo \
+ ./$(DEPDIR)/mailbox-search-result.Plo \
+ ./$(DEPDIR)/mailbox-tree.Plo \
+ ./$(DEPDIR)/mailbox-uidvalidity.Plo \
+ ./$(DEPDIR)/mailbox-watch.Plo \
+ ./$(DEPDIR)/test-mail-search-args-imap.Po \
+ ./$(DEPDIR)/test-mail-search-args-simplify.Po \
+ ./$(DEPDIR)/test-mail-storage-common.Plo \
+ ./$(DEPDIR)/test-mail-storage.Po ./$(DEPDIR)/test-mail.Po \
+ ./$(DEPDIR)/test-mailbox-get.Po \
+ ./$(DEPDIR)/test-mailbox-list.Po
+am__mv = mv -f
+COMPILE = $(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) \
+ $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS)
+LTCOMPILE = $(LIBTOOL) $(AM_V_lt) --tag=CC $(AM_LIBTOOLFLAGS) \
+ $(LIBTOOLFLAGS) --mode=compile $(CC) $(DEFS) \
+ $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) $(CPPFLAGS) \
+ $(AM_CFLAGS) $(CFLAGS)
+AM_V_CC = $(am__v_CC_@AM_V@)
+am__v_CC_ = $(am__v_CC_@AM_DEFAULT_V@)
+am__v_CC_0 = @echo " CC " $@;
+am__v_CC_1 =
+CCLD = $(CC)
+LINK = $(LIBTOOL) $(AM_V_lt) --tag=CC $(AM_LIBTOOLFLAGS) \
+ $(LIBTOOLFLAGS) --mode=link $(CCLD) $(AM_CFLAGS) $(CFLAGS) \
+ $(AM_LDFLAGS) $(LDFLAGS) -o $@
+AM_V_CCLD = $(am__v_CCLD_@AM_V@)
+am__v_CCLD_ = $(am__v_CCLD_@AM_DEFAULT_V@)
+am__v_CCLD_0 = @echo " CCLD " $@;
+am__v_CCLD_1 =
+SOURCES = $(libdovecot_storage_lua_la_SOURCES) \
+ $(libdovecot_storage_la_SOURCES) $(libstorage_la_SOURCES) \
+ $(test_mail_SOURCES) $(test_mail_search_args_imap_SOURCES) \
+ $(test_mail_search_args_simplify_SOURCES) \
+ $(test_mail_storage_SOURCES) $(test_mailbox_get_SOURCES) \
+ $(test_mailbox_list_SOURCES)
+DIST_SOURCES = $(am__libdovecot_storage_lua_la_SOURCES_DIST) \
+ $(libdovecot_storage_la_SOURCES) $(libstorage_la_SOURCES) \
+ $(test_mail_SOURCES) $(test_mail_search_args_imap_SOURCES) \
+ $(test_mail_search_args_simplify_SOURCES) \
+ $(test_mail_storage_SOURCES) $(test_mailbox_get_SOURCES) \
+ $(test_mailbox_list_SOURCES)
+RECURSIVE_TARGETS = all-recursive check-recursive cscopelist-recursive \
+ ctags-recursive dvi-recursive html-recursive info-recursive \
+ install-data-recursive install-dvi-recursive \
+ install-exec-recursive install-html-recursive \
+ install-info-recursive install-pdf-recursive \
+ install-ps-recursive install-recursive installcheck-recursive \
+ installdirs-recursive pdf-recursive ps-recursive \
+ tags-recursive uninstall-recursive
+am__can_run_installinfo = \
+ case $$AM_UPDATE_INFO_DIR in \
+ n|no|NO) false;; \
+ *) (install-info --version) >/dev/null 2>&1;; \
+ esac
+am__pkginc_lib_HEADERS_DIST = fail-mail-storage.h mail-autoexpunge.h \
+ mail-copy.h mail-duplicate.h mail-error.h mail-namespace.h \
+ mail-search.h mail-search-build.h mail-search-mime.h \
+ mail-search-mime-build.h mail-search-mime-register.h \
+ mail-search-register.h mail-thread.h mail-storage.h \
+ mail-search-parser.h mail-search-parser-private.h \
+ mail-storage-private.h mail-storage-hooks.h \
+ mail-storage-service.h mail-storage-settings.h mail-user.h \
+ mailbox-attribute.h mailbox-attribute-internal.h \
+ mailbox-attribute-private.h mailbox-guid-cache.h \
+ mailbox-list.h mailbox-list-iter.h mailbox-list-private.h \
+ mailbox-list-notify.h mailbox-match-plugin.h \
+ mailbox-recent-flags.h mailbox-search-result-private.h \
+ mailbox-tree.h mailbox-uidvalidity.h mailbox-watch.h \
+ test-mail-storage-common.h mail-storage-lua.h \
+ mail-storage-lua-private.h
+HEADERS = $(noinst_HEADERS) $(pkginc_lib_HEADERS)
+RECURSIVE_CLEAN_TARGETS = mostlyclean-recursive clean-recursive \
+ distclean-recursive maintainer-clean-recursive
+am__recursive_targets = \
+ $(RECURSIVE_TARGETS) \
+ $(RECURSIVE_CLEAN_TARGETS) \
+ $(am__extra_recursive_targets)
+AM_RECURSIVE_TARGETS = $(am__recursive_targets:-recursive=) TAGS CTAGS \
+ distdir distdir-am
+am__tagged_files = $(HEADERS) $(SOURCES) $(TAGS_FILES) $(LISP)
+# Read a list of newline-separated strings from the standard input,
+# and print each of them once, without duplicates. Input order is
+# *not* preserved.
+am__uniquify_input = $(AWK) '\
+ BEGIN { nonempty = 0; } \
+ { items[$$0] = 1; nonempty = 1; } \
+ END { if (nonempty) { for (i in items) print i; }; } \
+'
+# Make sure the list of sources is unique. This is necessary because,
+# e.g., the same source file might be shared among _SOURCES variables
+# for different programs/libraries.
+am__define_uniq_tagged_files = \
+ list='$(am__tagged_files)'; \
+ unique=`for i in $$list; do \
+ if test -f "$$i"; then echo $$i; else echo $(srcdir)/$$i; fi; \
+ done | $(am__uniquify_input)`
+ETAGS = etags
+CTAGS = ctags
+DIST_SUBDIRS = $(SUBDIRS)
+am__DIST_COMMON = $(srcdir)/Makefile.in $(top_srcdir)/depcomp
+DISTFILES = $(DIST_COMMON) $(DIST_SOURCES) $(TEXINFOS) $(EXTRA_DIST)
+am__relativize = \
+ dir0=`pwd`; \
+ sed_first='s,^\([^/]*\)/.*$$,\1,'; \
+ sed_rest='s,^[^/]*/*,,'; \
+ sed_last='s,^.*/\([^/]*\)$$,\1,'; \
+ sed_butlast='s,/*[^/]*$$,,'; \
+ while test -n "$$dir1"; do \
+ first=`echo "$$dir1" | sed -e "$$sed_first"`; \
+ if test "$$first" != "."; then \
+ if test "$$first" = ".."; then \
+ dir2=`echo "$$dir0" | sed -e "$$sed_last"`/"$$dir2"; \
+ dir0=`echo "$$dir0" | sed -e "$$sed_butlast"`; \
+ else \
+ first2=`echo "$$dir2" | sed -e "$$sed_first"`; \
+ if test "$$first2" = "$$first"; then \
+ dir2=`echo "$$dir2" | sed -e "$$sed_rest"`; \
+ else \
+ dir2="../$$dir2"; \
+ fi; \
+ dir0="$$dir0"/"$$first"; \
+ fi; \
+ fi; \
+ dir1=`echo "$$dir1" | sed -e "$$sed_rest"`; \
+ done; \
+ reldir="$$dir2"
+ACLOCAL = @ACLOCAL@
+ACLOCAL_AMFLAGS = @ACLOCAL_AMFLAGS@
+AMTAR = @AMTAR@
+AM_DEFAULT_VERBOSITY = @AM_DEFAULT_VERBOSITY@
+APPARMOR_LIBS = @APPARMOR_LIBS@
+AR = @AR@
+AUTH_CFLAGS = @AUTH_CFLAGS@
+AUTH_LIBS = @AUTH_LIBS@
+AUTOCONF = @AUTOCONF@
+AUTOHEADER = @AUTOHEADER@
+AUTOMAKE = @AUTOMAKE@
+AWK = @AWK@
+BINARY_CFLAGS = @BINARY_CFLAGS@
+BINARY_LDFLAGS = @BINARY_LDFLAGS@
+BISON = @BISON@
+CASSANDRA_CFLAGS = @CASSANDRA_CFLAGS@
+CASSANDRA_LIBS = @CASSANDRA_LIBS@
+CC = @CC@
+CCDEPMODE = @CCDEPMODE@
+CDB_LIBS = @CDB_LIBS@
+CFLAGS = @CFLAGS@
+CLUCENE_CFLAGS = @CLUCENE_CFLAGS@
+CLUCENE_LIBS = @CLUCENE_LIBS@
+COMPRESS_LIBS = @COMPRESS_LIBS@
+CPP = @CPP@
+CPPFLAGS = @CPPFLAGS@
+CRYPT_LIBS = @CRYPT_LIBS@
+CXX = @CXX@
+CXXCPP = @CXXCPP@
+CXXDEPMODE = @CXXDEPMODE@
+CXXFLAGS = @CXXFLAGS@
+CYGPATH_W = @CYGPATH_W@
+DEFS = @DEFS@
+DEPDIR = @DEPDIR@
+DICT_LIBS = @DICT_LIBS@
+DLLIB = @DLLIB@
+DLLTOOL = @DLLTOOL@
+DSYMUTIL = @DSYMUTIL@
+DUMPBIN = @DUMPBIN@
+ECHO_C = @ECHO_C@
+ECHO_N = @ECHO_N@
+ECHO_T = @ECHO_T@
+EGREP = @EGREP@
+EXEEXT = @EXEEXT@
+FGREP = @FGREP@
+FLEX = @FLEX@
+FUZZER_CPPFLAGS = @FUZZER_CPPFLAGS@
+FUZZER_LDFLAGS = @FUZZER_LDFLAGS@
+GREP = @GREP@
+INSTALL = @INSTALL@
+INSTALL_DATA = @INSTALL_DATA@
+INSTALL_PROGRAM = @INSTALL_PROGRAM@
+INSTALL_SCRIPT = @INSTALL_SCRIPT@
+INSTALL_STRIP_PROGRAM = @INSTALL_STRIP_PROGRAM@
+KRB5CONFIG = @KRB5CONFIG@
+KRB5_CFLAGS = @KRB5_CFLAGS@
+KRB5_LIBS = @KRB5_LIBS@
+LD = @LD@
+LDAP_LIBS = @LDAP_LIBS@
+LDFLAGS = @LDFLAGS@
+LD_NO_WHOLE_ARCHIVE = @LD_NO_WHOLE_ARCHIVE@
+LD_WHOLE_ARCHIVE = @LD_WHOLE_ARCHIVE@
+LIBCAP = @LIBCAP@
+LIBDOVECOT = @LIBDOVECOT@
+LIBDOVECOT_COMPRESS = @LIBDOVECOT_COMPRESS@
+LIBDOVECOT_DEPS = @LIBDOVECOT_DEPS@
+LIBDOVECOT_DSYNC = @LIBDOVECOT_DSYNC@
+LIBDOVECOT_LA_LIBS = @LIBDOVECOT_LA_LIBS@
+LIBDOVECOT_LDA = @LIBDOVECOT_LDA@
+LIBDOVECOT_LDAP = @LIBDOVECOT_LDAP@
+LIBDOVECOT_LIBFTS = @LIBDOVECOT_LIBFTS@
+LIBDOVECOT_LIBFTS_DEPS = @LIBDOVECOT_LIBFTS_DEPS@
+LIBDOVECOT_LOGIN = @LIBDOVECOT_LOGIN@
+LIBDOVECOT_LUA = @LIBDOVECOT_LUA@
+LIBDOVECOT_LUA_DEPS = @LIBDOVECOT_LUA_DEPS@
+LIBDOVECOT_SQL = @LIBDOVECOT_SQL@
+LIBDOVECOT_STORAGE = @LIBDOVECOT_STORAGE@
+LIBDOVECOT_STORAGE_DEPS = @LIBDOVECOT_STORAGE_DEPS@
+LIBEXTTEXTCAT_CFLAGS = @LIBEXTTEXTCAT_CFLAGS@
+LIBEXTTEXTCAT_LIBS = @LIBEXTTEXTCAT_LIBS@
+LIBICONV = @LIBICONV@
+LIBICU_CFLAGS = @LIBICU_CFLAGS@
+LIBICU_LIBS = @LIBICU_LIBS@
+LIBOBJS = @LIBOBJS@
+LIBS = @LIBS@
+LIBSODIUM_CFLAGS = @LIBSODIUM_CFLAGS@
+LIBSODIUM_LIBS = @LIBSODIUM_LIBS@
+LIBTIRPC_CFLAGS = @LIBTIRPC_CFLAGS@
+LIBTIRPC_LIBS = @LIBTIRPC_LIBS@
+LIBTOOL = @LIBTOOL@
+LIBUNWIND_CFLAGS = @LIBUNWIND_CFLAGS@
+LIBUNWIND_LIBS = @LIBUNWIND_LIBS@
+LIBWRAP_LIBS = @LIBWRAP_LIBS@
+LINKED_STORAGE_LDADD = @LINKED_STORAGE_LDADD@
+LIPO = @LIPO@
+LN_S = @LN_S@
+LTLIBICONV = @LTLIBICONV@
+LTLIBOBJS = @LTLIBOBJS@
+LT_SYS_LIBRARY_PATH = @LT_SYS_LIBRARY_PATH@
+LUA_CFLAGS = @LUA_CFLAGS@
+LUA_LIBS = @LUA_LIBS@
+MAINT = @MAINT@
+MAKEINFO = @MAKEINFO@
+MANIFEST_TOOL = @MANIFEST_TOOL@
+MKDIR_P = @MKDIR_P@
+MODULE_LIBS = @MODULE_LIBS@
+MODULE_SUFFIX = @MODULE_SUFFIX@
+MYSQL_CFLAGS = @MYSQL_CFLAGS@
+MYSQL_CONFIG = @MYSQL_CONFIG@
+MYSQL_LIBS = @MYSQL_LIBS@
+NM = @NM@
+NMEDIT = @NMEDIT@
+NOPLUGIN_LDFLAGS = @NOPLUGIN_LDFLAGS@
+OBJDUMP = @OBJDUMP@
+OBJEXT = @OBJEXT@
+OTOOL = @OTOOL@
+OTOOL64 = @OTOOL64@
+PACKAGE = @PACKAGE@
+PACKAGE_BUGREPORT = @PACKAGE_BUGREPORT@
+PACKAGE_NAME = @PACKAGE_NAME@
+PACKAGE_STRING = @PACKAGE_STRING@
+PACKAGE_TARNAME = @PACKAGE_TARNAME@
+PACKAGE_URL = @PACKAGE_URL@
+PACKAGE_VERSION = @PACKAGE_VERSION@
+PANDOC = @PANDOC@
+PATH_SEPARATOR = @PATH_SEPARATOR@
+PGSQL_CFLAGS = @PGSQL_CFLAGS@
+PGSQL_LIBS = @PGSQL_LIBS@
+PG_CONFIG = @PG_CONFIG@
+PIE_CFLAGS = @PIE_CFLAGS@
+PIE_LDFLAGS = @PIE_LDFLAGS@
+PKG_CONFIG = @PKG_CONFIG@
+PKG_CONFIG_LIBDIR = @PKG_CONFIG_LIBDIR@
+PKG_CONFIG_PATH = @PKG_CONFIG_PATH@
+QUOTA_LIBS = @QUOTA_LIBS@
+RANLIB = @RANLIB@
+RELRO_LDFLAGS = @RELRO_LDFLAGS@
+RPCGEN = @RPCGEN@
+RUN_TEST = @RUN_TEST@
+SED = @SED@
+SETTING_FILES = @SETTING_FILES@
+SET_MAKE = @SET_MAKE@
+SHELL = @SHELL@
+SQLITE_CFLAGS = @SQLITE_CFLAGS@
+SQLITE_LIBS = @SQLITE_LIBS@
+SQL_CFLAGS = @SQL_CFLAGS@
+SQL_LIBS = @SQL_LIBS@
+SSL_CFLAGS = @SSL_CFLAGS@
+SSL_LIBS = @SSL_LIBS@
+STRIP = @STRIP@
+SYSTEMD_CFLAGS = @SYSTEMD_CFLAGS@
+SYSTEMD_LIBS = @SYSTEMD_LIBS@
+VALGRIND = @VALGRIND@
+VERSION = @VERSION@
+ZSTD_CFLAGS = @ZSTD_CFLAGS@
+ZSTD_LIBS = @ZSTD_LIBS@
+abs_builddir = @abs_builddir@
+abs_srcdir = @abs_srcdir@
+abs_top_builddir = @abs_top_builddir@
+abs_top_srcdir = @abs_top_srcdir@
+ac_ct_AR = @ac_ct_AR@
+ac_ct_CC = @ac_ct_CC@
+ac_ct_CXX = @ac_ct_CXX@
+ac_ct_DUMPBIN = @ac_ct_DUMPBIN@
+am__include = @am__include@
+am__leading_dot = @am__leading_dot@
+am__quote = @am__quote@
+am__tar = @am__tar@
+am__untar = @am__untar@
+bindir = @bindir@
+build = @build@
+build_alias = @build_alias@
+build_cpu = @build_cpu@
+build_os = @build_os@
+build_vendor = @build_vendor@
+builddir = @builddir@
+datadir = @datadir@
+datarootdir = @datarootdir@
+dict_drivers = @dict_drivers@
+docdir = @docdir@
+dvidir = @dvidir@
+exec_prefix = @exec_prefix@
+host = @host@
+host_alias = @host_alias@
+host_cpu = @host_cpu@
+host_os = @host_os@
+host_vendor = @host_vendor@
+htmldir = @htmldir@
+includedir = @includedir@
+infodir = @infodir@
+install_sh = @install_sh@
+libdir = @libdir@
+libexecdir = @libexecdir@
+localedir = @localedir@
+localstatedir = @localstatedir@
+mandir = @mandir@
+mkdir_p = @mkdir_p@
+moduledir = @moduledir@
+oldincludedir = @oldincludedir@
+pdfdir = @pdfdir@
+prefix = @prefix@
+program_transform_name = @program_transform_name@
+psdir = @psdir@
+rundir = @rundir@
+runstatedir = @runstatedir@
+sbindir = @sbindir@
+sharedstatedir = @sharedstatedir@
+sql_drivers = @sql_drivers@
+srcdir = @srcdir@
+ssldir = @ssldir@
+statedir = @statedir@
+sysconfdir = @sysconfdir@
+systemdservicetype = @systemdservicetype@
+systemdsystemunitdir = @systemdsystemunitdir@
+target_alias = @target_alias@
+top_build_prefix = @top_build_prefix@
+top_builddir = @top_builddir@
+top_srcdir = @top_srcdir@
+SUBDIRS = list index
+noinst_LTLIBRARIES = libstorage.la
+AM_CPPFLAGS = \
+ -I$(top_srcdir)/src/lib \
+ -I$(top_srcdir)/src/lib-test \
+ -I$(top_srcdir)/src/lib-auth \
+ -I$(top_srcdir)/src/lib-dict \
+ -I$(top_srcdir)/src/lib-sasl \
+ -I$(top_srcdir)/src/lib-ssl-iostream \
+ -I$(top_srcdir)/src/lib-fs \
+ -I$(top_srcdir)/src/lib-master \
+ -I$(top_srcdir)/src/lib-settings \
+ -I$(top_srcdir)/src/lib-charset \
+ -I$(top_srcdir)/src/lib-mail \
+ -I$(top_srcdir)/src/lib-smtp \
+ -I$(top_srcdir)/src/lib-imap \
+ -I$(top_srcdir)/src/lib-index \
+ -DPKG_RUNDIR=\""$(rundir)"\" \
+ -DMODULEDIR=\""$(moduledir)"\"
+
+libstorage_la_SOURCES = \
+ fail-mail-storage.c \
+ fail-mailbox.c \
+ fail-mail.c \
+ mail.c \
+ mail-autoexpunge.c \
+ mail-copy.c \
+ mail-duplicate.c \
+ mail-error.c \
+ mail-namespace.c \
+ mail-search.c \
+ mail-search-args-cmdline.c \
+ mail-search-args-imap.c \
+ mail-search-args-simplify.c \
+ mail-search-build.c \
+ mail-search-mime.c \
+ mail-search-mime-build.c \
+ mail-search-mime-register.c \
+ mail-search-parser.c \
+ mail-search-parser-imap.c \
+ mail-search-parser-cmdline.c \
+ mail-search-register.c \
+ mail-search-register-human.c \
+ mail-search-register-imap.c \
+ mail-storage.c \
+ mail-storage-hooks.c \
+ mail-storage-register.c \
+ mail-storage-service.c \
+ mail-storage-settings.c \
+ mail-thread.c \
+ mail-user.c \
+ mailbox-attribute.c \
+ mailbox-attribute-internal.c \
+ mailbox-get.c \
+ mailbox-guid-cache.c \
+ mailbox-header.c \
+ mailbox-keywords.c \
+ mailbox-list.c \
+ mailbox-list-notify.c \
+ mailbox-list-register.c \
+ mailbox-match-plugin.c \
+ mailbox-recent-flags.c \
+ mailbox-search-result.c \
+ mailbox-tree.c \
+ mailbox-uidvalidity.c \
+ mailbox-watch.c \
+ test-mail-storage-common.c
+
+headers = fail-mail-storage.h mail-autoexpunge.h mail-copy.h \
+ mail-duplicate.h mail-error.h mail-namespace.h mail-search.h \
+ mail-search-build.h mail-search-mime.h \
+ mail-search-mime-build.h mail-search-mime-register.h \
+ mail-search-register.h mail-thread.h mail-storage.h \
+ mail-search-parser.h mail-search-parser-private.h \
+ mail-storage-private.h mail-storage-hooks.h \
+ mail-storage-service.h mail-storage-settings.h mail-user.h \
+ mailbox-attribute.h mailbox-attribute-internal.h \
+ mailbox-attribute-private.h mailbox-guid-cache.h \
+ mailbox-list.h mailbox-list-iter.h mailbox-list-private.h \
+ mailbox-list-notify.h mailbox-match-plugin.h \
+ mailbox-recent-flags.h mailbox-search-result-private.h \
+ mailbox-tree.h mailbox-uidvalidity.h mailbox-watch.h \
+ test-mail-storage-common.h $(am__append_2)
+shlibs = \
+ index/shared/libstorage_shared.la \
+ index/dbox-multi/libstorage_dbox_multi.la \
+ index/dbox-single/libstorage_dbox_single.la \
+ index/dbox-common/libstorage_dbox_common.la \
+ index/maildir/libstorage_maildir.la \
+ index/mbox/libstorage_mbox.la \
+ index/imapc/libstorage_imapc.la \
+ ../lib-imap-client/libimap_client.la \
+ index/pop3c/libstorage_pop3c.la \
+ index/raw/libstorage_raw.la \
+ list/libstorage_list.la \
+ index/libstorage_index.la \
+ ../lib-index/libindex.la \
+ ../lib-imap-storage/libimap-storage.la
+
+libstorage_la_LIBADD = $(shlibs)
+libstorage_la_DEPENDENCIES = $(shlibs)
+pkglib_LTLIBRARIES = libdovecot-storage.la $(am__append_1)
+libdovecot_storage_la_SOURCES =
+libdovecot_storage_la_LIBADD = \
+ libstorage.la \
+ ../lib-dovecot/libdovecot.la \
+ $(LINKED_STORAGE_LDADD)
+
+libdovecot_storage_la_DEPENDENCIES = \
+ libstorage.la \
+ ../lib-dovecot/libdovecot.la \
+ $(LIBDOVECOT_DEPS)
+
+libdovecot_storage_la_LDFLAGS = -export-dynamic
+@HAVE_LUA_TRUE@libdovecot_storage_lua_la_SOURCES = \
+@HAVE_LUA_TRUE@ mail-lua.c \
+@HAVE_LUA_TRUE@ mail-user-lua.c \
+@HAVE_LUA_TRUE@ mail-storage-lua.c \
+@HAVE_LUA_TRUE@ mailbox-lua.c \
+@HAVE_LUA_TRUE@ mailbox-attribute-lua.c
+
+@HAVE_LUA_TRUE@libdovecot_storage_lua_la_CPPFLAGS = \
+@HAVE_LUA_TRUE@ $(AM_CPPFLAGS) \
+@HAVE_LUA_TRUE@ $(LUA_CFLAGS) \
+@HAVE_LUA_TRUE@ -I$(top_srcdir)/src/lib-lua
+
+@HAVE_LUA_TRUE@libdovecot_storage_lua_la_LIBADD = \
+@HAVE_LUA_TRUE@ libdovecot-storage.la \
+@HAVE_LUA_TRUE@ ../lib-lua/libdovecot-lua.la
+
+@HAVE_LUA_TRUE@libdovecot_storage_lua_la_DEPENDENCIES = \
+@HAVE_LUA_TRUE@ libdovecot-storage.la \
+@HAVE_LUA_TRUE@ ../lib-lua/libdovecot-lua.la
+
+@HAVE_LUA_TRUE@libdovecot_storage_lua_la_LDFLAGS = -export-dynamic
+test_programs = \
+ test-mail-search-args-imap \
+ test-mail-search-args-simplify \
+ test-mail \
+ test-mail-storage \
+ test-mailbox-get \
+ test-mailbox-list
+
+test_libs = \
+ $(top_builddir)/src/lib-test/libtest.la \
+ $(top_builddir)/src/lib/liblib.la
+
+test_mail_search_args_imap_SOURCES = test-mail-search-args-imap.c
+test_mail_search_args_imap_LDADD = libstorage.la $(LIBDOVECOT)
+test_mail_search_args_imap_DEPENDENCIES = libstorage.la $(LIBDOVECOT_DEPS)
+test_mail_search_args_simplify_SOURCES = test-mail-search-args-simplify.c
+test_mail_search_args_simplify_LDADD = libstorage.la $(LIBDOVECOT)
+test_mail_search_args_simplify_DEPENDENCIES = libstorage.la $(LIBDOVECOT_DEPS)
+test_mailbox_get_SOURCES = test-mailbox-get.c
+test_mailbox_get_LDADD = mailbox-get.lo $(test_libs)
+test_mailbox_get_DEPENDENCIES = $(noinst_LTLIBRARIES) $(test_libs)
+test_mail_SOURCES = test-mail.c
+test_mail_LDADD = libstorage.la $(LIBDOVECOT)
+test_mail_DEPENDENCIES = libstorage.la $(LIBDOVECOT_DEPS)
+test_mail_storage_SOURCES = test-mail-storage.c
+test_mail_storage_LDADD = libstorage.la $(LIBDOVECOT)
+test_mail_storage_DEPENDENCIES = libstorage.la $(LIBDOVECOT_DEPS)
+test_mailbox_list_SOURCES = test-mailbox-list.c
+test_mailbox_list_LDADD = libstorage.la $(LIBDOVECOT)
+test_mailbox_list_DEPENDENCIES = libstorage.la $(LIBDOVECOT_DEPS)
+pkginc_libdir = $(pkgincludedir)
+pkginc_lib_HEADERS = $(headers)
+noinst_HEADERS = $(test_headers)
+all: all-recursive
+
+.SUFFIXES:
+.SUFFIXES: .c .lo .o .obj
+$(srcdir)/Makefile.in: @MAINTAINER_MODE_TRUE@ $(srcdir)/Makefile.am $(am__configure_deps)
+ @for dep in $?; do \
+ case '$(am__configure_deps)' in \
+ *$$dep*) \
+ ( cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh ) \
+ && { if test -f $@; then exit 0; else break; fi; }; \
+ exit 1;; \
+ esac; \
+ done; \
+ echo ' cd $(top_srcdir) && $(AUTOMAKE) --foreign src/lib-storage/Makefile'; \
+ $(am__cd) $(top_srcdir) && \
+ $(AUTOMAKE) --foreign src/lib-storage/Makefile
+Makefile: $(srcdir)/Makefile.in $(top_builddir)/config.status
+ @case '$?' in \
+ *config.status*) \
+ cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh;; \
+ *) \
+ echo ' cd $(top_builddir) && $(SHELL) ./config.status $(subdir)/$@ $(am__maybe_remake_depfiles)'; \
+ cd $(top_builddir) && $(SHELL) ./config.status $(subdir)/$@ $(am__maybe_remake_depfiles);; \
+ esac;
+
+$(top_builddir)/config.status: $(top_srcdir)/configure $(CONFIG_STATUS_DEPENDENCIES)
+ cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh
+
+$(top_srcdir)/configure: @MAINTAINER_MODE_TRUE@ $(am__configure_deps)
+ cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh
+$(ACLOCAL_M4): @MAINTAINER_MODE_TRUE@ $(am__aclocal_m4_deps)
+ cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh
+$(am__aclocal_m4_deps):
+
+clean-noinstPROGRAMS:
+ @list='$(noinst_PROGRAMS)'; test -n "$$list" || exit 0; \
+ echo " rm -f" $$list; \
+ rm -f $$list || exit $$?; \
+ test -n "$(EXEEXT)" || exit 0; \
+ list=`for p in $$list; do echo "$$p"; done | sed 's/$(EXEEXT)$$//'`; \
+ echo " rm -f" $$list; \
+ rm -f $$list
+
+clean-noinstLTLIBRARIES:
+ -test -z "$(noinst_LTLIBRARIES)" || rm -f $(noinst_LTLIBRARIES)
+ @list='$(noinst_LTLIBRARIES)'; \
+ locs=`for p in $$list; do echo $$p; done | \
+ sed 's|^[^/]*$$|.|; s|/[^/]*$$||; s|$$|/so_locations|' | \
+ sort -u`; \
+ test -z "$$locs" || { \
+ echo rm -f $${locs}; \
+ rm -f $${locs}; \
+ }
+
+install-pkglibLTLIBRARIES: $(pkglib_LTLIBRARIES)
+ @$(NORMAL_INSTALL)
+ @list='$(pkglib_LTLIBRARIES)'; test -n "$(pkglibdir)" || list=; \
+ list2=; for p in $$list; do \
+ if test -f $$p; then \
+ list2="$$list2 $$p"; \
+ else :; fi; \
+ done; \
+ test -z "$$list2" || { \
+ echo " $(MKDIR_P) '$(DESTDIR)$(pkglibdir)'"; \
+ $(MKDIR_P) "$(DESTDIR)$(pkglibdir)" || exit 1; \
+ echo " $(LIBTOOL) $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=install $(INSTALL) $(INSTALL_STRIP_FLAG) $$list2 '$(DESTDIR)$(pkglibdir)'"; \
+ $(LIBTOOL) $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=install $(INSTALL) $(INSTALL_STRIP_FLAG) $$list2 "$(DESTDIR)$(pkglibdir)"; \
+ }
+
+uninstall-pkglibLTLIBRARIES:
+ @$(NORMAL_UNINSTALL)
+ @list='$(pkglib_LTLIBRARIES)'; test -n "$(pkglibdir)" || list=; \
+ for p in $$list; do \
+ $(am__strip_dir) \
+ echo " $(LIBTOOL) $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=uninstall rm -f '$(DESTDIR)$(pkglibdir)/$$f'"; \
+ $(LIBTOOL) $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=uninstall rm -f "$(DESTDIR)$(pkglibdir)/$$f"; \
+ done
+
+clean-pkglibLTLIBRARIES:
+ -test -z "$(pkglib_LTLIBRARIES)" || rm -f $(pkglib_LTLIBRARIES)
+ @list='$(pkglib_LTLIBRARIES)'; \
+ locs=`for p in $$list; do echo $$p; done | \
+ sed 's|^[^/]*$$|.|; s|/[^/]*$$||; s|$$|/so_locations|' | \
+ sort -u`; \
+ test -z "$$locs" || { \
+ echo rm -f $${locs}; \
+ rm -f $${locs}; \
+ }
+
+libdovecot-storage-lua.la: $(libdovecot_storage_lua_la_OBJECTS) $(libdovecot_storage_lua_la_DEPENDENCIES) $(EXTRA_libdovecot_storage_lua_la_DEPENDENCIES)
+ $(AM_V_CCLD)$(libdovecot_storage_lua_la_LINK) $(am_libdovecot_storage_lua_la_rpath) $(libdovecot_storage_lua_la_OBJECTS) $(libdovecot_storage_lua_la_LIBADD) $(LIBS)
+
+libdovecot-storage.la: $(libdovecot_storage_la_OBJECTS) $(libdovecot_storage_la_DEPENDENCIES) $(EXTRA_libdovecot_storage_la_DEPENDENCIES)
+ $(AM_V_CCLD)$(libdovecot_storage_la_LINK) -rpath $(pkglibdir) $(libdovecot_storage_la_OBJECTS) $(libdovecot_storage_la_LIBADD) $(LIBS)
+
+libstorage.la: $(libstorage_la_OBJECTS) $(libstorage_la_DEPENDENCIES) $(EXTRA_libstorage_la_DEPENDENCIES)
+ $(AM_V_CCLD)$(LINK) $(libstorage_la_OBJECTS) $(libstorage_la_LIBADD) $(LIBS)
+
+test-mail$(EXEEXT): $(test_mail_OBJECTS) $(test_mail_DEPENDENCIES) $(EXTRA_test_mail_DEPENDENCIES)
+ @rm -f test-mail$(EXEEXT)
+ $(AM_V_CCLD)$(LINK) $(test_mail_OBJECTS) $(test_mail_LDADD) $(LIBS)
+
+test-mail-search-args-imap$(EXEEXT): $(test_mail_search_args_imap_OBJECTS) $(test_mail_search_args_imap_DEPENDENCIES) $(EXTRA_test_mail_search_args_imap_DEPENDENCIES)
+ @rm -f test-mail-search-args-imap$(EXEEXT)
+ $(AM_V_CCLD)$(LINK) $(test_mail_search_args_imap_OBJECTS) $(test_mail_search_args_imap_LDADD) $(LIBS)
+
+test-mail-search-args-simplify$(EXEEXT): $(test_mail_search_args_simplify_OBJECTS) $(test_mail_search_args_simplify_DEPENDENCIES) $(EXTRA_test_mail_search_args_simplify_DEPENDENCIES)
+ @rm -f test-mail-search-args-simplify$(EXEEXT)
+ $(AM_V_CCLD)$(LINK) $(test_mail_search_args_simplify_OBJECTS) $(test_mail_search_args_simplify_LDADD) $(LIBS)
+
+test-mail-storage$(EXEEXT): $(test_mail_storage_OBJECTS) $(test_mail_storage_DEPENDENCIES) $(EXTRA_test_mail_storage_DEPENDENCIES)
+ @rm -f test-mail-storage$(EXEEXT)
+ $(AM_V_CCLD)$(LINK) $(test_mail_storage_OBJECTS) $(test_mail_storage_LDADD) $(LIBS)
+
+test-mailbox-get$(EXEEXT): $(test_mailbox_get_OBJECTS) $(test_mailbox_get_DEPENDENCIES) $(EXTRA_test_mailbox_get_DEPENDENCIES)
+ @rm -f test-mailbox-get$(EXEEXT)
+ $(AM_V_CCLD)$(LINK) $(test_mailbox_get_OBJECTS) $(test_mailbox_get_LDADD) $(LIBS)
+
+test-mailbox-list$(EXEEXT): $(test_mailbox_list_OBJECTS) $(test_mailbox_list_DEPENDENCIES) $(EXTRA_test_mailbox_list_DEPENDENCIES)
+ @rm -f test-mailbox-list$(EXEEXT)
+ $(AM_V_CCLD)$(LINK) $(test_mailbox_list_OBJECTS) $(test_mailbox_list_LDADD) $(LIBS)
+
+mostlyclean-compile:
+ -rm -f *.$(OBJEXT)
+
+distclean-compile:
+ -rm -f *.tab.c
+
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/fail-mail-storage.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/fail-mail.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/fail-mailbox.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/libdovecot_storage_lua_la-mail-lua.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/libdovecot_storage_lua_la-mail-storage-lua.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/libdovecot_storage_lua_la-mail-user-lua.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/libdovecot_storage_lua_la-mailbox-attribute-lua.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/libdovecot_storage_lua_la-mailbox-lua.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/mail-autoexpunge.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/mail-copy.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/mail-duplicate.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/mail-error.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/mail-namespace.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/mail-search-args-cmdline.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/mail-search-args-imap.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/mail-search-args-simplify.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/mail-search-build.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/mail-search-mime-build.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/mail-search-mime-register.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/mail-search-mime.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/mail-search-parser-cmdline.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/mail-search-parser-imap.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/mail-search-parser.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/mail-search-register-human.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/mail-search-register-imap.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/mail-search-register.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/mail-search.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/mail-storage-hooks.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/mail-storage-register.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/mail-storage-service.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/mail-storage-settings.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/mail-storage.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/mail-thread.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/mail-user.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/mail.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/mailbox-attribute-internal.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/mailbox-attribute.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/mailbox-get.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/mailbox-guid-cache.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/mailbox-header.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/mailbox-keywords.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/mailbox-list-notify.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/mailbox-list-register.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/mailbox-list.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/mailbox-match-plugin.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/mailbox-recent-flags.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/mailbox-search-result.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/mailbox-tree.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/mailbox-uidvalidity.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/mailbox-watch.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/test-mail-search-args-imap.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/test-mail-search-args-simplify.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/test-mail-storage-common.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/test-mail-storage.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/test-mail.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/test-mailbox-get.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/test-mailbox-list.Po@am__quote@ # am--include-marker
+
+$(am__depfiles_remade):
+ @$(MKDIR_P) $(@D)
+ @echo '# dummy' >$@-t && $(am__mv) $@-t $@
+
+am--depfiles: $(am__depfiles_remade)
+
+.c.o:
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(COMPILE) -MT $@ -MD -MP -MF $(DEPDIR)/$*.Tpo -c -o $@ $<
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/$*.Tpo $(DEPDIR)/$*.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='$<' object='$@' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(COMPILE) -c -o $@ $<
+
+.c.obj:
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(COMPILE) -MT $@ -MD -MP -MF $(DEPDIR)/$*.Tpo -c -o $@ `$(CYGPATH_W) '$<'`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/$*.Tpo $(DEPDIR)/$*.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='$<' object='$@' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(COMPILE) -c -o $@ `$(CYGPATH_W) '$<'`
+
+.c.lo:
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(LTCOMPILE) -MT $@ -MD -MP -MF $(DEPDIR)/$*.Tpo -c -o $@ $<
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/$*.Tpo $(DEPDIR)/$*.Plo
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='$<' object='$@' libtool=yes @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(LTCOMPILE) -c -o $@ $<
+
+libdovecot_storage_lua_la-mail-lua.lo: mail-lua.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(LIBTOOL) $(AM_V_lt) --tag=CC $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=compile $(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdovecot_storage_lua_la_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT libdovecot_storage_lua_la-mail-lua.lo -MD -MP -MF $(DEPDIR)/libdovecot_storage_lua_la-mail-lua.Tpo -c -o libdovecot_storage_lua_la-mail-lua.lo `test -f 'mail-lua.c' || echo '$(srcdir)/'`mail-lua.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/libdovecot_storage_lua_la-mail-lua.Tpo $(DEPDIR)/libdovecot_storage_lua_la-mail-lua.Plo
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='mail-lua.c' object='libdovecot_storage_lua_la-mail-lua.lo' libtool=yes @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(LIBTOOL) $(AM_V_lt) --tag=CC $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=compile $(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdovecot_storage_lua_la_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o libdovecot_storage_lua_la-mail-lua.lo `test -f 'mail-lua.c' || echo '$(srcdir)/'`mail-lua.c
+
+libdovecot_storage_lua_la-mail-user-lua.lo: mail-user-lua.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(LIBTOOL) $(AM_V_lt) --tag=CC $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=compile $(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdovecot_storage_lua_la_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT libdovecot_storage_lua_la-mail-user-lua.lo -MD -MP -MF $(DEPDIR)/libdovecot_storage_lua_la-mail-user-lua.Tpo -c -o libdovecot_storage_lua_la-mail-user-lua.lo `test -f 'mail-user-lua.c' || echo '$(srcdir)/'`mail-user-lua.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/libdovecot_storage_lua_la-mail-user-lua.Tpo $(DEPDIR)/libdovecot_storage_lua_la-mail-user-lua.Plo
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='mail-user-lua.c' object='libdovecot_storage_lua_la-mail-user-lua.lo' libtool=yes @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(LIBTOOL) $(AM_V_lt) --tag=CC $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=compile $(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdovecot_storage_lua_la_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o libdovecot_storage_lua_la-mail-user-lua.lo `test -f 'mail-user-lua.c' || echo '$(srcdir)/'`mail-user-lua.c
+
+libdovecot_storage_lua_la-mail-storage-lua.lo: mail-storage-lua.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(LIBTOOL) $(AM_V_lt) --tag=CC $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=compile $(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdovecot_storage_lua_la_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT libdovecot_storage_lua_la-mail-storage-lua.lo -MD -MP -MF $(DEPDIR)/libdovecot_storage_lua_la-mail-storage-lua.Tpo -c -o libdovecot_storage_lua_la-mail-storage-lua.lo `test -f 'mail-storage-lua.c' || echo '$(srcdir)/'`mail-storage-lua.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/libdovecot_storage_lua_la-mail-storage-lua.Tpo $(DEPDIR)/libdovecot_storage_lua_la-mail-storage-lua.Plo
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='mail-storage-lua.c' object='libdovecot_storage_lua_la-mail-storage-lua.lo' libtool=yes @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(LIBTOOL) $(AM_V_lt) --tag=CC $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=compile $(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdovecot_storage_lua_la_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o libdovecot_storage_lua_la-mail-storage-lua.lo `test -f 'mail-storage-lua.c' || echo '$(srcdir)/'`mail-storage-lua.c
+
+libdovecot_storage_lua_la-mailbox-lua.lo: mailbox-lua.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(LIBTOOL) $(AM_V_lt) --tag=CC $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=compile $(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdovecot_storage_lua_la_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT libdovecot_storage_lua_la-mailbox-lua.lo -MD -MP -MF $(DEPDIR)/libdovecot_storage_lua_la-mailbox-lua.Tpo -c -o libdovecot_storage_lua_la-mailbox-lua.lo `test -f 'mailbox-lua.c' || echo '$(srcdir)/'`mailbox-lua.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/libdovecot_storage_lua_la-mailbox-lua.Tpo $(DEPDIR)/libdovecot_storage_lua_la-mailbox-lua.Plo
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='mailbox-lua.c' object='libdovecot_storage_lua_la-mailbox-lua.lo' libtool=yes @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(LIBTOOL) $(AM_V_lt) --tag=CC $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=compile $(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdovecot_storage_lua_la_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o libdovecot_storage_lua_la-mailbox-lua.lo `test -f 'mailbox-lua.c' || echo '$(srcdir)/'`mailbox-lua.c
+
+libdovecot_storage_lua_la-mailbox-attribute-lua.lo: mailbox-attribute-lua.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(LIBTOOL) $(AM_V_lt) --tag=CC $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=compile $(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdovecot_storage_lua_la_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT libdovecot_storage_lua_la-mailbox-attribute-lua.lo -MD -MP -MF $(DEPDIR)/libdovecot_storage_lua_la-mailbox-attribute-lua.Tpo -c -o libdovecot_storage_lua_la-mailbox-attribute-lua.lo `test -f 'mailbox-attribute-lua.c' || echo '$(srcdir)/'`mailbox-attribute-lua.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/libdovecot_storage_lua_la-mailbox-attribute-lua.Tpo $(DEPDIR)/libdovecot_storage_lua_la-mailbox-attribute-lua.Plo
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='mailbox-attribute-lua.c' object='libdovecot_storage_lua_la-mailbox-attribute-lua.lo' libtool=yes @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(LIBTOOL) $(AM_V_lt) --tag=CC $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=compile $(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libdovecot_storage_lua_la_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o libdovecot_storage_lua_la-mailbox-attribute-lua.lo `test -f 'mailbox-attribute-lua.c' || echo '$(srcdir)/'`mailbox-attribute-lua.c
+
+mostlyclean-libtool:
+ -rm -f *.lo
+
+clean-libtool:
+ -rm -rf .libs _libs
+install-pkginc_libHEADERS: $(pkginc_lib_HEADERS)
+ @$(NORMAL_INSTALL)
+ @list='$(pkginc_lib_HEADERS)'; test -n "$(pkginc_libdir)" || list=; \
+ if test -n "$$list"; then \
+ echo " $(MKDIR_P) '$(DESTDIR)$(pkginc_libdir)'"; \
+ $(MKDIR_P) "$(DESTDIR)$(pkginc_libdir)" || exit 1; \
+ fi; \
+ for p in $$list; do \
+ if test -f "$$p"; then d=; else d="$(srcdir)/"; fi; \
+ echo "$$d$$p"; \
+ done | $(am__base_list) | \
+ while read files; do \
+ echo " $(INSTALL_HEADER) $$files '$(DESTDIR)$(pkginc_libdir)'"; \
+ $(INSTALL_HEADER) $$files "$(DESTDIR)$(pkginc_libdir)" || exit $$?; \
+ done
+
+uninstall-pkginc_libHEADERS:
+ @$(NORMAL_UNINSTALL)
+ @list='$(pkginc_lib_HEADERS)'; test -n "$(pkginc_libdir)" || list=; \
+ files=`for p in $$list; do echo $$p; done | sed -e 's|^.*/||'`; \
+ dir='$(DESTDIR)$(pkginc_libdir)'; $(am__uninstall_files_from_dir)
+
+# This directory's subdirectories are mostly independent; you can cd
+# into them and run 'make' without going through this Makefile.
+# To change the values of 'make' variables: instead of editing Makefiles,
+# (1) if the variable is set in 'config.status', edit 'config.status'
+# (which will cause the Makefiles to be regenerated when you run 'make');
+# (2) otherwise, pass the desired values on the 'make' command line.
+$(am__recursive_targets):
+ @fail=; \
+ if $(am__make_keepgoing); then \
+ failcom='fail=yes'; \
+ else \
+ failcom='exit 1'; \
+ fi; \
+ dot_seen=no; \
+ target=`echo $@ | sed s/-recursive//`; \
+ case "$@" in \
+ distclean-* | maintainer-clean-*) list='$(DIST_SUBDIRS)' ;; \
+ *) list='$(SUBDIRS)' ;; \
+ esac; \
+ for subdir in $$list; do \
+ echo "Making $$target in $$subdir"; \
+ if test "$$subdir" = "."; then \
+ dot_seen=yes; \
+ local_target="$$target-am"; \
+ else \
+ local_target="$$target"; \
+ fi; \
+ ($(am__cd) $$subdir && $(MAKE) $(AM_MAKEFLAGS) $$local_target) \
+ || eval $$failcom; \
+ done; \
+ if test "$$dot_seen" = "no"; then \
+ $(MAKE) $(AM_MAKEFLAGS) "$$target-am" || exit 1; \
+ fi; test -z "$$fail"
+
+ID: $(am__tagged_files)
+ $(am__define_uniq_tagged_files); mkid -fID $$unique
+tags: tags-recursive
+TAGS: tags
+
+tags-am: $(TAGS_DEPENDENCIES) $(am__tagged_files)
+ set x; \
+ here=`pwd`; \
+ if ($(ETAGS) --etags-include --version) >/dev/null 2>&1; then \
+ include_option=--etags-include; \
+ empty_fix=.; \
+ else \
+ include_option=--include; \
+ empty_fix=; \
+ fi; \
+ list='$(SUBDIRS)'; for subdir in $$list; do \
+ if test "$$subdir" = .; then :; else \
+ test ! -f $$subdir/TAGS || \
+ set "$$@" "$$include_option=$$here/$$subdir/TAGS"; \
+ fi; \
+ done; \
+ $(am__define_uniq_tagged_files); \
+ shift; \
+ if test -z "$(ETAGS_ARGS)$$*$$unique"; then :; else \
+ test -n "$$unique" || unique=$$empty_fix; \
+ if test $$# -gt 0; then \
+ $(ETAGS) $(ETAGSFLAGS) $(AM_ETAGSFLAGS) $(ETAGS_ARGS) \
+ "$$@" $$unique; \
+ else \
+ $(ETAGS) $(ETAGSFLAGS) $(AM_ETAGSFLAGS) $(ETAGS_ARGS) \
+ $$unique; \
+ fi; \
+ fi
+ctags: ctags-recursive
+
+CTAGS: ctags
+ctags-am: $(TAGS_DEPENDENCIES) $(am__tagged_files)
+ $(am__define_uniq_tagged_files); \
+ test -z "$(CTAGS_ARGS)$$unique" \
+ || $(CTAGS) $(CTAGSFLAGS) $(AM_CTAGSFLAGS) $(CTAGS_ARGS) \
+ $$unique
+
+GTAGS:
+ here=`$(am__cd) $(top_builddir) && pwd` \
+ && $(am__cd) $(top_srcdir) \
+ && gtags -i $(GTAGS_ARGS) "$$here"
+cscopelist: cscopelist-recursive
+
+cscopelist-am: $(am__tagged_files)
+ list='$(am__tagged_files)'; \
+ case "$(srcdir)" in \
+ [\\/]* | ?:[\\/]*) sdir="$(srcdir)" ;; \
+ *) sdir=$(subdir)/$(srcdir) ;; \
+ esac; \
+ for i in $$list; do \
+ if test -f "$$i"; then \
+ echo "$(subdir)/$$i"; \
+ else \
+ echo "$$sdir/$$i"; \
+ fi; \
+ done >> $(top_builddir)/cscope.files
+
+distclean-tags:
+ -rm -f TAGS ID GTAGS GRTAGS GSYMS GPATH tags
+
+distdir: $(BUILT_SOURCES)
+ $(MAKE) $(AM_MAKEFLAGS) distdir-am
+
+distdir-am: $(DISTFILES)
+ @srcdirstrip=`echo "$(srcdir)" | sed 's/[].[^$$\\*]/\\\\&/g'`; \
+ topsrcdirstrip=`echo "$(top_srcdir)" | sed 's/[].[^$$\\*]/\\\\&/g'`; \
+ list='$(DISTFILES)'; \
+ dist_files=`for file in $$list; do echo $$file; done | \
+ sed -e "s|^$$srcdirstrip/||;t" \
+ -e "s|^$$topsrcdirstrip/|$(top_builddir)/|;t"`; \
+ case $$dist_files in \
+ */*) $(MKDIR_P) `echo "$$dist_files" | \
+ sed '/\//!d;s|^|$(distdir)/|;s,/[^/]*$$,,' | \
+ sort -u` ;; \
+ esac; \
+ for file in $$dist_files; do \
+ if test -f $$file || test -d $$file; then d=.; else d=$(srcdir); fi; \
+ if test -d $$d/$$file; then \
+ dir=`echo "/$$file" | sed -e 's,/[^/]*$$,,'`; \
+ if test -d "$(distdir)/$$file"; then \
+ find "$(distdir)/$$file" -type d ! -perm -700 -exec chmod u+rwx {} \;; \
+ fi; \
+ if test -d $(srcdir)/$$file && test $$d != $(srcdir); then \
+ cp -fpR $(srcdir)/$$file "$(distdir)$$dir" || exit 1; \
+ find "$(distdir)/$$file" -type d ! -perm -700 -exec chmod u+rwx {} \;; \
+ fi; \
+ cp -fpR $$d/$$file "$(distdir)$$dir" || exit 1; \
+ else \
+ test -f "$(distdir)/$$file" \
+ || cp -p $$d/$$file "$(distdir)/$$file" \
+ || exit 1; \
+ fi; \
+ done
+ @list='$(DIST_SUBDIRS)'; for subdir in $$list; do \
+ if test "$$subdir" = .; then :; else \
+ $(am__make_dryrun) \
+ || test -d "$(distdir)/$$subdir" \
+ || $(MKDIR_P) "$(distdir)/$$subdir" \
+ || exit 1; \
+ dir1=$$subdir; dir2="$(distdir)/$$subdir"; \
+ $(am__relativize); \
+ new_distdir=$$reldir; \
+ dir1=$$subdir; dir2="$(top_distdir)"; \
+ $(am__relativize); \
+ new_top_distdir=$$reldir; \
+ echo " (cd $$subdir && $(MAKE) $(AM_MAKEFLAGS) top_distdir="$$new_top_distdir" distdir="$$new_distdir" \\"; \
+ echo " am__remove_distdir=: am__skip_length_check=: am__skip_mode_fix=: distdir)"; \
+ ($(am__cd) $$subdir && \
+ $(MAKE) $(AM_MAKEFLAGS) \
+ top_distdir="$$new_top_distdir" \
+ distdir="$$new_distdir" \
+ am__remove_distdir=: \
+ am__skip_length_check=: \
+ am__skip_mode_fix=: \
+ distdir) \
+ || exit 1; \
+ fi; \
+ done
+check-am: all-am
+ $(MAKE) $(AM_MAKEFLAGS) check-local
+check: check-recursive
+all-am: Makefile $(PROGRAMS) $(LTLIBRARIES) $(HEADERS)
+installdirs: installdirs-recursive
+installdirs-am:
+ for dir in "$(DESTDIR)$(pkglibdir)" "$(DESTDIR)$(pkginc_libdir)"; do \
+ test -z "$$dir" || $(MKDIR_P) "$$dir"; \
+ done
+install: install-recursive
+install-exec: install-exec-recursive
+install-data: install-data-recursive
+uninstall: uninstall-recursive
+
+install-am: all-am
+ @$(MAKE) $(AM_MAKEFLAGS) install-exec-am install-data-am
+
+installcheck: installcheck-recursive
+install-strip:
+ if test -z '$(STRIP)'; then \
+ $(MAKE) $(AM_MAKEFLAGS) INSTALL_PROGRAM="$(INSTALL_STRIP_PROGRAM)" \
+ install_sh_PROGRAM="$(INSTALL_STRIP_PROGRAM)" INSTALL_STRIP_FLAG=-s \
+ install; \
+ else \
+ $(MAKE) $(AM_MAKEFLAGS) INSTALL_PROGRAM="$(INSTALL_STRIP_PROGRAM)" \
+ install_sh_PROGRAM="$(INSTALL_STRIP_PROGRAM)" INSTALL_STRIP_FLAG=-s \
+ "INSTALL_PROGRAM_ENV=STRIPPROG='$(STRIP)'" install; \
+ fi
+mostlyclean-generic:
+
+clean-generic:
+
+distclean-generic:
+ -test -z "$(CONFIG_CLEAN_FILES)" || rm -f $(CONFIG_CLEAN_FILES)
+ -test . = "$(srcdir)" || test -z "$(CONFIG_CLEAN_VPATH_FILES)" || rm -f $(CONFIG_CLEAN_VPATH_FILES)
+
+maintainer-clean-generic:
+ @echo "This command is intended for maintainers to use"
+ @echo "it deletes files that may require special tools to rebuild."
+clean: clean-recursive
+
+clean-am: clean-generic clean-libtool clean-noinstLTLIBRARIES \
+ clean-noinstPROGRAMS clean-pkglibLTLIBRARIES mostlyclean-am
+
+distclean: distclean-recursive
+ -rm -f ./$(DEPDIR)/fail-mail-storage.Plo
+ -rm -f ./$(DEPDIR)/fail-mail.Plo
+ -rm -f ./$(DEPDIR)/fail-mailbox.Plo
+ -rm -f ./$(DEPDIR)/libdovecot_storage_lua_la-mail-lua.Plo
+ -rm -f ./$(DEPDIR)/libdovecot_storage_lua_la-mail-storage-lua.Plo
+ -rm -f ./$(DEPDIR)/libdovecot_storage_lua_la-mail-user-lua.Plo
+ -rm -f ./$(DEPDIR)/libdovecot_storage_lua_la-mailbox-attribute-lua.Plo
+ -rm -f ./$(DEPDIR)/libdovecot_storage_lua_la-mailbox-lua.Plo
+ -rm -f ./$(DEPDIR)/mail-autoexpunge.Plo
+ -rm -f ./$(DEPDIR)/mail-copy.Plo
+ -rm -f ./$(DEPDIR)/mail-duplicate.Plo
+ -rm -f ./$(DEPDIR)/mail-error.Plo
+ -rm -f ./$(DEPDIR)/mail-namespace.Plo
+ -rm -f ./$(DEPDIR)/mail-search-args-cmdline.Plo
+ -rm -f ./$(DEPDIR)/mail-search-args-imap.Plo
+ -rm -f ./$(DEPDIR)/mail-search-args-simplify.Plo
+ -rm -f ./$(DEPDIR)/mail-search-build.Plo
+ -rm -f ./$(DEPDIR)/mail-search-mime-build.Plo
+ -rm -f ./$(DEPDIR)/mail-search-mime-register.Plo
+ -rm -f ./$(DEPDIR)/mail-search-mime.Plo
+ -rm -f ./$(DEPDIR)/mail-search-parser-cmdline.Plo
+ -rm -f ./$(DEPDIR)/mail-search-parser-imap.Plo
+ -rm -f ./$(DEPDIR)/mail-search-parser.Plo
+ -rm -f ./$(DEPDIR)/mail-search-register-human.Plo
+ -rm -f ./$(DEPDIR)/mail-search-register-imap.Plo
+ -rm -f ./$(DEPDIR)/mail-search-register.Plo
+ -rm -f ./$(DEPDIR)/mail-search.Plo
+ -rm -f ./$(DEPDIR)/mail-storage-hooks.Plo
+ -rm -f ./$(DEPDIR)/mail-storage-register.Plo
+ -rm -f ./$(DEPDIR)/mail-storage-service.Plo
+ -rm -f ./$(DEPDIR)/mail-storage-settings.Plo
+ -rm -f ./$(DEPDIR)/mail-storage.Plo
+ -rm -f ./$(DEPDIR)/mail-thread.Plo
+ -rm -f ./$(DEPDIR)/mail-user.Plo
+ -rm -f ./$(DEPDIR)/mail.Plo
+ -rm -f ./$(DEPDIR)/mailbox-attribute-internal.Plo
+ -rm -f ./$(DEPDIR)/mailbox-attribute.Plo
+ -rm -f ./$(DEPDIR)/mailbox-get.Plo
+ -rm -f ./$(DEPDIR)/mailbox-guid-cache.Plo
+ -rm -f ./$(DEPDIR)/mailbox-header.Plo
+ -rm -f ./$(DEPDIR)/mailbox-keywords.Plo
+ -rm -f ./$(DEPDIR)/mailbox-list-notify.Plo
+ -rm -f ./$(DEPDIR)/mailbox-list-register.Plo
+ -rm -f ./$(DEPDIR)/mailbox-list.Plo
+ -rm -f ./$(DEPDIR)/mailbox-match-plugin.Plo
+ -rm -f ./$(DEPDIR)/mailbox-recent-flags.Plo
+ -rm -f ./$(DEPDIR)/mailbox-search-result.Plo
+ -rm -f ./$(DEPDIR)/mailbox-tree.Plo
+ -rm -f ./$(DEPDIR)/mailbox-uidvalidity.Plo
+ -rm -f ./$(DEPDIR)/mailbox-watch.Plo
+ -rm -f ./$(DEPDIR)/test-mail-search-args-imap.Po
+ -rm -f ./$(DEPDIR)/test-mail-search-args-simplify.Po
+ -rm -f ./$(DEPDIR)/test-mail-storage-common.Plo
+ -rm -f ./$(DEPDIR)/test-mail-storage.Po
+ -rm -f ./$(DEPDIR)/test-mail.Po
+ -rm -f ./$(DEPDIR)/test-mailbox-get.Po
+ -rm -f ./$(DEPDIR)/test-mailbox-list.Po
+ -rm -f Makefile
+distclean-am: clean-am distclean-compile distclean-generic \
+ distclean-tags
+
+dvi: dvi-recursive
+
+dvi-am:
+
+html: html-recursive
+
+html-am:
+
+info: info-recursive
+
+info-am:
+
+install-data-am: install-pkginc_libHEADERS
+
+install-dvi: install-dvi-recursive
+
+install-dvi-am:
+
+install-exec-am: install-pkglibLTLIBRARIES
+
+install-html: install-html-recursive
+
+install-html-am:
+
+install-info: install-info-recursive
+
+install-info-am:
+
+install-man:
+
+install-pdf: install-pdf-recursive
+
+install-pdf-am:
+
+install-ps: install-ps-recursive
+
+install-ps-am:
+
+installcheck-am:
+
+maintainer-clean: maintainer-clean-recursive
+ -rm -f ./$(DEPDIR)/fail-mail-storage.Plo
+ -rm -f ./$(DEPDIR)/fail-mail.Plo
+ -rm -f ./$(DEPDIR)/fail-mailbox.Plo
+ -rm -f ./$(DEPDIR)/libdovecot_storage_lua_la-mail-lua.Plo
+ -rm -f ./$(DEPDIR)/libdovecot_storage_lua_la-mail-storage-lua.Plo
+ -rm -f ./$(DEPDIR)/libdovecot_storage_lua_la-mail-user-lua.Plo
+ -rm -f ./$(DEPDIR)/libdovecot_storage_lua_la-mailbox-attribute-lua.Plo
+ -rm -f ./$(DEPDIR)/libdovecot_storage_lua_la-mailbox-lua.Plo
+ -rm -f ./$(DEPDIR)/mail-autoexpunge.Plo
+ -rm -f ./$(DEPDIR)/mail-copy.Plo
+ -rm -f ./$(DEPDIR)/mail-duplicate.Plo
+ -rm -f ./$(DEPDIR)/mail-error.Plo
+ -rm -f ./$(DEPDIR)/mail-namespace.Plo
+ -rm -f ./$(DEPDIR)/mail-search-args-cmdline.Plo
+ -rm -f ./$(DEPDIR)/mail-search-args-imap.Plo
+ -rm -f ./$(DEPDIR)/mail-search-args-simplify.Plo
+ -rm -f ./$(DEPDIR)/mail-search-build.Plo
+ -rm -f ./$(DEPDIR)/mail-search-mime-build.Plo
+ -rm -f ./$(DEPDIR)/mail-search-mime-register.Plo
+ -rm -f ./$(DEPDIR)/mail-search-mime.Plo
+ -rm -f ./$(DEPDIR)/mail-search-parser-cmdline.Plo
+ -rm -f ./$(DEPDIR)/mail-search-parser-imap.Plo
+ -rm -f ./$(DEPDIR)/mail-search-parser.Plo
+ -rm -f ./$(DEPDIR)/mail-search-register-human.Plo
+ -rm -f ./$(DEPDIR)/mail-search-register-imap.Plo
+ -rm -f ./$(DEPDIR)/mail-search-register.Plo
+ -rm -f ./$(DEPDIR)/mail-search.Plo
+ -rm -f ./$(DEPDIR)/mail-storage-hooks.Plo
+ -rm -f ./$(DEPDIR)/mail-storage-register.Plo
+ -rm -f ./$(DEPDIR)/mail-storage-service.Plo
+ -rm -f ./$(DEPDIR)/mail-storage-settings.Plo
+ -rm -f ./$(DEPDIR)/mail-storage.Plo
+ -rm -f ./$(DEPDIR)/mail-thread.Plo
+ -rm -f ./$(DEPDIR)/mail-user.Plo
+ -rm -f ./$(DEPDIR)/mail.Plo
+ -rm -f ./$(DEPDIR)/mailbox-attribute-internal.Plo
+ -rm -f ./$(DEPDIR)/mailbox-attribute.Plo
+ -rm -f ./$(DEPDIR)/mailbox-get.Plo
+ -rm -f ./$(DEPDIR)/mailbox-guid-cache.Plo
+ -rm -f ./$(DEPDIR)/mailbox-header.Plo
+ -rm -f ./$(DEPDIR)/mailbox-keywords.Plo
+ -rm -f ./$(DEPDIR)/mailbox-list-notify.Plo
+ -rm -f ./$(DEPDIR)/mailbox-list-register.Plo
+ -rm -f ./$(DEPDIR)/mailbox-list.Plo
+ -rm -f ./$(DEPDIR)/mailbox-match-plugin.Plo
+ -rm -f ./$(DEPDIR)/mailbox-recent-flags.Plo
+ -rm -f ./$(DEPDIR)/mailbox-search-result.Plo
+ -rm -f ./$(DEPDIR)/mailbox-tree.Plo
+ -rm -f ./$(DEPDIR)/mailbox-uidvalidity.Plo
+ -rm -f ./$(DEPDIR)/mailbox-watch.Plo
+ -rm -f ./$(DEPDIR)/test-mail-search-args-imap.Po
+ -rm -f ./$(DEPDIR)/test-mail-search-args-simplify.Po
+ -rm -f ./$(DEPDIR)/test-mail-storage-common.Plo
+ -rm -f ./$(DEPDIR)/test-mail-storage.Po
+ -rm -f ./$(DEPDIR)/test-mail.Po
+ -rm -f ./$(DEPDIR)/test-mailbox-get.Po
+ -rm -f ./$(DEPDIR)/test-mailbox-list.Po
+ -rm -f Makefile
+maintainer-clean-am: distclean-am maintainer-clean-generic
+
+mostlyclean: mostlyclean-recursive
+
+mostlyclean-am: mostlyclean-compile mostlyclean-generic \
+ mostlyclean-libtool
+
+pdf: pdf-recursive
+
+pdf-am:
+
+ps: ps-recursive
+
+ps-am:
+
+uninstall-am: uninstall-pkginc_libHEADERS uninstall-pkglibLTLIBRARIES
+
+.MAKE: $(am__recursive_targets) check-am install-am install-strip
+
+.PHONY: $(am__recursive_targets) CTAGS GTAGS TAGS all all-am \
+ am--depfiles check check-am check-local clean clean-generic \
+ clean-libtool clean-noinstLTLIBRARIES clean-noinstPROGRAMS \
+ clean-pkglibLTLIBRARIES cscopelist-am ctags ctags-am distclean \
+ distclean-compile distclean-generic distclean-libtool \
+ distclean-tags distdir dvi dvi-am html html-am info info-am \
+ install install-am install-data install-data-am install-dvi \
+ install-dvi-am install-exec install-exec-am install-html \
+ install-html-am install-info install-info-am install-man \
+ install-pdf install-pdf-am install-pkginc_libHEADERS \
+ install-pkglibLTLIBRARIES install-ps install-ps-am \
+ install-strip installcheck installcheck-am installdirs \
+ installdirs-am maintainer-clean maintainer-clean-generic \
+ mostlyclean mostlyclean-compile mostlyclean-generic \
+ mostlyclean-libtool pdf pdf-am ps ps-am tags tags-am uninstall \
+ uninstall-am uninstall-pkginc_libHEADERS \
+ uninstall-pkglibLTLIBRARIES
+
+.PRECIOUS: Makefile
+
+
+check-local:
+ for bin in $(test_programs); do \
+ if ! $(RUN_TEST) ./$$bin; then exit 1; fi; \
+ done
+
+# Tell versions [3.59,3.63) of GNU make to not export all variables.
+# Otherwise a system limit (for SysV at least) may be exceeded.
+.NOEXPORT:
diff --git a/src/lib-storage/fail-mail-storage.c b/src/lib-storage/fail-mail-storage.c
new file mode 100644
index 0000000..f89d6f6
--- /dev/null
+++ b/src/lib-storage/fail-mail-storage.c
@@ -0,0 +1,61 @@
+/* Copyright (c) 2009-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "mail-storage-private.h"
+#include "fail-mail-storage.h"
+
+static struct mail_storage *fail_storage_alloc(void)
+{
+ struct mail_storage *storage;
+ pool_t pool;
+
+ pool = pool_alloconly_create("fail mail storage", 1024);
+ storage = p_new(pool, struct mail_storage, 1);
+ *storage = fail_storage;
+ storage->pool = pool;
+ return storage;
+}
+
+static void fail_storage_destroy(struct mail_storage *storage ATTR_UNUSED)
+{
+}
+
+static void
+fail_storage_get_list_settings(const struct mail_namespace *ns ATTR_UNUSED,
+ struct mailbox_list_settings *set)
+{
+ if (set->layout == NULL)
+ set->layout = "fail";
+ if (set->subscription_fname == NULL)
+ set->subscription_fname = "subscriptions";
+}
+
+struct mail_storage fail_storage = {
+ .name = "fail",
+ .class_flags = MAIL_STORAGE_CLASS_FLAG_NO_ROOT,
+
+ .v = {
+ NULL,
+ fail_storage_alloc,
+ NULL,
+ fail_storage_destroy,
+ NULL,
+ fail_storage_get_list_settings,
+ NULL,
+ fail_mailbox_alloc,
+ NULL,
+ NULL,
+ }
+};
+
+struct mail_storage *fail_mail_storage_create(void)
+{
+ struct mail_storage *storage;
+
+ storage = fail_storage_alloc();
+ storage->refcount = 1;
+ storage->storage_class = &fail_storage;
+ p_array_init(&storage->module_contexts, storage->pool, 5);
+ return storage;
+}
diff --git a/src/lib-storage/fail-mail-storage.h b/src/lib-storage/fail-mail-storage.h
new file mode 100644
index 0000000..d6031a2
--- /dev/null
+++ b/src/lib-storage/fail-mail-storage.h
@@ -0,0 +1,19 @@
+#ifndef FAIL_MAIL_STORAGE_H
+#define FAIL_MAIL_STORAGE_H
+
+extern struct mail_storage fail_storage;
+extern struct mailbox fail_mailbox;
+extern struct mail_vfuncs fail_mail_vfuncs;
+
+struct mail_storage *fail_mail_storage_create(void);
+
+struct mailbox *
+fail_mailbox_alloc(struct mail_storage *storage, struct mailbox_list *list,
+ const char *vname, enum mailbox_flags flags);
+
+struct mail *
+fail_mailbox_mail_alloc(struct mailbox_transaction_context *t,
+ enum mail_fetch_field wanted_fields,
+ struct mailbox_header_lookup_ctx *wanted_headers);
+
+#endif
diff --git a/src/lib-storage/fail-mail.c b/src/lib-storage/fail-mail.c
new file mode 100644
index 0000000..f236d76
--- /dev/null
+++ b/src/lib-storage/fail-mail.c
@@ -0,0 +1,271 @@
+/* Copyright (c) 2009-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "mail-storage-private.h"
+#include "fail-mail-storage.h"
+
+struct mail *
+fail_mailbox_mail_alloc(struct mailbox_transaction_context *t,
+ enum mail_fetch_field wanted_fields ATTR_UNUSED,
+ struct mailbox_header_lookup_ctx *wanted_headers ATTR_UNUSED)
+{
+ struct mail_private *mail;
+ pool_t pool;
+
+ pool = pool_alloconly_create("fail mail", 1024);
+ mail = p_new(pool, struct mail_private, 1);
+ mail->mail.box = t->box;
+ mail->mail.transaction = t;
+ mail->v = fail_mail_vfuncs;
+ mail->pool = pool;
+ p_array_init(&mail->module_contexts, pool, 5);
+ return &mail->mail;
+}
+
+static void fail_mail_free(struct mail *mail)
+{
+ struct mail_private *pmail = (struct mail_private *)mail;
+
+ pool_unref(&pmail->pool);
+}
+
+static void fail_mail_set_seq(struct mail *mail, uint32_t seq, bool saving)
+{
+ mail->seq = seq;
+ mail->uid = seq;
+ mail->saving = saving;
+
+ mail->expunged = TRUE;
+ mail->has_nuls = FALSE;
+ mail->has_no_nuls = FALSE;
+}
+
+static bool fail_mail_set_uid(struct mail *mail, uint32_t uid)
+{
+ fail_mail_set_seq(mail, uid, FALSE);
+ return TRUE;
+}
+
+static void fail_mail_set_uid_cache_updates(struct mail *mail ATTR_UNUSED,
+ bool set ATTR_UNUSED)
+{
+}
+
+static bool fail_mail_prefetch(struct mail *mail ATTR_UNUSED)
+{
+ return TRUE;
+}
+
+static int fail_mail_precache(struct mail *mail ATTR_UNUSED)
+{
+ return 0;
+}
+
+static void
+fail_mail_add_temp_wanted_fields(struct mail *mail ATTR_UNUSED,
+ enum mail_fetch_field fields ATTR_UNUSED,
+ struct mailbox_header_lookup_ctx *headers ATTR_UNUSED)
+{
+}
+
+static enum mail_flags fail_mail_get_flags(struct mail *mail ATTR_UNUSED)
+{
+ return 0;
+}
+
+static const char *const *
+fail_mail_get_keywords(struct mail *mail ATTR_UNUSED)
+{
+ return t_new(const char *, 1);
+}
+
+static const ARRAY_TYPE(keyword_indexes) *
+fail_mail_get_keyword_indexes(struct mail *mail ATTR_UNUSED)
+{
+ ARRAY_TYPE(keyword_indexes) *kw_indexes;
+
+ kw_indexes = t_new(ARRAY_TYPE(keyword_indexes), 1);
+ t_array_init(kw_indexes, 1);
+ array_append_zero(kw_indexes);
+ return kw_indexes;
+}
+
+static uint64_t fail_mail_get_modseq(struct mail *mail ATTR_UNUSED)
+{
+ return 0;
+}
+
+static int
+fail_mail_get_parts(struct mail *mail ATTR_UNUSED,
+ struct message_part **parts_r ATTR_UNUSED)
+{
+ return -1;
+}
+
+static int
+fail_mail_get_date(struct mail *mail ATTR_UNUSED,
+ time_t *date_r ATTR_UNUSED, int *timezone_r ATTR_UNUSED)
+{
+ return -1;
+}
+
+static int
+fail_mail_get_received_date(struct mail *mail ATTR_UNUSED,
+ time_t *date_r ATTR_UNUSED)
+{
+ return -1;
+}
+
+static int
+fail_mail_get_save_date(struct mail *mail ATTR_UNUSED,
+ time_t *date_r ATTR_UNUSED)
+{
+ return -1;
+}
+
+static int
+fail_mail_get_fail_mail_size(struct mail *mail ATTR_UNUSED,
+ uoff_t *size_r ATTR_UNUSED)
+{
+ return -1;
+}
+
+static int
+fail_mail_get_physical_size(struct mail *mail ATTR_UNUSED,
+ uoff_t *size_r ATTR_UNUSED)
+{
+ return -1;
+}
+
+static int
+fail_mail_get_first_header(struct mail *mail ATTR_UNUSED,
+ const char *field ATTR_UNUSED,
+ bool decode_to_utf8 ATTR_UNUSED,
+ const char **value_r)
+{
+ *value_r = NULL;
+ return 0;
+}
+
+static int
+fail_mail_get_headers(struct mail *mail ATTR_UNUSED,
+ const char *field ATTR_UNUSED,
+ bool decode_to_utf8 ATTR_UNUSED,
+ const char *const **value_r)
+{
+ *value_r = NULL;
+ return 0;
+}
+
+static int
+fail_mail_get_header_stream(struct mail *mail ATTR_UNUSED,
+ struct mailbox_header_lookup_ctx *headers ATTR_UNUSED,
+ struct istream **stream_r ATTR_UNUSED)
+{
+ return -1;
+}
+
+static int
+fail_mail_get_stream(struct mail *mail ATTR_UNUSED, bool get_body ATTR_UNUSED,
+ struct message_size *hdr_size ATTR_UNUSED,
+ struct message_size *body_size ATTR_UNUSED,
+ struct istream **stream_r ATTR_UNUSED)
+{
+ return -1;
+}
+
+static int
+fail_mail_get_binary_stream(struct mail *_mail ATTR_UNUSED,
+ const struct message_part *part ATTR_UNUSED,
+ bool include_hdr ATTR_UNUSED,
+ uoff_t *size_r ATTR_UNUSED,
+ unsigned int *body_lines_r ATTR_UNUSED,
+ bool *binary_r ATTR_UNUSED,
+ struct istream **stream_r ATTR_UNUSED)
+{
+ return -1;
+}
+
+static int
+fail_mail_get_special(struct mail *mail ATTR_UNUSED,
+ enum mail_fetch_field field ATTR_UNUSED,
+ const char **value_r ATTR_UNUSED)
+{
+ return -1;
+}
+
+static int fail_mail_get_backend_mail(struct mail *mail,
+ struct mail **real_mail_r)
+{
+ *real_mail_r = mail;
+ return 0;
+}
+
+static void
+fail_mail_update_flags(struct mail *mail ATTR_UNUSED,
+ enum modify_type modify_type ATTR_UNUSED,
+ enum mail_flags flags ATTR_UNUSED)
+{
+}
+
+static void
+fail_mail_update_keywords(struct mail *mail ATTR_UNUSED,
+ enum modify_type modify_type ATTR_UNUSED,
+ struct mail_keywords *keywords ATTR_UNUSED)
+{
+}
+
+static void fail_mail_update_modseq(struct mail *mail ATTR_UNUSED,
+ uint64_t min_modseq ATTR_UNUSED)
+{
+}
+
+static void fail_mail_expunge(struct mail *mail ATTR_UNUSED)
+{
+}
+
+static void
+fail_mail_set_cache_corrupted(struct mail *mail ATTR_UNUSED,
+ enum mail_fetch_field field ATTR_UNUSED,
+ const char *reason ATTR_UNUSED)
+{
+}
+
+struct mail_vfuncs fail_mail_vfuncs = {
+ NULL,
+ fail_mail_free,
+ fail_mail_set_seq,
+ fail_mail_set_uid,
+ fail_mail_set_uid_cache_updates,
+ fail_mail_prefetch,
+ fail_mail_precache,
+ fail_mail_add_temp_wanted_fields,
+
+ fail_mail_get_flags,
+ fail_mail_get_keywords,
+ fail_mail_get_keyword_indexes,
+ fail_mail_get_modseq,
+ fail_mail_get_modseq,
+ fail_mail_get_parts,
+ fail_mail_get_date,
+ fail_mail_get_received_date,
+ fail_mail_get_save_date,
+ fail_mail_get_fail_mail_size,
+ fail_mail_get_physical_size,
+ fail_mail_get_first_header,
+ fail_mail_get_headers,
+ fail_mail_get_header_stream,
+ fail_mail_get_stream,
+ fail_mail_get_binary_stream,
+ fail_mail_get_special,
+ fail_mail_get_backend_mail,
+ fail_mail_update_flags,
+ fail_mail_update_keywords,
+ fail_mail_update_modseq,
+ fail_mail_update_modseq,
+ NULL,
+ fail_mail_expunge,
+ fail_mail_set_cache_corrupted,
+ NULL,
+};
diff --git a/src/lib-storage/fail-mailbox.c b/src/lib-storage/fail-mailbox.c
new file mode 100644
index 0000000..07ace29
--- /dev/null
+++ b/src/lib-storage/fail-mailbox.c
@@ -0,0 +1,344 @@
+/* Copyright (c) 2009-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "str-sanitize.h"
+#include "mail-storage-private.h"
+#include "mailbox-list-private.h"
+#include "fail-mail-storage.h"
+
+#define TEST_UID_VALIDITY 1
+
+static bool fail_mailbox_is_readonly(struct mailbox *box ATTR_UNUSED)
+{
+ return FALSE;
+}
+
+static int fail_mailbox_enable(struct mailbox *box,
+ enum mailbox_feature features)
+{
+ box->enabled_features = features;
+ return -1;
+}
+
+static int fail_mailbox_exists(struct mailbox *box ATTR_UNUSED,
+ bool auto_boxes ATTR_UNUSED,
+ enum mailbox_existence *existence_r)
+{
+ *existence_r = MAILBOX_EXISTENCE_NONE;
+ return 0;
+}
+
+static int fail_mailbox_open(struct mailbox *box)
+{
+ mail_storage_set_error(box->storage, MAIL_ERROR_NOTFOUND,
+ T_MAIL_ERR_MAILBOX_NOT_FOUND(box->vname));
+ return -1;
+}
+
+static void fail_mailbox_close(struct mailbox *box ATTR_UNUSED)
+{
+}
+
+static void fail_mailbox_free(struct mailbox *box)
+{
+ event_unref(&box->event);
+}
+
+static int
+fail_mailbox_create(struct mailbox *box,
+ const struct mailbox_update *update ATTR_UNUSED,
+ bool directory ATTR_UNUSED)
+{
+ mail_storage_set_error(box->storage, MAIL_ERROR_NOTPOSSIBLE,
+ "Mailbox can't be created");
+ return -1;
+}
+
+static int
+fail_mailbox_update(struct mailbox *box,
+ const struct mailbox_update *update ATTR_UNUSED)
+{
+ mail_storage_set_error(box->storage, MAIL_ERROR_NOTPOSSIBLE,
+ "Mailbox can't be updated");
+ return -1;
+}
+
+static int fail_mailbox_delete(struct mailbox *box)
+{
+ mail_storage_set_error(box->storage, MAIL_ERROR_NOTPOSSIBLE,
+ "Mailbox can't be deleted");
+ return -1;
+}
+
+static int fail_mailbox_rename(struct mailbox *src,
+ struct mailbox *dest ATTR_UNUSED)
+{
+ mail_storage_set_error(src->storage, MAIL_ERROR_NOTPOSSIBLE,
+ "Mailbox can't be renamed");
+ return -1;
+}
+
+static int fail_mailbox_get_status(struct mailbox *box ATTR_UNUSED,
+ enum mailbox_status_items items ATTR_UNUSED,
+ struct mailbox_status *status_r)
+{
+ status_r->uidvalidity = TEST_UID_VALIDITY;
+ status_r->uidnext = 1;
+ mail_storage_set_error(box->storage, MAIL_ERROR_NOTFOUND,
+ T_MAIL_ERR_MAILBOX_NOT_FOUND(box->vname));
+ return -1;
+}
+
+static int
+fail_mailbox_get_metadata(struct mailbox *box,
+ enum mailbox_metadata_items items ATTR_UNUSED,
+ struct mailbox_metadata *metadata_r ATTR_UNUSED)
+{
+ mail_storage_set_error(box->storage, MAIL_ERROR_NOTFOUND,
+ T_MAIL_ERR_MAILBOX_NOT_FOUND(box->vname));
+ return -1;
+}
+
+static int fail_mailbox_set_subscribed(struct mailbox *box,
+ bool set ATTR_UNUSED)
+{
+ mail_storage_set_error(box->storage, MAIL_ERROR_NOTPOSSIBLE,
+ "Mailbox can't be subscribed");
+ return -1;
+}
+
+static struct mailbox_sync_context *
+fail_mailbox_sync_init(struct mailbox *box,
+ enum mailbox_sync_flags flags ATTR_UNUSED)
+{
+ struct mailbox_sync_context *ctx;
+
+ ctx = i_new(struct mailbox_sync_context, 1);
+ ctx->box = box;
+ return ctx;
+}
+
+static bool
+fail_mailbox_sync_next(struct mailbox_sync_context *ctx ATTR_UNUSED,
+ struct mailbox_sync_rec *sync_rec_r ATTR_UNUSED)
+{
+ return FALSE;
+}
+
+static int
+fail_mailbox_sync_deinit(struct mailbox_sync_context *ctx,
+ struct mailbox_sync_status *status_r ATTR_UNUSED)
+{
+ mail_storage_set_error(ctx->box->storage, MAIL_ERROR_NOTFOUND,
+ T_MAIL_ERR_MAILBOX_NOT_FOUND(ctx->box->vname));
+ i_free(ctx);
+ return -1;
+}
+
+static void fail_mailbox_notify_changes(struct mailbox *box ATTR_UNUSED)
+{
+}
+
+static struct mailbox_transaction_context *
+fail_mailbox_transaction_begin(struct mailbox *box,
+ enum mailbox_transaction_flags flags,
+ const char *reason ATTR_UNUSED)
+{
+ struct mailbox_transaction_context *ctx;
+
+ ctx = i_new(struct mailbox_transaction_context, 1);
+ ctx->box = box;
+ ctx->flags = flags;
+ i_array_init(&ctx->module_contexts, 5);
+ return ctx;
+}
+
+static void
+fail_mailbox_transaction_rollback(struct mailbox_transaction_context *t)
+{
+ array_free(&t->module_contexts);
+ i_free(t);
+}
+
+static int
+fail_mailbox_transaction_commit(struct mailbox_transaction_context *t,
+ struct mail_transaction_commit_changes *changes_r)
+{
+ changes_r->uid_validity = TEST_UID_VALIDITY;
+ fail_mailbox_transaction_rollback(t);
+ return 0;
+}
+
+static struct mail_search_context *
+fail_mailbox_search_init(struct mailbox_transaction_context *t,
+ struct mail_search_args *args,
+ const enum mail_sort_type *sort_program ATTR_UNUSED,
+ enum mail_fetch_field wanted_fields ATTR_UNUSED,
+ struct mailbox_header_lookup_ctx *wanted_headers ATTR_UNUSED)
+{
+ struct mail_search_context *ctx;
+
+ ctx = i_new(struct mail_search_context, 1);
+ ctx->transaction = t;
+ ctx->args = args;
+
+ i_array_init(&ctx->results, 5);
+ i_array_init(&ctx->module_contexts, 5);
+ return ctx;
+}
+
+static int fail_mailbox_search_deinit(struct mail_search_context *ctx)
+{
+ array_free(&ctx->results);
+ array_free(&ctx->module_contexts);
+ i_free(ctx);
+ return 0;
+}
+
+static bool
+fail_mailbox_search_next_nonblock(struct mail_search_context *ctx ATTR_UNUSED,
+ struct mail **mail_r, bool *tryagain_r)
+{
+ *tryagain_r = FALSE;
+ *mail_r = NULL;
+ return FALSE;
+}
+
+static bool
+fail_mailbox_search_next_update_seq(struct mail_search_context *ctx ATTR_UNUSED)
+{
+ return FALSE;
+}
+
+static int
+fail_mailbox_search_next_match_mail(struct mail_search_context *ctx ATTR_UNUSED,
+ struct mail *mail ATTR_UNUSED)
+{
+ return -1;
+}
+
+static struct mail_save_context *
+fail_mailbox_save_alloc(struct mailbox_transaction_context *t)
+{
+ struct mail_save_context *ctx;
+
+ ctx = i_new(struct mail_save_context, 1);
+ ctx->transaction = t;
+ return ctx;
+}
+
+static int
+fail_mailbox_save_begin(struct mail_save_context *ctx ATTR_UNUSED,
+ struct istream *input ATTR_UNUSED)
+{
+ return -1;
+}
+
+static int
+fail_mailbox_save_continue(struct mail_save_context *ctx ATTR_UNUSED)
+{
+ return -1;
+}
+
+static int
+fail_mailbox_save_finish(struct mail_save_context *ctx ATTR_UNUSED)
+{
+ return -1;
+}
+
+static void
+fail_mailbox_save_cancel(struct mail_save_context *ctx ATTR_UNUSED)
+{
+}
+
+static int
+fail_mailbox_copy(struct mail_save_context *ctx ATTR_UNUSED,
+ struct mail *mail ATTR_UNUSED)
+{
+ return -1;
+}
+
+static bool fail_mailbox_is_inconsistent(struct mailbox *box ATTR_UNUSED)
+{
+ return FALSE;
+}
+
+struct mailbox fail_mailbox = {
+ .v = {
+ fail_mailbox_is_readonly,
+ fail_mailbox_enable,
+ fail_mailbox_exists,
+ fail_mailbox_open,
+ fail_mailbox_close,
+ fail_mailbox_free,
+ fail_mailbox_create,
+ fail_mailbox_update,
+ fail_mailbox_delete,
+ fail_mailbox_rename,
+ fail_mailbox_get_status,
+ fail_mailbox_get_metadata,
+ fail_mailbox_set_subscribed,
+ NULL,
+ NULL,
+ NULL,
+ NULL,
+ NULL,
+ NULL,
+ NULL,
+ fail_mailbox_sync_init,
+ fail_mailbox_sync_next,
+ fail_mailbox_sync_deinit,
+ NULL,
+ fail_mailbox_notify_changes,
+ fail_mailbox_transaction_begin,
+ fail_mailbox_transaction_commit,
+ fail_mailbox_transaction_rollback,
+ NULL,
+ fail_mailbox_mail_alloc,
+ fail_mailbox_search_init,
+ fail_mailbox_search_deinit,
+ fail_mailbox_search_next_nonblock,
+ fail_mailbox_search_next_update_seq,
+ fail_mailbox_search_next_match_mail,
+ fail_mailbox_save_alloc,
+ fail_mailbox_save_begin,
+ fail_mailbox_save_continue,
+ fail_mailbox_save_finish,
+ fail_mailbox_save_cancel,
+ fail_mailbox_copy,
+ NULL,
+ NULL,
+ NULL,
+ fail_mailbox_is_inconsistent
+ }
+};
+
+struct mailbox *
+fail_mailbox_alloc(struct mail_storage *storage, struct mailbox_list *list,
+ const char *vname, enum mailbox_flags flags)
+{
+ struct mailbox *box;
+ pool_t pool;
+
+ pool = pool_alloconly_create("fail mailbox", 1024+512);
+ box = p_new(pool, struct mailbox, 1);
+ *box = fail_mailbox;
+ box->vname = p_strdup(pool, vname);
+ box->name = p_strdup(pool, mailbox_list_get_storage_name(list, vname));
+ box->storage = storage;
+ box->list = list;
+
+ box->pool = pool;
+ box->flags = flags;
+
+ box->event = event_create(box->storage->event);
+ event_add_category(box->event, &event_category_mailbox);
+ event_add_str(box->event, "mailbox", box->vname);
+ event_set_append_log_prefix(box->event,
+ t_strdup_printf("Mailbox %s: ", str_sanitize(box->vname, 128)));
+
+ p_array_init(&box->search_results, pool, 16);
+ p_array_init(&box->module_contexts, pool, 5);
+ return box;
+}
diff --git a/src/lib-storage/index/Makefile.am b/src/lib-storage/index/Makefile.am
new file mode 100644
index 0000000..23461e2
--- /dev/null
+++ b/src/lib-storage/index/Makefile.am
@@ -0,0 +1,58 @@
+SUBDIRS = maildir mbox dbox-common dbox-multi dbox-single imapc pop3c raw shared
+
+noinst_LTLIBRARIES = libstorage_index.la
+
+AM_CPPFLAGS = \
+ -I$(top_srcdir)/src/lib \
+ -I$(top_srcdir)/src/lib-test \
+ -I$(top_srcdir)/src/lib-dict \
+ -I$(top_srcdir)/src/lib-fs \
+ -I$(top_srcdir)/src/lib-mail \
+ -I$(top_srcdir)/src/lib-imap \
+ -I$(top_srcdir)/src/lib-index \
+ -I$(top_srcdir)/src/lib-storage
+
+libstorage_index_la_SOURCES = \
+ istream-mail.c \
+ index-attachment.c \
+ index-attribute.c \
+ index-mail.c \
+ index-mail-binary.c \
+ index-mail-headers.c \
+ index-mailbox-size.c \
+ index-pop3-uidl.c \
+ index-rebuild.c \
+ index-search.c \
+ index-search-mime.c \
+ index-search-result.c \
+ index-sort.c \
+ index-sort-string.c \
+ index-status.c \
+ index-storage.c \
+ index-sync.c \
+ index-sync-changes.c \
+ index-sync-pvt.c \
+ index-sync-search.c \
+ index-thread.c \
+ index-thread-finish.c \
+ index-thread-links.c \
+ index-transaction.c
+
+headers = \
+ istream-mail.h \
+ index-attachment.h \
+ index-mail.h \
+ index-mailbox-size.h \
+ index-pop3-uidl.h \
+ index-rebuild.h \
+ index-search-private.h \
+ index-search-result.h \
+ index-sort.h \
+ index-sort-private.h \
+ index-storage.h \
+ index-sync-changes.h \
+ index-sync-private.h \
+ index-thread-private.h
+
+pkginc_libdir=$(pkgincludedir)
+pkginc_lib_HEADERS = $(headers)
diff --git a/src/lib-storage/index/Makefile.in b/src/lib-storage/index/Makefile.in
new file mode 100644
index 0000000..3069140
--- /dev/null
+++ b/src/lib-storage/index/Makefile.in
@@ -0,0 +1,1061 @@
+# Makefile.in generated by automake 1.16.1 from Makefile.am.
+# @configure_input@
+
+# Copyright (C) 1994-2018 Free Software Foundation, Inc.
+
+# This Makefile.in is free software; the Free Software Foundation
+# gives unlimited permission to copy and/or distribute it,
+# with or without modifications, as long as this notice is preserved.
+
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY, to the extent permitted by law; without
+# even the implied warranty of MERCHANTABILITY or FITNESS FOR A
+# PARTICULAR PURPOSE.
+
+@SET_MAKE@
+
+
+VPATH = @srcdir@
+am__is_gnu_make = { \
+ if test -z '$(MAKELEVEL)'; then \
+ false; \
+ elif test -n '$(MAKE_HOST)'; then \
+ true; \
+ elif test -n '$(MAKE_VERSION)' && test -n '$(CURDIR)'; then \
+ true; \
+ else \
+ false; \
+ fi; \
+}
+am__make_running_with_option = \
+ case $${target_option-} in \
+ ?) ;; \
+ *) echo "am__make_running_with_option: internal error: invalid" \
+ "target option '$${target_option-}' specified" >&2; \
+ exit 1;; \
+ esac; \
+ has_opt=no; \
+ sane_makeflags=$$MAKEFLAGS; \
+ if $(am__is_gnu_make); then \
+ sane_makeflags=$$MFLAGS; \
+ else \
+ case $$MAKEFLAGS in \
+ *\\[\ \ ]*) \
+ bs=\\; \
+ sane_makeflags=`printf '%s\n' "$$MAKEFLAGS" \
+ | sed "s/$$bs$$bs[$$bs $$bs ]*//g"`;; \
+ esac; \
+ fi; \
+ skip_next=no; \
+ strip_trailopt () \
+ { \
+ flg=`printf '%s\n' "$$flg" | sed "s/$$1.*$$//"`; \
+ }; \
+ for flg in $$sane_makeflags; do \
+ test $$skip_next = yes && { skip_next=no; continue; }; \
+ case $$flg in \
+ *=*|--*) continue;; \
+ -*I) strip_trailopt 'I'; skip_next=yes;; \
+ -*I?*) strip_trailopt 'I';; \
+ -*O) strip_trailopt 'O'; skip_next=yes;; \
+ -*O?*) strip_trailopt 'O';; \
+ -*l) strip_trailopt 'l'; skip_next=yes;; \
+ -*l?*) strip_trailopt 'l';; \
+ -[dEDm]) skip_next=yes;; \
+ -[JT]) skip_next=yes;; \
+ esac; \
+ case $$flg in \
+ *$$target_option*) has_opt=yes; break;; \
+ esac; \
+ done; \
+ test $$has_opt = yes
+am__make_dryrun = (target_option=n; $(am__make_running_with_option))
+am__make_keepgoing = (target_option=k; $(am__make_running_with_option))
+pkgdatadir = $(datadir)/@PACKAGE@
+pkgincludedir = $(includedir)/@PACKAGE@
+pkglibdir = $(libdir)/@PACKAGE@
+pkglibexecdir = $(libexecdir)/@PACKAGE@
+am__cd = CDPATH="$${ZSH_VERSION+.}$(PATH_SEPARATOR)" && cd
+install_sh_DATA = $(install_sh) -c -m 644
+install_sh_PROGRAM = $(install_sh) -c
+install_sh_SCRIPT = $(install_sh) -c
+INSTALL_HEADER = $(INSTALL_DATA)
+transform = $(program_transform_name)
+NORMAL_INSTALL = :
+PRE_INSTALL = :
+POST_INSTALL = :
+NORMAL_UNINSTALL = :
+PRE_UNINSTALL = :
+POST_UNINSTALL = :
+build_triplet = @build@
+host_triplet = @host@
+subdir = src/lib-storage/index
+ACLOCAL_M4 = $(top_srcdir)/aclocal.m4
+am__aclocal_m4_deps = $(top_srcdir)/m4/ac_checktype2.m4 \
+ $(top_srcdir)/m4/ac_typeof.m4 $(top_srcdir)/m4/arc4random.m4 \
+ $(top_srcdir)/m4/blockdev.m4 $(top_srcdir)/m4/c99_vsnprintf.m4 \
+ $(top_srcdir)/m4/clock_gettime.m4 $(top_srcdir)/m4/crypt.m4 \
+ $(top_srcdir)/m4/crypt_xpg6.m4 $(top_srcdir)/m4/dbqlk.m4 \
+ $(top_srcdir)/m4/dirent_dtype.m4 $(top_srcdir)/m4/dovecot.m4 \
+ $(top_srcdir)/m4/fd_passing.m4 $(top_srcdir)/m4/fdatasync.m4 \
+ $(top_srcdir)/m4/flexible_array_member.m4 \
+ $(top_srcdir)/m4/glibc.m4 $(top_srcdir)/m4/gmtime_max.m4 \
+ $(top_srcdir)/m4/gmtime_tm_gmtoff.m4 \
+ $(top_srcdir)/m4/ioloop.m4 $(top_srcdir)/m4/iovec.m4 \
+ $(top_srcdir)/m4/ipv6.m4 $(top_srcdir)/m4/libcap.m4 \
+ $(top_srcdir)/m4/libtool.m4 $(top_srcdir)/m4/libwrap.m4 \
+ $(top_srcdir)/m4/linux_mremap.m4 $(top_srcdir)/m4/ltoptions.m4 \
+ $(top_srcdir)/m4/ltsugar.m4 $(top_srcdir)/m4/ltversion.m4 \
+ $(top_srcdir)/m4/lt~obsolete.m4 $(top_srcdir)/m4/mmap_write.m4 \
+ $(top_srcdir)/m4/mntctl.m4 $(top_srcdir)/m4/modules.m4 \
+ $(top_srcdir)/m4/notify.m4 $(top_srcdir)/m4/nsl.m4 \
+ $(top_srcdir)/m4/off_t_max.m4 $(top_srcdir)/m4/pkg.m4 \
+ $(top_srcdir)/m4/pr_set_dumpable.m4 \
+ $(top_srcdir)/m4/q_quotactl.m4 $(top_srcdir)/m4/quota.m4 \
+ $(top_srcdir)/m4/random.m4 $(top_srcdir)/m4/rlimit.m4 \
+ $(top_srcdir)/m4/sendfile.m4 $(top_srcdir)/m4/size_t_signed.m4 \
+ $(top_srcdir)/m4/sockpeercred.m4 $(top_srcdir)/m4/sql.m4 \
+ $(top_srcdir)/m4/ssl.m4 $(top_srcdir)/m4/st_tim.m4 \
+ $(top_srcdir)/m4/static_array.m4 $(top_srcdir)/m4/test_with.m4 \
+ $(top_srcdir)/m4/time_t.m4 $(top_srcdir)/m4/typeof.m4 \
+ $(top_srcdir)/m4/typeof_dev_t.m4 \
+ $(top_srcdir)/m4/uoff_t_max.m4 $(top_srcdir)/m4/vararg.m4 \
+ $(top_srcdir)/m4/want_apparmor.m4 \
+ $(top_srcdir)/m4/want_bsdauth.m4 \
+ $(top_srcdir)/m4/want_bzlib.m4 \
+ $(top_srcdir)/m4/want_cassandra.m4 \
+ $(top_srcdir)/m4/want_cdb.m4 \
+ $(top_srcdir)/m4/want_checkpassword.m4 \
+ $(top_srcdir)/m4/want_clucene.m4 $(top_srcdir)/m4/want_db.m4 \
+ $(top_srcdir)/m4/want_gssapi.m4 $(top_srcdir)/m4/want_icu.m4 \
+ $(top_srcdir)/m4/want_ldap.m4 $(top_srcdir)/m4/want_lua.m4 \
+ $(top_srcdir)/m4/want_lz4.m4 $(top_srcdir)/m4/want_lzma.m4 \
+ $(top_srcdir)/m4/want_mysql.m4 $(top_srcdir)/m4/want_pam.m4 \
+ $(top_srcdir)/m4/want_passwd.m4 $(top_srcdir)/m4/want_pgsql.m4 \
+ $(top_srcdir)/m4/want_prefetch.m4 \
+ $(top_srcdir)/m4/want_shadow.m4 \
+ $(top_srcdir)/m4/want_sodium.m4 $(top_srcdir)/m4/want_solr.m4 \
+ $(top_srcdir)/m4/want_sqlite.m4 \
+ $(top_srcdir)/m4/want_stemmer.m4 \
+ $(top_srcdir)/m4/want_systemd.m4 \
+ $(top_srcdir)/m4/want_textcat.m4 \
+ $(top_srcdir)/m4/want_unwind.m4 $(top_srcdir)/m4/want_zlib.m4 \
+ $(top_srcdir)/m4/want_zstd.m4 $(top_srcdir)/configure.ac
+am__configure_deps = $(am__aclocal_m4_deps) $(CONFIGURE_DEPENDENCIES) \
+ $(ACLOCAL_M4)
+DIST_COMMON = $(srcdir)/Makefile.am $(pkginc_lib_HEADERS) \
+ $(am__DIST_COMMON)
+mkinstalldirs = $(install_sh) -d
+CONFIG_HEADER = $(top_builddir)/config.h
+CONFIG_CLEAN_FILES =
+CONFIG_CLEAN_VPATH_FILES =
+LTLIBRARIES = $(noinst_LTLIBRARIES)
+libstorage_index_la_LIBADD =
+am_libstorage_index_la_OBJECTS = istream-mail.lo index-attachment.lo \
+ index-attribute.lo index-mail.lo index-mail-binary.lo \
+ index-mail-headers.lo index-mailbox-size.lo index-pop3-uidl.lo \
+ index-rebuild.lo index-search.lo index-search-mime.lo \
+ index-search-result.lo index-sort.lo index-sort-string.lo \
+ index-status.lo index-storage.lo index-sync.lo \
+ index-sync-changes.lo index-sync-pvt.lo index-sync-search.lo \
+ index-thread.lo index-thread-finish.lo index-thread-links.lo \
+ index-transaction.lo
+libstorage_index_la_OBJECTS = $(am_libstorage_index_la_OBJECTS)
+AM_V_lt = $(am__v_lt_@AM_V@)
+am__v_lt_ = $(am__v_lt_@AM_DEFAULT_V@)
+am__v_lt_0 = --silent
+am__v_lt_1 =
+AM_V_P = $(am__v_P_@AM_V@)
+am__v_P_ = $(am__v_P_@AM_DEFAULT_V@)
+am__v_P_0 = false
+am__v_P_1 = :
+AM_V_GEN = $(am__v_GEN_@AM_V@)
+am__v_GEN_ = $(am__v_GEN_@AM_DEFAULT_V@)
+am__v_GEN_0 = @echo " GEN " $@;
+am__v_GEN_1 =
+AM_V_at = $(am__v_at_@AM_V@)
+am__v_at_ = $(am__v_at_@AM_DEFAULT_V@)
+am__v_at_0 = @
+am__v_at_1 =
+DEFAULT_INCLUDES = -I.@am__isrc@ -I$(top_builddir)
+depcomp = $(SHELL) $(top_srcdir)/depcomp
+am__maybe_remake_depfiles = depfiles
+am__depfiles_remade = ./$(DEPDIR)/index-attachment.Plo \
+ ./$(DEPDIR)/index-attribute.Plo \
+ ./$(DEPDIR)/index-mail-binary.Plo \
+ ./$(DEPDIR)/index-mail-headers.Plo ./$(DEPDIR)/index-mail.Plo \
+ ./$(DEPDIR)/index-mailbox-size.Plo \
+ ./$(DEPDIR)/index-pop3-uidl.Plo ./$(DEPDIR)/index-rebuild.Plo \
+ ./$(DEPDIR)/index-search-mime.Plo \
+ ./$(DEPDIR)/index-search-result.Plo \
+ ./$(DEPDIR)/index-search.Plo ./$(DEPDIR)/index-sort-string.Plo \
+ ./$(DEPDIR)/index-sort.Plo ./$(DEPDIR)/index-status.Plo \
+ ./$(DEPDIR)/index-storage.Plo \
+ ./$(DEPDIR)/index-sync-changes.Plo \
+ ./$(DEPDIR)/index-sync-pvt.Plo \
+ ./$(DEPDIR)/index-sync-search.Plo ./$(DEPDIR)/index-sync.Plo \
+ ./$(DEPDIR)/index-thread-finish.Plo \
+ ./$(DEPDIR)/index-thread-links.Plo \
+ ./$(DEPDIR)/index-thread.Plo ./$(DEPDIR)/index-transaction.Plo \
+ ./$(DEPDIR)/istream-mail.Plo
+am__mv = mv -f
+COMPILE = $(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) \
+ $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS)
+LTCOMPILE = $(LIBTOOL) $(AM_V_lt) --tag=CC $(AM_LIBTOOLFLAGS) \
+ $(LIBTOOLFLAGS) --mode=compile $(CC) $(DEFS) \
+ $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) $(CPPFLAGS) \
+ $(AM_CFLAGS) $(CFLAGS)
+AM_V_CC = $(am__v_CC_@AM_V@)
+am__v_CC_ = $(am__v_CC_@AM_DEFAULT_V@)
+am__v_CC_0 = @echo " CC " $@;
+am__v_CC_1 =
+CCLD = $(CC)
+LINK = $(LIBTOOL) $(AM_V_lt) --tag=CC $(AM_LIBTOOLFLAGS) \
+ $(LIBTOOLFLAGS) --mode=link $(CCLD) $(AM_CFLAGS) $(CFLAGS) \
+ $(AM_LDFLAGS) $(LDFLAGS) -o $@
+AM_V_CCLD = $(am__v_CCLD_@AM_V@)
+am__v_CCLD_ = $(am__v_CCLD_@AM_DEFAULT_V@)
+am__v_CCLD_0 = @echo " CCLD " $@;
+am__v_CCLD_1 =
+SOURCES = $(libstorage_index_la_SOURCES)
+DIST_SOURCES = $(libstorage_index_la_SOURCES)
+RECURSIVE_TARGETS = all-recursive check-recursive cscopelist-recursive \
+ ctags-recursive dvi-recursive html-recursive info-recursive \
+ install-data-recursive install-dvi-recursive \
+ install-exec-recursive install-html-recursive \
+ install-info-recursive install-pdf-recursive \
+ install-ps-recursive install-recursive installcheck-recursive \
+ installdirs-recursive pdf-recursive ps-recursive \
+ tags-recursive uninstall-recursive
+am__can_run_installinfo = \
+ case $$AM_UPDATE_INFO_DIR in \
+ n|no|NO) false;; \
+ *) (install-info --version) >/dev/null 2>&1;; \
+ esac
+am__vpath_adj_setup = srcdirstrip=`echo "$(srcdir)" | sed 's|.|.|g'`;
+am__vpath_adj = case $$p in \
+ $(srcdir)/*) f=`echo "$$p" | sed "s|^$$srcdirstrip/||"`;; \
+ *) f=$$p;; \
+ esac;
+am__strip_dir = f=`echo $$p | sed -e 's|^.*/||'`;
+am__install_max = 40
+am__nobase_strip_setup = \
+ srcdirstrip=`echo "$(srcdir)" | sed 's/[].[^$$\\*|]/\\\\&/g'`
+am__nobase_strip = \
+ for p in $$list; do echo "$$p"; done | sed -e "s|$$srcdirstrip/||"
+am__nobase_list = $(am__nobase_strip_setup); \
+ for p in $$list; do echo "$$p $$p"; done | \
+ sed "s| $$srcdirstrip/| |;"' / .*\//!s/ .*/ ./; s,\( .*\)/[^/]*$$,\1,' | \
+ $(AWK) 'BEGIN { files["."] = "" } { files[$$2] = files[$$2] " " $$1; \
+ if (++n[$$2] == $(am__install_max)) \
+ { print $$2, files[$$2]; n[$$2] = 0; files[$$2] = "" } } \
+ END { for (dir in files) print dir, files[dir] }'
+am__base_list = \
+ sed '$$!N;$$!N;$$!N;$$!N;$$!N;$$!N;$$!N;s/\n/ /g' | \
+ sed '$$!N;$$!N;$$!N;$$!N;s/\n/ /g'
+am__uninstall_files_from_dir = { \
+ test -z "$$files" \
+ || { test ! -d "$$dir" && test ! -f "$$dir" && test ! -r "$$dir"; } \
+ || { echo " ( cd '$$dir' && rm -f" $$files ")"; \
+ $(am__cd) "$$dir" && rm -f $$files; }; \
+ }
+am__installdirs = "$(DESTDIR)$(pkginc_libdir)"
+HEADERS = $(pkginc_lib_HEADERS)
+RECURSIVE_CLEAN_TARGETS = mostlyclean-recursive clean-recursive \
+ distclean-recursive maintainer-clean-recursive
+am__recursive_targets = \
+ $(RECURSIVE_TARGETS) \
+ $(RECURSIVE_CLEAN_TARGETS) \
+ $(am__extra_recursive_targets)
+AM_RECURSIVE_TARGETS = $(am__recursive_targets:-recursive=) TAGS CTAGS \
+ distdir distdir-am
+am__tagged_files = $(HEADERS) $(SOURCES) $(TAGS_FILES) $(LISP)
+# Read a list of newline-separated strings from the standard input,
+# and print each of them once, without duplicates. Input order is
+# *not* preserved.
+am__uniquify_input = $(AWK) '\
+ BEGIN { nonempty = 0; } \
+ { items[$$0] = 1; nonempty = 1; } \
+ END { if (nonempty) { for (i in items) print i; }; } \
+'
+# Make sure the list of sources is unique. This is necessary because,
+# e.g., the same source file might be shared among _SOURCES variables
+# for different programs/libraries.
+am__define_uniq_tagged_files = \
+ list='$(am__tagged_files)'; \
+ unique=`for i in $$list; do \
+ if test -f "$$i"; then echo $$i; else echo $(srcdir)/$$i; fi; \
+ done | $(am__uniquify_input)`
+ETAGS = etags
+CTAGS = ctags
+DIST_SUBDIRS = $(SUBDIRS)
+am__DIST_COMMON = $(srcdir)/Makefile.in $(top_srcdir)/depcomp
+DISTFILES = $(DIST_COMMON) $(DIST_SOURCES) $(TEXINFOS) $(EXTRA_DIST)
+am__relativize = \
+ dir0=`pwd`; \
+ sed_first='s,^\([^/]*\)/.*$$,\1,'; \
+ sed_rest='s,^[^/]*/*,,'; \
+ sed_last='s,^.*/\([^/]*\)$$,\1,'; \
+ sed_butlast='s,/*[^/]*$$,,'; \
+ while test -n "$$dir1"; do \
+ first=`echo "$$dir1" | sed -e "$$sed_first"`; \
+ if test "$$first" != "."; then \
+ if test "$$first" = ".."; then \
+ dir2=`echo "$$dir0" | sed -e "$$sed_last"`/"$$dir2"; \
+ dir0=`echo "$$dir0" | sed -e "$$sed_butlast"`; \
+ else \
+ first2=`echo "$$dir2" | sed -e "$$sed_first"`; \
+ if test "$$first2" = "$$first"; then \
+ dir2=`echo "$$dir2" | sed -e "$$sed_rest"`; \
+ else \
+ dir2="../$$dir2"; \
+ fi; \
+ dir0="$$dir0"/"$$first"; \
+ fi; \
+ fi; \
+ dir1=`echo "$$dir1" | sed -e "$$sed_rest"`; \
+ done; \
+ reldir="$$dir2"
+ACLOCAL = @ACLOCAL@
+ACLOCAL_AMFLAGS = @ACLOCAL_AMFLAGS@
+AMTAR = @AMTAR@
+AM_DEFAULT_VERBOSITY = @AM_DEFAULT_VERBOSITY@
+APPARMOR_LIBS = @APPARMOR_LIBS@
+AR = @AR@
+AUTH_CFLAGS = @AUTH_CFLAGS@
+AUTH_LIBS = @AUTH_LIBS@
+AUTOCONF = @AUTOCONF@
+AUTOHEADER = @AUTOHEADER@
+AUTOMAKE = @AUTOMAKE@
+AWK = @AWK@
+BINARY_CFLAGS = @BINARY_CFLAGS@
+BINARY_LDFLAGS = @BINARY_LDFLAGS@
+BISON = @BISON@
+CASSANDRA_CFLAGS = @CASSANDRA_CFLAGS@
+CASSANDRA_LIBS = @CASSANDRA_LIBS@
+CC = @CC@
+CCDEPMODE = @CCDEPMODE@
+CDB_LIBS = @CDB_LIBS@
+CFLAGS = @CFLAGS@
+CLUCENE_CFLAGS = @CLUCENE_CFLAGS@
+CLUCENE_LIBS = @CLUCENE_LIBS@
+COMPRESS_LIBS = @COMPRESS_LIBS@
+CPP = @CPP@
+CPPFLAGS = @CPPFLAGS@
+CRYPT_LIBS = @CRYPT_LIBS@
+CXX = @CXX@
+CXXCPP = @CXXCPP@
+CXXDEPMODE = @CXXDEPMODE@
+CXXFLAGS = @CXXFLAGS@
+CYGPATH_W = @CYGPATH_W@
+DEFS = @DEFS@
+DEPDIR = @DEPDIR@
+DICT_LIBS = @DICT_LIBS@
+DLLIB = @DLLIB@
+DLLTOOL = @DLLTOOL@
+DSYMUTIL = @DSYMUTIL@
+DUMPBIN = @DUMPBIN@
+ECHO_C = @ECHO_C@
+ECHO_N = @ECHO_N@
+ECHO_T = @ECHO_T@
+EGREP = @EGREP@
+EXEEXT = @EXEEXT@
+FGREP = @FGREP@
+FLEX = @FLEX@
+FUZZER_CPPFLAGS = @FUZZER_CPPFLAGS@
+FUZZER_LDFLAGS = @FUZZER_LDFLAGS@
+GREP = @GREP@
+INSTALL = @INSTALL@
+INSTALL_DATA = @INSTALL_DATA@
+INSTALL_PROGRAM = @INSTALL_PROGRAM@
+INSTALL_SCRIPT = @INSTALL_SCRIPT@
+INSTALL_STRIP_PROGRAM = @INSTALL_STRIP_PROGRAM@
+KRB5CONFIG = @KRB5CONFIG@
+KRB5_CFLAGS = @KRB5_CFLAGS@
+KRB5_LIBS = @KRB5_LIBS@
+LD = @LD@
+LDAP_LIBS = @LDAP_LIBS@
+LDFLAGS = @LDFLAGS@
+LD_NO_WHOLE_ARCHIVE = @LD_NO_WHOLE_ARCHIVE@
+LD_WHOLE_ARCHIVE = @LD_WHOLE_ARCHIVE@
+LIBCAP = @LIBCAP@
+LIBDOVECOT = @LIBDOVECOT@
+LIBDOVECOT_COMPRESS = @LIBDOVECOT_COMPRESS@
+LIBDOVECOT_DEPS = @LIBDOVECOT_DEPS@
+LIBDOVECOT_DSYNC = @LIBDOVECOT_DSYNC@
+LIBDOVECOT_LA_LIBS = @LIBDOVECOT_LA_LIBS@
+LIBDOVECOT_LDA = @LIBDOVECOT_LDA@
+LIBDOVECOT_LDAP = @LIBDOVECOT_LDAP@
+LIBDOVECOT_LIBFTS = @LIBDOVECOT_LIBFTS@
+LIBDOVECOT_LIBFTS_DEPS = @LIBDOVECOT_LIBFTS_DEPS@
+LIBDOVECOT_LOGIN = @LIBDOVECOT_LOGIN@
+LIBDOVECOT_LUA = @LIBDOVECOT_LUA@
+LIBDOVECOT_LUA_DEPS = @LIBDOVECOT_LUA_DEPS@
+LIBDOVECOT_SQL = @LIBDOVECOT_SQL@
+LIBDOVECOT_STORAGE = @LIBDOVECOT_STORAGE@
+LIBDOVECOT_STORAGE_DEPS = @LIBDOVECOT_STORAGE_DEPS@
+LIBEXTTEXTCAT_CFLAGS = @LIBEXTTEXTCAT_CFLAGS@
+LIBEXTTEXTCAT_LIBS = @LIBEXTTEXTCAT_LIBS@
+LIBICONV = @LIBICONV@
+LIBICU_CFLAGS = @LIBICU_CFLAGS@
+LIBICU_LIBS = @LIBICU_LIBS@
+LIBOBJS = @LIBOBJS@
+LIBS = @LIBS@
+LIBSODIUM_CFLAGS = @LIBSODIUM_CFLAGS@
+LIBSODIUM_LIBS = @LIBSODIUM_LIBS@
+LIBTIRPC_CFLAGS = @LIBTIRPC_CFLAGS@
+LIBTIRPC_LIBS = @LIBTIRPC_LIBS@
+LIBTOOL = @LIBTOOL@
+LIBUNWIND_CFLAGS = @LIBUNWIND_CFLAGS@
+LIBUNWIND_LIBS = @LIBUNWIND_LIBS@
+LIBWRAP_LIBS = @LIBWRAP_LIBS@
+LINKED_STORAGE_LDADD = @LINKED_STORAGE_LDADD@
+LIPO = @LIPO@
+LN_S = @LN_S@
+LTLIBICONV = @LTLIBICONV@
+LTLIBOBJS = @LTLIBOBJS@
+LT_SYS_LIBRARY_PATH = @LT_SYS_LIBRARY_PATH@
+LUA_CFLAGS = @LUA_CFLAGS@
+LUA_LIBS = @LUA_LIBS@
+MAINT = @MAINT@
+MAKEINFO = @MAKEINFO@
+MANIFEST_TOOL = @MANIFEST_TOOL@
+MKDIR_P = @MKDIR_P@
+MODULE_LIBS = @MODULE_LIBS@
+MODULE_SUFFIX = @MODULE_SUFFIX@
+MYSQL_CFLAGS = @MYSQL_CFLAGS@
+MYSQL_CONFIG = @MYSQL_CONFIG@
+MYSQL_LIBS = @MYSQL_LIBS@
+NM = @NM@
+NMEDIT = @NMEDIT@
+NOPLUGIN_LDFLAGS = @NOPLUGIN_LDFLAGS@
+OBJDUMP = @OBJDUMP@
+OBJEXT = @OBJEXT@
+OTOOL = @OTOOL@
+OTOOL64 = @OTOOL64@
+PACKAGE = @PACKAGE@
+PACKAGE_BUGREPORT = @PACKAGE_BUGREPORT@
+PACKAGE_NAME = @PACKAGE_NAME@
+PACKAGE_STRING = @PACKAGE_STRING@
+PACKAGE_TARNAME = @PACKAGE_TARNAME@
+PACKAGE_URL = @PACKAGE_URL@
+PACKAGE_VERSION = @PACKAGE_VERSION@
+PANDOC = @PANDOC@
+PATH_SEPARATOR = @PATH_SEPARATOR@
+PGSQL_CFLAGS = @PGSQL_CFLAGS@
+PGSQL_LIBS = @PGSQL_LIBS@
+PG_CONFIG = @PG_CONFIG@
+PIE_CFLAGS = @PIE_CFLAGS@
+PIE_LDFLAGS = @PIE_LDFLAGS@
+PKG_CONFIG = @PKG_CONFIG@
+PKG_CONFIG_LIBDIR = @PKG_CONFIG_LIBDIR@
+PKG_CONFIG_PATH = @PKG_CONFIG_PATH@
+QUOTA_LIBS = @QUOTA_LIBS@
+RANLIB = @RANLIB@
+RELRO_LDFLAGS = @RELRO_LDFLAGS@
+RPCGEN = @RPCGEN@
+RUN_TEST = @RUN_TEST@
+SED = @SED@
+SETTING_FILES = @SETTING_FILES@
+SET_MAKE = @SET_MAKE@
+SHELL = @SHELL@
+SQLITE_CFLAGS = @SQLITE_CFLAGS@
+SQLITE_LIBS = @SQLITE_LIBS@
+SQL_CFLAGS = @SQL_CFLAGS@
+SQL_LIBS = @SQL_LIBS@
+SSL_CFLAGS = @SSL_CFLAGS@
+SSL_LIBS = @SSL_LIBS@
+STRIP = @STRIP@
+SYSTEMD_CFLAGS = @SYSTEMD_CFLAGS@
+SYSTEMD_LIBS = @SYSTEMD_LIBS@
+VALGRIND = @VALGRIND@
+VERSION = @VERSION@
+ZSTD_CFLAGS = @ZSTD_CFLAGS@
+ZSTD_LIBS = @ZSTD_LIBS@
+abs_builddir = @abs_builddir@
+abs_srcdir = @abs_srcdir@
+abs_top_builddir = @abs_top_builddir@
+abs_top_srcdir = @abs_top_srcdir@
+ac_ct_AR = @ac_ct_AR@
+ac_ct_CC = @ac_ct_CC@
+ac_ct_CXX = @ac_ct_CXX@
+ac_ct_DUMPBIN = @ac_ct_DUMPBIN@
+am__include = @am__include@
+am__leading_dot = @am__leading_dot@
+am__quote = @am__quote@
+am__tar = @am__tar@
+am__untar = @am__untar@
+bindir = @bindir@
+build = @build@
+build_alias = @build_alias@
+build_cpu = @build_cpu@
+build_os = @build_os@
+build_vendor = @build_vendor@
+builddir = @builddir@
+datadir = @datadir@
+datarootdir = @datarootdir@
+dict_drivers = @dict_drivers@
+docdir = @docdir@
+dvidir = @dvidir@
+exec_prefix = @exec_prefix@
+host = @host@
+host_alias = @host_alias@
+host_cpu = @host_cpu@
+host_os = @host_os@
+host_vendor = @host_vendor@
+htmldir = @htmldir@
+includedir = @includedir@
+infodir = @infodir@
+install_sh = @install_sh@
+libdir = @libdir@
+libexecdir = @libexecdir@
+localedir = @localedir@
+localstatedir = @localstatedir@
+mandir = @mandir@
+mkdir_p = @mkdir_p@
+moduledir = @moduledir@
+oldincludedir = @oldincludedir@
+pdfdir = @pdfdir@
+prefix = @prefix@
+program_transform_name = @program_transform_name@
+psdir = @psdir@
+rundir = @rundir@
+runstatedir = @runstatedir@
+sbindir = @sbindir@
+sharedstatedir = @sharedstatedir@
+sql_drivers = @sql_drivers@
+srcdir = @srcdir@
+ssldir = @ssldir@
+statedir = @statedir@
+sysconfdir = @sysconfdir@
+systemdservicetype = @systemdservicetype@
+systemdsystemunitdir = @systemdsystemunitdir@
+target_alias = @target_alias@
+top_build_prefix = @top_build_prefix@
+top_builddir = @top_builddir@
+top_srcdir = @top_srcdir@
+SUBDIRS = maildir mbox dbox-common dbox-multi dbox-single imapc pop3c raw shared
+noinst_LTLIBRARIES = libstorage_index.la
+AM_CPPFLAGS = \
+ -I$(top_srcdir)/src/lib \
+ -I$(top_srcdir)/src/lib-test \
+ -I$(top_srcdir)/src/lib-dict \
+ -I$(top_srcdir)/src/lib-fs \
+ -I$(top_srcdir)/src/lib-mail \
+ -I$(top_srcdir)/src/lib-imap \
+ -I$(top_srcdir)/src/lib-index \
+ -I$(top_srcdir)/src/lib-storage
+
+libstorage_index_la_SOURCES = \
+ istream-mail.c \
+ index-attachment.c \
+ index-attribute.c \
+ index-mail.c \
+ index-mail-binary.c \
+ index-mail-headers.c \
+ index-mailbox-size.c \
+ index-pop3-uidl.c \
+ index-rebuild.c \
+ index-search.c \
+ index-search-mime.c \
+ index-search-result.c \
+ index-sort.c \
+ index-sort-string.c \
+ index-status.c \
+ index-storage.c \
+ index-sync.c \
+ index-sync-changes.c \
+ index-sync-pvt.c \
+ index-sync-search.c \
+ index-thread.c \
+ index-thread-finish.c \
+ index-thread-links.c \
+ index-transaction.c
+
+headers = \
+ istream-mail.h \
+ index-attachment.h \
+ index-mail.h \
+ index-mailbox-size.h \
+ index-pop3-uidl.h \
+ index-rebuild.h \
+ index-search-private.h \
+ index-search-result.h \
+ index-sort.h \
+ index-sort-private.h \
+ index-storage.h \
+ index-sync-changes.h \
+ index-sync-private.h \
+ index-thread-private.h
+
+pkginc_libdir = $(pkgincludedir)
+pkginc_lib_HEADERS = $(headers)
+all: all-recursive
+
+.SUFFIXES:
+.SUFFIXES: .c .lo .o .obj
+$(srcdir)/Makefile.in: @MAINTAINER_MODE_TRUE@ $(srcdir)/Makefile.am $(am__configure_deps)
+ @for dep in $?; do \
+ case '$(am__configure_deps)' in \
+ *$$dep*) \
+ ( cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh ) \
+ && { if test -f $@; then exit 0; else break; fi; }; \
+ exit 1;; \
+ esac; \
+ done; \
+ echo ' cd $(top_srcdir) && $(AUTOMAKE) --foreign src/lib-storage/index/Makefile'; \
+ $(am__cd) $(top_srcdir) && \
+ $(AUTOMAKE) --foreign src/lib-storage/index/Makefile
+Makefile: $(srcdir)/Makefile.in $(top_builddir)/config.status
+ @case '$?' in \
+ *config.status*) \
+ cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh;; \
+ *) \
+ echo ' cd $(top_builddir) && $(SHELL) ./config.status $(subdir)/$@ $(am__maybe_remake_depfiles)'; \
+ cd $(top_builddir) && $(SHELL) ./config.status $(subdir)/$@ $(am__maybe_remake_depfiles);; \
+ esac;
+
+$(top_builddir)/config.status: $(top_srcdir)/configure $(CONFIG_STATUS_DEPENDENCIES)
+ cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh
+
+$(top_srcdir)/configure: @MAINTAINER_MODE_TRUE@ $(am__configure_deps)
+ cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh
+$(ACLOCAL_M4): @MAINTAINER_MODE_TRUE@ $(am__aclocal_m4_deps)
+ cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh
+$(am__aclocal_m4_deps):
+
+clean-noinstLTLIBRARIES:
+ -test -z "$(noinst_LTLIBRARIES)" || rm -f $(noinst_LTLIBRARIES)
+ @list='$(noinst_LTLIBRARIES)'; \
+ locs=`for p in $$list; do echo $$p; done | \
+ sed 's|^[^/]*$$|.|; s|/[^/]*$$||; s|$$|/so_locations|' | \
+ sort -u`; \
+ test -z "$$locs" || { \
+ echo rm -f $${locs}; \
+ rm -f $${locs}; \
+ }
+
+libstorage_index.la: $(libstorage_index_la_OBJECTS) $(libstorage_index_la_DEPENDENCIES) $(EXTRA_libstorage_index_la_DEPENDENCIES)
+ $(AM_V_CCLD)$(LINK) $(libstorage_index_la_OBJECTS) $(libstorage_index_la_LIBADD) $(LIBS)
+
+mostlyclean-compile:
+ -rm -f *.$(OBJEXT)
+
+distclean-compile:
+ -rm -f *.tab.c
+
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/index-attachment.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/index-attribute.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/index-mail-binary.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/index-mail-headers.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/index-mail.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/index-mailbox-size.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/index-pop3-uidl.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/index-rebuild.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/index-search-mime.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/index-search-result.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/index-search.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/index-sort-string.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/index-sort.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/index-status.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/index-storage.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/index-sync-changes.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/index-sync-pvt.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/index-sync-search.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/index-sync.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/index-thread-finish.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/index-thread-links.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/index-thread.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/index-transaction.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/istream-mail.Plo@am__quote@ # am--include-marker
+
+$(am__depfiles_remade):
+ @$(MKDIR_P) $(@D)
+ @echo '# dummy' >$@-t && $(am__mv) $@-t $@
+
+am--depfiles: $(am__depfiles_remade)
+
+.c.o:
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(COMPILE) -MT $@ -MD -MP -MF $(DEPDIR)/$*.Tpo -c -o $@ $<
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/$*.Tpo $(DEPDIR)/$*.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='$<' object='$@' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(COMPILE) -c -o $@ $<
+
+.c.obj:
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(COMPILE) -MT $@ -MD -MP -MF $(DEPDIR)/$*.Tpo -c -o $@ `$(CYGPATH_W) '$<'`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/$*.Tpo $(DEPDIR)/$*.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='$<' object='$@' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(COMPILE) -c -o $@ `$(CYGPATH_W) '$<'`
+
+.c.lo:
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(LTCOMPILE) -MT $@ -MD -MP -MF $(DEPDIR)/$*.Tpo -c -o $@ $<
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/$*.Tpo $(DEPDIR)/$*.Plo
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='$<' object='$@' libtool=yes @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(LTCOMPILE) -c -o $@ $<
+
+mostlyclean-libtool:
+ -rm -f *.lo
+
+clean-libtool:
+ -rm -rf .libs _libs
+install-pkginc_libHEADERS: $(pkginc_lib_HEADERS)
+ @$(NORMAL_INSTALL)
+ @list='$(pkginc_lib_HEADERS)'; test -n "$(pkginc_libdir)" || list=; \
+ if test -n "$$list"; then \
+ echo " $(MKDIR_P) '$(DESTDIR)$(pkginc_libdir)'"; \
+ $(MKDIR_P) "$(DESTDIR)$(pkginc_libdir)" || exit 1; \
+ fi; \
+ for p in $$list; do \
+ if test -f "$$p"; then d=; else d="$(srcdir)/"; fi; \
+ echo "$$d$$p"; \
+ done | $(am__base_list) | \
+ while read files; do \
+ echo " $(INSTALL_HEADER) $$files '$(DESTDIR)$(pkginc_libdir)'"; \
+ $(INSTALL_HEADER) $$files "$(DESTDIR)$(pkginc_libdir)" || exit $$?; \
+ done
+
+uninstall-pkginc_libHEADERS:
+ @$(NORMAL_UNINSTALL)
+ @list='$(pkginc_lib_HEADERS)'; test -n "$(pkginc_libdir)" || list=; \
+ files=`for p in $$list; do echo $$p; done | sed -e 's|^.*/||'`; \
+ dir='$(DESTDIR)$(pkginc_libdir)'; $(am__uninstall_files_from_dir)
+
+# This directory's subdirectories are mostly independent; you can cd
+# into them and run 'make' without going through this Makefile.
+# To change the values of 'make' variables: instead of editing Makefiles,
+# (1) if the variable is set in 'config.status', edit 'config.status'
+# (which will cause the Makefiles to be regenerated when you run 'make');
+# (2) otherwise, pass the desired values on the 'make' command line.
+$(am__recursive_targets):
+ @fail=; \
+ if $(am__make_keepgoing); then \
+ failcom='fail=yes'; \
+ else \
+ failcom='exit 1'; \
+ fi; \
+ dot_seen=no; \
+ target=`echo $@ | sed s/-recursive//`; \
+ case "$@" in \
+ distclean-* | maintainer-clean-*) list='$(DIST_SUBDIRS)' ;; \
+ *) list='$(SUBDIRS)' ;; \
+ esac; \
+ for subdir in $$list; do \
+ echo "Making $$target in $$subdir"; \
+ if test "$$subdir" = "."; then \
+ dot_seen=yes; \
+ local_target="$$target-am"; \
+ else \
+ local_target="$$target"; \
+ fi; \
+ ($(am__cd) $$subdir && $(MAKE) $(AM_MAKEFLAGS) $$local_target) \
+ || eval $$failcom; \
+ done; \
+ if test "$$dot_seen" = "no"; then \
+ $(MAKE) $(AM_MAKEFLAGS) "$$target-am" || exit 1; \
+ fi; test -z "$$fail"
+
+ID: $(am__tagged_files)
+ $(am__define_uniq_tagged_files); mkid -fID $$unique
+tags: tags-recursive
+TAGS: tags
+
+tags-am: $(TAGS_DEPENDENCIES) $(am__tagged_files)
+ set x; \
+ here=`pwd`; \
+ if ($(ETAGS) --etags-include --version) >/dev/null 2>&1; then \
+ include_option=--etags-include; \
+ empty_fix=.; \
+ else \
+ include_option=--include; \
+ empty_fix=; \
+ fi; \
+ list='$(SUBDIRS)'; for subdir in $$list; do \
+ if test "$$subdir" = .; then :; else \
+ test ! -f $$subdir/TAGS || \
+ set "$$@" "$$include_option=$$here/$$subdir/TAGS"; \
+ fi; \
+ done; \
+ $(am__define_uniq_tagged_files); \
+ shift; \
+ if test -z "$(ETAGS_ARGS)$$*$$unique"; then :; else \
+ test -n "$$unique" || unique=$$empty_fix; \
+ if test $$# -gt 0; then \
+ $(ETAGS) $(ETAGSFLAGS) $(AM_ETAGSFLAGS) $(ETAGS_ARGS) \
+ "$$@" $$unique; \
+ else \
+ $(ETAGS) $(ETAGSFLAGS) $(AM_ETAGSFLAGS) $(ETAGS_ARGS) \
+ $$unique; \
+ fi; \
+ fi
+ctags: ctags-recursive
+
+CTAGS: ctags
+ctags-am: $(TAGS_DEPENDENCIES) $(am__tagged_files)
+ $(am__define_uniq_tagged_files); \
+ test -z "$(CTAGS_ARGS)$$unique" \
+ || $(CTAGS) $(CTAGSFLAGS) $(AM_CTAGSFLAGS) $(CTAGS_ARGS) \
+ $$unique
+
+GTAGS:
+ here=`$(am__cd) $(top_builddir) && pwd` \
+ && $(am__cd) $(top_srcdir) \
+ && gtags -i $(GTAGS_ARGS) "$$here"
+cscopelist: cscopelist-recursive
+
+cscopelist-am: $(am__tagged_files)
+ list='$(am__tagged_files)'; \
+ case "$(srcdir)" in \
+ [\\/]* | ?:[\\/]*) sdir="$(srcdir)" ;; \
+ *) sdir=$(subdir)/$(srcdir) ;; \
+ esac; \
+ for i in $$list; do \
+ if test -f "$$i"; then \
+ echo "$(subdir)/$$i"; \
+ else \
+ echo "$$sdir/$$i"; \
+ fi; \
+ done >> $(top_builddir)/cscope.files
+
+distclean-tags:
+ -rm -f TAGS ID GTAGS GRTAGS GSYMS GPATH tags
+
+distdir: $(BUILT_SOURCES)
+ $(MAKE) $(AM_MAKEFLAGS) distdir-am
+
+distdir-am: $(DISTFILES)
+ @srcdirstrip=`echo "$(srcdir)" | sed 's/[].[^$$\\*]/\\\\&/g'`; \
+ topsrcdirstrip=`echo "$(top_srcdir)" | sed 's/[].[^$$\\*]/\\\\&/g'`; \
+ list='$(DISTFILES)'; \
+ dist_files=`for file in $$list; do echo $$file; done | \
+ sed -e "s|^$$srcdirstrip/||;t" \
+ -e "s|^$$topsrcdirstrip/|$(top_builddir)/|;t"`; \
+ case $$dist_files in \
+ */*) $(MKDIR_P) `echo "$$dist_files" | \
+ sed '/\//!d;s|^|$(distdir)/|;s,/[^/]*$$,,' | \
+ sort -u` ;; \
+ esac; \
+ for file in $$dist_files; do \
+ if test -f $$file || test -d $$file; then d=.; else d=$(srcdir); fi; \
+ if test -d $$d/$$file; then \
+ dir=`echo "/$$file" | sed -e 's,/[^/]*$$,,'`; \
+ if test -d "$(distdir)/$$file"; then \
+ find "$(distdir)/$$file" -type d ! -perm -700 -exec chmod u+rwx {} \;; \
+ fi; \
+ if test -d $(srcdir)/$$file && test $$d != $(srcdir); then \
+ cp -fpR $(srcdir)/$$file "$(distdir)$$dir" || exit 1; \
+ find "$(distdir)/$$file" -type d ! -perm -700 -exec chmod u+rwx {} \;; \
+ fi; \
+ cp -fpR $$d/$$file "$(distdir)$$dir" || exit 1; \
+ else \
+ test -f "$(distdir)/$$file" \
+ || cp -p $$d/$$file "$(distdir)/$$file" \
+ || exit 1; \
+ fi; \
+ done
+ @list='$(DIST_SUBDIRS)'; for subdir in $$list; do \
+ if test "$$subdir" = .; then :; else \
+ $(am__make_dryrun) \
+ || test -d "$(distdir)/$$subdir" \
+ || $(MKDIR_P) "$(distdir)/$$subdir" \
+ || exit 1; \
+ dir1=$$subdir; dir2="$(distdir)/$$subdir"; \
+ $(am__relativize); \
+ new_distdir=$$reldir; \
+ dir1=$$subdir; dir2="$(top_distdir)"; \
+ $(am__relativize); \
+ new_top_distdir=$$reldir; \
+ echo " (cd $$subdir && $(MAKE) $(AM_MAKEFLAGS) top_distdir="$$new_top_distdir" distdir="$$new_distdir" \\"; \
+ echo " am__remove_distdir=: am__skip_length_check=: am__skip_mode_fix=: distdir)"; \
+ ($(am__cd) $$subdir && \
+ $(MAKE) $(AM_MAKEFLAGS) \
+ top_distdir="$$new_top_distdir" \
+ distdir="$$new_distdir" \
+ am__remove_distdir=: \
+ am__skip_length_check=: \
+ am__skip_mode_fix=: \
+ distdir) \
+ || exit 1; \
+ fi; \
+ done
+check-am: all-am
+check: check-recursive
+all-am: Makefile $(LTLIBRARIES) $(HEADERS)
+installdirs: installdirs-recursive
+installdirs-am:
+ for dir in "$(DESTDIR)$(pkginc_libdir)"; do \
+ test -z "$$dir" || $(MKDIR_P) "$$dir"; \
+ done
+install: install-recursive
+install-exec: install-exec-recursive
+install-data: install-data-recursive
+uninstall: uninstall-recursive
+
+install-am: all-am
+ @$(MAKE) $(AM_MAKEFLAGS) install-exec-am install-data-am
+
+installcheck: installcheck-recursive
+install-strip:
+ if test -z '$(STRIP)'; then \
+ $(MAKE) $(AM_MAKEFLAGS) INSTALL_PROGRAM="$(INSTALL_STRIP_PROGRAM)" \
+ install_sh_PROGRAM="$(INSTALL_STRIP_PROGRAM)" INSTALL_STRIP_FLAG=-s \
+ install; \
+ else \
+ $(MAKE) $(AM_MAKEFLAGS) INSTALL_PROGRAM="$(INSTALL_STRIP_PROGRAM)" \
+ install_sh_PROGRAM="$(INSTALL_STRIP_PROGRAM)" INSTALL_STRIP_FLAG=-s \
+ "INSTALL_PROGRAM_ENV=STRIPPROG='$(STRIP)'" install; \
+ fi
+mostlyclean-generic:
+
+clean-generic:
+
+distclean-generic:
+ -test -z "$(CONFIG_CLEAN_FILES)" || rm -f $(CONFIG_CLEAN_FILES)
+ -test . = "$(srcdir)" || test -z "$(CONFIG_CLEAN_VPATH_FILES)" || rm -f $(CONFIG_CLEAN_VPATH_FILES)
+
+maintainer-clean-generic:
+ @echo "This command is intended for maintainers to use"
+ @echo "it deletes files that may require special tools to rebuild."
+clean: clean-recursive
+
+clean-am: clean-generic clean-libtool clean-noinstLTLIBRARIES \
+ mostlyclean-am
+
+distclean: distclean-recursive
+ -rm -f ./$(DEPDIR)/index-attachment.Plo
+ -rm -f ./$(DEPDIR)/index-attribute.Plo
+ -rm -f ./$(DEPDIR)/index-mail-binary.Plo
+ -rm -f ./$(DEPDIR)/index-mail-headers.Plo
+ -rm -f ./$(DEPDIR)/index-mail.Plo
+ -rm -f ./$(DEPDIR)/index-mailbox-size.Plo
+ -rm -f ./$(DEPDIR)/index-pop3-uidl.Plo
+ -rm -f ./$(DEPDIR)/index-rebuild.Plo
+ -rm -f ./$(DEPDIR)/index-search-mime.Plo
+ -rm -f ./$(DEPDIR)/index-search-result.Plo
+ -rm -f ./$(DEPDIR)/index-search.Plo
+ -rm -f ./$(DEPDIR)/index-sort-string.Plo
+ -rm -f ./$(DEPDIR)/index-sort.Plo
+ -rm -f ./$(DEPDIR)/index-status.Plo
+ -rm -f ./$(DEPDIR)/index-storage.Plo
+ -rm -f ./$(DEPDIR)/index-sync-changes.Plo
+ -rm -f ./$(DEPDIR)/index-sync-pvt.Plo
+ -rm -f ./$(DEPDIR)/index-sync-search.Plo
+ -rm -f ./$(DEPDIR)/index-sync.Plo
+ -rm -f ./$(DEPDIR)/index-thread-finish.Plo
+ -rm -f ./$(DEPDIR)/index-thread-links.Plo
+ -rm -f ./$(DEPDIR)/index-thread.Plo
+ -rm -f ./$(DEPDIR)/index-transaction.Plo
+ -rm -f ./$(DEPDIR)/istream-mail.Plo
+ -rm -f Makefile
+distclean-am: clean-am distclean-compile distclean-generic \
+ distclean-tags
+
+dvi: dvi-recursive
+
+dvi-am:
+
+html: html-recursive
+
+html-am:
+
+info: info-recursive
+
+info-am:
+
+install-data-am: install-pkginc_libHEADERS
+
+install-dvi: install-dvi-recursive
+
+install-dvi-am:
+
+install-exec-am:
+
+install-html: install-html-recursive
+
+install-html-am:
+
+install-info: install-info-recursive
+
+install-info-am:
+
+install-man:
+
+install-pdf: install-pdf-recursive
+
+install-pdf-am:
+
+install-ps: install-ps-recursive
+
+install-ps-am:
+
+installcheck-am:
+
+maintainer-clean: maintainer-clean-recursive
+ -rm -f ./$(DEPDIR)/index-attachment.Plo
+ -rm -f ./$(DEPDIR)/index-attribute.Plo
+ -rm -f ./$(DEPDIR)/index-mail-binary.Plo
+ -rm -f ./$(DEPDIR)/index-mail-headers.Plo
+ -rm -f ./$(DEPDIR)/index-mail.Plo
+ -rm -f ./$(DEPDIR)/index-mailbox-size.Plo
+ -rm -f ./$(DEPDIR)/index-pop3-uidl.Plo
+ -rm -f ./$(DEPDIR)/index-rebuild.Plo
+ -rm -f ./$(DEPDIR)/index-search-mime.Plo
+ -rm -f ./$(DEPDIR)/index-search-result.Plo
+ -rm -f ./$(DEPDIR)/index-search.Plo
+ -rm -f ./$(DEPDIR)/index-sort-string.Plo
+ -rm -f ./$(DEPDIR)/index-sort.Plo
+ -rm -f ./$(DEPDIR)/index-status.Plo
+ -rm -f ./$(DEPDIR)/index-storage.Plo
+ -rm -f ./$(DEPDIR)/index-sync-changes.Plo
+ -rm -f ./$(DEPDIR)/index-sync-pvt.Plo
+ -rm -f ./$(DEPDIR)/index-sync-search.Plo
+ -rm -f ./$(DEPDIR)/index-sync.Plo
+ -rm -f ./$(DEPDIR)/index-thread-finish.Plo
+ -rm -f ./$(DEPDIR)/index-thread-links.Plo
+ -rm -f ./$(DEPDIR)/index-thread.Plo
+ -rm -f ./$(DEPDIR)/index-transaction.Plo
+ -rm -f ./$(DEPDIR)/istream-mail.Plo
+ -rm -f Makefile
+maintainer-clean-am: distclean-am maintainer-clean-generic
+
+mostlyclean: mostlyclean-recursive
+
+mostlyclean-am: mostlyclean-compile mostlyclean-generic \
+ mostlyclean-libtool
+
+pdf: pdf-recursive
+
+pdf-am:
+
+ps: ps-recursive
+
+ps-am:
+
+uninstall-am: uninstall-pkginc_libHEADERS
+
+.MAKE: $(am__recursive_targets) install-am install-strip
+
+.PHONY: $(am__recursive_targets) CTAGS GTAGS TAGS all all-am \
+ am--depfiles check check-am clean clean-generic clean-libtool \
+ clean-noinstLTLIBRARIES cscopelist-am ctags ctags-am distclean \
+ distclean-compile distclean-generic distclean-libtool \
+ distclean-tags distdir dvi dvi-am html html-am info info-am \
+ install install-am install-data install-data-am install-dvi \
+ install-dvi-am install-exec install-exec-am install-html \
+ install-html-am install-info install-info-am install-man \
+ install-pdf install-pdf-am install-pkginc_libHEADERS \
+ install-ps install-ps-am install-strip installcheck \
+ installcheck-am installdirs installdirs-am maintainer-clean \
+ maintainer-clean-generic mostlyclean mostlyclean-compile \
+ mostlyclean-generic mostlyclean-libtool pdf pdf-am ps ps-am \
+ tags tags-am uninstall uninstall-am \
+ uninstall-pkginc_libHEADERS
+
+.PRECIOUS: Makefile
+
+
+# Tell versions [3.59,3.63) of GNU make to not export all variables.
+# Otherwise a system limit (for SysV at least) may be exceeded.
+.NOEXPORT:
diff --git a/src/lib-storage/index/dbox-common/Makefile.am b/src/lib-storage/index/dbox-common/Makefile.am
new file mode 100644
index 0000000..ec3d511
--- /dev/null
+++ b/src/lib-storage/index/dbox-common/Makefile.am
@@ -0,0 +1,29 @@
+noinst_LTLIBRARIES = libstorage_dbox_common.la
+
+AM_CPPFLAGS = \
+ -I$(top_srcdir)/src/lib \
+ -I$(top_srcdir)/src/lib-settings \
+ -I$(top_srcdir)/src/lib-fs \
+ -I$(top_srcdir)/src/lib-mail \
+ -I$(top_srcdir)/src/lib-imap \
+ -I$(top_srcdir)/src/lib-index \
+ -I$(top_srcdir)/src/lib-storage \
+ -I$(top_srcdir)/src/lib-storage/index
+
+libstorage_dbox_common_la_SOURCES = \
+ dbox-attachment.c \
+ dbox-file.c \
+ dbox-file-fix.c \
+ dbox-mail.c \
+ dbox-save.c \
+ dbox-storage.c
+
+headers = \
+ dbox-attachment.h \
+ dbox-file.h \
+ dbox-mail.h \
+ dbox-save.h \
+ dbox-storage.h
+
+pkginc_libdir=$(pkgincludedir)
+pkginc_lib_HEADERS = $(headers)
diff --git a/src/lib-storage/index/dbox-common/Makefile.in b/src/lib-storage/index/dbox-common/Makefile.in
new file mode 100644
index 0000000..60aaf66
--- /dev/null
+++ b/src/lib-storage/index/dbox-common/Makefile.in
@@ -0,0 +1,843 @@
+# Makefile.in generated by automake 1.16.1 from Makefile.am.
+# @configure_input@
+
+# Copyright (C) 1994-2018 Free Software Foundation, Inc.
+
+# This Makefile.in is free software; the Free Software Foundation
+# gives unlimited permission to copy and/or distribute it,
+# with or without modifications, as long as this notice is preserved.
+
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY, to the extent permitted by law; without
+# even the implied warranty of MERCHANTABILITY or FITNESS FOR A
+# PARTICULAR PURPOSE.
+
+@SET_MAKE@
+
+
+VPATH = @srcdir@
+am__is_gnu_make = { \
+ if test -z '$(MAKELEVEL)'; then \
+ false; \
+ elif test -n '$(MAKE_HOST)'; then \
+ true; \
+ elif test -n '$(MAKE_VERSION)' && test -n '$(CURDIR)'; then \
+ true; \
+ else \
+ false; \
+ fi; \
+}
+am__make_running_with_option = \
+ case $${target_option-} in \
+ ?) ;; \
+ *) echo "am__make_running_with_option: internal error: invalid" \
+ "target option '$${target_option-}' specified" >&2; \
+ exit 1;; \
+ esac; \
+ has_opt=no; \
+ sane_makeflags=$$MAKEFLAGS; \
+ if $(am__is_gnu_make); then \
+ sane_makeflags=$$MFLAGS; \
+ else \
+ case $$MAKEFLAGS in \
+ *\\[\ \ ]*) \
+ bs=\\; \
+ sane_makeflags=`printf '%s\n' "$$MAKEFLAGS" \
+ | sed "s/$$bs$$bs[$$bs $$bs ]*//g"`;; \
+ esac; \
+ fi; \
+ skip_next=no; \
+ strip_trailopt () \
+ { \
+ flg=`printf '%s\n' "$$flg" | sed "s/$$1.*$$//"`; \
+ }; \
+ for flg in $$sane_makeflags; do \
+ test $$skip_next = yes && { skip_next=no; continue; }; \
+ case $$flg in \
+ *=*|--*) continue;; \
+ -*I) strip_trailopt 'I'; skip_next=yes;; \
+ -*I?*) strip_trailopt 'I';; \
+ -*O) strip_trailopt 'O'; skip_next=yes;; \
+ -*O?*) strip_trailopt 'O';; \
+ -*l) strip_trailopt 'l'; skip_next=yes;; \
+ -*l?*) strip_trailopt 'l';; \
+ -[dEDm]) skip_next=yes;; \
+ -[JT]) skip_next=yes;; \
+ esac; \
+ case $$flg in \
+ *$$target_option*) has_opt=yes; break;; \
+ esac; \
+ done; \
+ test $$has_opt = yes
+am__make_dryrun = (target_option=n; $(am__make_running_with_option))
+am__make_keepgoing = (target_option=k; $(am__make_running_with_option))
+pkgdatadir = $(datadir)/@PACKAGE@
+pkgincludedir = $(includedir)/@PACKAGE@
+pkglibdir = $(libdir)/@PACKAGE@
+pkglibexecdir = $(libexecdir)/@PACKAGE@
+am__cd = CDPATH="$${ZSH_VERSION+.}$(PATH_SEPARATOR)" && cd
+install_sh_DATA = $(install_sh) -c -m 644
+install_sh_PROGRAM = $(install_sh) -c
+install_sh_SCRIPT = $(install_sh) -c
+INSTALL_HEADER = $(INSTALL_DATA)
+transform = $(program_transform_name)
+NORMAL_INSTALL = :
+PRE_INSTALL = :
+POST_INSTALL = :
+NORMAL_UNINSTALL = :
+PRE_UNINSTALL = :
+POST_UNINSTALL = :
+build_triplet = @build@
+host_triplet = @host@
+subdir = src/lib-storage/index/dbox-common
+ACLOCAL_M4 = $(top_srcdir)/aclocal.m4
+am__aclocal_m4_deps = $(top_srcdir)/m4/ac_checktype2.m4 \
+ $(top_srcdir)/m4/ac_typeof.m4 $(top_srcdir)/m4/arc4random.m4 \
+ $(top_srcdir)/m4/blockdev.m4 $(top_srcdir)/m4/c99_vsnprintf.m4 \
+ $(top_srcdir)/m4/clock_gettime.m4 $(top_srcdir)/m4/crypt.m4 \
+ $(top_srcdir)/m4/crypt_xpg6.m4 $(top_srcdir)/m4/dbqlk.m4 \
+ $(top_srcdir)/m4/dirent_dtype.m4 $(top_srcdir)/m4/dovecot.m4 \
+ $(top_srcdir)/m4/fd_passing.m4 $(top_srcdir)/m4/fdatasync.m4 \
+ $(top_srcdir)/m4/flexible_array_member.m4 \
+ $(top_srcdir)/m4/glibc.m4 $(top_srcdir)/m4/gmtime_max.m4 \
+ $(top_srcdir)/m4/gmtime_tm_gmtoff.m4 \
+ $(top_srcdir)/m4/ioloop.m4 $(top_srcdir)/m4/iovec.m4 \
+ $(top_srcdir)/m4/ipv6.m4 $(top_srcdir)/m4/libcap.m4 \
+ $(top_srcdir)/m4/libtool.m4 $(top_srcdir)/m4/libwrap.m4 \
+ $(top_srcdir)/m4/linux_mremap.m4 $(top_srcdir)/m4/ltoptions.m4 \
+ $(top_srcdir)/m4/ltsugar.m4 $(top_srcdir)/m4/ltversion.m4 \
+ $(top_srcdir)/m4/lt~obsolete.m4 $(top_srcdir)/m4/mmap_write.m4 \
+ $(top_srcdir)/m4/mntctl.m4 $(top_srcdir)/m4/modules.m4 \
+ $(top_srcdir)/m4/notify.m4 $(top_srcdir)/m4/nsl.m4 \
+ $(top_srcdir)/m4/off_t_max.m4 $(top_srcdir)/m4/pkg.m4 \
+ $(top_srcdir)/m4/pr_set_dumpable.m4 \
+ $(top_srcdir)/m4/q_quotactl.m4 $(top_srcdir)/m4/quota.m4 \
+ $(top_srcdir)/m4/random.m4 $(top_srcdir)/m4/rlimit.m4 \
+ $(top_srcdir)/m4/sendfile.m4 $(top_srcdir)/m4/size_t_signed.m4 \
+ $(top_srcdir)/m4/sockpeercred.m4 $(top_srcdir)/m4/sql.m4 \
+ $(top_srcdir)/m4/ssl.m4 $(top_srcdir)/m4/st_tim.m4 \
+ $(top_srcdir)/m4/static_array.m4 $(top_srcdir)/m4/test_with.m4 \
+ $(top_srcdir)/m4/time_t.m4 $(top_srcdir)/m4/typeof.m4 \
+ $(top_srcdir)/m4/typeof_dev_t.m4 \
+ $(top_srcdir)/m4/uoff_t_max.m4 $(top_srcdir)/m4/vararg.m4 \
+ $(top_srcdir)/m4/want_apparmor.m4 \
+ $(top_srcdir)/m4/want_bsdauth.m4 \
+ $(top_srcdir)/m4/want_bzlib.m4 \
+ $(top_srcdir)/m4/want_cassandra.m4 \
+ $(top_srcdir)/m4/want_cdb.m4 \
+ $(top_srcdir)/m4/want_checkpassword.m4 \
+ $(top_srcdir)/m4/want_clucene.m4 $(top_srcdir)/m4/want_db.m4 \
+ $(top_srcdir)/m4/want_gssapi.m4 $(top_srcdir)/m4/want_icu.m4 \
+ $(top_srcdir)/m4/want_ldap.m4 $(top_srcdir)/m4/want_lua.m4 \
+ $(top_srcdir)/m4/want_lz4.m4 $(top_srcdir)/m4/want_lzma.m4 \
+ $(top_srcdir)/m4/want_mysql.m4 $(top_srcdir)/m4/want_pam.m4 \
+ $(top_srcdir)/m4/want_passwd.m4 $(top_srcdir)/m4/want_pgsql.m4 \
+ $(top_srcdir)/m4/want_prefetch.m4 \
+ $(top_srcdir)/m4/want_shadow.m4 \
+ $(top_srcdir)/m4/want_sodium.m4 $(top_srcdir)/m4/want_solr.m4 \
+ $(top_srcdir)/m4/want_sqlite.m4 \
+ $(top_srcdir)/m4/want_stemmer.m4 \
+ $(top_srcdir)/m4/want_systemd.m4 \
+ $(top_srcdir)/m4/want_textcat.m4 \
+ $(top_srcdir)/m4/want_unwind.m4 $(top_srcdir)/m4/want_zlib.m4 \
+ $(top_srcdir)/m4/want_zstd.m4 $(top_srcdir)/configure.ac
+am__configure_deps = $(am__aclocal_m4_deps) $(CONFIGURE_DEPENDENCIES) \
+ $(ACLOCAL_M4)
+DIST_COMMON = $(srcdir)/Makefile.am $(pkginc_lib_HEADERS) \
+ $(am__DIST_COMMON)
+mkinstalldirs = $(install_sh) -d
+CONFIG_HEADER = $(top_builddir)/config.h
+CONFIG_CLEAN_FILES =
+CONFIG_CLEAN_VPATH_FILES =
+LTLIBRARIES = $(noinst_LTLIBRARIES)
+libstorage_dbox_common_la_LIBADD =
+am_libstorage_dbox_common_la_OBJECTS = dbox-attachment.lo dbox-file.lo \
+ dbox-file-fix.lo dbox-mail.lo dbox-save.lo dbox-storage.lo
+libstorage_dbox_common_la_OBJECTS = \
+ $(am_libstorage_dbox_common_la_OBJECTS)
+AM_V_lt = $(am__v_lt_@AM_V@)
+am__v_lt_ = $(am__v_lt_@AM_DEFAULT_V@)
+am__v_lt_0 = --silent
+am__v_lt_1 =
+AM_V_P = $(am__v_P_@AM_V@)
+am__v_P_ = $(am__v_P_@AM_DEFAULT_V@)
+am__v_P_0 = false
+am__v_P_1 = :
+AM_V_GEN = $(am__v_GEN_@AM_V@)
+am__v_GEN_ = $(am__v_GEN_@AM_DEFAULT_V@)
+am__v_GEN_0 = @echo " GEN " $@;
+am__v_GEN_1 =
+AM_V_at = $(am__v_at_@AM_V@)
+am__v_at_ = $(am__v_at_@AM_DEFAULT_V@)
+am__v_at_0 = @
+am__v_at_1 =
+DEFAULT_INCLUDES = -I.@am__isrc@ -I$(top_builddir)
+depcomp = $(SHELL) $(top_srcdir)/depcomp
+am__maybe_remake_depfiles = depfiles
+am__depfiles_remade = ./$(DEPDIR)/dbox-attachment.Plo \
+ ./$(DEPDIR)/dbox-file-fix.Plo ./$(DEPDIR)/dbox-file.Plo \
+ ./$(DEPDIR)/dbox-mail.Plo ./$(DEPDIR)/dbox-save.Plo \
+ ./$(DEPDIR)/dbox-storage.Plo
+am__mv = mv -f
+COMPILE = $(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) \
+ $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS)
+LTCOMPILE = $(LIBTOOL) $(AM_V_lt) --tag=CC $(AM_LIBTOOLFLAGS) \
+ $(LIBTOOLFLAGS) --mode=compile $(CC) $(DEFS) \
+ $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) $(CPPFLAGS) \
+ $(AM_CFLAGS) $(CFLAGS)
+AM_V_CC = $(am__v_CC_@AM_V@)
+am__v_CC_ = $(am__v_CC_@AM_DEFAULT_V@)
+am__v_CC_0 = @echo " CC " $@;
+am__v_CC_1 =
+CCLD = $(CC)
+LINK = $(LIBTOOL) $(AM_V_lt) --tag=CC $(AM_LIBTOOLFLAGS) \
+ $(LIBTOOLFLAGS) --mode=link $(CCLD) $(AM_CFLAGS) $(CFLAGS) \
+ $(AM_LDFLAGS) $(LDFLAGS) -o $@
+AM_V_CCLD = $(am__v_CCLD_@AM_V@)
+am__v_CCLD_ = $(am__v_CCLD_@AM_DEFAULT_V@)
+am__v_CCLD_0 = @echo " CCLD " $@;
+am__v_CCLD_1 =
+SOURCES = $(libstorage_dbox_common_la_SOURCES)
+DIST_SOURCES = $(libstorage_dbox_common_la_SOURCES)
+am__can_run_installinfo = \
+ case $$AM_UPDATE_INFO_DIR in \
+ n|no|NO) false;; \
+ *) (install-info --version) >/dev/null 2>&1;; \
+ esac
+am__vpath_adj_setup = srcdirstrip=`echo "$(srcdir)" | sed 's|.|.|g'`;
+am__vpath_adj = case $$p in \
+ $(srcdir)/*) f=`echo "$$p" | sed "s|^$$srcdirstrip/||"`;; \
+ *) f=$$p;; \
+ esac;
+am__strip_dir = f=`echo $$p | sed -e 's|^.*/||'`;
+am__install_max = 40
+am__nobase_strip_setup = \
+ srcdirstrip=`echo "$(srcdir)" | sed 's/[].[^$$\\*|]/\\\\&/g'`
+am__nobase_strip = \
+ for p in $$list; do echo "$$p"; done | sed -e "s|$$srcdirstrip/||"
+am__nobase_list = $(am__nobase_strip_setup); \
+ for p in $$list; do echo "$$p $$p"; done | \
+ sed "s| $$srcdirstrip/| |;"' / .*\//!s/ .*/ ./; s,\( .*\)/[^/]*$$,\1,' | \
+ $(AWK) 'BEGIN { files["."] = "" } { files[$$2] = files[$$2] " " $$1; \
+ if (++n[$$2] == $(am__install_max)) \
+ { print $$2, files[$$2]; n[$$2] = 0; files[$$2] = "" } } \
+ END { for (dir in files) print dir, files[dir] }'
+am__base_list = \
+ sed '$$!N;$$!N;$$!N;$$!N;$$!N;$$!N;$$!N;s/\n/ /g' | \
+ sed '$$!N;$$!N;$$!N;$$!N;s/\n/ /g'
+am__uninstall_files_from_dir = { \
+ test -z "$$files" \
+ || { test ! -d "$$dir" && test ! -f "$$dir" && test ! -r "$$dir"; } \
+ || { echo " ( cd '$$dir' && rm -f" $$files ")"; \
+ $(am__cd) "$$dir" && rm -f $$files; }; \
+ }
+am__installdirs = "$(DESTDIR)$(pkginc_libdir)"
+HEADERS = $(pkginc_lib_HEADERS)
+am__tagged_files = $(HEADERS) $(SOURCES) $(TAGS_FILES) $(LISP)
+# Read a list of newline-separated strings from the standard input,
+# and print each of them once, without duplicates. Input order is
+# *not* preserved.
+am__uniquify_input = $(AWK) '\
+ BEGIN { nonempty = 0; } \
+ { items[$$0] = 1; nonempty = 1; } \
+ END { if (nonempty) { for (i in items) print i; }; } \
+'
+# Make sure the list of sources is unique. This is necessary because,
+# e.g., the same source file might be shared among _SOURCES variables
+# for different programs/libraries.
+am__define_uniq_tagged_files = \
+ list='$(am__tagged_files)'; \
+ unique=`for i in $$list; do \
+ if test -f "$$i"; then echo $$i; else echo $(srcdir)/$$i; fi; \
+ done | $(am__uniquify_input)`
+ETAGS = etags
+CTAGS = ctags
+am__DIST_COMMON = $(srcdir)/Makefile.in $(top_srcdir)/depcomp
+DISTFILES = $(DIST_COMMON) $(DIST_SOURCES) $(TEXINFOS) $(EXTRA_DIST)
+ACLOCAL = @ACLOCAL@
+ACLOCAL_AMFLAGS = @ACLOCAL_AMFLAGS@
+AMTAR = @AMTAR@
+AM_DEFAULT_VERBOSITY = @AM_DEFAULT_VERBOSITY@
+APPARMOR_LIBS = @APPARMOR_LIBS@
+AR = @AR@
+AUTH_CFLAGS = @AUTH_CFLAGS@
+AUTH_LIBS = @AUTH_LIBS@
+AUTOCONF = @AUTOCONF@
+AUTOHEADER = @AUTOHEADER@
+AUTOMAKE = @AUTOMAKE@
+AWK = @AWK@
+BINARY_CFLAGS = @BINARY_CFLAGS@
+BINARY_LDFLAGS = @BINARY_LDFLAGS@
+BISON = @BISON@
+CASSANDRA_CFLAGS = @CASSANDRA_CFLAGS@
+CASSANDRA_LIBS = @CASSANDRA_LIBS@
+CC = @CC@
+CCDEPMODE = @CCDEPMODE@
+CDB_LIBS = @CDB_LIBS@
+CFLAGS = @CFLAGS@
+CLUCENE_CFLAGS = @CLUCENE_CFLAGS@
+CLUCENE_LIBS = @CLUCENE_LIBS@
+COMPRESS_LIBS = @COMPRESS_LIBS@
+CPP = @CPP@
+CPPFLAGS = @CPPFLAGS@
+CRYPT_LIBS = @CRYPT_LIBS@
+CXX = @CXX@
+CXXCPP = @CXXCPP@
+CXXDEPMODE = @CXXDEPMODE@
+CXXFLAGS = @CXXFLAGS@
+CYGPATH_W = @CYGPATH_W@
+DEFS = @DEFS@
+DEPDIR = @DEPDIR@
+DICT_LIBS = @DICT_LIBS@
+DLLIB = @DLLIB@
+DLLTOOL = @DLLTOOL@
+DSYMUTIL = @DSYMUTIL@
+DUMPBIN = @DUMPBIN@
+ECHO_C = @ECHO_C@
+ECHO_N = @ECHO_N@
+ECHO_T = @ECHO_T@
+EGREP = @EGREP@
+EXEEXT = @EXEEXT@
+FGREP = @FGREP@
+FLEX = @FLEX@
+FUZZER_CPPFLAGS = @FUZZER_CPPFLAGS@
+FUZZER_LDFLAGS = @FUZZER_LDFLAGS@
+GREP = @GREP@
+INSTALL = @INSTALL@
+INSTALL_DATA = @INSTALL_DATA@
+INSTALL_PROGRAM = @INSTALL_PROGRAM@
+INSTALL_SCRIPT = @INSTALL_SCRIPT@
+INSTALL_STRIP_PROGRAM = @INSTALL_STRIP_PROGRAM@
+KRB5CONFIG = @KRB5CONFIG@
+KRB5_CFLAGS = @KRB5_CFLAGS@
+KRB5_LIBS = @KRB5_LIBS@
+LD = @LD@
+LDAP_LIBS = @LDAP_LIBS@
+LDFLAGS = @LDFLAGS@
+LD_NO_WHOLE_ARCHIVE = @LD_NO_WHOLE_ARCHIVE@
+LD_WHOLE_ARCHIVE = @LD_WHOLE_ARCHIVE@
+LIBCAP = @LIBCAP@
+LIBDOVECOT = @LIBDOVECOT@
+LIBDOVECOT_COMPRESS = @LIBDOVECOT_COMPRESS@
+LIBDOVECOT_DEPS = @LIBDOVECOT_DEPS@
+LIBDOVECOT_DSYNC = @LIBDOVECOT_DSYNC@
+LIBDOVECOT_LA_LIBS = @LIBDOVECOT_LA_LIBS@
+LIBDOVECOT_LDA = @LIBDOVECOT_LDA@
+LIBDOVECOT_LDAP = @LIBDOVECOT_LDAP@
+LIBDOVECOT_LIBFTS = @LIBDOVECOT_LIBFTS@
+LIBDOVECOT_LIBFTS_DEPS = @LIBDOVECOT_LIBFTS_DEPS@
+LIBDOVECOT_LOGIN = @LIBDOVECOT_LOGIN@
+LIBDOVECOT_LUA = @LIBDOVECOT_LUA@
+LIBDOVECOT_LUA_DEPS = @LIBDOVECOT_LUA_DEPS@
+LIBDOVECOT_SQL = @LIBDOVECOT_SQL@
+LIBDOVECOT_STORAGE = @LIBDOVECOT_STORAGE@
+LIBDOVECOT_STORAGE_DEPS = @LIBDOVECOT_STORAGE_DEPS@
+LIBEXTTEXTCAT_CFLAGS = @LIBEXTTEXTCAT_CFLAGS@
+LIBEXTTEXTCAT_LIBS = @LIBEXTTEXTCAT_LIBS@
+LIBICONV = @LIBICONV@
+LIBICU_CFLAGS = @LIBICU_CFLAGS@
+LIBICU_LIBS = @LIBICU_LIBS@
+LIBOBJS = @LIBOBJS@
+LIBS = @LIBS@
+LIBSODIUM_CFLAGS = @LIBSODIUM_CFLAGS@
+LIBSODIUM_LIBS = @LIBSODIUM_LIBS@
+LIBTIRPC_CFLAGS = @LIBTIRPC_CFLAGS@
+LIBTIRPC_LIBS = @LIBTIRPC_LIBS@
+LIBTOOL = @LIBTOOL@
+LIBUNWIND_CFLAGS = @LIBUNWIND_CFLAGS@
+LIBUNWIND_LIBS = @LIBUNWIND_LIBS@
+LIBWRAP_LIBS = @LIBWRAP_LIBS@
+LINKED_STORAGE_LDADD = @LINKED_STORAGE_LDADD@
+LIPO = @LIPO@
+LN_S = @LN_S@
+LTLIBICONV = @LTLIBICONV@
+LTLIBOBJS = @LTLIBOBJS@
+LT_SYS_LIBRARY_PATH = @LT_SYS_LIBRARY_PATH@
+LUA_CFLAGS = @LUA_CFLAGS@
+LUA_LIBS = @LUA_LIBS@
+MAINT = @MAINT@
+MAKEINFO = @MAKEINFO@
+MANIFEST_TOOL = @MANIFEST_TOOL@
+MKDIR_P = @MKDIR_P@
+MODULE_LIBS = @MODULE_LIBS@
+MODULE_SUFFIX = @MODULE_SUFFIX@
+MYSQL_CFLAGS = @MYSQL_CFLAGS@
+MYSQL_CONFIG = @MYSQL_CONFIG@
+MYSQL_LIBS = @MYSQL_LIBS@
+NM = @NM@
+NMEDIT = @NMEDIT@
+NOPLUGIN_LDFLAGS = @NOPLUGIN_LDFLAGS@
+OBJDUMP = @OBJDUMP@
+OBJEXT = @OBJEXT@
+OTOOL = @OTOOL@
+OTOOL64 = @OTOOL64@
+PACKAGE = @PACKAGE@
+PACKAGE_BUGREPORT = @PACKAGE_BUGREPORT@
+PACKAGE_NAME = @PACKAGE_NAME@
+PACKAGE_STRING = @PACKAGE_STRING@
+PACKAGE_TARNAME = @PACKAGE_TARNAME@
+PACKAGE_URL = @PACKAGE_URL@
+PACKAGE_VERSION = @PACKAGE_VERSION@
+PANDOC = @PANDOC@
+PATH_SEPARATOR = @PATH_SEPARATOR@
+PGSQL_CFLAGS = @PGSQL_CFLAGS@
+PGSQL_LIBS = @PGSQL_LIBS@
+PG_CONFIG = @PG_CONFIG@
+PIE_CFLAGS = @PIE_CFLAGS@
+PIE_LDFLAGS = @PIE_LDFLAGS@
+PKG_CONFIG = @PKG_CONFIG@
+PKG_CONFIG_LIBDIR = @PKG_CONFIG_LIBDIR@
+PKG_CONFIG_PATH = @PKG_CONFIG_PATH@
+QUOTA_LIBS = @QUOTA_LIBS@
+RANLIB = @RANLIB@
+RELRO_LDFLAGS = @RELRO_LDFLAGS@
+RPCGEN = @RPCGEN@
+RUN_TEST = @RUN_TEST@
+SED = @SED@
+SETTING_FILES = @SETTING_FILES@
+SET_MAKE = @SET_MAKE@
+SHELL = @SHELL@
+SQLITE_CFLAGS = @SQLITE_CFLAGS@
+SQLITE_LIBS = @SQLITE_LIBS@
+SQL_CFLAGS = @SQL_CFLAGS@
+SQL_LIBS = @SQL_LIBS@
+SSL_CFLAGS = @SSL_CFLAGS@
+SSL_LIBS = @SSL_LIBS@
+STRIP = @STRIP@
+SYSTEMD_CFLAGS = @SYSTEMD_CFLAGS@
+SYSTEMD_LIBS = @SYSTEMD_LIBS@
+VALGRIND = @VALGRIND@
+VERSION = @VERSION@
+ZSTD_CFLAGS = @ZSTD_CFLAGS@
+ZSTD_LIBS = @ZSTD_LIBS@
+abs_builddir = @abs_builddir@
+abs_srcdir = @abs_srcdir@
+abs_top_builddir = @abs_top_builddir@
+abs_top_srcdir = @abs_top_srcdir@
+ac_ct_AR = @ac_ct_AR@
+ac_ct_CC = @ac_ct_CC@
+ac_ct_CXX = @ac_ct_CXX@
+ac_ct_DUMPBIN = @ac_ct_DUMPBIN@
+am__include = @am__include@
+am__leading_dot = @am__leading_dot@
+am__quote = @am__quote@
+am__tar = @am__tar@
+am__untar = @am__untar@
+bindir = @bindir@
+build = @build@
+build_alias = @build_alias@
+build_cpu = @build_cpu@
+build_os = @build_os@
+build_vendor = @build_vendor@
+builddir = @builddir@
+datadir = @datadir@
+datarootdir = @datarootdir@
+dict_drivers = @dict_drivers@
+docdir = @docdir@
+dvidir = @dvidir@
+exec_prefix = @exec_prefix@
+host = @host@
+host_alias = @host_alias@
+host_cpu = @host_cpu@
+host_os = @host_os@
+host_vendor = @host_vendor@
+htmldir = @htmldir@
+includedir = @includedir@
+infodir = @infodir@
+install_sh = @install_sh@
+libdir = @libdir@
+libexecdir = @libexecdir@
+localedir = @localedir@
+localstatedir = @localstatedir@
+mandir = @mandir@
+mkdir_p = @mkdir_p@
+moduledir = @moduledir@
+oldincludedir = @oldincludedir@
+pdfdir = @pdfdir@
+prefix = @prefix@
+program_transform_name = @program_transform_name@
+psdir = @psdir@
+rundir = @rundir@
+runstatedir = @runstatedir@
+sbindir = @sbindir@
+sharedstatedir = @sharedstatedir@
+sql_drivers = @sql_drivers@
+srcdir = @srcdir@
+ssldir = @ssldir@
+statedir = @statedir@
+sysconfdir = @sysconfdir@
+systemdservicetype = @systemdservicetype@
+systemdsystemunitdir = @systemdsystemunitdir@
+target_alias = @target_alias@
+top_build_prefix = @top_build_prefix@
+top_builddir = @top_builddir@
+top_srcdir = @top_srcdir@
+noinst_LTLIBRARIES = libstorage_dbox_common.la
+AM_CPPFLAGS = \
+ -I$(top_srcdir)/src/lib \
+ -I$(top_srcdir)/src/lib-settings \
+ -I$(top_srcdir)/src/lib-fs \
+ -I$(top_srcdir)/src/lib-mail \
+ -I$(top_srcdir)/src/lib-imap \
+ -I$(top_srcdir)/src/lib-index \
+ -I$(top_srcdir)/src/lib-storage \
+ -I$(top_srcdir)/src/lib-storage/index
+
+libstorage_dbox_common_la_SOURCES = \
+ dbox-attachment.c \
+ dbox-file.c \
+ dbox-file-fix.c \
+ dbox-mail.c \
+ dbox-save.c \
+ dbox-storage.c
+
+headers = \
+ dbox-attachment.h \
+ dbox-file.h \
+ dbox-mail.h \
+ dbox-save.h \
+ dbox-storage.h
+
+pkginc_libdir = $(pkgincludedir)
+pkginc_lib_HEADERS = $(headers)
+all: all-am
+
+.SUFFIXES:
+.SUFFIXES: .c .lo .o .obj
+$(srcdir)/Makefile.in: @MAINTAINER_MODE_TRUE@ $(srcdir)/Makefile.am $(am__configure_deps)
+ @for dep in $?; do \
+ case '$(am__configure_deps)' in \
+ *$$dep*) \
+ ( cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh ) \
+ && { if test -f $@; then exit 0; else break; fi; }; \
+ exit 1;; \
+ esac; \
+ done; \
+ echo ' cd $(top_srcdir) && $(AUTOMAKE) --foreign src/lib-storage/index/dbox-common/Makefile'; \
+ $(am__cd) $(top_srcdir) && \
+ $(AUTOMAKE) --foreign src/lib-storage/index/dbox-common/Makefile
+Makefile: $(srcdir)/Makefile.in $(top_builddir)/config.status
+ @case '$?' in \
+ *config.status*) \
+ cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh;; \
+ *) \
+ echo ' cd $(top_builddir) && $(SHELL) ./config.status $(subdir)/$@ $(am__maybe_remake_depfiles)'; \
+ cd $(top_builddir) && $(SHELL) ./config.status $(subdir)/$@ $(am__maybe_remake_depfiles);; \
+ esac;
+
+$(top_builddir)/config.status: $(top_srcdir)/configure $(CONFIG_STATUS_DEPENDENCIES)
+ cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh
+
+$(top_srcdir)/configure: @MAINTAINER_MODE_TRUE@ $(am__configure_deps)
+ cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh
+$(ACLOCAL_M4): @MAINTAINER_MODE_TRUE@ $(am__aclocal_m4_deps)
+ cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh
+$(am__aclocal_m4_deps):
+
+clean-noinstLTLIBRARIES:
+ -test -z "$(noinst_LTLIBRARIES)" || rm -f $(noinst_LTLIBRARIES)
+ @list='$(noinst_LTLIBRARIES)'; \
+ locs=`for p in $$list; do echo $$p; done | \
+ sed 's|^[^/]*$$|.|; s|/[^/]*$$||; s|$$|/so_locations|' | \
+ sort -u`; \
+ test -z "$$locs" || { \
+ echo rm -f $${locs}; \
+ rm -f $${locs}; \
+ }
+
+libstorage_dbox_common.la: $(libstorage_dbox_common_la_OBJECTS) $(libstorage_dbox_common_la_DEPENDENCIES) $(EXTRA_libstorage_dbox_common_la_DEPENDENCIES)
+ $(AM_V_CCLD)$(LINK) $(libstorage_dbox_common_la_OBJECTS) $(libstorage_dbox_common_la_LIBADD) $(LIBS)
+
+mostlyclean-compile:
+ -rm -f *.$(OBJEXT)
+
+distclean-compile:
+ -rm -f *.tab.c
+
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/dbox-attachment.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/dbox-file-fix.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/dbox-file.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/dbox-mail.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/dbox-save.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/dbox-storage.Plo@am__quote@ # am--include-marker
+
+$(am__depfiles_remade):
+ @$(MKDIR_P) $(@D)
+ @echo '# dummy' >$@-t && $(am__mv) $@-t $@
+
+am--depfiles: $(am__depfiles_remade)
+
+.c.o:
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(COMPILE) -MT $@ -MD -MP -MF $(DEPDIR)/$*.Tpo -c -o $@ $<
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/$*.Tpo $(DEPDIR)/$*.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='$<' object='$@' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(COMPILE) -c -o $@ $<
+
+.c.obj:
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(COMPILE) -MT $@ -MD -MP -MF $(DEPDIR)/$*.Tpo -c -o $@ `$(CYGPATH_W) '$<'`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/$*.Tpo $(DEPDIR)/$*.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='$<' object='$@' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(COMPILE) -c -o $@ `$(CYGPATH_W) '$<'`
+
+.c.lo:
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(LTCOMPILE) -MT $@ -MD -MP -MF $(DEPDIR)/$*.Tpo -c -o $@ $<
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/$*.Tpo $(DEPDIR)/$*.Plo
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='$<' object='$@' libtool=yes @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(LTCOMPILE) -c -o $@ $<
+
+mostlyclean-libtool:
+ -rm -f *.lo
+
+clean-libtool:
+ -rm -rf .libs _libs
+install-pkginc_libHEADERS: $(pkginc_lib_HEADERS)
+ @$(NORMAL_INSTALL)
+ @list='$(pkginc_lib_HEADERS)'; test -n "$(pkginc_libdir)" || list=; \
+ if test -n "$$list"; then \
+ echo " $(MKDIR_P) '$(DESTDIR)$(pkginc_libdir)'"; \
+ $(MKDIR_P) "$(DESTDIR)$(pkginc_libdir)" || exit 1; \
+ fi; \
+ for p in $$list; do \
+ if test -f "$$p"; then d=; else d="$(srcdir)/"; fi; \
+ echo "$$d$$p"; \
+ done | $(am__base_list) | \
+ while read files; do \
+ echo " $(INSTALL_HEADER) $$files '$(DESTDIR)$(pkginc_libdir)'"; \
+ $(INSTALL_HEADER) $$files "$(DESTDIR)$(pkginc_libdir)" || exit $$?; \
+ done
+
+uninstall-pkginc_libHEADERS:
+ @$(NORMAL_UNINSTALL)
+ @list='$(pkginc_lib_HEADERS)'; test -n "$(pkginc_libdir)" || list=; \
+ files=`for p in $$list; do echo $$p; done | sed -e 's|^.*/||'`; \
+ dir='$(DESTDIR)$(pkginc_libdir)'; $(am__uninstall_files_from_dir)
+
+ID: $(am__tagged_files)
+ $(am__define_uniq_tagged_files); mkid -fID $$unique
+tags: tags-am
+TAGS: tags
+
+tags-am: $(TAGS_DEPENDENCIES) $(am__tagged_files)
+ set x; \
+ here=`pwd`; \
+ $(am__define_uniq_tagged_files); \
+ shift; \
+ if test -z "$(ETAGS_ARGS)$$*$$unique"; then :; else \
+ test -n "$$unique" || unique=$$empty_fix; \
+ if test $$# -gt 0; then \
+ $(ETAGS) $(ETAGSFLAGS) $(AM_ETAGSFLAGS) $(ETAGS_ARGS) \
+ "$$@" $$unique; \
+ else \
+ $(ETAGS) $(ETAGSFLAGS) $(AM_ETAGSFLAGS) $(ETAGS_ARGS) \
+ $$unique; \
+ fi; \
+ fi
+ctags: ctags-am
+
+CTAGS: ctags
+ctags-am: $(TAGS_DEPENDENCIES) $(am__tagged_files)
+ $(am__define_uniq_tagged_files); \
+ test -z "$(CTAGS_ARGS)$$unique" \
+ || $(CTAGS) $(CTAGSFLAGS) $(AM_CTAGSFLAGS) $(CTAGS_ARGS) \
+ $$unique
+
+GTAGS:
+ here=`$(am__cd) $(top_builddir) && pwd` \
+ && $(am__cd) $(top_srcdir) \
+ && gtags -i $(GTAGS_ARGS) "$$here"
+cscopelist: cscopelist-am
+
+cscopelist-am: $(am__tagged_files)
+ list='$(am__tagged_files)'; \
+ case "$(srcdir)" in \
+ [\\/]* | ?:[\\/]*) sdir="$(srcdir)" ;; \
+ *) sdir=$(subdir)/$(srcdir) ;; \
+ esac; \
+ for i in $$list; do \
+ if test -f "$$i"; then \
+ echo "$(subdir)/$$i"; \
+ else \
+ echo "$$sdir/$$i"; \
+ fi; \
+ done >> $(top_builddir)/cscope.files
+
+distclean-tags:
+ -rm -f TAGS ID GTAGS GRTAGS GSYMS GPATH tags
+
+distdir: $(BUILT_SOURCES)
+ $(MAKE) $(AM_MAKEFLAGS) distdir-am
+
+distdir-am: $(DISTFILES)
+ @srcdirstrip=`echo "$(srcdir)" | sed 's/[].[^$$\\*]/\\\\&/g'`; \
+ topsrcdirstrip=`echo "$(top_srcdir)" | sed 's/[].[^$$\\*]/\\\\&/g'`; \
+ list='$(DISTFILES)'; \
+ dist_files=`for file in $$list; do echo $$file; done | \
+ sed -e "s|^$$srcdirstrip/||;t" \
+ -e "s|^$$topsrcdirstrip/|$(top_builddir)/|;t"`; \
+ case $$dist_files in \
+ */*) $(MKDIR_P) `echo "$$dist_files" | \
+ sed '/\//!d;s|^|$(distdir)/|;s,/[^/]*$$,,' | \
+ sort -u` ;; \
+ esac; \
+ for file in $$dist_files; do \
+ if test -f $$file || test -d $$file; then d=.; else d=$(srcdir); fi; \
+ if test -d $$d/$$file; then \
+ dir=`echo "/$$file" | sed -e 's,/[^/]*$$,,'`; \
+ if test -d "$(distdir)/$$file"; then \
+ find "$(distdir)/$$file" -type d ! -perm -700 -exec chmod u+rwx {} \;; \
+ fi; \
+ if test -d $(srcdir)/$$file && test $$d != $(srcdir); then \
+ cp -fpR $(srcdir)/$$file "$(distdir)$$dir" || exit 1; \
+ find "$(distdir)/$$file" -type d ! -perm -700 -exec chmod u+rwx {} \;; \
+ fi; \
+ cp -fpR $$d/$$file "$(distdir)$$dir" || exit 1; \
+ else \
+ test -f "$(distdir)/$$file" \
+ || cp -p $$d/$$file "$(distdir)/$$file" \
+ || exit 1; \
+ fi; \
+ done
+check-am: all-am
+check: check-am
+all-am: Makefile $(LTLIBRARIES) $(HEADERS)
+installdirs:
+ for dir in "$(DESTDIR)$(pkginc_libdir)"; do \
+ test -z "$$dir" || $(MKDIR_P) "$$dir"; \
+ done
+install: install-am
+install-exec: install-exec-am
+install-data: install-data-am
+uninstall: uninstall-am
+
+install-am: all-am
+ @$(MAKE) $(AM_MAKEFLAGS) install-exec-am install-data-am
+
+installcheck: installcheck-am
+install-strip:
+ if test -z '$(STRIP)'; then \
+ $(MAKE) $(AM_MAKEFLAGS) INSTALL_PROGRAM="$(INSTALL_STRIP_PROGRAM)" \
+ install_sh_PROGRAM="$(INSTALL_STRIP_PROGRAM)" INSTALL_STRIP_FLAG=-s \
+ install; \
+ else \
+ $(MAKE) $(AM_MAKEFLAGS) INSTALL_PROGRAM="$(INSTALL_STRIP_PROGRAM)" \
+ install_sh_PROGRAM="$(INSTALL_STRIP_PROGRAM)" INSTALL_STRIP_FLAG=-s \
+ "INSTALL_PROGRAM_ENV=STRIPPROG='$(STRIP)'" install; \
+ fi
+mostlyclean-generic:
+
+clean-generic:
+
+distclean-generic:
+ -test -z "$(CONFIG_CLEAN_FILES)" || rm -f $(CONFIG_CLEAN_FILES)
+ -test . = "$(srcdir)" || test -z "$(CONFIG_CLEAN_VPATH_FILES)" || rm -f $(CONFIG_CLEAN_VPATH_FILES)
+
+maintainer-clean-generic:
+ @echo "This command is intended for maintainers to use"
+ @echo "it deletes files that may require special tools to rebuild."
+clean: clean-am
+
+clean-am: clean-generic clean-libtool clean-noinstLTLIBRARIES \
+ mostlyclean-am
+
+distclean: distclean-am
+ -rm -f ./$(DEPDIR)/dbox-attachment.Plo
+ -rm -f ./$(DEPDIR)/dbox-file-fix.Plo
+ -rm -f ./$(DEPDIR)/dbox-file.Plo
+ -rm -f ./$(DEPDIR)/dbox-mail.Plo
+ -rm -f ./$(DEPDIR)/dbox-save.Plo
+ -rm -f ./$(DEPDIR)/dbox-storage.Plo
+ -rm -f Makefile
+distclean-am: clean-am distclean-compile distclean-generic \
+ distclean-tags
+
+dvi: dvi-am
+
+dvi-am:
+
+html: html-am
+
+html-am:
+
+info: info-am
+
+info-am:
+
+install-data-am: install-pkginc_libHEADERS
+
+install-dvi: install-dvi-am
+
+install-dvi-am:
+
+install-exec-am:
+
+install-html: install-html-am
+
+install-html-am:
+
+install-info: install-info-am
+
+install-info-am:
+
+install-man:
+
+install-pdf: install-pdf-am
+
+install-pdf-am:
+
+install-ps: install-ps-am
+
+install-ps-am:
+
+installcheck-am:
+
+maintainer-clean: maintainer-clean-am
+ -rm -f ./$(DEPDIR)/dbox-attachment.Plo
+ -rm -f ./$(DEPDIR)/dbox-file-fix.Plo
+ -rm -f ./$(DEPDIR)/dbox-file.Plo
+ -rm -f ./$(DEPDIR)/dbox-mail.Plo
+ -rm -f ./$(DEPDIR)/dbox-save.Plo
+ -rm -f ./$(DEPDIR)/dbox-storage.Plo
+ -rm -f Makefile
+maintainer-clean-am: distclean-am maintainer-clean-generic
+
+mostlyclean: mostlyclean-am
+
+mostlyclean-am: mostlyclean-compile mostlyclean-generic \
+ mostlyclean-libtool
+
+pdf: pdf-am
+
+pdf-am:
+
+ps: ps-am
+
+ps-am:
+
+uninstall-am: uninstall-pkginc_libHEADERS
+
+.MAKE: install-am install-strip
+
+.PHONY: CTAGS GTAGS TAGS all all-am am--depfiles check check-am clean \
+ clean-generic clean-libtool clean-noinstLTLIBRARIES \
+ cscopelist-am ctags ctags-am distclean distclean-compile \
+ distclean-generic distclean-libtool distclean-tags distdir dvi \
+ dvi-am html html-am info info-am install install-am \
+ install-data install-data-am install-dvi install-dvi-am \
+ install-exec install-exec-am install-html install-html-am \
+ install-info install-info-am install-man install-pdf \
+ install-pdf-am install-pkginc_libHEADERS install-ps \
+ install-ps-am install-strip installcheck installcheck-am \
+ installdirs maintainer-clean maintainer-clean-generic \
+ mostlyclean mostlyclean-compile mostlyclean-generic \
+ mostlyclean-libtool pdf pdf-am ps ps-am tags tags-am uninstall \
+ uninstall-am uninstall-pkginc_libHEADERS
+
+.PRECIOUS: Makefile
+
+
+# Tell versions [3.59,3.63) of GNU make to not export all variables.
+# Otherwise a system limit (for SysV at least) may be exceeded.
+.NOEXPORT:
diff --git a/src/lib-storage/index/dbox-common/dbox-attachment.c b/src/lib-storage/index/dbox-common/dbox-attachment.c
new file mode 100644
index 0000000..cfc3f62
--- /dev/null
+++ b/src/lib-storage/index/dbox-common/dbox-attachment.c
@@ -0,0 +1,77 @@
+/* Copyright (c) 2010-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "istream.h"
+#include "str.h"
+#include "dbox-file.h"
+#include "dbox-save.h"
+#include "dbox-attachment.h"
+
+void dbox_attachment_save_write_metadata(struct mail_save_context *ctx,
+ string_t *str)
+{
+ const ARRAY_TYPE(mail_attachment_extref) *extrefs;
+
+ extrefs = index_attachment_save_get_extrefs(ctx);
+ if (extrefs == NULL || array_count(extrefs) == 0)
+ return;
+
+ str_append_c(str, DBOX_METADATA_EXT_REF);
+ index_attachment_append_extrefs(str, extrefs);
+ str_append_c(str, '\n');
+}
+
+static int
+dbox_attachment_file_get_stream_from(struct dbox_file *file,
+ const char *ext_refs,
+ struct istream **stream,
+ const char **error_r)
+{
+ const char *path_suffix;
+ uoff_t msg_size;
+
+ if (file->storage->attachment_dir == NULL) {
+ mail_storage_set_critical(&file->storage->storage,
+ "%s contains references to external attachments, "
+ "but mail_attachment_dir is unset", file->cur_path);
+ return -1;
+ }
+
+ msg_size = dbox_file_get_plaintext_size(file);
+ path_suffix = file->storage->v.get_attachment_path_suffix(file);
+ if (index_attachment_stream_get(file->storage->attachment_fs,
+ file->storage->attachment_dir,
+ path_suffix, stream, msg_size,
+ ext_refs, error_r) < 0)
+ return 0;
+ return 1;
+}
+
+int dbox_attachment_file_get_stream(struct dbox_file *file,
+ struct istream **stream)
+{
+ const char *ext_refs, *error;
+ int ret;
+
+ /* need to read metadata in case there are external references */
+ if ((ret = dbox_file_metadata_read(file)) <= 0)
+ return ret;
+
+ i_stream_seek(file->input, file->cur_offset + file->msg_header_size);
+
+ ext_refs = dbox_file_metadata_get(file, DBOX_METADATA_EXT_REF);
+ if (ext_refs == NULL)
+ return 1;
+
+ /* we have external references. */
+ T_BEGIN {
+ ret = dbox_attachment_file_get_stream_from(file, ext_refs,
+ stream, &error);
+ if (ret == 0) {
+ dbox_file_set_corrupted(file,
+ "Corrupted ext-refs metadata %s: %s",
+ ext_refs, error);
+ }
+ } T_END;
+ return ret;
+}
diff --git a/src/lib-storage/index/dbox-common/dbox-attachment.h b/src/lib-storage/index/dbox-common/dbox-attachment.h
new file mode 100644
index 0000000..a90ba54
--- /dev/null
+++ b/src/lib-storage/index/dbox-common/dbox-attachment.h
@@ -0,0 +1,16 @@
+#ifndef DBOX_ATTACHMENT_H
+#define DBOX_ATTACHMENT_H
+
+#include "index-attachment.h"
+
+struct dbox_file;
+
+void dbox_attachment_save_write_metadata(struct mail_save_context *ctx,
+ string_t *str);
+
+/* Build a single message body stream out of the current message and all of its
+ attachments. */
+int dbox_attachment_file_get_stream(struct dbox_file *file,
+ struct istream **stream);
+
+#endif
diff --git a/src/lib-storage/index/dbox-common/dbox-file-fix.c b/src/lib-storage/index/dbox-common/dbox-file-fix.c
new file mode 100644
index 0000000..1c44ca4
--- /dev/null
+++ b/src/lib-storage/index/dbox-common/dbox-file-fix.c
@@ -0,0 +1,519 @@
+/* Copyright (c) 2009-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "hex-dec.h"
+#include "istream.h"
+#include "ostream.h"
+#include "message-size.h"
+#include "dbox-storage.h"
+#include "dbox-file.h"
+
+#include <stdio.h>
+
+#define DBOX_MAIL_FILE_BROKEN_COPY_SUFFIX ".broken"
+
+static int
+dbox_file_match_pre_magic(struct istream *input,
+ uoff_t *pre_offset, size_t *need_bytes)
+{
+ const struct dbox_message_header *hdr;
+ const unsigned char *data;
+ size_t size;
+ uoff_t offset = input->v_offset;
+ bool have_lf = FALSE;
+
+ data = i_stream_get_data(input, &size);
+ if (data[0] == '\n') {
+ data++; size--; offset++;
+ have_lf = TRUE;
+ }
+ i_assert(data[0] == DBOX_MAGIC_PRE[0]);
+ if (size < sizeof(*hdr)) {
+ *need_bytes = sizeof(*hdr) + (have_lf ? 1 : 0);
+ return -1;
+ }
+ hdr = (const void *)data;
+ if (memcmp(hdr->magic_pre, DBOX_MAGIC_PRE, strlen(DBOX_MAGIC_PRE)) != 0)
+ return 0;
+ if (hdr->type != DBOX_MESSAGE_TYPE_NORMAL)
+ return 0;
+ if (hdr->space1 != ' ' || hdr->space2 != ' ')
+ return 0;
+ if (hex2dec(hdr->message_size_hex, sizeof(hdr->message_size_hex)) == 0 &&
+ memcmp(hdr->message_size_hex, "0000000000000000", sizeof(hdr->message_size_hex)) != 0)
+ return 0;
+
+ *pre_offset = offset;
+ return 1;
+}
+
+static bool memchr_nocontrol(const unsigned char *data, char chr,
+ unsigned int len, const unsigned char **pos_r)
+{
+ unsigned int i;
+
+ for (i = 0; i < len; i++) {
+ if (data[i] == chr) {
+ *pos_r = data+i;
+ return TRUE;
+ }
+ if (data[i] < ' ')
+ return FALSE;
+ }
+ *pos_r = NULL;
+ return TRUE;
+}
+
+static int
+dbox_file_match_post_magic(struct istream *input, bool input_full,
+ size_t *need_bytes)
+{
+ const unsigned char *data, *p;
+ size_t i, size;
+ bool allow_control;
+
+ data = i_stream_get_data(input, &size);
+ if (size < strlen(DBOX_MAGIC_POST)) {
+ *need_bytes = strlen(DBOX_MAGIC_POST);
+ return -1;
+ }
+ if (memcmp(data, DBOX_MAGIC_POST, strlen(DBOX_MAGIC_POST)) != 0)
+ return 0;
+
+ /* see if the metadata block looks valid */
+ for (i = strlen(DBOX_MAGIC_POST); i < size; ) {
+ switch (data[i]) {
+ case '\n':
+ return 1;
+ case DBOX_METADATA_GUID:
+ case DBOX_METADATA_POP3_UIDL:
+ case DBOX_METADATA_ORIG_MAILBOX:
+ case DBOX_METADATA_OLDV1_KEYWORDS:
+ /* these could contain anything */
+ allow_control = TRUE;
+ break;
+ case DBOX_METADATA_POP3_ORDER:
+ case DBOX_METADATA_RECEIVED_TIME:
+ case DBOX_METADATA_PHYSICAL_SIZE:
+ case DBOX_METADATA_VIRTUAL_SIZE:
+ case DBOX_METADATA_EXT_REF:
+ case DBOX_METADATA_OLDV1_EXPUNGED:
+ case DBOX_METADATA_OLDV1_FLAGS:
+ case DBOX_METADATA_OLDV1_SAVE_TIME:
+ case DBOX_METADATA_OLDV1_SPACE:
+ /* no control chars */
+ allow_control = FALSE;
+ break;
+ default:
+ if (data[i] < 'A' || data[i] > 'Z')
+ return 0;
+ /* unknown */
+ allow_control = TRUE;
+ break;
+ }
+ if (allow_control) {
+ p = memchr(data+i, '\n', size-i);
+ } else {
+ if (!memchr_nocontrol(data+i, '\n', size-i, &p))
+ return 0;
+ }
+ if (p == NULL) {
+ /* LF not found - try to find the end-of-metadata LF */
+ if (input_full) {
+ /* can't look any further - assume it's ok */
+ return 1;
+ }
+ *need_bytes = size+1;
+ return -1;
+ }
+ i = p - data+1;
+ }
+ *need_bytes = size+1;
+ return -1;
+}
+
+static int
+dbox_file_find_next_magic(struct dbox_file *file, uoff_t *offset_r, bool *pre_r)
+{
+ /* We're scanning message bodies here, trying to find the beginning of
+ the next message. Although our magic strings are very unlikely to
+ be found in regular emails, they are much more likely when emails
+ are stored compressed.. So try to be sure we find the correct
+ magic markers. */
+
+ struct istream *input = file->input;
+ uoff_t orig_offset, pre_offset, post_offset, prev_offset;
+ const unsigned char *data, *magic;
+ size_t size, need_bytes, prev_need_bytes;
+ int ret, match;
+
+ *pre_r = FALSE;
+
+ orig_offset = prev_offset = input->v_offset;
+ need_bytes = strlen(DBOX_MAGIC_POST); prev_need_bytes = 0;
+ while ((ret = i_stream_read_bytes(input, &data, &size, need_bytes)) > 0 ||
+ ret == -2) {
+ /* search for the beginning of a potential pre/post magic */
+ i_assert(size > 1);
+ i_assert(prev_offset != input->v_offset ||
+ need_bytes > prev_need_bytes);
+ prev_offset = input->v_offset;
+ prev_need_bytes = need_bytes;
+
+ magic = memchr(data, DBOX_MAGIC_PRE[0], size);
+ if (magic == NULL) {
+ i_stream_skip(input, size-1);
+ need_bytes = strlen(DBOX_MAGIC_POST);
+ continue;
+ }
+ if (magic == data && input->v_offset == orig_offset) {
+ /* beginning of the file */
+ } else if (magic != data && magic[-1] == '\n') {
+ /* PRE/POST block? leave \n */
+ i_stream_skip(input, magic-data-1);
+ } else {
+ i_stream_skip(input, magic-data+1);
+ need_bytes = strlen(DBOX_MAGIC_POST);
+ continue;
+ }
+
+ pre_offset = UOFF_T_MAX;
+ match = dbox_file_match_pre_magic(input, &pre_offset, &need_bytes);
+ if (match < 0) {
+ /* more data needed */
+ if (ret == -2) {
+ i_stream_skip(input, 2);
+ need_bytes = strlen(DBOX_MAGIC_POST);
+ }
+ continue;
+ }
+ if (match > 0)
+ *pre_r = TRUE;
+
+ match = dbox_file_match_post_magic(input, ret == -2, &need_bytes);
+ if (match < 0) {
+ /* more data needed */
+ if (ret == -2) {
+ i_stream_skip(input, 2);
+ need_bytes = strlen(DBOX_MAGIC_POST);
+ }
+ continue;
+ }
+ if (match > 0) {
+ post_offset = input->v_offset;
+ if (pre_offset == UOFF_T_MAX ||
+ post_offset < pre_offset) {
+ pre_offset = post_offset;
+ *pre_r = FALSE;
+ }
+ }
+
+ if (pre_offset != UOFF_T_MAX) {
+ *offset_r = pre_offset;
+ ret = 1;
+ break;
+ }
+ i_stream_skip(input, size-1);
+ }
+ if (ret <= 0) {
+ i_assert(ret == -1);
+ if (input->stream_errno != 0)
+ dbox_file_set_syscall_error(file, "read()");
+ else {
+ ret = 0;
+ *offset_r = input->v_offset;
+ }
+ }
+ i_stream_seek(input, orig_offset);
+ return ret <= 0 ? ret : 1;
+}
+
+static int
+stream_copy(struct dbox_file *file, struct ostream *output,
+ const char *out_path, uoff_t count)
+{
+ struct istream *input;
+ int ret = 0;
+
+ input = i_stream_create_limit(file->input, count);
+ o_stream_nsend_istream(output, input);
+
+ if (input->stream_errno != 0) {
+ mail_storage_set_critical(&file->storage->storage,
+ "read(%s) failed: %s", file->cur_path,
+ i_stream_get_error(input));
+ ret = -1;
+ } else if (o_stream_flush(output) < 0) {
+ mail_storage_set_critical(&file->storage->storage,
+ "write(%s) failed: %s", out_path,
+ o_stream_get_error(output));
+ ret = -1;
+ } else if (input->v_offset != count) {
+ mail_storage_set_critical(&file->storage->storage,
+ "o_stream_send_istream(%s) copied only %"
+ PRIuUOFF_T" of %"PRIuUOFF_T" bytes",
+ out_path, input->v_offset, count);
+ ret = -1;
+ }
+ i_stream_unref(&input);
+ return ret;
+}
+
+static void dbox_file_skip_broken_header(struct dbox_file *file)
+{
+ const size_t magic_len = strlen(DBOX_MAGIC_PRE);
+ const unsigned char *data;
+ size_t i, size;
+
+ /* if there's LF close to our position, assume that the header ends
+ there. */
+ data = i_stream_get_data(file->input, &size);
+ if (size > file->msg_header_size + 16)
+ size = file->msg_header_size + 16;
+ for (i = 0; i < size; i++) {
+ if (data[i] == '\n') {
+ i_stream_skip(file->input, i);
+ return;
+ }
+ }
+
+ /* skip at least the magic bytes if possible */
+ if (size > magic_len && memcmp(data, DBOX_MAGIC_PRE, magic_len) == 0)
+ i_stream_skip(file->input, magic_len);
+}
+
+static void
+dbox_file_copy_metadata(struct dbox_file *file, struct ostream *output,
+ bool *have_guid_r)
+{
+ const char *line;
+ uoff_t prev_offset = file->input->v_offset;
+
+ *have_guid_r = FALSE;
+ while ((line = i_stream_read_next_line(file->input)) != NULL) {
+ if (*line == DBOX_METADATA_OLDV1_SPACE || *line == '\0') {
+ /* end of metadata */
+ return;
+ }
+ if (*line < 32) {
+ /* broken - possibly a new pre-magic block */
+ i_stream_seek(file->input, prev_offset);
+ return;
+ }
+ if (*line == DBOX_METADATA_VIRTUAL_SIZE) {
+ /* it may be wrong - recreate it */
+ continue;
+ }
+ if (*line == DBOX_METADATA_GUID)
+ *have_guid_r = TRUE;
+ o_stream_nsend_str(output, line);
+ o_stream_nsend_str(output, "\n");
+ }
+}
+
+static int
+dbox_file_fix_write_stream(struct dbox_file *file, uoff_t start_offset,
+ const char *temp_path, struct ostream *output)
+{
+ struct dbox_message_header msg_hdr;
+ uoff_t offset, msg_size, hdr_offset, body_offset;
+ bool pre, write_header, have_guid;
+ struct message_size body;
+ bool has_nuls;
+ struct istream *body_input;
+ guid_128_t guid_128;
+ int ret;
+
+ i_stream_seek(file->input, 0);
+ if (start_offset > 0) {
+ /* copy the valid data */
+ if (stream_copy(file, output, temp_path, start_offset) < 0)
+ return -1;
+ } else {
+ /* the file header is broken. recreate it */
+ if (dbox_file_header_write(file, output) < 0) {
+ dbox_file_set_syscall_error(file, "write()");
+ return -1;
+ }
+ }
+
+ while ((ret = dbox_file_find_next_magic(file, &offset, &pre)) > 0) {
+ msg_size = offset - file->input->v_offset;
+ if (msg_size < 256 && pre) {
+ /* probably some garbage or some broken headers.
+ we most likely don't miss anything by skipping
+ over this data. */
+ i_stream_skip(file->input, msg_size);
+ hdr_offset = file->input->v_offset;
+ ret = dbox_file_read_mail_header(file, &msg_size);
+ if (ret <= 0) {
+ if (ret < 0)
+ return -1;
+ dbox_file_skip_broken_header(file);
+ body_offset = file->input->v_offset;
+ msg_size = UOFF_T_MAX;
+ } else {
+ i_stream_skip(file->input,
+ file->msg_header_size);
+ body_offset = file->input->v_offset;
+ i_stream_skip(file->input, msg_size);
+ }
+
+ ret = dbox_file_find_next_magic(file, &offset, &pre);
+ if (ret <= 0)
+ break;
+
+ if (!pre && msg_size == offset - body_offset) {
+ /* msg header ok, copy it */
+ i_stream_seek(file->input, hdr_offset);
+ if (stream_copy(file, output, temp_path,
+ file->msg_header_size) < 0)
+ return -1;
+ write_header = FALSE;
+ } else {
+ /* msg header is broken. write our own. */
+ i_stream_seek(file->input, body_offset);
+ if (msg_size != UOFF_T_MAX) {
+ /* previous magic find might have
+ skipped too much. seek back and
+ make sure */
+ ret = dbox_file_find_next_magic(file, &offset, &pre);
+ if (ret <= 0)
+ break;
+ }
+
+ write_header = TRUE;
+ msg_size = offset - body_offset;
+ }
+ } else {
+ /* treat this data as a separate message. */
+ write_header = TRUE;
+ body_offset = file->input->v_offset;
+ }
+ /* write msg header */
+ if (write_header) {
+ dbox_msg_header_fill(&msg_hdr, msg_size);
+ o_stream_nsend(output, &msg_hdr, sizeof(msg_hdr));
+ }
+ /* write msg body */
+ i_assert(file->input->v_offset == body_offset);
+ if (stream_copy(file, output, temp_path, msg_size) < 0)
+ return -1;
+ i_assert(file->input->v_offset == offset);
+
+ /* get message body size */
+ i_stream_seek(file->input, body_offset);
+ body_input = i_stream_create_limit(file->input, msg_size);
+ ret = message_get_body_size(body_input, &body, &has_nuls);
+ i_stream_unref(&body_input);
+ if (ret < 0) {
+ mail_storage_set_critical(&file->storage->storage,
+ "read(%s) failed: %s", file->cur_path,
+ i_stream_get_error(body_input));
+ return -1;
+ }
+
+ /* write msg metadata. */
+ i_assert(file->input->v_offset == offset);
+ ret = dbox_file_metadata_skip_header(file);
+ if (ret < 0)
+ return -1;
+ o_stream_nsend_str(output, DBOX_MAGIC_POST);
+ if (ret == 0)
+ have_guid = FALSE;
+ else
+ dbox_file_copy_metadata(file, output, &have_guid);
+ if (!have_guid) {
+ guid_128_generate(guid_128);
+ o_stream_nsend_str(output,
+ t_strdup_printf("%c%s\n", DBOX_METADATA_GUID,
+ guid_128_to_string(guid_128)));
+ }
+ o_stream_nsend_str(output,
+ t_strdup_printf("%c%llx\n", DBOX_METADATA_VIRTUAL_SIZE,
+ (unsigned long long)body.virtual_size));
+ o_stream_nsend_str(output, "\n");
+ if (output->stream_errno != 0)
+ break;
+ }
+ if (o_stream_flush(output) < 0) {
+ mail_storage_set_critical(&file->storage->storage,
+ "write(%s) failed: %s", temp_path, o_stream_get_error(output));
+ ret = -1;
+ }
+ return ret;
+}
+
+int dbox_file_fix(struct dbox_file *file, uoff_t start_offset)
+{
+ struct ostream *output;
+ const char *dir, *p, *temp_path, *broken_path;
+ bool deleted, have_messages;
+ int fd, ret;
+
+ i_assert(dbox_file_is_open(file));
+
+ p = strrchr(file->cur_path, '/');
+ i_assert(p != NULL);
+ dir = t_strdup_until(file->cur_path, p);
+
+ temp_path = t_strdup_printf("%s/%s", dir, dbox_generate_tmp_filename());
+ fd = file->storage->v.file_create_fd(file, temp_path, FALSE);
+ if (fd == -1)
+ return -1;
+
+ output = o_stream_create_fd_file(fd, 0, FALSE);
+ o_stream_cork(output);
+ ret = dbox_file_fix_write_stream(file, start_offset, temp_path, output);
+ if (ret < 0)
+ o_stream_abort(output);
+ have_messages = output->offset > file->file_header_size;
+ o_stream_unref(&output);
+ if (close(fd) < 0) {
+ mail_storage_set_critical(&file->storage->storage,
+ "close(%s) failed: %m", temp_path);
+ ret = -1;
+ }
+ if (ret < 0) {
+ if (unlink(temp_path) < 0) {
+ mail_storage_set_critical(&file->storage->storage,
+ "unlink(%s) failed: %m", temp_path);
+ }
+ return -1;
+ }
+ /* keep a copy of the original file in case someone wants to look
+ at it */
+ broken_path = t_strconcat(file->cur_path,
+ DBOX_MAIL_FILE_BROKEN_COPY_SUFFIX, NULL);
+ if (link(file->cur_path, broken_path) < 0) {
+ mail_storage_set_critical(&file->storage->storage,
+ "link(%s, %s) failed: %m",
+ file->cur_path, broken_path);
+ } else {
+ i_warning("dbox: Copy of the broken file saved to %s",
+ broken_path);
+ }
+ if (!have_messages) {
+ /* the resulting file has no messages. just delete the file. */
+ dbox_file_close(file);
+ i_unlink(temp_path);
+ i_unlink(file->cur_path);
+ return 0;
+ }
+ if (rename(temp_path, file->cur_path) < 0) {
+ mail_storage_set_critical(&file->storage->storage,
+ "rename(%s, %s) failed: %m",
+ temp_path, file->cur_path);
+ return -1;
+ }
+
+ /* file was successfully recreated - reopen it */
+ dbox_file_close(file);
+ if (dbox_file_open(file, &deleted) <= 0) {
+ mail_storage_set_critical(&file->storage->storage,
+ "dbox_file_fix(%s): reopening file failed",
+ file->cur_path);
+ return -1;
+ }
+ return 1;
+}
diff --git a/src/lib-storage/index/dbox-common/dbox-file.c b/src/lib-storage/index/dbox-common/dbox-file.c
new file mode 100644
index 0000000..16810b0
--- /dev/null
+++ b/src/lib-storage/index/dbox-common/dbox-file.c
@@ -0,0 +1,796 @@
+/* Copyright (c) 2007-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "ioloop.h"
+#include "array.h"
+#include "hex-dec.h"
+#include "hex-binary.h"
+#include "hostpid.h"
+#include "istream.h"
+#include "ostream.h"
+#include "file-lock.h"
+#include "file-dotlock.h"
+#include "mkdir-parents.h"
+#include "eacces-error.h"
+#include "str.h"
+#include "dbox-storage.h"
+#include "dbox-file.h"
+
+#include <stdio.h>
+#include <unistd.h>
+#include <ctype.h>
+#include <fcntl.h>
+
+#define DBOX_READ_BLOCK_SIZE IO_BLOCK_SIZE
+
+#ifndef DBOX_FILE_LOCK_METHOD_FLOCK
+static const struct dotlock_settings dotlock_set = {
+ .stale_timeout = 60*10,
+ .use_excl_lock = TRUE
+};
+#endif
+
+const char *dbox_generate_tmp_filename(void)
+{
+ static unsigned int create_count = 0;
+
+ return t_strdup_printf(DBOX_TEMP_FILE_PREFIX"%"PRIdTIME_T".P%sQ%uM%u.%s",
+ ioloop_timeval.tv_sec, my_pid,
+ create_count++,
+ (unsigned int)ioloop_timeval.tv_usec,
+ my_hostname);
+}
+
+void dbox_file_set_syscall_error(struct dbox_file *file, const char *function)
+{
+ mail_storage_set_critical(&file->storage->storage,
+ "%s failed for file %s: %m",
+ function, file->cur_path);
+}
+
+void dbox_file_set_corrupted(struct dbox_file *file, const char *reason, ...)
+{
+ va_list args;
+
+ va_start(args, reason);
+ mail_storage_set_critical(&file->storage->storage,
+ "Corrupted dbox file %s (around offset=%"PRIuUOFF_T"): %s",
+ file->cur_path, file->input == NULL ? 0 : file->input->v_offset,
+ t_strdup_vprintf(reason, args));
+ va_end(args);
+
+ file->storage->v.set_file_corrupted(file);
+}
+
+void dbox_file_init(struct dbox_file *file)
+{
+ file->refcount = 1;
+ file->fd = -1;
+ file->cur_offset = UOFF_T_MAX;
+ file->cur_path = file->primary_path;
+}
+
+void dbox_file_free(struct dbox_file *file)
+{
+ i_assert(file->refcount == 0);
+
+ pool_unref(&file->metadata_pool);
+ dbox_file_close(file);
+ i_free(file->primary_path);
+ i_free(file->alt_path);
+ i_free(file);
+}
+
+void dbox_file_unref(struct dbox_file **_file)
+{
+ struct dbox_file *file = *_file;
+
+ *_file = NULL;
+
+ i_assert(file->refcount > 0);
+ if (--file->refcount == 0)
+ file->storage->v.file_unrefed(file);
+}
+
+static int dbox_file_parse_header(struct dbox_file *file, const char *line)
+{
+ const char *const *tmp, *value;
+ enum dbox_header_key key;
+
+ file->file_version = *line - '0';
+ if (!i_isdigit(line[0]) || line[1] != ' ' ||
+ (file->file_version != 1 && file->file_version != DBOX_VERSION)) {
+ dbox_file_set_corrupted(file, "Invalid dbox version");
+ return -1;
+ }
+ line += 2;
+
+ file->msg_header_size = 0;
+
+ for (tmp = t_strsplit(line, " "); *tmp != NULL; tmp++) {
+ uintmax_t time;
+ key = **tmp;
+ value = *tmp + 1;
+
+ switch (key) {
+ case DBOX_HEADER_OLDV1_APPEND_OFFSET:
+ break;
+ case DBOX_HEADER_MSG_HEADER_SIZE:
+ if (str_to_uint_hex(value, &file->msg_header_size) < 0) {
+ dbox_file_set_corrupted(file, "Invalid message header size");
+ return -1;
+ }
+ break;
+ case DBOX_HEADER_CREATE_STAMP:
+ if (str_to_uintmax_hex(value, &time) < 0) {
+ dbox_file_set_corrupted(file, "Invalid create time stamp");
+ return -1;
+ }
+ file->create_time = (time_t)time;
+ break;
+ }
+ }
+
+ if (file->msg_header_size == 0) {
+ dbox_file_set_corrupted(file, "Missing message header size");
+ return -1;
+ }
+ return 0;
+}
+
+static int dbox_file_read_header(struct dbox_file *file)
+{
+ const char *line;
+ unsigned int hdr_size;
+ int ret;
+
+ i_stream_seek(file->input, 0);
+ line = i_stream_read_next_line(file->input);
+ if (line == NULL) {
+ if (file->input->stream_errno == 0) {
+ dbox_file_set_corrupted(file,
+ "EOF while reading file header");
+ return 0;
+ }
+
+ dbox_file_set_syscall_error(file, "read()");
+ return -1;
+ }
+ hdr_size = file->input->v_offset;
+ T_BEGIN {
+ ret = dbox_file_parse_header(file, line) < 0 ? 0 : 1;
+ } T_END;
+ if (ret > 0)
+ file->file_header_size = hdr_size;
+ return ret;
+}
+
+static int dbox_file_open_fd(struct dbox_file *file, bool try_altpath)
+{
+ const char *path;
+ int flags = O_RDWR;
+ bool alt = FALSE;
+
+ /* try the primary path first */
+ path = file->primary_path;
+ while ((file->fd = open(path, flags)) == -1) {
+ if (errno == EACCES && flags == O_RDWR) {
+ flags = O_RDONLY;
+ continue;
+ }
+ if (errno != ENOENT) {
+ mail_storage_set_critical(&file->storage->storage,
+ "open(%s) failed: %m", path);
+ return -1;
+ }
+
+ if (file->alt_path == NULL || alt || !try_altpath) {
+ /* not found */
+ return 0;
+ }
+
+ /* try the alternative path */
+ path = file->alt_path;
+ alt = TRUE;
+ }
+ file->cur_path = path;
+ return 1;
+}
+
+static int dbox_file_open_full(struct dbox_file *file, bool try_altpath,
+ bool *notfound_r)
+{
+ int ret, fd;
+
+ *notfound_r = FALSE;
+ if (file->input != NULL)
+ return 1;
+
+ if (file->fd == -1) {
+ T_BEGIN {
+ ret = dbox_file_open_fd(file, try_altpath);
+ } T_END;
+ if (ret <= 0) {
+ if (ret < 0)
+ return -1;
+ *notfound_r = TRUE;
+ return 1;
+ }
+ }
+
+ /* we're manually checking at dbox_file_close() if we need to close the
+ fd or not. */
+ fd = file->fd;
+ file->input = i_stream_create_fd_autoclose(&fd, DBOX_READ_BLOCK_SIZE);
+ i_stream_set_name(file->input, file->cur_path);
+ i_stream_set_init_buffer_size(file->input, DBOX_READ_BLOCK_SIZE);
+ return dbox_file_read_header(file);
+}
+
+int dbox_file_open(struct dbox_file *file, bool *deleted_r)
+{
+ return dbox_file_open_full(file, TRUE, deleted_r);
+}
+
+int dbox_file_open_primary(struct dbox_file *file, bool *notfound_r)
+{
+ return dbox_file_open_full(file, FALSE, notfound_r);
+}
+
+int dbox_file_stat(struct dbox_file *file, struct stat *st_r)
+{
+ const char *path;
+ bool alt = FALSE;
+
+ if (dbox_file_is_open(file)) {
+ if (fstat(file->fd, st_r) < 0) {
+ mail_storage_set_critical(&file->storage->storage,
+ "fstat(%s) failed: %m", file->cur_path);
+ return -1;
+ }
+ return 0;
+ }
+
+ /* try the primary path first */
+ path = file->primary_path;
+ while (stat(path, st_r) < 0) {
+ if (errno != ENOENT) {
+ mail_storage_set_critical(&file->storage->storage,
+ "stat(%s) failed: %m", path);
+ return -1;
+ }
+
+ if (file->alt_path == NULL || alt) {
+ /* not found */
+ return -1;
+ }
+
+ /* try the alternative path */
+ path = file->alt_path;
+ alt = TRUE;
+ }
+ file->cur_path = path;
+ return 0;
+}
+
+int dbox_file_header_write(struct dbox_file *file, struct ostream *output)
+{
+ string_t *hdr;
+
+ hdr = t_str_new(128);
+ str_printfa(hdr, "%u %c%x %c%x\n", DBOX_VERSION,
+ DBOX_HEADER_MSG_HEADER_SIZE,
+ (unsigned int)sizeof(struct dbox_message_header),
+ DBOX_HEADER_CREATE_STAMP, (unsigned int)ioloop_time);
+
+ file->file_version = DBOX_VERSION;
+ file->file_header_size = str_len(hdr);
+ file->msg_header_size = sizeof(struct dbox_message_header);
+ return o_stream_send(output, str_data(hdr), str_len(hdr));
+}
+
+void dbox_file_close(struct dbox_file *file)
+{
+ dbox_file_unlock(file);
+ if (file->input != NULL) {
+ /* stream autocloses the fd when it gets destroyed. note that
+ the stream may outlive the struct dbox_file. */
+ i_stream_unref(&file->input);
+ file->fd = -1;
+ } else if (file->fd != -1) {
+ if (close(file->fd) < 0)
+ dbox_file_set_syscall_error(file, "close()");
+ file->fd = -1;
+ }
+ file->cur_offset = UOFF_T_MAX;
+}
+
+int dbox_file_try_lock(struct dbox_file *file)
+{
+ const char *error;
+ int ret;
+
+ i_assert(file->fd != -1);
+
+#ifdef DBOX_FILE_LOCK_METHOD_FLOCK
+ struct file_lock_settings lock_set = {
+ .lock_method = FILE_LOCK_METHOD_FLOCK,
+ };
+ ret = file_try_lock(file->fd, file->cur_path, F_WRLCK,
+ &lock_set, &file->lock, &error);
+ if (ret < 0) {
+ mail_storage_set_critical(&file->storage->storage,
+ "file_try_lock(%s) failed: %s", file->cur_path, error);
+ }
+#else
+ ret = file_dotlock_create(&dotlock_set, file->cur_path,
+ DOTLOCK_CREATE_FLAG_NONBLOCK, &file->lock);
+ if (ret < 0) {
+ mail_storage_set_critical(&file->storage->storage,
+ "file_dotlock_create(%s) failed: %m", file->cur_path);
+ }
+#endif
+ return ret;
+}
+
+void dbox_file_unlock(struct dbox_file *file)
+{
+ i_assert(!file->appending || file->lock == NULL);
+
+ if (file->lock != NULL) {
+#ifdef DBOX_FILE_LOCK_METHOD_FLOCK
+ file_unlock(&file->lock);
+#else
+ file_dotlock_delete(&file->lock);
+#endif
+ }
+ if (file->input != NULL)
+ i_stream_sync(file->input);
+}
+
+int dbox_file_read_mail_header(struct dbox_file *file, uoff_t *physical_size_r)
+{
+ struct dbox_message_header hdr;
+ const unsigned char *data;
+ size_t size;
+ int ret;
+
+ ret = i_stream_read_bytes(file->input, &data, &size,
+ file->msg_header_size);
+ if (ret <= 0) {
+ if (file->input->stream_errno == 0) {
+ /* EOF, broken offset or file truncated */
+ dbox_file_set_corrupted(file, "EOF reading msg header "
+ "(got %zu/%u bytes)",
+ size, file->msg_header_size);
+ return 0;
+ }
+ dbox_file_set_syscall_error(file, "read()");
+ return -1;
+ }
+ memcpy(&hdr, data, I_MIN(sizeof(hdr), file->msg_header_size));
+ if (memcmp(hdr.magic_pre, DBOX_MAGIC_PRE, sizeof(hdr.magic_pre)) != 0) {
+ /* probably broken offset */
+ dbox_file_set_corrupted(file, "msg header has bad magic value");
+ return 0;
+ }
+
+ if (data[file->msg_header_size-1] != '\n') {
+ dbox_file_set_corrupted(file, "msg header doesn't end with LF");
+ return 0;
+ }
+
+ *physical_size_r = hex2dec(hdr.message_size_hex,
+ sizeof(hdr.message_size_hex));
+ return 1;
+}
+
+int dbox_file_seek(struct dbox_file *file, uoff_t offset)
+{
+ uoff_t size;
+ int ret;
+
+ i_assert(file->input != NULL);
+
+ if (offset == 0)
+ offset = file->file_header_size;
+
+ if (offset != file->cur_offset) {
+ i_stream_seek(file->input, offset);
+ ret = dbox_file_read_mail_header(file, &size);
+ if (ret <= 0)
+ return ret;
+ file->cur_offset = offset;
+ file->cur_physical_size = size;
+ }
+ i_stream_seek(file->input, offset + file->msg_header_size);
+ return 1;
+}
+
+static int
+dbox_file_seek_next_at_metadata(struct dbox_file *file, uoff_t *offset)
+{
+ const char *line;
+ size_t buf_size;
+ int ret;
+
+ i_stream_seek(file->input, *offset);
+ if ((ret = dbox_file_metadata_skip_header(file)) <= 0)
+ return ret;
+
+ /* skip over the actual metadata */
+ buf_size = i_stream_get_max_buffer_size(file->input);
+ i_stream_set_max_buffer_size(file->input, SIZE_MAX);
+ while ((line = i_stream_read_next_line(file->input)) != NULL) {
+ if (*line == DBOX_METADATA_OLDV1_SPACE || *line == '\0') {
+ /* end of metadata */
+ break;
+ }
+ }
+ i_stream_set_max_buffer_size(file->input, buf_size);
+ *offset = file->input->v_offset;
+ return 1;
+}
+
+void dbox_file_seek_rewind(struct dbox_file *file)
+{
+ file->cur_offset = UOFF_T_MAX;
+}
+
+int dbox_file_seek_next(struct dbox_file *file, uoff_t *offset_r, bool *last_r)
+{
+ uoff_t offset;
+ int ret;
+
+ i_assert(file->input != NULL);
+
+ if (file->cur_offset == UOFF_T_MAX) {
+ /* first mail. we may not have read the file at all yet,
+ so set the offset afterwards. */
+ offset = 0;
+ } else {
+ offset = file->cur_offset + file->msg_header_size +
+ file->cur_physical_size;
+ if ((ret = dbox_file_seek_next_at_metadata(file, &offset)) <= 0) {
+ *offset_r = file->cur_offset;
+ return ret;
+ }
+ if (i_stream_read_eof(file->input)) {
+ *last_r = TRUE;
+ return 0;
+ }
+ }
+ *offset_r = offset;
+
+ *last_r = FALSE;
+
+ ret = dbox_file_seek(file, offset);
+ if (*offset_r == 0)
+ *offset_r = file->file_header_size;
+ return ret;
+}
+
+struct dbox_file_append_context *dbox_file_append_init(struct dbox_file *file)
+{
+ struct dbox_file_append_context *ctx;
+
+ i_assert(!file->appending);
+
+ file->appending = TRUE;
+
+ ctx = i_new(struct dbox_file_append_context, 1);
+ ctx->file = file;
+ if (file->fd != -1) {
+ ctx->output = o_stream_create_fd_file(file->fd, 0, FALSE);
+ o_stream_set_name(ctx->output, file->cur_path);
+ o_stream_set_finish_via_child(ctx->output, FALSE);
+ o_stream_cork(ctx->output);
+ }
+ return ctx;
+}
+
+int dbox_file_append_commit(struct dbox_file_append_context **_ctx)
+{
+ struct dbox_file_append_context *ctx = *_ctx;
+ int ret;
+
+ i_assert(ctx->file->appending);
+
+ *_ctx = NULL;
+
+ ret = dbox_file_append_flush(ctx);
+ if (ctx->last_checkpoint_offset != ctx->output->offset) {
+ o_stream_close(ctx->output);
+ if (ftruncate(ctx->file->fd, ctx->last_checkpoint_offset) < 0) {
+ dbox_file_set_syscall_error(ctx->file, "ftruncate()");
+ return -1;
+ }
+ }
+ o_stream_unref(&ctx->output);
+ ctx->file->appending = FALSE;
+ i_free(ctx);
+ return ret;
+}
+
+void dbox_file_append_rollback(struct dbox_file_append_context **_ctx)
+{
+ struct dbox_file_append_context *ctx = *_ctx;
+ struct dbox_file *file = ctx->file;
+ bool close_file = FALSE;
+
+ i_assert(ctx->file->appending);
+
+ *_ctx = NULL;
+ if (ctx->first_append_offset == 0) {
+ /* nothing changed */
+ } else if (ctx->first_append_offset == file->file_header_size) {
+ /* rolling back everything */
+ if (unlink(file->cur_path) < 0)
+ dbox_file_set_syscall_error(file, "unlink()");
+ close_file = TRUE;
+ } else {
+ /* truncating only some mails */
+ o_stream_close(ctx->output);
+ if (ftruncate(file->fd, ctx->first_append_offset) < 0)
+ dbox_file_set_syscall_error(file, "ftruncate()");
+ }
+ if (ctx->output != NULL) {
+ o_stream_abort(ctx->output);
+ o_stream_unref(&ctx->output);
+ }
+ i_free(ctx);
+
+ if (close_file)
+ dbox_file_close(file);
+ file->appending = FALSE;
+}
+
+int dbox_file_append_flush(struct dbox_file_append_context *ctx)
+{
+ struct mail_storage *storage = &ctx->file->storage->storage;
+
+ if (ctx->last_flush_offset == ctx->output->offset &&
+ ctx->last_checkpoint_offset == ctx->output->offset)
+ return 0;
+
+ if (o_stream_flush(ctx->output) < 0) {
+ dbox_file_set_syscall_error(ctx->file, "write()");
+ return -1;
+ }
+
+ if (ctx->last_checkpoint_offset != ctx->output->offset) {
+ if (ftruncate(ctx->file->fd, ctx->last_checkpoint_offset) < 0) {
+ dbox_file_set_syscall_error(ctx->file, "ftruncate()");
+ return -1;
+ }
+ if (o_stream_seek(ctx->output, ctx->last_checkpoint_offset) < 0) {
+ dbox_file_set_syscall_error(ctx->file, "lseek()");
+ return -1;
+ }
+ }
+
+ if (storage->set->parsed_fsync_mode != FSYNC_MODE_NEVER) {
+ if (fdatasync(ctx->file->fd) < 0) {
+ dbox_file_set_syscall_error(ctx->file, "fdatasync()");
+ return -1;
+ }
+ }
+ ctx->last_flush_offset = ctx->output->offset;
+ return 0;
+}
+
+void dbox_file_append_checkpoint(struct dbox_file_append_context *ctx)
+{
+ ctx->last_checkpoint_offset = ctx->output->offset;
+}
+
+int dbox_file_get_append_stream(struct dbox_file_append_context *ctx,
+ struct ostream **output_r)
+{
+ struct dbox_file *file = ctx->file;
+ struct stat st;
+
+ if (ctx->output == NULL) {
+ /* file creation had failed */
+ return -1;
+ }
+ if (ctx->last_checkpoint_offset != ctx->output->offset) {
+ /* a message was aborted. don't try appending to this
+ file anymore. */
+ return -1;
+ }
+
+ if (file->file_version == 0) {
+ /* newly created file, write the file header */
+ if (dbox_file_header_write(file, ctx->output) < 0) {
+ dbox_file_set_syscall_error(file, "write()");
+ return -1;
+ }
+ *output_r = ctx->output;
+ return 1;
+ }
+
+ /* file has existing mails */
+ if (file->file_version != DBOX_VERSION ||
+ file->msg_header_size != sizeof(struct dbox_message_header)) {
+ /* created by an incompatible version, can't append */
+ return 0;
+ }
+
+ if (ctx->output->offset == 0) {
+ /* first append to existing file. seek to eof first. */
+ if (fstat(file->fd, &st) < 0) {
+ dbox_file_set_syscall_error(file, "fstat()");
+ return -1;
+ }
+ if (st.st_size < file->msg_header_size) {
+ dbox_file_set_corrupted(file,
+ "dbox file size too small");
+ return 0;
+ }
+ if (o_stream_seek(ctx->output, st.st_size) < 0) {
+ dbox_file_set_syscall_error(file, "lseek()");
+ return -1;
+ }
+ }
+ *output_r = ctx->output;
+ return 1;
+}
+
+int dbox_file_metadata_skip_header(struct dbox_file *file)
+{
+ struct dbox_metadata_header metadata_hdr;
+ const unsigned char *data;
+ size_t size;
+ int ret;
+
+ ret = i_stream_read_bytes(file->input, &data, &size,
+ sizeof(metadata_hdr));
+ if (ret <= 0) {
+ if (file->input->stream_errno == 0) {
+ /* EOF, broken offset */
+ dbox_file_set_corrupted(file,
+ "Unexpected EOF while reading metadata header");
+ return 0;
+ }
+ dbox_file_set_syscall_error(file, "read()");
+ return -1;
+ }
+ memcpy(&metadata_hdr, data, sizeof(metadata_hdr));
+ if (memcmp(metadata_hdr.magic_post, DBOX_MAGIC_POST,
+ sizeof(metadata_hdr.magic_post)) != 0) {
+ /* probably broken offset */
+ dbox_file_set_corrupted(file,
+ "metadata header has bad magic value");
+ return 0;
+ }
+ i_stream_skip(file->input, sizeof(metadata_hdr));
+ return 1;
+}
+
+static int
+dbox_file_metadata_read_at(struct dbox_file *file, uoff_t metadata_offset)
+{
+ const char *line;
+ size_t buf_size;
+ int ret;
+
+ if (file->metadata_pool != NULL)
+ p_clear(file->metadata_pool);
+ else {
+ file->metadata_pool =
+ pool_alloconly_create("dbox metadata", 1024);
+ }
+ p_array_init(&file->metadata, file->metadata_pool, 16);
+
+ i_stream_seek(file->input, metadata_offset);
+ if ((ret = dbox_file_metadata_skip_header(file)) <= 0)
+ return ret;
+
+ ret = 0;
+ buf_size = i_stream_get_max_buffer_size(file->input);
+ /* use unlimited line length for metadata */
+ i_stream_set_max_buffer_size(file->input, SIZE_MAX);
+ while ((line = i_stream_read_next_line(file->input)) != NULL) {
+ if (*line == DBOX_METADATA_OLDV1_SPACE || *line == '\0') {
+ /* end of metadata */
+ ret = 1;
+ break;
+ }
+ line = p_strdup(file->metadata_pool, line);
+ array_push_back(&file->metadata, &line);
+ }
+ i_stream_set_max_buffer_size(file->input, buf_size);
+ if (ret == 0)
+ dbox_file_set_corrupted(file, "missing end-of-metadata line");
+ return ret;
+}
+
+int dbox_file_metadata_read(struct dbox_file *file)
+{
+ uoff_t metadata_offset;
+ int ret;
+
+ i_assert(file->cur_offset != UOFF_T_MAX);
+
+ if (file->metadata_read_offset == file->cur_offset)
+ return 1;
+
+ metadata_offset = file->cur_offset + file->msg_header_size +
+ file->cur_physical_size;
+ ret = dbox_file_metadata_read_at(file, metadata_offset);
+ if (ret <= 0)
+ return ret;
+
+ file->metadata_read_offset = file->cur_offset;
+ return 1;
+}
+
+const char *dbox_file_metadata_get(struct dbox_file *file,
+ enum dbox_metadata_key key)
+{
+ const char *const *metadata;
+ unsigned int i, count;
+
+ metadata = array_get(&file->metadata, &count);
+ for (i = 0; i < count; i++) {
+ if (*metadata[i] == (char)key)
+ return metadata[i] + 1;
+ }
+ return NULL;
+}
+
+uoff_t dbox_file_get_plaintext_size(struct dbox_file *file)
+{
+ const char *value;
+ uintmax_t size;
+
+ i_assert(file->metadata_read_offset == file->cur_offset);
+
+ /* see if we have it in metadata */
+ value = dbox_file_metadata_get(file, DBOX_METADATA_PHYSICAL_SIZE);
+ if (value == NULL ||
+ str_to_uintmax_hex(value, &size) < 0 ||
+ size > UOFF_T_MAX) {
+ /* no. that means we can use the size in the header */
+ return file->cur_physical_size;
+ }
+
+ return (uoff_t)size;
+}
+
+void dbox_msg_header_fill(struct dbox_message_header *dbox_msg_hdr,
+ uoff_t message_size)
+{
+ memset(dbox_msg_hdr, ' ', sizeof(*dbox_msg_hdr));
+ memcpy(dbox_msg_hdr->magic_pre, DBOX_MAGIC_PRE,
+ sizeof(dbox_msg_hdr->magic_pre));
+ dbox_msg_hdr->type = DBOX_MESSAGE_TYPE_NORMAL;
+ dec2hex(dbox_msg_hdr->message_size_hex, message_size,
+ sizeof(dbox_msg_hdr->message_size_hex));
+ dbox_msg_hdr->save_lf = '\n';
+}
+
+int dbox_file_unlink(struct dbox_file *file)
+{
+ const char *path;
+ bool alt = FALSE;
+
+ path = file->primary_path;
+ while (unlink(path) < 0) {
+ if (errno != ENOENT) {
+ mail_storage_set_critical(&file->storage->storage,
+ "unlink(%s) failed: %m", path);
+ return -1;
+ }
+ if (file->alt_path == NULL || alt) {
+ /* not found */
+ return 0;
+ }
+
+ /* try the alternative path */
+ path = file->alt_path;
+ alt = TRUE;
+ }
+ return 1;
+}
diff --git a/src/lib-storage/index/dbox-common/dbox-file.h b/src/lib-storage/index/dbox-common/dbox-file.h
new file mode 100644
index 0000000..309c705
--- /dev/null
+++ b/src/lib-storage/index/dbox-common/dbox-file.h
@@ -0,0 +1,218 @@
+#ifndef DBOX_FILE_H
+#define DBOX_FILE_H
+
+/* The file begins with a header followed by zero or more messages:
+
+ <dbox message header>
+ <LF>
+ <message body>
+ <metadata>
+
+ Metadata block begins with DBOX_MAGIC_POST, followed by zero or more lines
+ in format <key character><value><LF>. The block ends with an empty line.
+ Unknown metadata should be ignored, but preserved when copying.
+
+ There should be no duplicates for the current metadata, but future
+ extensions may need them so they should be preserved.
+*/
+#define DBOX_VERSION 2
+#define DBOX_MAGIC_PRE "\001\002"
+#define DBOX_MAGIC_POST "\n\001\003\n"
+
+/* prefer flock(). fcntl() locking currently breaks if trying to access the
+ same file from multiple mail_storages within same process. that's why we
+ fallback to dotlocks. */
+#ifdef HAVE_FLOCK
+# define DBOX_FILE_LOCK_METHOD_FLOCK
+#endif
+
+struct dbox_file;
+struct stat;
+
+enum dbox_header_key {
+ /* Must be sizeof(struct dbox_message_header) when appending (hex) */
+ DBOX_HEADER_MSG_HEADER_SIZE = 'M',
+ /* Creation UNIX timestamp (hex) */
+ DBOX_HEADER_CREATE_STAMP = 'C',
+
+ /* metadata used by old Dovecot versions */
+ DBOX_HEADER_OLDV1_APPEND_OFFSET = 'A'
+};
+
+/* NOTE: all valid keys are uppercase characters. if this changes, change
+ dbox-file-fix.c:dbox_file_match_post_magic() to recognize them */
+enum dbox_metadata_key {
+ /* Globally unique identifier for the message. Preserved when
+ copying. */
+ DBOX_METADATA_GUID = 'G',
+ /* POP3 UIDL overriding the default format */
+ DBOX_METADATA_POP3_UIDL = 'P',
+ /* POP3 message ordering (for migrated mails) */
+ DBOX_METADATA_POP3_ORDER = 'O',
+ /* Received UNIX timestamp in hex */
+ DBOX_METADATA_RECEIVED_TIME = 'R',
+ /* Physical message size in hex. Necessary only if it differs from
+ the dbox_message_header.message_size_hex, for example because the
+ message is compressed. */
+ DBOX_METADATA_PHYSICAL_SIZE = 'Z',
+ /* Virtual message size in hex (line feeds counted as CRLF) */
+ DBOX_METADATA_VIRTUAL_SIZE = 'V',
+ /* Pointer to external message data. Format is:
+ 1*(<start offset> <byte count> <options> <ref>) */
+ DBOX_METADATA_EXT_REF = 'X',
+ /* Mailbox name where this message was originally saved to.
+ When rebuild finds a message whose mailbox is unknown, it's
+ placed to this mailbox. */
+ DBOX_METADATA_ORIG_MAILBOX = 'B',
+
+ /* metadata used by old Dovecot versions */
+ DBOX_METADATA_OLDV1_EXPUNGED = 'E',
+ DBOX_METADATA_OLDV1_FLAGS = 'F',
+ DBOX_METADATA_OLDV1_KEYWORDS = 'K',
+ DBOX_METADATA_OLDV1_SAVE_TIME = 'S',
+ DBOX_METADATA_OLDV1_SPACE = ' '
+};
+
+enum dbox_message_type {
+ /* Normal message */
+ DBOX_MESSAGE_TYPE_NORMAL = 'N'
+};
+
+struct dbox_message_header {
+ unsigned char magic_pre[2];
+ unsigned char type;
+ unsigned char space1;
+ unsigned char oldv1_uid_hex[8];
+ unsigned char space2;
+ unsigned char message_size_hex[16];
+ /* <space reserved for future extensions, LF is always last> */
+ unsigned char save_lf;
+};
+
+struct dbox_metadata_header {
+ unsigned char magic_post[sizeof(DBOX_MAGIC_POST)-1];
+};
+
+struct dbox_file {
+ struct dbox_storage *storage;
+ int refcount;
+
+ time_t create_time;
+ unsigned int file_version;
+ unsigned int file_header_size;
+ unsigned int msg_header_size;
+
+ const char *cur_path;
+ char *primary_path, *alt_path;
+ int fd;
+ struct istream *input;
+#ifdef DBOX_FILE_LOCK_METHOD_FLOCK
+ struct file_lock *lock;
+#else
+ struct dotlock *lock;
+#endif
+
+ uoff_t cur_offset;
+ uoff_t cur_physical_size;
+
+ /* Metadata for the currently seeked metadata block. */
+ pool_t metadata_pool;
+ ARRAY(const char *) metadata;
+ uoff_t metadata_read_offset;
+
+ bool appending:1;
+ bool corrupted:1;
+};
+
+struct dbox_file_append_context {
+ struct dbox_file *file;
+
+ uoff_t first_append_offset, last_checkpoint_offset, last_flush_offset;
+ struct ostream *output;
+};
+
+#define dbox_file_is_open(file) ((file)->fd != -1)
+#define dbox_file_is_in_alt(file) ((file)->cur_path == (file)->alt_path)
+
+void dbox_file_init(struct dbox_file *file);
+void dbox_file_unref(struct dbox_file **file);
+
+/* Open the file. Returns 1 if ok, 0 if file header is corrupted, -1 if error.
+ If file is deleted, deleted_r=TRUE and 1 is returned. */
+int dbox_file_open(struct dbox_file *file, bool *deleted_r);
+/* Try to open file only from primary path. */
+int dbox_file_open_primary(struct dbox_file *file, bool *notfound_r);
+/* Close the file handle from the file, but don't free it. */
+void dbox_file_close(struct dbox_file *file);
+
+/* fstat() or stat() the file. If file is already deleted, fails with
+ errno=ENOENT. */
+int dbox_file_stat(struct dbox_file *file, struct stat *st_r);
+
+/* Try to lock the dbox file. Returns 1 if ok, 0 if already locked by someone
+ else, -1 if error. */
+int dbox_file_try_lock(struct dbox_file *file);
+void dbox_file_unlock(struct dbox_file *file);
+
+/* Seek to given offset in file. Returns 1 if ok/expunged, 0 if file/offset is
+ corrupted, -1 if I/O error. */
+int dbox_file_seek(struct dbox_file *file, uoff_t offset);
+/* Start seeking at the beginning of the file. */
+void dbox_file_seek_rewind(struct dbox_file *file);
+/* Seek to next message after current one. If there are no more messages,
+ returns 0 and last_r is set to TRUE. Returns 1 if ok, 0 if file is
+ corrupted, -1 if I/O error. */
+int dbox_file_seek_next(struct dbox_file *file, uoff_t *offset_r, bool *last_r);
+
+/* Start appending to dbox file */
+struct dbox_file_append_context *dbox_file_append_init(struct dbox_file *file);
+/* Finish writing appended mails. */
+int dbox_file_append_commit(struct dbox_file_append_context **ctx);
+/* Truncate appended mails. */
+void dbox_file_append_rollback(struct dbox_file_append_context **ctx);
+/* Get output stream for appending a new message. Returns 1 if ok, 0 if file
+ can't be appended to (old file version or corruption) or -1 if error. */
+int dbox_file_get_append_stream(struct dbox_file_append_context *ctx,
+ struct ostream **output_r);
+/* Call after message has been fully saved. If this isn't done, the writes
+ since the last checkpoint are truncated. */
+void dbox_file_append_checkpoint(struct dbox_file_append_context *ctx);
+/* Flush output buffer. */
+int dbox_file_append_flush(struct dbox_file_append_context *ctx);
+
+/* Read current message's metadata. Returns 1 if ok, 0 if metadata is
+ corrupted, -1 if I/O error. */
+int dbox_file_metadata_read(struct dbox_file *file);
+/* Return wanted metadata value, or NULL if not found. */
+const char *dbox_file_metadata_get(struct dbox_file *file,
+ enum dbox_metadata_key key);
+
+/* Returns DBOX_METADATA_PHYSICAL_SIZE if set, otherwise physical size from
+ header. They differ only for e.g. compressed mails. */
+uoff_t dbox_file_get_plaintext_size(struct dbox_file *file);
+
+/* Fix a broken dbox file by rename()ing over it with a fixed file. Everything
+ before start_offset is assumed to be valid and is simply copied. The file
+ is reopened afterwards. Returns 1 if ok, 0 if the resulting file has no
+ mails and was deleted, -1 if I/O error. */
+int dbox_file_fix(struct dbox_file *file, uoff_t start_offset);
+/* Delete the given dbox file. Returns 1 if deleted, 0 if file wasn't found
+ or -1 if error. */
+int dbox_file_unlink(struct dbox_file *file);
+
+/* Fill dbox_message_header with given size. */
+void dbox_msg_header_fill(struct dbox_message_header *dbox_msg_hdr,
+ uoff_t message_size);
+
+void dbox_file_set_syscall_error(struct dbox_file *file, const char *function);
+void dbox_file_set_corrupted(struct dbox_file *file, const char *reason, ...)
+ ATTR_FORMAT(2, 3);
+
+/* private: */
+const char *dbox_generate_tmp_filename(void);
+void dbox_file_free(struct dbox_file *file);
+int dbox_file_header_write(struct dbox_file *file, struct ostream *output);
+int dbox_file_read_mail_header(struct dbox_file *file, uoff_t *physical_size_r);
+int dbox_file_metadata_skip_header(struct dbox_file *file);
+
+#endif
diff --git a/src/lib-storage/index/dbox-common/dbox-mail.c b/src/lib-storage/index/dbox-common/dbox-mail.c
new file mode 100644
index 0000000..49279f9
--- /dev/null
+++ b/src/lib-storage/index/dbox-common/dbox-mail.c
@@ -0,0 +1,318 @@
+/* Copyright (c) 2007-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "istream.h"
+#include "str.h"
+#include "index-storage.h"
+#include "index-mail.h"
+#include "index-pop3-uidl.h"
+#include "dbox-attachment.h"
+#include "dbox-storage.h"
+#include "dbox-file.h"
+#include "dbox-mail.h"
+
+
+struct mail *
+dbox_mail_alloc(struct mailbox_transaction_context *t,
+ enum mail_fetch_field wanted_fields,
+ struct mailbox_header_lookup_ctx *wanted_headers)
+{
+ struct dbox_mail *mail;
+ pool_t pool;
+
+ pool = pool_alloconly_create("mail", 2048);
+ mail = p_new(pool, struct dbox_mail, 1);
+
+ index_mail_init(&mail->imail, t, wanted_fields, wanted_headers, pool, NULL);
+ return &mail->imail.mail.mail;
+}
+
+void dbox_mail_close(struct mail *_mail)
+{
+ struct dbox_mail *mail = DBOX_MAIL(_mail);
+
+ index_mail_close(_mail);
+ /* close the dbox file only after index is closed, since it may still
+ try to read from it. */
+ if (mail->open_file != NULL)
+ dbox_file_unref(&mail->open_file);
+}
+
+int dbox_mail_metadata_read(struct dbox_mail *mail, struct dbox_file **file_r)
+{
+ struct dbox_storage *storage =
+ DBOX_STORAGE(mail->imail.mail.mail.box->storage);
+ uoff_t offset;
+
+ if (storage->v.mail_open(mail, &offset, file_r) < 0)
+ return -1;
+
+ if (dbox_file_seek(*file_r, offset) <= 0)
+ return -1;
+ if (dbox_file_metadata_read(*file_r) <= 0)
+ return -1;
+
+ if (mail->imail.data.stream != NULL) {
+ /* we just messed up mail's input stream by reading metadata */
+ i_stream_seek((*file_r)->input, offset);
+ i_stream_sync(mail->imail.data.stream);
+ }
+ return 0;
+}
+
+static int
+dbox_mail_metadata_get(struct dbox_mail *mail, enum dbox_metadata_key key,
+ const char **value_r)
+{
+ struct dbox_file *file;
+
+ if (dbox_mail_metadata_read(mail, &file) < 0)
+ return -1;
+
+ *value_r = dbox_file_metadata_get(file, key);
+ return 0;
+}
+
+int dbox_mail_get_physical_size(struct mail *_mail, uoff_t *size_r)
+{
+ struct dbox_mail *mail = DBOX_MAIL(_mail);
+ struct index_mail_data *data = &mail->imail.data;
+ struct dbox_file *file;
+
+ if (index_mail_get_physical_size(_mail, size_r) == 0)
+ return 0;
+
+ if (dbox_mail_metadata_read(mail, &file) < 0)
+ return -1;
+
+ data->physical_size = dbox_file_get_plaintext_size(file);
+ *size_r = data->physical_size;
+ return 0;
+}
+
+int dbox_mail_get_virtual_size(struct mail *_mail, uoff_t *size_r)
+{
+ struct dbox_mail *mail = DBOX_MAIL(_mail);
+ struct index_mail_data *data = &mail->imail.data;
+ const char *value;
+ uintmax_t size;
+
+ if (index_mail_get_cached_virtual_size(&mail->imail, size_r))
+ return 0;
+
+ if (dbox_mail_metadata_get(mail, DBOX_METADATA_VIRTUAL_SIZE,
+ &value) < 0)
+ return -1;
+ if (value == NULL)
+ return index_mail_get_virtual_size(_mail, size_r);
+
+ if (str_to_uintmax_hex(value, &size) < 0 || size > UOFF_T_MAX)
+ return -1;
+ data->virtual_size = (uoff_t)size;
+ *size_r = data->virtual_size;
+ return 0;
+}
+
+int dbox_mail_get_received_date(struct mail *_mail, time_t *date_r)
+{
+ struct dbox_mail *mail = DBOX_MAIL(_mail);
+ struct index_mail_data *data = &mail->imail.data;
+ const char *value;
+ uintmax_t time;
+
+ if (index_mail_get_received_date(_mail, date_r) == 0)
+ return 0;
+
+ if (dbox_mail_metadata_get(mail, DBOX_METADATA_RECEIVED_TIME,
+ &value) < 0)
+ return -1;
+
+ time = 0;
+ if (value != NULL && str_to_uintmax_hex(value, &time) < 0)
+ return -1;
+
+ data->received_date = (time_t)time;
+ *date_r = data->received_date;
+ return 0;
+}
+
+int dbox_mail_get_save_date(struct mail *_mail, time_t *date_r)
+{
+ struct dbox_storage *storage = DBOX_STORAGE(_mail->box->storage);
+ struct dbox_mail *mail = DBOX_MAIL(_mail);
+ struct index_mail_data *data = &mail->imail.data;
+ struct dbox_file *file;
+ struct stat st;
+ uoff_t offset;
+
+ if (index_mail_get_save_date(_mail, date_r) > 0)
+ return 1;
+
+ if (storage->v.mail_open(mail, &offset, &file) < 0)
+ return -1;
+
+ _mail->transaction->stats.fstat_lookup_count++;
+ if (dbox_file_stat(file, &st) < 0) {
+ if (errno == ENOENT)
+ mail_set_expunged(_mail);
+ return -1;
+ }
+ *date_r = data->save_date = st.st_ctime;
+ return 1;
+}
+
+static int
+dbox_get_cached_metadata(struct dbox_mail *mail, enum dbox_metadata_key key,
+ enum index_cache_field cache_field,
+ const char **value_r)
+{
+ struct index_mail *imail = &mail->imail;
+ struct index_mailbox_context *ibox =
+ INDEX_STORAGE_CONTEXT(imail->mail.mail.box);
+ const char *value;
+ string_t *str;
+ uint32_t order;
+
+ str = str_new(imail->mail.data_pool, 64);
+ if (mail_cache_lookup_field(imail->mail.mail.transaction->cache_view,
+ str, imail->mail.mail.seq,
+ ibox->cache_fields[cache_field].idx) > 0) {
+ if (cache_field == MAIL_CACHE_POP3_ORDER) {
+ i_assert(str_len(str) == sizeof(order));
+ memcpy(&order, str_data(str), sizeof(order));
+ str_truncate(str, 0);
+ if (order != 0)
+ str_printfa(str, "%u", order);
+ else {
+ /* order=0 means it doesn't exist. we don't
+ want to return "0" though, because then the
+ mails get ordered to beginning, while
+ nonexistent are supposed to be ordered at
+ the end. */
+ }
+ }
+ *value_r = str_c(str);
+ return 0;
+ }
+
+ if (dbox_mail_metadata_get(mail, key, &value) < 0)
+ return -1;
+
+ if (value == NULL)
+ value = "";
+ if (cache_field != MAIL_CACHE_POP3_ORDER) {
+ index_mail_cache_add_idx(imail, ibox->cache_fields[cache_field].idx,
+ value, strlen(value));
+ } else {
+ if (str_to_uint(value, &order) < 0)
+ order = 0;
+ index_mail_cache_add_idx(imail, ibox->cache_fields[cache_field].idx,
+ &order, sizeof(order));
+ }
+
+ /* don't return pointer to dbox metadata directly, since it may
+ change unexpectedly */
+ str_truncate(str, 0);
+ str_append(str, value);
+ *value_r = str_c(str);
+ return 0;
+}
+
+int dbox_mail_get_special(struct mail *_mail, enum mail_fetch_field field,
+ const char **value_r)
+{
+ struct dbox_mail *mail = DBOX_MAIL(_mail);
+ int ret;
+
+ /* keep the UIDL in cache file, otherwise POP3 would open all
+ mail files and read the metadata. same for GUIDs if they're
+ used. */
+ switch (field) {
+ case MAIL_FETCH_UIDL_BACKEND:
+ if (!index_pop3_uidl_can_exist(_mail)) {
+ *value_r = "";
+ return 0;
+ }
+ ret = dbox_get_cached_metadata(mail, DBOX_METADATA_POP3_UIDL,
+ MAIL_CACHE_POP3_UIDL, value_r);
+ if (ret == 0) {
+ index_pop3_uidl_update_exists(&mail->imail.mail.mail,
+ (*value_r)[0] != '\0');
+ }
+ return ret;
+ case MAIL_FETCH_POP3_ORDER:
+ if (!index_pop3_uidl_can_exist(_mail)) {
+ /* we're assuming that if there's a POP3 order, there's
+ also a UIDL */
+ *value_r = "";
+ return 0;
+ }
+ return dbox_get_cached_metadata(mail, DBOX_METADATA_POP3_ORDER,
+ MAIL_CACHE_POP3_ORDER, value_r);
+ case MAIL_FETCH_GUID:
+ return dbox_get_cached_metadata(mail, DBOX_METADATA_GUID,
+ MAIL_CACHE_GUID, value_r);
+ default:
+ break;
+ }
+
+ return index_mail_get_special(_mail, field, value_r);
+}
+
+static int
+get_mail_stream(struct dbox_mail *mail, uoff_t offset,
+ struct istream **stream_r)
+{
+ struct mail_private *pmail = &mail->imail.mail;
+ struct dbox_file *file = mail->open_file;
+ int ret;
+
+ if ((ret = dbox_file_seek(file, offset)) <= 0) {
+ *stream_r = NULL;
+ return ret;
+ }
+
+ *stream_r = i_stream_create_limit(file->input, file->cur_physical_size);
+ if (pmail->v.istream_opened != NULL) {
+ if (pmail->v.istream_opened(&pmail->mail, stream_r) < 0)
+ return -1;
+ }
+ if (file->storage->attachment_dir == NULL)
+ return 1;
+ else
+ return dbox_attachment_file_get_stream(file, stream_r);
+}
+
+int dbox_mail_get_stream(struct mail *_mail, bool get_body ATTR_UNUSED,
+ struct message_size *hdr_size,
+ struct message_size *body_size,
+ struct istream **stream_r)
+{
+ struct dbox_storage *storage = DBOX_STORAGE(_mail->box->storage);
+ struct dbox_mail *mail = DBOX_MAIL(_mail);
+ struct index_mail_data *data = &mail->imail.data;
+ struct istream *input;
+ uoff_t offset;
+ int ret;
+
+ if (data->stream == NULL) {
+ if (storage->v.mail_open(mail, &offset, &mail->open_file) < 0)
+ return -1;
+
+ ret = get_mail_stream(mail, offset, &input);
+ if (ret <= 0) {
+ if (ret < 0)
+ return -1;
+ dbox_file_set_corrupted(mail->open_file,
+ "uid=%u points to broken data at offset="
+ "%"PRIuUOFF_T, _mail->uid, offset);
+ i_stream_unref(&input);
+ return -1;
+ }
+ data->stream = input;
+ index_mail_set_read_buffer_size(_mail, input);
+ }
+
+ return index_mail_init_stream(&mail->imail, hdr_size, body_size,
+ stream_r);
+}
diff --git a/src/lib-storage/index/dbox-common/dbox-mail.h b/src/lib-storage/index/dbox-common/dbox-mail.h
new file mode 100644
index 0000000..c03652c
--- /dev/null
+++ b/src/lib-storage/index/dbox-common/dbox-mail.h
@@ -0,0 +1,34 @@
+#ifndef DBOX_MAIL_H
+#define DBOX_MAIL_H
+
+#include "index-mail.h"
+
+struct dbox_mail {
+ struct index_mail imail;
+
+ struct dbox_file *open_file;
+ uoff_t offset;
+};
+
+#define DBOX_MAIL(s) container_of(s, struct dbox_mail, imail.mail.mail)
+
+struct mail *
+dbox_mail_alloc(struct mailbox_transaction_context *t,
+ enum mail_fetch_field wanted_fields,
+ struct mailbox_header_lookup_ctx *wanted_headers);
+void dbox_mail_close(struct mail *mail);
+
+int dbox_mail_get_physical_size(struct mail *mail, uoff_t *size_r);
+int dbox_mail_get_virtual_size(struct mail *mail, uoff_t *size_r);
+int dbox_mail_get_received_date(struct mail *mail, time_t *date_r);
+int dbox_mail_get_save_date(struct mail *_mail, time_t *date_r);
+int dbox_mail_get_special(struct mail *mail, enum mail_fetch_field field,
+ const char **value_r);
+int dbox_mail_get_stream(struct mail *_mail, bool get_body ATTR_UNUSED,
+ struct message_size *hdr_size,
+ struct message_size *body_size,
+ struct istream **stream_r);
+
+int dbox_mail_metadata_read(struct dbox_mail *mail, struct dbox_file **file_r);
+
+#endif
diff --git a/src/lib-storage/index/dbox-common/dbox-save.c b/src/lib-storage/index/dbox-common/dbox-save.c
new file mode 100644
index 0000000..c5af8cc
--- /dev/null
+++ b/src/lib-storage/index/dbox-common/dbox-save.c
@@ -0,0 +1,226 @@
+/* Copyright (c) 2007-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "istream.h"
+#include "istream-crlf.h"
+#include "ostream.h"
+#include "str.h"
+#include "hex-binary.h"
+#include "index-mail.h"
+#include "index-storage.h"
+#include "dbox-attachment.h"
+#include "dbox-file.h"
+#include "dbox-save.h"
+
+void dbox_save_add_to_index(struct dbox_save_context *ctx)
+{
+ struct mail_save_data *mdata = &ctx->ctx.data;
+ enum mail_flags save_flags;
+
+ save_flags = mdata->flags & ENUM_NEGATE(MAIL_RECENT);
+ mail_index_append(ctx->trans, mdata->uid, &ctx->seq);
+ mail_index_update_flags(ctx->trans, ctx->seq, MODIFY_REPLACE,
+ save_flags);
+ if (mdata->keywords != NULL) {
+ mail_index_update_keywords(ctx->trans, ctx->seq,
+ MODIFY_REPLACE, mdata->keywords);
+ }
+ if (mdata->min_modseq != 0) {
+ mail_index_update_modseq(ctx->trans, ctx->seq,
+ mdata->min_modseq);
+ }
+}
+
+void dbox_save_begin(struct dbox_save_context *ctx, struct istream *input)
+{
+ struct mail_save_context *_ctx = &ctx->ctx;
+ struct mail_storage *_storage = _ctx->transaction->box->storage;
+ struct dbox_storage *storage = DBOX_STORAGE(_storage);
+ struct dbox_message_header dbox_msg_hdr;
+ struct istream *crlf_input;
+
+ dbox_save_add_to_index(ctx);
+
+ mail_set_seq_saving(_ctx->dest_mail, ctx->seq);
+
+ crlf_input = i_stream_create_lf(input);
+ ctx->input = index_mail_cache_parse_init(_ctx->dest_mail, crlf_input);
+ i_stream_unref(&crlf_input);
+
+ /* write a dummy header. it'll get rewritten when we're finished */
+ i_zero(&dbox_msg_hdr);
+ o_stream_cork(ctx->dbox_output);
+ if (o_stream_send(ctx->dbox_output, &dbox_msg_hdr,
+ sizeof(dbox_msg_hdr)) < 0) {
+ mail_set_critical(_ctx->dest_mail, "write(%s) failed: %s",
+ o_stream_get_name(ctx->dbox_output),
+ o_stream_get_error(ctx->dbox_output));
+ ctx->failed = TRUE;
+ }
+ _ctx->data.output = ctx->dbox_output;
+
+ if (_ctx->data.received_date == (time_t)-1)
+ _ctx->data.received_date = ioloop_time;
+ index_attachment_save_begin(_ctx, storage->attachment_fs, ctx->input);
+}
+
+int dbox_save_continue(struct mail_save_context *_ctx)
+{
+ struct dbox_save_context *ctx = DBOX_SAVECTX(_ctx);
+
+ if (ctx->failed)
+ return -1;
+
+ if (_ctx->data.attach != NULL)
+ return index_attachment_save_continue(_ctx);
+
+ if (index_storage_save_continue(_ctx, ctx->input,
+ _ctx->dest_mail) < 0) {
+ ctx->failed = TRUE;
+ return -1;
+ }
+ return 0;
+}
+
+void dbox_save_end(struct dbox_save_context *ctx)
+{
+ struct mail_save_data *mdata = &ctx->ctx.data;
+ struct ostream *dbox_output = ctx->dbox_output;
+ int ret;
+
+ i_assert(mdata->output != NULL);
+
+ if (mdata->attach != NULL && !ctx->failed) {
+ if (index_attachment_save_finish(&ctx->ctx) < 0)
+ ctx->failed = TRUE;
+ }
+ if (mdata->output != dbox_output) {
+ /* e.g. zlib plugin had changed this. make sure we
+ successfully write the trailer. */
+ ret = o_stream_finish(mdata->output);
+ } else {
+ /* no plugins - flush the output so far */
+ ret = o_stream_flush(mdata->output);
+ }
+ if (ret < 0) {
+ mail_set_critical(ctx->ctx.dest_mail,
+ "write(%s) failed: %s",
+ o_stream_get_name(mdata->output),
+ o_stream_get_error(mdata->output));
+ ctx->failed = TRUE;
+ }
+ if (mdata->output != dbox_output) {
+ o_stream_ref(dbox_output);
+ o_stream_destroy(&mdata->output);
+ mdata->output = dbox_output;
+ }
+ index_mail_cache_parse_deinit(ctx->ctx.dest_mail,
+ ctx->ctx.data.received_date,
+ !ctx->failed);
+ if (!ctx->failed)
+ index_mail_cache_pop3_data(ctx->ctx.dest_mail,
+ mdata->pop3_uidl,
+ mdata->pop3_order);
+}
+
+void dbox_save_write_metadata(struct mail_save_context *_ctx,
+ struct ostream *output, uoff_t output_msg_size,
+ const char *orig_mailbox_name,
+ guid_128_t guid_128)
+{
+ struct dbox_save_context *ctx = DBOX_SAVECTX(_ctx);
+ struct mail_save_data *mdata = &ctx->ctx.data;
+ struct dbox_metadata_header metadata_hdr;
+ const char *guid;
+ string_t *str;
+ uoff_t vsize;
+
+ i_zero(&metadata_hdr);
+ memcpy(metadata_hdr.magic_post, DBOX_MAGIC_POST,
+ sizeof(metadata_hdr.magic_post));
+ o_stream_nsend(output, &metadata_hdr, sizeof(metadata_hdr));
+
+ str = t_str_new(256);
+ if (output_msg_size != ctx->input->v_offset) {
+ /* a plugin changed the data written to disk, so the
+ "message size" dbox header doesn't contain the actual
+ "physical" message size. we need to save it as a
+ separate metadata header. */
+ str_printfa(str, "%c%llx\n", DBOX_METADATA_PHYSICAL_SIZE,
+ (unsigned long long)ctx->input->v_offset);
+ }
+ str_printfa(str, "%c%"PRIxTIME_T"\n", DBOX_METADATA_RECEIVED_TIME,
+ mdata->received_date);
+ if (mail_get_virtual_size(_ctx->dest_mail, &vsize) < 0)
+ i_unreached();
+ str_printfa(str, "%c%llx\n", DBOX_METADATA_VIRTUAL_SIZE,
+ (unsigned long long)vsize);
+ if (mdata->pop3_uidl != NULL) {
+ i_assert(strchr(mdata->pop3_uidl, '\n') == NULL);
+ str_printfa(str, "%c%s\n", DBOX_METADATA_POP3_UIDL,
+ mdata->pop3_uidl);
+ ctx->have_pop3_uidls = TRUE;
+ ctx->highest_pop3_uidl_seq =
+ I_MAX(ctx->highest_pop3_uidl_seq, ctx->seq);
+ }
+ if (mdata->pop3_order != 0) {
+ str_printfa(str, "%c%u\n", DBOX_METADATA_POP3_ORDER,
+ mdata->pop3_order);
+ ctx->have_pop3_orders = TRUE;
+ ctx->highest_pop3_uidl_seq =
+ I_MAX(ctx->highest_pop3_uidl_seq, ctx->seq);
+ }
+
+ guid = mdata->guid;
+ if (guid != NULL)
+ mail_generate_guid_128_hash(guid, guid_128);
+ else {
+ guid_128_generate(guid_128);
+ guid = guid_128_to_string(guid_128);
+ }
+ str_printfa(str, "%c%s\n", DBOX_METADATA_GUID, guid);
+
+ if (orig_mailbox_name != NULL &&
+ strchr(orig_mailbox_name, '\r') == NULL &&
+ strchr(orig_mailbox_name, '\n') == NULL) {
+ /* save the original mailbox name so if mailbox indexes get
+ corrupted we can place at least some (hopefully most) of
+ the messages to correct mailboxes. */
+ str_printfa(str, "%c%s\n", DBOX_METADATA_ORIG_MAILBOX,
+ orig_mailbox_name);
+ }
+
+ dbox_attachment_save_write_metadata(_ctx, str);
+
+ str_append_c(str, '\n');
+ o_stream_nsend(output, str_data(str), str_len(str));
+}
+
+void dbox_save_update_header_flags(struct dbox_save_context *ctx,
+ struct mail_index_view *sync_view,
+ uint32_t ext_id,
+ unsigned int flags_offset)
+{
+ const void *data;
+ size_t data_size;
+ uint8_t old_flags = 0, flags;
+
+ mail_index_get_header_ext(sync_view, ext_id, &data, &data_size);
+ if (flags_offset < data_size)
+ old_flags = *((const uint8_t *)data + flags_offset);
+ else {
+ /* grow old dbox header */
+ mail_index_ext_resize_hdr(ctx->trans, ext_id, flags_offset+1);
+ }
+
+ flags = old_flags;
+ if (ctx->have_pop3_uidls)
+ flags |= DBOX_INDEX_HEADER_FLAG_HAVE_POP3_UIDLS;
+ if (ctx->have_pop3_orders)
+ flags |= DBOX_INDEX_HEADER_FLAG_HAVE_POP3_ORDERS;
+ if (flags != old_flags) {
+ /* flags changed, update them */
+ mail_index_update_header_ext(ctx->trans, ext_id,
+ flags_offset, &flags, 1);
+ }
+}
diff --git a/src/lib-storage/index/dbox-common/dbox-save.h b/src/lib-storage/index/dbox-common/dbox-save.h
new file mode 100644
index 0000000..a17c923
--- /dev/null
+++ b/src/lib-storage/index/dbox-common/dbox-save.h
@@ -0,0 +1,41 @@
+#ifndef DBOX_SAVE_H
+#define DBOX_SAVE_H
+
+#include "dbox-storage.h"
+
+struct dbox_save_context {
+ struct mail_save_context ctx;
+ struct mail_index_transaction *trans;
+
+ /* updated for each appended mail: */
+ uint32_t seq;
+ struct istream *input;
+
+ struct ostream *dbox_output;
+
+ uint32_t highest_pop3_uidl_seq;
+ bool failed:1;
+ bool finished:1;
+ bool have_pop3_uidls:1;
+ bool have_pop3_orders:1;
+};
+
+#define DBOX_SAVECTX(s) container_of(s, struct dbox_save_context, ctx)
+
+void dbox_save_begin(struct dbox_save_context *ctx, struct istream *input);
+int dbox_save_continue(struct mail_save_context *_ctx);
+void dbox_save_end(struct dbox_save_context *ctx);
+
+void dbox_save_write_metadata(struct mail_save_context *ctx,
+ struct ostream *output, uoff_t output_msg_size,
+ const char *orig_mailbox_name,
+ guid_128_t guid_128_r) ATTR_NULL(4);
+
+void dbox_save_add_to_index(struct dbox_save_context *ctx);
+
+void dbox_save_update_header_flags(struct dbox_save_context *ctx,
+ struct mail_index_view *sync_view,
+ uint32_t ext_id,
+ unsigned int flags_offset);
+
+#endif
diff --git a/src/lib-storage/index/dbox-common/dbox-storage.c b/src/lib-storage/index/dbox-common/dbox-storage.c
new file mode 100644
index 0000000..5cac0d6
--- /dev/null
+++ b/src/lib-storage/index/dbox-common/dbox-storage.c
@@ -0,0 +1,416 @@
+/* Copyright (c) 2007-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "path-util.h"
+#include "ioloop.h"
+#include "fs-api.h"
+#include "mkdir-parents.h"
+#include "unlink-old-files.h"
+#include "mailbox-uidvalidity.h"
+#include "mailbox-list-private.h"
+#include "index-storage.h"
+#include "dbox-storage.h"
+
+#include <stdio.h>
+#include <dirent.h>
+#include <unistd.h>
+#include <utime.h>
+
+void dbox_storage_get_list_settings(const struct mail_namespace *ns ATTR_UNUSED,
+ struct mailbox_list_settings *set)
+{
+ if (set->layout == NULL)
+ set->layout = MAILBOX_LIST_NAME_FS;
+ if (set->subscription_fname == NULL)
+ set->subscription_fname = DBOX_SUBSCRIPTION_FILE_NAME;
+ if (*set->maildir_name == '\0')
+ set->maildir_name = DBOX_MAILDIR_NAME;
+ if (*set->mailbox_dir_name == '\0')
+ set->mailbox_dir_name = DBOX_MAILBOX_DIR_NAME;
+}
+
+static bool
+dbox_alt_path_has_changed(const char *root_dir, const char *alt_path,
+ const char *alt_path2, const char *alt_symlink_path)
+{
+ const char *linkpath, *error;
+
+ if (t_readlink(alt_symlink_path, &linkpath, &error) < 0) {
+ if (errno == ENOENT)
+ return alt_path != NULL;
+ i_error("t_readlink(%s) failed: %s", alt_symlink_path, error);
+ return FALSE;
+ }
+
+ if (alt_path == NULL) {
+ i_warning("dbox %s: Original ALT=%s, "
+ "but currently no ALT path set", root_dir, linkpath);
+ return TRUE;
+ } else if (strcmp(linkpath, alt_path) != 0) {
+ if (strcmp(linkpath, alt_path2) == 0) {
+ /* FIXME: for backwards compatibility. old versions
+ created the symlink to mailboxes/ directory, which
+ was fine with sdbox, but didn't even exist with
+ mdbox. we'll silently replace the symlink. */
+ return TRUE;
+ }
+ i_warning("dbox %s: Original ALT=%s, "
+ "but currently ALT=%s", root_dir, linkpath, alt_path);
+ return TRUE;
+ }
+ return FALSE;
+}
+
+static void dbox_verify_alt_path(struct mailbox_list *list)
+{
+ const char *root_dir, *alt_symlink_path, *alt_path, *alt_path2;
+
+ root_dir = mailbox_list_get_root_forced(list, MAILBOX_LIST_PATH_TYPE_DIR);
+ alt_symlink_path =
+ t_strconcat(root_dir, "/"DBOX_ALT_SYMLINK_NAME, NULL);
+ (void)mailbox_list_get_root_path(list, MAILBOX_LIST_PATH_TYPE_ALT_DIR,
+ &alt_path);
+ (void)mailbox_list_get_root_path(list, MAILBOX_LIST_PATH_TYPE_ALT_MAILBOX,
+ &alt_path2);
+ if (!dbox_alt_path_has_changed(root_dir, alt_path, alt_path2,
+ alt_symlink_path))
+ return;
+
+ /* unlink/create the current alt path symlink */
+ i_unlink_if_exists(alt_symlink_path);
+ if (alt_path != NULL) {
+ int ret = symlink(alt_path, alt_symlink_path);
+ if (ret < 0 && errno == ENOENT) {
+ /* root_dir doesn't exist yet - create it */
+ if (mailbox_list_mkdir_root(list, root_dir,
+ MAILBOX_LIST_PATH_TYPE_DIR) < 0)
+ return;
+ ret = symlink(alt_path, alt_symlink_path);
+ }
+ if (ret < 0 && errno != EEXIST) {
+ i_error("symlink(%s, %s) failed: %m",
+ alt_path, alt_symlink_path);
+ }
+ }
+}
+
+int dbox_storage_create(struct mail_storage *_storage,
+ struct mail_namespace *ns,
+ const char **error_r)
+{
+ struct dbox_storage *storage = DBOX_STORAGE(_storage);
+ const struct mail_storage_settings *set = _storage->set;
+ const char *error;
+
+ if (*set->mail_attachment_fs != '\0' &&
+ *set->mail_attachment_dir != '\0') {
+ const char *name, *args, *dir;
+
+ args = strpbrk(set->mail_attachment_fs, ": ");
+ if (args == NULL) {
+ name = set->mail_attachment_fs;
+ args = "";
+ } else {
+ name = t_strdup_until(set->mail_attachment_fs, args++);
+ }
+ if (strcmp(name, "sis-queue") == 0 &&
+ (_storage->class_flags & MAIL_STORAGE_CLASS_FLAG_FILE_PER_MSG) != 0) {
+ /* FIXME: the deduplication part doesn't work, because
+ sdbox renames the files.. */
+ *error_r = "mail_attachment_fs: "
+ "sis-queue not currently supported by sdbox";
+ return -1;
+ }
+ dir = mail_user_home_expand(_storage->user,
+ set->mail_attachment_dir);
+ storage->attachment_dir = p_strdup(_storage->pool, dir);
+
+ if (mailbox_list_init_fs(ns->list, _storage->event, name, args,
+ storage->attachment_dir,
+ &storage->attachment_fs, &error) < 0) {
+ *error_r = t_strdup_printf("mail_attachment_fs: %s",
+ error);
+ return -1;
+ }
+ }
+
+ if (!ns->list->set.alt_dir_nocheck)
+ dbox_verify_alt_path(ns->list);
+ return 0;
+}
+
+void dbox_storage_destroy(struct mail_storage *_storage)
+{
+ struct dbox_storage *storage = DBOX_STORAGE(_storage);
+
+ fs_deinit(&storage->attachment_fs);
+ index_storage_destroy(_storage);
+}
+
+uint32_t dbox_get_uidvalidity_next(struct mailbox_list *list)
+{
+ const char *path;
+
+ path = mailbox_list_get_root_forced(list, MAILBOX_LIST_PATH_TYPE_CONTROL);
+ path = t_strconcat(path, "/"DBOX_UIDVALIDITY_FILE_NAME, NULL);
+ return mailbox_uidvalidity_next(list, path);
+}
+
+void dbox_notify_changes(struct mailbox *box)
+{
+ const char *dir, *path;
+
+ if (box->notify_callback == NULL)
+ mailbox_watch_remove_all(box);
+ else {
+ if (mailbox_get_path_to(box, MAILBOX_LIST_PATH_TYPE_INDEX,
+ &dir) <= 0)
+ return;
+ path = t_strdup_printf("%s/"MAIL_INDEX_PREFIX".log", dir);
+ mailbox_watch_add(box, path);
+ }
+}
+
+static bool
+dbox_cleanup_temp_files(struct mailbox_list *list, const char *path,
+ time_t last_scan_time, time_t last_change_time)
+{
+ unsigned int interval = list->mail_set->mail_temp_scan_interval;
+
+ /* check once in a while if there are temp files to clean up */
+ if (interval == 0) {
+ /* disabled */
+ return FALSE;
+ } else if (last_scan_time >= ioloop_time - (time_t)interval) {
+ /* not the time to scan it yet */
+ return FALSE;
+ } else {
+ bool stated = FALSE;
+ if (last_change_time == (time_t)-1) {
+ /* Don't know the ctime yet - look it up. */
+ struct stat st;
+
+ if (stat(path, &st) < 0) {
+ if (errno == ENOENT)
+ i_error("stat(%s) failed: %m", path);
+ return FALSE;
+ }
+ last_change_time = st.st_ctime;
+ stated = TRUE;
+ }
+ if (last_scan_time > last_change_time + DBOX_TMP_DELETE_SECS) {
+ /* there haven't been any changes to this directory
+ since we last checked it. If we did an extra stat(),
+ we need to update the last_scan_time to avoid
+ stat()ing the next time. */
+ return stated;
+ }
+ const char *prefix =
+ mailbox_list_get_global_temp_prefix(list);
+ (void)unlink_old_files(path, prefix,
+ ioloop_time - DBOX_TMP_DELETE_SECS);
+ return TRUE;
+ }
+ return FALSE;
+}
+
+int dbox_mailbox_check_existence(struct mailbox *box, time_t *path_ctime_r)
+{
+ const char *index_path, *box_path = mailbox_get_path(box);
+ struct stat st;
+ int ret = -1;
+
+ *path_ctime_r = (time_t)-1;
+
+ if (box->list->set.iter_from_index_dir) {
+ /* Just because the index directory exists, it doesn't mean
+ that the mailbox is selectable. Check that by seeing if
+ dovecot.index.log exists. If it doesn't, fallback to
+ checking for the dbox-Mails in the mail root directory.
+ So this also means that if a mailbox is \NoSelect, listing
+ it will always do a stat() for dbox-Mails in the mail root
+ directory. That's not ideal, but this makes the behavior
+ safer and \NoSelect mailboxes are somewhat rare. */
+ if (mailbox_get_path_to(box, MAILBOX_LIST_PATH_TYPE_INDEX,
+ &index_path) < 0)
+ return -1;
+ i_assert(index_path != NULL);
+ index_path = t_strconcat(index_path, "/", box->index_prefix,
+ ".log", NULL);
+ ret = stat(index_path, &st);
+ }
+ if (ret < 0) {
+ ret = stat(box_path, &st);
+ if (ret == 0)
+ *path_ctime_r = st.st_ctime;
+ }
+
+ if (ret == 0) {
+ return 0;
+ } else if (errno == ENOENT || errno == ENAMETOOLONG) {
+ mail_storage_set_error(box->storage, MAIL_ERROR_NOTFOUND,
+ T_MAIL_ERR_MAILBOX_NOT_FOUND(box->vname));
+ return -1;
+ } else if (errno == EACCES) {
+ mailbox_set_critical(box, "%s",
+ mail_error_eacces_msg("stat", box_path));
+ return -1;
+ } else {
+ mailbox_set_critical(box, "stat(%s) failed: %m", box_path);
+ return -1;
+ }
+}
+
+int dbox_mailbox_open(struct mailbox *box, time_t path_ctime)
+{
+ const char *box_path = mailbox_get_path(box);
+
+ if (index_storage_mailbox_open(box, FALSE) < 0)
+ return -1;
+ mail_index_set_fsync_mode(box->index,
+ box->storage->set->parsed_fsync_mode,
+ MAIL_INDEX_FSYNC_MASK_APPENDS |
+ MAIL_INDEX_FSYNC_MASK_EXPUNGES);
+
+ const struct mail_index_header *hdr = mail_index_get_header(box->view);
+ if (dbox_cleanup_temp_files(box->list, box_path,
+ hdr->last_temp_file_scan, path_ctime)) {
+ /* temp files were scanned. update the last scan timestamp. */
+ index_mailbox_update_last_temp_file_scan(box);
+ }
+ return 0;
+}
+
+static int dir_is_empty(struct mail_storage *storage, const char *path)
+{
+ DIR *dir;
+ struct dirent *d;
+ int ret = 1;
+
+ dir = opendir(path);
+ if (dir == NULL) {
+ if (errno == ENOENT) {
+ /* race condition with DELETE/RENAME? */
+ return 1;
+ }
+ mail_storage_set_critical(storage, "opendir(%s) failed: %m",
+ path);
+ return -1;
+ }
+ while ((d = readdir(dir)) != NULL) {
+ if (*d->d_name == '.')
+ continue;
+
+ ret = 0;
+ break;
+ }
+ if (closedir(dir) < 0) {
+ mail_storage_set_critical(storage, "closedir(%s) failed: %m",
+ path);
+ ret = -1;
+ }
+ return ret;
+}
+
+int dbox_mailbox_create(struct mailbox *box,
+ const struct mailbox_update *update, bool directory)
+{
+ struct dbox_storage *storage = DBOX_STORAGE(box->storage);
+ const char *alt_path;
+ struct stat st;
+ int ret;
+
+ if ((ret = index_storage_mailbox_create(box, directory)) <= 0)
+ return ret;
+ if (mailbox_open(box) < 0)
+ return -1;
+ if (mail_index_get_header(box->view)->uid_validity != 0 &&
+ !box->storage->rebuilding_list_index) {
+ mail_storage_set_error(box->storage, MAIL_ERROR_EXISTS,
+ "Mailbox already exists");
+ return -1;
+ }
+
+ /* if alt path already exists and contains files, rebuild storage so
+ that we don't start overwriting files. */
+ ret = mailbox_get_path_to(box, MAILBOX_LIST_PATH_TYPE_ALT_MAILBOX, &alt_path);
+ if (ret > 0 && stat(alt_path, &st) == 0) {
+ ret = dir_is_empty(box->storage, alt_path);
+ if (ret < 0)
+ return -1;
+ if (ret == 0) {
+ mailbox_set_critical(box,
+ "Existing files in alt path, "
+ "rebuilding storage to avoid losing messages");
+ storage->v.set_mailbox_corrupted(box);
+ return -1;
+ }
+ /* dir is empty, ignore it */
+ }
+ return dbox_mailbox_create_indexes(box, update);
+}
+
+int dbox_mailbox_create_indexes(struct mailbox *box,
+ const struct mailbox_update *update)
+{
+ struct dbox_storage *storage = DBOX_STORAGE(box->storage);
+ struct mail_index_sync_ctx *sync_ctx;
+ struct mail_index_view *view;
+ struct mail_index_transaction *trans;
+ int ret;
+
+ /* use syncing as a lock */
+ ret = mail_index_sync_begin(box->index, &sync_ctx, &view, &trans, 0);
+ if (ret <= 0) {
+ i_assert(ret != 0);
+ mailbox_set_index_error(box);
+ return -1;
+ }
+
+ if (mail_index_get_header(view)->uid_validity == 0) {
+ if (storage->v.mailbox_create_indexes(box, update, trans) < 0) {
+ mail_index_sync_rollback(&sync_ctx);
+ return -1;
+ }
+ }
+
+ return mail_index_sync_commit(&sync_ctx);
+}
+
+int dbox_verify_alt_storage(struct mailbox_list *list)
+{
+ const char *alt_path;
+ struct stat st;
+
+ if (!mailbox_list_get_root_path(list, MAILBOX_LIST_PATH_TYPE_ALT_DIR,
+ &alt_path))
+ return 0;
+
+ /* make sure alt storage is mounted. if it's not, abort the rebuild. */
+ if (stat(alt_path, &st) == 0)
+ return 0;
+ if (errno != ENOENT) {
+ i_error("stat(%s) failed: %m", alt_path);
+ return -1;
+ }
+
+ /* try to create the alt directory. if it fails, it means alt
+ storage isn't mounted. */
+ if (mailbox_list_mkdir_root(list, alt_path,
+ MAILBOX_LIST_PATH_TYPE_ALT_DIR) < 0)
+ return -1;
+ return 0;
+}
+
+bool dbox_header_have_flag(struct mailbox *box, uint32_t ext_id,
+ unsigned int flags_offset, uint8_t flag)
+{
+ const void *data;
+ size_t data_size;
+ uint8_t flags = 0;
+
+ mail_index_get_header_ext(box->view, ext_id, &data, &data_size);
+ if (flags_offset < data_size)
+ flags = *((const uint8_t *)data + flags_offset);
+ return (flags & flag) != 0;
+}
diff --git a/src/lib-storage/index/dbox-common/dbox-storage.h b/src/lib-storage/index/dbox-common/dbox-storage.h
new file mode 100644
index 0000000..af085df
--- /dev/null
+++ b/src/lib-storage/index/dbox-common/dbox-storage.h
@@ -0,0 +1,85 @@
+#ifndef DBOX_STORAGE_H
+#define DBOX_STORAGE_H
+
+#include "mail-storage-private.h"
+
+struct dbox_file;
+struct dbox_mail;
+struct dbox_storage;
+struct dbox_save_context;
+
+#define DBOX_SUBSCRIPTION_FILE_NAME "subscriptions"
+#define DBOX_UIDVALIDITY_FILE_NAME "dovecot-uidvalidity"
+#define DBOX_TEMP_FILE_PREFIX ".temp."
+#define DBOX_ALT_SYMLINK_NAME "dbox-alt-root"
+
+#define DBOX_MAILBOX_DIR_NAME "mailboxes"
+#define DBOX_TRASH_DIR_NAME "trash"
+#define DBOX_MAILDIR_NAME "dbox-Mails"
+
+/* Delete temp files having ctime older than this. */
+#define DBOX_TMP_DELETE_SECS (36*60*60)
+
+/* Flag specifies if the message should be in primary or alternative storage */
+#define DBOX_INDEX_FLAG_ALT MAIL_INDEX_MAIL_FLAG_BACKEND
+
+enum dbox_index_header_flags {
+ /* messages' metadata contain POP3 UIDLs */
+ DBOX_INDEX_HEADER_FLAG_HAVE_POP3_UIDLS = 0x01,
+ /* messages' metadata contain POP3 orders */
+ DBOX_INDEX_HEADER_FLAG_HAVE_POP3_ORDERS = 0x02
+};
+
+struct dbox_storage_vfuncs {
+ /* dbox file has zero references now. it should be either freed or
+ left open in case it's accessed again soon */
+ void (*file_unrefed)(struct dbox_file *file);
+ /* create a new file using the same permissions as file.
+ if parents=TRUE, create the directory if necessary */
+ int (*file_create_fd)(struct dbox_file *file, const char *path,
+ bool parents);
+ /* open the mail and return its file/offset */
+ int (*mail_open)(struct dbox_mail *mail, uoff_t *offset_r,
+ struct dbox_file **file_r);
+ /* create/update mailbox indexes */
+ int (*mailbox_create_indexes)(struct mailbox *box,
+ const struct mailbox_update *update,
+ struct mail_index_transaction *trans);
+ /* returns attachment path suffix. mdbox returns "", sdbox returns
+ "-<mailbox_guid>-<uid>" */
+ const char *(*get_attachment_path_suffix)(struct dbox_file *file);
+ /* mark the mailbox corrupted */
+ void (*set_mailbox_corrupted)(struct mailbox *box);
+ /* mark the file corrupted */
+ void (*set_file_corrupted)(struct dbox_file *file);
+};
+
+struct dbox_storage {
+ struct mail_storage storage;
+ struct dbox_storage_vfuncs v;
+
+ struct fs *attachment_fs;
+ const char *attachment_dir;
+};
+
+#define DBOX_STORAGE(s) container_of(s, struct dbox_storage, storage)
+
+void dbox_storage_get_list_settings(const struct mail_namespace *ns,
+ struct mailbox_list_settings *set);
+int dbox_storage_create(struct mail_storage *storage,
+ struct mail_namespace *ns,
+ const char **error_r);
+void dbox_storage_destroy(struct mail_storage *storage);
+uint32_t dbox_get_uidvalidity_next(struct mailbox_list *list);
+void dbox_notify_changes(struct mailbox *box);
+int dbox_mailbox_check_existence(struct mailbox *box, time_t *path_ctime_r);
+int dbox_mailbox_open(struct mailbox *box, time_t path_ctime);
+int dbox_mailbox_create(struct mailbox *box,
+ const struct mailbox_update *update, bool directory);
+int dbox_mailbox_create_indexes(struct mailbox *box,
+ const struct mailbox_update *update);
+int dbox_verify_alt_storage(struct mailbox_list *list);
+bool dbox_header_have_flag(struct mailbox *box, uint32_t ext_id,
+ unsigned int flags_offset, uint8_t flag);
+
+#endif
diff --git a/src/lib-storage/index/dbox-multi/Makefile.am b/src/lib-storage/index/dbox-multi/Makefile.am
new file mode 100644
index 0000000..810edd2
--- /dev/null
+++ b/src/lib-storage/index/dbox-multi/Makefile.am
@@ -0,0 +1,36 @@
+noinst_LTLIBRARIES = libstorage_dbox_multi.la
+
+AM_CPPFLAGS = \
+ -I$(top_srcdir)/src/lib \
+ -I$(top_srcdir)/src/lib-settings \
+ -I$(top_srcdir)/src/lib-master \
+ -I$(top_srcdir)/src/lib-mail \
+ -I$(top_srcdir)/src/lib-imap \
+ -I$(top_srcdir)/src/lib-index \
+ -I$(top_srcdir)/src/lib-storage \
+ -I$(top_srcdir)/src/lib-storage/index \
+ -I$(top_srcdir)/src/lib-storage/index/dbox-common
+
+libstorage_dbox_multi_la_SOURCES = \
+ mdbox-deleted-storage.c \
+ mdbox-file.c \
+ mdbox-mail.c \
+ mdbox-map.c \
+ mdbox-purge.c \
+ mdbox-save.c \
+ mdbox-settings.c \
+ mdbox-sync.c \
+ mdbox-storage.c \
+ mdbox-storage-rebuild.c
+
+headers = \
+ mdbox-file.h \
+ mdbox-map.h \
+ mdbox-map-private.h \
+ mdbox-settings.h \
+ mdbox-storage.h \
+ mdbox-storage-rebuild.h \
+ mdbox-sync.h
+
+pkginc_libdir=$(pkgincludedir)
+pkginc_lib_HEADERS = $(headers)
diff --git a/src/lib-storage/index/dbox-multi/Makefile.in b/src/lib-storage/index/dbox-multi/Makefile.in
new file mode 100644
index 0000000..7e87b89
--- /dev/null
+++ b/src/lib-storage/index/dbox-multi/Makefile.in
@@ -0,0 +1,866 @@
+# Makefile.in generated by automake 1.16.1 from Makefile.am.
+# @configure_input@
+
+# Copyright (C) 1994-2018 Free Software Foundation, Inc.
+
+# This Makefile.in is free software; the Free Software Foundation
+# gives unlimited permission to copy and/or distribute it,
+# with or without modifications, as long as this notice is preserved.
+
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY, to the extent permitted by law; without
+# even the implied warranty of MERCHANTABILITY or FITNESS FOR A
+# PARTICULAR PURPOSE.
+
+@SET_MAKE@
+
+
+VPATH = @srcdir@
+am__is_gnu_make = { \
+ if test -z '$(MAKELEVEL)'; then \
+ false; \
+ elif test -n '$(MAKE_HOST)'; then \
+ true; \
+ elif test -n '$(MAKE_VERSION)' && test -n '$(CURDIR)'; then \
+ true; \
+ else \
+ false; \
+ fi; \
+}
+am__make_running_with_option = \
+ case $${target_option-} in \
+ ?) ;; \
+ *) echo "am__make_running_with_option: internal error: invalid" \
+ "target option '$${target_option-}' specified" >&2; \
+ exit 1;; \
+ esac; \
+ has_opt=no; \
+ sane_makeflags=$$MAKEFLAGS; \
+ if $(am__is_gnu_make); then \
+ sane_makeflags=$$MFLAGS; \
+ else \
+ case $$MAKEFLAGS in \
+ *\\[\ \ ]*) \
+ bs=\\; \
+ sane_makeflags=`printf '%s\n' "$$MAKEFLAGS" \
+ | sed "s/$$bs$$bs[$$bs $$bs ]*//g"`;; \
+ esac; \
+ fi; \
+ skip_next=no; \
+ strip_trailopt () \
+ { \
+ flg=`printf '%s\n' "$$flg" | sed "s/$$1.*$$//"`; \
+ }; \
+ for flg in $$sane_makeflags; do \
+ test $$skip_next = yes && { skip_next=no; continue; }; \
+ case $$flg in \
+ *=*|--*) continue;; \
+ -*I) strip_trailopt 'I'; skip_next=yes;; \
+ -*I?*) strip_trailopt 'I';; \
+ -*O) strip_trailopt 'O'; skip_next=yes;; \
+ -*O?*) strip_trailopt 'O';; \
+ -*l) strip_trailopt 'l'; skip_next=yes;; \
+ -*l?*) strip_trailopt 'l';; \
+ -[dEDm]) skip_next=yes;; \
+ -[JT]) skip_next=yes;; \
+ esac; \
+ case $$flg in \
+ *$$target_option*) has_opt=yes; break;; \
+ esac; \
+ done; \
+ test $$has_opt = yes
+am__make_dryrun = (target_option=n; $(am__make_running_with_option))
+am__make_keepgoing = (target_option=k; $(am__make_running_with_option))
+pkgdatadir = $(datadir)/@PACKAGE@
+pkgincludedir = $(includedir)/@PACKAGE@
+pkglibdir = $(libdir)/@PACKAGE@
+pkglibexecdir = $(libexecdir)/@PACKAGE@
+am__cd = CDPATH="$${ZSH_VERSION+.}$(PATH_SEPARATOR)" && cd
+install_sh_DATA = $(install_sh) -c -m 644
+install_sh_PROGRAM = $(install_sh) -c
+install_sh_SCRIPT = $(install_sh) -c
+INSTALL_HEADER = $(INSTALL_DATA)
+transform = $(program_transform_name)
+NORMAL_INSTALL = :
+PRE_INSTALL = :
+POST_INSTALL = :
+NORMAL_UNINSTALL = :
+PRE_UNINSTALL = :
+POST_UNINSTALL = :
+build_triplet = @build@
+host_triplet = @host@
+subdir = src/lib-storage/index/dbox-multi
+ACLOCAL_M4 = $(top_srcdir)/aclocal.m4
+am__aclocal_m4_deps = $(top_srcdir)/m4/ac_checktype2.m4 \
+ $(top_srcdir)/m4/ac_typeof.m4 $(top_srcdir)/m4/arc4random.m4 \
+ $(top_srcdir)/m4/blockdev.m4 $(top_srcdir)/m4/c99_vsnprintf.m4 \
+ $(top_srcdir)/m4/clock_gettime.m4 $(top_srcdir)/m4/crypt.m4 \
+ $(top_srcdir)/m4/crypt_xpg6.m4 $(top_srcdir)/m4/dbqlk.m4 \
+ $(top_srcdir)/m4/dirent_dtype.m4 $(top_srcdir)/m4/dovecot.m4 \
+ $(top_srcdir)/m4/fd_passing.m4 $(top_srcdir)/m4/fdatasync.m4 \
+ $(top_srcdir)/m4/flexible_array_member.m4 \
+ $(top_srcdir)/m4/glibc.m4 $(top_srcdir)/m4/gmtime_max.m4 \
+ $(top_srcdir)/m4/gmtime_tm_gmtoff.m4 \
+ $(top_srcdir)/m4/ioloop.m4 $(top_srcdir)/m4/iovec.m4 \
+ $(top_srcdir)/m4/ipv6.m4 $(top_srcdir)/m4/libcap.m4 \
+ $(top_srcdir)/m4/libtool.m4 $(top_srcdir)/m4/libwrap.m4 \
+ $(top_srcdir)/m4/linux_mremap.m4 $(top_srcdir)/m4/ltoptions.m4 \
+ $(top_srcdir)/m4/ltsugar.m4 $(top_srcdir)/m4/ltversion.m4 \
+ $(top_srcdir)/m4/lt~obsolete.m4 $(top_srcdir)/m4/mmap_write.m4 \
+ $(top_srcdir)/m4/mntctl.m4 $(top_srcdir)/m4/modules.m4 \
+ $(top_srcdir)/m4/notify.m4 $(top_srcdir)/m4/nsl.m4 \
+ $(top_srcdir)/m4/off_t_max.m4 $(top_srcdir)/m4/pkg.m4 \
+ $(top_srcdir)/m4/pr_set_dumpable.m4 \
+ $(top_srcdir)/m4/q_quotactl.m4 $(top_srcdir)/m4/quota.m4 \
+ $(top_srcdir)/m4/random.m4 $(top_srcdir)/m4/rlimit.m4 \
+ $(top_srcdir)/m4/sendfile.m4 $(top_srcdir)/m4/size_t_signed.m4 \
+ $(top_srcdir)/m4/sockpeercred.m4 $(top_srcdir)/m4/sql.m4 \
+ $(top_srcdir)/m4/ssl.m4 $(top_srcdir)/m4/st_tim.m4 \
+ $(top_srcdir)/m4/static_array.m4 $(top_srcdir)/m4/test_with.m4 \
+ $(top_srcdir)/m4/time_t.m4 $(top_srcdir)/m4/typeof.m4 \
+ $(top_srcdir)/m4/typeof_dev_t.m4 \
+ $(top_srcdir)/m4/uoff_t_max.m4 $(top_srcdir)/m4/vararg.m4 \
+ $(top_srcdir)/m4/want_apparmor.m4 \
+ $(top_srcdir)/m4/want_bsdauth.m4 \
+ $(top_srcdir)/m4/want_bzlib.m4 \
+ $(top_srcdir)/m4/want_cassandra.m4 \
+ $(top_srcdir)/m4/want_cdb.m4 \
+ $(top_srcdir)/m4/want_checkpassword.m4 \
+ $(top_srcdir)/m4/want_clucene.m4 $(top_srcdir)/m4/want_db.m4 \
+ $(top_srcdir)/m4/want_gssapi.m4 $(top_srcdir)/m4/want_icu.m4 \
+ $(top_srcdir)/m4/want_ldap.m4 $(top_srcdir)/m4/want_lua.m4 \
+ $(top_srcdir)/m4/want_lz4.m4 $(top_srcdir)/m4/want_lzma.m4 \
+ $(top_srcdir)/m4/want_mysql.m4 $(top_srcdir)/m4/want_pam.m4 \
+ $(top_srcdir)/m4/want_passwd.m4 $(top_srcdir)/m4/want_pgsql.m4 \
+ $(top_srcdir)/m4/want_prefetch.m4 \
+ $(top_srcdir)/m4/want_shadow.m4 \
+ $(top_srcdir)/m4/want_sodium.m4 $(top_srcdir)/m4/want_solr.m4 \
+ $(top_srcdir)/m4/want_sqlite.m4 \
+ $(top_srcdir)/m4/want_stemmer.m4 \
+ $(top_srcdir)/m4/want_systemd.m4 \
+ $(top_srcdir)/m4/want_textcat.m4 \
+ $(top_srcdir)/m4/want_unwind.m4 $(top_srcdir)/m4/want_zlib.m4 \
+ $(top_srcdir)/m4/want_zstd.m4 $(top_srcdir)/configure.ac
+am__configure_deps = $(am__aclocal_m4_deps) $(CONFIGURE_DEPENDENCIES) \
+ $(ACLOCAL_M4)
+DIST_COMMON = $(srcdir)/Makefile.am $(pkginc_lib_HEADERS) \
+ $(am__DIST_COMMON)
+mkinstalldirs = $(install_sh) -d
+CONFIG_HEADER = $(top_builddir)/config.h
+CONFIG_CLEAN_FILES =
+CONFIG_CLEAN_VPATH_FILES =
+LTLIBRARIES = $(noinst_LTLIBRARIES)
+libstorage_dbox_multi_la_LIBADD =
+am_libstorage_dbox_multi_la_OBJECTS = mdbox-deleted-storage.lo \
+ mdbox-file.lo mdbox-mail.lo mdbox-map.lo mdbox-purge.lo \
+ mdbox-save.lo mdbox-settings.lo mdbox-sync.lo mdbox-storage.lo \
+ mdbox-storage-rebuild.lo
+libstorage_dbox_multi_la_OBJECTS = \
+ $(am_libstorage_dbox_multi_la_OBJECTS)
+AM_V_lt = $(am__v_lt_@AM_V@)
+am__v_lt_ = $(am__v_lt_@AM_DEFAULT_V@)
+am__v_lt_0 = --silent
+am__v_lt_1 =
+AM_V_P = $(am__v_P_@AM_V@)
+am__v_P_ = $(am__v_P_@AM_DEFAULT_V@)
+am__v_P_0 = false
+am__v_P_1 = :
+AM_V_GEN = $(am__v_GEN_@AM_V@)
+am__v_GEN_ = $(am__v_GEN_@AM_DEFAULT_V@)
+am__v_GEN_0 = @echo " GEN " $@;
+am__v_GEN_1 =
+AM_V_at = $(am__v_at_@AM_V@)
+am__v_at_ = $(am__v_at_@AM_DEFAULT_V@)
+am__v_at_0 = @
+am__v_at_1 =
+DEFAULT_INCLUDES = -I.@am__isrc@ -I$(top_builddir)
+depcomp = $(SHELL) $(top_srcdir)/depcomp
+am__maybe_remake_depfiles = depfiles
+am__depfiles_remade = ./$(DEPDIR)/mdbox-deleted-storage.Plo \
+ ./$(DEPDIR)/mdbox-file.Plo ./$(DEPDIR)/mdbox-mail.Plo \
+ ./$(DEPDIR)/mdbox-map.Plo ./$(DEPDIR)/mdbox-purge.Plo \
+ ./$(DEPDIR)/mdbox-save.Plo ./$(DEPDIR)/mdbox-settings.Plo \
+ ./$(DEPDIR)/mdbox-storage-rebuild.Plo \
+ ./$(DEPDIR)/mdbox-storage.Plo ./$(DEPDIR)/mdbox-sync.Plo
+am__mv = mv -f
+COMPILE = $(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) \
+ $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS)
+LTCOMPILE = $(LIBTOOL) $(AM_V_lt) --tag=CC $(AM_LIBTOOLFLAGS) \
+ $(LIBTOOLFLAGS) --mode=compile $(CC) $(DEFS) \
+ $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) $(CPPFLAGS) \
+ $(AM_CFLAGS) $(CFLAGS)
+AM_V_CC = $(am__v_CC_@AM_V@)
+am__v_CC_ = $(am__v_CC_@AM_DEFAULT_V@)
+am__v_CC_0 = @echo " CC " $@;
+am__v_CC_1 =
+CCLD = $(CC)
+LINK = $(LIBTOOL) $(AM_V_lt) --tag=CC $(AM_LIBTOOLFLAGS) \
+ $(LIBTOOLFLAGS) --mode=link $(CCLD) $(AM_CFLAGS) $(CFLAGS) \
+ $(AM_LDFLAGS) $(LDFLAGS) -o $@
+AM_V_CCLD = $(am__v_CCLD_@AM_V@)
+am__v_CCLD_ = $(am__v_CCLD_@AM_DEFAULT_V@)
+am__v_CCLD_0 = @echo " CCLD " $@;
+am__v_CCLD_1 =
+SOURCES = $(libstorage_dbox_multi_la_SOURCES)
+DIST_SOURCES = $(libstorage_dbox_multi_la_SOURCES)
+am__can_run_installinfo = \
+ case $$AM_UPDATE_INFO_DIR in \
+ n|no|NO) false;; \
+ *) (install-info --version) >/dev/null 2>&1;; \
+ esac
+am__vpath_adj_setup = srcdirstrip=`echo "$(srcdir)" | sed 's|.|.|g'`;
+am__vpath_adj = case $$p in \
+ $(srcdir)/*) f=`echo "$$p" | sed "s|^$$srcdirstrip/||"`;; \
+ *) f=$$p;; \
+ esac;
+am__strip_dir = f=`echo $$p | sed -e 's|^.*/||'`;
+am__install_max = 40
+am__nobase_strip_setup = \
+ srcdirstrip=`echo "$(srcdir)" | sed 's/[].[^$$\\*|]/\\\\&/g'`
+am__nobase_strip = \
+ for p in $$list; do echo "$$p"; done | sed -e "s|$$srcdirstrip/||"
+am__nobase_list = $(am__nobase_strip_setup); \
+ for p in $$list; do echo "$$p $$p"; done | \
+ sed "s| $$srcdirstrip/| |;"' / .*\//!s/ .*/ ./; s,\( .*\)/[^/]*$$,\1,' | \
+ $(AWK) 'BEGIN { files["."] = "" } { files[$$2] = files[$$2] " " $$1; \
+ if (++n[$$2] == $(am__install_max)) \
+ { print $$2, files[$$2]; n[$$2] = 0; files[$$2] = "" } } \
+ END { for (dir in files) print dir, files[dir] }'
+am__base_list = \
+ sed '$$!N;$$!N;$$!N;$$!N;$$!N;$$!N;$$!N;s/\n/ /g' | \
+ sed '$$!N;$$!N;$$!N;$$!N;s/\n/ /g'
+am__uninstall_files_from_dir = { \
+ test -z "$$files" \
+ || { test ! -d "$$dir" && test ! -f "$$dir" && test ! -r "$$dir"; } \
+ || { echo " ( cd '$$dir' && rm -f" $$files ")"; \
+ $(am__cd) "$$dir" && rm -f $$files; }; \
+ }
+am__installdirs = "$(DESTDIR)$(pkginc_libdir)"
+HEADERS = $(pkginc_lib_HEADERS)
+am__tagged_files = $(HEADERS) $(SOURCES) $(TAGS_FILES) $(LISP)
+# Read a list of newline-separated strings from the standard input,
+# and print each of them once, without duplicates. Input order is
+# *not* preserved.
+am__uniquify_input = $(AWK) '\
+ BEGIN { nonempty = 0; } \
+ { items[$$0] = 1; nonempty = 1; } \
+ END { if (nonempty) { for (i in items) print i; }; } \
+'
+# Make sure the list of sources is unique. This is necessary because,
+# e.g., the same source file might be shared among _SOURCES variables
+# for different programs/libraries.
+am__define_uniq_tagged_files = \
+ list='$(am__tagged_files)'; \
+ unique=`for i in $$list; do \
+ if test -f "$$i"; then echo $$i; else echo $(srcdir)/$$i; fi; \
+ done | $(am__uniquify_input)`
+ETAGS = etags
+CTAGS = ctags
+am__DIST_COMMON = $(srcdir)/Makefile.in $(top_srcdir)/depcomp
+DISTFILES = $(DIST_COMMON) $(DIST_SOURCES) $(TEXINFOS) $(EXTRA_DIST)
+ACLOCAL = @ACLOCAL@
+ACLOCAL_AMFLAGS = @ACLOCAL_AMFLAGS@
+AMTAR = @AMTAR@
+AM_DEFAULT_VERBOSITY = @AM_DEFAULT_VERBOSITY@
+APPARMOR_LIBS = @APPARMOR_LIBS@
+AR = @AR@
+AUTH_CFLAGS = @AUTH_CFLAGS@
+AUTH_LIBS = @AUTH_LIBS@
+AUTOCONF = @AUTOCONF@
+AUTOHEADER = @AUTOHEADER@
+AUTOMAKE = @AUTOMAKE@
+AWK = @AWK@
+BINARY_CFLAGS = @BINARY_CFLAGS@
+BINARY_LDFLAGS = @BINARY_LDFLAGS@
+BISON = @BISON@
+CASSANDRA_CFLAGS = @CASSANDRA_CFLAGS@
+CASSANDRA_LIBS = @CASSANDRA_LIBS@
+CC = @CC@
+CCDEPMODE = @CCDEPMODE@
+CDB_LIBS = @CDB_LIBS@
+CFLAGS = @CFLAGS@
+CLUCENE_CFLAGS = @CLUCENE_CFLAGS@
+CLUCENE_LIBS = @CLUCENE_LIBS@
+COMPRESS_LIBS = @COMPRESS_LIBS@
+CPP = @CPP@
+CPPFLAGS = @CPPFLAGS@
+CRYPT_LIBS = @CRYPT_LIBS@
+CXX = @CXX@
+CXXCPP = @CXXCPP@
+CXXDEPMODE = @CXXDEPMODE@
+CXXFLAGS = @CXXFLAGS@
+CYGPATH_W = @CYGPATH_W@
+DEFS = @DEFS@
+DEPDIR = @DEPDIR@
+DICT_LIBS = @DICT_LIBS@
+DLLIB = @DLLIB@
+DLLTOOL = @DLLTOOL@
+DSYMUTIL = @DSYMUTIL@
+DUMPBIN = @DUMPBIN@
+ECHO_C = @ECHO_C@
+ECHO_N = @ECHO_N@
+ECHO_T = @ECHO_T@
+EGREP = @EGREP@
+EXEEXT = @EXEEXT@
+FGREP = @FGREP@
+FLEX = @FLEX@
+FUZZER_CPPFLAGS = @FUZZER_CPPFLAGS@
+FUZZER_LDFLAGS = @FUZZER_LDFLAGS@
+GREP = @GREP@
+INSTALL = @INSTALL@
+INSTALL_DATA = @INSTALL_DATA@
+INSTALL_PROGRAM = @INSTALL_PROGRAM@
+INSTALL_SCRIPT = @INSTALL_SCRIPT@
+INSTALL_STRIP_PROGRAM = @INSTALL_STRIP_PROGRAM@
+KRB5CONFIG = @KRB5CONFIG@
+KRB5_CFLAGS = @KRB5_CFLAGS@
+KRB5_LIBS = @KRB5_LIBS@
+LD = @LD@
+LDAP_LIBS = @LDAP_LIBS@
+LDFLAGS = @LDFLAGS@
+LD_NO_WHOLE_ARCHIVE = @LD_NO_WHOLE_ARCHIVE@
+LD_WHOLE_ARCHIVE = @LD_WHOLE_ARCHIVE@
+LIBCAP = @LIBCAP@
+LIBDOVECOT = @LIBDOVECOT@
+LIBDOVECOT_COMPRESS = @LIBDOVECOT_COMPRESS@
+LIBDOVECOT_DEPS = @LIBDOVECOT_DEPS@
+LIBDOVECOT_DSYNC = @LIBDOVECOT_DSYNC@
+LIBDOVECOT_LA_LIBS = @LIBDOVECOT_LA_LIBS@
+LIBDOVECOT_LDA = @LIBDOVECOT_LDA@
+LIBDOVECOT_LDAP = @LIBDOVECOT_LDAP@
+LIBDOVECOT_LIBFTS = @LIBDOVECOT_LIBFTS@
+LIBDOVECOT_LIBFTS_DEPS = @LIBDOVECOT_LIBFTS_DEPS@
+LIBDOVECOT_LOGIN = @LIBDOVECOT_LOGIN@
+LIBDOVECOT_LUA = @LIBDOVECOT_LUA@
+LIBDOVECOT_LUA_DEPS = @LIBDOVECOT_LUA_DEPS@
+LIBDOVECOT_SQL = @LIBDOVECOT_SQL@
+LIBDOVECOT_STORAGE = @LIBDOVECOT_STORAGE@
+LIBDOVECOT_STORAGE_DEPS = @LIBDOVECOT_STORAGE_DEPS@
+LIBEXTTEXTCAT_CFLAGS = @LIBEXTTEXTCAT_CFLAGS@
+LIBEXTTEXTCAT_LIBS = @LIBEXTTEXTCAT_LIBS@
+LIBICONV = @LIBICONV@
+LIBICU_CFLAGS = @LIBICU_CFLAGS@
+LIBICU_LIBS = @LIBICU_LIBS@
+LIBOBJS = @LIBOBJS@
+LIBS = @LIBS@
+LIBSODIUM_CFLAGS = @LIBSODIUM_CFLAGS@
+LIBSODIUM_LIBS = @LIBSODIUM_LIBS@
+LIBTIRPC_CFLAGS = @LIBTIRPC_CFLAGS@
+LIBTIRPC_LIBS = @LIBTIRPC_LIBS@
+LIBTOOL = @LIBTOOL@
+LIBUNWIND_CFLAGS = @LIBUNWIND_CFLAGS@
+LIBUNWIND_LIBS = @LIBUNWIND_LIBS@
+LIBWRAP_LIBS = @LIBWRAP_LIBS@
+LINKED_STORAGE_LDADD = @LINKED_STORAGE_LDADD@
+LIPO = @LIPO@
+LN_S = @LN_S@
+LTLIBICONV = @LTLIBICONV@
+LTLIBOBJS = @LTLIBOBJS@
+LT_SYS_LIBRARY_PATH = @LT_SYS_LIBRARY_PATH@
+LUA_CFLAGS = @LUA_CFLAGS@
+LUA_LIBS = @LUA_LIBS@
+MAINT = @MAINT@
+MAKEINFO = @MAKEINFO@
+MANIFEST_TOOL = @MANIFEST_TOOL@
+MKDIR_P = @MKDIR_P@
+MODULE_LIBS = @MODULE_LIBS@
+MODULE_SUFFIX = @MODULE_SUFFIX@
+MYSQL_CFLAGS = @MYSQL_CFLAGS@
+MYSQL_CONFIG = @MYSQL_CONFIG@
+MYSQL_LIBS = @MYSQL_LIBS@
+NM = @NM@
+NMEDIT = @NMEDIT@
+NOPLUGIN_LDFLAGS = @NOPLUGIN_LDFLAGS@
+OBJDUMP = @OBJDUMP@
+OBJEXT = @OBJEXT@
+OTOOL = @OTOOL@
+OTOOL64 = @OTOOL64@
+PACKAGE = @PACKAGE@
+PACKAGE_BUGREPORT = @PACKAGE_BUGREPORT@
+PACKAGE_NAME = @PACKAGE_NAME@
+PACKAGE_STRING = @PACKAGE_STRING@
+PACKAGE_TARNAME = @PACKAGE_TARNAME@
+PACKAGE_URL = @PACKAGE_URL@
+PACKAGE_VERSION = @PACKAGE_VERSION@
+PANDOC = @PANDOC@
+PATH_SEPARATOR = @PATH_SEPARATOR@
+PGSQL_CFLAGS = @PGSQL_CFLAGS@
+PGSQL_LIBS = @PGSQL_LIBS@
+PG_CONFIG = @PG_CONFIG@
+PIE_CFLAGS = @PIE_CFLAGS@
+PIE_LDFLAGS = @PIE_LDFLAGS@
+PKG_CONFIG = @PKG_CONFIG@
+PKG_CONFIG_LIBDIR = @PKG_CONFIG_LIBDIR@
+PKG_CONFIG_PATH = @PKG_CONFIG_PATH@
+QUOTA_LIBS = @QUOTA_LIBS@
+RANLIB = @RANLIB@
+RELRO_LDFLAGS = @RELRO_LDFLAGS@
+RPCGEN = @RPCGEN@
+RUN_TEST = @RUN_TEST@
+SED = @SED@
+SETTING_FILES = @SETTING_FILES@
+SET_MAKE = @SET_MAKE@
+SHELL = @SHELL@
+SQLITE_CFLAGS = @SQLITE_CFLAGS@
+SQLITE_LIBS = @SQLITE_LIBS@
+SQL_CFLAGS = @SQL_CFLAGS@
+SQL_LIBS = @SQL_LIBS@
+SSL_CFLAGS = @SSL_CFLAGS@
+SSL_LIBS = @SSL_LIBS@
+STRIP = @STRIP@
+SYSTEMD_CFLAGS = @SYSTEMD_CFLAGS@
+SYSTEMD_LIBS = @SYSTEMD_LIBS@
+VALGRIND = @VALGRIND@
+VERSION = @VERSION@
+ZSTD_CFLAGS = @ZSTD_CFLAGS@
+ZSTD_LIBS = @ZSTD_LIBS@
+abs_builddir = @abs_builddir@
+abs_srcdir = @abs_srcdir@
+abs_top_builddir = @abs_top_builddir@
+abs_top_srcdir = @abs_top_srcdir@
+ac_ct_AR = @ac_ct_AR@
+ac_ct_CC = @ac_ct_CC@
+ac_ct_CXX = @ac_ct_CXX@
+ac_ct_DUMPBIN = @ac_ct_DUMPBIN@
+am__include = @am__include@
+am__leading_dot = @am__leading_dot@
+am__quote = @am__quote@
+am__tar = @am__tar@
+am__untar = @am__untar@
+bindir = @bindir@
+build = @build@
+build_alias = @build_alias@
+build_cpu = @build_cpu@
+build_os = @build_os@
+build_vendor = @build_vendor@
+builddir = @builddir@
+datadir = @datadir@
+datarootdir = @datarootdir@
+dict_drivers = @dict_drivers@
+docdir = @docdir@
+dvidir = @dvidir@
+exec_prefix = @exec_prefix@
+host = @host@
+host_alias = @host_alias@
+host_cpu = @host_cpu@
+host_os = @host_os@
+host_vendor = @host_vendor@
+htmldir = @htmldir@
+includedir = @includedir@
+infodir = @infodir@
+install_sh = @install_sh@
+libdir = @libdir@
+libexecdir = @libexecdir@
+localedir = @localedir@
+localstatedir = @localstatedir@
+mandir = @mandir@
+mkdir_p = @mkdir_p@
+moduledir = @moduledir@
+oldincludedir = @oldincludedir@
+pdfdir = @pdfdir@
+prefix = @prefix@
+program_transform_name = @program_transform_name@
+psdir = @psdir@
+rundir = @rundir@
+runstatedir = @runstatedir@
+sbindir = @sbindir@
+sharedstatedir = @sharedstatedir@
+sql_drivers = @sql_drivers@
+srcdir = @srcdir@
+ssldir = @ssldir@
+statedir = @statedir@
+sysconfdir = @sysconfdir@
+systemdservicetype = @systemdservicetype@
+systemdsystemunitdir = @systemdsystemunitdir@
+target_alias = @target_alias@
+top_build_prefix = @top_build_prefix@
+top_builddir = @top_builddir@
+top_srcdir = @top_srcdir@
+noinst_LTLIBRARIES = libstorage_dbox_multi.la
+AM_CPPFLAGS = \
+ -I$(top_srcdir)/src/lib \
+ -I$(top_srcdir)/src/lib-settings \
+ -I$(top_srcdir)/src/lib-master \
+ -I$(top_srcdir)/src/lib-mail \
+ -I$(top_srcdir)/src/lib-imap \
+ -I$(top_srcdir)/src/lib-index \
+ -I$(top_srcdir)/src/lib-storage \
+ -I$(top_srcdir)/src/lib-storage/index \
+ -I$(top_srcdir)/src/lib-storage/index/dbox-common
+
+libstorage_dbox_multi_la_SOURCES = \
+ mdbox-deleted-storage.c \
+ mdbox-file.c \
+ mdbox-mail.c \
+ mdbox-map.c \
+ mdbox-purge.c \
+ mdbox-save.c \
+ mdbox-settings.c \
+ mdbox-sync.c \
+ mdbox-storage.c \
+ mdbox-storage-rebuild.c
+
+headers = \
+ mdbox-file.h \
+ mdbox-map.h \
+ mdbox-map-private.h \
+ mdbox-settings.h \
+ mdbox-storage.h \
+ mdbox-storage-rebuild.h \
+ mdbox-sync.h
+
+pkginc_libdir = $(pkgincludedir)
+pkginc_lib_HEADERS = $(headers)
+all: all-am
+
+.SUFFIXES:
+.SUFFIXES: .c .lo .o .obj
+$(srcdir)/Makefile.in: @MAINTAINER_MODE_TRUE@ $(srcdir)/Makefile.am $(am__configure_deps)
+ @for dep in $?; do \
+ case '$(am__configure_deps)' in \
+ *$$dep*) \
+ ( cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh ) \
+ && { if test -f $@; then exit 0; else break; fi; }; \
+ exit 1;; \
+ esac; \
+ done; \
+ echo ' cd $(top_srcdir) && $(AUTOMAKE) --foreign src/lib-storage/index/dbox-multi/Makefile'; \
+ $(am__cd) $(top_srcdir) && \
+ $(AUTOMAKE) --foreign src/lib-storage/index/dbox-multi/Makefile
+Makefile: $(srcdir)/Makefile.in $(top_builddir)/config.status
+ @case '$?' in \
+ *config.status*) \
+ cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh;; \
+ *) \
+ echo ' cd $(top_builddir) && $(SHELL) ./config.status $(subdir)/$@ $(am__maybe_remake_depfiles)'; \
+ cd $(top_builddir) && $(SHELL) ./config.status $(subdir)/$@ $(am__maybe_remake_depfiles);; \
+ esac;
+
+$(top_builddir)/config.status: $(top_srcdir)/configure $(CONFIG_STATUS_DEPENDENCIES)
+ cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh
+
+$(top_srcdir)/configure: @MAINTAINER_MODE_TRUE@ $(am__configure_deps)
+ cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh
+$(ACLOCAL_M4): @MAINTAINER_MODE_TRUE@ $(am__aclocal_m4_deps)
+ cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh
+$(am__aclocal_m4_deps):
+
+clean-noinstLTLIBRARIES:
+ -test -z "$(noinst_LTLIBRARIES)" || rm -f $(noinst_LTLIBRARIES)
+ @list='$(noinst_LTLIBRARIES)'; \
+ locs=`for p in $$list; do echo $$p; done | \
+ sed 's|^[^/]*$$|.|; s|/[^/]*$$||; s|$$|/so_locations|' | \
+ sort -u`; \
+ test -z "$$locs" || { \
+ echo rm -f $${locs}; \
+ rm -f $${locs}; \
+ }
+
+libstorage_dbox_multi.la: $(libstorage_dbox_multi_la_OBJECTS) $(libstorage_dbox_multi_la_DEPENDENCIES) $(EXTRA_libstorage_dbox_multi_la_DEPENDENCIES)
+ $(AM_V_CCLD)$(LINK) $(libstorage_dbox_multi_la_OBJECTS) $(libstorage_dbox_multi_la_LIBADD) $(LIBS)
+
+mostlyclean-compile:
+ -rm -f *.$(OBJEXT)
+
+distclean-compile:
+ -rm -f *.tab.c
+
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/mdbox-deleted-storage.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/mdbox-file.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/mdbox-mail.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/mdbox-map.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/mdbox-purge.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/mdbox-save.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/mdbox-settings.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/mdbox-storage-rebuild.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/mdbox-storage.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/mdbox-sync.Plo@am__quote@ # am--include-marker
+
+$(am__depfiles_remade):
+ @$(MKDIR_P) $(@D)
+ @echo '# dummy' >$@-t && $(am__mv) $@-t $@
+
+am--depfiles: $(am__depfiles_remade)
+
+.c.o:
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(COMPILE) -MT $@ -MD -MP -MF $(DEPDIR)/$*.Tpo -c -o $@ $<
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/$*.Tpo $(DEPDIR)/$*.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='$<' object='$@' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(COMPILE) -c -o $@ $<
+
+.c.obj:
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(COMPILE) -MT $@ -MD -MP -MF $(DEPDIR)/$*.Tpo -c -o $@ `$(CYGPATH_W) '$<'`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/$*.Tpo $(DEPDIR)/$*.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='$<' object='$@' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(COMPILE) -c -o $@ `$(CYGPATH_W) '$<'`
+
+.c.lo:
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(LTCOMPILE) -MT $@ -MD -MP -MF $(DEPDIR)/$*.Tpo -c -o $@ $<
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/$*.Tpo $(DEPDIR)/$*.Plo
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='$<' object='$@' libtool=yes @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(LTCOMPILE) -c -o $@ $<
+
+mostlyclean-libtool:
+ -rm -f *.lo
+
+clean-libtool:
+ -rm -rf .libs _libs
+install-pkginc_libHEADERS: $(pkginc_lib_HEADERS)
+ @$(NORMAL_INSTALL)
+ @list='$(pkginc_lib_HEADERS)'; test -n "$(pkginc_libdir)" || list=; \
+ if test -n "$$list"; then \
+ echo " $(MKDIR_P) '$(DESTDIR)$(pkginc_libdir)'"; \
+ $(MKDIR_P) "$(DESTDIR)$(pkginc_libdir)" || exit 1; \
+ fi; \
+ for p in $$list; do \
+ if test -f "$$p"; then d=; else d="$(srcdir)/"; fi; \
+ echo "$$d$$p"; \
+ done | $(am__base_list) | \
+ while read files; do \
+ echo " $(INSTALL_HEADER) $$files '$(DESTDIR)$(pkginc_libdir)'"; \
+ $(INSTALL_HEADER) $$files "$(DESTDIR)$(pkginc_libdir)" || exit $$?; \
+ done
+
+uninstall-pkginc_libHEADERS:
+ @$(NORMAL_UNINSTALL)
+ @list='$(pkginc_lib_HEADERS)'; test -n "$(pkginc_libdir)" || list=; \
+ files=`for p in $$list; do echo $$p; done | sed -e 's|^.*/||'`; \
+ dir='$(DESTDIR)$(pkginc_libdir)'; $(am__uninstall_files_from_dir)
+
+ID: $(am__tagged_files)
+ $(am__define_uniq_tagged_files); mkid -fID $$unique
+tags: tags-am
+TAGS: tags
+
+tags-am: $(TAGS_DEPENDENCIES) $(am__tagged_files)
+ set x; \
+ here=`pwd`; \
+ $(am__define_uniq_tagged_files); \
+ shift; \
+ if test -z "$(ETAGS_ARGS)$$*$$unique"; then :; else \
+ test -n "$$unique" || unique=$$empty_fix; \
+ if test $$# -gt 0; then \
+ $(ETAGS) $(ETAGSFLAGS) $(AM_ETAGSFLAGS) $(ETAGS_ARGS) \
+ "$$@" $$unique; \
+ else \
+ $(ETAGS) $(ETAGSFLAGS) $(AM_ETAGSFLAGS) $(ETAGS_ARGS) \
+ $$unique; \
+ fi; \
+ fi
+ctags: ctags-am
+
+CTAGS: ctags
+ctags-am: $(TAGS_DEPENDENCIES) $(am__tagged_files)
+ $(am__define_uniq_tagged_files); \
+ test -z "$(CTAGS_ARGS)$$unique" \
+ || $(CTAGS) $(CTAGSFLAGS) $(AM_CTAGSFLAGS) $(CTAGS_ARGS) \
+ $$unique
+
+GTAGS:
+ here=`$(am__cd) $(top_builddir) && pwd` \
+ && $(am__cd) $(top_srcdir) \
+ && gtags -i $(GTAGS_ARGS) "$$here"
+cscopelist: cscopelist-am
+
+cscopelist-am: $(am__tagged_files)
+ list='$(am__tagged_files)'; \
+ case "$(srcdir)" in \
+ [\\/]* | ?:[\\/]*) sdir="$(srcdir)" ;; \
+ *) sdir=$(subdir)/$(srcdir) ;; \
+ esac; \
+ for i in $$list; do \
+ if test -f "$$i"; then \
+ echo "$(subdir)/$$i"; \
+ else \
+ echo "$$sdir/$$i"; \
+ fi; \
+ done >> $(top_builddir)/cscope.files
+
+distclean-tags:
+ -rm -f TAGS ID GTAGS GRTAGS GSYMS GPATH tags
+
+distdir: $(BUILT_SOURCES)
+ $(MAKE) $(AM_MAKEFLAGS) distdir-am
+
+distdir-am: $(DISTFILES)
+ @srcdirstrip=`echo "$(srcdir)" | sed 's/[].[^$$\\*]/\\\\&/g'`; \
+ topsrcdirstrip=`echo "$(top_srcdir)" | sed 's/[].[^$$\\*]/\\\\&/g'`; \
+ list='$(DISTFILES)'; \
+ dist_files=`for file in $$list; do echo $$file; done | \
+ sed -e "s|^$$srcdirstrip/||;t" \
+ -e "s|^$$topsrcdirstrip/|$(top_builddir)/|;t"`; \
+ case $$dist_files in \
+ */*) $(MKDIR_P) `echo "$$dist_files" | \
+ sed '/\//!d;s|^|$(distdir)/|;s,/[^/]*$$,,' | \
+ sort -u` ;; \
+ esac; \
+ for file in $$dist_files; do \
+ if test -f $$file || test -d $$file; then d=.; else d=$(srcdir); fi; \
+ if test -d $$d/$$file; then \
+ dir=`echo "/$$file" | sed -e 's,/[^/]*$$,,'`; \
+ if test -d "$(distdir)/$$file"; then \
+ find "$(distdir)/$$file" -type d ! -perm -700 -exec chmod u+rwx {} \;; \
+ fi; \
+ if test -d $(srcdir)/$$file && test $$d != $(srcdir); then \
+ cp -fpR $(srcdir)/$$file "$(distdir)$$dir" || exit 1; \
+ find "$(distdir)/$$file" -type d ! -perm -700 -exec chmod u+rwx {} \;; \
+ fi; \
+ cp -fpR $$d/$$file "$(distdir)$$dir" || exit 1; \
+ else \
+ test -f "$(distdir)/$$file" \
+ || cp -p $$d/$$file "$(distdir)/$$file" \
+ || exit 1; \
+ fi; \
+ done
+check-am: all-am
+check: check-am
+all-am: Makefile $(LTLIBRARIES) $(HEADERS)
+installdirs:
+ for dir in "$(DESTDIR)$(pkginc_libdir)"; do \
+ test -z "$$dir" || $(MKDIR_P) "$$dir"; \
+ done
+install: install-am
+install-exec: install-exec-am
+install-data: install-data-am
+uninstall: uninstall-am
+
+install-am: all-am
+ @$(MAKE) $(AM_MAKEFLAGS) install-exec-am install-data-am
+
+installcheck: installcheck-am
+install-strip:
+ if test -z '$(STRIP)'; then \
+ $(MAKE) $(AM_MAKEFLAGS) INSTALL_PROGRAM="$(INSTALL_STRIP_PROGRAM)" \
+ install_sh_PROGRAM="$(INSTALL_STRIP_PROGRAM)" INSTALL_STRIP_FLAG=-s \
+ install; \
+ else \
+ $(MAKE) $(AM_MAKEFLAGS) INSTALL_PROGRAM="$(INSTALL_STRIP_PROGRAM)" \
+ install_sh_PROGRAM="$(INSTALL_STRIP_PROGRAM)" INSTALL_STRIP_FLAG=-s \
+ "INSTALL_PROGRAM_ENV=STRIPPROG='$(STRIP)'" install; \
+ fi
+mostlyclean-generic:
+
+clean-generic:
+
+distclean-generic:
+ -test -z "$(CONFIG_CLEAN_FILES)" || rm -f $(CONFIG_CLEAN_FILES)
+ -test . = "$(srcdir)" || test -z "$(CONFIG_CLEAN_VPATH_FILES)" || rm -f $(CONFIG_CLEAN_VPATH_FILES)
+
+maintainer-clean-generic:
+ @echo "This command is intended for maintainers to use"
+ @echo "it deletes files that may require special tools to rebuild."
+clean: clean-am
+
+clean-am: clean-generic clean-libtool clean-noinstLTLIBRARIES \
+ mostlyclean-am
+
+distclean: distclean-am
+ -rm -f ./$(DEPDIR)/mdbox-deleted-storage.Plo
+ -rm -f ./$(DEPDIR)/mdbox-file.Plo
+ -rm -f ./$(DEPDIR)/mdbox-mail.Plo
+ -rm -f ./$(DEPDIR)/mdbox-map.Plo
+ -rm -f ./$(DEPDIR)/mdbox-purge.Plo
+ -rm -f ./$(DEPDIR)/mdbox-save.Plo
+ -rm -f ./$(DEPDIR)/mdbox-settings.Plo
+ -rm -f ./$(DEPDIR)/mdbox-storage-rebuild.Plo
+ -rm -f ./$(DEPDIR)/mdbox-storage.Plo
+ -rm -f ./$(DEPDIR)/mdbox-sync.Plo
+ -rm -f Makefile
+distclean-am: clean-am distclean-compile distclean-generic \
+ distclean-tags
+
+dvi: dvi-am
+
+dvi-am:
+
+html: html-am
+
+html-am:
+
+info: info-am
+
+info-am:
+
+install-data-am: install-pkginc_libHEADERS
+
+install-dvi: install-dvi-am
+
+install-dvi-am:
+
+install-exec-am:
+
+install-html: install-html-am
+
+install-html-am:
+
+install-info: install-info-am
+
+install-info-am:
+
+install-man:
+
+install-pdf: install-pdf-am
+
+install-pdf-am:
+
+install-ps: install-ps-am
+
+install-ps-am:
+
+installcheck-am:
+
+maintainer-clean: maintainer-clean-am
+ -rm -f ./$(DEPDIR)/mdbox-deleted-storage.Plo
+ -rm -f ./$(DEPDIR)/mdbox-file.Plo
+ -rm -f ./$(DEPDIR)/mdbox-mail.Plo
+ -rm -f ./$(DEPDIR)/mdbox-map.Plo
+ -rm -f ./$(DEPDIR)/mdbox-purge.Plo
+ -rm -f ./$(DEPDIR)/mdbox-save.Plo
+ -rm -f ./$(DEPDIR)/mdbox-settings.Plo
+ -rm -f ./$(DEPDIR)/mdbox-storage-rebuild.Plo
+ -rm -f ./$(DEPDIR)/mdbox-storage.Plo
+ -rm -f ./$(DEPDIR)/mdbox-sync.Plo
+ -rm -f Makefile
+maintainer-clean-am: distclean-am maintainer-clean-generic
+
+mostlyclean: mostlyclean-am
+
+mostlyclean-am: mostlyclean-compile mostlyclean-generic \
+ mostlyclean-libtool
+
+pdf: pdf-am
+
+pdf-am:
+
+ps: ps-am
+
+ps-am:
+
+uninstall-am: uninstall-pkginc_libHEADERS
+
+.MAKE: install-am install-strip
+
+.PHONY: CTAGS GTAGS TAGS all all-am am--depfiles check check-am clean \
+ clean-generic clean-libtool clean-noinstLTLIBRARIES \
+ cscopelist-am ctags ctags-am distclean distclean-compile \
+ distclean-generic distclean-libtool distclean-tags distdir dvi \
+ dvi-am html html-am info info-am install install-am \
+ install-data install-data-am install-dvi install-dvi-am \
+ install-exec install-exec-am install-html install-html-am \
+ install-info install-info-am install-man install-pdf \
+ install-pdf-am install-pkginc_libHEADERS install-ps \
+ install-ps-am install-strip installcheck installcheck-am \
+ installdirs maintainer-clean maintainer-clean-generic \
+ mostlyclean mostlyclean-compile mostlyclean-generic \
+ mostlyclean-libtool pdf pdf-am ps ps-am tags tags-am uninstall \
+ uninstall-am uninstall-pkginc_libHEADERS
+
+.PRECIOUS: Makefile
+
+
+# Tell versions [3.59,3.63) of GNU make to not export all variables.
+# Otherwise a system limit (for SysV at least) may be exceeded.
+.NOEXPORT:
diff --git a/src/lib-storage/index/dbox-multi/mdbox-deleted-storage.c b/src/lib-storage/index/dbox-multi/mdbox-deleted-storage.c
new file mode 100644
index 0000000..4fdf1ab
--- /dev/null
+++ b/src/lib-storage/index/dbox-multi/mdbox-deleted-storage.c
@@ -0,0 +1,319 @@
+/* Copyright (c) 2013-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "ioloop.h"
+#include "mkdir-parents.h"
+#include "master-service.h"
+#include "mail-index-modseq.h"
+#include "mail-index-alloc-cache.h"
+#include "mailbox-log.h"
+#include "mailbox-list-private.h"
+#include "mail-copy.h"
+#include "dbox-mail.h"
+#include "dbox-save.h"
+#include "mdbox-map.h"
+#include "mdbox-file.h"
+#include "mdbox-sync.h"
+#include "mdbox-storage-rebuild.h"
+#include "mdbox-storage.h"
+
+extern struct mail_storage mdbox_deleted_storage;
+extern struct mailbox mdbox_deleted_mailbox;
+extern struct dbox_storage_vfuncs mdbox_deleted_dbox_storage_vfuncs;
+
+static struct mail_storage *mdbox_deleted_storage_alloc(void)
+{
+ struct mdbox_storage *storage;
+ pool_t pool;
+
+ pool = pool_alloconly_create("mdbox deleted storage", 2048);
+ storage = p_new(pool, struct mdbox_storage, 1);
+ storage->storage.v = mdbox_dbox_storage_vfuncs;
+ storage->storage.storage = mdbox_deleted_storage;
+ storage->storage.storage.pool = pool;
+ return &storage->storage.storage;
+}
+
+static struct mailbox *
+mdbox_deleted_mailbox_alloc(struct mail_storage *storage,
+ struct mailbox_list *list,
+ const char *vname, enum mailbox_flags flags)
+{
+ struct mdbox_mailbox *mbox;
+ pool_t pool;
+
+ flags |= MAILBOX_FLAG_READONLY | MAILBOX_FLAG_NO_INDEX_FILES;
+
+ pool = pool_alloconly_create("mdbox deleted mailbox", 1024*3);
+ mbox = p_new(pool, struct mdbox_mailbox, 1);
+ mbox->box = mdbox_deleted_mailbox;
+ mbox->box.pool = pool;
+ mbox->box.storage = storage;
+ mbox->box.list = list;
+ mbox->box.mail_vfuncs = &mdbox_mail_vfuncs;
+
+ index_storage_mailbox_alloc(&mbox->box, vname, flags, MAIL_INDEX_PREFIX);
+
+ mbox->storage = MDBOX_STORAGE(storage);
+ return &mbox->box;
+}
+
+static int
+mdbox_deleted_mailbox_create_indexes(struct mailbox *box,
+ const struct mailbox_update *update,
+ struct mail_index_transaction *trans)
+{
+ struct mdbox_mailbox *mbox = MDBOX_MAILBOX(box);
+ struct mail_index_transaction *new_trans = NULL;
+ uint32_t uid_validity = ioloop_time;
+ uint32_t uid_next = 1;
+
+ if (update != NULL && update->uid_validity != 0)
+ uid_validity = update->uid_validity;
+
+ if (trans == NULL) {
+ new_trans = mail_index_transaction_begin(box->view, 0);
+ trans = new_trans;
+ }
+
+ mail_index_update_header(trans,
+ offsetof(struct mail_index_header, uid_validity),
+ &uid_validity, sizeof(uid_validity), TRUE);
+ mail_index_update_header(trans,
+ offsetof(struct mail_index_header, next_uid),
+ &uid_next, sizeof(uid_next), TRUE);
+ mbox->creating = TRUE;
+ mdbox_update_header(mbox, trans, update);
+ mbox->creating = FALSE;
+
+ if (new_trans != NULL) {
+ if (mail_index_transaction_commit(&new_trans) < 0) {
+ mailbox_set_index_error(box);
+ return -1;
+ }
+ }
+ return 0;
+}
+
+static const char *
+mdbox_get_attachment_path_suffix(struct dbox_file *file ATTR_UNUSED)
+{
+ return "";
+}
+
+static int
+mdbox_deleted_mailbox_get_metadata(struct mailbox *box,
+ enum mailbox_metadata_items items,
+ struct mailbox_metadata *metadata_r)
+{
+ if (index_mailbox_get_metadata(box, items, metadata_r) < 0)
+ return -1;
+
+ if ((items & MAILBOX_METADATA_GUID) != 0)
+ guid_128_generate(metadata_r->guid);
+ return 0;
+}
+
+static struct mail_save_context *
+mdbox_deleted_save_alloc(struct mailbox_transaction_context *t)
+{
+ struct mail_save_context *ctx;
+
+ ctx = i_new(struct mail_save_context, 1);
+ ctx->transaction = t;
+ return ctx;
+}
+
+static int
+mdbox_deleted_save_begin(struct mail_save_context *ctx,
+ struct istream *input ATTR_UNUSED)
+{
+ mail_storage_set_error(ctx->transaction->box->storage,
+ MAIL_ERROR_NOTPOSSIBLE, "mdbox_deleted doesn't support saving mails");
+ return -1;
+}
+
+static int
+mdbox_deleted_save_continue(struct mail_save_context *ctx ATTR_UNUSED)
+{
+ return -1;
+}
+
+static int mdbox_deleted_save_finish(struct mail_save_context *ctx)
+{
+ index_save_context_free(ctx);
+ return -1;
+}
+
+static void
+mdbox_deleted_save_cancel(struct mail_save_context *ctx)
+{
+ index_save_context_free(ctx);
+}
+
+static int mdbox_deleted_sync(struct mdbox_mailbox *mbox,
+ enum mdbox_sync_flags flags ATTR_UNUSED)
+{
+ struct mail_index_sync_ctx *index_sync_ctx;
+ struct mail_index_view *sync_view;
+ struct mail_index_transaction *trans;
+ struct mdbox_mail_index_record rec;
+ struct mdbox_map_mail_index_record map_rec;
+ enum mail_index_sync_flags sync_flags;
+ uint16_t refcount;
+ uint32_t map_seq, map_count, seq, uid = 0;
+ int ret = 0;
+
+ if (mbox->mdbox_deleted_synced) {
+ /* don't bother supporting incremental syncs */
+ return 0;
+ }
+ if (!mbox->box.inbox_user && mbox->box.name[0] != '\0') {
+ /* since mailbox list currently shows all the existing
+ mailboxes, we don't want all of them to list the deleted
+ messages. only show messages in user's INBOX or the
+ namespace prefix. */
+ return 0;
+ }
+
+ if (mdbox_map_open(mbox->storage->map) < 0)
+ return -1;
+
+ if (mdbox_deleted_mailbox_create_indexes(&mbox->box, NULL, NULL) < 0)
+ return -1;
+
+ i_zero(&rec);
+ rec.save_date = ioloop_time;
+
+ sync_flags = index_storage_get_sync_flags(&mbox->box);
+ if (mail_index_sync_begin(mbox->box.index, &index_sync_ctx,
+ &sync_view, &trans, sync_flags) < 0) {
+ mailbox_set_index_error(&mbox->box);
+ return -1;
+ }
+
+ map_count = mdbox_map_get_messages_count(mbox->storage->map);
+ for (map_seq = 1; map_seq <= map_count; map_seq++) {
+ if (mdbox_map_lookup_seq_full(mbox->storage->map, map_seq,
+ &map_rec, &refcount) < 0) {
+ ret = -1;
+ break;
+ }
+ if (refcount == 0) {
+ rec.map_uid = mdbox_map_lookup_uid(mbox->storage->map,
+ map_seq);
+ mail_index_append(trans, ++uid, &seq);
+ mail_index_update_ext(trans, seq,
+ mbox->ext_id, &rec, NULL);
+ }
+ }
+
+ if (ret < 0)
+ mail_index_sync_rollback(&index_sync_ctx);
+ else {
+ if (mail_index_sync_commit(&index_sync_ctx) < 0) {
+ mailbox_set_index_error(&mbox->box);
+ ret = -1;
+ } else {
+ mbox->mdbox_deleted_synced = TRUE;
+ }
+ }
+ return ret;
+}
+
+static struct mailbox_sync_context *
+mdbox_deleted_storage_sync_init(struct mailbox *box,
+ enum mailbox_sync_flags flags)
+{
+ struct mdbox_mailbox *mbox = MDBOX_MAILBOX(box);
+ enum mdbox_sync_flags mdbox_sync_flags = 0;
+ int ret = 0;
+
+ if (index_mailbox_want_full_sync(&mbox->box, flags) ||
+ mbox->storage->corrupted)
+ ret = mdbox_deleted_sync(mbox, mdbox_sync_flags);
+
+ return index_mailbox_sync_init(box, flags, ret < 0);
+}
+
+struct mail_storage mdbox_deleted_storage = {
+ .name = MDBOX_DELETED_STORAGE_NAME,
+ .class_flags = MAIL_STORAGE_CLASS_FLAG_UNIQUE_ROOT |
+ MAIL_STORAGE_CLASS_FLAG_HAVE_MAIL_GUIDS |
+ MAIL_STORAGE_CLASS_FLAG_HAVE_MAIL_SAVE_GUIDS |
+ MAIL_STORAGE_CLASS_FLAG_BINARY_DATA,
+
+ .v = {
+ mdbox_get_setting_parser_info,
+ mdbox_deleted_storage_alloc,
+ mdbox_storage_create,
+ mdbox_storage_destroy,
+ NULL,
+ dbox_storage_get_list_settings,
+ NULL,
+ mdbox_deleted_mailbox_alloc,
+ NULL,
+ NULL,
+ }
+};
+
+struct mailbox mdbox_deleted_mailbox = {
+ .v = {
+ index_storage_is_readonly,
+ index_storage_mailbox_enable,
+ index_storage_mailbox_exists,
+ mdbox_mailbox_open,
+ index_storage_mailbox_close,
+ index_storage_mailbox_free,
+ dbox_mailbox_create,
+ index_storage_mailbox_update,
+ index_storage_mailbox_delete,
+ index_storage_mailbox_rename,
+ index_storage_get_status,
+ mdbox_deleted_mailbox_get_metadata,
+ index_storage_set_subscribed,
+ index_storage_attribute_set,
+ index_storage_attribute_get,
+ index_storage_attribute_iter_init,
+ index_storage_attribute_iter_next,
+ index_storage_attribute_iter_deinit,
+ index_storage_list_index_has_changed,
+ index_storage_list_index_update_sync,
+ mdbox_deleted_storage_sync_init,
+ index_mailbox_sync_next,
+ index_mailbox_sync_deinit,
+ NULL,
+ dbox_notify_changes,
+ index_transaction_begin,
+ index_transaction_commit,
+ index_transaction_rollback,
+ NULL,
+ dbox_mail_alloc,
+ index_storage_search_init,
+ index_storage_search_deinit,
+ index_storage_search_next_nonblock,
+ index_storage_search_next_update_seq,
+ index_storage_search_next_match_mail,
+ mdbox_deleted_save_alloc,
+ mdbox_deleted_save_begin,
+ mdbox_deleted_save_continue,
+ mdbox_deleted_save_finish,
+ mdbox_deleted_save_cancel,
+ mail_storage_copy,
+ NULL,
+ NULL,
+ NULL,
+ index_storage_is_inconsistent
+ }
+};
+
+struct dbox_storage_vfuncs mdbox_deleted_dbox_storage_vfuncs = {
+ mdbox_file_unrefed,
+ mdbox_file_create_fd,
+ mdbox_mail_open,
+ mdbox_deleted_mailbox_create_indexes,
+ mdbox_get_attachment_path_suffix,
+ mdbox_set_mailbox_corrupted,
+ mdbox_set_file_corrupted
+};
diff --git a/src/lib-storage/index/dbox-multi/mdbox-file.c b/src/lib-storage/index/dbox-multi/mdbox-file.c
new file mode 100644
index 0000000..65138bd
--- /dev/null
+++ b/src/lib-storage/index/dbox-multi/mdbox-file.c
@@ -0,0 +1,349 @@
+/* Copyright (c) 2007-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "ioloop.h"
+#include "array.h"
+#include "hex-dec.h"
+#include "hex-binary.h"
+#include "hostpid.h"
+#include "istream.h"
+#include "ostream.h"
+#include "file-lock.h"
+#include "file-set-size.h"
+#include "mkdir-parents.h"
+#include "fdatasync-path.h"
+#include "eacces-error.h"
+#include "str.h"
+#include "mailbox-list-private.h"
+#include "mdbox-storage.h"
+#include "mdbox-map-private.h"
+#include "mdbox-file.h"
+
+#include <stdio.h>
+#include <unistd.h>
+#include <ctype.h>
+#include <fcntl.h>
+
+static struct mdbox_file *
+mdbox_find_and_move_open_file(struct mdbox_storage *storage, uint32_t file_id)
+{
+ struct mdbox_file *const *files;
+ unsigned int i, count;
+
+ files = array_get(&storage->open_files, &count);
+ for (i = 0; i < count; i++) {
+ if (files[i]->file_id == file_id)
+ return files[i];
+ }
+ return NULL;
+}
+
+void mdbox_files_free(struct mdbox_storage *storage)
+{
+ struct mdbox_file *const *files;
+ unsigned int i, count;
+
+ files = array_get(&storage->open_files, &count);
+ for (i = 0; i < count; i++)
+ dbox_file_free(&files[i]->file);
+ array_clear(&storage->open_files);
+}
+
+void mdbox_files_sync_input(struct mdbox_storage *storage)
+{
+ struct mdbox_file *const *files;
+ unsigned int i, count;
+
+ files = array_get(&storage->open_files, &count);
+ for (i = 0; i < count; i++) {
+ if (files[i]->file.input != NULL)
+ i_stream_sync(files[i]->file.input);
+ }
+}
+
+static void
+mdbox_close_open_files(struct mdbox_storage *storage, unsigned int close_count)
+{
+ struct mdbox_file *const *files;
+ unsigned int i, count;
+
+ files = array_get(&storage->open_files, &count);
+ for (i = 0; i < count;) {
+ if (files[i]->file.refcount == 0) {
+ dbox_file_free(&files[i]->file);
+ array_delete(&storage->open_files, i, 1);
+
+ if (--close_count == 0)
+ break;
+
+ files = array_get(&storage->open_files, &count);
+ } else {
+ i++;
+ }
+ }
+}
+
+static void
+mdbox_file_init_paths(struct mdbox_file *file, const char *fname, bool alt)
+{
+ i_free(file->file.primary_path);
+ i_free(file->file.alt_path);
+ file->file.primary_path =
+ i_strdup_printf("%s/%s", file->storage->storage_dir, fname);
+ if (file->storage->alt_storage_dir != NULL) {
+ file->file.alt_path =
+ i_strdup_printf("%s/%s", file->storage->alt_storage_dir,
+ fname);
+ }
+ file->file.cur_path = !alt ? file->file.primary_path :
+ file->file.alt_path;
+}
+
+static int mdbox_file_create(struct mdbox_file *file)
+{
+ struct dbox_file *_file = &file->file;
+ bool create_parents;
+ int ret;
+
+ create_parents = dbox_file_is_in_alt(_file);
+ _file->fd = _file->storage->v.
+ file_create_fd(_file, _file->cur_path, create_parents);
+ if (_file->fd == -1)
+ return -1;
+
+ if (file->storage->preallocate_space) {
+ ret = file_preallocate(_file->fd,
+ file->storage->set->mdbox_rotate_size);
+ if (ret < 0) {
+ switch (errno) {
+ case ENOSPC:
+ case EDQUOT:
+ /* ignore */
+ break;
+ default:
+ i_error("file_preallocate(%s) failed: %m",
+ _file->cur_path);
+ break;
+ }
+ } else if (ret == 0) {
+ /* not supported by filesystem, disable. */
+ file->storage->preallocate_space = FALSE;
+ }
+ }
+ return 0;
+}
+
+static struct dbox_file *
+mdbox_file_init_full(struct mdbox_storage *storage,
+ uint32_t file_id, bool alt_dir)
+{
+ struct mdbox_file *file;
+ const char *fname;
+ unsigned int count;
+
+ file = file_id == 0 ? NULL :
+ mdbox_find_and_move_open_file(storage, file_id);
+ if (file != NULL) {
+ file->file.refcount++;
+ return &file->file;
+ }
+
+ count = array_count(&storage->open_files);
+ if (count > MDBOX_MAX_OPEN_UNUSED_FILES) {
+ mdbox_close_open_files(storage,
+ count - MDBOX_MAX_OPEN_UNUSED_FILES);
+ }
+
+ file = i_new(struct mdbox_file, 1);
+ file->storage = storage;
+ file->file.storage = &storage->storage;
+ file->file_id = file_id;
+ fname = file_id == 0 ? dbox_generate_tmp_filename() :
+ t_strdup_printf(MDBOX_MAIL_FILE_FORMAT, file_id);
+ mdbox_file_init_paths(file, fname, FALSE);
+ dbox_file_init(&file->file);
+ if (alt_dir)
+ file->file.cur_path = file->file.alt_path;
+
+ if (file_id != 0)
+ array_push_back(&storage->open_files, &file);
+ else
+ (void)mdbox_file_create(file);
+ return &file->file;
+}
+
+struct dbox_file *
+mdbox_file_init(struct mdbox_storage *storage, uint32_t file_id)
+{
+ return mdbox_file_init_full(storage, file_id, FALSE);
+}
+
+struct dbox_file *
+mdbox_file_init_new_alt(struct mdbox_storage *storage)
+{
+ return mdbox_file_init_full(storage, 0, TRUE);
+}
+
+int mdbox_file_assign_file_id(struct mdbox_file *file, uint32_t file_id)
+{
+ struct stat st;
+ const char *old_path;
+ const char *new_dir, *new_fname, *new_path;
+
+ i_assert(file->file_id == 0);
+ i_assert(file_id != 0);
+
+ old_path = file->file.cur_path;
+ new_fname = t_strdup_printf(MDBOX_MAIL_FILE_FORMAT, file_id);
+ new_dir = !dbox_file_is_in_alt(&file->file) ?
+ file->storage->storage_dir : file->storage->alt_storage_dir;
+ new_path = t_strdup_printf("%s/%s", new_dir, new_fname);
+
+ if (stat(new_path, &st) == 0) {
+ mail_storage_set_critical(&file->file.storage->storage,
+ "mdbox: %s already exists, rebuilding index", new_path);
+ mdbox_storage_set_corrupted(file->storage);
+ return -1;
+ }
+ if (rename(old_path, new_path) < 0) {
+ mail_storage_set_critical(&file->storage->storage.storage,
+ "rename(%s, %s) failed: %m",
+ old_path, new_path);
+ return -1;
+ }
+ mdbox_file_init_paths(file, new_fname,
+ dbox_file_is_in_alt(&file->file));
+ file->file_id = file_id;
+ array_push_back(&file->storage->open_files, &file);
+ return 0;
+}
+
+static struct mdbox_file *
+mdbox_find_oldest_unused_file(struct mdbox_storage *storage,
+ unsigned int *idx_r)
+{
+ struct mdbox_file *const *files, *oldest_file = NULL;
+ unsigned int i, count;
+
+ files = array_get(&storage->open_files, &count);
+ *idx_r = count;
+ for (i = 0; i < count; i++) {
+ if (files[i]->file.refcount == 0) {
+ if (oldest_file == NULL ||
+ files[i]->close_time < oldest_file->close_time) {
+ oldest_file = files[i];
+ *idx_r = i;
+ }
+ }
+ }
+ return oldest_file;
+}
+
+static void mdbox_file_close_timeout(struct mdbox_storage *storage)
+{
+ struct mdbox_file *oldest;
+ unsigned int i;
+ time_t close_time = ioloop_time - MDBOX_CLOSE_UNUSED_FILES_TIMEOUT_SECS;
+
+ while ((oldest = mdbox_find_oldest_unused_file(storage, &i)) != NULL) {
+ if (oldest->close_time > close_time)
+ break;
+ array_delete(&storage->open_files, i, 1);
+ dbox_file_free(&oldest->file);
+ }
+
+ if (oldest == NULL)
+ timeout_remove(&storage->to_close_unused_files);
+}
+
+static void mdbox_file_close_later(struct mdbox_file *mfile)
+{
+ if (mfile->storage->to_close_unused_files == NULL) {
+ mfile->storage->to_close_unused_files =
+ timeout_add(MDBOX_CLOSE_UNUSED_FILES_TIMEOUT_SECS*1000,
+ mdbox_file_close_timeout, mfile->storage);
+ }
+}
+
+void mdbox_file_unrefed(struct dbox_file *file)
+{
+ struct mdbox_file *mfile = (struct mdbox_file *)file;
+ struct mdbox_file *oldest_file;
+ unsigned int i, count;
+
+ /* don't cache metadata seeks while file isn't being referenced */
+ file->metadata_read_offset = UOFF_T_MAX;
+ mfile->close_time = ioloop_time;
+
+ if (mfile->file_id != 0) {
+ count = array_count(&mfile->storage->open_files);
+ if (count <= MDBOX_MAX_OPEN_UNUSED_FILES) {
+ /* we can leave this file open for now */
+ mdbox_file_close_later(mfile);
+ return;
+ }
+
+ /* close the oldest file with refcount=0 */
+ oldest_file = mdbox_find_oldest_unused_file(mfile->storage, &i);
+ i_assert(oldest_file != NULL);
+ array_delete(&mfile->storage->open_files, i, 1);
+ if (oldest_file != mfile) {
+ dbox_file_free(&oldest_file->file);
+ mdbox_file_close_later(mfile);
+ return;
+ }
+ /* have to close ourself */
+ }
+ dbox_file_free(file);
+}
+
+int mdbox_file_create_fd(struct dbox_file *file, const char *path, bool parents)
+{
+ struct mdbox_file *mfile = (struct mdbox_file *)file;
+ struct mdbox_map *map = mfile->storage->map;
+ struct mailbox_permissions perm;
+ mode_t old_mask;
+ const char *p, *dir;
+ int fd;
+
+ mailbox_list_get_root_permissions(map->root_list, &perm);
+
+ old_mask = umask(0666 & ~perm.file_create_mode);
+ fd = open(path, O_RDWR | O_CREAT | O_TRUNC, 0666);
+ umask(old_mask);
+ if (fd == -1 && errno == ENOENT && parents &&
+ (p = strrchr(path, '/')) != NULL) {
+ dir = t_strdup_until(path, p);
+ if (mailbox_list_mkdir_root(map->root_list, dir,
+ path != file->alt_path ?
+ MAILBOX_LIST_PATH_TYPE_DIR :
+ MAILBOX_LIST_PATH_TYPE_ALT_DIR) < 0) {
+ mail_storage_copy_list_error(&file->storage->storage,
+ map->root_list);
+ return -1;
+ }
+ /* try again */
+ old_mask = umask(0666 & ~perm.file_create_mode);
+ fd = open(path, O_RDWR | O_CREAT | O_TRUNC, 0666);
+ umask(old_mask);
+ }
+ if (fd == -1) {
+ mail_storage_set_critical(&file->storage->storage,
+ "open(%s, O_CREAT) failed: %m", path);
+ } else if (perm.file_create_gid == (gid_t)-1) {
+ /* no group change */
+ } else if (fchown(fd, (uid_t)-1, perm.file_create_gid) < 0) {
+ if (errno == EPERM) {
+ mail_storage_set_critical(&file->storage->storage, "%s",
+ eperm_error_get_chgrp("fchown", path,
+ perm.file_create_gid,
+ perm.file_create_gid_origin));
+ } else {
+ mail_storage_set_critical(&file->storage->storage,
+ "fchown(%s, -1, %ld) failed: %m",
+ path, (long)perm.file_create_gid);
+ }
+ /* continue anyway */
+ }
+ return fd;
+}
diff --git a/src/lib-storage/index/dbox-multi/mdbox-file.h b/src/lib-storage/index/dbox-multi/mdbox-file.h
new file mode 100644
index 0000000..9095cce
--- /dev/null
+++ b/src/lib-storage/index/dbox-multi/mdbox-file.h
@@ -0,0 +1,29 @@
+#ifndef MDBOX_FILE_H
+#define MDBOX_FILE_H
+
+#include "dbox-file.h"
+
+struct mdbox_file {
+ struct dbox_file file;
+ struct mdbox_storage *storage;
+
+ uint32_t file_id;
+ time_t close_time;
+};
+
+struct dbox_file *
+mdbox_file_init(struct mdbox_storage *storage, uint32_t file_id);
+struct dbox_file *
+mdbox_file_init_new_alt(struct mdbox_storage *storage);
+
+/* Assign file ID for a newly created file. */
+int mdbox_file_assign_file_id(struct mdbox_file *file, uint32_t file_id);
+
+void mdbox_file_unrefed(struct dbox_file *file);
+int mdbox_file_create_fd(struct dbox_file *file, const char *path,
+ bool parents);
+
+void mdbox_files_free(struct mdbox_storage *storage);
+void mdbox_files_sync_input(struct mdbox_storage *storage);
+
+#endif
diff --git a/src/lib-storage/index/dbox-multi/mdbox-mail.c b/src/lib-storage/index/dbox-multi/mdbox-mail.c
new file mode 100644
index 0000000..ed3fe06
--- /dev/null
+++ b/src/lib-storage/index/dbox-multi/mdbox-mail.c
@@ -0,0 +1,265 @@
+/* Copyright (c) 2007-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "ioloop.h"
+#include "istream.h"
+#include "str.h"
+#include "index-mail.h"
+#include "dbox-mail.h"
+#include "mdbox-storage.h"
+#include "mdbox-sync.h"
+#include "mdbox-map.h"
+#include "mdbox-file.h"
+
+#include <sys/stat.h>
+
+int mdbox_mail_lookup(struct mdbox_mailbox *mbox, struct mail_index_view *view,
+ uint32_t seq, uint32_t *map_uid_r)
+{
+ const struct mdbox_mail_index_record *dbox_rec;
+ struct mdbox_index_header hdr;
+ const void *data;
+ uint32_t uid, cur_map_uid_validity;
+ bool need_resize;
+
+ mail_index_lookup_ext(view, seq, mbox->ext_id, &data, NULL);
+ dbox_rec = data;
+ if (dbox_rec == NULL || dbox_rec->map_uid == 0) {
+ mail_index_lookup_uid(view, seq, &uid);
+ mailbox_set_critical(&mbox->box,
+ "mdbox: map uid lost for uid %u", uid);
+ mdbox_storage_set_corrupted(mbox->storage);
+ return -1;
+ }
+
+ if (mbox->map_uid_validity == 0) {
+ if (mdbox_read_header(mbox, &hdr, &need_resize) < 0)
+ return -1;
+ mbox->map_uid_validity = hdr.map_uid_validity;
+ }
+ if (mdbox_map_open_or_create(mbox->storage->map) < 0)
+ return -1;
+
+ cur_map_uid_validity = mdbox_map_get_uid_validity(mbox->storage->map);
+ if (cur_map_uid_validity != mbox->map_uid_validity) {
+ mailbox_set_critical(&mbox->box,
+ "mdbox: map uidvalidity mismatch (%u vs %u)",
+ mbox->map_uid_validity, cur_map_uid_validity);
+ mdbox_storage_set_corrupted(mbox->storage);
+ return -1;
+ }
+ *map_uid_r = dbox_rec->map_uid;
+ return 0;
+}
+
+static void dbox_mail_set_expunged(struct dbox_mail *mail, uint32_t map_uid)
+{
+ struct mail *_mail = &mail->imail.mail.mail;
+ struct mdbox_mailbox *mbox = MDBOX_MAILBOX(_mail->box);
+
+ mail_index_refresh(_mail->box->index);
+ if (mail_index_is_expunged(_mail->transaction->view, _mail->seq)) {
+ mail_set_expunged(_mail);
+ return;
+ }
+
+ mdbox_map_set_corrupted(mbox->storage->map,
+ "Unexpectedly lost %s uid=%u map_uid=%u",
+ mailbox_get_vname(_mail->box),
+ _mail->uid, map_uid);
+}
+
+static int dbox_mail_open_init(struct dbox_mail *mail, uint32_t map_uid)
+{
+ struct mdbox_mailbox *mbox = MDBOX_MAILBOX(mail->imail.mail.mail.box);
+ uint32_t file_id;
+ int ret;
+
+ if ((ret = mdbox_map_lookup(mbox->storage->map, map_uid,
+ &file_id, &mail->offset)) <= 0) {
+ if (ret < 0)
+ return -1;
+
+ /* map_uid doesn't exist anymore. either it
+ got just expunged or the map index is
+ corrupted. */
+ dbox_mail_set_expunged(mail, map_uid);
+ return -1;
+ } else {
+ mail->open_file = mdbox_file_init(mbox->storage, file_id);
+ }
+ return 0;
+}
+
+int mdbox_mail_open(struct dbox_mail *mail, uoff_t *offset_r,
+ struct dbox_file **file_r)
+{
+ struct mail *_mail = &mail->imail.mail.mail;
+ struct mdbox_mailbox *mbox = MDBOX_MAILBOX(_mail->box);
+ uint32_t prev_file_id = 0, map_uid = 0;
+ bool deleted;
+
+ if (!mail_stream_access_start(_mail))
+ return -1;
+
+ do {
+ if (mail->open_file != NULL) {
+ /* already open */
+ } else if (!_mail->saving) {
+ if (mdbox_mail_lookup(mbox, _mail->transaction->view,
+ _mail->seq, &map_uid) < 0)
+ return -1;
+ if (dbox_mail_open_init(mail, map_uid) < 0)
+ return -1;
+ } else {
+ /* mail is being saved in this transaction */
+ mail->open_file =
+ mdbox_save_file_get_file(_mail->transaction,
+ _mail->seq,
+ &mail->offset);
+ }
+
+ if (!dbox_file_is_open(mail->open_file))
+ _mail->transaction->stats.open_lookup_count++;
+ if (dbox_file_open(mail->open_file, &deleted) <= 0)
+ return -1;
+ if (deleted) {
+ /* either it's expunged now or moved to another file. */
+ struct mdbox_file *mfile =
+ (struct mdbox_file *)mail->open_file;
+
+ if (mfile->file_id == prev_file_id) {
+ dbox_mail_set_expunged(mail, map_uid);
+ return -1;
+ }
+ prev_file_id = mfile->file_id;
+ if (mdbox_map_refresh(mbox->storage->map) < 0)
+ return -1;
+ dbox_file_unref(&mail->open_file);
+ }
+ } while (mail->open_file == NULL);
+
+ *file_r = mail->open_file;
+ *offset_r = mail->offset;
+ return 0;
+}
+
+static int mdbox_mail_get_save_date(struct mail *mail, time_t *date_r)
+{
+ struct mdbox_mailbox *mbox = MDBOX_MAILBOX(mail->transaction->box);
+ const struct mdbox_mail_index_record *dbox_rec;
+ const void *data;
+
+ mail_index_lookup_ext(mail->transaction->view, mail->seq,
+ mbox->ext_id, &data, NULL);
+ dbox_rec = data;
+ if (dbox_rec == NULL || dbox_rec->map_uid == 0) {
+ /* lost for some reason, use fallback */
+ return dbox_mail_get_save_date(mail, date_r);
+ }
+
+ *date_r = dbox_rec->save_date;
+ return 1;
+}
+
+static int
+mdbox_mail_get_special(struct mail *_mail, enum mail_fetch_field field,
+ const char **value_r)
+{
+ struct dbox_mail *mail = DBOX_MAIL(_mail);
+ struct mdbox_mailbox *mbox = MDBOX_MAILBOX(_mail->transaction->box);
+ struct mdbox_map_mail_index_record rec;
+ uint32_t map_uid;
+ uint16_t refcount;
+
+ switch (field) {
+ case MAIL_FETCH_REFCOUNT:
+ if (mdbox_mail_lookup(mbox, _mail->transaction->view,
+ _mail->seq, &map_uid) < 0)
+ return -1;
+ if (mdbox_map_lookup_full(mbox->storage->map, map_uid,
+ &rec, &refcount) < 0)
+ return -1;
+ *value_r = p_strdup_printf(mail->imail.mail.data_pool, "%u",
+ refcount);
+ return 0;
+ case MAIL_FETCH_REFCOUNT_ID:
+ if (mdbox_mail_lookup(mbox, _mail->transaction->view,
+ _mail->seq, &map_uid) < 0)
+ return -1;
+ *value_r = p_strdup_printf(mail->imail.mail.data_pool, "%u",
+ map_uid);
+ return 0;
+ case MAIL_FETCH_UIDL_BACKEND:
+ if (!dbox_header_have_flag(&mbox->box, mbox->hdr_ext_id,
+ offsetof(struct mdbox_index_header, flags),
+ DBOX_INDEX_HEADER_FLAG_HAVE_POP3_UIDLS)) {
+ *value_r = "";
+ return 0;
+ }
+ break;
+ case MAIL_FETCH_POP3_ORDER:
+ if (!dbox_header_have_flag(&mbox->box, mbox->hdr_ext_id,
+ offsetof(struct mdbox_index_header, flags),
+ DBOX_INDEX_HEADER_FLAG_HAVE_POP3_ORDERS)) {
+ *value_r = "";
+ return 0;
+ }
+ break;
+ default:
+ break;
+ }
+ return dbox_mail_get_special(_mail, field, value_r);
+}
+
+static void
+mdbox_mail_update_flags(struct mail *mail, enum modify_type modify_type,
+ enum mail_flags flags)
+{
+ if ((flags & DBOX_INDEX_FLAG_ALT) != 0) {
+ mdbox_purge_alt_flag_change(mail, modify_type != MODIFY_REMOVE);
+ flags &= ENUM_NEGATE(DBOX_INDEX_FLAG_ALT);
+ if (flags == 0 && modify_type != MODIFY_REPLACE)
+ return;
+ }
+
+ index_mail_update_flags(mail, modify_type, flags);
+}
+
+struct mail_vfuncs mdbox_mail_vfuncs = {
+ dbox_mail_close,
+ index_mail_free,
+ index_mail_set_seq,
+ index_mail_set_uid,
+ index_mail_set_uid_cache_updates,
+ index_mail_prefetch,
+ index_mail_precache,
+ index_mail_add_temp_wanted_fields,
+
+ index_mail_get_flags,
+ index_mail_get_keywords,
+ index_mail_get_keyword_indexes,
+ index_mail_get_modseq,
+ index_mail_get_pvt_modseq,
+ index_mail_get_parts,
+ index_mail_get_date,
+ dbox_mail_get_received_date,
+ mdbox_mail_get_save_date,
+ dbox_mail_get_virtual_size,
+ dbox_mail_get_physical_size,
+ index_mail_get_first_header,
+ index_mail_get_headers,
+ index_mail_get_header_stream,
+ dbox_mail_get_stream,
+ index_mail_get_binary_stream,
+ mdbox_mail_get_special,
+ index_mail_get_backend_mail,
+ mdbox_mail_update_flags,
+ index_mail_update_keywords,
+ index_mail_update_modseq,
+ index_mail_update_pvt_modseq,
+ NULL,
+ index_mail_expunge,
+ index_mail_set_cache_corrupted,
+ index_mail_opened,
+};
diff --git a/src/lib-storage/index/dbox-multi/mdbox-map-private.h b/src/lib-storage/index/dbox-multi/mdbox-map-private.h
new file mode 100644
index 0000000..259d854
--- /dev/null
+++ b/src/lib-storage/index/dbox-multi/mdbox-map-private.h
@@ -0,0 +1,64 @@
+#ifndef MDBOX_MAP_PRIVATE_H
+#define MDBOX_MAP_PRIVATE_H
+
+#include "mdbox-map.h"
+
+struct dbox_mail_lookup_rec {
+ uint32_t map_uid;
+ uint16_t refcount;
+ struct mdbox_map_mail_index_record rec;
+};
+
+struct mdbox_map {
+ struct mdbox_storage *storage;
+ const struct mdbox_settings *set;
+ char *path, *index_path;
+
+ struct mail_index *index;
+ struct mail_index_view *view;
+
+ uint32_t map_ext_id, ref_ext_id;
+
+ struct mailbox_list *root_list;
+
+ bool verify_existing_file_ids:1;
+};
+
+struct mdbox_map_append {
+ struct dbox_file_append_context *file_append;
+ uoff_t offset, size;
+};
+
+struct mdbox_map_append_context {
+ struct mdbox_map *map;
+ struct mdbox_map_atomic_context *atomic;
+ struct mail_index_transaction *trans;
+
+ ARRAY(struct dbox_file_append_context *) file_appends;
+ ARRAY(struct dbox_file *) files;
+ ARRAY(struct mdbox_map_append) appends;
+
+ uint32_t first_new_file_id;
+
+ unsigned int files_nonappendable_count;
+
+ bool failed:1;
+};
+
+struct mdbox_map_atomic_context {
+ struct mdbox_map *map;
+ struct mail_index_transaction *sync_trans;
+ struct mail_index_sync_ctx *sync_ctx;
+ struct mail_index_view *sync_view;
+
+ bool map_refreshed:1;
+ bool locked:1;
+ bool success:1;
+ bool failed:1;
+};
+
+int mdbox_map_view_lookup_rec(struct mdbox_map *map,
+ struct mail_index_view *view, uint32_t seq,
+ struct dbox_mail_lookup_rec *rec_r);
+
+#endif
diff --git a/src/lib-storage/index/dbox-multi/mdbox-map.c b/src/lib-storage/index/dbox-multi/mdbox-map.c
new file mode 100644
index 0000000..513db02
--- /dev/null
+++ b/src/lib-storage/index/dbox-multi/mdbox-map.c
@@ -0,0 +1,1502 @@
+/* Copyright (c) 2007-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "hash.h"
+#include "ostream.h"
+#include "mkdir-parents.h"
+#include "unlink-old-files.h"
+#include "mailbox-list-private.h"
+#include "mdbox-storage.h"
+#include "mdbox-file.h"
+#include "mdbox-map-private.h"
+
+#include <dirent.h>
+
+#define MAX_BACKWARDS_LOOKUPS 10
+
+#define DBOX_FORCE_PURGE_MIN_BYTES (1024*1024*10)
+#define DBOX_FORCE_PURGE_MIN_RATIO 0.5
+
+#define MAP_STORAGE(map) (&(map)->storage->storage.storage)
+
+struct mdbox_map_transaction_context {
+ struct mdbox_map_atomic_context *atomic;
+ struct mail_index_transaction *trans;
+
+ bool changed:1;
+ bool committed:1;
+};
+
+static int mdbox_map_generate_uid_validity(struct mdbox_map *map);
+
+void mdbox_map_set_corrupted(struct mdbox_map *map, const char *format, ...)
+{
+ va_list args;
+
+ va_start(args, format);
+ mail_storage_set_critical(MAP_STORAGE(map),
+ "mdbox map %s corrupted: %s",
+ map->index->filepath,
+ t_strdup_vprintf(format, args));
+ va_end(args);
+
+ mdbox_storage_set_corrupted(map->storage);
+}
+
+struct mdbox_map *
+mdbox_map_init(struct mdbox_storage *storage, struct mailbox_list *root_list)
+{
+ struct mdbox_map *map;
+ const char *root, *index_root;
+
+ root = mailbox_list_get_root_forced(root_list, MAILBOX_LIST_PATH_TYPE_DIR);
+ index_root = mailbox_list_get_root_forced(root_list, MAILBOX_LIST_PATH_TYPE_INDEX);
+
+ map = i_new(struct mdbox_map, 1);
+ map->storage = storage;
+ map->set = storage->set;
+ map->path = i_strconcat(root, "/"MDBOX_GLOBAL_DIR_NAME, NULL);
+ map->index_path =
+ i_strconcat(index_root, "/"MDBOX_GLOBAL_DIR_NAME, NULL);
+ map->index = mail_index_alloc(storage->storage.storage.event,
+ map->index_path,
+ MDBOX_GLOBAL_INDEX_PREFIX);
+ mail_index_set_fsync_mode(map->index,
+ MAP_STORAGE(map)->set->parsed_fsync_mode, 0);
+ mail_index_set_lock_method(map->index,
+ MAP_STORAGE(map)->set->parsed_lock_method,
+ mail_storage_get_lock_timeout(MAP_STORAGE(map), UINT_MAX));
+ map->root_list = root_list;
+ map->map_ext_id = mail_index_ext_register(map->index, "map",
+ sizeof(struct mdbox_map_mail_index_header),
+ sizeof(struct mdbox_map_mail_index_record),
+ sizeof(uint32_t));
+ map->ref_ext_id = mail_index_ext_register(map->index, "ref", 0,
+ sizeof(uint16_t), sizeof(uint16_t));
+ return map;
+}
+
+void mdbox_map_deinit(struct mdbox_map **_map)
+{
+ struct mdbox_map *map = *_map;
+
+ *_map = NULL;
+
+ if (map->view != NULL) {
+ mail_index_view_close(&map->view);
+ mail_index_close(map->index);
+ }
+ mail_index_free(&map->index);
+ i_free(map->index_path);
+ i_free(map->path);
+ i_free(map);
+}
+
+static int mdbox_map_mkdir_storage(struct mdbox_map *map)
+{
+ if (mailbox_list_mkdir_root(map->root_list, map->path,
+ MAILBOX_LIST_PATH_TYPE_DIR) < 0) {
+ mail_storage_copy_list_error(MAP_STORAGE(map), map->root_list);
+ return -1;
+ }
+
+ if (strcmp(map->path, map->index_path) != 0 &&
+ mailbox_list_mkdir_root(map->root_list, map->index_path,
+ MAILBOX_LIST_PATH_TYPE_INDEX) < 0) {
+ mail_storage_copy_list_error(MAP_STORAGE(map), map->root_list);
+ return -1;
+ }
+ return 0;
+}
+
+static void mdbox_map_cleanup(struct mdbox_map *map)
+{
+ unsigned int interval =
+ MAP_STORAGE(map)->set->mail_temp_scan_interval;
+ struct stat st;
+
+ if (stat(map->path, &st) < 0)
+ return;
+
+ /* check once in a while if there are temp files to clean up */
+ if (interval == 0) {
+ /* disabled */
+ } else if (st.st_atime > st.st_ctime + DBOX_TMP_DELETE_SECS) {
+ /* there haven't been any changes to this directory since we
+ last checked it. */
+ } else if (st.st_atime < ioloop_time - (time_t)interval) {
+ /* time to scan */
+ (void)unlink_old_files(map->path, DBOX_TEMP_FILE_PREFIX,
+ ioloop_time - DBOX_TMP_DELETE_SECS);
+ }
+}
+
+static int mdbox_map_open_internal(struct mdbox_map *map, bool create_missing)
+{
+ enum mail_index_open_flags open_flags;
+ struct mailbox_permissions perm;
+ int ret = 0;
+
+ if (map->view != NULL) {
+ /* already opened */
+ return 1;
+ }
+
+ mailbox_list_get_root_permissions(map->root_list, &perm);
+ mail_index_set_permissions(map->index, perm.file_create_mode,
+ perm.file_create_gid,
+ perm.file_create_gid_origin);
+
+ open_flags = MAIL_INDEX_OPEN_FLAG_NEVER_IN_MEMORY |
+ mail_storage_settings_to_index_flags(MAP_STORAGE(map)->set);
+ if (create_missing) {
+ if ((ret = mdbox_map_mkdir_storage(map)) < 0)
+ return -1;
+ if (ret > 0) {
+ /* storage/ directory already existed.
+ the index should exist also. */
+ } else {
+ open_flags |= MAIL_INDEX_OPEN_FLAG_CREATE;
+ }
+ }
+ ret = mail_index_open(map->index, open_flags);
+ if (ret == 0 && create_missing) {
+ /* storage/ already existed, but indexes didn't. we'll need to
+ take extra steps to make sure we won't overwrite any m.*
+ files that may already exist. */
+ map->verify_existing_file_ids = TRUE;
+ open_flags |= MAIL_INDEX_OPEN_FLAG_CREATE;
+ ret = mail_index_open(map->index, open_flags);
+ }
+ if (ret < 0) {
+ mail_storage_set_index_error(MAP_STORAGE(map), map->index);
+ return -1;
+ }
+ if (ret == 0) {
+ /* index not found - for now just return failure */
+ i_assert(!create_missing);
+ return 0;
+ }
+
+ map->view = mail_index_view_open(map->index);
+ mdbox_map_cleanup(map);
+
+ if (mail_index_get_header(map->view)->uid_validity == 0) {
+ if (mdbox_map_generate_uid_validity(map) < 0) {
+ mail_storage_set_index_error(MAP_STORAGE(map), map->index);
+ mail_index_close(map->index);
+ return -1;
+ }
+ if (mdbox_map_refresh(map) < 0) {
+ mail_index_close(map->index);
+ return -1;
+ }
+ }
+ return 1;
+}
+
+int mdbox_map_open(struct mdbox_map *map)
+{
+ return mdbox_map_open_internal(map, FALSE);
+}
+
+int mdbox_map_open_or_create(struct mdbox_map *map)
+{
+ return mdbox_map_open_internal(map, TRUE) <= 0 ? -1 : 0;
+}
+
+int mdbox_map_refresh(struct mdbox_map *map)
+{
+ struct mail_index_view_sync_ctx *ctx;
+ bool delayed_expunges, fscked;
+ int ret = 0;
+
+ /* some open files may have read partially written mails. now that
+ map syncing makes the new mails visible, we need to make sure the
+ partial data is flushed out of memory */
+ mdbox_files_sync_input(map->storage);
+
+ if (mail_index_refresh(map->view->index) < 0) {
+ mail_storage_set_index_error(MAP_STORAGE(map), map->index);
+ return -1;
+ }
+ if (mail_index_view_have_transactions(map->view)) {
+ /* can't sync when there are transactions */
+ return 0;
+ }
+
+ ctx = mail_index_view_sync_begin(map->view,
+ MAIL_INDEX_VIEW_SYNC_FLAG_FIX_INCONSISTENT);
+ fscked = mail_index_reset_fscked(map->view->index);
+ if (mail_index_view_sync_commit(&ctx, &delayed_expunges) < 0) {
+ mail_storage_set_index_error(MAP_STORAGE(map), map->index);
+ ret = -1;
+ }
+ if (fscked)
+ mdbox_storage_set_corrupted(map->storage);
+ return ret;
+}
+
+bool mdbox_map_is_fscked(struct mdbox_map *map)
+{
+ const struct mail_index_header *hdr;
+
+ if (map->view == NULL) {
+ /* map isn't opened yet. don't bother. */
+ return FALSE;
+ }
+
+ hdr = mail_index_get_header(map->view);
+ return (hdr->flags & MAIL_INDEX_HDR_FLAG_FSCKD) != 0;
+}
+
+static void
+mdbox_map_get_ext_hdr(struct mdbox_map *map, struct mail_index_view *view,
+ struct mdbox_map_mail_index_header *hdr_r)
+{
+ const void *data;
+ size_t data_size;
+
+ mail_index_get_header_ext(view, map->map_ext_id, &data, &data_size);
+ i_zero(hdr_r);
+ memcpy(hdr_r, data, I_MIN(data_size, sizeof(*hdr_r)));
+}
+
+uint32_t mdbox_map_get_rebuild_count(struct mdbox_map *map)
+{
+ struct mdbox_map_mail_index_header hdr;
+
+ mdbox_map_get_ext_hdr(map, map->view, &hdr);
+ return hdr.rebuild_count;
+}
+
+static int
+mdbox_map_lookup_seq(struct mdbox_map *map, uint32_t seq,
+ const struct mdbox_map_mail_index_record **rec_r)
+{
+ const struct mdbox_map_mail_index_record *rec;
+ const void *data;
+ uint32_t uid;
+
+ mail_index_lookup_ext(map->view, seq, map->map_ext_id, &data, NULL);
+ rec = data;
+
+ if (rec == NULL || rec->file_id == 0) {
+ mail_index_lookup_uid(map->view, seq, &uid);
+ mdbox_map_set_corrupted(map, "file_id=0 for map_uid=%u", uid);
+ return -1;
+ }
+ *rec_r = rec;
+ return 0;
+}
+
+static int
+mdbox_map_get_seq(struct mdbox_map *map, uint32_t map_uid, uint32_t *seq_r)
+{
+ if (!mail_index_lookup_seq(map->view, map_uid, seq_r)) {
+ /* not found - try again after a refresh */
+ if (mdbox_map_refresh(map) < 0)
+ return -1;
+ if (!mail_index_lookup_seq(map->view, map_uid, seq_r))
+ return 0;
+ }
+ return 1;
+}
+
+int mdbox_map_lookup(struct mdbox_map *map, uint32_t map_uid,
+ uint32_t *file_id_r, uoff_t *offset_r)
+{
+ const struct mdbox_map_mail_index_record *rec;
+ uint32_t seq;
+ int ret;
+
+ if (mdbox_map_open_or_create(map) < 0)
+ return -1;
+
+ if ((ret = mdbox_map_get_seq(map, map_uid, &seq)) <= 0)
+ return ret;
+
+ if (mdbox_map_lookup_seq(map, seq, &rec) < 0)
+ return -1;
+ *file_id_r = rec->file_id;
+ *offset_r = rec->offset;
+ return 1;
+}
+
+int mdbox_map_lookup_full(struct mdbox_map *map, uint32_t map_uid,
+ struct mdbox_map_mail_index_record *rec_r,
+ uint16_t *refcount_r)
+{
+ uint32_t seq;
+ int ret;
+
+ if (mdbox_map_open_or_create(map) < 0)
+ return -1;
+
+ if ((ret = mdbox_map_get_seq(map, map_uid, &seq)) <= 0)
+ return ret;
+
+ return mdbox_map_lookup_seq_full(map, seq, rec_r, refcount_r);
+}
+
+int mdbox_map_lookup_seq_full(struct mdbox_map *map, uint32_t seq,
+ struct mdbox_map_mail_index_record *rec_r,
+ uint16_t *refcount_r)
+{
+ const struct mdbox_map_mail_index_record *rec;
+ const uint16_t *ref16_p;
+ const void *data;
+
+ if (mdbox_map_lookup_seq(map, seq, &rec) < 0)
+ return -1;
+ *rec_r = *rec;
+
+ mail_index_lookup_ext(map->view, seq, map->ref_ext_id, &data, NULL);
+ if (data == NULL) {
+ mdbox_map_set_corrupted(map, "missing ref extension");
+ return -1;
+ }
+ ref16_p = data;
+ *refcount_r = *ref16_p;
+ return 1;
+}
+
+uint32_t mdbox_map_lookup_uid(struct mdbox_map *map, uint32_t seq)
+{
+ uint32_t uid;
+
+ mail_index_lookup_uid(map->view, seq, &uid);
+ return uid;
+}
+
+unsigned int mdbox_map_get_messages_count(struct mdbox_map *map)
+{
+ return mail_index_view_get_messages_count(map->view);
+}
+
+int mdbox_map_view_lookup_rec(struct mdbox_map *map,
+ struct mail_index_view *view, uint32_t seq,
+ struct dbox_mail_lookup_rec *rec_r)
+{
+ const uint16_t *ref16_p;
+ const void *data;
+
+ i_zero(rec_r);
+ mail_index_lookup_uid(view, seq, &rec_r->map_uid);
+
+ mail_index_lookup_ext(view, seq, map->map_ext_id, &data, NULL);
+ if (data == NULL) {
+ mdbox_map_set_corrupted(map, "missing map extension");
+ return -1;
+ }
+ memcpy(&rec_r->rec, data, sizeof(rec_r->rec));
+
+ mail_index_lookup_ext(view, seq, map->ref_ext_id, &data, NULL);
+ if (data == NULL) {
+ mdbox_map_set_corrupted(map, "missing ref extension");
+ return -1;
+ }
+ ref16_p = data;
+ rec_r->refcount = *ref16_p;
+ return 0;
+}
+
+int mdbox_map_get_file_msgs(struct mdbox_map *map, uint32_t file_id,
+ ARRAY_TYPE(mdbox_map_file_msg) *recs)
+{
+ const struct mail_index_header *hdr;
+ struct dbox_mail_lookup_rec rec;
+ struct mdbox_map_file_msg msg;
+ uint32_t seq;
+
+ if (mdbox_map_refresh(map) < 0)
+ return -1;
+ hdr = mail_index_get_header(map->view);
+
+ i_zero(&msg);
+ for (seq = 1; seq <= hdr->messages_count; seq++) {
+ if (mdbox_map_view_lookup_rec(map, map->view, seq, &rec) < 0)
+ return -1;
+
+ if (rec.rec.file_id == file_id) {
+ msg.map_uid = rec.map_uid;
+ msg.offset = rec.rec.offset;
+ msg.refcount = rec.refcount;
+ array_push_back(recs, &msg);
+ }
+ }
+ return 0;
+}
+
+int mdbox_map_get_zero_ref_files(struct mdbox_map *map,
+ ARRAY_TYPE(seq_range) *file_ids_r)
+{
+ const struct mail_index_header *hdr;
+ const struct mdbox_map_mail_index_record *rec;
+ const uint16_t *ref16_p;
+ const void *data;
+ uint32_t seq;
+ bool expunged;
+ int ret;
+
+ if ((ret = mdbox_map_open(map)) <= 0) {
+ /* no map / internal error */
+ return ret;
+ }
+ if (mdbox_map_refresh(map) < 0)
+ return -1;
+
+ hdr = mail_index_get_header(map->view);
+ for (seq = 1; seq <= hdr->messages_count; seq++) {
+ mail_index_lookup_ext(map->view, seq, map->ref_ext_id,
+ &data, &expunged);
+ if (data != NULL && !expunged) {
+ ref16_p = data;
+ if (*ref16_p != 0)
+ continue;
+ }
+
+ mail_index_lookup_ext(map->view, seq, map->map_ext_id,
+ &data, &expunged);
+ if (data != NULL && !expunged) {
+ rec = data;
+ seq_range_array_add(file_ids_r, rec->file_id);
+ }
+ }
+ return 0;
+}
+
+struct mdbox_map_atomic_context *mdbox_map_atomic_begin(struct mdbox_map *map)
+{
+ struct mdbox_map_atomic_context *atomic;
+
+ atomic = i_new(struct mdbox_map_atomic_context, 1);
+ atomic->map = map;
+ return atomic;
+}
+
+static void
+mdbox_map_sync_handle(struct mdbox_map *map,
+ struct mail_index_sync_ctx *sync_ctx)
+{
+ struct mail_index_sync_rec sync_rec;
+ uint32_t seq1, seq2;
+ uoff_t offset1, offset2;
+
+ mail_index_sync_get_offsets(sync_ctx, &seq1, &offset1, &seq2, &offset2);
+ if (offset1 != offset2 || seq1 != seq2) {
+ /* something had crashed. need a full resync. */
+ i_warning("mdbox %s: Inconsistency in map index "
+ "(%u,%"PRIuUOFF_T" != %u,%"PRIuUOFF_T")",
+ map->path, seq1, offset1, seq2, offset2);
+ mdbox_storage_set_corrupted(map->storage);
+ }
+ while (mail_index_sync_next(sync_ctx, &sync_rec)) ;
+}
+
+int mdbox_map_atomic_lock(struct mdbox_map_atomic_context *atomic,
+ const char *reason)
+{
+ int ret;
+
+ if (atomic->locked)
+ return 0;
+
+ if (mdbox_map_open_or_create(atomic->map) < 0)
+ return -1;
+
+ /* use syncing to lock the transaction log, so that we always see
+ log's head_offset = tail_offset */
+ ret = mail_index_sync_begin(atomic->map->index, &atomic->sync_ctx,
+ &atomic->sync_view, &atomic->sync_trans,
+ MAIL_INDEX_SYNC_FLAG_UPDATE_TAIL_OFFSET);
+ if (mail_index_reset_fscked(atomic->map->index))
+ mdbox_storage_set_corrupted(atomic->map->storage);
+ if (ret <= 0) {
+ i_assert(ret != 0);
+ mail_storage_set_index_error(MAP_STORAGE(atomic->map),
+ atomic->map->index);
+ return -1;
+ }
+ mail_index_sync_set_reason(atomic->sync_ctx, reason);
+ atomic->locked = TRUE;
+ /* reset refresh state so that if it's wanted to be done locked,
+ it gets the latest changes */
+ atomic->map_refreshed = FALSE;
+ mdbox_map_sync_handle(atomic->map, atomic->sync_ctx);
+ return 0;
+}
+
+bool mdbox_map_atomic_is_locked(struct mdbox_map_atomic_context *atomic)
+{
+ return atomic->locked;
+}
+
+void mdbox_map_atomic_set_failed(struct mdbox_map_atomic_context *atomic)
+{
+ atomic->success = FALSE;
+ atomic->failed = TRUE;
+}
+
+void mdbox_map_atomic_set_success(struct mdbox_map_atomic_context *atomic)
+{
+ if (!atomic->failed)
+ atomic->success = TRUE;
+}
+
+void mdbox_map_atomic_unset_fscked(struct mdbox_map_atomic_context *atomic)
+{
+ mail_index_unset_fscked(atomic->sync_trans);
+}
+
+int mdbox_map_atomic_finish(struct mdbox_map_atomic_context **_atomic)
+{
+ struct mdbox_map_atomic_context *atomic = *_atomic;
+ int ret = 0;
+
+ *_atomic = NULL;
+
+ if (atomic->sync_ctx == NULL) {
+ /* not locked */
+ i_assert(!atomic->locked);
+ } else if (atomic->success) {
+ if (mail_index_sync_commit(&atomic->sync_ctx) < 0) {
+ mail_storage_set_index_error(MAP_STORAGE(atomic->map),
+ atomic->map->index);
+ ret = -1;
+ }
+ } else {
+ mail_index_sync_rollback(&atomic->sync_ctx);
+ }
+ i_free(atomic);
+ return ret;
+}
+
+struct mdbox_map_transaction_context *
+mdbox_map_transaction_begin(struct mdbox_map_atomic_context *atomic,
+ bool external)
+{
+ struct mdbox_map_transaction_context *ctx;
+ enum mail_index_transaction_flags flags =
+ MAIL_INDEX_TRANSACTION_FLAG_FSYNC;
+ bool success;
+
+ if (external)
+ flags |= MAIL_INDEX_TRANSACTION_FLAG_EXTERNAL;
+
+ ctx = i_new(struct mdbox_map_transaction_context, 1);
+ ctx->atomic = atomic;
+ if (atomic->locked && atomic->map_refreshed) {
+ /* already refreshed within a lock, don't do it again */
+ success = TRUE;
+ } else {
+ success = mdbox_map_open(atomic->map) > 0 &&
+ mdbox_map_refresh(atomic->map) == 0;
+ }
+
+ if (success) {
+ atomic->map_refreshed = TRUE;
+ ctx->trans = mail_index_transaction_begin(atomic->map->view,
+ flags);
+ }
+ return ctx;
+}
+
+int mdbox_map_transaction_commit(struct mdbox_map_transaction_context *ctx,
+ const char *reason)
+{
+ i_assert(!ctx->committed);
+
+ ctx->committed = TRUE;
+ if (!ctx->changed)
+ return 0;
+
+ if (mdbox_map_atomic_lock(ctx->atomic, reason) < 0)
+ return -1;
+
+ if (mail_index_transaction_commit(&ctx->trans) < 0) {
+ mail_storage_set_index_error(MAP_STORAGE(ctx->atomic->map),
+ ctx->atomic->map->index);
+ return -1;
+ }
+ mdbox_map_atomic_set_success(ctx->atomic);
+ return 0;
+}
+
+void mdbox_map_transaction_free(struct mdbox_map_transaction_context **_ctx)
+{
+ struct mdbox_map_transaction_context *ctx = *_ctx;
+
+ *_ctx = NULL;
+
+ if (ctx->trans != NULL)
+ mail_index_transaction_rollback(&ctx->trans);
+ i_free(ctx);
+}
+
+int mdbox_map_update_refcount(struct mdbox_map_transaction_context *ctx,
+ uint32_t map_uid, int diff)
+{
+ struct mdbox_map *map = ctx->atomic->map;
+ const void *data;
+ uint32_t seq;
+ int old_diff, new_diff;
+
+ if (unlikely(ctx->trans == NULL))
+ return -1;
+
+ if (!mail_index_lookup_seq(map->view, map_uid, &seq)) {
+ /* we can't refresh map here since view has a
+ transaction open. */
+ if (diff > 0) {
+ /* the message was probably just purged */
+ mail_storage_set_error(MAP_STORAGE(map), MAIL_ERROR_EXPUNGED,
+ "Some of the requested messages no longer exist.");
+ } else {
+ mdbox_map_set_corrupted(map,
+ "refcount update lost map_uid=%u", map_uid);
+ }
+ return -1;
+ }
+ mail_index_lookup_ext(map->view, seq, map->ref_ext_id, &data, NULL);
+ old_diff = data == NULL ? 0 : *((const uint16_t *)data);
+ ctx->changed = TRUE;
+ new_diff = mail_index_atomic_inc_ext(ctx->trans, seq,
+ map->ref_ext_id, diff);
+ if (old_diff + new_diff < 0) {
+ mdbox_map_set_corrupted(map, "map_uid=%u refcount too low",
+ map_uid);
+ return -1;
+ }
+ if (old_diff + new_diff >= 32768 && new_diff > 0) {
+ /* we're getting close to the 64k limit. fail early
+ to make it less likely that two processes increase
+ the refcount enough times to cross the limit */
+ mail_storage_set_error(MAP_STORAGE(map), MAIL_ERROR_LIMIT,
+ t_strdup_printf("Message has been copied too many times (%d + %d)",
+ old_diff, new_diff));
+ return -1;
+ }
+ return 0;
+}
+
+int mdbox_map_update_refcounts(struct mdbox_map_transaction_context *ctx,
+ const ARRAY_TYPE(uint32_t) *map_uids, int diff)
+{
+ const uint32_t *uidp;
+ unsigned int i, count;
+
+ if (unlikely(ctx->trans == NULL))
+ return -1;
+
+ count = array_count(map_uids);
+ for (i = 0; i < count; i++) {
+ uidp = array_idx(map_uids, i);
+ if (mdbox_map_update_refcount(ctx, *uidp, diff) < 0)
+ return -1;
+ }
+ return 0;
+}
+
+int mdbox_map_remove_file_id(struct mdbox_map *map, uint32_t file_id)
+{
+ struct mdbox_map_atomic_context *atomic;
+ struct mdbox_map_transaction_context *map_trans;
+ const struct mail_index_header *hdr;
+ const struct mdbox_map_mail_index_record *rec;
+ const void *data;
+ uint32_t seq;
+ int ret = 0;
+
+ /* make sure the map is refreshed, otherwise we might be expunging
+ messages that have already been moved to other files. */
+
+ /* we need a per-file transaction, otherwise we can't refresh the map */
+ atomic = mdbox_map_atomic_begin(map);
+ map_trans = mdbox_map_transaction_begin(atomic, TRUE);
+
+ hdr = mail_index_get_header(map->view);
+ for (seq = 1; seq <= hdr->messages_count; seq++) {
+ mail_index_lookup_ext(map->view, seq, map->map_ext_id,
+ &data, NULL);
+ if (data == NULL) {
+ mdbox_map_set_corrupted(map, "missing map extension");
+ ret = -1;
+ break;
+ }
+
+ rec = data;
+ if (rec->file_id == file_id) {
+ map_trans->changed = TRUE;
+ mail_index_expunge(map_trans->trans, seq);
+ }
+ }
+ if (ret == 0)
+ ret = mdbox_map_transaction_commit(map_trans, "removing file");
+ mdbox_map_transaction_free(&map_trans);
+ if (mdbox_map_atomic_finish(&atomic) < 0)
+ ret = -1;
+ return ret;
+}
+
+struct mdbox_map_append_context *
+mdbox_map_append_begin(struct mdbox_map_atomic_context *atomic)
+{
+ struct mdbox_map_append_context *ctx;
+
+ ctx = i_new(struct mdbox_map_append_context, 1);
+ ctx->atomic = atomic;
+ ctx->map = atomic->map;
+ ctx->first_new_file_id = (uint32_t)-1;
+ i_array_init(&ctx->file_appends, 64);
+ i_array_init(&ctx->files, 64);
+ i_array_init(&ctx->appends, 128);
+
+ if (mdbox_map_open_or_create(atomic->map) < 0)
+ ctx->failed = TRUE;
+ else {
+ /* refresh the map so we can try appending to the
+ latest files */
+ if (mdbox_map_refresh(atomic->map) == 0)
+ atomic->map_refreshed = TRUE;
+ else
+ ctx->failed = TRUE;
+ }
+ return ctx;
+}
+
+static time_t day_begin_stamp(unsigned int interval)
+{
+ struct tm tm;
+ time_t stamp;
+ unsigned int unit = 1;
+
+ if (interval == 0)
+ return 0;
+
+ /* get the beginning of day/hour/minute depending on how large
+ the interval is */
+ tm = *localtime(&ioloop_time);
+ if (interval >= 60) {
+ tm.tm_sec = 0;
+ unit = 60;
+ if (interval >= 3600) {
+ tm.tm_min = 0;
+ unit = 3600;
+ if (interval >= 3600*24) {
+ tm.tm_hour = 0;
+ unit = 3600*24;
+ }
+ }
+ }
+ stamp = mktime(&tm);
+ if (stamp == (time_t)-1)
+ i_panic("mktime(today) failed");
+
+ return stamp - (interval - unit);
+}
+
+static bool dbox_try_open(struct dbox_file *file, bool want_altpath)
+{
+ bool notfound;
+
+ if (want_altpath) {
+ if (dbox_file_open(file, &notfound) <= 0)
+ return FALSE;
+ } else {
+ if (dbox_file_open_primary(file, &notfound) <= 0)
+ return FALSE;
+ }
+ if (notfound)
+ return FALSE;
+
+ if (file->lock != NULL) {
+ /* already locked, we're possibly in the middle of purging it
+ in which case we really don't want to write there. */
+ return FALSE;
+ }
+ if (dbox_file_is_in_alt(file) != want_altpath) {
+ /* different alt location than what we want, can't use it */
+ return FALSE;
+ }
+ return TRUE;
+}
+
+static bool dbox_file_is_ok_at(struct dbox_file *file, uoff_t offset)
+{
+ bool last;
+ int ret;
+
+ if (dbox_file_seek(file, offset) == 0)
+ return FALSE;
+
+ while ((ret = dbox_file_seek_next(file, &offset, &last)) > 0);
+ if (ret == 0 && !last)
+ return FALSE;
+ return TRUE;
+}
+
+static bool
+mdbox_map_file_try_append(struct mdbox_map_append_context *ctx,
+ bool want_altpath,
+ const struct mdbox_map_mail_index_record *rec,
+ time_t stamp, uoff_t mail_size,
+ struct dbox_file_append_context **file_append_r,
+ struct ostream **output_r, bool *retry_later_r)
+{
+ struct mdbox_map *map = ctx->map;
+ struct mdbox_storage *storage = map->storage;
+ struct dbox_file *file;
+ struct dbox_file_append_context *file_append;
+ struct stat st;
+ bool file_too_old = FALSE;
+ int ret;
+
+ *file_append_r = NULL;
+ *output_r = NULL;
+ *retry_later_r = FALSE;
+
+ file = mdbox_file_init(storage, rec->file_id);
+ if (!dbox_try_open(file, want_altpath)) {
+ dbox_file_unref(&file);
+ return TRUE;
+ }
+
+ if (file->create_time < stamp)
+ file_too_old = TRUE;
+ else if ((ret = dbox_file_try_lock(file)) <= 0) {
+ /* locking failed */
+ *retry_later_r = ret == 0;
+ } else if (stat(file->cur_path, &st) < 0) {
+ if (errno != ENOENT)
+ i_error("stat(%s) failed: %m", file->cur_path);
+ /* the file was unlinked between opening and locking it. */
+ } else if (st.st_size != rec->offset + rec->size &&
+ /* check if there's any garbage at the end of file.
+ note that there may be valid messages added by another
+ session before we locked it (but after we refreshed
+ map index). */
+ !dbox_file_is_ok_at(file, rec->offset + rec->size)) {
+ /* error message was already logged */
+ } else {
+ file_append = dbox_file_append_init(file);
+ if (dbox_file_get_append_stream(file_append, output_r) <= 0) {
+ /* couldn't append to this file */
+ } else if ((*output_r)->offset + mail_size > map->set->mdbox_rotate_size) {
+ /* file was too large after all */
+ } else {
+ /* success */
+ *file_append_r = file_append;
+ return TRUE;
+ }
+ dbox_file_append_rollback(&file_append);
+ }
+
+ /* failure */
+ dbox_file_unlock(file);
+ dbox_file_unref(&file);
+ return !file_too_old;
+}
+
+static bool
+mdbox_map_is_appending(struct mdbox_map_append_context *ctx, uint32_t file_id)
+{
+ struct dbox_file_append_context *const *file_appends;
+ unsigned int i, count;
+
+ /* there shouldn't be many files open, don't bother with anything
+ faster. */
+ file_appends = array_get(&ctx->file_appends, &count);
+ for (i = 0; i < count; i++) {
+ struct mdbox_file *mfile =
+ (struct mdbox_file *)file_appends[i]->file;
+
+ if (mfile->file_id == file_id)
+ return TRUE;
+ }
+ return FALSE;
+}
+
+static struct dbox_file_append_context *
+mdbox_map_find_existing_append(struct mdbox_map_append_context *ctx,
+ uoff_t mail_size, bool want_altpath,
+ struct ostream **output_r)
+{
+ struct mdbox_map *map = ctx->map;
+ struct dbox_file_append_context *const *file_appends, *append;
+ struct mdbox_file *mfile;
+ unsigned int i, count;
+ uoff_t append_offset;
+
+ /* first try to use files already used in this append */
+ file_appends = array_get(&ctx->file_appends, &count);
+ for (i = count; i > ctx->files_nonappendable_count; i--) {
+ append = file_appends[i-1];
+
+ if (dbox_file_is_in_alt(append->file) != want_altpath)
+ continue;
+ if (append->file->fd == -1) {
+ /* already closed it (below). we might be able to still
+ fit some small mail there, but that's too much
+ trouble */
+ continue;
+ }
+
+ append_offset = append->output->offset;
+ if (append_offset + mail_size <= map->set->mdbox_rotate_size &&
+ dbox_file_get_append_stream(append, output_r) > 0)
+ return append;
+
+ /* can't append to this file anymore. if we created this file,
+ close it so we don't waste fds. if we didn't, we can't close
+ it without also losing our lock too early. */
+ mfile = (struct mdbox_file *)append->file;
+ if (mfile->file_id == 0 && dbox_file_append_flush(append) == 0)
+ dbox_file_close(append->file);
+ }
+ ctx->files_nonappendable_count = count;
+ return NULL;
+}
+
+static int
+mdbox_map_find_primary_files(struct mdbox_map_append_context *ctx,
+ ARRAY_TYPE(seq_range) *file_ids_r)
+{
+ struct mdbox_storage *dstorage = ctx->map->storage;
+ struct mail_storage *storage = &dstorage->storage.storage;
+ DIR *dir;
+ struct dirent *d;
+ uint32_t file_id;
+ int ret = 0;
+
+ /* we want to quickly find the latest alt file, but we also want to
+ avoid accessing the alt storage as much as possible. typically most
+ of the older mails would be in alt storage, so we'll just put the
+ few m.* files in primary storage to checked_file_ids array. other
+ files are then known to exist in alt storage. */
+ dir = opendir(dstorage->storage_dir);
+ if (dir == NULL) {
+ mail_storage_set_critical(storage,
+ "opendir(%s) failed: %m", dstorage->storage_dir);
+ return -1;
+ }
+
+ for (errno = 0; (d = readdir(dir)) != NULL; errno = 0) {
+ if (strncmp(d->d_name, MDBOX_MAIL_FILE_PREFIX,
+ strlen(MDBOX_MAIL_FILE_PREFIX)) != 0)
+ continue;
+ if (str_to_uint32(d->d_name + strlen(MDBOX_MAIL_FILE_PREFIX),
+ &file_id) < 0)
+ continue;
+
+ seq_range_array_add(file_ids_r, file_id);
+ }
+ if (errno != 0) {
+ mail_storage_set_critical(storage,
+ "readdir(%s) failed: %m", dstorage->storage_dir);
+ ret = -1;
+ }
+ if (closedir(dir) < 0) {
+ mail_storage_set_critical(storage,
+ "closedir(%s) failed: %m", dstorage->storage_dir);
+ ret = -1;
+ }
+ return ret;
+}
+
+static int
+mdbox_map_find_appendable_file(struct mdbox_map_append_context *ctx,
+ uoff_t mail_size, bool want_altpath,
+ struct dbox_file_append_context **file_append_r,
+ struct ostream **output_r)
+{
+ struct mdbox_map *map = ctx->map;
+ ARRAY_TYPE(seq_range) checked_file_ids;
+ const struct mail_index_header *hdr;
+ const struct mdbox_map_mail_index_record *rec;
+ unsigned int backwards_lookup_count;
+ uint32_t seq, seq1, uid;
+ time_t stamp;
+ bool retry_later;
+
+ if (mail_size >= map->set->mdbox_rotate_size)
+ return 0;
+
+ /* try to find an existing appendable file */
+ stamp = day_begin_stamp(map->set->mdbox_rotate_interval);
+ hdr = mail_index_get_header(map->view);
+
+ backwards_lookup_count = 0;
+ t_array_init(&checked_file_ids, 16);
+
+ if (want_altpath) {
+ /* we want to save to alt storage. */
+ if (mdbox_map_find_primary_files(ctx, &checked_file_ids) < 0)
+ return -1;
+ }
+
+ for (seq = hdr->messages_count; seq > 0; seq--) {
+ if (mdbox_map_lookup_seq(map, seq, &rec) < 0)
+ return -1;
+
+ if (seq_range_exists(&checked_file_ids, rec->file_id))
+ continue;
+ seq_range_array_add(&checked_file_ids, rec->file_id);
+
+ if (++backwards_lookup_count > MAX_BACKWARDS_LOOKUPS) {
+ /* we've wasted enough time here */
+ break;
+ }
+
+ /* first lookup: this should be enough usually, but we can't
+ be sure until after locking. also if messages were recently
+ moved, this message might not be the last one in the file. */
+ if (rec->offset + rec->size + mail_size >=
+ map->set->mdbox_rotate_size)
+ continue;
+
+ if (mdbox_map_is_appending(ctx, rec->file_id)) {
+ /* already checked this */
+ continue;
+ }
+
+ mail_index_lookup_uid(map->view, seq, &uid);
+ if (!mdbox_map_file_try_append(ctx, want_altpath, rec,
+ stamp, mail_size, file_append_r,
+ output_r, &retry_later)) {
+ /* file is too old. the rest of the files are too. */
+ break;
+ }
+ /* NOTE: we've now refreshed map view. there are no guarantees
+ about sequences anymore. */
+ if (*file_append_r != NULL)
+ return 1;
+ /* FIXME: use retry_later somehow */
+ if (uid == 1 ||
+ !mail_index_lookup_seq_range(map->view, 1, uid-1,
+ &seq1, &seq))
+ break;
+ seq++;
+ }
+ return 0;
+}
+
+int mdbox_map_append_next(struct mdbox_map_append_context *ctx,
+ uoff_t mail_size, enum mdbox_map_append_flags flags,
+ struct dbox_file_append_context **file_append_ctx_r,
+ struct ostream **output_r)
+{
+ struct dbox_file *file;
+ struct mdbox_map_append *append;
+ struct dbox_file_append_context *file_append;
+ bool existing, want_altpath;
+ int ret;
+
+ if (ctx->failed)
+ return -1;
+
+ want_altpath = (flags & DBOX_MAP_APPEND_FLAG_ALT) != 0;
+ file_append = mdbox_map_find_existing_append(ctx, mail_size,
+ want_altpath, output_r);
+ if (file_append != NULL) {
+ ret = 1;
+ existing = TRUE;
+ } else {
+ ret = mdbox_map_find_appendable_file(ctx, mail_size, want_altpath,
+ &file_append, output_r);
+ existing = FALSE;
+ }
+ if (ret > 0)
+ file = file_append->file;
+ else if (ret < 0)
+ return -1;
+ else {
+ /* create a new file */
+ file = (flags & DBOX_MAP_APPEND_FLAG_ALT) == 0 ?
+ mdbox_file_init(ctx->map->storage, 0) :
+ mdbox_file_init_new_alt(ctx->map->storage);
+ file_append = dbox_file_append_init(file);
+
+ ret = dbox_file_get_append_stream(file_append, output_r);
+ if (ret <= 0) {
+ i_assert(ret < 0);
+ dbox_file_append_rollback(&file_append);
+ dbox_file_unref(&file);
+ return -1;
+ }
+ }
+
+ append = array_append_space(&ctx->appends);
+ append->file_append = file_append;
+ append->offset = (*output_r)->offset;
+ append->size = (uint32_t)-1;
+ if (!existing) {
+ i_assert(file_append->first_append_offset == 0);
+ file_append->first_append_offset = file_append->output->offset;
+ array_push_back(&ctx->file_appends, &file_append);
+ array_push_back(&ctx->files, &file);
+ }
+ *file_append_ctx_r = file_append;
+ return 0;
+}
+
+static void
+mdbox_map_append_close_if_unneeded(struct mdbox_map *map,
+ struct dbox_file_append_context *append_ctx)
+{
+ struct mdbox_file *mfile =
+ (struct mdbox_file *)append_ctx->file;
+ uoff_t end_offset = append_ctx->output->offset;
+
+ /* if this file is now large enough not to fit any other
+ mails and we created it, close its fd since it's not
+ needed anymore. */
+ if (end_offset > map->set->mdbox_rotate_size &&
+ mfile->file_id == 0 &&
+ dbox_file_append_flush(append_ctx) == 0)
+ dbox_file_close(append_ctx->file);
+}
+
+void mdbox_map_append_finish(struct mdbox_map_append_context *ctx)
+{
+ struct mdbox_map_append *appends, *last;
+ unsigned int count;
+ uoff_t cur_offset;
+
+ appends = array_get_modifiable(&ctx->appends, &count);
+ i_assert(count > 0);
+ last = &appends[count-1];
+ i_assert(last->size == (uint32_t)-1);
+
+ cur_offset = last->file_append->output->offset;
+ i_assert(cur_offset >= last->offset);
+ last->size = cur_offset - last->offset;
+ dbox_file_append_checkpoint(last->file_append);
+
+ mdbox_map_append_close_if_unneeded(ctx->map, last->file_append);
+}
+
+void mdbox_map_append_abort(struct mdbox_map_append_context *ctx)
+{
+ struct mdbox_map_append *appends;
+ unsigned int count;
+
+ appends = array_get_modifiable(&ctx->appends, &count);
+ i_assert(count > 0 && appends[count-1].size == (uint32_t)-1);
+ array_delete(&ctx->appends, count-1, 1);
+}
+
+static int
+mdbox_find_highest_file_id(struct mdbox_map *map, uint32_t *file_id_r)
+{
+ const size_t prefix_len = strlen(MDBOX_MAIL_FILE_PREFIX);
+ DIR *dir;
+ struct dirent *d;
+ unsigned int id, highest_id = 0;
+
+ dir = opendir(map->path);
+ if (dir == NULL) {
+ i_error("opendir(%s) failed: %m", map->path);
+ return -1;
+ }
+ while ((d = readdir(dir)) != NULL) {
+ if (strncmp(d->d_name, MDBOX_MAIL_FILE_PREFIX, prefix_len) == 0 &&
+ str_to_uint(d->d_name + prefix_len, &id) == 0) {
+ if (highest_id < id)
+ highest_id = id;
+ }
+ }
+ (void)closedir(dir);
+
+ *file_id_r = highest_id;
+ return 0;
+}
+
+static int
+mdbox_map_assign_file_ids(struct mdbox_map_append_context *ctx,
+ bool separate_transaction, const char *reason)
+{
+ struct dbox_file_append_context *const *file_appends;
+ unsigned int i, count;
+ struct mdbox_map_mail_index_header hdr;
+ uint32_t first_file_id, file_id, existing_id;
+
+ /* start the syncing. we'll need it even if there are no file ids to
+ be assigned. */
+ if (mdbox_map_atomic_lock(ctx->atomic, reason) < 0)
+ return -1;
+
+ mdbox_map_get_ext_hdr(ctx->map, ctx->atomic->sync_view, &hdr);
+ file_id = hdr.highest_file_id + 1;
+
+ if (ctx->map->verify_existing_file_ids) {
+ /* storage/ directory had been already created but
+ without indexes. scan to see if there exists a higher
+ m.* file id than what is in header, so we won't
+ accidentally overwrite any existing files. */
+ if (mdbox_find_highest_file_id(ctx->map, &existing_id) < 0)
+ return -1;
+ if (file_id < existing_id+1)
+ file_id = existing_id+1;
+ }
+
+ /* assign file_ids for newly created files */
+ first_file_id = file_id;
+ file_appends = array_get(&ctx->file_appends, &count);
+ for (i = 0; i < count; i++) {
+ struct mdbox_file *mfile =
+ (struct mdbox_file *)file_appends[i]->file;
+
+ if (dbox_file_append_flush(file_appends[i]) < 0)
+ return -1;
+
+ if (mfile->file_id == 0) {
+ if (mdbox_file_assign_file_id(mfile, file_id++) < 0)
+ return -1;
+ }
+ }
+
+ ctx->trans = !separate_transaction ? NULL :
+ mail_index_transaction_begin(ctx->map->view,
+ MAIL_INDEX_TRANSACTION_FLAG_FSYNC);
+
+ /* update the highest used file_id */
+ if (first_file_id != file_id) {
+ file_id--;
+ mail_index_update_header_ext(ctx->trans != NULL ? ctx->trans :
+ ctx->atomic->sync_trans,
+ ctx->map->map_ext_id,
+ 0, &file_id, sizeof(file_id));
+ }
+ return 0;
+}
+
+int mdbox_map_append_assign_map_uids(struct mdbox_map_append_context *ctx,
+ uint32_t *first_map_uid_r,
+ uint32_t *last_map_uid_r)
+{
+ const struct mdbox_map_append *appends;
+ const struct mail_index_header *hdr;
+ struct mdbox_map_mail_index_record rec;
+ unsigned int i, count;
+ ARRAY_TYPE(seq_range) uids;
+ const struct seq_range *range;
+ uint32_t seq;
+ uint16_t ref16;
+ int ret = 0;
+
+ if (array_count(&ctx->appends) == 0) {
+ *first_map_uid_r = 0;
+ *last_map_uid_r = 0;
+ return 0;
+ }
+
+ if (mdbox_map_assign_file_ids(ctx, TRUE, "saving - assign uids") < 0)
+ return -1;
+
+ /* append map records to index */
+ i_zero(&rec);
+ ref16 = 1;
+ appends = array_get(&ctx->appends, &count);
+ for (i = 0; i < count; i++) {
+ struct mdbox_file *mfile =
+ (struct mdbox_file *)appends[i].file_append->file;
+
+ i_assert(appends[i].offset <= (uint32_t)-1);
+ i_assert(appends[i].size <= (uint32_t)-1);
+
+ rec.file_id = mfile->file_id;
+ rec.offset = appends[i].offset;
+ rec.size = appends[i].size;
+
+ mail_index_append(ctx->trans, 0, &seq);
+ mail_index_update_ext(ctx->trans, seq, ctx->map->map_ext_id,
+ &rec, NULL);
+ mail_index_update_ext(ctx->trans, seq, ctx->map->ref_ext_id,
+ &ref16, NULL);
+ }
+
+ /* assign map UIDs for appended records */
+ hdr = mail_index_get_header(ctx->atomic->sync_view);
+ t_array_init(&uids, 1);
+ mail_index_append_finish_uids(ctx->trans, hdr->next_uid, &uids);
+ range = array_front(&uids);
+ i_assert(range[0].seq2 - range[0].seq1 + 1 == count);
+
+ if (hdr->uid_validity == 0) {
+ /* we don't really care about uidvalidity, but it can't be 0 */
+ uint32_t uid_validity = ioloop_time;
+ mail_index_update_header(ctx->trans,
+ offsetof(struct mail_index_header, uid_validity),
+ &uid_validity, sizeof(uid_validity), TRUE);
+ }
+
+ if (mail_index_transaction_commit(&ctx->trans) < 0) {
+ mail_storage_set_index_error(MAP_STORAGE(ctx->map),
+ ctx->map->index);
+ return -1;
+ }
+
+ *first_map_uid_r = range[0].seq1;
+ *last_map_uid_r = range[0].seq2;
+ return ret;
+}
+
+int mdbox_map_append_move(struct mdbox_map_append_context *ctx,
+ const ARRAY_TYPE(uint32_t) *map_uids,
+ const ARRAY_TYPE(seq_range) *expunge_map_uids)
+{
+ const struct mdbox_map_append *appends;
+ struct mdbox_map_mail_index_record rec;
+ struct seq_range_iter iter;
+ const uint32_t *uids;
+ unsigned int i, j, map_uids_count, appends_count;
+ uint32_t uid, seq, next_uid;
+
+ /* map is locked by this call */
+ if (mdbox_map_assign_file_ids(ctx, FALSE, "purging - update uids") < 0)
+ return -1;
+
+ i_zero(&rec);
+ appends = array_get(&ctx->appends, &appends_count);
+
+ next_uid = mail_index_get_header(ctx->atomic->sync_view)->next_uid;
+ uids = array_get(map_uids, &map_uids_count);
+ for (i = j = 0; i < map_uids_count; i++) {
+ struct mdbox_file *mfile =
+ (struct mdbox_file *)appends[j].file_append->file;
+
+ i_assert(j < appends_count);
+ rec.file_id = mfile->file_id;
+ rec.offset = appends[j].offset;
+ rec.size = appends[j].size;
+ j++;
+
+ if (!mail_index_lookup_seq(ctx->atomic->sync_view,
+ uids[i], &seq)) {
+ /* We wrote the email to the new m.* file, but another
+ process already expunged it and purged it. Deleting
+ the email from the new m.* file would be problematic
+ at this point, so just add the mail back to the map
+ with refcount=0 and the next purge will remove it. */
+ mail_index_append(ctx->atomic->sync_trans,
+ next_uid++, &seq);
+ }
+ mail_index_update_ext(ctx->atomic->sync_trans, seq,
+ ctx->map->map_ext_id, &rec, NULL);
+ }
+
+ seq_range_array_iter_init(&iter, expunge_map_uids); i = 0;
+ while (seq_range_array_iter_nth(&iter, i++, &uid)) {
+ if (!mail_index_lookup_seq(ctx->atomic->sync_view, uid, &seq))
+ i_unreached();
+ mail_index_expunge(ctx->atomic->sync_trans, seq);
+ }
+ return 0;
+}
+
+int mdbox_map_append_flush(struct mdbox_map_append_context *ctx)
+{
+ struct dbox_file_append_context **file_appends;
+ unsigned int i, count;
+
+ i_assert(ctx->trans == NULL);
+
+ file_appends = array_get_modifiable(&ctx->file_appends, &count);
+ for (i = 0; i < count; i++) {
+ if (dbox_file_append_flush(file_appends[i]) < 0)
+ return -1;
+ }
+ return 0;
+}
+
+int mdbox_map_append_commit(struct mdbox_map_append_context *ctx)
+{
+ struct dbox_file_append_context **file_appends;
+ unsigned int i, count;
+
+ i_assert(ctx->trans == NULL);
+
+ file_appends = array_get_modifiable(&ctx->file_appends, &count);
+ for (i = 0; i < count; i++) {
+ if (dbox_file_append_commit(&file_appends[i]) < 0)
+ return -1;
+ }
+ mdbox_map_atomic_set_success(ctx->atomic);
+ return 0;
+}
+
+void mdbox_map_append_free(struct mdbox_map_append_context **_ctx)
+{
+ struct mdbox_map_append_context *ctx = *_ctx;
+ struct dbox_file_append_context **file_appends;
+ struct dbox_file **files;
+ unsigned int i, count;
+
+ *_ctx = NULL;
+
+ if (ctx->trans != NULL)
+ mail_index_transaction_rollback(&ctx->trans);
+
+ file_appends = array_get_modifiable(&ctx->file_appends, &count);
+ for (i = 0; i < count; i++) {
+ if (file_appends[i] != NULL)
+ dbox_file_append_rollback(&file_appends[i]);
+ }
+
+ files = array_get_modifiable(&ctx->files, &count);
+ for (i = 0; i < count; i++) {
+ dbox_file_unlock(files[i]);
+ dbox_file_unref(&files[i]);
+ }
+
+ array_free(&ctx->appends);
+ array_free(&ctx->file_appends);
+ array_free(&ctx->files);
+ i_free(ctx);
+}
+
+static int mdbox_map_generate_uid_validity(struct mdbox_map *map)
+{
+ const struct mail_index_header *hdr;
+ struct mail_index_sync_ctx *sync_ctx;
+ struct mail_index_view *view;
+ struct mail_index_transaction *trans;
+ uint32_t uid_validity;
+ int ret;
+
+ /* do this inside syncing, so that we're locked and there are no
+ race conditions */
+ ret = mail_index_sync_begin(map->index, &sync_ctx, &view, &trans,
+ MAIL_INDEX_SYNC_FLAG_UPDATE_TAIL_OFFSET);
+ if (ret <= 0) {
+ i_assert(ret != 0);
+ return -1;
+ }
+ mdbox_map_sync_handle(map, sync_ctx);
+
+ hdr = mail_index_get_header(map->view);
+ if (hdr->uid_validity != 0) {
+ /* someone else beat us to it */
+ } else {
+ uid_validity = ioloop_time;
+ mail_index_update_header(trans,
+ offsetof(struct mail_index_header, uid_validity),
+ &uid_validity, sizeof(uid_validity), TRUE);
+ }
+ mail_index_sync_set_reason(sync_ctx, "uidvalidity initialization");
+ return mail_index_sync_commit(&sync_ctx);
+}
+
+uint32_t mdbox_map_get_uid_validity(struct mdbox_map *map)
+{
+ uint32_t uid_validity;
+
+ i_assert(map->view != NULL);
+
+ uid_validity = mail_index_get_header(map->view)->uid_validity;
+ if (uid_validity == 0)
+ mdbox_map_set_corrupted(map, "lost uidvalidity");
+ return uid_validity;
+}
diff --git a/src/lib-storage/index/dbox-multi/mdbox-map.h b/src/lib-storage/index/dbox-multi/mdbox-map.h
new file mode 100644
index 0000000..9571f0e
--- /dev/null
+++ b/src/lib-storage/index/dbox-multi/mdbox-map.h
@@ -0,0 +1,144 @@
+#ifndef MDBOX_MAP_H
+#define MDBOX_MAP_H
+
+#include "seq-range-array.h"
+
+struct dbox_file_append_context;
+struct mdbox_map_append_context;
+struct mdbox_storage;
+
+enum mdbox_map_append_flags {
+ DBOX_MAP_APPEND_FLAG_ALT = 0x01
+};
+
+struct mdbox_map_mail_index_header {
+ uint32_t highest_file_id;
+ /* increased every time storage is rebuilt */
+ uint32_t rebuild_count;
+};
+
+struct mdbox_map_mail_index_record {
+ uint32_t file_id;
+ uint32_t offset;
+ uint32_t size; /* including pre/post metadata */
+};
+
+struct mdbox_map_file_msg {
+ uint32_t map_uid;
+ uint32_t offset;
+ uint32_t refcount;
+};
+ARRAY_DEFINE_TYPE(mdbox_map_file_msg, struct mdbox_map_file_msg);
+
+struct mdbox_map *
+mdbox_map_init(struct mdbox_storage *storage, struct mailbox_list *root_list);
+void mdbox_map_deinit(struct mdbox_map **map);
+
+/* Open the map. Returns 1 if ok, 0 if map doesn't exist, -1 if error. */
+int mdbox_map_open(struct mdbox_map *map);
+/* Open or create the map. This is done automatically for most operations.
+ Returns 0 if ok, -1 if error. */
+int mdbox_map_open_or_create(struct mdbox_map *map);
+/* Refresh the map. Returns 0 if ok, -1 if error. */
+int mdbox_map_refresh(struct mdbox_map *map);
+/* Returns TRUE if map has been fsck'd. */
+bool mdbox_map_is_fscked(struct mdbox_map *map);
+
+/* Return the current rebuild counter */
+uint32_t mdbox_map_get_rebuild_count(struct mdbox_map *map);
+
+/* Look up file_id and offset for given map UID. Returns 1 if ok, 0 if UID
+ is already expunged, -1 if error. */
+int mdbox_map_lookup(struct mdbox_map *map, uint32_t map_uid,
+ uint32_t *file_id_r, uoff_t *offset_r);
+/* Like mdbox_map_lookup(), but look up everything. */
+int mdbox_map_lookup_full(struct mdbox_map *map, uint32_t map_uid,
+ struct mdbox_map_mail_index_record *rec_r,
+ uint16_t *refcount_r);
+/* Like mdbox_map_lookup_full(), but look up with sequence. */
+int mdbox_map_lookup_seq_full(struct mdbox_map *map, uint32_t seq,
+ struct mdbox_map_mail_index_record *rec_r,
+ uint16_t *refcount_r);
+/* Return map UID for the map sequence. */
+uint32_t mdbox_map_lookup_uid(struct mdbox_map *map, uint32_t seq);
+/* Returns the total number of messages in the map. */
+unsigned int mdbox_map_get_messages_count(struct mdbox_map *map);
+
+/* Get all messages from file */
+int mdbox_map_get_file_msgs(struct mdbox_map *map, uint32_t file_id,
+ ARRAY_TYPE(mdbox_map_file_msg) *recs);
+
+/* Begin atomic context. There can be multiple transactions/appends within the
+ same atomic context. */
+struct mdbox_map_atomic_context *mdbox_map_atomic_begin(struct mdbox_map *map);
+/* Lock the map immediately. */
+int mdbox_map_atomic_lock(struct mdbox_map_atomic_context *atomic,
+ const char *reason);
+/* Returns TRUE if map is locked */
+bool mdbox_map_atomic_is_locked(struct mdbox_map_atomic_context *atomic);
+/* When finish() is called, rollback the changes. If data was already written
+ to map's transaction log, this desyncs the map and causes a rebuild */
+void mdbox_map_atomic_set_failed(struct mdbox_map_atomic_context *atomic);
+/* Mark this atomic as having succeeded. This is internally done if
+ transaction or append is committed within this atomic, but not when the
+ atomic is used standalone. */
+void mdbox_map_atomic_set_success(struct mdbox_map_atomic_context *atomic);
+/* Remove fsck'd flag. */
+void mdbox_map_atomic_unset_fscked(struct mdbox_map_atomic_context *atomic);
+/* Commit/rollback changes within this atomic context. */
+int mdbox_map_atomic_finish(struct mdbox_map_atomic_context **atomic);
+
+struct mdbox_map_transaction_context *
+mdbox_map_transaction_begin(struct mdbox_map_atomic_context *atomic,
+ bool external);
+/* Write transaction to map and leave it locked. Call _free() to update tail
+ offset and unlock. */
+int mdbox_map_transaction_commit(struct mdbox_map_transaction_context *ctx,
+ const char *reason);
+void mdbox_map_transaction_free(struct mdbox_map_transaction_context **ctx);
+
+int mdbox_map_update_refcount(struct mdbox_map_transaction_context *ctx,
+ uint32_t map_uid, int diff);
+int mdbox_map_update_refcounts(struct mdbox_map_transaction_context *ctx,
+ const ARRAY_TYPE(uint32_t) *map_uids, int diff);
+int mdbox_map_remove_file_id(struct mdbox_map *map, uint32_t file_id);
+
+/* Return all files containing messages with zero refcount. */
+int mdbox_map_get_zero_ref_files(struct mdbox_map *map,
+ ARRAY_TYPE(seq_range) *file_ids_r);
+
+struct mdbox_map_append_context *
+mdbox_map_append_begin(struct mdbox_map_atomic_context *atomic);
+/* Request file for saving a new message with given size (if available). If an
+ existing file can be used, the record is locked and updated in index.
+ Returns 0 if ok, -1 if error. */
+int mdbox_map_append_next(struct mdbox_map_append_context *ctx, uoff_t mail_size,
+ enum mdbox_map_append_flags flags,
+ struct dbox_file_append_context **file_append_ctx_r,
+ struct ostream **output_r);
+/* Finished saving the last mail. Saves the message size. */
+void mdbox_map_append_finish(struct mdbox_map_append_context *ctx);
+/* Abort saving the last mail. */
+void mdbox_map_append_abort(struct mdbox_map_append_context *ctx);
+/* Assign map UIDs to all appended msgs to multi-files. */
+int mdbox_map_append_assign_map_uids(struct mdbox_map_append_context *ctx,
+ uint32_t *first_map_uid_r,
+ uint32_t *last_map_uid_r);
+/* The appends are existing messages that were simply moved to a new file.
+ map_uids contains the moved messages' map UIDs. */
+int mdbox_map_append_move(struct mdbox_map_append_context *ctx,
+ const ARRAY_TYPE(uint32_t) *map_uids,
+ const ARRAY_TYPE(seq_range) *expunge_map_uids);
+/* Flush/fsync appends. */
+int mdbox_map_append_flush(struct mdbox_map_append_context *ctx);
+/* Returns 0 if ok, -1 if error. */
+int mdbox_map_append_commit(struct mdbox_map_append_context *ctx);
+void mdbox_map_append_free(struct mdbox_map_append_context **ctx);
+
+/* Returns map's uidvalidity */
+uint32_t mdbox_map_get_uid_validity(struct mdbox_map *map);
+
+void mdbox_map_set_corrupted(struct mdbox_map *map, const char *format, ...)
+ ATTR_FORMAT(2, 3);
+
+#endif
diff --git a/src/lib-storage/index/dbox-multi/mdbox-purge.c b/src/lib-storage/index/dbox-multi/mdbox-purge.c
new file mode 100644
index 0000000..8461074
--- /dev/null
+++ b/src/lib-storage/index/dbox-multi/mdbox-purge.c
@@ -0,0 +1,690 @@
+/* Copyright (c) 2007-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "istream.h"
+#include "ostream.h"
+#include "str.h"
+#include "hash.h"
+#include "dbox-attachment.h"
+#include "mdbox-storage.h"
+#include "mdbox-storage-rebuild.h"
+#include "mdbox-file.h"
+#include "mdbox-map.h"
+#include "mdbox-sync.h"
+
+#include <dirent.h>
+
+/*
+ Altmoving works like:
+
+ 1. Message's DBOX_INDEX_FLAG_ALT flag is changed. This is caught by mdbox
+ code and map UID's alt-refcount is updated. It won't be written to disk.
+ 2. mdbox_purge() is called, which checks if map UID's refcount equals
+ to its alt-refcount. If it does, it's moved to alt storage. Moving to
+ primary storage is done if _ALT flag was removed from any message.
+*/
+
+enum mdbox_msg_action {
+ MDBOX_MSG_ACTION_MOVE_TO_ALT = 1,
+ MDBOX_MSG_ACTION_MOVE_FROM_ALT
+};
+
+struct mdbox_purge_context {
+ pool_t pool;
+ struct mdbox_storage *storage;
+
+ uint32_t lowest_primary_file_id;
+ /* list of file_ids that exist in primary storage. this list is looked
+ up while there is no locking, so it may not be accurate anymore by
+ the time it's used. */
+ ARRAY_TYPE(seq_range) primary_file_ids;
+ /* list of file_ids that we need to purge */
+ ARRAY_TYPE(seq_range) purge_file_ids;
+
+ /* uint32_t map_uid => enum mdbox_msg_action action */
+ HASH_TABLE(void *, void *) altmoves;
+ bool have_altmoves;
+
+ struct mdbox_map_atomic_context *atomic;
+ struct mdbox_map_append_context *append_ctx;
+};
+
+static int mdbox_map_file_msg_offset_cmp(const struct mdbox_map_file_msg *m1,
+ const struct mdbox_map_file_msg *m2)
+{
+ if (m1->offset < m2->offset)
+ return -1;
+ else if (m1->offset > m2->offset)
+ return 1;
+ else
+ return 0;
+}
+
+static int
+mdbox_file_read_metadata_hdr(struct dbox_file *file,
+ struct dbox_metadata_header *meta_hdr_r)
+{
+ const unsigned char *data;
+ size_t size;
+ int ret;
+
+ ret = i_stream_read_bytes(file->input, &data, &size,
+ sizeof(*meta_hdr_r));
+ if (ret <= 0) {
+ i_assert(ret == -1);
+ if (file->input->stream_errno == 0) {
+ dbox_file_set_corrupted(file, "missing metadata");
+ return 0;
+ }
+ mail_storage_set_critical(&file->storage->storage,
+ "read(%s) failed: %s", file->cur_path,
+ i_stream_get_error(file->input));
+ return -1;
+ }
+
+ memcpy(meta_hdr_r, data, sizeof(*meta_hdr_r));
+ if (memcmp(meta_hdr_r->magic_post, DBOX_MAGIC_POST,
+ sizeof(meta_hdr_r->magic_post)) != 0) {
+ dbox_file_set_corrupted(file, "invalid metadata magic");
+ return 0;
+ }
+ i_stream_skip(file->input, sizeof(*meta_hdr_r));
+ return 1;
+}
+
+static int
+mdbox_file_metadata_copy(struct dbox_file *file, struct ostream *output)
+{
+ struct dbox_metadata_header meta_hdr;
+ const char *line;
+ size_t buf_size;
+ int ret;
+
+ if ((ret = mdbox_file_read_metadata_hdr(file, &meta_hdr)) <= 0)
+ return ret;
+
+ o_stream_nsend(output, &meta_hdr, sizeof(meta_hdr));
+ buf_size = i_stream_get_max_buffer_size(file->input);
+ /* use unlimited line length for metadata */
+ i_stream_set_max_buffer_size(file->input, SIZE_MAX);
+ while ((line = i_stream_read_next_line(file->input)) != NULL) {
+ if (*line == '\0') {
+ /* end of metadata */
+ break;
+ }
+ o_stream_nsend_str(output, line);
+ o_stream_nsend(output, "\n", 1);
+ }
+ i_stream_set_max_buffer_size(file->input, buf_size);
+
+ if (line == NULL) {
+ dbox_file_set_corrupted(file, "missing end-of-metadata line");
+ return 0;
+ }
+ o_stream_nsend(output, "\n", 1);
+ return 1;
+}
+
+static int
+mdbox_metadata_get_extrefs(struct dbox_file *file, pool_t ext_refs_pool,
+ ARRAY_TYPE(mail_attachment_extref) *extrefs)
+{
+ struct dbox_metadata_header meta_hdr;
+ const char *line;
+ size_t buf_size;
+ int ret;
+
+ /* skip and ignore the header */
+ if ((ret = mdbox_file_read_metadata_hdr(file, &meta_hdr)) <= 0)
+ return ret;
+
+ buf_size = i_stream_get_max_buffer_size(file->input);
+ /* use unlimited line length for metadata */
+ i_stream_set_max_buffer_size(file->input, SIZE_MAX);
+ while ((line = i_stream_read_next_line(file->input)) != NULL) {
+ if (*line == '\0') {
+ /* end of metadata */
+ break;
+ }
+ if (*line == DBOX_METADATA_EXT_REF) T_BEGIN {
+ if (!index_attachment_parse_extrefs(line+1, ext_refs_pool,
+ extrefs)) {
+ i_warning("%s: Ignoring corrupted extref: %s",
+ file->cur_path, line);
+ }
+ } T_END;
+ }
+ i_stream_set_max_buffer_size(file->input, buf_size);
+
+ if (line == NULL) {
+ dbox_file_set_corrupted(file, "missing end-of-metadata line");
+ return 0;
+ }
+ return 1;
+}
+
+static bool
+mdbox_purge_want_altpath(struct mdbox_purge_context *ctx,
+ struct dbox_file *file, uint32_t map_uid)
+{
+ enum mdbox_msg_action action;
+ void *value;
+
+ if (dbox_file_is_in_alt(file))
+ return TRUE;
+
+ if (!ctx->have_altmoves)
+ return FALSE;
+
+ value = hash_table_lookup(ctx->altmoves, POINTER_CAST(map_uid));
+ action = POINTER_CAST_TO(value, enum mdbox_msg_action);
+ return action == MDBOX_MSG_ACTION_MOVE_TO_ALT;
+}
+
+static int
+mdbox_purge_save_msg(struct mdbox_purge_context *ctx, struct dbox_file *file,
+ const struct mdbox_map_file_msg *msg)
+{
+ struct dbox_file_append_context *out_file_append;
+ struct istream *input;
+ struct ostream *output;
+ enum mdbox_map_append_flags append_flags;
+ uoff_t msg_size;
+ int ret;
+
+ if (ctx->append_ctx == NULL)
+ ctx->append_ctx = mdbox_map_append_begin(ctx->atomic);
+
+ append_flags = !mdbox_purge_want_altpath(ctx, file, msg->map_uid) ? 0 :
+ DBOX_MAP_APPEND_FLAG_ALT;
+ msg_size = file->msg_header_size + file->cur_physical_size;
+ if (mdbox_map_append_next(ctx->append_ctx, file->cur_physical_size,
+ append_flags, &out_file_append, &output) < 0)
+ return -1;
+
+ i_assert(file != out_file_append->file);
+
+ input = i_stream_create_limit(file->input, msg_size);
+ o_stream_nsend_istream(output, input);
+ if (o_stream_flush(output) < 0) {
+ mail_storage_set_critical(&file->storage->storage,
+ "write(%s) failed: %s",
+ out_file_append->file->cur_path,
+ o_stream_get_error(output));
+ ret = -1;
+ } else if (input->v_offset != msg_size) {
+ i_assert(input->v_offset < msg_size);
+ i_assert(i_stream_read_eof(file->input));
+
+ dbox_file_set_corrupted(file, "truncated message at EOF");
+ ret = 0;
+ } else {
+ ret = 1;
+ }
+ i_stream_unref(&input);
+
+ if (ret > 0) {
+ /* copy metadata */
+ if ((ret = mdbox_file_metadata_copy(file, output)) <= 0)
+ return ret;
+
+ mdbox_map_append_finish(ctx->append_ctx);
+ }
+ return ret;
+}
+
+static int
+mdbox_file_purge_check_refcounts(struct mdbox_purge_context *ctx,
+ const ARRAY_TYPE(mdbox_map_file_msg) *msgs_arr)
+{
+ struct mdbox_map *map = ctx->storage->map;
+ struct mdbox_map_mail_index_record rec;
+ uint16_t refcount;
+ const struct mdbox_map_file_msg *msgs;
+ unsigned int i, count;
+ int ret;
+
+ if (mdbox_map_atomic_lock(ctx->atomic, "purging check") < 0)
+ return -1;
+
+ msgs = array_get(msgs_arr, &count);
+ for (i = 0; i < count; i++) {
+ if (msgs[i].refcount != 0)
+ continue;
+
+ ret = mdbox_map_lookup_full(map, msgs[i].map_uid, &rec,
+ &refcount);
+ if (ret <= 0) {
+ if (ret < 0)
+ return -1;
+ mdbox_map_set_corrupted(map,
+ "Purging unexpectedly lost map_uid=%u",
+ msgs[i].map_uid);
+ return -1;
+ }
+ if (refcount > 0)
+ return 0;
+ }
+ return 1;
+}
+
+static int
+mdbox_purge_attachments(struct mdbox_purge_context *ctx,
+ const ARRAY_TYPE(mail_attachment_extref) *extrefs_arr)
+{
+ struct dbox_storage *storage = &ctx->storage->storage;
+ const struct mail_attachment_extref *extref;
+ int ret = 0;
+
+ array_foreach(extrefs_arr, extref) {
+ if (index_attachment_delete(&storage->storage,
+ storage->attachment_fs,
+ extref->path) < 0)
+ ret = -1;
+ }
+ return ret;
+}
+
+static int
+mdbox_file_purge(struct mdbox_purge_context *ctx, struct dbox_file *file,
+ uint32_t file_id)
+{
+ struct mdbox_storage *dstorage = (struct mdbox_storage *)file->storage;
+ struct stat st;
+ ARRAY_TYPE(mdbox_map_file_msg) msgs_arr;
+ const struct mdbox_map_file_msg *msgs;
+ ARRAY_TYPE(seq_range) expunged_map_uids;
+ ARRAY_TYPE(uint32_t) copied_map_uids;
+ ARRAY_TYPE(mail_attachment_extref) ext_refs;
+ pool_t ext_refs_pool;
+ unsigned int i, count;
+ uoff_t offset;
+ int ret;
+
+ i_assert(ctx->atomic == NULL);
+ i_assert(ctx->append_ctx == NULL);
+
+ if ((ret = dbox_file_try_lock(file)) <= 0)
+ return ret;
+
+ /* make sure the file still exists. another process may have already
+ deleted it. */
+ if (stat(file->cur_path, &st) < 0) {
+ dbox_file_unlock(file);
+ if (errno == ENOENT)
+ return 0;
+
+ mail_storage_set_critical(&file->storage->storage,
+ "stat(%s) failed: %m", file->cur_path);
+ return -1;
+ }
+
+ /* get list of map UIDs that exist in this file (again has to be done
+ after locking) */
+ i_array_init(&msgs_arr, 128);
+ if (mdbox_map_get_file_msgs(dstorage->map, file_id,
+ &msgs_arr) < 0) {
+ array_free(&msgs_arr);
+ dbox_file_unlock(file);
+ return -1;
+ }
+ /* sort messages by their offset */
+ array_sort(&msgs_arr, mdbox_map_file_msg_offset_cmp);
+
+ ext_refs_pool = pool_alloconly_create("mdbox purge ext refs", 1024);
+ ctx->atomic = mdbox_map_atomic_begin(ctx->storage->map);
+ msgs = array_get(&msgs_arr, &count);
+ i_array_init(&ext_refs, 32);
+ i_array_init(&copied_map_uids, I_MIN(count, 1));
+ i_array_init(&expunged_map_uids, I_MIN(count, 1));
+ offset = file->file_header_size;
+ for (i = 0; i < count; i++) {
+ if ((ret = dbox_file_seek(file, offset)) <= 0)
+ break;
+
+ if (msgs[i].offset != offset) {
+ /* map doesn't match file's actual contents */
+ dbox_file_set_corrupted(file,
+ "purging found mismatched offsets "
+ "(%"PRIuUOFF_T" vs %u, %u/%u)",
+ offset, msgs[i].offset, i, count);
+ ret = 0;
+ break;
+ }
+
+ if (msgs[i].refcount == 0) {
+ /* skip over expunged message */
+ i_stream_seek(file->input, offset +
+ file->msg_header_size +
+ file->cur_physical_size);
+ /* skip metadata */
+ ret = mdbox_metadata_get_extrefs(file, ext_refs_pool,
+ &ext_refs);
+ if (ret <= 0)
+ break;
+ seq_range_array_add(&expunged_map_uids,
+ msgs[i].map_uid);
+ } else {
+ /* non-expunged message. write it to output file. */
+ i_stream_seek(file->input, offset);
+ ret = mdbox_purge_save_msg(ctx, file, &msgs[i]);
+ if (ret <= 0)
+ break;
+ array_push_back(&copied_map_uids, &msgs[i].map_uid);
+ }
+ offset = file->input->v_offset;
+ }
+ if (offset != (uoff_t)st.st_size && ret > 0) {
+ /* file has more messages than what map tells us */
+ dbox_file_set_corrupted(file,
+ "more messages available than in map "
+ "(%"PRIuUOFF_T" < %"PRIuUOFF_T")", offset, st.st_size);
+ ret = 0;
+ }
+ if (ret > 0 && ctx->append_ctx != NULL) {
+ /* flush writes before locking the map */
+ if (mdbox_map_append_flush(ctx->append_ctx) < 0)
+ ret = -1;
+ }
+
+ if (ret <= 0)
+ ret = -1;
+ else {
+ /* it's possible that one of the messages we purged was
+ just copied to another mailbox. the only way to prevent that
+ would be to keep map locked during the purge, but that could
+ keep it locked for too long. instead we'll check here if
+ there are such copies, and if there are cancel this file's
+ purge. */
+ ret = mdbox_file_purge_check_refcounts(ctx, &msgs_arr);
+ }
+ array_free(&msgs_arr); msgs = NULL;
+
+ if (ret <= 0) {
+ /* failed */
+ } else if (ctx->append_ctx == NULL) {
+ /* everything purged from this file */
+ ret = 1;
+ } else {
+ /* assign new file_id + offset to moved messages */
+ if (mdbox_map_append_move(ctx->append_ctx, &copied_map_uids,
+ &expunged_map_uids) < 0 ||
+ mdbox_map_append_commit(ctx->append_ctx) < 0)
+ ret = -1;
+ else
+ ret = 1;
+ }
+ if (ctx->append_ctx != NULL)
+ mdbox_map_append_free(&ctx->append_ctx);
+ (void)mdbox_map_atomic_finish(&ctx->atomic);
+
+ /* unlink only after unlocking map, so readers don't see it
+ temporarily vanished */
+ if (ret > 0) {
+ (void)dbox_file_unlink(file);
+ if (mdbox_map_remove_file_id(ctx->storage->map, file_id) < 0)
+ ret = -1;
+ } else {
+ dbox_file_unlock(file);
+ }
+ array_free(&copied_map_uids);
+ array_free(&expunged_map_uids);
+
+ (void)mdbox_purge_attachments(ctx, &ext_refs);
+ array_free(&ext_refs);
+ pool_unref(&ext_refs_pool);
+ return ret;
+}
+
+void mdbox_purge_alt_flag_change(struct mail *mail, bool move_to_alt)
+{
+ struct mdbox_mailbox *mbox = MDBOX_MAILBOX(mail->box);
+ ARRAY_TYPE(uint32_t) *dest;
+ uint32_t map_uid;
+
+ /* we'll assume here that alt flag won't be changed multiple times
+ for the same mail. it shouldn't happen with current code, and
+ checking for it would just slow down the code.
+
+ so the way it works currently is just that map_uids are added to
+ an array, which is later sorted and processed further. note that
+ it's still possible that the same map_uid exists in the array
+ multiple times. */
+ if (mdbox_mail_lookup(mbox, mbox->box.view, mail->seq, &map_uid) < 0)
+ return;
+
+ dest = move_to_alt ? &mbox->storage->move_to_alt_map_uids :
+ &mbox->storage->move_from_alt_map_uids;
+
+ if (!array_is_created(dest))
+ i_array_init(dest, 256);
+ array_push_back(dest, &map_uid);
+}
+
+static struct mdbox_purge_context *
+mdbox_purge_alloc(struct mdbox_storage *storage)
+{
+ struct mdbox_purge_context *ctx;
+ pool_t pool;
+
+ pool = pool_alloconly_create("mdbox purge context", 1024*32);
+ ctx = p_new(pool, struct mdbox_purge_context, 1);
+ ctx->pool = pool;
+ ctx->storage = storage;
+ ctx->lowest_primary_file_id = (uint32_t)-1;
+ i_array_init(&ctx->primary_file_ids, 64);
+ i_array_init(&ctx->purge_file_ids, 64);
+ hash_table_create_direct(&ctx->altmoves, pool, 0);
+ return ctx;
+}
+
+static void mdbox_purge_free(struct mdbox_purge_context **_ctx)
+{
+ struct mdbox_purge_context *ctx = *_ctx;
+
+ *_ctx = NULL;
+
+ hash_table_destroy(&ctx->altmoves);
+ array_free(&ctx->primary_file_ids);
+ array_free(&ctx->purge_file_ids);
+ pool_unref(&ctx->pool);
+}
+
+static int mdbox_purge_get_primary_files(struct mdbox_purge_context *ctx)
+{
+ struct mdbox_storage *dstorage = ctx->storage;
+ struct mail_storage *storage = &dstorage->storage.storage;
+ DIR *dir;
+ struct dirent *d;
+ string_t *path;
+ unsigned int file_id;
+ size_t dir_len;
+ int ret = 0;
+
+ if (!array_is_created(&dstorage->move_to_alt_map_uids) &&
+ !array_is_created(&dstorage->move_from_alt_map_uids)) {
+ /* we don't need to do alt moving, don't bother getting list
+ of primary files */
+ return 0;
+ }
+
+ dir = opendir(dstorage->storage_dir);
+ if (dir == NULL) {
+ if (errno == ENOENT) {
+ /* no storage directory at all yet */
+ return 0;
+ }
+ mail_storage_set_critical(storage,
+ "opendir(%s) failed: %m", dstorage->storage_dir);
+ return -1;
+ }
+
+ path = t_str_new(256);
+ str_append(path, dstorage->storage_dir);
+ str_append_c(path, '/');
+ dir_len = str_len(path);
+
+ for (errno = 0; (d = readdir(dir)) != NULL; errno = 0) {
+ if (!str_begins(d->d_name, MDBOX_MAIL_FILE_PREFIX))
+ continue;
+ if (str_to_uint32(d->d_name + strlen(MDBOX_MAIL_FILE_PREFIX),
+ &file_id) < 0)
+ continue;
+
+ str_truncate(path, dir_len);
+ str_append(path, d->d_name);
+ seq_range_array_add(&ctx->primary_file_ids, file_id);
+ }
+ if (array_count(&ctx->primary_file_ids) > 0) {
+ const struct seq_range *range =
+ array_front(&ctx->primary_file_ids);
+ ctx->lowest_primary_file_id = range[0].seq1;
+ }
+ if (errno != 0) {
+ mail_storage_set_critical(storage,
+ "readdir(%s) failed: %m", dstorage->storage_dir);
+ ret = -1;
+ }
+ if (closedir(dir) < 0) {
+ mail_storage_set_critical(storage,
+ "closedir(%s) failed: %m", dstorage->storage_dir);
+ ret = -1;
+ }
+ return ret;
+}
+
+static int uint32_t_cmp(const uint32_t *u1, const uint32_t *u2)
+{
+ if (*u1 < *u2)
+ return -1;
+ if (*u1 > *u2)
+ return 1;
+ return 0;
+}
+
+static int mdbox_altmove_add_files(struct mdbox_purge_context *ctx)
+{
+ struct mdbox_storage *dstorage = ctx->storage;
+ const uint32_t *map_uids;
+ unsigned int i, count, alt_refcount = 0;
+ struct mdbox_map_mail_index_record cur_rec;
+ enum mdbox_msg_action action;
+ uint32_t cur_map_uid;
+ uint16_t cur_refcount = 0;
+ uoff_t offset;
+ int ret = 0;
+
+ /* first add move-to-alt actions */
+ if (array_is_created(&dstorage->move_to_alt_map_uids)) {
+ array_sort(&dstorage->move_to_alt_map_uids, uint32_t_cmp);
+ map_uids = array_get(&dstorage->move_to_alt_map_uids, &count);
+ } else {
+ map_uids = NULL;
+ count = 0;
+ }
+ cur_map_uid = 0;
+ for (i = 0; i < count; i++) {
+ if (cur_map_uid != map_uids[i]) {
+ cur_map_uid = map_uids[i];
+ if (mdbox_map_lookup_full(dstorage->map, cur_map_uid,
+ &cur_rec, &cur_refcount) < 0) {
+ cur_refcount = (uint16_t)-1;
+ ret = -1;
+ }
+ alt_refcount = 1;
+ } else {
+ alt_refcount++;
+ }
+
+ if (alt_refcount == cur_refcount &&
+ seq_range_exists(&ctx->primary_file_ids, cur_rec.file_id)) {
+ /* all instances marked as moved to alt storage */
+ action = MDBOX_MSG_ACTION_MOVE_TO_ALT;
+ hash_table_insert(ctx->altmoves,
+ POINTER_CAST(cur_map_uid),
+ POINTER_CAST(action));
+ seq_range_array_add(&ctx->purge_file_ids,
+ cur_rec.file_id);
+ }
+ }
+
+ /* next add move-from-alt actions. they override move-to-alt actions
+ in case there happen to be any conflicts (shouldn't). only a single
+ move-from-alt record is needed to do the move. */
+ if (array_is_created(&dstorage->move_from_alt_map_uids))
+ map_uids = array_get(&dstorage->move_from_alt_map_uids, &count);
+ else {
+ map_uids = NULL;
+ count = 0;
+ }
+ cur_map_uid = 0;
+ for (i = 0; i < count; i++) {
+ if (cur_map_uid == map_uids[i])
+ continue;
+ cur_map_uid = map_uids[i];
+
+ if (mdbox_map_lookup(dstorage->map, cur_map_uid,
+ &cur_rec.file_id, &offset) < 0) {
+ ret = -1;
+ continue;
+ }
+ if (seq_range_exists(&ctx->primary_file_ids, cur_rec.file_id)) {
+ /* already in primary storage */
+ continue;
+ }
+
+ action = MDBOX_MSG_ACTION_MOVE_FROM_ALT;
+ hash_table_update(ctx->altmoves, POINTER_CAST(cur_map_uid),
+ POINTER_CAST(action));
+ seq_range_array_add(&ctx->purge_file_ids, cur_rec.file_id);
+ }
+ ctx->have_altmoves = hash_table_count(ctx->altmoves) > 0;
+ return ret;
+}
+
+int mdbox_purge(struct mail_storage *_storage)
+{
+ struct mdbox_storage *storage = (struct mdbox_storage *)_storage;
+ struct mdbox_purge_context *ctx;
+ struct dbox_file *file;
+ struct seq_range_iter iter;
+ unsigned int i = 0;
+ uint32_t file_id;
+ bool deleted;
+ int ret;
+
+ ctx = mdbox_purge_alloc(storage);
+ ret = mdbox_map_get_zero_ref_files(storage->map, &ctx->purge_file_ids);
+ if (storage->alt_storage_dir != NULL) {
+ if (mdbox_purge_get_primary_files(ctx) < 0)
+ ret = -1;
+ else {
+ /* add files that can be altmoved */
+ if (mdbox_altmove_add_files(ctx) < 0)
+ ret = -1;
+ }
+ }
+
+ seq_range_array_iter_init(&iter, &ctx->purge_file_ids); i = 0;
+ while (ret == 0 &&
+ seq_range_array_iter_nth(&iter, i++, &file_id)) T_BEGIN {
+ file = mdbox_file_init(storage, file_id);
+ if (dbox_file_open(file, &deleted) > 0 && !deleted) {
+ if (mdbox_file_purge(ctx, file, file_id) < 0)
+ ret = -1;
+ } else {
+ if (mdbox_map_remove_file_id(storage->map, file_id) < 0)
+ ret = -1;
+ }
+ dbox_file_unref(&file);
+ } T_END;
+ mdbox_purge_free(&ctx);
+
+ if (storage->corrupted) {
+ /* purging found corrupted files */
+ (void)mdbox_storage_rebuild(storage);
+ ret = -1;
+ }
+ return ret;
+}
diff --git a/src/lib-storage/index/dbox-multi/mdbox-save.c b/src/lib-storage/index/dbox-multi/mdbox-save.c
new file mode 100644
index 0000000..c0dcf5b
--- /dev/null
+++ b/src/lib-storage/index/dbox-multi/mdbox-save.c
@@ -0,0 +1,491 @@
+/* Copyright (c) 2007-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "fdatasync-path.h"
+#include "hex-binary.h"
+#include "hex-dec.h"
+#include "str.h"
+#include "istream.h"
+#include "istream-crlf.h"
+#include "ostream.h"
+#include "write-full.h"
+#include "index-mail.h"
+#include "index-pop3-uidl.h"
+#include "mail-copy.h"
+#include "dbox-save.h"
+#include "mdbox-storage.h"
+#include "mdbox-map.h"
+#include "mdbox-file.h"
+#include "mdbox-sync.h"
+
+
+struct dbox_save_mail {
+ struct dbox_file_append_context *file_append;
+ uint32_t seq;
+ uint32_t append_offset;
+ time_t save_date;
+ bool written_to_disk;
+};
+
+struct mdbox_save_context {
+ struct dbox_save_context ctx;
+
+ struct mdbox_mailbox *mbox;
+ struct mdbox_sync_context *sync_ctx;
+
+ struct dbox_file *cur_file;
+ struct dbox_file_append_context *cur_file_append;
+ struct mdbox_map_append_context *append_ctx;
+
+ ARRAY_TYPE(uint32_t) copy_map_uids;
+ struct mdbox_map_atomic_context *atomic;
+ struct mdbox_map_transaction_context *map_trans;
+
+ ARRAY(struct dbox_save_mail) mails;
+};
+
+#define MDBOX_SAVECTX(s) container_of(DBOX_SAVECTX(s), struct mdbox_save_context, ctx)
+
+static struct dbox_file *
+mdbox_copy_file_get_file(struct mailbox_transaction_context *t,
+ uint32_t seq, uoff_t *offset_r)
+{
+ struct mdbox_save_context *ctx = MDBOX_SAVECTX(t->save_ctx);
+ const struct mdbox_mail_index_record *rec;
+ const void *data;
+ uint32_t file_id;
+
+ mail_index_lookup_ext(t->view, seq, ctx->mbox->ext_id, &data, NULL);
+ rec = data;
+
+ if (mdbox_map_lookup(ctx->mbox->storage->map, rec->map_uid,
+ &file_id, offset_r) < 0)
+ i_unreached();
+
+ return mdbox_file_init(ctx->mbox->storage, file_id);
+}
+
+struct dbox_file *
+mdbox_save_file_get_file(struct mailbox_transaction_context *t,
+ uint32_t seq, uoff_t *offset_r)
+{
+ struct mdbox_save_context *ctx = MDBOX_SAVECTX(t->save_ctx);
+ const struct dbox_save_mail *mails, *mail;
+ unsigned int count;
+
+ mails = array_get(&ctx->mails, &count);
+ i_assert(count > 0);
+ i_assert(seq >= mails[0].seq);
+
+ mail = &mails[seq - mails[0].seq];
+ i_assert(mail->seq == seq);
+
+ if (mail->file_append == NULL) {
+ /* copied mail */
+ return mdbox_copy_file_get_file(t, seq, offset_r);
+ }
+
+ /* saved mail */
+ i_assert(mail->written_to_disk);
+ if (dbox_file_append_flush(mail->file_append) < 0)
+ ctx->ctx.failed = TRUE;
+
+ mail->file_append->file->refcount++;
+ *offset_r = mail->append_offset;
+ return mail->file_append->file;
+}
+
+struct mail_save_context *
+mdbox_save_alloc(struct mailbox_transaction_context *t)
+{
+ struct mdbox_mailbox *mbox = MDBOX_MAILBOX(t->box);
+ struct mdbox_save_context *ctx;
+
+ i_assert((t->flags & MAILBOX_TRANSACTION_FLAG_EXTERNAL) != 0);
+
+ if (t->save_ctx != NULL) {
+ /* use the existing allocated structure */
+ ctx = MDBOX_SAVECTX(t->save_ctx);
+ ctx->cur_file = NULL;
+ ctx->ctx.failed = FALSE;
+ ctx->ctx.finished = FALSE;
+ ctx->ctx.dbox_output = NULL;
+ ctx->cur_file_append = NULL;
+ return &ctx->ctx.ctx;
+ }
+
+ ctx = i_new(struct mdbox_save_context, 1);
+ ctx->ctx.ctx.transaction = t;
+ ctx->ctx.trans = t->itrans;
+ ctx->mbox = mbox;
+ ctx->atomic = mdbox_map_atomic_begin(mbox->storage->map);
+ ctx->append_ctx = mdbox_map_append_begin(ctx->atomic);
+ i_array_init(&ctx->mails, 32);
+ t->save_ctx = &ctx->ctx.ctx;
+ return t->save_ctx;
+}
+
+int mdbox_save_begin(struct mail_save_context *_ctx, struct istream *input)
+{
+ struct mdbox_save_context *ctx = MDBOX_SAVECTX(_ctx);
+ struct dbox_save_mail *save_mail;
+ uoff_t mail_size, append_offset;
+
+ /* get the size of the mail to be saved, if possible */
+ if (i_stream_get_size(input, TRUE, &mail_size) <= 0) {
+ /* we couldn't find out the exact size. fallback to non-exact,
+ maybe it'll give something useful. the mail size is used
+ only to figure out if it's causing mdbox file to grow
+ too large. */
+ if (i_stream_get_size(input, FALSE, &mail_size) <= 0)
+ mail_size = 0;
+ }
+ if (mdbox_map_append_next(ctx->append_ctx, mail_size, 0,
+ &ctx->cur_file_append,
+ &ctx->ctx.dbox_output) < 0) {
+ ctx->ctx.failed = TRUE;
+ return -1;
+ }
+ i_assert(ctx->ctx.dbox_output->offset <= (uint32_t)-1);
+ append_offset = ctx->ctx.dbox_output->offset;
+
+ ctx->cur_file = ctx->cur_file_append->file;
+ dbox_save_begin(&ctx->ctx, input);
+
+ save_mail = array_append_space(&ctx->mails);
+ save_mail->file_append = ctx->cur_file_append;
+ save_mail->seq = ctx->ctx.seq;
+ save_mail->append_offset = append_offset;
+ return ctx->ctx.failed ? -1 : 0;
+}
+
+static int mdbox_save_mail_write_metadata(struct mdbox_save_context *ctx,
+ struct dbox_save_mail *mail)
+{
+ struct dbox_file *file = mail->file_append->file;
+ struct dbox_message_header dbox_msg_hdr;
+ uoff_t message_size;
+ guid_128_t guid_128;
+
+ i_assert(file->msg_header_size == sizeof(dbox_msg_hdr));
+
+ message_size = ctx->ctx.dbox_output->offset -
+ mail->append_offset - mail->file_append->file->msg_header_size;
+
+ dbox_save_write_metadata(&ctx->ctx.ctx, ctx->ctx.dbox_output,
+ message_size, ctx->mbox->box.name, guid_128);
+ /* save the 128bit GUID to index so if the map index gets corrupted
+ we can still find the message */
+ mail_index_update_ext(ctx->ctx.trans, ctx->ctx.seq,
+ ctx->mbox->guid_ext_id, guid_128, NULL);
+
+ dbox_msg_header_fill(&dbox_msg_hdr, message_size);
+ if (o_stream_pwrite(ctx->ctx.dbox_output, &dbox_msg_hdr,
+ sizeof(dbox_msg_hdr), mail->append_offset) < 0) {
+ dbox_file_set_syscall_error(file, "pwrite()");
+ return -1;
+ }
+ mail->written_to_disk = TRUE;
+ mail->save_date = ctx->ctx.ctx.data.save_date;
+ return 0;
+}
+
+static int mdbox_save_finish_write(struct mail_save_context *_ctx)
+{
+ struct mdbox_save_context *ctx = (struct mdbox_save_context *)_ctx;
+ struct dbox_save_mail *mail;
+
+ ctx->ctx.finished = TRUE;
+ if (ctx->ctx.dbox_output == NULL)
+ return -1;
+
+ dbox_save_end(&ctx->ctx);
+
+ mail = array_back_modifiable(&ctx->mails);
+ if (!ctx->ctx.failed) T_BEGIN {
+ if (mdbox_save_mail_write_metadata(ctx, mail) < 0)
+ ctx->ctx.failed = TRUE;
+ else
+ mdbox_map_append_finish(ctx->append_ctx);
+ } T_END;
+
+ if (mail->file_append->file->input != NULL) {
+ /* if we try to read the saved mail before unlocking file,
+ make sure the input stream doesn't have stale data */
+ i_stream_sync(mail->file_append->file->input);
+ }
+ i_stream_unref(&ctx->ctx.input);
+
+ if (ctx->ctx.failed) {
+ index_storage_save_abort_last(&ctx->ctx.ctx, ctx->ctx.seq);
+ mdbox_map_append_abort(ctx->append_ctx);
+ array_pop_back(&ctx->mails);
+ return -1;
+ }
+ return 0;
+}
+
+int mdbox_save_finish(struct mail_save_context *ctx)
+{
+ int ret;
+
+ ret = mdbox_save_finish_write(ctx);
+ index_save_context_free(ctx);
+ return ret;
+}
+
+void mdbox_save_cancel(struct mail_save_context *_ctx)
+{
+ struct dbox_save_context *ctx = DBOX_SAVECTX(_ctx);
+
+ ctx->failed = TRUE;
+ (void)mdbox_save_finish(_ctx);
+}
+
+static void
+mdbox_save_set_map_uids(struct mdbox_save_context *ctx,
+ uint32_t first_map_uid, uint32_t last_map_uid)
+{
+ struct mdbox_mailbox *mbox = ctx->mbox;
+ struct mail_index_view *view = ctx->ctx.ctx.transaction->view;
+ const struct mdbox_mail_index_record *old_rec;
+ struct mdbox_mail_index_record rec;
+ const struct dbox_save_mail *mails;
+ unsigned int i, count;
+ const void *data;
+ uint32_t next_map_uid = first_map_uid;
+
+ mdbox_update_header(mbox, ctx->ctx.trans, NULL);
+
+ i_zero(&rec);
+ mails = array_get(&ctx->mails, &count);
+ for (i = 0; i < count; i++) {
+ mail_index_lookup_ext(view, mails[i].seq, mbox->ext_id,
+ &data, NULL);
+ old_rec = data;
+ if (old_rec != NULL && old_rec->map_uid != 0) {
+ /* message was copied. keep the existing map uid */
+ continue;
+ }
+
+ if (mails[i].save_date > 0)
+ rec.save_date = mails[i].save_date;
+ else
+ rec.save_date = ioloop_time;
+ rec.map_uid = next_map_uid++;
+ mail_index_update_ext(ctx->ctx.trans, mails[i].seq,
+ mbox->ext_id, &rec, NULL);
+ }
+ i_assert(next_map_uid == last_map_uid + 1);
+}
+
+int mdbox_transaction_save_commit_pre(struct mail_save_context *_ctx)
+{
+ struct mdbox_save_context *ctx = MDBOX_SAVECTX(_ctx);
+ struct mailbox_transaction_context *_t = _ctx->transaction;
+ const struct mail_index_header *hdr;
+ uint32_t first_map_uid, last_map_uid;
+
+ i_assert(ctx->ctx.finished);
+
+ /* flush/fsync writes to m.* files before locking the map */
+ if (mdbox_map_append_flush(ctx->append_ctx) < 0) {
+ mdbox_transaction_save_rollback(_ctx);
+ return -1;
+ }
+
+ /* make sure the map gets locked */
+ if (mdbox_map_atomic_lock(ctx->atomic, "saving") < 0) {
+ mdbox_transaction_save_rollback(_ctx);
+ return -1;
+ }
+ /* lock the mailbox after map to avoid deadlocks. if we've noticed
+ any corruption, deal with it later, otherwise we won't have
+ up-to-date atomic->sync_view */
+ if (mdbox_sync_begin(ctx->mbox, MDBOX_SYNC_FLAG_NO_PURGE |
+ MDBOX_SYNC_FLAG_FORCE |
+ MDBOX_SYNC_FLAG_FSYNC |
+ MDBOX_SYNC_FLAG_NO_REBUILD, ctx->atomic,
+ &ctx->sync_ctx) < 0) {
+ mdbox_transaction_save_rollback(_ctx);
+ return -1;
+ }
+ i_assert(ctx->sync_ctx != NULL);
+
+ /* assign map UIDs for newly saved messages after we've successfully
+ acquired all the locks. the transaction is now very unlikely to
+ fail. the UIDs are written to the transaction log immediately within
+ this function, but the map is left locked. */
+ if (mdbox_map_append_assign_map_uids(ctx->append_ctx, &first_map_uid,
+ &last_map_uid) < 0) {
+ mdbox_transaction_save_rollback(_ctx);
+ return -1;
+ }
+
+ /* update dbox header flags */
+ dbox_save_update_header_flags(&ctx->ctx, ctx->sync_ctx->sync_view,
+ ctx->mbox->hdr_ext_id, offsetof(struct mdbox_index_header, flags));
+
+ /* assign UIDs for new messages */
+ hdr = mail_index_get_header(ctx->sync_ctx->sync_view);
+ mail_index_append_finish_uids(ctx->ctx.trans, hdr->next_uid,
+ &_t->changes->saved_uids);
+
+ if (ctx->ctx.highest_pop3_uidl_seq != 0) {
+ const struct dbox_save_mail *mails;
+ struct seq_range_iter iter;
+ unsigned int highest_pop3_uidl_idx;
+ uint32_t uid;
+
+ mails = array_front(&ctx->mails);
+ highest_pop3_uidl_idx =
+ ctx->ctx.highest_pop3_uidl_seq - mails[0].seq;
+ i_assert(mails[highest_pop3_uidl_idx].seq == ctx->ctx.highest_pop3_uidl_seq);
+
+ seq_range_array_iter_init(&iter, &_t->changes->saved_uids);
+ if (!seq_range_array_iter_nth(&iter, highest_pop3_uidl_idx, &uid))
+ i_unreached();
+ index_pop3_uidl_set_max_uid(&ctx->mbox->box, ctx->ctx.trans, uid);
+ }
+
+ /* save map UIDs to mailbox index */
+ if (first_map_uid != 0)
+ mdbox_save_set_map_uids(ctx, first_map_uid, last_map_uid);
+
+ /* increase map's refcount for copied mails */
+ if (array_is_created(&ctx->copy_map_uids)) {
+ ctx->map_trans = mdbox_map_transaction_begin(ctx->atomic, FALSE);
+ if (mdbox_map_update_refcounts(ctx->map_trans,
+ &ctx->copy_map_uids, 1) < 0) {
+ mdbox_transaction_save_rollback(_ctx);
+ return -1;
+ }
+ mail_index_sync_set_reason(ctx->sync_ctx->index_sync_ctx, "copying");
+ } else {
+ mail_index_sync_set_reason(ctx->sync_ctx->index_sync_ctx, "saving");
+ }
+
+ _t->changes->uid_validity = hdr->uid_validity;
+ return 0;
+}
+
+void mdbox_transaction_save_commit_post(struct mail_save_context *_ctx,
+ struct mail_index_transaction_commit_result *result)
+{
+ struct mdbox_save_context *ctx = MDBOX_SAVECTX(_ctx);
+ struct mail_storage *storage = _ctx->transaction->box->storage;
+
+ _ctx->transaction = NULL; /* transaction is already freed */
+
+ mail_index_sync_set_commit_result(ctx->sync_ctx->index_sync_ctx,
+ result);
+
+ /* finish writing the mailbox APPENDs */
+ if (mdbox_sync_finish(&ctx->sync_ctx, TRUE) == 0) {
+ /* commit refcount increases for copied mails */
+ if (ctx->map_trans != NULL) {
+ if (mdbox_map_transaction_commit(ctx->map_trans, "copy refcount updates") < 0)
+ mdbox_map_atomic_set_failed(ctx->atomic);
+ }
+ /* flush file append writes */
+ if (mdbox_map_append_commit(ctx->append_ctx) < 0)
+ mdbox_map_atomic_set_failed(ctx->atomic);
+ }
+ mdbox_map_append_free(&ctx->append_ctx);
+ /* update the sync tail offset, everything else
+ was already written at this point. */
+ (void)mdbox_map_atomic_finish(&ctx->atomic);
+
+ if (storage->set->parsed_fsync_mode != FSYNC_MODE_NEVER) {
+ const char *box_path = mailbox_get_path(&ctx->mbox->box);
+
+ if (fdatasync_path(box_path) < 0) {
+ mail_set_critical(_ctx->dest_mail,
+ "fdatasync_path(%s) failed: %m", box_path);
+ }
+ }
+ mdbox_transaction_save_rollback(_ctx);
+}
+
+void mdbox_transaction_save_rollback(struct mail_save_context *_ctx)
+{
+ struct mdbox_save_context *ctx = MDBOX_SAVECTX(_ctx);
+
+ if (!ctx->ctx.finished)
+ mdbox_save_cancel(&ctx->ctx.ctx);
+ if (ctx->append_ctx != NULL)
+ mdbox_map_append_free(&ctx->append_ctx);
+ if (ctx->map_trans != NULL)
+ mdbox_map_transaction_free(&ctx->map_trans);
+ if (ctx->atomic != NULL)
+ (void)mdbox_map_atomic_finish(&ctx->atomic);
+ if (array_is_created(&ctx->copy_map_uids))
+ array_free(&ctx->copy_map_uids);
+
+ if (ctx->sync_ctx != NULL)
+ (void)mdbox_sync_finish(&ctx->sync_ctx, FALSE);
+
+ array_free(&ctx->mails);
+ i_free(ctx);
+}
+
+int mdbox_copy(struct mail_save_context *_ctx, struct mail *mail)
+{
+ struct mdbox_save_context *ctx = MDBOX_SAVECTX(_ctx);
+ struct dbox_save_mail *save_mail;
+ struct mdbox_mailbox *src_mbox;
+ struct mdbox_mail_index_record rec;
+ const void *guid_data;
+ guid_128_t wanted_guid;
+
+ ctx->ctx.finished = TRUE;
+
+ if (mail->box->storage != _ctx->transaction->box->storage ||
+ _ctx->transaction->box->disable_reflink_copy_to)
+ return mail_storage_copy(_ctx, mail);
+ src_mbox = MDBOX_MAILBOX(mail->box);
+
+ i_zero(&rec);
+ rec.save_date = ioloop_time;
+ if (mdbox_mail_lookup(src_mbox, mail->transaction->view, mail->seq,
+ &rec.map_uid) < 0) {
+ index_save_context_free(_ctx);
+ return -1;
+ }
+
+ mail_index_lookup_ext(mail->transaction->view, mail->seq,
+ src_mbox->guid_ext_id, &guid_data, NULL);
+ if (guid_data == NULL || guid_128_is_empty(guid_data)) {
+ /* missing GUID, something's broken. don't copy using
+ refcounting. */
+ return mail_storage_copy(_ctx, mail);
+ } else if (_ctx->data.guid != NULL &&
+ (guid_128_from_string(_ctx->data.guid, wanted_guid) < 0 ||
+ memcmp(guid_data, wanted_guid, sizeof(wanted_guid)) != 0)) {
+ /* GUID change requested. we can't do it with refcount
+ copying */
+ return mail_storage_copy(_ctx, mail);
+ }
+
+ /* remember the map_uid so we can later increase its refcount */
+ if (!array_is_created(&ctx->copy_map_uids))
+ i_array_init(&ctx->copy_map_uids, 32);
+ array_push_back(&ctx->copy_map_uids, &rec.map_uid);
+
+ /* add message to mailbox index */
+ dbox_save_add_to_index(&ctx->ctx);
+ mail_index_update_ext(ctx->ctx.trans, ctx->ctx.seq,
+ ctx->mbox->ext_id, &rec, NULL);
+
+ mail_index_update_ext(ctx->ctx.trans, ctx->ctx.seq,
+ ctx->mbox->guid_ext_id, guid_data, NULL);
+ index_copy_cache_fields(_ctx, mail, ctx->ctx.seq);
+
+ save_mail = array_append_space(&ctx->mails);
+ save_mail->seq = ctx->ctx.seq;
+
+ mail_set_seq_saving(_ctx->dest_mail, ctx->ctx.seq);
+ index_save_context_free(_ctx);
+ return 0;
+}
diff --git a/src/lib-storage/index/dbox-multi/mdbox-settings.c b/src/lib-storage/index/dbox-multi/mdbox-settings.c
new file mode 100644
index 0000000..dc9e989
--- /dev/null
+++ b/src/lib-storage/index/dbox-multi/mdbox-settings.c
@@ -0,0 +1,43 @@
+/* Copyright (c) 2006-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "settings-parser.h"
+#include "mail-storage-settings.h"
+#include "mdbox-settings.h"
+
+#include <stddef.h>
+
+#undef DEF
+#define DEF(type, name) \
+ SETTING_DEFINE_STRUCT_##type(#name, name, struct mdbox_settings)
+
+static const struct setting_define mdbox_setting_defines[] = {
+ DEF(BOOL, mdbox_preallocate_space),
+ DEF(SIZE, mdbox_rotate_size),
+ DEF(TIME, mdbox_rotate_interval),
+
+ SETTING_DEFINE_LIST_END
+};
+
+static const struct mdbox_settings mdbox_default_settings = {
+ .mdbox_preallocate_space = FALSE,
+ .mdbox_rotate_size = 10*1024*1024,
+ .mdbox_rotate_interval = 0
+};
+
+static const struct setting_parser_info mdbox_setting_parser_info = {
+ .module_name = "mdbox",
+ .defines = mdbox_setting_defines,
+ .defaults = &mdbox_default_settings,
+
+ .type_offset = SIZE_MAX,
+ .struct_size = sizeof(struct mdbox_settings),
+
+ .parent_offset = SIZE_MAX,
+ .parent = &mail_user_setting_parser_info
+};
+
+const struct setting_parser_info *mdbox_get_setting_parser_info(void)
+{
+ return &mdbox_setting_parser_info;
+}
diff --git a/src/lib-storage/index/dbox-multi/mdbox-settings.h b/src/lib-storage/index/dbox-multi/mdbox-settings.h
new file mode 100644
index 0000000..353da69
--- /dev/null
+++ b/src/lib-storage/index/dbox-multi/mdbox-settings.h
@@ -0,0 +1,12 @@
+#ifndef MDBOX_SETTINGS_H
+#define MDBOX_SETTINGS_H
+
+struct mdbox_settings {
+ bool mdbox_preallocate_space;
+ uoff_t mdbox_rotate_size;
+ unsigned int mdbox_rotate_interval;
+};
+
+const struct setting_parser_info *mdbox_get_setting_parser_info(void);
+
+#endif
diff --git a/src/lib-storage/index/dbox-multi/mdbox-storage-rebuild.c b/src/lib-storage/index/dbox-multi/mdbox-storage-rebuild.c
new file mode 100644
index 0000000..4277a1a
--- /dev/null
+++ b/src/lib-storage/index/dbox-multi/mdbox-storage-rebuild.c
@@ -0,0 +1,1005 @@
+/* Copyright (c) 2009-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "ioloop.h"
+#include "istream.h"
+#include "hash.h"
+#include "str.h"
+#include "mail-cache.h"
+#include "index-rebuild.h"
+#include "mail-namespace.h"
+#include "mailbox-list-private.h"
+#include "mdbox-storage.h"
+#include "mdbox-file.h"
+#include "mdbox-map-private.h"
+#include "mdbox-sync.h"
+#include "mdbox-storage-rebuild.h"
+
+#include <dirent.h>
+#include <unistd.h>
+
+#define REBUILD_MAX_REFCOUNT 32768
+
+struct mdbox_rebuild_msg {
+ struct mdbox_rebuild_msg *guid_hash_next;
+
+ guid_128_t guid_128;
+ uint32_t file_id;
+ uint32_t offset;
+ uint32_t rec_size;
+ uoff_t mail_size;
+ uint32_t map_uid;
+
+ uint16_t refcount;
+ bool seen_zero_ref_in_map:1;
+};
+
+struct rebuild_msg_mailbox {
+ struct mailbox *box;
+ struct mail_index_sync_ctx *sync_ctx;
+ struct mail_index_view *view;
+ struct mail_index_transaction *trans;
+ uint32_t next_uid;
+};
+
+struct mdbox_storage_rebuild_context {
+ struct mdbox_storage *storage;
+ struct mdbox_map_atomic_context *atomic;
+ pool_t pool;
+
+ struct mdbox_map_mail_index_header orig_map_hdr;
+ HASH_TABLE(uint8_t *, struct mdbox_rebuild_msg *) guid_hash;
+ ARRAY(struct mdbox_rebuild_msg *) msgs;
+ ARRAY_TYPE(seq_range) seen_file_ids;
+
+ uint32_t rebuild_count;
+ uint32_t highest_file_id;
+
+ struct mailbox_list *default_list;
+
+ struct rebuild_msg_mailbox prev_msg;
+
+ bool have_pop3_uidls:1;
+ bool have_pop3_orders:1;
+};
+
+static struct mdbox_storage_rebuild_context *
+mdbox_storage_rebuild_init(struct mdbox_storage *storage,
+ struct mdbox_map_atomic_context *atomic)
+{
+ struct mdbox_storage_rebuild_context *ctx;
+
+ i_assert(!storage->rebuilding_storage);
+
+ ctx = i_new(struct mdbox_storage_rebuild_context, 1);
+ ctx->storage = storage;
+ ctx->atomic = atomic;
+ ctx->pool = pool_alloconly_create("dbox map rebuild", 1024*256);
+ hash_table_create(&ctx->guid_hash, ctx->pool, 0,
+ guid_128_hash, guid_128_cmp);
+ i_array_init(&ctx->msgs, 512);
+ i_array_init(&ctx->seen_file_ids, 128);
+
+ ctx->storage->rebuilding_storage = TRUE;
+ return ctx;
+}
+
+static void
+mdbox_storage_rebuild_deinit(struct mdbox_storage_rebuild_context *ctx)
+{
+ i_assert(ctx->storage->rebuilding_storage);
+
+ ctx->storage->rebuilding_storage = FALSE;
+
+ hash_table_destroy(&ctx->guid_hash);
+ pool_unref(&ctx->pool);
+ array_free(&ctx->seen_file_ids);
+ array_free(&ctx->msgs);
+ i_free(ctx);
+}
+
+static int
+mdbox_rebuild_msg_offset_cmp(struct mdbox_rebuild_msg *const *m1,
+ struct mdbox_rebuild_msg *const *m2)
+{
+ if ((*m1)->file_id < (*m2)->file_id)
+ return -1;
+ if ((*m1)->file_id > (*m2)->file_id)
+ return 1;
+
+ if ((*m1)->offset < (*m2)->offset)
+ return -1;
+ if ((*m1)->offset > (*m2)->offset)
+ return 1;
+
+ if ((*m1)->rec_size < (*m2)->rec_size)
+ return -1;
+ if ((*m1)->rec_size > (*m2)->rec_size)
+ return 1;
+ return 0;
+}
+
+static int mdbox_rebuild_msg_uid_cmp(struct mdbox_rebuild_msg *const *m1,
+ struct mdbox_rebuild_msg *const *m2)
+{
+ if ((*m1)->map_uid < (*m2)->map_uid)
+ return -1;
+ if ((*m1)->map_uid > (*m2)->map_uid)
+ return 1;
+ return 0;
+}
+
+static void rebuild_scan_metadata(struct mdbox_storage_rebuild_context *ctx,
+ struct dbox_file *file)
+{
+ if (dbox_file_metadata_get(file, DBOX_METADATA_POP3_UIDL) != NULL)
+ ctx->have_pop3_uidls = TRUE;
+ if (dbox_file_metadata_get(file, DBOX_METADATA_POP3_ORDER) != NULL)
+ ctx->have_pop3_orders = TRUE;
+}
+
+static int rebuild_file_mails(struct mdbox_storage_rebuild_context *ctx,
+ struct dbox_file *file, uint32_t file_id)
+{
+ const char *guid;
+ uint8_t *guid_p;
+ struct mdbox_rebuild_msg *rec, *old_rec;
+ uoff_t offset, prev_offset;
+ bool last, first, fixed = FALSE;
+ int ret;
+
+ dbox_file_seek_rewind(file);
+ prev_offset = 0;
+ while ((ret = dbox_file_seek_next(file, &offset, &last)) >= 0) {
+ if (ret > 0) {
+ if ((ret = dbox_file_metadata_read(file)) < 0)
+ break;
+ }
+
+ if (ret == 0) {
+ /* file is corrupted. fix it and retry. */
+ if (fixed || last)
+ break;
+ first = prev_offset == 0;
+ if (prev_offset == 0) {
+ /* use existing file header if it was ok */
+ prev_offset = offset;
+ }
+ if ((ret = dbox_file_fix(file, prev_offset)) < 0)
+ break;
+ if (ret == 0) {
+ /* file was deleted */
+ return 1;
+ }
+ fixed = TRUE;
+ if (!first) {
+ /* seek to the offset where we last left off */
+ ret = dbox_file_seek(file, prev_offset);
+ if (ret <= 0)
+ break;
+ }
+ continue;
+ }
+ prev_offset = offset;
+
+ guid = dbox_file_metadata_get(file, DBOX_METADATA_GUID);
+ if (guid == NULL || *guid == '\0') {
+ dbox_file_set_corrupted(file,
+ "Message is missing GUID");
+ ret = 0;
+ break;
+ }
+ rebuild_scan_metadata(ctx, file);
+
+ rec = p_new(ctx->pool, struct mdbox_rebuild_msg, 1);
+ rec->file_id = file_id;
+ rec->offset = offset;
+ rec->rec_size = file->input->v_offset - offset;
+ rec->mail_size = dbox_file_get_plaintext_size(file);
+ mail_generate_guid_128_hash(guid, rec->guid_128);
+ i_assert(!guid_128_is_empty(rec->guid_128));
+ array_push_back(&ctx->msgs, &rec);
+
+ guid_p = rec->guid_128;
+ old_rec = hash_table_lookup(ctx->guid_hash, guid_p);
+ if (old_rec == NULL)
+ hash_table_insert(ctx->guid_hash, guid_p, rec);
+ else if (rec->mail_size == old_rec->mail_size) {
+ /* two mails' GUID and size are the same, which quite
+ likely means that their contents are the same as
+ well. we'll compare the mail sizes instead of the
+ record sizes, because the records' metadata may
+ differ.
+
+ save this duplicate mail with refcount=0 to the map,
+ so it will eventually be purged. */
+ rec->seen_zero_ref_in_map = TRUE;
+ } else {
+ /* duplicate GUID, but not a duplicate message. */
+ i_error("mdbox %s: Duplicate GUID %s in "
+ "m.%u:%u (size=%"PRIuUOFF_T") and m.%u:%u "
+ "(size=%"PRIuUOFF_T")",
+ ctx->storage->storage_dir, guid,
+ old_rec->file_id, old_rec->offset, old_rec->mail_size,
+ rec->file_id, rec->offset, rec->mail_size);
+ rec->guid_hash_next = old_rec->guid_hash_next;
+ old_rec->guid_hash_next = rec;
+ }
+ }
+ if (ret < 0)
+ return -1;
+ else if (ret == 0 && !last)
+ return 0;
+ else
+ return 1;
+}
+
+static int
+rebuild_rename_file(struct mdbox_storage_rebuild_context *ctx,
+ const char *dir, const char **fname_p, uint32_t *file_id_r)
+{
+ const char *old_path, *new_path, *fname = *fname_p;
+
+ old_path = t_strconcat(dir, "/", fname, NULL);
+ do {
+ new_path = t_strdup_printf("%s/"MDBOX_MAIL_FILE_FORMAT,
+ dir, ++ctx->highest_file_id);
+ /* use link()+unlink() instead of rename() to make sure we
+ don't overwrite any files. */
+ if (link(old_path, new_path) == 0) {
+ i_unlink(old_path);
+ *fname_p = strrchr(new_path, '/') + 1;
+ *file_id_r = ctx->highest_file_id;
+ return 0;
+ }
+ } while (errno == EEXIST);
+
+ i_error("link(%s, %s) failed: %m", old_path, new_path);
+ return -1;
+}
+
+static int rebuild_add_file(struct mdbox_storage_rebuild_context *ctx,
+ const char *dir, const char *fname)
+{
+ struct dbox_file *file;
+ uint32_t file_id;
+ const char *id_str, *ext;
+ bool deleted;
+ int ret = 0;
+
+ id_str = fname + strlen(MDBOX_MAIL_FILE_PREFIX);
+ if (str_to_uint32(id_str, &file_id) < 0 || file_id == 0) {
+ /* m.*.broken files are created by file fixing
+ m.*.lock files are created if flock() isn't available */
+ ext = strrchr(id_str, '.');
+ if (ext == NULL || (strcmp(ext, ".broken") != 0 &&
+ strcmp(ext, ".lock") != 0)) {
+ i_warning("mdbox rebuild: "
+ "Skipping file with missing ID: %s/%s",
+ dir, fname);
+ }
+ return 0;
+ }
+ if (!seq_range_exists(&ctx->seen_file_ids, file_id)) {
+ if (ctx->highest_file_id < file_id)
+ ctx->highest_file_id = file_id;
+ } else {
+ /* duplicate file. either readdir() returned it twice
+ (unlikely) or it exists in both alt and primary storage.
+ to make sure we don't lose any mails from either of the
+ files, give this file a new ID and rename it. */
+ if (rebuild_rename_file(ctx, dir, &fname, &file_id) < 0)
+ return -1;
+ }
+ seq_range_array_add(&ctx->seen_file_ids, file_id);
+
+ file = mdbox_file_init(ctx->storage, file_id);
+ if ((ret = dbox_file_open(file, &deleted)) > 0 && !deleted)
+ ret = rebuild_file_mails(ctx, file, file_id);
+ if (ret == 0)
+ i_error("mdbox rebuild: Failed to fix file %s/%s", dir, fname);
+ dbox_file_unref(&file);
+ return ret < 0 ? -1 : 0;
+}
+
+static void
+rebuild_add_missing_map_uids(struct mdbox_storage_rebuild_context *ctx,
+ uint32_t next_uid)
+{
+ struct mdbox_rebuild_msg **msgs;
+ struct mdbox_map_mail_index_record rec;
+ unsigned int i, count;
+ uint32_t seq;
+
+ i_zero(&rec);
+ msgs = array_get_modifiable(&ctx->msgs, &count);
+ for (i = 0; i < count; i++) {
+ if (msgs[i]->map_uid != 0)
+ continue;
+
+ rec.file_id = msgs[i]->file_id;
+ rec.offset = msgs[i]->offset;
+ rec.size = msgs[i]->rec_size;
+
+ msgs[i]->map_uid = next_uid++;
+ mail_index_append(ctx->atomic->sync_trans,
+ msgs[i]->map_uid, &seq);
+ mail_index_update_ext(ctx->atomic->sync_trans, seq,
+ ctx->storage->map->map_ext_id,
+ &rec, NULL);
+ }
+}
+
+static void rebuild_apply_map(struct mdbox_storage_rebuild_context *ctx)
+{
+ struct mdbox_map *map = ctx->storage->map;
+ const struct mail_index_header *hdr;
+ struct mdbox_rebuild_msg **pos;
+ struct mdbox_rebuild_msg search_msg, *search_msgp = &search_msg;
+ struct dbox_mail_lookup_rec rec;
+ uint32_t seq;
+
+ array_sort(&ctx->msgs, mdbox_rebuild_msg_offset_cmp);
+ /* msgs now contains a list of all messages that exists in m.* files,
+ sorted by file_id,offset */
+
+ hdr = mail_index_get_header(ctx->atomic->sync_view);
+ for (seq = 1; seq <= hdr->messages_count; seq++) {
+ if (mdbox_map_view_lookup_rec(map, ctx->atomic->sync_view,
+ seq, &rec) < 0) {
+ /* map or ref extension is missing from the index.
+ Just ignore the file entirely. (Don't try to
+ continue with other records, since they'll fail
+ as well, and each failure logs the same error.) */
+ i_assert(seq == 1);
+ break;
+ }
+
+ /* look up the rebuild msg record for this message based on
+ the (file_id, offset, size) triplet */
+ search_msg.file_id = rec.rec.file_id;
+ search_msg.offset = rec.rec.offset;
+ search_msg.rec_size = rec.rec.size;
+ pos = array_bsearch(&ctx->msgs, &search_msgp,
+ mdbox_rebuild_msg_offset_cmp);
+ if (pos == NULL || (*pos)->map_uid != 0) {
+ /* map record points to nonexistent or
+ a duplicate message. */
+ mail_index_expunge(ctx->atomic->sync_trans, seq);
+ } else {
+ /* remember this message's map_uid */
+ (*pos)->map_uid = rec.map_uid;
+ if (rec.refcount == 0)
+ (*pos)->seen_zero_ref_in_map = TRUE;
+ }
+ }
+ rebuild_add_missing_map_uids(ctx, hdr->next_uid);
+
+ /* afterwards we're interested in looking up map_uids.
+ re-sort the messages to make it easier. */
+ array_sort(&ctx->msgs, mdbox_rebuild_msg_uid_cmp);
+}
+
+static struct mdbox_rebuild_msg *
+rebuild_lookup_map_uid(struct mdbox_storage_rebuild_context *ctx,
+ uint32_t map_uid)
+{
+ struct mdbox_rebuild_msg search_msg, *search_msgp = &search_msg;
+ struct mdbox_rebuild_msg **pos;
+
+ search_msg.map_uid = map_uid;
+ pos = array_bsearch(&ctx->msgs, &search_msgp,
+ mdbox_rebuild_msg_uid_cmp);
+ return pos == NULL ? NULL : *pos;
+}
+
+static bool
+guid_hash_have_map_uid(struct mdbox_rebuild_msg **recp, uint32_t map_uid)
+{
+ struct mdbox_rebuild_msg *rec;
+
+ for (rec = *recp; rec != NULL; rec = rec->guid_hash_next) {
+ if (rec->map_uid == map_uid) {
+ *recp = rec;
+ return TRUE;
+ }
+ }
+ return FALSE;
+}
+
+static void
+rebuild_mailbox_multi(struct mdbox_storage_rebuild_context *ctx,
+ struct index_rebuild_context *rebuild_ctx,
+ struct mdbox_mailbox *mbox,
+ struct mail_index_view *view,
+ struct mail_index_transaction *trans)
+{
+ struct mdbox_mail_index_record new_dbox_rec;
+ const struct mail_index_header *hdr;
+ struct mdbox_rebuild_msg *rec;
+ const void *data;
+ const uint8_t *guid_p;
+ uint32_t old_seq, new_seq, uid, map_uid;
+
+ /* Rebuild the mailbox's index. Note that index is reset at this point,
+ so although we can still access the old messages, we'll need to
+ append anything we want to keep as new messages. */
+ hdr = mail_index_get_header(view);
+ for (old_seq = 1; old_seq <= hdr->messages_count; old_seq++) {
+ mail_index_lookup_ext(view, old_seq, mbox->ext_id,
+ &data, NULL);
+ if (data == NULL) {
+ i_zero(&new_dbox_rec);
+ map_uid = 0;
+ } else {
+ memcpy(&new_dbox_rec, data, sizeof(new_dbox_rec));
+ map_uid = new_dbox_rec.map_uid;
+ }
+
+ mail_index_lookup_ext(view, old_seq, mbox->guid_ext_id,
+ &data, NULL);
+ guid_p = data;
+
+ /* see if we can find this message based on
+ 1) GUID, 2) map_uid */
+ rec = guid_p == NULL ? NULL :
+ hash_table_lookup(ctx->guid_hash, guid_p);
+ if (rec == NULL) {
+ /* multi-dbox message that wasn't found with GUID.
+ either it's lost or GUID has been corrupted. we can
+ still try to look it up using map_uid. */
+ rec = map_uid == 0 ? NULL :
+ rebuild_lookup_map_uid(ctx, map_uid);
+ map_uid = rec == NULL ? 0 : rec->map_uid;
+ } else if (!guid_hash_have_map_uid(&rec, map_uid)) {
+ /* message's GUID and map_uid point to different
+ physical messages. assume that GUID is correct and
+ map_uid is wrong. */
+ map_uid = rec->map_uid;
+ } else {
+ /* everything was ok. use this specific record's
+ map_uid to avoid duplicating mails in case the same
+ GUID exists multiple times */
+ }
+
+ if (rec != NULL &&
+ rec->refcount < REBUILD_MAX_REFCOUNT) T_BEGIN {
+ /* keep this message. add it to mailbox index. */
+ i_assert(map_uid != 0);
+ rec->refcount++;
+
+ mail_index_lookup_uid(view, old_seq, &uid);
+ mail_index_append(trans, uid, &new_seq);
+ index_rebuild_index_metadata(rebuild_ctx,
+ new_seq, uid);
+
+ new_dbox_rec.map_uid = map_uid;
+ mail_index_update_ext(trans, new_seq, mbox->ext_id,
+ &new_dbox_rec, NULL);
+ mail_index_update_ext(trans, new_seq, mbox->guid_ext_id,
+ rec->guid_128, NULL);
+ } T_END;
+ }
+}
+
+static void
+mdbox_rebuild_get_header(struct mail_index_view *view, uint32_t hdr_ext_id,
+ struct mdbox_index_header *hdr_r, bool *need_resize_r)
+{
+ const void *data;
+ size_t data_size;
+
+ mail_index_get_header_ext(view, hdr_ext_id, &data, &data_size);
+ i_zero(hdr_r);
+ memcpy(hdr_r, data, I_MIN(data_size, sizeof(*hdr_r)));
+ *need_resize_r = data_size < sizeof(*hdr_r);
+}
+
+static void mdbox_header_update(struct mdbox_storage_rebuild_context *ctx,
+ struct index_rebuild_context *rebuild_ctx,
+ struct mdbox_mailbox *mbox)
+{
+ struct mdbox_index_header hdr, backup_hdr;
+ bool need_resize, need_resize_backup;
+
+ mdbox_rebuild_get_header(rebuild_ctx->view, mbox->hdr_ext_id,
+ &hdr, &need_resize);
+ if (rebuild_ctx->backup_view == NULL) {
+ i_zero(&backup_hdr);
+ need_resize = TRUE;
+ } else {
+ mdbox_rebuild_get_header(rebuild_ctx->backup_view,
+ mbox->hdr_ext_id, &backup_hdr,
+ &need_resize_backup);
+ }
+
+ /* make sure we have valid mailbox guid */
+ if (guid_128_is_empty(hdr.mailbox_guid)) {
+ if (!guid_128_is_empty(backup_hdr.mailbox_guid)) {
+ memcpy(hdr.mailbox_guid, backup_hdr.mailbox_guid,
+ sizeof(hdr.mailbox_guid));
+ } else {
+ guid_128_generate(hdr.mailbox_guid);
+ }
+ }
+
+ /* update map's uid-validity */
+ hdr.map_uid_validity = mdbox_map_get_uid_validity(mbox->storage->map);
+
+ if (ctx->have_pop3_uidls)
+ hdr.flags |= DBOX_INDEX_HEADER_FLAG_HAVE_POP3_UIDLS;
+ if (ctx->have_pop3_orders)
+ hdr.flags |= DBOX_INDEX_HEADER_FLAG_HAVE_POP3_ORDERS;
+
+ /* and write changes */
+ if (need_resize) {
+ mail_index_ext_resize_hdr(rebuild_ctx->trans, mbox->hdr_ext_id,
+ sizeof(hdr));
+ }
+ mail_index_update_header_ext(rebuild_ctx->trans, mbox->hdr_ext_id, 0,
+ &hdr, sizeof(hdr));
+}
+
+static int
+rebuild_mailbox(struct mdbox_storage_rebuild_context *ctx,
+ struct mail_namespace *ns, const char *vname)
+{
+ struct mailbox *box;
+ struct mdbox_mailbox *mbox;
+ struct mail_index_sync_ctx *sync_ctx;
+ struct mail_index_view *view;
+ struct mail_index_transaction *trans;
+ struct index_rebuild_context *rebuild_ctx;
+ enum mail_error error;
+ int ret;
+
+ box = mailbox_alloc(ns->list, vname, MAILBOX_FLAG_READONLY |
+ MAILBOX_FLAG_IGNORE_ACLS);
+ if (box->storage != &ctx->storage->storage.storage) {
+ /* the namespace has multiple storages. */
+ mailbox_free(&box);
+ return 0;
+ }
+ if (mailbox_open(box) < 0) {
+ error = mailbox_get_last_mail_error(box);
+ i_error("Couldn't open mailbox '%s': %s",
+ vname, mailbox_get_last_internal_error(box, NULL));
+ mailbox_free(&box);
+ if (error == MAIL_ERROR_TEMP)
+ return -1;
+ /* non-temporary error, ignore */
+ return 0;
+ }
+ mbox = MDBOX_MAILBOX(box);
+
+ ret = mail_index_sync_begin(box->index, &sync_ctx, &view, &trans,
+ MAIL_INDEX_SYNC_FLAG_AVOID_FLAG_UPDATES);
+ if (ret <= 0) {
+ i_assert(ret != 0);
+ mailbox_set_index_error(box);
+ mailbox_free(&box);
+ return -1;
+ }
+
+ rebuild_ctx = index_index_rebuild_init(&mbox->box, view, trans);
+ mdbox_header_update(ctx, rebuild_ctx, mbox);
+ rebuild_mailbox_multi(ctx, rebuild_ctx, mbox, view, trans);
+ index_index_rebuild_deinit(&rebuild_ctx, dbox_get_uidvalidity_next);
+ mail_index_unset_fscked(trans);
+
+ mail_index_sync_set_reason(sync_ctx, "mdbox storage rebuild");
+ if (mail_index_sync_commit(&sync_ctx) < 0) {
+ mailbox_set_index_error(box);
+ ret = -1;
+ }
+
+ mailbox_free(&box);
+ return ret < 0 ? -1 : 0;
+}
+
+static int
+rebuild_namespace_mailboxes(struct mdbox_storage_rebuild_context *ctx,
+ struct mail_namespace *ns)
+{
+ struct mailbox_list_iterate_context *iter;
+ const struct mailbox_info *info;
+ int ret = 0;
+
+ if (ctx->default_list == NULL ||
+ (ns->flags & NAMESPACE_FLAG_INBOX_ANY) != 0)
+ ctx->default_list = ns->list;
+
+ iter = mailbox_list_iter_init(ns->list, "*",
+ MAILBOX_LIST_ITER_RAW_LIST |
+ MAILBOX_LIST_ITER_RETURN_NO_FLAGS);
+ while ((info = mailbox_list_iter_next(iter)) != NULL) {
+ if ((info->flags & (MAILBOX_NONEXISTENT |
+ MAILBOX_NOSELECT)) == 0) {
+ T_BEGIN {
+ ret = rebuild_mailbox(ctx, ns, info->vname);
+ } T_END;
+ if (ret < 0) {
+ ret = -1;
+ break;
+ }
+ }
+ }
+ if (mailbox_list_iter_deinit(&iter) < 0)
+ ret = -1;
+ return ret;
+}
+
+static int rebuild_mailboxes(struct mdbox_storage_rebuild_context *ctx)
+{
+ struct mail_storage *storage = &ctx->storage->storage.storage;
+ struct mail_namespace *ns;
+
+ for (ns = storage->user->namespaces; ns != NULL; ns = ns->next) {
+ if (ns->storage == storage && ns->alias_for == NULL) {
+ if (rebuild_namespace_mailboxes(ctx, ns) < 0)
+ return -1;
+ }
+ }
+ if (ctx->default_list == NULL)
+ i_panic("No namespace found for storage=%s", storage->name);
+ return 0;
+}
+
+static int rebuild_msg_mailbox_commit(struct rebuild_msg_mailbox *msg)
+{
+ mail_index_sync_set_reason(msg->sync_ctx, "mdbox storage rebuild");
+ if (mail_index_sync_commit(&msg->sync_ctx) < 0)
+ return -1;
+ mailbox_free(&msg->box);
+ i_zero(msg);
+ return 0;
+}
+
+static int rebuild_restore_msg(struct mdbox_storage_rebuild_context *ctx,
+ struct mdbox_rebuild_msg *msg)
+{
+ struct mail_storage *storage = &ctx->storage->storage.storage;
+ struct dbox_file *file;
+ const struct mail_index_header *hdr;
+ struct mdbox_mail_index_record dbox_rec;
+ const char *mailbox = NULL;
+ struct mailbox *box;
+ struct mdbox_mailbox *mbox;
+ enum mail_error error;
+ bool deleted, created;
+ int ret;
+ uint32_t seq;
+
+ /* first see if message contains the mailbox it was originally
+ saved to */
+ file = mdbox_file_init(ctx->storage, msg->file_id);
+ ret = dbox_file_open(file, &deleted);
+ if (ret > 0 && !deleted)
+ ret = dbox_file_seek(file, msg->offset);
+ if (ret > 0 && !deleted && dbox_file_metadata_read(file) > 0) {
+ mailbox = dbox_file_metadata_get(file,
+ DBOX_METADATA_ORIG_MAILBOX);
+ if (mailbox != NULL) {
+ mailbox = mailbox_list_get_vname(ctx->default_list, mailbox);
+ mailbox = t_strdup(mailbox);
+ }
+ rebuild_scan_metadata(ctx, file);
+ }
+ dbox_file_unref(&file);
+ if (ret <= 0 || deleted) {
+ if (ret < 0)
+ return -1;
+ /* we shouldn't get here, so apparently we couldn't fix
+ something. just ignore the mail.. */
+ return 0;
+ }
+
+ if (mailbox == NULL)
+ mailbox = "INBOX";
+
+ /* we have the destination mailbox. now open it and add the message
+ there. */
+ created = FALSE;
+ box = ctx->prev_msg.box != NULL &&
+ strcmp(mailbox, ctx->prev_msg.box->vname) == 0 ?
+ ctx->prev_msg.box : NULL;
+ while (box == NULL) {
+ box = mailbox_alloc(ctx->default_list, mailbox,
+ MAILBOX_FLAG_READONLY |
+ MAILBOX_FLAG_IGNORE_ACLS);
+ i_assert(box->storage == storage);
+ if (mailbox_open(box) == 0)
+ break;
+
+ error = mailbox_get_last_mail_error(box);
+ if (error == MAIL_ERROR_NOTFOUND && !created) {
+ /* mailbox doesn't exist currently? see if creating
+ it helps. */
+ created = TRUE;
+ (void)mailbox_create(box, NULL, FALSE);
+ mailbox_free(&box);
+ continue;
+ }
+
+ mailbox_free(&box);
+ if (error == MAIL_ERROR_TEMP)
+ return -1;
+
+ if (strcmp(mailbox, "INBOX") != 0) {
+ /* see if we can save to INBOX instead. */
+ mailbox = "INBOX";
+ } else {
+ /* this shouldn't happen */
+ return -1;
+ }
+ }
+ mbox = MDBOX_MAILBOX(box);
+
+ /* switch the mailbox cache if necessary */
+ if (box != ctx->prev_msg.box && ctx->prev_msg.box != NULL) {
+ if (rebuild_msg_mailbox_commit(&ctx->prev_msg) < 0)
+ return -1;
+ }
+ if (ctx->prev_msg.box == NULL) {
+ ret = mail_index_sync_begin(box->index,
+ &ctx->prev_msg.sync_ctx,
+ &ctx->prev_msg.view,
+ &ctx->prev_msg.trans, 0);
+ if (ret <= 0) {
+ i_assert(ret != 0);
+ mailbox_set_index_error(box);
+ mailbox_free(&box);
+ return -1;
+ }
+ ctx->prev_msg.box = box;
+ hdr = mail_index_get_header(ctx->prev_msg.view);
+ ctx->prev_msg.next_uid = hdr->next_uid;
+ }
+
+ /* add the new message */
+ i_zero(&dbox_rec);
+ dbox_rec.map_uid = msg->map_uid;
+ dbox_rec.save_date = ioloop_time;
+ mail_index_append(ctx->prev_msg.trans, ctx->prev_msg.next_uid++, &seq);
+ mail_index_update_ext(ctx->prev_msg.trans, seq, mbox->ext_id,
+ &dbox_rec, NULL);
+ mail_index_update_ext(ctx->prev_msg.trans, seq, mbox->guid_ext_id,
+ msg->guid_128, NULL);
+
+ i_assert(msg->refcount == 0);
+ msg->refcount++;
+ return 0;
+}
+
+static int rebuild_handle_zero_refs(struct mdbox_storage_rebuild_context *ctx)
+{
+ struct mdbox_rebuild_msg **msgs;
+ unsigned int i, count;
+
+ /* if we have messages at this point which have refcount=0, they're
+ either already expunged or they were somehow lost for some reason.
+ we'll need to figure out what to do about them. */
+ msgs = array_get_modifiable(&ctx->msgs, &count);
+ for (i = 0; i < count; i++) {
+ if (msgs[i]->refcount != 0)
+ continue;
+
+ if (msgs[i]->seen_zero_ref_in_map) {
+ /* we've seen the map record, trust it. */
+ continue;
+ }
+ /* either map record was lost for this message or the message
+ was lost from its mailbox. safest way to handle this is to
+ restore the message. */
+ if (rebuild_restore_msg(ctx, msgs[i]) < 0)
+ return -1;
+ }
+ if (ctx->prev_msg.box != NULL) {
+ if (rebuild_msg_mailbox_commit(&ctx->prev_msg) < 0)
+ return -1;
+ }
+ return 0;
+}
+
+static void rebuild_update_refcounts(struct mdbox_storage_rebuild_context *ctx)
+{
+ const struct mail_index_header *hdr;
+ const void *data;
+ struct mdbox_rebuild_msg **msgs;
+ const uint16_t *ref16_p;
+ uint32_t seq, map_uid;
+ unsigned int i, count;
+
+ /* update refcounts for existing map records */
+ msgs = array_get_modifiable(&ctx->msgs, &count);
+ hdr = mail_index_get_header(ctx->atomic->sync_view);
+ for (seq = 1, i = 0; seq <= hdr->messages_count && i < count; seq++) {
+ mail_index_lookup_uid(ctx->atomic->sync_view, seq, &map_uid);
+ if (map_uid != msgs[i]->map_uid) {
+ /* we've already expunged this map record */
+ i_assert(map_uid < msgs[i]->map_uid);
+ continue;
+ }
+
+ mail_index_lookup_ext(ctx->atomic->sync_view, seq,
+ ctx->storage->map->ref_ext_id,
+ &data, NULL);
+ ref16_p = data;
+ if (ref16_p == NULL || *ref16_p != msgs[i]->refcount) {
+ mail_index_update_ext(ctx->atomic->sync_trans, seq,
+ ctx->storage->map->ref_ext_id,
+ &msgs[i]->refcount, NULL);
+ }
+ i++;
+ }
+
+ /* update refcounts for newly created map records */
+ for (; i < count; i++, seq++) {
+ mail_index_update_ext(ctx->atomic->sync_trans, seq,
+ ctx->storage->map->ref_ext_id,
+ &msgs[i]->refcount, NULL);
+ }
+}
+
+static int rebuild_finish(struct mdbox_storage_rebuild_context *ctx)
+{
+ struct mdbox_map_mail_index_header map_hdr;
+
+ i_assert(ctx->default_list != NULL);
+
+ if (rebuild_handle_zero_refs(ctx) < 0)
+ return -1;
+ rebuild_update_refcounts(ctx);
+
+ /* update map header */
+ map_hdr = ctx->orig_map_hdr;
+ map_hdr.highest_file_id = ctx->highest_file_id;
+ map_hdr.rebuild_count = ++ctx->rebuild_count;
+
+ mail_index_update_header_ext(ctx->atomic->sync_trans,
+ ctx->storage->map->map_ext_id,
+ 0, &map_hdr, sizeof(map_hdr));
+ return 0;
+}
+
+static int
+mdbox_storage_rebuild_scan_dir(struct mdbox_storage_rebuild_context *ctx,
+ const char *storage_dir, bool alt)
+{
+ DIR *dir;
+ struct dirent *d;
+ int ret = 0;
+
+ dir = opendir(storage_dir);
+ if (dir == NULL) {
+ if (alt && errno == ENOENT)
+ return 0;
+
+ mail_storage_set_critical(&ctx->storage->storage.storage,
+ "opendir(%s) failed: %m", storage_dir);
+ return -1;
+ }
+ for (errno = 0; (d = readdir(dir)) != NULL && ret == 0; errno = 0) {
+ if (strncmp(d->d_name, MDBOX_MAIL_FILE_PREFIX,
+ strlen(MDBOX_MAIL_FILE_PREFIX)) == 0) T_BEGIN {
+ ret = rebuild_add_file(ctx, storage_dir, d->d_name);
+ } T_END;
+ }
+ if (ret == 0 && errno != 0) {
+ mail_storage_set_critical(&ctx->storage->storage.storage,
+ "readdir(%s) failed: %m", storage_dir);
+ ret = -1;
+ }
+ if (closedir(dir) < 0) {
+ mail_storage_set_critical(&ctx->storage->storage.storage,
+ "closedir(%s) failed: %m", storage_dir);
+ ret = -1;
+ }
+ return ret;
+}
+
+static int
+mdbox_storage_rebuild_scan_prepare(struct mdbox_storage_rebuild_context *ctx)
+{
+ const void *data;
+ size_t data_size;
+
+ if (mdbox_map_open_or_create(ctx->storage->map) < 0)
+ return -1;
+
+ /* begin by locking the map, so that other processes can't try to
+ rebuild at the same time. */
+ if (mdbox_map_atomic_lock(ctx->atomic, "mdbox storage rebuild") < 0)
+ return -1;
+
+ /* fsck the map just in case its UIDs are broken */
+ if (mail_index_fsck(ctx->storage->map->index) < 0) {
+ mail_storage_set_index_error(&ctx->storage->storage.storage,
+ ctx->storage->map->index);
+ return -1;
+ }
+
+ /* get old map header */
+ mail_index_get_header_ext(ctx->atomic->sync_view,
+ ctx->storage->map->map_ext_id,
+ &data, &data_size);
+ i_zero(&ctx->orig_map_hdr);
+ memcpy(&ctx->orig_map_hdr, data,
+ I_MIN(data_size, sizeof(ctx->orig_map_hdr)));
+ ctx->highest_file_id = ctx->orig_map_hdr.highest_file_id;
+
+ /* get storage rebuild counter after locking */
+ ctx->rebuild_count = mdbox_map_get_rebuild_count(ctx->storage->map);
+ if (ctx->rebuild_count != ctx->storage->corrupted_rebuild_count &&
+ ctx->storage->corrupted) {
+ /* storage was already rebuilt by someone else */
+ return 0;
+ }
+ return 1;
+}
+
+static int mdbox_storage_rebuild_scan(struct mdbox_storage_rebuild_context *ctx)
+{
+ i_warning("mdbox %s: rebuilding indexes", ctx->storage->storage_dir);
+
+ if (mdbox_storage_rebuild_scan_dir(ctx, ctx->storage->storage_dir,
+ FALSE) < 0)
+ return -1;
+ if (ctx->storage->alt_storage_dir != NULL) {
+ if (mdbox_storage_rebuild_scan_dir(ctx,
+ ctx->storage->alt_storage_dir, TRUE) < 0)
+ return -1;
+ }
+
+ rebuild_apply_map(ctx);
+ if (rebuild_mailboxes(ctx) < 0 ||
+ rebuild_finish(ctx) < 0) {
+ mdbox_map_atomic_set_failed(ctx->atomic);
+ return -1;
+ }
+ return 0;
+}
+
+int mdbox_storage_rebuild_in_context(struct mdbox_storage *storage,
+ struct mdbox_map_atomic_context *atomic)
+{
+ struct mdbox_storage_rebuild_context *ctx;
+ int ret;
+
+ if (dbox_verify_alt_storage(storage->map->root_list) < 0) {
+ mail_storage_set_critical(&storage->storage.storage,
+ "mdbox rebuild: Alt storage %s not mounted, aborting",
+ storage->alt_storage_dir);
+ mdbox_map_atomic_set_failed(atomic);
+ return -1;
+ }
+
+ ctx = mdbox_storage_rebuild_init(storage, atomic);
+ if ((ret = mdbox_storage_rebuild_scan_prepare(ctx)) > 0) {
+ struct event_reason *reason = event_reason_begin("mdbox:rebuild");
+ ret = mdbox_storage_rebuild_scan(ctx);
+ event_reason_end(&reason);
+ }
+ mdbox_storage_rebuild_deinit(ctx);
+
+ if (ret == 0) {
+ storage->corrupted = FALSE;
+ storage->corrupted_rebuild_count = 0;
+ }
+ return ret;
+}
+
+int mdbox_storage_rebuild(struct mdbox_storage *storage)
+{
+ struct mdbox_map_atomic_context *atomic;
+ int ret;
+
+ atomic = mdbox_map_atomic_begin(storage->map);
+ ret = mdbox_storage_rebuild_in_context(storage, atomic);
+ mdbox_map_atomic_set_success(atomic);
+ mdbox_map_atomic_unset_fscked(atomic);
+ if (mdbox_map_atomic_finish(&atomic) < 0)
+ ret = -1;
+ return ret;
+}
diff --git a/src/lib-storage/index/dbox-multi/mdbox-storage-rebuild.h b/src/lib-storage/index/dbox-multi/mdbox-storage-rebuild.h
new file mode 100644
index 0000000..1194d29
--- /dev/null
+++ b/src/lib-storage/index/dbox-multi/mdbox-storage-rebuild.h
@@ -0,0 +1,10 @@
+#ifndef MDBOX_STORAGE_REBUILD_H
+#define MDBOX_STORAGE_REBUILD_H
+
+struct mdbox_map_atomic_context;
+
+int mdbox_storage_rebuild_in_context(struct mdbox_storage *storage,
+ struct mdbox_map_atomic_context *atomic);
+int mdbox_storage_rebuild(struct mdbox_storage *storage);
+
+#endif
diff --git a/src/lib-storage/index/dbox-multi/mdbox-storage.c b/src/lib-storage/index/dbox-multi/mdbox-storage.c
new file mode 100644
index 0000000..e7e7f60
--- /dev/null
+++ b/src/lib-storage/index/dbox-multi/mdbox-storage.c
@@ -0,0 +1,530 @@
+/* Copyright (c) 2007-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "ioloop.h"
+#include "mkdir-parents.h"
+#include "master-service.h"
+#include "mail-index-modseq.h"
+#include "mail-index-alloc-cache.h"
+#include "mailbox-log.h"
+#include "mailbox-list-private.h"
+#include "index-pop3-uidl.h"
+#include "dbox-mail.h"
+#include "dbox-save.h"
+#include "mdbox-map.h"
+#include "mdbox-file.h"
+#include "mdbox-sync.h"
+#include "mdbox-storage-rebuild.h"
+#include "mdbox-storage.h"
+
+extern struct mail_storage mdbox_storage;
+extern struct mailbox mdbox_mailbox;
+
+static struct event_category event_category_mdbox = {
+ .name = "mdbox",
+ .parent = &event_category_storage,
+};
+
+static struct mail_storage *mdbox_storage_alloc(void)
+{
+ struct mdbox_storage *storage;
+ pool_t pool;
+
+ pool = pool_alloconly_create("mdbox storage", 2048);
+ storage = p_new(pool, struct mdbox_storage, 1);
+ storage->storage.v = mdbox_dbox_storage_vfuncs;
+ storage->storage.storage = mdbox_storage;
+ storage->storage.storage.pool = pool;
+ return &storage->storage.storage;
+}
+
+int mdbox_storage_create(struct mail_storage *_storage,
+ struct mail_namespace *ns, const char **error_r)
+{
+ struct mdbox_storage *storage = MDBOX_STORAGE(_storage);
+ const char *dir;
+
+ storage->set = mail_namespace_get_driver_settings(ns, _storage);
+ storage->preallocate_space = storage->set->mdbox_preallocate_space;
+
+ if (*ns->list->set.mailbox_dir_name == '\0') {
+ *error_r = "mdbox: MAILBOXDIR must not be empty";
+ return -1;
+ }
+
+ _storage->unique_root_dir =
+ p_strdup(_storage->pool, ns->list->set.root_dir);
+
+ dir = mailbox_list_get_root_forced(ns->list, MAILBOX_LIST_PATH_TYPE_DIR);
+ storage->storage_dir = p_strconcat(_storage->pool, dir,
+ "/"MDBOX_GLOBAL_DIR_NAME, NULL);
+ if (ns->list->set.alt_dir != NULL) {
+ storage->alt_storage_dir = p_strconcat(_storage->pool,
+ ns->list->set.alt_dir,
+ "/"MDBOX_GLOBAL_DIR_NAME, NULL);
+ }
+ i_array_init(&storage->open_files, 64);
+
+ storage->map = mdbox_map_init(storage, ns->list);
+ return dbox_storage_create(_storage, ns, error_r);
+}
+
+void mdbox_storage_destroy(struct mail_storage *_storage)
+{
+ struct mdbox_storage *storage = MDBOX_STORAGE(_storage);
+
+ mdbox_files_free(storage);
+ mdbox_map_deinit(&storage->map);
+ timeout_remove(&storage->to_close_unused_files);
+
+ if (array_is_created(&storage->move_from_alt_map_uids))
+ array_free(&storage->move_from_alt_map_uids);
+ if (array_is_created(&storage->move_to_alt_map_uids))
+ array_free(&storage->move_to_alt_map_uids);
+ array_free(&storage->open_files);
+ dbox_storage_destroy(_storage);
+}
+
+static const char *
+mdbox_storage_find_root_dir(const struct mail_namespace *ns)
+{
+ bool debug = ns->mail_set->mail_debug;
+ const char *home, *path;
+
+ if (ns->owner != NULL &&
+ mail_user_get_home(ns->owner, &home) > 0) {
+ path = t_strconcat(home, "/mdbox", NULL);
+ if (access(path, R_OK|W_OK|X_OK) == 0) {
+ if (debug)
+ i_debug("mdbox: root exists (%s)", path);
+ return path;
+ }
+ if (debug)
+ i_debug("mdbox: access(%s, rwx): failed: %m", path);
+ }
+ return NULL;
+}
+
+static bool mdbox_storage_autodetect(const struct mail_namespace *ns,
+ struct mailbox_list_settings *set)
+{
+ bool debug = ns->mail_set->mail_debug;
+ struct stat st;
+ const char *path, *root_dir;
+
+ if (set->root_dir != NULL)
+ root_dir = set->root_dir;
+ else {
+ root_dir = mdbox_storage_find_root_dir(ns);
+ if (root_dir == NULL) {
+ if (debug)
+ i_debug("mdbox: couldn't find root dir");
+ return FALSE;
+ }
+ }
+
+ path = t_strconcat(root_dir, "/"MDBOX_GLOBAL_DIR_NAME, NULL);
+ if (stat(path, &st) < 0) {
+ if (debug)
+ i_debug("mdbox autodetect: stat(%s) failed: %m", path);
+ return FALSE;
+ }
+
+ if (!S_ISDIR(st.st_mode)) {
+ if (debug)
+ i_debug("mdbox autodetect: %s not a directory", path);
+ return FALSE;
+ }
+
+ set->root_dir = root_dir;
+ dbox_storage_get_list_settings(ns, set);
+ return TRUE;
+}
+
+static struct mailbox *
+mdbox_mailbox_alloc(struct mail_storage *storage, struct mailbox_list *list,
+ const char *vname, enum mailbox_flags flags)
+{
+ struct mdbox_mailbox *mbox;
+ struct index_mailbox_context *ibox;
+ pool_t pool;
+
+ /* dbox can't work without index files */
+ flags &= ENUM_NEGATE(MAILBOX_FLAG_NO_INDEX_FILES);
+
+ pool = pool_alloconly_create("mdbox mailbox", 1024*3);
+ mbox = p_new(pool, struct mdbox_mailbox, 1);
+ mbox->box = mdbox_mailbox;
+ mbox->box.pool = pool;
+ mbox->box.storage = storage;
+ mbox->box.list = list;
+ mbox->box.mail_vfuncs = &mdbox_mail_vfuncs;
+
+ index_storage_mailbox_alloc(&mbox->box, vname, flags, MAIL_INDEX_PREFIX);
+
+ ibox = INDEX_STORAGE_CONTEXT(&mbox->box);
+ ibox->index_flags |= MAIL_INDEX_OPEN_FLAG_KEEP_BACKUPS |
+ MAIL_INDEX_OPEN_FLAG_NEVER_IN_MEMORY;
+
+ mbox->storage = MDBOX_STORAGE(storage);
+ return &mbox->box;
+}
+
+int mdbox_mailbox_open(struct mailbox *box)
+{
+ struct mdbox_mailbox *mbox = MDBOX_MAILBOX(box);
+ time_t path_ctime;
+
+ if (dbox_mailbox_check_existence(box, &path_ctime) < 0)
+ return -1;
+ if (dbox_mailbox_open(box, path_ctime) < 0)
+ return -1;
+
+ mbox->ext_id =
+ mail_index_ext_register(mbox->box.index, "mdbox", 0,
+ sizeof(struct mdbox_mail_index_record),
+ sizeof(uint32_t));
+ mbox->hdr_ext_id =
+ mail_index_ext_register(mbox->box.index, "mdbox-hdr",
+ sizeof(struct mdbox_index_header), 0, 0);
+ mbox->guid_ext_id =
+ mail_index_ext_register(mbox->box.index, "guid",
+ 0, GUID_128_SIZE, 1);
+ return 0;
+}
+
+static void mdbox_mailbox_close(struct mailbox *box)
+{
+ struct mdbox_storage *mstorage = MDBOX_STORAGE(box->storage);
+
+ if (mstorage->corrupted && !mstorage->rebuilding_storage)
+ (void)mdbox_storage_rebuild(mstorage);
+
+ index_storage_mailbox_close(box);
+}
+
+int mdbox_read_header(struct mdbox_mailbox *mbox,
+ struct mdbox_index_header *hdr, bool *need_resize_r)
+{
+ const void *data;
+ size_t data_size;
+
+ i_assert(mbox->box.opened);
+
+ mail_index_get_header_ext(mbox->box.view, mbox->hdr_ext_id,
+ &data, &data_size);
+ if (data_size < MDBOX_INDEX_HEADER_MIN_SIZE &&
+ (!mbox->creating || data_size != 0)) {
+ mailbox_set_critical(&mbox->box,
+ "mdbox: Invalid dbox header size: %zu",
+ data_size);
+ mdbox_storage_set_corrupted(mbox->storage);
+ return -1;
+ }
+ i_zero(hdr);
+ if (data_size > 0)
+ memcpy(hdr, data, I_MIN(data_size, sizeof(*hdr)));
+ *need_resize_r = data_size < sizeof(*hdr);
+ return 0;
+}
+
+void mdbox_update_header(struct mdbox_mailbox *mbox,
+ struct mail_index_transaction *trans,
+ const struct mailbox_update *update)
+{
+ struct mdbox_index_header hdr, new_hdr;
+ bool need_resize;
+
+ if (mdbox_read_header(mbox, &hdr, &need_resize) < 0) {
+ i_zero(&hdr);
+ need_resize = TRUE;
+ }
+
+ new_hdr = hdr;
+
+ if (update != NULL && !guid_128_is_empty(update->mailbox_guid)) {
+ memcpy(new_hdr.mailbox_guid, update->mailbox_guid,
+ sizeof(new_hdr.mailbox_guid));
+ } else if (guid_128_is_empty(new_hdr.mailbox_guid)) {
+ guid_128_generate(new_hdr.mailbox_guid);
+ }
+
+ new_hdr.map_uid_validity =
+ mdbox_map_get_uid_validity(mbox->storage->map);
+ if (need_resize) {
+ mail_index_ext_resize_hdr(trans, mbox->hdr_ext_id,
+ sizeof(new_hdr));
+ }
+ if (memcmp(&hdr, &new_hdr, sizeof(hdr)) != 0) {
+ mail_index_update_header_ext(trans, mbox->hdr_ext_id, 0,
+ &new_hdr, sizeof(new_hdr));
+ }
+}
+
+static int ATTR_NULL(2, 3)
+mdbox_write_index_header(struct mailbox *box,
+ const struct mailbox_update *update,
+ struct mail_index_transaction *trans)
+{
+ struct mdbox_mailbox *mbox = (struct mdbox_mailbox *)box;
+ struct mail_index_transaction *new_trans = NULL;
+ struct mail_index_view *view;
+ const struct mail_index_header *hdr;
+ uint32_t uid_validity, uid_next;
+
+ if (mdbox_map_open_or_create(mbox->storage->map) < 0)
+ return -1;
+
+ if (trans == NULL) {
+ new_trans = mail_index_transaction_begin(box->view, 0);
+ trans = new_trans;
+ }
+
+ view = mail_index_view_open(box->index);
+ hdr = mail_index_get_header(view);
+ uid_validity = hdr->uid_validity;
+ if (update != NULL && update->uid_validity != 0)
+ uid_validity = update->uid_validity;
+ else if (uid_validity == 0) {
+ /* set uidvalidity */
+ uid_validity = dbox_get_uidvalidity_next(box->list);
+ }
+
+ if (hdr->uid_validity != uid_validity) {
+ mail_index_update_header(trans,
+ offsetof(struct mail_index_header, uid_validity),
+ &uid_validity, sizeof(uid_validity), TRUE);
+ }
+ if (update != NULL && hdr->next_uid < update->min_next_uid) {
+ uid_next = update->min_next_uid;
+ mail_index_update_header(trans,
+ offsetof(struct mail_index_header, next_uid),
+ &uid_next, sizeof(uid_next), TRUE);
+ }
+ if (update != NULL && update->min_first_recent_uid != 0 &&
+ hdr->first_recent_uid < update->min_first_recent_uid) {
+ uint32_t first_recent_uid = update->min_first_recent_uid;
+
+ mail_index_update_header(trans,
+ offsetof(struct mail_index_header, first_recent_uid),
+ &first_recent_uid, sizeof(first_recent_uid), FALSE);
+ }
+ if (update != NULL && update->min_highest_modseq != 0 &&
+ mail_index_modseq_get_highest(view) < update->min_highest_modseq) {
+ mail_index_modseq_enable(box->index);
+ mail_index_update_highest_modseq(trans,
+ update->min_highest_modseq);
+ }
+ mail_index_view_close(&view);
+
+ if (box->inbox_user && box->creating) {
+ /* initialize pop3-uidl header when creating mailbox
+ (not on mailbox_update()) */
+ index_pop3_uidl_set_max_uid(box, trans, 0);
+ }
+
+ mdbox_update_header(mbox, trans, update);
+ if (new_trans != NULL) {
+ if (mail_index_transaction_commit(&new_trans) < 0) {
+ mailbox_set_index_error(box);
+ return -1;
+ }
+ }
+ return 0;
+}
+
+int mdbox_mailbox_create_indexes(struct mailbox *box,
+ const struct mailbox_update *update,
+ struct mail_index_transaction *trans)
+{
+ struct mdbox_mailbox *mbox = MDBOX_MAILBOX(box);
+ int ret;
+
+ mbox->creating = TRUE;
+ ret = mdbox_write_index_header(box, update, trans);
+ mbox->creating = FALSE;
+ return ret;
+}
+
+void mdbox_storage_set_corrupted(struct mdbox_storage *storage)
+{
+ if (storage->corrupted) {
+ /* already set it corrupted (possibly recursing back here) */
+ return;
+ }
+
+ storage->corrupted = TRUE;
+ storage->corrupted_rebuild_count = (uint32_t)-1;
+
+ if (mdbox_map_open(storage->map) > 0 &&
+ mdbox_map_refresh(storage->map) == 0) {
+ storage->corrupted_rebuild_count =
+ mdbox_map_get_rebuild_count(storage->map);
+ }
+}
+
+static const char *
+mdbox_get_attachment_path_suffix(struct dbox_file *file ATTR_UNUSED)
+{
+ return "";
+}
+
+void mdbox_set_mailbox_corrupted(struct mailbox *box)
+{
+ struct mdbox_storage *mstorage = MDBOX_STORAGE(box->storage);
+
+ mdbox_storage_set_corrupted(mstorage);
+}
+
+void mdbox_set_file_corrupted(struct dbox_file *file)
+{
+ struct mdbox_storage *mstorage = MDBOX_DBOX_STORAGE(file->storage);
+
+ mdbox_storage_set_corrupted(mstorage);
+}
+
+static int
+mdbox_mailbox_get_guid(struct mdbox_mailbox *mbox, guid_128_t guid_r)
+{
+ const struct mail_index_header *idx_hdr;
+ struct mdbox_index_header hdr;
+ bool need_resize;
+ int ret = 0;
+
+ i_assert(!mbox->creating);
+
+ /* there's a race condition between mkdir and getting the mailbox GUID.
+ normally this is handled by mdbox syncing, but GUID can be looked up
+ without syncing. when we detect this situation we'll try to finish
+ creating the indexes first, which usually means just waiting for
+ the sync lock to get unlocked by the other process creating them. */
+ idx_hdr = mail_index_get_header(mbox->box.view);
+ if (idx_hdr->uid_validity == 0 && idx_hdr->next_uid == 1) {
+ if (dbox_mailbox_create_indexes(&mbox->box, NULL) < 0)
+ return -1;
+ }
+
+ if (mdbox_read_header(mbox, &hdr, &need_resize) < 0)
+ i_zero(&hdr);
+
+ if (guid_128_is_empty(hdr.mailbox_guid)) {
+ /* regenerate it */
+ if (mdbox_write_index_header(&mbox->box, NULL, NULL) < 0 ||
+ mdbox_read_header(mbox, &hdr, &need_resize) < 0)
+ ret = -1;
+ }
+ if (ret == 0)
+ memcpy(guid_r, hdr.mailbox_guid, GUID_128_SIZE);
+ return ret;
+}
+
+static int
+mdbox_mailbox_get_metadata(struct mailbox *box,
+ enum mailbox_metadata_items items,
+ struct mailbox_metadata *metadata_r)
+{
+ struct mdbox_mailbox *mbox = MDBOX_MAILBOX(box);
+
+ if (index_mailbox_get_metadata(box, items, metadata_r) < 0)
+ return -1;
+ if ((items & MAILBOX_METADATA_GUID) != 0) {
+ if (mdbox_mailbox_get_guid(mbox, metadata_r->guid) < 0)
+ return -1;
+ }
+ return 0;
+}
+
+static int
+mdbox_mailbox_update(struct mailbox *box, const struct mailbox_update *update)
+{
+ if (!box->opened) {
+ if (mailbox_open(box) < 0)
+ return -1;
+ }
+ if (mdbox_write_index_header(box, update, NULL) < 0)
+ return -1;
+ return index_storage_mailbox_update_common(box, update);
+}
+
+struct mail_storage mdbox_storage = {
+ .name = MDBOX_STORAGE_NAME,
+ .class_flags = MAIL_STORAGE_CLASS_FLAG_UNIQUE_ROOT |
+ MAIL_STORAGE_CLASS_FLAG_HAVE_MAIL_GUIDS |
+ MAIL_STORAGE_CLASS_FLAG_HAVE_MAIL_SAVE_GUIDS |
+ MAIL_STORAGE_CLASS_FLAG_BINARY_DATA,
+ .event_category = &event_category_mdbox,
+
+ .v = {
+ mdbox_get_setting_parser_info,
+ mdbox_storage_alloc,
+ mdbox_storage_create,
+ mdbox_storage_destroy,
+ NULL,
+ dbox_storage_get_list_settings,
+ mdbox_storage_autodetect,
+ mdbox_mailbox_alloc,
+ mdbox_purge,
+ mail_storage_list_index_rebuild,
+ }
+};
+
+struct mailbox mdbox_mailbox = {
+ .v = {
+ index_storage_is_readonly,
+ index_storage_mailbox_enable,
+ index_storage_mailbox_exists,
+ mdbox_mailbox_open,
+ mdbox_mailbox_close,
+ index_storage_mailbox_free,
+ dbox_mailbox_create,
+ mdbox_mailbox_update,
+ index_storage_mailbox_delete,
+ index_storage_mailbox_rename,
+ index_storage_get_status,
+ mdbox_mailbox_get_metadata,
+ index_storage_set_subscribed,
+ index_storage_attribute_set,
+ index_storage_attribute_get,
+ index_storage_attribute_iter_init,
+ index_storage_attribute_iter_next,
+ index_storage_attribute_iter_deinit,
+ index_storage_list_index_has_changed,
+ index_storage_list_index_update_sync,
+ mdbox_storage_sync_init,
+ index_mailbox_sync_next,
+ index_mailbox_sync_deinit,
+ NULL,
+ dbox_notify_changes,
+ index_transaction_begin,
+ index_transaction_commit,
+ index_transaction_rollback,
+ NULL,
+ dbox_mail_alloc,
+ index_storage_search_init,
+ index_storage_search_deinit,
+ index_storage_search_next_nonblock,
+ index_storage_search_next_update_seq,
+ index_storage_search_next_match_mail,
+ mdbox_save_alloc,
+ mdbox_save_begin,
+ dbox_save_continue,
+ mdbox_save_finish,
+ mdbox_save_cancel,
+ mdbox_copy,
+ mdbox_transaction_save_commit_pre,
+ mdbox_transaction_save_commit_post,
+ mdbox_transaction_save_rollback,
+ index_storage_is_inconsistent
+ }
+};
+
+struct dbox_storage_vfuncs mdbox_dbox_storage_vfuncs = {
+ mdbox_file_unrefed,
+ mdbox_file_create_fd,
+ mdbox_mail_open,
+ mdbox_mailbox_create_indexes,
+ mdbox_get_attachment_path_suffix,
+ mdbox_set_mailbox_corrupted,
+ mdbox_set_file_corrupted
+};
diff --git a/src/lib-storage/index/dbox-multi/mdbox-storage.h b/src/lib-storage/index/dbox-multi/mdbox-storage.h
new file mode 100644
index 0000000..ea99532
--- /dev/null
+++ b/src/lib-storage/index/dbox-multi/mdbox-storage.h
@@ -0,0 +1,118 @@
+#ifndef MDBOX_STORAGE_H
+#define MDBOX_STORAGE_H
+
+#include "index-storage.h"
+#include "dbox-storage.h"
+#include "mdbox-settings.h"
+
+#define MDBOX_STORAGE_NAME "mdbox"
+#define MDBOX_DELETED_STORAGE_NAME "mdbox_deleted"
+#define MDBOX_GLOBAL_INDEX_PREFIX "dovecot.map.index"
+#define MDBOX_GLOBAL_DIR_NAME "storage"
+#define MDBOX_MAIL_FILE_PREFIX "m."
+#define MDBOX_MAIL_FILE_FORMAT MDBOX_MAIL_FILE_PREFIX"%u"
+#define MDBOX_MAX_OPEN_UNUSED_FILES 2
+#define MDBOX_CLOSE_UNUSED_FILES_TIMEOUT_SECS 30
+
+#define MDBOX_INDEX_HEADER_MIN_SIZE (sizeof(uint32_t))
+struct mdbox_index_header {
+ uint32_t map_uid_validity;
+ guid_128_t mailbox_guid;
+ uint8_t flags; /* enum dbox_index_header_flags */
+ uint8_t unused[3];
+};
+
+struct mdbox_storage {
+ struct dbox_storage storage;
+ const struct mdbox_settings *set;
+
+ /* paths for storage directories */
+ const char *storage_dir, *alt_storage_dir;
+ struct mdbox_map *map;
+
+ ARRAY(struct mdbox_file *) open_files;
+ struct timeout *to_close_unused_files;
+
+ ARRAY_TYPE(uint32_t) move_to_alt_map_uids;
+ ARRAY_TYPE(uint32_t) move_from_alt_map_uids;
+
+ /* if non-zero, storage should be rebuilt (except if rebuild_count
+ has changed from this value) */
+ uint32_t corrupted_rebuild_count;
+
+ bool corrupted:1;
+ bool rebuilding_storage:1;
+ bool preallocate_space:1;
+};
+
+struct mdbox_mail_index_record {
+ uint32_t map_uid;
+ /* UNIX timestamp of when the message was saved/copied to this
+ mailbox */
+ uint32_t save_date;
+};
+
+struct mdbox_mailbox {
+ struct mailbox box;
+ struct mdbox_storage *storage;
+
+ uint32_t map_uid_validity;
+ uint32_t ext_id, hdr_ext_id, guid_ext_id;
+
+ bool mdbox_deleted_synced:1;
+ bool creating:1;
+};
+
+#define MDBOX_DBOX_STORAGE(s) container_of(s, struct mdbox_storage, storage)
+#define MDBOX_STORAGE(s) MDBOX_DBOX_STORAGE(DBOX_STORAGE(s))
+#define MDBOX_MAILBOX(s) container_of(s, struct mdbox_mailbox, box)
+
+extern struct dbox_storage_vfuncs mdbox_dbox_storage_vfuncs;
+extern struct mail_vfuncs mdbox_mail_vfuncs;
+
+int mdbox_mail_open(struct dbox_mail *mail, uoff_t *offset_r,
+ struct dbox_file **file_r);
+
+/* Get map_uid for wanted message. */
+int mdbox_mail_lookup(struct mdbox_mailbox *mbox, struct mail_index_view *view,
+ uint32_t seq, uint32_t *map_uid_r);
+uint32_t dbox_get_uidvalidity_next(struct mailbox_list *list);
+int mdbox_read_header(struct mdbox_mailbox *mbox,
+ struct mdbox_index_header *hdr, bool *need_resize_r);
+void mdbox_update_header(struct mdbox_mailbox *mbox,
+ struct mail_index_transaction *trans,
+ const struct mailbox_update *update) ATTR_NULL(3);
+int mdbox_mailbox_create_indexes(struct mailbox *box,
+ const struct mailbox_update *update,
+ struct mail_index_transaction *trans);
+
+struct mail_save_context *
+mdbox_save_alloc(struct mailbox_transaction_context *_t);
+int mdbox_save_begin(struct mail_save_context *ctx, struct istream *input);
+int mdbox_save_finish(struct mail_save_context *ctx);
+void mdbox_save_cancel(struct mail_save_context *ctx);
+
+struct dbox_file *
+mdbox_save_file_get_file(struct mailbox_transaction_context *t,
+ uint32_t seq, uoff_t *offset_r);
+
+int mdbox_transaction_save_commit_pre(struct mail_save_context *ctx);
+void mdbox_transaction_save_commit_post(struct mail_save_context *ctx,
+ struct mail_index_transaction_commit_result *result);
+void mdbox_transaction_save_rollback(struct mail_save_context *ctx);
+
+int mdbox_copy(struct mail_save_context *ctx, struct mail *mail);
+
+void mdbox_purge_alt_flag_change(struct mail *mail, bool move_to_alt);
+int mdbox_purge(struct mail_storage *storage);
+
+int mdbox_storage_create(struct mail_storage *_storage,
+ struct mail_namespace *ns, const char **error_r);
+void mdbox_storage_destroy(struct mail_storage *_storage);
+int mdbox_mailbox_open(struct mailbox *box);
+
+void mdbox_storage_set_corrupted(struct mdbox_storage *storage);
+void mdbox_set_mailbox_corrupted(struct mailbox *box);
+void mdbox_set_file_corrupted(struct dbox_file *file);
+
+#endif
diff --git a/src/lib-storage/index/dbox-multi/mdbox-sync.c b/src/lib-storage/index/dbox-multi/mdbox-sync.c
new file mode 100644
index 0000000..0c2d2d4
--- /dev/null
+++ b/src/lib-storage/index/dbox-multi/mdbox-sync.c
@@ -0,0 +1,377 @@
+/* Copyright (c) 2007-2018 Dovecot authors, see the included COPYING file */
+
+/*
+ Expunging works like:
+
+ 1. Lock map index by beginning a map sync.
+ 2. Write map UID refcount changes to map index (=> tail != head).
+ 3. Expunge messages from mailbox index.
+ 4. Finish map sync, which updates tail=head and unlocks map index.
+
+ If something crashes after 2 but before 4 is finished, tail != head and
+ reader can do a full resync to figure out what got broken.
+*/
+
+#include "lib.h"
+#include "array.h"
+#include "ioloop.h"
+#include "str.h"
+#include "mdbox-storage.h"
+#include "mdbox-storage-rebuild.h"
+#include "mdbox-map.h"
+#include "mdbox-file.h"
+#include "mdbox-sync.h"
+#include "mailbox-recent-flags.h"
+
+
+/* returns -1 on error, 1 on success, 0 if guid is empty/missing */
+static int
+dbox_sync_verify_expunge_guid(struct mdbox_sync_context *ctx, uint32_t seq,
+ const guid_128_t guid_128)
+{
+ const void *data;
+ uint32_t uid;
+
+ mail_index_lookup_uid(ctx->sync_view, seq, &uid);
+ mail_index_lookup_ext(ctx->sync_view, seq,
+ ctx->mbox->guid_ext_id, &data, NULL);
+
+ if ((data == NULL) || guid_128_is_empty(data))
+ return 0;
+
+ if (guid_128_is_empty(guid_128) ||
+ memcmp(data, guid_128, GUID_128_SIZE) == 0)
+ return 1;
+
+ mailbox_set_critical(&ctx->mbox->box,
+ "Expunged GUID mismatch for UID %u: %s vs %s",
+ uid, guid_128_to_string(data), guid_128_to_string(guid_128));
+ mdbox_storage_set_corrupted(ctx->mbox->storage);
+ return -1;
+}
+
+static int mdbox_sync_expunge(struct mdbox_sync_context *ctx, uint32_t seq,
+ const guid_128_t guid_128)
+{
+ uint32_t map_uid;
+ int ret;
+
+ if (seq_range_array_add(&ctx->expunged_seqs, seq)) {
+ /* already marked as expunged in this sync */
+ return 0;
+ }
+
+ ret = dbox_sync_verify_expunge_guid(ctx, seq, guid_128);
+ if (ret <= 0)
+ return ret;
+ if (mdbox_mail_lookup(ctx->mbox, ctx->sync_view, seq, &map_uid) < 0)
+ return -1;
+ if (mdbox_map_update_refcount(ctx->map_trans, map_uid, -1) < 0)
+ return -1;
+ return 0;
+}
+
+static int mdbox_sync_rec(struct mdbox_sync_context *ctx,
+ const struct mail_index_sync_rec *sync_rec)
+{
+ uint32_t seq, seq1, seq2;
+
+ if (sync_rec->type != MAIL_INDEX_SYNC_TYPE_EXPUNGE) {
+ /* not interested */
+ return 0;
+ }
+
+ if (!mail_index_lookup_seq_range(ctx->sync_view,
+ sync_rec->uid1, sync_rec->uid2,
+ &seq1, &seq2)) {
+ /* already expunged everything. nothing to do. */
+ return 0;
+ }
+
+ for (seq = seq1; seq <= seq2; seq++) {
+ if (mdbox_sync_expunge(ctx, seq, sync_rec->guid_128) < 0)
+ return -1;
+ }
+ return 0;
+}
+
+static int dbox_sync_mark_expunges(struct mdbox_sync_context *ctx)
+{
+ enum mail_index_transaction_flags flags =
+ MAIL_INDEX_TRANSACTION_FLAG_EXTERNAL;
+ struct mailbox *box = &ctx->mbox->box;
+ struct mail_index_transaction *trans;
+ struct seq_range_iter iter;
+ unsigned int n;
+ const void *data;
+ uint32_t seq, uid;
+
+ /* use a separate transaction here so that we can commit the changes
+ during map transaction */
+ trans = mail_index_transaction_begin(ctx->sync_view, flags);
+ seq_range_array_iter_init(&iter, &ctx->expunged_seqs); n = 0;
+ while (seq_range_array_iter_nth(&iter, n++, &seq)) {
+ mail_index_lookup_uid(ctx->sync_view, seq, &uid);
+ mail_index_lookup_ext(ctx->sync_view, seq,
+ ctx->mbox->guid_ext_id, &data, NULL);
+ if ((data == NULL) || guid_128_is_empty(data))
+ mail_index_expunge(trans, seq);
+ else
+ mail_index_expunge_guid(trans, seq, data);
+ }
+ if (mail_index_transaction_commit(&trans) < 0)
+ return -1;
+
+ box->tmp_sync_view = ctx->sync_view;
+ seq_range_array_iter_init(&iter, &ctx->expunged_seqs); n = 0;
+ while (seq_range_array_iter_nth(&iter, n++, &seq)) {
+ mail_index_lookup_uid(ctx->sync_view, seq, &uid);
+ mailbox_sync_notify(box, uid, MAILBOX_SYNC_TYPE_EXPUNGE);
+ }
+ box->tmp_sync_view = NULL;
+ return 0;
+}
+
+static int mdbox_sync_index(struct mdbox_sync_context *ctx)
+{
+ struct mailbox *box = &ctx->mbox->box;
+ const struct mail_index_header *hdr;
+ struct mail_index_sync_rec sync_rec;
+ uint32_t seq1, seq2;
+ int ret = 0;
+
+ hdr = mail_index_get_header(ctx->sync_view);
+ if (hdr->uid_validity == 0) {
+ /* newly created index file */
+ if (hdr->next_uid == 1) {
+ /* could be just a race condition where we opened the
+ mailbox between mkdir and index creation. fix this
+ silently. */
+ if (mdbox_mailbox_create_indexes(box, NULL, ctx->trans) < 0)
+ return -1;
+ return 1;
+ }
+ mailbox_set_critical(box, "Broken index: missing UIDVALIDITY");
+ return 0;
+ }
+
+ /* mark the newly seen messages as recent */
+ if (mail_index_lookup_seq_range(ctx->sync_view, hdr->first_recent_uid,
+ hdr->next_uid, &seq1, &seq2)) {
+ mailbox_recent_flags_set_seqs(&ctx->mbox->box, ctx->sync_view,
+ seq1, seq2);
+ }
+
+ /* handle syncing records without map being locked. */
+ if (mdbox_map_atomic_is_locked(ctx->atomic)) {
+ ctx->map_trans = mdbox_map_transaction_begin(ctx->atomic, FALSE);
+ i_array_init(&ctx->expunged_seqs, 64);
+ }
+ while (mail_index_sync_next(ctx->index_sync_ctx, &sync_rec)) {
+ if ((ret = mdbox_sync_rec(ctx, &sync_rec)) < 0)
+ break;
+ }
+
+ /* write refcount changes to map index. transaction commit updates the
+ log head, while tail is left behind. */
+ if (mdbox_map_atomic_is_locked(ctx->atomic)) {
+ if (ret == 0)
+ ret = mdbox_map_transaction_commit(ctx->map_trans, "mdbox syncing");
+ /* write changes to mailbox index */
+ if (ret == 0)
+ ret = dbox_sync_mark_expunges(ctx);
+
+ /* finish the map changes and unlock the map. this also updates
+ map's tail -> head. */
+ if (ret < 0)
+ mdbox_map_atomic_set_failed(ctx->atomic);
+ mdbox_map_transaction_free(&ctx->map_trans);
+ ctx->expunged_count = seq_range_count(&ctx->expunged_seqs);
+ array_free(&ctx->expunged_seqs);
+ }
+
+ mailbox_sync_notify(box, 0, 0);
+
+ return ret == 0 ? 1 :
+ (ctx->mbox->storage->corrupted ? 0 : -1);
+}
+
+static int mdbox_sync_try_begin(struct mdbox_sync_context *ctx,
+ enum mail_index_sync_flags sync_flags)
+{
+ struct mdbox_mailbox *mbox = ctx->mbox;
+ int ret;
+
+ ret = index_storage_expunged_sync_begin(&mbox->box, &ctx->index_sync_ctx,
+ &ctx->sync_view, &ctx->trans, sync_flags);
+ if (mail_index_reset_fscked(mbox->box.index))
+ mdbox_storage_set_corrupted(mbox->storage);
+ if (ret <= 0)
+ return ret; /* error / nothing to do */
+
+ if (!mdbox_map_atomic_is_locked(ctx->atomic) &&
+ mail_index_sync_has_expunges(ctx->index_sync_ctx)) {
+ /* we have expunges, so we need to write to map.
+ it needs to be locked before mailbox index. */
+ mail_index_sync_set_reason(ctx->index_sync_ctx, "mdbox expunge check");
+ mail_index_sync_rollback(&ctx->index_sync_ctx);
+ index_storage_expunging_deinit(&ctx->mbox->box);
+
+ if (mdbox_map_atomic_lock(ctx->atomic, "mdbox syncing with expunges") < 0)
+ return -1;
+ return mdbox_sync_try_begin(ctx, sync_flags);
+ }
+ return 1;
+}
+
+int mdbox_sync_begin(struct mdbox_mailbox *mbox, enum mdbox_sync_flags flags,
+ struct mdbox_map_atomic_context *atomic,
+ struct mdbox_sync_context **ctx_r)
+{
+ const struct mail_index_header *hdr =
+ mail_index_get_header(mbox->box.view);
+ struct mdbox_sync_context *ctx;
+ const char *reason;
+ enum mail_index_sync_flags sync_flags;
+ int ret;
+ bool rebuild, storage_rebuilt = FALSE;
+
+ *ctx_r = NULL;
+
+ /* avoid race conditions with mailbox creation, don't check for dbox
+ headers until syncing has locked the mailbox */
+ rebuild = mbox->storage->corrupted ||
+ (hdr->flags & MAIL_INDEX_HDR_FLAG_FSCKD) != 0 ||
+ mdbox_map_is_fscked(mbox->storage->map) ||
+ (flags & MDBOX_SYNC_FLAG_FORCE_REBUILD) != 0;
+ if (rebuild && (flags & MDBOX_SYNC_FLAG_NO_REBUILD) == 0) {
+ if (mdbox_storage_rebuild_in_context(mbox->storage, atomic) < 0)
+ return -1;
+ mailbox_recent_flags_reset(&mbox->box);
+ storage_rebuilt = TRUE;
+ }
+
+ ctx = i_new(struct mdbox_sync_context, 1);
+ ctx->mbox = mbox;
+ ctx->flags = flags;
+ ctx->atomic = atomic;
+
+ sync_flags = index_storage_get_sync_flags(&mbox->box);
+ if (!rebuild && (flags & MDBOX_SYNC_FLAG_FORCE) == 0)
+ sync_flags |= MAIL_INDEX_SYNC_FLAG_REQUIRE_CHANGES;
+ if ((flags & MDBOX_SYNC_FLAG_FSYNC) != 0)
+ sync_flags |= MAIL_INDEX_SYNC_FLAG_FSYNC;
+ /* don't write unnecessary dirty flag updates */
+ sync_flags |= MAIL_INDEX_SYNC_FLAG_AVOID_FLAG_UPDATES;
+
+ ret = mdbox_sync_try_begin(ctx, sync_flags);
+ if (ret <= 0) {
+ /* failed / nothing to do */
+ index_storage_expunging_deinit(&mbox->box);
+ i_free(ctx);
+ return ret;
+ }
+
+ if ((ret = mdbox_sync_index(ctx)) <= 0) {
+ mail_index_sync_set_reason(ctx->index_sync_ctx,
+ ret < 0 ? "mdbox syncing failed" :
+ "mdbox syncing found corruption");
+ mail_index_sync_rollback(&ctx->index_sync_ctx);
+ index_storage_expunging_deinit(&mbox->box);
+ i_free_and_null(ctx);
+
+ if (ret < 0)
+ return -1;
+
+ /* corrupted */
+ if (storage_rebuilt) {
+ mailbox_set_critical(&mbox->box,
+ "mdbox: Storage keeps breaking");
+ return -1;
+ }
+
+ /* we'll need to rebuild storage.
+ try again from the beginning. */
+ mdbox_storage_set_corrupted(mbox->storage);
+ if ((flags & MDBOX_SYNC_FLAG_NO_REBUILD) != 0) {
+ mailbox_set_critical(&mbox->box,
+ "mdbox: Can't rebuild storage");
+ return -1;
+ }
+ return mdbox_sync_begin(mbox, flags, atomic, ctx_r);
+ }
+ index_storage_expunging_deinit(&mbox->box);
+
+ if (!mdbox_map_atomic_is_locked(ctx->atomic))
+ reason = "mdbox synced";
+ else {
+ /* may be 0 msgs, but that still informs that the map
+ was locked */
+ reason = t_strdup_printf("mdbox synced - %u msgs expunged",
+ ctx->expunged_count);
+ }
+ mail_index_sync_set_reason(ctx->index_sync_ctx, reason);
+
+ *ctx_r = ctx;
+ return 0;
+}
+
+int mdbox_sync_finish(struct mdbox_sync_context **_ctx, bool success)
+{
+ struct mdbox_sync_context *ctx = *_ctx;
+ struct mail_storage *storage = &ctx->mbox->storage->storage.storage;
+ int ret = success ? 0 : -1;
+
+ *_ctx = NULL;
+
+ if (success) {
+ if (mail_index_sync_commit(&ctx->index_sync_ctx) < 0) {
+ mailbox_set_index_error(&ctx->mbox->box);
+ ret = -1;
+ }
+ } else {
+ mail_index_sync_rollback(&ctx->index_sync_ctx);
+ }
+
+ if (storage->rebuild_list_index)
+ ret = mail_storage_list_index_rebuild_and_set_uncorrupted(storage);
+
+ i_free(ctx);
+ return ret;
+}
+
+int mdbox_sync(struct mdbox_mailbox *mbox, enum mdbox_sync_flags flags)
+{
+ struct mdbox_sync_context *sync_ctx;
+ struct mdbox_map_atomic_context *atomic;
+ int ret;
+
+ atomic = mdbox_map_atomic_begin(mbox->storage->map);
+ ret = mdbox_sync_begin(mbox, flags, atomic, &sync_ctx);
+ if (ret == 0 && sync_ctx != NULL)
+ ret = mdbox_sync_finish(&sync_ctx, TRUE);
+ if (ret == 0)
+ mdbox_map_atomic_set_success(atomic);
+ if (mdbox_map_atomic_finish(&atomic) < 0)
+ ret = -1;
+ return ret;
+}
+
+struct mailbox_sync_context *
+mdbox_storage_sync_init(struct mailbox *box, enum mailbox_sync_flags flags)
+{
+ struct mdbox_mailbox *mbox = MDBOX_MAILBOX(box);
+ enum mdbox_sync_flags mdbox_sync_flags = 0;
+ int ret = 0;
+
+ if (mail_index_reset_fscked(box->index))
+ mdbox_storage_set_corrupted(mbox->storage);
+ if (index_mailbox_want_full_sync(&mbox->box, flags) ||
+ mbox->storage->corrupted) {
+ if ((flags & MAILBOX_SYNC_FLAG_FORCE_RESYNC) != 0)
+ mdbox_sync_flags |= MDBOX_SYNC_FLAG_FORCE_REBUILD;
+ ret = mdbox_sync(mbox, mdbox_sync_flags);
+ }
+
+ return index_mailbox_sync_init(box, flags, ret < 0);
+}
diff --git a/src/lib-storage/index/dbox-multi/mdbox-sync.h b/src/lib-storage/index/dbox-multi/mdbox-sync.h
new file mode 100644
index 0000000..a9bc565
--- /dev/null
+++ b/src/lib-storage/index/dbox-multi/mdbox-sync.h
@@ -0,0 +1,37 @@
+#ifndef MDBOX_SYNC_H
+#define MDBOX_SYNC_H
+
+struct mailbox;
+struct mdbox_mailbox;
+
+enum mdbox_sync_flags {
+ MDBOX_SYNC_FLAG_FORCE = 0x01,
+ MDBOX_SYNC_FLAG_FSYNC = 0x02,
+ MDBOX_SYNC_FLAG_FORCE_REBUILD = 0x04,
+ MDBOX_SYNC_FLAG_NO_PURGE = 0x08,
+ MDBOX_SYNC_FLAG_NO_REBUILD = 0x10
+};
+
+struct mdbox_sync_context {
+ struct mdbox_mailbox *mbox;
+ struct mail_index_sync_ctx *index_sync_ctx;
+ struct mail_index_view *sync_view;
+ struct mail_index_transaction *trans;
+ struct mdbox_map_transaction_context *map_trans;
+ struct mdbox_map_atomic_context *atomic;
+ enum mdbox_sync_flags flags;
+
+ ARRAY_TYPE(seq_range) expunged_seqs;
+ unsigned int expunged_count;
+};
+
+int mdbox_sync_begin(struct mdbox_mailbox *mbox, enum mdbox_sync_flags flags,
+ struct mdbox_map_atomic_context *atomic,
+ struct mdbox_sync_context **ctx_r);
+int mdbox_sync_finish(struct mdbox_sync_context **ctx, bool success);
+int mdbox_sync(struct mdbox_mailbox *mbox, enum mdbox_sync_flags flags);
+
+struct mailbox_sync_context *
+mdbox_storage_sync_init(struct mailbox *box, enum mailbox_sync_flags flags);
+
+#endif
diff --git a/src/lib-storage/index/dbox-single/Makefile.am b/src/lib-storage/index/dbox-single/Makefile.am
new file mode 100644
index 0000000..b6f59bd
--- /dev/null
+++ b/src/lib-storage/index/dbox-single/Makefile.am
@@ -0,0 +1,30 @@
+noinst_LTLIBRARIES = libstorage_dbox_single.la
+
+AM_CPPFLAGS = \
+ -I$(top_srcdir)/src/lib \
+ -I$(top_srcdir)/src/lib-settings \
+ -I$(top_srcdir)/src/lib-master \
+ -I$(top_srcdir)/src/lib-fs \
+ -I$(top_srcdir)/src/lib-mail \
+ -I$(top_srcdir)/src/lib-imap \
+ -I$(top_srcdir)/src/lib-index \
+ -I$(top_srcdir)/src/lib-storage \
+ -I$(top_srcdir)/src/lib-storage/index \
+ -I$(top_srcdir)/src/lib-storage/index/dbox-common
+
+libstorage_dbox_single_la_SOURCES = \
+ sdbox-copy.c \
+ sdbox-file.c \
+ sdbox-mail.c \
+ sdbox-save.c \
+ sdbox-sync.c \
+ sdbox-sync-rebuild.c \
+ sdbox-storage.c
+
+headers = \
+ sdbox-file.h \
+ sdbox-storage.h \
+ sdbox-sync.h
+
+pkginc_libdir=$(pkgincludedir)
+pkginc_lib_HEADERS = $(headers)
diff --git a/src/lib-storage/index/dbox-single/Makefile.in b/src/lib-storage/index/dbox-single/Makefile.in
new file mode 100644
index 0000000..f3e5076
--- /dev/null
+++ b/src/lib-storage/index/dbox-single/Makefile.in
@@ -0,0 +1,848 @@
+# Makefile.in generated by automake 1.16.1 from Makefile.am.
+# @configure_input@
+
+# Copyright (C) 1994-2018 Free Software Foundation, Inc.
+
+# This Makefile.in is free software; the Free Software Foundation
+# gives unlimited permission to copy and/or distribute it,
+# with or without modifications, as long as this notice is preserved.
+
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY, to the extent permitted by law; without
+# even the implied warranty of MERCHANTABILITY or FITNESS FOR A
+# PARTICULAR PURPOSE.
+
+@SET_MAKE@
+
+
+VPATH = @srcdir@
+am__is_gnu_make = { \
+ if test -z '$(MAKELEVEL)'; then \
+ false; \
+ elif test -n '$(MAKE_HOST)'; then \
+ true; \
+ elif test -n '$(MAKE_VERSION)' && test -n '$(CURDIR)'; then \
+ true; \
+ else \
+ false; \
+ fi; \
+}
+am__make_running_with_option = \
+ case $${target_option-} in \
+ ?) ;; \
+ *) echo "am__make_running_with_option: internal error: invalid" \
+ "target option '$${target_option-}' specified" >&2; \
+ exit 1;; \
+ esac; \
+ has_opt=no; \
+ sane_makeflags=$$MAKEFLAGS; \
+ if $(am__is_gnu_make); then \
+ sane_makeflags=$$MFLAGS; \
+ else \
+ case $$MAKEFLAGS in \
+ *\\[\ \ ]*) \
+ bs=\\; \
+ sane_makeflags=`printf '%s\n' "$$MAKEFLAGS" \
+ | sed "s/$$bs$$bs[$$bs $$bs ]*//g"`;; \
+ esac; \
+ fi; \
+ skip_next=no; \
+ strip_trailopt () \
+ { \
+ flg=`printf '%s\n' "$$flg" | sed "s/$$1.*$$//"`; \
+ }; \
+ for flg in $$sane_makeflags; do \
+ test $$skip_next = yes && { skip_next=no; continue; }; \
+ case $$flg in \
+ *=*|--*) continue;; \
+ -*I) strip_trailopt 'I'; skip_next=yes;; \
+ -*I?*) strip_trailopt 'I';; \
+ -*O) strip_trailopt 'O'; skip_next=yes;; \
+ -*O?*) strip_trailopt 'O';; \
+ -*l) strip_trailopt 'l'; skip_next=yes;; \
+ -*l?*) strip_trailopt 'l';; \
+ -[dEDm]) skip_next=yes;; \
+ -[JT]) skip_next=yes;; \
+ esac; \
+ case $$flg in \
+ *$$target_option*) has_opt=yes; break;; \
+ esac; \
+ done; \
+ test $$has_opt = yes
+am__make_dryrun = (target_option=n; $(am__make_running_with_option))
+am__make_keepgoing = (target_option=k; $(am__make_running_with_option))
+pkgdatadir = $(datadir)/@PACKAGE@
+pkgincludedir = $(includedir)/@PACKAGE@
+pkglibdir = $(libdir)/@PACKAGE@
+pkglibexecdir = $(libexecdir)/@PACKAGE@
+am__cd = CDPATH="$${ZSH_VERSION+.}$(PATH_SEPARATOR)" && cd
+install_sh_DATA = $(install_sh) -c -m 644
+install_sh_PROGRAM = $(install_sh) -c
+install_sh_SCRIPT = $(install_sh) -c
+INSTALL_HEADER = $(INSTALL_DATA)
+transform = $(program_transform_name)
+NORMAL_INSTALL = :
+PRE_INSTALL = :
+POST_INSTALL = :
+NORMAL_UNINSTALL = :
+PRE_UNINSTALL = :
+POST_UNINSTALL = :
+build_triplet = @build@
+host_triplet = @host@
+subdir = src/lib-storage/index/dbox-single
+ACLOCAL_M4 = $(top_srcdir)/aclocal.m4
+am__aclocal_m4_deps = $(top_srcdir)/m4/ac_checktype2.m4 \
+ $(top_srcdir)/m4/ac_typeof.m4 $(top_srcdir)/m4/arc4random.m4 \
+ $(top_srcdir)/m4/blockdev.m4 $(top_srcdir)/m4/c99_vsnprintf.m4 \
+ $(top_srcdir)/m4/clock_gettime.m4 $(top_srcdir)/m4/crypt.m4 \
+ $(top_srcdir)/m4/crypt_xpg6.m4 $(top_srcdir)/m4/dbqlk.m4 \
+ $(top_srcdir)/m4/dirent_dtype.m4 $(top_srcdir)/m4/dovecot.m4 \
+ $(top_srcdir)/m4/fd_passing.m4 $(top_srcdir)/m4/fdatasync.m4 \
+ $(top_srcdir)/m4/flexible_array_member.m4 \
+ $(top_srcdir)/m4/glibc.m4 $(top_srcdir)/m4/gmtime_max.m4 \
+ $(top_srcdir)/m4/gmtime_tm_gmtoff.m4 \
+ $(top_srcdir)/m4/ioloop.m4 $(top_srcdir)/m4/iovec.m4 \
+ $(top_srcdir)/m4/ipv6.m4 $(top_srcdir)/m4/libcap.m4 \
+ $(top_srcdir)/m4/libtool.m4 $(top_srcdir)/m4/libwrap.m4 \
+ $(top_srcdir)/m4/linux_mremap.m4 $(top_srcdir)/m4/ltoptions.m4 \
+ $(top_srcdir)/m4/ltsugar.m4 $(top_srcdir)/m4/ltversion.m4 \
+ $(top_srcdir)/m4/lt~obsolete.m4 $(top_srcdir)/m4/mmap_write.m4 \
+ $(top_srcdir)/m4/mntctl.m4 $(top_srcdir)/m4/modules.m4 \
+ $(top_srcdir)/m4/notify.m4 $(top_srcdir)/m4/nsl.m4 \
+ $(top_srcdir)/m4/off_t_max.m4 $(top_srcdir)/m4/pkg.m4 \
+ $(top_srcdir)/m4/pr_set_dumpable.m4 \
+ $(top_srcdir)/m4/q_quotactl.m4 $(top_srcdir)/m4/quota.m4 \
+ $(top_srcdir)/m4/random.m4 $(top_srcdir)/m4/rlimit.m4 \
+ $(top_srcdir)/m4/sendfile.m4 $(top_srcdir)/m4/size_t_signed.m4 \
+ $(top_srcdir)/m4/sockpeercred.m4 $(top_srcdir)/m4/sql.m4 \
+ $(top_srcdir)/m4/ssl.m4 $(top_srcdir)/m4/st_tim.m4 \
+ $(top_srcdir)/m4/static_array.m4 $(top_srcdir)/m4/test_with.m4 \
+ $(top_srcdir)/m4/time_t.m4 $(top_srcdir)/m4/typeof.m4 \
+ $(top_srcdir)/m4/typeof_dev_t.m4 \
+ $(top_srcdir)/m4/uoff_t_max.m4 $(top_srcdir)/m4/vararg.m4 \
+ $(top_srcdir)/m4/want_apparmor.m4 \
+ $(top_srcdir)/m4/want_bsdauth.m4 \
+ $(top_srcdir)/m4/want_bzlib.m4 \
+ $(top_srcdir)/m4/want_cassandra.m4 \
+ $(top_srcdir)/m4/want_cdb.m4 \
+ $(top_srcdir)/m4/want_checkpassword.m4 \
+ $(top_srcdir)/m4/want_clucene.m4 $(top_srcdir)/m4/want_db.m4 \
+ $(top_srcdir)/m4/want_gssapi.m4 $(top_srcdir)/m4/want_icu.m4 \
+ $(top_srcdir)/m4/want_ldap.m4 $(top_srcdir)/m4/want_lua.m4 \
+ $(top_srcdir)/m4/want_lz4.m4 $(top_srcdir)/m4/want_lzma.m4 \
+ $(top_srcdir)/m4/want_mysql.m4 $(top_srcdir)/m4/want_pam.m4 \
+ $(top_srcdir)/m4/want_passwd.m4 $(top_srcdir)/m4/want_pgsql.m4 \
+ $(top_srcdir)/m4/want_prefetch.m4 \
+ $(top_srcdir)/m4/want_shadow.m4 \
+ $(top_srcdir)/m4/want_sodium.m4 $(top_srcdir)/m4/want_solr.m4 \
+ $(top_srcdir)/m4/want_sqlite.m4 \
+ $(top_srcdir)/m4/want_stemmer.m4 \
+ $(top_srcdir)/m4/want_systemd.m4 \
+ $(top_srcdir)/m4/want_textcat.m4 \
+ $(top_srcdir)/m4/want_unwind.m4 $(top_srcdir)/m4/want_zlib.m4 \
+ $(top_srcdir)/m4/want_zstd.m4 $(top_srcdir)/configure.ac
+am__configure_deps = $(am__aclocal_m4_deps) $(CONFIGURE_DEPENDENCIES) \
+ $(ACLOCAL_M4)
+DIST_COMMON = $(srcdir)/Makefile.am $(pkginc_lib_HEADERS) \
+ $(am__DIST_COMMON)
+mkinstalldirs = $(install_sh) -d
+CONFIG_HEADER = $(top_builddir)/config.h
+CONFIG_CLEAN_FILES =
+CONFIG_CLEAN_VPATH_FILES =
+LTLIBRARIES = $(noinst_LTLIBRARIES)
+libstorage_dbox_single_la_LIBADD =
+am_libstorage_dbox_single_la_OBJECTS = sdbox-copy.lo sdbox-file.lo \
+ sdbox-mail.lo sdbox-save.lo sdbox-sync.lo \
+ sdbox-sync-rebuild.lo sdbox-storage.lo
+libstorage_dbox_single_la_OBJECTS = \
+ $(am_libstorage_dbox_single_la_OBJECTS)
+AM_V_lt = $(am__v_lt_@AM_V@)
+am__v_lt_ = $(am__v_lt_@AM_DEFAULT_V@)
+am__v_lt_0 = --silent
+am__v_lt_1 =
+AM_V_P = $(am__v_P_@AM_V@)
+am__v_P_ = $(am__v_P_@AM_DEFAULT_V@)
+am__v_P_0 = false
+am__v_P_1 = :
+AM_V_GEN = $(am__v_GEN_@AM_V@)
+am__v_GEN_ = $(am__v_GEN_@AM_DEFAULT_V@)
+am__v_GEN_0 = @echo " GEN " $@;
+am__v_GEN_1 =
+AM_V_at = $(am__v_at_@AM_V@)
+am__v_at_ = $(am__v_at_@AM_DEFAULT_V@)
+am__v_at_0 = @
+am__v_at_1 =
+DEFAULT_INCLUDES = -I.@am__isrc@ -I$(top_builddir)
+depcomp = $(SHELL) $(top_srcdir)/depcomp
+am__maybe_remake_depfiles = depfiles
+am__depfiles_remade = ./$(DEPDIR)/sdbox-copy.Plo \
+ ./$(DEPDIR)/sdbox-file.Plo ./$(DEPDIR)/sdbox-mail.Plo \
+ ./$(DEPDIR)/sdbox-save.Plo ./$(DEPDIR)/sdbox-storage.Plo \
+ ./$(DEPDIR)/sdbox-sync-rebuild.Plo ./$(DEPDIR)/sdbox-sync.Plo
+am__mv = mv -f
+COMPILE = $(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) \
+ $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS)
+LTCOMPILE = $(LIBTOOL) $(AM_V_lt) --tag=CC $(AM_LIBTOOLFLAGS) \
+ $(LIBTOOLFLAGS) --mode=compile $(CC) $(DEFS) \
+ $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) $(CPPFLAGS) \
+ $(AM_CFLAGS) $(CFLAGS)
+AM_V_CC = $(am__v_CC_@AM_V@)
+am__v_CC_ = $(am__v_CC_@AM_DEFAULT_V@)
+am__v_CC_0 = @echo " CC " $@;
+am__v_CC_1 =
+CCLD = $(CC)
+LINK = $(LIBTOOL) $(AM_V_lt) --tag=CC $(AM_LIBTOOLFLAGS) \
+ $(LIBTOOLFLAGS) --mode=link $(CCLD) $(AM_CFLAGS) $(CFLAGS) \
+ $(AM_LDFLAGS) $(LDFLAGS) -o $@
+AM_V_CCLD = $(am__v_CCLD_@AM_V@)
+am__v_CCLD_ = $(am__v_CCLD_@AM_DEFAULT_V@)
+am__v_CCLD_0 = @echo " CCLD " $@;
+am__v_CCLD_1 =
+SOURCES = $(libstorage_dbox_single_la_SOURCES)
+DIST_SOURCES = $(libstorage_dbox_single_la_SOURCES)
+am__can_run_installinfo = \
+ case $$AM_UPDATE_INFO_DIR in \
+ n|no|NO) false;; \
+ *) (install-info --version) >/dev/null 2>&1;; \
+ esac
+am__vpath_adj_setup = srcdirstrip=`echo "$(srcdir)" | sed 's|.|.|g'`;
+am__vpath_adj = case $$p in \
+ $(srcdir)/*) f=`echo "$$p" | sed "s|^$$srcdirstrip/||"`;; \
+ *) f=$$p;; \
+ esac;
+am__strip_dir = f=`echo $$p | sed -e 's|^.*/||'`;
+am__install_max = 40
+am__nobase_strip_setup = \
+ srcdirstrip=`echo "$(srcdir)" | sed 's/[].[^$$\\*|]/\\\\&/g'`
+am__nobase_strip = \
+ for p in $$list; do echo "$$p"; done | sed -e "s|$$srcdirstrip/||"
+am__nobase_list = $(am__nobase_strip_setup); \
+ for p in $$list; do echo "$$p $$p"; done | \
+ sed "s| $$srcdirstrip/| |;"' / .*\//!s/ .*/ ./; s,\( .*\)/[^/]*$$,\1,' | \
+ $(AWK) 'BEGIN { files["."] = "" } { files[$$2] = files[$$2] " " $$1; \
+ if (++n[$$2] == $(am__install_max)) \
+ { print $$2, files[$$2]; n[$$2] = 0; files[$$2] = "" } } \
+ END { for (dir in files) print dir, files[dir] }'
+am__base_list = \
+ sed '$$!N;$$!N;$$!N;$$!N;$$!N;$$!N;$$!N;s/\n/ /g' | \
+ sed '$$!N;$$!N;$$!N;$$!N;s/\n/ /g'
+am__uninstall_files_from_dir = { \
+ test -z "$$files" \
+ || { test ! -d "$$dir" && test ! -f "$$dir" && test ! -r "$$dir"; } \
+ || { echo " ( cd '$$dir' && rm -f" $$files ")"; \
+ $(am__cd) "$$dir" && rm -f $$files; }; \
+ }
+am__installdirs = "$(DESTDIR)$(pkginc_libdir)"
+HEADERS = $(pkginc_lib_HEADERS)
+am__tagged_files = $(HEADERS) $(SOURCES) $(TAGS_FILES) $(LISP)
+# Read a list of newline-separated strings from the standard input,
+# and print each of them once, without duplicates. Input order is
+# *not* preserved.
+am__uniquify_input = $(AWK) '\
+ BEGIN { nonempty = 0; } \
+ { items[$$0] = 1; nonempty = 1; } \
+ END { if (nonempty) { for (i in items) print i; }; } \
+'
+# Make sure the list of sources is unique. This is necessary because,
+# e.g., the same source file might be shared among _SOURCES variables
+# for different programs/libraries.
+am__define_uniq_tagged_files = \
+ list='$(am__tagged_files)'; \
+ unique=`for i in $$list; do \
+ if test -f "$$i"; then echo $$i; else echo $(srcdir)/$$i; fi; \
+ done | $(am__uniquify_input)`
+ETAGS = etags
+CTAGS = ctags
+am__DIST_COMMON = $(srcdir)/Makefile.in $(top_srcdir)/depcomp
+DISTFILES = $(DIST_COMMON) $(DIST_SOURCES) $(TEXINFOS) $(EXTRA_DIST)
+ACLOCAL = @ACLOCAL@
+ACLOCAL_AMFLAGS = @ACLOCAL_AMFLAGS@
+AMTAR = @AMTAR@
+AM_DEFAULT_VERBOSITY = @AM_DEFAULT_VERBOSITY@
+APPARMOR_LIBS = @APPARMOR_LIBS@
+AR = @AR@
+AUTH_CFLAGS = @AUTH_CFLAGS@
+AUTH_LIBS = @AUTH_LIBS@
+AUTOCONF = @AUTOCONF@
+AUTOHEADER = @AUTOHEADER@
+AUTOMAKE = @AUTOMAKE@
+AWK = @AWK@
+BINARY_CFLAGS = @BINARY_CFLAGS@
+BINARY_LDFLAGS = @BINARY_LDFLAGS@
+BISON = @BISON@
+CASSANDRA_CFLAGS = @CASSANDRA_CFLAGS@
+CASSANDRA_LIBS = @CASSANDRA_LIBS@
+CC = @CC@
+CCDEPMODE = @CCDEPMODE@
+CDB_LIBS = @CDB_LIBS@
+CFLAGS = @CFLAGS@
+CLUCENE_CFLAGS = @CLUCENE_CFLAGS@
+CLUCENE_LIBS = @CLUCENE_LIBS@
+COMPRESS_LIBS = @COMPRESS_LIBS@
+CPP = @CPP@
+CPPFLAGS = @CPPFLAGS@
+CRYPT_LIBS = @CRYPT_LIBS@
+CXX = @CXX@
+CXXCPP = @CXXCPP@
+CXXDEPMODE = @CXXDEPMODE@
+CXXFLAGS = @CXXFLAGS@
+CYGPATH_W = @CYGPATH_W@
+DEFS = @DEFS@
+DEPDIR = @DEPDIR@
+DICT_LIBS = @DICT_LIBS@
+DLLIB = @DLLIB@
+DLLTOOL = @DLLTOOL@
+DSYMUTIL = @DSYMUTIL@
+DUMPBIN = @DUMPBIN@
+ECHO_C = @ECHO_C@
+ECHO_N = @ECHO_N@
+ECHO_T = @ECHO_T@
+EGREP = @EGREP@
+EXEEXT = @EXEEXT@
+FGREP = @FGREP@
+FLEX = @FLEX@
+FUZZER_CPPFLAGS = @FUZZER_CPPFLAGS@
+FUZZER_LDFLAGS = @FUZZER_LDFLAGS@
+GREP = @GREP@
+INSTALL = @INSTALL@
+INSTALL_DATA = @INSTALL_DATA@
+INSTALL_PROGRAM = @INSTALL_PROGRAM@
+INSTALL_SCRIPT = @INSTALL_SCRIPT@
+INSTALL_STRIP_PROGRAM = @INSTALL_STRIP_PROGRAM@
+KRB5CONFIG = @KRB5CONFIG@
+KRB5_CFLAGS = @KRB5_CFLAGS@
+KRB5_LIBS = @KRB5_LIBS@
+LD = @LD@
+LDAP_LIBS = @LDAP_LIBS@
+LDFLAGS = @LDFLAGS@
+LD_NO_WHOLE_ARCHIVE = @LD_NO_WHOLE_ARCHIVE@
+LD_WHOLE_ARCHIVE = @LD_WHOLE_ARCHIVE@
+LIBCAP = @LIBCAP@
+LIBDOVECOT = @LIBDOVECOT@
+LIBDOVECOT_COMPRESS = @LIBDOVECOT_COMPRESS@
+LIBDOVECOT_DEPS = @LIBDOVECOT_DEPS@
+LIBDOVECOT_DSYNC = @LIBDOVECOT_DSYNC@
+LIBDOVECOT_LA_LIBS = @LIBDOVECOT_LA_LIBS@
+LIBDOVECOT_LDA = @LIBDOVECOT_LDA@
+LIBDOVECOT_LDAP = @LIBDOVECOT_LDAP@
+LIBDOVECOT_LIBFTS = @LIBDOVECOT_LIBFTS@
+LIBDOVECOT_LIBFTS_DEPS = @LIBDOVECOT_LIBFTS_DEPS@
+LIBDOVECOT_LOGIN = @LIBDOVECOT_LOGIN@
+LIBDOVECOT_LUA = @LIBDOVECOT_LUA@
+LIBDOVECOT_LUA_DEPS = @LIBDOVECOT_LUA_DEPS@
+LIBDOVECOT_SQL = @LIBDOVECOT_SQL@
+LIBDOVECOT_STORAGE = @LIBDOVECOT_STORAGE@
+LIBDOVECOT_STORAGE_DEPS = @LIBDOVECOT_STORAGE_DEPS@
+LIBEXTTEXTCAT_CFLAGS = @LIBEXTTEXTCAT_CFLAGS@
+LIBEXTTEXTCAT_LIBS = @LIBEXTTEXTCAT_LIBS@
+LIBICONV = @LIBICONV@
+LIBICU_CFLAGS = @LIBICU_CFLAGS@
+LIBICU_LIBS = @LIBICU_LIBS@
+LIBOBJS = @LIBOBJS@
+LIBS = @LIBS@
+LIBSODIUM_CFLAGS = @LIBSODIUM_CFLAGS@
+LIBSODIUM_LIBS = @LIBSODIUM_LIBS@
+LIBTIRPC_CFLAGS = @LIBTIRPC_CFLAGS@
+LIBTIRPC_LIBS = @LIBTIRPC_LIBS@
+LIBTOOL = @LIBTOOL@
+LIBUNWIND_CFLAGS = @LIBUNWIND_CFLAGS@
+LIBUNWIND_LIBS = @LIBUNWIND_LIBS@
+LIBWRAP_LIBS = @LIBWRAP_LIBS@
+LINKED_STORAGE_LDADD = @LINKED_STORAGE_LDADD@
+LIPO = @LIPO@
+LN_S = @LN_S@
+LTLIBICONV = @LTLIBICONV@
+LTLIBOBJS = @LTLIBOBJS@
+LT_SYS_LIBRARY_PATH = @LT_SYS_LIBRARY_PATH@
+LUA_CFLAGS = @LUA_CFLAGS@
+LUA_LIBS = @LUA_LIBS@
+MAINT = @MAINT@
+MAKEINFO = @MAKEINFO@
+MANIFEST_TOOL = @MANIFEST_TOOL@
+MKDIR_P = @MKDIR_P@
+MODULE_LIBS = @MODULE_LIBS@
+MODULE_SUFFIX = @MODULE_SUFFIX@
+MYSQL_CFLAGS = @MYSQL_CFLAGS@
+MYSQL_CONFIG = @MYSQL_CONFIG@
+MYSQL_LIBS = @MYSQL_LIBS@
+NM = @NM@
+NMEDIT = @NMEDIT@
+NOPLUGIN_LDFLAGS = @NOPLUGIN_LDFLAGS@
+OBJDUMP = @OBJDUMP@
+OBJEXT = @OBJEXT@
+OTOOL = @OTOOL@
+OTOOL64 = @OTOOL64@
+PACKAGE = @PACKAGE@
+PACKAGE_BUGREPORT = @PACKAGE_BUGREPORT@
+PACKAGE_NAME = @PACKAGE_NAME@
+PACKAGE_STRING = @PACKAGE_STRING@
+PACKAGE_TARNAME = @PACKAGE_TARNAME@
+PACKAGE_URL = @PACKAGE_URL@
+PACKAGE_VERSION = @PACKAGE_VERSION@
+PANDOC = @PANDOC@
+PATH_SEPARATOR = @PATH_SEPARATOR@
+PGSQL_CFLAGS = @PGSQL_CFLAGS@
+PGSQL_LIBS = @PGSQL_LIBS@
+PG_CONFIG = @PG_CONFIG@
+PIE_CFLAGS = @PIE_CFLAGS@
+PIE_LDFLAGS = @PIE_LDFLAGS@
+PKG_CONFIG = @PKG_CONFIG@
+PKG_CONFIG_LIBDIR = @PKG_CONFIG_LIBDIR@
+PKG_CONFIG_PATH = @PKG_CONFIG_PATH@
+QUOTA_LIBS = @QUOTA_LIBS@
+RANLIB = @RANLIB@
+RELRO_LDFLAGS = @RELRO_LDFLAGS@
+RPCGEN = @RPCGEN@
+RUN_TEST = @RUN_TEST@
+SED = @SED@
+SETTING_FILES = @SETTING_FILES@
+SET_MAKE = @SET_MAKE@
+SHELL = @SHELL@
+SQLITE_CFLAGS = @SQLITE_CFLAGS@
+SQLITE_LIBS = @SQLITE_LIBS@
+SQL_CFLAGS = @SQL_CFLAGS@
+SQL_LIBS = @SQL_LIBS@
+SSL_CFLAGS = @SSL_CFLAGS@
+SSL_LIBS = @SSL_LIBS@
+STRIP = @STRIP@
+SYSTEMD_CFLAGS = @SYSTEMD_CFLAGS@
+SYSTEMD_LIBS = @SYSTEMD_LIBS@
+VALGRIND = @VALGRIND@
+VERSION = @VERSION@
+ZSTD_CFLAGS = @ZSTD_CFLAGS@
+ZSTD_LIBS = @ZSTD_LIBS@
+abs_builddir = @abs_builddir@
+abs_srcdir = @abs_srcdir@
+abs_top_builddir = @abs_top_builddir@
+abs_top_srcdir = @abs_top_srcdir@
+ac_ct_AR = @ac_ct_AR@
+ac_ct_CC = @ac_ct_CC@
+ac_ct_CXX = @ac_ct_CXX@
+ac_ct_DUMPBIN = @ac_ct_DUMPBIN@
+am__include = @am__include@
+am__leading_dot = @am__leading_dot@
+am__quote = @am__quote@
+am__tar = @am__tar@
+am__untar = @am__untar@
+bindir = @bindir@
+build = @build@
+build_alias = @build_alias@
+build_cpu = @build_cpu@
+build_os = @build_os@
+build_vendor = @build_vendor@
+builddir = @builddir@
+datadir = @datadir@
+datarootdir = @datarootdir@
+dict_drivers = @dict_drivers@
+docdir = @docdir@
+dvidir = @dvidir@
+exec_prefix = @exec_prefix@
+host = @host@
+host_alias = @host_alias@
+host_cpu = @host_cpu@
+host_os = @host_os@
+host_vendor = @host_vendor@
+htmldir = @htmldir@
+includedir = @includedir@
+infodir = @infodir@
+install_sh = @install_sh@
+libdir = @libdir@
+libexecdir = @libexecdir@
+localedir = @localedir@
+localstatedir = @localstatedir@
+mandir = @mandir@
+mkdir_p = @mkdir_p@
+moduledir = @moduledir@
+oldincludedir = @oldincludedir@
+pdfdir = @pdfdir@
+prefix = @prefix@
+program_transform_name = @program_transform_name@
+psdir = @psdir@
+rundir = @rundir@
+runstatedir = @runstatedir@
+sbindir = @sbindir@
+sharedstatedir = @sharedstatedir@
+sql_drivers = @sql_drivers@
+srcdir = @srcdir@
+ssldir = @ssldir@
+statedir = @statedir@
+sysconfdir = @sysconfdir@
+systemdservicetype = @systemdservicetype@
+systemdsystemunitdir = @systemdsystemunitdir@
+target_alias = @target_alias@
+top_build_prefix = @top_build_prefix@
+top_builddir = @top_builddir@
+top_srcdir = @top_srcdir@
+noinst_LTLIBRARIES = libstorage_dbox_single.la
+AM_CPPFLAGS = \
+ -I$(top_srcdir)/src/lib \
+ -I$(top_srcdir)/src/lib-settings \
+ -I$(top_srcdir)/src/lib-master \
+ -I$(top_srcdir)/src/lib-fs \
+ -I$(top_srcdir)/src/lib-mail \
+ -I$(top_srcdir)/src/lib-imap \
+ -I$(top_srcdir)/src/lib-index \
+ -I$(top_srcdir)/src/lib-storage \
+ -I$(top_srcdir)/src/lib-storage/index \
+ -I$(top_srcdir)/src/lib-storage/index/dbox-common
+
+libstorage_dbox_single_la_SOURCES = \
+ sdbox-copy.c \
+ sdbox-file.c \
+ sdbox-mail.c \
+ sdbox-save.c \
+ sdbox-sync.c \
+ sdbox-sync-rebuild.c \
+ sdbox-storage.c
+
+headers = \
+ sdbox-file.h \
+ sdbox-storage.h \
+ sdbox-sync.h
+
+pkginc_libdir = $(pkgincludedir)
+pkginc_lib_HEADERS = $(headers)
+all: all-am
+
+.SUFFIXES:
+.SUFFIXES: .c .lo .o .obj
+$(srcdir)/Makefile.in: @MAINTAINER_MODE_TRUE@ $(srcdir)/Makefile.am $(am__configure_deps)
+ @for dep in $?; do \
+ case '$(am__configure_deps)' in \
+ *$$dep*) \
+ ( cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh ) \
+ && { if test -f $@; then exit 0; else break; fi; }; \
+ exit 1;; \
+ esac; \
+ done; \
+ echo ' cd $(top_srcdir) && $(AUTOMAKE) --foreign src/lib-storage/index/dbox-single/Makefile'; \
+ $(am__cd) $(top_srcdir) && \
+ $(AUTOMAKE) --foreign src/lib-storage/index/dbox-single/Makefile
+Makefile: $(srcdir)/Makefile.in $(top_builddir)/config.status
+ @case '$?' in \
+ *config.status*) \
+ cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh;; \
+ *) \
+ echo ' cd $(top_builddir) && $(SHELL) ./config.status $(subdir)/$@ $(am__maybe_remake_depfiles)'; \
+ cd $(top_builddir) && $(SHELL) ./config.status $(subdir)/$@ $(am__maybe_remake_depfiles);; \
+ esac;
+
+$(top_builddir)/config.status: $(top_srcdir)/configure $(CONFIG_STATUS_DEPENDENCIES)
+ cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh
+
+$(top_srcdir)/configure: @MAINTAINER_MODE_TRUE@ $(am__configure_deps)
+ cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh
+$(ACLOCAL_M4): @MAINTAINER_MODE_TRUE@ $(am__aclocal_m4_deps)
+ cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh
+$(am__aclocal_m4_deps):
+
+clean-noinstLTLIBRARIES:
+ -test -z "$(noinst_LTLIBRARIES)" || rm -f $(noinst_LTLIBRARIES)
+ @list='$(noinst_LTLIBRARIES)'; \
+ locs=`for p in $$list; do echo $$p; done | \
+ sed 's|^[^/]*$$|.|; s|/[^/]*$$||; s|$$|/so_locations|' | \
+ sort -u`; \
+ test -z "$$locs" || { \
+ echo rm -f $${locs}; \
+ rm -f $${locs}; \
+ }
+
+libstorage_dbox_single.la: $(libstorage_dbox_single_la_OBJECTS) $(libstorage_dbox_single_la_DEPENDENCIES) $(EXTRA_libstorage_dbox_single_la_DEPENDENCIES)
+ $(AM_V_CCLD)$(LINK) $(libstorage_dbox_single_la_OBJECTS) $(libstorage_dbox_single_la_LIBADD) $(LIBS)
+
+mostlyclean-compile:
+ -rm -f *.$(OBJEXT)
+
+distclean-compile:
+ -rm -f *.tab.c
+
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/sdbox-copy.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/sdbox-file.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/sdbox-mail.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/sdbox-save.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/sdbox-storage.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/sdbox-sync-rebuild.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/sdbox-sync.Plo@am__quote@ # am--include-marker
+
+$(am__depfiles_remade):
+ @$(MKDIR_P) $(@D)
+ @echo '# dummy' >$@-t && $(am__mv) $@-t $@
+
+am--depfiles: $(am__depfiles_remade)
+
+.c.o:
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(COMPILE) -MT $@ -MD -MP -MF $(DEPDIR)/$*.Tpo -c -o $@ $<
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/$*.Tpo $(DEPDIR)/$*.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='$<' object='$@' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(COMPILE) -c -o $@ $<
+
+.c.obj:
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(COMPILE) -MT $@ -MD -MP -MF $(DEPDIR)/$*.Tpo -c -o $@ `$(CYGPATH_W) '$<'`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/$*.Tpo $(DEPDIR)/$*.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='$<' object='$@' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(COMPILE) -c -o $@ `$(CYGPATH_W) '$<'`
+
+.c.lo:
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(LTCOMPILE) -MT $@ -MD -MP -MF $(DEPDIR)/$*.Tpo -c -o $@ $<
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/$*.Tpo $(DEPDIR)/$*.Plo
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='$<' object='$@' libtool=yes @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(LTCOMPILE) -c -o $@ $<
+
+mostlyclean-libtool:
+ -rm -f *.lo
+
+clean-libtool:
+ -rm -rf .libs _libs
+install-pkginc_libHEADERS: $(pkginc_lib_HEADERS)
+ @$(NORMAL_INSTALL)
+ @list='$(pkginc_lib_HEADERS)'; test -n "$(pkginc_libdir)" || list=; \
+ if test -n "$$list"; then \
+ echo " $(MKDIR_P) '$(DESTDIR)$(pkginc_libdir)'"; \
+ $(MKDIR_P) "$(DESTDIR)$(pkginc_libdir)" || exit 1; \
+ fi; \
+ for p in $$list; do \
+ if test -f "$$p"; then d=; else d="$(srcdir)/"; fi; \
+ echo "$$d$$p"; \
+ done | $(am__base_list) | \
+ while read files; do \
+ echo " $(INSTALL_HEADER) $$files '$(DESTDIR)$(pkginc_libdir)'"; \
+ $(INSTALL_HEADER) $$files "$(DESTDIR)$(pkginc_libdir)" || exit $$?; \
+ done
+
+uninstall-pkginc_libHEADERS:
+ @$(NORMAL_UNINSTALL)
+ @list='$(pkginc_lib_HEADERS)'; test -n "$(pkginc_libdir)" || list=; \
+ files=`for p in $$list; do echo $$p; done | sed -e 's|^.*/||'`; \
+ dir='$(DESTDIR)$(pkginc_libdir)'; $(am__uninstall_files_from_dir)
+
+ID: $(am__tagged_files)
+ $(am__define_uniq_tagged_files); mkid -fID $$unique
+tags: tags-am
+TAGS: tags
+
+tags-am: $(TAGS_DEPENDENCIES) $(am__tagged_files)
+ set x; \
+ here=`pwd`; \
+ $(am__define_uniq_tagged_files); \
+ shift; \
+ if test -z "$(ETAGS_ARGS)$$*$$unique"; then :; else \
+ test -n "$$unique" || unique=$$empty_fix; \
+ if test $$# -gt 0; then \
+ $(ETAGS) $(ETAGSFLAGS) $(AM_ETAGSFLAGS) $(ETAGS_ARGS) \
+ "$$@" $$unique; \
+ else \
+ $(ETAGS) $(ETAGSFLAGS) $(AM_ETAGSFLAGS) $(ETAGS_ARGS) \
+ $$unique; \
+ fi; \
+ fi
+ctags: ctags-am
+
+CTAGS: ctags
+ctags-am: $(TAGS_DEPENDENCIES) $(am__tagged_files)
+ $(am__define_uniq_tagged_files); \
+ test -z "$(CTAGS_ARGS)$$unique" \
+ || $(CTAGS) $(CTAGSFLAGS) $(AM_CTAGSFLAGS) $(CTAGS_ARGS) \
+ $$unique
+
+GTAGS:
+ here=`$(am__cd) $(top_builddir) && pwd` \
+ && $(am__cd) $(top_srcdir) \
+ && gtags -i $(GTAGS_ARGS) "$$here"
+cscopelist: cscopelist-am
+
+cscopelist-am: $(am__tagged_files)
+ list='$(am__tagged_files)'; \
+ case "$(srcdir)" in \
+ [\\/]* | ?:[\\/]*) sdir="$(srcdir)" ;; \
+ *) sdir=$(subdir)/$(srcdir) ;; \
+ esac; \
+ for i in $$list; do \
+ if test -f "$$i"; then \
+ echo "$(subdir)/$$i"; \
+ else \
+ echo "$$sdir/$$i"; \
+ fi; \
+ done >> $(top_builddir)/cscope.files
+
+distclean-tags:
+ -rm -f TAGS ID GTAGS GRTAGS GSYMS GPATH tags
+
+distdir: $(BUILT_SOURCES)
+ $(MAKE) $(AM_MAKEFLAGS) distdir-am
+
+distdir-am: $(DISTFILES)
+ @srcdirstrip=`echo "$(srcdir)" | sed 's/[].[^$$\\*]/\\\\&/g'`; \
+ topsrcdirstrip=`echo "$(top_srcdir)" | sed 's/[].[^$$\\*]/\\\\&/g'`; \
+ list='$(DISTFILES)'; \
+ dist_files=`for file in $$list; do echo $$file; done | \
+ sed -e "s|^$$srcdirstrip/||;t" \
+ -e "s|^$$topsrcdirstrip/|$(top_builddir)/|;t"`; \
+ case $$dist_files in \
+ */*) $(MKDIR_P) `echo "$$dist_files" | \
+ sed '/\//!d;s|^|$(distdir)/|;s,/[^/]*$$,,' | \
+ sort -u` ;; \
+ esac; \
+ for file in $$dist_files; do \
+ if test -f $$file || test -d $$file; then d=.; else d=$(srcdir); fi; \
+ if test -d $$d/$$file; then \
+ dir=`echo "/$$file" | sed -e 's,/[^/]*$$,,'`; \
+ if test -d "$(distdir)/$$file"; then \
+ find "$(distdir)/$$file" -type d ! -perm -700 -exec chmod u+rwx {} \;; \
+ fi; \
+ if test -d $(srcdir)/$$file && test $$d != $(srcdir); then \
+ cp -fpR $(srcdir)/$$file "$(distdir)$$dir" || exit 1; \
+ find "$(distdir)/$$file" -type d ! -perm -700 -exec chmod u+rwx {} \;; \
+ fi; \
+ cp -fpR $$d/$$file "$(distdir)$$dir" || exit 1; \
+ else \
+ test -f "$(distdir)/$$file" \
+ || cp -p $$d/$$file "$(distdir)/$$file" \
+ || exit 1; \
+ fi; \
+ done
+check-am: all-am
+check: check-am
+all-am: Makefile $(LTLIBRARIES) $(HEADERS)
+installdirs:
+ for dir in "$(DESTDIR)$(pkginc_libdir)"; do \
+ test -z "$$dir" || $(MKDIR_P) "$$dir"; \
+ done
+install: install-am
+install-exec: install-exec-am
+install-data: install-data-am
+uninstall: uninstall-am
+
+install-am: all-am
+ @$(MAKE) $(AM_MAKEFLAGS) install-exec-am install-data-am
+
+installcheck: installcheck-am
+install-strip:
+ if test -z '$(STRIP)'; then \
+ $(MAKE) $(AM_MAKEFLAGS) INSTALL_PROGRAM="$(INSTALL_STRIP_PROGRAM)" \
+ install_sh_PROGRAM="$(INSTALL_STRIP_PROGRAM)" INSTALL_STRIP_FLAG=-s \
+ install; \
+ else \
+ $(MAKE) $(AM_MAKEFLAGS) INSTALL_PROGRAM="$(INSTALL_STRIP_PROGRAM)" \
+ install_sh_PROGRAM="$(INSTALL_STRIP_PROGRAM)" INSTALL_STRIP_FLAG=-s \
+ "INSTALL_PROGRAM_ENV=STRIPPROG='$(STRIP)'" install; \
+ fi
+mostlyclean-generic:
+
+clean-generic:
+
+distclean-generic:
+ -test -z "$(CONFIG_CLEAN_FILES)" || rm -f $(CONFIG_CLEAN_FILES)
+ -test . = "$(srcdir)" || test -z "$(CONFIG_CLEAN_VPATH_FILES)" || rm -f $(CONFIG_CLEAN_VPATH_FILES)
+
+maintainer-clean-generic:
+ @echo "This command is intended for maintainers to use"
+ @echo "it deletes files that may require special tools to rebuild."
+clean: clean-am
+
+clean-am: clean-generic clean-libtool clean-noinstLTLIBRARIES \
+ mostlyclean-am
+
+distclean: distclean-am
+ -rm -f ./$(DEPDIR)/sdbox-copy.Plo
+ -rm -f ./$(DEPDIR)/sdbox-file.Plo
+ -rm -f ./$(DEPDIR)/sdbox-mail.Plo
+ -rm -f ./$(DEPDIR)/sdbox-save.Plo
+ -rm -f ./$(DEPDIR)/sdbox-storage.Plo
+ -rm -f ./$(DEPDIR)/sdbox-sync-rebuild.Plo
+ -rm -f ./$(DEPDIR)/sdbox-sync.Plo
+ -rm -f Makefile
+distclean-am: clean-am distclean-compile distclean-generic \
+ distclean-tags
+
+dvi: dvi-am
+
+dvi-am:
+
+html: html-am
+
+html-am:
+
+info: info-am
+
+info-am:
+
+install-data-am: install-pkginc_libHEADERS
+
+install-dvi: install-dvi-am
+
+install-dvi-am:
+
+install-exec-am:
+
+install-html: install-html-am
+
+install-html-am:
+
+install-info: install-info-am
+
+install-info-am:
+
+install-man:
+
+install-pdf: install-pdf-am
+
+install-pdf-am:
+
+install-ps: install-ps-am
+
+install-ps-am:
+
+installcheck-am:
+
+maintainer-clean: maintainer-clean-am
+ -rm -f ./$(DEPDIR)/sdbox-copy.Plo
+ -rm -f ./$(DEPDIR)/sdbox-file.Plo
+ -rm -f ./$(DEPDIR)/sdbox-mail.Plo
+ -rm -f ./$(DEPDIR)/sdbox-save.Plo
+ -rm -f ./$(DEPDIR)/sdbox-storage.Plo
+ -rm -f ./$(DEPDIR)/sdbox-sync-rebuild.Plo
+ -rm -f ./$(DEPDIR)/sdbox-sync.Plo
+ -rm -f Makefile
+maintainer-clean-am: distclean-am maintainer-clean-generic
+
+mostlyclean: mostlyclean-am
+
+mostlyclean-am: mostlyclean-compile mostlyclean-generic \
+ mostlyclean-libtool
+
+pdf: pdf-am
+
+pdf-am:
+
+ps: ps-am
+
+ps-am:
+
+uninstall-am: uninstall-pkginc_libHEADERS
+
+.MAKE: install-am install-strip
+
+.PHONY: CTAGS GTAGS TAGS all all-am am--depfiles check check-am clean \
+ clean-generic clean-libtool clean-noinstLTLIBRARIES \
+ cscopelist-am ctags ctags-am distclean distclean-compile \
+ distclean-generic distclean-libtool distclean-tags distdir dvi \
+ dvi-am html html-am info info-am install install-am \
+ install-data install-data-am install-dvi install-dvi-am \
+ install-exec install-exec-am install-html install-html-am \
+ install-info install-info-am install-man install-pdf \
+ install-pdf-am install-pkginc_libHEADERS install-ps \
+ install-ps-am install-strip installcheck installcheck-am \
+ installdirs maintainer-clean maintainer-clean-generic \
+ mostlyclean mostlyclean-compile mostlyclean-generic \
+ mostlyclean-libtool pdf pdf-am ps ps-am tags tags-am uninstall \
+ uninstall-am uninstall-pkginc_libHEADERS
+
+.PRECIOUS: Makefile
+
+
+# Tell versions [3.59,3.63) of GNU make to not export all variables.
+# Otherwise a system limit (for SysV at least) may be exceeded.
+.NOEXPORT:
diff --git a/src/lib-storage/index/dbox-single/sdbox-copy.c b/src/lib-storage/index/dbox-single/sdbox-copy.c
new file mode 100644
index 0000000..48a3f59
--- /dev/null
+++ b/src/lib-storage/index/dbox-single/sdbox-copy.c
@@ -0,0 +1,185 @@
+/* Copyright (c) 2002-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "nfs-workarounds.h"
+#include "fs-api.h"
+#include "dbox-save.h"
+#include "dbox-attachment.h"
+#include "sdbox-storage.h"
+#include "sdbox-file.h"
+#include "mail-copy.h"
+
+static int
+sdbox_file_copy_attachments(struct sdbox_file *src_file,
+ struct sdbox_file *dest_file)
+{
+ struct dbox_storage *src_storage = src_file->file.storage;
+ struct dbox_storage *dest_storage = dest_file->file.storage;
+ struct fs_file *src_fsfile, *dest_fsfile;
+ ARRAY_TYPE(mail_attachment_extref) extrefs;
+ const struct mail_attachment_extref *extref;
+ const char *extrefs_line, *src, *dest, *dest_relpath;
+ pool_t pool;
+ int ret;
+
+ if (src_storage->attachment_dir == NULL) {
+ /* no attachments in source storage */
+ return 1;
+ }
+ if (dest_storage->attachment_dir == NULL ||
+ strcmp(src_storage->attachment_dir,
+ dest_storage->attachment_dir) != 0 ||
+ strcmp(src_storage->storage.set->mail_attachment_fs,
+ dest_storage->storage.set->mail_attachment_fs) != 0 ||
+ strcmp(src_storage->storage.set->mail_attachment_hash,
+ dest_storage->storage.set->mail_attachment_hash) != 0) {
+ /* different attachment dirs/settings between storages.
+ have to copy the slow way. */
+ return 0;
+ }
+
+ if ((ret = sdbox_file_get_attachments(&src_file->file,
+ &extrefs_line)) <= 0)
+ return ret < 0 ? -1 : 1;
+
+ pool = pool_alloconly_create("sdbox attachments copy", 1024);
+ p_array_init(&extrefs, pool, 16);
+ if (!index_attachment_parse_extrefs(extrefs_line, pool, &extrefs)) {
+ mailbox_set_critical(&dest_file->mbox->box,
+ "Can't copy %s with corrupted extref metadata: %s",
+ src_file->file.cur_path, extrefs_line);
+ pool_unref(&pool);
+ return -1;
+ }
+
+ dest_file->attachment_pool =
+ pool_alloconly_create("sdbox attachment copy paths", 512);
+ p_array_init(&dest_file->attachment_paths, dest_file->attachment_pool,
+ array_count(&extrefs));
+
+ ret = 1;
+ array_foreach(&extrefs, extref) T_BEGIN {
+ src = t_strdup_printf("%s/%s", dest_storage->attachment_dir,
+ sdbox_file_attachment_relpath(src_file, extref->path));
+ dest_relpath = p_strconcat(dest_file->attachment_pool,
+ extref->path, "-",
+ guid_generate(), NULL);
+ dest = t_strdup_printf("%s/%s", dest_storage->attachment_dir,
+ dest_relpath);
+ /* we verified above that attachment_fs is compatible for
+ src and dest, so it doesn't matter which storage's
+ attachment_fs we use. in any case we need to use the same
+ one or fs_copy() will crash with assert. */
+ src_fsfile = fs_file_init(dest_storage->attachment_fs, src,
+ FS_OPEN_MODE_READONLY);
+ dest_fsfile = fs_file_init(dest_storage->attachment_fs, dest,
+ FS_OPEN_MODE_READONLY);
+ if (fs_copy(src_fsfile, dest_fsfile) < 0) {
+ mailbox_set_critical(&dest_file->mbox->box, "%s",
+ fs_file_last_error(dest_fsfile));
+ ret = -1;
+ } else {
+ array_push_back(&dest_file->attachment_paths,
+ &dest_relpath);
+ }
+ fs_file_deinit(&src_fsfile);
+ fs_file_deinit(&dest_fsfile);
+ } T_END;
+ pool_unref(&pool);
+ return ret;
+}
+
+static int
+sdbox_copy_hardlink(struct mail_save_context *_ctx, struct mail *mail)
+{
+ struct dbox_save_context *ctx = DBOX_SAVECTX(_ctx);
+ struct sdbox_mailbox *dest_mbox = SDBOX_MAILBOX(_ctx->transaction->box);
+ struct sdbox_mailbox *src_mbox;
+ struct dbox_file *src_file, *dest_file;
+ const char *src_path, *dest_path;
+ int ret;
+
+ if (strcmp(mail->box->storage->name, SDBOX_STORAGE_NAME) == 0)
+ src_mbox = SDBOX_MAILBOX(mail->box);
+ else {
+ /* Source storage isn't sdbox, can't hard link */
+ return 0;
+ }
+
+ src_file = sdbox_file_init(src_mbox, mail->uid);
+ dest_file = sdbox_file_init(dest_mbox, 0);
+
+ ctx->ctx.data.flags &= ENUM_NEGATE(DBOX_INDEX_FLAG_ALT);
+
+ src_path = src_file->primary_path;
+ dest_path = dest_file->primary_path;
+ ret = nfs_safe_link(src_path, dest_path, FALSE);
+ if (ret < 0 && errno == ENOENT && src_file->alt_path != NULL) {
+ src_path = src_file->alt_path;
+ if (dest_file->alt_path != NULL) {
+ dest_path = dest_file->cur_path = dest_file->alt_path;
+ ctx->ctx.data.flags |= DBOX_INDEX_FLAG_ALT;
+ }
+ ret = nfs_safe_link(src_path, dest_path, FALSE);
+ }
+ if (ret < 0) {
+ if (ECANTLINK(errno))
+ ret = 0;
+ else if (errno == ENOENT) {
+ /* try if the fallback copying code can still
+ read the file (the mail could still have the
+ stream open) */
+ ret = 0;
+ } else {
+ mail_set_critical(mail, "link(%s, %s) failed: %m",
+ src_path, dest_path);
+ }
+ dbox_file_unref(&src_file);
+ dbox_file_unref(&dest_file);
+ return ret;
+ }
+
+ ret = sdbox_file_copy_attachments((struct sdbox_file *)src_file,
+ (struct sdbox_file *)dest_file);
+ if (ret <= 0) {
+ (void)sdbox_file_unlink_aborted_save((struct sdbox_file *)dest_file);
+ dbox_file_unref(&src_file);
+ dbox_file_unref(&dest_file);
+ return ret;
+ }
+ ((struct sdbox_file *)dest_file)->written_to_disk = TRUE;
+
+ dbox_save_add_to_index(ctx);
+ index_copy_cache_fields(_ctx, mail, ctx->seq);
+
+ sdbox_save_add_file(_ctx, dest_file);
+ mail_set_seq_saving(_ctx->dest_mail, ctx->seq);
+ dbox_file_unref(&src_file);
+ return 1;
+}
+
+int sdbox_copy(struct mail_save_context *_ctx, struct mail *mail)
+{
+ struct dbox_save_context *ctx = (struct dbox_save_context *)_ctx;
+ struct mailbox_transaction_context *_t = _ctx->transaction;
+ struct sdbox_mailbox *mbox = (struct sdbox_mailbox *)_t->box;
+ int ret;
+
+ i_assert((_t->flags & MAILBOX_TRANSACTION_FLAG_EXTERNAL) != 0);
+
+ ctx->finished = TRUE;
+ if (mail_storage_copy_can_use_hardlink(mail->box, &mbox->box) &&
+ _ctx->data.guid == NULL) {
+ T_BEGIN {
+ ret = sdbox_copy_hardlink(_ctx, mail);
+ } T_END;
+
+ if (ret != 0) {
+ index_save_context_free(_ctx);
+ return ret > 0 ? 0 : -1;
+ }
+
+ /* non-fatal hardlinking failure, try the slow way */
+ }
+ return mail_storage_copy(_ctx, mail);
+}
diff --git a/src/lib-storage/index/dbox-single/sdbox-file.c b/src/lib-storage/index/dbox-single/sdbox-file.c
new file mode 100644
index 0000000..fed111a
--- /dev/null
+++ b/src/lib-storage/index/dbox-single/sdbox-file.c
@@ -0,0 +1,447 @@
+/* Copyright (c) 2007-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "eacces-error.h"
+#include "fdatasync-path.h"
+#include "mkdir-parents.h"
+#include "istream.h"
+#include "ostream.h"
+#include "str.h"
+#include "fs-api.h"
+#include "dbox-attachment.h"
+#include "sdbox-storage.h"
+#include "sdbox-file.h"
+
+#include <stdio.h>
+#include <utime.h>
+
+static void sdbox_file_init_paths(struct sdbox_file *file, const char *fname)
+{
+ struct mailbox *box = &file->mbox->box;
+ const char *alt_path;
+
+ i_free(file->file.primary_path);
+ i_free(file->file.alt_path);
+ file->file.primary_path =
+ i_strdup_printf("%s/%s", mailbox_get_path(box), fname);
+ file->file.cur_path = file->file.primary_path;
+
+ if (mailbox_get_path_to(box, MAILBOX_LIST_PATH_TYPE_ALT_MAILBOX,
+ &alt_path) > 0)
+ file->file.alt_path = i_strdup_printf("%s/%s", alt_path, fname);
+}
+
+struct dbox_file *sdbox_file_init(struct sdbox_mailbox *mbox, uint32_t uid)
+{
+ struct sdbox_file *file;
+ const char *fname;
+
+ file = i_new(struct sdbox_file, 1);
+ file->file.storage = &mbox->storage->storage;
+ file->mbox = mbox;
+ T_BEGIN {
+ if (uid != 0) {
+ fname = t_strdup_printf(SDBOX_MAIL_FILE_FORMAT, uid);
+ sdbox_file_init_paths(file, fname);
+ file->uid = uid;
+ } else {
+ sdbox_file_init_paths(file, dbox_generate_tmp_filename());
+ }
+ } T_END;
+ dbox_file_init(&file->file);
+ return &file->file;
+}
+
+struct dbox_file *sdbox_file_create(struct sdbox_mailbox *mbox)
+{
+ struct dbox_file *file;
+
+ file = sdbox_file_init(mbox, 0);
+ file->fd = file->storage->v.
+ file_create_fd(file, file->primary_path, FALSE);
+ return file;
+}
+
+void sdbox_file_free(struct dbox_file *file)
+{
+ struct sdbox_file *sfile = (struct sdbox_file *)file;
+
+ pool_unref(&sfile->attachment_pool);
+ dbox_file_free(file);
+}
+
+int sdbox_file_get_attachments(struct dbox_file *file, const char **extrefs_r)
+{
+ const char *line;
+ bool deleted;
+ int ret;
+
+ *extrefs_r = NULL;
+
+ /* read the metadata */
+ ret = dbox_file_open(file, &deleted);
+ if (ret > 0) {
+ if (deleted)
+ return 0;
+ if ((ret = dbox_file_seek(file, 0)) > 0)
+ ret = dbox_file_metadata_read(file);
+ }
+ if (ret <= 0) {
+ if (ret < 0)
+ return -1;
+ /* corrupted file. we're deleting it anyway. */
+ line = NULL;
+ } else {
+ line = dbox_file_metadata_get(file, DBOX_METADATA_EXT_REF);
+ }
+ if (line == NULL) {
+ /* no attachments */
+ return 0;
+ }
+ *extrefs_r = line;
+ return 1;
+}
+
+const char *
+sdbox_file_attachment_relpath(struct sdbox_file *file, const char *srcpath)
+{
+ const char *p;
+
+ p = strchr(srcpath, '-');
+ if (p == NULL) {
+ mailbox_set_critical(&file->mbox->box,
+ "sdbox attachment path in invalid format: %s", srcpath);
+ } else {
+ p = strchr(p+1, '-');
+ }
+ return t_strdup_printf("%s-%s-%u",
+ p == NULL ? srcpath : t_strdup_until(srcpath, p),
+ guid_128_to_string(file->mbox->mailbox_guid),
+ file->uid);
+}
+
+static int sdbox_file_rename_attachments(struct sdbox_file *file)
+{
+ struct dbox_storage *storage = file->file.storage;
+ struct fs_file *src_file, *dest_file;
+ const char *path, *src, *dest;
+ int ret = 0;
+
+ array_foreach_elem(&file->attachment_paths, path) T_BEGIN {
+ src = t_strdup_printf("%s/%s", storage->attachment_dir, path);
+ dest = t_strdup_printf("%s/%s", storage->attachment_dir,
+ sdbox_file_attachment_relpath(file, path));
+ src_file = fs_file_init(storage->attachment_fs, src,
+ FS_OPEN_MODE_READONLY);
+ dest_file = fs_file_init(storage->attachment_fs, dest,
+ FS_OPEN_MODE_READONLY);
+ if (fs_rename(src_file, dest_file) < 0) {
+ mailbox_set_critical(&file->mbox->box, "%s",
+ fs_file_last_error(dest_file));
+ ret = -1;
+ }
+ fs_file_deinit(&src_file);
+ fs_file_deinit(&dest_file);
+ } T_END;
+ return ret;
+}
+
+int sdbox_file_assign_uid(struct sdbox_file *file, uint32_t uid)
+{
+ const char *p, *old_path, *dir, *new_fname, *new_path;
+ struct stat st;
+
+ i_assert(file->uid == 0);
+ i_assert(uid != 0);
+
+ old_path = file->file.cur_path;
+ p = strrchr(old_path, '/');
+ i_assert(p != NULL);
+ dir = t_strdup_until(old_path, p);
+
+ new_fname = t_strdup_printf(SDBOX_MAIL_FILE_FORMAT, uid);
+ new_path = t_strdup_printf("%s/%s", dir, new_fname);
+
+ if (stat(new_path, &st) == 0) {
+ mailbox_set_critical(&file->mbox->box,
+ "sdbox: %s already exists, rebuilding index", new_path);
+ sdbox_set_mailbox_corrupted(&file->mbox->box);
+ return -1;
+ }
+ if (rename(old_path, new_path) < 0) {
+ mailbox_set_critical(&file->mbox->box,
+ "rename(%s, %s) failed: %m",
+ old_path, new_path);
+ return -1;
+ }
+ sdbox_file_init_paths(file, new_fname);
+ file->uid = uid;
+
+ if (array_is_created(&file->attachment_paths)) {
+ if (sdbox_file_rename_attachments(file) < 0)
+ return -1;
+ }
+ return 0;
+}
+
+static int sdbox_file_unlink_aborted_save_attachments(struct sdbox_file *file)
+{
+ struct dbox_storage *storage = file->file.storage;
+ struct fs *fs = storage->attachment_fs;
+ struct fs_file *fs_file;
+ const char *path, *att_path;
+ int ret = 0;
+
+ array_foreach_elem(&file->attachment_paths, att_path) T_BEGIN {
+ /* we don't know if we aborted before renaming this attachment,
+ so try deleting both source and dest path. the source paths
+ point to temporary files (not to source messages'
+ attachment paths), so it's safe to delete them. */
+ path = t_strdup_printf("%s/%s", storage->attachment_dir,
+ att_path);
+ fs_file = fs_file_init(fs, path, FS_OPEN_MODE_READONLY);
+ if (fs_delete(fs_file) < 0 &&
+ errno != ENOENT) {
+ mailbox_set_critical(&file->mbox->box, "%s",
+ fs_file_last_error(fs_file));
+ ret = -1;
+ }
+ fs_file_deinit(&fs_file);
+
+ path = t_strdup_printf("%s/%s", storage->attachment_dir,
+ sdbox_file_attachment_relpath(file, att_path));
+ fs_file = fs_file_init(fs, path, FS_OPEN_MODE_READONLY);
+ if (fs_delete(fs_file) < 0 &&
+ errno != ENOENT) {
+ mailbox_set_critical(&file->mbox->box, "%s",
+ fs_file_last_error(fs_file));
+ ret = -1;
+ }
+ fs_file_deinit(&fs_file);
+ } T_END;
+ return ret;
+}
+
+int sdbox_file_unlink_aborted_save(struct sdbox_file *file)
+{
+ int ret = 0;
+
+ if (unlink(file->file.cur_path) < 0) {
+ mailbox_set_critical(&file->mbox->box,
+ "unlink(%s) failed: %m", file->file.cur_path);
+ ret = -1;
+ }
+ if (array_is_created(&file->attachment_paths)) {
+ if (sdbox_file_unlink_aborted_save_attachments(file) < 0)
+ ret = -1;
+ }
+ return ret;
+}
+
+int sdbox_file_create_fd(struct dbox_file *file, const char *path, bool parents)
+{
+ struct sdbox_file *sfile = (struct sdbox_file *)file;
+ struct mailbox *box = &sfile->mbox->box;
+ const struct mailbox_permissions *perm = mailbox_get_permissions(box);
+ const char *p, *dir;
+ mode_t old_mask;
+ int fd;
+
+ old_mask = umask(0666 & ~perm->file_create_mode);
+ fd = open(path, O_RDWR | O_CREAT | O_TRUNC, 0666);
+ umask(old_mask);
+ if (fd == -1 && errno == ENOENT && parents &&
+ (p = strrchr(path, '/')) != NULL) {
+ dir = t_strdup_until(path, p);
+ if (mkdir_parents_chgrp(dir, perm->dir_create_mode,
+ perm->file_create_gid,
+ perm->file_create_gid_origin) < 0 &&
+ errno != EEXIST) {
+ mailbox_set_critical(box,
+ "mkdir_parents(%s) failed: %m", dir);
+ return -1;
+ }
+ /* try again */
+ old_mask = umask(0666 & ~perm->file_create_mode);
+ fd = open(path, O_RDWR | O_CREAT | O_TRUNC, 0666);
+ umask(old_mask);
+ }
+ if (fd == -1) {
+ mailbox_set_critical(box, "open(%s, O_CREAT) failed: %m", path);
+ } else if (perm->file_create_gid == (gid_t)-1) {
+ /* no group change */
+ } else if (fchown(fd, (uid_t)-1, perm->file_create_gid) < 0) {
+ if (errno == EPERM) {
+ mailbox_set_critical(box, "%s",
+ eperm_error_get_chgrp("fchown", path,
+ perm->file_create_gid,
+ perm->file_create_gid_origin));
+ } else {
+ mailbox_set_critical(box,
+ "fchown(%s, -1, %ld) failed: %m",
+ path, (long)perm->file_create_gid);
+ }
+ /* continue anyway */
+ }
+ return fd;
+}
+
+int sdbox_file_move(struct dbox_file *file, bool alt_path)
+{
+ struct mail_storage *storage = &file->storage->storage;
+ struct ostream *output;
+ const char *dest_dir, *temp_path, *dest_path, *p;
+ struct stat st;
+ struct utimbuf ut;
+ bool deleted;
+ int out_fd, ret = 0;
+
+ i_assert(file->input != NULL);
+
+ if (dbox_file_is_in_alt(file) == alt_path)
+ return 0;
+ if (file->alt_path == NULL)
+ return 0;
+
+ if (stat(file->cur_path, &st) < 0 && errno == ENOENT) {
+ /* already expunged/moved by another session */
+ return 0;
+ }
+
+ dest_path = !alt_path ? file->primary_path : file->alt_path;
+
+ i_assert(dest_path != NULL);
+
+ p = strrchr(dest_path, '/');
+ i_assert(p != NULL);
+ dest_dir = t_strdup_until(dest_path, p);
+ temp_path = t_strdup_printf("%s/%s", dest_dir,
+ dbox_generate_tmp_filename());
+
+ /* first copy the file. make sure to catch every possible error
+ since we really don't want to break the file. */
+ out_fd = file->storage->v.file_create_fd(file, temp_path, TRUE);
+ if (out_fd == -1)
+ return -1;
+
+ output = o_stream_create_fd_file(out_fd, 0, FALSE);
+ i_stream_seek(file->input, 0);
+ o_stream_nsend_istream(output, file->input);
+ if (o_stream_finish(output) < 0) {
+ mail_storage_set_critical(storage, "write(%s) failed: %s",
+ temp_path, o_stream_get_error(output));
+ ret = -1;
+ }
+ o_stream_unref(&output);
+
+ if (storage->set->parsed_fsync_mode != FSYNC_MODE_NEVER && ret == 0) {
+ if (fsync(out_fd) < 0) {
+ mail_storage_set_critical(storage,
+ "fsync(%s) failed: %m", temp_path);
+ ret = -1;
+ }
+ }
+ if (close(out_fd) < 0) {
+ mail_storage_set_critical(storage,
+ "close(%s) failed: %m", temp_path);
+ ret = -1;
+ }
+ if (ret < 0) {
+ i_unlink(temp_path);
+ return -1;
+ }
+ /* preserve the original atime/mtime. this isn't necessary for Dovecot,
+ but could be useful for external reasons. */
+ ut.actime = st.st_atime;
+ ut.modtime = st.st_mtime;
+ if (utime(temp_path, &ut) < 0) {
+ mail_storage_set_critical(storage,
+ "utime(%s) failed: %m", temp_path);
+ }
+
+ /* the temp file was successfully written. rename it now to the
+ destination file. the destination shouldn't exist, but if it does
+ its contents should be the same (except for maybe older metadata) */
+ if (rename(temp_path, dest_path) < 0) {
+ mail_storage_set_critical(storage,
+ "rename(%s, %s) failed: %m", temp_path, dest_path);
+ i_unlink_if_exists(temp_path);
+ return -1;
+ }
+ if (storage->set->parsed_fsync_mode != FSYNC_MODE_NEVER) {
+ if (fdatasync_path(dest_dir) < 0) {
+ mail_storage_set_critical(storage,
+ "fdatasync(%s) failed: %m", dest_dir);
+ i_unlink(dest_path);
+ return -1;
+ }
+ }
+ if (unlink(file->cur_path) < 0) {
+ dbox_file_set_syscall_error(file, "unlink()");
+ if (errno == EACCES) {
+ /* configuration problem? revert the write */
+ i_unlink(dest_path);
+ }
+ /* who knows what happened to the file. keep both just to be
+ sure both won't get deleted. */
+ return -1;
+ }
+
+ /* file was successfully moved - reopen it */
+ dbox_file_close(file);
+ if (dbox_file_open(file, &deleted) <= 0) {
+ mail_storage_set_critical(storage,
+ "dbox_file_move(%s): reopening file failed", dest_path);
+ return -1;
+ }
+ return 0;
+}
+
+static int
+sdbox_unlink_attachments(struct sdbox_file *sfile,
+ const ARRAY_TYPE(mail_attachment_extref) *extrefs)
+{
+ struct dbox_storage *storage = sfile->file.storage;
+ const struct mail_attachment_extref *extref;
+ const char *path;
+ int ret = 0;
+
+ array_foreach(extrefs, extref) T_BEGIN {
+ path = sdbox_file_attachment_relpath(sfile, extref->path);
+ if (index_attachment_delete(&storage->storage,
+ storage->attachment_fs, path) < 0)
+ ret = -1;
+ } T_END;
+ return ret;
+}
+
+int sdbox_file_unlink_with_attachments(struct sdbox_file *sfile)
+{
+ ARRAY_TYPE(mail_attachment_extref) extrefs;
+ const char *extrefs_line;
+ pool_t pool;
+ int ret;
+
+ ret = sdbox_file_get_attachments(&sfile->file, &extrefs_line);
+ if (ret < 0)
+ return -1;
+ if (ret == 0) {
+ /* no attachments */
+ return dbox_file_unlink(&sfile->file);
+ }
+
+ pool = pool_alloconly_create("sdbox attachments unlink", 1024);
+ p_array_init(&extrefs, pool, 16);
+ if (!index_attachment_parse_extrefs(extrefs_line, pool, &extrefs)) {
+ i_warning("%s: Ignoring corrupted extref: %s",
+ sfile->file.cur_path, extrefs_line);
+ array_clear(&extrefs);
+ }
+
+ /* try to delete the file first, so if it fails we don't have
+ missing attachments */
+ if ((ret = dbox_file_unlink(&sfile->file)) >= 0)
+ (void)sdbox_unlink_attachments(sfile, &extrefs);
+ pool_unref(&pool);
+ return ret;
+}
diff --git a/src/lib-storage/index/dbox-single/sdbox-file.h b/src/lib-storage/index/dbox-single/sdbox-file.h
new file mode 100644
index 0000000..ba5a7f9
--- /dev/null
+++ b/src/lib-storage/index/dbox-single/sdbox-file.h
@@ -0,0 +1,43 @@
+#ifndef SDBOX_FILE_H
+#define SDBOX_FILE_H
+
+#include "dbox-file.h"
+
+struct sdbox_file {
+ struct dbox_file file;
+ struct sdbox_mailbox *mbox;
+
+ /* 0 while file is being created */
+ uint32_t uid;
+
+ /* list of attachment paths while saving/copying message */
+ pool_t attachment_pool;
+ ARRAY_TYPE(const_string) attachment_paths;
+ bool written_to_disk;
+};
+
+struct dbox_file *sdbox_file_init(struct sdbox_mailbox *mbox, uint32_t uid);
+struct dbox_file *sdbox_file_create(struct sdbox_mailbox *mbox);
+void sdbox_file_free(struct dbox_file *file);
+
+/* Get file's extrefs metadata. */
+int sdbox_file_get_attachments(struct dbox_file *file, const char **extrefs_r);
+/* Returns attachment path for this file, given the source path. The result is
+ always <hash>-<guid>-<mailbox_guid>-<uid>. The source path is expected to
+ contain <hash>-<guid>[-*]. */
+const char *
+sdbox_file_attachment_relpath(struct sdbox_file *file, const char *srcpath);
+
+/* Assign UID for a newly created file (by renaming it) */
+int sdbox_file_assign_uid(struct sdbox_file *file, uint32_t uid);
+
+int sdbox_file_create_fd(struct dbox_file *file, const char *path,
+ bool parents);
+/* Move the file to alt path or back. */
+int sdbox_file_move(struct dbox_file *file, bool alt_path);
+/* Unlink file and all of its referenced attachments. */
+int sdbox_file_unlink_with_attachments(struct sdbox_file *sfile);
+/* Unlink file and its attachments when rolling back a saved message. */
+int sdbox_file_unlink_aborted_save(struct sdbox_file *file);
+
+#endif
diff --git a/src/lib-storage/index/dbox-single/sdbox-mail.c b/src/lib-storage/index/dbox-single/sdbox-mail.c
new file mode 100644
index 0000000..3b0352c
--- /dev/null
+++ b/src/lib-storage/index/dbox-single/sdbox-mail.c
@@ -0,0 +1,182 @@
+/* Copyright (c) 2007-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "ioloop.h"
+#include "istream.h"
+#include "str.h"
+#include "index-mail.h"
+#include "dbox-mail.h"
+#include "sdbox-storage.h"
+#include "sdbox-file.h"
+
+#include <sys/stat.h>
+
+static void sdbox_mail_set_expunged(struct dbox_mail *mail)
+{
+ struct mail *_mail = &mail->imail.mail.mail;
+
+ mail_index_refresh(_mail->box->index);
+ if (mail_index_is_expunged(_mail->transaction->view, _mail->seq)) {
+ mail_set_expunged(_mail);
+ return;
+ }
+
+ mail_set_critical(_mail, "dbox: Unexpectedly lost uid");
+ sdbox_set_mailbox_corrupted(_mail->box);
+}
+
+static int sdbox_mail_file_set(struct dbox_mail *mail)
+{
+ struct mail *_mail = &mail->imail.mail.mail;
+ struct sdbox_mailbox *mbox = SDBOX_MAILBOX(_mail->box);
+ bool deleted;
+ int ret;
+
+ if (mail->open_file != NULL) {
+ /* already set */
+ return 0;
+ } else if (!_mail->saving) {
+ mail->open_file = sdbox_file_init(mbox, _mail->uid);
+ return 0;
+ } else {
+ /* mail is being saved in this transaction */
+ mail->open_file =
+ sdbox_save_file_get_file(_mail->transaction,
+ _mail->seq);
+ mail->open_file->refcount++;
+
+ /* it doesn't have input stream yet */
+ ret = dbox_file_open(mail->open_file, &deleted);
+ if (ret <= 0) {
+ mail_set_critical(_mail,
+ "dbox: Unexpectedly lost mail being saved");
+ sdbox_set_mailbox_corrupted(_mail->box);
+ return -1;
+ }
+ return 1;
+ }
+}
+
+static int
+sdbox_mail_get_special(struct mail *_mail, enum mail_fetch_field field,
+ const char **value_r)
+{
+ struct sdbox_mailbox *mbox = SDBOX_MAILBOX(_mail->box);
+ struct dbox_mail *mail = DBOX_MAIL(_mail);
+ struct stat st;
+
+ switch (field) {
+ case MAIL_FETCH_REFCOUNT:
+ if (sdbox_mail_file_set(mail) < 0)
+ return -1;
+
+ _mail->transaction->stats.fstat_lookup_count++;
+ if (dbox_file_stat(mail->open_file, &st) < 0) {
+ if (errno == ENOENT)
+ mail_set_expunged(_mail);
+ return -1;
+ }
+ *value_r = p_strdup_printf(mail->imail.mail.data_pool, "%lu",
+ (unsigned long)st.st_nlink);
+ return 0;
+ case MAIL_FETCH_REFCOUNT_ID:
+ if (sdbox_mail_file_set(mail) < 0)
+ return -1;
+
+ _mail->transaction->stats.fstat_lookup_count++;
+ if (dbox_file_stat(mail->open_file, &st) < 0) {
+ if (errno == ENOENT)
+ mail_set_expunged(_mail);
+ return -1;
+ }
+ *value_r = p_strdup_printf(mail->imail.mail.data_pool, "%llu",
+ (unsigned long long)st.st_ino);
+ return 0;
+ case MAIL_FETCH_UIDL_BACKEND:
+ if (!dbox_header_have_flag(&mbox->box, mbox->hdr_ext_id,
+ offsetof(struct sdbox_index_header, flags),
+ DBOX_INDEX_HEADER_FLAG_HAVE_POP3_UIDLS)) {
+ *value_r = "";
+ return 0;
+ }
+ break;
+ case MAIL_FETCH_POP3_ORDER:
+ if (!dbox_header_have_flag(&mbox->box, mbox->hdr_ext_id,
+ offsetof(struct sdbox_index_header, flags),
+ DBOX_INDEX_HEADER_FLAG_HAVE_POP3_ORDERS)) {
+ *value_r = "";
+ return 0;
+ }
+ break;
+ default:
+ break;
+ }
+ return dbox_mail_get_special(_mail, field, value_r);
+}
+
+int sdbox_mail_open(struct dbox_mail *mail, uoff_t *offset_r,
+ struct dbox_file **file_r)
+{
+ struct mail *_mail = &mail->imail.mail.mail;
+ bool deleted;
+ int ret;
+
+ if (!mail_stream_access_start(_mail))
+ return -1;
+
+ ret = sdbox_mail_file_set(mail);
+ if (ret < 0)
+ return -1;
+ if (ret == 0) {
+ if (!dbox_file_is_open(mail->open_file))
+ _mail->transaction->stats.open_lookup_count++;
+ if (dbox_file_open(mail->open_file, &deleted) <= 0)
+ return -1;
+ if (deleted) {
+ sdbox_mail_set_expunged(mail);
+ return -1;
+ }
+ }
+
+ *file_r = mail->open_file;
+ *offset_r = 0;
+ return 0;
+}
+
+struct mail_vfuncs sdbox_mail_vfuncs = {
+ dbox_mail_close,
+ index_mail_free,
+ index_mail_set_seq,
+ index_mail_set_uid,
+ index_mail_set_uid_cache_updates,
+ index_mail_prefetch,
+ index_mail_precache,
+ index_mail_add_temp_wanted_fields,
+
+ index_mail_get_flags,
+ index_mail_get_keywords,
+ index_mail_get_keyword_indexes,
+ index_mail_get_modseq,
+ index_mail_get_pvt_modseq,
+ index_mail_get_parts,
+ index_mail_get_date,
+ dbox_mail_get_received_date,
+ dbox_mail_get_save_date,
+ dbox_mail_get_virtual_size,
+ dbox_mail_get_physical_size,
+ index_mail_get_first_header,
+ index_mail_get_headers,
+ index_mail_get_header_stream,
+ dbox_mail_get_stream,
+ index_mail_get_binary_stream,
+ sdbox_mail_get_special,
+ index_mail_get_backend_mail,
+ index_mail_update_flags,
+ index_mail_update_keywords,
+ index_mail_update_modseq,
+ index_mail_update_pvt_modseq,
+ NULL,
+ index_mail_expunge,
+ index_mail_set_cache_corrupted,
+ index_mail_opened,
+};
diff --git a/src/lib-storage/index/dbox-single/sdbox-save.c b/src/lib-storage/index/dbox-single/sdbox-save.c
new file mode 100644
index 0000000..03e24ab
--- /dev/null
+++ b/src/lib-storage/index/dbox-single/sdbox-save.c
@@ -0,0 +1,359 @@
+/* Copyright (c) 2007-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "fdatasync-path.h"
+#include "hex-binary.h"
+#include "hex-dec.h"
+#include "str.h"
+#include "istream.h"
+#include "istream-crlf.h"
+#include "ostream.h"
+#include "write-full.h"
+#include "index-mail.h"
+#include "mail-copy.h"
+#include "index-pop3-uidl.h"
+#include "dbox-attachment.h"
+#include "dbox-save.h"
+#include "sdbox-storage.h"
+#include "sdbox-file.h"
+#include "sdbox-sync.h"
+
+
+struct sdbox_save_context {
+ struct dbox_save_context ctx;
+
+ struct sdbox_mailbox *mbox;
+ struct sdbox_sync_context *sync_ctx;
+
+ struct dbox_file *cur_file;
+ struct dbox_file_append_context *append_ctx;
+
+ uint32_t first_saved_seq;
+ ARRAY(struct dbox_file *) files;
+};
+
+#define SDBOX_SAVECTX(s) container_of(DBOX_SAVECTX(s), struct sdbox_save_context, ctx)
+
+struct dbox_file *
+sdbox_save_file_get_file(struct mailbox_transaction_context *t, uint32_t seq)
+{
+ struct sdbox_save_context *ctx = SDBOX_SAVECTX(t->save_ctx);
+ struct dbox_file *const *files, *file;
+ unsigned int count;
+
+ i_assert(seq >= ctx->first_saved_seq);
+
+ files = array_get(&ctx->files, &count);
+ i_assert(count > 0);
+ i_assert(seq - ctx->first_saved_seq < count);
+
+ file = files[seq - ctx->first_saved_seq];
+ i_assert(((struct sdbox_file *)file)->written_to_disk);
+ return file;
+}
+
+struct mail_save_context *
+sdbox_save_alloc(struct mailbox_transaction_context *t)
+{
+ struct sdbox_mailbox *mbox = SDBOX_MAILBOX(t->box);
+ struct sdbox_save_context *ctx;
+
+ i_assert((t->flags & MAILBOX_TRANSACTION_FLAG_EXTERNAL) != 0);
+
+ if (t->save_ctx != NULL) {
+ /* use the existing allocated structure */
+ ctx = SDBOX_SAVECTX(t->save_ctx);
+ ctx->cur_file = NULL;
+ ctx->ctx.failed = FALSE;
+ ctx->ctx.finished = FALSE;
+ ctx->ctx.dbox_output = NULL;
+ return &ctx->ctx.ctx;
+ }
+
+ ctx = i_new(struct sdbox_save_context, 1);
+ ctx->ctx.ctx.transaction = t;
+ ctx->ctx.trans = t->itrans;
+ ctx->mbox = mbox;
+ i_array_init(&ctx->files, 32);
+ t->save_ctx = &ctx->ctx.ctx;
+ return t->save_ctx;
+}
+
+void sdbox_save_add_file(struct mail_save_context *_ctx, struct dbox_file *file)
+{
+ struct sdbox_save_context *ctx = SDBOX_SAVECTX(_ctx);
+ struct dbox_file *const *files;
+ unsigned int count;
+
+ if (ctx->first_saved_seq == 0)
+ ctx->first_saved_seq = ctx->ctx.seq;
+
+ files = array_get(&ctx->files, &count);
+ if (count > 0) {
+ /* a plugin may leave a previously saved file open.
+ we'll close it here to avoid eating too many fds. */
+ dbox_file_close(files[count-1]);
+ }
+ array_push_back(&ctx->files, &file);
+}
+
+int sdbox_save_begin(struct mail_save_context *_ctx, struct istream *input)
+{
+ struct sdbox_save_context *ctx = SDBOX_SAVECTX(_ctx);
+ struct dbox_file *file;
+ int ret;
+
+ file = sdbox_file_create(ctx->mbox);
+ ctx->append_ctx = dbox_file_append_init(file);
+ ret = dbox_file_get_append_stream(ctx->append_ctx,
+ &ctx->ctx.dbox_output);
+ if (ret <= 0) {
+ i_assert(ret != 0);
+ dbox_file_append_rollback(&ctx->append_ctx);
+ dbox_file_unref(&file);
+ ctx->ctx.failed = TRUE;
+ return -1;
+ }
+ ctx->cur_file = file;
+ dbox_save_begin(&ctx->ctx, input);
+
+ sdbox_save_add_file(_ctx, file);
+ return ctx->ctx.failed ? -1 : 0;
+}
+
+static int dbox_save_mail_write_metadata(struct dbox_save_context *ctx,
+ struct dbox_file *file)
+{
+ struct sdbox_file *sfile = (struct sdbox_file *)file;
+ const ARRAY_TYPE(mail_attachment_extref) *extrefs_arr;
+ const struct mail_attachment_extref *extrefs;
+ struct dbox_message_header dbox_msg_hdr;
+ uoff_t message_size;
+ guid_128_t guid_128;
+ unsigned int i, count;
+
+ i_assert(file->msg_header_size == sizeof(dbox_msg_hdr));
+
+ message_size = ctx->dbox_output->offset -
+ file->msg_header_size - file->file_header_size;
+
+ dbox_save_write_metadata(&ctx->ctx, ctx->dbox_output,
+ message_size, NULL, guid_128);
+ dbox_msg_header_fill(&dbox_msg_hdr, message_size);
+ if (o_stream_pwrite(ctx->dbox_output, &dbox_msg_hdr,
+ sizeof(dbox_msg_hdr),
+ file->file_header_size) < 0) {
+ dbox_file_set_syscall_error(file, "pwrite()");
+ return -1;
+ }
+ sfile->written_to_disk = TRUE;
+
+ /* remember the attachment paths until commit time */
+ extrefs_arr = index_attachment_save_get_extrefs(&ctx->ctx);
+ if (extrefs_arr != NULL)
+ extrefs = array_get(extrefs_arr, &count);
+ else {
+ extrefs = NULL;
+ count = 0;
+ }
+ if (count > 0) {
+ sfile->attachment_pool =
+ pool_alloconly_create("sdbox attachment paths", 512);
+ p_array_init(&sfile->attachment_paths,
+ sfile->attachment_pool, count);
+ for (i = 0; i < count; i++) {
+ const char *path = p_strdup(sfile->attachment_pool,
+ extrefs[i].path);
+ array_push_back(&sfile->attachment_paths, &path);
+ }
+ }
+ return 0;
+}
+
+static int dbox_save_finish_write(struct mail_save_context *_ctx)
+{
+ struct sdbox_save_context *ctx = (struct sdbox_save_context *)_ctx;
+ struct dbox_file **files;
+
+ ctx->ctx.finished = TRUE;
+ if (ctx->ctx.dbox_output == NULL)
+ return -1;
+
+ if (_ctx->data.save_date != (time_t)-1) {
+ /* we can't change ctime, but we can add the date to cache */
+ struct index_mail *mail = (struct index_mail *)_ctx->dest_mail;
+ uint32_t t = _ctx->data.save_date;
+
+ index_mail_cache_add(mail, MAIL_CACHE_SAVE_DATE, &t, sizeof(t));
+ }
+ dbox_save_end(&ctx->ctx);
+
+ files = array_back_modifiable(&ctx->files);
+ if (!ctx->ctx.failed) T_BEGIN {
+ if (dbox_save_mail_write_metadata(&ctx->ctx, *files) < 0)
+ ctx->ctx.failed = TRUE;
+ } T_END;
+
+ if (ctx->ctx.failed) {
+ index_storage_save_abort_last(&ctx->ctx.ctx, ctx->ctx.seq);
+ dbox_file_append_rollback(&ctx->append_ctx);
+ dbox_file_unlink(*files);
+ dbox_file_unref(files);
+ array_pop_back(&ctx->files);
+ } else {
+ dbox_file_append_checkpoint(ctx->append_ctx);
+ if (dbox_file_append_commit(&ctx->append_ctx) < 0)
+ ctx->ctx.failed = TRUE;
+ dbox_file_close(*files);
+ }
+
+ i_stream_unref(&ctx->ctx.input);
+ ctx->ctx.dbox_output = NULL;
+
+ return ctx->ctx.failed ? -1 : 0;
+}
+
+int sdbox_save_finish(struct mail_save_context *ctx)
+{
+ int ret;
+
+ ret = dbox_save_finish_write(ctx);
+ index_save_context_free(ctx);
+ return ret;
+}
+
+void sdbox_save_cancel(struct mail_save_context *_ctx)
+{
+ struct dbox_save_context *ctx = DBOX_SAVECTX(_ctx);
+
+ ctx->failed = TRUE;
+ (void)sdbox_save_finish(_ctx);
+}
+
+static int dbox_save_assign_uids(struct sdbox_save_context *ctx,
+ const ARRAY_TYPE(seq_range) *uids)
+{
+ struct dbox_file *const *files;
+ struct seq_range_iter iter;
+ unsigned int i, count, n = 0;
+ uint32_t uid;
+ bool ret;
+
+ seq_range_array_iter_init(&iter, uids);
+ files = array_get(&ctx->files, &count);
+ for (i = 0; i < count; i++) {
+ struct sdbox_file *sfile = (struct sdbox_file *)files[i];
+
+ ret = seq_range_array_iter_nth(&iter, n++, &uid);
+ i_assert(ret);
+ if (sdbox_file_assign_uid(sfile, uid) < 0)
+ return -1;
+ if (ctx->ctx.highest_pop3_uidl_seq == i+1) {
+ index_pop3_uidl_set_max_uid(&ctx->mbox->box,
+ ctx->ctx.trans, uid);
+ }
+ }
+ i_assert(!seq_range_array_iter_nth(&iter, n, &uid));
+ return 0;
+}
+
+static void dbox_save_unref_files(struct sdbox_save_context *ctx)
+{
+ struct dbox_file **files;
+ unsigned int i, count;
+
+ files = array_get_modifiable(&ctx->files, &count);
+ for (i = 0; i < count; i++) {
+ if (ctx->ctx.failed) {
+ struct sdbox_file *sfile =
+ (struct sdbox_file *)files[i];
+
+ (void)sdbox_file_unlink_aborted_save(sfile);
+ }
+ dbox_file_unref(&files[i]);
+ }
+ array_free(&ctx->files);
+}
+
+int sdbox_transaction_save_commit_pre(struct mail_save_context *_ctx)
+{
+ struct sdbox_save_context *ctx = SDBOX_SAVECTX(_ctx);
+ struct mailbox_transaction_context *_t = _ctx->transaction;
+ const struct mail_index_header *hdr;
+
+ i_assert(ctx->ctx.finished);
+
+ if (array_count(&ctx->files) == 0) {
+ /* the mail must be freed in the commit_pre() */
+ return 0;
+ }
+
+ if (sdbox_sync_begin(ctx->mbox, SDBOX_SYNC_FLAG_FORCE |
+ SDBOX_SYNC_FLAG_FSYNC, &ctx->sync_ctx) < 0) {
+ sdbox_transaction_save_rollback(_ctx);
+ return -1;
+ }
+
+ /* update dbox header flags */
+ dbox_save_update_header_flags(&ctx->ctx, ctx->sync_ctx->sync_view,
+ ctx->mbox->hdr_ext_id, offsetof(struct sdbox_index_header, flags));
+
+ /* assign UIDs for new messages */
+ hdr = mail_index_get_header(ctx->sync_ctx->sync_view);
+ mail_index_append_finish_uids(ctx->ctx.trans, hdr->next_uid,
+ &_t->changes->saved_uids);
+ if (dbox_save_assign_uids(ctx, &_t->changes->saved_uids) < 0) {
+ sdbox_transaction_save_rollback(_ctx);
+ return -1;
+ }
+
+ _t->changes->uid_validity = hdr->uid_validity;
+ return 0;
+}
+
+void sdbox_transaction_save_commit_post(struct mail_save_context *_ctx,
+ struct mail_index_transaction_commit_result *result)
+{
+ struct sdbox_save_context *ctx = SDBOX_SAVECTX(_ctx);
+ struct mail_storage *storage = _ctx->transaction->box->storage;
+
+ _ctx->transaction = NULL; /* transaction is already freed */
+
+ if (array_count(&ctx->files) == 0) {
+ sdbox_transaction_save_rollback(_ctx);
+ return;
+ }
+
+ mail_index_sync_set_commit_result(ctx->sync_ctx->index_sync_ctx,
+ result);
+
+ if (sdbox_sync_finish(&ctx->sync_ctx, TRUE) < 0)
+ ctx->ctx.failed = TRUE;
+
+ if (storage->set->parsed_fsync_mode != FSYNC_MODE_NEVER) {
+ const char *box_path = mailbox_get_path(&ctx->mbox->box);
+
+ if (fdatasync_path(box_path) < 0) {
+ mail_set_critical(_ctx->dest_mail,
+ "fdatasync_path(%s) failed: %m", box_path);
+ }
+ }
+ i_assert(ctx->ctx.finished);
+ dbox_save_unref_files(ctx);
+ i_free(ctx);
+}
+
+void sdbox_transaction_save_rollback(struct mail_save_context *_ctx)
+{
+ struct sdbox_save_context *ctx = SDBOX_SAVECTX(_ctx);
+
+ ctx->ctx.failed = TRUE;
+ if (!ctx->ctx.finished)
+ sdbox_save_cancel(_ctx);
+ dbox_save_unref_files(ctx);
+
+ if (ctx->sync_ctx != NULL)
+ (void)sdbox_sync_finish(&ctx->sync_ctx, FALSE);
+ i_free(ctx);
+}
diff --git a/src/lib-storage/index/dbox-single/sdbox-storage.c b/src/lib-storage/index/dbox-single/sdbox-storage.c
new file mode 100644
index 0000000..5a6fcad
--- /dev/null
+++ b/src/lib-storage/index/dbox-single/sdbox-storage.c
@@ -0,0 +1,534 @@
+/* Copyright (c) 2007-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "fs-api.h"
+#include "master-service.h"
+#include "mail-index-modseq.h"
+#include "mail-search-build.h"
+#include "mailbox-list-private.h"
+#include "index-pop3-uidl.h"
+#include "dbox-mail.h"
+#include "dbox-save.h"
+#include "sdbox-file.h"
+#include "sdbox-sync.h"
+#include "sdbox-storage.h"
+
+extern struct mail_storage dbox_storage, sdbox_storage;
+extern struct mailbox sdbox_mailbox;
+extern struct dbox_storage_vfuncs sdbox_dbox_storage_vfuncs;
+
+static struct event_category event_category_sdbox = {
+ .name = "sdbox",
+ .parent = &event_category_storage,
+};
+
+static struct mail_storage *sdbox_storage_alloc(void)
+{
+ struct sdbox_storage *storage;
+ pool_t pool;
+
+ pool = pool_alloconly_create("sdbox storage", 512+256);
+ storage = p_new(pool, struct sdbox_storage, 1);
+ storage->storage.v = sdbox_dbox_storage_vfuncs;
+ storage->storage.storage = sdbox_storage;
+ storage->storage.storage.pool = pool;
+ return &storage->storage.storage;
+}
+
+static int sdbox_storage_create(struct mail_storage *_storage,
+ struct mail_namespace *ns,
+ const char **error_r)
+{
+ struct dbox_storage *storage = DBOX_STORAGE(_storage);
+ enum fs_properties props;
+
+ if (dbox_storage_create(_storage, ns, error_r) < 0)
+ return -1;
+
+ if (storage->attachment_fs != NULL) {
+ props = fs_get_properties(storage->attachment_fs);
+ if ((props & FS_PROPERTY_RENAME) == 0) {
+ *error_r = "mail_attachment_fs: "
+ "Backend doesn't support renaming";
+ return -1;
+ }
+ }
+ return 0;
+}
+
+static const char *
+sdbox_storage_find_root_dir(const struct mail_namespace *ns)
+{
+ bool debug = ns->mail_set->mail_debug;
+ const char *home, *path;
+
+ if (ns->owner != NULL &&
+ mail_user_get_home(ns->owner, &home) > 0) {
+ path = t_strconcat(home, "/sdbox", NULL);
+ if (access(path, R_OK|W_OK|X_OK) == 0) {
+ if (debug)
+ i_debug("sdbox: root exists (%s)", path);
+ return path;
+ }
+ if (debug)
+ i_debug("sdbox: access(%s, rwx): failed: %m", path);
+ }
+ return NULL;
+}
+
+static bool sdbox_storage_autodetect(const struct mail_namespace *ns,
+ struct mailbox_list_settings *set)
+{
+ bool debug = ns->mail_set->mail_debug;
+ struct stat st;
+ const char *path, *root_dir;
+
+ if (set->root_dir != NULL)
+ root_dir = set->root_dir;
+ else {
+ root_dir = sdbox_storage_find_root_dir(ns);
+ if (root_dir == NULL) {
+ if (debug)
+ i_debug("sdbox: couldn't find root dir");
+ return FALSE;
+ }
+ }
+
+ /* NOTE: this check works for mdbox as well. we'll rely on the
+ autodetect ordering to catch mdbox before we get here. */
+ path = t_strconcat(root_dir, "/"DBOX_MAILBOX_DIR_NAME, NULL);
+ if (stat(path, &st) < 0) {
+ if (debug)
+ i_debug("sdbox autodetect: stat(%s) failed: %m", path);
+ return FALSE;
+ }
+
+ if (!S_ISDIR(st.st_mode)) {
+ if (debug)
+ i_debug("sdbox autodetect: %s not a directory", path);
+ return FALSE;
+ }
+
+ set->root_dir = root_dir;
+ dbox_storage_get_list_settings(ns, set);
+ return TRUE;
+}
+
+static struct mailbox *
+sdbox_mailbox_alloc(struct mail_storage *storage, struct mailbox_list *list,
+ const char *vname, enum mailbox_flags flags)
+{
+ struct sdbox_mailbox *mbox;
+ struct index_mailbox_context *ibox;
+ pool_t pool;
+
+ /* dbox can't work without index files */
+ flags &= ENUM_NEGATE(MAILBOX_FLAG_NO_INDEX_FILES);
+
+ pool = pool_alloconly_create("sdbox mailbox", 1024*3);
+ mbox = p_new(pool, struct sdbox_mailbox, 1);
+ mbox->box = sdbox_mailbox;
+ mbox->box.pool = pool;
+ mbox->box.storage = storage;
+ mbox->box.list = list;
+ mbox->box.mail_vfuncs = &sdbox_mail_vfuncs;
+
+ index_storage_mailbox_alloc(&mbox->box, vname, flags, MAIL_INDEX_PREFIX);
+
+ ibox = INDEX_STORAGE_CONTEXT(&mbox->box);
+ ibox->index_flags |= MAIL_INDEX_OPEN_FLAG_KEEP_BACKUPS |
+ MAIL_INDEX_OPEN_FLAG_NEVER_IN_MEMORY;
+
+ mbox->storage = SDBOX_STORAGE(storage);
+ return &mbox->box;
+}
+
+int sdbox_read_header(struct sdbox_mailbox *mbox,
+ struct sdbox_index_header *hdr, bool log_error,
+ bool *need_resize_r)
+{
+ struct mail_index_view *view;
+ const void *data;
+ size_t data_size;
+ int ret = 0;
+
+ i_assert(mbox->box.opened);
+
+ view = mail_index_view_open(mbox->box.index);
+ mail_index_get_header_ext(view, mbox->hdr_ext_id,
+ &data, &data_size);
+ if (data_size < SDBOX_INDEX_HEADER_MIN_SIZE &&
+ (!mbox->box.creating || data_size != 0)) {
+ if (log_error) {
+ mailbox_set_critical(&mbox->box,
+ "sdbox: Invalid dbox header size");
+ }
+ ret = -1;
+ } else {
+ i_zero(hdr);
+ memcpy(hdr, data, I_MIN(data_size, sizeof(*hdr)));
+ if (guid_128_is_empty(hdr->mailbox_guid))
+ ret = -1;
+ else {
+ /* data is valid. remember it in case mailbox
+ is being reset */
+ mail_index_set_ext_init_data(mbox->box.index,
+ mbox->hdr_ext_id,
+ hdr, sizeof(*hdr));
+ }
+ }
+ mail_index_view_close(&view);
+ *need_resize_r = data_size < sizeof(*hdr);
+ return ret;
+}
+
+static void sdbox_update_header(struct sdbox_mailbox *mbox,
+ struct mail_index_transaction *trans,
+ const struct mailbox_update *update)
+{
+ struct sdbox_index_header hdr, new_hdr;
+ bool need_resize;
+
+ if (sdbox_read_header(mbox, &hdr, TRUE, &need_resize) < 0) {
+ i_zero(&hdr);
+ need_resize = TRUE;
+ }
+
+ new_hdr = hdr;
+
+ if (update != NULL && !guid_128_is_empty(update->mailbox_guid)) {
+ memcpy(new_hdr.mailbox_guid, update->mailbox_guid,
+ sizeof(new_hdr.mailbox_guid));
+ } else if (guid_128_is_empty(new_hdr.mailbox_guid)) {
+ guid_128_generate(new_hdr.mailbox_guid);
+ }
+
+ if (need_resize) {
+ mail_index_ext_resize_hdr(trans, mbox->hdr_ext_id,
+ sizeof(new_hdr));
+ }
+ if (memcmp(&hdr, &new_hdr, sizeof(hdr)) != 0) {
+ mail_index_update_header_ext(trans, mbox->hdr_ext_id, 0,
+ &new_hdr, sizeof(new_hdr));
+ }
+ memcpy(mbox->mailbox_guid, new_hdr.mailbox_guid,
+ sizeof(mbox->mailbox_guid));
+}
+
+int sdbox_mailbox_create_indexes(struct mailbox *box,
+ const struct mailbox_update *update,
+ struct mail_index_transaction *trans)
+{
+ struct sdbox_mailbox *mbox = SDBOX_MAILBOX(box);
+ struct mail_index_transaction *new_trans = NULL;
+ const struct mail_index_header *hdr;
+ uint32_t uid_validity, uid_next;
+
+ if (trans == NULL) {
+ new_trans = mail_index_transaction_begin(box->view, 0);
+ trans = new_trans;
+ }
+
+ hdr = mail_index_get_header(box->view);
+ if (update != NULL && update->uid_validity != 0)
+ uid_validity = update->uid_validity;
+ else if (hdr->uid_validity != 0)
+ uid_validity = hdr->uid_validity;
+ else {
+ /* set uidvalidity */
+ uid_validity = dbox_get_uidvalidity_next(box->list);
+ }
+
+ if (hdr->uid_validity != uid_validity) {
+ mail_index_update_header(trans,
+ offsetof(struct mail_index_header, uid_validity),
+ &uid_validity, sizeof(uid_validity), TRUE);
+ }
+ if (update != NULL && hdr->next_uid < update->min_next_uid) {
+ uid_next = update->min_next_uid;
+ mail_index_update_header(trans,
+ offsetof(struct mail_index_header, next_uid),
+ &uid_next, sizeof(uid_next), TRUE);
+ }
+ if (update != NULL && update->min_first_recent_uid != 0 &&
+ hdr->first_recent_uid < update->min_first_recent_uid) {
+ uint32_t first_recent_uid = update->min_first_recent_uid;
+
+ mail_index_update_header(trans,
+ offsetof(struct mail_index_header, first_recent_uid),
+ &first_recent_uid, sizeof(first_recent_uid), FALSE);
+ }
+ if (update != NULL && update->min_highest_modseq != 0 &&
+ mail_index_modseq_get_highest(box->view) <
+ update->min_highest_modseq) {
+ mail_index_modseq_enable(box->index);
+ mail_index_update_highest_modseq(trans,
+ update->min_highest_modseq);
+ }
+
+ if (box->inbox_user && box->creating) {
+ /* initialize pop3-uidl header when creating mailbox
+ (not on mailbox_update()) */
+ index_pop3_uidl_set_max_uid(box, trans, 0);
+ }
+
+ sdbox_update_header(mbox, trans, update);
+ if (new_trans != NULL) {
+ if (mail_index_transaction_commit(&new_trans) < 0) {
+ mailbox_set_index_error(box);
+ return -1;
+ }
+ }
+ return 0;
+}
+
+static const char *
+sdbox_get_attachment_path_suffix(struct dbox_file *_file)
+{
+ struct sdbox_file *file = (struct sdbox_file *)_file;
+
+ return t_strdup_printf("-%s-%u",
+ guid_128_to_string(file->mbox->mailbox_guid),
+ file->uid);
+}
+
+void sdbox_set_mailbox_corrupted(struct mailbox *box)
+{
+ struct sdbox_mailbox *mbox = SDBOX_MAILBOX(box);
+ struct sdbox_index_header hdr;
+ bool need_resize;
+
+ if (sdbox_read_header(mbox, &hdr, TRUE, &need_resize) < 0 ||
+ hdr.rebuild_count == 0)
+ mbox->corrupted_rebuild_count = 1;
+ else
+ mbox->corrupted_rebuild_count = hdr.rebuild_count;
+}
+
+static void sdbox_set_file_corrupted(struct dbox_file *_file)
+{
+ struct sdbox_file *file = (struct sdbox_file *)_file;
+
+ sdbox_set_mailbox_corrupted(&file->mbox->box);
+}
+
+static int sdbox_mailbox_alloc_index(struct sdbox_mailbox *mbox)
+{
+ struct sdbox_index_header hdr;
+
+ if (index_storage_mailbox_alloc_index(&mbox->box) < 0)
+ return -1;
+
+ mbox->hdr_ext_id =
+ mail_index_ext_register(mbox->box.index, "dbox-hdr",
+ sizeof(struct sdbox_index_header), 0, 0);
+ /* set the initialization data in case the mailbox is created */
+ i_zero(&hdr);
+ guid_128_generate(hdr.mailbox_guid);
+ mail_index_set_ext_init_data(mbox->box.index, mbox->hdr_ext_id,
+ &hdr, sizeof(hdr));
+ return 0;
+}
+
+static int sdbox_mailbox_open(struct mailbox *box)
+{
+ struct sdbox_mailbox *mbox = SDBOX_MAILBOX(box);
+ struct sdbox_index_header hdr;
+ bool need_resize;
+ time_t path_ctime;
+
+ if (dbox_mailbox_check_existence(box, &path_ctime) < 0)
+ return -1;
+
+ if (sdbox_mailbox_alloc_index(mbox) < 0)
+ return -1;
+
+ if (dbox_mailbox_open(box, path_ctime) < 0)
+ return -1;
+
+ if (box->creating) {
+ /* wait for mailbox creation to initialize the index */
+ return 0;
+ }
+
+ /* get/generate mailbox guid */
+ if (sdbox_read_header(mbox, &hdr, FALSE, &need_resize) < 0) {
+ /* looks like the mailbox is corrupted */
+ (void)sdbox_sync(mbox, SDBOX_SYNC_FLAG_FORCE);
+ if (sdbox_read_header(mbox, &hdr, TRUE, &need_resize) < 0)
+ i_zero(&hdr);
+ }
+
+ if (guid_128_is_empty(hdr.mailbox_guid)) {
+ /* regenerate it */
+ if (sdbox_mailbox_create_indexes(box, NULL, NULL) < 0 ||
+ sdbox_read_header(mbox, &hdr, TRUE, &need_resize) < 0)
+ return -1;
+ }
+ memcpy(mbox->mailbox_guid, hdr.mailbox_guid,
+ sizeof(mbox->mailbox_guid));
+ return 0;
+}
+
+static void sdbox_mailbox_close(struct mailbox *box)
+{
+ struct sdbox_mailbox *mbox = SDBOX_MAILBOX(box);
+
+ if (mbox->corrupted_rebuild_count != 0)
+ (void)sdbox_sync(mbox, 0);
+ index_storage_mailbox_close(box);
+}
+
+static int
+sdbox_mailbox_create(struct mailbox *box,
+ const struct mailbox_update *update, bool directory)
+{
+ struct sdbox_mailbox *mbox = SDBOX_MAILBOX(box);
+ struct sdbox_index_header hdr;
+ bool need_resize;
+
+ if (dbox_mailbox_create(box, update, directory) < 0)
+ return -1;
+ if (directory || !guid_128_is_empty(mbox->mailbox_guid))
+ return 0;
+
+ /* another process just created the mailbox. read the mailbox_guid. */
+ if (sdbox_read_header(mbox, &hdr, FALSE, &need_resize) < 0) {
+ mailbox_set_critical(box,
+ "sdbox: Failed to read newly created dbox header");
+ return -1;
+ }
+ memcpy(mbox->mailbox_guid, hdr.mailbox_guid,
+ sizeof(mbox->mailbox_guid));
+ i_assert(!guid_128_is_empty(mbox->mailbox_guid));
+ return 0;
+}
+
+static int
+sdbox_mailbox_get_metadata(struct mailbox *box,
+ enum mailbox_metadata_items items,
+ struct mailbox_metadata *metadata_r)
+{
+ struct sdbox_mailbox *mbox = SDBOX_MAILBOX(box);
+
+ if (index_mailbox_get_metadata(box, items, metadata_r) < 0)
+ return -1;
+ if ((items & MAILBOX_METADATA_GUID) != 0) {
+ memcpy(metadata_r->guid, mbox->mailbox_guid,
+ sizeof(metadata_r->guid));
+ }
+ return 0;
+}
+
+static int
+dbox_mailbox_update(struct mailbox *box, const struct mailbox_update *update)
+{
+ if (!box->opened) {
+ if (mailbox_open(box) < 0)
+ return -1;
+ }
+ if (sdbox_mailbox_create_indexes(box, update, NULL) < 0)
+ return -1;
+ return index_storage_mailbox_update_common(box, update);
+}
+
+struct mail_storage sdbox_storage = {
+ .name = SDBOX_STORAGE_NAME,
+ .class_flags = MAIL_STORAGE_CLASS_FLAG_FILE_PER_MSG |
+ MAIL_STORAGE_CLASS_FLAG_HAVE_MAIL_GUIDS |
+ MAIL_STORAGE_CLASS_FLAG_HAVE_MAIL_SAVE_GUIDS |
+ MAIL_STORAGE_CLASS_FLAG_BINARY_DATA,
+ .event_category = &event_category_sdbox,
+
+ .v = {
+ NULL,
+ sdbox_storage_alloc,
+ sdbox_storage_create,
+ dbox_storage_destroy,
+ NULL,
+ dbox_storage_get_list_settings,
+ sdbox_storage_autodetect,
+ sdbox_mailbox_alloc,
+ NULL,
+ mail_storage_list_index_rebuild,
+ }
+};
+
+struct mail_storage dbox_storage = {
+ .name = "dbox", /* alias */
+ .class_flags = MAIL_STORAGE_CLASS_FLAG_FILE_PER_MSG,
+ .event_category = &event_category_sdbox,
+
+ .v = {
+ NULL,
+ sdbox_storage_alloc,
+ sdbox_storage_create,
+ dbox_storage_destroy,
+ NULL,
+ dbox_storage_get_list_settings,
+ sdbox_storage_autodetect,
+ sdbox_mailbox_alloc,
+ NULL,
+ mail_storage_list_index_rebuild,
+ }
+};
+
+struct mailbox sdbox_mailbox = {
+ .v = {
+ index_storage_is_readonly,
+ index_storage_mailbox_enable,
+ index_storage_mailbox_exists,
+ sdbox_mailbox_open,
+ sdbox_mailbox_close,
+ index_storage_mailbox_free,
+ sdbox_mailbox_create,
+ dbox_mailbox_update,
+ index_storage_mailbox_delete,
+ index_storage_mailbox_rename,
+ index_storage_get_status,
+ sdbox_mailbox_get_metadata,
+ index_storage_set_subscribed,
+ index_storage_attribute_set,
+ index_storage_attribute_get,
+ index_storage_attribute_iter_init,
+ index_storage_attribute_iter_next,
+ index_storage_attribute_iter_deinit,
+ index_storage_list_index_has_changed,
+ index_storage_list_index_update_sync,
+ sdbox_storage_sync_init,
+ index_mailbox_sync_next,
+ index_mailbox_sync_deinit,
+ NULL,
+ dbox_notify_changes,
+ index_transaction_begin,
+ index_transaction_commit,
+ index_transaction_rollback,
+ NULL,
+ dbox_mail_alloc,
+ index_storage_search_init,
+ index_storage_search_deinit,
+ index_storage_search_next_nonblock,
+ index_storage_search_next_update_seq,
+ index_storage_search_next_match_mail,
+ sdbox_save_alloc,
+ sdbox_save_begin,
+ dbox_save_continue,
+ sdbox_save_finish,
+ sdbox_save_cancel,
+ sdbox_copy,
+ sdbox_transaction_save_commit_pre,
+ sdbox_transaction_save_commit_post,
+ sdbox_transaction_save_rollback,
+ index_storage_is_inconsistent
+ }
+};
+
+struct dbox_storage_vfuncs sdbox_dbox_storage_vfuncs = {
+ sdbox_file_free,
+ sdbox_file_create_fd,
+ sdbox_mail_open,
+ sdbox_mailbox_create_indexes,
+ sdbox_get_attachment_path_suffix,
+ sdbox_set_mailbox_corrupted,
+ sdbox_set_file_corrupted
+};
diff --git a/src/lib-storage/index/dbox-single/sdbox-storage.h b/src/lib-storage/index/dbox-single/sdbox-storage.h
new file mode 100644
index 0000000..66d08da
--- /dev/null
+++ b/src/lib-storage/index/dbox-single/sdbox-storage.h
@@ -0,0 +1,69 @@
+#ifndef SDBOX_STORAGE_H
+#define SDBOX_STORAGE_H
+
+#include "index-storage.h"
+#include "dbox-storage.h"
+
+#define SDBOX_STORAGE_NAME "sdbox"
+#define SDBOX_MAIL_FILE_PREFIX "u."
+#define SDBOX_MAIL_FILE_FORMAT SDBOX_MAIL_FILE_PREFIX"%u"
+
+#define SDBOX_INDEX_HEADER_MIN_SIZE (sizeof(uint32_t))
+struct sdbox_index_header {
+ /* increased every time a full mailbox rebuild is done */
+ uint32_t rebuild_count;
+ guid_128_t mailbox_guid;
+ uint8_t flags; /* enum dbox_index_header_flags */
+ uint8_t unused[3];
+};
+
+struct sdbox_storage {
+ struct dbox_storage storage;
+};
+
+struct sdbox_mailbox {
+ struct mailbox box;
+ struct sdbox_storage *storage;
+
+ uint32_t hdr_ext_id;
+ /* if non-zero, storage should be rebuilt (except if rebuild_count
+ has changed from this value) */
+ uint32_t corrupted_rebuild_count;
+
+ guid_128_t mailbox_guid;
+};
+
+#define SDBOX_STORAGE(s) container_of(DBOX_STORAGE(s), struct sdbox_storage, storage)
+#define SDBOX_MAILBOX(s) container_of(s, struct sdbox_mailbox, box)
+
+extern struct mail_vfuncs sdbox_mail_vfuncs;
+
+int sdbox_mail_open(struct dbox_mail *mail, uoff_t *offset_r,
+ struct dbox_file **file_r);
+
+int sdbox_read_header(struct sdbox_mailbox *mbox,
+ struct sdbox_index_header *hdr, bool log_error,
+ bool *need_resize_r);
+int sdbox_mailbox_create_indexes(struct mailbox *box,
+ const struct mailbox_update *update,
+ struct mail_index_transaction *trans);
+void sdbox_set_mailbox_corrupted(struct mailbox *box);
+
+struct mail_save_context *
+sdbox_save_alloc(struct mailbox_transaction_context *_t);
+int sdbox_save_begin(struct mail_save_context *ctx, struct istream *input);
+int sdbox_save_finish(struct mail_save_context *ctx);
+void sdbox_save_cancel(struct mail_save_context *ctx);
+
+struct dbox_file *
+sdbox_save_file_get_file(struct mailbox_transaction_context *t, uint32_t seq);
+void sdbox_save_add_file(struct mail_save_context *ctx, struct dbox_file *file);
+
+int sdbox_transaction_save_commit_pre(struct mail_save_context *ctx);
+void sdbox_transaction_save_commit_post(struct mail_save_context *ctx,
+ struct mail_index_transaction_commit_result *result);
+void sdbox_transaction_save_rollback(struct mail_save_context *ctx);
+
+int sdbox_copy(struct mail_save_context *ctx, struct mail *mail);
+
+#endif
diff --git a/src/lib-storage/index/dbox-single/sdbox-sync-rebuild.c b/src/lib-storage/index/dbox-single/sdbox-sync-rebuild.c
new file mode 100644
index 0000000..aa4832f
--- /dev/null
+++ b/src/lib-storage/index/dbox-single/sdbox-sync-rebuild.c
@@ -0,0 +1,218 @@
+/* Copyright (c) 2007-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "index-rebuild.h"
+#include "mail-cache.h"
+#include "sdbox-storage.h"
+#include "sdbox-file.h"
+#include "sdbox-sync.h"
+
+#include <dirent.h>
+
+static void sdbox_sync_set_uidvalidity(struct index_rebuild_context *ctx)
+{
+ uint32_t uid_validity;
+
+ /* if uidvalidity is set in the old index, use it */
+ uid_validity = mail_index_get_header(ctx->view)->uid_validity;
+ if (uid_validity == 0)
+ uid_validity = dbox_get_uidvalidity_next(ctx->box->list);
+
+ mail_index_update_header(ctx->trans,
+ offsetof(struct mail_index_header, uid_validity),
+ &uid_validity, sizeof(uid_validity), TRUE);
+}
+
+static int
+sdbox_sync_add_file_index(struct index_rebuild_context *ctx,
+ struct dbox_file *file, uint32_t uid, bool primary)
+{
+ uint32_t seq;
+ bool deleted;
+ int ret;
+
+ if ((ret = dbox_file_open(file, &deleted)) > 0) {
+ if (deleted)
+ return 0;
+ ret = dbox_file_seek(file, 0);
+ }
+ if (ret == 0) {
+ if ((ret = dbox_file_fix(file, 0)) > 0)
+ ret = dbox_file_seek(file, 0);
+ }
+
+ if (ret <= 0) {
+ if (ret < 0)
+ return -1;
+
+ i_warning("sdbox: Skipping unfixable file: %s", file->cur_path);
+ return 0;
+ }
+
+ if (!dbox_file_is_in_alt(file) && !primary) {
+ /* we were supposed to open the file in alt storage, but it
+ exists in primary storage as well. skip it to avoid adding
+ it twice. */
+ return 0;
+ }
+
+ mail_index_append(ctx->trans, uid, &seq);
+ T_BEGIN {
+ index_rebuild_index_metadata(ctx, seq, uid);
+ } T_END;
+ return 0;
+}
+
+static int
+sdbox_sync_add_file(struct index_rebuild_context *ctx,
+ const char *fname, bool primary)
+{
+ struct sdbox_mailbox *mbox = SDBOX_MAILBOX(ctx->box);
+ struct dbox_file *file;
+ uint32_t uid;
+ int ret;
+
+ if (!str_begins(fname, SDBOX_MAIL_FILE_PREFIX))
+ return 0;
+ fname += strlen(SDBOX_MAIL_FILE_PREFIX);
+
+ if (str_to_uint32(fname, &uid) < 0 || uid == 0) {
+ i_warning("sdbox %s: Ignoring invalid filename %s",
+ mailbox_get_path(ctx->box), fname);
+ return 0;
+ }
+
+ file = sdbox_file_init(mbox, uid);
+ if (!primary)
+ file->cur_path = file->alt_path;
+ ret = sdbox_sync_add_file_index(ctx, file, uid, primary);
+ dbox_file_unref(&file);
+ return ret;
+}
+
+static int sdbox_sync_index_rebuild_dir(struct index_rebuild_context *ctx,
+ const char *path, bool primary)
+{
+ DIR *dir;
+ struct dirent *d;
+ int ret = 0;
+
+ dir = opendir(path);
+ if (dir == NULL) {
+ if (errno == ENOENT) {
+ if (!primary) {
+ /* alt directory doesn't exist, ignore */
+ return 0;
+ }
+ return index_mailbox_fix_inconsistent_existence(ctx->box, path);
+ }
+ mailbox_set_critical(ctx->box, "opendir(%s) failed: %m", path);
+ return -1;
+ }
+ do {
+ errno = 0;
+ if ((d = readdir(dir)) == NULL)
+ break;
+
+ ret = sdbox_sync_add_file(ctx, d->d_name, primary);
+ } while (ret >= 0);
+ if (errno != 0) {
+ mailbox_set_critical(ctx->box, "readdir(%s) failed: %m", path);
+ ret = -1;
+ }
+
+ if (closedir(dir) < 0) {
+ mailbox_set_critical(ctx->box, "closedir(%s) failed: %m", path);
+ ret = -1;
+ }
+ return ret;
+}
+
+static void sdbox_sync_update_header(struct index_rebuild_context *ctx)
+{
+ struct sdbox_mailbox *mbox = SDBOX_MAILBOX(ctx->box);
+ struct sdbox_index_header hdr;
+ bool need_resize;
+
+ if (sdbox_read_header(mbox, &hdr, FALSE, &need_resize) < 0)
+ i_zero(&hdr);
+ if (guid_128_is_empty(hdr.mailbox_guid))
+ guid_128_generate(hdr.mailbox_guid);
+ if (++hdr.rebuild_count == 0)
+ hdr.rebuild_count = 1;
+ /* mailbox is being reset. this gets written directly there */
+ mail_index_set_ext_init_data(ctx->box->index, mbox->hdr_ext_id,
+ &hdr, sizeof(hdr));
+}
+
+static int
+sdbox_sync_index_rebuild_singles(struct index_rebuild_context *ctx)
+{
+ const char *path, *alt_path;
+ int ret = 0;
+
+ path = mailbox_get_path(ctx->box);
+ if (mailbox_get_path_to(ctx->box, MAILBOX_LIST_PATH_TYPE_ALT_MAILBOX,
+ &alt_path) < 0)
+ return -1;
+
+ sdbox_sync_set_uidvalidity(ctx);
+ if (sdbox_sync_index_rebuild_dir(ctx, path, TRUE) < 0) {
+ mailbox_set_critical(ctx->box, "sdbox: Rebuilding failed");
+ ret = -1;
+ } else if (alt_path != NULL) {
+ if (sdbox_sync_index_rebuild_dir(ctx, alt_path, FALSE) < 0) {
+ mailbox_set_critical(ctx->box,
+ "sdbox: Rebuilding failed on alt path %s",
+ alt_path);
+ ret = -1;
+ }
+ }
+ sdbox_sync_update_header(ctx);
+ return ret;
+}
+
+int sdbox_sync_index_rebuild(struct sdbox_mailbox *mbox, bool force)
+{
+ struct index_rebuild_context *ctx;
+ struct mail_index_view *view;
+ struct mail_index_transaction *trans;
+ struct sdbox_index_header hdr;
+ bool need_resize;
+ int ret;
+
+ if (!force && sdbox_read_header(mbox, &hdr, FALSE, &need_resize) == 0) {
+ if (hdr.rebuild_count != mbox->corrupted_rebuild_count &&
+ hdr.rebuild_count != 0) {
+ /* already rebuilt by someone else */
+ return 0;
+ }
+ }
+ i_warning("sdbox %s: Rebuilding index", mailbox_get_path(&mbox->box));
+
+ if (dbox_verify_alt_storage(mbox->box.list) < 0) {
+ mailbox_set_critical(&mbox->box,
+ "sdbox: Alt storage not mounted, "
+ "aborting index rebuild");
+ return -1;
+ }
+
+ view = mail_index_view_open(mbox->box.index);
+ trans = mail_index_transaction_begin(view,
+ MAIL_INDEX_TRANSACTION_FLAG_EXTERNAL);
+
+ ctx = index_index_rebuild_init(&mbox->box, view, trans);
+ ret = sdbox_sync_index_rebuild_singles(ctx);
+ index_index_rebuild_deinit(&ctx, dbox_get_uidvalidity_next);
+
+ if (ret < 0)
+ mail_index_transaction_rollback(&trans);
+ else {
+ mail_index_unset_fscked(trans);
+ ret = mail_index_transaction_commit(&trans);
+ }
+ mail_index_view_close(&view);
+ mbox->corrupted_rebuild_count = 0;
+ return ret;
+}
diff --git a/src/lib-storage/index/dbox-single/sdbox-sync.c b/src/lib-storage/index/dbox-single/sdbox-sync.c
new file mode 100644
index 0000000..670b768
--- /dev/null
+++ b/src/lib-storage/index/dbox-single/sdbox-sync.c
@@ -0,0 +1,326 @@
+/* Copyright (c) 2007-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "dbox-attachment.h"
+#include "sdbox-storage.h"
+#include "sdbox-file.h"
+#include "sdbox-sync.h"
+#include "mailbox-recent-flags.h"
+
+#define SDBOX_REBUILD_COUNT 3
+
+static void
+dbox_sync_file_move_if_needed(struct dbox_file *file,
+ enum sdbox_sync_entry_type type)
+{
+ struct stat st;
+ bool move_to_alt = type == SDBOX_SYNC_ENTRY_TYPE_MOVE_TO_ALT;
+ bool deleted;
+
+ if (move_to_alt == dbox_file_is_in_alt(file) &&
+ !move_to_alt) {
+ /* unopened dbox files default to primary dir.
+ stat the file to update its location. */
+ (void)dbox_file_stat(file, &st);
+
+ }
+ if (move_to_alt != dbox_file_is_in_alt(file)) {
+ /* move the file. if it fails, nothing broke so
+ don't worry about it. */
+ if (dbox_file_open(file, &deleted) > 0 && !deleted)
+ (void)sdbox_file_move(file, move_to_alt);
+ }
+}
+
+static void sdbox_sync_file(struct sdbox_sync_context *ctx,
+ uint32_t seq, uint32_t uid,
+ enum sdbox_sync_entry_type type)
+{
+ struct dbox_file *file;
+ enum modify_type modify_type;
+
+ switch (type) {
+ case SDBOX_SYNC_ENTRY_TYPE_EXPUNGE:
+ if (!mail_index_transaction_is_expunged(ctx->trans, seq)) {
+ mail_index_expunge(ctx->trans, seq);
+ array_push_back(&ctx->expunged_uids, &uid);
+ }
+ break;
+ case SDBOX_SYNC_ENTRY_TYPE_MOVE_FROM_ALT:
+ case SDBOX_SYNC_ENTRY_TYPE_MOVE_TO_ALT:
+ /* update flags in the sync transaction, mainly to make
+ sure that these alt changes get marked as synced
+ and won't be retried */
+ modify_type = type == SDBOX_SYNC_ENTRY_TYPE_MOVE_TO_ALT ?
+ MODIFY_ADD : MODIFY_REMOVE;
+ mail_index_update_flags(ctx->trans, seq, modify_type,
+ (enum mail_flags)DBOX_INDEX_FLAG_ALT);
+ file = sdbox_file_init(ctx->mbox, uid);
+ dbox_sync_file_move_if_needed(file, type);
+ dbox_file_unref(&file);
+ break;
+ }
+}
+
+static void sdbox_sync_add(struct sdbox_sync_context *ctx,
+ const struct mail_index_sync_rec *sync_rec)
+{
+ uint32_t uid;
+ enum sdbox_sync_entry_type type;
+ uint32_t seq, seq1, seq2;
+
+ if (sync_rec->type == MAIL_INDEX_SYNC_TYPE_EXPUNGE) {
+ /* we're interested */
+ type = SDBOX_SYNC_ENTRY_TYPE_EXPUNGE;
+ } else if (sync_rec->type == MAIL_INDEX_SYNC_TYPE_FLAGS) {
+ /* we care only about alt flag changes */
+ if ((sync_rec->add_flags & DBOX_INDEX_FLAG_ALT) != 0)
+ type = SDBOX_SYNC_ENTRY_TYPE_MOVE_TO_ALT;
+ else if ((sync_rec->remove_flags & DBOX_INDEX_FLAG_ALT) != 0)
+ type = SDBOX_SYNC_ENTRY_TYPE_MOVE_FROM_ALT;
+ else
+ return;
+ } else {
+ /* not interested */
+ return;
+ }
+
+ if (!mail_index_lookup_seq_range(ctx->sync_view,
+ sync_rec->uid1, sync_rec->uid2,
+ &seq1, &seq2)) {
+ /* already expunged everything. nothing to do. */
+ return;
+ }
+
+ for (seq = seq1; seq <= seq2; seq++) {
+ mail_index_lookup_uid(ctx->sync_view, seq, &uid);
+ sdbox_sync_file(ctx, seq, uid, type);
+ }
+}
+
+static int sdbox_sync_index(struct sdbox_sync_context *ctx)
+{
+ struct mailbox *box = &ctx->mbox->box;
+ const struct mail_index_header *hdr;
+ struct mail_index_sync_rec sync_rec;
+ uint32_t seq1, seq2;
+
+ hdr = mail_index_get_header(ctx->sync_view);
+ if (hdr->uid_validity == 0) {
+ /* newly created index file */
+ if (hdr->next_uid == 1) {
+ /* could be just a race condition where we opened the
+ mailbox between mkdir and index creation. fix this
+ silently. */
+ if (sdbox_mailbox_create_indexes(box, NULL, ctx->trans) < 0)
+ return -1;
+ return 1;
+ }
+ mailbox_set_critical(box,
+ "sdbox: Broken index: missing UIDVALIDITY");
+ sdbox_set_mailbox_corrupted(box);
+ return 0;
+ }
+
+ /* mark the newly seen messages as recent */
+ if (mail_index_lookup_seq_range(ctx->sync_view, hdr->first_recent_uid,
+ hdr->next_uid, &seq1, &seq2))
+ mailbox_recent_flags_set_seqs(box, ctx->sync_view, seq1, seq2);
+
+ while (mail_index_sync_next(ctx->index_sync_ctx, &sync_rec))
+ sdbox_sync_add(ctx, &sync_rec);
+ return 1;
+}
+
+static void dbox_sync_file_expunge(struct sdbox_sync_context *ctx,
+ uint32_t uid)
+{
+ struct mailbox *box = &ctx->mbox->box;
+ struct dbox_file *file;
+ struct sdbox_file *sfile;
+ int ret;
+
+ file = sdbox_file_init(ctx->mbox, uid);
+ sfile = (struct sdbox_file *)file;
+ if (file->storage->attachment_dir != NULL)
+ ret = sdbox_file_unlink_with_attachments(sfile);
+ else
+ ret = dbox_file_unlink(file);
+
+ /* do sync_notify only when the file was unlinked by us */
+ if (ret > 0)
+ mailbox_sync_notify(box, uid, MAILBOX_SYNC_TYPE_EXPUNGE);
+ dbox_file_unref(&file);
+}
+
+static void dbox_sync_expunge_files(struct sdbox_sync_context *ctx)
+{
+ uint32_t uid;
+
+ /* NOTE: Index is no longer locked. Multiple processes may be unlinking
+ the files at the same time. */
+ ctx->mbox->box.tmp_sync_view = ctx->sync_view;
+ array_foreach_elem(&ctx->expunged_uids, uid) T_BEGIN {
+ dbox_sync_file_expunge(ctx, uid);
+ } T_END;
+ mailbox_sync_notify(&ctx->mbox->box, 0, 0);
+ ctx->mbox->box.tmp_sync_view = NULL;
+}
+
+static int
+sdbox_refresh_header(struct sdbox_mailbox *mbox, bool retry, bool log_error)
+{
+ struct mail_index_view *view;
+ struct sdbox_index_header hdr;
+ bool need_resize;
+ int ret;
+
+ view = mail_index_view_open(mbox->box.index);
+ ret = sdbox_read_header(mbox, &hdr, log_error, &need_resize);
+ mail_index_view_close(&view);
+
+ if (ret < 0 && retry) {
+ mail_index_refresh(mbox->box.index);
+ return sdbox_refresh_header(mbox, FALSE, log_error);
+ }
+ return ret;
+}
+
+int sdbox_sync_begin(struct sdbox_mailbox *mbox, enum sdbox_sync_flags flags,
+ struct sdbox_sync_context **ctx_r)
+{
+ const struct mail_index_header *hdr =
+ mail_index_get_header(mbox->box.view);
+ struct sdbox_sync_context *ctx;
+ enum mail_index_sync_flags sync_flags;
+ unsigned int i;
+ int ret;
+ bool rebuild, force_rebuild;
+
+ force_rebuild = (flags & SDBOX_SYNC_FLAG_FORCE_REBUILD) != 0;
+ rebuild = force_rebuild ||
+ (hdr->flags & MAIL_INDEX_HDR_FLAG_FSCKD) != 0 ||
+ mbox->corrupted_rebuild_count != 0 ||
+ sdbox_refresh_header(mbox, TRUE, FALSE) < 0;
+
+ ctx = i_new(struct sdbox_sync_context, 1);
+ ctx->mbox = mbox;
+ ctx->flags = flags;
+ i_array_init(&ctx->expunged_uids, 32);
+
+ sync_flags = index_storage_get_sync_flags(&mbox->box);
+ if (!rebuild && (flags & SDBOX_SYNC_FLAG_FORCE) == 0)
+ sync_flags |= MAIL_INDEX_SYNC_FLAG_REQUIRE_CHANGES;
+ if ((flags & SDBOX_SYNC_FLAG_FSYNC) != 0)
+ sync_flags |= MAIL_INDEX_SYNC_FLAG_FSYNC;
+ /* don't write unnecessary dirty flag updates */
+ sync_flags |= MAIL_INDEX_SYNC_FLAG_AVOID_FLAG_UPDATES;
+
+ for (i = 0;; i++) {
+ ret = index_storage_expunged_sync_begin(&mbox->box,
+ &ctx->index_sync_ctx, &ctx->sync_view,
+ &ctx->trans, sync_flags);
+ if (mail_index_reset_fscked(mbox->box.index))
+ sdbox_set_mailbox_corrupted(&mbox->box);
+ if (ret <= 0) {
+ array_free(&ctx->expunged_uids);
+ i_free(ctx);
+ *ctx_r = NULL;
+ return ret;
+ }
+
+ if (rebuild)
+ ret = 0;
+ else {
+ if ((ret = sdbox_sync_index(ctx)) > 0)
+ break;
+ }
+
+ /* failure. keep the index locked while we're doing a
+ rebuild. */
+ if (ret == 0) {
+ if (i >= SDBOX_REBUILD_COUNT) {
+ mailbox_set_critical(&ctx->mbox->box,
+ "sdbox: Index keeps breaking");
+ ret = -1;
+ } else {
+ /* do a full resync and try again. */
+ rebuild = FALSE;
+ ret = sdbox_sync_index_rebuild(mbox,
+ force_rebuild);
+ }
+ }
+ mail_index_sync_rollback(&ctx->index_sync_ctx);
+ if (ret < 0) {
+ index_storage_expunging_deinit(&ctx->mbox->box);
+ array_free(&ctx->expunged_uids);
+ i_free(ctx);
+ return -1;
+ }
+ }
+
+ *ctx_r = ctx;
+ return 0;
+}
+
+int sdbox_sync_finish(struct sdbox_sync_context **_ctx, bool success)
+{
+ struct sdbox_sync_context *ctx = *_ctx;
+ struct mail_storage *storage = &ctx->mbox->storage->storage.storage;
+ int ret = success ? 0 : -1;
+
+ *_ctx = NULL;
+
+ if (success) {
+ mail_index_view_ref(ctx->sync_view);
+
+ if (mail_index_sync_commit(&ctx->index_sync_ctx) < 0) {
+ mailbox_set_index_error(&ctx->mbox->box);
+ ret = -1;
+ } else {
+ dbox_sync_expunge_files(ctx);
+ mail_index_view_close(&ctx->sync_view);
+ }
+ } else {
+ mail_index_sync_rollback(&ctx->index_sync_ctx);
+ }
+
+ if (storage->rebuild_list_index)
+ ret = mail_storage_list_index_rebuild_and_set_uncorrupted(storage);
+
+ index_storage_expunging_deinit(&ctx->mbox->box);
+ array_free(&ctx->expunged_uids);
+ i_free(ctx);
+ return ret;
+}
+
+int sdbox_sync(struct sdbox_mailbox *mbox, enum sdbox_sync_flags flags)
+{
+ struct sdbox_sync_context *sync_ctx;
+
+ if (sdbox_sync_begin(mbox, flags, &sync_ctx) < 0)
+ return -1;
+
+ if (sync_ctx == NULL)
+ return 0;
+ return sdbox_sync_finish(&sync_ctx, TRUE);
+}
+
+struct mailbox_sync_context *
+sdbox_storage_sync_init(struct mailbox *box, enum mailbox_sync_flags flags)
+{
+ struct sdbox_mailbox *mbox = SDBOX_MAILBOX(box);
+ enum sdbox_sync_flags sdbox_sync_flags = 0;
+ int ret = 0;
+
+ if (mail_index_reset_fscked(box->index))
+ sdbox_set_mailbox_corrupted(box);
+ if (index_mailbox_want_full_sync(&mbox->box, flags) ||
+ mbox->corrupted_rebuild_count != 0) {
+ if ((flags & MAILBOX_SYNC_FLAG_FORCE_RESYNC) != 0)
+ sdbox_sync_flags |= SDBOX_SYNC_FLAG_FORCE_REBUILD;
+ ret = sdbox_sync(mbox, sdbox_sync_flags);
+ }
+
+ return index_mailbox_sync_init(box, flags, ret < 0);
+}
diff --git a/src/lib-storage/index/dbox-single/sdbox-sync.h b/src/lib-storage/index/dbox-single/sdbox-sync.h
new file mode 100644
index 0000000..70306c9
--- /dev/null
+++ b/src/lib-storage/index/dbox-single/sdbox-sync.h
@@ -0,0 +1,38 @@
+#ifndef SDBOX_SYNC_H
+#define SDBOX_SYNC_H
+
+struct mailbox;
+struct sdbox_mailbox;
+
+enum sdbox_sync_flags {
+ SDBOX_SYNC_FLAG_FORCE = 0x01,
+ SDBOX_SYNC_FLAG_FSYNC = 0x02,
+ SDBOX_SYNC_FLAG_FORCE_REBUILD = 0x04
+};
+
+enum sdbox_sync_entry_type {
+ SDBOX_SYNC_ENTRY_TYPE_EXPUNGE,
+ SDBOX_SYNC_ENTRY_TYPE_MOVE_FROM_ALT,
+ SDBOX_SYNC_ENTRY_TYPE_MOVE_TO_ALT
+};
+
+struct sdbox_sync_context {
+ struct sdbox_mailbox *mbox;
+ struct mail_index_sync_ctx *index_sync_ctx;
+ struct mail_index_view *sync_view;
+ struct mail_index_transaction *trans;
+ enum sdbox_sync_flags flags;
+ ARRAY_TYPE(uint32_t) expunged_uids;
+};
+
+int sdbox_sync_begin(struct sdbox_mailbox *mbox, enum sdbox_sync_flags flags,
+ struct sdbox_sync_context **ctx_r);
+int sdbox_sync_finish(struct sdbox_sync_context **ctx, bool success);
+int sdbox_sync(struct sdbox_mailbox *mbox, enum sdbox_sync_flags flags);
+
+int sdbox_sync_index_rebuild(struct sdbox_mailbox *mbox, bool force);
+
+struct mailbox_sync_context *
+sdbox_storage_sync_init(struct mailbox *box, enum mailbox_sync_flags flags);
+
+#endif
diff --git a/src/lib-storage/index/imapc/Makefile.am b/src/lib-storage/index/imapc/Makefile.am
new file mode 100644
index 0000000..72ee102
--- /dev/null
+++ b/src/lib-storage/index/imapc/Makefile.am
@@ -0,0 +1,36 @@
+noinst_LTLIBRARIES = libstorage_imapc.la
+
+AM_CPPFLAGS = \
+ -I$(top_srcdir)/src/lib \
+ -I$(top_srcdir)/src/lib-test \
+ -I$(top_srcdir)/src/lib-settings \
+ -I$(top_srcdir)/src/lib-mail \
+ -I$(top_srcdir)/src/lib-imap \
+ -I$(top_srcdir)/src/lib-imap-client \
+ -I$(top_srcdir)/src/lib-index \
+ -I$(top_srcdir)/src/lib-storage \
+ -I$(top_srcdir)/src/lib-storage/list \
+ -I$(top_srcdir)/src/lib-storage/index \
+ -I$(top_srcdir)/src/lib-ssl-iostream
+
+libstorage_imapc_la_SOURCES = \
+ imapc-list.c \
+ imapc-mail.c \
+ imapc-mail-fetch.c \
+ imapc-mailbox.c \
+ imapc-save.c \
+ imapc-search.c \
+ imapc-settings.c \
+ imapc-sync.c \
+ imapc-storage.c
+
+headers = \
+ imapc-list.h \
+ imapc-mail.h \
+ imapc-search.h \
+ imapc-settings.h \
+ imapc-storage.h \
+ imapc-sync.h
+
+pkginc_libdir=$(pkgincludedir)
+pkginc_lib_HEADERS = $(headers)
diff --git a/src/lib-storage/index/imapc/Makefile.in b/src/lib-storage/index/imapc/Makefile.in
new file mode 100644
index 0000000..d36e1ce
--- /dev/null
+++ b/src/lib-storage/index/imapc/Makefile.in
@@ -0,0 +1,861 @@
+# Makefile.in generated by automake 1.16.1 from Makefile.am.
+# @configure_input@
+
+# Copyright (C) 1994-2018 Free Software Foundation, Inc.
+
+# This Makefile.in is free software; the Free Software Foundation
+# gives unlimited permission to copy and/or distribute it,
+# with or without modifications, as long as this notice is preserved.
+
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY, to the extent permitted by law; without
+# even the implied warranty of MERCHANTABILITY or FITNESS FOR A
+# PARTICULAR PURPOSE.
+
+@SET_MAKE@
+
+
+VPATH = @srcdir@
+am__is_gnu_make = { \
+ if test -z '$(MAKELEVEL)'; then \
+ false; \
+ elif test -n '$(MAKE_HOST)'; then \
+ true; \
+ elif test -n '$(MAKE_VERSION)' && test -n '$(CURDIR)'; then \
+ true; \
+ else \
+ false; \
+ fi; \
+}
+am__make_running_with_option = \
+ case $${target_option-} in \
+ ?) ;; \
+ *) echo "am__make_running_with_option: internal error: invalid" \
+ "target option '$${target_option-}' specified" >&2; \
+ exit 1;; \
+ esac; \
+ has_opt=no; \
+ sane_makeflags=$$MAKEFLAGS; \
+ if $(am__is_gnu_make); then \
+ sane_makeflags=$$MFLAGS; \
+ else \
+ case $$MAKEFLAGS in \
+ *\\[\ \ ]*) \
+ bs=\\; \
+ sane_makeflags=`printf '%s\n' "$$MAKEFLAGS" \
+ | sed "s/$$bs$$bs[$$bs $$bs ]*//g"`;; \
+ esac; \
+ fi; \
+ skip_next=no; \
+ strip_trailopt () \
+ { \
+ flg=`printf '%s\n' "$$flg" | sed "s/$$1.*$$//"`; \
+ }; \
+ for flg in $$sane_makeflags; do \
+ test $$skip_next = yes && { skip_next=no; continue; }; \
+ case $$flg in \
+ *=*|--*) continue;; \
+ -*I) strip_trailopt 'I'; skip_next=yes;; \
+ -*I?*) strip_trailopt 'I';; \
+ -*O) strip_trailopt 'O'; skip_next=yes;; \
+ -*O?*) strip_trailopt 'O';; \
+ -*l) strip_trailopt 'l'; skip_next=yes;; \
+ -*l?*) strip_trailopt 'l';; \
+ -[dEDm]) skip_next=yes;; \
+ -[JT]) skip_next=yes;; \
+ esac; \
+ case $$flg in \
+ *$$target_option*) has_opt=yes; break;; \
+ esac; \
+ done; \
+ test $$has_opt = yes
+am__make_dryrun = (target_option=n; $(am__make_running_with_option))
+am__make_keepgoing = (target_option=k; $(am__make_running_with_option))
+pkgdatadir = $(datadir)/@PACKAGE@
+pkgincludedir = $(includedir)/@PACKAGE@
+pkglibdir = $(libdir)/@PACKAGE@
+pkglibexecdir = $(libexecdir)/@PACKAGE@
+am__cd = CDPATH="$${ZSH_VERSION+.}$(PATH_SEPARATOR)" && cd
+install_sh_DATA = $(install_sh) -c -m 644
+install_sh_PROGRAM = $(install_sh) -c
+install_sh_SCRIPT = $(install_sh) -c
+INSTALL_HEADER = $(INSTALL_DATA)
+transform = $(program_transform_name)
+NORMAL_INSTALL = :
+PRE_INSTALL = :
+POST_INSTALL = :
+NORMAL_UNINSTALL = :
+PRE_UNINSTALL = :
+POST_UNINSTALL = :
+build_triplet = @build@
+host_triplet = @host@
+subdir = src/lib-storage/index/imapc
+ACLOCAL_M4 = $(top_srcdir)/aclocal.m4
+am__aclocal_m4_deps = $(top_srcdir)/m4/ac_checktype2.m4 \
+ $(top_srcdir)/m4/ac_typeof.m4 $(top_srcdir)/m4/arc4random.m4 \
+ $(top_srcdir)/m4/blockdev.m4 $(top_srcdir)/m4/c99_vsnprintf.m4 \
+ $(top_srcdir)/m4/clock_gettime.m4 $(top_srcdir)/m4/crypt.m4 \
+ $(top_srcdir)/m4/crypt_xpg6.m4 $(top_srcdir)/m4/dbqlk.m4 \
+ $(top_srcdir)/m4/dirent_dtype.m4 $(top_srcdir)/m4/dovecot.m4 \
+ $(top_srcdir)/m4/fd_passing.m4 $(top_srcdir)/m4/fdatasync.m4 \
+ $(top_srcdir)/m4/flexible_array_member.m4 \
+ $(top_srcdir)/m4/glibc.m4 $(top_srcdir)/m4/gmtime_max.m4 \
+ $(top_srcdir)/m4/gmtime_tm_gmtoff.m4 \
+ $(top_srcdir)/m4/ioloop.m4 $(top_srcdir)/m4/iovec.m4 \
+ $(top_srcdir)/m4/ipv6.m4 $(top_srcdir)/m4/libcap.m4 \
+ $(top_srcdir)/m4/libtool.m4 $(top_srcdir)/m4/libwrap.m4 \
+ $(top_srcdir)/m4/linux_mremap.m4 $(top_srcdir)/m4/ltoptions.m4 \
+ $(top_srcdir)/m4/ltsugar.m4 $(top_srcdir)/m4/ltversion.m4 \
+ $(top_srcdir)/m4/lt~obsolete.m4 $(top_srcdir)/m4/mmap_write.m4 \
+ $(top_srcdir)/m4/mntctl.m4 $(top_srcdir)/m4/modules.m4 \
+ $(top_srcdir)/m4/notify.m4 $(top_srcdir)/m4/nsl.m4 \
+ $(top_srcdir)/m4/off_t_max.m4 $(top_srcdir)/m4/pkg.m4 \
+ $(top_srcdir)/m4/pr_set_dumpable.m4 \
+ $(top_srcdir)/m4/q_quotactl.m4 $(top_srcdir)/m4/quota.m4 \
+ $(top_srcdir)/m4/random.m4 $(top_srcdir)/m4/rlimit.m4 \
+ $(top_srcdir)/m4/sendfile.m4 $(top_srcdir)/m4/size_t_signed.m4 \
+ $(top_srcdir)/m4/sockpeercred.m4 $(top_srcdir)/m4/sql.m4 \
+ $(top_srcdir)/m4/ssl.m4 $(top_srcdir)/m4/st_tim.m4 \
+ $(top_srcdir)/m4/static_array.m4 $(top_srcdir)/m4/test_with.m4 \
+ $(top_srcdir)/m4/time_t.m4 $(top_srcdir)/m4/typeof.m4 \
+ $(top_srcdir)/m4/typeof_dev_t.m4 \
+ $(top_srcdir)/m4/uoff_t_max.m4 $(top_srcdir)/m4/vararg.m4 \
+ $(top_srcdir)/m4/want_apparmor.m4 \
+ $(top_srcdir)/m4/want_bsdauth.m4 \
+ $(top_srcdir)/m4/want_bzlib.m4 \
+ $(top_srcdir)/m4/want_cassandra.m4 \
+ $(top_srcdir)/m4/want_cdb.m4 \
+ $(top_srcdir)/m4/want_checkpassword.m4 \
+ $(top_srcdir)/m4/want_clucene.m4 $(top_srcdir)/m4/want_db.m4 \
+ $(top_srcdir)/m4/want_gssapi.m4 $(top_srcdir)/m4/want_icu.m4 \
+ $(top_srcdir)/m4/want_ldap.m4 $(top_srcdir)/m4/want_lua.m4 \
+ $(top_srcdir)/m4/want_lz4.m4 $(top_srcdir)/m4/want_lzma.m4 \
+ $(top_srcdir)/m4/want_mysql.m4 $(top_srcdir)/m4/want_pam.m4 \
+ $(top_srcdir)/m4/want_passwd.m4 $(top_srcdir)/m4/want_pgsql.m4 \
+ $(top_srcdir)/m4/want_prefetch.m4 \
+ $(top_srcdir)/m4/want_shadow.m4 \
+ $(top_srcdir)/m4/want_sodium.m4 $(top_srcdir)/m4/want_solr.m4 \
+ $(top_srcdir)/m4/want_sqlite.m4 \
+ $(top_srcdir)/m4/want_stemmer.m4 \
+ $(top_srcdir)/m4/want_systemd.m4 \
+ $(top_srcdir)/m4/want_textcat.m4 \
+ $(top_srcdir)/m4/want_unwind.m4 $(top_srcdir)/m4/want_zlib.m4 \
+ $(top_srcdir)/m4/want_zstd.m4 $(top_srcdir)/configure.ac
+am__configure_deps = $(am__aclocal_m4_deps) $(CONFIGURE_DEPENDENCIES) \
+ $(ACLOCAL_M4)
+DIST_COMMON = $(srcdir)/Makefile.am $(pkginc_lib_HEADERS) \
+ $(am__DIST_COMMON)
+mkinstalldirs = $(install_sh) -d
+CONFIG_HEADER = $(top_builddir)/config.h
+CONFIG_CLEAN_FILES =
+CONFIG_CLEAN_VPATH_FILES =
+LTLIBRARIES = $(noinst_LTLIBRARIES)
+libstorage_imapc_la_LIBADD =
+am_libstorage_imapc_la_OBJECTS = imapc-list.lo imapc-mail.lo \
+ imapc-mail-fetch.lo imapc-mailbox.lo imapc-save.lo \
+ imapc-search.lo imapc-settings.lo imapc-sync.lo \
+ imapc-storage.lo
+libstorage_imapc_la_OBJECTS = $(am_libstorage_imapc_la_OBJECTS)
+AM_V_lt = $(am__v_lt_@AM_V@)
+am__v_lt_ = $(am__v_lt_@AM_DEFAULT_V@)
+am__v_lt_0 = --silent
+am__v_lt_1 =
+AM_V_P = $(am__v_P_@AM_V@)
+am__v_P_ = $(am__v_P_@AM_DEFAULT_V@)
+am__v_P_0 = false
+am__v_P_1 = :
+AM_V_GEN = $(am__v_GEN_@AM_V@)
+am__v_GEN_ = $(am__v_GEN_@AM_DEFAULT_V@)
+am__v_GEN_0 = @echo " GEN " $@;
+am__v_GEN_1 =
+AM_V_at = $(am__v_at_@AM_V@)
+am__v_at_ = $(am__v_at_@AM_DEFAULT_V@)
+am__v_at_0 = @
+am__v_at_1 =
+DEFAULT_INCLUDES = -I.@am__isrc@ -I$(top_builddir)
+depcomp = $(SHELL) $(top_srcdir)/depcomp
+am__maybe_remake_depfiles = depfiles
+am__depfiles_remade = ./$(DEPDIR)/imapc-list.Plo \
+ ./$(DEPDIR)/imapc-mail-fetch.Plo ./$(DEPDIR)/imapc-mail.Plo \
+ ./$(DEPDIR)/imapc-mailbox.Plo ./$(DEPDIR)/imapc-save.Plo \
+ ./$(DEPDIR)/imapc-search.Plo ./$(DEPDIR)/imapc-settings.Plo \
+ ./$(DEPDIR)/imapc-storage.Plo ./$(DEPDIR)/imapc-sync.Plo
+am__mv = mv -f
+COMPILE = $(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) \
+ $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS)
+LTCOMPILE = $(LIBTOOL) $(AM_V_lt) --tag=CC $(AM_LIBTOOLFLAGS) \
+ $(LIBTOOLFLAGS) --mode=compile $(CC) $(DEFS) \
+ $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) $(CPPFLAGS) \
+ $(AM_CFLAGS) $(CFLAGS)
+AM_V_CC = $(am__v_CC_@AM_V@)
+am__v_CC_ = $(am__v_CC_@AM_DEFAULT_V@)
+am__v_CC_0 = @echo " CC " $@;
+am__v_CC_1 =
+CCLD = $(CC)
+LINK = $(LIBTOOL) $(AM_V_lt) --tag=CC $(AM_LIBTOOLFLAGS) \
+ $(LIBTOOLFLAGS) --mode=link $(CCLD) $(AM_CFLAGS) $(CFLAGS) \
+ $(AM_LDFLAGS) $(LDFLAGS) -o $@
+AM_V_CCLD = $(am__v_CCLD_@AM_V@)
+am__v_CCLD_ = $(am__v_CCLD_@AM_DEFAULT_V@)
+am__v_CCLD_0 = @echo " CCLD " $@;
+am__v_CCLD_1 =
+SOURCES = $(libstorage_imapc_la_SOURCES)
+DIST_SOURCES = $(libstorage_imapc_la_SOURCES)
+am__can_run_installinfo = \
+ case $$AM_UPDATE_INFO_DIR in \
+ n|no|NO) false;; \
+ *) (install-info --version) >/dev/null 2>&1;; \
+ esac
+am__vpath_adj_setup = srcdirstrip=`echo "$(srcdir)" | sed 's|.|.|g'`;
+am__vpath_adj = case $$p in \
+ $(srcdir)/*) f=`echo "$$p" | sed "s|^$$srcdirstrip/||"`;; \
+ *) f=$$p;; \
+ esac;
+am__strip_dir = f=`echo $$p | sed -e 's|^.*/||'`;
+am__install_max = 40
+am__nobase_strip_setup = \
+ srcdirstrip=`echo "$(srcdir)" | sed 's/[].[^$$\\*|]/\\\\&/g'`
+am__nobase_strip = \
+ for p in $$list; do echo "$$p"; done | sed -e "s|$$srcdirstrip/||"
+am__nobase_list = $(am__nobase_strip_setup); \
+ for p in $$list; do echo "$$p $$p"; done | \
+ sed "s| $$srcdirstrip/| |;"' / .*\//!s/ .*/ ./; s,\( .*\)/[^/]*$$,\1,' | \
+ $(AWK) 'BEGIN { files["."] = "" } { files[$$2] = files[$$2] " " $$1; \
+ if (++n[$$2] == $(am__install_max)) \
+ { print $$2, files[$$2]; n[$$2] = 0; files[$$2] = "" } } \
+ END { for (dir in files) print dir, files[dir] }'
+am__base_list = \
+ sed '$$!N;$$!N;$$!N;$$!N;$$!N;$$!N;$$!N;s/\n/ /g' | \
+ sed '$$!N;$$!N;$$!N;$$!N;s/\n/ /g'
+am__uninstall_files_from_dir = { \
+ test -z "$$files" \
+ || { test ! -d "$$dir" && test ! -f "$$dir" && test ! -r "$$dir"; } \
+ || { echo " ( cd '$$dir' && rm -f" $$files ")"; \
+ $(am__cd) "$$dir" && rm -f $$files; }; \
+ }
+am__installdirs = "$(DESTDIR)$(pkginc_libdir)"
+HEADERS = $(pkginc_lib_HEADERS)
+am__tagged_files = $(HEADERS) $(SOURCES) $(TAGS_FILES) $(LISP)
+# Read a list of newline-separated strings from the standard input,
+# and print each of them once, without duplicates. Input order is
+# *not* preserved.
+am__uniquify_input = $(AWK) '\
+ BEGIN { nonempty = 0; } \
+ { items[$$0] = 1; nonempty = 1; } \
+ END { if (nonempty) { for (i in items) print i; }; } \
+'
+# Make sure the list of sources is unique. This is necessary because,
+# e.g., the same source file might be shared among _SOURCES variables
+# for different programs/libraries.
+am__define_uniq_tagged_files = \
+ list='$(am__tagged_files)'; \
+ unique=`for i in $$list; do \
+ if test -f "$$i"; then echo $$i; else echo $(srcdir)/$$i; fi; \
+ done | $(am__uniquify_input)`
+ETAGS = etags
+CTAGS = ctags
+am__DIST_COMMON = $(srcdir)/Makefile.in $(top_srcdir)/depcomp
+DISTFILES = $(DIST_COMMON) $(DIST_SOURCES) $(TEXINFOS) $(EXTRA_DIST)
+ACLOCAL = @ACLOCAL@
+ACLOCAL_AMFLAGS = @ACLOCAL_AMFLAGS@
+AMTAR = @AMTAR@
+AM_DEFAULT_VERBOSITY = @AM_DEFAULT_VERBOSITY@
+APPARMOR_LIBS = @APPARMOR_LIBS@
+AR = @AR@
+AUTH_CFLAGS = @AUTH_CFLAGS@
+AUTH_LIBS = @AUTH_LIBS@
+AUTOCONF = @AUTOCONF@
+AUTOHEADER = @AUTOHEADER@
+AUTOMAKE = @AUTOMAKE@
+AWK = @AWK@
+BINARY_CFLAGS = @BINARY_CFLAGS@
+BINARY_LDFLAGS = @BINARY_LDFLAGS@
+BISON = @BISON@
+CASSANDRA_CFLAGS = @CASSANDRA_CFLAGS@
+CASSANDRA_LIBS = @CASSANDRA_LIBS@
+CC = @CC@
+CCDEPMODE = @CCDEPMODE@
+CDB_LIBS = @CDB_LIBS@
+CFLAGS = @CFLAGS@
+CLUCENE_CFLAGS = @CLUCENE_CFLAGS@
+CLUCENE_LIBS = @CLUCENE_LIBS@
+COMPRESS_LIBS = @COMPRESS_LIBS@
+CPP = @CPP@
+CPPFLAGS = @CPPFLAGS@
+CRYPT_LIBS = @CRYPT_LIBS@
+CXX = @CXX@
+CXXCPP = @CXXCPP@
+CXXDEPMODE = @CXXDEPMODE@
+CXXFLAGS = @CXXFLAGS@
+CYGPATH_W = @CYGPATH_W@
+DEFS = @DEFS@
+DEPDIR = @DEPDIR@
+DICT_LIBS = @DICT_LIBS@
+DLLIB = @DLLIB@
+DLLTOOL = @DLLTOOL@
+DSYMUTIL = @DSYMUTIL@
+DUMPBIN = @DUMPBIN@
+ECHO_C = @ECHO_C@
+ECHO_N = @ECHO_N@
+ECHO_T = @ECHO_T@
+EGREP = @EGREP@
+EXEEXT = @EXEEXT@
+FGREP = @FGREP@
+FLEX = @FLEX@
+FUZZER_CPPFLAGS = @FUZZER_CPPFLAGS@
+FUZZER_LDFLAGS = @FUZZER_LDFLAGS@
+GREP = @GREP@
+INSTALL = @INSTALL@
+INSTALL_DATA = @INSTALL_DATA@
+INSTALL_PROGRAM = @INSTALL_PROGRAM@
+INSTALL_SCRIPT = @INSTALL_SCRIPT@
+INSTALL_STRIP_PROGRAM = @INSTALL_STRIP_PROGRAM@
+KRB5CONFIG = @KRB5CONFIG@
+KRB5_CFLAGS = @KRB5_CFLAGS@
+KRB5_LIBS = @KRB5_LIBS@
+LD = @LD@
+LDAP_LIBS = @LDAP_LIBS@
+LDFLAGS = @LDFLAGS@
+LD_NO_WHOLE_ARCHIVE = @LD_NO_WHOLE_ARCHIVE@
+LD_WHOLE_ARCHIVE = @LD_WHOLE_ARCHIVE@
+LIBCAP = @LIBCAP@
+LIBDOVECOT = @LIBDOVECOT@
+LIBDOVECOT_COMPRESS = @LIBDOVECOT_COMPRESS@
+LIBDOVECOT_DEPS = @LIBDOVECOT_DEPS@
+LIBDOVECOT_DSYNC = @LIBDOVECOT_DSYNC@
+LIBDOVECOT_LA_LIBS = @LIBDOVECOT_LA_LIBS@
+LIBDOVECOT_LDA = @LIBDOVECOT_LDA@
+LIBDOVECOT_LDAP = @LIBDOVECOT_LDAP@
+LIBDOVECOT_LIBFTS = @LIBDOVECOT_LIBFTS@
+LIBDOVECOT_LIBFTS_DEPS = @LIBDOVECOT_LIBFTS_DEPS@
+LIBDOVECOT_LOGIN = @LIBDOVECOT_LOGIN@
+LIBDOVECOT_LUA = @LIBDOVECOT_LUA@
+LIBDOVECOT_LUA_DEPS = @LIBDOVECOT_LUA_DEPS@
+LIBDOVECOT_SQL = @LIBDOVECOT_SQL@
+LIBDOVECOT_STORAGE = @LIBDOVECOT_STORAGE@
+LIBDOVECOT_STORAGE_DEPS = @LIBDOVECOT_STORAGE_DEPS@
+LIBEXTTEXTCAT_CFLAGS = @LIBEXTTEXTCAT_CFLAGS@
+LIBEXTTEXTCAT_LIBS = @LIBEXTTEXTCAT_LIBS@
+LIBICONV = @LIBICONV@
+LIBICU_CFLAGS = @LIBICU_CFLAGS@
+LIBICU_LIBS = @LIBICU_LIBS@
+LIBOBJS = @LIBOBJS@
+LIBS = @LIBS@
+LIBSODIUM_CFLAGS = @LIBSODIUM_CFLAGS@
+LIBSODIUM_LIBS = @LIBSODIUM_LIBS@
+LIBTIRPC_CFLAGS = @LIBTIRPC_CFLAGS@
+LIBTIRPC_LIBS = @LIBTIRPC_LIBS@
+LIBTOOL = @LIBTOOL@
+LIBUNWIND_CFLAGS = @LIBUNWIND_CFLAGS@
+LIBUNWIND_LIBS = @LIBUNWIND_LIBS@
+LIBWRAP_LIBS = @LIBWRAP_LIBS@
+LINKED_STORAGE_LDADD = @LINKED_STORAGE_LDADD@
+LIPO = @LIPO@
+LN_S = @LN_S@
+LTLIBICONV = @LTLIBICONV@
+LTLIBOBJS = @LTLIBOBJS@
+LT_SYS_LIBRARY_PATH = @LT_SYS_LIBRARY_PATH@
+LUA_CFLAGS = @LUA_CFLAGS@
+LUA_LIBS = @LUA_LIBS@
+MAINT = @MAINT@
+MAKEINFO = @MAKEINFO@
+MANIFEST_TOOL = @MANIFEST_TOOL@
+MKDIR_P = @MKDIR_P@
+MODULE_LIBS = @MODULE_LIBS@
+MODULE_SUFFIX = @MODULE_SUFFIX@
+MYSQL_CFLAGS = @MYSQL_CFLAGS@
+MYSQL_CONFIG = @MYSQL_CONFIG@
+MYSQL_LIBS = @MYSQL_LIBS@
+NM = @NM@
+NMEDIT = @NMEDIT@
+NOPLUGIN_LDFLAGS = @NOPLUGIN_LDFLAGS@
+OBJDUMP = @OBJDUMP@
+OBJEXT = @OBJEXT@
+OTOOL = @OTOOL@
+OTOOL64 = @OTOOL64@
+PACKAGE = @PACKAGE@
+PACKAGE_BUGREPORT = @PACKAGE_BUGREPORT@
+PACKAGE_NAME = @PACKAGE_NAME@
+PACKAGE_STRING = @PACKAGE_STRING@
+PACKAGE_TARNAME = @PACKAGE_TARNAME@
+PACKAGE_URL = @PACKAGE_URL@
+PACKAGE_VERSION = @PACKAGE_VERSION@
+PANDOC = @PANDOC@
+PATH_SEPARATOR = @PATH_SEPARATOR@
+PGSQL_CFLAGS = @PGSQL_CFLAGS@
+PGSQL_LIBS = @PGSQL_LIBS@
+PG_CONFIG = @PG_CONFIG@
+PIE_CFLAGS = @PIE_CFLAGS@
+PIE_LDFLAGS = @PIE_LDFLAGS@
+PKG_CONFIG = @PKG_CONFIG@
+PKG_CONFIG_LIBDIR = @PKG_CONFIG_LIBDIR@
+PKG_CONFIG_PATH = @PKG_CONFIG_PATH@
+QUOTA_LIBS = @QUOTA_LIBS@
+RANLIB = @RANLIB@
+RELRO_LDFLAGS = @RELRO_LDFLAGS@
+RPCGEN = @RPCGEN@
+RUN_TEST = @RUN_TEST@
+SED = @SED@
+SETTING_FILES = @SETTING_FILES@
+SET_MAKE = @SET_MAKE@
+SHELL = @SHELL@
+SQLITE_CFLAGS = @SQLITE_CFLAGS@
+SQLITE_LIBS = @SQLITE_LIBS@
+SQL_CFLAGS = @SQL_CFLAGS@
+SQL_LIBS = @SQL_LIBS@
+SSL_CFLAGS = @SSL_CFLAGS@
+SSL_LIBS = @SSL_LIBS@
+STRIP = @STRIP@
+SYSTEMD_CFLAGS = @SYSTEMD_CFLAGS@
+SYSTEMD_LIBS = @SYSTEMD_LIBS@
+VALGRIND = @VALGRIND@
+VERSION = @VERSION@
+ZSTD_CFLAGS = @ZSTD_CFLAGS@
+ZSTD_LIBS = @ZSTD_LIBS@
+abs_builddir = @abs_builddir@
+abs_srcdir = @abs_srcdir@
+abs_top_builddir = @abs_top_builddir@
+abs_top_srcdir = @abs_top_srcdir@
+ac_ct_AR = @ac_ct_AR@
+ac_ct_CC = @ac_ct_CC@
+ac_ct_CXX = @ac_ct_CXX@
+ac_ct_DUMPBIN = @ac_ct_DUMPBIN@
+am__include = @am__include@
+am__leading_dot = @am__leading_dot@
+am__quote = @am__quote@
+am__tar = @am__tar@
+am__untar = @am__untar@
+bindir = @bindir@
+build = @build@
+build_alias = @build_alias@
+build_cpu = @build_cpu@
+build_os = @build_os@
+build_vendor = @build_vendor@
+builddir = @builddir@
+datadir = @datadir@
+datarootdir = @datarootdir@
+dict_drivers = @dict_drivers@
+docdir = @docdir@
+dvidir = @dvidir@
+exec_prefix = @exec_prefix@
+host = @host@
+host_alias = @host_alias@
+host_cpu = @host_cpu@
+host_os = @host_os@
+host_vendor = @host_vendor@
+htmldir = @htmldir@
+includedir = @includedir@
+infodir = @infodir@
+install_sh = @install_sh@
+libdir = @libdir@
+libexecdir = @libexecdir@
+localedir = @localedir@
+localstatedir = @localstatedir@
+mandir = @mandir@
+mkdir_p = @mkdir_p@
+moduledir = @moduledir@
+oldincludedir = @oldincludedir@
+pdfdir = @pdfdir@
+prefix = @prefix@
+program_transform_name = @program_transform_name@
+psdir = @psdir@
+rundir = @rundir@
+runstatedir = @runstatedir@
+sbindir = @sbindir@
+sharedstatedir = @sharedstatedir@
+sql_drivers = @sql_drivers@
+srcdir = @srcdir@
+ssldir = @ssldir@
+statedir = @statedir@
+sysconfdir = @sysconfdir@
+systemdservicetype = @systemdservicetype@
+systemdsystemunitdir = @systemdsystemunitdir@
+target_alias = @target_alias@
+top_build_prefix = @top_build_prefix@
+top_builddir = @top_builddir@
+top_srcdir = @top_srcdir@
+noinst_LTLIBRARIES = libstorage_imapc.la
+AM_CPPFLAGS = \
+ -I$(top_srcdir)/src/lib \
+ -I$(top_srcdir)/src/lib-test \
+ -I$(top_srcdir)/src/lib-settings \
+ -I$(top_srcdir)/src/lib-mail \
+ -I$(top_srcdir)/src/lib-imap \
+ -I$(top_srcdir)/src/lib-imap-client \
+ -I$(top_srcdir)/src/lib-index \
+ -I$(top_srcdir)/src/lib-storage \
+ -I$(top_srcdir)/src/lib-storage/list \
+ -I$(top_srcdir)/src/lib-storage/index \
+ -I$(top_srcdir)/src/lib-ssl-iostream
+
+libstorage_imapc_la_SOURCES = \
+ imapc-list.c \
+ imapc-mail.c \
+ imapc-mail-fetch.c \
+ imapc-mailbox.c \
+ imapc-save.c \
+ imapc-search.c \
+ imapc-settings.c \
+ imapc-sync.c \
+ imapc-storage.c
+
+headers = \
+ imapc-list.h \
+ imapc-mail.h \
+ imapc-search.h \
+ imapc-settings.h \
+ imapc-storage.h \
+ imapc-sync.h
+
+pkginc_libdir = $(pkgincludedir)
+pkginc_lib_HEADERS = $(headers)
+all: all-am
+
+.SUFFIXES:
+.SUFFIXES: .c .lo .o .obj
+$(srcdir)/Makefile.in: @MAINTAINER_MODE_TRUE@ $(srcdir)/Makefile.am $(am__configure_deps)
+ @for dep in $?; do \
+ case '$(am__configure_deps)' in \
+ *$$dep*) \
+ ( cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh ) \
+ && { if test -f $@; then exit 0; else break; fi; }; \
+ exit 1;; \
+ esac; \
+ done; \
+ echo ' cd $(top_srcdir) && $(AUTOMAKE) --foreign src/lib-storage/index/imapc/Makefile'; \
+ $(am__cd) $(top_srcdir) && \
+ $(AUTOMAKE) --foreign src/lib-storage/index/imapc/Makefile
+Makefile: $(srcdir)/Makefile.in $(top_builddir)/config.status
+ @case '$?' in \
+ *config.status*) \
+ cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh;; \
+ *) \
+ echo ' cd $(top_builddir) && $(SHELL) ./config.status $(subdir)/$@ $(am__maybe_remake_depfiles)'; \
+ cd $(top_builddir) && $(SHELL) ./config.status $(subdir)/$@ $(am__maybe_remake_depfiles);; \
+ esac;
+
+$(top_builddir)/config.status: $(top_srcdir)/configure $(CONFIG_STATUS_DEPENDENCIES)
+ cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh
+
+$(top_srcdir)/configure: @MAINTAINER_MODE_TRUE@ $(am__configure_deps)
+ cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh
+$(ACLOCAL_M4): @MAINTAINER_MODE_TRUE@ $(am__aclocal_m4_deps)
+ cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh
+$(am__aclocal_m4_deps):
+
+clean-noinstLTLIBRARIES:
+ -test -z "$(noinst_LTLIBRARIES)" || rm -f $(noinst_LTLIBRARIES)
+ @list='$(noinst_LTLIBRARIES)'; \
+ locs=`for p in $$list; do echo $$p; done | \
+ sed 's|^[^/]*$$|.|; s|/[^/]*$$||; s|$$|/so_locations|' | \
+ sort -u`; \
+ test -z "$$locs" || { \
+ echo rm -f $${locs}; \
+ rm -f $${locs}; \
+ }
+
+libstorage_imapc.la: $(libstorage_imapc_la_OBJECTS) $(libstorage_imapc_la_DEPENDENCIES) $(EXTRA_libstorage_imapc_la_DEPENDENCIES)
+ $(AM_V_CCLD)$(LINK) $(libstorage_imapc_la_OBJECTS) $(libstorage_imapc_la_LIBADD) $(LIBS)
+
+mostlyclean-compile:
+ -rm -f *.$(OBJEXT)
+
+distclean-compile:
+ -rm -f *.tab.c
+
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/imapc-list.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/imapc-mail-fetch.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/imapc-mail.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/imapc-mailbox.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/imapc-save.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/imapc-search.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/imapc-settings.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/imapc-storage.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/imapc-sync.Plo@am__quote@ # am--include-marker
+
+$(am__depfiles_remade):
+ @$(MKDIR_P) $(@D)
+ @echo '# dummy' >$@-t && $(am__mv) $@-t $@
+
+am--depfiles: $(am__depfiles_remade)
+
+.c.o:
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(COMPILE) -MT $@ -MD -MP -MF $(DEPDIR)/$*.Tpo -c -o $@ $<
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/$*.Tpo $(DEPDIR)/$*.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='$<' object='$@' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(COMPILE) -c -o $@ $<
+
+.c.obj:
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(COMPILE) -MT $@ -MD -MP -MF $(DEPDIR)/$*.Tpo -c -o $@ `$(CYGPATH_W) '$<'`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/$*.Tpo $(DEPDIR)/$*.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='$<' object='$@' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(COMPILE) -c -o $@ `$(CYGPATH_W) '$<'`
+
+.c.lo:
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(LTCOMPILE) -MT $@ -MD -MP -MF $(DEPDIR)/$*.Tpo -c -o $@ $<
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/$*.Tpo $(DEPDIR)/$*.Plo
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='$<' object='$@' libtool=yes @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(LTCOMPILE) -c -o $@ $<
+
+mostlyclean-libtool:
+ -rm -f *.lo
+
+clean-libtool:
+ -rm -rf .libs _libs
+install-pkginc_libHEADERS: $(pkginc_lib_HEADERS)
+ @$(NORMAL_INSTALL)
+ @list='$(pkginc_lib_HEADERS)'; test -n "$(pkginc_libdir)" || list=; \
+ if test -n "$$list"; then \
+ echo " $(MKDIR_P) '$(DESTDIR)$(pkginc_libdir)'"; \
+ $(MKDIR_P) "$(DESTDIR)$(pkginc_libdir)" || exit 1; \
+ fi; \
+ for p in $$list; do \
+ if test -f "$$p"; then d=; else d="$(srcdir)/"; fi; \
+ echo "$$d$$p"; \
+ done | $(am__base_list) | \
+ while read files; do \
+ echo " $(INSTALL_HEADER) $$files '$(DESTDIR)$(pkginc_libdir)'"; \
+ $(INSTALL_HEADER) $$files "$(DESTDIR)$(pkginc_libdir)" || exit $$?; \
+ done
+
+uninstall-pkginc_libHEADERS:
+ @$(NORMAL_UNINSTALL)
+ @list='$(pkginc_lib_HEADERS)'; test -n "$(pkginc_libdir)" || list=; \
+ files=`for p in $$list; do echo $$p; done | sed -e 's|^.*/||'`; \
+ dir='$(DESTDIR)$(pkginc_libdir)'; $(am__uninstall_files_from_dir)
+
+ID: $(am__tagged_files)
+ $(am__define_uniq_tagged_files); mkid -fID $$unique
+tags: tags-am
+TAGS: tags
+
+tags-am: $(TAGS_DEPENDENCIES) $(am__tagged_files)
+ set x; \
+ here=`pwd`; \
+ $(am__define_uniq_tagged_files); \
+ shift; \
+ if test -z "$(ETAGS_ARGS)$$*$$unique"; then :; else \
+ test -n "$$unique" || unique=$$empty_fix; \
+ if test $$# -gt 0; then \
+ $(ETAGS) $(ETAGSFLAGS) $(AM_ETAGSFLAGS) $(ETAGS_ARGS) \
+ "$$@" $$unique; \
+ else \
+ $(ETAGS) $(ETAGSFLAGS) $(AM_ETAGSFLAGS) $(ETAGS_ARGS) \
+ $$unique; \
+ fi; \
+ fi
+ctags: ctags-am
+
+CTAGS: ctags
+ctags-am: $(TAGS_DEPENDENCIES) $(am__tagged_files)
+ $(am__define_uniq_tagged_files); \
+ test -z "$(CTAGS_ARGS)$$unique" \
+ || $(CTAGS) $(CTAGSFLAGS) $(AM_CTAGSFLAGS) $(CTAGS_ARGS) \
+ $$unique
+
+GTAGS:
+ here=`$(am__cd) $(top_builddir) && pwd` \
+ && $(am__cd) $(top_srcdir) \
+ && gtags -i $(GTAGS_ARGS) "$$here"
+cscopelist: cscopelist-am
+
+cscopelist-am: $(am__tagged_files)
+ list='$(am__tagged_files)'; \
+ case "$(srcdir)" in \
+ [\\/]* | ?:[\\/]*) sdir="$(srcdir)" ;; \
+ *) sdir=$(subdir)/$(srcdir) ;; \
+ esac; \
+ for i in $$list; do \
+ if test -f "$$i"; then \
+ echo "$(subdir)/$$i"; \
+ else \
+ echo "$$sdir/$$i"; \
+ fi; \
+ done >> $(top_builddir)/cscope.files
+
+distclean-tags:
+ -rm -f TAGS ID GTAGS GRTAGS GSYMS GPATH tags
+
+distdir: $(BUILT_SOURCES)
+ $(MAKE) $(AM_MAKEFLAGS) distdir-am
+
+distdir-am: $(DISTFILES)
+ @srcdirstrip=`echo "$(srcdir)" | sed 's/[].[^$$\\*]/\\\\&/g'`; \
+ topsrcdirstrip=`echo "$(top_srcdir)" | sed 's/[].[^$$\\*]/\\\\&/g'`; \
+ list='$(DISTFILES)'; \
+ dist_files=`for file in $$list; do echo $$file; done | \
+ sed -e "s|^$$srcdirstrip/||;t" \
+ -e "s|^$$topsrcdirstrip/|$(top_builddir)/|;t"`; \
+ case $$dist_files in \
+ */*) $(MKDIR_P) `echo "$$dist_files" | \
+ sed '/\//!d;s|^|$(distdir)/|;s,/[^/]*$$,,' | \
+ sort -u` ;; \
+ esac; \
+ for file in $$dist_files; do \
+ if test -f $$file || test -d $$file; then d=.; else d=$(srcdir); fi; \
+ if test -d $$d/$$file; then \
+ dir=`echo "/$$file" | sed -e 's,/[^/]*$$,,'`; \
+ if test -d "$(distdir)/$$file"; then \
+ find "$(distdir)/$$file" -type d ! -perm -700 -exec chmod u+rwx {} \;; \
+ fi; \
+ if test -d $(srcdir)/$$file && test $$d != $(srcdir); then \
+ cp -fpR $(srcdir)/$$file "$(distdir)$$dir" || exit 1; \
+ find "$(distdir)/$$file" -type d ! -perm -700 -exec chmod u+rwx {} \;; \
+ fi; \
+ cp -fpR $$d/$$file "$(distdir)$$dir" || exit 1; \
+ else \
+ test -f "$(distdir)/$$file" \
+ || cp -p $$d/$$file "$(distdir)/$$file" \
+ || exit 1; \
+ fi; \
+ done
+check-am: all-am
+check: check-am
+all-am: Makefile $(LTLIBRARIES) $(HEADERS)
+installdirs:
+ for dir in "$(DESTDIR)$(pkginc_libdir)"; do \
+ test -z "$$dir" || $(MKDIR_P) "$$dir"; \
+ done
+install: install-am
+install-exec: install-exec-am
+install-data: install-data-am
+uninstall: uninstall-am
+
+install-am: all-am
+ @$(MAKE) $(AM_MAKEFLAGS) install-exec-am install-data-am
+
+installcheck: installcheck-am
+install-strip:
+ if test -z '$(STRIP)'; then \
+ $(MAKE) $(AM_MAKEFLAGS) INSTALL_PROGRAM="$(INSTALL_STRIP_PROGRAM)" \
+ install_sh_PROGRAM="$(INSTALL_STRIP_PROGRAM)" INSTALL_STRIP_FLAG=-s \
+ install; \
+ else \
+ $(MAKE) $(AM_MAKEFLAGS) INSTALL_PROGRAM="$(INSTALL_STRIP_PROGRAM)" \
+ install_sh_PROGRAM="$(INSTALL_STRIP_PROGRAM)" INSTALL_STRIP_FLAG=-s \
+ "INSTALL_PROGRAM_ENV=STRIPPROG='$(STRIP)'" install; \
+ fi
+mostlyclean-generic:
+
+clean-generic:
+
+distclean-generic:
+ -test -z "$(CONFIG_CLEAN_FILES)" || rm -f $(CONFIG_CLEAN_FILES)
+ -test . = "$(srcdir)" || test -z "$(CONFIG_CLEAN_VPATH_FILES)" || rm -f $(CONFIG_CLEAN_VPATH_FILES)
+
+maintainer-clean-generic:
+ @echo "This command is intended for maintainers to use"
+ @echo "it deletes files that may require special tools to rebuild."
+clean: clean-am
+
+clean-am: clean-generic clean-libtool clean-noinstLTLIBRARIES \
+ mostlyclean-am
+
+distclean: distclean-am
+ -rm -f ./$(DEPDIR)/imapc-list.Plo
+ -rm -f ./$(DEPDIR)/imapc-mail-fetch.Plo
+ -rm -f ./$(DEPDIR)/imapc-mail.Plo
+ -rm -f ./$(DEPDIR)/imapc-mailbox.Plo
+ -rm -f ./$(DEPDIR)/imapc-save.Plo
+ -rm -f ./$(DEPDIR)/imapc-search.Plo
+ -rm -f ./$(DEPDIR)/imapc-settings.Plo
+ -rm -f ./$(DEPDIR)/imapc-storage.Plo
+ -rm -f ./$(DEPDIR)/imapc-sync.Plo
+ -rm -f Makefile
+distclean-am: clean-am distclean-compile distclean-generic \
+ distclean-tags
+
+dvi: dvi-am
+
+dvi-am:
+
+html: html-am
+
+html-am:
+
+info: info-am
+
+info-am:
+
+install-data-am: install-pkginc_libHEADERS
+
+install-dvi: install-dvi-am
+
+install-dvi-am:
+
+install-exec-am:
+
+install-html: install-html-am
+
+install-html-am:
+
+install-info: install-info-am
+
+install-info-am:
+
+install-man:
+
+install-pdf: install-pdf-am
+
+install-pdf-am:
+
+install-ps: install-ps-am
+
+install-ps-am:
+
+installcheck-am:
+
+maintainer-clean: maintainer-clean-am
+ -rm -f ./$(DEPDIR)/imapc-list.Plo
+ -rm -f ./$(DEPDIR)/imapc-mail-fetch.Plo
+ -rm -f ./$(DEPDIR)/imapc-mail.Plo
+ -rm -f ./$(DEPDIR)/imapc-mailbox.Plo
+ -rm -f ./$(DEPDIR)/imapc-save.Plo
+ -rm -f ./$(DEPDIR)/imapc-search.Plo
+ -rm -f ./$(DEPDIR)/imapc-settings.Plo
+ -rm -f ./$(DEPDIR)/imapc-storage.Plo
+ -rm -f ./$(DEPDIR)/imapc-sync.Plo
+ -rm -f Makefile
+maintainer-clean-am: distclean-am maintainer-clean-generic
+
+mostlyclean: mostlyclean-am
+
+mostlyclean-am: mostlyclean-compile mostlyclean-generic \
+ mostlyclean-libtool
+
+pdf: pdf-am
+
+pdf-am:
+
+ps: ps-am
+
+ps-am:
+
+uninstall-am: uninstall-pkginc_libHEADERS
+
+.MAKE: install-am install-strip
+
+.PHONY: CTAGS GTAGS TAGS all all-am am--depfiles check check-am clean \
+ clean-generic clean-libtool clean-noinstLTLIBRARIES \
+ cscopelist-am ctags ctags-am distclean distclean-compile \
+ distclean-generic distclean-libtool distclean-tags distdir dvi \
+ dvi-am html html-am info info-am install install-am \
+ install-data install-data-am install-dvi install-dvi-am \
+ install-exec install-exec-am install-html install-html-am \
+ install-info install-info-am install-man install-pdf \
+ install-pdf-am install-pkginc_libHEADERS install-ps \
+ install-ps-am install-strip installcheck installcheck-am \
+ installdirs maintainer-clean maintainer-clean-generic \
+ mostlyclean mostlyclean-compile mostlyclean-generic \
+ mostlyclean-libtool pdf pdf-am ps ps-am tags tags-am uninstall \
+ uninstall-am uninstall-pkginc_libHEADERS
+
+.PRECIOUS: Makefile
+
+
+# Tell versions [3.59,3.63) of GNU make to not export all variables.
+# Otherwise a system limit (for SysV at least) may be exceeded.
+.NOEXPORT:
diff --git a/src/lib-storage/index/imapc/imapc-list.c b/src/lib-storage/index/imapc/imapc-list.c
new file mode 100644
index 0000000..a9e03ec
--- /dev/null
+++ b/src/lib-storage/index/imapc/imapc-list.c
@@ -0,0 +1,1011 @@
+/* Copyright (c) 2011-2018 Dovecot authors, see the included COPYING file */
+
+/*
+ There are various different mailbox names here. Here's an example assuming
+ - imapc_list_prefix = "prefix"
+ - remote imapc server separator = '/'
+ - mailbox_list separator = '^' (actually this is currently always the same
+ as remote separator, but this clarifies the example)
+ - namespace separator = ':'
+ - fs_list separator = '.'
+ - mailbox_list storage_name_escape_char = '+'
+ - mailbox_list vname_escape_char = '~'
+ - fs_list storage_name_escape_char = '%'
+
+ remote_name = "prefix/~foo/bar^baz+_%_."
+ storage_name = "prefix^~foo^bar+5ebaz+2b_%_."
+ - separator is changed from / to ^
+ - conflicting ^ separator in remote_name is escaped as +5e
+ - storage_name_escape character + is escaped as +2b
+ vname = "~7efoo:bar.baz+_%_."
+ - imapc_list_prefix is dropped
+ - vname_escape_character ~ is escaped into ~7e
+ - separator is changed from ^ to :
+ - storage_name_escape_characters are unescaped
+ fs_name = "prefix.~foo.bar^baz+_%25_%2e"
+ - this is generated from remote_name
+ - separator is changed from / to .
+ - storage_name_escape_character=% and fs_list separator . are escaped
+*/
+
+#include "lib.h"
+#include "ioloop.h"
+#include "str.h"
+#include "imap-arg.h"
+#include "imap-match.h"
+#include "imap-utf7.h"
+#include "mailbox-tree.h"
+#include "mailbox-list-subscriptions.h"
+#include "imapc-storage.h"
+#include "imapc-list.h"
+
+struct imapc_mailbox_list_iterate_context {
+ struct mailbox_list_iterate_context ctx;
+ struct mailbox_tree_context *tree;
+ struct mailbox_node *ns_root;
+
+ struct mailbox_tree_iterate_context *iter;
+ struct mailbox_info info;
+ string_t *special_use;
+};
+
+static struct {
+ const char *str;
+ enum mailbox_info_flags flag;
+} imap_list_flags[] = {
+ { "\\NoSelect", MAILBOX_NOSELECT },
+ { "\\NonExistent", MAILBOX_NONEXISTENT },
+ { "\\NoInferiors", MAILBOX_NOINFERIORS },
+ { "\\Subscribed", MAILBOX_SUBSCRIBED },
+ { "\\All", MAILBOX_SPECIALUSE_ALL },
+ { "\\Archive", MAILBOX_SPECIALUSE_ARCHIVE },
+ { "\\Drafts", MAILBOX_SPECIALUSE_DRAFTS },
+ { "\\Flagged", MAILBOX_SPECIALUSE_FLAGGED },
+ { "\\Junk", MAILBOX_SPECIALUSE_JUNK },
+ { "\\Sent", MAILBOX_SPECIALUSE_SENT },
+ { "\\Trash", MAILBOX_SPECIALUSE_TRASH },
+ { "\\Important", MAILBOX_SPECIALUSE_IMPORTANT }
+};
+
+extern struct mailbox_list imapc_mailbox_list;
+
+static void imapc_list_send_hierarchy_sep_lookup(struct imapc_mailbox_list *list);
+static void imapc_untagged_list(const struct imapc_untagged_reply *reply,
+ struct imapc_storage_client *client);
+static void imapc_untagged_lsub(const struct imapc_untagged_reply *reply,
+ struct imapc_storage_client *client);
+
+static struct mailbox_list *imapc_list_alloc(void)
+{
+ struct imapc_mailbox_list *list;
+ pool_t pool;
+
+ pool = pool_alloconly_create("imapc mailbox list", 1024);
+ list = p_new(pool, struct imapc_mailbox_list, 1);
+ list->list = imapc_mailbox_list;
+ list->list.pool = pool;
+ /* separator is set lazily */
+ list->mailboxes = mailbox_tree_init('\0');
+ mailbox_tree_set_parents_nonexistent(list->mailboxes);
+ return &list->list;
+}
+
+static int imapc_list_init(struct mailbox_list *_list, const char **error_r)
+{
+ struct imapc_mailbox_list *list = (struct imapc_mailbox_list *)_list;
+
+ list->set = mail_user_set_get_driver_settings(_list->ns->user->set_info,
+ _list->ns->user_set,
+ IMAPC_STORAGE_NAME);
+ if (imapc_storage_client_create(_list->ns, list->set, _list->mail_set,
+ &list->client, error_r) < 0)
+ return -1;
+ list->client->_list = list;
+
+ imapc_storage_client_register_untagged(list->client, "LIST",
+ imapc_untagged_list);
+ imapc_storage_client_register_untagged(list->client, "LSUB",
+ imapc_untagged_lsub);
+ imapc_list_send_hierarchy_sep_lookup(list);
+ return 0;
+}
+
+static void imapc_list_deinit(struct mailbox_list *_list)
+{
+ struct imapc_mailbox_list *list = (struct imapc_mailbox_list *)_list;
+
+ /* make sure all pending commands are aborted before anything is
+ deinitialized */
+ if (list->client != NULL) {
+ list->client->destroying = TRUE;
+ imapc_client_logout(list->client->client);
+ imapc_storage_client_unref(&list->client);
+ }
+ if (list->index_list != NULL)
+ mailbox_list_destroy(&list->index_list);
+ mailbox_tree_deinit(&list->mailboxes);
+ if (list->tmp_subscriptions != NULL)
+ mailbox_tree_deinit(&list->tmp_subscriptions);
+ pool_unref(&list->list.pool);
+}
+
+static void
+imapc_list_copy_error_from_reply(struct imapc_mailbox_list *list,
+ enum mail_error default_error,
+ const struct imapc_command_reply *reply)
+{
+ enum mail_error error;
+
+ if (imapc_resp_text_code_parse(reply->resp_text_key, &error)) {
+ mailbox_list_set_error(&list->list, error,
+ reply->text_without_resp);
+ } else {
+ mailbox_list_set_error(&list->list, default_error,
+ reply->text_without_resp);
+ }
+}
+
+static void imapc_list_simple_callback(const struct imapc_command_reply *reply,
+ void *context)
+{
+ struct imapc_simple_context *ctx = context;
+
+ if (reply->state == IMAPC_COMMAND_STATE_OK)
+ ctx->ret = 0;
+ else if (reply->state == IMAPC_COMMAND_STATE_NO) {
+ imapc_list_copy_error_from_reply(ctx->client->_list,
+ MAIL_ERROR_PARAMS, reply);
+ ctx->ret = -1;
+ } else if (imapc_storage_client_handle_auth_failure(ctx->client)) {
+ ctx->ret = -1;
+ } else if (reply->state == IMAPC_COMMAND_STATE_DISCONNECTED) {
+ mailbox_list_set_internal_error(&ctx->client->_list->list);
+ ctx->ret = -1;
+ } else {
+ mailbox_list_set_critical(&ctx->client->_list->list,
+ "imapc: Command failed: %s", reply->text_full);
+ ctx->ret = -1;
+ }
+ imapc_client_stop(ctx->client->client);
+}
+
+static bool
+imap_list_flag_parse(const char *str, enum mailbox_info_flags *flag_r)
+{
+ unsigned int i;
+
+ for (i = 0; i < N_ELEMENTS(imap_list_flags); i++) {
+ if (strcasecmp(str, imap_list_flags[i].str) == 0) {
+ *flag_r = imap_list_flags[i].flag;
+ return TRUE;
+ }
+ }
+ return FALSE;
+}
+
+static const char *
+imapc_list_remote_to_storage_name(struct imapc_mailbox_list *list,
+ const char *remote_name)
+{
+ /* typically mailbox_list_escape_name() is used to escape vname into
+ a list name. but we want to convert remote IMAP name to a list name,
+ so we need to use the remote IMAP separator. */
+ return mailbox_list_escape_name_params(remote_name, "",
+ list->root_sep,
+ mailbox_list_get_hierarchy_sep(&list->list),
+ list->list.set.storage_name_escape_char, "");
+}
+
+static const char *
+imapc_list_remote_to_vname(struct imapc_mailbox_list *list,
+ const char *remote_name)
+{
+ return mailbox_list_get_vname(&list->list,
+ imapc_list_remote_to_storage_name(list, remote_name));
+}
+
+const char *
+imapc_list_storage_to_remote_name(struct imapc_mailbox_list *list,
+ const char *storage_name)
+{
+ return mailbox_list_unescape_name_params(storage_name, "", list->root_sep,
+ mailbox_list_get_hierarchy_sep(&list->list),
+ list->list.set.storage_name_escape_char);
+}
+
+static struct mailbox_node *
+imapc_list_update_tree(struct imapc_mailbox_list *list,
+ struct mailbox_tree_context *tree,
+ const struct imap_arg *args)
+{
+ struct mailbox_node *node;
+ const struct imap_arg *flags;
+ const char *remote_name, *flag;
+ enum mailbox_info_flags info_flag, info_flags = 0;
+ bool created;
+
+ if (!imap_arg_get_list(&args[0], &flags) ||
+ args[1].type == IMAP_ARG_EOL ||
+ !imap_arg_get_astring(&args[2], &remote_name))
+ return NULL;
+
+ while (imap_arg_get_atom(flags, &flag)) {
+ if (imap_list_flag_parse(flag, &info_flag))
+ info_flags |= info_flag;
+ flags++;
+ }
+
+ T_BEGIN {
+ const char *vname =
+ imapc_list_remote_to_vname(list, remote_name);
+
+ if ((info_flags & MAILBOX_NONEXISTENT) != 0)
+ node = mailbox_tree_lookup(tree, vname);
+ else
+ node = mailbox_tree_get(tree, vname, &created);
+ } T_END;
+ if (node != NULL)
+ node->flags = info_flags;
+ return node;
+}
+
+static void imapc_untagged_list(const struct imapc_untagged_reply *reply,
+ struct imapc_storage_client *client)
+{
+ struct imapc_mailbox_list *list = client->_list;
+ const struct imap_arg *args = reply->args;
+ const char *sep, *remote_name;
+
+ if (list->root_sep == '\0') {
+ /* we haven't asked for the separator yet.
+ lets see if this is the reply for its request. */
+ if (args[0].type == IMAP_ARG_EOL ||
+ !imap_arg_get_nstring(&args[1], &sep) ||
+ !imap_arg_get_astring(&args[2], &remote_name))
+ return;
+
+ /* we can't handle NIL separator yet */
+ list->root_sep = sep == NULL ? '/' : sep[0];
+ mailbox_tree_set_separator(list->mailboxes, list->root_sep);
+ } else {
+ (void)imapc_list_update_tree(list, list->mailboxes, args);
+ }
+}
+
+static void imapc_untagged_lsub(const struct imapc_untagged_reply *reply,
+ struct imapc_storage_client *client)
+{
+ struct imapc_mailbox_list *list = client->_list;
+ const struct imap_arg *args = reply->args;
+ struct mailbox_node *node;
+
+ if (list->root_sep == '\0') {
+ /* we haven't asked for the separator yet */
+ return;
+ }
+ node = imapc_list_update_tree(list, list->tmp_subscriptions != NULL ?
+ list->tmp_subscriptions :
+ list->list.subscriptions, args);
+ if (node != NULL) {
+ if ((node->flags & MAILBOX_NOSELECT) == 0)
+ node->flags |= MAILBOX_SUBSCRIBED;
+ else {
+ /* LSUB \Noselect means that the mailbox isn't
+ subscribed, but it has children that are */
+ node->flags &= ENUM_NEGATE(MAILBOX_NOSELECT);
+ }
+ }
+}
+
+static void imapc_list_sep_verify(struct imapc_mailbox_list *list)
+{
+ const char *imapc_list_prefix = list->set->imapc_list_prefix;
+
+ if (list->root_sep == '\0') {
+ mailbox_list_set_critical(&list->list,
+ "imapc: LIST didn't return hierarchy separator");
+ } else if (imapc_list_prefix[0] != '\0' &&
+ imapc_list_prefix[strlen(imapc_list_prefix)-1] == list->root_sep) {
+ mailbox_list_set_critical(&list->list,
+ "imapc_list_prefix must not end with hierarchy separator");
+ }
+}
+
+static void imapc_storage_sep_callback(const struct imapc_command_reply *reply,
+ void *context)
+{
+ struct imapc_mailbox_list *list = context;
+
+ list->root_sep_pending = FALSE;
+ if (reply->state == IMAPC_COMMAND_STATE_OK)
+ imapc_list_sep_verify(list);
+ else if (reply->state == IMAPC_COMMAND_STATE_NO)
+ imapc_list_copy_error_from_reply(list, MAIL_ERROR_PARAMS, reply);
+ else if (imapc_storage_client_handle_auth_failure(list->client))
+ ;
+ else if (reply->state == IMAPC_COMMAND_STATE_DISCONNECTED)
+ mailbox_list_set_internal_error(&list->list);
+ else if (!list->list.ns->user->deinitializing) {
+ mailbox_list_set_critical(&list->list,
+ "imapc: Command failed: %s", reply->text_full);
+ }
+ imapc_client_stop(list->client->client);
+}
+
+static void imapc_list_send_hierarchy_sep_lookup(struct imapc_mailbox_list *list)
+{
+ struct imapc_command *cmd;
+
+ if (list->root_sep_pending)
+ return;
+ list->root_sep_pending = TRUE;
+
+ cmd = imapc_client_cmd(list->client->client,
+ imapc_storage_sep_callback, list);
+ imapc_command_set_flags(cmd, IMAPC_COMMAND_FLAG_RETRIABLE);
+ imapc_command_send(cmd, "LIST \"\" \"\"");
+}
+
+int imapc_list_try_get_root_sep(struct imapc_mailbox_list *list, char *sep_r)
+{
+ if (list->root_sep == '\0') {
+ if (imapc_storage_client_handle_auth_failure(list->client))
+ return -1;
+ imapc_list_send_hierarchy_sep_lookup(list);
+ while (list->root_sep_pending)
+ imapc_client_run(list->client->client);
+ if (list->root_sep == '\0')
+ return -1;
+ }
+ *sep_r = list->root_sep;
+ return 0;
+}
+
+static char imapc_list_get_hierarchy_sep(struct mailbox_list *_list)
+{
+ struct imapc_mailbox_list *list = (struct imapc_mailbox_list *)_list;
+ char sep;
+
+ if (imapc_list_try_get_root_sep(list, &sep) < 0) {
+ /* we can't really return a failure here. just return a common
+ separator and fail all the future list operations. */
+ return '/';
+ }
+ return sep;
+}
+
+static const char *
+imapc_list_get_storage_name(struct mailbox_list *_list, const char *vname)
+{
+ struct imapc_mailbox_list *list = (struct imapc_mailbox_list *)_list;
+ const char *prefix = list->set->imapc_list_prefix;
+ const char *storage_name;
+
+ storage_name = mailbox_list_default_get_storage_name(_list, vname);
+ if (*prefix != '\0' && strcasecmp(storage_name, "INBOX") != 0) {
+ storage_name = storage_name[0] == '\0' ? prefix :
+ t_strdup_printf("%s%c%s", prefix,
+ mailbox_list_get_hierarchy_sep(_list),
+ storage_name);
+ }
+ return storage_name;
+}
+
+static const char *
+imapc_list_get_vname(struct mailbox_list *_list, const char *storage_name)
+{
+ struct imapc_mailbox_list *list = (struct imapc_mailbox_list *)_list;
+ const char *prefix = list->set->imapc_list_prefix;
+ size_t prefix_len;
+
+ if (*storage_name == '\0') {
+ /* ACL plugin does these lookups */
+ } else if (*prefix != '\0' && strcasecmp(storage_name, "INBOX") != 0) {
+ prefix_len = strlen(prefix);
+ i_assert(str_begins(storage_name, prefix));
+ storage_name += prefix_len;
+ if (storage_name[0] == '\0') {
+ /* we're looking up the prefix itself */
+ } else {
+ i_assert(storage_name[0] ==
+ mailbox_list_get_hierarchy_sep(_list));
+ storage_name++;
+ }
+ }
+ return mailbox_list_default_get_vname(_list, storage_name);
+}
+
+static struct mailbox_list *imapc_list_get_fs(struct imapc_mailbox_list *list)
+{
+ struct mailbox_list_settings list_set;
+ const char *error, *dir;
+
+ dir = list->list.set.index_dir;
+ if (dir == NULL)
+ dir = list->list.set.root_dir;
+
+ if (dir == NULL || dir[0] == '\0') {
+ /* indexes disabled */
+ } else if (list->index_list == NULL && !list->index_list_failed) {
+ mailbox_list_settings_init_defaults(&list_set);
+ list_set.layout = MAILBOX_LIST_NAME_MAILDIRPLUSPLUS;
+ list_set.root_dir = dir;
+ list_set.index_pvt_dir = p_strdup_empty(list->list.pool, list->list.set.index_pvt_dir);
+ /* Filesystem needs to be able to store any kind of a mailbox
+ name. */
+ list_set.storage_name_escape_char =
+ IMAPC_LIST_FS_NAME_ESCAPE_CHAR;
+
+ if (mailbox_list_create(list_set.layout, list->list.ns,
+ &list_set, MAILBOX_LIST_FLAG_SECONDARY,
+ &list->index_list, &error) < 0) {
+ i_error("imapc: Couldn't create %s mailbox list: %s",
+ list_set.layout, error);
+ list->index_list_failed = TRUE;
+ }
+ }
+ return list->index_list;
+}
+
+static const char *
+imapc_list_storage_to_fs_name(struct imapc_mailbox_list *list,
+ const char *storage_name)
+{
+ struct mailbox_list *fs_list = imapc_list_get_fs(list);
+ const char *remote_name;
+
+ if (storage_name == NULL)
+ return NULL;
+
+ remote_name = imapc_list_storage_to_remote_name(list, storage_name);
+ return mailbox_list_escape_name_params(remote_name, "",
+ list->root_sep,
+ mailbox_list_get_hierarchy_sep(fs_list),
+ fs_list->set.storage_name_escape_char, "");
+}
+
+static const char *
+imapc_list_fs_to_storage_name(struct imapc_mailbox_list *list,
+ const char *fs_name)
+{
+ struct mailbox_list *fs_list = imapc_list_get_fs(list);
+ const char *remote_name;
+
+ if (fs_name == NULL)
+ return NULL;
+
+ remote_name = mailbox_list_unescape_name_params(fs_name, "",
+ list->root_sep,
+ mailbox_list_get_hierarchy_sep(fs_list),
+ fs_list->set.storage_name_escape_char);
+ return imapc_list_remote_to_storage_name(list, remote_name);
+}
+
+static int
+imapc_list_get_path(struct mailbox_list *_list, const char *name,
+ enum mailbox_list_path_type type, const char **path_r)
+{
+ struct imapc_mailbox_list *list = (struct imapc_mailbox_list *)_list;
+ struct mailbox_list *fs_list = imapc_list_get_fs(list);
+ const char *fs_name;
+
+ if (fs_list != NULL) {
+ fs_name = imapc_list_storage_to_fs_name(list, name);
+ return mailbox_list_get_path(fs_list, fs_name, type, path_r);
+ } else {
+ *path_r = NULL;
+ return 0;
+ }
+}
+
+static const char *
+imapc_list_get_temp_prefix(struct mailbox_list *_list, bool global)
+{
+ struct imapc_mailbox_list *list = (struct imapc_mailbox_list *)_list;
+ struct mailbox_list *fs_list = imapc_list_get_fs(list);
+
+ if (fs_list != NULL) {
+ return global ?
+ mailbox_list_get_global_temp_prefix(fs_list) :
+ mailbox_list_get_temp_prefix(fs_list);
+ } else {
+ i_panic("imapc: Can't return a temp prefix for '%s'",
+ _list->ns->prefix);
+ }
+}
+
+static const char *
+imapc_list_join_refpattern(struct mailbox_list *list ATTR_UNUSED,
+ const char *ref, const char *pattern)
+{
+ return t_strconcat(ref, pattern, NULL);
+}
+
+static struct imapc_command *
+imapc_list_simple_context_init(struct imapc_simple_context *ctx,
+ struct imapc_mailbox_list *list)
+{
+ imapc_simple_context_init(ctx, list->client);
+ return imapc_client_cmd(list->client->client,
+ imapc_list_simple_callback, ctx);
+}
+
+static void imapc_list_delete_unused_indexes(struct imapc_mailbox_list *list)
+{
+ struct mailbox_list *fs_list = imapc_list_get_fs(list);
+ struct mailbox_list_iterate_context *iter;
+ const struct mailbox_info *info;
+ const char *fs_name, *storage_name, *vname;
+
+ if (fs_list == NULL)
+ return;
+
+ iter = mailbox_list_iter_init(fs_list, "*",
+ MAILBOX_LIST_ITER_RAW_LIST |
+ MAILBOX_LIST_ITER_NO_AUTO_BOXES |
+ MAILBOX_LIST_ITER_RETURN_NO_FLAGS);
+ while ((info = mailbox_list_iter_next(iter)) != NULL) T_BEGIN {
+ fs_name = mailbox_list_get_storage_name(fs_list, info->vname);
+ storage_name = imapc_list_fs_to_storage_name(list, fs_name);
+ vname = mailbox_list_get_vname(&list->list, storage_name);
+
+ /* list->mailboxes contains proper vnames. fs_vname */
+ if (mailbox_tree_lookup(list->mailboxes, vname) == NULL)
+ (void)fs_list->v.delete_mailbox(fs_list, fs_name);
+ } T_END;
+ (void)mailbox_list_iter_deinit(&iter);
+}
+
+static int imapc_list_refresh(struct imapc_mailbox_list *list)
+{
+ struct imapc_command *cmd;
+ struct imapc_simple_context ctx;
+ struct mailbox_node *node;
+ const char *pattern;
+ char sep;
+
+ if (imapc_list_try_get_root_sep(list, &sep) < 0)
+ return -1;
+ if (list->refreshed_mailboxes)
+ return 0;
+
+ if (*list->set->imapc_list_prefix == '\0')
+ pattern = "*";
+ else {
+ /* list "prefix*" instead of "prefix.*". this may return a bit
+ more than we want, but we're also interested in the flags
+ of the prefix itself. */
+ pattern = t_strdup_printf("%s*", list->set->imapc_list_prefix);
+ }
+
+ cmd = imapc_list_simple_context_init(&ctx, list);
+ imapc_command_set_flags(cmd, IMAPC_COMMAND_FLAG_RETRIABLE);
+ imapc_command_sendf(cmd, "LIST \"\" %s", pattern);
+ mailbox_tree_deinit(&list->mailboxes);
+ list->mailboxes = mailbox_tree_init(mail_namespace_get_sep(list->list.ns));
+ mailbox_tree_set_parents_nonexistent(list->mailboxes);
+ imapc_simple_run(&ctx, &cmd);
+
+ if ((list->list.ns->flags & NAMESPACE_FLAG_INBOX_USER) != 0) {
+ /* INBOX always exists in IMAP server. since this namespace is
+ marked with inbox=yes, show the INBOX even if
+ imapc_list_prefix doesn't match it */
+ bool created;
+ node = mailbox_tree_get(list->mailboxes, "INBOX", &created);
+ if (*list->set->imapc_list_prefix != '\0') {
+ /* this listing didn't include the INBOX itself, but
+ might have included its children. make sure there
+ aren't any extra flags in it (especially
+ \NonExistent) */
+ node->flags &= MAILBOX_CHILDREN;
+ }
+ }
+
+ if (ctx.ret == 0) {
+ list->refreshed_mailboxes = TRUE;
+ list->refreshed_mailboxes_recently = TRUE;
+ list->last_refreshed_mailboxes = ioloop_time;
+ imapc_list_delete_unused_indexes(list);
+ }
+ return ctx.ret;
+}
+
+static void
+imapc_list_build_match_tree(struct imapc_mailbox_list_iterate_context *ctx)
+{
+ struct imapc_mailbox_list *list =
+ (struct imapc_mailbox_list *)ctx->ctx.list;
+ struct mailbox_list_iter_update_context update_ctx;
+ struct mailbox_tree_iterate_context *iter;
+ struct mailbox_node *node;
+ const char *vname;
+
+ i_zero(&update_ctx);
+ update_ctx.iter_ctx = &ctx->ctx;
+ update_ctx.tree_ctx = ctx->tree;
+ update_ctx.glob = ctx->ctx.glob;
+ update_ctx.match_parents = TRUE;
+
+ iter = mailbox_tree_iterate_init(list->mailboxes, NULL, 0);
+ while ((node = mailbox_tree_iterate_next(iter, &vname)) != NULL) {
+ update_ctx.leaf_flags = node->flags;
+ mailbox_list_iter_update(&update_ctx, vname);
+ }
+ mailbox_tree_iterate_deinit(&iter);
+}
+
+static struct mailbox_list_iterate_context *
+imapc_list_iter_init(struct mailbox_list *_list, const char *const *patterns,
+ enum mailbox_list_iter_flags flags)
+{
+ struct imapc_mailbox_list *list = (struct imapc_mailbox_list *)_list;
+ struct mailbox_list_iterate_context *_ctx;
+ struct imapc_mailbox_list_iterate_context *ctx;
+ pool_t pool;
+ const char *ns_root_name;
+ char ns_sep;
+ int ret = 0;
+
+ if ((flags & MAILBOX_LIST_ITER_SELECT_SUBSCRIBED) == 0 ||
+ (flags & MAILBOX_LIST_ITER_RETURN_NO_FLAGS) == 0)
+ ret = imapc_list_refresh(list);
+
+ list->iter_count++;
+
+ if ((flags & MAILBOX_LIST_ITER_SELECT_SUBSCRIBED) != 0) {
+ /* we're listing only subscriptions. just use the cached
+ subscriptions list. */
+ _ctx = mailbox_list_subscriptions_iter_init(_list, patterns,
+ flags);
+ if (ret < 0)
+ _ctx->failed = TRUE;
+ return _ctx;
+ }
+
+ /* if we've already failed, make sure we don't call
+ mailbox_list_get_hierarchy_sep(), since it clears the error */
+ ns_sep = ret < 0 ? '/' : mail_namespace_get_sep(_list->ns);
+
+ pool = pool_alloconly_create("mailbox list imapc iter", 1024);
+ ctx = p_new(pool, struct imapc_mailbox_list_iterate_context, 1);
+ ctx->ctx.pool = pool;
+ ctx->ctx.list = _list;
+ ctx->ctx.flags = flags;
+ ctx->ctx.glob = imap_match_init_multiple(pool, patterns, FALSE, ns_sep);
+ array_create(&ctx->ctx.module_contexts, pool, sizeof(void *), 5);
+
+ ctx->info.ns = _list->ns;
+
+ ctx->tree = mailbox_tree_init(ns_sep);
+ mailbox_tree_set_parents_nonexistent(ctx->tree);
+ if (ret == 0)
+ imapc_list_build_match_tree(ctx);
+
+ if (list->list.ns->prefix_len > 0) {
+ ns_root_name = t_strndup(_list->ns->prefix,
+ _list->ns->prefix_len - 1);
+ ctx->ns_root = mailbox_tree_lookup(ctx->tree, ns_root_name);
+ }
+
+ ctx->iter = mailbox_tree_iterate_init(ctx->tree, NULL, 0);
+ if (ret < 0)
+ ctx->ctx.failed = TRUE;
+ return &ctx->ctx;
+}
+
+static void
+imapc_list_write_special_use(struct imapc_mailbox_list_iterate_context *ctx,
+ struct mailbox_node *node)
+{
+ unsigned int i;
+
+ if (ctx->special_use == NULL)
+ ctx->special_use = str_new(ctx->ctx.pool, 64);
+ str_truncate(ctx->special_use, 0);
+
+ for (i = 0; i < N_ELEMENTS(imap_list_flags); i++) {
+ if ((node->flags & imap_list_flags[i].flag) != 0 &&
+ (node->flags & MAILBOX_SPECIALUSE_MASK) != 0) {
+ str_append(ctx->special_use, imap_list_flags[i].str);
+ str_append_c(ctx->special_use, ' ');
+ }
+ }
+
+ if (str_len(ctx->special_use) > 0) {
+ str_truncate(ctx->special_use, str_len(ctx->special_use) - 1);
+ ctx->info.special_use = str_c(ctx->special_use);
+ } else {
+ ctx->info.special_use = NULL;
+ }
+}
+
+static bool
+imapc_list_is_ns_root(struct imapc_mailbox_list_iterate_context *ctx,
+ struct mailbox_node *node)
+{
+ struct mailbox_node *root_node = ctx->ns_root;
+
+ while (root_node != NULL) {
+ if (node == root_node)
+ return TRUE;
+ root_node = root_node->parent;
+ }
+ return FALSE;
+}
+
+static const struct mailbox_info *
+imapc_list_iter_next(struct mailbox_list_iterate_context *_ctx)
+{
+ struct imapc_mailbox_list_iterate_context *ctx =
+ (struct imapc_mailbox_list_iterate_context *)_ctx;
+ struct imapc_mailbox_list *list =
+ (struct imapc_mailbox_list *)_ctx->list;
+ struct mailbox_node *node;
+ const char *vname;
+
+ if (_ctx->failed)
+ return NULL;
+
+ if ((_ctx->flags & MAILBOX_LIST_ITER_SELECT_SUBSCRIBED) != 0)
+ return mailbox_list_subscriptions_iter_next(_ctx);
+
+ do {
+ node = mailbox_tree_iterate_next(ctx->iter, &vname);
+ if (node == NULL)
+ return mailbox_list_iter_default_next(_ctx);
+ } while ((node->flags & MAILBOX_MATCHED) == 0 ||
+ imapc_list_is_ns_root(ctx, node));
+
+ if (ctx->info.ns->prefix_len > 0 &&
+ strcasecmp(vname, "INBOX") != 0 &&
+ strncmp(vname, ctx->info.ns->prefix, ctx->info.ns->prefix_len-1) == 0 &&
+ vname[ctx->info.ns->prefix_len] == '\0' &&
+ list->set->imapc_list_prefix[0] == '\0') {
+ /* don't return "" name */
+ return imapc_list_iter_next(_ctx);
+ }
+
+ ctx->info.vname = vname;
+ ctx->info.flags = node->flags;
+ if ((_ctx->list->ns->flags & NAMESPACE_FLAG_INBOX_USER) != 0) {
+ /* we're iterating the INBOX namespace. pass through the
+ SPECIAL-USE flags if they exist. */
+ imapc_list_write_special_use(ctx, node);
+ } else {
+ ctx->info.special_use = NULL;
+ }
+ return &ctx->info;
+}
+
+static int imapc_list_iter_deinit(struct mailbox_list_iterate_context *_ctx)
+{
+ struct imapc_mailbox_list_iterate_context *ctx =
+ (struct imapc_mailbox_list_iterate_context *)_ctx;
+ struct imapc_mailbox_list *list =
+ (struct imapc_mailbox_list *)_ctx->list;
+ int ret = _ctx->failed ? -1 : 0;
+
+ i_assert(list->iter_count > 0);
+
+ if (--list->iter_count == 0) {
+ list->refreshed_mailboxes = FALSE;
+ list->refreshed_subscriptions = FALSE;
+ }
+
+ if ((_ctx->flags & MAILBOX_LIST_ITER_SELECT_SUBSCRIBED) != 0)
+ return mailbox_list_subscriptions_iter_deinit(_ctx);
+
+ mailbox_tree_iterate_deinit(&ctx->iter);
+ mailbox_tree_deinit(&ctx->tree);
+ pool_unref(&_ctx->pool);
+ return ret;
+}
+
+static int
+imapc_list_subscriptions_refresh(struct mailbox_list *_src_list,
+ struct mailbox_list *dest_list)
+{
+ struct imapc_mailbox_list *src_list =
+ (struct imapc_mailbox_list *)_src_list;
+ struct imapc_simple_context ctx;
+ struct imapc_command *cmd;
+ const char *pattern;
+ char list_sep, dest_sep = mail_namespace_get_sep(dest_list->ns);
+
+ i_assert(src_list->tmp_subscriptions == NULL);
+
+ if (imapc_list_try_get_root_sep(src_list, &list_sep) < 0)
+ return -1;
+
+ if (src_list->refreshed_subscriptions) {
+ if (dest_list->subscriptions == NULL)
+ dest_list->subscriptions = mailbox_tree_init(dest_sep);
+ return 0;
+ }
+
+ src_list->tmp_subscriptions =
+ mailbox_tree_init(mail_namespace_get_sep(_src_list->ns));
+
+ cmd = imapc_list_simple_context_init(&ctx, src_list);
+ if (*src_list->set->imapc_list_prefix == '\0')
+ pattern = "*";
+ else
+ pattern = t_strdup_printf("%s*", src_list->set->imapc_list_prefix);
+ imapc_command_set_flags(cmd, IMAPC_COMMAND_FLAG_RETRIABLE);
+ imapc_command_sendf(cmd, "LSUB \"\" %s", pattern);
+ imapc_simple_run(&ctx, &cmd);
+
+ if (ctx.ret < 0)
+ return -1;
+
+ /* replace subscriptions tree in destination */
+ if (dest_list->subscriptions != NULL)
+ mailbox_tree_deinit(&dest_list->subscriptions);
+ dest_list->subscriptions = src_list->tmp_subscriptions;
+ src_list->tmp_subscriptions = NULL;
+ mailbox_tree_set_separator(dest_list->subscriptions, dest_sep);
+
+ src_list->refreshed_subscriptions = TRUE;
+ return 0;
+}
+
+static int imapc_list_set_subscribed(struct mailbox_list *_list,
+ const char *name, bool set)
+{
+ struct imapc_mailbox_list *list = (struct imapc_mailbox_list *)_list;
+ struct imapc_command *cmd;
+ struct imapc_simple_context ctx;
+
+ cmd = imapc_list_simple_context_init(&ctx, list);
+ imapc_command_set_flags(cmd, IMAPC_COMMAND_FLAG_RETRIABLE);
+ imapc_command_sendf(cmd, set ? "SUBSCRIBE %s" : "UNSUBSCRIBE %s",
+ imapc_list_storage_to_remote_name(list, name));
+ imapc_simple_run(&ctx, &cmd);
+ return ctx.ret;
+}
+
+static int
+imapc_list_delete_mailbox(struct mailbox_list *_list, const char *name)
+{
+ struct imapc_mailbox_list *list = (struct imapc_mailbox_list *)_list;
+ struct mailbox_list *fs_list = imapc_list_get_fs(list);
+ enum imapc_capability capa;
+ struct imapc_command *cmd;
+ struct imapc_simple_context ctx;
+
+ if (imapc_storage_client_handle_auth_failure(list->client))
+ return -1;
+ if (imapc_client_get_capabilities(list->client->client, &capa) < 0)
+ return -1;
+
+ cmd = imapc_list_simple_context_init(&ctx, list);
+ imapc_command_set_flags(cmd, IMAPC_COMMAND_FLAG_RETRIABLE);
+ if (!imapc_command_connection_is_selected(cmd))
+ imapc_command_abort(&cmd);
+ else {
+ imapc_command_set_flags(cmd, IMAPC_COMMAND_FLAG_SELECT);
+ if ((capa & IMAPC_CAPABILITY_UNSELECT) != 0)
+ imapc_command_sendf(cmd, "UNSELECT");
+ else
+ imapc_command_sendf(cmd, "SELECT \"~~~\"");
+ imapc_simple_run(&ctx, &cmd);
+ }
+
+ cmd = imapc_list_simple_context_init(&ctx, list);
+ imapc_command_set_flags(cmd, IMAPC_COMMAND_FLAG_RETRIABLE);
+ imapc_command_sendf(cmd, "DELETE %s", imapc_list_storage_to_remote_name(list, name));
+ imapc_simple_run(&ctx, &cmd);
+
+ if (fs_list != NULL && ctx.ret == 0) {
+ const char *fs_name = imapc_list_storage_to_fs_name(list, name);
+ (void)fs_list->v.delete_mailbox(fs_list, fs_name);
+ }
+ return ctx.ret;
+}
+
+static int
+imapc_list_delete_dir(struct mailbox_list *_list, const char *name)
+{
+ struct imapc_mailbox_list *list = (struct imapc_mailbox_list *)_list;
+ struct mailbox_list *fs_list = imapc_list_get_fs(list);
+
+ if (fs_list != NULL) {
+ const char *fs_name = imapc_list_storage_to_fs_name(list, name);
+ (void)mailbox_list_delete_dir(fs_list, fs_name);
+ }
+ return 0;
+}
+
+static int
+imapc_list_delete_symlink(struct mailbox_list *list,
+ const char *name ATTR_UNUSED)
+{
+ mailbox_list_set_error(list, MAIL_ERROR_NOTPOSSIBLE, "Not supported");
+ return -1;
+}
+
+static int
+imapc_list_rename_mailbox(struct mailbox_list *oldlist, const char *oldname,
+ struct mailbox_list *newlist, const char *newname)
+{
+ struct imapc_mailbox_list *list = (struct imapc_mailbox_list *)oldlist;
+ struct mailbox_list *fs_list = imapc_list_get_fs(list);
+ struct imapc_command *cmd;
+ struct imapc_simple_context ctx;
+
+ if (oldlist != newlist) {
+ mailbox_list_set_error(oldlist, MAIL_ERROR_NOTPOSSIBLE,
+ "Can't rename mailboxes across storages.");
+ return -1;
+ }
+
+ cmd = imapc_list_simple_context_init(&ctx, list);
+ imapc_command_sendf(cmd, "RENAME %s %s",
+ imapc_list_storage_to_remote_name(list, oldname),
+ imapc_list_storage_to_remote_name(list, newname));
+ imapc_simple_run(&ctx, &cmd);
+ if (ctx.ret == 0 && fs_list != NULL && oldlist == newlist) {
+ const char *old_fs_name =
+ imapc_list_storage_to_fs_name(list, oldname);
+ const char *new_fs_name =
+ imapc_list_storage_to_fs_name(list, newname);
+ (void)fs_list->v.rename_mailbox(fs_list, old_fs_name,
+ fs_list, new_fs_name);
+ }
+ return ctx.ret;
+}
+
+int imapc_list_get_mailbox_flags(struct mailbox_list *_list, const char *name,
+ enum mailbox_info_flags *flags_r)
+{
+ struct imapc_mailbox_list *list = (struct imapc_mailbox_list *)_list;
+ struct mailbox_node *node;
+ const char *vname;
+
+ vname = mailbox_list_get_vname(_list, name);
+ if (!list->refreshed_mailboxes_recently) {
+ if (imapc_list_refresh(list) < 0)
+ return -1;
+ i_assert(list->refreshed_mailboxes_recently);
+ }
+
+ if (list->mailboxes == NULL) {
+ /* imapc list isn't used, but e.g. LAYOUT=none */
+ *flags_r = 0;
+ return 0;
+ }
+ node = mailbox_tree_lookup(list->mailboxes, vname);
+ if (node == NULL)
+ *flags_r = MAILBOX_NONEXISTENT;
+ else
+ *flags_r = node->flags;
+ return 0;
+}
+
+struct mailbox_list imapc_mailbox_list = {
+ .name = MAILBOX_LIST_NAME_IMAPC,
+ .props = MAILBOX_LIST_PROP_NO_ROOT | MAILBOX_LIST_PROP_AUTOCREATE_DIRS |
+ MAILBOX_LIST_PROP_NO_LIST_INDEX,
+ .mailbox_name_max_length = MAILBOX_LIST_NAME_MAX_LENGTH,
+
+ .v = {
+ .alloc = imapc_list_alloc,
+ .init = imapc_list_init,
+ .deinit = imapc_list_deinit,
+ .get_hierarchy_sep = imapc_list_get_hierarchy_sep,
+ .get_vname = imapc_list_get_vname,
+ .get_storage_name = imapc_list_get_storage_name,
+ .get_path = imapc_list_get_path,
+ .get_temp_prefix = imapc_list_get_temp_prefix,
+ .join_refpattern = imapc_list_join_refpattern,
+ .iter_init = imapc_list_iter_init,
+ .iter_next = imapc_list_iter_next,
+ .iter_deinit = imapc_list_iter_deinit,
+ .subscriptions_refresh = imapc_list_subscriptions_refresh,
+ .set_subscribed = imapc_list_set_subscribed,
+ .delete_mailbox = imapc_list_delete_mailbox,
+ .delete_dir = imapc_list_delete_dir,
+ .delete_symlink = imapc_list_delete_symlink,
+ .rename_mailbox = imapc_list_rename_mailbox,
+ }
+};
diff --git a/src/lib-storage/index/imapc/imapc-list.h b/src/lib-storage/index/imapc/imapc-list.h
new file mode 100644
index 0000000..11df6b0
--- /dev/null
+++ b/src/lib-storage/index/imapc/imapc-list.h
@@ -0,0 +1,41 @@
+#ifndef IMAPC_LIST_H
+#define IMAPC_LIST_H
+
+struct imap_arg;
+
+#include "mailbox-list-private.h"
+
+#define MAILBOX_LIST_NAME_IMAPC "imapc"
+
+struct imapc_mailbox_list {
+ struct mailbox_list list;
+ const struct imapc_settings *set;
+ struct imapc_storage_client *client;
+ struct mailbox_list *index_list;
+
+ /* mailboxes are stored as vnames */
+ struct mailbox_tree_context *mailboxes, *tmp_subscriptions;
+ char root_sep;
+ time_t last_refreshed_mailboxes;
+
+ unsigned int iter_count;
+
+ /* mailboxes/subscriptions are fully refreshed only during
+ mailbox list iteration. */
+ bool refreshed_subscriptions:1;
+ bool refreshed_mailboxes:1;
+ /* mailbox list's "recently refreshed" state is reset by syncing a
+ mailbox. mainly we use this to cache mailboxes' existence to avoid
+ issuing a LIST command every time. */
+ bool refreshed_mailboxes_recently:1;
+ bool index_list_failed:1;
+ bool root_sep_pending:1;
+};
+
+int imapc_list_get_mailbox_flags(struct mailbox_list *list, const char *name,
+ enum mailbox_info_flags *flags_r);
+int imapc_list_try_get_root_sep(struct imapc_mailbox_list *list, char *sep_r);
+const char *imapc_list_storage_to_remote_name(struct imapc_mailbox_list *list,
+ const char *storage_name);
+
+#endif
diff --git a/src/lib-storage/index/imapc/imapc-mail-fetch.c b/src/lib-storage/index/imapc/imapc-mail-fetch.c
new file mode 100644
index 0000000..1b0eee7
--- /dev/null
+++ b/src/lib-storage/index/imapc/imapc-mail-fetch.c
@@ -0,0 +1,911 @@
+/* Copyright (c) 2011-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "str.h"
+#include "ioloop.h"
+#include "istream.h"
+#include "istream-concat.h"
+#include "istream-header-filter.h"
+#include "message-header-parser.h"
+#include "imap-arg.h"
+#include "imap-util.h"
+#include "imap-date.h"
+#include "imap-quote.h"
+#include "imap-bodystructure.h"
+#include "imap-resp-code.h"
+#include "imapc-mail.h"
+#include "imapc-storage.h"
+
+static void imapc_mail_set_failure(struct imapc_mail *mail,
+ const struct imapc_command_reply *reply)
+{
+ struct imapc_mailbox *mbox = IMAPC_MAILBOX(mail->imail.mail.mail.box);
+
+ mail->last_fetch_reply = p_strdup(mail->imail.mail.pool, reply->text_full);
+
+ switch (reply->state) {
+ case IMAPC_COMMAND_STATE_OK:
+ break;
+ case IMAPC_COMMAND_STATE_NO:
+ if (!IMAPC_BOX_HAS_FEATURE(mbox, IMAPC_FEATURE_FETCH_FIX_BROKEN_MAILS)) {
+ /* fetch-fix-broken-mails feature disabled -
+ fail any mails with missing replies */
+ break;
+ }
+ if (reply->resp_text_key != NULL &&
+ (strcasecmp(reply->resp_text_key, IMAP_RESP_CODE_SERVERBUG) == 0 ||
+ strcasecmp(reply->resp_text_key, IMAP_RESP_CODE_LIMIT) == 0)) {
+ /* this is a temporary error, retrying should work.
+ Yahoo sends * BYE +
+ NO [LIMIT] UID FETCH Rate limit hit. */
+ } else {
+ /* hopefully this is a permanent failure */
+ mail->fetch_ignore_if_missing = TRUE;
+ }
+ break;
+ case IMAPC_COMMAND_STATE_BAD:
+ case IMAPC_COMMAND_STATE_DISCONNECTED:
+ case IMAPC_COMMAND_STATE_AUTH_FAILED:
+ mail->fetch_failed = TRUE;
+ break;
+ }
+}
+
+static void
+imapc_mail_fetch_callback(const struct imapc_command_reply *reply,
+ void *context)
+{
+ struct imapc_fetch_request *request = context;
+ struct imapc_fetch_request *const *requests;
+ struct imapc_mail *mail;
+ struct imapc_mailbox *mbox = NULL;
+ unsigned int i, count;
+
+ array_foreach_elem(&request->mails, mail) {
+ i_assert(mail->fetch_count > 0);
+ imapc_mail_set_failure(mail, reply);
+ if (--mail->fetch_count == 0)
+ mail->fetching_fields = 0;
+ mbox = IMAPC_MAILBOX(mail->imail.mail.mail.box);
+ }
+ i_assert(mbox != NULL);
+
+ requests = array_get(&mbox->fetch_requests, &count);
+ for (i = 0; i < count; i++) {
+ if (requests[i] == request) {
+ array_delete(&mbox->fetch_requests, i, 1);
+ break;
+ }
+ }
+ i_assert(i < count);
+
+ array_free(&request->mails);
+ i_free(request);
+
+ if (reply->state == IMAPC_COMMAND_STATE_OK)
+ ;
+ else if (reply->state == IMAPC_COMMAND_STATE_NO) {
+ imapc_copy_error_from_reply(mbox->storage, MAIL_ERROR_PARAMS,
+ reply);
+ } else if (reply->state == IMAPC_COMMAND_STATE_DISCONNECTED) {
+ /* The disconnection message was already logged */
+ mail_storage_set_internal_error(&mbox->storage->storage);
+ } else {
+ mailbox_set_critical(&mbox->box,
+ "imapc: Mail FETCH failed: %s", reply->text_full);
+ }
+ imapc_client_stop(mbox->storage->client->client);
+}
+
+static bool
+headers_have_subset(const char *const *superset, const char *const *subset)
+{
+ unsigned int i;
+
+ if (superset == NULL)
+ return FALSE;
+ if (subset != NULL) {
+ for (i = 0; subset[i] != NULL; i++) {
+ if (!str_array_icase_find(superset, subset[i]))
+ return FALSE;
+ }
+ }
+ return TRUE;
+}
+
+static const char *const *
+headers_merge(pool_t pool, const char *const *h1, const char *const *h2)
+{
+ ARRAY_TYPE(const_string) headers;
+ const char *value;
+ unsigned int i;
+
+ p_array_init(&headers, pool, 16);
+ if (h1 != NULL) {
+ for (i = 0; h1[i] != NULL; i++) {
+ value = p_strdup(pool, h1[i]);
+ array_push_back(&headers, &value);
+ }
+ }
+ if (h2 != NULL) {
+ for (i = 0; h2[i] != NULL; i++) {
+ if (h1 == NULL || !str_array_icase_find(h1, h2[i])) {
+ value = p_strdup(pool, h2[i]);
+ array_push_back(&headers, &value);
+ }
+ }
+ }
+ array_append_zero(&headers);
+ return array_front(&headers);
+}
+
+static bool
+imapc_mail_try_merge_fetch(struct imapc_mailbox *mbox, string_t *str)
+{
+ const char *s1 = str_c(str);
+ const char *s2 = str_c(mbox->pending_fetch_cmd);
+ const char *p1, *p2;
+
+ i_assert(str_begins(s1, "UID FETCH "));
+ i_assert(str_begins(s2, "UID FETCH "));
+
+ /* skip over UID range */
+ p1 = strchr(s1+10, ' ');
+ p2 = strchr(s2+10, ' ');
+
+ if (null_strcmp(p1, p2) != 0)
+ return FALSE;
+ /* append the new UID to the pending FETCH UID range */
+ str_truncate(str, p1-s1);
+ str_insert(mbox->pending_fetch_cmd, p2-s2, ",");
+ str_insert(mbox->pending_fetch_cmd, p2-s2+1, str_c(str) + 10);
+ return TRUE;
+}
+
+static void
+imapc_mail_delayed_send_or_merge(struct imapc_mail *mail, string_t *str)
+{
+ struct imapc_mailbox *mbox = IMAPC_MAILBOX(mail->imail.mail.mail.box);
+
+ if (mbox->pending_fetch_request != NULL &&
+ !imapc_mail_try_merge_fetch(mbox, str)) {
+ /* send the previous FETCH and create a new one */
+ imapc_mail_fetch_flush(mbox);
+ }
+ if (mbox->pending_fetch_request == NULL) {
+ mbox->pending_fetch_request =
+ i_new(struct imapc_fetch_request, 1);
+ i_array_init(&mbox->pending_fetch_request->mails, 4);
+ i_assert(mbox->pending_fetch_cmd->used == 0);
+ str_append_str(mbox->pending_fetch_cmd, str);
+ }
+ array_push_back(&mbox->pending_fetch_request->mails, &mail);
+
+ if (mbox->to_pending_fetch_send == NULL &&
+ array_count(&mbox->pending_fetch_request->mails) >
+ mbox->box.storage->set->mail_prefetch_count) {
+ /* we're now prefetching the maximum number of mails. this
+ most likely means that we need to flush out the command now
+ before sending anything else. delay it a little bit though
+ in case the sending code doesn't actually use
+ mail_prefetch_count and wants to fetch more.
+
+ note that we don't want to add this timeout too early,
+ because we want to optimize the maximum number of messages
+ placed into a single FETCH. even without timeout the command
+ gets flushed by imapc_mail_fetch() call. */
+ mbox->to_pending_fetch_send =
+ timeout_add_short(0, imapc_mail_fetch_flush, mbox);
+ }
+}
+
+static int
+imapc_mail_send_fetch(struct mail *_mail, enum mail_fetch_field fields,
+ const char *const *headers)
+{
+ struct imapc_mail *mail = IMAPC_MAIL(_mail);
+ struct imapc_mailbox *mbox = IMAPC_MAILBOX(_mail->box);
+ struct mail_index_view *view;
+ string_t *str;
+ uint32_t seq;
+ unsigned int i;
+
+ i_assert(headers == NULL ||
+ IMAPC_BOX_HAS_FEATURE(mbox, IMAPC_FEATURE_FETCH_HEADERS));
+
+ if (!mbox->selected) {
+ mail_storage_set_error(_mail->box->storage,
+ MAIL_ERROR_NOTPOSSIBLE, "Can't fetch mails before selecting mailbox");
+ return -1;
+ }
+
+ if (!mail_stream_access_start(_mail))
+ return -1;
+
+ /* drop any fields that we may already be fetching currently */
+ fields &= ENUM_NEGATE(mail->fetching_fields);
+ if (headers_have_subset(mail->fetching_headers, headers))
+ headers = NULL;
+ if (fields == 0 && headers == NULL)
+ return mail->fetch_sent ? 0 : 1;
+
+ if (!_mail->saving) {
+ /* if we already know that the mail is expunged,
+ don't try to FETCH it */
+ view = mbox->delayed_sync_view != NULL ?
+ mbox->delayed_sync_view : mbox->box.view;
+ if (!mail_index_lookup_seq(view, _mail->uid, &seq) ||
+ mail_index_is_expunged(view, seq)) {
+ mail_set_expunged(_mail);
+ return -1;
+ }
+ } else if (mbox->client_box == NULL) {
+ /* opened as save-only. we'll need to fetch the mail,
+ so actually SELECT/EXAMINE the mailbox */
+ i_assert(mbox->box.opened);
+
+ if (imapc_mailbox_select(mbox) < 0)
+ return -1;
+ }
+
+ if ((fields & MAIL_FETCH_STREAM_BODY) != 0)
+ fields |= MAIL_FETCH_STREAM_HEADER;
+
+ str = t_str_new(64);
+ str_printfa(str, "UID FETCH %u (", _mail->uid);
+ if ((fields & MAIL_FETCH_RECEIVED_DATE) != 0)
+ str_append(str, "INTERNALDATE ");
+ if ((fields & MAIL_FETCH_SAVE_DATE) != 0) {
+ i_assert(HAS_ALL_BITS(mbox->capabilities,
+ IMAPC_CAPABILITY_SAVEDATE));
+ str_append(str, "SAVEDATE ");
+ }
+ if ((fields & (MAIL_FETCH_PHYSICAL_SIZE | MAIL_FETCH_VIRTUAL_SIZE)) != 0)
+ str_append(str, "RFC822.SIZE ");
+ if ((fields & MAIL_FETCH_GUID) != 0) {
+ str_append(str, mbox->guid_fetch_field_name);
+ str_append_c(str, ' ');
+ }
+ if ((fields & MAIL_FETCH_IMAP_BODY) != 0)
+ str_append(str, "BODY ");
+ if ((fields & MAIL_FETCH_IMAP_BODYSTRUCTURE) != 0)
+ str_append(str, "BODYSTRUCTURE ");
+
+ if ((fields & MAIL_FETCH_STREAM_BODY) != 0) {
+ if (!IMAPC_BOX_HAS_FEATURE(mbox, IMAPC_FEATURE_ZIMBRA_WORKAROUNDS))
+ str_append(str, "BODY.PEEK[] ");
+ else {
+ /* BODY.PEEK[] can return different headers than
+ BODY.PEEK[HEADER] (e.g. invalid 8bit chars replaced
+ with '?' in HEADER) - this violates IMAP protocol
+ and messes up dsync since it sometimes fetches the
+ full body and sometimes only the headers. */
+ str_append(str, "BODY.PEEK[HEADER] BODY.PEEK[TEXT] ");
+ }
+ fields |= MAIL_FETCH_STREAM_HEADER;
+ } else if ((fields & MAIL_FETCH_STREAM_HEADER) != 0)
+ str_append(str, "BODY.PEEK[HEADER] ");
+ else if (headers != NULL) {
+ mail->fetching_headers =
+ headers_merge(mail->imail.mail.data_pool, headers,
+ mail->fetching_headers);
+ str_append(str, "BODY.PEEK[HEADER.FIELDS (");
+ for (i = 0; mail->fetching_headers[i] != NULL; i++) {
+ if (i > 0)
+ str_append_c(str, ' ');
+ imap_append_astring(str, mail->fetching_headers[i]);
+ }
+ str_append(str, ")] ");
+ mail->header_list_fetched = FALSE;
+ }
+ str_truncate(str, str_len(str)-1);
+ str_append_c(str, ')');
+
+ mail->fetching_fields |= fields;
+ mail->fetch_count++;
+ mail->fetch_sent = FALSE;
+ mail->fetch_failed = FALSE;
+
+ imapc_mail_delayed_send_or_merge(mail, str);
+ return 1;
+}
+
+static void imapc_mail_cache_get(struct imapc_mail *mail,
+ struct imapc_mail_cache *cache)
+{
+ if (mail->body_fetched)
+ return;
+
+ if (cache->fd != -1) {
+ mail->fd = cache->fd;
+ mail->imail.data.stream = i_stream_create_fd(mail->fd, 0);
+ cache->fd = -1;
+ } else if (cache->buf != NULL) {
+ mail->body = cache->buf;
+ mail->imail.data.stream =
+ i_stream_create_from_data(mail->body->data,
+ mail->body->used);
+ cache->buf = NULL;
+ } else {
+ return;
+ }
+ mail->header_fetched = TRUE;
+ mail->body_fetched = TRUE;
+ /* The stream was already accessed and now it's cached.
+ It still needs to be set accessed to avoid assert-crash. */
+ mail->imail.mail.mail.mail_stream_accessed = TRUE;
+ imapc_mail_init_stream(mail);
+}
+
+static enum mail_fetch_field
+imapc_mail_get_wanted_fetch_fields(struct imapc_mail *mail)
+{
+ struct imapc_mailbox *mbox = IMAPC_MAILBOX(mail->imail.mail.mail.box);
+ struct index_mail_data *data = &mail->imail.data;
+ enum mail_fetch_field fields = 0;
+
+ if ((data->wanted_fields & MAIL_FETCH_RECEIVED_DATE) != 0 &&
+ data->received_date == (time_t)-1)
+ fields |= MAIL_FETCH_RECEIVED_DATE;
+ if ((data->wanted_fields & MAIL_FETCH_SAVE_DATE) != 0 &&
+ data->save_date == (time_t)-1) {
+ if (HAS_ALL_BITS(mbox->capabilities, IMAPC_CAPABILITY_SAVEDATE))
+ fields |= MAIL_FETCH_SAVE_DATE;
+ else
+ fields |= MAIL_FETCH_RECEIVED_DATE;
+ }
+ if ((data->wanted_fields & (MAIL_FETCH_PHYSICAL_SIZE |
+ MAIL_FETCH_VIRTUAL_SIZE)) != 0 &&
+ data->physical_size == UOFF_T_MAX &&
+ IMAPC_BOX_HAS_FEATURE(mbox, IMAPC_FEATURE_RFC822_SIZE))
+ fields |= MAIL_FETCH_PHYSICAL_SIZE | MAIL_FETCH_VIRTUAL_SIZE;
+ if ((data->wanted_fields & MAIL_FETCH_IMAP_BODY) != 0 &&
+ data->body == NULL &&
+ IMAPC_BOX_HAS_FEATURE(mbox, IMAPC_FEATURE_FETCH_BODYSTRUCTURE))
+ fields |= MAIL_FETCH_IMAP_BODY;
+ if ((data->wanted_fields & MAIL_FETCH_IMAP_BODYSTRUCTURE) != 0 &&
+ data->bodystructure == NULL &&
+ IMAPC_BOX_HAS_FEATURE(mbox, IMAPC_FEATURE_FETCH_BODYSTRUCTURE))
+ fields |= MAIL_FETCH_IMAP_BODYSTRUCTURE;
+ if ((data->wanted_fields & MAIL_FETCH_GUID) != 0 &&
+ data->guid == NULL && mbox->guid_fetch_field_name != NULL)
+ fields |= MAIL_FETCH_GUID;
+
+ if (data->stream == NULL && data->access_part != 0) {
+ if ((data->access_part & (READ_BODY | PARSE_BODY)) != 0)
+ fields |= MAIL_FETCH_STREAM_BODY;
+ fields |= MAIL_FETCH_STREAM_HEADER;
+ }
+ return fields;
+}
+
+void imapc_mail_try_init_stream_from_cache(struct imapc_mail *mail)
+{
+ struct mail *_mail = &mail->imail.mail.mail;
+ struct imapc_mailbox *mbox = IMAPC_MAILBOX(_mail->box);
+
+ if (mbox->prev_mail_cache.uid == _mail->uid)
+ imapc_mail_cache_get(mail, &mbox->prev_mail_cache);
+}
+
+bool imapc_mail_prefetch(struct mail *_mail)
+{
+ struct imapc_mail *mail = IMAPC_MAIL(_mail);
+ struct imapc_mailbox *mbox = IMAPC_MAILBOX(_mail->box);
+ struct index_mail_data *data = &mail->imail.data;
+ enum mail_fetch_field fields;
+ const char *const *headers = NULL;
+
+ /* try to get as much from cache as possible */
+ imapc_mail_update_access_parts(&mail->imail);
+ /* If mail is already cached we can avoid re-FETCHing the mail.
+ However, don't initialize the stream if we don't actually want to
+ access the mail. */
+ if (mail->imail.data.access_part != 0)
+ imapc_mail_try_init_stream_from_cache(mail);
+
+ fields = imapc_mail_get_wanted_fetch_fields(mail);
+ if (data->wanted_headers != NULL && data->stream == NULL &&
+ (fields & MAIL_FETCH_STREAM_HEADER) == 0 &&
+ !imapc_mail_has_headers_in_cache(&mail->imail, data->wanted_headers)) {
+ /* fetch specific headers */
+ if (IMAPC_BOX_HAS_FEATURE(mbox, IMAPC_FEATURE_FETCH_HEADERS))
+ headers = data->wanted_headers->name;
+ else
+ fields |= MAIL_FETCH_STREAM_HEADER;
+ }
+ if (fields != 0 || headers != NULL) T_BEGIN {
+ if (imapc_mail_send_fetch(_mail, fields, headers) > 0)
+ mail->imail.data.prefetch_sent = TRUE;
+ } T_END;
+ return !mail->imail.data.prefetch_sent;
+}
+
+static bool
+imapc_mail_have_fields(struct imapc_mail *imail, enum mail_fetch_field fields)
+{
+ struct imapc_mailbox *mbox = IMAPC_MAILBOX(imail->imail.mail.mail.box);
+
+ if ((fields & MAIL_FETCH_RECEIVED_DATE) != 0) {
+ if (imail->imail.data.received_date == (time_t)-1)
+ return FALSE;
+ fields &= ENUM_NEGATE(MAIL_FETCH_RECEIVED_DATE);
+ }
+ if ((fields & MAIL_FETCH_SAVE_DATE) != 0) {
+ i_assert(HAS_ALL_BITS(mbox->capabilities,
+ IMAPC_CAPABILITY_SAVEDATE));
+ if (imail->imail.data.save_date == (time_t)-1)
+ return FALSE;
+ fields &= ENUM_NEGATE(MAIL_FETCH_SAVE_DATE);
+ }
+ if ((fields & (MAIL_FETCH_PHYSICAL_SIZE | MAIL_FETCH_VIRTUAL_SIZE)) != 0) {
+ if (imail->imail.data.physical_size == UOFF_T_MAX)
+ return FALSE;
+ fields &= ENUM_NEGATE(MAIL_FETCH_PHYSICAL_SIZE | MAIL_FETCH_VIRTUAL_SIZE);
+ }
+ if ((fields & MAIL_FETCH_GUID) != 0) {
+ if (imail->imail.data.guid == NULL)
+ return FALSE;
+ fields &= ENUM_NEGATE(MAIL_FETCH_GUID);
+ }
+ if ((fields & MAIL_FETCH_IMAP_BODY) != 0) {
+ if (imail->imail.data.body == NULL)
+ return FALSE;
+ fields &= ENUM_NEGATE(MAIL_FETCH_IMAP_BODY);
+ }
+ if ((fields & MAIL_FETCH_IMAP_BODYSTRUCTURE) != 0) {
+ if (imail->imail.data.bodystructure == NULL)
+ return FALSE;
+ fields &= ENUM_NEGATE(MAIL_FETCH_IMAP_BODYSTRUCTURE);
+ }
+ if ((fields & (MAIL_FETCH_STREAM_HEADER |
+ MAIL_FETCH_STREAM_BODY)) != 0) {
+ if (imail->imail.data.stream == NULL)
+ return FALSE;
+ fields &= ENUM_NEGATE(MAIL_FETCH_STREAM_HEADER | MAIL_FETCH_STREAM_BODY);
+ }
+ i_assert(fields == 0);
+ return TRUE;
+}
+
+int imapc_mail_fetch(struct mail *_mail, enum mail_fetch_field fields,
+ const char *const *headers)
+{
+ struct imapc_mail *imail = IMAPC_MAIL(_mail);
+ struct imapc_mailbox *mbox = IMAPC_MAILBOX(_mail->box);
+ int ret;
+
+ if ((fields & MAIL_FETCH_GUID) != 0 &&
+ mbox->guid_fetch_field_name == NULL) {
+ mail_storage_set_error(_mail->box->storage,
+ MAIL_ERROR_NOTPOSSIBLE,
+ "Message GUID not available in this server");
+ return -1;
+ }
+ if (_mail->saving) {
+ mail_storage_set_error(_mail->box->storage,
+ MAIL_ERROR_NOTPOSSIBLE,
+ "Attempting to issue FETCH for a mail not yet committed");
+ return -1;
+ }
+
+ fields |= imapc_mail_get_wanted_fetch_fields(imail);
+ T_BEGIN {
+ ret = imapc_mail_send_fetch(_mail, fields, headers);
+ } T_END;
+ if (ret < 0)
+ return -1;
+
+ /* we'll continue waiting until we've got all the fields we wanted,
+ or until all FETCH replies have been received (i.e. some FETCHes
+ failed) */
+ if (ret > 0)
+ imapc_mail_fetch_flush(mbox);
+ while (imail->fetch_count > 0 &&
+ (!imapc_mail_have_fields(imail, fields) ||
+ !imail->header_list_fetched)) {
+ imapc_mailbox_run_nofetch(mbox);
+ }
+ if (imail->fetch_failed) {
+ mail_storage_set_internal_error(&mbox->storage->storage);
+ return -1;
+ }
+ return 0;
+}
+
+void imapc_mail_fetch_flush(struct imapc_mailbox *mbox)
+{
+ struct imapc_command *cmd;
+ struct imapc_mail *mail;
+
+ if (mbox->pending_fetch_request == NULL) {
+ i_assert(mbox->to_pending_fetch_send == NULL);
+ return;
+ }
+
+ array_foreach_elem(&mbox->pending_fetch_request->mails, mail)
+ mail->fetch_sent = TRUE;
+
+ cmd = imapc_client_mailbox_cmd(mbox->client_box,
+ imapc_mail_fetch_callback,
+ mbox->pending_fetch_request);
+ imapc_command_set_flags(cmd, IMAPC_COMMAND_FLAG_RETRIABLE);
+ array_push_back(&mbox->fetch_requests, &mbox->pending_fetch_request);
+
+ imapc_command_send(cmd, str_c(mbox->pending_fetch_cmd));
+
+ mbox->pending_fetch_request = NULL;
+ timeout_remove(&mbox->to_pending_fetch_send);
+ str_truncate(mbox->pending_fetch_cmd, 0);
+}
+
+static bool imapc_find_lfile_arg(const struct imapc_untagged_reply *reply,
+ const struct imap_arg *arg, int *fd_r)
+{
+ const struct imap_arg *list;
+ unsigned int i, count;
+
+ for (i = 0; i < reply->file_args_count; i++) {
+ const struct imapc_arg_file *farg = &reply->file_args[i];
+
+ if (farg->parent_arg == arg->parent &&
+ imap_arg_get_list_full(arg->parent, &list, &count) &&
+ farg->list_idx < count && &list[farg->list_idx] == arg) {
+ *fd_r = farg->fd;
+ return TRUE;
+ }
+ }
+ return FALSE;
+}
+
+static void imapc_stream_filter(struct istream **input)
+{
+ static const char *imapc_hide_headers[] = {
+ /* Added by MS Exchange 2010 when \Flagged flag is set.
+ This violates IMAP guarantee of messages being immutable. */
+ "X-Message-Flag"
+ };
+ struct istream *filter_input;
+
+ filter_input = i_stream_create_header_filter(*input,
+ HEADER_FILTER_EXCLUDE,
+ imapc_hide_headers, N_ELEMENTS(imapc_hide_headers),
+ *null_header_filter_callback, NULL);
+ i_stream_unref(input);
+ *input = filter_input;
+}
+
+void imapc_mail_init_stream(struct imapc_mail *mail)
+{
+ struct index_mail *imail = &mail->imail;
+ struct mail *_mail = &imail->mail.mail;
+ struct imapc_mailbox *mbox = IMAPC_MAILBOX(_mail->box);
+ struct istream *input;
+ uoff_t size;
+ int ret;
+
+ i_stream_set_name(imail->data.stream,
+ t_strdup_printf("imapc mail uid=%u", _mail->uid));
+ index_mail_set_read_buffer_size(_mail, imail->data.stream);
+
+ if (!IMAPC_BOX_HAS_FEATURE(mbox, IMAPC_FEATURE_RFC822_SIZE)) {
+ /* enable filtering only when we're not passing through
+ RFC822.SIZE. otherwise we'll get size mismatches. */
+ imapc_stream_filter(&imail->data.stream);
+ }
+ if (imail->mail.v.istream_opened != NULL) {
+ if (imail->mail.v.istream_opened(_mail,
+ &imail->data.stream) < 0) {
+ index_mail_close_streams(imail);
+ return;
+ }
+ }
+ ret = i_stream_get_size(imail->data.stream, TRUE, &size);
+ if (ret < 0) {
+ index_mail_close_streams(imail);
+ return;
+ }
+ i_assert(ret != 0);
+ /* Once message body is fetched, we can be sure of what its size is.
+ If we had already received RFC822.SIZE, overwrite it here in case
+ it's wrong. Also in more special cases the RFC822.SIZE may be
+ smaller than the fetched message header. In this case change the
+ size as well, otherwise reading via istream-mail will fail. */
+ if (mail->body_fetched || imail->data.physical_size < size) {
+ if (mail->body_fetched) {
+ imail->data.inexact_total_sizes = FALSE;
+ /* Don't trust any existing virtual_size. Also don't
+ set it to size, because there's no guarantees about
+ the content having proper CRLF newlines, especially
+ not if istream_opened() has changed the stream. */
+ imail->data.virtual_size = UOFF_T_MAX;
+ }
+ imail->data.physical_size = size;
+ }
+
+ imail->data.stream_has_only_header = !mail->body_fetched;
+ if (index_mail_init_stream(imail, NULL, NULL, &input) < 0)
+ index_mail_close_streams(imail);
+}
+
+static void
+imapc_fetch_stream(struct imapc_mail *mail,
+ const struct imapc_untagged_reply *reply,
+ const struct imap_arg *arg,
+ bool have_header, bool have_body)
+{
+ struct index_mail *imail = &mail->imail;
+ struct imapc_mailbox *mbox = IMAPC_MAILBOX(imail->mail.mail.box);
+ struct istream *hdr_stream = NULL;
+ const char *value;
+ int fd;
+
+ if (imail->data.stream != NULL) {
+ i_assert(mail->header_fetched);
+ if (mail->body_fetched || !have_body)
+ return;
+ if (have_header) {
+ /* replace the existing stream */
+ } else if (mail->fd == -1) {
+ /* append this body stream to the existing
+ header stream */
+ hdr_stream = imail->data.stream;
+ i_stream_ref(hdr_stream);
+ } else {
+ /* append this body stream to the existing
+ header stream. we'll need to recreate the stream
+ with autoclosed fd. */
+ if (lseek(mail->fd, 0, SEEK_SET) < 0)
+ i_error("lseek(imapc) failed: %m");
+ hdr_stream = i_stream_create_fd_autoclose(&mail->fd, 0);
+ }
+ index_mail_close_streams(imail);
+ i_close_fd(&mail->fd);
+ } else {
+ if (!have_header) {
+ /* BODY.PEEK[TEXT] received - we can't currently handle
+ this before receiving BODY.PEEK[HEADER] reply */
+ return;
+ }
+ }
+
+ if (arg->type == IMAP_ARG_LITERAL_SIZE) {
+ if (!imapc_find_lfile_arg(reply, arg, &fd)) {
+ i_stream_unref(&hdr_stream);
+ return;
+ }
+ if ((fd = dup(fd)) == -1) {
+ i_error("dup() failed: %m");
+ i_stream_unref(&hdr_stream);
+ return;
+ }
+ mail->fd = fd;
+ imail->data.stream = i_stream_create_fd(fd, 0);
+ } else {
+ if (!imap_arg_get_nstring(arg, &value))
+ value = NULL;
+ if (value == NULL ||
+ (value[0] == '\0' &&
+ IMAPC_BOX_HAS_FEATURE(mbox, IMAPC_FEATURE_FETCH_EMPTY_IS_EXPUNGED))) {
+ mail_set_expunged(&imail->mail.mail);
+ i_stream_unref(&hdr_stream);
+ return;
+ }
+ if (mail->body == NULL) {
+ mail->body = buffer_create_dynamic(default_pool,
+ arg->str_len + 1);
+ } else if (!have_header && hdr_stream != NULL) {
+ /* header is already in the buffer - add body now
+ without destroying the existing header data */
+ i_stream_unref(&hdr_stream);
+ } else {
+ buffer_set_used_size(mail->body, 0);
+ }
+ buffer_append(mail->body, value, arg->str_len);
+ imail->data.stream = i_stream_create_from_data(mail->body->data,
+ mail->body->used);
+ }
+ if (have_header)
+ mail->header_fetched = TRUE;
+ mail->body_fetched = have_body;
+
+ if (hdr_stream != NULL) {
+ struct istream *inputs[3];
+
+ inputs[0] = hdr_stream;
+ inputs[1] = imail->data.stream;
+ inputs[2] = NULL;
+ imail->data.stream = i_stream_create_concat(inputs);
+ i_stream_unref(&inputs[0]);
+ i_stream_unref(&inputs[1]);
+ }
+
+ imapc_mail_init_stream(mail);
+}
+
+static void
+imapc_fetch_header_stream(struct imapc_mail *mail,
+ const struct imapc_untagged_reply *reply,
+ const struct imap_arg *args)
+{
+ const enum message_header_parser_flags hdr_parser_flags =
+ MESSAGE_HEADER_PARSER_FLAG_SKIP_INITIAL_LWSP |
+ MESSAGE_HEADER_PARSER_FLAG_DROP_CR;
+ const struct imap_arg *hdr_list;
+ struct mailbox_header_lookup_ctx *headers_ctx;
+ struct message_header_parser_ctx *parser;
+ struct message_header_line *hdr;
+ struct istream *input;
+ ARRAY_TYPE(const_string) hdr_arr;
+ const char *value;
+ int ret, fd;
+
+ if (!imap_arg_get_list(args, &hdr_list))
+ return;
+ if (!imap_arg_atom_equals(args+1, "]"))
+ return;
+ args += 2;
+
+ /* see if this is reply to the latest headers list request
+ (parse it even if it's not) */
+ t_array_init(&hdr_arr, 16);
+ while (imap_arg_get_astring(hdr_list, &value)) {
+ array_push_back(&hdr_arr, &value);
+ hdr_list++;
+ }
+ if (hdr_list->type != IMAP_ARG_EOL)
+ return;
+ array_append_zero(&hdr_arr);
+
+ if (headers_have_subset(array_front(&hdr_arr), mail->fetching_headers))
+ mail->header_list_fetched = TRUE;
+
+ if (args->type == IMAP_ARG_LITERAL_SIZE) {
+ if (!imapc_find_lfile_arg(reply, args, &fd))
+ return;
+ input = i_stream_create_fd(fd, 0);
+ } else {
+ if (!imap_arg_get_nstring(args, &value))
+ return;
+ if (value == NULL) {
+ mail_set_expunged(&mail->imail.mail.mail);
+ return;
+ }
+ input = i_stream_create_from_data(value, args->str_len);
+ }
+
+ headers_ctx = mailbox_header_lookup_init(mail->imail.mail.mail.box,
+ array_front(&hdr_arr));
+ index_mail_parse_header_init(&mail->imail, headers_ctx);
+
+ parser = message_parse_header_init(input, NULL, hdr_parser_flags);
+ while ((ret = message_parse_header_next(parser, &hdr)) > 0)
+ index_mail_parse_header(NULL, hdr, &mail->imail);
+ i_assert(ret != 0);
+ index_mail_parse_header(NULL, NULL, &mail->imail);
+ message_parse_header_deinit(&parser);
+
+ mailbox_header_lookup_unref(&headers_ctx);
+ i_stream_destroy(&input);
+}
+
+static const char *
+imapc_args_to_bodystructure(struct imapc_mail *mail,
+ const struct imap_arg *list_arg, bool extended)
+{
+ const struct imap_arg *args;
+ struct message_part *parts = NULL;
+ const char *ret, *error;
+ pool_t pool;
+
+ if (!imap_arg_get_list(list_arg, &args)) {
+ mail_set_critical(&mail->imail.mail.mail,
+ "imapc: Server sent invalid BODYSTRUCTURE parameters");
+ return NULL;
+ }
+
+ pool = pool_alloconly_create("imap bodystructure", 1024);
+ if (imap_bodystructure_parse_args(args, pool, &parts, &error) < 0) {
+ mail_set_critical(&mail->imail.mail.mail,
+ "imapc: Server sent invalid BODYSTRUCTURE: %s", error);
+ ret = NULL;
+ } else {
+ string_t *str = t_str_new(128);
+ if (imap_bodystructure_write(parts, str, extended, &error) < 0) {
+ /* All the input to imap_bodystructure_write() came
+ from imap_bodystructure_parse_args(). We should never
+ get here. Instead, if something is wrong the
+ parsing should have returned an error already. */
+ str_truncate(str, 0);
+ imap_write_args(str, args);
+ i_panic("Failed to write parsed BODYSTRUCTURE: %s "
+ "(original string: '%s')", error, str_c(str));
+ }
+ ret = p_strdup(mail->imail.mail.data_pool, str_c(str));
+ }
+ pool_unref(&pool);
+ return ret;
+}
+
+void imapc_mail_fetch_update(struct imapc_mail *mail,
+ const struct imapc_untagged_reply *reply,
+ const struct imap_arg *args)
+{
+ struct imapc_mailbox *mbox = IMAPC_MAILBOX(mail->imail.mail.mail.box);
+ const char *key, *value;
+ unsigned int i;
+ uoff_t size;
+ time_t t;
+ int tz;
+ bool match = FALSE;
+
+ for (i = 0; args[i].type != IMAP_ARG_EOL; i += 2) {
+ if (!imap_arg_get_atom(&args[i], &key) ||
+ args[i+1].type == IMAP_ARG_EOL)
+ break;
+
+ if (strcasecmp(key, "BODY[]") == 0) {
+ imapc_fetch_stream(mail, reply, &args[i+1], TRUE, TRUE);
+ match = TRUE;
+ } else if (strcasecmp(key, "BODY[HEADER]") == 0) {
+ imapc_fetch_stream(mail, reply, &args[i+1], TRUE, FALSE);
+ match = TRUE;
+ } else if (strcasecmp(key, "BODY[TEXT]") == 0) {
+ imapc_fetch_stream(mail, reply, &args[i+1], FALSE, TRUE);
+ match = TRUE;
+ } else if (strcasecmp(key, "BODY[HEADER.FIELDS") == 0) {
+ imapc_fetch_header_stream(mail, reply, &args[i+1]);
+ match = TRUE;
+ } else if (strcasecmp(key, "INTERNALDATE") == 0) {
+ if (imap_arg_get_astring(&args[i+1], &value) &&
+ imap_parse_datetime(value, &t, &tz)) {
+ mail->imail.data.received_date = t;
+ if (HAS_NO_BITS(mbox->capabilities,
+ IMAPC_CAPABILITY_SAVEDATE))
+ mail->imail.data.save_date = t;
+ }
+ match = TRUE;
+ } else if (strcasecmp(key, "SAVEDATE") == 0) {
+ if (imap_arg_get_astring(&args[i+1], &value)) {
+ if (strcasecmp(value, "NIL") == 0)
+ mail->imail.data.save_date = 0;
+ else if (imap_parse_datetime(value, &t, &tz))
+ mail->imail.data.save_date = t;
+ }
+ match = TRUE;
+ } else if (strcasecmp(key, "BODY") == 0) {
+ if (IMAPC_BOX_HAS_FEATURE(mbox, IMAPC_FEATURE_FETCH_BODYSTRUCTURE)) {
+ mail->imail.data.body =
+ imapc_args_to_bodystructure(mail, &args[i+1], FALSE);
+ }
+ match = TRUE;
+ } else if (strcasecmp(key, "BODYSTRUCTURE") == 0) {
+ if (IMAPC_BOX_HAS_FEATURE(mbox, IMAPC_FEATURE_FETCH_BODYSTRUCTURE)) {
+ mail->imail.data.bodystructure =
+ imapc_args_to_bodystructure(mail, &args[i+1], TRUE);
+ }
+ match = TRUE;
+ } else if (strcasecmp(key, "RFC822.SIZE") == 0) {
+ if (imap_arg_get_atom(&args[i+1], &value) &&
+ str_to_uoff(value, &size) == 0 &&
+ IMAPC_BOX_HAS_FEATURE(mbox, IMAPC_FEATURE_RFC822_SIZE)) {
+ mail->imail.data.physical_size = size;
+ mail->imail.data.virtual_size = size;
+ mail->imail.data.inexact_total_sizes = TRUE;
+ }
+ match = TRUE;
+ } else if (strcasecmp(key, "X-GM-MSGID") == 0 ||
+ strcasecmp(key, "X-GUID") == 0) {
+ if (imap_arg_get_astring(&args[i+1], &value)) {
+ mail->imail.data.guid =
+ p_strdup(mail->imail.mail.pool, value);
+ }
+ match = TRUE;
+ }
+ }
+ if (!match) {
+ /* this is only a FETCH FLAGS update for the wanted mail */
+ } else {
+ imapc_client_stop(mbox->storage->client->client);
+ }
+}
diff --git a/src/lib-storage/index/imapc/imapc-mail.c b/src/lib-storage/index/imapc/imapc-mail.c
new file mode 100644
index 0000000..1ecf03e
--- /dev/null
+++ b/src/lib-storage/index/imapc/imapc-mail.c
@@ -0,0 +1,675 @@
+/* Copyright (c) 2011-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "str.h"
+#include "hex-binary.h"
+#include "sha1.h"
+#include "istream.h"
+#include "message-part-data.h"
+#include "imap-envelope.h"
+#include "imapc-msgmap.h"
+#include "imapc-mail.h"
+#include "imapc-storage.h"
+
+static bool imapc_mail_get_cached_guid(struct mail *_mail);
+
+struct mail *
+imapc_mail_alloc(struct mailbox_transaction_context *t,
+ enum mail_fetch_field wanted_fields,
+ struct mailbox_header_lookup_ctx *wanted_headers)
+{
+ struct imapc_mail *mail;
+ pool_t pool;
+
+ pool = pool_alloconly_create("mail", 2048);
+ mail = p_new(pool, struct imapc_mail, 1);
+ mail->fd = -1;
+
+ index_mail_init(&mail->imail, t, wanted_fields, wanted_headers, pool, NULL);
+ return &mail->imail.mail.mail;
+}
+
+static bool imapc_mail_is_expunged(struct mail *_mail)
+{
+ struct imapc_mailbox *mbox = IMAPC_MAILBOX(_mail->box);
+ struct imapc_msgmap *msgmap;
+ uint32_t lseq, rseq;
+
+ if (!mbox->initial_sync_done) {
+ /* unknown at this point */
+ return FALSE;
+ }
+
+ if (mbox->sync_view != NULL) {
+ /* check if another session has already expunged it */
+ if (!mail_index_lookup_seq(mbox->sync_view, _mail->uid, &lseq))
+ return TRUE;
+ }
+
+ /* check if we've received EXPUNGE for it */
+ msgmap = imapc_client_mailbox_get_msgmap(mbox->client_box);
+ if (!imapc_msgmap_uid_to_rseq(msgmap, _mail->uid, &rseq))
+ return TRUE;
+
+ /* we may be running against a server that hasn't bothered sending
+ us an EXPUNGE. see if NOOP sends it. */
+ imapc_mailbox_noop(mbox);
+ if (!mbox->initial_sync_done) {
+ /* NOOP caused a reconnection and desync */
+ return FALSE;
+ }
+
+ return !imapc_msgmap_uid_to_rseq(msgmap, _mail->uid, &rseq);
+}
+
+static int imapc_mail_failed(struct mail *mail, const char *field)
+{
+ struct imapc_mail *imail = IMAPC_MAIL(mail);
+ struct imapc_mailbox *mbox = IMAPC_MAILBOX(mail->box);
+ bool fix_broken_mail = FALSE;
+
+ if (mail->expunged || imapc_mail_is_expunged(mail)) {
+ mail_set_expunged(mail);
+ } else if (!imapc_client_mailbox_is_opened(mbox->client_box)) {
+ /* we've already logged a disconnection error */
+ mail_storage_set_internal_error(mail->box->storage);
+ } else {
+ /* By default we'll assume that this is a critical failure,
+ because we don't want to lose any data. We can be here
+ either because it's a temporary failure on the server or
+ it's a permanent failure. Unfortunately we can't know
+ which case it is, so permanent failures need to be worked
+ around by setting imapc_features=fetch-fix-broken-mails.
+
+ One reason for permanent failures was that earlier Exchange
+ versions failed to return any data for messages in Calendars
+ mailbox. This seems to be fixed in newer versions.
+ */
+ fix_broken_mail = imail->fetch_ignore_if_missing;
+ mail_set_critical(mail,
+ "imapc: Remote server didn't send %s%s (FETCH replied: %s)",
+ field, fix_broken_mail ? " - treating it as empty" : "",
+ imail->last_fetch_reply);
+ }
+ return fix_broken_mail ? 0 : -1;
+}
+
+static uint64_t imapc_mail_get_modseq(struct mail *_mail)
+{
+ struct imapc_mailbox *mbox = IMAPC_MAILBOX(_mail->box);
+ struct imapc_msgmap *msgmap;
+ const uint64_t *modseqs;
+ unsigned int count;
+ uint32_t rseq;
+
+ if (!imapc_mailbox_has_modseqs(mbox))
+ return index_mail_get_modseq(_mail);
+
+ msgmap = imapc_client_mailbox_get_msgmap(mbox->client_box);
+ if (imapc_msgmap_uid_to_rseq(msgmap, _mail->uid, &rseq)) {
+ modseqs = array_get(&mbox->rseq_modseqs, &count);
+ if (rseq <= count)
+ return modseqs[rseq-1];
+ }
+ return 1; /* unknown modseq */
+}
+
+static int imapc_mail_get_received_date(struct mail *_mail, time_t *date_r)
+{
+ struct index_mail *mail = INDEX_MAIL(_mail);
+ struct index_mail_data *data = &mail->data;
+
+ if (index_mail_get_received_date(_mail, date_r) == 0)
+ return 0;
+
+ if (data->received_date == (time_t)-1) {
+ if (imapc_mail_fetch(_mail, MAIL_FETCH_RECEIVED_DATE, NULL) < 0)
+ return -1;
+ if (data->received_date == (time_t)-1) {
+ if (imapc_mail_failed(_mail, "INTERNALDATE") < 0)
+ return -1;
+ /* assume that the server never returns INTERNALDATE
+ for this mail (see BODY[] failure handling) */
+ data->received_date = 0;
+ }
+ }
+ *date_r = data->received_date;
+ return 0;
+}
+
+static int imapc_mail_get_save_date(struct mail *_mail, time_t *date_r)
+{
+ struct imapc_mailbox *mbox = IMAPC_MAILBOX(_mail->box);
+ struct index_mail *mail = INDEX_MAIL(_mail);
+ struct index_mail_data *data = &mail->data;
+
+ if (data->save_date != 0 && index_mail_get_save_date(_mail, date_r) > 0)
+ return 1;
+
+ if (HAS_NO_BITS(mbox->capabilities, IMAPC_CAPABILITY_SAVEDATE)) {
+ data->save_date = 0;
+ } else if (data->save_date == (time_t)-1) {
+ if (imapc_mail_fetch(_mail, MAIL_FETCH_SAVE_DATE, NULL) < 0)
+ return -1;
+ if (data->save_date == (time_t)-1 &&
+ imapc_mail_failed(_mail, "SAVEDATE") < 0)
+ return -1;
+ }
+ if (data->save_date == (time_t)-1 || data->save_date == 0) {
+ if (imapc_mail_get_received_date(_mail, date_r) < 0)
+ return -1;
+ return 0;
+ }
+ *date_r = data->save_date;
+ return 1;
+}
+
+static int imapc_mail_get_physical_size(struct mail *_mail, uoff_t *size_r)
+{
+ struct imapc_mailbox *mbox = IMAPC_MAILBOX(_mail->box);
+ struct index_mail *mail = INDEX_MAIL(_mail);
+ struct index_mail_data *data = &mail->data;
+ struct istream *input;
+ uoff_t old_offset;
+ int ret;
+
+ if (data->physical_size == UOFF_T_MAX)
+ (void)index_mail_get_physical_size(_mail, size_r);
+ if (data->physical_size != UOFF_T_MAX) {
+ *size_r = data->physical_size;
+ return 0;
+ }
+
+ if (IMAPC_BOX_HAS_FEATURE(mbox, IMAPC_FEATURE_RFC822_SIZE) &&
+ data->stream == NULL) {
+ /* Trust RFC822.SIZE to be correct enough to present to the
+ IMAP client. However, it can be wrong in some implementation
+ so try not to trust it too much. */
+ if (imapc_mail_fetch(_mail, MAIL_FETCH_PHYSICAL_SIZE, NULL) < 0)
+ return -1;
+ if (data->physical_size == UOFF_T_MAX) {
+ if (imapc_mail_failed(_mail, "RFC822.SIZE") < 0)
+ return -1;
+ /* assume that the server never returns RFC822.SIZE
+ for this mail (see BODY[] failure handling) */
+ data->physical_size = 0;
+ }
+ *size_r = data->physical_size;
+ return 0;
+ }
+
+ old_offset = data->stream == NULL ? 0 : data->stream->v_offset;
+ if (mail_get_stream(_mail, NULL, NULL, &input) < 0)
+ return -1;
+ i_assert(data->stream != NULL);
+ i_stream_seek(data->stream, old_offset);
+
+ ret = i_stream_get_size(data->stream, TRUE,
+ &data->physical_size);
+ if (ret <= 0) {
+ i_assert(ret != 0);
+ mail_set_critical(_mail, "imapc: stat(%s) failed: %m",
+ i_stream_get_name(data->stream));
+ return -1;
+ }
+ *size_r = data->physical_size;
+ return 0;
+}
+
+static int imapc_mail_get_virtual_size(struct mail *_mail, uoff_t *size_r)
+{
+ struct index_mail *mail = INDEX_MAIL(_mail);
+ struct index_mail_data *data = &mail->data;
+
+ if (imapc_mail_get_physical_size(_mail, size_r) < 0)
+ return -1;
+ data->virtual_size = data->physical_size;
+ return 0;
+}
+
+static int
+imapc_mail_get_header_stream(struct mail *_mail,
+ struct mailbox_header_lookup_ctx *headers,
+ struct istream **stream_r)
+{
+ struct imapc_mail *mail = IMAPC_MAIL(_mail);
+ struct imapc_mailbox *mbox = IMAPC_MAILBOX(_mail->box);
+ enum mail_lookup_abort old_abort = _mail->lookup_abort;
+ int ret;
+
+ if (mail->imail.data.access_part != 0 ||
+ !IMAPC_BOX_HAS_FEATURE(mbox, IMAPC_FEATURE_FETCH_HEADERS)) {
+ /* we're going to be reading the header/body anyway */
+ return index_mail_get_header_stream(_mail, headers, stream_r);
+ }
+
+ /* see if the wanted headers are already in cache */
+ _mail->lookup_abort = MAIL_LOOKUP_ABORT_READ_MAIL;
+ ret = index_mail_get_header_stream(_mail, headers, stream_r);
+ _mail->lookup_abort = old_abort;
+ if (ret == 0)
+ return 0;
+
+ /* fetch only the wanted headers */
+ if (imapc_mail_fetch(_mail, 0, headers->name) < 0)
+ return -1;
+ /* the headers should cached now. */
+ return index_mail_get_header_stream(_mail, headers, stream_r);
+}
+
+static int
+imapc_mail_get_headers(struct mail *_mail, const char *field,
+ bool decode_to_utf8, const char *const **value_r)
+{
+ struct mailbox_header_lookup_ctx *headers;
+ const char *header_names[2];
+ const unsigned char *data;
+ size_t size;
+ struct istream *input;
+ int ret;
+
+ header_names[0] = field;
+ header_names[1] = NULL;
+ headers = mailbox_header_lookup_init(_mail->box, header_names);
+ ret = mail_get_header_stream(_mail, headers, &input);
+ mailbox_header_lookup_unref(&headers);
+ if (ret < 0)
+ return -1;
+
+ while (i_stream_read_more(input, &data, &size) > 0)
+ i_stream_skip(input, size);
+ /* the header should cached now. */
+ return index_mail_get_headers(_mail, field, decode_to_utf8, value_r);
+}
+
+static int
+imapc_mail_get_first_header(struct mail *_mail, const char *field,
+ bool decode_to_utf8, const char **value_r)
+{
+ const char *const *values;
+ int ret;
+
+ ret = imapc_mail_get_headers(_mail, field, decode_to_utf8, &values);
+ if (ret <= 0)
+ return ret;
+ *value_r = values[0];
+ return 1;
+}
+
+static int
+imapc_mail_get_stream(struct mail *_mail, bool get_body,
+ struct message_size *hdr_size,
+ struct message_size *body_size, struct istream **stream_r)
+{
+ struct imapc_mail *mail = IMAPC_MAIL(_mail);
+ struct index_mail_data *data = &mail->imail.data;
+ enum mail_fetch_field fetch_field;
+
+ if (get_body && !mail->body_fetched &&
+ mail->imail.data.stream != NULL) {
+ /* we've fetched the header, but we need the body now too */
+ index_mail_close_streams(&mail->imail);
+ /* don't re-use any cached header sizes. we may be
+ intentionally downloading the full body because the header
+ wasn't returned correctly (e.g. pop3-migration does this) */
+ data->hdr_size_set = FALSE;
+ }
+
+ /* See if we can get it from cache. If the wanted_fields/headers are
+ set properly, this is usually already done by prefetching. */
+ imapc_mail_try_init_stream_from_cache(mail);
+
+ if (data->stream == NULL) {
+ if (!data->initialized) {
+ /* coming here from mail_set_seq() */
+ mail_set_aborted(_mail);
+ return -1;
+ }
+ if (_mail->expunged) {
+ /* We already detected that the mail is expunged.
+ Don't spend time trying to FETCH it again. */
+ mail_set_expunged(_mail);
+ return -1;
+ }
+ fetch_field = get_body ||
+ (data->access_part & READ_BODY) != 0 ?
+ MAIL_FETCH_STREAM_BODY : MAIL_FETCH_STREAM_HEADER;
+ if (imapc_mail_fetch(_mail, fetch_field, NULL) < 0)
+ return -1;
+
+ if (data->stream == NULL) {
+ if (imapc_mail_failed(_mail, "BODY[]") < 0)
+ return -1;
+ i_assert(data->stream == NULL);
+
+ /* return the broken email as empty */
+ mail->body_fetched = TRUE;
+ data->stream = i_stream_create_from_data(NULL, 0);
+ imapc_mail_init_stream(mail);
+ }
+ }
+
+ return index_mail_init_stream(&mail->imail, hdr_size, body_size,
+ stream_r);
+}
+
+bool imapc_mail_has_headers_in_cache(struct index_mail *mail,
+ struct mailbox_header_lookup_ctx *headers)
+{
+ struct mail *_mail = &mail->mail.mail;
+ unsigned int i;
+
+ for (i = 0; i < headers->count; i++) {
+ if (mail_cache_field_exists(_mail->transaction->cache_view,
+ _mail->seq, headers->idx[i]) <= 0)
+ return FALSE;
+ }
+ return TRUE;
+}
+
+void imapc_mail_update_access_parts(struct index_mail *mail)
+{
+ struct mail *_mail = &mail->mail.mail;
+ struct imapc_mailbox *mbox = IMAPC_MAILBOX(_mail->box);
+ struct index_mail_data *data = &mail->data;
+ struct mailbox_header_lookup_ctx *header_ctx;
+ const char *str;
+ time_t date;
+ uoff_t size;
+
+ if ((data->wanted_fields & MAIL_FETCH_RECEIVED_DATE) != 0)
+ (void)index_mail_get_received_date(_mail, &date);
+ if ((data->wanted_fields & MAIL_FETCH_SAVE_DATE) != 0) {
+ if (index_mail_get_save_date(_mail, &date) < 0 &&
+ HAS_NO_BITS(mbox->capabilities,
+ IMAPC_CAPABILITY_SAVEDATE)) {
+ (void)index_mail_get_received_date(_mail, &date);
+ data->save_date = data->received_date;
+ }
+ }
+ if ((data->wanted_fields & (MAIL_FETCH_PHYSICAL_SIZE |
+ MAIL_FETCH_VIRTUAL_SIZE)) != 0) {
+ if (index_mail_get_physical_size(_mail, &size) < 0 &&
+ !IMAPC_BOX_HAS_FEATURE(mbox, IMAPC_FEATURE_RFC822_SIZE))
+ data->access_part |= READ_HDR | READ_BODY;
+ }
+ if ((data->wanted_fields & MAIL_FETCH_GUID) != 0)
+ (void)imapc_mail_get_cached_guid(_mail);
+ if ((data->wanted_fields & MAIL_FETCH_IMAP_BODY) != 0)
+ (void)index_mail_get_cached_body(mail, &str);
+ if ((data->wanted_fields & MAIL_FETCH_IMAP_BODYSTRUCTURE) != 0)
+ (void)index_mail_get_cached_bodystructure(mail, &str);
+
+ if (data->access_part == 0 && data->wanted_headers != NULL &&
+ !IMAPC_BOX_HAS_FEATURE(mbox, IMAPC_FEATURE_FETCH_HEADERS)) {
+ /* see if all wanted headers exist in cache */
+ if (!imapc_mail_has_headers_in_cache(mail, data->wanted_headers))
+ data->access_part |= PARSE_HDR;
+ }
+ if (data->access_part == 0 &&
+ (data->wanted_fields & MAIL_FETCH_IMAP_ENVELOPE) != 0 &&
+ !IMAPC_BOX_HAS_FEATURE(mbox, IMAPC_FEATURE_FETCH_HEADERS)) {
+ /* the common code already checked this partially,
+ but we need a guaranteed correct answer */
+ header_ctx = mailbox_header_lookup_init(_mail->box,
+ message_part_envelope_headers);
+ if (!imapc_mail_has_headers_in_cache(mail, header_ctx))
+ data->access_part |= PARSE_HDR;
+ mailbox_header_lookup_unref(&header_ctx);
+ }
+}
+
+static void imapc_mail_set_seq(struct mail *_mail, uint32_t seq, bool saving)
+{
+ struct imapc_mail *imail = IMAPC_MAIL(_mail);
+ struct index_mail *mail = &imail->imail;
+ struct imapc_mailbox *mbox = (struct imapc_mailbox *)_mail->box;
+
+ index_mail_set_seq(_mail, seq, saving);
+ if (IMAPC_BOX_HAS_FEATURE(mbox, IMAPC_FEATURE_RFC822_SIZE)) {
+ /* RFC822.SIZE may be read from vsize record or cache. It may
+ not be exactly correct. */
+ mail->data.inexact_total_sizes = TRUE;
+ }
+
+ /* searching code handles prefetching internally,
+ elsewhere we want to do it immediately */
+ if (!mail->mail.search_mail && !_mail->saving)
+ (void)imapc_mail_prefetch(_mail);
+}
+
+static void
+imapc_mail_add_temp_wanted_fields(struct mail *_mail,
+ enum mail_fetch_field fields,
+ struct mailbox_header_lookup_ctx *headers)
+{
+ struct index_mail *mail = INDEX_MAIL(_mail);
+
+ index_mail_add_temp_wanted_fields(_mail, fields, headers);
+ if (_mail->seq != 0)
+ imapc_mail_update_access_parts(mail);
+}
+
+static void imapc_mail_close(struct mail *_mail)
+{
+ struct imapc_mail *mail = IMAPC_MAIL(_mail);
+ struct imapc_mailbox *mbox = IMAPC_MAILBOX(_mail->box);
+ struct imapc_mail_cache *cache = &mbox->prev_mail_cache;
+
+ if (mail->fetch_count > 0) {
+ imapc_mail_fetch_flush(mbox);
+ while (mail->fetch_count > 0)
+ imapc_mailbox_run_nofetch(mbox);
+ }
+
+ index_mail_close(_mail);
+
+ mail->fetching_headers = NULL;
+ if (mail->body_fetched) {
+ imapc_mail_cache_free(cache);
+ cache->uid = _mail->uid;
+ if (mail->fd != -1) {
+ cache->fd = mail->fd;
+ mail->fd = -1;
+ } else {
+ cache->buf = mail->body;
+ mail->body = NULL;
+ }
+ }
+ i_close_fd(&mail->fd);
+ buffer_free(&mail->body);
+ mail->header_fetched = FALSE;
+ mail->body_fetched = FALSE;
+
+ i_assert(mail->fetch_count == 0);
+}
+
+static int imapc_mail_get_hdr_hash(struct index_mail *imail)
+{
+ struct istream *input;
+ const unsigned char *data;
+ size_t size;
+ uoff_t old_offset;
+ struct sha1_ctxt sha1_ctx;
+ unsigned char sha1_output[SHA1_RESULTLEN];
+ const char *sha1_str;
+
+ sha1_init(&sha1_ctx);
+ old_offset = imail->data.stream == NULL ? 0 :
+ imail->data.stream->v_offset;
+ if (mail_get_hdr_stream(&imail->mail.mail, NULL, &input) < 0)
+ return -1;
+ i_assert(imail->data.stream != NULL);
+ while (i_stream_read_more(input, &data, &size) > 0) {
+ sha1_loop(&sha1_ctx, data, size);
+ i_stream_skip(input, size);
+ }
+ i_stream_seek(imail->data.stream, old_offset);
+ sha1_result(&sha1_ctx, sha1_output);
+
+ sha1_str = binary_to_hex(sha1_output, sizeof(sha1_output));
+ imail->data.guid = p_strdup(imail->mail.data_pool, sha1_str);
+ return 0;
+}
+
+static bool imapc_mail_get_cached_guid(struct mail *_mail)
+{
+ struct index_mail *imail = INDEX_MAIL(_mail);
+ const enum index_cache_field cache_idx =
+ imail->ibox->cache_fields[MAIL_CACHE_GUID].idx;
+ string_t *str;
+
+ if (imail->data.guid != NULL) {
+ if (mail_cache_field_can_add(_mail->transaction->cache_trans,
+ _mail->seq, cache_idx)) {
+ /* GUID was prefetched - add to cache */
+ index_mail_cache_add_idx(imail, cache_idx,
+ imail->data.guid, strlen(imail->data.guid));
+ }
+ return TRUE;
+ }
+
+ str = str_new(imail->mail.data_pool, 64);
+ if (mail_cache_lookup_field(_mail->transaction->cache_view,
+ str, imail->mail.mail.seq, cache_idx) > 0) {
+ imail->data.guid = str_c(str);
+ return TRUE;
+ }
+ return FALSE;
+}
+
+static int imapc_mail_get_guid(struct mail *_mail, const char **value_r)
+{
+ struct index_mail *imail = INDEX_MAIL(_mail);
+ struct imapc_mailbox *mbox = IMAPC_MAILBOX(_mail->box);
+ const enum index_cache_field cache_idx =
+ imail->ibox->cache_fields[MAIL_CACHE_GUID].idx;
+
+ if (imapc_mail_get_cached_guid(_mail)) {
+ *value_r = imail->data.guid;
+ return 0;
+ }
+
+ /* GUID not in cache, fetch it */
+ if (mbox->guid_fetch_field_name != NULL) {
+ if (imapc_mail_fetch(_mail, MAIL_FETCH_GUID, NULL) < 0)
+ return -1;
+ if (imail->data.guid == NULL) {
+ (void)imapc_mail_failed(_mail, mbox->guid_fetch_field_name);
+ return -1;
+ }
+ } else {
+ /* use hash of message headers as the GUID */
+ if (imapc_mail_get_hdr_hash(imail) < 0)
+ return -1;
+ }
+
+ index_mail_cache_add_idx(imail, cache_idx,
+ imail->data.guid, strlen(imail->data.guid));
+ *value_r = imail->data.guid;
+ return 0;
+}
+
+static int
+imapc_mail_get_special(struct mail *_mail, enum mail_fetch_field field,
+ const char **value_r)
+{
+ struct imapc_mailbox *mbox = IMAPC_MAILBOX(_mail->box);
+ struct index_mail *imail = INDEX_MAIL(_mail);
+ uint64_t num;
+
+ switch (field) {
+ case MAIL_FETCH_GUID:
+ if (!IMAPC_BOX_HAS_FEATURE(mbox, IMAPC_FEATURE_GUID_FORCED) &&
+ mbox->guid_fetch_field_name == NULL) {
+ /* GUIDs not supported by server */
+ break;
+ }
+ *value_r = "";
+ return imapc_mail_get_guid(_mail, value_r);
+ case MAIL_FETCH_UIDL_BACKEND:
+ if (!IMAPC_BOX_HAS_FEATURE(mbox, IMAPC_FEATURE_GMAIL_MIGRATION))
+ break;
+ if (imapc_mail_get_guid(_mail, value_r) < 0)
+ return -1;
+ if (str_to_uint64(*value_r, &num) < 0) {
+ mail_set_critical(_mail,
+ "X-GM-MSGID not 64bit integer as expected for POP3 UIDL generation: %s", *value_r);
+ return -1;
+ }
+
+ *value_r = p_strdup_printf(imail->mail.data_pool,
+ "GmailId%"PRIx64, num);
+ return 0;
+ case MAIL_FETCH_IMAP_BODY:
+ if (!IMAPC_BOX_HAS_FEATURE(mbox, IMAPC_FEATURE_FETCH_BODYSTRUCTURE))
+ break;
+
+ if (index_mail_get_cached_body(imail, value_r))
+ return 0;
+ if (imapc_mail_fetch(_mail, field, NULL) < 0)
+ return -1;
+ if (imail->data.body == NULL) {
+ (void)imapc_mail_failed(_mail, "BODY");
+ return -1;
+ }
+ *value_r = imail->data.body;
+ return 0;
+ case MAIL_FETCH_IMAP_BODYSTRUCTURE:
+ if (!IMAPC_BOX_HAS_FEATURE(mbox, IMAPC_FEATURE_FETCH_BODYSTRUCTURE))
+ break;
+
+ if (index_mail_get_cached_bodystructure(imail, value_r))
+ return 0;
+ if (imapc_mail_fetch(_mail, field, NULL) < 0)
+ return -1;
+ if (imail->data.bodystructure == NULL) {
+ (void)imapc_mail_failed(_mail, "BODYSTRUCTURE");
+ return -1;
+ }
+ *value_r = imail->data.bodystructure;
+ return 0;
+ default:
+ break;
+ }
+
+ return index_mail_get_special(_mail, field, value_r);
+}
+
+struct mail_vfuncs imapc_mail_vfuncs = {
+ imapc_mail_close,
+ index_mail_free,
+ imapc_mail_set_seq,
+ index_mail_set_uid,
+ index_mail_set_uid_cache_updates,
+ imapc_mail_prefetch,
+ index_mail_precache,
+ imapc_mail_add_temp_wanted_fields,
+
+ index_mail_get_flags,
+ index_mail_get_keywords,
+ index_mail_get_keyword_indexes,
+ imapc_mail_get_modseq,
+ index_mail_get_pvt_modseq,
+ index_mail_get_parts,
+ index_mail_get_date,
+ imapc_mail_get_received_date,
+ imapc_mail_get_save_date,
+ imapc_mail_get_virtual_size,
+ imapc_mail_get_physical_size,
+ imapc_mail_get_first_header,
+ imapc_mail_get_headers,
+ imapc_mail_get_header_stream,
+ imapc_mail_get_stream,
+ index_mail_get_binary_stream,
+ imapc_mail_get_special,
+ index_mail_get_backend_mail,
+ index_mail_update_flags,
+ index_mail_update_keywords,
+ index_mail_update_modseq,
+ index_mail_update_pvt_modseq,
+ NULL,
+ index_mail_expunge,
+ index_mail_set_cache_corrupted,
+ index_mail_opened,
+};
diff --git a/src/lib-storage/index/imapc/imapc-mail.h b/src/lib-storage/index/imapc/imapc-mail.h
new file mode 100644
index 0000000..52dfe5e
--- /dev/null
+++ b/src/lib-storage/index/imapc/imapc-mail.h
@@ -0,0 +1,51 @@
+#ifndef IMAPC_MAIL_H
+#define IMAPC_MAIL_H
+
+#include "index-mail.h"
+
+struct imap_arg;
+struct imapc_untagged_reply;
+struct imapc_mailbox;
+
+struct imapc_mail {
+ struct index_mail imail;
+
+ enum mail_fetch_field fetching_fields;
+ const char *const *fetching_headers;
+ unsigned int fetch_count;
+ bool fetch_sent;
+ const char *last_fetch_reply;
+
+ int fd;
+ buffer_t *body;
+ bool header_fetched;
+ bool body_fetched;
+ bool header_list_fetched;
+ bool fetch_ignore_if_missing;
+ bool fetch_failed;
+};
+
+#define IMAPC_MAIL(s) container_of(s, struct imapc_mail, imail.mail.mail)
+
+extern struct mail_vfuncs imapc_mail_vfuncs;
+
+struct mail *
+imapc_mail_alloc(struct mailbox_transaction_context *t,
+ enum mail_fetch_field wanted_fields,
+ struct mailbox_header_lookup_ctx *wanted_headers);
+int imapc_mail_fetch(struct mail *mail, enum mail_fetch_field fields,
+ const char *const *headers);
+void imapc_mail_try_init_stream_from_cache(struct imapc_mail *mail);
+bool imapc_mail_prefetch(struct mail *mail);
+void imapc_mail_fetch_flush(struct imapc_mailbox *mbox);
+void imapc_mail_init_stream(struct imapc_mail *mail);
+bool imapc_mail_has_headers_in_cache(struct index_mail *mail,
+ struct mailbox_header_lookup_ctx *headers);
+
+void imapc_mail_fetch_update(struct imapc_mail *mail,
+ const struct imapc_untagged_reply *reply,
+ const struct imap_arg *args);
+void imapc_mail_update_access_parts(struct index_mail *mail);
+void imapc_mail_command_flush(struct imapc_mailbox *mbox);
+
+#endif
diff --git a/src/lib-storage/index/imapc/imapc-mailbox.c b/src/lib-storage/index/imapc/imapc-mailbox.c
new file mode 100644
index 0000000..ff915d9
--- /dev/null
+++ b/src/lib-storage/index/imapc/imapc-mailbox.c
@@ -0,0 +1,994 @@
+/* Copyright (c) 2011-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "ioloop.h"
+#include "str.h"
+#include "mail-index-modseq.h"
+#include "imap-arg.h"
+#include "imap-seqset.h"
+#include "imap-util.h"
+#include "imapc-mail.h"
+#include "imapc-msgmap.h"
+#include "imapc-list.h"
+#include "imapc-search.h"
+#include "imapc-sync.h"
+#include "imapc-storage.h"
+
+#define NOTIFY_DELAY_MSECS 500
+
+void imapc_mailbox_set_corrupted(struct imapc_mailbox *mbox,
+ const char *reason, ...)
+{
+ const char *errmsg;
+ va_list va;
+
+ va_start(va, reason);
+ errmsg = t_strdup_printf("Mailbox '%s' state corrupted: %s",
+ mbox->box.name, t_strdup_vprintf(reason, va));
+ va_end(va);
+
+ mail_storage_set_internal_error(&mbox->storage->storage);
+
+ if (!mbox->initial_sync_done) {
+ /* we failed during initial sync. need to rebuild indexes if
+ we want to get this fixed */
+ mail_index_mark_corrupted(mbox->box.index);
+ } else {
+ /* maybe the remote server is buggy and has become confused.
+ try reconnecting. */
+ }
+ imapc_client_mailbox_reconnect(mbox->client_box, errmsg);
+}
+
+struct mail_index_view *
+imapc_mailbox_get_sync_view(struct imapc_mailbox *mbox)
+{
+ if (mbox->sync_view == NULL)
+ mbox->sync_view = mail_index_view_open(mbox->box.index);
+ return mbox->sync_view;
+}
+
+static void imapc_mailbox_init_delayed_trans(struct imapc_mailbox *mbox)
+{
+ if (mbox->delayed_sync_trans != NULL)
+ return;
+
+ i_assert(mbox->delayed_sync_cache_view == NULL);
+ i_assert(mbox->delayed_sync_cache_trans == NULL);
+
+ mbox->delayed_sync_trans =
+ mail_index_transaction_begin(imapc_mailbox_get_sync_view(mbox),
+ MAIL_INDEX_TRANSACTION_FLAG_EXTERNAL);
+ mbox->delayed_sync_view =
+ mail_index_transaction_open_updated_view(mbox->delayed_sync_trans);
+
+ mbox->delayed_sync_cache_view =
+ mail_cache_view_open(mbox->box.cache, mbox->delayed_sync_view);
+ mbox->delayed_sync_cache_trans =
+ mail_cache_get_transaction(mbox->delayed_sync_cache_view,
+ mbox->delayed_sync_trans);
+}
+
+static int imapc_mailbox_commit_delayed_expunges(struct imapc_mailbox *mbox)
+{
+ struct mail_index_view *view = imapc_mailbox_get_sync_view(mbox);
+ struct mail_index_transaction *trans;
+ struct seq_range_iter iter;
+ unsigned int n;
+ uint32_t lseq, uid;
+ int ret;
+
+ trans = mail_index_transaction_begin(view,
+ MAIL_INDEX_TRANSACTION_FLAG_EXTERNAL);
+
+ seq_range_array_iter_init(&iter, &mbox->delayed_expunged_uids); n = 0;
+ while (seq_range_array_iter_nth(&iter, n++, &uid)) {
+ if (mail_index_lookup_seq(view, uid, &lseq))
+ mail_index_expunge(trans, lseq);
+ }
+ array_clear(&mbox->delayed_expunged_uids);
+ ret = mail_index_transaction_commit(&trans);
+ if (ret < 0)
+ mailbox_set_index_error(&mbox->box);
+ return ret;
+}
+
+int imapc_mailbox_commit_delayed_trans(struct imapc_mailbox *mbox,
+ bool force, bool *changes_r)
+{
+ int ret = 0;
+
+ *changes_r = FALSE;
+
+ if (mbox->delayed_sync_view != NULL)
+ mail_index_view_close(&mbox->delayed_sync_view);
+ if (mbox->delayed_sync_trans == NULL)
+ ;
+ else if (!mbox->selected && !force) {
+ /* ignore any changes done during SELECT */
+ mail_index_transaction_rollback(&mbox->delayed_sync_trans);
+ } else {
+ if (mail_index_transaction_commit(&mbox->delayed_sync_trans) < 0) {
+ mailbox_set_index_error(&mbox->box);
+ ret = -1;
+ }
+ *changes_r = TRUE;
+ }
+ mbox->delayed_sync_cache_trans = NULL;
+ if (mbox->delayed_sync_cache_view != NULL)
+ mail_cache_view_close(&mbox->delayed_sync_cache_view);
+
+ if (array_count(&mbox->delayed_expunged_uids) > 0) {
+ /* delayed expunges - commit them now in a separate
+ transaction. Reopen mbox->sync_view to see changes
+ committed in delayed_sync_trans. */
+ if (mbox->sync_view != NULL)
+ mail_index_view_close(&mbox->sync_view);
+ if (imapc_mailbox_commit_delayed_expunges(mbox) < 0)
+ ret = -1;
+ }
+
+ if (mbox->sync_view != NULL)
+ mail_index_view_close(&mbox->sync_view);
+ i_assert(mbox->delayed_sync_trans == NULL);
+ i_assert(mbox->delayed_sync_view == NULL);
+ i_assert(mbox->delayed_sync_cache_trans == NULL);
+ return ret;
+}
+
+static void imapc_mailbox_idle_timeout(struct imapc_mailbox *mbox)
+{
+ timeout_remove(&mbox->to_idle_delay);
+ if (mbox->box.notify_callback != NULL)
+ mbox->box.notify_callback(&mbox->box, mbox->box.notify_context);
+}
+
+static void imapc_mailbox_idle_notify(struct imapc_mailbox *mbox)
+{
+ struct ioloop *old_ioloop = current_ioloop;
+
+ if (mbox->box.notify_callback != NULL &&
+ mbox->to_idle_delay == NULL) {
+ io_loop_set_current(mbox->storage->root_ioloop);
+ mbox->to_idle_delay =
+ timeout_add_short(NOTIFY_DELAY_MSECS,
+ imapc_mailbox_idle_timeout, mbox);
+ io_loop_set_current(old_ioloop);
+ }
+}
+
+static void
+imapc_mailbox_index_expunge(struct imapc_mailbox *mbox, uint32_t uid)
+{
+ uint32_t lseq;
+
+ if (mail_index_lookup_seq(mbox->sync_view, uid, &lseq))
+ mail_index_expunge(mbox->delayed_sync_trans, lseq);
+ else if (mail_index_lookup_seq(mbox->delayed_sync_view, uid, &lseq)) {
+ /* this message exists only in this transaction. lib-index
+ can't currently handle expunging anything except the last
+ appended message in a transaction, and fixing it would be
+ quite a lot of trouble. so instead we'll just delay doing
+ this expunge until after the current transaction has been
+ committed. */
+ seq_range_array_add(&mbox->delayed_expunged_uids, uid);
+ } else {
+ /* already expunged by another session */
+ }
+}
+
+static void
+imapc_mailbox_fetch_state_finish(struct imapc_mailbox *mbox)
+{
+ uint32_t lseq, uid, msg_count;
+
+ if (mbox->sync_next_lseq == 0) {
+ /* FETCH n:*, not 1:* */
+ i_assert(mbox->state_fetched_success ||
+ (mbox->box.flags & MAILBOX_FLAG_SAVEONLY) != 0);
+ return;
+ }
+
+ /* if we haven't seen FETCH reply for some messages at the end of
+ mailbox they've been externally expunged. */
+ msg_count = mail_index_view_get_messages_count(mbox->delayed_sync_view);
+ for (lseq = mbox->sync_next_lseq; lseq <= msg_count; lseq++) {
+ mail_index_lookup_uid(mbox->delayed_sync_view, lseq, &uid);
+ if (uid >= mbox->sync_uid_next) {
+ /* another process already added new messages to index
+ that our IMAP connection hasn't seen yet */
+ break;
+ }
+ imapc_mailbox_index_expunge(mbox, uid);
+ }
+
+ mbox->sync_next_lseq = 0;
+ mbox->sync_next_rseq = 0;
+ mbox->state_fetched_success = TRUE;
+}
+
+static void
+imapc_mailbox_fetch_state_callback(const struct imapc_command_reply *reply,
+ void *context)
+{
+ struct imapc_mailbox *mbox = context;
+
+ mbox->state_fetching_uid1 = FALSE;
+ mbox->delayed_untagged_exists = FALSE;
+ imapc_client_stop(mbox->storage->client->client);
+
+ switch (reply->state) {
+ case IMAPC_COMMAND_STATE_OK:
+ imapc_mailbox_fetch_state_finish(mbox);
+ break;
+ case IMAPC_COMMAND_STATE_NO:
+ imapc_copy_error_from_reply(mbox->storage, MAIL_ERROR_PARAMS, reply);
+ break;
+ case IMAPC_COMMAND_STATE_DISCONNECTED:
+ mail_storage_set_internal_error(mbox->box.storage);
+
+ break;
+ default:
+ mail_storage_set_critical(mbox->box.storage,
+ "imapc: state FETCH failed: %s", reply->text_full);
+ break;
+ }
+}
+
+void imap_mailbox_select_finish(struct imapc_mailbox *mbox)
+{
+ if (mbox->exists_count == 0) {
+ /* no mails. expunge everything. */
+ mbox->sync_next_lseq = 1;
+ imapc_mailbox_init_delayed_trans(mbox);
+ imapc_mailbox_fetch_state_finish(mbox);
+ }
+ mbox->selected = TRUE;
+}
+
+bool
+imapc_mailbox_fetch_state(struct imapc_mailbox *mbox, uint32_t first_uid)
+{
+ struct imapc_command *cmd;
+
+ if (mbox->exists_count == 0) {
+ /* empty mailbox - no point in fetching anything.
+ just make sure everything is expunged in local index.
+ Delay calling imapc_mailbox_fetch_state_finish() until
+ SELECT finishes, so we see the updated UIDNEXT. */
+ return FALSE;
+ }
+ if (mbox->state_fetching_uid1) {
+ /* retrying after reconnection - don't send duplicate */
+ return FALSE;
+ }
+
+ string_t *str = t_str_new(64);
+ str_printfa(str, "UID FETCH %u:* (FLAGS", first_uid);
+ if (imapc_mailbox_has_modseqs(mbox)) {
+ str_append(str, " MODSEQ");
+ mail_index_modseq_enable(mbox->box.index);
+ }
+ if (IMAPC_BOX_HAS_FEATURE(mbox, IMAPC_FEATURE_GMAIL_MIGRATION)) {
+ enum mailbox_info_flags flags;
+
+ if (!mail_index_is_in_memory(mbox->box.index)) {
+ /* these can be efficiently fetched among flags and
+ stored into cache */
+ str_append(str, " X-GM-MSGID");
+ }
+ /* do this only for the \All mailbox */
+ if (imapc_list_get_mailbox_flags(mbox->box.list,
+ mbox->box.name, &flags) == 0 &&
+ (flags & MAILBOX_SPECIALUSE_ALL) != 0)
+ str_append(str, " X-GM-LABELS");
+
+ }
+ str_append_c(str, ')');
+
+ cmd = imapc_client_mailbox_cmd(mbox->client_box,
+ imapc_mailbox_fetch_state_callback, mbox);
+ if (first_uid == 1) {
+ mbox->sync_next_lseq = 1;
+ mbox->sync_next_rseq = 1;
+ mbox->state_fetched_success = FALSE;
+ /* only the FETCH 1:* is retriable - others will be retried
+ by the 1:* after the reconnection */
+ imapc_command_set_flags(cmd, IMAPC_COMMAND_FLAG_RETRIABLE);
+ }
+ mbox->state_fetching_uid1 = first_uid == 1;
+ imapc_command_send(cmd, str_c(str));
+ return TRUE;
+}
+
+static void
+imapc_untagged_exists(const struct imapc_untagged_reply *reply,
+ struct imapc_mailbox *mbox)
+{
+ struct mail_index_view *view;
+ uint32_t exists_count = reply->num;
+
+ if (mbox == NULL)
+ return;
+ if (mbox->exists_received &&
+ IMAPC_BOX_HAS_FEATURE(mbox, IMAPC_FEATURE_NO_MSN_UPDATES)) {
+ /* ignore all except the first EXISTS reply (returned by
+ SELECT) */
+ return;
+ }
+
+ mbox->exists_count = exists_count;
+ mbox->exists_received = TRUE;
+
+ view = mbox->delayed_sync_view;
+ if (view == NULL)
+ view = imapc_mailbox_get_sync_view(mbox);
+
+ if (mbox->selecting) {
+ /* We don't know the latest flags, refresh them. */
+ (void)imapc_mailbox_fetch_state(mbox, 1);
+ } else if (mbox->sync_fetch_first_uid != 1) {
+ const struct mail_index_header *hdr;
+ hdr = mail_index_get_header(view);
+ mbox->sync_fetch_first_uid = hdr->next_uid;
+ mbox->delayed_untagged_exists = TRUE;
+ }
+ imapc_mailbox_idle_notify(mbox);
+}
+
+static bool keywords_are_equal(struct mail_keywords *kw,
+ const ARRAY_TYPE(keyword_indexes) *kw_arr)
+{
+ const unsigned int *kw_idx;
+ unsigned int i, j, count;
+
+ kw_idx = array_get(kw_arr, &count);
+ if (count != kw->count)
+ return FALSE;
+
+ /* there are normally only a few keywords, so O(n^2) is fine */
+ for (i = 0; i < count; i++) {
+ for (j = 0; j < count; j++) {
+ if (kw->idx[i] == kw_idx[j])
+ break;
+ }
+ if (j == count)
+ return FALSE;
+ }
+ return TRUE;
+}
+
+static int
+imapc_mailbox_msgmap_update(struct imapc_mailbox *mbox,
+ uint32_t rseq, uint32_t fetch_uid,
+ uint32_t *lseq_r, uint32_t *uid_r,
+ bool *new_message_r)
+{
+ struct imapc_msgmap *msgmap;
+ uint32_t uid, msg_count, rseq2;
+
+ *lseq_r = 0;
+ *uid_r = uid = fetch_uid;
+ *new_message_r = FALSE;
+
+ if (rseq > mbox->exists_count) {
+ /* Receiving a FETCH for a message that EXISTS hasn't
+ announced yet. MS Exchange has a bug where our UID FETCH
+ request sometimes sends replies where sequences are above
+ EXISTS value, but their UIDs are for existing messages.
+ We'll just ignore these replies. */
+ return 0;
+ }
+ if (rseq < mbox->prev_skipped_rseq &&
+ fetch_uid > mbox->prev_skipped_uid) {
+ /* This was the initial attempt at catching the above
+ MS Exchange bug, but the above one appears to catch all
+ these cases. But keep it here just in case. */
+ imapc_mailbox_set_corrupted(mbox,
+ "FETCH sequence/UID order is mixed "
+ "(seq=%u,%u vs uid=%u,%u)",
+ mbox->prev_skipped_rseq, rseq,
+ mbox->prev_skipped_uid, fetch_uid);
+ return -1;
+ }
+
+ msgmap = imapc_client_mailbox_get_msgmap(mbox->client_box);
+ msg_count = imapc_msgmap_count(msgmap);
+ if (fetch_uid != 0 && mbox->state_fetched_success &&
+ (IMAPC_BOX_HAS_FEATURE(mbox, IMAPC_FEATURE_FETCH_MSN_WORKAROUNDS) ||
+ IMAPC_BOX_HAS_FEATURE(mbox, IMAPC_FEATURE_NO_MSN_UPDATES))) {
+ /* if we know the UID, use own own generated rseq instead of
+ the potentially broken rseq that the server sent.
+ Skip this during the initial FETCH 1:* (UID ..) handling,
+ or we can't detect duplicate UIDs and will instead
+ assert-crash later on. */
+ uint32_t fixed_rseq;
+
+ if (imapc_msgmap_uid_to_rseq(msgmap, fetch_uid, &fixed_rseq))
+ rseq = fixed_rseq;
+ else if (fetch_uid >= imapc_msgmap_uidnext(msgmap) &&
+ rseq <= msg_count) {
+ /* The current rseq is wrong. Lets hope that the
+ correct rseq is the next new one. This happens
+ especially with no-msn-updates when mails have been
+ expunged and new mails arrive in the same session. */
+ rseq = msg_count+1;
+ }
+ }
+
+ if (rseq <= msg_count) {
+ uid = imapc_msgmap_rseq_to_uid(msgmap, rseq);
+ if (uid != fetch_uid && fetch_uid != 0) {
+ imapc_mailbox_set_corrupted(mbox,
+ "FETCH UID mismatch (%u != %u)",
+ fetch_uid, uid);
+ return -1;
+ }
+ *uid_r = uid;
+ } else if (fetch_uid == 0 || rseq != msg_count+1) {
+ /* probably a flag update for a message we haven't yet
+ received our initial UID FETCH for. we should get
+ another one. */
+ if (fetch_uid == 0)
+ return 0;
+
+ if (imapc_msgmap_uid_to_rseq(msgmap, fetch_uid, &rseq2)) {
+ imapc_mailbox_set_corrupted(mbox,
+ "FETCH returned wrong sequence for UID %u "
+ "(%u != %u)", fetch_uid, rseq, rseq2);
+ return -1;
+ }
+ mbox->prev_skipped_rseq = rseq;
+ mbox->prev_skipped_uid = fetch_uid;
+ /* Check if this uid must be added later when syncing. */
+ *new_message_r = TRUE;
+ } else if (fetch_uid < imapc_msgmap_uidnext(msgmap)) {
+ imapc_mailbox_set_corrupted(mbox,
+ "Expunged message reappeared in session "
+ "(uid=%u < next_uid=%u)",
+ fetch_uid, imapc_msgmap_uidnext(msgmap));
+ return -1;
+ } else {
+ /* newly seen message */
+ imapc_msgmap_append(msgmap, rseq, uid);
+ if (uid < mbox->min_append_uid ||
+ uid < mail_index_get_header(mbox->delayed_sync_view)->next_uid) {
+ /* message is already added to index */
+ } else if (mbox->state_fetching_uid1) {
+ /* Initial fetching, allow messages to be appened to
+ index directly */
+ mail_index_append(mbox->delayed_sync_trans,
+ uid, lseq_r);
+ mbox->min_append_uid = uid + 1;
+ } else {
+ /* message is not yet added to index, in order to
+ prevent log synchronization errors add this
+ message later, when the mailbox is synced. */
+ *new_message_r = TRUE;
+ }
+ }
+ return 0;
+}
+
+bool imapc_mailbox_name_equals(struct imapc_mailbox *mbox,
+ const char *remote_name)
+{
+ const char *imapc_remote_name =
+ imapc_mailbox_get_remote_name(mbox);
+
+ if (strcmp(imapc_remote_name, remote_name) == 0) {
+ /* match */
+ return TRUE;
+ } else if (strcasecmp(mbox->box.name, "INBOX") == 0 &&
+ strcasecmp(remote_name, "INBOX") == 0) {
+ /* case-insensitive INBOX */
+ return TRUE;
+ }
+ return FALSE;
+}
+
+static struct imapc_untagged_fetch_ctx *
+imapc_untagged_fetch_ctx_create(void)
+{
+ pool_t pool = pool_alloconly_create("imapc untagged fetch ctx", 128);
+ struct imapc_untagged_fetch_ctx *ctx =
+ p_new(pool, struct imapc_untagged_fetch_ctx, 1);
+ ctx->pool = pool;
+ return ctx;
+}
+
+void imapc_untagged_fetch_ctx_free(struct imapc_untagged_fetch_ctx **_ctx)
+{
+ struct imapc_untagged_fetch_ctx *ctx = *_ctx;
+
+ *_ctx = NULL;
+ i_assert(ctx != NULL);
+
+ pool_unref(&ctx->pool);
+}
+
+void imapc_untagged_fetch_update_flags(struct imapc_mailbox *mbox,
+ struct imapc_untagged_fetch_ctx *ctx,
+ struct mail_index_view *view,
+ uint32_t lseq)
+{
+ ARRAY_TYPE(keyword_indexes) old_kws;
+ struct mail_keywords *kw;
+ const struct mail_index_record *rec = NULL;
+ const char *atom;
+
+ if (!ctx->have_flags)
+ return;
+
+ rec = mail_index_lookup(view, lseq);
+ if (rec->flags != ctx->flags) {
+ mail_index_update_flags(mbox->delayed_sync_trans, lseq,
+ MODIFY_REPLACE, ctx->flags);
+ }
+
+ t_array_init(&old_kws, 8);
+ mail_index_lookup_keywords(view, lseq, &old_kws);
+
+ if (ctx->have_gmail_labels) {
+ /* add keyword for mails that have GMail labels.
+ this can be used for "All Mail" mailbox migrations
+ with dsync */
+ atom = "$GMailHaveLabels";
+ array_push_back(&ctx->keywords, &atom);
+ }
+
+ array_append_zero(&ctx->keywords);
+ kw = mail_index_keywords_create(mbox->box.index,
+ array_front(&ctx->keywords));
+ if (!keywords_are_equal(kw, &old_kws)) {
+ mail_index_update_keywords(mbox->delayed_sync_trans,
+ lseq, MODIFY_REPLACE, kw);
+ }
+ mail_index_keywords_unref(&kw);
+}
+
+static bool imapc_untagged_fetch_handle(struct imapc_mailbox *mbox,
+ struct imapc_untagged_fetch_ctx *ctx,
+ uint32_t rseq)
+{
+ uint32_t lseq;
+ bool new_message;
+
+ imapc_mailbox_init_delayed_trans(mbox);
+ if (imapc_mailbox_msgmap_update(mbox, rseq, ctx->fetch_uid,
+ &lseq, &ctx->uid,
+ &new_message) < 0 || ctx->uid == 0)
+ return FALSE;
+
+ if ((ctx->flags & MAIL_RECENT) == 0 && mbox->highest_nonrecent_uid < ctx->uid) {
+ /* remember for STATUS_FIRST_RECENT_UID */
+ mbox->highest_nonrecent_uid = ctx->uid;
+ }
+ /* FIXME: we should ideally also pass these through so they show up
+ to clients. */
+ ctx->flags &= ENUM_NEGATE(MAIL_RECENT);
+
+ if (lseq == 0) {
+ if (!mail_index_lookup_seq(mbox->delayed_sync_view,
+ ctx->uid, &lseq)) {
+ /* already expunged by another session */
+ if (rseq == mbox->sync_next_rseq)
+ mbox->sync_next_rseq++;
+ return new_message;
+ }
+ }
+
+ if (rseq == mbox->sync_next_rseq) {
+ /* we're doing the initial full sync of mails. expunge any
+ mails that no longer exist. */
+ while (mbox->sync_next_lseq < lseq) {
+ mail_index_lookup_uid(mbox->delayed_sync_view,
+ mbox->sync_next_lseq, &ctx->uid);
+ imapc_mailbox_index_expunge(mbox, ctx->uid);
+ mbox->sync_next_lseq++;
+ }
+ i_assert(lseq == mbox->sync_next_lseq);
+ mbox->sync_next_rseq++;
+ mbox->sync_next_lseq++;
+ }
+
+ if (!new_message) {
+ /* Only update flags immediately for existing messages */
+ imapc_untagged_fetch_update_flags(mbox, ctx,
+ mbox->delayed_sync_view, lseq);
+ }
+
+ if (ctx->modseq != 0) {
+ if (mail_index_modseq_lookup(mbox->delayed_sync_view, lseq) < ctx->modseq)
+ mail_index_update_modseq(mbox->delayed_sync_trans, lseq, ctx->modseq);
+ array_idx_set(&mbox->rseq_modseqs, rseq-1, &ctx->modseq);
+ }
+ if (ctx->guid != NULL) {
+ struct index_mailbox_context *ibox = INDEX_STORAGE_CONTEXT(&mbox->box);
+ const enum index_cache_field guid_cache_idx =
+ ibox->cache_fields[MAIL_CACHE_GUID].idx;
+
+ if (mail_cache_field_can_add(mbox->delayed_sync_cache_trans,
+ lseq, guid_cache_idx)) {
+ mail_cache_add(mbox->delayed_sync_cache_trans, lseq,
+ guid_cache_idx, ctx->guid, strlen(ctx->guid));
+ }
+ }
+ return new_message;
+}
+
+static bool imapc_untagged_fetch_parse(struct imapc_mailbox *mbox,
+ struct imapc_untagged_fetch_ctx *ctx,
+ const struct imap_arg *list)
+{
+ const struct imap_arg *flags_list, *modseq_list;
+ const char *atom, *patom;
+ unsigned int i, j;
+
+ ctx->fetch_uid = 0; ctx->flags = 0;
+ for (i = 0; list[i].type != IMAP_ARG_EOL; i += 2) {
+ if (!imap_arg_get_atom(&list[i], &atom) ||
+ list[i+1].type == IMAP_ARG_EOL)
+ return FALSE;
+
+ if (strcasecmp(atom, "UID") == 0) {
+ if (!imap_arg_get_atom(&list[i+1], &atom) ||
+ str_to_uint32(atom, &ctx->fetch_uid) < 0)
+ return FALSE;
+ } else if (strcasecmp(atom, "FLAGS") == 0) {
+ if (!imap_arg_get_list(&list[i+1], &flags_list))
+ return FALSE;
+
+ p_array_init(&ctx->keywords, ctx->pool, 8);
+ ctx->have_flags = TRUE;
+ for (j = 0; flags_list[j].type != IMAP_ARG_EOL; j++) {
+ if (!imap_arg_get_atom(&flags_list[j], &atom))
+ return FALSE;
+ if (atom[0] == '\\')
+ ctx->flags |= imap_parse_system_flag(atom);
+ else {
+ patom = p_strdup(ctx->pool, atom);
+ /* keyword */
+ array_push_back(&ctx->keywords, &patom);
+ }
+ }
+ } else if (strcasecmp(atom, "MODSEQ") == 0 &&
+ imapc_mailbox_has_modseqs(mbox)) {
+ /* (modseq-number) */
+ if (!imap_arg_get_list(&list[i+1], &modseq_list))
+ return FALSE;
+ if (!imap_arg_get_atom(&modseq_list[0], &atom) ||
+ str_to_uint64(atom, &ctx->modseq) < 0 ||
+ modseq_list[1].type != IMAP_ARG_EOL)
+ return FALSE;
+ } else if (strcasecmp(atom, "X-GM-MSGID") == 0 &&
+ !mbox->initial_sync_done) {
+ if (imap_arg_get_atom(&list[i+1], &atom))
+ ctx->guid = atom;
+ } else if (strcasecmp(atom, "X-GM-LABELS") == 0 &&
+ IMAPC_BOX_HAS_FEATURE(mbox, IMAPC_FEATURE_GMAIL_MIGRATION)) {
+ if (!imap_arg_get_list(&list[i+1], &flags_list))
+ return FALSE;
+ for (j = 0; flags_list[j].type != IMAP_ARG_EOL; j++) {
+ if (!imap_arg_get_astring(&flags_list[j], &atom))
+ return FALSE;
+ if (strcasecmp(atom, "\\Muted") != 0)
+ ctx->have_gmail_labels = TRUE;
+ }
+ }
+ }
+ if (ctx->fetch_uid == 0 &&
+ IMAPC_BOX_HAS_FEATURE(mbox, IMAPC_FEATURE_NO_MSN_UPDATES)) {
+ /* UID missing and we're not tracking MSNs */
+ return FALSE;
+ }
+
+ return TRUE;
+}
+
+static void imapc_untagged_fetch(const struct imapc_untagged_reply *reply,
+ struct imapc_mailbox *mbox)
+{
+ const struct imap_arg *list;
+ struct imapc_fetch_request *fetch_request;
+ struct imapc_mail *mail;
+ bool new_message = FALSE;
+
+ if (mbox == NULL || reply->num == 0 || !imap_arg_get_list(reply->args, &list))
+ return;
+
+ struct imapc_untagged_fetch_ctx *ctx =
+ imapc_untagged_fetch_ctx_create();
+ if (!imapc_untagged_fetch_parse(mbox, ctx, list)) {
+ imapc_untagged_fetch_ctx_free(&ctx);
+ return;
+ }
+
+ new_message = imapc_untagged_fetch_handle(mbox, ctx, reply->num);
+
+ /* if this is a reply to some FETCH request, update the mail's fields */
+ array_foreach_elem(&mbox->fetch_requests, fetch_request) {
+ array_foreach_elem(&fetch_request->mails, mail) {
+ if (mail->imail.mail.mail.uid == ctx->uid)
+ imapc_mail_fetch_update(mail, reply, list);
+ }
+ }
+
+ if (!new_message) {
+ /* Handling this context is finished if the mail was not new
+ to the local index. It has not been added to
+ mbox->untagged_fetch_contexts so no need to delete it from
+ the array. The context itself can be freed here. */
+ imapc_untagged_fetch_ctx_free(&ctx);
+ } else {
+ /* If this is a new message store this context to be handled
+ when syncing */
+ array_push_back(&mbox->untagged_fetch_contexts, &ctx);
+ }
+ imapc_mailbox_idle_notify(mbox);
+}
+
+static void imapc_untagged_expunge(const struct imapc_untagged_reply *reply,
+ struct imapc_mailbox *mbox)
+{
+ struct imapc_msgmap *msgmap;
+ uint32_t uid, rseq = reply->num;
+
+ if (mbox == NULL || rseq == 0 ||
+ IMAPC_BOX_HAS_FEATURE(mbox, IMAPC_FEATURE_NO_MSN_UPDATES))
+ return;
+
+ mbox->prev_skipped_rseq = 0;
+ mbox->prev_skipped_uid = 0;
+
+ if (mbox->exists_count == 0) {
+ imapc_mailbox_set_corrupted(mbox,
+ "EXPUNGE received for empty mailbox");
+ return;
+ }
+ mbox->exists_count--;
+
+ msgmap = imapc_client_mailbox_get_msgmap(mbox->client_box);
+ if (rseq > imapc_msgmap_count(msgmap)) {
+ /* we haven't even seen this message yet */
+ return;
+ }
+ uid = imapc_msgmap_rseq_to_uid(msgmap, rseq);
+ imapc_msgmap_expunge(msgmap, rseq);
+ if (array_is_created(&mbox->rseq_modseqs))
+ array_delete(&mbox->rseq_modseqs, rseq-1, 1);
+
+ imapc_mailbox_init_delayed_trans(mbox);
+ imapc_mailbox_index_expunge(mbox, uid);
+ imapc_mailbox_idle_notify(mbox);
+}
+
+static void
+imapc_untagged_esearch_gmail_pop3(const struct imap_arg *args,
+ struct imapc_mailbox *mbox)
+{
+ struct imapc_msgmap *msgmap;
+ const char *atom;
+ struct seq_range_iter iter;
+ ARRAY_TYPE(seq_range) rseqs;
+ unsigned int n;
+ uint32_t rseq, lseq, uid;
+ ARRAY_TYPE(keyword_indexes) keywords;
+ struct mail_keywords *kw;
+ unsigned int pop3_deleted_kw_idx;
+
+ i_free_and_null(mbox->sync_gmail_pop3_search_tag);
+
+ /* It should contain ALL <seqset> or nonexistent if nothing matched */
+ if (args[0].type == IMAP_ARG_EOL)
+ return;
+ t_array_init(&rseqs, 64);
+ if (!imap_arg_atom_equals(&args[0], "ALL") ||
+ !imap_arg_get_atom(&args[1], &atom) ||
+ imap_seq_set_nostar_parse(atom, &rseqs) < 0) {
+ i_error("Invalid gmail-pop3 ESEARCH reply");
+ return;
+ }
+
+ mail_index_keyword_lookup_or_create(mbox->box.index,
+ mbox->storage->set->pop3_deleted_flag, &pop3_deleted_kw_idx);
+
+ t_array_init(&keywords, 1);
+ array_push_back(&keywords, &pop3_deleted_kw_idx);
+ kw = mail_index_keywords_create_from_indexes(mbox->box.index, &keywords);
+
+ msgmap = imapc_client_mailbox_get_msgmap(mbox->client_box);
+ seq_range_array_iter_init(&iter, &rseqs); n = 0;
+ while (seq_range_array_iter_nth(&iter, n++, &rseq)) {
+ if (rseq > imapc_msgmap_count(msgmap)) {
+ /* we haven't even seen this message yet */
+ break;
+ }
+ uid = imapc_msgmap_rseq_to_uid(msgmap, rseq);
+ if (!mail_index_lookup_seq(mbox->delayed_sync_view,
+ uid, &lseq))
+ continue;
+
+ /* add the pop3_deleted_flag */
+ mail_index_update_keywords(mbox->delayed_sync_trans,
+ lseq, MODIFY_ADD, kw);
+ }
+ mail_index_keywords_unref(&kw);
+}
+
+static void imapc_untagged_search(const struct imapc_untagged_reply *reply,
+ struct imapc_mailbox *mbox)
+{
+ if (mbox != NULL)
+ imapc_search_reply_search(reply->args, mbox);
+}
+
+static void imapc_untagged_esearch(const struct imapc_untagged_reply *reply,
+ struct imapc_mailbox *mbox)
+{
+ const struct imap_arg *tag_list;
+ const char *str;
+
+ if (mbox == NULL || !imap_arg_get_list(reply->args, &tag_list))
+ return;
+
+ /* ESEARCH begins with (TAG <tag>) */
+ if (!imap_arg_atom_equals(&tag_list[0], "TAG") ||
+ !imap_arg_get_string(&tag_list[1], &str) ||
+ tag_list[2].type != IMAP_ARG_EOL)
+ return;
+
+ /* for now the only ESEARCH reply that we have is for getting GMail's
+ list of hidden POP3 messages. */
+ if (mbox->sync_gmail_pop3_search_tag != NULL &&
+ strcmp(mbox->sync_gmail_pop3_search_tag, str) == 0)
+ imapc_untagged_esearch_gmail_pop3(reply->args+1, mbox);
+ else
+ imapc_search_reply_esearch(reply->args+1, mbox);
+}
+
+static void imapc_sync_uid_validity(struct imapc_mailbox *mbox)
+{
+ const struct mail_index_header *hdr;
+
+ imapc_mailbox_init_delayed_trans(mbox);
+ hdr = mail_index_get_header(mbox->delayed_sync_view);
+ if (hdr->uid_validity != mbox->sync_uid_validity &&
+ mbox->sync_uid_validity != 0) {
+ if (hdr->uid_validity != 0) {
+ /* uidvalidity changed, reset the entire mailbox */
+ mail_index_reset(mbox->delayed_sync_trans);
+ mbox->sync_fetch_first_uid = 1;
+ /* The reset needs to be committed before FETCH 1:*
+ results are received. */
+ bool changes;
+ if (imapc_mailbox_commit_delayed_trans(mbox, TRUE, &changes) < 0)
+ mail_index_mark_corrupted(mbox->box.index);
+ imapc_mailbox_init_delayed_trans(mbox);
+ }
+ mail_index_update_header(mbox->delayed_sync_trans,
+ offsetof(struct mail_index_header, uid_validity),
+ &mbox->sync_uid_validity,
+ sizeof(mbox->sync_uid_validity), TRUE);
+ }
+}
+
+static void
+imapc_resp_text_uidvalidity(const struct imapc_untagged_reply *reply,
+ struct imapc_mailbox *mbox)
+{
+ uint32_t uid_validity;
+
+ if (mbox == NULL ||
+ str_to_uint32(reply->resp_text_value, &uid_validity) < 0 ||
+ uid_validity == 0)
+ return;
+
+ if (mbox->sync_uid_validity != uid_validity) {
+ mbox->sync_uid_validity = uid_validity;
+ imapc_mail_cache_free(&mbox->prev_mail_cache);
+ imapc_sync_uid_validity(mbox);
+ }
+}
+
+static void
+imapc_resp_text_uidnext(const struct imapc_untagged_reply *reply,
+ struct imapc_mailbox *mbox)
+{
+ uint32_t uid_next;
+
+ if (mbox == NULL ||
+ str_to_uint32(reply->resp_text_value, &uid_next) < 0)
+ return;
+
+ mbox->sync_uid_next = uid_next;
+}
+
+static void
+imapc_resp_text_highestmodseq(const struct imapc_untagged_reply *reply,
+ struct imapc_mailbox *mbox)
+{
+ uint64_t highestmodseq;
+
+ if (mbox == NULL ||
+ str_to_uint64(reply->resp_text_value, &highestmodseq) < 0)
+ return;
+
+ mbox->sync_highestmodseq = highestmodseq;
+}
+
+static void
+imapc_resp_text_permanentflags(const struct imapc_untagged_reply *reply,
+ struct imapc_mailbox *mbox)
+{
+ const struct imap_arg *flags_args, *arg;
+ const char *flag;
+ unsigned int idx;
+
+ i_assert(reply->args[0].type == IMAP_ARG_ATOM);
+
+ if (mbox == NULL || !imap_arg_get_list(&reply->args[1], &flags_args))
+ return;
+
+ mbox->permanent_flags = 0;
+ mbox->box.disallow_new_keywords = TRUE;
+
+ for (arg = flags_args; arg->type != IMAP_ARG_EOL; arg++) {
+ if (!imap_arg_get_atom(arg, &flag))
+ continue;
+
+ if (strcmp(flag, "\\*") == 0)
+ mbox->box.disallow_new_keywords = FALSE;
+ else if (*flag == '\\')
+ mbox->permanent_flags |= imap_parse_system_flag(flag);
+ else {
+ /* we'll simply make sure that it exists in the index */
+ mail_index_keyword_lookup_or_create(mbox->box.index,
+ flag, &idx);
+ }
+ }
+}
+
+void imapc_mailbox_register_untagged(struct imapc_mailbox *mbox,
+ const char *key,
+ imapc_mailbox_callback_t *callback)
+{
+ struct imapc_mailbox_event_callback *cb;
+
+ cb = array_append_space(&mbox->untagged_callbacks);
+ cb->name = p_strdup(mbox->box.pool, key);
+ cb->callback = callback;
+}
+
+void imapc_mailbox_register_resp_text(struct imapc_mailbox *mbox,
+ const char *key,
+ imapc_mailbox_callback_t *callback)
+{
+ struct imapc_mailbox_event_callback *cb;
+
+ cb = array_append_space(&mbox->resp_text_callbacks);
+ cb->name = p_strdup(mbox->box.pool, key);
+ cb->callback = callback;
+}
+
+void imapc_mailbox_register_callbacks(struct imapc_mailbox *mbox)
+{
+ imapc_mailbox_register_untagged(mbox, "EXISTS",
+ imapc_untagged_exists);
+ imapc_mailbox_register_untagged(mbox, "FETCH",
+ imapc_untagged_fetch);
+ imapc_mailbox_register_untagged(mbox, "EXPUNGE",
+ imapc_untagged_expunge);
+ imapc_mailbox_register_untagged(mbox, "SEARCH",
+ imapc_untagged_search);
+ imapc_mailbox_register_untagged(mbox, "ESEARCH",
+ imapc_untagged_esearch);
+ imapc_mailbox_register_resp_text(mbox, "UIDVALIDITY",
+ imapc_resp_text_uidvalidity);
+ imapc_mailbox_register_resp_text(mbox, "UIDNEXT",
+ imapc_resp_text_uidnext);
+ imapc_mailbox_register_resp_text(mbox, "HIGHESTMODSEQ",
+ imapc_resp_text_highestmodseq);
+ imapc_mailbox_register_resp_text(mbox, "PERMANENTFLAGS",
+ imapc_resp_text_permanentflags);
+}
diff --git a/src/lib-storage/index/imapc/imapc-save.c b/src/lib-storage/index/imapc/imapc-save.c
new file mode 100644
index 0000000..c50f46b
--- /dev/null
+++ b/src/lib-storage/index/imapc/imapc-save.c
@@ -0,0 +1,829 @@
+/* Copyright (c) 2011-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "str.h"
+#include "istream.h"
+#include "istream-crlf.h"
+#include "ostream.h"
+#include "imap-date.h"
+#include "imap-util.h"
+#include "imap-seqset.h"
+#include "imap-quote.h"
+#include "index-mail.h"
+#include "mail-copy.h"
+#include "mailbox-list-private.h"
+#include "imapc-msgmap.h"
+#include "imapc-storage.h"
+#include "imapc-sync.h"
+#include "imapc-mail.h"
+#include "seq-set-builder.h"
+
+struct imapc_save_context {
+ struct mail_save_context ctx;
+
+ struct imapc_mailbox *mbox;
+ struct imapc_mailbox *src_mbox;
+ struct mail_index_transaction *trans;
+
+ int fd;
+ char *temp_path;
+ struct istream *input;
+
+ uint32_t dest_uid_validity;
+ ARRAY_TYPE(seq_range) dest_saved_uids;
+ unsigned int save_count;
+
+ bool failed:1;
+ bool finished:1;
+};
+
+struct imapc_save_cmd_context {
+ struct imapc_save_context *ctx;
+ int ret;
+};
+
+#define IMAPC_SAVECTX(s) container_of(s, struct imapc_save_context, ctx)
+#define IMAPC_SERVER_CMDLINE_MAX_LEN 8000
+
+void imapc_transaction_save_rollback(struct mail_save_context *_ctx);
+static void imapc_mail_copy_bulk_flush(struct imapc_mailbox *mbox);
+
+struct mail_save_context *
+imapc_save_alloc(struct mailbox_transaction_context *t)
+{
+ struct imapc_mailbox *mbox = IMAPC_MAILBOX(t->box);
+ struct imapc_save_context *ctx;
+
+ i_assert((t->flags & MAILBOX_TRANSACTION_FLAG_EXTERNAL) != 0);
+
+ if (t->save_ctx == NULL) {
+ ctx = i_new(struct imapc_save_context, 1);
+ ctx->ctx.transaction = t;
+ ctx->mbox = mbox;
+ ctx->src_mbox = NULL;
+ ctx->trans = t->itrans;
+ ctx->fd = -1;
+ t->save_ctx = &ctx->ctx;
+ }
+ return t->save_ctx;
+}
+
+int imapc_save_begin(struct mail_save_context *_ctx, struct istream *input)
+{
+ struct imapc_save_context *ctx = IMAPC_SAVECTX(_ctx);
+ const char *path;
+
+ i_assert(ctx->fd == -1);
+
+ if (imapc_storage_client_handle_auth_failure(ctx->mbox->storage->client))
+ return -1;
+
+ ctx->fd = imapc_client_create_temp_fd(ctx->mbox->storage->client->client,
+ &path);
+ if (ctx->fd == -1) {
+ mail_set_critical(_ctx->dest_mail,
+ "Couldn't create temp file %s", path);
+ ctx->failed = TRUE;
+ return -1;
+ }
+ /* we may not know the size of the input, or be sure that it contains
+ only CRLFs. so we'll always first write the mail to a temp file and
+ upload it from there to remote server. */
+ ctx->finished = FALSE;
+ ctx->temp_path = i_strdup(path);
+ ctx->input = i_stream_create_crlf(input);
+ _ctx->data.output = o_stream_create_fd_file(ctx->fd, 0, FALSE);
+ o_stream_cork(_ctx->data.output);
+ return 0;
+}
+
+int imapc_save_continue(struct mail_save_context *_ctx)
+{
+ struct imapc_save_context *ctx = IMAPC_SAVECTX(_ctx);
+
+ if (ctx->failed)
+ return -1;
+
+ if (index_storage_save_continue(_ctx, ctx->input, NULL) < 0) {
+ ctx->failed = TRUE;
+ return -1;
+ }
+ return 0;
+}
+
+static void imapc_save_appenduid(struct imapc_save_context *ctx,
+ const struct imapc_command_reply *reply,
+ uint32_t *uid_r)
+{
+ const char *const *args;
+ uint32_t uid_validity, dest_uid;
+
+ *uid_r = 0;
+
+ /* <uidvalidity> <dest uid-set> */
+ args = t_strsplit(reply->resp_text_value, " ");
+ if (str_array_length(args) != 2)
+ return;
+
+ if (str_to_uint32(args[0], &uid_validity) < 0)
+ return;
+ if (ctx->dest_uid_validity == 0)
+ ctx->dest_uid_validity = uid_validity;
+ else if (ctx->dest_uid_validity != uid_validity)
+ return;
+
+ if (str_to_uint32(args[1], &dest_uid) == 0) {
+ seq_range_array_add_with_init(&ctx->dest_saved_uids,
+ 32, dest_uid);
+ *uid_r = dest_uid;
+ }
+}
+
+static void
+imapc_save_add_to_index(struct imapc_save_context *ctx, uint32_t uid)
+{
+ struct mail *_mail = ctx->ctx.dest_mail;
+ struct index_mail *imail = INDEX_MAIL(_mail);
+ uint32_t seq;
+
+ /* we'll temporarily append messages and at commit time expunge
+ them all, since we can't guarantee that no one else has saved
+ messages to remote server during our transaction */
+ mail_index_append(ctx->trans, uid, &seq);
+ mail_set_seq_saving(_mail, seq);
+ imail->data.no_caching = TRUE;
+ imail->data.forced_no_caching = TRUE;
+
+ if (ctx->fd != -1) {
+ struct imapc_mail *imapc_mail = IMAPC_MAIL(_mail);
+ imail->data.stream = i_stream_create_fd_autoclose(&ctx->fd, 0);
+ imapc_mail->header_fetched = TRUE;
+ imapc_mail->body_fetched = TRUE;
+ /* The saved stream wasn't actually read, but it needs to be
+ set accessed to avoid assert-crash. */
+ _mail->mail_stream_accessed = TRUE;
+ imapc_mail_init_stream(imapc_mail);
+ }
+
+ ctx->save_count++;
+}
+
+static void imapc_save_callback(const struct imapc_command_reply *reply,
+ void *context)
+{
+ struct imapc_save_cmd_context *ctx = context;
+ uint32_t uid = 0;
+
+ if (reply->state == IMAPC_COMMAND_STATE_OK) {
+ if (reply->resp_text_key != NULL &&
+ strcasecmp(reply->resp_text_key, "APPENDUID") == 0)
+ imapc_save_appenduid(ctx->ctx, reply, &uid);
+ imapc_save_add_to_index(ctx->ctx, uid);
+ ctx->ret = 0;
+ } else if (imapc_storage_client_handle_auth_failure(ctx->ctx->mbox->storage->client)) {
+ ctx->ret = -1;
+ } else if (reply->state == IMAPC_COMMAND_STATE_NO) {
+ imapc_copy_error_from_reply(ctx->ctx->mbox->storage,
+ MAIL_ERROR_PARAMS, reply);
+ ctx->ret = -1;
+ } else {
+ mailbox_set_critical(&ctx->ctx->mbox->box,
+ "imapc: APPEND failed: %s", reply->text_full);
+ ctx->ret = -1;
+ }
+ imapc_client_stop(ctx->ctx->mbox->storage->client->client);
+}
+
+static void
+imapc_save_noop_callback(const struct imapc_command_reply *reply ATTR_UNUSED,
+ void *context)
+{
+ struct imapc_save_cmd_context *ctx = context;
+
+ /* we don't really care about the reply */
+ ctx->ret = 0;
+ imapc_client_stop(ctx->ctx->mbox->storage->client->client);
+}
+
+static void
+imapc_copy_rollback_store_callback(const struct imapc_command_reply *reply ATTR_UNUSED,
+ void *context)
+{
+ struct imapc_save_context *ctx = context;
+ /* Can't do much about a non successful STORE here */
+ if (reply->state != IMAPC_COMMAND_STATE_OK) {
+ e_error(ctx->src_mbox->box.event,
+ "imapc: Failed to set \\Deleted flag for rolling back "
+ "failed copy: %s", reply->text_full);
+ ctx->src_mbox->rollback_pending = FALSE;
+ ctx->finished = TRUE;
+ ctx->failed = TRUE;
+ } else {
+ i_assert(ctx->src_mbox->rollback_pending);
+ }
+ /* No need stop the imapc client here there is always an additional
+ expunge callback after this. */
+}
+
+static void
+imapc_copy_rollback_expunge_callback(const struct imapc_command_reply *reply ATTR_UNUSED,
+ void *context)
+{
+ struct imapc_save_context *ctx = context;
+
+ /* Can't do much about a non successful EXPUNGE here */
+ if (reply->state != IMAPC_COMMAND_STATE_OK) {
+ e_error(ctx->src_mbox->box.event,
+ "imapc: Failed to expunge messages for rolling back "
+ "failed copy: %s", reply->text_full);
+ ctx->src_mbox->rollback_pending = FALSE;
+ ctx->finished = TRUE;
+ ctx->failed = TRUE;
+ } else {
+ ctx->finished = TRUE;
+ ctx->src_mbox->rollback_pending = FALSE;
+ }
+ imapc_client_stop(ctx->src_mbox->storage->client->client);
+}
+
+static void
+imapc_append_keywords(string_t *str, struct mail_keywords *kw)
+{
+ const ARRAY_TYPE(keywords) *kw_arr;
+ const char *kw_str;
+ unsigned int i;
+
+ kw_arr = mail_index_get_keywords(kw->index);
+ for (i = 0; i < kw->count; i++) {
+ kw_str = array_idx_elem(kw_arr, kw->idx[i]);
+ if (str_len(str) > 1)
+ str_append_c(str, ' ');
+ str_append(str, kw_str);
+ }
+}
+
+static int imapc_save_append(struct imapc_save_context *ctx)
+{
+ struct mail_save_context *_ctx = &ctx->ctx;
+ struct mail_save_data *mdata = &_ctx->data;
+ struct imapc_command *cmd;
+ struct imapc_save_cmd_context sctx;
+ struct istream *input;
+ const char *flags = "", *internaldate = "";
+
+ if (mdata->flags != 0 || mdata->keywords != NULL) {
+ string_t *str = t_str_new(64);
+
+ str_append(str, " (");
+ imap_write_flags(str, mdata->flags & ENUM_NEGATE(MAIL_RECENT),
+ NULL);
+ if (mdata->keywords != NULL)
+ imapc_append_keywords(str, mdata->keywords);
+ str_append_c(str, ')');
+ flags = str_c(str);
+ }
+ if (mdata->received_date != (time_t)-1) {
+ internaldate = t_strdup_printf(" \"%s\"",
+ imap_to_datetime(mdata->received_date));
+ }
+
+ ctx->mbox->exists_received = FALSE;
+
+ input = i_stream_create_fd(ctx->fd, IO_BLOCK_SIZE);
+ sctx.ctx = ctx;
+ sctx.ret = -2;
+ cmd = imapc_client_cmd(ctx->mbox->storage->client->client,
+ imapc_save_callback, &sctx);
+ imapc_command_sendf(cmd, "APPEND %s%1s%1s %p",
+ imapc_mailbox_get_remote_name(ctx->mbox),
+ flags, internaldate, input);
+ i_stream_unref(&input);
+ while (sctx.ret == -2)
+ imapc_mailbox_run(ctx->mbox);
+
+ if (sctx.ret == 0 && ctx->mbox->selected &&
+ !ctx->mbox->exists_received) {
+ /* e.g. Courier doesn't send EXISTS reply before the tagged
+ APPEND reply. That isn't exactly required by the IMAP RFC,
+ but it makes the behavior better. See if NOOP finds
+ the mail. */
+ sctx.ret = -2;
+ cmd = imapc_client_cmd(ctx->mbox->storage->client->client,
+ imapc_save_noop_callback, &sctx);
+ imapc_command_set_flags(cmd, IMAPC_COMMAND_FLAG_RETRIABLE);
+ imapc_command_send(cmd, "NOOP");
+ while (sctx.ret == -2)
+ imapc_mailbox_run(ctx->mbox);
+ }
+ return sctx.ret;
+}
+
+int imapc_save_finish(struct mail_save_context *_ctx)
+{
+ struct imapc_save_context *ctx = IMAPC_SAVECTX(_ctx);
+ struct mail_storage *storage = _ctx->transaction->box->storage;
+
+ ctx->finished = TRUE;
+
+ if (!ctx->failed) {
+ if (o_stream_finish(_ctx->data.output) < 0) {
+ if (!mail_storage_set_error_from_errno(storage)) {
+ mail_set_critical(_ctx->dest_mail,
+ "write(%s) failed: %s", ctx->temp_path,
+ o_stream_get_error(_ctx->data.output));
+ }
+ ctx->failed = TRUE;
+ }
+ }
+
+ if (!ctx->failed) {
+ if (imapc_save_append(ctx) < 0)
+ ctx->failed = TRUE;
+ }
+
+ o_stream_unref(&_ctx->data.output);
+ i_stream_unref(&ctx->input);
+ i_close_fd_path(&ctx->fd, ctx->temp_path);
+ i_free(ctx->temp_path);
+ index_save_context_free(_ctx);
+ return ctx->failed ? -1 : 0;
+}
+
+void imapc_save_cancel(struct mail_save_context *_ctx)
+{
+ struct imapc_save_context *ctx = IMAPC_SAVECTX(_ctx);
+
+ ctx->failed = TRUE;
+ (void)imapc_transaction_save_commit_pre(_ctx);
+ (void)imapc_save_finish(_ctx);
+}
+
+static void imapc_copy_bulk_finish(struct imapc_save_context *ctx)
+{
+ while (ctx->src_mbox != NULL && ctx->src_mbox->pending_copy_request != NULL)
+ imapc_mailbox_run_nofetch(ctx->src_mbox);
+}
+
+int imapc_transaction_save_commit_pre(struct mail_save_context *_ctx)
+{
+ struct imapc_save_context *ctx = IMAPC_SAVECTX(_ctx);
+ struct mail_transaction_commit_changes *changes =
+ _ctx->transaction->changes;
+ uint32_t i, last_seq;
+
+ i_assert(ctx->finished || ctx->failed);
+
+ /* expunge all added messages from index before commit */
+ last_seq = mail_index_view_get_messages_count(_ctx->transaction->view);
+ if (last_seq == 0)
+ return -1;
+ for (i = 0; i < ctx->save_count; i++)
+ mail_index_expunge(ctx->trans, last_seq - i);
+
+ if (!ctx->failed && array_is_created(&ctx->dest_saved_uids)) {
+ changes->uid_validity = ctx->dest_uid_validity;
+ array_append_array(&changes->saved_uids, &ctx->dest_saved_uids);
+ }
+ return 0;
+}
+
+int imapc_transaction_save_commit(struct mailbox_transaction_context *t)
+{
+ struct imapc_save_context *ctx = NULL;
+ struct imapc_mailbox *src_mbox = NULL;
+
+ if (t->save_ctx != NULL) {
+ ctx = IMAPC_SAVECTX(t->save_ctx);
+ src_mbox = ctx->src_mbox;
+ }
+
+ if (src_mbox != NULL && src_mbox->pending_copy_request != NULL) {
+ /* If there is still a copy command to send flush it now */
+ imapc_mail_copy_bulk_flush(src_mbox);
+ imapc_copy_bulk_finish(ctx);
+ }
+
+ if (ctx != NULL)
+ return ctx->failed ? -1 : 0;
+ return 0;
+}
+
+void imapc_transaction_save_commit_post(struct mail_save_context *_ctx,
+ struct mail_index_transaction_commit_result *result ATTR_UNUSED)
+{
+ imapc_transaction_save_rollback(_ctx);
+}
+
+static void
+imapc_expunge_construct_cmd_str(string_t *store_cmd,
+ string_t *expunge_cmd,
+ string_t *uids)
+{
+ str_append(store_cmd, "UID STORE ");
+ str_append_str(store_cmd, uids);
+ str_append(store_cmd, " +FLAGS (\\Deleted)");
+ str_append(expunge_cmd, "UID EXPUNGE ");
+ str_append_str(expunge_cmd, uids);
+ /* Clear already appened uids */
+ str_truncate(uids, 0);
+}
+
+static void
+imapc_expunge_send_cmd_str(struct imapc_save_context *ctx,
+ string_t *uids)
+{
+ struct imapc_command *store_cmd, *expunge_cmd;
+
+ string_t *store_cmd_str, *expunge_cmd_str;
+ store_cmd_str = t_str_new(128);
+ expunge_cmd_str = t_str_new(128);
+
+ imapc_expunge_construct_cmd_str(store_cmd_str, expunge_cmd_str, uids);
+ /* Make sure line length is less than 8k */
+ i_assert(str_len(store_cmd_str) < IMAPC_SERVER_CMDLINE_MAX_LEN);
+ i_assert(str_len(expunge_cmd_str) < IMAPC_SERVER_CMDLINE_MAX_LEN);
+
+ store_cmd = imapc_client_mailbox_cmd(ctx->src_mbox->client_box,
+ imapc_copy_rollback_store_callback,
+ ctx);
+ expunge_cmd = imapc_client_mailbox_cmd(ctx->src_mbox->client_box,
+ imapc_copy_rollback_expunge_callback,
+ ctx);
+ ctx->src_mbox->rollback_pending = TRUE;
+ imapc_command_send(store_cmd, str_c(store_cmd_str));
+ imapc_command_send(expunge_cmd, str_c(expunge_cmd_str));
+}
+
+static void
+imapc_rollback_send_expunge(struct imapc_save_context *ctx)
+{
+ string_t *uids_str;
+ struct seqset_builder *seqset_builder;
+ struct seq_range_iter iter;
+ unsigned int i = 0;
+ uint32_t uid;
+
+ if (!array_not_empty(&ctx->src_mbox->copy_rollback_expunge_uids))
+ return;
+
+ uids_str = t_str_new(128);
+ seqset_builder = seqset_builder_init(uids_str);
+ seq_range_array_iter_init(&iter, &ctx->src_mbox->copy_rollback_expunge_uids);
+
+ /* Iterate over all uids that must be rolled back */
+ while (seq_range_array_iter_nth(&iter, i++, &uid)) {
+ /* Try to add the to the seqset builder while respecting
+ the maximum length of IMAPC_SERVER_CMDLINE_MAX_LEN. */
+ if (!seqset_builder_try_add(seqset_builder,
+ IMAPC_SERVER_CMDLINE_MAX_LEN -
+ strlen("UID STORE +FLAGS (\\Deleted)"),
+ uid)) {
+ /* Maximum length is reached send the rollback
+ and wait for it to be finished. */
+ imapc_expunge_send_cmd_str(ctx, uids_str);
+ while (ctx->src_mbox->rollback_pending)
+ imapc_mailbox_run_nofetch(ctx->src_mbox);
+
+ /* Truncate the uids_str and create a new
+ seqset_builder for the next command */
+ seqset_builder_deinit(&seqset_builder);
+ str_truncate(uids_str, 0);
+ seqset_builder = seqset_builder_init(uids_str);
+ /* Make sure the current uid which is part of
+ the next uid_str */
+ seqset_builder_add(seqset_builder, uid);
+ }
+ }
+ if (str_len(uids_str) > 0)
+ imapc_expunge_send_cmd_str(ctx, uids_str);
+ while (ctx->src_mbox->rollback_pending)
+ imapc_mailbox_run_nofetch(ctx->src_mbox);
+}
+
+static void imapc_copy_bulk_ctx_deinit(struct imapc_save_context *ctx)
+{
+ /* Clean up the pending copy and the context attached to it */
+ str_truncate(ctx->src_mbox->pending_copy_cmd, 0);
+ i_free(ctx->src_mbox->copy_dest_box);
+}
+
+void imapc_transaction_save_rollback(struct mail_save_context *_ctx)
+{
+ struct imapc_save_context *ctx = IMAPC_SAVECTX(_ctx);
+
+ if ((ctx->src_mbox != NULL && ctx->src_mbox->pending_copy_request != NULL) ||
+ !ctx->finished) {
+ /* There is still a pending copy which should not be send
+ as rollback() is called or the transaction has not yet
+ finished and rollback is called */
+ ctx->failed = TRUE;
+ (void)imapc_transaction_save_commit_pre(_ctx);
+
+ i_assert(ctx->finished || ctx->src_mbox != NULL);
+ /* Clean up the pending copy and the context attached to it */
+ if (ctx->src_mbox != NULL) {
+ if (ctx->src_mbox->pending_copy_request != NULL) {
+ seqset_builder_deinit(&ctx->src_mbox->pending_copy_request->uidset_builder);
+ i_free(ctx->src_mbox->pending_copy_request);
+ }
+ imapc_copy_bulk_ctx_deinit(ctx);
+ imapc_client_stop(ctx->src_mbox->storage->client->client);
+ }
+ }
+
+ /* Expunge all added messages from index */
+ if (ctx->failed && array_is_created(&ctx->dest_saved_uids)) {
+ i_assert(ctx->src_mbox != NULL);
+ seq_range_array_merge(&ctx->src_mbox->copy_rollback_expunge_uids, &ctx->dest_saved_uids);
+ /* Make sure context is not finished already */
+ ctx->finished = FALSE;
+ imapc_rollback_send_expunge(ctx);
+ array_free(&ctx->dest_saved_uids);
+ }
+
+ if (ctx->finished || ctx->failed) {
+ array_free(&ctx->dest_saved_uids);
+ i_free(ctx);
+ }
+}
+
+static bool imapc_save_copyuid(struct imapc_save_context *ctx,
+ const struct imapc_command_reply *reply,
+ uint32_t *uid_r)
+{
+ ARRAY_TYPE(seq_range) dest_uidset, source_uidset;
+ struct seq_range_iter iter;
+ const char *const *args;
+ uint32_t uid_validity;
+
+ *uid_r = 0;
+
+ /* <uidvalidity> <source uid-set> <dest uid-set> */
+ args = t_strsplit(reply->resp_text_value, " ");
+ if (str_array_length(args) != 3)
+ return FALSE;
+
+ if (str_to_uint32(args[0], &uid_validity) < 0)
+ return FALSE;
+ if (ctx->dest_uid_validity == 0)
+ ctx->dest_uid_validity = uid_validity;
+ else if (ctx->dest_uid_validity != uid_validity)
+ return FALSE;
+
+ t_array_init(&source_uidset, 8);
+ t_array_init(&dest_uidset, 8);
+
+ if (imap_seq_set_nostar_parse(args[1], &source_uidset) < 0)
+ return FALSE;
+ if (imap_seq_set_nostar_parse(args[2], &dest_uidset) < 0)
+ return FALSE;
+
+ if (!array_is_created(&ctx->dest_saved_uids))
+ i_array_init(&ctx->dest_saved_uids, 8);
+
+ seq_range_array_merge(&ctx->dest_saved_uids, &dest_uidset);
+
+ seq_range_array_iter_init(&iter, &dest_uidset);
+ (void)seq_range_array_iter_nth(&iter, 0, uid_r);
+ return TRUE;
+}
+
+static void imapc_copy_set_error(struct imapc_save_context *sctx,
+ const struct imapc_command_reply *reply)
+{
+ sctx->failed = TRUE;
+
+ if (reply->state != IMAPC_COMMAND_STATE_BAD)
+ imapc_copy_error_from_reply(sctx->mbox->storage,
+ MAIL_ERROR_PARAMS, reply);
+ else
+ mailbox_set_critical(&sctx->mbox->box,
+ "imapc: COPY failed: %s",
+ reply->text_full);
+}
+
+static void
+imapc_copy_simple_callback(const struct imapc_command_reply *reply,
+ void *context)
+{
+ struct imapc_save_cmd_context *ctx = context;
+ uint32_t uid = 0;
+
+ if (reply->state == IMAPC_COMMAND_STATE_OK) {
+ if (reply->resp_text_key != NULL &&
+ strcasecmp(reply->resp_text_key, "COPYUID") == 0)
+ imapc_save_copyuid(ctx->ctx, reply, &uid);
+ imapc_save_add_to_index(ctx->ctx, uid);
+ ctx->ret = 0;
+ } else if (reply->state == IMAPC_COMMAND_STATE_NO) {
+ imapc_copy_error_from_reply(ctx->ctx->mbox->storage,
+ MAIL_ERROR_PARAMS, reply);
+ ctx->ret = -1;
+ } else {
+ mailbox_set_critical(&ctx->ctx->mbox->box,
+ "imapc: COPY failed: %s", reply->text_full);
+ ctx->ret = -1;
+ }
+ imapc_client_stop(ctx->ctx->mbox->storage->client->client);
+}
+
+static int
+imapc_copy_simple(struct mail_save_context *_ctx, struct mail *mail)
+{
+ struct imapc_save_context *ctx = IMAPC_SAVECTX(_ctx);
+ struct mailbox_transaction_context *_t = _ctx->transaction;
+ struct imapc_save_cmd_context sctx;
+ struct imapc_command *cmd;
+
+ sctx.ret = -2;
+ sctx.ctx = ctx;
+ cmd = imapc_client_mailbox_cmd(ctx->src_mbox->client_box,
+ imapc_copy_simple_callback,
+ &sctx);
+ imapc_command_sendf(cmd, "UID COPY %u %s", mail->uid, _t->box->name);
+ while (sctx.ret == -2)
+ imapc_mailbox_run(ctx->src_mbox);
+ ctx->finished = TRUE;
+ return sctx.ret;
+}
+
+static void imapc_copy_bulk_callback(const struct imapc_command_reply *reply,
+ void *context)
+{
+ struct imapc_copy_request *request = context;
+ struct imapc_save_context *ctx = request->sctx;
+ struct imapc_mailbox *mbox = ctx->src_mbox;
+ unsigned int uid;
+
+ i_assert(mbox != NULL);
+ i_assert(request == mbox->pending_copy_request);
+
+ /* Check the reply state and add uid's to index and
+ dest_saved_uids. */
+ if (ctx->failed) {
+ /* If the saving already failed try to find UIDs already
+ copied from the reply so that rollback can expunge
+ them */
+ if (null_strcasecmp(reply->resp_text_key, "COPYUID") == 0) {
+ (void)imapc_save_copyuid(ctx, reply, &uid);
+ imapc_transaction_save_rollback(&ctx->ctx);
+ }
+ } else if (reply->state == IMAPC_COMMAND_STATE_OK) {
+ if (reply->resp_text_key != NULL &&
+ strcasecmp(reply->resp_text_key, "COPYUID") == 0 &&
+ imapc_save_copyuid(ctx, reply, &uid)) {
+ ctx->finished = TRUE;
+ }
+ } else {
+ imapc_copy_set_error(ctx, reply);
+ }
+
+ ctx->src_mbox->pending_copy_request = NULL;
+ i_free(request);
+ imapc_client_stop(mbox->storage->client->client);
+}
+
+static void imapc_mail_copy_bulk_flush(struct imapc_mailbox *mbox)
+{
+ struct imapc_command *cmd;
+
+ i_assert(mbox != NULL);
+ i_assert(mbox->pending_copy_request != NULL);
+ i_assert(mbox->client_box != NULL);
+
+ cmd = imapc_client_mailbox_cmd(mbox->client_box,
+ imapc_copy_bulk_callback,
+ mbox->pending_copy_request);
+
+ seqset_builder_deinit(&mbox->pending_copy_request->uidset_builder);
+
+ str_append(mbox->pending_copy_cmd, " ");
+ imap_append_astring(mbox->pending_copy_cmd, mbox->copy_dest_box);
+
+ imapc_command_send(cmd, str_c(mbox->pending_copy_cmd));
+
+ imapc_copy_bulk_ctx_deinit(mbox->pending_copy_request->sctx);
+}
+
+static bool
+imapc_mail_copy_bulk_try_merge(struct imapc_mailbox *mbox, uint32_t uid,
+ const char *box)
+{
+ i_assert(str_begins(str_c(mbox->pending_copy_cmd), "UID COPY "));
+
+ if (strcmp(box, mbox->copy_dest_box) != 0) {
+ /* Not the same mailbox merging not possible */
+ return FALSE;
+ }
+ return seqset_builder_try_add(mbox->pending_copy_request->uidset_builder,
+ IMAPC_SERVER_CMDLINE_MAX_LEN, uid);
+}
+
+static void
+imapc_mail_copy_bulk_delayed_send_or_merge(struct imapc_save_context *ctx,
+ uint32_t uid,
+ const char *box)
+{
+ struct imapc_mailbox *mbox = ctx->src_mbox;
+
+ if (mbox->pending_copy_request != NULL &&
+ !imapc_mail_copy_bulk_try_merge(mbox, uid, box)) {
+ /* send the previous COPY and create new one after
+ waiting for this one to be finished. */
+ imapc_mail_copy_bulk_flush(mbox);
+ imapc_copy_bulk_finish(mbox->pending_copy_request->sctx);
+ }
+ if (mbox->pending_copy_request == NULL) {
+ mbox->pending_copy_request =
+ i_new(struct imapc_copy_request, 1);
+ str_printfa(mbox->pending_copy_cmd, "UID COPY ");
+ mbox->pending_copy_request->uidset_builder =
+ seqset_builder_init(mbox->pending_copy_cmd);
+ seqset_builder_add(mbox->pending_copy_request->uidset_builder,
+ uid);
+ mbox->copy_dest_box = i_strdup(box);
+ } else {
+ i_assert(mbox->pending_copy_request->sctx == ctx);
+ }
+ mbox->pending_copy_request->sctx = ctx;
+}
+
+static int
+imapc_copy_bulk(struct imapc_save_context *ctx, struct mail *mail)
+{
+ struct imapc_mailbox *mbox = IMAPC_MAILBOX(ctx->ctx.transaction->box);
+
+ imapc_mail_copy_bulk_delayed_send_or_merge(ctx, mail->uid,
+ imapc_mailbox_get_remote_name(mbox));
+ imapc_save_add_to_index(ctx, 0);
+
+ return ctx->failed ? -1 : 0;
+}
+
+static bool imapc_is_mail_expunged(struct imapc_mailbox *mbox, uint32_t uid)
+{
+ if (array_is_created(&mbox->delayed_expunged_uids) &&
+ seq_range_exists(&mbox->delayed_expunged_uids, uid))
+ return TRUE;
+ if (mbox->delayed_sync_trans == NULL)
+ return FALSE;
+
+ struct mail_index_view *view =
+ mail_index_transaction_get_view(mbox->delayed_sync_trans);
+ uint32_t seq;
+ return mail_index_lookup_seq(view, uid, &seq) &&
+ mail_index_transaction_is_expunged(mbox->delayed_sync_trans, seq);
+}
+
+int imapc_copy(struct mail_save_context *_ctx, struct mail *mail)
+{
+ struct imapc_save_context *ctx = IMAPC_SAVECTX(_ctx);
+ struct mailbox_transaction_context *_t = _ctx->transaction;
+ struct imapc_msgmap *src_msgmap;
+ uint32_t rseq;
+ int ret;
+
+ i_assert((_t->flags & MAILBOX_TRANSACTION_FLAG_EXTERNAL) != 0);
+
+ if (_t->box->storage == mail->box->storage) {
+ /* Currently we don't support copying mails from multiple
+ different source mailboxes within the same transaction. */
+ i_assert(ctx->src_mbox == NULL || &ctx->src_mbox->box == mail->box);
+ ctx->src_mbox = IMAPC_MAILBOX(mail->box);
+ if (!mail->expunged && imapc_is_mail_expunged(ctx->mbox, mail->uid))
+ mail_set_expunged(mail);
+ /* same server, we can use COPY for the mail */
+ src_msgmap =
+ imapc_client_mailbox_get_msgmap(ctx->src_mbox->client_box);
+ if (mail->expunged ||
+ !imapc_msgmap_uid_to_rseq(src_msgmap, mail->uid, &rseq)) {
+ mail_storage_set_error(mail->box->storage,
+ MAIL_ERROR_EXPUNGED,
+ "Some of the requested messages no longer exist.");
+ ctx->finished = TRUE;
+ index_save_context_free(_ctx);
+ return -1;
+ }
+ /* Mail has not been expunged and can be copied. */
+ if (ctx->mbox->capabilities == 0) {
+ /* The destination mailbox has not yet been selected
+ so the capabilities are unknown */
+ if (imapc_client_get_capabilities(ctx->mbox->storage->client->client,
+ &ctx->mbox->capabilities) < 0) {
+ mail_storage_set_error(mail->box->storage,
+ MAIL_ERROR_UNAVAILABLE,
+ "Failed to determine capabilities for mailbox.");
+ ctx->finished = TRUE;
+ index_save_context_free(_ctx);
+ return -1;
+ }
+ }
+ if ((ctx->mbox->capabilities & IMAPC_CAPABILITY_UIDPLUS) != 0)
+ ret = imapc_copy_bulk(ctx, mail);
+ else
+ ret = imapc_copy_simple(_ctx, mail);
+ index_save_context_free(_ctx);
+ return ret;
+ }
+ return mail_storage_copy(_ctx, mail);
+}
diff --git a/src/lib-storage/index/imapc/imapc-search.c b/src/lib-storage/index/imapc/imapc-search.c
new file mode 100644
index 0000000..e395919
--- /dev/null
+++ b/src/lib-storage/index/imapc/imapc-search.c
@@ -0,0 +1,330 @@
+/* Copyright (c) 2015-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "str.h"
+#include "imap-arg.h"
+#include "imap-seqset.h"
+#include "imap-util.h"
+#include "mail-search.h"
+#include "imapc-msgmap.h"
+#include "imapc-storage.h"
+#include "imapc-search.h"
+
+#define IMAPC_SEARCHCTX(obj) \
+ MODULE_CONTEXT(obj, imapc_storage_module)
+
+struct imapc_search_context {
+ union mail_search_module_context module_ctx;
+
+ ARRAY_TYPE(seq_range) rseqs;
+ struct seq_range_iter iter;
+ unsigned int n;
+ bool finished;
+ bool success;
+};
+
+static MODULE_CONTEXT_DEFINE_INIT(imapc_storage_module,
+ &mail_storage_module_register);
+
+static bool
+imapc_build_search_query_args(struct imapc_mailbox *mbox,
+ const struct mail_search_arg *args,
+ bool parent_or, string_t *str);
+
+static bool imapc_search_is_fast_local(const struct mail_search_arg *args)
+{
+ const struct mail_search_arg *arg;
+
+ for (arg = args; arg != NULL; arg = arg->next) {
+ switch (arg->type) {
+ case SEARCH_OR:
+ case SEARCH_SUB:
+ if (!imapc_search_is_fast_local(arg->value.subargs))
+ return FALSE;
+ break;
+ case SEARCH_ALL:
+ case SEARCH_SEQSET:
+ case SEARCH_UIDSET:
+ case SEARCH_FLAGS:
+ case SEARCH_KEYWORDS:
+ case SEARCH_MODSEQ:
+ case SEARCH_MAILBOX:
+ case SEARCH_MAILBOX_GUID:
+ case SEARCH_MAILBOX_GLOB:
+ case SEARCH_REAL_UID:
+ break;
+ default:
+ return FALSE;
+ }
+ }
+ return TRUE;
+}
+
+static bool
+imapc_build_search_query_arg(struct imapc_mailbox *mbox,
+ const struct mail_search_arg *arg,
+ string_t *str)
+{
+ struct mail_search_arg arg2 = *arg;
+ const char *error;
+
+ if (arg->match_not)
+ str_append(str, "NOT ");
+ arg2.match_not = FALSE;
+ arg = &arg2;
+
+ switch (arg->type) {
+ case SEARCH_OR:
+ imapc_build_search_query_args(mbox, arg->value.subargs, TRUE, str);
+ return TRUE;
+ case SEARCH_SUB:
+ str_append_c(str, '(');
+ imapc_build_search_query_args(mbox, arg->value.subargs, FALSE, str);
+ str_append_c(str, ')');
+ return TRUE;
+ case SEARCH_SEQSET:
+ /* translate to UIDs */
+ T_BEGIN {
+ ARRAY_TYPE(seq_range) uids;
+
+ t_array_init(&uids, 64);
+ mailbox_get_uid_range(&mbox->box, &arg->value.seqset,
+ &uids);
+ str_append(str, "UID ");
+ imap_write_seq_range(str, &uids);
+ } T_END;
+ return TRUE;
+ case SEARCH_BEFORE:
+ case SEARCH_SINCE:
+ case SEARCH_ON:
+ if (arg->type != SEARCH_ON &&
+ (mbox->capabilities & IMAPC_CAPABILITY_WITHIN) == 0) {
+ /* a bit kludgy way to check this.. */
+ size_t pos = str_len(str);
+ if (!mail_search_arg_to_imap(str, arg, &error))
+ return FALSE;
+ if (strncasecmp(str_c(str) + pos, "OLDER", 5) == 0 ||
+ strncasecmp(str_c(str) + pos, "YOUNGER", 7) == 0)
+ return FALSE;
+ return TRUE;
+ }
+ if (arg->value.date_type == MAIL_SEARCH_DATE_TYPE_SAVED &&
+ (mbox->capabilities & IMAPC_CAPABILITY_SAVEDATE) == 0) {
+ /* Fall back to internal date if save date is not
+ supported. */
+ arg2.value.date_type = MAIL_SEARCH_DATE_TYPE_RECEIVED;
+ }
+ /* fall through */
+ case SEARCH_ALL:
+ case SEARCH_UIDSET:
+ case SEARCH_FLAGS:
+ case SEARCH_KEYWORDS:
+ case SEARCH_SMALLER:
+ case SEARCH_LARGER:
+ case SEARCH_HEADER:
+ case SEARCH_HEADER_ADDRESS:
+ case SEARCH_HEADER_COMPRESS_LWSP:
+ case SEARCH_BODY:
+ case SEARCH_TEXT:
+ return mail_search_arg_to_imap(str, arg, &error);
+ /* extensions */
+ case SEARCH_MODSEQ:
+ if ((mbox->capabilities & IMAPC_CAPABILITY_CONDSTORE) == 0)
+ return FALSE;
+ return mail_search_arg_to_imap(str, arg, &error);
+ case SEARCH_SAVEDATESUPPORTED:
+ if ((mbox->capabilities & IMAPC_CAPABILITY_SAVEDATE) == 0)
+ return FALSE;
+ return mail_search_arg_to_imap(str, arg, &error);
+ case SEARCH_INTHREAD:
+ case SEARCH_GUID:
+ case SEARCH_MAILBOX:
+ case SEARCH_MAILBOX_GUID:
+ case SEARCH_MAILBOX_GLOB:
+ case SEARCH_REAL_UID:
+ case SEARCH_MIMEPART:
+ /* not supported for now */
+ break;
+ }
+ return FALSE;
+}
+
+static bool
+imapc_build_search_query_args(struct imapc_mailbox *mbox,
+ const struct mail_search_arg *args,
+ bool parent_or, string_t *str)
+{
+ const struct mail_search_arg *arg;
+
+ for (arg = args; arg != NULL; arg = arg->next) {
+ if (parent_or && arg->next != NULL)
+ str_append(str, "OR ");
+ if (!imapc_build_search_query_arg(mbox, arg, str))
+ return FALSE;
+ str_append_c(str, ' ');
+ }
+ str_truncate(str, str_len(str)-1);
+ return TRUE;
+}
+
+static bool imapc_build_search_query(struct imapc_mailbox *mbox,
+ const struct mail_search_args *args,
+ const char **query_r)
+{
+ string_t *str = t_str_new(128);
+
+ if (!IMAPC_BOX_HAS_FEATURE(mbox, IMAPC_FEATURE_SEARCH)) {
+ /* SEARCH command passthrough not enabled */
+ return FALSE;
+ }
+ if (imapc_search_is_fast_local(args->args))
+ return FALSE;
+
+ if ((mbox->capabilities & IMAPC_CAPABILITY_ESEARCH) != 0)
+ str_append(str, "SEARCH RETURN (ALL) ");
+ else
+ str_append(str, "UID SEARCH ");
+ if (!imapc_build_search_query_args(mbox, args->args, FALSE, str))
+ return FALSE;
+ *query_r = str_c(str);
+ return TRUE;
+}
+
+static void imapc_search_callback(const struct imapc_command_reply *reply,
+ void *context)
+{
+ struct mail_search_context *ctx = context;
+ struct imapc_mailbox *mbox = IMAPC_MAILBOX(ctx->transaction->box);
+ struct imapc_search_context *ictx = IMAPC_SEARCHCTX(ctx);
+ i_assert(ictx != NULL);
+
+ ictx->finished = TRUE;
+ if (reply->state == IMAPC_COMMAND_STATE_OK) {
+ seq_range_array_iter_init(&ictx->iter, &ictx->rseqs);
+ ictx->success = TRUE;
+ } else if (reply->state == IMAPC_COMMAND_STATE_DISCONNECTED) {
+ mail_storage_set_internal_error(mbox->box.storage);
+ } else {
+ mailbox_set_critical(&mbox->box,
+ "imapc: Command failed: %s", reply->text_full);
+ }
+ imapc_client_stop(mbox->storage->client->client);
+}
+
+struct mail_search_context *
+imapc_search_init(struct mailbox_transaction_context *t,
+ struct mail_search_args *args,
+ const enum mail_sort_type *sort_program,
+ enum mail_fetch_field wanted_fields,
+ struct mailbox_header_lookup_ctx *wanted_headers)
+{
+ struct imapc_mailbox *mbox = IMAPC_MAILBOX(t->box);
+ struct mail_search_context *ctx;
+ struct imapc_search_context *ictx;
+ struct imapc_command *cmd;
+ const char *search_query;
+
+ ctx = index_storage_search_init(t, args, sort_program,
+ wanted_fields, wanted_headers);
+
+ if (!imapc_build_search_query(mbox, args, &search_query)) {
+ /* can't optimize this with SEARCH */
+ return ctx;
+ }
+
+ ictx = i_new(struct imapc_search_context, 1);
+ i_array_init(&ictx->rseqs, 64);
+ MODULE_CONTEXT_SET(ctx, imapc_storage_module, ictx);
+
+ cmd = imapc_client_mailbox_cmd(mbox->client_box,
+ imapc_search_callback, ctx);
+ imapc_command_set_flags(cmd, IMAPC_COMMAND_FLAG_RETRIABLE);
+ imapc_command_send(cmd, search_query);
+
+ i_assert(mbox->search_ctx == NULL);
+ mbox->search_ctx = ictx;
+ while (!ictx->finished)
+ imapc_client_run(mbox->storage->client->client);
+ mbox->search_ctx = NULL;
+ return ctx;
+}
+
+static void imapc_search_set_matches(struct mail_search_arg *args)
+{
+ for (; args != NULL; args = args->next) {
+ if (args->type == SEARCH_OR ||
+ args->type == SEARCH_SUB)
+ imapc_search_set_matches(args->value.subargs);
+ args->match_always = TRUE;
+ args->result = 1;
+ }
+}
+
+bool imapc_search_next_update_seq(struct mail_search_context *ctx)
+{
+ struct imapc_search_context *ictx = IMAPC_SEARCHCTX(ctx);
+
+ if (ictx == NULL || !ictx->success)
+ return index_storage_search_next_update_seq(ctx);
+
+ if (!seq_range_array_iter_nth(&ictx->iter, ictx->n++, &ctx->seq))
+ return FALSE;
+ ctx->progress_cur = ctx->seq;
+
+ imapc_search_set_matches(ctx->args->args);
+ return TRUE;
+}
+
+int imapc_search_deinit(struct mail_search_context *ctx)
+{
+ struct imapc_search_context *ictx = IMAPC_SEARCHCTX(ctx);
+
+ if (ictx != NULL) {
+ array_free(&ictx->rseqs);
+ i_free(ictx);
+ }
+ return index_storage_search_deinit(ctx);
+}
+
+void imapc_search_reply_search(const struct imap_arg *args,
+ struct imapc_mailbox *mbox)
+{
+ struct imapc_msgmap *msgmap =
+ imapc_client_mailbox_get_msgmap(mbox->client_box);
+ const char *atom;
+ uint32_t uid, rseq;
+
+ if (mbox->search_ctx == NULL) {
+ i_error("Unexpected SEARCH reply");
+ return;
+ }
+
+ /* we're doing UID SEARCH, so need to convert UIDs to sequences */
+ for (unsigned int i = 0; args[i].type != IMAP_ARG_EOL; i++) {
+ if (!imap_arg_get_atom(&args[i], &atom) ||
+ str_to_uint32(atom, &uid) < 0 || uid == 0) {
+ i_error("Invalid SEARCH reply");
+ break;
+ }
+ if (imapc_msgmap_uid_to_rseq(msgmap, uid, &rseq))
+ seq_range_array_add(&mbox->search_ctx->rseqs, rseq);
+ }
+}
+
+void imapc_search_reply_esearch(const struct imap_arg *args,
+ struct imapc_mailbox *mbox)
+{
+ const char *atom;
+
+ if (mbox->search_ctx == NULL) {
+ i_error("Unexpected ESEARCH reply");
+ return;
+ }
+
+ /* It should contain ALL <seqset> or nonexistent if nothing matched */
+ if (args[0].type != IMAP_ARG_EOL &&
+ (!imap_arg_atom_equals(&args[0], "ALL") ||
+ !imap_arg_get_atom(&args[1], &atom) ||
+ imap_seq_set_nostar_parse(atom, &mbox->search_ctx->rseqs) < 0))
+ i_error("Invalid ESEARCH reply");
+}
diff --git a/src/lib-storage/index/imapc/imapc-search.h b/src/lib-storage/index/imapc/imapc-search.h
new file mode 100644
index 0000000..0966569
--- /dev/null
+++ b/src/lib-storage/index/imapc/imapc-search.h
@@ -0,0 +1,18 @@
+#ifndef IMAPC_SEARCH_H
+#define IMAPC_SEARCH_H
+
+struct mail_search_context *
+imapc_search_init(struct mailbox_transaction_context *t,
+ struct mail_search_args *args,
+ const enum mail_sort_type *sort_program,
+ enum mail_fetch_field wanted_fields,
+ struct mailbox_header_lookup_ctx *wanted_headers);
+bool imapc_search_next_update_seq(struct mail_search_context *ctx);
+int imapc_search_deinit(struct mail_search_context *ctx);
+
+void imapc_search_reply_search(const struct imap_arg *args,
+ struct imapc_mailbox *mbox);
+void imapc_search_reply_esearch(const struct imap_arg *args,
+ struct imapc_mailbox *mbox);
+
+#endif
diff --git a/src/lib-storage/index/imapc/imapc-settings.c b/src/lib-storage/index/imapc/imapc-settings.c
new file mode 100644
index 0000000..6183f8f
--- /dev/null
+++ b/src/lib-storage/index/imapc/imapc-settings.c
@@ -0,0 +1,173 @@
+/* Copyright (c) 2011-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "settings-parser.h"
+#include "mail-storage-settings.h"
+#include "imapc-settings.h"
+
+#include <stddef.h>
+
+#undef DEF
+#define DEF(type, name) \
+ SETTING_DEFINE_STRUCT_##type(#name, name, struct imapc_settings)
+
+static bool imapc_settings_check(void *_set, pool_t pool, const char **error_r);
+
+static const struct setting_define imapc_setting_defines[] = {
+ DEF(STR, imapc_host),
+ DEF(IN_PORT, imapc_port),
+
+ DEF(STR_VARS, imapc_user),
+ DEF(STR_VARS, imapc_master_user),
+ DEF(STR, imapc_password),
+ DEF(STR, imapc_sasl_mechanisms),
+
+ DEF(ENUM, imapc_ssl),
+ DEF(BOOL, imapc_ssl_verify),
+
+ DEF(STR, imapc_features),
+ DEF(STR, imapc_rawlog_dir),
+ DEF(STR, imapc_list_prefix),
+ DEF(TIME, imapc_cmd_timeout),
+ DEF(TIME, imapc_max_idle_time),
+ DEF(UINT, imapc_connection_retry_count),
+ DEF(TIME_MSECS, imapc_connection_retry_interval),
+ DEF(SIZE, imapc_max_line_length),
+
+ DEF(STR, pop3_deleted_flag),
+
+ SETTING_DEFINE_LIST_END
+};
+
+static const struct imapc_settings imapc_default_settings = {
+ .imapc_host = "",
+ .imapc_port = 143,
+
+ .imapc_user = "",
+ .imapc_master_user = "",
+ .imapc_password = "",
+ .imapc_sasl_mechanisms = "",
+
+ .imapc_ssl = "no:imaps:starttls",
+ .imapc_ssl_verify = TRUE,
+
+ .imapc_features = "",
+ .imapc_rawlog_dir = "",
+ .imapc_list_prefix = "",
+ .imapc_cmd_timeout = 5*60,
+ .imapc_max_idle_time = 60*29,
+ .imapc_connection_retry_count = 1,
+ .imapc_connection_retry_interval = 1000,
+ .imapc_max_line_length = 0,
+
+ .pop3_deleted_flag = ""
+};
+
+static const struct setting_parser_info imapc_setting_parser_info = {
+ .module_name = "imapc",
+ .defines = imapc_setting_defines,
+ .defaults = &imapc_default_settings,
+
+ .type_offset = SIZE_MAX,
+ .struct_size = sizeof(struct imapc_settings),
+
+ .parent_offset = SIZE_MAX,
+ .parent = &mail_user_setting_parser_info,
+
+ .check_func = imapc_settings_check
+};
+
+const struct setting_parser_info *imapc_get_setting_parser_info(void)
+{
+ return &imapc_setting_parser_info;
+}
+
+/* <settings checks> */
+struct imapc_feature_list {
+ const char *name;
+ enum imapc_features num;
+};
+
+static const struct imapc_feature_list imapc_feature_list[] = {
+ { "rfc822.size", IMAPC_FEATURE_RFC822_SIZE },
+ { "guid-forced", IMAPC_FEATURE_GUID_FORCED },
+ { "fetch-headers", IMAPC_FEATURE_FETCH_HEADERS },
+ { "gmail-migration", IMAPC_FEATURE_GMAIL_MIGRATION },
+ { "search", IMAPC_FEATURE_SEARCH },
+ { "zimbra-workarounds", IMAPC_FEATURE_ZIMBRA_WORKAROUNDS },
+ { "no-examine", IMAPC_FEATURE_NO_EXAMINE },
+ { "proxyauth", IMAPC_FEATURE_PROXYAUTH },
+ { "fetch-msn-workarounds", IMAPC_FEATURE_FETCH_MSN_WORKAROUNDS },
+ { "fetch-fix-broken-mails", IMAPC_FEATURE_FETCH_FIX_BROKEN_MAILS },
+ { "modseq", IMAPC_FEATURE_MODSEQ },
+ { "delay-login", IMAPC_FEATURE_DELAY_LOGIN },
+ { "fetch-bodystructure", IMAPC_FEATURE_FETCH_BODYSTRUCTURE },
+ { "send-id", IMAPC_FEATURE_SEND_ID },
+ { "fetch-empty-is-expunged", IMAPC_FEATURE_FETCH_EMPTY_IS_EXPUNGED },
+ { "no-msn-updates", IMAPC_FEATURE_NO_MSN_UPDATES },
+ { "acl", IMAPC_FEATURE_ACL },
+ { NULL, 0 }
+};
+
+static int
+imapc_settings_parse_throttle(struct imapc_settings *set,
+ const char *throttle_str, const char **error_r)
+{
+ const char *const *tmp;
+
+ tmp = t_strsplit(throttle_str, ":");
+ if (str_array_length(tmp) != 3 ||
+ str_to_uint(tmp[0], &set->throttle_init_msecs) < 0 ||
+ str_to_uint(tmp[1], &set->throttle_max_msecs) < 0 ||
+ str_to_uint(tmp[2], &set->throttle_shrink_min_msecs) < 0) {
+ *error_r = "imapc_features: Invalid throttle settings";
+ return -1;
+ }
+ return 0;
+}
+
+static int
+imapc_settings_parse_features(struct imapc_settings *set,
+ const char **error_r)
+{
+ enum imapc_features features = 0;
+ const struct imapc_feature_list *list;
+ const char *const *str;
+
+ str = t_strsplit_spaces(set->imapc_features, " ,");
+ for (; *str != NULL; str++) {
+ list = imapc_feature_list;
+ for (; list->name != NULL; list++) {
+ if (strcasecmp(*str, list->name) == 0) {
+ features |= list->num;
+ break;
+ }
+ }
+ if (strncasecmp(*str, "throttle:", 9) == 0) {
+ if (imapc_settings_parse_throttle(set, *str + 9, error_r) < 0)
+ return -1;
+ continue;
+ }
+ if (list->name == NULL) {
+ *error_r = t_strdup_printf("imapc_features: "
+ "Unknown feature: %s", *str);
+ return -1;
+ }
+ }
+ set->parsed_features = features;
+ return 0;
+}
+
+static bool imapc_settings_check(void *_set, pool_t pool ATTR_UNUSED,
+ const char **error_r)
+{
+ struct imapc_settings *set = _set;
+
+ if (set->imapc_max_idle_time == 0) {
+ *error_r = "imapc_max_idle_time must not be 0";
+ return FALSE;
+ }
+ if (imapc_settings_parse_features(set, error_r) < 0)
+ return FALSE;
+ return TRUE;
+}
diff --git a/src/lib-storage/index/imapc/imapc-settings.h b/src/lib-storage/index/imapc/imapc-settings.h
new file mode 100644
index 0000000..e4c1da1
--- /dev/null
+++ b/src/lib-storage/index/imapc/imapc-settings.h
@@ -0,0 +1,63 @@
+#ifndef IMAPC_SETTINGS_H
+#define IMAPC_SETTINGS_H
+
+#include "net.h"
+
+/* <settings checks> */
+enum imapc_features {
+ IMAPC_FEATURE_RFC822_SIZE = 0x01,
+ IMAPC_FEATURE_GUID_FORCED = 0x02,
+ IMAPC_FEATURE_FETCH_HEADERS = 0x04,
+ IMAPC_FEATURE_GMAIL_MIGRATION = 0x08,
+ IMAPC_FEATURE_SEARCH = 0x10,
+ IMAPC_FEATURE_ZIMBRA_WORKAROUNDS = 0x20,
+ IMAPC_FEATURE_NO_EXAMINE = 0x40,
+ IMAPC_FEATURE_PROXYAUTH = 0x80,
+ IMAPC_FEATURE_FETCH_MSN_WORKAROUNDS = 0x100,
+ IMAPC_FEATURE_FETCH_FIX_BROKEN_MAILS = 0x200,
+ IMAPC_FEATURE_MODSEQ = 0x400,
+ IMAPC_FEATURE_DELAY_LOGIN = 0x800,
+ IMAPC_FEATURE_FETCH_BODYSTRUCTURE = 0x1000,
+ IMAPC_FEATURE_SEND_ID = 0x2000,
+ IMAPC_FEATURE_FETCH_EMPTY_IS_EXPUNGED = 0x4000,
+ IMAPC_FEATURE_NO_MSN_UPDATES = 0x8000,
+ IMAPC_FEATURE_ACL = 0x10000,
+};
+/* </settings checks> */
+
+/*
+ * NOTE: Any additions here should be reflected in imapc_storage_create's
+ * serialization of settings.
+ */
+struct imapc_settings {
+ const char *imapc_host;
+ in_port_t imapc_port;
+
+ const char *imapc_user;
+ const char *imapc_master_user;
+ const char *imapc_password;
+ const char *imapc_sasl_mechanisms;
+
+ const char *imapc_ssl;
+ bool imapc_ssl_verify;
+
+ const char *imapc_features;
+ const char *imapc_rawlog_dir;
+ const char *imapc_list_prefix;
+ unsigned int imapc_cmd_timeout;
+ unsigned int imapc_max_idle_time;
+ unsigned int imapc_connection_retry_count;
+ unsigned int imapc_connection_retry_interval;
+ uoff_t imapc_max_line_length;
+
+ const char *pop3_deleted_flag;
+
+ enum imapc_features parsed_features;
+ unsigned int throttle_init_msecs;
+ unsigned int throttle_max_msecs;
+ unsigned int throttle_shrink_min_msecs;
+};
+
+const struct setting_parser_info *imapc_get_setting_parser_info(void);
+
+#endif
diff --git a/src/lib-storage/index/imapc/imapc-storage.c b/src/lib-storage/index/imapc/imapc-storage.c
new file mode 100644
index 0000000..43d90de
--- /dev/null
+++ b/src/lib-storage/index/imapc/imapc-storage.c
@@ -0,0 +1,1344 @@
+/* Copyright (c) 2011-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "ioloop.h"
+#include "str.h"
+#include "imap-arg.h"
+#include "imap-resp-code.h"
+#include "mailbox-tree.h"
+#include "imapc-connection.h"
+#include "imapc-msgmap.h"
+#include "imapc-mail.h"
+#include "imapc-list.h"
+#include "imapc-search.h"
+#include "imapc-sync.h"
+#include "imapc-settings.h"
+#include "imapc-storage.h"
+
+#define DNS_CLIENT_SOCKET_NAME "dns-client"
+
+struct imapc_open_context {
+ struct imapc_mailbox *mbox;
+ int ret;
+};
+
+struct imapc_resp_code_map {
+ const char *code;
+ enum mail_error error;
+};
+
+extern struct mail_storage imapc_storage;
+extern struct mailbox imapc_mailbox;
+
+static struct event_category event_category_imapc = {
+ .name = "imapc",
+ .parent = &event_category_storage,
+};
+
+static struct imapc_resp_code_map imapc_resp_code_map[] = {
+ { IMAP_RESP_CODE_UNAVAILABLE, MAIL_ERROR_TEMP },
+ { IMAP_RESP_CODE_AUTHFAILED, MAIL_ERROR_PERM },
+ { IMAP_RESP_CODE_AUTHZFAILED, MAIL_ERROR_PERM },
+ { IMAP_RESP_CODE_EXPIRED, MAIL_ERROR_PERM },
+ { IMAP_RESP_CODE_PRIVACYREQUIRED, MAIL_ERROR_PERM },
+ { IMAP_RESP_CODE_CONTACTADMIN, MAIL_ERROR_PERM },
+ { IMAP_RESP_CODE_NOPERM, MAIL_ERROR_PERM },
+ { IMAP_RESP_CODE_INUSE, MAIL_ERROR_INUSE },
+ { IMAP_RESP_CODE_EXPUNGEISSUED, MAIL_ERROR_EXPUNGED },
+ { IMAP_RESP_CODE_CORRUPTION, MAIL_ERROR_TEMP },
+ { IMAP_RESP_CODE_SERVERBUG, MAIL_ERROR_TEMP },
+ /* { IMAP_RESP_CODE_CLIENTBUG, 0 }, */
+ { IMAP_RESP_CODE_CANNOT, MAIL_ERROR_NOTPOSSIBLE },
+ { IMAP_RESP_CODE_LIMIT, MAIL_ERROR_LIMIT },
+ { IMAP_RESP_CODE_OVERQUOTA, MAIL_ERROR_NOQUOTA },
+ { IMAP_RESP_CODE_ALREADYEXISTS, MAIL_ERROR_EXISTS },
+ { IMAP_RESP_CODE_NONEXISTENT, MAIL_ERROR_NOTFOUND }
+};
+
+static void imapc_untagged_status(const struct imapc_untagged_reply *reply,
+ struct imapc_storage_client *client);
+static void imapc_untagged_namespace(const struct imapc_untagged_reply *reply,
+ struct imapc_storage_client *client);
+static int imapc_mailbox_run_status(struct mailbox *box,
+ enum mailbox_status_items items,
+ struct mailbox_status *status_r);
+
+bool imapc_resp_text_code_parse(const char *str, enum mail_error *error_r)
+{
+ unsigned int i;
+
+ if (str == NULL)
+ return FALSE;
+
+ for (i = 0; i < N_ELEMENTS(imapc_resp_code_map); i++) {
+ if (strcmp(imapc_resp_code_map[i].code, str) == 0) {
+ *error_r = imapc_resp_code_map[i].error;
+ return TRUE;
+ }
+ }
+ return FALSE;
+}
+
+bool imapc_mail_error_to_resp_text_code(enum mail_error error, const char **str_r)
+{
+ unsigned int i;
+
+ for (i = 0; i < N_ELEMENTS(imapc_resp_code_map); i++) {
+ if (imapc_resp_code_map[i].error == error) {
+ *str_r = imapc_resp_code_map[i].code;
+ return TRUE;
+ }
+ }
+ return FALSE;
+}
+
+bool imapc_mailbox_has_modseqs(struct imapc_mailbox *mbox)
+{
+ return (mbox->capabilities & (IMAPC_CAPABILITY_CONDSTORE |
+ IMAPC_CAPABILITY_QRESYNC)) != 0 &&
+ IMAPC_BOX_HAS_FEATURE(mbox, IMAPC_FEATURE_MODSEQ);
+}
+
+static struct mail_storage *imapc_storage_alloc(void)
+{
+ struct imapc_storage *storage;
+ pool_t pool;
+
+ pool = pool_alloconly_create("imapc storage", 2048);
+ storage = p_new(pool, struct imapc_storage, 1);
+ storage->storage = imapc_storage;
+ storage->storage.pool = pool;
+ storage->root_ioloop = current_ioloop;
+ return &storage->storage;
+}
+
+void imapc_copy_error_from_reply(struct imapc_storage *storage,
+ enum mail_error default_error,
+ const struct imapc_command_reply *reply)
+{
+ enum mail_error error;
+
+ if (imapc_resp_text_code_parse(reply->resp_text_key, &error)) {
+ mail_storage_set_error(&storage->storage, error,
+ reply->text_without_resp);
+ } else {
+ mail_storage_set_error(&storage->storage, default_error,
+ reply->text_without_resp);
+ }
+}
+
+void imapc_simple_context_init(struct imapc_simple_context *sctx,
+ struct imapc_storage_client *client)
+{
+ i_zero(sctx);
+ sctx->client = client;
+ sctx->ret = -2;
+}
+
+void imapc_simple_run(struct imapc_simple_context *sctx,
+ struct imapc_command **cmd)
+{
+ if (imapc_storage_client_handle_auth_failure(sctx->client)) {
+ imapc_command_abort(cmd);
+ imapc_client_logout(sctx->client->client);
+ sctx->ret = -1;
+ }
+ *cmd = NULL;
+ while (sctx->ret == -2)
+ imapc_client_run(sctx->client->client);
+}
+
+void imapc_mailbox_run(struct imapc_mailbox *mbox)
+{
+ imapc_mail_fetch_flush(mbox);
+ imapc_mailbox_run_nofetch(mbox);
+}
+
+void imapc_mailbox_run_nofetch(struct imapc_mailbox *mbox)
+{
+ do {
+ imapc_client_run(mbox->storage->client->client);
+ } while (mbox->storage->reopen_count > 0 ||
+ mbox->state_fetching_uid1);
+}
+
+void imapc_simple_callback(const struct imapc_command_reply *reply,
+ void *context)
+{
+ struct imapc_simple_context *ctx = context;
+
+ if (reply->state == IMAPC_COMMAND_STATE_OK)
+ ctx->ret = 0;
+ else if (reply->state == IMAPC_COMMAND_STATE_NO) {
+ imapc_copy_error_from_reply(ctx->client->_storage,
+ MAIL_ERROR_PARAMS, reply);
+ ctx->ret = -1;
+ } else if (imapc_storage_client_handle_auth_failure(ctx->client)) {
+ ctx->ret = -1;
+ } else if (reply->state == IMAPC_COMMAND_STATE_DISCONNECTED) {
+ mail_storage_set_internal_error(&ctx->client->_storage->storage);
+ ctx->ret = -1;
+ } else {
+ mail_storage_set_critical(&ctx->client->_storage->storage,
+ "imapc: Command failed: %s", reply->text_full);
+ ctx->ret = -1;
+ }
+ imapc_client_stop(ctx->client->client);
+}
+
+void imapc_mailbox_noop(struct imapc_mailbox *mbox)
+{
+ struct imapc_command *cmd;
+ struct imapc_simple_context sctx;
+
+ if (mbox->client_box == NULL) {
+ /* mailbox opening hasn't finished yet */
+ return;
+ }
+
+ imapc_simple_context_init(&sctx, mbox->storage->client);
+ cmd = imapc_client_mailbox_cmd(mbox->client_box,
+ imapc_simple_callback, &sctx);
+ imapc_command_send(cmd, "NOOP");
+ imapc_simple_run(&sctx, &cmd);
+}
+
+static void
+imapc_storage_client_untagged_cb(const struct imapc_untagged_reply *reply,
+ void *context)
+{
+ struct imapc_storage_client *client = context;
+ struct imapc_mailbox *mbox = reply->untagged_box_context;
+ const struct imapc_storage_event_callback *cb;
+ const struct imapc_mailbox_event_callback *mcb;
+
+ array_foreach(&client->untagged_callbacks, cb) {
+ if (strcasecmp(reply->name, cb->name) == 0)
+ cb->callback(reply, client);
+ }
+
+ if (mbox == NULL)
+ return;
+
+ array_foreach(&mbox->untagged_callbacks, mcb) {
+ if (strcasecmp(reply->name, mcb->name) == 0)
+ mcb->callback(reply, mbox);
+ }
+
+ if (reply->resp_text_key != NULL) {
+ array_foreach(&mbox->resp_text_callbacks, mcb) {
+ if (strcasecmp(reply->resp_text_key, mcb->name) == 0)
+ mcb->callback(reply, mbox);
+ }
+ }
+}
+
+static void
+imapc_storage_client_login_callback(const struct imapc_command_reply *reply,
+ void *context)
+{
+ struct imapc_storage_client *client = context;
+
+ client->auth_returned = TRUE;
+ imapc_client_stop(client->client);
+
+ if (reply->state == IMAPC_COMMAND_STATE_OK)
+ return;
+ if (client->destroying &&
+ reply->state == IMAPC_COMMAND_STATE_DISCONNECTED) {
+ /* user's work was finished before imapc login finished -
+ it's not an error */
+ return;
+ }
+
+ client->auth_failed_state = reply->state;
+ client->auth_failed_reason = i_strdup(reply->text_full);
+ if (!imapc_storage_client_handle_auth_failure(client))
+ i_unreached();
+}
+
+bool imapc_storage_client_handle_auth_failure(struct imapc_storage_client *client)
+{
+ if (client->auth_failed_state == IMAPC_COMMAND_STATE_OK)
+ return FALSE;
+
+ /* We need to set the error to either storage or to list, depending on
+ whether the caller is from mail-storage.h API or mailbox-list.h API.
+ We don't know here what the caller is though, so just set the error
+ to both of them. */
+ if (client->_storage != NULL) {
+ if (client->auth_failed_state == IMAPC_COMMAND_STATE_DISCONNECTED)
+ mail_storage_set_internal_error(&client->_storage->storage);
+ else {
+ mail_storage_set_error(&client->_storage->storage,
+ MAIL_ERROR_PERM, client->auth_failed_reason);
+ }
+ }
+ if (client->_list != NULL) {
+ if (client->auth_failed_state == IMAPC_COMMAND_STATE_DISCONNECTED)
+ mailbox_list_set_internal_error(&client->_list->list);
+ else {
+ mailbox_list_set_error(&client->_list->list,
+ MAIL_ERROR_PERM, client->auth_failed_reason);
+ }
+ }
+ return TRUE;
+}
+
+static void imapc_storage_client_login(struct imapc_storage_client *client,
+ struct mail_user *user, const char *host)
+{
+ imapc_client_login(client->client);
+ if (!user->namespaces_created) {
+ /* we're still initializing the user. wait for the
+ login to finish, so we can fail the user creation
+ if it fails. */
+ while (!client->auth_returned)
+ imapc_client_run(client->client);
+ if (imapc_storage_client_handle_auth_failure(client)) {
+ user->error = p_strdup_printf(user->pool,
+ "imapc: Login to %s failed: %s",
+ host, client->auth_failed_reason);
+ }
+ }
+}
+
+int imapc_storage_client_create(struct mail_namespace *ns,
+ const struct imapc_settings *imapc_set,
+ const struct mail_storage_settings *mail_set,
+ struct imapc_storage_client **client_r,
+ const char **error_r)
+{
+ struct imapc_storage_client *client;
+ struct imapc_client_settings set;
+ string_t *str;
+
+ i_zero(&set);
+ set.host = imapc_set->imapc_host;
+ if (*set.host == '\0') {
+ *error_r = "missing imapc_host";
+ return -1;
+ }
+ set.port = imapc_set->imapc_port;
+ if (imapc_set->imapc_user[0] != '\0')
+ set.username = imapc_set->imapc_user;
+ else if (ns->owner != NULL)
+ set.username = ns->owner->username;
+ else
+ set.username = ns->user->username;
+ set.master_user = imapc_set->imapc_master_user;
+ set.password = imapc_set->imapc_password;
+ if (*set.password == '\0') {
+ *error_r = "missing imapc_password";
+ return -1;
+ }
+ set.sasl_mechanisms = imapc_set->imapc_sasl_mechanisms;
+ set.use_proxyauth = (imapc_set->parsed_features & IMAPC_FEATURE_PROXYAUTH) != 0;
+ set.cmd_timeout_msecs = imapc_set->imapc_cmd_timeout * 1000;
+ set.connect_retry_count = imapc_set->imapc_connection_retry_count;
+ set.connect_retry_interval_msecs = imapc_set->imapc_connection_retry_interval;
+ set.max_idle_time = imapc_set->imapc_max_idle_time;
+ set.max_line_length = imapc_set->imapc_max_line_length;
+ set.dns_client_socket_path = *ns->user->set->base_dir == '\0' ? "" :
+ t_strconcat(ns->user->set->base_dir, "/",
+ DNS_CLIENT_SOCKET_NAME, NULL);
+ set.debug = mail_set->mail_debug;
+ set.rawlog_dir = mail_user_home_expand(ns->user,
+ imapc_set->imapc_rawlog_dir);
+ if ((imapc_set->parsed_features & IMAPC_FEATURE_SEND_ID) != 0)
+ set.session_id_prefix = ns->user->session_id;
+
+ str = t_str_new(128);
+ mail_user_set_get_temp_prefix(str, ns->user->set);
+ set.temp_path_prefix = str_c(str);
+
+ mail_user_init_ssl_client_settings(ns->user, &set.ssl_set);
+ if (!imapc_set->imapc_ssl_verify)
+ set.ssl_set.allow_invalid_cert = TRUE;
+
+ if (strcmp(imapc_set->imapc_ssl, "imaps") == 0)
+ set.ssl_mode = IMAPC_CLIENT_SSL_MODE_IMMEDIATE;
+ else if (strcmp(imapc_set->imapc_ssl, "starttls") == 0)
+ set.ssl_mode = IMAPC_CLIENT_SSL_MODE_STARTTLS;
+ else
+ set.ssl_mode = IMAPC_CLIENT_SSL_MODE_NONE;
+
+ set.throttle_set.init_msecs = imapc_set->throttle_init_msecs;
+ set.throttle_set.max_msecs = imapc_set->throttle_max_msecs;
+ set.throttle_set.shrink_min_msecs = imapc_set->throttle_shrink_min_msecs;
+
+ client = i_new(struct imapc_storage_client, 1);
+ client->refcount = 1;
+ i_array_init(&client->untagged_callbacks, 16);
+ /* FIXME: storage->event would be better, but we first get here when
+ creating mailbox_list, and storage doesn't even exist yet. */
+ client->client = imapc_client_init(&set, ns->user->event);
+ imapc_client_register_untagged(client->client,
+ imapc_storage_client_untagged_cb, client);
+
+ imapc_client_set_login_callback(client->client, imapc_storage_client_login_callback, client);
+
+ if ((ns->flags & NAMESPACE_FLAG_LIST_PREFIX) != 0 &&
+ (imapc_set->parsed_features & IMAPC_FEATURE_DELAY_LOGIN) == 0) {
+ /* start logging in immediately */
+ imapc_storage_client_login(client, ns->user, set.host);
+ }
+
+ *client_r = client;
+ return 0;
+}
+
+void imapc_storage_client_unref(struct imapc_storage_client **_client)
+{
+ struct imapc_storage_client *client = *_client;
+ struct imapc_storage_event_callback *cb;
+
+ *_client = NULL;
+
+ i_assert(client->refcount > 0);
+ if (--client->refcount > 0)
+ return;
+ imapc_client_deinit(&client->client);
+ array_foreach_modifiable(&client->untagged_callbacks, cb)
+ i_free(cb->name);
+ array_free(&client->untagged_callbacks);
+ i_free(client->auth_failed_reason);
+ i_free(client);
+}
+
+static int
+imapc_storage_create(struct mail_storage *_storage,
+ struct mail_namespace *ns,
+ const char **error_r)
+{
+ struct imapc_storage *storage = IMAPC_STORAGE(_storage);
+ struct imapc_mailbox_list *imapc_list = NULL;
+
+ storage->set = mail_namespace_get_driver_settings(ns, _storage);
+
+ /* serialize all the settings */
+ _storage->unique_root_dir = p_strdup_printf(_storage->pool,
+ "%s%s://(%s|%s):%s@%s:%u/%s mechs:%s features:%s "
+ "rawlog:%s cmd_timeout:%u maxidle:%u maxline:%zuu "
+ "pop3delflg:%s root_dir:%s",
+ storage->set->imapc_ssl,
+ storage->set->imapc_ssl_verify ? "(verify)" : "",
+ storage->set->imapc_user,
+ storage->set->imapc_master_user,
+ storage->set->imapc_password,
+ storage->set->imapc_host,
+ storage->set->imapc_port,
+ storage->set->imapc_list_prefix,
+ storage->set->imapc_sasl_mechanisms,
+ storage->set->imapc_features,
+ storage->set->imapc_rawlog_dir,
+ storage->set->imapc_cmd_timeout,
+ storage->set->imapc_max_idle_time,
+ (size_t) storage->set->imapc_max_line_length,
+ storage->set->pop3_deleted_flag,
+ ns->list->set.root_dir);
+
+ if (strcmp(ns->list->name, MAILBOX_LIST_NAME_IMAPC) == 0) {
+ imapc_list = (struct imapc_mailbox_list *)ns->list;
+ storage->client = imapc_list->client;
+ storage->client->refcount++;
+ } else {
+ if (imapc_storage_client_create(ns, storage->set, _storage->set,
+ &storage->client, error_r) < 0)
+ return -1;
+ }
+ storage->client->_storage = storage;
+ p_array_init(&storage->remote_namespaces, _storage->pool, 4);
+ if (IMAPC_HAS_FEATURE(storage, IMAPC_FEATURE_FETCH_BODYSTRUCTURE)) {
+ _storage->nonbody_access_fields |=
+ MAIL_FETCH_IMAP_BODY | MAIL_FETCH_IMAP_BODYSTRUCTURE;
+ }
+
+ imapc_storage_client_register_untagged(storage->client, "STATUS",
+ imapc_untagged_status);
+ imapc_storage_client_register_untagged(storage->client, "NAMESPACE",
+ imapc_untagged_namespace);
+
+ return 0;
+}
+
+static void imapc_storage_destroy(struct mail_storage *_storage)
+{
+ struct imapc_storage *storage = IMAPC_STORAGE(_storage);
+
+ storage->client->destroying = TRUE;
+
+ /* make sure all pending commands are aborted before anything is
+ deinitialized */
+ imapc_client_logout(storage->client->client);
+
+ imapc_storage_client_unref(&storage->client);
+ index_storage_destroy(_storage);
+}
+
+void imapc_storage_client_register_untagged(struct imapc_storage_client *client,
+ const char *name,
+ imapc_storage_callback_t *callback)
+{
+ struct imapc_storage_event_callback *cb;
+
+ cb = array_append_space(&client->untagged_callbacks);
+ cb->name = i_strdup(name);
+ cb->callback = callback;
+}
+
+void imapc_storage_client_unregister_untagged(struct imapc_storage_client *client,
+ const char *name)
+{
+ struct imapc_storage_event_callback *cb;
+ unsigned int idx;
+ array_foreach_modifiable(&client->untagged_callbacks, cb) {
+ if (strcmp(cb->name, name) == 0) {
+ idx = array_foreach_idx(&client->untagged_callbacks, cb);
+ i_free(cb->name);
+ array_delete(&client->untagged_callbacks, idx, 1);
+ return;
+ }
+ }
+ i_unreached();
+}
+
+static void
+imapc_storage_get_list_settings(const struct mail_namespace *ns ATTR_UNUSED,
+ struct mailbox_list_settings *set)
+{
+ if (set->layout == NULL)
+ set->layout = MAILBOX_LIST_NAME_IMAPC;
+ set->storage_name_escape_char = IMAPC_LIST_STORAGE_NAME_ESCAPE_CHAR;
+ /* We want to have all imapc mailboxes accessible, so escape them if
+ necessary. */
+ if (set->vname_escape_char == '\0')
+ set->vname_escape_char = IMAPC_LIST_VNAME_ESCAPE_CHAR;
+}
+
+static struct mailbox *
+imapc_mailbox_alloc(struct mail_storage *storage, struct mailbox_list *list,
+ const char *vname, enum mailbox_flags flags)
+{
+ struct imapc_mailbox *mbox;
+ pool_t pool;
+
+ pool = pool_alloconly_create("imapc mailbox", 1024*4);
+ mbox = p_new(pool, struct imapc_mailbox, 1);
+ mbox->box = imapc_mailbox;
+ mbox->box.pool = pool;
+ mbox->box.storage = storage;
+ mbox->box.list = list;
+ mbox->box.mail_vfuncs = &imapc_mail_vfuncs;
+
+ index_storage_mailbox_alloc(&mbox->box, vname, flags, MAIL_INDEX_PREFIX);
+
+ mbox->storage = IMAPC_STORAGE(storage);
+
+ p_array_init(&mbox->untagged_callbacks, pool, 16);
+ p_array_init(&mbox->resp_text_callbacks, pool, 16);
+ p_array_init(&mbox->fetch_requests, pool, 16);
+ p_array_init(&mbox->untagged_fetch_contexts, pool, 16);
+ p_array_init(&mbox->delayed_expunged_uids, pool, 16);
+ p_array_init(&mbox->copy_rollback_expunge_uids, pool, 16);
+ mbox->pending_fetch_cmd = str_new(pool, 128);
+ mbox->pending_copy_cmd = str_new(pool, 128);
+ mbox->prev_mail_cache.fd = -1;
+ imapc_mailbox_register_callbacks(mbox);
+ return &mbox->box;
+}
+
+const char *imapc_mailbox_get_remote_name(struct imapc_mailbox *mbox)
+{
+ struct imapc_mailbox_list *list =
+ container_of(mbox->box.list, struct imapc_mailbox_list, list);
+
+ if (strcmp(mbox->box.list->name, MAILBOX_LIST_NAME_IMAPC) != 0)
+ return mbox->box.name;
+ return imapc_list_storage_to_remote_name(list, mbox->box.name);
+}
+
+static int
+imapc_mailbox_exists(struct mailbox *box, bool auto_boxes,
+ enum mailbox_existence *existence_r)
+{
+ if (auto_boxes && mailbox_is_autocreated(box)) {
+ *existence_r = MAILBOX_EXISTENCE_SELECT;
+ return 0;
+ }
+
+ if (strcmp(box->list->name, MAILBOX_LIST_NAME_IMAPC) != 0) {
+ if (box->inbox_any)
+ *existence_r = MAILBOX_EXISTENCE_SELECT;
+ else
+ *existence_r = MAILBOX_EXISTENCE_NONE;
+ return 0;
+ }
+
+ enum mailbox_info_flags flags;
+
+ struct imapc_mailbox_list *list = (struct imapc_mailbox_list *)box->list;
+
+ if (imapc_storage_client_handle_auth_failure(list->client)) {
+ mail_storage_copy_list_error(box->storage, box->list);
+ return -1;
+ }
+ if (imapc_list_get_mailbox_flags(box->list, box->name, &flags) < 0) {
+ mail_storage_copy_list_error(box->storage, box->list);
+ return -1;
+ }
+ if ((flags & MAILBOX_NONEXISTENT) != 0)
+ *existence_r = MAILBOX_EXISTENCE_NONE;
+ else if ((flags & MAILBOX_NOSELECT) != 0)
+ *existence_r = MAILBOX_EXISTENCE_NOSELECT;
+ else
+ *existence_r = MAILBOX_EXISTENCE_SELECT;
+ return 0;
+}
+
+static bool imapc_mailbox_want_examine(struct imapc_mailbox *mbox)
+{
+ if (IMAPC_BOX_HAS_FEATURE(mbox, IMAPC_FEATURE_NO_EXAMINE)) {
+ /* mainly a Courier-workaround: With POP3-only Maildir that
+ doesn't have UIDVALIDITY set, EXAMINE won't generate a
+ permanent UIDVALIDITY while SELECT will. */
+ return FALSE;
+ }
+ return (mbox->box.flags & MAILBOX_FLAG_DROP_RECENT) == 0 &&
+ ((mbox->box.flags & MAILBOX_FLAG_READONLY) != 0 ||
+ (mbox->box.flags & MAILBOX_FLAG_SAVEONLY) != 0);
+}
+
+static bool
+imapc_mailbox_verify_select(struct imapc_mailbox *mbox, const char **error_r)
+{
+ if (!mbox->exists_received)
+ *error_r = "EXISTS not received";
+ else if (mbox->sync_uid_validity == 0)
+ *error_r = "UIDVALIDITY not received";
+ else
+ return TRUE;
+ return FALSE;
+}
+
+static void
+imapc_mailbox_reopen_callback(const struct imapc_command_reply *reply,
+ void *context)
+{
+ struct imapc_mailbox *mbox = context;
+ const char *errmsg;
+
+ i_assert(mbox->storage->reopen_count > 0);
+ mbox->storage->reopen_count--;
+ mbox->selecting = FALSE;
+ if (reply->state != IMAPC_COMMAND_STATE_OK)
+ errmsg = reply->text_full;
+ else if (imapc_mailbox_verify_select(mbox, &errmsg)) {
+ imap_mailbox_select_finish(mbox);
+ errmsg = NULL;
+ }
+
+ if (errmsg != NULL) {
+ imapc_client_mailbox_reconnect(mbox->client_box,
+ t_strdup_printf("Reopening mailbox '%s' failed: %s",
+ mbox->box.name, errmsg));
+ }
+
+ imapc_client_stop(mbox->storage->client->client);
+}
+
+static void imapc_mailbox_reopen(void *context)
+{
+ struct imapc_mailbox *mbox = context;
+ struct imapc_command *cmd;
+
+ /* we're reconnecting and need to reopen the mailbox */
+ mbox->prev_skipped_rseq = 0;
+ mbox->prev_skipped_uid = 0;
+ imapc_msgmap_reset(imapc_client_mailbox_get_msgmap(mbox->client_box));
+
+ if (mbox->selecting) {
+ /* We reconnected during the initial SELECT/EXAMINE. It'll be
+ automatically resent by lib-imap-client, so we don't need to
+ send it again here. */
+ i_assert(!mbox->initial_sync_done);
+ return;
+ }
+ if (!mbox->initial_sync_done) {
+ /* Initial FETCH 1:* didn't fully succeed. We're reconnecting
+ and lib-imap-client is automatically resending it. But we
+ need to reset the sync_next_* state so that if any of the
+ mails are now expunged we won't get confused and crash. */
+ mbox->sync_next_lseq = 1;
+ mbox->sync_next_rseq = 1;
+ }
+
+ mbox->state_fetched_success = FALSE;
+ mbox->initial_sync_done = FALSE;
+ mbox->selecting = TRUE;
+ mbox->selected = FALSE;
+ mbox->exists_received = FALSE;
+
+ cmd = imapc_client_mailbox_cmd(mbox->client_box,
+ imapc_mailbox_reopen_callback, mbox);
+ imapc_command_set_flags(cmd, IMAPC_COMMAND_FLAG_SELECT);
+ if (imapc_mailbox_want_examine(mbox)) {
+ imapc_command_sendf(cmd, "EXAMINE %s",
+ imapc_mailbox_get_remote_name(mbox));
+ } else {
+ imapc_command_sendf(cmd, "SELECT %s",
+ imapc_mailbox_get_remote_name(mbox));
+ }
+ mbox->storage->reopen_count++;
+}
+
+static void
+imapc_mailbox_open_callback(const struct imapc_command_reply *reply,
+ void *context)
+{
+ struct imapc_open_context *ctx = context;
+ const char *error;
+
+ ctx->mbox->selecting = FALSE;
+ if (reply->state == IMAPC_COMMAND_STATE_OK) {
+ if (!imapc_mailbox_verify_select(ctx->mbox, &error)) {
+ mailbox_set_critical(&ctx->mbox->box,
+ "imapc: Opening mailbox failed: %s", error);
+ ctx->ret = -1;
+ } else {
+ imap_mailbox_select_finish(ctx->mbox);
+ ctx->ret = 0;
+ }
+ } else if (reply->state == IMAPC_COMMAND_STATE_NO) {
+ /* Unless the remote IMAP server supports sending
+ resp-text-code, we don't know if the NO reply is because
+ the mailbox doesn't exist or because of some internal error.
+ We'll default to assuming it doesn't exist, so e.g.
+ mailbox { auto=create } will auto-create missing mailboxes.
+ However, INBOX is a special mailbox, which is always
+ autocreated if it doesn't exist. This is true in both the
+ local Dovecot and the remote IMAP server. This means that
+ there's no point in trying to send CREATE INBOX to the
+ remote server. We'll avoid that by defaulting to temporary
+ failure with INBOX. */
+ enum mail_error default_error =
+ ctx->mbox->box.inbox_any ?
+ MAIL_ERROR_TEMP : MAIL_ERROR_NOTFOUND;
+ imapc_copy_error_from_reply(ctx->mbox->storage,
+ default_error, reply);
+ ctx->ret = -1;
+ } else if (imapc_storage_client_handle_auth_failure(ctx->mbox->storage->client)) {
+ ctx->ret = -1;
+ } else if (reply->state == IMAPC_COMMAND_STATE_DISCONNECTED) {
+ ctx->ret = -1;
+ mail_storage_set_internal_error(ctx->mbox->box.storage);
+ } else {
+ mailbox_set_critical(&ctx->mbox->box,
+ "imapc: Opening mailbox failed: %s", reply->text_full);
+ ctx->ret = -1;
+ }
+ imapc_client_stop(ctx->mbox->storage->client->client);
+}
+
+static int imapc_mailbox_get_capabilities(struct imapc_mailbox *mbox)
+{
+ /* If authentication failed, don't check again. */
+ if (imapc_storage_client_handle_auth_failure(mbox->storage->client))
+ return -1;
+
+ return imapc_client_get_capabilities(mbox->storage->client->client,
+ &mbox->capabilities);
+
+}
+
+static void imapc_mailbox_get_extensions(struct imapc_mailbox *mbox)
+{
+ if (mbox->guid_fetch_field_name == NULL) {
+ /* see if we can get message GUIDs somehow */
+ if ((mbox->capabilities & IMAPC_CAPABILITY_X_GM_EXT_1) != 0) {
+ /* GMail */
+ mbox->guid_fetch_field_name = "X-GM-MSGID";
+ }
+ }
+}
+
+int imapc_mailbox_select(struct imapc_mailbox *mbox)
+{
+ struct imapc_command *cmd;
+ struct imapc_open_context ctx;
+
+ i_assert(mbox->client_box == NULL);
+
+ if (imapc_mailbox_get_capabilities(mbox) < 0)
+ return -1;
+
+ if (imapc_mailbox_has_modseqs(mbox)) {
+ if (!array_is_created(&mbox->rseq_modseqs))
+ i_array_init(&mbox->rseq_modseqs, 32);
+ else
+ array_clear(&mbox->rseq_modseqs);
+ }
+
+ mbox->client_box =
+ imapc_client_mailbox_open(mbox->storage->client->client, mbox);
+ imapc_client_mailbox_set_reopen_cb(mbox->client_box,
+ imapc_mailbox_reopen, mbox);
+
+ imapc_mailbox_get_extensions(mbox);
+
+ mbox->selecting = TRUE;
+ mbox->exists_received = FALSE;
+ ctx.mbox = mbox;
+ ctx.ret = -2;
+ cmd = imapc_client_mailbox_cmd(mbox->client_box,
+ imapc_mailbox_open_callback, &ctx);
+ imapc_command_set_flags(cmd, IMAPC_COMMAND_FLAG_SELECT |
+ IMAPC_COMMAND_FLAG_RETRIABLE);
+ if (imapc_mailbox_want_examine(mbox)) {
+ imapc_command_sendf(cmd, "EXAMINE %s",
+ imapc_mailbox_get_remote_name(mbox));
+ } else {
+ imapc_command_sendf(cmd, "SELECT %s",
+ imapc_mailbox_get_remote_name(mbox));
+ }
+
+ while (ctx.ret == -2 || mbox->state_fetching_uid1)
+ imapc_mailbox_run(mbox);
+ if (!mbox->state_fetched_success)
+ ctx.ret = -1;
+ return ctx.ret;
+}
+
+static int imapc_mailbox_open(struct mailbox *box)
+{
+ struct imapc_mailbox *mbox = IMAPC_MAILBOX(box);
+
+ if (index_storage_mailbox_open(box, FALSE) < 0)
+ return -1;
+
+ if (box->deleting || (box->flags & MAILBOX_FLAG_SAVEONLY) != 0) {
+ /* We don't actually want to SELECT the mailbox. */
+ return 0;
+ }
+
+ if (*box->name == '\0' &&
+ (box->list->ns->flags & NAMESPACE_FLAG_INBOX_ANY) != 0) {
+ /* trying to open INBOX as the namespace prefix.
+ Don't allow this. */
+ mail_storage_set_error(box->storage, MAIL_ERROR_NOTFOUND,
+ "Mailbox isn't selectable");
+ mailbox_close(box);
+ return -1;
+ }
+
+ if (imapc_mailbox_select(mbox) < 0) {
+ mailbox_close(box);
+ return -1;
+ }
+ return 0;
+}
+
+void imapc_mail_cache_free(struct imapc_mail_cache *cache)
+{
+ i_close_fd(&cache->fd);
+ buffer_free(&cache->buf);
+ cache->uid = 0;
+}
+
+static void imapc_mailbox_close(struct mailbox *box)
+{
+ struct imapc_mailbox *mbox = IMAPC_MAILBOX(box);
+ bool changes;
+
+ (void)imapc_mailbox_commit_delayed_trans(mbox, FALSE, &changes);
+ imapc_mail_fetch_flush(mbox);
+
+ /* Arriving here we may have fetch contexts still unprocessed,
+ if there have been no mailbox_sync() after receiving the untagged replies.
+ Losing these changes isn't a problem, since the same changes will be found
+ out after connecting to the server the next time. */
+ struct imapc_untagged_fetch_ctx *untagged_fetch_context;
+ array_foreach_elem(&mbox->untagged_fetch_contexts, untagged_fetch_context)
+ imapc_untagged_fetch_ctx_free(&untagged_fetch_context);
+ array_clear(&mbox->untagged_fetch_contexts);
+
+ if (mbox->client_box != NULL)
+ imapc_client_mailbox_close(&mbox->client_box);
+ if (array_is_created(&mbox->rseq_modseqs))
+ array_free(&mbox->rseq_modseqs);
+ if (mbox->sync_view != NULL)
+ mail_index_view_close(&mbox->sync_view);
+ timeout_remove(&mbox->to_idle_delay);
+ timeout_remove(&mbox->to_idle_check);
+ imapc_mail_cache_free(&mbox->prev_mail_cache);
+ index_storage_mailbox_close(box);
+}
+
+static int
+imapc_mailbox_create(struct mailbox *box,
+ const struct mailbox_update *update ATTR_UNUSED,
+ bool directory)
+{
+ struct imapc_mailbox *mbox = IMAPC_MAILBOX(box);
+ struct imapc_command *cmd;
+ struct imapc_simple_context sctx;
+ const char *remote_name = imapc_mailbox_get_remote_name(mbox);
+
+ if (!directory)
+ ;
+ else if (strcmp(box->list->name, MAILBOX_LIST_NAME_IMAPC) == 0) {
+ struct imapc_mailbox_list *imapc_list =
+ (struct imapc_mailbox_list *)box->list;
+ remote_name = t_strdup_printf("%s%c", remote_name,
+ imapc_list->root_sep);
+ } else {
+ remote_name = t_strdup_printf("%s%c", remote_name,
+ mailbox_list_get_hierarchy_sep(box->list));
+ }
+ imapc_simple_context_init(&sctx, mbox->storage->client);
+ cmd = imapc_client_cmd(mbox->storage->client->client,
+ imapc_simple_callback, &sctx);
+ imapc_command_sendf(cmd, "CREATE %s", remote_name);
+ imapc_simple_run(&sctx, &cmd);
+ return sctx.ret;
+}
+
+static int imapc_mailbox_update(struct mailbox *box,
+ const struct mailbox_update *update)
+{
+ if (!guid_128_is_empty(update->mailbox_guid) ||
+ update->uid_validity != 0 || update->min_next_uid != 0 ||
+ update->min_first_recent_uid != 0) {
+ mail_storage_set_error(box->storage, MAIL_ERROR_NOTPOSSIBLE,
+ "Not supported");
+ }
+ return index_storage_mailbox_update(box, update);
+}
+
+static void imapc_untagged_status(const struct imapc_untagged_reply *reply,
+ struct imapc_storage_client *client)
+{
+ struct imapc_storage *storage = client->_storage;
+ struct mailbox_status *status;
+ const struct imap_arg *list;
+ const char *remote_name, *key, *value;
+ uint32_t num;
+ unsigned int i;
+
+ if (!imap_arg_get_astring(&reply->args[0], &remote_name) ||
+ !imap_arg_get_list(&reply->args[1], &list))
+ return;
+
+ if (storage->cur_status_box == NULL)
+ return;
+
+ if (!imapc_mailbox_name_equals(storage->cur_status_box,
+ remote_name))
+ return;
+
+ status = storage->cur_status;
+ for (i = 0; list[i].type != IMAP_ARG_EOL; i += 2) {
+ if (!imap_arg_get_atom(&list[i], &key) ||
+ !imap_arg_get_atom(&list[i+1], &value) ||
+ str_to_uint32(value, &num) < 0)
+ return;
+
+ if (strcasecmp(key, "MESSAGES") == 0)
+ status->messages = num;
+ else if (strcasecmp(key, "RECENT") == 0)
+ status->recent = num;
+ else if (strcasecmp(key, "UIDNEXT") == 0)
+ status->uidnext = num;
+ else if (strcasecmp(key, "UIDVALIDITY") == 0)
+ status->uidvalidity = num;
+ else if (strcasecmp(key, "UNSEEN") == 0)
+ status->unseen = num;
+ else if (strcasecmp(key, "HIGHESTMODSEQ") == 0 &&
+ imapc_mailbox_has_modseqs(storage->cur_status_box))
+ status->highest_modseq = num;
+ }
+}
+
+static void imapc_untagged_namespace(const struct imapc_untagged_reply *reply,
+ struct imapc_storage_client *client)
+{
+ struct imapc_storage *storage = client->_storage;
+ static enum mail_namespace_type ns_types[] = {
+ MAIL_NAMESPACE_TYPE_PRIVATE,
+ MAIL_NAMESPACE_TYPE_SHARED,
+ MAIL_NAMESPACE_TYPE_PUBLIC
+ };
+ struct imapc_namespace *ns;
+ const struct imap_arg *list, *list2;
+ const char *prefix, *sep;
+ unsigned int i;
+
+ array_clear(&storage->remote_namespaces);
+ for (i = 0; i < N_ELEMENTS(ns_types); i++) {
+ if (reply->args[i].type == IMAP_ARG_NIL)
+ continue;
+ if (!imap_arg_get_list(&reply->args[i], &list))
+ break;
+
+ for (; list->type != IMAP_ARG_EOL; list++) {
+ if (!imap_arg_get_list(list, &list2) ||
+ !imap_arg_get_astring(&list2[0], &prefix) ||
+ !imap_arg_get_nstring(&list2[1], &sep))
+ break;
+
+ ns = array_append_space(&storage->remote_namespaces);
+ ns->prefix = p_strdup(storage->storage.pool, prefix);
+ ns->separator = sep == NULL ? '\0' : sep[0];
+ ns->type = ns_types[i];
+ }
+ }
+}
+
+static void imapc_mailbox_get_selected_status(struct imapc_mailbox *mbox,
+ enum mailbox_status_items items,
+ struct mailbox_status *status_r)
+{
+ index_storage_get_open_status(&mbox->box, items, status_r);
+ if ((items & STATUS_PERMANENT_FLAGS) != 0)
+ status_r->permanent_flags = mbox->permanent_flags;
+ if ((items & STATUS_FIRST_RECENT_UID) != 0)
+ status_r->first_recent_uid = mbox->highest_nonrecent_uid + 1;
+ if ((items & STATUS_HIGHESTMODSEQ) != 0) {
+ /* FIXME: this doesn't work perfectly. we're now just returning
+ the HIGHESTMODSEQ from the current index, which may or may
+ not be correct. with QRESYNC enabled we could be returning
+ sync_highestmodseq, but that would require implementing
+ VANISHED replies. and without QRESYNC we'd have to issue
+ STATUS (HIGHESTMODSEQ), which isn't efficient since we get
+ here constantly (after every IMAP command). */
+ }
+ if (imapc_mailbox_has_modseqs(mbox)) {
+ /* even if local indexes are only in memory, we still
+ have modseqs on the IMAP server itself. */
+ status_r->nonpermanent_modseqs = FALSE;
+ }
+}
+
+static int imapc_mailbox_delete(struct mailbox *box)
+{
+ box->delete_skip_empty_check = TRUE;
+ return index_storage_mailbox_delete(box);
+}
+
+static int imapc_mailbox_run_status(struct mailbox *box,
+ enum mailbox_status_items items,
+ struct mailbox_status *status_r)
+{
+ struct imapc_mailbox *mbox = IMAPC_MAILBOX(box);
+ struct imapc_command *cmd;
+ struct imapc_simple_context sctx;
+ string_t *str;
+
+ if (imapc_mailbox_get_capabilities(mbox) < 0)
+ return -1;
+
+ str = t_str_new(256);
+ if ((items & STATUS_MESSAGES) != 0)
+ str_append(str, " MESSAGES");
+ if ((items & STATUS_RECENT) != 0)
+ str_append(str, " RECENT");
+ if ((items & STATUS_UIDNEXT) != 0)
+ str_append(str, " UIDNEXT");
+ if ((items & STATUS_UIDVALIDITY) != 0)
+ str_append(str, " UIDVALIDITY");
+ if ((items & STATUS_UNSEEN) != 0)
+ str_append(str, " UNSEEN");
+ if ((items & STATUS_HIGHESTMODSEQ) != 0 &&
+ imapc_mailbox_has_modseqs(mbox))
+ str_append(str, " HIGHESTMODSEQ");
+
+ if (str_len(str) == 0) {
+ /* nothing requested */
+ return 0;
+ }
+
+ imapc_simple_context_init(&sctx, mbox->storage->client);
+ mbox->storage->cur_status_box = mbox;
+ mbox->storage->cur_status = status_r;
+ cmd = imapc_client_cmd(mbox->storage->client->client,
+ imapc_simple_callback, &sctx);
+ imapc_command_set_flags(cmd, IMAPC_COMMAND_FLAG_RETRIABLE);
+ imapc_command_sendf(cmd, "STATUS %s (%1s)",
+ imapc_mailbox_get_remote_name(mbox), str_c(str)+1);
+ imapc_simple_run(&sctx, &cmd);
+ mbox->storage->cur_status_box = NULL;
+ mbox->storage->cur_status = NULL;
+ return sctx.ret;
+}
+
+static int imapc_mailbox_get_status(struct mailbox *box,
+ enum mailbox_status_items items,
+ struct mailbox_status *status_r)
+{
+ struct imapc_mailbox *mbox = IMAPC_MAILBOX(box);
+
+ if (mbox->guid_fetch_field_name != NULL ||
+ IMAPC_BOX_HAS_FEATURE(mbox, IMAPC_FEATURE_GUID_FORCED))
+ status_r->have_guids = TRUE;
+
+ if (box->opened) {
+ imapc_mailbox_get_selected_status(mbox, items, status_r);
+ } else if ((items & (STATUS_FIRST_UNSEEN_SEQ | STATUS_KEYWORDS |
+ STATUS_PERMANENT_FLAGS |
+ STATUS_FIRST_RECENT_UID)) != 0) {
+ /* getting these requires opening the mailbox */
+ if (mailbox_open(box) < 0)
+ return -1;
+ imapc_mailbox_get_selected_status(mbox, items, status_r);
+ } else {
+ if (imapc_mailbox_run_status(box, items, status_r) < 0)
+ return -1;
+ }
+
+ if (box->opened && !box->deleting && (items & STATUS_UIDNEXT) != 0 &&
+ mbox->sync_uid_next == 0) {
+ /* Courier-workaround, it doesn't send UIDNEXT on SELECT */
+ if (imapc_mailbox_run_status(box, STATUS_UIDNEXT, status_r) < 0)
+ return -1;
+ }
+ return 0;
+}
+
+static int imapc_mailbox_get_namespaces(struct imapc_mailbox *mbox)
+{
+ struct imapc_storage *storage = mbox->storage;
+ struct imapc_command *cmd;
+ struct imapc_simple_context sctx;
+
+ if (storage->namespaces_requested)
+ return 0;
+
+ if (imapc_mailbox_get_capabilities(mbox) < 0)
+ return -1;
+ if ((mbox->capabilities & IMAPC_CAPABILITY_NAMESPACE) == 0) {
+ /* NAMESPACE capability not supported */
+ return 0;
+ }
+
+ imapc_simple_context_init(&sctx, storage->client);
+ cmd = imapc_client_cmd(storage->client->client,
+ imapc_simple_callback, &sctx);
+ imapc_command_set_flags(cmd, IMAPC_COMMAND_FLAG_RETRIABLE);
+ imapc_command_send(cmd, "NAMESPACE");
+ imapc_simple_run(&sctx, &cmd);
+
+ if (sctx.ret < 0)
+ return -1;
+ storage->namespaces_requested = TRUE;
+ return 0;
+}
+
+static const struct imapc_namespace *
+imapc_namespace_find_mailbox(struct imapc_storage *storage,
+ const char *remote_name)
+{
+ const struct imapc_namespace *ns, *best_ns = NULL;
+ size_t best_len = UINT_MAX, len;
+
+ array_foreach(&storage->remote_namespaces, ns) {
+ len = strlen(ns->prefix);
+ if (str_begins(remote_name, ns->prefix)) {
+ if (best_len > len) {
+ best_ns = ns;
+ best_len = len;
+ }
+ }
+ }
+ return best_ns;
+}
+
+static int imapc_mailbox_get_metadata(struct mailbox *box,
+ enum mailbox_metadata_items items,
+ struct mailbox_metadata *metadata_r)
+{
+ struct imapc_mailbox *mbox = IMAPC_MAILBOX(box);
+ const struct imapc_namespace *ns;
+
+ if ((items & MAILBOX_METADATA_GUID) != 0) {
+ /* a bit ugly way to do this, but better than nothing for now.
+ FIXME: if indexes are enabled, keep this there. */
+ mail_generate_guid_128_hash(box->name, metadata_r->guid);
+ items &= ENUM_NEGATE(MAILBOX_METADATA_GUID);
+ }
+ if ((items & MAILBOX_METADATA_BACKEND_NAMESPACE) != 0) {
+ if (imapc_mailbox_get_namespaces(mbox) < 0)
+ return -1;
+
+ const char *remote_name = imapc_mailbox_get_remote_name(mbox);
+ ns = imapc_namespace_find_mailbox(mbox->storage, remote_name);
+ if (ns != NULL) {
+ metadata_r->backend_ns_prefix = ns->prefix;
+ metadata_r->backend_ns_type = ns->type;
+ }
+ items &= ENUM_NEGATE(MAILBOX_METADATA_BACKEND_NAMESPACE);
+ }
+ if (items != 0) {
+ if (index_mailbox_get_metadata(box, items, metadata_r) < 0)
+ return -1;
+ }
+ return 0;
+}
+
+static void imapc_noop_callback(const struct imapc_command_reply *reply,
+ void *context)
+
+{
+ struct imapc_storage *storage = context;
+
+ if (reply->state == IMAPC_COMMAND_STATE_OK)
+ ;
+ else if (reply->state == IMAPC_COMMAND_STATE_NO)
+ imapc_copy_error_from_reply(storage, MAIL_ERROR_PARAMS, reply);
+ else if (reply->state == IMAPC_COMMAND_STATE_DISCONNECTED)
+ mail_storage_set_internal_error(&storage->storage);
+ else {
+ mail_storage_set_critical(&storage->storage,
+ "imapc: NOOP failed: %s", reply->text_full);
+ }
+}
+
+static void imapc_idle_timeout(struct imapc_mailbox *mbox)
+{
+ struct imapc_command *cmd;
+
+ cmd = imapc_client_mailbox_cmd(mbox->client_box,
+ imapc_noop_callback, mbox->storage);
+ imapc_command_send(cmd, "NOOP");
+}
+
+static void imapc_idle_noop_callback(const struct imapc_command_reply *reply,
+ void *context)
+
+{
+ struct imapc_mailbox *mbox = context;
+
+ imapc_noop_callback(reply, mbox->box.storage);
+ if (mbox->client_box != NULL)
+ imapc_client_mailbox_idle(mbox->client_box);
+}
+
+static void imapc_notify_changes(struct mailbox *box)
+{
+ struct imapc_mailbox *mbox = IMAPC_MAILBOX(box);
+ const struct mail_storage_settings *set = box->storage->set;
+ struct imapc_command *cmd;
+
+ if (box->notify_callback == NULL) {
+ timeout_remove(&mbox->to_idle_check);
+ return;
+ }
+
+ if ((mbox->capabilities & IMAPC_CAPABILITY_IDLE) != 0) {
+ /* remote server is already in IDLE. but since some servers
+ don't notice changes immediately, we'll force them to check
+ here by sending a NOOP. this helps with clients that break
+ IDLE when clicking "get mail". */
+ cmd = imapc_client_mailbox_cmd(mbox->client_box,
+ imapc_idle_noop_callback, mbox);
+ imapc_command_send(cmd, "NOOP");
+ } else {
+ /* remote server doesn't support IDLE.
+ check for changes with NOOP every once in a while. */
+ i_assert(!imapc_client_is_running(mbox->storage->client->client));
+ mbox->to_idle_check =
+ timeout_add(set->mailbox_idle_check_interval * 1000,
+ imapc_idle_timeout, mbox);
+ }
+}
+
+static bool imapc_is_inconsistent(struct mailbox *box)
+{
+ struct imapc_mailbox *mbox = IMAPC_MAILBOX(box);
+
+ if (box->view != NULL &&
+ mail_index_view_is_inconsistent(box->view))
+ return TRUE;
+
+ return mbox->client_box == NULL ? FALSE :
+ !imapc_client_mailbox_is_opened(mbox->client_box);
+}
+
+struct mail_storage imapc_storage = {
+ .name = IMAPC_STORAGE_NAME,
+ .class_flags = MAIL_STORAGE_CLASS_FLAG_NO_ROOT |
+ MAIL_STORAGE_CLASS_FLAG_UNIQUE_ROOT |
+ MAIL_STORAGE_CLASS_FLAG_SECONDARY_INDEX,
+ .event_category = &event_category_imapc,
+
+ .v = {
+ imapc_get_setting_parser_info,
+ imapc_storage_alloc,
+ imapc_storage_create,
+ imapc_storage_destroy,
+ NULL,
+ imapc_storage_get_list_settings,
+ NULL,
+ imapc_mailbox_alloc,
+ NULL,
+ NULL,
+ }
+};
+
+static int
+imapc_mailbox_transaction_commit(struct mailbox_transaction_context *t,
+ struct mail_transaction_commit_changes *changes_r)
+{
+ int ret = imapc_transaction_save_commit(t);
+ int ret2 = index_transaction_commit(t, changes_r);
+ return ret >= 0 && ret2 >= 0 ? 0 : -1;
+}
+
+struct mailbox imapc_mailbox = {
+ .v = {
+ index_storage_is_readonly,
+ index_storage_mailbox_enable,
+ imapc_mailbox_exists,
+ imapc_mailbox_open,
+ imapc_mailbox_close,
+ index_storage_mailbox_free,
+ imapc_mailbox_create,
+ imapc_mailbox_update,
+ imapc_mailbox_delete,
+ index_storage_mailbox_rename,
+ imapc_mailbox_get_status,
+ imapc_mailbox_get_metadata,
+ index_storage_set_subscribed,
+ index_storage_attribute_set,
+ index_storage_attribute_get,
+ index_storage_attribute_iter_init,
+ index_storage_attribute_iter_next,
+ index_storage_attribute_iter_deinit,
+ NULL,
+ NULL,
+ imapc_mailbox_sync_init,
+ index_mailbox_sync_next,
+ imapc_mailbox_sync_deinit,
+ NULL,
+ imapc_notify_changes,
+ index_transaction_begin,
+ imapc_mailbox_transaction_commit,
+ index_transaction_rollback,
+ NULL,
+ imapc_mail_alloc,
+ imapc_search_init,
+ imapc_search_deinit,
+ index_storage_search_next_nonblock,
+ imapc_search_next_update_seq,
+ index_storage_search_next_match_mail,
+ imapc_save_alloc,
+ imapc_save_begin,
+ imapc_save_continue,
+ imapc_save_finish,
+ imapc_save_cancel,
+ imapc_copy,
+ imapc_transaction_save_commit_pre,
+ imapc_transaction_save_commit_post,
+ imapc_transaction_save_rollback,
+ imapc_is_inconsistent
+ }
+};
diff --git a/src/lib-storage/index/imapc/imapc-storage.h b/src/lib-storage/index/imapc/imapc-storage.h
new file mode 100644
index 0000000..e02c568
--- /dev/null
+++ b/src/lib-storage/index/imapc/imapc-storage.h
@@ -0,0 +1,268 @@
+#ifndef IMAPC_STORAGE_H
+#define IMAPC_STORAGE_H
+
+#include "index-storage.h"
+#include "imapc-settings.h"
+#include "imapc-client.h"
+
+#define IMAPC_STORAGE_NAME "imapc"
+/* storage_name separator */
+#define IMAPC_LIST_STORAGE_NAME_ESCAPE_CHAR '%'
+/* fs_name separator */
+#define IMAPC_LIST_FS_NAME_ESCAPE_CHAR '%'
+/* vname separator */
+#define IMAPC_LIST_VNAME_ESCAPE_CHAR '~'
+
+struct imap_arg;
+struct imapc_untagged_reply;
+struct imapc_command_reply;
+struct imapc_mailbox;
+struct imapc_storage_client;
+
+typedef void imapc_storage_callback_t(const struct imapc_untagged_reply *reply,
+ struct imapc_storage_client *client);
+typedef void imapc_mailbox_callback_t(const struct imapc_untagged_reply *reply,
+ struct imapc_mailbox *mbox);
+
+struct imapc_storage_event_callback {
+ char *name;
+ imapc_storage_callback_t *callback;
+};
+
+struct imapc_mailbox_event_callback {
+ const char *name;
+ imapc_mailbox_callback_t *callback;
+};
+
+#define IMAPC_HAS_FEATURE(mstorage, feature) \
+ (((mstorage)->set->parsed_features & feature) != 0)
+#define IMAPC_BOX_HAS_FEATURE(mbox, feature) \
+ (((mbox)->storage->set->parsed_features & feature) != 0)
+
+struct imapc_namespace {
+ const char *prefix;
+ char separator;
+ enum mail_namespace_type type;
+};
+
+struct imapc_storage_client {
+ int refcount;
+
+ /* either one of these may not be available: */
+ struct imapc_storage *_storage;
+ struct imapc_mailbox_list *_list;
+
+ struct imapc_client *client;
+
+ ARRAY(struct imapc_storage_event_callback) untagged_callbacks;
+
+ /* IMAPC_COMMAND_STATE_OK if no auth failure (yet), otherwise result to
+ the LOGIN/AUTHENTICATE command. */
+ enum imapc_command_state auth_failed_state;
+ char *auth_failed_reason;
+
+ /* Authentication reply was received (success or failure) */
+ bool auth_returned:1;
+ bool destroying:1;
+};
+
+struct imapc_storage {
+ struct mail_storage storage;
+ const struct imapc_settings *set;
+
+ struct ioloop *root_ioloop;
+ struct imapc_storage_client *client;
+
+ struct imapc_mailbox *cur_status_box;
+ struct mailbox_status *cur_status;
+ unsigned int reopen_count;
+
+ ARRAY(struct imapc_namespace) remote_namespaces;
+
+ bool namespaces_requested:1;
+};
+
+struct imapc_mail_cache {
+ uint32_t uid;
+
+ /* either fd != -1 or buf != NULL */
+ int fd;
+ buffer_t *buf;
+};
+
+struct imapc_fetch_request {
+ ARRAY(struct imapc_mail *) mails;
+};
+
+struct imapc_untagged_fetch_ctx {
+ pool_t pool;
+
+ /* keywords, flags, guid, modseq and fetch_uid may or may not be
+ received with an untagged fetch response */
+ ARRAY_TYPE(const_string) keywords;
+ /* Is set if have_flags is TRUE */
+ enum mail_flags flags;
+ const char *guid;
+ uint64_t modseq;
+ uint32_t fetch_uid;
+
+ /* uid is generated locally based on the remote MSN or fetch_uid */
+ uint32_t uid;
+
+ bool have_gmail_labels:1;
+ bool have_flags:1;
+};
+
+struct imapc_copy_request {
+ struct imapc_save_context *sctx;
+ struct seqset_builder *uidset_builder;
+};
+
+struct imapc_mailbox {
+ struct mailbox box;
+ struct imapc_storage *storage;
+ struct imapc_client_mailbox *client_box;
+ enum imapc_capability capabilities;
+
+ struct mail_index_transaction *delayed_sync_trans;
+ struct mail_index_view *sync_view, *delayed_sync_view;
+ struct mail_cache_view *delayed_sync_cache_view;
+ struct mail_cache_transaction_ctx *delayed_sync_cache_trans;
+ struct timeout *to_idle_check, *to_idle_delay;
+
+ ARRAY(struct imapc_fetch_request *) fetch_requests;
+ ARRAY(struct imapc_untagged_fetch_ctx *) untagged_fetch_contexts;
+ /* if non-empty, contains the latest FETCH command we're going to be
+ sending soon (but still waiting to see if we can increase its
+ UID range) */
+ string_t *pending_fetch_cmd;
+ /* if non-empty, contains the latest COPY command we're going to be
+ sending soon. */
+ string_t *pending_copy_cmd;
+ char *copy_dest_box;
+ struct imapc_fetch_request *pending_fetch_request;
+ struct imapc_copy_request *pending_copy_request;
+ struct timeout *to_pending_fetch_send;
+
+ ARRAY(struct imapc_mailbox_event_callback) untagged_callbacks;
+ ARRAY(struct imapc_mailbox_event_callback) resp_text_callbacks;
+
+ enum mail_flags permanent_flags;
+ uint32_t highest_nonrecent_uid;
+
+ ARRAY(uint64_t) rseq_modseqs;
+ ARRAY_TYPE(seq_range) delayed_expunged_uids;
+ ARRAY_TYPE(seq_range) copy_rollback_expunge_uids;
+ uint32_t sync_uid_validity;
+ uint32_t sync_uid_next;
+ uint64_t sync_highestmodseq;
+ uint32_t sync_fetch_first_uid;
+ uint32_t sync_next_lseq;
+ uint32_t sync_next_rseq;
+ uint32_t exists_count;
+ uint32_t min_append_uid;
+ char *sync_gmail_pop3_search_tag;
+
+ /* keep the previous fetched message body cached,
+ mainly for partial IMAP fetches */
+ struct imapc_mail_cache prev_mail_cache;
+
+ uint32_t prev_skipped_rseq, prev_skipped_uid;
+ struct imapc_sync_context *sync_ctx;
+
+ const char *guid_fetch_field_name;
+ struct imapc_search_context *search_ctx;
+
+ bool selecting:1;
+ bool syncing:1;
+ bool initial_sync_done:1;
+ bool selected:1;
+ bool exists_received:1;
+ bool state_fetching_uid1:1;
+ bool state_fetched_success:1;
+ bool rollback_pending:1;
+ bool delayed_untagged_exists:1;
+};
+
+struct imapc_simple_context {
+ struct imapc_storage_client *client;
+ int ret;
+};
+
+#define IMAPC_STORAGE(s) container_of(s, struct imapc_storage, storage)
+#define IMAPC_MAILBOX(s) container_of(s, struct imapc_mailbox, box)
+
+int imapc_storage_client_create(struct mail_namespace *ns,
+ const struct imapc_settings *imapc_set,
+ const struct mail_storage_settings *mail_set,
+ struct imapc_storage_client **client_r,
+ const char **error_r);
+void imapc_storage_client_unref(struct imapc_storage_client **client);
+bool imapc_storage_client_handle_auth_failure(struct imapc_storage_client *client);
+
+struct mail_save_context *
+imapc_save_alloc(struct mailbox_transaction_context *_t);
+int imapc_save_begin(struct mail_save_context *ctx, struct istream *input);
+int imapc_save_continue(struct mail_save_context *ctx);
+int imapc_save_finish(struct mail_save_context *ctx);
+void imapc_save_cancel(struct mail_save_context *ctx);
+int imapc_copy(struct mail_save_context *ctx, struct mail *mail);
+
+int imapc_transaction_save_commit(struct mailbox_transaction_context *t);
+int imapc_transaction_save_commit_pre(struct mail_save_context *ctx);
+void imapc_transaction_save_commit_post(struct mail_save_context *ctx,
+ struct mail_index_transaction_commit_result *result);
+void imapc_transaction_save_rollback(struct mail_save_context *ctx);
+
+void imapc_mailbox_run(struct imapc_mailbox *mbox);
+void imapc_mailbox_run_nofetch(struct imapc_mailbox *mbox);
+void imapc_mail_cache_free(struct imapc_mail_cache *cache);
+int imapc_mailbox_select(struct imapc_mailbox *mbox);
+void imap_mailbox_select_finish(struct imapc_mailbox *mbox);
+
+bool imapc_mailbox_has_modseqs(struct imapc_mailbox *mbox);
+bool imapc_resp_text_code_parse(const char *str, enum mail_error *error_r);
+bool imapc_mail_error_to_resp_text_code(enum mail_error error, const char **str_r);
+void imapc_copy_error_from_reply(struct imapc_storage *storage,
+ enum mail_error default_error,
+ const struct imapc_command_reply *reply);
+void imapc_simple_context_init(struct imapc_simple_context *sctx,
+ struct imapc_storage_client *client);
+void imapc_simple_run(struct imapc_simple_context *sctx,
+ struct imapc_command **cmd);
+void imapc_simple_callback(const struct imapc_command_reply *reply,
+ void *context);
+int imapc_mailbox_commit_delayed_trans(struct imapc_mailbox *mbox,
+ bool force, bool *changes_r);
+bool imapc_mailbox_name_equals(struct imapc_mailbox *mbox,
+ const char *remote_name);
+void imapc_mailbox_noop(struct imapc_mailbox *mbox);
+void imapc_mailbox_set_corrupted(struct imapc_mailbox *mbox,
+ const char *reason, ...) ATTR_FORMAT(2, 3);
+const char *imapc_mailbox_get_remote_name(struct imapc_mailbox *mbox);
+
+void imapc_storage_client_register_untagged(struct imapc_storage_client *client,
+ const char *name,
+ imapc_storage_callback_t *callback);
+void imapc_storage_client_unregister_untagged(struct imapc_storage_client *client,
+ const char *name);
+void imapc_mailbox_register_untagged(struct imapc_mailbox *mbox,
+ const char *name,
+ imapc_mailbox_callback_t *callback);
+void imapc_mailbox_register_resp_text(struct imapc_mailbox *mbox,
+ const char *key,
+ imapc_mailbox_callback_t *callback);
+
+void imapc_mailbox_register_callbacks(struct imapc_mailbox *mbox);
+
+struct mail_index_view *
+imapc_mailbox_get_sync_view(struct imapc_mailbox *mbox);
+
+void imapc_untagged_fetch_ctx_free(struct imapc_untagged_fetch_ctx **_ctx);
+void imapc_untagged_fetch_update_flags(struct imapc_mailbox *mbox,
+ struct imapc_untagged_fetch_ctx *ctx,
+ struct mail_index_view *view,
+ uint32_t lseq);
+bool imapc_mailbox_fetch_state(struct imapc_mailbox *mbox, uint32_t first_uid);
+
+#endif
diff --git a/src/lib-storage/index/imapc/imapc-sync.c b/src/lib-storage/index/imapc/imapc-sync.c
new file mode 100644
index 0000000..3130121
--- /dev/null
+++ b/src/lib-storage/index/imapc/imapc-sync.c
@@ -0,0 +1,702 @@
+/* Copyright (c) 2007-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "ioloop.h"
+#include "hash.h"
+#include "str.h"
+#include "sort.h"
+#include "imap-util.h"
+#include "mail-cache.h"
+#include "mail-index-modseq.h"
+#include "index-sync-private.h"
+#include "imapc-msgmap.h"
+#include "imapc-list.h"
+#include "imapc-storage.h"
+#include "imapc-sync.h"
+
+struct imapc_sync_command {
+ struct imapc_sync_context *ctx;
+ char *cmd_str;
+ bool ignore_no;
+};
+
+static void imapc_sync_callback(const struct imapc_command_reply *reply,
+ void *context)
+{
+ struct imapc_sync_command *cmd = context;
+ struct imapc_sync_context *ctx = cmd->ctx;
+
+ i_assert(ctx->sync_command_count > 0);
+
+ if (reply->state == IMAPC_COMMAND_STATE_OK)
+ ;
+ else if (reply->state == IMAPC_COMMAND_STATE_NO && cmd->ignore_no) {
+ /* maybe the message was expunged already.
+ some servers fail STOREs with NO in such situation. */
+ } else if (reply->state == IMAPC_COMMAND_STATE_DISCONNECTED) {
+ /* the disconnection is already logged, don't flood
+ the logs unnecessarily */
+ mail_storage_set_internal_error(&ctx->mbox->storage->storage);
+ ctx->failed = TRUE;
+ } else {
+ mailbox_set_critical(&ctx->mbox->box,
+ "imapc: Sync command '%s' failed: %s",
+ cmd->cmd_str, reply->text_full);
+ ctx->failed = TRUE;
+ }
+
+ if (--ctx->sync_command_count == 0)
+ imapc_client_stop(ctx->mbox->storage->client->client);
+ i_free(cmd->cmd_str);
+ i_free(cmd);
+}
+
+static struct imapc_command *
+imapc_sync_cmd_full(struct imapc_sync_context *ctx, const char *cmd_str,
+ bool ignore_no)
+{
+ struct imapc_sync_command *sync_cmd;
+ struct imapc_command *cmd;
+
+ sync_cmd = i_new(struct imapc_sync_command, 1);
+ sync_cmd->ctx = ctx;
+ sync_cmd->cmd_str = i_strdup(cmd_str);
+ sync_cmd->ignore_no = ignore_no;
+
+ ctx->sync_command_count++;
+ cmd = imapc_client_mailbox_cmd(ctx->mbox->client_box,
+ imapc_sync_callback, sync_cmd);
+ imapc_command_set_flags(cmd, IMAPC_COMMAND_FLAG_RETRIABLE);
+ imapc_command_send(cmd, cmd_str);
+ return cmd;
+}
+
+static struct imapc_command *
+imapc_sync_cmd(struct imapc_sync_context *ctx, const char *cmd_str)
+{
+ return imapc_sync_cmd_full(ctx, cmd_str, FALSE);
+}
+
+static unsigned int imapc_sync_store_hash(const struct imapc_sync_store *store)
+{
+ return str_hash(store->flags) ^ store->modify_type;
+}
+
+static int imapc_sync_store_cmp(const struct imapc_sync_store *store1,
+ const struct imapc_sync_store *store2)
+{
+ if (store1->modify_type != store2->modify_type)
+ return 1;
+ return strcmp(store1->flags, store2->flags);
+}
+
+static const char *imapc_sync_flags_sort(const char *flags)
+{
+ if (strchr(flags, ' ') == NULL)
+ return flags;
+
+ const char **str = t_strsplit(flags, " ");
+ i_qsort(str, str_array_length(str), sizeof(const char *),
+ i_strcasecmp_p);
+ return t_strarray_join(str, " ");
+}
+
+static void
+imapc_sync_store_flush(struct imapc_sync_context *ctx)
+{
+ struct imapc_sync_store *store;
+ const char *sorted_flags;
+
+ if (ctx->prev_uid1 == 0)
+ return;
+
+ sorted_flags = imapc_sync_flags_sort(str_c(ctx->prev_flags));
+ struct imapc_sync_store store_lookup = {
+ .modify_type = ctx->prev_modify_type,
+ .flags = sorted_flags,
+ };
+ store = hash_table_lookup(ctx->stores, &store_lookup);
+ if (store == NULL) {
+ store = p_new(ctx->pool, struct imapc_sync_store, 1);
+ store->modify_type = ctx->prev_modify_type;
+ store->flags = p_strdup(ctx->pool, sorted_flags);
+ p_array_init(&store->uids, ctx->pool, 4);
+ hash_table_insert(ctx->stores, store, store);
+ }
+ seq_range_array_add_range(&store->uids, ctx->prev_uid1, ctx->prev_uid2);
+}
+
+static void
+imapc_sync_store(struct imapc_sync_context *ctx,
+ enum modify_type modify_type, uint32_t uid1, uint32_t uid2,
+ const char *flags)
+{
+ if (ctx->prev_flags == NULL) {
+ ctx->prev_flags = str_new(ctx->pool, 128);
+ hash_table_create(&ctx->stores, ctx->pool, 0,
+ imapc_sync_store_hash, imapc_sync_store_cmp);
+ }
+
+ if (ctx->prev_uid1 != uid1 || ctx->prev_uid2 != uid2 ||
+ ctx->prev_modify_type != modify_type) {
+ imapc_sync_store_flush(ctx);
+ ctx->prev_uid1 = uid1;
+ ctx->prev_uid2 = uid2;
+ ctx->prev_modify_type = modify_type;
+ str_truncate(ctx->prev_flags, 0);
+ }
+ if (str_len(ctx->prev_flags) > 0)
+ str_append_c(ctx->prev_flags, ' ');
+ str_append(ctx->prev_flags, flags);
+}
+
+static void
+imapc_sync_finish_store(struct imapc_sync_context *ctx)
+{
+ struct hash_iterate_context *iter;
+ struct imapc_sync_store *store;
+ string_t *cmd = t_str_new(128);
+
+ imapc_sync_store_flush(ctx);
+
+ if (!hash_table_is_created(ctx->stores))
+ return;
+
+ iter = hash_table_iterate_init(ctx->stores);
+ while (hash_table_iterate(iter, ctx->stores, &store, &store)) {
+ str_truncate(cmd, 0);
+ str_append(cmd, "UID STORE ");
+ imap_write_seq_range(cmd, &store->uids);
+ str_printfa(cmd, " %cFLAGS (%s)",
+ store->modify_type == MODIFY_ADD ? '+' : '-',
+ store->flags);
+ imapc_sync_cmd_full(ctx, str_c(cmd), TRUE);
+ }
+ hash_table_iterate_deinit(&iter);
+ hash_table_destroy(&ctx->stores);
+}
+
+static void
+imapc_sync_add_missing_deleted_flags(struct imapc_sync_context *ctx,
+ uint32_t seq1, uint32_t seq2)
+{
+ const struct mail_index_record *rec;
+ uint32_t seq, uid1, uid2;
+
+ /* if any of them has a missing \Deleted flag,
+ just add it to all of them. */
+ for (seq = seq1; seq <= seq2; seq++) {
+ rec = mail_index_lookup(ctx->sync_view, seq);
+ if ((rec->flags & MAIL_DELETED) == 0)
+ break;
+ }
+
+ if (seq <= seq2) {
+ mail_index_lookup_uid(ctx->sync_view, seq1, &uid1);
+ mail_index_lookup_uid(ctx->sync_view, seq2, &uid2);
+
+ imapc_sync_store(ctx, MODIFY_ADD, uid1, uid2, "\\Deleted");
+ }
+}
+
+static void imapc_sync_index_flags(struct imapc_sync_context *ctx,
+ const struct mail_index_sync_rec *sync_rec)
+{
+ string_t *str = t_str_new(128);
+
+ i_assert(sync_rec->type == MAIL_INDEX_SYNC_TYPE_FLAGS);
+
+ if (sync_rec->add_flags != 0) {
+ i_assert((sync_rec->add_flags & MAIL_RECENT) == 0);
+
+ imap_write_flags(str, sync_rec->add_flags, NULL);
+ imapc_sync_store(ctx, MODIFY_ADD, sync_rec->uid1,
+ sync_rec->uid2, str_c(str));
+ }
+
+ if (sync_rec->remove_flags != 0) {
+ i_assert((sync_rec->remove_flags & MAIL_RECENT) == 0);
+ str_truncate(str, 0);
+ imap_write_flags(str, sync_rec->remove_flags, NULL);
+ imapc_sync_store(ctx, MODIFY_REMOVE, sync_rec->uid1,
+ sync_rec->uid2, str_c(str));
+ }
+}
+
+static void
+imapc_sync_index_keyword(struct imapc_sync_context *ctx,
+ const struct mail_index_sync_rec *sync_rec)
+{
+ const char *kw_str;
+ enum modify_type modify_type;
+
+ switch (sync_rec->type) {
+ case MAIL_INDEX_SYNC_TYPE_KEYWORD_ADD:
+ modify_type = MODIFY_ADD;
+ break;
+ case MAIL_INDEX_SYNC_TYPE_KEYWORD_REMOVE:
+ modify_type = MODIFY_REMOVE;
+ break;
+ default:
+ i_unreached();
+ }
+
+ kw_str = array_idx_elem(ctx->keywords, sync_rec->keyword_idx);
+ imapc_sync_store(ctx, modify_type, sync_rec->uid1,
+ sync_rec->uid2, kw_str);
+}
+
+static void imapc_sync_expunge_finish(struct imapc_sync_context *ctx)
+{
+ string_t *str;
+
+ if (array_count(&ctx->expunged_uids) == 0)
+ return;
+
+ if ((ctx->mbox->capabilities & IMAPC_CAPABILITY_UIDPLUS) == 0) {
+ /* just expunge everything */
+ imapc_sync_cmd(ctx, "EXPUNGE");
+ return;
+ }
+
+ /* build a list of UIDs to expunge */
+ str = t_str_new(128);
+ str_append(str, "UID EXPUNGE ");
+ imap_write_seq_range(str, &ctx->expunged_uids);
+ imapc_sync_cmd(ctx, str_c(str));
+}
+
+static void imapc_sync_uid_next(struct imapc_sync_context *ctx)
+{
+ struct imapc_mailbox *mbox = ctx->mbox;
+ const struct mail_index_header *hdr;
+ uint32_t uid_next = mbox->sync_uid_next;
+
+ if (uid_next < mbox->min_append_uid)
+ uid_next = mbox->min_append_uid;
+
+ hdr = mail_index_get_header(ctx->sync_view);
+ if (hdr->next_uid < uid_next) {
+ mail_index_update_header(ctx->trans,
+ offsetof(struct mail_index_header, next_uid),
+ &uid_next, sizeof(uid_next), FALSE);
+ }
+}
+
+static void imapc_sync_highestmodseq(struct imapc_sync_context *ctx)
+{
+ if (imapc_mailbox_has_modseqs(ctx->mbox) &&
+ mail_index_modseq_get_highest(ctx->sync_view) < ctx->mbox->sync_highestmodseq)
+ mail_index_update_highest_modseq(ctx->trans, ctx->mbox->sync_highestmodseq);
+}
+
+static void
+imapc_initial_sync_check(struct imapc_sync_context *ctx, bool nooped)
+{
+ struct imapc_msgmap *msgmap =
+ imapc_client_mailbox_get_msgmap(ctx->mbox->client_box);
+ struct mail_index_view *view = ctx->mbox->delayed_sync_view;
+ const struct mail_index_header *hdr = mail_index_get_header(view);
+ uint32_t rseq, lseq, ruid, luid, rcount, lcount;
+
+ rseq = lseq = 1;
+ rcount = imapc_msgmap_count(msgmap);
+ lcount = mail_index_view_get_messages_count(view);
+ while (rseq <= rcount || lseq <= lcount) {
+ if (rseq <= rcount)
+ ruid = imapc_msgmap_rseq_to_uid(msgmap, rseq);
+ else
+ ruid = (uint32_t)-1;
+ if (lseq <= lcount)
+ mail_index_lookup_uid(view, lseq, &luid);
+ else
+ luid = (uint32_t)-1;
+
+ if (ruid == luid) {
+ /* message exists in index and in remote server */
+ lseq++; rseq++;
+ } else if (luid < ruid) {
+ /* message exists in index but not in remote server */
+ if (luid >= ctx->mbox->sync_uid_next) {
+ /* the message was added to index by another
+ imapc session, and it's not visible yet
+ in this session */
+ break;
+ }
+ /* it's already expunged and we should have marked it */
+ i_assert(mail_index_is_expunged(view, lseq) ||
+ seq_range_exists(&ctx->mbox->delayed_expunged_uids, luid));
+ lseq++;
+ } else {
+ /* message doesn't exist in index, but exists in
+ remote server */
+ if (lseq > lcount && ruid >= hdr->next_uid) {
+ /* the message hasn't been yet added to index */
+ break;
+ }
+
+ /* another imapc session expunged it =>
+ NOOP should send us an EXPUNGE event */
+ if (!nooped) {
+ imapc_mailbox_noop(ctx->mbox);
+ imapc_initial_sync_check(ctx, TRUE);
+ return;
+ }
+ /* already nooped => index is corrupted */
+ imapc_mailbox_set_corrupted(ctx->mbox,
+ "Expunged message uid=%u reappeared", ruid);
+ ctx->failed = TRUE;
+ return;
+ }
+ }
+}
+
+static void
+imapc_sync_send_commands(struct imapc_sync_context *ctx)
+{
+ if (ctx->mbox->exists_count == 0) {
+ /* empty mailbox - no point in fetching anything */
+ return;
+ }
+
+ if (IMAPC_BOX_HAS_FEATURE(ctx->mbox, IMAPC_FEATURE_GMAIL_MIGRATION) &&
+ ctx->mbox->storage->set->pop3_deleted_flag[0] != '\0') {
+ struct imapc_command *cmd;
+
+ cmd = imapc_sync_cmd(ctx, "SEARCH RETURN (ALL) X-GM-RAW \"in:^pop\"");
+ i_free(ctx->mbox->sync_gmail_pop3_search_tag);
+ ctx->mbox->sync_gmail_pop3_search_tag =
+ i_strdup(imapc_command_get_tag(cmd));
+ }
+}
+
+static void imapc_sync_index(struct imapc_sync_context *ctx)
+{
+ struct imapc_mailbox *mbox = ctx->mbox;
+ struct mail_index_sync_rec sync_rec;
+ uint32_t seq1, seq2;
+
+ i_array_init(&ctx->expunged_uids, 64);
+ ctx->keywords = mail_index_get_keywords(mbox->box.index);
+ ctx->pool = pool_alloconly_create("imapc sync pool", 1024);
+
+ while (mail_index_sync_next(ctx->index_sync_ctx, &sync_rec)) T_BEGIN {
+ if (!mail_index_lookup_seq_range(ctx->sync_view,
+ sync_rec.uid1, sync_rec.uid2,
+ &seq1, &seq2)) {
+ /* already expunged, nothing to do. */
+ } else switch (sync_rec.type) {
+ case MAIL_INDEX_SYNC_TYPE_EXPUNGE:
+ imapc_sync_add_missing_deleted_flags(ctx, seq1, seq2);
+ seq_range_array_add_range(&ctx->expunged_uids,
+ sync_rec.uid1, sync_rec.uid2);
+ break;
+ case MAIL_INDEX_SYNC_TYPE_FLAGS:
+ imapc_sync_index_flags(ctx, &sync_rec);
+ break;
+ case MAIL_INDEX_SYNC_TYPE_KEYWORD_ADD:
+ case MAIL_INDEX_SYNC_TYPE_KEYWORD_REMOVE:
+ imapc_sync_index_keyword(ctx, &sync_rec);
+ break;
+ }
+ } T_END;
+ imapc_sync_finish_store(ctx);
+ pool_unref(&ctx->pool);
+
+ if (!mbox->initial_sync_done)
+ imapc_sync_send_commands(ctx);
+
+ imapc_sync_expunge_finish(ctx);
+ while (ctx->sync_command_count > 0)
+ imapc_mailbox_run(mbox);
+ array_free(&ctx->expunged_uids);
+
+ if (!mbox->state_fetched_success) {
+ /* All the sync commands succeeded, but we got disconnected.
+ imapc_initial_sync_check() will crash if we go there. */
+ ctx->failed = TRUE;
+ }
+
+ /* add uidnext & highestmodseq after all appends */
+ imapc_sync_uid_next(ctx);
+ imapc_sync_highestmodseq(ctx);
+
+ mailbox_sync_notify(&mbox->box, 0, 0);
+
+ if (!ctx->failed) {
+ /* reset only after a successful sync */
+ mbox->sync_fetch_first_uid = 0;
+ }
+ if (!mbox->initial_sync_done && !ctx->failed) {
+ imapc_initial_sync_check(ctx, FALSE);
+ mbox->initial_sync_done = TRUE;
+ }
+}
+
+static int
+imapc_sync_begin(struct imapc_mailbox *mbox,
+ struct imapc_sync_context **ctx_r, bool force)
+{
+ struct imapc_sync_context *ctx;
+ enum mail_index_sync_flags sync_flags;
+ int ret;
+
+ i_assert(!mbox->syncing);
+
+ ctx = i_new(struct imapc_sync_context, 1);
+ ctx->mbox = mbox;
+
+ sync_flags = index_storage_get_sync_flags(&mbox->box) |
+ MAIL_INDEX_SYNC_FLAG_FLUSH_DIRTY;
+ if (!force)
+ sync_flags |= MAIL_INDEX_SYNC_FLAG_REQUIRE_CHANGES;
+
+ ret = mail_index_sync_begin(mbox->box.index, &ctx->index_sync_ctx,
+ &ctx->sync_view, &ctx->trans,
+ sync_flags);
+ if (ret <= 0) {
+ if (ret < 0)
+ mailbox_set_index_error(&mbox->box);
+ i_free(ctx);
+ *ctx_r = NULL;
+ return ret;
+ }
+
+ i_assert(mbox->sync_view == NULL);
+ i_assert(mbox->delayed_sync_trans == NULL);
+ mbox->sync_view = ctx->sync_view;
+ mbox->delayed_sync_view =
+ mail_index_transaction_open_updated_view(ctx->trans);
+ mbox->delayed_sync_trans = ctx->trans;
+ mbox->delayed_sync_cache_view =
+ mail_cache_view_open(mbox->box.cache, mbox->delayed_sync_view);
+ mbox->delayed_sync_cache_trans =
+ mail_cache_get_transaction(mbox->delayed_sync_cache_view,
+ mbox->delayed_sync_trans);
+ mbox->min_append_uid = mail_index_get_header(ctx->sync_view)->next_uid;
+
+ mbox->syncing = TRUE;
+ mbox->sync_ctx = ctx;
+
+ if (mbox->delayed_untagged_exists) {
+ bool fetch_send = imapc_mailbox_fetch_state(mbox,
+ mbox->min_append_uid);
+ while (fetch_send && mbox->delayed_untagged_exists)
+ imapc_mailbox_run(mbox);
+ }
+
+ if (!mbox->box.deleting)
+ imapc_sync_index(ctx);
+
+ mail_index_view_close(&mbox->delayed_sync_view);
+ mbox->delayed_sync_trans = NULL;
+ mbox->sync_view = NULL;
+
+ *ctx_r = ctx;
+ return 0;
+}
+
+static int imapc_sync_finish(struct imapc_sync_context **_ctx)
+{
+ struct imapc_sync_context *ctx = *_ctx;
+ bool changes;
+ int ret = ctx->failed ? -1 : 0;
+
+ *_ctx = NULL;
+ /* Commit the transaction even if we failed. This is important, because
+ during the sync delayed_sync_trans points to the sync transaction.
+ Even if the syncing doesn't fully succeed, we don't want to lose
+ changes in delayed_sync_trans. */
+ if (mail_index_sync_commit(&ctx->index_sync_ctx) < 0) {
+ mailbox_set_index_error(&ctx->mbox->box);
+ ret = -1;
+ }
+ if (ctx->mbox->sync_gmail_pop3_search_tag != NULL) {
+ mailbox_set_critical(&ctx->mbox->box,
+ "gmail-pop3 search not successful");
+ i_free_and_null(ctx->mbox->sync_gmail_pop3_search_tag);
+ ret = -1;
+ }
+ mail_cache_view_close(&ctx->mbox->delayed_sync_cache_view);
+ ctx->mbox->delayed_sync_cache_trans = NULL;
+
+ ctx->mbox->syncing = FALSE;
+ ctx->mbox->sync_ctx = NULL;
+
+ /* this is done simply to commit delayed expunges if there are any
+ (has to be done after sync is committed) */
+ if (imapc_mailbox_commit_delayed_trans(ctx->mbox, FALSE, &changes) < 0)
+ ctx->failed = TRUE;
+
+ i_free(ctx);
+ return ret;
+}
+
+static int imapc_untagged_fetch_uid_cmp(struct imapc_untagged_fetch_ctx *const *ctx1,
+ struct imapc_untagged_fetch_ctx *const *ctx2)
+{
+ return (*ctx1)->uid < (*ctx2)->uid ? -1 :
+ (*ctx1)->uid > (*ctx2)->uid ? 1 : 0;
+}
+
+static void imapc_sync_handle_untagged_fetches(struct imapc_mailbox *mbox)
+{
+ struct imapc_untagged_fetch_ctx *untagged_fetch_context;
+ struct mail_index_view *updated_view;
+ uint32_t lseq;
+
+ i_assert(array_count(&mbox->untagged_fetch_contexts) > 0);
+ i_assert(mbox->delayed_sync_trans == NULL);
+
+ array_sort(&mbox->untagged_fetch_contexts, imapc_untagged_fetch_uid_cmp);
+
+ mbox->delayed_sync_trans =
+ mail_index_transaction_begin(imapc_mailbox_get_sync_view(mbox),
+ MAIL_INDEX_TRANSACTION_FLAG_EXTERNAL);
+
+ array_foreach_elem(&mbox->untagged_fetch_contexts, untagged_fetch_context) {
+ if (untagged_fetch_context->uid < mbox->min_append_uid ||
+ untagged_fetch_context->uid < mail_index_get_header(mbox->sync_view)->next_uid) {
+ /* The message was already added */
+ continue;
+ }
+
+ mail_index_append(mbox->delayed_sync_trans,
+ untagged_fetch_context->uid,
+ &lseq);
+ mbox->min_append_uid = untagged_fetch_context->uid + 1;
+ }
+
+ updated_view = mail_index_transaction_open_updated_view(mbox->delayed_sync_trans);
+
+ array_foreach_elem(&mbox->untagged_fetch_contexts, untagged_fetch_context) {
+ if (!untagged_fetch_context->have_flags) {
+ imapc_untagged_fetch_ctx_free(&untagged_fetch_context);
+ continue;
+ }
+
+ /* Lookup the mail belonging to this context using the
+ context->uid */
+ if (!mail_index_lookup_seq(updated_view,
+ untagged_fetch_context->uid,
+ &lseq)) {
+ /* mail is expunged already */
+ imapc_untagged_fetch_ctx_free(&untagged_fetch_context);
+ continue;
+ }
+
+ imapc_untagged_fetch_update_flags(mbox, untagged_fetch_context,
+ updated_view, lseq);
+ imapc_untagged_fetch_ctx_free(&untagged_fetch_context);
+ }
+
+ mail_index_view_close(&updated_view);
+ if (mail_index_transaction_commit(&mbox->delayed_sync_trans) < 0)
+ mailbox_set_index_error(&mbox->box);
+ array_clear(&mbox->untagged_fetch_contexts);
+}
+
+static int imapc_sync(struct imapc_mailbox *mbox)
+{
+ struct imapc_sync_context *sync_ctx;
+ bool force = mbox->sync_fetch_first_uid != 0;
+
+ if ((mbox->box.flags & MAILBOX_FLAG_SAVEONLY) != 0) {
+ /* we're only saving mails here - no syncing actually wanted */
+ return 0;
+ }
+
+ if (imapc_sync_begin(mbox, &sync_ctx, force) < 0)
+ return -1;
+
+ if (!array_is_empty(&mbox->untagged_fetch_contexts))
+ imapc_sync_handle_untagged_fetches(mbox);
+
+ if (sync_ctx == NULL)
+ return 0;
+ if (imapc_sync_finish(&sync_ctx) < 0)
+ return -1;
+ return 0;
+}
+
+static void
+imapc_noop_if_needed(struct imapc_mailbox *mbox, enum mailbox_sync_flags flags)
+{
+ if (!mbox->initial_sync_done) {
+ /* we just SELECTed/EXAMINEd the mailbox, don't do another
+ NOOP. */
+ } else if ((flags & MAILBOX_SYNC_FLAG_FAST) == 0 &&
+ ((mbox->capabilities & IMAPC_CAPABILITY_IDLE) == 0 ||
+ (flags & MAILBOX_SYNC_FLAG_FULL_READ) != 0)) {
+ /* do NOOP to make sure we have the latest changes before
+ starting sync. this is necessary either because se don't
+ support IDLE at all, or because we want to be sure that we
+ have the latest changes (IDLE is started with a small delay,
+ so we might not actually even be in IDLE right not) */
+ imapc_mailbox_noop(mbox);
+ }
+}
+
+static bool imapc_mailbox_need_initial_fetch(struct imapc_mailbox *mbox)
+{
+ if (mbox->box.deleting) {
+ /* If the mailbox is about to be deleted there is no need to
+ expect initial fetch to be done */
+ return FALSE;
+ }
+ if ((mbox->box.flags & MAILBOX_FLAG_SAVEONLY) != 0) {
+ /* The mailbox is opened only for saving there is no need to
+ expect initial fetchting do be done. */
+ return FALSE;
+ }
+ return TRUE;
+}
+
+struct mailbox_sync_context *
+imapc_mailbox_sync_init(struct mailbox *box, enum mailbox_sync_flags flags)
+{
+ struct imapc_mailbox *mbox = IMAPC_MAILBOX(box);
+ struct imapc_mailbox_list *list = mbox->storage->client->_list;
+ bool changes;
+ int ret = 0;
+
+ if (list != NULL) {
+ if (!list->refreshed_mailboxes &&
+ list->last_refreshed_mailboxes < ioloop_time)
+ list->refreshed_mailboxes_recently = FALSE;
+ }
+
+ imapc_noop_if_needed(mbox, flags);
+
+ if (imapc_storage_client_handle_auth_failure(mbox->storage->client))
+ ret = -1;
+ else if (!mbox->state_fetched_success && !mbox->state_fetching_uid1 &&
+ imapc_mailbox_need_initial_fetch(mbox)) {
+ /* initial FETCH failed already */
+ ret = -1;
+ }
+ if (imapc_mailbox_commit_delayed_trans(mbox, FALSE, &changes) < 0)
+ ret = -1;
+ if ((changes || mbox->sync_fetch_first_uid != 0 ||
+ index_mailbox_want_full_sync(&mbox->box, flags)) &&
+ ret == 0)
+ ret = imapc_sync(mbox);
+
+ return index_mailbox_sync_init(box, flags, ret < 0);
+}
+
+int imapc_mailbox_sync_deinit(struct mailbox_sync_context *ctx,
+ struct mailbox_sync_status *status_r)
+{
+ struct imapc_mailbox *mbox = IMAPC_MAILBOX(ctx->box);
+ int ret;
+
+ ret = index_mailbox_sync_deinit(ctx, status_r);
+ ctx = NULL;
+
+ if (mbox->client_box == NULL)
+ return ret;
+
+ imapc_client_mailbox_idle(mbox->client_box);
+ return ret;
+}
diff --git a/src/lib-storage/index/imapc/imapc-sync.h b/src/lib-storage/index/imapc/imapc-sync.h
new file mode 100644
index 0000000..54a0368
--- /dev/null
+++ b/src/lib-storage/index/imapc/imapc-sync.h
@@ -0,0 +1,39 @@
+#ifndef IMAPC_SYNC_H
+#define IMAPC_SYNC_H
+
+struct mailbox;
+struct mailbox_sync_status;
+
+struct imapc_sync_store {
+ enum modify_type modify_type;
+ const char *flags;
+
+ ARRAY_TYPE(seq_range) uids;
+};
+
+struct imapc_sync_context {
+ struct imapc_mailbox *mbox;
+ struct mail_index_sync_ctx *index_sync_ctx;
+ struct mail_index_view *sync_view;
+ struct mail_index_transaction *trans;
+
+ const ARRAY_TYPE(keywords) *keywords;
+ ARRAY_TYPE(seq_range) expunged_uids;
+ unsigned int sync_command_count;
+
+ pool_t pool;
+ HASH_TABLE(struct imapc_sync_store *, struct imapc_sync_store *) stores;
+
+ uint32_t prev_uid1, prev_uid2;
+ enum modify_type prev_modify_type;
+ string_t *prev_flags;
+
+ bool failed:1;
+};
+
+struct mailbox_sync_context *
+imapc_mailbox_sync_init(struct mailbox *box, enum mailbox_sync_flags flags);
+int imapc_mailbox_sync_deinit(struct mailbox_sync_context *ctx,
+ struct mailbox_sync_status *status_r);
+
+#endif
diff --git a/src/lib-storage/index/index-attachment.c b/src/lib-storage/index/index-attachment.c
new file mode 100644
index 0000000..6e51fab
--- /dev/null
+++ b/src/lib-storage/index/index-attachment.c
@@ -0,0 +1,446 @@
+/* Copyright (c) 2010-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "safe-mkstemp.h"
+#include "fs-api.h"
+#include "istream.h"
+#include "ostream.h"
+#include "base64.h"
+#include "hash-format.h"
+#include "str.h"
+#include "message-parser.h"
+#include "rfc822-parser.h"
+#include "fs-api.h"
+#include "istream-fs-file.h"
+#include "istream-attachment-connector.h"
+#include "istream-attachment-extractor.h"
+#include "mail-user.h"
+#include "index-mail.h"
+#include "index-attachment.h"
+
+enum mail_attachment_decode_option {
+ MAIL_ATTACHMENT_DECODE_OPTION_NONE = '-',
+ MAIL_ATTACHMENT_DECODE_OPTION_BASE64 = 'B',
+ MAIL_ATTACHMENT_DECODE_OPTION_CRLF = 'C'
+};
+
+struct mail_save_attachment {
+ pool_t pool;
+ struct fs *fs;
+ struct istream *input;
+
+ struct fs_file *cur_file;
+ ARRAY_TYPE(mail_attachment_extref) extrefs;
+};
+
+static const char *index_attachment_dir_get(struct mail_storage *storage)
+{
+ return mail_user_home_expand(storage->user,
+ storage->set->mail_attachment_dir);
+}
+
+static bool index_attachment_want(const struct istream_attachment_header *hdr,
+ void *context)
+{
+ struct mail_save_context *ctx = context;
+ struct mail_attachment_part apart;
+
+ i_zero(&apart);
+ apart.part = hdr->part;
+ apart.content_type = hdr->content_type;
+ apart.content_disposition = hdr->content_disposition;
+
+ if (ctx->part_is_attachment != NULL)
+ return ctx->part_is_attachment(ctx, &apart);
+
+ /* don't treat text/ parts as attachments */
+ return hdr->content_type != NULL &&
+ strncasecmp(hdr->content_type, "text/", 5) != 0;
+}
+
+static int index_attachment_open_temp_fd(void *context)
+{
+ struct mail_save_context *ctx = context;
+ struct mail_storage *storage = ctx->transaction->box->storage;
+ string_t *temp_path;
+ int fd;
+
+ temp_path = t_str_new(256);
+ mail_user_set_get_temp_prefix(temp_path, storage->user->set);
+ fd = safe_mkstemp_hostpid(temp_path, 0600, (uid_t)-1, (gid_t)-1);
+ if (fd == -1) {
+ mailbox_set_critical(ctx->transaction->box,
+ "safe_mkstemp(%s) failed: %m", str_c(temp_path));
+ return -1;
+ }
+ if (unlink(str_c(temp_path)) < 0) {
+ mailbox_set_critical(ctx->transaction->box,
+ "unlink(%s) failed: %m", str_c(temp_path));
+ i_close_fd(&fd);
+ return -1;
+ }
+ return fd;
+}
+
+static int
+index_attachment_open_ostream(struct istream_attachment_info *info,
+ struct ostream **output_r,
+ const char **error_r ATTR_UNUSED, void *context)
+{
+ struct mail_save_context *ctx = context;
+ struct mail_save_attachment *attach = ctx->data.attach;
+ struct mail_storage *storage = ctx->transaction->box->storage;
+ struct mail_attachment_extref *extref;
+ enum fs_open_flags flags = 0;
+ const char *attachment_dir, *path, *digest = info->hash;
+ guid_128_t guid_128;
+
+ i_assert(attach->cur_file == NULL);
+
+ if (storage->set->parsed_fsync_mode != FSYNC_MODE_NEVER)
+ flags |= FS_OPEN_FLAG_FSYNC;
+
+ if (strlen(digest) < 4) {
+ /* make sure we can access first 4 bytes without accessing
+ out of bounds memory */
+ digest = t_strconcat(digest, "\0\0\0\0", NULL);
+ }
+
+ guid_128_generate(guid_128);
+ attachment_dir = index_attachment_dir_get(storage);
+ path = t_strdup_printf("%s/%c%c/%c%c/%s-%s", attachment_dir,
+ digest[0], digest[1],
+ digest[2], digest[3], digest,
+ guid_128_to_string(guid_128));
+ attach->cur_file = fs_file_init(attach->fs, path,
+ FS_OPEN_MODE_REPLACE | flags);
+
+ extref = array_append_space(&attach->extrefs);
+ extref->start_offset = info->start_offset;
+ extref->size = info->encoded_size;
+ extref->path = p_strdup(attach->pool,
+ path + strlen(attachment_dir) + 1);
+ extref->base64_blocks_per_line = info->base64_blocks_per_line;
+ extref->base64_have_crlf = info->base64_have_crlf;
+
+ *output_r = fs_write_stream(attach->cur_file);
+ return 0;
+}
+
+static int
+index_attachment_close_ostream(struct ostream *output, bool success,
+ const char **error, void *context)
+{
+ struct mail_save_context *ctx = context;
+ struct mail_save_attachment *attach = ctx->data.attach;
+ int ret = success ? 0 : -1;
+
+ i_assert(attach->cur_file != NULL);
+
+ if (ret < 0)
+ fs_write_stream_abort_error(attach->cur_file, &output, "%s", *error);
+ else if (fs_write_stream_finish(attach->cur_file, &output) < 0) {
+ *error = t_strdup_printf("Couldn't create attachment %s: %s",
+ fs_file_path(attach->cur_file),
+ fs_file_last_error(attach->cur_file));
+ ret = -1;
+ }
+ fs_file_deinit(&attach->cur_file);
+
+ if (ret < 0) {
+ array_pop_back(&attach->extrefs);
+ }
+ return ret;
+}
+
+void index_attachment_save_begin(struct mail_save_context *ctx,
+ struct fs *fs, struct istream *input)
+{
+ struct mail_storage *storage = ctx->transaction->box->storage;
+ struct mail_save_attachment *attach;
+ struct istream_attachment_settings set;
+ const char *error;
+ pool_t pool;
+
+ i_assert(ctx->data.attach == NULL);
+
+ if (*storage->set->mail_attachment_dir == '\0')
+ return;
+
+ i_zero(&set);
+ set.min_size = storage->set->mail_attachment_min_size;
+ if (hash_format_init(storage->set->mail_attachment_hash,
+ &set.hash_format, &error) < 0) {
+ /* we already checked this when verifying settings */
+ i_panic("mail_attachment_hash=%s unexpectedly failed: %s",
+ storage->set->mail_attachment_hash, error);
+ }
+ set.want_attachment = index_attachment_want;
+ set.open_temp_fd = index_attachment_open_temp_fd;
+ set.open_attachment_ostream = index_attachment_open_ostream;
+ set.close_attachment_ostream = index_attachment_close_ostream;
+
+ pool = pool_alloconly_create("save attachment", 1024);
+ attach = p_new(pool, struct mail_save_attachment, 1);
+ attach->pool = pool;
+ attach->fs = fs;
+ attach->input = i_stream_create_attachment_extractor(input, &set, ctx);
+ p_array_init(&attach->extrefs, attach->pool, 8);
+ ctx->data.attach = attach;
+}
+
+static int save_check_write_error(struct mail_save_context *ctx,
+ struct ostream *output)
+{
+ struct mail_storage *storage = ctx->transaction->box->storage;
+
+ if (output->stream_errno == 0)
+ return 0;
+
+ if (!mail_storage_set_error_from_errno(storage)) {
+ mail_set_critical(ctx->dest_mail, "write(%s) failed: %s",
+ o_stream_get_name(output), o_stream_get_error(output));
+ }
+ return -1;
+}
+
+int index_attachment_save_continue(struct mail_save_context *ctx)
+{
+ struct mail_save_attachment *attach = ctx->data.attach;
+ const unsigned char *data;
+ size_t size;
+ ssize_t ret;
+
+ if (attach->input->stream_errno != 0)
+ return -1;
+
+ do {
+ ret = i_stream_read(attach->input);
+ if (ret > 0 || ret == -2) {
+ data = i_stream_get_data(attach->input, &size);
+ o_stream_nsend(ctx->data.output, data, size);
+ i_stream_skip(attach->input, size);
+ }
+ index_mail_cache_parse_continue(ctx->dest_mail);
+ if (ret == 0 && !i_stream_attachment_extractor_can_retry(attach->input)) {
+ /* need more input */
+ return 0;
+ }
+ } while (ret != -1);
+
+ if (attach->input->stream_errno != 0) {
+ mail_set_critical(ctx->dest_mail, "read(%s) failed: %s",
+ i_stream_get_name(attach->input),
+ i_stream_get_error(attach->input));
+ return -1;
+ }
+ if (ctx->data.output != NULL) {
+ if (save_check_write_error(ctx, ctx->data.output) < 0)
+ return -1;
+ }
+ return 0;
+}
+
+int index_attachment_save_finish(struct mail_save_context *ctx)
+{
+ struct mail_save_attachment *attach = ctx->data.attach;
+
+ (void)i_stream_read(attach->input);
+ i_assert(attach->input->eof);
+ return attach->input->stream_errno == 0 ? 0 : -1;
+}
+
+void index_attachment_save_free(struct mail_save_context *ctx)
+{
+ struct mail_save_attachment *attach = ctx->data.attach;
+
+ if (attach != NULL) {
+ i_stream_unref(&attach->input);
+ pool_unref(&attach->pool);
+ ctx->data.attach = NULL;
+ }
+}
+
+const ARRAY_TYPE(mail_attachment_extref) *
+index_attachment_save_get_extrefs(struct mail_save_context *ctx)
+{
+ return ctx->data.attach == NULL ? NULL :
+ &ctx->data.attach->extrefs;
+}
+
+static int
+index_attachment_delete_real(struct mail_storage *storage,
+ struct fs *fs, const char *name)
+{
+ struct fs_file *file;
+ const char *path;
+ int ret;
+
+ path = t_strdup_printf("%s/%s", index_attachment_dir_get(storage), name);
+ file = fs_file_init(fs, path, FS_OPEN_MODE_READONLY);
+ if ((ret = fs_delete(file)) < 0)
+ mail_storage_set_critical(storage, "%s", fs_file_last_error(file));
+ fs_file_deinit(&file);
+ return ret;
+}
+
+int index_attachment_delete(struct mail_storage *storage,
+ struct fs *fs, const char *name)
+{
+ int ret;
+
+ T_BEGIN {
+ ret = index_attachment_delete_real(storage, fs, name);
+ } T_END;
+ return ret;
+}
+
+void index_attachment_append_extrefs(string_t *str,
+ const ARRAY_TYPE(mail_attachment_extref) *extrefs)
+{
+ const struct mail_attachment_extref *extref;
+ bool add_space = FALSE;
+ unsigned int startpos;
+
+ array_foreach(extrefs, extref) {
+ if (!add_space)
+ add_space = TRUE;
+ else
+ str_append_c(str, ' ');
+ str_printfa(str, "%"PRIuUOFF_T" %"PRIuUOFF_T" ",
+ extref->start_offset, extref->size);
+
+ startpos = str_len(str);
+ if (extref->base64_have_crlf)
+ str_append_c(str, MAIL_ATTACHMENT_DECODE_OPTION_CRLF);
+ if (extref->base64_blocks_per_line > 0) {
+ str_printfa(str, "%c%u",
+ MAIL_ATTACHMENT_DECODE_OPTION_BASE64,
+ extref->base64_blocks_per_line * 4);
+ }
+ if (startpos == str_len(str)) {
+ /* make it clear there are no options */
+ str_append_c(str, MAIL_ATTACHMENT_DECODE_OPTION_NONE);
+ }
+ str_append_c(str, ' ');
+ str_append(str, extref->path);
+ }
+}
+
+static bool
+parse_extref_decode_options(const char *str,
+ struct mail_attachment_extref *extref)
+{
+ unsigned int num;
+
+ if (*str == MAIL_ATTACHMENT_DECODE_OPTION_NONE)
+ return str[1] == '\0';
+
+ while (*str != '\0') {
+ switch (*str) {
+ case MAIL_ATTACHMENT_DECODE_OPTION_BASE64:
+ str++; num = 0;
+ while (*str >= '0' && *str <= '9') {
+ num = num*10 + (*str-'0');
+ str++;
+ }
+ if (num == 0 || num % 4 != 0)
+ return FALSE;
+
+ extref->base64_blocks_per_line = num/4;
+ break;
+ case MAIL_ATTACHMENT_DECODE_OPTION_CRLF:
+ extref->base64_have_crlf = TRUE;
+ str++;
+ break;
+ default:
+ return FALSE;
+ }
+ }
+ return TRUE;
+}
+
+bool index_attachment_parse_extrefs(const char *line, pool_t pool,
+ ARRAY_TYPE(mail_attachment_extref) *extrefs)
+{
+ struct mail_attachment_extref extref;
+ const char *const *args;
+ unsigned int i, len;
+ uoff_t last_voffset;
+
+ args = t_strsplit(line, " ");
+ len = str_array_length(args);
+ if ((len % 4) != 0)
+ return FALSE;
+
+ last_voffset = 0;
+ for (i = 0; args[i] != NULL; i += 4) {
+ const char *start_offset_str = args[i+0];
+ const char *size_str = args[i+1];
+ const char *decode_options = args[i+2];
+ const char *path = args[i+3];
+
+ i_zero(&extref);
+ if (str_to_uoff(start_offset_str, &extref.start_offset) < 0 ||
+ str_to_uoff(size_str, &extref.size) < 0 ||
+ extref.start_offset < last_voffset ||
+ !parse_extref_decode_options(decode_options, &extref))
+ return FALSE;
+
+ last_voffset += extref.size +
+ (extref.start_offset - last_voffset);
+
+ extref.path = p_strdup(pool, path);
+ array_push_back(extrefs, &extref);
+ }
+ return TRUE;
+}
+
+int index_attachment_stream_get(struct fs *fs, const char *attachment_dir,
+ const char *path_suffix,
+ struct istream **stream, uoff_t full_size,
+ const char *ext_refs, const char **error_r)
+{
+ ARRAY_TYPE(mail_attachment_extref) extrefs_arr;
+ const struct mail_attachment_extref *extref;
+ struct istream_attachment_connector *conn;
+ struct istream *input;
+ struct fs_file *file;
+ const char *path;
+ int ret;
+
+ *error_r = NULL;
+
+ t_array_init(&extrefs_arr, 16);
+ if (!index_attachment_parse_extrefs(ext_refs, pool_datastack_create(),
+ &extrefs_arr)) {
+ *error_r = "Broken ext-refs string";
+ return -1;
+ }
+ conn = istream_attachment_connector_begin(*stream, full_size);
+
+ array_foreach(&extrefs_arr, extref) {
+ path = t_strdup_printf("%s/%s%s", attachment_dir,
+ extref->path, path_suffix);
+ file = fs_file_init(fs, path, FS_OPEN_MODE_READONLY |
+ FS_OPEN_FLAG_SEEKABLE);
+ input = i_stream_create_fs_file(&file, IO_BLOCK_SIZE);
+
+ ret = istream_attachment_connector_add(conn, input,
+ extref->start_offset, extref->size,
+ extref->base64_blocks_per_line,
+ extref->base64_have_crlf, error_r);
+ i_stream_unref(&input);
+ if (ret < 0) {
+ istream_attachment_connector_abort(&conn);
+ return -1;
+ }
+ }
+
+ input = istream_attachment_connector_finish(&conn);
+ i_stream_set_name(input, t_strdup_printf(
+ "attachments-connector(%s)", i_stream_get_name(*stream)));
+ i_stream_unref(stream);
+ *stream = input;
+ return 0;
+}
diff --git a/src/lib-storage/index/index-attachment.h b/src/lib-storage/index/index-attachment.h
new file mode 100644
index 0000000..a52f158
--- /dev/null
+++ b/src/lib-storage/index/index-attachment.h
@@ -0,0 +1,52 @@
+#ifndef INDEX_ATTACHMENT_H
+#define INDEX_ATTACHMENT_H
+
+#include "sha1.h"
+
+struct fs;
+struct mail_save_context;
+struct mail_storage;
+
+struct mail_attachment_extref {
+ /* path without attachment_dir/ prefix */
+ const char *path;
+ /* offset in input stream where part begins */
+ uoff_t start_offset;
+ uoff_t size;
+
+ /* If non-zero, this attachment was saved as base64-decoded and it
+ need to be encoded back before presenting it to client. Each line
+ (except last one) consists of this many base64 blocks (4 chars of
+ base64 encoded data). */
+ unsigned int base64_blocks_per_line;
+ /* Line feeds are CRLF instead of LF */
+ bool base64_have_crlf;
+};
+ARRAY_DEFINE_TYPE(mail_attachment_extref, struct mail_attachment_extref);
+
+void index_attachment_save_begin(struct mail_save_context *ctx,
+ struct fs *fs, struct istream *input);
+int index_attachment_save_continue(struct mail_save_context *ctx);
+int index_attachment_save_finish(struct mail_save_context *ctx);
+void index_attachment_save_free(struct mail_save_context *ctx);
+const ARRAY_TYPE(mail_attachment_extref) *
+index_attachment_save_get_extrefs(struct mail_save_context *ctx);
+
+/* Delete a given attachment name from storage
+ (name is same as mail_attachment_extref.name). */
+int index_attachment_delete(struct mail_storage *storage,
+ struct fs *fs, const char *name);
+
+void index_attachment_append_extrefs(string_t *str,
+ const ARRAY_TYPE(mail_attachment_extref) *extrefs);
+/* Parse extrefs value to given array. Names are allocated from the
+ given pool. */
+bool index_attachment_parse_extrefs(const char *line, pool_t pool,
+ ARRAY_TYPE(mail_attachment_extref) *extrefs);
+
+int index_attachment_stream_get(struct fs *fs, const char *attachment_dir,
+ const char *path_suffix,
+ struct istream **stream, uoff_t full_size,
+ const char *ext_refs, const char **error_r);
+
+#endif
diff --git a/src/lib-storage/index/index-attribute.c b/src/lib-storage/index/index-attribute.c
new file mode 100644
index 0000000..3d4c415
--- /dev/null
+++ b/src/lib-storage/index/index-attribute.c
@@ -0,0 +1,333 @@
+/* Copyright (c) 2013-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "ioloop.h"
+#include "dict.h"
+#include "index-storage.h"
+
+struct index_storage_attribute_iter {
+ struct mailbox_attribute_iter iter;
+ struct dict_iterate_context *diter;
+ char *prefix;
+ size_t prefix_len;
+ bool dict_disabled;
+};
+
+static struct mail_namespace *
+mail_user_find_attribute_namespace(struct mail_user *user)
+{
+ struct mail_namespace *ns;
+
+ ns = mail_namespace_find_inbox(user->namespaces);
+ if (ns != NULL)
+ return ns;
+
+ for (ns = user->namespaces; ns != NULL; ns = ns->next) {
+ if (ns->type == MAIL_NAMESPACE_TYPE_PRIVATE)
+ return ns;
+ }
+ return NULL;
+}
+
+static int
+index_storage_get_user_dict(struct mail_storage *err_storage,
+ struct mail_user *user, struct dict **dict_r)
+{
+ struct dict_settings dict_set;
+ struct mail_namespace *ns;
+ struct mail_storage *attr_storage;
+ const char *error;
+
+ if (user->_attr_dict != NULL) {
+ *dict_r = user->_attr_dict;
+ return 0;
+ }
+ if (user->attr_dict_failed) {
+ mail_storage_set_internal_error(err_storage);
+ return -1;
+ }
+
+ ns = mail_user_find_attribute_namespace(user);
+ if (ns == NULL) {
+ /* probably never happens? */
+ mail_storage_set_error(err_storage, MAIL_ERROR_NOTPOSSIBLE,
+ "Mailbox attributes not available for this mailbox");
+ return -1;
+ }
+ attr_storage = mail_namespace_get_default_storage(ns);
+
+ if (*attr_storage->set->mail_attribute_dict == '\0') {
+ mail_storage_set_error(err_storage, MAIL_ERROR_NOTPOSSIBLE,
+ "Mailbox attributes not enabled");
+ return -1;
+ }
+
+ i_zero(&dict_set);
+ dict_set.base_dir = user->set->base_dir;
+ dict_set.event_parent = user->event;
+ if (dict_init(attr_storage->set->mail_attribute_dict, &dict_set,
+ &user->_attr_dict, &error) < 0) {
+ mail_storage_set_critical(err_storage,
+ "mail_attribute_dict: dict_init(%s) failed: %s",
+ attr_storage->set->mail_attribute_dict, error);
+ user->attr_dict_failed = TRUE;
+ return -1;
+ }
+ *dict_r = user->_attr_dict;
+ return 0;
+}
+
+static int
+index_storage_get_dict(struct mailbox *box, enum mail_attribute_type type_flags,
+ struct dict **dict_r, const char **mailbox_prefix_r)
+{
+ enum mail_attribute_type type = type_flags & MAIL_ATTRIBUTE_TYPE_MASK;
+ struct mail_storage *storage = box->storage;
+ struct mail_namespace *ns;
+ struct mailbox_metadata metadata;
+ struct dict_settings set;
+ const char *error;
+
+ if ((type_flags & MAIL_ATTRIBUTE_TYPE_FLAG_VALIDATED) != 0) {
+ /* IMAP METADATA support isn't enabled, so don't allow using
+ mail_attribute_dict. */
+ mail_storage_set_error(storage, MAIL_ERROR_NOTPOSSIBLE,
+ "Generic mailbox attributes not enabled");
+ return -1;
+ }
+
+ if (mailbox_get_metadata(box, MAILBOX_METADATA_GUID, &metadata) < 0)
+ return -1;
+ *mailbox_prefix_r = guid_128_to_string(metadata.guid);
+
+ ns = mailbox_get_namespace(box);
+ if (type == MAIL_ATTRIBUTE_TYPE_PRIVATE) {
+ /* private attributes are stored in user's own dict */
+ return index_storage_get_user_dict(storage, storage->user, dict_r);
+ } else if (ns->user == ns->owner) {
+ /* user owns the mailbox. shared attributes are stored in
+ the same dict. */
+ return index_storage_get_user_dict(storage, storage->user, dict_r);
+ } else if (ns->owner != NULL) {
+ /* accessing shared attribute of a shared mailbox.
+ use the owner's dict. */
+ return index_storage_get_user_dict(storage, ns->owner, dict_r);
+ }
+
+ /* accessing shared attributes of a public mailbox. no user owns it,
+ so use the storage's dict. */
+ if (storage->_shared_attr_dict != NULL) {
+ *dict_r = storage->_shared_attr_dict;
+ return 0;
+ }
+ if (*storage->set->mail_attribute_dict == '\0') {
+ mail_storage_set_error(storage, MAIL_ERROR_NOTPOSSIBLE,
+ "Mailbox attributes not enabled");
+ return -1;
+ }
+ if (storage->shared_attr_dict_failed) {
+ mail_storage_set_internal_error(storage);
+ return -1;
+ }
+
+ i_zero(&set);
+ set.base_dir = storage->user->set->base_dir;
+ set.event_parent = storage->user->event;
+ if (dict_init(storage->set->mail_attribute_dict, &set,
+ &storage->_shared_attr_dict, &error) < 0) {
+ mail_storage_set_critical(storage,
+ "mail_attribute_dict: dict_init(%s) failed: %s",
+ storage->set->mail_attribute_dict, error);
+ storage->shared_attr_dict_failed = TRUE;
+ return -1;
+ }
+ *dict_r = storage->_shared_attr_dict;
+ return 0;
+}
+
+static const char *
+key_get_prefixed(enum mail_attribute_type type_flags, const char *mailbox_prefix,
+ const char *key)
+{
+ enum mail_attribute_type type = type_flags & MAIL_ATTRIBUTE_TYPE_MASK;
+
+ switch (type) {
+ case MAIL_ATTRIBUTE_TYPE_PRIVATE:
+ return t_strconcat(DICT_PATH_PRIVATE, mailbox_prefix, "/",
+ key, NULL);
+ case MAIL_ATTRIBUTE_TYPE_SHARED:
+ return t_strconcat(DICT_PATH_SHARED, mailbox_prefix, "/",
+ key, NULL);
+ }
+ i_unreached();
+}
+
+static int
+index_storage_attribute_get_dict_trans(struct mailbox_transaction_context *t,
+ enum mail_attribute_type type_flags,
+ struct dict_transaction_context **dtrans_r,
+ const char **mailbox_prefix_r)
+{
+ enum mail_attribute_type type = type_flags & MAIL_ATTRIBUTE_TYPE_MASK;
+ struct dict_transaction_context **dtransp = NULL;
+ struct dict *dict;
+ struct mailbox_metadata metadata;
+
+ switch (type) {
+ case MAIL_ATTRIBUTE_TYPE_PRIVATE:
+ dtransp = &t->attr_pvt_trans;
+ break;
+ case MAIL_ATTRIBUTE_TYPE_SHARED:
+ dtransp = &t->attr_shared_trans;
+ break;
+ }
+ i_assert(dtransp != NULL);
+
+ if (*dtransp != NULL &&
+ (type_flags & MAIL_ATTRIBUTE_TYPE_FLAG_VALIDATED) == 0) {
+ /* Transaction already created. Even if it was, don't use it
+ if _FLAG_VALIDATED is being used. It'll be handled below by
+ returning failure. */
+ if (mailbox_get_metadata(t->box, MAILBOX_METADATA_GUID,
+ &metadata) < 0)
+ return -1;
+ *mailbox_prefix_r = guid_128_to_string(metadata.guid);
+ *dtrans_r = *dtransp;
+ return 0;
+ }
+
+ if (index_storage_get_dict(t->box, type_flags, &dict, mailbox_prefix_r) < 0)
+ return -1;
+ i_assert(*dtransp == NULL);
+
+ struct mail_user *user = mailbox_list_get_user(t->box->list);
+ const struct dict_op_settings *set = mail_user_get_dict_op_settings(user);
+ *dtransp = *dtrans_r = dict_transaction_begin(dict, set);
+ return 0;
+}
+
+int index_storage_attribute_set(struct mailbox_transaction_context *t,
+ enum mail_attribute_type type_flags,
+ const char *key,
+ const struct mail_attribute_value *value)
+{
+ enum mail_attribute_type type = type_flags & MAIL_ATTRIBUTE_TYPE_MASK;
+ struct dict_transaction_context *dtrans;
+ const char *mailbox_prefix;
+ bool pvt = type == MAIL_ATTRIBUTE_TYPE_PRIVATE;
+ time_t ts = value->last_change != 0 ? value->last_change : ioloop_time;
+ int ret = 0;
+
+ if (index_storage_attribute_get_dict_trans(t, type_flags, &dtrans,
+ &mailbox_prefix) < 0)
+ return -1;
+
+ T_BEGIN {
+ const char *prefixed_key =
+ key_get_prefixed(type_flags, mailbox_prefix, key);
+ const char *value_str;
+
+ if (mailbox_attribute_value_to_string(t->box->storage, value,
+ &value_str) < 0) {
+ ret = -1;
+ } else if (value_str != NULL) {
+ dict_set(dtrans, prefixed_key, value_str);
+ mail_index_attribute_set(t->itrans, pvt, key,
+ ts, strlen(value_str));
+ } else {
+ dict_unset(dtrans, prefixed_key);
+ mail_index_attribute_unset(t->itrans, pvt, key, ts);
+ }
+ } T_END;
+ return ret;
+}
+
+int index_storage_attribute_get(struct mailbox *box,
+ enum mail_attribute_type type_flags,
+ const char *key,
+ struct mail_attribute_value *value_r)
+{
+ struct dict *dict;
+ const char *mailbox_prefix, *error;
+ int ret;
+
+ i_zero(value_r);
+
+ if (index_storage_get_dict(box, type_flags, &dict, &mailbox_prefix) < 0)
+ return -1;
+
+ struct mail_user *user = mailbox_list_get_user(box->list);
+ const struct dict_op_settings *set = mail_user_get_dict_op_settings(user);
+ ret = dict_lookup(dict, set, pool_datastack_create(),
+ key_get_prefixed(type_flags, mailbox_prefix, key),
+ &value_r->value, &error);
+ if (ret < 0) {
+ mailbox_set_critical(box,
+ "Failed to get attribute %s: %s", key, error);
+ return -1;
+ }
+ return ret;
+}
+
+struct mailbox_attribute_iter *
+index_storage_attribute_iter_init(struct mailbox *box,
+ enum mail_attribute_type type_flags,
+ const char *prefix)
+{
+ struct index_storage_attribute_iter *iter;
+ struct dict *dict;
+ const char *mailbox_prefix;
+
+ iter = i_new(struct index_storage_attribute_iter, 1);
+ iter->iter.box = box;
+ if (index_storage_get_dict(box, type_flags, &dict, &mailbox_prefix) < 0) {
+ if (mailbox_get_last_mail_error(box) == MAIL_ERROR_NOTPOSSIBLE)
+ iter->dict_disabled = TRUE;
+ } else {
+ iter->prefix = i_strdup(key_get_prefixed(type_flags, mailbox_prefix,
+ prefix));
+ iter->prefix_len = strlen(iter->prefix);
+ struct mail_user *user = mailbox_list_get_user(box->list);
+ const struct dict_op_settings *set = mail_user_get_dict_op_settings(user);
+ iter->diter = dict_iterate_init(dict, set, iter->prefix,
+ DICT_ITERATE_FLAG_RECURSE |
+ DICT_ITERATE_FLAG_NO_VALUE);
+ }
+ return &iter->iter;
+}
+
+const char *
+index_storage_attribute_iter_next(struct mailbox_attribute_iter *_iter)
+{
+ struct index_storage_attribute_iter *iter =
+ (struct index_storage_attribute_iter *)_iter;
+ const char *key, *value;
+
+ if (iter->diter == NULL || !dict_iterate(iter->diter, &key, &value))
+ return NULL;
+
+ i_assert(strncmp(key, iter->prefix, iter->prefix_len) == 0);
+ key += iter->prefix_len;
+ return key;
+}
+
+int index_storage_attribute_iter_deinit(struct mailbox_attribute_iter *_iter)
+{
+ struct index_storage_attribute_iter *iter =
+ (struct index_storage_attribute_iter *)_iter;
+ const char *error;
+ int ret;
+
+ if (iter->diter == NULL) {
+ ret = iter->dict_disabled ? 0 : -1;
+ } else {
+ if ((ret = dict_iterate_deinit(&iter->diter, &error)) < 0) {
+ mailbox_set_critical(_iter->box,
+ "dict_iterate(%s) failed: %s",
+ iter->prefix, error);
+ }
+ }
+ i_free(iter->prefix);
+ i_free(iter);
+ return ret;
+}
diff --git a/src/lib-storage/index/index-mail-binary.c b/src/lib-storage/index/index-mail-binary.c
new file mode 100644
index 0000000..80c319e
--- /dev/null
+++ b/src/lib-storage/index/index-mail-binary.c
@@ -0,0 +1,598 @@
+/* Copyright (c) 2002-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "str.h"
+#include "safe-mkstemp.h"
+#include "istream.h"
+#include "istream-crlf.h"
+#include "istream-seekable.h"
+#include "istream-base64.h"
+#include "istream-qp.h"
+#include "istream-header-filter.h"
+#include "ostream.h"
+#include "message-binary-part.h"
+#include "message-parser.h"
+#include "message-decoder.h"
+#include "mail-user.h"
+#include "index-storage.h"
+#include "index-mail.h"
+
+#define MAIL_BINARY_CACHE_EXPIRE_MSECS (60*1000)
+
+#define IS_CONVERTED_CTE(cte) \
+ ((cte) == MESSAGE_CTE_QP || (cte) == MESSAGE_CTE_BASE64)
+
+struct binary_block {
+ struct istream *input;
+ uoff_t physical_pos;
+ unsigned int body_lines_count;
+ bool converted, converted_hdr;
+};
+
+struct binary_ctx {
+ struct mail *mail;
+ struct istream *input;
+ bool has_nuls, converted;
+ /* each block is its own input stream. basically each converted MIME
+ body has its own block and the parts between the MIME bodies are
+ unconverted blocks */
+ ARRAY(struct binary_block) blocks;
+
+ uoff_t copy_start_offset;
+};
+
+static void binary_copy_to(struct binary_ctx *ctx, uoff_t end_offset)
+{
+ struct binary_block *block;
+ struct istream *linput, *cinput;
+ uoff_t orig_offset, size;
+
+ i_assert(end_offset >= ctx->copy_start_offset);
+
+ if (end_offset == ctx->copy_start_offset)
+ return;
+
+ size = end_offset - ctx->copy_start_offset;
+ orig_offset = ctx->input->v_offset;
+
+ i_stream_seek(ctx->input, ctx->copy_start_offset);
+ linput = i_stream_create_limit(ctx->input, size);
+ cinput = i_stream_create_crlf(linput);
+ i_stream_unref(&linput);
+
+ block = array_append_space(&ctx->blocks);
+ block->input = cinput;
+
+ i_stream_seek(ctx->input, orig_offset);
+}
+
+static void
+binary_cte_filter_callback(struct header_filter_istream *input,
+ struct message_header_line *hdr,
+ bool *matched ATTR_UNUSED, void *context ATTR_UNUSED)
+{
+ static const char *cte_binary = "Content-Transfer-Encoding: binary\r\n";
+
+ if (hdr != NULL && hdr->eoh) {
+ i_stream_header_filter_add(input, cte_binary,
+ strlen(cte_binary));
+ }
+}
+
+static int
+add_binary_part(struct binary_ctx *ctx, const struct message_part *part,
+ bool include_hdr)
+{
+ static const char *filter_headers[] = {
+ "Content-Transfer-Encoding",
+ };
+ struct message_header_parser_ctx *parser;
+ struct message_header_line *hdr;
+ struct message_part *child;
+ struct message_size hdr_size;
+ struct istream *linput;
+ struct binary_block *block;
+ enum message_cte cte;
+ uoff_t part_end_offset;
+ int ret;
+
+ /* first parse the header to find c-t-e. */
+ i_stream_seek(ctx->input, part->physical_pos);
+
+ cte = MESSAGE_CTE_78BIT;
+ parser = message_parse_header_init(ctx->input, &hdr_size, 0);
+ while ((ret = message_parse_header_next(parser, &hdr)) > 0) {
+ if (strcasecmp(hdr->name, "Content-Transfer-Encoding") == 0)
+ cte = message_decoder_parse_cte(hdr);
+ }
+ i_assert(ret < 0);
+ if (message_parse_header_has_nuls(parser)) {
+ /* we're not converting NULs to 0x80 when doing a binary fetch,
+ even if they're in the message header. */
+ ctx->has_nuls = TRUE;
+ }
+ message_parse_header_deinit(&parser);
+
+ if (ctx->input->stream_errno != 0) {
+ mail_set_critical(ctx->mail,
+ "read(%s) failed: %s", i_stream_get_name(ctx->input),
+ i_stream_get_error(ctx->input));
+ return -1;
+ }
+
+ if (cte == MESSAGE_CTE_UNKNOWN) {
+ mail_storage_set_error(ctx->mail->box->storage,
+ MAIL_ERROR_CONVERSION,
+ "Unknown Content-Transfer-Encoding.");
+ return -1;
+ }
+
+ i_stream_seek(ctx->input, part->physical_pos);
+ if (!include_hdr) {
+ /* body only */
+ } else if (IS_CONVERTED_CTE(cte)) {
+ /* write header with modified content-type */
+ if (ctx->copy_start_offset != 0)
+ binary_copy_to(ctx, part->physical_pos);
+ block = array_append_space(&ctx->blocks);
+ block->physical_pos = part->physical_pos;
+ block->converted = TRUE;
+ block->converted_hdr = TRUE;
+
+ linput = i_stream_create_limit(ctx->input, UOFF_T_MAX);
+ block->input = i_stream_create_header_filter(linput,
+ HEADER_FILTER_EXCLUDE | HEADER_FILTER_HIDE_BODY,
+ filter_headers, N_ELEMENTS(filter_headers),
+ binary_cte_filter_callback, NULL);
+ i_stream_unref(&linput);
+ } else {
+ /* copy everything as-is until the end of this header */
+ binary_copy_to(ctx, part->physical_pos +
+ part->header_size.physical_size);
+ }
+ ctx->copy_start_offset = part->physical_pos +
+ part->header_size.physical_size;
+ part_end_offset = part->physical_pos +
+ part->header_size.physical_size +
+ part->body_size.physical_size;
+
+ if (part->children != NULL) {
+ /* multipart */
+ for (child = part->children; child != NULL; child = child->next) {
+ if (add_binary_part(ctx, child, TRUE) < 0)
+ return -1;
+ }
+ binary_copy_to(ctx, part_end_offset);
+ ctx->copy_start_offset = part_end_offset;
+ return 0;
+ }
+ if (part->body_size.physical_size == 0) {
+ /* no body */
+ ctx->copy_start_offset = part_end_offset;
+ return 0;
+ }
+
+ /* single part - write decoded data */
+ block = array_append_space(&ctx->blocks);
+ block->physical_pos = part->physical_pos;
+
+ i_stream_seek(ctx->input, part->physical_pos +
+ part->header_size.physical_size);
+ linput = i_stream_create_limit(ctx->input, part->body_size.physical_size);
+ switch (cte) {
+ case MESSAGE_CTE_UNKNOWN:
+ i_unreached();
+ case MESSAGE_CTE_78BIT:
+ case MESSAGE_CTE_BINARY:
+ /* no conversion necessary */
+ if ((part->flags & MESSAGE_PART_FLAG_HAS_NULS) != 0)
+ ctx->has_nuls = TRUE;
+ block->input = i_stream_create_crlf(linput);
+ break;
+ case MESSAGE_CTE_QP:
+ block->input = i_stream_create_qp_decoder(linput);
+ ctx->converted = block->converted = TRUE;
+ break;
+ case MESSAGE_CTE_BASE64:
+ block->input = i_stream_create_base64_decoder(linput);
+ ctx->converted = block->converted = TRUE;
+ break;
+ }
+ i_stream_unref(&linput);
+
+ ctx->copy_start_offset = part_end_offset;
+ return 0;
+}
+
+static int fd_callback(const char **path_r, void *context)
+{
+ struct mail *_mail = context;
+ string_t *path;
+ int fd;
+
+ path = t_str_new(256);
+ mail_user_set_get_temp_prefix(path, _mail->box->storage->user->set);
+ fd = safe_mkstemp_hostpid(path, 0600, (uid_t)-1, (gid_t)-1);
+ if (fd == -1) {
+ i_error("Temp file creation to %s failed: %m", str_c(path));
+ return -1;
+ }
+
+ /* we just want the fd, unlink it */
+ if (i_unlink(str_c(path)) < 0) {
+ /* shouldn't happen.. */
+ i_close_fd(&fd);
+ return -1;
+ }
+ *path_r = str_c(path);
+ return fd;
+}
+
+static void binary_streams_free(struct binary_ctx *ctx)
+{
+ struct binary_block *block;
+
+ array_foreach_modifiable(&ctx->blocks, block)
+ i_stream_unref(&block->input);
+}
+
+static void
+binary_parts_update(struct binary_ctx *ctx, const struct message_part *part,
+ struct message_binary_part **msg_bin_parts)
+{
+ struct index_mail *mail = INDEX_MAIL(ctx->mail);
+ struct binary_block *blocks;
+ struct message_binary_part bin_part;
+ unsigned int i, count;
+ uoff_t size;
+ bool found;
+
+ blocks = array_get_modifiable(&ctx->blocks, &count);
+ for (; part != NULL; part = part->next) {
+ binary_parts_update(ctx, part->children, msg_bin_parts);
+
+ i_zero(&bin_part);
+ /* default to unchanged header */
+ bin_part.binary_hdr_size = part->header_size.virtual_size;
+ bin_part.physical_pos = part->physical_pos;
+ found = FALSE;
+ for (i = 0; i < count; i++) {
+ if (blocks[i].physical_pos != part->physical_pos ||
+ !blocks[i].converted)
+ continue;
+
+ size = blocks[i].input->v_offset;
+ if (blocks[i].converted_hdr)
+ bin_part.binary_hdr_size = size;
+ else
+ bin_part.binary_body_size = size;
+ found = TRUE;
+ }
+ if (found) {
+ bin_part.next = *msg_bin_parts;
+ *msg_bin_parts = p_new(mail->mail.data_pool,
+ struct message_binary_part, 1);
+ **msg_bin_parts = bin_part;
+ }
+ }
+}
+
+static void binary_parts_cache(struct binary_ctx *ctx)
+{
+ struct index_mail *mail = INDEX_MAIL(ctx->mail);
+ buffer_t *buf;
+
+ buf = t_buffer_create(128);
+ message_binary_part_serialize(mail->data.bin_parts, buf);
+ index_mail_cache_add(mail, MAIL_CACHE_BINARY_PARTS,
+ buf->data, buf->used);
+}
+
+static struct istream **blocks_get_streams(struct binary_ctx *ctx)
+{
+ struct istream **streams;
+ const struct binary_block *blocks;
+ unsigned int i, count;
+
+ blocks = array_get(&ctx->blocks, &count);
+ streams = t_new(struct istream *, count+1);
+ for (i = 0; i < count; i++) {
+ streams[i] = blocks[i].input;
+ i_assert(streams[i]->v_offset == 0);
+ }
+ return streams;
+}
+
+static int
+blocks_count_lines(struct binary_ctx *ctx, struct istream *full_input)
+{
+ struct binary_block *blocks, *cur_block;
+ unsigned int block_idx, block_count;
+ uoff_t cur_block_offset, cur_block_size;
+ const unsigned char *data, *p;
+ size_t size, skip;
+ ssize_t ret;
+
+ blocks = array_get_modifiable(&ctx->blocks, &block_count);
+ cur_block = blocks;
+ cur_block_offset = 0;
+ block_idx = 0;
+
+ /* count the number of lines each block contains */
+ while ((ret = i_stream_read_more(full_input, &data, &size)) > 0) {
+ i_assert(cur_block_offset <= cur_block->input->v_offset);
+ if (cur_block->input->eof) {
+ /* this is the last input for this block. the input
+ may also contain the next block's data, which we
+ don't want to include in this block's line count. */
+ cur_block_size = cur_block->input->v_offset +
+ i_stream_get_data_size(cur_block->input);
+ i_assert(size >= cur_block_size - cur_block_offset);
+ size = cur_block_size - cur_block_offset;
+ }
+ skip = size;
+ while ((p = memchr(data, '\n', size)) != NULL) {
+ size -= p-data+1;
+ data = p+1;
+ cur_block->body_lines_count++;
+ }
+ i_stream_skip(full_input, skip);
+ cur_block_offset += skip;
+
+ if (i_stream_read_eof(cur_block->input)) {
+ /* go to the next block */
+ if (block_idx+1 == block_count) {
+ i_assert(i_stream_read_eof(full_input));
+ ret = -1;
+ break;
+ }
+ block_idx++;
+ cur_block++;
+ cur_block_offset = 0;
+ }
+ }
+ i_assert(ret == -1);
+ if (full_input->stream_errno != 0)
+ return -1;
+ i_assert(block_count == 0 || !i_stream_have_bytes_left(cur_block->input));
+ i_assert(block_count == 0 || block_idx+1 == block_count);
+ return 0;
+}
+
+static int
+index_mail_read_binary_to_cache(struct mail *_mail,
+ const struct message_part *part,
+ bool include_hdr, const char *reason,
+ bool *binary_r, bool *converted_r)
+{
+ struct index_mail *mail = INDEX_MAIL(_mail);
+ struct mail_binary_cache *cache = &_mail->box->storage->binary_cache;
+ struct binary_ctx ctx;
+ struct istream *is;
+
+ i_zero(&ctx);
+ ctx.mail = _mail;
+ t_array_init(&ctx.blocks, 8);
+
+ mail_storage_free_binary_cache(_mail->box->storage);
+ if (mail_get_stream_because(_mail, NULL, NULL, reason, &ctx.input) < 0)
+ return -1;
+
+ if (add_binary_part(&ctx, part, include_hdr) < 0) {
+ binary_streams_free(&ctx);
+ return -1;
+ }
+
+ if (array_count(&ctx.blocks) != 0) {
+ is = i_streams_merge(blocks_get_streams(&ctx),
+ IO_BLOCK_SIZE,
+ fd_callback, _mail);
+ } else {
+ is = i_stream_create_from_data("", 0);
+ }
+ i_stream_set_name(is, t_strdup_printf(
+ "<binary stream of mailbox %s UID %u>",
+ _mail->box->vname, _mail->uid));
+ if (blocks_count_lines(&ctx, is) < 0) {
+ if (is->stream_errno == EINVAL) {
+ /* MIME part contains invalid data */
+ mail_storage_set_error(_mail->box->storage,
+ MAIL_ERROR_INVALIDDATA,
+ "Invalid data in MIME part");
+ } else {
+ mail_set_critical(_mail, "read(%s) failed: %s",
+ i_stream_get_name(is),
+ i_stream_get_error(is));
+ }
+ i_stream_unref(&is);
+ binary_streams_free(&ctx);
+ return -1;
+ }
+
+ if (_mail->uid > 0) {
+ cache->to = timeout_add(MAIL_BINARY_CACHE_EXPIRE_MSECS,
+ mail_storage_free_binary_cache,
+ _mail->box->storage);
+ cache->box = _mail->box;
+ cache->uid = _mail->uid;
+ cache->orig_physical_pos = part->physical_pos;
+ cache->include_hdr = include_hdr;
+ cache->input = is;
+ }
+
+ i_assert(!i_stream_have_bytes_left(is));
+ cache->size = is->v_offset;
+ i_stream_seek(is, 0);
+
+ if (part->parent == NULL && include_hdr &&
+ mail->data.bin_parts == NULL) {
+ binary_parts_update(&ctx, part, &mail->data.bin_parts);
+ if (_mail->uid > 0)
+ binary_parts_cache(&ctx);
+ }
+ binary_streams_free(&ctx);
+
+ *binary_r = ctx.converted ? TRUE : ctx.has_nuls;
+ *converted_r = ctx.converted;
+ return 0;
+}
+
+static bool get_cached_binary_parts(struct index_mail *mail)
+{
+ const unsigned int field_idx =
+ mail->ibox->cache_fields[MAIL_CACHE_BINARY_PARTS].idx;
+ buffer_t *part_buf;
+ int ret;
+
+ if (mail->data.bin_parts != NULL)
+ return TRUE;
+
+ part_buf = t_buffer_create(128);
+ ret = index_mail_cache_lookup_field(mail, part_buf, field_idx);
+ if (ret <= 0)
+ return FALSE;
+
+ if (message_binary_part_deserialize(mail->mail.data_pool,
+ part_buf->data, part_buf->used,
+ &mail->data.bin_parts) < 0) {
+ mail_set_mail_cache_corrupted(&mail->mail.mail,
+ "Corrupted cached binary.parts data");
+ return FALSE;
+ }
+ return TRUE;
+}
+
+static struct message_part *
+msg_part_find(struct message_part *parts, uoff_t physical_pos)
+{
+ struct message_part *part, *child;
+
+ for (part = parts; part != NULL; part = part->next) {
+ if (part->physical_pos == physical_pos)
+ return part;
+ child = msg_part_find(part->children, physical_pos);
+ if (child != NULL)
+ return child;
+ }
+ return NULL;
+}
+
+static int
+index_mail_get_binary_size(struct mail *_mail,
+ const struct message_part *part, bool include_hdr,
+ uoff_t *size_r, unsigned int *lines_r)
+{
+ struct index_mail *mail = INDEX_MAIL(_mail);
+ struct message_part *all_parts, *msg_part;
+ const struct message_binary_part *bin_part, *root_bin_part;
+ uoff_t size, end_offset;
+ unsigned int lines;
+ bool binary, converted;
+
+ if (mail_get_parts(_mail, &all_parts) < 0)
+ return -1;
+
+ /* first lookup from cache */
+ if (!get_cached_binary_parts(mail)) {
+ /* not found. parse the whole message */
+ if (index_mail_read_binary_to_cache(_mail, all_parts, TRUE,
+ "binary.size", &binary, &converted) < 0)
+ return -1;
+ }
+
+ size = part->header_size.virtual_size +
+ part->body_size.virtual_size;
+ /* note that we assume here that binary translation doesn't change the
+ headers' line counts. this isn't true if the original message
+ contained duplicate Content-Transfer-Encoding lines, but since
+ that's invalid anyway we don't bother trying to handle it. */
+ lines = part->header_size.lines + part->body_size.lines;
+ end_offset = part->physical_pos + size;
+
+ bin_part = mail->data.bin_parts; root_bin_part = NULL;
+ for (; bin_part != NULL; bin_part = bin_part->next) {
+ msg_part = msg_part_find(all_parts, bin_part->physical_pos);
+ if (msg_part == NULL) {
+ /* either binary.parts or mime.parts is broken */
+ mail_set_cache_corrupted(_mail, MAIL_FETCH_MESSAGE_PARTS, t_strdup_printf(
+ "BINARY part at offset %"PRIuUOFF_T" not found from MIME parts",
+ bin_part->physical_pos));
+ return -1;
+ }
+ if (msg_part->physical_pos >= part->physical_pos &&
+ msg_part->physical_pos < end_offset) {
+ if (msg_part->physical_pos == part->physical_pos)
+ root_bin_part = bin_part;
+ size -= msg_part->header_size.virtual_size +
+ msg_part->body_size.virtual_size;
+ size += bin_part->binary_hdr_size +
+ bin_part->binary_body_size;
+ lines -= msg_part->body_size.lines;
+ lines += bin_part->binary_body_lines_count;
+ }
+ }
+ if (!include_hdr) {
+ if (root_bin_part != NULL)
+ size -= root_bin_part->binary_hdr_size;
+ else
+ size -= part->header_size.virtual_size;
+ lines -= part->header_size.lines;
+ }
+ *size_r = size;
+ *lines_r = lines;
+ return 0;
+}
+
+int index_mail_get_binary_stream(struct mail *_mail,
+ const struct message_part *part,
+ bool include_hdr, uoff_t *size_r,
+ unsigned int *lines_r, bool *binary_r,
+ struct istream **stream_r)
+{
+ struct index_mail *mail = INDEX_MAIL(_mail);
+ struct mail_binary_cache *cache = &_mail->box->storage->binary_cache;
+ struct istream *input;
+ bool binary, converted;
+
+ if (stream_r == NULL) {
+ return index_mail_get_binary_size(_mail, part, include_hdr,
+ size_r, lines_r);
+ }
+ /* current implementation doesn't bother implementing this,
+ because it's not needed by anything. */
+ i_assert(lines_r == NULL);
+
+ /* FIXME: always put the header to temp file. skip it when needed. */
+ if (cache->box == _mail->box && cache->uid == _mail->uid &&
+ cache->orig_physical_pos == part->physical_pos &&
+ cache->include_hdr == include_hdr) {
+ /* we have this cached already */
+ i_stream_seek(cache->input, 0);
+ timeout_reset(cache->to);
+ binary = TRUE;
+ converted = TRUE;
+ } else {
+ if (index_mail_read_binary_to_cache(_mail, part, include_hdr,
+ "binary stream", &binary, &converted) < 0)
+ return -1;
+ mail->data.cache_fetch_fields |= MAIL_FETCH_STREAM_BINARY;
+ }
+ *size_r = cache->size;
+ *binary_r = binary;
+ if (!converted) {
+ /* don't keep this cached. it's exactly the same as
+ the original stream */
+ i_assert(mail->data.stream != NULL);
+ i_stream_seek(mail->data.stream, part->physical_pos +
+ (include_hdr ? 0 :
+ part->header_size.physical_size));
+ input = i_stream_create_crlf(mail->data.stream);
+ *stream_r = i_stream_create_limit(input, *size_r);
+ i_stream_unref(&input);
+ mail_storage_free_binary_cache(_mail->box->storage);
+ } else {
+ *stream_r = cache->input;
+ i_stream_ref(cache->input);
+ }
+ return 0;
+}
diff --git a/src/lib-storage/index/index-mail-headers.c b/src/lib-storage/index/index-mail-headers.c
new file mode 100644
index 0000000..ce23e9d
--- /dev/null
+++ b/src/lib-storage/index/index-mail-headers.c
@@ -0,0 +1,990 @@
+/* Copyright (c) 2003-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "istream.h"
+#include "array.h"
+#include "buffer.h"
+#include "str.h"
+#include "message-date.h"
+#include "message-part-data.h"
+#include "message-parser.h"
+#include "message-header-decode.h"
+#include "istream-tee.h"
+#include "istream-header-filter.h"
+#include "imap-envelope.h"
+#include "imap-bodystructure.h"
+#include "index-storage.h"
+#include "index-mail.h"
+
+static const struct message_parser_settings msg_parser_set = {
+ .hdr_flags = MESSAGE_HEADER_PARSER_FLAG_SKIP_INITIAL_LWSP |
+ MESSAGE_HEADER_PARSER_FLAG_DROP_CR,
+ .flags = MESSAGE_PARSER_FLAG_SKIP_BODY_BLOCK,
+};
+
+static void index_mail_filter_stream_destroy(struct index_mail *mail);
+
+static int header_line_cmp(const struct index_mail_line *l1,
+ const struct index_mail_line *l2)
+{
+ int diff;
+
+ diff = (int)l1->field_idx - (int)l2->field_idx;
+ return diff != 0 ? diff :
+ (int)l1->line_num - (int)l2->line_num;
+}
+
+void index_mail_parse_header_deinit(struct index_mail *mail)
+{
+ mail->data.header_parser_initialized = FALSE;
+}
+
+static void index_mail_parse_header_finish(struct index_mail *mail)
+{
+ struct mail *_mail = &mail->mail.mail;
+ const struct index_mail_line *lines;
+ const unsigned char *header;
+ const uint8_t *match;
+ buffer_t *buf;
+ unsigned int i, j, count, match_idx, match_count;
+ bool noncontiguous;
+
+ /* sort it first so fields are grouped together and ordered by
+ line number */
+ array_sort(&mail->header_lines, header_line_cmp);
+
+ lines = array_get(&mail->header_lines, &count);
+ match = array_get(&mail->header_match, &match_count);
+ header = mail->header_data->data;
+ buf = t_buffer_create(256);
+
+ /* go through all the header lines we found */
+ for (i = match_idx = 0; i < count; i = j) {
+ /* matches and header lines are both sorted, all matches
+ until lines[i] weren't found */
+ while (match_idx < lines[i].field_idx &&
+ match_idx < match_count) {
+ if (HEADER_MATCH_USABLE(mail, match[match_idx]) &&
+ mail_cache_field_can_add(_mail->transaction->cache_trans,
+ _mail->seq, match_idx)) {
+ /* this header doesn't exist. remember that. */
+ i_assert((match[match_idx] &
+ HEADER_MATCH_FLAG_FOUND) == 0);
+ index_mail_cache_add_idx(mail, match_idx,
+ "", 0);
+ }
+ match_idx++;
+ }
+
+ if (match_idx < match_count) {
+ /* save index to first header line */
+ i_assert(match_idx == lines[i].field_idx);
+ j = i + 1;
+ array_idx_set(&mail->header_match_lines, match_idx, &j);
+ match_idx++;
+ }
+
+ if (!mail_cache_field_can_add(_mail->transaction->cache_trans,
+ _mail->seq, lines[i].field_idx)) {
+ /* header is already cached. skip over all the
+ header lines. */
+ for (j = i+1; j < count; j++) {
+ if (lines[j].field_idx != lines[i].field_idx)
+ break;
+ }
+ continue;
+ }
+
+ /* buffer contains: { uint32_t line_num[], 0, header texts }
+ noncontiguous is just a small optimization.. */
+ buffer_set_used_size(buf, 0);
+ buffer_append(buf, &lines[i].line_num,
+ sizeof(lines[i].line_num));
+
+ noncontiguous = FALSE;
+ for (j = i+1; j < count; j++) {
+ if (lines[j].field_idx != lines[i].field_idx)
+ break;
+
+ if (lines[j].start_pos != lines[j-1].end_pos)
+ noncontiguous = TRUE;
+ buffer_append(buf, &lines[j].line_num,
+ sizeof(lines[j].line_num));
+ }
+ buffer_append_zero(buf, sizeof(uint32_t));
+
+ if (noncontiguous) {
+ for (; i < j; i++) {
+ buffer_append(buf, header + lines[i].start_pos,
+ lines[i].end_pos -
+ lines[i].start_pos);
+ }
+ i--;
+ } else {
+ buffer_append(buf, header + lines[i].start_pos,
+ lines[j-1].end_pos - lines[i].start_pos);
+ }
+
+ index_mail_cache_add_idx(mail, lines[i].field_idx,
+ buf->data, buf->used);
+ }
+
+ for (; match_idx < match_count; match_idx++) {
+ if (HEADER_MATCH_USABLE(mail, match[match_idx]) &&
+ mail_cache_field_can_add(_mail->transaction->cache_trans,
+ _mail->seq, match_idx)) {
+ /* this header doesn't exist. remember that. */
+ i_assert((match[match_idx] &
+ HEADER_MATCH_FLAG_FOUND) == 0);
+ index_mail_cache_add_idx(mail, match_idx, "", 0);
+ }
+ }
+
+ mail->data.dont_cache_field_idx = UINT_MAX;
+ index_mail_parse_header_deinit(mail);
+}
+
+static unsigned int
+get_header_field_idx(struct mailbox *box, const char *field)
+{
+ struct mail_cache_field header_field;
+
+ i_zero(&header_field);
+ header_field.type = MAIL_CACHE_FIELD_HEADER;
+ /* Always register with NO decision. The field should be added soon
+ with mail_cache_add(), which changes the decision to TEMP. Most
+ importantly doing it this way emits mail_cache_decision event. */
+ header_field.decision = MAIL_CACHE_DECISION_NO;
+ T_BEGIN {
+ header_field.name = t_strconcat("hdr.", field, NULL);
+ mail_cache_register_fields(box->cache, &header_field, 1);
+ } T_END;
+ return header_field.idx;
+}
+
+bool index_mail_want_parse_headers(struct index_mail *mail)
+{
+ if (mail->data.wanted_headers != NULL ||
+ mail->data.save_bodystructure_header)
+ return TRUE;
+
+ if ((mail->data.cache_fetch_fields & MAIL_FETCH_DATE) != 0 &&
+ !mail->data.sent_date_parsed)
+ return TRUE;
+ return FALSE;
+}
+
+static void index_mail_parse_header_register_all_wanted(struct index_mail *mail)
+{
+ struct mail *_mail = &mail->mail.mail;
+ const struct mail_cache_field *all_cache_fields;
+ unsigned int i, count;
+
+ all_cache_fields =
+ mail_cache_register_get_list(_mail->box->cache,
+ pool_datastack_create(), &count);
+ for (i = 0; i < count; i++) {
+ if (strncasecmp(all_cache_fields[i].name, "hdr.", 4) != 0)
+ continue;
+ if (!mail_cache_field_want_add(_mail->transaction->cache_trans,
+ _mail->seq, i))
+ continue;
+
+ array_idx_set(&mail->header_match, all_cache_fields[i].idx,
+ &mail->header_match_value);
+ }
+}
+
+void index_mail_parse_header_init(struct index_mail *mail,
+ struct mailbox_header_lookup_ctx *headers)
+{
+ struct index_mail_data *data = &mail->data;
+ const uint8_t *match;
+ unsigned int i, field_idx, match_count;
+
+ index_mail_filter_stream_destroy(mail);
+ i_assert(!mail->data.header_parser_initialized);
+
+ mail->header_seq = mail->mail.mail.seq;
+ if (mail->header_data == NULL) {
+ mail->header_data = buffer_create_dynamic(default_pool, 4096);
+ i_array_init(&mail->header_lines, 32);
+ i_array_init(&mail->header_match, 32);
+ i_array_init(&mail->header_match_lines, 32);
+ mail->header_match_value = HEADER_MATCH_SKIP_COUNT;
+ } else {
+ buffer_set_used_size(mail->header_data, 0);
+ array_clear(&mail->header_lines);
+ array_clear(&mail->header_match_lines);
+
+ i_assert((mail->header_match_value &
+ (HEADER_MATCH_SKIP_COUNT-1)) == 0);
+ if (mail->header_match_value + HEADER_MATCH_SKIP_COUNT <= UINT8_MAX)
+ mail->header_match_value += HEADER_MATCH_SKIP_COUNT;
+ else {
+ /* wrapped, we'll have to clear the buffer */
+ array_clear(&mail->header_match);
+ mail->header_match_value = HEADER_MATCH_SKIP_COUNT;
+ }
+ }
+
+ if (headers != NULL) {
+ for (i = 0; i < headers->count; i++) {
+ array_idx_set(&mail->header_match, headers->idx[i],
+ &mail->header_match_value);
+ }
+ }
+
+ if (data->wanted_headers != NULL && data->wanted_headers != headers) {
+ headers = data->wanted_headers;
+ for (i = 0; i < headers->count; i++) {
+ array_idx_set(&mail->header_match, headers->idx[i],
+ &mail->header_match_value);
+ }
+ }
+
+ /* register also all the other headers that exist in cache file */
+ T_BEGIN {
+ index_mail_parse_header_register_all_wanted(mail);
+ } T_END;
+
+ /* if we want sent date, it doesn't mean that we also want to cache
+ Date: header. if we have Date field's index set at this point we
+ know that we want it. otherwise add it and remember that we don't
+ want it cached. */
+ field_idx = get_header_field_idx(mail->mail.mail.box, "Date");
+ match = array_get(&mail->header_match, &match_count);
+ if (field_idx < match_count &&
+ match[field_idx] == mail->header_match_value) {
+ /* cache Date: header */
+ } else if ((data->cache_fetch_fields & MAIL_FETCH_DATE) != 0 ||
+ data->save_sent_date) {
+ /* parse Date: header, but don't cache it. */
+ data->dont_cache_field_idx = field_idx;
+ array_idx_set(&mail->header_match, field_idx,
+ &mail->header_match_value);
+ }
+ mail->data.header_parser_initialized = TRUE;
+ mail->data.parse_line_num = 0;
+ i_zero(&mail->data.parse_line);
+}
+
+static void index_mail_parse_finish_imap_envelope(struct index_mail *mail)
+{
+ struct mail *_mail = &mail->mail.mail;
+ const unsigned int cache_field_envelope =
+ mail->ibox->cache_fields[MAIL_CACHE_IMAP_ENVELOPE].idx;
+ string_t *str;
+
+ str = str_new(mail->mail.data_pool, 256);
+ imap_envelope_write(mail->data.envelope_data, str);
+ mail->data.envelope = str_c(str);
+ mail->data.save_envelope = FALSE;
+
+ if (mail_cache_field_can_add(_mail->transaction->cache_trans,
+ _mail->seq, cache_field_envelope)) {
+ index_mail_cache_add_idx(mail, cache_field_envelope,
+ str_data(str), str_len(str));
+ }
+}
+
+void index_mail_parse_header(struct message_part *part,
+ struct message_header_line *hdr,
+ struct index_mail *mail)
+{
+ struct mail *_mail = &mail->mail.mail;
+ struct index_mail_data *data = &mail->data;
+ unsigned int field_idx, count;
+ uint8_t *match;
+
+ i_assert(data->header_parser_initialized);
+
+ data->parse_line_num++;
+
+ if (data->save_bodystructure_header &&
+ !data->parsed_bodystructure_header) {
+ i_assert(part != NULL);
+ message_part_data_parse_from_header(mail->mail.data_pool, part, hdr);
+ }
+
+ if (data->save_envelope) {
+ message_part_envelope_parse_from_header(mail->mail.data_pool,
+ &data->envelope_data, hdr);
+
+ if (hdr == NULL)
+ index_mail_parse_finish_imap_envelope(mail);
+ }
+
+ if (hdr == NULL) {
+ /* end of headers */
+ if (mail->data.save_sent_date)
+ mail->data.sent_date_parsed = TRUE;
+ T_BEGIN {
+ index_mail_parse_header_finish(mail);
+ } T_END;
+ if (data->save_bodystructure_header) {
+ i_assert(data->parser_ctx != NULL);
+ data->parsed_bodystructure_header = TRUE;
+ }
+ return;
+ }
+
+ if (!hdr->continued) {
+ T_BEGIN {
+ const char *cache_field_name =
+ t_strconcat("hdr.", hdr->name, NULL);
+ data->parse_line.field_idx =
+ mail_cache_register_lookup(_mail->box->cache,
+ cache_field_name);
+ } T_END;
+ }
+ field_idx = data->parse_line.field_idx;
+ match = array_get_modifiable(&mail->header_match, &count);
+ if (field_idx >= count ||
+ !HEADER_MATCH_USABLE(mail, match[field_idx])) {
+ /* we don't want this header. */
+ return;
+ }
+
+ if (!hdr->continued) {
+ /* beginning of a line. add the header name. */
+ data->parse_line.start_pos = str_len(mail->header_data);
+ data->parse_line.line_num = data->parse_line_num;
+ str_append(mail->header_data, hdr->name);
+ str_append_data(mail->header_data, hdr->middle, hdr->middle_len);
+
+ /* remember that we saw this header so we don't add it to
+ cache as nonexistent. */
+ match[field_idx] |= HEADER_MATCH_FLAG_FOUND;
+ }
+ str_append_data(mail->header_data, hdr->value, hdr->value_len);
+ if (!hdr->no_newline)
+ str_append(mail->header_data, "\n");
+ if (!hdr->continues) {
+ data->parse_line.end_pos = str_len(mail->header_data);
+ array_push_back(&mail->header_lines, &data->parse_line);
+ }
+}
+
+static void
+index_mail_parse_part_header_cb(struct message_part *part,
+ struct message_header_line *hdr,
+ struct index_mail *mail)
+{
+ index_mail_parse_header(part, hdr, mail);
+}
+
+static void
+index_mail_parse_header_cb(struct message_header_line *hdr,
+ struct index_mail *mail)
+{
+ index_mail_parse_header(mail->data.parts, hdr, mail);
+}
+
+struct istream *
+index_mail_cache_parse_init(struct mail *_mail, struct istream *input)
+{
+ struct index_mail *mail = INDEX_MAIL(_mail);
+ struct istream *input2;
+
+ i_assert(mail->data.tee_stream == NULL);
+ i_assert(mail->data.parser_ctx == NULL);
+
+ /* we're doing everything for now, figure out later if we want to
+ save them. */
+ mail->data.save_sent_date = TRUE;
+ mail->data.save_bodystructure_header = TRUE;
+ mail->data.save_bodystructure_body = TRUE;
+ /* Don't unnecessarily waste time generating a snippet, since it's
+ not as cheap as the others to generate. */
+ if (index_mail_want_cache(mail, MAIL_CACHE_BODY_SNIPPET))
+ mail->data.save_body_snippet = TRUE;
+
+ mail->data.tee_stream = tee_i_stream_create(input);
+ input = tee_i_stream_create_child(mail->data.tee_stream);
+ input2 = tee_i_stream_create_child(mail->data.tee_stream);
+
+ index_mail_parse_header_init(mail, NULL);
+ mail->data.parser_input = input;
+ mail->data.parser_ctx =
+ message_parser_init(mail->mail.data_pool, input,
+ &msg_parser_set);
+ i_stream_unref(&input);
+ return input2;
+}
+
+static void index_mail_init_parser(struct index_mail *mail)
+{
+ struct index_mail_data *data = &mail->data;
+ struct message_part *parts;
+ const char *error;
+
+ if (data->parser_ctx != NULL) {
+ data->parser_input = NULL;
+ if (message_parser_deinit_from_parts(&data->parser_ctx, &parts, &error) < 0) {
+ index_mail_set_message_parts_corrupted(&mail->mail.mail, error);
+ index_mail_parts_reset(mail);
+ }
+ if (data->parts == NULL || data->parts != parts) {
+ /* The previous parsing didn't finish, so we're
+ re-parsing the header. The new parts don't have data
+ filled anymore. */
+ data->parsed_bodystructure_header = FALSE;
+ }
+ }
+
+ /* make sure parsing starts from the beginning of the stream */
+ i_stream_seek(mail->data.stream, 0);
+ if (data->parts == NULL) {
+ data->parser_input = data->stream;
+ data->parser_ctx = message_parser_init(mail->mail.data_pool,
+ data->stream,
+ &msg_parser_set);
+ } else {
+ data->parser_ctx =
+ message_parser_init_from_parts(data->parts,
+ data->stream,
+ &msg_parser_set);
+ }
+}
+
+int index_mail_parse_headers_internal(struct index_mail *mail,
+ struct mailbox_header_lookup_ctx *headers)
+{
+ struct index_mail_data *data = &mail->data;
+
+ i_assert(data->stream != NULL);
+
+ index_mail_parse_header_init(mail, headers);
+
+ if (data->parts == NULL || data->save_bodystructure_header ||
+ (data->access_part & PARSE_BODY) != 0) {
+ /* initialize bodystructure parsing in case we read the whole
+ message. */
+ index_mail_init_parser(mail);
+ message_parser_parse_header(data->parser_ctx, &data->hdr_size,
+ index_mail_parse_part_header_cb,
+ mail);
+ } else {
+ /* just read the header */
+ i_assert(!data->save_bodystructure_body ||
+ data->parser_ctx != NULL);
+ message_parse_header(data->stream, &data->hdr_size,
+ msg_parser_set.hdr_flags,
+ index_mail_parse_header_cb, mail);
+ }
+ if (index_mail_stream_check_failure(mail) < 0) {
+ index_mail_parse_header_deinit(mail);
+ return -1;
+ }
+ i_assert(!mail->data.header_parser_initialized);
+ data->hdr_size_set = TRUE;
+ data->access_part &= ENUM_NEGATE(PARSE_HDR);
+ return 0;
+}
+
+int index_mail_parse_headers(struct index_mail *mail,
+ struct mailbox_header_lookup_ctx *headers,
+ const char *reason)
+{
+ struct index_mail_data *data = &mail->data;
+ struct istream *input;
+ uoff_t old_offset;
+
+ old_offset = data->stream == NULL ? 0 : data->stream->v_offset;
+
+ if (mail_get_hdr_stream_because(&mail->mail.mail, NULL, reason, &input) < 0)
+ return -1;
+
+ int ret = index_mail_parse_headers_internal(mail, headers);
+ i_stream_seek(data->stream, old_offset);
+ return ret;
+}
+
+static void
+imap_envelope_parse_callback(struct message_header_line *hdr,
+ struct index_mail *mail)
+{
+ message_part_envelope_parse_from_header(mail->mail.data_pool,
+ &mail->data.envelope_data, hdr);
+
+ if (hdr == NULL)
+ index_mail_parse_finish_imap_envelope(mail);
+}
+
+int index_mail_headers_get_envelope(struct index_mail *mail)
+{
+ const unsigned int cache_field_envelope =
+ mail->ibox->cache_fields[MAIL_CACHE_IMAP_ENVELOPE].idx;
+ struct mailbox_header_lookup_ctx *header_ctx;
+ struct istream *stream;
+ uoff_t old_offset;
+ string_t *str;
+
+ str = str_new(mail->mail.data_pool, 256);
+ if (index_mail_cache_lookup_field(mail, str,
+ cache_field_envelope) > 0) {
+ mail->data.envelope = str_c(str);
+ return 0;
+ }
+ str_free(&str);
+
+ old_offset = mail->data.stream == NULL ? 0 :
+ mail->data.stream->v_offset;
+
+ /* Make sure header_cache_callback() isn't also parsing the ENVELOPE.
+ Otherwise two callbacks are doing it and mixing up results. */
+ mail->data.save_envelope = FALSE;
+
+ header_ctx = mailbox_header_lookup_init(mail->mail.mail.box,
+ message_part_envelope_headers);
+ if (mail_get_header_stream(&mail->mail.mail, header_ctx, &stream) < 0) {
+ mailbox_header_lookup_unref(&header_ctx);
+ return -1;
+ }
+ mailbox_header_lookup_unref(&header_ctx);
+
+ if (mail->data.envelope == NULL) {
+ /* we got the headers from cache - parse them to get the
+ envelope */
+ message_parse_header(stream, NULL, msg_parser_set.hdr_flags,
+ imap_envelope_parse_callback, mail);
+ if (stream->stream_errno != 0) {
+ index_mail_stream_log_failure_for(mail, stream);
+ return -1;
+ }
+ i_assert(mail->data.envelope != NULL);
+ }
+
+ if (mail->data.stream != NULL)
+ i_stream_seek(mail->data.stream, old_offset);
+ return 0;
+}
+
+static size_t get_header_size(buffer_t *buffer, size_t pos)
+{
+ const unsigned char *data = buffer->data;
+ size_t i, size = buffer->used;
+
+ i_assert(pos <= size);
+
+ for (i = pos; i < size; i++) {
+ if (data[i] == '\n') {
+ if (i+1 == size ||
+ (data[i+1] != ' ' && data[i+1] != '\t'))
+ return i - pos;
+ }
+ }
+ return size - pos;
+}
+
+static int index_mail_header_is_parsed(struct index_mail *mail,
+ unsigned int field_idx)
+{
+ const uint8_t *match;
+ unsigned int count;
+
+ match = array_get(&mail->header_match, &count);
+ if (field_idx < count && HEADER_MATCH_USABLE(mail, match[field_idx]))
+ return (match[field_idx] & HEADER_MATCH_FLAG_FOUND) != 0 ? 1 : 0;
+ return -1;
+}
+
+static bool skip_header(const unsigned char **data, size_t len)
+{
+ const unsigned char *p = *data;
+ size_t i;
+
+ for (i = 0; i < len; i++) {
+ if (p[i] == ':')
+ break;
+ }
+ if (i == len)
+ return FALSE;
+
+ for (i++; i < len; i++) {
+ if (!IS_LWSP(p[i]))
+ break;
+ }
+
+ *data = p + i;
+ return TRUE;
+}
+
+static const char *const *
+index_mail_get_parsed_header(struct index_mail *mail, unsigned int field_idx)
+{
+ ARRAY(const char *) header_values;
+ const struct index_mail_line *lines;
+ const unsigned char *header, *value_start, *value_end;
+ const unsigned int *line_idx;
+ const char *value;
+ unsigned int i, lines_count, first_line_idx;
+
+ line_idx = array_idx(&mail->header_match_lines, field_idx);
+ i_assert(*line_idx != 0);
+ first_line_idx = *line_idx - 1;
+
+ p_array_init(&header_values, mail->mail.data_pool, 4);
+ header = mail->header_data->data;
+
+ lines = array_get(&mail->header_lines, &lines_count);
+ for (i = first_line_idx; i < lines_count; i++) {
+ if (lines[i].field_idx != lines[first_line_idx].field_idx)
+ break;
+
+ /* skip header: and drop ending LF */
+ value_start = header + lines[i].start_pos;
+ value_end = header + lines[i].end_pos;
+ if (skip_header(&value_start, value_end - value_start)) {
+ if (value_start != value_end && value_end[-1] == '\n')
+ value_end--;
+ value = message_header_strdup(mail->mail.data_pool,
+ value_start,
+ value_end - value_start);
+ array_push_back(&header_values, &value);
+ }
+ }
+
+ array_append_zero(&header_values);
+ return array_front(&header_values);
+}
+
+static int
+index_mail_get_raw_headers(struct index_mail *mail, const char *field,
+ const char *const **value_r)
+{
+ struct mail *_mail = &mail->mail.mail;
+ const char *headers[2], *value;
+ struct mailbox_header_lookup_ctx *headers_ctx;
+ const unsigned char *data;
+ unsigned int field_idx;
+ string_t *dest;
+ size_t i, len, len2;
+ int ret;
+ ARRAY(const char *) header_values;
+
+ i_assert(field != NULL);
+
+ field_idx = get_header_field_idx(_mail->box, field);
+
+ dest = t_str_new(128);
+ if (mail_cache_lookup_headers(_mail->transaction->cache_view, dest,
+ _mail->seq, &field_idx, 1) <= 0) {
+ /* not in cache / error - first see if it's already parsed */
+ p_free(mail->mail.data_pool, dest);
+ if (mail->data.header_parser_initialized) {
+ /* don't try to parse headers recursively. we're here
+ because message size was wrong and istream-mail
+ wants to log some cached headers. */
+ i_assert(mail->mail.mail.lookup_abort >= MAIL_LOOKUP_ABORT_NOT_IN_CACHE);
+ mail_set_aborted(&mail->mail.mail);
+ return -1;
+ }
+ if (mail->header_seq != mail->mail.mail.seq ||
+ index_mail_header_is_parsed(mail, field_idx) < 0) {
+ /* parse */
+ const char *reason = index_mail_cache_reason(_mail,
+ t_strdup_printf("header %s", field));
+ headers[0] = field; headers[1] = NULL;
+ headers_ctx = mailbox_header_lookup_init(_mail->box,
+ headers);
+ ret = index_mail_parse_headers(mail, headers_ctx, reason);
+ mailbox_header_lookup_unref(&headers_ctx);
+ if (ret < 0)
+ return -1;
+ }
+
+ if ((ret = index_mail_header_is_parsed(mail, field_idx)) <= 0) {
+ /* not found */
+ i_assert(ret != -1);
+ *value_r = p_new(mail->mail.data_pool, const char *, 1);
+ return 0;
+ }
+ *value_r = index_mail_get_parsed_header(mail, field_idx);
+ return 0;
+ }
+ _mail->transaction->stats.cache_hit_count++;
+ data = buffer_get_data(dest, &len);
+
+ if (len == 0) {
+ /* cached as nonexistent. */
+ *value_r = p_new(mail->mail.data_pool, const char *, 1);
+ return 0;
+ }
+
+ p_array_init(&header_values, mail->mail.data_pool, 4);
+
+ /* cached. skip "header name: " parts in dest. */
+ for (i = 0; i < len; i++) {
+ if (data[i] == ':') {
+ i++;
+ while (i < len && IS_LWSP(data[i])) i++;
+
+ /* @UNSAFE */
+ len2 = get_header_size(dest, i);
+ value = message_header_strdup(mail->mail.data_pool,
+ data + i, len2);
+ i += len2 + 1;
+
+ array_push_back(&header_values, &value);
+ }
+ }
+
+ array_append_zero(&header_values);
+ *value_r = array_front(&header_values);
+ return 0;
+}
+
+static int unfold_header(pool_t pool, const char **_str)
+{
+ const char *str = *_str;
+ char *new_str;
+ unsigned int i, j;
+
+ for (i = 0; str[i] != '\0'; i++) {
+ if (str[i] == '\n')
+ break;
+ }
+ if (str[i] == '\0')
+ return 0;
+
+ /* @UNSAFE */
+ new_str = p_malloc(pool, i + strlen(str+i) + 1);
+ memcpy(new_str, str, i);
+ for (j = i; str[i] != '\0'; i++) {
+ if (str[i] == '\n') {
+ new_str[j++] = ' ';
+ i++;
+ if (str[i] == '\0')
+ break;
+
+ if (str[i] != ' ' && str[i] != '\t') {
+ /* corrupted */
+ return -1;
+ }
+ } else {
+ new_str[j++] = str[i];
+ }
+ }
+ new_str[j] = '\0';
+ *_str = new_str;
+ return 0;
+}
+
+static void str_replace_nuls(string_t *str)
+{
+ char *data = str_c_modifiable(str);
+ size_t i, len = str_len(str);
+
+ for (i = 0; i < len; i++) {
+ if (data[i] == '\0')
+ data[i] = ' ';
+ }
+}
+
+static int
+index_mail_headers_decode(struct index_mail *mail, const char *const **_list,
+ unsigned int max_count)
+{
+ const char *const *list = *_list;
+ const char **decoded_list, *input;
+ unsigned int i, count;
+ string_t *str;
+
+ count = str_array_length(list);
+ if (count > max_count)
+ count = max_count;
+ decoded_list = p_new(mail->mail.data_pool, const char *, count + 1);
+
+ str = t_str_new(512);
+ for (i = 0; i < count; i++) {
+ str_truncate(str, 0);
+ input = list[i];
+ /* unfold all lines into a single line */
+ if (unfold_header(mail->mail.data_pool, &input) < 0)
+ return -1;
+
+ /* decode MIME encoded-words. decoding may also add new LFs. */
+ message_header_decode_utf8((const unsigned char *)input,
+ strlen(input), str, NULL);
+ if (strcmp(str_c(str), input) != 0) {
+ if (strlen(str_c(str)) != str_len(str)) {
+ /* replace NULs with spaces */
+ str_replace_nuls(str);
+ }
+ input = p_strdup(mail->mail.data_pool, str_c(str));
+ }
+ decoded_list[i] = input;
+ }
+ *_list = decoded_list;
+ return 0;
+}
+
+int index_mail_get_headers(struct mail *_mail, const char *field,
+ bool decode_to_utf8, const char *const **value_r)
+{
+ struct index_mail *mail = INDEX_MAIL(_mail);
+ bool retry = TRUE;
+ int ret;
+
+ for (;; retry = FALSE) {
+ if (index_mail_get_raw_headers(mail, field, value_r) < 0)
+ return -1;
+ if (**value_r == NULL)
+ return 0;
+ if (!decode_to_utf8)
+ return 1;
+
+ T_BEGIN {
+ ret = index_mail_headers_decode(mail, value_r, UINT_MAX);
+ } T_END;
+
+ if (ret < 0 && retry) {
+ mail_set_mail_cache_corrupted(_mail, "Broken header %s",
+ field);
+ } else {
+ break;
+ }
+ }
+ if (ret < 0) {
+ i_panic("BUG: Broken header %s for mail UID %u "
+ "wasn't fixed by re-parsing the header",
+ field, _mail->uid);
+ }
+ return 1;
+}
+
+int index_mail_get_first_header(struct mail *_mail, const char *field,
+ bool decode_to_utf8, const char **value_r)
+{
+ struct index_mail *mail = INDEX_MAIL(_mail);
+ const char *const *list;
+ bool retry = TRUE;
+ int ret;
+
+ for (;; retry = FALSE) {
+ if (index_mail_get_raw_headers(mail, field, &list) < 0)
+ return -1;
+ if (!decode_to_utf8 || list[0] == NULL) {
+ ret = 0;
+ break;
+ }
+
+ T_BEGIN {
+ ret = index_mail_headers_decode(mail, &list, 1);
+ } T_END;
+
+ if (ret < 0 && retry) {
+ mail_set_mail_cache_corrupted(_mail, "Broken header %s",
+ field);
+ /* retry by parsing the full header */
+ } else {
+ break;
+ }
+ }
+ if (ret < 0) {
+ i_panic("BUG: Broken header %s for mail UID %u "
+ "wasn't fixed by re-parsing the header",
+ field, _mail->uid);
+ }
+ *value_r = list[0];
+ return list[0] != NULL ? 1 : 0;
+}
+
+static void
+header_cache_callback(struct header_filter_istream *input ATTR_UNUSED,
+ struct message_header_line *hdr,
+ bool *matched ATTR_UNUSED, struct index_mail *mail)
+{
+ index_mail_parse_header(NULL, hdr, mail);
+}
+
+static void index_mail_filter_stream_destroy(struct index_mail *mail)
+{
+ if (mail->data.filter_stream == NULL)
+ return;
+
+ const unsigned char *data;
+ size_t size;
+
+ /* read through the previous filter_stream. this makes sure that the
+ fields are added to cache, and most importantly it resets
+ header_parser_initialized=FALSE so we don't assert on it. */
+ while (i_stream_read_more(mail->data.filter_stream, &data, &size) > 0)
+ i_stream_skip(mail->data.filter_stream, size);
+ if (mail->data.header_parser_initialized) {
+ /* istream failed while reading the header */
+ i_assert(mail->data.filter_stream->stream_errno != 0);
+ index_mail_parse_header_deinit(mail);
+ }
+ i_stream_destroy(&mail->data.filter_stream);
+}
+
+int index_mail_get_header_stream(struct mail *_mail,
+ struct mailbox_header_lookup_ctx *headers,
+ struct istream **stream_r)
+{
+ struct index_mail *mail = INDEX_MAIL(_mail);
+ struct istream *input;
+ string_t *dest;
+
+ index_mail_filter_stream_destroy(mail);
+
+ if (mail->data.save_bodystructure_header) {
+ /* we have to parse the header. */
+ const char *reason =
+ index_mail_cache_reason(_mail, "bodystructure");
+ mail->data.access_reason_code = "mail:header_fields";
+ if (index_mail_parse_headers(mail, headers, reason) < 0)
+ return -1;
+ }
+
+ dest = str_new(mail->mail.data_pool, 256);
+ if (mail_cache_lookup_headers(_mail->transaction->cache_view, dest,
+ _mail->seq, headers->idx,
+ headers->count) > 0) {
+ str_append(dest, "\n");
+ _mail->transaction->stats.cache_hit_count++;
+ mail->data.filter_stream =
+ i_stream_create_from_data(str_data(dest),
+ str_len(dest));
+ *stream_r = mail->data.filter_stream;
+ return 0;
+ }
+ /* not in cache / error */
+ p_free(mail->mail.data_pool, dest);
+
+ unsigned int first_not_found = UINT_MAX, not_found_count = 0;
+ for (unsigned int i = 0; i < headers->count; i++) {
+ if (mail_cache_field_exists(_mail->transaction->cache_view,
+ _mail->seq, headers->idx[i]) <= 0) {
+ if (not_found_count++ == 0)
+ first_not_found = i;
+ }
+ }
+
+ const char *reason;
+ if (not_found_count == 0)
+ reason = "BUG: all headers seem to exist in cache";
+ else {
+ i_assert(first_not_found != UINT_MAX);
+ reason = index_mail_cache_reason(_mail, t_strdup_printf(
+ "%u/%u headers not cached (first=%s)",
+ not_found_count, headers->count, headers->name[first_not_found]));
+ }
+ mail->data.access_reason_code = "mail:header_fields";
+ if (mail_get_hdr_stream_because(_mail, NULL, reason, &input) < 0)
+ return -1;
+
+ index_mail_parse_header_init(mail, headers);
+ mail->data.filter_stream =
+ i_stream_create_header_filter(mail->data.stream,
+ HEADER_FILTER_INCLUDE |
+ HEADER_FILTER_ADD_MISSING_EOH |
+ HEADER_FILTER_HIDE_BODY,
+ headers->name, headers->count,
+ header_cache_callback, mail);
+ *stream_r = mail->data.filter_stream;
+ return 0;
+}
diff --git a/src/lib-storage/index/index-mail.c b/src/lib-storage/index/index-mail.c
new file mode 100644
index 0000000..48122b1
--- /dev/null
+++ b/src/lib-storage/index/index-mail.c
@@ -0,0 +1,2619 @@
+/* Copyright (c) 2002-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "buffer.h"
+#include "ioloop.h"
+#include "istream.h"
+#include "hex-binary.h"
+#include "str.h"
+#include "mailbox-recent-flags.h"
+#include "message-date.h"
+#include "message-part-data.h"
+#include "message-part-serialize.h"
+#include "message-parser.h"
+#include "message-snippet.h"
+#include "imap-bodystructure.h"
+#include "imap-envelope.h"
+#include "mail-cache.h"
+#include "mail-index-modseq.h"
+#include "index-storage.h"
+#include "istream-mail.h"
+#include "index-mail.h"
+
+#include <fcntl.h>
+
+#define BODY_SNIPPET_ALGO_V1 "1"
+#define BODY_SNIPPET_MAX_CHARS 200
+
+struct mail_cache_field global_cache_fields[MAIL_INDEX_CACHE_FIELD_COUNT] = {
+ { .name = "flags",
+ .type = MAIL_CACHE_FIELD_BITMASK,
+ .field_size = sizeof(uint32_t) },
+ { .name = "date.sent",
+ .type = MAIL_CACHE_FIELD_FIXED_SIZE,
+ .field_size = sizeof(struct mail_sent_date) },
+ { .name = "date.received",
+ .type = MAIL_CACHE_FIELD_FIXED_SIZE,
+ .field_size = sizeof(uint32_t) },
+ { .name = "date.save",
+ .type = MAIL_CACHE_FIELD_FIXED_SIZE,
+ .field_size = sizeof(uint32_t) },
+ { .name = "size.virtual",
+ .type = MAIL_CACHE_FIELD_FIXED_SIZE,
+ .field_size = sizeof(uoff_t) },
+ { .name = "size.physical",
+ .type = MAIL_CACHE_FIELD_FIXED_SIZE,
+ .field_size = sizeof(uoff_t) },
+ { .name = "imap.body",
+ .type = MAIL_CACHE_FIELD_STRING },
+ { .name = "imap.bodystructure",
+ .type = MAIL_CACHE_FIELD_STRING },
+ { .name = "imap.envelope",
+ .type = MAIL_CACHE_FIELD_STRING },
+ { .name = "pop3.uidl",
+ .type = MAIL_CACHE_FIELD_STRING },
+ { .name = "pop3.order",
+ .type = MAIL_CACHE_FIELD_FIXED_SIZE,
+ .field_size = sizeof(uint32_t) },
+ { .name = "guid",
+ .type = MAIL_CACHE_FIELD_STRING },
+ { .name = "mime.parts",
+ .type = MAIL_CACHE_FIELD_VARIABLE_SIZE },
+ { .name = "binary.parts",
+ .type = MAIL_CACHE_FIELD_VARIABLE_SIZE },
+ { .name = "body.snippet",
+ .type = MAIL_CACHE_FIELD_VARIABLE_SIZE }
+ /* FIXME: for now need to update get_metadata_precache_fields() in
+ index-status.c when adding more fields. those fields should probably
+ just be moved here to the same struct. */
+};
+
+static void index_mail_init_data(struct index_mail *mail);
+static int index_mail_parse_body(struct index_mail *mail,
+ enum index_cache_field field);
+static int index_mail_write_body_snippet(struct index_mail *mail);
+
+int index_mail_cache_lookup_field(struct index_mail *mail, buffer_t *buf,
+ unsigned int field_idx)
+{
+ struct mail *_mail = &mail->mail.mail;
+ int ret;
+
+ ret = mail_cache_lookup_field(mail->mail.mail.transaction->cache_view,
+ buf, mail->mail.mail.seq, field_idx);
+ if (ret > 0)
+ mail->mail.mail.transaction->stats.cache_hit_count++;
+
+ /* If the request was lazy mark the field as cache wanted. */
+ if (_mail->lookup_abort == MAIL_LOOKUP_ABORT_NOT_IN_CACHE_START_CACHING &&
+ mail_cache_field_get_decision(_mail->box->cache, field_idx) ==
+ MAIL_CACHE_DECISION_NO) {
+ mail_cache_decision_add(_mail->transaction->cache_view,
+ _mail->seq, field_idx);
+ }
+
+ return ret;
+}
+
+static void index_mail_try_set_attachment_keywords(struct index_mail *mail)
+{
+ enum mail_lookup_abort orig_lookup_abort = mail->mail.mail.lookup_abort;
+ mail->mail.mail.lookup_abort = MAIL_LOOKUP_ABORT_NOT_IN_CACHE;
+ (void)mail_set_attachment_keywords(&mail->mail.mail);
+ mail->mail.mail.lookup_abort = orig_lookup_abort;
+}
+
+static bool
+index_mail_want_attachment_keywords_on_fetch(struct index_mail *mail)
+{
+ const struct mail_storage_settings *mail_set =
+ mailbox_get_settings(mail->mail.mail.box);
+
+ return mail_set->parsed_mail_attachment_detection_add_flags &&
+ !mail_set->parsed_mail_attachment_detection_no_flags_on_fetch &&
+ !mail_has_attachment_keywords(&mail->mail.mail);
+}
+
+static int get_serialized_parts(struct index_mail *mail, buffer_t **part_buf_r)
+{
+ const unsigned int field_idx =
+ mail->ibox->cache_fields[MAIL_CACHE_MESSAGE_PARTS].idx;
+
+ *part_buf_r = t_buffer_create(128);
+ return index_mail_cache_lookup_field(mail, *part_buf_r, field_idx);
+}
+
+static struct message_part *get_unserialized_parts(struct index_mail *mail)
+{
+ struct message_part *parts;
+ buffer_t *part_buf;
+ const char *error;
+
+ if (get_serialized_parts(mail, &part_buf) <= 0)
+ return NULL;
+
+ parts = message_part_deserialize(mail->mail.data_pool, part_buf->data,
+ part_buf->used, &error);
+ if (parts == NULL) {
+ mail_set_mail_cache_corrupted(&mail->mail.mail,
+ "Corrupted cached mime.parts data: %s (parts=%s)",
+ error, binary_to_hex(part_buf->data, part_buf->used));
+ }
+ return parts;
+}
+
+static bool message_parts_have_nuls(const struct message_part *part)
+{
+ for (; part != NULL; part = part->next) {
+ if ((part->flags & MESSAGE_PART_FLAG_HAS_NULS) != 0)
+ return TRUE;
+ if (part->children != NULL) {
+ if (message_parts_have_nuls(part->children))
+ return TRUE;
+ }
+ }
+ return FALSE;
+}
+
+static bool get_cached_parts(struct index_mail *mail)
+{
+ struct message_part *part;
+
+ if (mail->data.parts != NULL)
+ return TRUE;
+ if (mail->data.parser_ctx != NULL) {
+ /* Message is already being parsed. Get the message parts by
+ finishing its parsing, so there won't be any confusion about
+ whether e.g. data->parsed_bodystructure=TRUE match data->parts */
+ return FALSE;
+ }
+
+ T_BEGIN {
+ part = get_unserialized_parts(mail);
+ } T_END;
+ if (part == NULL)
+ return FALSE;
+
+ /* we know the NULs now, update them */
+ if (message_parts_have_nuls(part)) {
+ mail->mail.mail.has_nuls = TRUE;
+ mail->mail.mail.has_no_nuls = FALSE;
+ } else {
+ mail->mail.mail.has_nuls = FALSE;
+ mail->mail.mail.has_no_nuls = TRUE;
+ }
+
+ mail->data.parts = part;
+ if (index_mail_want_attachment_keywords_on_fetch(mail))
+ index_mail_try_set_attachment_keywords(mail);
+ return TRUE;
+}
+
+void index_mail_set_message_parts_corrupted(struct mail *mail, const char *error)
+{
+ buffer_t *part_buf;
+ const char *parts_str;
+
+ if (get_serialized_parts(INDEX_MAIL(mail), &part_buf) <= 0)
+ parts_str = "";
+ else
+ parts_str = binary_to_hex(part_buf->data, part_buf->used);
+
+ mail_set_cache_corrupted(mail,
+ MAIL_FETCH_MESSAGE_PARTS, t_strdup_printf(
+ "Cached MIME parts don't match message during parsing: %s (parts=%s)",
+ error, parts_str));
+}
+
+static bool index_mail_get_fixed_field(struct index_mail *mail,
+ enum index_cache_field field,
+ void *data, size_t data_size)
+{
+ const unsigned int field_idx = mail->ibox->cache_fields[field].idx;
+ buffer_t buf;
+ bool ret;
+
+ buffer_create_from_data(&buf, data, data_size);
+ if (index_mail_cache_lookup_field(mail, &buf, field_idx) <= 0)
+ ret = FALSE;
+ else {
+ i_assert(buf.used == data_size);
+ ret = TRUE;
+ }
+ return ret;
+}
+
+bool index_mail_get_cached_uoff_t(struct index_mail *mail,
+ enum index_cache_field field, uoff_t *size_r)
+{
+ return index_mail_get_fixed_field(mail, field,
+ size_r, sizeof(*size_r));
+}
+
+static bool index_mail_get_pvt(struct mail *_mail)
+{
+ struct mail_private *mail = (struct mail_private *)_mail;
+
+ if (mail->seq_pvt != 0)
+ return TRUE;
+ if (_mail->box->view_pvt == NULL) {
+ /* no private view (set by view syncing) -> no private flags */
+ return FALSE;
+ }
+ if (_mail->saving) {
+ /* mail is still being saved, it has no private flags yet */
+ return FALSE;
+ }
+ i_assert(_mail->uid != 0);
+
+ index_transaction_init_pvt(_mail->transaction);
+ if (!mail_index_lookup_seq(_mail->transaction->view_pvt, _mail->uid,
+ &mail->seq_pvt))
+ mail->seq_pvt = 0;
+ return mail->seq_pvt != 0;
+}
+
+enum mail_flags index_mail_get_flags(struct mail *_mail)
+{
+ struct mail_private *mail = (struct mail_private *)_mail;
+ const struct mail_index_record *rec;
+ enum mail_flags flags, pvt_flags_mask;
+
+ rec = mail_index_lookup(_mail->transaction->view, _mail->seq);
+ flags = rec->flags & (MAIL_FLAGS_NONRECENT |
+ MAIL_INDEX_MAIL_FLAG_BACKEND);
+
+ if (mailbox_recent_flags_have_uid(_mail->box, _mail->uid))
+ flags |= MAIL_RECENT;
+
+ if (index_mail_get_pvt(_mail)) {
+ /* mailbox has private flags */
+ pvt_flags_mask = mailbox_get_private_flags_mask(_mail->box);
+ flags &= ENUM_NEGATE(pvt_flags_mask);
+ rec = mail_index_lookup(_mail->transaction->view_pvt,
+ mail->seq_pvt);
+ flags |= rec->flags & pvt_flags_mask;
+ }
+ return flags;
+}
+
+uint64_t index_mail_get_modseq(struct mail *_mail)
+{
+ struct index_mail *mail = INDEX_MAIL(_mail);
+
+ if (mail->data.modseq != 0)
+ return mail->data.modseq;
+
+ mail_index_modseq_enable(_mail->box->index);
+ mail->data.modseq =
+ mail_index_modseq_lookup(_mail->transaction->view, _mail->seq);
+ return mail->data.modseq;
+}
+
+uint64_t index_mail_get_pvt_modseq(struct mail *_mail)
+{
+ struct index_mail *mail = INDEX_MAIL(_mail);
+
+ if (mail->data.pvt_modseq != 0)
+ return mail->data.pvt_modseq;
+
+ if (mailbox_open_index_pvt(_mail->box) <= 0)
+ return 0;
+ index_transaction_init_pvt(_mail->transaction);
+
+ mail_index_modseq_enable(_mail->box->index_pvt);
+ mail->data.pvt_modseq =
+ mail_index_modseq_lookup(_mail->transaction->view_pvt,
+ _mail->seq);
+ return mail->data.pvt_modseq;
+}
+
+const char *const *index_mail_get_keywords(struct mail *_mail)
+{
+ struct index_mail *mail = INDEX_MAIL(_mail);
+ struct index_mail_data *data = &mail->data;
+ const char *const *names;
+ const unsigned int *keyword_indexes;
+ unsigned int i, count, names_count;
+
+ if (array_is_created(&data->keywords))
+ return array_front(&data->keywords);
+
+ (void)index_mail_get_keyword_indexes(_mail);
+
+ keyword_indexes = array_get(&data->keyword_indexes, &count);
+ names = array_get(mail->ibox->keyword_names, &names_count);
+ p_array_init(&data->keywords, mail->mail.data_pool, count + 1);
+ for (i = 0; i < count; i++) {
+ const char *name;
+ i_assert(keyword_indexes[i] < names_count);
+
+ name = names[keyword_indexes[i]];
+ array_push_back(&data->keywords, &name);
+ }
+
+ /* end with NULL */
+ array_append_zero(&data->keywords);
+ return array_front(&data->keywords);
+}
+
+const ARRAY_TYPE(keyword_indexes) *
+index_mail_get_keyword_indexes(struct mail *_mail)
+{
+ struct index_mail *mail = INDEX_MAIL(_mail);
+ struct index_mail_data *data = &mail->data;
+
+ if (!array_is_created(&data->keyword_indexes)) {
+ p_array_init(&data->keyword_indexes, mail->mail.data_pool, 32);
+ mail_index_lookup_keywords(_mail->transaction->view,
+ mail->mail.mail.seq,
+ &data->keyword_indexes);
+ }
+ return &data->keyword_indexes;
+}
+
+int index_mail_get_parts(struct mail *_mail, struct message_part **parts_r)
+{
+ struct index_mail *mail = INDEX_MAIL(_mail);
+ struct index_mail_data *data = &mail->data;
+
+ data->cache_fetch_fields |= MAIL_FETCH_MESSAGE_PARTS;
+ if (data->parts != NULL || get_cached_parts(mail)) {
+ *parts_r = data->parts;
+ return 0;
+ }
+
+ if (data->parser_ctx == NULL) {
+ const char *reason =
+ index_mail_cache_reason(_mail, "mime parts");
+ if (index_mail_parse_headers(mail, NULL, reason) < 0)
+ return -1;
+ /* parts may be set now as a result of some plugin */
+ }
+
+ if (data->parts == NULL) {
+ data->save_message_parts = TRUE;
+ if (index_mail_parse_body(mail, 0) < 0)
+ return -1;
+ }
+
+ *parts_r = data->parts;
+ return 0;
+}
+
+int index_mail_get_received_date(struct mail *_mail, time_t *date_r)
+{
+ struct index_mail *mail = INDEX_MAIL(_mail);
+ struct index_mail_data *data = &mail->data;
+
+ data->cache_fetch_fields |= MAIL_FETCH_RECEIVED_DATE;
+ if (data->received_date == (time_t)-1) {
+ uint32_t t;
+
+ if (index_mail_get_fixed_field(mail, MAIL_CACHE_RECEIVED_DATE,
+ &t, sizeof(t)))
+ data->received_date = t;
+ }
+
+ *date_r = data->received_date;
+ return *date_r == (time_t)-1 ? -1 : 0;
+}
+
+int index_mail_get_save_date(struct mail *_mail, time_t *date_r)
+{
+ struct index_mail *mail = INDEX_MAIL(_mail);
+ struct index_mail_data *data = &mail->data;
+
+ data->cache_fetch_fields |= MAIL_FETCH_SAVE_DATE;
+ if (data->save_date == (time_t)-1) {
+ uint32_t t;
+
+ if (index_mail_get_fixed_field(mail, MAIL_CACHE_SAVE_DATE,
+ &t, sizeof(t)))
+ data->save_date = t;
+ }
+
+ *date_r = data->save_date;
+ return *date_r == (time_t)-1 ? -1 : 1;
+}
+
+static int index_mail_cache_sent_date(struct index_mail *mail)
+{
+ struct index_mail_data *data = &mail->data;
+ const char *str;
+ time_t t;
+ int ret, tz;
+
+ if (data->sent_date.time != (uint32_t)-1)
+ return 0;
+
+ if ((ret = mail_get_first_header(&mail->mail.mail, "Date", &str)) < 0)
+ return ret;
+
+ if (ret == 0 ||
+ !message_date_parse((const unsigned char *)str,
+ strlen(str), &t, &tz)) {
+ /* 0 = not found / invalid */
+ t = 0;
+ tz = 0;
+ }
+ data->sent_date.time = t;
+ data->sent_date.timezone = tz;
+ index_mail_cache_add(mail, MAIL_CACHE_SENT_DATE,
+ &data->sent_date, sizeof(data->sent_date));
+ return 0;
+}
+
+int index_mail_get_date(struct mail *_mail, time_t *date_r, int *timezone_r)
+{
+ struct index_mail *mail = INDEX_MAIL(_mail);
+ struct index_mail_data *data = &mail->data;
+ struct mail_sent_date sentdate;
+
+ data->cache_fetch_fields |= MAIL_FETCH_DATE;
+ if (data->sent_date.time != (uint32_t)-1) {
+ *timezone_r = data->sent_date.timezone;
+ *date_r = data->sent_date.time;
+ return 0;
+ }
+
+ if (index_mail_get_fixed_field(mail, MAIL_CACHE_SENT_DATE,
+ &sentdate, sizeof(sentdate)))
+ data->sent_date = sentdate;
+
+ if (index_mail_cache_sent_date(mail) < 0)
+ return -1;
+
+ *timezone_r = data->sent_date.timezone;
+ *date_r = data->sent_date.time;
+ return 0;
+}
+
+static bool get_cached_msgpart_sizes(struct index_mail *mail)
+{
+ struct index_mail_data *data = &mail->data;
+
+ if (data->parts == NULL)
+ (void)get_cached_parts(mail);
+
+ if (data->parts != NULL) {
+ data->hdr_size_set = TRUE;
+ data->hdr_size = data->parts->header_size;
+ data->body_size = data->parts->body_size;
+ data->body_size_set = TRUE;
+ data->virtual_size = data->parts->header_size.virtual_size +
+ data->body_size.virtual_size;
+ data->physical_size = data->parts->header_size.physical_size +
+ data->body_size.physical_size;
+ }
+
+ return data->parts != NULL;
+}
+
+const uint32_t *index_mail_get_vsize_extension(struct mail *_mail)
+{
+ const void *idata;
+ bool expunged ATTR_UNUSED;
+
+ mail_index_lookup_ext(_mail->transaction->view, _mail->seq,
+ _mail->box->mail_vsize_ext_id, &idata, &expunged);
+ const uint32_t *vsize = idata;
+ return vsize;
+}
+
+static void index_mail_try_set_body_size(struct index_mail *mail)
+{
+ struct index_mail_data *data = &mail->data;
+
+ if (data->hdr_size_set && !data->inexact_total_sizes &&
+ data->physical_size != UOFF_T_MAX &&
+ data->virtual_size != UOFF_T_MAX) {
+ /* We know the total size of this mail and we know the
+ header size, so we can calculate also the body size.
+ However, don't do this if there's a possibility that
+ physical_size or virtual_size don't actually match the
+ mail stream's size (e.g. buggy imapc servers). */
+ if (data->physical_size < data->hdr_size.physical_size) {
+ mail_set_cache_corrupted(&mail->mail.mail,
+ MAIL_FETCH_PHYSICAL_SIZE, t_strdup_printf(
+ "Cached physical size smaller than header size "
+ "(%"PRIuUOFF_T" < %"PRIuUOFF_T")",
+ data->physical_size, data->hdr_size.physical_size));
+ } else if (data->virtual_size < data->hdr_size.virtual_size) {
+ mail_set_cache_corrupted(&mail->mail.mail,
+ MAIL_FETCH_VIRTUAL_SIZE, t_strdup_printf(
+ "Cached virtual size smaller than header size "
+ "(%"PRIuUOFF_T" < %"PRIuUOFF_T")",
+ data->virtual_size, data->hdr_size.virtual_size));
+ } else {
+ data->body_size.physical_size = data->physical_size -
+ data->hdr_size.physical_size;
+ data->body_size.virtual_size = data->virtual_size -
+ data->hdr_size.virtual_size;
+ data->body_size_set = TRUE;
+ }
+ }
+}
+
+bool index_mail_get_cached_virtual_size(struct index_mail *mail, uoff_t *size_r)
+{
+ struct index_mail_data *data = &mail->data;
+ struct mail *_mail = &mail->mail.mail;
+ uoff_t size;
+ unsigned int idx ATTR_UNUSED;
+
+ /* see if we can get it from index */
+ const uint32_t *vsize = index_mail_get_vsize_extension(_mail);
+
+ data->cache_fetch_fields |= MAIL_FETCH_VIRTUAL_SIZE;
+ if (data->virtual_size == UOFF_T_MAX && vsize != NULL && *vsize > 0)
+ data->virtual_size = (*vsize)-1;
+ if (data->virtual_size == UOFF_T_MAX) {
+ if (index_mail_get_cached_uoff_t(mail,
+ MAIL_CACHE_VIRTUAL_FULL_SIZE,
+ &size))
+ data->virtual_size = size;
+ else {
+ if (!get_cached_msgpart_sizes(mail))
+ return FALSE;
+ }
+ }
+ index_mail_try_set_body_size(mail);
+ *size_r = data->virtual_size;
+
+ /* if vsize is present and wanted for index, but missing from index
+ add it to index. */
+ if (vsize != NULL && *vsize == 0 &&
+ data->virtual_size < (uint32_t)-1) {
+ uint32_t vsize = data->virtual_size+1;
+ mail_index_update_ext(_mail->transaction->itrans, _mail->seq,
+ _mail->box->mail_vsize_ext_id, &vsize, NULL);
+ }
+
+ return TRUE;
+}
+
+static void index_mail_get_cached_body_size(struct index_mail *mail)
+{
+ struct index_mail_data *data = &mail->data;
+ uoff_t tmp;
+
+ if (!data->hdr_size_set)
+ return;
+
+ /* we've already called get_cached_msgpart_sizes() and it didn't work.
+ try to do this by using cached virtual size and a quick physical
+ size lookup. */
+ if (!index_mail_get_cached_virtual_size(mail, &tmp))
+ return;
+
+ if (!data->body_size_set) {
+ enum mail_lookup_abort old_abort = mail->mail.mail.lookup_abort;
+
+ /* get the physical size, but not if it requires reading
+ through the whole message */
+ if (mail->mail.mail.lookup_abort < MAIL_LOOKUP_ABORT_READ_MAIL)
+ mail->mail.mail.lookup_abort = MAIL_LOOKUP_ABORT_READ_MAIL;
+ if (mail_get_physical_size(&mail->mail.mail, &tmp) == 0) {
+ /* we should have everything now. try again. */
+ (void)index_mail_get_cached_virtual_size(mail, &tmp);
+ }
+ mail->mail.mail.lookup_abort = old_abort;
+ }
+}
+
+int index_mail_get_virtual_size(struct mail *_mail, uoff_t *size_r)
+{
+ struct index_mail *mail = INDEX_MAIL(_mail);
+ struct index_mail_data *data = &mail->data;
+ struct message_size hdr_size, body_size;
+ struct istream *input;
+ uoff_t old_offset;
+
+ if (index_mail_get_cached_virtual_size(mail, size_r))
+ return 0;
+
+ old_offset = data->stream == NULL ? 0 : data->stream->v_offset;
+ if (mail_get_stream_because(_mail, &hdr_size, &body_size,
+ index_mail_cache_reason(_mail, "virtual size"), &input) < 0)
+ return -1;
+ i_stream_seek(data->stream, old_offset);
+
+ i_assert(data->virtual_size != UOFF_T_MAX);
+ *size_r = data->virtual_size;
+ return 0;
+}
+
+int index_mail_get_physical_size(struct mail *_mail, uoff_t *size_r)
+{
+ struct index_mail *mail = INDEX_MAIL(_mail);
+ struct index_mail_data *data = &mail->data;
+ uoff_t size;
+
+ if (_mail->lookup_abort != MAIL_LOOKUP_ABORT_NOT_IN_CACHE &&
+ _mail->lookup_abort != MAIL_LOOKUP_ABORT_READ_MAIL) {
+ /* If size.physical isn't in cache yet, add it. Do this only
+ when the caller appears to actually want it to be cached.
+ We don't want to cache the size when coming in here from
+ i_stream_mail_try_get_cached_size() or
+ index_mail_get_cached_body_size(). */
+ data->cache_fetch_fields |= MAIL_FETCH_PHYSICAL_SIZE;
+ }
+ if (data->physical_size == UOFF_T_MAX) {
+ if (index_mail_get_cached_uoff_t(mail,
+ MAIL_CACHE_PHYSICAL_FULL_SIZE,
+ &size))
+ data->physical_size = size;
+ else
+ (void)get_cached_msgpart_sizes(mail);
+ }
+ *size_r = data->physical_size;
+ return *size_r == UOFF_T_MAX ? -1 : 0;
+}
+
+void index_mail_cache_add(struct index_mail *mail, enum index_cache_field field,
+ const void *data, size_t data_size)
+{
+ index_mail_cache_add_idx(mail, mail->ibox->cache_fields[field].idx,
+ data, data_size);
+}
+
+void index_mail_cache_add_idx(struct index_mail *mail, unsigned int field_idx,
+ const void *data, size_t data_size)
+{
+ struct mail *_mail = &mail->mail.mail;
+ const struct mail_storage_settings *set = _mail->box->storage->set;
+ const struct mail_index_header *hdr;
+
+ if (set->mail_cache_min_mail_count > 0) {
+ /* First check if we've configured caching not to be used with
+ low enough message count. */
+ hdr = mail_index_get_header(_mail->transaction->view);
+ if (hdr->messages_count < set->mail_cache_min_mail_count)
+ return;
+ }
+
+ if (!mail->data.no_caching &&
+ mail->data.dont_cache_field_idx != field_idx &&
+ !_mail->box->mail_cache_disabled) {
+ mail_cache_add(_mail->transaction->cache_trans, _mail->seq,
+ field_idx, data, data_size);
+ }
+}
+
+void index_mail_cache_pop3_data(struct mail *_mail,
+ const char *uidl, uint32_t order)
+{
+ struct index_mail *mail = INDEX_MAIL(_mail);
+
+ if (uidl != NULL)
+ index_mail_cache_add(mail, MAIL_CACHE_POP3_UIDL,
+ uidl, strlen(uidl));
+
+ if (order != 0)
+ index_mail_cache_add(mail, MAIL_CACHE_POP3_ORDER,
+ &order, sizeof(order));
+}
+
+static void parse_bodystructure_part_header(struct message_part *part,
+ struct message_header_line *hdr,
+ pool_t pool)
+{
+ message_part_data_parse_from_header(pool, part, hdr);
+}
+
+static bool want_plain_bodystructure_cached(struct index_mail *mail)
+{
+ const unsigned int cache_field_body =
+ mail->ibox->cache_fields[MAIL_CACHE_IMAP_BODY].idx;
+ const unsigned int cache_field_bodystructure =
+ mail->ibox->cache_fields[MAIL_CACHE_IMAP_BODYSTRUCTURE].idx;
+ struct mail *_mail = &mail->mail.mail;
+
+ if ((mail->data.wanted_fields & (MAIL_FETCH_IMAP_BODY |
+ MAIL_FETCH_IMAP_BODYSTRUCTURE)) != 0)
+ return TRUE;
+
+ if (mail_cache_field_want_add(_mail->transaction->cache_trans,
+ _mail->seq, cache_field_body))
+ return TRUE;
+ if (mail_cache_field_want_add(_mail->transaction->cache_trans,
+ _mail->seq, cache_field_bodystructure))
+ return TRUE;
+ return FALSE;
+}
+
+static void index_mail_body_parsed_cache_flags(struct index_mail *mail)
+{
+ struct mail *_mail = &mail->mail.mail;
+ struct index_mail_data *data = &mail->data;
+ unsigned int cache_flags_idx;
+ uint32_t cache_flags = data->cache_flags;
+ bool want_cached;
+
+ cache_flags_idx = mail->ibox->cache_fields[MAIL_CACHE_FLAGS].idx;
+ want_cached = mail_cache_field_want_add(_mail->transaction->cache_trans,
+ _mail->seq, cache_flags_idx);
+
+ if (data->parsed_bodystructure &&
+ message_part_data_is_plain_7bit(data->parts) &&
+ (want_cached || want_plain_bodystructure_cached(mail))) {
+ cache_flags |= MAIL_CACHE_FLAG_TEXT_PLAIN_7BIT_ASCII;
+ /* we need message_parts cached to be able to
+ actually use it in BODY/BODYSTRUCTURE reply */
+ want_cached = TRUE;
+ data->save_message_parts = TRUE;
+ }
+
+ /* cache flags should never get unset as long as the message doesn't
+ change, but try to handle it anyway */
+ cache_flags &= ENUM_NEGATE(MAIL_CACHE_FLAG_BINARY_HEADER |
+ MAIL_CACHE_FLAG_BINARY_BODY |
+ MAIL_CACHE_FLAG_HAS_NULS |
+ MAIL_CACHE_FLAG_HAS_NO_NULS);
+ if (message_parts_have_nuls(data->parts)) {
+ _mail->has_nuls = TRUE;
+ _mail->has_no_nuls = FALSE;
+ cache_flags |= MAIL_CACHE_FLAG_HAS_NULS;
+ } else {
+ _mail->has_nuls = FALSE;
+ _mail->has_no_nuls = TRUE;
+ cache_flags |= MAIL_CACHE_FLAG_HAS_NO_NULS;
+ }
+
+ if (data->hdr_size.virtual_size == data->hdr_size.physical_size)
+ cache_flags |= MAIL_CACHE_FLAG_BINARY_HEADER;
+ if (data->body_size.virtual_size == data->body_size.physical_size)
+ cache_flags |= MAIL_CACHE_FLAG_BINARY_BODY;
+
+ if (cache_flags != data->cache_flags && want_cached) {
+ index_mail_cache_add_idx(mail, cache_flags_idx,
+ &cache_flags, sizeof(cache_flags));
+ }
+ data->cache_flags = cache_flags;
+}
+
+static void index_mail_body_parsed_cache_message_parts(struct index_mail *mail)
+{
+ struct mail *_mail = &mail->mail.mail;
+ struct index_mail_data *data = &mail->data;
+ const unsigned int cache_field =
+ mail->ibox->cache_fields[MAIL_CACHE_MESSAGE_PARTS].idx;
+ enum mail_cache_decision_type decision;
+ buffer_t *buffer;
+
+ if (data->messageparts_saved_to_cache ||
+ mail_cache_field_exists(_mail->transaction->cache_view, _mail->seq,
+ cache_field) != 0) {
+ /* already cached */
+ return;
+ }
+
+ decision = mail_cache_field_get_decision(_mail->box->cache,
+ cache_field);
+ if (decision == (MAIL_CACHE_DECISION_NO | MAIL_CACHE_DECISION_FORCED)) {
+ /* we never want it cached */
+ return;
+ }
+ if (decision == MAIL_CACHE_DECISION_NO &&
+ !data->save_message_parts &&
+ (data->wanted_fields & MAIL_FETCH_MESSAGE_PARTS) == 0) {
+ /* we didn't really care about the message parts themselves,
+ just wanted to use something that depended on it */
+ return;
+ }
+
+ T_BEGIN {
+ buffer = t_buffer_create(1024);
+ message_part_serialize(mail->data.parts, buffer);
+ index_mail_cache_add_idx(mail, cache_field,
+ buffer->data, buffer->used);
+ } T_END;
+
+ data->messageparts_saved_to_cache = TRUE;
+}
+
+static int
+index_mail_write_bodystructure(struct index_mail *mail, string_t *str,
+ bool extended)
+{
+ const char *error;
+
+ if (imap_bodystructure_write(mail->data.parts, str, extended,
+ &error) < 0) {
+ mail_set_cache_corrupted(&mail->mail.mail,
+ MAIL_FETCH_MESSAGE_PARTS, error);
+ return -1;
+ }
+ return 0;
+}
+
+static void
+index_mail_body_parsed_cache_bodystructure(struct index_mail *mail,
+ enum index_cache_field field)
+{
+ struct mail *_mail = &mail->mail.mail;
+ struct index_mail_data *data = &mail->data;
+ const unsigned int cache_field_parts =
+ mail->ibox->cache_fields[MAIL_CACHE_MESSAGE_PARTS].idx;
+ const unsigned int cache_field_body =
+ mail->ibox->cache_fields[MAIL_CACHE_IMAP_BODY].idx;
+ const unsigned int cache_field_bodystructure =
+ mail->ibox->cache_fields[MAIL_CACHE_IMAP_BODYSTRUCTURE].idx;
+ enum mail_cache_decision_type dec;
+ string_t *str;
+ bool bodystructure_cached = FALSE;
+ bool plain_bodystructure = FALSE;
+ bool cache_bodystructure, cache_body;
+
+ if ((data->cache_flags & MAIL_CACHE_FLAG_TEXT_PLAIN_7BIT_ASCII) != 0) {
+ if (data->messageparts_saved_to_cache ||
+ mail_cache_field_exists(_mail->transaction->cache_view,
+ _mail->seq, cache_field_parts) > 0) {
+ /* cached it as flag + message_parts */
+ plain_bodystructure = TRUE;
+ }
+ }
+
+ if (!data->parsed_bodystructure)
+ return;
+ i_assert(data->parts != NULL);
+
+ /* If BODY is fetched first but BODYSTRUCTURE is also wanted, we don't
+ normally want to first cache BODY and then BODYSTRUCTURE. So check
+ the wanted_fields also in here. */
+ if (plain_bodystructure)
+ cache_bodystructure = FALSE;
+ else if (field == MAIL_CACHE_IMAP_BODYSTRUCTURE ||
+ (data->wanted_fields & MAIL_FETCH_IMAP_BODYSTRUCTURE) != 0) {
+ cache_bodystructure =
+ mail_cache_field_can_add(_mail->transaction->cache_trans,
+ _mail->seq, cache_field_bodystructure);
+ } else {
+ cache_bodystructure =
+ mail_cache_field_want_add(_mail->transaction->cache_trans,
+ _mail->seq, cache_field_bodystructure);
+ }
+ if (cache_bodystructure) {
+ str = str_new(mail->mail.data_pool, 128);
+ if (index_mail_write_bodystructure(mail, str, TRUE) == 0) {
+ data->bodystructure = str_c(str);
+ index_mail_cache_add(mail, MAIL_CACHE_IMAP_BODYSTRUCTURE,
+ str_c(str), str_len(str));
+ bodystructure_cached = TRUE;
+ }
+ } else {
+ bodystructure_cached =
+ mail_cache_field_exists(_mail->transaction->cache_view,
+ _mail->seq, cache_field_bodystructure) > 0;
+ }
+
+ /* normally don't cache both BODY and BODYSTRUCTURE, but do it
+ if BODY is forced to be cached */
+ dec = mail_cache_field_get_decision(_mail->box->cache,
+ cache_field_body);
+ if (plain_bodystructure ||
+ (bodystructure_cached &&
+ (dec != (MAIL_CACHE_DECISION_FORCED | MAIL_CACHE_DECISION_YES))))
+ cache_body = FALSE;
+ else if (field == MAIL_CACHE_IMAP_BODY) {
+ cache_body =
+ mail_cache_field_can_add(_mail->transaction->cache_trans,
+ _mail->seq, cache_field_body);
+ } else {
+ cache_body =
+ mail_cache_field_want_add(_mail->transaction->cache_trans,
+ _mail->seq, cache_field_body);
+ }
+
+ if (cache_body) {
+ str = str_new(mail->mail.data_pool, 128);
+ if (index_mail_write_bodystructure(mail, str, FALSE) == 0) {
+ data->body = str_c(str);
+ index_mail_cache_add(mail, MAIL_CACHE_IMAP_BODY,
+ str_c(str), str_len(str));
+ }
+ }
+}
+
+bool index_mail_want_cache(struct index_mail *mail, enum index_cache_field field)
+{
+ struct mail *_mail = &mail->mail.mail;
+ enum mail_fetch_field fetch_field;
+ unsigned int cache_field;
+
+ switch (field) {
+ case MAIL_CACHE_SENT_DATE:
+ fetch_field = MAIL_FETCH_DATE;
+ break;
+ case MAIL_CACHE_RECEIVED_DATE:
+ fetch_field = MAIL_FETCH_RECEIVED_DATE;
+ break;
+ case MAIL_CACHE_SAVE_DATE:
+ fetch_field = MAIL_FETCH_SAVE_DATE;
+ break;
+ case MAIL_CACHE_VIRTUAL_FULL_SIZE:
+ fetch_field = MAIL_FETCH_VIRTUAL_SIZE;
+ break;
+ case MAIL_CACHE_PHYSICAL_FULL_SIZE:
+ fetch_field = MAIL_FETCH_PHYSICAL_SIZE;
+ break;
+ case MAIL_CACHE_BODY_SNIPPET:
+ fetch_field = MAIL_FETCH_BODY_SNIPPET;
+ break;
+ default:
+ i_unreached();
+ }
+
+ if ((mail->data.dont_cache_fetch_fields & fetch_field) != 0)
+ return FALSE;
+
+ /* If a field has been explicitly requested to be fetched, it's
+ included in data.cache_fetch_fields. In that case use _can_add() to
+ add it to the cache file if at all possible. Otherwise, use
+ _want_add() to use previous caching decisions. */
+ cache_field = mail->ibox->cache_fields[field].idx;
+ if ((mail->data.cache_fetch_fields & fetch_field) != 0) {
+ return mail_cache_field_can_add(_mail->transaction->cache_trans,
+ _mail->seq, cache_field);
+ } else {
+ return mail_cache_field_want_add(_mail->transaction->cache_trans,
+ _mail->seq, cache_field);
+ }
+}
+
+static void index_mail_save_finish_make_snippet(struct index_mail *mail)
+{
+ if (mail->data.save_body_snippet) {
+ if (index_mail_write_body_snippet(mail) < 0)
+ return;
+ mail->data.save_body_snippet = FALSE;
+ }
+
+ if (mail->data.body_snippet != NULL &&
+ index_mail_want_cache(mail, MAIL_CACHE_BODY_SNIPPET)) {
+ index_mail_cache_add(mail, MAIL_CACHE_BODY_SNIPPET,
+ mail->data.body_snippet,
+ strlen(mail->data.body_snippet));
+ }
+}
+
+static void index_mail_cache_sizes(struct index_mail *mail)
+{
+ struct mail *_mail = &mail->mail.mail;
+ struct mail_index_view *view = _mail->transaction->view;
+
+ static enum index_cache_field size_fields[] = {
+ MAIL_CACHE_VIRTUAL_FULL_SIZE,
+ MAIL_CACHE_PHYSICAL_FULL_SIZE
+ };
+ uoff_t sizes[N_ELEMENTS(size_fields)];
+ unsigned int i;
+ uint32_t vsize;
+ uint32_t idx ATTR_UNUSED;
+
+ sizes[0] = mail->data.virtual_size;
+ sizes[1] = mail->data.physical_size;
+
+ /* store the virtual size in index if
+ extension for it exists or
+ extension for box virtual size exists and
+ size fits and is present and
+ size is not cached or
+ cached size differs
+ */
+ if ((mail_index_map_get_ext_idx(view->index->map, _mail->box->mail_vsize_ext_id, &idx) ||
+ mail_index_map_get_ext_idx(view->index->map, _mail->box->vsize_hdr_ext_id, &idx)) &&
+ (sizes[0] != UOFF_T_MAX &&
+ sizes[0] < (uint32_t)-1)) {
+ const uint32_t *vsize_ext =
+ index_mail_get_vsize_extension(_mail);
+ /* vsize = 0 means it's not present in index, consult cache.
+ we store vsize for every +4GB-1 mail to cache because
+ index can only hold 2^32-1 size. Cache will not be used
+ when vsize is stored in index. */
+ vsize = sizes[0] + 1;
+ if (vsize_ext == NULL || vsize != *vsize_ext) {
+ mail_index_update_ext(_mail->transaction->itrans, _mail->seq,
+ _mail->box->mail_vsize_ext_id, &vsize, NULL);
+ }
+ /* it's already in index, so don't update cache */
+ sizes[0] = UOFF_T_MAX;
+ }
+
+ for (i = 0; i < N_ELEMENTS(size_fields); i++) {
+ if (sizes[i] != UOFF_T_MAX &&
+ index_mail_want_cache(mail, size_fields[i])) {
+ index_mail_cache_add(mail, size_fields[i],
+ &sizes[i], sizeof(sizes[i]));
+ }
+ }
+}
+
+static void index_mail_cache_dates(struct index_mail *mail)
+{
+ static enum index_cache_field date_fields[] = {
+ MAIL_CACHE_RECEIVED_DATE,
+ MAIL_CACHE_SAVE_DATE
+ };
+ time_t dates[N_ELEMENTS(date_fields)];
+ unsigned int i;
+ uint32_t t;
+
+ dates[0] = mail->data.received_date;
+ dates[1] = mail->mail.mail.saving ? ioloop_time :
+ mail->data.save_date;
+
+ for (i = 0; i < N_ELEMENTS(date_fields); i++) {
+ if (dates[i] != (time_t)-1 &&
+ index_mail_want_cache(mail, date_fields[i])) {
+ t = dates[i];
+ index_mail_cache_add(mail, date_fields[i],
+ &t, sizeof(t));
+ }
+ }
+
+ if (mail->data.sent_date_parsed &&
+ index_mail_want_cache(mail, MAIL_CACHE_SENT_DATE))
+ (void)index_mail_cache_sent_date(mail);
+}
+
+static struct message_part *
+index_mail_find_first_text_mime_part(struct message_part *parts)
+{
+ struct message_part_data *body_data = parts->data;
+ struct message_part *part;
+
+ i_assert(body_data != NULL);
+
+ if (body_data->content_type == NULL ||
+ strcasecmp(body_data->content_type, "text") == 0) {
+ /* use any text/ part, even if we don't know what exactly
+ it is. */
+ return parts;
+ }
+ if (strcasecmp(body_data->content_type, "multipart") != 0) {
+ /* for now we support only text Content-Types */
+ return NULL;
+ }
+
+ if (strcasecmp(body_data->content_subtype, "alternative") == 0) {
+ /* text/plain > text/html > text/ */
+ struct message_part *html_part = NULL, *text_part = NULL;
+
+ for (part = parts->children; part != NULL; part = part->next) {
+ struct message_part_data *sub_body_data =
+ part->data;
+
+ i_assert(sub_body_data != NULL);
+
+ if (sub_body_data->content_type == NULL ||
+ strcasecmp(sub_body_data->content_type, "text") == 0) {
+ if (sub_body_data->content_subtype == NULL ||
+ strcasecmp(sub_body_data->content_subtype, "plain") == 0)
+ return part;
+ if (strcasecmp(sub_body_data->content_subtype, "html") == 0)
+ html_part = part;
+ else
+ text_part = part;
+ }
+ }
+ return html_part != NULL ? html_part : text_part;
+ }
+ /* find the first usable MIME part */
+ for (part = parts->children; part != NULL; part = part->next) {
+ struct message_part *subpart =
+ index_mail_find_first_text_mime_part(part);
+ if (subpart != NULL)
+ return subpart;
+ }
+ return NULL;
+}
+
+static int index_mail_write_body_snippet(struct index_mail *mail)
+{
+ struct message_part *part;
+ struct istream *input;
+ uoff_t old_offset;
+ string_t *str;
+ int ret;
+
+ i_assert(mail->data.parsed_bodystructure);
+
+ part = index_mail_find_first_text_mime_part(mail->data.parts);
+ if (part == NULL) {
+ mail->data.body_snippet = BODY_SNIPPET_ALGO_V1;
+ return 0;
+ }
+
+ old_offset = mail->data.stream == NULL ? 0 : mail->data.stream->v_offset;
+ const char *reason = index_mail_cache_reason(&mail->mail.mail, "snippet");
+ if (mail_get_stream_because(&mail->mail.mail, NULL, NULL, reason, &input) < 0)
+ return -1;
+ i_assert(mail->data.stream != NULL);
+
+ i_stream_seek(input, part->physical_pos);
+ input = i_stream_create_limit(input, part->header_size.physical_size +
+ part->body_size.physical_size);
+
+ str = str_new(mail->mail.data_pool, 128);
+ str_append(str, BODY_SNIPPET_ALGO_V1);
+ ret = message_snippet_generate(input, BODY_SNIPPET_MAX_CHARS, str);
+ if (ret == 0)
+ mail->data.body_snippet = str_c(str);
+ i_stream_destroy(&input);
+
+ i_stream_seek(mail->data.stream, old_offset);
+ return ret;
+}
+
+void index_mail_parts_reset(struct index_mail *mail)
+{
+ mail->data.parts = NULL;
+ mail->data.parsed_bodystructure_header = FALSE;
+ mail->data.parsed_bodystructure = FALSE;
+}
+
+static int
+index_mail_parse_body_finish(struct index_mail *mail,
+ enum index_cache_field field, bool success)
+{
+ struct istream *parser_input = mail->data.parser_input;
+ const struct mail_storage_settings *mail_set =
+ mailbox_get_settings(mail->mail.mail.box);
+ const char *error = NULL;
+ int ret;
+
+ if (parser_input == NULL) {
+ ret = message_parser_deinit_from_parts(&mail->data.parser_ctx,
+ &mail->data.parts, &error) < 0 ? 0 : 1;
+ } else {
+ mail->data.parser_input = NULL;
+ i_stream_ref(parser_input);
+ ret = message_parser_deinit_from_parts(&mail->data.parser_ctx,
+ &mail->data.parts, &error) < 0 ? 0 : 1;
+ if (success && (parser_input->stream_errno == 0 ||
+ parser_input->stream_errno == EPIPE)) {
+ /* do one final read, which verifies that the message
+ size is correct. */
+ if (i_stream_read(parser_input) != -1 ||
+ i_stream_have_bytes_left(parser_input))
+ i_unreached();
+ }
+ /* EPIPE = input already closed. allow the caller to
+ decide if that is an error or not. (for example we
+ could be coming here from IMAP APPEND when IMAP
+ client has closed the connection too early. we
+ don't want to log an error in that case.)
+ Note that EPIPE may also come from istream-mail which
+ detects a corrupted message size. Either way, the
+ body wasn't successfully parsed. */
+ if (parser_input->stream_errno == 0)
+ ;
+ else if (parser_input->stream_errno == EPIPE)
+ ret = -1;
+ else {
+ index_mail_stream_log_failure_for(mail, parser_input);
+ ret = -1;
+ }
+ i_stream_unref(&parser_input);
+ }
+ if (ret <= 0) {
+ if (ret == 0) {
+ i_assert(error != NULL);
+ index_mail_set_message_parts_corrupted(&mail->mail.mail, error);
+ }
+ index_mail_parts_reset(mail);
+ if (mail->data.save_bodystructure_body)
+ mail->data.save_bodystructure_header = TRUE;
+ if (mail->data.header_parser_initialized)
+ index_mail_parse_header_deinit(mail);
+ return -1;
+ }
+ if (mail->data.header_parser_initialized) {
+ i_assert(!success);
+ index_mail_parse_header_deinit(mail);
+ }
+
+ if (mail->data.save_bodystructure_body) {
+ mail->data.parsed_bodystructure = TRUE;
+ mail->data.save_bodystructure_header = FALSE;
+ mail->data.save_bodystructure_body = FALSE;
+ i_assert(mail->data.parts != NULL);
+ }
+
+ if (mail->data.no_caching) {
+ /* if we're here because we aborted parsing, don't get any
+ further or we may crash while generating output from
+ incomplete data */
+ return 0;
+ }
+
+ (void)get_cached_msgpart_sizes(mail);
+
+ index_mail_body_parsed_cache_flags(mail);
+ index_mail_body_parsed_cache_message_parts(mail);
+ index_mail_body_parsed_cache_bodystructure(mail, field);
+ index_mail_cache_sizes(mail);
+ index_mail_cache_dates(mail);
+ if (mail_set->parsed_mail_attachment_detection_add_flags &&
+ !mail_has_attachment_keywords(&mail->mail.mail))
+ index_mail_try_set_attachment_keywords(mail);
+ return 0;
+}
+
+static void index_mail_stream_log_failure(struct index_mail *mail)
+{
+ index_mail_stream_log_failure_for(mail, mail->data.stream);
+}
+
+int index_mail_stream_check_failure(struct index_mail *mail)
+{
+ if (mail->data.stream->stream_errno == 0)
+ return 0;
+ index_mail_stream_log_failure(mail);
+ return -1;
+}
+
+void index_mail_refresh_expunged(struct mail *mail)
+{
+ mail_index_refresh(mail->box->index);
+ if (mail_index_is_expunged(mail->transaction->view, mail->seq))
+ mail_set_expunged(mail);
+}
+
+void index_mail_stream_log_failure_for(struct index_mail *mail,
+ struct istream *input)
+{
+ struct mail *_mail = &mail->mail.mail;
+
+ i_assert(input->stream_errno != 0);
+
+ if (input->stream_errno == ENOENT) {
+ /* was the mail just expunged? we could get here especially if
+ external attachments are used and the attachment is deleted
+ before we've opened the file. */
+ index_mail_refresh_expunged(_mail);
+ if (_mail->expunged)
+ return;
+ }
+
+ const char *old_error =
+ mailbox_get_last_internal_error(_mail->box, NULL);
+ const char *new_error = t_strdup_printf("read(%s) failed: %s",
+ i_stream_get_name(input), i_stream_get_error(input));
+
+ if (mail->data.istream_error_logged &&
+ strstr(old_error, new_error) != NULL) {
+ /* Avoid logging the same istream error multiple times
+ (even if the read reason is different). The old_error begins
+ with the UID=n prefix, which we can ignore since we know
+ that this mail already logged a critical error, so it has
+ to be about this same mail. */
+ return;
+ }
+ mail->data.istream_error_logged = TRUE;
+ mail_set_critical(_mail, "%s (read reason=%s)", new_error,
+ mail->mail.get_stream_reason == NULL ? "" :
+ mail->mail.get_stream_reason);
+}
+
+static int index_mail_parse_body(struct index_mail *mail,
+ enum index_cache_field field)
+{
+ struct index_mail_data *data = &mail->data;
+ uoff_t old_offset;
+ int ret;
+
+ i_assert(data->parser_ctx != NULL);
+
+ old_offset = data->stream->v_offset;
+ i_stream_seek(data->stream, data->hdr_size.physical_size);
+
+ if (data->save_bodystructure_body) {
+ /* bodystructure header is parsed, we want the body's mime
+ headers too */
+ i_assert(data->parsed_bodystructure_header);
+ message_parser_parse_body(data->parser_ctx,
+ parse_bodystructure_part_header,
+ mail->mail.data_pool);
+ } else {
+ message_parser_parse_body(data->parser_ctx,
+ *null_message_part_header_callback, NULL);
+ }
+ ret = index_mail_stream_check_failure(mail);
+ if (index_mail_parse_body_finish(mail, field, TRUE) < 0)
+ ret = -1;
+
+ i_stream_seek(data->stream, old_offset);
+ return ret;
+}
+
+static void index_mail_stream_destroy_callback(struct index_mail *mail)
+{
+ i_assert(mail->data.destroying_stream);
+
+ mail->data.destroying_stream = FALSE;
+}
+
+void index_mail_set_read_buffer_size(struct mail *_mail, struct istream *input)
+{
+ struct index_mail *mail = INDEX_MAIL(_mail);
+ unsigned int block_size;
+
+ i_stream_set_max_buffer_size(input, MAIL_READ_FULL_BLOCK_SIZE);
+ block_size = (mail->data.access_part & (READ_BODY | PARSE_BODY)) != 0 ?
+ MAIL_READ_FULL_BLOCK_SIZE : MAIL_READ_HDR_BLOCK_SIZE;
+ i_stream_set_init_buffer_size(input, block_size);
+}
+
+int index_mail_init_stream(struct index_mail *mail,
+ struct message_size *hdr_size,
+ struct message_size *body_size,
+ struct istream **stream_r)
+{
+ struct mail *_mail = &mail->mail.mail;
+ struct index_mail_data *data = &mail->data;
+ struct istream *input;
+ bool has_nuls, body_size_from_stream = FALSE;
+ int ret;
+
+ i_assert(_mail->mail_stream_accessed);
+
+ if (!data->initialized_wrapper_stream &&
+ _mail->transaction->stats_track) {
+ input = i_stream_create_mail(_mail, data->stream,
+ !data->stream_has_only_header);
+ i_stream_unref(&data->stream);
+ data->stream = input;
+ data->initialized_wrapper_stream = TRUE;
+ }
+
+ if (!data->destroy_callback_set) {
+ /* do this only once in case a plugin changes the stream.
+ otherwise the check would break. */
+ data->destroy_callback_set = TRUE;
+ i_stream_add_destroy_callback(data->stream,
+ index_mail_stream_destroy_callback, mail);
+ }
+
+ bool want_attachment_kw =
+ index_mail_want_attachment_keywords_on_fetch(mail);
+ if (want_attachment_kw) {
+ data->access_part |= PARSE_HDR | PARSE_BODY;
+ data->access_reason_code = "mail:attachment_keywords";
+ }
+
+ if (hdr_size != NULL || body_size != NULL)
+ (void)get_cached_msgpart_sizes(mail);
+
+ bool want_body_parsing = want_attachment_kw ||
+ (body_size != NULL && !data->body_size_set &&
+ (data->access_part & PARSE_BODY) != 0);
+
+ if (hdr_size != NULL || body_size != NULL || want_body_parsing) {
+ i_stream_seek(data->stream, 0);
+ if (!data->hdr_size_set || want_body_parsing) {
+ if ((data->access_part & (PARSE_HDR | PARSE_BODY)) != 0) {
+ (void)get_cached_parts(mail);
+ if (index_mail_parse_headers_internal(mail, NULL) < 0)
+ return -1;
+ } else {
+ if (message_get_header_size(data->stream,
+ &data->hdr_size,
+ &has_nuls) < 0) {
+ index_mail_stream_log_failure(mail);
+ return -1;
+ }
+ data->hdr_size_set = TRUE;
+ }
+ }
+
+ if (hdr_size != NULL)
+ *hdr_size = data->hdr_size;
+ }
+
+ if (body_size != NULL || want_body_parsing) {
+ if (!data->body_size_set && body_size != NULL)
+ index_mail_get_cached_body_size(mail);
+ if (!data->body_size_set || want_body_parsing) {
+ i_stream_seek(data->stream,
+ data->hdr_size.physical_size);
+ if ((data->access_part & PARSE_BODY) != 0) {
+ if (index_mail_parse_body(mail, 0) < 0)
+ return -1;
+ } else {
+ if (message_get_body_size(data->stream,
+ &data->body_size,
+ &has_nuls) < 0) {
+ index_mail_stream_log_failure(mail);
+ return -1;
+ }
+ data->body_size_set = TRUE;
+ }
+ body_size_from_stream = TRUE;
+ }
+
+ if (body_size != NULL)
+ *body_size = data->body_size;
+ }
+
+ if (data->hdr_size_set && data->body_size_set) {
+ data->virtual_size = data->hdr_size.virtual_size +
+ data->body_size.virtual_size;
+ data->physical_size = data->hdr_size.physical_size +
+ data->body_size.physical_size;
+ if (body_size_from_stream) {
+ /* the sizes were just calculated */
+ data->inexact_total_sizes = FALSE;
+ }
+ } else {
+ /* If body_size==NULL, the caller doesn't care about it.
+ However, try to set it anyway if it can be calculated. */
+ index_mail_try_set_body_size(mail);
+ }
+ ret = index_mail_stream_check_failure(mail);
+
+ i_stream_seek(data->stream, 0);
+ if (ret < 0)
+ return -1;
+ *stream_r = data->stream;
+ return 0;
+}
+
+static int
+index_mail_parse_bodystructure_full(struct index_mail *mail,
+ enum index_cache_field field)
+{
+ struct index_mail_data *data = &mail->data;
+
+ if ((data->save_bodystructure_header &&
+ !data->parsed_bodystructure_header) ||
+ !data->save_bodystructure_body ||
+ field == MAIL_CACHE_BODY_SNIPPET) {
+ /* we haven't parsed the header yet */
+ const char *reason =
+ index_mail_cache_reason(&mail->mail.mail, "bodystructure");
+ bool orig_bodystructure_header =
+ data->save_bodystructure_header;
+ bool orig_bodystructure_body =
+ data->save_bodystructure_body;
+ data->save_bodystructure_header = TRUE;
+ data->save_bodystructure_body = TRUE;
+ (void)get_cached_parts(mail);
+ if (index_mail_parse_headers(mail, NULL, reason) < 0) {
+ data->save_bodystructure_header =
+ orig_bodystructure_header;
+ data->save_bodystructure_body =
+ orig_bodystructure_body;
+ return -1;
+ }
+ i_assert(data->parser_ctx != NULL);
+ }
+
+ return index_mail_parse_body(mail, field);
+}
+
+static int index_mail_parse_bodystructure(struct index_mail *mail,
+ enum index_cache_field field)
+{
+ struct index_mail_data *data = &mail->data;
+ string_t *str;
+
+ if (data->parsed_bodystructure && field != MAIL_CACHE_BODY_SNIPPET) {
+ /* we have everything parsed already, but just not written to
+ a string */
+ index_mail_body_parsed_cache_bodystructure(mail, field);
+ } else {
+ if (index_mail_parse_bodystructure_full(mail, field) < 0)
+ return -1;
+ if (data->parts == NULL) {
+ /* Corrupted mime.parts detected. Retry by parsing
+ the mail. */
+ data->parsed_bodystructure = FALSE;
+ data->parsed_bodystructure_header = FALSE;
+ data->save_bodystructure_header = TRUE;
+ data->save_bodystructure_body = TRUE;
+ if (index_mail_parse_bodystructure_full(mail, field) < 0)
+ return -1;
+ }
+ }
+ i_assert(data->parts != NULL);
+
+ /* if we didn't want to have the body(structure) cached,
+ it's still not written. */
+ switch (field) {
+ case MAIL_CACHE_IMAP_BODY:
+ if (data->body == NULL) {
+ str = str_new(mail->mail.data_pool, 128);
+ if (index_mail_write_bodystructure(mail, str, FALSE) < 0)
+ return -1;
+ data->body = str_c(str);
+ }
+ break;
+ case MAIL_CACHE_IMAP_BODYSTRUCTURE:
+ if (data->bodystructure == NULL) {
+ str = str_new(mail->mail.data_pool, 128);
+ if (index_mail_write_bodystructure(mail, str, TRUE) < 0)
+ return -1;
+ data->bodystructure = str_c(str);
+ }
+ break;
+ case MAIL_CACHE_BODY_SNIPPET:
+ if (data->body_snippet == NULL) {
+ if (index_mail_write_body_snippet(mail) < 0)
+ return -1;
+
+ if (index_mail_want_cache(mail, MAIL_CACHE_BODY_SNIPPET))
+ index_mail_cache_add(mail, MAIL_CACHE_BODY_SNIPPET,
+ mail->data.body_snippet,
+ strlen(mail->data.body_snippet));
+ }
+ i_assert(data->body_snippet != NULL &&
+ data->body_snippet[0] != '\0');
+ break;
+ default:
+ i_unreached();
+ }
+ return 0;
+}
+
+static void
+index_mail_get_plain_bodystructure(struct index_mail *mail, string_t *str,
+ bool extended)
+{
+ str_printfa(str, IMAP_BODY_PLAIN_7BIT_ASCII" %"PRIuUOFF_T" %u",
+ mail->data.parts->body_size.virtual_size,
+ mail->data.parts->body_size.lines);
+ if (extended)
+ str_append(str, " NIL NIL NIL NIL");
+}
+
+static int
+index_mail_fetch_body_snippet(struct index_mail *mail, const char **value_r)
+{
+ const struct mail_cache_field *cache_fields = mail->ibox->cache_fields;
+ const unsigned int cache_field =
+ cache_fields[MAIL_CACHE_BODY_SNIPPET].idx;
+ string_t *str;
+
+ mail->data.cache_fetch_fields |= MAIL_FETCH_BODY_SNIPPET;
+ if (mail->data.body_snippet == NULL) {
+ str = str_new(mail->mail.data_pool, 128);
+ if (index_mail_cache_lookup_field(mail, str, cache_field) > 0 &&
+ str_len(str) > 0)
+ mail->data.body_snippet = str_c(str);
+ }
+ if (mail->data.body_snippet != NULL) {
+ *value_r = mail->data.body_snippet;
+ return 0;
+ }
+
+ /* reuse the IMAP bodystructure parsing code to get all the useful
+ headers that we need. */
+ mail->data.save_body_snippet = TRUE;
+ if (index_mail_parse_bodystructure(mail, MAIL_CACHE_BODY_SNIPPET) < 0)
+ return -1;
+ i_assert(mail->data.body_snippet != NULL);
+ *value_r = mail->data.body_snippet;
+ return 0;
+}
+
+bool index_mail_get_cached_body(struct index_mail *mail, const char **value_r)
+{
+ const struct mail_cache_field *cache_fields = mail->ibox->cache_fields;
+ const unsigned int body_cache_field =
+ cache_fields[MAIL_CACHE_IMAP_BODY].idx;
+ const unsigned int bodystructure_cache_field =
+ cache_fields[MAIL_CACHE_IMAP_BODYSTRUCTURE].idx;
+ struct index_mail_data *data = &mail->data;
+ string_t *str;
+ const char *error;
+
+ if (data->body != NULL) {
+ *value_r = data->body;
+ return TRUE;
+ }
+
+ str = str_new(mail->mail.data_pool, 128);
+ if ((data->cache_flags & MAIL_CACHE_FLAG_TEXT_PLAIN_7BIT_ASCII) != 0 &&
+ get_cached_parts(mail)) {
+ index_mail_get_plain_bodystructure(mail, str, FALSE);
+ *value_r = data->body = str_c(str);
+ return TRUE;
+ }
+
+ /* 2) get BODY if it exists */
+ if (index_mail_cache_lookup_field(mail, str, body_cache_field) > 0) {
+ *value_r = data->body = str_c(str);
+ return TRUE;
+ }
+ /* 3) get it using BODYSTRUCTURE if it exists */
+ if (index_mail_cache_lookup_field(mail, str, bodystructure_cache_field) > 0) {
+ data->bodystructure =
+ p_strdup(mail->mail.data_pool, str_c(str));
+ str_truncate(str, 0);
+
+ if (imap_body_parse_from_bodystructure(data->bodystructure,
+ str, &error) < 0) {
+ /* broken, continue.. */
+ mail_set_cache_corrupted(&mail->mail.mail,
+ MAIL_FETCH_IMAP_BODYSTRUCTURE, t_strdup_printf(
+ "Invalid BODYSTRUCTURE %s: %s",
+ data->bodystructure, error));
+ } else {
+ *value_r = data->body = str_c(str);
+ return TRUE;
+ }
+ }
+
+ str_free(&str);
+ return FALSE;
+}
+
+bool index_mail_get_cached_bodystructure(struct index_mail *mail,
+ const char **value_r)
+{
+ const struct mail_cache_field *cache_fields = mail->ibox->cache_fields;
+ const unsigned int bodystructure_cache_field =
+ cache_fields[MAIL_CACHE_IMAP_BODYSTRUCTURE].idx;
+ struct index_mail_data *data = &mail->data;
+ string_t *str;
+
+ if (data->bodystructure != NULL) {
+ *value_r = data->bodystructure;
+ return TRUE;
+ }
+
+ str = str_new(mail->mail.data_pool, 128);
+ if ((data->cache_flags & MAIL_CACHE_FLAG_TEXT_PLAIN_7BIT_ASCII) != 0 &&
+ get_cached_parts(mail))
+ index_mail_get_plain_bodystructure(mail, str, TRUE);
+ else if (index_mail_cache_lookup_field(mail, str,
+ bodystructure_cache_field) <= 0) {
+ str_free(&str);
+ return FALSE;
+ }
+
+ *value_r = data->bodystructure = str_c(str);
+ if (index_mail_want_attachment_keywords_on_fetch(mail))
+ index_mail_try_set_attachment_keywords(mail);
+ return TRUE;
+}
+
+int index_mail_get_special(struct mail *_mail,
+ enum mail_fetch_field field, const char **value_r)
+{
+ struct index_mail *mail = INDEX_MAIL(_mail);
+ struct index_mail_data *data = &mail->data;
+
+ switch (field) {
+ case MAIL_FETCH_IMAP_BODY:
+ if (index_mail_get_cached_body(mail, value_r))
+ return 0;
+
+ /* parse body structure, and save BODY/BODYSTRUCTURE
+ depending on what we want cached */
+ if (index_mail_parse_bodystructure(mail, MAIL_CACHE_IMAP_BODY) < 0)
+ return -1;
+ i_assert(data->body != NULL);
+ *value_r = data->body;
+ return 0;
+ case MAIL_FETCH_IMAP_BODYSTRUCTURE:
+ if (index_mail_get_cached_bodystructure(mail, value_r))
+ return 0;
+
+ if (index_mail_parse_bodystructure(mail, MAIL_CACHE_IMAP_BODYSTRUCTURE) < 0)
+ return -1;
+ i_assert(data->bodystructure != NULL);
+ *value_r = data->bodystructure;
+ return 0;
+ case MAIL_FETCH_IMAP_ENVELOPE:
+ if (data->envelope == NULL) {
+ if (index_mail_headers_get_envelope(mail) < 0)
+ return -1;
+ }
+ *value_r = data->envelope;
+ return 0;
+ case MAIL_FETCH_FROM_ENVELOPE:
+ *value_r = data->from_envelope != NULL ?
+ data->from_envelope : "";
+ return 0;
+ case MAIL_FETCH_BODY_SNIPPET:
+ return index_mail_fetch_body_snippet(mail, value_r);
+ case MAIL_FETCH_STORAGE_ID:
+ case MAIL_FETCH_UIDL_BACKEND:
+ case MAIL_FETCH_SEARCH_RELEVANCY:
+ case MAIL_FETCH_GUID:
+ case MAIL_FETCH_HEADER_MD5:
+ case MAIL_FETCH_POP3_ORDER:
+ case MAIL_FETCH_REFCOUNT:
+ case MAIL_FETCH_REFCOUNT_ID:
+ *value_r = "";
+ return 0;
+ case MAIL_FETCH_MAILBOX_NAME:
+ *value_r = _mail->box->vname;
+ return 0;
+ default:
+ i_unreached();
+ }
+}
+
+int index_mail_get_backend_mail(struct mail *mail,
+ struct mail **real_mail_r)
+{
+ *real_mail_r = mail;
+ return 0;
+}
+
+struct mail *
+index_mail_alloc(struct mailbox_transaction_context *t,
+ enum mail_fetch_field wanted_fields,
+ struct mailbox_header_lookup_ctx *wanted_headers)
+{
+ struct index_mail *mail;
+ pool_t pool;
+
+ pool = pool_alloconly_create("mail", 2048);
+ mail = p_new(pool, struct index_mail, 1);
+
+ index_mail_init(mail, t, wanted_fields, wanted_headers, pool, NULL);
+ return &mail->mail.mail;
+}
+
+void index_mail_init(struct index_mail *mail,
+ struct mailbox_transaction_context *t,
+ enum mail_fetch_field wanted_fields,
+ struct mailbox_header_lookup_ctx *wanted_headers,
+ struct pool *mail_pool,
+ struct pool *data_pool)
+{
+ mail->mail.pool = mail_pool;
+ array_create(&mail->mail.module_contexts, mail->mail.pool,
+ sizeof(void *), 5);
+
+ mail->mail.v = *t->box->mail_vfuncs;
+ mail->mail.mail.box = t->box;
+ mail->mail.mail.transaction = t;
+ t->mail_ref_count++;
+ if (data_pool != NULL)
+ mail->mail.data_pool = data_pool;
+ else
+ mail->mail.data_pool = pool_alloconly_create("index_mail", 16384);
+ mail->ibox = INDEX_STORAGE_CONTEXT(t->box);
+ mail->mail.wanted_fields = wanted_fields;
+ if (wanted_headers != NULL) {
+ mail->mail.wanted_headers = wanted_headers;
+ mailbox_header_lookup_ref(wanted_headers);
+ }
+ index_mail_init_data(mail);
+}
+
+static void index_mail_close_streams_full(struct index_mail *mail, bool closing)
+{
+ struct index_mail_data *data = &mail->data;
+ struct message_part *parts;
+ const char *error;
+
+ if (data->parser_ctx != NULL) {
+ if (message_parser_deinit_from_parts(&data->parser_ctx, &parts, &error) < 0)
+ index_mail_set_message_parts_corrupted(&mail->mail.mail, error);
+ mail->data.parser_input = NULL;
+ if (mail->data.save_bodystructure_body)
+ mail->data.save_bodystructure_header = TRUE;
+ }
+ i_stream_unref(&data->filter_stream);
+ if (data->stream != NULL) {
+ struct istream *orig_stream = data->stream;
+
+ data->destroying_stream = TRUE;
+ if (!closing && data->destroy_callback_set) {
+ /* we're replacing the stream with a new one. it's
+ allowed to have references until the mail is closed
+ (but we can't really check that) */
+ i_stream_remove_destroy_callback(data->stream,
+ index_mail_stream_destroy_callback);
+ }
+ i_stream_unref(&data->stream);
+ /* there must be no references to the mail when the
+ mail is being closed. */
+ if (!closing)
+ data->destroying_stream = FALSE;
+ else if (mail->data.destroying_stream) {
+ i_panic("Input stream %s unexpectedly has references",
+ i_stream_get_name(orig_stream));
+ }
+
+ data->initialized_wrapper_stream = FALSE;
+ data->destroy_callback_set = FALSE;
+ }
+}
+
+void index_mail_close_streams(struct index_mail *mail)
+{
+ index_mail_close_streams_full(mail, FALSE);
+}
+
+static void index_mail_init_data(struct index_mail *mail)
+{
+ struct index_mail_data *data = &mail->data;
+
+ data->virtual_size = UOFF_T_MAX;
+ data->physical_size = UOFF_T_MAX;
+ data->save_date = (time_t)-1;
+ data->received_date = (time_t)-1;
+ data->sent_date.time = (uint32_t)-1;
+ data->dont_cache_field_idx = UINT_MAX;
+
+ data->wanted_fields = mail->mail.wanted_fields;
+ if (mail->mail.wanted_headers != NULL) {
+ data->wanted_headers = mail->mail.wanted_headers;
+ mailbox_header_lookup_ref(data->wanted_headers);
+ }
+}
+
+static void index_mail_reset_data(struct index_mail *mail)
+{
+ i_zero(&mail->data);
+ p_clear(mail->mail.data_pool);
+
+ index_mail_init_data(mail);
+
+ mail->mail.mail.seq = 0;
+ mail->mail.mail.uid = 0;
+ mail->mail.seq_pvt = 0;
+ mail->mail.mail.expunged = FALSE;
+ mail->mail.mail.has_nuls = FALSE;
+ mail->mail.mail.has_no_nuls = FALSE;
+ mail->mail.mail.saving = FALSE;
+ mail->mail.mail.mail_stream_accessed = FALSE;
+ mail->mail.mail.mail_metadata_accessed = FALSE;
+}
+
+void index_mail_close(struct mail *_mail)
+{
+ struct index_mail *mail = INDEX_MAIL(_mail);
+
+ if (mail->mail.mail.seq == 0) {
+ /* mail_set_seq*() hasn't been called yet, or is being called
+ right now. Don't reset anything yet. We especially don't
+ want to reset wanted_fields or wanted_headers so that
+ mail_add_temp_wanted_fields() can be called by plugins
+ before mail_set_seq_saving() for
+ mail_save_context.dest_mail. */
+ return;
+ }
+
+ /* make sure old mail isn't visible in the event anymore even if it's
+ attempted to be used. */
+ event_unref(&mail->mail._event);
+
+ /* If uid == 0 but seq != 0, we came here from saving a (non-mbox)
+ message. If that happens, don't bother checking if anything should
+ be cached since it was already checked. Also by now the transaction
+ may have already been rollbacked and seq point to a nonexistent
+ message. */
+ if (mail->mail.mail.uid != 0) {
+ index_mail_cache_sizes(mail);
+ index_mail_cache_dates(mail);
+ }
+
+ index_mail_close_streams_full(mail, TRUE);
+ /* Notify cache that the mail is no longer open. This mainly helps
+ with INDEX=MEMORY to keep all data added with mail_cache_add() in
+ memory until this point. */
+ mail_cache_close_mail(_mail->transaction->cache_trans, _mail->seq);
+
+ mailbox_header_lookup_unref(&mail->data.wanted_headers);
+ if (!mail->freeing)
+ index_mail_reset_data(mail);
+}
+
+static void check_envelope(struct index_mail *mail)
+{
+ struct mail *_mail = &mail->mail.mail;
+ const unsigned int cache_field_envelope =
+ mail->ibox->cache_fields[MAIL_CACHE_IMAP_ENVELOPE].idx;
+ unsigned int cache_field_hdr;
+
+ if ((mail->data.access_part & PARSE_HDR) != 0) {
+ mail->data.save_envelope = TRUE;
+ return;
+ }
+
+ /* if "imap.envelope" is cached, that's all we need */
+ if (mail_cache_field_exists(_mail->transaction->cache_view,
+ _mail->seq, cache_field_envelope) > 0)
+ return;
+
+ /* don't waste time doing full checks for all required
+ headers. assume that if we have "hdr.message-id" cached,
+ we don't need to parse the header. */
+ cache_field_hdr = mail_cache_register_lookup(_mail->box->cache,
+ "hdr.message-id");
+ if (cache_field_hdr == UINT_MAX ||
+ mail_cache_field_exists(_mail->transaction->cache_view,
+ _mail->seq, cache_field_hdr) <= 0) {
+ mail->data.access_reason_code = "mail:imap_envelope";
+ mail->data.access_part |= PARSE_HDR;
+ }
+ mail->data.save_envelope = TRUE;
+}
+
+void index_mail_update_access_parts_pre(struct mail *_mail)
+{
+ struct index_mail *mail = INDEX_MAIL(_mail);
+ struct index_mail_data *data = &mail->data;
+ struct mail_storage *storage = _mail->box->storage;
+ const struct mail_cache_field *cache_fields = mail->ibox->cache_fields;
+ struct mail_cache_view *cache_view = _mail->transaction->cache_view;
+ const struct mail_storage_settings *mail_set = _mail->box->storage->set;
+
+ if (_mail->seq == 0) {
+ /* mail_add_temp_wanted_fields() called before mail_set_seq*().
+ We'll allow this, since it can be useful for plugins to
+ call it for mail_save_context.dest_mail. This function
+ is called again in mail_set_seq*(). */
+ return;
+ }
+
+ if ((data->wanted_fields & (MAIL_FETCH_NUL_STATE |
+ MAIL_FETCH_IMAP_BODY |
+ MAIL_FETCH_IMAP_BODYSTRUCTURE)) != 0 &&
+ !_mail->has_nuls && !_mail->has_no_nuls) {
+ (void)index_mail_get_fixed_field(mail, MAIL_CACHE_FLAGS,
+ &data->cache_flags,
+ sizeof(data->cache_flags));
+ _mail->has_nuls =
+ (data->cache_flags & MAIL_CACHE_FLAG_HAS_NULS) != 0;
+ _mail->has_no_nuls =
+ (data->cache_flags & MAIL_CACHE_FLAG_HAS_NO_NULS) != 0;
+ /* we currently don't forcibly set the nul state. if it's not
+ already cached, the caller can figure out itself what to
+ do when neither is set */
+ }
+
+ /* see if wanted_fields can tell us if we need to read/parse
+ header/body */
+ if ((data->wanted_fields & MAIL_FETCH_MESSAGE_PARTS) != 0 &&
+ (storage->nonbody_access_fields & MAIL_FETCH_MESSAGE_PARTS) == 0 &&
+ data->parts == NULL) {
+ const unsigned int cache_field =
+ cache_fields[MAIL_CACHE_MESSAGE_PARTS].idx;
+
+ if (mail_cache_field_exists(cache_view, _mail->seq,
+ cache_field) <= 0) {
+ data->access_reason_code = "mail:mime_parts";
+ data->access_part |= PARSE_HDR | PARSE_BODY;
+ data->save_message_parts = TRUE;
+ }
+ }
+
+ if ((data->wanted_fields & MAIL_FETCH_IMAP_ENVELOPE) != 0 &&
+ (storage->nonbody_access_fields & MAIL_FETCH_IMAP_ENVELOPE) == 0 &&
+ data->envelope == NULL)
+ check_envelope(mail);
+
+ if ((data->wanted_fields & MAIL_FETCH_IMAP_BODY) != 0 &&
+ (data->cache_flags & MAIL_CACHE_FLAG_TEXT_PLAIN_7BIT_ASCII) == 0 &&
+ (storage->nonbody_access_fields & MAIL_FETCH_IMAP_BODY) == 0 &&
+ data->body == NULL) {
+ /* we need either imap.body or imap.bodystructure */
+ const unsigned int cache_field1 =
+ cache_fields[MAIL_CACHE_IMAP_BODY].idx;
+ const unsigned int cache_field2 =
+ cache_fields[MAIL_CACHE_IMAP_BODYSTRUCTURE].idx;
+
+ if (mail_cache_field_exists(cache_view, _mail->seq,
+ cache_field1) <= 0 &&
+ mail_cache_field_exists(cache_view, _mail->seq,
+ cache_field2) <= 0) {
+ data->access_reason_code = "mail:imap_bodystructure";
+ data->access_part |= PARSE_HDR | PARSE_BODY;
+ data->save_bodystructure_header = TRUE;
+ data->save_bodystructure_body = TRUE;
+ }
+ }
+
+ if ((data->wanted_fields & MAIL_FETCH_IMAP_BODYSTRUCTURE) != 0 &&
+ (data->cache_flags & MAIL_CACHE_FLAG_TEXT_PLAIN_7BIT_ASCII) == 0 &&
+ (storage->nonbody_access_fields & MAIL_FETCH_IMAP_BODYSTRUCTURE) == 0 &&
+ data->bodystructure == NULL) {
+ const unsigned int cache_field =
+ cache_fields[MAIL_CACHE_IMAP_BODYSTRUCTURE].idx;
+
+ if (mail_cache_field_exists(cache_view, _mail->seq,
+ cache_field) <= 0) {
+ data->access_reason_code = "mail:imap_bodystructure";
+ data->access_part |= PARSE_HDR | PARSE_BODY;
+ data->save_bodystructure_header = TRUE;
+ data->save_bodystructure_body = TRUE;
+ }
+ }
+
+ if ((data->wanted_fields & MAIL_FETCH_DATE) != 0 &&
+ (storage->nonbody_access_fields & MAIL_FETCH_DATE) == 0 &&
+ data->sent_date.time == (uint32_t)-1) {
+ const unsigned int cache_field =
+ cache_fields[MAIL_CACHE_SENT_DATE].idx;
+
+ if (mail_cache_field_exists(cache_view, _mail->seq,
+ cache_field) <= 0) {
+ data->access_reason_code = "mail:date";
+ data->access_part |= PARSE_HDR;
+ data->save_sent_date = TRUE;
+ }
+ }
+ if ((data->wanted_fields & MAIL_FETCH_BODY_SNIPPET) != 0 &&
+ (storage->nonbody_access_fields & MAIL_FETCH_BODY_SNIPPET) == 0) {
+ const unsigned int cache_field =
+ cache_fields[MAIL_CACHE_BODY_SNIPPET].idx;
+
+ if (mail_cache_field_exists(cache_view, _mail->seq,
+ cache_field) <= 0) {
+ data->access_reason_code = "mail:snippet";
+ data->access_part |= PARSE_HDR | PARSE_BODY;
+ data->save_body_snippet = TRUE;
+ }
+ }
+ if ((data->wanted_fields & (MAIL_FETCH_STREAM_HEADER |
+ MAIL_FETCH_STREAM_BODY)) != 0) {
+ /* Clear reason_code if set. The mail is going to be read
+ in any case, so the previous reason for deciding to open
+ the mail won't matter. */
+ data->access_reason_code = NULL;
+ if ((data->wanted_fields & MAIL_FETCH_STREAM_HEADER) != 0)
+ data->access_part |= READ_HDR;
+ if ((data->wanted_fields & MAIL_FETCH_STREAM_BODY) != 0)
+ data->access_part |= READ_BODY;
+ }
+
+ /* NOTE: Keep this attachment detection the last, so that the
+ access_part check works correctly.
+
+ The attachment flag detection is done while parsing BODYSTRUCTURE.
+ We want to do this for mails that are being saved, but also when
+ we need to open the mail body anyway. */
+ if (mail_set->parsed_mail_attachment_detection_add_flags &&
+ (_mail->saving || data->access_part != 0) &&
+ !mail_has_attachment_keywords(&mail->mail.mail)) {
+ data->save_bodystructure_header = TRUE;
+ data->save_bodystructure_body = TRUE;
+ }
+}
+
+void index_mail_update_access_parts_post(struct mail *_mail)
+{
+ struct index_mail *mail = INDEX_MAIL(_mail);
+ struct index_mail_data *data = &mail->data;
+ const struct mail_index_header *hdr;
+ struct istream *input;
+
+ if (_mail->seq == 0) {
+ /* see index_mail_update_access_parts_pre() */
+ return;
+ }
+
+ /* when mail_prefetch_count>1, at this point we've started the
+ prefetching to all the mails and we're now starting to access the
+ first mail. */
+
+ if (data->access_part != 0) {
+ /* open stream immediately to set expunged flag if
+ it's already lost */
+
+ /* open the stream only if we didn't get here from
+ mailbox_save_init() */
+ hdr = mail_index_get_header(_mail->transaction->view);
+ if (!_mail->saving && _mail->uid < hdr->next_uid) {
+ if ((data->access_part & (READ_BODY | PARSE_BODY)) != 0)
+ (void)mail_get_stream_because(_mail, NULL, NULL, "access", &input);
+ else
+ (void)mail_get_hdr_stream(_mail, NULL, &input);
+ }
+ }
+}
+
+void index_mail_set_seq(struct mail *_mail, uint32_t seq, bool saving)
+{
+ struct index_mail *mail = INDEX_MAIL(_mail);
+ const struct mail_index_record *rec;
+ struct mail_index_map *map;
+ bool expunged;
+
+ if (mail->mail.mail.seq == seq) {
+ if (!saving)
+ return;
+ /* we started saving a mail, aborted it, and now we're saving
+ another mail with the same sequence. make sure the mail
+ gets reset. */
+ }
+
+ mail->mail.v.close(&mail->mail.mail);
+
+ mail->mail.mail.seq = seq;
+ mail->mail.mail.saving = saving;
+
+ rec = mail_index_lookup_full(_mail->transaction->view, seq,
+ &map, &expunged);
+ mail->mail.mail.uid = rec->uid;
+
+ /* Recreate the mail event when changing mails. Even though the same
+ mail struct is reused, they are practically different mails. The
+ event should have already been freed by close(). */
+ i_assert(mail->mail._event == NULL);
+
+ if (mail_index_view_is_inconsistent(_mail->transaction->view)) {
+ mail_set_expunged(&mail->mail.mail);
+ return;
+ }
+ /* Allow callers to easily find out if this mail was already expunged
+ by another session. It's possible that it could still be
+ successfully accessed. */
+ if (expunged)
+ mail_set_expunged(&mail->mail.mail);
+
+ if (!mail->mail.search_mail) {
+ index_mail_update_access_parts_pre(_mail);
+ index_mail_update_access_parts_post(_mail);
+ } else {
+ /* searching code will call the
+ index_mail_update_access_parts_*() after we know the mail is
+ actually wanted to be fetched. */
+ }
+ mail->data.initialized = TRUE;
+}
+
+bool index_mail_prefetch(struct mail *_mail)
+{
+ struct index_mail *mail = INDEX_MAIL(_mail);
+/* HAVE_POSIX_FADVISE alone isn't enough for CentOS 4.9 */
+#if defined(HAVE_POSIX_FADVISE) && defined(POSIX_FADV_WILLNEED)
+ struct mail_storage *storage = _mail->box->storage;
+ struct istream *input;
+ off_t len;
+ int fd;
+
+ if ((storage->class_flags & MAIL_STORAGE_CLASS_FLAG_FILE_PER_MSG) == 0) {
+ /* we're handling only file-per-msg storages for now. */
+ return TRUE;
+ }
+ if (mail->data.access_part == 0) {
+ /* everything we need is cached */
+ return TRUE;
+ }
+
+ if (mail->data.stream == NULL) {
+ (void)mail_get_stream_because(_mail, NULL, NULL, "prefetch", &input);
+ if (mail->data.stream == NULL)
+ return TRUE;
+ }
+
+ /* tell OS to start reading the file into memory */
+ fd = i_stream_get_fd(mail->data.stream);
+ if (fd != -1) {
+ if ((mail->data.access_part & (READ_BODY | PARSE_BODY)) != 0)
+ len = 0;
+ else
+ len = MAIL_READ_HDR_BLOCK_SIZE;
+ if (posix_fadvise(fd, 0, len, POSIX_FADV_WILLNEED) < 0) {
+ i_error("posix_fadvise(%s) failed: %m",
+ i_stream_get_name(mail->data.stream));
+ }
+ mail->data.prefetch_sent = TRUE;
+ }
+#endif
+ return !mail->data.prefetch_sent;
+}
+
+bool index_mail_set_uid(struct mail *_mail, uint32_t uid)
+{
+ struct index_mail *mail = INDEX_MAIL(_mail);
+ uint32_t seq;
+
+ if (mail_index_lookup_seq(_mail->transaction->view, uid, &seq)) {
+ index_mail_set_seq(_mail, seq, FALSE);
+ return TRUE;
+ } else {
+ mail->mail.v.close(&mail->mail.mail);
+ mail->mail.mail.uid = uid;
+ mail_set_expunged(&mail->mail.mail);
+ return FALSE;
+ }
+}
+
+void index_mail_add_temp_wanted_fields(struct mail *_mail,
+ enum mail_fetch_field fields,
+ struct mailbox_header_lookup_ctx *headers)
+{
+ struct index_mail *mail = INDEX_MAIL(_mail);
+ struct index_mail_data *data = &mail->data;
+ struct mailbox_header_lookup_ctx *new_wanted_headers;
+
+ data->wanted_fields |= fields;
+ if (headers == NULL) {
+ /* keep old ones */
+ } else if (data->wanted_headers == NULL) {
+ data->wanted_headers = headers;
+ mailbox_header_lookup_ref(headers);
+ } else {
+ /* merge headers */
+ new_wanted_headers = mailbox_header_lookup_merge(data->wanted_headers,
+ headers);
+ mailbox_header_lookup_unref(&data->wanted_headers);
+ data->wanted_headers = new_wanted_headers;
+ }
+ index_mail_update_access_parts_pre(_mail);
+ /* Don't call _post(), which would try to open the stream. It should be
+ enough to delay the opening until it happens anyway.
+
+ Otherwise there's not really any good place to call this in the
+ plugins: set_seq() call get_stream() internally, which can already
+ start parsing the headers, so it's too late. If we use get_stream()
+ and there's a _post() call here, it gets into infinite loop. The
+ loop could probably be prevented in some way, but it's probably
+ better to eventually try to remove the _post() call entirely
+ everywhere. */
+}
+
+void index_mail_set_uid_cache_updates(struct mail *_mail, bool set)
+{
+ struct index_mail *mail = INDEX_MAIL(_mail);
+
+ mail->data.no_caching = set || mail->data.forced_no_caching;
+}
+
+void index_mail_free(struct mail *_mail)
+{
+ struct index_mail *mail = INDEX_MAIL(_mail);
+
+ mail->freeing = TRUE;
+ mail->mail.v.close(_mail);
+
+ i_assert(_mail->transaction->mail_ref_count > 0);
+ _mail->transaction->mail_ref_count--;
+
+ buffer_free(&mail->header_data);
+ if (array_is_created(&mail->header_lines))
+ array_free(&mail->header_lines);
+ if (array_is_created(&mail->header_match))
+ array_free(&mail->header_match);
+ if (array_is_created(&mail->header_match_lines))
+ array_free(&mail->header_match_lines);
+
+ mailbox_header_lookup_unref(&mail->data.wanted_headers);
+ mailbox_header_lookup_unref(&mail->mail.wanted_headers);
+ event_unref(&mail->mail._event);
+ pool_unref(&mail->mail.data_pool);
+ pool_unref(&mail->mail.pool);
+}
+
+void index_mail_cache_parse_continue(struct mail *_mail)
+{
+ struct index_mail *mail = INDEX_MAIL(_mail);
+ struct message_block block;
+
+ while (message_parser_parse_next_block(mail->data.parser_ctx,
+ &block) > 0) {
+ if (block.size != 0)
+ continue;
+
+ if (!mail->data.header_parsed) {
+ index_mail_parse_header(block.part, block.hdr, mail);
+ if (block.hdr == NULL)
+ mail->data.header_parsed = TRUE;
+ } else {
+ message_part_data_parse_from_header(mail->mail.data_pool,
+ block.part, block.hdr);
+ }
+ }
+}
+
+void index_mail_cache_parse_deinit(struct mail *_mail, time_t received_date,
+ bool success)
+{
+ struct index_mail *mail = INDEX_MAIL(_mail);
+
+ if (!success) {
+ /* we're going to delete this mail anyway,
+ don't bother trying to update cache file */
+ mail->data.no_caching = TRUE;
+ mail->data.forced_no_caching = TRUE;
+
+ if (mail->data.parser_ctx == NULL) {
+ /* we didn't even start cache parsing */
+ i_assert(!mail->data.header_parser_initialized);
+ return;
+ }
+ }
+
+ /* This is needed with 0 byte mails to get hdr=NULL call done. */
+ index_mail_cache_parse_continue(_mail);
+
+ if (mail->data.received_date == (time_t)-1)
+ mail->data.received_date = received_date;
+ if (mail->data.save_date == (time_t)-1) {
+ /* this save_date may not be exactly the same as what we get
+ in future, but then again neither mbox nor maildir
+ guarantees it anyway. */
+ mail->data.save_date = ioloop_time;
+ }
+
+ (void)index_mail_parse_body_finish(mail, 0, success);
+}
+
+static bool
+index_mail_update_pvt_flags(struct mail *_mail, enum modify_type modify_type,
+ enum mail_flags pvt_flags)
+{
+ struct mail_private *mail = (struct mail_private *)_mail;
+ const struct mail_index_record *rec;
+ enum mail_flags old_pvt_flags;
+
+ if (!index_mail_get_pvt(_mail))
+ return FALSE;
+ if (pvt_flags == 0 && modify_type != MODIFY_REPLACE)
+ return FALSE;
+
+ /* see if the flags actually change anything */
+ rec = mail_index_lookup(_mail->transaction->view_pvt, mail->seq_pvt);
+ old_pvt_flags = rec->flags & mailbox_get_private_flags_mask(_mail->box);
+
+ switch (modify_type) {
+ case MODIFY_ADD:
+ return (old_pvt_flags & pvt_flags) != pvt_flags;
+ case MODIFY_REPLACE:
+ return old_pvt_flags != pvt_flags;
+ case MODIFY_REMOVE:
+ return (old_pvt_flags & pvt_flags) != 0;
+ }
+ i_unreached();
+}
+
+void index_mail_update_flags(struct mail *_mail, enum modify_type modify_type,
+ enum mail_flags flags)
+{
+ struct mail_private *mail = (struct mail_private *)_mail;
+ enum mail_flags pvt_flags_mask, pvt_flags = 0;
+ bool update_modseq = FALSE;
+
+ flags &= MAIL_FLAGS_NONRECENT | MAIL_INDEX_MAIL_FLAG_BACKEND;
+
+ if (_mail->box->view_pvt != NULL) {
+ /* mailbox has private flags */
+ pvt_flags_mask = mailbox_get_private_flags_mask(_mail->box);
+ pvt_flags = flags & pvt_flags_mask;
+ flags &= ENUM_NEGATE(pvt_flags_mask);
+ if (index_mail_update_pvt_flags(_mail, modify_type, pvt_flags)) {
+ mail_index_update_flags(_mail->transaction->itrans_pvt,
+ mail->seq_pvt,
+ modify_type, pvt_flags);
+ update_modseq = TRUE;
+ }
+ }
+
+ if (!update_modseq) {
+ /* no forced modseq update */
+ } else if (modify_type == MODIFY_REMOVE) {
+ /* add the modseq update separately */
+ mail_index_update_flags(_mail->transaction->itrans, _mail->seq,
+ MODIFY_ADD, (enum mail_flags )MAIL_INDEX_MAIL_FLAG_UPDATE_MODSEQ);
+ } else {
+ /* add as part of the flag updates */
+ flags |= MAIL_INDEX_MAIL_FLAG_UPDATE_MODSEQ;
+ }
+ mail_index_update_flags(_mail->transaction->itrans, _mail->seq,
+ modify_type, flags);
+}
+
+void index_mail_update_keywords(struct mail *mail, enum modify_type modify_type,
+ struct mail_keywords *keywords)
+{
+ struct index_mail *imail = INDEX_MAIL(mail);
+
+ if (array_is_created(&imail->data.keyword_indexes))
+ array_free(&imail->data.keyword_indexes);
+ if (array_is_created(&imail->data.keywords)) {
+ /* clear the keywords array so the next mail_get_keywords()
+ returns the updated keywords. don't free the array, because
+ then any existing mail_get_keywords() return values would
+ point to broken data. this won't leak memory because the
+ array is allocated from mail's memory pool. */
+ memset(&imail->data.keywords, 0,
+ sizeof(imail->data.keywords));
+ }
+
+ mail_index_update_keywords(mail->transaction->itrans, mail->seq,
+ modify_type, keywords);
+}
+
+void index_mail_update_modseq(struct mail *mail, uint64_t min_modseq)
+{
+ mail_index_update_modseq(mail->transaction->itrans, mail->seq,
+ min_modseq);
+}
+
+void index_mail_update_pvt_modseq(struct mail *mail, uint64_t min_pvt_modseq)
+{
+ if (mail->box->view_pvt == NULL)
+ return;
+ index_transaction_init_pvt(mail->transaction);
+ mail_index_update_modseq(mail->transaction->itrans_pvt, mail->seq,
+ min_pvt_modseq);
+}
+
+void index_mail_expunge(struct mail *mail)
+{
+ enum mail_lookup_abort old_abort = mail->lookup_abort;
+ const char *value;
+ guid_128_t guid_128;
+
+ mail->lookup_abort = MAIL_LOOKUP_ABORT_NOT_IN_CACHE;
+ if (mail_get_special(mail, MAIL_FETCH_GUID, &value) < 0)
+ mail_index_expunge(mail->transaction->itrans, mail->seq);
+ else {
+ mail_generate_guid_128_hash(value, guid_128);
+ mail_index_expunge_guid(mail->transaction->itrans,
+ mail->seq, guid_128);
+ }
+ mail->lookup_abort = old_abort;
+}
+
+static void index_mail_parse(struct mail *mail, bool parse_body)
+{
+ struct index_mail *imail = INDEX_MAIL(mail);
+
+ imail->data.access_part |= PARSE_HDR;
+ if (index_mail_parse_headers(imail, NULL, "precache") == 0) {
+ if (parse_body) {
+ imail->data.access_part |= PARSE_BODY;
+ (void)index_mail_parse_body(imail, 0);
+ }
+ }
+}
+
+int index_mail_precache(struct mail *mail)
+{
+ struct index_mail *imail = INDEX_MAIL(mail);
+ enum mail_fetch_field cache;
+ time_t date;
+ uoff_t size;
+ const char *str;
+
+ if (mail_cache_field_exists_any(mail->transaction->cache_view,
+ mail->seq)) {
+ /* already cached this mail (we should get here only if FTS
+ plugin decreased the first precached seq) */
+ return 0;
+ }
+
+ cache = imail->data.wanted_fields;
+ if ((cache & (MAIL_FETCH_STREAM_HEADER | MAIL_FETCH_STREAM_BODY)) != 0)
+ index_mail_parse(mail, (cache & MAIL_FETCH_STREAM_BODY) != 0);
+ if ((cache & MAIL_FETCH_RECEIVED_DATE) != 0)
+ (void)mail_get_received_date(mail, &date);
+ if ((cache & MAIL_FETCH_SAVE_DATE) != 0)
+ (void)mail_get_save_date(mail, &date);
+ if ((cache & MAIL_FETCH_VIRTUAL_SIZE) != 0)
+ (void)mail_get_virtual_size(mail, &size);
+ if ((cache & MAIL_FETCH_PHYSICAL_SIZE) != 0)
+ (void)mail_get_physical_size(mail, &size);
+ if ((cache & MAIL_FETCH_UIDL_BACKEND) != 0)
+ (void)mail_get_special(mail, MAIL_FETCH_UIDL_BACKEND, &str);
+ if ((cache & MAIL_FETCH_POP3_ORDER) != 0)
+ (void)mail_get_special(mail, MAIL_FETCH_POP3_ORDER, &str);
+ if ((cache & MAIL_FETCH_GUID) != 0)
+ (void)mail_get_special(mail, MAIL_FETCH_GUID, &str);
+ return 0;
+}
+
+static void
+index_mail_reset_vsize_ext(struct mail *mail)
+{
+ unsigned int idx;
+ uint32_t vsize = 0;
+ struct mail_index_view *view = mail->transaction->view;
+ if (mail_index_map_get_ext_idx(view->map, mail->box->mail_vsize_ext_id,
+ &idx)) {
+ mail_index_update_ext(mail->transaction->itrans, mail->seq,
+ mail->box->mail_vsize_ext_id, &vsize, NULL);
+ }
+}
+
+void index_mail_set_cache_corrupted(struct mail *mail,
+ enum mail_fetch_field field,
+ const char *reason)
+{
+ struct index_mail *imail = INDEX_MAIL(mail);
+ const char *field_name;
+
+ switch ((int)field) {
+ case 0:
+ field_name = "fields";
+ break;
+ case MAIL_FETCH_PHYSICAL_SIZE:
+ field_name = "physical size";
+ imail->data.physical_size = UOFF_T_MAX;
+ imail->data.virtual_size = UOFF_T_MAX;
+ index_mail_parts_reset(imail);
+ index_mail_reset_vsize_ext(mail);
+ break;
+ case MAIL_FETCH_VIRTUAL_SIZE:
+ field_name = "virtual size";
+ imail->data.physical_size = UOFF_T_MAX;
+ imail->data.virtual_size = UOFF_T_MAX;
+ index_mail_parts_reset(imail);
+ index_mail_reset_vsize_ext(mail);
+ break;
+ case MAIL_FETCH_MESSAGE_PARTS:
+ field_name = "MIME parts";
+ index_mail_parts_reset(imail);
+ break;
+ case MAIL_FETCH_IMAP_BODY:
+ field_name = "IMAP BODY";
+ imail->data.body = NULL;
+ imail->data.bodystructure = NULL;
+ break;
+ case MAIL_FETCH_IMAP_BODYSTRUCTURE:
+ field_name = "IMAP BODYSTRUCTURE";
+ imail->data.body = NULL;
+ imail->data.bodystructure = NULL;
+ break;
+ default:
+ field_name = t_strdup_printf("#%x", field);
+ }
+
+ /* make sure we don't cache invalid values */
+ mail_cache_transaction_reset(mail->transaction->cache_trans);
+ imail->data.no_caching = TRUE;
+ imail->data.forced_no_caching = TRUE;
+
+ if (mail->saving) {
+ mail_set_critical(mail,
+ "BUG: Broken %s found while saving a new mail: %s",
+ field_name, reason);
+ } else if (reason[0] == '\0') {
+ mail_set_mail_cache_corrupted(mail,
+ "Broken %s in mailbox %s",
+ field_name, mail->box->vname);
+ } else {
+ mail_set_mail_cache_corrupted(mail,
+ "Broken %s in mailbox %s: %s",
+ field_name, mail->box->vname, reason);
+ }
+}
+
+int index_mail_opened(struct mail *mail,
+ struct istream **stream ATTR_UNUSED)
+{
+ struct index_mail *imail =
+ container_of(mail, struct index_mail, mail.mail);
+ struct event_reason *reason = NULL;
+
+ if (imail->data.access_reason_code != NULL)
+ reason = event_reason_begin(imail->data.access_reason_code);
+ mail_opened_event(mail);
+ event_reason_end(&reason);
+ return 0;
+}
+
+void index_mail_save_finish(struct mail_save_context *ctx)
+{
+ struct index_mail *imail = INDEX_MAIL(ctx->dest_mail);
+
+ index_mail_save_finish_make_snippet(imail);
+
+ if (ctx->data.from_envelope != NULL &&
+ imail->data.from_envelope == NULL) {
+ imail->data.from_envelope =
+ p_strdup(imail->mail.data_pool, ctx->data.from_envelope);
+ }
+}
+
+const char *index_mail_cache_reason(struct mail *mail, const char *reason)
+{
+ const char *cache_reason =
+ mail_cache_get_missing_reason(mail->transaction->cache_view, mail->seq);
+ return t_strdup_printf("%s (%s)", reason, cache_reason);
+}
diff --git a/src/lib-storage/index/index-mail.h b/src/lib-storage/index/index-mail.h
new file mode 100644
index 0000000..74107d2
--- /dev/null
+++ b/src/lib-storage/index/index-mail.h
@@ -0,0 +1,292 @@
+#ifndef INDEX_MAIL_H
+#define INDEX_MAIL_H
+
+#include "message-size.h"
+#include "mail-cache.h"
+#include "mail-storage-private.h"
+
+enum index_cache_field {
+ /* fixed size fields */
+ MAIL_CACHE_FLAGS = 0,
+ MAIL_CACHE_SENT_DATE,
+ MAIL_CACHE_RECEIVED_DATE,
+ MAIL_CACHE_SAVE_DATE,
+ MAIL_CACHE_VIRTUAL_FULL_SIZE,
+ MAIL_CACHE_PHYSICAL_FULL_SIZE,
+
+ /* variable sized field */
+ MAIL_CACHE_IMAP_BODY,
+ MAIL_CACHE_IMAP_BODYSTRUCTURE,
+ MAIL_CACHE_IMAP_ENVELOPE,
+ MAIL_CACHE_POP3_UIDL,
+ MAIL_CACHE_POP3_ORDER,
+ MAIL_CACHE_GUID,
+ MAIL_CACHE_MESSAGE_PARTS,
+ MAIL_CACHE_BINARY_PARTS,
+ MAIL_CACHE_BODY_SNIPPET,
+
+ MAIL_INDEX_CACHE_FIELD_COUNT
+};
+extern struct mail_cache_field
+ global_cache_fields[MAIL_INDEX_CACHE_FIELD_COUNT];
+
+#define IMAP_BODY_PLAIN_7BIT_ASCII \
+ "\"text\" \"plain\" (\"charset\" \"us-ascii\") NIL NIL \"7bit\""
+
+enum mail_cache_record_flag {
+ /* If binary flags are set, it's not checked whether mail is
+ missing CRs. So this flag may be set as an optimization for
+ regular non-binary mails as well if it's known that it contains
+ valid CR+LF line breaks. */
+ MAIL_CACHE_FLAG_BINARY_HEADER = 0x0001,
+ MAIL_CACHE_FLAG_BINARY_BODY = 0x0002,
+
+ /* Mail header or body is known to contain NUL characters. */
+ MAIL_CACHE_FLAG_HAS_NULS = 0x0004,
+ /* Mail header or body is known to not contain NUL characters. */
+ MAIL_CACHE_FLAG_HAS_NO_NULS = 0x0020,
+ /* obsolete _HAS_NO_NULS flag, which was being set incorrectly */
+ MAIL_CACHE_FLAG_HAS_NO_NULS_BROKEN = 0x0008,
+
+ /* BODY is IMAP_BODY_PLAIN_7BIT_ASCII and rest of BODYSTRUCTURE
+ fields are NIL */
+ MAIL_CACHE_FLAG_TEXT_PLAIN_7BIT_ASCII = 0x0010
+};
+
+enum index_mail_access_part {
+ READ_HDR = 0x01,
+ READ_BODY = 0x02,
+ PARSE_HDR = 0x04,
+ PARSE_BODY = 0x08
+};
+
+struct mail_sent_date {
+ uint32_t time;
+ int32_t timezone;
+};
+
+struct index_mail_line {
+ unsigned int field_idx;
+ uint32_t start_pos, end_pos;
+ uint32_t line_num;
+};
+
+struct message_header_line;
+
+struct index_mail_data {
+ time_t date, received_date, save_date;
+ uoff_t virtual_size, physical_size;
+
+ struct mail_sent_date sent_date;
+ struct index_mail_line parse_line;
+ uint32_t parse_line_num;
+
+ struct message_part *parts;
+ struct message_binary_part *bin_parts;
+ const char *envelope, *body, *bodystructure, *guid, *filename;
+ const char *from_envelope, *body_snippet;
+ struct message_part_envelope *envelope_data;
+
+ uint32_t cache_flags;
+ uint64_t modseq, pvt_modseq;
+ enum index_mail_access_part access_part;
+ const char *access_reason_code;
+ /* dont_cache_fields overrides cache_fields */
+ enum mail_fetch_field cache_fetch_fields, dont_cache_fetch_fields;
+ unsigned int dont_cache_field_idx;
+ enum mail_fetch_field wanted_fields;
+ struct mailbox_header_lookup_ctx *wanted_headers;
+
+ buffer_t *search_results;
+
+ struct istream *stream, *filter_stream;
+ struct tee_istream *tee_stream;
+ struct message_size hdr_size, body_size;
+ struct istream *parser_input;
+ struct message_parser_ctx *parser_ctx;
+ int parsing_count;
+ ARRAY_TYPE(keywords) keywords;
+ ARRAY_TYPE(keyword_indexes) keyword_indexes;
+
+ bool initialized:1;
+ bool save_sent_date:1;
+ bool sent_date_parsed:1;
+ bool save_envelope:1;
+ bool save_bodystructure_header:1;
+ bool save_bodystructure_body:1;
+ bool save_message_parts:1;
+ bool save_body_snippet:1;
+ bool stream_has_only_header:1;
+ bool parsed_bodystructure:1;
+ bool parsed_bodystructure_header:1;
+ bool hdr_size_set:1;
+ bool body_size_set:1;
+ bool messageparts_saved_to_cache:1;
+ bool header_parsed:1;
+ bool no_caching:1;
+ bool forced_no_caching:1;
+ bool istream_error_logged:1;
+ bool destroying_stream:1;
+ bool initialized_wrapper_stream:1;
+ bool destroy_callback_set:1;
+ bool prefetch_sent:1;
+ bool header_parser_initialized:1;
+ /* virtual_size and physical_size may not match the stream size.
+ Try to avoid trusting them too much. */
+ bool inexact_total_sizes:1;
+};
+
+struct index_mail {
+ struct mail_private mail;
+ struct index_mail_data data;
+ struct index_mailbox_context *ibox;
+
+ int pop3_state;
+
+ /* per-mail variables, here for performance reasons: */
+ uint32_t header_seq;
+ string_t *header_data;
+ ARRAY(struct index_mail_line) header_lines;
+#define HEADER_MATCH_FLAG_FOUND 1
+#define HEADER_MATCH_SKIP_COUNT 2
+#define HEADER_MATCH_USABLE(mail, num) \
+ ((num & ~1U) == (mail)->header_match_value)
+ ARRAY(uint8_t) header_match;
+ ARRAY(unsigned int) header_match_lines;
+ uint8_t header_match_value;
+
+ bool pop3_state_set:1;
+ /* close() is being called from mail_free() */
+ bool freeing:1;
+};
+
+#define INDEX_MAIL(s) container_of(s, struct index_mail, mail.mail)
+
+struct mail *
+index_mail_alloc(struct mailbox_transaction_context *t,
+ enum mail_fetch_field wanted_fields,
+ struct mailbox_header_lookup_ctx *wanted_headers);
+void index_mail_init(struct index_mail *mail,
+ struct mailbox_transaction_context *_t,
+ enum mail_fetch_field wanted_fields,
+ struct mailbox_header_lookup_ctx *_wanted_headers,
+ struct pool *mail_pool,
+ struct pool *data_pool);
+
+void index_mail_set_seq(struct mail *mail, uint32_t seq, bool saving);
+bool index_mail_set_uid(struct mail *mail, uint32_t uid);
+void index_mail_set_uid_cache_updates(struct mail *mail, bool set);
+bool index_mail_prefetch(struct mail *mail);
+void index_mail_add_temp_wanted_fields(struct mail *mail,
+ enum mail_fetch_field fields,
+ struct mailbox_header_lookup_ctx *headers);
+void index_mail_update_access_parts_pre(struct mail *mail);
+void index_mail_update_access_parts_post(struct mail *_mail);
+void index_mail_close(struct mail *mail);
+void index_mail_close_streams(struct index_mail *mail);
+void index_mail_free(struct mail *mail);
+void index_mail_set_message_parts_corrupted(struct mail *mail, const char *error);
+
+bool index_mail_want_parse_headers(struct index_mail *mail);
+void index_mail_parse_header_init(struct index_mail *mail,
+ struct mailbox_header_lookup_ctx *headers)
+ ATTR_NULL(2);
+void index_mail_parse_header(struct message_part *part,
+ struct message_header_line *hdr,
+ struct index_mail *mail) ATTR_NULL(1);
+int index_mail_parse_headers(struct index_mail *mail,
+ struct mailbox_header_lookup_ctx *headers,
+ const char *reason)
+ ATTR_NULL(2);
+void index_mail_parse_header_deinit(struct index_mail *mail);
+/* Same as index_mail_parse_headers(), but assume that the stream is
+ already opened. */
+int index_mail_parse_headers_internal(struct index_mail *mail,
+ struct mailbox_header_lookup_ctx *headers)
+ ATTR_NULL(2);
+int index_mail_headers_get_envelope(struct index_mail *mail);
+void index_mail_parts_reset(struct index_mail *mail);
+
+int index_mail_get_first_header(struct mail *_mail, const char *field,
+ bool decode_to_utf8, const char **value_r);
+int index_mail_get_headers(struct mail *_mail, const char *field,
+ bool decode_to_utf8, const char *const **value_r);
+int index_mail_get_header_stream(struct mail *_mail,
+ struct mailbox_header_lookup_ctx *headers,
+ struct istream **stream_r);
+void index_mail_set_read_buffer_size(struct mail *mail, struct istream *input);
+
+enum mail_flags index_mail_get_flags(struct mail *_mail);
+uint64_t index_mail_get_modseq(struct mail *_mail);
+uint64_t index_mail_get_pvt_modseq(struct mail *_mail);
+const char *const *index_mail_get_keywords(struct mail *_mail);
+const ARRAY_TYPE(keyword_indexes) *
+index_mail_get_keyword_indexes(struct mail *_mail);
+int index_mail_get_parts(struct mail *_mail, struct message_part **parts_r);
+int index_mail_get_received_date(struct mail *_mail, time_t *date_r);
+int index_mail_get_save_date(struct mail *_mail, time_t *date_r);
+int index_mail_get_date(struct mail *_mail, time_t *date_r, int *timezone_r);
+int index_mail_get_virtual_size(struct mail *mail, uoff_t *size_r);
+int index_mail_get_physical_size(struct mail *mail, uoff_t *size_r);
+int index_mail_init_stream(struct index_mail *mail,
+ struct message_size *hdr_size,
+ struct message_size *body_size,
+ struct istream **stream_r) ATTR_NULL(2, 3);
+int index_mail_get_binary_stream(struct mail *_mail,
+ const struct message_part *part,
+ bool include_hdr, uoff_t *size_r,
+ unsigned int *body_lines_r, bool *binary_r,
+ struct istream **stream_r);
+int index_mail_get_special(struct mail *_mail, enum mail_fetch_field field,
+ const char **value_r);
+int index_mail_get_backend_mail(struct mail *mail, struct mail **real_mail_r);
+
+void index_mail_update_flags(struct mail *mail, enum modify_type modify_type,
+ enum mail_flags flags);
+void index_mail_update_keywords(struct mail *mail, enum modify_type modify_type,
+ struct mail_keywords *keywords);
+void index_mail_update_modseq(struct mail *mail, uint64_t min_modseq);
+void index_mail_update_pvt_modseq(struct mail *mail, uint64_t min_pvt_modseq);
+void index_mail_expunge(struct mail *mail);
+int index_mail_precache(struct mail *mail);
+void index_mail_set_cache_corrupted(struct mail *mail,
+ enum mail_fetch_field field,
+ const char *reason);
+int index_mail_opened(struct mail *mail, struct istream **stream);
+int index_mail_stream_check_failure(struct index_mail *mail);
+void index_mail_stream_log_failure_for(struct index_mail *mail,
+ struct istream *input);
+void index_mail_refresh_expunged(struct mail *mail);
+struct index_mail *index_mail_get_index_mail(struct mail *mail);
+
+bool index_mail_get_cached_uoff_t(struct index_mail *mail,
+ enum index_cache_field field, uoff_t *size_r);
+bool index_mail_get_cached_virtual_size(struct index_mail *mail,
+ uoff_t *size_r);
+bool index_mail_get_cached_body(struct index_mail *mail, const char **value_r);
+bool index_mail_get_cached_bodystructure(struct index_mail *mail,
+ const char **value_r);
+const uint32_t *index_mail_get_vsize_extension(struct mail *_mail);
+
+bool index_mail_want_cache(struct index_mail *mail, enum index_cache_field field);
+void index_mail_cache_add(struct index_mail *mail, enum index_cache_field field,
+ const void *data, size_t data_size);
+void index_mail_cache_add_idx(struct index_mail *mail, unsigned int field_idx,
+ const void *data, size_t data_size);
+
+void index_mail_cache_pop3_data(struct mail *_mail,
+ const char *uidl, uint32_t order);
+
+struct istream *index_mail_cache_parse_init(struct mail *mail,
+ struct istream *input);
+void index_mail_cache_parse_continue(struct mail *mail);
+void index_mail_cache_parse_deinit(struct mail *mail, time_t received_date,
+ bool success);
+
+int index_mail_cache_lookup_field(struct index_mail *mail, buffer_t *buf,
+ unsigned int field_idx);
+void index_mail_save_finish(struct mail_save_context *ctx);
+
+const char *index_mail_cache_reason(struct mail *mail, const char *reason);
+
+#endif
diff --git a/src/lib-storage/index/index-mailbox-size.c b/src/lib-storage/index/index-mailbox-size.c
new file mode 100644
index 0000000..35b1081
--- /dev/null
+++ b/src/lib-storage/index/index-mailbox-size.c
@@ -0,0 +1,502 @@
+/* Copyright (c) 2002-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "str.h"
+#include "strescape.h"
+#include "net.h"
+#include "write-full.h"
+#include "mail-search-build.h"
+#include "index-storage.h"
+#include "index-mailbox-size.h"
+
+/*
+ Saving new mails: After transaction is committed and synced, trigger
+ vsize updating. Lock vsize updates. Check if the message count +
+ last-indexed-uid are still valid. If they are, add all the missing new
+ mails. Unlock.
+
+ Fetching vsize: Lock vsize updates. Check if the message count +
+ last-indexed-uid are still valid. If not, set them to zero. Add all
+ the missing mails. Unlock.
+
+ Expunging mails: Check if syncing would expunge any mails. If so, lock the
+ vsize updates before locking syncing (to avoid deadlocks). Check if the
+ message count + last-indexed-uid are still valid. If not, unlock vsize and
+ do nothing else. Otherwise, for each expunged mail whose UID <=
+ last-indexed-uid, decrease the message count and the vsize in memory. After
+ syncing is successfully committed, write the changes to header. Unlock.
+
+ Note that the final expunge handling with some mailbox formats is done while
+ syncing is no longer locked. Because of this we need to have the vsize
+ locking. The final vsize header update requires committing a transaction,
+ which internally is the same as a sync lock. So to avoid deadlocks we always
+ need to lock vsize updates before sync.
+*/
+
+#define VSIZE_LOCK_SUFFIX "dovecot-vsize.lock"
+#define VSIZE_UPDATE_MAX_LOCK_SECS 10
+
+#define INDEXER_SOCKET_NAME "indexer"
+#define INDEXER_HANDSHAKE "VERSION\tindexer\t1\t0\n"
+
+struct mailbox_vsize_update {
+ struct mailbox *box;
+ struct mail_index_view *view;
+ struct mailbox_index_vsize vsize_hdr, orig_vsize_hdr;
+
+ struct file_lock *lock;
+ bool lock_failed;
+ bool skip_write;
+ bool rebuild;
+ bool written;
+ bool finish_in_background;
+};
+
+static void vsize_header_refresh(struct mailbox_vsize_update *update)
+{
+ const void *data;
+ size_t size;
+
+ if (update->view != NULL)
+ mail_index_view_close(&update->view);
+ (void)mail_index_refresh(update->box->index);
+ update->view = mail_index_view_open(update->box->index);
+
+ mail_index_get_header_ext(update->view, update->box->vsize_hdr_ext_id,
+ &data, &size);
+ if (size > 0) {
+ memcpy(&update->orig_vsize_hdr, data,
+ I_MIN(size, sizeof(update->orig_vsize_hdr)));
+ }
+ if (size == sizeof(update->vsize_hdr))
+ memcpy(&update->vsize_hdr, data, sizeof(update->vsize_hdr));
+ else {
+ if (size != 0) {
+ mailbox_set_critical(update->box,
+ "vsize-hdr has invalid size: %zu",
+ size);
+ }
+ update->rebuild = TRUE;
+ i_zero(&update->vsize_hdr);
+ }
+}
+
+static void
+index_mailbox_vsize_check_rebuild(struct mailbox_vsize_update *update)
+{
+ uint32_t seq1, seq2;
+
+ if (update->vsize_hdr.highest_uid == 0)
+ return;
+ if (!mail_index_lookup_seq_range(update->view, 1,
+ update->vsize_hdr.highest_uid,
+ &seq1, &seq2))
+ seq2 = 0;
+
+ if (update->vsize_hdr.message_count != seq2) {
+ if (update->vsize_hdr.message_count < seq2) {
+ mailbox_set_critical(update->box,
+ "vsize-hdr has invalid message-count (%u < %u)",
+ update->vsize_hdr.message_count, seq2);
+ } else {
+ /* some messages have been expunged, rescan */
+ }
+ i_zero(&update->vsize_hdr);
+ update->rebuild = TRUE;
+ }
+}
+
+struct mailbox_vsize_update *
+index_mailbox_vsize_update_init(struct mailbox *box)
+{
+ struct mailbox_vsize_update *update;
+
+ i_assert(box->opened);
+
+ update = i_new(struct mailbox_vsize_update, 1);
+ update->box = box;
+
+ vsize_header_refresh(update);
+ return update;
+}
+
+static bool vsize_update_lock_full(struct mailbox_vsize_update *update,
+ unsigned int lock_secs)
+{
+ struct mailbox *box = update->box;
+ const char *error;
+ int ret;
+
+ if (update->lock != NULL)
+ return TRUE;
+ if (update->lock_failed)
+ return FALSE;
+ if (MAIL_INDEX_IS_IN_MEMORY(box->index))
+ return FALSE;
+
+ ret = mailbox_lock_file_create(box, VSIZE_LOCK_SUFFIX, lock_secs,
+ &update->lock, &error);
+ if (ret <= 0) {
+ /* don't log lock timeouts, because we're somewhat expecting
+ them. Especially when lock_secs is 0. */
+ if (ret < 0)
+ mailbox_set_critical(box, "%s", error);
+ update->lock_failed = TRUE;
+ return FALSE;
+ }
+ update->rebuild = FALSE;
+ vsize_header_refresh(update);
+ index_mailbox_vsize_check_rebuild(update);
+ return TRUE;
+}
+
+bool index_mailbox_vsize_update_try_lock(struct mailbox_vsize_update *update)
+{
+ return vsize_update_lock_full(update, 0);
+}
+
+bool index_mailbox_vsize_update_wait_lock(struct mailbox_vsize_update *update)
+{
+ return vsize_update_lock_full(update, VSIZE_UPDATE_MAX_LOCK_SECS);
+}
+
+bool index_mailbox_vsize_want_updates(struct mailbox_vsize_update *update)
+{
+ return update->vsize_hdr.highest_uid > 0;
+}
+
+static void
+index_mailbox_vsize_update_write_to_index(struct mailbox_vsize_update *update)
+{
+ struct mail_index_transaction *trans;
+
+ trans = mail_index_transaction_begin(update->view,
+ MAIL_INDEX_TRANSACTION_FLAG_EXTERNAL);
+ mail_index_update_header_ext(trans, update->box->vsize_hdr_ext_id,
+ 0, &update->vsize_hdr,
+ sizeof(update->vsize_hdr));
+ (void)mail_index_transaction_commit(&trans);
+}
+
+static void
+index_mailbox_vsize_update_write(struct mailbox_vsize_update *update)
+{
+ if (update->written)
+ return;
+ update->written = TRUE;
+
+ if (update->rebuild == FALSE &&
+ memcmp(&update->orig_vsize_hdr, &update->vsize_hdr,
+ sizeof(update->vsize_hdr)) == 0) {
+ /* no changes */
+ return;
+ }
+ index_mailbox_vsize_update_write_to_index(update);
+}
+
+static void index_mailbox_vsize_notify_indexer(struct mailbox *box)
+{
+ string_t *str = t_str_new(256);
+ const char *path;
+ int fd;
+
+ path = t_strconcat(box->storage->user->set->base_dir,
+ "/"INDEXER_SOCKET_NAME, NULL);
+ fd = net_connect_unix(path);
+ if (fd == -1) {
+ mailbox_set_critical(box,
+ "Can't start vsize building on background: "
+ "net_connect_unix(%s) failed: %m", path);
+ return;
+ }
+ str_append(str, INDEXER_HANDSHAKE);
+ str_append(str, "APPEND\t0\t");
+ str_append_tabescaped(str, box->storage->user->username);
+ str_append_c(str, '\t');
+ str_append_tabescaped(str, box->vname);
+ str_append_c(str, '\n');
+
+ if (write_full(fd, str_data(str), str_len(str)) < 0) {
+ mailbox_set_critical(box,
+ "Can't start vsize building on background: "
+ "write(%s) failed: %m", path);
+ }
+ i_close_fd(&fd);
+}
+
+void index_mailbox_vsize_update_deinit(struct mailbox_vsize_update **_update)
+{
+ struct mailbox_vsize_update *update = *_update;
+
+ *_update = NULL;
+
+ if ((update->lock != NULL || update->rebuild) && !update->skip_write)
+ index_mailbox_vsize_update_write(update);
+ file_lock_free(&update->lock);
+ if (update->finish_in_background)
+ index_mailbox_vsize_notify_indexer(update->box);
+
+ mail_index_view_close(&update->view);
+ i_free(update);
+}
+
+void index_mailbox_vsize_hdr_expunge(struct mailbox_vsize_update *update,
+ uint32_t uid, uoff_t vsize)
+{
+ i_assert(update->lock != NULL);
+
+ if (uid > update->vsize_hdr.highest_uid)
+ return;
+ if (update->vsize_hdr.message_count == 0) {
+ mailbox_set_critical(update->box,
+ "vsize-hdr's message_count shrank below 0");
+ i_zero(&update->vsize_hdr);
+ return;
+ }
+ update->vsize_hdr.message_count--;
+ if (update->vsize_hdr.vsize < vsize) {
+ mailbox_set_critical(update->box,
+ "vsize-hdr's vsize shrank below 0");
+ i_zero(&update->vsize_hdr);
+ return;
+ }
+ update->vsize_hdr.vsize -= vsize;
+}
+
+static void
+index_mailbox_vsize_finish_bg(struct mailbox_vsize_update *update,
+ bool require_result)
+{
+ mail_storage_set_error(update->box->storage, MAIL_ERROR_INUSE,
+ "Finishing vsize calculation on background");
+ if (require_result)
+ update->finish_in_background = TRUE;
+}
+
+static int
+index_mailbox_vsize_hdr_add_missing(struct mailbox_vsize_update *update,
+ bool require_result)
+{
+ struct mailbox_index_vsize *vsize_hdr = &update->vsize_hdr;
+ struct mailbox_transaction_context *trans;
+ struct mail_search_context *search_ctx;
+ struct mail_search_args *search_args;
+ struct mailbox_status status;
+ struct mail *mail;
+ unsigned int idx, mails_left;
+ uint32_t seq1, seq2;
+ uoff_t vsize;
+ int ret = 0;
+
+ mailbox_get_open_status(update->box, STATUS_UIDNEXT, &status);
+ if (vsize_hdr->highest_uid + 1 >= status.uidnext) {
+ /* nothing to do - we should have usually caught this already
+ before locking */
+ return 0;
+ }
+
+ /* note that update->view may be more up-to-date than box->view.
+ we'll just add whatever new mails are in box->view. if we'll notice
+ that some of the new mails are missing, we'll need to stop there
+ since that expunge will be applied later on to the vsize header. */
+ search_args = mail_search_build_init();
+ if (!mail_index_lookup_seq_range(update->box->view,
+ vsize_hdr->highest_uid + 1,
+ status.uidnext-1, &seq1, &seq2)) {
+ /* nothing existed, but update uidnext */
+ vsize_hdr->highest_uid = status.uidnext - 1;
+ mail_search_args_unref(&search_args);
+ return 0;
+ }
+ mail_search_build_add_seqset(search_args, seq1, seq2);
+
+ if (!mail_index_map_get_ext_idx(update->box->view->map,
+ update->box->vsize_hdr_ext_id, &idx)) {
+ /* vsize header doesn't exist yet. Create it here early so
+ that vsize mail records get created (instead of adding
+ size.virtuals to cache). */
+ index_mailbox_vsize_update_write_to_index(update);
+ }
+
+ trans = mailbox_transaction_begin(update->box, 0, "vsize update");
+ search_ctx = mailbox_search_init(trans, search_args, NULL,
+ MAIL_FETCH_VIRTUAL_SIZE, NULL);
+ if (!require_result)
+ mails_left = 0;
+ else if (update->box->storage->set->mail_vsize_bg_after_count == 0)
+ mails_left = UINT_MAX;
+ else
+ mails_left = update->box->storage->set->mail_vsize_bg_after_count;
+
+ while (mailbox_search_next(search_ctx, &mail)) {
+ if (mails_left == 0) {
+ if (mail->mail_stream_accessed) {
+ /* Seems stream is opened by mailbox search, so we
+ will stop here, and finish it on background. */
+ index_mailbox_vsize_finish_bg(update,
+ require_result);
+ ret = -1;
+ break;
+ }
+ /* if there are any more mails whose vsize can't be
+ looked up from cache, abort and finish on
+ background. */
+ mail->lookup_abort = MAIL_LOOKUP_ABORT_NOT_IN_CACHE;
+ }
+ ret = mail_get_virtual_size(mail, &vsize);
+ mail->lookup_abort = MAIL_LOOKUP_ABORT_NEVER;
+
+ if (ret < 0 &&
+ mailbox_get_last_mail_error(update->box) == MAIL_ERROR_LOOKUP_ABORTED) {
+ /* abort and finish on background */
+ i_assert(mails_left == 0);
+ index_mailbox_vsize_finish_bg(update, require_result);
+ break;
+ }
+ if (mail->mail_stream_accessed ||
+ mail->mail_metadata_accessed) {
+ /* slow vsize lookup */
+ i_assert(mails_left > 0);
+ mails_left--;
+ }
+
+ if (ret < 0) {
+ if (mail->expunged)
+ continue;
+ ret = -1;
+ break;
+ }
+ vsize_hdr->vsize += vsize;
+ vsize_hdr->highest_uid = mail->uid;
+ vsize_hdr->message_count++;
+ }
+ if (mailbox_search_deinit(&search_ctx) < 0)
+ ret = -1;
+ mail_search_args_unref(&search_args);
+
+ if (ret == 0) {
+ /* success, cache all */
+ vsize_hdr->highest_uid = status.uidnext - 1;
+ } else {
+ /* search failed, cache only up to highest seen uid */
+ }
+ (void)mailbox_transaction_commit(&trans);
+ return ret;
+}
+
+int index_mailbox_get_virtual_size(struct mailbox *box,
+ struct mailbox_metadata *metadata_r)
+{
+ struct mailbox_vsize_update *update;
+ struct mailbox_status status;
+ int ret;
+
+ mailbox_get_open_status(box, STATUS_MESSAGES | STATUS_UIDNEXT, &status);
+ update = index_mailbox_vsize_update_init(box);
+ if (update->vsize_hdr.highest_uid + 1 == status.uidnext &&
+ update->vsize_hdr.message_count == status.messages) {
+ /* up to date */
+ metadata_r->virtual_size = update->vsize_hdr.vsize;
+ index_mailbox_vsize_update_deinit(&update);
+ return 0;
+ }
+
+ /* we need to update it - lock it if possible. if not, update it
+ anyway internally even though we won't be saving the result. */
+ (void)index_mailbox_vsize_update_wait_lock(update);
+
+ struct event_reason *reason = event_reason_begin("mailbox:vsize");
+ ret = index_mailbox_vsize_hdr_add_missing(update, TRUE);
+ event_reason_end(&reason);
+ metadata_r->virtual_size = update->vsize_hdr.vsize;
+ index_mailbox_vsize_update_deinit(&update);
+ return ret;
+}
+
+int index_mailbox_get_physical_size(struct mailbox *box,
+ struct mailbox_metadata *metadata_r)
+{
+ struct mailbox_transaction_context *trans;
+ struct mail_search_context *ctx;
+ struct mail *mail;
+ struct mail_search_args *search_args;
+ uoff_t size;
+ int ret = 0;
+
+ /* if physical size = virtual size always for the storage, we can
+ use the optimized vsize code for this */
+ if (box->mail_vfuncs->get_physical_size ==
+ box->mail_vfuncs->get_virtual_size) {
+ if (index_mailbox_get_virtual_size(box, metadata_r) < 0)
+ return -1;
+ metadata_r->physical_size = metadata_r->virtual_size;
+ return 0;
+ }
+ /* do it the slow way (we could implement similar logic as for vsize,
+ but for now it's not really needed) */
+ if (mailbox_sync(box, MAILBOX_SYNC_FLAG_FULL_READ) < 0)
+ return -1;
+
+ trans = mailbox_transaction_begin(box, 0, "mailbox physical size");
+
+ search_args = mail_search_build_init();
+ mail_search_build_add_all(search_args);
+ ctx = mailbox_search_init(trans, search_args, NULL,
+ MAIL_FETCH_PHYSICAL_SIZE, NULL);
+ mail_search_args_unref(&search_args);
+
+ metadata_r->physical_size = 0;
+ while (mailbox_search_next(ctx, &mail)) {
+ if (mail_get_physical_size(mail, &size) == 0)
+ metadata_r->physical_size += size;
+ else {
+ const char *errstr;
+ enum mail_error error;
+
+ errstr = mailbox_get_last_internal_error(box, &error);
+ if (error != MAIL_ERROR_EXPUNGED) {
+ i_error("Couldn't get size of mail UID %u in %s: %s",
+ mail->uid, box->vname, errstr);
+ ret = -1;
+ break;
+ }
+ }
+ }
+ if (mailbox_search_deinit(&ctx) < 0) {
+ i_error("Listing mails in %s failed: %s",
+ box->vname, mailbox_get_last_internal_error(box, NULL));
+ ret = -1;
+ }
+ (void)mailbox_transaction_commit(&trans);
+ return ret;
+}
+
+void index_mailbox_vsize_update_appends(struct mailbox *box)
+{
+ struct mailbox_vsize_update *update;
+ struct mailbox_status status;
+
+ update = index_mailbox_vsize_update_init(box);
+ if (update->rebuild) {
+ /* The vsize header doesn't exist. Don't create it. */
+ update->skip_write = TRUE;
+ }
+
+ /* update here only if we don't need to rebuild the whole vsize. */
+ index_mailbox_vsize_check_rebuild(update);
+ if (index_mailbox_vsize_want_updates(update)) {
+ /* Get the UIDNEXT only after checking that vsize updating is
+ even potentially wanted for this mailbox. We especially
+ don't want to do this with imapc, because it could trigger
+ a remote STATUS (UIDNEXT) call. */
+ mailbox_get_open_status(update->box, STATUS_UIDNEXT, &status);
+ if (update->vsize_hdr.highest_uid + 1 != status.uidnext &&
+ index_mailbox_vsize_update_try_lock(update)) {
+ struct event_reason *reason =
+ event_reason_begin("mailbox:vsize");
+ (void)index_mailbox_vsize_hdr_add_missing(update, FALSE);
+ event_reason_end(&reason);
+ }
+ }
+ index_mailbox_vsize_update_deinit(&update);
+}
diff --git a/src/lib-storage/index/index-mailbox-size.h b/src/lib-storage/index/index-mailbox-size.h
new file mode 100644
index 0000000..002da22
--- /dev/null
+++ b/src/lib-storage/index/index-mailbox-size.h
@@ -0,0 +1,20 @@
+#ifndef INDEX_MAILBOX_SIZE_H
+#define INDEX_MAILBOX_SIZE_H
+
+struct mailbox;
+
+struct mailbox_vsize_update *
+index_mailbox_vsize_update_init(struct mailbox *box);
+void index_mailbox_vsize_update_deinit(struct mailbox_vsize_update **update);
+
+void index_mailbox_vsize_hdr_expunge(struct mailbox_vsize_update *update,
+ uint32_t uid, uoff_t vsize);
+
+bool index_mailbox_vsize_update_try_lock(struct mailbox_vsize_update *update);
+bool index_mailbox_vsize_update_wait_lock(struct mailbox_vsize_update *update);
+/* Returns TRUE if expunges & appends should be updating the header. */
+bool index_mailbox_vsize_want_updates(struct mailbox_vsize_update *update);
+
+void index_mailbox_vsize_update_appends(struct mailbox *box);
+
+#endif
diff --git a/src/lib-storage/index/index-pop3-uidl.c b/src/lib-storage/index/index-pop3-uidl.c
new file mode 100644
index 0000000..e537e9f
--- /dev/null
+++ b/src/lib-storage/index/index-pop3-uidl.c
@@ -0,0 +1,104 @@
+/* Copyright (c) 2016-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "index-storage.h"
+#include "index-mail.h"
+#include "index-pop3-uidl.h"
+
+void index_pop3_uidl_set_max_uid(struct mailbox *box,
+ struct mail_index_transaction *trans,
+ uint32_t uid)
+{
+ struct mailbox_index_pop3_uidl uidl;
+
+ i_zero(&uidl);
+ uidl.max_uid_with_pop3_uidl = uid;
+
+ mail_index_update_header_ext(trans, box->pop3_uidl_hdr_ext_id,
+ 0, &uidl, sizeof(uidl));
+}
+
+bool index_pop3_uidl_can_exist(struct mail *mail)
+{
+ struct mailbox_index_pop3_uidl uidl;
+ const void *data;
+ size_t size;
+
+ /* We'll assume that if the header exists, it's up-to-date. normally
+ UIDLs are set only during migration, so this value never changes.
+ Also even if it does, it becomes out-of-date only when the mailbox
+ is modified with old Dovecot versions. To fix that we'd have to
+ add and keep updating "max tracked uid" in this header for every
+ saved mail, which isn't worth it. */
+ mail_index_get_header_ext(mail->transaction->view,
+ mail->box->pop3_uidl_hdr_ext_id,
+ &data, &size);
+ if (size < sizeof(uidl)) {
+ /* this header isn't set yet */
+ return TRUE;
+ }
+ memcpy(&uidl, data, sizeof(uidl));
+ return mail->uid <= uidl.max_uid_with_pop3_uidl;
+}
+
+void index_pop3_uidl_update_exists(struct mail *mail, bool exists)
+{
+ struct mailbox_transaction_context *trans = mail->transaction;
+
+ if (exists) {
+ if (trans->highest_pop3_uidl_uid < mail->uid) {
+ trans->highest_pop3_uidl_uid = mail->uid;
+ trans->prev_pop3_uidl_tracking_seq = mail->seq;
+ }
+ } else if (mail->seq == trans->prev_pop3_uidl_tracking_seq+1) {
+ trans->prev_pop3_uidl_tracking_seq++;
+ } else {
+ /* skipping mails. we don't know the state. */
+ }
+}
+
+void index_pop3_uidl_update_exists_finish(struct mailbox_transaction_context *trans)
+{
+ struct mail_index_view *view;
+ struct mailbox_index_pop3_uidl uidl;
+ const void *data;
+ size_t size;
+ bool seen_all_msgs;
+
+ mail_index_get_header_ext(trans->view, trans->box->pop3_uidl_hdr_ext_id,
+ &data, &size);
+
+ if (trans->highest_pop3_uidl_uid == 0 && size >= sizeof(uidl)) {
+ /* header already set and nothing to change */
+ return;
+ }
+
+ /* First check that we actually looked at UIDL for all messages.
+ Otherwise we can't say for sure if the newest messages had UIDLs. */
+ if (trans->prev_pop3_uidl_tracking_seq !=
+ mail_index_view_get_messages_count(trans->view))
+ return;
+
+ /* Just to be sure: Refresh the index and check again. POP3 keeps
+ transactions open for duration of the entire session. Maybe another
+ process already added new mails (and already updated this header).
+ This check is racy, but normally UIDLs aren't added after migration
+ so it's a bit questionable if it's even worth having this check in
+ there. */
+ view = mail_index_view_open(trans->box->index);
+ seen_all_msgs = mail_index_refresh(trans->box->index) == 0 &&
+ trans->prev_pop3_uidl_tracking_seq ==
+ mail_index_view_get_messages_count(view);
+ mail_index_view_close(&view);
+ if (!seen_all_msgs)
+ return;
+
+ /* check if we have already the same header */
+ if (size >= sizeof(uidl)) {
+ memcpy(&uidl, data, sizeof(uidl));
+ if (trans->highest_pop3_uidl_uid == uidl.max_uid_with_pop3_uidl)
+ return;
+ }
+ index_pop3_uidl_set_max_uid(trans->box, trans->itrans,
+ trans->highest_pop3_uidl_uid);
+}
diff --git a/src/lib-storage/index/index-pop3-uidl.h b/src/lib-storage/index/index-pop3-uidl.h
new file mode 100644
index 0000000..956ab7d
--- /dev/null
+++ b/src/lib-storage/index/index-pop3-uidl.h
@@ -0,0 +1,16 @@
+#ifndef INDEX_POP3_H
+#define INDEX_POP3_H
+
+struct mail_index_transaction;
+struct mail;
+struct mailbox;
+struct mailbox_transaction_context;
+
+void index_pop3_uidl_set_max_uid(struct mailbox *box,
+ struct mail_index_transaction *trans,
+ uint32_t uid);
+bool index_pop3_uidl_can_exist(struct mail *mail);
+void index_pop3_uidl_update_exists(struct mail *mail, bool exists);
+void index_pop3_uidl_update_exists_finish(struct mailbox_transaction_context *trans);
+
+#endif
diff --git a/src/lib-storage/index/index-rebuild.c b/src/lib-storage/index/index-rebuild.c
new file mode 100644
index 0000000..0eaaab5
--- /dev/null
+++ b/src/lib-storage/index/index-rebuild.c
@@ -0,0 +1,257 @@
+/* Copyright (c) 2007-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "mail-cache.h"
+#include "mail-index-modseq.h"
+#include "mailbox-list-private.h"
+#include "mailbox-recent-flags.h"
+#include "index-storage.h"
+#include "index-rebuild.h"
+
+static void
+index_index_copy_vsize(struct index_rebuild_context *ctx,
+ struct mail_index_view *view,
+ uint32_t old_seq, uint32_t new_seq)
+{
+ const void *data;
+ bool expunged;
+
+ mail_index_lookup_ext(view, old_seq, ctx->box->mail_vsize_ext_id,
+ &data, &expunged);
+ if (data != NULL && !expunged) {
+ mail_index_update_ext(ctx->trans, new_seq,
+ ctx->box->mail_vsize_ext_id, data, NULL);
+ }
+}
+
+static void
+index_index_copy_cache(struct index_rebuild_context *ctx,
+ struct mail_index_view *view,
+ uint32_t old_seq, uint32_t new_seq)
+{
+ struct mail_index_map *map;
+ const void *data;
+ uint32_t reset_id = 0;
+ bool expunged;
+
+ if (ctx->cache_ext_id == (uint32_t)-1)
+ return;
+
+ mail_index_lookup_ext_full(view, old_seq, ctx->cache_ext_id,
+ &map, &data, &expunged);
+ if (expunged)
+ return;
+
+ if (!mail_index_ext_get_reset_id(view, map, ctx->cache_ext_id,
+ &reset_id) || reset_id == 0)
+ return;
+
+ if (!ctx->cache_used) {
+ /* set reset id */
+ ctx->cache_used = TRUE;
+ ctx->cache_reset_id = reset_id;
+ mail_index_ext_reset(ctx->trans, ctx->cache_ext_id,
+ ctx->cache_reset_id, TRUE);
+ }
+ if (ctx->cache_reset_id == reset_id) {
+ mail_index_update_ext(ctx->trans, new_seq,
+ ctx->cache_ext_id, data, NULL);
+ }
+}
+
+static void
+index_index_copy_from_old(struct index_rebuild_context *ctx,
+ struct mail_index_view *view,
+ uint32_t old_seq, uint32_t new_seq)
+{
+ struct mail_index *index = mail_index_view_get_index(view);
+ const struct mail_index_record *rec;
+ ARRAY_TYPE(keyword_indexes) old_keywords;
+ struct mail_keywords *kw;
+ uint64_t modseq;
+
+ /* copy flags */
+ rec = mail_index_lookup(view, old_seq);
+ mail_index_update_flags(ctx->trans, new_seq,
+ MODIFY_REPLACE, rec->flags);
+
+ /* copy keywords */
+ t_array_init(&old_keywords, 32);
+ mail_index_lookup_keywords(view, old_seq, &old_keywords);
+ kw = mail_index_keywords_create_from_indexes(index, &old_keywords);
+ mail_index_update_keywords(ctx->trans, new_seq, MODIFY_REPLACE, kw);
+ mail_index_keywords_unref(&kw);
+
+ /* copy modseq */
+ modseq = mail_index_modseq_lookup(view, old_seq);
+ mail_index_update_modseq(ctx->trans, new_seq, modseq);
+
+ index_index_copy_vsize(ctx, view, old_seq, new_seq);
+ index_index_copy_cache(ctx, view, old_seq, new_seq);
+}
+
+void index_rebuild_index_metadata(struct index_rebuild_context *ctx,
+ uint32_t new_seq, uint32_t uid)
+{
+ uint32_t old_seq;
+
+ if (mail_index_lookup_seq(ctx->view, uid, &old_seq)) {
+ /* the message exists in the old index.
+ copy the metadata from it. */
+ index_index_copy_from_old(ctx, ctx->view, old_seq, new_seq);
+ } else if (ctx->backup_view != NULL &&
+ mail_index_lookup_seq(ctx->backup_view, uid, &old_seq)) {
+ /* copy the metadata from backup index. */
+ index_index_copy_from_old(ctx, ctx->backup_view,
+ old_seq, new_seq);
+ }
+}
+
+static void
+index_rebuild_header(struct index_rebuild_context *ctx,
+ index_rebuild_generate_uidvalidity_t *gen_uidvalidity)
+{
+ const struct mail_index_header *hdr, *backup_hdr, *trans_hdr;
+ struct mail_index *index = mail_index_view_get_index(ctx->view);
+ struct mail_index_modseq_header modseq_hdr;
+ struct mail_index_view *trans_view;
+ uint32_t uid_validity, next_uid, first_recent_uid;
+ uint64_t modseq;
+
+ hdr = mail_index_get_header(ctx->view);
+ backup_hdr = ctx->backup_view == NULL ? NULL :
+ mail_index_get_header(ctx->backup_view);
+ trans_view = mail_index_transaction_open_updated_view(ctx->trans);
+ trans_hdr = mail_index_get_header(trans_view);
+
+ /* set uidvalidity */
+ if (hdr->uid_validity != 0)
+ uid_validity = hdr->uid_validity;
+ else if (backup_hdr != NULL && backup_hdr->uid_validity != 0)
+ uid_validity = backup_hdr->uid_validity;
+ else
+ uid_validity = gen_uidvalidity(ctx->box->list);
+ mail_index_update_header(ctx->trans,
+ offsetof(struct mail_index_header, uid_validity),
+ &uid_validity, sizeof(uid_validity), TRUE);
+
+ /* set next-uid */
+ if (hdr->next_uid != 0)
+ next_uid = hdr->next_uid;
+ else if (backup_hdr != NULL && backup_hdr->next_uid != 0)
+ next_uid = backup_hdr->next_uid;
+ else
+ next_uid = 1;
+ if (next_uid > trans_hdr->next_uid) {
+ mail_index_update_header(ctx->trans,
+ offsetof(struct mail_index_header, next_uid),
+ &next_uid, sizeof(next_uid), FALSE);
+ }
+
+ /* set first_recent_uid */
+ first_recent_uid = hdr->first_recent_uid;
+ if (backup_hdr != NULL &&
+ backup_hdr->first_recent_uid > first_recent_uid &&
+ backup_hdr->first_recent_uid <= next_uid)
+ first_recent_uid = backup_hdr->first_recent_uid;
+ first_recent_uid = I_MIN(first_recent_uid, next_uid);
+ mail_index_update_header(ctx->trans,
+ offsetof(struct mail_index_header, first_recent_uid),
+ &first_recent_uid, sizeof(first_recent_uid), FALSE);
+
+ /* set highest-modseq */
+ i_zero(&modseq_hdr);
+ modseq_hdr.highest_modseq = mail_index_modseq_get_highest(ctx->view);
+ if (ctx->backup_view != NULL) {
+ modseq = mail_index_modseq_get_highest(ctx->backup_view);
+ if (modseq_hdr.highest_modseq < modseq)
+ modseq_hdr.highest_modseq = modseq;
+ }
+ mail_index_update_header_ext(ctx->trans, index->modseq_ext_id,
+ 0, &modseq_hdr, sizeof(modseq_hdr));
+ mail_index_view_close(&trans_view);
+}
+
+static void
+index_rebuild_box_preserve_header(struct index_rebuild_context *ctx,
+ uint32_t ext_id)
+{
+ const void *hdr;
+ size_t hdr_size;
+
+ mail_index_get_header_ext(ctx->view, ext_id, &hdr, &hdr_size);
+ if (hdr_size == 0 && ctx->backup_view != NULL) {
+ mail_index_get_header_ext(ctx->backup_view, ext_id,
+ &hdr, &hdr_size);
+ }
+ if (hdr_size == 0)
+ return;
+ mail_index_update_header_ext(ctx->trans, ext_id, 0, hdr, hdr_size);
+}
+
+struct index_rebuild_context *
+index_index_rebuild_init(struct mailbox *box, struct mail_index_view *view,
+ struct mail_index_transaction *trans)
+{
+ struct index_rebuild_context *ctx;
+ const char *index_dir, *backup_path;
+ enum mail_index_open_flags open_flags = MAIL_INDEX_OPEN_FLAG_READONLY;
+
+ /* Rebuilding really should be done locked so multiple processes won't
+ try to rebuild concurrently. Also at the end of rebiuld cache
+ purging requires this lock. */
+ i_assert(mail_index_is_locked(view->index));
+
+ ctx = i_new(struct index_rebuild_context, 1);
+ ctx->box = box;
+ ctx->view = view;
+ ctx->trans = trans;
+ mail_index_reset(ctx->trans);
+ mailbox_recent_flags_reset(box);
+ (void)mail_index_ext_lookup(box->index, "cache", &ctx->cache_ext_id);
+
+ /* open cache and read the caching decisions. */
+ (void)mail_cache_open_and_verify(ctx->box->cache);
+
+ /* if backup index file exists, try to use it */
+ index_dir = mailbox_get_index_path(box);
+ backup_path = t_strconcat(box->index_prefix, ".backup", NULL);
+ ctx->backup_index = mail_index_alloc(box->event,
+ index_dir, backup_path);
+
+#ifndef MMAP_CONFLICTS_WRITE
+ if (box->storage->set->mmap_disable)
+#endif
+ open_flags |= MAIL_INDEX_OPEN_FLAG_MMAP_DISABLE;
+ mail_index_set_lock_method(ctx->backup_index,
+ box->storage->set->parsed_lock_method,
+ UINT_MAX);
+ if (mail_index_open(ctx->backup_index, open_flags) <= 0)
+ mail_index_free(&ctx->backup_index);
+ else
+ ctx->backup_view = mail_index_view_open(ctx->backup_index);
+ return ctx;
+}
+
+void index_index_rebuild_deinit(struct index_rebuild_context **_ctx,
+ index_rebuild_generate_uidvalidity_t *cb)
+{
+ struct index_rebuild_context *ctx = *_ctx;
+
+ *_ctx = NULL;
+
+ /* initialize cache file with the old field decisions */
+ (void)mail_cache_purge_with_trans(ctx->box->cache, ctx->trans,
+ (uint32_t)-1, "rebuilding index");
+ index_rebuild_header(ctx, cb);
+ index_rebuild_box_preserve_header(ctx, ctx->box->box_name_hdr_ext_id);
+ index_rebuild_box_preserve_header(ctx, ctx->box->box_last_rename_stamp_ext_id);
+ index_rebuild_box_preserve_header(ctx, ctx->box->pop3_uidl_hdr_ext_id);
+ if (ctx->backup_index != NULL) {
+ mail_index_view_close(&ctx->backup_view);
+ mail_index_close(ctx->backup_index);
+ mail_index_free(&ctx->backup_index);
+ }
+ i_free(ctx);
+}
diff --git a/src/lib-storage/index/index-rebuild.h b/src/lib-storage/index/index-rebuild.h
new file mode 100644
index 0000000..183da9d
--- /dev/null
+++ b/src/lib-storage/index/index-rebuild.h
@@ -0,0 +1,32 @@
+#ifndef INDEX_REBUILD_H
+#define INDEX_REBUILD_H
+
+struct mailbox_list;
+
+struct index_rebuild_context {
+ struct mailbox *box;
+
+ struct mail_index_view *view;
+ struct mail_index_transaction *trans;
+ uint32_t cache_ext_id;
+ uint32_t cache_reset_id;
+
+ struct mail_index *backup_index;
+ struct mail_index_view *backup_view;
+
+ bool cache_used:1;
+};
+
+typedef unsigned int
+index_rebuild_generate_uidvalidity_t(struct mailbox_list *list);
+
+struct index_rebuild_context *
+index_index_rebuild_init(struct mailbox *box, struct mail_index_view *view,
+ struct mail_index_transaction *trans);
+void index_index_rebuild_deinit(struct index_rebuild_context **ctx,
+ index_rebuild_generate_uidvalidity_t *cb);
+
+void index_rebuild_index_metadata(struct index_rebuild_context *ctx,
+ uint32_t new_seq, uint32_t uid);
+
+#endif
diff --git a/src/lib-storage/index/index-search-mime.c b/src/lib-storage/index/index-search-mime.c
new file mode 100644
index 0000000..da7e5e1
--- /dev/null
+++ b/src/lib-storage/index/index-search-mime.c
@@ -0,0 +1,624 @@
+/* Copyright (c) 2016-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "str.h"
+#include "message-date.h"
+#include "message-address.h"
+#include "message-part-data.h"
+#include "imap-bodystructure.h"
+#include "mail-search.h"
+#include "mail-search-mime.h"
+#include "index-search-private.h"
+
+struct search_mimepart_stack {
+ unsigned int index;
+};
+
+struct search_mimepart_context {
+ pool_t pool;
+ struct index_search_context *index_ctx;
+
+ /* message parts parsed from BODYSTRUCTURE */
+ struct message_part *mime_parts, *mime_part;
+
+ string_t *buf;
+
+ unsigned int depth, index;
+ ARRAY(struct search_mimepart_stack) stack;
+};
+
+static void search_mime_arg(struct mail_search_mime_arg *arg,
+ struct search_mimepart_context *mpctx);
+
+static int seach_arg_mime_parent_match(struct search_mimepart_context *mpctx,
+ struct mail_search_mime_arg *args)
+{
+ struct message_part *part = mpctx->mime_part;
+ unsigned int prev_depth, prev_index;
+ struct search_mimepart_stack *level;
+ int ret;
+
+ if (args->value.subargs == NULL) {
+ /* PARENT EXISTS: matches if this part has a parent.
+ */
+ return (part->parent != NULL ? 1 : 0);
+ }
+
+ /* PARENT <mpart-key>: matches if this part's parent matches the
+ mpart-key (subargs).
+ */
+
+ prev_depth = mpctx->depth;
+ prev_index = mpctx->index;
+
+ level = array_idx_modifiable
+ (&mpctx->stack, mpctx->depth-1);
+
+ mpctx->mime_part = part->parent;
+ mail_search_mime_args_reset(args->value.subargs, TRUE);
+
+ mpctx->index = level->index;
+ mpctx->depth = mpctx->depth-1;
+ ret = mail_search_mime_args_foreach
+ (args->value.subargs, search_mime_arg, mpctx);
+
+ mpctx->mime_part = part;
+ mpctx->index = prev_index;
+ mpctx->depth = prev_depth;
+ return ret;
+}
+
+static int seach_arg_mime_child_match(struct search_mimepart_context *mpctx,
+ struct mail_search_mime_arg *args)
+{
+ struct message_part *part, *prev_part;
+ unsigned int prev_depth, prev_index, depth;
+ struct search_mimepart_stack *level;
+ int ret = 0;
+
+ part = mpctx->mime_part;
+ if (args->value.subargs == NULL) {
+ /* CHILD EXISTS: matches if this part has any children; i.e., it is
+ multipart.
+ */
+ return (part->children != NULL ? 1 : 0);
+ }
+
+ /* CHILD <mpart-key>: matches if this part has any child that mathes
+ the mpart-key (subargs).
+ */
+
+ prev_part = part;
+ prev_depth = mpctx->depth;
+ prev_index = mpctx->index;
+
+ depth = mpctx->depth;
+ T_BEGIN {
+ ARRAY(struct search_mimepart_stack) prev_stack;
+
+ /* preserve current stack for any nested CHILD PARENT nastiness */
+ t_array_init(&prev_stack, 16);
+ array_copy(&prev_stack.arr, 0, &mpctx->stack.arr, 0,
+ array_count(&mpctx->stack));
+
+ depth++;
+ if (depth < array_count(&mpctx->stack))
+ level = array_idx_modifiable(&mpctx->stack, depth);
+ else {
+ i_assert(depth == array_count(&mpctx->stack));
+ level = array_append_space(&mpctx->stack);
+ }
+ level->index = 1;
+
+ part = part->children;
+ while (part != NULL) {
+ mpctx->mime_part = part;
+ mail_search_mime_args_reset(args->value.subargs, TRUE);
+
+ mpctx->depth = depth - prev_depth;
+ mpctx->index = level->index;
+ if ((ret=mail_search_mime_args_foreach
+ (args->value.subargs, search_mime_arg, mpctx)) != 0)
+ break;
+ if (part->children != NULL) {
+ depth++;
+ if (depth < array_count(&mpctx->stack))
+ level = array_idx_modifiable(&mpctx->stack, depth);
+ else {
+ i_assert(depth == array_count(&mpctx->stack));
+ level = array_append_space(&mpctx->stack);
+ }
+ level->index = 1;
+ part = part->children;
+ } else {
+ while (part->next == NULL) {
+ if (part->parent == NULL || part->parent == prev_part)
+ break;
+ depth--;
+ level = array_idx_modifiable(&mpctx->stack, depth);
+ part = part->parent;
+ }
+ level->index++;
+ part = part->next;
+ }
+ }
+
+ array_clear(&mpctx->stack);
+ array_copy(&mpctx->stack.arr, 0, &prev_stack.arr, 0,
+ array_count(&prev_stack));
+ } T_END;
+
+ mpctx->mime_part = prev_part;
+ mpctx->index = prev_index;
+ mpctx->depth = prev_depth;
+ return ret;
+}
+
+static int
+seach_arg_mime_substring_match(
+ struct search_mimepart_context *mpctx ATTR_UNUSED,
+ const char *key, const char *value)
+{
+ if (value == NULL)
+ return 0;
+
+ /* FIXME: Normalization is required */
+ return (strstr(value, key) != NULL ? 1 : 0);
+}
+
+static int
+seach_arg_mime_envelope_time_match(
+ struct search_mimepart_context *mpctx ATTR_UNUSED,
+ enum mail_search_mime_arg_type type, time_t search_time,
+ const struct message_part_envelope *envelope)
+{
+ time_t sent_time;
+ int timezone_offset;
+
+ if (envelope == NULL)
+ return 0;
+
+ /* NOTE: RFC-3501 specifies that timezone is ignored
+ in searches. sent_time is returned as UTC, so change it. */
+ // FIXME: adjust comment
+ if (!message_date_parse((const unsigned char *)envelope->date,
+ strlen(envelope->date), &sent_time, &timezone_offset))
+ return 0;
+ sent_time += timezone_offset * 60;
+
+ switch (type) {
+ case SEARCH_MIME_SENTBEFORE:
+ return sent_time < search_time ? 1 : 0;
+ case SEARCH_MIME_SENTON:
+ return (sent_time >= search_time &&
+ sent_time < search_time + 3600*24) ? 1 : 0;
+ case SEARCH_MIME_SENTSINCE:
+ return sent_time >= search_time ? 1 : 0;
+ default:
+ i_unreached();
+ }
+}
+
+static int
+seach_arg_mime_envelope_address_match(
+ struct search_mimepart_context *mpctx ATTR_UNUSED,
+ enum mail_search_mime_arg_type type, const char *key,
+ const struct message_part_envelope *envelope)
+{
+ const struct message_address *addrs;
+ string_t *addrs_enc;
+
+ if (envelope == NULL)
+ return 0;
+
+ switch (type) {
+ case SEARCH_MIME_CC:
+ addrs = envelope->cc;
+ break;
+ case SEARCH_MIME_BCC:
+ addrs = envelope->bcc;
+ break;
+ case SEARCH_MIME_FROM:
+ addrs = envelope->from;
+ break;
+ case SEARCH_MIME_SENDER:
+ addrs = envelope->sender;
+ break;
+ case SEARCH_MIME_REPLY_TO:
+ addrs = envelope->reply_to;
+ break;
+ case SEARCH_MIME_TO:
+ addrs = envelope->to;
+ break;
+ default:
+ i_unreached();
+ }
+
+ /* FIXME: do we need to normalize anything? at least case insensitivity.
+ MIME header encoding will make this a bit difficult, so it should
+ probably be normalized directly in the struct message_address. */
+
+ addrs_enc = t_str_new(128);
+ message_address_write(addrs_enc, addrs);
+ return (strstr(str_c(addrs_enc), key) != NULL ? 1 : 0);
+}
+
+static int
+seach_arg_mime_filename_match(struct search_mimepart_context *mpctx,
+ struct mail_search_mime_arg *arg)
+{
+ struct index_search_context *ictx = mpctx->index_ctx;
+ struct message_part *part = mpctx->mime_part;
+ char *key;
+ const char *value;
+ size_t vlen, alen;
+
+ if (!message_part_data_get_filename(part, &value))
+ return 0;
+
+ if (mpctx->buf == NULL)
+ mpctx->buf = str_new(default_pool, 256);
+
+ if (arg->context == NULL) {
+ str_truncate(mpctx->buf, 0);
+
+ if (ictx->mail_ctx.normalizer(arg->value.str,
+ strlen(arg->value.str), mpctx->buf) < 0)
+ i_panic("search key not utf8: %s", arg->value.str);
+ key = i_strdup(str_c(mpctx->buf));
+ arg->context = (void *)key;
+ } else {
+ key = (char *)arg->context;
+ }
+
+ str_truncate(mpctx->buf, 0);
+ if (ictx->mail_ctx.normalizer(value,
+ strlen(value), mpctx->buf) >= 0)
+ value = str_c(mpctx->buf);
+
+ switch (arg->type) {
+ case SEARCH_MIME_FILENAME_IS:
+ return (strcmp(value, key) == 0 ? 1 : 0);
+ case SEARCH_MIME_FILENAME_CONTAINS:
+ return (strstr(value, key) != NULL ? 1 : 0);
+ case SEARCH_MIME_FILENAME_BEGINS:
+ return (str_begins(value, key) ? 1 : 0);
+ case SEARCH_MIME_FILENAME_ENDS:
+ vlen = strlen(value);
+ alen = strlen(key);
+ return (str_begins(value + (vlen - alen), key) ? 1 : 0);
+ default:
+ break;
+ }
+ i_unreached();
+}
+static void
+search_arg_mime_filename_deinit(
+ struct search_mimepart_context *mpctx ATTR_UNUSED,
+ struct mail_search_mime_arg *arg)
+{
+ char *key = (char *)arg->context;
+
+ i_free(key);
+}
+
+static int
+seach_arg_mime_param_match(const struct message_part_param *params,
+ unsigned int params_count,
+ const char *name, const char *key)
+{
+ unsigned int i;
+
+ /* FIXME: Is normalization required? */
+
+ for (i = 0; i < params_count; i++) {
+ if (strcasecmp(params[i].name, name) == 0) {
+ if (key == NULL || *key == '\0')
+ return 1;
+ return (strstr(params[i].value, key) != NULL ? 1 : 0);
+ }
+ }
+ return 0;
+}
+
+static int
+seach_arg_mime_language_match(struct search_mimepart_context *mpctx,
+ const char *key)
+{
+ struct message_part_data *data = mpctx->mime_part->data;
+ const char *const *lang;
+
+ i_assert(data != NULL);
+
+ lang = data->content_language;
+ if (lang != NULL) {
+ while (*lang != NULL) {
+ /* FIXME: Should use RFC 4647 matching rules */
+ if (strcasecmp(*lang, key) == 0)
+ return 1;
+ lang++;
+ }
+ }
+ return 0;
+}
+
+/* Returns >0 = matched, 0 = not matched (unused), -1 = unknown */
+static int search_mime_arg_match(struct search_mimepart_context *mpctx,
+ struct mail_search_mime_arg *arg)
+{
+ struct message_part *part = mpctx->mime_part;
+ const struct message_part_data *data = part->data;
+
+ i_assert(data != NULL);
+
+ switch (arg->type) {
+ case SEARCH_MIME_OR:
+ case SEARCH_MIME_SUB:
+ i_unreached();
+
+ case SEARCH_MIME_SIZE_EQUAL:
+ return (part->body_size.virtual_size == arg->value.size ? 1 : 0);
+ case SEARCH_MIME_SIZE_LARGER:
+ return (part->body_size.virtual_size > arg->value.size ? 1 : 0);
+ case SEARCH_MIME_SIZE_SMALLER:
+ return (part->body_size.virtual_size < arg->value.size ? 1 : 0);
+
+ case SEARCH_MIME_DESCRIPTION:
+ return seach_arg_mime_substring_match(mpctx,
+ arg->value.str, data->content_description);
+ case SEARCH_MIME_DISPOSITION_TYPE:
+ return (data->content_disposition != NULL &&
+ strcasecmp(data->content_disposition,
+ arg->value.str) == 0 ? 1 : 0);
+ case SEARCH_MIME_DISPOSITION_PARAM:
+ return seach_arg_mime_param_match
+ (data->content_disposition_params,
+ data->content_disposition_params_count,
+ arg->field_name, arg->value.str);
+ case SEARCH_MIME_ENCODING:
+ return (data->content_transfer_encoding != NULL &&
+ strcasecmp(data->content_transfer_encoding,
+ arg->value.str) == 0 ? 1 : 0);
+ case SEARCH_MIME_ID:
+ return (data->content_id != NULL &&
+ strcasecmp(data->content_id,
+ arg->value.str) == 0 ? 1 : 0);
+ case SEARCH_MIME_LANGUAGE:
+ return seach_arg_mime_language_match(mpctx, arg->value.str);
+ case SEARCH_MIME_LOCATION:
+ return (data->content_location != NULL &&
+ strcasecmp(data->content_location,
+ arg->value.str) == 0 ? 1 : 0);
+ case SEARCH_MIME_MD5:
+ return (data->content_md5 != NULL &&
+ strcmp(data->content_md5,
+ arg->value.str) == 0 ? 1 : 0);
+
+ case SEARCH_MIME_TYPE:
+ return (data->content_type != NULL &&
+ strcasecmp(data->content_type,
+ arg->value.str) == 0 ? 1 : 0);
+ case SEARCH_MIME_SUBTYPE:
+ return (data->content_subtype != NULL &&
+ strcasecmp(data->content_subtype,
+ arg->value.str) == 0 ? 1 : 0);
+ case SEARCH_MIME_PARAM:
+ return seach_arg_mime_param_match
+ (data->content_type_params,
+ data->content_type_params_count,
+ arg->field_name, arg->value.str);
+
+ case SEARCH_MIME_SENTBEFORE:
+ case SEARCH_MIME_SENTON:
+ case SEARCH_MIME_SENTSINCE:
+ return seach_arg_mime_envelope_time_match
+ (mpctx, arg->type, arg->value.time, data->envelope);
+
+ case SEARCH_MIME_CC:
+ case SEARCH_MIME_BCC:
+ case SEARCH_MIME_FROM:
+ case SEARCH_MIME_REPLY_TO:
+ case SEARCH_MIME_SENDER:
+ case SEARCH_MIME_TO:
+ return seach_arg_mime_envelope_address_match
+ (mpctx, arg->type, arg->value.str, data->envelope);
+
+ case SEARCH_MIME_SUBJECT:
+ if (data->envelope == NULL)
+ return 0;
+ return seach_arg_mime_substring_match(mpctx,
+ arg->value.str, data->envelope->subject);
+ case SEARCH_MIME_IN_REPLY_TO:
+ if (data->envelope == NULL)
+ return 0;
+ return seach_arg_mime_substring_match(mpctx,
+ arg->value.str, data->envelope->in_reply_to);
+ case SEARCH_MIME_MESSAGE_ID:
+ if (data->envelope == NULL)
+ return 0;
+ return seach_arg_mime_substring_match(mpctx,
+ arg->value.str, data->envelope->message_id);
+
+ case SEARCH_MIME_DEPTH_EQUAL:
+ return (mpctx->depth == arg->value.number ? 1 : 0);
+ case SEARCH_MIME_DEPTH_MIN:
+ return (mpctx->depth >= arg->value.number ? 1 : 0);
+ case SEARCH_MIME_DEPTH_MAX:
+ return (mpctx->depth <= arg->value.number ? 1 : 0);
+ case SEARCH_MIME_INDEX:
+ return (mpctx->index == arg->value.number ? 1 : 0);
+
+ case SEARCH_MIME_PARENT:
+ return seach_arg_mime_parent_match(mpctx, arg);
+ case SEARCH_MIME_CHILD:
+ return seach_arg_mime_child_match(mpctx, arg);
+
+ case SEARCH_MIME_FILENAME_IS:
+ case SEARCH_MIME_FILENAME_CONTAINS:
+ case SEARCH_MIME_FILENAME_BEGINS:
+ case SEARCH_MIME_FILENAME_ENDS:
+ return seach_arg_mime_filename_match(mpctx, arg);
+
+ case SEARCH_MIME_HEADER:
+ case SEARCH_MIME_BODY:
+ case SEARCH_MIME_TEXT:
+ break;
+ }
+ return -1;
+}
+
+static void search_mime_arg(struct mail_search_mime_arg *arg,
+ struct search_mimepart_context *mpctx)
+{
+ switch (search_mime_arg_match(mpctx, arg)) {
+ case -1:
+ /* unknown */
+ break;
+ case 0:
+ ARG_SET_RESULT(arg, 0);
+ break;
+ default:
+ ARG_SET_RESULT(arg, 1);
+ break;
+ }
+}
+
+static int seach_arg_mime_parts_match(struct search_mimepart_context *mpctx,
+ struct mail_search_mime_arg *args,
+ struct message_part *parts)
+{
+ struct message_part *part;
+ struct search_mimepart_stack *level;
+ int ret;
+
+ level = array_append_space(&mpctx->stack);
+ level->index = 1;
+
+ part = parts;
+ while (part != NULL) {
+ mpctx->mime_part = part;
+ mail_search_mime_args_reset(args, TRUE);
+
+ mpctx->index = level->index;
+ mpctx->depth = array_count(&mpctx->stack)-1;
+
+ if ((ret=mail_search_mime_args_foreach
+ (args, search_mime_arg, mpctx)) != 0)
+ return ret;
+ if (part->children != NULL) {
+ level = array_append_space(&mpctx->stack);
+ level->index = 1;
+ part = part->children;
+ } else {
+ while (part->next == NULL) {
+ if (part->parent == NULL)
+ break;
+ array_pop_back(&mpctx->stack);
+ level = array_back_modifiable(&mpctx->stack);
+ part = part->parent;
+ }
+ level->index++;
+ part = part->next;
+ }
+ }
+
+ return 0;
+}
+
+/* Returns >0 = matched, 0 = not matched, -1 = unknown */
+static int search_arg_match_mimepart(struct search_mimepart_context *mpctx,
+ struct mail_search_arg *arg)
+{
+ struct index_search_context *ctx = mpctx->index_ctx;
+ const char *bodystructure, *error;
+
+ if (arg->type != SEARCH_MIMEPART)
+ return -1;
+
+ if (mpctx->pool == NULL) {
+ mpctx->pool = pool_alloconly_create
+ (MEMPOOL_GROWING"search mime parts", 4096);
+ p_array_init(&mpctx->stack, mpctx->pool, 16);
+ }
+ if (mpctx->mime_parts == NULL) {
+ /* FIXME: could the mail object already have message_part tree with
+ data? */
+ if (mail_get_special(ctx->cur_mail,
+ MAIL_FETCH_IMAP_BODYSTRUCTURE, &bodystructure) < 0)
+ return -1;
+ if (imap_bodystructure_parse_full(bodystructure, mpctx->pool,
+ &mpctx->mime_parts, &error) < 0)
+ return -1;
+ }
+
+ /* FIXME: implement HEADER, BODY and TEXT (not from BODYSTRUCTURE)
+ Needs to support FTS */
+ return seach_arg_mime_parts_match
+ (mpctx, arg->value.mime_part->args, mpctx->mime_parts);
+}
+
+static void search_mimepart_arg(struct mail_search_arg *arg,
+ struct search_mimepart_context *mpctx)
+{
+ switch (search_arg_match_mimepart(mpctx, arg)) {
+ case -1:
+ /* unknown */
+ break;
+ case 0:
+ ARG_SET_RESULT(arg, 0);
+ break;
+ default:
+ ARG_SET_RESULT(arg, 1);
+ break;
+ }
+}
+
+int index_search_mime_arg_match(struct mail_search_arg *args,
+ struct index_search_context *ctx)
+{
+ struct search_mimepart_context mpctx;
+ int ret;
+
+ i_zero(&mpctx);
+ mpctx.index_ctx = ctx;
+
+ ret = mail_search_args_foreach(args,
+ search_mimepart_arg, &mpctx);
+
+ pool_unref(&mpctx.pool);
+ str_free(&mpctx.buf);
+ return ret;
+}
+
+static void
+search_mime_arg_deinit(struct mail_search_mime_arg *arg,
+ struct search_mimepart_context *mpctx ATTR_UNUSED)
+{
+ switch (arg->type) {
+ case SEARCH_MIME_FILENAME_IS:
+ case SEARCH_MIME_FILENAME_CONTAINS:
+ case SEARCH_MIME_FILENAME_BEGINS:
+ case SEARCH_MIME_FILENAME_ENDS:
+ search_arg_mime_filename_deinit(mpctx, arg);
+ break;
+ default:
+ break;
+ }
+}
+
+void index_search_mime_arg_deinit(struct mail_search_arg *arg,
+ struct index_search_context *ctx)
+{
+ struct search_mimepart_context mpctx;
+ struct mail_search_mime_arg *args;
+
+ i_assert(arg->type == SEARCH_MIMEPART);
+ args = arg->value.mime_part->args;
+
+ i_zero(&mpctx);
+ mpctx.index_ctx = ctx;
+
+ mail_search_mime_args_reset(args, TRUE);
+ (void)mail_search_mime_args_foreach(args,
+ search_mime_arg_deinit, &mpctx);
+}
diff --git a/src/lib-storage/index/index-search-private.h b/src/lib-storage/index/index-search-private.h
new file mode 100644
index 0000000..f451b56
--- /dev/null
+++ b/src/lib-storage/index/index-search-private.h
@@ -0,0 +1,45 @@
+#ifndef INDEX_SEARCH_PRIVATE_H
+#define INDEX_SEARCH_PRIVATE_H
+
+#include "mail-storage-private.h"
+
+#include <sys/time.h>
+
+struct mail_search_mime_part;
+struct imap_message_part;
+
+struct index_search_context {
+ struct mail_search_context mail_ctx;
+ struct mail_index_view *view;
+ struct mailbox *box;
+
+ uint32_t pvt_uid, pvt_seq;
+
+ enum mail_fetch_field extra_wanted_fields;
+ struct mailbox_header_lookup_ctx *extra_wanted_headers;
+
+ uint32_t seq1, seq2;
+ struct mail *cur_mail;
+ struct index_mail *cur_imail;
+ struct mail_thread_context *thread_ctx;
+
+ struct timeval search_start_time, last_notify;
+ struct timeval last_nonblock_timeval;
+ unsigned long long cost, next_time_check_cost;
+
+ bool failed:1;
+ bool sorted:1;
+ bool have_seqsets:1;
+ bool have_index_args:1;
+ bool have_mailbox_args:1;
+ bool have_nonmatch_always:1;
+};
+
+struct mail *index_search_get_mail(struct index_search_context *ctx);
+
+int index_search_mime_arg_match(struct mail_search_arg *args,
+ struct index_search_context *ctx);
+void index_search_mime_arg_deinit(struct mail_search_arg *arg,
+ struct index_search_context *ctx);
+
+#endif
diff --git a/src/lib-storage/index/index-search-result.c b/src/lib-storage/index/index-search-result.c
new file mode 100644
index 0000000..6bce84f
--- /dev/null
+++ b/src/lib-storage/index/index-search-result.c
@@ -0,0 +1,194 @@
+/* Copyright (c) 2002-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "seq-range-array.h"
+#include "mail-search.h"
+#include "mailbox-search-result-private.h"
+#include "index-storage.h"
+#include "index-search-result.h"
+
+static void
+search_result_range_remove(struct mail_search_result *result,
+ const ARRAY_TYPE(seq_range) *changed_uids_arr,
+ unsigned int *idx,
+ uint32_t *next_uid, uint32_t last_uid)
+{
+ const struct seq_range *uids;
+ unsigned int i, count;
+ uint32_t uid;
+
+ /* remove full seq_ranges */
+ uid = *next_uid;
+ uids = array_get(changed_uids_arr, &count);
+ for (i = *idx; uids[i].seq2 < last_uid;) {
+ i_assert(uids[i].seq1 <= uid);
+ for (; uid <= uids[i].seq2; uid++)
+ mailbox_search_result_remove(result, uid);
+ i++;
+ i_assert(i < count);
+ uid = uids[i].seq1;
+ }
+
+ /* remove the last seq_range */
+ i_assert(uids[i].seq1 <= uid && uids[i].seq2 >= last_uid);
+ for (; uid <= last_uid; uid++)
+ mailbox_search_result_remove(result, uid);
+
+ if (uid > uids[i].seq2) {
+ /* finished this range */
+ if (++i < count)
+ uid = uids[i].seq1;
+ else {
+ /* this was the last searched message */
+ uid = 0;
+ }
+ }
+
+ *next_uid = uid;
+ *idx = i;
+}
+
+static int
+search_result_update_search(struct mail_search_result *result,
+ const ARRAY_TYPE(seq_range) *changed_uids_arr)
+{
+ struct mailbox_transaction_context *t;
+ struct mail_search_context *search_ctx;
+ struct mail *mail;
+ const struct seq_range *changed_uids;
+ unsigned int changed_count, changed_idx;
+ uint32_t next_uid;
+ int ret;
+
+ changed_uids = array_get(changed_uids_arr, &changed_count);
+ i_assert(changed_count > 0);
+ next_uid = changed_uids[0].seq1;
+ changed_idx = 0;
+
+ mail_search_args_init(result->search_args, result->box, FALSE, NULL);
+
+ t = mailbox_transaction_begin(result->box, 0, __func__);
+ search_ctx = mailbox_search_init(t, result->search_args, NULL, 0, NULL);
+ /* tell search that we're updating an existing search result,
+ so it can do some optimizations based on it */
+ search_ctx->update_result = result;
+
+ while (mailbox_search_next(search_ctx, &mail)) {
+ i_assert(next_uid != 0);
+
+ if (next_uid != mail->uid) {
+ /* some messages in changed_uids didn't match.
+ make sure they don't exist in the search result. */
+ search_result_range_remove(result, changed_uids_arr,
+ &changed_idx, &next_uid,
+ mail->uid-1);
+ i_assert(next_uid == mail->uid);
+ }
+ if (changed_uids[changed_idx].seq2 > next_uid) {
+ next_uid++;
+ } else if (++changed_idx < changed_count) {
+ next_uid = changed_uids[changed_idx].seq1;
+ } else {
+ /* this was the last searched message */
+ next_uid = 0;
+ }
+ /* match - make sure it exists in search result */
+ mailbox_search_result_add(result, mail->uid);
+ }
+ mail_search_args_deinit(result->search_args);
+ ret = mailbox_search_deinit(&search_ctx);
+
+ if (next_uid != 0 && ret == 0) {
+ /* last message(s) didn't match. make sure they don't exist
+ in the search result. */
+ search_result_range_remove(result, changed_uids_arr,
+ &changed_idx, &next_uid,
+ changed_uids[changed_count-1].seq2);
+ }
+
+ if (mailbox_transaction_commit(&t) < 0)
+ ret = -1;
+ return ret;
+}
+
+int index_search_result_update_flags(struct mail_search_result *result,
+ const ARRAY_TYPE(seq_range) *uids)
+{
+ struct mail_search_arg search_arg;
+ int ret;
+
+ if (array_count(uids) == 0)
+ return 0;
+
+ /* add a temporary search parameter to limit the search only to
+ the changed messages */
+ i_zero(&search_arg);
+ search_arg.type = SEARCH_UIDSET;
+ search_arg.value.seqset = *uids;
+ search_arg.next = result->search_args->args;
+ result->search_args->args = &search_arg;
+ ret = search_result_update_search(result, uids);
+ i_assert(result->search_args->args == &search_arg);
+ result->search_args->args = search_arg.next;
+ return ret;
+}
+
+int index_search_result_update_appends(struct mail_search_result *result,
+ unsigned int old_messages_count)
+{
+ struct mailbox_transaction_context *t;
+ struct mail_search_context *search_ctx;
+ struct mail *mail;
+ struct mail_search_arg search_arg;
+ uint32_t message_count;
+ int ret;
+
+ message_count = mail_index_view_get_messages_count(result->box->view);
+ if (old_messages_count == message_count) {
+ /* no new messages */
+ return 0;
+ }
+
+ /* add a temporary search parameter to limit the search only to
+ the new messages */
+ i_zero(&search_arg);
+ search_arg.type = SEARCH_SEQSET;
+ t_array_init(&search_arg.value.seqset, 1);
+ seq_range_array_add_range(&search_arg.value.seqset,
+ old_messages_count + 1, message_count);
+ search_arg.next = result->search_args->args;
+ result->search_args->args = &search_arg;
+
+ /* add all messages matching the search to search result */
+ t = mailbox_transaction_begin(result->box, 0, __func__);
+ search_ctx = mailbox_search_init(t, result->search_args, NULL, 0, NULL);
+
+ while (mailbox_search_next(search_ctx, &mail))
+ mailbox_search_result_add(result, mail->uid);
+
+ ret = mailbox_search_deinit(&search_ctx);
+ if (mailbox_transaction_commit(&t) < 0)
+ ret = -1;
+
+ i_assert(result->search_args->args == &search_arg);
+ result->search_args->args = search_arg.next;
+ return ret;
+}
+
+void index_search_results_update_expunges(struct mailbox *box,
+ const ARRAY_TYPE(seq_range) *expunges)
+{
+ const struct seq_range *seqs;
+ uint32_t seq, uid;
+
+ if (array_count(&box->search_results) == 0)
+ return;
+
+ array_foreach(expunges, seqs) {
+ for (seq = seqs->seq1; seq <= seqs->seq2; seq++) {
+ mail_index_lookup_uid(box->view, seq, &uid);
+ mailbox_search_results_remove(box, uid);
+ }
+ }
+}
diff --git a/src/lib-storage/index/index-search-result.h b/src/lib-storage/index/index-search-result.h
new file mode 100644
index 0000000..a22d368
--- /dev/null
+++ b/src/lib-storage/index/index-search-result.h
@@ -0,0 +1,11 @@
+#ifndef INDEX_SEARCH_RESULT_H
+#define INDEX_SEARCH_RESULT_H
+
+int index_search_result_update_flags(struct mail_search_result *result,
+ const ARRAY_TYPE(seq_range) *uids);
+int index_search_result_update_appends(struct mail_search_result *result,
+ unsigned int old_messages_count);
+void index_search_results_update_expunges(struct mailbox *box,
+ const ARRAY_TYPE(seq_range) *expunges);
+
+#endif
diff --git a/src/lib-storage/index/index-search.c b/src/lib-storage/index/index-search.c
new file mode 100644
index 0000000..dec52f3
--- /dev/null
+++ b/src/lib-storage/index/index-search.c
@@ -0,0 +1,1922 @@
+/* Copyright (c) 2002-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "ioloop.h"
+#include "array.h"
+#include "istream.h"
+#include "utc-offset.h"
+#include "str.h"
+#include "time-util.h"
+#include "unichar.h"
+#include "imap-match.h"
+#include "message-address.h"
+#include "message-date.h"
+#include "message-search.h"
+#include "message-parser.h"
+#include "mail-index-modseq.h"
+#include "index-storage.h"
+#include "index-mail.h"
+#include "index-sort.h"
+#include "mail-search.h"
+#include "mailbox-search-result-private.h"
+#include "mailbox-recent-flags.h"
+#include "index-search-private.h"
+
+#include <ctype.h>
+
+#define SEARCH_NOTIFY_INTERVAL_SECS 10
+
+#define SEARCH_COST_DENTRY 3ULL
+#define SEARCH_COST_ATTR 1ULL
+#define SEARCH_COST_FILES_READ 25ULL
+#define SEARCH_COST_KBYTE 15ULL
+#define SEARCH_COST_CACHE 1ULL
+
+#define SEARCH_MIN_NONBLOCK_USECS 200000
+#define SEARCH_MAX_NONBLOCK_USECS 250000
+#define SEARCH_INITIAL_MAX_COST 30000
+#define SEARCH_RECALC_MIN_USECS 50000
+
+struct search_header_context {
+ struct index_search_context *index_ctx;
+ struct index_mail *imail;
+ struct mail_search_arg *args;
+
+ struct message_block decoded_block;
+ bool decoded_block_set;
+
+ struct message_header_line *hdr;
+
+ bool parse_headers:1;
+ bool custom_header:1;
+ bool threading:1;
+};
+
+struct search_body_context {
+ struct index_search_context *index_ctx;
+ struct istream *input;
+ struct message_part *part;
+};
+
+static void search_parse_msgset_args(unsigned int messages_count,
+ struct mail_search_arg *args,
+ uint32_t *seq1_r, uint32_t *seq2_r);
+
+static void ATTR_NULL(2)
+search_none(struct mail_search_arg *arg ATTR_UNUSED, void *ctx ATTR_UNUSED)
+{
+}
+
+static void search_set_failed(struct index_search_context *ctx)
+{
+ if (ctx->failed)
+ return;
+
+ /* remember the first failure */
+ mail_storage_last_error_push(ctx->box->storage);
+ ctx->failed = TRUE;
+}
+
+static void search_cur_mail_failed(struct index_search_context *ctx)
+{
+ switch (mailbox_get_last_mail_error(ctx->cur_mail->box)) {
+ case MAIL_ERROR_EXPUNGED:
+ ctx->mail_ctx.seen_lost_data = TRUE;
+ break;
+ case MAIL_ERROR_LOOKUP_ABORTED:
+ /* expected failure */
+ break;
+ default:
+ search_set_failed(ctx);
+ break;
+ }
+}
+
+static void search_init_arg(struct mail_search_arg *arg,
+ struct index_search_context *ctx)
+{
+ struct mailbox_metadata metadata;
+ bool match;
+
+ switch (arg->type) {
+ case SEARCH_SEQSET:
+ ctx->have_seqsets = TRUE;
+ break;
+ case SEARCH_UIDSET:
+ case SEARCH_INTHREAD:
+ case SEARCH_FLAGS:
+ case SEARCH_KEYWORDS:
+ case SEARCH_MODSEQ:
+ if (arg->type == SEARCH_MODSEQ)
+ mail_index_modseq_enable(ctx->box->index);
+ ctx->have_index_args = TRUE;
+ break;
+ case SEARCH_MAILBOX_GUID:
+ if (mailbox_get_metadata(ctx->box, MAILBOX_METADATA_GUID,
+ &metadata) < 0) {
+ /* result will be unknown */
+ break;
+ }
+
+ match = strcmp(guid_128_to_string(metadata.guid),
+ arg->value.str) == 0;
+ if (match != arg->match_not)
+ arg->match_always = TRUE;
+ else {
+ arg->nonmatch_always = TRUE;
+ ctx->have_nonmatch_always = TRUE;
+ }
+ break;
+ case SEARCH_MAILBOX:
+ case SEARCH_MAILBOX_GLOB:
+ ctx->have_mailbox_args = TRUE;
+ break;
+ case SEARCH_ALL:
+ if (!arg->match_not)
+ arg->match_always = TRUE;
+ else {
+ arg->nonmatch_always = TRUE;
+ ctx->have_nonmatch_always = TRUE;
+ }
+ break;
+ default:
+ break;
+ }
+}
+
+static void search_seqset_arg(struct mail_search_arg *arg,
+ struct index_search_context *ctx)
+{
+ if (arg->type == SEARCH_SEQSET) {
+ if (seq_range_exists(&arg->value.seqset, ctx->mail_ctx.seq))
+ ARG_SET_RESULT(arg, 1);
+ else
+ ARG_SET_RESULT(arg, 0);
+ }
+}
+
+static int search_arg_match_keywords(struct index_search_context *ctx,
+ struct mail_search_arg *arg)
+{
+ ARRAY_TYPE(keyword_indexes) keyword_indexes_arr;
+ const struct mail_keywords *search_kws = arg->initialized.keywords;
+ const unsigned int *keyword_indexes;
+ unsigned int i, j, count;
+
+ if (search_kws->count == 0) {
+ /* invalid keyword - never matches */
+ return 0;
+ }
+
+ t_array_init(&keyword_indexes_arr, 128);
+ mail_index_lookup_keywords(ctx->view, ctx->mail_ctx.seq,
+ &keyword_indexes_arr);
+ keyword_indexes = array_get(&keyword_indexes_arr, &count);
+
+ /* there probably aren't many keywords, so O(n*m) for now */
+ for (i = 0; i < search_kws->count; i++) {
+ for (j = 0; j < count; j++) {
+ if (search_kws->idx[i] == keyword_indexes[j])
+ break;
+ }
+ if (j == count)
+ return 0;
+ }
+ return 1;
+}
+
+static bool
+index_search_get_pvt(struct index_search_context *ctx, uint32_t uid)
+{
+ index_transaction_init_pvt(ctx->mail_ctx.transaction);
+
+ if (ctx->pvt_uid == uid)
+ return ctx->pvt_seq != 0;
+ ctx->pvt_uid = uid;
+ return mail_index_lookup_seq(ctx->mail_ctx.transaction->view_pvt,
+ uid, &ctx->pvt_seq);
+}
+
+/* Returns >0 = matched, 0 = not matched, -1 = unknown */
+static int search_arg_match_index(struct index_search_context *ctx,
+ struct mail_search_arg *arg,
+ const struct mail_index_record *rec)
+{
+ enum mail_flags flags, pvt_flags_mask;
+ uint64_t modseq;
+ int ret;
+
+ switch (arg->type) {
+ case SEARCH_UIDSET:
+ case SEARCH_INTHREAD:
+ return seq_range_exists(&arg->value.seqset, rec->uid) ? 1 : 0;
+ case SEARCH_FLAGS:
+ /* recent flag shouldn't be set, but indexes from v1.0.x
+ may contain it. */
+ flags = rec->flags & ENUM_NEGATE(MAIL_RECENT);
+ if ((arg->value.flags & MAIL_RECENT) != 0 &&
+ mailbox_recent_flags_have_uid(ctx->box, rec->uid))
+ flags |= MAIL_RECENT;
+ if (ctx->box->view_pvt == NULL) {
+ /* no private view (set by view syncing) ->
+ no private flags */
+ } else {
+ pvt_flags_mask = mailbox_get_private_flags_mask(ctx->box);
+ flags &= ENUM_NEGATE(pvt_flags_mask);
+ if (index_search_get_pvt(ctx, rec->uid)) {
+ rec = mail_index_lookup(ctx->mail_ctx.transaction->view_pvt,
+ ctx->pvt_seq);
+ flags |= rec->flags & pvt_flags_mask;
+ }
+ }
+ return (flags & arg->value.flags) == arg->value.flags ? 1 : 0;
+ case SEARCH_KEYWORDS:
+ T_BEGIN {
+ ret = search_arg_match_keywords(ctx, arg);
+ } T_END;
+ return ret;
+ case SEARCH_MODSEQ: {
+ if (arg->value.flags != 0) {
+ modseq = mail_index_modseq_lookup_flags(ctx->view,
+ arg->value.flags, ctx->mail_ctx.seq);
+ } else if (arg->initialized.keywords != NULL) {
+ modseq = mail_index_modseq_lookup_keywords(ctx->view,
+ arg->initialized.keywords, ctx->mail_ctx.seq);
+ } else {
+ modseq = mail_index_modseq_lookup(ctx->view,
+ ctx->mail_ctx.seq);
+ }
+ return modseq >= arg->value.modseq->modseq ? 1 : 0;
+ }
+ default:
+ return -1;
+ }
+}
+
+static void search_index_arg(struct mail_search_arg *arg,
+ struct index_search_context *ctx)
+{
+ const struct mail_index_record *rec;
+
+ rec = mail_index_lookup(ctx->view, ctx->mail_ctx.seq);
+ switch (search_arg_match_index(ctx, arg, rec)) {
+ case -1:
+ /* unknown */
+ break;
+ case 0:
+ ARG_SET_RESULT(arg, 0);
+ break;
+ default:
+ ARG_SET_RESULT(arg, 1);
+ break;
+ }
+}
+
+/* Returns >0 = matched, 0 = not matched, -1 = unknown */
+static int search_arg_match_mailbox(struct index_search_context *ctx,
+ struct mail_search_arg *arg)
+{
+ struct mailbox *box = ctx->cur_mail->box;
+ const char *str;
+
+ switch (arg->type) {
+ case SEARCH_MAILBOX:
+ /* first try to match the mailbox name itself. this is
+ important when using "mailbox virtual/foo" parameter foin
+ doveadm's search query, otherwise we can never fetch
+ anything with doveadm from virtual mailboxes because the
+ mailbox parameter is compared to the mail's backend
+ mailbox. */
+ if (strcmp(box->vname, arg->value.str) == 0)
+ return 1;
+ if (mail_get_special(ctx->cur_mail, MAIL_FETCH_MAILBOX_NAME,
+ &str) < 0) {
+ search_cur_mail_failed(ctx);
+ return -1;
+ }
+
+ if (strcasecmp(str, "INBOX") == 0)
+ return strcasecmp(arg->value.str, "INBOX") == 0 ? 1 : 0;
+ return strcmp(str, arg->value.str) == 0 ? 1 : 0;
+ case SEARCH_MAILBOX_GLOB:
+ if (imap_match(arg->initialized.mailbox_glob, box->vname) == IMAP_MATCH_YES)
+ return 1;
+ if (mail_get_special(ctx->cur_mail, MAIL_FETCH_MAILBOX_NAME,
+ &str) < 0) {
+ search_cur_mail_failed(ctx);
+ return -1;
+ }
+ return imap_match(arg->initialized.mailbox_glob, str) == IMAP_MATCH_YES ? 1 : 0;
+ default:
+ return -1;
+ }
+}
+
+static void search_mailbox_arg(struct mail_search_arg *arg,
+ struct index_search_context *ctx)
+{
+ switch (search_arg_match_mailbox(ctx, arg)) {
+ case -1:
+ /* unknown */
+ break;
+ case 0:
+ ARG_SET_RESULT(arg, 0);
+ break;
+ default:
+ ARG_SET_RESULT(arg, 1);
+ break;
+ }
+}
+
+/* Returns >0 = matched, 0 = not matched, -1 = unknown */
+static int search_arg_match_cached(struct index_search_context *ctx,
+ struct mail_search_arg *arg)
+{
+ const char *str;
+ struct tm *tm;
+ uoff_t virtual_size;
+ time_t date;
+ int tz_offset;
+ bool have_tz_offset;
+ int ret;
+
+ switch (arg->type) {
+ /* internal dates */
+ case SEARCH_BEFORE:
+ case SEARCH_ON:
+ case SEARCH_SINCE:
+ have_tz_offset = FALSE; tz_offset = 0; date = (time_t)-1;
+ switch (arg->value.date_type) {
+ case MAIL_SEARCH_DATE_TYPE_SENT:
+ if (mail_get_date(ctx->cur_mail, &date, &tz_offset) < 0) {
+ search_cur_mail_failed(ctx);
+ return -1;
+ }
+ have_tz_offset = TRUE;
+ break;
+ case MAIL_SEARCH_DATE_TYPE_RECEIVED:
+ if (mail_get_received_date(ctx->cur_mail, &date) < 0) {
+ search_cur_mail_failed(ctx);
+ return -1;
+ }
+ break;
+ case MAIL_SEARCH_DATE_TYPE_SAVED:
+ if (mail_get_save_date(ctx->cur_mail, &date) < 0) {
+ search_cur_mail_failed(ctx);
+ return -1;
+ }
+ break;
+ }
+
+ if ((arg->value.search_flags &
+ MAIL_SEARCH_ARG_FLAG_UTC_TIMES) == 0) {
+ if (!have_tz_offset) {
+ tm = localtime(&date);
+ tz_offset = utc_offset(tm, date);
+ }
+ date += tz_offset * 60;
+ }
+
+ switch (arg->type) {
+ case SEARCH_BEFORE:
+ return date < arg->value.time ? 1 : 0;
+ case SEARCH_ON:
+ return (date >= arg->value.time &&
+ date < arg->value.time + 3600*24) ? 1 : 0;
+ case SEARCH_SINCE:
+ return date >= arg->value.time ? 1 : 0;
+ default:
+ i_unreached();
+ }
+
+ /* save date attribute */
+ case SEARCH_SAVEDATESUPPORTED:
+ ret = mail_get_save_date(ctx->cur_mail, &date);
+ if (ret < 0) {
+ search_cur_mail_failed(ctx);
+ return -1;
+ }
+ return ret;
+
+ /* sizes */
+ case SEARCH_SMALLER:
+ case SEARCH_LARGER:
+ if (mail_get_virtual_size(ctx->cur_mail, &virtual_size) < 0) {
+ search_cur_mail_failed(ctx);
+ return -1;
+ }
+
+ if (arg->type == SEARCH_SMALLER)
+ return virtual_size < arg->value.size ? 1 : 0;
+ else
+ return virtual_size > arg->value.size ? 1 : 0;
+
+ case SEARCH_GUID:
+ if (mail_get_special(ctx->cur_mail, MAIL_FETCH_GUID, &str) < 0) {
+ search_cur_mail_failed(ctx);
+ return -1;
+ }
+ return strcmp(str, arg->value.str) == 0 ? 1 : 0;
+ case SEARCH_REAL_UID: {
+ struct mail *real_mail;
+
+ if (mail_get_backend_mail(ctx->cur_mail, &real_mail) < 0) {
+ search_cur_mail_failed(ctx);
+ return -1;
+ }
+ return seq_range_exists(&arg->value.seqset, real_mail->uid) ? 1 : 0;
+ }
+ default:
+ return -1;
+ }
+}
+
+static void search_cached_arg(struct mail_search_arg *arg,
+ struct index_search_context *ctx)
+{
+ switch (search_arg_match_cached(ctx, arg)) {
+ case -1:
+ /* unknown */
+ break;
+ case 0:
+ ARG_SET_RESULT(arg, 0);
+ break;
+ default:
+ ARG_SET_RESULT(arg, 1);
+ break;
+ }
+}
+
+static int search_sent(enum mail_search_arg_type type, time_t search_time,
+ const unsigned char *sent_value, size_t sent_value_len)
+{
+ time_t sent_time;
+ int timezone_offset;
+
+ if (sent_value == NULL)
+ return 0;
+
+ /* NOTE: RFC-3501 specifies that timezone is ignored
+ in searches. sent_time is returned as UTC, so change it. */
+ if (!message_date_parse(sent_value, sent_value_len,
+ &sent_time, &timezone_offset))
+ return 0;
+ sent_time += timezone_offset * 60;
+
+ switch (type) {
+ case SEARCH_BEFORE:
+ return sent_time < search_time ? 1 : 0;
+ case SEARCH_ON:
+ return (sent_time >= search_time &&
+ sent_time < search_time + 3600*24) ? 1 : 0;
+ case SEARCH_SINCE:
+ return sent_time >= search_time ? 1 : 0;
+ default:
+ i_unreached();
+ }
+}
+
+static struct message_search_context *
+msg_search_arg_context(struct index_search_context *ctx,
+ struct mail_search_arg *arg)
+{
+ enum message_search_flags flags = 0;
+
+ if (arg->context == NULL) T_BEGIN {
+ string_t *dtc = t_str_new(128);
+
+ if (ctx->mail_ctx.normalizer(arg->value.str,
+ strlen(arg->value.str), dtc) < 0)
+ i_panic("search key not utf8: %s", arg->value.str);
+
+ if (arg->type == SEARCH_BODY)
+ flags |= MESSAGE_SEARCH_FLAG_SKIP_HEADERS;
+ /* we don't get here if arg is "", but dtc can be "" if it
+ only contains characters that we need to ignore. handle
+ those searches by returning them as non-matched. */
+ if (str_len(dtc) > 0) {
+ arg->context =
+ message_search_init(str_c(dtc),
+ ctx->mail_ctx.normalizer,
+ flags);
+ }
+ } T_END;
+ return arg->context;
+}
+
+static void compress_lwsp(string_t *dest, const unsigned char *src,
+ size_t src_len)
+{
+ size_t i;
+ bool prev_lwsp = TRUE;
+
+ for (i = 0; i < src_len; i++) {
+ if (IS_LWSP(src[i])) {
+ if (!prev_lwsp) {
+ prev_lwsp = TRUE;
+ str_append_c(dest, ' ');
+ }
+ } else {
+ prev_lwsp = FALSE;
+ str_append_c(dest, src[i]);
+ }
+ }
+}
+
+static void search_header_arg(struct mail_search_arg *arg,
+ struct search_header_context *ctx)
+{
+ struct message_search_context *msg_search_ctx;
+ struct message_block block;
+ struct message_header_line hdr;
+ int ret;
+
+ /* first check that the field name matches to argument. */
+ switch (arg->type) {
+ case SEARCH_BEFORE:
+ case SEARCH_ON:
+ case SEARCH_SINCE:
+ if (arg->value.date_type != MAIL_SEARCH_DATE_TYPE_SENT)
+ return;
+
+ /* date is handled differently than others */
+ if (strcasecmp(ctx->hdr->name, "Date") == 0) {
+ if (ctx->hdr->continues) {
+ ctx->hdr->use_full_value = TRUE;
+ return;
+ }
+ ret = search_sent(arg->type, arg->value.time,
+ ctx->hdr->full_value,
+ ctx->hdr->full_value_len);
+ ARG_SET_RESULT(arg, ret);
+ }
+ return;
+
+ case SEARCH_HEADER:
+ case SEARCH_HEADER_ADDRESS:
+ case SEARCH_HEADER_COMPRESS_LWSP:
+ ctx->custom_header = TRUE;
+
+ if (strcasecmp(ctx->hdr->name, arg->hdr_field_name) != 0)
+ return;
+ break;
+ default:
+ return;
+ }
+
+ if (arg->value.str[0] == '\0') {
+ /* we're just testing existence of the field. always matches. */
+ ARG_SET_RESULT(arg, 1);
+ return;
+ }
+
+ if (ctx->hdr->continues) {
+ ctx->hdr->use_full_value = TRUE;
+ return;
+ }
+
+ i_zero(&block);
+
+ /* We're searching only for values, so drop header name and middle
+ parts. We use header searching so that MIME words will be decoded. */
+ hdr = *ctx->hdr;
+ hdr.name = ""; hdr.name_len = 0;
+ hdr.middle_len = 0;
+ block.hdr = &hdr;
+
+ msg_search_ctx = msg_search_arg_context(ctx->index_ctx, arg);
+ if (msg_search_ctx == NULL)
+ return;
+
+ if (!ctx->decoded_block_set) { T_BEGIN {
+ struct message_address *addr;
+ string_t *str;
+
+ switch (arg->type) {
+ case SEARCH_HEADER:
+ /* simple match */
+ break;
+ case SEARCH_HEADER_ADDRESS:
+ /* we have to match against normalized address */
+ addr = message_address_parse(pool_datastack_create(),
+ ctx->hdr->full_value,
+ ctx->hdr->full_value_len,
+ UINT_MAX,
+ MESSAGE_ADDRESS_PARSE_FLAG_FILL_MISSING);
+ str = t_str_new(ctx->hdr->value_len);
+ message_address_write(str, addr);
+ hdr.value = hdr.full_value = str_data(str);
+ hdr.value_len = hdr.full_value_len = str_len(str);
+ break;
+ case SEARCH_HEADER_COMPRESS_LWSP:
+ /* convert LWSP to single spaces */
+ str = t_str_new(hdr.full_value_len);
+ compress_lwsp(str, hdr.full_value, hdr.full_value_len);
+ hdr.value = hdr.full_value = str_data(str);
+ hdr.value_len = hdr.full_value_len = str_len(str);
+ break;
+ default:
+ i_unreached();
+ }
+ ret = message_search_more_get_decoded(msg_search_ctx, &block,
+ &ctx->decoded_block) ? 1 : 0;
+ ctx->decoded_block_set = TRUE;
+ } T_END; } else {
+ /* this block was already decoded and saved by an earlier
+ search arg. use the already-decoded block to avoid
+ duplicating work. */
+ ret = message_search_more_decoded(msg_search_ctx,
+ &ctx->decoded_block) ? 1 : 0;
+ }
+
+ /* there may be multiple headers. don't mark this failed yet. */
+ if (ret > 0)
+ ARG_SET_RESULT(arg, 1);
+}
+
+static void search_header_unmatch(struct mail_search_arg *arg,
+ struct search_header_context *ctx ATTR_UNUSED)
+{
+ switch (arg->type) {
+ case SEARCH_BEFORE:
+ case SEARCH_ON:
+ case SEARCH_SINCE:
+ if (arg->value.date_type != MAIL_SEARCH_DATE_TYPE_SENT)
+ break;
+
+ if (arg->match_not) {
+ /* date header not found, so we match only for
+ NOT searches */
+ ARG_SET_RESULT(arg, 0);
+ }
+ break;
+ case SEARCH_HEADER:
+ case SEARCH_HEADER_ADDRESS:
+ case SEARCH_HEADER_COMPRESS_LWSP:
+ ARG_SET_RESULT(arg, 0);
+ break;
+ default:
+ break;
+ }
+}
+
+static void search_header(struct message_header_line *hdr,
+ struct search_header_context *ctx)
+{
+ if (ctx->parse_headers)
+ index_mail_parse_header(NULL, hdr, ctx->imail);
+
+ if (hdr == NULL) {
+ /* end of headers, mark all unknown SEARCH_HEADERs unmatched */
+ (void)mail_search_args_foreach(ctx->args, search_header_unmatch,
+ ctx);
+ return;
+ }
+
+ if (hdr->eoh)
+ return;
+
+ if (ctx->custom_header || strcasecmp(hdr->name, "Date") == 0) {
+ ctx->hdr = hdr;
+
+ ctx->decoded_block_set = FALSE;
+ ctx->custom_header = FALSE;
+ (void)mail_search_args_foreach(ctx->args, search_header_arg, ctx);
+ }
+}
+
+static void search_body(struct mail_search_arg *arg,
+ struct search_body_context *ctx)
+{
+ struct message_search_context *msg_search_ctx;
+ const char *error;
+ int ret;
+
+ switch (arg->type) {
+ case SEARCH_BODY:
+ case SEARCH_TEXT:
+ break;
+ default:
+ return;
+ }
+
+ msg_search_ctx = msg_search_arg_context(ctx->index_ctx, arg);
+ if (msg_search_ctx == NULL) {
+ ARG_SET_RESULT(arg, 0);
+ return;
+ }
+
+ i_stream_seek(ctx->input, 0);
+ ret = message_search_msg(msg_search_ctx, ctx->input, ctx->part, &error);
+ if (ret < 0 && ctx->input->stream_errno == 0) {
+ /* try again without cached parts */
+ index_mail_set_message_parts_corrupted(ctx->index_ctx->cur_mail, error);
+
+ i_stream_seek(ctx->input, 0);
+ ret = message_search_msg(msg_search_ctx, ctx->input, NULL, &error);
+ i_assert(ret >= 0 || ctx->input->stream_errno != 0);
+ }
+ if (ctx->input->stream_errno != 0) {
+ mailbox_set_critical(ctx->index_ctx->box,
+ "read(%s) failed: %s", i_stream_get_name(ctx->input),
+ i_stream_get_error(ctx->input));
+ }
+
+ ARG_SET_RESULT(arg, ret);
+}
+
+static int search_arg_match_text(struct mail_search_arg *args,
+ struct index_search_context *ctx)
+{
+ const enum message_header_parser_flags hdr_parser_flags =
+ MESSAGE_HEADER_PARSER_FLAG_CLEAN_ONELINE;
+ struct index_mail *imail = INDEX_MAIL(ctx->cur_mail);
+ struct mail *real_mail;
+ struct istream *input = NULL;
+ struct mailbox_header_lookup_ctx *headers_ctx;
+ struct search_header_context hdr_ctx;
+ struct search_body_context body_ctx;
+ const char *const *headers;
+ bool have_headers, have_body, failed = FALSE;
+ int ret;
+
+ /* first check what we need to use */
+ headers = mail_search_args_analyze(args, &have_headers, &have_body);
+ if (!have_headers && !have_body)
+ return -1;
+
+ i_zero(&hdr_ctx);
+ hdr_ctx.index_ctx = ctx;
+ /* hdr_ctx.imail is different from imail for mails in
+ virtual mailboxes */
+ if (mail_get_backend_mail(ctx->cur_mail, &real_mail) < 0) {
+ search_cur_mail_failed(ctx);
+ return -1;
+ }
+ hdr_ctx.imail = INDEX_MAIL(real_mail);
+ hdr_ctx.custom_header = TRUE;
+ hdr_ctx.args = args;
+
+ headers_ctx = headers == NULL ? NULL :
+ mailbox_header_lookup_init(ctx->box, headers);
+ if (headers != NULL &&
+ (!have_body ||
+ ctx->cur_mail->lookup_abort != MAIL_LOOKUP_ABORT_NEVER)) {
+ /* try to look up the specified headers from cache */
+ i_assert(*headers != NULL);
+
+ if (mail_get_header_stream(ctx->cur_mail, headers_ctx,
+ &input) < 0) {
+ search_cur_mail_failed(ctx);
+ failed = TRUE;
+ } else {
+ message_parse_header(input, NULL, hdr_parser_flags,
+ search_header, &hdr_ctx);
+ }
+ input = NULL;
+ } else if (have_headers) {
+ /* we need to read the entire header */
+ ret = have_body ?
+ mail_get_stream_because(ctx->cur_mail, NULL, NULL, "search", &input) :
+ mail_get_hdr_stream_because(ctx->cur_mail, NULL, "search", &input);
+ if (ret < 0) {
+ search_cur_mail_failed(ctx);
+ failed = TRUE;
+ } else {
+ /* FIXME: The header parsing here is an optimization to
+ avoid parsing the header twice: First when checking
+ whether the search matches, and secondly when
+ generating wanted fields. However, if we already
+ know that we want to generate a BODYSTRUCTURE reply,
+ index_mail_parse_header() must have a non-NULL part
+ parameter. That's not easily possible at this point
+ without larger code changes, so for now we'll just
+ disable this optimization for that case. */
+ hdr_ctx.parse_headers =
+ !hdr_ctx.imail->data.save_bodystructure_header &&
+ index_mail_want_parse_headers(hdr_ctx.imail);
+ if (hdr_ctx.parse_headers) {
+ index_mail_parse_header_init(hdr_ctx.imail,
+ headers_ctx);
+ }
+ message_parse_header(input, NULL, hdr_parser_flags,
+ search_header, &hdr_ctx);
+ if (input->stream_errno != 0) {
+ mailbox_set_critical(ctx->box,
+ "read(%s) failed: %s",
+ i_stream_get_name(input),
+ i_stream_get_error(input));
+ failed = TRUE;
+ search_set_failed(ctx);
+ }
+ }
+ }
+ mailbox_header_lookup_unref(&headers_ctx);
+
+ if (failed) {
+ /* opening mail failed. maybe because of lookup_abort.
+ update access_parts for prefetching */
+ if (have_body)
+ imail->data.access_part |= READ_HDR | READ_BODY;
+ else
+ imail->data.access_part |= READ_HDR;
+ return -1;
+ }
+
+ if (have_headers) {
+ /* see if the header search succeeded in finishing the search */
+ ret = mail_search_args_foreach(args, search_none, NULL);
+ if (ret >= 0 || !have_body)
+ return ret;
+ }
+
+ i_assert(have_body);
+
+ if (ctx->cur_mail->lookup_abort != MAIL_LOOKUP_ABORT_NEVER) {
+ imail->data.access_part |= READ_HDR | READ_BODY;
+ return -1;
+ }
+
+ if (input == NULL) {
+ /* we didn't search headers. */
+ struct message_size hdr_size;
+
+ if (mail_get_stream_because(ctx->cur_mail, &hdr_size, NULL, "search", &input) < 0) {
+ search_cur_mail_failed(ctx);
+ return -1;
+ }
+ i_stream_seek(input, hdr_size.physical_size);
+ }
+
+ i_zero(&body_ctx);
+ body_ctx.index_ctx = ctx;
+ body_ctx.input = input;
+ /* Get parts if they already exist in cache. If they don't,
+ message-search will parse the mail automatically. */
+ ctx->cur_mail->lookup_abort = MAIL_LOOKUP_ABORT_NOT_IN_CACHE;
+ (void)mail_get_parts(ctx->cur_mail, &body_ctx.part);
+ ctx->cur_mail->lookup_abort = MAIL_LOOKUP_ABORT_NEVER;
+
+ return mail_search_args_foreach(args, search_body, &body_ctx);
+}
+
+static bool
+search_msgset_fix_limits(unsigned int messages_count,
+ ARRAY_TYPE(seq_range) *seqset, bool match_not)
+{
+ struct seq_range *range;
+ unsigned int count;
+
+ i_assert(messages_count > 0);
+
+ range = array_get_modifiable(seqset, &count);
+ if (count > 0) {
+ i_assert(range[0].seq1 != 0);
+ if (range[count-1].seq2 == (uint32_t)-1) {
+ /* "*" used, make sure the last message is in the range
+ (e.g. with count+1:* we still want to include it) */
+ seq_range_array_add(seqset, messages_count);
+ }
+ /* remove all nonexistent messages */
+ seq_range_array_remove_range(seqset, messages_count + 1,
+ (uint32_t)-1);
+ }
+ if (!match_not)
+ return array_count(seqset) > 0;
+ else {
+ /* if all messages are in the range, it can't match */
+ range = array_get_modifiable(seqset, &count);
+ return count == 0 || range[0].seq1 != 1 ||
+ range[count-1].seq2 != messages_count;
+ }
+}
+
+static void
+search_msgset_fix(unsigned int messages_count,
+ ARRAY_TYPE(seq_range) *seqset,
+ uint32_t *seq1_r, uint32_t *seq2_r, bool match_not)
+{
+ const struct seq_range *range;
+ unsigned int count;
+ uint32_t min_seq, max_seq;
+
+ if (!search_msgset_fix_limits(messages_count, seqset, match_not)) {
+ *seq1_r = (uint32_t)-1;
+ *seq2_r = 0;
+ return;
+ }
+
+ range = array_get(seqset, &count);
+ if (!match_not) {
+ min_seq = range[0].seq1;
+ max_seq = range[count-1].seq2;
+ } else if (count == 0) {
+ /* matches all messages */
+ min_seq = 1;
+ max_seq = messages_count;
+ } else {
+ min_seq = range[0].seq1 > 1 ? 1 : range[0].seq2 + 1;
+ max_seq = range[count-1].seq2 < messages_count ?
+ messages_count : range[count-1].seq1 - 1;
+ if (min_seq > max_seq) {
+ *seq1_r = (uint32_t)-1;
+ *seq2_r = 0;
+ return;
+ }
+ }
+
+ if (*seq1_r < min_seq || *seq1_r == 0)
+ *seq1_r = min_seq;
+ if (*seq2_r > max_seq)
+ *seq2_r = max_seq;
+}
+
+static void search_or_parse_msgset_args(unsigned int messages_count,
+ struct mail_search_arg *args,
+ uint32_t *seq1_r, uint32_t *seq2_r)
+{
+ uint32_t seq1, seq2, min_seq1 = 0, max_seq2 = 0;
+
+ for (; args != NULL; args = args->next) {
+ seq1 = 1; seq2 = messages_count;
+
+ switch (args->type) {
+ case SEARCH_SUB:
+ i_assert(!args->match_not);
+ search_parse_msgset_args(messages_count,
+ args->value.subargs,
+ &seq1, &seq2);
+ break;
+ case SEARCH_OR:
+ i_assert(!args->match_not);
+ search_or_parse_msgset_args(messages_count,
+ args->value.subargs,
+ &seq1, &seq2);
+ break;
+ case SEARCH_SEQSET:
+ search_msgset_fix(messages_count, &args->value.seqset,
+ &seq1, &seq2, args->match_not);
+ break;
+ default:
+ break;
+ }
+
+ if (min_seq1 == 0) {
+ min_seq1 = seq1;
+ max_seq2 = seq2;
+ } else {
+ if (seq1 < min_seq1)
+ min_seq1 = seq1;
+ if (seq2 > max_seq2)
+ max_seq2 = seq2;
+ }
+ }
+ i_assert(min_seq1 != 0);
+
+ if (min_seq1 > *seq1_r)
+ *seq1_r = min_seq1;
+ if (max_seq2 < *seq2_r)
+ *seq2_r = max_seq2;
+}
+
+static void search_parse_msgset_args(unsigned int messages_count,
+ struct mail_search_arg *args,
+ uint32_t *seq1_r, uint32_t *seq2_r)
+{
+ for (; args != NULL; args = args->next) {
+ switch (args->type) {
+ case SEARCH_SUB:
+ i_assert(!args->match_not);
+ search_parse_msgset_args(messages_count,
+ args->value.subargs,
+ seq1_r, seq2_r);
+ break;
+ case SEARCH_OR:
+ /* go through our children and use the widest seqset
+ range */
+ i_assert(!args->match_not);
+ search_or_parse_msgset_args(messages_count,
+ args->value.subargs,
+ seq1_r, seq2_r);
+ break;
+ case SEARCH_SEQSET:
+ search_msgset_fix(messages_count, &args->value.seqset,
+ seq1_r, seq2_r, args->match_not);
+ break;
+ default:
+ break;
+ }
+ }
+}
+
+static void search_limit_lowwater(struct index_search_context *ctx,
+ uint32_t uid_lowwater, uint32_t *first_seq)
+{
+ uint32_t seq1, seq2;
+
+ if (uid_lowwater == 0)
+ return;
+
+ (void)mail_index_lookup_seq_range(ctx->view, uid_lowwater, (uint32_t)-1,
+ &seq1, &seq2);
+ if (*first_seq < seq1)
+ *first_seq = seq1;
+}
+
+static bool search_limit_by_hdr(struct index_search_context *ctx,
+ struct mail_search_arg *args,
+ uint32_t *seq1, uint32_t *seq2)
+{
+ const struct mail_index_header *hdr;
+ enum mail_flags pvt_flags_mask;
+ uint64_t highest_modseq;
+
+ hdr = mail_index_get_header(ctx->view);
+ /* we can't trust that private view's header is fully up to date,
+ so do this optimization only for non-private flags */
+ pvt_flags_mask = ctx->box->view_pvt == NULL ? 0 :
+ mailbox_get_private_flags_mask(ctx->box);
+
+ for (; args != NULL; args = args->next) {
+ switch (args->type) {
+ case SEARCH_ALL:
+ if (args->match_not) {
+ /* NOT ALL - pointless noop query */
+ return FALSE;
+ }
+ continue;
+ case SEARCH_MODSEQ:
+ /* MODSEQ higher than current HIGHESTMODSEQ? */
+ highest_modseq = mail_index_modseq_get_highest(ctx->view);
+ if (args->value.modseq->modseq > highest_modseq)
+ return FALSE;
+ continue;
+ default:
+ continue;
+ case SEARCH_FLAGS:
+ break;
+ }
+ if ((args->value.flags & MAIL_SEEN) != 0 &&
+ (pvt_flags_mask & MAIL_SEEN) == 0) {
+ /* SEEN with 0 seen? */
+ if (!args->match_not && hdr->seen_messages_count == 0)
+ return FALSE;
+
+ if (hdr->seen_messages_count == hdr->messages_count) {
+ /* UNSEEN with all seen? */
+ if (args->match_not)
+ return FALSE;
+ } else if (args->match_not) {
+ /* UNSEEN with lowwater limiting */
+ search_limit_lowwater(ctx,
+ hdr->first_unseen_uid_lowwater, seq1);
+ }
+ }
+ if ((args->value.flags & MAIL_DELETED) != 0 &&
+ (pvt_flags_mask & MAIL_DELETED) == 0) {
+ /* DELETED with 0 deleted? */
+ if (!args->match_not &&
+ hdr->deleted_messages_count == 0)
+ return FALSE;
+
+ if (hdr->deleted_messages_count == hdr->messages_count) {
+ /* UNDELETED with all deleted? */
+ if (args->match_not)
+ return FALSE;
+ } else if (!args->match_not) {
+ /* DELETED with lowwater limiting */
+ search_limit_lowwater(ctx,
+ hdr->first_deleted_uid_lowwater, seq1);
+ }
+ }
+ }
+
+ return *seq1 <= *seq2;
+}
+
+static void search_get_seqset(struct index_search_context *ctx,
+ unsigned int messages_count,
+ struct mail_search_arg *args)
+{
+ if (messages_count == 0) {
+ /* no messages, don't check sequence ranges. although we could
+ give error message then for FETCH, we shouldn't do it for
+ UID FETCH. */
+ ctx->seq1 = 1;
+ ctx->seq2 = 0;
+ return;
+ }
+
+ ctx->seq1 = 1;
+ ctx->seq2 = messages_count;
+
+ search_parse_msgset_args(messages_count, args, &ctx->seq1, &ctx->seq2);
+ if (ctx->seq1 == 0) {
+ ctx->seq1 = 1;
+ ctx->seq2 = messages_count;
+ }
+ if (ctx->seq1 > ctx->seq2) {
+ /* no matches */
+ return;
+ }
+
+ /* See if this search query can never match based on data in index's
+ header. We'll scan only the root level args, which is usually
+ enough. */
+ if (!search_limit_by_hdr(ctx, args, &ctx->seq1, &ctx->seq2)) {
+ /* no matches */
+ ctx->seq1 = 1;
+ ctx->seq2 = 0;
+ }
+}
+
+static int search_build_subthread(struct mail_thread_iterate_context *iter,
+ ARRAY_TYPE(seq_range) *uids)
+{
+ struct mail_thread_iterate_context *child_iter;
+ const struct mail_thread_child_node *node;
+ int ret = 0;
+
+ while ((node = mail_thread_iterate_next(iter, &child_iter)) != NULL) {
+ if (child_iter != NULL) {
+ if (search_build_subthread(child_iter, uids) < 0)
+ ret = -1;
+ }
+ seq_range_array_add(uids, node->uid);
+ }
+ if (mail_thread_iterate_deinit(&iter) < 0)
+ ret = -1;
+ return ret;
+}
+
+static int search_build_inthread_result(struct index_search_context *ctx,
+ struct mail_search_arg *arg)
+{
+ struct mail_thread_iterate_context *iter, *child_iter;
+ const struct mail_thread_child_node *node;
+ const ARRAY_TYPE(seq_range) *search_uids;
+ ARRAY_TYPE(seq_range) thread_uids;
+ int ret = 0;
+
+ /* mail_search_args_init() must have been called by now */
+ i_assert(arg->initialized.search_args != NULL);
+
+ p_array_init(&arg->value.seqset, ctx->mail_ctx.args->pool, 64);
+ if (mailbox_search_result_build(ctx->mail_ctx.transaction,
+ arg->initialized.search_args,
+ MAILBOX_SEARCH_RESULT_FLAG_UPDATE |
+ MAILBOX_SEARCH_RESULT_FLAG_QUEUE_SYNC,
+ &arg->value.search_result) < 0)
+ return -1;
+ if (ctx->thread_ctx == NULL) {
+ /* failed earlier */
+ return -1;
+ }
+
+ search_uids = mailbox_search_result_get(arg->value.search_result);
+ if (array_count(search_uids) == 0) {
+ /* search found nothing - no threads can match */
+ return 0;
+ }
+
+ t_array_init(&thread_uids, 128);
+ iter = mail_thread_iterate_init(ctx->thread_ctx,
+ arg->value.thread_type, FALSE);
+ while ((node = mail_thread_iterate_next(iter, &child_iter)) != NULL) {
+ seq_range_array_add(&thread_uids, node->uid);
+ if (child_iter != NULL) {
+ if (search_build_subthread(child_iter,
+ &thread_uids) < 0)
+ ret = -1;
+ }
+ if (seq_range_array_have_common(&thread_uids, search_uids)) {
+ /* yes, we want this thread */
+ seq_range_array_merge(&arg->value.seqset, &thread_uids);
+ }
+ array_clear(&thread_uids);
+ }
+ if (mail_thread_iterate_deinit(&iter) < 0)
+ ret = -1;
+ return ret;
+}
+
+static int search_build_inthreads(struct index_search_context *ctx,
+ struct mail_search_arg *arg)
+{
+ int ret = 0;
+
+ for (; arg != NULL; arg = arg->next) {
+ switch (arg->type) {
+ case SEARCH_OR:
+ case SEARCH_SUB:
+ if (search_build_inthreads(ctx, arg->value.subargs) < 0)
+ ret = -1;
+ break;
+ case SEARCH_INTHREAD:
+ if (search_build_inthread_result(ctx, arg) < 0)
+ ret = -1;
+ break;
+ default:
+ break;
+ }
+ }
+ return ret;
+}
+
+static void
+wanted_sort_fields_get(struct mailbox *box,
+ const enum mail_sort_type *sort_program,
+ struct mailbox_header_lookup_ctx *wanted_headers,
+ enum mail_fetch_field *wanted_fields_r,
+ struct mailbox_header_lookup_ctx **headers_ctx_r)
+{
+ ARRAY_TYPE(const_string) headers;
+ const char *header;
+ unsigned int i;
+
+ *wanted_fields_r = 0;
+ *headers_ctx_r = NULL;
+
+ t_array_init(&headers, 8);
+ for (i = 0; sort_program[i] != MAIL_SORT_END; i++) {
+ header = NULL;
+
+ switch (sort_program[i] & MAIL_SORT_MASK) {
+ case MAIL_SORT_ARRIVAL:
+ *wanted_fields_r |= MAIL_FETCH_RECEIVED_DATE;
+ break;
+ case MAIL_SORT_CC:
+ header = "Cc";
+ break;
+ case MAIL_SORT_DATE:
+ *wanted_fields_r |= MAIL_FETCH_DATE;
+ break;
+ case MAIL_SORT_FROM:
+ header = "From";
+ break;
+ case MAIL_SORT_SIZE:
+ *wanted_fields_r |= MAIL_FETCH_VIRTUAL_SIZE;
+ break;
+ case MAIL_SORT_SUBJECT:
+ header = "Subject";
+ break;
+ case MAIL_SORT_TO:
+ header = "To";
+ break;
+ }
+ if (header != NULL)
+ array_push_back(&headers, &header);
+ }
+
+ if (wanted_headers != NULL) {
+ for (i = 0; wanted_headers->name[i] != NULL; i++)
+ array_push_back(&headers, &wanted_headers->name[i]);
+ }
+
+ if (array_count(&headers) > 0) {
+ array_append_zero(&headers);
+ *headers_ctx_r = mailbox_header_lookup_init(box,
+ array_front(&headers));
+ }
+}
+
+struct mail_search_context *
+index_storage_search_init(struct mailbox_transaction_context *t,
+ struct mail_search_args *args,
+ const enum mail_sort_type *sort_program,
+ enum mail_fetch_field wanted_fields,
+ struct mailbox_header_lookup_ctx *wanted_headers)
+{
+ struct index_search_context *ctx;
+ struct mailbox_status status;
+
+ ctx = i_new(struct index_search_context, 1);
+ ctx->mail_ctx.transaction = t;
+ ctx->mail_ctx.normalizer = t->box->storage->user->default_normalizer;
+ ctx->box = t->box;
+ ctx->view = t->view;
+ ctx->mail_ctx.args = args;
+ ctx->mail_ctx.sort_program = index_sort_program_init(t, sort_program);
+
+ ctx->mail_ctx.max_mails = t->box->storage->set->mail_prefetch_count + 1;
+ if (ctx->mail_ctx.max_mails == 0)
+ ctx->mail_ctx.max_mails = UINT_MAX;
+ ctx->next_time_check_cost = SEARCH_INITIAL_MAX_COST;
+ i_gettimeofday(&ctx->last_nonblock_timeval);
+
+ mailbox_get_open_status(t->box, STATUS_MESSAGES, &status);
+ ctx->mail_ctx.progress_max = status.messages;
+
+ i_array_init(&ctx->mail_ctx.results, 5);
+ array_create(&ctx->mail_ctx.module_contexts, default_pool,
+ sizeof(void *), 5);
+ i_array_init(&ctx->mail_ctx.mails, ctx->mail_ctx.max_mails);
+
+ mail_search_args_reset(ctx->mail_ctx.args->args, TRUE);
+ if (args->have_inthreads) {
+ if (mail_thread_init(t->box, NULL, &ctx->thread_ctx) < 0)
+ search_set_failed(ctx);
+ if (search_build_inthreads(ctx, args->args) < 0)
+ search_set_failed(ctx);
+ }
+
+ if (sort_program != NULL) {
+ wanted_sort_fields_get(ctx->box, sort_program, wanted_headers,
+ &ctx->mail_ctx.wanted_fields,
+ &ctx->mail_ctx.wanted_headers);
+ } else if (wanted_headers != NULL) {
+ ctx->mail_ctx.wanted_headers = wanted_headers;
+ mailbox_header_lookup_ref(wanted_headers);
+ }
+ ctx->mail_ctx.wanted_fields |= wanted_fields;
+
+ search_get_seqset(ctx, status.messages, args->args);
+ (void)mail_search_args_foreach(args->args, search_init_arg, ctx);
+
+ /* Need to reset results for match_always cases */
+ mail_search_args_reset(ctx->mail_ctx.args->args, FALSE);
+ return &ctx->mail_ctx;
+}
+
+static void ATTR_NULL(2)
+search_arg_deinit(struct mail_search_arg *arg,
+ struct index_search_context *ctx)
+{
+ switch (arg->type) {
+ case SEARCH_MIMEPART:
+ index_search_mime_arg_deinit(arg, ctx);
+ break;
+ default:
+ if (arg->context != NULL) {
+ struct message_search_context *search_ctx = arg->context;
+ message_search_deinit(&search_ctx);
+ arg->context = NULL;
+ }
+ }
+}
+
+int index_storage_search_deinit(struct mail_search_context *_ctx)
+{
+ struct index_search_context *ctx = (struct index_search_context *)_ctx;
+ struct mail *mail;
+ int ret;
+
+ ret = ctx->failed ? -1 : 0;
+
+ mail_search_args_reset(ctx->mail_ctx.args->args, FALSE);
+ (void)mail_search_args_foreach(ctx->mail_ctx.args->args,
+ search_arg_deinit, ctx);
+
+ mailbox_header_lookup_unref(&ctx->mail_ctx.wanted_headers);
+ if (ctx->mail_ctx.sort_program != NULL) {
+ if (index_sort_program_deinit(&ctx->mail_ctx.sort_program) < 0)
+ ret = -1;
+ }
+ if (ctx->thread_ctx != NULL)
+ mail_thread_deinit(&ctx->thread_ctx);
+ array_free(&ctx->mail_ctx.results);
+ array_free(&ctx->mail_ctx.module_contexts);
+
+ array_foreach_elem(&ctx->mail_ctx.mails, mail) {
+ struct index_mail *imail = INDEX_MAIL(mail);
+
+ imail->mail.search_mail = FALSE;
+ mail_free(&mail);
+ }
+
+ if (ctx->failed)
+ mail_storage_last_error_pop(ctx->box->storage);
+ array_free(&ctx->mail_ctx.mails);
+ i_free(ctx);
+ return ret;
+}
+
+static unsigned long long
+search_get_cost(struct mailbox_transaction_context *trans)
+{
+ return trans->stats.open_lookup_count * SEARCH_COST_DENTRY +
+ trans->stats.stat_lookup_count * SEARCH_COST_DENTRY +
+ trans->stats.fstat_lookup_count * SEARCH_COST_ATTR +
+ trans->stats.cache_hit_count * SEARCH_COST_CACHE +
+ trans->stats.files_read_count * SEARCH_COST_FILES_READ +
+ (trans->stats.files_read_bytes/1024) * SEARCH_COST_KBYTE;
+}
+
+static int search_match_once(struct index_search_context *ctx)
+{
+ int ret;
+
+ ret = mail_search_args_foreach(ctx->mail_ctx.args->args,
+ search_cached_arg, ctx);
+ if (ret < 0)
+ ret = search_arg_match_text(ctx->mail_ctx.args->args, ctx);
+ if (ret < 0)
+ ret = index_search_mime_arg_match(ctx->mail_ctx.args->args, ctx);
+ return ret;
+}
+
+static bool search_arg_is_static(struct mail_search_arg *arg)
+{
+ struct mail_search_arg *subarg;
+
+ switch (arg->type) {
+ case SEARCH_OR:
+ case SEARCH_SUB:
+ /* they're static only if all subargs are static */
+ subarg = arg->value.subargs;
+ for (; subarg != NULL; subarg = subarg->next) {
+ if (!search_arg_is_static(subarg))
+ return FALSE;
+ }
+ return TRUE;
+ case SEARCH_SEQSET:
+ /* changes between syncs, but we can't really handle this
+ currently. seqsets should be converted to uidsets first. */
+ case SEARCH_FLAGS:
+ case SEARCH_KEYWORDS:
+ case SEARCH_MODSEQ:
+ case SEARCH_INTHREAD:
+ break;
+ case SEARCH_ALL:
+ case SEARCH_UIDSET:
+ case SEARCH_BEFORE:
+ case SEARCH_ON:
+ case SEARCH_SINCE:
+ case SEARCH_SMALLER:
+ case SEARCH_LARGER:
+ case SEARCH_HEADER:
+ case SEARCH_HEADER_ADDRESS:
+ case SEARCH_HEADER_COMPRESS_LWSP:
+ case SEARCH_BODY:
+ case SEARCH_TEXT:
+ case SEARCH_SAVEDATESUPPORTED:
+ case SEARCH_GUID:
+ case SEARCH_MAILBOX:
+ case SEARCH_MAILBOX_GUID:
+ case SEARCH_MAILBOX_GLOB:
+ case SEARCH_REAL_UID:
+ case SEARCH_MIMEPART:
+ return TRUE;
+ }
+ return FALSE;
+}
+
+static void search_set_static_matches(struct mail_search_arg *arg)
+{
+ for (; arg != NULL; arg = arg->next) {
+ if (search_arg_is_static(arg))
+ arg->result = 1;
+ }
+}
+
+static bool search_has_static_nonmatches(struct mail_search_arg *arg)
+{
+ for (; arg != NULL; arg = arg->next) {
+ if (arg->result == 0 && search_arg_is_static(arg))
+ return TRUE;
+ }
+ return FALSE;
+}
+
+static void search_match_finish(struct index_search_context *ctx, int match)
+{
+ if (match == 0 &&
+ search_has_static_nonmatches(ctx->mail_ctx.args->args)) {
+ /* if there are saved search results remember
+ that this message never matches */
+ mailbox_search_results_never(&ctx->mail_ctx,
+ ctx->cur_mail->uid);
+ }
+}
+
+static int search_match_next(struct index_search_context *ctx)
+{
+ static enum mail_lookup_abort cache_lookups[] = {
+ MAIL_LOOKUP_ABORT_NOT_IN_CACHE,
+ MAIL_LOOKUP_ABORT_READ_MAIL,
+ MAIL_LOOKUP_ABORT_NEVER
+ };
+ unsigned int i, n = N_ELEMENTS(cache_lookups);
+ int ret = -1;
+
+ if (ctx->have_mailbox_args) {
+ /* check that the mailbox name matches.
+ this makes sense only with virtual mailboxes. */
+ ret = mail_search_args_foreach(ctx->mail_ctx.args->args,
+ search_mailbox_arg, ctx);
+ }
+
+ /* avoid doing extra work for as long as possible */
+ if (ctx->mail_ctx.max_mails > 1) {
+ /* we're doing prefetching. if we have to read the mail,
+ do a prefetch first and the final search later */
+ n--;
+ }
+
+ i_assert(ctx->cur_mail->lookup_abort == MAIL_LOOKUP_ABORT_NEVER);
+ for (i = 0; i < n && ret < 0; i++) {
+ ctx->cur_mail->lookup_abort = cache_lookups[i];
+ T_BEGIN {
+ ret = search_match_once(ctx);
+ } T_END;
+ }
+ ctx->cur_mail->lookup_abort = MAIL_LOOKUP_ABORT_NEVER;
+ search_match_finish(ctx, ret);
+ return ret;
+}
+
+static void index_storage_search_notify(struct mailbox *box,
+ struct index_search_context *ctx)
+{
+ float percentage;
+ unsigned int msecs, secs;
+
+ if (ctx->last_notify.tv_sec == 0) {
+ /* set the search time in here, in case a plugin
+ already spent some time indexing the mailbox */
+ ctx->search_start_time = ioloop_timeval;
+ } else if (box->storage->callbacks.notify_ok != NULL &&
+ !ctx->mail_ctx.progress_hidden) {
+ percentage = ctx->mail_ctx.progress_cur * 100.0 /
+ ctx->mail_ctx.progress_max;
+ msecs = timeval_diff_msecs(&ioloop_timeval,
+ &ctx->search_start_time);
+ secs = (msecs / (percentage / 100.0) - msecs) / 1000;
+
+ T_BEGIN {
+ const char *text;
+
+ text = t_strdup_printf("Searched %d%% of the mailbox, "
+ "ETA %d:%02d", (int)percentage,
+ secs/60, secs%60);
+ box->storage->callbacks.
+ notify_ok(box, text,
+ box->storage->callback_context);
+ } T_END;
+ }
+ ctx->last_notify = ioloop_timeval;
+}
+
+static bool search_would_block(struct index_search_context *ctx)
+{
+ struct timeval now;
+ unsigned long long guess_cost;
+ long long usecs;
+ bool ret;
+
+ if (ctx->cost < ctx->next_time_check_cost)
+ return FALSE;
+
+ i_gettimeofday(&now);
+
+ usecs = timeval_diff_usecs(&now, &ctx->last_nonblock_timeval);
+ if (usecs < 0) {
+ /* clock moved backwards. */
+ ctx->last_nonblock_timeval = now;
+ ctx->next_time_check_cost = SEARCH_INITIAL_MAX_COST;
+ return TRUE;
+ } else if (usecs < SEARCH_MIN_NONBLOCK_USECS) {
+ /* not finished yet. estimate the next time lookup */
+ ret = FALSE;
+ } else {
+ /* done, or close enough anyway */
+ ctx->last_nonblock_timeval = now;
+ ret = TRUE;
+ }
+ guess_cost = ctx->cost *
+ (SEARCH_MAX_NONBLOCK_USECS / (double)usecs);
+ if (usecs < SEARCH_RECALC_MIN_USECS) {
+ /* the estimate may not be very good since we spent
+ so little time doing this search. don't allow huge changes
+ to the guess, but allow anyway large enough so that we can
+ move to right direction. */
+ if (guess_cost > ctx->next_time_check_cost*3)
+ guess_cost = ctx->next_time_check_cost*3;
+ else if (guess_cost < ctx->next_time_check_cost/3)
+ guess_cost = ctx->next_time_check_cost/3;
+ }
+ if (ret)
+ ctx->cost = 0;
+ ctx->next_time_check_cost = guess_cost;
+ return ret;
+}
+
+int index_storage_search_next_match_mail(struct mail_search_context *_ctx,
+ struct mail *mail)
+{
+ struct index_search_context *ctx =
+ container_of(_ctx, struct index_search_context, mail_ctx);
+ struct index_mail *imail = INDEX_MAIL(mail);
+ int match;
+
+ ctx->cur_mail = mail;
+ /* mail's access_type is SEARCH only while using it to process
+ the search query. afterwards the mail can still be accessed
+ for fetching. */
+ ctx->cur_mail->access_type = MAIL_ACCESS_TYPE_SEARCH;
+ T_BEGIN {
+ match = search_match_next(ctx);
+ } T_END;
+ ctx->cur_mail->access_type = MAIL_ACCESS_TYPE_DEFAULT;
+ ctx->cur_mail = NULL;
+
+ i_assert(imail->data.search_results == NULL);
+ if (match < 0) {
+ /* result isn't known yet, do a prefetch and
+ finish later */
+ imail->data.search_results =
+ buffer_create_dynamic(imail->mail.data_pool, 64);
+ mail_search_args_result_serialize(_ctx->args,
+ imail->data.search_results);
+ }
+
+ mail_search_args_reset(_ctx->args->args, FALSE);
+
+ if (match != 0) {
+ /* either matched or result is still unknown.
+ anyway we're far enough now that we probably want
+ to update the access_parts. the only problem here is
+ if searching would want fewer access_parts than the
+ fetching part, but that's probably not a big problem
+ usually. */
+ index_mail_update_access_parts_pre(mail);
+ return 1;
+ }
+
+ /* non-match */
+ if (_ctx->args->stop_on_nonmatch)
+ return -1;
+ return 0;
+}
+
+static int search_more_with_mail(struct index_search_context *ctx,
+ struct mail *mail)
+{
+ struct mail_search_context *_ctx = &ctx->mail_ctx;
+ struct mailbox *box = _ctx->transaction->box;
+ unsigned long long cost1, cost2;
+ int ret;
+
+ if (search_would_block(ctx)) {
+ /* this lookup is useful when a large number of
+ messages match */
+ return 0;
+ }
+
+ if (ioloop_time - ctx->last_notify.tv_sec >=
+ SEARCH_NOTIFY_INTERVAL_SECS)
+ index_storage_search_notify(box, ctx);
+
+ mail_search_args_reset(_ctx->args->args, FALSE);
+
+ cost1 = search_get_cost(mail->transaction);
+ ret = -1;
+ while (box->v.search_next_update_seq(_ctx)) {
+ mail_set_seq(mail, _ctx->seq);
+
+ ret = box->v.search_next_match_mail(_ctx, mail);
+ if (ret != 0)
+ break;
+
+ cost2 = search_get_cost(mail->transaction);
+ ctx->cost += cost2 - cost1;
+ cost1 = cost2;
+
+ if (search_would_block(ctx))
+ break;
+ ret = -1;
+ }
+ cost2 = search_get_cost(mail->transaction);
+ ctx->cost += cost2 - cost1;
+ return ret;
+}
+
+struct mail *index_search_get_mail(struct index_search_context *ctx)
+{
+ struct index_mail *imail;
+ struct mail *const *mails, *mail;
+ unsigned int count;
+
+ if (ctx->mail_ctx.unused_mail_idx == ctx->mail_ctx.max_mails)
+ return NULL;
+
+ mails = array_get(&ctx->mail_ctx.mails, &count);
+ if (ctx->mail_ctx.unused_mail_idx < count)
+ return mails[ctx->mail_ctx.unused_mail_idx];
+
+ mail = mail_alloc(ctx->mail_ctx.transaction,
+ ctx->mail_ctx.wanted_fields,
+ ctx->mail_ctx.wanted_headers);
+ imail = INDEX_MAIL(mail);
+ imail->mail.search_mail = TRUE;
+ ctx->mail_ctx.transaction->stats_track = TRUE;
+
+ array_push_back(&ctx->mail_ctx.mails, &mail);
+ return mail;
+}
+
+static int search_more_with_prefetching(struct index_search_context *ctx,
+ struct mail **mail_r)
+{
+ struct mail *mail, *const *mails;
+ unsigned int count;
+ int ret = 0;
+
+ while ((mail = index_search_get_mail(ctx)) != NULL) {
+ T_BEGIN {
+ ret = search_more_with_mail(ctx, mail);
+ } T_END;
+ if (ret <= 0)
+ break;
+
+ if (ctx->mail_ctx.sort_program != NULL) {
+ /* don't prefetch when using a sort program,
+ since the mails' access order will change */
+ i_assert(ctx->mail_ctx.unused_mail_idx == 0);
+ *mail_r = mail;
+ return 1;
+ }
+ if (mail_prefetch(mail) && ctx->mail_ctx.unused_mail_idx == 0) {
+ /* no prefetching done, return it immediately */
+ *mail_r = mail;
+ return 1;
+ }
+ ctx->mail_ctx.unused_mail_idx++;
+ }
+
+ if (mail != NULL) {
+ if (ret == 0) {
+ /* wait */
+ return 0;
+ }
+ i_assert(ret < 0);
+ if (ctx->mail_ctx.unused_mail_idx == 0) {
+ /* finished */
+ return -1;
+ }
+ } else {
+ /* prefetch buffer is full. */
+ }
+
+ /* return the next message */
+ i_assert(ctx->mail_ctx.unused_mail_idx > 0);
+
+ mails = array_get(&ctx->mail_ctx.mails, &count);
+ *mail_r = mails[0];
+ if (--ctx->mail_ctx.unused_mail_idx > 0) {
+ array_pop_front(&ctx->mail_ctx.mails);
+ array_push_back(&ctx->mail_ctx.mails, mail_r);
+ }
+ index_mail_update_access_parts_post(*mail_r);
+ return 1;
+}
+
+static bool search_finish_prefetch(struct index_search_context *ctx,
+ struct index_mail *imail)
+{
+ int ret;
+
+ i_assert(imail->mail.mail.lookup_abort == MAIL_LOOKUP_ABORT_NEVER);
+
+ ctx->cur_mail = &imail->mail.mail;
+ ctx->cur_mail->access_type = MAIL_ACCESS_TYPE_SEARCH;
+ mail_search_args_result_deserialize(ctx->mail_ctx.args,
+ imail->data.search_results->data,
+ imail->data.search_results->used);
+ T_BEGIN {
+ ret = search_match_once(ctx);
+ search_match_finish(ctx, ret);
+ } T_END;
+ ctx->cur_mail->access_type = MAIL_ACCESS_TYPE_DEFAULT;
+ ctx->cur_mail = NULL;
+ return ret > 0;
+}
+
+static int search_more(struct index_search_context *ctx,
+ struct mail **mail_r)
+{
+ struct index_mail *imail;
+ int ret;
+
+ while ((ret = search_more_with_prefetching(ctx, mail_r)) > 0) {
+ imail = INDEX_MAIL(*mail_r);
+ if (imail->data.search_results == NULL)
+ break;
+
+ /* prefetch running - searching wasn't finished yet */
+ if (search_finish_prefetch(ctx, imail))
+ break;
+ /* search finished as non-match */
+ if (ctx->mail_ctx.args->stop_on_nonmatch) {
+ ret = -1;
+ break;
+ }
+ }
+ return ret;
+}
+
+bool index_storage_search_next_nonblock(struct mail_search_context *_ctx,
+ struct mail **mail_r, bool *tryagain_r)
+{
+ struct index_search_context *ctx = (struct index_search_context *)_ctx;
+ struct mail *mail, *const *mailp;
+ uint32_t seq;
+ int ret;
+
+ *tryagain_r = FALSE;
+
+ if (_ctx->sort_program == NULL) {
+ ret = search_more(ctx, &mail);
+ if (ret == 0) {
+ *tryagain_r = TRUE;
+ return FALSE;
+ }
+ if (ret < 0)
+ return FALSE;
+ *mail_r = mail;
+ return TRUE;
+ }
+
+ if (!ctx->sorted) {
+ while ((ret = search_more(ctx, &mail)) > 0)
+ index_sort_list_add(_ctx->sort_program, mail);
+
+ if (ret == 0) {
+ *tryagain_r = TRUE;
+ return FALSE;
+ }
+ /* finished searching the messages. now sort them and start
+ returning the messages. */
+ ctx->sorted = TRUE;
+ index_sort_list_finish(_ctx->sort_program);
+ }
+
+ /* everything searched at this point already. just returning
+ matches from sort list. FIXME: we could do prefetching here also. */
+ if (!index_sort_list_next(_ctx->sort_program, &seq))
+ return FALSE;
+
+ mailp = array_front(&ctx->mail_ctx.mails);
+ mail_set_seq(*mailp, seq);
+ index_mail_update_access_parts_pre(*mailp);
+ index_mail_update_access_parts_post(*mailp);
+ *mail_r = *mailp;
+ return TRUE;
+}
+
+bool index_storage_search_next_update_seq(struct mail_search_context *_ctx)
+{
+ struct index_search_context *ctx = (struct index_search_context *)_ctx;
+ uint32_t uid;
+ int ret;
+
+ if (_ctx->seq == 0) {
+ /* first time */
+ _ctx->seq = ctx->seq1;
+ } else {
+ _ctx->seq++;
+ }
+
+ if (!ctx->have_seqsets && !ctx->have_index_args &&
+ !ctx->have_nonmatch_always && _ctx->update_result == NULL) {
+ _ctx->progress_cur = _ctx->seq;
+ return _ctx->seq <= ctx->seq2;
+ }
+
+ ret = 0;
+ while (_ctx->seq <= ctx->seq2) {
+ /* check if the sequence matches */
+ ret = mail_search_args_foreach(ctx->mail_ctx.args->args,
+ search_seqset_arg, ctx);
+ if (ret != 0 && ctx->have_index_args) {
+ /* check if flags/keywords match before anything else
+ is done. mail_set_seq() can be a bit slow. */
+ ret = mail_search_args_foreach(ctx->mail_ctx.args->args,
+ search_index_arg, ctx);
+ }
+ if (ret != 0 && _ctx->update_result != NULL) {
+ /* see if this message never matches */
+ mail_index_lookup_uid(ctx->view, _ctx->seq, &uid);
+ if (seq_range_exists(&_ctx->update_result->never_uids,
+ uid))
+ ret = 0;
+ }
+ if (ret != 0)
+ break;
+
+ /* doesn't, try next one */
+ _ctx->seq++;
+ mail_search_args_reset(ctx->mail_ctx.args->args, FALSE);
+ }
+
+ if (ret != 0 && _ctx->update_result != NULL) {
+ mail_index_lookup_uid(ctx->view, _ctx->seq, &uid);
+ if (seq_range_exists(&_ctx->update_result->uids, uid)) {
+ /* we already know that the static data
+ matches. mark it as such. */
+ search_set_static_matches(_ctx->args->args);
+ }
+ }
+ ctx->mail_ctx.progress_cur = _ctx->seq;
+ return ret != 0;
+}
diff --git a/src/lib-storage/index/index-sort-private.h b/src/lib-storage/index/index-sort-private.h
new file mode 100644
index 0000000..3dd0772
--- /dev/null
+++ b/src/lib-storage/index/index-sort-private.h
@@ -0,0 +1,35 @@
+#ifndef INDEX_SORT_PRIVATE_H
+#define INDEX_SORT_PRIVATE_H
+
+#include "index-sort.h"
+
+struct mail_search_sort_program {
+ struct mailbox_transaction_context *t;
+ enum mail_sort_type sort_program[MAX_SORT_PROGRAM_SIZE];
+ struct mail *temp_mail;
+ unsigned int slow_mails_left;
+
+ void (*sort_list_add)(struct mail_search_sort_program *program,
+ struct mail *mail);
+ void (*sort_list_finish)(struct mail_search_sort_program *program);
+ void *context;
+
+ ARRAY_TYPE(uint32_t) seqs;
+ unsigned int iter_idx;
+
+ bool failed;
+};
+
+/* Returns 1 on success, 0 if mail is already expunged, -1 on other errors. */
+int index_sort_header_get(struct mail_search_sort_program *program, uint32_t seq,
+ enum mail_sort_type sort_type, string_t *dest);
+int index_sort_node_cmp_type(struct mail_search_sort_program *program,
+ const enum mail_sort_type *sort_program,
+ uint32_t seq1, uint32_t seq2);
+
+void index_sort_list_init_string(struct mail_search_sort_program *program);
+void index_sort_list_add_string(struct mail_search_sort_program *program,
+ struct mail *mail);
+void index_sort_list_finish_string(struct mail_search_sort_program *program);
+
+#endif
diff --git a/src/lib-storage/index/index-sort-string.c b/src/lib-storage/index/index-sort-string.c
new file mode 100644
index 0000000..c518e78
--- /dev/null
+++ b/src/lib-storage/index/index-sort-string.c
@@ -0,0 +1,944 @@
+/* Copyright (c) 2006-2018 Dovecot authors, see the included COPYING file */
+
+/* The idea is that we use 32bit integers for string sort IDs which specify
+ the sort order for primary sort condition. The whole 32bit integer space is
+ used and whenever adding a string, the available space is halved and the new
+ ID is added in the middle. For example if we add one mail the first time, it
+ gets ID 2^31. If we then add two mails which are sorted before the first
+ one, they get IDs 2^31/3 and 2^31/3*2. Once we run out of the available
+ space between IDs, more space is made by renumbering some IDs.
+*/
+#include "lib.h"
+#include "array.h"
+#include "str.h"
+#include "index-storage.h"
+#include "index-sort-private.h"
+
+
+struct mail_sort_node {
+ uint32_t seq:29;
+ bool wanted:1;
+ bool no_update:1;
+ bool sort_id_changed:1;
+ uint32_t sort_id;
+};
+ARRAY_DEFINE_TYPE(mail_sort_node, struct mail_sort_node);
+
+struct sort_string_context {
+ struct mail_search_sort_program *program;
+ const char *primary_sort_name;
+
+ ARRAY_TYPE(mail_sort_node) zero_nodes, nonzero_nodes, sorted_nodes;
+ const char **sort_strings;
+ pool_t sort_string_pool;
+ unsigned int first_missing_sort_id_idx;
+
+ uint32_t ext_id, last_seq, highest_reset_id, prev_seq;
+ uint32_t lowest_nonexpunged_zero;
+
+ bool regetting:1;
+ bool have_all_wanted:1;
+ bool no_writing:1;
+ bool reverse:1;
+ bool seqs_nonsorted:1;
+ bool broken:1;
+ bool failed:1;
+};
+
+static struct sort_string_context *static_zero_cmp_context;
+
+static void index_sort_list_reset_broken(struct sort_string_context *ctx,
+ const char *reason);
+static void index_sort_node_add(struct sort_string_context *ctx,
+ struct mail_sort_node *node);
+
+void index_sort_list_init_string(struct mail_search_sort_program *program)
+{
+ struct sort_string_context *ctx;
+ const char *name;
+
+ switch (program->sort_program[0] & MAIL_SORT_MASK) {
+ case MAIL_SORT_CC:
+ name = "sort-c";
+ break;
+ case MAIL_SORT_FROM:
+ name = "sort-f";
+ break;
+ case MAIL_SORT_SUBJECT:
+ name = "sort-s";
+ break;
+ case MAIL_SORT_TO:
+ name = "sort-t";
+ break;
+ case MAIL_SORT_DISPLAYFROM:
+ name = "sort-df";
+ break;
+ case MAIL_SORT_DISPLAYTO:
+ name = "sort-dt";
+ break;
+ default:
+ i_unreached();
+ }
+
+ program->context = ctx = i_new(struct sort_string_context, 1);
+ ctx->reverse = (program->sort_program[0] & MAIL_SORT_FLAG_REVERSE) != 0;
+ ctx->program = program;
+ ctx->primary_sort_name = name;
+ ctx->ext_id = mail_index_ext_register(program->t->box->index, name, 0,
+ sizeof(uint32_t),
+ sizeof(uint32_t));
+ i_array_init(&ctx->zero_nodes, 128);
+ i_array_init(&ctx->nonzero_nodes, 128);
+}
+
+static int sort_node_seq_cmp(const struct mail_sort_node *n1,
+ const struct mail_sort_node *n2)
+{
+ if (n1->seq < n2->seq)
+ return -1;
+ if (n1->seq > n2->seq)
+ return 1;
+ return 0;
+}
+
+static void index_sort_generate_seqs(struct sort_string_context *ctx)
+{
+ struct mail_sort_node *nodes, *nodes2;
+ unsigned int i, j, count, count2;
+ uint32_t seq;
+
+ nodes = array_get_modifiable(&ctx->nonzero_nodes, &count);
+ nodes2 = array_get_modifiable(&ctx->zero_nodes, &count2);
+
+ if (!array_is_created(&ctx->program->seqs))
+ i_array_init(&ctx->program->seqs, count + count2);
+ else
+ array_clear(&ctx->program->seqs);
+
+ for (i = j = 0;;) {
+ if (i < count && j < count2) {
+ if (nodes[i].seq < nodes2[j].seq)
+ seq = nodes[i++].seq;
+ else
+ seq = nodes2[j++].seq;
+ } else if (i < count) {
+ seq = nodes[i++].seq;
+ } else if (j < count2) {
+ seq = nodes2[j++].seq;
+ } else {
+ break;
+ }
+ array_push_back(&ctx->program->seqs, &seq);
+ }
+}
+
+static void index_sort_reget_sort_ids(struct sort_string_context *ctx)
+{
+ struct mail_sort_node node;
+ const uint32_t *seqs;
+ unsigned int i, count;
+
+ i_assert(!ctx->regetting);
+ ctx->regetting = TRUE;
+
+ index_sort_generate_seqs(ctx);
+ array_clear(&ctx->zero_nodes);
+ array_clear(&ctx->nonzero_nodes);
+
+ i_zero(&node);
+ node.wanted = TRUE;
+ seqs = array_get(&ctx->program->seqs, &count);
+ for (i = 0; i < count; i++) {
+ node.seq = seqs[i];
+ index_sort_node_add(ctx, &node);
+ }
+ ctx->regetting = FALSE;
+}
+
+static void index_sort_node_add(struct sort_string_context *ctx,
+ struct mail_sort_node *node)
+{
+ struct mail_index_map *map;
+ const void *data;
+ uint32_t reset_id;
+ bool expunged;
+
+ mail_index_lookup_ext_full(ctx->program->t->view, node->seq,
+ ctx->ext_id, &map, &data, &expunged);
+ if (expunged) {
+ /* we don't want to update expunged messages' sort IDs */
+ node->no_update = TRUE;
+ /* we can't trust expunged messages' sort IDs. they might be
+ valid, but it's also possible that sort IDs were updated
+ and the expunged messages' sort IDs became invalid. we could
+ use sort ID if we could know the extension's reset_id at the
+ time of the expunge so we could compare it to
+ highest_reset_id, but this isn't currently possible. */
+ node->sort_id = 0;
+ } else {
+ node->sort_id = ctx->broken || data == NULL ? 0 :
+ *(const uint32_t *)data;
+ if (node->sort_id == 0) {
+ if (ctx->lowest_nonexpunged_zero > node->seq ||
+ ctx->lowest_nonexpunged_zero == 0)
+ ctx->lowest_nonexpunged_zero = node->seq;
+ } else if (ctx->lowest_nonexpunged_zero != 0 &&
+ ctx->lowest_nonexpunged_zero <= node->seq) {
+ uint32_t nonzero_uid, zero_uid;
+
+ mail_index_lookup_uid(ctx->program->t->view,
+ node->seq, &nonzero_uid);
+ mail_index_lookup_uid(ctx->program->t->view,
+ ctx->lowest_nonexpunged_zero, &zero_uid);
+ index_sort_list_reset_broken(ctx, t_strdup_printf(
+ "sort_id=0 found in the middle "
+ "(uid=%u has sort_id, uid=%u doesn't)",
+ nonzero_uid, zero_uid));
+ ctx->broken = TRUE;
+ node->sort_id = 0;
+ }
+ }
+
+ if (node->sort_id != 0) {
+ /* if reset ID increases, lookup all existing messages' sort
+ IDs again. if it decreases, ignore the sort ID. */
+ if (!mail_index_ext_get_reset_id(ctx->program->t->view, map,
+ ctx->ext_id, &reset_id))
+ reset_id = 0;
+ if (reset_id != ctx->highest_reset_id) {
+ if (reset_id < ctx->highest_reset_id) {
+ i_assert(expunged);
+ node->sort_id = 0;
+ } else if (ctx->have_all_wanted) {
+ /* a bit late to start changing the reset_id.
+ the node lists aren't ordered by sequence
+ anymore. */
+ node->sort_id = 0;
+ ctx->no_writing = TRUE;
+ } else {
+ ctx->highest_reset_id = reset_id;
+ index_sort_reget_sort_ids(ctx);
+ }
+ }
+ }
+
+ if (node->sort_id == 0)
+ array_push_back(&ctx->zero_nodes, node);
+ else
+ array_push_back(&ctx->nonzero_nodes, node);
+ if (ctx->last_seq < node->seq)
+ ctx->last_seq = node->seq;
+}
+
+void index_sort_list_add_string(struct mail_search_sort_program *program,
+ struct mail *mail)
+{
+ struct sort_string_context *ctx = program->context;
+ struct mail_sort_node node;
+
+ i_zero(&node);
+ node.seq = mail->seq;
+ node.wanted = TRUE;
+
+ if (mail->seq < ctx->prev_seq)
+ ctx->seqs_nonsorted = TRUE;
+ ctx->prev_seq = mail->seq;
+
+ index_sort_node_add(ctx, &node);
+}
+
+static int sort_node_zero_string_cmp(const struct mail_sort_node *n1,
+ const struct mail_sort_node *n2)
+{
+ struct sort_string_context *ctx = static_zero_cmp_context;
+ int ret;
+
+ ret = strcmp(ctx->sort_strings[n1->seq], ctx->sort_strings[n2->seq]);
+ if (ret != 0)
+ return !ctx->reverse ? ret : -ret;
+
+ return index_sort_node_cmp_type(ctx->program,
+ ctx->program->sort_program + 1,
+ n1->seq, n2->seq);
+}
+
+static void index_sort_zeroes(struct sort_string_context *ctx)
+{
+ enum mail_sort_type sort_type = ctx->program->sort_program[0];
+ string_t *str;
+ pool_t pool;
+ struct mail_sort_node *nodes;
+ unsigned int i, count;
+
+ /* first get all the messages' sort strings. although this takes more
+ memory, it makes error handling easier and probably also helps
+ CPU caching. */
+ ctx->sort_strings = i_new(const char *, ctx->last_seq + 1);
+ ctx->sort_string_pool = pool =
+ pool_alloconly_create("sort strings", 1024*64);
+ str = str_new(default_pool, 512);
+ nodes = array_get_modifiable(&ctx->zero_nodes, &count);
+ for (i = 0; i < count; i++) {
+ i_assert(nodes[i].seq <= ctx->last_seq);
+
+ T_BEGIN {
+ if (index_sort_header_get(ctx->program, nodes[i].seq,
+ sort_type, str) < 0) {
+ nodes[i].no_update = TRUE;
+ ctx->failed = TRUE;
+ }
+ ctx->sort_strings[nodes[i].seq] =
+ str_len(str) == 0 ? "" :
+ p_strdup(pool, str_c(str));
+ } T_END;
+ }
+ str_free(&str);
+
+ /* we have all strings, sort nodes based on them */
+ static_zero_cmp_context = ctx;
+ array_sort(&ctx->zero_nodes, sort_node_zero_string_cmp);
+}
+
+static bool
+index_sort_get_expunged_string(struct sort_string_context *ctx, uint32_t idx,
+ string_t *str, const char **result_r)
+{
+ enum mail_sort_type sort_type = ctx->program->sort_program[0];
+ const struct mail_sort_node *nodes;
+ const char *result = NULL;
+ unsigned int i, count;
+ uint32_t sort_id;
+
+ /* Look forwards and backwards to see if there are
+ identical sort_ids. If we do find them, try to get
+ their sort string and use it to update the rest. */
+ nodes = array_get(&ctx->nonzero_nodes, &count);
+ sort_id = nodes[idx].sort_id;
+ /* If previous sort ID is identical and its sort string is set, we can
+ trust it. If it's expunged, we already verified that there are no
+ non-expunged messages. */
+ if (idx > 0 && nodes[idx-1].sort_id == sort_id &&
+ ctx->sort_strings[nodes[idx].seq] != NULL) {
+ *result_r = ctx->sort_strings[nodes[idx].seq];
+ return TRUE;
+ }
+
+ /* Go forwards as long as there are identical sort IDs. If we find one
+ that's not expunged, update string table for all messages with
+ identical sort IDs. */
+ for (i = idx + 1; i < count; i++) {
+ if (nodes[i].sort_id != sort_id)
+ break;
+
+ if (ctx->sort_strings[nodes[i].seq] != NULL) {
+ /* usually we fill all identical sort_ids and this
+ shouldn't happen, but we can get here if we skipped
+ over messages when binary searching */
+ result = ctx->sort_strings[nodes[i].seq];
+ break;
+ }
+ if (index_sort_header_get(ctx->program, nodes[i].seq,
+ sort_type, str) > 0) {
+ result = str_len(str) == 0 ? "" :
+ p_strdup(ctx->sort_string_pool, str_c(str));
+ break;
+ }
+ }
+ if (result == NULL) {
+ /* unknown */
+ return FALSE;
+ }
+
+ /* fill all identical sort_ids with the same value */
+ for (i = idx; i > 0 && nodes[i-1].sort_id == sort_id; i--) ;
+ for (i = idx; i < count && nodes[i].sort_id == sort_id; i++)
+ ctx->sort_strings[nodes[i].seq] = result;
+ *result_r = result;
+ return TRUE;
+}
+
+static bool
+index_sort_get_string(struct sort_string_context *ctx,
+ uint32_t idx, struct mail_sort_node *node,
+ const char **str_r)
+{
+ uint32_t seq = node->seq;
+ int ret = 1;
+
+ if (node->no_update) {
+ /* we've already determined that we can't do this lookup */
+ *str_r = ctx->sort_strings[seq];
+ return FALSE;
+ }
+
+ if (ctx->sort_strings[seq] == NULL) T_BEGIN {
+ string_t *str;
+ const char *result;
+
+ str = t_str_new(256);
+ ret = index_sort_header_get(ctx->program, seq,
+ ctx->program->sort_program[0], str);
+ if (ret < 0)
+ ctx->failed = TRUE;
+ else if (ret == 0) {
+ if (!index_sort_get_expunged_string(ctx, idx, str, &result))
+ ctx->sort_strings[seq] = "";
+ else {
+ /* found the expunged string - return success */
+ ctx->sort_strings[seq] = result;
+ ret = 1;
+ }
+ } else {
+ ctx->sort_strings[seq] = str_len(str) == 0 ? "" :
+ p_strdup(ctx->sort_string_pool, str_c(str));
+ }
+ } T_END;
+
+ if (ret <= 0)
+ node->no_update = TRUE;
+ *str_r = ctx->sort_strings[seq];
+ return ret > 0;
+}
+
+static void
+index_sort_bsearch(struct sort_string_context *ctx, const char *key,
+ unsigned int start_idx, unsigned int *idx_r,
+ const char **prev_str_r)
+{
+ struct mail_sort_node *nodes;
+ const char *str, *str2;
+ unsigned int idx, left_idx, right_idx, prev;
+ int ret;
+
+ nodes = array_get_modifiable(&ctx->nonzero_nodes, &right_idx);
+ i_assert(right_idx < INT_MAX);
+ idx = left_idx = start_idx;
+ while (left_idx < right_idx) {
+ idx = (left_idx + right_idx) / 2;
+ if (index_sort_get_string(ctx, idx, &nodes[idx], &str))
+ ret = strcmp(key, str);
+ else {
+ /* put expunged (and otherwise failed) messages first */
+ ret = 1;
+ for (prev = idx; prev > 0; ) {
+ prev--;
+ if (index_sort_get_string(ctx, prev,
+ &nodes[prev],
+ &str2)) {
+ ret = strcmp(key, str2);
+ if (ret <= 0) {
+ idx = prev;
+ str = str2;
+ }
+ break;
+ }
+ }
+ }
+ if (ret > 0)
+ left_idx = idx+1;
+ else if (ret < 0)
+ right_idx = idx;
+ else {
+ *idx_r = idx + 1;
+ *prev_str_r = str;
+ return;
+ }
+ }
+
+ if (left_idx > idx)
+ idx++;
+
+ *idx_r = idx;
+ if (idx > start_idx) {
+ bool success;
+
+ prev = idx;
+ do {
+ prev--;
+ success = index_sort_get_string(ctx, prev,
+ &nodes[prev], &str2);
+ } while (!success && prev > 0 &&
+ nodes[prev-1].sort_id == nodes[prev].sort_id);
+ *prev_str_r = str2;
+ }
+}
+
+static void index_sort_merge(struct sort_string_context *ctx)
+{
+ struct mail_sort_node *znodes, *nznodes;
+ const char *zstr, *nzstr, *prev_str;
+ unsigned int zpos, nzpos, nz_next_pos, zcount, nzcount;
+ int ret;
+
+ /* both zero_nodes and nonzero_nodes are sorted. we'll now just have
+ to merge them together. use sorted_nodes as the result array. */
+ i_array_init(&ctx->sorted_nodes, array_count(&ctx->nonzero_nodes) +
+ array_count(&ctx->zero_nodes));
+
+ znodes = array_get_modifiable(&ctx->zero_nodes, &zcount);
+ nznodes = array_get_modifiable(&ctx->nonzero_nodes, &nzcount);
+
+ prev_str = NULL;
+ for (zpos = nzpos = 0; zpos < zcount && nzpos < nzcount; ) {
+ zstr = ctx->sort_strings[znodes[zpos].seq];
+ if (index_sort_get_string(ctx, nzpos, &nznodes[nzpos], &nzstr))
+ ret = strcmp(zstr, nzstr);
+ else if (prev_str != NULL && strcmp(zstr, prev_str) == 0) {
+ /* identical to previous message, must keep them
+ together */
+ ret = -1;
+ } else {
+ /* we can't be yet sure about the order, but future
+ nznodes may reveal that the znode must be added
+ later. if future nznodes don't reveal that, we have
+ no idea about these nodes' order. so just always
+ put the expunged message first. */
+ ret = 1;
+ }
+
+ if (ret == 0) {
+ ret = index_sort_node_cmp_type(ctx->program,
+ ctx->program->sort_program + 1,
+ znodes[zpos].seq, nznodes[nzpos].seq);
+ }
+ if (ret <= 0) {
+ array_push_back(&ctx->sorted_nodes, &znodes[zpos]);
+ prev_str = zstr;
+ zpos++;
+ } else {
+ array_push_back(&ctx->sorted_nodes, &nznodes[nzpos]);
+ prev_str = nzstr;
+ nzpos++;
+
+ /* avoid looking up all existing messages' strings by
+ binary searching the next zero-node position. don't
+ bother if it looks like more work than linear
+ scanning. */
+ if (zcount - zpos < (nzcount - nzpos)/2) {
+ index_sort_bsearch(ctx, zstr, nzpos,
+ &nz_next_pos, &prev_str);
+ array_append(&ctx->sorted_nodes,
+ &nznodes[nzpos],
+ nz_next_pos - nzpos);
+ nzpos = nz_next_pos;
+ }
+ }
+ }
+ /* only one of zero_nodes and nonzero_nodes can be non-empty now */
+ for (; zpos < zcount; zpos++)
+ array_push_back(&ctx->sorted_nodes, &znodes[zpos]);
+ for (; nzpos < nzcount; nzpos++)
+ array_push_back(&ctx->sorted_nodes, &nznodes[nzpos]);
+
+ /* future index_sort_get_string() calls use ctx->nonzero_nodes, but we
+ use only ctx->sorted_nodes. make them identical. */
+ array_free(&ctx->nonzero_nodes);
+ ctx->nonzero_nodes = ctx->sorted_nodes;
+}
+
+static int
+index_sort_add_ids_range(struct sort_string_context *ctx,
+ unsigned int left_idx, unsigned int right_idx,
+ const char **reason_r)
+{
+
+ struct mail_sort_node *nodes;
+ unsigned int i, count, rightmost_idx, skip;
+ const char *left_str = NULL, *right_str = NULL, *str = NULL;
+ uint32_t left_sort_id, right_sort_id, diff, left_str_idx = 0;
+ bool no_left_str = FALSE, no_right_str = FALSE;
+ int ret;
+
+ nodes = array_get_modifiable(&ctx->sorted_nodes, &count);
+ rightmost_idx = count - 1;
+
+ /* get the sort IDs from left and right */
+ left_sort_id = nodes[left_idx].sort_id;
+ right_sort_id = nodes[right_idx].sort_id;
+ /* check if all of them should have the same sort IDs. we don't want
+ to hit the renumbering code in that situation. */
+ if (left_sort_id == right_sort_id && left_sort_id != 0) {
+ /* they should all have the same sort ID */
+ for (i = left_idx + 1; i < right_idx; i++) {
+ nodes[i].sort_id = left_sort_id;
+ nodes[i].sort_id_changed = TRUE;
+ }
+ return 0;
+ }
+
+ if (left_sort_id == 0) {
+ i_assert(left_idx == 0);
+ left_sort_id = 1;
+ }
+ if (right_sort_id == 0) {
+ i_assert(right_idx == rightmost_idx);
+ right_sort_id = (uint32_t)-1;
+ }
+ i_assert(left_sort_id <= right_sort_id);
+
+ diff = right_sort_id - left_sort_id;
+ while (diff / (right_idx-left_idx + 2) == 0) {
+ /* we most likely don't have enough space. we have to
+ renumber some of the existing sort IDs. do this by
+ widening the area we're giving sort IDs. */
+ while (left_idx > 0) {
+ if (nodes[--left_idx].sort_id != left_sort_id) {
+ left_sort_id = nodes[left_idx].sort_id;
+ if (left_sort_id == 0) {
+ i_assert(left_idx == 0);
+ left_sort_id = 1;
+ }
+ break;
+ }
+ }
+
+ while (right_idx < rightmost_idx) {
+ right_idx++;
+ if (nodes[right_idx].sort_id > right_sort_id)
+ break;
+ }
+ right_sort_id = nodes[right_idx].sort_id;
+ if (right_sort_id == 0) {
+ i_assert(right_idx == rightmost_idx);
+ right_sort_id = (uint32_t)-1;
+ }
+ i_assert(left_sort_id <= right_sort_id);
+
+ if (diff == right_sort_id - left_sort_id) {
+ /* we did nothing, but there's still not enough space.
+ have to renumber the leftmost/rightmost node(s) */
+ i_assert(left_idx == 0 && right_idx == rightmost_idx);
+ if (left_sort_id > 1) {
+ left_sort_id = 1;
+ no_left_str = TRUE;
+ } else {
+ i_assert(right_sort_id != (uint32_t)-1);
+ right_sort_id = (uint32_t)-1;
+ no_right_str = TRUE;
+ }
+ }
+ diff = right_sort_id - left_sort_id;
+ }
+
+ if (nodes[left_idx].sort_id != 0 && !no_left_str) {
+ if (!index_sort_get_string(ctx, left_idx,
+ &nodes[left_idx], &left_str)) {
+ /* not equivalent with any message */
+ left_str = NULL;
+ } else {
+ left_str_idx = left_idx;
+ }
+ left_idx++;
+ }
+ if (nodes[right_idx].sort_id != 0 && !no_right_str) {
+ if (!index_sort_get_string(ctx, right_idx,
+ &nodes[right_idx], &right_str)) {
+ /* not equivalent with any message */
+ right_str = NULL;
+ }
+ right_idx--;
+ }
+ i_assert(left_idx <= right_idx);
+
+ /* give (new) sort IDs to all nodes in left_idx..right_idx range.
+ divide the available space so that each message gets an equal sized
+ share. some messages' sort strings may be equivalent, so give them
+ the same sort IDs. */
+ for (i = left_idx; i <= right_idx; i++) {
+ if (!index_sort_get_string(ctx, i, &nodes[i], &str)) {
+ /* it doesn't really matter what we give to this
+ message, since it's only temporary and we don't
+ know its correct position anyway. so let's assume
+ it's equivalent to previous message. */
+ nodes[i].sort_id = left_sort_id;
+ continue;
+ }
+
+ ret = left_str == NULL ? 1 : strcmp(str, left_str);
+ if (ret <= 0) {
+ if (ret < 0) {
+ /* broken sort_ids */
+ uint32_t str_uid, left_str_uid;
+
+ mail_index_lookup_uid(ctx->program->t->view,
+ nodes[i].seq, &str_uid);
+ mail_index_lookup_uid(ctx->program->t->view,
+ nodes[left_str_idx].seq, &left_str_uid);
+ *reason_r = t_strdup_printf(
+ "(idx=%u, seq=%u, uid=%u) '%s' < left string (idx=%u, seq=%u, uid=%u) '%s'",
+ i, nodes[i].seq, str_uid, str,
+ left_str_idx, nodes[left_str_idx].seq, left_str_uid, left_str);
+ return -1;
+ }
+ nodes[i].sort_id = left_sort_id;
+ } else if (right_str != NULL && strcmp(str, right_str) == 0) {
+ /* the rest of the sort IDs should be the same */
+ nodes[i].sort_id = right_sort_id;
+ left_sort_id = right_sort_id;
+ } else {
+ /* divide the available space equally. leave the same
+ sized space also between the first and the last
+ messages */
+ skip = (right_sort_id - left_sort_id) /
+ (right_idx - i + 2);
+ if (skip == 0) {
+ /* broken sort IDs (we previously assigned
+ left_sort_id=right_sort_id) */
+ uint32_t uid;
+ mail_index_lookup_uid(ctx->program->t->view,
+ nodes[i].seq, &uid);
+ *reason_r = t_strdup_printf(
+ "no sort_id space for uid=%u", uid);
+ return -1;
+ }
+ left_sort_id += skip;
+ i_assert(left_sort_id < right_sort_id);
+
+ nodes[i].sort_id = left_sort_id;
+ left_str = str;
+ left_str_idx = i;
+ }
+ nodes[i].sort_id_changed = TRUE;
+ }
+ i_assert(str != NULL);
+
+ if (right_str == NULL || strcmp(str, right_str) < 0 ||
+ (strcmp(str, right_str) == 0 &&
+ nodes[i-1].sort_id == right_sort_id))
+ return 0;
+
+ *reason_r = t_strdup_printf("Invalid sort_id order ('%s' > '%s')",
+ str, right_str);
+ return -1;
+}
+
+static int
+index_sort_add_sort_ids(struct sort_string_context *ctx, const char **reason_r)
+{
+ const struct mail_sort_node *nodes;
+ unsigned int i, left_idx, right_idx, count;
+
+ nodes = array_get(&ctx->sorted_nodes, &count);
+ for (i = 0; i < count; i++) {
+ if (nodes[i].sort_id != 0)
+ continue;
+
+ /* get the range for all sort_id=0 nodes. include the nodes
+ left and right of the range as well */
+ for (right_idx = i + 1; right_idx < count; right_idx++) {
+ if (nodes[right_idx].sort_id != 0)
+ break;
+ }
+ if (right_idx == count)
+ right_idx--;
+ left_idx = i == 0 ? 0 : i - 1;
+ if (index_sort_add_ids_range(ctx, left_idx, right_idx, reason_r) < 0)
+ return -1;
+ }
+ return 0;
+}
+
+static void index_sort_write_changed_sort_ids(struct sort_string_context *ctx)
+{
+ struct mail_index_transaction *itrans = ctx->program->t->itrans;
+ uint32_t ext_id = ctx->ext_id;
+ const struct mail_sort_node *nodes;
+ unsigned int i, count;
+ uint32_t lowest_failed_seq;
+
+ if (ctx->no_writing) {
+ /* our reset_id is already stale - don't even bother
+ trying to write */
+ return;
+ }
+
+ mail_index_ext_reset_inc(itrans, ext_id,
+ ctx->highest_reset_id, FALSE);
+
+ /* We require that there aren't sort_id=0 gaps in the middle of the
+ mails. At this point they could exist though, because some of the
+ mail lookups may have failed. Failures due to expunges don't matter,
+ because on the next lookup those mails will be lost anyway.
+ Otherwise, make sure we don't write those gaps out
+
+ First find the lowest non-expunged mail that has no_update set. */
+ nodes = array_get_modifiable(&ctx->sorted_nodes, &count);
+ lowest_failed_seq = (uint32_t)-1;
+ for (i = 0; i < count; i++) {
+ uint32_t seq = nodes[i].seq;
+
+ if (nodes[i].no_update && lowest_failed_seq > seq &&
+ !mail_index_is_expunged(ctx->program->t->view, seq))
+ lowest_failed_seq = seq;
+ }
+
+ /* add the missing sort IDs to index, but only for those sequences
+ that are below lowest_failed_seq */
+ nodes = array_get_modifiable(&ctx->sorted_nodes, &count);
+ for (i = 0; i < count; i++) {
+ i_assert(nodes[i].sort_id != 0);
+ if (!nodes[i].sort_id_changed || nodes[i].no_update ||
+ nodes[i].seq >= lowest_failed_seq)
+ continue;
+
+ mail_index_update_ext(itrans, nodes[i].seq, ext_id,
+ &nodes[i].sort_id, NULL);
+ }
+}
+
+static int sort_node_cmp(const struct mail_sort_node *n1,
+ const struct mail_sort_node *n2)
+{
+ struct sort_string_context *ctx = static_zero_cmp_context;
+
+ if (n1->sort_id < n2->sort_id)
+ return !ctx->reverse ? -1 : 1;
+ if (n1->sort_id > n2->sort_id)
+ return !ctx->reverse ? 1 : -1;
+
+ return index_sort_node_cmp_type(ctx->program,
+ ctx->program->sort_program + 1,
+ n1->seq, n2->seq);
+}
+
+static void index_sort_add_missing(struct sort_string_context *ctx)
+{
+ struct mail_sort_node node;
+ const uint32_t *seqs;
+ unsigned int i, count;
+ uint32_t seq, next_seq;
+
+ ctx->have_all_wanted = TRUE;
+
+ seqs = array_get(&ctx->program->seqs, &count);
+ for (i = 0, next_seq = 1; i < count; i++) {
+ if (seqs[i] == next_seq)
+ next_seq++;
+ else {
+ i_assert(next_seq < seqs[i]);
+ for (seq = next_seq; seq < seqs[i]; seq++) {
+ i_zero(&node);
+ node.seq = seq;
+ index_sort_node_add(ctx, &node);
+ }
+ next_seq = seqs[i] + 1;
+ }
+ }
+
+ if (ctx->lowest_nonexpunged_zero == 0) {
+ /* we're handling only expunged zeros. if it causes us to
+ renumber some existing sort IDs, don't save them. */
+ ctx->no_writing = TRUE;
+ }
+}
+
+static void index_sort_list_reset_broken(struct sort_string_context *ctx,
+ const char *reason)
+{
+ struct mailbox *box = ctx->program->t->box;
+ struct mail_sort_node *node;
+
+ mailbox_set_critical(box, "Broken %s indexes, resetting: %s",
+ ctx->primary_sort_name, reason);
+
+ array_clear(&ctx->zero_nodes);
+ array_append_array(&ctx->zero_nodes,
+ &ctx->nonzero_nodes);
+ array_clear(&ctx->nonzero_nodes);
+
+ array_foreach_modifiable(&ctx->zero_nodes, node)
+ node->sort_id = 0;
+}
+
+void index_sort_list_finish_string(struct mail_search_sort_program *program)
+{
+ struct sort_string_context *ctx = program->context;
+ const struct mail_sort_node *nodes;
+ unsigned int i, count;
+ const char *reason;
+ uint32_t seq;
+
+ static_zero_cmp_context = ctx;
+ if (array_count(&ctx->zero_nodes) == 0) {
+ /* fast path: we have all sort IDs */
+ array_sort(&ctx->nonzero_nodes, sort_node_cmp);
+
+ nodes = array_get(&ctx->nonzero_nodes, &count);
+ if (!array_is_created(&program->seqs))
+ i_array_init(&program->seqs, count);
+ else
+ array_clear(&program->seqs);
+
+ for (i = 0; i < count; i++) {
+ seq = nodes[i].seq;
+ array_push_back(&program->seqs, &seq);
+ }
+ array_free(&ctx->nonzero_nodes);
+ } else {
+ if (ctx->seqs_nonsorted) {
+ /* the nodes need to be sorted by sequence initially */
+ array_sort(&ctx->zero_nodes, sort_node_seq_cmp);
+ array_sort(&ctx->nonzero_nodes, sort_node_seq_cmp);
+ }
+
+ /* we have to add some sort IDs. we'll do this for all
+ messages, so first remember what messages we wanted
+ to know about. */
+ index_sort_generate_seqs(ctx);
+ /* add messages not in seqs list */
+ index_sort_add_missing(ctx);
+ /* sort all messages with sort IDs */
+ array_sort(&ctx->nonzero_nodes, sort_node_cmp);
+ for (;;) {
+ /* sort all messages without sort IDs */
+ index_sort_zeroes(ctx);
+
+ if (ctx->reverse) {
+ /* sort lists are descending currently, but
+ merging and sort ID assigning works only
+ with ascending lists. reverse the lists
+ temporarily. we can't do this while earlier
+ because secondary sort conditions must not
+ be reversed in results (but while assigning
+ sort IDs it doesn't matter). */
+ array_reverse(&ctx->nonzero_nodes);
+ array_reverse(&ctx->zero_nodes);
+ }
+
+ /* merge zero and non-zero arrays into sorted_nodes */
+ index_sort_merge(ctx);
+ /* give sort IDs to messages missing them */
+ if (index_sort_add_sort_ids(ctx, &reason) == 0)
+ break;
+
+ /* broken, try again with sort IDs reset */
+ index_sort_list_reset_broken(ctx, reason);
+ }
+ index_sort_write_changed_sort_ids(ctx);
+
+ if (ctx->reverse) {
+ /* restore the correct sort order */
+ array_reverse(&ctx->sorted_nodes);
+ }
+
+ nodes = array_get(&ctx->sorted_nodes, &count);
+ array_clear(&program->seqs);
+ for (i = 0; i < count; i++) {
+ if (nodes[i].wanted) {
+ seq = nodes[i].seq;
+ array_push_back(&program->seqs, &seq);
+ }
+ }
+ pool_unref(&ctx->sort_string_pool);
+ i_free(ctx->sort_strings);
+ array_free(&ctx->sorted_nodes);
+ /* NOTE: we already freed nonzero_nodes and made it point to
+ sorted_nodes. */
+ }
+ if (ctx->failed)
+ program->failed = TRUE;
+
+ array_free(&ctx->zero_nodes);
+ i_free(ctx);
+ program->context = NULL;
+}
diff --git a/src/lib-storage/index/index-sort.c b/src/lib-storage/index/index-sort.c
new file mode 100644
index 0000000..924a8d1
--- /dev/null
+++ b/src/lib-storage/index/index-sort.c
@@ -0,0 +1,738 @@
+/* Copyright (c) 2006-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "str.h"
+#include "unichar.h"
+#include "message-address.h"
+#include "message-header-decode.h"
+#include "imap-base-subject.h"
+#include "index-storage.h"
+#include "index-sort-private.h"
+
+
+struct mail_sort_node_date {
+ uint32_t seq;
+ time_t date;
+};
+ARRAY_DEFINE_TYPE(mail_sort_node_date, struct mail_sort_node_date);
+
+struct mail_sort_node_size {
+ uint32_t seq;
+ uoff_t size;
+};
+ARRAY_DEFINE_TYPE(mail_sort_node_size, struct mail_sort_node_size);
+
+struct mail_sort_node_float {
+ uint32_t seq;
+ float num;
+};
+ARRAY_DEFINE_TYPE(mail_sort_node_float, struct mail_sort_node_float);
+
+struct sort_cmp_context {
+ struct mail_search_sort_program *program;
+ bool reverse;
+};
+
+static struct sort_cmp_context static_node_cmp_context;
+
+static void
+index_sort_program_set_mail_failed(struct mail_search_sort_program *program,
+ struct mail *mail)
+{
+ switch (mailbox_get_last_mail_error(mail->box)) {
+ case MAIL_ERROR_EXPUNGED:
+ break;
+ case MAIL_ERROR_LOOKUP_ABORTED:
+ /* just change the error message */
+ i_assert(program->slow_mails_left == 0);
+ mail_storage_set_error(program->t->box->storage, MAIL_ERROR_LIMIT,
+ "Requested sort would have taken too long.");
+ /* fall through */
+ default:
+ program->failed = TRUE;
+ break;
+ }
+}
+
+static time_t
+index_sort_program_set_date_failed(struct mail_search_sort_program *program,
+ struct mail *mail)
+{
+ index_sort_program_set_mail_failed(program, mail);
+
+ if (mailbox_get_last_mail_error(mail->box) == MAIL_ERROR_LIMIT) {
+ /* limit reached - sort the rest of the mails at the end of
+ the list by their UIDs */
+ return LONG_MAX;
+ } else {
+ /* expunged / some other error - sort in the beginning */
+ return 0;
+ }
+}
+
+static void
+index_sort_list_add_arrival(struct mail_search_sort_program *program,
+ struct mail *mail)
+{
+ ARRAY_TYPE(mail_sort_node_date) *nodes = program->context;
+ struct mail_sort_node_date *node;
+
+ node = array_append_space(nodes);
+ node->seq = mail->seq;
+ if (mail_get_received_date(mail, &node->date) < 0)
+ node->date = index_sort_program_set_date_failed(program, mail);
+}
+
+static void
+index_sort_list_add_date(struct mail_search_sort_program *program,
+ struct mail *mail)
+{
+ ARRAY_TYPE(mail_sort_node_date) *nodes = program->context;
+ struct mail_sort_node_date *node;
+ int tz;
+
+ node = array_append_space(nodes);
+ node->seq = mail->seq;
+ if (mail_get_date(mail, &node->date, &tz) < 0)
+ node->date = index_sort_program_set_date_failed(program, mail);
+ else if (node->date == 0) {
+ if (mail_get_received_date(mail, &node->date) < 0)
+ node->date = index_sort_program_set_date_failed(program, mail);
+ }
+}
+
+static void
+index_sort_list_add_size(struct mail_search_sort_program *program,
+ struct mail *mail)
+{
+ ARRAY_TYPE(mail_sort_node_size) *nodes = program->context;
+ struct mail_sort_node_size *node;
+
+ node = array_append_space(nodes);
+ node->seq = mail->seq;
+ if (mail_get_virtual_size(mail, &node->size) < 0) {
+ index_sort_program_set_mail_failed(program, mail);
+ node->size = 0;
+ }
+}
+
+static int index_sort_get_pop3_order(struct mail *mail, uoff_t *size_r)
+{
+ const char *str;
+
+ if (mail_get_special(mail, MAIL_FETCH_POP3_ORDER, &str) < 0) {
+ *size_r = (uint32_t)-1;
+ return -1;
+ }
+
+ if (str_to_uoff(str, size_r) < 0)
+ *size_r = (uint32_t)-1;
+ return 0;
+}
+
+static void
+index_sort_list_add_pop3_order(struct mail_search_sort_program *program,
+ struct mail *mail)
+{
+ ARRAY_TYPE(mail_sort_node_size) *nodes = program->context;
+ struct mail_sort_node_size *node;
+
+ node = array_append_space(nodes);
+ node->seq = mail->seq;
+ (void)index_sort_get_pop3_order(mail, &node->size);
+}
+
+static int index_sort_get_relevancy(struct mail *mail, float *result_r)
+{
+ const char *str;
+
+ if (mail_get_special(mail, MAIL_FETCH_SEARCH_RELEVANCY, &str) < 0) {
+ *result_r = 0;
+ return -1;
+ }
+ *result_r = strtod(str, NULL);
+ return 0;
+}
+
+static void
+index_sort_list_add_relevancy(struct mail_search_sort_program *program,
+ struct mail *mail)
+{
+ ARRAY_TYPE(mail_sort_node_float) *nodes = program->context;
+ struct mail_sort_node_float *node;
+
+ node = array_append_space(nodes);
+ node->seq = mail->seq;
+ (void)index_sort_get_relevancy(mail, &node->num);
+}
+
+void index_sort_list_add(struct mail_search_sort_program *program,
+ struct mail *mail)
+{
+ enum mail_access_type orig_access_type = mail->access_type;
+ bool prev_slow = mail->mail_stream_accessed ||
+ mail->mail_metadata_accessed;
+
+ i_assert(mail->transaction == program->t);
+ /* if lookup_abort isn't NEVER, mail_sort_max_read_count handling
+ doesn't work right. */
+ i_assert(mail->lookup_abort == MAIL_LOOKUP_ABORT_NEVER);
+
+ if (program->slow_mails_left == 0)
+ mail->lookup_abort = MAIL_LOOKUP_ABORT_NOT_IN_CACHE;
+
+ mail->access_type = MAIL_ACCESS_TYPE_SORT;
+ T_BEGIN {
+ program->sort_list_add(program, mail);
+ } T_END;
+ mail->access_type = orig_access_type;
+
+ if (!prev_slow && (mail->mail_stream_accessed ||
+ mail->mail_metadata_accessed)) {
+ i_assert(program->slow_mails_left > 0);
+ program->slow_mails_left--;
+ }
+ mail->lookup_abort = MAIL_LOOKUP_ABORT_NEVER;
+}
+
+static int sort_node_date_cmp(const struct mail_sort_node_date *n1,
+ const struct mail_sort_node_date *n2)
+{
+ struct sort_cmp_context *ctx = &static_node_cmp_context;
+
+ if (n1->date < n2->date)
+ return !ctx->reverse ? -1 : 1;
+ if (n1->date > n2->date)
+ return !ctx->reverse ? 1 : -1;
+
+ return index_sort_node_cmp_type(ctx->program,
+ ctx->program->sort_program + 1,
+ n1->seq, n2->seq);
+}
+
+static void
+index_sort_list_finish_date(struct mail_search_sort_program *program)
+{
+ ARRAY_TYPE(mail_sort_node_date) *nodes = program->context;
+
+ array_sort(nodes, sort_node_date_cmp);
+ memcpy(&program->seqs, nodes, sizeof(program->seqs));
+ i_free(nodes);
+ program->context = NULL;
+}
+
+static int sort_node_size_cmp(const struct mail_sort_node_size *n1,
+ const struct mail_sort_node_size *n2)
+{
+ struct sort_cmp_context *ctx = &static_node_cmp_context;
+
+ if (n1->size < n2->size)
+ return !ctx->reverse ? -1 : 1;
+ if (n1->size > n2->size)
+ return !ctx->reverse ? 1 : -1;
+
+ return index_sort_node_cmp_type(ctx->program,
+ ctx->program->sort_program + 1,
+ n1->seq, n2->seq);
+}
+
+static void
+index_sort_list_finish_size(struct mail_search_sort_program *program)
+{
+ ARRAY_TYPE(mail_sort_node_size) *nodes = program->context;
+
+ array_sort(nodes, sort_node_size_cmp);
+ memcpy(&program->seqs, nodes, sizeof(program->seqs));
+ i_free(nodes);
+ program->context = NULL;
+}
+
+static int sort_node_float_cmp(const struct mail_sort_node_float *n1,
+ const struct mail_sort_node_float *n2)
+{
+ struct sort_cmp_context *ctx = &static_node_cmp_context;
+
+ if (n1->num < n2->num)
+ return !ctx->reverse ? -1 : 1;
+ if (n1->num > n2->num)
+ return !ctx->reverse ? 1 : -1;
+
+ return index_sort_node_cmp_type(ctx->program,
+ ctx->program->sort_program + 1,
+ n1->seq, n2->seq);
+}
+
+static void
+index_sort_list_finish_float(struct mail_search_sort_program *program)
+{
+ ARRAY_TYPE(mail_sort_node_float) *nodes = program->context;
+
+ /* NOTE: higher relevancy is returned first, unlike with all
+ other number based sort keys, so temporarily reverse the search */
+ static_node_cmp_context.reverse = !static_node_cmp_context.reverse;
+ array_sort(nodes, sort_node_float_cmp);
+ static_node_cmp_context.reverse = !static_node_cmp_context.reverse;
+
+ memcpy(&program->seqs, nodes, sizeof(program->seqs));
+ i_free(nodes);
+ program->context = NULL;
+}
+
+void index_sort_list_finish(struct mail_search_sort_program *program)
+{
+ i_zero(&static_node_cmp_context);
+ static_node_cmp_context.program = program;
+ static_node_cmp_context.reverse =
+ (program->sort_program[0] & MAIL_SORT_FLAG_REVERSE) != 0;
+
+ struct event_reason *reason = event_reason_begin("mailbox:sort");
+ program->sort_list_finish(program);
+ event_reason_end(&reason);
+}
+
+bool index_sort_list_next(struct mail_search_sort_program *program,
+ uint32_t *seq_r)
+{
+ const uint32_t *seqp;
+
+ if (program->iter_idx == array_count(&program->seqs))
+ return FALSE;
+
+ seqp = array_idx(&program->seqs, program->iter_idx++);
+ *seq_r = *seqp;
+ return TRUE;
+}
+
+static void
+get_wanted_fields(struct mailbox *box, const enum mail_sort_type *sort_program,
+ enum mail_fetch_field *wanted_fields_r,
+ struct mailbox_header_lookup_ctx **wanted_headers_r)
+{
+ enum mail_fetch_field fields = 0;
+ ARRAY_TYPE(const_string) headers;
+ const char *hdr_name;
+
+ t_array_init(&headers, 4);
+ for (unsigned int i = 0; sort_program[i] != MAIL_SORT_END; i++) {
+ enum mail_sort_type sort_type =
+ sort_program[i] & MAIL_SORT_MASK;
+ switch (sort_type) {
+ case MAIL_SORT_ARRIVAL:
+ fields |= MAIL_FETCH_RECEIVED_DATE;
+ break;
+ case MAIL_SORT_CC:
+ hdr_name = "Cc";
+ array_push_back(&headers, &hdr_name);
+ break;
+ case MAIL_SORT_DATE:
+ fields |= MAIL_FETCH_DATE;
+ break;
+ case MAIL_SORT_FROM:
+ case MAIL_SORT_DISPLAYFROM:
+ hdr_name = "From";
+ array_push_back(&headers, &hdr_name);
+ break;
+ case MAIL_SORT_SIZE:
+ fields |= MAIL_FETCH_VIRTUAL_SIZE;
+ break;
+ case MAIL_SORT_SUBJECT:
+ hdr_name = "Subject";
+ array_push_back(&headers, &hdr_name);
+ break;
+ case MAIL_SORT_TO:
+ case MAIL_SORT_DISPLAYTO:
+ hdr_name = "To";
+ array_push_back(&headers, &hdr_name);
+ break;
+ case MAIL_SORT_RELEVANCY:
+ fields |= MAIL_FETCH_SEARCH_RELEVANCY;
+ break;
+ case MAIL_SORT_POP3_ORDER:
+ fields |= MAIL_FETCH_POP3_ORDER;
+ break;
+
+ case MAIL_SORT_MASK:
+ case MAIL_SORT_FLAG_REVERSE:
+ case MAIL_SORT_END:
+ i_unreached();
+ }
+ }
+ *wanted_fields_r = fields;
+ if (array_count(&headers) == 0)
+ *wanted_headers_r = NULL;
+ else {
+ array_append_zero(&headers);
+ *wanted_headers_r =
+ mailbox_header_lookup_init(box, array_idx(&headers, 0));
+ }
+}
+
+struct mail_search_sort_program *
+index_sort_program_init(struct mailbox_transaction_context *t,
+ const enum mail_sort_type *sort_program)
+{
+ struct mail_search_sort_program *program;
+ enum mail_fetch_field wanted_fields;
+ struct mailbox_header_lookup_ctx *wanted_headers;
+ unsigned int i;
+
+ if (sort_program == NULL || sort_program[0] == MAIL_SORT_END)
+ return NULL;
+
+ get_wanted_fields(t->box, sort_program, &wanted_fields, &wanted_headers);
+
+ /* we support internal sorting by the primary condition */
+ program = i_new(struct mail_search_sort_program, 1);
+ program->t = t;
+ program->temp_mail = mail_alloc(t, wanted_fields, wanted_headers);
+ program->temp_mail->access_type = MAIL_ACCESS_TYPE_SORT;
+ if (wanted_headers != NULL)
+ mailbox_header_lookup_unref(&wanted_headers);
+
+ program->slow_mails_left =
+ program->t->box->storage->set->mail_sort_max_read_count;
+ if (program->slow_mails_left == 0)
+ program->slow_mails_left = UINT_MAX;
+
+ for (i = 0; i < MAX_SORT_PROGRAM_SIZE; i++) {
+ program->sort_program[i] = sort_program[i];
+ if (sort_program[i] == MAIL_SORT_END)
+ break;
+ }
+ if (i == MAX_SORT_PROGRAM_SIZE)
+ i_panic("index_sort_program_init(): Invalid sort program");
+
+ switch (program->sort_program[0] & MAIL_SORT_MASK) {
+ case MAIL_SORT_ARRIVAL:
+ case MAIL_SORT_DATE: {
+ ARRAY_TYPE(mail_sort_node_date) *nodes;
+
+ nodes = i_malloc(sizeof(*nodes));
+ i_array_init(nodes, 128);
+
+ if ((program->sort_program[0] &
+ MAIL_SORT_MASK) == MAIL_SORT_ARRIVAL)
+ program->sort_list_add = index_sort_list_add_arrival;
+ else
+ program->sort_list_add = index_sort_list_add_date;
+ program->sort_list_finish = index_sort_list_finish_date;
+ program->context = nodes;
+ break;
+ }
+ case MAIL_SORT_SIZE: {
+ ARRAY_TYPE(mail_sort_node_size) *nodes;
+
+ nodes = i_malloc(sizeof(*nodes));
+ i_array_init(nodes, 128);
+ program->sort_list_add = index_sort_list_add_size;
+ program->sort_list_finish = index_sort_list_finish_size;
+ program->context = nodes;
+ break;
+ }
+ case MAIL_SORT_CC:
+ case MAIL_SORT_FROM:
+ case MAIL_SORT_SUBJECT:
+ case MAIL_SORT_TO:
+ case MAIL_SORT_DISPLAYFROM:
+ case MAIL_SORT_DISPLAYTO:
+ program->sort_list_add = index_sort_list_add_string;
+ program->sort_list_finish = index_sort_list_finish_string;
+ index_sort_list_init_string(program);
+ break;
+ case MAIL_SORT_RELEVANCY: {
+ ARRAY_TYPE(mail_sort_node_float) *nodes;
+
+ nodes = i_malloc(sizeof(*nodes));
+ i_array_init(nodes, 128);
+ program->sort_list_add = index_sort_list_add_relevancy;
+ program->sort_list_finish = index_sort_list_finish_float;
+ program->context = nodes;
+ break;
+ }
+ case MAIL_SORT_POP3_ORDER: {
+ ARRAY_TYPE(mail_sort_node_size) *nodes;
+
+ nodes = i_malloc(sizeof(*nodes));
+ i_array_init(nodes, 128);
+ program->sort_list_add = index_sort_list_add_pop3_order;
+ program->sort_list_finish = index_sort_list_finish_size;
+ program->context = nodes;
+ break;
+ }
+ default:
+ i_unreached();
+ }
+ return program;
+}
+
+int index_sort_program_deinit(struct mail_search_sort_program **_program)
+{
+ struct mail_search_sort_program *program = *_program;
+
+ *_program = NULL;
+
+ if (program->context != NULL)
+ index_sort_list_finish(program);
+ mail_free(&program->temp_mail);
+ array_free(&program->seqs);
+
+ int ret = program->failed ? -1 : 0;
+ i_free(program);
+ return ret;
+}
+
+static int
+get_first_addr(struct mail *mail, const char *header,
+ struct message_address **addr_r)
+{
+ const char *str;
+ int ret;
+
+ if ((ret = mail_get_first_header(mail, header, &str)) <= 0) {
+ *addr_r = NULL;
+ return ret;
+ }
+
+ *addr_r = message_address_parse(pool_datastack_create(),
+ (const unsigned char *)str,
+ strlen(str), 1,
+ MESSAGE_ADDRESS_PARSE_FLAG_FILL_MISSING);
+ return 0;
+}
+
+static int
+get_first_mailbox(struct mail *mail, const char *header, const char **mailbox_r)
+{
+ struct message_address *addr;
+
+ if (get_first_addr(mail, header, &addr) < 0) {
+ *mailbox_r = "";
+ return -1;
+ }
+ *mailbox_r = addr != NULL && addr->mailbox != NULL ? addr->mailbox : "";
+ return 0;
+}
+
+static int
+get_display_name(struct mail *mail, const char *header, const char **name_r)
+{
+ struct message_address *addr;
+
+ *name_r = "";
+
+ if (get_first_addr(mail, header, &addr) < 0)
+ return -1;
+ if (addr == NULL)
+ return 0;
+
+ if (addr->name != NULL) {
+ string_t *str;
+ size_t len = strlen(addr->name);
+
+ str = t_str_new(len*2);
+ (void)message_header_decode_utf8(
+ (const unsigned char *)addr->name, len, str, NULL);
+ if (str_len(str) > 0) {
+ *name_r = str_c(str);
+ return 0;
+ }
+ }
+ if (addr->mailbox != NULL && addr->domain != NULL)
+ *name_r = t_strconcat(addr->mailbox, "@", addr->domain, NULL);
+ else if (addr->mailbox != NULL)
+ *name_r = addr->mailbox;
+ return 0;
+}
+
+static void
+index_sort_set_seq(struct mail_search_sort_program *program,
+ struct mail *mail, uint32_t seq)
+{
+ if ((mail->mail_stream_accessed || mail->mail_metadata_accessed) &&
+ program->slow_mails_left > 0)
+ program->slow_mails_left--;
+ mail_set_seq(mail, seq);
+ if (program->slow_mails_left == 0) {
+ /* too many slow lookups - just return the rest of the results
+ in whatever order. */
+ mail->lookup_abort = MAIL_LOOKUP_ABORT_NOT_IN_CACHE;
+ }
+}
+
+int index_sort_header_get(struct mail_search_sort_program *program, uint32_t seq,
+ enum mail_sort_type sort_type, string_t *dest)
+{
+ struct mail *mail = program->temp_mail;
+ const char *str;
+ int ret;
+ bool reply_or_fw;
+
+ index_sort_set_seq(program, mail, seq);
+ str_truncate(dest, 0);
+
+ switch (sort_type & MAIL_SORT_MASK) {
+ case MAIL_SORT_SUBJECT:
+ ret = mail_get_first_header(mail, "Subject", &str);
+ if (ret < 0)
+ break;
+ if (ret == 0) {
+ /* nonexistent header */
+ return 1;
+ }
+ str = imap_get_base_subject_cased(pool_datastack_create(),
+ str, &reply_or_fw);
+ str_append(dest, str);
+ return 1;
+ case MAIL_SORT_CC:
+ ret = get_first_mailbox(mail, "Cc", &str);
+ break;
+ case MAIL_SORT_FROM:
+ ret = get_first_mailbox(mail, "From", &str);
+ break;
+ case MAIL_SORT_TO:
+ ret = get_first_mailbox(mail, "To", &str);
+ break;
+ case MAIL_SORT_DISPLAYFROM:
+ ret = get_display_name(mail, "From", &str);
+ break;
+ case MAIL_SORT_DISPLAYTO:
+ ret = get_display_name(mail, "To", &str);
+ break;
+ default:
+ i_unreached();
+ }
+ if (ret < 0) {
+ index_sort_program_set_mail_failed(program, mail);
+ if (!program->failed)
+ return 0;
+ return -1;
+ }
+
+ (void)uni_utf8_to_decomposed_titlecase(str, strlen(str), dest);
+ return 1;
+}
+
+int index_sort_node_cmp_type(struct mail_search_sort_program *program,
+ const enum mail_sort_type *sort_program,
+ uint32_t seq1, uint32_t seq2)
+{
+ struct mail *mail = program->temp_mail;
+ enum mail_sort_type sort_type;
+ time_t time1, time2;
+ uoff_t size1, size2;
+ float float1, float2;
+ int tz, ret = 0;
+
+ sort_type = *sort_program & MAIL_SORT_MASK;
+ switch (sort_type) {
+ case MAIL_SORT_CC:
+ case MAIL_SORT_FROM:
+ case MAIL_SORT_TO:
+ case MAIL_SORT_SUBJECT:
+ case MAIL_SORT_DISPLAYFROM:
+ case MAIL_SORT_DISPLAYTO:
+ T_BEGIN {
+ string_t *str1, *str2;
+
+ str1 = t_str_new(256);
+ str2 = t_str_new(256);
+ if (index_sort_header_get(program, seq1, sort_type, str1) < 0)
+ index_sort_program_set_mail_failed(program, mail);
+ if (index_sort_header_get(program, seq2, sort_type, str2) < 0)
+ index_sort_program_set_mail_failed(program, mail);
+
+ ret = strcmp(str_c(str1), str_c(str2));
+ } T_END;
+ break;
+ case MAIL_SORT_ARRIVAL:
+ index_sort_set_seq(program, mail, seq1);
+ if (mail_get_received_date(mail, &time1) < 0)
+ time1 = index_sort_program_set_date_failed(program, mail);
+
+ index_sort_set_seq(program, mail, seq2);
+ if (mail_get_received_date(mail, &time2) < 0)
+ time2 = index_sort_program_set_date_failed(program, mail);
+
+ ret = time1 < time2 ? -1 :
+ (time1 > time2 ? 1 : 0);
+ break;
+ case MAIL_SORT_DATE:
+ index_sort_set_seq(program, mail, seq1);
+ if (mail_get_date(mail, &time1, &tz) < 0)
+ time1 = index_sort_program_set_date_failed(program, mail);
+ else if (time1 == 0) {
+ if (mail_get_received_date(mail, &time1) < 0)
+ time1 = index_sort_program_set_date_failed(program, mail);
+ }
+
+ index_sort_set_seq(program, mail, seq2);
+ if (mail_get_date(mail, &time2, &tz) < 0)
+ time2 = index_sort_program_set_date_failed(program, mail);
+ else if (time2 == 0) {
+ if (mail_get_received_date(mail, &time2) < 0)
+ time2 = index_sort_program_set_date_failed(program, mail);
+ }
+
+ ret = time1 < time2 ? -1 :
+ (time1 > time2 ? 1 : 0);
+ break;
+ case MAIL_SORT_SIZE:
+ index_sort_set_seq(program, mail, seq1);
+ if (mail_get_virtual_size(mail, &size1) < 0) {
+ index_sort_program_set_mail_failed(program, mail);
+ size1 = 0;
+ }
+
+ index_sort_set_seq(program, mail, seq2);
+ if (mail_get_virtual_size(mail, &size2) < 0) {
+ index_sort_program_set_mail_failed(program, mail);
+ size2 = 0;
+ }
+
+ ret = size1 < size2 ? -1 :
+ (size1 > size2 ? 1 : 0);
+ break;
+ case MAIL_SORT_RELEVANCY:
+ index_sort_set_seq(program, mail, seq1);
+ if (index_sort_get_relevancy(mail, &float1) < 0)
+ index_sort_program_set_mail_failed(program, mail);
+ index_sort_set_seq(program, mail, seq2);
+ if (index_sort_get_relevancy(mail, &float2) < 0)
+ index_sort_program_set_mail_failed(program, mail);
+
+ /* NOTE: higher relevancy is returned first, unlike with all
+ other number based sort keys */
+ ret = float1 < float2 ? 1 :
+ (float1 > float2 ? -1 : 0);
+ break;
+ case MAIL_SORT_POP3_ORDER:
+ /* 32bit numbers would be enough, but since there is already
+ existing code for uoff_t in sizes, just use them. */
+ index_sort_set_seq(program, mail, seq1);
+ if (index_sort_get_pop3_order(mail, &size1) < 0)
+ index_sort_program_set_mail_failed(program, mail);
+ index_sort_set_seq(program, mail, seq2);
+ if (index_sort_get_pop3_order(mail, &size2) < 0)
+ index_sort_program_set_mail_failed(program, mail);
+
+ ret = size1 < size2 ? -1 :
+ (size1 > size2 ? 1 : 0);
+ break;
+ case MAIL_SORT_END:
+ return seq1 < seq2 ? -1 :
+ (seq1 > seq2 ? 1 : 0);
+ case MAIL_SORT_MASK:
+ case MAIL_SORT_FLAG_REVERSE:
+ i_unreached();
+ }
+
+ if (ret == 0) {
+ return index_sort_node_cmp_type(program, sort_program+1,
+ seq1, seq2);
+ }
+
+ if ((*sort_program & MAIL_SORT_FLAG_REVERSE) != 0)
+ ret = ret < 0 ? 1 : -1;
+ return ret;
+}
diff --git a/src/lib-storage/index/index-sort.h b/src/lib-storage/index/index-sort.h
new file mode 100644
index 0000000..b0e2770
--- /dev/null
+++ b/src/lib-storage/index/index-sort.h
@@ -0,0 +1,18 @@
+#ifndef INDEX_SORT_H
+#define INDEX_SORT_H
+
+struct mail_search_sort_program;
+
+struct mail_search_sort_program *
+index_sort_program_init(struct mailbox_transaction_context *t,
+ const enum mail_sort_type *sort_program);
+int index_sort_program_deinit(struct mail_search_sort_program **program);
+
+void index_sort_list_add(struct mail_search_sort_program *program,
+ struct mail *mail);
+void index_sort_list_finish(struct mail_search_sort_program *program);
+
+bool index_sort_list_next(struct mail_search_sort_program *program,
+ uint32_t *seq_r);
+
+#endif
diff --git a/src/lib-storage/index/index-status.c b/src/lib-storage/index/index-status.c
new file mode 100644
index 0000000..9d751b9
--- /dev/null
+++ b/src/lib-storage/index/index-status.c
@@ -0,0 +1,344 @@
+/* Copyright (c) 2002-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "mail-cache.h"
+#include "mail-index-modseq.h"
+#include "mailbox-recent-flags.h"
+#include "index-storage.h"
+
+static void
+get_last_cached_seq(struct mailbox *box, uint32_t *last_cached_seq_r)
+{
+ const struct mail_index_header *hdr;
+ struct mail_cache_view *cache_view;
+ uint32_t seq;
+
+ *last_cached_seq_r = 0;
+ if (!mail_cache_exists(box->cache))
+ return;
+
+ cache_view = mail_cache_view_open(box->cache, box->view);
+ hdr = mail_index_get_header(box->view);
+ for (seq = hdr->messages_count; seq > 0; seq--) {
+ if (mail_cache_field_exists_any(cache_view, seq)) {
+ *last_cached_seq_r = seq;
+ break;
+ }
+ }
+ mail_cache_view_close(&cache_view);
+}
+
+int index_storage_get_status(struct mailbox *box,
+ enum mailbox_status_items items,
+ struct mailbox_status *status_r)
+{
+ if (items == 0) {
+ /* caller could have wanted only e.g. mailbox_status.have_*
+ flags */
+ return 0;
+ }
+
+ if (!box->opened) {
+ if (mailbox_open(box) < 0)
+ return -1;
+ if (mailbox_sync(box, MAILBOX_SYNC_FLAG_FAST) < 0)
+ return -1;
+ }
+ index_storage_get_open_status(box, items, status_r);
+ return 0;
+}
+
+static unsigned int index_storage_count_pvt_unseen(struct mailbox *box)
+{
+ const struct mail_index_record *pvt_rec;
+ uint32_t shared_seq, pvt_seq, shared_count, pvt_count;
+ uint32_t shared_uid;
+ unsigned int unseen_count = 0;
+
+ /* we can't trust private index to be up to date. we'll need to go
+ through the shared index and for each existing mail lookup its
+ private flags. if a mail doesn't exist in private index then its
+ flags are 0. */
+ shared_count = mail_index_view_get_messages_count(box->view);
+ pvt_count = mail_index_view_get_messages_count(box->view_pvt);
+ shared_seq = pvt_seq = 1;
+ while (shared_seq <= shared_count && pvt_seq <= pvt_count) {
+ mail_index_lookup_uid(box->view, shared_seq, &shared_uid);
+ pvt_rec = mail_index_lookup(box->view_pvt, pvt_seq);
+
+ if (shared_uid == pvt_rec->uid) {
+ if ((pvt_rec->flags & MAIL_SEEN) == 0)
+ unseen_count++;
+ shared_seq++; pvt_seq++;
+ } else if (shared_uid < pvt_rec->uid) {
+ shared_seq++;
+ } else {
+ pvt_seq++;
+ }
+ }
+ unseen_count += (shared_count+1) - shared_seq;
+ return unseen_count;
+}
+
+static uint32_t index_storage_find_first_pvt_unseen_seq(struct mailbox *box)
+{
+ const struct mail_index_header *pvt_hdr;
+ const struct mail_index_record *pvt_rec;
+ uint32_t pvt_seq, pvt_count, shared_seq, seq2;
+
+ pvt_count = mail_index_view_get_messages_count(box->view_pvt);
+ mail_index_lookup_first(box->view_pvt, 0, MAIL_SEEN, &pvt_seq);
+ if (pvt_seq == 0)
+ pvt_seq = pvt_count+1;
+ for (; pvt_seq <= pvt_count; pvt_seq++) {
+ pvt_rec = mail_index_lookup(box->view_pvt, pvt_seq);
+ if ((pvt_rec->flags & MAIL_SEEN) == 0 &&
+ mail_index_lookup_seq(box->view, pvt_rec->uid, &shared_seq))
+ return shared_seq;
+ }
+ /* if shared index has any messages that don't exist in private index,
+ the first of them is the first unseen message */
+ pvt_hdr = mail_index_get_header(box->view_pvt);
+ if (mail_index_lookup_seq_range(box->view,
+ pvt_hdr->next_uid, (uint32_t)-1,
+ &shared_seq, &seq2))
+ return shared_seq;
+ return 0;
+}
+
+void index_storage_get_open_status(struct mailbox *box,
+ enum mailbox_status_items items,
+ struct mailbox_status *status_r)
+{
+ const struct mail_index_header *hdr;
+
+ /* we can get most of the status items without any trouble */
+ hdr = mail_index_get_header(box->view);
+ status_r->messages = hdr->messages_count;
+ if ((items & STATUS_RECENT) != 0) {
+ if ((box->flags & MAILBOX_FLAG_DROP_RECENT) != 0) {
+ /* recent flags are set and dropped by the previous
+ sync while index was locked. if we updated the
+ recent flags here we'd have a race condition. */
+ i_assert(box->synced);
+ } else {
+ /* make sure recent count is set, in case we haven't
+ synced yet */
+ index_sync_update_recent_count(box);
+ }
+ status_r->recent = mailbox_recent_flags_count(box);
+ i_assert(status_r->recent <= status_r->messages);
+ }
+ if ((items & STATUS_UNSEEN) != 0) {
+ if (box->view_pvt == NULL ||
+ (mailbox_get_private_flags_mask(box) & MAIL_SEEN) == 0) {
+ status_r->unseen = hdr->messages_count -
+ hdr->seen_messages_count;
+ } else {
+ status_r->unseen = index_storage_count_pvt_unseen(box);
+ }
+ }
+ status_r->uidvalidity = hdr->uid_validity;
+ status_r->uidnext = hdr->next_uid;
+ status_r->first_recent_uid = hdr->first_recent_uid;
+ if ((items & STATUS_HIGHESTMODSEQ) != 0) {
+ status_r->nonpermanent_modseqs =
+ mail_index_is_in_memory(box->index);
+ status_r->no_modseq_tracking =
+ !mail_index_have_modseq_tracking(box->index);
+ status_r->highest_modseq =
+ mail_index_modseq_get_highest(box->view);
+ if (status_r->highest_modseq == 0) {
+ /* modseqs not enabled yet, but we can't return 0 */
+ status_r->highest_modseq = 1;
+ }
+ }
+ if ((items & STATUS_HIGHESTPVTMODSEQ) != 0 && box->view_pvt != NULL) {
+ status_r->highest_pvt_modseq =
+ mail_index_modseq_get_highest(box->view_pvt);
+ if (status_r->highest_pvt_modseq == 0) {
+ /* modseqs not enabled yet, but we can't return 0 */
+ status_r->highest_pvt_modseq = 1;
+ }
+ }
+
+ if ((items & STATUS_FIRST_UNSEEN_SEQ) != 0) {
+ if (box->view_pvt == NULL ||
+ (mailbox_get_private_flags_mask(box) & MAIL_SEEN) == 0) {
+ mail_index_lookup_first(box->view, 0, MAIL_SEEN,
+ &status_r->first_unseen_seq);
+ } else {
+ status_r->first_unseen_seq =
+ index_storage_find_first_pvt_unseen_seq(box);
+ }
+ }
+ if ((items & STATUS_LAST_CACHED_SEQ) != 0)
+ get_last_cached_seq(box, &status_r->last_cached_seq);
+
+ if ((items & STATUS_KEYWORDS) != 0)
+ status_r->keywords = mail_index_get_keywords(box->index);
+ if ((items & STATUS_PERMANENT_FLAGS) != 0) {
+ if (!mailbox_is_readonly(box)) {
+ status_r->permanent_flags = MAIL_FLAGS_NONRECENT;
+ status_r->permanent_keywords = TRUE;
+ status_r->allow_new_keywords =
+ !box->disallow_new_keywords;
+ }
+ status_r->flags = MAIL_FLAGS_NONRECENT;
+ }
+}
+
+static void
+get_metadata_cache_fields(struct mailbox *box,
+ struct mailbox_metadata *metadata_r)
+{
+ const struct mail_cache_field *fields;
+ enum mail_cache_decision_type dec;
+ ARRAY_TYPE(mailbox_cache_field) *cache_fields;
+ struct mailbox_cache_field *cf;
+ unsigned int i, count;
+
+ fields = mail_cache_register_get_list(box->cache,
+ pool_datastack_create(), &count);
+
+ cache_fields = t_new(ARRAY_TYPE(mailbox_cache_field), 1);
+ t_array_init(cache_fields, count);
+ for (i = 0; i < count; i++) {
+ dec = fields[i].decision & ENUM_NEGATE(MAIL_CACHE_DECISION_FORCED);
+ if (dec != MAIL_CACHE_DECISION_NO) {
+ cf = array_append_space(cache_fields);
+ cf->name = fields[i].name;
+ cf->decision = fields[i].decision;
+ cf->last_used = fields[i].last_used;
+ }
+ }
+ metadata_r->cache_fields = cache_fields;
+}
+
+static void get_metadata_precache_fields(struct mailbox *box,
+ struct mailbox_metadata *metadata_r)
+{
+ const struct mail_cache_field *fields;
+ unsigned int i, count;
+ enum mail_fetch_field cache = 0;
+
+ fields = mail_cache_register_get_list(box->cache,
+ pool_datastack_create(), &count);
+ for (i = 0; i < count; i++) {
+ const char *name = fields[i].name;
+
+ if (str_begins(name, "hdr.") ||
+ strcmp(name, "date.sent") == 0 ||
+ strcmp(name, "imap.envelope") == 0)
+ cache |= MAIL_FETCH_STREAM_HEADER;
+ else if (strcmp(name, "mime.parts") == 0 ||
+ strcmp(name, "binary.parts") == 0 ||
+ strcmp(name, "imap.body") == 0 ||
+ strcmp(name, "imap.bodystructure") == 0 ||
+ strcmp(name, "body.snippet") == 0)
+ cache |= MAIL_FETCH_STREAM_BODY;
+ else if (strcmp(name, "date.received") == 0)
+ cache |= MAIL_FETCH_RECEIVED_DATE;
+ else if (strcmp(name, "date.save") == 0)
+ cache |= MAIL_FETCH_SAVE_DATE;
+ else if (strcmp(name, "size.virtual") == 0)
+ cache |= MAIL_FETCH_VIRTUAL_SIZE;
+ else if (strcmp(name, "size.physical") == 0)
+ cache |= MAIL_FETCH_PHYSICAL_SIZE;
+ else if (strcmp(name, "pop3.uidl") == 0)
+ cache |= MAIL_FETCH_UIDL_BACKEND;
+ else if (strcmp(name, "pop3.order") == 0)
+ cache |= MAIL_FETCH_POP3_ORDER;
+ else if (strcmp(name, "guid") == 0)
+ cache |= MAIL_FETCH_GUID;
+ else if (strcmp(name, "flags") == 0) {
+ /* just ignore for now at least.. */
+ } else
+ e_debug(box->event,
+ "Ignoring unknown cache field: %s", name);
+ }
+ metadata_r->precache_fields = cache;
+}
+
+static int
+index_mailbox_get_first_save_date(struct mailbox *box,
+ struct mailbox_metadata *metadata_r)
+{
+ const struct mail_index_header *hdr;
+ struct mailbox_transaction_context *t;
+ struct mail *mail;
+ uint32_t seq;
+ int ret = -1;
+
+ hdr = mail_index_get_header(box->view);
+ if (hdr->messages_count == 0) {
+ metadata_r->first_save_date = (time_t)-1;
+ return 0;
+ }
+
+ t = mailbox_transaction_begin(box, 0, __func__);
+ mail = mail_alloc(t, 0, NULL);
+ for (seq = 1; seq <= hdr->messages_count; seq++) {
+ mail_set_seq(mail, seq);
+ if (mail_get_save_date(mail, &metadata_r->first_save_date) >= 0) {
+ ret = 0;
+ break;
+ }
+ if (mailbox_get_last_mail_error(box) != MAIL_ERROR_EXPUNGED) {
+ /* failed */
+ break;
+ }
+ }
+ mail_free(&mail);
+ (void)mailbox_transaction_commit(&t);
+ if (seq > hdr->messages_count) {
+ /* all messages were expunged after all */
+ metadata_r->first_save_date = (time_t)-1;
+ return 0;
+ }
+ return ret;
+}
+
+int index_mailbox_get_metadata(struct mailbox *box,
+ enum mailbox_metadata_items items,
+ struct mailbox_metadata *metadata_r)
+{
+ /* handle items that don't require opening the mailbox */
+ if ((items & MAILBOX_METADATA_BACKEND_NAMESPACE) != 0) {
+ metadata_r->backend_ns_prefix = "";
+ metadata_r->backend_ns_type =
+ mailbox_list_get_namespace(box->list)->type;
+ items &= ENUM_NEGATE(MAILBOX_METADATA_BACKEND_NAMESPACE);
+ }
+ if (items == 0)
+ return 0;
+
+ /* handle items that require opening the mailbox */
+ if (!box->opened) {
+ if (mailbox_open(box) < 0)
+ return -1;
+ }
+ if (!box->synced && (items & MAILBOX_METADATA_SYNC_ITEMS) != 0) {
+ if (mailbox_sync(box, MAILBOX_SYNC_FLAG_FAST) < 0)
+ return -1;
+ }
+
+ if ((items & MAILBOX_METADATA_VIRTUAL_SIZE) != 0) {
+ if (index_mailbox_get_virtual_size(box, metadata_r) < 0)
+ return -1;
+ }
+ if ((items & MAILBOX_METADATA_PHYSICAL_SIZE) != 0) {
+ if (index_mailbox_get_physical_size(box, metadata_r) < 0)
+ return -1;
+ }
+ if ((items & MAILBOX_METADATA_FIRST_SAVE_DATE) != 0) {
+ if (index_mailbox_get_first_save_date(box, metadata_r) < 0)
+ return -1;
+ }
+ if ((items & MAILBOX_METADATA_CACHE_FIELDS) != 0)
+ get_metadata_cache_fields(box, metadata_r);
+ if ((items & MAILBOX_METADATA_PRECACHE_FIELDS) != 0)
+ get_metadata_precache_fields(box, metadata_r);
+ return 0;
+}
diff --git a/src/lib-storage/index/index-storage.c b/src/lib-storage/index/index-storage.c
new file mode 100644
index 0000000..e1998b0
--- /dev/null
+++ b/src/lib-storage/index/index-storage.c
@@ -0,0 +1,1291 @@
+/* Copyright (c) 2002-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "istream.h"
+#include "ostream.h"
+#include "ioloop.h"
+#include "str.h"
+#include "str-sanitize.h"
+#include "mkdir-parents.h"
+#include "dict.h"
+#include "fs-api.h"
+#include "message-header-parser.h"
+#include "mail-index-alloc-cache.h"
+#include "mail-index-private.h"
+#include "mail-index-modseq.h"
+#include "mailbox-log.h"
+#include "mailbox-list-private.h"
+#include "mail-search-build.h"
+#include "index-storage.h"
+#include "index-mail.h"
+#include "index-attachment.h"
+#include "index-thread-private.h"
+#include "index-mailbox-size.h"
+
+#include <time.h>
+#include <unistd.h>
+#include <sys/stat.h>
+
+#define LOCK_NOTIFY_INTERVAL 30
+
+struct index_storage_module index_storage_module =
+ MODULE_CONTEXT_INIT(&mail_storage_module_register);
+
+static void set_cache_decisions(struct mail_cache *cache,
+ const char *set, const char *fields,
+ enum mail_cache_decision_type dec)
+{
+ struct mail_cache_field field;
+ const char *const *arr;
+ unsigned int idx;
+
+ if (fields == NULL || *fields == '\0')
+ return;
+
+ for (arr = t_strsplit_spaces(fields, " ,"); *arr != NULL; arr++) {
+ const char *name = *arr;
+
+ idx = mail_cache_register_lookup(cache, name);
+ if (idx != UINT_MAX) {
+ field = *mail_cache_register_get_field(cache, idx);
+ } else if (strncasecmp(name, "hdr.", 4) == 0) {
+ /* Do some sanity checking for the header name. Mainly
+ to make sure there aren't UTF-8 characters that look
+ like their ASCII equivalents or are completely
+ invisible. */
+ if (message_header_name_is_valid(name+4)) {
+ i_zero(&field);
+ field.name = name;
+ field.type = MAIL_CACHE_FIELD_HEADER;
+ } else {
+ i_error("%s: Header name '%s' has invalid character, ignoring",
+ set, name);
+ continue;
+ }
+ } else {
+ i_error("%s: Unknown cache field name '%s', ignoring",
+ set, *arr);
+ continue;
+ }
+
+ field.decision = dec;
+ mail_cache_register_fields(cache, &field, 1);
+ }
+}
+
+static void index_cache_register_defaults(struct mailbox *box)
+{
+ struct index_mailbox_context *ibox = INDEX_STORAGE_CONTEXT(box);
+ const struct mail_storage_settings *set = box->storage->set;
+ struct mail_cache *cache = box->cache;
+
+ ibox->cache_fields = i_malloc(sizeof(global_cache_fields));
+ memcpy(ibox->cache_fields, global_cache_fields,
+ sizeof(global_cache_fields));
+ mail_cache_register_fields(cache, ibox->cache_fields,
+ MAIL_INDEX_CACHE_FIELD_COUNT);
+
+ if (strcmp(set->mail_never_cache_fields, "*") == 0) {
+ /* all caching disabled for now */
+ box->mail_cache_disabled = TRUE;
+ return;
+ }
+
+ set_cache_decisions(cache, "mail_cache_fields",
+ set->mail_cache_fields,
+ MAIL_CACHE_DECISION_TEMP);
+ set_cache_decisions(cache, "mail_always_cache_fields",
+ set->mail_always_cache_fields,
+ MAIL_CACHE_DECISION_YES |
+ MAIL_CACHE_DECISION_FORCED);
+ set_cache_decisions(cache, "mail_never_cache_fields",
+ set->mail_never_cache_fields,
+ MAIL_CACHE_DECISION_NO |
+ MAIL_CACHE_DECISION_FORCED);
+}
+
+void index_storage_lock_notify(struct mailbox *box,
+ enum mailbox_lock_notify_type notify_type,
+ unsigned int secs_left)
+{
+ struct index_mailbox_context *ibox = INDEX_STORAGE_CONTEXT(box);
+ struct mail_storage *storage = box->storage;
+ const char *str;
+ time_t now;
+
+ /* if notify type changes, print the message immediately */
+ now = time(NULL);
+ if (ibox->last_notify_type == MAILBOX_LOCK_NOTIFY_NONE ||
+ ibox->last_notify_type == notify_type) {
+ if (ibox->last_notify_type == MAILBOX_LOCK_NOTIFY_NONE &&
+ notify_type == MAILBOX_LOCK_NOTIFY_MAILBOX_OVERRIDE) {
+ /* first override notification, show it */
+ } else {
+ if (now < ibox->next_lock_notify || secs_left < 15)
+ return;
+ }
+ }
+
+ ibox->next_lock_notify = now + LOCK_NOTIFY_INTERVAL;
+ ibox->last_notify_type = notify_type;
+
+ switch (notify_type) {
+ case MAILBOX_LOCK_NOTIFY_NONE:
+ break;
+ case MAILBOX_LOCK_NOTIFY_MAILBOX_ABORT:
+ if (storage->callbacks.notify_no == NULL)
+ break;
+
+ str = t_strdup_printf("Mailbox is locked, will abort in "
+ "%u seconds", secs_left);
+ storage->callbacks.
+ notify_no(box, str, storage->callback_context);
+ break;
+ case MAILBOX_LOCK_NOTIFY_MAILBOX_OVERRIDE:
+ if (storage->callbacks.notify_ok == NULL)
+ break;
+
+ str = t_strdup_printf("Stale mailbox lock file detected, "
+ "will override in %u seconds", secs_left);
+ storage->callbacks.
+ notify_ok(box, str, storage->callback_context);
+ break;
+ }
+}
+
+void index_storage_lock_notify_reset(struct mailbox *box)
+{
+ struct index_mailbox_context *ibox = INDEX_STORAGE_CONTEXT(box);
+
+ ibox->next_lock_notify = time(NULL) + LOCK_NOTIFY_INTERVAL;
+ ibox->last_notify_type = MAILBOX_LOCK_NOTIFY_NONE;
+}
+
+static int
+index_mailbox_alloc_index(struct mailbox *box, struct mail_index **index_r)
+{
+ const char *index_dir, *mailbox_path;
+
+ if (mailbox_get_path_to(box, MAILBOX_LIST_PATH_TYPE_MAILBOX,
+ &mailbox_path) < 0)
+ return -1;
+ if ((box->flags & MAILBOX_FLAG_NO_INDEX_FILES) != 0 ||
+ mailbox_get_path_to(box, MAILBOX_LIST_PATH_TYPE_INDEX,
+ &index_dir) <= 0)
+ index_dir = NULL;
+ /* Note that this may cause box->event to live longer than box */
+ *index_r = mail_index_alloc_cache_get(box->event,
+ mailbox_path, index_dir,
+ box->index_prefix);
+ return 0;
+}
+
+int index_storage_mailbox_exists(struct mailbox *box, bool auto_boxes,
+ enum mailbox_existence *existence_r)
+{
+ if (auto_boxes && mailbox_is_autocreated(box)) {
+ *existence_r = MAILBOX_EXISTENCE_SELECT;
+ return 0;
+ }
+
+ return index_storage_mailbox_exists_full(box, NULL, existence_r);
+}
+
+int index_storage_mailbox_exists_full(struct mailbox *box, const char *subdir,
+ enum mailbox_existence *existence_r)
+{
+ struct stat st;
+ const char *path, *path2, *index_path;
+ int ret;
+
+ /* see if it's selectable */
+ ret = mailbox_get_path_to(box, MAILBOX_LIST_PATH_TYPE_MAILBOX, &path);
+ if (ret < 0) {
+ if (mailbox_get_last_mail_error(box) != MAIL_ERROR_NOTFOUND)
+ return -1;
+ *existence_r = MAILBOX_EXISTENCE_NONE;
+ return 0;
+ }
+ if (ret == 0) {
+ /* no mailboxes in this storage? */
+ *existence_r = MAILBOX_EXISTENCE_NONE;
+ return 0;
+ }
+
+ ret = (subdir != NULL || !box->list->set.iter_from_index_dir) ? 0 :
+ mailbox_get_path_to(box, MAILBOX_LIST_PATH_TYPE_INDEX, &index_path);
+ if (ret > 0 && strcmp(path, index_path) != 0) {
+ /* index directory is different - prefer looking it up first
+ since it might be on a faster storage. since the directory
+ itself exists also for \NoSelect mailboxes, we'll need to
+ check the dovecot.index.log existence. */
+ index_path = t_strconcat(index_path, "/", box->index_prefix,
+ ".log", NULL);
+ if (stat(index_path, &st) == 0) {
+ *existence_r = MAILBOX_EXISTENCE_SELECT;
+ return 0;
+ }
+ }
+
+ if (subdir != NULL)
+ path = t_strconcat(path, "/", subdir, NULL);
+ if (stat(path, &st) == 0) {
+ *existence_r = MAILBOX_EXISTENCE_SELECT;
+ return 0;
+ }
+ if (!ENOTFOUND(errno) && errno != EACCES) {
+ mailbox_set_critical(box, "stat(%s) failed: %m", path);
+ return -1;
+ }
+
+ /* see if it's non-selectable */
+ if (mailbox_get_path_to(box, MAILBOX_LIST_PATH_TYPE_DIR, &path2) <= 0 ||
+ (strcmp(path, path2) != 0 && stat(path2, &st) == 0)) {
+ *existence_r = MAILBOX_EXISTENCE_NOSELECT;
+ return 0;
+ }
+ *existence_r = MAILBOX_EXISTENCE_NONE;
+ return 0;
+}
+
+int index_storage_mailbox_alloc_index(struct mailbox *box)
+{
+ const char *cache_dir;
+
+ if (box->index != NULL)
+ return 0;
+
+ if (mailbox_create_missing_dir(box, MAILBOX_LIST_PATH_TYPE_INDEX) < 0)
+ return -1;
+ if (index_mailbox_alloc_index(box, &box->index) < 0)
+ return -1;
+
+ if (mailbox_get_path_to(box, MAILBOX_LIST_PATH_TYPE_INDEX_CACHE,
+ &cache_dir) > 0) {
+ if (mailbox_create_missing_dir(box, MAILBOX_LIST_PATH_TYPE_INDEX_CACHE) < 0)
+ return -1;
+ mail_index_set_cache_dir(box->index, cache_dir);
+ }
+ mail_index_set_fsync_mode(box->index,
+ box->storage->set->parsed_fsync_mode, 0);
+ mail_index_set_lock_method(box->index,
+ box->storage->set->parsed_lock_method,
+ mail_storage_get_lock_timeout(box->storage, UINT_MAX));
+
+ const struct mail_storage_settings *set = box->storage->set;
+ struct mail_index_optimization_settings optimization_set = {
+ .index = {
+ .rewrite_min_log_bytes = set->mail_index_rewrite_min_log_bytes,
+ .rewrite_max_log_bytes = set->mail_index_rewrite_max_log_bytes,
+ },
+ .log = {
+ .min_size = set->mail_index_log_rotate_min_size,
+ .max_size = set->mail_index_log_rotate_max_size,
+ .min_age_secs = set->mail_index_log_rotate_min_age,
+ .log2_max_age_secs = set->mail_index_log2_max_age,
+ },
+ .cache = {
+ .unaccessed_field_drop_secs = set->mail_cache_unaccessed_field_drop,
+ .record_max_size = set->mail_cache_record_max_size,
+ .max_size = set->mail_cache_max_size,
+ .purge_min_size = set->mail_cache_purge_min_size,
+ .purge_delete_percentage = set->mail_cache_purge_delete_percentage,
+ .purge_continued_percentage = set->mail_cache_purge_continued_percentage,
+ .purge_header_continue_count = set->mail_cache_purge_header_continue_count,
+ },
+ };
+ mail_index_set_optimization_settings(box->index, &optimization_set);
+ return 0;
+}
+
+int index_storage_mailbox_open(struct mailbox *box, bool move_to_memory)
+{
+ struct index_mailbox_context *ibox = INDEX_STORAGE_CONTEXT(box);
+ enum mail_index_open_flags index_flags;
+ int ret;
+
+ i_assert(!box->opened);
+
+ index_flags = ibox->index_flags;
+ if (move_to_memory)
+ index_flags &= ENUM_NEGATE(MAIL_INDEX_OPEN_FLAG_CREATE);
+
+ if (index_storage_mailbox_alloc_index(box) < 0)
+ return -1;
+
+ /* make sure mail_index_set_permissions() has been called */
+ (void)mailbox_get_permissions(box);
+
+ ret = mail_index_open(box->index, index_flags);
+ if (ret <= 0 || move_to_memory) {
+ if ((index_flags & MAIL_INDEX_OPEN_FLAG_NEVER_IN_MEMORY) != 0) {
+ i_assert(ret <= 0);
+ mailbox_set_index_error(box);
+ return -1;
+ }
+
+ if (mail_index_move_to_memory(box->index) < 0) {
+ /* try opening once more. it should be created
+ directly into memory now. */
+ if (mail_index_open_or_create(box->index,
+ index_flags) < 0)
+ i_panic("in-memory index creation failed");
+ }
+ }
+ if ((index_flags & MAIL_INDEX_OPEN_FLAG_NEVER_IN_MEMORY) != 0) {
+ if (mail_index_is_in_memory(box->index)) {
+ mailbox_set_critical(box,
+ "Couldn't create index file");
+ mail_index_close(box->index);
+ return -1;
+ }
+ }
+
+ if ((box->flags & MAILBOX_FLAG_OPEN_DELETED) == 0) {
+ if (mail_index_is_deleted(box->index)) {
+ mailbox_set_deleted(box);
+ mail_index_close(box->index);
+ return -1;
+ }
+ }
+ if ((box->flags & MAILBOX_FLAG_FSCK) != 0) {
+ if (mail_index_fsck(box->index) < 0) {
+ mailbox_set_index_error(box);
+ return -1;
+ }
+ }
+
+ box->cache = mail_index_get_cache(box->index);
+ index_cache_register_defaults(box);
+ box->view = mail_index_view_open(box->index);
+ ibox->keyword_names = mail_index_get_keywords(box->index);
+ box->vsize_hdr_ext_id =
+ mail_index_ext_register(box->index, "hdr-vsize",
+ sizeof(struct mailbox_index_vsize), 0,
+ sizeof(uint64_t));
+ box->pop3_uidl_hdr_ext_id =
+ mail_index_ext_register(box->index, "hdr-pop3-uidl",
+ sizeof(struct mailbox_index_pop3_uidl), 0, 0);
+ box->box_name_hdr_ext_id =
+ mail_index_ext_register(box->index, "box-name", 0, 0, 0);
+
+ box->box_last_rename_stamp_ext_id =
+ mail_index_ext_register(box->index, "last-rename-stamp",
+ sizeof(uint32_t), 0, sizeof(uint32_t));
+ box->mail_vsize_ext_id = mail_index_ext_register(box->index, "vsize", 0,
+ sizeof(uint32_t),
+ sizeof(uint32_t));
+
+ box->opened = TRUE;
+
+ if ((box->enabled_features & MAILBOX_FEATURE_CONDSTORE) != 0)
+ mail_index_modseq_enable(box->index);
+
+ index_thread_mailbox_opened(box);
+ hook_mailbox_opened(box);
+ return 0;
+}
+
+void index_storage_mailbox_alloc(struct mailbox *box, const char *vname,
+ enum mailbox_flags flags,
+ const char *index_prefix)
+{
+ static unsigned int mailbox_generation_sequence = 0;
+ struct index_mailbox_context *ibox;
+
+ i_assert(vname != NULL);
+
+ box->generation_sequence = ++mailbox_generation_sequence;
+ box->vname = p_strdup(box->pool, vname);
+ box->name = p_strdup(box->pool,
+ mailbox_list_get_storage_name(box->list, vname));
+ box->flags = flags;
+ box->index_prefix = p_strdup(box->pool, index_prefix);
+ box->event = event_create(box->storage->event);
+ event_add_category(box->event, &event_category_mailbox);
+ event_add_str(box->event, "mailbox", box->vname);
+ event_set_append_log_prefix(box->event,
+ t_strdup_printf("Mailbox %s: ", str_sanitize(box->vname, 128)));
+
+ p_array_init(&box->search_results, box->pool, 16);
+ array_create(&box->module_contexts,
+ box->pool, sizeof(void *), 5);
+
+ ibox = p_new(box->pool, struct index_mailbox_context, 1);
+ ibox->list_index_sync_ext_id = (uint32_t)-1;
+ ibox->index_flags = MAIL_INDEX_OPEN_FLAG_CREATE |
+ mail_storage_settings_to_index_flags(box->storage->set);
+ if ((box->flags & MAILBOX_FLAG_SAVEONLY) != 0)
+ ibox->index_flags |= MAIL_INDEX_OPEN_FLAG_SAVEONLY;
+ if (event_want_debug(box->event))
+ ibox->index_flags |= MAIL_INDEX_OPEN_FLAG_DEBUG;
+ ibox->next_lock_notify = time(NULL) + LOCK_NOTIFY_INTERVAL;
+ MODULE_CONTEXT_SET(box, index_storage_module, ibox);
+
+ box->inbox_user = strcmp(box->name, "INBOX") == 0 &&
+ (box->list->ns->flags & NAMESPACE_FLAG_INBOX_USER) != 0;
+ box->inbox_any = strcmp(box->name, "INBOX") == 0 &&
+ (box->list->ns->flags & NAMESPACE_FLAG_INBOX_ANY) != 0;
+}
+
+int index_storage_mailbox_enable(struct mailbox *box,
+ enum mailbox_feature feature)
+{
+ if ((feature & MAILBOX_FEATURE_CONDSTORE) != 0) {
+ box->enabled_features |= MAILBOX_FEATURE_CONDSTORE;
+ if (box->opened)
+ mail_index_modseq_enable(box->index);
+ }
+ return 0;
+}
+
+void index_storage_mailbox_close(struct mailbox *box)
+{
+ struct index_mailbox_context *ibox = INDEX_STORAGE_CONTEXT(box);
+
+ mailbox_watch_remove_all(box);
+ i_stream_unref(&box->input);
+
+ if (box->view_pvt != NULL)
+ mail_index_view_close(&box->view_pvt);
+ if (box->index_pvt != NULL)
+ mail_index_close(box->index_pvt);
+ if (box->view != NULL) {
+ mail_index_view_close(&box->view);
+ mail_index_close(box->index);
+ }
+ box->cache = NULL;
+
+ ibox->keyword_names = NULL;
+ i_free_and_null(ibox->cache_fields);
+
+ ibox->sync_last_check = 0;
+}
+
+static void index_storage_mailbox_unref_indexes(struct mailbox *box)
+{
+ if (box->index_pvt != NULL)
+ mail_index_alloc_cache_unref(&box->index_pvt);
+ if (box->index != NULL)
+ mail_index_alloc_cache_unref(&box->index);
+}
+
+void index_storage_mailbox_free(struct mailbox *box)
+{
+ index_storage_mailbox_unref_indexes(box);
+ event_unref(&box->event);
+}
+
+static void
+index_storage_mailbox_update_cache(struct mailbox *box,
+ const struct mailbox_update *update)
+{
+ const struct mailbox_cache_field *updates = update->cache_updates;
+ ARRAY(struct mail_cache_field) new_fields;
+ const struct mail_cache_field *old_fields;
+ struct mail_cache_field field;
+ unsigned int i, j, old_count;
+
+ old_fields = mail_cache_register_get_list(box->cache,
+ pool_datastack_create(),
+ &old_count);
+
+ /* There shouldn't be many fields, so don't worry about O(n^2). */
+ t_array_init(&new_fields, 32);
+ for (i = 0; updates[i].name != NULL; i++) {
+ /* see if it's an existing field */
+ for (j = 0; j < old_count; j++) {
+ if (strcmp(updates[i].name, old_fields[j].name) == 0)
+ break;
+ }
+ if (j != old_count) {
+ field = old_fields[j];
+ } else if (str_begins(updates[i].name, "hdr.")) {
+ /* new header */
+ i_zero(&field);
+ field.name = updates[i].name;
+ field.type = MAIL_CACHE_FIELD_HEADER;
+ } else {
+ /* new unknown field. we can't do anything about
+ this since we don't know its type */
+ continue;
+ }
+ field.decision = updates[i].decision;
+ if (updates[i].last_used != (time_t)-1)
+ field.last_used = updates[i].last_used;
+ array_push_back(&new_fields, &field);
+ }
+ if (array_count(&new_fields) > 0) {
+ mail_cache_register_fields(box->cache,
+ array_front_modifiable(&new_fields),
+ array_count(&new_fields));
+ }
+}
+
+static int
+index_storage_mailbox_update_pvt(struct mailbox *box,
+ const struct mailbox_update *update)
+{
+ struct mail_index_transaction *trans;
+ struct mail_index_view *view;
+ int ret;
+
+ if ((ret = mailbox_open_index_pvt(box)) <= 0)
+ return ret;
+
+ mail_index_refresh(box->index_pvt);
+ view = mail_index_view_open(box->index_pvt);
+ trans = mail_index_transaction_begin(view,
+ MAIL_INDEX_TRANSACTION_FLAG_EXTERNAL);
+ if (update->min_highest_modseq != 0 &&
+ mail_index_modseq_get_highest(view) < update->min_highest_pvt_modseq) {
+ mail_index_modseq_enable(box->index_pvt);
+ mail_index_update_highest_modseq(trans,
+ update->min_highest_pvt_modseq);
+ }
+
+ if ((ret = mail_index_transaction_commit(&trans)) < 0)
+ mailbox_set_index_error(box);
+ mail_index_view_close(&view);
+ return ret;
+}
+
+int index_storage_mailbox_update_common(struct mailbox *box,
+ const struct mailbox_update *update)
+{
+ int ret = 0;
+
+ if (update->cache_updates != NULL)
+ index_storage_mailbox_update_cache(box, update);
+
+ if (update->min_highest_pvt_modseq != 0) {
+ if (index_storage_mailbox_update_pvt(box, update) < 0)
+ ret = -1;
+ }
+ return ret;
+}
+
+int index_storage_mailbox_update(struct mailbox *box,
+ const struct mailbox_update *update)
+{
+ const struct mail_index_header *hdr;
+ struct mail_index_view *view;
+ struct mail_index_transaction *trans;
+ int ret;
+
+ if (mailbox_open(box) < 0)
+ return -1;
+
+ /* make sure we get the latest index info */
+ mail_index_refresh(box->index);
+ view = mail_index_view_open(box->index);
+ hdr = mail_index_get_header(view);
+
+ trans = mail_index_transaction_begin(view,
+ MAIL_INDEX_TRANSACTION_FLAG_EXTERNAL);
+ if (update->uid_validity != 0 &&
+ hdr->uid_validity != update->uid_validity) {
+ uint32_t uid_validity = update->uid_validity;
+
+ if (hdr->uid_validity != 0) {
+ /* UIDVALIDITY change requires index to be reset */
+ mail_index_reset(trans);
+ }
+ mail_index_update_header(trans,
+ offsetof(struct mail_index_header, uid_validity),
+ &uid_validity, sizeof(uid_validity), TRUE);
+ }
+ if (update->min_next_uid != 0 &&
+ hdr->next_uid < update->min_next_uid) {
+ uint32_t next_uid = update->min_next_uid;
+
+ mail_index_update_header(trans,
+ offsetof(struct mail_index_header, next_uid),
+ &next_uid, sizeof(next_uid), FALSE);
+ }
+ if (update->min_first_recent_uid != 0 &&
+ hdr->first_recent_uid < update->min_first_recent_uid) {
+ uint32_t first_recent_uid = update->min_first_recent_uid;
+
+ mail_index_update_header(trans,
+ offsetof(struct mail_index_header, first_recent_uid),
+ &first_recent_uid, sizeof(first_recent_uid), FALSE);
+ }
+ if (update->min_highest_modseq != 0 &&
+ mail_index_modseq_get_highest(view) < update->min_highest_modseq) {
+ mail_index_modseq_enable(box->index);
+ mail_index_update_highest_modseq(trans,
+ update->min_highest_modseq);
+ }
+
+ if ((ret = mail_index_transaction_commit(&trans)) < 0)
+ mailbox_set_index_error(box);
+ mail_index_view_close(&view);
+ return ret < 0 ? -1 :
+ index_storage_mailbox_update_common(box, update);
+}
+
+int index_storage_mailbox_create(struct mailbox *box, bool directory)
+{
+ const char *path, *p;
+ enum mailbox_list_path_type type;
+ enum mailbox_existence existence;
+ bool create_parent_dir;
+ int ret;
+
+ if ((box->list->props & MAILBOX_LIST_PROP_NO_NOSELECT) != 0) {
+ /* Layout doesn't support creating \NoSelect mailboxes.
+ Switch to creating a selectable mailbox. */
+ directory = FALSE;
+ }
+
+ type = directory ? MAILBOX_LIST_PATH_TYPE_DIR :
+ MAILBOX_LIST_PATH_TYPE_MAILBOX;
+ if ((ret = mailbox_get_path_to(box, type, &path)) < 0)
+ return -1;
+ if (ret == 0) {
+ /* layout=none */
+ mail_storage_set_error(box->storage, MAIL_ERROR_NOTPOSSIBLE,
+ "Mailbox creation not supported");
+ return -1;
+ }
+ create_parent_dir = !directory &&
+ (box->list->flags & MAILBOX_LIST_FLAG_MAILBOX_FILES) != 0;
+ if (create_parent_dir) {
+ /* we only need to make sure that the parent directory exists */
+ p = strrchr(path, '/');
+ if (p == NULL)
+ return 1;
+ path = t_strdup_until(path, p);
+ }
+
+ if ((ret = mailbox_mkdir(box, path, type)) < 0)
+ return -1;
+ if (box->list->set.iter_from_index_dir) {
+ /* need to also create the directory to index path or
+ iteration won't find it. */
+ int ret2;
+
+ if (mailbox_get_path_to(box, MAILBOX_LIST_PATH_TYPE_INDEX, &path) <= 0)
+ i_unreached();
+ if ((ret2 = mailbox_mkdir(box, path, type)) < 0)
+ return -1;
+ if (ret == 0 && ret2 > 0) {
+ /* finish partial creation: existed in mail directory,
+ but not in index directory. */
+ ret = 1;
+ }
+ }
+ mailbox_refresh_permissions(box);
+ if (ret == 0) {
+ /* directory already exists */
+ if (create_parent_dir)
+ return 1;
+ if (!directory && *box->list->set.mailbox_dir_name == '\0') {
+ /* For example: layout=fs, path=~/Maildir/foo
+ might itself exist, but does it have the
+ cur|new|tmp subdirs? */
+ if (mailbox_exists(box, FALSE, &existence) < 0)
+ return -1;
+ if (existence != MAILBOX_EXISTENCE_SELECT)
+ return 1;
+ } else if (!box->storage->rebuilding_list_index) {
+ /* ignore existing location if we are recovering list index */
+ mail_storage_set_error(box->storage, MAIL_ERROR_EXISTS,
+ "Mailbox already exists");
+ return -1;
+ }
+ }
+
+ if (directory) {
+ /* we only wanted to create the directory and it's done now */
+ return 0;
+ }
+ /* the caller should still create the mailbox */
+ return 1;
+}
+
+int index_storage_mailbox_delete_dir(struct mailbox *box, bool mailbox_deleted)
+{
+ guid_128_t dir_sha128;
+ enum mail_error error;
+
+ if (mailbox_list_delete_dir(box->list, box->name) == 0)
+ return 0;
+
+ mailbox_list_get_last_error(box->list, &error);
+ if (error != MAIL_ERROR_NOTFOUND || !mailbox_deleted) {
+ mail_storage_copy_list_error(box->storage, box->list);
+ return -1;
+ }
+ /* failed directory deletion, but mailbox deletion succeeded.
+ this was probably maildir++, which internally deleted the
+ directory as well. add changelog record about that too. */
+ mailbox_name_get_sha128(box->vname, dir_sha128);
+ mailbox_list_add_change(box->list, MAILBOX_LOG_RECORD_DELETE_DIR,
+ dir_sha128);
+ return 0;
+}
+
+static int
+mailbox_delete_all_attributes(struct mailbox_transaction_context *t,
+ enum mail_attribute_type type)
+{
+ struct mailbox_attribute_iter *iter;
+ const char *key;
+ int ret = 0;
+ bool inbox = t->box->inbox_any;
+
+ iter = mailbox_attribute_iter_init(t->box, type, "");
+ while ((key = mailbox_attribute_iter_next(iter)) != NULL) {
+ if (inbox &&
+ str_begins(key, MAILBOX_ATTRIBUTE_PREFIX_DOVECOT_PVT_SERVER))
+ continue;
+
+ if (mailbox_attribute_unset(t, type, key) < 0) {
+ if (mailbox_get_last_mail_error(t->box) != MAIL_ERROR_NOTPOSSIBLE) {
+ ret = -1;
+ break;
+ }
+ }
+ }
+ if (mailbox_attribute_iter_deinit(&iter) < 0)
+ ret = -1;
+ return ret;
+}
+
+static int mailbox_expunge_all_data(struct mailbox *box)
+{
+ struct mail_search_context *ctx;
+ struct mailbox_transaction_context *t;
+ struct mail *mail;
+ struct mail_search_args *search_args;
+
+ (void)mailbox_sync(box, MAILBOX_SYNC_FLAG_FULL_READ);
+
+ t = mailbox_transaction_begin(box, 0, __func__);
+
+ search_args = mail_search_build_init();
+ mail_search_build_add_all(search_args);
+ ctx = mailbox_search_init(t, search_args, NULL, 0, NULL);
+ mail_search_args_unref(&search_args);
+
+ while (mailbox_search_next(ctx, &mail))
+ mail_expunge(mail);
+
+ if (mailbox_search_deinit(&ctx) < 0) {
+ mailbox_transaction_rollback(&t);
+ return -1;
+ }
+
+ if (mailbox_delete_all_attributes(t, MAIL_ATTRIBUTE_TYPE_PRIVATE) < 0 ||
+ mailbox_delete_all_attributes(t, MAIL_ATTRIBUTE_TYPE_SHARED) < 0) {
+ mailbox_transaction_rollback(&t);
+ return -1;
+ }
+ if (mailbox_transaction_commit(&t) < 0)
+ return -1;
+ /* sync to actually perform the expunges */
+ return mailbox_sync(box, 0);
+}
+
+int index_storage_mailbox_delete_pre(struct mailbox *box)
+{
+ struct mailbox_status status;
+
+ if (!box->opened) {
+ /* \noselect mailbox, try deleting only the directory */
+ if (index_storage_mailbox_delete_dir(box, FALSE) == 0)
+ return 0;
+ if (mailbox_is_autocreated(box)) {
+ /* Return success when trying to delete autocreated
+ mailbox. The client sees it as existing, so we
+ shouldn't be returning an error. */
+ return 0;
+ }
+ return -1;
+ }
+
+ if ((box->list->flags & MAILBOX_LIST_FLAG_MAILBOX_FILES) == 0) {
+ /* specifically support symlinked shared mailboxes. a deletion
+ will simply remove the symlink, not actually expunge any
+ mails */
+ if (mailbox_list_delete_symlink(box->list, box->name) == 0)
+ return 0;
+ }
+
+ /* we can't easily atomically delete all mails and the mailbox. so:
+ 1) expunge all mails
+ 2) mark the mailbox deleted (modifications after this will fail)
+ 3) check if a race condition between 1) and 2) added any mails:
+ yes) abort and undelete mailbox
+ no) finish deleting the mailbox
+ */
+
+ if (!box->deleting_must_be_empty) {
+ if (mailbox_expunge_all_data(box) < 0)
+ return -1;
+ }
+ if (mailbox_mark_index_deleted(box, TRUE) < 0)
+ return -1;
+
+ if (!box->delete_skip_empty_check || box->deleting_must_be_empty) {
+ if (mailbox_sync(box, MAILBOX_SYNC_FLAG_FULL_READ) < 0)
+ return -1;
+ mailbox_get_open_status(box, STATUS_MESSAGES, &status);
+ if (status.messages == 0)
+ ;
+ else if (box->deleting_must_be_empty) {
+ mail_storage_set_error(box->storage, MAIL_ERROR_EXISTS,
+ "Mailbox isn't empty");
+ return -1;
+ } else {
+ mail_storage_set_error(box->storage, MAIL_ERROR_EXISTS,
+ "New mails were added to mailbox during deletion");
+ return -1;
+ }
+ }
+ return 1;
+}
+
+int index_storage_mailbox_delete_post(struct mailbox *box)
+{
+ struct mailbox_metadata metadata;
+ int ret_guid;
+
+ ret_guid = mailbox_get_metadata(box, MAILBOX_METADATA_GUID, &metadata);
+
+ /* Make sure the indexes are closed before trying to delete the
+ directory that contains them. It can still fail with some NFS
+ implementations if indexes are opened by another session, but
+ that can't really be helped. */
+ mailbox_close(box);
+ index_storage_mailbox_unref_indexes(box);
+ mail_index_alloc_cache_destroy_unrefed();
+
+ if (box->list->v.delete_mailbox(box->list, box->name) < 0) {
+ mail_storage_copy_list_error(box->storage, box->list);
+ return -1;
+ }
+
+ if (ret_guid == 0) {
+ mailbox_list_add_change(box->list,
+ MAILBOX_LOG_RECORD_DELETE_MAILBOX,
+ metadata.guid);
+ }
+ if (index_storage_mailbox_delete_dir(box, TRUE) < 0) {
+ if (mailbox_get_last_mail_error(box) != MAIL_ERROR_EXISTS)
+ return -1;
+ /* we deleted the mailbox, but couldn't delete the directory
+ because it has children. that's not an error. */
+ }
+ return 0;
+}
+
+int index_storage_mailbox_delete(struct mailbox *box)
+{
+ int ret;
+
+ if ((ret = index_storage_mailbox_delete_pre(box)) <= 0)
+ return ret;
+ /* mails have been now successfully deleted. some mailbox formats may
+ at this point do some other deletion that is required for it.
+ the _post() deletion will close the index and delete the
+ directory. */
+ return index_storage_mailbox_delete_post(box);
+}
+
+int index_storage_mailbox_rename(struct mailbox *src, struct mailbox *dest)
+{
+ guid_128_t guid;
+
+ if (src->list->v.rename_mailbox(src->list, src->name,
+ dest->list, dest->name) < 0) {
+ mail_storage_copy_list_error(src->storage, src->list);
+ return -1;
+ }
+
+ if (mailbox_open(dest) == 0) {
+ struct mail_index_transaction *t =
+ mail_index_transaction_begin(dest->view, 0);
+
+ uint32_t stamp = ioloop_time;
+
+ mail_index_update_header_ext(t, dest->box_last_rename_stamp_ext_id,
+ 0, &stamp, sizeof(stamp));
+
+ /* can't do much if this fails anyways */
+ (void)mail_index_transaction_commit(&t);
+ }
+
+ /* we'll track mailbox names, instead of GUIDs. We may be renaming a
+ non-selectable mailbox (directory), which doesn't even have a GUID */
+ mailbox_name_get_sha128(dest->vname, guid);
+ mailbox_list_add_change(src->list, MAILBOX_LOG_RECORD_RENAME, guid);
+ return 0;
+}
+
+int index_mailbox_update_last_temp_file_scan(struct mailbox *box)
+{
+ uint32_t last_temp_file_scan = ioloop_time;
+ struct mail_index_transaction *trans =
+ mail_index_transaction_begin(box->view,
+ MAIL_INDEX_TRANSACTION_FLAG_EXTERNAL);
+ mail_index_update_header(trans,
+ offsetof(struct mail_index_header, last_temp_file_scan),
+ &last_temp_file_scan, sizeof(last_temp_file_scan), TRUE);
+ if (mail_index_transaction_commit(&trans) < 0) {
+ mailbox_set_index_error(box);
+ return -1;
+ }
+ return 0;
+}
+
+bool index_storage_is_readonly(struct mailbox *box)
+{
+ return (box->flags & MAILBOX_FLAG_READONLY) != 0;
+}
+
+bool index_storage_is_inconsistent(struct mailbox *box)
+{
+ return box->view != NULL &&
+ mail_index_view_is_inconsistent(box->view);
+}
+
+void index_save_context_free(struct mail_save_context *ctx)
+{
+ i_assert(ctx->dest_mail != NULL);
+
+ index_mail_save_finish(ctx);
+ if (ctx->data.keywords != NULL)
+ mailbox_keywords_unref(&ctx->data.keywords);
+ i_free_and_null(ctx->data.from_envelope);
+ i_free_and_null(ctx->data.guid);
+ i_free_and_null(ctx->data.pop3_uidl);
+ index_attachment_save_free(ctx);
+ i_zero(&ctx->data);
+
+ ctx->unfinished = FALSE;
+}
+
+static void
+mail_copy_cache_field(struct mail_save_context *ctx, struct mail *src_mail,
+ uint32_t dest_seq, const char *name, buffer_t *buf)
+{
+ struct mailbox_transaction_context *dest_trans = ctx->transaction;
+ const struct mail_cache_field *dest_field;
+ unsigned int src_field_idx, dest_field_idx;
+ uint32_t t;
+ bool add = FALSE;
+
+ src_field_idx = mail_cache_register_lookup(src_mail->box->cache, name);
+ i_assert(src_field_idx != UINT_MAX);
+
+ dest_field_idx = mail_cache_register_lookup(dest_trans->box->cache, name);
+ if (dest_field_idx == UINT_MAX) {
+ /* unknown field */
+ return;
+ }
+ dest_field = mail_cache_register_get_field(dest_trans->box->cache,
+ dest_field_idx);
+ if ((dest_field->decision &
+ ENUM_NEGATE(MAIL_CACHE_DECISION_FORCED)) == MAIL_CACHE_DECISION_NO) {
+ /* field not wanted in destination mailbox */
+ return;
+ }
+
+ buffer_set_used_size(buf, 0);
+ if (strcmp(name, "date.save") == 0) {
+ /* save date must update when mail is copied */
+ t = ioloop_time;
+ buffer_append(buf, &t, sizeof(t));
+ add = TRUE;
+ } else if (mail_cache_lookup_field(src_mail->transaction->cache_view, buf,
+ src_mail->seq, src_field_idx) <= 0) {
+ /* error / not found */
+ buffer_set_used_size(buf, 0);
+ } else {
+ if (strcmp(name, "size.physical") == 0 ||
+ strcmp(name, "size.virtual") == 0) {
+ /* FIXME: until mail_cache_lookup() can read unwritten
+ cached data from buffer, we'll do this optimization
+ to make quota plugin's work faster */
+ struct index_mail *imail =
+ INDEX_MAIL(ctx->dest_mail);
+ uoff_t size;
+
+ i_assert(buf->used == sizeof(size));
+ memcpy(&size, buf->data, sizeof(size));
+ if (strcmp(name, "size.physical") == 0)
+ imail->data.physical_size = size;
+ else
+ imail->data.virtual_size = size;
+ }
+ /* NOTE: we'll want to add also nonexistent headers, which
+ will keep the buf empty */
+ add = TRUE;
+ }
+ if (add) {
+ mail_cache_add(dest_trans->cache_trans, dest_seq,
+ dest_field_idx, buf->data, buf->used);
+ }
+}
+
+static void
+index_copy_vsize_extension(struct mail_save_context *ctx,
+ struct mail *src_mail, uint32_t dest_seq)
+{
+ const uint32_t *vsizep;
+ bool expunged ATTR_UNUSED;
+
+ vsizep = index_mail_get_vsize_extension(src_mail);
+ if (vsizep == NULL || *vsizep == 0)
+ return;
+ uint32_t vsize = *vsizep;
+
+ if (vsize < (uint32_t)-1) {
+ /* copy the vsize record to the destination index */
+ mail_index_update_ext(ctx->transaction->itrans, dest_seq,
+ ctx->transaction->box->mail_vsize_ext_id,
+ &vsize, NULL);
+ }
+}
+
+void index_copy_cache_fields(struct mail_save_context *ctx,
+ struct mail *src_mail, uint32_t dest_seq)
+{
+ T_BEGIN {
+ struct mailbox_metadata src_metadata, dest_metadata;
+ const struct mailbox_cache_field *field;
+ buffer_t *buf;
+
+ if (mailbox_get_metadata(src_mail->box,
+ MAILBOX_METADATA_CACHE_FIELDS,
+ &src_metadata) < 0)
+ i_unreached();
+ /* the only reason we're doing the destination lookup is to
+ make sure that the cache file is opened and the cache
+ decisions are up to date */
+ if (mailbox_get_metadata(ctx->transaction->box,
+ MAILBOX_METADATA_CACHE_FIELDS,
+ &dest_metadata) < 0)
+ i_unreached();
+
+ buf = t_buffer_create(1024);
+ array_foreach(src_metadata.cache_fields, field) {
+ mail_copy_cache_field(ctx, src_mail, dest_seq,
+ field->name, buf);
+ }
+ index_copy_vsize_extension(ctx, src_mail, dest_seq);
+ } T_END;
+}
+
+int index_storage_set_subscribed(struct mailbox *box, bool set)
+{
+ struct mail_namespace *ns;
+ struct mailbox_list *list = box->list;
+ const char *subs_name;
+ guid_128_t guid;
+
+ if ((list->ns->flags & NAMESPACE_FLAG_SUBSCRIPTIONS) != 0)
+ subs_name = box->name;
+ else {
+ /* subscriptions=no namespace, find another one where we can
+ add the subscription to */
+ ns = mail_namespace_find_subscribable(list->ns->user->namespaces,
+ box->vname);
+ if (ns == NULL) {
+ mail_storage_set_error(box->storage, MAIL_ERROR_NOTPOSSIBLE,
+ "This namespace has no subscriptions");
+ return -1;
+ }
+ /* use <orig ns prefix><orig storage name> as the
+ subscription name */
+ subs_name = t_strconcat(list->ns->prefix, box->name, NULL);
+ /* drop the common prefix (typically there isn't one) */
+ i_assert(str_begins(subs_name, ns->prefix));
+ subs_name += strlen(ns->prefix);
+
+ list = ns->list;
+ }
+ if (mailbox_list_set_subscribed(list, subs_name, set) < 0) {
+ mail_storage_copy_list_error(box->storage, list);
+ return -1;
+ }
+
+ /* subscriptions are about names, not about mailboxes. it's possible
+ to have a subscription to nonexistent mailbox. renames also don't
+ change subscriptions. so instead of using actual GUIDs, we'll use
+ hash of the name. */
+ mailbox_name_get_sha128(box->vname, guid);
+ mailbox_list_add_change(list, set ? MAILBOX_LOG_RECORD_SUBSCRIBE :
+ MAILBOX_LOG_RECORD_UNSUBSCRIBE, guid);
+ return 0;
+}
+
+void index_storage_destroy(struct mail_storage *storage)
+{
+ if (storage->_shared_attr_dict != NULL) {
+ dict_wait(storage->_shared_attr_dict);
+ dict_deinit(&storage->_shared_attr_dict);
+ }
+ fs_unref(&storage->mailboxes_fs);
+}
+
+static void index_storage_expunging_init(struct mailbox *box)
+{
+ struct index_mailbox_context *ibox = INDEX_STORAGE_CONTEXT(box);
+
+ if (ibox->vsize_update != NULL)
+ return;
+
+ ibox->vsize_update = index_mailbox_vsize_update_init(box);
+ if (!index_mailbox_vsize_want_updates(ibox->vsize_update) ||
+ !index_mailbox_vsize_update_wait_lock(ibox->vsize_update))
+ index_mailbox_vsize_update_deinit(&ibox->vsize_update);
+}
+
+void index_storage_expunging_deinit(struct mailbox *box)
+{
+ struct index_mailbox_context *ibox = INDEX_STORAGE_CONTEXT(box);
+
+ if (ibox->vsize_update != NULL)
+ index_mailbox_vsize_update_deinit(&ibox->vsize_update);
+}
+
+static bool index_storage_expunging_want_updates(struct mailbox *box)
+{
+ struct index_mailbox_context *ibox = INDEX_STORAGE_CONTEXT(box);
+ bool ret;
+
+ i_assert(ibox->vsize_update == NULL);
+
+ ibox->vsize_update = index_mailbox_vsize_update_init(box);
+ ret = index_mailbox_vsize_want_updates(ibox->vsize_update);
+ index_mailbox_vsize_update_deinit(&ibox->vsize_update);
+ return ret;
+}
+
+int index_storage_expunged_sync_begin(struct mailbox *box,
+ struct mail_index_sync_ctx **ctx_r,
+ struct mail_index_view **view_r,
+ struct mail_index_transaction **trans_r,
+ enum mail_index_sync_flags flags)
+{
+ struct index_mailbox_context *ibox = INDEX_STORAGE_CONTEXT(box);
+ int ret;
+
+ /* try to avoid locking vsize updates by checking if we see any
+ expunges */
+ if (mail_index_sync_have_any_expunges(box->index))
+ index_storage_expunging_init(box);
+
+ ret = mail_index_sync_begin(box->index, ctx_r, view_r,
+ trans_r, flags);
+ if (ret <= 0) {
+ if (ret < 0)
+ mailbox_set_index_error(box);
+ index_storage_expunging_deinit(box);
+ return ret;
+ }
+ if (ibox->vsize_update == NULL &&
+ mail_index_sync_has_expunges(*ctx_r) &&
+ index_storage_expunging_want_updates(box)) {
+ /* race condition - need to abort the sync and retry with
+ the vsize locked */
+ mail_index_sync_rollback(ctx_r);
+ index_storage_expunging_deinit(box);
+ return index_storage_expunged_sync_begin(box, ctx_r, view_r,
+ trans_r, flags);
+ }
+ return 1;
+}
+
+int index_storage_save_continue(struct mail_save_context *ctx,
+ struct istream *input,
+ struct mail *cache_dest_mail)
+{
+ struct mail_storage *storage = ctx->transaction->box->storage;
+
+ do {
+ switch (o_stream_send_istream(ctx->data.output, input)) {
+ case OSTREAM_SEND_ISTREAM_RESULT_FINISHED:
+ break;
+ case OSTREAM_SEND_ISTREAM_RESULT_WAIT_INPUT:
+ break;
+ case OSTREAM_SEND_ISTREAM_RESULT_WAIT_OUTPUT:
+ i_unreached();
+ case OSTREAM_SEND_ISTREAM_RESULT_ERROR_INPUT:
+ /* handle below */
+ break;
+ case OSTREAM_SEND_ISTREAM_RESULT_ERROR_OUTPUT:
+ if (!mail_storage_set_error_from_errno(storage)) {
+ mail_set_critical(ctx->dest_mail,
+ "save: write(%s) failed: %s",
+ o_stream_get_name(ctx->data.output),
+ o_stream_get_error(ctx->data.output));
+ }
+ return -1;
+ }
+ if (cache_dest_mail != NULL)
+ index_mail_cache_parse_continue(cache_dest_mail);
+
+ /* both tee input readers may consume data from our primary
+ input stream. we'll have to make sure we don't return with
+ one of the streams still having data in them. */
+ } while (i_stream_read(input) > 0);
+
+ if (input->stream_errno != 0) {
+ mail_set_critical(ctx->dest_mail, "save: read(%s) failed: %s",
+ i_stream_get_name(input), i_stream_get_error(input));
+ return -1;
+ }
+ return 0;
+}
+
+void index_storage_save_abort_last(struct mail_save_context *ctx, uint32_t seq)
+{
+ struct index_mail *imail = INDEX_MAIL(ctx->dest_mail);
+
+ /* Close the mail before it's expunged. This allows it to be
+ reset cleanly. */
+ imail->data.no_caching = TRUE;
+ imail->mail.v.close(&imail->mail.mail);
+
+ mail_index_expunge(ctx->transaction->itrans, seq);
+ /* currently we can't just drop pending cache updates for this one
+ specific record, so we'll reset the whole cache transaction. */
+ mail_cache_transaction_reset(ctx->transaction->cache_trans);
+}
+
+int index_mailbox_fix_inconsistent_existence(struct mailbox *box,
+ const char *path)
+{
+ const char *index_path;
+ struct stat st;
+
+ /* Could be a race condition or could be because ITERINDEX is used
+ and the index directory exists, but the storage directory doesn't.
+ Handle the existence inconsistency by creating this directory if
+ the index directory exists (don't bother checking if ITERINDEX is
+ set or not - it doesn't matter since either both dirs should exist
+ or not). */
+ if (mailbox_get_path_to(box, MAILBOX_LIST_PATH_TYPE_INDEX,
+ &index_path) < 0)
+ return -1;
+
+ if (strcmp(index_path, path) == 0) {
+ /* there's no separate index path - mailbox was just deleted */
+ } else if (stat(index_path, &st) == 0) {
+ /* inconsistency - create also the mail directory */
+ return mailbox_mkdir(box, path, MAILBOX_LIST_PATH_TYPE_MAILBOX);
+ } else if (errno == ENOENT) {
+ /* race condition - mailbox was just deleted */
+ } else {
+ mailbox_set_critical(box, "stat(%s) failed: %m", index_path);
+ return -1;
+ }
+ mailbox_set_deleted(box);
+ return -1;
+}
diff --git a/src/lib-storage/index/index-storage.h b/src/lib-storage/index/index-storage.h
new file mode 100644
index 0000000..55cf459
--- /dev/null
+++ b/src/lib-storage/index/index-storage.h
@@ -0,0 +1,193 @@
+#ifndef INDEX_STORAGE_H
+#define INDEX_STORAGE_H
+
+#include "file-dotlock.h"
+#include "mail-storage-private.h"
+#include "mail-index-private.h"
+#include "mailbox-watch.h"
+
+#define MAILBOX_FULL_SYNC_INTERVAL 5
+
+enum mailbox_lock_notify_type {
+ MAILBOX_LOCK_NOTIFY_NONE,
+
+ /* Mailbox is locked, will abort in secs_left */
+ MAILBOX_LOCK_NOTIFY_MAILBOX_ABORT,
+ /* Mailbox lock looks stale, will override in secs_left */
+ MAILBOX_LOCK_NOTIFY_MAILBOX_OVERRIDE
+};
+
+enum index_storage_list_change {
+ INDEX_STORAGE_LIST_CHANGE_ERROR = -1,
+ INDEX_STORAGE_LIST_CHANGE_NONE = 0,
+ INDEX_STORAGE_LIST_CHANGE_INMEMORY,
+ INDEX_STORAGE_LIST_CHANGE_NORECORD,
+ INDEX_STORAGE_LIST_CHANGE_NOT_IN_FS,
+ INDEX_STORAGE_LIST_CHANGE_SIZE_CHANGED,
+ INDEX_STORAGE_LIST_CHANGE_MTIME_CHANGED
+};
+
+struct index_mailbox_context {
+ union mailbox_module_context module_ctx;
+ enum mail_index_open_flags index_flags;
+
+ time_t next_lock_notify; /* temporary */
+ enum mailbox_lock_notify_type last_notify_type;
+
+ const ARRAY_TYPE(keywords) *keyword_names;
+ struct mail_cache_field *cache_fields;
+
+ struct mailbox_vsize_update *vsize_update;
+
+ uint32_t recent_flags_prev_first_recent_uid;
+ uint32_t recent_flags_last_check_nextuid;
+
+ time_t sync_last_check;
+ uint32_t list_index_sync_ext_id;
+};
+
+#define INDEX_STORAGE_CONTEXT(obj) \
+ MODULE_CONTEXT_REQUIRE(obj, index_storage_module)
+extern MODULE_CONTEXT_DEFINE(index_storage_module,
+ &mail_storage_module_register);
+
+void index_storage_lock_notify(struct mailbox *box,
+ enum mailbox_lock_notify_type notify_type,
+ unsigned int secs_left);
+void index_storage_lock_notify_reset(struct mailbox *box);
+
+int index_storage_mailbox_alloc_index(struct mailbox *box);
+void index_storage_mailbox_alloc(struct mailbox *box, const char *vname,
+ enum mailbox_flags flags,
+ const char *index_prefix);
+int index_storage_mailbox_exists(struct mailbox *box, bool auto_boxes,
+ enum mailbox_existence *existence_r);
+int index_storage_mailbox_exists_full(struct mailbox *box, const char *subdir,
+ enum mailbox_existence *existence_r)
+ ATTR_NULL(2);
+int index_storage_mailbox_open(struct mailbox *box, bool move_to_memory);
+int index_storage_mailbox_enable(struct mailbox *box,
+ enum mailbox_feature feature);
+void index_storage_mailbox_close(struct mailbox *box);
+void index_storage_mailbox_free(struct mailbox *box);
+int index_storage_mailbox_update(struct mailbox *box,
+ const struct mailbox_update *update);
+int index_storage_mailbox_update_common(struct mailbox *box,
+ const struct mailbox_update *update);
+int index_storage_mailbox_create(struct mailbox *box, bool directory);
+int index_storage_mailbox_delete_pre(struct mailbox *box);
+int index_storage_mailbox_delete_post(struct mailbox *box);
+int index_storage_mailbox_delete(struct mailbox *box);
+int index_storage_mailbox_delete_dir(struct mailbox *box, bool mailbox_deleted);
+int index_storage_mailbox_rename(struct mailbox *src, struct mailbox *dest);
+
+int index_mailbox_update_last_temp_file_scan(struct mailbox *box);
+int index_mailbox_fix_inconsistent_existence(struct mailbox *box,
+ const char *path);
+
+bool index_storage_is_readonly(struct mailbox *box);
+bool index_storage_is_inconsistent(struct mailbox *box);
+
+enum mail_index_sync_flags index_storage_get_sync_flags(struct mailbox *box);
+bool index_mailbox_want_full_sync(struct mailbox *box,
+ enum mailbox_sync_flags flags);
+struct mailbox_sync_context *
+index_mailbox_sync_init(struct mailbox *box, enum mailbox_sync_flags flags,
+ bool failed);
+bool index_mailbox_sync_next(struct mailbox_sync_context *ctx,
+ struct mailbox_sync_rec *sync_rec_r);
+int index_mailbox_sync_deinit(struct mailbox_sync_context *ctx,
+ struct mailbox_sync_status *status_r);
+
+int index_storage_sync(struct mailbox *box, enum mailbox_sync_flags flags);
+enum mailbox_sync_type index_sync_type_convert(enum mail_index_sync_type type);
+void index_sync_update_recent_count(struct mailbox *box);
+int index_storage_get_status(struct mailbox *box,
+ enum mailbox_status_items items,
+ struct mailbox_status *status_r);
+void index_storage_get_open_status(struct mailbox *box,
+ enum mailbox_status_items items,
+ struct mailbox_status *status_r);
+int index_mailbox_get_metadata(struct mailbox *box,
+ enum mailbox_metadata_items items,
+ struct mailbox_metadata *metadata_r);
+int index_mailbox_get_virtual_size(struct mailbox *box,
+ struct mailbox_metadata *metadata_r);
+int index_mailbox_get_physical_size(struct mailbox *box,
+ struct mailbox_metadata *metadata_r);
+
+int index_storage_attribute_set(struct mailbox_transaction_context *t,
+ enum mail_attribute_type type_flags,
+ const char *key,
+ const struct mail_attribute_value *value);
+int index_storage_attribute_get(struct mailbox *box,
+ enum mail_attribute_type type_flags,
+ const char *key,
+ struct mail_attribute_value *value_r);
+struct mailbox_attribute_iter *
+index_storage_attribute_iter_init(struct mailbox *box,
+ enum mail_attribute_type type_flags,
+ const char *prefix);
+const char *
+index_storage_attribute_iter_next(struct mailbox_attribute_iter *iter);
+int index_storage_attribute_iter_deinit(struct mailbox_attribute_iter *iter);
+
+struct mail_search_context *
+index_storage_search_init(struct mailbox_transaction_context *t,
+ struct mail_search_args *args,
+ const enum mail_sort_type *sort_program,
+ enum mail_fetch_field wanted_fields,
+ struct mailbox_header_lookup_ctx *wanted_headers);
+int index_storage_search_deinit(struct mail_search_context *ctx);
+bool index_storage_search_next_nonblock(struct mail_search_context *ctx,
+ struct mail **mail_r, bool *tryagain_r);
+bool index_storage_search_next_update_seq(struct mail_search_context *ctx);
+int index_storage_search_next_match_mail(struct mail_search_context *ctx,
+ struct mail *mail);
+
+struct mailbox_transaction_context *
+index_transaction_begin(struct mailbox *box,
+ enum mailbox_transaction_flags flags,
+ const char *reason);
+void index_transaction_init(struct mailbox_transaction_context *t,
+ struct mailbox *box,
+ enum mailbox_transaction_flags flags,
+ const char *reason);
+void index_transaction_init_pvt(struct mailbox_transaction_context *t);
+int index_transaction_commit(struct mailbox_transaction_context *t,
+ struct mail_transaction_commit_changes *changes_r);
+void index_transaction_rollback(struct mailbox_transaction_context *t);
+void index_save_context_free(struct mail_save_context *ctx);
+void index_copy_cache_fields(struct mail_save_context *ctx,
+ struct mail *src_mail, uint32_t dest_seq);
+int index_storage_set_subscribed(struct mailbox *box, bool set);
+void index_storage_destroy(struct mail_storage *storage);
+
+bool index_keyword_array_cmp(const ARRAY_TYPE(keyword_indexes) *k1,
+ const ARRAY_TYPE(keyword_indexes) *k2);
+
+int index_storage_list_index_has_changed(struct mailbox *box,
+ struct mail_index_view *list_view,
+ uint32_t seq, bool quick,
+ const char **reason_r);
+enum index_storage_list_change
+index_storage_list_index_has_changed_full(struct mailbox *box,
+ struct mail_index_view *list_view,
+ uint32_t seq, const char **reason_r);
+void index_storage_list_index_update_sync(struct mailbox *box,
+ struct mail_index_transaction *trans,
+ uint32_t seq);
+
+int index_storage_expunged_sync_begin(struct mailbox *box,
+ struct mail_index_sync_ctx **ctx_r,
+ struct mail_index_view **view_r,
+ struct mail_index_transaction **trans_r,
+ enum mail_index_sync_flags flags);
+void index_storage_expunging_deinit(struct mailbox *box);
+
+int index_storage_save_continue(struct mail_save_context *ctx,
+ struct istream *input,
+ struct mail *cache_dest_mail);
+void index_storage_save_abort_last(struct mail_save_context *ctx, uint32_t seq);
+
+#endif
diff --git a/src/lib-storage/index/index-sync-changes.c b/src/lib-storage/index/index-sync-changes.c
new file mode 100644
index 0000000..6d8cd99
--- /dev/null
+++ b/src/lib-storage/index/index-sync-changes.c
@@ -0,0 +1,200 @@
+/* Copyright (c) 2007-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "index-storage.h"
+#include "index-sync-changes.h"
+
+struct index_sync_changes_context {
+ struct mail_index_sync_ctx *index_sync_ctx;
+ struct mail_index_view *sync_view;
+ struct mail_index_transaction *sync_trans;
+
+ ARRAY(struct mail_index_sync_rec) syncs;
+ struct mail_index_sync_rec sync_rec;
+ bool dirty_flag_updates;
+};
+
+struct index_sync_changes_context *
+index_sync_changes_init(struct mail_index_sync_ctx *index_sync_ctx,
+ struct mail_index_view *sync_view,
+ struct mail_index_transaction *sync_trans,
+ bool dirty_flag_updates)
+{
+ struct index_sync_changes_context *ctx;
+
+ ctx = i_new(struct index_sync_changes_context, 1);
+ ctx->index_sync_ctx = index_sync_ctx;
+ ctx->sync_view = sync_view;
+ ctx->sync_trans = sync_trans;
+ ctx->dirty_flag_updates = dirty_flag_updates;
+ i_array_init(&ctx->syncs, 16);
+ return ctx;
+}
+
+void index_sync_changes_deinit(struct index_sync_changes_context **_ctx)
+{
+ struct index_sync_changes_context *ctx = *_ctx;
+
+ *_ctx = NULL;
+ array_free(&ctx->syncs);
+ i_free(ctx);
+}
+
+void index_sync_changes_reset(struct index_sync_changes_context *ctx)
+{
+ array_clear(&ctx->syncs);
+ i_zero(&ctx->sync_rec);
+}
+
+void index_sync_changes_delete_to(struct index_sync_changes_context *ctx,
+ uint32_t last_uid)
+{
+ struct mail_index_sync_rec *syncs;
+ unsigned int src, dest, count;
+
+ syncs = array_get_modifiable(&ctx->syncs, &count);
+
+ for (src = dest = 0; src < count; src++) {
+ i_assert(last_uid >= syncs[src].uid1);
+ if (last_uid <= syncs[src].uid2) {
+ /* keep it */
+ if (src != dest)
+ syncs[dest] = syncs[src];
+ dest++;
+ }
+ }
+
+ array_delete(&ctx->syncs, dest, count - dest);
+}
+
+static bool
+index_sync_changes_have_expunges(struct index_sync_changes_context *ctx,
+ unsigned int count,
+ guid_128_t expunged_guid_128_r)
+{
+ const struct mail_index_sync_rec *syncs;
+ unsigned int i;
+
+ syncs = array_front(&ctx->syncs);
+ for (i = 0; i < count; i++) {
+ if (syncs[i].type == MAIL_INDEX_SYNC_TYPE_EXPUNGE) {
+ memcpy(expunged_guid_128_r, syncs[i].guid_128,
+ GUID_128_SIZE);
+ return TRUE;
+ }
+ }
+ return FALSE;
+}
+
+void index_sync_changes_read(struct index_sync_changes_context *ctx,
+ uint32_t uid, bool *sync_expunge_r,
+ guid_128_t expunged_guid_128_r)
+{
+ struct mail_index_sync_rec *sync_rec = &ctx->sync_rec;
+ uint32_t seq1, seq2;
+ unsigned int orig_count;
+
+ *sync_expunge_r = FALSE;
+
+ index_sync_changes_delete_to(ctx, uid);
+ orig_count = array_count(&ctx->syncs);
+
+ while (uid >= sync_rec->uid1) {
+ if (uid <= sync_rec->uid2) {
+ array_push_back(&ctx->syncs, sync_rec);
+
+ if (sync_rec->type == MAIL_INDEX_SYNC_TYPE_EXPUNGE) {
+ *sync_expunge_r = TRUE;
+ memcpy(expunged_guid_128_r, sync_rec->guid_128,
+ GUID_128_SIZE);
+ }
+ }
+
+ if (!mail_index_sync_next(ctx->index_sync_ctx, sync_rec)) {
+ i_zero(sync_rec);
+ break;
+ }
+
+ switch (sync_rec->type) {
+ case MAIL_INDEX_SYNC_TYPE_EXPUNGE:
+ break;
+ case MAIL_INDEX_SYNC_TYPE_FLAGS:
+ case MAIL_INDEX_SYNC_TYPE_KEYWORD_ADD:
+ case MAIL_INDEX_SYNC_TYPE_KEYWORD_REMOVE:
+ if (!ctx->dirty_flag_updates)
+ break;
+
+ /* mark the changes as dirty */
+ (void)mail_index_lookup_seq_range(ctx->sync_view,
+ sync_rec->uid1,
+ sync_rec->uid2,
+ &seq1, &seq2);
+ i_zero(sync_rec);
+
+ if (seq1 == 0)
+ break;
+
+ mail_index_update_flags_range(ctx->sync_trans,
+ seq1, seq2, MODIFY_ADD,
+ (enum mail_flags)MAIL_INDEX_MAIL_FLAG_DIRTY);
+ break;
+ }
+ }
+
+ if (!*sync_expunge_r && orig_count > 0) {
+ *sync_expunge_r =
+ index_sync_changes_have_expunges(ctx, orig_count,
+ expunged_guid_128_r);
+ }
+}
+
+bool index_sync_changes_have(struct index_sync_changes_context *ctx)
+{
+ return array_count(&ctx->syncs) > 0;
+}
+
+uint32_t
+index_sync_changes_get_next_uid(struct index_sync_changes_context *ctx)
+{
+ return ctx->sync_rec.uid1;
+}
+
+void index_sync_changes_apply(struct index_sync_changes_context *ctx,
+ pool_t pool, uint8_t *flags,
+ ARRAY_TYPE(keyword_indexes) *keywords,
+ enum mail_index_sync_type *sync_type_r)
+{
+ const struct mail_index_sync_rec *syncs;
+ unsigned int i, count;
+ enum mail_index_sync_type sync_type = 0;
+
+ syncs = array_get(&ctx->syncs, &count);
+ for (i = 0; i < count; i++) {
+ switch (syncs[i].type) {
+ case MAIL_INDEX_SYNC_TYPE_FLAGS:
+ mail_index_sync_flags_apply(&syncs[i], flags);
+ sync_type |= MAIL_INDEX_SYNC_TYPE_FLAGS;
+ break;
+ case MAIL_INDEX_SYNC_TYPE_KEYWORD_ADD:
+ case MAIL_INDEX_SYNC_TYPE_KEYWORD_REMOVE:
+ if (!array_is_created(keywords)) {
+ /* no existing keywords */
+ if (syncs[i].type !=
+ MAIL_INDEX_SYNC_TYPE_KEYWORD_ADD)
+ break;
+
+ /* adding, create the array */
+ p_array_init(keywords, pool,
+ I_MIN(10, count - i));
+ }
+ if (mail_index_sync_keywords_apply(&syncs[i], keywords))
+ sync_type |= syncs[i].type;
+ break;
+ default:
+ break;
+ }
+ }
+
+ *sync_type_r = sync_type;
+}
diff --git a/src/lib-storage/index/index-sync-changes.h b/src/lib-storage/index/index-sync-changes.h
new file mode 100644
index 0000000..e6f5d47
--- /dev/null
+++ b/src/lib-storage/index/index-sync-changes.h
@@ -0,0 +1,28 @@
+#ifndef INDEX_SYNC_CHANGES_H
+#define INDEX_SYNC_CHANGES_H
+
+struct index_sync_changes_context *
+index_sync_changes_init(struct mail_index_sync_ctx *index_sync_ctx,
+ struct mail_index_view *sync_view,
+ struct mail_index_transaction *sync_trans,
+ bool dirty_flag_updates);
+void index_sync_changes_deinit(struct index_sync_changes_context **_ctx);
+
+void index_sync_changes_reset(struct index_sync_changes_context *ctx);
+void index_sync_changes_delete_to(struct index_sync_changes_context *ctx,
+ uint32_t last_uid);
+
+void index_sync_changes_read(struct index_sync_changes_context *ctx,
+ uint32_t uid, bool *sync_expunge_r,
+ guid_128_t expunged_guid_128);
+bool index_sync_changes_have(struct index_sync_changes_context *ctx);
+uint32_t
+index_sync_changes_get_next_uid(struct index_sync_changes_context *ctx);
+
+void index_sync_changes_apply(struct index_sync_changes_context *ctx,
+ pool_t pool, uint8_t *flags,
+ ARRAY_TYPE(keyword_indexes) *keywords,
+ enum mail_index_sync_type *sync_type_r)
+ ATTR_NULL(2);
+
+#endif
diff --git a/src/lib-storage/index/index-sync-private.h b/src/lib-storage/index/index-sync-private.h
new file mode 100644
index 0000000..7d670c4
--- /dev/null
+++ b/src/lib-storage/index/index-sync-private.h
@@ -0,0 +1,38 @@
+#ifndef INDEX_SYNC_PRIVATE_H
+#define INDEX_SYNC_PRIVATE_H
+
+#include "index-storage.h"
+
+struct index_mailbox_sync_pvt_context;
+
+struct index_mailbox_sync_context {
+ struct mailbox_sync_context ctx;
+
+ struct mail_index_view_sync_ctx *sync_ctx;
+ uint32_t messages_count;
+
+ ARRAY_TYPE(seq_range) flag_updates;
+ ARRAY_TYPE(seq_range) hidden_updates;
+ ARRAY_TYPE(seq_range) all_flag_update_uids;
+ const ARRAY_TYPE(seq_range) *expunges;
+ unsigned int flag_update_idx, hidden_update_idx, expunge_pos;
+
+ bool failed;
+};
+
+void index_sync_search_results_uidify(struct index_mailbox_sync_context *ctx);
+void index_sync_search_results_update(struct index_mailbox_sync_context *ctx);
+void index_sync_search_results_expunge(struct index_mailbox_sync_context *ctx);
+
+/* Returns 1 = ok, 0 = no private indexes, -1 = error */
+int index_mailbox_sync_pvt_init(struct mailbox *box, bool lock,
+ enum mail_index_view_sync_flags flags,
+ struct index_mailbox_sync_pvt_context **ctx_r);
+int index_mailbox_sync_pvt_newmails(struct index_mailbox_sync_pvt_context *ctx,
+ struct mailbox_transaction_context *trans);
+int index_mailbox_sync_pvt_view(struct index_mailbox_sync_pvt_context *ctx,
+ ARRAY_TYPE(seq_range) *flag_updates,
+ ARRAY_TYPE(seq_range) *hidden_updates);
+void index_mailbox_sync_pvt_deinit(struct index_mailbox_sync_pvt_context **ctx);
+
+#endif
diff --git a/src/lib-storage/index/index-sync-pvt.c b/src/lib-storage/index/index-sync-pvt.c
new file mode 100644
index 0000000..92eb4e8
--- /dev/null
+++ b/src/lib-storage/index/index-sync-pvt.c
@@ -0,0 +1,345 @@
+/* Copyright (c) 2013-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "mailbox-list-private.h"
+#include "index-sync-private.h"
+
+struct index_mailbox_sync_pvt_context {
+ struct mailbox *box;
+
+ struct mail_index_sync_ctx *sync_ctx;
+ struct mail_index_view *view_pvt;
+ struct mail_index_transaction *trans_pvt;
+ struct mail_index_view *view_shared;
+
+ enum mail_index_view_sync_flags flags;
+};
+
+static int sync_pvt_expunges(struct index_mailbox_sync_pvt_context *ctx)
+{
+ uint32_t seq_shared, seq_pvt, count_shared, count_pvt;
+ uint32_t uid_shared, uid_pvt;
+
+ count_shared = mail_index_view_get_messages_count(ctx->view_shared);
+ count_pvt = mail_index_view_get_messages_count(ctx->view_pvt);
+ seq_shared = seq_pvt = 1;
+ while (seq_pvt <= count_pvt && seq_shared <= count_shared) {
+ mail_index_lookup_uid(ctx->view_pvt, seq_pvt, &uid_pvt);
+ mail_index_lookup_uid(ctx->view_shared, seq_shared, &uid_shared);
+ if (uid_pvt == uid_shared) {
+ seq_pvt++;
+ seq_shared++;
+ } else if (uid_pvt < uid_shared) {
+ /* message expunged */
+ mail_index_expunge(ctx->trans_pvt, seq_pvt);
+ seq_pvt++;
+ } else {
+ mailbox_set_critical(ctx->box,
+ "%s: Message UID=%u unexpectedly inserted to mailbox",
+ ctx->box->index_pvt->filepath, uid_shared);
+ return -1;
+ }
+ }
+ return 0;
+}
+
+static void
+sync_pvt_copy_self_flags(struct index_mailbox_sync_pvt_context *ctx,
+ ARRAY_TYPE(keyword_indexes) *keywords,
+ uint32_t seq_old, uint32_t seq_new)
+{
+ const struct mail_index_record *old_rec;
+
+ old_rec = mail_index_lookup(ctx->view_pvt, seq_old);
+ mail_index_lookup_keywords(ctx->view_pvt, seq_old, keywords);
+ if (old_rec->flags != 0) {
+ mail_index_update_flags(ctx->trans_pvt, seq_new,
+ MODIFY_ADD, old_rec->flags);
+ }
+ if (array_count(keywords) > 0) {
+ struct mail_keywords *kw;
+
+ kw = mail_index_keywords_create_from_indexes(ctx->box->index_pvt,
+ keywords);
+ mail_index_update_keywords(ctx->trans_pvt, seq_new,
+ MODIFY_ADD, kw);
+ mail_index_keywords_unref(&kw);
+ }
+}
+
+static void
+sync_pvt_copy_shared_flags(struct index_mailbox_sync_pvt_context *ctx,
+ uint32_t seq_shared, uint32_t seq_pvt)
+{
+ const struct mail_index_record *rec;
+
+ rec = mail_index_lookup(ctx->view_shared, seq_shared);
+ mail_index_update_flags(ctx->trans_pvt, seq_pvt, MODIFY_ADD,
+ rec->flags & mailbox_get_private_flags_mask(ctx->box));
+}
+
+static int
+index_mailbox_sync_view_refresh(struct index_mailbox_sync_pvt_context *ctx)
+{
+ /* open a view for the latest version of the index */
+ if (mail_index_refresh(ctx->box->index_pvt) < 0 ||
+ mail_index_refresh(ctx->box->index) < 0) {
+ mailbox_set_index_error(ctx->box);
+ return -1;
+ }
+ if (ctx->view_shared != NULL)
+ mail_index_view_close(&ctx->view_shared);
+ ctx->view_shared = mail_index_view_open(ctx->box->index);
+ return 0;
+}
+
+static int
+index_mailbox_sync_open(struct index_mailbox_sync_pvt_context *ctx, bool force)
+{
+ const struct mail_index_header *hdr_shared, *hdr_pvt;
+
+ if (index_mailbox_sync_view_refresh(ctx) < 0)
+ return -1;
+
+ hdr_shared = mail_index_get_header(ctx->view_shared);
+ if (hdr_shared->uid_validity == 0 && !force) {
+ /* the mailbox hasn't been fully created yet,
+ no need for a private index yet */
+ return 0;
+ }
+ hdr_pvt = mail_index_get_header(ctx->box->view_pvt);
+ if (hdr_pvt->next_uid == hdr_shared->next_uid &&
+ hdr_pvt->messages_count == hdr_shared->messages_count && !force) {
+ /* no new or expunged mails, don't bother syncing */
+ return 0;
+ }
+ if (mail_index_sync_begin(ctx->box->index_pvt, &ctx->sync_ctx,
+ &ctx->view_pvt, &ctx->trans_pvt, 0) < 0) {
+ mailbox_set_index_error(ctx->box);
+ return -1;
+ }
+ /* refresh once more now that we're locked */
+ if (index_mailbox_sync_view_refresh(ctx) < 0)
+ return -1;
+ return 1;
+}
+
+int index_mailbox_sync_pvt_init(struct mailbox *box, bool lock,
+ enum mail_index_view_sync_flags flags,
+ struct index_mailbox_sync_pvt_context **ctx_r)
+{
+ struct index_mailbox_sync_pvt_context *ctx;
+ int ret;
+
+ *ctx_r = NULL;
+
+ if ((ret = mailbox_open_index_pvt(box)) <= 0)
+ return ret;
+
+ ctx = i_new(struct index_mailbox_sync_pvt_context, 1);
+ ctx->box = box;
+ ctx->flags = flags;
+ if (lock) {
+ if (index_mailbox_sync_open(ctx, TRUE) < 0) {
+ index_mailbox_sync_pvt_deinit(&ctx);
+ return -1;
+ }
+ }
+
+ *ctx_r = ctx;
+ return 1;
+}
+
+static int
+index_mailbox_sync_pvt_index(struct index_mailbox_sync_pvt_context *ctx,
+ const struct mail_save_private_changes *pvt_changes,
+ unsigned int pvt_changes_count)
+{
+ const struct mail_index_header *hdr_shared, *hdr_pvt;
+ ARRAY_TYPE(keyword_indexes) keywords;
+ uint32_t seq_shared, seq_pvt, seq_old_pvt, seq2, count_shared, uid;
+ unsigned int pc_idx = 0;
+ bool reset = FALSE, preserve_old_flags = FALSE, copy_shared_flags;
+ bool initial_index = FALSE;
+ int ret;
+
+ if (ctx->sync_ctx == NULL) {
+ if ((ret = index_mailbox_sync_open(ctx, FALSE)) <= 0)
+ return ret;
+ }
+ hdr_pvt = mail_index_get_header(ctx->view_pvt);
+ hdr_shared = mail_index_get_header(ctx->view_shared);
+
+ if (hdr_shared->uid_validity == hdr_pvt->uid_validity) {
+ /* same mailbox. expunge messages from private index that
+ no longer exist. */
+ if (sync_pvt_expunges(ctx) < 0) {
+ reset = TRUE;
+ preserve_old_flags = TRUE;
+ t_array_init(&keywords, 32);
+ }
+ } else if (hdr_pvt->uid_validity == 0 && hdr_pvt->next_uid <= 1) {
+ /* creating the initial index - no logging */
+ reset = TRUE;
+ initial_index = TRUE;
+ } else {
+ /* mailbox created/recreated */
+ reset = TRUE;
+ i_info("Mailbox %s UIDVALIDITY changed (%u -> %u), reseting private index",
+ ctx->box->vname, hdr_pvt->uid_validity,
+ hdr_shared->uid_validity);
+ }
+ /* for public namespaces copy the initial private flags from the shared
+ index. this allows Sieve scripts to set the initial flags. */
+ copy_shared_flags =
+ ctx->box->list->ns->type == MAIL_NAMESPACE_TYPE_PUBLIC;
+
+ count_shared = mail_index_view_get_messages_count(ctx->view_shared);
+ if (!reset) {
+ if (!mail_index_lookup_seq_range(ctx->view_shared,
+ hdr_pvt->next_uid,
+ hdr_shared->next_uid,
+ &seq_shared, &seq2)) {
+ /* no new messages */
+ seq_shared = count_shared+1;
+ }
+ } else {
+ if (!initial_index)
+ mail_index_reset(ctx->trans_pvt);
+ mail_index_update_header(ctx->trans_pvt,
+ offsetof(struct mail_index_header, uid_validity),
+ &hdr_shared->uid_validity,
+ sizeof(hdr_shared->uid_validity), TRUE);
+ seq_shared = 1;
+ }
+
+ uid = 0;
+ for (; seq_shared <= count_shared; seq_shared++) {
+ mail_index_lookup_uid(ctx->view_shared, seq_shared, &uid);
+ mail_index_append(ctx->trans_pvt, uid, &seq_pvt);
+ if (preserve_old_flags &&
+ mail_index_lookup_seq(ctx->view_pvt, uid, &seq_old_pvt)) {
+ /* copy flags from the original private index */
+ sync_pvt_copy_self_flags(ctx, &keywords,
+ seq_old_pvt, seq_pvt);
+ } else if (copy_shared_flags) {
+ sync_pvt_copy_shared_flags(ctx, seq_shared, seq_pvt);
+ }
+
+ /* add private flags for the recently saved/copied messages */
+ while (pc_idx < pvt_changes_count &&
+ pvt_changes[pc_idx].mailnum <= uid) {
+ if (pvt_changes[pc_idx].mailnum == uid) {
+ mail_index_update_flags(ctx->trans_pvt, seq_pvt,
+ MODIFY_ADD, pvt_changes[pc_idx].flags);
+ }
+ pc_idx++;
+ }
+ }
+
+ if (uid < hdr_shared->next_uid) {
+ mail_index_update_header(ctx->trans_pvt,
+ offsetof(struct mail_index_header, next_uid),
+ &hdr_shared->next_uid,
+ sizeof(hdr_shared->next_uid), FALSE);
+ }
+
+ if ((ret = mail_index_sync_commit(&ctx->sync_ctx)) < 0)
+ mailbox_set_index_error(ctx->box);
+ return ret;
+}
+
+static int
+mail_save_private_changes_mailnum_cmp(const struct mail_save_private_changes *c1,
+ const struct mail_save_private_changes *c2)
+{
+ if (c1->mailnum < c2->mailnum)
+ return -1;
+ if (c1->mailnum > c2->mailnum)
+ return 1;
+ return 0;
+}
+
+int index_mailbox_sync_pvt_newmails(struct index_mailbox_sync_pvt_context *ctx,
+ struct mailbox_transaction_context *trans)
+{
+ struct mail_save_private_changes *pvt_changes;
+ struct seq_range_iter iter;
+ unsigned int i, n, pvt_count;
+ uint32_t uid;
+
+ if (index_mailbox_sync_view_refresh(ctx) < 0)
+ return -1;
+
+ /* translate mail numbers to UIDs */
+ pvt_changes = array_get_modifiable(&trans->pvt_saves, &pvt_count);
+
+ n = i = 0;
+ seq_range_array_iter_init(&iter, &trans->changes->saved_uids);
+ while (seq_range_array_iter_nth(&iter, n, &uid)) {
+ if (pvt_changes[i].mailnum == n) {
+ pvt_changes[i].mailnum = uid;
+ i++;
+ }
+ n++;
+ }
+ /* sort the changes by UID */
+ array_sort(&trans->pvt_saves, mail_save_private_changes_mailnum_cmp);
+
+ /* add new mails to the private index with the private flags */
+ return index_mailbox_sync_pvt_index(ctx, pvt_changes, pvt_count);
+}
+
+int index_mailbox_sync_pvt_view(struct index_mailbox_sync_pvt_context *ctx,
+ ARRAY_TYPE(seq_range) *flag_updates,
+ ARRAY_TYPE(seq_range) *hidden_updates)
+{
+ struct mail_index_view_sync_ctx *view_sync_ctx;
+ struct mail_index_view_sync_rec sync_rec;
+ uint32_t seq1, seq2;
+ bool delayed_expunges;
+
+ /* sync private index against shared index by adding/removing mails */
+ if (index_mailbox_sync_pvt_index(ctx, NULL, 0) < 0)
+ return -1;
+
+ /* Indicate to view syncing that this is a secondary index view */
+ ctx->flags |= MAIL_INDEX_VIEW_SYNC_FLAG_2ND_INDEX;
+
+ /* sync the private view */
+ view_sync_ctx = mail_index_view_sync_begin(ctx->box->view_pvt, ctx->flags);
+ while (mail_index_view_sync_next(view_sync_ctx, &sync_rec)) {
+ if (sync_rec.type != MAIL_INDEX_VIEW_SYNC_TYPE_FLAGS)
+ continue;
+
+ /* *_updates contains ctx->box->view sequences (not view_pvt
+ sequences) */
+ if (mail_index_lookup_seq_range(ctx->box->view,
+ sync_rec.uid1, sync_rec.uid2,
+ &seq1, &seq2)) {
+ if (!sync_rec.hidden) {
+ seq_range_array_add_range(flag_updates,
+ seq1, seq2);
+ } else {
+ seq_range_array_add_range(hidden_updates,
+ seq1, seq2);
+ }
+ }
+ }
+ if (mail_index_view_sync_commit(&view_sync_ctx, &delayed_expunges) < 0)
+ return -1;
+ return 0;
+}
+
+void index_mailbox_sync_pvt_deinit(struct index_mailbox_sync_pvt_context **_ctx)
+{
+ struct index_mailbox_sync_pvt_context *ctx = *_ctx;
+
+ *_ctx = NULL;
+
+ if (ctx->sync_ctx != NULL)
+ mail_index_sync_rollback(&ctx->sync_ctx);
+ if (ctx->view_shared != NULL)
+ mail_index_view_close(&ctx->view_shared);
+ i_free(ctx);
+}
diff --git a/src/lib-storage/index/index-sync-search.c b/src/lib-storage/index/index-sync-search.c
new file mode 100644
index 0000000..95cb89d
--- /dev/null
+++ b/src/lib-storage/index/index-sync-search.c
@@ -0,0 +1,97 @@
+/* Copyright (c) 2002-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "seq-range-array.h"
+#include "mail-search.h"
+#include "mailbox-search-result-private.h"
+#include "index-search-result.h"
+#include "index-sync-private.h"
+
+static bool
+search_result_want_flag_updates(const struct mail_search_result *result)
+{
+ if (!result->args_have_flags && !result->args_have_keywords &&
+ !result->args_have_modseq) {
+ /* search result doesn't care about flag changes */
+ return FALSE;
+ }
+ return TRUE;
+}
+
+static void index_sync_uidify_array(struct index_mailbox_sync_context *ctx,
+ const ARRAY_TYPE(seq_range) *changes)
+{
+ const struct seq_range *range;
+ uint32_t seq, uid;
+
+ array_foreach(changes, range) {
+ for (seq = range->seq1; seq <= range->seq2; seq++) {
+ mail_index_lookup_uid(ctx->ctx.box->view, seq, &uid);
+ seq_range_array_add(&ctx->all_flag_update_uids, uid);
+ }
+ }
+}
+
+static void index_sync_uidify(struct index_mailbox_sync_context *ctx)
+{
+ unsigned int count;
+
+ count = array_count(&ctx->flag_updates) +
+ array_count(&ctx->hidden_updates);
+ i_array_init(&ctx->all_flag_update_uids, count*2);
+
+ index_sync_uidify_array(ctx, &ctx->flag_updates);
+ index_sync_uidify_array(ctx, &ctx->hidden_updates);
+}
+
+void index_sync_search_results_uidify(struct index_mailbox_sync_context *ctx)
+{
+ struct mail_search_result *const *results;
+ unsigned int i, count;
+
+ i_assert(!array_is_created(&ctx->all_flag_update_uids));
+
+ results = array_get(&ctx->ctx.box->search_results, &count);
+ for (i = 0; i < count; i++) {
+ if ((results[i]->flags & MAILBOX_SEARCH_RESULT_FLAG_UPDATE) != 0 &&
+ search_result_want_flag_updates(results[i])) {
+ index_sync_uidify(ctx);
+ break;
+ }
+ }
+}
+
+static void
+search_result_update(struct index_mailbox_sync_context *ctx,
+ struct mail_search_result *result)
+{
+ if ((result->flags & MAILBOX_SEARCH_RESULT_FLAG_UPDATE) == 0) {
+ /* not an updateable search result */
+ return;
+ }
+
+ if (search_result_want_flag_updates(result)) {
+ (void)index_search_result_update_flags(result,
+ &ctx->all_flag_update_uids);
+ }
+ (void)index_search_result_update_appends(result, ctx->messages_count);
+}
+
+void index_sync_search_results_update(struct index_mailbox_sync_context *ctx)
+{
+ struct mail_search_result *const *results;
+ unsigned int i, count;
+
+ results = array_get(&ctx->ctx.box->search_results, &count);
+ for (i = 0; i < count; i++)
+ search_result_update(ctx, results[i]);
+}
+
+void index_sync_search_results_expunge(struct index_mailbox_sync_context *ctx)
+{
+ if (ctx->expunges != NULL) {
+ index_search_results_update_expunges(ctx->ctx.box,
+ ctx->expunges);
+ }
+}
diff --git a/src/lib-storage/index/index-sync.c b/src/lib-storage/index/index-sync.c
new file mode 100644
index 0000000..73ea99c
--- /dev/null
+++ b/src/lib-storage/index/index-sync.c
@@ -0,0 +1,560 @@
+/* Copyright (c) 2002-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "seq-range-array.h"
+#include "ioloop.h"
+#include "array.h"
+#include "index-mailbox-size.h"
+#include "index-sync-private.h"
+#include "mailbox-recent-flags.h"
+
+struct index_storage_list_index_record {
+ uint32_t size;
+ uint32_t mtime;
+};
+
+enum mail_index_sync_flags index_storage_get_sync_flags(struct mailbox *box)
+{
+ enum mail_index_sync_flags sync_flags = 0;
+
+ if ((box->flags & MAILBOX_FLAG_DROP_RECENT) != 0)
+ sync_flags |= MAIL_INDEX_SYNC_FLAG_DROP_RECENT;
+ if (box->deleting) {
+ sync_flags |= box->delete_sync_check ?
+ MAIL_INDEX_SYNC_FLAG_TRY_DELETING_INDEX :
+ MAIL_INDEX_SYNC_FLAG_DELETING_INDEX;
+ }
+ return sync_flags;
+}
+
+bool index_mailbox_want_full_sync(struct mailbox *box,
+ enum mailbox_sync_flags flags)
+{
+ struct index_mailbox_context *ibox = INDEX_STORAGE_CONTEXT(box);
+
+ if ((flags & MAILBOX_SYNC_FLAG_FAST) != 0 &&
+ ioloop_time < ibox->sync_last_check + MAILBOX_FULL_SYNC_INTERVAL)
+ return FALSE;
+
+ if ((flags & MAILBOX_SYNC_FLAG_FAST) != 0 &&
+ (box->flags & MAILBOX_FLAG_SAVEONLY) != 0) {
+ /* lib-lda is syncing the mailbox after saving a mail.
+ it only wants to find the new mail for potentially copying
+ to other mailboxes. that's mainly an optimization, and since
+ the mail was most likely already added to index we don't
+ need to do a full sync to find it. the main benefit here is
+ to avoid a very costly sync with a large Maildir/new/ */
+ return FALSE;
+ }
+
+ if (box->to_notify != NULL)
+ timeout_reset(box->to_notify);
+ ibox->sync_last_check = ioloop_time;
+ return TRUE;
+}
+
+static void index_view_sync_recs_get(struct index_mailbox_sync_context *ctx)
+{
+ struct mail_index_view_sync_rec sync_rec;
+ uint32_t seq1, seq2;
+
+ i_array_init(&ctx->flag_updates, 128);
+ i_array_init(&ctx->hidden_updates, 32);
+ while (mail_index_view_sync_next(ctx->sync_ctx, &sync_rec)) {
+ switch (sync_rec.type) {
+ case MAIL_INDEX_VIEW_SYNC_TYPE_MODSEQ:
+ case MAIL_INDEX_VIEW_SYNC_TYPE_FLAGS:
+ if (!mail_index_lookup_seq_range(ctx->ctx.box->view,
+ sync_rec.uid1,
+ sync_rec.uid2,
+ &seq1, &seq2))
+ break;
+
+ if (!sync_rec.hidden &&
+ sync_rec.type == MAIL_INDEX_VIEW_SYNC_TYPE_FLAGS) {
+ seq_range_array_add_range(&ctx->flag_updates,
+ seq1, seq2);
+ } else {
+ seq_range_array_add_range(&ctx->hidden_updates,
+ seq1, seq2);
+ }
+ break;
+ }
+ }
+}
+
+static void
+index_view_sync_cleanup_updates(struct index_mailbox_sync_context *ctx)
+{
+ /* remove expunged messages from flag updates */
+ if (ctx->expunges != NULL) {
+ seq_range_array_remove_seq_range(&ctx->flag_updates,
+ ctx->expunges);
+ seq_range_array_remove_seq_range(&ctx->hidden_updates,
+ ctx->expunges);
+ }
+ /* remove flag updates from hidden updates */
+ seq_range_array_remove_seq_range(&ctx->hidden_updates,
+ &ctx->flag_updates);
+}
+
+struct mailbox_sync_context *
+index_mailbox_sync_init(struct mailbox *box, enum mailbox_sync_flags flags,
+ bool failed)
+{
+ struct index_mailbox_sync_context *ctx;
+ struct index_mailbox_sync_pvt_context *pvt_ctx;
+ enum mail_index_view_sync_flags sync_flags = 0;
+
+ ctx = i_new(struct index_mailbox_sync_context, 1);
+ ctx->ctx.box = box;
+ ctx->ctx.flags = flags;
+
+ if (failed) {
+ ctx->failed = TRUE;
+ return &ctx->ctx;
+ }
+
+ if ((flags & MAILBOX_SYNC_FLAG_NO_EXPUNGES) != 0)
+ sync_flags |= MAIL_INDEX_VIEW_SYNC_FLAG_NOEXPUNGES;
+
+ if ((flags & MAILBOX_SYNC_FLAG_FIX_INCONSISTENT) != 0) {
+ sync_flags |= MAIL_INDEX_VIEW_SYNC_FLAG_FIX_INCONSISTENT;
+ ctx->messages_count = 0;
+ } else {
+ ctx->messages_count =
+ mail_index_view_get_messages_count(box->view);
+ }
+
+ if ((flags & MAILBOX_SYNC_FLAG_FAST) != 0) {
+ /* we most likely did a fast sync. refresh the index anyway in
+ case there were some new changes. */
+ (void)mail_index_refresh(box->index);
+ }
+ ctx->sync_ctx = mail_index_view_sync_begin(box->view, sync_flags);
+ if ((flags & MAILBOX_SYNC_FLAG_NO_EXPUNGES) == 0) {
+ mail_index_view_sync_get_expunges(ctx->sync_ctx,
+ &ctx->expunges);
+ ctx->expunge_pos = array_count(ctx->expunges);
+ }
+ index_view_sync_recs_get(ctx);
+ index_sync_search_results_expunge(ctx);
+
+ /* sync private index if needed. it doesn't use box->view, so it
+ doesn't matter if it's called at _sync_init() or _sync_deinit().
+ however we also need to know if any private flags have changed
+ since last sync, so we need to call it before _sync_next() calls. */
+ if (index_mailbox_sync_pvt_init(box, FALSE, sync_flags, &pvt_ctx) > 0) {
+ (void)index_mailbox_sync_pvt_view(pvt_ctx, &ctx->flag_updates,
+ &ctx->hidden_updates);
+ index_mailbox_sync_pvt_deinit(&pvt_ctx);
+
+ }
+ index_view_sync_cleanup_updates(ctx);
+ return &ctx->ctx;
+}
+
+static bool
+index_mailbox_sync_next_expunge(struct index_mailbox_sync_context *ctx,
+ struct mailbox_sync_rec *sync_rec_r)
+{
+ const struct seq_range *range;
+
+ if (ctx->expunge_pos == 0)
+ return FALSE;
+
+ /* expunges is a sorted array of sequences. it's easiest for
+ us to print them from end to beginning. */
+ ctx->expunge_pos--;
+ range = array_idx(ctx->expunges, ctx->expunge_pos);
+ i_assert(range->seq2 <= ctx->messages_count);
+
+ mailbox_recent_flags_expunge_seqs(ctx->ctx.box, range->seq1, range->seq2);
+ ctx->messages_count -= range->seq2 - range->seq1 + 1;
+
+ sync_rec_r->seq1 = range->seq1;
+ sync_rec_r->seq2 = range->seq2;
+ sync_rec_r->type = MAILBOX_SYNC_TYPE_EXPUNGE;
+ return TRUE;
+}
+
+bool index_mailbox_sync_next(struct mailbox_sync_context *_ctx,
+ struct mailbox_sync_rec *sync_rec_r)
+{
+ struct index_mailbox_sync_context *ctx =
+ (struct index_mailbox_sync_context *)_ctx;
+ const struct seq_range *range;
+ unsigned int count;
+
+ if (ctx->failed)
+ return FALSE;
+
+ range = array_get(&ctx->flag_updates, &count);
+ if (ctx->flag_update_idx < count) {
+ sync_rec_r->type = MAILBOX_SYNC_TYPE_FLAGS;
+ sync_rec_r->seq1 = range[ctx->flag_update_idx].seq1;
+ sync_rec_r->seq2 = range[ctx->flag_update_idx].seq2;
+ ctx->flag_update_idx++;
+ return TRUE;
+ }
+ if ((_ctx->box->enabled_features & MAILBOX_FEATURE_CONDSTORE) != 0) {
+ /* hidden flag changes' MODSEQs still need to be returned */
+ range = array_get(&ctx->hidden_updates, &count);
+ if (ctx->hidden_update_idx < count) {
+ sync_rec_r->type = MAILBOX_SYNC_TYPE_MODSEQ;
+ sync_rec_r->seq1 = range[ctx->hidden_update_idx].seq1;
+ sync_rec_r->seq2 = range[ctx->hidden_update_idx].seq2;
+ ctx->hidden_update_idx++;
+ return TRUE;
+ }
+ }
+
+ return index_mailbox_sync_next_expunge(ctx, sync_rec_r);
+}
+
+static void
+index_mailbox_expunge_unseen_recent(struct index_mailbox_sync_context *ctx)
+{
+ struct mailbox *box = ctx->ctx.box;
+ struct mail_index_view *view = ctx->ctx.box->view;
+ const struct mail_index_header *hdr;
+ uint32_t seq, start_uid, uid;
+
+ if (!array_is_created(&box->recent_flags))
+ return;
+
+ /* expunges array contained expunges for the messages that were already
+ visible in this view, but append+expunge would be invisible.
+ recent_flags may however contain the append UID, so we'll have to
+ remove it separately */
+ hdr = mail_index_get_header(view);
+ if (ctx->messages_count == 0)
+ uid = 0;
+ else if (ctx->messages_count <= hdr->messages_count)
+ mail_index_lookup_uid(view, ctx->messages_count, &uid);
+ else {
+ i_assert(mail_index_view_is_inconsistent(view));
+ return;
+ }
+
+ for (seq = ctx->messages_count + 1; seq <= hdr->messages_count; seq++) {
+ start_uid = uid;
+ mail_index_lookup_uid(view, seq, &uid);
+ if (start_uid + 1 > uid - 1)
+ continue;
+
+ box->recent_flags_count -=
+ seq_range_array_remove_range(&box->recent_flags,
+ start_uid + 1, uid - 1);
+ }
+
+ if (uid + 1 < hdr->next_uid) {
+ box->recent_flags_count -=
+ seq_range_array_remove_range(&box->recent_flags,
+ uid + 1,
+ hdr->next_uid - 1);
+ }
+#ifdef DEBUG
+ if (!mail_index_view_is_inconsistent(view)) {
+ const struct seq_range *range;
+ unsigned int i, count;
+
+ range = array_get(&box->recent_flags, &count);
+ for (i = 0; i < count; i++) {
+ for (uid = range[i].seq1; uid <= range[i].seq2; uid++) {
+ if (uid >= hdr->next_uid)
+ break;
+ (void)mail_index_lookup_seq(view, uid, &seq);
+ i_assert(seq != 0);
+ }
+ }
+ }
+#endif
+}
+
+void index_sync_update_recent_count(struct mailbox *box)
+{
+ struct index_mailbox_context *ibox = INDEX_STORAGE_CONTEXT(box);
+ const struct mail_index_header *hdr;
+ uint32_t seq1, seq2;
+
+ hdr = mail_index_get_header(box->view);
+ if (hdr->first_recent_uid < ibox->recent_flags_prev_first_recent_uid) {
+ mailbox_set_critical(box,
+ "first_recent_uid unexpectedly shrank: %u -> %u",
+ ibox->recent_flags_prev_first_recent_uid,
+ hdr->first_recent_uid);
+ mailbox_recent_flags_reset(box);
+ }
+
+ if (hdr->first_recent_uid > box->recent_flags_prev_uid ||
+ hdr->next_uid > ibox->recent_flags_last_check_nextuid) {
+ ibox->recent_flags_prev_first_recent_uid = hdr->first_recent_uid;
+ ibox->recent_flags_last_check_nextuid = hdr->next_uid;
+ if (mail_index_lookup_seq_range(box->view,
+ hdr->first_recent_uid,
+ hdr->next_uid,
+ &seq1, &seq2)) {
+ mailbox_recent_flags_set_seqs(box, box->view,
+ seq1, seq2);
+ }
+ }
+}
+
+static void index_mailbox_sync_free(struct index_mailbox_sync_context *ctx)
+{
+ if (array_is_created(&ctx->flag_updates))
+ array_free(&ctx->flag_updates);
+ if (array_is_created(&ctx->hidden_updates))
+ array_free(&ctx->hidden_updates);
+ if (array_is_created(&ctx->all_flag_update_uids))
+ array_free(&ctx->all_flag_update_uids);
+ i_free(ctx);
+}
+
+int index_mailbox_sync_deinit(struct mailbox_sync_context *_ctx,
+ struct mailbox_sync_status *status_r)
+{
+ struct index_mailbox_sync_context *ctx =
+ (struct index_mailbox_sync_context *)_ctx;
+ struct mailbox_sync_rec sync_rec;
+ bool delayed_expunges = FALSE;
+ int ret = ctx->failed ? -1 : 0;
+
+ /* finish handling expunges, so we don't break when updating
+ recent flags */
+ while (index_mailbox_sync_next_expunge(ctx, &sync_rec)) ;
+
+ /* convert sequences to uids before syncing view */
+ index_sync_search_results_uidify(ctx);
+
+ if (ctx->sync_ctx != NULL) {
+ if (mail_index_view_sync_commit(&ctx->sync_ctx,
+ &delayed_expunges) < 0) {
+ mailbox_set_index_error(_ctx->box);
+ ret = -1;
+ }
+ }
+ if (ret < 0) {
+ index_mailbox_sync_free(ctx);
+ return -1;
+ }
+ index_mailbox_expunge_unseen_recent(ctx);
+
+ if ((_ctx->box->flags & MAILBOX_FLAG_DROP_RECENT) == 0 &&
+ _ctx->box->opened) {
+ /* mailbox syncing didn't necessarily update our recent state */
+ index_sync_update_recent_count(_ctx->box);
+ }
+
+ if (status_r != NULL)
+ status_r->sync_delayed_expunges = delayed_expunges;
+
+ /* update search results after private index is updated */
+ index_sync_search_results_update(ctx);
+ /* update vsize header if wanted */
+ index_mailbox_vsize_update_appends(_ctx->box);
+
+ if (ret == 0 && mail_index_view_is_inconsistent(_ctx->box->view)) {
+ /* we probably had MAILBOX_SYNC_FLAG_FIX_INCONSISTENT set,
+ but the view got broken in the middle. FIXME: We could
+ attempt to fix it automatically. In any case now the view
+ isn't usable and we can't return success. */
+ mailbox_set_index_error(_ctx->box);
+ ret = -1;
+ }
+
+ index_mailbox_sync_free(ctx);
+ return ret;
+}
+
+bool index_keyword_array_cmp(const ARRAY_TYPE(keyword_indexes) *k1,
+ const ARRAY_TYPE(keyword_indexes) *k2)
+{
+ const unsigned int *idx1, *idx2;
+ unsigned int i, j, count1, count2;
+
+ if (!array_is_created(k1))
+ return !array_is_created(k2) || array_count(k2) == 0;
+ if (!array_is_created(k2))
+ return array_count(k1) == 0;
+
+ /* The arrays may not be sorted, but they usually are. Optimize for
+ the assumption that they are */
+ idx1 = array_get(k1, &count1);
+ idx2 = array_get(k2, &count2);
+
+ if (count1 != count2)
+ return FALSE;
+
+ for (i = 0; i < count1; i++) {
+ if (idx1[i] != idx2[i]) {
+ /* not found / unsorted array. check. */
+ for (j = 0; j < count1; j++) {
+ if (idx1[i] == idx2[j])
+ break;
+ }
+ if (j == count1)
+ return FALSE;
+ }
+ }
+ return TRUE;
+}
+
+enum mailbox_sync_type index_sync_type_convert(enum mail_index_sync_type type)
+{
+ enum mailbox_sync_type ret = 0;
+
+ if ((type & MAIL_INDEX_SYNC_TYPE_EXPUNGE) != 0)
+ ret |= MAILBOX_SYNC_TYPE_EXPUNGE;
+ if ((type & (MAIL_INDEX_SYNC_TYPE_FLAGS |
+ MAIL_INDEX_SYNC_TYPE_KEYWORD_ADD |
+ MAIL_INDEX_SYNC_TYPE_KEYWORD_REMOVE)) != 0)
+ ret |= MAILBOX_SYNC_TYPE_FLAGS;
+ return ret;
+}
+
+static uint32_t
+index_list_get_ext_id(struct mailbox *box, struct mail_index_view *view)
+{
+ struct index_mailbox_context *ibox = INDEX_STORAGE_CONTEXT(box);
+
+ if (ibox->list_index_sync_ext_id == (uint32_t)-1) {
+ ibox->list_index_sync_ext_id =
+ mail_index_ext_register(mail_index_view_get_index(view),
+ "index sync", 0,
+ sizeof(struct index_storage_list_index_record),
+ sizeof(uint32_t));
+ }
+ return ibox->list_index_sync_ext_id;
+}
+
+enum index_storage_list_change
+index_storage_list_index_has_changed_full(struct mailbox *box,
+ struct mail_index_view *list_view,
+ uint32_t seq, const char **reason_r)
+{
+ const struct index_storage_list_index_record *rec;
+ const void *data;
+ const char *dir, *path;
+ struct stat st;
+ uint32_t ext_id;
+ bool expunged;
+ int ret;
+
+ *reason_r = NULL;
+
+ if (mail_index_is_in_memory(mail_index_view_get_index(list_view))) {
+ *reason_r = "List index is in memory";
+ return INDEX_STORAGE_LIST_CHANGE_INMEMORY;
+ }
+
+ ext_id = index_list_get_ext_id(box, list_view);
+ mail_index_lookup_ext(list_view, seq, ext_id, &data, &expunged);
+ rec = data;
+
+ if (rec == NULL) {
+ *reason_r = "Storage record is missing";
+ return INDEX_STORAGE_LIST_CHANGE_NORECORD;
+ } else if (expunged) {
+ *reason_r = "Storage record is expunged";
+ return INDEX_STORAGE_LIST_CHANGE_NORECORD;
+ } else if (rec->size == 0) {
+ *reason_r = "Storage record size=0";
+ return INDEX_STORAGE_LIST_CHANGE_NORECORD;
+ } else if (rec->mtime == 0) {
+ *reason_r = "Storage record mtime=0";
+ return INDEX_STORAGE_LIST_CHANGE_NORECORD;
+ }
+ if (box->storage->set->mailbox_list_index_very_dirty_syncs)
+ return INDEX_STORAGE_LIST_CHANGE_NONE;
+
+ ret = mailbox_get_path_to(box, MAILBOX_LIST_PATH_TYPE_INDEX, &dir);
+ if (ret < 0)
+ return INDEX_STORAGE_LIST_CHANGE_ERROR;
+ i_assert(ret > 0);
+
+ path = t_strconcat(dir, "/", box->index_prefix, ".log", NULL);
+ if (stat(path, &st) < 0) {
+ if (errno == ENOENT) {
+ *reason_r = t_strdup_printf("%s not found", path);
+ return INDEX_STORAGE_LIST_CHANGE_NOT_IN_FS;
+ }
+ mailbox_set_critical(box, "stat(%s) failed: %m", path);
+ return INDEX_STORAGE_LIST_CHANGE_ERROR;
+ }
+ uint32_t new_size = st.st_size & 0xffffffffU;
+ if (rec->size != new_size) {
+ *reason_r = t_strdup_printf("Storage size changed %u != %u",
+ rec->size, new_size);
+ return INDEX_STORAGE_LIST_CHANGE_SIZE_CHANGED;
+ }
+ uint32_t new_mtime = st.st_mtime & 0xffffffffU;
+ if (rec->mtime != new_mtime) {
+ *reason_r = t_strdup_printf("Storage mtime changed %u != %u",
+ rec->mtime, new_mtime);
+ return INDEX_STORAGE_LIST_CHANGE_MTIME_CHANGED;
+ }
+ return INDEX_STORAGE_LIST_CHANGE_NONE;
+}
+
+int index_storage_list_index_has_changed(struct mailbox *box,
+ struct mail_index_view *list_view,
+ uint32_t seq, bool quick ATTR_UNUSED,
+ const char **reason_r)
+{
+ switch (index_storage_list_index_has_changed_full(box, list_view, seq,
+ reason_r)) {
+ case INDEX_STORAGE_LIST_CHANGE_ERROR:
+ return -1;
+ case INDEX_STORAGE_LIST_CHANGE_NONE:
+ return 0;
+ default:
+ return 1;
+ }
+}
+
+void index_storage_list_index_update_sync(struct mailbox *box,
+ struct mail_index_transaction *trans,
+ uint32_t seq)
+{
+ struct mail_index_view *list_view;
+ const struct index_storage_list_index_record *old_rec;
+ struct index_storage_list_index_record new_rec;
+ const void *data;
+ const char *dir, *path;
+ struct stat st;
+ uint32_t ext_id;
+ bool expunged;
+ int ret;
+
+ list_view = mail_index_transaction_get_view(trans);
+ if (mail_index_is_in_memory(mail_index_view_get_index(list_view)))
+ return;
+
+ /* get the current record */
+ ext_id = index_list_get_ext_id(box, list_view);
+ mail_index_lookup_ext(list_view, seq, ext_id, &data, &expunged);
+ if (expunged)
+ return;
+ old_rec = data;
+
+ ret = mailbox_get_path_to(box, MAILBOX_LIST_PATH_TYPE_INDEX, &dir);
+ if (ret < 0)
+ return;
+ i_assert(ret > 0);
+
+ path = t_strconcat(dir, "/", box->index_prefix, ".log", NULL);
+ if (stat(path, &st) < 0) {
+ mailbox_set_critical(box, "stat(%s) failed: %m", path);
+ return;
+ }
+
+ i_zero(&new_rec);
+ new_rec.size = st.st_size & 0xffffffffU;
+ new_rec.mtime = st.st_mtime & 0xffffffffU;
+
+ if (old_rec == NULL ||
+ memcmp(old_rec, &new_rec, sizeof(*old_rec)) != 0)
+ mail_index_update_ext(trans, seq, ext_id, &new_rec, NULL);
+}
diff --git a/src/lib-storage/index/index-thread-finish.c b/src/lib-storage/index/index-thread-finish.c
new file mode 100644
index 0000000..42326b6
--- /dev/null
+++ b/src/lib-storage/index/index-thread-finish.c
@@ -0,0 +1,682 @@
+/* Copyright (c) 2002-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "hash.h"
+#include "imap-base-subject.h"
+#include "mail-storage-private.h"
+#include "index-thread-private.h"
+
+
+struct mail_thread_shadow_node {
+ uint32_t first_child_idx, next_sibling_idx;
+};
+
+struct mail_thread_root_node {
+ /* node.idx usually points to indexes from mail hash. However
+ REFERENCES step (5) may add temporary dummy roots. They use larger
+ index numbers than exist in the hash. */
+ struct mail_thread_child_node node;
+
+ /* Used temporarily by (5)(B) base subject gathering.
+ root_idx1 is node's index in roots[] array + 1.
+ parent_root_idx points to root_idx1, or 0 for root. */
+ unsigned int root_idx1;
+ uint32_t parent_root_idx1;
+
+ /* subject contained a Re: or Fwd: */
+ bool reply_or_forward:1;
+ /* a dummy node */
+ bool dummy:1;
+ /* ignore this node - it's a dummy without children */
+ bool ignore:1;
+};
+
+struct thread_finish_context {
+ unsigned int refcount;
+
+ struct mail *tmp_mail;
+ struct mail_thread_cache *cache;
+
+ ARRAY(struct mail_thread_root_node) roots;
+ ARRAY(struct mail_thread_shadow_node) shadow_nodes;
+ unsigned int next_new_root_idx;
+
+ bool use_sent_date:1;
+ bool return_seqs:1;
+};
+
+struct mail_thread_iterate_context {
+ struct thread_finish_context *ctx;
+
+ ARRAY_TYPE(mail_thread_child_node) children;
+ unsigned int next_idx;
+ bool failed;
+};
+
+struct subject_gather_context {
+ struct thread_finish_context *ctx;
+
+ pool_t subject_pool;
+ HASH_TABLE(char *, struct mail_thread_root_node *) subject_hash;
+};
+
+static void
+add_base_subject(struct subject_gather_context *ctx, const char *subject,
+ struct mail_thread_root_node *node)
+{
+ struct mail_thread_root_node *hash_node;
+ char *hash_subject;
+ bool is_reply_or_forward;
+
+ subject = imap_get_base_subject_cased(pool_datastack_create(), subject,
+ &is_reply_or_forward);
+ /* (ii) If the thread subject is empty, skip this message. */
+ if (*subject == '\0')
+ return;
+
+ /* (iii) Look up the message associated with the thread
+ subject in the subject table. */
+ if (!hash_table_lookup_full(ctx->subject_hash, subject, &hash_subject,
+ &hash_node)) {
+ /* (iv) If there is no message in the subject table with the
+ thread subject, add the current message and the thread
+ subject to the subject table. */
+ hash_subject = p_strdup(ctx->subject_pool, subject);
+ hash_table_insert(ctx->subject_hash, hash_subject, node);
+ } else {
+ /* Otherwise, if the message in the subject table is not a
+ dummy, AND either of the following criteria are true:
+
+ The current message is a dummy, OR
+
+ The message in the subject table is a reply or forward
+ and the current message is not.
+
+ then replace the message in the subject table with the
+ current message. */
+ if (!hash_node->dummy &&
+ (node->dummy ||
+ (hash_node->reply_or_forward && !is_reply_or_forward))) {
+ hash_node->parent_root_idx1 = node->root_idx1;
+ hash_table_update(ctx->subject_hash, hash_subject, node);
+ } else {
+ node->parent_root_idx1 = hash_node->root_idx1;
+ }
+ }
+
+ node->reply_or_forward = is_reply_or_forward;
+}
+
+static int mail_thread_child_node_cmp(const struct mail_thread_child_node *c1,
+ const struct mail_thread_child_node *c2)
+{
+ if (c1->sort_date < c2->sort_date)
+ return -1;
+ if (c1->sort_date > c2->sort_date)
+ return 1;
+
+ if (c1->uid < c2->uid)
+ return -1;
+ if (c1->uid > c2->uid)
+ return 1;
+ return 0;
+}
+
+static int mail_thread_root_node_cmp(const struct mail_thread_root_node *r1,
+ const struct mail_thread_root_node *r2)
+{
+ return mail_thread_child_node_cmp(&r1->node, &r2->node);
+}
+
+static uint32_t
+thread_lookup_existing(struct thread_finish_context *ctx, uint32_t idx)
+{
+ const struct mail_thread_node *node;
+
+ node = array_idx(&ctx->cache->thread_nodes, idx);
+ i_assert(MAIL_THREAD_NODE_EXISTS(node));
+ i_assert(node->uid != 0);
+ return node->uid;
+}
+
+static void
+thread_child_node_fill(struct thread_finish_context *ctx,
+ struct mail_thread_child_node *child)
+{
+ int tz;
+
+ child->uid = thread_lookup_existing(ctx, child->idx);
+
+ if (!mail_set_uid(ctx->tmp_mail, child->uid)) {
+ /* the UID should have existed. we would have rebuild
+ the thread tree otherwise. */
+ i_unreached();
+ }
+
+ /* get sent date if we want to use it and if it's valid */
+ if (!ctx->use_sent_date)
+ child->sort_date = 0;
+ else if (mail_get_date(ctx->tmp_mail, &child->sort_date, &tz) < 0)
+ child->sort_date = 0;
+
+ if (child->sort_date == 0) {
+ /* fallback to received date */
+ (void)mail_get_received_date(ctx->tmp_mail, &child->sort_date);
+ }
+}
+
+static void
+thread_sort_children(struct thread_finish_context *ctx, uint32_t parent_idx,
+ ARRAY_TYPE(mail_thread_child_node) *sorted_children)
+{
+ const struct mail_thread_shadow_node *shadows;
+ struct mail_thread_child_node child;
+ unsigned int count;
+
+ i_zero(&child);
+ array_clear(sorted_children);
+
+ /* add all child indexes to the array */
+ shadows = array_get(&ctx->shadow_nodes, &count);
+ child.idx = shadows[parent_idx].first_child_idx;
+ i_assert(child.idx != 0);
+ if (shadows[child.idx].next_sibling_idx == 0) {
+ /* only child - don't bother setting sort date */
+ child.uid = thread_lookup_existing(ctx, child.idx);
+
+ array_push_back(sorted_children, &child);
+ return;
+ }
+ while (child.idx != 0) {
+ thread_child_node_fill(ctx, &child);
+
+ array_push_back(sorted_children, &child);
+ child.idx = shadows[child.idx].next_sibling_idx;
+ }
+
+ /* sort the children */
+ array_sort(sorted_children, mail_thread_child_node_cmp);
+}
+
+static void gather_base_subjects(struct thread_finish_context *ctx)
+{
+ struct subject_gather_context gather_ctx;
+ struct mail_thread_root_node *roots;
+ const char *subject;
+ unsigned int i, count;
+ ARRAY_TYPE(mail_thread_child_node) sorted_children;
+ const struct mail_thread_child_node *children;
+ uint32_t idx, uid;
+
+ i_zero(&gather_ctx);
+ gather_ctx.ctx = ctx;
+
+ roots = array_get_modifiable(&ctx->roots, &count);
+ if (count == 0)
+ return;
+ gather_ctx.subject_pool =
+ pool_alloconly_create(MEMPOOL_GROWING"base subjects",
+ nearest_power(count * 20));
+ hash_table_create(&gather_ctx.subject_hash, gather_ctx.subject_pool,
+ count * 2, str_hash, strcmp);
+
+ i_array_init(&sorted_children, 64);
+ for (i = 0; i < count; i++) {
+ roots[i].root_idx1 = i + 1;
+ if (!roots[i].dummy)
+ idx = roots[i].node.idx;
+ else if (!roots[i].ignore) {
+ /* find the oldest child */
+ thread_sort_children(ctx, roots[i].node.idx,
+ &sorted_children);
+ children = array_front(&sorted_children);
+ idx = children[0].idx;
+ } else {
+ /* dummy without children */
+ continue;
+ }
+
+ uid = thread_lookup_existing(ctx, idx);
+ if (!mail_set_uid(ctx->tmp_mail, uid)) {
+ /* the UID should have existed. we would have rebuild
+ the thread tree otherwise. */
+ i_unreached();
+ }
+ if (mail_get_first_header(ctx->tmp_mail, HDR_SUBJECT,
+ &subject) > 0) T_BEGIN {
+ add_base_subject(&gather_ctx, subject, &roots[i]);
+ } T_END;
+ }
+ i_assert(roots[count-1].parent_root_idx1 <= count);
+ array_free(&sorted_children);
+ hash_table_destroy(&gather_ctx.subject_hash);
+ pool_unref(&gather_ctx.subject_pool);
+}
+
+static void thread_add_shadow_child(struct thread_finish_context *ctx,
+ uint32_t parent_idx, uint32_t child_idx)
+{
+ struct mail_thread_shadow_node *parent_shadow, *child_shadow;
+
+ parent_shadow = array_idx_get_space(&ctx->shadow_nodes, parent_idx);
+ child_shadow = array_idx_modifiable(&ctx->shadow_nodes, child_idx);
+
+ child_shadow->next_sibling_idx = parent_shadow->first_child_idx;
+ parent_shadow->first_child_idx = child_idx;
+}
+
+static void mail_thread_root_thread_merge(struct thread_finish_context *ctx,
+ struct mail_thread_root_node *cur)
+{
+ struct mail_thread_root_node *roots, *root, new_root;
+ struct mail_thread_shadow_node *shadows;
+ unsigned int count;
+ uint32_t idx, next_idx;
+
+ i_assert(cur->parent_root_idx1 != 0);
+
+ /* The highest parent is the same as the current message in the
+ subject table. */
+ roots = array_get_modifiable(&ctx->roots, &count);
+ root = cur;
+ do {
+ i_assert(root->parent_root_idx1 <= count);
+ root = &roots[root->parent_root_idx1 - 1];
+ } while (root->parent_root_idx1 != 0);
+ i_assert(!root->ignore);
+
+ shadows = array_front_modifiable(&ctx->shadow_nodes);
+ if (cur->dummy) {
+ /* If both messages are dummies, append the current
+ message's children to the children of the message in
+ the subject table (the children of both messages
+ become siblings), and then delete the current message. */
+ i_assert(root->dummy);
+
+ idx = shadows[cur->node.idx].first_child_idx;
+ while (idx != 0) {
+ next_idx = shadows[idx].next_sibling_idx;
+ thread_add_shadow_child(ctx, root->node.idx, idx);
+ idx = next_idx;
+ }
+
+ shadows[cur->node.idx].first_child_idx = 0;
+ cur->ignore = TRUE;
+ } else if (root->dummy || (cur->reply_or_forward &&
+ !root->reply_or_forward)) {
+ /* a) If the message in the subject table is a dummy and the
+ current message is not, make the current message a
+ child of the message in the subject table (a sibling
+ of its children).
+
+ b) If the current message is a reply or forward and the
+ message in the subject table is not, make the current
+ message a child of the message in the subject table (a
+ sibling of its children). */
+ thread_add_shadow_child(ctx, root->node.idx, cur->node.idx);
+ cur->ignore = TRUE;
+ } else {
+ /* Otherwise, create a new dummy message and make both
+ the current message and the message in the subject
+ table children of the dummy. Then replace the message
+ in the subject table with the dummy message. */
+ i_zero(&new_root);
+ new_root.root_idx1 = array_count(&ctx->roots) + 1;
+ new_root.node.idx = ctx->next_new_root_idx++;
+ new_root.dummy = TRUE;
+
+ thread_add_shadow_child(ctx, new_root.node.idx, root->node.idx);
+ thread_add_shadow_child(ctx, new_root.node.idx, cur->node.idx);
+
+ root->parent_root_idx1 = new_root.root_idx1;
+ root->ignore = TRUE;
+ cur->ignore = TRUE;
+
+ /* append last, since it breaks root and cur pointers */
+ array_push_back(&ctx->roots, &new_root);
+
+ /* make sure all shadow indexes are accessible directly */
+ (void)array_idx_modifiable(&ctx->shadow_nodes,
+ new_root.node.idx);
+ }
+}
+
+static bool merge_subject_threads(struct thread_finish_context *ctx)
+{
+ struct mail_thread_root_node *roots;
+ unsigned int i, count;
+ bool changed = FALSE;
+
+ roots = array_get_modifiable(&ctx->roots, &count);
+ for (i = 0; i < count; i++) {
+ if (roots[i].parent_root_idx1 != 0 && !roots[i].ignore) {
+ mail_thread_root_thread_merge(ctx, &roots[i]);
+ /* more roots may have been added */
+ roots = array_front_modifiable(&ctx->roots);
+ changed = TRUE;
+ }
+ }
+
+ return changed;
+}
+
+static void sort_root_nodes(struct thread_finish_context *ctx)
+{
+ ARRAY_TYPE(mail_thread_child_node) sorted_children;
+ const struct mail_thread_child_node *children;
+ const struct mail_thread_shadow_node *shadows;
+ struct mail_thread_root_node *roots;
+ unsigned int i, count, child_count;
+
+ i_array_init(&sorted_children, 64);
+ shadows = array_front(&ctx->shadow_nodes);
+ roots = array_get_modifiable(&ctx->roots, &count);
+ for (i = 0; i < count; i++) {
+ if (roots[i].ignore)
+ continue;
+ if (roots[i].dummy) {
+ /* sort by the first child */
+ if (shadows[roots[i].node.idx].first_child_idx == 0) {
+ /* childless dummy node */
+ roots[i].ignore = TRUE;
+ continue;
+ }
+ thread_sort_children(ctx, roots[i].node.idx,
+ &sorted_children);
+ children = array_get(&sorted_children, &child_count);
+ if (child_count == 1) {
+ /* only one child - deferred step (3).
+ promote the child to the root. */
+ roots[i].node = children[0];
+ thread_child_node_fill(ctx, &roots[i].node);
+ roots[i].dummy = FALSE;
+ } else {
+ roots[i].node.uid = children[0].uid;
+ roots[i].node.sort_date = children[0].sort_date;
+ }
+ } else {
+ thread_child_node_fill(ctx, &roots[i].node);
+ }
+ }
+ array_free(&sorted_children);
+ array_sort(&ctx->roots, mail_thread_root_node_cmp);
+}
+
+static int mail_thread_root_node_idx_cmp(const void *key, const void *value)
+{
+ const uint32_t *idx = key;
+ const struct mail_thread_root_node *root = value;
+
+ return *idx < root->node.idx ? -1 :
+ *idx > root->node.idx ? 1 : 0;
+}
+
+static void sort_root_nodes_ref2(struct thread_finish_context *ctx,
+ uint32_t record_count)
+{
+ const struct mail_thread_node *node;
+ struct mail_thread_root_node *roots, *root;
+ struct mail_thread_child_node child;
+ const struct mail_thread_shadow_node *shadows;
+ unsigned int root_count;
+ uint32_t idx, parent_idx;
+
+ roots = array_get_modifiable(&ctx->roots, &root_count);
+
+ /* drop childless dummy nodes */
+ shadows = array_front(&ctx->shadow_nodes);
+ for (idx = 1; idx < root_count; idx++) {
+ if (roots[idx].dummy &&
+ shadows[roots[idx].node.idx].first_child_idx == 0)
+ roots[idx].ignore = TRUE;
+ }
+
+ for (idx = 1; idx < record_count; idx++) {
+ node = array_idx(&ctx->cache->thread_nodes, idx);
+ if (!MAIL_THREAD_NODE_EXISTS(node))
+ continue;
+
+ child.idx = idx;
+ thread_child_node_fill(ctx, &child);
+
+ parent_idx = idx;
+ while (node->parent_idx != 0) {
+ parent_idx = node->parent_idx;
+ node = array_idx(&ctx->cache->thread_nodes,
+ node->parent_idx);
+ }
+ root = bsearch(&parent_idx, roots, root_count, sizeof(*roots),
+ mail_thread_root_node_idx_cmp);
+ i_assert(root != NULL);
+
+ if (root->node.sort_date < child.sort_date)
+ root->node.sort_date = child.sort_date;
+ }
+ array_sort(&ctx->roots, mail_thread_root_node_cmp);
+}
+
+static void mail_thread_create_shadows(struct thread_finish_context *ctx,
+ uint32_t record_count)
+{
+ const struct mail_thread_node *node, *parent;
+ struct mail_thread_root_node root;
+ struct mail_thread_child_node child;
+ uint32_t idx, parent_idx;
+
+ ctx->use_sent_date = FALSE;
+
+ i_zero(&root);
+ i_zero(&child);
+
+ /* We may see dummy messages without parents or children. We can't
+ free them since the nodes are in an array, but they may get reused
+ later so just leave them be. With the current algorithm when this
+ happens all the struct fields are always zero at that point, so
+ we don't even have to try to zero them. */
+ for (idx = 1; idx < record_count; idx++) {
+ node = array_idx(&ctx->cache->thread_nodes, idx);
+
+ if (node->parent_idx == 0) {
+ /* root node - add to roots list */
+ root.node.idx = idx;
+ if (!MAIL_THREAD_NODE_EXISTS(node)) {
+ root.dummy = TRUE;
+ root.node.uid = 0;
+ } else {
+ root.dummy = FALSE;
+ root.node.uid = node->uid;
+ }
+ array_push_back(&ctx->roots, &root);
+ continue;
+ }
+ i_assert(node->parent_idx < record_count);
+
+ if (!MAIL_THREAD_NODE_EXISTS(node)) {
+ /* dummy node */
+ continue;
+ }
+
+ /* Find the node's first non-dummy parent and add the
+ node as its child. If there are no non-dummy
+ parents, add it as the highest dummy's child. */
+ parent_idx = node->parent_idx;
+ parent = array_idx(&ctx->cache->thread_nodes, parent_idx);
+ while (!MAIL_THREAD_NODE_EXISTS(parent) &&
+ parent->parent_idx != 0) {
+ parent_idx = parent->parent_idx;
+ parent = array_idx(&ctx->cache->thread_nodes,
+ parent_idx);
+ }
+ thread_add_shadow_child(ctx, parent_idx, idx);
+ }
+}
+
+static void mail_thread_finish(struct thread_finish_context *ctx,
+ enum mail_thread_type thread_type)
+{
+ unsigned int record_count = array_count(&ctx->cache->thread_nodes);
+
+ ctx->next_new_root_idx = record_count + 1;
+
+ /* (2) save root nodes and (3) remove dummy messages */
+ i_array_init(&ctx->roots, I_MIN(128, record_count));
+ i_array_init(&ctx->shadow_nodes, record_count);
+ /* make sure all shadow indexes are accessible directly. */
+ (void)array_idx_get_space(&ctx->shadow_nodes, record_count);
+
+ mail_thread_create_shadows(ctx, record_count);
+
+ /* (4) */
+ ctx->use_sent_date = TRUE;
+ switch (thread_type) {
+ case MAIL_THREAD_REFERENCES:
+ sort_root_nodes(ctx);
+ /* (5) Gather together messages under the root that have
+ the same base subject text. */
+ gather_base_subjects(ctx);
+ /* (5.C) Merge threads with the same thread subject. */
+ if (merge_subject_threads(ctx)) {
+ /* root ordering may have changed, sort them again. */
+ sort_root_nodes(ctx);
+ }
+ break;
+ case MAIL_THREAD_REFS:
+ sort_root_nodes_ref2(ctx, record_count);
+ break;
+ default:
+ i_unreached();
+ }
+}
+
+static void
+nodes_change_uids_to_seqs(struct mail_thread_iterate_context *iter, bool root)
+{
+ struct mail_thread_child_node *children;
+ struct mailbox *box = iter->ctx->tmp_mail->box;
+ unsigned int i, count;
+ uint32_t uid, seq;
+
+ children = array_get_modifiable(&iter->children, &count);
+ for (i = 0; i < count; i++) {
+ uid = children[i].uid;
+ if (uid == 0) {
+ /* dummy root */
+ if (root)
+ continue;
+ i_unreached();
+ } else {
+ mailbox_get_seq_range(box, uid, uid, &seq, &seq);
+ i_assert(seq != 0);
+ }
+ children[i].uid = seq;
+ }
+}
+
+static void
+mail_thread_iterate_fill_root(struct mail_thread_iterate_context *iter)
+{
+ struct mail_thread_root_node *roots;
+ unsigned int i, count;
+
+ roots = array_get_modifiable(&iter->ctx->roots, &count);
+ i_array_init(&iter->children, count);
+ for (i = 0; i < count; i++) {
+ if (!roots[i].ignore) {
+ if (roots[i].dummy)
+ roots[i].node.uid = 0;
+ array_push_back(&iter->children, &roots[i].node);
+ }
+ }
+}
+
+static struct mail_thread_iterate_context *
+mail_thread_iterate_children(struct mail_thread_iterate_context *parent_iter,
+ uint32_t parent_idx)
+{
+ struct mail_thread_iterate_context *child_iter;
+
+ child_iter = i_new(struct mail_thread_iterate_context, 1);
+ child_iter->ctx = parent_iter->ctx;
+ child_iter->ctx->refcount++;
+
+ i_array_init(&child_iter->children, 8);
+ struct event_reason *reason = event_reason_begin("mailbox:thread");
+ thread_sort_children(child_iter->ctx, parent_idx,
+ &child_iter->children);
+ if (child_iter->ctx->return_seqs)
+ nodes_change_uids_to_seqs(child_iter, FALSE);
+ event_reason_end(&reason);
+ return child_iter;
+}
+
+struct mail_thread_iterate_context *
+mail_thread_iterate_init_full(struct mail_thread_cache *cache,
+ struct mail *tmp_mail,
+ enum mail_thread_type thread_type,
+ bool return_seqs)
+{
+ struct mail_thread_iterate_context *iter;
+ struct thread_finish_context *ctx;
+
+ iter = i_new(struct mail_thread_iterate_context, 1);
+ ctx = iter->ctx = i_new(struct thread_finish_context, 1);
+ ctx->refcount = 1;
+ ctx->cache = cache;
+ ctx->tmp_mail = tmp_mail;
+ ctx->return_seqs = return_seqs;
+
+ struct event_reason *reason = event_reason_begin("mailbox:thread");
+ mail_thread_finish(ctx, thread_type);
+
+ mail_thread_iterate_fill_root(iter);
+ if (return_seqs)
+ nodes_change_uids_to_seqs(iter, TRUE);
+ event_reason_end(&reason);
+ return iter;
+}
+
+const struct mail_thread_child_node *
+mail_thread_iterate_next(struct mail_thread_iterate_context *iter,
+ struct mail_thread_iterate_context **child_iter_r)
+{
+ const struct mail_thread_child_node *children, *child;
+ const struct mail_thread_shadow_node *shadow;
+ unsigned int count;
+
+ children = array_get(&iter->children, &count);
+ if (iter->next_idx >= count)
+ return NULL;
+
+ child = &children[iter->next_idx++];
+ shadow = array_idx(&iter->ctx->shadow_nodes, child->idx);
+ *child_iter_r = shadow->first_child_idx == 0 ? NULL :
+ mail_thread_iterate_children(iter, child->idx);
+ if (child->uid == 0 && *child_iter_r == NULL) {
+ /* this is a dummy node without children,
+ there's no point in returning it */
+ return mail_thread_iterate_next(iter, child_iter_r);
+ }
+ return child;
+}
+
+unsigned int mail_thread_iterate_count(struct mail_thread_iterate_context *iter)
+{
+ return array_count(&iter->children);
+}
+
+int mail_thread_iterate_deinit(struct mail_thread_iterate_context **_iter)
+{
+ struct mail_thread_iterate_context *iter = *_iter;
+
+ *_iter = NULL;
+
+ if (--iter->ctx->refcount == 0) {
+ array_free(&iter->ctx->roots);
+ array_free(&iter->ctx->shadow_nodes);
+ i_free(iter->ctx);
+ }
+ array_free(&iter->children);
+ i_free(iter);
+ return 0;
+}
diff --git a/src/lib-storage/index/index-thread-links.c b/src/lib-storage/index/index-thread-links.c
new file mode 100644
index 0000000..9affb83
--- /dev/null
+++ b/src/lib-storage/index/index-thread-links.c
@@ -0,0 +1,242 @@
+/* Copyright (c) 2002-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "message-id.h"
+#include "mail-storage.h"
+#include "index-thread-private.h"
+
+static uint32_t thread_msg_add(struct mail_thread_cache *cache,
+ uint32_t uid, uint32_t msgid_idx)
+{
+ struct mail_thread_node *node;
+
+ i_assert(msgid_idx != 0);
+ i_assert(msgid_idx < cache->first_invalid_msgid_str_idx);
+
+ node = array_idx_get_space(&cache->thread_nodes, msgid_idx);
+ if (node->uid == 0)
+ node->uid = uid;
+ else {
+ /* duplicate message-id, keep the original.
+ if the original ever gets expunged, rebuild. */
+ node->expunge_rebuilds = TRUE;
+
+ msgid_idx = cache->next_invalid_msgid_str_idx++;
+ node = array_idx_get_space(&cache->thread_nodes, msgid_idx);
+ node->uid = uid;
+ }
+ return msgid_idx;
+}
+
+static bool thread_node_has_ancestor(struct mail_thread_cache *cache,
+ const struct mail_thread_node *node,
+ const struct mail_thread_node *ancestor)
+{
+ while (node != ancestor) {
+ if (node->parent_idx == 0)
+ return FALSE;
+
+ node = array_idx(&cache->thread_nodes, node->parent_idx);
+ }
+ return TRUE;
+}
+
+static void thread_link_reference(struct mail_thread_cache *cache,
+ uint32_t parent_idx, uint32_t child_idx)
+{
+ struct mail_thread_node *node, *parent, *child;
+ uint32_t idx;
+
+ i_assert(parent_idx < cache->first_invalid_msgid_str_idx);
+
+ /* either child_idx or parent_idx may cause thread_nodes array to
+ grow. in such situation the other pointer may become invalid if
+ we don't get the pointers in correct order. */
+ if (child_idx < parent_idx) {
+ parent = array_idx_get_space(&cache->thread_nodes, parent_idx);
+ child = array_idx_modifiable(&cache->thread_nodes, child_idx);
+ } else {
+ child = array_idx_get_space(&cache->thread_nodes, child_idx);
+ parent = array_idx_modifiable(&cache->thread_nodes, parent_idx);
+ }
+
+ child->parent_link_refcount++;
+ if (thread_node_has_ancestor(cache, parent, child)) {
+ if (parent == child) {
+ /* loops to itself - ignore */
+ return;
+ }
+
+ /* child is an ancestor of parent. Adding child -> parent_node
+ would introduce a loop. If any messages referencing the path
+ between parent_node's parent and child_node get expunged, we
+ have to rebuild the tree because the loop might break.
+ For example:
+
+ #1: a -> b (a.ref=1, b.ref=1)
+ #2: b -> a (a.ref=2, b.ref=2)
+ #3: c -> a -> b (a.ref=3, b.ref=3, c.ref=1)
+
+ Expunging #3 wouldn't break the loop, but expunging #1
+ would. */
+ node = parent;
+ do {
+ idx = node->parent_idx;
+ i_assert(idx != 0);
+ node = array_idx_modifiable(&cache->thread_nodes, idx);
+ node->child_unref_rebuilds = TRUE;
+ } while (node != child);
+ return;
+ } else if (child->parent_idx == parent_idx) {
+ /* The same link already exists */
+ return;
+ }
+
+ /* Set parent_node as child_node's parent */
+ if (child->parent_idx == 0) {
+ child->parent_idx = parent_idx;
+ } else {
+ /* Conflicting parent already exists, keep the original */
+ if (MAIL_THREAD_NODE_EXISTS(child)) {
+ /* If this message gets expunged,
+ the parent is changed. */
+ child->expunge_rebuilds = TRUE;
+ } else {
+ /* Message doesn't exist, so it was one of the node's
+ children that created the original reference. If
+ that reference gets dropped, the parent is changed.
+ We could catch this in one of several ways:
+
+ a) Link to parent node gets unreferenced
+ b) Link to this node gets unreferenced
+ c) Any of the child nodes gets expunged
+
+ b) is probably the least likely to happen,
+ so use it */
+ child->child_unref_rebuilds = TRUE;
+ }
+ }
+}
+
+static uint32_t
+thread_link_references(struct mail_thread_cache *cache, uint32_t uid,
+ const struct mail_index_strmap_rec *msgid_map,
+ unsigned int *msgid_map_idx)
+{
+ uint32_t parent_idx;
+
+ if (msgid_map->uid != uid)
+ return 0;
+
+ parent_idx = msgid_map->str_idx;
+ msgid_map++;
+ *msgid_map_idx += 1;
+
+ for (; msgid_map->uid == uid; msgid_map++) {
+ thread_link_reference(cache, parent_idx, msgid_map->str_idx);
+ parent_idx = msgid_map->str_idx;
+ *msgid_map_idx += 1;
+ }
+ i_assert(parent_idx < cache->first_invalid_msgid_str_idx);
+ return parent_idx;
+}
+
+void mail_thread_add(struct mail_thread_cache *cache,
+ const struct mail_index_strmap_rec *msgid_map,
+ unsigned int *msgid_map_idx)
+{
+ struct mail_thread_node *node;
+ uint32_t idx, parent_idx;
+
+ i_assert(msgid_map->ref_index == MAIL_THREAD_NODE_REF_MSGID);
+ i_assert(cache->last_uid <= msgid_map->uid);
+
+ cache->last_uid = msgid_map->uid;
+
+ idx = thread_msg_add(cache, msgid_map->uid, msgid_map->str_idx);
+ parent_idx = thread_link_references(cache, msgid_map->uid,
+ msgid_map + 1, msgid_map_idx);
+
+ node = array_idx_modifiable(&cache->thread_nodes, idx);
+ if (node->parent_idx != parent_idx && node->parent_idx != 0) {
+ /* conflicting parent, remove it. */
+ node->parent_idx = 0;
+ /* If this message gets expunged, we have to revert back to
+ the original parent. */
+ node->expunge_rebuilds = TRUE;
+ }
+ if (parent_idx != 0)
+ thread_link_reference(cache, parent_idx, idx);
+ *msgid_map_idx += 1;
+}
+
+static bool
+mail_thread_unref_link(struct mail_thread_cache *cache,
+ uint32_t parent_idx, uint32_t child_idx)
+{
+ struct mail_thread_node *parent, *child;
+
+ parent = array_idx_modifiable(&cache->thread_nodes, parent_idx);
+ if (parent->child_unref_rebuilds)
+ return FALSE;
+
+ child = array_idx_modifiable(&cache->thread_nodes, child_idx);
+ i_assert(child->parent_link_refcount > 0);
+
+ child->parent_link_refcount--;
+ if (child->parent_link_refcount == 0) {
+ /* we don't have a root anymore */
+ child->parent_idx = 0;
+ }
+ return TRUE;
+}
+
+bool mail_thread_remove(struct mail_thread_cache *cache,
+ const struct mail_index_strmap_rec *msgid_map,
+ unsigned int *msgid_map_idx)
+{
+ struct mail_thread_node *node;
+ uint32_t idx, parent_idx;
+ unsigned int count = 1;
+
+ idx = msgid_map->str_idx;
+ i_assert(idx != 0);
+
+ if (msgid_map->uid > cache->last_uid) {
+ /* this message was never added to the cache, skip */
+ while (msgid_map[count].uid == msgid_map->uid)
+ count++;
+ *msgid_map_idx += count;
+ return TRUE;
+ }
+
+ node = array_idx_modifiable(&cache->thread_nodes, idx);
+ if (node->expunge_rebuilds) {
+ /* this catches the duplicate message-id case */
+ return FALSE;
+ }
+ i_assert(node->uid == msgid_map->uid);
+
+ /* update link refcounts */
+ if (msgid_map[count].uid == node->uid) {
+ parent_idx = msgid_map[count].str_idx;
+ count++;
+ while (msgid_map[count].uid == node->uid) {
+ if (!mail_thread_unref_link(cache, parent_idx,
+ msgid_map[count].str_idx))
+ return FALSE;
+ parent_idx = msgid_map[count].str_idx;
+ count++;
+ }
+ if (!mail_thread_unref_link(cache, parent_idx, idx))
+ return FALSE;
+ }
+ /* mark this message as expunged */
+ node->uid = 0;
+
+ /* we don't know (and don't want to waste time figuring out) if other
+ messages point to this removed message, so don't delete the node */
+ *msgid_map_idx += count;
+ return TRUE;
+}
diff --git a/src/lib-storage/index/index-thread-private.h b/src/lib-storage/index/index-thread-private.h
new file mode 100644
index 0000000..ed2837b
--- /dev/null
+++ b/src/lib-storage/index/index-thread-private.h
@@ -0,0 +1,82 @@
+#ifndef INDEX_THREAD_PRIVATE_H
+#define INDEX_THREAD_PRIVATE_H
+
+#include "crc32.h"
+#include "mail-thread.h"
+#include "mail-index-strmap.h"
+
+#define MAIL_THREAD_INDEX_SUFFIX ".thread"
+
+/* After initially building the index, assign first_invalid_msgid_idx to
+ the next unused index + SKIP_COUNT. When more messages are added and
+ the next valid msgid conflicts with the first invalid msgid, the invalid
+ msgids will be moved forward again this many indexes. */
+#define THREAD_INVALID_MSGID_STR_IDX_SKIP_COUNT \
+ (4096 / sizeof(struct mail_thread_node))
+
+#define HDR_MESSAGE_ID "message-id"
+#define HDR_IN_REPLY_TO "in-reply-to"
+#define HDR_REFERENCES "references"
+#define HDR_SUBJECT "subject"
+
+#define MAIL_THREAD_NODE_REF_MSGID 0
+#define MAIL_THREAD_NODE_REF_INREPLYTO 1
+#define MAIL_THREAD_NODE_REF_REFERENCES1 2
+
+struct mail_thread_node {
+ /* UID of the message, or 0 for dummy nodes */
+ uint32_t uid;
+ /* Index for this node's parent node, 0 = this is root */
+ uint32_t parent_idx;
+ /* Number of messages containing "this message" -> "parent message"
+ link, i.e. "number of links to parent node". However since parents
+ can change, not all of these references might be from our current
+ child nodes. When this refcount reaches 0, it means we must detach
+ from our parent. */
+ unsigned int parent_link_refcount:30;
+ /* If uid is expunged, rebuild the thread tree. */
+ bool expunge_rebuilds:1;
+ /* If a link between this node and its child gets unreferenced,
+ rebuild the thread tree. */
+ bool child_unref_rebuilds:1;
+};
+ARRAY_DEFINE_TYPE(mail_thread_node, struct mail_thread_node);
+#define MAIL_THREAD_NODE_EXISTS(node) \
+ ((node)->uid != 0)
+
+struct mail_thread_cache {
+ uint32_t last_uid;
+ /* indexes used for invalid Message-IDs. that means no other messages
+ point to them and they can safely be moved around whenever
+ necessary. */
+ uint32_t first_invalid_msgid_str_idx;
+ uint32_t next_invalid_msgid_str_idx;
+
+ struct mail_search_result *search_result;
+
+ /* indexed by mail_index_strmap_rec.str_idx */
+ ARRAY_TYPE(mail_thread_node) thread_nodes;
+};
+
+static inline uint32_t crc32_str_nonzero(const char *str)
+{
+ uint32_t value = crc32_str(str);
+ return value == 0 ? 1 : value;
+}
+
+void mail_thread_add(struct mail_thread_cache *cache,
+ const struct mail_index_strmap_rec *msgid_map,
+ unsigned int *msgid_map_idx);
+bool mail_thread_remove(struct mail_thread_cache *cache,
+ const struct mail_index_strmap_rec *msgid_map,
+ unsigned int *msgid_map_idx);
+
+struct mail_thread_iterate_context *
+mail_thread_iterate_init_full(struct mail_thread_cache *cache,
+ struct mail *tmp_mail,
+ enum mail_thread_type thread_type,
+ bool return_seqs);
+
+void index_thread_mailbox_opened(struct mailbox *box);
+
+#endif
diff --git a/src/lib-storage/index/index-thread.c b/src/lib-storage/index/index-thread.c
new file mode 100644
index 0000000..2b1ba04
--- /dev/null
+++ b/src/lib-storage/index/index-thread.c
@@ -0,0 +1,670 @@
+/* Copyright (c) 2002-2018 Dovecot authors, see the included COPYING file */
+
+/* doc/thread-refs.txt describes the incremental algorithm we use here. */
+
+#include "lib.h"
+#include "array.h"
+#include "bsearch-insert-pos.h"
+#include "hash2.h"
+#include "message-id.h"
+#include "mail-search.h"
+#include "mail-search-build.h"
+#include "mailbox-search-result-private.h"
+#include "index-storage.h"
+#include "index-thread-private.h"
+
+
+#define MAIL_THREAD_CONTEXT(obj) \
+ MODULE_CONTEXT(obj, mail_thread_storage_module)
+#define MAIL_THREAD_CONTEXT_REQUIRE(obj) \
+ MODULE_CONTEXT_REQUIRE(obj, mail_thread_storage_module)
+
+struct mail_thread_context {
+ struct mailbox *box;
+ struct mailbox_transaction_context *t;
+ struct mail_index_strmap_view_sync *strmap_sync;
+
+ struct mail *tmp_mail;
+ struct mail_search_args *search_args;
+ ARRAY_TYPE(seq_range) added_uids;
+
+ bool failed:1;
+ bool corrupted:1;
+};
+
+struct mail_thread_mailbox {
+ union mailbox_module_context module_ctx;
+
+ unsigned int next_msgid_idx;
+ struct mail_thread_cache *cache;
+
+ struct mail_index_strmap *strmap;
+ struct mail_index_strmap_view *strmap_view;
+ /* sorted by UID, ref_index */
+ const ARRAY_TYPE(mail_index_strmap_rec) *msgid_map;
+ const struct hash2_table *msgid_hash;
+
+ /* set only temporarily while needed */
+ struct mail_thread_context *ctx;
+};
+
+static MODULE_CONTEXT_DEFINE_INIT(mail_thread_storage_module,
+ &mail_storage_module_register);
+
+static void mail_thread_clear(struct mail_thread_context *ctx);
+
+static int
+mail_strmap_rec_get_msgid(struct mail_thread_context *ctx,
+ const struct mail_index_strmap_rec *rec,
+ const char **msgid_r)
+{
+ struct mail *mail = ctx->tmp_mail;
+ const char *msgids = NULL, *msgid;
+ unsigned int n = 0;
+ int ret;
+
+ if (!mail_set_uid(mail, rec->uid))
+ return 0;
+
+ switch (rec->ref_index) {
+ case MAIL_THREAD_NODE_REF_MSGID:
+ /* Message-ID: header */
+ ret = mail_get_first_header(mail, HDR_MESSAGE_ID, &msgids);
+ break;
+ case MAIL_THREAD_NODE_REF_INREPLYTO:
+ /* In-Reply-To: header */
+ ret = mail_get_first_header(mail, HDR_IN_REPLY_TO, &msgids);
+ break;
+ default:
+ /* References: header */
+ ret = mail_get_first_header(mail, HDR_REFERENCES, &msgids);
+ n = rec->ref_index - MAIL_THREAD_NODE_REF_REFERENCES1;
+ break;
+ }
+
+ if (ret < 0) {
+ if (mail->expunged) {
+ /* treat it as if it didn't exist. trying to add it
+ again will result in failure. */
+ return 0;
+ }
+ return -1;
+ }
+
+ /* get the nth message-id */
+ msgid = message_id_get_next(&msgids);
+ if (msgid != NULL) {
+ for (; n > 0; n--)
+ msgid = message_id_get_next(&msgids);
+ }
+
+ if (msgid == NULL) {
+ /* shouldn't have happened, probably corrupted */
+ mail_set_critical(mail,
+ "Corrupted thread index: lost Message ID %u",
+ rec->ref_index);
+ ctx->failed = TRUE;
+ ctx->corrupted = TRUE;
+ return -1;
+ }
+ *msgid_r = msgid;
+ return 1;
+}
+
+static bool
+mail_thread_hash_key_cmp(const char *key,
+ const struct mail_index_strmap_rec *rec,
+ void *context)
+{
+ struct mail_thread_mailbox *tbox = context;
+ struct mail_thread_context *ctx = tbox->ctx;
+ const char *msgid;
+ bool cmp_ret;
+ int ret;
+
+ /* either a match or a collision, need to look closer */
+ T_BEGIN {
+ ret = mail_strmap_rec_get_msgid(ctx, rec, &msgid);
+ if (ret <= 0) {
+ if (ret < 0)
+ ctx->failed = TRUE;
+ cmp_ret = FALSE;
+ } else {
+ cmp_ret = strcmp(msgid, key) == 0;
+ }
+ } T_END;
+ return cmp_ret;
+}
+
+static int
+mail_thread_hash_rec_cmp(const struct mail_index_strmap_rec *rec1,
+ const struct mail_index_strmap_rec *rec2,
+ void *context)
+{
+ struct mail_thread_mailbox *tbox = context;
+ struct mail_thread_context *ctx = tbox->ctx;
+ const char *msgid1, *msgid2;
+ int ret;
+
+ T_BEGIN {
+ ret = mail_strmap_rec_get_msgid(ctx, rec1, &msgid1);
+ if (ret > 0) {
+ msgid1 = t_strdup(msgid1);
+ ret = mail_strmap_rec_get_msgid(ctx, rec2, &msgid2);
+ }
+ ret = ret <= 0 ? -1 :
+ strcmp(msgid1, msgid2) == 0;
+ } T_END;
+ return ret;
+}
+
+static void mail_thread_strmap_remap(const uint32_t *idx_map,
+ unsigned int old_count,
+ unsigned int new_count, void *context)
+{
+ struct mail_thread_mailbox *tbox = context;
+ struct mail_thread_cache *cache = tbox->cache;
+ ARRAY_TYPE(mail_thread_node) new_nodes;
+ const struct mail_thread_node *old_nodes;
+ struct mail_thread_node *node;
+ unsigned int i, nodes_count, max, new_first_invalid, invalid_count;
+
+ if (cache->search_result == NULL)
+ return;
+
+ if (new_count == 0) {
+ /* strmap was reset, we'll need to rebuild thread */
+ mailbox_search_result_free(&cache->search_result);
+ return;
+ }
+
+ invalid_count = cache->next_invalid_msgid_str_idx -
+ cache->first_invalid_msgid_str_idx;
+
+ old_nodes = array_get(&cache->thread_nodes, &nodes_count);
+ i_array_init(&new_nodes, new_count + invalid_count + 32);
+
+ /* optimization: allocate all nodes initially */
+ (void)array_idx_modifiable(&new_nodes, new_count-1);
+
+ /* renumber existing valid nodes. all existing records in old_nodes
+ should also exist in idx_map since we've removed expunged messages
+ from the cache before committing the sync. */
+ max = I_MIN(I_MIN(old_count, nodes_count),
+ cache->first_invalid_msgid_str_idx);
+ for (i = 0; i < max; i++) {
+ if (idx_map[i] == 0) {
+ /* expunged record. */
+ i_assert(old_nodes[i].uid == 0);
+ } else {
+ node = array_idx_modifiable(&new_nodes, idx_map[i]);
+ *node = old_nodes[i];
+ if (node->parent_idx != 0) {
+ node->parent_idx = idx_map[node->parent_idx];
+ i_assert(node->parent_idx != 0);
+ }
+ }
+ }
+
+ /* copy invalid nodes, if any. no other messages point to them,
+ so this is safe. we still need to update their parent_idx
+ pointers though. */
+ new_first_invalid = new_count + 1 +
+ THREAD_INVALID_MSGID_STR_IDX_SKIP_COUNT;
+ for (i = 0; i < invalid_count; i++) {
+ node = array_idx_modifiable(&new_nodes, new_first_invalid + i);
+ *node = old_nodes[cache->first_invalid_msgid_str_idx + i];
+ if (node->parent_idx != 0) {
+ node->parent_idx = idx_map[node->parent_idx];
+ i_assert(node->parent_idx != 0);
+ }
+ }
+ cache->first_invalid_msgid_str_idx = new_first_invalid;
+ cache->next_invalid_msgid_str_idx = new_first_invalid + invalid_count;
+
+ /* replace the old nodes with the renumbered ones */
+ array_free(&cache->thread_nodes);
+ cache->thread_nodes = new_nodes;
+}
+
+static int thread_get_mail_header(struct mail *mail, const char *name,
+ const char **value_r)
+{
+ if (mail_get_first_header(mail, name, value_r) < 0) {
+ if (!mail->expunged)
+ return -1;
+
+ /* Message is expunged. Instead of failing the entire THREAD
+ command, just treat the header as nonexistent. */
+ *value_r = NULL;
+ }
+ return 0;
+}
+
+static int
+mail_thread_map_add_mail(struct mail_thread_context *ctx, struct mail *mail)
+{
+ const char *message_id, *in_reply_to, *references, *msgid;
+ uint32_t ref_index;
+
+ if (thread_get_mail_header(mail, HDR_MESSAGE_ID, &message_id) < 0 ||
+ thread_get_mail_header(mail, HDR_REFERENCES, &references) < 0)
+ return -1;
+
+ /* add Message-ID: */
+ msgid = message_id_get_next(&message_id);
+ if (msgid != NULL) {
+ mail_index_strmap_view_sync_add(ctx->strmap_sync, mail->uid,
+ MAIL_THREAD_NODE_REF_MSGID,
+ msgid);
+ } else {
+ mail_index_strmap_view_sync_add_unique(ctx->strmap_sync,
+ mail->uid, MAIL_THREAD_NODE_REF_MSGID);
+ }
+
+ /* add References: if there are any valid ones */
+ msgid = message_id_get_next(&references);
+ if (msgid != NULL) {
+ ref_index = MAIL_THREAD_NODE_REF_REFERENCES1;
+ do {
+ mail_index_strmap_view_sync_add(ctx->strmap_sync,
+ mail->uid,
+ ref_index, msgid);
+ ref_index++;
+ msgid = message_id_get_next(&references);
+ } while (msgid != NULL);
+ } else {
+ /* no References:, use In-Reply-To: */
+ if (thread_get_mail_header(mail, HDR_IN_REPLY_TO,
+ &in_reply_to) < 0)
+ return -1;
+
+ msgid = message_id_get_next(&in_reply_to);
+ if (msgid != NULL) {
+ mail_index_strmap_view_sync_add(ctx->strmap_sync,
+ mail->uid, MAIL_THREAD_NODE_REF_INREPLYTO,
+ msgid);
+ }
+ }
+ if (ctx->failed) {
+ /* message-id lookup failed in hash compare */
+ return -1;
+ }
+ return 0;
+}
+
+static int mail_thread_index_map_build(struct mail_thread_context *ctx)
+{
+ static const char *wanted_headers[] = {
+ HDR_MESSAGE_ID, HDR_IN_REPLY_TO, HDR_REFERENCES, HDR_SUBJECT,
+ NULL
+ };
+ struct mail_thread_mailbox *tbox = MAIL_THREAD_CONTEXT_REQUIRE(ctx->box);
+ struct mailbox_header_lookup_ctx *headers_ctx;
+ struct mail_search_args *search_args;
+ struct mail_search_context *search_ctx;
+ struct mail *mail;
+ uint32_t last_uid, seq1, seq2;
+ int ret = 0;
+
+ if (tbox->strmap_view == NULL) {
+ /* first time we're threading this mailbox */
+ tbox->strmap_view =
+ mail_index_strmap_view_open(tbox->strmap,
+ ctx->box->view,
+ mail_thread_hash_key_cmp,
+ mail_thread_hash_rec_cmp,
+ mail_thread_strmap_remap,
+ tbox, &tbox->msgid_map,
+ &tbox->msgid_hash);
+ }
+
+ headers_ctx = mailbox_header_lookup_init(ctx->box, wanted_headers);
+ ctx->tmp_mail = mail_alloc(ctx->t, MAIL_FETCH_DATE |
+ MAIL_FETCH_RECEIVED_DATE, headers_ctx);
+
+ /* add all missing UIDs */
+ ctx->strmap_sync = mail_index_strmap_view_sync_init(tbox->strmap_view,
+ &last_uid);
+ mailbox_get_seq_range(ctx->box, last_uid + 1, (uint32_t)-1,
+ &seq1, &seq2);
+ if (seq1 == 0) {
+ /* nothing is missing */
+ mail_index_strmap_view_sync_commit(&ctx->strmap_sync);
+ mailbox_header_lookup_unref(&headers_ctx);
+ return 0;
+ }
+
+ search_args = mail_search_build_init();
+ mail_search_build_add_seqset(search_args, seq1, seq2);
+ search_ctx = mailbox_search_init(ctx->t, search_args, NULL,
+ MAIL_FETCH_DATE |
+ MAIL_FETCH_RECEIVED_DATE,
+ headers_ctx);
+ mailbox_header_lookup_unref(&headers_ctx);
+ mail_search_args_unref(&search_args);
+
+ while (mailbox_search_next(search_ctx, &mail)) {
+ if (mail_thread_map_add_mail(ctx, mail) < 0) {
+ ret = -1;
+ break;
+ }
+ }
+ if (mailbox_search_deinit(&search_ctx) < 0)
+ ret = -1;
+
+ if (ret < 0)
+ mail_index_strmap_view_sync_rollback(&ctx->strmap_sync);
+ else
+ mail_index_strmap_view_sync_commit(&ctx->strmap_sync);
+ return ret;
+}
+
+static int msgid_map_cmp(const uint32_t *uid,
+ const struct mail_index_strmap_rec *rec)
+{
+ return *uid < rec->uid ? -1 :
+ (*uid > rec->uid ? 1 : 0);
+}
+
+static bool mail_thread_cache_update_removes(struct mail_thread_mailbox *tbox,
+ ARRAY_TYPE(seq_range) *added_uids)
+{
+ struct mail_thread_cache *cache = tbox->cache;
+ ARRAY_TYPE(seq_range) removed_uids;
+ const struct seq_range *uids;
+ const struct mail_index_strmap_rec *msgid_map;
+ unsigned int i, j, idx, map_count, uid_count;
+ uint32_t uid;
+
+ t_array_init(&removed_uids, 64);
+ mailbox_search_result_sync(cache->search_result,
+ &removed_uids, added_uids);
+
+ /* first check that we're not inserting any messages in the middle */
+ uids = array_get(added_uids, &uid_count);
+ if (uid_count > 0 && uids[0].seq1 <= cache->last_uid)
+ return FALSE;
+
+ /* next remove messages so we'll see early if we have to rebuild.
+ we expect to find all removed UIDs from msgid_map that are <= max
+ UID in msgid_map */
+ msgid_map = array_get(tbox->msgid_map, &map_count);
+ uids = array_get(&removed_uids, &uid_count);
+ for (i = j = 0; i < uid_count; i++) {
+ /* find and remove from the map */
+ bsearch_insert_pos(&uids[i].seq1, &msgid_map[j],
+ map_count - j, sizeof(*msgid_map),
+ msgid_map_cmp, &idx);
+ j += idx;
+ if (j == map_count) {
+ /* all removals after this are about messages we never
+ even added to the cache */
+ i_assert(uids[i].seq1 > cache->last_uid);
+ break;
+ }
+ while (j > 0 && msgid_map[j-1].uid == msgid_map[j].uid)
+ j--;
+
+ /* remove the messages from cache */
+ for (uid = uids[i].seq1; uid <= uids[i].seq2; uid++) {
+ if (j == map_count) {
+ i_assert(uid > cache->last_uid);
+ break;
+ }
+ i_assert(msgid_map[j].uid == uid);
+ if (!mail_thread_remove(cache, msgid_map + j, &j))
+ return FALSE;
+ }
+ }
+ return TRUE;
+}
+
+static void mail_thread_cache_update_adds(struct mail_thread_mailbox *tbox,
+ ARRAY_TYPE(seq_range) *added_uids)
+{
+ struct mail_thread_cache *cache = tbox->cache;
+ const struct seq_range *uids;
+ const struct mail_index_strmap_rec *msgid_map;
+ unsigned int i, j, map_count, uid_count;
+ uint32_t uid;
+
+ /* everything removed successfully, add the new messages. all of them
+ should already be in msgid_map. */
+ uids = array_get(added_uids, &uid_count);
+ if (uid_count == 0)
+ return;
+
+ (void)array_bsearch_insert_pos(tbox->msgid_map, &uids[0].seq1,
+ msgid_map_cmp, &j);
+ msgid_map = array_get(tbox->msgid_map, &map_count);
+ i_assert(j < map_count);
+ while (j > 0 && msgid_map[j-1].uid == msgid_map[j].uid)
+ j--;
+
+ for (i = 0; i < uid_count; i++) {
+ for (uid = uids[i].seq1; uid <= uids[i].seq2; uid++) {
+ while (j < map_count && msgid_map[j].uid < uid)
+ j++;
+ i_assert(j < map_count && msgid_map[j].uid == uid);
+ mail_thread_add(cache, msgid_map+j, &j);
+ }
+ }
+}
+
+static void
+mail_thread_cache_fix_invalid_indexes(struct mail_thread_mailbox *tbox)
+{
+ struct mail_thread_cache *cache = tbox->cache;
+ uint32_t highest_idx, new_first_idx, count;
+
+ highest_idx = mail_index_strmap_view_get_highest_idx(tbox->strmap_view);
+ new_first_idx = highest_idx + 1 +
+ THREAD_INVALID_MSGID_STR_IDX_SKIP_COUNT;
+ count = cache->next_invalid_msgid_str_idx -
+ cache->first_invalid_msgid_str_idx;
+
+ if (count == 0) {
+ /* there are no invalid indexes yet, we can update the first
+ invalid index position to delay conflicts. */
+ cache->first_invalid_msgid_str_idx =
+ cache->next_invalid_msgid_str_idx = new_first_idx;
+ } else if (highest_idx >= cache->first_invalid_msgid_str_idx) {
+ /* conflict - move the invalid indexes forward */
+ array_copy(&cache->thread_nodes.arr, new_first_idx,
+ &cache->thread_nodes.arr,
+ cache->first_invalid_msgid_str_idx, count);
+ cache->first_invalid_msgid_str_idx = new_first_idx;
+ cache->next_invalid_msgid_str_idx = new_first_idx + count;
+ }
+}
+
+static void mail_thread_cache_sync_remove(struct mail_thread_mailbox *tbox,
+ struct mail_thread_context *ctx)
+{
+ struct mail_thread_cache *cache = tbox->cache;
+
+ if (cache->search_result == NULL)
+ return;
+
+ if (mail_search_args_equal(ctx->search_args,
+ cache->search_result->search_args)) {
+ t_array_init(&ctx->added_uids, 64);
+ if (mail_thread_cache_update_removes(tbox, &ctx->added_uids)) {
+ /* successfully updated the cache */
+ return;
+ }
+ }
+ /* failed to use the cache, rebuild */
+ mailbox_search_result_free(&cache->search_result);
+}
+
+static void mail_thread_cache_sync_add(struct mail_thread_mailbox *tbox,
+ struct mail_thread_context *ctx,
+ struct mail_search_context *search_ctx)
+{
+ struct mail_thread_cache *cache = tbox->cache;
+ struct mail *mail;
+ const struct mail_index_strmap_rec *msgid_map;
+ unsigned int i, count;
+
+ mail_thread_cache_fix_invalid_indexes(tbox);
+
+ if (cache->search_result != NULL) {
+ /* we already checked at sync_remove that we can use this
+ search result. */
+ mail_thread_cache_update_adds(tbox, &ctx->added_uids);
+ return;
+ }
+
+ cache->last_uid = 0;
+ cache->first_invalid_msgid_str_idx = cache->next_invalid_msgid_str_idx =
+ mail_index_strmap_view_get_highest_idx(tbox->strmap_view) + 1 +
+ THREAD_INVALID_MSGID_STR_IDX_SKIP_COUNT;
+ array_clear(&cache->thread_nodes);
+
+ cache->search_result =
+ mailbox_search_result_save(search_ctx,
+ MAILBOX_SEARCH_RESULT_FLAG_UPDATE |
+ MAILBOX_SEARCH_RESULT_FLAG_QUEUE_SYNC);
+
+ msgid_map = array_get(tbox->msgid_map, &count);
+ /* we're relying on the array being zero-terminated (outside used
+ count - kind of kludgy) */
+ i_assert(msgid_map[count].uid == 0);
+ i = 0;
+ while (i < count && mailbox_search_next(search_ctx, &mail)) {
+ while (msgid_map[i].uid < mail->uid)
+ i++;
+ i_assert(i < count);
+ mail_thread_add(cache, msgid_map+i, &i);
+ }
+}
+
+int mail_thread_init(struct mailbox *box, struct mail_search_args *args,
+ struct mail_thread_context **ctx_r)
+{
+ struct mail_thread_mailbox *tbox = MAIL_THREAD_CONTEXT_REQUIRE(box);
+ struct mail_thread_context *ctx;
+ struct mail_search_context *search_ctx;
+ int ret;
+
+ i_assert(tbox->ctx == NULL);
+
+ struct event_reason *reason = event_reason_begin("mailbox:thread");
+ if (args != NULL)
+ mail_search_args_ref(args);
+ else {
+ args = mail_search_build_init();
+ mail_search_build_add_all(args);
+ mail_search_args_init(args, box, FALSE, NULL);
+ }
+
+ ctx = i_new(struct mail_thread_context, 1);
+ ctx->box = box;
+ ctx->search_args = args;
+ ctx->t = mailbox_transaction_begin(ctx->box, 0, __func__);
+ /* perform search first, so we don't break if there are INTHREAD keys */
+ search_ctx = mailbox_search_init(ctx->t, args, NULL, 0, NULL);
+
+ tbox->ctx = ctx;
+
+ mail_thread_cache_sync_remove(tbox, ctx);
+ ret = mail_thread_index_map_build(ctx);
+ if (ret == 0)
+ mail_thread_cache_sync_add(tbox, ctx, search_ctx);
+ if (mailbox_search_deinit(&search_ctx) < 0)
+ ret = -1;
+ if (ctx->failed) {
+ ret = -1;
+ if (ctx->corrupted)
+ mail_index_strmap_view_set_corrupted(tbox->strmap_view);
+ }
+ event_reason_end(&reason);
+ if (ret < 0) {
+ mail_thread_deinit(&ctx);
+ return -1;
+ } else {
+ i_zero(&ctx->added_uids);
+ *ctx_r = ctx;
+ return 0;
+ }
+}
+
+static void mail_thread_clear(struct mail_thread_context *ctx)
+{
+ mail_free(&ctx->tmp_mail);
+ (void)mailbox_transaction_commit(&ctx->t);
+}
+
+void mail_thread_deinit(struct mail_thread_context **_ctx)
+{
+ struct mail_thread_context *ctx = *_ctx;
+ struct mail_thread_mailbox *tbox = MAIL_THREAD_CONTEXT_REQUIRE(ctx->box);
+
+ *_ctx = NULL;
+
+ mail_thread_clear(ctx);
+ mail_search_args_unref(&ctx->search_args);
+ tbox->ctx = NULL;
+ i_free(ctx);
+}
+
+struct mail_thread_iterate_context *
+mail_thread_iterate_init(struct mail_thread_context *ctx,
+ enum mail_thread_type thread_type, bool write_seqs)
+{
+ struct mail_thread_mailbox *tbox = MAIL_THREAD_CONTEXT_REQUIRE(ctx->box);
+
+ return mail_thread_iterate_init_full(tbox->cache, ctx->tmp_mail,
+ thread_type, write_seqs);
+}
+
+static void mail_thread_mailbox_close(struct mailbox *box)
+{
+ struct mail_thread_mailbox *tbox = MAIL_THREAD_CONTEXT_REQUIRE(box);
+
+ i_assert(tbox->ctx == NULL);
+
+ if (tbox->strmap_view != NULL)
+ mail_index_strmap_view_close(&tbox->strmap_view);
+ if (tbox->cache->search_result != NULL)
+ mailbox_search_result_free(&tbox->cache->search_result);
+ tbox->module_ctx.super.close(box);
+}
+
+static void mail_thread_mailbox_free(struct mailbox *box)
+{
+ struct mail_thread_mailbox *tbox = MAIL_THREAD_CONTEXT_REQUIRE(box);
+
+ mail_index_strmap_deinit(&tbox->strmap);
+ tbox->module_ctx.super.free(box);
+
+ array_free(&tbox->cache->thread_nodes);
+ i_free(tbox->cache);
+ i_free(tbox);
+}
+
+void index_thread_mailbox_opened(struct mailbox *box)
+{
+ struct mail_thread_mailbox *tbox = MAIL_THREAD_CONTEXT(box);
+
+ if (tbox != NULL) {
+ /* mailbox was already opened+closed once. */
+ return;
+ }
+
+ tbox = i_new(struct mail_thread_mailbox, 1);
+ tbox->module_ctx.super = box->v;
+ box->v.close = mail_thread_mailbox_close;
+ box->v.free = mail_thread_mailbox_free;
+
+ tbox->strmap = mail_index_strmap_init(box->index,
+ MAIL_THREAD_INDEX_SUFFIX);
+ tbox->next_msgid_idx = 1;
+
+ tbox->cache = i_new(struct mail_thread_cache, 1);
+ i_array_init(&tbox->cache->thread_nodes, 128);
+
+ MODULE_CONTEXT_SET(box, mail_thread_storage_module, tbox);
+}
diff --git a/src/lib-storage/index/index-transaction.c b/src/lib-storage/index/index-transaction.c
new file mode 100644
index 0000000..edd6c3f
--- /dev/null
+++ b/src/lib-storage/index/index-transaction.c
@@ -0,0 +1,227 @@
+/* Copyright (c) 2003-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "dict.h"
+#include "index-storage.h"
+#include "index-sync-private.h"
+#include "index-pop3-uidl.h"
+#include "index-mail.h"
+
+static void index_transaction_free(struct mailbox_transaction_context *t)
+{
+ if (t->view_pvt != NULL)
+ mail_index_view_close(&t->view_pvt);
+ mail_cache_view_close(&t->cache_view);
+ mail_index_view_close(&t->view);
+ if (array_is_created(&t->pvt_saves))
+ array_free(&t->pvt_saves);
+ array_free(&t->module_contexts);
+ i_free(t->reason);
+ i_free(t);
+}
+
+static int
+index_transaction_index_commit(struct mail_index_transaction *index_trans,
+ struct mail_index_transaction_commit_result *result_r)
+{
+ struct mailbox_transaction_context *t =
+ MAIL_STORAGE_CONTEXT_REQUIRE(index_trans);
+ struct index_mailbox_sync_pvt_context *pvt_sync_ctx = NULL;
+ const char *error;
+ int ret = 0;
+
+ index_pop3_uidl_update_exists_finish(t);
+
+ if (t->attr_pvt_trans != NULL) {
+ if (dict_transaction_commit(&t->attr_pvt_trans, &error) < 0) {
+ mailbox_set_critical(t->box,
+ "Dict private transaction commit failed: %s", error);
+ ret = -1;
+ }
+ }
+ if (t->attr_shared_trans != NULL) {
+ if (dict_transaction_commit(&t->attr_shared_trans, &error) < 0) {
+ mailbox_set_critical(t->box,
+ "Dict shared transaction commit failed: %s", error);
+ ret = -1;
+ }
+ }
+
+ if (t->save_ctx != NULL) {
+ mailbox_save_context_deinit(t->save_ctx);
+ if (ret < 0) {
+ t->box->v.transaction_save_rollback(t->save_ctx);
+ t->save_ctx = NULL;
+ } else if (t->box->v.transaction_save_commit_pre(t->save_ctx) < 0) {
+ t->save_ctx = NULL;
+ ret = -1;
+ }
+ }
+
+ if (array_is_created(&t->pvt_saves)) {
+ if (index_mailbox_sync_pvt_init(t->box, TRUE, 0, &pvt_sync_ctx) < 0)
+ ret = -1;
+ }
+
+ i_assert(t->mail_ref_count == 0);
+ if (ret < 0)
+ t->super.rollback(index_trans);
+ else {
+ if (t->super.commit(index_trans, result_r) < 0) {
+ mailbox_set_index_error(t->box);
+ ret = -1;
+ } else {
+ t->changes->changes_mask = result_r->changes_mask;
+ }
+ }
+
+ if (t->save_ctx != NULL) {
+ i_assert(t->save_ctx->dest_mail == NULL);
+ t->box->v.transaction_save_commit_post(t->save_ctx, result_r);
+ }
+
+ if (pvt_sync_ctx != NULL) {
+ if (index_mailbox_sync_pvt_newmails(pvt_sync_ctx, t) < 0) {
+ /* failed to add private flags. a bit too late to
+ return failure though, so just ignore silently */
+ }
+ index_mailbox_sync_pvt_deinit(&pvt_sync_ctx);
+ }
+
+ if (ret < 0)
+ mail_index_set_error_nolog(t->box->index, mailbox_get_last_error(t->box, NULL));
+ index_transaction_free(t);
+ return ret;
+}
+
+static void
+index_transaction_index_rollback(struct mail_index_transaction *index_trans)
+{
+ struct mailbox_transaction_context *t =
+ MAIL_STORAGE_CONTEXT_REQUIRE(index_trans);
+
+ dict_transaction_rollback(&t->attr_pvt_trans);
+ dict_transaction_rollback(&t->attr_shared_trans);
+
+ if (t->save_ctx != NULL) {
+ mailbox_save_context_deinit(t->save_ctx);
+ t->box->v.transaction_save_rollback(t->save_ctx);
+ }
+
+ i_assert(t->mail_ref_count == 0);
+ t->super.rollback(index_trans);
+ index_transaction_free(t);
+}
+
+static enum mail_index_transaction_flags
+index_transaction_flags_get(enum mailbox_transaction_flags flags)
+{
+ enum mail_index_transaction_flags itrans_flags;
+
+ itrans_flags = MAIL_INDEX_TRANSACTION_FLAG_AVOID_FLAG_UPDATES;
+ if ((flags & MAILBOX_TRANSACTION_FLAG_HIDE) != 0)
+ itrans_flags |= MAIL_INDEX_TRANSACTION_FLAG_HIDE;
+ if ((flags & MAILBOX_TRANSACTION_FLAG_EXTERNAL) != 0)
+ itrans_flags |= MAIL_INDEX_TRANSACTION_FLAG_EXTERNAL;
+ if ((flags & MAILBOX_TRANSACTION_FLAG_SYNC) != 0)
+ itrans_flags |= MAIL_INDEX_TRANSACTION_FLAG_SYNC;
+ return itrans_flags;
+}
+
+void index_transaction_init_pvt(struct mailbox_transaction_context *t)
+{
+ enum mail_index_transaction_flags itrans_flags;
+
+ if (t->box->view_pvt == NULL || t->itrans_pvt != NULL)
+ return;
+
+ itrans_flags = index_transaction_flags_get(t->flags);
+ t->itrans_pvt = mail_index_transaction_begin(t->box->view_pvt,
+ itrans_flags);
+ t->view_pvt = mail_index_transaction_open_updated_view(t->itrans_pvt);
+}
+
+void index_transaction_init(struct mailbox_transaction_context *t,
+ struct mailbox *box,
+ enum mailbox_transaction_flags flags,
+ const char *reason)
+{
+ enum mail_index_transaction_flags itrans_flags;
+
+ i_assert(box->opened);
+
+ itrans_flags = index_transaction_flags_get(flags);
+ if ((flags & MAILBOX_TRANSACTION_FLAG_REFRESH) != 0)
+ mail_index_refresh(box->index);
+
+ t->flags = flags;
+ t->box = box;
+ t->reason = i_strdup(reason);
+ t->itrans = mail_index_transaction_begin(box->view, itrans_flags);
+ t->view = mail_index_transaction_open_updated_view(t->itrans);
+
+ array_create(&t->module_contexts, default_pool,
+ sizeof(void *), 5);
+
+ t->cache_view = mail_cache_view_open(box->cache, t->view);
+ t->cache_trans = mail_cache_get_transaction(t->cache_view, t->itrans);
+
+ if ((flags & MAILBOX_TRANSACTION_FLAG_NO_CACHE_DEC) != 0)
+ mail_cache_view_update_cache_decisions(t->cache_view, FALSE);
+
+ /* set up after mail_cache_get_transaction(), so that we'll still
+ have the cache_trans available in _index_commit() */
+ t->super = t->itrans->v;
+ t->itrans->v.commit = index_transaction_index_commit;
+ t->itrans->v.rollback = index_transaction_index_rollback;
+ MODULE_CONTEXT_SET(t->itrans, mail_storage_mail_index_module, t);
+}
+
+struct mailbox_transaction_context *
+index_transaction_begin(struct mailbox *box,
+ enum mailbox_transaction_flags flags,
+ const char *reason)
+{
+ struct mailbox_transaction_context *t;
+
+ t = i_new(struct mailbox_transaction_context, 1);
+ index_transaction_init(t, box, flags, reason);
+ return t;
+}
+
+int index_transaction_commit(struct mailbox_transaction_context *t,
+ struct mail_transaction_commit_changes *changes_r)
+{
+ struct mailbox *box = t->box;
+ struct mail_index_transaction *itrans = t->itrans;
+ struct mail_index_transaction_commit_result result;
+ int ret = 0;
+
+ i_zero(changes_r);
+ changes_r->pool = pool_alloconly_create(MEMPOOL_GROWING
+ "transaction changes", 512);
+ p_array_init(&changes_r->saved_uids, changes_r->pool, 32);
+ t->changes = changes_r;
+
+ if (t->itrans_pvt != NULL)
+ ret = mail_index_transaction_commit(&t->itrans_pvt);
+ if (mail_index_transaction_commit_full(&itrans, &result) < 0)
+ ret = -1;
+ t = NULL;
+
+ if (ret < 0 && mail_index_is_deleted(box->index))
+ mailbox_set_deleted(box);
+
+ changes_r->ignored_modseq_changes = result.ignored_modseq_changes;
+ return ret;
+}
+
+void index_transaction_rollback(struct mailbox_transaction_context *t)
+{
+ struct mail_index_transaction *itrans = t->itrans;
+
+ if (t->itrans_pvt != NULL)
+ mail_index_transaction_rollback(&t->itrans_pvt);
+ mail_index_transaction_rollback(&itrans);
+}
diff --git a/src/lib-storage/index/istream-mail.c b/src/lib-storage/index/istream-mail.c
new file mode 100644
index 0000000..1477823
--- /dev/null
+++ b/src/lib-storage/index/istream-mail.c
@@ -0,0 +1,173 @@
+/* Copyright (c) 2009-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "mail-storage-private.h"
+#include "istream-private.h"
+#include "index-mail.h"
+#include "istream-mail.h"
+
+struct mail_istream {
+ struct istream_private istream;
+
+ struct mail *mail;
+ uoff_t expected_size;
+ bool files_read_increased:1;
+ bool input_has_body:1;
+};
+
+static bool i_stream_mail_try_get_cached_size(struct mail_istream *mstream)
+{
+ struct mail *mail = mstream->mail;
+ enum mail_lookup_abort orig_lookup_abort;
+
+ if (mstream->expected_size != UOFF_T_MAX)
+ return TRUE;
+
+ /* make sure this call doesn't change any existing error message,
+ just in case there's already something important in it. */
+ mail_storage_last_error_push(mail->box->storage);
+ orig_lookup_abort = mail->lookup_abort;
+ mail->lookup_abort = MAIL_LOOKUP_ABORT_NOT_IN_CACHE;
+ if (mail_get_physical_size(mail, &mstream->expected_size) < 0)
+ mstream->expected_size = UOFF_T_MAX;
+ mail->lookup_abort = orig_lookup_abort;
+ mail_storage_last_error_pop(mail->box->storage);
+ return mstream->expected_size != UOFF_T_MAX;
+}
+
+static const char *
+i_stream_mail_get_cached_mail_id(struct mail_istream *mstream ATTR_UNUSED)
+{
+#if 0
+ /* FIXME: This function may get called in the middle of header parsing,
+ which then goes into parsing cached headers and causes crashes.
+ So disable this for now. Eventually it would be nice if recursion
+ was possible by each parser using its own private struct. */
+ static const char *headers[] = {
+ "Message-Id",
+ "Date",
+ "Subject"
+ };
+ struct mail *mail = mstream->mail;
+ enum mail_lookup_abort orig_lookup_abort;
+ const char *value, *ret = "";
+ unsigned int i;
+
+ orig_lookup_abort = mail->lookup_abort;
+ mail->lookup_abort = MAIL_LOOKUP_ABORT_NOT_IN_CACHE;
+ for (i = 0; i < N_ELEMENTS(headers); i++) {
+ if (mail_get_first_header(mail, headers[i], &value) > 0) {
+ ret = t_strdup_printf("%s=%s", headers[i], value);
+ break;
+ }
+ }
+ mail->lookup_abort = orig_lookup_abort;
+ return ret;
+#else
+ return "";
+#endif
+}
+
+static void
+i_stream_mail_set_size_corrupted(struct mail_istream *mstream, size_t size)
+{
+ uoff_t cur_size = mstream->istream.istream.v_offset + size;
+ const char *str, *mail_id;
+ char chr;
+
+ if (mstream->expected_size < cur_size) {
+ /* input stream is larger than cached message size */
+ str = "smaller";
+ chr = '<';
+ mstream->istream.istream.stream_errno = EINVAL;
+ } else {
+ /* input stream is smaller than cached message size */
+ str = "larger";
+ chr = '>';
+ mstream->istream.istream.stream_errno = EPIPE;
+ }
+
+ mail_id = i_stream_mail_get_cached_mail_id(mstream);
+ if (mail_id[0] != '\0')
+ mail_id = t_strconcat(", cached ", mail_id, NULL);
+ io_stream_set_error(&mstream->istream.iostream,
+ "Cached message size %s than expected "
+ "(%"PRIuUOFF_T" %c %"PRIuUOFF_T", box=%s, UID=%u%s)", str,
+ mstream->expected_size, chr, cur_size,
+ mailbox_get_vname(mstream->mail->box),
+ mstream->mail->uid, mail_id);
+ mail_set_cache_corrupted(mstream->mail, MAIL_FETCH_PHYSICAL_SIZE,
+ t_strdup_printf("read(%s) failed: %s",
+ i_stream_get_name(&mstream->istream.istream),
+ mstream->istream.iostream.error));
+}
+
+static ssize_t
+i_stream_mail_read(struct istream_private *stream)
+{
+ struct mail_istream *mstream = (struct mail_istream *)stream;
+ size_t size;
+ ssize_t ret;
+
+ i_stream_seek(stream->parent, stream->parent_start_offset +
+ stream->istream.v_offset);
+
+ ret = i_stream_read_copy_from_parent(&stream->istream);
+ size = i_stream_get_data_size(&stream->istream);
+ if (ret > 0) {
+ mstream->mail->transaction->stats.files_read_bytes += ret;
+ if (!mstream->files_read_increased) {
+ mstream->files_read_increased = TRUE;
+ mstream->mail->transaction->stats.files_read_count++;
+ }
+ if (mstream->expected_size < stream->istream.v_offset + size) {
+ i_stream_mail_set_size_corrupted(mstream, size);
+ /* istream code expects that the position has not changed
+ when read error occurs, so move pos back. */
+ i_assert(stream->pos >= (size_t)ret);
+ stream->pos -= ret;
+ return -1;
+ }
+ } else if (ret == -1 && stream->istream.eof) {
+ if (!mstream->input_has_body) {
+ /* trying to read past the header, but this stream
+ doesn't have the body */
+ return -1;
+ }
+ if (stream->istream.stream_errno != 0) {
+ if (stream->istream.stream_errno == ENOENT) {
+ /* update mail's expunged-flag if needed */
+ index_mail_refresh_expunged(mstream->mail);
+ }
+ return -1;
+ }
+ if (i_stream_mail_try_get_cached_size(mstream) &&
+ mstream->expected_size > stream->istream.v_offset + size) {
+ i_stream_mail_set_size_corrupted(mstream, size);
+ return -1;
+ }
+ }
+ return ret;
+}
+
+struct istream *i_stream_create_mail(struct mail *mail, struct istream *input,
+ bool input_has_body)
+{
+ struct mail_istream *mstream;
+
+ mstream = i_new(struct mail_istream, 1);
+ mstream->mail = mail;
+ mstream->input_has_body = input_has_body;
+ mstream->expected_size = UOFF_T_MAX;
+ (void)i_stream_mail_try_get_cached_size(mstream);
+ mstream->istream.max_buffer_size = input->real_stream->max_buffer_size;
+ mstream->istream.stream_size_passthrough = TRUE;
+
+ mstream->istream.read = i_stream_mail_read;
+
+ mstream->istream.istream.readable_fd = input->readable_fd;
+ mstream->istream.istream.blocking = input->blocking;
+ mstream->istream.istream.seekable = input->seekable;
+ return i_stream_create(&mstream->istream, input,
+ i_stream_get_fd(input), 0);
+}
diff --git a/src/lib-storage/index/istream-mail.h b/src/lib-storage/index/istream-mail.h
new file mode 100644
index 0000000..24bd567
--- /dev/null
+++ b/src/lib-storage/index/istream-mail.h
@@ -0,0 +1,7 @@
+#ifndef ISTREAM_MAIL_H
+#define ISTREAM_MAIL_H
+
+struct istream *i_stream_create_mail(struct mail *mail, struct istream *input,
+ bool input_has_body);
+
+#endif
diff --git a/src/lib-storage/index/maildir/Makefile.am b/src/lib-storage/index/maildir/Makefile.am
new file mode 100644
index 0000000..7acf249
--- /dev/null
+++ b/src/lib-storage/index/maildir/Makefile.am
@@ -0,0 +1,36 @@
+noinst_LTLIBRARIES = libstorage_maildir.la
+
+AM_CPPFLAGS = \
+ -I$(top_srcdir)/src/lib \
+ -I$(top_srcdir)/src/lib-settings \
+ -I$(top_srcdir)/src/lib-mail \
+ -I$(top_srcdir)/src/lib-imap \
+ -I$(top_srcdir)/src/lib-index \
+ -I$(top_srcdir)/src/lib-storage \
+ -I$(top_srcdir)/src/lib-storage/index
+
+libstorage_maildir_la_SOURCES = \
+ maildir-copy.c \
+ maildir-filename.c \
+ maildir-filename-flags.c \
+ maildir-keywords.c \
+ maildir-mail.c \
+ maildir-save.c \
+ maildir-settings.c \
+ maildir-storage.c \
+ maildir-sync.c \
+ maildir-sync-index.c \
+ maildir-uidlist.c \
+ maildir-util.c
+
+headers = \
+ maildir-filename.h \
+ maildir-filename-flags.h \
+ maildir-keywords.h \
+ maildir-storage.h \
+ maildir-settings.h \
+ maildir-sync.h \
+ maildir-uidlist.h
+
+pkginc_libdir=$(pkgincludedir)
+pkginc_lib_HEADERS = $(headers)
diff --git a/src/lib-storage/index/maildir/Makefile.in b/src/lib-storage/index/maildir/Makefile.in
new file mode 100644
index 0000000..7210c54
--- /dev/null
+++ b/src/lib-storage/index/maildir/Makefile.in
@@ -0,0 +1,875 @@
+# Makefile.in generated by automake 1.16.1 from Makefile.am.
+# @configure_input@
+
+# Copyright (C) 1994-2018 Free Software Foundation, Inc.
+
+# This Makefile.in is free software; the Free Software Foundation
+# gives unlimited permission to copy and/or distribute it,
+# with or without modifications, as long as this notice is preserved.
+
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY, to the extent permitted by law; without
+# even the implied warranty of MERCHANTABILITY or FITNESS FOR A
+# PARTICULAR PURPOSE.
+
+@SET_MAKE@
+
+
+VPATH = @srcdir@
+am__is_gnu_make = { \
+ if test -z '$(MAKELEVEL)'; then \
+ false; \
+ elif test -n '$(MAKE_HOST)'; then \
+ true; \
+ elif test -n '$(MAKE_VERSION)' && test -n '$(CURDIR)'; then \
+ true; \
+ else \
+ false; \
+ fi; \
+}
+am__make_running_with_option = \
+ case $${target_option-} in \
+ ?) ;; \
+ *) echo "am__make_running_with_option: internal error: invalid" \
+ "target option '$${target_option-}' specified" >&2; \
+ exit 1;; \
+ esac; \
+ has_opt=no; \
+ sane_makeflags=$$MAKEFLAGS; \
+ if $(am__is_gnu_make); then \
+ sane_makeflags=$$MFLAGS; \
+ else \
+ case $$MAKEFLAGS in \
+ *\\[\ \ ]*) \
+ bs=\\; \
+ sane_makeflags=`printf '%s\n' "$$MAKEFLAGS" \
+ | sed "s/$$bs$$bs[$$bs $$bs ]*//g"`;; \
+ esac; \
+ fi; \
+ skip_next=no; \
+ strip_trailopt () \
+ { \
+ flg=`printf '%s\n' "$$flg" | sed "s/$$1.*$$//"`; \
+ }; \
+ for flg in $$sane_makeflags; do \
+ test $$skip_next = yes && { skip_next=no; continue; }; \
+ case $$flg in \
+ *=*|--*) continue;; \
+ -*I) strip_trailopt 'I'; skip_next=yes;; \
+ -*I?*) strip_trailopt 'I';; \
+ -*O) strip_trailopt 'O'; skip_next=yes;; \
+ -*O?*) strip_trailopt 'O';; \
+ -*l) strip_trailopt 'l'; skip_next=yes;; \
+ -*l?*) strip_trailopt 'l';; \
+ -[dEDm]) skip_next=yes;; \
+ -[JT]) skip_next=yes;; \
+ esac; \
+ case $$flg in \
+ *$$target_option*) has_opt=yes; break;; \
+ esac; \
+ done; \
+ test $$has_opt = yes
+am__make_dryrun = (target_option=n; $(am__make_running_with_option))
+am__make_keepgoing = (target_option=k; $(am__make_running_with_option))
+pkgdatadir = $(datadir)/@PACKAGE@
+pkgincludedir = $(includedir)/@PACKAGE@
+pkglibdir = $(libdir)/@PACKAGE@
+pkglibexecdir = $(libexecdir)/@PACKAGE@
+am__cd = CDPATH="$${ZSH_VERSION+.}$(PATH_SEPARATOR)" && cd
+install_sh_DATA = $(install_sh) -c -m 644
+install_sh_PROGRAM = $(install_sh) -c
+install_sh_SCRIPT = $(install_sh) -c
+INSTALL_HEADER = $(INSTALL_DATA)
+transform = $(program_transform_name)
+NORMAL_INSTALL = :
+PRE_INSTALL = :
+POST_INSTALL = :
+NORMAL_UNINSTALL = :
+PRE_UNINSTALL = :
+POST_UNINSTALL = :
+build_triplet = @build@
+host_triplet = @host@
+subdir = src/lib-storage/index/maildir
+ACLOCAL_M4 = $(top_srcdir)/aclocal.m4
+am__aclocal_m4_deps = $(top_srcdir)/m4/ac_checktype2.m4 \
+ $(top_srcdir)/m4/ac_typeof.m4 $(top_srcdir)/m4/arc4random.m4 \
+ $(top_srcdir)/m4/blockdev.m4 $(top_srcdir)/m4/c99_vsnprintf.m4 \
+ $(top_srcdir)/m4/clock_gettime.m4 $(top_srcdir)/m4/crypt.m4 \
+ $(top_srcdir)/m4/crypt_xpg6.m4 $(top_srcdir)/m4/dbqlk.m4 \
+ $(top_srcdir)/m4/dirent_dtype.m4 $(top_srcdir)/m4/dovecot.m4 \
+ $(top_srcdir)/m4/fd_passing.m4 $(top_srcdir)/m4/fdatasync.m4 \
+ $(top_srcdir)/m4/flexible_array_member.m4 \
+ $(top_srcdir)/m4/glibc.m4 $(top_srcdir)/m4/gmtime_max.m4 \
+ $(top_srcdir)/m4/gmtime_tm_gmtoff.m4 \
+ $(top_srcdir)/m4/ioloop.m4 $(top_srcdir)/m4/iovec.m4 \
+ $(top_srcdir)/m4/ipv6.m4 $(top_srcdir)/m4/libcap.m4 \
+ $(top_srcdir)/m4/libtool.m4 $(top_srcdir)/m4/libwrap.m4 \
+ $(top_srcdir)/m4/linux_mremap.m4 $(top_srcdir)/m4/ltoptions.m4 \
+ $(top_srcdir)/m4/ltsugar.m4 $(top_srcdir)/m4/ltversion.m4 \
+ $(top_srcdir)/m4/lt~obsolete.m4 $(top_srcdir)/m4/mmap_write.m4 \
+ $(top_srcdir)/m4/mntctl.m4 $(top_srcdir)/m4/modules.m4 \
+ $(top_srcdir)/m4/notify.m4 $(top_srcdir)/m4/nsl.m4 \
+ $(top_srcdir)/m4/off_t_max.m4 $(top_srcdir)/m4/pkg.m4 \
+ $(top_srcdir)/m4/pr_set_dumpable.m4 \
+ $(top_srcdir)/m4/q_quotactl.m4 $(top_srcdir)/m4/quota.m4 \
+ $(top_srcdir)/m4/random.m4 $(top_srcdir)/m4/rlimit.m4 \
+ $(top_srcdir)/m4/sendfile.m4 $(top_srcdir)/m4/size_t_signed.m4 \
+ $(top_srcdir)/m4/sockpeercred.m4 $(top_srcdir)/m4/sql.m4 \
+ $(top_srcdir)/m4/ssl.m4 $(top_srcdir)/m4/st_tim.m4 \
+ $(top_srcdir)/m4/static_array.m4 $(top_srcdir)/m4/test_with.m4 \
+ $(top_srcdir)/m4/time_t.m4 $(top_srcdir)/m4/typeof.m4 \
+ $(top_srcdir)/m4/typeof_dev_t.m4 \
+ $(top_srcdir)/m4/uoff_t_max.m4 $(top_srcdir)/m4/vararg.m4 \
+ $(top_srcdir)/m4/want_apparmor.m4 \
+ $(top_srcdir)/m4/want_bsdauth.m4 \
+ $(top_srcdir)/m4/want_bzlib.m4 \
+ $(top_srcdir)/m4/want_cassandra.m4 \
+ $(top_srcdir)/m4/want_cdb.m4 \
+ $(top_srcdir)/m4/want_checkpassword.m4 \
+ $(top_srcdir)/m4/want_clucene.m4 $(top_srcdir)/m4/want_db.m4 \
+ $(top_srcdir)/m4/want_gssapi.m4 $(top_srcdir)/m4/want_icu.m4 \
+ $(top_srcdir)/m4/want_ldap.m4 $(top_srcdir)/m4/want_lua.m4 \
+ $(top_srcdir)/m4/want_lz4.m4 $(top_srcdir)/m4/want_lzma.m4 \
+ $(top_srcdir)/m4/want_mysql.m4 $(top_srcdir)/m4/want_pam.m4 \
+ $(top_srcdir)/m4/want_passwd.m4 $(top_srcdir)/m4/want_pgsql.m4 \
+ $(top_srcdir)/m4/want_prefetch.m4 \
+ $(top_srcdir)/m4/want_shadow.m4 \
+ $(top_srcdir)/m4/want_sodium.m4 $(top_srcdir)/m4/want_solr.m4 \
+ $(top_srcdir)/m4/want_sqlite.m4 \
+ $(top_srcdir)/m4/want_stemmer.m4 \
+ $(top_srcdir)/m4/want_systemd.m4 \
+ $(top_srcdir)/m4/want_textcat.m4 \
+ $(top_srcdir)/m4/want_unwind.m4 $(top_srcdir)/m4/want_zlib.m4 \
+ $(top_srcdir)/m4/want_zstd.m4 $(top_srcdir)/configure.ac
+am__configure_deps = $(am__aclocal_m4_deps) $(CONFIGURE_DEPENDENCIES) \
+ $(ACLOCAL_M4)
+DIST_COMMON = $(srcdir)/Makefile.am $(pkginc_lib_HEADERS) \
+ $(am__DIST_COMMON)
+mkinstalldirs = $(install_sh) -d
+CONFIG_HEADER = $(top_builddir)/config.h
+CONFIG_CLEAN_FILES =
+CONFIG_CLEAN_VPATH_FILES =
+LTLIBRARIES = $(noinst_LTLIBRARIES)
+libstorage_maildir_la_LIBADD =
+am_libstorage_maildir_la_OBJECTS = maildir-copy.lo maildir-filename.lo \
+ maildir-filename-flags.lo maildir-keywords.lo maildir-mail.lo \
+ maildir-save.lo maildir-settings.lo maildir-storage.lo \
+ maildir-sync.lo maildir-sync-index.lo maildir-uidlist.lo \
+ maildir-util.lo
+libstorage_maildir_la_OBJECTS = $(am_libstorage_maildir_la_OBJECTS)
+AM_V_lt = $(am__v_lt_@AM_V@)
+am__v_lt_ = $(am__v_lt_@AM_DEFAULT_V@)
+am__v_lt_0 = --silent
+am__v_lt_1 =
+AM_V_P = $(am__v_P_@AM_V@)
+am__v_P_ = $(am__v_P_@AM_DEFAULT_V@)
+am__v_P_0 = false
+am__v_P_1 = :
+AM_V_GEN = $(am__v_GEN_@AM_V@)
+am__v_GEN_ = $(am__v_GEN_@AM_DEFAULT_V@)
+am__v_GEN_0 = @echo " GEN " $@;
+am__v_GEN_1 =
+AM_V_at = $(am__v_at_@AM_V@)
+am__v_at_ = $(am__v_at_@AM_DEFAULT_V@)
+am__v_at_0 = @
+am__v_at_1 =
+DEFAULT_INCLUDES = -I.@am__isrc@ -I$(top_builddir)
+depcomp = $(SHELL) $(top_srcdir)/depcomp
+am__maybe_remake_depfiles = depfiles
+am__depfiles_remade = ./$(DEPDIR)/maildir-copy.Plo \
+ ./$(DEPDIR)/maildir-filename-flags.Plo \
+ ./$(DEPDIR)/maildir-filename.Plo \
+ ./$(DEPDIR)/maildir-keywords.Plo ./$(DEPDIR)/maildir-mail.Plo \
+ ./$(DEPDIR)/maildir-save.Plo ./$(DEPDIR)/maildir-settings.Plo \
+ ./$(DEPDIR)/maildir-storage.Plo \
+ ./$(DEPDIR)/maildir-sync-index.Plo \
+ ./$(DEPDIR)/maildir-sync.Plo ./$(DEPDIR)/maildir-uidlist.Plo \
+ ./$(DEPDIR)/maildir-util.Plo
+am__mv = mv -f
+COMPILE = $(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) \
+ $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS)
+LTCOMPILE = $(LIBTOOL) $(AM_V_lt) --tag=CC $(AM_LIBTOOLFLAGS) \
+ $(LIBTOOLFLAGS) --mode=compile $(CC) $(DEFS) \
+ $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) $(CPPFLAGS) \
+ $(AM_CFLAGS) $(CFLAGS)
+AM_V_CC = $(am__v_CC_@AM_V@)
+am__v_CC_ = $(am__v_CC_@AM_DEFAULT_V@)
+am__v_CC_0 = @echo " CC " $@;
+am__v_CC_1 =
+CCLD = $(CC)
+LINK = $(LIBTOOL) $(AM_V_lt) --tag=CC $(AM_LIBTOOLFLAGS) \
+ $(LIBTOOLFLAGS) --mode=link $(CCLD) $(AM_CFLAGS) $(CFLAGS) \
+ $(AM_LDFLAGS) $(LDFLAGS) -o $@
+AM_V_CCLD = $(am__v_CCLD_@AM_V@)
+am__v_CCLD_ = $(am__v_CCLD_@AM_DEFAULT_V@)
+am__v_CCLD_0 = @echo " CCLD " $@;
+am__v_CCLD_1 =
+SOURCES = $(libstorage_maildir_la_SOURCES)
+DIST_SOURCES = $(libstorage_maildir_la_SOURCES)
+am__can_run_installinfo = \
+ case $$AM_UPDATE_INFO_DIR in \
+ n|no|NO) false;; \
+ *) (install-info --version) >/dev/null 2>&1;; \
+ esac
+am__vpath_adj_setup = srcdirstrip=`echo "$(srcdir)" | sed 's|.|.|g'`;
+am__vpath_adj = case $$p in \
+ $(srcdir)/*) f=`echo "$$p" | sed "s|^$$srcdirstrip/||"`;; \
+ *) f=$$p;; \
+ esac;
+am__strip_dir = f=`echo $$p | sed -e 's|^.*/||'`;
+am__install_max = 40
+am__nobase_strip_setup = \
+ srcdirstrip=`echo "$(srcdir)" | sed 's/[].[^$$\\*|]/\\\\&/g'`
+am__nobase_strip = \
+ for p in $$list; do echo "$$p"; done | sed -e "s|$$srcdirstrip/||"
+am__nobase_list = $(am__nobase_strip_setup); \
+ for p in $$list; do echo "$$p $$p"; done | \
+ sed "s| $$srcdirstrip/| |;"' / .*\//!s/ .*/ ./; s,\( .*\)/[^/]*$$,\1,' | \
+ $(AWK) 'BEGIN { files["."] = "" } { files[$$2] = files[$$2] " " $$1; \
+ if (++n[$$2] == $(am__install_max)) \
+ { print $$2, files[$$2]; n[$$2] = 0; files[$$2] = "" } } \
+ END { for (dir in files) print dir, files[dir] }'
+am__base_list = \
+ sed '$$!N;$$!N;$$!N;$$!N;$$!N;$$!N;$$!N;s/\n/ /g' | \
+ sed '$$!N;$$!N;$$!N;$$!N;s/\n/ /g'
+am__uninstall_files_from_dir = { \
+ test -z "$$files" \
+ || { test ! -d "$$dir" && test ! -f "$$dir" && test ! -r "$$dir"; } \
+ || { echo " ( cd '$$dir' && rm -f" $$files ")"; \
+ $(am__cd) "$$dir" && rm -f $$files; }; \
+ }
+am__installdirs = "$(DESTDIR)$(pkginc_libdir)"
+HEADERS = $(pkginc_lib_HEADERS)
+am__tagged_files = $(HEADERS) $(SOURCES) $(TAGS_FILES) $(LISP)
+# Read a list of newline-separated strings from the standard input,
+# and print each of them once, without duplicates. Input order is
+# *not* preserved.
+am__uniquify_input = $(AWK) '\
+ BEGIN { nonempty = 0; } \
+ { items[$$0] = 1; nonempty = 1; } \
+ END { if (nonempty) { for (i in items) print i; }; } \
+'
+# Make sure the list of sources is unique. This is necessary because,
+# e.g., the same source file might be shared among _SOURCES variables
+# for different programs/libraries.
+am__define_uniq_tagged_files = \
+ list='$(am__tagged_files)'; \
+ unique=`for i in $$list; do \
+ if test -f "$$i"; then echo $$i; else echo $(srcdir)/$$i; fi; \
+ done | $(am__uniquify_input)`
+ETAGS = etags
+CTAGS = ctags
+am__DIST_COMMON = $(srcdir)/Makefile.in $(top_srcdir)/depcomp
+DISTFILES = $(DIST_COMMON) $(DIST_SOURCES) $(TEXINFOS) $(EXTRA_DIST)
+ACLOCAL = @ACLOCAL@
+ACLOCAL_AMFLAGS = @ACLOCAL_AMFLAGS@
+AMTAR = @AMTAR@
+AM_DEFAULT_VERBOSITY = @AM_DEFAULT_VERBOSITY@
+APPARMOR_LIBS = @APPARMOR_LIBS@
+AR = @AR@
+AUTH_CFLAGS = @AUTH_CFLAGS@
+AUTH_LIBS = @AUTH_LIBS@
+AUTOCONF = @AUTOCONF@
+AUTOHEADER = @AUTOHEADER@
+AUTOMAKE = @AUTOMAKE@
+AWK = @AWK@
+BINARY_CFLAGS = @BINARY_CFLAGS@
+BINARY_LDFLAGS = @BINARY_LDFLAGS@
+BISON = @BISON@
+CASSANDRA_CFLAGS = @CASSANDRA_CFLAGS@
+CASSANDRA_LIBS = @CASSANDRA_LIBS@
+CC = @CC@
+CCDEPMODE = @CCDEPMODE@
+CDB_LIBS = @CDB_LIBS@
+CFLAGS = @CFLAGS@
+CLUCENE_CFLAGS = @CLUCENE_CFLAGS@
+CLUCENE_LIBS = @CLUCENE_LIBS@
+COMPRESS_LIBS = @COMPRESS_LIBS@
+CPP = @CPP@
+CPPFLAGS = @CPPFLAGS@
+CRYPT_LIBS = @CRYPT_LIBS@
+CXX = @CXX@
+CXXCPP = @CXXCPP@
+CXXDEPMODE = @CXXDEPMODE@
+CXXFLAGS = @CXXFLAGS@
+CYGPATH_W = @CYGPATH_W@
+DEFS = @DEFS@
+DEPDIR = @DEPDIR@
+DICT_LIBS = @DICT_LIBS@
+DLLIB = @DLLIB@
+DLLTOOL = @DLLTOOL@
+DSYMUTIL = @DSYMUTIL@
+DUMPBIN = @DUMPBIN@
+ECHO_C = @ECHO_C@
+ECHO_N = @ECHO_N@
+ECHO_T = @ECHO_T@
+EGREP = @EGREP@
+EXEEXT = @EXEEXT@
+FGREP = @FGREP@
+FLEX = @FLEX@
+FUZZER_CPPFLAGS = @FUZZER_CPPFLAGS@
+FUZZER_LDFLAGS = @FUZZER_LDFLAGS@
+GREP = @GREP@
+INSTALL = @INSTALL@
+INSTALL_DATA = @INSTALL_DATA@
+INSTALL_PROGRAM = @INSTALL_PROGRAM@
+INSTALL_SCRIPT = @INSTALL_SCRIPT@
+INSTALL_STRIP_PROGRAM = @INSTALL_STRIP_PROGRAM@
+KRB5CONFIG = @KRB5CONFIG@
+KRB5_CFLAGS = @KRB5_CFLAGS@
+KRB5_LIBS = @KRB5_LIBS@
+LD = @LD@
+LDAP_LIBS = @LDAP_LIBS@
+LDFLAGS = @LDFLAGS@
+LD_NO_WHOLE_ARCHIVE = @LD_NO_WHOLE_ARCHIVE@
+LD_WHOLE_ARCHIVE = @LD_WHOLE_ARCHIVE@
+LIBCAP = @LIBCAP@
+LIBDOVECOT = @LIBDOVECOT@
+LIBDOVECOT_COMPRESS = @LIBDOVECOT_COMPRESS@
+LIBDOVECOT_DEPS = @LIBDOVECOT_DEPS@
+LIBDOVECOT_DSYNC = @LIBDOVECOT_DSYNC@
+LIBDOVECOT_LA_LIBS = @LIBDOVECOT_LA_LIBS@
+LIBDOVECOT_LDA = @LIBDOVECOT_LDA@
+LIBDOVECOT_LDAP = @LIBDOVECOT_LDAP@
+LIBDOVECOT_LIBFTS = @LIBDOVECOT_LIBFTS@
+LIBDOVECOT_LIBFTS_DEPS = @LIBDOVECOT_LIBFTS_DEPS@
+LIBDOVECOT_LOGIN = @LIBDOVECOT_LOGIN@
+LIBDOVECOT_LUA = @LIBDOVECOT_LUA@
+LIBDOVECOT_LUA_DEPS = @LIBDOVECOT_LUA_DEPS@
+LIBDOVECOT_SQL = @LIBDOVECOT_SQL@
+LIBDOVECOT_STORAGE = @LIBDOVECOT_STORAGE@
+LIBDOVECOT_STORAGE_DEPS = @LIBDOVECOT_STORAGE_DEPS@
+LIBEXTTEXTCAT_CFLAGS = @LIBEXTTEXTCAT_CFLAGS@
+LIBEXTTEXTCAT_LIBS = @LIBEXTTEXTCAT_LIBS@
+LIBICONV = @LIBICONV@
+LIBICU_CFLAGS = @LIBICU_CFLAGS@
+LIBICU_LIBS = @LIBICU_LIBS@
+LIBOBJS = @LIBOBJS@
+LIBS = @LIBS@
+LIBSODIUM_CFLAGS = @LIBSODIUM_CFLAGS@
+LIBSODIUM_LIBS = @LIBSODIUM_LIBS@
+LIBTIRPC_CFLAGS = @LIBTIRPC_CFLAGS@
+LIBTIRPC_LIBS = @LIBTIRPC_LIBS@
+LIBTOOL = @LIBTOOL@
+LIBUNWIND_CFLAGS = @LIBUNWIND_CFLAGS@
+LIBUNWIND_LIBS = @LIBUNWIND_LIBS@
+LIBWRAP_LIBS = @LIBWRAP_LIBS@
+LINKED_STORAGE_LDADD = @LINKED_STORAGE_LDADD@
+LIPO = @LIPO@
+LN_S = @LN_S@
+LTLIBICONV = @LTLIBICONV@
+LTLIBOBJS = @LTLIBOBJS@
+LT_SYS_LIBRARY_PATH = @LT_SYS_LIBRARY_PATH@
+LUA_CFLAGS = @LUA_CFLAGS@
+LUA_LIBS = @LUA_LIBS@
+MAINT = @MAINT@
+MAKEINFO = @MAKEINFO@
+MANIFEST_TOOL = @MANIFEST_TOOL@
+MKDIR_P = @MKDIR_P@
+MODULE_LIBS = @MODULE_LIBS@
+MODULE_SUFFIX = @MODULE_SUFFIX@
+MYSQL_CFLAGS = @MYSQL_CFLAGS@
+MYSQL_CONFIG = @MYSQL_CONFIG@
+MYSQL_LIBS = @MYSQL_LIBS@
+NM = @NM@
+NMEDIT = @NMEDIT@
+NOPLUGIN_LDFLAGS = @NOPLUGIN_LDFLAGS@
+OBJDUMP = @OBJDUMP@
+OBJEXT = @OBJEXT@
+OTOOL = @OTOOL@
+OTOOL64 = @OTOOL64@
+PACKAGE = @PACKAGE@
+PACKAGE_BUGREPORT = @PACKAGE_BUGREPORT@
+PACKAGE_NAME = @PACKAGE_NAME@
+PACKAGE_STRING = @PACKAGE_STRING@
+PACKAGE_TARNAME = @PACKAGE_TARNAME@
+PACKAGE_URL = @PACKAGE_URL@
+PACKAGE_VERSION = @PACKAGE_VERSION@
+PANDOC = @PANDOC@
+PATH_SEPARATOR = @PATH_SEPARATOR@
+PGSQL_CFLAGS = @PGSQL_CFLAGS@
+PGSQL_LIBS = @PGSQL_LIBS@
+PG_CONFIG = @PG_CONFIG@
+PIE_CFLAGS = @PIE_CFLAGS@
+PIE_LDFLAGS = @PIE_LDFLAGS@
+PKG_CONFIG = @PKG_CONFIG@
+PKG_CONFIG_LIBDIR = @PKG_CONFIG_LIBDIR@
+PKG_CONFIG_PATH = @PKG_CONFIG_PATH@
+QUOTA_LIBS = @QUOTA_LIBS@
+RANLIB = @RANLIB@
+RELRO_LDFLAGS = @RELRO_LDFLAGS@
+RPCGEN = @RPCGEN@
+RUN_TEST = @RUN_TEST@
+SED = @SED@
+SETTING_FILES = @SETTING_FILES@
+SET_MAKE = @SET_MAKE@
+SHELL = @SHELL@
+SQLITE_CFLAGS = @SQLITE_CFLAGS@
+SQLITE_LIBS = @SQLITE_LIBS@
+SQL_CFLAGS = @SQL_CFLAGS@
+SQL_LIBS = @SQL_LIBS@
+SSL_CFLAGS = @SSL_CFLAGS@
+SSL_LIBS = @SSL_LIBS@
+STRIP = @STRIP@
+SYSTEMD_CFLAGS = @SYSTEMD_CFLAGS@
+SYSTEMD_LIBS = @SYSTEMD_LIBS@
+VALGRIND = @VALGRIND@
+VERSION = @VERSION@
+ZSTD_CFLAGS = @ZSTD_CFLAGS@
+ZSTD_LIBS = @ZSTD_LIBS@
+abs_builddir = @abs_builddir@
+abs_srcdir = @abs_srcdir@
+abs_top_builddir = @abs_top_builddir@
+abs_top_srcdir = @abs_top_srcdir@
+ac_ct_AR = @ac_ct_AR@
+ac_ct_CC = @ac_ct_CC@
+ac_ct_CXX = @ac_ct_CXX@
+ac_ct_DUMPBIN = @ac_ct_DUMPBIN@
+am__include = @am__include@
+am__leading_dot = @am__leading_dot@
+am__quote = @am__quote@
+am__tar = @am__tar@
+am__untar = @am__untar@
+bindir = @bindir@
+build = @build@
+build_alias = @build_alias@
+build_cpu = @build_cpu@
+build_os = @build_os@
+build_vendor = @build_vendor@
+builddir = @builddir@
+datadir = @datadir@
+datarootdir = @datarootdir@
+dict_drivers = @dict_drivers@
+docdir = @docdir@
+dvidir = @dvidir@
+exec_prefix = @exec_prefix@
+host = @host@
+host_alias = @host_alias@
+host_cpu = @host_cpu@
+host_os = @host_os@
+host_vendor = @host_vendor@
+htmldir = @htmldir@
+includedir = @includedir@
+infodir = @infodir@
+install_sh = @install_sh@
+libdir = @libdir@
+libexecdir = @libexecdir@
+localedir = @localedir@
+localstatedir = @localstatedir@
+mandir = @mandir@
+mkdir_p = @mkdir_p@
+moduledir = @moduledir@
+oldincludedir = @oldincludedir@
+pdfdir = @pdfdir@
+prefix = @prefix@
+program_transform_name = @program_transform_name@
+psdir = @psdir@
+rundir = @rundir@
+runstatedir = @runstatedir@
+sbindir = @sbindir@
+sharedstatedir = @sharedstatedir@
+sql_drivers = @sql_drivers@
+srcdir = @srcdir@
+ssldir = @ssldir@
+statedir = @statedir@
+sysconfdir = @sysconfdir@
+systemdservicetype = @systemdservicetype@
+systemdsystemunitdir = @systemdsystemunitdir@
+target_alias = @target_alias@
+top_build_prefix = @top_build_prefix@
+top_builddir = @top_builddir@
+top_srcdir = @top_srcdir@
+noinst_LTLIBRARIES = libstorage_maildir.la
+AM_CPPFLAGS = \
+ -I$(top_srcdir)/src/lib \
+ -I$(top_srcdir)/src/lib-settings \
+ -I$(top_srcdir)/src/lib-mail \
+ -I$(top_srcdir)/src/lib-imap \
+ -I$(top_srcdir)/src/lib-index \
+ -I$(top_srcdir)/src/lib-storage \
+ -I$(top_srcdir)/src/lib-storage/index
+
+libstorage_maildir_la_SOURCES = \
+ maildir-copy.c \
+ maildir-filename.c \
+ maildir-filename-flags.c \
+ maildir-keywords.c \
+ maildir-mail.c \
+ maildir-save.c \
+ maildir-settings.c \
+ maildir-storage.c \
+ maildir-sync.c \
+ maildir-sync-index.c \
+ maildir-uidlist.c \
+ maildir-util.c
+
+headers = \
+ maildir-filename.h \
+ maildir-filename-flags.h \
+ maildir-keywords.h \
+ maildir-storage.h \
+ maildir-settings.h \
+ maildir-sync.h \
+ maildir-uidlist.h
+
+pkginc_libdir = $(pkgincludedir)
+pkginc_lib_HEADERS = $(headers)
+all: all-am
+
+.SUFFIXES:
+.SUFFIXES: .c .lo .o .obj
+$(srcdir)/Makefile.in: @MAINTAINER_MODE_TRUE@ $(srcdir)/Makefile.am $(am__configure_deps)
+ @for dep in $?; do \
+ case '$(am__configure_deps)' in \
+ *$$dep*) \
+ ( cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh ) \
+ && { if test -f $@; then exit 0; else break; fi; }; \
+ exit 1;; \
+ esac; \
+ done; \
+ echo ' cd $(top_srcdir) && $(AUTOMAKE) --foreign src/lib-storage/index/maildir/Makefile'; \
+ $(am__cd) $(top_srcdir) && \
+ $(AUTOMAKE) --foreign src/lib-storage/index/maildir/Makefile
+Makefile: $(srcdir)/Makefile.in $(top_builddir)/config.status
+ @case '$?' in \
+ *config.status*) \
+ cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh;; \
+ *) \
+ echo ' cd $(top_builddir) && $(SHELL) ./config.status $(subdir)/$@ $(am__maybe_remake_depfiles)'; \
+ cd $(top_builddir) && $(SHELL) ./config.status $(subdir)/$@ $(am__maybe_remake_depfiles);; \
+ esac;
+
+$(top_builddir)/config.status: $(top_srcdir)/configure $(CONFIG_STATUS_DEPENDENCIES)
+ cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh
+
+$(top_srcdir)/configure: @MAINTAINER_MODE_TRUE@ $(am__configure_deps)
+ cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh
+$(ACLOCAL_M4): @MAINTAINER_MODE_TRUE@ $(am__aclocal_m4_deps)
+ cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh
+$(am__aclocal_m4_deps):
+
+clean-noinstLTLIBRARIES:
+ -test -z "$(noinst_LTLIBRARIES)" || rm -f $(noinst_LTLIBRARIES)
+ @list='$(noinst_LTLIBRARIES)'; \
+ locs=`for p in $$list; do echo $$p; done | \
+ sed 's|^[^/]*$$|.|; s|/[^/]*$$||; s|$$|/so_locations|' | \
+ sort -u`; \
+ test -z "$$locs" || { \
+ echo rm -f $${locs}; \
+ rm -f $${locs}; \
+ }
+
+libstorage_maildir.la: $(libstorage_maildir_la_OBJECTS) $(libstorage_maildir_la_DEPENDENCIES) $(EXTRA_libstorage_maildir_la_DEPENDENCIES)
+ $(AM_V_CCLD)$(LINK) $(libstorage_maildir_la_OBJECTS) $(libstorage_maildir_la_LIBADD) $(LIBS)
+
+mostlyclean-compile:
+ -rm -f *.$(OBJEXT)
+
+distclean-compile:
+ -rm -f *.tab.c
+
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/maildir-copy.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/maildir-filename-flags.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/maildir-filename.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/maildir-keywords.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/maildir-mail.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/maildir-save.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/maildir-settings.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/maildir-storage.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/maildir-sync-index.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/maildir-sync.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/maildir-uidlist.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/maildir-util.Plo@am__quote@ # am--include-marker
+
+$(am__depfiles_remade):
+ @$(MKDIR_P) $(@D)
+ @echo '# dummy' >$@-t && $(am__mv) $@-t $@
+
+am--depfiles: $(am__depfiles_remade)
+
+.c.o:
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(COMPILE) -MT $@ -MD -MP -MF $(DEPDIR)/$*.Tpo -c -o $@ $<
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/$*.Tpo $(DEPDIR)/$*.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='$<' object='$@' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(COMPILE) -c -o $@ $<
+
+.c.obj:
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(COMPILE) -MT $@ -MD -MP -MF $(DEPDIR)/$*.Tpo -c -o $@ `$(CYGPATH_W) '$<'`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/$*.Tpo $(DEPDIR)/$*.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='$<' object='$@' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(COMPILE) -c -o $@ `$(CYGPATH_W) '$<'`
+
+.c.lo:
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(LTCOMPILE) -MT $@ -MD -MP -MF $(DEPDIR)/$*.Tpo -c -o $@ $<
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/$*.Tpo $(DEPDIR)/$*.Plo
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='$<' object='$@' libtool=yes @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(LTCOMPILE) -c -o $@ $<
+
+mostlyclean-libtool:
+ -rm -f *.lo
+
+clean-libtool:
+ -rm -rf .libs _libs
+install-pkginc_libHEADERS: $(pkginc_lib_HEADERS)
+ @$(NORMAL_INSTALL)
+ @list='$(pkginc_lib_HEADERS)'; test -n "$(pkginc_libdir)" || list=; \
+ if test -n "$$list"; then \
+ echo " $(MKDIR_P) '$(DESTDIR)$(pkginc_libdir)'"; \
+ $(MKDIR_P) "$(DESTDIR)$(pkginc_libdir)" || exit 1; \
+ fi; \
+ for p in $$list; do \
+ if test -f "$$p"; then d=; else d="$(srcdir)/"; fi; \
+ echo "$$d$$p"; \
+ done | $(am__base_list) | \
+ while read files; do \
+ echo " $(INSTALL_HEADER) $$files '$(DESTDIR)$(pkginc_libdir)'"; \
+ $(INSTALL_HEADER) $$files "$(DESTDIR)$(pkginc_libdir)" || exit $$?; \
+ done
+
+uninstall-pkginc_libHEADERS:
+ @$(NORMAL_UNINSTALL)
+ @list='$(pkginc_lib_HEADERS)'; test -n "$(pkginc_libdir)" || list=; \
+ files=`for p in $$list; do echo $$p; done | sed -e 's|^.*/||'`; \
+ dir='$(DESTDIR)$(pkginc_libdir)'; $(am__uninstall_files_from_dir)
+
+ID: $(am__tagged_files)
+ $(am__define_uniq_tagged_files); mkid -fID $$unique
+tags: tags-am
+TAGS: tags
+
+tags-am: $(TAGS_DEPENDENCIES) $(am__tagged_files)
+ set x; \
+ here=`pwd`; \
+ $(am__define_uniq_tagged_files); \
+ shift; \
+ if test -z "$(ETAGS_ARGS)$$*$$unique"; then :; else \
+ test -n "$$unique" || unique=$$empty_fix; \
+ if test $$# -gt 0; then \
+ $(ETAGS) $(ETAGSFLAGS) $(AM_ETAGSFLAGS) $(ETAGS_ARGS) \
+ "$$@" $$unique; \
+ else \
+ $(ETAGS) $(ETAGSFLAGS) $(AM_ETAGSFLAGS) $(ETAGS_ARGS) \
+ $$unique; \
+ fi; \
+ fi
+ctags: ctags-am
+
+CTAGS: ctags
+ctags-am: $(TAGS_DEPENDENCIES) $(am__tagged_files)
+ $(am__define_uniq_tagged_files); \
+ test -z "$(CTAGS_ARGS)$$unique" \
+ || $(CTAGS) $(CTAGSFLAGS) $(AM_CTAGSFLAGS) $(CTAGS_ARGS) \
+ $$unique
+
+GTAGS:
+ here=`$(am__cd) $(top_builddir) && pwd` \
+ && $(am__cd) $(top_srcdir) \
+ && gtags -i $(GTAGS_ARGS) "$$here"
+cscopelist: cscopelist-am
+
+cscopelist-am: $(am__tagged_files)
+ list='$(am__tagged_files)'; \
+ case "$(srcdir)" in \
+ [\\/]* | ?:[\\/]*) sdir="$(srcdir)" ;; \
+ *) sdir=$(subdir)/$(srcdir) ;; \
+ esac; \
+ for i in $$list; do \
+ if test -f "$$i"; then \
+ echo "$(subdir)/$$i"; \
+ else \
+ echo "$$sdir/$$i"; \
+ fi; \
+ done >> $(top_builddir)/cscope.files
+
+distclean-tags:
+ -rm -f TAGS ID GTAGS GRTAGS GSYMS GPATH tags
+
+distdir: $(BUILT_SOURCES)
+ $(MAKE) $(AM_MAKEFLAGS) distdir-am
+
+distdir-am: $(DISTFILES)
+ @srcdirstrip=`echo "$(srcdir)" | sed 's/[].[^$$\\*]/\\\\&/g'`; \
+ topsrcdirstrip=`echo "$(top_srcdir)" | sed 's/[].[^$$\\*]/\\\\&/g'`; \
+ list='$(DISTFILES)'; \
+ dist_files=`for file in $$list; do echo $$file; done | \
+ sed -e "s|^$$srcdirstrip/||;t" \
+ -e "s|^$$topsrcdirstrip/|$(top_builddir)/|;t"`; \
+ case $$dist_files in \
+ */*) $(MKDIR_P) `echo "$$dist_files" | \
+ sed '/\//!d;s|^|$(distdir)/|;s,/[^/]*$$,,' | \
+ sort -u` ;; \
+ esac; \
+ for file in $$dist_files; do \
+ if test -f $$file || test -d $$file; then d=.; else d=$(srcdir); fi; \
+ if test -d $$d/$$file; then \
+ dir=`echo "/$$file" | sed -e 's,/[^/]*$$,,'`; \
+ if test -d "$(distdir)/$$file"; then \
+ find "$(distdir)/$$file" -type d ! -perm -700 -exec chmod u+rwx {} \;; \
+ fi; \
+ if test -d $(srcdir)/$$file && test $$d != $(srcdir); then \
+ cp -fpR $(srcdir)/$$file "$(distdir)$$dir" || exit 1; \
+ find "$(distdir)/$$file" -type d ! -perm -700 -exec chmod u+rwx {} \;; \
+ fi; \
+ cp -fpR $$d/$$file "$(distdir)$$dir" || exit 1; \
+ else \
+ test -f "$(distdir)/$$file" \
+ || cp -p $$d/$$file "$(distdir)/$$file" \
+ || exit 1; \
+ fi; \
+ done
+check-am: all-am
+check: check-am
+all-am: Makefile $(LTLIBRARIES) $(HEADERS)
+installdirs:
+ for dir in "$(DESTDIR)$(pkginc_libdir)"; do \
+ test -z "$$dir" || $(MKDIR_P) "$$dir"; \
+ done
+install: install-am
+install-exec: install-exec-am
+install-data: install-data-am
+uninstall: uninstall-am
+
+install-am: all-am
+ @$(MAKE) $(AM_MAKEFLAGS) install-exec-am install-data-am
+
+installcheck: installcheck-am
+install-strip:
+ if test -z '$(STRIP)'; then \
+ $(MAKE) $(AM_MAKEFLAGS) INSTALL_PROGRAM="$(INSTALL_STRIP_PROGRAM)" \
+ install_sh_PROGRAM="$(INSTALL_STRIP_PROGRAM)" INSTALL_STRIP_FLAG=-s \
+ install; \
+ else \
+ $(MAKE) $(AM_MAKEFLAGS) INSTALL_PROGRAM="$(INSTALL_STRIP_PROGRAM)" \
+ install_sh_PROGRAM="$(INSTALL_STRIP_PROGRAM)" INSTALL_STRIP_FLAG=-s \
+ "INSTALL_PROGRAM_ENV=STRIPPROG='$(STRIP)'" install; \
+ fi
+mostlyclean-generic:
+
+clean-generic:
+
+distclean-generic:
+ -test -z "$(CONFIG_CLEAN_FILES)" || rm -f $(CONFIG_CLEAN_FILES)
+ -test . = "$(srcdir)" || test -z "$(CONFIG_CLEAN_VPATH_FILES)" || rm -f $(CONFIG_CLEAN_VPATH_FILES)
+
+maintainer-clean-generic:
+ @echo "This command is intended for maintainers to use"
+ @echo "it deletes files that may require special tools to rebuild."
+clean: clean-am
+
+clean-am: clean-generic clean-libtool clean-noinstLTLIBRARIES \
+ mostlyclean-am
+
+distclean: distclean-am
+ -rm -f ./$(DEPDIR)/maildir-copy.Plo
+ -rm -f ./$(DEPDIR)/maildir-filename-flags.Plo
+ -rm -f ./$(DEPDIR)/maildir-filename.Plo
+ -rm -f ./$(DEPDIR)/maildir-keywords.Plo
+ -rm -f ./$(DEPDIR)/maildir-mail.Plo
+ -rm -f ./$(DEPDIR)/maildir-save.Plo
+ -rm -f ./$(DEPDIR)/maildir-settings.Plo
+ -rm -f ./$(DEPDIR)/maildir-storage.Plo
+ -rm -f ./$(DEPDIR)/maildir-sync-index.Plo
+ -rm -f ./$(DEPDIR)/maildir-sync.Plo
+ -rm -f ./$(DEPDIR)/maildir-uidlist.Plo
+ -rm -f ./$(DEPDIR)/maildir-util.Plo
+ -rm -f Makefile
+distclean-am: clean-am distclean-compile distclean-generic \
+ distclean-tags
+
+dvi: dvi-am
+
+dvi-am:
+
+html: html-am
+
+html-am:
+
+info: info-am
+
+info-am:
+
+install-data-am: install-pkginc_libHEADERS
+
+install-dvi: install-dvi-am
+
+install-dvi-am:
+
+install-exec-am:
+
+install-html: install-html-am
+
+install-html-am:
+
+install-info: install-info-am
+
+install-info-am:
+
+install-man:
+
+install-pdf: install-pdf-am
+
+install-pdf-am:
+
+install-ps: install-ps-am
+
+install-ps-am:
+
+installcheck-am:
+
+maintainer-clean: maintainer-clean-am
+ -rm -f ./$(DEPDIR)/maildir-copy.Plo
+ -rm -f ./$(DEPDIR)/maildir-filename-flags.Plo
+ -rm -f ./$(DEPDIR)/maildir-filename.Plo
+ -rm -f ./$(DEPDIR)/maildir-keywords.Plo
+ -rm -f ./$(DEPDIR)/maildir-mail.Plo
+ -rm -f ./$(DEPDIR)/maildir-save.Plo
+ -rm -f ./$(DEPDIR)/maildir-settings.Plo
+ -rm -f ./$(DEPDIR)/maildir-storage.Plo
+ -rm -f ./$(DEPDIR)/maildir-sync-index.Plo
+ -rm -f ./$(DEPDIR)/maildir-sync.Plo
+ -rm -f ./$(DEPDIR)/maildir-uidlist.Plo
+ -rm -f ./$(DEPDIR)/maildir-util.Plo
+ -rm -f Makefile
+maintainer-clean-am: distclean-am maintainer-clean-generic
+
+mostlyclean: mostlyclean-am
+
+mostlyclean-am: mostlyclean-compile mostlyclean-generic \
+ mostlyclean-libtool
+
+pdf: pdf-am
+
+pdf-am:
+
+ps: ps-am
+
+ps-am:
+
+uninstall-am: uninstall-pkginc_libHEADERS
+
+.MAKE: install-am install-strip
+
+.PHONY: CTAGS GTAGS TAGS all all-am am--depfiles check check-am clean \
+ clean-generic clean-libtool clean-noinstLTLIBRARIES \
+ cscopelist-am ctags ctags-am distclean distclean-compile \
+ distclean-generic distclean-libtool distclean-tags distdir dvi \
+ dvi-am html html-am info info-am install install-am \
+ install-data install-data-am install-dvi install-dvi-am \
+ install-exec install-exec-am install-html install-html-am \
+ install-info install-info-am install-man install-pdf \
+ install-pdf-am install-pkginc_libHEADERS install-ps \
+ install-ps-am install-strip installcheck installcheck-am \
+ installdirs maintainer-clean maintainer-clean-generic \
+ mostlyclean mostlyclean-compile mostlyclean-generic \
+ mostlyclean-libtool pdf pdf-am ps ps-am tags tags-am uninstall \
+ uninstall-am uninstall-pkginc_libHEADERS
+
+.PRECIOUS: Makefile
+
+
+# Tell versions [3.59,3.63) of GNU make to not export all variables.
+# Otherwise a system limit (for SysV at least) may be exceeded.
+.NOEXPORT:
diff --git a/src/lib-storage/index/maildir/maildir-copy.c b/src/lib-storage/index/maildir/maildir-copy.c
new file mode 100644
index 0000000..1645ddb
--- /dev/null
+++ b/src/lib-storage/index/maildir/maildir-copy.c
@@ -0,0 +1,149 @@
+/* Copyright (c) 2002-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "ioloop.h"
+#include "nfs-workarounds.h"
+#include "maildir-storage.h"
+#include "maildir-uidlist.h"
+#include "maildir-filename.h"
+#include "maildir-keywords.h"
+#include "maildir-sync.h"
+#include "index-mail.h"
+#include "mail-copy.h"
+
+#include <unistd.h>
+#include <sys/stat.h>
+
+struct hardlink_ctx {
+ const char *dest_path;
+ bool success:1;
+};
+
+static int do_hardlink(struct maildir_mailbox *mbox, const char *path,
+ struct hardlink_ctx *ctx)
+{
+ int ret;
+
+ if (mbox->storage->storage.set->mail_nfs_storage)
+ ret = nfs_safe_link(path, ctx->dest_path, FALSE);
+ else
+ ret = link(path, ctx->dest_path);
+ if (ret < 0) {
+ if (errno == ENOENT)
+ return 0;
+
+ if (ENOQUOTA(errno)) {
+ mail_storage_set_error(&mbox->storage->storage,
+ MAIL_ERROR_NOQUOTA, MAIL_ERRSTR_NO_QUOTA);
+ return -1;
+ }
+
+ /* we could handle the EEXIST condition by changing the
+ filename, but it practically never happens so just fallback
+ to standard copying for the rare cases when it does. */
+ if (errno == EACCES || ECANTLINK(errno) || errno == EEXIST)
+ return 1;
+
+ mailbox_set_critical(&mbox->box, "link(%s, %s) failed: %m",
+ path, ctx->dest_path);
+ return -1;
+ }
+
+ ctx->success = TRUE;
+ return 1;
+}
+
+static int
+maildir_copy_hardlink(struct mail_save_context *ctx, struct mail *mail)
+{
+ struct maildir_mailbox *dest_mbox = MAILDIR_MAILBOX(ctx->transaction->box);
+ struct maildir_mailbox *src_mbox;
+ struct maildir_filename *mf;
+ struct hardlink_ctx do_ctx;
+ const char *path, *guid, *dest_fname;
+ uoff_t vsize, size;
+ enum mail_lookup_abort old_abort;
+
+ if (strcmp(mail->box->storage->name, MAILDIR_STORAGE_NAME) == 0)
+ src_mbox = MAILDIR_MAILBOX(mail->box);
+ else if (strcmp(mail->box->storage->name, "raw") == 0) {
+ /* lda uses raw format */
+ src_mbox = NULL;
+ } else {
+ /* Can't hard link files from the source storage */
+ return 0;
+ }
+
+ /* hard link to tmp/ with a newly generated filename and later when we
+ have uidlist locked, move it to new/cur. */
+ dest_fname = maildir_filename_generate();
+ i_zero(&do_ctx);
+ do_ctx.dest_path =
+ t_strdup_printf("%s/tmp/%s", mailbox_get_path(&dest_mbox->box),
+ dest_fname);
+ if (src_mbox != NULL) {
+ /* maildir */
+ if (maildir_file_do(src_mbox, mail->uid,
+ do_hardlink, &do_ctx) < 0)
+ return -1;
+ } else {
+ /* raw / lda */
+ if (mail_get_special(mail, MAIL_FETCH_STORAGE_ID,
+ &path) < 0 || *path == '\0')
+ return 0;
+ if (do_hardlink(dest_mbox, path, &do_ctx) < 0)
+ return -1;
+ }
+
+ if (!do_ctx.success) {
+ /* couldn't copy with hardlinking, fallback to copying */
+ return 0;
+ }
+
+ /* hardlinked to tmp/, treat as normal copied mail */
+ mf = maildir_save_add(ctx, dest_fname, mail);
+ if (mail_get_special(mail, MAIL_FETCH_GUID, &guid) == 0) {
+ if (*guid != '\0')
+ maildir_save_set_dest_basename(ctx, mf, guid);
+ }
+
+ /* finish copying keywords */
+ maildir_save_finish_keywords(ctx);
+
+ /* remember size/vsize if possible */
+ old_abort = mail->lookup_abort;
+ mail->lookup_abort = MAIL_LOOKUP_ABORT_READ_MAIL;
+ if (mail_get_physical_size(mail, &size) < 0)
+ size = UOFF_T_MAX;
+ if (mail_get_virtual_size(mail, &vsize) < 0)
+ vsize = UOFF_T_MAX;
+ maildir_save_set_sizes(mf, size, vsize);
+ mail->lookup_abort = old_abort;
+ return 1;
+}
+
+int maildir_copy(struct mail_save_context *ctx, struct mail *mail)
+{
+ struct mailbox_transaction_context *_t = ctx->transaction;
+ struct maildir_mailbox *mbox = MAILDIR_MAILBOX(_t->box);
+ int ret;
+
+ i_assert((_t->flags & MAILBOX_TRANSACTION_FLAG_EXTERNAL) != 0);
+
+ if (mbox->storage->set->maildir_copy_with_hardlinks &&
+ mail_storage_copy_can_use_hardlink(mail->box, &mbox->box)) {
+ T_BEGIN {
+ ret = maildir_copy_hardlink(ctx, mail);
+ } T_END;
+
+ if (ret != 0) {
+ index_save_context_free(ctx);
+ return ret > 0 ? 0 : -1;
+ }
+
+ /* non-fatal hardlinking failure, try the slow way */
+ }
+
+ return mail_storage_copy(ctx, mail);
+}
diff --git a/src/lib-storage/index/maildir/maildir-filename-flags.c b/src/lib-storage/index/maildir/maildir-filename-flags.c
new file mode 100644
index 0000000..ac68908
--- /dev/null
+++ b/src/lib-storage/index/maildir/maildir-filename-flags.c
@@ -0,0 +1,185 @@
+/* Copyright (c) 2002-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "str.h"
+#include "maildir-storage.h"
+#include "maildir-keywords.h"
+#include "maildir-filename-flags.h"
+
+
+void maildir_filename_flags_get(struct maildir_keywords_sync_ctx *ctx,
+ const char *fname, enum mail_flags *flags_r,
+ ARRAY_TYPE(keyword_indexes) *keywords_r)
+{
+ const char *info;
+
+ array_clear(keywords_r);
+ *flags_r = 0;
+
+ info = strrchr(fname, MAILDIR_INFO_SEP);
+ if (info == NULL || info[1] != '2' || info[2] != MAILDIR_FLAGS_SEP)
+ return;
+
+ for (info += 3; *info != '\0' && *info != MAILDIR_FLAGS_SEP; info++) {
+ switch (*info) {
+ case 'R': /* replied */
+ *flags_r |= MAIL_ANSWERED;
+ break;
+ case 'S': /* seen */
+ *flags_r |= MAIL_SEEN;
+ break;
+ case 'T': /* trashed */
+ *flags_r |= MAIL_DELETED;
+ break;
+ case 'D': /* draft */
+ *flags_r |= MAIL_DRAFT;
+ break;
+ case 'F': /* flagged */
+ *flags_r |= MAIL_FLAGGED;
+ break;
+ default:
+ if (*info >= MAILDIR_KEYWORD_FIRST &&
+ *info <= MAILDIR_KEYWORD_LAST) {
+ int idx;
+
+ idx = maildir_keywords_char_idx(ctx, *info);
+ if (idx < 0) {
+ /* unknown keyword. */
+ break;
+ }
+
+ array_push_back(keywords_r,
+ (unsigned int *)&idx);
+ break;
+ }
+
+ /* unknown flag - ignore */
+ break;
+ }
+ }
+}
+
+static int char_cmp(const void *p1, const void *p2)
+{
+ const unsigned char *c1 = p1, *c2 = p2;
+
+ return *c1 - *c2;
+}
+
+static void
+maildir_filename_append_keywords(struct maildir_keywords_sync_ctx *ctx,
+ ARRAY_TYPE(keyword_indexes) *keywords,
+ string_t *fname)
+{
+ const unsigned int *indexes;
+ unsigned int i, count;
+ size_t start = str_len(fname);
+ char chr;
+
+ indexes = array_get(keywords, &count);
+ for (i = 0; i < count; i++) {
+ chr = maildir_keywords_idx_char(ctx, indexes[i]);
+ if (chr != '\0')
+ str_append_c(fname, chr);
+ }
+
+ qsort(str_c_modifiable(fname) + start, str_len(fname) - start, 1,
+ char_cmp);
+}
+
+static const char * ATTR_NULL(1, 4)
+maildir_filename_flags_full_set(struct maildir_keywords_sync_ctx *ctx,
+ const char *fname, enum mail_flags flags,
+ ARRAY_TYPE(keyword_indexes) *keywords)
+{
+ string_t *flags_str;
+ enum mail_flags flags_left;
+ const char *info, *oldflags;
+ int nextflag;
+
+ /* remove the old :info from file name, and get the old flags */
+ info = strrchr(fname, MAILDIR_INFO_SEP);
+ if (info != NULL && strrchr(fname, '/') > info)
+ info = NULL;
+
+ oldflags = "";
+ if (info != NULL) {
+ fname = t_strdup_until(fname, info);
+ if (info[1] == '2' && info[2] == MAILDIR_FLAGS_SEP)
+ oldflags = info+3;
+ }
+
+ /* insert the new flags between old flags. flags must be sorted by
+ their ASCII code. unknown flags are kept. */
+ flags_str = t_str_new(256);
+ str_append(flags_str, fname);
+ str_append(flags_str, MAILDIR_FLAGS_FULL_SEP);
+ flags_left = flags;
+ for (;;) {
+ /* skip all known flags */
+ while (*oldflags == 'D' || *oldflags == 'F' ||
+ *oldflags == 'R' || *oldflags == 'S' ||
+ *oldflags == 'T' ||
+ (*oldflags >= MAILDIR_KEYWORD_FIRST &&
+ *oldflags <= MAILDIR_KEYWORD_LAST))
+ oldflags++;
+
+ nextflag = *oldflags == '\0' || *oldflags == MAILDIR_FLAGS_SEP ?
+ 256 : (unsigned char) *oldflags;
+
+ if ((flags_left & MAIL_DRAFT) != 0 && nextflag > 'D') {
+ str_append_c(flags_str, 'D');
+ flags_left &= ENUM_NEGATE(MAIL_DRAFT);
+ }
+ if ((flags_left & MAIL_FLAGGED) != 0 && nextflag > 'F') {
+ str_append_c(flags_str, 'F');
+ flags_left &= ENUM_NEGATE(MAIL_FLAGGED);
+ }
+ if ((flags_left & MAIL_ANSWERED) != 0 && nextflag > 'R') {
+ str_append_c(flags_str, 'R');
+ flags_left &= ENUM_NEGATE(MAIL_ANSWERED);
+ }
+ if ((flags_left & MAIL_SEEN) != 0 && nextflag > 'S') {
+ str_append_c(flags_str, 'S');
+ flags_left &= ENUM_NEGATE(MAIL_SEEN);
+ }
+ if ((flags_left & MAIL_DELETED) != 0 && nextflag > 'T') {
+ str_append_c(flags_str, 'T');
+ flags_left &= ENUM_NEGATE(MAIL_DELETED);
+ }
+
+ if (keywords != NULL && array_is_created(keywords) &&
+ nextflag > MAILDIR_KEYWORD_FIRST) {
+ maildir_filename_append_keywords(ctx, keywords,
+ flags_str);
+ keywords = NULL;
+ }
+
+ if (*oldflags == '\0' || *oldflags == MAILDIR_FLAGS_SEP)
+ break;
+
+ str_append_c(flags_str, *oldflags);
+ oldflags++;
+ }
+
+ if (*oldflags == MAILDIR_FLAGS_SEP) {
+ /* another flagset, we don't know about these, just keep them */
+ while (*oldflags != '\0')
+ str_append_c(flags_str, *oldflags++);
+ }
+
+ return str_c(flags_str);
+}
+
+const char *maildir_filename_flags_set(const char *fname, enum mail_flags flags)
+{
+ return maildir_filename_flags_full_set(NULL, fname, flags, NULL);
+}
+
+const char *maildir_filename_flags_kw_set(struct maildir_keywords_sync_ctx *ctx,
+ const char *fname, enum mail_flags flags,
+ ARRAY_TYPE(keyword_indexes) *keywords)
+{
+ return maildir_filename_flags_full_set(ctx, fname, flags, keywords);
+}
diff --git a/src/lib-storage/index/maildir/maildir-filename-flags.h b/src/lib-storage/index/maildir/maildir-filename-flags.h
new file mode 100644
index 0000000..2676e5c
--- /dev/null
+++ b/src/lib-storage/index/maildir/maildir-filename-flags.h
@@ -0,0 +1,13 @@
+#ifndef MAILDIR_FILENAME_FLAGS_H
+#define MAILDIR_FILENAME_FLAGS_H
+
+void maildir_filename_flags_get(struct maildir_keywords_sync_ctx *ctx,
+ const char *fname, enum mail_flags *flags_r,
+ ARRAY_TYPE(keyword_indexes) *keywords_r);
+
+const char *maildir_filename_flags_set(const char *fname, enum mail_flags flags);
+const char *maildir_filename_flags_kw_set(struct maildir_keywords_sync_ctx *ctx,
+ const char *fname, enum mail_flags flags,
+ ARRAY_TYPE(keyword_indexes) *keywords);
+
+#endif
diff --git a/src/lib-storage/index/maildir/maildir-filename.c b/src/lib-storage/index/maildir/maildir-filename.c
new file mode 100644
index 0000000..9deba82
--- /dev/null
+++ b/src/lib-storage/index/maildir/maildir-filename.c
@@ -0,0 +1,143 @@
+/* Copyright (c) 2002-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "ioloop.h"
+#include "time-util.h"
+#include "hostpid.h"
+#include "maildir-storage.h"
+#include "maildir-filename.h"
+
+const char *maildir_filename_generate(void)
+{
+ static struct timeval last_tv = { 0, 0 };
+ struct timeval tv;
+
+ /* use secs + usecs to guarantee uniqueness within this process. */
+ if (timeval_cmp(&ioloop_timeval, &last_tv) > 0)
+ tv = ioloop_timeval;
+ else {
+ tv = last_tv;
+ if (++tv.tv_usec == 1000000) {
+ tv.tv_sec++;
+ tv.tv_usec = 0;
+ }
+ }
+ last_tv = tv;
+
+ return t_strdup_printf("%s.M%sP%s.%s",
+ dec2str(tv.tv_sec), dec2str(tv.tv_usec),
+ my_pid, my_hostname);
+}
+
+bool maildir_filename_get_size(const char *fname, char type, uoff_t *size_r)
+{
+ uoff_t size = 0;
+
+ for (; *fname != '\0'; fname++) {
+ i_assert(*fname != '/');
+ if (*fname == ',' && fname[1] == type && fname[2] == '=') {
+ fname += 3;
+ break;
+ }
+ }
+
+ if (*fname == '\0')
+ return FALSE;
+
+ while (*fname >= '0' && *fname <= '9') {
+ size = size * 10 + (*fname - '0');
+ fname++;
+ }
+
+ if (*fname != MAILDIR_INFO_SEP &&
+ *fname != MAILDIR_EXTRA_SEP &&
+ *fname != '\0')
+ return FALSE;
+
+ *size_r = size;
+ return TRUE;
+}
+
+/* a char* hash function from ASU -- from glib */
+unsigned int ATTR_NO_SANITIZE_INTEGER
+maildir_filename_base_hash(const char *s)
+{
+ unsigned int g, h = 0;
+
+ while (*s != MAILDIR_INFO_SEP && *s != '\0') {
+ i_assert(*s != '/');
+ h = (h << 4) + *s;
+ if ((g = h & 0xf0000000UL) != 0) {
+ h = h ^ (g >> 24);
+ h = h ^ g;
+ }
+
+ s++;
+ }
+
+ return h;
+}
+
+int maildir_filename_base_cmp(const char *fname1, const char *fname2)
+{
+ while (*fname1 == *fname2 && *fname1 != MAILDIR_INFO_SEP &&
+ *fname1 != '\0') {
+ i_assert(*fname1 != '/');
+ fname1++; fname2++;
+ }
+
+ if ((*fname1 == '\0' || *fname1 == MAILDIR_INFO_SEP) &&
+ (*fname2 == '\0' || *fname2 == MAILDIR_INFO_SEP))
+ return 0;
+ return *fname1 - *fname2;
+}
+
+static bool maildir_fname_get_usecs(const char *fname, int *usecs_r)
+{
+ int usecs = 0;
+
+ /* Assume we already read the timestamp. Next up is
+ ".<uniqueness>.<host>". Find usecs inside the uniqueness. */
+ if (*fname != '.')
+ return FALSE;
+
+ fname++;
+ while (*fname != '\0' && *fname != '.' && *fname != MAILDIR_INFO_SEP) {
+ if (*fname++ == 'M') {
+ while (*fname >= '0' && *fname <= '9') {
+ usecs = usecs * 10 + (*fname - '0');
+ fname++;
+ }
+ *usecs_r = usecs;
+ return TRUE;
+ }
+ }
+ return FALSE;
+}
+
+int maildir_filename_sort_cmp(const char *fname1, const char *fname2)
+{
+ const char *s1, *s2;
+ time_t secs1 = 0, secs2 = 0;
+ int ret, usecs1, usecs2;
+
+ /* sort primarily by the timestamp in file name */
+ for (s1 = fname1; *s1 >= '0' && *s1 <= '9'; s1++)
+ secs1 = secs1 * 10 + (*s1 - '0');
+ for (s2 = fname2; *s2 >= '0' && *s2 <= '9'; s2++)
+ secs2 = secs2 * 10 + (*s2 - '0');
+
+ ret = (int)((long)secs1 - (long)secs2);
+ if (ret == 0) {
+ /* sort secondarily by microseconds, if they exist */
+ if (maildir_fname_get_usecs(s1, &usecs1) &&
+ maildir_fname_get_usecs(s2, &usecs2))
+ ret = usecs1 - usecs2;
+
+ if (ret == 0) {
+ /* fallback to comparing the base file name */
+ ret = maildir_filename_base_cmp(s1, s2);
+ }
+ }
+ return ret;
+}
diff --git a/src/lib-storage/index/maildir/maildir-filename.h b/src/lib-storage/index/maildir/maildir-filename.h
new file mode 100644
index 0000000..a691dea
--- /dev/null
+++ b/src/lib-storage/index/maildir/maildir-filename.h
@@ -0,0 +1,14 @@
+#ifndef MAILDIR_FILENAME_H
+#define MAILDIR_FILENAME_H
+
+struct maildir_keywords_sync_ctx;
+
+const char *maildir_filename_generate(void);
+
+bool maildir_filename_get_size(const char *fname, char type, uoff_t *size_r);
+
+unsigned int maildir_filename_base_hash(const char *fname);
+int maildir_filename_base_cmp(const char *fname1, const char *fname2);
+int maildir_filename_sort_cmp(const char *fname1, const char *fname2);
+
+#endif
diff --git a/src/lib-storage/index/maildir/maildir-keywords.c b/src/lib-storage/index/maildir/maildir-keywords.c
new file mode 100644
index 0000000..a25d112
--- /dev/null
+++ b/src/lib-storage/index/maildir/maildir-keywords.c
@@ -0,0 +1,499 @@
+/* Copyright (c) 2005-2018 Dovecot authors, see the included COPYING file */
+
+/* note that everything here depends on uidlist file being locked the whole
+ time. that's why we don't have any locking of our own, or that we do things
+ that would be racy otherwise. */
+
+#include "lib.h"
+#include "array.h"
+#include "ioloop.h"
+#include "hash.h"
+#include "str.h"
+#include "istream.h"
+#include "eacces-error.h"
+#include "file-dotlock.h"
+#include "write-full.h"
+#include "nfs-workarounds.h"
+#include "maildir-storage.h"
+#include "maildir-uidlist.h"
+#include "maildir-keywords.h"
+
+#include <sys/stat.h>
+#include <utime.h>
+
+/* how many seconds to wait before overriding dovecot-keywords.lock */
+#define KEYWORDS_LOCK_STALE_TIMEOUT (60*2)
+
+struct maildir_keywords {
+ struct maildir_mailbox *mbox;
+ struct mail_storage *storage;
+ char *path;
+
+ pool_t pool;
+ ARRAY_TYPE(keywords) list;
+ HASH_TABLE(char *, void *) hash; /* name -> idx+1 */
+
+ struct dotlock_settings dotlock_settings;
+
+ time_t synced_mtime;
+ bool synced:1;
+ bool changed:1;
+};
+
+struct maildir_keywords_sync_ctx {
+ struct maildir_keywords *mk;
+ struct mail_index *index;
+
+ const ARRAY_TYPE(keywords) *keywords;
+ ARRAY(char) idx_to_chr;
+ unsigned int chridx_to_idx[MAILDIR_MAX_KEYWORDS];
+ bool readonly;
+};
+
+struct maildir_keywords *maildir_keywords_init(struct maildir_mailbox *mbox)
+{
+ struct maildir_keywords *mk;
+
+ mk = maildir_keywords_init_readonly(&mbox->box);
+ mk->mbox = mbox;
+ return mk;
+}
+
+struct maildir_keywords *
+maildir_keywords_init_readonly(struct mailbox *box)
+{
+ struct maildir_keywords *mk;
+ const char *dir;
+
+ if (mailbox_get_path_to(box, MAILBOX_LIST_PATH_TYPE_CONTROL, &dir) <= 0)
+ i_unreached();
+
+ mk = i_new(struct maildir_keywords, 1);
+ mk->storage = box->storage;
+ mk->path = i_strconcat(dir, "/" MAILDIR_KEYWORDS_NAME, NULL);
+ mk->pool = pool_alloconly_create("maildir keywords", 512);
+ i_array_init(&mk->list, MAILDIR_MAX_KEYWORDS);
+ hash_table_create(&mk->hash, mk->pool, 0, strcase_hash, strcasecmp);
+
+ mk->dotlock_settings.use_excl_lock =
+ box->storage->set->dotlock_use_excl;
+ mk->dotlock_settings.nfs_flush =
+ box->storage->set->mail_nfs_storage;
+ mk->dotlock_settings.timeout =
+ mail_storage_get_lock_timeout(box->storage,
+ KEYWORDS_LOCK_STALE_TIMEOUT + 2);
+ mk->dotlock_settings.stale_timeout = KEYWORDS_LOCK_STALE_TIMEOUT;
+ mk->dotlock_settings.temp_prefix =
+ mailbox_list_get_temp_prefix(box->list);
+ return mk;
+}
+
+void maildir_keywords_deinit(struct maildir_keywords **_mk)
+{
+ struct maildir_keywords *mk = *_mk;
+
+ *_mk = NULL;
+ hash_table_destroy(&mk->hash);
+ array_free(&mk->list);
+ pool_unref(&mk->pool);
+ i_free(mk->path);
+ i_free(mk);
+}
+
+static void maildir_keywords_clear(struct maildir_keywords *mk)
+{
+ array_clear(&mk->list);
+ hash_table_clear(mk->hash, TRUE);
+ p_clear(mk->pool);
+}
+
+static int maildir_keywords_sync(struct maildir_keywords *mk)
+{
+ struct istream *input;
+ struct stat st;
+ char *line, *p, *new_name;
+ const char **strp;
+ unsigned int idx;
+ int fd;
+
+ /* Remember that we rely on uidlist file locking in here. That's why
+ we rely on stat()'s timestamp and don't bother handling ESTALE
+ errors. */
+
+ if (mk->storage->set->mail_nfs_storage) {
+ /* file is updated only by replacing it, no need to flush
+ attribute cache */
+ nfs_flush_file_handle_cache(mk->path);
+ }
+
+ if (nfs_safe_stat(mk->path, &st) < 0) {
+ if (errno == ENOENT) {
+ maildir_keywords_clear(mk);
+ mk->synced = TRUE;
+ return 0;
+ }
+ mailbox_set_critical(&mk->mbox->box,
+ "stat(%s) failed: %m", mk->path);
+ return -1;
+ }
+
+ if (st.st_mtime == mk->synced_mtime) {
+ /* hasn't changed */
+ mk->synced = TRUE;
+ return 0;
+ }
+ mk->synced_mtime = st.st_mtime;
+
+ fd = open(mk->path, O_RDONLY);
+ if (fd == -1) {
+ if (errno == ENOENT) {
+ maildir_keywords_clear(mk);
+ mk->synced = TRUE;
+ return 0;
+ }
+ mailbox_set_critical(&mk->mbox->box,
+ "open(%s) failed: %m", mk->path);
+ return -1;
+ }
+
+ maildir_keywords_clear(mk);
+ input = i_stream_create_fd(fd, 1024);
+ while ((line = i_stream_read_next_line(input)) != NULL) {
+ p = strchr(line, ' ');
+ if (p == NULL) {
+ /* note that when converting .customflags file this
+ case happens in the first line. */
+ continue;
+ }
+ *p++ = '\0';
+
+ if (str_to_uint(line, &idx) < 0 ||
+ idx >= MAILDIR_MAX_KEYWORDS || *p == '\0' ||
+ hash_table_lookup(mk->hash, p) != NULL) {
+ /* shouldn't happen */
+ continue;
+ }
+
+ /* save it */
+ new_name = p_strdup(mk->pool, p);
+ hash_table_insert(mk->hash, new_name, POINTER_CAST(idx + 1));
+
+ strp = array_idx_get_space(&mk->list, idx);
+ *strp = new_name;
+ }
+ i_stream_destroy(&input);
+
+ if (close(fd) < 0) {
+ mailbox_set_critical(&mk->mbox->box,
+ "close(%s) failed: %m", mk->path);
+ return -1;
+ }
+
+ mk->synced = TRUE;
+ return 0;
+}
+
+static int
+maildir_keywords_lookup(struct maildir_keywords *mk, const char *name,
+ unsigned int *chridx_r)
+{
+ void *value;
+
+ value = hash_table_lookup(mk->hash, name);
+ if (value == NULL) {
+ if (mk->synced)
+ return 0;
+
+ if (maildir_keywords_sync(mk) < 0)
+ return -1;
+ i_assert(mk->synced);
+
+ value = hash_table_lookup(mk->hash, name);
+ if (value == NULL)
+ return 0;
+ }
+
+ *chridx_r = POINTER_CAST_TO(value, unsigned int)-1;
+ return 1;
+}
+
+static void
+maildir_keywords_create(struct maildir_keywords *mk, const char *name,
+ unsigned int chridx)
+{
+ const char **strp;
+ char *new_name;
+
+ i_assert(chridx < MAILDIR_MAX_KEYWORDS);
+
+ new_name = p_strdup(mk->pool, name);
+ hash_table_insert(mk->hash, new_name, POINTER_CAST(chridx + 1));
+
+ strp = array_idx_get_space(&mk->list, chridx);
+ *strp = new_name;
+
+ mk->changed = TRUE;
+}
+
+static int
+maildir_keywords_lookup_or_create(struct maildir_keywords *mk, const char *name,
+ unsigned int *chridx_r)
+{
+ const char *const *keywords;
+ unsigned int i, count;
+ int ret;
+
+ if ((ret = maildir_keywords_lookup(mk, name, chridx_r)) != 0)
+ return ret;
+
+ /* see if we are full */
+ keywords = array_get(&mk->list, &count);
+ for (i = 0; i < count; i++) {
+ if (keywords[i] == NULL)
+ break;
+ }
+
+ if (i == count && count >= MAILDIR_MAX_KEYWORDS)
+ return -1;
+
+ if (!maildir_uidlist_is_locked(mk->mbox->uidlist))
+ return -1;
+
+ maildir_keywords_create(mk, name, i);
+ *chridx_r = i;
+ return 1;
+}
+
+static const char *
+maildir_keywords_idx(struct maildir_keywords *mk, unsigned int idx)
+{
+ const char *const *keywords;
+ unsigned int count;
+
+ keywords = array_get(&mk->list, &count);
+ if (idx >= count) {
+ if (mk->synced)
+ return NULL;
+
+ if (maildir_keywords_sync(mk) < 0)
+ return NULL;
+ i_assert(mk->synced);
+
+ keywords = array_get(&mk->list, &count);
+ }
+ return idx >= count ? NULL : keywords[idx];
+}
+
+static int maildir_keywords_write_fd(struct maildir_keywords *mk,
+ const char *path, int fd)
+{
+ struct maildir_mailbox *mbox = mk->mbox;
+ struct mailbox *box = &mbox->box;
+ const struct mailbox_permissions *perm = mailbox_get_permissions(box);
+ const char *const *keywords;
+ unsigned int i, count;
+ string_t *str;
+ struct stat st;
+
+ str = t_str_new(256);
+ keywords = array_get(&mk->list, &count);
+ for (i = 0; i < count; i++) {
+ if (keywords[i] != NULL)
+ str_printfa(str, "%u %s\n", i, keywords[i]);
+ }
+ if (write_full(fd, str_data(str), str_len(str)) < 0) {
+ mailbox_set_critical(&mk->mbox->box,
+ "write_full(%s) failed: %m", path);
+ return -1;
+ }
+
+ if (fstat(fd, &st) < 0) {
+ mailbox_set_critical(&mk->mbox->box,
+ "fstat(%s) failed: %m", path);
+ return -1;
+ }
+
+ if (st.st_gid != perm->file_create_gid &&
+ perm->file_create_gid != (gid_t)-1) {
+ if (fchown(fd, (uid_t)-1, perm->file_create_gid) < 0) {
+ if (errno == EPERM) {
+ mailbox_set_critical(&mk->mbox->box, "%s",
+ eperm_error_get_chgrp("fchown", path,
+ perm->file_create_gid,
+ perm->file_create_gid_origin));
+ } else {
+ mailbox_set_critical(&mk->mbox->box,
+ "fchown(%s) failed: %m", path);
+ }
+ }
+ }
+
+ /* mtime must grow every time */
+ if (st.st_mtime <= mk->synced_mtime) {
+ struct utimbuf ut;
+
+ mk->synced_mtime = ioloop_time <= mk->synced_mtime ?
+ mk->synced_mtime + 1 : ioloop_time;
+ ut.actime = ioloop_time;
+ ut.modtime = mk->synced_mtime;
+ if (utime(path, &ut) < 0) {
+ mailbox_set_critical(&mk->mbox->box,
+ "utime(%s) failed: %m", path);
+ return -1;
+ }
+ }
+
+ if (fsync(fd) < 0) {
+ mailbox_set_critical(&mk->mbox->box,
+ "fsync(%s) failed: %m", path);
+ return -1;
+ }
+ return 0;
+}
+
+static int maildir_keywords_commit(struct maildir_keywords *mk)
+{
+ const struct mailbox_permissions *perm;
+ struct dotlock *dotlock;
+ const char *lock_path;
+ mode_t old_mask;
+ int i, fd;
+
+ mk->synced = FALSE;
+
+ if (!mk->changed || mk->mbox == NULL)
+ return 0;
+
+ lock_path = t_strconcat(mk->path, ".lock", NULL);
+ i_unlink_if_exists(lock_path);
+
+ perm = mailbox_get_permissions(&mk->mbox->box);
+ for (i = 0;; i++) {
+ /* we could just create the temp file directly, but doing it
+ this ways avoids potential problems with overwriting
+ contents in malicious symlinks */
+ old_mask = umask(0777 & ~perm->file_create_mode);
+ fd = file_dotlock_open(&mk->dotlock_settings, mk->path,
+ DOTLOCK_CREATE_FLAG_NONBLOCK, &dotlock);
+ umask(old_mask);
+ if (fd != -1)
+ break;
+
+ if (errno != ENOENT || i == MAILDIR_DELETE_RETRY_COUNT) {
+ mailbox_set_critical(&mk->mbox->box,
+ "file_dotlock_open(%s) failed: %m", mk->path);
+ return -1;
+ }
+ /* the control dir doesn't exist. create it unless the whole
+ mailbox was just deleted. */
+ if (!maildir_set_deleted(&mk->mbox->box))
+ return -1;
+ }
+
+ if (maildir_keywords_write_fd(mk, lock_path, fd) < 0) {
+ file_dotlock_delete(&dotlock);
+ return -1;
+ }
+
+ if (file_dotlock_replace(&dotlock, 0) < 0) {
+ mailbox_set_critical(&mk->mbox->box,
+ "file_dotlock_replace(%s) failed: %m", mk->path);
+ return -1;
+ }
+
+ mk->changed = FALSE;
+ return 0;
+}
+
+struct maildir_keywords_sync_ctx *
+maildir_keywords_sync_init(struct maildir_keywords *mk,
+ struct mail_index *index)
+{
+ struct maildir_keywords_sync_ctx *ctx;
+
+ ctx = i_new(struct maildir_keywords_sync_ctx, 1);
+ ctx->mk = mk;
+ ctx->index = index;
+ ctx->keywords = mail_index_get_keywords(index);
+ i_array_init(&ctx->idx_to_chr, MAILDIR_MAX_KEYWORDS);
+ return ctx;
+}
+
+struct maildir_keywords_sync_ctx *
+maildir_keywords_sync_init_readonly(struct maildir_keywords *mk,
+ struct mail_index *index)
+{
+ struct maildir_keywords_sync_ctx *ctx;
+
+ ctx = maildir_keywords_sync_init(mk, index);
+ ctx->readonly = TRUE;
+ return ctx;
+}
+
+void maildir_keywords_sync_deinit(struct maildir_keywords_sync_ctx **_ctx)
+{
+ struct maildir_keywords_sync_ctx *ctx = *_ctx;
+
+ *_ctx = NULL;
+
+ T_BEGIN {
+ (void)maildir_keywords_commit(ctx->mk);
+ } T_END;
+
+ array_free(&ctx->idx_to_chr);
+ i_free(ctx);
+}
+
+unsigned int maildir_keywords_char_idx(struct maildir_keywords_sync_ctx *ctx,
+ char keyword)
+{
+ const char *name;
+ unsigned int chridx, idx;
+
+ i_assert(keyword >= MAILDIR_KEYWORD_FIRST &&
+ keyword <= MAILDIR_KEYWORD_LAST);
+ chridx = keyword - MAILDIR_KEYWORD_FIRST;
+
+ if (ctx->chridx_to_idx[chridx] != 0)
+ return ctx->chridx_to_idx[chridx];
+
+ /* lookup / create */
+ name = maildir_keywords_idx(ctx->mk, chridx);
+ if (name == NULL) {
+ /* name is lost. just generate one ourself. */
+ name = t_strdup_printf("unknown-%u", chridx);
+ while (maildir_keywords_lookup(ctx->mk, name, &idx) > 0) {
+ /* don't create a duplicate name.
+ keep changing the name until it doesn't exist */
+ name = t_strconcat(name, "?", NULL);
+ }
+ maildir_keywords_create(ctx->mk, name, chridx);
+ }
+
+ mail_index_keyword_lookup_or_create(ctx->index, name, &idx);
+ ctx->chridx_to_idx[chridx] = idx;
+ return idx;
+}
+
+char maildir_keywords_idx_char(struct maildir_keywords_sync_ctx *ctx,
+ unsigned int idx)
+{
+ const char *name;
+ char *chr_p;
+ unsigned int chridx;
+ int ret;
+
+ chr_p = array_idx_get_space(&ctx->idx_to_chr, idx);
+ if (*chr_p != '\0')
+ return *chr_p;
+
+ name = array_idx_elem(ctx->keywords, idx);
+ ret = !ctx->readonly ?
+ maildir_keywords_lookup_or_create(ctx->mk, name, &chridx) :
+ maildir_keywords_lookup(ctx->mk, name, &chridx);
+ if (ret <= 0)
+ return '\0';
+
+ *chr_p = chridx + MAILDIR_KEYWORD_FIRST;
+ return *chr_p;
+}
diff --git a/src/lib-storage/index/maildir/maildir-keywords.h b/src/lib-storage/index/maildir/maildir-keywords.h
new file mode 100644
index 0000000..43e47d7
--- /dev/null
+++ b/src/lib-storage/index/maildir/maildir-keywords.h
@@ -0,0 +1,36 @@
+#ifndef MAILDIR_KEYWORDS_H
+#define MAILDIR_KEYWORDS_H
+
+#define MAILDIR_KEYWORDS_NAME "dovecot-keywords"
+
+struct maildir_mailbox;
+struct maildir_keywords;
+struct maildir_keywords_sync_ctx;
+
+struct maildir_keywords *maildir_keywords_init(struct maildir_mailbox *mbox);
+void maildir_keywords_deinit(struct maildir_keywords **mk);
+
+/* Initialize a read-only maildir_keywords instance. Mailbox needs to contain
+ the dovecot-keywords file, but otherwise it doesn't have to be in maildir
+ format. */
+struct maildir_keywords *
+maildir_keywords_init_readonly(struct mailbox *box);
+
+struct maildir_keywords_sync_ctx *
+maildir_keywords_sync_init(struct maildir_keywords *mk,
+ struct mail_index *index);
+/* Don't try to add any nonexistent keywords */
+struct maildir_keywords_sync_ctx *
+maildir_keywords_sync_init_readonly(struct maildir_keywords *mk,
+ struct mail_index *index);
+void maildir_keywords_sync_deinit(struct maildir_keywords_sync_ctx **ctx);
+
+/* Returns keyword index. */
+unsigned int maildir_keywords_char_idx(struct maildir_keywords_sync_ctx *ctx,
+ char keyword);
+/* Returns keyword character for given index, or \0 if keyword couldn't be
+ added. */
+char maildir_keywords_idx_char(struct maildir_keywords_sync_ctx *ctx,
+ unsigned int idx);
+
+#endif
diff --git a/src/lib-storage/index/maildir/maildir-mail.c b/src/lib-storage/index/maildir/maildir-mail.c
new file mode 100644
index 0000000..c3abbd3
--- /dev/null
+++ b/src/lib-storage/index/maildir/maildir-mail.c
@@ -0,0 +1,809 @@
+/* Copyright (c) 2003-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "istream.h"
+#include "nfs-workarounds.h"
+#include "index-mail.h"
+#include "maildir-storage.h"
+#include "maildir-filename.h"
+#include "maildir-uidlist.h"
+#include "maildir-sync.h"
+
+#include <stdio.h>
+#include <fcntl.h>
+#include <unistd.h>
+#include <sys/stat.h>
+
+struct maildir_open_context {
+ int fd;
+ char *path;
+};
+
+static int
+do_open(struct maildir_mailbox *mbox, const char *path,
+ struct maildir_open_context *ctx)
+{
+ ctx->fd = nfs_safe_open(path, O_RDONLY);
+ if (ctx->fd != -1) {
+ ctx->path = i_strdup(path);
+ return 1;
+ }
+ if (errno == ENOENT)
+ return 0;
+
+ if (errno == EACCES) {
+ mailbox_set_critical(&mbox->box, "%s",
+ mail_error_eacces_msg("open", path));
+ } else {
+ mailbox_set_critical(&mbox->box,
+ "open(%s) failed: %m", path);
+ }
+ return -1;
+}
+
+static int
+do_stat(struct maildir_mailbox *mbox, const char *path, struct stat *st)
+{
+ if (stat(path, st) == 0)
+ return 1;
+ if (errno == ENOENT)
+ return 0;
+
+ if (errno == EACCES) {
+ mailbox_set_critical(&mbox->box, "%s",
+ mail_error_eacces_msg("stat", path));
+ } else {
+ mailbox_set_critical(&mbox->box, "stat(%s) failed: %m", path);
+ }
+ return -1;
+}
+
+static struct istream *
+maildir_open_mail(struct maildir_mailbox *mbox, struct mail *mail,
+ bool *deleted_r)
+{
+ struct istream *input;
+ const char *path;
+ struct maildir_open_context ctx;
+
+ *deleted_r = FALSE;
+
+ if (!mail_stream_access_start(mail))
+ return NULL;
+
+ ctx.fd = -1;
+ ctx.path = NULL;
+
+ mail->transaction->stats.open_lookup_count++;
+ if (!mail->saving) {
+ if (maildir_file_do(mbox, mail->uid, do_open, &ctx) < 0)
+ return NULL;
+ } else {
+ path = maildir_save_file_get_path(mail->transaction, mail->seq);
+ if (do_open(mbox, path, &ctx) <= 0)
+ return NULL;
+ }
+
+ if (ctx.fd == -1) {
+ *deleted_r = TRUE;
+ return NULL;
+ }
+
+ input = i_stream_create_fd_autoclose(&ctx.fd, 0);
+ if (input->stream_errno == EISDIR) {
+ i_stream_destroy(&input);
+ if (maildir_lose_unexpected_dir(&mbox->storage->storage,
+ ctx.path) >= 0)
+ *deleted_r = TRUE;
+ } else {
+ i_stream_set_name(input, ctx.path);
+ index_mail_set_read_buffer_size(mail, input);
+ }
+ i_free(ctx.path);
+ return input;
+}
+
+static int maildir_mail_stat(struct mail *mail, struct stat *st_r)
+{
+ struct maildir_mailbox *mbox = MAILDIR_MAILBOX(mail->box);
+ struct index_mail *imail = INDEX_MAIL(mail);
+ const char *path;
+ int fd, ret;
+
+ if (!mail_metadata_access_start(mail))
+ return -1;
+
+ if (imail->data.access_part != 0 &&
+ imail->data.stream == NULL) {
+ /* we're going to open the mail anyway */
+ struct istream *input;
+
+ (void)mail_get_stream(mail, NULL, NULL, &input);
+ }
+
+ if (imail->data.stream != NULL &&
+ (fd = i_stream_get_fd(imail->data.stream)) != -1) {
+ mail->transaction->stats.fstat_lookup_count++;
+ if (fstat(fd, st_r) < 0) {
+ mail_set_critical(mail, "fstat(%s) failed: %m",
+ i_stream_get_name(imail->data.stream));
+ return -1;
+ }
+ } else if (!mail->saving) {
+ mail->transaction->stats.stat_lookup_count++;
+ ret = maildir_file_do(mbox, mail->uid, do_stat, st_r);
+ if (ret <= 0) {
+ if (ret == 0)
+ mail_set_expunged(mail);
+ return -1;
+ }
+ } else {
+ mail->transaction->stats.stat_lookup_count++;
+ path = maildir_save_file_get_path(mail->transaction, mail->seq);
+ if (stat(path, st_r) < 0) {
+ mail_set_critical(mail, "stat(%s) failed: %m", path);
+ return -1;
+ }
+ }
+ return 0;
+}
+
+static int maildir_mail_get_received_date(struct mail *_mail, time_t *date_r)
+{
+ struct index_mail *mail = INDEX_MAIL(_mail);
+ struct index_mail_data *data = &mail->data;
+ struct stat st;
+
+ if (index_mail_get_received_date(_mail, date_r) == 0)
+ return 0;
+
+ if (maildir_mail_stat(_mail, &st) < 0)
+ return -1;
+
+ *date_r = data->received_date = st.st_mtime;
+ return 0;
+}
+
+static int maildir_mail_get_save_date(struct mail *_mail, time_t *date_r)
+{
+ struct index_mail *mail = INDEX_MAIL(_mail);
+ struct index_mail_data *data = &mail->data;
+ struct stat st;
+
+ if (index_mail_get_save_date(_mail, date_r) > 0)
+ return 1;
+
+ if (maildir_mail_stat(_mail, &st) < 0)
+ return -1;
+
+ *date_r = data->save_date = st.st_ctime;
+ return 1;
+}
+
+static int
+maildir_mail_get_fname(struct maildir_mailbox *mbox, struct mail *mail,
+ const char **fname_r)
+{
+ enum maildir_uidlist_rec_flag flags;
+ struct mail_index_view *view;
+ uint32_t seq;
+ bool exists;
+ int ret;
+
+ ret = maildir_sync_lookup(mbox, mail->uid, &flags, fname_r);
+ if (ret != 0)
+ return ret;
+
+ /* file exists in index file, but not in dovecot-uidlist anymore. */
+ mail_set_expunged(mail);
+
+ /* one reason this could happen is if we delayed opening
+ dovecot-uidlist and we're trying to open a mail that got recently
+ expunged. Let's test this theory first: */
+ mail_index_refresh(mbox->box.index);
+ view = mail_index_view_open(mbox->box.index);
+ exists = mail_index_lookup_seq(view, mail->uid, &seq);
+ mail_index_view_close(&view);
+
+ if (exists) {
+ /* the message still exists in index. this means there's some
+ kind of a desync, which doesn't get fixed if cur/ mtime is
+ the same as in index. fix this by forcing a resync. */
+ (void)maildir_storage_sync_force(mbox, mail->uid);
+ }
+ return 0;
+}
+
+static int maildir_get_pop3_state(struct index_mail *mail)
+{
+ struct mailbox *box = mail->mail.mail.box;
+ struct index_mailbox_context *ibox = INDEX_STORAGE_CONTEXT(box);
+ const struct mail_cache_field *fields;
+ unsigned int i, count, psize_idx, vsize_idx;
+ enum mail_cache_decision_type dec, vsize_dec;
+ enum mail_fetch_field allowed_pop3_fields;
+ bool not_pop3_only = FALSE;
+
+ if (mail->pop3_state_set)
+ return mail->pop3_state;
+
+ /* if this mail itself has non-pop3 fields we know we're not
+ pop3-only */
+ allowed_pop3_fields = MAIL_FETCH_FLAGS | MAIL_FETCH_STREAM_HEADER |
+ MAIL_FETCH_STREAM_BODY | MAIL_FETCH_STORAGE_ID |
+ MAIL_FETCH_VIRTUAL_SIZE;
+
+ if (mail->data.wanted_headers != NULL ||
+ (mail->data.wanted_fields & ENUM_NEGATE(allowed_pop3_fields)) != 0)
+ not_pop3_only = TRUE;
+
+ /* get vsize decisions */
+ psize_idx = ibox->cache_fields[MAIL_CACHE_PHYSICAL_FULL_SIZE].idx;
+ vsize_idx = ibox->cache_fields[MAIL_CACHE_VIRTUAL_FULL_SIZE].idx;
+ if (not_pop3_only) {
+ vsize_dec = mail_cache_field_get_decision(box->cache,
+ vsize_idx);
+ vsize_dec &= ENUM_NEGATE(MAIL_CACHE_DECISION_FORCED);
+ } else {
+ /* also check if there are any non-[pv]size cached fields */
+ vsize_dec = MAIL_CACHE_DECISION_NO;
+ fields = mail_cache_register_get_list(box->cache,
+ pool_datastack_create(),
+ &count);
+ for (i = 0; i < count; i++) {
+ dec = fields[i].decision & ENUM_NEGATE(MAIL_CACHE_DECISION_FORCED);
+ if (fields[i].idx == vsize_idx)
+ vsize_dec = dec;
+ else if (dec != MAIL_CACHE_DECISION_NO &&
+ fields[i].idx != psize_idx)
+ not_pop3_only = TRUE;
+ }
+ }
+
+ if (index_mail_get_vsize_extension(&mail->mail.mail) != NULL) {
+ /* having a vsize extension in index is the same as having
+ vsize's caching decision YES */
+ vsize_dec = MAIL_CACHE_DECISION_YES;
+ }
+
+ if (!not_pop3_only) {
+ /* either nothing is cached, or only vsize is cached. */
+ mail->pop3_state = 1;
+ } else if (vsize_dec != MAIL_CACHE_DECISION_YES &&
+ (box->flags & MAILBOX_FLAG_POP3_SESSION) == 0) {
+ /* if virtual size isn't cached permanently,
+ POP3 isn't being used */
+ mail->pop3_state = -1;
+ } else {
+ /* possibly a mixed pop3/imap */
+ mail->pop3_state = 0;
+ }
+ mail->pop3_state_set = TRUE;
+ return mail->pop3_state;
+}
+
+static int maildir_quick_size_lookup(struct index_mail *mail, bool vsize,
+ uoff_t *size_r)
+{
+ struct mail *_mail = &mail->mail.mail;
+ struct maildir_mailbox *mbox = MAILDIR_MAILBOX(_mail->box);
+ enum maildir_uidlist_rec_ext_key key;
+ const char *path, *fname, *value;
+
+ if (!_mail->saving) {
+ if (maildir_mail_get_fname(mbox, _mail, &fname) <= 0)
+ return -1;
+ } else {
+ if (maildir_save_file_get_size(_mail->transaction, _mail->seq,
+ vsize, size_r) == 0)
+ return 1;
+
+ path = maildir_save_file_get_path(_mail->transaction,
+ _mail->seq);
+ fname = strrchr(path, '/');
+ fname = fname != NULL ? fname + 1 : path;
+ }
+
+ /* size can be included in filename */
+ if (vsize || !mbox->storage->set->maildir_broken_filename_sizes) {
+ if (maildir_filename_get_size(fname,
+ vsize ? MAILDIR_EXTRA_VIRTUAL_SIZE :
+ MAILDIR_EXTRA_FILE_SIZE, size_r))
+ return 1;
+ }
+
+ /* size can be included in uidlist entry */
+ if (!_mail->saving) {
+ key = vsize ? MAILDIR_UIDLIST_REC_EXT_VSIZE :
+ MAILDIR_UIDLIST_REC_EXT_PSIZE;
+ value = maildir_uidlist_lookup_ext(mbox->uidlist, _mail->uid,
+ key);
+ if (value != NULL && str_to_uoff(value, size_r) == 0)
+ return 1;
+ }
+ return 0;
+}
+
+static void
+maildir_handle_size_caching(struct index_mail *mail, bool quick_check,
+ bool vsize)
+{
+ struct mailbox *box = mail->mail.mail.box;
+ struct maildir_mailbox *mbox = MAILDIR_MAILBOX(box);
+ enum mail_fetch_field field;
+ uoff_t size;
+ int pop3_state;
+
+ field = vsize ? MAIL_FETCH_VIRTUAL_SIZE : MAIL_FETCH_PHYSICAL_SIZE;
+ if ((mail->data.dont_cache_fetch_fields & field) != 0)
+ return;
+
+ if (quick_check && maildir_quick_size_lookup(mail, vsize, &size) > 0) {
+ /* already in filename / uidlist. don't add it anywhere,
+ including to the uidlist if it's already in filename.
+ do some extra checks here to catch potential cache bugs. */
+ if (vsize && mail->data.virtual_size != size) {
+ mail_set_mail_cache_corrupted(&mail->mail.mail,
+ "Corrupted virtual size: "
+ "%"PRIuUOFF_T" != %"PRIuUOFF_T,
+ mail->data.virtual_size, size);
+ mail->data.virtual_size = size;
+ } else if (!vsize && mail->data.physical_size != size) {
+ mail_set_mail_cache_corrupted(&mail->mail.mail,
+ "Corrupted physical size: "
+ "%"PRIuUOFF_T" != %"PRIuUOFF_T,
+ mail->data.physical_size, size);
+ mail->data.physical_size = size;
+ }
+ mail->data.dont_cache_fetch_fields |= field;
+ return;
+ }
+
+ /* 1 = pop3-only, 0 = mixed, -1 = no pop3 */
+ pop3_state = maildir_get_pop3_state(mail);
+ if (pop3_state >= 0 && mail->mail.mail.uid != 0) {
+ /* if size is wanted permanently, store it to uidlist
+ so that in case cache file gets lost we can get it quickly */
+ mail->data.dont_cache_fetch_fields |= field;
+ size = vsize ? mail->data.virtual_size :
+ mail->data.physical_size;
+ maildir_uidlist_set_ext(mbox->uidlist, mail->mail.mail.uid,
+ vsize ? MAILDIR_UIDLIST_REC_EXT_VSIZE :
+ MAILDIR_UIDLIST_REC_EXT_PSIZE,
+ dec2str(size));
+ }
+}
+
+static int maildir_mail_get_virtual_size(struct mail *_mail, uoff_t *size_r)
+{
+ struct maildir_mailbox *mbox = MAILDIR_MAILBOX(_mail->box);
+ struct index_mail *mail = INDEX_MAIL(_mail);
+ struct index_mail_data *data = &mail->data;
+ struct message_size hdr_size, body_size;
+ struct istream *input;
+ uoff_t old_offset;
+
+ if (maildir_uidlist_is_read(mbox->uidlist) ||
+ (_mail->box->flags & MAILBOX_FLAG_POP3_SESSION) != 0) {
+ /* try to get the size from uidlist. this is especially useful
+ with pop3 to avoid unnecessarily opening the cache file. */
+ if (maildir_quick_size_lookup(mail, TRUE,
+ &data->virtual_size) < 0)
+ return -1;
+ }
+
+ if (data->virtual_size == UOFF_T_MAX) {
+ if (index_mail_get_cached_virtual_size(mail, size_r)) {
+ i_assert(mail->data.virtual_size != UOFF_T_MAX);
+ maildir_handle_size_caching(mail, TRUE, TRUE);
+ return 0;
+ }
+ if (maildir_quick_size_lookup(mail, TRUE,
+ &data->virtual_size) < 0)
+ return -1;
+ }
+ if (data->virtual_size != UOFF_T_MAX) {
+ data->dont_cache_fetch_fields |= MAIL_FETCH_VIRTUAL_SIZE;
+ *size_r = data->virtual_size;
+ return 0;
+ }
+
+ /* fallback to reading the file */
+ old_offset = data->stream == NULL ? 0 : data->stream->v_offset;
+ if (mail_get_stream(_mail, &hdr_size, &body_size, &input) < 0)
+ return -1;
+ i_stream_seek(data->stream, old_offset);
+
+ maildir_handle_size_caching(mail, FALSE, TRUE);
+ *size_r = data->virtual_size;
+ return 0;
+}
+
+static int maildir_mail_get_physical_size(struct mail *_mail, uoff_t *size_r)
+{
+ struct index_mail *mail = INDEX_MAIL(_mail);
+ struct maildir_mailbox *mbox = MAILDIR_MAILBOX(_mail->box);
+ struct index_mail_data *data = &mail->data;
+ struct stat st;
+ struct message_size hdr_size, body_size;
+ struct istream *input;
+ const char *path;
+ int ret;
+
+ if (maildir_uidlist_is_read(mbox->uidlist) ||
+ (_mail->box->flags & MAILBOX_FLAG_POP3_SESSION) != 0) {
+ /* try to get the size from uidlist (see virtual size above) */
+ if (maildir_quick_size_lookup(mail, FALSE,
+ &data->physical_size) < 0)
+ return -1;
+ }
+
+ if (data->physical_size == UOFF_T_MAX) {
+ if (index_mail_get_physical_size(_mail, size_r) == 0) {
+ i_assert(mail->data.physical_size != UOFF_T_MAX);
+ maildir_handle_size_caching(mail, TRUE, FALSE);
+ return 0;
+ }
+ if (maildir_quick_size_lookup(mail, FALSE,
+ &data->physical_size) < 0)
+ return -1;
+ }
+ if (data->physical_size != UOFF_T_MAX) {
+ data->dont_cache_fetch_fields |= MAIL_FETCH_PHYSICAL_SIZE;
+ *size_r = data->physical_size;
+ return 0;
+ }
+
+ if (mail->mail.v.istream_opened != NULL) {
+ /* we can't use stat(), because this may be a mail that some
+ plugin has changed (e.g. zlib). need to do it the slow
+ way. */
+ if (mail_get_stream(_mail, &hdr_size, &body_size, &input) < 0)
+ return -1;
+ st.st_size = hdr_size.physical_size + body_size.physical_size;
+ } else if (!_mail->saving) {
+ ret = maildir_file_do(mbox, _mail->uid, do_stat, &st);
+ if (ret <= 0) {
+ if (ret == 0)
+ mail_set_expunged(_mail);
+ return -1;
+ }
+ } else {
+ /* saved mail which hasn't been committed yet */
+ path = maildir_save_file_get_path(_mail->transaction,
+ _mail->seq);
+ if (stat(path, &st) < 0) {
+ mail_set_critical(_mail, "stat(%s) failed: %m", path);
+ return -1;
+ }
+ }
+
+ data->physical_size = st.st_size;
+ maildir_handle_size_caching(mail, FALSE, FALSE);
+ *size_r = st.st_size;
+ return 0;
+}
+
+static int
+maildir_mail_get_special(struct mail *_mail, enum mail_fetch_field field,
+ const char **value_r)
+{
+ struct index_mail *mail = INDEX_MAIL(_mail);
+ struct maildir_mailbox *mbox = MAILDIR_MAILBOX(_mail->box);
+ const char *path, *fname = NULL, *end, *guid, *uidl, *order;
+ struct stat st;
+
+ switch (field) {
+ case MAIL_FETCH_GUID:
+ /* use GUID from uidlist if it exists */
+ i_assert(!_mail->saving);
+
+ if (mail->data.guid != NULL) {
+ *value_r = mail->data.guid;
+ return 0;
+ }
+
+ /* first make sure that we have a refreshed uidlist */
+ if (maildir_mail_get_fname(mbox, _mail, &fname) <= 0)
+ return -1;
+
+ guid = maildir_uidlist_lookup_ext(mbox->uidlist, _mail->uid,
+ MAILDIR_UIDLIST_REC_EXT_GUID);
+ if (guid != NULL) {
+ if (*guid != '\0') {
+ *value_r = mail->data.guid =
+ p_strdup(mail->mail.data_pool, guid);
+ return 0;
+ }
+
+ mail_set_critical(_mail,
+ "Maildir: Corrupted dovecot-uidlist: "
+ "UID had empty GUID, clearing it");
+ maildir_uidlist_unset_ext(mbox->uidlist, _mail->uid,
+ MAILDIR_UIDLIST_REC_EXT_GUID);
+ }
+
+ /* default to base filename: */
+ if (maildir_mail_get_special(_mail, MAIL_FETCH_STORAGE_ID,
+ value_r) < 0)
+ return -1;
+ mail->data.guid = mail->data.filename;
+ return 0;
+ case MAIL_FETCH_STORAGE_ID:
+ if (mail->data.filename != NULL) {
+ *value_r = mail->data.filename;
+ return 0;
+ }
+ if (fname != NULL) {
+ /* we came here from MAIL_FETCH_GUID,
+ avoid a second lookup */
+ } else if (!_mail->saving) {
+ if (maildir_mail_get_fname(mbox, _mail, &fname) <= 0)
+ return -1;
+ } else {
+ path = maildir_save_file_get_path(_mail->transaction,
+ _mail->seq);
+ fname = strrchr(path, '/');
+ fname = fname != NULL ? fname + 1 : path;
+ }
+ end = strchr(fname, MAILDIR_INFO_SEP);
+ mail->data.filename = end == NULL ?
+ p_strdup(mail->mail.data_pool, fname) :
+ p_strdup_until(mail->mail.data_pool, fname, end);
+ *value_r = mail->data.filename;
+ return 0;
+ case MAIL_FETCH_UIDL_BACKEND:
+ uidl = maildir_uidlist_lookup_ext(mbox->uidlist, _mail->uid,
+ MAILDIR_UIDLIST_REC_EXT_POP3_UIDL);
+ if (uidl == NULL) {
+ /* use the default */
+ *value_r = "";
+ } else if (*uidl == '\0') {
+ /* special optimization case: use the base file name */
+ return maildir_mail_get_special(_mail,
+ MAIL_FETCH_STORAGE_ID, value_r);
+ } else {
+ *value_r = p_strdup(mail->mail.data_pool, uidl);
+ }
+ return 0;
+ case MAIL_FETCH_POP3_ORDER:
+ order = maildir_uidlist_lookup_ext(mbox->uidlist, _mail->uid,
+ MAILDIR_UIDLIST_REC_EXT_POP3_ORDER);
+ if (order == NULL) {
+ *value_r = "";
+ } else {
+ *value_r = p_strdup(mail->mail.data_pool, order);
+ }
+ return 0;
+ case MAIL_FETCH_REFCOUNT:
+ if (maildir_mail_stat(_mail, &st) < 0)
+ return -1;
+ *value_r = p_strdup_printf(mail->mail.data_pool, "%lu",
+ (unsigned long)st.st_nlink);
+ return 0;
+ case MAIL_FETCH_REFCOUNT_ID:
+ if (maildir_mail_stat(_mail, &st) < 0)
+ return -1;
+ *value_r = p_strdup_printf(mail->mail.data_pool, "%llu",
+ (unsigned long long)st.st_ino);
+ return 0;
+ default:
+ return index_mail_get_special(_mail, field, value_r);
+ }
+}
+
+static int
+maildir_mail_get_stream(struct mail *_mail, bool get_body ATTR_UNUSED,
+ struct message_size *hdr_size,
+ struct message_size *body_size,
+ struct istream **stream_r)
+{
+ struct index_mail *mail = INDEX_MAIL(_mail);
+ struct maildir_mailbox *mbox = MAILDIR_MAILBOX(_mail->box);
+ struct index_mail_data *data = &mail->data;
+ bool deleted;
+
+ if (data->stream == NULL) {
+ data->stream = maildir_open_mail(mbox, _mail, &deleted);
+ if (data->stream == NULL) {
+ if (deleted)
+ mail_set_expunged(_mail);
+ return -1;
+ }
+ if (mail->mail.v.istream_opened != NULL) {
+ if (mail->mail.v.istream_opened(_mail,
+ &data->stream) < 0) {
+ i_stream_unref(&data->stream);
+ return -1;
+ }
+ }
+ }
+
+ return index_mail_init_stream(mail, hdr_size, body_size, stream_r);
+}
+
+static void maildir_update_pop3_uidl(struct mail *_mail, const char *uidl)
+{
+ struct maildir_mailbox *mbox = MAILDIR_MAILBOX(_mail->box);
+ const char *fname;
+
+ if (maildir_mail_get_special(_mail, MAIL_FETCH_STORAGE_ID,
+ &fname) == 0 &&
+ strcmp(uidl, fname) == 0) {
+ /* special case optimization: empty UIDL means the same
+ as base filename */
+ uidl = "";
+ }
+
+ maildir_uidlist_set_ext(mbox->uidlist, _mail->uid,
+ MAILDIR_UIDLIST_REC_EXT_POP3_UIDL, uidl);
+}
+
+static void maildir_mail_remove_sizes_from_uidlist(struct mail *mail)
+{
+ struct maildir_mailbox *mbox = MAILDIR_MAILBOX(mail->box);
+
+ if (maildir_uidlist_lookup_ext(mbox->uidlist, mail->uid,
+ MAILDIR_UIDLIST_REC_EXT_VSIZE) != NULL) {
+ maildir_uidlist_unset_ext(mbox->uidlist, mail->uid,
+ MAILDIR_UIDLIST_REC_EXT_VSIZE);
+ }
+ if (maildir_uidlist_lookup_ext(mbox->uidlist, mail->uid,
+ MAILDIR_UIDLIST_REC_EXT_PSIZE) != NULL) {
+ maildir_uidlist_unset_ext(mbox->uidlist, mail->uid,
+ MAILDIR_UIDLIST_REC_EXT_PSIZE);
+ }
+}
+
+struct maildir_size_fix_ctx {
+ uoff_t physical_size;
+ char wrong_key;
+};
+
+static int
+do_fix_size(struct maildir_mailbox *mbox, const char *path,
+ struct maildir_size_fix_ctx *ctx)
+{
+ const char *fname, *newpath, *extra, *info, *dir;
+ struct stat st;
+
+ fname = strrchr(path, '/');
+ i_assert(fname != NULL);
+ dir = t_strdup_until(path, fname++);
+
+ extra = strchr(fname, MAILDIR_EXTRA_SEP);
+ i_assert(extra != NULL);
+ info = strchr(fname, MAILDIR_INFO_SEP);
+ if (info == NULL) info = "";
+
+ if (ctx->physical_size == UOFF_T_MAX) {
+ if (stat(path, &st) < 0) {
+ if (errno == ENOENT)
+ return 0;
+ mailbox_set_critical(&mbox->box,
+ "stat(%s) failed: %m", path);
+ return -1;
+ }
+ ctx->physical_size = st.st_size;
+ }
+
+ newpath = t_strdup_printf("%s/%s,S=%"PRIuUOFF_T"%s", dir,
+ t_strdup_until(fname, extra),
+ ctx->physical_size, info);
+
+ if (rename(path, newpath) == 0) {
+ mailbox_set_critical(&mbox->box,
+ "Maildir filename has wrong %c value, "
+ "renamed the file from %s to %s",
+ ctx->wrong_key, path, newpath);
+ return 1;
+ }
+ if (errno == ENOENT)
+ return 0;
+
+ mailbox_set_critical(&mbox->box, "rename(%s, %s) failed: %m",
+ path, newpath);
+ return -1;
+}
+
+static void
+maildir_mail_remove_sizes_from_filename(struct mail *mail,
+ enum mail_fetch_field field)
+{
+ struct maildir_mailbox *mbox = MAILDIR_MAILBOX(mail->box);
+ struct mail_private *pmail = (struct mail_private *)mail;
+ enum maildir_uidlist_rec_flag flags;
+ const char *fname;
+ uoff_t size;
+ struct maildir_size_fix_ctx ctx;
+
+ if (mbox->storage->set->maildir_broken_filename_sizes) {
+ /* never try to fix sizes in maildir filenames */
+ return;
+ }
+
+ if (maildir_sync_lookup(mbox, mail->uid, &flags, &fname) <= 0)
+ return;
+ if (strchr(fname, MAILDIR_EXTRA_SEP) == NULL)
+ return;
+
+ i_zero(&ctx);
+ ctx.physical_size = UOFF_T_MAX;
+ if (field == MAIL_FETCH_VIRTUAL_SIZE &&
+ maildir_filename_get_size(fname, MAILDIR_EXTRA_VIRTUAL_SIZE,
+ &size)) {
+ ctx.wrong_key = 'W';
+ } else if (field == MAIL_FETCH_PHYSICAL_SIZE &&
+ maildir_filename_get_size(fname, MAILDIR_EXTRA_FILE_SIZE,
+ &size)) {
+ ctx.wrong_key = 'S';
+ } else {
+ /* the broken size isn't in filename */
+ return;
+ }
+
+ if (pmail->v.istream_opened != NULL) {
+ /* the mail could be e.g. compressed. get the physical size
+ the slow way by actually reading the mail. */
+ struct istream *input;
+ const struct stat *stp;
+
+ if (mail_get_stream(mail, NULL, NULL, &input) < 0)
+ return;
+ if (i_stream_stat(input, TRUE, &stp) < 0)
+ return;
+ ctx.physical_size = stp->st_size;
+ }
+
+ (void)maildir_file_do(mbox, mail->uid, do_fix_size, &ctx);
+}
+
+static void maildir_mail_set_cache_corrupted(struct mail *_mail,
+ enum mail_fetch_field field,
+ const char *reason)
+{
+ if (field == MAIL_FETCH_PHYSICAL_SIZE ||
+ field == MAIL_FETCH_VIRTUAL_SIZE) {
+ maildir_mail_remove_sizes_from_uidlist(_mail);
+ maildir_mail_remove_sizes_from_filename(_mail, field);
+ }
+ index_mail_set_cache_corrupted(_mail, field, reason);
+}
+
+struct mail_vfuncs maildir_mail_vfuncs = {
+ index_mail_close,
+ index_mail_free,
+ index_mail_set_seq,
+ index_mail_set_uid,
+ index_mail_set_uid_cache_updates,
+ index_mail_prefetch,
+ index_mail_precache,
+ index_mail_add_temp_wanted_fields,
+
+ index_mail_get_flags,
+ index_mail_get_keywords,
+ index_mail_get_keyword_indexes,
+ index_mail_get_modseq,
+ index_mail_get_pvt_modseq,
+ index_mail_get_parts,
+ index_mail_get_date,
+ maildir_mail_get_received_date,
+ maildir_mail_get_save_date,
+ maildir_mail_get_virtual_size,
+ maildir_mail_get_physical_size,
+ index_mail_get_first_header,
+ index_mail_get_headers,
+ index_mail_get_header_stream,
+ maildir_mail_get_stream,
+ index_mail_get_binary_stream,
+ maildir_mail_get_special,
+ index_mail_get_backend_mail,
+ index_mail_update_flags,
+ index_mail_update_keywords,
+ index_mail_update_modseq,
+ index_mail_update_pvt_modseq,
+ maildir_update_pop3_uidl,
+ index_mail_expunge,
+ maildir_mail_set_cache_corrupted,
+ index_mail_opened,
+};
diff --git a/src/lib-storage/index/maildir/maildir-save.c b/src/lib-storage/index/maildir/maildir-save.c
new file mode 100644
index 0000000..5cf7d6a
--- /dev/null
+++ b/src/lib-storage/index/maildir/maildir-save.c
@@ -0,0 +1,1084 @@
+/* Copyright (c) 2002-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "ioloop.h"
+#include "array.h"
+#include "buffer.h"
+#include "istream.h"
+#include "istream-crlf.h"
+#include "ostream.h"
+#include "fdatasync-path.h"
+#include "eacces-error.h"
+#include "str.h"
+#include "index-mail.h"
+#include "maildir-storage.h"
+#include "maildir-uidlist.h"
+#include "maildir-keywords.h"
+#include "maildir-filename.h"
+#include "maildir-filename-flags.h"
+#include "maildir-sync.h"
+#include "mailbox-recent-flags.h"
+
+#include <stdio.h>
+#include <unistd.h>
+#include <fcntl.h>
+#include <utime.h>
+#include <sys/stat.h>
+
+#define MAILDIR_FILENAME_FLAG_MOVED 0x10000000
+
+struct maildir_filename {
+ struct maildir_filename *next;
+ const char *tmp_name, *dest_basename;
+ const char *pop3_uidl, *guid;
+
+ uoff_t size, vsize;
+ enum mail_flags flags;
+ unsigned int pop3_order;
+ bool preserve_filename:1;
+ ARRAY_TYPE(keyword_indexes) keywords;
+};
+
+struct maildir_save_context {
+ struct mail_save_context ctx;
+ pool_t pool;
+
+ struct maildir_mailbox *mbox;
+ struct mail_index_transaction *trans;
+ struct maildir_uidlist_sync_ctx *uidlist_sync_ctx;
+ struct maildir_keywords_sync_ctx *keywords_sync_ctx;
+ struct maildir_index_sync_context *sync_ctx;
+ struct mail *cur_dest_mail;
+
+ const char *tmpdir, *newdir, *curdir;
+ struct maildir_filename *files, **files_tail, *file_last;
+ unsigned int files_count;
+
+ struct istream *input;
+ int fd;
+ uint32_t first_seq, seq, last_nonrecent_uid;
+
+ bool have_keywords:1;
+ bool have_preserved_filenames:1;
+ bool locked:1;
+ bool failed:1;
+ bool last_save_finished:1;
+ bool locked_uidlist_refresh:1;
+};
+
+#define MAILDIR_SAVECTX(s) container_of(s, struct maildir_save_context, ctx)
+
+static int maildir_file_move(struct maildir_save_context *ctx,
+ struct maildir_filename *mf, const char *destname,
+ bool newdir)
+{
+ struct mail_storage *storage = &ctx->mbox->storage->storage;
+ const char *tmp_path, *new_path;
+
+ i_assert(*destname != '\0');
+ i_assert(*mf->tmp_name != '\0');
+
+ /* if we have flags, we'll move it to cur/ directly, because files in
+ new/ directory can't have flags. alternative would be to write it
+ in new/ and set the flags dirty in index file, but in that case
+ external MUAs would see wrong flags. */
+ tmp_path = t_strconcat(ctx->tmpdir, "/", mf->tmp_name, NULL);
+ new_path = newdir ?
+ t_strconcat(ctx->newdir, "/", destname, NULL) :
+ t_strconcat(ctx->curdir, "/", destname, NULL);
+
+ /* maildir spec says we should use link() + unlink() here. however
+ since our filename is guaranteed to be unique, rename() works just
+ as well, except faster. even if the filename wasn't unique, the
+ problem could still happen if the file was already moved from
+ new/ to cur/, so link() doesn't really provide any safety anyway.
+
+ Besides the small temporary performance benefits, this rename() is
+ almost required with OSX's HFS+ filesystem, since it implements
+ hard links in a pretty ugly way, which makes the performance crawl
+ when a lot of hard links are used. */
+ if (rename(tmp_path, new_path) == 0) {
+ mf->flags |= MAILDIR_FILENAME_FLAG_MOVED;
+ return 0;
+ } else if (ENOQUOTA(errno)) {
+ mail_storage_set_error(storage, MAIL_ERROR_NOQUOTA,
+ MAIL_ERRSTR_NO_QUOTA);
+ return -1;
+ } else {
+ mailbox_set_critical(&ctx->mbox->box,
+ "rename(%s, %s) failed: %m",
+ tmp_path, new_path);
+ return -1;
+ }
+}
+
+static struct mail_save_context *
+maildir_save_transaction_init(struct mailbox_transaction_context *t)
+{
+ struct maildir_mailbox *mbox = MAILDIR_MAILBOX(t->box);
+ struct maildir_save_context *ctx;
+ const char *path;
+ pool_t pool;
+
+ pool = pool_alloconly_create("maildir_save_context", 4096);
+ ctx = p_new(pool, struct maildir_save_context, 1);
+ ctx->ctx.transaction = t;
+ ctx->pool = pool;
+ ctx->mbox = mbox;
+ ctx->trans = t->itrans;
+ ctx->files_tail = &ctx->files;
+ ctx->fd = -1;
+
+ path = mailbox_get_path(&mbox->box);
+ ctx->tmpdir = p_strconcat(pool, path, "/tmp", NULL);
+ ctx->newdir = p_strconcat(pool, path, "/new", NULL);
+ ctx->curdir = p_strconcat(pool, path, "/cur", NULL);
+
+ ctx->last_save_finished = TRUE;
+ return &ctx->ctx;
+}
+
+struct maildir_filename *
+maildir_save_add(struct mail_save_context *_ctx, const char *tmp_fname,
+ struct mail *src_mail)
+{
+ struct maildir_save_context *ctx = MAILDIR_SAVECTX(_ctx);
+ struct mail_save_data *mdata = &_ctx->data;
+ struct maildir_filename *mf;
+ struct istream *input;
+
+ i_assert(*tmp_fname != '\0');
+
+ /* allow caller to specify recent flag only when uid is specified
+ (we're replicating, converting, etc.). */
+ if (mdata->uid == 0)
+ mdata->flags |= MAIL_RECENT;
+ else if ((mdata->flags & MAIL_RECENT) == 0 &&
+ ctx->last_nonrecent_uid < mdata->uid)
+ ctx->last_nonrecent_uid = mdata->uid;
+
+ /* now, we want to be able to rollback the whole append session,
+ so we'll just store the name of this temp file and move it later
+ into new/ or cur/. */
+ mf = p_new(ctx->pool, struct maildir_filename, 1);
+ mf->tmp_name = mf->dest_basename = p_strdup(ctx->pool, tmp_fname);
+ mf->flags = mdata->flags;
+ mf->size = UOFF_T_MAX;
+ mf->vsize = UOFF_T_MAX;
+
+ ctx->file_last = mf;
+ i_assert(*ctx->files_tail == NULL);
+ *ctx->files_tail = mf;
+ ctx->files_tail = &mf->next;
+ ctx->files_count++;
+
+ if (mdata->pop3_uidl != NULL)
+ mf->pop3_uidl = p_strdup(ctx->pool, mdata->pop3_uidl);
+ mf->pop3_order = mdata->pop3_order;
+
+ /* insert into index */
+ mail_index_append(ctx->trans, mdata->uid, &ctx->seq);
+ mail_index_update_flags(ctx->trans, ctx->seq,
+ MODIFY_REPLACE,
+ mdata->flags & ENUM_NEGATE(MAIL_RECENT));
+ if (mdata->keywords != NULL) {
+ mail_index_update_keywords(ctx->trans, ctx->seq,
+ MODIFY_REPLACE, mdata->keywords);
+ }
+ if (mdata->min_modseq != 0) {
+ mail_index_update_modseq(ctx->trans, ctx->seq,
+ mdata->min_modseq);
+ }
+
+ if (ctx->first_seq == 0) {
+ ctx->first_seq = ctx->seq;
+ i_assert(ctx->files->next == NULL);
+ }
+
+ mail_set_seq_saving(_ctx->dest_mail, ctx->seq);
+
+ if (ctx->input == NULL) {
+ /* copying with hardlinking. */
+ i_assert(src_mail != NULL);
+ index_copy_cache_fields(_ctx, src_mail, ctx->seq);
+ ctx->cur_dest_mail = NULL;
+ } else {
+ input = index_mail_cache_parse_init(_ctx->dest_mail,
+ ctx->input);
+ i_stream_unref(&ctx->input);
+ ctx->input = input;
+ ctx->cur_dest_mail = _ctx->dest_mail;
+ }
+ return mf;
+}
+
+void maildir_save_set_dest_basename(struct mail_save_context *_ctx,
+ struct maildir_filename *mf,
+ const char *basename)
+{
+ struct maildir_save_context *ctx = MAILDIR_SAVECTX(_ctx);
+
+ mf->preserve_filename = TRUE;
+ mf->dest_basename = p_strdup(ctx->pool, basename);
+ ctx->have_preserved_filenames = TRUE;
+}
+
+void maildir_save_set_sizes(struct maildir_filename *mf,
+ uoff_t size, uoff_t vsize)
+{
+ mf->size = size;
+ mf->vsize = vsize;
+}
+
+static bool
+maildir_get_dest_filename(struct maildir_save_context *ctx,
+ struct maildir_filename *mf,
+ const char **fname_r)
+{
+ const char *basename = mf->dest_basename;
+
+ if (mf->size != UOFF_T_MAX && !mf->preserve_filename) {
+ basename = t_strdup_printf("%s,%c=%"PRIuUOFF_T, basename,
+ MAILDIR_EXTRA_FILE_SIZE, mf->size);
+ }
+
+ if (mf->vsize != UOFF_T_MAX && !mf->preserve_filename) {
+ basename = t_strdup_printf("%s,%c=%"PRIuUOFF_T, basename,
+ MAILDIR_EXTRA_VIRTUAL_SIZE,
+ mf->vsize);
+ }
+
+ if (!array_is_created(&mf->keywords) || array_count(&mf->keywords) == 0) {
+ if ((mf->flags & MAIL_FLAGS_MASK) == MAIL_RECENT) {
+ *fname_r = basename;
+ return TRUE;
+ }
+
+ *fname_r = maildir_filename_flags_set(basename,
+ mf->flags & MAIL_FLAGS_MASK);
+ return FALSE;
+ }
+
+ i_assert(ctx->keywords_sync_ctx != NULL ||
+ !array_is_created(&mf->keywords) || array_count(&mf->keywords) == 0);
+ *fname_r = maildir_filename_flags_kw_set(ctx->keywords_sync_ctx,
+ basename,
+ mf->flags & MAIL_FLAGS_MASK,
+ &mf->keywords);
+ return FALSE;
+}
+
+static const char *maildir_mf_get_path(struct maildir_save_context *ctx,
+ struct maildir_filename *mf)
+{
+ const char *fname, *dir;
+
+ if ((mf->flags & MAILDIR_FILENAME_FLAG_MOVED) == 0) {
+ /* file is still in tmp/ */
+ return t_strdup_printf("%s/%s", ctx->tmpdir, mf->tmp_name);
+ }
+
+ /* already moved to new/ or cur/ */
+ dir = maildir_get_dest_filename(ctx, mf, &fname) ?
+ ctx->newdir : ctx->curdir;
+ return t_strdup_printf("%s/%s", dir, fname);
+}
+
+
+static struct maildir_filename *
+maildir_save_get_mf(struct mailbox_transaction_context *t, uint32_t seq)
+{
+ struct maildir_save_context *save_ctx = MAILDIR_SAVECTX(t->save_ctx);
+ struct maildir_filename *mf;
+
+ i_assert(seq >= save_ctx->first_seq);
+
+ seq -= save_ctx->first_seq;
+ mf = save_ctx->files;
+ while (seq > 0) {
+ mf = mf->next;
+ i_assert(mf != NULL);
+ seq--;
+ }
+ return mf;
+}
+
+int maildir_save_file_get_size(struct mailbox_transaction_context *t,
+ uint32_t seq, bool vsize, uoff_t *size_r)
+{
+ struct maildir_filename *mf = maildir_save_get_mf(t, seq);
+
+ *size_r = vsize ? mf->vsize : mf->size;
+ return *size_r == UOFF_T_MAX ? -1 : 0;
+}
+
+const char *maildir_save_file_get_path(struct mailbox_transaction_context *t,
+ uint32_t seq)
+{
+ struct maildir_save_context *save_ctx = MAILDIR_SAVECTX(t->save_ctx);
+ struct maildir_filename *mf = maildir_save_get_mf(t, seq);
+
+ return maildir_mf_get_path(save_ctx, mf);
+}
+
+static int maildir_create_tmp(struct maildir_mailbox *mbox, const char *dir,
+ const char **fname_r)
+{
+ struct mailbox *box = &mbox->box;
+ const struct mailbox_permissions *perm = mailbox_get_permissions(box);
+ size_t prefix_len;
+ const char *tmp_fname;
+ string_t *path;
+ mode_t old_mask;
+ int fd;
+
+ path = t_str_new(256);
+ str_append(path, dir);
+ str_append_c(path, '/');
+ prefix_len = str_len(path);
+
+ do {
+ tmp_fname = maildir_filename_generate();
+ str_truncate(path, prefix_len);
+ str_append(path, tmp_fname);
+
+ /* the generated filename is unique. the only reason why it
+ might return an existing filename is if the time moved
+ backwards. so we'll use O_EXCL anyway, although it's mostly
+ useless. */
+ old_mask = umask(0777 & ~perm->file_create_mode);
+ fd = open(str_c(path),
+ O_WRONLY | O_CREAT | O_TRUNC | O_EXCL, 0777);
+ umask(old_mask);
+ } while (fd == -1 && errno == EEXIST);
+
+ *fname_r = tmp_fname;
+ if (fd == -1) {
+ if (ENOQUOTA(errno)) {
+ mail_storage_set_error(box->storage,
+ MAIL_ERROR_NOQUOTA, MAIL_ERRSTR_NO_QUOTA);
+ } else {
+ mailbox_set_critical(box,
+ "open(%s) failed: %m", str_c(path));
+ }
+ } else if (perm->file_create_gid != (gid_t)-1) {
+ if (fchown(fd, (uid_t)-1, perm->file_create_gid) < 0) {
+ if (errno == EPERM) {
+ mailbox_set_critical(box, "%s",
+ eperm_error_get_chgrp("fchown",
+ str_c(path),
+ perm->file_create_gid,
+ perm->file_create_gid_origin));
+ } else {
+ mailbox_set_critical(box,
+ "fchown(%s) failed: %m", str_c(path));
+ }
+ }
+ }
+
+ return fd;
+}
+
+struct mail_save_context *
+maildir_save_alloc(struct mailbox_transaction_context *t)
+{
+ i_assert((t->flags & MAILBOX_TRANSACTION_FLAG_EXTERNAL) != 0);
+
+ if (t->save_ctx == NULL)
+ t->save_ctx = maildir_save_transaction_init(t);
+ return t->save_ctx;
+}
+
+int maildir_save_begin(struct mail_save_context *_ctx, struct istream *input)
+{
+ struct maildir_save_context *ctx = MAILDIR_SAVECTX(_ctx);
+ struct maildir_filename *mf;
+
+ /* new mail, new failure state */
+ ctx->failed = FALSE;
+
+ T_BEGIN {
+ /* create a new file in tmp/ directory */
+ const char *fname;
+
+ ctx->fd = maildir_create_tmp(ctx->mbox, ctx->tmpdir, &fname);
+ if (ctx->fd == -1)
+ ctx->failed = TRUE;
+ else {
+ if (ctx->mbox->storage->storage.set->mail_save_crlf)
+ ctx->input = i_stream_create_crlf(input);
+ else
+ ctx->input = i_stream_create_lf(input);
+ mf = maildir_save_add(_ctx, fname, NULL);
+ if (_ctx->data.guid != NULL) {
+ maildir_save_set_dest_basename(_ctx, mf,
+ _ctx->data.guid);
+ }
+ }
+ } T_END;
+
+ if (!ctx->failed) {
+ _ctx->data.output = o_stream_create_fd_file(ctx->fd, 0, FALSE);
+ o_stream_set_name(_ctx->data.output, t_strdup_printf(
+ "%s/%s", ctx->tmpdir, ctx->file_last->tmp_name));
+ o_stream_cork(_ctx->data.output);
+ ctx->last_save_finished = FALSE;
+ }
+ return ctx->failed ? -1 : 0;
+}
+
+int maildir_save_continue(struct mail_save_context *_ctx)
+{
+ struct maildir_save_context *ctx = MAILDIR_SAVECTX(_ctx);
+
+ if (ctx->failed)
+ return -1;
+
+ if (index_storage_save_continue(_ctx, ctx->input,
+ ctx->cur_dest_mail) < 0) {
+ ctx->failed = TRUE;
+ return -1;
+ }
+ return 0;
+}
+
+static int maildir_save_finish_received_date(struct maildir_save_context *ctx,
+ const char *path)
+{
+ struct utimbuf buf;
+ struct stat st;
+
+ if (ctx->ctx.data.received_date != (time_t)-1) {
+ /* set the received_date by modifying mtime */
+ buf.actime = ioloop_time;
+ buf.modtime = ctx->ctx.data.received_date;
+
+ if (utime(path, &buf) < 0) {
+ mail_set_critical(ctx->ctx.dest_mail,
+ "utime(%s) failed: %m", path);
+ return -1;
+ }
+ } else if (ctx->fd != -1) {
+ if (fstat(ctx->fd, &st) == 0)
+ ctx->ctx.data.received_date = st.st_mtime;
+ else {
+ mail_set_critical(ctx->ctx.dest_mail,
+ "fstat(%s) failed: %m", path);
+ return -1;
+ }
+ } else {
+ /* hardlinked */
+ if (stat(path, &st) == 0)
+ ctx->ctx.data.received_date = st.st_mtime;
+ else {
+ mail_set_critical(ctx->ctx.dest_mail,
+ "stat(%s) failed: %m", path);
+ return -1;
+ }
+ }
+ return 0;
+}
+
+static void maildir_save_remove_last_filename(struct maildir_save_context *ctx)
+{
+ struct maildir_filename **fm;
+
+ index_storage_save_abort_last(&ctx->ctx, ctx->seq);
+ ctx->seq--;
+
+ for (fm = &ctx->files; (*fm)->next != NULL; fm = &(*fm)->next) ;
+ i_assert(*fm == ctx->file_last);
+ *fm = NULL;
+
+ ctx->files_tail = fm;
+ ctx->file_last = NULL;
+ ctx->files_count--;
+}
+
+void maildir_save_finish_keywords(struct mail_save_context *_ctx)
+{
+ struct maildir_save_context *ctx = MAILDIR_SAVECTX(_ctx);
+
+ ARRAY_TYPE(keyword_indexes) keyword_idx;
+ t_array_init(&keyword_idx, 8);
+ mail_index_lookup_keywords(ctx->ctx.transaction->view, ctx->seq,
+ &keyword_idx);
+
+ if (array_count(&keyword_idx) > 0) {
+ /* copy keywords */
+ p_array_init(&ctx->file_last->keywords, ctx->pool,
+ array_count(&keyword_idx));
+ array_copy(&ctx->file_last->keywords.arr, 0, &keyword_idx.arr, 0,
+ array_count(&keyword_idx));
+ ctx->have_keywords = TRUE;
+ }
+}
+
+static int maildir_save_finish_real(struct mail_save_context *_ctx)
+{
+ struct maildir_save_context *ctx = MAILDIR_SAVECTX(_ctx);
+ struct mail_storage *storage = &ctx->mbox->storage->storage;
+ const char *path, *output_errstr;
+ off_t real_size;
+ uoff_t size;
+ int output_errno;
+
+ ctx->last_save_finished = TRUE;
+ if (ctx->failed && ctx->fd == -1) {
+ /* tmp file creation failed */
+ return -1;
+ }
+
+ path = t_strconcat(ctx->tmpdir, "/", ctx->file_last->tmp_name, NULL);
+ if (o_stream_finish(_ctx->data.output) < 0) {
+ if (!mail_storage_set_error_from_errno(storage)) {
+ mail_set_critical(_ctx->dest_mail,
+ "write(%s) failed: %s", path,
+ o_stream_get_error(_ctx->data.output));
+ }
+ ctx->failed = TRUE;
+ }
+
+ if (_ctx->data.save_date != (time_t)-1) {
+ /* we can't change ctime, but we can add the date to cache */
+ struct index_mail *mail = INDEX_MAIL(_ctx->dest_mail);
+ uint32_t t = _ctx->data.save_date;
+
+ index_mail_cache_add(mail, MAIL_CACHE_SAVE_DATE, &t, sizeof(t));
+ }
+
+ if (maildir_save_finish_received_date(ctx, path) < 0)
+ ctx->failed = TRUE;
+
+ if (ctx->cur_dest_mail != NULL) {
+ index_mail_cache_parse_deinit(ctx->cur_dest_mail,
+ ctx->ctx.data.received_date,
+ !ctx->failed);
+ }
+ i_stream_unref(&ctx->input);
+
+ /* remember the size in case we want to add it to filename */
+ ctx->file_last->size = _ctx->data.output->offset;
+ if (ctx->cur_dest_mail == NULL ||
+ mail_get_virtual_size(ctx->cur_dest_mail,
+ &ctx->file_last->vsize) < 0)
+ ctx->file_last->vsize = UOFF_T_MAX;
+
+ output_errno = _ctx->data.output->stream_errno;
+ output_errstr = t_strdup(o_stream_get_error(_ctx->data.output));
+ o_stream_destroy(&_ctx->data.output);
+
+ maildir_save_finish_keywords(_ctx);
+
+ if (storage->set->parsed_fsync_mode != FSYNC_MODE_NEVER &&
+ !ctx->failed) {
+ if (fsync(ctx->fd) < 0) {
+ if (!mail_storage_set_error_from_errno(storage)) {
+ mail_set_critical(_ctx->dest_mail,
+ "fsync(%s) failed: %m", path);
+ }
+ ctx->failed = TRUE;
+ }
+ }
+ real_size = lseek(ctx->fd, 0, SEEK_END);
+ if (real_size == (off_t)-1) {
+ mail_set_critical(_ctx->dest_mail, "lseek(%s) failed: %m", path);
+ } else if (real_size != (off_t)ctx->file_last->size &&
+ (!maildir_filename_get_size(ctx->file_last->dest_basename,
+ MAILDIR_EXTRA_FILE_SIZE, &size) ||
+ size != ctx->file_last->size)) {
+ /* e.g. zlib plugin was used. the "physical size" must be in
+ the maildir filename, since stat() will return wrong size */
+ ctx->file_last->preserve_filename = FALSE;
+ /* preserve the GUID if needed */
+ if (ctx->file_last->guid == NULL)
+ ctx->file_last->guid = ctx->file_last->dest_basename;
+ /* reset the base name as well, just in case there's a
+ ,W=vsize */
+ ctx->file_last->dest_basename = ctx->file_last->tmp_name;
+ }
+ if (close(ctx->fd) < 0) {
+ if (!mail_storage_set_error_from_errno(storage)) {
+ mail_set_critical(_ctx->dest_mail,
+ "close(%s) failed: %m", path);
+ }
+ ctx->failed = TRUE;
+ }
+ ctx->fd = -1;
+
+ if (ctx->failed) {
+ /* delete the tmp file */
+ i_unlink_if_exists(path);
+
+ if (ENOQUOTA(output_errno)) {
+ mail_storage_set_error(storage,
+ MAIL_ERROR_NOQUOTA, MAIL_ERRSTR_NO_QUOTA);
+ } else if (output_errno != 0) {
+ mail_set_critical(_ctx->dest_mail,
+ "write(%s) failed: %s", path, output_errstr);
+ }
+
+ maildir_save_remove_last_filename(ctx);
+ return -1;
+ }
+
+ ctx->file_last = NULL;
+ return 0;
+}
+
+int maildir_save_finish(struct mail_save_context *ctx)
+{
+ int ret;
+
+ T_BEGIN {
+ ret = maildir_save_finish_real(ctx);
+ } T_END;
+ index_save_context_free(ctx);
+ return ret;
+}
+
+void maildir_save_cancel(struct mail_save_context *_ctx)
+{
+ struct maildir_save_context *ctx = MAILDIR_SAVECTX(_ctx);
+
+ ctx->failed = TRUE;
+ (void)maildir_save_finish(_ctx);
+}
+
+static void
+maildir_save_unlink_files(struct maildir_save_context *ctx)
+{
+ struct maildir_filename *mf;
+
+ for (mf = ctx->files; mf != NULL; mf = mf->next) T_BEGIN {
+ i_unlink(maildir_mf_get_path(ctx, mf));
+ } T_END;
+ ctx->files = NULL;
+}
+
+static int maildir_transaction_fsync_dirs(struct maildir_save_context *ctx,
+ bool new_changed, bool cur_changed)
+{
+ struct mail_storage *storage = &ctx->mbox->storage->storage;
+
+ if (storage->set->parsed_fsync_mode == FSYNC_MODE_NEVER)
+ return 0;
+
+ if (new_changed) {
+ if (fdatasync_path(ctx->newdir) < 0) {
+ mailbox_set_critical(&ctx->mbox->box,
+ "fdatasync_path(%s) failed: %m", ctx->newdir);
+ return -1;
+ }
+ }
+ if (cur_changed) {
+ if (fdatasync_path(ctx->curdir) < 0) {
+ mailbox_set_critical(&ctx->mbox->box,
+ "fdatasync_path(%s) failed: %m", ctx->curdir);
+ return -1;
+ }
+ }
+ return 0;
+}
+
+static int seq_range_cmp(const struct seq_range *r1, const struct seq_range *r2)
+{
+ if (r1->seq1 < r2->seq2)
+ return -1;
+ else if (r1->seq1 > r2->seq2)
+ return 1;
+ else
+ return 0;
+}
+
+static uint32_t
+maildir_save_set_recent_flags(struct maildir_save_context *ctx)
+{
+ struct maildir_mailbox *mbox = ctx->mbox;
+ ARRAY_TYPE(seq_range) saved_sorted_uids;
+ const struct seq_range *uids;
+ unsigned int i, count;
+ uint32_t uid;
+
+ count = array_count(&ctx->ctx.transaction->changes->saved_uids);
+ if (count == 0)
+ return 0;
+
+ t_array_init(&saved_sorted_uids, count);
+ array_append_array(&saved_sorted_uids,
+ &ctx->ctx.transaction->changes->saved_uids);
+ array_sort(&saved_sorted_uids, seq_range_cmp);
+
+ uids = array_get(&saved_sorted_uids, &count);
+ for (i = 0; i < count; i++) {
+ for (uid = uids[i].seq1; uid <= uids[i].seq2; uid++)
+ mailbox_recent_flags_set_uid(&mbox->box, uid);
+ }
+ return uids[count-1].seq2 + 1;
+}
+
+static int
+maildir_save_sync_index(struct maildir_save_context *ctx)
+{
+ struct mailbox_transaction_context *_t = ctx->ctx.transaction;
+ struct maildir_mailbox *mbox = ctx->mbox;
+ uint32_t first_uid, next_uid, first_recent_uid;
+ int ret;
+
+ /* we'll need to keep the lock past the sync deinit */
+ ret = maildir_uidlist_lock(mbox->uidlist);
+ i_assert(ret > 0);
+
+ if (maildir_sync_header_refresh(mbox) < 0)
+ return -1;
+ if ((ret = maildir_uidlist_refresh_fast_init(mbox->uidlist)) < 0)
+ return -1;
+
+ if (ret == 0) {
+ /* uidlist doesn't exist. make sure all existing message
+ are added to uidlist first. */
+ (void)maildir_storage_sync_force(mbox, 0);
+ }
+
+ if (maildir_sync_index_begin(mbox, NULL, &ctx->sync_ctx) < 0)
+ return -1;
+ ctx->keywords_sync_ctx =
+ maildir_sync_get_keywords_sync_ctx(ctx->sync_ctx);
+
+ /* now that uidlist is locked, make sure all the existing mails
+ have been added to index. we don't really look into the
+ maildir, just add all the new mails listed in
+ dovecot-uidlist to index. */
+ if (maildir_sync_index(ctx->sync_ctx, TRUE) < 0)
+ return -1;
+
+ /* if messages were added to index, assign them UIDs */
+ first_uid = maildir_uidlist_get_next_uid(mbox->uidlist);
+ i_assert(first_uid != 0);
+ mail_index_append_finish_uids(ctx->trans, first_uid,
+ &_t->changes->saved_uids);
+ i_assert(ctx->files_count == seq_range_count(&_t->changes->saved_uids));
+
+ /* these mails are all recent in our session */
+ T_BEGIN {
+ next_uid = maildir_save_set_recent_flags(ctx);
+ } T_END;
+
+ if ((mbox->box.flags & MAILBOX_FLAG_DROP_RECENT) != 0)
+ first_recent_uid = next_uid;
+ else if (ctx->last_nonrecent_uid != 0)
+ first_recent_uid = ctx->last_nonrecent_uid + 1;
+ else
+ first_recent_uid = 0;
+
+ if (first_recent_uid != 0) {
+ /* maildir_sync_index() dropped recent flags from
+ existing messages. we'll still need to drop recent
+ flags from these newly added messages. */
+ mail_index_update_header(ctx->trans,
+ offsetof(struct mail_index_header,
+ first_recent_uid),
+ &first_recent_uid,
+ sizeof(first_recent_uid), FALSE);
+ }
+ return 0;
+}
+
+static void
+maildir_save_rollback_index_changes(struct maildir_save_context *ctx)
+{
+ uint32_t seq;
+
+ if (ctx->seq == 0)
+ return;
+
+ for (seq = ctx->seq; seq >= ctx->first_seq; seq--)
+ mail_index_expunge(ctx->trans, seq);
+
+ mail_cache_transaction_reset(ctx->ctx.transaction->cache_trans);
+}
+
+static bool maildir_filename_has_conflict(struct maildir_filename *mf,
+ struct maildir_filename *prev_mf)
+{
+ if (strcmp(mf->dest_basename, prev_mf->dest_basename) == 0) {
+ /* already used this */
+ return TRUE;
+ }
+ if (prev_mf->guid != NULL &&
+ strcmp(mf->dest_basename, prev_mf->guid) == 0) {
+ /* previous filename also had a conflict */
+ return TRUE;
+ }
+ return FALSE;
+}
+
+static void
+maildir_filename_check_conflicts(struct maildir_save_context *ctx,
+ struct maildir_filename *mf,
+ struct maildir_filename *prev_mf)
+{
+ uoff_t size, vsize;
+
+ if (!ctx->locked_uidlist_refresh && ctx->locked) {
+ (void)maildir_uidlist_refresh(ctx->mbox->uidlist);
+ ctx->locked_uidlist_refresh = TRUE;
+ }
+
+ if (!maildir_filename_get_size(mf->dest_basename,
+ MAILDIR_EXTRA_FILE_SIZE, &size))
+ size = UOFF_T_MAX;
+ if (!maildir_filename_get_size(mf->dest_basename,
+ MAILDIR_EXTRA_VIRTUAL_SIZE, &vsize))
+ vsize = UOFF_T_MAX;
+
+ if (size != mf->size || vsize != mf->vsize ||
+ !ctx->locked_uidlist_refresh ||
+ (prev_mf != NULL && maildir_filename_has_conflict(mf, prev_mf)) ||
+ maildir_uidlist_get_full_filename(ctx->mbox->uidlist,
+ mf->dest_basename) != NULL) {
+ /* a) dest_basename didn't contain the (correct) size/vsize.
+ they're required for good performance.
+
+ b) file already exists. give it another name.
+ but preserve the size/vsize in the filename if possible */
+ if (mf->size == UOFF_T_MAX)
+ mf->size = size;
+ if (mf->vsize == UOFF_T_MAX)
+ mf->vsize = size;
+
+ mf->guid = mf->dest_basename;
+ mf->dest_basename = p_strdup(ctx->pool,
+ maildir_filename_generate());
+ mf->preserve_filename = FALSE;
+ }
+}
+
+static int
+maildir_filename_dest_basename_cmp(struct maildir_filename *const *f1,
+ struct maildir_filename *const *f2)
+{
+ return strcmp((*f1)->dest_basename, (*f2)->dest_basename);
+}
+
+static int
+maildir_save_move_files_to_newcur(struct maildir_save_context *ctx)
+{
+ ARRAY(struct maildir_filename *) files;
+ struct maildir_filename *mf, *prev_mf;
+ bool newdir, new_changed, cur_changed;
+ int ret;
+
+ /* put files into an array sorted by the destination filename.
+ this way we can easily check if there are duplicate destination
+ filenames within this transaction. */
+ t_array_init(&files, ctx->files_count);
+ for (mf = ctx->files; mf != NULL; mf = mf->next)
+ array_push_back(&files, &mf);
+ array_sort(&files, maildir_filename_dest_basename_cmp);
+
+ new_changed = cur_changed = FALSE;
+ prev_mf = NULL;
+ array_foreach_elem(&files, mf) {
+ T_BEGIN {
+ const char *dest;
+
+ if (mf->preserve_filename) {
+ maildir_filename_check_conflicts(ctx, mf,
+ prev_mf);
+ }
+
+ newdir = maildir_get_dest_filename(ctx, mf, &dest);
+ if (newdir)
+ new_changed = TRUE;
+ else
+ cur_changed = TRUE;
+ ret = maildir_file_move(ctx, mf, dest, newdir);
+ } T_END;
+ if (ret < 0)
+ return -1;
+ prev_mf = mf;
+ }
+
+ if (ctx->locked) {
+ i_assert(ctx->sync_ctx != NULL);
+ maildir_sync_set_new_msgs_count(ctx->sync_ctx,
+ array_count(&files));
+ }
+ return maildir_transaction_fsync_dirs(ctx, new_changed, cur_changed);
+}
+
+static void maildir_save_sync_uidlist(struct maildir_save_context *ctx)
+{
+ struct mailbox_transaction_context *t = ctx->ctx.transaction;
+ struct maildir_filename *mf;
+ struct seq_range_iter iter;
+ enum maildir_uidlist_rec_flag flags;
+ struct maildir_uidlist_rec *rec;
+ unsigned int n = 0;
+ uint32_t uid;
+ bool newdir, bret;
+ int ret;
+
+ seq_range_array_iter_init(&iter, &t->changes->saved_uids);
+ for (mf = ctx->files; mf != NULL; mf = mf->next) T_BEGIN {
+ const char *dest;
+
+ bret = seq_range_array_iter_nth(&iter, n++, &uid);
+ i_assert(bret);
+
+ newdir = maildir_get_dest_filename(ctx, mf, &dest);
+ flags = MAILDIR_UIDLIST_REC_FLAG_RECENT;
+ if (newdir)
+ flags |= MAILDIR_UIDLIST_REC_FLAG_NEW_DIR;
+ ret = maildir_uidlist_sync_next_uid(ctx->uidlist_sync_ctx,
+ dest, uid, flags, &rec);
+ i_assert(ret > 0);
+ i_assert(rec != NULL);
+ if (mf->guid != NULL) {
+ maildir_uidlist_sync_set_ext(ctx->uidlist_sync_ctx, rec,
+ MAILDIR_UIDLIST_REC_EXT_GUID, mf->guid);
+ }
+ if (mf->pop3_uidl != NULL) {
+ maildir_uidlist_sync_set_ext(ctx->uidlist_sync_ctx, rec,
+ MAILDIR_UIDLIST_REC_EXT_POP3_UIDL,
+ mf->pop3_uidl);
+ }
+ if (mf->pop3_order > 0) {
+ maildir_uidlist_sync_set_ext(ctx->uidlist_sync_ctx, rec,
+ MAILDIR_UIDLIST_REC_EXT_POP3_ORDER,
+ t_strdup_printf("%u", mf->pop3_order));
+ }
+ } T_END;
+ i_assert(!seq_range_array_iter_nth(&iter, n, &uid));
+}
+
+int maildir_transaction_save_commit_pre(struct mail_save_context *_ctx)
+{
+ struct maildir_save_context *ctx = MAILDIR_SAVECTX(_ctx);
+ struct mailbox_transaction_context *_t = _ctx->transaction;
+ enum maildir_uidlist_sync_flags sync_flags;
+ int ret;
+
+ i_assert(_ctx->data.output == NULL);
+ i_assert(ctx->last_save_finished);
+
+ if (ctx->files_count == 0)
+ return 0;
+
+ sync_flags = MAILDIR_UIDLIST_SYNC_PARTIAL |
+ MAILDIR_UIDLIST_SYNC_NOREFRESH;
+
+ if ((_t->flags & MAILBOX_TRANSACTION_FLAG_ASSIGN_UIDS) != 0) {
+ /* we want to assign UIDs, we must lock uidlist */
+ } else if (ctx->have_keywords) {
+ /* keywords file updating relies on uidlist lock. */
+ } else if (ctx->have_preserved_filenames) {
+ /* we're trying to use some potentially existing filenames.
+ we must lock to avoid race conditions where two sessions
+ try to save the same filename. */
+ } else {
+ /* no requirement to lock uidlist. if we happen to get a lock,
+ assign uids. */
+ sync_flags |= MAILDIR_UIDLIST_SYNC_TRYLOCK;
+ }
+ ret = maildir_uidlist_sync_init(ctx->mbox->uidlist, sync_flags,
+ &ctx->uidlist_sync_ctx);
+ if (ret > 0) {
+ ctx->locked = TRUE;
+ if (maildir_save_sync_index(ctx) < 0) {
+ maildir_transaction_save_rollback(_ctx);
+ return -1;
+ }
+ } else if (ret == 0 &&
+ (sync_flags & MAILDIR_UIDLIST_SYNC_TRYLOCK) != 0) {
+ ctx->locked = FALSE;
+ i_assert(ctx->uidlist_sync_ctx == NULL);
+ /* since we couldn't lock uidlist, we'll have to drop the
+ appends to index. */
+ maildir_save_rollback_index_changes(ctx);
+ } else {
+ maildir_transaction_save_rollback(_ctx);
+ return -1;
+ }
+
+ T_BEGIN {
+ ret = maildir_save_move_files_to_newcur(ctx);
+ } T_END;
+ if (ctx->locked) {
+ if (ret == 0) {
+ /* update dovecot-uidlist file. */
+ maildir_save_sync_uidlist(ctx);
+ }
+
+ if (maildir_uidlist_sync_deinit(&ctx->uidlist_sync_ctx,
+ ret == 0) < 0)
+ ret = -1;
+ }
+
+ _t->changes->uid_validity =
+ maildir_uidlist_get_uid_validity(ctx->mbox->uidlist);
+
+ if (ctx->locked) {
+ /* It doesn't matter if index syncing fails */
+ ctx->keywords_sync_ctx = NULL;
+ if (ret < 0)
+ maildir_sync_index_rollback(&ctx->sync_ctx);
+ else
+ (void)maildir_sync_index_commit(&ctx->sync_ctx);
+ }
+
+ if (ret < 0) {
+ ctx->keywords_sync_ctx = !ctx->have_keywords ? NULL :
+ maildir_keywords_sync_init(ctx->mbox->keywords,
+ ctx->mbox->box.index);
+
+ /* unlink the files we just moved in an attempt to rollback
+ the transaction. uidlist is still locked, so at least other
+ Dovecot instances haven't yet seen the files. we need
+ to have the keywords sync context to be able to generate
+ the destination filenames if keywords were used. */
+ maildir_save_unlink_files(ctx);
+
+ if (ctx->keywords_sync_ctx != NULL)
+ maildir_keywords_sync_deinit(&ctx->keywords_sync_ctx);
+ /* returning failure finishes the save_context */
+ maildir_transaction_save_rollback(_ctx);
+ return -1;
+ }
+ return 0;
+}
+
+void maildir_transaction_save_commit_post(struct mail_save_context *_ctx,
+ struct mail_index_transaction_commit_result *result ATTR_UNUSED)
+{
+ struct maildir_save_context *ctx = MAILDIR_SAVECTX(_ctx);
+
+ _ctx->transaction = NULL; /* transaction is already freed */
+
+ if (ctx->locked)
+ maildir_uidlist_unlock(ctx->mbox->uidlist);
+ pool_unref(&ctx->pool);
+}
+
+void maildir_transaction_save_rollback(struct mail_save_context *_ctx)
+{
+ struct maildir_save_context *ctx = MAILDIR_SAVECTX(_ctx);
+
+ i_assert(_ctx->data.output == NULL);
+
+ if (!ctx->last_save_finished)
+ maildir_save_cancel(&ctx->ctx);
+
+ /* delete files in tmp/ */
+ maildir_save_unlink_files(ctx);
+
+ if (ctx->uidlist_sync_ctx != NULL)
+ (void)maildir_uidlist_sync_deinit(&ctx->uidlist_sync_ctx, FALSE);
+ if (ctx->sync_ctx != NULL)
+ maildir_sync_index_rollback(&ctx->sync_ctx);
+ if (ctx->locked)
+ maildir_uidlist_unlock(ctx->mbox->uidlist);
+
+ pool_unref(&ctx->pool);
+}
diff --git a/src/lib-storage/index/maildir/maildir-settings.c b/src/lib-storage/index/maildir/maildir-settings.c
new file mode 100644
index 0000000..d986d18
--- /dev/null
+++ b/src/lib-storage/index/maildir/maildir-settings.c
@@ -0,0 +1,46 @@
+/* Copyright (c) 2005-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "settings-parser.h"
+#include "mail-storage-settings.h"
+#include "maildir-settings.h"
+
+#include <stddef.h>
+
+#undef DEF
+#define DEF(type, name) \
+ SETTING_DEFINE_STRUCT_##type(#name, name, struct maildir_settings)
+
+static const struct setting_define maildir_setting_defines[] = {
+ DEF(BOOL, maildir_copy_with_hardlinks),
+ DEF(BOOL, maildir_very_dirty_syncs),
+ DEF(BOOL, maildir_broken_filename_sizes),
+ DEF(BOOL, maildir_empty_new),
+
+ SETTING_DEFINE_LIST_END
+};
+
+static const struct maildir_settings maildir_default_settings = {
+ .maildir_copy_with_hardlinks = TRUE,
+ .maildir_very_dirty_syncs = FALSE,
+ .maildir_broken_filename_sizes = FALSE,
+ .maildir_empty_new = FALSE
+};
+
+static const struct setting_parser_info maildir_setting_parser_info = {
+ .module_name = "maildir",
+ .defines = maildir_setting_defines,
+ .defaults = &maildir_default_settings,
+
+ .type_offset = SIZE_MAX,
+ .struct_size = sizeof(struct maildir_settings),
+
+ .parent_offset = SIZE_MAX,
+ .parent = &mail_user_setting_parser_info
+};
+
+const struct setting_parser_info *maildir_get_setting_parser_info(void)
+{
+ return &maildir_setting_parser_info;
+}
+
diff --git a/src/lib-storage/index/maildir/maildir-settings.h b/src/lib-storage/index/maildir/maildir-settings.h
new file mode 100644
index 0000000..cfcb732
--- /dev/null
+++ b/src/lib-storage/index/maildir/maildir-settings.h
@@ -0,0 +1,13 @@
+#ifndef MAILDIR_SETTINGS_H
+#define MAILDIR_SETTINGS_H
+
+struct maildir_settings {
+ bool maildir_copy_with_hardlinks;
+ bool maildir_very_dirty_syncs;
+ bool maildir_broken_filename_sizes;
+ bool maildir_empty_new;
+};
+
+const struct setting_parser_info *maildir_get_setting_parser_info(void);
+
+#endif
diff --git a/src/lib-storage/index/maildir/maildir-storage.c b/src/lib-storage/index/maildir/maildir-storage.c
new file mode 100644
index 0000000..e65579f
--- /dev/null
+++ b/src/lib-storage/index/maildir/maildir-storage.c
@@ -0,0 +1,749 @@
+/* Copyright (c) 2002-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "ioloop.h"
+#include "mkdir-parents.h"
+#include "eacces-error.h"
+#include "unlink-old-files.h"
+#include "mailbox-uidvalidity.h"
+#include "mailbox-list-private.h"
+#include "maildir-storage.h"
+#include "maildir-uidlist.h"
+#include "maildir-keywords.h"
+#include "maildir-sync.h"
+#include "index-mail.h"
+
+#include <sys/stat.h>
+
+#define MAILDIR_LIST_CONTEXT(obj) \
+ MODULE_CONTEXT(obj, maildir_mailbox_list_module)
+#define MAILDIR_SUBFOLDER_FILENAME "maildirfolder"
+
+struct maildir_mailbox_list_context {
+ union mailbox_list_module_context module_ctx;
+ const struct maildir_settings *set;
+};
+
+extern struct mail_storage maildir_storage;
+extern struct mailbox maildir_mailbox;
+
+static struct event_category event_category_maildir = {
+ .name = "maildir",
+ .parent = &event_category_storage,
+};
+
+static MODULE_CONTEXT_DEFINE_INIT(maildir_mailbox_list_module,
+ &mailbox_list_module_register);
+static const char *maildir_subdirs[] = { "cur", "new", "tmp" };
+
+static void maildir_mailbox_close(struct mailbox *box);
+
+static struct mail_storage *maildir_storage_alloc(void)
+{
+ struct maildir_storage *storage;
+ pool_t pool;
+
+ pool = pool_alloconly_create("maildir storage", 512+256);
+ storage = p_new(pool, struct maildir_storage, 1);
+ storage->storage = maildir_storage;
+ storage->storage.pool = pool;
+ return &storage->storage;
+}
+
+static int
+maildir_storage_create(struct mail_storage *_storage, struct mail_namespace *ns,
+ const char **error_r ATTR_UNUSED)
+{
+ struct maildir_storage *storage = MAILDIR_STORAGE(_storage);
+ struct mailbox_list *list = ns->list;
+ const char *dir;
+
+ storage->set = mail_namespace_get_driver_settings(ns, _storage);
+
+ storage->temp_prefix = p_strdup(_storage->pool,
+ mailbox_list_get_temp_prefix(list));
+
+ if (list->set.control_dir == NULL && list->set.inbox_path == NULL &&
+ (ns->flags & NAMESPACE_FLAG_INBOX_ANY) != 0) {
+ /* put the temp files into tmp/ directory preferably */
+ storage->temp_prefix = p_strconcat(_storage->pool, "tmp/",
+ storage->temp_prefix, NULL);
+ dir = mailbox_list_get_root_forced(list, MAILBOX_LIST_PATH_TYPE_DIR);
+ } else {
+ /* control dir should also be writable */
+ dir = mailbox_list_get_root_forced(list, MAILBOX_LIST_PATH_TYPE_CONTROL);
+ }
+ _storage->temp_path_prefix = p_strconcat(_storage->pool, dir, "/",
+ storage->temp_prefix, NULL);
+ return 0;
+}
+
+static void maildir_storage_get_list_settings(const struct mail_namespace *ns,
+ struct mailbox_list_settings *set)
+{
+ if (set->layout == NULL)
+ set->layout = MAILBOX_LIST_NAME_MAILDIRPLUSPLUS;
+ if (set->subscription_fname == NULL)
+ set->subscription_fname = MAILDIR_SUBSCRIPTION_FILE_NAME;
+
+ if (set->inbox_path == NULL && *set->maildir_name == '\0' &&
+ (strcmp(set->layout, MAILBOX_LIST_NAME_MAILDIRPLUSPLUS) == 0 ||
+ strcmp(set->layout, MAILBOX_LIST_NAME_FS) == 0) &&
+ (ns->flags & NAMESPACE_FLAG_INBOX_ANY) != 0) {
+ /* Maildir++ INBOX is the Maildir base itself */
+ set->inbox_path = set->root_dir;
+ }
+}
+
+static const char *
+maildir_storage_find_root_dir(const struct mail_namespace *ns)
+{
+ bool debug = ns->mail_set->mail_debug;
+ const char *home, *path;
+
+ /* we'll need to figure out the maildir location ourself.
+ It's ~/Maildir unless we are chrooted. */
+ if (ns->owner != NULL &&
+ mail_user_get_home(ns->owner, &home) > 0) {
+ path = t_strconcat(home, "/Maildir", NULL);
+ if (access(path, R_OK|W_OK|X_OK) == 0) {
+ if (debug)
+ i_debug("maildir: root exists (%s)", path);
+ return path;
+ }
+ if (debug)
+ i_debug("maildir: access(%s, rwx): failed: %m", path);
+ } else {
+ if (debug)
+ i_debug("maildir: Home directory not set");
+ if (access("/cur", R_OK|W_OK|X_OK) == 0) {
+ if (debug)
+ i_debug("maildir: /cur exists, assuming chroot");
+ return "/";
+ }
+ }
+ return NULL;
+}
+
+static bool maildir_storage_autodetect(const struct mail_namespace *ns,
+ struct mailbox_list_settings *set)
+{
+ bool debug = ns->mail_set->mail_debug;
+ struct stat st;
+ const char *path, *root_dir;
+
+ if (set->root_dir != NULL)
+ root_dir = set->root_dir;
+ else {
+ root_dir = maildir_storage_find_root_dir(ns);
+ if (root_dir == NULL) {
+ if (debug)
+ i_debug("maildir: couldn't find root dir");
+ return FALSE;
+ }
+ }
+
+ path = t_strconcat(root_dir, "/cur", NULL);
+ if (stat(path, &st) < 0) {
+ if (debug)
+ i_debug("maildir autodetect: stat(%s) failed: %m", path);
+ return FALSE;
+ }
+
+ if (!S_ISDIR(st.st_mode)) {
+ if (debug)
+ i_debug("maildir autodetect: %s not a directory", path);
+ return FALSE;
+ }
+
+ set->root_dir = root_dir;
+ maildir_storage_get_list_settings(ns, set);
+ return TRUE;
+}
+
+static int
+mkdir_verify(struct mailbox *box, const char *dir, bool verify)
+{
+ const struct mailbox_permissions *perm;
+ struct stat st;
+
+ if (verify) {
+ if (stat(dir, &st) == 0)
+ return 0;
+
+ if (errno != ENOENT) {
+ mailbox_set_critical(box, "stat(%s) failed: %m", dir);
+ return -1;
+ }
+ }
+
+ perm = mailbox_get_permissions(box);
+ if (mkdir_parents_chgrp(dir, perm->dir_create_mode,
+ perm->file_create_gid,
+ perm->file_create_gid_origin) == 0)
+ return 0;
+
+ if (errno == EEXIST) {
+ if (verify)
+ return 0;
+ mail_storage_set_error(box->storage, MAIL_ERROR_EXISTS,
+ "Mailbox already exists");
+ } else if (errno == ENOENT) {
+ mail_storage_set_error(box->storage, MAIL_ERROR_NOTFOUND,
+ "Mailbox was deleted while it was being created");
+ } else if (errno == EACCES) {
+ if (box->list->ns->type == MAIL_NAMESPACE_TYPE_SHARED) {
+ /* shared namespace, don't log permission errors */
+ mail_storage_set_error(box->storage, MAIL_ERROR_PERM,
+ MAIL_ERRSTR_NO_PERMISSION);
+ return -1;
+ }
+ mailbox_set_critical(box, "%s",
+ mail_error_create_eacces_msg("mkdir", dir));
+ } else {
+ mailbox_set_critical(box, "mkdir(%s) failed: %m", dir);
+ }
+ return -1;
+}
+
+static int maildir_check_tmp(struct mail_storage *storage, const char *dir)
+{
+ unsigned int interval = storage->set->mail_temp_scan_interval;
+ const char *path;
+ struct stat st;
+
+ /* if tmp/ directory exists, we need to clean it up once in a while */
+ path = t_strconcat(dir, "/tmp", NULL);
+ if (stat(path, &st) < 0) {
+ if (errno == ENOENT || errno == ENAMETOOLONG)
+ return 0;
+ if (errno == EACCES) {
+ mail_storage_set_critical(storage, "%s",
+ mail_error_eacces_msg("stat", path));
+ return -1;
+ }
+ mail_storage_set_critical(storage, "stat(%s) failed: %m", path);
+ return -1;
+ }
+
+ if (interval == 0) {
+ /* disabled */
+ } else if (st.st_atime > st.st_ctime + MAILDIR_TMP_DELETE_SECS) {
+ /* the directory should be empty. we won't do anything
+ until ctime changes. */
+ } else if (st.st_atime < ioloop_time - (time_t)interval) {
+ /* time to scan */
+ (void)unlink_old_files(path, "",
+ ioloop_time - MAILDIR_TMP_DELETE_SECS);
+ }
+ return 1;
+}
+
+/* create or fix maildir, ignore if it already exists */
+static int create_maildir_subdirs(struct mailbox *box, bool verify)
+{
+ const char *path, *box_path;
+ unsigned int i;
+ enum mail_error error;
+ int ret = 0;
+
+ if (mailbox_get_path_to(box, MAILBOX_LIST_PATH_TYPE_MAILBOX,
+ &box_path) < 0)
+ return -1;
+
+ for (i = 0; i < N_ELEMENTS(maildir_subdirs); i++) {
+ path = t_strconcat(box_path, "/", maildir_subdirs[i], NULL);
+ if (mkdir_verify(box, path, verify) < 0) {
+ error = mailbox_get_last_mail_error(box);
+ if (error != MAIL_ERROR_EXISTS)
+ return -1;
+ /* try to create all of the directories in case one
+ of them doesn't exist */
+ ret = -1;
+ }
+ }
+ return ret;
+}
+
+static void maildir_lock_touch_timeout(struct maildir_mailbox *mbox)
+{
+ (void)maildir_uidlist_lock_touch(mbox->uidlist);
+}
+
+static struct mailbox *
+maildir_mailbox_alloc(struct mail_storage *storage, struct mailbox_list *list,
+ const char *vname, enum mailbox_flags flags)
+{
+ struct maildir_mailbox *mbox;
+ pool_t pool;
+
+ pool = pool_alloconly_create("maildir mailbox", 1024*3);
+ mbox = p_new(pool, struct maildir_mailbox, 1);
+ mbox->box = maildir_mailbox;
+ mbox->box.pool = pool;
+ mbox->box.storage = storage;
+ mbox->box.list = list;
+ mbox->box.mail_vfuncs = &maildir_mail_vfuncs;
+ mbox->maildir_list_index_ext_id = (uint32_t)-1;
+
+ index_storage_mailbox_alloc(&mbox->box, vname, flags, MAIL_INDEX_PREFIX);
+
+ mbox->storage = MAILDIR_STORAGE(storage);
+ return &mbox->box;
+}
+
+static int maildir_mailbox_open_existing(struct mailbox *box)
+{
+ struct maildir_mailbox *mbox = MAILDIR_MAILBOX(box);
+
+ mbox->uidlist = maildir_uidlist_init(mbox);
+ mbox->keywords = maildir_keywords_init(mbox);
+
+ if ((box->flags & MAILBOX_FLAG_KEEP_LOCKED) != 0) {
+ if (maildir_uidlist_lock(mbox->uidlist) <= 0) {
+ maildir_mailbox_close(box);
+ return -1;
+ }
+ mbox->keep_lock_to = timeout_add(MAILDIR_LOCK_TOUCH_SECS * 1000,
+ maildir_lock_touch_timeout,
+ mbox);
+ }
+
+ if (index_storage_mailbox_open(box, FALSE) < 0) {
+ maildir_mailbox_close(box);
+ return -1;
+ }
+
+ mbox->maildir_ext_id =
+ mail_index_ext_register(mbox->box.index, "maildir",
+ sizeof(mbox->maildir_hdr), 0, 0);
+ return 0;
+}
+
+static bool maildir_storage_is_readonly(struct mailbox *box)
+{
+ struct maildir_mailbox *mbox = MAILDIR_MAILBOX(box);
+
+ if (index_storage_is_readonly(box))
+ return TRUE;
+
+ if (maildir_is_backend_readonly(mbox)) {
+ /* return read-only only if there are no private flags
+ (that are stored in index files) */
+ if (mailbox_get_private_flags_mask(box) == 0)
+ return TRUE;
+ }
+ return FALSE;
+}
+
+static int
+maildir_mailbox_exists(struct mailbox *box, bool auto_boxes,
+ enum mailbox_existence *existence_r)
+{
+ if (auto_boxes && mailbox_is_autocreated(box)) {
+ *existence_r = MAILBOX_EXISTENCE_SELECT;
+ return 0;
+ }
+
+ return index_storage_mailbox_exists_full(box, "cur", existence_r);
+}
+
+static int maildir_mailbox_open(struct mailbox *box)
+{
+ const char *box_path = mailbox_get_path(box);
+ const char *root_dir;
+ struct stat st;
+ int ret;
+
+ /* begin by checking if tmp/ directory exists and if it should be
+ cleaned up. */
+ ret = maildir_check_tmp(box->storage, box_path);
+ if (ret > 0) {
+ /* exists */
+ return maildir_mailbox_open_existing(box);
+ }
+ if (ret < 0)
+ return -1;
+
+ /* tmp/ directory doesn't exist. does the maildir? autocreate missing
+ dirs only with Maildir++ and imapdir layouts. */
+ if (strcmp(box->list->name, MAILBOX_LIST_NAME_MAILDIRPLUSPLUS) != 0 &&
+ strcmp(box->list->name, MAILBOX_LIST_NAME_IMAPDIR) != 0) {
+ mail_storage_set_error(box->storage, MAIL_ERROR_NOTFOUND,
+ T_MAIL_ERR_MAILBOX_NOT_FOUND(box->vname));
+ return -1;
+ }
+ root_dir = mailbox_list_get_root_forced(box->list,
+ MAILBOX_LIST_PATH_TYPE_MAILBOX);
+ if (strcmp(box_path, root_dir) == 0 && !box->inbox_any) {
+ /* root directory for some namespace. */
+ errno = ENOENT;
+ } else if (stat(box_path, &st) == 0) {
+ /* yes, we'll need to create the missing dirs */
+ if (create_maildir_subdirs(box, TRUE) < 0)
+ return -1;
+
+ return maildir_mailbox_open_existing(box);
+ }
+
+ if (errno == ENOENT || errno == ENAMETOOLONG) {
+ mail_storage_set_error(box->storage, MAIL_ERROR_NOTFOUND,
+ T_MAIL_ERR_MAILBOX_NOT_FOUND(box->vname));
+ return -1;
+ } else {
+ mailbox_set_critical(box, "stat(%s) failed: %m", box_path);
+ return -1;
+ }
+}
+
+static int maildir_create_shared(struct mailbox *box)
+{
+ const struct mailbox_permissions *perm = mailbox_get_permissions(box);
+ const char *path;
+ mode_t old_mask;
+ int fd, ret;
+
+ ret = mailbox_get_path_to(box, MAILBOX_LIST_PATH_TYPE_MAILBOX,
+ &path);
+ if (ret < 0)
+ return -1;
+ i_assert(ret > 0);
+
+ old_mask = umask(0);
+ path = t_strconcat(path, "/dovecot-shared", NULL);
+ fd = open(path, O_WRONLY | O_CREAT, perm->file_create_mode);
+ umask(old_mask);
+
+ if (fd == -1) {
+ mailbox_set_critical(box, "open(%s) failed: %m", path);
+ return -1;
+ }
+
+ if (fchown(fd, (uid_t)-1, perm->file_create_gid) < 0) {
+ if (errno == EPERM) {
+ mailbox_set_critical(box, "%s",
+ eperm_error_get_chgrp("fchown", path,
+ perm->file_create_gid,
+ perm->file_create_gid_origin));
+ } else {
+ mailbox_set_critical(box,
+ "fchown(%s) failed: %m", path);
+ }
+ }
+ i_close_fd(&fd);
+ return 0;
+}
+
+static int
+maildir_mailbox_update(struct mailbox *box, const struct mailbox_update *update)
+{
+ struct maildir_mailbox *mbox = MAILDIR_MAILBOX(box);
+ struct maildir_uidlist *uidlist;
+ bool locked = FALSE;
+ int ret = 0;
+
+ if (!box->opened) {
+ if (mailbox_open(box) < 0)
+ return -1;
+ }
+ uidlist = mbox->uidlist;
+
+ if (update->uid_validity != 0 || update->min_next_uid != 0 ||
+ !guid_128_is_empty(update->mailbox_guid)) {
+ if (maildir_uidlist_lock(uidlist) <= 0)
+ return -1;
+
+ locked = TRUE;
+ if (!guid_128_is_empty(update->mailbox_guid))
+ maildir_uidlist_set_mailbox_guid(uidlist, update->mailbox_guid);
+ if (update->uid_validity != 0)
+ maildir_uidlist_set_uid_validity(uidlist, update->uid_validity);
+ if (update->min_next_uid != 0) {
+ maildir_uidlist_set_next_uid(uidlist, update->min_next_uid,
+ FALSE);
+ }
+ ret = maildir_uidlist_update(uidlist);
+ }
+ if (ret == 0)
+ ret = index_storage_mailbox_update(box, update);
+ if (locked)
+ maildir_uidlist_unlock(uidlist);
+ return ret;
+}
+
+static int maildir_create_maildirfolder_file(struct mailbox *box)
+{
+ const struct mailbox_permissions *perm;
+ const char *path;
+ mode_t old_mask;
+ int fd;
+
+ /* Maildir++ spec wants that maildirfolder named file is created for
+ all subfolders. Do this only with Maildir++ layout. */
+ if (strcmp(box->list->name, MAILBOX_LIST_NAME_MAILDIRPLUSPLUS) != 0)
+ return 0;
+ perm = mailbox_get_permissions(box);
+
+ path = t_strconcat(mailbox_get_path(box),
+ "/"MAILDIR_SUBFOLDER_FILENAME, NULL);
+ old_mask = umask(0);
+ fd = open(path, O_CREAT | O_WRONLY, perm->file_create_mode);
+ umask(old_mask);
+ if (fd != -1) {
+ /* ok */
+ } else if (errno == ENOENT) {
+ mail_storage_set_error(box->storage, MAIL_ERROR_NOTFOUND,
+ "Mailbox was deleted while it was being created");
+ return -1;
+ } else {
+ mailbox_set_critical(box, "open(%s, O_CREAT) failed: %m", path);
+ return -1;
+ }
+
+ if (perm->file_create_gid != (gid_t)-1) {
+ if (fchown(fd, (uid_t)-1, perm->file_create_gid) == 0) {
+ /* ok */
+ } else if (errno == EPERM) {
+ mailbox_set_critical(box, "%s",
+ eperm_error_get_chgrp("fchown", path,
+ perm->file_create_gid,
+ perm->file_create_gid_origin));
+ } else {
+ mailbox_set_critical(box, "fchown(%s) failed: %m", path);
+ }
+ }
+ i_close_fd(&fd);
+ return 0;
+}
+
+static int
+maildir_mailbox_create(struct mailbox *box, const struct mailbox_update *update,
+ bool directory)
+{
+ const char *root_dir, *shared_path;
+ /* allow physical location to exist when we rebuild list index, this
+ happens with LAYOUT=INDEX only. */
+ bool verify = box->storage->rebuilding_list_index;
+ struct stat st;
+ int ret;
+
+ if ((ret = index_storage_mailbox_create(box, directory)) <= 0)
+ return ret;
+ ret = 0;
+ /* the maildir is created now. finish the creation as best as we can */
+ if (create_maildir_subdirs(box, verify) < 0)
+ ret = -1;
+ if (maildir_create_maildirfolder_file(box) < 0)
+ ret = -1;
+ /* if dovecot-shared exists in the root dir, copy it to newly
+ created mailboxes */
+ root_dir = mailbox_list_get_root_forced(box->list,
+ MAILBOX_LIST_PATH_TYPE_MAILBOX);
+ shared_path = t_strconcat(root_dir, "/dovecot-shared", NULL);
+ if (stat(shared_path, &st) == 0) {
+ if (maildir_create_shared(box) < 0)
+ ret = -1;
+ }
+ if (update != NULL) {
+ if (maildir_mailbox_update(box, update) < 0)
+ ret = -1;
+ }
+ return ret;
+}
+
+static int
+maildir_mailbox_get_metadata(struct mailbox *box,
+ enum mailbox_metadata_items items,
+ struct mailbox_metadata *metadata_r)
+{
+ struct maildir_mailbox *mbox = MAILDIR_MAILBOX(box);
+
+ if (index_mailbox_get_metadata(box, items, metadata_r) < 0)
+ return -1;
+
+ if ((items & MAILBOX_METADATA_GUID) != 0) {
+ if (maildir_uidlist_get_mailbox_guid(mbox->uidlist,
+ metadata_r->guid) < 0)
+ return -1;
+ }
+ return 0;
+}
+
+static void maildir_mailbox_close(struct mailbox *box)
+{
+ struct maildir_mailbox *mbox = MAILDIR_MAILBOX(box);
+
+ if (mbox->keep_lock_to != NULL) {
+ maildir_uidlist_unlock(mbox->uidlist);
+ timeout_remove(&mbox->keep_lock_to);
+ }
+
+ if (mbox->flags_view != NULL)
+ mail_index_view_close(&mbox->flags_view);
+ if (mbox->keywords != NULL)
+ maildir_keywords_deinit(&mbox->keywords);
+ if (mbox->uidlist != NULL)
+ maildir_uidlist_deinit(&mbox->uidlist);
+ index_storage_mailbox_close(box);
+}
+
+static void maildir_notify_changes(struct mailbox *box)
+{
+ struct maildir_mailbox *mbox = MAILDIR_MAILBOX(box);
+ const char *box_path = mailbox_get_path(box);
+
+ if (box->notify_callback == NULL)
+ mailbox_watch_remove_all(&mbox->box);
+ else {
+ mailbox_watch_add(&mbox->box,
+ t_strconcat(box_path, "/new", NULL));
+ mailbox_watch_add(&mbox->box,
+ t_strconcat(box_path, "/cur", NULL));
+ }
+}
+
+static bool
+maildir_is_internal_name(struct mailbox_list *list ATTR_UNUSED,
+ const char *name)
+{
+ return strcmp(name, "cur") == 0 ||
+ strcmp(name, "new") == 0 ||
+ strcmp(name, "tmp") == 0;
+}
+
+static void maildir_storage_add_list(struct mail_storage *storage,
+ struct mailbox_list *list)
+{
+ struct maildir_mailbox_list_context *mlist;
+
+ mlist = p_new(list->pool, struct maildir_mailbox_list_context, 1);
+ mlist->module_ctx.super = list->v;
+ mlist->set = mail_namespace_get_driver_settings(list->ns, storage);
+
+ list->v.is_internal_name = maildir_is_internal_name;
+ MODULE_CONTEXT_SET(list, maildir_mailbox_list_module, mlist);
+}
+
+uint32_t maildir_get_uidvalidity_next(struct mailbox_list *list)
+{
+ const char *path;
+
+ path = mailbox_list_get_root_forced(list, MAILBOX_LIST_PATH_TYPE_CONTROL);
+ path = t_strconcat(path, "/"MAILDIR_UIDVALIDITY_FNAME, NULL);
+ return mailbox_uidvalidity_next(list, path);
+}
+
+static enum mail_flags maildir_get_private_flags_mask(struct mailbox *box)
+{
+ struct maildir_mailbox *mbox = MAILDIR_MAILBOX(box);
+ const char *path, *path2;
+ struct stat st;
+
+ if (mbox->private_flags_mask_set)
+ return mbox->_private_flags_mask;
+ mbox->private_flags_mask_set = TRUE;
+
+ path = mailbox_list_get_root_forced(box->list, MAILBOX_LIST_PATH_TYPE_MAILBOX);
+ if (box->list->set.index_pvt_dir != NULL) {
+ /* private index directory is set. we'll definitely have
+ private flags. */
+ mbox->_private_flags_mask = MAIL_SEEN;
+ } else if (!mailbox_list_get_root_path(box->list,
+ MAILBOX_LIST_PATH_TYPE_INDEX,
+ &path2) ||
+ strcmp(path, path2) == 0) {
+ /* no separate index directory. we can't have private flags,
+ so don't even bother checking if dovecot-shared exists */
+ } else {
+ path = t_strconcat(mailbox_get_path(box),
+ "/dovecot-shared", NULL);
+ if (stat(path, &st) == 0)
+ mbox->_private_flags_mask = MAIL_SEEN;
+ }
+ return mbox->_private_flags_mask;
+}
+
+bool maildir_is_backend_readonly(struct maildir_mailbox *mbox)
+{
+ if (!mbox->backend_readonly_set) {
+ const char *box_path = mailbox_get_path(&mbox->box);
+
+ mbox->backend_readonly_set = TRUE;
+ if (access(t_strconcat(box_path, "/cur", NULL), W_OK) < 0 &&
+ errno == EACCES)
+ mbox->backend_readonly = TRUE;
+ }
+ return mbox->backend_readonly;
+}
+
+struct mail_storage maildir_storage = {
+ .name = MAILDIR_STORAGE_NAME,
+ .class_flags = MAIL_STORAGE_CLASS_FLAG_FILE_PER_MSG |
+ MAIL_STORAGE_CLASS_FLAG_HAVE_MAIL_GUIDS |
+ MAIL_STORAGE_CLASS_FLAG_HAVE_MAIL_SAVE_GUIDS |
+ MAIL_STORAGE_CLASS_FLAG_BINARY_DATA,
+ .event_category = &event_category_maildir,
+
+ .v = {
+ maildir_get_setting_parser_info,
+ maildir_storage_alloc,
+ maildir_storage_create,
+ index_storage_destroy,
+ maildir_storage_add_list,
+ maildir_storage_get_list_settings,
+ maildir_storage_autodetect,
+ maildir_mailbox_alloc,
+ NULL,
+ mail_storage_list_index_rebuild,
+ }
+};
+
+struct mailbox maildir_mailbox = {
+ .v = {
+ maildir_storage_is_readonly,
+ index_storage_mailbox_enable,
+ maildir_mailbox_exists,
+ maildir_mailbox_open,
+ maildir_mailbox_close,
+ index_storage_mailbox_free,
+ maildir_mailbox_create,
+ maildir_mailbox_update,
+ index_storage_mailbox_delete,
+ index_storage_mailbox_rename,
+ index_storage_get_status,
+ maildir_mailbox_get_metadata,
+ index_storage_set_subscribed,
+ index_storage_attribute_set,
+ index_storage_attribute_get,
+ index_storage_attribute_iter_init,
+ index_storage_attribute_iter_next,
+ index_storage_attribute_iter_deinit,
+ maildir_list_index_has_changed,
+ maildir_list_index_update_sync,
+ maildir_storage_sync_init,
+ index_mailbox_sync_next,
+ index_mailbox_sync_deinit,
+ NULL,
+ maildir_notify_changes,
+ index_transaction_begin,
+ index_transaction_commit,
+ index_transaction_rollback,
+ maildir_get_private_flags_mask,
+ index_mail_alloc,
+ index_storage_search_init,
+ index_storage_search_deinit,
+ index_storage_search_next_nonblock,
+ index_storage_search_next_update_seq,
+ index_storage_search_next_match_mail,
+ maildir_save_alloc,
+ maildir_save_begin,
+ maildir_save_continue,
+ maildir_save_finish,
+ maildir_save_cancel,
+ maildir_copy,
+ maildir_transaction_save_commit_pre,
+ maildir_transaction_save_commit_post,
+ maildir_transaction_save_rollback,
+ index_storage_is_inconsistent
+ }
+};
diff --git a/src/lib-storage/index/maildir/maildir-storage.h b/src/lib-storage/index/maildir/maildir-storage.h
new file mode 100644
index 0000000..da92866
--- /dev/null
+++ b/src/lib-storage/index/maildir/maildir-storage.h
@@ -0,0 +1,148 @@
+#ifndef MAILDIR_STORAGE_H
+#define MAILDIR_STORAGE_H
+
+#include "maildir-settings.h"
+
+#define MAILDIR_STORAGE_NAME "maildir"
+#define MAILDIR_SUBSCRIPTION_FILE_NAME "subscriptions"
+#define MAILDIR_UIDVALIDITY_FNAME "dovecot-uidvalidity"
+
+/* "base,S=123:2," means:
+ <base> [<extra sep> <extra data> [..]] <info sep> 2 <flags sep> */
+#define MAILDIR_INFO_SEP ':'
+#define MAILDIR_EXTRA_SEP ','
+#define MAILDIR_FLAGS_SEP ','
+
+#define MAILDIR_INFO_SEP_S ":"
+#define MAILDIR_EXTRA_SEP_S ","
+#define MAILDIR_FLAGS_SEP_S ","
+
+/* ":2," is the standard flags separator */
+#define MAILDIR_FLAGS_FULL_SEP MAILDIR_INFO_SEP_S "2" MAILDIR_FLAGS_SEP_S
+
+#define MAILDIR_KEYWORD_FIRST 'a'
+#define MAILDIR_KEYWORD_LAST 'z'
+#define MAILDIR_MAX_KEYWORDS (MAILDIR_KEYWORD_LAST - MAILDIR_KEYWORD_FIRST + 1)
+
+/* Maildir++ extension: include file size in the filename to avoid stat() */
+#define MAILDIR_EXTRA_FILE_SIZE 'S'
+/* Something (can't remember what anymore) could use 'W' in filename to avoid
+ calculating file's virtual size (added missing CRs). */
+#define MAILDIR_EXTRA_VIRTUAL_SIZE 'W'
+
+/* Delete files having ctime older than this from tmp/. 36h is standard. */
+#define MAILDIR_TMP_DELETE_SECS (36*60*60)
+
+/* How often to touch the uidlist lock file when it's locked.
+ This is done both when using KEEP_LOCKED flag and when syncing a large
+ maildir. */
+#define MAILDIR_LOCK_TOUCH_SECS 10
+
+/* If an operation fails with ENOENT, we'll check if the mailbox is deleted
+ or if some directory is just missing. If it's missing, we'll create the
+ directories and try again this many times before failing. */
+#define MAILDIR_DELETE_RETRY_COUNT 3
+
+#include "index-storage.h"
+
+struct timeval;
+struct maildir_save_context;
+struct maildir_copy_context;
+
+struct maildir_index_header {
+ uint32_t new_check_time, new_mtime, new_mtime_nsecs;
+ uint32_t cur_check_time, cur_mtime, cur_mtime_nsecs;
+ uint32_t uidlist_mtime, uidlist_mtime_nsecs, uidlist_size;
+};
+
+struct maildir_list_index_record {
+ uint32_t new_mtime, cur_mtime;
+};
+
+struct maildir_storage {
+ struct mail_storage storage;
+
+ const struct maildir_settings *set;
+ const char *temp_prefix;
+};
+
+struct maildir_mailbox {
+ struct mailbox box;
+ struct maildir_storage *storage;
+ struct mail_index_view *flags_view;
+
+ struct timeout *keep_lock_to;
+
+ /* Filled lazily by mailbox_get_private_flags_mask() */
+ enum mail_flags _private_flags_mask;
+
+ /* maildir sync: */
+ struct maildir_uidlist *uidlist;
+ struct maildir_keywords *keywords;
+
+ struct maildir_index_header maildir_hdr;
+ uint32_t maildir_ext_id;
+ uint32_t maildir_list_index_ext_id;
+
+ bool synced:1;
+ bool syncing_commit:1;
+ bool private_flags_mask_set:1;
+ bool backend_readonly:1;
+ bool backend_readonly_set:1;
+ bool sync_uidlist_refreshed:1;
+};
+
+#define MAILDIR_STORAGE(s) container_of(s, struct maildir_storage, storage)
+#define MAILDIR_MAILBOX(s) container_of(s, struct maildir_mailbox, box)
+
+extern struct mail_vfuncs maildir_mail_vfuncs;
+
+/* Return -1 = error, 0 = file not found, 1 = ok */
+typedef int maildir_file_do_func(struct maildir_mailbox *mbox,
+ const char *path, void *context);
+
+int maildir_file_do(struct maildir_mailbox *mbox, uint32_t uid,
+ maildir_file_do_func *callback, void *context);
+#define maildir_file_do(mbox, seq, callback, context) \
+ maildir_file_do(mbox, seq - \
+ CALLBACK_TYPECHECK(callback, int (*)( \
+ struct maildir_mailbox *, const char *, typeof(context))), \
+ (maildir_file_do_func *)callback, context)
+
+bool maildir_set_deleted(struct mailbox *box);
+uint32_t maildir_get_uidvalidity_next(struct mailbox_list *list);
+int maildir_lose_unexpected_dir(struct mail_storage *storage, const char *path);
+bool maildir_is_backend_readonly(struct maildir_mailbox *mbox);
+
+struct mail_save_context *
+maildir_save_alloc(struct mailbox_transaction_context *_t);
+int maildir_save_begin(struct mail_save_context *ctx, struct istream *input);
+int maildir_save_continue(struct mail_save_context *ctx);
+void maildir_save_finish_keywords(struct mail_save_context *ctx);
+int maildir_save_finish(struct mail_save_context *ctx);
+void maildir_save_cancel(struct mail_save_context *ctx);
+
+struct maildir_filename *
+maildir_save_add(struct mail_save_context *_ctx, const char *tmp_fname,
+ struct mail *src_mail) ATTR_NULL(3);
+void maildir_save_set_dest_basename(struct mail_save_context *ctx,
+ struct maildir_filename *mf,
+ const char *basename);
+void maildir_save_set_sizes(struct maildir_filename *mf,
+ uoff_t size, uoff_t vsize);
+
+int maildir_save_file_get_size(struct mailbox_transaction_context *t,
+ uint32_t seq, bool vsize, uoff_t *size_r);
+const char *maildir_save_file_get_path(struct mailbox_transaction_context *t,
+ uint32_t seq);
+
+int maildir_transaction_save_commit_pre(struct mail_save_context *ctx);
+void maildir_transaction_save_commit_post(struct mail_save_context *ctx,
+ struct mail_index_transaction_commit_result *result);
+void maildir_transaction_save_rollback(struct mail_save_context *ctx);
+
+int maildir_copy(struct mail_save_context *ctx, struct mail *mail);
+int maildir_transaction_copy_commit(struct maildir_copy_context *ctx);
+void maildir_transaction_copy_rollback(struct maildir_copy_context *ctx);
+
+#endif
diff --git a/src/lib-storage/index/maildir/maildir-sync-index.c b/src/lib-storage/index/maildir/maildir-sync-index.c
new file mode 100644
index 0000000..da0e40c
--- /dev/null
+++ b/src/lib-storage/index/maildir/maildir-sync-index.c
@@ -0,0 +1,810 @@
+/* Copyright (c) 2007-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "ioloop.h"
+#include "array.h"
+#include "maildir-storage.h"
+#include "index-sync-changes.h"
+#include "maildir-uidlist.h"
+#include "maildir-keywords.h"
+#include "maildir-filename-flags.h"
+#include "maildir-sync.h"
+#include "mailbox-recent-flags.h"
+
+#include <stdio.h>
+#include <unistd.h>
+
+struct maildir_index_sync_context {
+ struct maildir_mailbox *mbox;
+ struct maildir_sync_context *maildir_sync_ctx;
+
+ struct mail_index_view *view;
+ struct mail_index_sync_ctx *sync_ctx;
+ struct maildir_keywords_sync_ctx *keywords_sync_ctx;
+ struct mail_index_transaction *trans;
+
+ struct maildir_uidlist_sync_ctx *uidlist_sync_ctx;
+ struct index_sync_changes_context *sync_changes;
+ enum mail_flags flags;
+ ARRAY_TYPE(keyword_indexes) keywords, idx_keywords;
+
+ uint32_t uid;
+ bool update_maildir_hdr_cur;
+
+ time_t start_time;
+ unsigned int flag_change_count, expunge_count, new_msgs_count;
+};
+
+struct maildir_keywords_sync_ctx *
+maildir_sync_get_keywords_sync_ctx(struct maildir_index_sync_context *ctx)
+{
+ return ctx->keywords_sync_ctx;
+}
+
+void maildir_sync_set_new_msgs_count(struct maildir_index_sync_context *ctx,
+ unsigned int count)
+{
+ ctx->new_msgs_count = count;
+}
+
+static bool
+maildir_expunge_is_valid_guid(struct maildir_index_sync_context *ctx,
+ uint32_t uid, const char *filename,
+ guid_128_t expunged_guid_128)
+{
+ guid_128_t guid_128;
+ const char *guid;
+
+ if (guid_128_is_empty(expunged_guid_128)) {
+ /* no GUID associated with expunge */
+ return TRUE;
+ }
+
+ T_BEGIN {
+ guid = maildir_uidlist_lookup_ext(ctx->mbox->uidlist, uid,
+ MAILDIR_UIDLIST_REC_EXT_GUID);
+ if (guid == NULL)
+ guid = t_strcut(filename, *MAILDIR_INFO_SEP_S);
+ mail_generate_guid_128_hash(guid, guid_128);
+ } T_END;
+
+ if (memcmp(guid_128, expunged_guid_128, sizeof(guid_128)) == 0)
+ return TRUE;
+
+ mailbox_set_critical(&ctx->mbox->box,
+ "Expunged GUID mismatch for UID %u: %s vs %s",
+ ctx->uid, guid_128_to_string(guid_128),
+ guid_128_to_string(expunged_guid_128));
+ return FALSE;
+}
+
+static int maildir_expunge(struct maildir_mailbox *mbox, const char *path,
+ struct maildir_index_sync_context *ctx)
+{
+ struct mailbox *box = &mbox->box;
+
+ ctx->expunge_count++;
+
+ if (unlink(path) == 0) {
+ mailbox_sync_notify(box, ctx->uid, MAILBOX_SYNC_TYPE_EXPUNGE);
+ return 1;
+ }
+ if (errno == ENOENT)
+ return 0;
+ if (UNLINK_EISDIR(errno))
+ return maildir_lose_unexpected_dir(box->storage, path);
+
+ mailbox_set_critical(&mbox->box, "unlink(%s) failed: %m", path);
+ return -1;
+}
+
+static int maildir_sync_flags(struct maildir_mailbox *mbox, const char *path,
+ struct maildir_index_sync_context *ctx)
+{
+ struct mailbox *box = &mbox->box;
+ struct stat st;
+ const char *dir, *fname, *newfname, *newpath;
+ enum mail_index_sync_type sync_type;
+ uint8_t flags8;
+
+ ctx->flag_change_count++;
+
+ fname = strrchr(path, '/');
+ i_assert(fname != NULL);
+ fname++;
+ dir = t_strdup_until(path, fname);
+
+ i_assert(*fname != '\0');
+
+ /* get the current flags and keywords */
+ maildir_filename_flags_get(ctx->keywords_sync_ctx,
+ fname, &ctx->flags, &ctx->keywords);
+
+ /* apply changes */
+ flags8 = ctx->flags;
+ index_sync_changes_apply(ctx->sync_changes, NULL,
+ &flags8, &ctx->keywords, &sync_type);
+ ctx->flags = flags8;
+
+ /* and try renaming with the new name */
+ newfname = maildir_filename_flags_kw_set(ctx->keywords_sync_ctx, fname,
+ ctx->flags, &ctx->keywords);
+ newpath = t_strconcat(dir, newfname, NULL);
+ if (strcmp(path, newpath) == 0) {
+ /* just make sure that the file still exists. avoid rename()
+ here because it's slow on HFS. */
+ if (stat(path, &st) < 0) {
+ if (errno == ENOENT)
+ return 0;
+ mailbox_set_critical(box, "stat(%s) failed: %m", path);
+ return -1;
+ }
+ } else {
+ if (rename(path, newpath) < 0) {
+ if (errno == ENOENT)
+ return 0;
+ if (!ENOSPACE(errno) && errno != EACCES) {
+ mailbox_set_critical(box,
+ "rename(%s, %s) failed: %m",
+ path, newpath);
+ }
+ return -1;
+ }
+ }
+ mailbox_sync_notify(box, ctx->uid, index_sync_type_convert(sync_type));
+ return 1;
+}
+
+static int maildir_handle_uid_insertion(struct maildir_index_sync_context *ctx,
+ enum maildir_uidlist_rec_flag uflags,
+ const char *filename, uint32_t uid)
+{
+ int ret;
+
+ if ((uflags & MAILDIR_UIDLIST_REC_FLAG_NONSYNCED) != 0) {
+ /* partial syncing */
+ return 0;
+ }
+
+ /* most likely a race condition: we read the maildir, then someone else
+ expunged messages and committed changes to index. so, this message
+ shouldn't actually exist. */
+ if ((uflags & MAILDIR_UIDLIST_REC_FLAG_RACING) == 0) {
+ /* mark it racy and check in next sync */
+ ctx->mbox->maildir_hdr.cur_check_time = 0;
+ maildir_sync_set_racing(ctx->maildir_sync_ctx);
+ maildir_uidlist_add_flags(ctx->mbox->uidlist, filename,
+ MAILDIR_UIDLIST_REC_FLAG_RACING);
+ return 0;
+ }
+
+ if (ctx->uidlist_sync_ctx == NULL) {
+ ret = maildir_uidlist_sync_init(ctx->mbox->uidlist,
+ MAILDIR_UIDLIST_SYNC_PARTIAL |
+ MAILDIR_UIDLIST_SYNC_KEEP_STATE,
+ &ctx->uidlist_sync_ctx);
+ if (ret <= 0)
+ return -1;
+ }
+
+ uflags &= MAILDIR_UIDLIST_REC_FLAG_NEW_DIR;
+ maildir_uidlist_sync_remove(ctx->uidlist_sync_ctx, filename);
+ ret = maildir_uidlist_sync_next(ctx->uidlist_sync_ctx,
+ filename, uflags);
+ i_assert(ret > 0);
+
+ /* give the new UID to it immediately */
+ maildir_uidlist_sync_finish(ctx->uidlist_sync_ctx);
+
+ i_warning("Maildir %s: Expunged message reappeared, giving a new UID "
+ "(old uid=%u, file=%s)%s", mailbox_get_path(&ctx->mbox->box),
+ uid, filename, !str_begins(filename, "msg.") ? "" :
+ " (Your MDA is saving MH files into Maildir?)");
+ return 0;
+}
+
+int maildir_sync_index_begin(struct maildir_mailbox *mbox,
+ struct maildir_sync_context *maildir_sync_ctx,
+ struct maildir_index_sync_context **ctx_r)
+{
+ struct mailbox *_box = &mbox->box;
+ struct maildir_index_sync_context *ctx;
+ struct mail_index_sync_ctx *sync_ctx;
+ struct mail_index_view *view;
+ struct mail_index_transaction *trans;
+ enum mail_index_sync_flags sync_flags;
+
+ sync_flags = index_storage_get_sync_flags(&mbox->box);
+ /* don't drop recent messages if we're saving messages */
+ if (maildir_sync_ctx == NULL)
+ sync_flags &= ENUM_NEGATE(MAIL_INDEX_SYNC_FLAG_DROP_RECENT);
+
+ if (index_storage_expunged_sync_begin(_box, &sync_ctx, &view,
+ &trans, sync_flags) < 0)
+ return -1;
+
+ ctx = i_new(struct maildir_index_sync_context, 1);
+ ctx->mbox = mbox;
+ ctx->maildir_sync_ctx = maildir_sync_ctx;
+ ctx->sync_ctx = sync_ctx;
+ ctx->view = view;
+ ctx->trans = trans;
+ ctx->keywords_sync_ctx =
+ maildir_keywords_sync_init(mbox->keywords, _box->index);
+ ctx->sync_changes =
+ index_sync_changes_init(ctx->sync_ctx, ctx->view, ctx->trans,
+ maildir_is_backend_readonly(mbox));
+ ctx->start_time = time(NULL);
+
+ *ctx_r = ctx;
+ return 0;
+}
+
+static bool
+maildir_index_header_has_changed(const struct maildir_index_header *old_hdr,
+ const struct maildir_index_header *new_hdr)
+{
+#define DIR_DELAYED_REFRESH(hdr, name) \
+ ((hdr)->name ## _check_time <= \
+ (hdr)->name ## _mtime + MAILDIR_SYNC_SECS)
+
+ if (old_hdr->new_mtime != new_hdr->new_mtime ||
+ old_hdr->new_mtime_nsecs != new_hdr->new_mtime_nsecs ||
+ old_hdr->cur_mtime != new_hdr->cur_mtime ||
+ old_hdr->cur_mtime_nsecs != new_hdr->cur_mtime_nsecs ||
+ old_hdr->uidlist_mtime != new_hdr->uidlist_mtime ||
+ old_hdr->uidlist_mtime_nsecs != new_hdr->uidlist_mtime_nsecs ||
+ old_hdr->uidlist_size != new_hdr->uidlist_size)
+ return TRUE;
+
+ return DIR_DELAYED_REFRESH(old_hdr, new) !=
+ DIR_DELAYED_REFRESH(new_hdr, new) ||
+ DIR_DELAYED_REFRESH(old_hdr, cur) !=
+ DIR_DELAYED_REFRESH(new_hdr, cur);
+}
+
+static void
+maildir_sync_index_update_ext_header(struct maildir_index_sync_context *ctx)
+{
+ struct maildir_mailbox *mbox = ctx->mbox;
+ const char *cur_path;
+ const void *data;
+ size_t data_size;
+ struct stat st;
+
+ cur_path = t_strconcat(mailbox_get_path(&mbox->box), "/cur", NULL);
+ if (ctx->update_maildir_hdr_cur && stat(cur_path, &st) == 0) {
+ if ((time_t)mbox->maildir_hdr.cur_check_time < st.st_mtime)
+ mbox->maildir_hdr.cur_check_time = st.st_mtime;
+ mbox->maildir_hdr.cur_mtime = st.st_mtime;
+ mbox->maildir_hdr.cur_mtime_nsecs = ST_MTIME_NSEC(st);
+ }
+
+ mail_index_get_header_ext(mbox->box.view, mbox->maildir_ext_id,
+ &data, &data_size);
+ if (data_size != sizeof(mbox->maildir_hdr) ||
+ maildir_index_header_has_changed(data, &mbox->maildir_hdr)) {
+ mail_index_update_header_ext(ctx->trans, mbox->maildir_ext_id,
+ 0, &mbox->maildir_hdr,
+ sizeof(mbox->maildir_hdr));
+ }
+}
+
+static int maildir_sync_index_finish(struct maildir_index_sync_context *ctx,
+ bool success)
+{
+ struct maildir_mailbox *mbox = ctx->mbox;
+ unsigned int time_diff;
+ int ret = success ? 0 : -1;
+
+ time_diff = time(NULL) - ctx->start_time;
+ if (time_diff >= MAILDIR_SYNC_TIME_WARN_SECS) {
+ i_warning("Maildir %s: Synchronization took %u seconds "
+ "(%u new msgs, %u flag change attempts, "
+ "%u expunge attempts)",
+ mailbox_get_path(&ctx->mbox->box), time_diff,
+ ctx->new_msgs_count, ctx->flag_change_count,
+ ctx->expunge_count);
+ mail_index_sync_no_warning(ctx->sync_ctx);
+ }
+
+ if (ret < 0)
+ mail_index_sync_rollback(&ctx->sync_ctx);
+ else {
+ maildir_sync_index_update_ext_header(ctx);
+
+ /* Set syncing_commit=TRUE so that if any sync callbacks try
+ to access mails which got lost (eg. expunge callback trying
+ to open the file which was just unlinked) we don't try to
+ start a second index sync and crash. */
+ mbox->syncing_commit = TRUE;
+ if (mail_index_sync_commit(&ctx->sync_ctx) < 0) {
+ mailbox_set_index_error(&mbox->box);
+ ret = -1;
+ }
+ mbox->syncing_commit = FALSE;
+ }
+
+ index_storage_expunging_deinit(&mbox->box);
+ maildir_keywords_sync_deinit(&ctx->keywords_sync_ctx);
+ index_sync_changes_deinit(&ctx->sync_changes);
+ i_free(ctx);
+ return ret;
+}
+
+int maildir_sync_index_commit(struct maildir_index_sync_context **_ctx)
+{
+ struct maildir_index_sync_context *ctx = *_ctx;
+
+ *_ctx = NULL;
+ return maildir_sync_index_finish(ctx, TRUE);
+}
+
+void maildir_sync_index_rollback(struct maildir_index_sync_context **_ctx)
+{
+ struct maildir_index_sync_context *ctx = *_ctx;
+
+ *_ctx = NULL;
+ (void)maildir_sync_index_finish(ctx, FALSE);
+}
+
+static int uint_cmp(const unsigned int *i1, const unsigned int *i2)
+{
+ if (*i1 < *i2)
+ return -1;
+ else if (*i1 > *i2)
+ return 1;
+ else
+ return 0;
+}
+
+static void
+maildir_sync_mail_keywords(struct maildir_index_sync_context *ctx, uint32_t seq)
+{
+ struct mailbox *box = &ctx->mbox->box;
+ struct mail_keywords *kw;
+ unsigned int i, j, old_count, new_count;
+ const unsigned int *old_indexes, *new_indexes;
+ bool have_indexonly_keywords;
+ int diff;
+
+ mail_index_lookup_keywords(ctx->view, seq, &ctx->idx_keywords);
+ if (index_keyword_array_cmp(&ctx->keywords, &ctx->idx_keywords)) {
+ /* no changes - we should get here usually */
+ return;
+ }
+
+ /* sort the keywords */
+ array_sort(&ctx->idx_keywords, uint_cmp);
+ array_sort(&ctx->keywords, uint_cmp);
+
+ /* drop keywords that are in index-only. we don't want to touch them. */
+ old_indexes = array_get(&ctx->idx_keywords, &old_count);
+ have_indexonly_keywords = FALSE;
+ for (i = old_count; i > 0; i--) {
+ if (maildir_keywords_idx_char(ctx->keywords_sync_ctx,
+ old_indexes[i-1]) == '\0') {
+ have_indexonly_keywords = TRUE;
+ array_delete(&ctx->idx_keywords, i-1, 1);
+ }
+ }
+
+ if (!have_indexonly_keywords) {
+ /* no index-only keywords found, so something changed.
+ just replace them all. */
+ kw = mail_index_keywords_create_from_indexes(box->index,
+ &ctx->keywords);
+ mail_index_update_keywords(ctx->trans, seq, MODIFY_REPLACE, kw);
+ mail_index_keywords_unref(&kw);
+ return;
+ }
+
+ /* check again if non-index-only keywords changed */
+ if (index_keyword_array_cmp(&ctx->keywords, &ctx->idx_keywords))
+ return;
+
+ /* we can't reset all the keywords or we'd drop indexonly keywords too.
+ so first remove the unwanted keywords and then add back the wanted
+ ones. we can get these lists easily by removing common elements
+ from old and new keywords. */
+ new_indexes = array_get(&ctx->keywords, &new_count);
+ for (i = j = 0; i < old_count && j < new_count; ) {
+ diff = (int)old_indexes[i] - (int)new_indexes[j];
+ if (diff == 0) {
+ array_delete(&ctx->keywords, j, 1);
+ array_delete(&ctx->idx_keywords, i, 1);
+ old_indexes = array_get(&ctx->idx_keywords, &old_count);
+ new_indexes = array_get(&ctx->keywords, &new_count);
+ } else if (diff < 0) {
+ i++;
+ } else {
+ j++;
+ }
+ }
+
+ if (array_count(&ctx->idx_keywords) > 0) {
+ kw = mail_index_keywords_create_from_indexes(box->index,
+ &ctx->idx_keywords);
+ mail_index_update_keywords(ctx->trans, seq, MODIFY_REMOVE, kw);
+ mail_index_keywords_unref(&kw);
+ }
+
+ if (array_count(&ctx->keywords) > 0) {
+ kw = mail_index_keywords_create_from_indexes(box->index,
+ &ctx->keywords);
+ mail_index_update_keywords(ctx->trans, seq, MODIFY_ADD, kw);
+ mail_index_keywords_unref(&kw);
+ }
+}
+
+int maildir_sync_index(struct maildir_index_sync_context *ctx,
+ bool partial)
+{
+ struct maildir_mailbox *mbox = ctx->mbox;
+ struct mail_index_view *view = ctx->view;
+ struct mail_index_view *view2;
+ struct maildir_uidlist_iter_ctx *iter;
+ struct mail_index_transaction *trans = ctx->trans;
+ const struct mail_index_header *hdr;
+ struct mail_index_header empty_hdr;
+ const struct mail_index_record *rec;
+ uint32_t seq, seq2, uid, prev_uid;
+ enum maildir_uidlist_rec_flag uflags;
+ const char *filename;
+ uint32_t uid_validity, next_uid, hdr_next_uid, first_recent_uid;
+ uint32_t first_uid;
+ unsigned int changes = 0;
+ int ret = 0;
+ time_t time_before_sync;
+ guid_128_t expunged_guid_128;
+ enum mail_flags private_flags_mask;
+ bool expunged, full_rescan = FALSE;
+
+ i_assert(!mbox->syncing_commit);
+
+ first_uid = 1;
+ hdr = mail_index_get_header(view);
+ uid_validity = maildir_uidlist_get_uid_validity(mbox->uidlist);
+ if (uid_validity != hdr->uid_validity &&
+ uid_validity != 0 && hdr->uid_validity != 0) {
+ /* uidvalidity changed and index isn't being synced for the
+ first time, reset the index so we can add all messages as
+ new */
+ i_warning("Maildir %s: UIDVALIDITY changed (%u -> %u)",
+ mailbox_get_path(&ctx->mbox->box),
+ hdr->uid_validity, uid_validity);
+ mail_index_reset(trans);
+ mailbox_recent_flags_reset(&mbox->box);
+
+ first_uid = hdr->messages_count + 1;
+ i_zero(&empty_hdr);
+ empty_hdr.next_uid = 1;
+ hdr = &empty_hdr;
+ }
+ hdr_next_uid = hdr->next_uid;
+
+ ctx->mbox->box.tmp_sync_view = view;
+ private_flags_mask = mailbox_get_private_flags_mask(&mbox->box);
+ time_before_sync = time(NULL);
+ mbox->syncing_commit = TRUE;
+ seq = prev_uid = 0; first_recent_uid = I_MAX(hdr->first_recent_uid, 1);
+ i_array_init(&ctx->keywords, MAILDIR_MAX_KEYWORDS);
+ i_array_init(&ctx->idx_keywords, MAILDIR_MAX_KEYWORDS);
+ iter = maildir_uidlist_iter_init(mbox->uidlist);
+ while (maildir_uidlist_iter_next(iter, &uid, &uflags, &filename)) {
+ maildir_filename_flags_get(ctx->keywords_sync_ctx, filename,
+ &ctx->flags, &ctx->keywords);
+
+ i_assert(uid > prev_uid);
+ prev_uid = uid;
+
+ /* the private flags are kept only in indexes. don't use them
+ at all even for newly seen mails */
+ ctx->flags &= ENUM_NEGATE(private_flags_mask);
+
+ again:
+ seq++;
+ ctx->uid = uid;
+
+ if (seq > hdr->messages_count) {
+ if (uid < hdr_next_uid) {
+ if (maildir_handle_uid_insertion(ctx, uflags,
+ filename,
+ uid) < 0)
+ ret = -1;
+ seq--;
+ continue;
+ }
+
+ /* Trust uidlist recent flags only for newly added
+ messages. When saving/copying messages with flags
+ they're stored to cur/ and uidlist treats them
+ as non-recent. */
+ if ((uflags & MAILDIR_UIDLIST_REC_FLAG_RECENT) == 0) {
+ if (uid >= first_recent_uid)
+ first_recent_uid = uid + 1;
+ }
+
+ hdr_next_uid = uid + 1;
+ mail_index_append(trans, uid, &seq);
+ mail_index_update_flags(trans, seq, MODIFY_REPLACE,
+ ctx->flags);
+ if (array_count(&ctx->keywords) > 0) {
+ struct mail_keywords *kw;
+
+ kw = mail_index_keywords_create_from_indexes(
+ mbox->box.index, &ctx->keywords);
+ mail_index_update_keywords(trans, seq,
+ MODIFY_REPLACE, kw);
+ mail_index_keywords_unref(&kw);
+ }
+ continue;
+ }
+
+ rec = mail_index_lookup(view, seq);
+ if (uid > rec->uid) {
+ /* already expunged (no point in showing guid in the
+ expunge record anymore) */
+ mail_index_expunge(ctx->trans, seq);
+ goto again;
+ }
+
+ if (uid < rec->uid) {
+ if (maildir_handle_uid_insertion(ctx, uflags,
+ filename, uid) < 0)
+ ret = -1;
+ seq--;
+ continue;
+ }
+
+ index_sync_changes_read(ctx->sync_changes, ctx->uid, &expunged,
+ expunged_guid_128);
+ if (expunged) {
+ if (!maildir_expunge_is_valid_guid(ctx, ctx->uid,
+ filename,
+ expunged_guid_128))
+ continue;
+ if (maildir_file_do(mbox, ctx->uid,
+ maildir_expunge, ctx) >= 0) {
+ /* successful expunge */
+ mail_index_expunge(ctx->trans, seq);
+ }
+ if ((++changes % MAILDIR_SLOW_MOVE_COUNT) == 0)
+ maildir_sync_notify(ctx->maildir_sync_ctx);
+ continue;
+ }
+
+ /* the private flags are stored only in indexes, keep them */
+ ctx->flags |= rec->flags & private_flags_mask;
+
+ if (index_sync_changes_have(ctx->sync_changes)) {
+ /* apply flag changes to maildir */
+ if (maildir_file_do(mbox, ctx->uid,
+ maildir_sync_flags, ctx) < 0)
+ ctx->flags |= MAIL_INDEX_MAIL_FLAG_DIRTY;
+ if ((++changes % MAILDIR_SLOW_MOVE_COUNT) == 0)
+ maildir_sync_notify(ctx->maildir_sync_ctx);
+ }
+
+ if ((uflags & MAILDIR_UIDLIST_REC_FLAG_NONSYNCED) != 0) {
+ /* partial syncing */
+ if ((uflags & MAILDIR_UIDLIST_REC_FLAG_NEW_DIR) != 0) {
+ /* we last saw this mail in new/, but it's
+ not there anymore. possibly expunged,
+ make sure. */
+ full_rescan = TRUE;
+ }
+ continue;
+ }
+
+ if ((rec->flags & MAIL_INDEX_MAIL_FLAG_DIRTY) != 0) {
+ /* we haven't been able to update maildir with this
+ record's flag changes. don't sync them. */
+ continue;
+ }
+
+ if (ctx->flags != (rec->flags & MAIL_FLAGS_NONRECENT)) {
+ mail_index_update_flags(trans, seq, MODIFY_REPLACE,
+ ctx->flags);
+ }
+
+ maildir_sync_mail_keywords(ctx, seq);
+ }
+ maildir_uidlist_iter_deinit(&iter);
+
+ if (!partial) {
+ /* expunge the rest */
+ for (seq++; seq <= hdr->messages_count; seq++)
+ mail_index_expunge(ctx->trans, seq);
+ }
+
+ /* add \Recent flags. use updated view so it contains newly
+ appended messages. */
+ view2 = mail_index_transaction_open_updated_view(trans);
+ if (mail_index_lookup_seq_range(view2, first_recent_uid, (uint32_t)-1,
+ &seq, &seq2) && seq2 >= first_uid) {
+ if (seq < first_uid) {
+ /* UIDVALIDITY changed, skip over the old messages */
+ seq = first_uid;
+ }
+ mailbox_recent_flags_set_seqs(&mbox->box, view2, seq, seq2);
+ }
+ mail_index_view_close(&view2);
+
+ if (ctx->uidlist_sync_ctx != NULL) {
+ if (maildir_uidlist_sync_deinit(&ctx->uidlist_sync_ctx,
+ TRUE) < 0)
+ ret = -1;
+ }
+
+ mailbox_sync_notify(&mbox->box, 0, 0);
+ ctx->mbox->box.tmp_sync_view = NULL;
+
+ /* check cur/ mtime later. if we came here from saving messages they
+ could still be moved to cur/ directory. */
+ ctx->update_maildir_hdr_cur = TRUE;
+ mbox->maildir_hdr.cur_check_time = time_before_sync;
+
+ if (uid_validity == 0) {
+ uid_validity = hdr->uid_validity != 0 ? hdr->uid_validity :
+ maildir_get_uidvalidity_next(mbox->box.list);
+ maildir_uidlist_set_uid_validity(mbox->uidlist, uid_validity);
+ }
+ maildir_uidlist_set_next_uid(mbox->uidlist, hdr_next_uid, FALSE);
+
+ if (uid_validity != hdr->uid_validity) {
+ mail_index_update_header(trans,
+ offsetof(struct mail_index_header, uid_validity),
+ &uid_validity, sizeof(uid_validity), TRUE);
+ }
+
+ next_uid = maildir_uidlist_get_next_uid(mbox->uidlist);
+ if (hdr_next_uid < next_uid) {
+ mail_index_update_header(trans,
+ offsetof(struct mail_index_header, next_uid),
+ &next_uid, sizeof(next_uid), FALSE);
+ }
+
+ i_assert(hdr->first_recent_uid <= first_recent_uid);
+ if (hdr->first_recent_uid < first_recent_uid) {
+ mail_index_update_header(ctx->trans,
+ offsetof(struct mail_index_header, first_recent_uid),
+ &first_recent_uid, sizeof(first_recent_uid), FALSE);
+ }
+ array_free(&ctx->keywords);
+ array_free(&ctx->idx_keywords);
+ mbox->syncing_commit = FALSE;
+ return ret < 0 ? -1 : (full_rescan ? 0 : 1);
+}
+
+static unsigned int
+maildir_list_get_ext_id(struct maildir_mailbox *mbox,
+ struct mail_index_view *view)
+{
+ if (mbox->maildir_list_index_ext_id == (uint32_t)-1) {
+ mbox->maildir_list_index_ext_id =
+ mail_index_ext_register(mail_index_view_get_index(view),
+ "maildir", 0,
+ sizeof(struct maildir_list_index_record),
+ sizeof(uint32_t));
+ }
+ return mbox->maildir_list_index_ext_id;
+}
+
+int maildir_list_index_has_changed(struct mailbox *box,
+ struct mail_index_view *list_view,
+ uint32_t seq, bool quick,
+ const char **reason_r)
+{
+ struct maildir_mailbox *mbox = MAILDIR_MAILBOX(box);
+ const struct maildir_list_index_record *rec;
+ const void *data;
+ const char *root_dir, *new_dir, *cur_dir;
+ struct stat st;
+ uint32_t ext_id;
+ bool expunged;
+ int ret;
+
+ ret = index_storage_list_index_has_changed(box, list_view, seq,
+ quick, reason_r);
+ if (ret != 0 || box->storage->set->mailbox_list_index_very_dirty_syncs)
+ return ret;
+ if (mbox->storage->set->maildir_very_dirty_syncs) {
+ /* we don't track cur/new directories with dirty syncs */
+ return 0;
+ }
+
+ ext_id = maildir_list_get_ext_id(mbox, list_view);
+ mail_index_lookup_ext(list_view, seq, ext_id, &data, &expunged);
+ rec = data;
+
+ if (rec == NULL) {
+ *reason_r = "Maildir record is missing";
+ return 1;
+ } else if (expunged) {
+ *reason_r = "Maildir record is expunged";
+ return 1;
+ } else if (rec->new_mtime == 0) {
+ /* not synced */
+ *reason_r = "Maildir record new_mtime=0";
+ return 1;
+ } else if (rec->cur_mtime == 0) {
+ /* dirty-synced */
+ *reason_r = "Maildir record cur_mtime=0";
+ return 1;
+ }
+
+ ret = mailbox_get_path_to(box, MAILBOX_LIST_PATH_TYPE_MAILBOX,
+ &root_dir);
+ if (ret < 0)
+ return ret;
+ i_assert(ret > 0);
+
+ /* check if new/ changed */
+ new_dir = t_strconcat(root_dir, "/new", NULL);
+ if (stat(new_dir, &st) < 0) {
+ mailbox_set_critical(box, "stat(%s) failed: %m", new_dir);
+ return -1;
+ }
+ if ((time_t)rec->new_mtime != st.st_mtime) {
+ *reason_r = t_strdup_printf(
+ "Maildir new_mtime changed %u != %"PRIdTIME_T,
+ rec->new_mtime, st.st_mtime);
+ return 1;
+ }
+
+ /* check if cur/ changed */
+ cur_dir = t_strconcat(root_dir, "/cur", NULL);
+ if (stat(cur_dir, &st) < 0) {
+ mailbox_set_critical(box, "stat(%s) failed: %m", cur_dir);
+ return -1;
+ }
+ if ((time_t)rec->cur_mtime != st.st_mtime) {
+ *reason_r = t_strdup_printf(
+ "Maildir cur_mtime changed %u != %"PRIdTIME_T,
+ rec->cur_mtime, st.st_mtime);
+ return 1;
+ }
+ return 0;
+}
+
+void maildir_list_index_update_sync(struct mailbox *box,
+ struct mail_index_transaction *trans,
+ uint32_t seq)
+{
+ struct maildir_mailbox *mbox = MAILDIR_MAILBOX(box);
+ struct mail_index_view *list_view;
+ const struct maildir_index_header *mhdr = &mbox->maildir_hdr;
+ const struct maildir_list_index_record *old_rec;
+ struct maildir_list_index_record new_rec;
+ const void *data;
+ uint32_t ext_id;
+ bool expunged;
+
+ index_storage_list_index_update_sync(box, trans, seq);
+ if (mbox->storage->set->maildir_very_dirty_syncs) {
+ /* we don't track cur/new directories with dirty syncs */
+ return;
+ }
+
+ /* get the current record */
+ list_view = mail_index_transaction_get_view(trans);
+ ext_id = maildir_list_get_ext_id(mbox, list_view);
+ mail_index_lookup_ext(list_view, seq, ext_id, &data, &expunged);
+ if (expunged)
+ return;
+ old_rec = data;
+
+ i_zero(&new_rec);
+ if (mhdr->new_check_time <= mhdr->new_mtime + MAILDIR_SYNC_SECS ||
+ mhdr->cur_check_time <= mhdr->cur_mtime + MAILDIR_SYNC_SECS) {
+ /* dirty, we need a refresh next time */
+ } else {
+ new_rec.new_mtime = mhdr->new_mtime;
+ new_rec.cur_mtime = mhdr->cur_mtime;
+ }
+
+ if (old_rec == NULL ||
+ memcmp(old_rec, &new_rec, sizeof(*old_rec)) != 0)
+ mail_index_update_ext(trans, seq, ext_id, &new_rec, NULL);
+}
diff --git a/src/lib-storage/index/maildir/maildir-sync.c b/src/lib-storage/index/maildir/maildir-sync.c
new file mode 100644
index 0000000..0814415
--- /dev/null
+++ b/src/lib-storage/index/maildir/maildir-sync.c
@@ -0,0 +1,1132 @@
+/* Copyright (c) 2004-2018 Dovecot authors, see the included COPYING file */
+
+/*
+ Here's a description of how we handle Maildir synchronization and
+ it's problems:
+
+ We want to be as efficient as we can. The most efficient way to
+ check if changes have occurred is to stat() the new/ and cur/
+ directories and uidlist file - if their mtimes haven't changed,
+ there's no changes and we don't need to do anything.
+
+ Problem 1: Multiple changes can happen within a single second -
+ nothing guarantees that once we synced it, someone else didn't just
+ then make a modification. Such modifications wouldn't get noticed
+ until a new modification occurred later.
+
+ Problem 2: Syncing cur/ directory is much more costly than syncing
+ new/. Moving mails from new/ to cur/ will always change mtime of
+ cur/ causing us to sync it as well.
+
+ Problem 3: We may not be able to move mail from new/ to cur/
+ because we're out of quota, or simply because we're accessing a
+ read-only mailbox.
+
+
+ MAILDIR_SYNC_SECS
+ -----------------
+
+ Several checks below use MAILDIR_SYNC_SECS, which should be maximum
+ clock drift between all computers accessing the maildir (eg. via
+ NFS), rounded up to next second. Our default is 1 second, since
+ everyone should be using NTP.
+
+ Note that setting it to 0 works only if there's only one computer
+ accessing the maildir. It's practically impossible to make two
+ clocks _exactly_ synchronized.
+
+ It might be possible to only use file server's clock by looking at
+ the atime field, but I don't know how well that would actually work.
+
+ cur directory
+ -------------
+
+ We have dirty_cur_time variable which is set to cur/ directory's
+ mtime when it's >= time() - MAILDIR_SYNC_SECS and we _think_ we have
+ synchronized the directory.
+
+ When dirty_cur_time is non-zero, we don't synchronize the cur/
+ directory until
+
+ a) cur/'s mtime changes
+ b) opening a mail fails with ENOENT
+ c) time() > dirty_cur_time + MAILDIR_SYNC_SECS
+
+ This allows us to modify the maildir multiple times without having
+ to sync it at every change. The sync will eventually be done to
+ make sure we didn't miss any external changes.
+
+ The dirty_cur_time is set when:
+
+ - we change message flags
+ - we expunge messages
+ - we move mail from new/ to cur/
+ - we sync cur/ directory and it's mtime is >= time() - MAILDIR_SYNC_SECS
+
+ It's unset when we do the final syncing, ie. when mtime is
+ older than time() - MAILDIR_SYNC_SECS.
+
+ new directory
+ -------------
+
+ If new/'s mtime is >= time() - MAILDIR_SYNC_SECS, always synchronize
+ it. dirty_cur_time-like feature might save us a few syncs, but
+ that might break a client which saves a mail in one connection and
+ tries to fetch it in another one. new/ directory is almost always
+ empty, so syncing it should be very fast anyway. Actually this can
+ still happen if we sync only new/ dir while another client is also
+ moving mails from it to cur/ - it takes us a while to see them.
+ That's pretty unlikely to happen however, and only way to fix it
+ would be to always synchronize cur/ after new/.
+
+ Normally we move all mails from new/ to cur/ whenever we sync it. If
+ it's not possible for some reason, we mark the mail with "probably
+ exists in new/ directory" flag.
+
+ If rename() still fails because of ENOSPC or EDQUOT, we still save
+ the flag changes in index with dirty-flag on. When moving the mail
+ to cur/ directory, or when we notice it's already moved there, we
+ apply the flag changes to the filename, rename it and remove the
+ dirty flag. If there's dirty flags, this should be tried every time
+ after expunge or when closing the mailbox.
+
+ uidlist
+ -------
+
+ This file contains UID <-> filename mappings. It's updated only when
+ new mail arrives, so it may contain filenames that have already been
+ deleted. Updating is done by getting uidlist.lock file, writing the
+ whole uidlist into it and rename()ing it over the old uidlist. This
+ means there's no need to lock the file for reading.
+
+ Whenever uidlist is rewritten, it's mtime must be larger than the old
+ one's. Use utime() before rename() if needed. Note that inode checking
+ wouldn't have been sufficient as inode numbers can be reused.
+
+ This file is usually read the first time you need to know filename for
+ given UID. After that it's not re-read unless new mails come that we
+ don't know about.
+
+ broken clients
+ --------------
+
+ Originally the middle identifier in Maildir filename was specified
+ only as <process id>_<delivery counter>. That however created a
+ problem with randomized PIDs which made it possible that the same
+ PID was reused within one second.
+
+ So if within one second a mail was delivered, MUA moved it to cur/
+ and another mail was delivered by a new process using same PID as
+ the first one, we likely ended up overwriting the first mail when
+ the second mail was moved over it.
+
+ Nowadays everyone should be giving a bit more specific identifier,
+ for example include microseconds in it which Dovecot does.
+
+ There's a simple way to prevent this from happening in some cases:
+ Don't move the mail from new/ to cur/ if it's mtime is >= time() -
+ MAILDIR_SYNC_SECS. The second delivery's link() call then fails
+ because the file is already in new/, and it will then use a
+ different filename. There's a few problems with this however:
+
+ - it requires extra stat() call which is unneeded extra I/O
+ - another MUA might still move the mail to cur/
+ - if first file's flags are modified by either Dovecot or another
+ MUA, it's moved to cur/ (you _could_ just do the dirty-flagging
+ but that'd be ugly)
+
+ Because this is useful only for very few people and it requires
+ extra I/O, I decided not to implement this. It should be however
+ quite easy to do since we need to be able to deal with files in new/
+ in any case.
+
+ It's also possible to never accidentally overwrite a mail by using
+ link() + unlink() rather than rename(). This however isn't very
+ good idea as it introduces potential race conditions when multiple
+ clients are accessing the mailbox:
+
+ Trying to move the same mail from new/ to cur/ at the same time:
+
+ a) Client 1 uses slightly different filename than client 2,
+ for example one sets read-flag on but the other doesn't.
+ You have the same mail duplicated now.
+
+ b) Client 3 sees the mail between Client 1's and 2's link() calls
+ and changes it's flag. You have the same mail duplicated now.
+
+ And it gets worse when they're unlink()ing in cur/ directory:
+
+ c) Client 1 changes mails's flag and client 2 changes it back
+ between 1's link() and unlink(). The mail is now expunged.
+
+ d) If you try to deal with the duplicates by unlink()ing another
+ one of them, you might end up unlinking both of them.
+
+ So, what should we do then if we notice a duplicate? First of all,
+ it might not be a duplicate at all, readdir() might have just
+ returned it twice because it was just renamed. What we should do is
+ create a completely new base name for it and rename() it to that.
+ If the call fails with ENOENT, it only means that it wasn't a
+ duplicate after all.
+*/
+
+#include "lib.h"
+#include "ioloop.h"
+#include "array.h"
+#include "buffer.h"
+#include "hash.h"
+#include "str.h"
+#include "eacces-error.h"
+#include "nfs-workarounds.h"
+#include "maildir-storage.h"
+#include "maildir-uidlist.h"
+#include "maildir-filename.h"
+#include "maildir-sync.h"
+
+#include <stdio.h>
+#include <stddef.h>
+#include <unistd.h>
+#include <dirent.h>
+#include <sys/stat.h>
+
+#define MAILDIR_FILENAME_FLAG_FOUND 128
+
+/* When rename()ing many files from new/ to cur/, it's possible that next
+ readdir() skips some files. we don't of course wish to lose them, so we
+ go and rescan the new/ directory again from beginning until no files are
+ left. This value is just an optimization to avoid checking the directory
+ twice needlessly. usually only NFS is the problem case. 1 is the safest
+ bet here, but I guess 5 will do just fine too. */
+#define MAILDIR_RENAME_RESCAN_COUNT 5
+
+/* This is mostly to avoid infinite looping when rename() destination already
+ exists as the hard link of the file itself. */
+#define MAILDIR_SCAN_DIR_MAX_COUNT 5
+
+#define DUPE_LINKS_DELETE_SECS 30
+
+enum maildir_scan_why {
+ WHY_FORCED = 0x01,
+ WHY_FIRSTSYNC = 0x02,
+ WHY_NEWCHANGED = 0x04,
+ WHY_CURCHANGED = 0x08,
+ WHY_DROPRECENT = 0x10,
+ WHY_FINDRECENT = 0x20,
+ WHY_DELAYEDNEW = 0x40,
+ WHY_DELAYEDCUR = 0x80
+};
+
+struct maildir_sync_context {
+ struct maildir_mailbox *mbox;
+ const char *new_dir, *cur_dir;
+
+ enum mailbox_sync_flags flags;
+ time_t last_touch, last_notify;
+
+ struct maildir_uidlist_sync_ctx *uidlist_sync_ctx;
+ struct maildir_index_sync_context *index_sync_ctx;
+
+ bool partial:1;
+ bool locked:1;
+ bool racing:1;
+};
+
+void maildir_sync_set_racing(struct maildir_sync_context *ctx)
+{
+ ctx->racing = TRUE;
+}
+
+void maildir_sync_notify(struct maildir_sync_context *ctx)
+{
+ time_t now;
+
+ if (ctx == NULL) {
+ /* we got here from maildir-save.c. it has no
+ maildir_sync_context, */
+ return;
+ }
+
+ now = time(NULL);
+ if (now - ctx->last_touch > MAILDIR_LOCK_TOUCH_SECS && ctx->locked) {
+ (void)maildir_uidlist_lock_touch(ctx->mbox->uidlist);
+ ctx->last_touch = now;
+ }
+ if (now - ctx->last_notify > MAIL_STORAGE_STAYALIVE_SECS) {
+ struct mailbox *box = &ctx->mbox->box;
+
+ if (box->storage->callbacks.notify_ok != NULL) {
+ box->storage->callbacks.
+ notify_ok(box, "Hang in there..",
+ box->storage->callback_context);
+ }
+ ctx->last_notify = now;
+ }
+}
+
+static struct maildir_sync_context *
+maildir_sync_context_new(struct maildir_mailbox *mbox,
+ enum mailbox_sync_flags flags)
+{
+ struct maildir_sync_context *ctx;
+
+ ctx = t_new(struct maildir_sync_context, 1);
+ ctx->mbox = mbox;
+ ctx->new_dir = t_strconcat(mailbox_get_path(&mbox->box), "/new", NULL);
+ ctx->cur_dir = t_strconcat(mailbox_get_path(&mbox->box), "/cur", NULL);
+ ctx->last_touch = ioloop_time;
+ ctx->last_notify = ioloop_time;
+ ctx->flags = flags;
+ return ctx;
+}
+
+static void maildir_sync_deinit(struct maildir_sync_context *ctx)
+{
+ if (ctx->uidlist_sync_ctx != NULL)
+ (void)maildir_uidlist_sync_deinit(&ctx->uidlist_sync_ctx, FALSE);
+ if (ctx->index_sync_ctx != NULL)
+ maildir_sync_index_rollback(&ctx->index_sync_ctx);
+ if (ctx->mbox->storage->storage.rebuild_list_index)
+ (void)mail_storage_list_index_rebuild_and_set_uncorrupted(&ctx->mbox->storage->storage);
+}
+
+static int maildir_fix_duplicate(struct maildir_sync_context *ctx,
+ const char *dir, const char *fname2)
+{
+ const char *fname1, *path1, *path2;
+ const char *new_fname, *new_path;
+ struct stat st1, st2;
+ uoff_t size;
+
+ fname1 = maildir_uidlist_sync_get_full_filename(ctx->uidlist_sync_ctx,
+ fname2);
+ i_assert(fname1 != NULL);
+
+ path1 = t_strconcat(dir, "/", fname1, NULL);
+ path2 = t_strconcat(dir, "/", fname2, NULL);
+
+ if (stat(path1, &st1) < 0 || stat(path2, &st2) < 0) {
+ /* most likely the files just don't exist anymore.
+ don't really care about other errors much. */
+ return 0;
+ }
+ if (st1.st_ino == st2.st_ino &&
+ CMP_DEV_T(st1.st_dev, st2.st_dev)) {
+ /* Files are the same. this means either a race condition
+ between stat() calls, or that the files were link()ed. */
+ if (st1.st_nlink > 1 && st2.st_nlink == st1.st_nlink &&
+ st1.st_ctime == st2.st_ctime &&
+ st1.st_ctime < ioloop_time - DUPE_LINKS_DELETE_SECS) {
+ /* The file has hard links and it hasn't had any
+ changes (such as renames) for a while, so this
+ isn't a race condition.
+
+ rename()ing one file on top of the other would fix
+ this safely, except POSIX decided that rename()
+ doesn't work that way. So we'll have unlink() one
+ and hope that another process didn't just decide to
+ unlink() the other (uidlist lock prevents this from
+ happening) */
+ if (i_unlink(path2) == 0)
+ i_warning("Unlinked a duplicate: %s", path2);
+ }
+ return 0;
+ }
+
+ new_fname = maildir_filename_generate();
+ /* preserve S= and W= sizes if they're available.
+ (S=size is required for zlib plugin to work) */
+ if (maildir_filename_get_size(fname2, MAILDIR_EXTRA_FILE_SIZE, &size)) {
+ new_fname = t_strdup_printf("%s,%c=%"PRIuUOFF_T,
+ new_fname, MAILDIR_EXTRA_FILE_SIZE, size);
+ }
+ if (maildir_filename_get_size(fname2, MAILDIR_EXTRA_VIRTUAL_SIZE, &size)) {
+ new_fname = t_strdup_printf("%s,%c=%"PRIuUOFF_T,
+ new_fname, MAILDIR_EXTRA_VIRTUAL_SIZE, size);
+ }
+ new_path = t_strconcat(mailbox_get_path(&ctx->mbox->box),
+ "/new/", new_fname, NULL);
+
+ if (rename(path2, new_path) == 0)
+ i_warning("Fixed a duplicate: %s -> %s", path2, new_fname);
+ else if (errno != ENOENT) {
+ mailbox_set_critical(&ctx->mbox->box,
+ "Couldn't fix a duplicate: rename(%s, %s) failed: %m",
+ path2, new_path);
+ return -1;
+ }
+ return 0;
+}
+
+static int
+maildir_rename_empty_basename(struct maildir_sync_context *ctx,
+ const char *dir, const char *fname)
+{
+ const char *old_path, *new_fname, *new_path;
+
+ old_path = t_strconcat(dir, "/", fname, NULL);
+ new_fname = maildir_filename_generate();
+ new_path = t_strconcat(mailbox_get_path(&ctx->mbox->box),
+ "/new/", new_fname, NULL);
+ if (rename(old_path, new_path) == 0)
+ i_warning("Fixed broken filename: %s -> %s", old_path, new_fname);
+ else if (errno != ENOENT) {
+ mailbox_set_critical(&ctx->mbox->box,
+ "Couldn't fix a broken filename: rename(%s, %s) failed: %m",
+ old_path, new_path);
+ return -1;
+ }
+ return 0;
+}
+
+static int
+maildir_stat(struct maildir_mailbox *mbox, const char *path, struct stat *st_r)
+{
+ struct mailbox *box = &mbox->box;
+ int i;
+
+ for (i = 0;; i++) {
+ if (nfs_safe_stat(path, st_r) == 0)
+ return 0;
+ if (errno != ENOENT || i == MAILDIR_DELETE_RETRY_COUNT)
+ break;
+
+ if (!maildir_set_deleted(box))
+ return -1;
+ /* try again */
+ }
+
+ mailbox_set_critical(box, "stat(%s) failed: %m", path);
+ return -1;
+}
+
+static int
+maildir_scan_dir(struct maildir_sync_context *ctx, bool new_dir, bool final,
+ enum maildir_scan_why why)
+{
+ const char *path;
+ DIR *dirp;
+ string_t *src, *dest;
+ struct dirent *dp;
+ struct stat st;
+ enum maildir_uidlist_rec_flag flags;
+ unsigned int time_diff, i, readdir_count = 0, move_count = 0;
+ time_t start_time;
+ int ret = 1;
+ bool move_new, dir_changed = FALSE;
+
+ path = new_dir ? ctx->new_dir : ctx->cur_dir;
+ for (i = 0;; i++) {
+ dirp = opendir(path);
+ if (dirp != NULL)
+ break;
+
+ if (errno != ENOENT || i == MAILDIR_DELETE_RETRY_COUNT) {
+ if (errno == EACCES) {
+ mailbox_set_critical(&ctx->mbox->box, "%s",
+ eacces_error_get("opendir", path));
+ } else {
+ mailbox_set_critical(&ctx->mbox->box,
+ "opendir(%s) failed: %m", path);
+ }
+ return -1;
+ }
+
+ if (!maildir_set_deleted(&ctx->mbox->box))
+ return -1;
+ /* try again */
+ }
+
+#ifdef HAVE_DIRFD
+ if (fstat(dirfd(dirp), &st) < 0) {
+ mailbox_set_critical(&ctx->mbox->box,
+ "fstat(%s) failed: %m", path);
+ (void)closedir(dirp);
+ return -1;
+ }
+#else
+ if (maildir_stat(ctx->mbox, path, &st) < 0) {
+ (void)closedir(dirp);
+ return -1;
+ }
+#endif
+
+ start_time = time(NULL);
+ if (new_dir) {
+ ctx->mbox->maildir_hdr.new_check_time = start_time;
+ ctx->mbox->maildir_hdr.new_mtime = st.st_mtime;
+ ctx->mbox->maildir_hdr.new_mtime_nsecs = ST_MTIME_NSEC(st);
+ } else {
+ ctx->mbox->maildir_hdr.cur_check_time = start_time;
+ ctx->mbox->maildir_hdr.cur_mtime = st.st_mtime;
+ ctx->mbox->maildir_hdr.cur_mtime_nsecs = ST_MTIME_NSEC(st);
+ }
+
+ src = t_str_new(1024);
+ dest = t_str_new(1024);
+
+ move_new = new_dir && ctx->locked &&
+ ((ctx->mbox->box.flags & MAILBOX_FLAG_DROP_RECENT) != 0 ||
+ ctx->mbox->storage->set->maildir_empty_new);
+
+ errno = 0;
+ for (; (dp = readdir(dirp)) != NULL; errno = 0) {
+ if (dp->d_name[0] == '.')
+ continue;
+
+ if (dp->d_name[0] == MAILDIR_INFO_SEP) {
+ /* don't even try to use file with empty base name */
+ if (maildir_rename_empty_basename(ctx, path,
+ dp->d_name) < 0)
+ break;
+ continue;
+ }
+
+ flags = 0;
+ if (move_new) {
+ i_assert(dp->d_name[0] != '\0');
+
+ str_truncate(src, 0);
+ str_truncate(dest, 0);
+ str_printfa(src, "%s/%s", ctx->new_dir, dp->d_name);
+ str_printfa(dest, "%s/%s", ctx->cur_dir, dp->d_name);
+ if (strchr(dp->d_name, MAILDIR_INFO_SEP) == NULL) {
+ str_append(dest, MAILDIR_FLAGS_FULL_SEP);
+ }
+ if (rename(str_c(src), str_c(dest)) == 0) {
+ /* we moved it - it's \Recent for us */
+ dir_changed = TRUE;
+ move_count++;
+ flags |= MAILDIR_UIDLIST_REC_FLAG_MOVED |
+ MAILDIR_UIDLIST_REC_FLAG_RECENT;
+ } else if (ENOTFOUND(errno)) {
+ /* someone else moved it already */
+ dir_changed = TRUE;
+ move_count++;
+ flags |= MAILDIR_UIDLIST_REC_FLAG_MOVED |
+ MAILDIR_UIDLIST_REC_FLAG_RECENT;
+ } else if (ENOSPACE(errno) || errno == EACCES) {
+ /* not enough disk space / read-only maildir,
+ leave here */
+ flags |= MAILDIR_UIDLIST_REC_FLAG_NEW_DIR;
+ move_new = FALSE;
+ } else {
+ flags |= MAILDIR_UIDLIST_REC_FLAG_NEW_DIR;
+ mailbox_set_critical(&ctx->mbox->box,
+ "rename(%s, %s) failed: %m",
+ str_c(src), str_c(dest));
+ }
+ if ((move_count % MAILDIR_SLOW_MOVE_COUNT) == 0)
+ maildir_sync_notify(ctx);
+ } else if (new_dir) {
+ flags |= MAILDIR_UIDLIST_REC_FLAG_NEW_DIR |
+ MAILDIR_UIDLIST_REC_FLAG_RECENT;
+ }
+
+ readdir_count++;
+ if ((readdir_count % MAILDIR_SLOW_CHECK_COUNT) == 0)
+ maildir_sync_notify(ctx);
+
+ ret = maildir_uidlist_sync_next(ctx->uidlist_sync_ctx,
+ dp->d_name, flags);
+ if (ret <= 0) {
+ if (ret < 0)
+ break;
+
+ /* possibly duplicate - try fixing it */
+ T_BEGIN {
+ ret = maildir_fix_duplicate(ctx, path,
+ dp->d_name);
+ } T_END;
+ if (ret < 0)
+ break;
+ }
+ }
+
+#ifdef __APPLE__
+ if (errno == EINVAL && move_count > 0 && !final) {
+ /* OS X HFS+: readdir() fails sometimes when rename()
+ have been done. */
+ move_count = MAILDIR_RENAME_RESCAN_COUNT + 1;
+ } else
+#endif
+
+ if (errno != 0) {
+ mailbox_set_critical(&ctx->mbox->box,
+ "readdir(%s) failed: %m", path);
+ ret = -1;
+ }
+
+ if (closedir(dirp) < 0) {
+ mailbox_set_critical(&ctx->mbox->box,
+ "closedir(%s) failed: %m", path);
+ ret = -1;
+ }
+
+ if (dir_changed) {
+ /* save the exact new times. the new mtimes should be >=
+ "start_time", but just in case something weird happens and
+ mtime doesn't update, use "start_time". */
+ if (stat(ctx->new_dir, &st) == 0) {
+ ctx->mbox->maildir_hdr.new_check_time =
+ I_MAX(st.st_mtime, start_time);
+ ctx->mbox->maildir_hdr.new_mtime = st.st_mtime;
+ ctx->mbox->maildir_hdr.new_mtime_nsecs =
+ ST_MTIME_NSEC(st);
+ }
+ if (stat(ctx->cur_dir, &st) == 0) {
+ ctx->mbox->maildir_hdr.new_check_time =
+ I_MAX(st.st_mtime, start_time);
+ ctx->mbox->maildir_hdr.cur_mtime = st.st_mtime;
+ ctx->mbox->maildir_hdr.cur_mtime_nsecs =
+ ST_MTIME_NSEC(st);
+ }
+ }
+ time_diff = time(NULL) - start_time;
+ if (time_diff >= MAILDIR_SYNC_TIME_WARN_SECS) {
+ i_warning("Maildir: Scanning %s took %u seconds "
+ "(%u readdir()s, %u rename()s to cur/, why=0x%x)",
+ path, time_diff, readdir_count, move_count, why);
+ }
+
+ return ret < 0 ? -1 :
+ (move_count <= MAILDIR_RENAME_RESCAN_COUNT || final ? 0 : 1);
+}
+
+static void maildir_sync_get_header(struct maildir_mailbox *mbox)
+{
+ const void *data;
+ size_t data_size;
+
+ mail_index_get_header_ext(mbox->box.view, mbox->maildir_ext_id,
+ &data, &data_size);
+ if (data_size == 0) {
+ /* header doesn't exist */
+ } else {
+ memcpy(&mbox->maildir_hdr, data,
+ I_MIN(sizeof(mbox->maildir_hdr), data_size));
+ }
+}
+
+int maildir_sync_header_refresh(struct maildir_mailbox *mbox)
+{
+ if (mail_index_refresh(mbox->box.index) < 0) {
+ mailbox_set_index_error(&mbox->box);
+ return -1;
+ }
+ maildir_sync_get_header(mbox);
+ return 0;
+}
+
+static int maildir_sync_quick_check(struct maildir_mailbox *mbox, bool undirty,
+ const char *new_dir, const char *cur_dir,
+ bool *new_changed_r, bool *cur_changed_r,
+ enum maildir_scan_why *why_r)
+{
+#define DIR_DELAYED_REFRESH(hdr, name) \
+ ((hdr)->name ## _check_time <= \
+ (hdr)->name ## _mtime + MAILDIR_SYNC_SECS && \
+ (undirty || \
+ (time_t)(hdr)->name ## _check_time < ioloop_time - MAILDIR_SYNC_SECS))
+
+#define DIR_MTIME_CHANGED(st, hdr, name) \
+ ((st).st_mtime != (time_t)(hdr)->name ## _mtime || \
+ !ST_NTIMES_EQUAL(ST_MTIME_NSEC(st), (hdr)->name ## _mtime_nsecs))
+
+ struct maildir_index_header *hdr = &mbox->maildir_hdr;
+ struct stat new_st, cur_st;
+ bool refreshed = FALSE, check_new = FALSE, check_cur = FALSE;
+
+ *why_r = 0;
+
+ if (mbox->maildir_hdr.new_mtime == 0) {
+ maildir_sync_get_header(mbox);
+ if (mbox->maildir_hdr.new_mtime == 0) {
+ /* first sync */
+ *why_r |= WHY_FIRSTSYNC;
+ *new_changed_r = *cur_changed_r = TRUE;
+ return 0;
+ }
+ }
+
+ *new_changed_r = *cur_changed_r = FALSE;
+
+ /* try to avoid stat()ing by first checking delayed changes */
+ if (DIR_DELAYED_REFRESH(hdr, new) ||
+ (DIR_DELAYED_REFRESH(hdr, cur) &&
+ !mbox->storage->set->maildir_very_dirty_syncs)) {
+ /* refresh index and try again */
+ if (maildir_sync_header_refresh(mbox) < 0)
+ return -1;
+ refreshed = TRUE;
+
+ if (DIR_DELAYED_REFRESH(hdr, new)) {
+ *why_r |= WHY_DELAYEDNEW;
+ *new_changed_r = TRUE;
+ }
+ if (DIR_DELAYED_REFRESH(hdr, cur) &&
+ !mbox->storage->set->maildir_very_dirty_syncs) {
+ *why_r |= WHY_DELAYEDCUR;
+ *cur_changed_r = TRUE;
+ }
+ if (*new_changed_r && *cur_changed_r)
+ return 0;
+ }
+
+ if (!*new_changed_r) {
+ if (maildir_stat(mbox, new_dir, &new_st) < 0)
+ return -1;
+ check_new = TRUE;
+ }
+ if (!*cur_changed_r) {
+ if (maildir_stat(mbox, cur_dir, &cur_st) < 0)
+ return -1;
+ check_cur = TRUE;
+ }
+
+ for (;;) {
+ if (check_new) {
+ *new_changed_r = DIR_MTIME_CHANGED(new_st, hdr, new);
+ if (*new_changed_r)
+ *why_r |= WHY_NEWCHANGED;
+ }
+ if (check_cur) {
+ *cur_changed_r = DIR_MTIME_CHANGED(cur_st, hdr, cur);
+ if (*cur_changed_r)
+ *why_r |= WHY_CURCHANGED;
+ }
+
+ if ((!*new_changed_r && !*cur_changed_r) || refreshed)
+ break;
+
+ /* refresh index and try again */
+ if (maildir_sync_header_refresh(mbox) < 0)
+ return -1;
+ refreshed = TRUE;
+ }
+
+ return 0;
+}
+
+static void maildir_sync_update_next_uid(struct maildir_mailbox *mbox)
+{
+ const struct mail_index_header *hdr;
+ uint32_t uid_validity;
+
+ hdr = mail_index_get_header(mbox->box.view);
+ if (hdr->uid_validity == 0)
+ return;
+
+ uid_validity = maildir_uidlist_get_uid_validity(mbox->uidlist);
+ if (uid_validity == hdr->uid_validity || uid_validity == 0) {
+ /* make sure uidlist's next_uid is at least as large as
+ index file's. typically this happens only if uidlist gets
+ deleted. */
+ maildir_uidlist_set_uid_validity(mbox->uidlist,
+ hdr->uid_validity);
+ maildir_uidlist_set_next_uid(mbox->uidlist,
+ hdr->next_uid, FALSE);
+ }
+}
+
+static bool
+have_recent_messages(struct maildir_sync_context *ctx, bool seen_changes)
+{
+ const struct mail_index_header *hdr;
+ uint32_t next_uid;
+
+ hdr = mail_index_get_header(ctx->mbox->box.view);
+ if (!seen_changes) {
+ /* index is up to date. get the next-uid from it */
+ next_uid = hdr->next_uid;
+ } else {
+ (void)maildir_uidlist_refresh(ctx->mbox->uidlist);
+ next_uid = maildir_uidlist_get_next_uid(ctx->mbox->uidlist);
+ }
+ return hdr->first_recent_uid < next_uid;
+}
+
+static int maildir_sync_get_changes(struct maildir_sync_context *ctx,
+ bool *new_changed_r, bool *cur_changed_r,
+ enum maildir_scan_why *why_r)
+{
+ struct maildir_mailbox *mbox = ctx->mbox;
+ enum mail_index_sync_flags flags = 0;
+ bool undirty = (ctx->flags & MAILBOX_SYNC_FLAG_FULL_READ) != 0;
+
+ *why_r = 0;
+
+ if (maildir_sync_quick_check(mbox, undirty, ctx->new_dir, ctx->cur_dir,
+ new_changed_r, cur_changed_r, why_r) < 0)
+ return -1;
+
+ /* if there are files in new/, we'll need to move them. we'll check
+ this by seeing if we have any recent messages */
+ if ((mbox->box.flags & MAILBOX_FLAG_DROP_RECENT) != 0) {
+ if (!*new_changed_r && have_recent_messages(ctx, FALSE)) {
+ *new_changed_r = TRUE;
+ *why_r |= WHY_DROPRECENT;
+ }
+ } else if (*new_changed_r) {
+ /* if recent messages have been externally deleted from new/,
+ we need to get them out of index. this requires that
+ we make sure they weren't just moved to cur/. */
+ if (!*cur_changed_r && have_recent_messages(ctx, TRUE)) {
+ *cur_changed_r = TRUE;
+ *why_r |= WHY_FINDRECENT;
+ }
+ }
+
+ if (*new_changed_r || *cur_changed_r)
+ return 1;
+
+ if ((mbox->box.flags & MAILBOX_FLAG_DROP_RECENT) != 0)
+ flags |= MAIL_INDEX_SYNC_FLAG_DROP_RECENT;
+
+ if (mbox->synced) {
+ /* refresh index only after the first sync, i.e. avoid wasting
+ time on refreshing it immediately after it was just opened */
+ mail_index_refresh(mbox->box.index);
+ }
+ return mail_index_sync_have_any(mbox->box.index, flags) ? 1 : 0;
+}
+
+static int ATTR_NULL(3)
+maildir_sync_context(struct maildir_sync_context *ctx, bool forced,
+ uint32_t *find_uid, bool *lost_files_r)
+{
+ enum maildir_uidlist_sync_flags sync_flags;
+ enum maildir_uidlist_rec_flag flags;
+ bool new_changed, cur_changed, lock_failure;
+ const char *fname;
+ enum maildir_scan_why why;
+ int ret;
+
+ *lost_files_r = FALSE;
+
+ if (forced) {
+ new_changed = cur_changed = TRUE;
+ why = WHY_FORCED;
+ } else {
+ ret = maildir_sync_get_changes(ctx, &new_changed, &cur_changed,
+ &why);
+ if (ret <= 0)
+ return ret;
+ }
+
+ /*
+ Locking, locking, locking.. Wasn't maildir supposed to be lockless?
+
+ We can get here either as beginning a real maildir sync, or when
+ committing changes to maildir but a file was lost (maybe renamed).
+
+ So, we're going to need two locks. One for index and one for
+ uidlist. To avoid deadlocking do the uidlist lock always first.
+
+ uidlist is needed only for figuring out UIDs for newly seen files,
+ so theoretically we wouldn't need to lock it unless there are new
+ files. It has a few problems though, assuming the index lock didn't
+ already protect it (eg. in-memory indexes):
+
+ 1. Just because you see a new file which doesn't exist in uidlist
+ file, doesn't mean that the file really exists anymore, or that
+ your readdir() lists all new files. Meaning that this is possible:
+
+ A: opendir(), readdir() -> new file ...
+ -- new files are written to the maildir --
+ B: opendir(), readdir() -> new file, lock uidlist,
+ readdir() -> another new file, rewrite uidlist, unlock
+ A: ... lock uidlist, readdir() -> nothing left, rewrite uidlist,
+ unlock
+
+ The second time running A didn't see the two new files. To handle
+ this correctly, it must not remove the new unseen files from
+ uidlist. This is possible to do, but adds extra complexity.
+
+ 2. If another process is rename()ing files while we are
+ readdir()ing, it's possible that readdir() never lists some files,
+ causing Dovecot to assume they were expunged. In next sync they
+ would show up again, but client could have already been notified of
+ that and they would show up under new UIDs, so the damage is
+ already done.
+
+ Both of the problems can be avoided if we simply lock the uidlist
+ before syncing and keep it until sync is finished. Typically this
+ would happen in any case, as there is the index lock..
+
+ The second case is still a problem with external changes though,
+ because maildir doesn't require any kind of locking. Luckily this
+ problem rarely happens except under high amount of modifications.
+ */
+
+ if (!cur_changed) {
+ ctx->partial = TRUE;
+ sync_flags = MAILDIR_UIDLIST_SYNC_PARTIAL;
+ } else {
+ ctx->partial = FALSE;
+ sync_flags = 0;
+ if (forced)
+ sync_flags |= MAILDIR_UIDLIST_SYNC_FORCE;
+ if ((ctx->flags & MAILBOX_SYNC_FLAG_FAST) != 0)
+ sync_flags |= MAILDIR_UIDLIST_SYNC_TRYLOCK;
+ }
+ ret = maildir_uidlist_sync_init(ctx->mbox->uidlist, sync_flags,
+ &ctx->uidlist_sync_ctx);
+ lock_failure = ret <= 0;
+ if (ret <= 0) {
+ struct mail_storage *storage = ctx->mbox->box.storage;
+
+ if (ret == 0) {
+ /* timeout */
+ return 0;
+ }
+ /* locking failed. sync anyway without locking so that it's
+ possible to expunge messages when out of quota. */
+ if (forced) {
+ /* we're already forcing a sync, we're trying to find
+ a message that was probably already expunged, don't
+ loop for a long time trying to find it. */
+ return -1;
+ }
+ ret = maildir_uidlist_sync_init(ctx->mbox->uidlist, sync_flags |
+ MAILDIR_UIDLIST_SYNC_NOLOCK,
+ &ctx->uidlist_sync_ctx);
+ if (ret <= 0) {
+ i_assert(ret != 0);
+ return -1;
+ }
+
+ if (storage->callbacks.notify_no != NULL) {
+ storage->callbacks.notify_no(&ctx->mbox->box,
+ "Internal mailbox synchronization failure, "
+ "showing only old mails.",
+ storage->callback_context);
+ }
+ }
+ ctx->locked = maildir_uidlist_is_locked(ctx->mbox->uidlist);
+ if (!ctx->locked)
+ ctx->partial = TRUE;
+
+ if (!ctx->mbox->syncing_commit && (ctx->locked || lock_failure)) {
+ if (maildir_sync_index_begin(ctx->mbox, ctx,
+ &ctx->index_sync_ctx) < 0)
+ return -1;
+ }
+
+ if (new_changed || cur_changed) {
+ /* if we're going to check cur/ dir our current logic requires
+ that new/ dir is checked as well. it's a good idea anyway. */
+ unsigned int count = 0;
+ bool final = FALSE;
+
+ while ((ret = maildir_scan_dir(ctx, TRUE, final, why)) > 0) {
+ /* rename()d at least some files, which might have
+ caused some other files to be missed. check again
+ (see MAILDIR_RENAME_RESCAN_COUNT). */
+ if (++count >= MAILDIR_SCAN_DIR_MAX_COUNT)
+ final = TRUE;
+ }
+ if (ret < 0)
+ return -1;
+
+ if (cur_changed) {
+ if (maildir_scan_dir(ctx, FALSE, TRUE, why) < 0)
+ return -1;
+ }
+
+ maildir_sync_update_next_uid(ctx->mbox);
+
+ /* finish uidlist syncing, but keep it still locked */
+ maildir_uidlist_sync_finish(ctx->uidlist_sync_ctx);
+ }
+
+ if (!ctx->locked) {
+ /* make sure we sync the maildir later */
+ ctx->mbox->maildir_hdr.new_mtime = 0;
+ ctx->mbox->maildir_hdr.cur_mtime = 0;
+ }
+
+ if (ctx->index_sync_ctx != NULL) {
+ /* NOTE: index syncing here might cause a re-sync due to
+ files getting lost, so this function might be called
+ reentrantly. */
+ ret = maildir_sync_index(ctx->index_sync_ctx, ctx->partial);
+ if (ret < 0)
+ maildir_sync_index_rollback(&ctx->index_sync_ctx);
+ else if (maildir_sync_index_commit(&ctx->index_sync_ctx) < 0)
+ return -1;
+
+ if (ret < 0)
+ return -1;
+ if (ret == 0)
+ *lost_files_r = TRUE;
+
+ i_assert(maildir_uidlist_is_locked(ctx->mbox->uidlist) ||
+ lock_failure);
+ }
+
+ if (find_uid != NULL && *find_uid != 0) {
+ ret = maildir_uidlist_lookup(ctx->mbox->uidlist,
+ *find_uid, &flags, &fname);
+ if (ret < 0)
+ return -1;
+ if (ret == 0) {
+ /* UID is expunged */
+ *find_uid = 0;
+ } else if ((flags & MAILDIR_UIDLIST_REC_FLAG_NONSYNCED) == 0) {
+ *find_uid = 0;
+ } else {
+ /* we didn't find it, possibly expunged? */
+ }
+ }
+
+ return maildir_uidlist_sync_deinit(&ctx->uidlist_sync_ctx, TRUE);
+}
+
+int maildir_sync_lookup(struct maildir_mailbox *mbox, uint32_t uid,
+ enum maildir_uidlist_rec_flag *flags_r,
+ const char **fname_r)
+{
+ int ret;
+
+ ret = maildir_uidlist_lookup(mbox->uidlist, uid, flags_r, fname_r);
+ if (ret != 0)
+ return ret;
+
+ if (maildir_uidlist_is_open(mbox->uidlist)) {
+ /* refresh uidlist and check again in case it was added
+ after the last mailbox sync */
+ if (mbox->sync_uidlist_refreshed) {
+ /* we've already refreshed it, don't bother again */
+ return ret;
+ }
+ mbox->sync_uidlist_refreshed = TRUE;
+ if (maildir_uidlist_refresh(mbox->uidlist) < 0)
+ return -1;
+ } else {
+ /* the uidlist doesn't exist. */
+ if (maildir_storage_sync_force(mbox, uid) < 0)
+ return -1;
+ }
+
+ /* try again */
+ return maildir_uidlist_lookup(mbox->uidlist, uid, flags_r, fname_r);
+}
+
+static int maildir_sync_run(struct maildir_mailbox *mbox,
+ enum mailbox_sync_flags flags, bool force_resync,
+ uint32_t *uid, bool *lost_files_r)
+{
+ struct maildir_sync_context *ctx;
+ bool retry, lost_files;
+ int ret;
+
+ T_BEGIN {
+ ctx = maildir_sync_context_new(mbox, flags);
+ ret = maildir_sync_context(ctx, force_resync, uid, lost_files_r);
+ retry = ctx->racing;
+ maildir_sync_deinit(ctx);
+ } T_END;
+
+ if (retry) T_BEGIN {
+ /* we're racing some file. retry the sync again to see if the
+ file is really gone or not. if it is, this is a bit of
+ unnecessary work, but if it's not, this is necessary for
+ e.g. doveadm force-resync to work. */
+ ctx = maildir_sync_context_new(mbox, 0);
+ ret = maildir_sync_context(ctx, TRUE, NULL, &lost_files);
+ maildir_sync_deinit(ctx);
+ } T_END;
+ return ret;
+}
+
+int maildir_storage_sync_force(struct maildir_mailbox *mbox, uint32_t uid)
+{
+ bool lost_files;
+ int ret;
+
+ ret = maildir_sync_run(mbox, MAILBOX_SYNC_FLAG_FAST,
+ TRUE, &uid, &lost_files);
+ if (uid != 0) {
+ /* maybe it's expunged. check again. */
+ ret = maildir_sync_run(mbox, 0, TRUE, NULL, &lost_files);
+ }
+ return ret;
+}
+
+int maildir_sync_refresh_flags_view(struct maildir_mailbox *mbox)
+{
+ struct mail_index_view_sync_ctx *sync_ctx;
+ bool delayed_expunges;
+
+ mail_index_refresh(mbox->box.index);
+ if (mbox->flags_view == NULL)
+ mbox->flags_view = mail_index_view_open(mbox->box.index);
+
+ sync_ctx = mail_index_view_sync_begin(mbox->flags_view,
+ MAIL_INDEX_VIEW_SYNC_FLAG_FIX_INCONSISTENT);
+ if (mail_index_view_sync_commit(&sync_ctx, &delayed_expunges) < 0) {
+ mailbox_set_index_error(&mbox->box);
+ return -1;
+ }
+ /* make sure the map stays in private memory */
+ if (mbox->flags_view->map->refcount > 1) {
+ struct mail_index_map *map;
+
+ map = mail_index_map_clone(mbox->flags_view->map);
+ mail_index_unmap(&mbox->flags_view->map);
+ mbox->flags_view->map = map;
+ }
+ mail_index_record_map_move_to_private(mbox->flags_view->map);
+ mail_index_map_move_to_memory(mbox->flags_view->map);
+ return 0;
+}
+
+struct mailbox_sync_context *
+maildir_storage_sync_init(struct mailbox *box, enum mailbox_sync_flags flags)
+{
+ struct maildir_mailbox *mbox = MAILDIR_MAILBOX(box);
+ bool lost_files, force_resync;
+ int ret = 0;
+
+ force_resync = (flags & MAILBOX_SYNC_FLAG_FORCE_RESYNC) != 0;
+ if (index_mailbox_want_full_sync(&mbox->box, flags)) {
+ ret = maildir_sync_run(mbox, flags, force_resync,
+ NULL, &lost_files);
+ i_assert(!maildir_uidlist_is_locked(mbox->uidlist) ||
+ (box->flags & MAILBOX_FLAG_KEEP_LOCKED) != 0);
+
+ if (lost_files) {
+ /* lost some files from new/, see if they're in cur/ */
+ ret = maildir_storage_sync_force(mbox, 0);
+ }
+ }
+
+ if (mbox->storage->set->maildir_very_dirty_syncs) {
+ if (maildir_sync_refresh_flags_view(mbox) < 0)
+ ret = -1;
+ maildir_uidlist_set_all_nonsynced(mbox->uidlist);
+ }
+ mbox->synced = TRUE;
+ mbox->sync_uidlist_refreshed = FALSE;
+ return index_mailbox_sync_init(box, flags, ret < 0);
+}
+
+int maildir_sync_is_synced(struct maildir_mailbox *mbox)
+{
+ bool new_changed, cur_changed;
+ enum maildir_scan_why why;
+ int ret;
+
+ T_BEGIN {
+ const char *box_path = mailbox_get_path(&mbox->box);
+ const char *new_dir, *cur_dir;
+
+ new_dir = t_strconcat(box_path, "/new", NULL);
+ cur_dir = t_strconcat(box_path, "/cur", NULL);
+
+ ret = maildir_sync_quick_check(mbox, FALSE, new_dir, cur_dir,
+ &new_changed, &cur_changed,
+ &why);
+ } T_END;
+ return ret < 0 ? -1 : (!new_changed && !cur_changed);
+}
diff --git a/src/lib-storage/index/maildir/maildir-sync.h b/src/lib-storage/index/maildir/maildir-sync.h
new file mode 100644
index 0000000..9bc6db4
--- /dev/null
+++ b/src/lib-storage/index/maildir/maildir-sync.h
@@ -0,0 +1,59 @@
+#ifndef MAILDIR_SYNC_H
+#define MAILDIR_SYNC_H
+
+/* All systems accessing the filesystem must have their clock less than this
+ many seconds apart from each others. 0 works only for local filesystems. */
+#define MAILDIR_SYNC_SECS 1
+
+/* After moving this many mails from new/ to cur/, check if we need to touch
+ the uidlist lock. */
+#define MAILDIR_SLOW_MOVE_COUNT 100
+/* readdir() should be pretty fast to do, but check anyway every n files
+ to see if we need to touch the uidlist lock. */
+#define MAILDIR_SLOW_CHECK_COUNT 10000
+/* If syncing takes longer than this, log a warning. */
+#define MAILDIR_SYNC_TIME_WARN_SECS MAIL_TRANSACTION_LOG_LOCK_WARN_SECS
+
+struct maildir_mailbox;
+struct maildir_sync_context;
+struct maildir_keywords_sync_ctx;
+struct maildir_index_sync_context;
+
+int maildir_sync_is_synced(struct maildir_mailbox *mbox);
+
+struct mailbox_sync_context *
+maildir_storage_sync_init(struct mailbox *box, enum mailbox_sync_flags flags);
+int maildir_storage_sync_force(struct maildir_mailbox *mbox, uint32_t uid);
+
+int maildir_sync_header_refresh(struct maildir_mailbox *mbox);
+
+int maildir_sync_index_begin(struct maildir_mailbox *mbox,
+ struct maildir_sync_context *maildir_sync_ctx,
+ struct maildir_index_sync_context **ctx_r)
+ ATTR_NULL(2);
+int maildir_sync_index(struct maildir_index_sync_context *sync_ctx,
+ bool partial);
+int maildir_sync_index_commit(struct maildir_index_sync_context **_ctx);
+void maildir_sync_index_rollback(struct maildir_index_sync_context **_ctx);
+
+struct maildir_keywords_sync_ctx *
+maildir_sync_get_keywords_sync_ctx(struct maildir_index_sync_context *ctx);
+void maildir_sync_set_racing(struct maildir_sync_context *ctx);
+void maildir_sync_notify(struct maildir_sync_context *ctx);
+void maildir_sync_set_new_msgs_count(struct maildir_index_sync_context *ctx,
+ unsigned int count);
+int maildir_sync_refresh_flags_view(struct maildir_mailbox *mbox);
+
+int maildir_sync_lookup(struct maildir_mailbox *mbox, uint32_t uid,
+ enum maildir_uidlist_rec_flag *flags_r,
+ const char **fname_r);
+
+int maildir_list_index_has_changed(struct mailbox *box,
+ struct mail_index_view *list_view,
+ uint32_t seq, bool quick,
+ const char **reason_r);
+void maildir_list_index_update_sync(struct mailbox *box,
+ struct mail_index_transaction *trans,
+ uint32_t seq);
+
+#endif
diff --git a/src/lib-storage/index/maildir/maildir-uidlist.c b/src/lib-storage/index/maildir/maildir-uidlist.c
new file mode 100644
index 0000000..bb17b9c
--- /dev/null
+++ b/src/lib-storage/index/maildir/maildir-uidlist.c
@@ -0,0 +1,2151 @@
+/* Copyright (c) 2003-2018 Dovecot authors, see the included COPYING file */
+
+/*
+ Version 1 format has been used for most versions of Dovecot up to v1.0.x.
+ It's also compatible with Courier IMAP's courierimapuiddb file.
+ The format is:
+
+ header: 1 <uid validity> <next uid>
+ entry: <uid> <filename>
+
+ --
+
+ Version 2 format was written by a few development Dovecot versions, but
+ v1.0.x still parses the format. The format has <flags> field after <uid>.
+
+ --
+
+ Version 3 format is an extensible format used by Dovecot v1.1 and later.
+ It's also parsed by v1.0.2 (and later). The format is:
+
+ header: 3 [<key><value> ...]
+ entry: <uid> [<key><value> ...] :<filename>
+
+ See enum maildir_uidlist_*_ext_key for used keys.
+*/
+
+#include "lib.h"
+#include "array.h"
+#include "hash.h"
+#include "istream.h"
+#include "ostream.h"
+#include "str.h"
+#include "file-dotlock.h"
+#include "nfs-workarounds.h"
+#include "eacces-error.h"
+#include "maildir-storage.h"
+#include "maildir-filename.h"
+#include "maildir-uidlist.h"
+
+#include <stdio.h>
+#include <sys/stat.h>
+
+/* NFS: How many times to retry reading dovecot-uidlist file if ESTALE
+ error occurs in the middle of reading it */
+#define UIDLIST_ESTALE_RETRY_COUNT NFS_ESTALE_RETRY_COUNT
+
+#define UIDLIST_VERSION 3
+#define UIDLIST_COMPRESS_PERCENTAGE 75
+
+#define UIDLIST_IS_LOCKED(uidlist) \
+ ((uidlist)->lock_count > 0)
+
+struct maildir_uidlist_rec {
+ uint32_t uid;
+ uint32_t flags;
+ char *filename;
+ unsigned char *extensions; /* <data>\0[<data>\0 ...]\0 */
+};
+ARRAY_DEFINE_TYPE(maildir_uidlist_rec_p, struct maildir_uidlist_rec *);
+
+HASH_TABLE_DEFINE_TYPE(path_to_maildir_uidlist_rec,
+ char *, struct maildir_uidlist_rec *);
+
+struct maildir_uidlist {
+ struct mailbox *box;
+ char *path;
+ struct maildir_index_header *mhdr;
+
+ int fd;
+ dev_t fd_dev;
+ ino_t fd_ino;
+ off_t fd_size;
+
+ unsigned int lock_count;
+
+ struct dotlock_settings dotlock_settings;
+ struct dotlock *dotlock;
+
+ pool_t record_pool;
+ ARRAY_TYPE(maildir_uidlist_rec_p) records;
+ HASH_TABLE_TYPE(path_to_maildir_uidlist_rec) files;
+ unsigned int change_counter;
+
+ unsigned int version;
+ unsigned int uid_validity, next_uid, prev_read_uid, last_seen_uid;
+ unsigned int hdr_next_uid;
+ unsigned int read_records_count, read_line_count;
+ uoff_t last_read_offset;
+ string_t *hdr_extensions;
+
+ guid_128_t mailbox_guid;
+
+ bool recreate:1;
+ bool recreate_on_change:1;
+ bool initial_read:1;
+ bool initial_hdr_read:1;
+ bool retry_rewind:1;
+ bool locked_refresh:1;
+ bool unsorted:1;
+ bool have_mailbox_guid:1;
+ bool opened_readonly:1;
+};
+
+struct maildir_uidlist_sync_ctx {
+ struct maildir_uidlist *uidlist;
+ enum maildir_uidlist_sync_flags sync_flags;
+
+ pool_t record_pool;
+ ARRAY_TYPE(maildir_uidlist_rec_p) records;
+ HASH_TABLE_TYPE(path_to_maildir_uidlist_rec) files;
+
+ unsigned int first_unwritten_pos, first_new_pos;
+ unsigned int new_files_count;
+ unsigned int finish_change_counter;
+
+ bool partial:1;
+ bool finished:1;
+ bool changed:1;
+ bool failed:1;
+ bool locked:1;
+};
+
+struct maildir_uidlist_iter_ctx {
+ struct maildir_uidlist *uidlist;
+ struct maildir_uidlist_rec *const *next, *const *end;
+
+ unsigned int change_counter;
+ uint32_t prev_uid;
+};
+
+static int maildir_uidlist_open_latest(struct maildir_uidlist *uidlist);
+static bool maildir_uidlist_iter_next_rec(struct maildir_uidlist_iter_ctx *ctx,
+ struct maildir_uidlist_rec **rec_r);
+
+static int maildir_uidlist_lock_timeout(struct maildir_uidlist *uidlist,
+ bool nonblock, bool refresh,
+ bool refresh_when_locked)
+{
+ struct mailbox *box = uidlist->box;
+ const struct mailbox_permissions *perm = mailbox_get_permissions(box);
+ const char *path = uidlist->path;
+ mode_t old_mask;
+ const enum dotlock_create_flags dotlock_flags =
+ nonblock ? DOTLOCK_CREATE_FLAG_NONBLOCK : 0;
+ int i, ret;
+
+ if (uidlist->lock_count > 0) {
+ if (!uidlist->locked_refresh && refresh_when_locked) {
+ if (maildir_uidlist_refresh(uidlist) < 0)
+ return -1;
+ }
+ uidlist->lock_count++;
+ return 1;
+ }
+
+ index_storage_lock_notify_reset(box);
+
+ for (i = 0;; i++) {
+ old_mask = umask(0777 & ~perm->file_create_mode);
+ ret = file_dotlock_create(&uidlist->dotlock_settings, path,
+ dotlock_flags, &uidlist->dotlock);
+ umask(old_mask);
+ if (ret > 0)
+ break;
+
+ /* failure */
+ if (ret == 0) {
+ mail_storage_set_error(box->storage,
+ MAIL_ERROR_TEMP, MAIL_ERRSTR_LOCK_TIMEOUT);
+ return 0;
+ }
+ if (errno != ENOENT || i == MAILDIR_DELETE_RETRY_COUNT) {
+ if (errno == EACCES) {
+ mailbox_set_critical(box, "%s",
+ eacces_error_get_creating("file_dotlock_create", path));
+ } else {
+ mailbox_set_critical(box,
+ "file_dotlock_create(%s) failed: %m",
+ path);
+ }
+ return -1;
+ }
+ /* the control dir doesn't exist. create it unless the whole
+ mailbox was just deleted. */
+ if (!maildir_set_deleted(uidlist->box))
+ return -1;
+ }
+
+ uidlist->lock_count++;
+ uidlist->locked_refresh = FALSE;
+
+ if (refresh) {
+ /* make sure we have the latest changes before
+ changing anything */
+ if (maildir_uidlist_refresh(uidlist) < 0) {
+ maildir_uidlist_unlock(uidlist);
+ return -1;
+ }
+ }
+ return 1;
+}
+
+int maildir_uidlist_lock(struct maildir_uidlist *uidlist)
+{
+ return maildir_uidlist_lock_timeout(uidlist, FALSE, TRUE, FALSE);
+}
+
+int maildir_uidlist_try_lock(struct maildir_uidlist *uidlist)
+{
+ return maildir_uidlist_lock_timeout(uidlist, TRUE, TRUE, FALSE);
+}
+
+int maildir_uidlist_lock_touch(struct maildir_uidlist *uidlist)
+{
+ i_assert(UIDLIST_IS_LOCKED(uidlist));
+
+ return file_dotlock_touch(uidlist->dotlock);
+}
+
+bool maildir_uidlist_is_locked(struct maildir_uidlist *uidlist)
+{
+ return UIDLIST_IS_LOCKED(uidlist);
+}
+
+bool maildir_uidlist_is_read(struct maildir_uidlist *uidlist)
+{
+ return uidlist->initial_read;
+}
+
+bool maildir_uidlist_is_open(struct maildir_uidlist *uidlist)
+{
+ return uidlist->fd != -1;
+}
+
+void maildir_uidlist_unlock(struct maildir_uidlist *uidlist)
+{
+ i_assert(uidlist->lock_count > 0);
+
+ if (--uidlist->lock_count > 0)
+ return;
+
+ uidlist->locked_refresh = FALSE;
+ file_dotlock_delete(&uidlist->dotlock);
+}
+
+static bool dotlock_callback(unsigned int secs_left, bool stale, void *context)
+{
+ struct mailbox *box = context;
+
+ index_storage_lock_notify(box, stale ?
+ MAILBOX_LOCK_NOTIFY_MAILBOX_OVERRIDE :
+ MAILBOX_LOCK_NOTIFY_MAILBOX_ABORT,
+ secs_left);
+ return TRUE;
+}
+
+struct maildir_uidlist *maildir_uidlist_init(struct maildir_mailbox *mbox)
+{
+ struct mailbox *box = &mbox->box;
+ struct maildir_uidlist *uidlist;
+ const char *control_dir;
+
+ if (mailbox_get_path_to(box, MAILBOX_LIST_PATH_TYPE_CONTROL,
+ &control_dir) <= 0)
+ i_unreached();
+
+ uidlist = i_new(struct maildir_uidlist, 1);
+ uidlist->box = box;
+ uidlist->mhdr = &mbox->maildir_hdr;
+ uidlist->fd = -1;
+ uidlist->path = i_strconcat(control_dir, "/"MAILDIR_UIDLIST_NAME, NULL);
+ i_array_init(&uidlist->records, 128);
+ hash_table_create(&uidlist->files, default_pool, 4096,
+ maildir_filename_base_hash,
+ maildir_filename_base_cmp);
+ uidlist->next_uid = 1;
+ uidlist->hdr_extensions = str_new(default_pool, 128);
+
+ uidlist->dotlock_settings.use_io_notify = TRUE;
+ uidlist->dotlock_settings.use_excl_lock =
+ box->storage->set->dotlock_use_excl;
+ uidlist->dotlock_settings.nfs_flush =
+ box->storage->set->mail_nfs_storage;
+ uidlist->dotlock_settings.timeout =
+ mail_storage_get_lock_timeout(box->storage,
+ MAILDIR_UIDLIST_LOCK_STALE_TIMEOUT + 2);
+ uidlist->dotlock_settings.stale_timeout =
+ MAILDIR_UIDLIST_LOCK_STALE_TIMEOUT;
+ uidlist->dotlock_settings.callback = dotlock_callback;
+ uidlist->dotlock_settings.context = box;
+ uidlist->dotlock_settings.temp_prefix = mbox->storage->temp_prefix;
+ return uidlist;
+}
+
+static void maildir_uidlist_close(struct maildir_uidlist *uidlist)
+{
+ if (uidlist->fd != -1) {
+ if (close(uidlist->fd) < 0) {
+ mailbox_set_critical(uidlist->box,
+ "close(%s) failed: %m", uidlist->path);
+ }
+ uidlist->fd = -1;
+ uidlist->fd_ino = 0;
+ }
+ uidlist->last_read_offset = 0;
+ uidlist->read_line_count = 0;
+}
+
+static void maildir_uidlist_reset(struct maildir_uidlist *uidlist)
+{
+ maildir_uidlist_close(uidlist);
+ uidlist->last_seen_uid = 0;
+ uidlist->initial_hdr_read = FALSE;
+ uidlist->read_records_count = 0;
+
+ hash_table_clear(uidlist->files, FALSE);
+ array_clear(&uidlist->records);
+}
+
+void maildir_uidlist_deinit(struct maildir_uidlist **_uidlist)
+{
+ struct maildir_uidlist *uidlist = *_uidlist;
+
+ i_assert(!UIDLIST_IS_LOCKED(uidlist));
+
+ *_uidlist = NULL;
+ (void)maildir_uidlist_update(uidlist);
+ maildir_uidlist_close(uidlist);
+
+ hash_table_destroy(&uidlist->files);
+ pool_unref(&uidlist->record_pool);
+
+ array_free(&uidlist->records);
+ str_free(&uidlist->hdr_extensions);
+ i_free(uidlist->path);
+ i_free(uidlist);
+}
+
+static int maildir_uid_cmp(struct maildir_uidlist_rec *const *rec1,
+ struct maildir_uidlist_rec *const *rec2)
+{
+ return (*rec1)->uid < (*rec2)->uid ? -1 :
+ (*rec1)->uid > (*rec2)->uid ? 1 : 0;
+}
+
+static void ATTR_FORMAT(2, 3)
+maildir_uidlist_set_corrupted(struct maildir_uidlist *uidlist,
+ const char *fmt, ...)
+{
+ va_list args;
+
+ va_start(args, fmt);
+ if (uidlist->retry_rewind) {
+ mailbox_set_critical(uidlist->box,
+ "Broken or unexpectedly changed file %s "
+ "line %u: %s - re-reading from beginning",
+ uidlist->path, uidlist->read_line_count,
+ t_strdup_vprintf(fmt, args));
+ } else {
+ mailbox_set_critical(uidlist->box, "Broken file %s line %u: %s",
+ uidlist->path, uidlist->read_line_count,
+ t_strdup_vprintf(fmt, args));
+ }
+ va_end(args);
+}
+
+static void maildir_uidlist_update_hdr(struct maildir_uidlist *uidlist,
+ const struct stat *st)
+{
+ struct maildir_index_header *mhdr = uidlist->mhdr;
+
+ if (mhdr->uidlist_mtime == 0 && uidlist->version != UIDLIST_VERSION) {
+ /* upgrading from older version. don't update the
+ uidlist times until it uses the new format */
+ uidlist->recreate = TRUE;
+ return;
+ }
+ mhdr->uidlist_mtime = st->st_mtime;
+ mhdr->uidlist_mtime_nsecs = ST_MTIME_NSEC(*st);
+ mhdr->uidlist_size = st->st_size;
+}
+
+static unsigned int
+maildir_uidlist_records_array_delete(struct maildir_uidlist *uidlist,
+ struct maildir_uidlist_rec *rec)
+{
+ struct maildir_uidlist_rec *const *recs, *const *pos;
+ unsigned int idx, count;
+
+ recs = array_get(&uidlist->records, &count);
+ if (!uidlist->unsorted) {
+ pos = array_bsearch(&uidlist->records, &rec, maildir_uid_cmp);
+ i_assert(pos != NULL);
+ idx = pos - recs;
+ } else {
+ for (idx = 0; idx < count; idx++) {
+ if (recs[idx]->uid == rec->uid)
+ break;
+ }
+ i_assert(idx != count);
+ }
+ array_delete(&uidlist->records, idx, 1);
+ return idx;
+}
+
+static bool
+maildir_uidlist_read_extended(struct maildir_uidlist *uidlist,
+ const char **line_p,
+ struct maildir_uidlist_rec *rec)
+{
+ const char *start, *line = *line_p;
+ buffer_t *buf;
+
+ buf = t_buffer_create(128);
+ while (*line != '\0' && *line != ':') {
+ /* skip over an extension field */
+ start = line;
+ while (*line != ' ' && *line != '\0') line++;
+ if (MAILDIR_UIDLIST_REC_EXT_KEY_IS_VALID(*start)) {
+ buffer_append(buf, start, line - start);
+ buffer_append_c(buf, '\0');
+ } else {
+ maildir_uidlist_set_corrupted(uidlist,
+ "Invalid extension record, removing: %s",
+ t_strdup_until(start, line));
+ uidlist->recreate = TRUE;
+ }
+ while (*line == ' ') line++;
+ }
+
+ if (buf->used > 0) {
+ /* save the extensions */
+ buffer_append_c(buf, '\0');
+ rec->extensions = p_malloc(uidlist->record_pool, buf->used);
+ memcpy(rec->extensions, buf->data, buf->used);
+ }
+
+ if (*line == ':')
+ line++;
+ if (*line == '\0')
+ return FALSE;
+
+ *line_p = line;
+ return TRUE;
+}
+
+static bool maildir_uidlist_next(struct maildir_uidlist *uidlist,
+ const char *line)
+{
+ struct maildir_uidlist_rec *rec, *old_rec, *const *recs;
+ unsigned int count;
+ uint32_t uid;
+
+ uid = 0;
+ while (*line >= '0' && *line <= '9') {
+ uid = uid*10 + (*line - '0');
+ line++;
+ }
+
+ if (uid == 0 || *line != ' ') {
+ /* invalid file */
+ maildir_uidlist_set_corrupted(uidlist, "Invalid data: %s",
+ line);
+ return FALSE;
+ }
+ if (uid <= uidlist->prev_read_uid) {
+ maildir_uidlist_set_corrupted(uidlist,
+ "UIDs not ordered (%u >= %u)",
+ uid, uidlist->prev_read_uid);
+ return FALSE;
+ }
+ if (uid >= (uint32_t)-1) {
+ maildir_uidlist_set_corrupted(uidlist,
+ "UID too high (%u)", uid);
+ return FALSE;
+ }
+ uidlist->prev_read_uid = uid;
+
+ if (uid <= uidlist->last_seen_uid) {
+ /* we already have this */
+ return TRUE;
+ }
+ uidlist->last_seen_uid = uid;
+
+ if (uid >= uidlist->next_uid && uidlist->version == 1) {
+ maildir_uidlist_set_corrupted(uidlist,
+ "UID larger than next_uid (%u >= %u)",
+ uid, uidlist->next_uid);
+ return FALSE;
+ }
+
+ rec = p_new(uidlist->record_pool, struct maildir_uidlist_rec, 1);
+ rec->uid = uid;
+ rec->flags = MAILDIR_UIDLIST_REC_FLAG_NONSYNCED;
+
+ while (*line == ' ') line++;
+
+ if (uidlist->version == UIDLIST_VERSION) {
+ /* read extended fields */
+ bool ret;
+
+ T_BEGIN {
+ ret = maildir_uidlist_read_extended(uidlist, &line,
+ rec);
+ } T_END;
+ if (!ret) {
+ maildir_uidlist_set_corrupted(uidlist,
+ "Invalid extended fields: %s", line);
+ return FALSE;
+ }
+ }
+
+ if (strchr(line, '/') != NULL) {
+ maildir_uidlist_set_corrupted(uidlist,
+ "%s: Broken filename at line %u: %s",
+ uidlist->path, uidlist->read_line_count, line);
+ return FALSE;
+ }
+
+ old_rec = hash_table_lookup(uidlist->files, line);
+ if (old_rec == NULL) {
+ /* no conflicts */
+ } else if (old_rec->uid == uid) {
+ /* most likely this is a record we saved ourself, but couldn't
+ update last_seen_uid because uidlist wasn't refreshed while
+ it was locked.
+
+ another possibility is a duplicate file record. currently
+ it would be a bug, but not that big of a deal. also perhaps
+ in future such duplicate lines could be used to update
+ extended fields. so just let it through anyway.
+
+ we'll waste a bit of memory here by allocating the record
+ twice, but that's not really a problem. */
+ rec->filename = old_rec->filename;
+ hash_table_update(uidlist->files, rec->filename, rec);
+ uidlist->unsorted = TRUE;
+ return TRUE;
+ } else {
+ /* This can happen if expunged file is moved back and the file
+ was appended to uidlist. */
+ i_warning("%s: Duplicate file entry at line %u: "
+ "%s (uid %u -> %u)%s",
+ uidlist->path, uidlist->read_line_count, line,
+ old_rec->uid, uid, uidlist->retry_rewind ?
+ " - retrying by re-reading from beginning" : "");
+ if (uidlist->retry_rewind)
+ return FALSE;
+ /* Delete the old UID */
+ (void)maildir_uidlist_records_array_delete(uidlist, old_rec);
+ /* Replace the old record with this new one */
+ *old_rec = *rec;
+ rec = old_rec;
+ uidlist->recreate = TRUE;
+ }
+
+ recs = array_get(&uidlist->records, &count);
+ if (count > 0 && recs[count-1]->uid > uid) {
+ /* we most likely have some records in the array that we saved
+ ourself without refreshing uidlist */
+ uidlist->unsorted = TRUE;
+ }
+
+ rec->filename = p_strdup(uidlist->record_pool, line);
+ hash_table_update(uidlist->files, rec->filename, rec);
+ array_push_back(&uidlist->records, &rec);
+ return TRUE;
+}
+
+static int
+maildir_uidlist_read_v3_header(struct maildir_uidlist *uidlist,
+ const char *line,
+ unsigned int *uid_validity_r,
+ unsigned int *next_uid_r)
+{
+ char key;
+
+ str_truncate(uidlist->hdr_extensions, 0);
+ while (*line != '\0') {
+ const char *value;
+
+ key = *line;
+ value = ++line;
+ while (*line != '\0' && *line != ' ') line++;
+ value = t_strdup_until(value, line);
+
+ switch (key) {
+ case MAILDIR_UIDLIST_HDR_EXT_UID_VALIDITY:
+ if (str_to_uint(value, uid_validity_r) < 0) {
+ maildir_uidlist_set_corrupted(uidlist,
+ "Invalid mailbox UID_VALIDITY: %s", value);
+ return -1;
+ }
+ break;
+ case MAILDIR_UIDLIST_HDR_EXT_NEXT_UID:
+ if (str_to_uint(value, next_uid_r) < 0) {
+ maildir_uidlist_set_corrupted(uidlist,
+ "Invalid mailbox NEXT_UID: %s", value);
+ return -1;
+ }
+ break;
+ case MAILDIR_UIDLIST_HDR_EXT_GUID:
+ if (guid_128_from_string(value,
+ uidlist->mailbox_guid) < 0) {
+ maildir_uidlist_set_corrupted(uidlist,
+ "Invalid mailbox GUID: %s", value);
+ return -1;
+ }
+ uidlist->have_mailbox_guid = TRUE;
+ break;
+ default:
+ if (str_len(uidlist->hdr_extensions) > 0)
+ str_append_c(uidlist->hdr_extensions, ' ');
+ str_printfa(uidlist->hdr_extensions,
+ "%c%s", key, value);
+ break;
+ }
+
+ while (*line == ' ') line++;
+ }
+ return 0;
+}
+
+static int maildir_uidlist_read_header(struct maildir_uidlist *uidlist,
+ struct istream *input)
+{
+ unsigned int uid_validity = 0, next_uid = 0;
+ const char *line;
+ int ret;
+
+ line = i_stream_read_next_line(input);
+ if (line == NULL) {
+ /* I/O error / empty file */
+ return input->stream_errno == 0 ? 0 : -1;
+ }
+ uidlist->read_line_count = 1;
+
+ if (*line < '0' || *line > '9' || line[1] != ' ') {
+ maildir_uidlist_set_corrupted(uidlist,
+ "Corrupted header (invalid version number)");
+ return 0;
+ }
+
+ uidlist->version = *line - '0';
+ line += 2;
+
+ switch (uidlist->version) {
+ case 1:
+ if (sscanf(line, "%u %u", &uid_validity, &next_uid) != 2) {
+ maildir_uidlist_set_corrupted(uidlist,
+ "Corrupted header (version 1)");
+ return 0;
+ }
+ break;
+ case UIDLIST_VERSION:
+ T_BEGIN {
+ ret = maildir_uidlist_read_v3_header(uidlist, line,
+ &uid_validity,
+ &next_uid);
+ } T_END;
+ if (ret < 0)
+ return 0;
+ break;
+ default:
+ maildir_uidlist_set_corrupted(uidlist, "Unsupported version %u",
+ uidlist->version);
+ return 0;
+ }
+
+ if (uid_validity == 0 || next_uid == 0) {
+ maildir_uidlist_set_corrupted(uidlist,
+ "Broken header (uidvalidity = %u, next_uid=%u)",
+ uid_validity, next_uid);
+ return 0;
+ }
+
+ if (uid_validity == uidlist->uid_validity &&
+ next_uid < uidlist->hdr_next_uid) {
+ maildir_uidlist_set_corrupted(uidlist,
+ "next_uid header was lowered (%u -> %u)",
+ uidlist->hdr_next_uid, next_uid);
+ return 0;
+ }
+
+ uidlist->uid_validity = uid_validity;
+ uidlist->next_uid = next_uid;
+ uidlist->hdr_next_uid = next_uid;
+ return 1;
+}
+
+static void maildir_uidlist_records_sort_by_uid(struct maildir_uidlist *uidlist)
+{
+ array_sort(&uidlist->records, maildir_uid_cmp);
+ uidlist->unsorted = FALSE;
+}
+
+static int
+maildir_uidlist_update_read(struct maildir_uidlist *uidlist,
+ bool *retry_r, bool try_retry)
+{
+ const char *line;
+ uint32_t orig_next_uid, orig_uid_validity;
+ struct istream *input;
+ struct stat st;
+ uoff_t last_read_offset;
+ int fd, ret;
+ bool readonly = FALSE;
+
+ *retry_r = FALSE;
+
+ if (uidlist->fd == -1) {
+ fd = nfs_safe_open(uidlist->path, O_RDWR);
+ if (fd == -1 && errno == EACCES) {
+ fd = nfs_safe_open(uidlist->path, O_RDONLY);
+ readonly = TRUE;
+ }
+ if (fd == -1) {
+ if (errno != ENOENT) {
+ mailbox_set_critical(uidlist->box,
+ "open(%s) failed: %m", uidlist->path);
+ return -1;
+ }
+ return 0;
+ }
+ last_read_offset = 0;
+ } else {
+ /* the file was updated */
+ fd = uidlist->fd;
+ if (lseek(fd, 0, SEEK_SET) < 0) {
+ if (errno == ESTALE && try_retry) {
+ *retry_r = TRUE;
+ return -1;
+ }
+ mailbox_set_critical(uidlist->box,
+ "lseek(%s) failed: %m", uidlist->path);
+ return -1;
+ }
+ uidlist->fd = -1;
+ uidlist->fd_ino = 0;
+ last_read_offset = uidlist->last_read_offset;
+ uidlist->last_read_offset = 0;
+ }
+
+ if (fstat(fd, &st) < 0) {
+ i_close_fd(&fd);
+ if (errno == ESTALE && try_retry) {
+ *retry_r = TRUE;
+ return -1;
+ }
+ mailbox_set_critical(uidlist->box,
+ "fstat(%s) failed: %m", uidlist->path);
+ return -1;
+ }
+
+ if (uidlist->record_pool == NULL) {
+ uidlist->record_pool =
+ pool_alloconly_create(MEMPOOL_GROWING
+ "uidlist record_pool",
+ nearest_power(st.st_size -
+ st.st_size/8));
+ }
+
+ input = i_stream_create_fd(fd, SIZE_MAX);
+ i_stream_seek(input, last_read_offset);
+
+ orig_uid_validity = uidlist->uid_validity;
+ orig_next_uid = uidlist->next_uid;
+ ret = input->v_offset != 0 ? 1 :
+ maildir_uidlist_read_header(uidlist, input);
+ if (ret > 0) {
+ uidlist->prev_read_uid = 0;
+ uidlist->change_counter++;
+ uidlist->retry_rewind = last_read_offset != 0 && try_retry;
+
+ ret = 1;
+ while ((line = i_stream_read_next_line(input)) != NULL) {
+ uidlist->read_records_count++;
+ uidlist->read_line_count++;
+ if (!maildir_uidlist_next(uidlist, line)) {
+ if (!uidlist->retry_rewind)
+ ret = 0;
+ else {
+ ret = -1;
+ *retry_r = TRUE;
+ }
+ break;
+ }
+ }
+ uidlist->retry_rewind = FALSE;
+ if (input->stream_errno != 0)
+ ret = -1;
+
+ if (uidlist->unsorted) {
+ uidlist->recreate_on_change = TRUE;
+ maildir_uidlist_records_sort_by_uid(uidlist);
+ }
+ if (uidlist->next_uid <= uidlist->prev_read_uid)
+ uidlist->next_uid = uidlist->prev_read_uid + 1;
+ if (ret > 0 && uidlist->uid_validity != orig_uid_validity &&
+ orig_uid_validity != 0) {
+ uidlist->recreate = TRUE;
+ } else if (ret > 0 && uidlist->next_uid < orig_next_uid) {
+ mailbox_set_critical(uidlist->box,
+ "%s: next_uid was lowered (%u -> %u, hdr=%u)",
+ uidlist->path, orig_next_uid,
+ uidlist->next_uid, uidlist->hdr_next_uid);
+ uidlist->recreate = TRUE;
+ uidlist->next_uid = orig_next_uid;
+ }
+ }
+
+ if (ret == 0) {
+ /* file is broken */
+ i_unlink(uidlist->path);
+ } else if (ret > 0) {
+ /* success */
+ if (readonly)
+ uidlist->recreate_on_change = TRUE;
+ uidlist->fd = fd;
+ uidlist->fd_dev = st.st_dev;
+ uidlist->fd_ino = st.st_ino;
+ uidlist->fd_size = st.st_size;
+ uidlist->last_read_offset = input->v_offset;
+ maildir_uidlist_update_hdr(uidlist, &st);
+ } else if (!*retry_r) {
+ /* I/O error */
+ if (input->stream_errno == ESTALE && try_retry)
+ *retry_r = TRUE;
+ else {
+ mailbox_set_critical(uidlist->box,
+ "read(%s) failed: %s", uidlist->path,
+ i_stream_get_error(input));
+ }
+ uidlist->last_read_offset = 0;
+ }
+
+ i_stream_destroy(&input);
+ if (ret <= 0) {
+ if (close(fd) < 0) {
+ mailbox_set_critical(uidlist->box,
+ "close(%s) failed: %m", uidlist->path);
+ }
+ }
+ return ret;
+}
+
+static int
+maildir_uidlist_stat(struct maildir_uidlist *uidlist, struct stat *st_r)
+{
+ struct mail_storage *storage = uidlist->box->storage;
+
+ if (storage->set->mail_nfs_storage) {
+ nfs_flush_file_handle_cache(uidlist->path);
+ nfs_flush_attr_cache_unlocked(uidlist->path);
+ }
+ if (nfs_safe_stat(uidlist->path, st_r) < 0) {
+ if (errno != ENOENT) {
+ mailbox_set_critical(uidlist->box,
+ "stat(%s) failed: %m", uidlist->path);
+ return -1;
+ }
+ return 0;
+ }
+ return 1;
+}
+
+static int
+maildir_uidlist_has_changed(struct maildir_uidlist *uidlist, bool *recreated_r)
+{
+ struct mail_storage *storage = uidlist->box->storage;
+ struct stat st;
+ int ret;
+
+ *recreated_r = FALSE;
+
+ if ((ret = maildir_uidlist_stat(uidlist, &st)) < 0)
+ return -1;
+ if (ret == 0) {
+ *recreated_r = TRUE;
+ return 1;
+ }
+
+ if (st.st_ino != uidlist->fd_ino ||
+ !CMP_DEV_T(st.st_dev, uidlist->fd_dev)) {
+ /* file recreated */
+ *recreated_r = TRUE;
+ return 1;
+ }
+
+ if (storage->set->mail_nfs_storage) {
+ /* NFS: either the file hasn't been changed, or it has already
+ been deleted and the inodes just happen to be the same.
+ check if the fd is still valid. */
+ if (fstat(uidlist->fd, &st) < 0) {
+ if (errno == ESTALE) {
+ *recreated_r = TRUE;
+ return 1;
+ }
+ mailbox_set_critical(uidlist->box,
+ "fstat(%s) failed: %m", uidlist->path);
+ return -1;
+ }
+ }
+
+ if (st.st_size != uidlist->fd_size) {
+ /* file modified but not recreated */
+ return 1;
+ } else {
+ /* unchanged */
+ return 0;
+ }
+}
+
+static int maildir_uidlist_open_latest(struct maildir_uidlist *uidlist)
+{
+ bool recreated;
+ int ret;
+
+ if (uidlist->fd != -1) {
+ ret = maildir_uidlist_has_changed(uidlist, &recreated);
+ if (ret <= 0) {
+ if (UIDLIST_IS_LOCKED(uidlist))
+ uidlist->locked_refresh = TRUE;
+ return ret < 0 ? -1 : 1;
+ }
+
+ if (!recreated)
+ return 0;
+ maildir_uidlist_reset(uidlist);
+ }
+
+ uidlist->fd = nfs_safe_open(uidlist->path, O_RDWR);
+ if (uidlist->fd == -1 && errno == EACCES) {
+ uidlist->fd = nfs_safe_open(uidlist->path, O_RDONLY);
+ uidlist->recreate_on_change = TRUE;
+ }
+ if (uidlist->fd == -1 && errno != ENOENT) {
+ mailbox_set_critical(uidlist->box,
+ "open(%s) failed: %m", uidlist->path);
+ return -1;
+ }
+ return 0;
+}
+
+int maildir_uidlist_refresh(struct maildir_uidlist *uidlist)
+{
+ unsigned int i;
+ bool retry;
+ int ret;
+
+ if (maildir_uidlist_open_latest(uidlist) < 0)
+ return -1;
+
+ for (i = 0; ; i++) {
+ ret = maildir_uidlist_update_read(uidlist, &retry,
+ i < UIDLIST_ESTALE_RETRY_COUNT);
+ if (!retry)
+ break;
+ /* ESTALE - try reopening and rereading */
+ maildir_uidlist_close(uidlist);
+ }
+ if (ret >= 0) {
+ uidlist->initial_read = TRUE;
+ uidlist->initial_hdr_read = TRUE;
+ if (UIDLIST_IS_LOCKED(uidlist))
+ uidlist->locked_refresh = TRUE;
+ if (!uidlist->have_mailbox_guid) {
+ uidlist->recreate = TRUE;
+ (void)maildir_uidlist_update(uidlist);
+ }
+ }
+ return ret;
+}
+
+int maildir_uidlist_refresh_fast_init(struct maildir_uidlist *uidlist)
+{
+ const struct maildir_index_header *mhdr = uidlist->mhdr;
+ struct mail_index *index = uidlist->box->index;
+ struct mail_index_view *view;
+ const struct mail_index_header *hdr;
+ struct stat st;
+ int ret;
+
+ i_assert(UIDLIST_IS_LOCKED(uidlist));
+
+ if (uidlist->fd != -1)
+ return maildir_uidlist_refresh(uidlist);
+
+ if ((ret = maildir_uidlist_stat(uidlist, &st)) < 0)
+ return ret;
+
+ if (ret > 0 && st.st_size == mhdr->uidlist_size &&
+ st.st_mtime == (time_t)mhdr->uidlist_mtime &&
+ ST_NTIMES_EQUAL(ST_MTIME_NSEC(st), mhdr->uidlist_mtime_nsecs) &&
+ (!mail_index_is_in_memory(index) || st.st_mtime < ioloop_time-1)) {
+ /* index is up-to-date. look up the uidvalidity and next-uid
+ from it. we'll need to create a new view temporarily to
+ make sure we get the latest values. */
+ view = mail_index_view_open(index);
+ hdr = mail_index_get_header(view);
+ uidlist->uid_validity = hdr->uid_validity;
+ uidlist->next_uid = hdr->next_uid;
+ uidlist->initial_hdr_read = TRUE;
+ mail_index_view_close(&view);
+
+ if (UIDLIST_IS_LOCKED(uidlist))
+ uidlist->locked_refresh = TRUE;
+ return 1;
+ } else {
+ return maildir_uidlist_refresh(uidlist);
+ }
+}
+
+static int
+maildir_uid_bsearch_cmp(const uint32_t *uidp,
+ struct maildir_uidlist_rec *const *recp)
+{
+ return *uidp < (*recp)->uid ? -1 :
+ *uidp > (*recp)->uid ? 1 : 0;
+}
+
+static int
+maildir_uidlist_lookup_rec(struct maildir_uidlist *uidlist, uint32_t uid,
+ struct maildir_uidlist_rec **rec_r)
+{
+ struct maildir_uidlist_rec *const *pos;
+
+ if (!uidlist->initial_read) {
+ /* first time we need to read uidlist */
+ if (maildir_uidlist_refresh(uidlist) < 0)
+ return -1;
+ }
+
+ pos = array_bsearch(&uidlist->records, &uid,
+ maildir_uid_bsearch_cmp);
+ if (pos == NULL) {
+ *rec_r = NULL;
+ return 0;
+ }
+ *rec_r = *pos;
+ return 1;
+}
+
+int maildir_uidlist_lookup(struct maildir_uidlist *uidlist, uint32_t uid,
+ enum maildir_uidlist_rec_flag *flags_r,
+ const char **fname_r)
+{
+ struct maildir_uidlist_rec *rec;
+ int ret;
+
+ if ((ret = maildir_uidlist_lookup_rec(uidlist, uid, &rec)) <= 0)
+ return ret;
+
+ *flags_r = rec->flags;
+ *fname_r = rec->filename;
+ return 1;
+}
+
+const char *
+maildir_uidlist_lookup_ext(struct maildir_uidlist *uidlist, uint32_t uid,
+ enum maildir_uidlist_rec_ext_key key)
+{
+ struct maildir_uidlist_rec *rec;
+ const unsigned char *p;
+ int ret;
+
+ ret = maildir_uidlist_lookup_rec(uidlist, uid, &rec);
+ if (ret <= 0 || rec->extensions == NULL)
+ return NULL;
+
+ p = rec->extensions;
+ while (*p != '\0') {
+ /* <key><value>\0 */
+ if (*p == (unsigned char)key)
+ return (const char *)p + 1;
+
+ p += strlen((const char *)p) + 1;
+ }
+ return NULL;
+}
+
+uint32_t maildir_uidlist_get_uid_validity(struct maildir_uidlist *uidlist)
+{
+ return uidlist->uid_validity;
+}
+
+uint32_t maildir_uidlist_get_next_uid(struct maildir_uidlist *uidlist)
+{
+ return !uidlist->initial_hdr_read ? 0 : uidlist->next_uid;
+}
+
+int maildir_uidlist_get_mailbox_guid(struct maildir_uidlist *uidlist,
+ guid_128_t mailbox_guid)
+{
+ if (!uidlist->initial_hdr_read) {
+ if (maildir_uidlist_refresh(uidlist) < 0)
+ return -1;
+ }
+ if (!uidlist->have_mailbox_guid) {
+ uidlist->recreate = TRUE;
+ if (maildir_uidlist_update(uidlist) < 0)
+ return -1;
+ }
+ memcpy(mailbox_guid, uidlist->mailbox_guid, GUID_128_SIZE);
+ return 0;
+}
+
+void maildir_uidlist_set_mailbox_guid(struct maildir_uidlist *uidlist,
+ const guid_128_t mailbox_guid)
+{
+ if (memcmp(uidlist->mailbox_guid, mailbox_guid,
+ sizeof(uidlist->mailbox_guid)) != 0) {
+ memcpy(uidlist->mailbox_guid, mailbox_guid,
+ sizeof(uidlist->mailbox_guid));
+ uidlist->recreate = TRUE;
+ }
+}
+
+void maildir_uidlist_set_uid_validity(struct maildir_uidlist *uidlist,
+ uint32_t uid_validity)
+{
+ i_assert(uid_validity != 0);
+
+ if (uid_validity != uidlist->uid_validity) {
+ uidlist->uid_validity = uid_validity;
+ uidlist->recreate = TRUE;
+ }
+}
+
+void maildir_uidlist_set_next_uid(struct maildir_uidlist *uidlist,
+ uint32_t next_uid, bool force)
+{
+ if (uidlist->next_uid < next_uid || force) {
+ i_assert(next_uid != 0);
+ uidlist->next_uid = next_uid;
+ uidlist->recreate = TRUE;
+ }
+}
+
+static void
+maildir_uidlist_rec_set_ext(struct maildir_uidlist_rec *rec, pool_t pool,
+ enum maildir_uidlist_rec_ext_key key,
+ const char *value)
+{
+ const unsigned char *p;
+ buffer_t *buf;
+ size_t len;
+
+ /* copy existing extensions, except for the one we're updating */
+ buf = t_buffer_create(128);
+ if (rec->extensions != NULL) {
+ p = rec->extensions;
+ while (*p != '\0') {
+ /* <key><value>\0 */
+ i_assert(MAILDIR_UIDLIST_REC_EXT_KEY_IS_VALID(*p));
+
+ len = strlen((const char *)p) + 1;
+ if (*p != (unsigned char)key)
+ buffer_append(buf, p, len);
+ p += len;
+ }
+ }
+ if (value != NULL) {
+ buffer_append_c(buf, key);
+ buffer_append(buf, value, strlen(value) + 1);
+ }
+ buffer_append_c(buf, '\0');
+
+ rec->extensions = p_malloc(pool, buf->used);
+ memcpy(rec->extensions, buf->data, buf->used);
+}
+
+static void ATTR_NULL(4)
+maildir_uidlist_set_ext_internal(struct maildir_uidlist *uidlist, uint32_t uid,
+ enum maildir_uidlist_rec_ext_key key,
+ const char *value)
+{
+ struct maildir_uidlist_rec *rec;
+ int ret;
+
+ i_assert(MAILDIR_UIDLIST_REC_EXT_KEY_IS_VALID(key));
+
+ ret = maildir_uidlist_lookup_rec(uidlist, uid, &rec);
+ if (ret <= 0) {
+ if (ret < 0)
+ return;
+
+ /* maybe it's a new message */
+ if (maildir_uidlist_refresh(uidlist) < 0)
+ return;
+ if (maildir_uidlist_lookup_rec(uidlist, uid, &rec) <= 0) {
+ /* message is already expunged, ignore */
+ return;
+ }
+ }
+
+ T_BEGIN {
+ maildir_uidlist_rec_set_ext(rec, uidlist->record_pool,
+ key, value);
+ } T_END;
+
+ if (rec->uid != (uint32_t)-1) {
+ /* message already exists in uidlist, need to recreate it */
+ uidlist->recreate = TRUE;
+ }
+}
+
+void maildir_uidlist_set_ext(struct maildir_uidlist *uidlist, uint32_t uid,
+ enum maildir_uidlist_rec_ext_key key,
+ const char *value)
+{
+ maildir_uidlist_set_ext_internal(uidlist, uid, key, value);
+}
+
+void maildir_uidlist_unset_ext(struct maildir_uidlist *uidlist, uint32_t uid,
+ enum maildir_uidlist_rec_ext_key key)
+{
+ maildir_uidlist_set_ext_internal(uidlist, uid, key, NULL);
+}
+
+static void
+maildir_uidlist_generate_uid_validity(struct maildir_uidlist *uidlist)
+{
+ const struct mail_index_header *hdr;
+
+ if (uidlist->box->opened) {
+ hdr = mail_index_get_header(uidlist->box->view);
+ if (hdr->uid_validity != 0) {
+ uidlist->uid_validity = hdr->uid_validity;
+ return;
+ }
+ }
+ uidlist->uid_validity =
+ maildir_get_uidvalidity_next(uidlist->box->list);
+}
+
+static int maildir_uidlist_write_fd(struct maildir_uidlist *uidlist, int fd,
+ const char *path, unsigned int first_idx,
+ uoff_t *file_size_r)
+{
+ struct mail_storage *storage = uidlist->box->storage;
+ struct maildir_uidlist_iter_ctx *iter;
+ struct ostream *output;
+ struct maildir_uidlist_rec *rec;
+ string_t *str;
+ const unsigned char *p;
+ const char *strp;
+ size_t len;
+
+ i_assert(fd != -1);
+
+ output = o_stream_create_fd_file(fd, UOFF_T_MAX, FALSE);
+ o_stream_cork(output);
+ str = t_str_new(512);
+
+ if (output->offset == 0) {
+ i_assert(first_idx == 0);
+ uidlist->version = UIDLIST_VERSION;
+
+ if (uidlist->uid_validity == 0)
+ maildir_uidlist_generate_uid_validity(uidlist);
+ if (!uidlist->have_mailbox_guid)
+ guid_128_generate(uidlist->mailbox_guid);
+
+ i_assert(uidlist->next_uid > 0);
+ str_printfa(str, "%u %c%u %c%u %c%s", uidlist->version,
+ MAILDIR_UIDLIST_HDR_EXT_UID_VALIDITY,
+ uidlist->uid_validity,
+ MAILDIR_UIDLIST_HDR_EXT_NEXT_UID,
+ uidlist->next_uid,
+ MAILDIR_UIDLIST_HDR_EXT_GUID,
+ guid_128_to_string(uidlist->mailbox_guid));
+ if (str_len(uidlist->hdr_extensions) > 0) {
+ str_append_c(str, ' ');
+ str_append_str(str, uidlist->hdr_extensions);
+ }
+ str_append_c(str, '\n');
+ o_stream_nsend(output, str_data(str), str_len(str));
+ }
+
+ iter = maildir_uidlist_iter_init(uidlist);
+ i_assert(first_idx <= array_count(&uidlist->records));
+ iter->next += first_idx;
+
+ while (maildir_uidlist_iter_next_rec(iter, &rec)) {
+ uidlist->read_records_count++;
+ str_truncate(str, 0);
+ str_printfa(str, "%u", rec->uid);
+ if (rec->extensions != NULL) {
+ for (p = rec->extensions; *p != '\0'; ) {
+ i_assert(MAILDIR_UIDLIST_REC_EXT_KEY_IS_VALID(*p));
+ len = strlen((const char *)p);
+ str_append_c(str, ' ');
+ str_append_data(str, p, len);
+ p += len + 1;
+ }
+ }
+ str_append(str, " :");
+ strp = strchr(rec->filename, *MAILDIR_INFO_SEP_S);
+ if (strp == NULL)
+ str_append(str, rec->filename);
+ else
+ str_append_data(str, rec->filename, strp - rec->filename);
+ str_append_c(str, '\n');
+ o_stream_nsend(output, str_data(str), str_len(str));
+ }
+ maildir_uidlist_iter_deinit(&iter);
+
+ if (o_stream_finish(output) < 0) {
+ mailbox_set_critical(uidlist->box, "write(%s) failed: %s",
+ path, o_stream_get_error(output));
+ o_stream_unref(&output);
+ return -1;
+ }
+
+ *file_size_r = output->offset;
+ o_stream_unref(&output);
+
+ if (storage->set->parsed_fsync_mode != FSYNC_MODE_NEVER) {
+ if (fdatasync(fd) < 0) {
+ mailbox_set_critical(uidlist->box,
+ "fdatasync(%s) failed: %m", path);
+ return -1;
+ }
+ }
+ return 0;
+}
+
+static void
+maildir_uidlist_records_drop_expunges(struct maildir_uidlist *uidlist)
+{
+ struct mail_index_view *view;
+ struct maildir_uidlist_rec *const *recs;
+ ARRAY_TYPE(maildir_uidlist_rec_p) new_records;
+ const struct mail_index_header *hdr;
+ const struct mail_index_record *rec;
+ unsigned int i, count;
+ uint32_t seq;
+
+ /* we could get here when opening and locking mailbox,
+ before index files have been opened. */
+ if (!uidlist->box->opened)
+ return;
+
+ mail_index_refresh(uidlist->box->index);
+ view = mail_index_view_open(uidlist->box->index);
+ count = array_count(&uidlist->records);
+ hdr = mail_index_get_header(view);
+ if (count * UIDLIST_COMPRESS_PERCENTAGE / 100 <= hdr->messages_count) {
+ /* too much trouble to be worth it */
+ mail_index_view_close(&view);
+ return;
+ }
+
+ i_array_init(&new_records, hdr->messages_count + 64);
+ recs = array_get(&uidlist->records, &count);
+ for (i = 0, seq = 1; i < count && seq <= hdr->messages_count; ) {
+ rec = mail_index_lookup(view, seq);
+ if (recs[i]->uid < rec->uid) {
+ /* expunged entry */
+ hash_table_remove(uidlist->files, recs[i]->filename);
+ i++;
+ } else if (recs[i]->uid > rec->uid) {
+ /* index isn't up to date. we're probably just
+ syncing it here. ignore this entry. */
+ seq++;
+ } else {
+ array_push_back(&new_records, &recs[i]);
+ seq++; i++;
+ }
+ }
+
+ /* drop messages expunged at the end of index */
+ while (i < count && recs[i]->uid < hdr->next_uid) {
+ hash_table_remove(uidlist->files, recs[i]->filename);
+ i++;
+ }
+ /* view might not be completely up-to-date, so preserve any
+ messages left */
+ for (; i < count; i++)
+ array_push_back(&new_records, &recs[i]);
+
+ array_free(&uidlist->records);
+ uidlist->records = new_records;
+
+ mail_index_view_close(&view);
+}
+
+static int maildir_uidlist_recreate(struct maildir_uidlist *uidlist)
+{
+ struct mailbox *box = uidlist->box;
+ const struct mailbox_permissions *perm = mailbox_get_permissions(box);
+ const char *control_dir, *temp_path;
+ struct stat st;
+ mode_t old_mask;
+ uoff_t file_size;
+ int i, fd, ret;
+
+ i_assert(uidlist->initial_read);
+
+ maildir_uidlist_records_drop_expunges(uidlist);
+
+ if (mailbox_get_path_to(box, MAILBOX_LIST_PATH_TYPE_CONTROL,
+ &control_dir) <= 0)
+ i_unreached();
+ temp_path = t_strconcat(control_dir,
+ "/" MAILDIR_UIDLIST_NAME ".tmp", NULL);
+
+ for (i = 0;; i++) {
+ old_mask = umask(0777 & ~perm->file_create_mode);
+ fd = open(temp_path, O_RDWR | O_CREAT | O_TRUNC, 0777);
+ umask(old_mask);
+ if (fd != -1)
+ break;
+
+ if (errno != ENOENT || i == MAILDIR_DELETE_RETRY_COUNT) {
+ mailbox_set_critical(box,
+ "open(%s, O_CREAT) failed: %m", temp_path);
+ return -1;
+ }
+ /* the control dir doesn't exist. create it unless the whole
+ mailbox was just deleted. */
+ if (!maildir_set_deleted(uidlist->box))
+ return -1;
+ }
+
+ if (perm->file_create_gid != (gid_t)-1 &&
+ fchown(fd, (uid_t)-1, perm->file_create_gid) < 0) {
+ if (errno == EPERM) {
+ mailbox_set_critical(box, "%s",
+ eperm_error_get_chgrp("fchown", temp_path,
+ perm->file_create_gid,
+ perm->file_create_gid_origin));
+ } else {
+ mailbox_set_critical(box,
+ "fchown(%s) failed: %m", temp_path);
+ }
+ }
+
+ uidlist->read_records_count = 0;
+ ret = maildir_uidlist_write_fd(uidlist, fd, temp_path, 0, &file_size);
+ if (ret == 0) {
+ if (rename(temp_path, uidlist->path) < 0) {
+ mailbox_set_critical(box,
+ "rename(%s, %s) failed: %m",
+ temp_path, uidlist->path);
+ ret = -1;
+ }
+ }
+
+ if (ret < 0)
+ i_unlink(temp_path);
+ else if (fstat(fd, &st) < 0) {
+ mailbox_set_critical(box,
+ "fstat(%s) failed: %m", temp_path);
+ ret = -1;
+ } else if (file_size != (uoff_t)st.st_size) {
+ i_assert(!file_dotlock_is_locked(uidlist->dotlock));
+ mailbox_set_critical(box,
+ "Maildir uidlist dotlock overridden: %s",
+ uidlist->path);
+ ret = -1;
+ } else {
+ maildir_uidlist_close(uidlist);
+ uidlist->fd = fd;
+ uidlist->fd_dev = st.st_dev;
+ uidlist->fd_ino = st.st_ino;
+ uidlist->fd_size = st.st_size;
+ uidlist->last_read_offset = st.st_size;
+ uidlist->recreate = FALSE;
+ uidlist->recreate_on_change = FALSE;
+ uidlist->have_mailbox_guid = TRUE;
+ maildir_uidlist_update_hdr(uidlist, &st);
+ }
+ if (ret < 0)
+ i_close_fd(&fd);
+ return ret;
+}
+
+int maildir_uidlist_update(struct maildir_uidlist *uidlist)
+{
+ int ret;
+
+ if (!uidlist->recreate)
+ return 0;
+
+ if (maildir_uidlist_lock(uidlist) <= 0)
+ return -1;
+ ret = maildir_uidlist_recreate(uidlist);
+ maildir_uidlist_unlock(uidlist);
+ return ret;
+}
+
+static bool maildir_uidlist_want_compress(struct maildir_uidlist_sync_ctx *ctx)
+{
+ unsigned int min_rewrite_count;
+
+ if (!ctx->uidlist->locked_refresh)
+ return FALSE;
+ if (ctx->uidlist->recreate)
+ return TRUE;
+
+ min_rewrite_count =
+ (ctx->uidlist->read_records_count + ctx->new_files_count) *
+ UIDLIST_COMPRESS_PERCENTAGE / 100;
+ return min_rewrite_count >= array_count(&ctx->uidlist->records);
+}
+
+static bool maildir_uidlist_want_recreate(struct maildir_uidlist_sync_ctx *ctx)
+{
+ struct maildir_uidlist *uidlist = ctx->uidlist;
+
+ if (!uidlist->locked_refresh || !uidlist->initial_read)
+ return FALSE;
+
+ if (ctx->finish_change_counter != uidlist->change_counter)
+ return TRUE;
+ if (uidlist->fd == -1 || uidlist->version != UIDLIST_VERSION ||
+ !uidlist->have_mailbox_guid)
+ return TRUE;
+ return maildir_uidlist_want_compress(ctx);
+}
+
+static int maildir_uidlist_sync_update(struct maildir_uidlist_sync_ctx *ctx)
+{
+ struct maildir_uidlist *uidlist = ctx->uidlist;
+ struct stat st;
+ uoff_t file_size;
+
+ if (maildir_uidlist_want_recreate(ctx) || uidlist->recreate_on_change)
+ return maildir_uidlist_recreate(uidlist);
+
+ if (!uidlist->locked_refresh || uidlist->fd == -1) {
+ /* make sure we have the latest file (e.g. NOREFRESH used) */
+ i_assert(uidlist->initial_hdr_read);
+ if (maildir_uidlist_open_latest(uidlist) < 0)
+ return -1;
+ if (uidlist->recreate_on_change)
+ return maildir_uidlist_recreate(uidlist);
+ }
+ i_assert(ctx->first_unwritten_pos != UINT_MAX);
+
+ if (lseek(uidlist->fd, 0, SEEK_END) < 0) {
+ mailbox_set_critical(uidlist->box,
+ "lseek(%s) failed: %m", uidlist->path);
+ return -1;
+ }
+
+ if (maildir_uidlist_write_fd(uidlist, uidlist->fd, uidlist->path,
+ ctx->first_unwritten_pos, &file_size) < 0)
+ return -1;
+
+ if (fstat(uidlist->fd, &st) < 0) {
+ mailbox_set_critical(uidlist->box,
+ "fstat(%s) failed: %m", uidlist->path);
+ return -1;
+ }
+ if ((uoff_t)st.st_size != file_size) {
+ i_warning("%s: file size changed unexpectedly after write",
+ uidlist->path);
+ } else if (uidlist->locked_refresh) {
+ uidlist->fd_size = st.st_size;
+ uidlist->last_read_offset = st.st_size;
+ maildir_uidlist_update_hdr(uidlist, &st);
+ }
+ return 0;
+}
+
+static void maildir_uidlist_mark_all(struct maildir_uidlist *uidlist,
+ bool nonsynced)
+{
+ struct maildir_uidlist_rec **recs;
+ unsigned int i, count;
+
+ recs = array_get_modifiable(&uidlist->records, &count);
+ if (nonsynced) {
+ for (i = 0; i < count; i++)
+ recs[i]->flags |= MAILDIR_UIDLIST_REC_FLAG_NONSYNCED;
+ } else {
+ for (i = 0; i < count; i++)
+ recs[i]->flags &= ENUM_NEGATE(MAILDIR_UIDLIST_REC_FLAG_NONSYNCED);
+ }
+}
+
+static int maildir_uidlist_sync_lock(struct maildir_uidlist *uidlist,
+ enum maildir_uidlist_sync_flags sync_flags,
+ bool *locked_r)
+{
+ bool nonblock, refresh;
+ int ret;
+
+ *locked_r = FALSE;
+
+ if ((sync_flags & MAILDIR_UIDLIST_SYNC_NOLOCK) != 0) {
+ if (maildir_uidlist_refresh(uidlist) < 0)
+ return -1;
+ return 1;
+ }
+
+ nonblock = (sync_flags & MAILDIR_UIDLIST_SYNC_TRYLOCK) != 0;
+ refresh = (sync_flags & MAILDIR_UIDLIST_SYNC_NOREFRESH) == 0;
+
+ ret = maildir_uidlist_lock_timeout(uidlist, nonblock, refresh, refresh);
+ if (ret <= 0) {
+ if (ret < 0 || !nonblock)
+ return ret;
+
+ /* couldn't lock it */
+ if ((sync_flags & MAILDIR_UIDLIST_SYNC_FORCE) == 0)
+ return 0;
+ /* forcing the sync anyway */
+ if (maildir_uidlist_refresh(uidlist) < 0)
+ return -1;
+ } else {
+ *locked_r = TRUE;
+ }
+ return 1;
+}
+
+void maildir_uidlist_set_all_nonsynced(struct maildir_uidlist *uidlist)
+{
+ maildir_uidlist_mark_all(uidlist, TRUE);
+}
+
+int maildir_uidlist_sync_init(struct maildir_uidlist *uidlist,
+ enum maildir_uidlist_sync_flags sync_flags,
+ struct maildir_uidlist_sync_ctx **sync_ctx_r)
+{
+ struct maildir_uidlist_sync_ctx *ctx;
+ bool locked;
+ int ret;
+
+ ret = maildir_uidlist_sync_lock(uidlist, sync_flags, &locked);
+ if (ret <= 0)
+ return ret;
+
+ *sync_ctx_r = ctx = i_new(struct maildir_uidlist_sync_ctx, 1);
+ ctx->uidlist = uidlist;
+ ctx->sync_flags = sync_flags;
+ ctx->partial = !locked ||
+ (sync_flags & MAILDIR_UIDLIST_SYNC_PARTIAL) != 0;
+ ctx->locked = locked;
+ ctx->first_unwritten_pos = UINT_MAX;
+ ctx->first_new_pos = UINT_MAX;
+
+ if (ctx->partial) {
+ if ((sync_flags & MAILDIR_UIDLIST_SYNC_KEEP_STATE) == 0) {
+ /* initially mark all nonsynced */
+ maildir_uidlist_mark_all(uidlist, TRUE);
+ }
+ return 1;
+ }
+ i_assert(uidlist->locked_refresh);
+
+ ctx->record_pool = pool_alloconly_create(MEMPOOL_GROWING
+ "maildir_uidlist_sync", 16384);
+ hash_table_create(&ctx->files, ctx->record_pool, 4096,
+ maildir_filename_base_hash,
+ maildir_filename_base_cmp);
+
+ i_array_init(&ctx->records, array_count(&uidlist->records));
+ return 1;
+}
+
+static int
+maildir_uidlist_sync_next_partial(struct maildir_uidlist_sync_ctx *ctx,
+ const char *filename, uint32_t uid,
+ enum maildir_uidlist_rec_flag flags,
+ struct maildir_uidlist_rec **rec_r)
+{
+ struct maildir_uidlist *uidlist = ctx->uidlist;
+ struct maildir_uidlist_rec *rec, *const *recs;
+ unsigned int count;
+
+ /* we'll update uidlist directly */
+ rec = hash_table_lookup(uidlist->files, filename);
+ if (rec == NULL) {
+ /* doesn't exist in uidlist */
+ if (!ctx->locked) {
+ /* we can't add it, so just ignore it */
+ return 1;
+ }
+ if (ctx->first_new_pos == UINT_MAX)
+ ctx->first_new_pos = array_count(&uidlist->records);
+ ctx->new_files_count++;
+ ctx->changed = TRUE;
+
+ if (uidlist->record_pool == NULL) {
+ uidlist->record_pool =
+ pool_alloconly_create(MEMPOOL_GROWING
+ "uidlist record_pool",
+ 1024);
+ }
+
+ rec = p_new(uidlist->record_pool,
+ struct maildir_uidlist_rec, 1);
+ rec->uid = (uint32_t)-1;
+ rec->filename = p_strdup(uidlist->record_pool, filename);
+ array_push_back(&uidlist->records, &rec);
+ uidlist->change_counter++;
+
+ hash_table_insert(uidlist->files, rec->filename, rec);
+ } else if (strcmp(rec->filename, filename) != 0) {
+ rec->filename = p_strdup(uidlist->record_pool, filename);
+ }
+ if (uid != 0) {
+ if (rec->uid != uid && rec->uid != (uint32_t)-1) {
+ mailbox_set_critical(uidlist->box,
+ "Maildir: %s changed UID %u -> %u",
+ filename, rec->uid, uid);
+ return -1;
+ }
+ rec->uid = uid;
+ if (uidlist->next_uid <= uid)
+ uidlist->next_uid = uid + 1;
+ else {
+ recs = array_get(&uidlist->records, &count);
+ if (count > 1 && uid < recs[count-1]->uid)
+ uidlist->unsorted = TRUE;
+ }
+ }
+
+ rec->flags &= ENUM_NEGATE(MAILDIR_UIDLIST_REC_FLAG_NEW_DIR);
+ rec->flags = (rec->flags | flags) &
+ ENUM_NEGATE(MAILDIR_UIDLIST_REC_FLAG_NONSYNCED);
+
+ ctx->finished = FALSE;
+ *rec_r = rec;
+ return 1;
+}
+
+static unsigned char *ext_dup(pool_t pool, const unsigned char *extensions)
+{
+ unsigned char *ret;
+
+ if (extensions == NULL)
+ return NULL;
+
+ T_BEGIN {
+ unsigned int len;
+
+ for (len = 0; extensions[len] != '\0'; len++) {
+ while (extensions[len] != '\0') len++;
+ }
+ ret = p_malloc(pool, len + 1);
+ memcpy(ret, extensions, len);
+ } T_END;
+ return ret;
+}
+
+int maildir_uidlist_sync_next(struct maildir_uidlist_sync_ctx *ctx,
+ const char *filename,
+ enum maildir_uidlist_rec_flag flags)
+{
+ struct maildir_uidlist_rec *rec;
+
+ return maildir_uidlist_sync_next_uid(ctx, filename, 0, flags, &rec);
+}
+
+int maildir_uidlist_sync_next_uid(struct maildir_uidlist_sync_ctx *ctx,
+ const char *filename, uint32_t uid,
+ enum maildir_uidlist_rec_flag flags,
+ struct maildir_uidlist_rec **rec_r)
+{
+ struct maildir_uidlist *uidlist = ctx->uidlist;
+ struct maildir_uidlist_rec *rec, *old_rec;
+ const char *p;
+
+ *rec_r = NULL;
+
+ if (ctx->failed)
+ return -1;
+ for (p = filename; *p != '\0'; p++) {
+ if (*p == 13 || *p == 10) {
+ i_warning("Maildir %s: Ignoring a file with #0x%x: %s",
+ mailbox_get_path(uidlist->box), *p, filename);
+ return 1;
+ }
+ }
+
+ if (ctx->partial) {
+ return maildir_uidlist_sync_next_partial(ctx, filename,
+ uid, flags, rec_r);
+ }
+
+ rec = hash_table_lookup(ctx->files, filename);
+ if (rec != NULL) {
+ if ((rec->flags & (MAILDIR_UIDLIST_REC_FLAG_NEW_DIR |
+ MAILDIR_UIDLIST_REC_FLAG_MOVED)) == 0) {
+ /* possibly duplicate */
+ return 0;
+ }
+
+ /* probably was in new/ and now we're seeing it in cur/.
+ remove new/moved flags so if this happens again we'll know
+ to check for duplicates. */
+ rec->flags &= ENUM_NEGATE(MAILDIR_UIDLIST_REC_FLAG_NEW_DIR |
+ MAILDIR_UIDLIST_REC_FLAG_MOVED);
+ if (strcmp(rec->filename, filename) != 0)
+ rec->filename = p_strdup(ctx->record_pool, filename);
+ } else {
+ old_rec = hash_table_lookup(uidlist->files, filename);
+ i_assert(old_rec != NULL || UIDLIST_IS_LOCKED(uidlist));
+
+ rec = p_new(ctx->record_pool, struct maildir_uidlist_rec, 1);
+
+ if (old_rec != NULL) {
+ *rec = *old_rec;
+ rec->extensions =
+ ext_dup(ctx->record_pool, rec->extensions);
+ } else {
+ rec->uid = (uint32_t)-1;
+ ctx->new_files_count++;
+ ctx->changed = TRUE;
+ /* didn't exist in uidlist, it's recent */
+ flags |= MAILDIR_UIDLIST_REC_FLAG_RECENT;
+ }
+ rec->filename = p_strdup(ctx->record_pool, filename);
+ hash_table_insert(ctx->files, rec->filename, rec);
+
+ array_push_back(&ctx->records, &rec);
+ }
+ if (uid != 0) {
+ rec->uid = uid;
+ if (uidlist->next_uid <= uid)
+ uidlist->next_uid = uid + 1;
+ }
+
+ rec->flags = (rec->flags | flags) & ENUM_NEGATE(MAILDIR_UIDLIST_REC_FLAG_NONSYNCED);
+ *rec_r = rec;
+ return 1;
+}
+
+void maildir_uidlist_sync_remove(struct maildir_uidlist_sync_ctx *ctx,
+ const char *filename)
+{
+ struct maildir_uidlist_rec *rec;
+ unsigned int idx;
+
+ i_assert(ctx->partial);
+ i_assert(ctx->uidlist->locked_refresh);
+
+ rec = hash_table_lookup(ctx->uidlist->files, filename);
+ i_assert(rec != NULL);
+ i_assert(rec->uid != (uint32_t)-1);
+
+ hash_table_remove(ctx->uidlist->files, filename);
+ idx = maildir_uidlist_records_array_delete(ctx->uidlist, rec);
+
+ if (ctx->first_unwritten_pos != UINT_MAX) {
+ i_assert(ctx->first_unwritten_pos > idx);
+ ctx->first_unwritten_pos--;
+ }
+ if (ctx->first_new_pos != UINT_MAX) {
+ i_assert(ctx->first_new_pos > idx);
+ ctx->first_new_pos--;
+ }
+
+ ctx->changed = TRUE;
+ ctx->uidlist->recreate = TRUE;
+}
+
+void maildir_uidlist_sync_set_ext(struct maildir_uidlist_sync_ctx *ctx,
+ struct maildir_uidlist_rec *rec,
+ enum maildir_uidlist_rec_ext_key key,
+ const char *value)
+{
+ pool_t pool = ctx->partial ?
+ ctx->uidlist->record_pool : ctx->record_pool;
+
+ i_assert(MAILDIR_UIDLIST_REC_EXT_KEY_IS_VALID(key));
+
+ maildir_uidlist_rec_set_ext(rec, pool, key, value);
+}
+
+const char *
+maildir_uidlist_sync_get_full_filename(struct maildir_uidlist_sync_ctx *ctx,
+ const char *filename)
+{
+ struct maildir_uidlist_rec *rec;
+
+ rec = hash_table_lookup(ctx->files, filename);
+ return rec == NULL ? NULL : rec->filename;
+}
+
+bool maildir_uidlist_get_uid(struct maildir_uidlist *uidlist,
+ const char *filename, uint32_t *uid_r)
+{
+ struct maildir_uidlist_rec *rec;
+
+ rec = hash_table_lookup(uidlist->files, filename);
+ if (rec == NULL)
+ return FALSE;
+
+ *uid_r = rec->uid;
+ return TRUE;
+}
+
+void maildir_uidlist_update_fname(struct maildir_uidlist *uidlist,
+ const char *filename)
+{
+ struct maildir_uidlist_rec *rec;
+
+ rec = hash_table_lookup(uidlist->files, filename);
+ if (rec == NULL)
+ return;
+
+ rec->flags &= ENUM_NEGATE(MAILDIR_UIDLIST_REC_FLAG_NONSYNCED);
+ if (strcmp(rec->filename, filename) != 0)
+ rec->filename = p_strdup(uidlist->record_pool, filename);
+}
+
+const char *
+maildir_uidlist_get_full_filename(struct maildir_uidlist *uidlist,
+ const char *filename)
+{
+ struct maildir_uidlist_rec *rec;
+
+ rec = hash_table_lookup(uidlist->files, filename);
+ return rec == NULL ? NULL : rec->filename;
+}
+
+static int maildir_assign_uid_cmp(const void *p1, const void *p2)
+{
+ const struct maildir_uidlist_rec *const *rec1 = p1, *const *rec2 = p2;
+
+ if ((*rec1)->uid != (*rec2)->uid) {
+ if ((*rec1)->uid < (*rec2)->uid)
+ return -1;
+ else
+ return 1;
+ }
+ return maildir_filename_sort_cmp((*rec1)->filename, (*rec2)->filename);
+}
+
+static void maildir_uidlist_assign_uids(struct maildir_uidlist_sync_ctx *ctx)
+{
+ struct maildir_uidlist_rec **recs;
+ unsigned int dest, count;
+
+ i_assert(UIDLIST_IS_LOCKED(ctx->uidlist));
+ i_assert(ctx->first_new_pos != UINT_MAX);
+
+ if (ctx->first_unwritten_pos == UINT_MAX)
+ ctx->first_unwritten_pos = ctx->first_new_pos;
+
+ /* sort new files and assign UIDs for them */
+ recs = array_get_modifiable(&ctx->uidlist->records, &count);
+ qsort(recs + ctx->first_new_pos, count - ctx->first_new_pos,
+ sizeof(*recs), maildir_assign_uid_cmp);
+
+ for (dest = ctx->first_new_pos; dest < count; dest++) {
+ if (recs[dest]->uid == (uint32_t)-1)
+ break;
+ }
+
+ for (; dest < count; dest++) {
+ i_assert(recs[dest]->uid == (uint32_t)-1);
+ i_assert(ctx->uidlist->next_uid < (uint32_t)-1);
+ recs[dest]->uid = ctx->uidlist->next_uid++;
+ recs[dest]->flags &= ENUM_NEGATE(MAILDIR_UIDLIST_REC_FLAG_MOVED);
+ }
+
+ if (ctx->uidlist->locked_refresh && ctx->uidlist->initial_read)
+ ctx->uidlist->last_seen_uid = ctx->uidlist->next_uid-1;
+
+ ctx->new_files_count = 0;
+ ctx->first_new_pos = UINT_MAX;
+ ctx->uidlist->change_counter++;
+ ctx->finish_change_counter = ctx->uidlist->change_counter;
+}
+
+static void maildir_uidlist_swap(struct maildir_uidlist_sync_ctx *ctx)
+{
+ struct maildir_uidlist *uidlist = ctx->uidlist;
+
+ /* buffer is unsorted, sort it by UID */
+ array_sort(&ctx->records, maildir_uid_cmp);
+
+ array_free(&uidlist->records);
+ uidlist->records = ctx->records;
+ ctx->records.arr.buffer = NULL;
+ i_assert(array_is_created(&uidlist->records));
+
+ hash_table_destroy(&uidlist->files);
+ uidlist->files = ctx->files;
+ i_zero(&ctx->files);
+
+ pool_unref(&uidlist->record_pool);
+ uidlist->record_pool = ctx->record_pool;
+ ctx->record_pool = NULL;
+
+ if (ctx->new_files_count != 0) {
+ ctx->first_new_pos = array_count(&uidlist->records) -
+ ctx->new_files_count;
+ maildir_uidlist_assign_uids(ctx);
+ } else {
+ ctx->uidlist->change_counter++;
+ }
+}
+
+void maildir_uidlist_sync_recreate(struct maildir_uidlist_sync_ctx *ctx)
+{
+ ctx->uidlist->recreate = TRUE;
+}
+
+void maildir_uidlist_sync_finish(struct maildir_uidlist_sync_ctx *ctx)
+{
+ if (!ctx->partial) {
+ if (!ctx->failed)
+ maildir_uidlist_swap(ctx);
+ } else {
+ if (ctx->new_files_count != 0 && !ctx->failed) {
+ i_assert(ctx->changed);
+ i_assert(ctx->locked);
+ maildir_uidlist_assign_uids(ctx);
+ }
+ }
+
+ ctx->finished = TRUE;
+
+ /* mbox=NULL means we're coming from dbox rebuilding code.
+ the dbox is already locked, so allow uidlist recreation */
+ i_assert(ctx->locked || !ctx->changed);
+ if ((ctx->changed || maildir_uidlist_want_compress(ctx)) &&
+ !ctx->failed && ctx->locked) {
+ T_BEGIN {
+ if (maildir_uidlist_sync_update(ctx) < 0) {
+ /* we couldn't write everything we wanted. make
+ sure we don't continue using those UIDs */
+ maildir_uidlist_reset(ctx->uidlist);
+ ctx->failed = TRUE;
+ }
+ } T_END;
+ }
+}
+
+int maildir_uidlist_sync_deinit(struct maildir_uidlist_sync_ctx **_ctx,
+ bool success)
+{
+ struct maildir_uidlist_sync_ctx *ctx = *_ctx;
+ int ret;
+
+ *_ctx = NULL;
+
+ if (!success)
+ ctx->failed = TRUE;
+ ret = ctx->failed ? -1 : 0;
+
+ if (!ctx->finished)
+ maildir_uidlist_sync_finish(ctx);
+ if (ctx->partial)
+ maildir_uidlist_mark_all(ctx->uidlist, FALSE);
+ if (ctx->locked)
+ maildir_uidlist_unlock(ctx->uidlist);
+
+ hash_table_destroy(&ctx->files);
+ pool_unref(&ctx->record_pool);
+ if (array_is_created(&ctx->records))
+ array_free(&ctx->records);
+ i_free(ctx);
+ return ret;
+}
+
+void maildir_uidlist_add_flags(struct maildir_uidlist *uidlist,
+ const char *filename,
+ enum maildir_uidlist_rec_flag flags)
+{
+ struct maildir_uidlist_rec *rec;
+
+ rec = hash_table_lookup(uidlist->files, filename);
+ i_assert(rec != NULL);
+
+ rec->flags |= flags;
+}
+
+struct maildir_uidlist_iter_ctx *
+maildir_uidlist_iter_init(struct maildir_uidlist *uidlist)
+{
+ struct maildir_uidlist_iter_ctx *ctx;
+ unsigned int count;
+
+ ctx = i_new(struct maildir_uidlist_iter_ctx, 1);
+ ctx->uidlist = uidlist;
+ ctx->next = array_get(&uidlist->records, &count);
+ ctx->end = ctx->next + count;
+ ctx->change_counter = ctx->uidlist->change_counter;
+ return ctx;
+}
+
+static void
+maildir_uidlist_iter_update_idx(struct maildir_uidlist_iter_ctx *ctx)
+{
+ unsigned int old_rev_idx, idx, count;
+
+ old_rev_idx = ctx->end - ctx->next;
+ ctx->next = array_get(&ctx->uidlist->records, &count);
+ ctx->end = ctx->next + count;
+
+ idx = old_rev_idx >= count ? 0 :
+ count - old_rev_idx;
+ while (idx < count && ctx->next[idx]->uid <= ctx->prev_uid)
+ idx++;
+ while (idx > 0 && ctx->next[idx-1]->uid > ctx->prev_uid)
+ idx--;
+
+ ctx->next += idx;
+}
+
+static bool maildir_uidlist_iter_next_rec(struct maildir_uidlist_iter_ctx *ctx,
+ struct maildir_uidlist_rec **rec_r)
+{
+ struct maildir_uidlist_rec *rec;
+
+ if (ctx->change_counter != ctx->uidlist->change_counter)
+ maildir_uidlist_iter_update_idx(ctx);
+
+ if (ctx->next == ctx->end)
+ return FALSE;
+
+ rec = *ctx->next;
+ i_assert(rec->uid != (uint32_t)-1);
+
+ ctx->prev_uid = rec->uid;
+ ctx->next++;
+
+ *rec_r = rec;
+ return TRUE;
+}
+
+bool maildir_uidlist_iter_next(struct maildir_uidlist_iter_ctx *ctx,
+ uint32_t *uid_r,
+ enum maildir_uidlist_rec_flag *flags_r,
+ const char **filename_r)
+{
+ struct maildir_uidlist_rec *rec;
+
+ if (!maildir_uidlist_iter_next_rec(ctx, &rec))
+ return FALSE;
+
+ *uid_r = rec->uid;
+ *flags_r = rec->flags;
+ *filename_r = rec->filename;
+ return TRUE;
+}
+
+void maildir_uidlist_iter_deinit(struct maildir_uidlist_iter_ctx **_ctx)
+{
+ i_free(*_ctx);
+ *_ctx = NULL;
+}
diff --git a/src/lib-storage/index/maildir/maildir-uidlist.h b/src/lib-storage/index/maildir/maildir-uidlist.h
new file mode 100644
index 0000000..8b45723
--- /dev/null
+++ b/src/lib-storage/index/maildir/maildir-uidlist.h
@@ -0,0 +1,161 @@
+#ifndef MAILDIR_UIDLIST_H
+#define MAILDIR_UIDLIST_H
+
+#include "mail-storage.h"
+
+#define MAILDIR_UIDLIST_NAME "dovecot-uidlist"
+/* how many seconds to wait before overriding uidlist.lock */
+#define MAILDIR_UIDLIST_LOCK_STALE_TIMEOUT (60*2)
+
+struct maildir_mailbox;
+struct maildir_uidlist;
+struct maildir_uidlist_sync_ctx;
+struct maildir_uidlist_rec;
+
+enum maildir_uidlist_sync_flags {
+ MAILDIR_UIDLIST_SYNC_PARTIAL = 0x01,
+ MAILDIR_UIDLIST_SYNC_KEEP_STATE = 0x02,
+ MAILDIR_UIDLIST_SYNC_FORCE = 0x04,
+ MAILDIR_UIDLIST_SYNC_TRYLOCK = 0x08,
+ MAILDIR_UIDLIST_SYNC_NOREFRESH = 0x10,
+ MAILDIR_UIDLIST_SYNC_NOLOCK = 0x20
+};
+
+enum maildir_uidlist_rec_flag {
+ MAILDIR_UIDLIST_REC_FLAG_NEW_DIR = 0x01,
+ MAILDIR_UIDLIST_REC_FLAG_MOVED = 0x02,
+ MAILDIR_UIDLIST_REC_FLAG_RECENT = 0x04,
+ MAILDIR_UIDLIST_REC_FLAG_NONSYNCED = 0x08,
+ MAILDIR_UIDLIST_REC_FLAG_RACING = 0x10
+};
+
+enum maildir_uidlist_hdr_ext_key {
+ MAILDIR_UIDLIST_HDR_EXT_UID_VALIDITY = 'V',
+ MAILDIR_UIDLIST_HDR_EXT_NEXT_UID = 'N',
+ MAILDIR_UIDLIST_HDR_EXT_GUID = 'G',
+ /* POP3 UIDL format unless overridden by records */
+ MAILDIR_UIDLIST_HDR_EXT_POP3_UIDL_FORMAT = 'P'
+};
+
+#define MAILDIR_UIDLIST_REC_EXT_KEY_IS_VALID(c) \
+ ((c) >= 'A' && (c) <= 'Z')
+enum maildir_uidlist_rec_ext_key {
+ /* Physical message size. If filename also contains ,S=<vsize> this
+ isn't written to uidlist. */
+ MAILDIR_UIDLIST_REC_EXT_PSIZE = 'S',
+ /* Virtual message size. If filename also contains ,W=<vsize> this
+ isn't written to uidlist. */
+ MAILDIR_UIDLIST_REC_EXT_VSIZE = 'W',
+ /* POP3 UIDL overriding the default format */
+ MAILDIR_UIDLIST_REC_EXT_POP3_UIDL = 'P',
+ /* POP3 message ordering number. Lower numbered messages are listed
+ first. Messages without ordering number are listed after them.
+ The idea is to be able to preserve POP3 UIDL list and IMAP UIDs
+ perfectly when migrating from other servers. */
+ MAILDIR_UIDLIST_REC_EXT_POP3_ORDER = 'O',
+ /* Message GUID (default is the base filename) */
+ MAILDIR_UIDLIST_REC_EXT_GUID = 'G'
+};
+
+int maildir_uidlist_lock(struct maildir_uidlist *uidlist);
+int maildir_uidlist_try_lock(struct maildir_uidlist *uidlist);
+int maildir_uidlist_lock_touch(struct maildir_uidlist *uidlist);
+void maildir_uidlist_unlock(struct maildir_uidlist *uidlist);
+bool maildir_uidlist_is_locked(struct maildir_uidlist *uidlist);
+bool maildir_uidlist_is_read(struct maildir_uidlist *uidlist);
+/* Returns TRUE if uidlist file is currently open */
+bool maildir_uidlist_is_open(struct maildir_uidlist *uidlist);
+
+struct maildir_uidlist *maildir_uidlist_init(struct maildir_mailbox *mbox);
+void maildir_uidlist_deinit(struct maildir_uidlist **uidlist);
+
+/* Returns -1 if error, 0 if file is broken or lost, 1 if ok. If nfs_flush=TRUE
+ and storage has NFS_FLUSH flag set, the NFS attribute cache is flushed to
+ make sure that we see the latest uidlist file. */
+int maildir_uidlist_refresh(struct maildir_uidlist *uidlist);
+/* Like maildir_uidlist_refresh(), but if uidlist isn't opened yet, try to
+ fill in the uidvalidity/nextuid from index file instead. */
+int maildir_uidlist_refresh_fast_init(struct maildir_uidlist *uidlist);
+
+/* Look up uidlist record for given filename. Returns 1 if found,
+ 0 if not found, -1 if error */
+int maildir_uidlist_lookup(struct maildir_uidlist *uidlist, uint32_t uid,
+ enum maildir_uidlist_rec_flag *flags_r,
+ const char **fname_r);
+/* Returns extension's value or NULL if it doesn't exist. */
+const char *
+maildir_uidlist_lookup_ext(struct maildir_uidlist *uidlist, uint32_t uid,
+ enum maildir_uidlist_rec_ext_key key);
+
+uint32_t maildir_uidlist_get_uid_validity(struct maildir_uidlist *uidlist);
+uint32_t maildir_uidlist_get_next_uid(struct maildir_uidlist *uidlist);
+int maildir_uidlist_get_mailbox_guid(struct maildir_uidlist *uidlist,
+ guid_128_t mailbox_guid);
+void maildir_uidlist_set_mailbox_guid(struct maildir_uidlist *uidlist,
+ const guid_128_t mailbox_guid);
+
+void maildir_uidlist_set_uid_validity(struct maildir_uidlist *uidlist,
+ uint32_t uid_validity);
+void maildir_uidlist_set_next_uid(struct maildir_uidlist *uidlist,
+ uint32_t next_uid, bool force);
+
+/* Update extended record. */
+void maildir_uidlist_set_ext(struct maildir_uidlist *uidlist, uint32_t uid,
+ enum maildir_uidlist_rec_ext_key key,
+ const char *value);
+void maildir_uidlist_unset_ext(struct maildir_uidlist *uidlist, uint32_t uid,
+ enum maildir_uidlist_rec_ext_key key);
+
+/* If uidlist has changed, update it. This is mostly meant to be used with
+ maildir_uidlist_set_ext() */
+int maildir_uidlist_update(struct maildir_uidlist *uidlist);
+
+void maildir_uidlist_set_all_nonsynced(struct maildir_uidlist *uidlist);
+/* Sync uidlist with what's actually on maildir. Returns same as
+ maildir_uidlist_lock(). */
+int maildir_uidlist_sync_init(struct maildir_uidlist *uidlist,
+ enum maildir_uidlist_sync_flags sync_flags,
+ struct maildir_uidlist_sync_ctx **sync_ctx_r);
+int maildir_uidlist_sync_next(struct maildir_uidlist_sync_ctx *ctx,
+ const char *filename,
+ enum maildir_uidlist_rec_flag flags);
+int maildir_uidlist_sync_next_uid(struct maildir_uidlist_sync_ctx *ctx,
+ const char *filename, uint32_t uid,
+ enum maildir_uidlist_rec_flag flags,
+ struct maildir_uidlist_rec **rec_r);
+void maildir_uidlist_sync_remove(struct maildir_uidlist_sync_ctx *ctx,
+ const char *filename);
+void maildir_uidlist_sync_set_ext(struct maildir_uidlist_sync_ctx *ctx,
+ struct maildir_uidlist_rec *rec,
+ enum maildir_uidlist_rec_ext_key key,
+ const char *value);
+void maildir_uidlist_update_fname(struct maildir_uidlist *uidlist,
+ const char *filename);
+const char *
+maildir_uidlist_sync_get_full_filename(struct maildir_uidlist_sync_ctx *ctx,
+ const char *filename);
+void maildir_uidlist_sync_recreate(struct maildir_uidlist_sync_ctx *ctx);
+void maildir_uidlist_sync_finish(struct maildir_uidlist_sync_ctx *ctx);
+int maildir_uidlist_sync_deinit(struct maildir_uidlist_sync_ctx **ctx,
+ bool success);
+
+bool maildir_uidlist_get_uid(struct maildir_uidlist *uidlist,
+ const char *filename, uint32_t *uid_r);
+const char *
+maildir_uidlist_get_full_filename(struct maildir_uidlist *uidlist,
+ const char *filename);
+
+void maildir_uidlist_add_flags(struct maildir_uidlist *uidlist,
+ const char *filename,
+ enum maildir_uidlist_rec_flag flags);
+
+/* List all maildir files. */
+struct maildir_uidlist_iter_ctx *
+maildir_uidlist_iter_init(struct maildir_uidlist *uidlist);
+bool maildir_uidlist_iter_next(struct maildir_uidlist_iter_ctx *ctx,
+ uint32_t *uid_r,
+ enum maildir_uidlist_rec_flag *flags_r,
+ const char **filename_r);
+void maildir_uidlist_iter_deinit(struct maildir_uidlist_iter_ctx **ctx);
+
+#endif
diff --git a/src/lib-storage/index/maildir/maildir-util.c b/src/lib-storage/index/maildir/maildir-util.c
new file mode 100644
index 0000000..c03546e
--- /dev/null
+++ b/src/lib-storage/index/maildir/maildir-util.c
@@ -0,0 +1,323 @@
+/* Copyright (c) 2004-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "ioloop.h"
+#include "str.h"
+#include "mkdir-parents.h"
+#include "mailbox-list-private.h"
+#include "maildir-storage.h"
+#include "maildir-uidlist.h"
+#include "maildir-keywords.h"
+#include "maildir-filename-flags.h"
+#include "maildir-sync.h"
+#include "mailbox-recent-flags.h"
+
+#include <stdio.h>
+#include <unistd.h>
+#include <dirent.h>
+#include <fcntl.h>
+#include <utime.h>
+#include <sys/stat.h>
+
+#define MAILDIR_RESYNC_RETRY_COUNT 10
+
+static const char *
+maildir_filename_guess(struct maildir_mailbox *mbox, uint32_t uid,
+ const char *fname,
+ enum maildir_uidlist_rec_flag *uidlist_flags,
+ bool *have_flags_r)
+
+{
+ struct mail_index_view *view = mbox->flags_view;
+ struct maildir_keywords_sync_ctx *kw_ctx;
+ enum mail_flags flags;
+ ARRAY_TYPE(keyword_indexes) keywords;
+ const char *p;
+ uint32_t seq;
+
+ if (view == NULL || !mail_index_lookup_seq(view, uid, &seq)) {
+ *have_flags_r = FALSE;
+ return fname;
+ }
+
+ t_array_init(&keywords, 32);
+ mail_index_lookup_view_flags(view, seq, &flags, &keywords);
+ if (array_count(&keywords) == 0) {
+ *have_flags_r = (flags & MAIL_FLAGS_NONRECENT) != 0;
+ fname = maildir_filename_flags_set(fname, flags);
+ } else {
+ *have_flags_r = TRUE;
+ kw_ctx = maildir_keywords_sync_init_readonly(mbox->keywords,
+ mbox->box.index);
+ fname = maildir_filename_flags_kw_set(kw_ctx, fname,
+ flags, &keywords);
+ maildir_keywords_sync_deinit(&kw_ctx);
+ }
+
+ if (*have_flags_r) {
+ /* don't even bother looking into new/ dir */
+ *uidlist_flags &= MAILDIR_UIDLIST_REC_FLAG_NEW_DIR;
+ } else if ((*uidlist_flags & MAILDIR_UIDLIST_REC_FLAG_MOVED) == 0 &&
+ ((*uidlist_flags & MAILDIR_UIDLIST_REC_FLAG_NEW_DIR) != 0 ||
+ mailbox_recent_flags_have_uid(&mbox->box, uid))) {
+ /* probably in new/ dir, drop ":2," from fname */
+ *uidlist_flags |= MAILDIR_UIDLIST_REC_FLAG_NEW_DIR;
+ p = strrchr(fname, MAILDIR_INFO_SEP);
+ if (p != NULL)
+ fname = t_strdup_until(fname, p);
+ }
+
+ return fname;
+}
+
+static int maildir_file_do_try(struct maildir_mailbox *mbox, uint32_t uid,
+ maildir_file_do_func *callback, void *context)
+{
+ const char *path, *fname;
+ enum maildir_uidlist_rec_flag flags;
+ bool have_flags;
+ int ret;
+
+ ret = maildir_sync_lookup(mbox, uid, &flags, &fname);
+ if (ret <= 0)
+ return ret == 0 ? -2 : -1;
+
+ if ((flags & MAILDIR_UIDLIST_REC_FLAG_NONSYNCED) != 0) {
+ /* let's see if we can guess the filename based on index */
+ fname = maildir_filename_guess(mbox, uid, fname,
+ &flags, &have_flags);
+ }
+ /* make a copy, just in case callback refreshes uidlist and
+ the pointer becomes invalid. */
+ fname = t_strdup(fname);
+
+ ret = 0;
+ if ((flags & MAILDIR_UIDLIST_REC_FLAG_NEW_DIR) != 0) {
+ /* probably in new/ dir */
+ path = t_strconcat(mailbox_get_path(&mbox->box),
+ "/new/", fname, NULL);
+ ret = callback(mbox, path, context);
+ }
+ if (ret == 0) {
+ path = t_strconcat(mailbox_get_path(&mbox->box), "/cur/",
+ fname, NULL);
+ ret = callback(mbox, path, context);
+ }
+ if (ret > 0 && (flags & MAILDIR_UIDLIST_REC_FLAG_NONSYNCED) != 0) {
+ /* file was found. make sure we remember its latest name. */
+ maildir_uidlist_update_fname(mbox->uidlist, fname);
+ } else if (ret == 0 &&
+ (flags & MAILDIR_UIDLIST_REC_FLAG_NONSYNCED) == 0) {
+ /* file wasn't found. mark this message nonsynced, so we can
+ retry the lookup by guessing the flags */
+ maildir_uidlist_add_flags(mbox->uidlist, fname,
+ MAILDIR_UIDLIST_REC_FLAG_NONSYNCED);
+ }
+ return ret;
+}
+
+static int do_racecheck(struct maildir_mailbox *mbox, const char *path,
+ void *context)
+{
+ const uint32_t *uidp = context;
+ struct stat st;
+ int ret;
+
+ ret = lstat(path, &st);
+ if (ret == 0 && (st.st_mode & S_IFMT) == S_IFLNK) {
+ /* most likely a symlink pointing to a nonexistent file */
+ mailbox_set_critical(&mbox->box,
+ "Maildir: Symlink destination doesn't exist for UID=%u: %s", *uidp, path);
+ return -2;
+ } else if (ret < 0 && errno != ENOENT) {
+ mailbox_set_critical(&mbox->box, "lstat(%s) failed: %m", path);
+ return -1;
+ } else {
+ /* success or ENOENT, either way we're done */
+ mailbox_set_critical(&mbox->box,
+ "maildir_file_do(%s): Filename keeps changing for UID=%u", path, *uidp);
+ return -1;
+ }
+}
+
+#undef maildir_file_do
+int maildir_file_do(struct maildir_mailbox *mbox, uint32_t uid,
+ maildir_file_do_func *callback, void *context)
+{
+ int i, ret;
+
+ T_BEGIN {
+ ret = maildir_file_do_try(mbox, uid, callback, context);
+ } T_END;
+ if (ret == 0 && mbox->storage->set->maildir_very_dirty_syncs) T_BEGIN {
+ /* try guessing again with refreshed flags */
+ if (maildir_sync_refresh_flags_view(mbox) == 0)
+ ret = maildir_file_do_try(mbox, uid, callback, context);
+ } T_END;
+ for (i = 0; i < MAILDIR_RESYNC_RETRY_COUNT && ret == 0; i++) {
+ /* file is either renamed or deleted. sync the maildir and
+ see which one. if file appears to be renamed constantly,
+ don't try to open it more than 10 times. */
+ if (maildir_storage_sync_force(mbox, uid) < 0)
+ return -1;
+
+ T_BEGIN {
+ ret = maildir_file_do_try(mbox, uid, callback, context);
+ } T_END;
+ }
+
+ if (i == MAILDIR_RESYNC_RETRY_COUNT) T_BEGIN {
+ ret = maildir_file_do_try(mbox, uid, do_racecheck, &uid);
+ } T_END;
+
+ return ret == -2 ? 0 : ret;
+}
+
+static int maildir_create_path(struct mailbox *box, const char *path,
+ enum mailbox_list_path_type type, bool retry)
+{
+ const struct mailbox_permissions *perm = mailbox_get_permissions(box);
+ const char *p, *parent;
+
+ if (mkdir_chgrp(path, perm->dir_create_mode, perm->file_create_gid,
+ perm->file_create_gid_origin) == 0)
+ return 0;
+
+ switch (errno) {
+ case EEXIST:
+ return 0;
+ case ENOENT:
+ p = strrchr(path, '/');
+ if (type == MAILBOX_LIST_PATH_TYPE_MAILBOX ||
+ p == NULL || !retry) {
+ /* mailbox was being deleted just now */
+ mailbox_set_deleted(box);
+ return -1;
+ }
+ /* create index/control root directory */
+ parent = t_strdup_until(path, p);
+ if (mailbox_list_mkdir_root(box->list, parent, type) < 0) {
+ mail_storage_copy_list_error(box->storage, box->list);
+ return -1;
+ }
+ /* should work now, try again */
+ return maildir_create_path(box, path, type, FALSE);
+ default:
+ mailbox_set_critical(box, "mkdir(%s) failed: %m", path);
+ return -1;
+ }
+}
+
+static int maildir_create_subdirs(struct mailbox *box)
+{
+ static const char *subdirs[] = { "cur", "new", "tmp" };
+ const char *dirs[N_ELEMENTS(subdirs) + 2];
+ enum mailbox_list_path_type types[N_ELEMENTS(subdirs) + 2];
+ struct stat st;
+ const char *path;
+ unsigned int i, count;
+
+ /* @UNSAFE: get a list of directories we want to create */
+ for (i = 0; i < N_ELEMENTS(subdirs); i++) {
+ types[i] = MAILBOX_LIST_PATH_TYPE_MAILBOX;
+ dirs[i] = t_strconcat(mailbox_get_path(box),
+ "/", subdirs[i], NULL);
+ }
+ if (mailbox_get_path_to(box, MAILBOX_LIST_PATH_TYPE_CONTROL, &path) > 0) {
+ types[i] = MAILBOX_LIST_PATH_TYPE_CONTROL;
+ dirs[i++] = path;
+ }
+ if (mailbox_get_path_to(box, MAILBOX_LIST_PATH_TYPE_INDEX, &path) > 0) {
+ types[i] = MAILBOX_LIST_PATH_TYPE_INDEX;
+ dirs[i++] = path;
+ }
+ count = i;
+ i_assert(count <= N_ELEMENTS(dirs));
+
+ for (i = 0; i < count; i++) {
+ path = dirs[i];
+ if (stat(path, &st) == 0)
+ continue;
+ if (errno != ENOENT) {
+ mailbox_set_critical(box, "stat(%s) failed: %m", path);
+ break;
+ }
+ if (maildir_create_path(box, path, types[i], TRUE) < 0)
+ break;
+ }
+ return i == N_ELEMENTS(dirs) ? 0 : -1;
+}
+
+bool maildir_set_deleted(struct mailbox *box)
+{
+ struct stat st;
+ int ret;
+
+ if (stat(mailbox_get_path(box), &st) < 0) {
+ if (errno == ENOENT)
+ mailbox_set_deleted(box);
+ else {
+ mailbox_set_critical(box,
+ "stat(%s) failed: %m", mailbox_get_path(box));
+ }
+ return FALSE;
+ }
+ /* maildir itself exists. create all of its subdirectories in case
+ they got lost. */
+ T_BEGIN {
+ ret = maildir_create_subdirs(box);
+ } T_END;
+ return ret < 0 ? FALSE : TRUE;
+}
+
+int maildir_lose_unexpected_dir(struct mail_storage *storage, const char *path)
+{
+ const char *dest, *fname, *p;
+
+ /* There's a directory in maildir, get rid of it.
+
+ In some installations this was caused by a messed up configuration
+ where e.g. mails was initially delivered to new/new/ directory.
+ Also Dovecot v2.0.0 - v2.0.4 sometimes may have renamed tmp/
+ directory under new/ or cur/. */
+ if (rmdir(path) == 0) {
+ mail_storage_set_critical(storage,
+ "Maildir: rmdir()ed unwanted empty directory: %s",
+ path);
+ return 1;
+ } else if (errno == ENOENT) {
+ /* someone else rmdired or renamed it */
+ return 0;
+ } else if (errno != ENOTEMPTY) {
+ mail_storage_set_critical(storage,
+ "Maildir: Found unwanted directory %s, "
+ "but rmdir() failed: %m", path);
+ return -1;
+ }
+
+ /* It's not safe to delete this directory since it has some files in it,
+ but it's also not helpful to log this message over and over again.
+ Get rid of this error by renaming the directory elsewhere */
+ p = strrchr(path, '/');
+ i_assert(p != NULL);
+ fname = p + 1;
+ while (p != path && p[-1] != '/') p--;
+ i_assert(p != NULL);
+
+ dest = t_strconcat(t_strdup_until(path, p), "extra-", fname, NULL);
+ if (rename(path, dest) == 0) {
+ mail_storage_set_critical(storage,
+ "Maildir: renamed unwanted directory %s to %s",
+ path, dest);
+ return 1;
+ } else if (errno == ENOENT) {
+ /* someone else renamed it (could have been flag change) */
+ return 0;
+ } else {
+ mail_storage_set_critical(storage,
+ "Maildir: Found unwanted directory, "
+ "but rename(%s, %s) failed: %m", path, dest);
+ return -1;
+ }
+}
diff --git a/src/lib-storage/index/mbox/Makefile.am b/src/lib-storage/index/mbox/Makefile.am
new file mode 100644
index 0000000..4165a20
--- /dev/null
+++ b/src/lib-storage/index/mbox/Makefile.am
@@ -0,0 +1,39 @@
+noinst_LTLIBRARIES = libstorage_mbox.la
+
+AM_CPPFLAGS = \
+ -I$(top_srcdir)/src/lib \
+ -I$(top_srcdir)/src/lib-settings \
+ -I$(top_srcdir)/src/lib-master \
+ -I$(top_srcdir)/src/lib-mail \
+ -I$(top_srcdir)/src/lib-imap \
+ -I$(top_srcdir)/src/lib-index \
+ -I$(top_srcdir)/src/lib-storage \
+ -I$(top_srcdir)/src/lib-storage/index
+
+libstorage_mbox_la_SOURCES = \
+ istream-raw-mbox.c \
+ mbox-file.c \
+ mbox-lock.c \
+ mbox-mail.c \
+ mbox-md5-apop3d.c \
+ mbox-md5-all.c \
+ mbox-save.c \
+ mbox-settings.c \
+ mbox-sync-list-index.c \
+ mbox-sync-parse.c \
+ mbox-sync-rewrite.c \
+ mbox-sync-update.c \
+ mbox-sync.c \
+ mbox-storage.c
+
+headers = \
+ istream-raw-mbox.h \
+ mbox-file.h \
+ mbox-lock.h \
+ mbox-md5.h \
+ mbox-settings.h \
+ mbox-storage.h \
+ mbox-sync-private.h
+
+pkginc_libdir=$(pkgincludedir)
+pkginc_lib_HEADERS = $(headers)
diff --git a/src/lib-storage/index/mbox/Makefile.in b/src/lib-storage/index/mbox/Makefile.in
new file mode 100644
index 0000000..924b48f
--- /dev/null
+++ b/src/lib-storage/index/mbox/Makefile.in
@@ -0,0 +1,884 @@
+# Makefile.in generated by automake 1.16.1 from Makefile.am.
+# @configure_input@
+
+# Copyright (C) 1994-2018 Free Software Foundation, Inc.
+
+# This Makefile.in is free software; the Free Software Foundation
+# gives unlimited permission to copy and/or distribute it,
+# with or without modifications, as long as this notice is preserved.
+
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY, to the extent permitted by law; without
+# even the implied warranty of MERCHANTABILITY or FITNESS FOR A
+# PARTICULAR PURPOSE.
+
+@SET_MAKE@
+
+
+VPATH = @srcdir@
+am__is_gnu_make = { \
+ if test -z '$(MAKELEVEL)'; then \
+ false; \
+ elif test -n '$(MAKE_HOST)'; then \
+ true; \
+ elif test -n '$(MAKE_VERSION)' && test -n '$(CURDIR)'; then \
+ true; \
+ else \
+ false; \
+ fi; \
+}
+am__make_running_with_option = \
+ case $${target_option-} in \
+ ?) ;; \
+ *) echo "am__make_running_with_option: internal error: invalid" \
+ "target option '$${target_option-}' specified" >&2; \
+ exit 1;; \
+ esac; \
+ has_opt=no; \
+ sane_makeflags=$$MAKEFLAGS; \
+ if $(am__is_gnu_make); then \
+ sane_makeflags=$$MFLAGS; \
+ else \
+ case $$MAKEFLAGS in \
+ *\\[\ \ ]*) \
+ bs=\\; \
+ sane_makeflags=`printf '%s\n' "$$MAKEFLAGS" \
+ | sed "s/$$bs$$bs[$$bs $$bs ]*//g"`;; \
+ esac; \
+ fi; \
+ skip_next=no; \
+ strip_trailopt () \
+ { \
+ flg=`printf '%s\n' "$$flg" | sed "s/$$1.*$$//"`; \
+ }; \
+ for flg in $$sane_makeflags; do \
+ test $$skip_next = yes && { skip_next=no; continue; }; \
+ case $$flg in \
+ *=*|--*) continue;; \
+ -*I) strip_trailopt 'I'; skip_next=yes;; \
+ -*I?*) strip_trailopt 'I';; \
+ -*O) strip_trailopt 'O'; skip_next=yes;; \
+ -*O?*) strip_trailopt 'O';; \
+ -*l) strip_trailopt 'l'; skip_next=yes;; \
+ -*l?*) strip_trailopt 'l';; \
+ -[dEDm]) skip_next=yes;; \
+ -[JT]) skip_next=yes;; \
+ esac; \
+ case $$flg in \
+ *$$target_option*) has_opt=yes; break;; \
+ esac; \
+ done; \
+ test $$has_opt = yes
+am__make_dryrun = (target_option=n; $(am__make_running_with_option))
+am__make_keepgoing = (target_option=k; $(am__make_running_with_option))
+pkgdatadir = $(datadir)/@PACKAGE@
+pkgincludedir = $(includedir)/@PACKAGE@
+pkglibdir = $(libdir)/@PACKAGE@
+pkglibexecdir = $(libexecdir)/@PACKAGE@
+am__cd = CDPATH="$${ZSH_VERSION+.}$(PATH_SEPARATOR)" && cd
+install_sh_DATA = $(install_sh) -c -m 644
+install_sh_PROGRAM = $(install_sh) -c
+install_sh_SCRIPT = $(install_sh) -c
+INSTALL_HEADER = $(INSTALL_DATA)
+transform = $(program_transform_name)
+NORMAL_INSTALL = :
+PRE_INSTALL = :
+POST_INSTALL = :
+NORMAL_UNINSTALL = :
+PRE_UNINSTALL = :
+POST_UNINSTALL = :
+build_triplet = @build@
+host_triplet = @host@
+subdir = src/lib-storage/index/mbox
+ACLOCAL_M4 = $(top_srcdir)/aclocal.m4
+am__aclocal_m4_deps = $(top_srcdir)/m4/ac_checktype2.m4 \
+ $(top_srcdir)/m4/ac_typeof.m4 $(top_srcdir)/m4/arc4random.m4 \
+ $(top_srcdir)/m4/blockdev.m4 $(top_srcdir)/m4/c99_vsnprintf.m4 \
+ $(top_srcdir)/m4/clock_gettime.m4 $(top_srcdir)/m4/crypt.m4 \
+ $(top_srcdir)/m4/crypt_xpg6.m4 $(top_srcdir)/m4/dbqlk.m4 \
+ $(top_srcdir)/m4/dirent_dtype.m4 $(top_srcdir)/m4/dovecot.m4 \
+ $(top_srcdir)/m4/fd_passing.m4 $(top_srcdir)/m4/fdatasync.m4 \
+ $(top_srcdir)/m4/flexible_array_member.m4 \
+ $(top_srcdir)/m4/glibc.m4 $(top_srcdir)/m4/gmtime_max.m4 \
+ $(top_srcdir)/m4/gmtime_tm_gmtoff.m4 \
+ $(top_srcdir)/m4/ioloop.m4 $(top_srcdir)/m4/iovec.m4 \
+ $(top_srcdir)/m4/ipv6.m4 $(top_srcdir)/m4/libcap.m4 \
+ $(top_srcdir)/m4/libtool.m4 $(top_srcdir)/m4/libwrap.m4 \
+ $(top_srcdir)/m4/linux_mremap.m4 $(top_srcdir)/m4/ltoptions.m4 \
+ $(top_srcdir)/m4/ltsugar.m4 $(top_srcdir)/m4/ltversion.m4 \
+ $(top_srcdir)/m4/lt~obsolete.m4 $(top_srcdir)/m4/mmap_write.m4 \
+ $(top_srcdir)/m4/mntctl.m4 $(top_srcdir)/m4/modules.m4 \
+ $(top_srcdir)/m4/notify.m4 $(top_srcdir)/m4/nsl.m4 \
+ $(top_srcdir)/m4/off_t_max.m4 $(top_srcdir)/m4/pkg.m4 \
+ $(top_srcdir)/m4/pr_set_dumpable.m4 \
+ $(top_srcdir)/m4/q_quotactl.m4 $(top_srcdir)/m4/quota.m4 \
+ $(top_srcdir)/m4/random.m4 $(top_srcdir)/m4/rlimit.m4 \
+ $(top_srcdir)/m4/sendfile.m4 $(top_srcdir)/m4/size_t_signed.m4 \
+ $(top_srcdir)/m4/sockpeercred.m4 $(top_srcdir)/m4/sql.m4 \
+ $(top_srcdir)/m4/ssl.m4 $(top_srcdir)/m4/st_tim.m4 \
+ $(top_srcdir)/m4/static_array.m4 $(top_srcdir)/m4/test_with.m4 \
+ $(top_srcdir)/m4/time_t.m4 $(top_srcdir)/m4/typeof.m4 \
+ $(top_srcdir)/m4/typeof_dev_t.m4 \
+ $(top_srcdir)/m4/uoff_t_max.m4 $(top_srcdir)/m4/vararg.m4 \
+ $(top_srcdir)/m4/want_apparmor.m4 \
+ $(top_srcdir)/m4/want_bsdauth.m4 \
+ $(top_srcdir)/m4/want_bzlib.m4 \
+ $(top_srcdir)/m4/want_cassandra.m4 \
+ $(top_srcdir)/m4/want_cdb.m4 \
+ $(top_srcdir)/m4/want_checkpassword.m4 \
+ $(top_srcdir)/m4/want_clucene.m4 $(top_srcdir)/m4/want_db.m4 \
+ $(top_srcdir)/m4/want_gssapi.m4 $(top_srcdir)/m4/want_icu.m4 \
+ $(top_srcdir)/m4/want_ldap.m4 $(top_srcdir)/m4/want_lua.m4 \
+ $(top_srcdir)/m4/want_lz4.m4 $(top_srcdir)/m4/want_lzma.m4 \
+ $(top_srcdir)/m4/want_mysql.m4 $(top_srcdir)/m4/want_pam.m4 \
+ $(top_srcdir)/m4/want_passwd.m4 $(top_srcdir)/m4/want_pgsql.m4 \
+ $(top_srcdir)/m4/want_prefetch.m4 \
+ $(top_srcdir)/m4/want_shadow.m4 \
+ $(top_srcdir)/m4/want_sodium.m4 $(top_srcdir)/m4/want_solr.m4 \
+ $(top_srcdir)/m4/want_sqlite.m4 \
+ $(top_srcdir)/m4/want_stemmer.m4 \
+ $(top_srcdir)/m4/want_systemd.m4 \
+ $(top_srcdir)/m4/want_textcat.m4 \
+ $(top_srcdir)/m4/want_unwind.m4 $(top_srcdir)/m4/want_zlib.m4 \
+ $(top_srcdir)/m4/want_zstd.m4 $(top_srcdir)/configure.ac
+am__configure_deps = $(am__aclocal_m4_deps) $(CONFIGURE_DEPENDENCIES) \
+ $(ACLOCAL_M4)
+DIST_COMMON = $(srcdir)/Makefile.am $(pkginc_lib_HEADERS) \
+ $(am__DIST_COMMON)
+mkinstalldirs = $(install_sh) -d
+CONFIG_HEADER = $(top_builddir)/config.h
+CONFIG_CLEAN_FILES =
+CONFIG_CLEAN_VPATH_FILES =
+LTLIBRARIES = $(noinst_LTLIBRARIES)
+libstorage_mbox_la_LIBADD =
+am_libstorage_mbox_la_OBJECTS = istream-raw-mbox.lo mbox-file.lo \
+ mbox-lock.lo mbox-mail.lo mbox-md5-apop3d.lo mbox-md5-all.lo \
+ mbox-save.lo mbox-settings.lo mbox-sync-list-index.lo \
+ mbox-sync-parse.lo mbox-sync-rewrite.lo mbox-sync-update.lo \
+ mbox-sync.lo mbox-storage.lo
+libstorage_mbox_la_OBJECTS = $(am_libstorage_mbox_la_OBJECTS)
+AM_V_lt = $(am__v_lt_@AM_V@)
+am__v_lt_ = $(am__v_lt_@AM_DEFAULT_V@)
+am__v_lt_0 = --silent
+am__v_lt_1 =
+AM_V_P = $(am__v_P_@AM_V@)
+am__v_P_ = $(am__v_P_@AM_DEFAULT_V@)
+am__v_P_0 = false
+am__v_P_1 = :
+AM_V_GEN = $(am__v_GEN_@AM_V@)
+am__v_GEN_ = $(am__v_GEN_@AM_DEFAULT_V@)
+am__v_GEN_0 = @echo " GEN " $@;
+am__v_GEN_1 =
+AM_V_at = $(am__v_at_@AM_V@)
+am__v_at_ = $(am__v_at_@AM_DEFAULT_V@)
+am__v_at_0 = @
+am__v_at_1 =
+DEFAULT_INCLUDES = -I.@am__isrc@ -I$(top_builddir)
+depcomp = $(SHELL) $(top_srcdir)/depcomp
+am__maybe_remake_depfiles = depfiles
+am__depfiles_remade = ./$(DEPDIR)/istream-raw-mbox.Plo \
+ ./$(DEPDIR)/mbox-file.Plo ./$(DEPDIR)/mbox-lock.Plo \
+ ./$(DEPDIR)/mbox-mail.Plo ./$(DEPDIR)/mbox-md5-all.Plo \
+ ./$(DEPDIR)/mbox-md5-apop3d.Plo ./$(DEPDIR)/mbox-save.Plo \
+ ./$(DEPDIR)/mbox-settings.Plo ./$(DEPDIR)/mbox-storage.Plo \
+ ./$(DEPDIR)/mbox-sync-list-index.Plo \
+ ./$(DEPDIR)/mbox-sync-parse.Plo \
+ ./$(DEPDIR)/mbox-sync-rewrite.Plo \
+ ./$(DEPDIR)/mbox-sync-update.Plo ./$(DEPDIR)/mbox-sync.Plo
+am__mv = mv -f
+COMPILE = $(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) \
+ $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS)
+LTCOMPILE = $(LIBTOOL) $(AM_V_lt) --tag=CC $(AM_LIBTOOLFLAGS) \
+ $(LIBTOOLFLAGS) --mode=compile $(CC) $(DEFS) \
+ $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) $(CPPFLAGS) \
+ $(AM_CFLAGS) $(CFLAGS)
+AM_V_CC = $(am__v_CC_@AM_V@)
+am__v_CC_ = $(am__v_CC_@AM_DEFAULT_V@)
+am__v_CC_0 = @echo " CC " $@;
+am__v_CC_1 =
+CCLD = $(CC)
+LINK = $(LIBTOOL) $(AM_V_lt) --tag=CC $(AM_LIBTOOLFLAGS) \
+ $(LIBTOOLFLAGS) --mode=link $(CCLD) $(AM_CFLAGS) $(CFLAGS) \
+ $(AM_LDFLAGS) $(LDFLAGS) -o $@
+AM_V_CCLD = $(am__v_CCLD_@AM_V@)
+am__v_CCLD_ = $(am__v_CCLD_@AM_DEFAULT_V@)
+am__v_CCLD_0 = @echo " CCLD " $@;
+am__v_CCLD_1 =
+SOURCES = $(libstorage_mbox_la_SOURCES)
+DIST_SOURCES = $(libstorage_mbox_la_SOURCES)
+am__can_run_installinfo = \
+ case $$AM_UPDATE_INFO_DIR in \
+ n|no|NO) false;; \
+ *) (install-info --version) >/dev/null 2>&1;; \
+ esac
+am__vpath_adj_setup = srcdirstrip=`echo "$(srcdir)" | sed 's|.|.|g'`;
+am__vpath_adj = case $$p in \
+ $(srcdir)/*) f=`echo "$$p" | sed "s|^$$srcdirstrip/||"`;; \
+ *) f=$$p;; \
+ esac;
+am__strip_dir = f=`echo $$p | sed -e 's|^.*/||'`;
+am__install_max = 40
+am__nobase_strip_setup = \
+ srcdirstrip=`echo "$(srcdir)" | sed 's/[].[^$$\\*|]/\\\\&/g'`
+am__nobase_strip = \
+ for p in $$list; do echo "$$p"; done | sed -e "s|$$srcdirstrip/||"
+am__nobase_list = $(am__nobase_strip_setup); \
+ for p in $$list; do echo "$$p $$p"; done | \
+ sed "s| $$srcdirstrip/| |;"' / .*\//!s/ .*/ ./; s,\( .*\)/[^/]*$$,\1,' | \
+ $(AWK) 'BEGIN { files["."] = "" } { files[$$2] = files[$$2] " " $$1; \
+ if (++n[$$2] == $(am__install_max)) \
+ { print $$2, files[$$2]; n[$$2] = 0; files[$$2] = "" } } \
+ END { for (dir in files) print dir, files[dir] }'
+am__base_list = \
+ sed '$$!N;$$!N;$$!N;$$!N;$$!N;$$!N;$$!N;s/\n/ /g' | \
+ sed '$$!N;$$!N;$$!N;$$!N;s/\n/ /g'
+am__uninstall_files_from_dir = { \
+ test -z "$$files" \
+ || { test ! -d "$$dir" && test ! -f "$$dir" && test ! -r "$$dir"; } \
+ || { echo " ( cd '$$dir' && rm -f" $$files ")"; \
+ $(am__cd) "$$dir" && rm -f $$files; }; \
+ }
+am__installdirs = "$(DESTDIR)$(pkginc_libdir)"
+HEADERS = $(pkginc_lib_HEADERS)
+am__tagged_files = $(HEADERS) $(SOURCES) $(TAGS_FILES) $(LISP)
+# Read a list of newline-separated strings from the standard input,
+# and print each of them once, without duplicates. Input order is
+# *not* preserved.
+am__uniquify_input = $(AWK) '\
+ BEGIN { nonempty = 0; } \
+ { items[$$0] = 1; nonempty = 1; } \
+ END { if (nonempty) { for (i in items) print i; }; } \
+'
+# Make sure the list of sources is unique. This is necessary because,
+# e.g., the same source file might be shared among _SOURCES variables
+# for different programs/libraries.
+am__define_uniq_tagged_files = \
+ list='$(am__tagged_files)'; \
+ unique=`for i in $$list; do \
+ if test -f "$$i"; then echo $$i; else echo $(srcdir)/$$i; fi; \
+ done | $(am__uniquify_input)`
+ETAGS = etags
+CTAGS = ctags
+am__DIST_COMMON = $(srcdir)/Makefile.in $(top_srcdir)/depcomp
+DISTFILES = $(DIST_COMMON) $(DIST_SOURCES) $(TEXINFOS) $(EXTRA_DIST)
+ACLOCAL = @ACLOCAL@
+ACLOCAL_AMFLAGS = @ACLOCAL_AMFLAGS@
+AMTAR = @AMTAR@
+AM_DEFAULT_VERBOSITY = @AM_DEFAULT_VERBOSITY@
+APPARMOR_LIBS = @APPARMOR_LIBS@
+AR = @AR@
+AUTH_CFLAGS = @AUTH_CFLAGS@
+AUTH_LIBS = @AUTH_LIBS@
+AUTOCONF = @AUTOCONF@
+AUTOHEADER = @AUTOHEADER@
+AUTOMAKE = @AUTOMAKE@
+AWK = @AWK@
+BINARY_CFLAGS = @BINARY_CFLAGS@
+BINARY_LDFLAGS = @BINARY_LDFLAGS@
+BISON = @BISON@
+CASSANDRA_CFLAGS = @CASSANDRA_CFLAGS@
+CASSANDRA_LIBS = @CASSANDRA_LIBS@
+CC = @CC@
+CCDEPMODE = @CCDEPMODE@
+CDB_LIBS = @CDB_LIBS@
+CFLAGS = @CFLAGS@
+CLUCENE_CFLAGS = @CLUCENE_CFLAGS@
+CLUCENE_LIBS = @CLUCENE_LIBS@
+COMPRESS_LIBS = @COMPRESS_LIBS@
+CPP = @CPP@
+CPPFLAGS = @CPPFLAGS@
+CRYPT_LIBS = @CRYPT_LIBS@
+CXX = @CXX@
+CXXCPP = @CXXCPP@
+CXXDEPMODE = @CXXDEPMODE@
+CXXFLAGS = @CXXFLAGS@
+CYGPATH_W = @CYGPATH_W@
+DEFS = @DEFS@
+DEPDIR = @DEPDIR@
+DICT_LIBS = @DICT_LIBS@
+DLLIB = @DLLIB@
+DLLTOOL = @DLLTOOL@
+DSYMUTIL = @DSYMUTIL@
+DUMPBIN = @DUMPBIN@
+ECHO_C = @ECHO_C@
+ECHO_N = @ECHO_N@
+ECHO_T = @ECHO_T@
+EGREP = @EGREP@
+EXEEXT = @EXEEXT@
+FGREP = @FGREP@
+FLEX = @FLEX@
+FUZZER_CPPFLAGS = @FUZZER_CPPFLAGS@
+FUZZER_LDFLAGS = @FUZZER_LDFLAGS@
+GREP = @GREP@
+INSTALL = @INSTALL@
+INSTALL_DATA = @INSTALL_DATA@
+INSTALL_PROGRAM = @INSTALL_PROGRAM@
+INSTALL_SCRIPT = @INSTALL_SCRIPT@
+INSTALL_STRIP_PROGRAM = @INSTALL_STRIP_PROGRAM@
+KRB5CONFIG = @KRB5CONFIG@
+KRB5_CFLAGS = @KRB5_CFLAGS@
+KRB5_LIBS = @KRB5_LIBS@
+LD = @LD@
+LDAP_LIBS = @LDAP_LIBS@
+LDFLAGS = @LDFLAGS@
+LD_NO_WHOLE_ARCHIVE = @LD_NO_WHOLE_ARCHIVE@
+LD_WHOLE_ARCHIVE = @LD_WHOLE_ARCHIVE@
+LIBCAP = @LIBCAP@
+LIBDOVECOT = @LIBDOVECOT@
+LIBDOVECOT_COMPRESS = @LIBDOVECOT_COMPRESS@
+LIBDOVECOT_DEPS = @LIBDOVECOT_DEPS@
+LIBDOVECOT_DSYNC = @LIBDOVECOT_DSYNC@
+LIBDOVECOT_LA_LIBS = @LIBDOVECOT_LA_LIBS@
+LIBDOVECOT_LDA = @LIBDOVECOT_LDA@
+LIBDOVECOT_LDAP = @LIBDOVECOT_LDAP@
+LIBDOVECOT_LIBFTS = @LIBDOVECOT_LIBFTS@
+LIBDOVECOT_LIBFTS_DEPS = @LIBDOVECOT_LIBFTS_DEPS@
+LIBDOVECOT_LOGIN = @LIBDOVECOT_LOGIN@
+LIBDOVECOT_LUA = @LIBDOVECOT_LUA@
+LIBDOVECOT_LUA_DEPS = @LIBDOVECOT_LUA_DEPS@
+LIBDOVECOT_SQL = @LIBDOVECOT_SQL@
+LIBDOVECOT_STORAGE = @LIBDOVECOT_STORAGE@
+LIBDOVECOT_STORAGE_DEPS = @LIBDOVECOT_STORAGE_DEPS@
+LIBEXTTEXTCAT_CFLAGS = @LIBEXTTEXTCAT_CFLAGS@
+LIBEXTTEXTCAT_LIBS = @LIBEXTTEXTCAT_LIBS@
+LIBICONV = @LIBICONV@
+LIBICU_CFLAGS = @LIBICU_CFLAGS@
+LIBICU_LIBS = @LIBICU_LIBS@
+LIBOBJS = @LIBOBJS@
+LIBS = @LIBS@
+LIBSODIUM_CFLAGS = @LIBSODIUM_CFLAGS@
+LIBSODIUM_LIBS = @LIBSODIUM_LIBS@
+LIBTIRPC_CFLAGS = @LIBTIRPC_CFLAGS@
+LIBTIRPC_LIBS = @LIBTIRPC_LIBS@
+LIBTOOL = @LIBTOOL@
+LIBUNWIND_CFLAGS = @LIBUNWIND_CFLAGS@
+LIBUNWIND_LIBS = @LIBUNWIND_LIBS@
+LIBWRAP_LIBS = @LIBWRAP_LIBS@
+LINKED_STORAGE_LDADD = @LINKED_STORAGE_LDADD@
+LIPO = @LIPO@
+LN_S = @LN_S@
+LTLIBICONV = @LTLIBICONV@
+LTLIBOBJS = @LTLIBOBJS@
+LT_SYS_LIBRARY_PATH = @LT_SYS_LIBRARY_PATH@
+LUA_CFLAGS = @LUA_CFLAGS@
+LUA_LIBS = @LUA_LIBS@
+MAINT = @MAINT@
+MAKEINFO = @MAKEINFO@
+MANIFEST_TOOL = @MANIFEST_TOOL@
+MKDIR_P = @MKDIR_P@
+MODULE_LIBS = @MODULE_LIBS@
+MODULE_SUFFIX = @MODULE_SUFFIX@
+MYSQL_CFLAGS = @MYSQL_CFLAGS@
+MYSQL_CONFIG = @MYSQL_CONFIG@
+MYSQL_LIBS = @MYSQL_LIBS@
+NM = @NM@
+NMEDIT = @NMEDIT@
+NOPLUGIN_LDFLAGS = @NOPLUGIN_LDFLAGS@
+OBJDUMP = @OBJDUMP@
+OBJEXT = @OBJEXT@
+OTOOL = @OTOOL@
+OTOOL64 = @OTOOL64@
+PACKAGE = @PACKAGE@
+PACKAGE_BUGREPORT = @PACKAGE_BUGREPORT@
+PACKAGE_NAME = @PACKAGE_NAME@
+PACKAGE_STRING = @PACKAGE_STRING@
+PACKAGE_TARNAME = @PACKAGE_TARNAME@
+PACKAGE_URL = @PACKAGE_URL@
+PACKAGE_VERSION = @PACKAGE_VERSION@
+PANDOC = @PANDOC@
+PATH_SEPARATOR = @PATH_SEPARATOR@
+PGSQL_CFLAGS = @PGSQL_CFLAGS@
+PGSQL_LIBS = @PGSQL_LIBS@
+PG_CONFIG = @PG_CONFIG@
+PIE_CFLAGS = @PIE_CFLAGS@
+PIE_LDFLAGS = @PIE_LDFLAGS@
+PKG_CONFIG = @PKG_CONFIG@
+PKG_CONFIG_LIBDIR = @PKG_CONFIG_LIBDIR@
+PKG_CONFIG_PATH = @PKG_CONFIG_PATH@
+QUOTA_LIBS = @QUOTA_LIBS@
+RANLIB = @RANLIB@
+RELRO_LDFLAGS = @RELRO_LDFLAGS@
+RPCGEN = @RPCGEN@
+RUN_TEST = @RUN_TEST@
+SED = @SED@
+SETTING_FILES = @SETTING_FILES@
+SET_MAKE = @SET_MAKE@
+SHELL = @SHELL@
+SQLITE_CFLAGS = @SQLITE_CFLAGS@
+SQLITE_LIBS = @SQLITE_LIBS@
+SQL_CFLAGS = @SQL_CFLAGS@
+SQL_LIBS = @SQL_LIBS@
+SSL_CFLAGS = @SSL_CFLAGS@
+SSL_LIBS = @SSL_LIBS@
+STRIP = @STRIP@
+SYSTEMD_CFLAGS = @SYSTEMD_CFLAGS@
+SYSTEMD_LIBS = @SYSTEMD_LIBS@
+VALGRIND = @VALGRIND@
+VERSION = @VERSION@
+ZSTD_CFLAGS = @ZSTD_CFLAGS@
+ZSTD_LIBS = @ZSTD_LIBS@
+abs_builddir = @abs_builddir@
+abs_srcdir = @abs_srcdir@
+abs_top_builddir = @abs_top_builddir@
+abs_top_srcdir = @abs_top_srcdir@
+ac_ct_AR = @ac_ct_AR@
+ac_ct_CC = @ac_ct_CC@
+ac_ct_CXX = @ac_ct_CXX@
+ac_ct_DUMPBIN = @ac_ct_DUMPBIN@
+am__include = @am__include@
+am__leading_dot = @am__leading_dot@
+am__quote = @am__quote@
+am__tar = @am__tar@
+am__untar = @am__untar@
+bindir = @bindir@
+build = @build@
+build_alias = @build_alias@
+build_cpu = @build_cpu@
+build_os = @build_os@
+build_vendor = @build_vendor@
+builddir = @builddir@
+datadir = @datadir@
+datarootdir = @datarootdir@
+dict_drivers = @dict_drivers@
+docdir = @docdir@
+dvidir = @dvidir@
+exec_prefix = @exec_prefix@
+host = @host@
+host_alias = @host_alias@
+host_cpu = @host_cpu@
+host_os = @host_os@
+host_vendor = @host_vendor@
+htmldir = @htmldir@
+includedir = @includedir@
+infodir = @infodir@
+install_sh = @install_sh@
+libdir = @libdir@
+libexecdir = @libexecdir@
+localedir = @localedir@
+localstatedir = @localstatedir@
+mandir = @mandir@
+mkdir_p = @mkdir_p@
+moduledir = @moduledir@
+oldincludedir = @oldincludedir@
+pdfdir = @pdfdir@
+prefix = @prefix@
+program_transform_name = @program_transform_name@
+psdir = @psdir@
+rundir = @rundir@
+runstatedir = @runstatedir@
+sbindir = @sbindir@
+sharedstatedir = @sharedstatedir@
+sql_drivers = @sql_drivers@
+srcdir = @srcdir@
+ssldir = @ssldir@
+statedir = @statedir@
+sysconfdir = @sysconfdir@
+systemdservicetype = @systemdservicetype@
+systemdsystemunitdir = @systemdsystemunitdir@
+target_alias = @target_alias@
+top_build_prefix = @top_build_prefix@
+top_builddir = @top_builddir@
+top_srcdir = @top_srcdir@
+noinst_LTLIBRARIES = libstorage_mbox.la
+AM_CPPFLAGS = \
+ -I$(top_srcdir)/src/lib \
+ -I$(top_srcdir)/src/lib-settings \
+ -I$(top_srcdir)/src/lib-master \
+ -I$(top_srcdir)/src/lib-mail \
+ -I$(top_srcdir)/src/lib-imap \
+ -I$(top_srcdir)/src/lib-index \
+ -I$(top_srcdir)/src/lib-storage \
+ -I$(top_srcdir)/src/lib-storage/index
+
+libstorage_mbox_la_SOURCES = \
+ istream-raw-mbox.c \
+ mbox-file.c \
+ mbox-lock.c \
+ mbox-mail.c \
+ mbox-md5-apop3d.c \
+ mbox-md5-all.c \
+ mbox-save.c \
+ mbox-settings.c \
+ mbox-sync-list-index.c \
+ mbox-sync-parse.c \
+ mbox-sync-rewrite.c \
+ mbox-sync-update.c \
+ mbox-sync.c \
+ mbox-storage.c
+
+headers = \
+ istream-raw-mbox.h \
+ mbox-file.h \
+ mbox-lock.h \
+ mbox-md5.h \
+ mbox-settings.h \
+ mbox-storage.h \
+ mbox-sync-private.h
+
+pkginc_libdir = $(pkgincludedir)
+pkginc_lib_HEADERS = $(headers)
+all: all-am
+
+.SUFFIXES:
+.SUFFIXES: .c .lo .o .obj
+$(srcdir)/Makefile.in: @MAINTAINER_MODE_TRUE@ $(srcdir)/Makefile.am $(am__configure_deps)
+ @for dep in $?; do \
+ case '$(am__configure_deps)' in \
+ *$$dep*) \
+ ( cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh ) \
+ && { if test -f $@; then exit 0; else break; fi; }; \
+ exit 1;; \
+ esac; \
+ done; \
+ echo ' cd $(top_srcdir) && $(AUTOMAKE) --foreign src/lib-storage/index/mbox/Makefile'; \
+ $(am__cd) $(top_srcdir) && \
+ $(AUTOMAKE) --foreign src/lib-storage/index/mbox/Makefile
+Makefile: $(srcdir)/Makefile.in $(top_builddir)/config.status
+ @case '$?' in \
+ *config.status*) \
+ cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh;; \
+ *) \
+ echo ' cd $(top_builddir) && $(SHELL) ./config.status $(subdir)/$@ $(am__maybe_remake_depfiles)'; \
+ cd $(top_builddir) && $(SHELL) ./config.status $(subdir)/$@ $(am__maybe_remake_depfiles);; \
+ esac;
+
+$(top_builddir)/config.status: $(top_srcdir)/configure $(CONFIG_STATUS_DEPENDENCIES)
+ cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh
+
+$(top_srcdir)/configure: @MAINTAINER_MODE_TRUE@ $(am__configure_deps)
+ cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh
+$(ACLOCAL_M4): @MAINTAINER_MODE_TRUE@ $(am__aclocal_m4_deps)
+ cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh
+$(am__aclocal_m4_deps):
+
+clean-noinstLTLIBRARIES:
+ -test -z "$(noinst_LTLIBRARIES)" || rm -f $(noinst_LTLIBRARIES)
+ @list='$(noinst_LTLIBRARIES)'; \
+ locs=`for p in $$list; do echo $$p; done | \
+ sed 's|^[^/]*$$|.|; s|/[^/]*$$||; s|$$|/so_locations|' | \
+ sort -u`; \
+ test -z "$$locs" || { \
+ echo rm -f $${locs}; \
+ rm -f $${locs}; \
+ }
+
+libstorage_mbox.la: $(libstorage_mbox_la_OBJECTS) $(libstorage_mbox_la_DEPENDENCIES) $(EXTRA_libstorage_mbox_la_DEPENDENCIES)
+ $(AM_V_CCLD)$(LINK) $(libstorage_mbox_la_OBJECTS) $(libstorage_mbox_la_LIBADD) $(LIBS)
+
+mostlyclean-compile:
+ -rm -f *.$(OBJEXT)
+
+distclean-compile:
+ -rm -f *.tab.c
+
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/istream-raw-mbox.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/mbox-file.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/mbox-lock.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/mbox-mail.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/mbox-md5-all.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/mbox-md5-apop3d.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/mbox-save.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/mbox-settings.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/mbox-storage.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/mbox-sync-list-index.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/mbox-sync-parse.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/mbox-sync-rewrite.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/mbox-sync-update.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/mbox-sync.Plo@am__quote@ # am--include-marker
+
+$(am__depfiles_remade):
+ @$(MKDIR_P) $(@D)
+ @echo '# dummy' >$@-t && $(am__mv) $@-t $@
+
+am--depfiles: $(am__depfiles_remade)
+
+.c.o:
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(COMPILE) -MT $@ -MD -MP -MF $(DEPDIR)/$*.Tpo -c -o $@ $<
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/$*.Tpo $(DEPDIR)/$*.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='$<' object='$@' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(COMPILE) -c -o $@ $<
+
+.c.obj:
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(COMPILE) -MT $@ -MD -MP -MF $(DEPDIR)/$*.Tpo -c -o $@ `$(CYGPATH_W) '$<'`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/$*.Tpo $(DEPDIR)/$*.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='$<' object='$@' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(COMPILE) -c -o $@ `$(CYGPATH_W) '$<'`
+
+.c.lo:
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(LTCOMPILE) -MT $@ -MD -MP -MF $(DEPDIR)/$*.Tpo -c -o $@ $<
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/$*.Tpo $(DEPDIR)/$*.Plo
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='$<' object='$@' libtool=yes @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(LTCOMPILE) -c -o $@ $<
+
+mostlyclean-libtool:
+ -rm -f *.lo
+
+clean-libtool:
+ -rm -rf .libs _libs
+install-pkginc_libHEADERS: $(pkginc_lib_HEADERS)
+ @$(NORMAL_INSTALL)
+ @list='$(pkginc_lib_HEADERS)'; test -n "$(pkginc_libdir)" || list=; \
+ if test -n "$$list"; then \
+ echo " $(MKDIR_P) '$(DESTDIR)$(pkginc_libdir)'"; \
+ $(MKDIR_P) "$(DESTDIR)$(pkginc_libdir)" || exit 1; \
+ fi; \
+ for p in $$list; do \
+ if test -f "$$p"; then d=; else d="$(srcdir)/"; fi; \
+ echo "$$d$$p"; \
+ done | $(am__base_list) | \
+ while read files; do \
+ echo " $(INSTALL_HEADER) $$files '$(DESTDIR)$(pkginc_libdir)'"; \
+ $(INSTALL_HEADER) $$files "$(DESTDIR)$(pkginc_libdir)" || exit $$?; \
+ done
+
+uninstall-pkginc_libHEADERS:
+ @$(NORMAL_UNINSTALL)
+ @list='$(pkginc_lib_HEADERS)'; test -n "$(pkginc_libdir)" || list=; \
+ files=`for p in $$list; do echo $$p; done | sed -e 's|^.*/||'`; \
+ dir='$(DESTDIR)$(pkginc_libdir)'; $(am__uninstall_files_from_dir)
+
+ID: $(am__tagged_files)
+ $(am__define_uniq_tagged_files); mkid -fID $$unique
+tags: tags-am
+TAGS: tags
+
+tags-am: $(TAGS_DEPENDENCIES) $(am__tagged_files)
+ set x; \
+ here=`pwd`; \
+ $(am__define_uniq_tagged_files); \
+ shift; \
+ if test -z "$(ETAGS_ARGS)$$*$$unique"; then :; else \
+ test -n "$$unique" || unique=$$empty_fix; \
+ if test $$# -gt 0; then \
+ $(ETAGS) $(ETAGSFLAGS) $(AM_ETAGSFLAGS) $(ETAGS_ARGS) \
+ "$$@" $$unique; \
+ else \
+ $(ETAGS) $(ETAGSFLAGS) $(AM_ETAGSFLAGS) $(ETAGS_ARGS) \
+ $$unique; \
+ fi; \
+ fi
+ctags: ctags-am
+
+CTAGS: ctags
+ctags-am: $(TAGS_DEPENDENCIES) $(am__tagged_files)
+ $(am__define_uniq_tagged_files); \
+ test -z "$(CTAGS_ARGS)$$unique" \
+ || $(CTAGS) $(CTAGSFLAGS) $(AM_CTAGSFLAGS) $(CTAGS_ARGS) \
+ $$unique
+
+GTAGS:
+ here=`$(am__cd) $(top_builddir) && pwd` \
+ && $(am__cd) $(top_srcdir) \
+ && gtags -i $(GTAGS_ARGS) "$$here"
+cscopelist: cscopelist-am
+
+cscopelist-am: $(am__tagged_files)
+ list='$(am__tagged_files)'; \
+ case "$(srcdir)" in \
+ [\\/]* | ?:[\\/]*) sdir="$(srcdir)" ;; \
+ *) sdir=$(subdir)/$(srcdir) ;; \
+ esac; \
+ for i in $$list; do \
+ if test -f "$$i"; then \
+ echo "$(subdir)/$$i"; \
+ else \
+ echo "$$sdir/$$i"; \
+ fi; \
+ done >> $(top_builddir)/cscope.files
+
+distclean-tags:
+ -rm -f TAGS ID GTAGS GRTAGS GSYMS GPATH tags
+
+distdir: $(BUILT_SOURCES)
+ $(MAKE) $(AM_MAKEFLAGS) distdir-am
+
+distdir-am: $(DISTFILES)
+ @srcdirstrip=`echo "$(srcdir)" | sed 's/[].[^$$\\*]/\\\\&/g'`; \
+ topsrcdirstrip=`echo "$(top_srcdir)" | sed 's/[].[^$$\\*]/\\\\&/g'`; \
+ list='$(DISTFILES)'; \
+ dist_files=`for file in $$list; do echo $$file; done | \
+ sed -e "s|^$$srcdirstrip/||;t" \
+ -e "s|^$$topsrcdirstrip/|$(top_builddir)/|;t"`; \
+ case $$dist_files in \
+ */*) $(MKDIR_P) `echo "$$dist_files" | \
+ sed '/\//!d;s|^|$(distdir)/|;s,/[^/]*$$,,' | \
+ sort -u` ;; \
+ esac; \
+ for file in $$dist_files; do \
+ if test -f $$file || test -d $$file; then d=.; else d=$(srcdir); fi; \
+ if test -d $$d/$$file; then \
+ dir=`echo "/$$file" | sed -e 's,/[^/]*$$,,'`; \
+ if test -d "$(distdir)/$$file"; then \
+ find "$(distdir)/$$file" -type d ! -perm -700 -exec chmod u+rwx {} \;; \
+ fi; \
+ if test -d $(srcdir)/$$file && test $$d != $(srcdir); then \
+ cp -fpR $(srcdir)/$$file "$(distdir)$$dir" || exit 1; \
+ find "$(distdir)/$$file" -type d ! -perm -700 -exec chmod u+rwx {} \;; \
+ fi; \
+ cp -fpR $$d/$$file "$(distdir)$$dir" || exit 1; \
+ else \
+ test -f "$(distdir)/$$file" \
+ || cp -p $$d/$$file "$(distdir)/$$file" \
+ || exit 1; \
+ fi; \
+ done
+check-am: all-am
+check: check-am
+all-am: Makefile $(LTLIBRARIES) $(HEADERS)
+installdirs:
+ for dir in "$(DESTDIR)$(pkginc_libdir)"; do \
+ test -z "$$dir" || $(MKDIR_P) "$$dir"; \
+ done
+install: install-am
+install-exec: install-exec-am
+install-data: install-data-am
+uninstall: uninstall-am
+
+install-am: all-am
+ @$(MAKE) $(AM_MAKEFLAGS) install-exec-am install-data-am
+
+installcheck: installcheck-am
+install-strip:
+ if test -z '$(STRIP)'; then \
+ $(MAKE) $(AM_MAKEFLAGS) INSTALL_PROGRAM="$(INSTALL_STRIP_PROGRAM)" \
+ install_sh_PROGRAM="$(INSTALL_STRIP_PROGRAM)" INSTALL_STRIP_FLAG=-s \
+ install; \
+ else \
+ $(MAKE) $(AM_MAKEFLAGS) INSTALL_PROGRAM="$(INSTALL_STRIP_PROGRAM)" \
+ install_sh_PROGRAM="$(INSTALL_STRIP_PROGRAM)" INSTALL_STRIP_FLAG=-s \
+ "INSTALL_PROGRAM_ENV=STRIPPROG='$(STRIP)'" install; \
+ fi
+mostlyclean-generic:
+
+clean-generic:
+
+distclean-generic:
+ -test -z "$(CONFIG_CLEAN_FILES)" || rm -f $(CONFIG_CLEAN_FILES)
+ -test . = "$(srcdir)" || test -z "$(CONFIG_CLEAN_VPATH_FILES)" || rm -f $(CONFIG_CLEAN_VPATH_FILES)
+
+maintainer-clean-generic:
+ @echo "This command is intended for maintainers to use"
+ @echo "it deletes files that may require special tools to rebuild."
+clean: clean-am
+
+clean-am: clean-generic clean-libtool clean-noinstLTLIBRARIES \
+ mostlyclean-am
+
+distclean: distclean-am
+ -rm -f ./$(DEPDIR)/istream-raw-mbox.Plo
+ -rm -f ./$(DEPDIR)/mbox-file.Plo
+ -rm -f ./$(DEPDIR)/mbox-lock.Plo
+ -rm -f ./$(DEPDIR)/mbox-mail.Plo
+ -rm -f ./$(DEPDIR)/mbox-md5-all.Plo
+ -rm -f ./$(DEPDIR)/mbox-md5-apop3d.Plo
+ -rm -f ./$(DEPDIR)/mbox-save.Plo
+ -rm -f ./$(DEPDIR)/mbox-settings.Plo
+ -rm -f ./$(DEPDIR)/mbox-storage.Plo
+ -rm -f ./$(DEPDIR)/mbox-sync-list-index.Plo
+ -rm -f ./$(DEPDIR)/mbox-sync-parse.Plo
+ -rm -f ./$(DEPDIR)/mbox-sync-rewrite.Plo
+ -rm -f ./$(DEPDIR)/mbox-sync-update.Plo
+ -rm -f ./$(DEPDIR)/mbox-sync.Plo
+ -rm -f Makefile
+distclean-am: clean-am distclean-compile distclean-generic \
+ distclean-tags
+
+dvi: dvi-am
+
+dvi-am:
+
+html: html-am
+
+html-am:
+
+info: info-am
+
+info-am:
+
+install-data-am: install-pkginc_libHEADERS
+
+install-dvi: install-dvi-am
+
+install-dvi-am:
+
+install-exec-am:
+
+install-html: install-html-am
+
+install-html-am:
+
+install-info: install-info-am
+
+install-info-am:
+
+install-man:
+
+install-pdf: install-pdf-am
+
+install-pdf-am:
+
+install-ps: install-ps-am
+
+install-ps-am:
+
+installcheck-am:
+
+maintainer-clean: maintainer-clean-am
+ -rm -f ./$(DEPDIR)/istream-raw-mbox.Plo
+ -rm -f ./$(DEPDIR)/mbox-file.Plo
+ -rm -f ./$(DEPDIR)/mbox-lock.Plo
+ -rm -f ./$(DEPDIR)/mbox-mail.Plo
+ -rm -f ./$(DEPDIR)/mbox-md5-all.Plo
+ -rm -f ./$(DEPDIR)/mbox-md5-apop3d.Plo
+ -rm -f ./$(DEPDIR)/mbox-save.Plo
+ -rm -f ./$(DEPDIR)/mbox-settings.Plo
+ -rm -f ./$(DEPDIR)/mbox-storage.Plo
+ -rm -f ./$(DEPDIR)/mbox-sync-list-index.Plo
+ -rm -f ./$(DEPDIR)/mbox-sync-parse.Plo
+ -rm -f ./$(DEPDIR)/mbox-sync-rewrite.Plo
+ -rm -f ./$(DEPDIR)/mbox-sync-update.Plo
+ -rm -f ./$(DEPDIR)/mbox-sync.Plo
+ -rm -f Makefile
+maintainer-clean-am: distclean-am maintainer-clean-generic
+
+mostlyclean: mostlyclean-am
+
+mostlyclean-am: mostlyclean-compile mostlyclean-generic \
+ mostlyclean-libtool
+
+pdf: pdf-am
+
+pdf-am:
+
+ps: ps-am
+
+ps-am:
+
+uninstall-am: uninstall-pkginc_libHEADERS
+
+.MAKE: install-am install-strip
+
+.PHONY: CTAGS GTAGS TAGS all all-am am--depfiles check check-am clean \
+ clean-generic clean-libtool clean-noinstLTLIBRARIES \
+ cscopelist-am ctags ctags-am distclean distclean-compile \
+ distclean-generic distclean-libtool distclean-tags distdir dvi \
+ dvi-am html html-am info info-am install install-am \
+ install-data install-data-am install-dvi install-dvi-am \
+ install-exec install-exec-am install-html install-html-am \
+ install-info install-info-am install-man install-pdf \
+ install-pdf-am install-pkginc_libHEADERS install-ps \
+ install-ps-am install-strip installcheck installcheck-am \
+ installdirs maintainer-clean maintainer-clean-generic \
+ mostlyclean mostlyclean-compile mostlyclean-generic \
+ mostlyclean-libtool pdf pdf-am ps ps-am tags tags-am uninstall \
+ uninstall-am uninstall-pkginc_libHEADERS
+
+.PRECIOUS: Makefile
+
+
+# Tell versions [3.59,3.63) of GNU make to not export all variables.
+# Otherwise a system limit (for SysV at least) may be exceeded.
+.NOEXPORT:
diff --git a/src/lib-storage/index/mbox/istream-raw-mbox.c b/src/lib-storage/index/mbox/istream-raw-mbox.c
new file mode 100644
index 0000000..e91e581
--- /dev/null
+++ b/src/lib-storage/index/mbox/istream-raw-mbox.c
@@ -0,0 +1,821 @@
+/* Copyright (c) 2003-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "buffer.h"
+#include "istream-private.h"
+#include "istream-raw-mbox.h"
+#include "mbox-from.h"
+
+struct raw_mbox_istream {
+ struct istream_private istream;
+
+ time_t received_time, next_received_time;
+ char *sender, *next_sender;
+
+ uoff_t from_offset, hdr_offset, body_offset, mail_size;
+ uoff_t input_peak_offset;
+
+ bool locked:1;
+ bool seeked:1;
+ bool crlf_ending:1;
+ bool corrupted:1;
+ bool mail_size_forced:1;
+ bool eof:1;
+ bool header_missing_eoh:1;
+};
+
+static void mbox_istream_log_read_error(struct raw_mbox_istream *rstream)
+{
+ if (rstream->istream.parent->stream_errno != 0) {
+ /* Log e.g. compression istream error */
+ i_error("Failed to read mbox file %s: %s",
+ i_stream_get_name(&rstream->istream.istream),
+ i_stream_get_error(rstream->istream.parent));
+ }
+}
+
+static void i_stream_raw_mbox_destroy(struct iostream_private *stream)
+{
+ struct raw_mbox_istream *rstream = (struct raw_mbox_istream *)stream;
+
+ i_free(rstream->sender);
+ i_free(rstream->next_sender);
+
+ i_stream_seek(rstream->istream.parent,
+ rstream->istream.istream.v_offset);
+}
+
+static int mbox_read_from_line(struct raw_mbox_istream *rstream)
+{
+ const unsigned char *buf, *p;
+ char *sender;
+ time_t received_time;
+ size_t pos, line_pos;
+ ssize_t ret;
+ unsigned int skip;
+ int tz;
+
+ buf = i_stream_get_data(rstream->istream.parent, &pos);
+ i_assert(pos > 0);
+
+ /* from_offset points to "\nFrom ", so unless we're at the beginning
+ of the file, skip the initial \n */
+ if (rstream->from_offset == 0)
+ skip = 0;
+ else {
+ skip = 1;
+ if (*buf == '\r')
+ skip++;
+ }
+
+ while ((p = memchr(buf+skip, '\n', pos-skip)) == NULL) {
+ ret = i_stream_read_memarea(rstream->istream.parent);
+ buf = i_stream_get_data(rstream->istream.parent, &pos);
+ if (ret < 0) {
+ if (ret == -2) {
+ /* From_-line is too long, but we should be
+ able to parse what we have so far. */
+ break;
+ }
+ /* EOF shouldn't happen */
+ rstream->istream.istream.eof =
+ rstream->istream.parent->eof;
+ rstream->istream.istream.stream_errno =
+ rstream->istream.parent->stream_errno;
+ return -1;
+ }
+ i_assert(pos > 0);
+ }
+ line_pos = p == NULL ? 0 : (size_t)(p - buf);
+
+ /* beginning of mbox */
+ if (memcmp(buf+skip, "From ", 5) != 0 ||
+ mbox_from_parse((buf+skip)+5, (pos-skip)-5,
+ &received_time, &tz, &sender) < 0) {
+ /* broken From - should happen only at beginning of
+ file if this isn't a mbox.. */
+ io_stream_set_error(&rstream->istream.iostream,
+ "mbox file doesn't begin with 'From ' line");
+ rstream->istream.istream.stream_errno = EINVAL;
+ return -1;
+ }
+
+ if (rstream->istream.istream.v_offset == rstream->from_offset) {
+ rstream->received_time = received_time;
+ i_free(rstream->sender);
+ rstream->sender = sender;
+ } else {
+ rstream->next_received_time = received_time;
+ i_free(rstream->next_sender);
+ rstream->next_sender = sender;
+ }
+
+ /* skip over From-line */
+ if (line_pos == 0) {
+ /* line was too long. skip the input until we find LF. */
+ rstream->istream.istream.v_offset += pos;
+ i_stream_skip(rstream->istream.parent, pos);
+
+ while ((ret = i_stream_read_memarea(rstream->istream.parent)) > 0) {
+ p = memchr(buf, '\n', pos);
+ if (p != NULL)
+ break;
+ rstream->istream.istream.v_offset += pos;
+ i_stream_skip(rstream->istream.parent, pos);
+ }
+ if (ret <= 0) {
+ i_assert(ret == -1);
+ /* EOF shouldn't happen */
+ rstream->istream.istream.eof =
+ rstream->istream.parent->eof;
+ rstream->istream.istream.stream_errno =
+ rstream->istream.parent->stream_errno;
+ return -1;
+ }
+ line_pos = (size_t)(p - buf);
+ }
+ rstream->istream.istream.v_offset += line_pos+1;
+ i_stream_skip(rstream->istream.parent, line_pos+1);
+
+ rstream->hdr_offset = rstream->istream.istream.v_offset;
+ return 0;
+}
+
+static void handle_end_of_mail(struct raw_mbox_istream *rstream, size_t pos)
+{
+ rstream->mail_size = rstream->istream.istream.v_offset + pos -
+ rstream->hdr_offset;
+
+ if (rstream->hdr_offset + rstream->mail_size < rstream->body_offset) {
+ uoff_t new_body_offset =
+ rstream->hdr_offset + rstream->mail_size;
+
+ if (rstream->body_offset != UOFF_T_MAX) {
+ /* Header didn't have ending \n */
+ rstream->header_missing_eoh = TRUE;
+ } else {
+ /* "headers\n\nFrom ..", the second \n belongs to next
+ message which we didn't know at the time yet. */
+ }
+
+ /* The +2 check is for CR+LF linefeeds */
+ i_assert(rstream->body_offset == UOFF_T_MAX ||
+ rstream->body_offset == new_body_offset + 1 ||
+ rstream->body_offset == new_body_offset + 2);
+ rstream->body_offset = new_body_offset;
+ }
+}
+
+static ssize_t i_stream_raw_mbox_read(struct istream_private *stream)
+{
+ static const char *mbox_from = "\nFrom ";
+ struct raw_mbox_istream *rstream = (struct raw_mbox_istream *)stream;
+ const unsigned char *buf;
+ const char *fromp;
+ char *sender;
+ time_t received_time;
+ size_t i, pos, new_pos, from_start_pos, from_after_pos;
+ ssize_t ret = 0;
+ int eoh_char, tz;
+ bool crlf_ending = FALSE;
+
+ i_assert(rstream->seeked);
+ i_assert(stream->istream.v_offset >= rstream->from_offset);
+
+ if (stream->istream.eof)
+ return -1;
+ if (rstream->corrupted) {
+ rstream->istream.istream.stream_errno = EINVAL;
+ return -1;
+ }
+
+ i_stream_seek(stream->parent, stream->istream.v_offset);
+
+ stream->pos -= stream->skip;
+ stream->skip = 0;
+ stream->buffer = NULL;
+
+ do {
+ buf = i_stream_get_data(stream->parent, &pos);
+ if (pos > 1 && stream->istream.v_offset + pos >
+ rstream->input_peak_offset) {
+ /* fake our read count. needed because if in the end
+ we have only one character in buffer and we skip it
+ (as potential CR), we want to get back to this
+ i_stream_raw_mbox_read() to read more data. */
+ ret = pos;
+ break;
+ }
+ ret = i_stream_read_memarea(stream->parent);
+ } while (ret > 0);
+ stream->istream.stream_errno = stream->parent->stream_errno;
+
+ if (ret < 0) {
+ if (ret == -1)
+ mbox_istream_log_read_error(rstream);
+ if (ret == -2) {
+ if (stream->skip == stream->pos) {
+ /* From_-line is longer than our input buffer.
+ finish the check without seeing the LF. */
+ } else if (stream->istream.v_offset + pos ==
+ rstream->input_peak_offset) {
+ /* we've read everything our parent stream
+ has to offer. */
+ stream->buffer = buf;
+ return -2;
+ }
+ /* parent stream is full, but we haven't returned
+ all its bytes to our caller yet. */
+ } else if (stream->istream.v_offset != 0 || pos == 0) {
+ /* we've read the whole file, final byte should be
+ the \n trailer */
+ if (pos > 0 && buf[pos-1] == '\n') {
+ pos--;
+ if (pos > 0 && buf[pos-1] == '\r') {
+ crlf_ending = TRUE;
+ pos--;
+ }
+ }
+
+ i_assert(pos >= stream->pos);
+ ret = pos == stream->pos ? -1 :
+ (ssize_t)(pos - stream->pos);
+
+ stream->buffer = buf;
+ stream->pos = pos;
+
+ if (stream->istream.v_offset == rstream->from_offset) {
+ /* haven't seen From-line yet, so this mbox
+ stream is now at EOF */
+ rstream->eof = TRUE;
+ }
+ stream->istream.eof = TRUE;
+ rstream->crlf_ending = crlf_ending;
+ handle_end_of_mail(rstream, pos);
+ return ret < 0 ? i_stream_raw_mbox_read(stream) : ret;
+ }
+ }
+
+ if (stream->istream.v_offset == rstream->from_offset) {
+ /* beginning of message, we haven't yet read our From-line */
+ if (pos == 2 && ret > 0) {
+ /* we're at the end of file with CR+LF linefeeds?
+ need more data to verify it. */
+ rstream->input_peak_offset =
+ stream->istream.v_offset + pos;
+ return i_stream_raw_mbox_read(stream);
+ }
+ if (mbox_read_from_line(rstream) < 0) {
+ io_stream_set_error(&stream->iostream,
+ "Next message unexpectedly corrupted in mbox file "
+ "%s at %"PRIuUOFF_T,
+ i_stream_get_name(&stream->istream),
+ stream->istream.v_offset);
+ if (stream->istream.v_offset != 0)
+ i_error("%s", stream->iostream.error);
+ stream->pos = 0;
+ rstream->eof = TRUE;
+ rstream->corrupted = TRUE;
+ return -1;
+ }
+
+ /* got it. we don't want to return it however,
+ so start again from headers */
+ buf = i_stream_get_data(stream->parent, &pos);
+ if (pos == 0)
+ return i_stream_raw_mbox_read(stream);
+ }
+
+ /* See if we have From-line here - note that it works right only
+ because all characters are different in mbox_from. */
+ fromp = mbox_from; from_start_pos = from_after_pos = SIZE_MAX;
+ eoh_char = rstream->body_offset == UOFF_T_MAX ? '\n' : -1;
+ for (i = stream->pos; i < pos; i++) {
+ if (buf[i] == eoh_char &&
+ ((i > 0 && buf[i-1] == '\n') ||
+ (i > 1 && buf[i-1] == '\r' && buf[i-2] == '\n') ||
+ stream->istream.v_offset + i == rstream->hdr_offset)) {
+ rstream->body_offset = stream->istream.v_offset + i + 1;
+ eoh_char = -1;
+ }
+ if ((char)buf[i] == *fromp) {
+ if (*++fromp == '\0') {
+ /* potential From-line, see if we have the
+ rest of the line buffered. */
+ i++;
+ if (i >= 7 && buf[i-7] == '\r') {
+ /* CR also belongs to it. */
+ crlf_ending = TRUE;
+ from_start_pos = i - 7;
+ } else {
+ crlf_ending = FALSE;
+ from_start_pos = i - 6;
+ }
+
+ if (rstream->mail_size == UOFF_T_MAX ||
+ rstream->hdr_offset + rstream->mail_size ==
+ stream->istream.v_offset + from_start_pos) {
+ from_after_pos = i;
+ if (ret == -2) {
+ /* even if we don't have the
+ whole line, we need to
+ finish this check now. */
+ goto mbox_verify;
+ }
+ }
+ fromp = mbox_from;
+ } else if (from_after_pos != SIZE_MAX) {
+ /* we have the whole From-line here now.
+ See if it's a valid one. */
+ mbox_verify:
+ if (mbox_from_parse(buf + from_after_pos,
+ pos - from_after_pos,
+ &received_time, &tz,
+ &sender) == 0) {
+ /* yep, we stop here. */
+ rstream->next_received_time =
+ received_time;
+ i_free(rstream->next_sender);
+ rstream->next_sender = sender;
+ stream->istream.eof = TRUE;
+
+ rstream->crlf_ending = crlf_ending;
+ handle_end_of_mail(rstream,
+ from_start_pos);
+ break;
+ }
+ from_after_pos = SIZE_MAX;
+ }
+ } else {
+ fromp = mbox_from;
+ if ((char)buf[i] == *fromp)
+ fromp++;
+ }
+ }
+
+ /* we want to go at least one byte further next time */
+ rstream->input_peak_offset = stream->istream.v_offset + i;
+
+ if (from_after_pos != SIZE_MAX) {
+ /* we're waiting for the \n at the end of From-line */
+ new_pos = from_start_pos;
+ } else {
+ /* leave out the beginnings of potential From-line + CR */
+ new_pos = i - (fromp - mbox_from);
+ if (new_pos > 0)
+ new_pos--;
+ }
+
+ if (stream->istream.v_offset -
+ rstream->hdr_offset + new_pos > rstream->mail_size) {
+ /* istream_raw_mbox_set_next_offset() used invalid
+ cached next_offset? */
+ io_stream_set_error(&stream->iostream,
+ "Next message unexpectedly lost from mbox file "
+ "%s at %"PRIuUOFF_T" (%s)",
+ i_stream_get_name(&stream->istream),
+ rstream->hdr_offset + rstream->mail_size,
+ rstream->mail_size_forced ? "cached" : "noncached");
+ i_error("%s", stream->iostream.error);
+ rstream->eof = TRUE;
+ rstream->corrupted = TRUE;
+ rstream->istream.istream.stream_errno = EINVAL;
+ stream->pos = 0;
+ return -1;
+ }
+
+ stream->buffer = buf;
+ if (new_pos == stream->pos) {
+ if (stream->istream.eof || ret > 0)
+ return i_stream_raw_mbox_read(stream);
+ i_assert(new_pos > 0);
+ ret = -2;
+ } else {
+ i_assert(new_pos > stream->pos);
+ ret = new_pos - stream->pos;
+ stream->pos = new_pos;
+ }
+ return ret;
+}
+
+static void i_stream_raw_mbox_seek(struct istream_private *stream,
+ uoff_t v_offset, bool mark ATTR_UNUSED)
+{
+ struct raw_mbox_istream *rstream = (struct raw_mbox_istream *)stream;
+
+ stream->istream.v_offset = v_offset;
+ stream->skip = stream->pos = 0;
+ stream->buffer = NULL;
+
+ rstream->input_peak_offset = 0;
+ rstream->eof = FALSE;
+}
+
+static void i_stream_raw_mbox_sync(struct istream_private *stream)
+{
+ struct raw_mbox_istream *rstream = (struct raw_mbox_istream *)stream;
+
+ i_stream_sync(stream->parent);
+
+ rstream->istream.skip = 0;
+ rstream->istream.pos = 0;
+ rstream->input_peak_offset = 0;
+}
+
+static int
+i_stream_raw_mbox_stat(struct istream_private *stream, bool exact)
+{
+ const struct stat *st;
+ struct raw_mbox_istream *rstream = (struct raw_mbox_istream *)stream;
+
+ if (i_stream_stat(stream->parent, exact, &st) < 0) {
+ stream->istream.stream_errno = stream->parent->stream_errno;
+ return -1;
+ }
+
+ stream->statbuf = *st;
+ stream->statbuf.st_size =
+ !exact && rstream->seeked && rstream->mail_size != UOFF_T_MAX ?
+ (off_t)rstream->mail_size : -1;
+ return 0;
+}
+
+struct istream *i_stream_create_raw_mbox(struct istream *input)
+{
+ struct raw_mbox_istream *rstream;
+
+ i_assert(input->v_offset == 0);
+
+ rstream = i_new(struct raw_mbox_istream, 1);
+
+ rstream->body_offset = UOFF_T_MAX;
+ rstream->mail_size = UOFF_T_MAX;
+ rstream->received_time = (time_t)-1;
+ rstream->next_received_time = (time_t)-1;
+
+ rstream->istream.iostream.destroy = i_stream_raw_mbox_destroy;
+ rstream->istream.max_buffer_size = input->real_stream->max_buffer_size;
+ rstream->istream.read = i_stream_raw_mbox_read;
+ rstream->istream.seek = i_stream_raw_mbox_seek;
+ rstream->istream.sync = i_stream_raw_mbox_sync;
+ rstream->istream.stat = i_stream_raw_mbox_stat;
+
+ rstream->istream.istream.readable_fd = input->readable_fd;
+ rstream->istream.istream.blocking = input->blocking;
+ rstream->istream.istream.seekable = input->seekable;
+
+ return i_stream_create(&rstream->istream, input, -1, 0);
+}
+
+static int istream_raw_mbox_is_valid_from(struct raw_mbox_istream *rstream)
+{
+ const unsigned char *data;
+ size_t size = 0;
+ time_t received_time;
+ char *sender;
+ int tz;
+ ssize_t ret = 0;
+
+ /* minimal: "From x Thu Nov 29 22:33:52 2001" = 31 chars */
+ do {
+ data = i_stream_get_data(rstream->istream.parent, &size);
+ if (size >= 31)
+ break;
+ } while ((ret = i_stream_read_memarea(rstream->istream.parent)) > 0);
+ if (ret == -1)
+ mbox_istream_log_read_error(rstream);
+
+ if ((size == 1 && data[0] == '\n') ||
+ (size == 2 && data[0] == '\r' && data[1] == '\n')) {
+ /* EOF */
+ return 1;
+ }
+
+ if (size > 31 && memcmp(data, "\nFrom ", 6) == 0) {
+ data += 6;
+ size -= 6;
+ } else if (size > 32 && memcmp(data, "\r\nFrom ", 7) == 0) {
+ data += 7;
+ size -= 7;
+ } else {
+ return 0;
+ }
+
+ while (memchr(data, '\n', size) == NULL) {
+ ret = i_stream_read_bytes(rstream->istream.parent,
+ &data, &size, size+1);
+ if (ret < 0) {
+ if (ret == -1)
+ mbox_istream_log_read_error(rstream);
+ break;
+ }
+ }
+
+ if (mbox_from_parse(data, size, &received_time, &tz, &sender) < 0)
+ return 0;
+
+ rstream->next_received_time = received_time;
+ i_free(rstream->next_sender);
+ rstream->next_sender = sender;
+ return 1;
+}
+
+uoff_t istream_raw_mbox_get_start_offset(struct istream *stream)
+{
+ struct raw_mbox_istream *rstream =
+ (struct raw_mbox_istream *)stream->real_stream;
+
+ i_assert(rstream->seeked);
+
+ return rstream->from_offset;
+}
+
+int istream_raw_mbox_get_header_offset(struct istream *stream,
+ uoff_t *hdr_offset_r)
+{
+ struct raw_mbox_istream *rstream =
+ (struct raw_mbox_istream *)stream->real_stream;
+
+ i_assert(rstream->seeked);
+
+ if (rstream->hdr_offset == rstream->from_offset)
+ (void)i_stream_read(stream);
+
+ if (rstream->corrupted) {
+ i_error("Unexpectedly lost From-line from mbox file %s at "
+ "%"PRIuUOFF_T, i_stream_get_name(stream),
+ rstream->from_offset);
+ return -1;
+ }
+ if (stream->stream_errno != 0)
+ return -1;
+
+ *hdr_offset_r = rstream->hdr_offset;
+ return 0;
+}
+
+int istream_raw_mbox_get_body_offset(struct istream *stream,
+ uoff_t *body_offset_r)
+{
+ struct raw_mbox_istream *rstream =
+ (struct raw_mbox_istream *)stream->real_stream;
+ uoff_t offset;
+
+ i_assert(rstream->seeked);
+
+ if (rstream->body_offset != UOFF_T_MAX) {
+ *body_offset_r = rstream->body_offset;
+ return 0;
+ }
+
+ offset = stream->v_offset;
+ i_stream_seek(stream, rstream->hdr_offset);
+ while (rstream->body_offset == UOFF_T_MAX) {
+ i_stream_skip(stream, i_stream_get_data_size(stream));
+
+ if (i_stream_read(stream) < 0) {
+ if (rstream->corrupted) {
+ i_error("Unexpectedly lost From-line from mbox file "
+ "%s at %"PRIuUOFF_T,
+ i_stream_get_name(stream),
+ rstream->from_offset);
+ } else {
+ i_assert(rstream->body_offset != UOFF_T_MAX);
+ }
+ return -1;
+ }
+ }
+
+ i_stream_seek(stream, offset);
+ *body_offset_r = rstream->body_offset;
+ return 0;
+}
+
+int istream_raw_mbox_get_body_size(struct istream *stream,
+ uoff_t expected_body_size,
+ uoff_t *body_size_r)
+{
+ struct raw_mbox_istream *rstream =
+ (struct raw_mbox_istream *)stream->real_stream;
+ const unsigned char *data;
+ size_t size;
+ uoff_t old_offset, body_offset, body_size, next_body_offset;
+
+ i_assert(rstream->seeked);
+ i_assert(rstream->hdr_offset != UOFF_T_MAX);
+
+ if (istream_raw_mbox_get_body_offset(stream, &body_offset) < 0)
+ return -1;
+ body_size = rstream->mail_size == UOFF_T_MAX ? UOFF_T_MAX :
+ rstream->mail_size - (rstream->body_offset -
+ rstream->hdr_offset);
+ old_offset = stream->v_offset;
+ if (expected_body_size != UOFF_T_MAX) {
+ /* if we already have the existing body size, use it as long as
+ it's >= expected body_size. otherwise the previous parsing
+ may have stopped at a From_-line that belongs to the body. */
+ if (body_size != UOFF_T_MAX && body_size >= expected_body_size) {
+ *body_size_r = body_size;
+ return 0;
+ }
+
+ next_body_offset = rstream->body_offset + expected_body_size;
+ /* If header_missing_eoh is set, the message body begins with
+ a From_-line and the body_offset is pointing to the line
+ *before* the first line of the body, i.e. the empty line
+ separating the headers from the body. If that is the case,
+ we'll have to skip over the empty line to get the correct
+ next_body_offset. */
+ if (rstream->header_missing_eoh) {
+ i_assert(body_size == 0);
+ next_body_offset += rstream->crlf_ending ? 2 : 1;
+ }
+
+ i_stream_seek(rstream->istream.parent, next_body_offset);
+ if (istream_raw_mbox_is_valid_from(rstream) > 0) {
+ rstream->mail_size =
+ next_body_offset - rstream->hdr_offset;
+ i_stream_seek(stream, old_offset);
+ *body_size_r = expected_body_size;
+ return 0;
+ }
+ /* invalid expected_body_size */
+ }
+ if (body_size != UOFF_T_MAX) {
+ *body_size_r = body_size;
+ return 0;
+ }
+
+ /* have to read through the message body */
+ while (i_stream_read_more(stream, &data, &size) > 0)
+ i_stream_skip(stream, size);
+ i_stream_seek(stream, old_offset);
+ if (stream->stream_errno != 0)
+ return -1;
+
+ i_assert(rstream->mail_size != UOFF_T_MAX);
+ *body_size_r = rstream->mail_size -
+ (rstream->body_offset - rstream->hdr_offset);
+ return 0;
+}
+
+time_t istream_raw_mbox_get_received_time(struct istream *stream)
+{
+ struct raw_mbox_istream *rstream =
+ (struct raw_mbox_istream *)stream->real_stream;
+
+ i_assert(rstream->seeked);
+
+ if (rstream->received_time == (time_t)-1)
+ (void)i_stream_read(stream);
+ return rstream->received_time;
+}
+
+const char *istream_raw_mbox_get_sender(struct istream *stream)
+{
+ struct raw_mbox_istream *rstream =
+ (struct raw_mbox_istream *)stream->real_stream;
+
+ i_assert(rstream->seeked);
+
+ if (rstream->sender == NULL)
+ (void)i_stream_read(stream);
+ return rstream->sender == NULL ? "" : rstream->sender;
+}
+
+bool istream_raw_mbox_has_crlf_ending(struct istream *stream)
+{
+ struct raw_mbox_istream *rstream =
+ (struct raw_mbox_istream *)stream->real_stream;
+
+ i_assert(rstream->seeked);
+
+ return rstream->crlf_ending;
+}
+
+int istream_raw_mbox_next(struct istream *stream, uoff_t expected_body_size)
+{
+ struct raw_mbox_istream *rstream =
+ (struct raw_mbox_istream *)stream->real_stream;
+ uoff_t body_size;
+
+ if (istream_raw_mbox_get_body_size(stream, expected_body_size,
+ &body_size) < 0)
+ return -1;
+ rstream->mail_size = UOFF_T_MAX;
+
+ rstream->received_time = rstream->next_received_time;
+ rstream->next_received_time = (time_t)-1;
+
+ i_free(rstream->sender);
+ rstream->sender = rstream->next_sender;
+ rstream->next_sender = NULL;
+
+ rstream->from_offset = rstream->body_offset + body_size;
+ rstream->hdr_offset = rstream->from_offset;
+ rstream->body_offset = UOFF_T_MAX;
+ rstream->header_missing_eoh = FALSE;
+
+ if (stream->v_offset != rstream->from_offset)
+ i_stream_seek_mark(stream, rstream->from_offset);
+ i_stream_seek_mark(rstream->istream.parent, rstream->from_offset);
+
+ rstream->eof = FALSE;
+ rstream->istream.istream.eof = FALSE;
+ return 0;
+}
+
+int istream_raw_mbox_seek(struct istream *stream, uoff_t offset)
+{
+ struct raw_mbox_istream *rstream =
+ (struct raw_mbox_istream *)stream->real_stream;
+ bool check;
+
+ i_assert(rstream->locked);
+
+ /* reset any (corruption) errors */
+ stream->stream_errno = 0;
+ i_free_and_null(stream->real_stream->iostream.error);
+ rstream->corrupted = FALSE;
+ rstream->eof = FALSE;
+ rstream->istream.istream.eof = FALSE;
+
+ /* if seeked is FALSE, we unlocked in the middle. don't try to use
+ any cached state then. */
+ if (rstream->mail_size != UOFF_T_MAX && rstream->seeked &&
+ rstream->hdr_offset + rstream->mail_size == offset)
+ return istream_raw_mbox_next(stream, UOFF_T_MAX);
+
+ if (offset == rstream->from_offset && rstream->seeked) {
+ /* back to beginning of current message */
+ offset = rstream->hdr_offset;
+ check = offset == 0;
+ } else {
+ rstream->body_offset = UOFF_T_MAX;
+ rstream->mail_size = UOFF_T_MAX;
+ rstream->received_time = (time_t)-1;
+ rstream->next_received_time = (time_t)-1;
+ rstream->header_missing_eoh = FALSE;
+
+ i_free(rstream->sender);
+ rstream->sender = NULL;
+ i_free(rstream->next_sender);
+ rstream->next_sender = NULL;
+
+ rstream->from_offset = offset;
+ rstream->hdr_offset = offset;
+ check = TRUE;
+ }
+ rstream->seeked = TRUE;
+
+ i_stream_seek_mark(stream, offset);
+ i_stream_seek_mark(rstream->istream.parent, offset);
+
+ if (check)
+ (void)i_stream_read(stream);
+ return rstream->corrupted ? -1 : 0;
+}
+
+void istream_raw_mbox_set_next_offset(struct istream *stream, uoff_t offset)
+{
+ struct raw_mbox_istream *rstream =
+ (struct raw_mbox_istream *)stream->real_stream;
+
+ i_assert(rstream->hdr_offset != UOFF_T_MAX);
+
+ rstream->mail_size_forced = TRUE;
+ rstream->mail_size = offset - rstream->hdr_offset;
+}
+
+bool istream_raw_mbox_is_eof(struct istream *stream)
+{
+ struct raw_mbox_istream *rstream =
+ (struct raw_mbox_istream *)stream->real_stream;
+
+ return rstream->eof;
+}
+
+bool istream_raw_mbox_is_corrupted(struct istream *stream)
+{
+ struct raw_mbox_istream *rstream =
+ (struct raw_mbox_istream *)stream->real_stream;
+
+ return rstream->corrupted;
+}
+
+void istream_raw_mbox_set_locked(struct istream *stream)
+{
+ struct raw_mbox_istream *rstream =
+ (struct raw_mbox_istream *)stream->real_stream;
+
+ rstream->locked = TRUE;
+}
+
+void istream_raw_mbox_set_unlocked(struct istream *stream)
+{
+ struct raw_mbox_istream *rstream =
+ (struct raw_mbox_istream *)stream->real_stream;
+
+ rstream->locked = FALSE;
+ rstream->seeked = FALSE;
+}
diff --git a/src/lib-storage/index/mbox/istream-raw-mbox.h b/src/lib-storage/index/mbox/istream-raw-mbox.h
new file mode 100644
index 0000000..4543841
--- /dev/null
+++ b/src/lib-storage/index/mbox/istream-raw-mbox.h
@@ -0,0 +1,56 @@
+#ifndef ISTREAM_RAW_MBOX_H
+#define ISTREAM_RAW_MBOX_H
+
+/* Create a mbox stream for parsing mbox. Reading stops before From-line,
+ you'll have to call istream_raw_mbox_next() to get to next message.
+ path is used only for logging purposes. */
+struct istream *i_stream_create_raw_mbox(struct istream *input);
+
+/* Return offset to beginning of the "\nFrom"-line. */
+uoff_t istream_raw_mbox_get_start_offset(struct istream *stream);
+/* Return offset to beginning of the headers. */
+int istream_raw_mbox_get_header_offset(struct istream *stream,
+ uoff_t *hdr_offset_r);
+/* Return offset to beginning of the body. */
+int istream_raw_mbox_get_body_offset(struct istream *stream,
+ uoff_t *body_offset_r);
+
+/* Return the number of bytes in the body of this message. If
+ expected_body_size isn't UOFF_T_MAX, we'll use it as potentially valid body
+ size to avoid actually reading through the whole message. */
+int istream_raw_mbox_get_body_size(struct istream *stream,
+ uoff_t expected_body_size,
+ uoff_t *body_size_r);
+
+/* Return received time of current message, or (time_t)-1 if the timestamp is
+ broken. */
+time_t istream_raw_mbox_get_received_time(struct istream *stream);
+
+/* Return sender of current message. */
+const char *istream_raw_mbox_get_sender(struct istream *stream);
+/* Return TRUE if the empty line between this and the next mail contains CR. */
+bool istream_raw_mbox_has_crlf_ending(struct istream *stream);
+
+/* Jump to next message. If expected_body_size isn't UOFF_T_MAX, we'll use it
+ as potentially valid body size. */
+int istream_raw_mbox_next(struct istream *stream, uoff_t expected_body_size);
+
+/* Seek to message at given offset. offset must point to beginning of
+ "\nFrom ", or 0 for beginning of file. Returns -1 if it offset doesn't
+ contain a valid From-line. */
+int istream_raw_mbox_seek(struct istream *stream, uoff_t offset);
+/* Set next message's start offset. If this isn't set, read stops at the next
+ valid From_-line, even if it belongs to the current message's body
+ (Content-Length: header can be used to determine that). */
+void istream_raw_mbox_set_next_offset(struct istream *stream, uoff_t offset);
+
+/* Returns TRUE if we've read the whole mbox. */
+bool istream_raw_mbox_is_eof(struct istream *stream);
+/* Returns TRUE if we've noticed corruption in used offsets/sizes. */
+bool istream_raw_mbox_is_corrupted(struct istream *stream);
+/* Change stream's locking state. We'll assert-crash if stream is tried to be
+ read while it's unlocked. */
+void istream_raw_mbox_set_locked(struct istream *stream);
+void istream_raw_mbox_set_unlocked(struct istream *stream);
+
+#endif
diff --git a/src/lib-storage/index/mbox/mbox-file.c b/src/lib-storage/index/mbox/mbox-file.c
new file mode 100644
index 0000000..b71abb8
--- /dev/null
+++ b/src/lib-storage/index/mbox/mbox-file.c
@@ -0,0 +1,207 @@
+/* Copyright (c) 2002-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "istream.h"
+#include "mbox-storage.h"
+#include "mbox-sync-private.h"
+#include "mbox-file.h"
+#include "istream-raw-mbox.h"
+
+#include <sys/stat.h>
+#include <utime.h>
+
+#define MBOX_READ_BLOCK_SIZE IO_BLOCK_SIZE
+
+int mbox_file_open(struct mbox_mailbox *mbox)
+{
+ struct stat st;
+ int fd;
+
+ i_assert(mbox->mbox_fd == -1);
+
+ if (mbox->mbox_file_stream != NULL) {
+ /* read-only mbox stream */
+ i_assert(mbox_is_backend_readonly(mbox));
+ return 0;
+ }
+
+ fd = open(mailbox_get_path(&mbox->box),
+ mbox_is_backend_readonly(mbox) ? O_RDONLY : O_RDWR);
+ if (fd == -1 && errno == EACCES && !mbox->backend_readonly) {
+ mbox->backend_readonly = TRUE;
+ fd = open(mailbox_get_path(&mbox->box), O_RDONLY);
+ }
+
+ if (fd == -1) {
+ mbox_set_syscall_error(mbox, "open()");
+ return -1;
+ }
+
+ if (fstat(fd, &st) < 0) {
+ mbox_set_syscall_error(mbox, "fstat()");
+ i_close_fd(&fd);
+ return -1;
+ }
+
+ mbox->mbox_writeonly = S_ISFIFO(st.st_mode);
+ mbox->mbox_fd = fd;
+ mbox->mbox_dev = st.st_dev;
+ mbox->mbox_ino = st.st_ino;
+ return 0;
+}
+
+void mbox_file_close(struct mbox_mailbox *mbox)
+{
+ mbox_file_close_stream(mbox);
+
+ if (mbox->mbox_fd != -1) {
+ if (close(mbox->mbox_fd) < 0)
+ mbox_set_syscall_error(mbox, "close()");
+ mbox->mbox_fd = -1;
+ }
+}
+
+int mbox_file_open_stream(struct mbox_mailbox *mbox)
+{
+ if (mbox->mbox_stream != NULL)
+ return 0;
+
+ if (mbox->mbox_file_stream != NULL) {
+ /* read-only mbox stream */
+ i_assert(mbox->mbox_fd == -1 && mbox_is_backend_readonly(mbox));
+ } else {
+ if (mbox->mbox_fd == -1) {
+ if (mbox_file_open(mbox) < 0)
+ return -1;
+ }
+
+ if (mbox->mbox_writeonly) {
+ mbox->mbox_file_stream =
+ i_stream_create_from_data("", 0);
+ } else {
+ mbox->mbox_file_stream =
+ i_stream_create_fd(mbox->mbox_fd,
+ MBOX_READ_BLOCK_SIZE);
+ i_stream_set_init_buffer_size(mbox->mbox_file_stream,
+ MBOX_READ_BLOCK_SIZE);
+ }
+ i_stream_set_name(mbox->mbox_file_stream,
+ mailbox_get_path(&mbox->box));
+ }
+
+ mbox->mbox_stream = i_stream_create_raw_mbox(mbox->mbox_file_stream);
+ if (mbox->mbox_lock_type != F_UNLCK)
+ istream_raw_mbox_set_locked(mbox->mbox_stream);
+ return 0;
+}
+
+static void mbox_file_fix_atime(struct mbox_mailbox *mbox)
+{
+ struct utimbuf buf;
+ struct stat st;
+
+ if (mbox->box.recent_flags_count > 0 &&
+ (mbox->box.flags & MAILBOX_FLAG_DROP_RECENT) == 0 &&
+ mbox->mbox_fd != -1 && !mbox_is_backend_readonly(mbox)) {
+ /* we've seen recent messages which we want to keep recent.
+ keep file's atime lower than mtime so \Marked status
+ gets shown while listing */
+ if (fstat(mbox->mbox_fd, &st) < 0) {
+ mbox_set_syscall_error(mbox, "fstat()");
+ return;
+ }
+ if (st.st_atime >= st.st_mtime) {
+ buf.modtime = st.st_mtime;
+ buf.actime = buf.modtime - 1;
+ /* EPERM can happen with shared mailboxes */
+ if (utime(mailbox_get_path(&mbox->box), &buf) < 0 &&
+ errno != EPERM)
+ mbox_set_syscall_error(mbox, "utime()");
+ }
+ }
+}
+void mbox_file_close_stream(struct mbox_mailbox *mbox)
+{
+ /* if we read anything, fix the atime if needed */
+ mbox_file_fix_atime(mbox);
+
+ i_stream_destroy(&mbox->mbox_stream);
+
+ if (mbox->mbox_file_stream != NULL) {
+ if (mbox->mbox_fd == -1) {
+ /* read-only mbox stream */
+ i_assert(mbox_is_backend_readonly(mbox));
+ i_stream_seek(mbox->mbox_file_stream, 0);
+ } else {
+ i_stream_destroy(&mbox->mbox_file_stream);
+ }
+ }
+}
+
+int mbox_file_lookup_offset(struct mbox_mailbox *mbox,
+ struct mail_index_view *view,
+ uint32_t seq, uoff_t *offset_r)
+{
+ const void *data;
+ bool deleted;
+
+ mail_index_lookup_ext(view, seq, mbox->mbox_ext_idx, &data, &deleted);
+ if (deleted)
+ return -1;
+
+ if (data == NULL) {
+ mailbox_set_critical(&mbox->box,
+ "Cached message offset lost for seq %u in mbox", seq);
+ mbox->mbox_hdr.dirty_flag = 1;
+ mbox->mbox_broken_offsets = TRUE;
+ return 0;
+ }
+
+ *offset_r = *((const uint64_t *)data);
+ return 1;
+}
+
+int mbox_file_seek(struct mbox_mailbox *mbox, struct mail_index_view *view,
+ uint32_t seq, bool *deleted_r)
+{
+ uoff_t offset;
+ int ret;
+
+ ret = mbox_file_lookup_offset(mbox, view, seq, &offset);
+ if (ret <= 0) {
+ *deleted_r = ret < 0;
+ return ret;
+ }
+ *deleted_r = FALSE;
+
+ if (istream_raw_mbox_seek(mbox->mbox_stream, offset) < 0) {
+ if (offset == 0) {
+ mbox->invalid_mbox_file = TRUE;
+ mail_storage_set_error(&mbox->storage->storage,
+ MAIL_ERROR_NOTPOSSIBLE,
+ "Mailbox isn't a valid mbox file");
+ return -1;
+ }
+
+ if (mbox->mbox_hdr.dirty_flag != 0)
+ return 0;
+
+ mailbox_set_critical(&mbox->box,
+ "Cached message offset %s is invalid for mbox",
+ dec2str(offset));
+ mbox->mbox_hdr.dirty_flag = 1;
+ mbox->mbox_broken_offsets = TRUE;
+ return 0;
+ }
+
+ if (mbox->mbox_hdr.dirty_flag != 0) {
+ /* we're dirty - make sure this is the correct mail */
+ if (!mbox_sync_parse_match_mail(mbox, view, seq))
+ return 0;
+
+ ret = istream_raw_mbox_seek(mbox->mbox_stream, offset);
+ i_assert(ret == 0);
+ }
+
+ return 1;
+}
diff --git a/src/lib-storage/index/mbox/mbox-file.h b/src/lib-storage/index/mbox/mbox-file.h
new file mode 100644
index 0000000..529a2e0
--- /dev/null
+++ b/src/lib-storage/index/mbox/mbox-file.h
@@ -0,0 +1,16 @@
+#ifndef MBOX_FILE_H
+#define MBOX_FILE_H
+
+int mbox_file_open(struct mbox_mailbox *mbox);
+void mbox_file_close(struct mbox_mailbox *mbox);
+
+int mbox_file_open_stream(struct mbox_mailbox *mbox);
+void mbox_file_close_stream(struct mbox_mailbox *mbox);
+
+int mbox_file_lookup_offset(struct mbox_mailbox *mbox,
+ struct mail_index_view *view,
+ uint32_t seq, uoff_t *offset_r);
+int mbox_file_seek(struct mbox_mailbox *mbox, struct mail_index_view *view,
+ uint32_t seq, bool *deleted_r);
+
+#endif
diff --git a/src/lib-storage/index/mbox/mbox-lock.c b/src/lib-storage/index/mbox/mbox-lock.c
new file mode 100644
index 0000000..cebff48
--- /dev/null
+++ b/src/lib-storage/index/mbox/mbox-lock.c
@@ -0,0 +1,900 @@
+/* Copyright (c) 2002-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "eacces-error.h"
+#include "restrict-access.h"
+#include "nfs-workarounds.h"
+#include "ipwd.h"
+#include "mail-index-private.h"
+#include "mbox-storage.h"
+#include "istream-raw-mbox.h"
+#include "mbox-file.h"
+#include "mbox-lock.h"
+
+#include <time.h>
+#include <unistd.h>
+#include <fcntl.h>
+#include <sys/stat.h>
+
+#ifdef HAVE_FLOCK
+# include <sys/file.h>
+#endif
+
+/* 0.1 .. 0.2msec */
+#define LOCK_RANDOM_USLEEP_TIME (100000 + (unsigned int)i_rand() % 100000)
+
+enum mbox_lock_type {
+ MBOX_LOCK_DOTLOCK,
+ MBOX_LOCK_DOTLOCK_TRY,
+ MBOX_LOCK_FCNTL,
+ MBOX_LOCK_FLOCK,
+ MBOX_LOCK_LOCKF,
+
+ MBOX_LOCK_COUNT
+};
+
+enum mbox_dotlock_op {
+ MBOX_DOTLOCK_OP_LOCK,
+ MBOX_DOTLOCK_OP_UNLOCK,
+ MBOX_DOTLOCK_OP_TOUCH
+};
+
+struct mbox_lock_context {
+ struct mbox_mailbox *mbox;
+ bool locked_status[MBOX_LOCK_COUNT];
+ bool checked_file;
+
+ int lock_type;
+ bool dotlock_last_stale;
+ bool fcntl_locked;
+ bool using_privileges;
+};
+
+struct mbox_lock_data {
+ enum mbox_lock_type type;
+ const char *name;
+ int (*func)(struct mbox_lock_context *ctx, int lock_type,
+ time_t max_wait_time);
+};
+
+static int mbox_lock_dotlock(struct mbox_lock_context *ctx, int lock_type,
+ time_t max_wait_time);
+static int mbox_lock_dotlock_try(struct mbox_lock_context *ctx, int lock_type,
+ time_t max_wait_time);
+static int mbox_lock_fcntl(struct mbox_lock_context *ctx, int lock_type,
+ time_t max_wait_time);
+#ifdef HAVE_FLOCK
+static int mbox_lock_flock(struct mbox_lock_context *ctx, int lock_type,
+ time_t max_wait_time);
+#else
+# define mbox_lock_flock NULL
+#endif
+#ifdef HAVE_LOCKF
+static int mbox_lock_lockf(struct mbox_lock_context *ctx, int lock_type,
+ time_t max_wait_time);
+#else
+# define mbox_lock_lockf NULL
+#endif
+
+static struct mbox_lock_data lock_data[] = {
+ { MBOX_LOCK_DOTLOCK, "dotlock", mbox_lock_dotlock },
+ { MBOX_LOCK_DOTLOCK_TRY, "dotlock_try", mbox_lock_dotlock_try },
+ { MBOX_LOCK_FCNTL, "fcntl", mbox_lock_fcntl },
+ { MBOX_LOCK_FLOCK, "flock", mbox_lock_flock },
+ { MBOX_LOCK_LOCKF, "lockf", mbox_lock_lockf },
+ { 0, NULL, NULL }
+};
+
+static int ATTR_NOWARN_UNUSED_RESULT
+mbox_lock_list(struct mbox_lock_context *ctx, int lock_type,
+ time_t max_wait_time, int idx);
+static int ATTR_NOWARN_UNUSED_RESULT
+mbox_unlock_files(struct mbox_lock_context *ctx);
+
+static void mbox_read_lock_methods(const char *str, const char *env,
+ enum mbox_lock_type *locks)
+{
+ enum mbox_lock_type type;
+ const char *const *lock;
+ int i, dest;
+
+ for (lock = t_strsplit(str, " "), dest = 0; *lock != NULL; lock++) {
+ for (type = 0; lock_data[type].name != NULL; type++) {
+ if (strcasecmp(*lock, lock_data[type].name) == 0) {
+ type = lock_data[type].type;
+ break;
+ }
+ }
+ if (lock_data[type].name == NULL)
+ i_fatal("%s: Invalid value %s", env, *lock);
+ if (lock_data[type].func == NULL) {
+ i_fatal("%s: Support for lock type %s "
+ "not compiled into binary", env, *lock);
+ }
+
+ for (i = 0; i < dest; i++) {
+ if (locks[i] == type)
+ i_fatal("%s: Duplicated value %s", env, *lock);
+ }
+
+ /* @UNSAFE */
+ locks[dest++] = type;
+ }
+ locks[dest] = (enum mbox_lock_type)-1;
+}
+
+static void mbox_init_lock_settings(struct mbox_storage *storage)
+{
+ enum mbox_lock_type read_locks[MBOX_LOCK_COUNT+1];
+ enum mbox_lock_type write_locks[MBOX_LOCK_COUNT+1];
+ int r, w;
+
+ mbox_read_lock_methods(storage->set->mbox_read_locks,
+ "mbox_read_locks", read_locks);
+ mbox_read_lock_methods(storage->set->mbox_write_locks,
+ "mbox_write_locks", write_locks);
+
+ /* check that read/write list orders match. write_locks must contain
+ at least read_locks and possibly more. */
+ for (r = w = 0; write_locks[w] != (enum mbox_lock_type)-1; w++) {
+ if (read_locks[r] == (enum mbox_lock_type)-1)
+ break;
+ if (read_locks[r] == write_locks[w])
+ r++;
+ }
+ if (read_locks[r] != (enum mbox_lock_type)-1) {
+ i_fatal("mbox read/write lock list settings are invalid. "
+ "Lock ordering must be the same with both, "
+ "and write locks must contain all read locks "
+ "(and possibly more)");
+ }
+
+ storage->read_locks = p_new(storage->storage.pool,
+ enum mbox_lock_type, MBOX_LOCK_COUNT+1);
+ memcpy(storage->read_locks, read_locks,
+ sizeof(*storage->read_locks) * (MBOX_LOCK_COUNT+1));
+
+ storage->write_locks = p_new(storage->storage.pool,
+ enum mbox_lock_type, MBOX_LOCK_COUNT+1);
+ memcpy(storage->write_locks, write_locks,
+ sizeof(*storage->write_locks) * (MBOX_LOCK_COUNT+1));
+
+ storage->lock_settings_initialized = TRUE;
+}
+
+static int mbox_file_open_latest(struct mbox_lock_context *ctx, int lock_type)
+{
+ struct mbox_mailbox *mbox = ctx->mbox;
+ struct stat st;
+
+ if (ctx->checked_file || lock_type == F_UNLCK)
+ return 0;
+
+ if (mbox->mbox_fd != -1) {
+ /* we could flush NFS file handle cache here if we wanted to
+ be sure that the file is latest, but mbox files get rarely
+ deleted and the flushing might cause errors (e.g. EBUSY for
+ trying to flush a /var/mail mountpoint) */
+ if (nfs_safe_stat(mailbox_get_path(&mbox->box), &st) < 0) {
+ if (errno == ENOENT)
+ mailbox_set_deleted(&mbox->box);
+ else
+ mbox_set_syscall_error(mbox, "stat()");
+ return -1;
+ }
+
+ if (st.st_ino != mbox->mbox_ino ||
+ !CMP_DEV_T(st.st_dev, mbox->mbox_dev))
+ mbox_file_close(mbox);
+ }
+
+ if (mbox->mbox_fd == -1) {
+ if (mbox_file_open(mbox) < 0)
+ return -1;
+ }
+
+ ctx->checked_file = TRUE;
+ return 0;
+}
+
+static bool dotlock_callback(unsigned int secs_left, bool stale, void *context)
+{
+ struct mbox_lock_context *ctx = context;
+ enum mbox_lock_type *lock_types;
+ int i;
+
+ if (ctx->using_privileges)
+ restrict_access_drop_priv_gid();
+
+ if (stale && !ctx->dotlock_last_stale) {
+ /* get next index we wish to try locking. it's the one after
+ dotlocking. */
+ lock_types = ctx->lock_type == F_WRLCK ||
+ (ctx->lock_type == F_UNLCK &&
+ ctx->mbox->mbox_lock_type == F_WRLCK) ?
+ ctx->mbox->storage->write_locks :
+ ctx->mbox->storage->read_locks;
+
+ for (i = 0; lock_types[i] != (enum mbox_lock_type)-1; i++) {
+ if (lock_types[i] == MBOX_LOCK_DOTLOCK)
+ break;
+ }
+
+ if (lock_types[i] != (enum mbox_lock_type)-1 &&
+ lock_types[i+1] != (enum mbox_lock_type)-1) {
+ i++;
+ if (mbox_lock_list(ctx, ctx->lock_type, 0, i) <= 0) {
+ /* we couldn't get fd lock -
+ it's really locked */
+ ctx->dotlock_last_stale = TRUE;
+ return FALSE;
+ }
+ mbox_lock_list(ctx, F_UNLCK, 0, i);
+ }
+ }
+ ctx->dotlock_last_stale = stale;
+
+ index_storage_lock_notify(&ctx->mbox->box, stale ?
+ MAILBOX_LOCK_NOTIFY_MAILBOX_OVERRIDE :
+ MAILBOX_LOCK_NOTIFY_MAILBOX_ABORT,
+ secs_left);
+ if (ctx->using_privileges) {
+ if (restrict_access_use_priv_gid() < 0) {
+ /* shouldn't get here */
+ return FALSE;
+ }
+ }
+ return TRUE;
+}
+
+static int ATTR_NULL(2) ATTR_NOWARN_UNUSED_RESULT
+mbox_dotlock_privileged_op(struct mbox_mailbox *mbox,
+ struct dotlock_settings *set,
+ enum mbox_dotlock_op op)
+{
+ const char *box_path, *dir, *fname;
+ int ret = -1, orig_dir_fd, orig_errno;
+
+ orig_dir_fd = open(".", O_RDONLY);
+ if (orig_dir_fd == -1) {
+ mailbox_set_critical(&mbox->box, "open(.) failed: %m");
+ return -1;
+ }
+
+ /* allow dotlocks to be created only for files we can read while we're
+ unprivileged. to make sure there are no race conditions we first
+ have to chdir to the mbox file's directory and then use relative
+ paths. unless this is done, users could:
+ - create *.lock files to any directory writable by the
+ privileged group
+ - DoS other users by dotlocking their mailboxes infinitely
+ */
+ box_path = mailbox_get_path(&mbox->box);
+ fname = strrchr(box_path, '/');
+ if (fname == NULL) {
+ /* already relative */
+ fname = box_path;
+ } else {
+ dir = t_strdup_until(box_path, fname);
+ if (chdir(dir) < 0) {
+ mailbox_set_critical(&mbox->box,
+ "chdir(%s) failed: %m", dir);
+ i_close_fd(&orig_dir_fd);
+ return -1;
+ }
+ fname++;
+ }
+ if (op == MBOX_DOTLOCK_OP_LOCK) {
+ if (access(fname, R_OK) < 0) {
+ mailbox_set_critical(&mbox->box,
+ "access(%s) failed: %m", box_path);
+ i_close_fd(&orig_dir_fd);
+ return -1;
+ }
+ }
+
+ if (restrict_access_use_priv_gid() < 0) {
+ i_close_fd(&orig_dir_fd);
+ return -1;
+ }
+
+ switch (op) {
+ case MBOX_DOTLOCK_OP_LOCK:
+ /* we're now privileged - avoid doing as much as possible */
+ ret = file_dotlock_create(set, fname, 0, &mbox->mbox_dotlock);
+ if (ret > 0)
+ mbox->mbox_used_privileges = TRUE;
+ else if (ret < 0 && errno == EACCES) {
+ const char *errmsg =
+ eacces_error_get_creating("file_dotlock_create",
+ fname);
+ mailbox_set_critical(&mbox->box, "%s", errmsg);
+ } else {
+ mbox_set_syscall_error(mbox, "file_dotlock_create()");
+ }
+ break;
+ case MBOX_DOTLOCK_OP_UNLOCK:
+ /* we're now privileged - avoid doing as much as possible */
+ ret = file_dotlock_delete(&mbox->mbox_dotlock);
+ if (ret < 0)
+ mbox_set_syscall_error(mbox, "file_dotlock_delete()");
+ mbox->mbox_used_privileges = FALSE;
+ break;
+ case MBOX_DOTLOCK_OP_TOUCH:
+ ret = file_dotlock_touch(mbox->mbox_dotlock);
+ if (ret < 0)
+ mbox_set_syscall_error(mbox, "file_dotlock_touch()");
+ break;
+ }
+
+ orig_errno = errno;
+ restrict_access_drop_priv_gid();
+
+ if (fchdir(orig_dir_fd) < 0) {
+ mailbox_set_critical(&mbox->box, "fchdir() failed: %m");
+ }
+ i_close_fd(&orig_dir_fd);
+ errno = orig_errno;
+ return ret;
+}
+
+static void
+mbox_dotlock_log_eacces_error(struct mbox_mailbox *mbox, const char *path)
+{
+ const char *dir, *errmsg, *name;
+ struct stat st;
+ struct group group;
+ int orig_errno = errno;
+
+ errmsg = eacces_error_get_creating("file_dotlock_create", path);
+ dir = strrchr(path, '/');
+ dir = dir == NULL ? "." : t_strdup_until(path, dir);
+ /* allow privileged locking for
+ a) user's own INBOX,
+ b) another user's shared INBOX, and
+ c) anything called INBOX (in inbox=no namespace) */
+ if (!mbox->box.inbox_any && strcmp(mbox->box.name, "INBOX") != 0) {
+ mailbox_set_critical(&mbox->box,
+ "%s (not INBOX -> no privileged locking)", errmsg);
+ } else if (!mbox->mbox_privileged_locking) {
+ dir = mailbox_list_get_root_forced(mbox->box.list,
+ MAILBOX_LIST_PATH_TYPE_DIR);
+ mailbox_set_critical(&mbox->box,
+ "%s (under root dir %s -> no privileged locking)",
+ errmsg, dir);
+ } else if (stat(dir, &st) == 0 &&
+ (st.st_mode & 02) == 0 && /* not world-writable */
+ (st.st_mode & 020) != 0) { /* group-writable */
+ if (i_getgrgid(st.st_gid, &group) <= 0)
+ name = dec2str(st.st_gid);
+ else
+ name = group.gr_name;
+ mailbox_set_critical(&mbox->box,
+ "%s (set mail_privileged_group=%s)", errmsg, name);
+ } else {
+ mailbox_set_critical(&mbox->box,
+ "%s (nonstandard permissions in %s)", errmsg, dir);
+ }
+ errno = orig_errno;
+}
+
+static int
+mbox_lock_dotlock_int(struct mbox_lock_context *ctx, int lock_type, bool try)
+{
+ struct mbox_mailbox *mbox = ctx->mbox;
+ struct dotlock_settings set;
+ int ret;
+
+ if (lock_type == F_UNLCK) {
+ if (!mbox->mbox_dotlocked)
+ return 1;
+
+ if (!mbox->mbox_used_privileges) {
+ if (file_dotlock_delete(&mbox->mbox_dotlock) <= 0) {
+ mbox_set_syscall_error(mbox,
+ "file_dotlock_delete()");
+ }
+ } else {
+ ctx->using_privileges = TRUE;
+ mbox_dotlock_privileged_op(mbox, NULL,
+ MBOX_DOTLOCK_OP_UNLOCK);
+ ctx->using_privileges = FALSE;
+ }
+ mbox->mbox_dotlocked = FALSE;
+ return 1;
+ }
+
+ if (mbox->mbox_dotlocked)
+ return 1;
+
+ ctx->dotlock_last_stale = TRUE;
+
+ i_zero(&set);
+ set.use_excl_lock = mbox->storage->storage.set->dotlock_use_excl;
+ set.nfs_flush = mbox->storage->storage.set->mail_nfs_storage;
+ set.timeout = mail_storage_get_lock_timeout(&mbox->storage->storage,
+ mbox->storage->set->mbox_lock_timeout);
+ set.stale_timeout = mbox->storage->set->mbox_dotlock_change_timeout;
+ set.callback = dotlock_callback;
+ set.context = ctx;
+
+ ret = file_dotlock_create(&set, mailbox_get_path(&mbox->box), 0,
+ &mbox->mbox_dotlock);
+ if (ret >= 0) {
+ /* success / timeout */
+ } else if (errno == EACCES && restrict_access_have_priv_gid() &&
+ mbox->mbox_privileged_locking) {
+ /* try again, this time with extra privileges */
+ ret = mbox_dotlock_privileged_op(mbox, &set,
+ MBOX_DOTLOCK_OP_LOCK);
+ } else if (errno == EACCES)
+ mbox_dotlock_log_eacces_error(mbox, mailbox_get_path(&mbox->box));
+ else
+ mbox_set_syscall_error(mbox, "file_dotlock_create()");
+
+ if (ret < 0) {
+ if ((ENOSPACE(errno) || errno == EACCES) && try)
+ return 1;
+ return -1;
+ }
+ if (ret == 0) {
+ mail_storage_set_error(&mbox->storage->storage,
+ MAIL_ERROR_TEMP, MAIL_ERRSTR_LOCK_TIMEOUT);
+ return 0;
+ }
+ mbox->mbox_dotlocked = TRUE;
+
+ if (mbox_file_open_latest(ctx, lock_type) < 0)
+ return -1;
+ return 1;
+}
+
+static int mbox_lock_dotlock(struct mbox_lock_context *ctx, int lock_type,
+ time_t max_wait_time ATTR_UNUSED)
+{
+ return mbox_lock_dotlock_int(ctx, lock_type, FALSE);
+}
+
+static int mbox_lock_dotlock_try(struct mbox_lock_context *ctx, int lock_type,
+ time_t max_wait_time ATTR_UNUSED)
+{
+ return mbox_lock_dotlock_int(ctx, lock_type, TRUE);
+}
+
+#ifdef HAVE_FLOCK
+static int mbox_lock_flock(struct mbox_lock_context *ctx, int lock_type,
+ time_t max_wait_time)
+{
+ time_t now;
+ unsigned int next_alarm;
+
+ if (mbox_file_open_latest(ctx, lock_type) < 0)
+ return -1;
+
+ if (lock_type == F_UNLCK && ctx->mbox->mbox_fd == -1)
+ return 1;
+
+ if (lock_type == F_WRLCK)
+ lock_type = LOCK_EX;
+ else if (lock_type == F_RDLCK)
+ lock_type = LOCK_SH;
+ else
+ lock_type = LOCK_UN;
+
+ if (max_wait_time == 0) {
+ /* usually we're waiting here, but if we came from
+ mbox_lock_dotlock(), we just want to try locking */
+ lock_type |= LOCK_NB;
+ } else {
+ now = time(NULL);
+ if (now >= max_wait_time)
+ alarm(1);
+ else
+ alarm(I_MIN(max_wait_time - now, 5));
+ }
+
+ while (flock(ctx->mbox->mbox_fd, lock_type) < 0) {
+ if (errno != EINTR) {
+ if (errno == EWOULDBLOCK && max_wait_time == 0) {
+ /* non-blocking lock trying failed */
+ return 0;
+ }
+ alarm(0);
+ mbox_set_syscall_error(ctx->mbox, "flock()");
+ return -1;
+ }
+
+ now = time(NULL);
+ if (now >= max_wait_time) {
+ alarm(0);
+ return 0;
+ }
+
+ /* notify locks once every 5 seconds.
+ try to use rounded values. */
+ next_alarm = (max_wait_time - now) % 5;
+ if (next_alarm == 0)
+ next_alarm = 5;
+ alarm(next_alarm);
+
+ index_storage_lock_notify(&ctx->mbox->box,
+ MAILBOX_LOCK_NOTIFY_MAILBOX_ABORT,
+ max_wait_time - now);
+ }
+
+ alarm(0);
+ return 1;
+}
+#endif
+
+#ifdef HAVE_LOCKF
+static int mbox_lock_lockf(struct mbox_lock_context *ctx, int lock_type,
+ time_t max_wait_time)
+{
+ time_t now;
+ unsigned int next_alarm;
+
+ if (mbox_file_open_latest(ctx, lock_type) < 0)
+ return -1;
+
+ if (lock_type == F_UNLCK && ctx->mbox->mbox_fd == -1)
+ return 1;
+
+ if (lock_type == F_UNLCK)
+ lock_type = F_ULOCK;
+ else if (max_wait_time == 0) {
+ /* usually we're waiting here, but if we came from
+ mbox_lock_dotlock(), we just want to try locking */
+ lock_type = F_TLOCK;
+ } else {
+ now = time(NULL);
+ if (now >= max_wait_time)
+ alarm(1);
+ else
+ alarm(I_MIN(max_wait_time - now, 5));
+ lock_type = F_LOCK;
+ }
+
+ while (lockf(ctx->mbox->mbox_fd, lock_type, 0) < 0) {
+ if (errno != EINTR) {
+ if ((errno == EACCES || errno == EAGAIN) &&
+ max_wait_time == 0) {
+ /* non-blocking lock trying failed */
+ return 0;
+ }
+ alarm(0);
+ mbox_set_syscall_error(ctx->mbox, "lockf()");
+ return -1;
+ }
+
+ now = time(NULL);
+ if (now >= max_wait_time) {
+ alarm(0);
+ return 0;
+ }
+
+ /* notify locks once every 5 seconds.
+ try to use rounded values. */
+ next_alarm = (max_wait_time - now) % 5;
+ if (next_alarm == 0)
+ next_alarm = 5;
+ alarm(next_alarm);
+
+ index_storage_lock_notify(&ctx->mbox->box,
+ MAILBOX_LOCK_NOTIFY_MAILBOX_ABORT,
+ max_wait_time - now);
+ }
+
+ alarm(0);
+ return 1;
+}
+#endif
+
+static int mbox_lock_fcntl(struct mbox_lock_context *ctx, int lock_type,
+ time_t max_wait_time)
+{
+ struct flock fl;
+ time_t now;
+ unsigned int next_alarm;
+ int wait_type;
+
+ if (mbox_file_open_latest(ctx, lock_type) < 0)
+ return -1;
+
+ if (lock_type == F_UNLCK && ctx->mbox->mbox_fd == -1)
+ return 1;
+
+ i_zero(&fl);
+ fl.l_type = lock_type;
+ fl.l_whence = SEEK_SET;
+ fl.l_start = 0;
+ fl.l_len = 0;
+
+ if (max_wait_time == 0) {
+ /* usually we're waiting here, but if we came from
+ mbox_lock_dotlock(), we just want to try locking */
+ wait_type = F_SETLK;
+ } else {
+ wait_type = F_SETLKW;
+ now = time(NULL);
+ if (now >= max_wait_time)
+ alarm(1);
+ else
+ alarm(I_MIN(max_wait_time - now, 5));
+ }
+
+ while (fcntl(ctx->mbox->mbox_fd, wait_type, &fl) < 0) {
+ if (errno != EINTR) {
+ if ((errno == EACCES || errno == EAGAIN) &&
+ wait_type == F_SETLK) {
+ /* non-blocking lock trying failed */
+ return 0;
+ }
+ alarm(0);
+ if (errno != EACCES) {
+ mbox_set_syscall_error(ctx->mbox, "fcntl()");
+ return -1;
+ }
+ mailbox_set_critical(&ctx->mbox->box,
+ "fcntl() failed with mbox file %s: "
+ "File is locked by another process (EACCES)",
+ mailbox_get_path(&ctx->mbox->box));
+ return -1;
+ }
+
+ now = time(NULL);
+ if (now >= max_wait_time) {
+ alarm(0);
+ return 0;
+ }
+
+ /* notify locks once every 5 seconds.
+ try to use rounded values. */
+ next_alarm = (max_wait_time - now) % 5;
+ if (next_alarm == 0)
+ next_alarm = 5;
+ alarm(next_alarm);
+
+ index_storage_lock_notify(&ctx->mbox->box,
+ MAILBOX_LOCK_NOTIFY_MAILBOX_ABORT,
+ max_wait_time - now);
+ }
+
+ alarm(0);
+ ctx->fcntl_locked = TRUE;
+ return 1;
+}
+
+static int ATTR_NOWARN_UNUSED_RESULT
+mbox_lock_list(struct mbox_lock_context *ctx, int lock_type,
+ time_t max_wait_time, int idx)
+{
+ enum mbox_lock_type *lock_types;
+ enum mbox_lock_type type;
+ int i, ret = 0;
+ bool locked_status;
+
+ ctx->lock_type = lock_type;
+
+ lock_types = lock_type == F_WRLCK ||
+ (lock_type == F_UNLCK && ctx->mbox->mbox_lock_type == F_WRLCK) ?
+ ctx->mbox->storage->write_locks :
+ ctx->mbox->storage->read_locks;
+ for (i = idx; lock_types[i] != (enum mbox_lock_type)-1; i++) {
+ type = lock_types[i];
+ locked_status = lock_type != F_UNLCK;
+
+ if (ctx->locked_status[type] == locked_status)
+ continue;
+ ctx->locked_status[type] = locked_status;
+
+ ret = lock_data[type].func(ctx, lock_type, max_wait_time);
+ if (ret <= 0)
+ break;
+ }
+ return ret;
+}
+
+static int mbox_update_locking(struct mbox_mailbox *mbox, int lock_type,
+ bool *fcntl_locked_r)
+{
+ struct mbox_lock_context ctx;
+ time_t max_wait_time;
+ int ret, i;
+ bool drop_locks;
+
+ *fcntl_locked_r = FALSE;
+
+ index_storage_lock_notify_reset(&mbox->box);
+
+ if (!mbox->storage->lock_settings_initialized)
+ mbox_init_lock_settings(mbox->storage);
+
+ if (mbox->mbox_fd == -1 && mbox->mbox_file_stream != NULL) {
+ /* read-only mbox stream. no need to lock. */
+ i_assert(mbox_is_backend_readonly(mbox));
+ mbox->mbox_lock_type = lock_type;
+ return 1;
+ }
+
+ max_wait_time = time(NULL) +
+ mail_storage_get_lock_timeout(&mbox->storage->storage,
+ mbox->storage->set->mbox_lock_timeout);
+
+ i_zero(&ctx);
+ ctx.mbox = mbox;
+
+ if (mbox->mbox_lock_type == F_WRLCK) {
+ /* dropping to shared lock. first drop those that we
+ don't remove completely. */
+ const enum mbox_lock_type *read_locks =
+ mbox->storage->read_locks;
+
+ for (i = 0; i < MBOX_LOCK_COUNT; i++)
+ ctx.locked_status[i] = TRUE;
+ for (i = 0; read_locks[i] != (enum mbox_lock_type)-1; i++)
+ ctx.locked_status[read_locks[i]] = FALSE;
+ drop_locks = TRUE;
+ } else {
+ drop_locks = FALSE;
+ }
+
+ mbox->mbox_lock_type = lock_type;
+ ret = mbox_lock_list(&ctx, lock_type, max_wait_time, 0);
+ if (ret <= 0) {
+ if (!drop_locks)
+ mbox_unlock_files(&ctx);
+ if (ret == 0) {
+ mail_storage_set_error(&mbox->storage->storage,
+ MAIL_ERROR_TEMP, MAIL_ERRSTR_LOCK_TIMEOUT);
+ }
+ return ret;
+ }
+
+ if (drop_locks) {
+ /* dropping to shared lock: drop the locks that are only
+ in write list */
+ const enum mbox_lock_type *read_locks =
+ mbox->storage->read_locks;
+ const enum mbox_lock_type *write_locks =
+ mbox->storage->write_locks;
+
+ memset(ctx.locked_status, 0, sizeof(ctx.locked_status));
+ for (i = 0; write_locks[i] != (enum mbox_lock_type)-1; i++)
+ ctx.locked_status[write_locks[i]] = TRUE;
+ for (i = 0; read_locks[i] != (enum mbox_lock_type)-1; i++)
+ ctx.locked_status[read_locks[i]] = FALSE;
+
+ mbox->mbox_lock_type = F_WRLCK;
+ mbox_lock_list(&ctx, F_UNLCK, 0, 0);
+ mbox->mbox_lock_type = F_RDLCK;
+ }
+
+ *fcntl_locked_r = ctx.fcntl_locked;
+ return 1;
+}
+
+int mbox_lock(struct mbox_mailbox *mbox, int lock_type,
+ unsigned int *lock_id_r)
+{
+ const char *path = mailbox_get_path(&mbox->box);
+ int mbox_fd = mbox->mbox_fd;
+ bool fcntl_locked;
+ int ret;
+
+ if (lock_type == F_RDLCK && mbox->external_transactions > 0 &&
+ mbox->mbox_lock_type != F_RDLCK) {
+ /* we have a transaction open that is going to save mails
+ and apparently also wants to read from the same mailbox
+ (copy, move, catenate). we need to write lock the mailbox,
+ since we can't later upgrade a read lock to write lock. */
+ lock_type = F_WRLCK;
+ }
+
+ /* allow only unlock -> shared/exclusive or exclusive -> shared */
+ i_assert(lock_type == F_RDLCK || lock_type == F_WRLCK);
+ i_assert(lock_type == F_RDLCK || mbox->mbox_lock_type != F_RDLCK);
+
+ if (mbox->mbox_lock_type == F_UNLCK) {
+ ret = mbox_update_locking(mbox, lock_type, &fcntl_locked);
+ if (ret <= 0)
+ return ret;
+
+ if (mbox->storage->storage.set->mail_nfs_storage) {
+ if (fcntl_locked) {
+ nfs_flush_attr_cache_fd_locked(path, mbox_fd);
+ nfs_flush_read_cache_locked(path, mbox_fd);
+ } else {
+ nfs_flush_attr_cache_unlocked(path);
+ nfs_flush_read_cache_unlocked(path, mbox_fd);
+ }
+ }
+
+ mbox->mbox_lock_id += 2;
+ }
+
+ if (lock_type == F_RDLCK) {
+ mbox->mbox_shared_locks++;
+ *lock_id_r = mbox->mbox_lock_id;
+ } else {
+ mbox->mbox_excl_locks++;
+ *lock_id_r = mbox->mbox_lock_id + 1;
+ }
+ if (mbox->mbox_stream != NULL)
+ istream_raw_mbox_set_locked(mbox->mbox_stream);
+ return 1;
+}
+
+static int mbox_unlock_files(struct mbox_lock_context *ctx)
+{
+ int ret = 0;
+
+ if (mbox_lock_list(ctx, F_UNLCK, 0, 0) < 0)
+ ret = -1;
+
+ ctx->mbox->mbox_lock_id += 2;
+ ctx->mbox->mbox_lock_type = F_UNLCK;
+ return ret;
+}
+
+int mbox_unlock(struct mbox_mailbox *mbox, unsigned int lock_id)
+{
+ struct mbox_lock_context ctx;
+ bool fcntl_locked;
+ int i;
+
+ i_assert(mbox->mbox_lock_id == (lock_id & ~1U));
+
+ if ((lock_id & 1) != 0) {
+ /* dropping exclusive lock */
+ i_assert(mbox->mbox_excl_locks > 0);
+ if (--mbox->mbox_excl_locks > 0)
+ return 0;
+ if (mbox->mbox_shared_locks > 0) {
+ /* drop to shared lock */
+ if (mbox_update_locking(mbox, F_RDLCK,
+ &fcntl_locked) < 0)
+ return -1;
+ return 0;
+ }
+ } else {
+ /* dropping shared lock */
+ i_assert(mbox->mbox_shared_locks > 0);
+ if (--mbox->mbox_shared_locks > 0)
+ return 0;
+ if (mbox->mbox_excl_locks > 0)
+ return 0;
+ }
+ /* all locks gone */
+
+ /* make sure we don't read the stream while unlocked */
+ if (mbox->mbox_stream != NULL)
+ istream_raw_mbox_set_unlocked(mbox->mbox_stream);
+
+ i_zero(&ctx);
+ ctx.mbox = mbox;
+
+ for (i = 0; i < MBOX_LOCK_COUNT; i++)
+ ctx.locked_status[i] = TRUE;
+
+ return mbox_unlock_files(&ctx);
+}
+
+unsigned int mbox_get_cur_lock_id(struct mbox_mailbox *mbox)
+{
+ return mbox->mbox_lock_id +
+ (mbox->mbox_excl_locks > 0 ? 1 : 0);
+}
+
+void mbox_dotlock_touch(struct mbox_mailbox *mbox)
+{
+ if (mbox->mbox_dotlock == NULL)
+ return;
+
+ if (!mbox->mbox_used_privileges)
+ (void)file_dotlock_touch(mbox->mbox_dotlock);
+ else {
+ mbox_dotlock_privileged_op(mbox, NULL,
+ MBOX_DOTLOCK_OP_TOUCH);
+ }
+}
diff --git a/src/lib-storage/index/mbox/mbox-lock.h b/src/lib-storage/index/mbox/mbox-lock.h
new file mode 100644
index 0000000..2175908
--- /dev/null
+++ b/src/lib-storage/index/mbox/mbox-lock.h
@@ -0,0 +1,15 @@
+#ifndef MBOX_LOCK_H
+#define MBOX_LOCK_H
+
+/* NOTE: if mbox file is not open, it's opened. if it is open but file has
+ been overwritten (ie. inode has changed), it's reopened. */
+int mbox_lock(struct mbox_mailbox *mbox, int lock_type,
+ unsigned int *lock_id_r);
+int ATTR_NOWARN_UNUSED_RESULT
+mbox_unlock(struct mbox_mailbox *mbox, unsigned int lock_id);
+
+unsigned int mbox_get_cur_lock_id(struct mbox_mailbox *mbox);
+
+void mbox_dotlock_touch(struct mbox_mailbox *mbox);
+
+#endif
diff --git a/src/lib-storage/index/mbox/mbox-mail.c b/src/lib-storage/index/mbox/mbox-mail.c
new file mode 100644
index 0000000..09223b0
--- /dev/null
+++ b/src/lib-storage/index/mbox/mbox-mail.c
@@ -0,0 +1,439 @@
+/* Copyright (c) 2003-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "ioloop.h"
+#include "istream.h"
+#include "hex-binary.h"
+#include "index-mail.h"
+#include "mbox-storage.h"
+#include "mbox-file.h"
+#include "mbox-lock.h"
+#include "mbox-sync-private.h"
+#include "istream-raw-mbox.h"
+#include "istream-header-filter.h"
+
+#include <fcntl.h>
+#include <unistd.h>
+#include <sys/stat.h>
+
+static void mbox_prepare_resync(struct mail *mail)
+{
+ struct mbox_transaction_context *t = MBOX_TRANSCTX(mail->transaction);
+ struct mbox_mailbox *mbox = MBOX_MAILBOX(mail->box);
+
+ if (mbox->mbox_lock_type == F_RDLCK) {
+ if (mbox->mbox_lock_id == t->read_lock_id)
+ t->read_lock_id = 0;
+ mbox_unlock(mbox, mbox->mbox_lock_id);
+ i_assert(mbox->mbox_lock_type == F_UNLCK);
+ }
+}
+
+static int mbox_mail_seek(struct index_mail *mail)
+{
+ struct mail *_mail = &mail->mail.mail;
+ struct mbox_transaction_context *t = MBOX_TRANSCTX(_mail->transaction);
+ struct mbox_mailbox *mbox = MBOX_MAILBOX(_mail->box);
+ enum mbox_sync_flags sync_flags = 0;
+ int ret, try;
+ bool deleted;
+
+ if (_mail->expunged || mbox->syncing)
+ return -1;
+
+ if (!mail_stream_access_start(_mail))
+ return -1;
+
+ if (mbox->mbox_stream != NULL &&
+ istream_raw_mbox_is_corrupted(mbox->mbox_stream)) {
+ /* clear the corruption by forcing a full resync */
+ sync_flags |= MBOX_SYNC_UNDIRTY | MBOX_SYNC_FORCE_SYNC;
+ }
+
+ for (try = 0; try < 2; try++) {
+ if ((sync_flags & MBOX_SYNC_FORCE_SYNC) != 0) {
+ /* dirty offsets are broken. make sure we can sync. */
+ mbox_prepare_resync(_mail);
+ }
+ if (mbox->mbox_lock_type == F_UNLCK) {
+ i_assert(t->read_lock_id == 0);
+ sync_flags |= MBOX_SYNC_LOCK_READING;
+ if (mbox_sync(mbox, sync_flags) < 0)
+ return -1;
+ t->read_lock_id = mbox_get_cur_lock_id(mbox);
+ i_assert(t->read_lock_id != 0);
+
+ /* refresh index file after mbox has been locked to
+ make sure we get only up-to-date mbox offsets. */
+ if (mail_index_refresh(mbox->box.index) < 0) {
+ mailbox_set_index_error(&mbox->box);
+ return -1;
+ }
+
+ i_assert(mbox->mbox_lock_type != F_UNLCK);
+ } else if (t->read_lock_id == 0) {
+ /* file is already locked by another transaction, but
+ we must keep it locked for the entire transaction,
+ so increase the lock counter. */
+ if (mbox_lock(mbox, mbox->mbox_lock_type,
+ &t->read_lock_id) < 0)
+ i_unreached();
+ }
+
+ if (mbox_file_open_stream(mbox) < 0)
+ return -1;
+
+ ret = mbox_file_seek(mbox, _mail->transaction->view,
+ _mail->seq, &deleted);
+ if (ret > 0) {
+ /* success */
+ break;
+ }
+ if (ret < 0) {
+ if (deleted)
+ mail_set_expunged(_mail);
+ return -1;
+ }
+
+ /* we'll need to re-sync it completely */
+ sync_flags |= MBOX_SYNC_UNDIRTY | MBOX_SYNC_FORCE_SYNC;
+ }
+ if (ret == 0) {
+ mail_set_critical(_mail, "mbox: Losing sync");
+ }
+ return 0;
+}
+
+static int mbox_mail_get_received_date(struct mail *_mail, time_t *date_r)
+{
+ struct index_mail *mail = INDEX_MAIL(_mail);
+ struct index_mail_data *data = &mail->data;
+ struct mbox_mailbox *mbox = MBOX_MAILBOX(_mail->box);
+
+ if (index_mail_get_received_date(_mail, date_r) == 0)
+ return 0;
+
+ if (mbox_mail_seek(mail) < 0)
+ return -1;
+ data->received_date =
+ istream_raw_mbox_get_received_time(mbox->mbox_stream);
+ if (data->received_date == (time_t)-1) {
+ /* it's broken and conflicts with our "not found"
+ return value. change it. */
+ data->received_date = 0;
+ }
+
+ *date_r = data->received_date;
+ return 0;
+}
+
+static int mbox_mail_get_save_date(struct mail *_mail, time_t *date_r)
+{
+ struct index_mail *mail = INDEX_MAIL(_mail);
+ struct index_mail_data *data = &mail->data;
+
+ if (index_mail_get_save_date(_mail, date_r) > 0)
+ return 0;
+
+ /* no way to know this. save the current time into cache and use
+ that from now on. this works only as long as the index files
+ are permanent */
+ data->save_date = ioloop_time;
+ *date_r = data->save_date;
+ return 0;
+}
+
+static int
+mbox_mail_get_md5_header(struct index_mail *mail, const char **value_r)
+{
+ struct mail *_mail = &mail->mail.mail;
+ static uint8_t empty_md5[16] =
+ { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 };
+ struct mbox_mailbox *mbox = MBOX_MAILBOX(_mail->box);
+ const void *ext_data;
+
+ if (mail->data.guid != NULL) {
+ *value_r = mail->data.guid;
+ return 1;
+ }
+
+ mail_index_lookup_ext(_mail->transaction->view, _mail->seq,
+ mbox->md5hdr_ext_idx, &ext_data, NULL);
+ if (ext_data != NULL && memcmp(ext_data, empty_md5, 16) != 0) {
+ mail->data.guid = p_strdup(mail->mail.data_pool,
+ binary_to_hex(ext_data, 16));
+ *value_r = mail->data.guid;
+ return 1;
+ } else if (mail_index_is_expunged(_mail->transaction->view, _mail->seq)) {
+ mail_set_expunged(_mail);
+ return -1;
+ } else {
+ return 0;
+ }
+}
+
+static int
+mbox_mail_get_special(struct mail *_mail, enum mail_fetch_field field,
+ const char **value_r)
+{
+ struct index_mail *mail = INDEX_MAIL(_mail);
+ struct mbox_mailbox *mbox = MBOX_MAILBOX(_mail->box);
+ uoff_t offset;
+ bool move_offset;
+ int ret;
+
+ switch (field) {
+ case MAIL_FETCH_FROM_ENVELOPE:
+ if (mbox_mail_seek(mail) < 0)
+ return -1;
+
+ *value_r = istream_raw_mbox_get_sender(mbox->mbox_stream);
+ return 0;
+ case MAIL_FETCH_GUID:
+ case MAIL_FETCH_HEADER_MD5:
+ if ((ret = mbox_mail_get_md5_header(mail, value_r)) != 0)
+ return ret < 0 ? -1 : 0;
+
+ /* i guess in theory the empty_md5 is valid and can happen,
+ but it's almost guaranteed that it means the MD5 sum is
+ missing. recalculate it. */
+ if (mbox->mbox_lock_type == F_UNLCK ||
+ mbox->mbox_stream == NULL) {
+ offset = 0;
+ move_offset = FALSE;
+ } else {
+ offset = istream_raw_mbox_get_start_offset(mbox->mbox_stream);
+ move_offset = TRUE;
+ }
+ mbox->mbox_save_md5 = TRUE;
+ if (mbox_sync(mbox, MBOX_SYNC_FORCE_SYNC |
+ MBOX_SYNC_READONLY) < 0)
+ return -1;
+ if (move_offset) {
+ if (istream_raw_mbox_seek(mbox->mbox_stream,
+ offset) < 0) {
+ i_error("mbox %s sync lost during MD5 syncing",
+ _mail->box->name);
+ return -1;
+ }
+ }
+
+ if ((ret = mbox_mail_get_md5_header(mail, value_r)) == 0) {
+ i_error("mbox %s resyncing didn't save header MD5 values",
+ _mail->box->name);
+ return -1;
+ }
+ return ret < 0 ? -1 : 0;
+ default:
+ break;
+ }
+
+ return index_mail_get_special(_mail, field, value_r);
+}
+
+static int
+mbox_mail_get_next_offset(struct index_mail *mail, uoff_t *next_offset_r)
+{
+ struct mbox_mailbox *mbox = MBOX_MAILBOX(mail->mail.mail.box);
+ struct mail_index_view *view;
+ const struct mail_index_header *hdr;
+ uint32_t seq;
+ int trailer_size;
+ int ret = 1;
+
+ *next_offset_r = UOFF_T_MAX;
+
+ hdr = mail_index_get_header(mail->mail.mail.transaction->view);
+ if (mail->mail.mail.seq > hdr->messages_count) {
+ /* we're appending a new message */
+ return 0;
+ }
+
+ /* We can't really trust trans_view. The next message may already be
+ expunged from it. Also hdr.messages_count may be incorrect there.
+ So refresh the index to get the latest changes and get the next
+ message's offset using a new view. */
+ i_assert(mbox->mbox_lock_type != F_UNLCK);
+ if (mbox_sync_header_refresh(mbox) < 0)
+ return -1;
+
+ view = mail_index_view_open(mail->mail.mail.box->index);
+ hdr = mail_index_get_header(view);
+ if (!mail_index_lookup_seq(view, mail->mail.mail.uid, &seq))
+ i_panic("Message unexpectedly expunged from index");
+
+ if (seq < hdr->messages_count) {
+ if (mbox_file_lookup_offset(mbox, view, seq + 1,
+ next_offset_r) <= 0)
+ ret = -1;
+ } else if (mail->mail.mail.box->input != NULL) {
+ /* opened the mailbox as input stream. we can't trust the
+ sync_size, since it's wrong with compressed mailboxes */
+ ret = 0;
+ } else {
+ /* last message, use the synced mbox size */
+ trailer_size =
+ mbox->storage->storage.set->mail_save_crlf ? 2 : 1;
+ *next_offset_r = mbox->mbox_hdr.sync_size - trailer_size;
+ }
+ mail_index_view_close(&view);
+ return ret;
+}
+
+static int mbox_mail_get_physical_size(struct mail *_mail, uoff_t *size_r)
+{
+ struct index_mail *mail = INDEX_MAIL(_mail);
+ struct index_mail_data *data = &mail->data;
+ struct mbox_mailbox *mbox = MBOX_MAILBOX(_mail->box);
+ struct istream *input;
+ struct message_size hdr_size;
+ uoff_t old_offset, body_offset, body_size, next_offset;
+
+ if (index_mail_get_physical_size(_mail, size_r) == 0)
+ return 0;
+
+ /* we want to return the header size as seen by mail_get_stream(). */
+ old_offset = data->stream == NULL ? 0 : data->stream->v_offset;
+ if (mail_get_stream(_mail, &hdr_size, NULL, &input) < 0)
+ return -1;
+
+ /* our header size varies, so don't do any caching */
+ if (istream_raw_mbox_get_body_offset(mbox->mbox_stream, &body_offset) < 0) {
+ mail_set_critical(_mail, "mbox: Couldn't get body offset");
+ return -1;
+ }
+
+ /* use the next message's offset to avoid reading through the entire
+ message body to find out its size */
+ if (mbox_mail_get_next_offset(mail, &next_offset) > 0)
+ body_size = next_offset - body_offset;
+ else
+ body_size = UOFF_T_MAX;
+
+ /* verify that the calculated body size is correct */
+ if (istream_raw_mbox_get_body_size(mbox->mbox_stream,
+ body_size, &body_size) < 0) {
+ mail_set_critical(_mail, "mbox: Couldn't get body size");
+ return -1;
+ }
+
+ data->physical_size = hdr_size.physical_size + body_size;
+ *size_r = data->physical_size;
+
+ i_stream_seek(input, old_offset);
+ return 0;
+}
+
+static int mbox_mail_init_stream(struct index_mail *mail)
+{
+ struct mbox_mailbox *mbox = MBOX_MAILBOX(mail->mail.mail.box);
+ struct istream *raw_stream;
+ uoff_t hdr_offset, next_offset;
+ int ret;
+
+ if (mbox_mail_seek(mail) < 0)
+ return -1;
+
+ ret = mbox_mail_get_next_offset(mail, &next_offset);
+ if (ret < 0) {
+ if (mbox_mail_seek(mail) < 0)
+ return -1;
+ ret = mbox_mail_get_next_offset(mail, &next_offset);
+ if (ret < 0) {
+ i_warning("mbox %s: Can't find next message offset "
+ "for uid=%u", mailbox_get_path(&mbox->box),
+ mail->mail.mail.uid);
+ }
+ }
+
+ raw_stream = mbox->mbox_stream;
+ if (istream_raw_mbox_get_header_offset(raw_stream, &hdr_offset) < 0) {
+ mail_set_critical(&mail->mail.mail,
+ "mbox: Couldn't get header offset");
+ return -1;
+ }
+ i_stream_seek(raw_stream, hdr_offset);
+
+ if (next_offset != UOFF_T_MAX)
+ istream_raw_mbox_set_next_offset(raw_stream, next_offset);
+
+ raw_stream = i_stream_create_limit(raw_stream, UOFF_T_MAX);
+ mail->data.stream =
+ i_stream_create_header_filter(raw_stream,
+ HEADER_FILTER_EXCLUDE | HEADER_FILTER_NO_CR,
+ mbox_hide_headers, mbox_hide_headers_count,
+ *null_header_filter_callback, NULL);
+ i_stream_unref(&raw_stream);
+ return 0;
+}
+
+static int mbox_mail_get_stream(struct mail *_mail, bool get_body ATTR_UNUSED,
+ struct message_size *hdr_size,
+ struct message_size *body_size,
+ struct istream **stream_r)
+{
+ struct index_mail *mail = INDEX_MAIL(_mail);
+
+ if (mail->data.stream == NULL) {
+ if (mbox_mail_init_stream(mail) < 0)
+ return -1;
+ }
+
+ return index_mail_init_stream(mail, hdr_size, body_size, stream_r);
+}
+
+static void mbox_mail_set_seq(struct mail *_mail, uint32_t seq, bool saving)
+{
+ struct index_mail *mail = INDEX_MAIL(_mail);
+
+ index_mail_set_seq(_mail, seq, saving);
+ mail->data.dont_cache_fetch_fields |= MAIL_FETCH_PHYSICAL_SIZE;
+}
+
+static bool mbox_mail_set_uid(struct mail *_mail, uint32_t uid)
+{
+ struct index_mail *mail = INDEX_MAIL(_mail);
+ bool ret;
+
+ ret = index_mail_set_uid(_mail, uid);
+ mail->data.dont_cache_fetch_fields |= MAIL_FETCH_PHYSICAL_SIZE;
+ return ret;
+}
+
+struct mail_vfuncs mbox_mail_vfuncs = {
+ index_mail_close,
+ index_mail_free,
+ mbox_mail_set_seq,
+ mbox_mail_set_uid,
+ index_mail_set_uid_cache_updates,
+ index_mail_prefetch,
+ index_mail_precache,
+ index_mail_add_temp_wanted_fields,
+
+ index_mail_get_flags,
+ index_mail_get_keywords,
+ index_mail_get_keyword_indexes,
+ index_mail_get_modseq,
+ index_mail_get_pvt_modseq,
+ index_mail_get_parts,
+ index_mail_get_date,
+ mbox_mail_get_received_date,
+ mbox_mail_get_save_date,
+ index_mail_get_virtual_size,
+ mbox_mail_get_physical_size,
+ index_mail_get_first_header,
+ index_mail_get_headers,
+ index_mail_get_header_stream,
+ mbox_mail_get_stream,
+ index_mail_get_binary_stream,
+ mbox_mail_get_special,
+ index_mail_get_backend_mail,
+ index_mail_update_flags,
+ index_mail_update_keywords,
+ index_mail_update_modseq,
+ index_mail_update_pvt_modseq,
+ NULL,
+ index_mail_expunge,
+ index_mail_set_cache_corrupted,
+ index_mail_opened,
+};
diff --git a/src/lib-storage/index/mbox/mbox-md5-all.c b/src/lib-storage/index/mbox/mbox-md5-all.c
new file mode 100644
index 0000000..9a09fb2
--- /dev/null
+++ b/src/lib-storage/index/mbox/mbox-md5-all.c
@@ -0,0 +1,39 @@
+/* Copyright (c) 2004-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "md5.h"
+#include "message-parser.h"
+#include "mbox-md5.h"
+
+
+struct mbox_md5_context {
+ struct md5_context hdr_md5_ctx;
+};
+
+static struct mbox_md5_context *mbox_md5_all_init(void)
+{
+ struct mbox_md5_context *ctx;
+
+ ctx = i_new(struct mbox_md5_context, 1);
+ md5_init(&ctx->hdr_md5_ctx);
+ return ctx;
+}
+
+static void mbox_md5_all_more(struct mbox_md5_context *ctx,
+ struct message_header_line *hdr)
+{
+ md5_update(&ctx->hdr_md5_ctx, hdr->value, hdr->value_len);
+}
+
+static void mbox_md5_all_finish(struct mbox_md5_context *ctx,
+ unsigned char result[STATIC_ARRAY 16])
+{
+ md5_final(&ctx->hdr_md5_ctx, result);
+ i_free(ctx);
+}
+
+struct mbox_md5_vfuncs mbox_md5_all = {
+ mbox_md5_all_init,
+ mbox_md5_all_more,
+ mbox_md5_all_finish
+};
diff --git a/src/lib-storage/index/mbox/mbox-md5-apop3d.c b/src/lib-storage/index/mbox/mbox-md5-apop3d.c
new file mode 100644
index 0000000..56f6f91
--- /dev/null
+++ b/src/lib-storage/index/mbox/mbox-md5-apop3d.c
@@ -0,0 +1,119 @@
+/* Copyright (c) 2004-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "md5.h"
+#include "message-parser.h"
+#include "mbox-md5.h"
+
+
+struct mbox_md5_context {
+ struct md5_context hdr_md5_ctx;
+ bool seen_received_hdr;
+};
+
+struct mbox_md5_header_func {
+ const char *header;
+ bool (*func)(struct mbox_md5_context *ctx,
+ struct message_header_line *hdr);
+};
+
+static bool parse_date(struct mbox_md5_context *ctx,
+ struct message_header_line *hdr)
+{
+ if (!ctx->seen_received_hdr) {
+ /* Received-header contains date too, and more trusted one */
+ md5_update(&ctx->hdr_md5_ctx, hdr->value, hdr->value_len);
+ }
+ return TRUE;
+}
+
+static bool parse_delivered_to(struct mbox_md5_context *ctx,
+ struct message_header_line *hdr)
+{
+ md5_update(&ctx->hdr_md5_ctx, hdr->value, hdr->value_len);
+ return TRUE;
+}
+
+static bool parse_message_id(struct mbox_md5_context *ctx,
+ struct message_header_line *hdr)
+{
+ if (!ctx->seen_received_hdr) {
+ /* Received-header contains unique ID too,
+ and more trusted one */
+ md5_update(&ctx->hdr_md5_ctx, hdr->value, hdr->value_len);
+ }
+ return TRUE;
+}
+
+static bool parse_received(struct mbox_md5_context *ctx,
+ struct message_header_line *hdr)
+{
+ if (!ctx->seen_received_hdr) {
+ /* get only the first received-header */
+ md5_update(&ctx->hdr_md5_ctx, hdr->value, hdr->value_len);
+ if (!hdr->continues)
+ ctx->seen_received_hdr = TRUE;
+ }
+ return TRUE;
+}
+
+static bool parse_x_delivery_id(struct mbox_md5_context *ctx,
+ struct message_header_line *hdr)
+{
+ /* Let the local delivery agent help generate unique ID's but don't
+ blindly trust this header alone as it could just as easily come from
+ the remote. */
+ md5_update(&ctx->hdr_md5_ctx, hdr->value, hdr->value_len);
+ return TRUE;
+}
+
+
+static struct mbox_md5_header_func md5_header_funcs[] = {
+ { "Date", parse_date },
+ { "Delivered-To", parse_delivered_to },
+ { "Message-ID", parse_message_id },
+ { "Received", parse_received },
+ { "X-Delivery-ID", parse_x_delivery_id }
+};
+
+static int bsearch_header_func_cmp(const void *p1, const void *p2)
+{
+ const char *key = p1;
+ const struct mbox_md5_header_func *func = p2;
+
+ return strcasecmp(key, func->header);
+}
+
+static struct mbox_md5_context *mbox_md5_apop3d_init(void)
+{
+ struct mbox_md5_context *ctx;
+
+ ctx = i_new(struct mbox_md5_context, 1);
+ md5_init(&ctx->hdr_md5_ctx);
+ return ctx;
+}
+
+static void mbox_md5_apop3d_more(struct mbox_md5_context *ctx,
+ struct message_header_line *hdr)
+{
+ struct mbox_md5_header_func *func;
+
+ func = bsearch(hdr->name, md5_header_funcs,
+ N_ELEMENTS(md5_header_funcs), sizeof(*md5_header_funcs),
+ bsearch_header_func_cmp);
+ if (func != NULL)
+ (void)func->func(ctx, hdr);
+}
+
+static void mbox_md5_apop3d_finish(struct mbox_md5_context *ctx,
+ unsigned char result[STATIC_ARRAY 16])
+{
+ md5_final(&ctx->hdr_md5_ctx, result);
+ i_free(ctx);
+}
+
+struct mbox_md5_vfuncs mbox_md5_apop3d = {
+ mbox_md5_apop3d_init,
+ mbox_md5_apop3d_more,
+ mbox_md5_apop3d_finish
+};
diff --git a/src/lib-storage/index/mbox/mbox-md5.h b/src/lib-storage/index/mbox/mbox-md5.h
new file mode 100644
index 0000000..7584052
--- /dev/null
+++ b/src/lib-storage/index/mbox/mbox-md5.h
@@ -0,0 +1,17 @@
+#ifndef MBOX_MD5_H
+#define MBOX_MD5_H
+
+struct message_header_line;
+
+struct mbox_md5_vfuncs {
+ struct mbox_md5_context *(*init)(void);
+ void (*more)(struct mbox_md5_context *ctx,
+ struct message_header_line *hdr);
+ void (*finish)(struct mbox_md5_context *ctx,
+ unsigned char result[STATIC_ARRAY 16]);
+};
+
+extern struct mbox_md5_vfuncs mbox_md5_apop3d;
+extern struct mbox_md5_vfuncs mbox_md5_all;
+
+#endif
diff --git a/src/lib-storage/index/mbox/mbox-save.c b/src/lib-storage/index/mbox/mbox-save.c
new file mode 100644
index 0000000..2fb3e19
--- /dev/null
+++ b/src/lib-storage/index/mbox/mbox-save.c
@@ -0,0 +1,833 @@
+/* Copyright (c) 2002-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "ioloop.h"
+#include "array.h"
+#include "base64.h"
+#include "hostpid.h"
+#include "randgen.h"
+#include "istream.h"
+#include "ostream.h"
+#include "str.h"
+#include "write-full.h"
+#include "istream-header-filter.h"
+#include "istream-crlf.h"
+#include "istream-concat.h"
+#include "message-parser.h"
+#include "mail-user.h"
+#include "index-mail.h"
+#include "mbox-storage.h"
+#include "mbox-file.h"
+#include "mbox-from.h"
+#include "mbox-lock.h"
+#include "mbox-md5.h"
+#include "mbox-sync-private.h"
+
+#include <stddef.h>
+#include <unistd.h>
+#include <fcntl.h>
+#include <sys/stat.h>
+#include <utime.h>
+
+#define MBOX_DELIVERY_ID_RAND_BYTES (64/8)
+
+struct mbox_save_context {
+ struct mail_save_context ctx;
+
+ struct mbox_mailbox *mbox;
+ struct mail_index_transaction *trans;
+ uoff_t append_offset, mail_offset;
+ time_t orig_atime;
+
+ string_t *headers;
+ size_t space_end_idx;
+ uint32_t seq, next_uid, uid_validity;
+
+ struct istream *input;
+ struct ostream *output;
+ uoff_t extra_hdr_offset, eoh_offset;
+ char last_char;
+
+ struct mbox_md5_context *mbox_md5_ctx;
+ char *x_delivery_id_header;
+
+ bool synced:1;
+ bool failed:1;
+ bool finished:1;
+};
+
+#define MBOX_SAVECTX(s) container_of(s, struct mbox_save_context, ctx)
+
+static void ostream_error(struct mbox_save_context *ctx, const char *func)
+{
+ mbox_ostream_set_syscall_error(ctx->mbox, ctx->output, func);
+ ctx->failed = TRUE;
+}
+
+static void write_stream_error(struct mbox_save_context *ctx)
+{
+ ostream_error(ctx, "write()");
+}
+
+static void lseek_stream_error(struct mbox_save_context *ctx)
+{
+ ostream_error(ctx, "o_stream_seek()");
+}
+
+static int mbox_seek_to_end(struct mbox_save_context *ctx, uoff_t *offset)
+{
+ struct stat st;
+ char ch;
+ int fd;
+
+ if (ctx->mbox->mbox_writeonly) {
+ *offset = 0;
+ return 0;
+ }
+
+ fd = ctx->mbox->mbox_fd;
+ if (fstat(fd, &st) < 0) {
+ mbox_set_syscall_error(ctx->mbox, "fstat()");
+ return -1;
+ }
+
+ ctx->orig_atime = st.st_atime;
+
+ *offset = (uoff_t)st.st_size;
+ if (st.st_size == 0)
+ return 0;
+
+ if (lseek(fd, st.st_size-1, SEEK_SET) < 0) {
+ mbox_set_syscall_error(ctx->mbox, "lseek()");
+ return -1;
+ }
+
+ if (read(fd, &ch, 1) != 1) {
+ mbox_set_syscall_error(ctx->mbox, "read()");
+ return -1;
+ }
+
+ if (ch != '\n') {
+ if (write_full(fd, "\n", 1) < 0) {
+ mbox_set_syscall_error(ctx->mbox, "write()");
+ return -1;
+ }
+ *offset += 1;
+ }
+
+ return 0;
+}
+
+static int mbox_append_lf(struct mbox_save_context *ctx)
+{
+ if (o_stream_send(ctx->output, "\n", 1) < 0) {
+ write_stream_error(ctx);
+ return -1;
+ }
+
+ return 0;
+}
+
+static int write_from_line(struct mbox_save_context *ctx, time_t received_date,
+ const char *from_envelope)
+{
+ int ret;
+
+ T_BEGIN {
+ const char *line;
+
+ if (from_envelope == NULL) {
+ struct mail_storage *storage =
+ &ctx->mbox->storage->storage;
+
+ from_envelope =
+ strchr(storage->user->username, '@') != NULL ?
+ storage->user->username :
+ t_strconcat(storage->user->username,
+ "@", my_hostdomain(), NULL);
+ } else if (*from_envelope == '\0') {
+ /* can't write empty envelope */
+ from_envelope = "MAILER-DAEMON";
+ }
+
+ /* save in local timezone, no matter what it was given with */
+ line = mbox_from_create(from_envelope, received_date);
+
+ if ((ret = o_stream_send_str(ctx->output, line)) < 0)
+ write_stream_error(ctx);
+ } T_END;
+ return ret;
+}
+
+static int mbox_write_content_length(struct mbox_save_context *ctx)
+{
+ uoff_t end_offset;
+ const char *str;
+ size_t len;
+
+ i_assert(ctx->eoh_offset != UOFF_T_MAX);
+
+ if (ctx->mbox->mbox_writeonly) {
+ /* we can't seek, don't set Content-Length */
+ return 0;
+ }
+
+ end_offset = ctx->output->offset;
+
+ /* write Content-Length headers */
+ str = t_strdup_printf("\nContent-Length: %s",
+ dec2str(end_offset - ctx->eoh_offset));
+ len = strlen(str);
+
+ /* flush manually here so that we don't confuse seek() errors with
+ buffer flushing errors */
+ if (o_stream_flush(ctx->output) < 0) {
+ write_stream_error(ctx);
+ return -1;
+ }
+ if (o_stream_seek(ctx->output, ctx->extra_hdr_offset +
+ ctx->space_end_idx - len) < 0) {
+ lseek_stream_error(ctx);
+ return -1;
+ }
+
+ if (o_stream_send(ctx->output, str, len) < 0 ||
+ o_stream_flush(ctx->output) < 0) {
+ write_stream_error(ctx);
+ return -1;
+ }
+
+ if (o_stream_seek(ctx->output, end_offset) < 0) {
+ lseek_stream_error(ctx);
+ return -1;
+ }
+ return 0;
+}
+
+static void mbox_save_init_sync(struct mailbox_transaction_context *t)
+{
+ struct mbox_mailbox *mbox = MBOX_MAILBOX(t->box);
+ struct mbox_save_context *ctx = MBOX_SAVECTX(t->save_ctx);
+ const struct mail_index_header *hdr;
+ struct mail_index_view *view;
+
+ /* open a new view to get the header. this is required if we just
+ synced the mailbox so we can get updated next_uid. */
+ mail_index_refresh(mbox->box.index);
+ view = mail_index_view_open(mbox->box.index);
+ hdr = mail_index_get_header(view);
+
+ ctx->next_uid = hdr->next_uid;
+ ctx->uid_validity = hdr->uid_validity;
+ ctx->synced = TRUE;
+
+ mail_index_view_close(&view);
+}
+
+static void status_flags_append(string_t *str, enum mail_flags flags,
+ const struct mbox_flag_type *flags_list)
+{
+ int i;
+
+ flags ^= MBOX_NONRECENT_KLUDGE;
+ for (i = 0; flags_list[i].chr != 0; i++) {
+ if ((flags & flags_list[i].flag) != 0)
+ str_append_c(str, flags_list[i].chr);
+ }
+}
+
+static void mbox_save_append_flag_headers(string_t *str, enum mail_flags flags)
+{
+ /* write the Status: header always. It always gets added soon anyway. */
+ str_append(str, "Status: ");
+ status_flags_append(str, flags, mbox_status_flags);
+ str_append_c(str, '\n');
+
+ if ((flags & XSTATUS_FLAGS_MASK) != 0) {
+ str_append(str, "X-Status: ");
+ status_flags_append(str, flags, mbox_xstatus_flags);
+ str_append_c(str, '\n');
+ }
+}
+
+static void
+mbox_save_append_keyword_headers(struct mbox_save_context *ctx,
+ struct mail_keywords *keywords)
+{
+ unsigned char space[MBOX_HEADER_PADDING+1 +
+ sizeof("Content-Length: \n")-1 + MAX_INT_STRLEN];
+ const ARRAY_TYPE(keywords) *keyword_names_list;
+ const char *const *keyword_names;
+ unsigned int i, count, keyword_names_count;
+
+ keyword_names_list = mail_index_get_keywords(ctx->mbox->box.index);
+ keyword_names = array_get(keyword_names_list, &keyword_names_count);
+
+ str_append(ctx->headers, "X-Keywords:");
+ count = keywords == NULL ? 0 : keywords->count;
+ for (i = 0; i < count; i++) {
+ i_assert(keywords->idx[i] < keyword_names_count);
+
+ str_append_c(ctx->headers, ' ');
+ str_append(ctx->headers, keyword_names[keywords->idx[i]]);
+ }
+
+ memset(space, ' ', sizeof(space));
+ str_append_data(ctx->headers, space, sizeof(space));
+ ctx->space_end_idx = str_len(ctx->headers);
+ str_append_c(ctx->headers, '\n');
+}
+
+static int
+mbox_save_init_file(struct mbox_save_context *ctx,
+ struct mbox_transaction_context *t)
+{
+ struct mailbox_transaction_context *_t = &t->t;
+ struct mbox_mailbox *mbox = ctx->mbox;
+ struct mail_storage *storage = &mbox->storage->storage;
+ int ret;
+
+ if (mbox_is_backend_readonly(ctx->mbox)) {
+ mail_storage_set_error(storage, MAIL_ERROR_PERM,
+ "Read-only mbox");
+ return -1;
+ }
+
+ if (ctx->append_offset == UOFF_T_MAX) {
+ /* first appended mail in this transaction */
+ if (t->write_lock_id == 0) {
+ if (mbox_lock(mbox, F_WRLCK, &t->write_lock_id) <= 0)
+ return -1;
+ }
+
+ if (mbox->mbox_fd == -1) {
+ if (mbox_file_open(mbox) < 0)
+ return -1;
+ }
+
+ /* update mbox_sync_dirty state */
+ ret = mbox_sync_has_changed(mbox, TRUE);
+ if (ret < 0)
+ return -1;
+ }
+
+ if (!ctx->synced) {
+ /* we'll need to assign UID for the mail immediately. */
+ if (mbox_sync(mbox, 0) < 0)
+ return -1;
+ mbox_save_init_sync(_t);
+ }
+
+ /* the syncing above could have changed the append offset */
+ if (ctx->append_offset == UOFF_T_MAX) {
+ if (mbox_seek_to_end(ctx, &ctx->append_offset) < 0)
+ return -1;
+
+ i_assert(mbox->mbox_fd != -1);
+ ctx->output = o_stream_create_fd_file(mbox->mbox_fd,
+ ctx->append_offset,
+ FALSE);
+ o_stream_cork(ctx->output);
+ }
+ return 0;
+}
+
+static void
+save_header_callback(struct header_filter_istream *input ATTR_UNUSED,
+ struct message_header_line *hdr,
+ bool *matched, struct mbox_save_context *ctx)
+{
+ if (hdr != NULL) {
+ if (str_begins(hdr->name, "From ")) {
+ /* we can't allow From_-lines in headers. there's no
+ legitimate reason for allowing them in any case,
+ so just drop them. */
+ *matched = TRUE;
+ return;
+ }
+
+ if (!*matched && ctx->mbox_md5_ctx != NULL)
+ ctx->mbox->md5_v.more(ctx->mbox_md5_ctx, hdr);
+ }
+}
+
+static void mbox_save_x_delivery_id(struct mbox_save_context *ctx)
+{
+ unsigned char md5_result[MD5_RESULTLEN];
+ buffer_t *buf;
+ string_t *str;
+ void *randbuf;
+
+ buf = t_buffer_create(256);
+ buffer_append(buf, &ioloop_time, sizeof(ioloop_time));
+ buffer_append(buf, &ioloop_timeval.tv_usec,
+ sizeof(ioloop_timeval.tv_usec));
+
+ randbuf = buffer_append_space_unsafe(buf, MBOX_DELIVERY_ID_RAND_BYTES);
+ random_fill(randbuf, MBOX_DELIVERY_ID_RAND_BYTES);
+
+ md5_get_digest(buf->data, buf->used, md5_result);
+
+ str = t_str_new(128);
+ str_append(str, "X-Delivery-ID: ");
+ base64_encode(md5_result, sizeof(md5_result), str);
+ str_append_c(str, '\n');
+
+ ctx->x_delivery_id_header = i_strdup(str_c(str));
+}
+
+static struct istream *
+mbox_save_get_input_stream(struct mbox_save_context *ctx, struct istream *input)
+{
+ struct istream *filter, *ret, *cache_input, *streams[3];
+
+ /* filter out unwanted headers and keep track of headers' MD5 sum */
+ filter = i_stream_create_header_filter(input, HEADER_FILTER_EXCLUDE |
+ HEADER_FILTER_NO_CR |
+ HEADER_FILTER_ADD_MISSING_EOH |
+ HEADER_FILTER_END_BODY_WITH_LF,
+ mbox_save_drop_headers,
+ mbox_save_drop_headers_count,
+ save_header_callback, ctx);
+
+ if ((ctx->mbox->storage->storage.flags &
+ MAIL_STORAGE_FLAG_KEEP_HEADER_MD5) != 0) {
+ /* we're using MD5 sums to generate POP3 UIDLs.
+ clients don't like it much if there are duplicates,
+ so make sure that there can't be any by appending
+ our own X-Delivery-ID header. */
+ const char *hdr;
+
+ T_BEGIN {
+ mbox_save_x_delivery_id(ctx);
+ } T_END;
+ hdr = ctx->x_delivery_id_header;
+
+ streams[0] = i_stream_create_from_data(hdr, strlen(hdr));
+ streams[1] = filter;
+ streams[2] = NULL;
+ ret = i_stream_create_concat(streams);
+ i_stream_unref(&filter);
+ filter = ret;
+ }
+
+ /* convert linefeeds to wanted format */
+ ret = ctx->mbox->storage->storage.set->mail_save_crlf ?
+ i_stream_create_crlf(filter) : i_stream_create_lf(filter);
+ i_stream_unref(&filter);
+
+ /* caching creates a tee stream */
+ cache_input = index_mail_cache_parse_init(ctx->ctx.dest_mail, ret);
+ i_stream_unref(&ret);
+ ret = cache_input;
+ return ret;
+}
+
+struct mail_save_context *
+mbox_save_alloc(struct mailbox_transaction_context *t)
+{
+ struct mbox_mailbox *mbox = MBOX_MAILBOX(t->box);
+ struct mbox_save_context *ctx;
+
+ i_assert((t->flags & MAILBOX_TRANSACTION_FLAG_EXTERNAL) != 0);
+
+ if (t->save_ctx == NULL) {
+ ctx = i_new(struct mbox_save_context, 1);
+ ctx->ctx.transaction = t;
+ ctx->mbox = mbox;
+ ctx->trans = t->itrans;
+ ctx->append_offset = UOFF_T_MAX;
+ ctx->headers = str_new(default_pool, 512);
+ ctx->mail_offset = UOFF_T_MAX;
+ t->save_ctx = &ctx->ctx;
+ }
+ return t->save_ctx;
+}
+
+int mbox_save_begin(struct mail_save_context *_ctx, struct istream *input)
+{
+ struct mbox_save_context *ctx = MBOX_SAVECTX(_ctx);
+ struct mail_save_data *mdata = &_ctx->data;
+ struct mbox_transaction_context *t = MBOX_TRANSCTX(_ctx->transaction);
+ enum mail_flags save_flags;
+ uint64_t offset;
+
+ /* FIXME: we could write timezone_offset to From-line.. */
+ if (mdata->received_date == (time_t)-1)
+ mdata->received_date = ioloop_time;
+
+ ctx->failed = FALSE;
+ ctx->seq = 0;
+
+ if (mbox_save_init_file(ctx, t) < 0) {
+ ctx->failed = TRUE;
+ return -1;
+ }
+
+ save_flags = mdata->flags;
+ if (mdata->uid == 0)
+ save_flags |= MAIL_RECENT;
+ str_truncate(ctx->headers, 0);
+ if (ctx->synced) {
+ if (ctx->mbox->mbox_save_md5)
+ ctx->mbox_md5_ctx = ctx->mbox->md5_v.init();
+ if (ctx->next_uid < mdata->uid) {
+ /* we can use the wanted UID */
+ ctx->next_uid = mdata->uid;
+ }
+ if (ctx->output->offset == 0) {
+ /* writing the first mail. Insert X-IMAPbase as well. */
+ str_printfa(ctx->headers, "X-IMAPbase: %u %010u\n",
+ ctx->uid_validity, ctx->next_uid);
+ }
+ str_printfa(ctx->headers, "X-UID: %u\n", ctx->next_uid);
+
+ mail_index_append(ctx->trans, ctx->next_uid, &ctx->seq);
+ mail_index_update_flags(ctx->trans, ctx->seq, MODIFY_REPLACE,
+ save_flags & ENUM_NEGATE(MAIL_RECENT));
+ if (mdata->keywords != NULL) {
+ mail_index_update_keywords(ctx->trans, ctx->seq,
+ MODIFY_REPLACE,
+ mdata->keywords);
+ }
+ if (mdata->min_modseq != 0) {
+ mail_index_update_modseq(ctx->trans, ctx->seq,
+ mdata->min_modseq);
+ }
+
+ offset = ctx->output->offset == 0 ? 0 :
+ ctx->output->offset - 1;
+ mail_index_update_ext(ctx->trans, ctx->seq,
+ ctx->mbox->mbox_ext_idx, &offset, NULL);
+ ctx->next_uid++;
+
+ /* parse and cache the mail headers as we read it */
+ mail_set_seq_saving(_ctx->dest_mail, ctx->seq);
+ }
+ mbox_save_append_flag_headers(ctx->headers, save_flags);
+ mbox_save_append_keyword_headers(ctx, mdata->keywords);
+ str_append_c(ctx->headers, '\n');
+
+ i_assert(ctx->mbox->mbox_lock_type == F_WRLCK);
+
+ ctx->mail_offset = ctx->output->offset;
+ ctx->eoh_offset = UOFF_T_MAX;
+ ctx->last_char = '\n';
+
+ if (write_from_line(ctx, mdata->received_date, mdata->from_envelope) < 0)
+ ctx->failed = TRUE;
+ else
+ ctx->input = mbox_save_get_input_stream(ctx, input);
+ return ctx->failed ? -1 : 0;
+}
+
+static int mbox_save_body_input(struct mbox_save_context *ctx)
+{
+ const unsigned char *data;
+ size_t size;
+
+ data = i_stream_get_data(ctx->input, &size);
+ if (size > 0) {
+ if (o_stream_send(ctx->output, data, size) < 0) {
+ write_stream_error(ctx);
+ return -1;
+ }
+ ctx->last_char = data[size-1];
+ i_stream_skip(ctx->input, size);
+ }
+ return 0;
+}
+
+static int mbox_save_body(struct mbox_save_context *ctx)
+{
+ ssize_t ret;
+
+ while ((ret = i_stream_read(ctx->input)) != -1) {
+ if (mbox_save_body_input(ctx) < 0)
+ return -1;
+ /* i_stream_read() may have returned 0 at EOF
+ because of this parser */
+ index_mail_cache_parse_continue(ctx->ctx.dest_mail);
+ if (ret == 0)
+ return 0;
+ }
+
+ i_assert(ctx->last_char == '\n');
+ return 0;
+}
+
+static int mbox_save_finish_headers(struct mbox_save_context *ctx)
+{
+ i_assert(ctx->eoh_offset == UOFF_T_MAX);
+
+ /* append our own headers and ending empty line */
+ ctx->extra_hdr_offset = ctx->output->offset;
+ if (o_stream_send(ctx->output, str_data(ctx->headers),
+ str_len(ctx->headers)) < 0) {
+ write_stream_error(ctx);
+ return -1;
+ }
+ ctx->eoh_offset = ctx->output->offset;
+ return 0;
+}
+
+int mbox_save_continue(struct mail_save_context *_ctx)
+{
+ struct mbox_save_context *ctx = MBOX_SAVECTX(_ctx);
+ const unsigned char *data;
+ size_t i, size;
+ ssize_t ret;
+
+ if (ctx->failed)
+ return -1;
+
+ if (ctx->eoh_offset != UOFF_T_MAX) {
+ /* writing body */
+ return mbox_save_body(ctx);
+ }
+
+ while ((ret = i_stream_read_more(ctx->input, &data, &size)) > 0) {
+ for (i = 0; i < size; i++) {
+ if (data[i] == '\n' &&
+ ((i == 0 && ctx->last_char == '\n') ||
+ (i > 0 && data[i-1] == '\n'))) {
+ /* end of headers. we don't need to worry about
+ CRs because they're dropped */
+ break;
+ }
+ }
+ if (i != size) {
+ /* found end of headers. write the rest of them
+ (not including the finishing empty line) */
+ if (o_stream_send(ctx->output, data, i) < 0) {
+ write_stream_error(ctx);
+ return -1;
+ }
+ ctx->last_char = '\n';
+ i_stream_skip(ctx->input, i + 1);
+ break;
+ }
+
+ if (o_stream_send(ctx->output, data, size) < 0) {
+ write_stream_error(ctx);
+ return -1;
+ }
+ i_assert(size > 0);
+ ctx->last_char = data[size-1];
+ i_stream_skip(ctx->input, size);
+ index_mail_cache_parse_continue(ctx->ctx.dest_mail);
+ }
+ if (ret == 0)
+ return 0;
+ if (ctx->input->stream_errno != 0) {
+ i_error("read(%s) failed: %s", i_stream_get_name(ctx->input),
+ i_stream_get_error(ctx->input));
+ ctx->failed = TRUE;
+ return -1;
+ }
+
+ i_assert(ctx->last_char == '\n');
+
+ if (ctx->mbox_md5_ctx != NULL) {
+ unsigned char hdr_md5_sum[16];
+
+ if (ctx->x_delivery_id_header != NULL) {
+ struct message_header_line hdr;
+
+ i_zero(&hdr);
+ hdr.name = ctx->x_delivery_id_header;
+ hdr.name_len = sizeof("X-Delivery-ID")-1;
+ hdr.middle = (const unsigned char *)hdr.name +
+ hdr.name_len;
+ hdr.middle_len = 2;
+ hdr.value = hdr.full_value =
+ hdr.middle + hdr.middle_len;
+ hdr.value_len = strlen((const char *)hdr.value);
+ ctx->mbox->md5_v.more(ctx->mbox_md5_ctx, &hdr);
+ }
+ ctx->mbox->md5_v.finish(ctx->mbox_md5_ctx, hdr_md5_sum);
+ mail_index_update_ext(ctx->trans, ctx->seq,
+ ctx->mbox->md5hdr_ext_idx,
+ hdr_md5_sum, NULL);
+ }
+
+ if (mbox_save_finish_headers(ctx) < 0)
+ return -1;
+
+ /* write body */
+ if (mbox_save_body_input(ctx) < 0)
+ return -1;
+ return ctx->input->eof ? 0 : mbox_save_body(ctx);
+}
+
+int mbox_save_finish(struct mail_save_context *_ctx)
+{
+ struct mbox_save_context *ctx = MBOX_SAVECTX(_ctx);
+
+ if (!ctx->failed && ctx->eoh_offset == UOFF_T_MAX)
+ (void)mbox_save_finish_headers(ctx);
+
+ if (ctx->output != NULL) {
+ /* make sure everything is written */
+ if (o_stream_flush(ctx->output) < 0)
+ write_stream_error(ctx);
+ }
+
+ ctx->finished = TRUE;
+ if (!ctx->failed) {
+ i_assert(ctx->output != NULL);
+ T_BEGIN {
+ if (mbox_write_content_length(ctx) < 0 ||
+ mbox_append_lf(ctx) < 0)
+ ctx->failed = TRUE;
+ } T_END;
+ }
+
+ index_mail_cache_parse_deinit(ctx->ctx.dest_mail,
+ ctx->ctx.data.received_date,
+ !ctx->failed);
+ if (ctx->input != NULL)
+ i_stream_destroy(&ctx->input);
+
+ if (ctx->failed && ctx->mail_offset != UOFF_T_MAX) {
+ /* saving this mail failed - truncate back to beginning of it */
+ i_assert(ctx->output != NULL);
+ (void)o_stream_flush(ctx->output);
+ if (ftruncate(ctx->mbox->mbox_fd, (off_t)ctx->mail_offset) < 0)
+ mbox_set_syscall_error(ctx->mbox, "ftruncate()");
+ (void)o_stream_seek(ctx->output, ctx->mail_offset);
+ ctx->mail_offset = UOFF_T_MAX;
+ }
+
+ if (ctx->seq != 0 && ctx->failed) {
+ index_storage_save_abort_last(&ctx->ctx, ctx->seq);
+ }
+ index_save_context_free(_ctx);
+ return ctx->failed ? -1 : 0;
+}
+
+void mbox_save_cancel(struct mail_save_context *_ctx)
+{
+ struct mbox_save_context *ctx = MBOX_SAVECTX(_ctx);
+
+ ctx->failed = TRUE;
+ (void)mbox_save_finish(_ctx);
+}
+
+static void mbox_transaction_save_deinit(struct mbox_save_context *ctx)
+{
+ o_stream_destroy(&ctx->output);
+ str_free(&ctx->headers);
+}
+
+static void mbox_save_truncate(struct mbox_save_context *ctx)
+{
+ if (ctx->append_offset == UOFF_T_MAX || ctx->mbox->mbox_fd == -1)
+ return;
+
+ i_assert(ctx->mbox->mbox_lock_type == F_WRLCK);
+
+ /* failed, truncate file back to original size. output stream needs to
+ be flushed before truncating so unref() won't write anything. */
+ if (ctx->output != NULL)
+ (void)o_stream_flush(ctx->output);
+
+ if (ftruncate(ctx->mbox->mbox_fd, (off_t)ctx->append_offset) < 0)
+ mbox_set_syscall_error(ctx->mbox, "ftruncate()");
+}
+
+int mbox_transaction_save_commit_pre(struct mail_save_context *_ctx)
+{
+ struct mbox_save_context *ctx = MBOX_SAVECTX(_ctx);
+ struct mailbox_transaction_context *_t = _ctx->transaction;
+ struct mbox_mailbox *mbox = ctx->mbox;
+ struct stat st;
+ int ret = 0;
+
+ i_assert(ctx->finished);
+ i_assert(mbox->mbox_fd != -1);
+
+ if (fstat(mbox->mbox_fd, &st) < 0) {
+ mbox_set_syscall_error(mbox, "fstat()");
+ ret = -1;
+ }
+
+ if (ctx->synced) {
+ _t->changes->uid_validity = ctx->uid_validity;
+ mail_index_append_finish_uids(ctx->trans, 0,
+ &_t->changes->saved_uids);
+
+ mail_index_update_header(ctx->trans,
+ offsetof(struct mail_index_header, next_uid),
+ &ctx->next_uid, sizeof(ctx->next_uid), FALSE);
+
+ if (ret == 0) {
+ mbox->mbox_hdr.sync_mtime = st.st_mtime;
+ mbox->mbox_hdr.sync_size = st.st_size;
+ mail_index_update_header_ext(ctx->trans,
+ mbox->mbox_ext_idx,
+ 0, &mbox->mbox_hdr,
+ sizeof(mbox->mbox_hdr));
+ }
+ }
+
+ if (ret == 0 && ctx->orig_atime != st.st_atime) {
+ /* try to set atime back to its original value.
+ (it'll fail with EPERM for shared mailboxes where we aren't
+ the file's owner) */
+ struct utimbuf buf;
+
+ buf.modtime = st.st_mtime;
+ buf.actime = ctx->orig_atime;
+ if (utime(mailbox_get_path(&mbox->box), &buf) < 0 &&
+ errno != EPERM)
+ mbox_set_syscall_error(mbox, "utime()");
+ }
+
+ if (ctx->output != NULL) {
+ /* flush the final LF */
+ if (o_stream_flush(ctx->output) < 0)
+ write_stream_error(ctx);
+ }
+ if (mbox->mbox_fd != -1 && !mbox->mbox_writeonly &&
+ mbox->storage->storage.set->parsed_fsync_mode != FSYNC_MODE_NEVER) {
+ if (fdatasync(mbox->mbox_fd) < 0) {
+ mbox_set_syscall_error(mbox, "fdatasync()");
+ mbox_save_truncate(ctx);
+ ret = -1;
+ }
+ }
+
+ mbox_transaction_save_deinit(ctx);
+ if (ret < 0)
+ i_free(ctx);
+ return ret;
+}
+
+void mbox_transaction_save_commit_post(struct mail_save_context *_ctx,
+ struct mail_index_transaction_commit_result *result ATTR_UNUSED)
+{
+ struct mbox_save_context *ctx = MBOX_SAVECTX(_ctx);
+
+ i_assert(ctx->mbox->mbox_lock_type == F_WRLCK);
+
+ if (ctx->synced) {
+ /* after saving mails with UIDs we need to update
+ the last-uid */
+ (void)mbox_sync(ctx->mbox, MBOX_SYNC_HEADER |
+ MBOX_SYNC_REWRITE);
+ }
+ i_free(ctx);
+}
+
+void mbox_transaction_save_rollback(struct mail_save_context *_ctx)
+{
+ struct mbox_save_context *ctx = MBOX_SAVECTX(_ctx);
+
+ if (!ctx->finished)
+ mbox_save_cancel(&ctx->ctx);
+
+ mbox_save_truncate(ctx);
+ mbox_transaction_save_deinit(ctx);
+ i_free(ctx);
+}
diff --git a/src/lib-storage/index/mbox/mbox-settings.c b/src/lib-storage/index/mbox/mbox-settings.c
new file mode 100644
index 0000000..1df2452
--- /dev/null
+++ b/src/lib-storage/index/mbox/mbox-settings.c
@@ -0,0 +1,55 @@
+/* Copyright (c) 2005-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "settings-parser.h"
+#include "mail-storage-settings.h"
+#include "mbox-settings.h"
+
+#include <stddef.h>
+
+#undef DEF
+#define DEF(type, name) \
+ SETTING_DEFINE_STRUCT_##type(#name, name, struct mbox_settings)
+
+static const struct setting_define mbox_setting_defines[] = {
+ DEF(STR, mbox_read_locks),
+ DEF(STR, mbox_write_locks),
+ DEF(TIME, mbox_lock_timeout),
+ DEF(TIME, mbox_dotlock_change_timeout),
+ DEF(SIZE, mbox_min_index_size),
+ DEF(BOOL, mbox_dirty_syncs),
+ DEF(BOOL, mbox_very_dirty_syncs),
+ DEF(BOOL, mbox_lazy_writes),
+ DEF(ENUM, mbox_md5),
+
+ SETTING_DEFINE_LIST_END
+};
+
+static const struct mbox_settings mbox_default_settings = {
+ .mbox_read_locks = "fcntl",
+ .mbox_write_locks = "dotlock fcntl",
+ .mbox_lock_timeout = 5*60,
+ .mbox_dotlock_change_timeout = 2*60,
+ .mbox_min_index_size = 0,
+ .mbox_dirty_syncs = TRUE,
+ .mbox_very_dirty_syncs = FALSE,
+ .mbox_lazy_writes = TRUE,
+ .mbox_md5 = "apop3d:all"
+};
+
+static const struct setting_parser_info mbox_setting_parser_info = {
+ .module_name = "mbox",
+ .defines = mbox_setting_defines,
+ .defaults = &mbox_default_settings,
+
+ .type_offset = SIZE_MAX,
+ .struct_size = sizeof(struct mbox_settings),
+
+ .parent_offset = SIZE_MAX,
+ .parent = &mail_user_setting_parser_info
+};
+
+const struct setting_parser_info *mbox_get_setting_parser_info(void)
+{
+ return &mbox_setting_parser_info;
+}
diff --git a/src/lib-storage/index/mbox/mbox-settings.h b/src/lib-storage/index/mbox/mbox-settings.h
new file mode 100644
index 0000000..eb99d82
--- /dev/null
+++ b/src/lib-storage/index/mbox/mbox-settings.h
@@ -0,0 +1,18 @@
+#ifndef MBOX_SETTINGS_H
+#define MBOX_SETTINGS_H
+
+struct mbox_settings {
+ const char *mbox_read_locks;
+ const char *mbox_write_locks;
+ unsigned int mbox_lock_timeout;
+ unsigned int mbox_dotlock_change_timeout;
+ uoff_t mbox_min_index_size;
+ bool mbox_dirty_syncs;
+ bool mbox_very_dirty_syncs;
+ bool mbox_lazy_writes;
+ const char *mbox_md5;
+};
+
+const struct setting_parser_info *mbox_get_setting_parser_info(void);
+
+#endif
diff --git a/src/lib-storage/index/mbox/mbox-storage.c b/src/lib-storage/index/mbox/mbox-storage.c
new file mode 100644
index 0000000..4b2bb35
--- /dev/null
+++ b/src/lib-storage/index/mbox/mbox-storage.c
@@ -0,0 +1,911 @@
+/* Copyright (c) 2002-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "ioloop.h"
+#include "istream.h"
+#include "ostream.h"
+#include "restrict-access.h"
+#include "master-service.h"
+#include "mailbox-list-private.h"
+#include "mbox-storage.h"
+#include "mbox-lock.h"
+#include "mbox-file.h"
+#include "mbox-sync-private.h"
+#include "istream-raw-mbox.h"
+#include "mail-copy.h"
+#include "index-mail.h"
+
+#include <sys/stat.h>
+
+/* How often to touch the dotlock file when using KEEP_LOCKED flag */
+#define MBOX_LOCK_TOUCH_MSECS (10*1000)
+
+/* Assume that if atime < mtime, there are new mails. If it's good enough for
+ UW-IMAP, it's good enough for us. */
+#define STAT_GET_MARKED(st) \
+ ((st).st_size == 0 ? MAILBOX_UNMARKED : \
+ (st).st_atime < (st).st_mtime ? MAILBOX_MARKED : MAILBOX_UNMARKED)
+
+#define MBOX_LIST_CONTEXT(obj) \
+ MODULE_CONTEXT_REQUIRE(obj, mbox_mailbox_list_module)
+
+struct mbox_mailbox_list {
+ union mailbox_list_module_context module_ctx;
+ const struct mbox_settings *set;
+};
+
+/* NOTE: must be sorted for istream-header-filter. Note that it's not such
+ a good idea to change this list, as the messages will then change from
+ client's point of view. So if you do it, change all mailboxes' UIDVALIDITY
+ so all caches are reset. */
+const char *mbox_hide_headers[] = {
+ "Content-Length",
+ "Status",
+ "X-IMAP",
+ "X-IMAPbase",
+ "X-Keywords",
+ "X-Status",
+ "X-UID"
+};
+unsigned int mbox_hide_headers_count = N_ELEMENTS(mbox_hide_headers);
+
+/* A bit ugly duplification of the above list. It's safe to modify this list
+ without bad side effects, just keep the list sorted. */
+const char *mbox_save_drop_headers[] = {
+ "Content-Length",
+ "Status",
+ "X-Delivery-ID",
+ "X-IMAP",
+ "X-IMAPbase",
+ "X-Keywords",
+ "X-Status",
+ "X-UID"
+};
+unsigned int mbox_save_drop_headers_count = N_ELEMENTS(mbox_save_drop_headers);
+
+extern struct mail_storage mbox_storage;
+extern struct mailbox mbox_mailbox;
+
+static struct event_category event_category_mbox = {
+ .name = "mbox",
+ .parent = &event_category_storage,
+};
+
+static MODULE_CONTEXT_DEFINE_INIT(mbox_mailbox_list_module,
+ &mailbox_list_module_register);
+
+static void
+mbox_set_syscall_error_str(struct mbox_mailbox *mbox, const char *function,
+ const char *error)
+{
+ i_assert(function != NULL);
+
+ if (ENOQUOTA(errno)) {
+ mail_storage_set_error(&mbox->storage->storage,
+ MAIL_ERROR_NOQUOTA, MAIL_ERRSTR_NO_QUOTA);
+ } else {
+ const char *toobig_error = errno != EFBIG ? "" :
+ " (process was started with ulimit -f limit)";
+ mailbox_set_critical(&mbox->box, "%s failed with mbox: %s%s",
+ function, error, toobig_error);
+ }
+}
+
+void mbox_set_syscall_error(struct mbox_mailbox *mbox, const char *function)
+{
+ mbox_set_syscall_error_str(mbox, function, strerror(errno));
+}
+
+void mbox_istream_set_syscall_error(struct mbox_mailbox *mbox,
+ struct istream *input,
+ const char *function)
+{
+ errno = input->stream_errno;
+ mbox_set_syscall_error_str(mbox, function, i_stream_get_error(input));
+}
+
+void mbox_ostream_set_syscall_error(struct mbox_mailbox *mbox,
+ struct ostream *output,
+ const char *function)
+{
+ errno = output->stream_errno;
+ mbox_set_syscall_error_str(mbox, function, o_stream_get_error(output));
+}
+
+static int
+mbox_list_get_path(struct mailbox_list *list, const char *name,
+ enum mailbox_list_path_type type, const char **path_r)
+{
+ struct mbox_mailbox_list *mlist = MBOX_LIST_CONTEXT(list);
+ const char *path, *p;
+ int ret;
+
+ *path_r = NULL;
+
+ ret = mlist->module_ctx.super.get_path(list, name, type, &path);
+ if (ret <= 0)
+ return ret;
+
+ switch (type) {
+ case MAILBOX_LIST_PATH_TYPE_CONTROL:
+ case MAILBOX_LIST_PATH_TYPE_INDEX:
+ case MAILBOX_LIST_PATH_TYPE_INDEX_CACHE:
+ case MAILBOX_LIST_PATH_TYPE_LIST_INDEX:
+ if (name == NULL && type == MAILBOX_LIST_PATH_TYPE_CONTROL &&
+ list->set.control_dir != NULL) {
+ /* kind of a kludge for backwards compatibility:
+ the subscriptions file is in the root control_dir
+ without .imap/ suffix */
+ *path_r = path;
+ return 1;
+ }
+ if (name == NULL) {
+ *path_r = t_strconcat(path, "/"MBOX_INDEX_DIR_NAME, NULL);
+ return 1;
+ }
+
+ p = strrchr(path, '/');
+ if (p == NULL)
+ return 0;
+
+ *path_r = t_strconcat(t_strdup_until(path, p),
+ "/"MBOX_INDEX_DIR_NAME"/", p+1, NULL);
+ break;
+ case MAILBOX_LIST_PATH_TYPE_DIR:
+ case MAILBOX_LIST_PATH_TYPE_ALT_DIR:
+ case MAILBOX_LIST_PATH_TYPE_MAILBOX:
+ case MAILBOX_LIST_PATH_TYPE_ALT_MAILBOX:
+ case MAILBOX_LIST_PATH_TYPE_INDEX_PRIVATE:
+ *path_r = path;
+ break;
+ }
+ return 1;
+}
+
+static struct mail_storage *mbox_storage_alloc(void)
+{
+ struct mbox_storage *storage;
+ pool_t pool;
+
+ pool = pool_alloconly_create("mbox storage", 512+256);
+ storage = p_new(pool, struct mbox_storage, 1);
+ storage->storage = mbox_storage;
+ storage->storage.pool = pool;
+ return &storage->storage;
+}
+
+static int
+mbox_storage_create(struct mail_storage *_storage, struct mail_namespace *ns,
+ const char **error_r)
+{
+ struct mbox_storage *storage = MBOX_STORAGE(_storage);
+ struct stat st;
+ const char *dir;
+
+ if (master_service_get_client_limit(master_service) > 1) {
+ /* we can't handle locking related problems. */
+ *error_r = "mbox requires client_limit=1 for service";
+ return -1;
+ }
+
+ storage->set = mail_namespace_get_driver_settings(ns, _storage);
+
+ if (mailbox_list_get_root_path(ns->list, MAILBOX_LIST_PATH_TYPE_INDEX, &dir)) {
+ _storage->temp_path_prefix = p_strconcat(_storage->pool, dir,
+ "/", mailbox_list_get_temp_prefix(ns->list), NULL);
+ }
+ if (stat(ns->list->set.root_dir, &st) == 0 && !S_ISDIR(st.st_mode)) {
+ *error_r = t_strdup_printf(
+ "mbox root directory can't be a file: %s "
+ "(http://wiki2.dovecot.org/MailLocation/Mbox)",
+ ns->list->set.root_dir);
+ return -1;
+ }
+ return 0;
+}
+
+static void mbox_storage_get_list_settings(const struct mail_namespace *ns,
+ struct mailbox_list_settings *set)
+{
+ if (set->layout == NULL)
+ set->layout = MAILBOX_LIST_NAME_FS;
+ if (set->subscription_fname == NULL)
+ set->subscription_fname = MBOX_SUBSCRIPTION_FILE_NAME;
+
+ if (set->inbox_path == NULL &&
+ strcasecmp(set->layout, MAILBOX_LIST_NAME_FS) == 0) {
+ set->inbox_path = t_strconcat(set->root_dir, "/inbox", NULL);
+ e_debug(ns->user->event, "mbox: INBOX defaulted to %s", set->inbox_path);
+ }
+}
+
+static bool mbox_is_file(const char *path, const char *name, bool debug)
+{
+ struct stat st;
+
+ if (stat(path, &st) < 0) {
+ if (debug) {
+ i_debug("mbox autodetect: %s: stat(%s) failed: %m",
+ name, path);
+ }
+ return FALSE;
+ }
+ if (S_ISDIR(st.st_mode)) {
+ if (debug) {
+ i_debug("mbox autodetect: %s: is a directory (%s)",
+ name, path);
+ }
+ return FALSE;
+ }
+ if (access(path, R_OK|W_OK) < 0) {
+ if (debug) {
+ i_debug("mbox autodetect: %s: no R/W access (%s)",
+ name, path);
+ }
+ return FALSE;
+ }
+
+ if (debug)
+ i_debug("mbox autodetect: %s: yes (%s)", name, path);
+ return TRUE;
+}
+
+static bool mbox_is_dir(const char *path, const char *name, bool debug)
+{
+ struct stat st;
+
+ if (stat(path, &st) < 0) {
+ if (debug) {
+ i_debug("mbox autodetect: %s: stat(%s) failed: %m",
+ name, path);
+ }
+ return FALSE;
+ }
+ if (!S_ISDIR(st.st_mode)) {
+ if (debug) {
+ i_debug("mbox autodetect: %s: is not a directory (%s)",
+ name, path);
+ }
+ return FALSE;
+ }
+ if (access(path, R_OK|W_OK|X_OK) < 0) {
+ if (debug) {
+ i_debug("mbox autodetect: %s: no R/W/X access (%s)",
+ name, path);
+ }
+ return FALSE;
+ }
+
+ if (debug)
+ i_debug("mbox autodetect: %s: yes (%s)", name, path);
+ return TRUE;
+}
+
+static bool mbox_storage_is_root_dir(const char *dir, bool debug)
+{
+ if (mbox_is_dir(t_strconcat(dir, "/"MBOX_INDEX_DIR_NAME, NULL),
+ "has "MBOX_INDEX_DIR_NAME"/", debug))
+ return TRUE;
+ if (mbox_is_file(t_strconcat(dir, "/inbox", NULL), "has inbox", debug))
+ return TRUE;
+ if (mbox_is_file(t_strconcat(dir, "/mbox", NULL), "has mbox", debug))
+ return TRUE;
+ return FALSE;
+}
+
+static const char *mbox_storage_find_root_dir(const struct mail_namespace *ns)
+{
+ bool debug = ns->mail_set->mail_debug;
+ const char *home, *path;
+
+ if (ns->owner == NULL ||
+ mail_user_get_home(ns->owner, &home) <= 0) {
+ if (debug)
+ i_debug("maildir: Home directory not set");
+ home = "";
+ }
+
+ path = t_strconcat(home, "/mail", NULL);
+ if (mbox_storage_is_root_dir(path, debug))
+ return path;
+
+ path = t_strconcat(home, "/Mail", NULL);
+ if (mbox_storage_is_root_dir(path, debug))
+ return path;
+ return NULL;
+}
+
+static const char *
+mbox_storage_find_inbox_file(const char *user, bool debug)
+{
+ const char *path;
+
+ path = t_strconcat("/var/mail/", user, NULL);
+ if (access(path, R_OK|W_OK) == 0) {
+ if (debug)
+ i_debug("mbox: INBOX exists (%s)", path);
+ return path;
+ }
+ if (debug)
+ i_debug("mbox: INBOX: access(%s, rw) failed: %m", path);
+
+ path = t_strconcat("/var/spool/mail/", user, NULL);
+ if (access(path, R_OK|W_OK) == 0) {
+ if (debug)
+ i_debug("mbox: INBOX exists (%s)", path);
+ return path;
+ }
+ if (debug)
+ i_debug("mbox: INBOX: access(%s, rw) failed: %m", path);
+ return NULL;
+}
+
+static bool mbox_storage_autodetect(const struct mail_namespace *ns,
+ struct mailbox_list_settings *set)
+{
+ bool debug = ns->mail_set->mail_debug;
+ const char *root_dir, *inbox_path;
+
+ root_dir = set->root_dir;
+ inbox_path = set->inbox_path;
+
+ if (root_dir != NULL) {
+ if (inbox_path == NULL &&
+ mbox_is_file(root_dir, "INBOX file", debug)) {
+ /* using location=<INBOX> */
+ inbox_path = root_dir;
+ root_dir = NULL;
+ } else if (!mbox_storage_is_root_dir(root_dir, debug))
+ return FALSE;
+ }
+ if (root_dir == NULL) {
+ root_dir = mbox_storage_find_root_dir(ns);
+ if (root_dir == NULL) {
+ if (debug)
+ i_debug("mbox: couldn't find root dir");
+ return FALSE;
+ }
+ }
+ if (inbox_path == NULL) {
+ inbox_path = mbox_storage_find_inbox_file(ns->user->username,
+ debug);
+ }
+ set->root_dir = root_dir;
+ set->inbox_path = inbox_path;
+
+ mbox_storage_get_list_settings(ns, set);
+ return TRUE;
+}
+
+static bool want_memory_indexes(struct mbox_storage *storage, const char *path)
+{
+ struct stat st;
+
+ if (storage->set->mbox_min_index_size == 0)
+ return FALSE;
+
+ if (stat(path, &st) < 0) {
+ if (errno == ENOENT)
+ st.st_size = 0;
+ else {
+ mail_storage_set_critical(&storage->storage,
+ "stat(%s) failed: %m", path);
+ return FALSE;
+ }
+ }
+ return (uoff_t)st.st_size < storage->set->mbox_min_index_size;
+}
+
+static struct mailbox *
+mbox_mailbox_alloc(struct mail_storage *storage, struct mailbox_list *list,
+ const char *vname, enum mailbox_flags flags)
+{
+ struct mbox_mailbox *mbox;
+ pool_t pool;
+
+ pool = pool_alloconly_create("mbox mailbox", 1024*3);
+ mbox = p_new(pool, struct mbox_mailbox, 1);
+ mbox->box = mbox_mailbox;
+ mbox->box.pool = pool;
+ mbox->box.storage = storage;
+ mbox->box.list = list;
+ mbox->box.mail_vfuncs = &mbox_mail_vfuncs;
+
+ index_storage_mailbox_alloc(&mbox->box, vname, flags, MAIL_INDEX_PREFIX);
+
+ mbox->storage = MBOX_STORAGE(storage);
+ mbox->mbox_fd = -1;
+ mbox->mbox_lock_type = F_UNLCK;
+ mbox->mbox_list_index_ext_id = (uint32_t)-1;
+
+ if (strcmp(mbox->storage->set->mbox_md5, "apop3d") == 0)
+ mbox->md5_v = mbox_md5_apop3d;
+ else if (strcmp(mbox->storage->set->mbox_md5, "all") == 0)
+ mbox->md5_v = mbox_md5_all;
+ else {
+ i_fatal("Invalid mbox_md5 setting: %s",
+ mbox->storage->set->mbox_md5);
+ }
+
+ if ((storage->flags & MAIL_STORAGE_FLAG_KEEP_HEADER_MD5) != 0)
+ mbox->mbox_save_md5 = TRUE;
+ return &mbox->box;
+}
+
+static void mbox_lock_touch_timeout(struct mbox_mailbox *mbox)
+{
+ mbox_dotlock_touch(mbox);
+}
+
+static int
+mbox_mailbox_open_finish(struct mbox_mailbox *mbox, bool move_to_memory)
+{
+ if (index_storage_mailbox_open(&mbox->box, move_to_memory) < 0)
+ return -1;
+
+ mbox->mbox_ext_idx =
+ mail_index_ext_register(mbox->box.index, "mbox",
+ sizeof(mbox->mbox_hdr),
+ sizeof(uint64_t), sizeof(uint64_t));
+ mbox->md5hdr_ext_idx =
+ mail_index_ext_register(mbox->box.index, "header-md5",
+ 0, 16, 1);
+ return 0;
+}
+
+static int mbox_mailbox_open_existing(struct mbox_mailbox *mbox)
+{
+ struct mailbox *box = &mbox->box;
+ const char *rootdir, *box_path = mailbox_get_path(box);
+ bool move_to_memory;
+
+ move_to_memory = want_memory_indexes(mbox->storage, box_path);
+
+ if (box->inbox_any || strcmp(box->name, "INBOX") == 0) {
+ /* if INBOX isn't under the root directory, it's probably in
+ /var/mail and we want to allow privileged dotlocking */
+ rootdir = mailbox_list_get_root_forced(box->list,
+ MAILBOX_LIST_PATH_TYPE_DIR);
+ if (!str_begins(box_path, rootdir))
+ mbox->mbox_privileged_locking = TRUE;
+ }
+ if ((box->flags & MAILBOX_FLAG_KEEP_LOCKED) != 0) {
+ if (mbox_lock(mbox, F_WRLCK, &mbox->mbox_global_lock_id) <= 0)
+ return -1;
+
+ if (mbox->mbox_dotlock != NULL) {
+ mbox->keep_lock_to =
+ timeout_add(MBOX_LOCK_TOUCH_MSECS,
+ mbox_lock_touch_timeout, mbox);
+ }
+ }
+ return mbox_mailbox_open_finish(mbox, move_to_memory);
+}
+
+static bool mbox_storage_is_readonly(struct mailbox *box)
+{
+ struct mbox_mailbox *mbox = MBOX_MAILBOX(box);
+
+ if (index_storage_is_readonly(box))
+ return TRUE;
+
+ if (mbox_is_backend_readonly(mbox)) {
+ /* return read-only only if there are no private flags
+ (that are stored in index files) */
+ if (mailbox_get_private_flags_mask(box) == 0)
+ return TRUE;
+ }
+ return FALSE;
+}
+
+static int mbox_mailbox_open(struct mailbox *box)
+{
+ struct mbox_mailbox *mbox = MBOX_MAILBOX(box);
+ struct stat st;
+ int ret;
+
+ if (box->input != NULL) {
+ i_stream_ref(box->input);
+ mbox->mbox_file_stream = box->input;
+ mbox->backend_readonly = TRUE;
+ mbox->backend_readonly_set = TRUE;
+ mbox->no_mbox_file = TRUE;
+ return mbox_mailbox_open_finish(mbox, FALSE);
+ }
+
+ ret = stat(mailbox_get_path(box), &st);
+ if (ret == 0) {
+ if (!S_ISDIR(st.st_mode))
+ return mbox_mailbox_open_existing(mbox);
+ mail_storage_set_error(box->storage, MAIL_ERROR_NOTFOUND,
+ "Mailbox isn't selectable");
+ return -1;
+ } else if (ENOTFOUND(errno)) {
+ mail_storage_set_error(box->storage, MAIL_ERROR_NOTFOUND,
+ T_MAIL_ERR_MAILBOX_NOT_FOUND(box->vname));
+ return -1;
+ } else if (mail_storage_set_error_from_errno(box->storage)) {
+ return -1;
+ } else {
+ mailbox_set_critical(box,
+ "stat(%s) failed: %m", mailbox_get_path(box));
+ return -1;
+ }
+}
+
+static int
+mbox_mailbox_update(struct mailbox *box, const struct mailbox_update *update)
+{
+ struct mbox_mailbox *mbox = MBOX_MAILBOX(box);
+ int ret = 0;
+
+ if (!box->opened) {
+ if (mailbox_open(box) < 0)
+ return -1;
+ }
+
+ if (update->uid_validity != 0 || update->min_next_uid != 0 ||
+ !guid_128_is_empty(update->mailbox_guid)) {
+ mbox->sync_hdr_update = update;
+ ret = mbox_sync(mbox, MBOX_SYNC_HEADER | MBOX_SYNC_FORCE_SYNC |
+ MBOX_SYNC_REWRITE);
+ mbox->sync_hdr_update = NULL;
+ }
+ if (ret == 0)
+ ret = index_storage_mailbox_update(box, update);
+ return ret;
+}
+
+static int create_inbox(struct mailbox *box)
+{
+ const char *inbox_path;
+ int fd;
+
+ inbox_path = mailbox_get_path(box);
+
+ fd = open(inbox_path, O_RDWR | O_CREAT | O_EXCL, 0660);
+ if (fd == -1 && errno == EACCES) {
+ /* try again with increased privileges */
+ (void)restrict_access_use_priv_gid();
+ fd = open(inbox_path, O_RDWR | O_CREAT | O_EXCL, 0660);
+ restrict_access_drop_priv_gid();
+ }
+ if (fd != -1) {
+ i_close_fd(&fd);
+ return 0;
+ } else if (errno == EACCES) {
+ mailbox_set_critical(box, "%s",
+ mail_error_create_eacces_msg("open", inbox_path));
+ return -1;
+ } else if (errno == EEXIST) {
+ mail_storage_set_error(box->storage, MAIL_ERROR_EXISTS,
+ "Mailbox already exists");
+ return -1;
+ } else {
+ mailbox_set_critical(box,
+ "open(%s, O_CREAT) failed: %m", inbox_path);
+ return -1;
+ }
+}
+
+static int
+mbox_mailbox_create(struct mailbox *box, const struct mailbox_update *update,
+ bool directory)
+{
+ int fd, ret;
+
+ if ((ret = index_storage_mailbox_create(box, directory)) <= 0)
+ return ret;
+
+ if (box->inbox_any) {
+ if (create_inbox(box) < 0)
+ return -1;
+ } else {
+ /* create the mbox file */
+ ret = mailbox_create_fd(box, mailbox_get_path(box),
+ O_RDWR | O_CREAT | O_EXCL, &fd);
+ if (ret < 0)
+ return -1;
+ if (ret == 0) {
+ mail_storage_set_error(box->storage, MAIL_ERROR_EXISTS,
+ "Mailbox already exists");
+ return -1;
+ }
+ i_close_fd(&fd);
+ }
+ return update == NULL ? 0 : mbox_mailbox_update(box, update);
+}
+
+static void mbox_mailbox_close(struct mailbox *box)
+{
+ struct mbox_mailbox *mbox = MBOX_MAILBOX(box);
+ const struct mail_index_header *hdr;
+ enum mbox_sync_flags sync_flags = 0;
+
+ if (mbox->mbox_stream != NULL &&
+ istream_raw_mbox_is_corrupted(mbox->mbox_stream)) {
+ /* clear the corruption by forcing a full resync */
+ sync_flags |= MBOX_SYNC_UNDIRTY | MBOX_SYNC_FORCE_SYNC;
+ }
+
+ if (box->view != NULL) {
+ hdr = mail_index_get_header(box->view);
+ if ((hdr->flags & MAIL_INDEX_HDR_FLAG_HAVE_DIRTY) != 0 &&
+ !mbox_is_backend_readonly(mbox)) {
+ /* we've done changes to mbox which haven't been
+ written yet. do it now. */
+ sync_flags |= MBOX_SYNC_REWRITE;
+ }
+ }
+ if (sync_flags != 0 && !mbox->invalid_mbox_file)
+ (void)mbox_sync(mbox, sync_flags);
+
+ if (mbox->mbox_global_lock_id != 0)
+ mbox_unlock(mbox, mbox->mbox_global_lock_id);
+ timeout_remove(&mbox->keep_lock_to);
+
+ mbox_file_close(mbox);
+ i_stream_destroy(&mbox->mbox_file_stream);
+
+ index_storage_mailbox_close(box);
+}
+
+static int
+mbox_mailbox_get_guid(struct mbox_mailbox *mbox, guid_128_t guid_r)
+{
+ const char *errstr;
+
+ if (mail_index_is_in_memory(mbox->box.index)) {
+ errstr = "Mailbox GUIDs are not permanent without index files";
+ if (mbox->storage->set->mbox_min_index_size != 0) {
+ errstr = t_strconcat(errstr,
+ " (mbox_min_index_size is non-zero)", NULL);
+ }
+ mail_storage_set_error(mbox->box.storage,
+ MAIL_ERROR_NOTPOSSIBLE, errstr);
+ return -1;
+ }
+ if (mbox_sync_header_refresh(mbox) < 0)
+ return -1;
+
+ if (!guid_128_is_empty(mbox->mbox_hdr.mailbox_guid)) {
+ /* we have the GUID */
+ } else if (mbox_file_open(mbox) < 0)
+ return -1;
+ else if (mbox->backend_readonly) {
+ mail_storage_set_error(mbox->box.storage, MAIL_ERROR_PERM,
+ "Can't set mailbox GUID to a read-only mailbox");
+ return -1;
+ } else {
+ /* create another mailbox and sync */
+ struct mailbox *box2;
+ struct mbox_mailbox *mbox2;
+ int ret;
+
+ i_assert(mbox->mbox_lock_type == F_UNLCK);
+ box2 = mailbox_alloc(mbox->box.list, mbox->box.vname, 0);
+ ret = mailbox_sync(box2, 0);
+ mbox2 = MBOX_MAILBOX(box2);
+ memcpy(guid_r, mbox2->mbox_hdr.mailbox_guid, GUID_128_SIZE);
+ mailbox_free(&box2);
+ return ret;
+ }
+ memcpy(guid_r, mbox->mbox_hdr.mailbox_guid, GUID_128_SIZE);
+ return 0;
+}
+
+static int
+mbox_mailbox_get_metadata(struct mailbox *box,
+ enum mailbox_metadata_items items,
+ struct mailbox_metadata *metadata_r)
+{
+ struct mbox_mailbox *mbox = MBOX_MAILBOX(box);
+
+ if (index_mailbox_get_metadata(box, items, metadata_r) < 0)
+ return -1;
+ if ((items & MAILBOX_METADATA_GUID) != 0) {
+ if (mbox_mailbox_get_guid(mbox, metadata_r->guid) < 0)
+ return -1;
+ }
+ return 0;
+}
+
+static void mbox_notify_changes(struct mailbox *box)
+{
+ struct mbox_mailbox *mbox = MBOX_MAILBOX(box);
+
+ if (box->notify_callback == NULL)
+ mailbox_watch_remove_all(box);
+ else if (!mbox->no_mbox_file)
+ mailbox_watch_add(box, mailbox_get_path(box));
+}
+
+static bool
+mbox_is_internal_name(struct mailbox_list *list ATTR_UNUSED,
+ const char *name)
+{
+ size_t len;
+
+ /* don't allow *.lock files/dirs */
+ len = strlen(name);
+ if (len > 5 && strcmp(name+len-5, ".lock") == 0)
+ return TRUE;
+
+ return strcmp(name, MBOX_INDEX_DIR_NAME) == 0;
+}
+
+static void mbox_storage_add_list(struct mail_storage *storage,
+ struct mailbox_list *list)
+{
+ struct mbox_mailbox_list *mlist;
+
+ mlist = p_new(list->pool, struct mbox_mailbox_list, 1);
+ mlist->module_ctx.super = list->v;
+ mlist->set = mail_namespace_get_driver_settings(list->ns, storage);
+
+ if (*list->set.maildir_name == '\0') {
+ /* have to use .imap/ directories */
+ list->v.get_path = mbox_list_get_path;
+ }
+ list->v.is_internal_name = mbox_is_internal_name;
+
+ MODULE_CONTEXT_SET(list, mbox_mailbox_list_module, mlist);
+}
+
+static struct mailbox_transaction_context *
+mbox_transaction_begin(struct mailbox *box,
+ enum mailbox_transaction_flags flags,
+ const char *reason)
+{
+ struct mbox_mailbox *mbox = MBOX_MAILBOX(box);
+ struct mbox_transaction_context *mt;
+
+ if ((flags & MAILBOX_TRANSACTION_FLAG_EXTERNAL) != 0)
+ mbox->external_transactions++;
+
+ mt = i_new(struct mbox_transaction_context, 1);
+ index_transaction_init(&mt->t, box, flags, reason);
+ return &mt->t;
+}
+
+static void
+mbox_transaction_unlock(struct mailbox *box, unsigned int lock_id1,
+ unsigned int lock_id2)
+{
+ struct mbox_mailbox *mbox = MBOX_MAILBOX(box);
+
+ if (lock_id1 != 0)
+ mbox_unlock(mbox, lock_id1);
+ if (lock_id2 != 0)
+ mbox_unlock(mbox, lock_id2);
+ if (mbox->mbox_global_lock_id == 0) {
+ i_assert(mbox->box.transaction_count > 0);
+ i_assert(mbox->box.transaction_count > 1 ||
+ mbox->external_transactions > 0 ||
+ mbox->mbox_lock_type == F_UNLCK);
+ } else {
+ /* mailbox opened with MAILBOX_FLAG_KEEP_LOCKED */
+ i_assert(mbox->mbox_lock_type == F_WRLCK);
+ }
+}
+
+static int
+mbox_transaction_commit(struct mailbox_transaction_context *t,
+ struct mail_transaction_commit_changes *changes_r)
+{
+ struct mbox_transaction_context *mt = MBOX_TRANSCTX(t);
+ struct mailbox *box = t->box;
+ struct mbox_mailbox *mbox = MBOX_MAILBOX(box);
+ unsigned int read_lock_id = mt->read_lock_id;
+ unsigned int write_lock_id = mt->write_lock_id;
+ int ret;
+
+ if ((t->flags & MAILBOX_TRANSACTION_FLAG_EXTERNAL) != 0) {
+ i_assert(mbox->external_transactions > 0);
+ mbox->external_transactions--;
+ }
+
+ ret = index_transaction_commit(t, changes_r);
+ mbox_transaction_unlock(box, read_lock_id, write_lock_id);
+ return ret;
+}
+
+static void
+mbox_transaction_rollback(struct mailbox_transaction_context *t)
+{
+ struct mbox_transaction_context *mt = MBOX_TRANSCTX(t);
+ struct mailbox *box = t->box;
+ struct mbox_mailbox *mbox = MBOX_MAILBOX(box);
+ unsigned int read_lock_id = mt->read_lock_id;
+ unsigned int write_lock_id = mt->write_lock_id;
+
+ if ((t->flags & MAILBOX_TRANSACTION_FLAG_EXTERNAL) != 0) {
+ i_assert(mbox->external_transactions > 0);
+ mbox->external_transactions--;
+ }
+
+ index_transaction_rollback(t);
+ mbox_transaction_unlock(box, read_lock_id, write_lock_id);
+}
+
+bool mbox_is_backend_readonly(struct mbox_mailbox *mbox)
+{
+ if (!mbox->backend_readonly_set) {
+ mbox->backend_readonly_set = TRUE;
+ if (access(mailbox_get_path(&mbox->box), R_OK|W_OK) < 0 &&
+ errno == EACCES)
+ mbox->backend_readonly = TRUE;
+ }
+ return mbox->backend_readonly;
+}
+
+struct mail_storage mbox_storage = {
+ .name = MBOX_STORAGE_NAME,
+ .class_flags = MAIL_STORAGE_CLASS_FLAG_MAILBOX_IS_FILE |
+ MAIL_STORAGE_CLASS_FLAG_OPEN_STREAMS |
+ MAIL_STORAGE_CLASS_FLAG_HAVE_MAIL_GUIDS,
+ .event_category = &event_category_mbox,
+
+ .v = {
+ mbox_get_setting_parser_info,
+ mbox_storage_alloc,
+ mbox_storage_create,
+ index_storage_destroy,
+ mbox_storage_add_list,
+ mbox_storage_get_list_settings,
+ mbox_storage_autodetect,
+ mbox_mailbox_alloc,
+ NULL,
+ NULL,
+ }
+};
+
+struct mailbox mbox_mailbox = {
+ .v = {
+ mbox_storage_is_readonly,
+ index_storage_mailbox_enable,
+ index_storage_mailbox_exists,
+ mbox_mailbox_open,
+ mbox_mailbox_close,
+ index_storage_mailbox_free,
+ mbox_mailbox_create,
+ mbox_mailbox_update,
+ index_storage_mailbox_delete,
+ index_storage_mailbox_rename,
+ index_storage_get_status,
+ mbox_mailbox_get_metadata,
+ index_storage_set_subscribed,
+ index_storage_attribute_set,
+ index_storage_attribute_get,
+ index_storage_attribute_iter_init,
+ index_storage_attribute_iter_next,
+ index_storage_attribute_iter_deinit,
+ mbox_list_index_has_changed,
+ mbox_list_index_update_sync,
+ mbox_storage_sync_init,
+ index_mailbox_sync_next,
+ index_mailbox_sync_deinit,
+ NULL,
+ mbox_notify_changes,
+ mbox_transaction_begin,
+ mbox_transaction_commit,
+ mbox_transaction_rollback,
+ NULL,
+ index_mail_alloc,
+ index_storage_search_init,
+ index_storage_search_deinit,
+ index_storage_search_next_nonblock,
+ index_storage_search_next_update_seq,
+ index_storage_search_next_match_mail,
+ mbox_save_alloc,
+ mbox_save_begin,
+ mbox_save_continue,
+ mbox_save_finish,
+ mbox_save_cancel,
+ mail_storage_copy,
+ mbox_transaction_save_commit_pre,
+ mbox_transaction_save_commit_post,
+ mbox_transaction_save_rollback,
+ index_storage_is_inconsistent
+ }
+};
diff --git a/src/lib-storage/index/mbox/mbox-storage.h b/src/lib-storage/index/mbox/mbox-storage.h
new file mode 100644
index 0000000..e35a795
--- /dev/null
+++ b/src/lib-storage/index/mbox/mbox-storage.h
@@ -0,0 +1,115 @@
+#ifndef MBOX_STORAGE_H
+#define MBOX_STORAGE_H
+
+#include "index-storage.h"
+#include "mbox-settings.h"
+#include "mbox-md5.h"
+
+/* Padding to leave in X-Keywords header when rewriting mbox */
+#define MBOX_HEADER_PADDING 50
+/* Don't write Content-Length header unless it's value is larger than this. */
+#define MBOX_MIN_CONTENT_LENGTH_SIZE 1024
+
+#define MBOX_STORAGE_NAME "mbox"
+#define MBOX_SUBSCRIPTION_FILE_NAME ".subscriptions"
+#define MBOX_INDEX_DIR_NAME ".imap"
+#define MBOX_UIDVALIDITY_FNAME "dovecot-uidvalidity"
+
+struct mbox_index_header {
+ uint64_t sync_size;
+ uint32_t sync_mtime;
+ uint8_t dirty_flag;
+ uint8_t unused[3];
+ guid_128_t mailbox_guid;
+};
+
+struct mbox_list_index_record {
+ uint32_t mtime;
+ uint32_t size;
+};
+
+struct mbox_storage {
+ struct mail_storage storage;
+
+ const struct mbox_settings *set;
+ enum mbox_lock_type *read_locks;
+ enum mbox_lock_type *write_locks;
+ bool lock_settings_initialized:1;
+};
+
+struct mbox_mailbox {
+ struct mailbox box;
+ struct mbox_storage *storage;
+
+ int mbox_fd;
+ struct istream *mbox_stream, *mbox_file_stream;
+ int mbox_lock_type;
+ dev_t mbox_dev;
+ ino_t mbox_ino;
+ unsigned int mbox_excl_locks, mbox_shared_locks;
+ struct dotlock *mbox_dotlock;
+ unsigned int mbox_lock_id, mbox_global_lock_id;
+ struct timeout *keep_lock_to;
+ bool mbox_writeonly;
+ unsigned int external_transactions;
+
+ uint32_t mbox_ext_idx, md5hdr_ext_idx, mbox_list_index_ext_id;
+ struct mbox_index_header mbox_hdr;
+ const struct mailbox_update *sync_hdr_update;
+
+ struct mbox_md5_vfuncs md5_v;
+
+ bool no_mbox_file:1;
+ bool invalid_mbox_file:1;
+ bool mbox_broken_offsets:1;
+ bool mbox_save_md5:1;
+ bool mbox_dotlocked:1;
+ bool mbox_used_privileges:1;
+ bool mbox_privileged_locking:1;
+ bool syncing:1;
+ bool backend_readonly:1;
+ bool backend_readonly_set:1;
+};
+
+struct mbox_transaction_context {
+ struct mailbox_transaction_context t;
+ union mail_index_transaction_module_context module_ctx;
+
+ unsigned int read_lock_id;
+ unsigned int write_lock_id;
+};
+
+#define MBOX_STORAGE(s) container_of(s, struct mbox_storage, storage)
+#define MBOX_MAILBOX(s) container_of(s, struct mbox_mailbox, box)
+#define MBOX_TRANSCTX(s) container_of(s, struct mbox_transaction_context, t)
+
+extern struct mail_vfuncs mbox_mail_vfuncs;
+extern const char *mbox_hide_headers[], *mbox_save_drop_headers[];
+extern unsigned int mbox_hide_headers_count, mbox_save_drop_headers_count;
+
+void mbox_set_syscall_error(struct mbox_mailbox *mbox, const char *function);
+void mbox_istream_set_syscall_error(struct mbox_mailbox *mbox,
+ struct istream *input,
+ const char *function);
+void mbox_ostream_set_syscall_error(struct mbox_mailbox *mbox,
+ struct ostream *output,
+ const char *function);
+
+struct mailbox_sync_context *
+mbox_storage_sync_init(struct mailbox *box, enum mailbox_sync_flags flags);
+
+struct mail_save_context *
+mbox_save_alloc(struct mailbox_transaction_context *_t);
+int mbox_save_begin(struct mail_save_context *ctx, struct istream *input);
+int mbox_save_continue(struct mail_save_context *ctx);
+int mbox_save_finish(struct mail_save_context *ctx);
+void mbox_save_cancel(struct mail_save_context *ctx);
+
+int mbox_transaction_save_commit_pre(struct mail_save_context *ctx);
+void mbox_transaction_save_commit_post(struct mail_save_context *ctx,
+ struct mail_index_transaction_commit_result *result);
+void mbox_transaction_save_rollback(struct mail_save_context *ctx);
+
+bool mbox_is_backend_readonly(struct mbox_mailbox *mbox);
+
+#endif
diff --git a/src/lib-storage/index/mbox/mbox-sync-list-index.c b/src/lib-storage/index/mbox/mbox-sync-list-index.c
new file mode 100644
index 0000000..934b7ea
--- /dev/null
+++ b/src/lib-storage/index/mbox/mbox-sync-list-index.c
@@ -0,0 +1,109 @@
+/* Copyright (c) 2013-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "mbox-storage.h"
+#include "mbox-sync-private.h"
+
+static unsigned int
+mbox_list_get_ext_id(struct mbox_mailbox *mbox,
+ struct mail_index_view *view)
+{
+ if (mbox->mbox_list_index_ext_id == (uint32_t)-1) {
+ mbox->mbox_list_index_ext_id =
+ mail_index_ext_register(mail_index_view_get_index(view),
+ "mbox", 0,
+ sizeof(struct mbox_list_index_record),
+ sizeof(uint32_t));
+ }
+ return mbox->mbox_list_index_ext_id;
+}
+
+int mbox_list_index_has_changed(struct mailbox *box,
+ struct mail_index_view *list_view,
+ uint32_t seq, bool quick, const char **reason_r)
+{
+ struct mbox_mailbox *mbox = MBOX_MAILBOX(box);
+ const struct mbox_list_index_record *rec;
+ const void *data;
+ const char *path;
+ struct stat st;
+ uint32_t ext_id;
+ bool expunged;
+ int ret;
+
+ ret = index_storage_list_index_has_changed(box, list_view, seq,
+ quick, reason_r);
+ if (ret != 0 || box->storage->set->mailbox_list_index_very_dirty_syncs)
+ return ret;
+
+ ext_id = mbox_list_get_ext_id(mbox, list_view);
+ mail_index_lookup_ext(list_view, seq, ext_id, &data, &expunged);
+ rec = data;
+
+ if (rec == NULL) {
+ *reason_r = "mbox record is missing";
+ return 1;
+ } else if (expunged) {
+ *reason_r = "mbox record is expunged";
+ return 1;
+ } else if (rec->mtime == 0) {
+ /* not synced */
+ *reason_r = "mbox record mtime=0";
+ return 1;
+ }
+
+ ret = mailbox_get_path_to(box, MAILBOX_LIST_PATH_TYPE_MAILBOX, &path);
+ if (ret < 0)
+ return ret;
+ i_assert(ret > 0);
+
+ if (stat(path, &st) < 0) {
+ mailbox_set_critical(box, "stat(%s) failed: %m", path);
+ return -1;
+ }
+ if ((time_t)rec->mtime != st.st_mtime) {
+ *reason_r = t_strdup_printf(
+ "mbox record mtime changed %u != %"PRIdTIME_T,
+ rec->mtime, st.st_mtime);
+ return 1;
+ }
+ uint32_t new_size = (uint32_t)(st.st_size & 0xffffffffU);
+ if (rec->size != new_size) {
+ *reason_r = t_strdup_printf("mbox record size changed %u != %u",
+ rec->size, new_size);
+ return 1;
+ }
+ return 0;
+}
+
+void mbox_list_index_update_sync(struct mailbox *box,
+ struct mail_index_transaction *trans,
+ uint32_t seq)
+{
+ struct mbox_mailbox *mbox = MBOX_MAILBOX(box);
+ struct mail_index_view *list_view;
+ const struct mbox_index_header *mhdr = &mbox->mbox_hdr;
+ const struct mbox_list_index_record *old_rec;
+ struct mbox_list_index_record new_rec;
+ const void *data;
+ uint32_t ext_id;
+ bool expunged;
+
+ index_storage_list_index_update_sync(box, trans, seq);
+
+ /* get the current record */
+ list_view = mail_index_transaction_get_view(trans);
+ ext_id = mbox_list_get_ext_id(mbox, list_view);
+ mail_index_lookup_ext(list_view, seq, ext_id, &data, &expunged);
+ if (expunged)
+ return;
+ old_rec = data;
+
+ i_zero(&new_rec);
+ new_rec.mtime = mhdr->sync_mtime;
+ new_rec.size = mhdr->sync_size & 0xffffffffU;
+
+ if (old_rec == NULL ||
+ memcmp(old_rec, &new_rec, sizeof(*old_rec)) != 0)
+ mail_index_update_ext(trans, seq, ext_id, &new_rec, NULL);
+}
diff --git a/src/lib-storage/index/mbox/mbox-sync-parse.c b/src/lib-storage/index/mbox/mbox-sync-parse.c
new file mode 100644
index 0000000..bbc2da2
--- /dev/null
+++ b/src/lib-storage/index/mbox/mbox-sync-parse.c
@@ -0,0 +1,616 @@
+/* Copyright (c) 2004-2018 Dovecot authors, see the included COPYING file */
+
+/* MD5 header summing logic was pretty much copy&pasted from popa3d by
+ Solar Designer */
+
+#include "lib.h"
+#include "ioloop.h"
+#include "array.h"
+#include "buffer.h"
+#include "istream.h"
+#include "str.h"
+#include "write-full.h"
+#include "message-parser.h"
+#include "mail-index.h"
+#include "mbox-storage.h"
+#include "mbox-md5.h"
+#include "mbox-sync-private.h"
+
+
+#define IS_LWSP_LF(c) (IS_LWSP(c) || (c) == '\n')
+
+struct mbox_sync_header_func {
+ const char *header;
+ bool (*func)(struct mbox_sync_mail_context *ctx,
+ struct message_header_line *hdr);
+};
+
+struct mbox_flag_type mbox_status_flags[] = {
+ { 'R', MAIL_SEEN },
+ { 'O', MBOX_NONRECENT_KLUDGE },
+ { 0, 0 }
+};
+
+struct mbox_flag_type mbox_xstatus_flags[] = {
+ { 'A', MAIL_ANSWERED },
+ { 'F', MAIL_FLAGGED },
+ { 'T', MAIL_DRAFT },
+ { 'D', MAIL_DELETED },
+ { 0, 0 }
+};
+
+static void parse_trailing_whitespace(struct mbox_sync_mail_context *ctx,
+ struct message_header_line *hdr)
+{
+ size_t i, space = 0;
+
+ /* the value may contain newlines. we can't count whitespace before
+ and after it as a single contiguous whitespace block, as that may
+ get us into situation where removing whitespace goes eg.
+ " \n \n" -> " \n\n" which would then be treated as end of headers.
+
+ that could probably be avoided by being careful, but as newlines
+ should never be there (we don't generate them), it's not worth the
+ trouble. */
+
+ for (i = hdr->full_value_len; i > 0; i--) {
+ if (!IS_LWSP(hdr->full_value[i-1]))
+ break;
+ space++;
+ }
+
+ if ((ssize_t)space > ctx->mail.space) {
+ i_assert(space != 0);
+ ctx->mail.offset = ctx->hdr_offset + str_len(ctx->header) + i;
+ ctx->mail.space = space;
+ }
+}
+
+static enum mail_flags mbox_flag_find(struct mbox_flag_type *flags, char chr)
+{
+ int i;
+
+ for (i = 0; flags[i].chr != 0; i++) {
+ if (flags[i].chr == chr)
+ return flags[i].flag;
+ }
+
+ return 0;
+}
+
+static bool parse_status_flags(struct mbox_sync_mail_context *ctx,
+ struct message_header_line *hdr,
+ struct mbox_flag_type *flags_list)
+{
+ enum mail_flags flag;
+ size_t i;
+ bool duplicates = FALSE;
+
+ ctx->mail.flags ^= MBOX_NONRECENT_KLUDGE;
+ for (i = 0; i < hdr->full_value_len; i++) {
+ flag = mbox_flag_find(flags_list, hdr->full_value[i]);
+ if ((ctx->mail.flags & flag) != 0)
+ duplicates = TRUE;
+ else
+ ctx->mail.flags |= flag;
+ }
+ ctx->mail.flags ^= MBOX_NONRECENT_KLUDGE;
+ return duplicates;
+}
+
+static bool parse_status(struct mbox_sync_mail_context *ctx,
+ struct message_header_line *hdr)
+{
+ if (parse_status_flags(ctx, hdr, mbox_status_flags))
+ ctx->mail.status_broken = TRUE;
+ ctx->hdr_pos[MBOX_HDR_STATUS] = str_len(ctx->header);
+ return TRUE;
+}
+
+static bool parse_x_status(struct mbox_sync_mail_context *ctx,
+ struct message_header_line *hdr)
+{
+ if (parse_status_flags(ctx, hdr, mbox_xstatus_flags))
+ ctx->mail.xstatus_broken = TRUE;
+ ctx->hdr_pos[MBOX_HDR_X_STATUS] = str_len(ctx->header);
+ return TRUE;
+}
+
+static void
+parse_imap_keywords_list(struct mbox_sync_mail_context *ctx,
+ struct message_header_line *hdr, size_t pos)
+{
+ struct mailbox *box = &ctx->sync_ctx->mbox->box;
+ struct index_mailbox_context *ibox = INDEX_STORAGE_CONTEXT(box);
+ const char *keyword, *error;
+ size_t keyword_start;
+ unsigned int idx, count;
+
+ count = 0;
+ while (pos < hdr->full_value_len) {
+ if (IS_LWSP_LF(hdr->full_value[pos])) {
+ pos++;
+ continue;
+ }
+
+ /* read the keyword */
+ keyword_start = pos;
+ for (; pos < hdr->full_value_len; pos++) {
+ if (IS_LWSP_LF(hdr->full_value[pos]))
+ break;
+ }
+
+ /* add it to index's keyword list if it's not there already */
+ keyword = t_strndup(hdr->full_value + keyword_start,
+ pos - keyword_start);
+ if (mailbox_keyword_is_valid(&ctx->sync_ctx->mbox->box,
+ keyword, &error)) {
+ mail_index_keyword_lookup_or_create(box->index,
+ keyword, &idx);
+ }
+ count++;
+ }
+
+ if (count != array_count(ibox->keyword_names)) {
+ /* need to update this list */
+ ctx->imapbase_rewrite = TRUE;
+ ctx->need_rewrite = TRUE;
+ }
+}
+
+static bool parse_x_imap_base(struct mbox_sync_mail_context *ctx,
+ struct message_header_line *hdr)
+{
+ size_t i, j, uid_last_pos;
+ uint32_t uid_validity, uid_last;
+
+ if (ctx->seq != 1 || ctx->seen_imapbase ||
+ ctx->sync_ctx->renumber_uids) {
+ /* Valid only in first message */
+ return FALSE;
+ }
+
+ /* <uid-validity> 10x<uid-last> */
+ for (i = 0, uid_validity = 0; i < hdr->full_value_len; i++) {
+ if (hdr->full_value[i] < '0' || hdr->full_value[i] > '9') {
+ if (hdr->full_value[i] != ' ')
+ return FALSE;
+ break;
+ }
+ uid_validity = uid_validity * 10 + (hdr->full_value[i] - '0');
+ }
+
+ if (uid_validity == 0) {
+ /* broken */
+ return FALSE;
+ }
+
+ for (; i < hdr->full_value_len; i++) {
+ if (!IS_LWSP_LF(hdr->full_value[i]))
+ break;
+ }
+ uid_last_pos = i;
+
+ for (uid_last = 0, j = 0; i < hdr->full_value_len; i++, j++) {
+ if (hdr->full_value[i] < '0' || hdr->full_value[i] > '9') {
+ if (!IS_LWSP_LF(hdr->full_value[i]))
+ return FALSE;
+ break;
+ }
+ uid_last = uid_last * 10 + (hdr->full_value[i] - '0');
+ }
+
+ if (j != 10 ||
+ hdr->full_value_offset != ctx->hdr_offset + str_len(ctx->header)) {
+ /* uid-last field must be exactly 10 characters to make
+ rewriting it easier. also don't try to do this if some
+ headers have been removed */
+ ctx->imapbase_rewrite = TRUE;
+ ctx->need_rewrite = TRUE;
+ } else {
+ ctx->last_uid_value_start_pos = uid_last_pos;
+ ctx->sync_ctx->base_uid_last_offset =
+ hdr->full_value_offset + uid_last_pos;
+ }
+
+ if (ctx->sync_ctx->base_uid_validity == 0) {
+ /* first time parsing this (ie. we're not rewriting).
+ save the values. */
+ ctx->sync_ctx->base_uid_validity = uid_validity;
+ ctx->sync_ctx->base_uid_last = uid_last;
+
+ if (ctx->sync_ctx->next_uid-1 <= uid_last) {
+ /* new messages have been added since our last sync.
+ just update our internal next_uid. */
+ ctx->sync_ctx->next_uid = uid_last+1;
+ } else {
+ /* we need to rewrite the next-uid */
+ ctx->need_rewrite = TRUE;
+ }
+ i_assert(ctx->sync_ctx->next_uid > ctx->sync_ctx->prev_msg_uid);
+ }
+
+ ctx->hdr_pos[MBOX_HDR_X_IMAPBASE] = str_len(ctx->header);
+ ctx->seen_imapbase = TRUE;
+
+ T_BEGIN {
+ parse_imap_keywords_list(ctx, hdr, i);
+ } T_END;
+ parse_trailing_whitespace(ctx, hdr);
+ return TRUE;
+}
+
+static bool parse_x_imap(struct mbox_sync_mail_context *ctx,
+ struct message_header_line *hdr)
+{
+ if (!parse_x_imap_base(ctx, hdr))
+ return FALSE;
+
+ /* this is the c-client style "FOLDER INTERNAL DATA" message.
+ skip it. */
+ ctx->mail.pseudo = TRUE;
+ return TRUE;
+}
+
+static bool parse_x_keywords_real(struct mbox_sync_mail_context *ctx,
+ struct message_header_line *hdr)
+{
+ struct mailbox *box = &ctx->sync_ctx->mbox->box;
+ ARRAY_TYPE(keyword_indexes) keyword_list;
+ const unsigned int *list;
+ string_t *keyword;
+ size_t keyword_start;
+ unsigned int i, idx, count;
+ size_t pos;
+
+ if (array_is_created(&ctx->mail.keywords))
+ return FALSE; /* duplicate header, delete */
+
+ /* read keyword indexes to temporary array first */
+ keyword = t_str_new(128);
+ t_array_init(&keyword_list, 16);
+
+ for (pos = 0; pos < hdr->full_value_len; ) {
+ if (IS_LWSP_LF(hdr->full_value[pos])) {
+ pos++;
+ continue;
+ }
+
+ /* read the keyword string */
+ keyword_start = pos;
+ for (; pos < hdr->full_value_len; pos++) {
+ if (IS_LWSP_LF(hdr->full_value[pos]))
+ break;
+ }
+
+ str_truncate(keyword, 0);
+ str_append_data(keyword, hdr->full_value + keyword_start,
+ pos - keyword_start);
+ if (!mail_index_keyword_lookup(box->index, str_c(keyword),
+ &idx)) {
+ /* keyword wasn't found. that means the sent mail
+ originally contained X-Keywords header. Delete it. */
+ return FALSE;
+ }
+
+ /* check that the keyword isn't already added there.
+ we don't want duplicates. */
+ list = array_get(&keyword_list, &count);
+ for (i = 0; i < count; i++) {
+ if (list[i] == idx)
+ break;
+ }
+
+ if (i == count)
+ array_push_back(&keyword_list, &idx);
+ }
+
+ /* once we know how many keywords there are, we can allocate the array
+ from mail_keyword_pool without wasting memory. */
+ if (array_count(&keyword_list) > 0) {
+ p_array_init(&ctx->mail.keywords,
+ ctx->sync_ctx->mail_keyword_pool,
+ array_count(&keyword_list));
+ array_append_array(&ctx->mail.keywords, &keyword_list);
+ }
+
+ ctx->hdr_pos[MBOX_HDR_X_KEYWORDS] = str_len(ctx->header);
+ parse_trailing_whitespace(ctx, hdr);
+ return TRUE;
+}
+
+static bool parse_x_keywords(struct mbox_sync_mail_context *ctx,
+ struct message_header_line *hdr)
+{
+ bool ret;
+
+ T_BEGIN {
+ ret = parse_x_keywords_real(ctx, hdr);
+ } T_END;
+ return ret;
+}
+
+static bool parse_x_uid(struct mbox_sync_mail_context *ctx,
+ struct message_header_line *hdr)
+{
+ uint32_t value = 0;
+ size_t i;
+
+ if (ctx->mail.uid != 0) {
+ /* duplicate */
+ return FALSE;
+ }
+
+ for (i = 0; i < hdr->full_value_len; i++) {
+ if (hdr->full_value[i] < '0' || hdr->full_value[i] > '9')
+ break;
+ value = value*10 + (hdr->full_value[i] - '0');
+ }
+
+ for (; i < hdr->full_value_len; i++) {
+ if (!IS_LWSP_LF(hdr->full_value[i])) {
+ /* broken value */
+ return FALSE;
+ }
+ }
+
+ if (ctx->sync_ctx == NULL) {
+ /* we're in mbox_sync_parse_match_mail().
+ don't do any extra checks. */
+ ctx->mail.uid = value;
+ return TRUE;
+ }
+
+ if (ctx->seq == 1 && !ctx->seen_imapbase) {
+ /* Don't bother allowing X-UID before X-IMAPbase
+ header. c-client doesn't allow it either, and this
+ way the UID doesn't have to be reset if X-IMAPbase
+ header isn't what we expect it to be. */
+ return FALSE;
+ }
+
+ if (value == ctx->sync_ctx->next_uid) {
+ /* X-UID is the next expected one. allow it because
+ we'd just use this UID anyway. X-IMAPbase header
+ still needs to be updated for this. */
+ ctx->sync_ctx->next_uid++;
+ } else if (value > ctx->sync_ctx->next_uid) {
+ /* UID is larger than expected. Don't allow it because
+ incoming mails can contain untrusted X-UID fields,
+ causing possibly DoS if the UIDs get large enough. */
+ ctx->mail.uid_broken = TRUE;
+ return FALSE;
+ }
+
+ if (value <= ctx->sync_ctx->prev_msg_uid) {
+ /* broken - UIDs must be growing */
+ ctx->mail.uid_broken = TRUE;
+ return FALSE;
+ }
+
+ ctx->mail.uid = value;
+ /* if we had multiple X-UID headers, we could have
+ uid_broken=TRUE here. */
+ ctx->mail.uid_broken = FALSE;
+
+ if (ctx->sync_ctx->dest_first_mail && ctx->seq != 1) {
+ /* if we're expunging the first mail, delete this header since
+ otherwise X-IMAPbase header would be added after this, which
+ we don't like */
+ return FALSE;
+ }
+
+ ctx->hdr_pos[MBOX_HDR_X_UID] = str_len(ctx->header);
+ ctx->parsed_uid = value;
+ parse_trailing_whitespace(ctx, hdr);
+ return TRUE;
+}
+
+static bool parse_content_length(struct mbox_sync_mail_context *ctx,
+ struct message_header_line *hdr)
+{
+ uoff_t value = 0;
+ size_t i;
+
+ if (ctx->content_length != UOFF_T_MAX) {
+ /* duplicate */
+ return FALSE;
+ }
+
+ for (i = 0; i < hdr->full_value_len; i++) {
+ if (hdr->full_value[i] < '0' || hdr->full_value[i] > '9')
+ break;
+ value = value*10 + (hdr->full_value[i] - '0');
+ }
+
+ for (; i < hdr->full_value_len; i++) {
+ if (!IS_LWSP_LF(hdr->full_value[i])) {
+ /* broken value */
+ return FALSE;
+ }
+ }
+
+ ctx->content_length = value;
+ return TRUE;
+}
+
+static struct mbox_sync_header_func header_funcs[] = {
+ { "Content-Length", parse_content_length },
+ { "Status", parse_status },
+ { "X-IMAP", parse_x_imap },
+ { "X-IMAPbase", parse_x_imap_base },
+ { "X-Keywords", parse_x_keywords },
+ { "X-Status", parse_x_status },
+ { "X-UID", parse_x_uid }
+};
+
+static int mbox_sync_bsearch_header_func_cmp(const void *p1, const void *p2)
+{
+ const char *key = p1;
+ const struct mbox_sync_header_func *func = p2;
+
+ return strcasecmp(key, func->header);
+}
+
+int mbox_sync_parse_next_mail(struct istream *input,
+ struct mbox_sync_mail_context *ctx)
+{
+ struct mbox_sync_context *sync_ctx = ctx->sync_ctx;
+ struct message_header_parser_ctx *hdr_ctx;
+ struct message_header_line *hdr;
+ struct mbox_sync_header_func *func;
+ struct mbox_md5_context *mbox_md5_ctx;
+ size_t line_start_pos;
+ int i, ret;
+
+ ctx->hdr_offset = ctx->mail.offset;
+ ctx->mail.flags = MAIL_RECENT; /* default to having recent flag */
+
+ ctx->header_first_change = SIZE_MAX;
+ ctx->header_last_change = 0;
+
+ for (i = 0; i < MBOX_HDR_COUNT; i++)
+ ctx->hdr_pos[i] = SIZE_MAX;
+
+ ctx->content_length = UOFF_T_MAX;
+ str_truncate(ctx->header, 0);
+
+ mbox_md5_ctx = ctx->sync_ctx->mbox->md5_v.init();
+
+ line_start_pos = 0;
+ hdr_ctx = message_parse_header_init(input, NULL, 0);
+ while ((ret = message_parse_header_next(hdr_ctx, &hdr)) > 0) {
+ if (hdr->eoh) {
+ ctx->have_eoh = TRUE;
+ break;
+ }
+
+ if (!hdr->continued) {
+ line_start_pos = str_len(ctx->header);
+ str_append(ctx->header, hdr->name);
+ str_append_data(ctx->header, hdr->middle, hdr->middle_len);
+ }
+
+ func = bsearch(hdr->name, header_funcs,
+ N_ELEMENTS(header_funcs), sizeof(*header_funcs),
+ mbox_sync_bsearch_header_func_cmp);
+
+ if (func != NULL) {
+ if (hdr->continues) {
+ hdr->use_full_value = TRUE;
+ continue;
+ }
+
+ if (!func->func(ctx, hdr)) {
+ /* this header is broken, remove it */
+ ctx->need_rewrite = TRUE;
+ str_truncate(ctx->header, line_start_pos);
+ if (ctx->header_first_change == SIZE_MAX) {
+ ctx->header_first_change =
+ line_start_pos;
+ }
+ continue;
+ }
+ buffer_append(ctx->header, hdr->full_value,
+ hdr->full_value_len);
+ } else {
+ ctx->sync_ctx->mbox->md5_v.more(mbox_md5_ctx, hdr);
+ buffer_append(ctx->header, hdr->value,
+ hdr->value_len);
+ }
+ if (!hdr->no_newline) {
+ if (hdr->crlf_newline)
+ str_append_c(ctx->header, '\r');
+ str_append_c(ctx->header, '\n');
+ }
+ }
+ i_assert(ret != 0);
+ message_parse_header_deinit(&hdr_ctx);
+
+ ctx->sync_ctx->mbox->md5_v.finish(mbox_md5_ctx, ctx->hdr_md5_sum);
+
+ if ((ctx->seq == 1 && !ctx->seen_imapbase) ||
+ (ctx->seq > 1 && sync_ctx->dest_first_mail)) {
+ /* missing X-IMAPbase */
+ ctx->need_rewrite = TRUE;
+ if (sync_ctx->base_uid_validity == 0) {
+ /* figure out a new UIDVALIDITY for us. */
+ sync_ctx->base_uid_validity =
+ sync_ctx->hdr->uid_validity != 0 &&
+ !sync_ctx->renumber_uids ?
+ sync_ctx->hdr->uid_validity :
+ I_MAX((uint32_t)ioloop_time, 1);
+ }
+ }
+
+ ctx->body_offset = input->v_offset;
+ if (input->stream_errno != 0) {
+ mbox_sync_set_critical(ctx->sync_ctx, "read(%s) failed: %s",
+ i_stream_get_name(input), i_stream_get_error(input));
+ return -1;
+ }
+ return 0;
+}
+
+bool mbox_sync_parse_match_mail(struct mbox_mailbox *mbox,
+ struct mail_index_view *view, uint32_t seq)
+{
+ struct mbox_sync_mail_context ctx;
+ struct message_header_parser_ctx *hdr_ctx;
+ struct message_header_line *hdr;
+ struct header_func *func;
+ struct mbox_md5_context *mbox_md5_ctx;
+ const void *data;
+ bool expunged;
+ uint32_t uid;
+ int ret;
+
+ /* we only wish to be sure that this mail actually is what we expect
+ it to be. If there's X-UID header and it matches our UID, we use it.
+ Otherwise it could mean that the X-UID header is invalid and it's
+ just not yet been rewritten. In that case use MD5 sum, if it
+ exists. */
+
+ mail_index_lookup_uid(view, seq, &uid);
+ i_zero(&ctx);
+ mbox_md5_ctx = mbox->md5_v.init();
+
+ hdr_ctx = message_parse_header_init(mbox->mbox_stream, NULL, 0);
+ while ((ret = message_parse_header_next(hdr_ctx, &hdr)) > 0) {
+ if (hdr->eoh)
+ break;
+
+ func = bsearch(hdr->name, header_funcs,
+ N_ELEMENTS(header_funcs), sizeof(*header_funcs),
+ mbox_sync_bsearch_header_func_cmp);
+ if (func != NULL) {
+ if (strcasecmp(hdr->name, "X-UID") == 0) {
+ if (hdr->continues) {
+ hdr->use_full_value = TRUE;
+ continue;
+ }
+ (void)parse_x_uid(&ctx, hdr);
+
+ if (ctx.mail.uid == uid)
+ break;
+ }
+ } else {
+ mbox->md5_v.more(mbox_md5_ctx, hdr);
+ }
+ }
+ i_assert(ret != 0);
+ message_parse_header_deinit(&hdr_ctx);
+
+ mbox->md5_v.finish(mbox_md5_ctx, ctx.hdr_md5_sum);
+
+ if (ctx.mail.uid == uid)
+ return TRUE;
+
+ /* match by MD5 sum */
+ mbox->mbox_save_md5 = TRUE;
+
+ mail_index_lookup_ext(view, seq, mbox->md5hdr_ext_idx,
+ &data, &expunged);
+ return data == NULL ? 0 :
+ memcmp(data, ctx.hdr_md5_sum, 16) == 0;
+}
diff --git a/src/lib-storage/index/mbox/mbox-sync-private.h b/src/lib-storage/index/mbox/mbox-sync-private.h
new file mode 100644
index 0000000..7585e4d
--- /dev/null
+++ b/src/lib-storage/index/mbox/mbox-sync-private.h
@@ -0,0 +1,192 @@
+#ifndef MBOX_SYNC_PRIVATE_H
+#define MBOX_SYNC_PRIVATE_H
+
+#include "md5.h"
+#include "mail-index.h"
+
+#include <sys/stat.h>
+
+enum mbox_sync_flags {
+ MBOX_SYNC_HEADER = 0x02,
+ MBOX_SYNC_LOCK_READING = 0x04,
+ MBOX_SYNC_UNDIRTY = 0x08,
+ MBOX_SYNC_REWRITE = 0x10,
+ MBOX_SYNC_FORCE_SYNC = 0x20,
+ MBOX_SYNC_READONLY = 0x40
+};
+
+struct mbox_flag_type {
+ char chr;
+ enum mail_flags flag;
+};
+
+enum header_position {
+ MBOX_HDR_STATUS,
+ MBOX_HDR_X_IMAPBASE,
+ MBOX_HDR_X_KEYWORDS,
+ MBOX_HDR_X_STATUS,
+ MBOX_HDR_X_UID,
+
+ MBOX_HDR_COUNT
+};
+
+/* kludgy. swap MAIL_RECENT with MBOX_NONRECENT_KLUDGE when writing Status
+ header, because 'O' flag means non-recent but internally we want to use
+ recent flag. */
+#define MBOX_NONRECENT_KLUDGE MAIL_RECENT
+
+#define STATUS_FLAGS_MASK (MAIL_SEEN|MBOX_NONRECENT_KLUDGE)
+#define XSTATUS_FLAGS_MASK (MAIL_ANSWERED|MAIL_FLAGGED|MAIL_DRAFT|MAIL_DELETED)
+extern struct mbox_flag_type mbox_status_flags[];
+extern struct mbox_flag_type mbox_xstatus_flags[];
+
+struct mbox_sync_mail {
+ /* uid=0 can mean that this mail describes an expunged area or that
+ this is a pseudo message */
+ uint32_t uid;
+ uint32_t idx_seq;
+
+ ARRAY_TYPE(keyword_indexes) keywords;
+ uint8_t flags;
+
+ bool uid_broken:1;
+ bool expunged:1;
+ bool pseudo:1;
+ bool status_broken:1;
+ bool xstatus_broken:1;
+
+ uoff_t from_offset;
+ uoff_t body_size;
+
+ /* following variables have a bit overloaded functionality:
+
+ a) space <= 0 : offset points to beginning of headers. space is the
+ amount of space missing that is required to be able to rewrite
+ the headers
+ b) space > 0 : offset points to beginning of whitespace that can
+ be removed. space is the amount of data that can be removed from
+ there. note that the message may contain more whitespace
+ elsewhere. */
+ uoff_t offset;
+ off_t space;
+};
+
+struct mbox_sync_mail_context {
+ struct mbox_sync_context *sync_ctx;
+ struct mbox_sync_mail mail;
+
+ uint32_t seq;
+ uoff_t hdr_offset, body_offset;
+
+ size_t header_first_change, header_last_change;
+ string_t *header;
+
+ unsigned char hdr_md5_sum[16];
+
+ uoff_t content_length;
+
+ size_t hdr_pos[MBOX_HDR_COUNT];
+ uint32_t parsed_uid, last_uid_updated_value;
+ unsigned int last_uid_value_start_pos;
+
+ bool have_eoh:1;
+ bool need_rewrite:1;
+ bool seen_imapbase:1;
+ bool updated:1;
+ bool recent:1;
+ bool dirty:1;
+ bool imapbase_rewrite:1;
+ bool imapbase_updated:1;
+};
+
+struct mbox_sync_context {
+ struct mbox_mailbox *mbox;
+ enum mbox_sync_flags flags;
+ struct istream *input, *file_input;
+ int write_fd;
+
+ time_t orig_mtime, orig_atime;
+ uoff_t orig_size;
+ struct stat last_stat;
+
+ struct mail_index_sync_ctx *index_sync_ctx;
+ struct mail_index_view *sync_view;
+ struct mail_index_transaction *t;
+
+ struct mail_index_header reset_hdr;
+ const struct mail_index_header *hdr;
+
+ string_t *header, *from_line;
+
+ /* header state: */
+ uint32_t base_uid_validity, base_uid_last;
+ uoff_t base_uid_last_offset;
+
+ /* mail state: */
+ ARRAY(struct mbox_sync_mail) mails;
+ struct index_sync_changes_context *sync_changes;
+
+ /* per-mail pool */
+ pool_t mail_keyword_pool;
+ /* used for mails[].keywords */
+ pool_t saved_keywords_pool;
+
+ uint32_t prev_msg_uid, next_uid, idx_next_uid;
+ uint32_t seq, idx_seq, need_space_seq;
+ uint32_t last_nonrecent_uid;
+ off_t expunged_space, space_diff;
+
+ bool dest_first_mail:1;
+ bool first_mail_crlf_expunged:1;
+
+ /* global flags: */
+ bool keep_recent:1;
+ bool readonly:1;
+ bool delay_writes:1;
+ bool renumber_uids:1;
+ bool moved_offsets:1;
+ bool ext_modified:1;
+ bool index_reset:1;
+ bool errors:1;
+};
+
+int mbox_sync_header_refresh(struct mbox_mailbox *mbox);
+int mbox_sync(struct mbox_mailbox *mbox, enum mbox_sync_flags flags);
+int mbox_sync_has_changed(struct mbox_mailbox *mbox, bool leave_dirty);
+void mbox_sync_set_critical(struct mbox_sync_context *sync_ctx,
+ const char *fmt, ...) ATTR_FORMAT(2, 3);
+
+int mbox_sync_parse_next_mail(struct istream *input,
+ struct mbox_sync_mail_context *ctx);
+bool mbox_sync_parse_match_mail(struct mbox_mailbox *mbox,
+ struct mail_index_view *view, uint32_t seq);
+
+void mbox_sync_update_header(struct mbox_sync_mail_context *ctx);
+void mbox_sync_update_header_from(struct mbox_sync_mail_context *ctx,
+ const struct mbox_sync_mail *mail);
+int mbox_sync_try_rewrite(struct mbox_sync_mail_context *ctx, off_t move_diff);
+int mbox_sync_rewrite(struct mbox_sync_context *sync_ctx,
+ struct mbox_sync_mail_context *mail_ctx,
+ uoff_t end_offset, off_t move_diff, uoff_t extra_space,
+ uint32_t first_seq, uint32_t last_seq);
+
+int mbox_sync_seek(struct mbox_sync_context *sync_ctx, uoff_t from_offset);
+void mbox_sync_file_update_ext_modified(struct mbox_sync_context *sync_ctx);
+void mbox_sync_file_updated(struct mbox_sync_context *sync_ctx, bool dirty);
+int mbox_move(struct mbox_sync_context *sync_ctx,
+ uoff_t dest, uoff_t source, uoff_t size);
+void mbox_sync_move_buffer(struct mbox_sync_mail_context *ctx,
+ size_t pos, size_t need, size_t have);
+void mbox_sync_headers_add_space(struct mbox_sync_mail_context *ctx,
+ size_t size);
+int mbox_sync_get_guid(struct mbox_mailbox *mbox);
+
+int mbox_list_index_has_changed(struct mailbox *box,
+ struct mail_index_view *list_view,
+ uint32_t seq, bool quick,
+ const char **reason_r);
+void mbox_list_index_update_sync(struct mailbox *box,
+ struct mail_index_transaction *trans,
+ uint32_t seq);
+
+#endif
diff --git a/src/lib-storage/index/mbox/mbox-sync-rewrite.c b/src/lib-storage/index/mbox/mbox-sync-rewrite.c
new file mode 100644
index 0000000..eeb4878
--- /dev/null
+++ b/src/lib-storage/index/mbox/mbox-sync-rewrite.c
@@ -0,0 +1,615 @@
+/* Copyright (c) 2004-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "buffer.h"
+#include "istream.h"
+#include "ostream.h"
+#include "str.h"
+#include "write-full.h"
+#include "message-parser.h"
+#include "mbox-storage.h"
+#include "mbox-sync-private.h"
+#include "istream-raw-mbox.h"
+
+int mbox_move(struct mbox_sync_context *sync_ctx,
+ uoff_t dest, uoff_t source, uoff_t size)
+{
+ struct mbox_mailbox *mbox = sync_ctx->mbox;
+ struct istream *input;
+ struct ostream *output;
+ int ret;
+
+ i_assert(source > 0 || (dest != 1 && dest != 2));
+ i_assert(size < OFF_T_MAX);
+
+ if (size == 0 || source == dest)
+ return 0;
+
+ i_stream_sync(sync_ctx->input);
+
+ output = o_stream_create_fd_file(sync_ctx->write_fd, UOFF_T_MAX, FALSE);
+ i_stream_seek(sync_ctx->file_input, source);
+ if (o_stream_seek(output, dest) < 0) {
+ mbox_ostream_set_syscall_error(sync_ctx->mbox, output,
+ "o_stream_seek()");
+ o_stream_unref(&output);
+ return -1;
+ }
+
+ /* we're moving data within a file. it really shouldn't be failing at
+ this point or we're corrupted. */
+ input = i_stream_create_limit(sync_ctx->file_input, size);
+ o_stream_nsend_istream(output, input);
+ if (input->stream_errno != 0) {
+ mailbox_set_critical(&mbox->box,
+ "read() failed with mbox: %s",
+ i_stream_get_error(input));
+ ret = -1;
+ } else if (output->stream_errno != 0) {
+ mailbox_set_critical(&mbox->box,
+ "write() failed with mbox: %s",
+ o_stream_get_error(output));
+ ret = -1;
+ } else if (input->v_offset != size) {
+ mbox_sync_set_critical(sync_ctx,
+ "mbox_move(%"PRIuUOFF_T", %"PRIuUOFF_T", %"PRIuUOFF_T
+ ") moved only %"PRIuUOFF_T" bytes",
+ dest, source, size, input->v_offset);
+ ret = -1;
+ } else {
+ ret = 0;
+ }
+ i_stream_unref(&input);
+
+ mbox_sync_file_updated(sync_ctx, FALSE);
+ o_stream_destroy(&output);
+ return ret;
+}
+
+static int mbox_fill_space(struct mbox_sync_context *sync_ctx,
+ uoff_t offset, uoff_t size)
+{
+ unsigned char space[1024];
+
+ memset(space, ' ', sizeof(space));
+ while (size > sizeof(space)) {
+ if (pwrite_full(sync_ctx->write_fd, space,
+ sizeof(space), offset) < 0) {
+ mbox_set_syscall_error(sync_ctx->mbox, "pwrite_full()");
+ return -1;
+ }
+ size -= sizeof(space);
+ }
+
+ if (pwrite_full(sync_ctx->write_fd, space, size, offset) < 0) {
+ mbox_set_syscall_error(sync_ctx->mbox, "pwrite_full()");
+ return -1;
+ }
+ mbox_sync_file_updated(sync_ctx, TRUE);
+ return 0;
+}
+
+void mbox_sync_headers_add_space(struct mbox_sync_mail_context *ctx,
+ size_t size)
+{
+ size_t data_size, pos, start_pos;
+ const unsigned char *data;
+ void *p;
+
+ i_assert(size < SSIZE_T_MAX);
+
+ if (ctx->mail.pseudo)
+ start_pos = ctx->hdr_pos[MBOX_HDR_X_IMAPBASE];
+ else if (ctx->mail.space > 0) {
+ /* update the header using the existing offset.
+ otherwise we might chose wrong header and just decrease
+ the available space */
+ start_pos = ctx->mail.offset - ctx->hdr_offset;
+ } else {
+ /* Append at the end of X-Keywords header,
+ or X-UID if it doesn't exist */
+ start_pos = ctx->hdr_pos[MBOX_HDR_X_KEYWORDS] != SIZE_MAX ?
+ ctx->hdr_pos[MBOX_HDR_X_KEYWORDS] :
+ ctx->hdr_pos[MBOX_HDR_X_UID];
+ }
+
+ data = str_data(ctx->header);
+ data_size = str_len(ctx->header);
+ i_assert(start_pos < data_size);
+
+ for (pos = start_pos; pos < data_size; pos++) {
+ if (data[pos] == '\n') {
+ /* possibly continues in next line */
+ if (pos+1 == data_size || !IS_LWSP(data[pos+1]))
+ break;
+ start_pos = pos+1;
+ } else if (!IS_LWSP(data[pos]) && data[pos] != '\r') {
+ start_pos = pos+1;
+ }
+ }
+
+ /* pos points to end of header now, and start_pos to beginning
+ of whitespace. */
+ mbox_sync_move_buffer(ctx, pos, size, 0);
+
+ p = buffer_get_space_unsafe(ctx->header, pos, size);
+ memset(p, ' ', size);
+
+ if (ctx->header_first_change > pos)
+ ctx->header_first_change = pos;
+ ctx->header_last_change = SIZE_MAX;
+
+ ctx->mail.space = (pos - start_pos) + size;
+ ctx->mail.offset = ctx->hdr_offset;
+ if (ctx->mail.space > 0)
+ ctx->mail.offset += start_pos;
+}
+
+static void mbox_sync_header_remove_space(struct mbox_sync_mail_context *ctx,
+ size_t start_pos, size_t *size)
+{
+ const unsigned char *data;
+ size_t data_size, pos, last_line_pos;
+
+ /* find the end of the LWSP */
+ data = str_data(ctx->header);
+ data_size = str_len(ctx->header);
+
+ for (pos = last_line_pos = start_pos; pos < data_size; pos++) {
+ if (data[pos] == '\n') {
+ /* possibly continues in next line */
+ if (pos+1 == data_size || !IS_LWSP(data[pos+1])) {
+ data_size = pos;
+ break;
+ }
+ last_line_pos = pos+1;
+ } else if (!IS_LWSP(data[pos]) && data[pos] != '\r') {
+ start_pos = last_line_pos = pos+1;
+ }
+ }
+
+ if (start_pos == data_size)
+ return;
+
+ /* and remove what we can */
+ if (ctx->header_first_change > start_pos)
+ ctx->header_first_change = start_pos;
+ ctx->header_last_change = SIZE_MAX;
+
+ if (data_size - start_pos <= *size) {
+ /* remove it all */
+ mbox_sync_move_buffer(ctx, start_pos, 0, data_size - start_pos);
+ *size -= data_size - start_pos;
+ return;
+ }
+
+ /* we have more space than needed. since we're removing from
+ the beginning of header instead of end, we don't have to
+ worry about multiline-headers. */
+ mbox_sync_move_buffer(ctx, start_pos, 0, *size);
+ if (last_line_pos <= start_pos + *size)
+ last_line_pos = start_pos;
+ else
+ last_line_pos -= *size;
+ data_size -= *size;
+
+ *size = 0;
+
+ if (ctx->mail.space < (off_t)(data_size - last_line_pos)) {
+ ctx->mail.space = data_size - last_line_pos;
+ ctx->mail.offset = ctx->hdr_offset;
+ if (ctx->mail.space > 0)
+ ctx->mail.offset += last_line_pos;
+ }
+}
+
+static void mbox_sync_headers_remove_space(struct mbox_sync_mail_context *ctx,
+ size_t size)
+{
+ static enum header_position space_positions[] = {
+ MBOX_HDR_X_UID,
+ MBOX_HDR_X_KEYWORDS,
+ MBOX_HDR_X_IMAPBASE
+ };
+ enum header_position pos;
+ int i;
+
+ ctx->mail.space = 0;
+ ctx->mail.offset = ctx->hdr_offset;
+
+ for (i = 0; i < 3 && size > 0; i++) {
+ pos = space_positions[i];
+ if (ctx->hdr_pos[pos] != SIZE_MAX) {
+ mbox_sync_header_remove_space(ctx, ctx->hdr_pos[pos],
+ &size);
+ }
+ }
+
+ /* FIXME: see if we could remove X-Keywords header completely */
+}
+
+static void mbox_sync_first_mail_written(struct mbox_sync_mail_context *ctx,
+ uoff_t hdr_offset)
+{
+ /* we wrote the first mail. update last-uid offset so we can find
+ it later */
+ i_assert(ctx->last_uid_value_start_pos != 0);
+ i_assert(ctx->hdr_pos[MBOX_HDR_X_IMAPBASE] != SIZE_MAX);
+
+ ctx->sync_ctx->base_uid_last_offset = hdr_offset +
+ ctx->hdr_pos[MBOX_HDR_X_IMAPBASE] +
+ ctx->last_uid_value_start_pos;
+
+ if (ctx->imapbase_updated) {
+ /* update so a) we don't try to update it later needlessly,
+ b) if we do actually update it, we see the correct value */
+ ctx->sync_ctx->base_uid_last = ctx->last_uid_updated_value;
+ }
+}
+
+int mbox_sync_try_rewrite(struct mbox_sync_mail_context *ctx, off_t move_diff)
+{
+ struct mbox_sync_context *sync_ctx = ctx->sync_ctx;
+ size_t old_hdr_size, new_hdr_size;
+
+ i_assert(sync_ctx->mbox->mbox_lock_type == F_WRLCK);
+
+ old_hdr_size = ctx->body_offset - ctx->hdr_offset;
+ new_hdr_size = str_len(ctx->header);
+
+ if (new_hdr_size <= old_hdr_size) {
+ /* add space. note that we must call add_space() even if we're
+ not adding anything so mail.offset gets fixed. */
+ mbox_sync_headers_add_space(ctx, old_hdr_size - new_hdr_size);
+ } else if (new_hdr_size > old_hdr_size) {
+ /* try removing the space where we can */
+ mbox_sync_headers_remove_space(ctx,
+ new_hdr_size - old_hdr_size);
+ new_hdr_size = str_len(ctx->header);
+
+ if (new_hdr_size <= old_hdr_size) {
+ /* good, we removed enough. */
+ i_assert(new_hdr_size == old_hdr_size);
+ } else if (move_diff < 0 &&
+ new_hdr_size - old_hdr_size <= (uoff_t)-move_diff) {
+ /* moving backwards - we can use the extra space from
+ it, just update expunged_space accordingly */
+ i_assert(ctx->mail.space == 0);
+ i_assert(sync_ctx->expunged_space >=
+ (off_t)(new_hdr_size - old_hdr_size));
+ sync_ctx->expunged_space -= new_hdr_size - old_hdr_size;
+ } else {
+ /* couldn't get enough space */
+ i_assert(ctx->mail.space == 0);
+ ctx->mail.space =
+ -(ssize_t)(new_hdr_size - old_hdr_size);
+ return 0;
+ }
+ }
+
+ i_assert(ctx->mail.space >= 0);
+
+ if (ctx->header_first_change == SIZE_MAX && move_diff == 0) {
+ /* no changes actually. we get here if index sync record told
+ us to do something that was already there */
+ return 1;
+ }
+
+ if (move_diff != 0) {
+ /* forget about partial write optimizations */
+ ctx->header_first_change = 0;
+ ctx->header_last_change = 0;
+ }
+
+ if (ctx->header_last_change != SIZE_MAX &&
+ ctx->header_last_change != 0)
+ str_truncate(ctx->header, ctx->header_last_change);
+
+ if (pwrite_full(sync_ctx->write_fd,
+ str_data(ctx->header) + ctx->header_first_change,
+ str_len(ctx->header) - ctx->header_first_change,
+ (off_t)ctx->hdr_offset + (off_t)ctx->header_first_change +
+ move_diff) < 0) {
+ mbox_set_syscall_error(sync_ctx->mbox, "pwrite_full()");
+ return -1;
+ }
+
+ if (sync_ctx->dest_first_mail &&
+ (ctx->imapbase_updated || ctx->sync_ctx->base_uid_last != 0)) {
+ /* the position might have moved as a result of moving
+ whitespace */
+ mbox_sync_first_mail_written(ctx, (off_t)ctx->hdr_offset + move_diff);
+ }
+
+ mbox_sync_file_updated(sync_ctx, FALSE);
+ return 1;
+}
+
+static int mbox_sync_read_next(struct mbox_sync_context *sync_ctx,
+ struct mbox_sync_mail_context *mail_ctx,
+ struct mbox_sync_mail *mails,
+ uint32_t seq, uint32_t idx,
+ uoff_t expunged_space)
+{
+ unsigned int first_mail_expunge_extra;
+ uint32_t orig_next_uid;
+
+ i_zero(mail_ctx);
+ mail_ctx->sync_ctx = sync_ctx;
+ mail_ctx->seq = seq;
+ mail_ctx->header = sync_ctx->header;
+
+ if (istream_raw_mbox_get_header_offset(sync_ctx->input,
+ &mail_ctx->mail.offset) < 0) {
+ mbox_sync_set_critical(sync_ctx,
+ "Couldn't get header offset for seq=%u", seq);
+ return -1;
+ }
+ mail_ctx->mail.body_size = mails[idx].body_size;
+
+ orig_next_uid = sync_ctx->next_uid;
+ if (mails[idx].uid != 0) {
+ /* This will force the UID to be the one that we originally
+ assigned to it, regardless of whether it's broken or not in
+ the file. */
+ sync_ctx->next_uid = mails[idx].uid;
+ sync_ctx->prev_msg_uid = mails[idx].uid - 1;
+ } else {
+ /* Pseudo mail shouldn't have X-UID header at all */
+ i_assert(mails[idx].pseudo);
+ sync_ctx->prev_msg_uid = 0;
+ }
+
+ first_mail_expunge_extra = 1 +
+ (sync_ctx->first_mail_crlf_expunged ? 1 : 0);
+ if (mails[idx].from_offset +
+ first_mail_expunge_extra - expunged_space != 0) {
+ sync_ctx->dest_first_mail = mails[idx].from_offset == 0;
+ } else {
+ /* we need to skip over the initial \n (it's already counted in
+ expunged_space) */
+ sync_ctx->dest_first_mail = TRUE;
+ mails[idx].from_offset += first_mail_expunge_extra;
+ }
+
+ if (mbox_sync_parse_next_mail(sync_ctx->input, mail_ctx) < 0)
+ return -1;
+ i_assert(mail_ctx->mail.pseudo == mails[idx].pseudo);
+
+ /* set next_uid back before updating the headers. this is important
+ if we're updating the first message to make X-IMAP[base] header
+ have the correct value. */
+ sync_ctx->next_uid = orig_next_uid;
+
+ if (mails[idx].space != 0) {
+ if (mails[idx].space < 0) {
+ /* remove all possible spacing before updating */
+ mbox_sync_headers_remove_space(mail_ctx, SIZE_MAX);
+ }
+ mbox_sync_update_header_from(mail_ctx, &mails[idx]);
+ } else {
+ /* updating might just try to add headers and mess up our
+ calculations completely. so only add the EOH here. */
+ if (mail_ctx->have_eoh)
+ str_append_c(mail_ctx->header, '\n');
+ }
+ return 0;
+}
+
+static int mbox_sync_read_and_move(struct mbox_sync_context *sync_ctx,
+ struct mbox_sync_mail_context *mail_ctx,
+ struct mbox_sync_mail *mails,
+ uint32_t seq, uint32_t idx, uint32_t padding,
+ off_t move_diff, uoff_t expunged_space,
+ uoff_t end_offset, bool first_nonexpunged)
+{
+ struct mbox_sync_mail_context new_mail_ctx;
+ uoff_t offset, dest_offset;
+ size_t need_space;
+
+ if (mail_ctx == NULL) {
+ if (mbox_sync_seek(sync_ctx, mails[idx].from_offset) < 0)
+ return -1;
+
+ if (mbox_sync_read_next(sync_ctx, &new_mail_ctx, mails, seq, idx,
+ expunged_space) < 0)
+ return -1;
+ mail_ctx = &new_mail_ctx;
+ } else {
+ i_assert(seq == mail_ctx->seq);
+ if (mail_ctx->mail.space < 0)
+ mail_ctx->mail.space = 0;
+ i_stream_seek(sync_ctx->input, mail_ctx->body_offset);
+ }
+
+ if (mail_ctx->mail.space <= 0) {
+ need_space = str_len(mail_ctx->header) - mail_ctx->mail.space -
+ (mail_ctx->body_offset - mail_ctx->hdr_offset);
+ if (need_space != (uoff_t)-mails[idx].space) {
+ /* this check works only if we're doing the first
+ write, or if the file size was changed externally */
+ mbox_sync_file_update_ext_modified(sync_ctx);
+
+ mbox_sync_set_critical(sync_ctx,
+ "seq=%u uid=%u uid_broken=%d "
+ "originally needed %"PRIuUOFF_T
+ " bytes, now needs %zu bytes",
+ seq, mails[idx].uid, mails[idx].uid_broken ? 1 : 0,
+ (uoff_t)-mails[idx].space, need_space);
+ return -1;
+ }
+ }
+
+ if (first_nonexpunged && expunged_space > 0) {
+ /* move From-line (after parsing headers so we don't
+ overwrite them) */
+ i_assert(mails[idx].from_offset >= expunged_space);
+ if (mbox_move(sync_ctx, mails[idx].from_offset - expunged_space,
+ mails[idx].from_offset,
+ mails[idx].offset - mails[idx].from_offset) < 0)
+ return -1;
+ }
+
+ if (mails[idx].space == 0) {
+ /* don't touch spacing */
+ } else if (padding < (uoff_t)mail_ctx->mail.space) {
+ mbox_sync_headers_remove_space(mail_ctx, mail_ctx->mail.space -
+ padding);
+ } else {
+ mbox_sync_headers_add_space(mail_ctx, padding -
+ mail_ctx->mail.space);
+ }
+
+ /* move the body of this message and headers of next message forward,
+ then write the headers */
+ offset = sync_ctx->input->v_offset;
+ dest_offset = offset + move_diff;
+ i_assert(offset <= end_offset);
+ if (mbox_move(sync_ctx, dest_offset, offset, end_offset - offset) < 0)
+ return -1;
+
+ /* the header may actually be moved backwards if there was expunged
+ space which we wanted to remove */
+ i_assert(dest_offset >= str_len(mail_ctx->header));
+ dest_offset -= str_len(mail_ctx->header);
+ i_assert(dest_offset >= mails[idx].from_offset - expunged_space);
+ if (pwrite_full(sync_ctx->write_fd, str_data(mail_ctx->header),
+ str_len(mail_ctx->header), dest_offset) < 0) {
+ mbox_set_syscall_error(sync_ctx->mbox, "pwrite_full()");
+ return -1;
+ }
+ mbox_sync_file_updated(sync_ctx, TRUE);
+
+ if (sync_ctx->dest_first_mail) {
+ mbox_sync_first_mail_written(mail_ctx, dest_offset);
+ sync_ctx->dest_first_mail = FALSE;
+ }
+
+ mails[idx].offset = dest_offset +
+ (mail_ctx->mail.offset - mail_ctx->hdr_offset);
+ mails[idx].space = mail_ctx->mail.space;
+ return 0;
+}
+
+int mbox_sync_rewrite(struct mbox_sync_context *sync_ctx,
+ struct mbox_sync_mail_context *mail_ctx,
+ uoff_t end_offset, off_t move_diff, uoff_t extra_space,
+ uint32_t first_seq, uint32_t last_seq)
+{
+ struct mbox_sync_mail *mails;
+ uoff_t offset, dest_offset, next_end_offset, next_move_diff;
+ uoff_t start_offset, expunged_space;
+ uint32_t idx, first_nonexpunged_idx, padding_per_mail;
+ uint32_t orig_prev_msg_uid;
+ unsigned int count;
+ int ret = 0;
+
+ i_assert(extra_space < OFF_T_MAX);
+ i_assert(sync_ctx->mbox->mbox_lock_type == F_WRLCK);
+
+ mails = array_get_modifiable(&sync_ctx->mails, &count);
+ i_assert(count == last_seq - first_seq + 1);
+
+ /* if there's expunges in mails[], we would get more correct balancing
+ by counting only them here. however, that might make us overwrite
+ data which hasn't yet been copied backwards. to avoid too much
+ complexity, we just leave all the rest of the extra space to first
+ mail */
+ idx = last_seq - first_seq + 1;
+ padding_per_mail = extra_space / idx;
+
+ /* after expunge the next mail must have been missing space, or we
+ would have moved it backwards already */
+ expunged_space = 0;
+ start_offset = mails[0].from_offset;
+ for (first_nonexpunged_idx = 0;; first_nonexpunged_idx++) {
+ i_assert(first_nonexpunged_idx != idx);
+ if (!mails[first_nonexpunged_idx].expunged)
+ break;
+ expunged_space += mails[first_nonexpunged_idx].space;
+ }
+ i_assert(mails[first_nonexpunged_idx].space < 0);
+
+ orig_prev_msg_uid = sync_ctx->prev_msg_uid;
+
+ /* start moving backwards. */
+ while (idx > first_nonexpunged_idx) {
+ idx--;
+ if (idx == first_nonexpunged_idx) {
+ /* give the rest of the extra space to first mail.
+ we might also have to move the mail backwards to
+ fill the expunged space */
+ padding_per_mail = move_diff + (off_t)expunged_space +
+ (off_t)mails[idx].space;
+ }
+
+ next_end_offset = mails[idx].offset;
+
+ if (mails[idx].space <= 0 && !mails[idx].expunged) {
+ /* give space to this mail. end_offset is left to
+ contain this message's From-line (ie. below we
+ move only headers + body). */
+ bool first_nonexpunged = idx == first_nonexpunged_idx;
+
+ next_move_diff = -mails[idx].space;
+ if (mbox_sync_read_and_move(sync_ctx, mail_ctx, mails,
+ first_seq + idx, idx,
+ padding_per_mail,
+ move_diff, expunged_space,
+ end_offset,
+ first_nonexpunged) < 0) {
+ ret = -1;
+ break;
+ }
+ move_diff -= next_move_diff + mails[idx].space;
+ } else {
+ /* this mail provides more space. just move it forward
+ from the extra space offset and set end_offset to
+ point to beginning of extra space. that way the
+ header will be moved along with previous mail's
+ body.
+
+ if this is expunged mail, we're moving following
+ mail's From-line and maybe headers. */
+ offset = mails[idx].offset + mails[idx].space;
+ dest_offset = offset + move_diff;
+ i_assert(offset <= end_offset);
+ if (mbox_move(sync_ctx, dest_offset, offset,
+ end_offset - offset) < 0) {
+ ret = -1;
+ break;
+ }
+
+ move_diff += mails[idx].space;
+ if (!mails[idx].expunged) {
+ move_diff -= padding_per_mail;
+ mails[idx].space = padding_per_mail;
+
+ if (mbox_fill_space(sync_ctx, move_diff +
+ mails[idx].offset,
+ padding_per_mail) < 0) {
+ ret = -1;
+ break;
+ }
+ }
+ mails[idx].offset += move_diff;
+ }
+ mail_ctx = NULL;
+
+ i_assert(move_diff >= 0 || idx == first_nonexpunged_idx);
+ i_assert(next_end_offset <= end_offset);
+
+ end_offset = next_end_offset;
+ mails[idx].from_offset += move_diff;
+ }
+
+ if (ret == 0) {
+ i_assert(mails[idx].from_offset == start_offset);
+ i_assert(move_diff + (off_t)expunged_space >= 0);
+ }
+
+ mbox_sync_file_updated(sync_ctx, FALSE);
+ sync_ctx->prev_msg_uid = orig_prev_msg_uid;
+ return ret;
+}
diff --git a/src/lib-storage/index/mbox/mbox-sync-update.c b/src/lib-storage/index/mbox/mbox-sync-update.c
new file mode 100644
index 0000000..8442013
--- /dev/null
+++ b/src/lib-storage/index/mbox/mbox-sync-update.c
@@ -0,0 +1,466 @@
+/* Copyright (c) 2004-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "buffer.h"
+#include "str.h"
+#include "message-parser.h"
+#include "index-storage.h"
+#include "index-sync-changes.h"
+#include "mbox-storage.h"
+#include "mbox-sync-private.h"
+
+/* Line length when to wrap X-IMAP, X-IMAPbase and X-Keywords headers to next
+ line. Keep this pretty long, as after we wrap we lose compatibility with
+ UW-IMAP */
+#define KEYWORD_WRAP_LINE_LENGTH 1024
+
+static void status_flags_append(struct mbox_sync_mail_context *ctx,
+ const struct mbox_flag_type *flags_list)
+{
+ int i;
+
+ for (i = 0; flags_list[i].chr != 0; i++) {
+ if ((ctx->mail.flags & flags_list[i].flag) != 0)
+ str_append_c(ctx->header, flags_list[i].chr);
+ }
+}
+
+void mbox_sync_move_buffer(struct mbox_sync_mail_context *ctx,
+ size_t pos, size_t need, size_t have)
+{
+ ssize_t diff = (ssize_t)need - (ssize_t)have;
+ int i;
+
+ i_assert(have < SSIZE_T_MAX);
+
+ if (diff == 0) {
+ if (ctx->header_last_change < pos + have ||
+ ctx->header_last_change == SIZE_MAX)
+ ctx->header_last_change = pos + have;
+ } else {
+ /* FIXME: if (diff < ctx->space && pos < ctx->offset) then
+ move the data only up to space offset and give/take the
+ space from there. update header_last_change accordingly.
+ (except pos and offset can't be compared directly) */
+ ctx->header_last_change = SIZE_MAX;
+ for (i = 0; i < MBOX_HDR_COUNT; i++) {
+ if (ctx->hdr_pos[i] > pos &&
+ ctx->hdr_pos[i] != SIZE_MAX)
+ ctx->hdr_pos[i] = (ssize_t)ctx->hdr_pos[i] + diff;
+ }
+
+ if (ctx->mail.space > 0) {
+ i_assert(ctx->mail.offset + ctx->mail.space <=
+ ctx->hdr_offset + pos ||
+ ctx->mail.offset > ctx->hdr_offset + pos + have);
+ if (ctx->mail.offset > ctx->hdr_offset + pos) {
+ /* free space offset moves */
+ ctx->mail.offset = (ssize_t)ctx->mail.offset + diff;
+ }
+ }
+
+ if (diff < 0)
+ str_delete(ctx->header, pos, -diff);
+ else {
+ ctx->header_last_change = SIZE_MAX;
+ buffer_copy(ctx->header, pos + diff,
+ ctx->header, pos, SIZE_MAX);
+ }
+ }
+}
+
+static void status_flags_replace(struct mbox_sync_mail_context *ctx, size_t pos,
+ const struct mbox_flag_type *flags_list)
+{
+ unsigned char *data;
+ size_t size;
+ int i, need, have;
+
+ ctx->mail.flags ^= MBOX_NONRECENT_KLUDGE;
+
+ if (ctx->header_first_change > pos)
+ ctx->header_first_change = pos;
+
+ /* how many bytes do we need? */
+ for (i = 0, need = 0; flags_list[i].chr != 0; i++) {
+ if ((ctx->mail.flags & flags_list[i].flag) != 0)
+ need++;
+ }
+
+ /* how many bytes do we have now? */
+ data = buffer_get_modifiable_data(ctx->header, &size);
+ for (have = 0; pos < size; pos++) {
+ if (data[pos] == '\n' || data[pos] == '\r')
+ break;
+
+ /* see if this is unknown flag for us */
+ for (i = 0; flags_list[i].chr != 0; i++) {
+ if (flags_list[i].chr == (char)data[pos])
+ break;
+ }
+
+ if (flags_list[i].chr != 0)
+ have++;
+ else {
+ /* save this one */
+ data[pos-have] = data[pos];
+ }
+ }
+ pos -= have;
+ mbox_sync_move_buffer(ctx, pos, need, have);
+
+ /* @UNSAFE */
+ data = buffer_get_space_unsafe(ctx->header, pos, need);
+ for (i = 0; flags_list[i].chr != 0; i++) {
+ if ((ctx->mail.flags & flags_list[i].flag) != 0)
+ *data++ = flags_list[i].chr;
+ }
+
+ ctx->mail.flags ^= MBOX_NONRECENT_KLUDGE;
+}
+
+static void
+keywords_append(struct mbox_sync_context *sync_ctx, string_t *dest,
+ const ARRAY_TYPE(keyword_indexes) *keyword_indexes_arr)
+{
+ struct index_mailbox_context *ibox =
+ INDEX_STORAGE_CONTEXT(&sync_ctx->mbox->box);
+ const char *const *keyword_names;
+ const unsigned int *keyword_indexes;
+ unsigned int i, idx_count, keywords_count;
+ size_t last_break;
+
+ keyword_names = array_get(ibox->keyword_names,
+ &keywords_count);
+ keyword_indexes = array_get(keyword_indexes_arr, &idx_count);
+
+ for (i = 0, last_break = str_len(dest); i < idx_count; i++) {
+ i_assert(keyword_indexes[i] < keywords_count);
+
+ /* wrap the line whenever it gets too long */
+ if (str_len(dest) - last_break < KEYWORD_WRAP_LINE_LENGTH) {
+ if (i > 0)
+ str_append_c(dest, ' ');
+ } else {
+ str_append(dest, "\n\t");
+ last_break = str_len(dest);
+ }
+ str_append(dest, keyword_names[keyword_indexes[i]]);
+ }
+}
+
+static void
+keywords_append_all(struct mbox_sync_mail_context *ctx, string_t *dest,
+ size_t startpos)
+{
+ struct index_mailbox_context *ibox =
+ INDEX_STORAGE_CONTEXT(&ctx->sync_ctx->mbox->box);
+ const char *const *names;
+ const unsigned char *p;
+ unsigned int i, count;
+ size_t last_break;
+
+ p = str_data(dest);
+ if (str_len(dest) - startpos < KEYWORD_WRAP_LINE_LENGTH)
+ last_break = startpos;
+ else {
+ /* set last_break to beginning of line */
+ for (last_break = str_len(dest); last_break > 0; last_break--) {
+ if (p[last_break-1] == '\n')
+ break;
+ }
+ }
+
+ names = array_get(ibox->keyword_names, &count);
+ for (i = 0; i < count; i++) {
+ /* wrap the line whenever it gets too long */
+ if (str_len(dest) - last_break < KEYWORD_WRAP_LINE_LENGTH)
+ str_append_c(dest, ' ');
+ else {
+ str_append(dest, "\n\t");
+ last_break = str_len(dest);
+ }
+ str_append(dest, names[i]);
+ }
+}
+
+static void mbox_sync_add_missing_headers(struct mbox_sync_mail_context *ctx)
+{
+ size_t new_hdr_size, startpos;
+
+ new_hdr_size = str_len(ctx->header);
+ if (new_hdr_size > 0 &&
+ str_data(ctx->header)[new_hdr_size-1] != '\n') {
+ /* broken header - doesn't end with \n. fix it. */
+ str_append_c(ctx->header, '\n');
+ }
+
+ if (ctx->sync_ctx->dest_first_mail &&
+ ctx->hdr_pos[MBOX_HDR_X_IMAPBASE] == SIZE_MAX) {
+ i_assert(ctx->sync_ctx->base_uid_validity != 0);
+
+ str_append(ctx->header, "X-IMAPbase: ");
+ ctx->hdr_pos[MBOX_HDR_X_IMAPBASE] = str_len(ctx->header);
+ /* startpos must start from identical position as when
+ updating */
+ startpos = str_len(ctx->header);
+ str_printfa(ctx->header, "%u ",
+ ctx->sync_ctx->base_uid_validity);
+
+ ctx->last_uid_updated_value = ctx->sync_ctx->next_uid-1;
+ ctx->last_uid_value_start_pos = str_len(ctx->header) -
+ ctx->hdr_pos[MBOX_HDR_X_IMAPBASE];
+ ctx->imapbase_updated = TRUE;
+ str_printfa(ctx->header, "%010u", ctx->last_uid_updated_value);
+
+ keywords_append_all(ctx, ctx->header, startpos);
+ str_append_c(ctx->header, '\n');
+ }
+
+ if (ctx->hdr_pos[MBOX_HDR_X_UID] == SIZE_MAX && !ctx->mail.pseudo) {
+ str_append(ctx->header, "X-UID: ");
+ ctx->hdr_pos[MBOX_HDR_X_UID] = str_len(ctx->header);
+ str_printfa(ctx->header, "%u\n", ctx->mail.uid);
+ }
+
+ ctx->mail.flags ^= MBOX_NONRECENT_KLUDGE;
+
+ if (ctx->hdr_pos[MBOX_HDR_STATUS] == SIZE_MAX &&
+ (ctx->mail.flags & STATUS_FLAGS_MASK) != 0) {
+ str_append(ctx->header, "Status: ");
+ ctx->hdr_pos[MBOX_HDR_STATUS] = str_len(ctx->header);
+ status_flags_append(ctx, mbox_status_flags);
+ str_append_c(ctx->header, '\n');
+ }
+
+ if (ctx->hdr_pos[MBOX_HDR_X_STATUS] == SIZE_MAX &&
+ (ctx->mail.flags & XSTATUS_FLAGS_MASK) != 0) {
+ str_append(ctx->header, "X-Status: ");
+ ctx->hdr_pos[MBOX_HDR_X_STATUS] = str_len(ctx->header);
+ status_flags_append(ctx, mbox_xstatus_flags);
+ str_append_c(ctx->header, '\n');
+ }
+
+ ctx->mail.flags ^= MBOX_NONRECENT_KLUDGE;
+
+ if (ctx->hdr_pos[MBOX_HDR_X_KEYWORDS] == SIZE_MAX &&
+ array_is_created(&ctx->mail.keywords) &&
+ array_count(&ctx->mail.keywords) > 0) {
+ str_append(ctx->header, "X-Keywords: ");
+ ctx->hdr_pos[MBOX_HDR_X_KEYWORDS] = str_len(ctx->header);
+ keywords_append(ctx->sync_ctx, ctx->header,
+ &ctx->mail.keywords);
+ str_append_c(ctx->header, '\n');
+ }
+
+ if (ctx->content_length == UOFF_T_MAX &&
+ ctx->mail.body_size >= MBOX_MIN_CONTENT_LENGTH_SIZE) {
+ str_printfa(ctx->header, "Content-Length: %"PRIuUOFF_T"\n",
+ ctx->mail.body_size);
+ }
+
+ if (str_len(ctx->header) != new_hdr_size) {
+ if (ctx->header_first_change == SIZE_MAX)
+ ctx->header_first_change = new_hdr_size;
+ ctx->header_last_change = SIZE_MAX;
+ }
+
+ if (ctx->have_eoh)
+ str_append_c(ctx->header, '\n');
+}
+
+static void mbox_sync_update_status(struct mbox_sync_mail_context *ctx)
+{
+ if (ctx->hdr_pos[MBOX_HDR_STATUS] != SIZE_MAX) {
+ status_flags_replace(ctx, ctx->hdr_pos[MBOX_HDR_STATUS],
+ mbox_status_flags);
+ }
+}
+
+static void mbox_sync_update_xstatus(struct mbox_sync_mail_context *ctx)
+{
+ if (ctx->hdr_pos[MBOX_HDR_X_STATUS] != SIZE_MAX) {
+ status_flags_replace(ctx, ctx->hdr_pos[MBOX_HDR_X_STATUS],
+ mbox_xstatus_flags);
+ }
+}
+
+static void mbox_sync_update_line(struct mbox_sync_mail_context *ctx,
+ size_t pos, string_t *new_line)
+{
+ const char *hdr, *p;
+ uoff_t file_pos;
+
+ if (ctx->header_first_change > pos)
+ ctx->header_first_change = pos;
+
+ /* set p = end of header, handle also wrapped headers */
+ hdr = p = str_c(ctx->header) + pos;
+ for (;;) {
+ p = strchr(p, '\n');
+ if (p == NULL) {
+ /* shouldn't really happen, but allow anyway.. */
+ p = hdr + strlen(hdr);
+ break;
+ }
+ if (p[1] != '\t' && p[1] != ' ')
+ break;
+ p += 2;
+ }
+
+ file_pos = pos + ctx->hdr_offset;
+ if (ctx->mail.space > 0 && ctx->mail.offset >= file_pos &&
+ ctx->mail.offset < file_pos + (p - hdr)) {
+ /* extra space points to this line. remove it. */
+ ctx->mail.offset = ctx->hdr_offset;
+ ctx->mail.space = 0;
+ }
+
+ mbox_sync_move_buffer(ctx, pos, str_len(new_line), p - hdr + 1);
+ buffer_copy(ctx->header, pos, new_line, 0, SIZE_MAX);
+}
+
+static void mbox_sync_update_xkeywords(struct mbox_sync_mail_context *ctx)
+{
+ string_t *str;
+
+ if (ctx->hdr_pos[MBOX_HDR_X_KEYWORDS] == SIZE_MAX)
+ return;
+
+ str = t_str_new(256);
+ if (array_is_created(&ctx->mail.keywords))
+ keywords_append(ctx->sync_ctx, str, &ctx->mail.keywords);
+ str_append_c(str, '\n');
+ mbox_sync_update_line(ctx, ctx->hdr_pos[MBOX_HDR_X_KEYWORDS], str);
+}
+
+static void mbox_sync_update_x_imap_base(struct mbox_sync_mail_context *ctx)
+{
+ struct mbox_sync_context *sync_ctx = ctx->sync_ctx;
+ string_t *str;
+
+ i_assert(sync_ctx->base_uid_validity != 0);
+
+ if (!sync_ctx->dest_first_mail ||
+ ctx->hdr_pos[MBOX_HDR_X_IMAPBASE] == SIZE_MAX)
+ return;
+
+ if (!ctx->imapbase_rewrite) {
+ /* uid-last might need updating, but we'll do it later by
+ writing it directly where needed. */
+ return;
+ }
+
+ /* a) keyword list changed, b) uid-last didn't use 10 digits */
+ str = t_str_new(200);
+ str_printfa(str, "%u ", sync_ctx->base_uid_validity);
+
+ ctx->last_uid_updated_value = sync_ctx->next_uid-1;
+ ctx->last_uid_value_start_pos = str_len(str);
+ ctx->imapbase_updated = TRUE;
+ str_printfa(str, "%010u", ctx->last_uid_updated_value);
+
+ keywords_append_all(ctx, str, 0);
+ str_append_c(str, '\n');
+
+ mbox_sync_update_line(ctx, ctx->hdr_pos[MBOX_HDR_X_IMAPBASE], str);
+}
+
+static void mbox_sync_update_x_uid(struct mbox_sync_mail_context *ctx)
+{
+ string_t *str;
+
+ if (ctx->hdr_pos[MBOX_HDR_X_UID] == SIZE_MAX ||
+ ctx->mail.uid == ctx->parsed_uid)
+ return;
+
+ str = t_str_new(64);
+ str_printfa(str, "%u\n", ctx->mail.uid);
+ mbox_sync_update_line(ctx, ctx->hdr_pos[MBOX_HDR_X_UID], str);
+}
+
+static void mbox_sync_update_header_real(struct mbox_sync_mail_context *ctx)
+{
+ i_assert(ctx->mail.uid != 0 || ctx->mail.pseudo);
+
+ if (!ctx->sync_ctx->keep_recent)
+ ctx->mail.flags &= ENUM_NEGATE(MAIL_RECENT);
+
+ mbox_sync_update_status(ctx);
+ mbox_sync_update_xstatus(ctx);
+ mbox_sync_update_xkeywords(ctx);
+
+ mbox_sync_update_x_imap_base(ctx);
+ mbox_sync_update_x_uid(ctx);
+
+ mbox_sync_add_missing_headers(ctx);
+ ctx->updated = TRUE;
+}
+
+void mbox_sync_update_header(struct mbox_sync_mail_context *ctx)
+{
+ T_BEGIN {
+ mbox_sync_update_header_real(ctx);
+ } T_END;
+}
+
+static void
+mbox_sync_update_header_from_real(struct mbox_sync_mail_context *ctx,
+ const struct mbox_sync_mail *mail)
+{
+ if (mail->status_broken ||
+ (ctx->mail.flags & STATUS_FLAGS_MASK) !=
+ (mail->flags & STATUS_FLAGS_MASK) ||
+ (ctx->mail.flags & MAIL_RECENT) != 0) {
+ ctx->mail.flags = (ctx->mail.flags & ENUM_NEGATE(STATUS_FLAGS_MASK)) |
+ (mail->flags & STATUS_FLAGS_MASK);
+ if (!ctx->sync_ctx->keep_recent)
+ ctx->mail.flags &= ENUM_NEGATE(MAIL_RECENT);
+ mbox_sync_update_status(ctx);
+ }
+ if (mail->xstatus_broken ||
+ (ctx->mail.flags & XSTATUS_FLAGS_MASK) !=
+ (mail->flags & XSTATUS_FLAGS_MASK)) {
+ ctx->mail.flags = (ctx->mail.flags & ENUM_NEGATE(XSTATUS_FLAGS_MASK)) |
+ (mail->flags & XSTATUS_FLAGS_MASK);
+ mbox_sync_update_xstatus(ctx);
+ }
+ if (!array_is_created(&mail->keywords) ||
+ array_count(&mail->keywords) == 0) {
+ /* no keywords for this mail */
+ if (array_is_created(&ctx->mail.keywords)) {
+ array_clear(&ctx->mail.keywords);
+ mbox_sync_update_xkeywords(ctx);
+ }
+ } else if (!array_is_created(&ctx->mail.keywords)) {
+ /* adding first keywords */
+ p_array_init(&ctx->mail.keywords,
+ ctx->sync_ctx->mail_keyword_pool,
+ array_count(&mail->keywords));
+ array_append_array(&ctx->mail.keywords,
+ &mail->keywords);
+ mbox_sync_update_xkeywords(ctx);
+ } else if (!array_cmp(&ctx->mail.keywords, &mail->keywords)) {
+ /* keywords changed. */
+ array_clear(&ctx->mail.keywords);
+ array_append_array(&ctx->mail.keywords,
+ &mail->keywords);
+ mbox_sync_update_xkeywords(ctx);
+ }
+
+ i_assert(ctx->mail.uid == 0 || ctx->mail.uid == mail->uid);
+ ctx->mail.uid = mail->uid;
+
+ mbox_sync_update_x_imap_base(ctx);
+ mbox_sync_update_x_uid(ctx);
+ mbox_sync_add_missing_headers(ctx);
+}
+
+void mbox_sync_update_header_from(struct mbox_sync_mail_context *ctx,
+ const struct mbox_sync_mail *mail)
+{
+ T_BEGIN {
+ mbox_sync_update_header_from_real(ctx, mail);
+ } T_END;
+}
diff --git a/src/lib-storage/index/mbox/mbox-sync.c b/src/lib-storage/index/mbox/mbox-sync.c
new file mode 100644
index 0000000..0d2aa7f
--- /dev/null
+++ b/src/lib-storage/index/mbox/mbox-sync.c
@@ -0,0 +1,2066 @@
+/* Copyright (c) 2004-2018 Dovecot authors, see the included COPYING file */
+
+/*
+ Modifying mbox can be slow, so we try to do it all at once minimizing the
+ required disk I/O. We may need to:
+
+ - Update message flags in Status, X-Status and X-Keywords headers
+ - Write missing X-UID and X-IMAPbase headers
+ - Write missing or broken Content-Length header if there's space
+ - Expunge specified messages
+
+ Here's how we do it:
+
+ - Start reading the mails from the beginning
+ - X-Keywords, X-UID and X-IMAPbase headers may contain padding at the end
+ of them, remember how much each message has and offset to beginning of the
+ padding
+ - If header needs to be rewritten and there's enough space, do it
+ - If we didn't have enough space, remember how much was missing
+ - Continue reading and counting the padding in each message. If available
+ padding is enough to rewrite all the previous messages needing it, do it
+ - When we encounter expunged message, treat all of it as padding and
+ rewrite previous messages if needed (and there's enough space).
+ Afterwards keep moving messages backwards to fill the expunged space.
+ Moving is done by rewriting each message's headers, with possibly adding
+ missing Content-Length header and padding. Message bodies are moved
+ without modifications.
+ - If we encounter end of file, grow the file and rewrite needed messages
+ - Rewriting is done by moving message body forward, rewriting message's
+ header and doing the same for previous message, until all of them are
+ rewritten.
+*/
+
+#include "lib.h"
+#include "ioloop.h"
+#include "array.h"
+#include "buffer.h"
+#include "hostpid.h"
+#include "istream.h"
+#include "file-set-size.h"
+#include "str.h"
+#include "read-full.h"
+#include "write-full.h"
+#include "sleep.h"
+#include "message-date.h"
+#include "istream-raw-mbox.h"
+#include "mbox-storage.h"
+#include "index-sync-changes.h"
+#include "mailbox-uidvalidity.h"
+#include "mailbox-recent-flags.h"
+#include "mbox-from.h"
+#include "mbox-file.h"
+#include "mbox-lock.h"
+#include "mbox-sync-private.h"
+
+#include <stddef.h>
+#include <utime.h>
+#include <sys/stat.h>
+
+/* The text below was taken exactly as c-client wrote it to my mailbox,
+ so it's probably copyrighted by University of Washington. */
+#define PSEUDO_MESSAGE_BODY \
+"This text is part of the internal format of your mail folder, and is not\n" \
+"a real message. It is created automatically by the mail system software.\n" \
+"If deleted, important folder data will be lost, and it will be re-created\n" \
+"with the data reset to initial values.\n"
+
+void mbox_sync_set_critical(struct mbox_sync_context *sync_ctx,
+ const char *fmt, ...)
+{
+ va_list va;
+
+ sync_ctx->errors = TRUE;
+ if (sync_ctx->ext_modified) {
+ mailbox_set_critical(&sync_ctx->mbox->box,
+ "mbox was modified while we were syncing, "
+ "check your locking settings");
+ }
+
+ va_start(va, fmt);
+ mailbox_set_critical(&sync_ctx->mbox->box,
+ "Sync failed for mbox: %s",
+ t_strdup_vprintf(fmt, va));
+ va_end(va);
+}
+
+int mbox_sync_seek(struct mbox_sync_context *sync_ctx, uoff_t from_offset)
+{
+ if (istream_raw_mbox_seek(sync_ctx->input, from_offset) < 0) {
+ mbox_sync_set_critical(sync_ctx,
+ "Unexpectedly lost From-line at offset %"PRIuUOFF_T,
+ from_offset);
+ return -1;
+ }
+ return 0;
+}
+
+void mbox_sync_file_update_ext_modified(struct mbox_sync_context *sync_ctx)
+{
+ struct stat st;
+
+ /* Do this even if ext_modified is already set. Expunging code relies
+ on last_stat being updated. */
+ if (fstat(sync_ctx->write_fd, &st) < 0) {
+ mbox_set_syscall_error(sync_ctx->mbox, "fstat()");
+ return;
+ }
+
+ if (st.st_size != sync_ctx->last_stat.st_size ||
+ (sync_ctx->last_stat.st_mtime != 0 &&
+ !CMP_ST_MTIME(&st, &sync_ctx->last_stat)))
+ sync_ctx->ext_modified = TRUE;
+
+ sync_ctx->last_stat = st;
+}
+
+void mbox_sync_file_updated(struct mbox_sync_context *sync_ctx, bool dirty)
+{
+ if (dirty) {
+ /* just mark the stat as dirty. */
+ sync_ctx->last_stat.st_mtime = 0;
+ return;
+ }
+ if (fstat(sync_ctx->write_fd, &sync_ctx->last_stat) < 0)
+ mbox_set_syscall_error(sync_ctx->mbox, "fstat()");
+ i_stream_sync(sync_ctx->input);
+}
+
+static int
+mbox_sync_read_next_mail(struct mbox_sync_context *sync_ctx,
+ struct mbox_sync_mail_context *mail_ctx)
+{
+ uoff_t offset;
+
+ /* get EOF */
+ (void)istream_raw_mbox_get_header_offset(sync_ctx->input, &offset);
+ if (istream_raw_mbox_is_eof(sync_ctx->input))
+ return 0;
+
+ p_clear(sync_ctx->mail_keyword_pool);
+ i_zero(mail_ctx);
+ mail_ctx->sync_ctx = sync_ctx;
+ mail_ctx->seq = ++sync_ctx->seq;
+ mail_ctx->header = sync_ctx->header;
+
+ mail_ctx->mail.from_offset =
+ istream_raw_mbox_get_start_offset(sync_ctx->input);
+ if (istream_raw_mbox_get_header_offset(sync_ctx->input, &mail_ctx->mail.offset) < 0) {
+ mbox_sync_set_critical(sync_ctx,
+ "Couldn't get header offset for seq=%u", mail_ctx->seq);
+ return -1;
+ }
+
+ if (mbox_sync_parse_next_mail(sync_ctx->input, mail_ctx) < 0)
+ return -1;
+ if (istream_raw_mbox_is_corrupted(sync_ctx->input))
+ return -1;
+
+ i_assert(sync_ctx->input->v_offset != mail_ctx->mail.from_offset ||
+ sync_ctx->input->eof);
+
+ if (istream_raw_mbox_get_body_size(sync_ctx->input,
+ mail_ctx->content_length,
+ &mail_ctx->mail.body_size) < 0) {
+ mbox_sync_set_critical(sync_ctx,
+ "Couldn't get body size for seq=%u", mail_ctx->seq);
+ return -1;
+ }
+ i_assert(mail_ctx->mail.body_size < OFF_T_MAX);
+
+ if ((mail_ctx->mail.flags & MAIL_RECENT) != 0 &&
+ !mail_ctx->mail.pseudo) {
+ if (!sync_ctx->keep_recent) {
+ /* need to add 'O' flag to Status-header */
+ mail_ctx->need_rewrite = TRUE;
+ }
+ mail_ctx->recent = TRUE;
+ }
+ return 1;
+}
+
+static void mbox_sync_read_index_syncs(struct mbox_sync_context *sync_ctx,
+ uint32_t uid, bool *sync_expunge_r)
+{
+ guid_128_t expunged_guid_128;
+
+ if (uid == 0 || sync_ctx->index_reset) {
+ /* nothing for this or the future ones */
+ uid = (uint32_t)-1;
+ }
+
+ index_sync_changes_read(sync_ctx->sync_changes, uid, sync_expunge_r,
+ expunged_guid_128);
+ if (sync_ctx->readonly) {
+ /* we can't expunge anything from read-only mboxes */
+ *sync_expunge_r = FALSE;
+ }
+}
+
+static bool
+mbox_sync_read_index_rec(struct mbox_sync_context *sync_ctx,
+ uint32_t uid, const struct mail_index_record **rec_r)
+{
+ const struct mail_index_record *rec = NULL;
+ uint32_t messages_count;
+ bool ret = FALSE;
+
+ if (sync_ctx->index_reset) {
+ *rec_r = NULL;
+ return TRUE;
+ }
+
+ messages_count =
+ mail_index_view_get_messages_count(sync_ctx->sync_view);
+ while (sync_ctx->idx_seq <= messages_count) {
+ rec = mail_index_lookup(sync_ctx->sync_view, sync_ctx->idx_seq);
+ if (uid <= rec->uid)
+ break;
+
+ /* externally expunged message, remove from index */
+ mail_index_expunge(sync_ctx->t, sync_ctx->idx_seq);
+ sync_ctx->idx_seq++;
+ rec = NULL;
+ }
+
+ if (rec == NULL && uid < sync_ctx->idx_next_uid) {
+ /* this UID was already in index and it was expunged */
+ mbox_sync_set_critical(sync_ctx,
+ "Expunged message reappeared to mailbox "
+ "(UID %u < %u, seq=%u, idx_msgs=%u)",
+ uid, sync_ctx->idx_next_uid,
+ sync_ctx->seq, messages_count);
+ ret = FALSE; rec = NULL;
+ } else if (rec != NULL && rec->uid != uid) {
+ /* new UID in the middle of the mailbox - shouldn't happen */
+ mbox_sync_set_critical(sync_ctx,
+ "UID inserted in the middle of mailbox "
+ "(%u > %u, seq=%u, idx_msgs=%u)",
+ rec->uid, uid, sync_ctx->seq, messages_count);
+ ret = FALSE; rec = NULL;
+ } else {
+ ret = TRUE;
+ }
+
+ *rec_r = rec;
+ return ret;
+}
+
+static void mbox_sync_find_index_md5(struct mbox_sync_context *sync_ctx,
+ unsigned char hdr_md5_sum[],
+ const struct mail_index_record **rec_r)
+{
+ const struct mail_index_record *rec = NULL;
+ uint32_t messages_count;
+ const void *data;
+
+ if (sync_ctx->index_reset) {
+ *rec_r = NULL;
+ return;
+ }
+
+ messages_count =
+ mail_index_view_get_messages_count(sync_ctx->sync_view);
+ while (sync_ctx->idx_seq <= messages_count) {
+ rec = mail_index_lookup(sync_ctx->sync_view, sync_ctx->idx_seq);
+ mail_index_lookup_ext(sync_ctx->sync_view,
+ sync_ctx->idx_seq,
+ sync_ctx->mbox->md5hdr_ext_idx,
+ &data, NULL);
+ if (data != NULL && memcmp(data, hdr_md5_sum, 16) == 0)
+ break;
+
+ /* externally expunged message, remove from index */
+ mail_index_expunge(sync_ctx->t, sync_ctx->idx_seq);
+ sync_ctx->idx_seq++;
+ rec = NULL;
+ }
+
+ *rec_r = rec;
+}
+
+static void
+mbox_sync_update_from_offset(struct mbox_sync_context *sync_ctx,
+ struct mbox_sync_mail *mail,
+ bool nocheck)
+{
+ const void *data;
+ uint64_t offset;
+
+ if (!nocheck) {
+ /* see if from_offset needs updating */
+ mail_index_lookup_ext(sync_ctx->sync_view, sync_ctx->idx_seq,
+ sync_ctx->mbox->mbox_ext_idx,
+ &data, NULL);
+ if (data != NULL &&
+ *((const uint64_t *)data) == mail->from_offset)
+ return;
+ }
+
+ offset = mail->from_offset;
+ mail_index_update_ext(sync_ctx->t, sync_ctx->idx_seq,
+ sync_ctx->mbox->mbox_ext_idx, &offset, NULL);
+}
+
+static void
+mbox_sync_update_index_keywords(struct mbox_sync_mail_context *mail_ctx)
+{
+ struct mbox_sync_context *sync_ctx = mail_ctx->sync_ctx;
+ struct mail_index *index = sync_ctx->mbox->box.index;
+ struct mail_keywords *keywords;
+
+ keywords = !array_is_created(&mail_ctx->mail.keywords) ?
+ mail_index_keywords_create(index, NULL) :
+ mail_index_keywords_create_from_indexes(index,
+ &mail_ctx->mail.keywords);
+ mail_index_update_keywords(sync_ctx->t, sync_ctx->idx_seq,
+ MODIFY_REPLACE, keywords);
+ mail_index_keywords_unref(&keywords);
+}
+
+static void
+mbox_sync_update_md5_if_changed(struct mbox_sync_mail_context *mail_ctx)
+{
+ struct mbox_sync_context *sync_ctx = mail_ctx->sync_ctx;
+ const void *ext_data;
+
+ mail_index_lookup_ext(sync_ctx->sync_view, sync_ctx->idx_seq,
+ sync_ctx->mbox->md5hdr_ext_idx, &ext_data, NULL);
+ if (ext_data == NULL ||
+ memcmp(mail_ctx->hdr_md5_sum, ext_data, 16) != 0) {
+ mail_index_update_ext(sync_ctx->t, sync_ctx->idx_seq,
+ sync_ctx->mbox->md5hdr_ext_idx,
+ mail_ctx->hdr_md5_sum, NULL);
+ }
+}
+
+static void mbox_sync_get_dirty_flags(struct mbox_sync_mail_context *mail_ctx,
+ const struct mail_index_record *rec)
+{
+ struct mbox_sync_context *sync_ctx = mail_ctx->sync_ctx;
+ ARRAY_TYPE(keyword_indexes) idx_keywords;
+ uint8_t idx_flags, mbox_flags;
+
+ /* default to undirtying the message. it gets added back if
+ flags/keywords don't match what is in the index. */
+ mail_ctx->mail.flags &= ENUM_NEGATE(MAIL_INDEX_MAIL_FLAG_DIRTY);
+
+ /* replace flags */
+ idx_flags = rec->flags & MAIL_FLAGS_NONRECENT;
+ mbox_flags = mail_ctx->mail.flags & MAIL_FLAGS_NONRECENT;
+ if (idx_flags != mbox_flags) {
+ mail_ctx->need_rewrite = TRUE;
+ mail_ctx->mail.flags = (mail_ctx->mail.flags & MAIL_RECENT) |
+ idx_flags | MAIL_INDEX_MAIL_FLAG_DIRTY;
+ }
+
+ /* replace keywords */
+ t_array_init(&idx_keywords, 32);
+ mail_index_lookup_keywords(sync_ctx->sync_view, sync_ctx->idx_seq,
+ &idx_keywords);
+ if (!index_keyword_array_cmp(&idx_keywords, &mail_ctx->mail.keywords)) {
+ mail_ctx->need_rewrite = TRUE;
+ mail_ctx->mail.flags |= MAIL_INDEX_MAIL_FLAG_DIRTY;
+
+ if (!array_is_created(&mail_ctx->mail.keywords)) {
+ p_array_init(&mail_ctx->mail.keywords,
+ sync_ctx->mail_keyword_pool,
+ array_count(&idx_keywords));
+ }
+ array_clear(&mail_ctx->mail.keywords);
+ array_append_array(&mail_ctx->mail.keywords, &idx_keywords);
+ }
+}
+
+static void mbox_sync_update_flags(struct mbox_sync_mail_context *mail_ctx,
+ const struct mail_index_record *rec)
+{
+ struct mbox_sync_context *sync_ctx = mail_ctx->sync_ctx;
+ struct mailbox *box = &sync_ctx->mbox->box;
+ struct mbox_sync_mail *mail = &mail_ctx->mail;
+ enum mail_index_sync_type sync_type;
+ ARRAY_TYPE(keyword_indexes) orig_keywords = ARRAY_INIT;
+ uint8_t flags, orig_flags;
+
+ if (rec != NULL) {
+ if ((rec->flags & MAIL_INDEX_MAIL_FLAG_DIRTY) != 0) {
+ /* flags and keywords are dirty. replace the current
+ ones from the flags in index file. */
+ mbox_sync_get_dirty_flags(mail_ctx, rec);
+ }
+ }
+
+ flags = orig_flags = mail->flags & MAIL_FLAGS_NONRECENT;
+ if (array_is_created(&mail->keywords)) {
+ t_array_init(&orig_keywords, 32);
+ array_append_array(&orig_keywords, &mail->keywords);
+ }
+
+ /* apply new changes */
+ index_sync_changes_apply(sync_ctx->sync_changes,
+ sync_ctx->mail_keyword_pool,
+ &flags, &mail->keywords, &sync_type);
+ if (flags != orig_flags ||
+ !index_keyword_array_cmp(&mail->keywords, &orig_keywords)) {
+ mail_ctx->need_rewrite = TRUE;
+ mail->flags = flags | (mail->flags & MAIL_RECENT) |
+ MAIL_INDEX_MAIL_FLAG_DIRTY;
+ }
+ if (sync_type != 0) {
+ mailbox_sync_notify(box, mail_ctx->mail.uid,
+ index_sync_type_convert(sync_type));
+ }
+}
+
+static void mbox_sync_update_index(struct mbox_sync_mail_context *mail_ctx,
+ const struct mail_index_record *rec)
+{
+ struct mbox_sync_context *sync_ctx = mail_ctx->sync_ctx;
+ struct mbox_sync_mail *mail = &mail_ctx->mail;
+ ARRAY_TYPE(keyword_indexes) idx_keywords;
+ uint8_t mbox_flags;
+
+ mbox_flags = mail->flags & ENUM_NEGATE(MAIL_RECENT);
+ if (!sync_ctx->delay_writes) {
+ /* changes are written to the mbox file */
+ mbox_flags &= ENUM_NEGATE(MAIL_INDEX_MAIL_FLAG_DIRTY);
+ } else if (mail_ctx->need_rewrite) {
+ /* make sure this message gets written later */
+ mbox_flags |= MAIL_INDEX_MAIL_FLAG_DIRTY;
+ }
+
+ if (rec == NULL) {
+ /* new message */
+ mail_index_append(sync_ctx->t, mail->uid, &sync_ctx->idx_seq);
+ mail_index_update_flags(sync_ctx->t, sync_ctx->idx_seq,
+ MODIFY_REPLACE, mbox_flags);
+ mbox_sync_update_index_keywords(mail_ctx);
+
+ if (sync_ctx->mbox->mbox_save_md5) {
+ mail_index_update_ext(sync_ctx->t, sync_ctx->idx_seq,
+ sync_ctx->mbox->md5hdr_ext_idx,
+ mail_ctx->hdr_md5_sum, NULL);
+ }
+ } else {
+ if ((rec->flags & MAIL_FLAGS_NONRECENT) !=
+ (mbox_flags & MAIL_FLAGS_NONRECENT)) {
+ /* flags other than recent/dirty have changed */
+ mail_index_update_flags(sync_ctx->t, sync_ctx->idx_seq,
+ MODIFY_REPLACE, mbox_flags);
+ } else if (((rec->flags ^ mbox_flags) &
+ MAIL_INDEX_MAIL_FLAG_DIRTY) != 0) {
+ /* only dirty flag state changed */
+ bool dirty;
+
+ dirty = (mbox_flags & MAIL_INDEX_MAIL_FLAG_DIRTY) != 0;
+ mail_index_update_flags(sync_ctx->t, sync_ctx->idx_seq,
+ dirty ? MODIFY_ADD : MODIFY_REMOVE,
+ (enum mail_flags)MAIL_INDEX_MAIL_FLAG_DIRTY);
+ }
+
+ /* see if keywords changed */
+ t_array_init(&idx_keywords, 32);
+ mail_index_lookup_keywords(sync_ctx->sync_view,
+ sync_ctx->idx_seq, &idx_keywords);
+ if (!index_keyword_array_cmp(&idx_keywords, &mail->keywords))
+ mbox_sync_update_index_keywords(mail_ctx);
+
+ /* see if we need to update md5 sum. */
+ if (sync_ctx->mbox->mbox_save_md5)
+ mbox_sync_update_md5_if_changed(mail_ctx);
+ }
+
+ if (!mail_ctx->recent) {
+ /* Mail has "Status: O" header. No messages before this
+ can be recent. */
+ sync_ctx->last_nonrecent_uid = mail->uid;
+ }
+
+ /* update from_offsets, but not if we're going to rewrite this message.
+ rewriting would just move it anyway. */
+ if (sync_ctx->need_space_seq == 0) {
+ bool nocheck = rec == NULL || sync_ctx->expunged_space > 0;
+ mbox_sync_update_from_offset(sync_ctx, mail, nocheck);
+ }
+}
+
+static int mbox_read_from_line(struct mbox_sync_mail_context *ctx)
+{
+ struct istream *input = ctx->sync_ctx->file_input;
+ const unsigned char *data;
+ size_t size, from_line_size;
+
+ buffer_set_used_size(ctx->sync_ctx->from_line, 0);
+ from_line_size = ctx->hdr_offset - ctx->mail.from_offset;
+
+ i_stream_seek(input, ctx->mail.from_offset);
+ for (;;) {
+ data = i_stream_get_data(input, &size);
+ if (size >= from_line_size)
+ size = from_line_size;
+
+ buffer_append(ctx->sync_ctx->from_line, data, size);
+ i_stream_skip(input, size);
+ from_line_size -= size;
+
+ if (from_line_size == 0)
+ break;
+
+ if (i_stream_read(input) < 0)
+ return -1;
+ }
+
+ return 0;
+}
+
+static int mbox_rewrite_base_uid_last(struct mbox_sync_context *sync_ctx)
+{
+ unsigned char buf[10];
+ const char *str;
+ uint32_t uid_last;
+ unsigned int i;
+ int ret;
+
+ i_assert(sync_ctx->base_uid_last_offset != 0);
+
+ /* first check that the 10 bytes are there and they're exactly as
+ expected. just an extra safety check to make sure we never write
+ to wrong location in the mbox file. */
+ ret = pread_full(sync_ctx->write_fd, buf, sizeof(buf),
+ sync_ctx->base_uid_last_offset);
+ if (ret < 0) {
+ mbox_set_syscall_error(sync_ctx->mbox, "pread_full()");
+ return -1;
+ }
+ if (ret == 0) {
+ mbox_sync_set_critical(sync_ctx,
+ "X-IMAPbase uid-last offset unexpectedly outside mbox");
+ return -1;
+ }
+
+ for (i = 0, uid_last = 0; i < sizeof(buf); i++) {
+ if (buf[i] < '0' || buf[i] > '9') {
+ uid_last = (uint32_t)-1;
+ break;
+ }
+ uid_last = uid_last * 10 + (buf[i] - '0');
+ }
+
+ if (uid_last != sync_ctx->base_uid_last) {
+ mbox_sync_set_critical(sync_ctx,
+ "X-IMAPbase uid-last unexpectedly lost");
+ return -1;
+ }
+
+ /* and write it */
+ str = t_strdup_printf("%010u", sync_ctx->next_uid - 1);
+ if (pwrite_full(sync_ctx->write_fd, str, 10,
+ sync_ctx->base_uid_last_offset) < 0) {
+ mbox_set_syscall_error(sync_ctx->mbox, "pwrite_full()");
+ return -1;
+ }
+ mbox_sync_file_updated(sync_ctx, FALSE);
+
+ sync_ctx->base_uid_last = sync_ctx->next_uid - 1;
+ return 0;
+}
+
+static int
+mbox_write_from_line(struct mbox_sync_mail_context *ctx)
+{
+ string_t *str = ctx->sync_ctx->from_line;
+
+ if (pwrite_full(ctx->sync_ctx->write_fd, str_data(str), str_len(str),
+ ctx->mail.from_offset) < 0) {
+ mbox_set_syscall_error(ctx->sync_ctx->mbox, "pwrite_full()");
+ return -1;
+ }
+
+ mbox_sync_file_updated(ctx->sync_ctx, FALSE);
+ return 0;
+}
+
+static void update_from_offsets(struct mbox_sync_context *sync_ctx)
+{
+ const struct mbox_sync_mail *mails;
+ unsigned int i, count;
+ uint32_t ext_idx;
+ uint64_t offset;
+
+ ext_idx = sync_ctx->mbox->mbox_ext_idx;
+
+ mails = array_get(&sync_ctx->mails, &count);
+ for (i = 0; i < count; i++) {
+ if (mails[i].idx_seq == 0 || mails[i].expunged)
+ continue;
+
+ sync_ctx->moved_offsets = TRUE;
+ offset = mails[i].from_offset;
+ mail_index_update_ext(sync_ctx->t, mails[i].idx_seq,
+ ext_idx, &offset, NULL);
+ }
+}
+
+static void mbox_sync_handle_expunge(struct mbox_sync_mail_context *mail_ctx)
+{
+ struct mbox_sync_context *sync_ctx = mail_ctx->sync_ctx;
+ struct mailbox *box = &sync_ctx->mbox->box;
+
+ mailbox_sync_notify(box, mail_ctx->mail.uid,
+ MAILBOX_SYNC_TYPE_EXPUNGE);
+ mail_index_expunge(sync_ctx->t, mail_ctx->mail.idx_seq);
+
+ mail_ctx->mail.expunged = TRUE;
+ mail_ctx->mail.offset = mail_ctx->mail.from_offset;
+ mail_ctx->mail.space =
+ mail_ctx->body_offset - mail_ctx->mail.from_offset +
+ mail_ctx->mail.body_size;
+ mail_ctx->mail.body_size = 0;
+ mail_ctx->mail.uid = 0;
+
+ if (sync_ctx->seq == 1) {
+ /* expunging first message, fix space to contain next
+ message's \n header too since it will be removed. */
+ mail_ctx->mail.space++;
+ if (istream_raw_mbox_has_crlf_ending(sync_ctx->input)) {
+ mail_ctx->mail.space++;
+ sync_ctx->first_mail_crlf_expunged = TRUE;
+ }
+
+ /* uid-last offset is invalid now */
+ sync_ctx->base_uid_last_offset = 0;
+ }
+
+ sync_ctx->expunged_space += mail_ctx->mail.space;
+}
+
+static int mbox_sync_handle_header(struct mbox_sync_mail_context *mail_ctx)
+{
+ struct mbox_sync_context *sync_ctx = mail_ctx->sync_ctx;
+ uoff_t orig_from_offset, postlf_from_offset = UOFF_T_MAX;
+ off_t move_diff;
+ int ret;
+
+ if (sync_ctx->expunged_space > 0 && sync_ctx->need_space_seq == 0) {
+ /* move the header backwards to fill expunged space */
+ move_diff = -sync_ctx->expunged_space;
+
+ orig_from_offset = mail_ctx->mail.from_offset;
+ if (sync_ctx->dest_first_mail) {
+ /* we're moving this mail to beginning of file.
+ skip the initial \n (it's already counted in
+ expunged_space) */
+ mail_ctx->mail.from_offset++;
+ if (sync_ctx->first_mail_crlf_expunged)
+ mail_ctx->mail.from_offset++;
+ }
+ postlf_from_offset = mail_ctx->mail.from_offset;
+
+ /* read the From-line before rewriting overwrites it */
+ if (mbox_read_from_line(mail_ctx) < 0)
+ return -1;
+ i_assert((off_t)mail_ctx->mail.from_offset + move_diff != 1 &&
+ (off_t)mail_ctx->mail.from_offset + move_diff != 2);
+
+ mbox_sync_update_header(mail_ctx);
+ ret = mbox_sync_try_rewrite(mail_ctx, move_diff);
+ if (ret < 0)
+ return -1;
+
+ if (ret > 0) {
+ /* rewrite successful, write From-line to
+ new location */
+ i_assert((off_t)mail_ctx->mail.from_offset >=
+ -move_diff);
+ mail_ctx->mail.from_offset = (off_t)mail_ctx->mail.from_offset + move_diff;
+ mail_ctx->mail.offset = (off_t)mail_ctx->mail.offset + move_diff;
+ if (mbox_write_from_line(mail_ctx) < 0)
+ return -1;
+ } else {
+ if (sync_ctx->dest_first_mail) {
+ /* didn't have enough space, move the offset
+ back so seeking into it doesn't fail */
+ mail_ctx->mail.from_offset = orig_from_offset;
+ }
+ }
+ } else if (mail_ctx->need_rewrite) {
+ mbox_sync_update_header(mail_ctx);
+ if (sync_ctx->delay_writes && sync_ctx->need_space_seq == 0) {
+ /* mark it dirty and do it later. we can't do this
+ if we're in the middle of rewriting acquiring more
+ space. */
+ mail_ctx->dirty = TRUE;
+ return 0;
+ }
+
+ if ((ret = mbox_sync_try_rewrite(mail_ctx, 0)) < 0)
+ return -1;
+ } else {
+ /* nothing to do */
+ return 0;
+ }
+
+ if (ret == 0 && sync_ctx->need_space_seq == 0) {
+ /* first mail with no space to write it */
+ sync_ctx->need_space_seq = sync_ctx->seq;
+ sync_ctx->space_diff = 0;
+
+ if (sync_ctx->expunged_space > 0) {
+ /* create dummy message to describe the expunged data */
+ struct mbox_sync_mail mail;
+
+ /* if this is going to be the first mail, increase the
+ from_offset to point to the beginning of the
+ From-line, because the previous [CR]LF is already
+ covered by expunged_space. */
+ i_assert(postlf_from_offset != UOFF_T_MAX);
+ mail_ctx->mail.from_offset = postlf_from_offset;
+
+ i_zero(&mail);
+ mail.expunged = TRUE;
+ mail.offset = mail.from_offset =
+ mail_ctx->mail.from_offset -
+ sync_ctx->expunged_space;
+ mail.space = sync_ctx->expunged_space;
+
+ sync_ctx->space_diff = sync_ctx->expunged_space;
+ sync_ctx->expunged_space = 0;
+ i_assert(sync_ctx->space_diff < -mail_ctx->mail.space);
+
+ sync_ctx->need_space_seq--;
+ array_push_back(&sync_ctx->mails, &mail);
+ }
+ }
+ return 0;
+}
+
+static int
+mbox_sync_handle_missing_space(struct mbox_sync_mail_context *mail_ctx)
+{
+ struct mbox_sync_context *sync_ctx = mail_ctx->sync_ctx;
+ uoff_t end_offset, move_diff, extra_space, needed_space;
+ uint32_t last_seq;
+ ARRAY_TYPE(keyword_indexes) keywords_copy;
+
+ i_assert(mail_ctx->mail.uid == 0 || mail_ctx->mail.space > 0 ||
+ mail_ctx->mail.offset == mail_ctx->hdr_offset);
+
+ if (array_is_created(&mail_ctx->mail.keywords)) {
+ /* mail's keywords are allocated from a pool that's cleared
+ for each mail. we'll need to copy it to something more
+ permanent. */
+ p_array_init(&keywords_copy, sync_ctx->saved_keywords_pool,
+ array_count(&mail_ctx->mail.keywords));
+ array_append_array(&keywords_copy, &mail_ctx->mail.keywords);
+ mail_ctx->mail.keywords = keywords_copy;
+ }
+ array_push_back(&sync_ctx->mails, &mail_ctx->mail);
+
+ sync_ctx->space_diff += mail_ctx->mail.space;
+ if (sync_ctx->space_diff < 0) {
+ if (sync_ctx->expunged_space > 0) {
+ i_assert(sync_ctx->expunged_space ==
+ mail_ctx->mail.space);
+ sync_ctx->expunged_space = 0;
+ }
+ return 0;
+ }
+
+ /* we have enough space now */
+ if (mail_ctx->mail.uid == 0) {
+ /* this message was expunged. fill more or less of the space.
+ space_diff now consists of a negative "bytes needed" sum,
+ plus the expunged space of this message. so it contains how
+ many bytes of _extra_ space we have. */
+ i_assert(mail_ctx->mail.space >= sync_ctx->space_diff);
+ extra_space = MBOX_HEADER_PADDING *
+ (sync_ctx->seq - sync_ctx->need_space_seq + 1);
+ needed_space = mail_ctx->mail.space - sync_ctx->space_diff;
+ if ((uoff_t)sync_ctx->space_diff > needed_space + extra_space) {
+ /* don't waste too much on padding */
+ move_diff = needed_space + extra_space;
+ sync_ctx->expunged_space =
+ mail_ctx->mail.space - move_diff;
+ } else {
+ move_diff = mail_ctx->mail.space;
+ extra_space = sync_ctx->space_diff;
+ sync_ctx->expunged_space = 0;
+ }
+ last_seq = sync_ctx->seq - 1;
+ array_pop_back(&sync_ctx->mails);
+ end_offset = mail_ctx->mail.from_offset;
+ } else {
+ /* this message gave enough space from headers. rewriting stops
+ at the end of this message's headers. */
+ sync_ctx->expunged_space = 0;
+ last_seq = sync_ctx->seq;
+ end_offset = mail_ctx->body_offset;
+
+ move_diff = 0;
+ extra_space = sync_ctx->space_diff;
+ }
+
+ mbox_sync_file_update_ext_modified(sync_ctx);
+ if (mbox_sync_rewrite(sync_ctx,
+ last_seq == sync_ctx->seq ? mail_ctx : NULL,
+ end_offset, move_diff, extra_space,
+ sync_ctx->need_space_seq, last_seq) < 0)
+ return -1;
+
+ update_from_offsets(sync_ctx);
+
+ /* mail_ctx may contain wrong data after rewrite, so make sure we
+ don't try to access it */
+ i_zero(mail_ctx);
+
+ sync_ctx->need_space_seq = 0;
+ sync_ctx->space_diff = 0;
+ array_clear(&sync_ctx->mails);
+ p_clear(sync_ctx->saved_keywords_pool);
+ return 0;
+}
+
+static int
+mbox_sync_seek_to_seq(struct mbox_sync_context *sync_ctx, uint32_t seq)
+{
+ struct mbox_mailbox *mbox = sync_ctx->mbox;
+ uoff_t old_offset, offset;
+ uint32_t uid;
+ int ret;
+ bool deleted;
+
+ if (seq == 0) {
+ if (istream_raw_mbox_seek(mbox->mbox_stream, 0) < 0) {
+ mbox->invalid_mbox_file = TRUE;
+ mail_storage_set_error(&mbox->storage->storage,
+ MAIL_ERROR_NOTPOSSIBLE,
+ "Mailbox isn't a valid mbox file");
+ return -1;
+ }
+ seq++;
+ } else {
+ old_offset = istream_raw_mbox_get_start_offset(sync_ctx->input);
+
+ ret = mbox_file_seek(mbox, sync_ctx->sync_view, seq, &deleted);
+ if (ret < 0) {
+ if (deleted) {
+ mbox_sync_set_critical(sync_ctx,
+ "Message was expunged unexpectedly");
+ }
+ return -1;
+ }
+ if (ret == 0) {
+ if (istream_raw_mbox_seek(mbox->mbox_stream,
+ old_offset) < 0) {
+ mbox_sync_set_critical(sync_ctx,
+ "Error seeking back to original "
+ "offset %s", dec2str(old_offset));
+ return -1;
+ }
+ return 0;
+ }
+ }
+
+ if (seq <= 1)
+ uid = 0;
+ else
+ mail_index_lookup_uid(sync_ctx->sync_view, seq-1, &uid);
+
+ sync_ctx->prev_msg_uid = uid;
+
+ /* set to -1, since it's always increased later */
+ sync_ctx->seq = seq-1;
+ if (sync_ctx->seq == 0 &&
+ istream_raw_mbox_get_start_offset(sync_ctx->input) != 0) {
+ /* this mbox has pseudo mail which contains the X-IMAP header */
+ sync_ctx->seq++;
+ }
+
+ sync_ctx->idx_seq = seq;
+ sync_ctx->dest_first_mail = sync_ctx->seq == 0;
+ if (istream_raw_mbox_get_body_offset(sync_ctx->input, &offset) < 0) {
+ mbox_sync_set_critical(sync_ctx,
+ "Message body offset lookup failed");
+ return -1;
+ }
+ return 1;
+}
+
+static int
+mbox_sync_seek_to_uid(struct mbox_sync_context *sync_ctx, uint32_t uid)
+{
+ struct mail_index_view *sync_view = sync_ctx->sync_view;
+ uint32_t seq1, seq2;
+ uoff_t size;
+ int ret;
+
+ i_assert(!sync_ctx->index_reset);
+
+ if (!mail_index_lookup_seq_range(sync_view, uid, (uint32_t)-1,
+ &seq1, &seq2)) {
+ /* doesn't exist anymore, seek to end of file */
+ ret = i_stream_get_size(sync_ctx->file_input, TRUE, &size);
+ if (ret < 0) {
+ mbox_istream_set_syscall_error(sync_ctx->mbox,
+ sync_ctx->file_input, "i_stream_get_size()");
+ return -1;
+ }
+ i_assert(ret != 0);
+
+ if (istream_raw_mbox_seek(sync_ctx->mbox->mbox_stream,
+ size) < 0) {
+ mbox_sync_set_critical(sync_ctx,
+ "Error seeking to end of mbox");
+ return -1;
+ }
+ sync_ctx->idx_seq =
+ mail_index_view_get_messages_count(sync_view) + 1;
+ return 1;
+ }
+
+ return mbox_sync_seek_to_seq(sync_ctx, seq1);
+}
+
+static int mbox_sync_partial_seek_next(struct mbox_sync_context *sync_ctx,
+ uint32_t next_uid, bool *partial,
+ bool *skipped_mails)
+{
+ uint32_t messages_count, uid;
+ int ret;
+
+ i_assert(!sync_ctx->index_reset);
+
+ /* delete sync records up to next message. so if there's still
+ something left in array, it means the next message needs modifying */
+ index_sync_changes_delete_to(sync_ctx->sync_changes, next_uid);
+ if (index_sync_changes_have(sync_ctx->sync_changes))
+ return 1;
+
+ if (sync_ctx->hdr->first_recent_uid <= next_uid &&
+ !sync_ctx->keep_recent) {
+ /* we'll need to rewrite Status: O headers */
+ return 1;
+ }
+
+ uid = index_sync_changes_get_next_uid(sync_ctx->sync_changes);
+
+ if (sync_ctx->hdr->first_recent_uid < sync_ctx->hdr->next_uid &&
+ (uid > sync_ctx->hdr->first_recent_uid || uid == 0) &&
+ !sync_ctx->keep_recent) {
+ /* we'll need to rewrite Status: O headers */
+ uid = sync_ctx->hdr->first_recent_uid;
+ }
+
+ if (uid != 0) {
+ /* we can skip forward to next record which needs updating. */
+ if (uid != next_uid) {
+ *skipped_mails = TRUE;
+ next_uid = uid;
+ }
+ ret = mbox_sync_seek_to_uid(sync_ctx, next_uid);
+ } else {
+ /* if there's no sync records left, we can stop. except if
+ this is a dirty sync, check if there are new messages. */
+ if (sync_ctx->mbox->mbox_hdr.dirty_flag == 0)
+ return 0;
+
+ messages_count =
+ mail_index_view_get_messages_count(sync_ctx->sync_view);
+ if (sync_ctx->seq + 1 != messages_count) {
+ ret = mbox_sync_seek_to_seq(sync_ctx, messages_count);
+ *skipped_mails = TRUE;
+ } else {
+ ret = 1;
+ }
+ *partial = FALSE;
+ }
+
+ if (ret == 0) {
+ /* seek failed because the offset is dirty. just ignore and
+ continue from where we are now. */
+ *partial = FALSE;
+ ret = 1;
+ }
+ return ret;
+}
+
+static void mbox_sync_hdr_update(struct mbox_sync_context *sync_ctx,
+ struct mbox_sync_mail_context *mail_ctx)
+{
+ const struct mailbox_update *update = sync_ctx->mbox->sync_hdr_update;
+
+ if (update->uid_validity != 0) {
+ sync_ctx->base_uid_validity = update->uid_validity;
+ mail_ctx->imapbase_rewrite = TRUE;
+ mail_ctx->need_rewrite = TRUE;
+ }
+ if (update->min_next_uid != 0 &&
+ sync_ctx->base_uid_last+1 < update->min_next_uid) {
+ i_assert(sync_ctx->next_uid <= update->min_next_uid);
+ sync_ctx->base_uid_last = update->min_next_uid-1;
+ sync_ctx->next_uid = update->min_next_uid;
+ mail_ctx->imapbase_rewrite = TRUE;
+ mail_ctx->need_rewrite = TRUE;
+ }
+}
+
+static bool mbox_sync_imapbase(struct mbox_sync_context *sync_ctx,
+ struct mbox_sync_mail_context *mail_ctx)
+{
+ if (sync_ctx->base_uid_validity != 0 &&
+ sync_ctx->hdr->uid_validity != 0 &&
+ sync_ctx->base_uid_validity != sync_ctx->hdr->uid_validity) {
+ i_warning("UIDVALIDITY changed (%u -> %u) in mbox file %s",
+ sync_ctx->hdr->uid_validity,
+ sync_ctx->base_uid_validity,
+ mailbox_get_path(&sync_ctx->mbox->box));
+ sync_ctx->index_reset = TRUE;
+ return TRUE;
+ }
+ if (sync_ctx->mbox->sync_hdr_update != NULL)
+ mbox_sync_hdr_update(sync_ctx, mail_ctx);
+ return FALSE;
+}
+
+static int mbox_sync_loop(struct mbox_sync_context *sync_ctx,
+ struct mbox_sync_mail_context *mail_ctx,
+ bool partial)
+{
+ const struct mail_index_record *rec;
+ uint32_t uid, messages_count;
+ uoff_t offset;
+ int ret;
+ bool expunged, skipped_mails, uids_broken;
+
+ messages_count =
+ mail_index_view_get_messages_count(sync_ctx->sync_view);
+
+ /* always start from first message so we can read X-IMAP or
+ X-IMAPbase header */
+ ret = mbox_sync_seek_to_seq(sync_ctx, 0);
+ if (ret <= 0)
+ return ret;
+
+ if (sync_ctx->renumber_uids) {
+ /* expunge everything */
+ while (sync_ctx->idx_seq <= messages_count) {
+ mail_index_expunge(sync_ctx->t,
+ sync_ctx->idx_seq++);
+ }
+ }
+
+ skipped_mails = uids_broken = FALSE;
+ while ((ret = mbox_sync_read_next_mail(sync_ctx, mail_ctx)) > 0) {
+ uid = mail_ctx->mail.uid;
+
+ if (mail_ctx->seq == 1) {
+ if (mbox_sync_imapbase(sync_ctx, mail_ctx)) {
+ sync_ctx->mbox->mbox_hdr.dirty_flag = 1;
+ return 0;
+ }
+ }
+
+ if (mail_ctx->mail.uid_broken && partial) {
+ /* UID ordering problems, resync everything to make
+ sure we get everything right */
+ if (sync_ctx->mbox->mbox_hdr.dirty_flag != 0)
+ return 0;
+
+ mbox_sync_set_critical(sync_ctx,
+ "UIDs broken with partial sync");
+
+ sync_ctx->mbox->mbox_hdr.dirty_flag = 1;
+ return 0;
+ }
+ if (mail_ctx->mail.uid_broken)
+ uids_broken = TRUE;
+
+ if (mail_ctx->mail.pseudo)
+ uid = 0;
+
+ rec = NULL; ret = 1;
+ if (uid != 0) {
+ if (!mbox_sync_read_index_rec(sync_ctx, uid, &rec))
+ ret = 0;
+ }
+
+ if (ret == 0) {
+ /* UID found but it's broken */
+ uid = 0;
+ } else if (uid == 0 &&
+ !mail_ctx->mail.pseudo &&
+ (sync_ctx->delay_writes ||
+ sync_ctx->idx_seq <= messages_count)) {
+ /* If we can't use/store X-UID header, use MD5 sum.
+ Also check for existing MD5 sums when we're actually
+ able to write X-UIDs. */
+ sync_ctx->mbox->mbox_save_md5 = TRUE;
+
+ mbox_sync_find_index_md5(sync_ctx,
+ mail_ctx->hdr_md5_sum, &rec);
+ if (rec != NULL)
+ uid = mail_ctx->mail.uid = rec->uid;
+ }
+
+ /* get all sync records related to this message. with pseudo
+ message just get the first sync record so we can jump to
+ it with partial seeking. */
+ mbox_sync_read_index_syncs(sync_ctx,
+ mail_ctx->mail.pseudo ? 1 : uid,
+ &expunged);
+
+ if (mail_ctx->mail.pseudo) {
+ /* if it was set, it was for the next message */
+ expunged = FALSE;
+ } else {
+ if (rec == NULL) {
+ /* message wasn't found from index. we have to
+ read everything from now on, no skipping */
+ partial = FALSE;
+ }
+ }
+
+ if (uid == 0 && !mail_ctx->mail.pseudo) {
+ /* missing/broken X-UID. all the rest of the mails
+ need new UIDs. */
+ while (sync_ctx->idx_seq <= messages_count) {
+ mail_index_expunge(sync_ctx->t,
+ sync_ctx->idx_seq++);
+ }
+
+ if (sync_ctx->next_uid == (uint32_t)-1) {
+ /* oh no, we're out of UIDs. this shouldn't
+ happen normally, so just try to get it fixed
+ without crashing. */
+ mailbox_set_critical(&sync_ctx->mbox->box,
+ "Out of UIDs, renumbering them in mbox");
+ sync_ctx->renumber_uids = TRUE;
+ return 0;
+ }
+
+ mail_ctx->need_rewrite = TRUE;
+ mail_ctx->mail.uid = sync_ctx->next_uid++;
+ }
+ sync_ctx->prev_msg_uid = mail_ctx->mail.uid;
+
+ if (!mail_ctx->mail.pseudo)
+ mail_ctx->mail.idx_seq = sync_ctx->idx_seq;
+
+ if (!expunged) {
+ if (!mail_ctx->mail.pseudo) T_BEGIN {
+ mbox_sync_update_flags(mail_ctx, rec);
+ } T_END;
+ if (mbox_sync_handle_header(mail_ctx) < 0)
+ return -1;
+ sync_ctx->dest_first_mail = FALSE;
+ } else {
+ mbox_sync_handle_expunge(mail_ctx);
+ }
+
+ if (!mail_ctx->mail.pseudo) {
+ if (!expunged) T_BEGIN {
+ mbox_sync_update_index(mail_ctx, rec);
+ } T_END;
+ sync_ctx->idx_seq++;
+ }
+
+ if (istream_raw_mbox_next(sync_ctx->input,
+ mail_ctx->mail.body_size) < 0)
+ return -1;
+ offset = istream_raw_mbox_get_start_offset(sync_ctx->input);
+
+ if (sync_ctx->need_space_seq != 0) {
+ if (mbox_sync_handle_missing_space(mail_ctx) < 0)
+ return -1;
+ if (mbox_sync_seek(sync_ctx, offset) < 0)
+ return -1;
+ } else if (sync_ctx->expunged_space > 0) {
+ if (!expunged) {
+ /* move the body */
+ mbox_sync_file_update_ext_modified(sync_ctx);
+ if (mbox_move(sync_ctx,
+ mail_ctx->body_offset -
+ sync_ctx->expunged_space,
+ mail_ctx->body_offset,
+ mail_ctx->mail.body_size) < 0)
+ return -1;
+ if (mbox_sync_seek(sync_ctx, offset) < 0)
+ return -1;
+ }
+ } else if (partial) {
+ ret = mbox_sync_partial_seek_next(sync_ctx, uid + 1,
+ &partial,
+ &skipped_mails);
+ if (ret <= 0)
+ break;
+ }
+ }
+ if (ret < 0)
+ return -1;
+
+ if (istream_raw_mbox_is_eof(sync_ctx->input)) {
+ /* rest of the messages in index don't exist -> expunge them */
+ while (sync_ctx->idx_seq <= messages_count)
+ mail_index_expunge(sync_ctx->t, sync_ctx->idx_seq++);
+ }
+
+ if (!skipped_mails)
+ sync_ctx->mbox->mbox_hdr.dirty_flag = 0;
+ sync_ctx->mbox->mbox_broken_offsets = FALSE;
+
+ if (uids_broken && sync_ctx->delay_writes) {
+ /* once we get around to writing the changes, we'll need to do
+ a full sync to avoid the "UIDs broken in partial sync"
+ error */
+ sync_ctx->mbox->mbox_hdr.dirty_flag = 1;
+ }
+ return 1;
+}
+
+static int mbox_write_pseudo(struct mbox_sync_context *sync_ctx, bool force)
+{
+ string_t *str;
+ unsigned int uid_validity;
+
+ i_assert(sync_ctx->write_fd != -1);
+
+ if (sync_ctx->mbox->sync_hdr_update != NULL) {
+ const struct mailbox_update *update =
+ sync_ctx->mbox->sync_hdr_update;
+ bool change = FALSE;
+
+ if (update->uid_validity != 0) {
+ sync_ctx->base_uid_validity = update->uid_validity;
+ change = TRUE;
+ }
+ if (update->min_next_uid != 0) {
+ sync_ctx->base_uid_last = update->min_next_uid-1;
+ change = TRUE;
+ }
+ if (!change && !force)
+ return 0;
+ }
+
+ uid_validity = sync_ctx->base_uid_validity != 0 ?
+ sync_ctx->base_uid_validity : sync_ctx->hdr->uid_validity;
+ i_assert(uid_validity != 0);
+
+ str = t_str_new(1024);
+ str_printfa(str, "%sDate: %s\n"
+ "From: Mail System Internal Data <MAILER-DAEMON@%s>\n"
+ "Subject: DON'T DELETE THIS MESSAGE -- FOLDER INTERNAL DATA"
+ "\nMessage-ID: <%s@%s>\n"
+ "X-IMAP: %u %010u\n"
+ "Status: RO\n"
+ "\n"
+ PSEUDO_MESSAGE_BODY
+ "\n",
+ mbox_from_create("MAILER_DAEMON", ioloop_time),
+ message_date_create(ioloop_time),
+ my_hostname, dec2str(ioloop_time), my_hostname,
+ uid_validity, sync_ctx->next_uid-1);
+
+ if (pwrite_full(sync_ctx->write_fd,
+ str_data(str), str_len(str), 0) < 0) {
+ if (!ENOSPACE(errno)) {
+ mbox_set_syscall_error(sync_ctx->mbox,
+ "pwrite_full()");
+ return -1;
+ }
+
+ /* out of disk space, truncate to empty */
+ if (ftruncate(sync_ctx->write_fd, 0) < 0)
+ mbox_set_syscall_error(sync_ctx->mbox, "ftruncate()");
+ }
+
+ sync_ctx->base_uid_validity = uid_validity;
+ sync_ctx->base_uid_last_offset = 0; /* don't bother calculating */
+ sync_ctx->base_uid_last = sync_ctx->next_uid-1;
+ return 0;
+}
+
+static int mbox_append_zero(struct mbox_sync_context *sync_ctx,
+ uoff_t orig_file_size, uoff_t count)
+{
+ char block[IO_BLOCK_SIZE];
+ uoff_t offset = orig_file_size;
+ ssize_t ret = 0;
+
+ memset(block, 0, I_MIN(sizeof(block), count));
+ while (count > 0) {
+ ret = pwrite(sync_ctx->write_fd, block,
+ I_MIN(sizeof(block), count), offset);
+ if (ret < 0)
+ break;
+ offset += ret;
+ count -= ret;
+ }
+
+ if (ret < 0) {
+ mbox_set_syscall_error(sync_ctx->mbox, "pwrite()");
+ if (ftruncate(sync_ctx->write_fd, orig_file_size) < 0)
+ mbox_set_syscall_error(sync_ctx->mbox, "ftruncate()");
+ return -1;
+ }
+ return 0;
+}
+
+static int mbox_sync_handle_eof_updates(struct mbox_sync_context *sync_ctx,
+ struct mbox_sync_mail_context *mail_ctx)
+{
+ uoff_t file_size, offset, padding, trailer_size;
+ int ret;
+
+ if (!istream_raw_mbox_is_eof(sync_ctx->input)) {
+ i_assert(sync_ctx->need_space_seq == 0);
+ i_assert(sync_ctx->expunged_space == 0);
+ return 0;
+ }
+
+ ret = i_stream_get_size(sync_ctx->file_input, TRUE, &file_size);
+ if (ret < 0) {
+ mbox_istream_set_syscall_error(sync_ctx->mbox,
+ sync_ctx->file_input, "i_stream_get_size()");
+ return -1;
+ }
+ if (ret == 0) {
+ /* Not a file - allow anyway */
+ return 0;
+ }
+
+ if (file_size < sync_ctx->file_input->v_offset) {
+ mbox_sync_set_critical(sync_ctx,
+ "file size unexpectedly shrank "
+ "(%"PRIuUOFF_T" vs %"PRIuUOFF_T")", file_size,
+ sync_ctx->file_input->v_offset);
+ return -1;
+ }
+ trailer_size = file_size - sync_ctx->file_input->v_offset;
+ i_assert(trailer_size <= 2);
+
+ if (sync_ctx->need_space_seq != 0) {
+ i_assert(sync_ctx->write_fd != -1);
+
+ i_assert(sync_ctx->space_diff < 0);
+ padding = MBOX_HEADER_PADDING *
+ (sync_ctx->seq - sync_ctx->need_space_seq + 1);
+ sync_ctx->space_diff -= padding;
+
+ i_assert(sync_ctx->expunged_space <= -sync_ctx->space_diff);
+ sync_ctx->space_diff += sync_ctx->expunged_space;
+ sync_ctx->expunged_space = 0;
+
+ if (mail_ctx->have_eoh && !mail_ctx->updated)
+ str_append_c(mail_ctx->header, '\n');
+
+ i_assert(sync_ctx->space_diff < 0);
+
+ if (mbox_append_zero(sync_ctx, file_size,
+ -sync_ctx->space_diff) < 0)
+ return -1;
+ mbox_sync_file_updated(sync_ctx, FALSE);
+
+ if (mbox_sync_rewrite(sync_ctx, mail_ctx, file_size,
+ -sync_ctx->space_diff, padding,
+ sync_ctx->need_space_seq,
+ sync_ctx->seq) < 0)
+ return -1;
+
+ update_from_offsets(sync_ctx);
+
+ sync_ctx->need_space_seq = 0;
+ array_clear(&sync_ctx->mails);
+ p_clear(sync_ctx->saved_keywords_pool);
+ }
+
+ if (sync_ctx->expunged_space > 0) {
+ i_assert(sync_ctx->write_fd != -1);
+
+ mbox_sync_file_update_ext_modified(sync_ctx);
+
+ /* copy trailer, then truncate the file */
+ file_size = sync_ctx->last_stat.st_size;
+ if (file_size == (uoff_t)sync_ctx->expunged_space) {
+ /* everything deleted, the trailer_size still contains
+ the \n trailer though */
+ trailer_size = 0;
+ } else if (sync_ctx->expunged_space == (off_t)file_size + 1 ||
+ sync_ctx->expunged_space == (off_t)file_size + 2) {
+ /* everything deleted and we didn't have a proper
+ trailer. */
+ trailer_size = 0;
+ sync_ctx->expunged_space = file_size;
+ }
+
+ i_assert(file_size >= sync_ctx->expunged_space + trailer_size);
+ offset = file_size - sync_ctx->expunged_space - trailer_size;
+ i_assert(offset == 0 || offset > 31);
+
+ if (mbox_move(sync_ctx, offset,
+ offset + sync_ctx->expunged_space,
+ trailer_size) < 0)
+ return -1;
+ if (ftruncate(sync_ctx->write_fd,
+ offset + trailer_size) < 0) {
+ mbox_set_syscall_error(sync_ctx->mbox, "ftruncate()");
+ return -1;
+ }
+
+ if (offset == 0) {
+ if (mbox_write_pseudo(sync_ctx, TRUE) < 0)
+ return -1;
+ }
+
+ sync_ctx->expunged_space = 0;
+ mbox_sync_file_updated(sync_ctx, FALSE);
+ } else {
+ if (file_size == 0 && sync_ctx->mbox->sync_hdr_update != NULL) {
+ if (mbox_write_pseudo(sync_ctx, FALSE) < 0)
+ return -1;
+ }
+ }
+ return 0;
+}
+
+static void
+mbox_sync_index_update_ext_header(struct mbox_mailbox *mbox,
+ struct mail_index_transaction *trans)
+{
+ const struct mailbox_update *update = mbox->sync_hdr_update;
+ const void *data;
+ size_t data_size;
+
+ if (update != NULL && !guid_128_is_empty(update->mailbox_guid)) {
+ memcpy(mbox->mbox_hdr.mailbox_guid, update->mailbox_guid,
+ sizeof(mbox->mbox_hdr.mailbox_guid));
+ } else if (guid_128_is_empty(mbox->mbox_hdr.mailbox_guid)) {
+ guid_128_generate(mbox->mbox_hdr.mailbox_guid);
+ }
+
+ mail_index_get_header_ext(mbox->box.view, mbox->mbox_ext_idx,
+ &data, &data_size);
+ if (data_size != sizeof(mbox->mbox_hdr) ||
+ memcmp(data, &mbox->mbox_hdr, data_size) != 0) {
+ if (data_size != sizeof(mbox->mbox_hdr)) {
+ /* upgrading from v1.x */
+ mail_index_ext_resize(trans, mbox->mbox_ext_idx,
+ sizeof(mbox->mbox_hdr),
+ sizeof(uint64_t),
+ sizeof(uint64_t));
+ }
+ mail_index_update_header_ext(trans, mbox->mbox_ext_idx,
+ 0, &mbox->mbox_hdr,
+ sizeof(mbox->mbox_hdr));
+ }
+}
+
+static uint32_t mbox_get_uidvalidity_next(struct mailbox_list *list)
+{
+ const char *path;
+
+ path = mailbox_list_get_root_forced(list, MAILBOX_LIST_PATH_TYPE_CONTROL);
+ path = t_strconcat(path, "/"MBOX_UIDVALIDITY_FNAME, NULL);
+ return mailbox_uidvalidity_next(list, path);
+}
+
+static int mbox_sync_update_index_header(struct mbox_sync_context *sync_ctx)
+{
+ struct mail_index_view *view;
+ const struct stat *st;
+ uint32_t first_recent_uid, seq, seq2;
+
+ if (i_stream_stat(sync_ctx->file_input, FALSE, &st) < 0) {
+ mbox_istream_set_syscall_error(sync_ctx->mbox,
+ sync_ctx->file_input, "i_stream_stat()");
+ return -1;
+ }
+
+ if (sync_ctx->moved_offsets &&
+ ((uint64_t)st->st_size == sync_ctx->mbox->mbox_hdr.sync_size ||
+ (uint64_t)st->st_size == sync_ctx->orig_size)) {
+ /* We moved messages inside the mbox file without changing
+ the file's size. If mtime doesn't change, another process
+ not using the same index file as us can't know that the file
+ was changed. So make sure the mtime changes. This should
+ happen rarely enough that the sleeping doesn't become a
+ performance problem.
+
+ Note that to do this perfectly safe we should do this wait
+ whenever mails are moved or expunged, regardless of whether
+ the file's size changed. That however could become a
+ performance problem and the consequences of being wrong are
+ quite minimal (an extra logged error message). */
+ while (sync_ctx->orig_mtime == st->st_mtime) {
+ i_sleep_msecs(500);
+ if (utime(mailbox_get_path(&sync_ctx->mbox->box), NULL) < 0) {
+ mbox_set_syscall_error(sync_ctx->mbox,
+ "utime()");
+ return -1;
+ }
+
+ if (i_stream_stat(sync_ctx->file_input, FALSE, &st) < 0) {
+ mbox_istream_set_syscall_error(sync_ctx->mbox,
+ sync_ctx->file_input, "i_stream_stat()");
+ return -1;
+ }
+ }
+ }
+
+ sync_ctx->mbox->mbox_hdr.sync_mtime = st->st_mtime;
+ sync_ctx->mbox->mbox_hdr.sync_size = st->st_size;
+ mbox_sync_index_update_ext_header(sync_ctx->mbox, sync_ctx->t);
+
+ /* only reason not to have UID validity at this point is if the file
+ is entirely empty. In that case just make up a new one if needed. */
+ i_assert(sync_ctx->base_uid_validity != 0 || st->st_size <= 0);
+
+ if (sync_ctx->base_uid_validity == 0) {
+ sync_ctx->base_uid_validity = sync_ctx->hdr->uid_validity != 0 ?
+ sync_ctx->hdr->uid_validity :
+ mbox_get_uidvalidity_next(sync_ctx->mbox->box.list);
+ }
+ if (sync_ctx->base_uid_validity != sync_ctx->hdr->uid_validity) {
+ mail_index_update_header(sync_ctx->t,
+ offsetof(struct mail_index_header, uid_validity),
+ &sync_ctx->base_uid_validity,
+ sizeof(sync_ctx->base_uid_validity), TRUE);
+ }
+
+ if (istream_raw_mbox_is_eof(sync_ctx->input) &&
+ sync_ctx->next_uid != sync_ctx->hdr->next_uid) {
+ i_assert(sync_ctx->next_uid != 0);
+ mail_index_update_header(sync_ctx->t,
+ offsetof(struct mail_index_header, next_uid),
+ &sync_ctx->next_uid, sizeof(sync_ctx->next_uid), FALSE);
+ }
+
+ if (sync_ctx->last_nonrecent_uid < sync_ctx->hdr->first_recent_uid) {
+ /* other sessions have already marked more messages as
+ recent. */
+ sync_ctx->last_nonrecent_uid =
+ sync_ctx->hdr->first_recent_uid - 1;
+ }
+
+ /* mark recent messages */
+ view = mail_index_transaction_open_updated_view(sync_ctx->t);
+ if (mail_index_lookup_seq_range(view, sync_ctx->last_nonrecent_uid + 1,
+ (uint32_t)-1, &seq, &seq2)) {
+ mailbox_recent_flags_set_seqs(&sync_ctx->mbox->box,
+ view, seq, seq2);
+ }
+ mail_index_view_close(&view);
+
+ first_recent_uid = !sync_ctx->keep_recent ?
+ sync_ctx->next_uid : sync_ctx->last_nonrecent_uid + 1;
+ if (sync_ctx->hdr->first_recent_uid < first_recent_uid) {
+ mail_index_update_header(sync_ctx->t,
+ offsetof(struct mail_index_header, first_recent_uid),
+ &first_recent_uid, sizeof(first_recent_uid), FALSE);
+ }
+ return 0;
+}
+
+static void mbox_sync_restart(struct mbox_sync_context *sync_ctx)
+{
+ sync_ctx->base_uid_validity = 0;
+ sync_ctx->base_uid_last = 0;
+ sync_ctx->base_uid_last_offset = 0;
+
+ array_clear(&sync_ctx->mails);
+ p_clear(sync_ctx->saved_keywords_pool);
+
+ index_sync_changes_reset(sync_ctx->sync_changes);
+ mail_index_sync_reset(sync_ctx->index_sync_ctx);
+ mail_index_transaction_reset(sync_ctx->t);
+
+ if (sync_ctx->index_reset) {
+ mail_index_reset(sync_ctx->t);
+ sync_ctx->reset_hdr.next_uid = 1;
+ sync_ctx->hdr = &sync_ctx->reset_hdr;
+ mailbox_recent_flags_reset(&sync_ctx->mbox->box);
+ }
+
+ sync_ctx->prev_msg_uid = 0;
+ sync_ctx->next_uid = sync_ctx->hdr->next_uid;
+ sync_ctx->idx_next_uid = sync_ctx->hdr->next_uid;
+ sync_ctx->seq = 0;
+ sync_ctx->idx_seq = 1;
+ sync_ctx->need_space_seq = 0;
+ sync_ctx->expunged_space = 0;
+ sync_ctx->space_diff = 0;
+
+ sync_ctx->dest_first_mail = TRUE;
+ sync_ctx->ext_modified = FALSE;
+ sync_ctx->errors = FALSE;
+}
+
+static int mbox_sync_do(struct mbox_sync_context *sync_ctx,
+ enum mbox_sync_flags flags)
+{
+ struct mbox_index_header *mbox_hdr = &sync_ctx->mbox->mbox_hdr;
+ struct mbox_sync_mail_context mail_ctx;
+ const struct stat *st;
+ unsigned int i;
+ bool partial;
+ int ret;
+
+ if (i_stream_stat(sync_ctx->file_input, FALSE, &st) < 0) {
+ mbox_istream_set_syscall_error(sync_ctx->mbox,
+ sync_ctx->file_input, "i_stream_stat()");
+ return -1;
+ }
+ sync_ctx->last_stat = *st;
+ sync_ctx->orig_size = st->st_size;
+ sync_ctx->orig_atime = st->st_atime;
+ sync_ctx->orig_mtime = st->st_mtime;
+
+ if ((flags & MBOX_SYNC_FORCE_SYNC) != 0) {
+ /* forcing a full sync. assume file has changed. */
+ partial = FALSE;
+ mbox_hdr->dirty_flag = 1;
+ } else if ((uint32_t)st->st_mtime == mbox_hdr->sync_mtime &&
+ (uint64_t)st->st_size == mbox_hdr->sync_size) {
+ /* file is fully synced */
+ if (mbox_hdr->dirty_flag != 0 && (flags & MBOX_SYNC_UNDIRTY) != 0)
+ partial = FALSE;
+ else
+ partial = TRUE;
+ } else if ((flags & MBOX_SYNC_UNDIRTY) != 0 ||
+ (uint64_t)st->st_size == mbox_hdr->sync_size) {
+ /* we want to do full syncing. always do this if
+ file size hasn't changed but timestamp has. it most
+ likely means that someone had modified some header
+ and we probably want to know about it */
+ partial = FALSE;
+ sync_ctx->mbox->mbox_hdr.dirty_flag = 1;
+ } else {
+ /* see if we can delay syncing the whole file.
+ normally we only notice expunges and appends
+ in partial syncing. */
+ partial = TRUE;
+ sync_ctx->mbox->mbox_hdr.dirty_flag = 1;
+ }
+
+ mbox_sync_restart(sync_ctx);
+ for (i = 0;;) {
+ ret = mbox_sync_loop(sync_ctx, &mail_ctx, partial);
+ if (ret > 0 && !sync_ctx->errors)
+ break;
+ if (ret < 0)
+ return -1;
+
+ /* a) partial sync didn't work
+ b) we ran out of UIDs
+ c) syncing had errors */
+ if (sync_ctx->delay_writes &&
+ (sync_ctx->errors || sync_ctx->renumber_uids)) {
+ /* fixing a broken mbox state, be sure to write
+ the changes (except if we're readonly). */
+ if (!sync_ctx->readonly)
+ sync_ctx->delay_writes = FALSE;
+ }
+ if (++i == 3)
+ break;
+
+ mbox_sync_restart(sync_ctx);
+ partial = FALSE;
+ }
+
+ if (mbox_sync_handle_eof_updates(sync_ctx, &mail_ctx) < 0)
+ return -1;
+
+ /* only syncs left should be just appends (and their updates)
+ which weren't synced yet for some reason (crash). we'll just
+ ignore them, as we've overwritten them above. */
+ index_sync_changes_reset(sync_ctx->sync_changes);
+
+ if (sync_ctx->base_uid_last != sync_ctx->next_uid-1 &&
+ ret > 0 && !sync_ctx->delay_writes &&
+ sync_ctx->base_uid_last_offset != 0) {
+ /* Rewrite uid_last in X-IMAPbase header if we've seen it
+ (ie. the file isn't empty) */
+ ret = mbox_rewrite_base_uid_last(sync_ctx);
+ } else {
+ ret = 0;
+ }
+
+ if (mbox_sync_update_index_header(sync_ctx) < 0)
+ return -1;
+ return ret;
+}
+
+int mbox_sync_header_refresh(struct mbox_mailbox *mbox)
+{
+ const void *data;
+ size_t data_size;
+
+ if (mail_index_refresh(mbox->box.index) < 0) {
+ mailbox_set_index_error(&mbox->box);
+ return -1;
+ }
+
+ mail_index_get_header_ext(mbox->box.view, mbox->mbox_ext_idx,
+ &data, &data_size);
+ if (data_size == 0) {
+ /* doesn't exist yet. */
+ i_zero(&mbox->mbox_hdr);
+ return 0;
+ }
+
+ memcpy(&mbox->mbox_hdr, data, I_MIN(sizeof(mbox->mbox_hdr), data_size));
+ if (mbox->mbox_broken_offsets)
+ mbox->mbox_hdr.dirty_flag = 1;
+ return 0;
+}
+
+int mbox_sync_get_guid(struct mbox_mailbox *mbox)
+{
+ struct mail_index_transaction *trans;
+ unsigned int lock_id;
+ int ret;
+
+ if (mbox_lock(mbox, F_WRLCK, &lock_id) <= 0)
+ return -1;
+
+ ret = mbox_sync_header_refresh(mbox);
+ if (ret == 0) {
+ trans = mail_index_transaction_begin(mbox->box.view,
+ MAIL_INDEX_TRANSACTION_FLAG_EXTERNAL);
+ mbox_sync_index_update_ext_header(mbox, trans);
+ ret = mail_index_transaction_commit(&trans);
+ }
+ mbox_unlock(mbox, lock_id);
+ return ret;
+}
+
+int mbox_sync_has_changed(struct mbox_mailbox *mbox, bool leave_dirty)
+{
+ const struct stat *st;
+ struct stat statbuf;
+
+ if (mbox->mbox_file_stream != NULL && mbox->mbox_fd == -1) {
+ /* read-only stream */
+ if (i_stream_stat(mbox->mbox_file_stream, FALSE, &st) < 0) {
+ if (errno == ENOENT) {
+ mailbox_set_deleted(&mbox->box);
+ return 0;
+ }
+ mbox_istream_set_syscall_error(mbox,
+ mbox->mbox_file_stream, "i_stream_stat()");
+ return -1;
+ }
+ } else {
+ if (stat(mailbox_get_path(&mbox->box), &statbuf) < 0) {
+ if (errno == ENOENT) {
+ mailbox_set_deleted(&mbox->box);
+ return 0;
+ }
+ mbox_set_syscall_error(mbox, "stat()");
+ return -1;
+ }
+ st = &statbuf;
+ }
+
+ if (mbox_sync_header_refresh(mbox) < 0)
+ return -1;
+
+ if (guid_128_is_empty(mbox->mbox_hdr.mailbox_guid)) {
+ /* need to assign mailbox GUID */
+ return 1;
+ }
+
+ if ((uint32_t)st->st_mtime == mbox->mbox_hdr.sync_mtime &&
+ (uint64_t)st->st_size == mbox->mbox_hdr.sync_size) {
+ /* fully synced */
+ if (mbox->mbox_hdr.dirty_flag != 0 || leave_dirty)
+ return 0;
+ /* flushing dirtiness */
+ }
+
+ /* file changed */
+ return 1;
+}
+
+static void mbox_sync_context_free(struct mbox_sync_context *sync_ctx)
+{
+ index_sync_changes_deinit(&sync_ctx->sync_changes);
+ index_storage_expunging_deinit(&sync_ctx->mbox->box);
+ if (sync_ctx->index_sync_ctx != NULL)
+ mail_index_sync_rollback(&sync_ctx->index_sync_ctx);
+ pool_unref(&sync_ctx->mail_keyword_pool);
+ pool_unref(&sync_ctx->saved_keywords_pool);
+ str_free(&sync_ctx->header);
+ str_free(&sync_ctx->from_line);
+ array_free(&sync_ctx->mails);
+}
+
+static int mbox_sync_int(struct mbox_mailbox *mbox, enum mbox_sync_flags flags,
+ unsigned int *lock_id)
+{
+ struct mail_index_sync_ctx *index_sync_ctx;
+ struct mail_index_view *sync_view;
+ struct mail_index_transaction *trans;
+ struct mbox_sync_context sync_ctx;
+ enum mail_index_sync_flags sync_flags;
+ int ret;
+ bool changed, delay_writes, readonly;
+
+ readonly = mbox_is_backend_readonly(mbox) ||
+ (flags & MBOX_SYNC_READONLY) != 0;
+ delay_writes = readonly ||
+ ((flags & MBOX_SYNC_REWRITE) == 0 &&
+ mbox->storage->set->mbox_lazy_writes);
+
+ if (!mbox->storage->set->mbox_dirty_syncs &&
+ !mbox->storage->set->mbox_very_dirty_syncs)
+ flags |= MBOX_SYNC_UNDIRTY;
+
+ if ((flags & MBOX_SYNC_LOCK_READING) != 0) {
+ if (mbox_lock(mbox, F_RDLCK, lock_id) <= 0)
+ return -1;
+ }
+
+ if ((flags & MBOX_SYNC_HEADER) != 0 ||
+ (flags & MBOX_SYNC_FORCE_SYNC) != 0) {
+ if (mbox_sync_header_refresh(mbox) < 0)
+ return -1;
+ changed = TRUE;
+ } else {
+ bool leave_dirty = (flags & MBOX_SYNC_UNDIRTY) == 0;
+ if ((ret = mbox_sync_has_changed(mbox, leave_dirty)) < 0)
+ return -1;
+ changed = ret > 0;
+ }
+
+ if ((flags & MBOX_SYNC_LOCK_READING) != 0) {
+ /* we just want to lock it for reading. if mbox hasn't been
+ modified don't do any syncing. */
+ if (!changed)
+ return 0;
+
+ /* have to sync to make sure offsets have stayed the same */
+ mbox_unlock(mbox, *lock_id);
+ *lock_id = 0;
+ }
+
+ /* flush input streams' buffers */
+ if (mbox->mbox_stream != NULL)
+ i_stream_sync(mbox->mbox_stream);
+ if (mbox->mbox_file_stream != NULL)
+ i_stream_sync(mbox->mbox_file_stream);
+
+again:
+ if (changed) {
+ /* we're most likely modifying the mbox while syncing, just
+ lock it for writing immediately. the mbox must be locked
+ before index syncing is started to avoid deadlocks, so we
+ don't have much choice either (well, easy ones anyway). */
+ int lock_type = readonly ? F_RDLCK : F_WRLCK;
+
+ if ((ret = mbox_lock(mbox, lock_type, lock_id)) <= 0) {
+ if (ret == 0 || lock_type == F_RDLCK)
+ return -1;
+
+ /* try as read-only */
+ if (mbox_lock(mbox, F_RDLCK, lock_id) <= 0)
+ return -1;
+ mbox->backend_readonly = readonly = TRUE;
+ mbox->backend_readonly_set = TRUE;
+ delay_writes = TRUE;
+ }
+ }
+
+ sync_flags = index_storage_get_sync_flags(&mbox->box);
+ if ((flags & MBOX_SYNC_REWRITE) != 0)
+ sync_flags |= MAIL_INDEX_SYNC_FLAG_FLUSH_DIRTY;
+
+ ret = index_storage_expunged_sync_begin(&mbox->box, &index_sync_ctx,
+ &sync_view, &trans, sync_flags);
+ if (ret <= 0)
+ return ret;
+
+ if ((mbox->box.flags & MAILBOX_FLAG_DROP_RECENT) != 0) {
+ /* see if we need to drop recent flags */
+ sync_ctx.hdr = mail_index_get_header(sync_view);
+ if (sync_ctx.hdr->first_recent_uid < sync_ctx.hdr->next_uid)
+ changed = TRUE;
+ }
+
+ if (!changed && !mail_index_sync_have_more(index_sync_ctx)) {
+ /* nothing to do */
+ nothing_to_do:
+ /* index may need to do internal syncing though, so commit
+ instead of rolling back. */
+ index_storage_expunging_deinit(&mbox->box);
+ if (mail_index_sync_commit(&index_sync_ctx) < 0) {
+ mailbox_set_index_error(&mbox->box);
+ return -1;
+ }
+ return 0;
+ }
+
+ i_zero(&sync_ctx);
+ sync_ctx.mbox = mbox;
+ sync_ctx.keep_recent =
+ (mbox->box.flags & MAILBOX_FLAG_DROP_RECENT) == 0;
+
+ sync_ctx.hdr = mail_index_get_header(sync_view);
+ sync_ctx.from_line = str_new(default_pool, 256);
+ sync_ctx.header = str_new(default_pool, 4096);
+
+ sync_ctx.index_sync_ctx = index_sync_ctx;
+ sync_ctx.sync_view = sync_view;
+ sync_ctx.t = trans;
+ sync_ctx.mail_keyword_pool =
+ pool_alloconly_create("mbox keywords", 512);
+ sync_ctx.saved_keywords_pool =
+ pool_alloconly_create("mbox saved keywords", 4096);
+
+ /* make sure we've read the latest keywords in index */
+ (void)mail_index_get_keywords(mbox->box.index);
+
+ i_array_init(&sync_ctx.mails, 64);
+
+ sync_ctx.flags = flags;
+ sync_ctx.readonly = readonly;
+ sync_ctx.delay_writes = delay_writes;
+
+ sync_ctx.sync_changes =
+ index_sync_changes_init(index_sync_ctx, sync_view, trans,
+ sync_ctx.delay_writes);
+
+ if (!changed && delay_writes) {
+ /* if we have only flag changes, we don't need to open the
+ mbox file */
+ bool expunged;
+ uint32_t uid;
+
+ mbox_sync_read_index_syncs(&sync_ctx, 1, &expunged);
+ uid = expunged ? 1 :
+ index_sync_changes_get_next_uid(sync_ctx.sync_changes);
+ if (uid == 0) {
+ sync_ctx.index_sync_ctx = NULL;
+ mbox_sync_context_free(&sync_ctx);
+ goto nothing_to_do;
+ }
+ }
+
+ if (*lock_id == 0) {
+ /* ok, we have something to do but no locks. we'll have to
+ restart syncing to avoid deadlocking. */
+ mbox_sync_context_free(&sync_ctx);
+ changed = TRUE;
+ goto again;
+ }
+
+ if (mbox_file_open_stream(mbox) < 0) {
+ mbox_sync_context_free(&sync_ctx);
+ return -1;
+ }
+
+ sync_ctx.file_input = sync_ctx.mbox->mbox_file_stream;
+ sync_ctx.input = sync_ctx.mbox->mbox_stream;
+ sync_ctx.write_fd = sync_ctx.mbox->mbox_lock_type != F_WRLCK ? -1 :
+ sync_ctx.mbox->mbox_fd;
+
+ ret = mbox_sync_do(&sync_ctx, flags);
+
+ if (ret < 0)
+ mail_index_sync_rollback(&index_sync_ctx);
+ else if (mail_index_sync_commit(&index_sync_ctx) < 0) {
+ mailbox_set_index_error(&mbox->box);
+ ret = -1;
+ }
+ sync_ctx.t = NULL;
+ sync_ctx.index_sync_ctx = NULL;
+
+ if (ret == 0 && mbox->mbox_fd != -1 && sync_ctx.keep_recent &&
+ !readonly) {
+ /* try to set atime back to its original value.
+ (it'll fail with EPERM for shared mailboxes where we aren't
+ the file's owner) */
+ struct utimbuf buf;
+ struct stat st;
+
+ if (fstat(mbox->mbox_fd, &st) < 0)
+ mbox_set_syscall_error(mbox, "fstat()");
+ else {
+ buf.modtime = st.st_mtime;
+ buf.actime = sync_ctx.orig_atime;
+ if (utime(mailbox_get_path(&mbox->box), &buf) < 0 &&
+ errno != EPERM)
+ mbox_set_syscall_error(mbox, "utime()");
+ }
+ }
+
+ i_assert(*lock_id != 0);
+
+ if (mbox->storage->storage.set->mail_nfs_storage &&
+ mbox->mbox_fd != -1) {
+ if (fdatasync(mbox->mbox_fd) < 0) {
+ mbox_set_syscall_error(mbox, "fdatasync()");
+ ret = -1;
+ }
+ }
+
+ mbox_sync_context_free(&sync_ctx);
+ return ret;
+}
+
+int mbox_sync(struct mbox_mailbox *mbox, enum mbox_sync_flags flags)
+{
+ unsigned int lock_id = 0;
+ int ret;
+
+ i_assert(mbox->mbox_lock_type != F_RDLCK ||
+ (flags & MBOX_SYNC_READONLY) != 0);
+
+ mbox->syncing = TRUE;
+ ret = mbox_sync_int(mbox, flags, &lock_id);
+ mbox->syncing = FALSE;
+
+ if (lock_id != 0) {
+ if (ret < 0) {
+ /* syncing failed, don't leave it locked */
+ mbox_unlock(mbox, lock_id);
+ } else if ((flags & MBOX_SYNC_LOCK_READING) == 0) {
+ if (mbox_unlock(mbox, lock_id) < 0)
+ ret = -1;
+ } else if (mbox->mbox_lock_type != F_RDLCK) {
+ /* drop to read lock */
+ unsigned int read_lock_id = 0;
+
+ if (mbox_lock(mbox, F_RDLCK, &read_lock_id) <= 0)
+ ret = -1;
+ if (mbox_unlock(mbox, lock_id) < 0)
+ ret = -1;
+ }
+ }
+
+ mailbox_sync_notify(&mbox->box, 0, 0);
+ return ret;
+}
+
+struct mailbox_sync_context *
+mbox_storage_sync_init(struct mailbox *box, enum mailbox_sync_flags flags)
+{
+ struct mbox_mailbox *mbox = MBOX_MAILBOX(box);
+ enum mbox_sync_flags mbox_sync_flags = 0;
+ int ret = 0;
+
+ if (index_mailbox_want_full_sync(&mbox->box, flags)) {
+ if ((flags & MAILBOX_SYNC_FLAG_FULL_READ) != 0 &&
+ !mbox->storage->set->mbox_very_dirty_syncs)
+ mbox_sync_flags |= MBOX_SYNC_UNDIRTY;
+ if ((flags & MAILBOX_SYNC_FLAG_FULL_WRITE) != 0)
+ mbox_sync_flags |= MBOX_SYNC_REWRITE;
+ if ((flags & MAILBOX_SYNC_FLAG_FORCE_RESYNC) != 0) {
+ mbox_sync_flags |= MBOX_SYNC_UNDIRTY |
+ MBOX_SYNC_REWRITE | MBOX_SYNC_FORCE_SYNC;
+ }
+
+ ret = mbox_sync(mbox, mbox_sync_flags);
+ }
+
+ return index_mailbox_sync_init(box, flags, ret < 0);
+}
diff --git a/src/lib-storage/index/pop3c/Makefile.am b/src/lib-storage/index/pop3c/Makefile.am
new file mode 100644
index 0000000..868dc1e
--- /dev/null
+++ b/src/lib-storage/index/pop3c/Makefile.am
@@ -0,0 +1,28 @@
+noinst_LTLIBRARIES = libstorage_pop3c.la
+
+AM_CPPFLAGS = \
+ -I$(top_srcdir)/src/lib \
+ -I$(top_srcdir)/src/lib-settings \
+ -I$(top_srcdir)/src/lib-dns \
+ -I$(top_srcdir)/src/lib-ssl-iostream \
+ -I$(top_srcdir)/src/lib-mail \
+ -I$(top_srcdir)/src/lib-imap \
+ -I$(top_srcdir)/src/lib-index \
+ -I$(top_srcdir)/src/lib-storage \
+ -I$(top_srcdir)/src/lib-storage/index
+
+libstorage_pop3c_la_SOURCES = \
+ pop3c-client.c \
+ pop3c-mail.c \
+ pop3c-settings.c \
+ pop3c-storage.c \
+ pop3c-sync.c
+
+headers = \
+ pop3c-client.h \
+ pop3c-settings.h \
+ pop3c-storage.h \
+ pop3c-sync.h
+
+pkginc_libdir=$(pkgincludedir)
+pkginc_lib_HEADERS = $(headers)
diff --git a/src/lib-storage/index/pop3c/Makefile.in b/src/lib-storage/index/pop3c/Makefile.in
new file mode 100644
index 0000000..e66bdcb
--- /dev/null
+++ b/src/lib-storage/index/pop3c/Makefile.in
@@ -0,0 +1,837 @@
+# Makefile.in generated by automake 1.16.1 from Makefile.am.
+# @configure_input@
+
+# Copyright (C) 1994-2018 Free Software Foundation, Inc.
+
+# This Makefile.in is free software; the Free Software Foundation
+# gives unlimited permission to copy and/or distribute it,
+# with or without modifications, as long as this notice is preserved.
+
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY, to the extent permitted by law; without
+# even the implied warranty of MERCHANTABILITY or FITNESS FOR A
+# PARTICULAR PURPOSE.
+
+@SET_MAKE@
+
+
+VPATH = @srcdir@
+am__is_gnu_make = { \
+ if test -z '$(MAKELEVEL)'; then \
+ false; \
+ elif test -n '$(MAKE_HOST)'; then \
+ true; \
+ elif test -n '$(MAKE_VERSION)' && test -n '$(CURDIR)'; then \
+ true; \
+ else \
+ false; \
+ fi; \
+}
+am__make_running_with_option = \
+ case $${target_option-} in \
+ ?) ;; \
+ *) echo "am__make_running_with_option: internal error: invalid" \
+ "target option '$${target_option-}' specified" >&2; \
+ exit 1;; \
+ esac; \
+ has_opt=no; \
+ sane_makeflags=$$MAKEFLAGS; \
+ if $(am__is_gnu_make); then \
+ sane_makeflags=$$MFLAGS; \
+ else \
+ case $$MAKEFLAGS in \
+ *\\[\ \ ]*) \
+ bs=\\; \
+ sane_makeflags=`printf '%s\n' "$$MAKEFLAGS" \
+ | sed "s/$$bs$$bs[$$bs $$bs ]*//g"`;; \
+ esac; \
+ fi; \
+ skip_next=no; \
+ strip_trailopt () \
+ { \
+ flg=`printf '%s\n' "$$flg" | sed "s/$$1.*$$//"`; \
+ }; \
+ for flg in $$sane_makeflags; do \
+ test $$skip_next = yes && { skip_next=no; continue; }; \
+ case $$flg in \
+ *=*|--*) continue;; \
+ -*I) strip_trailopt 'I'; skip_next=yes;; \
+ -*I?*) strip_trailopt 'I';; \
+ -*O) strip_trailopt 'O'; skip_next=yes;; \
+ -*O?*) strip_trailopt 'O';; \
+ -*l) strip_trailopt 'l'; skip_next=yes;; \
+ -*l?*) strip_trailopt 'l';; \
+ -[dEDm]) skip_next=yes;; \
+ -[JT]) skip_next=yes;; \
+ esac; \
+ case $$flg in \
+ *$$target_option*) has_opt=yes; break;; \
+ esac; \
+ done; \
+ test $$has_opt = yes
+am__make_dryrun = (target_option=n; $(am__make_running_with_option))
+am__make_keepgoing = (target_option=k; $(am__make_running_with_option))
+pkgdatadir = $(datadir)/@PACKAGE@
+pkgincludedir = $(includedir)/@PACKAGE@
+pkglibdir = $(libdir)/@PACKAGE@
+pkglibexecdir = $(libexecdir)/@PACKAGE@
+am__cd = CDPATH="$${ZSH_VERSION+.}$(PATH_SEPARATOR)" && cd
+install_sh_DATA = $(install_sh) -c -m 644
+install_sh_PROGRAM = $(install_sh) -c
+install_sh_SCRIPT = $(install_sh) -c
+INSTALL_HEADER = $(INSTALL_DATA)
+transform = $(program_transform_name)
+NORMAL_INSTALL = :
+PRE_INSTALL = :
+POST_INSTALL = :
+NORMAL_UNINSTALL = :
+PRE_UNINSTALL = :
+POST_UNINSTALL = :
+build_triplet = @build@
+host_triplet = @host@
+subdir = src/lib-storage/index/pop3c
+ACLOCAL_M4 = $(top_srcdir)/aclocal.m4
+am__aclocal_m4_deps = $(top_srcdir)/m4/ac_checktype2.m4 \
+ $(top_srcdir)/m4/ac_typeof.m4 $(top_srcdir)/m4/arc4random.m4 \
+ $(top_srcdir)/m4/blockdev.m4 $(top_srcdir)/m4/c99_vsnprintf.m4 \
+ $(top_srcdir)/m4/clock_gettime.m4 $(top_srcdir)/m4/crypt.m4 \
+ $(top_srcdir)/m4/crypt_xpg6.m4 $(top_srcdir)/m4/dbqlk.m4 \
+ $(top_srcdir)/m4/dirent_dtype.m4 $(top_srcdir)/m4/dovecot.m4 \
+ $(top_srcdir)/m4/fd_passing.m4 $(top_srcdir)/m4/fdatasync.m4 \
+ $(top_srcdir)/m4/flexible_array_member.m4 \
+ $(top_srcdir)/m4/glibc.m4 $(top_srcdir)/m4/gmtime_max.m4 \
+ $(top_srcdir)/m4/gmtime_tm_gmtoff.m4 \
+ $(top_srcdir)/m4/ioloop.m4 $(top_srcdir)/m4/iovec.m4 \
+ $(top_srcdir)/m4/ipv6.m4 $(top_srcdir)/m4/libcap.m4 \
+ $(top_srcdir)/m4/libtool.m4 $(top_srcdir)/m4/libwrap.m4 \
+ $(top_srcdir)/m4/linux_mremap.m4 $(top_srcdir)/m4/ltoptions.m4 \
+ $(top_srcdir)/m4/ltsugar.m4 $(top_srcdir)/m4/ltversion.m4 \
+ $(top_srcdir)/m4/lt~obsolete.m4 $(top_srcdir)/m4/mmap_write.m4 \
+ $(top_srcdir)/m4/mntctl.m4 $(top_srcdir)/m4/modules.m4 \
+ $(top_srcdir)/m4/notify.m4 $(top_srcdir)/m4/nsl.m4 \
+ $(top_srcdir)/m4/off_t_max.m4 $(top_srcdir)/m4/pkg.m4 \
+ $(top_srcdir)/m4/pr_set_dumpable.m4 \
+ $(top_srcdir)/m4/q_quotactl.m4 $(top_srcdir)/m4/quota.m4 \
+ $(top_srcdir)/m4/random.m4 $(top_srcdir)/m4/rlimit.m4 \
+ $(top_srcdir)/m4/sendfile.m4 $(top_srcdir)/m4/size_t_signed.m4 \
+ $(top_srcdir)/m4/sockpeercred.m4 $(top_srcdir)/m4/sql.m4 \
+ $(top_srcdir)/m4/ssl.m4 $(top_srcdir)/m4/st_tim.m4 \
+ $(top_srcdir)/m4/static_array.m4 $(top_srcdir)/m4/test_with.m4 \
+ $(top_srcdir)/m4/time_t.m4 $(top_srcdir)/m4/typeof.m4 \
+ $(top_srcdir)/m4/typeof_dev_t.m4 \
+ $(top_srcdir)/m4/uoff_t_max.m4 $(top_srcdir)/m4/vararg.m4 \
+ $(top_srcdir)/m4/want_apparmor.m4 \
+ $(top_srcdir)/m4/want_bsdauth.m4 \
+ $(top_srcdir)/m4/want_bzlib.m4 \
+ $(top_srcdir)/m4/want_cassandra.m4 \
+ $(top_srcdir)/m4/want_cdb.m4 \
+ $(top_srcdir)/m4/want_checkpassword.m4 \
+ $(top_srcdir)/m4/want_clucene.m4 $(top_srcdir)/m4/want_db.m4 \
+ $(top_srcdir)/m4/want_gssapi.m4 $(top_srcdir)/m4/want_icu.m4 \
+ $(top_srcdir)/m4/want_ldap.m4 $(top_srcdir)/m4/want_lua.m4 \
+ $(top_srcdir)/m4/want_lz4.m4 $(top_srcdir)/m4/want_lzma.m4 \
+ $(top_srcdir)/m4/want_mysql.m4 $(top_srcdir)/m4/want_pam.m4 \
+ $(top_srcdir)/m4/want_passwd.m4 $(top_srcdir)/m4/want_pgsql.m4 \
+ $(top_srcdir)/m4/want_prefetch.m4 \
+ $(top_srcdir)/m4/want_shadow.m4 \
+ $(top_srcdir)/m4/want_sodium.m4 $(top_srcdir)/m4/want_solr.m4 \
+ $(top_srcdir)/m4/want_sqlite.m4 \
+ $(top_srcdir)/m4/want_stemmer.m4 \
+ $(top_srcdir)/m4/want_systemd.m4 \
+ $(top_srcdir)/m4/want_textcat.m4 \
+ $(top_srcdir)/m4/want_unwind.m4 $(top_srcdir)/m4/want_zlib.m4 \
+ $(top_srcdir)/m4/want_zstd.m4 $(top_srcdir)/configure.ac
+am__configure_deps = $(am__aclocal_m4_deps) $(CONFIGURE_DEPENDENCIES) \
+ $(ACLOCAL_M4)
+DIST_COMMON = $(srcdir)/Makefile.am $(pkginc_lib_HEADERS) \
+ $(am__DIST_COMMON)
+mkinstalldirs = $(install_sh) -d
+CONFIG_HEADER = $(top_builddir)/config.h
+CONFIG_CLEAN_FILES =
+CONFIG_CLEAN_VPATH_FILES =
+LTLIBRARIES = $(noinst_LTLIBRARIES)
+libstorage_pop3c_la_LIBADD =
+am_libstorage_pop3c_la_OBJECTS = pop3c-client.lo pop3c-mail.lo \
+ pop3c-settings.lo pop3c-storage.lo pop3c-sync.lo
+libstorage_pop3c_la_OBJECTS = $(am_libstorage_pop3c_la_OBJECTS)
+AM_V_lt = $(am__v_lt_@AM_V@)
+am__v_lt_ = $(am__v_lt_@AM_DEFAULT_V@)
+am__v_lt_0 = --silent
+am__v_lt_1 =
+AM_V_P = $(am__v_P_@AM_V@)
+am__v_P_ = $(am__v_P_@AM_DEFAULT_V@)
+am__v_P_0 = false
+am__v_P_1 = :
+AM_V_GEN = $(am__v_GEN_@AM_V@)
+am__v_GEN_ = $(am__v_GEN_@AM_DEFAULT_V@)
+am__v_GEN_0 = @echo " GEN " $@;
+am__v_GEN_1 =
+AM_V_at = $(am__v_at_@AM_V@)
+am__v_at_ = $(am__v_at_@AM_DEFAULT_V@)
+am__v_at_0 = @
+am__v_at_1 =
+DEFAULT_INCLUDES = -I.@am__isrc@ -I$(top_builddir)
+depcomp = $(SHELL) $(top_srcdir)/depcomp
+am__maybe_remake_depfiles = depfiles
+am__depfiles_remade = ./$(DEPDIR)/pop3c-client.Plo \
+ ./$(DEPDIR)/pop3c-mail.Plo ./$(DEPDIR)/pop3c-settings.Plo \
+ ./$(DEPDIR)/pop3c-storage.Plo ./$(DEPDIR)/pop3c-sync.Plo
+am__mv = mv -f
+COMPILE = $(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) \
+ $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS)
+LTCOMPILE = $(LIBTOOL) $(AM_V_lt) --tag=CC $(AM_LIBTOOLFLAGS) \
+ $(LIBTOOLFLAGS) --mode=compile $(CC) $(DEFS) \
+ $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) $(CPPFLAGS) \
+ $(AM_CFLAGS) $(CFLAGS)
+AM_V_CC = $(am__v_CC_@AM_V@)
+am__v_CC_ = $(am__v_CC_@AM_DEFAULT_V@)
+am__v_CC_0 = @echo " CC " $@;
+am__v_CC_1 =
+CCLD = $(CC)
+LINK = $(LIBTOOL) $(AM_V_lt) --tag=CC $(AM_LIBTOOLFLAGS) \
+ $(LIBTOOLFLAGS) --mode=link $(CCLD) $(AM_CFLAGS) $(CFLAGS) \
+ $(AM_LDFLAGS) $(LDFLAGS) -o $@
+AM_V_CCLD = $(am__v_CCLD_@AM_V@)
+am__v_CCLD_ = $(am__v_CCLD_@AM_DEFAULT_V@)
+am__v_CCLD_0 = @echo " CCLD " $@;
+am__v_CCLD_1 =
+SOURCES = $(libstorage_pop3c_la_SOURCES)
+DIST_SOURCES = $(libstorage_pop3c_la_SOURCES)
+am__can_run_installinfo = \
+ case $$AM_UPDATE_INFO_DIR in \
+ n|no|NO) false;; \
+ *) (install-info --version) >/dev/null 2>&1;; \
+ esac
+am__vpath_adj_setup = srcdirstrip=`echo "$(srcdir)" | sed 's|.|.|g'`;
+am__vpath_adj = case $$p in \
+ $(srcdir)/*) f=`echo "$$p" | sed "s|^$$srcdirstrip/||"`;; \
+ *) f=$$p;; \
+ esac;
+am__strip_dir = f=`echo $$p | sed -e 's|^.*/||'`;
+am__install_max = 40
+am__nobase_strip_setup = \
+ srcdirstrip=`echo "$(srcdir)" | sed 's/[].[^$$\\*|]/\\\\&/g'`
+am__nobase_strip = \
+ for p in $$list; do echo "$$p"; done | sed -e "s|$$srcdirstrip/||"
+am__nobase_list = $(am__nobase_strip_setup); \
+ for p in $$list; do echo "$$p $$p"; done | \
+ sed "s| $$srcdirstrip/| |;"' / .*\//!s/ .*/ ./; s,\( .*\)/[^/]*$$,\1,' | \
+ $(AWK) 'BEGIN { files["."] = "" } { files[$$2] = files[$$2] " " $$1; \
+ if (++n[$$2] == $(am__install_max)) \
+ { print $$2, files[$$2]; n[$$2] = 0; files[$$2] = "" } } \
+ END { for (dir in files) print dir, files[dir] }'
+am__base_list = \
+ sed '$$!N;$$!N;$$!N;$$!N;$$!N;$$!N;$$!N;s/\n/ /g' | \
+ sed '$$!N;$$!N;$$!N;$$!N;s/\n/ /g'
+am__uninstall_files_from_dir = { \
+ test -z "$$files" \
+ || { test ! -d "$$dir" && test ! -f "$$dir" && test ! -r "$$dir"; } \
+ || { echo " ( cd '$$dir' && rm -f" $$files ")"; \
+ $(am__cd) "$$dir" && rm -f $$files; }; \
+ }
+am__installdirs = "$(DESTDIR)$(pkginc_libdir)"
+HEADERS = $(pkginc_lib_HEADERS)
+am__tagged_files = $(HEADERS) $(SOURCES) $(TAGS_FILES) $(LISP)
+# Read a list of newline-separated strings from the standard input,
+# and print each of them once, without duplicates. Input order is
+# *not* preserved.
+am__uniquify_input = $(AWK) '\
+ BEGIN { nonempty = 0; } \
+ { items[$$0] = 1; nonempty = 1; } \
+ END { if (nonempty) { for (i in items) print i; }; } \
+'
+# Make sure the list of sources is unique. This is necessary because,
+# e.g., the same source file might be shared among _SOURCES variables
+# for different programs/libraries.
+am__define_uniq_tagged_files = \
+ list='$(am__tagged_files)'; \
+ unique=`for i in $$list; do \
+ if test -f "$$i"; then echo $$i; else echo $(srcdir)/$$i; fi; \
+ done | $(am__uniquify_input)`
+ETAGS = etags
+CTAGS = ctags
+am__DIST_COMMON = $(srcdir)/Makefile.in $(top_srcdir)/depcomp
+DISTFILES = $(DIST_COMMON) $(DIST_SOURCES) $(TEXINFOS) $(EXTRA_DIST)
+ACLOCAL = @ACLOCAL@
+ACLOCAL_AMFLAGS = @ACLOCAL_AMFLAGS@
+AMTAR = @AMTAR@
+AM_DEFAULT_VERBOSITY = @AM_DEFAULT_VERBOSITY@
+APPARMOR_LIBS = @APPARMOR_LIBS@
+AR = @AR@
+AUTH_CFLAGS = @AUTH_CFLAGS@
+AUTH_LIBS = @AUTH_LIBS@
+AUTOCONF = @AUTOCONF@
+AUTOHEADER = @AUTOHEADER@
+AUTOMAKE = @AUTOMAKE@
+AWK = @AWK@
+BINARY_CFLAGS = @BINARY_CFLAGS@
+BINARY_LDFLAGS = @BINARY_LDFLAGS@
+BISON = @BISON@
+CASSANDRA_CFLAGS = @CASSANDRA_CFLAGS@
+CASSANDRA_LIBS = @CASSANDRA_LIBS@
+CC = @CC@
+CCDEPMODE = @CCDEPMODE@
+CDB_LIBS = @CDB_LIBS@
+CFLAGS = @CFLAGS@
+CLUCENE_CFLAGS = @CLUCENE_CFLAGS@
+CLUCENE_LIBS = @CLUCENE_LIBS@
+COMPRESS_LIBS = @COMPRESS_LIBS@
+CPP = @CPP@
+CPPFLAGS = @CPPFLAGS@
+CRYPT_LIBS = @CRYPT_LIBS@
+CXX = @CXX@
+CXXCPP = @CXXCPP@
+CXXDEPMODE = @CXXDEPMODE@
+CXXFLAGS = @CXXFLAGS@
+CYGPATH_W = @CYGPATH_W@
+DEFS = @DEFS@
+DEPDIR = @DEPDIR@
+DICT_LIBS = @DICT_LIBS@
+DLLIB = @DLLIB@
+DLLTOOL = @DLLTOOL@
+DSYMUTIL = @DSYMUTIL@
+DUMPBIN = @DUMPBIN@
+ECHO_C = @ECHO_C@
+ECHO_N = @ECHO_N@
+ECHO_T = @ECHO_T@
+EGREP = @EGREP@
+EXEEXT = @EXEEXT@
+FGREP = @FGREP@
+FLEX = @FLEX@
+FUZZER_CPPFLAGS = @FUZZER_CPPFLAGS@
+FUZZER_LDFLAGS = @FUZZER_LDFLAGS@
+GREP = @GREP@
+INSTALL = @INSTALL@
+INSTALL_DATA = @INSTALL_DATA@
+INSTALL_PROGRAM = @INSTALL_PROGRAM@
+INSTALL_SCRIPT = @INSTALL_SCRIPT@
+INSTALL_STRIP_PROGRAM = @INSTALL_STRIP_PROGRAM@
+KRB5CONFIG = @KRB5CONFIG@
+KRB5_CFLAGS = @KRB5_CFLAGS@
+KRB5_LIBS = @KRB5_LIBS@
+LD = @LD@
+LDAP_LIBS = @LDAP_LIBS@
+LDFLAGS = @LDFLAGS@
+LD_NO_WHOLE_ARCHIVE = @LD_NO_WHOLE_ARCHIVE@
+LD_WHOLE_ARCHIVE = @LD_WHOLE_ARCHIVE@
+LIBCAP = @LIBCAP@
+LIBDOVECOT = @LIBDOVECOT@
+LIBDOVECOT_COMPRESS = @LIBDOVECOT_COMPRESS@
+LIBDOVECOT_DEPS = @LIBDOVECOT_DEPS@
+LIBDOVECOT_DSYNC = @LIBDOVECOT_DSYNC@
+LIBDOVECOT_LA_LIBS = @LIBDOVECOT_LA_LIBS@
+LIBDOVECOT_LDA = @LIBDOVECOT_LDA@
+LIBDOVECOT_LDAP = @LIBDOVECOT_LDAP@
+LIBDOVECOT_LIBFTS = @LIBDOVECOT_LIBFTS@
+LIBDOVECOT_LIBFTS_DEPS = @LIBDOVECOT_LIBFTS_DEPS@
+LIBDOVECOT_LOGIN = @LIBDOVECOT_LOGIN@
+LIBDOVECOT_LUA = @LIBDOVECOT_LUA@
+LIBDOVECOT_LUA_DEPS = @LIBDOVECOT_LUA_DEPS@
+LIBDOVECOT_SQL = @LIBDOVECOT_SQL@
+LIBDOVECOT_STORAGE = @LIBDOVECOT_STORAGE@
+LIBDOVECOT_STORAGE_DEPS = @LIBDOVECOT_STORAGE_DEPS@
+LIBEXTTEXTCAT_CFLAGS = @LIBEXTTEXTCAT_CFLAGS@
+LIBEXTTEXTCAT_LIBS = @LIBEXTTEXTCAT_LIBS@
+LIBICONV = @LIBICONV@
+LIBICU_CFLAGS = @LIBICU_CFLAGS@
+LIBICU_LIBS = @LIBICU_LIBS@
+LIBOBJS = @LIBOBJS@
+LIBS = @LIBS@
+LIBSODIUM_CFLAGS = @LIBSODIUM_CFLAGS@
+LIBSODIUM_LIBS = @LIBSODIUM_LIBS@
+LIBTIRPC_CFLAGS = @LIBTIRPC_CFLAGS@
+LIBTIRPC_LIBS = @LIBTIRPC_LIBS@
+LIBTOOL = @LIBTOOL@
+LIBUNWIND_CFLAGS = @LIBUNWIND_CFLAGS@
+LIBUNWIND_LIBS = @LIBUNWIND_LIBS@
+LIBWRAP_LIBS = @LIBWRAP_LIBS@
+LINKED_STORAGE_LDADD = @LINKED_STORAGE_LDADD@
+LIPO = @LIPO@
+LN_S = @LN_S@
+LTLIBICONV = @LTLIBICONV@
+LTLIBOBJS = @LTLIBOBJS@
+LT_SYS_LIBRARY_PATH = @LT_SYS_LIBRARY_PATH@
+LUA_CFLAGS = @LUA_CFLAGS@
+LUA_LIBS = @LUA_LIBS@
+MAINT = @MAINT@
+MAKEINFO = @MAKEINFO@
+MANIFEST_TOOL = @MANIFEST_TOOL@
+MKDIR_P = @MKDIR_P@
+MODULE_LIBS = @MODULE_LIBS@
+MODULE_SUFFIX = @MODULE_SUFFIX@
+MYSQL_CFLAGS = @MYSQL_CFLAGS@
+MYSQL_CONFIG = @MYSQL_CONFIG@
+MYSQL_LIBS = @MYSQL_LIBS@
+NM = @NM@
+NMEDIT = @NMEDIT@
+NOPLUGIN_LDFLAGS = @NOPLUGIN_LDFLAGS@
+OBJDUMP = @OBJDUMP@
+OBJEXT = @OBJEXT@
+OTOOL = @OTOOL@
+OTOOL64 = @OTOOL64@
+PACKAGE = @PACKAGE@
+PACKAGE_BUGREPORT = @PACKAGE_BUGREPORT@
+PACKAGE_NAME = @PACKAGE_NAME@
+PACKAGE_STRING = @PACKAGE_STRING@
+PACKAGE_TARNAME = @PACKAGE_TARNAME@
+PACKAGE_URL = @PACKAGE_URL@
+PACKAGE_VERSION = @PACKAGE_VERSION@
+PANDOC = @PANDOC@
+PATH_SEPARATOR = @PATH_SEPARATOR@
+PGSQL_CFLAGS = @PGSQL_CFLAGS@
+PGSQL_LIBS = @PGSQL_LIBS@
+PG_CONFIG = @PG_CONFIG@
+PIE_CFLAGS = @PIE_CFLAGS@
+PIE_LDFLAGS = @PIE_LDFLAGS@
+PKG_CONFIG = @PKG_CONFIG@
+PKG_CONFIG_LIBDIR = @PKG_CONFIG_LIBDIR@
+PKG_CONFIG_PATH = @PKG_CONFIG_PATH@
+QUOTA_LIBS = @QUOTA_LIBS@
+RANLIB = @RANLIB@
+RELRO_LDFLAGS = @RELRO_LDFLAGS@
+RPCGEN = @RPCGEN@
+RUN_TEST = @RUN_TEST@
+SED = @SED@
+SETTING_FILES = @SETTING_FILES@
+SET_MAKE = @SET_MAKE@
+SHELL = @SHELL@
+SQLITE_CFLAGS = @SQLITE_CFLAGS@
+SQLITE_LIBS = @SQLITE_LIBS@
+SQL_CFLAGS = @SQL_CFLAGS@
+SQL_LIBS = @SQL_LIBS@
+SSL_CFLAGS = @SSL_CFLAGS@
+SSL_LIBS = @SSL_LIBS@
+STRIP = @STRIP@
+SYSTEMD_CFLAGS = @SYSTEMD_CFLAGS@
+SYSTEMD_LIBS = @SYSTEMD_LIBS@
+VALGRIND = @VALGRIND@
+VERSION = @VERSION@
+ZSTD_CFLAGS = @ZSTD_CFLAGS@
+ZSTD_LIBS = @ZSTD_LIBS@
+abs_builddir = @abs_builddir@
+abs_srcdir = @abs_srcdir@
+abs_top_builddir = @abs_top_builddir@
+abs_top_srcdir = @abs_top_srcdir@
+ac_ct_AR = @ac_ct_AR@
+ac_ct_CC = @ac_ct_CC@
+ac_ct_CXX = @ac_ct_CXX@
+ac_ct_DUMPBIN = @ac_ct_DUMPBIN@
+am__include = @am__include@
+am__leading_dot = @am__leading_dot@
+am__quote = @am__quote@
+am__tar = @am__tar@
+am__untar = @am__untar@
+bindir = @bindir@
+build = @build@
+build_alias = @build_alias@
+build_cpu = @build_cpu@
+build_os = @build_os@
+build_vendor = @build_vendor@
+builddir = @builddir@
+datadir = @datadir@
+datarootdir = @datarootdir@
+dict_drivers = @dict_drivers@
+docdir = @docdir@
+dvidir = @dvidir@
+exec_prefix = @exec_prefix@
+host = @host@
+host_alias = @host_alias@
+host_cpu = @host_cpu@
+host_os = @host_os@
+host_vendor = @host_vendor@
+htmldir = @htmldir@
+includedir = @includedir@
+infodir = @infodir@
+install_sh = @install_sh@
+libdir = @libdir@
+libexecdir = @libexecdir@
+localedir = @localedir@
+localstatedir = @localstatedir@
+mandir = @mandir@
+mkdir_p = @mkdir_p@
+moduledir = @moduledir@
+oldincludedir = @oldincludedir@
+pdfdir = @pdfdir@
+prefix = @prefix@
+program_transform_name = @program_transform_name@
+psdir = @psdir@
+rundir = @rundir@
+runstatedir = @runstatedir@
+sbindir = @sbindir@
+sharedstatedir = @sharedstatedir@
+sql_drivers = @sql_drivers@
+srcdir = @srcdir@
+ssldir = @ssldir@
+statedir = @statedir@
+sysconfdir = @sysconfdir@
+systemdservicetype = @systemdservicetype@
+systemdsystemunitdir = @systemdsystemunitdir@
+target_alias = @target_alias@
+top_build_prefix = @top_build_prefix@
+top_builddir = @top_builddir@
+top_srcdir = @top_srcdir@
+noinst_LTLIBRARIES = libstorage_pop3c.la
+AM_CPPFLAGS = \
+ -I$(top_srcdir)/src/lib \
+ -I$(top_srcdir)/src/lib-settings \
+ -I$(top_srcdir)/src/lib-dns \
+ -I$(top_srcdir)/src/lib-ssl-iostream \
+ -I$(top_srcdir)/src/lib-mail \
+ -I$(top_srcdir)/src/lib-imap \
+ -I$(top_srcdir)/src/lib-index \
+ -I$(top_srcdir)/src/lib-storage \
+ -I$(top_srcdir)/src/lib-storage/index
+
+libstorage_pop3c_la_SOURCES = \
+ pop3c-client.c \
+ pop3c-mail.c \
+ pop3c-settings.c \
+ pop3c-storage.c \
+ pop3c-sync.c
+
+headers = \
+ pop3c-client.h \
+ pop3c-settings.h \
+ pop3c-storage.h \
+ pop3c-sync.h
+
+pkginc_libdir = $(pkgincludedir)
+pkginc_lib_HEADERS = $(headers)
+all: all-am
+
+.SUFFIXES:
+.SUFFIXES: .c .lo .o .obj
+$(srcdir)/Makefile.in: @MAINTAINER_MODE_TRUE@ $(srcdir)/Makefile.am $(am__configure_deps)
+ @for dep in $?; do \
+ case '$(am__configure_deps)' in \
+ *$$dep*) \
+ ( cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh ) \
+ && { if test -f $@; then exit 0; else break; fi; }; \
+ exit 1;; \
+ esac; \
+ done; \
+ echo ' cd $(top_srcdir) && $(AUTOMAKE) --foreign src/lib-storage/index/pop3c/Makefile'; \
+ $(am__cd) $(top_srcdir) && \
+ $(AUTOMAKE) --foreign src/lib-storage/index/pop3c/Makefile
+Makefile: $(srcdir)/Makefile.in $(top_builddir)/config.status
+ @case '$?' in \
+ *config.status*) \
+ cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh;; \
+ *) \
+ echo ' cd $(top_builddir) && $(SHELL) ./config.status $(subdir)/$@ $(am__maybe_remake_depfiles)'; \
+ cd $(top_builddir) && $(SHELL) ./config.status $(subdir)/$@ $(am__maybe_remake_depfiles);; \
+ esac;
+
+$(top_builddir)/config.status: $(top_srcdir)/configure $(CONFIG_STATUS_DEPENDENCIES)
+ cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh
+
+$(top_srcdir)/configure: @MAINTAINER_MODE_TRUE@ $(am__configure_deps)
+ cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh
+$(ACLOCAL_M4): @MAINTAINER_MODE_TRUE@ $(am__aclocal_m4_deps)
+ cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh
+$(am__aclocal_m4_deps):
+
+clean-noinstLTLIBRARIES:
+ -test -z "$(noinst_LTLIBRARIES)" || rm -f $(noinst_LTLIBRARIES)
+ @list='$(noinst_LTLIBRARIES)'; \
+ locs=`for p in $$list; do echo $$p; done | \
+ sed 's|^[^/]*$$|.|; s|/[^/]*$$||; s|$$|/so_locations|' | \
+ sort -u`; \
+ test -z "$$locs" || { \
+ echo rm -f $${locs}; \
+ rm -f $${locs}; \
+ }
+
+libstorage_pop3c.la: $(libstorage_pop3c_la_OBJECTS) $(libstorage_pop3c_la_DEPENDENCIES) $(EXTRA_libstorage_pop3c_la_DEPENDENCIES)
+ $(AM_V_CCLD)$(LINK) $(libstorage_pop3c_la_OBJECTS) $(libstorage_pop3c_la_LIBADD) $(LIBS)
+
+mostlyclean-compile:
+ -rm -f *.$(OBJEXT)
+
+distclean-compile:
+ -rm -f *.tab.c
+
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/pop3c-client.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/pop3c-mail.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/pop3c-settings.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/pop3c-storage.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/pop3c-sync.Plo@am__quote@ # am--include-marker
+
+$(am__depfiles_remade):
+ @$(MKDIR_P) $(@D)
+ @echo '# dummy' >$@-t && $(am__mv) $@-t $@
+
+am--depfiles: $(am__depfiles_remade)
+
+.c.o:
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(COMPILE) -MT $@ -MD -MP -MF $(DEPDIR)/$*.Tpo -c -o $@ $<
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/$*.Tpo $(DEPDIR)/$*.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='$<' object='$@' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(COMPILE) -c -o $@ $<
+
+.c.obj:
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(COMPILE) -MT $@ -MD -MP -MF $(DEPDIR)/$*.Tpo -c -o $@ `$(CYGPATH_W) '$<'`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/$*.Tpo $(DEPDIR)/$*.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='$<' object='$@' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(COMPILE) -c -o $@ `$(CYGPATH_W) '$<'`
+
+.c.lo:
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(LTCOMPILE) -MT $@ -MD -MP -MF $(DEPDIR)/$*.Tpo -c -o $@ $<
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/$*.Tpo $(DEPDIR)/$*.Plo
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='$<' object='$@' libtool=yes @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(LTCOMPILE) -c -o $@ $<
+
+mostlyclean-libtool:
+ -rm -f *.lo
+
+clean-libtool:
+ -rm -rf .libs _libs
+install-pkginc_libHEADERS: $(pkginc_lib_HEADERS)
+ @$(NORMAL_INSTALL)
+ @list='$(pkginc_lib_HEADERS)'; test -n "$(pkginc_libdir)" || list=; \
+ if test -n "$$list"; then \
+ echo " $(MKDIR_P) '$(DESTDIR)$(pkginc_libdir)'"; \
+ $(MKDIR_P) "$(DESTDIR)$(pkginc_libdir)" || exit 1; \
+ fi; \
+ for p in $$list; do \
+ if test -f "$$p"; then d=; else d="$(srcdir)/"; fi; \
+ echo "$$d$$p"; \
+ done | $(am__base_list) | \
+ while read files; do \
+ echo " $(INSTALL_HEADER) $$files '$(DESTDIR)$(pkginc_libdir)'"; \
+ $(INSTALL_HEADER) $$files "$(DESTDIR)$(pkginc_libdir)" || exit $$?; \
+ done
+
+uninstall-pkginc_libHEADERS:
+ @$(NORMAL_UNINSTALL)
+ @list='$(pkginc_lib_HEADERS)'; test -n "$(pkginc_libdir)" || list=; \
+ files=`for p in $$list; do echo $$p; done | sed -e 's|^.*/||'`; \
+ dir='$(DESTDIR)$(pkginc_libdir)'; $(am__uninstall_files_from_dir)
+
+ID: $(am__tagged_files)
+ $(am__define_uniq_tagged_files); mkid -fID $$unique
+tags: tags-am
+TAGS: tags
+
+tags-am: $(TAGS_DEPENDENCIES) $(am__tagged_files)
+ set x; \
+ here=`pwd`; \
+ $(am__define_uniq_tagged_files); \
+ shift; \
+ if test -z "$(ETAGS_ARGS)$$*$$unique"; then :; else \
+ test -n "$$unique" || unique=$$empty_fix; \
+ if test $$# -gt 0; then \
+ $(ETAGS) $(ETAGSFLAGS) $(AM_ETAGSFLAGS) $(ETAGS_ARGS) \
+ "$$@" $$unique; \
+ else \
+ $(ETAGS) $(ETAGSFLAGS) $(AM_ETAGSFLAGS) $(ETAGS_ARGS) \
+ $$unique; \
+ fi; \
+ fi
+ctags: ctags-am
+
+CTAGS: ctags
+ctags-am: $(TAGS_DEPENDENCIES) $(am__tagged_files)
+ $(am__define_uniq_tagged_files); \
+ test -z "$(CTAGS_ARGS)$$unique" \
+ || $(CTAGS) $(CTAGSFLAGS) $(AM_CTAGSFLAGS) $(CTAGS_ARGS) \
+ $$unique
+
+GTAGS:
+ here=`$(am__cd) $(top_builddir) && pwd` \
+ && $(am__cd) $(top_srcdir) \
+ && gtags -i $(GTAGS_ARGS) "$$here"
+cscopelist: cscopelist-am
+
+cscopelist-am: $(am__tagged_files)
+ list='$(am__tagged_files)'; \
+ case "$(srcdir)" in \
+ [\\/]* | ?:[\\/]*) sdir="$(srcdir)" ;; \
+ *) sdir=$(subdir)/$(srcdir) ;; \
+ esac; \
+ for i in $$list; do \
+ if test -f "$$i"; then \
+ echo "$(subdir)/$$i"; \
+ else \
+ echo "$$sdir/$$i"; \
+ fi; \
+ done >> $(top_builddir)/cscope.files
+
+distclean-tags:
+ -rm -f TAGS ID GTAGS GRTAGS GSYMS GPATH tags
+
+distdir: $(BUILT_SOURCES)
+ $(MAKE) $(AM_MAKEFLAGS) distdir-am
+
+distdir-am: $(DISTFILES)
+ @srcdirstrip=`echo "$(srcdir)" | sed 's/[].[^$$\\*]/\\\\&/g'`; \
+ topsrcdirstrip=`echo "$(top_srcdir)" | sed 's/[].[^$$\\*]/\\\\&/g'`; \
+ list='$(DISTFILES)'; \
+ dist_files=`for file in $$list; do echo $$file; done | \
+ sed -e "s|^$$srcdirstrip/||;t" \
+ -e "s|^$$topsrcdirstrip/|$(top_builddir)/|;t"`; \
+ case $$dist_files in \
+ */*) $(MKDIR_P) `echo "$$dist_files" | \
+ sed '/\//!d;s|^|$(distdir)/|;s,/[^/]*$$,,' | \
+ sort -u` ;; \
+ esac; \
+ for file in $$dist_files; do \
+ if test -f $$file || test -d $$file; then d=.; else d=$(srcdir); fi; \
+ if test -d $$d/$$file; then \
+ dir=`echo "/$$file" | sed -e 's,/[^/]*$$,,'`; \
+ if test -d "$(distdir)/$$file"; then \
+ find "$(distdir)/$$file" -type d ! -perm -700 -exec chmod u+rwx {} \;; \
+ fi; \
+ if test -d $(srcdir)/$$file && test $$d != $(srcdir); then \
+ cp -fpR $(srcdir)/$$file "$(distdir)$$dir" || exit 1; \
+ find "$(distdir)/$$file" -type d ! -perm -700 -exec chmod u+rwx {} \;; \
+ fi; \
+ cp -fpR $$d/$$file "$(distdir)$$dir" || exit 1; \
+ else \
+ test -f "$(distdir)/$$file" \
+ || cp -p $$d/$$file "$(distdir)/$$file" \
+ || exit 1; \
+ fi; \
+ done
+check-am: all-am
+check: check-am
+all-am: Makefile $(LTLIBRARIES) $(HEADERS)
+installdirs:
+ for dir in "$(DESTDIR)$(pkginc_libdir)"; do \
+ test -z "$$dir" || $(MKDIR_P) "$$dir"; \
+ done
+install: install-am
+install-exec: install-exec-am
+install-data: install-data-am
+uninstall: uninstall-am
+
+install-am: all-am
+ @$(MAKE) $(AM_MAKEFLAGS) install-exec-am install-data-am
+
+installcheck: installcheck-am
+install-strip:
+ if test -z '$(STRIP)'; then \
+ $(MAKE) $(AM_MAKEFLAGS) INSTALL_PROGRAM="$(INSTALL_STRIP_PROGRAM)" \
+ install_sh_PROGRAM="$(INSTALL_STRIP_PROGRAM)" INSTALL_STRIP_FLAG=-s \
+ install; \
+ else \
+ $(MAKE) $(AM_MAKEFLAGS) INSTALL_PROGRAM="$(INSTALL_STRIP_PROGRAM)" \
+ install_sh_PROGRAM="$(INSTALL_STRIP_PROGRAM)" INSTALL_STRIP_FLAG=-s \
+ "INSTALL_PROGRAM_ENV=STRIPPROG='$(STRIP)'" install; \
+ fi
+mostlyclean-generic:
+
+clean-generic:
+
+distclean-generic:
+ -test -z "$(CONFIG_CLEAN_FILES)" || rm -f $(CONFIG_CLEAN_FILES)
+ -test . = "$(srcdir)" || test -z "$(CONFIG_CLEAN_VPATH_FILES)" || rm -f $(CONFIG_CLEAN_VPATH_FILES)
+
+maintainer-clean-generic:
+ @echo "This command is intended for maintainers to use"
+ @echo "it deletes files that may require special tools to rebuild."
+clean: clean-am
+
+clean-am: clean-generic clean-libtool clean-noinstLTLIBRARIES \
+ mostlyclean-am
+
+distclean: distclean-am
+ -rm -f ./$(DEPDIR)/pop3c-client.Plo
+ -rm -f ./$(DEPDIR)/pop3c-mail.Plo
+ -rm -f ./$(DEPDIR)/pop3c-settings.Plo
+ -rm -f ./$(DEPDIR)/pop3c-storage.Plo
+ -rm -f ./$(DEPDIR)/pop3c-sync.Plo
+ -rm -f Makefile
+distclean-am: clean-am distclean-compile distclean-generic \
+ distclean-tags
+
+dvi: dvi-am
+
+dvi-am:
+
+html: html-am
+
+html-am:
+
+info: info-am
+
+info-am:
+
+install-data-am: install-pkginc_libHEADERS
+
+install-dvi: install-dvi-am
+
+install-dvi-am:
+
+install-exec-am:
+
+install-html: install-html-am
+
+install-html-am:
+
+install-info: install-info-am
+
+install-info-am:
+
+install-man:
+
+install-pdf: install-pdf-am
+
+install-pdf-am:
+
+install-ps: install-ps-am
+
+install-ps-am:
+
+installcheck-am:
+
+maintainer-clean: maintainer-clean-am
+ -rm -f ./$(DEPDIR)/pop3c-client.Plo
+ -rm -f ./$(DEPDIR)/pop3c-mail.Plo
+ -rm -f ./$(DEPDIR)/pop3c-settings.Plo
+ -rm -f ./$(DEPDIR)/pop3c-storage.Plo
+ -rm -f ./$(DEPDIR)/pop3c-sync.Plo
+ -rm -f Makefile
+maintainer-clean-am: distclean-am maintainer-clean-generic
+
+mostlyclean: mostlyclean-am
+
+mostlyclean-am: mostlyclean-compile mostlyclean-generic \
+ mostlyclean-libtool
+
+pdf: pdf-am
+
+pdf-am:
+
+ps: ps-am
+
+ps-am:
+
+uninstall-am: uninstall-pkginc_libHEADERS
+
+.MAKE: install-am install-strip
+
+.PHONY: CTAGS GTAGS TAGS all all-am am--depfiles check check-am clean \
+ clean-generic clean-libtool clean-noinstLTLIBRARIES \
+ cscopelist-am ctags ctags-am distclean distclean-compile \
+ distclean-generic distclean-libtool distclean-tags distdir dvi \
+ dvi-am html html-am info info-am install install-am \
+ install-data install-data-am install-dvi install-dvi-am \
+ install-exec install-exec-am install-html install-html-am \
+ install-info install-info-am install-man install-pdf \
+ install-pdf-am install-pkginc_libHEADERS install-ps \
+ install-ps-am install-strip installcheck installcheck-am \
+ installdirs maintainer-clean maintainer-clean-generic \
+ mostlyclean mostlyclean-compile mostlyclean-generic \
+ mostlyclean-libtool pdf pdf-am ps ps-am tags tags-am uninstall \
+ uninstall-am uninstall-pkginc_libHEADERS
+
+.PRECIOUS: Makefile
+
+
+# Tell versions [3.59,3.63) of GNU make to not export all variables.
+# Otherwise a system limit (for SysV at least) may be exceeded.
+.NOEXPORT:
diff --git a/src/lib-storage/index/pop3c/pop3c-client.c b/src/lib-storage/index/pop3c/pop3c-client.c
new file mode 100644
index 0000000..44544a3
--- /dev/null
+++ b/src/lib-storage/index/pop3c/pop3c-client.c
@@ -0,0 +1,902 @@
+/* Copyright (c) 2011-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "ioloop.h"
+#include "net.h"
+#include "istream.h"
+#include "istream-chain.h"
+#include "istream-dot.h"
+#include "istream-seekable.h"
+#include "ostream.h"
+#include "iostream-rawlog.h"
+#include "iostream-ssl.h"
+#include "safe-mkstemp.h"
+#include "base64.h"
+#include "str.h"
+#include "dns-lookup.h"
+#include "pop3c-client.h"
+
+#include <unistd.h>
+
+#define POP3C_MAX_INBUF_SIZE (1024*32)
+#define POP3C_DNS_LOOKUP_TIMEOUT_MSECS (1000*30)
+#define POP3C_CONNECT_TIMEOUT_MSECS (1000*30)
+#define POP3C_COMMAND_TIMEOUT_MSECS (1000*60*5)
+
+enum pop3c_client_state {
+ /* No connection */
+ POP3C_CLIENT_STATE_DISCONNECTED = 0,
+ /* Trying to connect */
+ POP3C_CLIENT_STATE_CONNECTING,
+ POP3C_CLIENT_STATE_STARTTLS,
+ /* Connected, trying to authenticate */
+ POP3C_CLIENT_STATE_USER,
+ POP3C_CLIENT_STATE_AUTH,
+ POP3C_CLIENT_STATE_PASS,
+ /* Post-authentication, asking for capabilities */
+ POP3C_CLIENT_STATE_CAPA,
+ /* Authenticated, ready to accept commands */
+ POP3C_CLIENT_STATE_DONE
+};
+
+struct pop3c_client_sync_cmd_ctx {
+ enum pop3c_command_state state;
+ char *reply;
+};
+
+struct pop3c_client_cmd {
+ struct istream *input;
+ struct istream_chain *chain;
+ bool reading_dot;
+
+ pop3c_cmd_callback_t *callback;
+ void *context;
+};
+
+struct pop3c_client {
+ pool_t pool;
+ struct event *event;
+ struct pop3c_client_settings set;
+ struct ssl_iostream_context *ssl_ctx;
+ struct ip_addr ip;
+
+ int fd;
+ struct io *io;
+ struct istream *input, *raw_input;
+ struct ostream *output, *raw_output;
+ struct ssl_iostream *ssl_iostream;
+ struct timeout *to;
+ struct dns_lookup *dns_lookup;
+
+ enum pop3c_client_state state;
+ enum pop3c_capability capabilities;
+ const char *auth_mech;
+
+ pop3c_login_callback_t *login_callback;
+ void *login_context;
+
+ ARRAY(struct pop3c_client_cmd) commands;
+ const char *input_line;
+ struct istream *dot_input;
+
+ bool running:1;
+};
+
+static void
+pop3c_dns_callback(const struct dns_lookup_result *result,
+ struct pop3c_client *client);
+static void pop3c_client_connect_ip(struct pop3c_client *client);
+static int pop3c_client_ssl_init(struct pop3c_client *client);
+static void pop3c_client_input(struct pop3c_client *client);
+
+struct pop3c_client *
+pop3c_client_init(const struct pop3c_client_settings *set,
+ struct event *event_parent)
+{
+ struct pop3c_client *client;
+ const char *error;
+ pool_t pool;
+
+ pool = pool_alloconly_create("pop3c client", 1024);
+ client = p_new(pool, struct pop3c_client, 1);
+ client->pool = pool;
+ client->event = event_create(event_parent);
+ client->fd = -1;
+ p_array_init(&client->commands, pool, 16);
+
+ client->set.debug = set->debug;
+ client->set.host = p_strdup(pool, set->host);
+ client->set.port = set->port;
+ client->set.master_user = p_strdup_empty(pool, set->master_user);
+ client->set.username = p_strdup(pool, set->username);
+ client->set.password = p_strdup(pool, set->password);
+ client->set.dns_client_socket_path =
+ p_strdup(pool, set->dns_client_socket_path);
+ client->set.temp_path_prefix = p_strdup(pool, set->temp_path_prefix);
+ client->set.rawlog_dir = p_strdup(pool, set->rawlog_dir);
+ client->set.ssl_mode = set->ssl_mode;
+
+ if (set->ssl_mode != POP3C_CLIENT_SSL_MODE_NONE) {
+ ssl_iostream_settings_init_from(client->pool, &client->set.ssl_set, &set->ssl_set);
+ client->set.ssl_set.verbose_invalid_cert = !client->set.ssl_set.allow_invalid_cert;
+ if (ssl_iostream_client_context_cache_get(&set->ssl_set,
+ &client->ssl_ctx,
+ &error) < 0) {
+ i_error("pop3c(%s:%u): Couldn't initialize SSL context: %s",
+ set->host, set->port, error);
+ }
+ }
+ return client;
+}
+
+static void
+client_login_callback(struct pop3c_client *client,
+ enum pop3c_command_state state, const char *reason)
+{
+ pop3c_login_callback_t *callback = client->login_callback;
+ void *context = client->login_context;
+
+ if (client->login_callback != NULL) {
+ client->login_callback = NULL;
+ client->login_context = NULL;
+ callback(state, reason, context);
+ }
+}
+
+static void
+pop3c_client_async_callback(struct pop3c_client *client,
+ enum pop3c_command_state state, const char *reply)
+{
+ struct pop3c_client_cmd *cmd, cmd_copy;
+ bool running = client->running;
+
+ i_assert(reply != NULL);
+ i_assert(array_count(&client->commands) > 0);
+
+ cmd = array_front_modifiable(&client->commands);
+ if (cmd->input != NULL && state == POP3C_COMMAND_STATE_OK &&
+ !cmd->reading_dot) {
+ /* read the full input into seekable-istream before calling
+ the callback */
+ i_assert(client->dot_input == NULL);
+ i_stream_chain_append(cmd->chain, client->input);
+ client->dot_input = cmd->input;
+ cmd->reading_dot = TRUE;
+ return;
+ }
+ cmd_copy = *cmd;
+ array_pop_front(&client->commands);
+
+ if (cmd_copy.input != NULL) {
+ i_stream_seek(cmd_copy.input, 0);
+ i_stream_unref(&cmd_copy.input);
+ }
+ if (cmd_copy.callback != NULL)
+ cmd_copy.callback(state, reply, cmd_copy.context);
+ if (running)
+ io_loop_stop(current_ioloop);
+}
+
+static void
+pop3c_client_async_callback_disconnected(struct pop3c_client *client)
+{
+ pop3c_client_async_callback(client, POP3C_COMMAND_STATE_DISCONNECTED,
+ "Disconnected");
+}
+
+static void pop3c_client_disconnect(struct pop3c_client *client)
+{
+ client->state = POP3C_CLIENT_STATE_DISCONNECTED;
+
+ if (client->running)
+ io_loop_stop(current_ioloop);
+
+ if (client->dns_lookup != NULL)
+ dns_lookup_abort(&client->dns_lookup);
+ timeout_remove(&client->to);
+ io_remove(&client->io);
+ i_stream_destroy(&client->input);
+ o_stream_destroy(&client->output);
+ ssl_iostream_destroy(&client->ssl_iostream);
+ i_close_fd(&client->fd);
+ while (array_count(&client->commands) > 0)
+ pop3c_client_async_callback_disconnected(client);
+ client_login_callback(client, POP3C_COMMAND_STATE_DISCONNECTED,
+ "Disconnected");
+}
+
+void pop3c_client_deinit(struct pop3c_client **_client)
+{
+ struct pop3c_client *client = *_client;
+
+ pop3c_client_disconnect(client);
+ if (client->ssl_ctx != NULL)
+ ssl_iostream_context_unref(&client->ssl_ctx);
+ event_unref(&client->event);
+ pool_unref(&client->pool);
+}
+
+static void pop3c_client_ioloop_changed(struct pop3c_client *client)
+{
+ if (client->to != NULL)
+ client->to = io_loop_move_timeout(&client->to);
+ if (client->io != NULL)
+ client->io = io_loop_move_io(&client->io);
+ if (client->output != NULL)
+ o_stream_switch_ioloop(client->output);
+}
+
+static void pop3c_client_timeout(struct pop3c_client *client)
+{
+ switch (client->state) {
+ case POP3C_CLIENT_STATE_CONNECTING:
+ i_error("pop3c(%s): connect(%s, %u) timed out after %u seconds",
+ client->set.host, net_ip2addr(&client->ip),
+ client->set.port, POP3C_CONNECT_TIMEOUT_MSECS/1000);
+ break;
+ case POP3C_CLIENT_STATE_DONE:
+ i_error("pop3c(%s): Command timed out after %u seconds",
+ client->set.host, POP3C_COMMAND_TIMEOUT_MSECS/1000);
+ break;
+ default:
+ i_error("pop3c(%s): Authentication timed out after %u seconds",
+ client->set.host, POP3C_CONNECT_TIMEOUT_MSECS/1000);
+ break;
+ }
+ pop3c_client_disconnect(client);
+}
+
+static int pop3c_client_dns_lookup(struct pop3c_client *client)
+{
+ struct dns_lookup_settings dns_set;
+
+ i_assert(client->state == POP3C_CLIENT_STATE_CONNECTING);
+
+ if (client->set.dns_client_socket_path[0] == '\0') {
+ struct ip_addr *ips;
+ unsigned int ips_count;
+ int ret;
+
+ ret = net_gethostbyname(client->set.host, &ips, &ips_count);
+ if (ret != 0) {
+ i_error("pop3c(%s): net_gethostbyname() failed: %s",
+ client->set.host, net_gethosterror(ret));
+ return -1;
+ }
+ i_assert(ips_count > 0);
+ client->ip = ips[0];
+ pop3c_client_connect_ip(client);
+ } else {
+ i_zero(&dns_set);
+ dns_set.dns_client_socket_path =
+ client->set.dns_client_socket_path;
+ dns_set.timeout_msecs = POP3C_DNS_LOOKUP_TIMEOUT_MSECS;
+ dns_set.event_parent = client->event;
+ if (dns_lookup(client->set.host, &dns_set,
+ pop3c_dns_callback, client,
+ &client->dns_lookup) < 0)
+ return -1;
+ }
+ return 0;
+}
+
+void pop3c_client_wait_one(struct pop3c_client *client)
+{
+ struct ioloop *ioloop, *prev_ioloop = current_ioloop;
+ bool timeout_added = FALSE, failed = FALSE;
+
+ if (client->state == POP3C_CLIENT_STATE_DISCONNECTED &&
+ array_count(&client->commands) > 0) {
+ while (array_count(&client->commands) > 0)
+ pop3c_client_async_callback_disconnected(client);
+ return;
+ }
+
+ i_assert(client->fd != -1 ||
+ client->state == POP3C_CLIENT_STATE_CONNECTING);
+ i_assert(array_count(&client->commands) > 0 ||
+ client->state == POP3C_CLIENT_STATE_CONNECTING);
+
+ ioloop = io_loop_create();
+ pop3c_client_ioloop_changed(client);
+
+ if (client->ip.family == 0) {
+ /* we're connecting, start DNS lookup after our ioloop
+ is created */
+ if (pop3c_client_dns_lookup(client) < 0)
+ failed = TRUE;
+ } else if (client->to == NULL) {
+ client->to = timeout_add(POP3C_COMMAND_TIMEOUT_MSECS,
+ pop3c_client_timeout, client);
+ timeout_added = TRUE;
+ }
+
+ if (!failed) {
+ client->running = TRUE;
+ io_loop_run(ioloop);
+ client->running = FALSE;
+ }
+
+ if (timeout_added && client->to != NULL)
+ timeout_remove(&client->to);
+
+ io_loop_set_current(prev_ioloop);
+ pop3c_client_ioloop_changed(client);
+ io_loop_set_current(ioloop);
+ io_loop_destroy(&ioloop);
+}
+
+static void pop3c_client_starttls(struct pop3c_client *client)
+{
+ o_stream_nsend_str(client->output, "STLS\r\n");
+ client->state = POP3C_CLIENT_STATE_STARTTLS;
+}
+
+static void pop3c_client_authenticate1(struct pop3c_client *client)
+{
+ const struct pop3c_client_settings *set = &client->set;
+
+ if (client->set.debug) {
+ if (set->master_user == NULL) {
+ i_debug("pop3c(%s): Authenticating as '%s' (with USER+PASS)",
+ client->set.host, set->username);
+ } else {
+ i_debug("pop3c(%s): Authenticating as master user '%s' for user '%s' (with SASL PLAIN)",
+ client->set.host, set->master_user,
+ set->username);
+ }
+ }
+
+ if (set->master_user == NULL) {
+ o_stream_nsend_str(client->output,
+ t_strdup_printf("USER %s\r\n", set->username));
+ client->state = POP3C_CLIENT_STATE_USER;
+ } else {
+ client->state = POP3C_CLIENT_STATE_AUTH;
+ o_stream_nsend_str(client->output, "AUTH PLAIN\r\n");
+ }
+}
+
+static const char *
+pop3c_client_get_sasl_plain_request(struct pop3c_client *client)
+{
+ const struct pop3c_client_settings *set = &client->set;
+ string_t *in, *out;
+
+ in = t_str_new(128);
+ if (set->master_user != NULL) {
+ str_append(in, set->username);
+ str_append_c(in, '\0');
+ str_append(in, set->master_user);
+ } else {
+ str_append_c(in, '\0');
+ str_append(in, set->username);
+ }
+ str_append_c(in, '\0');
+ str_append(in, set->password);
+
+ out = t_str_new(128);
+ base64_encode(str_data(in), str_len(in), out);
+ str_append(out, "\r\n");
+ return str_c(out);
+}
+
+static void pop3c_client_login_finished(struct pop3c_client *client)
+{
+ io_remove(&client->io);
+ client->io = io_add(client->fd, IO_READ, pop3c_client_input, client);
+
+ timeout_remove(&client->to);
+ client->state = POP3C_CLIENT_STATE_DONE;
+
+ if (client->running)
+ io_loop_stop(current_ioloop);
+}
+
+static int
+pop3c_client_prelogin_input_line(struct pop3c_client *client, const char *line)
+{
+ bool success = line[0] == '+';
+ const char *reply;
+
+ switch (client->state) {
+ case POP3C_CLIENT_STATE_CONNECTING:
+ if (!success) {
+ i_error("pop3c(%s): Server sent invalid banner: %s",
+ client->set.host, line);
+ return -1;
+ }
+ if (client->set.ssl_mode == POP3C_CLIENT_SSL_MODE_STARTTLS)
+ pop3c_client_starttls(client);
+ else
+ pop3c_client_authenticate1(client);
+ break;
+ case POP3C_CLIENT_STATE_STARTTLS:
+ if (!success) {
+ i_error("pop3c(%s): STLS failed: %s",
+ client->set.host, line);
+ return -1;
+ }
+ if (pop3c_client_ssl_init(client) < 0)
+ pop3c_client_disconnect(client);
+ break;
+ case POP3C_CLIENT_STATE_USER:
+ if (!success) {
+ i_error("pop3c(%s): USER failed: %s",
+ client->set.host, line);
+ return -1;
+ }
+
+ /* the PASS reply can take a long time.
+ switch to command timeout. */
+ timeout_remove(&client->to);
+ client->to = timeout_add(POP3C_COMMAND_TIMEOUT_MSECS,
+ pop3c_client_timeout, client);
+
+ o_stream_nsend_str(client->output,
+ t_strdup_printf("PASS %s\r\n", client->set.password));
+ client->state = POP3C_CLIENT_STATE_PASS;
+ client->auth_mech = "USER+PASS";
+ break;
+ case POP3C_CLIENT_STATE_AUTH:
+ if (line[0] != '+') {
+ i_error("pop3c(%s): AUTH PLAIN failed: %s",
+ client->set.host, line);
+ return -1;
+ }
+ o_stream_nsend_str(client->output,
+ pop3c_client_get_sasl_plain_request(client));
+ client->state = POP3C_CLIENT_STATE_PASS;
+ client->auth_mech = "AUTH PLAIN";
+ break;
+ case POP3C_CLIENT_STATE_PASS:
+ if (client->login_callback != NULL) {
+ reply = strncasecmp(line, "+OK ", 4) == 0 ? line + 4 :
+ strncasecmp(line, "-ERR ", 5) == 0 ? line + 5 :
+ line;
+ client_login_callback(client, success ?
+ POP3C_COMMAND_STATE_OK :
+ POP3C_COMMAND_STATE_ERR, reply);
+ } else if (!success) {
+ i_error("pop3c(%s): Authentication via %s failed: %s",
+ client->set.host, client->auth_mech, line);
+ }
+ if (!success)
+ return -1;
+
+ o_stream_nsend_str(client->output, "CAPA\r\n");
+ client->state = POP3C_CLIENT_STATE_CAPA;
+ break;
+ case POP3C_CLIENT_STATE_CAPA:
+ if (strncasecmp(line, "-ERR", 4) == 0) {
+ /* CAPA command not supported. some commands still
+ support UIDL though. */
+ client->capabilities |= POP3C_CAPABILITY_UIDL;
+ pop3c_client_login_finished(client);
+ break;
+ } else if (strcmp(line, ".") == 0) {
+ pop3c_client_login_finished(client);
+ break;
+ }
+ if ((client->set.parsed_features & POP3C_FEATURE_NO_PIPELINING) == 0 &&
+ strcasecmp(line, "PIPELINING") == 0)
+ client->capabilities |= POP3C_CAPABILITY_PIPELINING;
+ else if (strcasecmp(line, "TOP") == 0)
+ client->capabilities |= POP3C_CAPABILITY_TOP;
+ else if (strcasecmp(line, "UIDL") == 0)
+ client->capabilities |= POP3C_CAPABILITY_UIDL;
+ break;
+ case POP3C_CLIENT_STATE_DISCONNECTED:
+ case POP3C_CLIENT_STATE_DONE:
+ i_unreached();
+ }
+ return 0;
+}
+
+static void pop3c_client_prelogin_input(struct pop3c_client *client)
+{
+ const char *line, *errstr;
+
+ i_assert(client->state != POP3C_CLIENT_STATE_DONE);
+
+ /* we need to read as much as we can with SSL streams to avoid
+ hanging */
+ while ((line = i_stream_read_next_line(client->input)) != NULL) {
+ if (pop3c_client_prelogin_input_line(client, line) < 0) {
+ pop3c_client_disconnect(client);
+ return;
+ }
+ }
+
+ if (client->input->closed || client->input->eof ||
+ client->input->stream_errno != 0) {
+ /* disconnected */
+ if (client->ssl_iostream == NULL) {
+ i_error("pop3c(%s): Server disconnected unexpectedly",
+ client->set.host);
+ } else {
+ errstr = ssl_iostream_get_last_error(client->ssl_iostream);
+ if (errstr == NULL) {
+ errstr = client->input->stream_errno == 0 ? "EOF" :
+ strerror(client->input->stream_errno);
+ }
+ i_error("pop3c(%s): Server disconnected: %s",
+ client->set.host, errstr);
+ }
+ pop3c_client_disconnect(client);
+ }
+}
+
+static int pop3c_client_ssl_handshaked(const char **error_r, void *context)
+{
+ struct pop3c_client *client = context;
+ const char *error;
+
+ if (ssl_iostream_check_cert_validity(client->ssl_iostream,
+ client->set.host, &error) == 0) {
+ if (client->set.debug) {
+ i_debug("pop3c(%s): SSL handshake successful",
+ client->set.host);
+ }
+ return 0;
+ } else if (client->set.ssl_set.allow_invalid_cert) {
+ if (client->set.debug) {
+ i_debug("pop3c(%s): SSL handshake successful, "
+ "ignoring invalid certificate: %s",
+ client->set.host, error);
+ }
+ return 0;
+ } else {
+ *error_r = error;
+ return -1;
+ }
+}
+
+static int pop3c_client_ssl_init(struct pop3c_client *client)
+{
+ const char *error;
+
+ if (client->ssl_ctx == NULL) {
+ i_error("pop3c(%s): No SSL context", client->set.host);
+ return -1;
+ }
+
+ if (client->set.debug)
+ i_debug("pop3c(%s): Starting SSL handshake", client->set.host);
+
+ if (client->raw_input != client->input) {
+ /* recreate rawlog after STARTTLS */
+ i_stream_ref(client->raw_input);
+ o_stream_ref(client->raw_output);
+ i_stream_destroy(&client->input);
+ o_stream_destroy(&client->output);
+ client->input = client->raw_input;
+ client->output = client->raw_output;
+ }
+
+ if (io_stream_create_ssl_client(client->ssl_ctx, client->set.host,
+ &client->set.ssl_set, &client->input,
+ &client->output, &client->ssl_iostream, &error) < 0) {
+ i_error("pop3c(%s): Couldn't initialize SSL client: %s",
+ client->set.host, error);
+ return -1;
+ }
+ ssl_iostream_set_handshake_callback(client->ssl_iostream,
+ pop3c_client_ssl_handshaked,
+ client);
+ if (ssl_iostream_handshake(client->ssl_iostream) < 0) {
+ i_error("pop3c(%s): SSL handshake failed: %s", client->set.host,
+ ssl_iostream_get_last_error(client->ssl_iostream));
+ return -1;
+ }
+
+ if (*client->set.rawlog_dir != '\0') {
+ iostream_rawlog_create(client->set.rawlog_dir,
+ &client->input, &client->output);
+ }
+ return 0;
+}
+
+static void pop3c_client_connected(struct pop3c_client *client)
+{
+ int err;
+
+ err = net_geterror(client->fd);
+ if (err != 0) {
+ i_error("pop3c(%s): connect(%s, %u) failed: %s",
+ client->set.host, net_ip2addr(&client->ip),
+ client->set.port, strerror(err));
+ pop3c_client_disconnect(client);
+ return;
+ }
+ io_remove(&client->io);
+ client->io = io_add(client->fd, IO_READ,
+ pop3c_client_prelogin_input, client);
+
+ if (client->set.ssl_mode == POP3C_CLIENT_SSL_MODE_IMMEDIATE) {
+ if (pop3c_client_ssl_init(client) < 0)
+ pop3c_client_disconnect(client);
+ }
+}
+
+static void pop3c_client_connect_ip(struct pop3c_client *client)
+{
+ client->fd = net_connect_ip(&client->ip, client->set.port, NULL);
+ if (client->fd == -1) {
+ pop3c_client_disconnect(client);
+ return;
+ }
+
+ client->input = client->raw_input =
+ i_stream_create_fd(client->fd, POP3C_MAX_INBUF_SIZE);
+ client->output = client->raw_output =
+ o_stream_create_fd(client->fd, SIZE_MAX);
+ o_stream_set_no_error_handling(client->output, TRUE);
+
+ if (*client->set.rawlog_dir != '\0' &&
+ client->set.ssl_mode != POP3C_CLIENT_SSL_MODE_IMMEDIATE) {
+ iostream_rawlog_create(client->set.rawlog_dir,
+ &client->input, &client->output);
+ }
+ client->io = io_add(client->fd, IO_WRITE,
+ pop3c_client_connected, client);
+ client->to = timeout_add(POP3C_CONNECT_TIMEOUT_MSECS,
+ pop3c_client_timeout, client);
+ if (client->set.debug) {
+ i_debug("pop3c(%s): Connecting to %s:%u", client->set.host,
+ net_ip2addr(&client->ip), client->set.port);
+ }
+}
+
+static void
+pop3c_dns_callback(const struct dns_lookup_result *result,
+ struct pop3c_client *client)
+{
+ client->dns_lookup = NULL;
+
+ if (result->ret != 0) {
+ i_error("pop3c(%s): dns_lookup() failed: %s",
+ client->set.host, result->error);
+ pop3c_client_disconnect(client);
+ return;
+ }
+
+ i_assert(result->ips_count > 0);
+ client->ip = result->ips[0];
+ pop3c_client_connect_ip(client);
+}
+
+void pop3c_client_login(struct pop3c_client *client,
+ pop3c_login_callback_t *callback, void *context)
+{
+ if (client->fd != -1) {
+ i_assert(callback == NULL);
+ return;
+ }
+ i_assert(client->login_callback == NULL);
+ client->login_callback = callback;
+ client->login_context = context;
+ client->state = POP3C_CLIENT_STATE_CONNECTING;
+
+ if (client->set.debug)
+ i_debug("pop3c(%s): Looking up IP address", client->set.host);
+}
+
+bool pop3c_client_is_connected(struct pop3c_client *client)
+{
+ return client->fd != -1;
+}
+
+enum pop3c_capability
+pop3c_client_get_capabilities(struct pop3c_client *client)
+{
+ return client->capabilities;
+}
+
+static int pop3c_client_dot_input(struct pop3c_client *client)
+{
+ ssize_t ret;
+
+ while ((ret = i_stream_read(client->dot_input)) > 0 || ret == -2) {
+ i_stream_skip(client->dot_input,
+ i_stream_get_data_size(client->dot_input));
+ }
+ if (ret == 0)
+ return 0;
+ i_assert(ret == -1);
+
+ if (client->dot_input->stream_errno == 0)
+ ret = 1;
+ client->dot_input = NULL;
+
+ if (ret > 0) {
+ /* currently we don't actually care about preserving the
+ +OK reply line for multi-line replies, so just return
+ it as empty */
+ pop3c_client_async_callback(client, POP3C_COMMAND_STATE_OK, "");
+ return 1;
+ } else {
+ pop3c_client_async_callback_disconnected(client);
+ return -1;
+ }
+}
+
+static int
+pop3c_client_input_next_reply(struct pop3c_client *client)
+{
+ const char *line;
+ enum pop3c_command_state state;
+
+ line = i_stream_read_next_line(client->input);
+ if (line == NULL)
+ return client->input->eof ? -1 : 0;
+
+ if (strncasecmp(line, "+OK", 3) == 0) {
+ line += 3;
+ state = POP3C_COMMAND_STATE_OK;
+ } else if (strncasecmp(line, "-ERR", 4) == 0) {
+ line += 4;
+ state = POP3C_COMMAND_STATE_ERR;
+ } else {
+ i_error("pop3c(%s): Server sent unrecognized line: %s",
+ client->set.host, line);
+ state = POP3C_COMMAND_STATE_ERR;
+ }
+ if (line[0] == ' ')
+ line++;
+ if (array_count(&client->commands) == 0) {
+ i_error("pop3c(%s): Server sent line when no command was running: %s",
+ client->set.host, line);
+ } else {
+ pop3c_client_async_callback(client, state, line);
+ }
+ return 1;
+}
+
+static void pop3c_client_input(struct pop3c_client *client)
+{
+ int ret;
+
+ if (client->to != NULL)
+ timeout_reset(client->to);
+ do {
+ if (client->dot_input != NULL) {
+ /* continue reading the current multiline reply */
+ if ((ret = pop3c_client_dot_input(client)) == 0)
+ return;
+ } else {
+ ret = pop3c_client_input_next_reply(client);
+ }
+ } while (ret > 0);
+
+ if (ret < 0) {
+ i_error("pop3c(%s): Server disconnected unexpectedly",
+ client->set.host);
+ pop3c_client_disconnect(client);
+ }
+}
+
+static void pop3c_client_cmd_reply(enum pop3c_command_state state,
+ const char *reply, void *context)
+{
+ struct pop3c_client_sync_cmd_ctx *ctx = context;
+
+ i_assert(ctx->reply == NULL);
+
+ ctx->state = state;
+ ctx->reply = i_strdup(reply);
+}
+
+int pop3c_client_cmd_line(struct pop3c_client *client, const char *cmdline,
+ const char **reply_r)
+{
+ struct pop3c_client_sync_cmd_ctx ctx;
+
+ i_zero(&ctx);
+ pop3c_client_cmd_line_async(client, cmdline, pop3c_client_cmd_reply, &ctx);
+ while (ctx.reply == NULL)
+ pop3c_client_wait_one(client);
+ *reply_r = t_strdup(ctx.reply);
+ i_free(ctx.reply);
+ return ctx.state == POP3C_COMMAND_STATE_OK ? 0 : -1;
+}
+
+struct pop3c_client_cmd *
+pop3c_client_cmd_line_async(struct pop3c_client *client, const char *cmdline,
+ pop3c_cmd_callback_t *callback, void *context)
+{
+ struct pop3c_client_cmd *cmd;
+
+ if ((client->capabilities & POP3C_CAPABILITY_PIPELINING) == 0) {
+ while (array_count(&client->commands) > 0)
+ pop3c_client_wait_one(client);
+ }
+ i_assert(client->state == POP3C_CLIENT_STATE_DISCONNECTED ||
+ client->state == POP3C_CLIENT_STATE_DONE);
+ if (client->state == POP3C_CLIENT_STATE_DONE)
+ o_stream_nsend_str(client->output, cmdline);
+
+ cmd = array_append_space(&client->commands);
+ cmd->callback = callback;
+ cmd->context = context;
+ return cmd;
+}
+
+void pop3c_client_cmd_line_async_nocb(struct pop3c_client *client,
+ const char *cmdline)
+{
+ pop3c_client_cmd_line_async(client, cmdline, NULL, NULL);
+}
+
+static int seekable_fd_callback(const char **path_r, void *context)
+{
+ struct pop3c_client *client = context;
+ string_t *path;
+ int fd;
+
+ path = t_str_new(128);
+ str_append(path, client->set.temp_path_prefix);
+ fd = safe_mkstemp(path, 0600, (uid_t)-1, (gid_t)-1);
+ if (fd == -1) {
+ i_error("safe_mkstemp(%s) failed: %m", str_c(path));
+ return -1;
+ }
+
+ /* we just want the fd, unlink it */
+ if (i_unlink(str_c(path)) < 0) {
+ /* shouldn't happen.. */
+ i_close_fd(&fd);
+ return -1;
+ }
+
+ *path_r = str_c(path);
+ return fd;
+}
+
+int pop3c_client_cmd_stream(struct pop3c_client *client, const char *cmdline,
+ struct istream **input_r, const char **error_r)
+{
+ struct pop3c_client_sync_cmd_ctx ctx;
+ const char *reply;
+
+ if (client->state == POP3C_CLIENT_STATE_DISCONNECTED) {
+ *error_r = "Disconnected from server";
+ return -1;
+ }
+
+ i_zero(&ctx);
+ *input_r = pop3c_client_cmd_stream_async(client, cmdline,
+ pop3c_client_cmd_reply, &ctx);
+ while (ctx.reply == NULL)
+ pop3c_client_wait_one(client);
+ reply = t_strdup(ctx.reply);
+ i_free(ctx.reply);
+
+ if (ctx.state == POP3C_COMMAND_STATE_OK)
+ return 0;
+ i_stream_unref(input_r);
+ *error_r = reply;
+ return -1;
+}
+
+struct istream *
+pop3c_client_cmd_stream_async(struct pop3c_client *client, const char *cmdline,
+ pop3c_cmd_callback_t *callback, void *context)
+{
+ struct istream *input, *inputs[2];
+ struct pop3c_client_cmd *cmd;
+
+ cmd = pop3c_client_cmd_line_async(client, cmdline, callback, context);
+
+ input = i_stream_create_chain(&cmd->chain, POP3C_MAX_INBUF_SIZE);
+ inputs[0] = i_stream_create_dot(input, TRUE);
+ inputs[1] = NULL;
+ cmd->input = i_stream_create_seekable(inputs, POP3C_MAX_INBUF_SIZE,
+ seekable_fd_callback, client);
+ i_stream_unref(&input);
+ i_stream_unref(&inputs[0]);
+
+ i_stream_ref(cmd->input);
+ return cmd->input;
+}
diff --git a/src/lib-storage/index/pop3c/pop3c-client.h b/src/lib-storage/index/pop3c/pop3c-client.h
new file mode 100644
index 0000000..f0bbd64
--- /dev/null
+++ b/src/lib-storage/index/pop3c/pop3c-client.h
@@ -0,0 +1,86 @@
+#ifndef POP3C_CLIENT_H
+#define POP3C_CLIENT_H
+
+#include "net.h"
+#include "pop3c-settings.h"
+#include "iostream-ssl.h"
+
+enum pop3c_capability {
+ POP3C_CAPABILITY_PIPELINING = 0x01,
+ POP3C_CAPABILITY_TOP = 0x02,
+ POP3C_CAPABILITY_UIDL = 0x04
+};
+
+enum pop3c_command_state {
+ POP3C_COMMAND_STATE_OK,
+ POP3C_COMMAND_STATE_ERR,
+ POP3C_COMMAND_STATE_DISCONNECTED
+};
+
+enum pop3c_client_ssl_mode {
+ POP3C_CLIENT_SSL_MODE_NONE,
+ POP3C_CLIENT_SSL_MODE_IMMEDIATE,
+ POP3C_CLIENT_SSL_MODE_STARTTLS
+};
+
+struct pop3c_client_settings {
+ const char *host;
+ in_port_t port;
+
+ const char *master_user;
+ const char *username;
+ const char *password;
+
+ const char *dns_client_socket_path;
+ const char *temp_path_prefix;
+
+ enum pop3c_client_ssl_mode ssl_mode;
+ enum pop3c_features parsed_features;
+ struct ssl_iostream_settings ssl_set;
+
+ const char *rawlog_dir;
+ const char *ssl_crypto_device;
+ bool debug;
+};
+
+typedef void pop3c_login_callback_t(enum pop3c_command_state state,
+ const char *reply, void *context);
+typedef void pop3c_cmd_callback_t(enum pop3c_command_state state,
+ const char *reply, void *context);
+
+struct pop3c_client *
+pop3c_client_init(const struct pop3c_client_settings *set,
+ struct event *event_parent);
+void pop3c_client_deinit(struct pop3c_client **client);
+
+void pop3c_client_login(struct pop3c_client *client,
+ pop3c_login_callback_t *callback, void *context);
+
+bool pop3c_client_is_connected(struct pop3c_client *client);
+enum pop3c_capability
+pop3c_client_get_capabilities(struct pop3c_client *client);
+
+/* Returns 0 if received +OK reply, reply contains the text without the +OK.
+ Returns -1 if received -ERR reply or disconnected. */
+int pop3c_client_cmd_line(struct pop3c_client *client, const char *cmdline,
+ const char **reply_r);
+/* Start the command asynchronously. Call the callback when finished. */
+struct pop3c_client_cmd *
+pop3c_client_cmd_line_async(struct pop3c_client *client, const char *cmdline,
+ pop3c_cmd_callback_t *callback, void *context);
+/* Send a command, don't care if it succeeds or not. */
+void pop3c_client_cmd_line_async_nocb(struct pop3c_client *client,
+ const char *cmdline);
+/* Returns 0 and stream if succeeded, -1 and error if received -ERR reply or
+ disconnected. */
+int pop3c_client_cmd_stream(struct pop3c_client *client, const char *cmdline,
+ struct istream **input_r, const char **error_r);
+/* Start the command asynchronously. Call the callback when finished. */
+struct istream *
+pop3c_client_cmd_stream_async(struct pop3c_client *client, const char *cmdline,
+ pop3c_cmd_callback_t *callback, void *context);
+/* Wait for the next async command to finish. It's an error to call this when
+ there are no pending async commands. */
+void pop3c_client_wait_one(struct pop3c_client *client);
+
+#endif
diff --git a/src/lib-storage/index/pop3c/pop3c-mail.c b/src/lib-storage/index/pop3c/pop3c-mail.c
new file mode 100644
index 0000000..27a8427
--- /dev/null
+++ b/src/lib-storage/index/pop3c/pop3c-mail.c
@@ -0,0 +1,304 @@
+/* Copyright (c) 2011-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "ioloop.h"
+#include "istream.h"
+#include "index-mail.h"
+#include "pop3c-client.h"
+#include "pop3c-sync.h"
+#include "pop3c-storage.h"
+
+struct mail *
+pop3c_mail_alloc(struct mailbox_transaction_context *t,
+ enum mail_fetch_field wanted_fields,
+ struct mailbox_header_lookup_ctx *wanted_headers)
+{
+ struct pop3c_mail *mail;
+ pool_t pool;
+
+ pool = pool_alloconly_create("mail", 2048);
+ mail = p_new(pool, struct pop3c_mail, 1);
+
+ index_mail_init(&mail->imail, t, wanted_fields, wanted_headers, pool, NULL);
+ return &mail->imail.mail.mail;
+}
+
+static void pop3c_mail_close(struct mail *_mail)
+{
+ struct pop3c_mail *pmail = POP3C_MAIL(_mail);
+ struct pop3c_mailbox *mbox = POP3C_MAILBOX(_mail->box);
+
+ /* wait for any prefetch to finish before closing the mail */
+ while (pmail->prefetching)
+ pop3c_client_wait_one(mbox->client);
+ i_stream_unref(&pmail->prefetch_stream);
+ index_mail_close(_mail);
+}
+
+static int pop3c_mail_get_received_date(struct mail *_mail, time_t *date_r)
+{
+ struct pop3c_mailbox *mbox = POP3C_MAILBOX(_mail->box);
+ int tz;
+
+ if (mbox->storage->set->pop3c_quick_received_date) {
+ /* we don't care about the date, just return the current date */
+ *date_r = ioloop_time;
+ return 0;
+ }
+
+ /* FIXME: we could also parse the first Received: header and get
+ the date from there, but since this code is unlikely to be called
+ except during migration, I don't think it really matters. */
+ return index_mail_get_date(_mail, date_r, &tz);
+}
+
+static int pop3c_mail_get_save_date(struct mail *_mail, time_t *date_r)
+{
+ struct index_mail *mail = INDEX_MAIL(_mail);
+ struct index_mail_data *data = &mail->data;
+
+ if (data->save_date == (time_t)-1) {
+ /* FIXME: we could use a value stored in cache */
+ if (pop3c_mail_get_received_date(_mail, date_r) < 0)
+ return -1;
+ return 0;
+ }
+ *date_r = data->save_date;
+ return 0;
+}
+
+static int pop3c_mail_get_physical_size(struct mail *_mail, uoff_t *size_r)
+{
+ struct index_mail *mail = INDEX_MAIL(_mail);
+ struct pop3c_mailbox *mbox = POP3C_MAILBOX(_mail->box);
+ struct message_size hdr_size, body_size;
+ struct istream *input;
+
+ if (mail->data.virtual_size != UOFF_T_MAX) {
+ /* virtual size is already known. it's the same as our
+ (correct) physical size */
+ *size_r = mail->data.virtual_size;
+ return 0;
+ }
+ if (index_mail_get_physical_size(_mail, size_r) == 0) {
+ *size_r = mail->data.physical_size;
+ return 0;
+ }
+
+ if (_mail->lookup_abort == MAIL_LOOKUP_ABORT_READ_MAIL &&
+ (_mail->box->flags & MAILBOX_FLAG_POP3_SESSION) != 0) {
+ /* kludge: we want output for POP3 LIST with
+ pop3_fast_size_lookups=yes. use the remote's LIST values
+ regardless of their correctness */
+ if (mbox->msg_sizes == NULL) {
+ if (pop3c_sync_get_sizes(mbox) < 0)
+ return -1;
+ }
+ i_assert(_mail->seq <= mbox->msg_count);
+ *size_r = mbox->msg_sizes[_mail->seq-1];
+ return 0;
+ }
+
+ /* slow way: get the whole message body */
+ if (mail_get_stream(_mail, &hdr_size, &body_size, &input) < 0)
+ return -1;
+
+ i_assert(mail->data.physical_size != UOFF_T_MAX);
+ *size_r = mail->data.physical_size;
+ return 0;
+}
+
+static void pop3c_mail_cache_size(struct index_mail *mail)
+{
+ uoff_t size;
+
+ if (i_stream_get_size(mail->data.stream, TRUE, &size) <= 0)
+ return;
+ mail->data.virtual_size = size;
+ /* it'll be actually added to index when closing the mail in
+ index_mail_cache_sizes() */
+}
+
+static void
+pop3c_mail_prefetch_done(enum pop3c_command_state state,
+ const char *reply ATTR_UNUSED, void *context)
+{
+ struct pop3c_mail *pmail = context;
+
+ switch (state) {
+ case POP3C_COMMAND_STATE_OK:
+ break;
+ case POP3C_COMMAND_STATE_ERR:
+ case POP3C_COMMAND_STATE_DISCONNECTED:
+ i_stream_unref(&pmail->prefetch_stream);
+ /* let pop3c_mail_get_stream() figure out the error handling.
+ in case of a -ERR a retry might even work. */
+ break;
+ }
+ pmail->prefetching = FALSE;
+}
+
+static bool pop3c_mail_prefetch(struct mail *_mail)
+{
+ struct pop3c_mail *pmail = POP3C_MAIL(_mail);
+ struct pop3c_mailbox *mbox = POP3C_MAILBOX(_mail->box);
+ enum pop3c_capability capa;
+ const char *cmd;
+
+ if (pmail->imail.data.access_part != 0 &&
+ pmail->imail.data.stream == NULL &&
+ mail_stream_access_start(_mail)) {
+ capa = pop3c_client_get_capabilities(mbox->client);
+ pmail->prefetching_body = (capa & POP3C_CAPABILITY_TOP) == 0 ||
+ (pmail->imail.data.access_part & (READ_BODY | PARSE_BODY)) != 0;
+ if (pmail->prefetching_body)
+ cmd = t_strdup_printf("RETR %u\r\n", _mail->seq);
+ else
+ cmd = t_strdup_printf("TOP %u 0\r\n", _mail->seq);
+
+ pmail->prefetching = TRUE;
+ pmail->prefetch_stream =
+ pop3c_client_cmd_stream_async(mbox->client, cmd,
+ pop3c_mail_prefetch_done, pmail);
+ i_stream_set_name(pmail->prefetch_stream, t_strcut(cmd, '\r'));
+ return !pmail->prefetching;
+ }
+ return index_mail_prefetch(_mail);
+}
+
+static int
+pop3c_mail_get_stream(struct mail *_mail, bool get_body,
+ struct message_size *hdr_size,
+ struct message_size *body_size, struct istream **stream_r)
+{
+ struct pop3c_mail *pmail = POP3C_MAIL(_mail);
+ struct index_mail *mail = &pmail->imail;
+ struct pop3c_mailbox *mbox = POP3C_MAILBOX(_mail->box);
+ enum pop3c_capability capa;
+ const char *name, *cmd, *error;
+ struct istream *input;
+ bool new_stream = FALSE;
+
+ if ((mail->data.access_part & (READ_BODY | PARSE_BODY)) != 0)
+ get_body = TRUE;
+
+ while (pmail->prefetching) {
+ /* wait for prefetch to finish */
+ pop3c_client_wait_one(mbox->client);
+ }
+
+ if (pmail->prefetch_stream != NULL && mail->data.stream == NULL) {
+ mail->data.stream = pmail->prefetch_stream;
+ pmail->prefetch_stream = NULL;
+ new_stream = TRUE;
+ }
+
+ if (get_body && mail->data.stream != NULL) {
+ name = i_stream_get_name(mail->data.stream);
+ if (str_begins(name, "RETR")) {
+ /* we've fetched the body */
+ } else if (str_begins(name, "TOP")) {
+ /* we've fetched the header, but we need the body
+ now too */
+ index_mail_close_streams(mail);
+ } else {
+ i_panic("Unexpected POP3 stream name: %s", name);
+ }
+ }
+
+ if (mail->data.stream == NULL) {
+ if (!mail_stream_access_start(_mail))
+ return -1;
+ capa = pop3c_client_get_capabilities(mbox->client);
+ if (get_body || (capa & POP3C_CAPABILITY_TOP) == 0) {
+ cmd = t_strdup_printf("RETR %u\r\n", _mail->seq);
+ get_body = TRUE;
+ } else {
+ cmd = t_strdup_printf("TOP %u 0\r\n", _mail->seq);
+ }
+ if (pop3c_client_cmd_stream(mbox->client, cmd,
+ &input, &error) < 0) {
+ mail_storage_set_error(mbox->box.storage,
+ !pop3c_client_is_connected(mbox->client) ?
+ MAIL_ERROR_TEMP : MAIL_ERROR_EXPUNGED, error);
+ return -1;
+ }
+ mail->data.stream = input;
+ i_stream_set_name(mail->data.stream, t_strcut(cmd, '\r'));
+ new_stream = TRUE;
+ }
+ if (new_stream) {
+ if (mail->mail.v.istream_opened != NULL) {
+ if (mail->mail.v.istream_opened(_mail,
+ &mail->data.stream) < 0) {
+ index_mail_close_streams(mail);
+ return -1;
+ }
+ }
+ if (get_body)
+ pop3c_mail_cache_size(mail);
+ }
+ /* if this stream is used by some filter stream, make the
+ filter stream blocking */
+ mail->data.stream->blocking = TRUE;
+ return index_mail_init_stream(mail, hdr_size, body_size, stream_r);
+}
+
+static int
+pop3c_mail_get_special(struct mail *_mail, enum mail_fetch_field field,
+ const char **value_r)
+{
+ struct pop3c_mailbox *mbox = POP3C_MAILBOX(_mail->box);
+
+ switch (field) {
+ case MAIL_FETCH_UIDL_BACKEND:
+ case MAIL_FETCH_GUID:
+ if (mbox->msg_uidls == NULL) {
+ if (pop3c_sync_get_uidls(mbox) < 0)
+ return -1;
+ }
+ i_assert(_mail->seq <= mbox->msg_count);
+ *value_r = mbox->msg_uidls[_mail->seq-1];
+ return 0;
+ default:
+ return index_mail_get_special(_mail, field, value_r);
+ }
+}
+
+struct mail_vfuncs pop3c_mail_vfuncs = {
+ pop3c_mail_close,
+ index_mail_free,
+ index_mail_set_seq,
+ index_mail_set_uid,
+ index_mail_set_uid_cache_updates,
+ pop3c_mail_prefetch,
+ index_mail_precache,
+ index_mail_add_temp_wanted_fields,
+
+ index_mail_get_flags,
+ index_mail_get_keywords,
+ index_mail_get_keyword_indexes,
+ index_mail_get_modseq,
+ index_mail_get_pvt_modseq,
+ index_mail_get_parts,
+ index_mail_get_date,
+ pop3c_mail_get_received_date,
+ pop3c_mail_get_save_date,
+ index_mail_get_virtual_size,
+ pop3c_mail_get_physical_size,
+ index_mail_get_first_header,
+ index_mail_get_headers,
+ index_mail_get_header_stream,
+ pop3c_mail_get_stream,
+ index_mail_get_binary_stream,
+ pop3c_mail_get_special,
+ index_mail_get_backend_mail,
+ index_mail_update_flags,
+ index_mail_update_keywords,
+ index_mail_update_modseq,
+ index_mail_update_pvt_modseq,
+ NULL,
+ index_mail_expunge,
+ index_mail_set_cache_corrupted,
+ index_mail_opened,
+};
diff --git a/src/lib-storage/index/pop3c/pop3c-settings.c b/src/lib-storage/index/pop3c/pop3c-settings.c
new file mode 100644
index 0000000..db876e1
--- /dev/null
+++ b/src/lib-storage/index/pop3c/pop3c-settings.c
@@ -0,0 +1,116 @@
+/* Copyright (c) 2011-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "settings-parser.h"
+#include "mail-storage-settings.h"
+#include "pop3c-settings.h"
+
+#include <stddef.h>
+
+#undef DEF
+#define DEF(type, name) \
+ SETTING_DEFINE_STRUCT_##type(#name, name, struct pop3c_settings)
+
+static const struct setting_define pop3c_setting_defines[] = {
+ DEF(STR, pop3c_host),
+ DEF(IN_PORT, pop3c_port),
+
+ DEF(STR_VARS, pop3c_user),
+ DEF(STR_VARS, pop3c_master_user),
+ DEF(STR, pop3c_password),
+
+ DEF(ENUM, pop3c_ssl),
+ DEF(BOOL, pop3c_ssl_verify),
+
+ DEF(STR, pop3c_rawlog_dir),
+ DEF(BOOL, pop3c_quick_received_date),
+
+ DEF(STR, pop3c_features),
+
+ SETTING_DEFINE_LIST_END
+};
+
+static const struct pop3c_settings pop3c_default_settings = {
+ .pop3c_host = "",
+ .pop3c_port = 110,
+
+ .pop3c_user = "%u",
+ .pop3c_master_user = "",
+ .pop3c_password = "",
+
+ .pop3c_ssl = "no:pop3s:starttls",
+ .pop3c_ssl_verify = TRUE,
+
+ .pop3c_rawlog_dir = "",
+ .pop3c_quick_received_date = FALSE,
+
+ .pop3c_features = ""
+};
+
+/* <settings checks> */
+struct pop3c_feature_list {
+ const char *name;
+ enum pop3c_features num;
+};
+
+static const struct pop3c_feature_list pop3c_feature_list[] = {
+ { "no-pipelining", POP3C_FEATURE_NO_PIPELINING },
+ { NULL, 0 }
+};
+
+static int
+pop3c_settings_parse_features(struct pop3c_settings *set,
+ const char **error_r)
+{
+ enum pop3c_features features = 0;
+ const struct pop3c_feature_list *list;
+ const char *const *str;
+
+ str = t_strsplit_spaces(set->pop3c_features, " ,");
+ for (; *str != NULL; str++) {
+ list = pop3c_feature_list;
+ for (; list->name != NULL; list++) {
+ if (strcasecmp(*str, list->name) == 0) {
+ features |= list->num;
+ break;
+ }
+ }
+ if (list->name == NULL) {
+ *error_r = t_strdup_printf("pop3c_features: "
+ "Unknown feature: %s", *str);
+ return -1;
+ }
+ }
+ set->parsed_features = features;
+ return 0;
+}
+
+static bool pop3c_settings_check(void *_set, pool_t pool ATTR_UNUSED,
+ const char **error_r)
+{
+ struct pop3c_settings *set = _set;
+
+ if (pop3c_settings_parse_features(set, error_r) < 0)
+ return FALSE;
+ return TRUE;
+}
+/* </settings checks> */
+
+static const struct setting_parser_info pop3c_setting_parser_info = {
+ .module_name = "pop3c",
+ .defines = pop3c_setting_defines,
+ .defaults = &pop3c_default_settings,
+
+ .type_offset = SIZE_MAX,
+ .struct_size = sizeof(struct pop3c_settings),
+
+ .parent_offset = SIZE_MAX,
+ .parent = &mail_user_setting_parser_info,
+
+ .check_func = pop3c_settings_check
+};
+
+const struct setting_parser_info *pop3c_get_setting_parser_info(void)
+{
+ return &pop3c_setting_parser_info;
+}
diff --git a/src/lib-storage/index/pop3c/pop3c-settings.h b/src/lib-storage/index/pop3c/pop3c-settings.h
new file mode 100644
index 0000000..bf44e24
--- /dev/null
+++ b/src/lib-storage/index/pop3c/pop3c-settings.h
@@ -0,0 +1,33 @@
+#ifndef POP3C_SETTINGS_H
+#define POP3C_SETTINGS_H
+
+#include "net.h"
+
+/* <settings checks> */
+enum pop3c_features {
+ POP3C_FEATURE_NO_PIPELINING = 0x1,
+};
+/* </settings checks> */
+
+
+struct pop3c_settings {
+ const char *pop3c_host;
+ in_port_t pop3c_port;
+
+ const char *pop3c_user;
+ const char *pop3c_master_user;
+ const char *pop3c_password;
+
+ const char *pop3c_ssl;
+ bool pop3c_ssl_verify;
+
+ const char *pop3c_rawlog_dir;
+ bool pop3c_quick_received_date;
+
+ const char *pop3c_features;
+ enum pop3c_features parsed_features;
+};
+
+const struct setting_parser_info *pop3c_get_setting_parser_info(void);
+
+#endif
diff --git a/src/lib-storage/index/pop3c/pop3c-storage.c b/src/lib-storage/index/pop3c/pop3c-storage.c
new file mode 100644
index 0000000..f784683
--- /dev/null
+++ b/src/lib-storage/index/pop3c/pop3c-storage.c
@@ -0,0 +1,368 @@
+/* Copyright (c) 2011-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "ioloop.h"
+#include "str.h"
+#include "mail-copy.h"
+#include "mail-user.h"
+#include "mailbox-list-private.h"
+#include "index-mail.h"
+#include "pop3c-client.h"
+#include "pop3c-sync.h"
+#include "pop3c-storage.h"
+
+#define DNS_CLIENT_SOCKET_NAME "dns-client"
+
+extern struct mail_storage pop3c_storage;
+extern struct mailbox pop3c_mailbox;
+
+static struct event_category event_category_pop3c = {
+ .name = "pop3c",
+ .parent = &event_category_storage,
+};
+
+static struct mail_storage *pop3c_storage_alloc(void)
+{
+ struct pop3c_storage *storage;
+ pool_t pool;
+
+ pool = pool_alloconly_create("pop3c storage", 512+256);
+ storage = p_new(pool, struct pop3c_storage, 1);
+ storage->storage = pop3c_storage;
+ storage->storage.pool = pool;
+ return &storage->storage;
+}
+
+static int
+pop3c_storage_create(struct mail_storage *_storage,
+ struct mail_namespace *ns,
+ const char **error_r)
+{
+ struct pop3c_storage *storage = POP3C_STORAGE(_storage);
+
+ storage->set = mail_namespace_get_driver_settings(ns, _storage);
+ if (storage->set->pop3c_host[0] == '\0') {
+ *error_r = "missing pop3c_host";
+ return -1;
+ }
+ if (storage->set->pop3c_password[0] == '\0') {
+ *error_r = "missing pop3c_password";
+ return -1;
+ }
+
+ return 0;
+}
+
+static struct pop3c_client *
+pop3c_client_create_from_set(struct mail_storage *storage,
+ const struct pop3c_settings *set)
+{
+ struct pop3c_client_settings client_set;
+ string_t *str;
+
+ i_zero(&client_set);
+ client_set.host = set->pop3c_host;
+ client_set.port = set->pop3c_port;
+ client_set.username = set->pop3c_user;
+ client_set.master_user = set->pop3c_master_user;
+ client_set.password = set->pop3c_password;
+ client_set.dns_client_socket_path =
+ storage->user->set->base_dir[0] == '\0' ? "" :
+ t_strconcat(storage->user->set->base_dir, "/",
+ DNS_CLIENT_SOCKET_NAME, NULL);
+ str = t_str_new(128);
+ mail_user_set_get_temp_prefix(str, storage->user->set);
+ client_set.temp_path_prefix = str_c(str);
+
+ client_set.debug = storage->user->mail_debug;
+ client_set.rawlog_dir =
+ mail_user_home_expand(storage->user, set->pop3c_rawlog_dir);
+
+ mail_user_init_ssl_client_settings(storage->user, &client_set.ssl_set);
+
+ if (!set->pop3c_ssl_verify)
+ client_set.ssl_set.allow_invalid_cert = TRUE;
+
+ if (strcmp(set->pop3c_ssl, "pop3s") == 0)
+ client_set.ssl_mode = POP3C_CLIENT_SSL_MODE_IMMEDIATE;
+ else if (strcmp(set->pop3c_ssl, "starttls") == 0)
+ client_set.ssl_mode = POP3C_CLIENT_SSL_MODE_STARTTLS;
+ else
+ client_set.ssl_mode = POP3C_CLIENT_SSL_MODE_NONE;
+ return pop3c_client_init(&client_set, storage->event);
+}
+
+static void
+pop3c_storage_get_list_settings(const struct mail_namespace *ns ATTR_UNUSED,
+ struct mailbox_list_settings *set)
+{
+ set->layout = MAILBOX_LIST_NAME_FS;
+ if (set->root_dir != NULL && *set->root_dir != '\0' &&
+ set->index_dir == NULL) {
+ /* we don't really care about root_dir, but we
+ just need to get index_dir autocreated. */
+ set->index_dir = set->root_dir;
+ }
+}
+
+static struct mailbox *
+pop3c_mailbox_alloc(struct mail_storage *storage, struct mailbox_list *list,
+ const char *vname, enum mailbox_flags flags)
+{
+ struct pop3c_mailbox *mbox;
+ pool_t pool;
+
+ pool = pool_alloconly_create("pop3c mailbox", 1024*3);
+ mbox = p_new(pool, struct pop3c_mailbox, 1);
+ mbox->box = pop3c_mailbox;
+ mbox->box.pool = pool;
+ mbox->box.storage = storage;
+ mbox->box.list = list;
+ mbox->box.list->props |= MAILBOX_LIST_PROP_AUTOCREATE_DIRS;
+ mbox->box.mail_vfuncs = &pop3c_mail_vfuncs;
+ mbox->storage = POP3C_STORAGE(storage);
+
+ index_storage_mailbox_alloc(&mbox->box, vname, flags, MAIL_INDEX_PREFIX);
+ return &mbox->box;
+}
+
+static int
+pop3c_mailbox_exists(struct mailbox *box, bool auto_boxes,
+ enum mailbox_existence *existence_r)
+{
+ if ((auto_boxes && mailbox_is_autocreated(box)) || box->inbox_any)
+ *existence_r = MAILBOX_EXISTENCE_SELECT;
+ else
+ *existence_r = MAILBOX_EXISTENCE_NONE;
+ return 0;
+}
+
+static void pop3c_login_callback(enum pop3c_command_state state,
+ const char *reply, void *context)
+{
+ struct pop3c_mailbox *mbox = context;
+
+ switch (state) {
+ case POP3C_COMMAND_STATE_OK:
+ mbox->logged_in = TRUE;
+ break;
+ case POP3C_COMMAND_STATE_ERR:
+ if (str_begins(reply, "[IN-USE] ")) {
+ mail_storage_set_error(mbox->box.storage,
+ MAIL_ERROR_INUSE, reply + 9);
+ } else {
+ /* authentication failure probably */
+ mail_storage_set_error(mbox->box.storage,
+ MAIL_ERROR_PARAMS, reply);
+ }
+ break;
+ case POP3C_COMMAND_STATE_DISCONNECTED:
+ mailbox_set_critical(&mbox->box,
+ "pop3c: Disconnected from remote server");
+ break;
+ }
+}
+
+static int pop3c_mailbox_open(struct mailbox *box)
+{
+ struct pop3c_mailbox *mbox = POP3C_MAILBOX(box);
+
+ if (strcmp(box->name, "INBOX") != 0) {
+ mail_storage_set_error(box->storage, MAIL_ERROR_NOTFOUND,
+ T_MAIL_ERR_MAILBOX_NOT_FOUND(box->vname));
+ return -1;
+ }
+
+ if (index_storage_mailbox_open(box, FALSE) < 0)
+ return -1;
+
+ mbox->client = pop3c_client_create_from_set(box->storage,
+ mbox->storage->set);
+ pop3c_client_login(mbox->client, pop3c_login_callback, mbox);
+ pop3c_client_wait_one(mbox->client);
+ return mbox->logged_in ? 0 : -1;
+}
+
+static void pop3c_mailbox_close(struct mailbox *box)
+{
+ struct pop3c_mailbox *mbox = POP3C_MAILBOX(box);
+
+ pool_unref(&mbox->uidl_pool);
+ i_free_and_null(mbox->msg_uids);
+ i_free_and_null(mbox->msg_sizes);
+ pop3c_client_deinit(&mbox->client);
+ index_storage_mailbox_close(box);
+}
+
+static int
+pop3c_mailbox_create(struct mailbox *box,
+ const struct mailbox_update *update ATTR_UNUSED,
+ bool directory ATTR_UNUSED)
+{
+ mail_storage_set_error(box->storage, MAIL_ERROR_NOTPOSSIBLE,
+ "POP3 mailbox creation isn't supported");
+ return -1;
+}
+
+static int
+pop3c_mailbox_update(struct mailbox *box,
+ const struct mailbox_update *update ATTR_UNUSED)
+{
+ if (!guid_128_is_empty(update->mailbox_guid) ||
+ update->uid_validity != 0 || update->min_next_uid != 0 ||
+ update->min_first_recent_uid != 0) {
+ mail_storage_set_error(box->storage, MAIL_ERROR_NOTPOSSIBLE,
+ "POP3 mailbox update isn't supported");
+ }
+ return index_storage_mailbox_update(box, update);
+}
+
+static int pop3c_mailbox_get_status(struct mailbox *box,
+ enum mailbox_status_items items,
+ struct mailbox_status *status_r)
+{
+ struct pop3c_mailbox *mbox = POP3C_MAILBOX(box);
+
+ if (index_storage_get_status(box, items, status_r) < 0)
+ return -1;
+
+ if ((pop3c_client_get_capabilities(mbox->client) &
+ POP3C_CAPABILITY_UIDL) == 0)
+ status_r->have_guids = FALSE;
+ return 0;
+}
+
+static int pop3c_mailbox_get_metadata(struct mailbox *box,
+ enum mailbox_metadata_items items,
+ struct mailbox_metadata *metadata_r)
+{
+ if ((items & MAILBOX_METADATA_GUID) != 0) {
+ /* a bit ugly way to do this, but better than nothing for now.
+ FIXME: if indexes are enabled, keep this there. */
+ mail_generate_guid_128_hash(box->name, metadata_r->guid);
+ items &= ENUM_NEGATE(MAILBOX_METADATA_GUID);
+ }
+ if (items != 0) {
+ if (index_mailbox_get_metadata(box, items, metadata_r) < 0)
+ return -1;
+ }
+ return 0;
+}
+
+static void pop3c_notify_changes(struct mailbox *box ATTR_UNUSED)
+{
+}
+
+static struct mail_save_context *
+pop3c_save_alloc(struct mailbox_transaction_context *t)
+{
+ struct mail_save_context *ctx;
+
+ ctx = i_new(struct mail_save_context, 1);
+ ctx->transaction = t;
+ return ctx;
+}
+
+static int
+pop3c_save_begin(struct mail_save_context *ctx,
+ struct istream *input ATTR_UNUSED)
+{
+ mail_storage_set_error(ctx->transaction->box->storage,
+ MAIL_ERROR_NOTPOSSIBLE, "POP3 doesn't support saving mails");
+ return -1;
+}
+
+static int pop3c_save_continue(struct mail_save_context *ctx ATTR_UNUSED)
+{
+ return -1;
+}
+
+static int pop3c_save_finish(struct mail_save_context *ctx)
+{
+ index_save_context_free(ctx);
+ return -1;
+}
+
+static void
+pop3c_save_cancel(struct mail_save_context *ctx)
+{
+ index_save_context_free(ctx);
+}
+
+static bool pop3c_storage_is_inconsistent(struct mailbox *box)
+{
+ struct pop3c_mailbox *mbox = POP3C_MAILBOX(box);
+
+ return index_storage_is_inconsistent(box) ||
+ !pop3c_client_is_connected(mbox->client);
+}
+
+struct mail_storage pop3c_storage = {
+ .name = POP3C_STORAGE_NAME,
+ .class_flags = MAIL_STORAGE_CLASS_FLAG_NO_ROOT |
+ MAIL_STORAGE_CLASS_FLAG_HAVE_MAIL_GUIDS,
+ .event_category = &event_category_pop3c,
+
+ .v = {
+ pop3c_get_setting_parser_info,
+ pop3c_storage_alloc,
+ pop3c_storage_create,
+ index_storage_destroy,
+ NULL,
+ pop3c_storage_get_list_settings,
+ NULL,
+ pop3c_mailbox_alloc,
+ NULL,
+ NULL,
+ }
+};
+
+struct mailbox pop3c_mailbox = {
+ .v = {
+ index_storage_is_readonly,
+ index_storage_mailbox_enable,
+ pop3c_mailbox_exists,
+ pop3c_mailbox_open,
+ pop3c_mailbox_close,
+ index_storage_mailbox_free,
+ pop3c_mailbox_create,
+ pop3c_mailbox_update,
+ index_storage_mailbox_delete,
+ index_storage_mailbox_rename,
+ pop3c_mailbox_get_status,
+ pop3c_mailbox_get_metadata,
+ index_storage_set_subscribed,
+ index_storage_attribute_set,
+ index_storage_attribute_get,
+ index_storage_attribute_iter_init,
+ index_storage_attribute_iter_next,
+ index_storage_attribute_iter_deinit,
+ index_storage_list_index_has_changed,
+ index_storage_list_index_update_sync,
+ pop3c_storage_sync_init,
+ index_mailbox_sync_next,
+ index_mailbox_sync_deinit,
+ NULL,
+ pop3c_notify_changes,
+ index_transaction_begin,
+ index_transaction_commit,
+ index_transaction_rollback,
+ NULL,
+ pop3c_mail_alloc,
+ index_storage_search_init,
+ index_storage_search_deinit,
+ index_storage_search_next_nonblock,
+ index_storage_search_next_update_seq,
+ index_storage_search_next_match_mail,
+ pop3c_save_alloc,
+ pop3c_save_begin,
+ pop3c_save_continue,
+ pop3c_save_finish,
+ pop3c_save_cancel,
+ mail_storage_copy,
+ NULL,
+ NULL,
+ NULL,
+ pop3c_storage_is_inconsistent
+ }
+};
diff --git a/src/lib-storage/index/pop3c/pop3c-storage.h b/src/lib-storage/index/pop3c/pop3c-storage.h
new file mode 100644
index 0000000..f271a59
--- /dev/null
+++ b/src/lib-storage/index/pop3c/pop3c-storage.h
@@ -0,0 +1,51 @@
+#ifndef POP3C_STORAGE_H
+#define POP3C_STORAGE_H
+
+#include "index-storage.h"
+
+#define POP3C_STORAGE_NAME "pop3c"
+
+struct pop3c_storage {
+ struct mail_storage storage;
+ const struct pop3c_settings *set;
+};
+
+struct pop3c_mailbox {
+ struct mailbox box;
+ struct pop3c_storage *storage;
+
+ struct pop3c_client *client;
+
+ pool_t uidl_pool;
+ unsigned int msg_count;
+ /* LIST sizes */
+ uoff_t *msg_sizes;
+ /* UIDL strings */
+ const char *const *msg_uidls;
+ /* index UIDs for each message in this session.
+ the UID may not exist for the entire session */
+ uint32_t *msg_uids;
+
+ bool logged_in:1;
+};
+
+struct pop3c_mail {
+ struct index_mail imail;
+ struct istream *prefetch_stream;
+
+ bool prefetching:1;
+ bool prefetching_body:1;
+};
+
+#define POP3C_STORAGE(s) container_of(s, struct pop3c_storage, storage)
+#define POP3C_MAILBOX(s) container_of(s, struct pop3c_mailbox, box)
+#define POP3C_MAIL(s) container_of(s, struct pop3c_mail, imail.mail.mail)
+
+struct mail *
+pop3c_mail_alloc(struct mailbox_transaction_context *t,
+ enum mail_fetch_field wanted_fields,
+ struct mailbox_header_lookup_ctx *wanted_headers);
+
+extern struct mail_vfuncs pop3c_mail_vfuncs;
+
+#endif
diff --git a/src/lib-storage/index/pop3c/pop3c-sync.c b/src/lib-storage/index/pop3c/pop3c-sync.c
new file mode 100644
index 0000000..2d2dbe3
--- /dev/null
+++ b/src/lib-storage/index/pop3c/pop3c-sync.c
@@ -0,0 +1,361 @@
+/* Copyright (c) 2011-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "ioloop.h"
+#include "istream.h"
+#include "bsearch-insert-pos.h"
+#include "str.h"
+#include "sort.h"
+#include "strnum.h"
+#include "index-mail.h"
+#include "pop3c-client.h"
+#include "pop3c-storage.h"
+#include "pop3c-sync.h"
+#include "mailbox-recent-flags.h"
+
+struct pop3c_sync_msg {
+ uint32_t seq;
+ const char *uidl;
+};
+ARRAY_DEFINE_TYPE(pop3c_sync_msg, struct pop3c_sync_msg);
+
+int pop3c_sync_get_uidls(struct pop3c_mailbox *mbox)
+{
+ ARRAY_TYPE(const_string) uidls;
+ struct istream *input;
+ const char *error, *cline;
+ char *line, *p;
+ unsigned int seq, line_seq;
+
+ if (mbox->msg_uidls != NULL)
+ return 0;
+ if ((pop3c_client_get_capabilities(mbox->client) &
+ POP3C_CAPABILITY_UIDL) == 0) {
+ mail_storage_set_error(mbox->box.storage,
+ MAIL_ERROR_NOTPOSSIBLE,
+ "UIDLs not supported by server");
+ return -1;
+ }
+
+ if (pop3c_client_cmd_stream(mbox->client, "UIDL\r\n",
+ &input, &error) < 0) {
+ mailbox_set_critical(&mbox->box, "UIDL failed: %s", error);
+ return -1;
+ }
+
+ mbox->uidl_pool = pool_alloconly_create("POP3 UIDLs", 1024*32);
+ p_array_init(&uidls, mbox->uidl_pool, 64); seq = 0;
+ while ((line = i_stream_read_next_line(input)) != NULL) {
+ seq++;
+ p = strchr(line, ' ');
+ if (p == NULL) {
+ mailbox_set_critical(&mbox->box,
+ "Invalid UIDL line: %s", line);
+ break;
+ }
+ *p++ = '\0';
+ if (str_to_uint(line, &line_seq) < 0 || line_seq != seq) {
+ mailbox_set_critical(&mbox->box,
+ "Unexpected UIDL seq: %s != %u", line, seq);
+ break;
+ }
+
+ cline = p_strdup(mbox->uidl_pool, p);
+ array_push_back(&uidls, &cline);
+ }
+ i_stream_destroy(&input);
+ if (line != NULL) {
+ pool_unref(&mbox->uidl_pool);
+ return -1;
+ }
+ if (seq == 0) {
+ /* make msg_uidls non-NULL */
+ array_append_zero(&uidls);
+ }
+ mbox->msg_uidls = array_front(&uidls);
+ mbox->msg_count = seq;
+ return 0;
+}
+
+int pop3c_sync_get_sizes(struct pop3c_mailbox *mbox)
+{
+ struct istream *input;
+ const char *error;
+ char *line, *p;
+ unsigned int seq, line_seq;
+
+ i_assert(mbox->msg_sizes == NULL);
+
+ if (mbox->msg_uidls == NULL) {
+ if (pop3c_sync_get_uidls(mbox) < 0)
+ return -1;
+ }
+ if (mbox->msg_count == 0) {
+ mbox->msg_sizes = i_new(uoff_t, 1);
+ return 0;
+ }
+
+ if (pop3c_client_cmd_stream(mbox->client, "LIST\r\n",
+ &input, &error) < 0) {
+ mailbox_set_critical(&mbox->box, "LIST failed: %s", error);
+ return -1;
+ }
+
+ mbox->msg_sizes = i_new(uoff_t, mbox->msg_count); seq = 0;
+ while ((line = i_stream_read_next_line(input)) != NULL) {
+ if (++seq > mbox->msg_count) {
+ mailbox_set_critical(&mbox->box,
+ "Too much data in LIST: %s", line);
+ break;
+ }
+ p = strchr(line, ' ');
+ if (p == NULL) {
+ mailbox_set_critical(&mbox->box,
+ "Invalid LIST line: %s", line);
+ break;
+ }
+ *p++ = '\0';
+ if (str_to_uint(line, &line_seq) < 0 || line_seq != seq) {
+ mailbox_set_critical(&mbox->box,
+ "Unexpected LIST seq: %s != %u", line, seq);
+ break;
+ }
+ if (str_to_uoff(p, &mbox->msg_sizes[seq-1]) < 0) {
+ mailbox_set_critical(&mbox->box,
+ "Invalid LIST size: %s", p);
+ break;
+ }
+ }
+ i_stream_destroy(&input);
+ if (line != NULL) {
+ i_free_and_null(mbox->msg_sizes);
+ return -1;
+ }
+ return 0;
+}
+
+static void
+pop3c_get_local_msgs(pool_t pool, ARRAY_TYPE(pop3c_sync_msg) *local_msgs,
+ uint32_t messages_count,
+ struct mail_cache_view *cache_view,
+ unsigned int cache_idx)
+{
+ string_t *str = t_str_new(128);
+ struct pop3c_sync_msg msg;
+ uint32_t seq;
+
+ i_zero(&msg);
+ for (seq = 1; seq <= messages_count; seq++) {
+ str_truncate(str, 0);
+ if (mail_cache_lookup_field(cache_view, str, seq,
+ cache_idx) > 0)
+ msg.uidl = p_strdup(pool, str_c(str));
+ msg.seq = seq;
+ array_idx_set(local_msgs, seq-1, &msg);
+ }
+}
+
+static void
+pop3c_get_remote_msgs(ARRAY_TYPE(pop3c_sync_msg) *remote_msgs,
+ struct pop3c_mailbox *mbox)
+{
+ struct pop3c_sync_msg *msg;
+ uint32_t seq;
+
+ for (seq = 1; seq <= mbox->msg_count; seq++) {
+ msg = array_append_space(remote_msgs);
+ msg->seq = seq;
+ msg->uidl = mbox->msg_uidls[seq-1];
+ }
+}
+
+static int pop3c_sync_msg_uidl_cmp(const struct pop3c_sync_msg *msg1,
+ const struct pop3c_sync_msg *msg2)
+{
+ return null_strcmp(msg1->uidl, msg2->uidl);
+}
+
+static void
+pop3c_sync_messages(struct pop3c_mailbox *mbox,
+ struct mail_index_view *sync_view,
+ struct mail_index_transaction *sync_trans,
+ struct mail_cache_view *cache_view)
+{
+ struct index_mailbox_context *ibox =
+ INDEX_STORAGE_CONTEXT(&mbox->box);
+ const struct mail_index_header *hdr;
+ struct mail_cache_transaction_ctx *cache_trans;
+ ARRAY_TYPE(pop3c_sync_msg) local_msgs, remote_msgs;
+ const struct pop3c_sync_msg *lmsg, *rmsg;
+ uint32_t seq1, seq2, next_uid;
+ unsigned int lidx, ridx, lcount, rcount;
+ unsigned int cache_idx = ibox->cache_fields[MAIL_CACHE_POP3_UIDL].idx;
+ pool_t pool;
+
+ i_assert(mbox->msg_uids == NULL);
+
+ /* set our uidvalidity */
+ hdr = mail_index_get_header(sync_view);
+ if (hdr->uid_validity == 0) {
+ uint32_t uid_validity = ioloop_time;
+ mail_index_update_header(sync_trans,
+ offsetof(struct mail_index_header, uid_validity),
+ &uid_validity, sizeof(uid_validity), TRUE);
+ }
+
+ pool = pool_alloconly_create(MEMPOOL_GROWING"pop3c sync", 10240);
+ p_array_init(&local_msgs, pool, hdr->messages_count);
+ pop3c_get_local_msgs(pool, &local_msgs, hdr->messages_count,
+ cache_view, cache_idx);
+ p_array_init(&remote_msgs, pool, mbox->msg_count);
+ pop3c_get_remote_msgs(&remote_msgs, mbox);
+
+ /* sort the messages by UIDLs, because some servers reorder messages */
+ array_sort(&local_msgs, pop3c_sync_msg_uidl_cmp);
+ array_sort(&remote_msgs, pop3c_sync_msg_uidl_cmp);
+
+ /* skip over existing messages with matching UIDLs and expunge the ones
+ that no longer exist in remote. */
+ mbox->msg_uids = mbox->msg_count == 0 ?
+ i_new(uint32_t, 1) : /* avoid malloc(0) assert */
+ i_new(uint32_t, mbox->msg_count);
+ cache_trans = mail_cache_get_transaction(cache_view, sync_trans);
+
+ lmsg = array_get(&local_msgs, &lcount);
+ rmsg = array_get(&remote_msgs, &rcount);
+ next_uid = hdr->next_uid;
+ lidx = ridx = 0;
+ while (lidx < lcount || ridx < rcount) {
+ uint32_t lseq = lidx < lcount ? lmsg[lidx].seq : 0;
+ uint32_t rseq = ridx < rcount ? rmsg[ridx].seq : 0;
+ int ret;
+
+ if (lidx >= lcount)
+ ret = 1;
+ else if (ridx >= rcount || lmsg[lidx].uidl == NULL)
+ ret = -1;
+ else
+ ret = strcmp(lmsg[lidx].uidl, rmsg[ridx].uidl);
+ if (ret < 0) {
+ /* message expunged in remote, or we didn't have a
+ local message's UIDL in cache. */
+ mail_index_expunge(sync_trans, lseq);
+ lidx++;
+ } else if (ret > 0) {
+ /* new message in remote */
+ i_assert(mbox->msg_uids[rseq-1] == 0);
+ mbox->msg_uids[rseq-1] = next_uid;
+ mail_index_append(sync_trans, next_uid++, &lseq);
+ mail_cache_add(cache_trans, lseq, cache_idx,
+ rmsg[ridx].uidl,
+ strlen(rmsg[ridx].uidl));
+ ridx++;
+ } else {
+ /* UIDL matched */
+ i_assert(mbox->msg_uids[rseq-1] == 0);
+ mail_index_lookup_uid(sync_view, lseq,
+ &mbox->msg_uids[rseq-1]);
+ lidx++;
+ ridx++;
+ }
+ }
+
+ /* mark the newly seen messages as recent */
+ if (mail_index_lookup_seq_range(sync_view, hdr->first_recent_uid,
+ hdr->next_uid, &seq1, &seq2))
+ mailbox_recent_flags_set_seqs(&mbox->box, sync_view, seq1, seq2);
+ pool_unref(&pool);
+}
+
+int pop3c_sync(struct pop3c_mailbox *mbox)
+{
+ struct mail_index_sync_ctx *index_sync_ctx;
+ struct mail_index_view *sync_view, *trans_view;
+ struct mail_index_transaction *sync_trans;
+ struct mail_index_sync_rec sync_rec;
+ struct mail_cache_view *cache_view = NULL;
+ enum mail_index_sync_flags sync_flags;
+ unsigned int idx;
+ string_t *str;
+ const char *reply;
+ int ret;
+ bool deletions = FALSE;
+
+ if (pop3c_sync_get_uidls(mbox) < 0)
+ return -1;
+
+ sync_flags = index_storage_get_sync_flags(&mbox->box) |
+ MAIL_INDEX_SYNC_FLAG_FLUSH_DIRTY;
+
+ ret = mail_index_sync_begin(mbox->box.index, &index_sync_ctx,
+ &sync_view, &sync_trans, sync_flags);
+ if (ret <= 0) {
+ if (ret < 0)
+ mailbox_set_index_error(&mbox->box);
+ return ret;
+ }
+
+ if (mbox->msg_uids == NULL) {
+ trans_view = mail_index_transaction_open_updated_view(sync_trans);
+ cache_view = mail_cache_view_open(mbox->box.cache, trans_view);
+ pop3c_sync_messages(mbox, sync_view, sync_trans, cache_view);
+ }
+
+ /* mark expunges messages as deleted in this pop3 session,
+ if those exist */
+ str = t_str_new(32);
+ while (mail_index_sync_next(index_sync_ctx, &sync_rec)) {
+ if (sync_rec.type != MAIL_INDEX_SYNC_TYPE_EXPUNGE)
+ continue;
+
+ if (!bsearch_insert_pos(&sync_rec.uid1, mbox->msg_uids,
+ mbox->msg_count, sizeof(uint32_t),
+ uint32_cmp, &idx)) {
+ /* no such messages in this session */
+ continue;
+ }
+ for (; idx < mbox->msg_count; idx++) {
+ i_assert(mbox->msg_uids[idx] >= sync_rec.uid1);
+ if (mbox->msg_uids[idx] > sync_rec.uid2)
+ break;
+
+ str_truncate(str, 0);
+ str_printfa(str, "DELE %u\r\n", idx+1);
+ pop3c_client_cmd_line_async_nocb(mbox->client, str_c(str));
+ deletions = TRUE;
+ }
+ }
+
+ if (mail_index_sync_commit(&index_sync_ctx) < 0) {
+ mailbox_set_index_error(&mbox->box);
+ return -1;
+ }
+ if (cache_view != NULL) {
+ mail_cache_view_close(&cache_view);
+ mail_index_view_close(&trans_view);
+ }
+ if (deletions) {
+ if (pop3c_client_cmd_line(mbox->client, "QUIT\r\n",
+ &reply) < 0) {
+ mail_storage_set_error(mbox->box.storage,
+ MAIL_ERROR_TEMP, reply);
+ return -1;
+ }
+ }
+ return 0;
+}
+
+struct mailbox_sync_context *
+pop3c_storage_sync_init(struct mailbox *box, enum mailbox_sync_flags flags)
+{
+ struct pop3c_mailbox *mbox = POP3C_MAILBOX(box);
+ int ret = 0;
+
+ if ((flags & MAILBOX_SYNC_FLAG_FULL_READ) != 0 &&
+ mbox->msg_uidls == NULL) {
+ /* FIXME: reconnect */
+ }
+
+ ret = pop3c_sync(mbox);
+ return index_mailbox_sync_init(box, flags, ret < 0);
+}
diff --git a/src/lib-storage/index/pop3c/pop3c-sync.h b/src/lib-storage/index/pop3c/pop3c-sync.h
new file mode 100644
index 0000000..bf3c802
--- /dev/null
+++ b/src/lib-storage/index/pop3c/pop3c-sync.h
@@ -0,0 +1,14 @@
+#ifndef POP3C_SYNC_H
+#define POP3C_SYNC_H
+
+struct mailbox;
+struct pop3c_mailbox;
+
+struct mailbox_sync_context *
+pop3c_storage_sync_init(struct mailbox *box, enum mailbox_sync_flags flags);
+int pop3c_sync(struct pop3c_mailbox *mbox);
+
+int pop3c_sync_get_sizes(struct pop3c_mailbox *mbox);
+int pop3c_sync_get_uidls(struct pop3c_mailbox *mbox);
+
+#endif
diff --git a/src/lib-storage/index/raw/Makefile.am b/src/lib-storage/index/raw/Makefile.am
new file mode 100644
index 0000000..58a9df3
--- /dev/null
+++ b/src/lib-storage/index/raw/Makefile.am
@@ -0,0 +1,21 @@
+noinst_LTLIBRARIES = libstorage_raw.la
+
+AM_CPPFLAGS = \
+ -I$(top_srcdir)/src/lib \
+ -I$(top_srcdir)/src/lib-mail \
+ -I$(top_srcdir)/src/lib-imap \
+ -I$(top_srcdir)/src/lib-index \
+ -I$(top_srcdir)/src/lib-storage \
+ -I$(top_srcdir)/src/lib-storage/index
+
+libstorage_raw_la_SOURCES = \
+ raw-mail.c \
+ raw-sync.c \
+ raw-storage.c
+
+headers = \
+ raw-storage.h \
+ raw-sync.h
+
+pkginc_libdir=$(pkgincludedir)
+pkginc_lib_HEADERS = $(headers)
diff --git a/src/lib-storage/index/raw/Makefile.in b/src/lib-storage/index/raw/Makefile.in
new file mode 100644
index 0000000..c42d116
--- /dev/null
+++ b/src/lib-storage/index/raw/Makefile.in
@@ -0,0 +1,822 @@
+# Makefile.in generated by automake 1.16.1 from Makefile.am.
+# @configure_input@
+
+# Copyright (C) 1994-2018 Free Software Foundation, Inc.
+
+# This Makefile.in is free software; the Free Software Foundation
+# gives unlimited permission to copy and/or distribute it,
+# with or without modifications, as long as this notice is preserved.
+
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY, to the extent permitted by law; without
+# even the implied warranty of MERCHANTABILITY or FITNESS FOR A
+# PARTICULAR PURPOSE.
+
+@SET_MAKE@
+
+
+VPATH = @srcdir@
+am__is_gnu_make = { \
+ if test -z '$(MAKELEVEL)'; then \
+ false; \
+ elif test -n '$(MAKE_HOST)'; then \
+ true; \
+ elif test -n '$(MAKE_VERSION)' && test -n '$(CURDIR)'; then \
+ true; \
+ else \
+ false; \
+ fi; \
+}
+am__make_running_with_option = \
+ case $${target_option-} in \
+ ?) ;; \
+ *) echo "am__make_running_with_option: internal error: invalid" \
+ "target option '$${target_option-}' specified" >&2; \
+ exit 1;; \
+ esac; \
+ has_opt=no; \
+ sane_makeflags=$$MAKEFLAGS; \
+ if $(am__is_gnu_make); then \
+ sane_makeflags=$$MFLAGS; \
+ else \
+ case $$MAKEFLAGS in \
+ *\\[\ \ ]*) \
+ bs=\\; \
+ sane_makeflags=`printf '%s\n' "$$MAKEFLAGS" \
+ | sed "s/$$bs$$bs[$$bs $$bs ]*//g"`;; \
+ esac; \
+ fi; \
+ skip_next=no; \
+ strip_trailopt () \
+ { \
+ flg=`printf '%s\n' "$$flg" | sed "s/$$1.*$$//"`; \
+ }; \
+ for flg in $$sane_makeflags; do \
+ test $$skip_next = yes && { skip_next=no; continue; }; \
+ case $$flg in \
+ *=*|--*) continue;; \
+ -*I) strip_trailopt 'I'; skip_next=yes;; \
+ -*I?*) strip_trailopt 'I';; \
+ -*O) strip_trailopt 'O'; skip_next=yes;; \
+ -*O?*) strip_trailopt 'O';; \
+ -*l) strip_trailopt 'l'; skip_next=yes;; \
+ -*l?*) strip_trailopt 'l';; \
+ -[dEDm]) skip_next=yes;; \
+ -[JT]) skip_next=yes;; \
+ esac; \
+ case $$flg in \
+ *$$target_option*) has_opt=yes; break;; \
+ esac; \
+ done; \
+ test $$has_opt = yes
+am__make_dryrun = (target_option=n; $(am__make_running_with_option))
+am__make_keepgoing = (target_option=k; $(am__make_running_with_option))
+pkgdatadir = $(datadir)/@PACKAGE@
+pkgincludedir = $(includedir)/@PACKAGE@
+pkglibdir = $(libdir)/@PACKAGE@
+pkglibexecdir = $(libexecdir)/@PACKAGE@
+am__cd = CDPATH="$${ZSH_VERSION+.}$(PATH_SEPARATOR)" && cd
+install_sh_DATA = $(install_sh) -c -m 644
+install_sh_PROGRAM = $(install_sh) -c
+install_sh_SCRIPT = $(install_sh) -c
+INSTALL_HEADER = $(INSTALL_DATA)
+transform = $(program_transform_name)
+NORMAL_INSTALL = :
+PRE_INSTALL = :
+POST_INSTALL = :
+NORMAL_UNINSTALL = :
+PRE_UNINSTALL = :
+POST_UNINSTALL = :
+build_triplet = @build@
+host_triplet = @host@
+subdir = src/lib-storage/index/raw
+ACLOCAL_M4 = $(top_srcdir)/aclocal.m4
+am__aclocal_m4_deps = $(top_srcdir)/m4/ac_checktype2.m4 \
+ $(top_srcdir)/m4/ac_typeof.m4 $(top_srcdir)/m4/arc4random.m4 \
+ $(top_srcdir)/m4/blockdev.m4 $(top_srcdir)/m4/c99_vsnprintf.m4 \
+ $(top_srcdir)/m4/clock_gettime.m4 $(top_srcdir)/m4/crypt.m4 \
+ $(top_srcdir)/m4/crypt_xpg6.m4 $(top_srcdir)/m4/dbqlk.m4 \
+ $(top_srcdir)/m4/dirent_dtype.m4 $(top_srcdir)/m4/dovecot.m4 \
+ $(top_srcdir)/m4/fd_passing.m4 $(top_srcdir)/m4/fdatasync.m4 \
+ $(top_srcdir)/m4/flexible_array_member.m4 \
+ $(top_srcdir)/m4/glibc.m4 $(top_srcdir)/m4/gmtime_max.m4 \
+ $(top_srcdir)/m4/gmtime_tm_gmtoff.m4 \
+ $(top_srcdir)/m4/ioloop.m4 $(top_srcdir)/m4/iovec.m4 \
+ $(top_srcdir)/m4/ipv6.m4 $(top_srcdir)/m4/libcap.m4 \
+ $(top_srcdir)/m4/libtool.m4 $(top_srcdir)/m4/libwrap.m4 \
+ $(top_srcdir)/m4/linux_mremap.m4 $(top_srcdir)/m4/ltoptions.m4 \
+ $(top_srcdir)/m4/ltsugar.m4 $(top_srcdir)/m4/ltversion.m4 \
+ $(top_srcdir)/m4/lt~obsolete.m4 $(top_srcdir)/m4/mmap_write.m4 \
+ $(top_srcdir)/m4/mntctl.m4 $(top_srcdir)/m4/modules.m4 \
+ $(top_srcdir)/m4/notify.m4 $(top_srcdir)/m4/nsl.m4 \
+ $(top_srcdir)/m4/off_t_max.m4 $(top_srcdir)/m4/pkg.m4 \
+ $(top_srcdir)/m4/pr_set_dumpable.m4 \
+ $(top_srcdir)/m4/q_quotactl.m4 $(top_srcdir)/m4/quota.m4 \
+ $(top_srcdir)/m4/random.m4 $(top_srcdir)/m4/rlimit.m4 \
+ $(top_srcdir)/m4/sendfile.m4 $(top_srcdir)/m4/size_t_signed.m4 \
+ $(top_srcdir)/m4/sockpeercred.m4 $(top_srcdir)/m4/sql.m4 \
+ $(top_srcdir)/m4/ssl.m4 $(top_srcdir)/m4/st_tim.m4 \
+ $(top_srcdir)/m4/static_array.m4 $(top_srcdir)/m4/test_with.m4 \
+ $(top_srcdir)/m4/time_t.m4 $(top_srcdir)/m4/typeof.m4 \
+ $(top_srcdir)/m4/typeof_dev_t.m4 \
+ $(top_srcdir)/m4/uoff_t_max.m4 $(top_srcdir)/m4/vararg.m4 \
+ $(top_srcdir)/m4/want_apparmor.m4 \
+ $(top_srcdir)/m4/want_bsdauth.m4 \
+ $(top_srcdir)/m4/want_bzlib.m4 \
+ $(top_srcdir)/m4/want_cassandra.m4 \
+ $(top_srcdir)/m4/want_cdb.m4 \
+ $(top_srcdir)/m4/want_checkpassword.m4 \
+ $(top_srcdir)/m4/want_clucene.m4 $(top_srcdir)/m4/want_db.m4 \
+ $(top_srcdir)/m4/want_gssapi.m4 $(top_srcdir)/m4/want_icu.m4 \
+ $(top_srcdir)/m4/want_ldap.m4 $(top_srcdir)/m4/want_lua.m4 \
+ $(top_srcdir)/m4/want_lz4.m4 $(top_srcdir)/m4/want_lzma.m4 \
+ $(top_srcdir)/m4/want_mysql.m4 $(top_srcdir)/m4/want_pam.m4 \
+ $(top_srcdir)/m4/want_passwd.m4 $(top_srcdir)/m4/want_pgsql.m4 \
+ $(top_srcdir)/m4/want_prefetch.m4 \
+ $(top_srcdir)/m4/want_shadow.m4 \
+ $(top_srcdir)/m4/want_sodium.m4 $(top_srcdir)/m4/want_solr.m4 \
+ $(top_srcdir)/m4/want_sqlite.m4 \
+ $(top_srcdir)/m4/want_stemmer.m4 \
+ $(top_srcdir)/m4/want_systemd.m4 \
+ $(top_srcdir)/m4/want_textcat.m4 \
+ $(top_srcdir)/m4/want_unwind.m4 $(top_srcdir)/m4/want_zlib.m4 \
+ $(top_srcdir)/m4/want_zstd.m4 $(top_srcdir)/configure.ac
+am__configure_deps = $(am__aclocal_m4_deps) $(CONFIGURE_DEPENDENCIES) \
+ $(ACLOCAL_M4)
+DIST_COMMON = $(srcdir)/Makefile.am $(pkginc_lib_HEADERS) \
+ $(am__DIST_COMMON)
+mkinstalldirs = $(install_sh) -d
+CONFIG_HEADER = $(top_builddir)/config.h
+CONFIG_CLEAN_FILES =
+CONFIG_CLEAN_VPATH_FILES =
+LTLIBRARIES = $(noinst_LTLIBRARIES)
+libstorage_raw_la_LIBADD =
+am_libstorage_raw_la_OBJECTS = raw-mail.lo raw-sync.lo raw-storage.lo
+libstorage_raw_la_OBJECTS = $(am_libstorage_raw_la_OBJECTS)
+AM_V_lt = $(am__v_lt_@AM_V@)
+am__v_lt_ = $(am__v_lt_@AM_DEFAULT_V@)
+am__v_lt_0 = --silent
+am__v_lt_1 =
+AM_V_P = $(am__v_P_@AM_V@)
+am__v_P_ = $(am__v_P_@AM_DEFAULT_V@)
+am__v_P_0 = false
+am__v_P_1 = :
+AM_V_GEN = $(am__v_GEN_@AM_V@)
+am__v_GEN_ = $(am__v_GEN_@AM_DEFAULT_V@)
+am__v_GEN_0 = @echo " GEN " $@;
+am__v_GEN_1 =
+AM_V_at = $(am__v_at_@AM_V@)
+am__v_at_ = $(am__v_at_@AM_DEFAULT_V@)
+am__v_at_0 = @
+am__v_at_1 =
+DEFAULT_INCLUDES = -I.@am__isrc@ -I$(top_builddir)
+depcomp = $(SHELL) $(top_srcdir)/depcomp
+am__maybe_remake_depfiles = depfiles
+am__depfiles_remade = ./$(DEPDIR)/raw-mail.Plo \
+ ./$(DEPDIR)/raw-storage.Plo ./$(DEPDIR)/raw-sync.Plo
+am__mv = mv -f
+COMPILE = $(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) \
+ $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS)
+LTCOMPILE = $(LIBTOOL) $(AM_V_lt) --tag=CC $(AM_LIBTOOLFLAGS) \
+ $(LIBTOOLFLAGS) --mode=compile $(CC) $(DEFS) \
+ $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) $(CPPFLAGS) \
+ $(AM_CFLAGS) $(CFLAGS)
+AM_V_CC = $(am__v_CC_@AM_V@)
+am__v_CC_ = $(am__v_CC_@AM_DEFAULT_V@)
+am__v_CC_0 = @echo " CC " $@;
+am__v_CC_1 =
+CCLD = $(CC)
+LINK = $(LIBTOOL) $(AM_V_lt) --tag=CC $(AM_LIBTOOLFLAGS) \
+ $(LIBTOOLFLAGS) --mode=link $(CCLD) $(AM_CFLAGS) $(CFLAGS) \
+ $(AM_LDFLAGS) $(LDFLAGS) -o $@
+AM_V_CCLD = $(am__v_CCLD_@AM_V@)
+am__v_CCLD_ = $(am__v_CCLD_@AM_DEFAULT_V@)
+am__v_CCLD_0 = @echo " CCLD " $@;
+am__v_CCLD_1 =
+SOURCES = $(libstorage_raw_la_SOURCES)
+DIST_SOURCES = $(libstorage_raw_la_SOURCES)
+am__can_run_installinfo = \
+ case $$AM_UPDATE_INFO_DIR in \
+ n|no|NO) false;; \
+ *) (install-info --version) >/dev/null 2>&1;; \
+ esac
+am__vpath_adj_setup = srcdirstrip=`echo "$(srcdir)" | sed 's|.|.|g'`;
+am__vpath_adj = case $$p in \
+ $(srcdir)/*) f=`echo "$$p" | sed "s|^$$srcdirstrip/||"`;; \
+ *) f=$$p;; \
+ esac;
+am__strip_dir = f=`echo $$p | sed -e 's|^.*/||'`;
+am__install_max = 40
+am__nobase_strip_setup = \
+ srcdirstrip=`echo "$(srcdir)" | sed 's/[].[^$$\\*|]/\\\\&/g'`
+am__nobase_strip = \
+ for p in $$list; do echo "$$p"; done | sed -e "s|$$srcdirstrip/||"
+am__nobase_list = $(am__nobase_strip_setup); \
+ for p in $$list; do echo "$$p $$p"; done | \
+ sed "s| $$srcdirstrip/| |;"' / .*\//!s/ .*/ ./; s,\( .*\)/[^/]*$$,\1,' | \
+ $(AWK) 'BEGIN { files["."] = "" } { files[$$2] = files[$$2] " " $$1; \
+ if (++n[$$2] == $(am__install_max)) \
+ { print $$2, files[$$2]; n[$$2] = 0; files[$$2] = "" } } \
+ END { for (dir in files) print dir, files[dir] }'
+am__base_list = \
+ sed '$$!N;$$!N;$$!N;$$!N;$$!N;$$!N;$$!N;s/\n/ /g' | \
+ sed '$$!N;$$!N;$$!N;$$!N;s/\n/ /g'
+am__uninstall_files_from_dir = { \
+ test -z "$$files" \
+ || { test ! -d "$$dir" && test ! -f "$$dir" && test ! -r "$$dir"; } \
+ || { echo " ( cd '$$dir' && rm -f" $$files ")"; \
+ $(am__cd) "$$dir" && rm -f $$files; }; \
+ }
+am__installdirs = "$(DESTDIR)$(pkginc_libdir)"
+HEADERS = $(pkginc_lib_HEADERS)
+am__tagged_files = $(HEADERS) $(SOURCES) $(TAGS_FILES) $(LISP)
+# Read a list of newline-separated strings from the standard input,
+# and print each of them once, without duplicates. Input order is
+# *not* preserved.
+am__uniquify_input = $(AWK) '\
+ BEGIN { nonempty = 0; } \
+ { items[$$0] = 1; nonempty = 1; } \
+ END { if (nonempty) { for (i in items) print i; }; } \
+'
+# Make sure the list of sources is unique. This is necessary because,
+# e.g., the same source file might be shared among _SOURCES variables
+# for different programs/libraries.
+am__define_uniq_tagged_files = \
+ list='$(am__tagged_files)'; \
+ unique=`for i in $$list; do \
+ if test -f "$$i"; then echo $$i; else echo $(srcdir)/$$i; fi; \
+ done | $(am__uniquify_input)`
+ETAGS = etags
+CTAGS = ctags
+am__DIST_COMMON = $(srcdir)/Makefile.in $(top_srcdir)/depcomp
+DISTFILES = $(DIST_COMMON) $(DIST_SOURCES) $(TEXINFOS) $(EXTRA_DIST)
+ACLOCAL = @ACLOCAL@
+ACLOCAL_AMFLAGS = @ACLOCAL_AMFLAGS@
+AMTAR = @AMTAR@
+AM_DEFAULT_VERBOSITY = @AM_DEFAULT_VERBOSITY@
+APPARMOR_LIBS = @APPARMOR_LIBS@
+AR = @AR@
+AUTH_CFLAGS = @AUTH_CFLAGS@
+AUTH_LIBS = @AUTH_LIBS@
+AUTOCONF = @AUTOCONF@
+AUTOHEADER = @AUTOHEADER@
+AUTOMAKE = @AUTOMAKE@
+AWK = @AWK@
+BINARY_CFLAGS = @BINARY_CFLAGS@
+BINARY_LDFLAGS = @BINARY_LDFLAGS@
+BISON = @BISON@
+CASSANDRA_CFLAGS = @CASSANDRA_CFLAGS@
+CASSANDRA_LIBS = @CASSANDRA_LIBS@
+CC = @CC@
+CCDEPMODE = @CCDEPMODE@
+CDB_LIBS = @CDB_LIBS@
+CFLAGS = @CFLAGS@
+CLUCENE_CFLAGS = @CLUCENE_CFLAGS@
+CLUCENE_LIBS = @CLUCENE_LIBS@
+COMPRESS_LIBS = @COMPRESS_LIBS@
+CPP = @CPP@
+CPPFLAGS = @CPPFLAGS@
+CRYPT_LIBS = @CRYPT_LIBS@
+CXX = @CXX@
+CXXCPP = @CXXCPP@
+CXXDEPMODE = @CXXDEPMODE@
+CXXFLAGS = @CXXFLAGS@
+CYGPATH_W = @CYGPATH_W@
+DEFS = @DEFS@
+DEPDIR = @DEPDIR@
+DICT_LIBS = @DICT_LIBS@
+DLLIB = @DLLIB@
+DLLTOOL = @DLLTOOL@
+DSYMUTIL = @DSYMUTIL@
+DUMPBIN = @DUMPBIN@
+ECHO_C = @ECHO_C@
+ECHO_N = @ECHO_N@
+ECHO_T = @ECHO_T@
+EGREP = @EGREP@
+EXEEXT = @EXEEXT@
+FGREP = @FGREP@
+FLEX = @FLEX@
+FUZZER_CPPFLAGS = @FUZZER_CPPFLAGS@
+FUZZER_LDFLAGS = @FUZZER_LDFLAGS@
+GREP = @GREP@
+INSTALL = @INSTALL@
+INSTALL_DATA = @INSTALL_DATA@
+INSTALL_PROGRAM = @INSTALL_PROGRAM@
+INSTALL_SCRIPT = @INSTALL_SCRIPT@
+INSTALL_STRIP_PROGRAM = @INSTALL_STRIP_PROGRAM@
+KRB5CONFIG = @KRB5CONFIG@
+KRB5_CFLAGS = @KRB5_CFLAGS@
+KRB5_LIBS = @KRB5_LIBS@
+LD = @LD@
+LDAP_LIBS = @LDAP_LIBS@
+LDFLAGS = @LDFLAGS@
+LD_NO_WHOLE_ARCHIVE = @LD_NO_WHOLE_ARCHIVE@
+LD_WHOLE_ARCHIVE = @LD_WHOLE_ARCHIVE@
+LIBCAP = @LIBCAP@
+LIBDOVECOT = @LIBDOVECOT@
+LIBDOVECOT_COMPRESS = @LIBDOVECOT_COMPRESS@
+LIBDOVECOT_DEPS = @LIBDOVECOT_DEPS@
+LIBDOVECOT_DSYNC = @LIBDOVECOT_DSYNC@
+LIBDOVECOT_LA_LIBS = @LIBDOVECOT_LA_LIBS@
+LIBDOVECOT_LDA = @LIBDOVECOT_LDA@
+LIBDOVECOT_LDAP = @LIBDOVECOT_LDAP@
+LIBDOVECOT_LIBFTS = @LIBDOVECOT_LIBFTS@
+LIBDOVECOT_LIBFTS_DEPS = @LIBDOVECOT_LIBFTS_DEPS@
+LIBDOVECOT_LOGIN = @LIBDOVECOT_LOGIN@
+LIBDOVECOT_LUA = @LIBDOVECOT_LUA@
+LIBDOVECOT_LUA_DEPS = @LIBDOVECOT_LUA_DEPS@
+LIBDOVECOT_SQL = @LIBDOVECOT_SQL@
+LIBDOVECOT_STORAGE = @LIBDOVECOT_STORAGE@
+LIBDOVECOT_STORAGE_DEPS = @LIBDOVECOT_STORAGE_DEPS@
+LIBEXTTEXTCAT_CFLAGS = @LIBEXTTEXTCAT_CFLAGS@
+LIBEXTTEXTCAT_LIBS = @LIBEXTTEXTCAT_LIBS@
+LIBICONV = @LIBICONV@
+LIBICU_CFLAGS = @LIBICU_CFLAGS@
+LIBICU_LIBS = @LIBICU_LIBS@
+LIBOBJS = @LIBOBJS@
+LIBS = @LIBS@
+LIBSODIUM_CFLAGS = @LIBSODIUM_CFLAGS@
+LIBSODIUM_LIBS = @LIBSODIUM_LIBS@
+LIBTIRPC_CFLAGS = @LIBTIRPC_CFLAGS@
+LIBTIRPC_LIBS = @LIBTIRPC_LIBS@
+LIBTOOL = @LIBTOOL@
+LIBUNWIND_CFLAGS = @LIBUNWIND_CFLAGS@
+LIBUNWIND_LIBS = @LIBUNWIND_LIBS@
+LIBWRAP_LIBS = @LIBWRAP_LIBS@
+LINKED_STORAGE_LDADD = @LINKED_STORAGE_LDADD@
+LIPO = @LIPO@
+LN_S = @LN_S@
+LTLIBICONV = @LTLIBICONV@
+LTLIBOBJS = @LTLIBOBJS@
+LT_SYS_LIBRARY_PATH = @LT_SYS_LIBRARY_PATH@
+LUA_CFLAGS = @LUA_CFLAGS@
+LUA_LIBS = @LUA_LIBS@
+MAINT = @MAINT@
+MAKEINFO = @MAKEINFO@
+MANIFEST_TOOL = @MANIFEST_TOOL@
+MKDIR_P = @MKDIR_P@
+MODULE_LIBS = @MODULE_LIBS@
+MODULE_SUFFIX = @MODULE_SUFFIX@
+MYSQL_CFLAGS = @MYSQL_CFLAGS@
+MYSQL_CONFIG = @MYSQL_CONFIG@
+MYSQL_LIBS = @MYSQL_LIBS@
+NM = @NM@
+NMEDIT = @NMEDIT@
+NOPLUGIN_LDFLAGS = @NOPLUGIN_LDFLAGS@
+OBJDUMP = @OBJDUMP@
+OBJEXT = @OBJEXT@
+OTOOL = @OTOOL@
+OTOOL64 = @OTOOL64@
+PACKAGE = @PACKAGE@
+PACKAGE_BUGREPORT = @PACKAGE_BUGREPORT@
+PACKAGE_NAME = @PACKAGE_NAME@
+PACKAGE_STRING = @PACKAGE_STRING@
+PACKAGE_TARNAME = @PACKAGE_TARNAME@
+PACKAGE_URL = @PACKAGE_URL@
+PACKAGE_VERSION = @PACKAGE_VERSION@
+PANDOC = @PANDOC@
+PATH_SEPARATOR = @PATH_SEPARATOR@
+PGSQL_CFLAGS = @PGSQL_CFLAGS@
+PGSQL_LIBS = @PGSQL_LIBS@
+PG_CONFIG = @PG_CONFIG@
+PIE_CFLAGS = @PIE_CFLAGS@
+PIE_LDFLAGS = @PIE_LDFLAGS@
+PKG_CONFIG = @PKG_CONFIG@
+PKG_CONFIG_LIBDIR = @PKG_CONFIG_LIBDIR@
+PKG_CONFIG_PATH = @PKG_CONFIG_PATH@
+QUOTA_LIBS = @QUOTA_LIBS@
+RANLIB = @RANLIB@
+RELRO_LDFLAGS = @RELRO_LDFLAGS@
+RPCGEN = @RPCGEN@
+RUN_TEST = @RUN_TEST@
+SED = @SED@
+SETTING_FILES = @SETTING_FILES@
+SET_MAKE = @SET_MAKE@
+SHELL = @SHELL@
+SQLITE_CFLAGS = @SQLITE_CFLAGS@
+SQLITE_LIBS = @SQLITE_LIBS@
+SQL_CFLAGS = @SQL_CFLAGS@
+SQL_LIBS = @SQL_LIBS@
+SSL_CFLAGS = @SSL_CFLAGS@
+SSL_LIBS = @SSL_LIBS@
+STRIP = @STRIP@
+SYSTEMD_CFLAGS = @SYSTEMD_CFLAGS@
+SYSTEMD_LIBS = @SYSTEMD_LIBS@
+VALGRIND = @VALGRIND@
+VERSION = @VERSION@
+ZSTD_CFLAGS = @ZSTD_CFLAGS@
+ZSTD_LIBS = @ZSTD_LIBS@
+abs_builddir = @abs_builddir@
+abs_srcdir = @abs_srcdir@
+abs_top_builddir = @abs_top_builddir@
+abs_top_srcdir = @abs_top_srcdir@
+ac_ct_AR = @ac_ct_AR@
+ac_ct_CC = @ac_ct_CC@
+ac_ct_CXX = @ac_ct_CXX@
+ac_ct_DUMPBIN = @ac_ct_DUMPBIN@
+am__include = @am__include@
+am__leading_dot = @am__leading_dot@
+am__quote = @am__quote@
+am__tar = @am__tar@
+am__untar = @am__untar@
+bindir = @bindir@
+build = @build@
+build_alias = @build_alias@
+build_cpu = @build_cpu@
+build_os = @build_os@
+build_vendor = @build_vendor@
+builddir = @builddir@
+datadir = @datadir@
+datarootdir = @datarootdir@
+dict_drivers = @dict_drivers@
+docdir = @docdir@
+dvidir = @dvidir@
+exec_prefix = @exec_prefix@
+host = @host@
+host_alias = @host_alias@
+host_cpu = @host_cpu@
+host_os = @host_os@
+host_vendor = @host_vendor@
+htmldir = @htmldir@
+includedir = @includedir@
+infodir = @infodir@
+install_sh = @install_sh@
+libdir = @libdir@
+libexecdir = @libexecdir@
+localedir = @localedir@
+localstatedir = @localstatedir@
+mandir = @mandir@
+mkdir_p = @mkdir_p@
+moduledir = @moduledir@
+oldincludedir = @oldincludedir@
+pdfdir = @pdfdir@
+prefix = @prefix@
+program_transform_name = @program_transform_name@
+psdir = @psdir@
+rundir = @rundir@
+runstatedir = @runstatedir@
+sbindir = @sbindir@
+sharedstatedir = @sharedstatedir@
+sql_drivers = @sql_drivers@
+srcdir = @srcdir@
+ssldir = @ssldir@
+statedir = @statedir@
+sysconfdir = @sysconfdir@
+systemdservicetype = @systemdservicetype@
+systemdsystemunitdir = @systemdsystemunitdir@
+target_alias = @target_alias@
+top_build_prefix = @top_build_prefix@
+top_builddir = @top_builddir@
+top_srcdir = @top_srcdir@
+noinst_LTLIBRARIES = libstorage_raw.la
+AM_CPPFLAGS = \
+ -I$(top_srcdir)/src/lib \
+ -I$(top_srcdir)/src/lib-mail \
+ -I$(top_srcdir)/src/lib-imap \
+ -I$(top_srcdir)/src/lib-index \
+ -I$(top_srcdir)/src/lib-storage \
+ -I$(top_srcdir)/src/lib-storage/index
+
+libstorage_raw_la_SOURCES = \
+ raw-mail.c \
+ raw-sync.c \
+ raw-storage.c
+
+headers = \
+ raw-storage.h \
+ raw-sync.h
+
+pkginc_libdir = $(pkgincludedir)
+pkginc_lib_HEADERS = $(headers)
+all: all-am
+
+.SUFFIXES:
+.SUFFIXES: .c .lo .o .obj
+$(srcdir)/Makefile.in: @MAINTAINER_MODE_TRUE@ $(srcdir)/Makefile.am $(am__configure_deps)
+ @for dep in $?; do \
+ case '$(am__configure_deps)' in \
+ *$$dep*) \
+ ( cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh ) \
+ && { if test -f $@; then exit 0; else break; fi; }; \
+ exit 1;; \
+ esac; \
+ done; \
+ echo ' cd $(top_srcdir) && $(AUTOMAKE) --foreign src/lib-storage/index/raw/Makefile'; \
+ $(am__cd) $(top_srcdir) && \
+ $(AUTOMAKE) --foreign src/lib-storage/index/raw/Makefile
+Makefile: $(srcdir)/Makefile.in $(top_builddir)/config.status
+ @case '$?' in \
+ *config.status*) \
+ cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh;; \
+ *) \
+ echo ' cd $(top_builddir) && $(SHELL) ./config.status $(subdir)/$@ $(am__maybe_remake_depfiles)'; \
+ cd $(top_builddir) && $(SHELL) ./config.status $(subdir)/$@ $(am__maybe_remake_depfiles);; \
+ esac;
+
+$(top_builddir)/config.status: $(top_srcdir)/configure $(CONFIG_STATUS_DEPENDENCIES)
+ cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh
+
+$(top_srcdir)/configure: @MAINTAINER_MODE_TRUE@ $(am__configure_deps)
+ cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh
+$(ACLOCAL_M4): @MAINTAINER_MODE_TRUE@ $(am__aclocal_m4_deps)
+ cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh
+$(am__aclocal_m4_deps):
+
+clean-noinstLTLIBRARIES:
+ -test -z "$(noinst_LTLIBRARIES)" || rm -f $(noinst_LTLIBRARIES)
+ @list='$(noinst_LTLIBRARIES)'; \
+ locs=`for p in $$list; do echo $$p; done | \
+ sed 's|^[^/]*$$|.|; s|/[^/]*$$||; s|$$|/so_locations|' | \
+ sort -u`; \
+ test -z "$$locs" || { \
+ echo rm -f $${locs}; \
+ rm -f $${locs}; \
+ }
+
+libstorage_raw.la: $(libstorage_raw_la_OBJECTS) $(libstorage_raw_la_DEPENDENCIES) $(EXTRA_libstorage_raw_la_DEPENDENCIES)
+ $(AM_V_CCLD)$(LINK) $(libstorage_raw_la_OBJECTS) $(libstorage_raw_la_LIBADD) $(LIBS)
+
+mostlyclean-compile:
+ -rm -f *.$(OBJEXT)
+
+distclean-compile:
+ -rm -f *.tab.c
+
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/raw-mail.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/raw-storage.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/raw-sync.Plo@am__quote@ # am--include-marker
+
+$(am__depfiles_remade):
+ @$(MKDIR_P) $(@D)
+ @echo '# dummy' >$@-t && $(am__mv) $@-t $@
+
+am--depfiles: $(am__depfiles_remade)
+
+.c.o:
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(COMPILE) -MT $@ -MD -MP -MF $(DEPDIR)/$*.Tpo -c -o $@ $<
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/$*.Tpo $(DEPDIR)/$*.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='$<' object='$@' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(COMPILE) -c -o $@ $<
+
+.c.obj:
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(COMPILE) -MT $@ -MD -MP -MF $(DEPDIR)/$*.Tpo -c -o $@ `$(CYGPATH_W) '$<'`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/$*.Tpo $(DEPDIR)/$*.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='$<' object='$@' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(COMPILE) -c -o $@ `$(CYGPATH_W) '$<'`
+
+.c.lo:
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(LTCOMPILE) -MT $@ -MD -MP -MF $(DEPDIR)/$*.Tpo -c -o $@ $<
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/$*.Tpo $(DEPDIR)/$*.Plo
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='$<' object='$@' libtool=yes @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(LTCOMPILE) -c -o $@ $<
+
+mostlyclean-libtool:
+ -rm -f *.lo
+
+clean-libtool:
+ -rm -rf .libs _libs
+install-pkginc_libHEADERS: $(pkginc_lib_HEADERS)
+ @$(NORMAL_INSTALL)
+ @list='$(pkginc_lib_HEADERS)'; test -n "$(pkginc_libdir)" || list=; \
+ if test -n "$$list"; then \
+ echo " $(MKDIR_P) '$(DESTDIR)$(pkginc_libdir)'"; \
+ $(MKDIR_P) "$(DESTDIR)$(pkginc_libdir)" || exit 1; \
+ fi; \
+ for p in $$list; do \
+ if test -f "$$p"; then d=; else d="$(srcdir)/"; fi; \
+ echo "$$d$$p"; \
+ done | $(am__base_list) | \
+ while read files; do \
+ echo " $(INSTALL_HEADER) $$files '$(DESTDIR)$(pkginc_libdir)'"; \
+ $(INSTALL_HEADER) $$files "$(DESTDIR)$(pkginc_libdir)" || exit $$?; \
+ done
+
+uninstall-pkginc_libHEADERS:
+ @$(NORMAL_UNINSTALL)
+ @list='$(pkginc_lib_HEADERS)'; test -n "$(pkginc_libdir)" || list=; \
+ files=`for p in $$list; do echo $$p; done | sed -e 's|^.*/||'`; \
+ dir='$(DESTDIR)$(pkginc_libdir)'; $(am__uninstall_files_from_dir)
+
+ID: $(am__tagged_files)
+ $(am__define_uniq_tagged_files); mkid -fID $$unique
+tags: tags-am
+TAGS: tags
+
+tags-am: $(TAGS_DEPENDENCIES) $(am__tagged_files)
+ set x; \
+ here=`pwd`; \
+ $(am__define_uniq_tagged_files); \
+ shift; \
+ if test -z "$(ETAGS_ARGS)$$*$$unique"; then :; else \
+ test -n "$$unique" || unique=$$empty_fix; \
+ if test $$# -gt 0; then \
+ $(ETAGS) $(ETAGSFLAGS) $(AM_ETAGSFLAGS) $(ETAGS_ARGS) \
+ "$$@" $$unique; \
+ else \
+ $(ETAGS) $(ETAGSFLAGS) $(AM_ETAGSFLAGS) $(ETAGS_ARGS) \
+ $$unique; \
+ fi; \
+ fi
+ctags: ctags-am
+
+CTAGS: ctags
+ctags-am: $(TAGS_DEPENDENCIES) $(am__tagged_files)
+ $(am__define_uniq_tagged_files); \
+ test -z "$(CTAGS_ARGS)$$unique" \
+ || $(CTAGS) $(CTAGSFLAGS) $(AM_CTAGSFLAGS) $(CTAGS_ARGS) \
+ $$unique
+
+GTAGS:
+ here=`$(am__cd) $(top_builddir) && pwd` \
+ && $(am__cd) $(top_srcdir) \
+ && gtags -i $(GTAGS_ARGS) "$$here"
+cscopelist: cscopelist-am
+
+cscopelist-am: $(am__tagged_files)
+ list='$(am__tagged_files)'; \
+ case "$(srcdir)" in \
+ [\\/]* | ?:[\\/]*) sdir="$(srcdir)" ;; \
+ *) sdir=$(subdir)/$(srcdir) ;; \
+ esac; \
+ for i in $$list; do \
+ if test -f "$$i"; then \
+ echo "$(subdir)/$$i"; \
+ else \
+ echo "$$sdir/$$i"; \
+ fi; \
+ done >> $(top_builddir)/cscope.files
+
+distclean-tags:
+ -rm -f TAGS ID GTAGS GRTAGS GSYMS GPATH tags
+
+distdir: $(BUILT_SOURCES)
+ $(MAKE) $(AM_MAKEFLAGS) distdir-am
+
+distdir-am: $(DISTFILES)
+ @srcdirstrip=`echo "$(srcdir)" | sed 's/[].[^$$\\*]/\\\\&/g'`; \
+ topsrcdirstrip=`echo "$(top_srcdir)" | sed 's/[].[^$$\\*]/\\\\&/g'`; \
+ list='$(DISTFILES)'; \
+ dist_files=`for file in $$list; do echo $$file; done | \
+ sed -e "s|^$$srcdirstrip/||;t" \
+ -e "s|^$$topsrcdirstrip/|$(top_builddir)/|;t"`; \
+ case $$dist_files in \
+ */*) $(MKDIR_P) `echo "$$dist_files" | \
+ sed '/\//!d;s|^|$(distdir)/|;s,/[^/]*$$,,' | \
+ sort -u` ;; \
+ esac; \
+ for file in $$dist_files; do \
+ if test -f $$file || test -d $$file; then d=.; else d=$(srcdir); fi; \
+ if test -d $$d/$$file; then \
+ dir=`echo "/$$file" | sed -e 's,/[^/]*$$,,'`; \
+ if test -d "$(distdir)/$$file"; then \
+ find "$(distdir)/$$file" -type d ! -perm -700 -exec chmod u+rwx {} \;; \
+ fi; \
+ if test -d $(srcdir)/$$file && test $$d != $(srcdir); then \
+ cp -fpR $(srcdir)/$$file "$(distdir)$$dir" || exit 1; \
+ find "$(distdir)/$$file" -type d ! -perm -700 -exec chmod u+rwx {} \;; \
+ fi; \
+ cp -fpR $$d/$$file "$(distdir)$$dir" || exit 1; \
+ else \
+ test -f "$(distdir)/$$file" \
+ || cp -p $$d/$$file "$(distdir)/$$file" \
+ || exit 1; \
+ fi; \
+ done
+check-am: all-am
+check: check-am
+all-am: Makefile $(LTLIBRARIES) $(HEADERS)
+installdirs:
+ for dir in "$(DESTDIR)$(pkginc_libdir)"; do \
+ test -z "$$dir" || $(MKDIR_P) "$$dir"; \
+ done
+install: install-am
+install-exec: install-exec-am
+install-data: install-data-am
+uninstall: uninstall-am
+
+install-am: all-am
+ @$(MAKE) $(AM_MAKEFLAGS) install-exec-am install-data-am
+
+installcheck: installcheck-am
+install-strip:
+ if test -z '$(STRIP)'; then \
+ $(MAKE) $(AM_MAKEFLAGS) INSTALL_PROGRAM="$(INSTALL_STRIP_PROGRAM)" \
+ install_sh_PROGRAM="$(INSTALL_STRIP_PROGRAM)" INSTALL_STRIP_FLAG=-s \
+ install; \
+ else \
+ $(MAKE) $(AM_MAKEFLAGS) INSTALL_PROGRAM="$(INSTALL_STRIP_PROGRAM)" \
+ install_sh_PROGRAM="$(INSTALL_STRIP_PROGRAM)" INSTALL_STRIP_FLAG=-s \
+ "INSTALL_PROGRAM_ENV=STRIPPROG='$(STRIP)'" install; \
+ fi
+mostlyclean-generic:
+
+clean-generic:
+
+distclean-generic:
+ -test -z "$(CONFIG_CLEAN_FILES)" || rm -f $(CONFIG_CLEAN_FILES)
+ -test . = "$(srcdir)" || test -z "$(CONFIG_CLEAN_VPATH_FILES)" || rm -f $(CONFIG_CLEAN_VPATH_FILES)
+
+maintainer-clean-generic:
+ @echo "This command is intended for maintainers to use"
+ @echo "it deletes files that may require special tools to rebuild."
+clean: clean-am
+
+clean-am: clean-generic clean-libtool clean-noinstLTLIBRARIES \
+ mostlyclean-am
+
+distclean: distclean-am
+ -rm -f ./$(DEPDIR)/raw-mail.Plo
+ -rm -f ./$(DEPDIR)/raw-storage.Plo
+ -rm -f ./$(DEPDIR)/raw-sync.Plo
+ -rm -f Makefile
+distclean-am: clean-am distclean-compile distclean-generic \
+ distclean-tags
+
+dvi: dvi-am
+
+dvi-am:
+
+html: html-am
+
+html-am:
+
+info: info-am
+
+info-am:
+
+install-data-am: install-pkginc_libHEADERS
+
+install-dvi: install-dvi-am
+
+install-dvi-am:
+
+install-exec-am:
+
+install-html: install-html-am
+
+install-html-am:
+
+install-info: install-info-am
+
+install-info-am:
+
+install-man:
+
+install-pdf: install-pdf-am
+
+install-pdf-am:
+
+install-ps: install-ps-am
+
+install-ps-am:
+
+installcheck-am:
+
+maintainer-clean: maintainer-clean-am
+ -rm -f ./$(DEPDIR)/raw-mail.Plo
+ -rm -f ./$(DEPDIR)/raw-storage.Plo
+ -rm -f ./$(DEPDIR)/raw-sync.Plo
+ -rm -f Makefile
+maintainer-clean-am: distclean-am maintainer-clean-generic
+
+mostlyclean: mostlyclean-am
+
+mostlyclean-am: mostlyclean-compile mostlyclean-generic \
+ mostlyclean-libtool
+
+pdf: pdf-am
+
+pdf-am:
+
+ps: ps-am
+
+ps-am:
+
+uninstall-am: uninstall-pkginc_libHEADERS
+
+.MAKE: install-am install-strip
+
+.PHONY: CTAGS GTAGS TAGS all all-am am--depfiles check check-am clean \
+ clean-generic clean-libtool clean-noinstLTLIBRARIES \
+ cscopelist-am ctags ctags-am distclean distclean-compile \
+ distclean-generic distclean-libtool distclean-tags distdir dvi \
+ dvi-am html html-am info info-am install install-am \
+ install-data install-data-am install-dvi install-dvi-am \
+ install-exec install-exec-am install-html install-html-am \
+ install-info install-info-am install-man install-pdf \
+ install-pdf-am install-pkginc_libHEADERS install-ps \
+ install-ps-am install-strip installcheck installcheck-am \
+ installdirs maintainer-clean maintainer-clean-generic \
+ mostlyclean mostlyclean-compile mostlyclean-generic \
+ mostlyclean-libtool pdf pdf-am ps ps-am tags tags-am uninstall \
+ uninstall-am uninstall-pkginc_libHEADERS
+
+.PRECIOUS: Makefile
+
+
+# Tell versions [3.59,3.63) of GNU make to not export all variables.
+# Otherwise a system limit (for SysV at least) may be exceeded.
+.NOEXPORT:
diff --git a/src/lib-storage/index/raw/raw-mail.c b/src/lib-storage/index/raw/raw-mail.c
new file mode 100644
index 0000000..72d3688
--- /dev/null
+++ b/src/lib-storage/index/raw/raw-mail.c
@@ -0,0 +1,152 @@
+/* Copyright (c) 2007-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "istream.h"
+#include "index-mail.h"
+#include "raw-storage.h"
+
+#include <fcntl.h>
+#include <unistd.h>
+#include <sys/stat.h>
+
+static int raw_mail_stat(struct mail *mail)
+{
+ struct raw_mailbox *mbox = RAW_MAILBOX(mail->box);
+ const struct stat *st;
+
+ if (!mail_metadata_access_start(mail))
+ return -1;
+
+ mail->transaction->stats.fstat_lookup_count++;
+ if (i_stream_stat(mail->box->input, TRUE, &st) < 0) {
+ mail_set_critical(mail, "stat(%s) failed: %m",
+ i_stream_get_name(mail->box->input));
+ return -1;
+ }
+
+ if (mbox->mtime != (time_t)-1)
+ mbox->mtime = st->st_mtime;
+ if (mbox->ctime != (time_t)-1)
+ mbox->ctime = st->st_ctime;
+ mbox->size = (size_t)st->st_size;
+ return 0;
+}
+
+static int raw_mail_get_received_date(struct mail *_mail, time_t *date_r)
+{
+ struct index_mail *mail = INDEX_MAIL(_mail);
+ struct raw_mailbox *mbox = RAW_MAILBOX(_mail->box);
+
+ if (mbox->mtime == (time_t)-1) {
+ if (raw_mail_stat(_mail) < 0)
+ return -1;
+ }
+
+ *date_r = mail->data.received_date = mbox->mtime;
+ return 0;
+}
+
+static int raw_mail_get_save_date(struct mail *_mail, time_t *date_r)
+{
+ struct index_mail *mail = INDEX_MAIL(_mail);
+ struct raw_mailbox *mbox = RAW_MAILBOX(_mail->box);
+
+ if (mbox->ctime == (time_t)-1) {
+ if (raw_mail_stat(_mail) < 0)
+ return -1;
+ }
+
+ *date_r = mail->data.save_date = mbox->ctime;
+ return 1;
+}
+
+static int raw_mail_get_physical_size(struct mail *_mail, uoff_t *size_r)
+{
+ struct index_mail *mail = INDEX_MAIL(_mail);
+ struct raw_mailbox *mbox = RAW_MAILBOX(_mail->box);
+
+ if (mbox->size == UOFF_T_MAX) {
+ if (raw_mail_stat(_mail) < 0)
+ return -1;
+ }
+
+ *size_r = mail->data.physical_size = mbox->size;
+ return 0;
+}
+
+static int
+raw_mail_get_stream(struct mail *_mail, bool get_body ATTR_UNUSED,
+ struct message_size *hdr_size,
+ struct message_size *body_size, struct istream **stream_r)
+{
+ struct index_mail *mail = INDEX_MAIL(_mail);
+
+ if (mail->data.stream == NULL) {
+ if (!mail_stream_access_start(_mail))
+ return -1;
+ /* we can't just reference mbox->input, because
+ index_mail_close() expects to be able to free the stream */
+ mail->data.stream =
+ i_stream_create_limit(_mail->box->input, UOFF_T_MAX);
+ }
+
+ return index_mail_init_stream(mail, hdr_size, body_size, stream_r);
+}
+
+static int
+raw_mail_get_special(struct mail *_mail, enum mail_fetch_field field,
+ const char **value_r)
+{
+ struct raw_mailbox *mbox = RAW_MAILBOX(_mail->box);
+
+ switch (field) {
+ case MAIL_FETCH_FROM_ENVELOPE:
+ *value_r = mbox->envelope_sender != NULL ?
+ mbox->envelope_sender : "";
+ return 0;
+ case MAIL_FETCH_STORAGE_ID:
+ *value_r = mbox->have_filename ?
+ mailbox_get_path(_mail->box) : "";
+ return 0;
+ default:
+ return index_mail_get_special(_mail, field, value_r);
+ }
+}
+
+struct mail_vfuncs raw_mail_vfuncs = {
+ index_mail_close,
+ index_mail_free,
+ index_mail_set_seq,
+ index_mail_set_uid,
+ index_mail_set_uid_cache_updates,
+ index_mail_prefetch,
+ index_mail_precache,
+ index_mail_add_temp_wanted_fields,
+
+ index_mail_get_flags,
+ index_mail_get_keywords,
+ index_mail_get_keyword_indexes,
+ index_mail_get_modseq,
+ index_mail_get_pvt_modseq,
+ index_mail_get_parts,
+ index_mail_get_date,
+ raw_mail_get_received_date,
+ raw_mail_get_save_date,
+ index_mail_get_virtual_size,
+ raw_mail_get_physical_size,
+ index_mail_get_first_header,
+ index_mail_get_headers,
+ index_mail_get_header_stream,
+ raw_mail_get_stream,
+ index_mail_get_binary_stream,
+ raw_mail_get_special,
+ index_mail_get_backend_mail,
+ index_mail_update_flags,
+ index_mail_update_keywords,
+ index_mail_update_modseq,
+ index_mail_update_pvt_modseq,
+ NULL,
+ index_mail_expunge,
+ index_mail_set_cache_corrupted,
+ index_mail_opened,
+};
diff --git a/src/lib-storage/index/raw/raw-storage.c b/src/lib-storage/index/raw/raw-storage.c
new file mode 100644
index 0000000..e94ae10
--- /dev/null
+++ b/src/lib-storage/index/raw/raw-storage.c
@@ -0,0 +1,269 @@
+/* Copyright (c) 2007-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "ioloop.h"
+#include "istream.h"
+#include "index-mail.h"
+#include "mail-copy.h"
+#include "mailbox-list-private.h"
+#include "raw-sync.h"
+#include "raw-storage.h"
+
+extern struct mail_storage raw_storage;
+extern struct mailbox raw_mailbox;
+
+struct mail_user *
+raw_storage_create_from_set(const struct setting_parser_info *set_info,
+ const struct mail_user_settings *set)
+{
+ struct mail_user *user;
+ struct mail_namespace *ns;
+ struct mail_namespace_settings *ns_set;
+ struct mail_storage_settings *mail_set;
+ const char *error;
+
+ user = mail_user_alloc(NULL, "raw mail user", set_info, set);
+ user->autocreated = TRUE;
+ mail_user_set_home(user, "/");
+ if (mail_user_init(user, &error) < 0)
+ i_fatal("Raw user initialization failed: %s", error);
+
+ ns_set = p_new(user->pool, struct mail_namespace_settings, 1);
+ ns_set->name = "raw-storage";
+ ns_set->location = ":LAYOUT=none";
+ ns_set->separator = "/";
+
+ ns = mail_namespaces_init_empty(user);
+ /* raw storage doesn't have INBOX. We especially don't want LIST to
+ return INBOX. */
+ ns->flags &= ENUM_NEGATE(NAMESPACE_FLAG_INBOX_USER);
+ ns->flags |= NAMESPACE_FLAG_NOQUOTA | NAMESPACE_FLAG_NOACL;
+ ns->set = ns_set;
+ /* absolute paths are ok with raw storage */
+ mail_set = p_new(user->pool, struct mail_storage_settings, 1);
+ *mail_set = *ns->mail_set;
+ mail_set->mail_full_filesystem_access = TRUE;
+ ns->mail_set = mail_set;
+
+ if (mail_storage_create(ns, "raw", 0, &error) < 0)
+ i_fatal("Couldn't create internal raw storage: %s", error);
+ if (mail_namespaces_init_finish(ns, &error) < 0)
+ i_fatal("Couldn't create internal raw namespace: %s", error);
+ return user;
+}
+
+static int ATTR_NULL(2, 3)
+raw_mailbox_alloc_common(struct mail_user *user, struct istream *input,
+ const char *path, time_t received_time,
+ const char *envelope_sender, struct mailbox **box_r)
+{
+ struct mail_namespace *ns = user->namespaces;
+ struct mailbox *box;
+ struct raw_mailbox *raw_box;
+ const char *name;
+
+ name = path != NULL ? path : i_stream_get_name(input);
+ box = *box_r = mailbox_alloc(ns->list, name,
+ MAILBOX_FLAG_NO_INDEX_FILES);
+ if (input != NULL) {
+ if (mailbox_open_stream(box, input) < 0)
+ return -1;
+ } else {
+ if (mailbox_open(box) < 0)
+ return -1;
+ }
+ if (mailbox_sync(box, 0) < 0)
+ return -1;
+
+ i_assert(strcmp(box->storage->name, RAW_STORAGE_NAME) == 0);
+ raw_box = RAW_MAILBOX(box);
+ raw_box->envelope_sender = p_strdup(box->pool, envelope_sender);
+ raw_box->mtime = received_time;
+ return 0;
+}
+
+int raw_mailbox_alloc_stream(struct mail_user *user, struct istream *input,
+ time_t received_time, const char *envelope_sender,
+ struct mailbox **box_r)
+{
+ return raw_mailbox_alloc_common(user, input, NULL, received_time,
+ envelope_sender, box_r);
+}
+
+int raw_mailbox_alloc_path(struct mail_user *user, const char *path,
+ time_t received_time, const char *envelope_sender,
+ struct mailbox **box_r)
+{
+ return raw_mailbox_alloc_common(user, NULL, path, received_time,
+ envelope_sender, box_r);
+}
+
+static struct mail_storage *raw_storage_alloc(void)
+{
+ struct raw_storage *storage;
+ pool_t pool;
+
+ pool = pool_alloconly_create("raw storage", 512+256);
+ storage = p_new(pool, struct raw_storage, 1);
+ storage->storage = raw_storage;
+ storage->storage.pool = pool;
+ return &storage->storage;
+}
+
+static void
+raw_storage_get_list_settings(const struct mail_namespace *ns ATTR_UNUSED,
+ struct mailbox_list_settings *set)
+{
+ if (set->layout == NULL)
+ set->layout = MAILBOX_LIST_NAME_FS;
+ if (set->subscription_fname == NULL)
+ set->subscription_fname = RAW_SUBSCRIPTION_FILE_NAME;
+}
+
+static struct mailbox *
+raw_mailbox_alloc(struct mail_storage *storage, struct mailbox_list *list,
+ const char *vname, enum mailbox_flags flags)
+{
+ struct raw_mailbox *mbox;
+ pool_t pool;
+
+ flags |= MAILBOX_FLAG_READONLY | MAILBOX_FLAG_NO_INDEX_FILES;
+
+ pool = pool_alloconly_create("raw mailbox", 1024*3);
+ mbox = p_new(pool, struct raw_mailbox, 1);
+ mbox->box = raw_mailbox;
+ mbox->box.pool = pool;
+ mbox->box.storage = storage;
+ mbox->box.list = list;
+ mbox->box.mail_vfuncs = &raw_mail_vfuncs;
+
+ index_storage_mailbox_alloc(&mbox->box, vname, flags, "dovecot.index");
+
+ mbox->mtime = mbox->ctime = (time_t)-1;
+ mbox->storage = RAW_STORAGE(storage);
+ mbox->size = UOFF_T_MAX;
+ return &mbox->box;
+}
+
+static int raw_mailbox_open(struct mailbox *box)
+{
+ struct raw_mailbox *mbox = RAW_MAILBOX(box);
+ const char *path;
+ int fd;
+
+ if (box->input != NULL) {
+ mbox->mtime = mbox->ctime = ioloop_time;
+ return index_storage_mailbox_open(box, FALSE);
+ }
+
+ path = box->_path = box->name;
+ mbox->have_filename = TRUE;
+ fd = open(path, O_RDONLY);
+ if (fd == -1) {
+ if (ENOTFOUND(errno)) {
+ mail_storage_set_error(box->storage,
+ MAIL_ERROR_NOTFOUND,
+ T_MAIL_ERR_MAILBOX_NOT_FOUND(box->vname));
+ } else if (!mail_storage_set_error_from_errno(box->storage)) {
+ mailbox_set_critical(box, "open(%s) failed: %m", path);
+ }
+ return -1;
+ }
+ box->input = i_stream_create_fd_autoclose(&fd, MAIL_READ_FULL_BLOCK_SIZE);
+ i_stream_set_name(box->input, path);
+ i_stream_set_init_buffer_size(box->input, MAIL_READ_FULL_BLOCK_SIZE);
+ return index_storage_mailbox_open(box, FALSE);
+}
+
+static int
+raw_mailbox_create(struct mailbox *box,
+ const struct mailbox_update *update ATTR_UNUSED,
+ bool directory ATTR_UNUSED)
+{
+ mail_storage_set_error(box->storage, MAIL_ERROR_NOTPOSSIBLE,
+ "Raw mailbox creation isn't supported");
+ return -1;
+}
+
+static int
+raw_mailbox_update(struct mailbox *box,
+ const struct mailbox_update *update ATTR_UNUSED)
+{
+ mail_storage_set_error(box->storage, MAIL_ERROR_NOTPOSSIBLE,
+ "Raw mailbox update isn't supported");
+ return -1;
+}
+
+static void raw_notify_changes(struct mailbox *box ATTR_UNUSED)
+{
+}
+
+struct mail_storage raw_storage = {
+ .name = RAW_STORAGE_NAME,
+ .class_flags = MAIL_STORAGE_CLASS_FLAG_MAILBOX_IS_FILE |
+ MAIL_STORAGE_CLASS_FLAG_OPEN_STREAMS |
+ MAIL_STORAGE_CLASS_FLAG_BINARY_DATA,
+
+ .v = {
+ NULL,
+ raw_storage_alloc,
+ NULL,
+ index_storage_destroy,
+ NULL,
+ raw_storage_get_list_settings,
+ NULL,
+ raw_mailbox_alloc,
+ NULL,
+ NULL,
+ }
+};
+
+struct mailbox raw_mailbox = {
+ .v = {
+ index_storage_is_readonly,
+ index_storage_mailbox_enable,
+ index_storage_mailbox_exists,
+ raw_mailbox_open,
+ index_storage_mailbox_close,
+ index_storage_mailbox_free,
+ raw_mailbox_create,
+ raw_mailbox_update,
+ index_storage_mailbox_delete,
+ index_storage_mailbox_rename,
+ index_storage_get_status,
+ index_mailbox_get_metadata,
+ index_storage_set_subscribed,
+ index_storage_attribute_set,
+ index_storage_attribute_get,
+ index_storage_attribute_iter_init,
+ index_storage_attribute_iter_next,
+ index_storage_attribute_iter_deinit,
+ index_storage_list_index_has_changed,
+ index_storage_list_index_update_sync,
+ raw_storage_sync_init,
+ index_mailbox_sync_next,
+ index_mailbox_sync_deinit,
+ NULL,
+ raw_notify_changes,
+ index_transaction_begin,
+ index_transaction_commit,
+ index_transaction_rollback,
+ NULL,
+ index_mail_alloc,
+ index_storage_search_init,
+ index_storage_search_deinit,
+ index_storage_search_next_nonblock,
+ index_storage_search_next_update_seq,
+ index_storage_search_next_match_mail,
+ NULL,
+ NULL,
+ NULL,
+ NULL,
+ NULL,
+ mail_storage_copy,
+ NULL,
+ NULL,
+ NULL,
+ index_storage_is_inconsistent
+ }
+};
diff --git a/src/lib-storage/index/raw/raw-storage.h b/src/lib-storage/index/raw/raw-storage.h
new file mode 100644
index 0000000..124ca7e
--- /dev/null
+++ b/src/lib-storage/index/raw/raw-storage.h
@@ -0,0 +1,41 @@
+#ifndef RAW_STORAGE_H
+#define RAW_STORAGE_H
+
+#include "index-storage.h"
+
+#define RAW_STORAGE_NAME "raw"
+#define RAW_SUBSCRIPTION_FILE_NAME "subscriptions"
+
+struct raw_storage {
+ struct mail_storage storage;
+};
+
+struct raw_mailbox {
+ struct mailbox box;
+ struct raw_storage *storage;
+
+ time_t mtime, ctime;
+ uoff_t size;
+ const char *envelope_sender;
+
+ bool synced:1;
+ bool have_filename:1;
+};
+
+#define RAW_STORAGE(s) container_of(s, struct raw_storage, storage)
+#define RAW_MAILBOX(s) container_of(s, struct raw_mailbox, box)
+
+extern struct mail_vfuncs raw_mail_vfuncs;
+
+struct mail_user *
+raw_storage_create_from_set(const struct setting_parser_info *set_info,
+ const struct mail_user_settings *set);
+
+int raw_mailbox_alloc_stream(struct mail_user *user, struct istream *input,
+ time_t received_time, const char *envelope_sender,
+ struct mailbox **box_r);
+int raw_mailbox_alloc_path(struct mail_user *user, const char *path,
+ time_t received_time, const char *envelope_sender,
+ struct mailbox **box_r);
+
+#endif
diff --git a/src/lib-storage/index/raw/raw-sync.c b/src/lib-storage/index/raw/raw-sync.c
new file mode 100644
index 0000000..6511f72
--- /dev/null
+++ b/src/lib-storage/index/raw/raw-sync.c
@@ -0,0 +1,67 @@
+/* Copyright (c) 2007-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "ioloop.h"
+#include "raw-storage.h"
+#include "raw-sync.h"
+#include "mailbox-recent-flags.h"
+
+static int raw_sync(struct raw_mailbox *mbox)
+{
+ struct mail_index_sync_ctx *index_sync_ctx;
+ struct mail_index_view *sync_view;
+ struct mail_index_sync_rec sync_rec;
+ struct mail_index_transaction *trans;
+ uint32_t seq, uid_validity = ioloop_time;
+ enum mail_index_sync_flags sync_flags;
+ int ret;
+
+ i_assert(!mbox->synced);
+
+ sync_flags = index_storage_get_sync_flags(&mbox->box) |
+ MAIL_INDEX_SYNC_FLAG_FLUSH_DIRTY |
+ MAIL_INDEX_SYNC_FLAG_REQUIRE_CHANGES;
+
+ if (mail_index_view_get_messages_count(mbox->box.view) > 0) {
+ /* already-synced index was opened via
+ mail-index-alloc-cache. */
+ return 0;
+ }
+
+ ret = mail_index_sync_begin(mbox->box.index, &index_sync_ctx,
+ &sync_view, &trans, sync_flags);
+ if (ret <= 0) {
+ if (ret < 0)
+ mailbox_set_index_error(&mbox->box);
+ return ret;
+ }
+
+ /* set our uidvalidity */
+ mail_index_update_header(trans,
+ offsetof(struct mail_index_header, uid_validity),
+ &uid_validity, sizeof(uid_validity), TRUE);
+
+ /* add our one and only message */
+ mail_index_append(trans, 1, &seq);
+ mailbox_recent_flags_set_uid(&mbox->box, 1);
+
+ while (mail_index_sync_next(index_sync_ctx, &sync_rec)) ;
+ if (mail_index_sync_commit(&index_sync_ctx) < 0) {
+ mailbox_set_index_error(&mbox->box);
+ return -1;
+ }
+ mbox->synced = TRUE;
+ return 0;
+}
+
+struct mailbox_sync_context *
+raw_storage_sync_init(struct mailbox *box, enum mailbox_sync_flags flags)
+{
+ struct raw_mailbox *mbox = RAW_MAILBOX(box);
+ int ret = 0;
+
+ if (!mbox->synced)
+ ret = raw_sync(mbox);
+
+ return index_mailbox_sync_init(box, flags, ret < 0);
+}
diff --git a/src/lib-storage/index/raw/raw-sync.h b/src/lib-storage/index/raw/raw-sync.h
new file mode 100644
index 0000000..2c29c1f
--- /dev/null
+++ b/src/lib-storage/index/raw/raw-sync.h
@@ -0,0 +1,9 @@
+#ifndef RAW_SYNC_H
+#define RAW_SYNC_H
+
+struct mailbox;
+
+struct mailbox_sync_context *
+raw_storage_sync_init(struct mailbox *box, enum mailbox_sync_flags flags);
+
+#endif
diff --git a/src/lib-storage/index/shared/Makefile.am b/src/lib-storage/index/shared/Makefile.am
new file mode 100644
index 0000000..07980d6
--- /dev/null
+++ b/src/lib-storage/index/shared/Makefile.am
@@ -0,0 +1,19 @@
+noinst_LTLIBRARIES = libstorage_shared.la
+
+AM_CPPFLAGS = \
+ -I$(top_srcdir)/src/lib \
+ -I$(top_srcdir)/src/lib-mail \
+ -I$(top_srcdir)/src/lib-imap \
+ -I$(top_srcdir)/src/lib-index \
+ -I$(top_srcdir)/src/lib-storage \
+ -I$(top_srcdir)/src/lib-storage/index
+
+libstorage_shared_la_SOURCES = \
+ shared-list.c \
+ shared-storage.c
+
+headers = \
+ shared-storage.h
+
+pkginc_libdir=$(pkgincludedir)
+pkginc_lib_HEADERS = $(headers)
diff --git a/src/lib-storage/index/shared/Makefile.in b/src/lib-storage/index/shared/Makefile.in
new file mode 100644
index 0000000..3c7d4e1
--- /dev/null
+++ b/src/lib-storage/index/shared/Makefile.in
@@ -0,0 +1,817 @@
+# Makefile.in generated by automake 1.16.1 from Makefile.am.
+# @configure_input@
+
+# Copyright (C) 1994-2018 Free Software Foundation, Inc.
+
+# This Makefile.in is free software; the Free Software Foundation
+# gives unlimited permission to copy and/or distribute it,
+# with or without modifications, as long as this notice is preserved.
+
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY, to the extent permitted by law; without
+# even the implied warranty of MERCHANTABILITY or FITNESS FOR A
+# PARTICULAR PURPOSE.
+
+@SET_MAKE@
+
+
+VPATH = @srcdir@
+am__is_gnu_make = { \
+ if test -z '$(MAKELEVEL)'; then \
+ false; \
+ elif test -n '$(MAKE_HOST)'; then \
+ true; \
+ elif test -n '$(MAKE_VERSION)' && test -n '$(CURDIR)'; then \
+ true; \
+ else \
+ false; \
+ fi; \
+}
+am__make_running_with_option = \
+ case $${target_option-} in \
+ ?) ;; \
+ *) echo "am__make_running_with_option: internal error: invalid" \
+ "target option '$${target_option-}' specified" >&2; \
+ exit 1;; \
+ esac; \
+ has_opt=no; \
+ sane_makeflags=$$MAKEFLAGS; \
+ if $(am__is_gnu_make); then \
+ sane_makeflags=$$MFLAGS; \
+ else \
+ case $$MAKEFLAGS in \
+ *\\[\ \ ]*) \
+ bs=\\; \
+ sane_makeflags=`printf '%s\n' "$$MAKEFLAGS" \
+ | sed "s/$$bs$$bs[$$bs $$bs ]*//g"`;; \
+ esac; \
+ fi; \
+ skip_next=no; \
+ strip_trailopt () \
+ { \
+ flg=`printf '%s\n' "$$flg" | sed "s/$$1.*$$//"`; \
+ }; \
+ for flg in $$sane_makeflags; do \
+ test $$skip_next = yes && { skip_next=no; continue; }; \
+ case $$flg in \
+ *=*|--*) continue;; \
+ -*I) strip_trailopt 'I'; skip_next=yes;; \
+ -*I?*) strip_trailopt 'I';; \
+ -*O) strip_trailopt 'O'; skip_next=yes;; \
+ -*O?*) strip_trailopt 'O';; \
+ -*l) strip_trailopt 'l'; skip_next=yes;; \
+ -*l?*) strip_trailopt 'l';; \
+ -[dEDm]) skip_next=yes;; \
+ -[JT]) skip_next=yes;; \
+ esac; \
+ case $$flg in \
+ *$$target_option*) has_opt=yes; break;; \
+ esac; \
+ done; \
+ test $$has_opt = yes
+am__make_dryrun = (target_option=n; $(am__make_running_with_option))
+am__make_keepgoing = (target_option=k; $(am__make_running_with_option))
+pkgdatadir = $(datadir)/@PACKAGE@
+pkgincludedir = $(includedir)/@PACKAGE@
+pkglibdir = $(libdir)/@PACKAGE@
+pkglibexecdir = $(libexecdir)/@PACKAGE@
+am__cd = CDPATH="$${ZSH_VERSION+.}$(PATH_SEPARATOR)" && cd
+install_sh_DATA = $(install_sh) -c -m 644
+install_sh_PROGRAM = $(install_sh) -c
+install_sh_SCRIPT = $(install_sh) -c
+INSTALL_HEADER = $(INSTALL_DATA)
+transform = $(program_transform_name)
+NORMAL_INSTALL = :
+PRE_INSTALL = :
+POST_INSTALL = :
+NORMAL_UNINSTALL = :
+PRE_UNINSTALL = :
+POST_UNINSTALL = :
+build_triplet = @build@
+host_triplet = @host@
+subdir = src/lib-storage/index/shared
+ACLOCAL_M4 = $(top_srcdir)/aclocal.m4
+am__aclocal_m4_deps = $(top_srcdir)/m4/ac_checktype2.m4 \
+ $(top_srcdir)/m4/ac_typeof.m4 $(top_srcdir)/m4/arc4random.m4 \
+ $(top_srcdir)/m4/blockdev.m4 $(top_srcdir)/m4/c99_vsnprintf.m4 \
+ $(top_srcdir)/m4/clock_gettime.m4 $(top_srcdir)/m4/crypt.m4 \
+ $(top_srcdir)/m4/crypt_xpg6.m4 $(top_srcdir)/m4/dbqlk.m4 \
+ $(top_srcdir)/m4/dirent_dtype.m4 $(top_srcdir)/m4/dovecot.m4 \
+ $(top_srcdir)/m4/fd_passing.m4 $(top_srcdir)/m4/fdatasync.m4 \
+ $(top_srcdir)/m4/flexible_array_member.m4 \
+ $(top_srcdir)/m4/glibc.m4 $(top_srcdir)/m4/gmtime_max.m4 \
+ $(top_srcdir)/m4/gmtime_tm_gmtoff.m4 \
+ $(top_srcdir)/m4/ioloop.m4 $(top_srcdir)/m4/iovec.m4 \
+ $(top_srcdir)/m4/ipv6.m4 $(top_srcdir)/m4/libcap.m4 \
+ $(top_srcdir)/m4/libtool.m4 $(top_srcdir)/m4/libwrap.m4 \
+ $(top_srcdir)/m4/linux_mremap.m4 $(top_srcdir)/m4/ltoptions.m4 \
+ $(top_srcdir)/m4/ltsugar.m4 $(top_srcdir)/m4/ltversion.m4 \
+ $(top_srcdir)/m4/lt~obsolete.m4 $(top_srcdir)/m4/mmap_write.m4 \
+ $(top_srcdir)/m4/mntctl.m4 $(top_srcdir)/m4/modules.m4 \
+ $(top_srcdir)/m4/notify.m4 $(top_srcdir)/m4/nsl.m4 \
+ $(top_srcdir)/m4/off_t_max.m4 $(top_srcdir)/m4/pkg.m4 \
+ $(top_srcdir)/m4/pr_set_dumpable.m4 \
+ $(top_srcdir)/m4/q_quotactl.m4 $(top_srcdir)/m4/quota.m4 \
+ $(top_srcdir)/m4/random.m4 $(top_srcdir)/m4/rlimit.m4 \
+ $(top_srcdir)/m4/sendfile.m4 $(top_srcdir)/m4/size_t_signed.m4 \
+ $(top_srcdir)/m4/sockpeercred.m4 $(top_srcdir)/m4/sql.m4 \
+ $(top_srcdir)/m4/ssl.m4 $(top_srcdir)/m4/st_tim.m4 \
+ $(top_srcdir)/m4/static_array.m4 $(top_srcdir)/m4/test_with.m4 \
+ $(top_srcdir)/m4/time_t.m4 $(top_srcdir)/m4/typeof.m4 \
+ $(top_srcdir)/m4/typeof_dev_t.m4 \
+ $(top_srcdir)/m4/uoff_t_max.m4 $(top_srcdir)/m4/vararg.m4 \
+ $(top_srcdir)/m4/want_apparmor.m4 \
+ $(top_srcdir)/m4/want_bsdauth.m4 \
+ $(top_srcdir)/m4/want_bzlib.m4 \
+ $(top_srcdir)/m4/want_cassandra.m4 \
+ $(top_srcdir)/m4/want_cdb.m4 \
+ $(top_srcdir)/m4/want_checkpassword.m4 \
+ $(top_srcdir)/m4/want_clucene.m4 $(top_srcdir)/m4/want_db.m4 \
+ $(top_srcdir)/m4/want_gssapi.m4 $(top_srcdir)/m4/want_icu.m4 \
+ $(top_srcdir)/m4/want_ldap.m4 $(top_srcdir)/m4/want_lua.m4 \
+ $(top_srcdir)/m4/want_lz4.m4 $(top_srcdir)/m4/want_lzma.m4 \
+ $(top_srcdir)/m4/want_mysql.m4 $(top_srcdir)/m4/want_pam.m4 \
+ $(top_srcdir)/m4/want_passwd.m4 $(top_srcdir)/m4/want_pgsql.m4 \
+ $(top_srcdir)/m4/want_prefetch.m4 \
+ $(top_srcdir)/m4/want_shadow.m4 \
+ $(top_srcdir)/m4/want_sodium.m4 $(top_srcdir)/m4/want_solr.m4 \
+ $(top_srcdir)/m4/want_sqlite.m4 \
+ $(top_srcdir)/m4/want_stemmer.m4 \
+ $(top_srcdir)/m4/want_systemd.m4 \
+ $(top_srcdir)/m4/want_textcat.m4 \
+ $(top_srcdir)/m4/want_unwind.m4 $(top_srcdir)/m4/want_zlib.m4 \
+ $(top_srcdir)/m4/want_zstd.m4 $(top_srcdir)/configure.ac
+am__configure_deps = $(am__aclocal_m4_deps) $(CONFIGURE_DEPENDENCIES) \
+ $(ACLOCAL_M4)
+DIST_COMMON = $(srcdir)/Makefile.am $(pkginc_lib_HEADERS) \
+ $(am__DIST_COMMON)
+mkinstalldirs = $(install_sh) -d
+CONFIG_HEADER = $(top_builddir)/config.h
+CONFIG_CLEAN_FILES =
+CONFIG_CLEAN_VPATH_FILES =
+LTLIBRARIES = $(noinst_LTLIBRARIES)
+libstorage_shared_la_LIBADD =
+am_libstorage_shared_la_OBJECTS = shared-list.lo shared-storage.lo
+libstorage_shared_la_OBJECTS = $(am_libstorage_shared_la_OBJECTS)
+AM_V_lt = $(am__v_lt_@AM_V@)
+am__v_lt_ = $(am__v_lt_@AM_DEFAULT_V@)
+am__v_lt_0 = --silent
+am__v_lt_1 =
+AM_V_P = $(am__v_P_@AM_V@)
+am__v_P_ = $(am__v_P_@AM_DEFAULT_V@)
+am__v_P_0 = false
+am__v_P_1 = :
+AM_V_GEN = $(am__v_GEN_@AM_V@)
+am__v_GEN_ = $(am__v_GEN_@AM_DEFAULT_V@)
+am__v_GEN_0 = @echo " GEN " $@;
+am__v_GEN_1 =
+AM_V_at = $(am__v_at_@AM_V@)
+am__v_at_ = $(am__v_at_@AM_DEFAULT_V@)
+am__v_at_0 = @
+am__v_at_1 =
+DEFAULT_INCLUDES = -I.@am__isrc@ -I$(top_builddir)
+depcomp = $(SHELL) $(top_srcdir)/depcomp
+am__maybe_remake_depfiles = depfiles
+am__depfiles_remade = ./$(DEPDIR)/shared-list.Plo \
+ ./$(DEPDIR)/shared-storage.Plo
+am__mv = mv -f
+COMPILE = $(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) \
+ $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS)
+LTCOMPILE = $(LIBTOOL) $(AM_V_lt) --tag=CC $(AM_LIBTOOLFLAGS) \
+ $(LIBTOOLFLAGS) --mode=compile $(CC) $(DEFS) \
+ $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) $(CPPFLAGS) \
+ $(AM_CFLAGS) $(CFLAGS)
+AM_V_CC = $(am__v_CC_@AM_V@)
+am__v_CC_ = $(am__v_CC_@AM_DEFAULT_V@)
+am__v_CC_0 = @echo " CC " $@;
+am__v_CC_1 =
+CCLD = $(CC)
+LINK = $(LIBTOOL) $(AM_V_lt) --tag=CC $(AM_LIBTOOLFLAGS) \
+ $(LIBTOOLFLAGS) --mode=link $(CCLD) $(AM_CFLAGS) $(CFLAGS) \
+ $(AM_LDFLAGS) $(LDFLAGS) -o $@
+AM_V_CCLD = $(am__v_CCLD_@AM_V@)
+am__v_CCLD_ = $(am__v_CCLD_@AM_DEFAULT_V@)
+am__v_CCLD_0 = @echo " CCLD " $@;
+am__v_CCLD_1 =
+SOURCES = $(libstorage_shared_la_SOURCES)
+DIST_SOURCES = $(libstorage_shared_la_SOURCES)
+am__can_run_installinfo = \
+ case $$AM_UPDATE_INFO_DIR in \
+ n|no|NO) false;; \
+ *) (install-info --version) >/dev/null 2>&1;; \
+ esac
+am__vpath_adj_setup = srcdirstrip=`echo "$(srcdir)" | sed 's|.|.|g'`;
+am__vpath_adj = case $$p in \
+ $(srcdir)/*) f=`echo "$$p" | sed "s|^$$srcdirstrip/||"`;; \
+ *) f=$$p;; \
+ esac;
+am__strip_dir = f=`echo $$p | sed -e 's|^.*/||'`;
+am__install_max = 40
+am__nobase_strip_setup = \
+ srcdirstrip=`echo "$(srcdir)" | sed 's/[].[^$$\\*|]/\\\\&/g'`
+am__nobase_strip = \
+ for p in $$list; do echo "$$p"; done | sed -e "s|$$srcdirstrip/||"
+am__nobase_list = $(am__nobase_strip_setup); \
+ for p in $$list; do echo "$$p $$p"; done | \
+ sed "s| $$srcdirstrip/| |;"' / .*\//!s/ .*/ ./; s,\( .*\)/[^/]*$$,\1,' | \
+ $(AWK) 'BEGIN { files["."] = "" } { files[$$2] = files[$$2] " " $$1; \
+ if (++n[$$2] == $(am__install_max)) \
+ { print $$2, files[$$2]; n[$$2] = 0; files[$$2] = "" } } \
+ END { for (dir in files) print dir, files[dir] }'
+am__base_list = \
+ sed '$$!N;$$!N;$$!N;$$!N;$$!N;$$!N;$$!N;s/\n/ /g' | \
+ sed '$$!N;$$!N;$$!N;$$!N;s/\n/ /g'
+am__uninstall_files_from_dir = { \
+ test -z "$$files" \
+ || { test ! -d "$$dir" && test ! -f "$$dir" && test ! -r "$$dir"; } \
+ || { echo " ( cd '$$dir' && rm -f" $$files ")"; \
+ $(am__cd) "$$dir" && rm -f $$files; }; \
+ }
+am__installdirs = "$(DESTDIR)$(pkginc_libdir)"
+HEADERS = $(pkginc_lib_HEADERS)
+am__tagged_files = $(HEADERS) $(SOURCES) $(TAGS_FILES) $(LISP)
+# Read a list of newline-separated strings from the standard input,
+# and print each of them once, without duplicates. Input order is
+# *not* preserved.
+am__uniquify_input = $(AWK) '\
+ BEGIN { nonempty = 0; } \
+ { items[$$0] = 1; nonempty = 1; } \
+ END { if (nonempty) { for (i in items) print i; }; } \
+'
+# Make sure the list of sources is unique. This is necessary because,
+# e.g., the same source file might be shared among _SOURCES variables
+# for different programs/libraries.
+am__define_uniq_tagged_files = \
+ list='$(am__tagged_files)'; \
+ unique=`for i in $$list; do \
+ if test -f "$$i"; then echo $$i; else echo $(srcdir)/$$i; fi; \
+ done | $(am__uniquify_input)`
+ETAGS = etags
+CTAGS = ctags
+am__DIST_COMMON = $(srcdir)/Makefile.in $(top_srcdir)/depcomp
+DISTFILES = $(DIST_COMMON) $(DIST_SOURCES) $(TEXINFOS) $(EXTRA_DIST)
+ACLOCAL = @ACLOCAL@
+ACLOCAL_AMFLAGS = @ACLOCAL_AMFLAGS@
+AMTAR = @AMTAR@
+AM_DEFAULT_VERBOSITY = @AM_DEFAULT_VERBOSITY@
+APPARMOR_LIBS = @APPARMOR_LIBS@
+AR = @AR@
+AUTH_CFLAGS = @AUTH_CFLAGS@
+AUTH_LIBS = @AUTH_LIBS@
+AUTOCONF = @AUTOCONF@
+AUTOHEADER = @AUTOHEADER@
+AUTOMAKE = @AUTOMAKE@
+AWK = @AWK@
+BINARY_CFLAGS = @BINARY_CFLAGS@
+BINARY_LDFLAGS = @BINARY_LDFLAGS@
+BISON = @BISON@
+CASSANDRA_CFLAGS = @CASSANDRA_CFLAGS@
+CASSANDRA_LIBS = @CASSANDRA_LIBS@
+CC = @CC@
+CCDEPMODE = @CCDEPMODE@
+CDB_LIBS = @CDB_LIBS@
+CFLAGS = @CFLAGS@
+CLUCENE_CFLAGS = @CLUCENE_CFLAGS@
+CLUCENE_LIBS = @CLUCENE_LIBS@
+COMPRESS_LIBS = @COMPRESS_LIBS@
+CPP = @CPP@
+CPPFLAGS = @CPPFLAGS@
+CRYPT_LIBS = @CRYPT_LIBS@
+CXX = @CXX@
+CXXCPP = @CXXCPP@
+CXXDEPMODE = @CXXDEPMODE@
+CXXFLAGS = @CXXFLAGS@
+CYGPATH_W = @CYGPATH_W@
+DEFS = @DEFS@
+DEPDIR = @DEPDIR@
+DICT_LIBS = @DICT_LIBS@
+DLLIB = @DLLIB@
+DLLTOOL = @DLLTOOL@
+DSYMUTIL = @DSYMUTIL@
+DUMPBIN = @DUMPBIN@
+ECHO_C = @ECHO_C@
+ECHO_N = @ECHO_N@
+ECHO_T = @ECHO_T@
+EGREP = @EGREP@
+EXEEXT = @EXEEXT@
+FGREP = @FGREP@
+FLEX = @FLEX@
+FUZZER_CPPFLAGS = @FUZZER_CPPFLAGS@
+FUZZER_LDFLAGS = @FUZZER_LDFLAGS@
+GREP = @GREP@
+INSTALL = @INSTALL@
+INSTALL_DATA = @INSTALL_DATA@
+INSTALL_PROGRAM = @INSTALL_PROGRAM@
+INSTALL_SCRIPT = @INSTALL_SCRIPT@
+INSTALL_STRIP_PROGRAM = @INSTALL_STRIP_PROGRAM@
+KRB5CONFIG = @KRB5CONFIG@
+KRB5_CFLAGS = @KRB5_CFLAGS@
+KRB5_LIBS = @KRB5_LIBS@
+LD = @LD@
+LDAP_LIBS = @LDAP_LIBS@
+LDFLAGS = @LDFLAGS@
+LD_NO_WHOLE_ARCHIVE = @LD_NO_WHOLE_ARCHIVE@
+LD_WHOLE_ARCHIVE = @LD_WHOLE_ARCHIVE@
+LIBCAP = @LIBCAP@
+LIBDOVECOT = @LIBDOVECOT@
+LIBDOVECOT_COMPRESS = @LIBDOVECOT_COMPRESS@
+LIBDOVECOT_DEPS = @LIBDOVECOT_DEPS@
+LIBDOVECOT_DSYNC = @LIBDOVECOT_DSYNC@
+LIBDOVECOT_LA_LIBS = @LIBDOVECOT_LA_LIBS@
+LIBDOVECOT_LDA = @LIBDOVECOT_LDA@
+LIBDOVECOT_LDAP = @LIBDOVECOT_LDAP@
+LIBDOVECOT_LIBFTS = @LIBDOVECOT_LIBFTS@
+LIBDOVECOT_LIBFTS_DEPS = @LIBDOVECOT_LIBFTS_DEPS@
+LIBDOVECOT_LOGIN = @LIBDOVECOT_LOGIN@
+LIBDOVECOT_LUA = @LIBDOVECOT_LUA@
+LIBDOVECOT_LUA_DEPS = @LIBDOVECOT_LUA_DEPS@
+LIBDOVECOT_SQL = @LIBDOVECOT_SQL@
+LIBDOVECOT_STORAGE = @LIBDOVECOT_STORAGE@
+LIBDOVECOT_STORAGE_DEPS = @LIBDOVECOT_STORAGE_DEPS@
+LIBEXTTEXTCAT_CFLAGS = @LIBEXTTEXTCAT_CFLAGS@
+LIBEXTTEXTCAT_LIBS = @LIBEXTTEXTCAT_LIBS@
+LIBICONV = @LIBICONV@
+LIBICU_CFLAGS = @LIBICU_CFLAGS@
+LIBICU_LIBS = @LIBICU_LIBS@
+LIBOBJS = @LIBOBJS@
+LIBS = @LIBS@
+LIBSODIUM_CFLAGS = @LIBSODIUM_CFLAGS@
+LIBSODIUM_LIBS = @LIBSODIUM_LIBS@
+LIBTIRPC_CFLAGS = @LIBTIRPC_CFLAGS@
+LIBTIRPC_LIBS = @LIBTIRPC_LIBS@
+LIBTOOL = @LIBTOOL@
+LIBUNWIND_CFLAGS = @LIBUNWIND_CFLAGS@
+LIBUNWIND_LIBS = @LIBUNWIND_LIBS@
+LIBWRAP_LIBS = @LIBWRAP_LIBS@
+LINKED_STORAGE_LDADD = @LINKED_STORAGE_LDADD@
+LIPO = @LIPO@
+LN_S = @LN_S@
+LTLIBICONV = @LTLIBICONV@
+LTLIBOBJS = @LTLIBOBJS@
+LT_SYS_LIBRARY_PATH = @LT_SYS_LIBRARY_PATH@
+LUA_CFLAGS = @LUA_CFLAGS@
+LUA_LIBS = @LUA_LIBS@
+MAINT = @MAINT@
+MAKEINFO = @MAKEINFO@
+MANIFEST_TOOL = @MANIFEST_TOOL@
+MKDIR_P = @MKDIR_P@
+MODULE_LIBS = @MODULE_LIBS@
+MODULE_SUFFIX = @MODULE_SUFFIX@
+MYSQL_CFLAGS = @MYSQL_CFLAGS@
+MYSQL_CONFIG = @MYSQL_CONFIG@
+MYSQL_LIBS = @MYSQL_LIBS@
+NM = @NM@
+NMEDIT = @NMEDIT@
+NOPLUGIN_LDFLAGS = @NOPLUGIN_LDFLAGS@
+OBJDUMP = @OBJDUMP@
+OBJEXT = @OBJEXT@
+OTOOL = @OTOOL@
+OTOOL64 = @OTOOL64@
+PACKAGE = @PACKAGE@
+PACKAGE_BUGREPORT = @PACKAGE_BUGREPORT@
+PACKAGE_NAME = @PACKAGE_NAME@
+PACKAGE_STRING = @PACKAGE_STRING@
+PACKAGE_TARNAME = @PACKAGE_TARNAME@
+PACKAGE_URL = @PACKAGE_URL@
+PACKAGE_VERSION = @PACKAGE_VERSION@
+PANDOC = @PANDOC@
+PATH_SEPARATOR = @PATH_SEPARATOR@
+PGSQL_CFLAGS = @PGSQL_CFLAGS@
+PGSQL_LIBS = @PGSQL_LIBS@
+PG_CONFIG = @PG_CONFIG@
+PIE_CFLAGS = @PIE_CFLAGS@
+PIE_LDFLAGS = @PIE_LDFLAGS@
+PKG_CONFIG = @PKG_CONFIG@
+PKG_CONFIG_LIBDIR = @PKG_CONFIG_LIBDIR@
+PKG_CONFIG_PATH = @PKG_CONFIG_PATH@
+QUOTA_LIBS = @QUOTA_LIBS@
+RANLIB = @RANLIB@
+RELRO_LDFLAGS = @RELRO_LDFLAGS@
+RPCGEN = @RPCGEN@
+RUN_TEST = @RUN_TEST@
+SED = @SED@
+SETTING_FILES = @SETTING_FILES@
+SET_MAKE = @SET_MAKE@
+SHELL = @SHELL@
+SQLITE_CFLAGS = @SQLITE_CFLAGS@
+SQLITE_LIBS = @SQLITE_LIBS@
+SQL_CFLAGS = @SQL_CFLAGS@
+SQL_LIBS = @SQL_LIBS@
+SSL_CFLAGS = @SSL_CFLAGS@
+SSL_LIBS = @SSL_LIBS@
+STRIP = @STRIP@
+SYSTEMD_CFLAGS = @SYSTEMD_CFLAGS@
+SYSTEMD_LIBS = @SYSTEMD_LIBS@
+VALGRIND = @VALGRIND@
+VERSION = @VERSION@
+ZSTD_CFLAGS = @ZSTD_CFLAGS@
+ZSTD_LIBS = @ZSTD_LIBS@
+abs_builddir = @abs_builddir@
+abs_srcdir = @abs_srcdir@
+abs_top_builddir = @abs_top_builddir@
+abs_top_srcdir = @abs_top_srcdir@
+ac_ct_AR = @ac_ct_AR@
+ac_ct_CC = @ac_ct_CC@
+ac_ct_CXX = @ac_ct_CXX@
+ac_ct_DUMPBIN = @ac_ct_DUMPBIN@
+am__include = @am__include@
+am__leading_dot = @am__leading_dot@
+am__quote = @am__quote@
+am__tar = @am__tar@
+am__untar = @am__untar@
+bindir = @bindir@
+build = @build@
+build_alias = @build_alias@
+build_cpu = @build_cpu@
+build_os = @build_os@
+build_vendor = @build_vendor@
+builddir = @builddir@
+datadir = @datadir@
+datarootdir = @datarootdir@
+dict_drivers = @dict_drivers@
+docdir = @docdir@
+dvidir = @dvidir@
+exec_prefix = @exec_prefix@
+host = @host@
+host_alias = @host_alias@
+host_cpu = @host_cpu@
+host_os = @host_os@
+host_vendor = @host_vendor@
+htmldir = @htmldir@
+includedir = @includedir@
+infodir = @infodir@
+install_sh = @install_sh@
+libdir = @libdir@
+libexecdir = @libexecdir@
+localedir = @localedir@
+localstatedir = @localstatedir@
+mandir = @mandir@
+mkdir_p = @mkdir_p@
+moduledir = @moduledir@
+oldincludedir = @oldincludedir@
+pdfdir = @pdfdir@
+prefix = @prefix@
+program_transform_name = @program_transform_name@
+psdir = @psdir@
+rundir = @rundir@
+runstatedir = @runstatedir@
+sbindir = @sbindir@
+sharedstatedir = @sharedstatedir@
+sql_drivers = @sql_drivers@
+srcdir = @srcdir@
+ssldir = @ssldir@
+statedir = @statedir@
+sysconfdir = @sysconfdir@
+systemdservicetype = @systemdservicetype@
+systemdsystemunitdir = @systemdsystemunitdir@
+target_alias = @target_alias@
+top_build_prefix = @top_build_prefix@
+top_builddir = @top_builddir@
+top_srcdir = @top_srcdir@
+noinst_LTLIBRARIES = libstorage_shared.la
+AM_CPPFLAGS = \
+ -I$(top_srcdir)/src/lib \
+ -I$(top_srcdir)/src/lib-mail \
+ -I$(top_srcdir)/src/lib-imap \
+ -I$(top_srcdir)/src/lib-index \
+ -I$(top_srcdir)/src/lib-storage \
+ -I$(top_srcdir)/src/lib-storage/index
+
+libstorage_shared_la_SOURCES = \
+ shared-list.c \
+ shared-storage.c
+
+headers = \
+ shared-storage.h
+
+pkginc_libdir = $(pkgincludedir)
+pkginc_lib_HEADERS = $(headers)
+all: all-am
+
+.SUFFIXES:
+.SUFFIXES: .c .lo .o .obj
+$(srcdir)/Makefile.in: @MAINTAINER_MODE_TRUE@ $(srcdir)/Makefile.am $(am__configure_deps)
+ @for dep in $?; do \
+ case '$(am__configure_deps)' in \
+ *$$dep*) \
+ ( cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh ) \
+ && { if test -f $@; then exit 0; else break; fi; }; \
+ exit 1;; \
+ esac; \
+ done; \
+ echo ' cd $(top_srcdir) && $(AUTOMAKE) --foreign src/lib-storage/index/shared/Makefile'; \
+ $(am__cd) $(top_srcdir) && \
+ $(AUTOMAKE) --foreign src/lib-storage/index/shared/Makefile
+Makefile: $(srcdir)/Makefile.in $(top_builddir)/config.status
+ @case '$?' in \
+ *config.status*) \
+ cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh;; \
+ *) \
+ echo ' cd $(top_builddir) && $(SHELL) ./config.status $(subdir)/$@ $(am__maybe_remake_depfiles)'; \
+ cd $(top_builddir) && $(SHELL) ./config.status $(subdir)/$@ $(am__maybe_remake_depfiles);; \
+ esac;
+
+$(top_builddir)/config.status: $(top_srcdir)/configure $(CONFIG_STATUS_DEPENDENCIES)
+ cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh
+
+$(top_srcdir)/configure: @MAINTAINER_MODE_TRUE@ $(am__configure_deps)
+ cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh
+$(ACLOCAL_M4): @MAINTAINER_MODE_TRUE@ $(am__aclocal_m4_deps)
+ cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh
+$(am__aclocal_m4_deps):
+
+clean-noinstLTLIBRARIES:
+ -test -z "$(noinst_LTLIBRARIES)" || rm -f $(noinst_LTLIBRARIES)
+ @list='$(noinst_LTLIBRARIES)'; \
+ locs=`for p in $$list; do echo $$p; done | \
+ sed 's|^[^/]*$$|.|; s|/[^/]*$$||; s|$$|/so_locations|' | \
+ sort -u`; \
+ test -z "$$locs" || { \
+ echo rm -f $${locs}; \
+ rm -f $${locs}; \
+ }
+
+libstorage_shared.la: $(libstorage_shared_la_OBJECTS) $(libstorage_shared_la_DEPENDENCIES) $(EXTRA_libstorage_shared_la_DEPENDENCIES)
+ $(AM_V_CCLD)$(LINK) $(libstorage_shared_la_OBJECTS) $(libstorage_shared_la_LIBADD) $(LIBS)
+
+mostlyclean-compile:
+ -rm -f *.$(OBJEXT)
+
+distclean-compile:
+ -rm -f *.tab.c
+
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/shared-list.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/shared-storage.Plo@am__quote@ # am--include-marker
+
+$(am__depfiles_remade):
+ @$(MKDIR_P) $(@D)
+ @echo '# dummy' >$@-t && $(am__mv) $@-t $@
+
+am--depfiles: $(am__depfiles_remade)
+
+.c.o:
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(COMPILE) -MT $@ -MD -MP -MF $(DEPDIR)/$*.Tpo -c -o $@ $<
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/$*.Tpo $(DEPDIR)/$*.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='$<' object='$@' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(COMPILE) -c -o $@ $<
+
+.c.obj:
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(COMPILE) -MT $@ -MD -MP -MF $(DEPDIR)/$*.Tpo -c -o $@ `$(CYGPATH_W) '$<'`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/$*.Tpo $(DEPDIR)/$*.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='$<' object='$@' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(COMPILE) -c -o $@ `$(CYGPATH_W) '$<'`
+
+.c.lo:
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(LTCOMPILE) -MT $@ -MD -MP -MF $(DEPDIR)/$*.Tpo -c -o $@ $<
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/$*.Tpo $(DEPDIR)/$*.Plo
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='$<' object='$@' libtool=yes @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(LTCOMPILE) -c -o $@ $<
+
+mostlyclean-libtool:
+ -rm -f *.lo
+
+clean-libtool:
+ -rm -rf .libs _libs
+install-pkginc_libHEADERS: $(pkginc_lib_HEADERS)
+ @$(NORMAL_INSTALL)
+ @list='$(pkginc_lib_HEADERS)'; test -n "$(pkginc_libdir)" || list=; \
+ if test -n "$$list"; then \
+ echo " $(MKDIR_P) '$(DESTDIR)$(pkginc_libdir)'"; \
+ $(MKDIR_P) "$(DESTDIR)$(pkginc_libdir)" || exit 1; \
+ fi; \
+ for p in $$list; do \
+ if test -f "$$p"; then d=; else d="$(srcdir)/"; fi; \
+ echo "$$d$$p"; \
+ done | $(am__base_list) | \
+ while read files; do \
+ echo " $(INSTALL_HEADER) $$files '$(DESTDIR)$(pkginc_libdir)'"; \
+ $(INSTALL_HEADER) $$files "$(DESTDIR)$(pkginc_libdir)" || exit $$?; \
+ done
+
+uninstall-pkginc_libHEADERS:
+ @$(NORMAL_UNINSTALL)
+ @list='$(pkginc_lib_HEADERS)'; test -n "$(pkginc_libdir)" || list=; \
+ files=`for p in $$list; do echo $$p; done | sed -e 's|^.*/||'`; \
+ dir='$(DESTDIR)$(pkginc_libdir)'; $(am__uninstall_files_from_dir)
+
+ID: $(am__tagged_files)
+ $(am__define_uniq_tagged_files); mkid -fID $$unique
+tags: tags-am
+TAGS: tags
+
+tags-am: $(TAGS_DEPENDENCIES) $(am__tagged_files)
+ set x; \
+ here=`pwd`; \
+ $(am__define_uniq_tagged_files); \
+ shift; \
+ if test -z "$(ETAGS_ARGS)$$*$$unique"; then :; else \
+ test -n "$$unique" || unique=$$empty_fix; \
+ if test $$# -gt 0; then \
+ $(ETAGS) $(ETAGSFLAGS) $(AM_ETAGSFLAGS) $(ETAGS_ARGS) \
+ "$$@" $$unique; \
+ else \
+ $(ETAGS) $(ETAGSFLAGS) $(AM_ETAGSFLAGS) $(ETAGS_ARGS) \
+ $$unique; \
+ fi; \
+ fi
+ctags: ctags-am
+
+CTAGS: ctags
+ctags-am: $(TAGS_DEPENDENCIES) $(am__tagged_files)
+ $(am__define_uniq_tagged_files); \
+ test -z "$(CTAGS_ARGS)$$unique" \
+ || $(CTAGS) $(CTAGSFLAGS) $(AM_CTAGSFLAGS) $(CTAGS_ARGS) \
+ $$unique
+
+GTAGS:
+ here=`$(am__cd) $(top_builddir) && pwd` \
+ && $(am__cd) $(top_srcdir) \
+ && gtags -i $(GTAGS_ARGS) "$$here"
+cscopelist: cscopelist-am
+
+cscopelist-am: $(am__tagged_files)
+ list='$(am__tagged_files)'; \
+ case "$(srcdir)" in \
+ [\\/]* | ?:[\\/]*) sdir="$(srcdir)" ;; \
+ *) sdir=$(subdir)/$(srcdir) ;; \
+ esac; \
+ for i in $$list; do \
+ if test -f "$$i"; then \
+ echo "$(subdir)/$$i"; \
+ else \
+ echo "$$sdir/$$i"; \
+ fi; \
+ done >> $(top_builddir)/cscope.files
+
+distclean-tags:
+ -rm -f TAGS ID GTAGS GRTAGS GSYMS GPATH tags
+
+distdir: $(BUILT_SOURCES)
+ $(MAKE) $(AM_MAKEFLAGS) distdir-am
+
+distdir-am: $(DISTFILES)
+ @srcdirstrip=`echo "$(srcdir)" | sed 's/[].[^$$\\*]/\\\\&/g'`; \
+ topsrcdirstrip=`echo "$(top_srcdir)" | sed 's/[].[^$$\\*]/\\\\&/g'`; \
+ list='$(DISTFILES)'; \
+ dist_files=`for file in $$list; do echo $$file; done | \
+ sed -e "s|^$$srcdirstrip/||;t" \
+ -e "s|^$$topsrcdirstrip/|$(top_builddir)/|;t"`; \
+ case $$dist_files in \
+ */*) $(MKDIR_P) `echo "$$dist_files" | \
+ sed '/\//!d;s|^|$(distdir)/|;s,/[^/]*$$,,' | \
+ sort -u` ;; \
+ esac; \
+ for file in $$dist_files; do \
+ if test -f $$file || test -d $$file; then d=.; else d=$(srcdir); fi; \
+ if test -d $$d/$$file; then \
+ dir=`echo "/$$file" | sed -e 's,/[^/]*$$,,'`; \
+ if test -d "$(distdir)/$$file"; then \
+ find "$(distdir)/$$file" -type d ! -perm -700 -exec chmod u+rwx {} \;; \
+ fi; \
+ if test -d $(srcdir)/$$file && test $$d != $(srcdir); then \
+ cp -fpR $(srcdir)/$$file "$(distdir)$$dir" || exit 1; \
+ find "$(distdir)/$$file" -type d ! -perm -700 -exec chmod u+rwx {} \;; \
+ fi; \
+ cp -fpR $$d/$$file "$(distdir)$$dir" || exit 1; \
+ else \
+ test -f "$(distdir)/$$file" \
+ || cp -p $$d/$$file "$(distdir)/$$file" \
+ || exit 1; \
+ fi; \
+ done
+check-am: all-am
+check: check-am
+all-am: Makefile $(LTLIBRARIES) $(HEADERS)
+installdirs:
+ for dir in "$(DESTDIR)$(pkginc_libdir)"; do \
+ test -z "$$dir" || $(MKDIR_P) "$$dir"; \
+ done
+install: install-am
+install-exec: install-exec-am
+install-data: install-data-am
+uninstall: uninstall-am
+
+install-am: all-am
+ @$(MAKE) $(AM_MAKEFLAGS) install-exec-am install-data-am
+
+installcheck: installcheck-am
+install-strip:
+ if test -z '$(STRIP)'; then \
+ $(MAKE) $(AM_MAKEFLAGS) INSTALL_PROGRAM="$(INSTALL_STRIP_PROGRAM)" \
+ install_sh_PROGRAM="$(INSTALL_STRIP_PROGRAM)" INSTALL_STRIP_FLAG=-s \
+ install; \
+ else \
+ $(MAKE) $(AM_MAKEFLAGS) INSTALL_PROGRAM="$(INSTALL_STRIP_PROGRAM)" \
+ install_sh_PROGRAM="$(INSTALL_STRIP_PROGRAM)" INSTALL_STRIP_FLAG=-s \
+ "INSTALL_PROGRAM_ENV=STRIPPROG='$(STRIP)'" install; \
+ fi
+mostlyclean-generic:
+
+clean-generic:
+
+distclean-generic:
+ -test -z "$(CONFIG_CLEAN_FILES)" || rm -f $(CONFIG_CLEAN_FILES)
+ -test . = "$(srcdir)" || test -z "$(CONFIG_CLEAN_VPATH_FILES)" || rm -f $(CONFIG_CLEAN_VPATH_FILES)
+
+maintainer-clean-generic:
+ @echo "This command is intended for maintainers to use"
+ @echo "it deletes files that may require special tools to rebuild."
+clean: clean-am
+
+clean-am: clean-generic clean-libtool clean-noinstLTLIBRARIES \
+ mostlyclean-am
+
+distclean: distclean-am
+ -rm -f ./$(DEPDIR)/shared-list.Plo
+ -rm -f ./$(DEPDIR)/shared-storage.Plo
+ -rm -f Makefile
+distclean-am: clean-am distclean-compile distclean-generic \
+ distclean-tags
+
+dvi: dvi-am
+
+dvi-am:
+
+html: html-am
+
+html-am:
+
+info: info-am
+
+info-am:
+
+install-data-am: install-pkginc_libHEADERS
+
+install-dvi: install-dvi-am
+
+install-dvi-am:
+
+install-exec-am:
+
+install-html: install-html-am
+
+install-html-am:
+
+install-info: install-info-am
+
+install-info-am:
+
+install-man:
+
+install-pdf: install-pdf-am
+
+install-pdf-am:
+
+install-ps: install-ps-am
+
+install-ps-am:
+
+installcheck-am:
+
+maintainer-clean: maintainer-clean-am
+ -rm -f ./$(DEPDIR)/shared-list.Plo
+ -rm -f ./$(DEPDIR)/shared-storage.Plo
+ -rm -f Makefile
+maintainer-clean-am: distclean-am maintainer-clean-generic
+
+mostlyclean: mostlyclean-am
+
+mostlyclean-am: mostlyclean-compile mostlyclean-generic \
+ mostlyclean-libtool
+
+pdf: pdf-am
+
+pdf-am:
+
+ps: ps-am
+
+ps-am:
+
+uninstall-am: uninstall-pkginc_libHEADERS
+
+.MAKE: install-am install-strip
+
+.PHONY: CTAGS GTAGS TAGS all all-am am--depfiles check check-am clean \
+ clean-generic clean-libtool clean-noinstLTLIBRARIES \
+ cscopelist-am ctags ctags-am distclean distclean-compile \
+ distclean-generic distclean-libtool distclean-tags distdir dvi \
+ dvi-am html html-am info info-am install install-am \
+ install-data install-data-am install-dvi install-dvi-am \
+ install-exec install-exec-am install-html install-html-am \
+ install-info install-info-am install-man install-pdf \
+ install-pdf-am install-pkginc_libHEADERS install-ps \
+ install-ps-am install-strip installcheck installcheck-am \
+ installdirs maintainer-clean maintainer-clean-generic \
+ mostlyclean mostlyclean-compile mostlyclean-generic \
+ mostlyclean-libtool pdf pdf-am ps ps-am tags tags-am uninstall \
+ uninstall-am uninstall-pkginc_libHEADERS
+
+.PRECIOUS: Makefile
+
+
+# Tell versions [3.59,3.63) of GNU make to not export all variables.
+# Otherwise a system limit (for SysV at least) may be exceeded.
+.NOEXPORT:
diff --git a/src/lib-storage/index/shared/shared-list.c b/src/lib-storage/index/shared/shared-list.c
new file mode 100644
index 0000000..c69c4db
--- /dev/null
+++ b/src/lib-storage/index/shared/shared-list.c
@@ -0,0 +1,310 @@
+/* Copyright (c) 2008-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "imap-match.h"
+#include "mailbox-tree.h"
+#include "mailbox-list-private.h"
+#include "index-storage.h"
+#include "shared-storage.h"
+
+extern struct mailbox_list shared_mailbox_list;
+
+static struct mailbox_list *shared_list_alloc(void)
+{
+ struct mailbox_list *list;
+ pool_t pool;
+
+ pool = pool_alloconly_create("shared list", 2048);
+ list = p_new(pool, struct mailbox_list, 1);
+ *list = shared_mailbox_list;
+ list->pool = pool;
+ return list;
+}
+
+static void shared_list_deinit(struct mailbox_list *list)
+{
+ pool_unref(&list->pool);
+}
+
+static void shared_list_copy_error(struct mailbox_list *shared_list,
+ struct mail_namespace *backend_ns)
+{
+ const char *str;
+ enum mail_error error;
+
+ str = mailbox_list_get_last_error(backend_ns->list, &error);
+ mailbox_list_set_error(shared_list, error, str);
+}
+
+static int
+shared_get_storage(struct mailbox_list **list, const char *vname,
+ struct mail_storage **storage_r)
+{
+ struct mail_namespace *ns = (*list)->ns;
+ const char *name;
+
+ name = mailbox_list_get_storage_name(*list, vname);
+ if (*name == '\0' && (ns->flags & NAMESPACE_FLAG_AUTOCREATED) == 0) {
+ /* trying to access the shared/ prefix itself */
+ *storage_r = ns->storage;
+ return 0;
+ }
+
+ if (shared_storage_get_namespace(&ns, &name) < 0)
+ return -1;
+ *list = ns->list;
+ return mailbox_list_get_storage(list, vname, storage_r);
+}
+
+static char shared_list_get_hierarchy_sep(struct mailbox_list *list ATTR_UNUSED)
+{
+ return '/';
+}
+
+static int
+shared_list_get_path(struct mailbox_list *list, const char *name,
+ enum mailbox_list_path_type type, const char **path_r)
+{
+ struct mail_namespace *ns = list->ns;
+
+ if (mail_namespace_get_default_storage(list->ns) == NULL ||
+ name == NULL ||
+ shared_storage_get_namespace(&ns, &name) < 0) {
+ /* we don't have a directory we can use. */
+ *path_r = NULL;
+ return 0;
+ }
+ return mailbox_list_get_path(ns->list, name, type, path_r);
+}
+
+static const char *
+shared_list_get_temp_prefix(struct mailbox_list *list, bool global ATTR_UNUSED)
+{
+ i_panic("shared mailbox list: Can't return a temp prefix for '%s'",
+ list->ns->prefix);
+}
+
+static const char *
+shared_list_join_refpattern(struct mailbox_list *list,
+ const char *ref, const char *pattern)
+{
+ struct mail_namespace *ns = list->ns;
+ const char *ns_ref, *prefix = list->ns->prefix;
+ size_t prefix_len = strlen(prefix);
+
+ if (*ref != '\0' && str_begins(ref, prefix))
+ ns_ref = ref + prefix_len;
+ else
+ ns_ref = NULL;
+
+ if (ns_ref != NULL && *ns_ref != '\0' &&
+ shared_storage_get_namespace(&ns, &ns_ref) == 0)
+ return mailbox_list_join_refpattern(ns->list, ref, pattern);
+
+ /* fallback to default behavior */
+ if (*ref != '\0')
+ pattern = t_strconcat(ref, pattern, NULL);
+ return pattern;
+}
+
+static void
+shared_list_create_missing_namespaces(struct mailbox_list *list,
+ const char *const *patterns)
+{
+ struct mail_namespace *ns;
+ char sep = mail_namespace_get_sep(list->ns);
+ const char *list_pat, *name;
+ unsigned int i;
+
+ for (i = 0; patterns[i] != NULL; i++) {
+ const char *last = NULL, *p;
+
+ /* we'll require that the pattern begins with the list's
+ namespace prefix. we could also handle other patterns
+ (e.g. %/user/%), but it's more of a theoretical problem. */
+ if (strncmp(list->ns->prefix, patterns[i],
+ list->ns->prefix_len) != 0)
+ continue;
+ list_pat = patterns[i] + list->ns->prefix_len;
+
+ for (p = list_pat; *p != '\0'; p++) {
+ if (*p == '%' || *p == '*')
+ break;
+ if (*p == sep)
+ last = p;
+ }
+ if (last != NULL) {
+ ns = list->ns;
+ name = t_strdup_until(list_pat, last);
+ (void)shared_storage_get_namespace(&ns, &name);
+ }
+ }
+}
+
+static struct mailbox_list_iterate_context *
+shared_list_iter_init(struct mailbox_list *list, const char *const *patterns,
+ enum mailbox_list_iter_flags flags)
+{
+ struct mailbox_list_iterate_context *ctx;
+ pool_t pool;
+ char sep = mail_namespace_get_sep(list->ns);
+
+ pool = pool_alloconly_create("mailbox list shared iter", 1024);
+ ctx = p_new(pool, struct mailbox_list_iterate_context, 1);
+ ctx->pool = pool;
+ ctx->list = list;
+ ctx->flags = flags;
+ ctx->glob = imap_match_init_multiple(pool, patterns, FALSE, sep);
+ array_create(&ctx->module_contexts, pool, sizeof(void *), 5);
+
+ if ((flags & MAILBOX_LIST_ITER_SELECT_SUBSCRIBED) == 0 &&
+ (list->ns->flags & NAMESPACE_FLAG_AUTOCREATED) == 0) T_BEGIN {
+ shared_list_create_missing_namespaces(list, patterns);
+ } T_END;
+ return ctx;
+}
+
+static const struct mailbox_info *
+shared_list_iter_next(struct mailbox_list_iterate_context *ctx ATTR_UNUSED)
+{
+ return NULL;
+}
+
+static int shared_list_iter_deinit(struct mailbox_list_iterate_context *ctx)
+{
+ pool_unref(&ctx->pool);
+ return 0;
+}
+
+static int
+shared_list_subscriptions_refresh(struct mailbox_list *src_list,
+ struct mailbox_list *dest_list)
+{
+ char sep;
+
+ if (dest_list->subscriptions == NULL) {
+ sep = mail_namespace_get_sep(src_list->ns);
+ dest_list->subscriptions = mailbox_tree_init(sep);
+ }
+ return 0;
+}
+
+static int shared_list_set_subscribed(struct mailbox_list *list,
+ const char *name, bool set)
+{
+ struct mail_namespace *ns = list->ns;
+ int ret;
+
+ if (shared_storage_get_namespace(&ns, &name) < 0)
+ return -1;
+ ret = mailbox_list_set_subscribed(ns->list, name, set);
+ if (ret < 0)
+ shared_list_copy_error(list, ns);
+ return ret;
+}
+
+static int
+shared_list_delete_mailbox(struct mailbox_list *list, const char *name)
+{
+ struct mail_namespace *ns = list->ns;
+ int ret;
+
+ if (shared_storage_get_namespace(&ns, &name) < 0)
+ return -1;
+ ret = ns->list->v.delete_mailbox(ns->list, name);
+ if (ret < 0)
+ shared_list_copy_error(list, ns);
+ return ret;
+}
+
+static int
+shared_list_delete_dir(struct mailbox_list *list, const char *name)
+{
+ struct mail_namespace *ns = list->ns;
+ int ret;
+
+ if (shared_storage_get_namespace(&ns, &name) < 0)
+ return -1;
+ ret = mailbox_list_delete_dir(ns->list, name);
+ if (ret < 0)
+ shared_list_copy_error(list, ns);
+ return ret;
+}
+
+static int
+shared_list_delete_symlink(struct mailbox_list *list, const char *name)
+{
+ struct mail_namespace *ns = list->ns;
+ int ret;
+
+ if (shared_storage_get_namespace(&ns, &name) < 0)
+ return -1;
+ ret = mailbox_list_delete_symlink(ns->list, name);
+ if (ret < 0)
+ shared_list_copy_error(list, ns);
+ return ret;
+}
+
+static int shared_list_rename_get_ns(struct mailbox_list *oldlist,
+ const char **oldname,
+ struct mailbox_list *newlist,
+ const char **newname,
+ struct mail_namespace **ns_r)
+{
+ struct mail_namespace *old_ns = oldlist->ns, *new_ns = newlist->ns;
+
+ if (shared_storage_get_namespace(&old_ns, oldname) < 0 ||
+ shared_storage_get_namespace(&new_ns, newname) < 0)
+ return -1;
+ if (old_ns != new_ns) {
+ mailbox_list_set_error(oldlist, MAIL_ERROR_NOTPOSSIBLE,
+ "Can't rename shared mailboxes across storages.");
+ return -1;
+ }
+ *ns_r = old_ns;
+ return 0;
+}
+
+static int
+shared_list_rename_mailbox(struct mailbox_list *oldlist, const char *oldname,
+ struct mailbox_list *newlist, const char *newname)
+{
+ struct mail_namespace *ns;
+ int ret;
+
+ if (shared_list_rename_get_ns(oldlist, &oldname,
+ newlist, &newname, &ns) < 0)
+ return -1;
+
+ ret = ns->list->v.rename_mailbox(ns->list, oldname, ns->list, newname);
+ if (ret < 0)
+ shared_list_copy_error(oldlist, ns);
+ return ret;
+}
+
+struct mailbox_list shared_mailbox_list = {
+ .name = "shared",
+ .props = 0,
+ .mailbox_name_max_length = MAILBOX_LIST_NAME_MAX_LENGTH,
+
+ .v = {
+ .alloc = shared_list_alloc,
+ .deinit = shared_list_deinit,
+ .get_storage = shared_get_storage,
+ .get_hierarchy_sep = shared_list_get_hierarchy_sep,
+ .get_vname = mailbox_list_default_get_vname,
+ .get_storage_name = mailbox_list_default_get_storage_name,
+ .get_path = shared_list_get_path,
+ .get_temp_prefix = shared_list_get_temp_prefix,
+ .join_refpattern = shared_list_join_refpattern,
+ .iter_init = shared_list_iter_init,
+ .iter_next = shared_list_iter_next,
+ .iter_deinit = shared_list_iter_deinit,
+ .subscriptions_refresh = shared_list_subscriptions_refresh,
+ .set_subscribed = shared_list_set_subscribed,
+ .delete_mailbox = shared_list_delete_mailbox,
+ .delete_dir = shared_list_delete_dir,
+ .delete_symlink = shared_list_delete_symlink,
+ .rename_mailbox = shared_list_rename_mailbox,
+ }
+};
diff --git a/src/lib-storage/index/shared/shared-storage.c b/src/lib-storage/index/shared/shared-storage.c
new file mode 100644
index 0000000..978949e
--- /dev/null
+++ b/src/lib-storage/index/shared/shared-storage.c
@@ -0,0 +1,379 @@
+/* Copyright (c) 2008-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "str.h"
+#include "ioloop.h"
+#include "var-expand.h"
+#include "index-storage.h"
+#include "mail-storage-service.h"
+#include "mailbox-list-private.h"
+#include "fail-mail-storage.h"
+#include "shared-storage.h"
+
+#include <ctype.h>
+
+extern struct mail_storage shared_storage;
+
+static struct mail_storage *shared_storage_alloc(void)
+{
+ struct shared_storage *storage;
+ pool_t pool;
+
+ pool = pool_alloconly_create("shared storage", 1024);
+ storage = p_new(pool, struct shared_storage, 1);
+ storage->storage = shared_storage;
+ storage->storage.pool = pool;
+ return &storage->storage;
+}
+
+static int
+shared_storage_create(struct mail_storage *_storage, struct mail_namespace *ns,
+ const char **error_r)
+{
+ struct shared_storage *storage = SHARED_STORAGE(_storage);
+ const char *driver, *p;
+ char *wildcardp, key;
+ bool have_username;
+
+ /* location must begin with the actual mailbox driver */
+ p = strchr(ns->set->location, ':');
+ if (p == NULL) {
+ *error_r = "Shared mailbox location not prefixed with driver";
+ return -1;
+ }
+ driver = t_strdup_until(ns->set->location, p);
+ storage->location = p_strdup(_storage->pool, ns->set->location);
+ storage->unexpanded_location =
+ p_strdup(_storage->pool, ns->unexpanded_set->location);
+ storage->storage_class_name = p_strdup(_storage->pool, driver);
+
+ if (mail_user_get_storage_class(_storage->user, driver) == NULL &&
+ strcmp(driver, "auto") != 0) {
+ *error_r = t_strconcat("Unknown shared storage driver: ",
+ driver, NULL);
+ return -1;
+ }
+
+ wildcardp = strchr(ns->prefix, '%');
+ if (wildcardp == NULL) {
+ *error_r = "Shared namespace prefix doesn't contain %";
+ return -1;
+ }
+ storage->ns_prefix_pattern = p_strdup(_storage->pool, wildcardp);
+
+ have_username = FALSE;
+ for (p = storage->ns_prefix_pattern; *p != '\0'; p++) {
+ if (*p != '%')
+ continue;
+
+ key = p[1];
+ if (key == 'u' || key == 'n')
+ have_username = TRUE;
+ else if (key != '%' && key != 'd')
+ break;
+ }
+ if (*p != '\0') {
+ *error_r = "Shared namespace prefix contains unknown variables";
+ return -1;
+ }
+ if (!have_username) {
+ *error_r = "Shared namespace prefix doesn't contain %u or %n";
+ return -1;
+ }
+ if (p[-1] != mail_namespace_get_sep(ns) &&
+ (ns->flags & (NAMESPACE_FLAG_LIST_PREFIX |
+ NAMESPACE_FLAG_LIST_CHILDREN)) != 0) {
+ *error_r = "Shared namespace prefix doesn't end with hierarchy separator";
+ return -1;
+ }
+
+ /* truncate prefix after the above checks are done, so they can log
+ the full prefix in error conditions */
+ *wildcardp = '\0';
+ ns->prefix_len = strlen(ns->prefix);
+ return 0;
+}
+
+static void
+shared_storage_get_list_settings(const struct mail_namespace *ns ATTR_UNUSED,
+ struct mailbox_list_settings *set)
+{
+ set->layout = "shared";
+}
+
+static void
+get_nonexistent_user_location(struct shared_storage *storage,
+ const char *username, string_t *location)
+{
+ /* user wasn't found. we'll still need to create the storage
+ to avoid exposing which users exist and which don't. */
+ str_append(location, storage->storage_class_name);
+ str_append_c(location, ':');
+
+ /* use a reachable but nonexistent path as the mail root directory */
+ str_append(location, storage->storage.user->set->base_dir);
+ str_append(location, "/user-not-found/");
+ str_append(location, username);
+}
+
+static bool shared_namespace_exists(struct mail_namespace *ns)
+{
+ const char *path;
+ struct stat st;
+
+ if (!mailbox_list_get_root_path(ns->list, MAILBOX_LIST_PATH_TYPE_DIR,
+ &path)) {
+ /* we can't know if this exists */
+ return TRUE;
+ }
+ return stat(path, &st) == 0;
+}
+
+int shared_storage_get_namespace(struct mail_namespace **_ns,
+ const char **_name)
+{
+ struct mail_storage *_storage = (*_ns)->storage;
+ struct mailbox_list *list = (*_ns)->list;
+ struct shared_storage *storage = SHARED_STORAGE(_storage);
+ struct mail_user *user = _storage->user;
+ struct mail_namespace *new_ns, *ns = *_ns;
+ struct mail_namespace_settings *ns_set, *unexpanded_ns_set;
+ struct mail_user *owner;
+ const char *domain = NULL, *username = NULL, *userdomain = NULL;
+ const char *name, *p, *next, **dest, *error;
+ string_t *prefix, *location;
+ char ns_sep = mail_namespace_get_sep(ns);
+ int ret;
+
+ p = storage->ns_prefix_pattern;
+ for (name = *_name; *p != '\0';) {
+ if (*p != '%') {
+ if (*p != *name)
+ break;
+ p++; name++;
+ continue;
+ }
+ switch (*++p) {
+ case 'd':
+ dest = &domain;
+ break;
+ case 'n':
+ dest = &username;
+ break;
+ case 'u':
+ dest = &userdomain;
+ break;
+ default:
+ /* we checked this already above */
+ i_unreached();
+ }
+ p++;
+
+ next = strchr(name, *p != '\0' ? *p : ns_sep);
+ if (next == NULL) {
+ *dest = name;
+ name = "";
+ break;
+ }
+ *dest = t_strdup_until(name, next);
+ name = next;
+ }
+ if (*p != '\0') {
+ if (*name == '\0' ||
+ (name[1] == '\0' && *name == ns_sep)) {
+ /* trying to open <prefix>/<user> mailbox */
+ name = "INBOX";
+ } else {
+ mailbox_list_set_critical(list,
+ "Invalid namespace prefix %s vs %s",
+ storage->ns_prefix_pattern, *_name);
+ return -1;
+ }
+ }
+
+ /* successfully matched the name. */
+ if (userdomain != NULL) {
+ /* user@domain given */
+ domain = strchr(userdomain, '@');
+ if (domain == NULL)
+ username = userdomain;
+ else {
+ username = t_strdup_until(userdomain, domain);
+ domain++;
+ }
+ } else if (username == NULL) {
+ /* trying to open namespace "shared/domain"
+ namespace prefix. */
+ mailbox_list_set_error(list, MAIL_ERROR_NOTFOUND,
+ T_MAIL_ERR_MAILBOX_NOT_FOUND(*_name));
+ return -1;
+ } else {
+ if (domain == NULL) {
+ /* no domain given, use ours (if we have one) */
+ domain = i_strchr_to_next(user->username, '@');
+ }
+ userdomain = domain == NULL ? username :
+ t_strconcat(username, "@", domain, NULL);
+ }
+ if (*userdomain == '\0') {
+ mailbox_list_set_error(list, MAIL_ERROR_PARAMS,
+ "Empty username doesn't exist");
+ return -1;
+ }
+
+ /* expand the namespace prefix and see if it already exists.
+ this should normally happen only when the mailbox is being opened */
+ struct var_expand_table tab[] = {
+ { 'u', userdomain, "user" },
+ { 'n', username, "username" },
+ { 'd', domain, "domain" },
+ { 'h', NULL, "home" },
+ { '\0', NULL, NULL }
+ };
+
+ prefix = t_str_new(128);
+ str_append(prefix, ns->prefix);
+ if (var_expand(prefix, storage->ns_prefix_pattern, tab, &error) <= 0) {
+ mailbox_list_set_critical(list,
+ "Failed to expand namespace prefix '%s': %s",
+ storage->ns_prefix_pattern, error);
+ return -1;
+ }
+
+ *_ns = mail_namespace_find_prefix(user->namespaces, str_c(prefix));
+ if (*_ns != NULL) {
+ *_name = mailbox_list_get_storage_name(ns->list,
+ t_strconcat(ns->prefix, name, NULL));
+ return 0;
+ }
+
+ owner = mail_user_alloc(event_get_parent(user->event), userdomain,
+ user->set_info, user->unexpanded_set);
+ owner->_service_user = user->_service_user;
+ mail_storage_service_user_ref(owner->_service_user);
+ owner->creator = user;
+ owner->autocreated = TRUE;
+ owner->session_id = p_strdup(owner->pool, user->session_id);
+ if (mail_user_init(owner, &error) < 0) {
+ if (!owner->nonexistent) {
+ mailbox_list_set_critical(list,
+ "Couldn't create namespace '%s' for user %s: %s",
+ ns->prefix, userdomain, error);
+ mail_user_deinit(&owner);
+ return -1;
+ }
+ ret = 0;
+ } else if (!var_has_key(storage->location, 'h', "home")) {
+ ret = 1;
+ } else {
+ /* we'll need to look up the user's home directory */
+ if ((ret = mail_user_get_home(owner, &tab[3].value)) < 0) {
+ mailbox_list_set_critical(list, "Namespace '%s': "
+ "Could not lookup home for user %s",
+ ns->prefix, userdomain);
+ mail_user_deinit(&owner);
+ return -1;
+ }
+ }
+
+ location = t_str_new(256);
+ if (ret > 0 &&
+ var_expand(location, storage->location, tab, &error) <= 0) {
+ mailbox_list_set_critical(list,
+ "Failed to expand namespace location '%s': %s",
+ storage->location, error);
+ return -1;
+ }
+
+ /* create the new namespace */
+ new_ns = i_new(struct mail_namespace, 1);
+ new_ns->refcount = 1;
+ new_ns->type = MAIL_NAMESPACE_TYPE_SHARED;
+ new_ns->user = user;
+ new_ns->prefix = i_strdup(str_c(prefix));
+ new_ns->owner = owner;
+ new_ns->flags = (NAMESPACE_FLAG_SUBSCRIPTIONS & ns->flags) |
+ NAMESPACE_FLAG_LIST_PREFIX | NAMESPACE_FLAG_HIDDEN |
+ NAMESPACE_FLAG_AUTOCREATED | NAMESPACE_FLAG_INBOX_ANY;
+ new_ns->user_set = user->set;
+ new_ns->mail_set = _storage->set;
+ i_array_init(&new_ns->all_storages, 2);
+
+ if (ret <= 0) {
+ get_nonexistent_user_location(storage, userdomain, location);
+ new_ns->flags |= NAMESPACE_FLAG_UNUSABLE;
+ e_debug(ns->user->event,
+ "shared: Tried to access mails of "
+ "nonexistent user %s", userdomain);
+ }
+
+ ns_set = p_new(user->pool, struct mail_namespace_settings, 1);
+ ns_set->type = "shared";
+ ns_set->separator = p_strdup_printf(user->pool, "%c", ns_sep);
+ ns_set->prefix = new_ns->prefix;
+ ns_set->location = p_strdup(user->pool, str_c(location));
+ ns_set->hidden = TRUE;
+ ns_set->list = "yes";
+ new_ns->set = ns_set;
+
+ unexpanded_ns_set =
+ p_new(user->pool, struct mail_namespace_settings, 1);
+ *unexpanded_ns_set = *ns_set;
+ unexpanded_ns_set->location =
+ p_strdup(user->pool, storage->unexpanded_location);
+ new_ns->unexpanded_set = unexpanded_ns_set;
+
+ /* We need to create a prefix="" namespace for the owner */
+ if (mail_namespaces_init_location(owner, str_c(location), &error) < 0) {
+ /* owner gets freed by namespace deinit */
+ mail_namespace_destroy(new_ns);
+ return -1;
+ }
+
+ if (mail_storage_create(new_ns, NULL, _storage->flags |
+ MAIL_STORAGE_FLAG_NO_AUTOVERIFY, &error) < 0) {
+ mailbox_list_set_critical(list, "Namespace '%s': %s",
+ new_ns->prefix, error);
+ /* owner gets freed by namespace deinit */
+ mail_namespace_destroy(new_ns);
+ return -1;
+ }
+ if ((new_ns->flags & NAMESPACE_FLAG_UNUSABLE) == 0 &&
+ !shared_namespace_exists(new_ns)) {
+ /* this user doesn't have a usable storage */
+ new_ns->flags |= NAMESPACE_FLAG_UNUSABLE;
+ }
+ /* mark the shared namespace root as usable, since it now has
+ child namespaces */
+ ns->flags |= NAMESPACE_FLAG_USABLE;
+ *_name = mailbox_list_get_storage_name(new_ns->list,
+ t_strconcat(new_ns->prefix, name, NULL));
+ *_ns = new_ns;
+ if (_storage->class_flags == 0) {
+ /* flags are unset if we were using "auto" storage */
+ _storage->class_flags =
+ mail_namespace_get_default_storage(new_ns)->class_flags;
+ }
+
+ mail_user_add_namespace(user, &new_ns);
+ return 0;
+}
+
+struct mail_storage shared_storage = {
+ .name = MAIL_SHARED_STORAGE_NAME,
+ .class_flags = 0, /* unknown at this point */
+
+ .v = {
+ NULL,
+ shared_storage_alloc,
+ shared_storage_create,
+ index_storage_destroy,
+ NULL,
+ shared_storage_get_list_settings,
+ NULL,
+ fail_mailbox_alloc,
+ NULL,
+ NULL,
+ }
+};
diff --git a/src/lib-storage/index/shared/shared-storage.h b/src/lib-storage/index/shared/shared-storage.h
new file mode 100644
index 0000000..5463b1c
--- /dev/null
+++ b/src/lib-storage/index/shared/shared-storage.h
@@ -0,0 +1,22 @@
+#ifndef SHARED_STORAGE_H
+#define SHARED_STORAGE_H
+
+struct shared_storage {
+ struct mail_storage storage;
+ union mailbox_list_module_context list_module_ctx;
+
+ const char *ns_prefix_pattern;
+ const char *location, *unexpanded_location;
+
+ const char *storage_class_name;
+};
+
+#define SHARED_STORAGE(s) container_of(s, struct shared_storage, storage)
+
+struct mailbox_list *shared_mailbox_list_alloc(void);
+
+/* Returns -1 = error, 0 = user doesn't exist, 1 = ok */
+int shared_storage_get_namespace(struct mail_namespace **_ns,
+ const char **_name);
+
+#endif
diff --git a/src/lib-storage/list/Makefile.am b/src/lib-storage/list/Makefile.am
new file mode 100644
index 0000000..3a15e8e
--- /dev/null
+++ b/src/lib-storage/list/Makefile.am
@@ -0,0 +1,45 @@
+noinst_LTLIBRARIES = libstorage_list.la
+
+AM_CPPFLAGS = \
+ -I$(top_srcdir)/src/lib \
+ -I$(top_srcdir)/src/lib-fs \
+ -I$(top_srcdir)/src/lib-mail \
+ -I$(top_srcdir)/src/lib-index \
+ -I$(top_srcdir)/src/lib-imap \
+ -I$(top_srcdir)/src/lib-storage \
+ -I$(top_srcdir)/src/lib-storage/index
+
+libstorage_list_la_SOURCES = \
+ mail-storage-list-index-rebuild.c \
+ mailbox-list-delete.c \
+ mailbox-list-fs.c \
+ mailbox-list-fs-flags.c \
+ mailbox-list-fs-iter.c \
+ mailbox-list-index.c \
+ mailbox-list-index-backend.c \
+ mailbox-list-index-iter.c \
+ mailbox-list-index-notify.c \
+ mailbox-list-index-status.c \
+ mailbox-list-index-sync.c \
+ mailbox-list-iter.c \
+ mailbox-list-maildir.c \
+ mailbox-list-maildir-iter.c \
+ mailbox-list-none.c \
+ mailbox-list-notify-tree.c \
+ mailbox-list-subscriptions.c \
+ subscription-file.c
+
+headers = \
+ mailbox-list-delete.h \
+ mailbox-list-fs.h \
+ mailbox-list-index.h \
+ mailbox-list-index-storage.h \
+ mailbox-list-index-sync.h \
+ mailbox-list-iter-private.h \
+ mailbox-list-maildir.h \
+ mailbox-list-notify-tree.h \
+ mailbox-list-subscriptions.h \
+ subscription-file.h
+
+pkginc_libdir=$(pkgincludedir)
+pkginc_lib_HEADERS = $(headers)
diff --git a/src/lib-storage/list/Makefile.in b/src/lib-storage/list/Makefile.in
new file mode 100644
index 0000000..9a78a90
--- /dev/null
+++ b/src/lib-storage/list/Makefile.in
@@ -0,0 +1,916 @@
+# Makefile.in generated by automake 1.16.1 from Makefile.am.
+# @configure_input@
+
+# Copyright (C) 1994-2018 Free Software Foundation, Inc.
+
+# This Makefile.in is free software; the Free Software Foundation
+# gives unlimited permission to copy and/or distribute it,
+# with or without modifications, as long as this notice is preserved.
+
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY, to the extent permitted by law; without
+# even the implied warranty of MERCHANTABILITY or FITNESS FOR A
+# PARTICULAR PURPOSE.
+
+@SET_MAKE@
+
+
+VPATH = @srcdir@
+am__is_gnu_make = { \
+ if test -z '$(MAKELEVEL)'; then \
+ false; \
+ elif test -n '$(MAKE_HOST)'; then \
+ true; \
+ elif test -n '$(MAKE_VERSION)' && test -n '$(CURDIR)'; then \
+ true; \
+ else \
+ false; \
+ fi; \
+}
+am__make_running_with_option = \
+ case $${target_option-} in \
+ ?) ;; \
+ *) echo "am__make_running_with_option: internal error: invalid" \
+ "target option '$${target_option-}' specified" >&2; \
+ exit 1;; \
+ esac; \
+ has_opt=no; \
+ sane_makeflags=$$MAKEFLAGS; \
+ if $(am__is_gnu_make); then \
+ sane_makeflags=$$MFLAGS; \
+ else \
+ case $$MAKEFLAGS in \
+ *\\[\ \ ]*) \
+ bs=\\; \
+ sane_makeflags=`printf '%s\n' "$$MAKEFLAGS" \
+ | sed "s/$$bs$$bs[$$bs $$bs ]*//g"`;; \
+ esac; \
+ fi; \
+ skip_next=no; \
+ strip_trailopt () \
+ { \
+ flg=`printf '%s\n' "$$flg" | sed "s/$$1.*$$//"`; \
+ }; \
+ for flg in $$sane_makeflags; do \
+ test $$skip_next = yes && { skip_next=no; continue; }; \
+ case $$flg in \
+ *=*|--*) continue;; \
+ -*I) strip_trailopt 'I'; skip_next=yes;; \
+ -*I?*) strip_trailopt 'I';; \
+ -*O) strip_trailopt 'O'; skip_next=yes;; \
+ -*O?*) strip_trailopt 'O';; \
+ -*l) strip_trailopt 'l'; skip_next=yes;; \
+ -*l?*) strip_trailopt 'l';; \
+ -[dEDm]) skip_next=yes;; \
+ -[JT]) skip_next=yes;; \
+ esac; \
+ case $$flg in \
+ *$$target_option*) has_opt=yes; break;; \
+ esac; \
+ done; \
+ test $$has_opt = yes
+am__make_dryrun = (target_option=n; $(am__make_running_with_option))
+am__make_keepgoing = (target_option=k; $(am__make_running_with_option))
+pkgdatadir = $(datadir)/@PACKAGE@
+pkgincludedir = $(includedir)/@PACKAGE@
+pkglibdir = $(libdir)/@PACKAGE@
+pkglibexecdir = $(libexecdir)/@PACKAGE@
+am__cd = CDPATH="$${ZSH_VERSION+.}$(PATH_SEPARATOR)" && cd
+install_sh_DATA = $(install_sh) -c -m 644
+install_sh_PROGRAM = $(install_sh) -c
+install_sh_SCRIPT = $(install_sh) -c
+INSTALL_HEADER = $(INSTALL_DATA)
+transform = $(program_transform_name)
+NORMAL_INSTALL = :
+PRE_INSTALL = :
+POST_INSTALL = :
+NORMAL_UNINSTALL = :
+PRE_UNINSTALL = :
+POST_UNINSTALL = :
+build_triplet = @build@
+host_triplet = @host@
+subdir = src/lib-storage/list
+ACLOCAL_M4 = $(top_srcdir)/aclocal.m4
+am__aclocal_m4_deps = $(top_srcdir)/m4/ac_checktype2.m4 \
+ $(top_srcdir)/m4/ac_typeof.m4 $(top_srcdir)/m4/arc4random.m4 \
+ $(top_srcdir)/m4/blockdev.m4 $(top_srcdir)/m4/c99_vsnprintf.m4 \
+ $(top_srcdir)/m4/clock_gettime.m4 $(top_srcdir)/m4/crypt.m4 \
+ $(top_srcdir)/m4/crypt_xpg6.m4 $(top_srcdir)/m4/dbqlk.m4 \
+ $(top_srcdir)/m4/dirent_dtype.m4 $(top_srcdir)/m4/dovecot.m4 \
+ $(top_srcdir)/m4/fd_passing.m4 $(top_srcdir)/m4/fdatasync.m4 \
+ $(top_srcdir)/m4/flexible_array_member.m4 \
+ $(top_srcdir)/m4/glibc.m4 $(top_srcdir)/m4/gmtime_max.m4 \
+ $(top_srcdir)/m4/gmtime_tm_gmtoff.m4 \
+ $(top_srcdir)/m4/ioloop.m4 $(top_srcdir)/m4/iovec.m4 \
+ $(top_srcdir)/m4/ipv6.m4 $(top_srcdir)/m4/libcap.m4 \
+ $(top_srcdir)/m4/libtool.m4 $(top_srcdir)/m4/libwrap.m4 \
+ $(top_srcdir)/m4/linux_mremap.m4 $(top_srcdir)/m4/ltoptions.m4 \
+ $(top_srcdir)/m4/ltsugar.m4 $(top_srcdir)/m4/ltversion.m4 \
+ $(top_srcdir)/m4/lt~obsolete.m4 $(top_srcdir)/m4/mmap_write.m4 \
+ $(top_srcdir)/m4/mntctl.m4 $(top_srcdir)/m4/modules.m4 \
+ $(top_srcdir)/m4/notify.m4 $(top_srcdir)/m4/nsl.m4 \
+ $(top_srcdir)/m4/off_t_max.m4 $(top_srcdir)/m4/pkg.m4 \
+ $(top_srcdir)/m4/pr_set_dumpable.m4 \
+ $(top_srcdir)/m4/q_quotactl.m4 $(top_srcdir)/m4/quota.m4 \
+ $(top_srcdir)/m4/random.m4 $(top_srcdir)/m4/rlimit.m4 \
+ $(top_srcdir)/m4/sendfile.m4 $(top_srcdir)/m4/size_t_signed.m4 \
+ $(top_srcdir)/m4/sockpeercred.m4 $(top_srcdir)/m4/sql.m4 \
+ $(top_srcdir)/m4/ssl.m4 $(top_srcdir)/m4/st_tim.m4 \
+ $(top_srcdir)/m4/static_array.m4 $(top_srcdir)/m4/test_with.m4 \
+ $(top_srcdir)/m4/time_t.m4 $(top_srcdir)/m4/typeof.m4 \
+ $(top_srcdir)/m4/typeof_dev_t.m4 \
+ $(top_srcdir)/m4/uoff_t_max.m4 $(top_srcdir)/m4/vararg.m4 \
+ $(top_srcdir)/m4/want_apparmor.m4 \
+ $(top_srcdir)/m4/want_bsdauth.m4 \
+ $(top_srcdir)/m4/want_bzlib.m4 \
+ $(top_srcdir)/m4/want_cassandra.m4 \
+ $(top_srcdir)/m4/want_cdb.m4 \
+ $(top_srcdir)/m4/want_checkpassword.m4 \
+ $(top_srcdir)/m4/want_clucene.m4 $(top_srcdir)/m4/want_db.m4 \
+ $(top_srcdir)/m4/want_gssapi.m4 $(top_srcdir)/m4/want_icu.m4 \
+ $(top_srcdir)/m4/want_ldap.m4 $(top_srcdir)/m4/want_lua.m4 \
+ $(top_srcdir)/m4/want_lz4.m4 $(top_srcdir)/m4/want_lzma.m4 \
+ $(top_srcdir)/m4/want_mysql.m4 $(top_srcdir)/m4/want_pam.m4 \
+ $(top_srcdir)/m4/want_passwd.m4 $(top_srcdir)/m4/want_pgsql.m4 \
+ $(top_srcdir)/m4/want_prefetch.m4 \
+ $(top_srcdir)/m4/want_shadow.m4 \
+ $(top_srcdir)/m4/want_sodium.m4 $(top_srcdir)/m4/want_solr.m4 \
+ $(top_srcdir)/m4/want_sqlite.m4 \
+ $(top_srcdir)/m4/want_stemmer.m4 \
+ $(top_srcdir)/m4/want_systemd.m4 \
+ $(top_srcdir)/m4/want_textcat.m4 \
+ $(top_srcdir)/m4/want_unwind.m4 $(top_srcdir)/m4/want_zlib.m4 \
+ $(top_srcdir)/m4/want_zstd.m4 $(top_srcdir)/configure.ac
+am__configure_deps = $(am__aclocal_m4_deps) $(CONFIGURE_DEPENDENCIES) \
+ $(ACLOCAL_M4)
+DIST_COMMON = $(srcdir)/Makefile.am $(pkginc_lib_HEADERS) \
+ $(am__DIST_COMMON)
+mkinstalldirs = $(install_sh) -d
+CONFIG_HEADER = $(top_builddir)/config.h
+CONFIG_CLEAN_FILES =
+CONFIG_CLEAN_VPATH_FILES =
+LTLIBRARIES = $(noinst_LTLIBRARIES)
+libstorage_list_la_LIBADD =
+am_libstorage_list_la_OBJECTS = mail-storage-list-index-rebuild.lo \
+ mailbox-list-delete.lo mailbox-list-fs.lo \
+ mailbox-list-fs-flags.lo mailbox-list-fs-iter.lo \
+ mailbox-list-index.lo mailbox-list-index-backend.lo \
+ mailbox-list-index-iter.lo mailbox-list-index-notify.lo \
+ mailbox-list-index-status.lo mailbox-list-index-sync.lo \
+ mailbox-list-iter.lo mailbox-list-maildir.lo \
+ mailbox-list-maildir-iter.lo mailbox-list-none.lo \
+ mailbox-list-notify-tree.lo mailbox-list-subscriptions.lo \
+ subscription-file.lo
+libstorage_list_la_OBJECTS = $(am_libstorage_list_la_OBJECTS)
+AM_V_lt = $(am__v_lt_@AM_V@)
+am__v_lt_ = $(am__v_lt_@AM_DEFAULT_V@)
+am__v_lt_0 = --silent
+am__v_lt_1 =
+AM_V_P = $(am__v_P_@AM_V@)
+am__v_P_ = $(am__v_P_@AM_DEFAULT_V@)
+am__v_P_0 = false
+am__v_P_1 = :
+AM_V_GEN = $(am__v_GEN_@AM_V@)
+am__v_GEN_ = $(am__v_GEN_@AM_DEFAULT_V@)
+am__v_GEN_0 = @echo " GEN " $@;
+am__v_GEN_1 =
+AM_V_at = $(am__v_at_@AM_V@)
+am__v_at_ = $(am__v_at_@AM_DEFAULT_V@)
+am__v_at_0 = @
+am__v_at_1 =
+DEFAULT_INCLUDES = -I.@am__isrc@ -I$(top_builddir)
+depcomp = $(SHELL) $(top_srcdir)/depcomp
+am__maybe_remake_depfiles = depfiles
+am__depfiles_remade = ./$(DEPDIR)/mail-storage-list-index-rebuild.Plo \
+ ./$(DEPDIR)/mailbox-list-delete.Plo \
+ ./$(DEPDIR)/mailbox-list-fs-flags.Plo \
+ ./$(DEPDIR)/mailbox-list-fs-iter.Plo \
+ ./$(DEPDIR)/mailbox-list-fs.Plo \
+ ./$(DEPDIR)/mailbox-list-index-backend.Plo \
+ ./$(DEPDIR)/mailbox-list-index-iter.Plo \
+ ./$(DEPDIR)/mailbox-list-index-notify.Plo \
+ ./$(DEPDIR)/mailbox-list-index-status.Plo \
+ ./$(DEPDIR)/mailbox-list-index-sync.Plo \
+ ./$(DEPDIR)/mailbox-list-index.Plo \
+ ./$(DEPDIR)/mailbox-list-iter.Plo \
+ ./$(DEPDIR)/mailbox-list-maildir-iter.Plo \
+ ./$(DEPDIR)/mailbox-list-maildir.Plo \
+ ./$(DEPDIR)/mailbox-list-none.Plo \
+ ./$(DEPDIR)/mailbox-list-notify-tree.Plo \
+ ./$(DEPDIR)/mailbox-list-subscriptions.Plo \
+ ./$(DEPDIR)/subscription-file.Plo
+am__mv = mv -f
+COMPILE = $(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) \
+ $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS)
+LTCOMPILE = $(LIBTOOL) $(AM_V_lt) --tag=CC $(AM_LIBTOOLFLAGS) \
+ $(LIBTOOLFLAGS) --mode=compile $(CC) $(DEFS) \
+ $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) $(CPPFLAGS) \
+ $(AM_CFLAGS) $(CFLAGS)
+AM_V_CC = $(am__v_CC_@AM_V@)
+am__v_CC_ = $(am__v_CC_@AM_DEFAULT_V@)
+am__v_CC_0 = @echo " CC " $@;
+am__v_CC_1 =
+CCLD = $(CC)
+LINK = $(LIBTOOL) $(AM_V_lt) --tag=CC $(AM_LIBTOOLFLAGS) \
+ $(LIBTOOLFLAGS) --mode=link $(CCLD) $(AM_CFLAGS) $(CFLAGS) \
+ $(AM_LDFLAGS) $(LDFLAGS) -o $@
+AM_V_CCLD = $(am__v_CCLD_@AM_V@)
+am__v_CCLD_ = $(am__v_CCLD_@AM_DEFAULT_V@)
+am__v_CCLD_0 = @echo " CCLD " $@;
+am__v_CCLD_1 =
+SOURCES = $(libstorage_list_la_SOURCES)
+DIST_SOURCES = $(libstorage_list_la_SOURCES)
+am__can_run_installinfo = \
+ case $$AM_UPDATE_INFO_DIR in \
+ n|no|NO) false;; \
+ *) (install-info --version) >/dev/null 2>&1;; \
+ esac
+am__vpath_adj_setup = srcdirstrip=`echo "$(srcdir)" | sed 's|.|.|g'`;
+am__vpath_adj = case $$p in \
+ $(srcdir)/*) f=`echo "$$p" | sed "s|^$$srcdirstrip/||"`;; \
+ *) f=$$p;; \
+ esac;
+am__strip_dir = f=`echo $$p | sed -e 's|^.*/||'`;
+am__install_max = 40
+am__nobase_strip_setup = \
+ srcdirstrip=`echo "$(srcdir)" | sed 's/[].[^$$\\*|]/\\\\&/g'`
+am__nobase_strip = \
+ for p in $$list; do echo "$$p"; done | sed -e "s|$$srcdirstrip/||"
+am__nobase_list = $(am__nobase_strip_setup); \
+ for p in $$list; do echo "$$p $$p"; done | \
+ sed "s| $$srcdirstrip/| |;"' / .*\//!s/ .*/ ./; s,\( .*\)/[^/]*$$,\1,' | \
+ $(AWK) 'BEGIN { files["."] = "" } { files[$$2] = files[$$2] " " $$1; \
+ if (++n[$$2] == $(am__install_max)) \
+ { print $$2, files[$$2]; n[$$2] = 0; files[$$2] = "" } } \
+ END { for (dir in files) print dir, files[dir] }'
+am__base_list = \
+ sed '$$!N;$$!N;$$!N;$$!N;$$!N;$$!N;$$!N;s/\n/ /g' | \
+ sed '$$!N;$$!N;$$!N;$$!N;s/\n/ /g'
+am__uninstall_files_from_dir = { \
+ test -z "$$files" \
+ || { test ! -d "$$dir" && test ! -f "$$dir" && test ! -r "$$dir"; } \
+ || { echo " ( cd '$$dir' && rm -f" $$files ")"; \
+ $(am__cd) "$$dir" && rm -f $$files; }; \
+ }
+am__installdirs = "$(DESTDIR)$(pkginc_libdir)"
+HEADERS = $(pkginc_lib_HEADERS)
+am__tagged_files = $(HEADERS) $(SOURCES) $(TAGS_FILES) $(LISP)
+# Read a list of newline-separated strings from the standard input,
+# and print each of them once, without duplicates. Input order is
+# *not* preserved.
+am__uniquify_input = $(AWK) '\
+ BEGIN { nonempty = 0; } \
+ { items[$$0] = 1; nonempty = 1; } \
+ END { if (nonempty) { for (i in items) print i; }; } \
+'
+# Make sure the list of sources is unique. This is necessary because,
+# e.g., the same source file might be shared among _SOURCES variables
+# for different programs/libraries.
+am__define_uniq_tagged_files = \
+ list='$(am__tagged_files)'; \
+ unique=`for i in $$list; do \
+ if test -f "$$i"; then echo $$i; else echo $(srcdir)/$$i; fi; \
+ done | $(am__uniquify_input)`
+ETAGS = etags
+CTAGS = ctags
+am__DIST_COMMON = $(srcdir)/Makefile.in $(top_srcdir)/depcomp
+DISTFILES = $(DIST_COMMON) $(DIST_SOURCES) $(TEXINFOS) $(EXTRA_DIST)
+ACLOCAL = @ACLOCAL@
+ACLOCAL_AMFLAGS = @ACLOCAL_AMFLAGS@
+AMTAR = @AMTAR@
+AM_DEFAULT_VERBOSITY = @AM_DEFAULT_VERBOSITY@
+APPARMOR_LIBS = @APPARMOR_LIBS@
+AR = @AR@
+AUTH_CFLAGS = @AUTH_CFLAGS@
+AUTH_LIBS = @AUTH_LIBS@
+AUTOCONF = @AUTOCONF@
+AUTOHEADER = @AUTOHEADER@
+AUTOMAKE = @AUTOMAKE@
+AWK = @AWK@
+BINARY_CFLAGS = @BINARY_CFLAGS@
+BINARY_LDFLAGS = @BINARY_LDFLAGS@
+BISON = @BISON@
+CASSANDRA_CFLAGS = @CASSANDRA_CFLAGS@
+CASSANDRA_LIBS = @CASSANDRA_LIBS@
+CC = @CC@
+CCDEPMODE = @CCDEPMODE@
+CDB_LIBS = @CDB_LIBS@
+CFLAGS = @CFLAGS@
+CLUCENE_CFLAGS = @CLUCENE_CFLAGS@
+CLUCENE_LIBS = @CLUCENE_LIBS@
+COMPRESS_LIBS = @COMPRESS_LIBS@
+CPP = @CPP@
+CPPFLAGS = @CPPFLAGS@
+CRYPT_LIBS = @CRYPT_LIBS@
+CXX = @CXX@
+CXXCPP = @CXXCPP@
+CXXDEPMODE = @CXXDEPMODE@
+CXXFLAGS = @CXXFLAGS@
+CYGPATH_W = @CYGPATH_W@
+DEFS = @DEFS@
+DEPDIR = @DEPDIR@
+DICT_LIBS = @DICT_LIBS@
+DLLIB = @DLLIB@
+DLLTOOL = @DLLTOOL@
+DSYMUTIL = @DSYMUTIL@
+DUMPBIN = @DUMPBIN@
+ECHO_C = @ECHO_C@
+ECHO_N = @ECHO_N@
+ECHO_T = @ECHO_T@
+EGREP = @EGREP@
+EXEEXT = @EXEEXT@
+FGREP = @FGREP@
+FLEX = @FLEX@
+FUZZER_CPPFLAGS = @FUZZER_CPPFLAGS@
+FUZZER_LDFLAGS = @FUZZER_LDFLAGS@
+GREP = @GREP@
+INSTALL = @INSTALL@
+INSTALL_DATA = @INSTALL_DATA@
+INSTALL_PROGRAM = @INSTALL_PROGRAM@
+INSTALL_SCRIPT = @INSTALL_SCRIPT@
+INSTALL_STRIP_PROGRAM = @INSTALL_STRIP_PROGRAM@
+KRB5CONFIG = @KRB5CONFIG@
+KRB5_CFLAGS = @KRB5_CFLAGS@
+KRB5_LIBS = @KRB5_LIBS@
+LD = @LD@
+LDAP_LIBS = @LDAP_LIBS@
+LDFLAGS = @LDFLAGS@
+LD_NO_WHOLE_ARCHIVE = @LD_NO_WHOLE_ARCHIVE@
+LD_WHOLE_ARCHIVE = @LD_WHOLE_ARCHIVE@
+LIBCAP = @LIBCAP@
+LIBDOVECOT = @LIBDOVECOT@
+LIBDOVECOT_COMPRESS = @LIBDOVECOT_COMPRESS@
+LIBDOVECOT_DEPS = @LIBDOVECOT_DEPS@
+LIBDOVECOT_DSYNC = @LIBDOVECOT_DSYNC@
+LIBDOVECOT_LA_LIBS = @LIBDOVECOT_LA_LIBS@
+LIBDOVECOT_LDA = @LIBDOVECOT_LDA@
+LIBDOVECOT_LDAP = @LIBDOVECOT_LDAP@
+LIBDOVECOT_LIBFTS = @LIBDOVECOT_LIBFTS@
+LIBDOVECOT_LIBFTS_DEPS = @LIBDOVECOT_LIBFTS_DEPS@
+LIBDOVECOT_LOGIN = @LIBDOVECOT_LOGIN@
+LIBDOVECOT_LUA = @LIBDOVECOT_LUA@
+LIBDOVECOT_LUA_DEPS = @LIBDOVECOT_LUA_DEPS@
+LIBDOVECOT_SQL = @LIBDOVECOT_SQL@
+LIBDOVECOT_STORAGE = @LIBDOVECOT_STORAGE@
+LIBDOVECOT_STORAGE_DEPS = @LIBDOVECOT_STORAGE_DEPS@
+LIBEXTTEXTCAT_CFLAGS = @LIBEXTTEXTCAT_CFLAGS@
+LIBEXTTEXTCAT_LIBS = @LIBEXTTEXTCAT_LIBS@
+LIBICONV = @LIBICONV@
+LIBICU_CFLAGS = @LIBICU_CFLAGS@
+LIBICU_LIBS = @LIBICU_LIBS@
+LIBOBJS = @LIBOBJS@
+LIBS = @LIBS@
+LIBSODIUM_CFLAGS = @LIBSODIUM_CFLAGS@
+LIBSODIUM_LIBS = @LIBSODIUM_LIBS@
+LIBTIRPC_CFLAGS = @LIBTIRPC_CFLAGS@
+LIBTIRPC_LIBS = @LIBTIRPC_LIBS@
+LIBTOOL = @LIBTOOL@
+LIBUNWIND_CFLAGS = @LIBUNWIND_CFLAGS@
+LIBUNWIND_LIBS = @LIBUNWIND_LIBS@
+LIBWRAP_LIBS = @LIBWRAP_LIBS@
+LINKED_STORAGE_LDADD = @LINKED_STORAGE_LDADD@
+LIPO = @LIPO@
+LN_S = @LN_S@
+LTLIBICONV = @LTLIBICONV@
+LTLIBOBJS = @LTLIBOBJS@
+LT_SYS_LIBRARY_PATH = @LT_SYS_LIBRARY_PATH@
+LUA_CFLAGS = @LUA_CFLAGS@
+LUA_LIBS = @LUA_LIBS@
+MAINT = @MAINT@
+MAKEINFO = @MAKEINFO@
+MANIFEST_TOOL = @MANIFEST_TOOL@
+MKDIR_P = @MKDIR_P@
+MODULE_LIBS = @MODULE_LIBS@
+MODULE_SUFFIX = @MODULE_SUFFIX@
+MYSQL_CFLAGS = @MYSQL_CFLAGS@
+MYSQL_CONFIG = @MYSQL_CONFIG@
+MYSQL_LIBS = @MYSQL_LIBS@
+NM = @NM@
+NMEDIT = @NMEDIT@
+NOPLUGIN_LDFLAGS = @NOPLUGIN_LDFLAGS@
+OBJDUMP = @OBJDUMP@
+OBJEXT = @OBJEXT@
+OTOOL = @OTOOL@
+OTOOL64 = @OTOOL64@
+PACKAGE = @PACKAGE@
+PACKAGE_BUGREPORT = @PACKAGE_BUGREPORT@
+PACKAGE_NAME = @PACKAGE_NAME@
+PACKAGE_STRING = @PACKAGE_STRING@
+PACKAGE_TARNAME = @PACKAGE_TARNAME@
+PACKAGE_URL = @PACKAGE_URL@
+PACKAGE_VERSION = @PACKAGE_VERSION@
+PANDOC = @PANDOC@
+PATH_SEPARATOR = @PATH_SEPARATOR@
+PGSQL_CFLAGS = @PGSQL_CFLAGS@
+PGSQL_LIBS = @PGSQL_LIBS@
+PG_CONFIG = @PG_CONFIG@
+PIE_CFLAGS = @PIE_CFLAGS@
+PIE_LDFLAGS = @PIE_LDFLAGS@
+PKG_CONFIG = @PKG_CONFIG@
+PKG_CONFIG_LIBDIR = @PKG_CONFIG_LIBDIR@
+PKG_CONFIG_PATH = @PKG_CONFIG_PATH@
+QUOTA_LIBS = @QUOTA_LIBS@
+RANLIB = @RANLIB@
+RELRO_LDFLAGS = @RELRO_LDFLAGS@
+RPCGEN = @RPCGEN@
+RUN_TEST = @RUN_TEST@
+SED = @SED@
+SETTING_FILES = @SETTING_FILES@
+SET_MAKE = @SET_MAKE@
+SHELL = @SHELL@
+SQLITE_CFLAGS = @SQLITE_CFLAGS@
+SQLITE_LIBS = @SQLITE_LIBS@
+SQL_CFLAGS = @SQL_CFLAGS@
+SQL_LIBS = @SQL_LIBS@
+SSL_CFLAGS = @SSL_CFLAGS@
+SSL_LIBS = @SSL_LIBS@
+STRIP = @STRIP@
+SYSTEMD_CFLAGS = @SYSTEMD_CFLAGS@
+SYSTEMD_LIBS = @SYSTEMD_LIBS@
+VALGRIND = @VALGRIND@
+VERSION = @VERSION@
+ZSTD_CFLAGS = @ZSTD_CFLAGS@
+ZSTD_LIBS = @ZSTD_LIBS@
+abs_builddir = @abs_builddir@
+abs_srcdir = @abs_srcdir@
+abs_top_builddir = @abs_top_builddir@
+abs_top_srcdir = @abs_top_srcdir@
+ac_ct_AR = @ac_ct_AR@
+ac_ct_CC = @ac_ct_CC@
+ac_ct_CXX = @ac_ct_CXX@
+ac_ct_DUMPBIN = @ac_ct_DUMPBIN@
+am__include = @am__include@
+am__leading_dot = @am__leading_dot@
+am__quote = @am__quote@
+am__tar = @am__tar@
+am__untar = @am__untar@
+bindir = @bindir@
+build = @build@
+build_alias = @build_alias@
+build_cpu = @build_cpu@
+build_os = @build_os@
+build_vendor = @build_vendor@
+builddir = @builddir@
+datadir = @datadir@
+datarootdir = @datarootdir@
+dict_drivers = @dict_drivers@
+docdir = @docdir@
+dvidir = @dvidir@
+exec_prefix = @exec_prefix@
+host = @host@
+host_alias = @host_alias@
+host_cpu = @host_cpu@
+host_os = @host_os@
+host_vendor = @host_vendor@
+htmldir = @htmldir@
+includedir = @includedir@
+infodir = @infodir@
+install_sh = @install_sh@
+libdir = @libdir@
+libexecdir = @libexecdir@
+localedir = @localedir@
+localstatedir = @localstatedir@
+mandir = @mandir@
+mkdir_p = @mkdir_p@
+moduledir = @moduledir@
+oldincludedir = @oldincludedir@
+pdfdir = @pdfdir@
+prefix = @prefix@
+program_transform_name = @program_transform_name@
+psdir = @psdir@
+rundir = @rundir@
+runstatedir = @runstatedir@
+sbindir = @sbindir@
+sharedstatedir = @sharedstatedir@
+sql_drivers = @sql_drivers@
+srcdir = @srcdir@
+ssldir = @ssldir@
+statedir = @statedir@
+sysconfdir = @sysconfdir@
+systemdservicetype = @systemdservicetype@
+systemdsystemunitdir = @systemdsystemunitdir@
+target_alias = @target_alias@
+top_build_prefix = @top_build_prefix@
+top_builddir = @top_builddir@
+top_srcdir = @top_srcdir@
+noinst_LTLIBRARIES = libstorage_list.la
+AM_CPPFLAGS = \
+ -I$(top_srcdir)/src/lib \
+ -I$(top_srcdir)/src/lib-fs \
+ -I$(top_srcdir)/src/lib-mail \
+ -I$(top_srcdir)/src/lib-index \
+ -I$(top_srcdir)/src/lib-imap \
+ -I$(top_srcdir)/src/lib-storage \
+ -I$(top_srcdir)/src/lib-storage/index
+
+libstorage_list_la_SOURCES = \
+ mail-storage-list-index-rebuild.c \
+ mailbox-list-delete.c \
+ mailbox-list-fs.c \
+ mailbox-list-fs-flags.c \
+ mailbox-list-fs-iter.c \
+ mailbox-list-index.c \
+ mailbox-list-index-backend.c \
+ mailbox-list-index-iter.c \
+ mailbox-list-index-notify.c \
+ mailbox-list-index-status.c \
+ mailbox-list-index-sync.c \
+ mailbox-list-iter.c \
+ mailbox-list-maildir.c \
+ mailbox-list-maildir-iter.c \
+ mailbox-list-none.c \
+ mailbox-list-notify-tree.c \
+ mailbox-list-subscriptions.c \
+ subscription-file.c
+
+headers = \
+ mailbox-list-delete.h \
+ mailbox-list-fs.h \
+ mailbox-list-index.h \
+ mailbox-list-index-storage.h \
+ mailbox-list-index-sync.h \
+ mailbox-list-iter-private.h \
+ mailbox-list-maildir.h \
+ mailbox-list-notify-tree.h \
+ mailbox-list-subscriptions.h \
+ subscription-file.h
+
+pkginc_libdir = $(pkgincludedir)
+pkginc_lib_HEADERS = $(headers)
+all: all-am
+
+.SUFFIXES:
+.SUFFIXES: .c .lo .o .obj
+$(srcdir)/Makefile.in: @MAINTAINER_MODE_TRUE@ $(srcdir)/Makefile.am $(am__configure_deps)
+ @for dep in $?; do \
+ case '$(am__configure_deps)' in \
+ *$$dep*) \
+ ( cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh ) \
+ && { if test -f $@; then exit 0; else break; fi; }; \
+ exit 1;; \
+ esac; \
+ done; \
+ echo ' cd $(top_srcdir) && $(AUTOMAKE) --foreign src/lib-storage/list/Makefile'; \
+ $(am__cd) $(top_srcdir) && \
+ $(AUTOMAKE) --foreign src/lib-storage/list/Makefile
+Makefile: $(srcdir)/Makefile.in $(top_builddir)/config.status
+ @case '$?' in \
+ *config.status*) \
+ cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh;; \
+ *) \
+ echo ' cd $(top_builddir) && $(SHELL) ./config.status $(subdir)/$@ $(am__maybe_remake_depfiles)'; \
+ cd $(top_builddir) && $(SHELL) ./config.status $(subdir)/$@ $(am__maybe_remake_depfiles);; \
+ esac;
+
+$(top_builddir)/config.status: $(top_srcdir)/configure $(CONFIG_STATUS_DEPENDENCIES)
+ cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh
+
+$(top_srcdir)/configure: @MAINTAINER_MODE_TRUE@ $(am__configure_deps)
+ cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh
+$(ACLOCAL_M4): @MAINTAINER_MODE_TRUE@ $(am__aclocal_m4_deps)
+ cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh
+$(am__aclocal_m4_deps):
+
+clean-noinstLTLIBRARIES:
+ -test -z "$(noinst_LTLIBRARIES)" || rm -f $(noinst_LTLIBRARIES)
+ @list='$(noinst_LTLIBRARIES)'; \
+ locs=`for p in $$list; do echo $$p; done | \
+ sed 's|^[^/]*$$|.|; s|/[^/]*$$||; s|$$|/so_locations|' | \
+ sort -u`; \
+ test -z "$$locs" || { \
+ echo rm -f $${locs}; \
+ rm -f $${locs}; \
+ }
+
+libstorage_list.la: $(libstorage_list_la_OBJECTS) $(libstorage_list_la_DEPENDENCIES) $(EXTRA_libstorage_list_la_DEPENDENCIES)
+ $(AM_V_CCLD)$(LINK) $(libstorage_list_la_OBJECTS) $(libstorage_list_la_LIBADD) $(LIBS)
+
+mostlyclean-compile:
+ -rm -f *.$(OBJEXT)
+
+distclean-compile:
+ -rm -f *.tab.c
+
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/mail-storage-list-index-rebuild.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/mailbox-list-delete.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/mailbox-list-fs-flags.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/mailbox-list-fs-iter.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/mailbox-list-fs.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/mailbox-list-index-backend.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/mailbox-list-index-iter.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/mailbox-list-index-notify.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/mailbox-list-index-status.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/mailbox-list-index-sync.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/mailbox-list-index.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/mailbox-list-iter.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/mailbox-list-maildir-iter.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/mailbox-list-maildir.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/mailbox-list-none.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/mailbox-list-notify-tree.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/mailbox-list-subscriptions.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/subscription-file.Plo@am__quote@ # am--include-marker
+
+$(am__depfiles_remade):
+ @$(MKDIR_P) $(@D)
+ @echo '# dummy' >$@-t && $(am__mv) $@-t $@
+
+am--depfiles: $(am__depfiles_remade)
+
+.c.o:
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(COMPILE) -MT $@ -MD -MP -MF $(DEPDIR)/$*.Tpo -c -o $@ $<
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/$*.Tpo $(DEPDIR)/$*.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='$<' object='$@' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(COMPILE) -c -o $@ $<
+
+.c.obj:
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(COMPILE) -MT $@ -MD -MP -MF $(DEPDIR)/$*.Tpo -c -o $@ `$(CYGPATH_W) '$<'`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/$*.Tpo $(DEPDIR)/$*.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='$<' object='$@' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(COMPILE) -c -o $@ `$(CYGPATH_W) '$<'`
+
+.c.lo:
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(LTCOMPILE) -MT $@ -MD -MP -MF $(DEPDIR)/$*.Tpo -c -o $@ $<
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/$*.Tpo $(DEPDIR)/$*.Plo
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='$<' object='$@' libtool=yes @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(LTCOMPILE) -c -o $@ $<
+
+mostlyclean-libtool:
+ -rm -f *.lo
+
+clean-libtool:
+ -rm -rf .libs _libs
+install-pkginc_libHEADERS: $(pkginc_lib_HEADERS)
+ @$(NORMAL_INSTALL)
+ @list='$(pkginc_lib_HEADERS)'; test -n "$(pkginc_libdir)" || list=; \
+ if test -n "$$list"; then \
+ echo " $(MKDIR_P) '$(DESTDIR)$(pkginc_libdir)'"; \
+ $(MKDIR_P) "$(DESTDIR)$(pkginc_libdir)" || exit 1; \
+ fi; \
+ for p in $$list; do \
+ if test -f "$$p"; then d=; else d="$(srcdir)/"; fi; \
+ echo "$$d$$p"; \
+ done | $(am__base_list) | \
+ while read files; do \
+ echo " $(INSTALL_HEADER) $$files '$(DESTDIR)$(pkginc_libdir)'"; \
+ $(INSTALL_HEADER) $$files "$(DESTDIR)$(pkginc_libdir)" || exit $$?; \
+ done
+
+uninstall-pkginc_libHEADERS:
+ @$(NORMAL_UNINSTALL)
+ @list='$(pkginc_lib_HEADERS)'; test -n "$(pkginc_libdir)" || list=; \
+ files=`for p in $$list; do echo $$p; done | sed -e 's|^.*/||'`; \
+ dir='$(DESTDIR)$(pkginc_libdir)'; $(am__uninstall_files_from_dir)
+
+ID: $(am__tagged_files)
+ $(am__define_uniq_tagged_files); mkid -fID $$unique
+tags: tags-am
+TAGS: tags
+
+tags-am: $(TAGS_DEPENDENCIES) $(am__tagged_files)
+ set x; \
+ here=`pwd`; \
+ $(am__define_uniq_tagged_files); \
+ shift; \
+ if test -z "$(ETAGS_ARGS)$$*$$unique"; then :; else \
+ test -n "$$unique" || unique=$$empty_fix; \
+ if test $$# -gt 0; then \
+ $(ETAGS) $(ETAGSFLAGS) $(AM_ETAGSFLAGS) $(ETAGS_ARGS) \
+ "$$@" $$unique; \
+ else \
+ $(ETAGS) $(ETAGSFLAGS) $(AM_ETAGSFLAGS) $(ETAGS_ARGS) \
+ $$unique; \
+ fi; \
+ fi
+ctags: ctags-am
+
+CTAGS: ctags
+ctags-am: $(TAGS_DEPENDENCIES) $(am__tagged_files)
+ $(am__define_uniq_tagged_files); \
+ test -z "$(CTAGS_ARGS)$$unique" \
+ || $(CTAGS) $(CTAGSFLAGS) $(AM_CTAGSFLAGS) $(CTAGS_ARGS) \
+ $$unique
+
+GTAGS:
+ here=`$(am__cd) $(top_builddir) && pwd` \
+ && $(am__cd) $(top_srcdir) \
+ && gtags -i $(GTAGS_ARGS) "$$here"
+cscopelist: cscopelist-am
+
+cscopelist-am: $(am__tagged_files)
+ list='$(am__tagged_files)'; \
+ case "$(srcdir)" in \
+ [\\/]* | ?:[\\/]*) sdir="$(srcdir)" ;; \
+ *) sdir=$(subdir)/$(srcdir) ;; \
+ esac; \
+ for i in $$list; do \
+ if test -f "$$i"; then \
+ echo "$(subdir)/$$i"; \
+ else \
+ echo "$$sdir/$$i"; \
+ fi; \
+ done >> $(top_builddir)/cscope.files
+
+distclean-tags:
+ -rm -f TAGS ID GTAGS GRTAGS GSYMS GPATH tags
+
+distdir: $(BUILT_SOURCES)
+ $(MAKE) $(AM_MAKEFLAGS) distdir-am
+
+distdir-am: $(DISTFILES)
+ @srcdirstrip=`echo "$(srcdir)" | sed 's/[].[^$$\\*]/\\\\&/g'`; \
+ topsrcdirstrip=`echo "$(top_srcdir)" | sed 's/[].[^$$\\*]/\\\\&/g'`; \
+ list='$(DISTFILES)'; \
+ dist_files=`for file in $$list; do echo $$file; done | \
+ sed -e "s|^$$srcdirstrip/||;t" \
+ -e "s|^$$topsrcdirstrip/|$(top_builddir)/|;t"`; \
+ case $$dist_files in \
+ */*) $(MKDIR_P) `echo "$$dist_files" | \
+ sed '/\//!d;s|^|$(distdir)/|;s,/[^/]*$$,,' | \
+ sort -u` ;; \
+ esac; \
+ for file in $$dist_files; do \
+ if test -f $$file || test -d $$file; then d=.; else d=$(srcdir); fi; \
+ if test -d $$d/$$file; then \
+ dir=`echo "/$$file" | sed -e 's,/[^/]*$$,,'`; \
+ if test -d "$(distdir)/$$file"; then \
+ find "$(distdir)/$$file" -type d ! -perm -700 -exec chmod u+rwx {} \;; \
+ fi; \
+ if test -d $(srcdir)/$$file && test $$d != $(srcdir); then \
+ cp -fpR $(srcdir)/$$file "$(distdir)$$dir" || exit 1; \
+ find "$(distdir)/$$file" -type d ! -perm -700 -exec chmod u+rwx {} \;; \
+ fi; \
+ cp -fpR $$d/$$file "$(distdir)$$dir" || exit 1; \
+ else \
+ test -f "$(distdir)/$$file" \
+ || cp -p $$d/$$file "$(distdir)/$$file" \
+ || exit 1; \
+ fi; \
+ done
+check-am: all-am
+check: check-am
+all-am: Makefile $(LTLIBRARIES) $(HEADERS)
+installdirs:
+ for dir in "$(DESTDIR)$(pkginc_libdir)"; do \
+ test -z "$$dir" || $(MKDIR_P) "$$dir"; \
+ done
+install: install-am
+install-exec: install-exec-am
+install-data: install-data-am
+uninstall: uninstall-am
+
+install-am: all-am
+ @$(MAKE) $(AM_MAKEFLAGS) install-exec-am install-data-am
+
+installcheck: installcheck-am
+install-strip:
+ if test -z '$(STRIP)'; then \
+ $(MAKE) $(AM_MAKEFLAGS) INSTALL_PROGRAM="$(INSTALL_STRIP_PROGRAM)" \
+ install_sh_PROGRAM="$(INSTALL_STRIP_PROGRAM)" INSTALL_STRIP_FLAG=-s \
+ install; \
+ else \
+ $(MAKE) $(AM_MAKEFLAGS) INSTALL_PROGRAM="$(INSTALL_STRIP_PROGRAM)" \
+ install_sh_PROGRAM="$(INSTALL_STRIP_PROGRAM)" INSTALL_STRIP_FLAG=-s \
+ "INSTALL_PROGRAM_ENV=STRIPPROG='$(STRIP)'" install; \
+ fi
+mostlyclean-generic:
+
+clean-generic:
+
+distclean-generic:
+ -test -z "$(CONFIG_CLEAN_FILES)" || rm -f $(CONFIG_CLEAN_FILES)
+ -test . = "$(srcdir)" || test -z "$(CONFIG_CLEAN_VPATH_FILES)" || rm -f $(CONFIG_CLEAN_VPATH_FILES)
+
+maintainer-clean-generic:
+ @echo "This command is intended for maintainers to use"
+ @echo "it deletes files that may require special tools to rebuild."
+clean: clean-am
+
+clean-am: clean-generic clean-libtool clean-noinstLTLIBRARIES \
+ mostlyclean-am
+
+distclean: distclean-am
+ -rm -f ./$(DEPDIR)/mail-storage-list-index-rebuild.Plo
+ -rm -f ./$(DEPDIR)/mailbox-list-delete.Plo
+ -rm -f ./$(DEPDIR)/mailbox-list-fs-flags.Plo
+ -rm -f ./$(DEPDIR)/mailbox-list-fs-iter.Plo
+ -rm -f ./$(DEPDIR)/mailbox-list-fs.Plo
+ -rm -f ./$(DEPDIR)/mailbox-list-index-backend.Plo
+ -rm -f ./$(DEPDIR)/mailbox-list-index-iter.Plo
+ -rm -f ./$(DEPDIR)/mailbox-list-index-notify.Plo
+ -rm -f ./$(DEPDIR)/mailbox-list-index-status.Plo
+ -rm -f ./$(DEPDIR)/mailbox-list-index-sync.Plo
+ -rm -f ./$(DEPDIR)/mailbox-list-index.Plo
+ -rm -f ./$(DEPDIR)/mailbox-list-iter.Plo
+ -rm -f ./$(DEPDIR)/mailbox-list-maildir-iter.Plo
+ -rm -f ./$(DEPDIR)/mailbox-list-maildir.Plo
+ -rm -f ./$(DEPDIR)/mailbox-list-none.Plo
+ -rm -f ./$(DEPDIR)/mailbox-list-notify-tree.Plo
+ -rm -f ./$(DEPDIR)/mailbox-list-subscriptions.Plo
+ -rm -f ./$(DEPDIR)/subscription-file.Plo
+ -rm -f Makefile
+distclean-am: clean-am distclean-compile distclean-generic \
+ distclean-tags
+
+dvi: dvi-am
+
+dvi-am:
+
+html: html-am
+
+html-am:
+
+info: info-am
+
+info-am:
+
+install-data-am: install-pkginc_libHEADERS
+
+install-dvi: install-dvi-am
+
+install-dvi-am:
+
+install-exec-am:
+
+install-html: install-html-am
+
+install-html-am:
+
+install-info: install-info-am
+
+install-info-am:
+
+install-man:
+
+install-pdf: install-pdf-am
+
+install-pdf-am:
+
+install-ps: install-ps-am
+
+install-ps-am:
+
+installcheck-am:
+
+maintainer-clean: maintainer-clean-am
+ -rm -f ./$(DEPDIR)/mail-storage-list-index-rebuild.Plo
+ -rm -f ./$(DEPDIR)/mailbox-list-delete.Plo
+ -rm -f ./$(DEPDIR)/mailbox-list-fs-flags.Plo
+ -rm -f ./$(DEPDIR)/mailbox-list-fs-iter.Plo
+ -rm -f ./$(DEPDIR)/mailbox-list-fs.Plo
+ -rm -f ./$(DEPDIR)/mailbox-list-index-backend.Plo
+ -rm -f ./$(DEPDIR)/mailbox-list-index-iter.Plo
+ -rm -f ./$(DEPDIR)/mailbox-list-index-notify.Plo
+ -rm -f ./$(DEPDIR)/mailbox-list-index-status.Plo
+ -rm -f ./$(DEPDIR)/mailbox-list-index-sync.Plo
+ -rm -f ./$(DEPDIR)/mailbox-list-index.Plo
+ -rm -f ./$(DEPDIR)/mailbox-list-iter.Plo
+ -rm -f ./$(DEPDIR)/mailbox-list-maildir-iter.Plo
+ -rm -f ./$(DEPDIR)/mailbox-list-maildir.Plo
+ -rm -f ./$(DEPDIR)/mailbox-list-none.Plo
+ -rm -f ./$(DEPDIR)/mailbox-list-notify-tree.Plo
+ -rm -f ./$(DEPDIR)/mailbox-list-subscriptions.Plo
+ -rm -f ./$(DEPDIR)/subscription-file.Plo
+ -rm -f Makefile
+maintainer-clean-am: distclean-am maintainer-clean-generic
+
+mostlyclean: mostlyclean-am
+
+mostlyclean-am: mostlyclean-compile mostlyclean-generic \
+ mostlyclean-libtool
+
+pdf: pdf-am
+
+pdf-am:
+
+ps: ps-am
+
+ps-am:
+
+uninstall-am: uninstall-pkginc_libHEADERS
+
+.MAKE: install-am install-strip
+
+.PHONY: CTAGS GTAGS TAGS all all-am am--depfiles check check-am clean \
+ clean-generic clean-libtool clean-noinstLTLIBRARIES \
+ cscopelist-am ctags ctags-am distclean distclean-compile \
+ distclean-generic distclean-libtool distclean-tags distdir dvi \
+ dvi-am html html-am info info-am install install-am \
+ install-data install-data-am install-dvi install-dvi-am \
+ install-exec install-exec-am install-html install-html-am \
+ install-info install-info-am install-man install-pdf \
+ install-pdf-am install-pkginc_libHEADERS install-ps \
+ install-ps-am install-strip installcheck installcheck-am \
+ installdirs maintainer-clean maintainer-clean-generic \
+ mostlyclean mostlyclean-compile mostlyclean-generic \
+ mostlyclean-libtool pdf pdf-am ps ps-am tags tags-am uninstall \
+ uninstall-am uninstall-pkginc_libHEADERS
+
+.PRECIOUS: Makefile
+
+
+# Tell versions [3.59,3.63) of GNU make to not export all variables.
+# Otherwise a system limit (for SysV at least) may be exceeded.
+.NOEXPORT:
diff --git a/src/lib-storage/list/mail-storage-list-index-rebuild.c b/src/lib-storage/list/mail-storage-list-index-rebuild.c
new file mode 100644
index 0000000..1e4f462
--- /dev/null
+++ b/src/lib-storage/list/mail-storage-list-index-rebuild.c
@@ -0,0 +1,510 @@
+/* Copyright (c) 2021 Dovecot Authors, see the included COPYING file */
+
+#include "lib.h"
+#include "hash.h"
+#include "guid.h"
+#include "str.h"
+#include "str-sanitize.h"
+#include "hex-binary.h"
+#include "randgen.h"
+#include "fs-api.h"
+#include "mailbox-list-index.h"
+#include "mailbox-list-index-sync.h"
+#include "mail-storage-private.h"
+
+struct mail_storage_list_index_rebuild_mailbox {
+ guid_128_t guid;
+ const char *index_name;
+};
+
+struct mail_storage_list_index_rebuild_ns {
+ struct mail_namespace *ns;
+ struct mailbox_list_index_sync_context *list_sync_ctx;
+};
+
+struct mail_storage_list_index_rebuild_ctx {
+ struct mail_storage *storage;
+ pool_t pool;
+ struct mailbox_list *first_list;
+ HASH_TABLE(uint8_t *, struct mail_storage_list_index_rebuild_mailbox *) mailboxes;
+ ARRAY(struct mail_storage_list_index_rebuild_ns) rebuild_namespaces;
+};
+
+static bool
+mail_storage_list_index_rebuild_get_namespaces(struct mail_storage_list_index_rebuild_ctx *ctx)
+{
+ struct mail_namespace *ns;
+ struct mail_storage_list_index_rebuild_ns *rebuild_ns;
+
+ p_array_init(&ctx->rebuild_namespaces, ctx->pool, 4);
+ for (ns = ctx->storage->user->namespaces; ns != NULL; ns = ns->next) {
+ if (ns->storage != ctx->storage ||
+ ns->alias_for != NULL)
+ continue;
+
+ /* ignore any non-INDEX layout */
+ if (strcmp(ns->list->name, MAILBOX_LIST_NAME_INDEX) != 0)
+ continue;
+
+ /* track first list */
+ if (ctx->first_list == NULL)
+ ctx->first_list = ns->list;
+
+ rebuild_ns = array_append_space(&ctx->rebuild_namespaces);
+ rebuild_ns->ns = ns;
+ }
+
+ return array_count(&ctx->rebuild_namespaces) > 0;
+}
+
+static int rebuild_ns_cmp(const struct mail_storage_list_index_rebuild_ns *ns1,
+ const struct mail_storage_list_index_rebuild_ns *ns2)
+{
+ return strcmp(ns1->ns->prefix, ns2->ns->prefix);
+}
+
+static int
+mail_storage_list_index_rebuild_lock_lists(struct mail_storage_list_index_rebuild_ctx *ctx)
+{
+ struct mail_storage_list_index_rebuild_ns *rebuild_ns;
+
+ /* sort to make sure all processes lock the lists in the same order
+ to avoid deadlocks. this should be the only place that locks more
+ than one list. */
+ array_sort(&ctx->rebuild_namespaces, rebuild_ns_cmp);
+
+ array_foreach_modifiable(&ctx->rebuild_namespaces, rebuild_ns) {
+ if (mailbox_list_index_sync_begin(rebuild_ns->ns->list,
+ &rebuild_ns->list_sync_ctx) < 0) {
+ mail_storage_copy_list_error(ctx->storage,
+ rebuild_ns->ns->list);
+ return -1;
+ }
+ }
+ return 0;
+}
+
+static void
+mail_storage_list_index_rebuild_unlock_lists(struct mail_storage_list_index_rebuild_ctx *ctx)
+{
+ struct mail_storage_list_index_rebuild_ns *rebuild_ns;
+
+ array_foreach_modifiable(&ctx->rebuild_namespaces, rebuild_ns) {
+ if (rebuild_ns->list_sync_ctx != NULL)
+ (void)mailbox_list_index_sync_end(&rebuild_ns->list_sync_ctx, TRUE);
+ }
+}
+
+static int
+mail_storage_list_index_fill_storage_mailboxes(struct mail_storage_list_index_rebuild_ctx *ctx)
+{
+ struct mail_storage_list_index_rebuild_mailbox *box;
+ struct fs_iter *iter;
+ const char *path, *fname, *error;
+ guid_128_t guid;
+ uint8_t *guid_p;
+
+ path = mailbox_list_get_root_forced(ctx->first_list,
+ MAILBOX_LIST_PATH_TYPE_MAILBOX);
+ iter = fs_iter_init_with_event(ctx->storage->mailboxes_fs,
+ ctx->storage->event, path,
+ FS_ITER_FLAG_DIRS | FS_ITER_FLAG_NOCACHE);
+ while ((fname = fs_iter_next(iter)) != NULL) {
+ if (guid_128_from_string(fname, guid) < 0)
+ continue;
+
+ box = p_new(ctx->pool, struct mail_storage_list_index_rebuild_mailbox, 1);
+ guid_128_copy(box->guid, guid);
+ guid_p = box->guid;
+ hash_table_update(ctx->mailboxes, guid_p, box);
+ }
+
+ if (fs_iter_deinit(&iter, &error) < 0) {
+ mail_storage_set_critical(ctx->storage,
+ "List rebuild: fs_iter_deinit(%s) failed: %s", path,
+ error);
+ return -1;
+ }
+ return 0;
+}
+
+static int
+mail_storage_list_remove_duplicate(struct mail_storage_list_index_rebuild_ctx *ctx,
+ struct mail_storage_list_index_rebuild_ns *rebuild_ns,
+ struct mailbox *box,
+ struct mail_storage_list_index_rebuild_mailbox *rebuild_box)
+{
+ const char *delete_name, *keep_name;
+
+ if (strcmp(box->list->name, MAILBOX_LIST_NAME_INDEX) != 0) {
+ /* we're not using LAYOUT=index. not really supported now,
+ but just ignore that in here. */
+ return 0;
+ }
+ /* we'll need to delete one of these entries. if one of them begins with
+ "lost-", remove it. otherwise just pick one of them randomly. */
+ if (strncmp(box->name, ctx->storage->lost_mailbox_prefix,
+ strlen(ctx->storage->lost_mailbox_prefix)) == 0) {
+ delete_name = box->name;
+ keep_name = rebuild_box->index_name;
+ } else {
+ delete_name = rebuild_box->index_name;
+ keep_name = p_strdup(ctx->pool, box->name);
+ }
+
+ e_debug(ctx->storage->event,
+ "Removing duplicate mailbox '%s' in favor of mailbox '%s'",
+ str_sanitize(delete_name, 128), str_sanitize(keep_name, 128));
+
+ if (mailbox_list_index_sync_delete(rebuild_ns->list_sync_ctx,
+ delete_name, TRUE) < 0) {
+ mail_storage_set_critical(ctx->storage,
+ "List rebuild: Couldn't delete duplicate mailbox list index entry %s: %s",
+ delete_name, mailbox_list_get_last_internal_error(box->list, NULL));
+ return -1;
+ }
+ e_warning(box->event, "List rebuild: Duplicated mailbox GUID %s found - deleting mailbox entry %s (and keeping %s)",
+ guid_128_to_string(rebuild_box->guid), delete_name, keep_name);
+ rebuild_box->index_name = keep_name;
+ return 0;
+}
+
+static int
+mail_storage_list_index_find_indexed_mailbox(struct mail_storage_list_index_rebuild_ctx *ctx,
+ struct mail_storage_list_index_rebuild_ns *rebuild_ns,
+ const struct mailbox_info *info)
+{
+ struct mail_storage_list_index_rebuild_mailbox *rebuild_box;
+ struct mailbox *box;
+ struct mailbox_metadata metadata;
+ const uint8_t *guid_p;
+ int ret = 0;
+
+ if ((info->flags & (MAILBOX_NOSELECT | MAILBOX_NONEXISTENT)) != 0)
+ return 0;
+
+ box = mailbox_alloc(info->ns->list, info->vname, MAILBOX_FLAG_IGNORE_ACLS);
+ if (mailbox_get_metadata(box, MAILBOX_METADATA_GUID, &metadata) < 0) {
+ mail_storage_set_critical(rebuild_ns->ns->storage,
+ "List rebuild: Couldn't lookup mailbox %s GUID: %s",
+ info->vname, mailbox_get_last_internal_error(box, NULL));
+ ret = -1;
+ } else {
+ guid_p = metadata.guid;
+ rebuild_box = hash_table_lookup(ctx->mailboxes, guid_p);
+ if (rebuild_box == NULL) {
+ /* indexed but doesn't exist in storage. shouldn't
+ happen normally, but it'll be created when it gets
+ accessed. */
+ e_debug(box->event,
+ "Mailbox GUID %s exists in list index, but not in storage",
+ guid_128_to_string(guid_p));
+ } else if (rebuild_box->index_name == NULL) {
+ rebuild_box->index_name =
+ p_strdup(ctx->pool, box->name);
+ e_debug(box->event,
+ "Mailbox GUID %s exists in list index and in storage",
+ guid_128_to_string(guid_p));
+ } else {
+ /* duplicate GUIDs in index. in theory this could be
+ possible because of mailbox aliases, but we don't
+ support that for now. especially dsync doesn't like
+ duplicates. */
+ if (mail_storage_list_remove_duplicate(ctx, rebuild_ns,
+ box, rebuild_box) < 0)
+ ret = -1;
+ }
+ }
+ mailbox_free(&box);
+ return ret;
+}
+
+static int
+mail_storage_list_index_find_indexed_mailboxes(struct mail_storage_list_index_rebuild_ctx *ctx,
+ struct mail_storage_list_index_rebuild_ns *rebuild_ns)
+{
+ struct mailbox_list_iterate_context *iter;
+ const struct mailbox_info *info;
+ int ret = 0;
+
+ iter = mailbox_list_iter_init(rebuild_ns->ns->list, "*",
+ MAILBOX_LIST_ITER_RAW_LIST |
+ MAILBOX_LIST_ITER_NO_AUTO_BOXES |
+ MAILBOX_LIST_ITER_SKIP_ALIASES);
+ while (ret == 0 && (info = mailbox_list_iter_next(iter)) != NULL) T_BEGIN {
+ ret = mail_storage_list_index_find_indexed_mailbox(ctx, rebuild_ns, info);
+ } T_END;
+ if (mailbox_list_iter_deinit(&iter) < 0) {
+ mail_storage_set_critical(rebuild_ns->ns->storage,
+ "List rebuild: Failed to iterate mailboxes: %s",
+ mailbox_list_get_last_internal_error(rebuild_ns->ns->list, NULL));
+ return -1;
+ }
+ return ret;
+}
+
+static int
+mail_storage_list_mailbox_create(struct mailbox *box,
+ const struct mailbox_update *update)
+{
+ e_debug(box->event, "Attempting to create mailbox");
+ if (mailbox_create(box, update, FALSE) == 0)
+ return 1;
+
+ if (mailbox_get_last_mail_error(box) == MAIL_ERROR_NOTFOUND) {
+ /* if this is because mailbox was marked as deleted,
+ undelete it and retry. */
+ if (mailbox_mark_index_deleted(box, FALSE) < 0)
+ return -1;
+ if (mailbox_create(box, update, FALSE) == 0)
+ return 1;
+ }
+ if (mailbox_get_last_mail_error(box) == MAIL_ERROR_EXISTS)
+ return 0;
+ mailbox_set_critical(box,
+ "List rebuild: Couldn't create mailbox %s: %s",
+ mailbox_get_vname(box), mailbox_get_last_internal_error(box, NULL));
+ return -1;
+}
+
+static int
+mail_storage_list_index_try_create(struct mail_storage_list_index_rebuild_ctx *ctx,
+ const uint8_t *guid_p,
+ bool retry)
+{
+ struct mail_storage *storage = ctx->storage;
+ struct mailbox_list *list;
+ struct mailbox *box;
+ struct mailbox_update update;
+ enum mailbox_existence existence;
+ string_t *name = t_str_new(128);
+ unsigned char randomness[8];
+ int ret;
+
+ /* FIXME: we should find out the mailbox's original namespace from the
+ mailbox index's header. */
+ list = ctx->first_list;
+
+ i_zero(&update);
+ guid_128_copy(update.mailbox_guid, guid_p);
+
+ str_printfa(name, "%s%s%s", list->ns->prefix,
+ storage->lost_mailbox_prefix, guid_128_to_string(guid_p));
+ if (retry) {
+ random_fill(randomness, sizeof(randomness));
+ str_append_c(name, '-');
+ binary_to_hex_append(name, randomness, sizeof(randomness));
+ }
+ /* ignore ACLs to avoid interference */
+ box = mailbox_alloc(list, str_c(name), MAILBOX_FLAG_IGNORE_ACLS);
+ e_debug(box->event, "Mailbox GUID %s exists in storage, but not in list index",
+ guid_128_to_string(guid_p));
+
+ box->corrupted_mailbox_name = TRUE;
+ if (mailbox_exists(box, FALSE, &existence) < 0) {
+ mail_storage_set_critical(storage,
+ "List rebuild: Couldn't lookup mailbox %s existence: %s",
+ str_c(name), mailbox_get_last_internal_error(box, NULL));
+ ret = -1;
+ } else if (existence != MAILBOX_EXISTENCE_NONE) {
+ ret = 0;
+ } else if ((ret = mail_storage_list_mailbox_create(box, &update)) <= 0)
+ ;
+ else if (mailbox_sync(box, MAILBOX_SYNC_FLAG_FORCE_RESYNC) < 0) {
+ mail_storage_set_critical(storage,
+ "List rebuild: Couldn't force resync on created mailbox %s: %s",
+ str_c(name), mailbox_get_last_internal_error(box, NULL));
+ ret = -1;
+ }
+ mailbox_free(&box);
+
+ if (ret < 0)
+ return ret;
+
+ /* open a second time to rename the mailbox to its original name,
+ ignore ACLs to avoid interference. */
+ box = mailbox_alloc(list, str_c(name), MAILBOX_FLAG_IGNORE_ACLS);
+ e_debug(box->event, "Attempting to recover original name");
+ if (mailbox_open(box) < 0 &&
+ mailbox_get_last_mail_error(box) != MAIL_ERROR_NOTFOUND) {
+ mail_storage_set_critical(storage,
+ "List rebuild: Couldn't open recovered mailbox %s: %s",
+ str_c(name), mailbox_get_last_internal_error(box, NULL));
+ ret = -1;
+ }
+ mailbox_free(&box);
+ return ret;
+}
+
+static int
+mail_storage_list_index_create(struct mail_storage_list_index_rebuild_ctx *ctx,
+ const uint8_t *guid_p)
+{
+ int i, ret = 0;
+
+ for (i = 0; i < 100; i++) {
+ T_BEGIN {
+ ret = mail_storage_list_index_try_create(ctx, guid_p, i > 0);
+ } T_END;
+ if (ret != 0)
+ return ret;
+ }
+ mail_storage_set_critical(ctx->storage,
+ "List rebuild: Failed to create a new mailbox name for GUID %s - "
+ "everything seems to exist?",
+ guid_128_to_string(guid_p));
+ return -1;
+}
+
+static int mail_storage_list_index_add_missing(struct mail_storage_list_index_rebuild_ctx *ctx)
+{
+ struct hash_iterate_context *iter;
+ struct mail_storage_list_index_rebuild_mailbox *box;
+ uint8_t *guid_p;
+ unsigned int num_created = 0;
+ int ret = 0;
+
+ iter = hash_table_iterate_init(ctx->mailboxes);
+ while (hash_table_iterate(iter, ctx->mailboxes, &guid_p, &box)) T_BEGIN {
+ if (box->index_name == NULL) {
+ if (mail_storage_list_index_create(ctx, guid_p) < 0)
+ ret = -1;
+ else
+ num_created++;
+ }
+ } T_END;
+ hash_table_iterate_deinit(&iter);
+ if (num_created > 0) {
+ e_warning(ctx->storage->event,
+ "Mailbox list rescan found %u lost mailboxes",
+ num_created);
+ }
+ return ret;
+}
+
+static int mail_storage_list_index_rebuild_ctx(struct mail_storage_list_index_rebuild_ctx *ctx)
+{
+ struct mail_storage_list_index_rebuild_ns *rebuild_ns;
+
+ if (mail_storage_list_index_fill_storage_mailboxes(ctx) < 0)
+ return -1;
+
+ array_foreach_modifiable(&ctx->rebuild_namespaces, rebuild_ns) {
+ e_debug(ctx->storage->event,
+ "Rebuilding list index for namespace '%s'",
+ rebuild_ns->ns->prefix);
+ if (mail_storage_list_index_find_indexed_mailboxes(ctx, rebuild_ns) < 0)
+ return -1;
+ }
+
+ /* finish list syncing before creating mailboxes, because
+ mailbox_create() will internally try to re-acquire the lock.
+ (alternatively we could just add the mailbox to the list index
+ directly, but that's could cause problems as well.) */
+ mail_storage_list_index_rebuild_unlock_lists(ctx);
+ if (mail_storage_list_index_add_missing(ctx) < 0)
+ return -1;
+ return 0;
+}
+
+static int mail_storage_list_index_rebuild_int(struct mail_storage *storage)
+{
+ struct mail_storage_list_index_rebuild_ctx ctx;
+ int ret;
+
+ if (storage->mailboxes_fs == NULL) {
+ storage->rebuild_list_index = FALSE;
+ mail_storage_set_critical(storage,
+ "BUG: Can't rebuild mailbox list index: "
+ "Missing mailboxes_fs");
+ return 0;
+ }
+
+ if (storage->rebuilding_list_index)
+ return 0;
+ storage->rebuilding_list_index = TRUE;
+
+ i_zero(&ctx);
+ ctx.storage = storage;
+ ctx.pool = pool_alloconly_create("mailbox list index rebuild", 10240);
+ hash_table_create(&ctx.mailboxes, ctx.pool, 0, guid_128_hash, guid_128_cmp);
+
+ /* if no namespaces are found, do nothing */
+ if (!mail_storage_list_index_rebuild_get_namespaces(&ctx)) {
+ hash_table_destroy(&ctx.mailboxes);
+ pool_unref(&ctx.pool);
+ return 0;
+ }
+
+ /* Only perform this for INDEX layout */
+ if (strcmp(ctx.first_list->name, MAILBOX_LIST_NAME_INDEX) == 0) {
+ /* do this operation while keeping mailbox list index locked.
+ this avoids race conditions between other list rebuilds and also
+ makes sure that other processes creating/deleting mailboxes can't
+ cause confusion with race conditions. */
+ struct event_reason *reason =
+ event_reason_begin("storage:mailbox_list_rebuild");
+ if ((ret = mail_storage_list_index_rebuild_lock_lists(&ctx)) == 0)
+ ret = mail_storage_list_index_rebuild_ctx(&ctx);
+ mail_storage_list_index_rebuild_unlock_lists(&ctx);
+ event_reason_end(&reason);
+ } else
+ ret = 0;
+
+ hash_table_destroy(&ctx.mailboxes);
+ pool_unref(&ctx.pool);
+
+ if (ret == 0)
+ storage->rebuild_list_index = FALSE;
+ storage->rebuilding_list_index = FALSE;
+ return ret;
+}
+
+int mail_storage_list_index_rebuild_and_set_uncorrupted(struct mail_storage *storage)
+{
+ struct mail_namespace *ns;
+ int ret = 0;
+
+ /* If mailbox list index is disabled, stop any attempt already here.
+ This saves some allocations and iterating all namespaces. */
+ if (!storage->set->mailbox_list_index) {
+ storage->rebuild_list_index = FALSE;
+ return 0;
+ }
+
+ if (mail_storage_list_index_rebuild_int(storage) < 0)
+ return -1;
+ for (ns = storage->user->namespaces; ns != NULL; ns = ns->next) {
+ if (ns->storage != storage || ns->alias_for != NULL)
+ continue;
+ if (mailbox_list_index_set_uncorrupted(ns->list) < 0)
+ ret = -1;
+ }
+ return ret;
+}
+
+int mail_storage_list_index_rebuild(struct mail_storage *storage,
+ enum mail_storage_list_index_rebuild_reason reason)
+{
+ /* If mailbox list index is disabled, stop any attempt already here. */
+ if (!storage->set->mailbox_list_index) {
+ storage->rebuild_list_index = FALSE;
+ return 0;
+ }
+
+ switch (reason) {
+ case MAIL_STORAGE_LIST_INDEX_REBUILD_REASON_CORRUPTED:
+ e_warning(storage->event,
+ "Mailbox list index marked corrupted - rescanning");
+ break;
+ case MAIL_STORAGE_LIST_INDEX_REBUILD_REASON_FORCE_RESYNC:
+ e_debug(storage->event,
+ "Mailbox list index rebuild due to force resync");
+ break;
+ case MAIL_STORAGE_LIST_INDEX_REBUILD_REASON_NO_INBOX:
+ e_debug(storage->event,
+ "Mailbox list index rebuild due to no INBOX");
+ break;
+ }
+ return mail_storage_list_index_rebuild_int(storage);
+}
diff --git a/src/lib-storage/list/mailbox-list-delete.c b/src/lib-storage/list/mailbox-list-delete.c
new file mode 100644
index 0000000..ca773ee
--- /dev/null
+++ b/src/lib-storage/list/mailbox-list-delete.c
@@ -0,0 +1,489 @@
+/* Copyright (c) 2010-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "str.h"
+#include "hex-binary.h"
+#include "hostpid.h"
+#include "randgen.h"
+#include "sleep.h"
+#include "unlink-directory.h"
+#include "mailbox-list-private.h"
+#include "mailbox-list-delete.h"
+
+#include <stdio.h>
+#include <dirent.h>
+#include <unistd.h>
+
+static int
+mailbox_list_check_root_delete(struct mailbox_list *list, const char *name,
+ const char *path)
+{
+ const char *root_dir;
+
+ root_dir = mailbox_list_get_root_forced(list, MAILBOX_LIST_PATH_TYPE_DIR);
+ if (strcmp(root_dir, path) != 0)
+ return 0;
+
+ if (strcmp(name, "INBOX") == 0 &&
+ (list->ns->flags & NAMESPACE_FLAG_INBOX_ANY) != 0) {
+ mailbox_list_set_error(list, MAIL_ERROR_NOTPOSSIBLE,
+ "INBOX can't be deleted.");
+ return -1;
+ }
+ mailbox_list_set_error(list, MAIL_ERROR_NOTPOSSIBLE,
+ "Mail storage root can't be deleted.");
+ return -1;
+}
+
+static const char *unique_fname(void)
+{
+ unsigned char randbuf[8];
+
+ random_fill(randbuf, sizeof(randbuf));
+ return t_strdup_printf("%s.%s.%s", my_hostname, my_pid,
+ binary_to_hex(randbuf, sizeof(randbuf)));
+
+}
+
+int mailbox_list_delete_maildir_via_trash(struct mailbox_list *list,
+ const char *name,
+ const char *trash_dir)
+{
+ const char *src, *trash_dest, *error;
+ unsigned int count;
+
+ if (mailbox_list_get_path(list, name, MAILBOX_LIST_PATH_TYPE_MAILBOX,
+ &src) <= 0)
+ i_unreached();
+ if (mailbox_list_check_root_delete(list, name, src) < 0)
+ return -1;
+
+ /* rename the mailbox dir to trash dir, which atomically
+ marks it as being deleted. */
+ count = 0; trash_dest = trash_dir;
+ for (; rename(src, trash_dest) < 0; count++) {
+ if (ENOTFOUND(errno)) {
+ if (trash_dest != trash_dir && count < 5) {
+ /* either the source was just deleted or
+ the trash dir was deleted. */
+ trash_dest = trash_dir;
+ continue;
+ }
+ mailbox_list_set_error(list, MAIL_ERROR_NOTFOUND,
+ T_MAILBOX_LIST_ERR_NOT_FOUND(list, name));
+ return -1;
+ }
+ if (errno == EXDEV) {
+ /* can't do this the fast way */
+ return 0;
+ }
+ if (!EDESTDIREXISTS(errno)) {
+ if (mailbox_list_set_error_from_errno(list))
+ return -1;
+ mailbox_list_set_critical(list,
+ "rename(%s, %s) failed: %m", src, trash_dest);
+ return -1;
+ }
+
+ /* trash dir already exists. the reasons for this are:
+
+ a) another process is in the middle of deleting it
+ b) previous process crashed and didn't delete it
+ c) NFS: mailbox was recently deleted, but some connection
+ still has that mailbox open. the directory contains .nfs*
+ files that can't be deleted until the mailbox is closed.
+
+ Because of c) we'll first try to rename the mailbox under
+ the trash directory and only later try to delete the entire
+ trash directory. */
+ if (trash_dir == trash_dest) {
+ trash_dest = t_strconcat(trash_dir, "/",
+ unique_fname(), NULL);
+ } else if (mailbox_list_delete_trash(trash_dest, &error) < 0 &&
+ (errno != ENOTEMPTY || count >= 5)) {
+ mailbox_list_set_critical(list,
+ "unlink_directory(%s) failed: %s", trash_dest, error);
+ return -1;
+ }
+ }
+
+ if (mailbox_list_delete_trash(trash_dir, &error) < 0 &&
+ errno != ENOTEMPTY && errno != EBUSY) {
+ mailbox_list_set_critical(list,
+ "unlink_directory(%s) failed: %s", trash_dir, error);
+
+ /* it's already renamed to trash dir, which means it's
+ deleted as far as the client is concerned. Report
+ success. */
+ }
+ return 1;
+}
+
+int mailbox_list_delete_mailbox_file(struct mailbox_list *list,
+ const char *name, const char *path)
+{
+ /* we can simply unlink() the file */
+ if (unlink(path) == 0)
+ return 0;
+ else if (ENOTFOUND(errno)) {
+ mailbox_list_set_error(list, MAIL_ERROR_NOTFOUND,
+ T_MAILBOX_LIST_ERR_NOT_FOUND(list, name));
+ return -1;
+ } else {
+ if (!mailbox_list_set_error_from_errno(list)) {
+ mailbox_list_set_critical(list,
+ "unlink(%s) failed: %m", path);
+ }
+ return -1;
+ }
+}
+
+int mailbox_list_delete_mailbox_nonrecursive(struct mailbox_list *list,
+ const char *name, const char *path,
+ bool rmdir_path)
+{
+ DIR *dir;
+ struct dirent *d;
+ string_t *full_path;
+ size_t dir_len;
+ const char *error;
+ bool mailbox_dir, unlinked_something = FALSE;
+ int ret = 0;
+
+ if (mailbox_list_check_root_delete(list, name, path) < 0)
+ return -1;
+
+ dir = opendir(path);
+ if (dir == NULL) {
+ if (errno == ENOENT) {
+ mailbox_list_set_error(list, MAIL_ERROR_NOTFOUND,
+ T_MAILBOX_LIST_ERR_NOT_FOUND(list, name));
+ } else {
+ if (!mailbox_list_set_error_from_errno(list)) {
+ mailbox_list_set_critical(list,
+ "opendir(%s) failed: %m", path);
+ }
+ }
+ return -1;
+ }
+
+ full_path = t_str_new(256);
+ str_append(full_path, path);
+ str_append_c(full_path, '/');
+ dir_len = str_len(full_path);
+
+ for (errno = 0; (d = readdir(dir)) != NULL; errno = 0) {
+ if (d->d_name[0] == '.') {
+ /* skip . and .. */
+ if (d->d_name[1] == '\0')
+ continue;
+ if (d->d_name[1] == '.' && d->d_name[2] == '\0')
+ continue;
+ }
+
+ mailbox_dir = list->v.is_internal_name != NULL &&
+ list->v.is_internal_name(list, d->d_name);
+
+ str_truncate(full_path, dir_len);
+ str_append(full_path, d->d_name);
+
+ if (mailbox_dir) {
+ if (mailbox_list_delete_trash(str_c(full_path), &error) < 0) {
+ mailbox_list_set_critical(list,
+ "unlink_directory(%s) failed: %s",
+ str_c(full_path), error);
+ } else {
+ unlinked_something = TRUE;
+ }
+ continue;
+ }
+
+ /* trying to unlink() a directory gives either EPERM or EISDIR
+ (non-POSIX). it doesn't really work anywhere in practise,
+ so don't bother stat()ing the file first */
+ if (unlink(str_c(full_path)) == 0)
+ unlinked_something = TRUE;
+ else if (errno != ENOENT && !UNLINK_EISDIR(errno)) {
+ mailbox_list_set_critical(list,
+ "unlink(%s) failed: %m", str_c(full_path));
+ ret = -1;
+ } else {
+ /* child directories still exist */
+ rmdir_path = FALSE;
+ }
+ }
+ if (errno != 0) {
+ mailbox_list_set_critical(list, "readdir(%s) failed: %m", path);
+ ret = -1;
+ }
+ if (closedir(dir) < 0) {
+ mailbox_list_set_critical(list, "closedir(%s) failed: %m",
+ path);
+ ret = -1;
+ }
+ if (ret < 0)
+ return -1;
+
+ if (rmdir_path) {
+ unsigned int try_count = 0;
+ int ret = rmdir(path);
+ while (ret < 0 && errno == ENOTEMPTY && try_count++ < 10) {
+ /* We didn't see any child directories, so this is
+ either a race condition or .nfs* files were left
+ lying around. In case it's .nfs* files, retry after
+ waiting a bit. Hopefully all processes keeping those
+ files open will have closed them by then. */
+ i_sleep_msecs(100);
+ ret = rmdir(path);
+ }
+ if (rmdir(path) == 0)
+ unlinked_something = TRUE;
+ else if (errno == ENOENT) {
+ /* race condition with another process, which finished
+ deleting it first. */
+ mailbox_list_set_error(list, MAIL_ERROR_NOTFOUND,
+ T_MAILBOX_LIST_ERR_NOT_FOUND(list, name));
+ } else if (errno != ENOTEMPTY && errno != EEXIST) {
+ mailbox_list_set_critical(list, "rmdir(%s) failed: %m",
+ path);
+ return -1;
+ }
+ }
+
+ if (!unlinked_something) {
+ mailbox_list_set_error(list, MAIL_ERROR_NOTPOSSIBLE,
+ "Mailbox has children, can't delete it");
+ return -1;
+ }
+ return 0;
+}
+
+static bool mailbox_list_path_is_index(struct mailbox_list *list,
+ enum mailbox_list_path_type type)
+{
+ const char *index_root, *type_root;
+
+ if (type == MAILBOX_LIST_PATH_TYPE_INDEX)
+ return TRUE;
+
+ /* e.g. CONTROL dir could point to the same INDEX dir. */
+ type_root = mailbox_list_get_root_forced(list, type);
+ index_root = mailbox_list_get_root_forced(list, MAILBOX_LIST_PATH_TYPE_INDEX);
+ return strcmp(type_root, index_root) == 0;
+}
+
+void mailbox_list_delete_until_root(struct mailbox_list *list, const char *path,
+ enum mailbox_list_path_type type)
+{
+ const char *root_dir, *p;
+ size_t len;
+
+ if (list->set.iter_from_index_dir && !list->set.no_noselect &&
+ mailbox_list_path_is_index(list, type)) {
+ /* Don't auto-rmdir parent index directories with ITERINDEX.
+ Otherwise it'll get us into inconsistent state with a
+ \NoSelect mailbox in the mail directory but not in index
+ directory. */
+ return;
+ }
+
+ root_dir = mailbox_list_get_root_forced(list, type);
+ if (!str_begins(path, root_dir)) {
+ /* mbox workaround: name=child/box, root_dir=mail/.imap/,
+ path=mail/child/.imap/box. we'll want to try to delete
+ the .imap/ part, but no further. */
+ len = strlen(path);
+ while (len > 0 && path[len-1] != '/')
+ len--;
+ if (len == 0)
+ return;
+ len--;
+ while (len > 0 && path[len-1] != '/')
+ len--;
+ if (len == 0)
+ return;
+
+ root_dir = t_strndup(path, len-1);
+ }
+ while (strcmp(path, root_dir) != 0) {
+ if (rmdir(path) < 0 && errno != ENOENT) {
+ if (errno == ENOTEMPTY || errno == EEXIST)
+ return;
+
+ mailbox_list_set_critical(list, "rmdir(%s) failed: %m",
+ path);
+ return;
+ }
+ p = strrchr(path, '/');
+ if (p == NULL)
+ break;
+
+ path = t_strdup_until(path, p);
+ }
+}
+
+void mailbox_list_delete_mailbox_until_root(struct mailbox_list *list,
+ const char *storage_name)
+{
+ enum mailbox_list_path_type types[] = {
+ MAILBOX_LIST_PATH_TYPE_DIR,
+ MAILBOX_LIST_PATH_TYPE_ALT_DIR,
+ MAILBOX_LIST_PATH_TYPE_CONTROL,
+ MAILBOX_LIST_PATH_TYPE_INDEX,
+ MAILBOX_LIST_PATH_TYPE_INDEX_PRIVATE,
+ MAILBOX_LIST_PATH_TYPE_INDEX_CACHE,
+ };
+ const char *path;
+
+ for (unsigned int i = 0; i < N_ELEMENTS(types); i++) {
+ if (mailbox_list_get_path(list, storage_name, types[i], &path) > 0)
+ mailbox_list_delete_until_root(list, path, types[i]);
+ }
+}
+
+static int mailbox_list_try_delete(struct mailbox_list *list, const char *name,
+ enum mailbox_list_path_type type)
+{
+ const char *mailbox_path, *index_path, *path, *error;
+ int ret;
+
+ if (mailbox_list_get_path(list, name, MAILBOX_LIST_PATH_TYPE_MAILBOX,
+ &mailbox_path) <= 0 ||
+ mailbox_list_get_path(list, name, type, &path) <= 0 ||
+ strcmp(path, mailbox_path) == 0)
+ return 0;
+
+ if (type == MAILBOX_LIST_PATH_TYPE_CONTROL &&
+ mailbox_list_get_path(list, name, MAILBOX_LIST_PATH_TYPE_INDEX,
+ &index_path) > 0 &&
+ strcmp(index_path, path) == 0) {
+ /* CONTROL dir is the same as INDEX dir, which we already
+ deleted. We don't want to continue especially with
+ iter_from_index_dir=yes, because it could be deleting the
+ index directory. */
+ return 0;
+ }
+
+ /* Note that only ALT currently uses maildir_name in paths.
+ INDEX and CONTROL don't. */
+ if (type != MAILBOX_LIST_PATH_TYPE_ALT_MAILBOX ||
+ *list->set.maildir_name == '\0') {
+ /* this directory may contain also child mailboxes' data.
+ we don't want to delete that. */
+ bool rmdir_path = *list->set.maildir_name != '\0';
+ if (mailbox_list_delete_mailbox_nonrecursive(list, name, path,
+ rmdir_path) == 0)
+ ret = 1;
+ else {
+ enum mail_error error =
+ mailbox_list_get_last_mail_error(list);
+ if (error != MAIL_ERROR_NOTFOUND &&
+ error != MAIL_ERROR_NOTPOSSIBLE)
+ return -1;
+ ret = 0;
+ }
+ } else {
+ if (mailbox_list_delete_trash(path, &error) == 0)
+ ret = 1;
+ else if (errno == ENOTEMPTY)
+ ret = 0;
+ else {
+ mailbox_list_set_critical(list,
+ "unlink_directory(%s) failed: %s", path, error);
+ return -1;
+ }
+ }
+
+ /* Avoid leaving empty parent directories lying around.
+ These parent directories' existence or removal doesn't
+ affect our return value. */
+ mailbox_list_delete_until_root(list, path, type);
+ return ret;
+}
+
+int mailbox_list_delete_finish(struct mailbox_list *list, const char *name)
+{
+ int ret, ret2;
+
+ ret = mailbox_list_try_delete(list, name, MAILBOX_LIST_PATH_TYPE_INDEX);
+ ret2 = mailbox_list_try_delete(list, name, MAILBOX_LIST_PATH_TYPE_INDEX_CACHE);
+ if (ret == 0 || ret2 < 0)
+ ret = ret2;
+ ret2 = mailbox_list_try_delete(list, name, MAILBOX_LIST_PATH_TYPE_CONTROL);
+ if (ret == 0 || ret2 < 0)
+ ret = ret2;
+ ret2 = mailbox_list_try_delete(list, name, MAILBOX_LIST_PATH_TYPE_ALT_MAILBOX);
+ if (ret == 0 || ret2 < 0)
+ ret = ret2;
+ return ret;
+}
+
+int mailbox_list_delete_finish_ret(struct mailbox_list *list,
+ const char *name, bool root_delete_success)
+{
+ int ret2;
+
+ if (!root_delete_success &&
+ mailbox_list_get_last_mail_error(list) != MAIL_ERROR_NOTFOUND) {
+ /* unexpected error - preserve it */
+ return -1;
+ } else if ((ret2 = mailbox_list_delete_finish(list, name)) < 0) {
+ /* unexpected error */
+ return -1;
+ } else if (ret2 > 0) {
+ /* successfully deleted */
+ return 0;
+ } else if (root_delete_success) {
+ /* nothing deleted by us, but root was successfully deleted */
+ return 0;
+ } else {
+ /* nothing deleted by us and the root didn't exist either.
+ make sure the list has the correct error set, since it
+ could have been changed. */
+ mailbox_list_set_error(list, MAIL_ERROR_NOTFOUND,
+ T_MAILBOX_LIST_ERR_NOT_FOUND(list, name));
+ return -1;
+ }
+}
+
+int mailbox_list_delete_trash(const char *path, const char **error_r)
+{
+ if (unlink_directory(path, UNLINK_DIRECTORY_FLAG_RMDIR, error_r) < 0) {
+ if (errno == ELOOP) {
+ /* it's a symlink? try just deleting it */
+ if (unlink(path) == 0)
+ return 0;
+ errno = ELOOP;
+ return -1;
+ }
+ return -1;
+ }
+ return 0;
+}
+
+int mailbox_list_delete_symlink_default(struct mailbox_list *list,
+ const char *name)
+{
+ const char *path;
+ int ret;
+
+ ret = mailbox_list_get_path(list, name, MAILBOX_LIST_PATH_TYPE_DIR,
+ &path);
+ if (ret < 0)
+ return -1;
+ i_assert(ret > 0);
+
+ if (unlink(path) == 0)
+ return 0;
+
+ if (errno == ENOENT) {
+ mailbox_list_set_error(list, MAIL_ERROR_NOTFOUND,
+ T_MAILBOX_LIST_ERR_NOT_FOUND(list, name));
+ } else if (UNLINK_EISDIR(errno)) {
+ mailbox_list_set_error(list, MAIL_ERROR_NOTPOSSIBLE,
+ "Mailbox isn't a symlink");
+ } else {
+ mailbox_list_set_critical(list, "unlink(%s) failed: %m", path);
+ }
+ return -1;
+}
diff --git a/src/lib-storage/list/mailbox-list-delete.h b/src/lib-storage/list/mailbox-list-delete.h
new file mode 100644
index 0000000..9b46638
--- /dev/null
+++ b/src/lib-storage/list/mailbox-list-delete.h
@@ -0,0 +1,86 @@
+#ifndef MAILBOX_LIST_DELETE_H
+#define MAILBOX_LIST_DELETE_H
+
+#include "mailbox-list.h"
+
+/* Delete the mailbox atomically by rename()ing it to trash_dir and afterwards
+ recursively deleting the trash_dir. If the rename() fails because trash_dir
+ already exists, the trash_dir is first deleted and rename() is retried.
+
+ Returns 1 if the rename() succeeded. Returns 0 if rename() fails with EXDEV,
+ which means the source and destination are on different filesystems and
+ the rename can never succeeed.
+
+ If the path didn't exist, returns -1 and sets the list error to
+ MAIL_ERROR_NOTFOUND.
+
+ Attempting to delete INBOX or the namespace root returns -1 and sets the
+ list error to MAIL_ERROR_NOTPOSSIBLE.
+
+ Returns -1 and sets the list error on other errors. */
+int mailbox_list_delete_maildir_via_trash(struct mailbox_list *list,
+ const char *name,
+ const char *trash_dir);
+/* Try to unlink() the path. Returns 0 on success. If the path didn't exist,
+ returns -1 and sets the list error to MAIL_ERROR_NOTFOUND.
+ Returns -1 and sets the list error on other errors. */
+int mailbox_list_delete_mailbox_file(struct mailbox_list *list,
+ const char *name, const char *path);
+/* Delete all files from the given path. Also all internal directories
+ (as returned by is_internal_name() check) are recursively deleted.
+ Otherwise directories are left undeleted.
+
+ Returns 0 if anything was unlink()ed and no unexpected errors happened.
+ Also returns 0 if there were no files and the path was successfully
+ rmdir()ed.
+
+ If the path didn't exist, returns -1 and sets the list error to
+ MAIL_ERROR_NOTFOUND.
+
+ If the path exists and has subdirectories, but no files were unlink()ed,
+ returns -1 and sets the list error to MAIL_ERROR_NOTPOSSIBLE.
+
+ Attempting to delete INBOX or the namespace root returns -1 and sets the
+ list error to MAIL_ERROR_NOTPOSSIBLE.
+
+ Returns -1 and sets the list error on other errors. */
+int mailbox_list_delete_mailbox_nonrecursive(struct mailbox_list *list,
+ const char *name, const char *path,
+ bool rmdir_path);
+/* Lookup INDEX, CONTROL and ALT directories for the mailbox and delete them.
+ Returns 1 if anything was unlink()ed or rmdir()ed, 0 if not.
+ Returns -1 and sets the list error on any errors. */
+int mailbox_list_delete_finish(struct mailbox_list *list, const char *name);
+/* Finish mailbox deletion by calling mailbox_list_delete_finish() if needed.
+ Set root_delete_success to TRUE if the mail root directory was successfully
+ deleted, FALSE if not. The list is expected to have a proper error when
+ root_delete_success==FALSE.
+
+ Returns 0 if mailbox deletion should be treated as success. If not, returns
+ -1 and sets the list error if necessary. */
+int mailbox_list_delete_finish_ret(struct mailbox_list *list,
+ const char *name, bool root_delete_success);
+
+/* rmdir() path and its parent directories until the root directory is reached.
+ The root isn't rmdir()ed. */
+void mailbox_list_delete_until_root(struct mailbox_list *list, const char *path,
+ enum mailbox_list_path_type type);
+/* Call mailbox_list_delete_until_root() for all the paths of the mailbox. */
+void mailbox_list_delete_mailbox_until_root(struct mailbox_list *list,
+ const char *storage_name);
+/* Wrapper to unlink_directory(UNLINK_DIRECTORY_FLAG_RMDIR). If it fails due
+ to ELOOP, try to unlink() the path instead. */
+int mailbox_list_delete_trash(const char *path, const char **error_r);
+/* Try to unlink() the path to the mailbox. Returns 0 on success.
+
+ If the path didn't exist, returns -1 and sets the list error to
+ MAIL_ERROR_NOTFOUND.
+
+ If the path is a directory, returns -1 and sets the list error to
+ MAIL_ERROR_NOTPOSSIBLE.
+
+ Returns -1 and sets the list error on other errors. */
+int mailbox_list_delete_symlink_default(struct mailbox_list *list,
+ const char *name);
+
+#endif
diff --git a/src/lib-storage/list/mailbox-list-fs-flags.c b/src/lib-storage/list/mailbox-list-fs-flags.c
new file mode 100644
index 0000000..3f3bd94
--- /dev/null
+++ b/src/lib-storage/list/mailbox-list-fs-flags.c
@@ -0,0 +1,243 @@
+/* Copyright (c) 2002-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "mailbox-list-fs.h"
+
+#include <sys/stat.h>
+
+/* Assume that if atime < mtime, there are new mails. If it's good enough for
+ UW-IMAP, it's good enough for us. */
+#define STAT_GET_MARKED_FILE(st) \
+ ((st).st_size == 0 ? MAILBOX_UNMARKED : \
+ (st).st_atime < (st).st_mtime ? MAILBOX_MARKED : MAILBOX_UNMARKED)
+
+static int
+list_is_maildir_mailbox(struct mailbox_list *list, const char *dir,
+ const char *fname, enum mailbox_list_file_type type,
+ enum mailbox_info_flags *flags_r)
+{
+ const char *path, *maildir_path;
+ struct stat st, st2;
+ bool mailbox_files;
+
+ switch (type) {
+ case MAILBOX_LIST_FILE_TYPE_FILE:
+ case MAILBOX_LIST_FILE_TYPE_OTHER:
+ /* non-directories aren't valid */
+ *flags_r |= MAILBOX_NOSELECT | MAILBOX_NOINFERIORS;
+ return 0;
+
+ case MAILBOX_LIST_FILE_TYPE_DIR:
+ case MAILBOX_LIST_FILE_TYPE_UNKNOWN:
+ case MAILBOX_LIST_FILE_TYPE_SYMLINK:
+ break;
+ }
+
+ path = t_strdup_printf("%s/%s", dir, fname);
+ if (stat(path, &st) < 0) {
+ if (errno == ENOENT) {
+ *flags_r |= MAILBOX_NONEXISTENT;
+ return 0;
+ } else {
+ /* non-selectable. probably either access denied, or
+ symlink destination not found. don't bother logging
+ errors. */
+ *flags_r |= MAILBOX_NOSELECT;
+ return 1;
+ }
+ }
+ if (!S_ISDIR(st.st_mode)) {
+ if (str_begins(fname, ".nfs")) {
+ /* temporary NFS file */
+ *flags_r |= MAILBOX_NONEXISTENT;
+ } else {
+ *flags_r |= MAILBOX_NOSELECT | MAILBOX_NOINFERIORS;
+ }
+ return 0;
+ }
+
+ /* ok, we've got a directory. see what we can do about it. */
+
+ /* 1st link is "."
+ 2nd link is ".."
+ 3rd link is either child mailbox or mailbox dir
+ rest of the links are child mailboxes
+
+ if mailboxes are files, then 3+ links are all child mailboxes.
+ */
+ mailbox_files = (list->flags & MAILBOX_LIST_FLAG_MAILBOX_FILES) != 0;
+ if (st.st_nlink == 2 && !mailbox_files) {
+ *flags_r |= MAILBOX_NOSELECT;
+ return 1;
+ }
+
+ /* we have at least one directory. see if this mailbox is selectable */
+ maildir_path = t_strconcat(path, "/", list->set.maildir_name, NULL);
+ if (stat(maildir_path, &st2) < 0)
+ *flags_r |= MAILBOX_NOSELECT | MAILBOX_CHILDREN;
+ else if (!S_ISDIR(st2.st_mode)) {
+ if (mailbox_files) {
+ *flags_r |= st.st_nlink == 2 ?
+ MAILBOX_NOCHILDREN : MAILBOX_CHILDREN;
+ } else {
+ *flags_r |= MAILBOX_NOSELECT | MAILBOX_CHILDREN;
+ }
+ } else {
+ /* now we know what link count 3 means. */
+ if (st.st_nlink == 3)
+ *flags_r |= MAILBOX_NOCHILDREN;
+ else
+ *flags_r |= MAILBOX_CHILDREN;
+ }
+ *flags_r |= MAILBOX_SELECT;
+ return 1;
+}
+
+static bool
+is_inbox_file(struct mailbox_list *list, const char *path, const char *fname)
+{
+ const char *inbox_path;
+
+ if (strcasecmp(fname, "INBOX") != 0)
+ return FALSE;
+
+ if (mailbox_list_get_path(list, "INBOX",
+ MAILBOX_LIST_PATH_TYPE_MAILBOX,
+ &inbox_path) <= 0)
+ i_unreached();
+ return strcmp(inbox_path, path) == 0;
+}
+
+int fs_list_get_mailbox_flags(struct mailbox_list *list,
+ const char *dir, const char *fname,
+ enum mailbox_list_file_type type,
+ enum mailbox_info_flags *flags_r)
+{
+ struct stat st;
+ const char *path;
+
+ *flags_r = 0;
+
+ if (*list->set.maildir_name != '\0' && !list->set.iter_from_index_dir) {
+ /* maildir_name is set: This is the simple case that works for
+ all mail storage formats, because the only thing that
+ matters for existence or child checks is whether the
+ maildir_name exists or not. For example with Maildir this
+ doesn't care whether the "cur" directory exists; as long
+ as the parent maildir_name exists, the Maildir is
+ selectable. */
+ return list_is_maildir_mailbox(list, dir, fname, type, flags_r);
+ }
+ /* maildir_name is not set: Now we (may) need to use storage-specific
+ code to determine whether the mailbox is selectable or if it has
+ children.
+
+ We're here also when iterating from index directory, because even
+ though maildir_name is set, it's not used for index directory.
+ */
+
+ if (!list->set.iter_from_index_dir &&
+ list->v.is_internal_name != NULL &&
+ list->v.is_internal_name(list, fname)) {
+ /* skip internal dirs. For example Maildir's cur/new/tmp */
+ *flags_r |= MAILBOX_NOSELECT;
+ return 0;
+ }
+
+ switch (type) {
+ case MAILBOX_LIST_FILE_TYPE_DIR:
+ /* We know that we're looking at a directory. If the storage
+ uses files, it has to be a \NoSelect directory. */
+ if ((list->flags & MAILBOX_LIST_FLAG_MAILBOX_FILES) != 0) {
+ *flags_r |= MAILBOX_NOSELECT;
+ return 1;
+ }
+ break;
+ case MAILBOX_LIST_FILE_TYPE_FILE:
+ /* We know that we're looking at a file. If the storage
+ doesn't use files, it's not a mailbox and we want to skip
+ it. */
+ if ((list->flags & MAILBOX_LIST_FLAG_MAILBOX_FILES) == 0) {
+ *flags_r |= MAILBOX_NOSELECT | MAILBOX_NOINFERIORS;
+ return 0;
+ }
+ break;
+ default:
+ break;
+ }
+
+ /* we've done all filtering we can before stat()ing */
+ path = t_strconcat(dir, "/", fname, NULL);
+ if (stat(path, &st) < 0) {
+ if (ENOTFOUND(errno)) {
+ *flags_r |= MAILBOX_NONEXISTENT;
+ return 0;
+ } else if (ENOACCESS(errno)) {
+ *flags_r |= MAILBOX_NOSELECT;
+ return 1;
+ } else {
+ /* non-selectable. probably either access denied, or
+ symlink destination not found. don't bother logging
+ errors. */
+ mailbox_list_set_critical(list, "stat(%s) failed: %m",
+ path);
+ return -1;
+ }
+ }
+
+ if (!S_ISDIR(st.st_mode)) {
+ if (str_begins(fname, ".nfs")) {
+ /* temporary NFS file */
+ *flags_r |= MAILBOX_NONEXISTENT;
+ return 0;
+ }
+
+ if ((list->flags & MAILBOX_LIST_FLAG_MAILBOX_FILES) == 0) {
+ *flags_r |= MAILBOX_NOSELECT | MAILBOX_NOINFERIORS;
+ return 0;
+ }
+ /* looks like a valid mailbox file */
+ if (is_inbox_file(list, path, fname) &&
+ strcmp(fname, "INBOX") != 0) {
+ /* it's possible for INBOX to have child
+ mailboxes as long as the inbox file itself
+ isn't in <mail root>/INBOX */
+ } else {
+ *flags_r |= MAILBOX_NOINFERIORS;
+ }
+ /* Return mailbox files as always existing. The current
+ mailbox_exists() code would do the same stat() anyway
+ without further checks, so might as well avoid the second
+ stat(). */
+ *flags_r |= MAILBOX_SELECT;
+ *flags_r |= STAT_GET_MARKED_FILE(st);
+ return 1;
+ }
+
+ /* This is a directory */
+ if ((list->flags & MAILBOX_LIST_FLAG_MAILBOX_FILES) != 0) {
+ /* We should get here only if type is
+ MAILBOX_LIST_FILE_TYPE_UNKNOWN because the filesystem didn't
+ return the type. Normally this should have already been
+ handled by the MAILBOX_LIST_FILE_TYPE_DIR check above. */
+ *flags_r |= MAILBOX_NOSELECT;
+ return 1;
+ }
+
+ if (list->v.is_internal_name == NULL || list->set.iter_from_index_dir) {
+ /* This mailbox format doesn't use any special directories
+ (e.g. Maildir's cur/new/tmp). In that case we can look at
+ the directory's link count to determine whether there are
+ children or not. The directory's link count equals the
+ number of subdirectories it has. The first two links are
+ for "." and "..".
+
+ link count < 2 can happen with filesystems that don't
+ support link counts. we'll just ignore them for now.. */
+ if (st.st_nlink == 2)
+ *flags_r |= MAILBOX_NOCHILDREN;
+ else if (st.st_nlink > 2)
+ *flags_r |= MAILBOX_CHILDREN;
+ }
+ return 1;
+}
diff --git a/src/lib-storage/list/mailbox-list-fs-iter.c b/src/lib-storage/list/mailbox-list-fs-iter.c
new file mode 100644
index 0000000..b95aabc
--- /dev/null
+++ b/src/lib-storage/list/mailbox-list-fs-iter.c
@@ -0,0 +1,833 @@
+/* Copyright (c) 2002-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "str.h"
+#include "unichar.h"
+#include "imap-match.h"
+#include "imap-utf7.h"
+#include "mail-storage.h"
+#include "mailbox-tree.h"
+#include "mailbox-list-subscriptions.h"
+#include "mailbox-list-iter-private.h"
+#include "mailbox-list-fs.h"
+
+#include <stdio.h>
+#include <ctype.h>
+#include <dirent.h>
+#include <sys/stat.h>
+
+struct list_dir_entry {
+ const char *fname;
+ enum mailbox_info_flags info_flags;
+};
+
+struct list_dir_context {
+ struct list_dir_context *parent;
+
+ pool_t pool;
+ const char *storage_name;
+ /* this directory's info flags. */
+ enum mailbox_info_flags info_flags;
+
+ /* all files in this directory */
+ ARRAY(struct list_dir_entry) entries;
+ unsigned int entry_idx;
+};
+
+struct fs_list_iterate_context {
+ struct mailbox_list_iterate_context ctx;
+
+ const char *const *valid_patterns;
+ /* roots can be either /foo, ~user/bar or baz */
+ ARRAY(const char *) roots;
+ unsigned int root_idx;
+ char sep;
+
+ pool_t info_pool;
+ struct mailbox_info info;
+ /* current directory we're handling */
+ struct list_dir_context *dir;
+
+ bool inbox_found:1;
+ bool inbox_has_children:1;
+ bool listed_prefix_inbox:1;
+};
+
+static int
+fs_get_existence_info_flag(struct fs_list_iterate_context *ctx,
+ const char *vname,
+ enum mailbox_info_flags *info_flags)
+{
+ struct mailbox *box;
+ enum mailbox_flags flags = 0;
+ enum mailbox_existence existence;
+ bool auto_boxes;
+ int ret;
+
+ if ((ctx->ctx.flags & MAILBOX_LIST_ITER_RAW_LIST) != 0)
+ flags |= MAILBOX_FLAG_IGNORE_ACLS;
+ auto_boxes = (ctx->ctx.flags & MAILBOX_LIST_ITER_NO_AUTO_BOXES) == 0;
+ box = mailbox_alloc(ctx->ctx.list, vname, flags);
+ ret = mailbox_exists(box, auto_boxes, &existence);
+ mailbox_free(&box);
+
+ if (ret < 0) {
+ /* this can only be an internal error */
+ mailbox_list_set_internal_error(ctx->ctx.list);
+ return -1;
+ }
+ switch (existence) {
+ case MAILBOX_EXISTENCE_NONE:
+ /* We already found out that this mailbox exists. So this is
+ either a race condition or ACL plugin prevented access to
+ this. In any case treat this as a \NoSelect mailbox so that
+ we'll recurse into its potential children. This is
+ especially important if ACL disabled access to the parent
+ mailbox, but child mailboxes would be accessible. */
+ case MAILBOX_EXISTENCE_NOSELECT:
+ *info_flags |= MAILBOX_NOSELECT;
+ break;
+ case MAILBOX_EXISTENCE_SELECT:
+ *info_flags |= MAILBOX_SELECT;
+ break;
+ }
+ return 0;
+}
+
+static void
+fs_list_rename_invalid(struct fs_list_iterate_context *ctx,
+ const char *storage_name)
+{
+ /* the storage_name is completely invalid, rename it to
+ something more sensible. we could do this for all names that
+ aren't valid mUTF-7, but that might lead to accidents in
+ future when UTF-8 storage names are used */
+ string_t *destname = t_str_new(128);
+ string_t *dest = t_str_new(128);
+ const char *root, *src;
+
+ root = mailbox_list_get_root_forced(ctx->ctx.list,
+ MAILBOX_LIST_PATH_TYPE_MAILBOX);
+ src = t_strconcat(root, "/", storage_name, NULL);
+
+ if (uni_utf8_get_valid_data((const void *)storage_name,
+ strlen(storage_name), destname))
+ i_unreached(); /* already checked that it was invalid */
+
+ str_append(dest, root);
+ str_append_c(dest, '/');
+ (void)imap_utf8_to_utf7(str_c(destname), dest);
+
+ if (rename(src, str_c(dest)) < 0 && errno != ENOENT)
+ e_error(ctx->ctx.list->ns->user->event,
+ "rename(%s, %s) failed: %m", src, str_c(dest));
+}
+
+static const char *
+dir_get_storage_name(struct list_dir_context *dir, const char *fname)
+{
+ if (*dir->storage_name == '\0') {
+ /* regular root */
+ return fname;
+ } else if (strcmp(dir->storage_name, "/") == 0) {
+ /* full_filesystem_access=yes "/" root */
+ return t_strconcat("/", fname, NULL);
+ } else {
+ /* child */
+ return *fname == '\0' ? dir->storage_name :
+ t_strconcat(dir->storage_name, "/", fname, NULL);
+ }
+}
+
+static int
+dir_entry_get(struct fs_list_iterate_context *ctx, const char *dir_path,
+ struct list_dir_context *dir, const struct dirent *d)
+{
+ const char *storage_name, *vname, *root_dir;
+ struct list_dir_entry *entry;
+ enum imap_match_result match;
+ enum mailbox_info_flags info_flags;
+ int ret;
+
+ /* skip . and .. */
+ if (d->d_name[0] == '.' &&
+ (d->d_name[1] == '\0' ||
+ (d->d_name[1] == '.' && d->d_name[2] == '\0')))
+ return 0;
+
+ if (strcmp(d->d_name, ctx->ctx.list->set.maildir_name) == 0) {
+ /* mail storage's internal directory (e.g. dbox-Mails).
+ this also means that the parent is selectable */
+ dir->info_flags &= ENUM_NEGATE(MAILBOX_NOSELECT);
+ dir->info_flags |= MAILBOX_SELECT;
+ return 0;
+ }
+ if (ctx->ctx.list->set.subscription_fname != NULL &&
+ strcmp(d->d_name, ctx->ctx.list->set.subscription_fname) == 0) {
+ /* if this is the subscriptions file, skip it */
+ root_dir = mailbox_list_get_root_forced(ctx->ctx.list,
+ MAILBOX_LIST_PATH_TYPE_DIR);
+ if (strcmp(root_dir, dir_path) == 0)
+ return 0;
+ }
+
+ /* check the pattern */
+ storage_name = dir_get_storage_name(dir, d->d_name);
+ vname = mailbox_list_get_vname(ctx->ctx.list, storage_name);
+ if (!uni_utf8_str_is_valid(vname)) {
+ fs_list_rename_invalid(ctx, storage_name);
+ /* just skip this in this iteration, we'll see it on the
+ next list */
+ return 0;
+ }
+
+ match = imap_match(ctx->ctx.glob, vname);
+ if (strcmp(d->d_name, "INBOX") == 0 && strcmp(vname, "INBOX") == 0 &&
+ ctx->ctx.list->ns->prefix_len > 0) {
+ /* The glob was matched only against "INBOX", but this
+ directory may hold also prefix/INBOX. Just assume here
+ that it matches and verify later whether it was needed
+ or not. */
+ match = IMAP_MATCH_YES;
+ }
+
+ if ((dir->info_flags & (MAILBOX_CHILDREN | MAILBOX_NOCHILDREN |
+ MAILBOX_NOINFERIORS)) == 0 &&
+ (ctx->ctx.flags & MAILBOX_LIST_ITER_RETURN_CHILDREN) != 0) {
+ /* we don't know yet if the parent has children. need to figure
+ out if this file is actually a visible mailbox */
+ } else if (match != IMAP_MATCH_YES &&
+ (match & IMAP_MATCH_CHILDREN) == 0) {
+ /* mailbox doesn't match any patterns, we don't care about it */
+ return 0;
+ }
+ if ((ctx->ctx.flags & MAILBOX_LIST_ITER_SKIP_ALIASES) != 0) {
+ ret = mailbox_list_dirent_is_alias_symlink(ctx->ctx.list,
+ dir_path, d);
+ if (ret != 0)
+ return ret < 0 ? -1 : 0;
+ }
+ ret = ctx->ctx.list->v.
+ get_mailbox_flags(ctx->ctx.list, dir_path, d->d_name,
+ mailbox_list_get_file_type(d), &info_flags);
+ if (ret <= 0)
+ return ret;
+ if (!MAILBOX_INFO_FLAGS_FINISHED(info_flags)) {
+ /* mailbox existence isn't known yet. need to figure it out
+ the hard way. */
+ if (fs_get_existence_info_flag(ctx, vname, &info_flags) < 0)
+ return -1;
+ }
+ if ((info_flags & MAILBOX_NONEXISTENT) != 0)
+ return 0;
+
+ /* mailbox exists - make sure parent knows it has children */
+ dir->info_flags &= ENUM_NEGATE(MAILBOX_NOCHILDREN | MAILBOX_NOINFERIORS);
+ dir->info_flags |= MAILBOX_CHILDREN;
+
+ if (match != IMAP_MATCH_YES && (match & IMAP_MATCH_CHILDREN) == 0) {
+ /* this mailbox didn't actually match any pattern,
+ we just needed to know the children state */
+ return 0;
+ }
+
+ /* entry matched a pattern. we're going to return this. */
+ entry = array_append_space(&dir->entries);
+ entry->fname = p_strdup(dir->pool, d->d_name);
+ entry->info_flags = info_flags;
+ return 0;
+}
+
+static bool
+fs_list_get_storage_path(struct fs_list_iterate_context *ctx,
+ const char *storage_name, const char **path_r)
+{
+ const char *root, *path = storage_name;
+
+ if (*path == '~') {
+ if (!mailbox_list_try_get_absolute_path(ctx->ctx.list, &path)) {
+ /* a) couldn't expand ~user/
+ b) mailbox is under our mail root, we changed
+ path to storage_name */
+ }
+ /* NOTE: the path may have been translated to a storage_name
+ instead of path */
+ }
+ if (*path != '/') {
+ /* non-absolute path. add the mailbox root dir as prefix. */
+ enum mailbox_list_path_type type =
+ ctx->ctx.list->set.iter_from_index_dir ?
+ MAILBOX_LIST_PATH_TYPE_INDEX :
+ MAILBOX_LIST_PATH_TYPE_MAILBOX;
+ if (!mailbox_list_get_root_path(ctx->ctx.list, type, &root))
+ return FALSE;
+ if (ctx->ctx.list->set.iter_from_index_dir &&
+ ctx->ctx.list->set.mailbox_dir_name[0] != '\0') {
+ /* append "mailboxes/" to the index root */
+ root = t_strconcat(root, "/",
+ ctx->ctx.list->set.mailbox_dir_name, NULL);
+ }
+ path = *path == '\0' ? root :
+ t_strconcat(root, "/", path, NULL);
+ }
+ *path_r = path;
+ return TRUE;
+}
+
+static int
+fs_list_dir_read(struct fs_list_iterate_context *ctx,
+ struct list_dir_context *dir)
+{
+ DIR *fsdir;
+ struct dirent *d;
+ const char *path;
+ int ret = 0;
+
+ if (!fs_list_get_storage_path(ctx, dir->storage_name, &path))
+ return 0;
+ if (path == NULL) {
+ /* no mailbox root dir */
+ return 0;
+ }
+
+ fsdir = opendir(path);
+ if (fsdir == NULL) {
+ if (ENOTFOUND(errno)) {
+ /* root) user gave invalid hierarchy, ignore
+ sub) probably just race condition with other client
+ deleting the mailbox. */
+ return 0;
+ }
+ if (errno == EACCES) {
+ /* ignore permission errors */
+ return 0;
+ }
+ mailbox_list_set_critical(ctx->ctx.list,
+ "opendir(%s) failed: %m", path);
+ return -1;
+ }
+ if ((dir->info_flags & (MAILBOX_SELECT | MAILBOX_NOSELECT)) == 0) {
+ /* we don't know if the parent is selectable or not. start with
+ the assumption that it isn't, until we see maildir_name */
+ dir->info_flags |= MAILBOX_NOSELECT;
+ }
+
+ errno = 0;
+ while ((d = readdir(fsdir)) != NULL) T_BEGIN {
+ if (dir_entry_get(ctx, path, dir, d) < 0)
+ ret = -1;
+ errno = 0;
+ } T_END;
+ if (errno != 0) {
+ mailbox_list_set_critical(ctx->ctx.list,
+ "readdir(%s) failed: %m", path);
+ ret = -1;
+ }
+ if (closedir(fsdir) < 0) {
+ mailbox_list_set_critical(ctx->ctx.list,
+ "closedir(%s) failed: %m", path);
+ ret = -1;
+ }
+ return ret;
+}
+
+static struct list_dir_context *
+fs_list_read_dir(struct fs_list_iterate_context *ctx, const char *storage_name,
+ enum mailbox_info_flags info_flags)
+{
+ struct list_dir_context *dir;
+ pool_t pool;
+
+ pool = pool_alloconly_create(MEMPOOL_GROWING"fs iter dir", 256);
+ dir = p_new(pool, struct list_dir_context, 1);
+ dir->pool = pool;
+ dir->storage_name = p_strdup(pool, storage_name);
+ dir->info_flags = info_flags;
+ p_array_init(&dir->entries, pool, 16);
+
+ if (fs_list_dir_read(ctx, dir) < 0)
+ ctx->ctx.failed = TRUE;
+
+ if ((dir->info_flags & (MAILBOX_CHILDREN | MAILBOX_NOCHILDREN |
+ MAILBOX_NOINFERIORS)) == 0) {
+ /* assume this directory has no children */
+ dir->info_flags |= MAILBOX_NOCHILDREN;
+ }
+ return dir;
+}
+
+static bool
+fs_list_get_valid_patterns(struct fs_list_iterate_context *ctx,
+ const char *const *patterns)
+{
+ struct mailbox_list *_list = ctx->ctx.list;
+ ARRAY(const char *) valid_patterns;
+ const char *pattern, *test_pattern, *real_pattern, *error;
+ size_t prefix_len;
+
+ prefix_len = strlen(_list->ns->prefix);
+ p_array_init(&valid_patterns, ctx->ctx.pool, 8);
+ for (; *patterns != NULL; patterns++) {
+ /* check that we're not trying to do any "../../" lists */
+ test_pattern = *patterns;
+ /* skip namespace prefix if possible. this allows using
+ e.g. ~/mail/ prefix and have it pass the pattern
+ validation. */
+ if (strncmp(test_pattern, _list->ns->prefix, prefix_len) == 0)
+ test_pattern += prefix_len;
+ if (!uni_utf8_str_is_valid(test_pattern)) {
+ /* ignore invalid UTF8 patterns */
+ continue;
+ }
+ /* check pattern also when it's converted to use real
+ separators. */
+ real_pattern =
+ mailbox_list_get_storage_name(_list, test_pattern);
+ if (mailbox_list_is_valid_name(_list, test_pattern, &error) &&
+ mailbox_list_is_valid_name(_list, real_pattern, &error)) {
+ pattern = p_strdup(ctx->ctx.pool, *patterns);
+ array_push_back(&valid_patterns, &pattern);
+ }
+ }
+ array_append_zero(&valid_patterns); /* NULL-terminate */
+ ctx->valid_patterns = array_front(&valid_patterns);
+
+ return array_count(&valid_patterns) > 1;
+}
+
+static void fs_list_get_roots(struct fs_list_iterate_context *ctx)
+{
+ struct mail_namespace *ns = ctx->ctx.list->ns;
+ char ns_sep = mail_namespace_get_sep(ns);
+ bool full_fs_access =
+ ctx->ctx.list->mail_set->mail_full_filesystem_access;
+ const char *const *patterns, *pattern, *parent, *child;
+ const char *p, *last, *root, *prefix_vname;
+ unsigned int i;
+ size_t parentlen;
+
+ i_assert(*ctx->valid_patterns != NULL);
+
+ /* get the root dirs for all the patterns */
+ p_array_init(&ctx->roots, ctx->ctx.pool, 8);
+ for (patterns = ctx->valid_patterns; *patterns != NULL; patterns++) {
+ pattern = *patterns;
+
+ if (strncmp(pattern, ns->prefix, ns->prefix_len) != 0) {
+ /* typically e.g. prefix=foo/bar/, pattern=foo/%/%
+ we'll use root="" for this.
+
+ it might of course also be pattern=foo/%/prefix/%
+ where we could optimize with root=prefix, but
+ probably too much trouble to implement. */
+ prefix_vname = "";
+ } else {
+ for (p = last = pattern; *p != '\0'; p++) {
+ if (*p == '%' || *p == '*')
+ break;
+ if (*p == ns_sep)
+ last = p;
+ }
+ prefix_vname = t_strdup_until(pattern, last);
+ }
+
+ if (*pattern == ns_sep && full_fs_access) {
+ /* pattern=/something with full filesystem access.
+ (without full filesystem access we want to skip this
+ if namespace prefix begins with separator) */
+ root = "/";
+ } else if ((ns->flags & NAMESPACE_FLAG_INBOX_USER) != 0 &&
+ ns->prefix_len == 6 &&
+ strcasecmp(prefix_vname, "INBOX") == 0 &&
+ strncasecmp(ns->prefix, pattern, ns->prefix_len) == 0) {
+ /* special case: Namespace prefix is INBOX/ and
+ we just want to see its contents (not the
+ INBOX's children). */
+ root = "";
+ } else if ((ns->flags & NAMESPACE_FLAG_INBOX_ANY) != 0 &&
+ ns->type == MAIL_NAMESPACE_TYPE_SHARED &&
+ !ctx->ctx.list->mail_set->mail_shared_explicit_inbox &&
+ (prefix_vname[0] == '\0' ||
+ (strncmp(ns->prefix, prefix_vname, ns->prefix_len-1) == 0 &&
+ prefix_vname[ns->prefix_len-1] == '\0'))) {
+ /* we need to handle ns prefix explicitly here, because
+ getting storage name with
+ mail_shared_explicit_inbox=no would return
+ root=INBOX. (e.g. LIST "" shared/user/box has to
+ return the box when it doesn't exist but
+ shared/user/box/child exists) */
+ root = "";
+ } else {
+ root = mailbox_list_get_storage_name(ctx->ctx.list,
+ prefix_vname);
+ }
+
+ if (*root == '/') {
+ /* /absolute/path */
+ i_assert(full_fs_access);
+ } else if (*root == '~') {
+ /* ~user/path - don't expand the ~user/ path, since we
+ need to be able to convert the path back to vname */
+ i_assert(full_fs_access);
+ } else {
+ /* mailbox name */
+ }
+ root = p_strdup(ctx->ctx.pool, root);
+ array_push_back(&ctx->roots, &root);
+ }
+ /* sort the root dirs so that /foo is before /foo/bar */
+ array_sort(&ctx->roots, i_strcmp_p);
+ /* remove /foo/bar when there already exists /foo parent */
+ for (i = 1; i < array_count(&ctx->roots); ) {
+ parent = array_idx_elem(&ctx->roots, i-1);
+ child = array_idx_elem(&ctx->roots, i);
+ parentlen = strlen(parent);
+ if (str_begins(child, parent) &&
+ (parentlen == 0 ||
+ child[parentlen] == ctx->sep ||
+ child[parentlen] == '\0'))
+ array_delete(&ctx->roots, i, 1);
+ else
+ i++;
+ }
+}
+
+static void fs_list_next_root(struct fs_list_iterate_context *ctx)
+{
+ const char *const *roots;
+ unsigned int count;
+
+ i_assert(ctx->dir == NULL);
+
+ roots = array_get(&ctx->roots, &count);
+ if (ctx->root_idx == count)
+ return;
+ ctx->dir = fs_list_read_dir(ctx, roots[ctx->root_idx],
+ MAILBOX_NOSELECT);
+ ctx->root_idx++;
+}
+
+struct mailbox_list_iterate_context *
+fs_list_iter_init(struct mailbox_list *_list, const char *const *patterns,
+ enum mailbox_list_iter_flags flags)
+{
+ struct fs_list_iterate_context *ctx;
+ pool_t pool;
+
+ if ((flags & MAILBOX_LIST_ITER_SELECT_SUBSCRIBED) != 0) {
+ /* we're listing only subscribed mailboxes. we can't optimize
+ it, so just use the generic code. */
+ return mailbox_list_subscriptions_iter_init(_list, patterns,
+ flags);
+ }
+
+ pool = pool_alloconly_create("mailbox list fs iter", 2048);
+ ctx = p_new(pool, struct fs_list_iterate_context, 1);
+ ctx->ctx.pool = pool;
+ ctx->ctx.list = _list;
+ ctx->ctx.flags = flags;
+ array_create(&ctx->ctx.module_contexts, pool, sizeof(void *), 5);
+
+ ctx->info_pool = pool_alloconly_create("fs list", 1024);
+ ctx->sep = mail_namespace_get_sep(_list->ns);
+ ctx->info.ns = _list->ns;
+
+ if (!fs_list_get_valid_patterns(ctx, patterns)) {
+ /* we've only invalid patterns (or INBOX). create a glob
+ anyway to avoid any crashes due to glob being accessed
+ elsewhere */
+ ctx->ctx.glob = imap_match_init(pool, "", TRUE, ctx->sep);
+ return &ctx->ctx;
+ }
+ ctx->ctx.glob = imap_match_init_multiple(pool, ctx->valid_patterns,
+ TRUE, ctx->sep);
+ fs_list_get_roots(ctx);
+ fs_list_next_root(ctx);
+ return &ctx->ctx;
+}
+
+int fs_list_iter_deinit(struct mailbox_list_iterate_context *_ctx)
+{
+ struct fs_list_iterate_context *ctx =
+ (struct fs_list_iterate_context *)_ctx;
+ int ret = _ctx->failed ? -1 : 0;
+
+ if ((_ctx->flags & MAILBOX_LIST_ITER_SELECT_SUBSCRIBED) != 0)
+ return mailbox_list_subscriptions_iter_deinit(_ctx);
+
+ while (ctx->dir != NULL) {
+ struct list_dir_context *dir = ctx->dir;
+
+ ctx->dir = dir->parent;
+ pool_unref(&dir->pool);
+ }
+
+ pool_unref(&ctx->info_pool);
+ pool_unref(&_ctx->pool);
+ return ret;
+}
+
+static void inbox_flags_set(struct fs_list_iterate_context *ctx)
+{
+ /* INBOX is always selectable */
+ ctx->info.flags &= ENUM_NEGATE(MAILBOX_NOSELECT | MAILBOX_NONEXISTENT);
+
+ if (mail_namespace_is_inbox_noinferiors(ctx->info.ns)) {
+ ctx->info.flags &= ENUM_NEGATE(MAILBOX_CHILDREN | MAILBOX_NOCHILDREN);
+ ctx->info.flags |= MAILBOX_NOINFERIORS;
+ }
+}
+
+static const char *
+fs_list_get_inbox_vname(struct fs_list_iterate_context *ctx)
+{
+ struct mail_namespace *ns = ctx->ctx.list->ns;
+
+ if ((ns->flags & NAMESPACE_FLAG_INBOX_USER) != 0)
+ return "INBOX";
+ else
+ return p_strconcat(ctx->info_pool, ns->prefix, "INBOX", NULL);
+}
+
+static bool
+list_file_unfound_inbox(struct fs_list_iterate_context *ctx)
+{
+ ctx->info.flags = 0;
+ ctx->info.vname = fs_list_get_inbox_vname(ctx);
+
+ if (mailbox_list_mailbox(ctx->ctx.list, "INBOX", &ctx->info.flags) < 0)
+ ctx->ctx.failed = TRUE;
+
+ if ((ctx->ctx.flags & MAILBOX_LIST_ITER_NO_AUTO_BOXES) != 0 &&
+ (ctx->info.flags & MAILBOX_NONEXISTENT) != 0)
+ return FALSE;
+
+ inbox_flags_set(ctx);
+ if (ctx->inbox_has_children)
+ ctx->info.flags |= MAILBOX_CHILDREN;
+ else {
+ /* we got here because we didn't see INBOX among other mailboxes,
+ which means it has no children. */
+ ctx->info.flags |= MAILBOX_NOCHILDREN;
+ }
+ return TRUE;
+}
+
+static bool
+list_file_is_any_inbox(struct fs_list_iterate_context *ctx,
+ const char *storage_name)
+{
+ const char *path, *inbox_path;
+
+ if (!fs_list_get_storage_path(ctx, storage_name, &path))
+ return FALSE;
+
+ if (mailbox_list_get_path(ctx->ctx.list, "INBOX",
+ MAILBOX_LIST_PATH_TYPE_DIR, &inbox_path) <= 0)
+ i_unreached();
+ return strcmp(path, inbox_path) == 0;
+}
+
+static int
+fs_list_entry(struct fs_list_iterate_context *ctx,
+ const struct list_dir_entry *entry)
+{
+ struct mail_namespace *ns = ctx->ctx.list->ns;
+ struct list_dir_context *dir, *subdir = NULL;
+ enum imap_match_result match, child_dir_match;
+ const char *storage_name, *vname, *child_dir_name;
+
+ dir = ctx->dir;
+ storage_name = dir_get_storage_name(dir, entry->fname);
+
+ vname = mailbox_list_get_vname(ctx->ctx.list, storage_name);
+ ctx->info.vname = p_strdup(ctx->info_pool, vname);
+ ctx->info.flags = entry->info_flags;
+
+ match = imap_match(ctx->ctx.glob, ctx->info.vname);
+
+ if (strcmp(ctx->info.vname, "INBOX") == 0 &&
+ ctx->ctx.list->ns->prefix_len > 0) {
+ /* INBOX's children are matched as prefix/INBOX */
+ child_dir_name = t_strdup_printf("%sINBOX", ns->prefix);
+ } else {
+ child_dir_name =
+ t_strdup_printf("%s%c", ctx->info.vname, ctx->sep);
+ }
+ child_dir_match = imap_match(ctx->ctx.glob, child_dir_name);
+ if (child_dir_match == IMAP_MATCH_YES)
+ child_dir_match |= IMAP_MATCH_CHILDREN;
+
+ if ((ctx->info.flags &
+ (MAILBOX_NOCHILDREN | MAILBOX_NOINFERIORS)) != 0) {
+ /* mailbox has no children */
+ } else if ((ctx->info.flags & MAILBOX_CHILDREN) != 0 &&
+ (child_dir_match & IMAP_MATCH_CHILDREN) == 0) {
+ /* mailbox has children, but we don't want to list them */
+ } else if (((ctx->ctx.flags & MAILBOX_LIST_ITER_RETURN_CHILDREN) != 0 ||
+ (child_dir_match & IMAP_MATCH_CHILDREN) != 0) &&
+ *entry->fname != '\0') {
+ /* a) mailbox has children and we want to return them
+ b) we don't want to return mailbox's children, but we need
+ to know if it has any */
+ subdir = fs_list_read_dir(ctx, storage_name, entry->info_flags);
+ subdir->parent = dir;
+ ctx->dir = subdir;
+ /* the scanning may have updated the dir's info flags */
+ ctx->info.flags = subdir->info_flags;
+ }
+
+ /* handle INBOXes correctly */
+ if (strcasecmp(ctx->info.vname, "INBOX") == 0 &&
+ (ns->flags & NAMESPACE_FLAG_INBOX_USER) != 0) {
+ /* either this is user's INBOX, or it's a naming conflict */
+ if (!list_file_is_any_inbox(ctx, storage_name)) {
+ if (subdir == NULL) {
+ /* no children */
+ } else if ((ctx->ctx.list->flags &
+ MAILBOX_LIST_FLAG_MAILBOX_FILES) == 0) {
+ if (strcmp(storage_name, "INBOX") == 0) {
+ /* INBOX and its children are in
+ different paths */
+ ctx->inbox_has_children = TRUE;
+ } else {
+ /* naming conflict, skip its
+ children also */
+ ctx->dir = dir;
+ pool_unref(&subdir->pool);
+ }
+ } else if ((ctx->info.flags & MAILBOX_NOINFERIORS) == 0) {
+ /* INBOX itself is \NoInferiors, but this INBOX
+ is a directory, and we can make INBOX have
+ children using it. */
+ ctx->inbox_has_children = TRUE;
+ }
+ return 0;
+ }
+ if (subdir != NULL)
+ ctx->inbox_has_children = TRUE;
+ inbox_flags_set(ctx);
+ ctx->info.vname = "INBOX"; /* always return uppercased */
+ ctx->inbox_found = TRUE;
+ } else if (strcmp(storage_name, "INBOX") == 0 &&
+ (ns->flags & NAMESPACE_FLAG_INBOX_USER) != 0) {
+ /* this is <ns prefix>/INBOX. don't return it, unless it has
+ children. */
+ i_assert(*ns->prefix != '\0');
+ if ((ctx->info.flags & MAILBOX_CHILDREN) == 0)
+ return 0;
+ /* although it could be selected with this name,
+ it would be confusing for clients to see the same
+ mails in both INBOX and <ns prefix>/INBOX. */
+ ctx->info.flags &= ENUM_NEGATE(MAILBOX_SELECT);
+ ctx->info.flags |= MAILBOX_NOSELECT;
+ } else if ((ns->flags & NAMESPACE_FLAG_INBOX_ANY) != 0 &&
+ list_file_is_any_inbox(ctx, storage_name)) {
+ if ((ns->flags & NAMESPACE_FLAG_INBOX_USER) != 0) {
+ /* probably mbox inbox file */
+ return 0;
+ }
+ /* shared/user/INBOX */
+ ctx->info.flags &= ENUM_NEGATE(MAILBOX_NOSELECT | MAILBOX_NONEXISTENT);
+ ctx->info.flags |= MAILBOX_SELECT;
+ ctx->inbox_found = TRUE;
+ }
+
+ if (match != IMAP_MATCH_YES) {
+ /* mailbox's children may match, but the mailbox itself
+ doesn't */
+ return 0;
+ }
+ if (mailbox_list_iter_try_delete_noselect(&ctx->ctx, &ctx->info, storage_name))
+ return 0;
+ return 1;
+}
+
+static int
+fs_list_next(struct fs_list_iterate_context *ctx)
+{
+ struct list_dir_context *dir;
+ const struct list_dir_entry *entries;
+ unsigned int count;
+ int ret;
+
+ while (ctx->dir != NULL) {
+ /* NOTE: fs_list_entry() may change ctx->dir */
+ entries = array_get(&ctx->dir->entries, &count);
+ while (ctx->dir->entry_idx < count) {
+ p_clear(ctx->info_pool);
+ ret = fs_list_entry(ctx, &entries[ctx->dir->entry_idx++]);
+ if (ret > 0)
+ return 1;
+ if (ret < 0)
+ ctx->ctx.failed = TRUE;
+ entries = array_get(&ctx->dir->entries, &count);
+ }
+
+ dir = ctx->dir;
+ ctx->dir = dir->parent;
+ pool_unref(&dir->pool);
+
+ if (ctx->dir == NULL)
+ fs_list_next_root(ctx);
+ }
+
+ if (ctx->inbox_has_children && ctx->ctx.list->ns->prefix_len > 0 &&
+ !ctx->listed_prefix_inbox) {
+ ctx->info.flags = MAILBOX_CHILDREN | MAILBOX_NOSELECT;
+ ctx->info.vname =
+ p_strconcat(ctx->info_pool,
+ ctx->ctx.list->ns->prefix, "INBOX", NULL);
+ ctx->listed_prefix_inbox = TRUE;
+ if (imap_match(ctx->ctx.glob, ctx->info.vname) == IMAP_MATCH_YES)
+ return 1;
+ }
+ if (!ctx->inbox_found && ctx->ctx.glob != NULL &&
+ (ctx->ctx.list->ns->flags & NAMESPACE_FLAG_INBOX_ANY) != 0 &&
+ imap_match(ctx->ctx.glob,
+ fs_list_get_inbox_vname(ctx)) == IMAP_MATCH_YES) {
+ /* INBOX wasn't seen while listing other mailboxes. It might
+ be located elsewhere. */
+ ctx->inbox_found = TRUE;
+ return list_file_unfound_inbox(ctx) ? 1 : 0;
+ }
+
+ /* finished */
+ return 0;
+}
+
+const struct mailbox_info *
+fs_list_iter_next(struct mailbox_list_iterate_context *_ctx)
+{
+ struct fs_list_iterate_context *ctx =
+ (struct fs_list_iterate_context *)_ctx;
+ int ret;
+
+ if ((_ctx->flags & MAILBOX_LIST_ITER_SELECT_SUBSCRIBED) != 0)
+ return mailbox_list_subscriptions_iter_next(_ctx);
+
+ T_BEGIN {
+ ret = fs_list_next(ctx);
+ } T_END;
+
+ if (ret == 0)
+ return mailbox_list_iter_default_next(_ctx);
+ else if (ret < 0)
+ return NULL;
+
+ if (_ctx->list->ns->type == MAIL_NAMESPACE_TYPE_SHARED &&
+ !_ctx->list->ns->list->mail_set->mail_shared_explicit_inbox &&
+ strlen(ctx->info.vname) < _ctx->list->ns->prefix_len) {
+ /* shared/user INBOX, IMAP code already lists it */
+ return fs_list_iter_next(_ctx);
+ }
+
+ if ((ctx->ctx.flags & MAILBOX_LIST_ITER_RETURN_SUBSCRIBED) != 0) {
+ mailbox_list_set_subscription_flags(ctx->ctx.list,
+ ctx->info.vname,
+ &ctx->info.flags);
+ }
+ i_assert(ctx->info.vname != NULL);
+ return &ctx->info;
+}
diff --git a/src/lib-storage/list/mailbox-list-fs.c b/src/lib-storage/list/mailbox-list-fs.c
new file mode 100644
index 0000000..c1aabb9
--- /dev/null
+++ b/src/lib-storage/list/mailbox-list-fs.c
@@ -0,0 +1,558 @@
+/* Copyright (c) 2006-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "hostpid.h"
+#include "mkdir-parents.h"
+#include "mailbox-log.h"
+#include "subscription-file.h"
+#include "mail-storage.h"
+#include "mailbox-list-subscriptions.h"
+#include "mailbox-list-delete.h"
+#include "mailbox-list-fs.h"
+
+#include <stdio.h>
+#include <unistd.h>
+#include <sys/stat.h>
+
+#define GLOBAL_TEMP_PREFIX ".temp."
+
+extern struct mailbox_list fs_mailbox_list;
+
+static struct mailbox_list *fs_list_alloc(void)
+{
+ struct fs_mailbox_list *list;
+ pool_t pool;
+
+ pool = pool_alloconly_create("fs list", 2048);
+
+ list = p_new(pool, struct fs_mailbox_list, 1);
+ list->list = fs_mailbox_list;
+ list->list.pool = pool;
+
+ list->temp_prefix = p_strconcat(pool, GLOBAL_TEMP_PREFIX,
+ my_hostname, ".", my_pid, ".", NULL);
+ return &list->list;
+}
+
+static void fs_list_deinit(struct mailbox_list *_list)
+{
+ struct fs_mailbox_list *list = (struct fs_mailbox_list *)_list;
+
+ pool_unref(&list->list.pool);
+}
+
+static char fs_list_get_hierarchy_sep(struct mailbox_list *list ATTR_UNUSED)
+{
+ return '/';
+}
+
+static const char *
+fs_list_get_path_to(const struct mailbox_list_settings *set,
+ const char *root_dir, const char *name)
+{
+ if (*set->maildir_name != '\0' && set->index_control_use_maildir_name) {
+ return t_strdup_printf("%s/%s%s/%s", root_dir,
+ set->mailbox_dir_name, name,
+ set->maildir_name);
+ } else {
+ return t_strdup_printf("%s/%s%s", root_dir,
+ set->mailbox_dir_name, name);
+ }
+}
+
+static int
+fs_list_get_path(struct mailbox_list *_list, const char *name,
+ enum mailbox_list_path_type type, const char **path_r)
+{
+ const struct mailbox_list_settings *set = &_list->set;
+ const char *root_dir, *error;
+
+ if (name == NULL) {
+ /* return root directories */
+ return mailbox_list_set_get_root_path(set, type, path_r) ? 1 : 0;
+ }
+
+ i_assert(mailbox_list_is_valid_name(_list, name, &error));
+
+ if (mailbox_list_try_get_absolute_path(_list, &name)) {
+ if (type == MAILBOX_LIST_PATH_TYPE_INDEX &&
+ *set->index_dir == '\0')
+ return 0;
+ *path_r = name;
+ return 1;
+ }
+
+ root_dir = set->root_dir;
+ switch (type) {
+ case MAILBOX_LIST_PATH_TYPE_DIR:
+ if (*set->maildir_name != '\0') {
+ *path_r = t_strdup_printf("%s/%s%s", set->root_dir,
+ set->mailbox_dir_name, name);
+ return 1;
+ }
+ break;
+ case MAILBOX_LIST_PATH_TYPE_ALT_DIR:
+ if (set->alt_dir == NULL)
+ return 0;
+ if (*set->maildir_name != '\0') {
+ /* maildir_name is for the mailbox, caller is asking
+ for the directory name */
+ *path_r = t_strdup_printf("%s/%s%s", set->alt_dir,
+ set->mailbox_dir_name, name);
+ return 1;
+ }
+ root_dir = set->alt_dir;
+ break;
+ case MAILBOX_LIST_PATH_TYPE_MAILBOX:
+ break;
+ case MAILBOX_LIST_PATH_TYPE_ALT_MAILBOX:
+ if (set->alt_dir == NULL)
+ return 0;
+ root_dir = set->alt_dir;
+ break;
+ case MAILBOX_LIST_PATH_TYPE_CONTROL:
+ if (set->control_dir != NULL) {
+ *path_r = fs_list_get_path_to(set, set->control_dir, name);
+ return 1;
+ }
+ break;
+ case MAILBOX_LIST_PATH_TYPE_INDEX_CACHE:
+ if (set->index_cache_dir != NULL) {
+ *path_r = fs_list_get_path_to(set, set->index_cache_dir, name);
+ return 1;
+ }
+ /* fall through */
+ case MAILBOX_LIST_PATH_TYPE_INDEX:
+ if (set->index_dir != NULL) {
+ if (*set->index_dir == '\0')
+ return 0;
+ *path_r = fs_list_get_path_to(set, set->index_dir, name);
+ return 1;
+ }
+ break;
+ case MAILBOX_LIST_PATH_TYPE_INDEX_PRIVATE:
+ if (set->index_pvt_dir == NULL)
+ return 0;
+ *path_r = fs_list_get_path_to(set, set->index_pvt_dir, name);
+ return 1;
+ case MAILBOX_LIST_PATH_TYPE_LIST_INDEX:
+ i_unreached();
+ }
+
+ if (type == MAILBOX_LIST_PATH_TYPE_ALT_DIR ||
+ type == MAILBOX_LIST_PATH_TYPE_ALT_MAILBOX) {
+ /* don't use inbox_path */
+ } else if (strcmp(name, "INBOX") == 0 && set->inbox_path != NULL) {
+ /* If INBOX is a file, index and control directories are
+ located in root directory. */
+ if ((_list->flags & MAILBOX_LIST_FLAG_MAILBOX_FILES) == 0 ||
+ type == MAILBOX_LIST_PATH_TYPE_MAILBOX ||
+ type == MAILBOX_LIST_PATH_TYPE_DIR) {
+ *path_r = set->inbox_path;
+ return 1;
+ }
+ }
+
+ if (root_dir == NULL)
+ return 0;
+ if (*set->maildir_name == '\0') {
+ *path_r = t_strdup_printf("%s/%s%s", root_dir,
+ set->mailbox_dir_name, name);
+ } else {
+ *path_r = t_strdup_printf("%s/%s%s/%s", root_dir,
+ set->mailbox_dir_name, name,
+ set->maildir_name);
+ }
+ return 1;
+}
+
+static const char *
+fs_list_get_temp_prefix(struct mailbox_list *_list, bool global)
+{
+ struct fs_mailbox_list *list = (struct fs_mailbox_list *)_list;
+
+ return global ? GLOBAL_TEMP_PREFIX : list->temp_prefix;
+}
+
+static const char *
+fs_list_join_refpattern(struct mailbox_list *_list ATTR_UNUSED,
+ const char *ref, const char *pattern)
+{
+ if (*pattern == '/' || *pattern == '~') {
+ /* pattern overrides reference */
+ } else if (*ref != '\0') {
+ /* merge reference and pattern */
+ pattern = t_strconcat(ref, pattern, NULL);
+ }
+ return pattern;
+}
+
+static int fs_list_set_subscribed(struct mailbox_list *_list,
+ const char *name, bool set)
+{
+ struct fs_mailbox_list *list = (struct fs_mailbox_list *)_list;
+ enum mailbox_list_path_type type;
+ const char *path;
+
+ if (_list->set.subscription_fname == NULL) {
+ mailbox_list_set_error(_list, MAIL_ERROR_NOTPOSSIBLE,
+ "Subscriptions not supported");
+ return -1;
+ }
+
+ type = _list->set.control_dir != NULL ?
+ MAILBOX_LIST_PATH_TYPE_CONTROL : MAILBOX_LIST_PATH_TYPE_DIR;
+
+ path = t_strconcat(mailbox_list_get_root_forced(_list, type),
+ "/", _list->set.subscription_fname, NULL);
+ return subsfile_set_subscribed(_list, path, list->temp_prefix,
+ name, set);
+}
+
+
+static const char *mailbox_list_fs_get_trash_dir(struct mailbox_list *list)
+{
+ const char *root_dir;
+
+ root_dir = mailbox_list_get_root_forced(list, MAILBOX_LIST_PATH_TYPE_DIR);
+ return t_strdup_printf("%s/"MAILBOX_LIST_FS_TRASH_DIR_NAME, root_dir);
+}
+
+static int
+fs_list_delete_maildir(struct mailbox_list *list, const char *name)
+{
+ const char *path, *trash_dir;
+ bool rmdir_path;
+ int ret;
+
+ if (*list->set.maildir_name != '\0' &&
+ *list->set.mailbox_dir_name != '\0') {
+ trash_dir = mailbox_list_fs_get_trash_dir(list);
+ ret = mailbox_list_delete_maildir_via_trash(list, name,
+ trash_dir);
+ if (ret < 0)
+ return -1;
+
+ if (ret > 0) {
+ /* try to delete the parent directory */
+ if (mailbox_list_get_path(list, name,
+ MAILBOX_LIST_PATH_TYPE_DIR,
+ &path) <= 0)
+ i_unreached();
+ if (rmdir(path) < 0 && errno != ENOENT &&
+ errno != ENOTEMPTY && errno != EEXIST) {
+ mailbox_list_set_critical(list,
+ "rmdir(%s) failed: %m", path);
+ }
+ return 0;
+ }
+ }
+
+ rmdir_path = *list->set.maildir_name != '\0';
+ if (mailbox_list_get_path(list, name, MAILBOX_LIST_PATH_TYPE_MAILBOX,
+ &path) <= 0)
+ i_unreached();
+ return mailbox_list_delete_mailbox_nonrecursive(list, name, path,
+ rmdir_path);
+}
+
+static int fs_list_delete_mailbox(struct mailbox_list *list, const char *name)
+{
+ const char *path;
+ int ret;
+
+ ret = mailbox_list_get_path(list, name,
+ MAILBOX_LIST_PATH_TYPE_MAILBOX, &path);
+ if (ret < 0)
+ return -1;
+ i_assert(ret > 0);
+
+ if ((list->flags & MAILBOX_LIST_FLAG_MAILBOX_FILES) != 0) {
+ ret = mailbox_list_delete_mailbox_file(list, name, path);
+ } else {
+ ret = fs_list_delete_maildir(list, name);
+ }
+ if (ret == 0 && list->set.no_noselect)
+ mailbox_list_delete_until_root(list, path, MAILBOX_LIST_PATH_TYPE_MAILBOX);
+
+ i_assert(ret <= 0);
+ return mailbox_list_delete_finish_ret(list, name, ret == 0);
+}
+
+static int fs_list_rmdir(struct mailbox_list *list, const char *name,
+ const char *path)
+{
+ guid_128_t dir_sha128;
+
+ if (rmdir(path) < 0)
+ return -1;
+
+ mailbox_name_get_sha128(name, dir_sha128);
+ mailbox_list_add_change(list, MAILBOX_LOG_RECORD_DELETE_DIR,
+ dir_sha128);
+ return 0;
+}
+
+static int fs_list_delete_dir(struct mailbox_list *list, const char *name)
+{
+ const char *path, *child_name, *child_path, *p;
+ char sep;
+ int ret;
+
+ if (mailbox_list_get_path(list, name, MAILBOX_LIST_PATH_TYPE_DIR,
+ &path) <= 0)
+ i_unreached();
+ ret = fs_list_rmdir(list, name, path);
+ if (!list->set.iter_from_index_dir) {
+ /* it should exist only in the mail directory */
+ if (ret == 0)
+ return 0;
+ } else if (ret == 0 || errno == ENOENT) {
+ /* the primary list location is the index directory, but it
+ exists in both index and mail directories. */
+ if (mailbox_list_get_path(list, name, MAILBOX_LIST_PATH_TYPE_INDEX,
+ &path) <= 0)
+ i_unreached();
+ if (fs_list_rmdir(list, name, path) == 0)
+ return 0;
+ if (ret == 0 && errno == ENOENT) {
+ /* partial existence: exists in _DIR, but not in
+ _INDEX. return success anyway. */
+ return 0;
+ }
+ /* a) both directories didn't exist
+ b) index directory couldn't be rmdir()ed for some reason */
+ }
+
+ if (errno == ENOENT || errno == ENOTDIR) {
+ mailbox_list_set_error(list, MAIL_ERROR_NOTFOUND,
+ T_MAILBOX_LIST_ERR_NOT_FOUND(list, name));
+ } else if (errno == ENOTEMPTY || errno == EEXIST) {
+ /* mbox workaround: if only .imap/ directory is preventing the
+ deletion, remove it */
+ sep = mailbox_list_get_hierarchy_sep(list);
+ child_name = t_strdup_printf("%s%cchild", name, sep);
+ if (mailbox_list_get_path(list, child_name,
+ MAILBOX_LIST_PATH_TYPE_INDEX,
+ &child_path) > 0 &&
+ str_begins(child_path, path)) {
+ /* drop the "/child" part out. */
+ p = strrchr(child_path, '/');
+ if (rmdir(t_strdup_until(child_path, p)) == 0) {
+ /* try again */
+ if (fs_list_rmdir(list, name, path) == 0)
+ return 0;
+ }
+ }
+
+ mailbox_list_set_error(list, MAIL_ERROR_EXISTS,
+ "Mailbox has children, delete them first");
+ } else {
+ mailbox_list_set_critical(list, "rmdir(%s) failed: %m", path);
+ }
+ return -1;
+}
+
+static int rename_dir(struct mailbox_list *oldlist, const char *oldname,
+ struct mailbox_list *newlist, const char *newname,
+ enum mailbox_list_path_type type, bool rmdir_parent)
+{
+ struct stat st;
+ const char *oldpath, *newpath, *p, *oldparent, *newparent;
+
+ if (mailbox_list_get_path(oldlist, oldname, type, &oldpath) <= 0 ||
+ mailbox_list_get_path(newlist, newname, type, &newpath) <= 0)
+ return 0;
+
+ if (strcmp(oldpath, newpath) == 0)
+ return 0;
+
+ p = strrchr(oldpath, '/');
+ oldparent = p == NULL ? "/" : t_strdup_until(oldpath, p);
+ p = strrchr(newpath, '/');
+ newparent = p == NULL ? "/" : t_strdup_until(newpath, p);
+
+ if (strcmp(oldparent, newparent) != 0 && stat(oldpath, &st) == 0) {
+ /* make sure the newparent exists */
+ struct mailbox_permissions perm;
+
+ mailbox_list_get_root_permissions(newlist, &perm);
+ if (mkdir_parents_chgrp(newparent, perm.dir_create_mode,
+ perm.file_create_gid,
+ perm.file_create_gid_origin) < 0 &&
+ errno != EEXIST) {
+ if (mailbox_list_set_error_from_errno(oldlist))
+ return -1;
+
+ mailbox_list_set_critical(oldlist,
+ "mkdir_parents(%s) failed: %m", newparent);
+ return -1;
+ }
+ }
+
+ if (rename(oldpath, newpath) < 0 && errno != ENOENT) {
+ mailbox_list_set_critical(oldlist, "rename(%s, %s) failed: %m",
+ oldpath, newpath);
+ return -1;
+ }
+ if (rmdir_parent && (p = strrchr(oldpath, '/')) != NULL) {
+ oldpath = t_strdup_until(oldpath, p);
+ if (rmdir(oldpath) < 0 && errno != ENOENT &&
+ errno != ENOTEMPTY && errno != EEXIST) {
+ mailbox_list_set_critical(oldlist,
+ "rmdir(%s) failed: %m", oldpath);
+ }
+ }
+
+ /* avoid leaving empty directories lying around */
+ mailbox_list_delete_until_root(oldlist, oldpath, type);
+ return 0;
+}
+
+static int fs_list_rename_mailbox(struct mailbox_list *oldlist,
+ const char *oldname,
+ struct mailbox_list *newlist,
+ const char *newname)
+{
+ struct mail_storage *oldstorage;
+ const char *oldvname, *oldpath, *newpath, *alt_newpath, *root_path, *p;
+ struct stat st;
+ struct mailbox_permissions old_perm, new_perm;
+ bool rmdir_parent = FALSE;
+
+ oldvname = mailbox_list_get_vname(oldlist, oldname);
+ if (mailbox_list_get_storage(&oldlist, oldvname, &oldstorage) < 0)
+ return -1;
+
+ if (mailbox_list_get_path(oldlist, oldname,
+ MAILBOX_LIST_PATH_TYPE_DIR, &oldpath) <= 0 ||
+ mailbox_list_get_path(newlist, newname,
+ MAILBOX_LIST_PATH_TYPE_DIR, &newpath) <= 0)
+ i_unreached();
+ if (mailbox_list_get_path(newlist, newname, MAILBOX_LIST_PATH_TYPE_ALT_DIR,
+ &alt_newpath) < 0)
+ i_unreached();
+
+ root_path = mailbox_list_get_root_forced(oldlist, MAILBOX_LIST_PATH_TYPE_MAILBOX);
+ if (strcmp(oldpath, root_path) == 0) {
+ /* most likely INBOX */
+ mailbox_list_set_error(oldlist, MAIL_ERROR_NOTPOSSIBLE,
+ t_strdup_printf("Renaming %s isn't supported.",
+ oldname));
+ return -1;
+ }
+
+ mailbox_list_get_permissions(oldlist, oldname, &old_perm);
+ mailbox_list_get_permissions(newlist, newname, &new_perm);
+
+ /* if we're renaming under another mailbox, require its permissions
+ to be same as ours. */
+ if (strchr(newname, mailbox_list_get_hierarchy_sep(newlist)) != NULL &&
+ (new_perm.file_create_mode != old_perm.file_create_mode ||
+ new_perm.dir_create_mode != old_perm.dir_create_mode ||
+ new_perm.file_create_gid != old_perm.file_create_gid)) {
+ mailbox_list_set_error(oldlist, MAIL_ERROR_NOTPOSSIBLE,
+ "Renaming not supported across conflicting "
+ "directory permissions");
+ return -1;
+ }
+
+ /* create the hierarchy */
+ p = strrchr(newpath, '/');
+ if (p != NULL) {
+ p = t_strdup_until(newpath, p);
+ if (mkdir_parents_chgrp(p, new_perm.dir_create_mode,
+ new_perm.file_create_gid,
+ new_perm.file_create_gid_origin) < 0 &&
+ errno != EEXIST) {
+ if (mailbox_list_set_error_from_errno(oldlist))
+ return -1;
+
+ mailbox_list_set_critical(oldlist,
+ "mkdir_parents(%s) failed: %m", p);
+ return -1;
+ }
+ }
+
+ /* first check that the destination mailbox doesn't exist.
+ this is racy, but we need to be atomic and there's hardly any
+ possibility that someone actually tries to rename two mailboxes
+ to same new one */
+ if (lstat(newpath, &st) == 0) {
+ mailbox_list_set_error(oldlist, MAIL_ERROR_EXISTS,
+ "Target mailbox already exists");
+ return -1;
+ } else if (errno == ENOTDIR) {
+ mailbox_list_set_error(oldlist, MAIL_ERROR_NOTPOSSIBLE,
+ "Target mailbox doesn't allow inferior mailboxes");
+ return -1;
+ } else if (errno != ENOENT && errno != EACCES) {
+ mailbox_list_set_critical(oldlist, "lstat(%s) failed: %m",
+ newpath);
+ return -1;
+ }
+
+ if (alt_newpath != NULL) {
+ if (stat(alt_newpath, &st) == 0) {
+ /* race condition or a directory left there lying around?
+ safest to just report error. */
+ mailbox_list_set_error(oldlist, MAIL_ERROR_EXISTS,
+ "Target mailbox already exists");
+ return -1;
+ } else if (errno != ENOENT) {
+ mailbox_list_set_critical(oldlist, "stat(%s) failed: %m",
+ alt_newpath);
+ return -1;
+ }
+ }
+
+ if (rename(oldpath, newpath) < 0) {
+ if (ENOTFOUND(errno)) {
+ mailbox_list_set_error(oldlist, MAIL_ERROR_NOTFOUND,
+ T_MAILBOX_LIST_ERR_NOT_FOUND(oldlist, oldname));
+ } else if (!mailbox_list_set_error_from_errno(oldlist)) {
+ mailbox_list_set_critical(oldlist,
+ "rename(%s, %s) failed: %m", oldpath, newpath);
+ }
+ return -1;
+ }
+
+ if (alt_newpath != NULL) {
+ (void)rename_dir(oldlist, oldname, newlist, newname,
+ MAILBOX_LIST_PATH_TYPE_ALT_DIR, rmdir_parent);
+ }
+ (void)rename_dir(oldlist, oldname, newlist, newname,
+ MAILBOX_LIST_PATH_TYPE_CONTROL, rmdir_parent);
+ (void)rename_dir(oldlist, oldname, newlist, newname,
+ MAILBOX_LIST_PATH_TYPE_INDEX, rmdir_parent);
+ (void)rename_dir(oldlist, oldname, newlist, newname,
+ MAILBOX_LIST_PATH_TYPE_INDEX_CACHE, rmdir_parent);
+ return 0;
+}
+
+struct mailbox_list fs_mailbox_list = {
+ .name = MAILBOX_LIST_NAME_FS,
+ .props = 0,
+ .mailbox_name_max_length = MAILBOX_LIST_NAME_MAX_LENGTH,
+
+ .v = {
+ .alloc = fs_list_alloc,
+ .deinit = fs_list_deinit,
+ .get_hierarchy_sep = fs_list_get_hierarchy_sep,
+ .get_vname = mailbox_list_default_get_vname,
+ .get_storage_name = mailbox_list_default_get_storage_name,
+ .get_path = fs_list_get_path,
+ .get_temp_prefix = fs_list_get_temp_prefix,
+ .join_refpattern = fs_list_join_refpattern,
+ .iter_init = fs_list_iter_init,
+ .iter_next = fs_list_iter_next,
+ .iter_deinit = fs_list_iter_deinit,
+ .get_mailbox_flags = fs_list_get_mailbox_flags,
+ .subscriptions_refresh = mailbox_list_subscriptions_refresh,
+ .set_subscribed = fs_list_set_subscribed,
+ .delete_mailbox = fs_list_delete_mailbox,
+ .delete_dir = fs_list_delete_dir,
+ .delete_symlink = mailbox_list_delete_symlink_default,
+ .rename_mailbox = fs_list_rename_mailbox,
+ }
+};
diff --git a/src/lib-storage/list/mailbox-list-fs.h b/src/lib-storage/list/mailbox-list-fs.h
new file mode 100644
index 0000000..3841890
--- /dev/null
+++ b/src/lib-storage/list/mailbox-list-fs.h
@@ -0,0 +1,28 @@
+#ifndef MAILBOX_LIST_FS_H
+#define MAILBOX_LIST_FS_H
+
+#include "mailbox-list-private.h"
+
+/* When doing deletion via renaming it first to trash directory, use this as
+ the trash directory name */
+#define MAILBOX_LIST_FS_TRASH_DIR_NAME "..DOVECOT-TrasH"
+
+struct fs_mailbox_list {
+ struct mailbox_list list;
+
+ const char *temp_prefix;
+};
+
+struct mailbox_list_iterate_context *
+fs_list_iter_init(struct mailbox_list *_list, const char *const *patterns,
+ enum mailbox_list_iter_flags flags);
+int fs_list_iter_deinit(struct mailbox_list_iterate_context *ctx);
+const struct mailbox_info *
+fs_list_iter_next(struct mailbox_list_iterate_context *ctx);
+
+int fs_list_get_mailbox_flags(struct mailbox_list *list,
+ const char *dir, const char *fname,
+ enum mailbox_list_file_type type,
+ enum mailbox_info_flags *flags);
+
+#endif
diff --git a/src/lib-storage/list/mailbox-list-index-backend.c b/src/lib-storage/list/mailbox-list-index-backend.c
new file mode 100644
index 0000000..15dac41
--- /dev/null
+++ b/src/lib-storage/list/mailbox-list-index-backend.c
@@ -0,0 +1,979 @@
+/* Copyright (c) 2013-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "hostpid.h"
+#include "str.h"
+#include "mail-index.h"
+#include "subscription-file.h"
+#include "mailbox-list-delete.h"
+#include "mailbox-list-subscriptions.h"
+#include "mailbox-list-index-storage.h"
+#include "mailbox-list-index-sync.h"
+
+#include <stdio.h>
+
+#define GLOBAL_TEMP_PREFIX ".temp."
+#define MAILBOX_LIST_INDEX_DEFAULT_ESCAPE_CHAR '^'
+
+struct index_mailbox_list {
+ struct mailbox_list list;
+ const char *temp_prefix;
+
+ const char *create_mailbox_name;
+ guid_128_t create_mailbox_guid;
+};
+
+extern struct mailbox_list index_mailbox_list;
+
+static int
+index_list_rename_mailbox(struct mailbox_list *_oldlist, const char *oldname,
+ struct mailbox_list *_newlist, const char *newname);
+
+static struct mailbox_list *index_list_alloc(void)
+{
+ struct index_mailbox_list *list;
+ pool_t pool;
+
+ pool = pool_alloconly_create("index list", 2048);
+
+ list = p_new(pool, struct index_mailbox_list, 1);
+ list->list = index_mailbox_list;
+ list->list.pool = pool;
+ list->list.set.storage_name_escape_char = MAILBOX_LIST_INDEX_DEFAULT_ESCAPE_CHAR;
+
+ list->temp_prefix = p_strconcat(pool, GLOBAL_TEMP_PREFIX,
+ my_hostname, ".", my_pid, ".", NULL);
+ return &list->list;
+}
+
+static int index_list_init(struct mailbox_list *_list, const char **error_r)
+{
+ if (!_list->mail_set->mailbox_list_index) {
+ *error_r = "LAYOUT=index requires mailbox_list_index=yes";
+ return -1;
+ }
+ return 0;
+}
+
+static void index_list_deinit(struct mailbox_list *_list)
+{
+ struct index_mailbox_list *list = (struct index_mailbox_list *)_list;
+
+ pool_unref(&list->list.pool);
+}
+
+static char index_list_get_hierarchy_sep(struct mailbox_list *list)
+{
+ char sep = list->ns->set->separator[0];
+
+ if (sep == '\0')
+ sep = MAILBOX_LIST_INDEX_HIERARCHY_SEP;
+ if (sep == list->set.storage_name_escape_char) {
+ /* Separator conflicts with the escape character.
+ Use something else. */
+ if (sep != MAILBOX_LIST_INDEX_HIERARCHY_SEP)
+ sep = MAILBOX_LIST_INDEX_HIERARCHY_SEP;
+ else
+ sep = MAILBOX_LIST_INDEX_HIERARCHY_ALT_SEP;
+ }
+ return sep;
+}
+
+static int
+index_list_get_refreshed_node_seq(struct index_mailbox_list *list,
+ struct mail_index_view *view,
+ const char *name,
+ struct mailbox_list_index_node **node_r,
+ uint32_t *seq_r)
+{
+ unsigned int i;
+
+ *node_r = NULL;
+ *seq_r = 0;
+
+ for (i = 0; i < 2; i++) {
+ *node_r = mailbox_list_index_lookup(&list->list, name);
+ if (*node_r == NULL)
+ return 0;
+ if (mail_index_lookup_seq(view, (*node_r)->uid, seq_r))
+ return 1;
+ /* mailbox was just expunged. refreshing should notice it. */
+ if (mailbox_list_index_refresh_force(&list->list) < 0)
+ return -1;
+ }
+ i_panic("mailbox list index: refreshing doesn't lose expunged uid=%u",
+ (*node_r)->uid);
+ return -1;
+}
+
+static const char *
+index_get_guid_path(struct mailbox_list *_list, const char *root_dir,
+ const guid_128_t mailbox_guid)
+{
+ return t_strdup_printf("%s/%s%s", root_dir,
+ _list->set.mailbox_dir_name,
+ guid_128_to_string(mailbox_guid));
+}
+
+static int
+index_list_get_path(struct mailbox_list *_list, const char *name,
+ enum mailbox_list_path_type type, const char **path_r)
+{
+ struct index_mailbox_list *list = (struct index_mailbox_list *)_list;
+ struct mail_index_view *view;
+ struct mailbox_list_index_node *node;
+ struct mailbox_status status;
+ guid_128_t mailbox_guid;
+ const char *root_dir, *reason;
+ uint32_t seq;
+ int ret;
+
+ if (name == NULL) {
+ /* return root directories */
+ return mailbox_list_set_get_root_path(&_list->set, type,
+ path_r) ? 1 : 0;
+ }
+ /* consistently use mailbox_dir_name as part of all mailbox
+ directories (index/control/etc) */
+ switch (type) {
+ case MAILBOX_LIST_PATH_TYPE_MAILBOX:
+ type = MAILBOX_LIST_PATH_TYPE_DIR;
+ break;
+ case MAILBOX_LIST_PATH_TYPE_ALT_MAILBOX:
+ type = MAILBOX_LIST_PATH_TYPE_ALT_DIR;
+ break;
+ case MAILBOX_LIST_PATH_TYPE_LIST_INDEX:
+ i_unreached();
+ default:
+ break;
+ }
+ if (!mailbox_list_set_get_root_path(&_list->set, type, &root_dir))
+ return 0;
+
+ if (list->create_mailbox_name != NULL &&
+ strcmp(list->create_mailbox_name, name) == 0) {
+ *path_r = index_get_guid_path(_list, root_dir,
+ list->create_mailbox_guid);
+ return 1;
+ }
+
+ /* ilist is only required from this point onwards.
+ At least imapc calls index_list_get_path without this context*/
+ struct mailbox_list_index *ilist = INDEX_LIST_CONTEXT_REQUIRE(_list);
+
+ if (ilist->sync_ctx != NULL) {
+ /* we could get here during sync from
+ index_list_mailbox_create_selectable() */
+ view = ilist->sync_ctx->view;
+ node = mailbox_list_index_lookup(&list->list, name);
+ if (node == NULL) {
+ seq = 0;
+ ret = 0;
+ } else if (mail_index_lookup_seq(view, node->uid, &seq)) {
+ ret = 1;
+ } else {
+ i_panic("mailbox list index: lost uid=%u", node->uid);
+ }
+ } else {
+ if (mailbox_list_index_refresh(&list->list) < 0)
+ return -1;
+ view = mail_index_view_open(ilist->index);
+ ret = index_list_get_refreshed_node_seq(list, view, name, &node, &seq);
+ if (ret < 0) {
+ mail_index_view_close(&view);
+ return -1;
+ }
+ }
+ i_assert(ret == 0 || seq != 0);
+ if (ret == 0) {
+ mailbox_list_set_error(_list, MAIL_ERROR_NOTFOUND,
+ T_MAIL_ERR_MAILBOX_NOT_FOUND(name));
+ ret = -1;
+ } else if (!mailbox_list_index_status(_list, view, seq, 0,
+ &status, mailbox_guid,
+ NULL, &reason) ||
+ guid_128_is_empty(mailbox_guid)) {
+ mailbox_list_set_error(_list, MAIL_ERROR_NOTFOUND,
+ T_MAIL_ERR_MAILBOX_NOT_FOUND(name));
+ ret = -1;
+ } else {
+ *path_r = index_get_guid_path(_list, root_dir, mailbox_guid);
+ ret = 1;
+ }
+ if (ilist->sync_ctx == NULL)
+ mail_index_view_close(&view);
+ return ret;
+}
+
+static const char *
+index_list_get_temp_prefix(struct mailbox_list *_list, bool global)
+{
+ struct index_mailbox_list *list = (struct index_mailbox_list *)_list;
+
+ return global ? GLOBAL_TEMP_PREFIX : list->temp_prefix;
+}
+
+static int index_list_set_subscribed(struct mailbox_list *_list,
+ const char *name, bool set)
+{
+ struct index_mailbox_list *list = (struct index_mailbox_list *)_list;
+ const char *path;
+
+ if (_list->set.subscription_fname == NULL) {
+ mailbox_list_set_error(_list, MAIL_ERROR_NOTPOSSIBLE,
+ "Subscriptions not supported");
+ return -1;
+ }
+
+ path = t_strconcat(_list->set.control_dir != NULL ?
+ _list->set.control_dir : _list->set.root_dir,
+ "/", _list->set.subscription_fname, NULL);
+ return subsfile_set_subscribed(_list, path, list->temp_prefix,
+ name, set);
+}
+
+static int
+index_list_node_exists(struct index_mailbox_list *list, const char *name,
+ enum mailbox_existence *existence_r)
+{
+ struct mailbox_list_index_node *node;
+
+ *existence_r = MAILBOX_EXISTENCE_NONE;
+
+ if (mailbox_list_index_refresh(&list->list) < 0)
+ return -1;
+
+ node = mailbox_list_index_lookup(&list->list, name);
+ if (node == NULL)
+ return 0;
+
+ if ((node->flags & (MAILBOX_LIST_INDEX_FLAG_NONEXISTENT |
+ MAILBOX_LIST_INDEX_FLAG_NOSELECT)) == 0) {
+ /* selectable */
+ *existence_r = MAILBOX_EXISTENCE_SELECT;
+ } else {
+ /* non-selectable */
+ *existence_r = MAILBOX_EXISTENCE_NOSELECT;
+ }
+ return 0;
+}
+
+static int
+index_list_mailbox_create_dir(struct index_mailbox_list *list, const char *name)
+{
+ struct mailbox_list_index_sync_context *sync_ctx;
+ struct mailbox_list_index_node *node;
+ uint32_t seq;
+ bool created;
+ int ret;
+
+ if (mailbox_list_index_sync_begin(&list->list, &sync_ctx) < 0)
+ return -1;
+
+ seq = mailbox_list_index_sync_name(sync_ctx, name, &node, &created);
+ if (created || (node->flags & MAILBOX_LIST_INDEX_FLAG_NONEXISTENT) != 0) {
+ /* didn't already exist */
+ node->flags = MAILBOX_LIST_INDEX_FLAG_NOSELECT;
+ mail_index_update_flags(sync_ctx->trans, seq, MODIFY_REPLACE,
+ (enum mail_flags)node->flags);
+ ret = 1;
+ } else {
+ /* already existed */
+ ret = 0;
+ }
+ if (mailbox_list_index_sync_end(&sync_ctx, TRUE) < 0)
+ ret = -1;
+ return ret;
+}
+
+static int
+index_list_mailbox_create_selectable(struct mailbox *box,
+ const guid_128_t mailbox_guid)
+{
+ struct index_mailbox_list *list =
+ (struct index_mailbox_list *)box->list;
+ struct mailbox_list_index *ilist = INDEX_LIST_CONTEXT_REQUIRE(box->list);
+ struct mailbox_list_index_sync_context *sync_ctx;
+ struct mailbox_list_index_record rec;
+ struct mailbox_list_index_node *node;
+ const void *data;
+ bool expunged, created;
+ uint32_t seq;
+
+ if (mailbox_list_index_sync_begin(&list->list, &sync_ctx) < 0)
+ return -1;
+
+ seq = mailbox_list_index_sync_name(sync_ctx, box->name, &node, &created);
+ if (box->corrupted_mailbox_name) {
+ /* an existing mailbox is being created with a "unknown" name.
+ opening the mailbox will hopefully find its real name and
+ rename it. */
+ node->flags |= MAILBOX_LIST_INDEX_FLAG_CORRUPTED_NAME;
+ mail_index_update_flags(sync_ctx->trans, seq, MODIFY_ADD,
+ (enum mail_flags)MAILBOX_LIST_INDEX_FLAG_CORRUPTED_NAME);
+ }
+ if (!created &&
+ (node->flags & (MAILBOX_LIST_INDEX_FLAG_NONEXISTENT |
+ MAILBOX_LIST_INDEX_FLAG_NOSELECT)) == 0) {
+ /* already selectable */
+ (void)mailbox_list_index_sync_end(&sync_ctx, TRUE);
+ return 0;
+ }
+
+ mail_index_lookup_ext(sync_ctx->view, seq, ilist->ext_id,
+ &data, &expunged);
+ i_assert(data != NULL && !expunged);
+ memcpy(&rec, data, sizeof(rec));
+ i_assert(guid_128_is_empty(rec.guid));
+
+ /* make it selectable */
+ node->flags &= ENUM_NEGATE(MAILBOX_LIST_INDEX_FLAG_NONEXISTENT |
+ MAILBOX_LIST_INDEX_FLAG_NOSELECT |
+ MAILBOX_LIST_INDEX_FLAG_NOINFERIORS);
+ mail_index_update_flags(sync_ctx->trans, seq, MODIFY_REPLACE,
+ (enum mail_flags)node->flags);
+
+ /* set UIDVALIDITY if was set by the storage */
+ if (box->index != NULL) {
+ struct mail_index_view *view;
+
+ view = mail_index_view_open(box->index);
+ if (mail_index_get_header(view)->uid_validity != 0)
+ rec.uid_validity = mail_index_get_header(view)->uid_validity;
+ mail_index_view_close(&view);
+ }
+
+ /* set GUID */
+ memcpy(rec.guid, mailbox_guid, sizeof(rec.guid));
+ mail_index_update_ext(sync_ctx->trans, seq, ilist->ext_id, &rec, NULL);
+
+ if (mailbox_list_index_sync_end(&sync_ctx, TRUE) < 0) {
+ /* make sure we forget any changes done internally */
+ mailbox_list_index_reset(ilist);
+ return -1;
+ }
+ return 1;
+}
+
+static int
+index_list_mailbox_create(struct mailbox *box,
+ const struct mailbox_update *update, bool directory)
+{
+ struct index_list_mailbox *ibox = INDEX_LIST_STORAGE_CONTEXT(box);
+ struct index_mailbox_list *list =
+ (struct index_mailbox_list *)box->list;
+ struct mailbox_update new_update;
+ enum mailbox_existence existence;
+ int ret;
+
+ /* first do a quick check that it doesn't exist */
+ if (index_list_node_exists(list, box->name, &existence) < 0) {
+ mail_storage_copy_list_error(box->storage, box->list);
+ return -1;
+ }
+ if (existence == MAILBOX_EXISTENCE_NONE && directory) {
+ /* now add the directory to index locked */
+ if ((ret = index_list_mailbox_create_dir(list, box->name)) < 0) {
+ mail_storage_copy_list_error(box->storage, box->list);
+ return -1;
+ }
+ } else if (existence != MAILBOX_EXISTENCE_SELECT && !directory) {
+ /* if no GUID is requested, generate it ourself. set
+ UIDVALIDITY to index sometimes later. */
+ if (update == NULL)
+ i_zero(&new_update);
+ else
+ new_update = *update;
+ if (guid_128_is_empty(new_update.mailbox_guid))
+ guid_128_generate(new_update.mailbox_guid);
+
+ /* create the backend mailbox first before it exists in the
+ list. the mailbox creation wants to use get_path() though,
+ so use a bit kludgy create_mailbox_* variables during the
+ creation to return the path. we'll also support recursively
+ creating more mailboxes in here. */
+ const char *old_name;
+ guid_128_t old_guid;
+
+ old_name = list->create_mailbox_name;
+ guid_128_copy(old_guid, list->create_mailbox_guid);
+
+ list->create_mailbox_name = box->name;
+ guid_128_copy(list->create_mailbox_guid, new_update.mailbox_guid);
+
+ ret = ibox->module_ctx.super.create_box(box, &new_update, FALSE);
+
+ if (ret == 0) {
+ /* backend mailbox was successfully created. now add it
+ to the list. */
+ ret = index_list_mailbox_create_selectable(box, new_update.mailbox_guid);
+ if (ret < 0)
+ mail_storage_copy_list_error(box->storage, box->list);
+ if (ret <= 0) {
+ /* failed to add to list. rollback the backend
+ mailbox creation */
+ bool create_error = ret < 0;
+
+ if (create_error)
+ mail_storage_last_error_push(box->storage);
+ if (mailbox_delete(box) < 0)
+ ret = -1;
+ if (create_error)
+ mail_storage_last_error_pop(box->storage);
+ }
+ }
+ list->create_mailbox_name = old_name;
+ guid_128_copy(list->create_mailbox_guid, old_guid);
+ if (ret < 0)
+ return ret;
+ } else {
+ ret = 0;
+ }
+
+ if (ret == 0) {
+ mail_storage_set_error(box->storage, MAIL_ERROR_EXISTS,
+ "Mailbox already exists");
+ return -1;
+ }
+ return 0;
+}
+
+static int
+index_list_mailbox_update(struct mailbox *box,
+ const struct mailbox_update *update)
+{
+ struct index_list_mailbox *ibox = INDEX_LIST_STORAGE_CONTEXT(box);
+ const char *root_dir, *old_path, *new_path;
+
+ if (mailbox_list_get_path(box->list, box->name,
+ MAILBOX_LIST_PATH_TYPE_MAILBOX,
+ &old_path) <= 0)
+ old_path = NULL;
+
+ if (ibox->module_ctx.super.update_box(box, update) < 0)
+ return -1;
+
+ /* rename the directory */
+ if (!guid_128_is_empty(update->mailbox_guid) && old_path != NULL &&
+ mailbox_list_set_get_root_path(&box->list->set,
+ MAILBOX_LIST_PATH_TYPE_DIR,
+ &root_dir)) {
+ new_path = index_get_guid_path(box->list, root_dir,
+ update->mailbox_guid);
+ if (strcmp(old_path, new_path) == 0)
+ ;
+ else if (rename(old_path, new_path) == 0)
+ ;
+ else if (errno == ENOENT) {
+ mail_storage_set_error(box->storage, MAIL_ERROR_NOTFOUND,
+ T_MAIL_ERR_MAILBOX_NOT_FOUND(box->name));
+ return -1;
+ } else {
+ mailbox_set_critical(box, "rename(%s, %s) failed: %m",
+ old_path, new_path);
+ return -1;
+ }
+ }
+
+ mailbox_list_index_update_mailbox_index(box, update);
+ return 0;
+}
+
+static int
+index_list_mailbox_exists(struct mailbox *box, bool auto_boxes,
+ enum mailbox_existence *existence_r)
+{
+ if (auto_boxes && mailbox_is_autocreated(box)) {
+ *existence_r = MAILBOX_EXISTENCE_SELECT;
+ return 0;
+ }
+
+ struct index_mailbox_list *list =
+ (struct index_mailbox_list *)box->list;
+
+ if (index_list_node_exists(list, box->name, existence_r) < 0) {
+ mail_storage_copy_list_error(box->storage, box->list);
+ return -1;
+ }
+ return 0;
+}
+
+static bool mailbox_has_corrupted_name(struct mailbox *box)
+{
+ struct mailbox_list_index_node *node;
+
+ if (box->corrupted_mailbox_name)
+ return TRUE;
+
+ node = mailbox_list_index_lookup(box->list, box->name);
+ return node != NULL &&
+ (node->flags & MAILBOX_LIST_INDEX_FLAG_CORRUPTED_NAME) != 0;
+}
+
+static void index_list_rename_corrupted(struct mailbox *box, const char *newname)
+{
+ if (index_list_rename_mailbox(box->list, box->name,
+ box->list, newname) == 0 ||
+ box->list->error != MAIL_ERROR_EXISTS)
+ return;
+
+ /* mailbox already exists. don't give up yet, just use the newname
+ as prefix and add the "lost-xx" as suffix. */
+ char sep = mailbox_list_get_hierarchy_sep(box->list);
+ const char *oldname = box->name;
+
+ /* oldname should be at the root level, but check for hierarchies
+ anyway to be safe. */
+ const char *p = strrchr(oldname, sep);
+ if (p != NULL)
+ oldname = p+1;
+
+ newname = t_strdup_printf("%s-%s", newname, oldname);
+ (void)index_list_rename_mailbox(box->list, box->name,
+ box->list, newname);
+}
+
+static int index_list_mailbox_open(struct mailbox *box)
+{
+ struct index_list_mailbox *ibox = INDEX_LIST_STORAGE_CONTEXT(box);
+ const void *data;
+ const unsigned char *name_hdr;
+ size_t name_hdr_size;
+
+ if (ibox->module_ctx.super.open(box) < 0)
+ return -1;
+
+ if (box->view == NULL) {
+ /* FIXME: dsync-merge is performing a delete in obox - remove
+ this check once dsync-merging is no longer used. */
+ return 0;
+ }
+
+ /* if mailbox name has changed, update it to the header. Use \0
+ as the hierarchy separator in the header. This is to make sure
+ we don't keep rewriting the name just in case some backend switches
+ between separators when accessed different ways. */
+
+ /* Get the current mailbox name with \0 separators. */
+ char sep = mailbox_list_get_hierarchy_sep(box->list);
+ char *box_zerosep_name = t_strdup_noconst(box->name);
+ size_t box_name_len = strlen(box_zerosep_name);
+ for (size_t i = 0; i < box_name_len; i++) {
+ if (box_zerosep_name[i] == sep)
+ box_zerosep_name[i] = '\0';
+ }
+
+ /* Does it match what's in the header now? */
+ mail_index_get_header_ext(box->view, box->box_name_hdr_ext_id,
+ &data, &name_hdr_size);
+ name_hdr = data;
+ while (name_hdr_size > 0 && name_hdr[name_hdr_size-1] == '\0') {
+ /* Remove trailing \0 - header doesn't shrink always */
+ name_hdr_size--;
+ }
+ if (name_hdr_size == box_name_len &&
+ memcmp(box_zerosep_name, name_hdr, box_name_len) == 0) {
+ /* Same mailbox name */
+ } else if (!mailbox_has_corrupted_name(box)) {
+ /* Mailbox name changed - update */
+ struct mail_index_transaction *trans =
+ mail_index_transaction_begin(box->view, 0);
+ mail_index_ext_resize_hdr(trans, box->box_name_hdr_ext_id,
+ box_name_len);
+ mail_index_update_header_ext(trans, box->box_name_hdr_ext_id, 0,
+ box_zerosep_name, box_name_len);
+ (void)mail_index_transaction_commit(&trans);
+ } else if (name_hdr_size > 0) {
+ /* Mailbox name is corrupted. Rename it to the previous name. */
+ char sep = mailbox_list_get_hierarchy_sep(box->list);
+ char *newname = t_malloc0(name_hdr_size + 1);
+ memcpy(newname, name_hdr, name_hdr_size);
+ for (size_t i = 0; i < name_hdr_size; i++) {
+ if (newname[i] == '\0')
+ newname[i] = sep;
+ }
+
+ index_list_rename_corrupted(box, newname);
+ }
+ return 0;
+}
+
+void mailbox_list_index_backend_sync_init(struct mailbox *box,
+ enum mailbox_sync_flags flags)
+{
+ struct mailbox_list_index *ilist = INDEX_LIST_CONTEXT_REQUIRE(box->list);
+
+ if ((flags & MAILBOX_SYNC_FLAG_FORCE_RESYNC) != 0 &&
+ !ilist->force_resynced) {
+ enum mail_storage_list_index_rebuild_reason reason =
+ MAIL_STORAGE_LIST_INDEX_REBUILD_REASON_FORCE_RESYNC;
+
+ if (box->storage->v.list_index_rebuild != NULL &&
+ box->storage->v.list_index_rebuild(box->storage, reason) < 0)
+ ilist->force_resync_failed = TRUE;
+ /* try to rebuild list index only once - even if it failed */
+ ilist->force_resynced = TRUE;
+ }
+}
+
+int mailbox_list_index_backend_sync_deinit(struct mailbox *box)
+{
+ struct mailbox_list_index *ilist = INDEX_LIST_CONTEXT_REQUIRE(box->list);
+
+ if (ilist->force_resync_failed) {
+ /* fail this only once */
+ ilist->force_resync_failed = FALSE;
+ return -1;
+ }
+ return 0;
+}
+
+static void
+index_list_try_delete(struct mailbox_list *_list, const char *name,
+ enum mailbox_list_path_type type)
+{
+ const char *mailbox_path, *path, *error;
+
+ if (mailbox_list_get_path(_list, name, MAILBOX_LIST_PATH_TYPE_MAILBOX,
+ &mailbox_path) <= 0 ||
+ mailbox_list_get_path(_list, name, type, &path) <= 0 ||
+ strcmp(path, mailbox_path) == 0)
+ return;
+
+ if (*_list->set.maildir_name == '\0' &&
+ (_list->flags & MAILBOX_LIST_FLAG_MAILBOX_FILES) == 0) {
+ /* this directory may contain also child mailboxes' data.
+ we don't want to delete that. */
+ bool rmdir_path = *_list->set.maildir_name != '\0';
+ if (mailbox_list_delete_mailbox_nonrecursive(_list, name, path,
+ rmdir_path) < 0)
+ return;
+ } else {
+ if (mailbox_list_delete_trash(path, &error) < 0 &&
+ errno != ENOTEMPTY) {
+ mailbox_list_set_critical(_list,
+ "unlink_directory(%s) failed: %s", path, error);
+ }
+ }
+
+ /* avoid leaving empty directories lying around */
+ mailbox_list_delete_until_root(_list, path, type);
+}
+
+static void
+index_list_delete_finish(struct mailbox_list *list, const char *name)
+{
+ index_list_try_delete(list, name, MAILBOX_LIST_PATH_TYPE_INDEX);
+ index_list_try_delete(list, name, MAILBOX_LIST_PATH_TYPE_CONTROL);
+ index_list_try_delete(list, name, MAILBOX_LIST_PATH_TYPE_ALT_MAILBOX);
+}
+
+static int
+index_list_delete_entry(struct index_mailbox_list *list, const char *name,
+ bool delete_selectable)
+{
+ struct mailbox_list_index_sync_context *sync_ctx;
+ int ret;
+
+ if (list->create_mailbox_name != NULL &&
+ strcmp(name, list->create_mailbox_name) == 0) {
+ /* we're rolling back a failed create. if the name exists in the
+ list, it was done by somebody else so we don't want to
+ remove it. */
+ return 0;
+ }
+
+ if (mailbox_list_index_sync_begin(&list->list, &sync_ctx) < 0)
+ return -1;
+ ret = mailbox_list_index_sync_delete(sync_ctx, name, delete_selectable);
+ if (mailbox_list_index_sync_end(&sync_ctx, TRUE) < 0)
+ return -1;
+ return ret;
+}
+
+static int
+index_list_try_delete_nonexistent_parent(struct mailbox_list *_list,
+ const char *name)
+{
+ struct index_mailbox_list *list =
+ container_of(_list, struct index_mailbox_list, list);
+ struct mailbox_list_index_node *node;
+ string_t *full_name;
+ const char *p;
+ char sep = mailbox_list_get_hierarchy_sep(_list);
+
+ if ((p = strrchr(name, sep)) == NULL) {
+ /* No occurrences of the hierarchy separator could be found
+ in the name, so the mailbox has no parents. */
+ return 0;
+ }
+
+ /* Lookup parent node of of given "name" */
+ node = mailbox_list_index_lookup(_list, t_strdup_until(name, p));
+ full_name = t_str_new(32);
+
+ while (node != NULL) {
+ /* Attempt to delete all parent nodes that are NOSELECT or
+ NONEXISTENT */
+ if (node->children != NULL)
+ break;
+ if ((node->flags & MAILBOX_LIST_INDEX_FLAG_NOSELECT) != 0 ||
+ (node->flags & MAILBOX_LIST_INDEX_FLAG_NONEXISTENT) != 0) {
+ /* The parent mailbox has no other children and is not
+ existant or not selectable, delete it */
+ str_truncate(full_name, 0);
+ mailbox_list_index_node_get_path(node, sep, full_name);
+ if (index_list_delete_entry(list, str_c(full_name), FALSE) < 0)
+ return -1;
+
+ if ((p = strrchr(str_c(full_name), sep)) == NULL) {
+ /* No occurrences of the hierarchy separator
+ could be found in the mailbox that was
+ just deleted. */
+ node = NULL;
+ } else {
+ /* lookup parent node of the node just deleted */
+ str_truncate(full_name, p - str_c(full_name));
+ node = mailbox_list_index_lookup(_list, str_c(full_name));
+ }
+ } else
+ break;
+
+ }
+ return 0;
+}
+
+static int
+index_list_delete_mailbox(struct mailbox_list *_list, const char *name)
+{
+ struct index_mailbox_list *list = (struct index_mailbox_list *)_list;
+ const char *path;
+ int ret;
+
+ /* first delete the mailbox files */
+ ret = mailbox_list_get_path(_list, name, MAILBOX_LIST_PATH_TYPE_MAILBOX,
+ &path);
+ if (ret <= 0)
+ return ret;
+
+ if ((_list->flags & (MAILBOX_LIST_FLAG_NO_MAIL_FILES |
+ MAILBOX_LIST_FLAG_NO_DELETES)) != 0) {
+ ret = 0;
+ } else if ((_list->flags & MAILBOX_LIST_FLAG_MAILBOX_FILES) != 0) {
+ ret = mailbox_list_delete_mailbox_file(_list, name, path);
+ } else {
+ ret = mailbox_list_delete_mailbox_nonrecursive(_list, name,
+ path, TRUE);
+ }
+
+ if ((ret == 0 || (_list->props & MAILBOX_LIST_PROP_AUTOCREATE_DIRS) != 0) &&
+ (_list->flags & MAILBOX_LIST_FLAG_NO_DELETES) == 0)
+ index_list_delete_finish(_list, name);
+ if (ret == 0) {
+ if (index_list_delete_entry(list, name, TRUE) < 0)
+ return -1;
+ }
+ if (_list->set.no_noselect && ret == 0)
+ (void)index_list_try_delete_nonexistent_parent(_list, name);
+
+ return ret;
+}
+
+static int
+index_list_delete_dir(struct mailbox_list *_list, const char *name)
+{
+ struct index_mailbox_list *list = (struct index_mailbox_list *)_list;
+ int ret;
+
+ if ((ret = index_list_delete_entry(list, name, FALSE)) < 0)
+ return -1;
+ if (ret == 0) {
+ mailbox_list_set_error(_list, MAIL_ERROR_EXISTS,
+ "Mailbox has children, delete them first");
+ return -1;
+ }
+ return 0;
+}
+
+static int
+index_list_delete_symlink(struct mailbox_list *_list,
+ const char *name ATTR_UNUSED)
+{
+ mailbox_list_set_error(_list, MAIL_ERROR_NOTPOSSIBLE,
+ "Symlinks not supported");
+ return -1;
+}
+
+static int
+index_list_rename_mailbox(struct mailbox_list *_oldlist, const char *oldname,
+ struct mailbox_list *_newlist, const char *newname)
+{
+ struct index_mailbox_list *list = (struct index_mailbox_list *)_oldlist;
+ const size_t oldname_len = strlen(oldname);
+ struct mailbox_list_index_sync_context *sync_ctx;
+ struct mailbox_list_index_record oldrec, newrec;
+ struct mailbox_list_index_node *oldnode, *newnode, *child;
+ const void *data;
+ bool created, expunged;
+ uint32_t oldseq, newseq;
+ int ret;
+
+ if (_oldlist != _newlist) {
+ mailbox_list_set_error(_oldlist, MAIL_ERROR_NOTPOSSIBLE,
+ "Renaming not supported across namespaces.");
+ return -1;
+ }
+
+ if (str_begins(newname, oldname) &&
+ newname[oldname_len] == mailbox_list_get_hierarchy_sep(_newlist)) {
+ mailbox_list_set_error(_oldlist, MAIL_ERROR_NOTPOSSIBLE,
+ "Can't rename mailbox under itself.");
+ return -1;
+ }
+
+ if (mailbox_list_index_sync_begin(&list->list, &sync_ctx) < 0)
+ return -1;
+
+ oldnode = mailbox_list_index_lookup(&list->list, oldname);
+ if (oldnode == NULL) {
+ (void)mailbox_list_index_sync_end(&sync_ctx, FALSE);
+ mailbox_list_set_error(&list->list, MAIL_ERROR_NOTFOUND,
+ T_MAIL_ERR_MAILBOX_NOT_FOUND(oldname));
+ return -1;
+ }
+ if (!mail_index_lookup_seq(sync_ctx->view, oldnode->uid, &oldseq))
+ i_panic("mailbox list index: lost uid=%u", oldnode->uid);
+
+ newseq = mailbox_list_index_sync_name(sync_ctx, newname,
+ &newnode, &created);
+ if (!created) {
+ (void)mailbox_list_index_sync_end(&sync_ctx, FALSE);
+ mailbox_list_set_error(&list->list, MAIL_ERROR_EXISTS,
+ "Target mailbox already exists");
+ return -1;
+ }
+ i_assert(oldnode != newnode);
+
+ /* copy all the data from old node to new node */
+ newnode->uid = oldnode->uid;
+ newnode->flags = oldnode->flags;
+ newnode->children = oldnode->children; oldnode->children = NULL;
+ for (child = newnode->children; child != NULL; child = child->next)
+ child->parent = newnode;
+
+ /* remove the old node from existence */
+ mailbox_list_index_node_unlink(sync_ctx->ilist, oldnode);
+
+ /* update the old index record to contain the new name_id/parent_uid,
+ then expunge the added index record */
+ mail_index_lookup_ext(sync_ctx->view, oldseq, sync_ctx->ilist->ext_id,
+ &data, &expunged);
+ i_assert(data != NULL && !expunged);
+ memcpy(&oldrec, data, sizeof(oldrec));
+
+ mail_index_lookup_ext(sync_ctx->view, newseq, sync_ctx->ilist->ext_id,
+ &data, &expunged);
+ i_assert(data != NULL && !expunged);
+ memcpy(&newrec, data, sizeof(newrec));
+
+ oldrec.name_id = newrec.name_id;
+ oldrec.parent_uid = newrec.parent_uid;
+
+ if ((newnode->flags & MAILBOX_LIST_INDEX_FLAG_CORRUPTED_NAME) != 0) {
+ /* mailbox is renamed - clear away the corruption flag so the
+ new name will be written to the mailbox index header. */
+ newnode->flags &= ENUM_NEGATE(MAILBOX_LIST_INDEX_FLAG_CORRUPTED_NAME);
+ mail_index_update_flags(sync_ctx->trans, oldseq, MODIFY_REMOVE,
+ (enum mail_flags)MAILBOX_LIST_INDEX_FLAG_CORRUPTED_NAME);
+ }
+ mail_index_update_ext(sync_ctx->trans, oldseq,
+ sync_ctx->ilist->ext_id, &oldrec, NULL);
+ mail_index_expunge(sync_ctx->trans, newseq);
+
+ ret = mailbox_list_index_sync_end(&sync_ctx, TRUE);
+
+ if (_oldlist->set.no_noselect && ret == 0)
+ (void)index_list_try_delete_nonexistent_parent(_oldlist, oldname);
+
+ return ret;
+}
+
+static struct mailbox_list_iterate_context *
+index_list_iter_init(struct mailbox_list *list,
+ const char *const *patterns,
+ enum mailbox_list_iter_flags flags)
+{
+ struct mailbox_list_iterate_context *ctx;
+ pool_t pool;
+
+ if ((flags & MAILBOX_LIST_ITER_SELECT_SUBSCRIBED) != 0) {
+ return mailbox_list_subscriptions_iter_init(list, patterns,
+ flags);
+ }
+
+ pool = pool_alloconly_create("mailbox list index backend iter", 1024);
+ ctx = p_new(pool, struct mailbox_list_iterate_context, 1);
+ ctx->pool = pool;
+ ctx->list = list;
+ ctx->flags = flags;
+ array_create(&ctx->module_contexts, pool, sizeof(void *), 5);
+ return ctx;
+}
+
+static const struct mailbox_info *
+index_list_iter_next(struct mailbox_list_iterate_context *ctx)
+{
+ if ((ctx->flags & MAILBOX_LIST_ITER_SELECT_SUBSCRIBED) != 0)
+ return mailbox_list_subscriptions_iter_next(ctx);
+ return NULL;
+}
+
+static int index_list_iter_deinit(struct mailbox_list_iterate_context *ctx)
+{
+ if ((ctx->flags & MAILBOX_LIST_ITER_SELECT_SUBSCRIBED) != 0)
+ return mailbox_list_subscriptions_iter_deinit(ctx);
+ pool_unref(&ctx->pool);
+ return 0;
+}
+
+struct mailbox_list index_mailbox_list = {
+ .name = MAILBOX_LIST_NAME_INDEX,
+ .props = MAILBOX_LIST_PROP_NO_ROOT | MAILBOX_LIST_PROP_NO_INTERNAL_NAMES,
+ .mailbox_name_max_length = MAILBOX_LIST_NAME_MAX_LENGTH,
+
+ .v = {
+ .alloc = index_list_alloc,
+ .init = index_list_init,
+ .deinit = index_list_deinit,
+ .get_hierarchy_sep = index_list_get_hierarchy_sep,
+ .get_vname = mailbox_list_default_get_vname,
+ .get_storage_name = mailbox_list_default_get_storage_name,
+ .get_path = index_list_get_path,
+ .get_temp_prefix = index_list_get_temp_prefix,
+ .iter_init = index_list_iter_init,
+ .iter_next = index_list_iter_next,
+ .iter_deinit = index_list_iter_deinit,
+ .subscriptions_refresh = mailbox_list_subscriptions_refresh,
+ .set_subscribed = index_list_set_subscribed,
+ .delete_mailbox = index_list_delete_mailbox,
+ .delete_dir = index_list_delete_dir,
+ .delete_symlink = index_list_delete_symlink,
+ .rename_mailbox = index_list_rename_mailbox,
+ }
+};
+
+bool mailbox_list_index_backend_init_mailbox(struct mailbox *box,
+ struct mailbox_vfuncs *v)
+{
+ if (strcmp(box->list->name, MAILBOX_LIST_NAME_INDEX) != 0)
+ return TRUE;
+
+ /* NOTE: this is using the same v as
+ mailbox_list_index_status_init_mailbox(), so don't have them
+ accidentally override each others. */
+ v->create_box = index_list_mailbox_create;
+ v->update_box = index_list_mailbox_update;
+ v->exists = index_list_mailbox_exists;
+ v->open = index_list_mailbox_open;
+ return FALSE;
+}
diff --git a/src/lib-storage/list/mailbox-list-index-iter.c b/src/lib-storage/list/mailbox-list-index-iter.c
new file mode 100644
index 0000000..be8b265
--- /dev/null
+++ b/src/lib-storage/list/mailbox-list-index-iter.c
@@ -0,0 +1,266 @@
+/* Copyright (c) 2006-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "str.h"
+#include "imap-match.h"
+#include "mail-storage.h"
+#include "mailbox-list-subscriptions.h"
+#include "mailbox-list-iter-private.h"
+#include "mailbox-list-index.h"
+
+static bool iter_use_index(struct mailbox_list *list,
+ enum mailbox_list_iter_flags flags)
+{
+ struct mailbox_list_index *ilist = INDEX_LIST_CONTEXT_REQUIRE(list);
+
+ if ((flags & MAILBOX_LIST_ITER_SELECT_SUBSCRIBED) != 0) {
+ /* for now we don't use indexes when listing subscriptions,
+ because it needs to list also the nonexistent subscribed
+ mailboxes, which don't exist in the index. */
+ return FALSE;
+ }
+ if ((flags & MAILBOX_LIST_ITER_RAW_LIST) != 0 &&
+ ilist->has_backing_store) {
+ /* no indexing wanted with raw lists */
+ return FALSE;
+ }
+ if (mailbox_list_index_refresh(list) < 0 &&
+ ilist->has_backing_store) {
+ /* refresh failed */
+ return FALSE;
+ }
+ return TRUE;
+}
+
+struct mailbox_list_iterate_context *
+mailbox_list_index_iter_init(struct mailbox_list *list,
+ const char *const *patterns,
+ enum mailbox_list_iter_flags flags)
+{
+ struct mailbox_list_index *ilist = INDEX_LIST_CONTEXT_REQUIRE(list);
+ struct mailbox_list_index_iterate_context *ctx;
+ pool_t pool;
+ char ns_sep = mail_namespace_get_sep(list->ns);
+
+ if (!iter_use_index(list, flags)) {
+ /* no indexing */
+ return ilist->module_ctx.super.iter_init(list, patterns, flags);
+ }
+
+ pool = pool_alloconly_create("mailbox list index iter", 2048);
+ ctx = p_new(pool, struct mailbox_list_index_iterate_context, 1);
+ ctx->ctx.pool = pool;
+ ctx->ctx.list = list;
+ ctx->ctx.flags = flags;
+ ctx->ctx.glob = imap_match_init_multiple(pool, patterns, TRUE, ns_sep);
+ array_create(&ctx->ctx.module_contexts, pool, sizeof(void *), 5);
+ ctx->info_pool = pool_alloconly_create("mailbox list index iter info", 128);
+ ctx->ctx.index_iteration = TRUE;
+
+ /* listing mailboxes from index */
+ ctx->info.ns = list->ns;
+ ctx->path = str_new(pool, 128);
+ ctx->next_node = ilist->mailbox_tree;
+ ctx->mailbox_pool = ilist->mailbox_pool;
+ pool_ref(ctx->mailbox_pool);
+ return &ctx->ctx;
+}
+
+static void
+mailbox_list_get_escaped_mailbox_name(struct mailbox_list *list,
+ const char *raw_name,
+ string_t *escaped_name)
+{
+ const char escape_chars[] = {
+ list->set.storage_name_escape_char,
+ mailbox_list_get_hierarchy_sep(list),
+ '\0'
+ };
+ mailbox_list_name_escape(raw_name, escape_chars, escaped_name);
+}
+
+static void
+mailbox_list_index_update_info(struct mailbox_list_index_iterate_context *ctx)
+{
+ struct mailbox_list_index_node *node = ctx->next_node;
+ struct mailbox *box;
+
+ p_clear(ctx->info_pool);
+
+ str_truncate(ctx->path, ctx->parent_len);
+ /* the root directory may have an empty name. in that case we'll still
+ want to insert the separator, so check for non-NULL parent rather
+ than non-empty path. */
+ if (node->parent != NULL) {
+ str_append_c(ctx->path,
+ mailbox_list_get_hierarchy_sep(ctx->ctx.list));
+ }
+ mailbox_list_get_escaped_mailbox_name(ctx->ctx.list, node->raw_name,
+ ctx->path);
+
+ ctx->info.vname = mailbox_list_get_vname(ctx->ctx.list, str_c(ctx->path));
+ ctx->info.flags = node->children != NULL ?
+ MAILBOX_CHILDREN : MAILBOX_NOCHILDREN;
+ if (strcmp(ctx->info.vname, "INBOX") != 0) {
+ /* non-INBOX */
+ ctx->info.vname = p_strdup(ctx->info_pool, ctx->info.vname);
+ } else if (!ctx->prefix_inbox_list) {
+ /* listing INBOX itself */
+ ctx->info.vname = "INBOX";
+ if (mail_namespace_is_inbox_noinferiors(ctx->info.ns)) {
+ ctx->info.flags &= ENUM_NEGATE(MAILBOX_CHILDREN |
+ MAILBOX_NOCHILDREN);
+ ctx->info.flags |= MAILBOX_NOINFERIORS;
+ }
+ } else {
+ /* listing INBOX/INBOX */
+ ctx->info.vname = p_strconcat(ctx->info_pool,
+ ctx->ctx.list->ns->prefix, "INBOX", NULL);
+ ctx->info.flags |= MAILBOX_NONEXISTENT;
+ }
+ if ((node->flags & MAILBOX_LIST_INDEX_FLAG_NONEXISTENT) != 0)
+ ctx->info.flags |= MAILBOX_NONEXISTENT;
+ else if ((node->flags & MAILBOX_LIST_INDEX_FLAG_NOSELECT) != 0)
+ ctx->info.flags |= MAILBOX_NOSELECT;
+ if ((node->flags & MAILBOX_LIST_INDEX_FLAG_NOINFERIORS) != 0)
+ ctx->info.flags |= MAILBOX_NOINFERIORS;
+
+ if ((ctx->ctx.flags & (MAILBOX_LIST_ITER_SELECT_SUBSCRIBED |
+ MAILBOX_LIST_ITER_RETURN_SUBSCRIBED)) != 0) {
+ mailbox_list_set_subscription_flags(ctx->ctx.list,
+ ctx->info.vname,
+ &ctx->info.flags);
+ }
+
+ if ((ctx->ctx.flags & MAILBOX_LIST_ITER_RETURN_NO_FLAGS) == 0) {
+ box = mailbox_alloc(ctx->ctx.list, ctx->info.vname, 0);
+ mailbox_list_index_status_set_info_flags(box, node->uid,
+ &ctx->info.flags);
+ mailbox_free(&box);
+ }
+}
+
+static void
+mailbox_list_index_update_next(struct mailbox_list_index_iterate_context *ctx,
+ bool follow_children)
+{
+ struct mailbox_list_index_node *node = ctx->next_node;
+
+ if (!ctx->prefix_inbox_list && ctx->ctx.list->ns->prefix_len > 0 &&
+ strcmp(node->raw_name, "INBOX") == 0 && node->parent == NULL &&
+ node->children != NULL) {
+ /* prefix/INBOX has children */
+ ctx->prefix_inbox_list = TRUE;
+ return;
+ }
+
+ if (node->children != NULL && follow_children) {
+ ctx->parent_len = str_len(ctx->path);
+ ctx->next_node = node->children;
+ } else {
+ while (node->next == NULL) {
+ node = node->parent;
+ if (node != NULL) {
+ /* The storage name kept in the iteration context
+ is escaped. To calculate the right truncation
+ margin, the length of the name must be
+ calculated from the escaped storage name and
+ not from node->raw_name. */
+ string_t *escaped_name = t_str_new(64);
+ mailbox_list_get_escaped_mailbox_name(ctx->ctx.list,
+ node->raw_name,
+ escaped_name);
+ ctx->parent_len -= str_len(escaped_name);
+ if (node->parent != NULL)
+ ctx->parent_len--;
+ }
+ if (node == NULL) {
+ /* last one */
+ ctx->next_node = NULL;
+ return;
+ }
+ }
+ ctx->next_node = node->next;
+ }
+}
+
+static bool
+iter_subscriptions_ok(struct mailbox_list_index_iterate_context *ctx)
+{
+ if ((ctx->ctx.flags & MAILBOX_LIST_ITER_SELECT_SUBSCRIBED) == 0)
+ return TRUE;
+
+ if ((ctx->info.flags & MAILBOX_SUBSCRIBED) != 0)
+ return TRUE;
+
+ if ((ctx->ctx.flags & MAILBOX_LIST_ITER_SELECT_RECURSIVEMATCH) != 0 &&
+ (ctx->info.flags & MAILBOX_CHILD_SUBSCRIBED) != 0)
+ return TRUE;
+ return FALSE;
+}
+
+const struct mailbox_info *
+mailbox_list_index_iter_next(struct mailbox_list_iterate_context *_ctx)
+{
+ struct mailbox_list_index *ilist = INDEX_LIST_CONTEXT_REQUIRE(_ctx->list);
+ if (!_ctx->index_iteration) {
+ /* index isn't being used */
+ return ilist->module_ctx.super.iter_next(_ctx);
+ }
+
+ struct mailbox_list_index_iterate_context *ctx =
+ (struct mailbox_list_index_iterate_context *)_ctx;
+ bool follow_children;
+ enum imap_match_result match;
+
+ /* listing mailboxes from index */
+ while (ctx->next_node != NULL) {
+ T_BEGIN {
+ mailbox_list_index_update_info(ctx);
+ } T_END;
+ match = imap_match(_ctx->glob, ctx->info.vname);
+
+ follow_children = (match & (IMAP_MATCH_YES |
+ IMAP_MATCH_CHILDREN)) != 0;
+ if (match == IMAP_MATCH_YES && iter_subscriptions_ok(ctx)) {
+ /* If this is a) \NoSelect leaf, b) not LAYOUT=index
+ and c) NO-NOSELECT is set, try to rmdir the leaf
+ directores from filesystem. (With LAYOUT=index the
+ \NoSelect mailboxes aren't on the filesystem.) */
+ if (ilist->has_backing_store &&
+ mailbox_list_iter_try_delete_noselect(_ctx, &ctx->info,
+ str_c(ctx->path))) {
+ /* Deleted \NoSelect leaf. Refresh the index
+ later on so it gets removed from the index
+ as well. */
+ mailbox_list_index_refresh_later(_ctx->list);
+ } else {
+ mailbox_list_index_update_next(ctx, TRUE);
+ return &ctx->info;
+ }
+ } else if ((_ctx->flags & MAILBOX_LIST_ITER_SELECT_SUBSCRIBED) != 0 &&
+ (ctx->info.flags & MAILBOX_CHILD_SUBSCRIBED) == 0) {
+ /* listing only subscriptions, but there are no
+ subscribed children. */
+ follow_children = FALSE;
+ }
+ mailbox_list_index_update_next(ctx, follow_children);
+ }
+ return mailbox_list_iter_default_next(_ctx);
+}
+
+int mailbox_list_index_iter_deinit(struct mailbox_list_iterate_context *_ctx)
+{
+ struct mailbox_list_index *ilist = INDEX_LIST_CONTEXT_REQUIRE(_ctx->list);
+ if (!_ctx->index_iteration)
+ return ilist->module_ctx.super.iter_deinit(_ctx);
+
+ struct mailbox_list_index_iterate_context *ctx =
+ (struct mailbox_list_index_iterate_context *)_ctx;
+ int ret = ctx->failed ? -1 : 0;
+
+ pool_unref(&ctx->mailbox_pool);
+ pool_unref(&ctx->info_pool);
+ pool_unref(&_ctx->pool);
+ return ret;
+}
diff --git a/src/lib-storage/list/mailbox-list-index-notify.c b/src/lib-storage/list/mailbox-list-index-notify.c
new file mode 100644
index 0000000..c07e95b
--- /dev/null
+++ b/src/lib-storage/list/mailbox-list-index-notify.c
@@ -0,0 +1,967 @@
+/* Copyright (c) 2013-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "ioloop.h"
+#include "str.h"
+#include "mail-index-private.h"
+#include "mail-transaction-log-private.h"
+#include "mail-storage-private.h"
+#include "mailbox-list-notify.h"
+#include "mailbox-list-notify-tree.h"
+#include "mailbox-list-index.h"
+
+#include <sys/stat.h>
+
+#define NOTIFY_DELAY_MSECS 500
+
+enum ilist_ext_type {
+ ILIST_EXT_NONE,
+ ILIST_EXT_BASE,
+ ILIST_EXT_MSGS,
+ ILIST_EXT_HIGHESTMODSEQ,
+ ILIST_EXT_UNKNOWN
+};
+
+struct mailbox_list_notify_rename {
+ uint32_t old_uid, new_uid;
+};
+
+struct mailbox_list_inotify_entry {
+ uint32_t uid;
+ guid_128_t guid;
+ bool expunge;
+};
+
+struct mailbox_list_notify_index {
+ struct mailbox_list_notify notify;
+
+ struct mailbox_tree_context *subscriptions;
+ struct mailbox_list_notify_tree *tree;
+ struct mail_index_view *view, *old_view;
+ struct mail_index_view_sync_ctx *sync_ctx;
+ enum ilist_ext_type cur_ext;
+ uint32_t cur_ext_id;
+
+ void (*wait_callback)(void *context);
+ void *wait_context;
+ struct io *io_wait, *io_wait_inbox;
+ struct timeout *to_wait, *to_notify;
+
+ ARRAY_TYPE(seq_range) new_uids, expunged_uids, changed_uids;
+ ARRAY_TYPE(const_string) new_subscriptions, new_unsubscriptions;
+ ARRAY(struct mailbox_list_notify_rename) renames;
+ struct seq_range_iter new_uids_iter, expunged_uids_iter;
+ struct seq_range_iter changed_uids_iter;
+ unsigned int new_uids_n, expunged_uids_n, changed_uids_n;
+ unsigned int rename_idx, subscription_idx, unsubscription_idx;
+
+ struct mailbox_list_notify_rec notify_rec;
+ string_t *rec_name;
+
+ char *list_log_path, *inbox_log_path;
+ struct stat list_last_st, inbox_last_st;
+ struct mailbox *inbox;
+
+ bool initialized:1;
+ bool read_failed:1;
+ bool inbox_event_pending:1;
+};
+
+static const enum mailbox_status_items notify_status_items =
+ STATUS_UIDVALIDITY | STATUS_UIDNEXT | STATUS_MESSAGES |
+ STATUS_UNSEEN | STATUS_HIGHESTMODSEQ;
+
+static enum mailbox_list_notify_event
+mailbox_list_index_get_changed_events(const struct mailbox_notify_node *nnode,
+ const struct mailbox_status *status)
+{
+ enum mailbox_list_notify_event events = 0;
+
+ if (nnode->uidvalidity != status->uidvalidity)
+ events |= MAILBOX_LIST_NOTIFY_UIDVALIDITY;
+ if (nnode->uidnext != status->uidnext)
+ events |= MAILBOX_LIST_NOTIFY_APPENDS;
+ if (nnode->messages > status->messages) {
+ /* NOTE: not entirely reliable, since there could be both
+ expunges and appends.. but it shouldn't make any difference
+ in practise, since anybody interested in expunges is most
+ likely also interested in appends. */
+ events |= MAILBOX_LIST_NOTIFY_EXPUNGES;
+ }
+ if (nnode->unseen != status->unseen)
+ events |= MAILBOX_LIST_NOTIFY_SEEN_CHANGES;
+ if (nnode->highest_modseq < status->highest_modseq)
+ events |= MAILBOX_LIST_NOTIFY_MODSEQ_CHANGES;
+ return events;
+}
+
+static void
+mailbox_notify_node_update_status(struct mailbox_notify_node *nnode,
+ struct mailbox_status *status)
+{
+ nnode->uidvalidity = status->uidvalidity;
+ nnode->uidnext = status->uidnext;
+ nnode->messages = status->messages;
+ nnode->unseen = status->unseen;
+ nnode->highest_modseq = status->highest_modseq;
+}
+
+static void
+mailbox_list_index_notify_init_inbox(struct mailbox_list_notify_index *inotify)
+{
+ inotify->inbox = mailbox_alloc(inotify->notify.list, "INBOX",
+ MAILBOX_FLAG_READONLY);
+ if (mailbox_open(inotify->inbox) < 0)
+ mailbox_free(&inotify->inbox);
+ else
+ inotify->inbox_log_path =
+ i_strconcat(inotify->inbox->index->filepath,
+ ".log", NULL);
+}
+
+int mailbox_list_index_notify_init(struct mailbox_list *list,
+ enum mailbox_list_notify_event mask,
+ struct mailbox_list_notify **notify_r)
+{
+ struct mailbox_list_index *ilist = INDEX_LIST_CONTEXT(list);
+ struct mailbox_list_notify_index *inotify;
+ const char *index_dir;
+
+ if (ilist == NULL) {
+ /* can't do this without mailbox list indexes */
+ return -1;
+ }
+
+ (void)mailbox_list_index_refresh(list);
+
+ inotify = i_new(struct mailbox_list_notify_index, 1);
+ inotify->notify.list = list;
+ inotify->notify.mask = mask;
+ inotify->view = mail_index_view_open(ilist->index);
+ inotify->old_view = mail_index_view_dup_private(inotify->view);
+ inotify->tree = mailbox_list_notify_tree_init(list);
+ i_array_init(&inotify->new_uids, 8);
+ i_array_init(&inotify->expunged_uids, 8);
+ i_array_init(&inotify->changed_uids, 16);
+ i_array_init(&inotify->renames, 16);
+ i_array_init(&inotify->new_subscriptions, 16);
+ i_array_init(&inotify->new_unsubscriptions, 16);
+ inotify->rec_name = str_new(default_pool, 64);
+ if ((mask & (MAILBOX_LIST_NOTIFY_SUBSCRIBE |
+ MAILBOX_LIST_NOTIFY_UNSUBSCRIBE)) != 0) {
+ (void)mailbox_list_iter_subscriptions_refresh(list);
+ mailbox_tree_sort(list->subscriptions);
+ inotify->subscriptions = mailbox_tree_dup(list->subscriptions);
+ }
+ inotify->list_log_path = i_strdup(ilist->index->log->filepath);
+ if (list->mail_set->mailbox_list_index_include_inbox) {
+ /* INBOX can be handled also using mailbox list index */
+ } else if ((list->ns->flags & NAMESPACE_FLAG_INBOX_ANY) == 0) {
+ /* no INBOX in this namespace */
+ } else if ((mask & MAILBOX_LIST_NOTIFY_STATUS) == 0) {
+ /* not interested in mailbox changes */
+ } else if (mailbox_list_get_path(list, "INBOX", MAILBOX_LIST_PATH_TYPE_INDEX,
+ &index_dir) <= 0) {
+ /* no indexes for INBOX? can't handle it */
+ } else {
+ mailbox_list_index_notify_init_inbox(inotify);
+ }
+
+ *notify_r = &inotify->notify;
+ return 1;
+}
+
+void mailbox_list_index_notify_deinit(struct mailbox_list_notify *notify)
+{
+ struct mailbox_list_notify_index *inotify =
+ (struct mailbox_list_notify_index *)notify;
+ bool b;
+
+ if (inotify->inbox != NULL)
+ mailbox_free(&inotify->inbox);
+ if (inotify->subscriptions != NULL)
+ mailbox_tree_deinit(&inotify->subscriptions);
+ io_remove(&inotify->io_wait);
+ io_remove(&inotify->io_wait_inbox);
+ timeout_remove(&inotify->to_wait);
+ timeout_remove(&inotify->to_notify);
+ if (inotify->sync_ctx != NULL)
+ (void)mail_index_view_sync_commit(&inotify->sync_ctx, &b);
+ mail_index_view_close(&inotify->view);
+ mail_index_view_close(&inotify->old_view);
+ mailbox_list_notify_tree_deinit(&inotify->tree);
+ array_free(&inotify->new_subscriptions);
+ array_free(&inotify->new_unsubscriptions);
+ array_free(&inotify->new_uids);
+ array_free(&inotify->expunged_uids);
+ array_free(&inotify->changed_uids);
+ array_free(&inotify->renames);
+ str_free(&inotify->rec_name);
+ i_free(inotify->list_log_path);
+ i_free(inotify->inbox_log_path);
+ i_free(inotify);
+}
+
+static struct mailbox_list_index_node *
+notify_lookup_guid(struct mailbox_list_notify_index *inotify,
+ struct mail_index_view *view,
+ uint32_t uid, enum mailbox_status_items items,
+ struct mailbox_status *status_r, guid_128_t guid_r)
+{
+ struct mailbox_list_index *ilist =
+ INDEX_LIST_CONTEXT_REQUIRE(inotify->notify.list);
+ struct mailbox_list_index_node *index_node;
+ const char *reason;
+ uint32_t seq;
+
+ if (!mail_index_lookup_seq(view, uid, &seq))
+ return NULL;
+
+ index_node = mailbox_list_index_lookup_uid(ilist, uid);
+ if (index_node == NULL) {
+ /* re-parse the index list using the given view. we could be
+ jumping here between old and new view. */
+ (void)mailbox_list_index_parse(inotify->notify.list,
+ view, FALSE);
+ index_node = mailbox_list_index_lookup_uid(ilist, uid);
+ if (index_node == NULL)
+ return NULL;
+ }
+
+ /* get GUID */
+ i_zero(status_r);
+ memset(guid_r, 0, GUID_128_SIZE);
+ (void)mailbox_list_index_status(inotify->notify.list, view, seq,
+ items, status_r, guid_r, NULL, &reason);
+ return index_node;
+}
+
+static void notify_update_stat(struct mailbox_list_notify_index *inotify,
+ bool stat_list, bool stat_inbox)
+{
+ bool call = FALSE;
+
+ if (stat_list &&
+ stat(inotify->list_log_path, &inotify->list_last_st) < 0 &&
+ errno != ENOENT) {
+ e_error(inotify->notify.list->ns->user->event,
+ "stat(%s) failed: %m", inotify->list_log_path);
+ call = TRUE;
+ }
+ if (inotify->inbox_log_path != NULL && stat_inbox) {
+ if (stat(inotify->inbox_log_path, &inotify->inbox_last_st) < 0 &&
+ errno != ENOENT) {
+ e_error(inotify->notify.list->ns->user->event,
+ "stat(%s) failed: %m", inotify->inbox_log_path);
+ call = TRUE;
+ }
+ }
+ if (call)
+ mailbox_list_index_notify_wait(&inotify->notify, NULL, NULL);
+}
+
+static void
+mailbox_list_index_notify_sync_init(struct mailbox_list_notify_index *inotify)
+{
+ struct mail_index_view_sync_rec sync_rec;
+
+ notify_update_stat(inotify, TRUE, TRUE);
+ (void)mail_index_refresh(inotify->view->index);
+
+ /* sync the view so that map extensions gets updated */
+ inotify->sync_ctx = mail_index_view_sync_begin(inotify->view, 0);
+ mail_transaction_log_view_mark(inotify->view->log_view);
+ while (mail_index_view_sync_next(inotify->sync_ctx, &sync_rec)) ;
+ mail_transaction_log_view_rewind(inotify->view->log_view);
+
+ inotify->cur_ext = ILIST_EXT_NONE;
+ inotify->cur_ext_id = (uint32_t)-1;
+}
+
+static bool notify_ext_rec(struct mailbox_list_notify_index *inotify,
+ uint32_t uid)
+{
+ struct mailbox_list_notify *notify = &inotify->notify;
+
+ switch (inotify->cur_ext) {
+ case ILIST_EXT_NONE:
+ i_unreached();
+ case ILIST_EXT_BASE:
+ /* UIDVALIDITY changed */
+ if ((notify->mask & MAILBOX_LIST_NOTIFY_UIDVALIDITY) == 0)
+ return FALSE;
+ break;
+ case ILIST_EXT_MSGS:
+ /* APPEND, EXPUNGE, \Seen or \Recent flag change */
+ if ((notify->mask & MAILBOX_LIST_NOTIFY_STATUS) == 0)
+ return FALSE;
+ break;
+ case ILIST_EXT_HIGHESTMODSEQ:
+ /* when this doesn't come with EXT_MSGS update,
+ it can only be a flag change or an explicit
+ modseq change */
+ if ((notify->mask & MAILBOX_LIST_NOTIFY_MODSEQ_CHANGES) == 0)
+ return FALSE;
+ break;
+ case ILIST_EXT_UNKNOWN:
+ return FALSE;
+ }
+ seq_range_array_add(&inotify->changed_uids, uid);
+ return TRUE;
+}
+
+static int
+mailbox_list_index_notify_read_next(struct mailbox_list_notify_index *inotify)
+{
+ struct mailbox_list_notify *notify = &inotify->notify;
+ struct mailbox_list_index *ilist = INDEX_LIST_CONTEXT_REQUIRE(notify->list);
+ const struct mail_transaction_header *hdr;
+ const void *data;
+ int ret;
+
+ ret = mail_transaction_log_view_next(inotify->view->log_view,
+ &hdr, &data);
+ if (ret <= 0)
+ return ret;
+
+ if ((hdr->type & MAIL_TRANSACTION_EXTERNAL) == 0) {
+ /* all mailbox index updates are external */
+ return 1;
+ }
+ switch (hdr->type & MAIL_TRANSACTION_TYPE_MASK) {
+ case MAIL_TRANSACTION_APPEND: {
+ /* mailbox added or renamed */
+ const struct mail_index_record *rec, *end;
+
+ if ((notify->mask & (MAILBOX_LIST_NOTIFY_CREATE |
+ MAILBOX_LIST_NOTIFY_RENAME)) == 0)
+ break;
+
+ end = CONST_PTR_OFFSET(data, hdr->size);
+ for (rec = data; rec != end; rec++)
+ seq_range_array_add(&inotify->new_uids, rec->uid);
+ break;
+ }
+ case MAIL_TRANSACTION_EXPUNGE_GUID: {
+ /* mailbox deleted or renamed */
+ const struct mail_transaction_expunge_guid *rec, *end;
+
+ if ((notify->mask & (MAILBOX_LIST_NOTIFY_DELETE |
+ MAILBOX_LIST_NOTIFY_RENAME)) == 0)
+ break;
+
+ end = CONST_PTR_OFFSET(data, hdr->size);
+ for (rec = data; rec != end; rec++)
+ seq_range_array_add(&inotify->expunged_uids, rec->uid);
+ break;
+ }
+ case MAIL_TRANSACTION_EXT_INTRO: {
+ struct mail_index_map *map = inotify->view->map;
+ const struct mail_transaction_ext_intro *rec = data;
+ const struct mail_index_ext *ext = NULL;
+ const char *name;
+ uint32_t ext_map_idx;
+
+ if (!array_is_created(&map->extensions))
+ break;
+ /* we want to know what extension the future
+ ext-rec-updates are changing. we're assuming here that
+ there is only one ext-intro record before those,
+ which is true at least for now. */
+ if (rec->ext_id != (uint32_t)-1 &&
+ rec->ext_id < array_count(&map->extensions)) {
+ /* get extension by id */
+ ext = array_idx(&map->extensions, rec->ext_id);
+ } else if (rec->name_size > 0) {
+ /* by name */
+ name = t_strndup(rec+1, rec->name_size);
+ if (mail_index_map_lookup_ext(map, name, &ext_map_idx))
+ ext = array_idx(&map->extensions, ext_map_idx);
+ }
+ if (ext != NULL) {
+ if (ext->index_idx == ilist->ext_id)
+ inotify->cur_ext = ILIST_EXT_BASE;
+ else if (ext->index_idx == ilist->msgs_ext_id)
+ inotify->cur_ext = ILIST_EXT_MSGS;
+ else if (ext->index_idx == ilist->hmodseq_ext_id)
+ inotify->cur_ext = ILIST_EXT_HIGHESTMODSEQ;
+ else
+ inotify->cur_ext = ILIST_EXT_UNKNOWN;
+ inotify->cur_ext_id = ext->index_idx;
+ }
+ break;
+ }
+ case MAIL_TRANSACTION_EXT_REC_UPDATE: {
+ const struct mail_index_registered_ext *ext;
+ const struct mail_transaction_ext_rec_update *rec;
+ unsigned int i, record_size;
+
+ if (inotify->cur_ext == ILIST_EXT_NONE) {
+ e_error(ilist->index->event,
+ "%s: Missing ext-intro for ext-rec-update",
+ ilist->index->filepath);
+ break;
+ }
+
+ /* the record is padded to 32bits in the transaction log */
+ ext = array_idx(&inotify->view->index->extensions,
+ inotify->cur_ext_id);
+ record_size = (sizeof(*rec) + ext->record_size + 3) & ~3U;
+ for (i = 0; i < hdr->size; i += record_size) {
+ rec = CONST_PTR_OFFSET(data, i);
+
+ if (i + record_size > hdr->size)
+ break;
+ if (!notify_ext_rec(inotify, rec->uid))
+ break;
+ }
+ break;
+ }
+ }
+ return 1;
+}
+
+static int
+mailbox_list_inotify_entry_guid_cmp(const struct mailbox_list_inotify_entry *r1,
+ const struct mailbox_list_inotify_entry *r2)
+{
+ int ret;
+
+ ret = memcmp(r1->guid, r2->guid, sizeof(r1->guid));
+ if (ret != 0)
+ return ret;
+
+ if (r1->expunge == r2->expunge) {
+ /* this really shouldn't happen */
+ return 0;
+ }
+ return r1->expunge ? -1 : 1;
+}
+
+static void
+mailbox_list_index_notify_find_renames(struct mailbox_list_notify_index *inotify)
+{
+ ARRAY(struct mailbox_list_inotify_entry) entries;
+ struct mailbox_status status;
+ struct mailbox_list_notify_rename *rename;
+ struct mailbox_list_inotify_entry *entry;
+ const struct mailbox_list_inotify_entry *e;
+ unsigned int i, count;
+ guid_128_t guid;
+ uint32_t uid;
+
+ /* first get all of the added and expunged GUIDs */
+ t_array_init(&entries, array_count(&inotify->new_uids) +
+ array_count(&inotify->expunged_uids));
+ while (seq_range_array_iter_nth(&inotify->expunged_uids_iter,
+ inotify->expunged_uids_n++, &uid)) {
+ if (notify_lookup_guid(inotify, inotify->old_view, uid,
+ 0, &status, guid) != NULL &&
+ !guid_128_is_empty(guid)) {
+ entry = array_append_space(&entries);
+ entry->uid = uid;
+ entry->expunge = TRUE;
+ memcpy(entry->guid, guid, sizeof(entry->guid));
+ }
+ }
+
+ (void)mailbox_list_index_parse(inotify->notify.list,
+ inotify->view, TRUE);
+ while (seq_range_array_iter_nth(&inotify->new_uids_iter,
+ inotify->new_uids_n++, &uid)) {
+ if (notify_lookup_guid(inotify, inotify->view, uid,
+ 0, &status, guid) != NULL &&
+ !guid_128_is_empty(guid)) {
+ entry = array_append_space(&entries);
+ entry->uid = uid;
+ memcpy(entry->guid, guid, sizeof(entry->guid));
+ }
+ }
+
+ /* now sort the entries by GUID and find those that have been both
+ added and expunged */
+ array_sort(&entries, mailbox_list_inotify_entry_guid_cmp);
+
+ e = array_get(&entries, &count);
+ for (i = 1; i < count; i++) {
+ if (e[i-1].expunge && !e[i].expunge &&
+ memcmp(e[i-1].guid, e[i].guid, sizeof(e[i].guid)) == 0) {
+ rename = array_append_space(&inotify->renames);
+ rename->old_uid = e[i-1].uid;
+ rename->new_uid = e[i].uid;
+
+ seq_range_array_remove(&inotify->expunged_uids,
+ rename->old_uid);
+ seq_range_array_remove(&inotify->new_uids,
+ rename->new_uid);
+ }
+ }
+}
+
+static void
+mailbox_list_index_notify_find_subscribes(struct mailbox_list_notify_index *inotify)
+{
+ struct mailbox_tree_iterate_context *old_iter, *new_iter;
+ struct mailbox_tree_context *old_tree, *new_tree;
+ const char *old_path = NULL, *new_path = NULL;
+ pool_t pool;
+ int ret;
+
+ if (mailbox_list_iter_subscriptions_refresh(inotify->notify.list) < 0)
+ return;
+ mailbox_tree_sort(inotify->notify.list->subscriptions);
+
+ old_tree = inotify->subscriptions;
+ new_tree = mailbox_tree_dup(inotify->notify.list->subscriptions);
+
+ old_iter = mailbox_tree_iterate_init(old_tree, NULL, MAILBOX_SUBSCRIBED);
+ new_iter = mailbox_tree_iterate_init(new_tree, NULL, MAILBOX_SUBSCRIBED);
+
+ pool = mailbox_tree_get_pool(new_tree);
+ for (;;) {
+ if (old_path == NULL) {
+ if (mailbox_tree_iterate_next(old_iter, &old_path) == NULL)
+ old_path = NULL;
+ }
+ if (new_path == NULL) {
+ if (mailbox_tree_iterate_next(new_iter, &new_path) == NULL)
+ new_path = NULL;
+ }
+
+ if (old_path == NULL) {
+ if (new_path == NULL)
+ break;
+ ret = 1;
+ } else if (new_path == NULL)
+ ret = -1;
+ else {
+ ret = strcmp(old_path, new_path);
+ }
+
+ if (ret == 0) {
+ old_path = NULL;
+ new_path = NULL;
+ } else if (ret > 0) {
+ new_path = p_strdup(pool, new_path);
+ array_push_back(&inotify->new_subscriptions,
+ &new_path);
+ new_path = NULL;
+ } else {
+ old_path = p_strdup(pool, old_path);
+ array_push_back(&inotify->new_unsubscriptions,
+ &old_path);
+ old_path = NULL;
+ }
+ }
+ mailbox_tree_iterate_deinit(&old_iter);
+ mailbox_tree_iterate_deinit(&new_iter);
+
+ mailbox_tree_deinit(&inotify->subscriptions);
+ inotify->subscriptions = new_tree;
+}
+
+static void
+mailbox_list_index_notify_reset_iters(struct mailbox_list_notify_index *inotify)
+{
+ seq_range_array_iter_init(&inotify->new_uids_iter,
+ &inotify->new_uids);
+ seq_range_array_iter_init(&inotify->expunged_uids_iter,
+ &inotify->expunged_uids);
+ seq_range_array_iter_init(&inotify->changed_uids_iter,
+ &inotify->changed_uids);
+ inotify->changed_uids_n = 0;
+ inotify->new_uids_n = 0;
+ inotify->expunged_uids_n = 0;
+ inotify->rename_idx = 0;
+ inotify->subscription_idx = 0;
+ inotify->unsubscription_idx = 0;
+}
+
+static void
+mailbox_list_index_notify_read_init(struct mailbox_list_notify_index *inotify)
+{
+ bool b;
+ int ret;
+
+ mailbox_list_index_notify_sync_init(inotify);
+
+ /* read all changes from .log file */
+ while ((ret = mailbox_list_index_notify_read_next(inotify)) > 0) ;
+ inotify->read_failed = ret < 0;
+
+ (void)mail_index_view_sync_commit(&inotify->sync_ctx, &b);
+
+ /* remove changes for already deleted mailboxes */
+ seq_range_array_remove_seq_range(&inotify->new_uids,
+ &inotify->expunged_uids);
+ seq_range_array_remove_seq_range(&inotify->changed_uids,
+ &inotify->expunged_uids);
+ mailbox_list_index_notify_reset_iters(inotify);
+ if (array_count(&inotify->new_uids) > 0 &&
+ array_count(&inotify->expunged_uids) > 0) {
+ mailbox_list_index_notify_find_renames(inotify);
+ mailbox_list_index_notify_reset_iters(inotify);
+ }
+ if (inotify->subscriptions != NULL)
+ mailbox_list_index_notify_find_subscribes(inotify);
+
+ inotify->initialized = TRUE;
+}
+
+static void
+mailbox_list_index_notify_read_deinit(struct mailbox_list_notify_index *inotify)
+{
+ /* save the old view so we can look up expunged records */
+ mail_index_view_close(&inotify->old_view);
+ inotify->old_view = mail_index_view_dup_private(inotify->view);
+
+ array_clear(&inotify->new_subscriptions);
+ array_clear(&inotify->new_unsubscriptions);
+ array_clear(&inotify->new_uids);
+ array_clear(&inotify->expunged_uids);
+ array_clear(&inotify->changed_uids);
+ array_clear(&inotify->renames);
+
+ inotify->initialized = FALSE;
+}
+
+static bool
+mailbox_list_index_notify_lookup(struct mailbox_list_notify_index *inotify,
+ struct mail_index_view *view,
+ uint32_t uid, enum mailbox_status_items items,
+ struct mailbox_status *status_r,
+ struct mailbox_list_notify_rec **rec_r)
+{
+ struct mailbox_list_notify_rec *rec = &inotify->notify_rec;
+ struct mailbox_list_index_node *index_node;
+ const char *storage_name;
+ char ns_sep = mailbox_list_get_hierarchy_sep(inotify->notify.list);
+
+ i_zero(rec);
+ index_node = notify_lookup_guid(inotify, view, uid,
+ items, status_r, rec->guid);
+ if (index_node == NULL)
+ return FALSE;
+
+ /* get storage_name */
+ str_truncate(inotify->rec_name, 0);
+ mailbox_list_index_node_get_path(index_node, ns_sep, inotify->rec_name);
+ storage_name = str_c(inotify->rec_name);
+
+ rec->storage_name = storage_name;
+ rec->vname = mailbox_list_get_vname(inotify->notify.list,
+ rec->storage_name);
+ *rec_r = rec;
+ return TRUE;
+}
+
+static bool
+mailbox_list_index_notify_rename(struct mailbox_list_notify_index *inotify,
+ unsigned int idx)
+{
+ const struct mailbox_list_notify_rename *rename;
+ struct mailbox_list_notify_rec *rec;
+ struct mailbox_status status;
+ const char *old_vname;
+
+ rename = array_idx(&inotify->renames, idx);
+
+ /* lookup the old name */
+ if (!mailbox_list_index_notify_lookup(inotify, inotify->old_view,
+ rename->old_uid, 0, &status, &rec))
+ return FALSE;
+ old_vname = t_strdup(rec->vname);
+
+ /* return using the new name */
+ if (!mailbox_list_index_notify_lookup(inotify, inotify->view,
+ rename->new_uid, 0, &status, &rec))
+ return FALSE;
+
+ rec->old_vname = old_vname;
+ rec->events = MAILBOX_LIST_NOTIFY_RENAME;
+ return TRUE;
+}
+
+static bool
+mailbox_list_index_notify_subscribe(struct mailbox_list_notify_index *inotify,
+ unsigned int idx)
+{
+ struct mailbox_list_notify_rec *rec = &inotify->notify_rec;
+
+ i_zero(rec);
+ rec->vname = array_idx_elem(&inotify->new_subscriptions, idx);
+ rec->storage_name = mailbox_list_get_storage_name(inotify->notify.list,
+ rec->vname);
+ rec->events = MAILBOX_LIST_NOTIFY_SUBSCRIBE;
+ return TRUE;
+}
+
+static bool
+mailbox_list_index_notify_unsubscribe(struct mailbox_list_notify_index *inotify,
+ unsigned int idx)
+{
+ struct mailbox_list_notify_rec *rec = &inotify->notify_rec;
+
+ i_zero(rec);
+ rec->vname = array_idx_elem(&inotify->new_unsubscriptions, idx);
+ rec->storage_name = mailbox_list_get_storage_name(inotify->notify.list,
+ rec->vname);
+ rec->events = MAILBOX_LIST_NOTIFY_UNSUBSCRIBE;
+ return TRUE;
+}
+
+static bool
+mailbox_list_index_notify_expunge(struct mailbox_list_notify_index *inotify,
+ uint32_t uid)
+{
+ struct mailbox_list_notify_rec *rec;
+ struct mailbox_status status;
+
+ if (!mailbox_list_index_notify_lookup(inotify, inotify->old_view,
+ uid, 0, &status, &rec))
+ return FALSE;
+ rec->events = MAILBOX_LIST_NOTIFY_DELETE;
+ return TRUE;
+}
+
+static bool
+mailbox_list_index_notify_new(struct mailbox_list_notify_index *inotify,
+ uint32_t uid)
+{
+ struct mailbox_list_notify_rec *rec;
+ struct mailbox_status status;
+
+ if (!mailbox_list_index_notify_lookup(inotify, inotify->view,
+ uid, 0, &status, &rec))
+ i_unreached();
+ rec->events = MAILBOX_LIST_NOTIFY_CREATE;
+ return TRUE;
+}
+
+static bool
+mailbox_list_index_notify_change(struct mailbox_list_notify_index *inotify,
+ uint32_t uid)
+{
+ struct mailbox_list_notify_rec *rec;
+ struct mailbox_notify_node *nnode, empty_node;
+ struct mailbox_status status;
+
+ if (!mailbox_list_index_notify_lookup(inotify, inotify->view,
+ uid, notify_status_items,
+ &status, &rec)) {
+ /* Mailbox is already deleted. We won't get here if we're
+ tracking MAILBOX_LIST_NOTIFY_DELETE or _RENAME
+ (which update expunged_uids). */
+ return FALSE;
+ }
+
+ /* get the old status */
+ nnode = mailbox_list_notify_tree_lookup(inotify->tree,
+ rec->storage_name);
+ if (nnode == NULL) {
+ /* mailbox didn't exist earlier - report all events as new */
+ i_zero(&empty_node);
+ nnode = &empty_node;
+ }
+ rec->events |= mailbox_list_index_get_changed_events(nnode, &status);
+ /* update internal state */
+ mailbox_notify_node_update_status(nnode, &status);
+ return rec->events != 0;
+}
+
+static bool
+mailbox_list_index_notify_try_next(struct mailbox_list_notify_index *inotify)
+{
+ uint32_t uid;
+
+ /* first show mailbox deletes */
+ if (seq_range_array_iter_nth(&inotify->expunged_uids_iter,
+ inotify->expunged_uids_n++, &uid))
+ return mailbox_list_index_notify_expunge(inotify, uid);
+
+ /* mailbox renames */
+ if (inotify->rename_idx < array_count(&inotify->renames)) {
+ return mailbox_list_index_notify_rename(inotify,
+ inotify->rename_idx++);
+ }
+
+ /* next mailbox creates */
+ if (seq_range_array_iter_nth(&inotify->new_uids_iter,
+ inotify->new_uids_n++, &uid))
+ return mailbox_list_index_notify_new(inotify, uid);
+
+ /* subscribes */
+ if (inotify->subscription_idx < array_count(&inotify->new_subscriptions)) {
+ return mailbox_list_index_notify_subscribe(inotify,
+ inotify->subscription_idx++);
+ }
+ if (inotify->unsubscription_idx < array_count(&inotify->new_unsubscriptions)) {
+ return mailbox_list_index_notify_unsubscribe(inotify,
+ inotify->unsubscription_idx++);
+ }
+
+ /* STATUS updates */
+ while (seq_range_array_iter_nth(&inotify->changed_uids_iter,
+ inotify->changed_uids_n++, &uid)) {
+ if (mailbox_list_index_notify_change(inotify, uid))
+ return TRUE;
+ }
+ return FALSE;
+}
+
+static enum mailbox_list_notify_event
+mailbox_list_notify_inbox_get_events(struct mailbox_list_notify_index *inotify)
+{
+ struct mailbox_status old_status, new_status;
+ struct mailbox_notify_node old_nnode;
+
+ mailbox_get_open_status(inotify->inbox, notify_status_items, &old_status);
+ if (mailbox_sync(inotify->inbox, MAILBOX_SYNC_FLAG_FAST) < 0) {
+ e_error(inotify->notify.list->ns->user->event,
+ "Mailbox list index notify: Failed to sync INBOX: %s",
+ mailbox_get_last_internal_error(inotify->inbox, NULL));
+ return 0;
+ }
+ mailbox_get_open_status(inotify->inbox, notify_status_items, &new_status);
+
+ mailbox_notify_node_update_status(&old_nnode, &old_status);
+ return mailbox_list_index_get_changed_events(&old_nnode, &new_status);
+}
+
+int mailbox_list_index_notify_next(struct mailbox_list_notify *notify,
+ const struct mailbox_list_notify_rec **rec_r)
+{
+ struct mailbox_list_notify_index *inotify =
+ (struct mailbox_list_notify_index *)notify;
+
+ if (!inotify->initialized)
+ mailbox_list_index_notify_read_init(inotify);
+ if (mailbox_list_index_handle_corruption(notify->list) < 0)
+ return -1;
+
+ while (mailbox_list_index_notify_try_next(inotify)) {
+ if ((inotify->notify_rec.events & inotify->notify.mask) != 0) {
+ *rec_r = &inotify->notify_rec;
+ return 1;
+ } else {
+ /* caller doesn't care about this change */
+ }
+ }
+ if (inotify->inbox_event_pending) {
+ inotify->inbox_event_pending = FALSE;
+ i_zero(&inotify->notify_rec);
+ inotify->notify_rec.vname = "INBOX";
+ inotify->notify_rec.storage_name = "INBOX";
+ inotify->notify_rec.events =
+ mailbox_list_notify_inbox_get_events(inotify);
+ *rec_r = &inotify->notify_rec;
+ return 1;
+ }
+
+ mailbox_list_index_notify_read_deinit(inotify);
+ return inotify->read_failed ? -1 : 0;
+}
+
+static void notify_now_callback(struct mailbox_list_notify_index *inotify)
+{
+ timeout_remove(&inotify->to_notify);
+ inotify->wait_callback(inotify->wait_context);
+}
+
+static void list_notify_callback(struct mailbox_list_notify_index *inotify)
+{
+ struct stat list_prev_st = inotify->list_last_st;
+
+ if (inotify->to_notify != NULL) {
+ /* there's a pending notification already -
+ no need to stat() again */
+ return;
+ }
+
+ notify_update_stat(inotify, TRUE, FALSE);
+ if (ST_CHANGED(inotify->list_last_st, list_prev_st)) {
+ /* log has changed. call the callback with a small delay
+ to allow bundling multiple changes together */
+ inotify->to_notify =
+ timeout_add_short(NOTIFY_DELAY_MSECS,
+ notify_now_callback, inotify);
+ }
+}
+
+static void inbox_notify_callback(struct mailbox_list_notify_index *inotify)
+{
+ struct stat inbox_prev_st = inotify->inbox_last_st;
+
+ if (inotify->to_notify != NULL && inotify->inbox_event_pending) {
+ /* there's a pending INBOX notification already -
+ no need to stat() again */
+ return;
+ }
+
+ notify_update_stat(inotify, FALSE, TRUE);
+ if (ST_CHANGED(inotify->inbox_last_st, inbox_prev_st))
+ inotify->inbox_event_pending = TRUE;
+ if (inotify->inbox_event_pending && inotify->to_notify == NULL) {
+ /* log has changed. call the callback with a small delay
+ to allow bundling multiple changes together */
+ inotify->to_notify =
+ timeout_add_short(NOTIFY_DELAY_MSECS,
+ notify_now_callback, inotify);
+ }
+}
+
+static void full_notify_callback(struct mailbox_list_notify_index *inotify)
+{
+ list_notify_callback(inotify);
+ inbox_notify_callback(inotify);
+}
+
+void mailbox_list_index_notify_wait(struct mailbox_list_notify *notify,
+ void (*callback)(void *context),
+ void *context)
+{
+ struct mailbox_list_notify_index *inotify =
+ (struct mailbox_list_notify_index *)notify;
+ unsigned int check_interval;
+
+ inotify->wait_callback = callback;
+ inotify->wait_context = context;
+
+ if (callback == NULL) {
+ io_remove(&inotify->io_wait);
+ io_remove(&inotify->io_wait_inbox);
+ timeout_remove(&inotify->to_wait);
+ timeout_remove(&inotify->to_notify);
+ } else if (inotify->to_wait == NULL) {
+ (void)io_add_notify(inotify->list_log_path, list_notify_callback,
+ inotify, &inotify->io_wait);
+ /* we need to check for INBOX explicitly, because INBOX changes
+ don't get added to mailbox.list.index.log */
+ if (inotify->inbox_log_path != NULL) {
+ (void)io_add_notify(inotify->inbox_log_path,
+ inbox_notify_callback, inotify,
+ &inotify->io_wait_inbox);
+ }
+ /* check with timeout as well, in case io_add_notify()
+ doesn't work (e.g. NFS) */
+ check_interval = notify->list->mail_set->mailbox_idle_check_interval;
+ i_assert(check_interval > 0);
+ inotify->to_wait = timeout_add(check_interval * 1000,
+ full_notify_callback, inotify);
+ notify_update_stat(inotify, TRUE, TRUE);
+ }
+}
+
+void mailbox_list_index_notify_flush(struct mailbox_list_notify *notify)
+{
+ struct mailbox_list_notify_index *inotify =
+ (struct mailbox_list_notify_index *)notify;
+
+ if (inotify->to_notify == NULL &&
+ notify->list->mail_set->mailbox_idle_check_interval > 0) {
+ /* no pending notification - check if anything had changed */
+ full_notify_callback(inotify);
+ }
+ if (inotify->to_notify != NULL)
+ notify_now_callback(inotify);
+}
diff --git a/src/lib-storage/list/mailbox-list-index-status.c b/src/lib-storage/list/mailbox-list-index-status.c
new file mode 100644
index 0000000..4218ae7
--- /dev/null
+++ b/src/lib-storage/list/mailbox-list-index-status.c
@@ -0,0 +1,862 @@
+/* Copyright (c) 2006-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "mail-index-modseq.h"
+#include "mailbox-list-index-storage.h"
+#include "mailbox-list-index.h"
+
+#define CACHED_STATUS_ITEMS \
+ (STATUS_MESSAGES | STATUS_UNSEEN | STATUS_RECENT | \
+ STATUS_UIDNEXT | STATUS_UIDVALIDITY | STATUS_HIGHESTMODSEQ)
+
+struct index_list_changes {
+ struct mailbox_status status;
+ guid_128_t guid;
+ uint32_t seq;
+ struct mailbox_index_vsize vsize;
+ uint32_t first_uid;
+
+ bool rec_changed;
+ bool msgs_changed;
+ bool hmodseq_changed;
+ bool vsize_changed;
+ bool first_saved_changed;
+};
+
+struct index_list_storage_module index_list_storage_module =
+ MODULE_CONTEXT_INIT(&mail_storage_module_register);
+
+static int
+index_list_exists(struct mailbox *box, bool auto_boxes,
+ enum mailbox_existence *existence_r)
+{
+ struct index_list_mailbox *ibox = INDEX_LIST_STORAGE_CONTEXT(box);
+ struct mail_index_view *view;
+ const struct mail_index_record *rec;
+ enum mailbox_list_index_flags flags;
+ uint32_t seq;
+
+ if (mailbox_list_index_view_open(box, FALSE, &view, &seq) <= 0) {
+ /* failure / not found. fallback to the real storage check
+ just in case to see if the mailbox was just created. */
+ return ibox->module_ctx.super.
+ exists(box, auto_boxes, existence_r);
+ }
+ rec = mail_index_lookup(view, seq);
+ flags = rec->flags;
+ mail_index_view_close(&view);
+
+ if ((flags & MAILBOX_LIST_INDEX_FLAG_NONEXISTENT) != 0)
+ *existence_r = MAILBOX_EXISTENCE_NONE;
+ else if ((flags & MAILBOX_LIST_INDEX_FLAG_NOSELECT) != 0)
+ *existence_r = MAILBOX_EXISTENCE_NOSELECT;
+ else
+ *existence_r = MAILBOX_EXISTENCE_SELECT;
+ return 0;
+}
+
+bool mailbox_list_index_status(struct mailbox_list *list,
+ struct mail_index_view *view,
+ uint32_t seq, enum mailbox_status_items items,
+ struct mailbox_status *status_r,
+ uint8_t *mailbox_guid,
+ struct mailbox_index_vsize *vsize_r,
+ const char **reason_r)
+{
+ struct mailbox_list_index *ilist = INDEX_LIST_CONTEXT_REQUIRE(list);
+ const void *data;
+ bool expunged;
+ const char *reason = NULL;
+
+ if ((items & STATUS_UIDVALIDITY) != 0 || mailbox_guid != NULL) {
+ const struct mailbox_list_index_record *rec;
+
+ mail_index_lookup_ext(view, seq, ilist->ext_id,
+ &data, &expunged);
+ rec = data;
+ if (rec == NULL) {
+ if ((items & STATUS_UIDVALIDITY) != 0)
+ reason = "Record for UIDVALIDITY";
+ else
+ reason = "Record for GUID";
+ } else {
+ if ((items & STATUS_UIDVALIDITY) != 0 &&
+ rec->uid_validity == 0)
+ reason = "UIDVALIDITY=0";
+ else
+ status_r->uidvalidity = rec->uid_validity;
+ if (mailbox_guid != NULL)
+ memcpy(mailbox_guid, rec->guid, GUID_128_SIZE);
+ }
+ }
+
+ if ((items & (STATUS_MESSAGES | STATUS_UNSEEN |
+ STATUS_RECENT | STATUS_UIDNEXT)) != 0) {
+ const struct mailbox_list_index_msgs_record *rec;
+
+ mail_index_lookup_ext(view, seq, ilist->msgs_ext_id,
+ &data, &expunged);
+ rec = data;
+ if (rec == NULL)
+ reason = "Record for message counts";
+ else if (rec->uidnext == 0) {
+ reason = "Empty record for message counts";
+ } else {
+ status_r->messages = rec->messages;
+ status_r->unseen = rec->unseen;
+ status_r->recent = rec->recent;
+ status_r->uidnext = rec->uidnext;
+ }
+ }
+ if ((items & STATUS_HIGHESTMODSEQ) != 0) {
+ const uint64_t *rec;
+
+ mail_index_lookup_ext(view, seq, ilist->hmodseq_ext_id,
+ &data, &expunged);
+ rec = data;
+ if (rec == NULL)
+ reason = "Record for HIGHESTMODSEQ";
+ else if (*rec == 0)
+ reason = "HIGHESTMODSEQ=0";
+ else
+ status_r->highest_modseq = *rec;
+ }
+ if (vsize_r != NULL) {
+ mail_index_lookup_ext(view, seq, ilist->vsize_ext_id,
+ &data, &expunged);
+ if (data == NULL)
+ reason = "Record for vsize";
+ else
+ memcpy(vsize_r, data, sizeof(*vsize_r));
+ }
+ *reason_r = reason;
+ return reason == NULL;
+}
+
+static int
+index_list_get_cached_status(struct mailbox *box,
+ enum mailbox_status_items items,
+ struct mailbox_status *status_r)
+{
+ struct mail_index_view *view;
+ const char *reason;
+ uint32_t seq;
+ int ret;
+
+ if (items == 0)
+ return 1;
+
+ if ((items & STATUS_UNSEEN) != 0 &&
+ (mailbox_get_private_flags_mask(box) & MAIL_SEEN) != 0) {
+ /* can't get UNSEEN from list index, since each user has
+ different \Seen flags */
+ return 0;
+ }
+
+ if ((ret = mailbox_list_index_view_open(box, TRUE, &view, &seq)) <= 0)
+ return ret;
+
+ ret = mailbox_list_index_status(box->list, view, seq, items,
+ status_r, NULL, NULL, &reason) ? 1 : 0;
+ if (ret == 0) {
+ e_debug(box->event,
+ "Couldn't get status items from mailbox list index: %s",
+ reason);
+ }
+ mail_index_view_close(&view);
+ return ret;
+}
+
+static int
+index_list_get_status(struct mailbox *box, enum mailbox_status_items items,
+ struct mailbox_status *status_r)
+{
+ struct index_list_mailbox *ibox = INDEX_LIST_STORAGE_CONTEXT(box);
+
+ if ((items & ENUM_NEGATE(CACHED_STATUS_ITEMS)) == 0 && !box->opened) {
+ if (index_list_get_cached_status(box, items, status_r) > 0)
+ return 0;
+ /* nonsynced / error, fallback to doing it the slow way */
+ }
+ return ibox->module_ctx.super.get_status(box, items, status_r);
+}
+
+/* Opportunistic function to see ïf we can extract guid from mailbox path */
+static bool index_list_get_guid_from_path(struct mailbox *box, guid_128_t guid_r)
+{
+ const char *path;
+ if (mailbox_get_path_to(box, MAILBOX_LIST_PATH_TYPE_MAILBOX, &path) <= 0)
+ return FALSE;
+ const char *ptr = strrchr(path, '/');
+ if (ptr == NULL)
+ return FALSE;
+ return guid_128_from_string(ptr + 1, guid_r) == 0;
+}
+
+static int
+index_list_get_cached_guid(struct mailbox *box, guid_128_t guid_r)
+{
+ struct mailbox_list_index *ilist = INDEX_LIST_CONTEXT_REQUIRE(box->list);
+ struct mailbox_status status;
+ struct mail_index_view *view;
+ const char *reason;
+ uint32_t seq;
+ int ret;
+
+ /* If using INDEX layout, try determine GUID from mailbox path */
+ if (!ilist->has_backing_store &&
+ index_list_get_guid_from_path(box, guid_r))
+ return 1;
+
+ if (ilist->syncing) {
+ /* syncing wants to know the GUID for a new mailbox. */
+ return 0;
+ }
+
+ if ((ret = mailbox_list_index_view_open(box, FALSE, &view, &seq)) <= 0)
+ return ret;
+
+ ret = mailbox_list_index_status(box->list, view, seq, 0,
+ &status, guid_r, NULL, &reason) ? 1 : 0;
+ if (ret > 0 && guid_128_is_empty(guid_r)) {
+ reason = "GUID is empty";
+ ret = 0;
+ }
+ if (ret == 0) {
+ e_debug(box->event,
+ "Couldn't get GUID from mailbox list index: %s",
+ reason);
+ }
+ mail_index_view_close(&view);
+ return ret;
+}
+
+static int index_list_get_cached_vsize(struct mailbox *box, uoff_t *vsize_r)
+{
+ struct mailbox_list_index *ilist = INDEX_LIST_CONTEXT_REQUIRE(box->list);
+ struct mailbox_status status;
+ struct mailbox_index_vsize vsize;
+ struct mail_index_view *view;
+ const char *reason;
+ uint32_t seq;
+ int ret;
+
+ i_assert(!ilist->syncing);
+
+ if ((ret = mailbox_list_index_view_open(box, TRUE, &view, &seq)) <= 0)
+ return ret;
+
+ ret = mailbox_list_index_status(box->list, view, seq,
+ STATUS_MESSAGES | STATUS_UIDNEXT,
+ &status, NULL, &vsize, &reason) ? 1 : 0;
+ if (ret > 0 && status.messages == 0 && status.uidnext > 0) {
+ /* mailbox is empty. its size has to be zero, regardless of
+ what the vsize header says. */
+ vsize.vsize = 0;
+ } else if (ret > 0 && (vsize.highest_uid + 1 != status.uidnext ||
+ vsize.message_count != status.messages)) {
+ /* out of date vsize info */
+ reason = "out of date vsize info";
+ ret = 0;
+ }
+ if (ret > 0)
+ *vsize_r = vsize.vsize;
+ else if (ret == 0) {
+ e_debug(box->event,
+ "Couldn't get vsize from mailbox list index: %s",
+ reason);
+ }
+ mail_index_view_close(&view);
+ return ret;
+}
+
+static int
+index_list_get_cached_first_saved(struct mailbox *box,
+ struct mailbox_index_first_saved *first_saved_r)
+{
+ struct mailbox_list_index *ilist = INDEX_LIST_CONTEXT_REQUIRE(box->list);
+ struct mail_index_view *view;
+ struct mailbox_status status;
+ const char *reason;
+ const void *data;
+ bool expunged;
+ uint32_t seq;
+ int ret;
+
+ i_zero(first_saved_r);
+
+ if ((ret = mailbox_list_index_view_open(box, TRUE, &view, &seq)) <= 0)
+ return ret;
+
+ mail_index_lookup_ext(view, seq, ilist->first_saved_ext_id,
+ &data, &expunged);
+ if (data != NULL)
+ memcpy(first_saved_r, data, sizeof(*first_saved_r));
+ if (first_saved_r->timestamp != 0 && first_saved_r->uid == 0) {
+ /* mailbox was empty the last time we updated this.
+ we'll need to verify if it still is. */
+ if (!mailbox_list_index_status(box->list, view, seq,
+ STATUS_MESSAGES,
+ &status, NULL, NULL, &reason) ||
+ status.messages != 0)
+ first_saved_r->timestamp = 0;
+ }
+ mail_index_view_close(&view);
+ return first_saved_r->timestamp != 0 ? 1 : 0;
+}
+
+static int
+index_list_try_get_metadata(struct mailbox *box,
+ enum mailbox_metadata_items items,
+ struct mailbox_metadata *metadata_r)
+{
+ enum mailbox_metadata_items noncached_items;
+ int ret;
+
+ i_assert(metadata_r != NULL);
+
+ if (box->opened) {
+ /* if mailbox is already opened, don't bother using the values
+ in mailbox list index. they have a higher chance of being
+ wrong. */
+ return 0;
+ }
+ /* see if we have a chance of fulfilling this without opening
+ the mailbox. */
+ noncached_items = items & ENUM_NEGATE(MAILBOX_METADATA_GUID |
+ MAILBOX_METADATA_VIRTUAL_SIZE |
+ MAILBOX_METADATA_FIRST_SAVE_DATE);
+ if ((noncached_items & MAILBOX_METADATA_PHYSICAL_SIZE) != 0 &&
+ box->mail_vfuncs->get_physical_size ==
+ box->mail_vfuncs->get_virtual_size)
+ noncached_items = items & ENUM_NEGATE(MAILBOX_METADATA_PHYSICAL_SIZE);
+
+ if (noncached_items != 0)
+ return 0;
+
+ if ((items & MAILBOX_METADATA_GUID) != 0) {
+ if ((ret = index_list_get_cached_guid(box, metadata_r->guid)) <= 0)
+ return ret;
+ }
+ if ((items & (MAILBOX_METADATA_VIRTUAL_SIZE |
+ MAILBOX_METADATA_PHYSICAL_SIZE)) != 0) {
+ if ((ret = index_list_get_cached_vsize(box, &metadata_r->virtual_size)) <= 0)
+ return ret;
+ if ((items & MAILBOX_METADATA_PHYSICAL_SIZE) != 0)
+ metadata_r->physical_size = metadata_r->virtual_size;
+ }
+ if ((items & MAILBOX_METADATA_FIRST_SAVE_DATE) != 0) {
+ struct mailbox_index_first_saved first_saved;
+
+ /* start writing first_saved to mailbox list index if it wasn't
+ there already. */
+ box->update_first_saved = TRUE;
+
+ if ((ret = index_list_get_cached_first_saved(box, &first_saved)) <= 0)
+ return ret;
+ metadata_r->first_save_date =
+ first_saved.timestamp == (uint32_t)-1 ? (time_t)-1 :
+ (time_t)first_saved.timestamp;
+ }
+ return 1;
+}
+
+static int
+index_list_get_metadata(struct mailbox *box,
+ enum mailbox_metadata_items items,
+ struct mailbox_metadata *metadata_r)
+{
+ struct index_list_mailbox *ibox = INDEX_LIST_STORAGE_CONTEXT(box);
+
+ if (index_list_try_get_metadata(box, items, metadata_r) != 0)
+ return 0;
+ return ibox->module_ctx.super.get_metadata(box, items, metadata_r);
+}
+
+static void
+index_list_update_fill_vsize(struct mailbox *box,
+ struct mail_index_view *view,
+ struct index_list_changes *changes_r)
+{
+ const void *data;
+ size_t size;
+
+ mail_index_get_header_ext(view, box->vsize_hdr_ext_id,
+ &data, &size);
+ if (size == sizeof(changes_r->vsize))
+ memcpy(&changes_r->vsize, data, sizeof(changes_r->vsize));
+}
+
+static bool
+index_list_update_fill_changes(struct mailbox *box,
+ struct mail_index_view *list_view,
+ struct index_list_changes *changes_r)
+{
+ struct mailbox_list_index_node *node;
+ struct mail_index_view *view;
+ const struct mail_index_header *hdr;
+ struct mailbox_metadata metadata;
+ uint32_t seq1, seq2;
+
+ i_zero(changes_r);
+
+ node = mailbox_list_index_lookup(box->list, box->name);
+ if (node == NULL)
+ return FALSE;
+ if (!mail_index_lookup_seq(list_view, node->uid, &changes_r->seq))
+ return FALSE;
+
+ /* get STATUS info using the latest data in index.
+ note that for shared mailboxes (with private indexes) this
+ also means that the unseen count is always the owner's
+ count, not what exists in the private index. */
+ view = mail_index_view_open(box->index);
+ hdr = mail_index_get_header(view);
+
+ changes_r->status.messages = hdr->messages_count;
+ changes_r->status.unseen =
+ hdr->messages_count - hdr->seen_messages_count;
+ changes_r->status.uidvalidity = hdr->uid_validity;
+ changes_r->status.uidnext = hdr->next_uid;
+
+ if (!mail_index_lookup_seq_range(view, hdr->first_recent_uid,
+ (uint32_t)-1, &seq1, &seq2))
+ changes_r->status.recent = 0;
+ else
+ changes_r->status.recent = seq2 - seq1 + 1;
+
+ changes_r->status.highest_modseq = mail_index_modseq_get_highest(view);
+ if (changes_r->status.highest_modseq == 0) {
+ /* modseqs not enabled yet, but we can't return 0 */
+ changes_r->status.highest_modseq = 1;
+ }
+ index_list_update_fill_vsize(box, view, changes_r);
+ mail_index_view_close(&view); hdr = NULL;
+
+ if (mailbox_get_metadata(box, MAILBOX_METADATA_GUID, &metadata) == 0)
+ memcpy(changes_r->guid, metadata.guid, sizeof(changes_r->guid));
+ return TRUE;
+}
+
+static void
+index_list_first_saved_update_changes(struct mailbox *box,
+ struct mail_index_view *list_view,
+ struct index_list_changes *changes)
+{
+ struct mailbox_list_index *ilist = INDEX_LIST_CONTEXT_REQUIRE(box->list);
+ struct mailbox_index_first_saved first_saved;
+ const void *data;
+ bool expunged;
+
+ mail_index_lookup_ext(list_view, changes->seq,
+ ilist->first_saved_ext_id, &data, &expunged);
+ if (data == NULL)
+ i_zero(&first_saved);
+ else
+ memcpy(&first_saved, data, sizeof(first_saved));
+ if (mail_index_view_get_messages_count(box->view) > 0)
+ mail_index_lookup_uid(box->view, 1, &changes->first_uid);
+ if (first_saved.uid == 0 && first_saved.timestamp == 0) {
+ /* it's not in the index yet. we'll set it only if we've
+ just called MAILBOX_METADATA_FIRST_SAVE_DATE. */
+ changes->first_saved_changed = box->update_first_saved;
+ } else {
+ changes->first_saved_changed =
+ changes->first_uid != first_saved.uid;
+ }
+}
+
+static bool
+index_list_has_changed(struct mailbox *box, struct mail_index_view *list_view,
+ struct index_list_changes *changes)
+{
+ struct mailbox_status old_status;
+ struct mailbox_index_vsize old_vsize;
+ const char *reason;
+ guid_128_t old_guid;
+
+ i_zero(&old_status);
+ i_zero(&old_vsize);
+ memset(old_guid, 0, sizeof(old_guid));
+ (void)mailbox_list_index_status(box->list, list_view, changes->seq,
+ CACHED_STATUS_ITEMS,
+ &old_status, old_guid,
+ &old_vsize, &reason);
+
+ changes->rec_changed =
+ old_status.uidvalidity != changes->status.uidvalidity &&
+ changes->status.uidvalidity != 0;
+ if (!guid_128_equals(changes->guid, old_guid) &&
+ !guid_128_is_empty(changes->guid))
+ changes->rec_changed = TRUE;
+
+ if (MAILBOX_IS_NEVER_IN_INDEX(box)) {
+ /* check only UIDVALIDITY and GUID changes for INBOX */
+ return changes->rec_changed;
+ }
+
+ changes->msgs_changed =
+ old_status.messages != changes->status.messages ||
+ old_status.unseen != changes->status.unseen ||
+ old_status.recent != changes->status.recent ||
+ old_status.uidnext != changes->status.uidnext;
+ /* update highest-modseq only if they're ever been used */
+ if (old_status.highest_modseq == changes->status.highest_modseq) {
+ changes->hmodseq_changed = FALSE;
+ } else {
+ changes->hmodseq_changed = TRUE;
+ }
+ if (memcmp(&old_vsize, &changes->vsize, sizeof(old_vsize)) != 0)
+ changes->vsize_changed = TRUE;
+ index_list_first_saved_update_changes(box, list_view, changes);
+
+ return changes->rec_changed || changes->msgs_changed ||
+ changes->hmodseq_changed || changes->vsize_changed ||
+ changes->first_saved_changed;
+}
+
+static void
+index_list_update_first_saved(struct mailbox *box,
+ struct mail_index_transaction *list_trans,
+ const struct index_list_changes *changes)
+{
+ struct mailbox_list_index *ilist = INDEX_LIST_CONTEXT_REQUIRE(box->list);
+ struct mailbox_transaction_context *t;
+ struct mail *mail;
+ struct mailbox_index_first_saved first_saved;
+ uint32_t seq, messages_count;
+ time_t save_date;
+ int ret = 0;
+
+ i_zero(&first_saved);
+ first_saved.timestamp = (uint32_t)-1;
+
+ if (changes->first_uid != 0) {
+ t = mailbox_transaction_begin(box, 0, __func__);
+ mail = mail_alloc(t, MAIL_FETCH_SAVE_DATE, NULL);
+ messages_count = mail_index_view_get_messages_count(box->view);
+ for (seq = 1; seq <= messages_count; seq++) {
+ mail_set_seq(mail, seq);
+ if (mail_get_save_date(mail, &save_date) >= 0) {
+ first_saved.uid = mail->uid;
+ first_saved.timestamp = save_date;
+ break;
+ }
+ if (mailbox_get_last_mail_error(box) != MAIL_ERROR_EXPUNGED) {
+ ret = -1;
+ break;
+ }
+ }
+ mail_free(&mail);
+ (void)mailbox_transaction_commit(&t);
+ }
+ if (ret == 0) {
+ mail_index_update_ext(list_trans, changes->seq,
+ ilist->first_saved_ext_id,
+ &first_saved, NULL);
+ }
+}
+
+
+static void
+index_list_update(struct mailbox *box, struct mail_index_view *list_view,
+ struct mail_index_transaction *list_trans,
+ const struct index_list_changes *changes)
+{
+ struct mailbox_list_index *ilist = INDEX_LIST_CONTEXT_REQUIRE(box->list);
+
+ if (changes->rec_changed) {
+ struct mailbox_list_index_record rec;
+ const void *old_data;
+ bool expunged;
+
+ mail_index_lookup_ext(list_view, changes->seq, ilist->ext_id,
+ &old_data, &expunged);
+ i_assert(old_data != NULL);
+ memcpy(&rec, old_data, sizeof(rec));
+
+ if (changes->status.uidvalidity != 0)
+ rec.uid_validity = changes->status.uidvalidity;
+ if (!guid_128_is_empty(changes->guid))
+ memcpy(rec.guid, changes->guid, sizeof(rec.guid));
+ mail_index_update_ext(list_trans, changes->seq, ilist->ext_id,
+ &rec, NULL);
+ }
+
+ if (changes->msgs_changed) {
+ struct mailbox_list_index_msgs_record msgs;
+
+ i_zero(&msgs);
+ msgs.messages = changes->status.messages;
+ msgs.unseen = changes->status.unseen;
+ msgs.recent = changes->status.recent;
+ msgs.uidnext = changes->status.uidnext;
+
+ mail_index_update_ext(list_trans, changes->seq,
+ ilist->msgs_ext_id, &msgs, NULL);
+ }
+ if (changes->hmodseq_changed) {
+ mail_index_update_ext(list_trans, changes->seq,
+ ilist->hmodseq_ext_id,
+ &changes->status.highest_modseq, NULL);
+ }
+ if (changes->vsize_changed) {
+ mail_index_update_ext(list_trans, changes->seq,
+ ilist->vsize_ext_id,
+ &changes->vsize, NULL);
+ }
+ if (changes->first_saved_changed)
+ index_list_update_first_saved(box, list_trans, changes);
+}
+
+static int index_list_update_mailbox(struct mailbox *box)
+{
+ struct mailbox_list_index *ilist = INDEX_LIST_CONTEXT_REQUIRE(box->list);
+ struct mail_index_sync_ctx *list_sync_ctx;
+ struct mail_index_view *list_view;
+ struct mail_index_transaction *list_trans;
+ struct index_list_changes changes;
+ int ret;
+
+ i_assert(box->opened);
+
+ if (ilist->syncing || ilist->updating_status)
+ return 0;
+ if (box->deleting) {
+ /* don't update status info while mailbox is being deleted.
+ especially not a good idea if we're rolling back a created
+ mailbox that somebody else had just created */
+ return 0;
+ }
+
+ /* refresh the mailbox list index once. we can't do this again after
+ locking, because it could trigger list syncing. */
+ (void)mailbox_list_index_refresh(box->list);
+
+ /* first do a quick check while unlocked to see if anything changes */
+ list_view = mail_index_view_open(ilist->index);
+ if (!index_list_update_fill_changes(box, list_view, &changes))
+ ret = -1;
+ else if (index_list_has_changed(box, list_view, &changes))
+ ret = 1;
+ else {
+ /* if backend state changed on the last check, update it here
+ now. we probably don't need to bother checking again if the
+ state had changed? */
+ ret = ilist->index_last_check_changed ? 1 : 0;
+ }
+ mail_index_view_close(&list_view);
+ if (ret <= 0) {
+ if (ret < 0)
+ mailbox_list_index_refresh_later(box->list);
+ return 0;
+ }
+
+ /* looks like there are some changes. now lock the list index and do
+ the whole thing all over again while locked. this guarantees
+ that we'll always write the latest state of the mailbox. */
+ if (mail_index_sync_begin(ilist->index, &list_sync_ctx,
+ &list_view, &list_trans, 0) < 0) {
+ mailbox_set_index_error(box);
+ return -1;
+ }
+ /* refresh to latest state of the mailbox now that we're locked */
+ if (mail_index_refresh(box->index) < 0) {
+ mailbox_set_index_error(box);
+ mail_index_sync_rollback(&list_sync_ctx);
+ return -1;
+ }
+
+ if (!index_list_update_fill_changes(box, list_view, &changes))
+ mailbox_list_index_refresh_later(box->list);
+ else {
+ ilist->updating_status = TRUE;
+ if (index_list_has_changed(box, list_view, &changes))
+ index_list_update(box, list_view, list_trans, &changes);
+ if (box->v.list_index_update_sync != NULL &&
+ !MAILBOX_IS_NEVER_IN_INDEX(box)) {
+ box->v.list_index_update_sync(box, list_trans,
+ changes.seq);
+ }
+ ilist->updating_status = FALSE;
+ }
+
+ struct mail_index_sync_rec sync_rec;
+ while (mail_index_sync_next(list_sync_ctx, &sync_rec)) ;
+ if (mail_index_sync_commit(&list_sync_ctx) < 0) {
+ mailbox_set_index_error(box);
+ return -1;
+ }
+ ilist->index_last_check_changed = FALSE;
+ return 0;
+}
+
+void mailbox_list_index_update_mailbox_index(struct mailbox *box,
+ const struct mailbox_update *update)
+{
+ struct mail_index_view *list_view;
+ struct mail_index_transaction *list_trans;
+ struct index_list_changes changes;
+ struct mailbox_status status;
+ const char *reason;
+ guid_128_t mailbox_guid;
+ bool guid_changed = FALSE;
+
+ i_zero(&changes);
+ /* update the mailbox list index even if it has some other pending
+ changes. */
+ if (mailbox_list_index_view_open(box, FALSE, &list_view, &changes.seq) <= 0)
+ return;
+
+ guid_128_empty(mailbox_guid);
+ (void)mailbox_list_index_status(box->list, list_view, changes.seq,
+ CACHED_STATUS_ITEMS, &status,
+ mailbox_guid, NULL, &reason);
+
+ if (update->uid_validity != 0) {
+ changes.rec_changed = TRUE;
+ changes.status.uidvalidity = update->uid_validity;
+ }
+ if (!guid_128_equals(update->mailbox_guid, mailbox_guid) &&
+ !guid_128_is_empty(update->mailbox_guid)) {
+ changes.rec_changed = TRUE;
+ memcpy(changes.guid, update->mailbox_guid, sizeof(changes.guid));
+ guid_changed = TRUE;
+ }
+ if (guid_changed ||
+ update->uid_validity != 0 ||
+ update->min_next_uid != 0 ||
+ update->min_first_recent_uid != 0 ||
+ update->min_highest_modseq != 0) {
+ /* reset status counters to 0. let the syncing later figure out
+ their correct values. */
+ changes.msgs_changed = TRUE;
+ changes.hmodseq_changed = TRUE;
+ }
+ list_trans = mail_index_transaction_begin(list_view,
+ MAIL_INDEX_TRANSACTION_FLAG_EXTERNAL);
+ index_list_update(box, list_view, list_trans, &changes);
+ (void)mail_index_transaction_commit(&list_trans);
+ mail_index_view_close(&list_view);
+}
+
+void mailbox_list_index_status_sync_init(struct mailbox *box)
+{
+ struct index_list_mailbox *ibox = INDEX_LIST_STORAGE_CONTEXT(box);
+ const struct mail_index_header *hdr;
+
+ hdr = mail_index_get_header(box->view);
+ ibox->pre_sync_log_file_seq = hdr->log_file_seq;
+ ibox->pre_sync_log_file_head_offset = hdr->log_file_head_offset;
+}
+
+void mailbox_list_index_status_sync_deinit(struct mailbox *box)
+{
+ struct index_list_mailbox *ibox = INDEX_LIST_STORAGE_CONTEXT(box);
+ struct mailbox_list_index *ilist = INDEX_LIST_CONTEXT_REQUIRE(box->list);
+ const struct mail_index_header *hdr;
+
+ hdr = mail_index_get_header(box->view);
+ if (!ilist->opened &&
+ ibox->pre_sync_log_file_head_offset == hdr->log_file_head_offset &&
+ ibox->pre_sync_log_file_seq == hdr->log_file_seq) {
+ /* List index isn't open and sync changed nothing.
+ Don't bother opening the list index. */
+ return;
+ }
+
+ /* it probably doesn't matter much here if we push/pop the error,
+ but might as well do it. */
+ mail_storage_last_error_push(mailbox_get_storage(box));
+ (void)index_list_update_mailbox(box);
+ mail_storage_last_error_pop(mailbox_get_storage(box));
+}
+
+static int
+index_list_transaction_commit(struct mailbox_transaction_context *t,
+ struct mail_transaction_commit_changes *changes_r)
+{
+ struct mailbox *box = t->box;
+ struct index_list_mailbox *ibox = INDEX_LIST_STORAGE_CONTEXT(box);
+
+ if (ibox->module_ctx.super.transaction_commit(t, changes_r) < 0)
+ return -1;
+ t = NULL;
+
+ /* check all changes here, because e.g. vsize update is _OTHERS */
+ if (changes_r->changes_mask == 0)
+ return 0;
+
+ /* this transaction commit may have been done in error handling path
+ and the caller still wants to access the current error. make sure
+ that whatever we do here won't change the error. */
+ mail_storage_last_error_push(mailbox_get_storage(box));
+ (void)index_list_update_mailbox(box);
+ mail_storage_last_error_pop(mailbox_get_storage(box));
+ return 0;
+}
+
+void mailbox_list_index_status_set_info_flags(struct mailbox *box, uint32_t uid,
+ enum mailbox_info_flags *flags)
+{
+ struct mailbox_list_index *ilist = INDEX_LIST_CONTEXT_REQUIRE(box->list);
+ struct mail_index_view *view;
+ struct mailbox_status status;
+ const char *reason;
+ uint32_t seq;
+ int ret;
+
+ view = mail_index_view_open(ilist->index);
+ if (!mail_index_lookup_seq(view, uid, &seq)) {
+ /* our in-memory tree is out of sync */
+ ret = 1;
+ } else {
+ ret = box->v.list_index_has_changed == NULL ? 0 :
+ box->v.list_index_has_changed(box, view, seq, TRUE,
+ &reason);
+ }
+
+ if (ret != 0) {
+ /* error / not up to date. don't waste time with it. */
+ mail_index_view_close(&view);
+ return;
+ }
+
+ status.recent = 0;
+ (void)mailbox_list_index_status(box->list, view, seq, STATUS_RECENT,
+ &status, NULL, NULL, &reason);
+ mail_index_view_close(&view);
+
+ if (status.recent != 0)
+ *flags |= MAILBOX_MARKED;
+ else
+ *flags |= MAILBOX_UNMARKED;
+}
+
+void mailbox_list_index_status_init_mailbox(struct mailbox_vfuncs *v)
+{
+ v->exists = index_list_exists;
+ v->get_status = index_list_get_status;
+ v->get_metadata = index_list_get_metadata;
+ v->transaction_commit = index_list_transaction_commit;
+}
+
+void mailbox_list_index_status_init_finish(struct mailbox_list *list)
+{
+ struct mailbox_list_index *ilist = INDEX_LIST_CONTEXT_REQUIRE(list);
+
+ ilist->msgs_ext_id = mail_index_ext_register(ilist->index, "msgs", 0,
+ sizeof(struct mailbox_list_index_msgs_record),
+ sizeof(uint32_t));
+
+ ilist->hmodseq_ext_id =
+ mail_index_ext_register(ilist->index, "hmodseq", 0,
+ sizeof(uint64_t), sizeof(uint64_t));
+ ilist->vsize_ext_id =
+ mail_index_ext_register(ilist->index, "vsize", 0,
+ sizeof(struct mailbox_index_vsize), sizeof(uint64_t));
+ ilist->first_saved_ext_id =
+ mail_index_ext_register(ilist->index, "1saved", 0,
+ sizeof(struct mailbox_index_first_saved), sizeof(uint32_t));
+}
diff --git a/src/lib-storage/list/mailbox-list-index-storage.h b/src/lib-storage/list/mailbox-list-index-storage.h
new file mode 100644
index 0000000..6aeec8c
--- /dev/null
+++ b/src/lib-storage/list/mailbox-list-index-storage.h
@@ -0,0 +1,21 @@
+#ifndef MAILBOX_LIST_INDEX_STORAGE_H
+#define MAILBOX_LIST_INDEX_STORAGE_H
+
+#include "mail-storage-private.h"
+
+#define INDEX_LIST_STORAGE_CONTEXT(obj) \
+ MODULE_CONTEXT_REQUIRE(obj, index_list_storage_module)
+
+struct index_list_mailbox {
+ union mailbox_module_context module_ctx;
+
+ uint32_t pre_sync_log_file_seq;
+ uoff_t pre_sync_log_file_head_offset;
+
+ bool have_backend:1;
+};
+
+extern MODULE_CONTEXT_DEFINE(index_list_storage_module,
+ &mail_storage_module_register);
+
+#endif
diff --git a/src/lib-storage/list/mailbox-list-index-sync.c b/src/lib-storage/list/mailbox-list-index-sync.c
new file mode 100644
index 0000000..2994bb8
--- /dev/null
+++ b/src/lib-storage/list/mailbox-list-index-sync.c
@@ -0,0 +1,521 @@
+/* Copyright (c) 2006-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "ioloop.h"
+#include "hash.h"
+#include "str.h"
+#include "sort.h"
+#include "mail-index.h"
+#include "mail-storage.h"
+#include "mailbox-list-index-sync.h"
+
+static void
+node_lookup_guid(struct mailbox_list_index_sync_context *ctx,
+ const struct mailbox_list_index_node *node, guid_128_t guid_r)
+{
+ struct mailbox *box;
+ struct mailbox_metadata metadata;
+ const char *vname;
+ string_t *str = t_str_new(128);
+ char ns_sep = mailbox_list_get_hierarchy_sep(ctx->list);
+
+ mailbox_list_index_node_get_path(node, ns_sep, str);
+
+ vname = mailbox_list_get_vname(ctx->list, str_c(str));
+ box = mailbox_alloc(ctx->list, vname, 0);
+ if (mailbox_get_metadata(box, MAILBOX_METADATA_GUID, &metadata) == 0)
+ memcpy(guid_r, metadata.guid, GUID_128_SIZE);
+ mailbox_free(&box);
+}
+
+static void
+node_add_to_index(struct mailbox_list_index_sync_context *ctx,
+ const struct mailbox_list_index_node *node, uint32_t *seq_r)
+{
+ struct mailbox_list_index_record irec;
+ uint32_t seq;
+
+ i_zero(&irec);
+ irec.name_id = node->name_id;
+ if (node->parent != NULL)
+ irec.parent_uid = node->parent->uid;
+
+ /* get mailbox GUID if possible. we need to do this early in here to
+ make mailbox rename detection work in NOTIFY */
+ if (ctx->syncing_list) T_BEGIN {
+ node_lookup_guid(ctx, node, irec.guid);
+ } T_END;
+
+ mail_index_append(ctx->trans, node->uid, &seq);
+ mail_index_update_flags(ctx->trans, seq, MODIFY_REPLACE,
+ (enum mail_flags)MAILBOX_LIST_INDEX_FLAG_NONEXISTENT);
+ mail_index_update_ext(ctx->trans, seq, ctx->ilist->ext_id, &irec, NULL);
+
+ *seq_r = seq;
+}
+
+static struct mailbox_list_index_node *
+mailbox_list_index_node_add(struct mailbox_list_index_sync_context *ctx,
+ struct mailbox_list_index_node *parent,
+ const char *name, uint32_t *seq_r)
+{
+ struct mailbox_list_index_node *node;
+ char *dup_name;
+ mailbox_list_name_unescape(&name, ctx->list->set.storage_name_escape_char);
+
+ node = p_new(ctx->ilist->mailbox_pool,
+ struct mailbox_list_index_node, 1);
+ node->flags = MAILBOX_LIST_INDEX_FLAG_NONEXISTENT |
+ MAILBOX_LIST_INDEX_FLAG_SYNC_EXISTS;
+ /* we don't bother doing name deduplication here, even though it would
+ be possible. */
+ node->raw_name = dup_name = p_strdup(ctx->ilist->mailbox_pool, name);
+ node->name_id = ++ctx->ilist->highest_name_id;
+ node->uid = ctx->next_uid++;
+
+ if (parent != NULL) {
+ node->parent = parent;
+ node->next = parent->children;
+ parent->children = node;
+ } else {
+ node->next = ctx->ilist->mailbox_tree;
+ ctx->ilist->mailbox_tree = node;
+ }
+ hash_table_insert(ctx->ilist->mailbox_hash,
+ POINTER_CAST(node->uid), node);
+ hash_table_insert(ctx->ilist->mailbox_names,
+ POINTER_CAST(node->name_id), dup_name);
+
+ node_add_to_index(ctx, node, seq_r);
+ return node;
+}
+
+uint32_t mailbox_list_index_sync_name(struct mailbox_list_index_sync_context *ctx,
+ const char *name,
+ struct mailbox_list_index_node **node_r,
+ bool *created_r)
+{
+ const char *const *path, *empty_path[] = { "", NULL };
+ struct mailbox_list_index_node *node, *parent;
+ unsigned int i;
+ uint32_t seq = 0;
+
+ path = *name == '\0' ? empty_path :
+ t_strsplit(name, ctx->sep);
+ /* find the last node that exists in the path */
+ node = ctx->ilist->mailbox_tree; parent = NULL;
+ for (i = 0; path[i] != NULL; i++) {
+ node = mailbox_list_index_node_find_sibling(ctx->list, node, path[i]);
+ if (node == NULL)
+ break;
+
+ node->flags |= MAILBOX_LIST_INDEX_FLAG_SYNC_EXISTS;
+ parent = node;
+ node = node->children;
+ }
+
+ node = parent;
+ if (path[i] == NULL) {
+ /* the entire path exists */
+ i_assert(node != NULL);
+ if (!mail_index_lookup_seq(ctx->view, node->uid, &seq))
+ i_panic("mailbox list index: lost uid=%u", node->uid);
+ *created_r = FALSE;
+ } else {
+ /* create missing parts of the path */
+ for (; path[i] != NULL; i++) {
+ node = mailbox_list_index_node_add(ctx, node, path[i],
+ &seq);
+ }
+ *created_r = TRUE;
+ }
+
+ *node_r = node;
+ return seq;
+}
+
+static void
+get_existing_name_ids(ARRAY_TYPE(uint32_t) *ids,
+ const struct mailbox_list_index_node *node)
+{
+ for (; node != NULL; node = node->next) {
+ if (node->children != NULL)
+ get_existing_name_ids(ids, node->children);
+ array_push_back(ids, &node->name_id);
+ }
+}
+
+static void
+mailbox_list_index_sync_names(struct mailbox_list_index_sync_context *ctx)
+{
+ struct mailbox_list_index *ilist = ctx->ilist;
+ ARRAY_TYPE(uint32_t) existing_name_ids;
+ buffer_t *hdr_buf;
+ const void *ext_data;
+ size_t ext_size;
+ const char *name;
+ uint32_t id, prev_id = 0;
+
+ /* get all existing name IDs sorted */
+ i_array_init(&existing_name_ids, 64);
+ get_existing_name_ids(&existing_name_ids, ilist->mailbox_tree);
+ array_sort(&existing_name_ids, uint32_cmp);
+
+ hdr_buf = buffer_create_dynamic(default_pool, 1024);
+ buffer_append_zero(hdr_buf, sizeof(struct mailbox_list_index_header));
+
+ /* add existing names to header (with deduplication) */
+ array_foreach_elem(&existing_name_ids, id) {
+ if (id != prev_id) {
+ buffer_append(hdr_buf, &id, sizeof(id));
+ name = hash_table_lookup(ilist->mailbox_names,
+ POINTER_CAST(id));
+ i_assert(name != NULL);
+ buffer_append(hdr_buf, name, strlen(name) + 1);
+ prev_id = id;
+ }
+ }
+ buffer_append_zero(hdr_buf, sizeof(id));
+
+ /* make sure header size is ok in index and update it */
+ mail_index_get_header_ext(ctx->view, ilist->ext_id,
+ &ext_data, &ext_size);
+ if (nearest_power(ext_size) != nearest_power(hdr_buf->used)) {
+ mail_index_ext_resize(ctx->trans, ilist->ext_id,
+ nearest_power(hdr_buf->used),
+ sizeof(struct mailbox_list_index_record),
+ sizeof(uint32_t));
+ }
+ mail_index_update_header_ext(ctx->trans, ilist->ext_id,
+ 0, hdr_buf->data, hdr_buf->used);
+ buffer_free(&hdr_buf);
+ array_free(&existing_name_ids);
+}
+
+static void
+mailbox_list_index_node_clear_exists(struct mailbox_list_index_node *node)
+{
+ while (node != NULL) {
+ if (node->children != NULL)
+ mailbox_list_index_node_clear_exists(node->children);
+
+ node->flags &= ENUM_NEGATE(MAILBOX_LIST_INDEX_FLAG_SYNC_EXISTS);
+ node = node->next;
+ }
+}
+
+static void
+sync_expunge_nonexistent(struct mailbox_list_index_sync_context *sync_ctx,
+ struct mailbox_list_index_node *node)
+{
+ uint32_t seq;
+
+ while (node != NULL) {
+ if (node->children != NULL)
+ sync_expunge_nonexistent(sync_ctx, node->children);
+
+ if ((node->flags & MAILBOX_LIST_INDEX_FLAG_SYNC_EXISTS) == 0) {
+ if (mail_index_lookup_seq(sync_ctx->view, node->uid,
+ &seq))
+ mail_index_expunge(sync_ctx->trans, seq);
+ mailbox_list_index_node_unlink(sync_ctx->ilist, node);
+ }
+ node = node->next;
+ }
+}
+
+int mailbox_list_index_sync_begin(struct mailbox_list *list,
+ struct mailbox_list_index_sync_context **sync_ctx_r)
+{
+ struct mailbox_list_index *ilist = INDEX_LIST_CONTEXT_REQUIRE(list);
+ struct mailbox_list_index_sync_context *sync_ctx;
+ struct mail_index_sync_ctx *index_sync_ctx;
+ struct mail_index_view *view;
+ struct mail_index_transaction *trans;
+ const struct mail_index_header *hdr;
+ bool fix_attempted = FALSE;
+
+ i_assert(!ilist->syncing);
+
+retry:
+ if (mailbox_list_index_index_open(list) < 0)
+ return -1;
+
+ if (mail_index_sync_begin(ilist->index, &index_sync_ctx, &view, &trans,
+ MAIL_INDEX_SYNC_FLAG_AVOID_FLAG_UPDATES) < 0) {
+ mailbox_list_index_set_index_error(list);
+ return -1;
+ }
+ mailbox_list_index_reset(ilist);
+
+ /* re-parse mailbox list now that it's refreshed and locked */
+ if (mailbox_list_index_parse(list, view, TRUE) < 0) {
+ mail_index_sync_rollback(&index_sync_ctx);
+ return -1;
+ }
+ if (ilist->call_corruption_callback && !fix_attempted) {
+ /* unlock and resync the index */
+ mail_index_sync_rollback(&index_sync_ctx);
+ if (mailbox_list_index_handle_corruption(list) < 0)
+ return -1;
+ fix_attempted = TRUE;
+ goto retry;
+ }
+
+ sync_ctx = i_new(struct mailbox_list_index_sync_context, 1);
+ sync_ctx->list = list;
+ sync_ctx->ilist = ilist;
+ sync_ctx->sep[0] = mailbox_list_get_hierarchy_sep(list);
+ sync_ctx->orig_highest_name_id = ilist->highest_name_id;
+ sync_ctx->index_sync_ctx = index_sync_ctx;
+ sync_ctx->trans = trans;
+
+ hdr = mail_index_get_header(view);
+ sync_ctx->next_uid = hdr->next_uid;
+
+ if (hdr->uid_validity == 0) {
+ /* first time indexing, set uidvalidity */
+ uint32_t uid_validity = ioloop_time;
+
+ mail_index_update_header(trans,
+ offsetof(struct mail_index_header, uid_validity),
+ &uid_validity, sizeof(uid_validity), TRUE);
+ }
+ sync_ctx->view = mail_index_transaction_open_updated_view(trans);
+ ilist->sync_ctx = sync_ctx;
+ ilist->syncing = TRUE;
+
+ *sync_ctx_r = sync_ctx;
+ return 0;
+}
+
+static int
+mailbox_list_index_sync_list(struct mailbox_list_index_sync_context *sync_ctx)
+{
+ struct mailbox_list_iterate_context *iter;
+ const struct mailbox_info *info;
+ enum mailbox_list_index_flags flags;
+ const char *patterns[2];
+ struct mailbox_list_index_node *node;
+ uint32_t seq;
+ bool created;
+
+ /* clear EXISTS-flags, so after sync we know what can be expunged */
+ mailbox_list_index_node_clear_exists(sync_ctx->ilist->mailbox_tree);
+
+ /* don't include autocreated mailboxes in index until they're
+ actually created. this index may be used by multiple users, so
+ we also want to ignore ACLs here. */
+ patterns[0] = "*"; patterns[1] = NULL;
+ iter = sync_ctx->ilist->module_ctx.super.
+ iter_init(sync_ctx->list, patterns,
+ MAILBOX_LIST_ITER_RAW_LIST |
+ MAILBOX_LIST_ITER_NO_AUTO_BOXES);
+
+ sync_ctx->syncing_list = TRUE;
+ while ((info = sync_ctx->ilist->module_ctx.super.iter_next(iter)) != NULL) T_BEGIN {
+ flags = 0;
+ if ((info->flags & MAILBOX_NONEXISTENT) != 0)
+ flags |= MAILBOX_LIST_INDEX_FLAG_NONEXISTENT;
+ if ((info->flags & MAILBOX_NOSELECT) != 0)
+ flags |= MAILBOX_LIST_INDEX_FLAG_NOSELECT;
+ if ((info->flags & MAILBOX_NOINFERIORS) != 0)
+ flags |= MAILBOX_LIST_INDEX_FLAG_NOINFERIORS;
+
+ const char *name = mailbox_list_get_storage_name(info->ns->list,
+ info->vname);
+ if (strcmp(name, "INBOX") == 0 &&
+ strcmp(info->vname, "INBOX") != 0 &&
+ (info->ns->flags & NAMESPACE_FLAG_INBOX_USER) != 0) {
+ /* prefix/INBOX - don't override INBOX with this */
+ } else {
+ seq = mailbox_list_index_sync_name(sync_ctx, name,
+ &node, &created);
+ node->flags = flags | MAILBOX_LIST_INDEX_FLAG_SYNC_EXISTS;
+ mail_index_update_flags(sync_ctx->trans, seq,
+ MODIFY_REPLACE,
+ (enum mail_flags)flags);
+ }
+ } T_END;
+ sync_ctx->syncing_list = FALSE;
+
+ if (sync_ctx->ilist->module_ctx.super.iter_deinit(iter) < 0)
+ return -1;
+
+ /* successfully listed everything, expunge any unseen mailboxes */
+ sync_expunge_nonexistent(sync_ctx, sync_ctx->ilist->mailbox_tree);
+ return 0;
+}
+
+static void
+mailbox_list_index_sync_update_hdr(struct mailbox_list_index_sync_context *sync_ctx)
+{
+ if (sync_ctx->orig_highest_name_id != sync_ctx->ilist->highest_name_id ||
+ sync_ctx->ilist->corrupted_names_or_parents) {
+ /* new names added. this implicitly resets refresh flag */
+ T_BEGIN {
+ mailbox_list_index_sync_names(sync_ctx);
+ } T_END;
+ sync_ctx->ilist->corrupted_names_or_parents = FALSE;
+ } else if (mailbox_list_index_need_refresh(sync_ctx->ilist,
+ sync_ctx->view)) {
+ /* we're synced, reset refresh flag */
+ struct mailbox_list_index_header new_hdr;
+
+ new_hdr.refresh_flag = 0;
+ mail_index_update_header_ext(sync_ctx->trans, sync_ctx->ilist->ext_id,
+ offsetof(struct mailbox_list_index_header, refresh_flag),
+ &new_hdr.refresh_flag, sizeof(new_hdr.refresh_flag));
+ }
+}
+
+static void
+mailbox_list_index_sync_update_corrupted_node(struct mailbox_list_index_sync_context *sync_ctx,
+ struct mailbox_list_index_node *node)
+{
+ struct mailbox_list_index_record irec;
+ uint32_t seq;
+ const void *data;
+ bool expunged;
+
+ if (!mail_index_lookup_seq(sync_ctx->view, node->uid, &seq))
+ return;
+
+ if (node->corrupted_ext) {
+ mail_index_lookup_ext(sync_ctx->view, seq,
+ sync_ctx->ilist->ext_id,
+ &data, &expunged);
+ i_assert(data != NULL);
+
+ memcpy(&irec, data, sizeof(irec));
+ irec.name_id = node->name_id;
+ irec.parent_uid = node->parent == NULL ? 0 : node->parent->uid;
+ mail_index_update_ext(sync_ctx->trans, seq,
+ sync_ctx->ilist->ext_id, &irec, NULL);
+ node->corrupted_ext = FALSE;
+ }
+ if (node->corrupted_flags) {
+ mail_index_update_flags(sync_ctx->trans, seq, MODIFY_REPLACE,
+ (enum mail_flags)node->flags);
+ node->corrupted_flags = FALSE;
+ } else if ((node->flags & MAILBOX_LIST_INDEX_FLAG_CORRUPTED_NAME) != 0) {
+ /* rely on lib-index to drop unnecessary updates */
+ mail_index_update_flags(sync_ctx->trans, seq, MODIFY_ADD,
+ (enum mail_flags)MAILBOX_LIST_INDEX_FLAG_CORRUPTED_NAME);
+ }
+}
+
+static void
+mailbox_list_index_sync_update_corrupted_nodes(struct mailbox_list_index_sync_context *sync_ctx,
+ struct mailbox_list_index_node *node)
+{
+ for (; node != NULL; node = node->next) {
+ mailbox_list_index_sync_update_corrupted_node(sync_ctx, node);
+ mailbox_list_index_sync_update_corrupted_nodes(sync_ctx, node->children);
+ }
+}
+
+static void
+mailbox_list_index_sync_update_corrupted(struct mailbox_list_index_sync_context *sync_ctx)
+{
+ if (!sync_ctx->ilist->corrupted_names_or_parents)
+ return;
+
+ mailbox_list_index_sync_update_corrupted_nodes(sync_ctx,
+ sync_ctx->ilist->mailbox_tree);
+}
+
+int mailbox_list_index_sync_end(struct mailbox_list_index_sync_context **_sync_ctx,
+ bool success)
+{
+ struct mailbox_list_index_sync_context *sync_ctx = *_sync_ctx;
+ int ret;
+
+ *_sync_ctx = NULL;
+
+ if (success) {
+ mailbox_list_index_sync_update_corrupted(sync_ctx);
+ mailbox_list_index_sync_update_hdr(sync_ctx);
+ }
+ mail_index_view_close(&sync_ctx->view);
+
+ if (success) {
+ struct mail_index_sync_rec sync_rec;
+ while (mail_index_sync_next(sync_ctx->index_sync_ctx, &sync_rec)) ;
+ if ((ret = mail_index_sync_commit(&sync_ctx->index_sync_ctx)) < 0)
+ mailbox_list_index_set_index_error(sync_ctx->list);
+ } else {
+ mail_index_sync_rollback(&sync_ctx->index_sync_ctx);
+ ret = -1;
+ }
+ sync_ctx->ilist->syncing = FALSE;
+ sync_ctx->ilist->sync_ctx = NULL;
+ i_free(sync_ctx);
+ return ret;
+}
+
+int mailbox_list_index_sync(struct mailbox_list *list, bool refresh)
+{
+ struct mailbox_list_index_sync_context *sync_ctx;
+ int ret = 0;
+
+ if (mailbox_list_index_sync_begin(list, &sync_ctx) < 0)
+ return -1;
+
+ if (!sync_ctx->ilist->has_backing_store) {
+ /* no backing store - we have nothing to sync to */
+ } else if (refresh ||
+ sync_ctx->ilist->call_corruption_callback ||
+ sync_ctx->ilist->corrupted_names_or_parents ||
+ sync_ctx->ilist->highest_name_id == 0 ||
+ !sync_ctx->list->mail_set->mailbox_list_index_very_dirty_syncs) {
+ /* sync the index against the backing store */
+ ret = mailbox_list_index_sync_list(sync_ctx);
+ }
+ return mailbox_list_index_sync_end(&sync_ctx, ret == 0);
+}
+
+int mailbox_list_index_sync_delete(struct mailbox_list_index_sync_context *sync_ctx,
+ const char *name, bool delete_selectable)
+{
+ struct mailbox_list_index_record rec;
+ struct mailbox_list_index_node *node;
+ const void *data;
+ bool expunged;
+ uint32_t seq;
+
+ node = mailbox_list_index_lookup(sync_ctx->list, name);
+ if (node == NULL) {
+ mailbox_list_set_error(sync_ctx->list, MAIL_ERROR_NOTFOUND,
+ T_MAIL_ERR_MAILBOX_NOT_FOUND(name));
+ return -1;
+ }
+ if (!mail_index_lookup_seq(sync_ctx->view, node->uid, &seq))
+ i_panic("mailbox list index: lost uid=%u", node->uid);
+ if (delete_selectable) {
+ /* make it at least non-selectable */
+ node->flags = MAILBOX_LIST_INDEX_FLAG_NOSELECT;
+ mail_index_update_flags(sync_ctx->trans, seq, MODIFY_REPLACE,
+ (enum mail_flags)node->flags);
+
+ mail_index_lookup_ext(sync_ctx->view, seq,
+ sync_ctx->ilist->ext_id,
+ &data, &expunged);
+ i_assert(data != NULL && !expunged);
+ memcpy(&rec, data, sizeof(rec));
+ rec.uid_validity = 0;
+ i_zero(&rec.guid);
+ mail_index_update_ext(sync_ctx->trans, seq,
+ sync_ctx->ilist->ext_id, &rec, NULL);
+ }
+ if (node->children != NULL) {
+ /* can't delete this directory before its children,
+ but we may have made it non-selectable already */
+ return 0;
+ }
+
+ /* we can remove the entire node */
+ mail_index_expunge(sync_ctx->trans, seq);
+ mailbox_list_index_node_unlink(sync_ctx->ilist, node);
+ return 1;
+}
diff --git a/src/lib-storage/list/mailbox-list-index-sync.h b/src/lib-storage/list/mailbox-list-index-sync.h
new file mode 100644
index 0000000..db20244
--- /dev/null
+++ b/src/lib-storage/list/mailbox-list-index-sync.h
@@ -0,0 +1,35 @@
+#ifndef MAILBOX_LIST_INDEX_SYNC_H
+#define MAILBOX_LIST_INDEX_SYNC_H
+
+#include "mailbox-list-index.h"
+
+struct mailbox_list_index_sync_context {
+ struct mailbox_list *list;
+ struct mailbox_list_index *ilist;
+ char sep[2];
+ uint32_t next_uid;
+ uint32_t orig_highest_name_id;
+
+ struct mail_index_sync_ctx *index_sync_ctx;
+ struct mail_index_view *view;
+ struct mail_index_transaction *trans;
+
+ bool syncing_list:1;
+};
+
+int mailbox_list_index_sync_begin(struct mailbox_list *list,
+ struct mailbox_list_index_sync_context **sync_ctx_r);
+int mailbox_list_index_sync_end(struct mailbox_list_index_sync_context **_sync_ctx,
+ bool success);
+int mailbox_list_index_sync(struct mailbox_list *list, bool refresh);
+
+/* Add name to index, return seq in index. */
+uint32_t mailbox_list_index_sync_name(struct mailbox_list_index_sync_context *ctx,
+ const char *name,
+ struct mailbox_list_index_node **node_r,
+ bool *created_r);
+
+int mailbox_list_index_sync_delete(struct mailbox_list_index_sync_context *sync_ctx,
+ const char *name, bool delete_selectable);
+
+#endif
diff --git a/src/lib-storage/list/mailbox-list-index.c b/src/lib-storage/list/mailbox-list-index.c
new file mode 100644
index 0000000..2ba8861
--- /dev/null
+++ b/src/lib-storage/list/mailbox-list-index.c
@@ -0,0 +1,1168 @@
+/* Copyright (c) 2006-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "ioloop.h"
+#include "hash.h"
+#include "str.h"
+#include "mail-index-view-private.h"
+#include "mail-storage-hooks.h"
+#include "mail-storage-private.h"
+#include "mailbox-list-index-storage.h"
+#include "mailbox-list-index-sync.h"
+
+#define MAILBOX_LIST_INDEX_REFRESH_DELAY_MSECS 1000
+
+/* dovecot.list.index.log doesn't have to be kept for that long. */
+#define MAILBOX_LIST_INDEX_LOG_ROTATE_MIN_SIZE (8*1024)
+#define MAILBOX_LIST_INDEX_LOG_ROTATE_MAX_SIZE (64*1024)
+#define MAILBOX_LIST_INDEX_LOG_ROTATE_MIN_AGE_SECS (5*60)
+#define MAILBOX_LIST_INDEX_LOG2_MAX_AGE_SECS (10*60)
+
+static void mailbox_list_index_init_finish(struct mailbox_list *list);
+
+struct mailbox_list_index_module mailbox_list_index_module =
+ MODULE_CONTEXT_INIT(&mailbox_list_module_register);
+
+void mailbox_list_index_set_index_error(struct mailbox_list *list)
+{
+ struct mailbox_list_index *ilist = INDEX_LIST_CONTEXT_REQUIRE(list);
+
+ mailbox_list_set_internal_error(list);
+ mail_index_reset_error(ilist->index);
+}
+
+static void mailbox_list_index_init_pool(struct mailbox_list_index *ilist)
+{
+ ilist->mailbox_pool = pool_alloconly_create("mailbox list index", 4096);
+ hash_table_create_direct(&ilist->mailbox_names, ilist->mailbox_pool, 0);
+ hash_table_create_direct(&ilist->mailbox_hash, ilist->mailbox_pool, 0);
+}
+
+void mailbox_list_index_reset(struct mailbox_list_index *ilist)
+{
+ hash_table_destroy(&ilist->mailbox_names);
+ hash_table_destroy(&ilist->mailbox_hash);
+ pool_unref(&ilist->mailbox_pool);
+
+ ilist->mailbox_tree = NULL;
+ ilist->highest_name_id = 0;
+ ilist->sync_log_file_seq = 0;
+ ilist->sync_log_file_offset = 0;
+
+ mailbox_list_index_init_pool(ilist);
+}
+
+int mailbox_list_index_index_open(struct mailbox_list *list)
+{
+ struct mailbox_list_index *ilist = INDEX_LIST_CONTEXT_REQUIRE(list);
+ const struct mail_storage_settings *set = list->mail_set;
+ enum mail_index_open_flags index_flags;
+ unsigned int lock_timeout;
+
+ if (ilist->opened)
+ return 0;
+
+ if (mailbox_list_mkdir_missing_list_index_root(list) < 0)
+ return -1;
+
+ i_assert(ilist->index != NULL);
+
+ index_flags = mail_storage_settings_to_index_flags(set);
+ if (strcmp(list->name, MAILBOX_LIST_NAME_INDEX) == 0) {
+ /* LAYOUT=index. this is the only location for the mailbox
+ data, so we must never move it into memory. */
+ index_flags |= MAIL_INDEX_OPEN_FLAG_NEVER_IN_MEMORY;
+ }
+ lock_timeout = set->mail_max_lock_timeout == 0 ? UINT_MAX :
+ set->mail_max_lock_timeout;
+
+ if (!mail_index_use_existing_permissions(ilist->index)) {
+ struct mailbox_permissions perm;
+
+ mailbox_list_get_root_permissions(list, &perm);
+ mail_index_set_permissions(ilist->index, perm.file_create_mode,
+ perm.file_create_gid,
+ perm.file_create_gid_origin);
+ }
+ const struct mail_index_optimization_settings optimize_set = {
+ .log = {
+ .min_size = MAILBOX_LIST_INDEX_LOG_ROTATE_MIN_SIZE,
+ .max_size = MAILBOX_LIST_INDEX_LOG_ROTATE_MAX_SIZE,
+ .min_age_secs = MAILBOX_LIST_INDEX_LOG_ROTATE_MIN_AGE_SECS,
+ .log2_max_age_secs = MAILBOX_LIST_INDEX_LOG2_MAX_AGE_SECS,
+ },
+ };
+ mail_index_set_optimization_settings(ilist->index, &optimize_set);
+
+ mail_index_set_fsync_mode(ilist->index, set->parsed_fsync_mode, 0);
+ mail_index_set_lock_method(ilist->index, set->parsed_lock_method,
+ lock_timeout);
+ if (mail_index_open_or_create(ilist->index, index_flags) < 0) {
+ if (mail_index_move_to_memory(ilist->index) < 0) {
+ /* try opening once more. it should be created
+ directly into memory now, except if it fails with
+ LAYOUT=index backend. */
+ if (mail_index_open_or_create(ilist->index,
+ index_flags) < 0) {
+ mailbox_list_set_internal_error(list);
+ return -1;
+ }
+ }
+ }
+ ilist->opened = TRUE;
+ return 0;
+}
+
+struct mailbox_list_index_node *
+mailbox_list_index_node_find_sibling(const struct mailbox_list *list,
+ struct mailbox_list_index_node *node,
+ const char *name)
+{
+ mailbox_list_name_unescape(&name, list->set.storage_name_escape_char);
+
+ while (node != NULL) {
+ if (strcmp(node->raw_name, name) == 0)
+ return node;
+ node = node->next;
+ }
+ return NULL;
+}
+
+static struct mailbox_list_index_node *
+mailbox_list_index_lookup_real(struct mailbox_list *list, const char *name)
+{
+ struct mailbox_list_index *ilist = INDEX_LIST_CONTEXT_REQUIRE(list);
+ struct mailbox_list_index_node *node = ilist->mailbox_tree;
+ const char *const *path;
+ unsigned int i;
+ char sep[2];
+
+ if (*name == '\0')
+ return mailbox_list_index_node_find_sibling(list, node, "");
+
+ sep[0] = mailbox_list_get_hierarchy_sep(list); sep[1] = '\0';
+ path = t_strsplit(name, sep);
+ for (i = 0;; i++) {
+ node = mailbox_list_index_node_find_sibling(list, node, path[i]);
+ if (node == NULL || path[i+1] == NULL)
+ break;
+ node = node->children;
+ }
+ return node;
+}
+
+struct mailbox_list_index_node *
+mailbox_list_index_lookup(struct mailbox_list *list, const char *name)
+{
+ struct mailbox_list_index_node *node;
+
+ T_BEGIN {
+ node = mailbox_list_index_lookup_real(list, name);
+ } T_END;
+ return node;
+}
+
+struct mailbox_list_index_node *
+mailbox_list_index_lookup_uid(struct mailbox_list_index *ilist, uint32_t uid)
+{
+ return hash_table_lookup(ilist->mailbox_hash, POINTER_CAST(uid));
+}
+
+void mailbox_list_index_node_get_path(const struct mailbox_list_index_node *node,
+ char sep, string_t *str)
+{
+ if (node->parent != NULL) {
+ mailbox_list_index_node_get_path(node->parent, sep, str);
+ str_append_c(str, sep);
+ }
+ str_append(str, node->raw_name);
+}
+
+void mailbox_list_index_node_unlink(struct mailbox_list_index *ilist,
+ struct mailbox_list_index_node *node)
+{
+ struct mailbox_list_index_node **prev;
+
+ prev = node->parent == NULL ?
+ &ilist->mailbox_tree : &node->parent->children;
+
+ while (*prev != node)
+ prev = &(*prev)->next;
+ *prev = node->next;
+}
+
+static int mailbox_list_index_parse_header(struct mailbox_list_index *ilist,
+ struct mail_index_view *view)
+{
+ const void *data, *name_start, *p;
+ size_t i, len, size;
+ uint32_t id, prev_id = 0;
+ string_t *str;
+ char *name;
+ int ret = 0;
+
+ mail_index_map_get_header_ext(view, view->map, ilist->ext_id, &data, &size);
+ if (size == 0)
+ return 0;
+
+ str = t_str_new(128);
+ for (i = sizeof(struct mailbox_list_index_header); i < size; ) {
+ /* get id */
+ if (i + sizeof(id) > size)
+ return -1;
+ memcpy(&id, CONST_PTR_OFFSET(data, i), sizeof(id));
+ i += sizeof(id);
+
+ if (id <= prev_id) {
+ /* allow extra space in the end as long as last id=0 */
+ return id == 0 ? 0 : -1;
+ }
+ prev_id = id;
+
+ /* get name */
+ p = memchr(CONST_PTR_OFFSET(data, i), '\0', size-i);
+ if (p == NULL)
+ return -1;
+ name_start = CONST_PTR_OFFSET(data, i);
+ len = (const char *)p - (const char *)name_start;
+
+ if (uni_utf8_get_valid_data(name_start, len, str)) {
+ name = p_strndup(ilist->mailbox_pool, name_start, len);
+ } else {
+ /* corrupted index. fix the name. */
+ name = p_strdup(ilist->mailbox_pool, str_c(str));
+ str_truncate(str, 0);
+ ret = -1;
+ }
+
+ i += len + 1;
+
+ /* add id => name to hash table */
+ hash_table_insert(ilist->mailbox_names, POINTER_CAST(id), name);
+ ilist->highest_name_id = id;
+ }
+ i_assert(i == size);
+ return ret;
+}
+
+static void
+mailbox_list_index_generate_name(struct mailbox_list_index *ilist,
+ struct mailbox_list_index_node *node,
+ const char *prefix)
+{
+ guid_128_t guid;
+ char *name;
+
+ i_assert(node->name_id != 0);
+
+ guid_128_generate(guid);
+ name = p_strdup_printf(ilist->mailbox_pool, "%s%s", prefix,
+ guid_128_to_string(guid));
+ node->raw_name = name;
+ node->flags |= MAILBOX_LIST_INDEX_FLAG_CORRUPTED_NAME;
+
+ hash_table_insert(ilist->mailbox_names,
+ POINTER_CAST(node->name_id), name);
+ if (ilist->highest_name_id < node->name_id)
+ ilist->highest_name_id = node->name_id;
+}
+
+static int mailbox_list_index_node_cmp(const struct mailbox_list_index_node *n1,
+ const struct mailbox_list_index_node *n2)
+{
+ return n1->parent == n2->parent &&
+ strcmp(n1->raw_name, n2->raw_name) == 0 ? 0 : -1;
+}
+
+static unsigned int
+mailbox_list_index_node_hash(const struct mailbox_list_index_node *node)
+{
+ return str_hash(node->raw_name) ^
+ POINTER_CAST_TO(node->parent, unsigned int);
+}
+
+static bool node_has_parent(const struct mailbox_list_index_node *parent,
+ const struct mailbox_list_index_node *node)
+{
+ const struct mailbox_list_index_node *n;
+
+ for (n = parent; n != NULL; n = n->parent) {
+ if (n == node)
+ return TRUE;
+ }
+ return FALSE;
+}
+
+static int mailbox_list_index_parse_records(struct mailbox_list_index *ilist,
+ struct mail_index_view *view,
+ const char **error_r)
+{
+ struct mailbox_list_index_node *node, *parent;
+ HASH_TABLE(struct mailbox_list_index_node *,
+ struct mailbox_list_index_node *) duplicate_hash;
+ const struct mail_index_record *rec;
+ const struct mailbox_list_index_record *irec;
+ const void *data;
+ bool expunged;
+ uint32_t seq, uid, count;
+ HASH_TABLE(uint8_t *, struct mailbox_list_index_node *) duplicate_guid;
+
+ *error_r = NULL;
+
+ pool_t dup_pool =
+ pool_alloconly_create(MEMPOOL_GROWING"duplicate pool", 2048);
+ hash_table_create(&duplicate_hash, dup_pool, 0,
+ mailbox_list_index_node_hash,
+ mailbox_list_index_node_cmp);
+ count = mail_index_view_get_messages_count(view);
+ if (!ilist->has_backing_store)
+ hash_table_create(&duplicate_guid, dup_pool, 0, guid_128_hash,
+ guid_128_cmp);
+
+ for (seq = 1; seq <= count; seq++) {
+ node = p_new(ilist->mailbox_pool,
+ struct mailbox_list_index_node, 1);
+ rec = mail_index_lookup(view, seq);
+ node->uid = rec->uid;
+ node->flags = rec->flags;
+
+ mail_index_lookup_ext(view, seq, ilist->ext_id,
+ &data, &expunged);
+ if (data == NULL) {
+ *error_r = "Missing list extension data";
+ /* list index is missing, no point trying
+ to do second scan either */
+ count = 0;
+ break;
+ }
+ irec = data;
+
+ node->name_id = irec->name_id;
+ if (node->name_id == 0) {
+ /* invalid name_id - assign a new one */
+ node->name_id = ++ilist->highest_name_id;
+ node->corrupted_ext = TRUE;
+ }
+ node->raw_name = hash_table_lookup(ilist->mailbox_names,
+ POINTER_CAST(irec->name_id));
+ if (node->raw_name == NULL) {
+ *error_r = t_strdup_printf(
+ "name_id=%u not in index header", irec->name_id);
+ if (ilist->has_backing_store)
+ break;
+ /* generate a new name and use it */
+ mailbox_list_index_generate_name(ilist, node, "unknown-");
+ }
+
+ if (!ilist->has_backing_store && guid_128_is_empty(irec->guid) &&
+ (rec->flags & (MAILBOX_LIST_INDEX_FLAG_NONEXISTENT |
+ MAILBOX_LIST_INDEX_FLAG_NOSELECT)) == 0) {
+ /* no backing store and mailbox has no GUID.
+ it can't be selectable, but the flag is missing. */
+ node->flags |= MAILBOX_LIST_INDEX_FLAG_NOSELECT;
+ *error_r = t_strdup_printf(
+ "mailbox '%s' (uid=%u) is missing GUID - "
+ "marking it non-selectable", node->raw_name, node->uid);
+ node->corrupted_flags = TRUE;
+ }
+ if (!ilist->has_backing_store && !guid_128_is_empty(irec->guid) &&
+ (rec->flags & (MAILBOX_LIST_INDEX_FLAG_NONEXISTENT |
+ MAILBOX_LIST_INDEX_FLAG_NOSELECT)) != 0) {
+ node->flags &= ENUM_NEGATE(MAILBOX_LIST_INDEX_FLAG_NONEXISTENT | MAILBOX_LIST_INDEX_FLAG_NOSELECT);
+ *error_r = t_strdup_printf(
+ "non-selectable mailbox '%s' (uid=%u) already has GUID - "
+ "marking it selectable", node->raw_name, node->uid);
+ node->corrupted_flags = TRUE;
+ }
+
+ if (!ilist->has_backing_store && !guid_128_is_empty(irec->guid)) {
+ struct mailbox_list_index_node *dup_node;
+ uint8_t *guid_p = p_memdup(dup_pool, irec->guid,
+ sizeof(guid_128_t));
+ if ((dup_node = hash_table_lookup(duplicate_guid, guid_p)) != NULL) {
+ *error_r = t_strdup_printf(
+ "duplicate GUID %s for mailbox '%s' and '%s'",
+ guid_128_to_string(guid_p),
+ node->raw_name,
+ dup_node->raw_name);
+ node->corrupted_ext = TRUE;
+ ilist->corrupted_names_or_parents = TRUE;
+ ilist->call_corruption_callback = TRUE;
+ } else {
+ hash_table_insert(duplicate_guid, guid_p, node);
+ }
+ }
+
+ hash_table_insert(ilist->mailbox_hash,
+ POINTER_CAST(node->uid), node);
+ }
+
+ /* do a second scan to create the actual mailbox tree hierarchy.
+ this is needed because the parent_uid may be smaller or higher than
+ the current node's uid */
+ if (*error_r != NULL && ilist->has_backing_store)
+ count = 0;
+ for (seq = 1; seq <= count; seq++) {
+ mail_index_lookup_uid(view, seq, &uid);
+ mail_index_lookup_ext(view, seq, ilist->ext_id,
+ &data, &expunged);
+ irec = data;
+
+ node = mailbox_list_index_lookup_uid(ilist, uid);
+ i_assert(node != NULL);
+
+ if (irec->parent_uid != 0) {
+ /* node should have a parent */
+ parent = mailbox_list_index_lookup_uid(ilist,
+ irec->parent_uid);
+ if (parent == NULL) {
+ *error_r = t_strdup_printf(
+ "parent_uid=%u points to nonexistent record",
+ irec->parent_uid);
+ if (ilist->has_backing_store)
+ break;
+ /* just place it under the root */
+ node->corrupted_ext = TRUE;
+ } else if (node_has_parent(parent, node)) {
+ *error_r = t_strdup_printf(
+ "parent_uid=%u loops to node itself (%s)",
+ uid, node->raw_name);
+ if (ilist->has_backing_store)
+ break;
+ /* just place it under the root */
+ node->corrupted_ext = TRUE;
+ } else {
+ node->parent = parent;
+ node->next = parent->children;
+ parent->children = node;
+ }
+ } else if (strcasecmp(node->raw_name, "INBOX") == 0) {
+ ilist->rebuild_on_missing_inbox = FALSE;
+ }
+ if (hash_table_lookup(duplicate_hash, node) == NULL)
+ hash_table_insert(duplicate_hash, node, node);
+ else {
+ const char *old_name = node->raw_name;
+
+ if (ilist->has_backing_store) {
+ *error_r = t_strdup_printf(
+ "Duplicate mailbox '%s' in index",
+ node->raw_name);
+ break;
+ }
+
+ /* we have only the mailbox list index and this node
+ may have a different GUID, so rename it. */
+ node->corrupted_ext = TRUE;
+ node->name_id = ++ilist->highest_name_id;
+ mailbox_list_index_generate_name(ilist, node,
+ t_strconcat(node->raw_name, "-duplicate-", NULL));
+ *error_r = t_strdup_printf(
+ "Duplicate mailbox '%s' in index, renaming to %s",
+ old_name, node->raw_name);
+ }
+ if (node->parent == NULL) {
+ node->next = ilist->mailbox_tree;
+ ilist->mailbox_tree = node;
+ }
+ }
+ hash_table_destroy(&duplicate_hash);
+ if (!ilist->has_backing_store)
+ hash_table_destroy(&duplicate_guid);
+ pool_unref(&dup_pool);
+ return *error_r == NULL ? 0 : -1;
+}
+
+int mailbox_list_index_parse(struct mailbox_list *list,
+ struct mail_index_view *view, bool force)
+{
+ struct mailbox_list_index *ilist = INDEX_LIST_CONTEXT_REQUIRE(list);
+ const struct mail_index_header *hdr;
+ const char *error;
+
+ hdr = mail_index_get_header(view);
+ if (!force &&
+ hdr->log_file_seq == ilist->sync_log_file_seq &&
+ hdr->log_file_head_offset == ilist->sync_log_file_offset) {
+ /* nothing changed */
+ return 0;
+ }
+ if ((hdr->flags & MAIL_INDEX_HDR_FLAG_FSCKD) != 0) {
+ mailbox_list_set_critical(list,
+ "Mailbox list index was marked as fsck'd %s", ilist->path);
+ ilist->call_corruption_callback = TRUE;
+ }
+
+ mailbox_list_index_reset(ilist);
+ ilist->sync_log_file_seq = hdr->log_file_seq;
+ ilist->sync_log_file_offset = hdr->log_file_head_offset;
+
+ if (mailbox_list_index_parse_header(ilist, view) < 0) {
+ mailbox_list_set_critical(list,
+ "Corrupted mailbox list index header %s", ilist->path);
+ if (ilist->has_backing_store) {
+ mail_index_mark_corrupted(ilist->index);
+ return -1;
+ }
+ ilist->call_corruption_callback = TRUE;
+ ilist->corrupted_names_or_parents = TRUE;
+ }
+ if (mailbox_list_index_parse_records(ilist, view, &error) < 0) {
+ mailbox_list_set_critical(list,
+ "Corrupted mailbox list index %s: %s",
+ ilist->path, error);
+ if (ilist->has_backing_store) {
+ mail_index_mark_corrupted(ilist->index);
+ return -1;
+ }
+ ilist->call_corruption_callback = TRUE;
+ ilist->corrupted_names_or_parents = TRUE;
+ }
+ return 0;
+}
+
+bool mailbox_list_index_need_refresh(struct mailbox_list_index *ilist,
+ struct mail_index_view *view)
+{
+ const struct mailbox_list_index_header *hdr;
+ const void *data;
+ size_t size;
+
+ if (!ilist->has_backing_store)
+ return FALSE;
+
+ mail_index_get_header_ext(view, ilist->ext_id, &data, &size);
+ hdr = data;
+ return hdr != NULL && hdr->refresh_flag != 0;
+}
+
+int mailbox_list_index_refresh(struct mailbox_list *list)
+{
+ struct mailbox_list_index *ilist = INDEX_LIST_CONTEXT_REQUIRE(list);
+
+ if (ilist->syncing)
+ return 0;
+ if (ilist->last_refresh_timeval.tv_usec == ioloop_timeval.tv_usec &&
+ ilist->last_refresh_timeval.tv_sec == ioloop_timeval.tv_sec) {
+ /* we haven't been to ioloop since last refresh, skip checking
+ it. when we're accessing many mailboxes at once (e.g.
+ opening a virtual mailbox) we don't want to stat/read the
+ index every single time. */
+ return 0;
+ }
+
+ return mailbox_list_index_refresh_force(list);
+}
+
+int mailbox_list_index_refresh_force(struct mailbox_list *list)
+{
+ struct mailbox_list_index *ilist = INDEX_LIST_CONTEXT_REQUIRE(list);
+ struct mail_index_view *view;
+ int ret;
+ bool refresh;
+
+ i_assert(!ilist->syncing);
+
+ ilist->last_refresh_timeval = ioloop_timeval;
+ if (mailbox_list_index_index_open(list) < 0)
+ return -1;
+ if (mail_index_refresh(ilist->index) < 0) {
+ mailbox_list_index_set_index_error(list);
+ return -1;
+ }
+
+ view = mail_index_view_open(ilist->index);
+ if ((refresh = mailbox_list_index_need_refresh(ilist, view)) ||
+ ilist->mailbox_tree == NULL) {
+ /* refresh list of mailboxes */
+ ret = mailbox_list_index_sync(list, refresh);
+ } else {
+ ret = mailbox_list_index_parse(list, view, FALSE);
+ }
+ mail_index_view_close(&view);
+
+ if (mailbox_list_index_handle_corruption(list) < 0) {
+ const char *errstr;
+ enum mail_error error;
+
+ errstr = mailbox_list_get_last_internal_error(list, &error);
+ mailbox_list_set_error(list, error, t_strdup_printf(
+ "Failed to rebuild mailbox list index: %s", errstr));
+ ret = -1;
+ }
+ return ret;
+}
+
+static void mailbox_list_index_refresh_timeout(struct mailbox_list *list)
+{
+ struct mailbox_list_index *ilist = INDEX_LIST_CONTEXT_REQUIRE(list);
+
+ timeout_remove(&ilist->to_refresh);
+ (void)mailbox_list_index_refresh(list);
+}
+
+void mailbox_list_index_refresh_later(struct mailbox_list *list)
+{
+ struct mailbox_list_index *ilist = INDEX_LIST_CONTEXT_REQUIRE(list);
+ struct mailbox_list_index_header new_hdr;
+ struct mail_index_view *view;
+ struct mail_index_transaction *trans;
+
+ memset(&ilist->last_refresh_timeval, 0,
+ sizeof(ilist->last_refresh_timeval));
+
+ if (!ilist->has_backing_store)
+ return;
+
+ (void)mailbox_list_index_index_open(list);
+
+ view = mail_index_view_open(ilist->index);
+ if (!mailbox_list_index_need_refresh(ilist, view)) {
+ new_hdr.refresh_flag = 1;
+
+ trans = mail_index_transaction_begin(view,
+ MAIL_INDEX_TRANSACTION_FLAG_EXTERNAL);
+ mail_index_update_header_ext(trans, ilist->ext_id,
+ offsetof(struct mailbox_list_index_header, refresh_flag),
+ &new_hdr.refresh_flag, sizeof(new_hdr.refresh_flag));
+ if (mail_index_transaction_commit(&trans) < 0)
+ mail_index_mark_corrupted(ilist->index);
+
+ }
+ mail_index_view_close(&view);
+
+ if (ilist->to_refresh == NULL) {
+ ilist->to_refresh =
+ timeout_add(MAILBOX_LIST_INDEX_REFRESH_DELAY_MSECS,
+ mailbox_list_index_refresh_timeout, list);
+ }
+}
+
+static int
+list_handle_corruption_locked(struct mailbox_list *list,
+ enum mail_storage_list_index_rebuild_reason reason)
+{
+ struct mail_storage *storage;
+ const char *errstr;
+ enum mail_error error;
+
+ array_foreach_elem(&list->ns->all_storages, storage) {
+ if (storage->v.list_index_rebuild == NULL)
+ continue;
+
+ if (storage->v.list_index_rebuild(storage, reason) < 0) {
+ errstr = mail_storage_get_last_internal_error(storage, &error);
+ mailbox_list_set_error(list, error, errstr);
+ return -1;
+ } else {
+ /* FIXME: implement a generic handler that
+ just lists mailbox directories in filesystem
+ and adds the missing ones to the index. */
+ }
+ }
+ return mailbox_list_index_set_uncorrupted(list);
+}
+
+int mailbox_list_index_handle_corruption(struct mailbox_list *list)
+{
+ struct mailbox_list_index *ilist = INDEX_LIST_CONTEXT_REQUIRE(list);
+ enum mail_storage_list_index_rebuild_reason reason;
+ int ret;
+
+ if (ilist->call_corruption_callback)
+ reason = MAIL_STORAGE_LIST_INDEX_REBUILD_REASON_CORRUPTED;
+ else if (ilist->rebuild_on_missing_inbox)
+ reason = MAIL_STORAGE_LIST_INDEX_REBUILD_REASON_NO_INBOX;
+ else
+ return 0;
+
+ if (list->disable_rebuild_on_corruption)
+ return 0;
+
+ /* make sure we don't recurse */
+ if (ilist->handling_corruption)
+ return 0;
+ ilist->handling_corruption = TRUE;
+
+ /* Perform the rebuilding locked. Note that if we're here because
+ INBOX wasn't found, this may be because another process is in the
+ middle of creating it. Waiting for the lock here makes sure that
+ we don't start rebuilding before it's finished. In that case the
+ rebuild is a bit unnecessary, but harmless (and avoiding the rebuild
+ just adds extra code complexity). */
+ if (mailbox_list_lock(list) < 0)
+ ret = -1;
+ else {
+ ret = list_handle_corruption_locked(list, reason);
+ mailbox_list_unlock(list);
+ }
+ ilist->handling_corruption = FALSE;
+ return ret;
+}
+
+int mailbox_list_index_set_uncorrupted(struct mailbox_list *list)
+{
+ struct mailbox_list_index *ilist = INDEX_LIST_CONTEXT_REQUIRE(list);
+ struct mailbox_list_index_sync_context *sync_ctx;
+
+ ilist->call_corruption_callback = FALSE;
+ ilist->rebuild_on_missing_inbox = FALSE;
+
+ if (mailbox_list_index_sync_begin(list, &sync_ctx) < 0)
+ return -1;
+
+ mail_index_unset_fscked(sync_ctx->trans);
+ return mailbox_list_index_sync_end(&sync_ctx, TRUE);
+}
+
+bool mailbox_list_index_get_index(struct mailbox_list *list,
+ struct mail_index **index_r)
+{
+ struct mailbox_list_index *ilist = INDEX_LIST_CONTEXT(list);
+
+ if (ilist == NULL)
+ return FALSE;
+ *index_r = ilist->index;
+ return TRUE;
+}
+
+int mailbox_list_index_view_open(struct mailbox *box, bool require_refreshed,
+ struct mail_index_view **view_r,
+ uint32_t *seq_r)
+{
+ struct mailbox_list_index *ilist = INDEX_LIST_CONTEXT(box->list);
+ struct mailbox_list_index_node *node;
+ struct mail_index_view *view;
+ const char *reason = NULL;
+ uint32_t seq;
+ int ret;
+
+ if (ilist == NULL) {
+ /* mailbox list indexes aren't enabled */
+ return 0;
+ }
+ if (MAILBOX_IS_NEVER_IN_INDEX(box) && require_refreshed) {
+ /* Optimization: Caller wants the list index to be up-to-date
+ for this mailbox, but this mailbox isn't updated to the list
+ index at all. */
+ return 0;
+ }
+ if (mailbox_list_index_refresh(box->list) < 0) {
+ mail_storage_copy_list_error(box->storage, box->list);
+ return -1;
+ }
+
+ node = mailbox_list_index_lookup(box->list, box->name);
+ if (node == NULL) {
+ /* mailbox not found */
+ e_debug(box->event, "Couldn't open mailbox in list index: "
+ "Mailbox not found");
+ return 0;
+ }
+
+ view = mail_index_view_open(ilist->index);
+ if (mailbox_list_index_need_refresh(ilist, view)) {
+ /* mailbox_list_index_refresh_later() was called.
+ Can't trust the index's contents. */
+ reason = "Refresh-flag set";
+ ret = 1;
+ } else if (!mail_index_lookup_seq(view, node->uid, &seq)) {
+ /* our in-memory tree is out of sync */
+ ret = 1;
+ reason = "Mailbox no longer exists in index";
+ } else if (!require_refreshed) {
+ /* this operation doesn't need the index to be up-to-date */
+ ret = 0;
+ } else {
+ ret = box->v.list_index_has_changed == NULL ? 0 :
+ box->v.list_index_has_changed(box, view, seq, FALSE,
+ &reason);
+ i_assert(ret <= 0 || reason != NULL);
+ }
+
+ if (ret != 0) {
+ /* error / mailbox has changed. we'll need to sync it. */
+ if (ret < 0)
+ mailbox_list_index_refresh_later(box->list);
+ else {
+ i_assert(reason != NULL);
+ e_debug(box->event,
+ "Couldn't open mailbox in list index: %s",
+ reason);
+ ilist->index_last_check_changed = TRUE;
+ }
+ mail_index_view_close(&view);
+ return ret < 0 ? -1 : 0;
+ }
+
+ *view_r = view;
+ *seq_r = seq;
+ return 1;
+}
+
+static void mailbox_list_index_deinit(struct mailbox_list *list)
+{
+ struct mailbox_list_index *ilist = INDEX_LIST_CONTEXT_REQUIRE(list);
+
+ timeout_remove(&ilist->to_refresh);
+ if (ilist->index != NULL) {
+ hash_table_destroy(&ilist->mailbox_hash);
+ hash_table_destroy(&ilist->mailbox_names);
+ pool_unref(&ilist->mailbox_pool);
+ if (ilist->opened)
+ mail_index_close(ilist->index);
+ mail_index_free(&ilist->index);
+ }
+ ilist->module_ctx.super.deinit(list);
+}
+
+static void
+mailbox_list_index_refresh_if_found(struct mailbox_list *list,
+ const char *name, bool selectable)
+{
+ struct mailbox_list_index *ilist = INDEX_LIST_CONTEXT_REQUIRE(list);
+ struct mailbox_list_index_node *node;
+
+ if (ilist->syncing)
+ return;
+
+ mailbox_list_last_error_push(list);
+ (void)mailbox_list_index_refresh_force(list);
+ node = mailbox_list_index_lookup(list, name);
+ if (node != NULL &&
+ (!selectable ||
+ (node->flags & (MAILBOX_LIST_INDEX_FLAG_NONEXISTENT |
+ MAILBOX_LIST_INDEX_FLAG_NOSELECT)) == 0)) {
+ /* index is out of sync - refresh */
+ mailbox_list_index_refresh_later(list);
+ }
+ mailbox_list_last_error_pop(list);
+}
+
+static void mailbox_list_index_refresh_if_not_found(struct mailbox_list *list,
+ const char *name)
+{
+ struct mailbox_list_index *ilist = INDEX_LIST_CONTEXT_REQUIRE(list);
+
+ if (ilist->syncing)
+ return;
+
+ mailbox_list_last_error_push(list);
+ (void)mailbox_list_index_refresh_force(list);
+ if (mailbox_list_index_lookup(list, name) == NULL) {
+ /* index is out of sync - refresh */
+ mailbox_list_index_refresh_later(list);
+ }
+ mailbox_list_last_error_pop(list);
+}
+
+static int mailbox_list_index_open_mailbox(struct mailbox *box)
+{
+ struct index_list_mailbox *ibox = INDEX_LIST_STORAGE_CONTEXT(box);
+
+ if (ibox->module_ctx.super.open(box) < 0) {
+ if (mailbox_get_last_mail_error(box) == MAIL_ERROR_NOTFOUND)
+ mailbox_list_index_refresh_if_found(box->list, box->name, TRUE);
+ return -1;
+ }
+ return 0;
+}
+
+static int
+mailbox_list_index_create_mailbox(struct mailbox *box,
+ const struct mailbox_update *update,
+ bool directory)
+{
+ struct index_list_mailbox *ibox = INDEX_LIST_STORAGE_CONTEXT(box);
+
+ if (ibox->module_ctx.super.create_box(box, update, directory) < 0) {
+ if (mailbox_get_last_mail_error(box) == MAIL_ERROR_EXISTS)
+ mailbox_list_index_refresh_if_not_found(box->list, box->name);
+ return -1;
+ }
+ mailbox_list_index_refresh_later(box->list);
+ return 0;
+}
+
+static int
+mailbox_list_index_update_mailbox(struct mailbox *box,
+ const struct mailbox_update *update)
+{
+ struct index_list_mailbox *ibox = INDEX_LIST_STORAGE_CONTEXT(box);
+
+ if (ibox->module_ctx.super.update_box(box, update) < 0) {
+ if (mailbox_get_last_mail_error(box) == MAIL_ERROR_NOTFOUND)
+ mailbox_list_index_refresh_if_found(box->list, box->name, TRUE);
+ return -1;
+ }
+
+ mailbox_list_index_update_mailbox_index(box, update);
+ return 0;
+}
+
+static int
+mailbox_list_index_delete_mailbox(struct mailbox_list *list, const char *name)
+{
+ struct mailbox_list_index *ilist = INDEX_LIST_CONTEXT_REQUIRE(list);
+
+ if (ilist->module_ctx.super.delete_mailbox(list, name) < 0) {
+ if (mailbox_list_get_last_mail_error(list) == MAIL_ERROR_NOTFOUND)
+ mailbox_list_index_refresh_if_found(list, name, FALSE);
+ return -1;
+ }
+ mailbox_list_index_refresh_later(list);
+ return 0;
+}
+
+static int
+mailbox_list_index_delete_dir(struct mailbox_list *list, const char *name)
+{
+ struct mailbox_list_index *ilist = INDEX_LIST_CONTEXT_REQUIRE(list);
+
+ if (ilist->module_ctx.super.delete_dir(list, name) < 0) {
+ if (mailbox_list_get_last_mail_error(list) == MAIL_ERROR_NOTFOUND)
+ mailbox_list_index_refresh_if_found(list, name, FALSE);
+ return -1;
+ }
+ mailbox_list_index_refresh_later(list);
+ return 0;
+}
+
+static int
+mailbox_list_index_rename_mailbox(struct mailbox_list *oldlist,
+ const char *oldname,
+ struct mailbox_list *newlist,
+ const char *newname)
+{
+ struct mailbox_list_index *oldilist = INDEX_LIST_CONTEXT_REQUIRE(oldlist);
+
+ if (oldilist->module_ctx.super.rename_mailbox(oldlist, oldname,
+ newlist, newname) < 0) {
+ if (mailbox_list_get_last_mail_error(oldlist) == MAIL_ERROR_NOTFOUND)
+ mailbox_list_index_refresh_if_found(oldlist, oldname, FALSE);
+ if (mailbox_list_get_last_mail_error(newlist) == MAIL_ERROR_EXISTS)
+ mailbox_list_index_refresh_if_not_found(newlist, newname);
+ return -1;
+ }
+ mailbox_list_index_refresh_later(oldlist);
+ if (oldlist != newlist)
+ mailbox_list_index_refresh_later(newlist);
+ return 0;
+}
+
+static int
+mailbox_list_index_set_subscribed(struct mailbox_list *_list,
+ const char *name, bool set)
+{
+ struct mailbox_list_index *ilist = INDEX_LIST_CONTEXT_REQUIRE(_list);
+ struct mail_index_view *view;
+ struct mail_index_transaction *trans;
+ const void *data;
+ size_t size;
+ uint32_t counter;
+
+ if (ilist->module_ctx.super.set_subscribed(_list, name, set) < 0)
+ return -1;
+
+ /* update the "subscriptions changed" counter/timestamp. its purpose
+ is to trigger NOTIFY watcher to handle SubscriptionChange events */
+ if (mailbox_list_index_index_open(_list) < 0)
+ return -1;
+ view = mail_index_view_open(ilist->index);
+ mail_index_get_header_ext(view, ilist->subs_hdr_ext_id, &data, &size);
+ if (size != sizeof(counter))
+ counter = ioloop_time;
+ else {
+ memcpy(&counter, data, size);
+ if (++counter < (uint32_t)ioloop_time)
+ counter = ioloop_time;
+ }
+
+ trans = mail_index_transaction_begin(view,
+ MAIL_INDEX_TRANSACTION_FLAG_EXTERNAL);
+ mail_index_update_header_ext(trans, ilist->subs_hdr_ext_id,
+ 0, &counter, sizeof(counter));
+ (void)mail_index_transaction_commit(&trans);
+ mail_index_view_close(&view);
+ return 0;
+}
+
+static bool mailbox_list_index_is_enabled(struct mailbox_list *list)
+{
+ if (!list->mail_set->mailbox_list_index ||
+ (list->props & MAILBOX_LIST_PROP_NO_LIST_INDEX) != 0)
+ return FALSE;
+
+ i_assert(list->set.list_index_fname != NULL);
+ if (list->set.list_index_fname[0] == '\0')
+ return FALSE;
+ return TRUE;
+}
+
+static void mailbox_list_index_created(struct mailbox_list *list)
+{
+ struct mailbox_list_vfuncs *v = list->vlast;
+ struct mailbox_list_index *ilist;
+ bool has_backing_store;
+
+ /* layout=index doesn't have any backing store */
+ has_backing_store = strcmp(list->name, MAILBOX_LIST_NAME_INDEX) != 0;
+
+ if (!mailbox_list_index_is_enabled(list)) {
+ /* reserve the module context anyway, so syncing code knows
+ that the index is disabled */
+ i_assert(has_backing_store);
+ ilist = NULL;
+ MODULE_CONTEXT_SET(list, mailbox_list_index_module, ilist);
+ return;
+ }
+
+ ilist = p_new(list->pool, struct mailbox_list_index, 1);
+ ilist->module_ctx.super = *v;
+ list->vlast = &ilist->module_ctx.super;
+ ilist->has_backing_store = has_backing_store;
+ ilist->pending_init = TRUE;
+
+ v->deinit = mailbox_list_index_deinit;
+ v->iter_init = mailbox_list_index_iter_init;
+ v->iter_deinit = mailbox_list_index_iter_deinit;
+ v->iter_next = mailbox_list_index_iter_next;
+
+ v->delete_mailbox = mailbox_list_index_delete_mailbox;
+ v->delete_dir = mailbox_list_index_delete_dir;
+ v->rename_mailbox = mailbox_list_index_rename_mailbox;
+ v->set_subscribed = mailbox_list_index_set_subscribed;
+
+ v->notify_init = mailbox_list_index_notify_init;
+ v->notify_next = mailbox_list_index_notify_next;
+ v->notify_deinit = mailbox_list_index_notify_deinit;
+ v->notify_wait = mailbox_list_index_notify_wait;
+ v->notify_flush = mailbox_list_index_notify_flush;
+
+ MODULE_CONTEXT_SET(list, mailbox_list_index_module, ilist);
+
+ if ((list->flags & MAILBOX_LIST_FLAG_SECONDARY) != 0) {
+ /* secondary lists aren't accessible via namespaces, so we
+ need to finish them now. */
+ mailbox_list_index_init_finish(list);
+ }
+}
+
+static void mailbox_list_index_init_finish(struct mailbox_list *list)
+{
+ struct mailbox_list_index *ilist = INDEX_LIST_CONTEXT(list);
+ const char *dir;
+
+ if (ilist == NULL || !ilist->pending_init)
+ return;
+ ilist->pending_init = FALSE;
+
+ /* we've delayed this part of the initialization so that mbox format
+ can override the index root directory path */
+ if (!mailbox_list_get_root_path(list, MAILBOX_LIST_PATH_TYPE_LIST_INDEX,
+ &dir)) {
+ /* in-memory indexes */
+ dir = NULL;
+ }
+ i_assert(ilist->has_backing_store || dir != NULL);
+
+ i_assert(list->set.list_index_fname != NULL);
+ ilist->path = dir == NULL ? "(in-memory mailbox list index)" :
+ p_strdup_printf(list->pool, "%s/%s", dir, list->set.list_index_fname);
+ ilist->index = mail_index_alloc(list->ns->user->event,
+ dir, list->set.list_index_fname);
+ ilist->rebuild_on_missing_inbox = !ilist->has_backing_store &&
+ (list->ns->flags & NAMESPACE_FLAG_INBOX_ANY) != 0;
+
+ ilist->ext_id = mail_index_ext_register(ilist->index, "list",
+ sizeof(struct mailbox_list_index_header),
+ sizeof(struct mailbox_list_index_record),
+ sizeof(uint32_t));
+ ilist->subs_hdr_ext_id = mail_index_ext_register(ilist->index, "subs",
+ sizeof(uint32_t), 0,
+ sizeof(uint32_t));
+ mailbox_list_index_init_pool(ilist);
+
+ mailbox_list_index_status_init_finish(list);
+}
+
+static void
+mailbox_list_index_namespaces_added(struct mail_namespace *namespaces)
+{
+ struct mail_namespace *ns;
+
+ for (ns = namespaces; ns != NULL; ns = ns->next)
+ mailbox_list_index_init_finish(ns->list);
+}
+
+static struct mailbox_sync_context *
+mailbox_list_index_sync_init(struct mailbox *box,
+ enum mailbox_sync_flags flags)
+{
+ struct index_list_mailbox *ibox = INDEX_LIST_STORAGE_CONTEXT(box);
+
+ mailbox_list_index_status_sync_init(box);
+ if (!ibox->have_backend)
+ mailbox_list_index_backend_sync_init(box, flags);
+ return ibox->module_ctx.super.sync_init(box, flags);
+}
+
+static int
+mailbox_list_index_sync_deinit(struct mailbox_sync_context *ctx,
+ struct mailbox_sync_status *status_r)
+{
+ struct index_list_mailbox *ibox = INDEX_LIST_STORAGE_CONTEXT(ctx->box);
+ struct mailbox *box = ctx->box;
+
+ if (ibox->module_ctx.super.sync_deinit(ctx, status_r) < 0)
+ return -1;
+ ctx = NULL;
+
+ mailbox_list_index_status_sync_deinit(box);
+ if (ibox->have_backend)
+ return mailbox_list_index_backend_sync_deinit(box);
+ else
+ return 0;
+}
+
+static void mailbox_list_index_mailbox_allocated(struct mailbox *box)
+{
+ struct mailbox_vfuncs *v = box->vlast;
+ struct mailbox_list_index *ilist = INDEX_LIST_CONTEXT(box->list);
+ struct index_list_mailbox *ibox;
+
+ if (ilist == NULL)
+ return;
+
+ ibox = p_new(box->pool, struct index_list_mailbox, 1);
+ ibox->module_ctx.super = *v;
+ box->vlast = &ibox->module_ctx.super;
+ MODULE_CONTEXT_SET(box, index_list_storage_module, ibox);
+
+ /* for layout=index these get overridden */
+ v->open = mailbox_list_index_open_mailbox;
+ v->create_box = mailbox_list_index_create_mailbox;
+ v->update_box = mailbox_list_index_update_mailbox;
+
+ /* These are used by both status and backend code, but they can't both
+ be overriding the same function pointer since they share the
+ super pointer. */
+ v->sync_init = mailbox_list_index_sync_init;
+ v->sync_deinit = mailbox_list_index_sync_deinit;
+
+ mailbox_list_index_status_init_mailbox(v);
+ ibox->have_backend = mailbox_list_index_backend_init_mailbox(box, v);
+}
+
+static struct mail_storage_hooks mailbox_list_index_hooks = {
+ .mailbox_list_created = mailbox_list_index_created,
+ .mail_namespaces_added = mailbox_list_index_namespaces_added,
+ .mailbox_allocated = mailbox_list_index_mailbox_allocated
+};
+
+void mailbox_list_index_init(void); /* called in mailbox-list-register.c */
+
+void mailbox_list_index_init(void)
+{
+ mail_storage_hooks_add_internal(&mailbox_list_index_hooks);
+}
diff --git a/src/lib-storage/list/mailbox-list-index.h b/src/lib-storage/list/mailbox-list-index.h
new file mode 100644
index 0000000..a2ca620
--- /dev/null
+++ b/src/lib-storage/list/mailbox-list-index.h
@@ -0,0 +1,240 @@
+#ifndef MAILBOX_LIST_INDEX_H
+#define MAILBOX_LIST_INDEX_H
+
+/* Mailbox list index basically contains:
+
+ Header contains ID => name mapping. The name isn't the full mailbox name,
+ but rather each hierarchy level has its own ID and name. For example a
+ mailbox name "foo/bar" (with '/' as separator) would have separate IDs for
+ "foo" and "bar" names.
+
+ The records contain { parent_uid, uid, name_id } field that can be used to
+ build the whole mailbox tree. parent_uid=0 means root, otherwise it's the
+ parent node's uid.
+
+ Each record also contains GUID for each selectable mailbox. If a mailbox
+ is recreated using the same name, its GUID also changes. Note however that
+ the UID doesn't change, because the UID refers to the mailbox name, not to
+ the mailbox itself.
+
+ The records may contain also extensions for allowing mailbox_get_status()
+ to return values directly from the mailbox list index. Storage backends
+ may also add their own extensions to figure out if a record is up to date.
+*/
+
+#include "module-context.h"
+#include "mail-types.h"
+#include "mail-storage.h"
+#include "mailbox-list-private.h"
+
+#include <sys/time.h>
+
+#define MAILBOX_LIST_INDEX_HIERARCHY_SEP '~'
+#define MAILBOX_LIST_INDEX_HIERARCHY_ALT_SEP '^'
+
+#define INDEX_LIST_CONTEXT(obj) \
+ MODULE_CONTEXT(obj, mailbox_list_index_module)
+#define INDEX_LIST_CONTEXT_REQUIRE(obj) \
+ MODULE_CONTEXT_REQUIRE(obj, mailbox_list_index_module)
+
+/* Should the STATUS information for this mailbox not be written to the
+ mailbox list index? */
+#define MAILBOX_IS_NEVER_IN_INDEX(box) \
+ ((box)->inbox_any && !(box)->storage->set->mailbox_list_index_include_inbox)
+
+struct mail_index_view;
+struct mailbox_index_vsize;
+struct mailbox_vfuncs;
+
+/* stored in mail_index_record.flags: */
+enum mailbox_list_index_flags {
+ MAILBOX_LIST_INDEX_FLAG_NONEXISTENT = MAIL_DELETED,
+ MAILBOX_LIST_INDEX_FLAG_NOSELECT = MAIL_DRAFT,
+ MAILBOX_LIST_INDEX_FLAG_NOINFERIORS = MAIL_ANSWERED,
+ MAILBOX_LIST_INDEX_FLAG_CORRUPTED_NAME = MAIL_SEEN,
+
+ /* set during syncing for mailboxes that still exist */
+ MAILBOX_LIST_INDEX_FLAG_SYNC_EXISTS = MAIL_FLAGGED
+};
+
+struct mailbox_list_index_header {
+ uint8_t refresh_flag;
+ /* array of { uint32_t id; char name[]; } */
+};
+
+struct mailbox_list_index_record {
+ /* points to given id in header */
+ uint32_t name_id;
+ /* parent mailbox's UID, 0 = root */
+ uint32_t parent_uid;
+
+ /* the following fields are temporarily zero while unknown,
+ also permanently zero for \NoSelect and \Nonexistent mailboxes: */
+
+ guid_128_t guid;
+ uint32_t uid_validity;
+};
+
+struct mailbox_list_index_msgs_record {
+ uint32_t messages;
+ uint32_t unseen;
+ uint32_t recent;
+ uint32_t uidnext;
+};
+
+struct mailbox_list_index_node {
+ struct mailbox_list_index_node *parent;
+ struct mailbox_list_index_node *next;
+ struct mailbox_list_index_node *children;
+
+ uint32_t name_id, uid;
+ enum mailbox_list_index_flags flags;
+ /* extension data is corrupted on disk - need to update it */
+ bool corrupted_ext;
+ /* flags are corrupted on disk - need to update it */
+ bool corrupted_flags;
+ const char *raw_name;
+};
+
+struct mailbox_list_index {
+ union mailbox_list_module_context module_ctx;
+
+ const char *path;
+ struct mail_index *index;
+ uint32_t ext_id, msgs_ext_id, hmodseq_ext_id, subs_hdr_ext_id;
+ uint32_t vsize_ext_id, first_saved_ext_id;
+ struct timeval last_refresh_timeval;
+
+ pool_t mailbox_pool;
+ /* uin32_t id => name */
+ HASH_TABLE(void *, char *) mailbox_names;
+ uint32_t highest_name_id;
+
+ struct mailbox_list_index_sync_context *sync_ctx;
+ uint32_t sync_log_file_seq;
+ uoff_t sync_log_file_offset;
+ uint32_t sync_stamp;
+ struct timeout *to_refresh;
+
+ /* uint32_t uid => node */
+ HASH_TABLE(void *, struct mailbox_list_index_node *) mailbox_hash;
+ struct mailbox_list_index_node *mailbox_tree;
+
+ bool pending_init:1;
+ bool opened:1;
+ bool syncing:1;
+ bool updating_status:1;
+ bool has_backing_store:1;
+ bool index_last_check_changed:1;
+ bool corrupted_names_or_parents:1;
+ bool handling_corruption:1;
+ bool call_corruption_callback:1;
+ bool rebuild_on_missing_inbox:1;
+ bool force_resynced:1;
+ bool force_resync_failed:1;
+};
+
+struct mailbox_list_index_iterate_context {
+ struct mailbox_list_iterate_context ctx;
+ pool_t mailbox_pool;
+
+ struct mailbox_info info;
+ pool_t info_pool;
+
+ size_t parent_len;
+ string_t *path;
+ struct mailbox_list_index_node *next_node;
+
+ bool failed:1;
+ bool prefix_inbox_list:1;
+};
+
+extern MODULE_CONTEXT_DEFINE(mailbox_list_index_module,
+ &mailbox_list_module_register);
+
+void mailbox_list_index_set_index_error(struct mailbox_list *list);
+struct mailbox_list_index_node *
+mailbox_list_index_lookup(struct mailbox_list *list, const char *name);
+struct mailbox_list_index_node *
+mailbox_list_index_lookup_uid(struct mailbox_list_index *ilist, uint32_t uid);
+void mailbox_list_index_node_get_path(const struct mailbox_list_index_node *node,
+ char sep, string_t *str);
+void mailbox_list_index_node_unlink(struct mailbox_list_index *ilist,
+ struct mailbox_list_index_node *node);
+
+int mailbox_list_index_index_open(struct mailbox_list *list);
+bool mailbox_list_index_need_refresh(struct mailbox_list_index *ilist,
+ struct mail_index_view *view);
+/* Refresh the index, but only if it hasn't been refreshed "recently"
+ (= within this same ioloop run) */
+int mailbox_list_index_refresh(struct mailbox_list *list);
+/* Refresh the index regardless of when the last refresh was done. */
+int mailbox_list_index_refresh_force(struct mailbox_list *list);
+void mailbox_list_index_refresh_later(struct mailbox_list *list);
+int mailbox_list_index_handle_corruption(struct mailbox_list *list);
+int mailbox_list_index_set_uncorrupted(struct mailbox_list *list);
+
+/* Returns TRUE and index_r if mailbox list index exists, FALSE if not. */
+bool mailbox_list_index_get_index(struct mailbox_list *list,
+ struct mail_index **index_r);
+/* Open mailbox list index's view and get the given mailbox's sequence number
+ in it. If require_refreshed is TRUE, the mailbox must have up-to-date
+ information in the mailbox list index. Returns 1 if ok, 0 if mailbox wasn't
+ found or it wasn't up-to-date as requested, -1 if there was an error. The
+ error is stored to the mailbox storage. */
+int mailbox_list_index_view_open(struct mailbox *box, bool require_refreshed,
+ struct mail_index_view **view_r,
+ uint32_t *seq_r);
+
+struct mailbox_list_index_node *
+mailbox_list_index_node_find_sibling(const struct mailbox_list *list,
+ struct mailbox_list_index_node *node,
+ const char *name);
+void mailbox_list_index_reset(struct mailbox_list_index *ilist);
+int mailbox_list_index_parse(struct mailbox_list *list,
+ struct mail_index_view *view, bool force);
+
+struct mailbox_list_iterate_context *
+mailbox_list_index_iter_init(struct mailbox_list *list,
+ const char *const *patterns,
+ enum mailbox_list_iter_flags flags);
+const struct mailbox_info *
+mailbox_list_index_iter_next(struct mailbox_list_iterate_context *ctx);
+int mailbox_list_index_iter_deinit(struct mailbox_list_iterate_context *ctx);
+
+bool mailbox_list_index_status(struct mailbox_list *list,
+ struct mail_index_view *view,
+ uint32_t seq, enum mailbox_status_items items,
+ struct mailbox_status *status_r,
+ uint8_t *mailbox_guid,
+ struct mailbox_index_vsize *vsize_r,
+ const char **reason_r);
+void mailbox_list_index_status_set_info_flags(struct mailbox *box, uint32_t uid,
+ enum mailbox_info_flags *flags);
+void mailbox_list_index_update_mailbox_index(struct mailbox *box,
+ const struct mailbox_update *update);
+
+int mailbox_list_index_notify_init(struct mailbox_list *list,
+ enum mailbox_list_notify_event mask,
+ struct mailbox_list_notify **notify_r);
+void mailbox_list_index_notify_deinit(struct mailbox_list_notify *notify);
+int mailbox_list_index_notify_next(struct mailbox_list_notify *notify,
+ const struct mailbox_list_notify_rec **rec_r);
+void mailbox_list_index_notify_wait(struct mailbox_list_notify *notify,
+ void (*callback)(void *context),
+ void *context);
+void mailbox_list_index_notify_flush(struct mailbox_list_notify *notify);
+
+void mailbox_list_index_status_init_mailbox(struct mailbox_vfuncs *v);
+bool mailbox_list_index_backend_init_mailbox(struct mailbox *box,
+ struct mailbox_vfuncs *v);
+void mailbox_list_index_status_init_finish(struct mailbox_list *list);
+
+void mailbox_list_index_status_sync_init(struct mailbox *box);
+void mailbox_list_index_status_sync_deinit(struct mailbox *box);
+
+void mailbox_list_index_backend_sync_init(struct mailbox *box,
+ enum mailbox_sync_flags flags);
+int mailbox_list_index_backend_sync_deinit(struct mailbox *box);
+
+#endif
diff --git a/src/lib-storage/list/mailbox-list-iter-private.h b/src/lib-storage/list/mailbox-list-iter-private.h
new file mode 100644
index 0000000..47de2ce
--- /dev/null
+++ b/src/lib-storage/list/mailbox-list-iter-private.h
@@ -0,0 +1,42 @@
+#ifndef MAILBOX_LIST_ITER_PRIVATE_H
+#define MAILBOX_LIST_ITER_PRIVATE_H
+
+#include "mailbox-list-private.h"
+#include "mailbox-list-iter.h"
+#include "mailbox-list-delete.h"
+
+struct autocreate_box {
+ const char *name;
+ const struct mailbox_settings *set;
+ enum mailbox_info_flags flags;
+ bool child_listed;
+};
+
+ARRAY_DEFINE_TYPE(mailbox_settings, struct mailbox_settings *);
+struct mailbox_list_autocreate_iterate_context {
+ unsigned int idx;
+ struct mailbox_info new_info;
+ ARRAY(struct autocreate_box) boxes;
+ ARRAY_TYPE(mailbox_settings) box_sets;
+ ARRAY_TYPE(mailbox_settings) all_ns_box_sets;
+ HASH_TABLE(char *, char *) duplicate_vnames;
+ bool listing_autoboxes:1;
+};
+
+static inline bool
+mailbox_list_iter_try_delete_noselect(struct mailbox_list_iterate_context *ctx,
+ const struct mailbox_info *info,
+ const char *storage_name)
+{
+ if ((info->flags & (MAILBOX_NOSELECT|MAILBOX_NOCHILDREN)) ==
+ (MAILBOX_NOSELECT|MAILBOX_NOCHILDREN) &&
+ ctx->list->set.no_noselect) {
+ /* Try to rmdir() all \NoSelect mailbox leafs and
+ afterwards their parents. */
+ mailbox_list_delete_mailbox_until_root(ctx->list, storage_name);
+ return TRUE;
+ }
+ return FALSE;
+}
+
+#endif
diff --git a/src/lib-storage/list/mailbox-list-iter.c b/src/lib-storage/list/mailbox-list-iter.c
new file mode 100644
index 0000000..4098d8f
--- /dev/null
+++ b/src/lib-storage/list/mailbox-list-iter.c
@@ -0,0 +1,1168 @@
+/* Copyright (c) 2006-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "hash.h"
+#include "imap-match.h"
+#include "mail-storage.h"
+#include "mailbox-tree.h"
+#include "mailbox-list-subscriptions.h"
+#include "mailbox-list-private.h"
+#include "mailbox-list-iter-private.h"
+
+enum autocreate_match_result {
+ /* list contains the mailbox */
+ AUTOCREATE_MATCH_RESULT_YES = 0x01,
+ /* list contains children of the mailbox */
+ AUTOCREATE_MATCH_RESULT_CHILDREN = 0x02,
+ /* list contains parents of the mailbox */
+ AUTOCREATE_MATCH_RESULT_PARENT = 0x04
+};
+
+struct ns_list_iterate_context {
+ struct mailbox_list_iterate_context ctx;
+ struct mailbox_list_iterate_context *backend_ctx;
+ struct mail_namespace *namespaces, *cur_ns;
+ struct mailbox_list *error_list;
+ pool_t pool;
+ const char **patterns, **patterns_ns_match;
+ enum mail_namespace_type type_mask;
+
+ struct mailbox_info ns_info;
+ struct mailbox_info inbox_info;
+ const struct mailbox_info *pending_backend_info;
+
+ bool cur_ns_prefix_sent:1;
+ bool inbox_list:1;
+ bool inbox_listed:1;
+ bool inbox_seen:1;
+};
+
+static void mailbox_list_ns_iter_failed(struct ns_list_iterate_context *ctx);
+static bool ns_match_next(struct ns_list_iterate_context *ctx,
+ struct mail_namespace *ns, const char *pattern);
+static int mailbox_list_match_anything(struct ns_list_iterate_context *ctx,
+ struct mail_namespace *ns,
+ const char *prefix);
+
+static struct mailbox_list_iterate_context mailbox_list_iter_failed;
+
+struct mailbox_list_iterate_context *
+mailbox_list_iter_init(struct mailbox_list *list, const char *pattern,
+ enum mailbox_list_iter_flags flags)
+{
+ const char *patterns[2];
+
+ patterns[0] = pattern;
+ patterns[1] = NULL;
+ return mailbox_list_iter_init_multiple(list, patterns, flags);
+}
+
+int mailbox_list_iter_subscriptions_refresh(struct mailbox_list *list)
+{
+ struct mail_namespace *ns = list->ns;
+
+ if ((ns->flags & NAMESPACE_FLAG_SUBSCRIPTIONS) == 0) {
+ /* no subscriptions in this namespace. find where they are. */
+ ns = mail_namespace_find_subscribable(ns->user->namespaces,
+ ns->prefix);
+ if (ns == NULL) {
+ /* no subscriptions. avoid crashes by initializing
+ a subscriptions tree. */
+ if (list->subscriptions == NULL) {
+ char sep = mail_namespace_get_sep(list->ns);
+ list->subscriptions = mailbox_tree_init(sep);
+ }
+ return 0;
+ }
+ }
+ return ns->list->v.subscriptions_refresh(ns->list, list);
+}
+
+static struct mailbox_settings *
+mailbox_settings_add_ns_prefix(pool_t pool, struct mail_namespace *ns,
+ struct mailbox_settings *in_set)
+{
+ struct mailbox_settings *out_set;
+
+ if (ns->prefix_len == 0 || strcasecmp(in_set->name, "INBOX") == 0)
+ return in_set;
+
+ out_set = p_new(pool, struct mailbox_settings, 1);
+ *out_set = *in_set;
+ if (*in_set->name == '\0') {
+ /* namespace prefix itself */
+ out_set->name = p_strndup(pool, ns->prefix, ns->prefix_len-1);
+ } else {
+ out_set->name =
+ p_strconcat(pool, ns->prefix, in_set->name, NULL);
+ }
+ return out_set;
+}
+
+static void
+mailbox_list_iter_init_autocreate(struct mailbox_list_iterate_context *ctx)
+{
+ struct mail_namespace *ns = ctx->list->ns;
+ struct mailbox_list_autocreate_iterate_context *actx;
+ struct mailbox_settings *const *box_sets, *set;
+ struct autocreate_box *autobox;
+ unsigned int i, count;
+
+ if (!array_is_created(&ns->set->mailboxes))
+ return;
+ box_sets = array_get(&ns->set->mailboxes, &count);
+ if (count == 0)
+ return;
+
+ actx = p_new(ctx->pool, struct mailbox_list_autocreate_iterate_context, 1);
+ ctx->autocreate_ctx = actx;
+ hash_table_create(&actx->duplicate_vnames, ctx->pool, 0,
+ str_hash, strcmp);
+
+ /* build the list of mailboxes we need to consider as existing */
+ p_array_init(&actx->boxes, ctx->pool, 16);
+ p_array_init(&actx->box_sets, ctx->pool, 16);
+ p_array_init(&actx->all_ns_box_sets, ctx->pool, 16);
+ for (i = 0; i < count; i++) {
+ if (strcmp(box_sets[i]->autocreate, MAILBOX_SET_AUTO_NO) == 0)
+ continue;
+
+ set = mailbox_settings_add_ns_prefix(ctx->pool,
+ ns, box_sets[i]);
+
+ /* autocreate mailbox belongs to listed namespace */
+ array_push_back(&actx->all_ns_box_sets, &set);
+ if ((ctx->flags & MAILBOX_LIST_ITER_SELECT_SUBSCRIBED) == 0 ||
+ strcmp(set->autocreate, MAILBOX_SET_AUTO_SUBSCRIBE) == 0) {
+ array_push_back(&actx->box_sets, &set);
+ autobox = array_append_space(&actx->boxes);
+ autobox->name = set->name;
+ autobox->set = set;
+ if (strcasecmp(autobox->name, "INBOX") == 0) {
+ /* make sure duplicate INBOX/Inbox/etc.
+ won't get created */
+ autobox->name = "INBOX";
+ }
+ }
+ }
+}
+
+struct mailbox_list_iterate_context *
+mailbox_list_iter_init_multiple(struct mailbox_list *list,
+ const char *const *patterns,
+ enum mailbox_list_iter_flags flags)
+{
+ struct mailbox_list_iterate_context *ctx;
+
+ i_assert(*patterns != NULL);
+
+ if ((flags & (MAILBOX_LIST_ITER_SELECT_SUBSCRIBED |
+ MAILBOX_LIST_ITER_RETURN_SUBSCRIBED)) != 0) {
+ if (mailbox_list_iter_subscriptions_refresh(list) < 0)
+ return &mailbox_list_iter_failed;
+ }
+
+ ctx = list->v.iter_init(list, patterns, flags);
+ if ((flags & MAILBOX_LIST_ITER_NO_AUTO_BOXES) == 0)
+ mailbox_list_iter_init_autocreate(ctx);
+ return ctx;
+}
+
+static bool
+ns_match_simple(struct ns_list_iterate_context *ctx, struct mail_namespace *ns)
+{
+ if ((ctx->type_mask & ns->type) == 0)
+ return FALSE;
+
+ if ((ctx->ctx.flags & MAILBOX_LIST_ITER_SKIP_ALIASES) != 0) {
+ if (ns->alias_for != NULL)
+ return FALSE;
+ }
+ return TRUE;
+}
+
+static bool
+ns_is_match_within_ns(struct ns_list_iterate_context *ctx,
+ struct mail_namespace *ns, const char *prefix_without_sep,
+ const char *pattern, enum imap_match_result result)
+{
+ if ((ctx->ctx.flags & MAILBOX_LIST_ITER_STAR_WITHIN_NS) == 0) {
+ switch (result) {
+ case IMAP_MATCH_YES:
+ case IMAP_MATCH_CHILDREN:
+ return TRUE;
+ case IMAP_MATCH_NO:
+ case IMAP_MATCH_PARENT:
+ break;
+ }
+ return FALSE;
+ }
+
+ switch (result) {
+ case IMAP_MATCH_YES:
+ /* allow matching prefix only when it's done without
+ wildcards */
+ if (strcmp(prefix_without_sep, pattern) == 0)
+ return TRUE;
+ break;
+ case IMAP_MATCH_CHILDREN: {
+ /* allow this only if there isn't another namespace
+ with longer prefix that matches this pattern
+ (namespaces are sorted by prefix length) */
+ struct mail_namespace *tmp;
+
+ T_BEGIN {
+ for (tmp = ns->next; tmp != NULL; tmp = tmp->next) {
+ if (ns_match_simple(ctx, tmp) &&
+ ns_match_next(ctx, tmp, pattern))
+ break;
+ }
+ } T_END;
+ if (tmp == NULL)
+ return TRUE;
+ break;
+ }
+ case IMAP_MATCH_NO:
+ case IMAP_MATCH_PARENT:
+ break;
+ }
+ return FALSE;
+}
+
+static bool list_pattern_has_wildcards(const char *pattern)
+{
+ for (; *pattern != '\0'; pattern++) {
+ if (*pattern == '%' || *pattern == '*')
+ return TRUE;
+ }
+ return FALSE;
+}
+
+static bool ns_match_next(struct ns_list_iterate_context *ctx,
+ struct mail_namespace *ns, const char *pattern)
+{
+ struct imap_match_glob *glob;
+ enum imap_match_result result;
+ const char *prefix_without_sep;
+ size_t len;
+
+ len = ns->prefix_len;
+ if (len > 0 && ns->prefix[len-1] == mail_namespace_get_sep(ns))
+ len--;
+
+ if ((ns->flags & (NAMESPACE_FLAG_LIST_PREFIX |
+ NAMESPACE_FLAG_LIST_CHILDREN)) == 0) {
+ /* non-listable namespace matches only with exact prefix */
+ if (strncmp(ns->prefix, pattern, ns->prefix_len) != 0)
+ return FALSE;
+ /* with prefix="", list=no we don't want to show anything,
+ except when the client explicitly lists a mailbox without
+ wildcards (e.g. LIST "" mailbox). this is mainly useful
+ for working around client bugs (and supporting a specific
+ IMAP client behavior that's not exactly buggy but not very
+ good IMAP behavior either). */
+ if (ns->prefix_len == 0 && list_pattern_has_wildcards(pattern))
+ return FALSE;
+ }
+
+ prefix_without_sep = t_strndup(ns->prefix, len);
+ if (*prefix_without_sep == '\0')
+ result = IMAP_MATCH_CHILDREN;
+ else {
+ glob = imap_match_init(pool_datastack_create(), pattern,
+ TRUE, mail_namespace_get_sep(ns));
+ result = imap_match(glob, prefix_without_sep);
+ }
+
+ return ns_is_match_within_ns(ctx, ns, prefix_without_sep,
+ pattern, result);
+}
+
+static bool
+mailbox_list_ns_match_patterns(struct ns_list_iterate_context *ctx)
+{
+ struct mail_namespace *ns = ctx->cur_ns;
+ unsigned int i;
+
+ if (!ns_match_simple(ctx, ns))
+ return FALSE;
+
+ /* filter out namespaces whose prefix doesn't match. this same code
+ handles both with and without STAR_WITHIN_NS, so the "without" case
+ is slower than necessary, but this shouldn't matter much */
+ T_BEGIN {
+ for (i = 0; ctx->patterns_ns_match[i] != NULL; i++) {
+ if (ns_match_next(ctx, ns, ctx->patterns_ns_match[i]))
+ break;
+ }
+ } T_END;
+
+ return ctx->patterns_ns_match[i] != NULL;
+}
+
+static bool
+iter_next_try_prefix_pattern(struct ns_list_iterate_context *ctx,
+ struct mail_namespace *ns, const char *pattern)
+{
+ struct imap_match_glob *glob;
+ enum imap_match_result result;
+ const char *prefix_without_sep;
+
+ i_assert(ns->prefix_len > 0);
+
+ if ((ns->flags & (NAMESPACE_FLAG_LIST_PREFIX |
+ NAMESPACE_FLAG_LIST_CHILDREN)) == 0) {
+ /* non-listable namespace matches only with exact prefix */
+ if (strncmp(ns->prefix, pattern, ns->prefix_len) != 0)
+ return FALSE;
+ }
+
+ prefix_without_sep = t_strndup(ns->prefix, ns->prefix_len-1);
+ glob = imap_match_init(pool_datastack_create(), pattern,
+ TRUE, mail_namespace_get_sep(ns));
+ result = imap_match(glob, prefix_without_sep);
+ return result == IMAP_MATCH_YES &&
+ ns_is_match_within_ns(ctx, ns, prefix_without_sep,
+ pattern, result);
+}
+
+static bool
+mailbox_list_ns_prefix_match(struct ns_list_iterate_context *ctx,
+ struct mail_namespace *ns)
+{
+ unsigned int i;
+ bool ret = FALSE;
+
+ for (i = 0; ctx->patterns_ns_match[i] != NULL; i++) {
+ T_BEGIN {
+ ret = iter_next_try_prefix_pattern(ctx, ns,
+ ctx->patterns_ns_match[i]);
+ } T_END;
+ if (ret)
+ break;
+ }
+ return ret;
+}
+
+static int
+ns_prefix_is_visible(struct ns_list_iterate_context *ctx,
+ struct mail_namespace *ns)
+{
+ int ret;
+
+ if ((ns->flags & NAMESPACE_FLAG_LIST_PREFIX) != 0)
+ return 1;
+ if ((ns->flags & NAMESPACE_FLAG_LIST_CHILDREN) != 0) {
+ if ((ret = mailbox_list_match_anything(ctx, ns, ns->prefix)) != 0)
+ return ret;
+ }
+ return 0;
+}
+
+static int
+ns_prefix_has_visible_child_namespace(struct ns_list_iterate_context *ctx,
+ const char *prefix)
+{
+ struct mail_namespace *ns;
+ size_t prefix_len = strlen(prefix);
+ int ret;
+
+ for (ns = ctx->namespaces; ns != NULL; ns = ns->next) {
+ if (ns->prefix_len > prefix_len &&
+ strncmp(ns->prefix, prefix, prefix_len) == 0) {
+ ret = ns_prefix_is_visible(ctx, ns);
+ if (ret != 0)
+ return ret;
+ }
+ }
+ return 0;
+}
+
+static bool
+mailbox_ns_prefix_is_shared_inbox(struct mail_namespace *ns)
+{
+ return ns->type == MAIL_NAMESPACE_TYPE_SHARED &&
+ (ns->flags & NAMESPACE_FLAG_INBOX_ANY) != 0 &&
+ !ns->list->mail_set->mail_shared_explicit_inbox;
+}
+
+static bool
+mailbox_is_shared_inbox(struct mail_namespace *ns, const char *vname)
+{
+ return mailbox_ns_prefix_is_shared_inbox(ns) &&
+ strncmp(ns->prefix, vname, ns->prefix_len-1) == 0 &&
+ vname[ns->prefix_len-1] == '\0';
+}
+
+static int
+mailbox_list_match_anything(struct ns_list_iterate_context *ctx,
+ struct mail_namespace *ns, const char *prefix)
+{
+ enum mailbox_list_iter_flags list_flags =
+ MAILBOX_LIST_ITER_RETURN_NO_FLAGS;
+ struct mailbox_list_iterate_context *list_iter;
+ const struct mailbox_info *info;
+ const char *pattern;
+ int ret;
+
+ if ((ret = ns_prefix_has_visible_child_namespace(ctx, prefix)) != 0)
+ return ret;
+
+ pattern = t_strconcat(prefix, "%", NULL);
+ list_iter = mailbox_list_iter_init(ns->list, pattern, list_flags);
+ info = mailbox_list_iter_next(list_iter);
+ if (info != NULL && mailbox_ns_prefix_is_shared_inbox(ns) &&
+ mailbox_is_shared_inbox(ns, info->vname)) {
+ /* we don't want to see this, try the next one */
+ info = mailbox_list_iter_next(list_iter);
+ }
+ ret = info != NULL ? 1 : 0;
+ if (mailbox_list_iter_deinit(&list_iter) < 0) {
+ if (ret == 0)
+ ret = -1;
+ }
+ return ret;
+}
+
+static bool
+mailbox_ns_prefix_check_selection_criteria(struct ns_list_iterate_context *ctx)
+{
+ if ((ctx->ctx.flags & MAILBOX_LIST_ITER_SELECT_SUBSCRIBED) != 0) {
+ if ((ctx->ns_info.flags & MAILBOX_SUBSCRIBED) != 0)
+ return TRUE;
+ if ((ctx->ctx.flags & MAILBOX_LIST_ITER_SELECT_RECURSIVEMATCH) != 0 &&
+ (ctx->ns_info.flags & MAILBOX_CHILD_SUBSCRIBED) != 0)
+ return TRUE;
+ return FALSE;
+ }
+ return TRUE;
+}
+
+static bool
+mailbox_list_ns_prefix_return(struct ns_list_iterate_context *ctx,
+ struct mail_namespace *ns, bool has_children)
+{
+ struct mailbox *box;
+ enum mailbox_existence existence;
+ int ret;
+
+ if (strncasecmp(ns->prefix, "INBOX", 5) == 0 &&
+ ns->prefix[5] == mail_namespace_get_sep(ns)) {
+ /* prefix=INBOX/ (or prefix=INBOX/something/) namespace exists.
+ so we can create children to INBOX. */
+ ctx->inbox_info.flags &= ENUM_NEGATE(MAILBOX_NOINFERIORS);
+ }
+
+ if (ns->prefix_len == 0 || !mailbox_list_ns_prefix_match(ctx, ns))
+ return FALSE;
+
+ i_zero(&ctx->ns_info);
+ ctx->ns_info.ns = ns;
+ ctx->ns_info.vname = p_strndup(ctx->pool, ns->prefix,
+ ns->prefix_len-1);
+ if (ns->special_use_mailboxes)
+ ctx->ns_info.flags |= MAILBOX_CHILD_SPECIALUSE;
+
+ if (strcasecmp(ctx->ns_info.vname, "INBOX") == 0) {
+ i_assert(!ctx->inbox_listed);
+ ctx->inbox_listed = TRUE;
+ ctx->ns_info.flags |= ctx->inbox_info.flags | MAILBOX_SELECT;
+ }
+
+ if ((ctx->ctx.flags & (MAILBOX_LIST_ITER_RETURN_SUBSCRIBED |
+ MAILBOX_LIST_ITER_SELECT_SUBSCRIBED)) != 0) {
+ /* Refresh subscriptions first, this won't cause a duplicate
+ call later on as this is only called when the namespace's
+ children definitely don't match */
+ if (mailbox_list_iter_subscriptions_refresh(ns->list) < 0) {
+ mailbox_list_ns_iter_failed(ctx);
+ return FALSE;
+ }
+ mailbox_list_set_subscription_flags(ns->list,
+ ctx->ns_info.vname,
+ &ctx->ns_info.flags);
+ }
+ if (!mailbox_ns_prefix_check_selection_criteria(ctx))
+ return FALSE;
+
+ /* see if the namespace has children */
+ if (has_children)
+ ctx->ns_info.flags |= MAILBOX_CHILDREN;
+ else if ((ctx->ctx.flags & MAILBOX_LIST_ITER_RETURN_CHILDREN) != 0 ||
+ (ns->flags & NAMESPACE_FLAG_LIST_CHILDREN) != 0) {
+ /* need to check this explicitly */
+ if ((ret = mailbox_list_match_anything(ctx, ns, ns->prefix)) > 0)
+ ctx->ns_info.flags |= MAILBOX_CHILDREN;
+ else if (ret == 0) {
+ if ((ns->flags & NAMESPACE_FLAG_LIST_CHILDREN) != 0 &&
+ !mailbox_ns_prefix_is_shared_inbox(ns)) {
+ /* no children -> not visible */
+ return FALSE;
+ }
+ ctx->ns_info.flags |= MAILBOX_NOCHILDREN;
+ }
+ }
+
+ if ((ctx->ns_info.flags & MAILBOX_SELECT) == 0) {
+ /* see if namespace prefix is selectable */
+ box = mailbox_alloc(ns->list, ctx->ns_info.vname, 0);
+ if (mailbox_exists(box, TRUE, &existence) == 0 &&
+ existence == MAILBOX_EXISTENCE_SELECT)
+ ctx->ns_info.flags |= MAILBOX_SELECT;
+ else
+ ctx->ns_info.flags |= MAILBOX_NONEXISTENT;
+ mailbox_free(&box);
+ }
+ return TRUE;
+}
+
+static void inbox_set_children_flags(struct ns_list_iterate_context *ctx)
+{
+ struct mail_namespace *ns;
+ const char *prefix;
+ int ret;
+
+ if ((ctx->ctx.flags & MAILBOX_LIST_ITER_RETURN_NO_FLAGS) != 0)
+ return;
+ if ((ctx->inbox_info.flags & (MAILBOX_CHILDREN | MAILBOX_NOINFERIORS |
+ MAILBOX_NOCHILDREN)) != 0)
+ return;
+
+ ns = mail_namespace_find_prefix(ctx->namespaces, "");
+ if (ns == NULL || (ns->flags & NAMESPACE_FLAG_UNUSABLE) != 0) {
+ /* prefix="" namespace doesn't exist, and neither does
+ anything beginning with prefix=INBOX/ (we checked this
+ earlier). there's no way to create children for INBOX. */
+ ctx->inbox_info.flags |= MAILBOX_NOINFERIORS;
+ return;
+ }
+
+ /* INBOX namespace doesn't exist and we didn't see any children listed
+ for INBOX. this could be because there truly aren't any children,
+ or that the list patterns just didn't match them. */
+ prefix = t_strdup_printf("INBOX%c",
+ mail_namespace_get_sep(ctx->inbox_info.ns));
+ ret = mailbox_list_match_anything(ctx, ctx->inbox_info.ns, prefix);
+ if (ret > 0)
+ ctx->inbox_info.flags |= MAILBOX_CHILDREN;
+ else if (ret == 0)
+ ctx->inbox_info.flags |= MAILBOX_NOCHILDREN;
+}
+
+static void mailbox_list_ns_iter_failed(struct ns_list_iterate_context *ctx)
+{
+ enum mail_error error;
+ const char *errstr;
+
+ if (ctx->cur_ns->list != ctx->error_list) {
+ errstr = mailbox_list_get_last_error(ctx->cur_ns->list, &error);
+ mailbox_list_set_error(ctx->error_list, error, errstr);
+ }
+ ctx->ctx.failed = TRUE;
+}
+
+static bool
+mailbox_list_ns_iter_try_next(struct mailbox_list_iterate_context *_ctx,
+ const struct mailbox_info **info_r)
+{
+ struct ns_list_iterate_context *ctx =
+ (struct ns_list_iterate_context *)_ctx;
+ struct mail_namespace *ns;
+ const struct mailbox_info *info;
+ bool has_children;
+
+ if (ctx->cur_ns == NULL) {
+ if (!ctx->inbox_listed && ctx->inbox_list && !_ctx->failed &&
+ ((_ctx->flags & MAILBOX_LIST_ITER_NO_AUTO_BOXES) == 0 ||
+ ctx->inbox_seen)) {
+ /* send delayed INBOX reply */
+ ctx->inbox_listed = TRUE;
+ inbox_set_children_flags(ctx);
+ *info_r = &ctx->inbox_info;
+ return TRUE;
+ }
+ *info_r = NULL;
+ return TRUE;
+ }
+
+ if (ctx->backend_ctx == NULL) {
+ i_assert(ctx->pending_backend_info == NULL);
+ if (!mailbox_list_ns_match_patterns(ctx)) {
+ /* namespace's children don't match the patterns,
+ but the namespace prefix itself might */
+ ns = ctx->cur_ns;
+ ctx->cur_ns = ctx->cur_ns->next;
+ if (mailbox_list_ns_prefix_return(ctx, ns, FALSE)) {
+ *info_r = &ctx->ns_info;
+ return TRUE;
+ }
+ return FALSE;
+ }
+ /* start listing this namespace's mailboxes */
+ ctx->backend_ctx =
+ mailbox_list_iter_init_multiple(ctx->cur_ns->list,
+ ctx->patterns,
+ _ctx->flags);
+ ctx->cur_ns_prefix_sent = FALSE;
+ }
+ if (ctx->pending_backend_info == NULL)
+ info = mailbox_list_iter_next(ctx->backend_ctx);
+ else {
+ info = ctx->pending_backend_info;
+ ctx->pending_backend_info = NULL;
+ }
+ if (!ctx->cur_ns_prefix_sent) {
+ /* delayed sending of namespace prefix */
+ ctx->cur_ns_prefix_sent = TRUE;
+ has_children = info != NULL &&
+ !mailbox_is_shared_inbox(info->ns, info->vname);
+ if (mailbox_list_ns_prefix_return(ctx, ctx->cur_ns,
+ has_children)) {
+ ctx->pending_backend_info = info;
+ *info_r = &ctx->ns_info;
+ return TRUE;
+ }
+ }
+ if (info != NULL) {
+ if (strcasecmp(info->vname, "INBOX") == 0 && ctx->inbox_list) {
+ /* delay sending INBOX reply. we already saved its
+ flags at init stage, except for \Noinferiors
+ and subscription states */
+ ctx->inbox_seen = TRUE;
+ ctx->inbox_info.flags |=
+ (info->flags & (MAILBOX_NOINFERIORS |
+ MAILBOX_SUBSCRIBED |
+ MAILBOX_CHILD_SUBSCRIBED));
+ return FALSE;
+ }
+ if (strncasecmp(info->vname, "INBOX", 5) == 0 &&
+ info->vname[5] == mail_namespace_get_sep(info->ns)) {
+ /* we know now that INBOX has children */
+ ctx->inbox_info.flags |= MAILBOX_CHILDREN;
+ ctx->inbox_info.flags &= ENUM_NEGATE(MAILBOX_NOINFERIORS);
+ }
+ if (info->ns->prefix_len > 0 &&
+ strncmp(info->vname, info->ns->prefix,
+ info->ns->prefix_len-1) == 0 &&
+ info->vname[info->ns->prefix_len-1] == '\0') {
+ /* this is an entry for namespace prefix, which we
+ already returned. (e.g. shared/$user/INBOX entry
+ returned as shared/$user, or when listing
+ subscribed namespace prefix). */
+ return FALSE;
+ }
+
+ *info_r = info;
+ return TRUE;
+ }
+
+ /* finished with this namespace */
+ if (mailbox_list_iter_deinit(&ctx->backend_ctx) < 0)
+ mailbox_list_ns_iter_failed(ctx);
+ ctx->cur_ns = ctx->cur_ns->next;
+ return FALSE;
+}
+
+static const struct mailbox_info *
+mailbox_list_ns_iter_next(struct mailbox_list_iterate_context *_ctx)
+{
+ const struct mailbox_info *info = NULL;
+
+ while (!mailbox_list_ns_iter_try_next(_ctx, &info)) ;
+ return info;
+}
+
+static int
+mailbox_list_ns_iter_deinit(struct mailbox_list_iterate_context *_ctx)
+{
+ struct ns_list_iterate_context *ctx =
+ (struct ns_list_iterate_context *)_ctx;
+ int ret;
+
+ if (ctx->backend_ctx != NULL) {
+ if (mailbox_list_iter_deinit(&ctx->backend_ctx) < 0)
+ mailbox_list_ns_iter_failed(ctx);
+ }
+ ret = _ctx->failed ? -1 : 0;
+ pool_unref(&ctx->pool);
+ return ret;
+}
+
+static const char **
+dup_patterns_without_stars(pool_t pool, const char *const *patterns,
+ unsigned int count)
+{
+ const char **dup;
+ unsigned int i;
+
+ dup = p_new(pool, const char *, count + 1);
+ for (i = 0; i < count; i++) {
+ char *p = p_strdup(pool, patterns[i]);
+ dup[i] = p;
+
+ for (; *p != '\0'; p++) {
+ if (*p == '*')
+ *p = '%';
+ }
+ }
+ return dup;
+}
+
+static bool
+patterns_match_inbox(struct mail_namespace *namespaces,
+ const char *const *patterns)
+{
+ struct mail_namespace *ns = mail_namespace_find_inbox(namespaces);
+ struct imap_match_glob *glob;
+
+ glob = imap_match_init_multiple(pool_datastack_create(), patterns,
+ TRUE, mail_namespace_get_sep(ns));
+ return imap_match(glob, "INBOX") == IMAP_MATCH_YES;
+}
+
+static int inbox_info_init(struct ns_list_iterate_context *ctx,
+ struct mail_namespace *namespaces)
+{
+ enum mailbox_info_flags flags;
+ int ret;
+
+ ctx->inbox_info.vname = "INBOX";
+ ctx->inbox_info.ns = mail_namespace_find_inbox(namespaces);
+ i_assert(ctx->inbox_info.ns != NULL);
+
+ if ((ret = mailbox_list_mailbox(ctx->inbox_info.ns->list, "INBOX", &flags)) > 0)
+ ctx->inbox_info.flags = flags;
+ else if (ret < 0) {
+ ctx->cur_ns = ctx->inbox_info.ns;
+ mailbox_list_ns_iter_failed(ctx);
+ }
+ return ret;
+}
+
+struct mailbox_list_iterate_context *
+mailbox_list_iter_init_namespaces(struct mail_namespace *namespaces,
+ const char *const *patterns,
+ enum mail_namespace_type type_mask,
+ enum mailbox_list_iter_flags flags)
+{
+ struct ns_list_iterate_context *ctx;
+ unsigned int i, count;
+ pool_t pool;
+
+ i_assert(namespaces != NULL);
+
+ pool = pool_alloconly_create("mailbox list namespaces", 1024);
+ ctx = p_new(pool, struct ns_list_iterate_context, 1);
+ ctx->pool = pool;
+ ctx->type_mask = type_mask;
+ ctx->ctx.flags = flags;
+ ctx->ctx.list = p_new(pool, struct mailbox_list, 1);
+ ctx->ctx.list->v.iter_next = mailbox_list_ns_iter_next;
+ ctx->ctx.list->v.iter_deinit = mailbox_list_ns_iter_deinit;
+ ctx->namespaces = namespaces;
+ ctx->error_list = namespaces->list;
+
+ count = str_array_length(patterns);
+ ctx->patterns = p_new(pool, const char *, count + 1);
+ for (i = 0; i < count; i++)
+ ctx->patterns[i] = p_strdup(pool, patterns[i]);
+ if (patterns_match_inbox(namespaces, ctx->patterns) &&
+ (flags & MAILBOX_LIST_ITER_SELECT_SUBSCRIBED) == 0) {
+ /* we're going to list the INBOX. get its own flags (i.e. not
+ [no]children) immediately, so if we end up seeing something
+ else called INBOX (e.g. namespace prefix) we can show it
+ immediately with the proper flags. */
+ ctx->inbox_list = TRUE;
+ if (inbox_info_init(ctx, namespaces) < 0) {
+ pool_unref(&pool);
+ return &mailbox_list_iter_failed;
+ }
+ }
+
+ if ((flags & MAILBOX_LIST_ITER_STAR_WITHIN_NS) != 0) {
+ /* create copies of patterns with '*' wildcard changed to '%'.
+ this is used only when checking which namespaces to list */
+ ctx->patterns_ns_match =
+ dup_patterns_without_stars(pool, ctx->patterns, count);
+ } else {
+ ctx->patterns_ns_match = ctx->patterns;
+ }
+
+ ctx->cur_ns = namespaces;
+ ctx->ctx.list->ns = namespaces;
+ return &ctx->ctx;
+}
+
+static enum autocreate_match_result
+autocreate_box_match(const ARRAY_TYPE(mailbox_settings) *boxes,
+ struct mail_namespace *ns, const char *name,
+ bool only_subscribed, unsigned int *idx_r)
+{
+ struct mailbox_settings *const *sets;
+ unsigned int i, count;
+ size_t len, name_len = strlen(name);
+ enum autocreate_match_result result = 0;
+ char sep = mail_namespace_get_sep(ns);
+
+ *idx_r = UINT_MAX;
+
+ sets = array_get(boxes, &count);
+ for (i = 0; i < count; i++) {
+ if (only_subscribed &&
+ strcmp(sets[i]->autocreate, MAILBOX_SET_AUTO_SUBSCRIBE) != 0)
+ continue;
+ len = I_MIN(name_len, strlen(sets[i]->name));
+ if (strncmp(name, sets[i]->name, len) != 0)
+ continue;
+
+ if (name[len] == '\0' && sets[i]->name[len] == '\0') {
+ result |= AUTOCREATE_MATCH_RESULT_YES;
+ *idx_r = i;
+ } else if (name[len] == '\0' && sets[i]->name[len] == sep)
+ result |= AUTOCREATE_MATCH_RESULT_CHILDREN;
+ else if (name[len] == sep && sets[i]->name[len] == '\0')
+ result |= AUTOCREATE_MATCH_RESULT_PARENT;
+ }
+ return result;
+}
+
+const struct mailbox_info *
+mailbox_list_iter_autocreate_filter(struct mailbox_list_iterate_context *ctx,
+ const struct mailbox_info *_info)
+{
+ struct mailbox_list_autocreate_iterate_context *actx =
+ ctx->autocreate_ctx;
+ if (actx == NULL || _info == NULL)
+ return _info;
+ actx->new_info = *_info;
+ struct mailbox_info *info = &actx->new_info;
+ enum autocreate_match_result match, match2;
+ unsigned int idx;
+
+ match = autocreate_box_match(&actx->box_sets, ctx->list->ns,
+ info->vname, FALSE, &idx);
+
+ if (!actx->listing_autoboxes) {
+ if ((match & AUTOCREATE_MATCH_RESULT_YES) != 0) {
+ /* we have an exact match in the list.
+ don't list it at the end. */
+ array_delete(&actx->boxes, idx, 1);
+ array_delete(&actx->box_sets, idx, 1);
+ }
+ if ((match & AUTOCREATE_MATCH_RESULT_CHILDREN) != 0 &&
+ hash_table_lookup(actx->duplicate_vnames, info->vname) == NULL) {
+ /* Prevent autocreate-iteration from adding this
+ mailbox as a duplicate. For example we're listing %
+ and we're here because "foo" was found. However,
+ there's also "foo/bar" with auto=create. We're
+ telling here to the autocreate iteration code that
+ "foo" was already found and it doesn't need to add
+ it again. */
+ char *vname = p_strdup(ctx->pool, info->vname);
+ hash_table_insert(actx->duplicate_vnames, vname, vname);
+ }
+ }
+
+ if ((match & AUTOCREATE_MATCH_RESULT_CHILDREN) != 0) {
+ if ((ctx->flags & MAILBOX_LIST_ITER_SELECT_SUBSCRIBED) != 0)
+ info->flags |= MAILBOX_CHILD_SUBSCRIBED;
+ else {
+ info->flags &= ENUM_NEGATE(MAILBOX_NOCHILDREN);
+ info->flags |= MAILBOX_CHILDREN;
+ }
+ }
+
+ /* make sure the mailbox existence flags are correct. */
+ if ((ctx->flags & MAILBOX_LIST_ITER_SELECT_SUBSCRIBED) == 0)
+ match2 = match;
+ else {
+ match2 = autocreate_box_match(&actx->all_ns_box_sets,
+ ctx->list->ns, info->vname,
+ FALSE, &idx);
+ }
+ if ((match2 & AUTOCREATE_MATCH_RESULT_YES) != 0)
+ info->flags &= ENUM_NEGATE(MAILBOX_NONEXISTENT);
+ if ((match2 & AUTOCREATE_MATCH_RESULT_CHILDREN) != 0) {
+ info->flags &= ENUM_NEGATE(MAILBOX_NOCHILDREN);
+ info->flags |= MAILBOX_CHILDREN;
+ }
+
+ if ((ctx->flags & MAILBOX_LIST_ITER_SELECT_SUBSCRIBED) == 0 &&
+ (ctx->flags & MAILBOX_LIST_ITER_RETURN_SUBSCRIBED) != 0) {
+ /* we're listing all mailboxes and want \Subscribed flag */
+ match2 = autocreate_box_match(&actx->all_ns_box_sets,
+ ctx->list->ns, info->vname,
+ TRUE, &idx);
+ if ((match2 & AUTOCREATE_MATCH_RESULT_YES) != 0) {
+ /* mailbox is also marked as autosubscribe */
+ info->flags |= MAILBOX_SUBSCRIBED;
+ }
+ if ((match2 & AUTOCREATE_MATCH_RESULT_CHILDREN) != 0) {
+ /* mailbox also has a children marked as
+ autosubscribe */
+ info->flags |= MAILBOX_CHILD_SUBSCRIBED;
+ }
+ }
+
+ if ((match & AUTOCREATE_MATCH_RESULT_PARENT) != 0) {
+ /* there are autocreate parent boxes.
+ set their children flag states. */
+ struct autocreate_box *autobox;
+ size_t name_len;
+ char sep = mail_namespace_get_sep(ctx->list->ns);
+
+ array_foreach_modifiable(&actx->boxes, autobox) {
+ name_len = strlen(autobox->name);
+ if (!str_begins(info->vname, autobox->name) ||
+ info->vname[name_len] != sep)
+ continue;
+
+ if ((info->flags & MAILBOX_NONEXISTENT) == 0)
+ autobox->flags |= MAILBOX_CHILDREN;
+ if ((info->flags & MAILBOX_SUBSCRIBED) != 0)
+ autobox->flags |= MAILBOX_CHILD_SUBSCRIBED;
+ autobox->child_listed = TRUE;
+ }
+ }
+ return info;
+}
+
+static bool autocreate_iter_autobox(struct mailbox_list_iterate_context *ctx,
+ const struct autocreate_box *autobox)
+{
+ struct mailbox_list_autocreate_iterate_context *actx =
+ ctx->autocreate_ctx;
+ enum imap_match_result match;
+
+ i_zero(&actx->new_info);
+ actx->new_info.ns = ctx->list->ns;
+ actx->new_info.vname = autobox->name;
+ actx->new_info.flags = autobox->flags;
+
+ if ((ctx->flags & MAILBOX_LIST_ITER_SELECT_SUBSCRIBED) != 0)
+ actx->new_info.flags |= MAILBOX_SUBSCRIBED;
+
+ if ((actx->new_info.flags & MAILBOX_CHILDREN) == 0) {
+ if ((ctx->list->flags & MAILBOX_LIST_FLAG_MAILBOX_FILES) != 0 &&
+ ctx->list->set.maildir_name[0] == '\0') {
+ /* mailbox format using files (e.g. mbox)
+ without DIRNAME specified */
+ actx->new_info.flags |= MAILBOX_NOINFERIORS;
+ } else {
+ actx->new_info.flags |= MAILBOX_NOCHILDREN;
+ }
+ }
+
+ match = imap_match(ctx->glob, actx->new_info.vname);
+ if (match == IMAP_MATCH_YES) {
+ actx->new_info.special_use =
+ *autobox->set->special_use == '\0' ? NULL :
+ autobox->set->special_use;
+ return TRUE;
+ }
+ if ((match & IMAP_MATCH_PARENT) != 0 && !autobox->child_listed) {
+ enum mailbox_info_flags old_flags = actx->new_info.flags;
+ char sep = mail_namespace_get_sep(ctx->list->ns);
+ const char *p;
+ char *vname;
+
+ /* e.g. autocreate=foo/bar and we're listing % */
+ actx->new_info.flags = MAILBOX_NONEXISTENT |
+ (old_flags & (MAILBOX_CHILDREN |
+ MAILBOX_CHILD_SUBSCRIBED));
+ if ((old_flags & MAILBOX_NONEXISTENT) == 0) {
+ actx->new_info.flags |= MAILBOX_CHILDREN;
+ actx->new_info.flags &= ENUM_NEGATE(MAILBOX_NOCHILDREN);
+ }
+ if ((old_flags & MAILBOX_SUBSCRIBED) != 0)
+ actx->new_info.flags |= MAILBOX_CHILD_SUBSCRIBED;
+ do {
+ p = strrchr(actx->new_info.vname, sep);
+ i_assert(p != NULL);
+ actx->new_info.vname = vname =
+ p_strdup_until(ctx->pool,
+ actx->new_info.vname, p);
+ match = imap_match(ctx->glob, actx->new_info.vname);
+ } while (match != IMAP_MATCH_YES);
+
+ if (hash_table_lookup(actx->duplicate_vnames, vname) == NULL) {
+ hash_table_insert(actx->duplicate_vnames, vname, vname);
+ return TRUE;
+ }
+ }
+ return FALSE;
+}
+
+static const struct mailbox_info *
+mailbox_list_iter_next_call(struct mailbox_list_iterate_context *ctx)
+{
+ const struct mailbox_info *info;
+ const struct mailbox_settings *set;
+
+ info = ctx->list->v.iter_next(ctx);
+ if (info == NULL)
+ return NULL;
+
+ ctx->list->ns->flags |= NAMESPACE_FLAG_USABLE;
+ if ((ctx->flags & MAILBOX_LIST_ITER_RETURN_SPECIALUSE) != 0) {
+ set = mailbox_settings_find(ctx->list->ns, info->vname);
+ if (set != NULL && *set->special_use != '\0') {
+ ctx->specialuse_info = *info;
+ ctx->specialuse_info.special_use =
+ *set->special_use == '\0' ? NULL :
+ set->special_use;
+ info = &ctx->specialuse_info;
+ }
+ }
+
+ return mailbox_list_iter_autocreate_filter(ctx, info);
+}
+
+const struct mailbox_info *
+mailbox_list_iter_default_next(struct mailbox_list_iterate_context *ctx)
+{
+ struct mailbox_list_autocreate_iterate_context *actx =
+ ctx->autocreate_ctx;
+ const struct autocreate_box *autoboxes, *autobox;
+ unsigned int count;
+
+ if (actx == NULL)
+ return NULL;
+
+ /* do not drop boxes anymore */
+ actx->listing_autoboxes = TRUE;
+
+ /* list missing mailboxes */
+ autoboxes = array_get(&actx->boxes, &count);
+ while (actx->idx < count) {
+ autobox = &autoboxes[actx->idx++];
+ if (autocreate_iter_autobox(ctx, autobox))
+ return &actx->new_info;
+ }
+ i_assert(array_count(&actx->boxes) == array_count(&actx->box_sets));
+ return NULL;
+}
+
+static bool
+special_use_selection(struct mailbox_list_iterate_context *ctx,
+ const struct mailbox_info *info)
+{
+ if ((ctx->flags & MAILBOX_LIST_ITER_SELECT_RECURSIVEMATCH) != 0 &&
+ (ctx->flags & MAILBOX_LIST_ITER_SELECT_SPECIALUSE) != 0) {
+ /* LIST (SPECIAL-USE RECURSIVEMATCH) used. for now we support
+ this only for namespace prefixes */
+ if ((info->flags & MAILBOX_CHILD_SPECIALUSE) != 0)
+ return TRUE;
+ }
+ return (ctx->flags & MAILBOX_LIST_ITER_SELECT_SPECIALUSE) == 0 ||
+ info->special_use != NULL;
+}
+
+const struct mailbox_info *
+mailbox_list_iter_next(struct mailbox_list_iterate_context *ctx)
+{
+ const struct mailbox_info *info;
+
+ if (ctx == &mailbox_list_iter_failed)
+ return NULL;
+ do {
+ T_BEGIN {
+ info = mailbox_list_iter_next_call(ctx);
+ } T_END;
+ } while (info != NULL && !special_use_selection(ctx, info));
+ return info;
+}
+
+int mailbox_list_iter_deinit(struct mailbox_list_iterate_context **_ctx)
+{
+ struct mailbox_list_iterate_context *ctx = *_ctx;
+
+ *_ctx = NULL;
+
+ if (ctx == &mailbox_list_iter_failed)
+ return -1;
+ if (ctx->autocreate_ctx != NULL)
+ hash_table_destroy(&ctx->autocreate_ctx->duplicate_vnames);
+ return ctx->list->v.iter_deinit(ctx);
+}
+
+static void node_fix_parents(struct mailbox_node *node)
+{
+ /* If we happened to create any of the parents, we need to mark them
+ nonexistent. */
+ node = node->parent;
+ for (; node != NULL; node = node->parent) {
+ if ((node->flags & MAILBOX_MATCHED) == 0)
+ node->flags |= MAILBOX_NONEXISTENT;
+ }
+}
+
+static void
+mailbox_list_iter_update_real(struct mailbox_list_iter_update_context *ctx,
+ const char *name)
+{
+ struct mail_namespace *ns = ctx->iter_ctx->list->ns;
+ struct mailbox_node *node;
+ enum mailbox_info_flags create_flags, always_flags;
+ enum imap_match_result match;
+ const char *p;
+ bool created, add_matched;
+
+ create_flags = MAILBOX_NOCHILDREN;
+ always_flags = ctx->leaf_flags;
+ add_matched = TRUE;
+
+ for (;;) {
+ created = FALSE;
+ match = imap_match(ctx->glob, name);
+ if (match == IMAP_MATCH_YES) {
+ node = ctx->update_only ?
+ mailbox_tree_lookup(ctx->tree_ctx, name) :
+ mailbox_tree_get(ctx->tree_ctx, name, &created);
+ if (created) {
+ node->flags = create_flags;
+ if (create_flags != 0)
+ node_fix_parents(node);
+ }
+ if (node != NULL) {
+ if (!ctx->update_only && add_matched)
+ node->flags |= MAILBOX_MATCHED;
+ if ((always_flags & MAILBOX_CHILDREN) != 0)
+ node->flags &= ENUM_NEGATE(MAILBOX_NOCHILDREN);
+ node->flags |= always_flags;
+ }
+ /* We don't want to show the parent mailboxes unless
+ something else matches them, but if they are matched
+ we want to show them having child subscriptions */
+ add_matched = FALSE;
+ } else {
+ if ((match & IMAP_MATCH_PARENT) == 0)
+ break;
+ /* We've a (possibly) non-subscribed parent mailbox
+ which has a subscribed child mailbox. Make sure we
+ return the parent mailbox. */
+ }
+
+ if (!ctx->match_parents)
+ break;
+
+ /* see if parent matches */
+ p = strrchr(name, mail_namespace_get_sep(ns));
+ if (p == NULL)
+ break;
+
+ name = t_strdup_until(name, p);
+ create_flags |= MAILBOX_NONEXISTENT;
+ create_flags &= ENUM_NEGATE(MAILBOX_NOCHILDREN);
+ always_flags = MAILBOX_CHILDREN | ctx->parent_flags;
+ }
+}
+
+void mailbox_list_iter_update(struct mailbox_list_iter_update_context *ctx,
+ const char *name)
+{
+ T_BEGIN {
+ mailbox_list_iter_update_real(ctx, name);
+ } T_END;
+}
diff --git a/src/lib-storage/list/mailbox-list-maildir-iter.c b/src/lib-storage/list/mailbox-list-maildir-iter.c
new file mode 100644
index 0000000..df5fdd7
--- /dev/null
+++ b/src/lib-storage/list/mailbox-list-maildir-iter.c
@@ -0,0 +1,524 @@
+/* Copyright (c) 2002-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "str.h"
+#include "ioloop.h"
+#include "unlink-directory.h"
+#include "unichar.h"
+#include "imap-match.h"
+#include "imap-utf7.h"
+#include "mailbox-tree.h"
+#include "mailbox-list-delete.h"
+#include "mailbox-list-subscriptions.h"
+#include "mailbox-list-maildir.h"
+
+#include <stdio.h>
+#include <dirent.h>
+#include <sys/stat.h>
+
+struct maildir_list_iterate_context {
+ struct mailbox_list_iterate_context ctx;
+
+ const char *dir;
+ char prefix_char;
+
+ struct mailbox_tree_context *tree_ctx;
+ struct mailbox_tree_iterate_context *tree_iter;
+
+ struct mailbox_info info;
+};
+
+static void node_fix_parents(struct mailbox_node *node)
+{
+ /* Fix parent nodes' children states. also if we happened to create any
+ of the parents, we need to mark them nonexistent. */
+ node = node->parent;
+ for (; node != NULL; node = node->parent) {
+ if ((node->flags & MAILBOX_MATCHED) == 0)
+ node->flags |= MAILBOX_NONEXISTENT;
+
+ node->flags |= MAILBOX_CHILDREN;
+ node->flags &= ENUM_NEGATE(MAILBOX_NOCHILDREN);
+ }
+}
+
+static void
+maildir_fill_parents(struct maildir_list_iterate_context *ctx,
+ struct imap_match_glob *glob, bool update_only,
+ const char *vname)
+{
+ struct mail_namespace *ns = ctx->ctx.list->ns;
+ struct mailbox_node *node;
+ const char *p;
+ size_t vname_len = strlen(vname);
+ bool created;
+ char ns_sep = mail_namespace_get_sep(ns);
+
+ while ((p = strrchr(vname, ns_sep)) != NULL) {
+ vname = t_strdup_until(vname, p);
+ if (imap_match(glob, vname) != IMAP_MATCH_YES)
+ continue;
+
+ if (ns->prefix_len > 0 && vname_len == ns->prefix_len-1 &&
+ strncmp(vname, ns->prefix, ns->prefix_len - 1) == 0 &&
+ vname[ns->prefix_len-1] == ns_sep) {
+ /* don't return matches to namespace prefix itself */
+ continue;
+ }
+
+ created = FALSE;
+ node = update_only ?
+ mailbox_tree_lookup(ctx->tree_ctx, vname) :
+ mailbox_tree_get(ctx->tree_ctx, vname, &created);
+ if (node != NULL) {
+ if (created) {
+ /* we haven't yet seen this mailbox,
+ but we might see it later */
+ node->flags = MAILBOX_NONEXISTENT;
+ }
+ if (!update_only)
+ node->flags |= MAILBOX_MATCHED;
+ node->flags |= MAILBOX_CHILDREN;
+ node->flags &= ENUM_NEGATE(MAILBOX_NOCHILDREN);
+ node_fix_parents(node);
+ }
+ }
+}
+
+static void maildir_set_children(struct maildir_list_iterate_context *ctx,
+ const char *vname)
+{
+ struct mailbox_node *node;
+ const char *p;
+ char hierarchy_sep;
+
+ hierarchy_sep = mail_namespace_get_sep(ctx->ctx.list->ns);
+
+ /* mark the first existing parent as containing children */
+ while ((p = strrchr(vname, hierarchy_sep)) != NULL) {
+ vname = t_strdup_until(vname, p);
+
+ node = mailbox_tree_lookup(ctx->tree_ctx, vname);
+ if (node != NULL) {
+ node->flags &= ENUM_NEGATE(MAILBOX_NOCHILDREN);
+ node->flags |= MAILBOX_CHILDREN;
+ break;
+ }
+ }
+}
+
+static int
+maildir_fill_inbox(struct maildir_list_iterate_context *ctx,
+ struct imap_match_glob *glob, const char *inbox_name,
+ bool update_only)
+{
+ struct mailbox_node *node;
+ enum mailbox_info_flags flags;
+ enum imap_match_result match;
+ bool created;
+ int ret;
+
+ if ((ctx->ctx.flags & MAILBOX_LIST_ITER_NO_AUTO_BOXES) == 0) {
+ /* always show INBOX */
+ } else {
+ /* INBOX may be Maildir root or completely elsewhere.
+ show it only if it has already been created */
+ ret = mailbox_list_mailbox(ctx->ctx.list, "INBOX", &flags);
+ if (ret < 0)
+ return -1;
+ if ((flags & MAILBOX_NONEXISTENT) != 0)
+ update_only = TRUE;
+ }
+
+ if (update_only) {
+ node = mailbox_tree_lookup(ctx->tree_ctx, inbox_name);
+ if (node != NULL)
+ node->flags &= ENUM_NEGATE(MAILBOX_NONEXISTENT);
+ return 0;
+ }
+
+ /* add the INBOX only if it matches the patterns */
+ match = imap_match(glob, inbox_name);
+ if (match == IMAP_MATCH_PARENT)
+ maildir_fill_parents(ctx, glob, FALSE, inbox_name);
+ else if (match == IMAP_MATCH_YES) {
+ node = mailbox_tree_get(ctx->tree_ctx, inbox_name, &created);
+ if (created)
+ node->flags = MAILBOX_NOCHILDREN;
+ else
+ node->flags &= ENUM_NEGATE(MAILBOX_NONEXISTENT);
+ node->flags |= MAILBOX_MATCHED;
+ }
+ return 0;
+}
+
+static bool
+maildir_get_type(const char *dir, const char *fname,
+ enum mailbox_list_file_type *type_r,
+ enum mailbox_info_flags *flags)
+{
+ const char *path;
+ struct stat st;
+
+ path = *fname == '\0' ? dir :
+ t_strdup_printf("%s/%s", dir, fname);
+ if (stat(path, &st) < 0) {
+ if (errno == ENOENT) {
+ /* just deleted? */
+ *flags |= MAILBOX_NONEXISTENT;
+ } else {
+ *flags |= MAILBOX_NOSELECT;
+ }
+ return FALSE;
+ }
+
+ if (S_ISDIR(st.st_mode)) {
+ *type_r = MAILBOX_LIST_FILE_TYPE_DIR;
+ return TRUE;
+ } else {
+ if (str_begins(fname, ".nfs"))
+ *flags |= MAILBOX_NONEXISTENT;
+ else
+ *flags |= MAILBOX_NOSELECT;
+ return FALSE;
+ }
+}
+
+int maildir_list_get_mailbox_flags(struct mailbox_list *list,
+ const char *dir, const char *fname,
+ enum mailbox_list_file_type type,
+ enum mailbox_info_flags *flags_r)
+{
+ *flags_r = 0;
+
+ switch (type) {
+ case MAILBOX_LIST_FILE_TYPE_DIR:
+ case MAILBOX_LIST_FILE_TYPE_FILE:
+ case MAILBOX_LIST_FILE_TYPE_OTHER:
+ break;
+ case MAILBOX_LIST_FILE_TYPE_UNKNOWN:
+ case MAILBOX_LIST_FILE_TYPE_SYMLINK:
+ /* need to check with stat() to be sure */
+ if (!list->mail_set->maildir_stat_dirs && *fname != '\0' &&
+ strcmp(list->name, MAILBOX_LIST_NAME_MAILDIRPLUSPLUS) == 0 &&
+ !str_begins(fname, ".nfs")) {
+ /* just assume it's a valid mailbox */
+ return 1;
+ }
+
+ if (!maildir_get_type(dir, fname, &type, flags_r))
+ return 0;
+ break;
+ }
+
+ switch (type) {
+ case MAILBOX_LIST_FILE_TYPE_DIR:
+ if ((list->flags & MAILBOX_LIST_FLAG_MAILBOX_FILES) != 0) {
+ *flags_r |= MAILBOX_NOSELECT;
+ return 0;
+ }
+ break;
+ case MAILBOX_LIST_FILE_TYPE_FILE:
+ if ((list->flags & MAILBOX_LIST_FLAG_MAILBOX_FILES) == 0) {
+ *flags_r |= MAILBOX_NOSELECT;
+ return 0;
+ }
+ break;
+ case MAILBOX_LIST_FILE_TYPE_OTHER:
+ *flags_r |= MAILBOX_NOSELECT;
+ return 0;
+
+ case MAILBOX_LIST_FILE_TYPE_UNKNOWN:
+ case MAILBOX_LIST_FILE_TYPE_SYMLINK:
+ i_unreached();
+ }
+ if (*fname != '\0') {
+ /* this tells maildir storage code that it doesn't need to
+ see if cur/ exists, because just the existence of .dir/
+ assumes that the mailbox exists. */
+ *flags_r |= MAILBOX_SELECT;
+ }
+ return 1;
+}
+
+static bool maildir_delete_trash_dir(struct maildir_list_iterate_context *ctx,
+ const char *fname)
+{
+ const char *path, *error;
+ struct stat st;
+
+ if (fname[1] != ctx->prefix_char || ctx->prefix_char == '\0' ||
+ strcmp(fname+2, MAILBOX_LIST_MAILDIR_TRASH_DIR_NAME) != 0)
+ return FALSE;
+
+ /* this directory is in the middle of being deleted, or the process
+ trying to delete it had died. delete it ourself if it's been there
+ longer than one hour. */
+ path = t_strdup_printf("%s/%s", ctx->dir, fname);
+ if (stat(path, &st) == 0 &&
+ st.st_mtime < ioloop_time - 3600)
+ (void)mailbox_list_delete_trash(path, &error);
+
+ return TRUE;
+}
+
+static int
+maildir_fill_readdir_entry(struct maildir_list_iterate_context *ctx,
+ struct imap_match_glob *glob, const struct dirent *d,
+ bool update_only)
+{
+ struct mailbox_list *list = ctx->ctx.list;
+ const char *fname, *storage_name, *vname;
+ enum mailbox_info_flags flags;
+ enum imap_match_result match;
+ struct mailbox_node *node;
+ bool created;
+ int ret;
+
+ fname = d->d_name;
+ if (fname[0] == ctx->prefix_char)
+ storage_name = fname + 1;
+ else {
+ if (ctx->prefix_char != '\0' || fname[0] == '.')
+ return 0;
+ storage_name = fname;
+ }
+
+ /* skip . and .. */
+ if (fname[0] == '.' &&
+ (fname[1] == '\0' || (fname[1] == '.' && fname[2] == '\0')))
+ return 0;
+
+ vname = mailbox_list_get_vname(list, storage_name);
+ if (!uni_utf8_str_is_valid(vname)) {
+ /* the storage_name is completely invalid, rename it to
+ something more sensible. we could do this for all names that
+ aren't valid mUTF-7, but that might lead to accidents in
+ future when UTF-8 storage names are used */
+ const char *src = t_strdup_printf("%s/%s", ctx->dir, fname);
+ string_t *destvname = t_str_new(128);
+ string_t *dest = t_str_new(128);
+
+ if (uni_utf8_get_valid_data((const void *)fname,
+ strlen(fname), destvname))
+ i_unreached(); /* already checked that it was invalid */
+
+ str_append(dest, ctx->dir);
+ str_append_c(dest, '/');
+ (void)imap_utf8_to_utf7(str_c(destvname), dest);
+
+ if (rename(src, str_c(dest)) < 0 && errno != ENOENT)
+ e_error(ctx->ctx.list->ns->user->event,
+ "rename(%s, %s) failed: %m", src, str_c(dest));
+ /* just skip this in this iteration, we'll see it on the
+ next list */
+ return 0;
+ }
+
+ /* make sure the pattern matches */
+ match = imap_match(glob, vname);
+ if ((match & (IMAP_MATCH_YES | IMAP_MATCH_PARENT)) == 0)
+ return 0;
+
+ /* check if this is an actual mailbox */
+ if (maildir_delete_trash_dir(ctx, fname))
+ return 0;
+
+ if ((ctx->ctx.flags & MAILBOX_LIST_ITER_SKIP_ALIASES) != 0) {
+ ret = mailbox_list_dirent_is_alias_symlink(list, ctx->dir, d);
+ if (ret != 0)
+ return ret < 0 ? -1 : 0;
+ }
+ T_BEGIN {
+ ret = list->v.get_mailbox_flags(list, ctx->dir, fname,
+ mailbox_list_get_file_type(d), &flags);
+ } T_END;
+ if (ret <= 0)
+ return ret;
+
+ /* we know the children flags ourself, so ignore if any of
+ them were set. */
+ flags &= ENUM_NEGATE(MAILBOX_NOINFERIORS | MAILBOX_CHILDREN | MAILBOX_NOCHILDREN);
+
+ if ((match & IMAP_MATCH_PARENT) != 0)
+ maildir_fill_parents(ctx, glob, update_only, vname);
+ else {
+ created = FALSE;
+ node = update_only ?
+ mailbox_tree_lookup(ctx->tree_ctx, vname) :
+ mailbox_tree_get(ctx->tree_ctx, vname, &created);
+
+ if (node != NULL) {
+ if (created)
+ node->flags = MAILBOX_NOCHILDREN;
+ else
+ node->flags &= ENUM_NEGATE(MAILBOX_NONEXISTENT);
+ if (!update_only)
+ node->flags |= MAILBOX_MATCHED;
+ node->flags |= flags;
+ node_fix_parents(node);
+ } else {
+ i_assert(update_only);
+ maildir_set_children(ctx, vname);
+ }
+ }
+ return 0;
+}
+
+static int
+maildir_fill_readdir(struct maildir_list_iterate_context *ctx,
+ struct imap_match_glob *glob, bool update_only)
+{
+ struct mailbox_list *list = ctx->ctx.list;
+ struct mail_namespace *ns = list->ns;
+ DIR *dirp;
+ struct dirent *d;
+ const char *vname;
+ int ret = 0;
+
+ dirp = opendir(ctx->dir);
+ if (dirp == NULL) {
+ if (errno == EACCES) {
+ mailbox_list_set_critical(list, "%s",
+ mail_error_eacces_msg("opendir", ctx->dir));
+ } else if (errno != ENOENT) {
+ mailbox_list_set_critical(list,
+ "opendir(%s) failed: %m", ctx->dir);
+ return -1;
+ }
+ return 0;
+ }
+
+ while ((d = readdir(dirp)) != NULL) {
+ T_BEGIN {
+ ret = maildir_fill_readdir_entry(ctx, glob, d,
+ update_only);
+ } T_END;
+ if (ret < 0)
+ break;
+ }
+
+ if (closedir(dirp) < 0) {
+ mailbox_list_set_critical(list, "readdir(%s) failed: %m",
+ ctx->dir);
+ return -1;
+ }
+ if (ret < 0)
+ return -1;
+
+ if ((ns->flags & NAMESPACE_FLAG_INBOX_USER) != 0) {
+ /* make sure INBOX is listed */
+ return maildir_fill_inbox(ctx, glob, "INBOX", update_only);
+ } else if ((ns->flags & NAMESPACE_FLAG_INBOX_ANY) != 0) {
+ /* show shared INBOX. */
+ vname = mailbox_list_get_vname(ns->list, "INBOX");
+ return maildir_fill_inbox(ctx, glob, vname, update_only);
+ } else {
+ return 0;
+ }
+}
+
+struct mailbox_list_iterate_context *
+maildir_list_iter_init(struct mailbox_list *_list, const char *const *patterns,
+ enum mailbox_list_iter_flags flags)
+{
+ struct maildir_mailbox_list *list =
+ (struct maildir_mailbox_list *)_list;
+ struct maildir_list_iterate_context *ctx;
+ pool_t pool;
+ char ns_sep = mail_namespace_get_sep(_list->ns);
+ int ret;
+
+ pool = pool_alloconly_create("mailbox list maildir iter", 1024);
+ ctx = p_new(pool, struct maildir_list_iterate_context, 1);
+ ctx->ctx.pool = pool;
+ ctx->ctx.list = _list;
+ ctx->ctx.flags = flags;
+ ctx->ctx.glob = imap_match_init_multiple(pool, patterns, TRUE, ns_sep);
+ array_create(&ctx->ctx.module_contexts, pool, sizeof(void *), 5);
+
+ ctx->tree_ctx = mailbox_tree_init(ns_sep);
+ ctx->info.ns = _list->ns;
+ ctx->prefix_char = strcmp(_list->name, MAILBOX_LIST_NAME_IMAPDIR) == 0 ?
+ '\0' : list->sep;
+
+ if (_list->set.iter_from_index_dir)
+ ctx->dir = _list->set.index_dir;
+ else
+ ctx->dir = _list->set.root_dir;
+
+ if ((flags & MAILBOX_LIST_ITER_SELECT_SUBSCRIBED) != 0) {
+ /* Listing only subscribed mailboxes.
+ Flags are set later if needed. */
+ bool default_nonexistent =
+ (flags & MAILBOX_LIST_ITER_RETURN_NO_FLAGS) == 0;
+
+ mailbox_list_subscriptions_fill(&ctx->ctx, ctx->tree_ctx,
+ default_nonexistent);
+ }
+
+ if ((flags & MAILBOX_LIST_ITER_SELECT_SUBSCRIBED) == 0 ||
+ (flags & MAILBOX_LIST_ITER_RETURN_NO_FLAGS) == 0) {
+ /* Add/update mailbox list with flags */
+ bool update_only =
+ (flags & MAILBOX_LIST_ITER_SELECT_SUBSCRIBED) != 0;
+
+ T_BEGIN {
+ ret = maildir_fill_readdir(ctx, ctx->ctx.glob,
+ update_only);
+ } T_END;
+ if (ret < 0) {
+ ctx->ctx.failed = TRUE;
+ return &ctx->ctx;
+ }
+ }
+
+ ctx->tree_iter = mailbox_tree_iterate_init(ctx->tree_ctx, NULL,
+ MAILBOX_MATCHED);
+ return &ctx->ctx;
+}
+
+int maildir_list_iter_deinit(struct mailbox_list_iterate_context *_ctx)
+{
+ struct maildir_list_iterate_context *ctx =
+ (struct maildir_list_iterate_context *)_ctx;
+ int ret = _ctx->failed ? -1 : 0;
+
+ if (ctx->tree_iter != NULL)
+ mailbox_tree_iterate_deinit(&ctx->tree_iter);
+ mailbox_tree_deinit(&ctx->tree_ctx);
+ pool_unref(&ctx->ctx.pool);
+ return ret;
+}
+
+const struct mailbox_info *
+maildir_list_iter_next(struct mailbox_list_iterate_context *_ctx)
+{
+ struct maildir_list_iterate_context *ctx =
+ (struct maildir_list_iterate_context *)_ctx;
+ struct mailbox_node *node;
+
+ if (_ctx->failed)
+ return NULL;
+
+ node = mailbox_tree_iterate_next(ctx->tree_iter, &ctx->info.vname);
+ if (node == NULL)
+ return mailbox_list_iter_default_next(_ctx);
+
+ ctx->info.flags = node->flags;
+ if (strcmp(ctx->info.vname, "INBOX") == 0 &&
+ mail_namespace_is_inbox_noinferiors(ctx->info.ns)) {
+ i_assert((ctx->info.flags & MAILBOX_NOCHILDREN) != 0);
+ ctx->info.flags &= ENUM_NEGATE(MAILBOX_NOCHILDREN);
+ ctx->info.flags |= MAILBOX_NOINFERIORS;
+ }
+ if ((_ctx->flags & MAILBOX_LIST_ITER_RETURN_SUBSCRIBED) != 0 &&
+ (_ctx->flags & MAILBOX_LIST_ITER_SELECT_SUBSCRIBED) == 0) {
+ /* we're listing all mailboxes but we want to know
+ \Subscribed flags */
+ mailbox_list_set_subscription_flags(_ctx->list, ctx->info.vname,
+ &ctx->info.flags);
+ }
+ return &ctx->info;
+}
diff --git a/src/lib-storage/list/mailbox-list-maildir.c b/src/lib-storage/list/mailbox-list-maildir.c
new file mode 100644
index 0000000..265f3d0
--- /dev/null
+++ b/src/lib-storage/list/mailbox-list-maildir.c
@@ -0,0 +1,546 @@
+/* Copyright (c) 2006-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "hostpid.h"
+#include "eacces-error.h"
+#include "mkdir-parents.h"
+#include "str.h"
+#include "subscription-file.h"
+#include "mailbox-list-subscriptions.h"
+#include "mailbox-list-delete.h"
+#include "mailbox-list-maildir.h"
+
+#include <stdio.h>
+#include <sys/stat.h>
+
+#define MAILDIR_GLOBAL_TEMP_PREFIX "temp."
+#define IMAPDIR_GLOBAL_TEMP_PREFIX ".temp."
+
+extern struct mailbox_list maildir_mailbox_list;
+extern struct mailbox_list imapdir_mailbox_list;
+
+static struct mailbox_list *maildir_list_alloc(void)
+{
+ struct maildir_mailbox_list *list;
+ pool_t pool;
+
+ pool = pool_alloconly_create("maildir++ list", 2048);
+ list = p_new(pool, struct maildir_mailbox_list, 1);
+ list->list = maildir_mailbox_list;
+ list->list.pool = pool;
+ list->sep = '.';
+
+ list->global_temp_prefix = MAILDIR_GLOBAL_TEMP_PREFIX;
+ list->temp_prefix = p_strconcat(pool, list->global_temp_prefix,
+ my_hostname, ".", my_pid, ".", NULL);
+ return &list->list;
+}
+
+static struct mailbox_list *imapdir_list_alloc(void)
+{
+ struct maildir_mailbox_list *list;
+ pool_t pool;
+
+ pool = pool_alloconly_create("imapdir list", 1024);
+ list = p_new(pool, struct maildir_mailbox_list, 1);
+ list->list = imapdir_mailbox_list;
+ list->list.pool = pool;
+ list->sep = '.';
+
+ list->global_temp_prefix = IMAPDIR_GLOBAL_TEMP_PREFIX;
+ list->temp_prefix = p_strconcat(pool, list->global_temp_prefix,
+ my_hostname, ".", my_pid, ".", NULL);
+ return &list->list;
+}
+
+static void maildir_list_deinit(struct mailbox_list *_list)
+{
+ struct maildir_mailbox_list *list =
+ (struct maildir_mailbox_list *)_list;
+
+ pool_unref(&list->list.pool);
+}
+
+static const char *
+maildir_list_get_dirname_path(struct mailbox_list *list, const char *dir,
+ const char *name)
+{
+ if (*name == '\0')
+ return dir;
+ else if (strcmp(list->name, imapdir_mailbox_list.name) == 0)
+ return t_strdup_printf("%s/%s", dir, name);
+
+ return t_strdup_printf("%s/%c%s", dir,
+ mailbox_list_get_hierarchy_sep(list), name);
+}
+
+static const char *
+maildir_list_get_absolute_path(struct mailbox_list *list, const char *name)
+{
+ const char *p;
+
+ if (!mailbox_list_try_get_absolute_path(list, &name)) {
+ /* fallback to using as ~name */
+ return name;
+ }
+
+ p = strrchr(name, '/');
+ if (p == NULL)
+ return name;
+ return maildir_list_get_dirname_path(list, t_strdup_until(name, p),
+ p+1);
+}
+
+static char maildir_list_get_hierarchy_sep(struct mailbox_list *_list)
+{
+ struct maildir_mailbox_list *list =
+ (struct maildir_mailbox_list *)_list;
+
+ return list->sep;
+}
+
+static int
+maildir_list_get_path(struct mailbox_list *_list, const char *name,
+ enum mailbox_list_path_type type, const char **path_r)
+{
+ const char *root_dir;
+
+ if (name == NULL) {
+ /* return root directories */
+ return mailbox_list_set_get_root_path(&_list->set, type,
+ path_r) ? 1 : 0;
+ }
+
+ if (_list->mail_set->mail_full_filesystem_access &&
+ (*name == '/' || *name == '~')) {
+ *path_r = maildir_list_get_absolute_path(_list, name);
+ return 1;
+ }
+
+ root_dir = _list->set.root_dir;
+ switch (type) {
+ case MAILBOX_LIST_PATH_TYPE_DIR:
+ case MAILBOX_LIST_PATH_TYPE_MAILBOX:
+ break;
+ case MAILBOX_LIST_PATH_TYPE_ALT_DIR:
+ case MAILBOX_LIST_PATH_TYPE_ALT_MAILBOX:
+ if (_list->set.alt_dir == NULL)
+ return 0;
+ root_dir = _list->set.alt_dir;
+ break;
+ case MAILBOX_LIST_PATH_TYPE_CONTROL:
+ if (_list->set.control_dir != NULL) {
+ *path_r = maildir_list_get_dirname_path(_list,
+ _list->set.control_dir, name);
+ return 1;
+ }
+ break;
+ case MAILBOX_LIST_PATH_TYPE_INDEX_CACHE:
+ if (_list->set.index_cache_dir != NULL) {
+ *path_r = maildir_list_get_dirname_path(_list,
+ _list->set.index_cache_dir, name);
+ return 1;
+ }
+ /* fall through */
+ case MAILBOX_LIST_PATH_TYPE_INDEX:
+ if (_list->set.index_dir != NULL) {
+ if (*_list->set.index_dir == '\0')
+ return 0;
+ *path_r = maildir_list_get_dirname_path(_list,
+ _list->set.index_dir, name);
+ return 1;
+ }
+ break;
+ case MAILBOX_LIST_PATH_TYPE_INDEX_PRIVATE:
+ if (_list->set.index_pvt_dir == NULL)
+ return 0;
+ *path_r = maildir_list_get_dirname_path(_list,
+ _list->set.index_pvt_dir, name);
+ return 1;
+ case MAILBOX_LIST_PATH_TYPE_LIST_INDEX:
+ i_unreached();
+ }
+
+ if (type == MAILBOX_LIST_PATH_TYPE_ALT_DIR ||
+ type == MAILBOX_LIST_PATH_TYPE_ALT_MAILBOX) {
+ /* don't use inbox_path */
+ } else if (strcmp(name, "INBOX") == 0 && _list->set.inbox_path != NULL) {
+ *path_r = _list->set.inbox_path;
+ return 1;
+ }
+
+ *path_r = maildir_list_get_dirname_path(_list, root_dir, name);
+ return 1;
+}
+
+static const char *
+maildir_list_get_temp_prefix(struct mailbox_list *_list, bool global)
+{
+ struct maildir_mailbox_list *list =
+ (struct maildir_mailbox_list *)_list;
+
+ return global ? list->global_temp_prefix : list->temp_prefix;
+}
+
+static int maildir_list_set_subscribed(struct mailbox_list *_list,
+ const char *name, bool set)
+{
+ struct maildir_mailbox_list *list =
+ (struct maildir_mailbox_list *)_list;
+ const char *path;
+
+ if (_list->set.subscription_fname == NULL) {
+ mailbox_list_set_error(_list, MAIL_ERROR_NOTPOSSIBLE,
+ "Subscriptions not supported");
+ return -1;
+ }
+
+ path = t_strconcat(_list->set.control_dir != NULL ?
+ _list->set.control_dir : _list->set.root_dir,
+ "/", _list->set.subscription_fname, NULL);
+
+ return subsfile_set_subscribed(_list, path, list->temp_prefix,
+ name, set);
+}
+
+static const char *
+mailbox_list_maildir_get_trash_dir(struct mailbox_list *_list)
+{
+ struct maildir_mailbox_list *list =
+ (struct maildir_mailbox_list *)_list;
+ const char *root_dir;
+
+ root_dir = mailbox_list_get_root_forced(_list, MAILBOX_LIST_PATH_TYPE_DIR);
+ return t_strdup_printf("%s/%c%c"MAILBOX_LIST_MAILDIR_TRASH_DIR_NAME,
+ root_dir, list->sep, list->sep);
+}
+
+static int
+maildir_list_delete_maildir(struct mailbox_list *list, const char *name)
+{
+ const char *path, *trash_dir;
+ int ret = 0;
+
+ trash_dir = mailbox_list_maildir_get_trash_dir(list);
+ ret = mailbox_list_delete_maildir_via_trash(list, name, trash_dir);
+ if (ret < 0)
+ return -1;
+
+ if (ret == 0) {
+ /* we could actually use just unlink_directory()
+ but error handling is easier this way :) */
+ if (mailbox_list_get_path(list, name,
+ MAILBOX_LIST_PATH_TYPE_MAILBOX,
+ &path) <= 0)
+ i_unreached();
+ if (mailbox_list_delete_mailbox_nonrecursive(list, name,
+ path, TRUE) < 0)
+ return -1;
+ }
+ return 0;
+}
+
+static int
+maildir_list_delete_mailbox(struct mailbox_list *list, const char *name)
+{
+ const char *path;
+ int ret;
+
+ if ((list->flags & MAILBOX_LIST_FLAG_MAILBOX_FILES) != 0) {
+ ret = mailbox_list_get_path(list, name,
+ MAILBOX_LIST_PATH_TYPE_MAILBOX,
+ &path);
+ if (ret < 0)
+ return -1;
+ i_assert(ret > 0);
+ ret = mailbox_list_delete_mailbox_file(list, name, path);
+ } else {
+ ret = maildir_list_delete_maildir(list, name);
+ }
+
+ i_assert(ret <= 0);
+ return mailbox_list_delete_finish_ret(list, name, ret == 0);
+}
+
+static int maildir_list_delete_dir(struct mailbox_list *list, const char *name)
+{
+ const char *path;
+ struct stat st;
+
+ /* with maildir++ there aren't any non-selectable mailboxes.
+ we'll always fail. */
+ if (mailbox_list_get_path(list, name, MAILBOX_LIST_PATH_TYPE_DIR,
+ &path) <= 0)
+ i_unreached();
+ if (stat(path, &st) == 0) {
+ mailbox_list_set_error(list, MAIL_ERROR_EXISTS,
+ "Mailbox exists");
+ } else if (errno == ENOENT || errno == ENOTDIR) {
+ mailbox_list_set_error(list, MAIL_ERROR_NOTFOUND,
+ T_MAILBOX_LIST_ERR_NOT_FOUND(list, name));
+ } else {
+ mailbox_list_set_critical(list, "stat(%s) failed: %m", path);
+ }
+ return -1;
+}
+
+static int rename_dir(struct mailbox_list *oldlist, const char *oldname,
+ struct mailbox_list *newlist, const char *newname,
+ enum mailbox_list_path_type type)
+{
+ const char *oldpath, *newpath;
+
+ if (mailbox_list_get_path(oldlist, oldname, type, &oldpath) <= 0 ||
+ mailbox_list_get_path(newlist, newname, type, &newpath) <= 0)
+ return 0;
+
+ if (strcmp(oldpath, newpath) == 0)
+ return 0;
+
+ if (rename(oldpath, newpath) < 0 && errno != ENOENT) {
+ mailbox_list_set_critical(oldlist, "rename(%s, %s) failed: %m",
+ oldpath, newpath);
+ return -1;
+ }
+ return 0;
+}
+
+static int
+maildir_rename_children(struct mailbox_list *oldlist, const char *oldname,
+ struct mailbox_list *newlist, const char *newname)
+{
+ struct mailbox_list_iterate_context *iter;
+ const struct mailbox_info *info;
+ ARRAY(const char *) names_arr;
+ const char *pattern, *oldpath, *newpath, *old_childname, *new_childname;
+ const char *const *names, *old_vname, *new_vname;
+ unsigned int i, count;
+ size_t old_vnamelen;
+ pool_t pool;
+ char old_ns_sep;
+ int ret;
+
+ ret = 0;
+
+ /* first get the list of the children and save them to memory, because
+ we can't rely on readdir() not skipping files while the directory
+ is being modified. this doesn't protect against modifications by
+ other processes though. */
+ pool = pool_alloconly_create("Maildir++ children list", 1024);
+ i_array_init(&names_arr, 64);
+
+ old_vname = mailbox_list_get_vname(oldlist, oldname);
+ old_vnamelen = strlen(old_vname);
+
+ new_vname = mailbox_list_get_vname(newlist, newname);
+
+ old_ns_sep = mail_namespace_get_sep(oldlist->ns);
+ pattern = t_strdup_printf("%s%c*", old_vname, old_ns_sep);
+ iter = mailbox_list_iter_init(oldlist, pattern,
+ MAILBOX_LIST_ITER_RETURN_NO_FLAGS |
+ MAILBOX_LIST_ITER_RAW_LIST);
+ while ((info = mailbox_list_iter_next(iter)) != NULL) {
+ const char *name;
+
+ /* verify that the prefix matches, otherwise we could have
+ problems with mailbox names containing '%' and '*' chars */
+ if (strncmp(info->vname, old_vname, old_vnamelen) == 0 &&
+ info->vname[old_vnamelen] == old_ns_sep) {
+ name = p_strdup(pool, info->vname + old_vnamelen);
+ array_push_back(&names_arr, &name);
+ }
+ }
+ if (mailbox_list_iter_deinit(&iter) < 0) {
+ ret = -1;
+ names = NULL; count = 0;
+ } else {
+ names = array_get(&names_arr, &count);
+ }
+
+ for (i = 0; i < count; i++) {
+ old_childname = mailbox_list_get_storage_name(oldlist,
+ t_strconcat(old_vname, names[i], NULL));
+ if (strcmp(old_childname, new_vname) == 0) {
+ /* When doing RENAME "a" "a.b" we see "a.b" here.
+ We don't want to rename it anymore to "a.b.b". */
+ continue;
+ }
+
+ new_childname = mailbox_list_get_storage_name(newlist,
+ t_strconcat(new_vname, names[i], NULL));
+ if (mailbox_list_get_path(oldlist, old_childname,
+ MAILBOX_LIST_PATH_TYPE_MAILBOX,
+ &oldpath) <= 0 ||
+ mailbox_list_get_path(newlist, new_childname,
+ MAILBOX_LIST_PATH_TYPE_MAILBOX,
+ &newpath) <= 0)
+ i_unreached();
+
+ /* FIXME: it's possible to merge two mailboxes if either one of
+ them doesn't have existing root mailbox. We could check this
+ but I'm not sure if it's worth it. It could be even
+ considered as a feature.
+
+ Anyway, the bug with merging is that if both mailboxes have
+ identically named child mailbox they conflict. Just ignore
+ those and leave them under the old mailbox. */
+ if (rename(oldpath, newpath) == 0 || EDESTDIREXISTS(errno))
+ ret = 1;
+ else {
+ mailbox_list_set_critical(oldlist,
+ "rename(%s, %s) failed: %m", oldpath, newpath);
+ ret = -1;
+ break;
+ }
+
+ (void)rename_dir(oldlist, old_childname, newlist, new_childname,
+ MAILBOX_LIST_PATH_TYPE_CONTROL);
+ (void)rename_dir(oldlist, old_childname, newlist, new_childname,
+ MAILBOX_LIST_PATH_TYPE_INDEX);
+ (void)rename_dir(oldlist, old_childname, newlist, new_childname,
+ MAILBOX_LIST_PATH_TYPE_INDEX_CACHE);
+ }
+ array_free(&names_arr);
+ pool_unref(&pool);
+
+ return ret;
+}
+
+static int
+maildir_list_rename_mailbox(struct mailbox_list *oldlist, const char *oldname,
+ struct mailbox_list *newlist, const char *newname)
+{
+ const char *oldpath, *newpath, *root_path;
+ int ret;
+ bool found;
+
+ /* NOTE: it's possible to rename a nonexistent mailbox which has
+ children. In that case we should ignore the rename() error. */
+ if (mailbox_list_get_path(oldlist, oldname,
+ MAILBOX_LIST_PATH_TYPE_MAILBOX, &oldpath) <= 0 ||
+ mailbox_list_get_path(newlist, newname,
+ MAILBOX_LIST_PATH_TYPE_MAILBOX, &newpath) <= 0)
+ i_unreached();
+
+ root_path = mailbox_list_get_root_forced(oldlist,
+ MAILBOX_LIST_PATH_TYPE_MAILBOX);
+ if (strcmp(oldpath, root_path) == 0) {
+ /* most likely INBOX */
+ mailbox_list_set_error(oldlist, MAIL_ERROR_NOTPOSSIBLE,
+ t_strdup_printf("Renaming %s isn't supported.",
+ oldname));
+ return -1;
+ }
+
+ /* if we're renaming under another mailbox, require its permissions
+ to be same as ours. */
+ if (strchr(newname, mailbox_list_get_hierarchy_sep(newlist)) != NULL) {
+ struct mailbox_permissions old_perm, new_perm;
+
+ mailbox_list_get_permissions(oldlist, oldname, &old_perm);
+ mailbox_list_get_permissions(newlist, newname, &new_perm);
+
+ if ((new_perm.file_create_mode != old_perm.file_create_mode ||
+ new_perm.dir_create_mode != old_perm.dir_create_mode ||
+ new_perm.file_create_gid != old_perm.file_create_gid)) {
+ mailbox_list_set_error(oldlist, MAIL_ERROR_NOTPOSSIBLE,
+ "Renaming not supported across conflicting "
+ "directory permissions");
+ return -1;
+ }
+ }
+
+
+ ret = rename(oldpath, newpath);
+ if (ret == 0 || errno == ENOENT) {
+ (void)rename_dir(oldlist, oldname, newlist, newname,
+ MAILBOX_LIST_PATH_TYPE_CONTROL);
+ (void)rename_dir(oldlist, oldname, newlist, newname,
+ MAILBOX_LIST_PATH_TYPE_INDEX);
+ (void)rename_dir(oldlist, oldname, newlist, newname,
+ MAILBOX_LIST_PATH_TYPE_INDEX_CACHE);
+
+ found = ret == 0;
+ T_BEGIN {
+ ret = maildir_rename_children(oldlist, oldname,
+ newlist, newname);
+ } T_END;
+ if (ret < 0)
+ return -1;
+ if (!found && ret == 0) {
+ mailbox_list_set_error(oldlist, MAIL_ERROR_NOTFOUND,
+ T_MAILBOX_LIST_ERR_NOT_FOUND(oldlist, oldname));
+ return -1;
+ }
+
+ return 0;
+ }
+
+ if (EDESTDIREXISTS(errno)) {
+ mailbox_list_set_error(oldlist, MAIL_ERROR_EXISTS,
+ "Target mailbox already exists");
+ } else {
+ mailbox_list_set_critical(oldlist, "rename(%s, %s) failed: %m",
+ oldpath, newpath);
+ }
+ return -1;
+}
+
+struct mailbox_list maildir_mailbox_list = {
+ .name = MAILBOX_LIST_NAME_MAILDIRPLUSPLUS,
+ .props = MAILBOX_LIST_PROP_NO_MAILDIR_NAME |
+ MAILBOX_LIST_PROP_NO_ALT_DIR |
+ MAILBOX_LIST_PROP_NO_NOSELECT |
+ MAILBOX_LIST_PROP_NO_INTERNAL_NAMES,
+ .mailbox_name_max_length = MAILBOX_LIST_NAME_MAX_LENGTH,
+
+ .v = {
+ .alloc = maildir_list_alloc,
+ .deinit = maildir_list_deinit,
+ .get_hierarchy_sep = maildir_list_get_hierarchy_sep,
+ .get_vname = mailbox_list_default_get_vname,
+ .get_storage_name = mailbox_list_default_get_storage_name,
+ .get_path = maildir_list_get_path,
+ .get_temp_prefix = maildir_list_get_temp_prefix,
+ .iter_init = maildir_list_iter_init,
+ .iter_next = maildir_list_iter_next,
+ .iter_deinit = maildir_list_iter_deinit,
+ .get_mailbox_flags = maildir_list_get_mailbox_flags,
+ .subscriptions_refresh = mailbox_list_subscriptions_refresh,
+ .set_subscribed = maildir_list_set_subscribed,
+ .delete_mailbox = maildir_list_delete_mailbox,
+ .delete_dir = maildir_list_delete_dir,
+ .delete_symlink = mailbox_list_delete_symlink_default,
+ .rename_mailbox = maildir_list_rename_mailbox,
+ }
+};
+
+struct mailbox_list imapdir_mailbox_list = {
+ .name = MAILBOX_LIST_NAME_IMAPDIR,
+ .props = MAILBOX_LIST_PROP_NO_MAILDIR_NAME |
+ MAILBOX_LIST_PROP_NO_ALT_DIR |
+ MAILBOX_LIST_PROP_NO_NOSELECT |
+ MAILBOX_LIST_PROP_NO_INTERNAL_NAMES,
+ .mailbox_name_max_length = MAILBOX_LIST_NAME_MAX_LENGTH,
+
+ .v = {
+ .alloc = imapdir_list_alloc,
+ .deinit = maildir_list_deinit,
+ .get_hierarchy_sep = maildir_list_get_hierarchy_sep,
+ .get_vname = mailbox_list_default_get_vname,
+ .get_storage_name = mailbox_list_default_get_storage_name,
+ .get_path = maildir_list_get_path,
+ .get_temp_prefix = maildir_list_get_temp_prefix,
+ .iter_init = maildir_list_iter_init,
+ .iter_next = maildir_list_iter_next,
+ .iter_deinit = maildir_list_iter_deinit,
+ .get_mailbox_flags = maildir_list_get_mailbox_flags,
+ .subscriptions_refresh = mailbox_list_subscriptions_refresh,
+ .set_subscribed = maildir_list_set_subscribed,
+ .delete_mailbox = maildir_list_delete_mailbox,
+ .delete_dir = maildir_list_delete_dir,
+ .delete_symlink = mailbox_list_delete_symlink_default,
+ .rename_mailbox = maildir_list_rename_mailbox,
+ }
+};
diff --git a/src/lib-storage/list/mailbox-list-maildir.h b/src/lib-storage/list/mailbox-list-maildir.h
new file mode 100644
index 0000000..3760378
--- /dev/null
+++ b/src/lib-storage/list/mailbox-list-maildir.h
@@ -0,0 +1,29 @@
+#ifndef MAILBOX_LIST_MAILDIR_H
+#define MAILBOX_LIST_MAILDIR_H
+
+#include "mailbox-list-private.h"
+
+/* When doing deletion via renaming it first to trash directory, use this as
+ the trash directory name */
+#define MAILBOX_LIST_MAILDIR_TRASH_DIR_NAME "DOVECOT-TRASHED"
+
+struct maildir_mailbox_list {
+ struct mailbox_list list;
+
+ const char *global_temp_prefix, *temp_prefix;
+ char sep;
+};
+
+struct mailbox_list_iterate_context *
+maildir_list_iter_init(struct mailbox_list *_list, const char *const *patterns,
+ enum mailbox_list_iter_flags flags);
+int maildir_list_iter_deinit(struct mailbox_list_iterate_context *ctx);
+const struct mailbox_info *
+maildir_list_iter_next(struct mailbox_list_iterate_context *ctx);
+
+int maildir_list_get_mailbox_flags(struct mailbox_list *list,
+ const char *dir, const char *fname,
+ enum mailbox_list_file_type type,
+ enum mailbox_info_flags *flags);
+
+#endif
diff --git a/src/lib-storage/list/mailbox-list-none.c b/src/lib-storage/list/mailbox-list-none.c
new file mode 100644
index 0000000..fb5c608
--- /dev/null
+++ b/src/lib-storage/list/mailbox-list-none.c
@@ -0,0 +1,178 @@
+/* Copyright (c) 2006-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "imap-match.h"
+#include "mailbox-list-private.h"
+
+#define GLOBAL_TEMP_PREFIX ".temp."
+
+struct noop_list_iterate_context {
+ struct mailbox_list_iterate_context ctx;
+ struct mailbox_info inbox_info;
+ bool list_inbox:1;
+};
+
+extern struct mailbox_list none_mailbox_list;
+
+static struct mailbox_list *none_list_alloc(void)
+{
+ struct mailbox_list *list;
+ pool_t pool;
+
+ pool = pool_alloconly_create("none list", 2048);
+
+ list = p_new(pool, struct mailbox_list, 1);
+ *list = none_mailbox_list;
+ list->props = MAILBOX_LIST_PROP_NO_LIST_INDEX;
+ list->pool = pool;
+ return list;
+}
+
+static void none_list_deinit(struct mailbox_list *list)
+{
+ pool_unref(&list->pool);
+}
+
+static char none_list_get_hierarchy_sep(struct mailbox_list *list ATTR_UNUSED)
+{
+ return '/';
+}
+
+static int
+none_list_get_path(struct mailbox_list *list ATTR_UNUSED,
+ const char *name ATTR_UNUSED,
+ enum mailbox_list_path_type type ATTR_UNUSED,
+ const char **path_r ATTR_UNUSED)
+{
+ return 0;
+}
+
+static const char *
+none_list_get_temp_prefix(struct mailbox_list *list ATTR_UNUSED,
+ bool global ATTR_UNUSED)
+{
+ return GLOBAL_TEMP_PREFIX;
+}
+
+static int
+none_list_subscriptions_refresh(struct mailbox_list *src_list ATTR_UNUSED,
+ struct mailbox_list *dest_list ATTR_UNUSED)
+{
+ return 0;
+}
+
+static int none_list_set_subscribed(struct mailbox_list *list,
+ const char *name ATTR_UNUSED,
+ bool set ATTR_UNUSED)
+{
+ mailbox_list_set_error(list, MAIL_ERROR_NOTPOSSIBLE, "Not supported");
+ return -1;
+}
+
+static int none_list_delete_mailbox(struct mailbox_list *list,
+ const char *name ATTR_UNUSED)
+{
+ mailbox_list_set_error(list, MAIL_ERROR_NOTPOSSIBLE, "Not supported");
+ return -1;
+}
+
+static int none_list_delete_dir(struct mailbox_list *list,
+ const char *name ATTR_UNUSED)
+{
+ mailbox_list_set_error(list, MAIL_ERROR_NOTPOSSIBLE, "Not supported");
+ return -1;
+}
+
+static int
+none_list_rename_mailbox(struct mailbox_list *oldlist,
+ const char *oldname ATTR_UNUSED,
+ struct mailbox_list *newlist ATTR_UNUSED,
+ const char *newname ATTR_UNUSED)
+{
+ mailbox_list_set_error(oldlist, MAIL_ERROR_NOTPOSSIBLE,
+ "Not supported");
+ return -1;
+}
+
+static struct mailbox_list_iterate_context *
+none_list_iter_init(struct mailbox_list *list,
+ const char *const *patterns,
+ enum mailbox_list_iter_flags flags)
+{
+ struct noop_list_iterate_context *ctx;
+ pool_t pool;
+
+ pool = pool_alloconly_create("mailbox list none iter", 1024);
+ ctx = p_new(pool, struct noop_list_iterate_context, 1);
+ ctx->ctx.pool = pool;
+ ctx->ctx.list = list;
+ ctx->ctx.flags = flags;
+ ctx->ctx.glob = imap_match_init_multiple(pool, patterns, TRUE,
+ mail_namespace_get_sep(list->ns));
+ array_create(&ctx->ctx.module_contexts, pool, sizeof(void *), 5);
+ if ((list->ns->flags & NAMESPACE_FLAG_INBOX_USER) != 0 &&
+ imap_match(ctx->ctx.glob, "INBOX") == IMAP_MATCH_YES) {
+ ctx->list_inbox = TRUE;
+ ctx->inbox_info.ns = list->ns;
+ ctx->inbox_info.vname = "INBOX";
+ }
+ return &ctx->ctx;
+}
+
+static int
+none_list_iter_deinit(struct mailbox_list_iterate_context *ctx)
+{
+ pool_unref(&ctx->pool);
+ return 0;
+}
+
+static const struct mailbox_info *
+none_list_iter_next(struct mailbox_list_iterate_context *_ctx)
+{
+ struct noop_list_iterate_context *ctx =
+ (struct noop_list_iterate_context *)_ctx;
+
+ if (ctx->list_inbox) {
+ ctx->list_inbox = FALSE;
+ return &ctx->inbox_info;
+ }
+ return NULL;
+}
+
+static int
+none_list_get_mailbox_flags(struct mailbox_list *list ATTR_UNUSED,
+ const char *dir ATTR_UNUSED,
+ const char *fname ATTR_UNUSED,
+ enum mailbox_list_file_type type ATTR_UNUSED,
+ enum mailbox_info_flags *flags)
+{
+ *flags = MAILBOX_NONEXISTENT;
+ return 0;
+}
+
+struct mailbox_list none_mailbox_list = {
+ .name = MAILBOX_LIST_NAME_NONE,
+ .props = MAILBOX_LIST_PROP_NO_ROOT,
+ .mailbox_name_max_length = MAILBOX_LIST_NAME_MAX_LENGTH,
+
+ .v = {
+ .alloc = none_list_alloc,
+ .deinit = none_list_deinit,
+ .get_hierarchy_sep = none_list_get_hierarchy_sep,
+ .get_vname = mailbox_list_default_get_vname,
+ .get_storage_name = mailbox_list_default_get_storage_name,
+ .get_path = none_list_get_path,
+ .get_temp_prefix = none_list_get_temp_prefix,
+ .iter_init = none_list_iter_init,
+ .iter_next = none_list_iter_next,
+ .iter_deinit = none_list_iter_deinit,
+ .get_mailbox_flags = none_list_get_mailbox_flags,
+ .subscriptions_refresh = none_list_subscriptions_refresh,
+ .set_subscribed = none_list_set_subscribed,
+ .delete_mailbox = none_list_delete_mailbox,
+ .delete_dir = none_list_delete_dir,
+ .delete_symlink = none_list_delete_dir,
+ .rename_mailbox = none_list_rename_mailbox,
+ }
+};
diff --git a/src/lib-storage/list/mailbox-list-notify-tree.c b/src/lib-storage/list/mailbox-list-notify-tree.c
new file mode 100644
index 0000000..6152dbb
--- /dev/null
+++ b/src/lib-storage/list/mailbox-list-notify-tree.c
@@ -0,0 +1,131 @@
+/* Copyright (c) 2013-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "str.h"
+#include "mail-index.h"
+#include "mail-storage.h"
+#include "mailbox-list-private.h"
+#include "mailbox-list-index.h"
+#include "mailbox-list-notify-tree.h"
+
+struct mailbox_list_notify_tree {
+ struct mailbox_list *list;
+ struct mailbox_tree_context *mailboxes;
+
+ struct mail_index_view *view;
+ bool failed;
+};
+
+static void
+mailbox_list_notify_node_get_status(struct mailbox_list_notify_tree *tree,
+ struct mailbox_notify_node *nnode)
+{
+ struct mailbox_status status;
+ const char *reason;
+ uint32_t seq;
+
+ if (!mail_index_lookup_seq(tree->view, nnode->index_uid, &seq))
+ return;
+
+ i_zero(&status);
+ (void)mailbox_list_index_status(tree->list, tree->view, seq,
+ STATUS_UIDVALIDITY | STATUS_UIDNEXT | STATUS_MESSAGES |
+ STATUS_UNSEEN | STATUS_HIGHESTMODSEQ, &status, nnode->guid,
+ NULL, &reason);
+ nnode->uidvalidity = status.uidvalidity;
+ nnode->uidnext = status.uidnext;
+ nnode->messages = status.messages;
+ nnode->unseen = status.unseen;
+ nnode->highest_modseq = status.highest_modseq;
+}
+
+static void
+mailbox_list_notify_node_build(struct mailbox_list_notify_tree *tree,
+ struct mailbox_list_index_node *index_node,
+ string_t *path)
+{
+ struct mailbox_node *node;
+ struct mailbox_notify_node *nnode;
+ size_t prefix_len;
+ bool created;
+
+ str_append(path, index_node->raw_name);
+
+ node = mailbox_tree_get(tree->mailboxes, str_c(path), &created);
+ nnode = (struct mailbox_notify_node *)node;
+ nnode->index_uid = index_node->uid;
+
+ if ((index_node->flags & MAILBOX_LIST_INDEX_FLAG_NONEXISTENT) != 0)
+ node->flags = MAILBOX_NONEXISTENT;
+ else if ((index_node->flags & MAILBOX_LIST_INDEX_FLAG_NOSELECT) != 0)
+ node->flags = MAILBOX_NOSELECT;
+ else {
+ node->flags = 0;
+ mailbox_list_notify_node_get_status(tree, nnode);
+ }
+ if ((index_node->flags & MAILBOX_LIST_INDEX_FLAG_NOINFERIORS) != 0)
+ node->flags |= MAILBOX_NOINFERIORS;
+
+ if (index_node->children != NULL) {
+ str_append_c(path, mailbox_list_get_hierarchy_sep(tree->list));
+ prefix_len = str_len(path);
+ index_node = index_node->children;
+ for (; index_node != NULL; index_node = index_node->next) {
+ str_truncate(path, prefix_len);
+ mailbox_list_notify_node_build(tree, index_node, path);
+ }
+ }
+}
+
+static void
+mailbox_list_notify_tree_build(struct mailbox_list_notify_tree *tree)
+{
+ struct mailbox_list_index *ilist = INDEX_LIST_CONTEXT_REQUIRE(tree->list);
+ struct mailbox_list_index_node *index_node;
+ string_t *path = t_str_new(128);
+
+ if (mailbox_list_index_refresh(tree->list) < 0)
+ tree->failed = TRUE;
+
+ tree->view = mail_index_view_open(ilist->index);
+ index_node = ilist->mailbox_tree;
+ for (; index_node != NULL; index_node = index_node->next) {
+ str_truncate(path, 0);
+ mailbox_list_notify_node_build(tree, index_node, path);
+ }
+ mail_index_view_close(&tree->view);
+}
+
+struct mailbox_list_notify_tree *
+mailbox_list_notify_tree_init(struct mailbox_list *list)
+{
+ struct mailbox_list_notify_tree *tree;
+
+ tree = i_new(struct mailbox_list_notify_tree, 1);
+ tree->list = list;
+ tree->mailboxes =
+ mailbox_tree_init_size(mailbox_list_get_hierarchy_sep(list),
+ sizeof(struct mailbox_notify_node));
+ mailbox_list_notify_tree_build(tree);
+ return tree;
+}
+
+void mailbox_list_notify_tree_deinit(struct mailbox_list_notify_tree **_tree)
+{
+ struct mailbox_list_notify_tree *tree = *_tree;
+
+ *_tree = NULL;
+
+ mailbox_tree_deinit(&tree->mailboxes);
+ i_free(tree);
+}
+
+struct mailbox_notify_node *
+mailbox_list_notify_tree_lookup(struct mailbox_list_notify_tree *tree,
+ const char *storage_name)
+{
+ struct mailbox_node *node;
+
+ node = mailbox_tree_lookup(tree->mailboxes, storage_name);
+ return (struct mailbox_notify_node *)node;
+}
diff --git a/src/lib-storage/list/mailbox-list-notify-tree.h b/src/lib-storage/list/mailbox-list-notify-tree.h
new file mode 100644
index 0000000..3666ac8
--- /dev/null
+++ b/src/lib-storage/list/mailbox-list-notify-tree.h
@@ -0,0 +1,27 @@
+#ifndef MAILBOX_LIST_NOTIFY_TREE_H
+#define MAILBOX_LIST_NOTIFY_TREE_H
+
+#include "mailbox-tree.h"
+
+struct mailbox_notify_node {
+ struct mailbox_node node;
+
+ guid_128_t guid;
+ uint32_t index_uid;
+
+ uint32_t uidvalidity;
+ uint32_t uidnext;
+ uint32_t messages;
+ uint32_t unseen;
+ uint64_t highest_modseq;
+};
+
+struct mailbox_list_notify_tree *
+mailbox_list_notify_tree_init(struct mailbox_list *list);
+void mailbox_list_notify_tree_deinit(struct mailbox_list_notify_tree **tree);
+
+struct mailbox_notify_node *
+mailbox_list_notify_tree_lookup(struct mailbox_list_notify_tree *tree,
+ const char *storage_name);
+
+#endif
diff --git a/src/lib-storage/list/mailbox-list-subscriptions.c b/src/lib-storage/list/mailbox-list-subscriptions.c
new file mode 100644
index 0000000..2e27c96
--- /dev/null
+++ b/src/lib-storage/list/mailbox-list-subscriptions.c
@@ -0,0 +1,318 @@
+/* Copyright (c) 2002-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "ioloop.h"
+#include "array.h"
+#include "unichar.h"
+#include "imap-match.h"
+#include "subscription-file.h"
+#include "mailbox-tree.h"
+#include "mailbox-list-private.h"
+#include "mailbox-list-subscriptions.h"
+
+#include <sys/stat.h>
+
+struct subscriptions_mailbox_list_iterate_context {
+ struct mailbox_list_iterate_context ctx;
+ struct mailbox_tree_context *tree;
+ struct mailbox_tree_iterate_context *iter;
+ struct mailbox_info info;
+};
+
+static int
+mailbox_list_subscription_fill_one(struct mailbox_list *list,
+ struct mailbox_list *src_list,
+ const char *name)
+{
+ struct mail_namespace *ns, *default_ns = list->ns;
+ struct mail_namespace *namespaces = default_ns->user->namespaces;
+ struct mailbox_node *node;
+ const char *vname, *ns_name, *error;
+ size_t len;
+ bool created;
+
+ /* default_ns is whatever namespace we're currently listing.
+ if we have e.g. prefix="" and prefix=pub/ namespaces with
+ pub/ namespace having subscriptions=no, we want to:
+
+ 1) when listing "" namespace we want to skip over any names
+ that begin with pub/. */
+ if (src_list->ns->prefix_len == 0)
+ ns_name = name;
+ else {
+ /* we could have two-level namespace: ns/ns2/ */
+ ns_name = t_strconcat(src_list->ns->prefix, name, NULL);
+ }
+ ns = mail_namespace_find_unsubscribable(namespaces, ns_name);
+ if (ns != NULL && ns != default_ns) {
+ if (ns->prefix_len > 0)
+ return 0;
+ /* prefix="" namespace=no : catching this is basically the
+ same as not finding any namespace. */
+ ns = NULL;
+ }
+
+ /* 2) when listing pub/ namespace, skip over entries that don't
+ begin with pub/. */
+ if (ns == NULL &&
+ (default_ns->flags & NAMESPACE_FLAG_SUBSCRIPTIONS) == 0)
+ return 0;
+
+ /* When listing shared namespace's subscriptions, we need to
+ autocreate all the visible child namespaces. their subscriptions
+ are listed later. */
+ if (ns != NULL && mail_namespace_is_shared_user_root(ns)) {
+ /* we'll need to get the namespace autocreated.
+ one easy way is to just ask to join a reference and
+ pattern */
+ (void)mailbox_list_join_refpattern(ns->list, ns_name, "");
+ }
+
+ /* When listing pub/ namespace, skip over the namespace
+ prefix in the name. the rest of the name is storage_name. */
+ if (ns == NULL)
+ ns = default_ns;
+ else if (strncmp(ns_name, ns->prefix, ns->prefix_len) == 0) {
+ ns_name += ns->prefix_len;
+ name = ns_name;
+ } else {
+ /* "pub" entry - this shouldn't be possible normally, because
+ it should be saved as "pub/", but handle it anyway */
+ i_assert(strncmp(ns_name, ns->prefix, ns->prefix_len-1) == 0 &&
+ ns_name[ns->prefix_len-1] == '\0');
+ name = "";
+ /* ns_name = ""; */
+ }
+
+ len = strlen(name);
+ if (len > 0 && name[len-1] == mail_namespace_get_sep(ns)) {
+ /* entry ends with hierarchy separator, remove it.
+ this exists mainly for backwards compatibility with old
+ Dovecot versions and non-Dovecot software that added them */
+ name = t_strndup(name, len-1);
+ }
+
+ if (!mailbox_list_is_valid_name(list, name, &error)) {
+ /* we'll only get into trouble if we show this */
+ return -1;
+ } else {
+ vname = mailbox_list_get_vname(list, name);
+ if (!uni_utf8_str_is_valid(vname))
+ return -1;
+ node = mailbox_tree_get(list->subscriptions, vname, &created);
+ node->flags = MAILBOX_SUBSCRIBED;
+ }
+ return 0;
+}
+
+int mailbox_list_subscriptions_refresh(struct mailbox_list *src_list,
+ struct mailbox_list *dest_list)
+{
+ struct subsfile_list_context *subsfile_ctx;
+ struct stat st;
+ enum mailbox_list_path_type type;
+ const char *path, *name;
+ char sep;
+ int ret;
+
+ /* src_list is subscriptions=yes, dest_list is subscriptions=no
+ (or the same as src_list) */
+ i_assert((src_list->ns->flags & NAMESPACE_FLAG_SUBSCRIPTIONS) != 0);
+
+ if (dest_list->subscriptions == NULL) {
+ sep = mail_namespace_get_sep(src_list->ns);
+ dest_list->subscriptions = mailbox_tree_init(sep);
+ }
+
+ type = src_list->set.control_dir != NULL ?
+ MAILBOX_LIST_PATH_TYPE_CONTROL : MAILBOX_LIST_PATH_TYPE_DIR;
+ if (!mailbox_list_get_root_path(src_list, type, &path) ||
+ src_list->set.subscription_fname == NULL) {
+ /* no subscriptions (e.g. pop3c) */
+ return 0;
+ }
+ path = t_strconcat(path, "/", src_list->set.subscription_fname, NULL);
+ if (stat(path, &st) < 0) {
+ if (errno == ENOENT) {
+ /* no subscriptions */
+ mailbox_tree_clear(dest_list->subscriptions);
+ dest_list->subscriptions_mtime = 0;
+ return 0;
+ }
+ mailbox_list_set_critical(dest_list, "stat(%s) failed: %m",
+ path);
+ return -1;
+ }
+ if (st.st_mtime == dest_list->subscriptions_mtime &&
+ st.st_mtime < dest_list->subscriptions_read_time-1) {
+ /* we're up to date */
+ return 0;
+ }
+
+ mailbox_tree_clear(dest_list->subscriptions);
+ dest_list->subscriptions_read_time = ioloop_time;
+
+ subsfile_ctx = subsfile_list_init(dest_list, path);
+ if (subsfile_list_fstat(subsfile_ctx, &st) == 0)
+ dest_list->subscriptions_mtime = st.st_mtime;
+ while ((name = subsfile_list_next(subsfile_ctx)) != NULL) T_BEGIN {
+ T_BEGIN {
+ ret = mailbox_list_subscription_fill_one(dest_list,
+ src_list, name);
+ } T_END;
+ if (ret < 0) {
+ e_warning(dest_list->ns->user->event,
+ "Subscriptions file %s: "
+ "Removing invalid entry: %s",
+ path, name);
+ (void)subsfile_set_subscribed(src_list, path,
+ mailbox_list_get_temp_prefix(src_list),
+ name, FALSE);
+
+ }
+ } T_END;
+
+ if (subsfile_list_deinit(&subsfile_ctx) < 0) {
+ dest_list->subscriptions_mtime = (time_t)-1;
+ return -1;
+ }
+ return 0;
+}
+
+void mailbox_list_set_subscription_flags(struct mailbox_list *list,
+ const char *vname,
+ enum mailbox_info_flags *flags)
+{
+ struct mailbox_node *node;
+
+ *flags &= ENUM_NEGATE(MAILBOX_SUBSCRIBED | MAILBOX_CHILD_SUBSCRIBED);
+
+ node = mailbox_tree_lookup(list->subscriptions, vname);
+ if (node != NULL) {
+ *flags |= node->flags & MAILBOX_SUBSCRIBED;
+
+ /* the only reason why node might have a child is if one of
+ them is subscribed */
+ if (node->children != NULL)
+ *flags |= MAILBOX_CHILD_SUBSCRIBED;
+ }
+}
+
+void mailbox_list_subscriptions_fill(struct mailbox_list_iterate_context *ctx,
+ struct mailbox_tree_context *tree,
+ bool default_nonexistent)
+{
+ struct mailbox_list_iter_update_context update_ctx;
+ struct mailbox_tree_iterate_context *iter;
+ const char *name;
+
+ i_zero(&update_ctx);
+ update_ctx.iter_ctx = ctx;
+ update_ctx.tree_ctx = tree;
+ update_ctx.glob = ctx->glob;
+ update_ctx.leaf_flags = MAILBOX_SUBSCRIBED;
+ if (default_nonexistent)
+ update_ctx.leaf_flags |= MAILBOX_NONEXISTENT;
+ update_ctx.parent_flags = MAILBOX_CHILD_SUBSCRIBED;
+ update_ctx.match_parents =
+ (ctx->flags & MAILBOX_LIST_ITER_SELECT_RECURSIVEMATCH) != 0;
+
+ iter = mailbox_tree_iterate_init(ctx->list->subscriptions, NULL,
+ MAILBOX_SUBSCRIBED);
+ while (mailbox_tree_iterate_next(iter, &name) != NULL)
+ mailbox_list_iter_update(&update_ctx, name);
+ mailbox_tree_iterate_deinit(&iter);
+}
+
+struct mailbox_list_iterate_context *
+mailbox_list_subscriptions_iter_init(struct mailbox_list *list,
+ const char *const *patterns,
+ enum mailbox_list_iter_flags flags)
+{
+ struct subscriptions_mailbox_list_iterate_context *ctx;
+ pool_t pool;
+ char sep = mail_namespace_get_sep(list->ns);
+
+ pool = pool_alloconly_create("mailbox list subscriptions iter", 1024);
+ ctx = p_new(pool, struct subscriptions_mailbox_list_iterate_context, 1);
+ ctx->ctx.pool = pool;
+ ctx->ctx.list = list;
+ ctx->ctx.flags = flags;
+ ctx->ctx.glob = imap_match_init_multiple(pool, patterns, TRUE, sep);
+ array_create(&ctx->ctx.module_contexts, pool, sizeof(void *), 5);
+
+ ctx->tree = mailbox_tree_init(sep);
+ mailbox_list_subscriptions_fill(&ctx->ctx, ctx->tree, FALSE);
+
+ ctx->info.ns = list->ns;
+ /* the tree usually has only those entries we want to iterate through,
+ but there are also non-matching root entries (e.g. "LSUB foo/%" will
+ include the "foo"), which we'll drop with MAILBOX_MATCHED. */
+ ctx->iter = mailbox_tree_iterate_init(ctx->tree, NULL, MAILBOX_MATCHED);
+ return &ctx->ctx;
+}
+
+const struct mailbox_info *
+mailbox_list_subscriptions_iter_next(struct mailbox_list_iterate_context *_ctx)
+{
+ struct subscriptions_mailbox_list_iterate_context *ctx =
+ (struct subscriptions_mailbox_list_iterate_context *)_ctx;
+ struct mailbox_list *list = _ctx->list;
+ struct mailbox_node *node;
+ enum mailbox_info_flags subs_flags;
+ const char *vname, *storage_name, *error;
+ int ret;
+
+ node = mailbox_tree_iterate_next(ctx->iter, &vname);
+ if (node == NULL)
+ return mailbox_list_iter_default_next(_ctx);
+
+ ctx->info.vname = vname;
+ subs_flags = node->flags & (MAILBOX_SUBSCRIBED |
+ MAILBOX_CHILD_SUBSCRIBED);
+
+ if ((_ctx->flags & MAILBOX_LIST_ITER_RETURN_NO_FLAGS) != 0 &&
+ (_ctx->flags & MAILBOX_LIST_ITER_RETURN_CHILDREN) == 0) {
+ /* don't care about flags, just return it */
+ ctx->info.flags = subs_flags;
+ return &ctx->info;
+ }
+
+ storage_name = mailbox_list_get_storage_name(list, vname);
+ if (!mailbox_list_is_valid_name(list, storage_name, &error)) {
+ /* broken entry in subscriptions file */
+ ctx->info.flags = MAILBOX_NONEXISTENT;
+ } else if (mailbox_list_mailbox(list, storage_name,
+ &ctx->info.flags) < 0) {
+ ctx->info.flags = 0;
+ _ctx->failed = TRUE;
+ } else if ((_ctx->flags & MAILBOX_LIST_ITER_RETURN_CHILDREN) != 0 &&
+ (ctx->info.flags & (MAILBOX_CHILDREN |
+ MAILBOX_NOCHILDREN)) == 0) {
+ ret = mailbox_has_children(list, storage_name);
+ if (ret < 0)
+ _ctx->failed = TRUE;
+ else if (ret == 0)
+ ctx->info.flags |= MAILBOX_NOCHILDREN;
+ else
+ ctx->info.flags |= MAILBOX_CHILDREN;
+
+ }
+
+ ctx->info.flags &= ENUM_NEGATE(MAILBOX_SUBSCRIBED | MAILBOX_CHILD_SUBSCRIBED);
+ ctx->info.flags |=
+ node->flags & (MAILBOX_SUBSCRIBED | MAILBOX_CHILD_SUBSCRIBED);
+ return &ctx->info;
+}
+
+int mailbox_list_subscriptions_iter_deinit(struct mailbox_list_iterate_context *_ctx)
+{
+ struct subscriptions_mailbox_list_iterate_context *ctx =
+ (struct subscriptions_mailbox_list_iterate_context *)_ctx;
+ int ret = _ctx->failed ? -1 : 0;
+
+ mailbox_tree_iterate_deinit(&ctx->iter);
+ mailbox_tree_deinit(&ctx->tree);
+ pool_unref(&_ctx->pool);
+ return ret;
+}
diff --git a/src/lib-storage/list/mailbox-list-subscriptions.h b/src/lib-storage/list/mailbox-list-subscriptions.h
new file mode 100644
index 0000000..283012d
--- /dev/null
+++ b/src/lib-storage/list/mailbox-list-subscriptions.h
@@ -0,0 +1,33 @@
+#ifndef MAILBOX_LIST_SUBSCRIPTIONS_H
+#define MAILBOX_LIST_SUBSCRIPTIONS_H
+
+#include "mailbox-list-iter.h"
+
+struct mailbox_tree_context;
+struct mailbox_list_iterate_context;
+
+int mailbox_list_subscriptions_refresh(struct mailbox_list *src_list,
+ struct mailbox_list *dest_list);
+
+/* Set MAILBOX_SUBSCRIBED and MAILBOX_CHILD_SUBSCRIBED flags,
+ clearing them if they already are there when they shouldn't. */
+void mailbox_list_set_subscription_flags(struct mailbox_list *list,
+ const char *vname,
+ enum mailbox_info_flags *flags);
+
+/* Add subscriptions matching the iteration to the given tree */
+void mailbox_list_subscriptions_fill(struct mailbox_list_iterate_context *ctx,
+ struct mailbox_tree_context *tree,
+ bool default_nonexistent);
+
+/* Iterate through subscriptions, call mailbox_list.get_mailbox_flags()
+ if necessary for mailboxes to get their flags. */
+struct mailbox_list_iterate_context *
+mailbox_list_subscriptions_iter_init(struct mailbox_list *list,
+ const char *const *patterns,
+ enum mailbox_list_iter_flags flags);
+const struct mailbox_info *
+mailbox_list_subscriptions_iter_next(struct mailbox_list_iterate_context *ctx);
+int mailbox_list_subscriptions_iter_deinit(struct mailbox_list_iterate_context *ctx);
+
+#endif
diff --git a/src/lib-storage/list/subscription-file.c b/src/lib-storage/list/subscription-file.c
new file mode 100644
index 0000000..4c5094a
--- /dev/null
+++ b/src/lib-storage/list/subscription-file.c
@@ -0,0 +1,380 @@
+/* Copyright (c) 2002-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "str.h"
+#include "strescape.h"
+#include "istream.h"
+#include "ostream.h"
+#include "nfs-workarounds.h"
+#include "mkdir-parents.h"
+#include "file-dotlock.h"
+#include "mailbox-list-private.h"
+#include "subscription-file.h"
+
+#include <unistd.h>
+#include <fcntl.h>
+
+#define SUBSCRIPTION_FILE_ESTALE_RETRY_COUNT NFS_ESTALE_RETRY_COUNT
+#define SUBSCRIPTION_FILE_LOCK_TIMEOUT 120
+#define SUBSCRIPTION_FILE_CHANGE_TIMEOUT 30
+
+struct subsfile_list_context {
+ struct mailbox_list *list;
+ struct istream *input;
+ char *path;
+ string_t *name;
+
+ unsigned int version;
+ bool failed;
+};
+
+static const char version2_header[] = "V\t2\n\n";
+
+static void subsread_set_syscall_error(struct mailbox_list *list,
+ const char *function, const char *path)
+{
+ if (errno == EACCES && !event_want_debug_log(list->ns->user->event)) {
+ mailbox_list_set_error(list, MAIL_ERROR_PERM,
+ "No permission to read subscriptions");
+ } else {
+ mailbox_list_set_critical(list,
+ "%s failed with subscription file %s: %m",
+ function, path);
+ }
+}
+
+static void subswrite_set_syscall_error(struct mailbox_list *list,
+ const char *function, const char *path)
+{
+ if (errno == EACCES && !event_want_debug_log(list->ns->user->event)) {
+ mailbox_list_set_error(list, MAIL_ERROR_PERM,
+ "No permission to modify subscriptions");
+ } else {
+ mailbox_list_set_critical(list,
+ "%s failed with subscription file %s: %m",
+ function, path);
+ }
+}
+
+static void
+subsfile_list_read_header(struct mailbox_list *list, struct istream *input,
+ unsigned int *version_r)
+{
+ const unsigned char version2_header_len = strlen(version2_header);
+ const unsigned char *data;
+ size_t size;
+ int ret;
+
+ *version_r = 0;
+
+ ret = i_stream_read_bytes(input, &data, &size, version2_header_len);
+ if (ret < 0) {
+ i_assert(ret == -1);
+ if (input->stream_errno != 0)
+ subswrite_set_syscall_error(list, "read()", i_stream_get_name(input));
+ return;
+ }
+ if (ret > 0 &&
+ memcmp(data, version2_header, version2_header_len) == 0) {
+ *version_r = 2;
+ i_stream_skip(input, version2_header_len);
+ }
+}
+
+static const char *next_line(struct mailbox_list *list, const char *path,
+ struct istream *input, bool *failed_r,
+ bool ignore_estale)
+{
+ const char *line;
+
+ *failed_r = FALSE;
+
+ while ((line = i_stream_next_line(input)) == NULL) {
+ switch (i_stream_read(input)) {
+ case -1:
+ if (input->stream_errno != 0 &&
+ (input->stream_errno != ESTALE || !ignore_estale)) {
+ subswrite_set_syscall_error(list,
+ "read()", path);
+ *failed_r = TRUE;
+ }
+ return NULL;
+ case -2:
+ /* mailbox name too large */
+ mailbox_list_set_critical(list,
+ "Subscription file %s contains lines longer "
+ "than %u characters", path,
+ (unsigned int)list->mailbox_name_max_length);
+ *failed_r = TRUE;
+ return NULL;
+ }
+ }
+
+ return line;
+}
+
+int subsfile_set_subscribed(struct mailbox_list *list, const char *path,
+ const char *temp_prefix, const char *name,
+ bool set)
+{
+ const struct mail_storage_settings *mail_set = list->mail_set;
+ struct dotlock_settings dotlock_set;
+ struct dotlock *dotlock;
+ struct mailbox_permissions perm;
+ const char *line, *dir, *fname, *escaped_name;
+ struct istream *input = NULL;
+ struct ostream *output;
+ int fd_in, fd_out;
+ enum mailbox_list_path_type type;
+ bool found, changed = FALSE, failed = FALSE;
+ unsigned int version = 2;
+
+ if (strcasecmp(name, "INBOX") == 0)
+ name = "INBOX";
+
+ i_zero(&dotlock_set);
+ dotlock_set.use_excl_lock = mail_set->dotlock_use_excl;
+ dotlock_set.nfs_flush = mail_set->mail_nfs_storage;
+ dotlock_set.temp_prefix = temp_prefix;
+ dotlock_set.timeout = SUBSCRIPTION_FILE_LOCK_TIMEOUT;
+ dotlock_set.stale_timeout = SUBSCRIPTION_FILE_CHANGE_TIMEOUT;
+
+ mailbox_list_get_root_permissions(list, &perm);
+ fd_out = file_dotlock_open_group(&dotlock_set, path, 0,
+ perm.file_create_mode,
+ perm.file_create_gid,
+ perm.file_create_gid_origin, &dotlock);
+ if (fd_out == -1 && errno == ENOENT) {
+ /* directory hasn't been created yet. */
+ type = list->set.control_dir != NULL ?
+ MAILBOX_LIST_PATH_TYPE_CONTROL :
+ MAILBOX_LIST_PATH_TYPE_DIR;
+ fname = strrchr(path, '/');
+ if (fname != NULL) {
+ dir = t_strdup_until(path, fname);
+ if (mailbox_list_mkdir_root(list, dir, type) < 0)
+ return -1;
+ }
+ fd_out = file_dotlock_open_group(&dotlock_set, path, 0,
+ perm.file_create_mode,
+ perm.file_create_gid,
+ perm.file_create_gid_origin,
+ &dotlock);
+ }
+ if (fd_out == -1) {
+ if (errno == EAGAIN) {
+ mailbox_list_set_error(list, MAIL_ERROR_TEMP,
+ "Timeout waiting for subscription file lock");
+ } else {
+ subswrite_set_syscall_error(list, "file_dotlock_open()",
+ path);
+ }
+ return -1;
+ }
+
+ fd_in = nfs_safe_open(path, O_RDONLY);
+ if (fd_in == -1 && errno != ENOENT) {
+ subswrite_set_syscall_error(list, "open()", path);
+ file_dotlock_delete(&dotlock);
+ return -1;
+ }
+ if (fd_in != -1) {
+ input = i_stream_create_fd_autoclose(&fd_in, list->mailbox_name_max_length+1);
+ i_stream_set_return_partial_line(input, TRUE);
+ subsfile_list_read_header(list, input, &version);
+ }
+
+ found = FALSE;
+ output = o_stream_create_fd_file(fd_out, 0, FALSE);
+ o_stream_cork(output);
+ if (version >= 2)
+ o_stream_nsend_str(output, version2_header);
+ if (version < 2 || name[0] == '\0')
+ escaped_name = name;
+ else {
+ const char *const *tmp;
+ char separators[2];
+ string_t *str = t_str_new(64);
+
+ separators[0] = mailbox_list_get_hierarchy_sep(list);
+ separators[1] = '\0';
+ tmp = t_strsplit(name, separators);
+ str_append_tabescaped(str, *tmp);
+ for (tmp++; *tmp != NULL; tmp++) {
+ str_append_c(str, '\t');
+ str_append_tabescaped(str, *tmp);
+ }
+ escaped_name = str_c(str);
+ }
+ if (input != NULL) {
+ while ((line = next_line(list, path, input,
+ &failed, FALSE)) != NULL) {
+ if (strcmp(line, escaped_name) == 0) {
+ found = TRUE;
+ if (!set) {
+ changed = TRUE;
+ continue;
+ }
+ }
+
+ o_stream_nsend_str(output, line);
+ o_stream_nsend(output, "\n", 1);
+ }
+ i_stream_destroy(&input);
+ }
+
+ if (!failed && set && !found) {
+ /* append subscription */
+ line = t_strconcat(escaped_name, "\n", NULL);
+ o_stream_nsend_str(output, line);
+ changed = TRUE;
+ }
+
+ if (changed && !failed) {
+ if (o_stream_finish(output) < 0) {
+ subswrite_set_syscall_error(list, "write()", path);
+ failed = TRUE;
+ } else if (mail_set->parsed_fsync_mode != FSYNC_MODE_NEVER) {
+ if (fsync(fd_out) < 0) {
+ subswrite_set_syscall_error(list, "fsync()",
+ path);
+ failed = TRUE;
+ }
+ }
+ } else {
+ o_stream_abort(output);
+ }
+ o_stream_destroy(&output);
+
+ if (failed || !changed) {
+ if (file_dotlock_delete(&dotlock) < 0) {
+ subswrite_set_syscall_error(list,
+ "file_dotlock_delete()", path);
+ failed = TRUE;
+ }
+ } else {
+ enum dotlock_replace_flags flags =
+ DOTLOCK_REPLACE_FLAG_VERIFY_OWNER;
+ if (file_dotlock_replace(&dotlock, flags) < 0) {
+ subswrite_set_syscall_error(list,
+ "file_dotlock_replace()", path);
+ failed = TRUE;
+ }
+ }
+ return failed ? -1 : (changed ? 1 : 0);
+}
+
+struct subsfile_list_context *
+subsfile_list_init(struct mailbox_list *list, const char *path)
+{
+ struct subsfile_list_context *ctx;
+ int fd;
+
+ ctx = i_new(struct subsfile_list_context, 1);
+ ctx->list = list;
+
+ fd = nfs_safe_open(path, O_RDONLY);
+ if (fd == -1) {
+ if (errno != ENOENT) {
+ subsread_set_syscall_error(list, "open()", path);
+ ctx->failed = TRUE;
+ }
+ } else {
+ ctx->input = i_stream_create_fd_autoclose(&fd,
+ list->mailbox_name_max_length+1);
+ i_stream_set_return_partial_line(ctx->input, TRUE);
+ subsfile_list_read_header(ctx->list, ctx->input, &ctx->version);
+ }
+ ctx->path = i_strdup(path);
+ ctx->name = str_new(default_pool, 128);
+ return ctx;
+}
+
+int subsfile_list_deinit(struct subsfile_list_context **_ctx)
+{
+ struct subsfile_list_context *ctx = *_ctx;
+ int ret = ctx->failed ? -1 : 0;
+
+ *_ctx = NULL;
+
+ i_stream_destroy(&ctx->input);
+ str_free(&ctx->name);
+ i_free(ctx->path);
+ i_free(ctx);
+ return ret;
+}
+
+int subsfile_list_fstat(struct subsfile_list_context *ctx, struct stat *st_r)
+{
+ const struct stat *st;
+
+ if (ctx->failed)
+ return -1;
+
+ if (i_stream_stat(ctx->input, FALSE, &st) < 0) {
+ ctx->failed = TRUE;
+ return -1;
+ }
+ *st_r = *st;
+ return 0;
+}
+
+static const char *
+subsfile_list_unescaped(struct subsfile_list_context *ctx, const char *line)
+{
+ const char *p;
+
+ str_truncate(ctx->name, 0);
+ while ((p = strchr(line, '\t')) != NULL) {
+ str_append_tabunescaped(ctx->name, line, p-line);
+ str_append_c(ctx->name, mailbox_list_get_hierarchy_sep(ctx->list));
+ line = p+1;
+ }
+ str_append_tabunescaped(ctx->name, line, strlen(line));
+ return str_c(ctx->name);
+}
+
+const char *subsfile_list_next(struct subsfile_list_context *ctx)
+{
+ const char *line;
+ unsigned int i;
+ int fd;
+
+ if (ctx->failed || ctx->input == NULL)
+ return NULL;
+
+ for (i = 0;; i++) {
+ line = next_line(ctx->list, ctx->path, ctx->input, &ctx->failed,
+ i < SUBSCRIPTION_FILE_ESTALE_RETRY_COUNT);
+ if (ctx->input->stream_errno != ESTALE ||
+ i == SUBSCRIPTION_FILE_ESTALE_RETRY_COUNT)
+ break;
+
+ /* Reopen the subscription file and re-send everything.
+ this isn't the optimal behavior, but it's allowed by
+ IMAP and this way we don't have to read everything into
+ memory or try to play any guessing games. */
+ i_stream_destroy(&ctx->input);
+
+ fd = nfs_safe_open(ctx->path, O_RDONLY);
+ if (fd == -1) {
+ /* In case of ENOENT all the subscriptions got lost.
+ Just return end of subscriptions list in that
+ case. */
+ if (errno != ENOENT) {
+ subsread_set_syscall_error(ctx->list, "open()",
+ ctx->path);
+ ctx->failed = TRUE;
+ }
+ return NULL;
+ }
+
+ ctx->input = i_stream_create_fd_autoclose(&fd,
+ ctx->list->mailbox_name_max_length+1);
+ i_stream_set_return_partial_line(ctx->input, TRUE);
+ }
+
+ if (ctx->version > 1 && line != NULL)
+ line = subsfile_list_unescaped(ctx, line);
+ return line;
+}
diff --git a/src/lib-storage/list/subscription-file.h b/src/lib-storage/list/subscription-file.h
new file mode 100644
index 0000000..fbaa0a3
--- /dev/null
+++ b/src/lib-storage/list/subscription-file.h
@@ -0,0 +1,25 @@
+#ifndef SUBSCRIPTION_FILE_H
+#define SUBSCRIPTION_FILE_H
+
+struct stat;
+struct mailbox_list;
+
+/* Initialize new subscription file listing. */
+struct subsfile_list_context *
+subsfile_list_init(struct mailbox_list *list, const char *path);
+/* Deinitialize subscription file listing. Returns 0 if ok, or -1 if some
+ error occurred while listing. */
+int subsfile_list_deinit(struct subsfile_list_context **ctx);
+
+/* Call fstat() for subscription file */
+int subsfile_list_fstat(struct subsfile_list_context *ctx, struct stat *st_r);
+
+/* Returns the next subscribed mailbox, or NULL. */
+const char *subsfile_list_next(struct subsfile_list_context *ctx);
+
+/* Returns 1 if subscribed, 0 if no changes done, -1 if error. */
+int subsfile_set_subscribed(struct mailbox_list *list, const char *path,
+ const char *temp_prefix, const char *name,
+ bool set);
+
+#endif
diff --git a/src/lib-storage/mail-autoexpunge.c b/src/lib-storage/mail-autoexpunge.c
new file mode 100644
index 0000000..73e764b
--- /dev/null
+++ b/src/lib-storage/mail-autoexpunge.c
@@ -0,0 +1,267 @@
+/* Copyright (c) 2015-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "ioloop.h"
+#include "mailbox-list-iter.h"
+#include "mail-storage-private.h"
+#include "mail-namespace.h"
+#include "mail-user.h"
+#include "mail-autoexpunge.h"
+
+#define AUTOEXPUNGE_LOCK_FNAME "dovecot.autoexpunge.lock"
+#define AUTOEXPUNGE_BATCH_SIZE 1000
+
+static bool
+mailbox_autoexpunge_lock(struct mail_user *user, struct file_lock **lock)
+{
+ const char *error;
+ int ret;
+
+ if (*lock != NULL)
+ return TRUE;
+
+ /* Try to lock the autoexpunging. If the lock already exists, another
+ process is already busy with expunging, so we don't have to do it.
+ The easiest place where to store the lock file to is the home
+ directory, but allow autoexpunging to work even if we can't get
+ it. The lock isn't really required; it 1) improves performance
+ so that multiple processes won't do the same work unnecessarily,
+ and 2) it helps to avoid duplicate mails being added with
+ lazy_expunge. */
+ ret = mail_user_lock_file_create(user, AUTOEXPUNGE_LOCK_FNAME,
+ 0, lock, &error);
+ if (ret < 0) {
+ e_error(user->event, "autoexpunge: Couldn't create %s lock: %s",
+ AUTOEXPUNGE_LOCK_FNAME, error);
+ /* do autoexpunging anyway */
+ return TRUE;
+ } else if (ret == 0) {
+ /* another process is autoexpunging, so we don't need to. */
+ return FALSE;
+ } else {
+ return TRUE;
+ }
+}
+
+/* returns -1 on error, 0 when done, and 1 when there is more to do */
+static int
+mailbox_autoexpunge_batch(struct mailbox *box,
+ const unsigned int interval_time,
+ const unsigned int max_mails,
+ const time_t expire_time,
+ unsigned int *expunged_count)
+{
+ struct mailbox_transaction_context *t;
+ struct mail *mail;
+ const struct mail_index_header *hdr;
+ uint32_t seq;
+ time_t timestamp, last_rename_stamp = 0;
+ const void *data;
+ size_t size;
+ unsigned int count = 0;
+ bool done = FALSE, expunges_found = FALSE;
+ int ret = 0;
+
+ mail_index_get_header_ext(box->view, box->box_last_rename_stamp_ext_id,
+ &data, &size);
+
+ if (size >= sizeof(uint32_t)) {
+ last_rename_stamp = *(const uint32_t*)data;
+ if (last_rename_stamp > ioloop_time+60) {
+ /* Seems to be corrupted, or way too far into the
+ future. Don't trust it. */
+ last_rename_stamp = 0;
+ }
+ }
+
+ t = mailbox_transaction_begin(box, 0, "autoexpunge");
+ mail = mail_alloc(t, 0, NULL);
+
+ hdr = mail_index_get_header(box->view);
+
+ for (seq = 1; seq <= I_MIN(hdr->messages_count, AUTOEXPUNGE_BATCH_SIZE); seq++) {
+ mail_set_seq(mail, seq);
+ if (max_mails > 0 && hdr->messages_count - seq + 1 > max_mails) {
+ /* max_mails is still being reached -> expunge.
+ don't even check saved-dates before we're
+ below max_mails. */
+ mail_autoexpunge(mail);
+ count++;
+ } else if (interval_time == 0) {
+ /* only max_mails is used. nothing further to do. */
+ done = TRUE;
+ break;
+ } else if (mail_get_save_date(mail, &timestamp) >= 0) {
+ if (I_MAX(last_rename_stamp, timestamp) > expire_time) {
+ done = TRUE;
+ break;
+ }
+ mail_autoexpunge(mail);
+ count++;
+ } else if (mailbox_get_last_mail_error(box) == MAIL_ERROR_EXPUNGED) {
+ /* already expunged */
+ expunges_found = TRUE;
+ } else {
+ /* failed */
+ ret = -1;
+ break;
+ }
+ }
+ mail_free(&mail);
+ if (mailbox_transaction_commit(&t) < 0)
+ ret = -1;
+ else if (count > 0 || expunges_found) {
+ if (mailbox_sync(box, 0) < 0)
+ ret = -1;
+ *expunged_count += count;
+ }
+
+ if (ret < 0)
+ return -1;
+ return (done || count == 0) ? 0 : 1;
+}
+
+static int
+mailbox_autoexpunge(struct mailbox *box, unsigned int interval_time,
+ unsigned int max_mails, unsigned int *expunged_count)
+{
+ struct mailbox_metadata metadata;
+ struct mailbox_status status;
+ time_t expire_time;
+ int ret;
+
+ if ((unsigned int)ioloop_time < interval_time)
+ expire_time = 0;
+ else
+ expire_time = ioloop_time - interval_time;
+
+ /* first try to check quickly from mailbox list index if we should
+ bother opening this mailbox. */
+ if (mailbox_get_status(box, STATUS_MESSAGES, &status) < 0) {
+ if (mailbox_get_last_mail_error(box) == MAIL_ERROR_NOTFOUND) {
+ /* autocreated mailbox doesn't exist yet */
+ return 0;
+ }
+ return -1;
+ }
+ if (interval_time == 0 && status.messages <= max_mails)
+ return 0;
+
+ if (max_mails == 0 || status.messages <= max_mails) {
+ if (mailbox_get_metadata(box, MAILBOX_METADATA_FIRST_SAVE_DATE,
+ &metadata) < 0)
+ return -1;
+ if (metadata.first_save_date == (time_t)-1 ||
+ metadata.first_save_date > expire_time)
+ return 0;
+ }
+
+ if (mailbox_sync(box, MAILBOX_SYNC_FLAG_FAST) < 0)
+ return -1;
+
+ do {
+ ret = mailbox_autoexpunge_batch(box, interval_time, max_mails,
+ expire_time, expunged_count);
+ } while (ret > 0);
+
+ return ret;
+}
+
+static void
+mailbox_autoexpunge_set(struct mail_namespace *ns, const char *vname,
+ unsigned int autoexpunge,
+ unsigned int autoexpunge_max_mails,
+ unsigned int *expunged_count)
+{
+ struct mailbox *box;
+
+ /* autoexpunge is configured by admin, so we can safely ignore
+ any ACLs the user might normally have against expunging in
+ the mailbox. */
+ box = mailbox_alloc(ns->list, vname, MAILBOX_FLAG_IGNORE_ACLS);
+ if (mailbox_autoexpunge(box, autoexpunge, autoexpunge_max_mails,
+ expunged_count) < 0) {
+ e_error(box->event, "Failed to autoexpunge: %s",
+ mailbox_get_last_internal_error(box, NULL));
+ }
+ mailbox_free(&box);
+}
+
+static void
+mailbox_autoexpunge_wildcards(struct mail_namespace *ns,
+ const struct mailbox_settings *set,
+ unsigned int *expunged_count)
+{
+ struct mailbox_list_iterate_context *iter;
+ const struct mailbox_info *info;
+ const char *iter_name;
+
+ iter_name = t_strconcat(ns->prefix, set->name, NULL);
+ iter = mailbox_list_iter_init(ns->list, iter_name,
+ MAILBOX_LIST_ITER_NO_AUTO_BOXES |
+ MAILBOX_LIST_ITER_SKIP_ALIASES |
+ MAILBOX_LIST_ITER_RETURN_NO_FLAGS);
+ while ((info = mailbox_list_iter_next(iter)) != NULL) T_BEGIN {
+ mailbox_autoexpunge_set(ns, info->vname, set->autoexpunge,
+ set->autoexpunge_max_mails,
+ expunged_count);
+ } T_END;
+ if (mailbox_list_iter_deinit(&iter) < 0) {
+ e_error(ns->user->event,
+ "Failed to iterate autoexpunge mailboxes '%s': %s",
+ iter_name, mailbox_list_get_last_internal_error(ns->list, NULL));
+ }
+}
+
+static bool
+mail_namespace_autoexpunge(struct mail_namespace *ns, struct file_lock **lock,
+ unsigned int *expunged_count)
+{
+ struct mailbox_settings *box_set;
+ const char *vname;
+
+ if (!array_is_created(&ns->set->mailboxes))
+ return TRUE;
+
+ array_foreach_elem(&ns->set->mailboxes, box_set) {
+ if (box_set->autoexpunge == 0 &&
+ box_set->autoexpunge_max_mails == 0)
+ continue;
+
+ if (!mailbox_autoexpunge_lock(ns->user, lock))
+ return FALSE;
+
+ if (strpbrk(box_set->name, "*?") != NULL)
+ mailbox_autoexpunge_wildcards(ns, box_set, expunged_count);
+ else {
+ if (box_set->name[0] == '\0' && ns->prefix_len > 0 &&
+ ns->prefix[ns->prefix_len-1] == mail_namespace_get_sep(ns))
+ vname = t_strndup(ns->prefix, ns->prefix_len - 1);
+ else
+ vname = t_strconcat(ns->prefix, box_set->name, NULL);
+ mailbox_autoexpunge_set(ns, vname, box_set->autoexpunge,
+ box_set->autoexpunge_max_mails,
+ expunged_count);
+ }
+ }
+ return TRUE;
+}
+
+unsigned int mail_user_autoexpunge(struct mail_user *user)
+{
+ struct file_lock *lock = NULL;
+ struct mail_namespace *ns;
+ unsigned int expunged_count = 0;
+ struct event_reason *reason =
+ event_reason_begin("storage:autoexpunge");
+
+ for (ns = user->namespaces; ns != NULL; ns = ns->next) {
+ if (ns->alias_for == NULL) {
+ if (!mail_namespace_autoexpunge(ns, &lock, &expunged_count))
+ break;
+ }
+ }
+ event_reason_end(&reason);
+ file_lock_free(&lock);
+ return expunged_count;
+}
diff --git a/src/lib-storage/mail-autoexpunge.h b/src/lib-storage/mail-autoexpunge.h
new file mode 100644
index 0000000..beeb03a
--- /dev/null
+++ b/src/lib-storage/mail-autoexpunge.h
@@ -0,0 +1,8 @@
+#ifndef MAIL_AUTOEXPUNGE_H
+#define MAIL_AUTOEXPUNGE_H
+
+/* Perform autoexpunging for all the user's mailboxes that have autoexpunging
+ configured. Returns number of mails that were autoexpunged. */
+unsigned int mail_user_autoexpunge(struct mail_user *user);
+
+#endif
diff --git a/src/lib-storage/mail-copy.c b/src/lib-storage/mail-copy.c
new file mode 100644
index 0000000..82c5f9a
--- /dev/null
+++ b/src/lib-storage/mail-copy.c
@@ -0,0 +1,126 @@
+/* Copyright (c) 2004-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "istream.h"
+#include "mail-storage-private.h"
+#include "mail-copy.h"
+
+static void
+mail_copy_set_failed(struct mail_save_context *ctx, struct mail *mail,
+ const char *func)
+{
+ const char *errstr;
+ enum mail_error error;
+
+ if (ctx->transaction->box->storage == mail->box->storage)
+ return;
+
+ errstr = mail_storage_get_last_error(mail->box->storage, &error);
+ mail_storage_set_error(ctx->transaction->box->storage, error,
+ t_strdup_printf("%s (%s)", errstr, func));
+}
+
+int mail_save_copy_default_metadata(struct mail_save_context *ctx,
+ struct mail *mail)
+{
+ const char *from_envelope, *guid;
+ time_t received_date;
+
+ if (ctx->data.received_date == (time_t)-1) {
+ if (mail_get_received_date(mail, &received_date) < 0) {
+ mail_copy_set_failed(ctx, mail, "received-date");
+ return -1;
+ }
+ mailbox_save_set_received_date(ctx, received_date, 0);
+ }
+ if (ctx->data.from_envelope == NULL) {
+ if (mail_get_special(mail, MAIL_FETCH_FROM_ENVELOPE,
+ &from_envelope) < 0) {
+ mail_copy_set_failed(ctx, mail, "from-envelope");
+ return -1;
+ }
+ if (*from_envelope != '\0')
+ mailbox_save_set_from_envelope(ctx, from_envelope);
+ }
+ if (ctx->data.guid == NULL) {
+ if (mail_get_special(mail, MAIL_FETCH_GUID, &guid) < 0) {
+ mail_copy_set_failed(ctx, mail, "guid");
+ return -1;
+ }
+ if (*guid != '\0')
+ mailbox_save_set_guid(ctx, guid);
+ }
+ return 0;
+}
+
+static int
+mail_storage_try_copy(struct mail_save_context **_ctx, struct mail *mail)
+{
+ struct mail_save_context *ctx = *_ctx;
+ struct mail_private *pmail = (struct mail_private *)mail;
+ struct istream *input;
+
+ ctx->copying_via_save = TRUE;
+
+ /* we need to open the file in any case. caching metadata is unlikely
+ to help anything. */
+ pmail->v.set_uid_cache_updates(mail, TRUE);
+
+ if (mail_get_stream_because(mail, NULL, NULL, "copying", &input) < 0) {
+ mail_copy_set_failed(ctx, mail, "stream");
+ return -1;
+ }
+ if (mail_save_copy_default_metadata(ctx, mail) < 0)
+ return -1;
+
+ if (mailbox_save_begin(_ctx, input) < 0)
+ return -1;
+
+ ssize_t ret;
+ do {
+ if (mailbox_save_continue(ctx) < 0)
+ break;
+ ret = i_stream_read(input);
+ i_assert(ret != 0);
+ } while (ret != -1);
+
+ if (input->stream_errno != 0) {
+ mailbox_set_critical(ctx->transaction->box,
+ "copy: i_stream_read(%s) failed: %s",
+ i_stream_get_name(input), i_stream_get_error(input));
+ return -1;
+ }
+ return 0;
+}
+
+int mail_storage_copy(struct mail_save_context *ctx, struct mail *mail)
+{
+ i_assert(ctx->copying_or_moving);
+
+ if (mail_storage_try_copy(&ctx, mail) < 0) {
+ if (ctx != NULL)
+ mailbox_save_cancel(&ctx);
+ return -1;
+ }
+ return mailbox_save_finish(&ctx);
+}
+
+bool mail_storage_copy_can_use_hardlink(struct mailbox *src,
+ struct mailbox *dest)
+{
+ const struct mailbox_permissions *src_perm =
+ mailbox_get_permissions(src);
+ const struct mailbox_permissions *dest_perm =
+ mailbox_get_permissions(dest);
+
+ if (src_perm->file_uid != dest_perm->file_uid) {
+ /* if we don't have read permissions, we can't hard link
+ (basically we'll catch 0600 files here) */
+ if ((src_perm->file_create_mode & 0022) == 0)
+ return FALSE;
+ }
+
+ return src_perm->file_create_mode == dest_perm->file_create_mode &&
+ src_perm->file_create_gid == dest_perm->file_create_gid &&
+ !dest->disable_reflink_copy_to;
+}
diff --git a/src/lib-storage/mail-copy.h b/src/lib-storage/mail-copy.h
new file mode 100644
index 0000000..02048db
--- /dev/null
+++ b/src/lib-storage/mail-copy.h
@@ -0,0 +1,20 @@
+#ifndef MAIL_COPY_H
+#define MAIL_COPY_H
+
+struct mail;
+struct mail_save_context;
+struct mailbox;
+
+int mail_storage_copy(struct mail_save_context *ctx, struct mail *mail);
+
+/* If save context already doesn't have some metadata fields set, copy them
+ from the given mail (e.g. received date, from envelope, guid). */
+int mail_save_copy_default_metadata(struct mail_save_context *ctx,
+ struct mail *mail);
+
+/* Returns TRUE if mail can be copied using hard linking from src to dest.
+ (Assuming the storage itself supports this.) */
+bool mail_storage_copy_can_use_hardlink(struct mailbox *src,
+ struct mailbox *dest);
+
+#endif
diff --git a/src/lib-storage/mail-duplicate.c b/src/lib-storage/mail-duplicate.c
new file mode 100644
index 0000000..7a78caa
--- /dev/null
+++ b/src/lib-storage/mail-duplicate.c
@@ -0,0 +1,767 @@
+/* Copyright (c) 2005-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "ioloop.h"
+#include "hex-binary.h"
+#include "mkdir-parents.h"
+#include "istream.h"
+#include "ostream.h"
+#include "time-util.h"
+#include "home-expand.h"
+#include "file-create-locked.h"
+#include "file-dotlock.h"
+#include "md5.h"
+#include "hash.h"
+#include "mail-user.h"
+#include "mail-storage-settings.h"
+#include "mail-duplicate.h"
+
+#include <fcntl.h>
+#include <unistd.h>
+
+#define COMPRESS_PERCENTAGE 10
+#define DUPLICATE_BUFSIZE 4096
+#define DUPLICATE_VERSION 2
+
+#define DUPLICATE_LOCK_FNAME_PREFIX "duplicate.lock."
+
+#define DUPLICATE_LOCK_TIMEOUT_SECS 65
+#define DUPLICATE_LOCK_WARN_SECS 4
+#define DUPLICATE_LOCK_MAX_LOCKS 100
+
+enum mail_duplicate_lock_result {
+ MAIL_DUPLICATE_LOCK_OK,
+ MAIL_DUPLICATE_LOCK_IO_ERROR,
+ MAIL_DUPLICATE_LOCK_TIMEOUT,
+ MAIL_DUPLICATE_LOCK_TOO_MANY,
+ MAIL_DUPLICATE_LOCK_DEADLOCK,
+};
+
+struct mail_duplicate_lock {
+ int fd;
+ char *path;
+ struct file_lock *lock;
+ struct timeval start_time;
+};
+
+struct mail_duplicate {
+ const void *id;
+ unsigned int id_size;
+
+ const char *user;
+ time_t time;
+ struct mail_duplicate_lock lock;
+
+ bool marked:1;
+ bool changed:1;
+};
+
+struct mail_duplicate_file_header {
+ uint32_t version;
+};
+
+struct mail_duplicate_record_header {
+ uint32_t stamp;
+ uint32_t id_size;
+ uint32_t user_size;
+};
+
+struct mail_duplicate_transaction {
+ pool_t pool;
+ struct mail_duplicate_db *db;
+ ino_t db_ino;
+ struct event *event;
+
+ HASH_TABLE(struct mail_duplicate *, struct mail_duplicate *) hash;
+ const char *path;
+ unsigned int id_lock_count;
+
+ bool changed:1;
+};
+
+struct mail_duplicate_db {
+ struct mail_user *user;
+ struct event *event;
+ char *path;
+ char *lock_dir;
+ struct dotlock_settings dotlock_set;
+
+ unsigned int transaction_count;
+};
+
+static const struct dotlock_settings default_mail_duplicate_dotlock_set = {
+ .timeout = 20,
+ .stale_timeout = 10,
+};
+
+static int
+mail_duplicate_cmp(const struct mail_duplicate *d1,
+ const struct mail_duplicate *d2)
+{
+ return (d1->id_size == d2->id_size &&
+ memcmp(d1->id, d2->id, d1->id_size) == 0 &&
+ strcasecmp(d1->user, d2->user) == 0) ? 0 : 1;
+}
+
+static unsigned int mail_duplicate_hash(const struct mail_duplicate *d)
+{
+ /* a char* hash function from ASU -- from glib */
+ const unsigned char *s = d->id, *end = s + d->id_size;
+ unsigned int g, h = 0;
+
+ while (s != end) {
+ h = (h << 4) + *s;
+ if ((g = h & 0xf0000000UL) != 0) {
+ h = h ^ (g >> 24);
+ h = h ^ g;
+ }
+ s++;
+ }
+
+ return h ^ strcase_hash(d->user);
+}
+
+static enum mail_duplicate_lock_result
+duplicate_lock_failed(struct mail_duplicate_transaction *trans,
+ struct mail_duplicate *dup, const char *error)
+{
+ struct mail_duplicate_lock *lock = &dup->lock;
+ enum mail_duplicate_lock_result result;
+ int diff;
+
+ i_assert(lock->fd == -1);
+ i_assert(lock->lock == NULL);
+
+ if (errno == EDEADLK) {
+ /* deadlock */
+ result = MAIL_DUPLICATE_LOCK_DEADLOCK;
+ } else if (errno != EAGAIN) {
+ /* not a lock timeout */
+ result = MAIL_DUPLICATE_LOCK_IO_ERROR;
+ } else {
+ diff = timeval_diff_msecs(&ioloop_timeval,
+ &lock->start_time);
+ error = t_strdup_printf("Lock timeout in %d.%03d secs",
+ diff/1000, diff%1000);
+ result = MAIL_DUPLICATE_LOCK_TIMEOUT;
+ }
+
+ e_error(trans->event, "Failed to lock %s: %s", lock->path, error);
+ i_free_and_null(lock->path);
+ i_zero(lock);
+ return result;
+}
+
+static bool mail_duplicate_is_locked(struct mail_duplicate *dup)
+{
+ struct mail_duplicate_lock *lock = &dup->lock;
+
+ return (lock->lock != NULL);
+}
+
+static enum mail_duplicate_lock_result
+mail_duplicate_lock(struct mail_duplicate_transaction *trans,
+ struct mail_duplicate *dup)
+{
+ struct file_create_settings lock_set = {
+ .lock_timeout_secs = DUPLICATE_LOCK_TIMEOUT_SECS,
+ .lock_settings = {
+ .lock_method = FILE_LOCK_METHOD_FCNTL,
+ .allow_deadlock = TRUE,
+ },
+ };
+ struct mail_duplicate_db *db = trans->db;
+ struct mail_duplicate_lock *lock = &dup->lock;
+ const char *error;
+ unsigned char id_md5[MD5_RESULTLEN];
+ bool created;
+ int diff;
+
+ if (mail_duplicate_is_locked(dup)) {
+ e_debug(trans->event, "Duplicate ID already locked");
+ return MAIL_DUPLICATE_LOCK_OK;
+ }
+ if (trans->id_lock_count >= DUPLICATE_LOCK_MAX_LOCKS) {
+ e_debug(trans->event, "Too many duplicate IDs locked");
+ return MAIL_DUPLICATE_LOCK_TOO_MANY;
+ }
+
+ i_assert(db->lock_dir != NULL);
+
+ lock->start_time = ioloop_timeval;
+ md5_get_digest(dup->id, dup->id_size, id_md5);
+ lock->path = i_strdup_printf("%s/"DUPLICATE_LOCK_FNAME_PREFIX"%s",
+ db->lock_dir,
+ binary_to_hex(id_md5, sizeof(id_md5)));
+
+ e_debug(trans->event, "Lock duplicate ID (path=%s)", lock->path);
+
+ lock->fd = file_create_locked(lock->path, &lock_set, &lock->lock,
+ &created, &error);
+ if (lock->fd == -1 && errno == ENOENT) {
+ /* parent directory missing - create it */
+ if (mkdir_parents(db->lock_dir, 0700) < 0 && errno != EEXIST) {
+ error = t_strdup_printf(
+ "mkdir_parents(%s) failed: %m", db->lock_dir);
+ } else {
+ lock->fd = file_create_locked(lock->path,
+ &lock_set, &lock->lock,
+ &created, &error);
+ }
+ }
+ if (lock->fd == -1)
+ return duplicate_lock_failed(trans, dup, error);
+
+ diff = timeval_diff_msecs(&ioloop_timeval, &lock->start_time);
+ if (diff >= (DUPLICATE_LOCK_WARN_SECS * 1000)) {
+ e_warning(trans->event, "Locking %s took %d.%03d secs",
+ lock->path, diff/1000, diff%1000);
+ }
+
+ i_assert(mail_duplicate_is_locked(dup));
+ trans->id_lock_count++;
+ return MAIL_DUPLICATE_LOCK_OK;
+}
+
+static void
+mail_duplicate_unlock(struct mail_duplicate_transaction *trans,
+ struct mail_duplicate *dup)
+{
+ int orig_errno = errno;
+
+ if (dup->lock.path != NULL) {
+ struct mail_duplicate_lock *lock = &dup->lock;
+
+ e_debug(trans->event, "Unlock duplicate ID (path=%s)",
+ lock->path);
+ i_unlink(lock->path);
+ file_lock_free(&lock->lock);
+ i_close_fd(&lock->fd);
+ i_free_and_null(lock->path);
+ i_zero(lock);
+
+ i_assert(trans->id_lock_count > 0);
+ trans->id_lock_count--;
+ }
+
+ errno = orig_errno;
+}
+
+static int
+mail_duplicate_read_records(struct mail_duplicate_transaction *trans,
+ struct istream *input,
+ unsigned int record_size)
+{
+ const unsigned char *data;
+ struct mail_duplicate_record_header hdr;
+ size_t size;
+ unsigned int change_count;
+
+ change_count = 0;
+ while (i_stream_read_bytes(input, &data, &size, record_size) > 0) {
+ if (record_size == sizeof(hdr))
+ memcpy(&hdr, data, sizeof(hdr));
+ else {
+ /* FIXME: backwards compatibility with v1.0 */
+ time_t stamp;
+
+ i_assert(record_size ==
+ sizeof(time_t) + sizeof(uint32_t)*2);
+ memcpy(&stamp, data, sizeof(stamp));
+ hdr.stamp = stamp;
+ memcpy(&hdr.id_size, data + sizeof(time_t),
+ sizeof(hdr.id_size));
+ memcpy(&hdr.user_size,
+ data + sizeof(time_t) + sizeof(uint32_t),
+ sizeof(hdr.user_size));
+ }
+ i_stream_skip(input, record_size);
+
+ if (hdr.id_size == 0 || hdr.user_size == 0 ||
+ hdr.id_size > DUPLICATE_BUFSIZE ||
+ hdr.user_size > DUPLICATE_BUFSIZE) {
+ e_error(trans->event,
+ "broken mail_duplicate file %s", trans->path);
+ return -1;
+ }
+
+ if (i_stream_read_bytes(input, &data, &size,
+ hdr.id_size + hdr.user_size) <= 0) {
+ e_error(trans->event,
+ "unexpected end of file in %s", trans->path);
+ return -1;
+ }
+
+ struct mail_duplicate dup_q, *dup;
+
+ dup_q.id = data;
+ dup_q.id_size = hdr.id_size;
+ dup_q.user = t_strndup(data + hdr.id_size, hdr.user_size);
+
+ dup = hash_table_lookup(trans->hash, &dup_q);
+ if ((time_t)hdr.stamp < ioloop_time) {
+ change_count++;
+ if (dup != NULL && !dup->changed)
+ dup->marked = FALSE;
+ } else {
+ if (dup == NULL) {
+ void *new_id;
+
+ new_id = p_malloc(trans->pool, hdr.id_size);
+ memcpy(new_id, data, hdr.id_size);
+
+ dup = p_new(trans->pool,
+ struct mail_duplicate, 1);
+ dup->id = new_id;
+ dup->id_size = hdr.id_size;
+ dup->user = p_strdup(trans->pool, dup_q.user);
+ hash_table_update(trans->hash, dup, dup);
+ }
+ if (!dup->changed) {
+ dup->marked = TRUE;
+ dup->time = hdr.stamp;
+ }
+ }
+ i_stream_skip(input, hdr.id_size + hdr.user_size);
+ }
+
+ if (hash_table_count(trans->hash) *
+ COMPRESS_PERCENTAGE / 100 > change_count)
+ trans->changed = TRUE;
+ return 0;
+}
+
+static int
+mail_duplicate_read_db_from_fd(struct mail_duplicate_transaction *trans, int fd)
+{
+ struct istream *input;
+ struct mail_duplicate_file_header hdr;
+ const unsigned char *data;
+ size_t size;
+ struct stat st;
+ unsigned int record_size = 0;
+
+ if (fstat(fd, &st) < 0) {
+ e_error(trans->event,
+ "stat(%s) failed: %m", trans->path);
+ return -1;
+ }
+ trans->db_ino = st.st_ino;
+
+ /* <timestamp> <id_size> <user_size> <id> <user> */
+ input = i_stream_create_fd(fd, DUPLICATE_BUFSIZE);
+ if (i_stream_read_bytes(input, &data, &size, sizeof(hdr)) > 0) {
+ memcpy(&hdr, data, sizeof(hdr));
+ if (hdr.version == 0 || hdr.version > DUPLICATE_VERSION + 10) {
+ /* FIXME: backwards compatibility with v1.0 */
+ record_size = sizeof(time_t) + sizeof(uint32_t)*2;
+ } else if (hdr.version == DUPLICATE_VERSION) {
+ record_size = sizeof(struct mail_duplicate_record_header);
+ i_stream_skip(input, sizeof(hdr));
+ }
+ }
+
+ if (record_size == 0)
+ i_unlink_if_exists(trans->path);
+ else T_BEGIN {
+ if (mail_duplicate_read_records(trans, input, record_size) < 0)
+ i_unlink_if_exists(trans->path);
+ } T_END;
+
+ i_stream_unref(&input);
+ return 0;
+}
+
+static int mail_duplicate_read_db_file(struct mail_duplicate_transaction *trans)
+{
+ int fd, ret;
+
+ e_debug(trans->event, "Reading %s", trans->path);
+
+ fd = open(trans->path, O_RDONLY);
+ if (fd == -1) {
+ if (errno == ENOENT)
+ return 0;
+ e_error(trans->event,
+ "open(%s) failed: %m", trans->path);
+ return -1;
+ }
+
+ ret = mail_duplicate_read_db_from_fd(trans, fd);
+
+ if (close(fd) < 0) {
+ e_error(trans->event,
+ "close(%s) failed: %m", trans->path);
+ }
+ return ret;
+}
+
+static void mail_duplicate_read(struct mail_duplicate_transaction *trans)
+{
+ struct mail_duplicate_db *db = trans->db;
+ int new_fd;
+ struct dotlock *dotlock;
+
+ new_fd = file_dotlock_open(&db->dotlock_set, trans->path, 0, &dotlock);
+ if (new_fd != -1)
+ ;
+ else if (errno != EAGAIN) {
+ e_error(trans->event,
+ "file_dotlock_open(%s) failed: %m", trans->path);
+ } else {
+ e_error(trans->event,
+ "Creating lock file for %s timed out in %u secs",
+ trans->path, db->dotlock_set.timeout);
+ }
+
+ (void)mail_duplicate_read_db_file(trans);
+
+ if (dotlock != NULL)
+ file_dotlock_delete(&dotlock);
+}
+
+static void mail_duplicate_update(struct mail_duplicate_transaction *trans)
+{
+ struct stat st;
+
+ if (stat(trans->path, &st) < 0) {
+ if (errno == ENOENT) {
+ e_debug(trans->event, "DB file not created yet");
+ } else {
+ e_error(trans->event,
+ "stat(%s) failed: %m", trans->path);
+ }
+ } else if (trans->db_ino == st.st_ino) {
+ e_debug(trans->event, "DB file not changed");
+ } else {
+ e_debug(trans->event, "DB file changed: "
+ "Updating duplicate records from DB file");
+
+ mail_duplicate_read(trans);
+ }
+}
+
+struct mail_duplicate_transaction *
+mail_duplicate_transaction_begin(struct mail_duplicate_db *db)
+{
+ struct mail_duplicate_transaction *trans;
+ pool_t pool;
+
+ db->transaction_count++;
+
+ pool = pool_alloconly_create("mail_duplicates", 10240);
+
+ trans = p_new(pool, struct mail_duplicate_transaction, 1);
+ trans->pool = pool;
+ trans->db = db;
+
+ trans->event = event_create(db->event);
+ event_set_append_log_prefix(trans->event, "transaction: ");
+
+ if (db->path == NULL) {
+ /* Duplicate database disabled; return dummy transaction */
+ e_debug(trans->event, "Transaction begin (dummy)");
+ return trans;
+ }
+
+ e_debug(trans->event, "Transaction begin; lock %s", db->path);
+
+ trans->path = p_strdup(pool, db->path);
+ hash_table_create(&trans->hash, pool, 0,
+ mail_duplicate_hash, mail_duplicate_cmp);
+
+ mail_duplicate_read(trans);
+
+ return trans;
+}
+
+static void
+mail_duplicate_transaction_free(struct mail_duplicate_transaction **_trans)
+{
+ struct mail_duplicate_transaction *trans = *_trans;
+ struct hash_iterate_context *iter;
+ struct mail_duplicate *d;
+
+ if (trans == NULL)
+ return;
+ *_trans = NULL;
+
+ e_debug(trans->event, "Transaction free");
+
+ i_assert(trans->db->transaction_count > 0);
+ trans->db->transaction_count--;
+
+ if (hash_table_is_created(trans->hash)) {
+ iter = hash_table_iterate_init(trans->hash);
+ while (hash_table_iterate(iter, trans->hash, &d, &d))
+ mail_duplicate_unlock(trans, d);
+ hash_table_iterate_deinit(&iter);
+ hash_table_destroy(&trans->hash);
+ }
+ i_assert(trans->id_lock_count == 0);
+
+ event_unref(&trans->event);
+ pool_unref(&trans->pool);
+}
+
+static struct mail_duplicate *
+mail_duplicate_get(struct mail_duplicate_transaction *trans,
+ const void *id, size_t id_size, const char *user)
+{
+ struct mail_duplicate dup_q, *dup;
+
+ dup_q.id = id;
+ dup_q.id_size = id_size;
+ dup_q.user = user;
+
+ dup = hash_table_lookup(trans->hash, &dup_q);
+ if (dup == NULL) {
+ dup = p_new(trans->pool, struct mail_duplicate, 1);
+ dup->id = p_memdup(trans->pool, id, id_size);
+ dup->id_size = id_size;
+ dup->user = p_strdup(trans->pool, user);
+ dup->time = (time_t)-1;
+
+ hash_table_insert(trans->hash, dup, dup);
+ }
+
+ return dup;
+}
+
+enum mail_duplicate_check_result
+mail_duplicate_check(struct mail_duplicate_transaction *trans,
+ const void *id, size_t id_size, const char *user)
+{
+ struct mail_duplicate *dup;
+
+ if (trans->path == NULL) {
+ /* Duplicate database disabled */
+ e_debug(trans->event, "Check ID (dummy)");
+ return MAIL_DUPLICATE_CHECK_RESULT_NOT_FOUND;
+ }
+
+ dup = mail_duplicate_get(trans, id, id_size, user);
+
+ switch (mail_duplicate_lock(trans, dup)) {
+ case MAIL_DUPLICATE_LOCK_OK:
+ break;
+ case MAIL_DUPLICATE_LOCK_IO_ERROR:
+ e_debug(trans->event,
+ "Check ID: I/O error occurred while locking");
+ return MAIL_DUPLICATE_CHECK_RESULT_IO_ERROR;
+ case MAIL_DUPLICATE_LOCK_TIMEOUT:
+ e_debug(trans->event,
+ "Check ID: lock timed out");
+ return MAIL_DUPLICATE_CHECK_RESULT_LOCK_TIMEOUT;
+ case MAIL_DUPLICATE_LOCK_TOO_MANY:
+ e_debug(trans->event,
+ "Check ID: too many IDs locked");
+ return MAIL_DUPLICATE_CHECK_RESULT_TOO_MANY_LOCKS;
+ case MAIL_DUPLICATE_LOCK_DEADLOCK:
+ e_debug(trans->event,
+ "Check ID: deadlock detected while locking");
+ return MAIL_DUPLICATE_CHECK_RESULT_DEADLOCK;
+ }
+
+ mail_duplicate_update(trans);
+ if (dup->marked) {
+ e_debug(trans->event, "Check ID: found");
+ return MAIL_DUPLICATE_CHECK_RESULT_EXISTS;
+ }
+
+ e_debug(trans->event, "Check ID: not found");
+ return MAIL_DUPLICATE_CHECK_RESULT_NOT_FOUND;
+}
+
+void mail_duplicate_mark(struct mail_duplicate_transaction *trans,
+ const void *id, size_t id_size,
+ const char *user, time_t timestamp)
+{
+ struct mail_duplicate *dup;
+
+ if (trans->path == NULL) {
+ /* Duplicate database disabled */
+ e_debug(trans->event, "Mark ID (dummy)");
+ return;
+ }
+
+ e_debug(trans->event, "Mark ID");
+
+ dup = mail_duplicate_get(trans, id, id_size, user);
+
+ /* Must already be checked and locked */
+ i_assert(mail_duplicate_is_locked(dup));
+
+ dup->time = timestamp;
+ dup->marked = TRUE;
+ dup->changed = TRUE;
+
+ trans->changed = TRUE;
+}
+
+void mail_duplicate_transaction_commit(
+ struct mail_duplicate_transaction **_trans)
+{
+ struct mail_duplicate_transaction *trans = *_trans;
+ struct mail_duplicate_file_header hdr;
+ struct mail_duplicate_record_header rec;
+ struct ostream *output;
+ struct hash_iterate_context *iter;
+ struct mail_duplicate *d;
+ int new_fd;
+ struct dotlock *dotlock;
+
+ if (trans == NULL)
+ return;
+ *_trans = NULL;
+
+ if (trans->path == NULL) {
+ e_debug(trans->event, "Commit (dummy)");
+ mail_duplicate_transaction_free(&trans);
+ return;
+ }
+ if (!trans->changed) {
+ e_debug(trans->event, "Commit; no changes");
+ mail_duplicate_transaction_free(&trans);
+ return;
+ }
+
+ struct mail_duplicate_db *db = trans->db;
+
+ i_assert(trans->path != NULL);
+ e_debug(trans->event, "Commit; overwrite %s", trans->path);
+
+ new_fd = file_dotlock_open(&db->dotlock_set, trans->path, 0, &dotlock);
+ if (new_fd != -1)
+ ;
+ else if (errno != EAGAIN) {
+ e_error(trans->event,
+ "file_dotlock_open(%s) failed: %m",
+ trans->path);
+ mail_duplicate_transaction_free(&trans);
+ return;
+ } else {
+ e_error(trans->event,
+ "Creating lock file for %s timed out in %u secs",
+ trans->path, db->dotlock_set.timeout);
+ mail_duplicate_transaction_free(&trans);
+ return;
+ }
+
+ i_zero(&hdr);
+ hdr.version = DUPLICATE_VERSION;
+
+ output = o_stream_create_fd_file(new_fd, 0, FALSE);
+ o_stream_cork(output);
+ o_stream_nsend(output, &hdr, sizeof(hdr));
+
+ i_zero(&rec);
+ iter = hash_table_iterate_init(trans->hash);
+ while (hash_table_iterate(iter, trans->hash, &d, &d)) {
+ if (d->marked) {
+ rec.stamp = d->time;
+ rec.id_size = d->id_size;
+ rec.user_size = strlen(d->user);
+
+ o_stream_nsend(output, &rec, sizeof(rec));
+ o_stream_nsend(output, d->id, rec.id_size);
+ o_stream_nsend(output, d->user, rec.user_size);
+ }
+ }
+ hash_table_iterate_deinit(&iter);
+
+ if (o_stream_finish(output) < 0) {
+ e_error(trans->event, "write(%s) failed: %s",
+ trans->path, o_stream_get_error(output));
+ o_stream_unref(&output);
+ mail_duplicate_transaction_free(&trans);
+ return;
+ }
+ o_stream_unref(&output);
+
+ if (file_dotlock_replace(&dotlock, 0) < 0) {
+ e_error(trans->event,
+ "file_dotlock_replace(%s) failed: %m", trans->path);
+ }
+
+ iter = hash_table_iterate_init(trans->hash);
+ while (hash_table_iterate(iter, trans->hash, &d, &d))
+ mail_duplicate_unlock(trans, d);
+ hash_table_iterate_deinit(&iter);
+
+ mail_duplicate_transaction_free(&trans);
+}
+
+void mail_duplicate_transaction_rollback(
+ struct mail_duplicate_transaction **_trans)
+{
+ struct mail_duplicate_transaction *trans = *_trans;
+
+ if (trans == NULL)
+ return;
+ *_trans = NULL;
+
+ if (trans->path == NULL)
+ e_debug(trans->event, "Rollback (dummy)");
+ else
+ e_debug(trans->event, "Rollback");
+
+ mail_duplicate_transaction_free(&trans);
+}
+
+struct mail_duplicate_db *
+mail_duplicate_db_init(struct mail_user *user, const char *name)
+{
+ struct mail_duplicate_db *db;
+ const struct mail_storage_settings *mail_set;
+ const char *home = NULL;
+ const char *lock_dir;
+
+ db = i_new(struct mail_duplicate_db, 1);
+
+ db->event = event_create(user->event);
+ event_set_append_log_prefix(db->event, "duplicate db: ");
+
+ e_debug(db->event, "Initialize");
+
+ db->user = user;
+
+ if (mail_user_get_home(user, &home) <= 0) {
+ e_error(db->event, "User %s doesn't have home dir set, "
+ "disabling duplicate database", user->username);
+ return db;
+ }
+
+ i_assert(home != NULL);
+
+ db->path = i_strconcat(home, "/.dovecot.", name, NULL);
+ db->dotlock_set = default_mail_duplicate_dotlock_set;
+
+ lock_dir = mail_user_get_volatile_dir(user);
+ if (lock_dir == NULL)
+ lock_dir = home;
+ db->lock_dir = i_strconcat(lock_dir, "/.dovecot.", name, ".locks",
+ NULL);
+
+ mail_set = mail_user_set_get_storage_set(user);
+ db->dotlock_set.use_excl_lock = mail_set->dotlock_use_excl;
+ db->dotlock_set.nfs_flush = mail_set->mail_nfs_storage;
+
+ return db;
+}
+
+void mail_duplicate_db_deinit(struct mail_duplicate_db **_db)
+{
+ struct mail_duplicate_db *db = *_db;
+
+ *_db = NULL;
+
+ e_debug(db->event, "Cleanup");
+
+ i_assert(db->transaction_count == 0);
+
+ event_unref(&db->event);
+ i_free(db->path);
+ i_free(db->lock_dir);
+ i_free(db);
+}
diff --git a/src/lib-storage/mail-duplicate.h b/src/lib-storage/mail-duplicate.h
new file mode 100644
index 0000000..c414893
--- /dev/null
+++ b/src/lib-storage/mail-duplicate.h
@@ -0,0 +1,51 @@
+#ifndef MAIL_DUPLICATE_H
+#define MAIL_DUPLICATE_H
+
+struct mail_duplicate_db;
+
+enum mail_duplicate_check_result {
+ /* The ID exists. The ID is not locked. */
+ MAIL_DUPLICATE_CHECK_RESULT_EXISTS,
+ /* The ID doesn't exist yet. The ID gets locked. */
+ MAIL_DUPLICATE_CHECK_RESULT_NOT_FOUND,
+ /* Internal I/O error (e.g. permission error) */
+ MAIL_DUPLICATE_CHECK_RESULT_IO_ERROR,
+ /* Locking timed out. */
+ MAIL_DUPLICATE_CHECK_RESULT_LOCK_TIMEOUT,
+ /* Too many locks held. */
+ MAIL_DUPLICATE_CHECK_RESULT_TOO_MANY_LOCKS,
+ /* Locking detected a deadlock. The caller should rollback the
+ transaction to release all locks, do a short random sleep, retry
+ and hope that the next attempt succeeds. */
+ MAIL_DUPLICATE_CHECK_RESULT_DEADLOCK,
+};
+
+#define MAIL_DUPLICATE_DEFAULT_KEEP (3600 * 24)
+
+struct mail_duplicate_transaction *
+mail_duplicate_transaction_begin(struct mail_duplicate_db *db);
+void mail_duplicate_transaction_rollback(
+ struct mail_duplicate_transaction **_trans);
+void mail_duplicate_transaction_commit(
+ struct mail_duplicate_transaction **_trans);
+
+/* Check if id exists in the duplicate database. If not, lock the id. Any
+ further checks for the same id in other processes will block until the first
+ one's transaction is finished. Because checks can be done in different order
+ by different processes, this can result in a deadlock. The caller should
+ handle it by rolling back the transaction and retrying. */
+enum mail_duplicate_check_result
+mail_duplicate_check(struct mail_duplicate_transaction *trans,
+ const void *id, size_t id_size, const char *user);
+/* Add id to the duplicate database. The writing isn't done until transaction
+ is committed. There's no locking done by this call. If locking is needed,
+ mail_duplicate_check() should be called first. */
+void mail_duplicate_mark(struct mail_duplicate_transaction *trans,
+ const void *id, size_t id_size,
+ const char *user, time_t timestamp);
+
+struct mail_duplicate_db *
+mail_duplicate_db_init(struct mail_user *user, const char *name);
+void mail_duplicate_db_deinit(struct mail_duplicate_db **db);
+
+#endif
diff --git a/src/lib-storage/mail-error.c b/src/lib-storage/mail-error.c
new file mode 100644
index 0000000..a3c6e00
--- /dev/null
+++ b/src/lib-storage/mail-error.c
@@ -0,0 +1,34 @@
+/* Copyright (c) 2007-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "eacces-error.h"
+#include "mail-error.h"
+
+bool mail_error_from_errno(enum mail_error *error_r,
+ const char **error_string_r)
+{
+ if (ENOACCESS(errno)) {
+ *error_r = MAIL_ERROR_PERM;
+ *error_string_r = MAIL_ERRSTR_NO_PERMISSION;
+ } else if (ENOQUOTA(errno)) {
+ *error_r = MAIL_ERROR_NOQUOTA;
+ *error_string_r = MAIL_ERRSTR_NO_QUOTA;
+ } else if (ENOTFOUND(errno)) {
+ *error_r = MAIL_ERROR_NOTFOUND;
+ *error_string_r = errno != ELOOP ? "Not found" :
+ "Directory structure is broken";
+ } else {
+ return FALSE;
+ }
+ return TRUE;
+}
+
+const char *mail_error_eacces_msg(const char *func, const char *path)
+{
+ return eacces_error_get(func, path);
+}
+
+const char *mail_error_create_eacces_msg(const char *func, const char *path)
+{
+ return eacces_error_get_creating(func, path);
+}
diff --git a/src/lib-storage/mail-error.h b/src/lib-storage/mail-error.h
new file mode 100644
index 0000000..9a0b08d
--- /dev/null
+++ b/src/lib-storage/mail-error.h
@@ -0,0 +1,70 @@
+#ifndef MAIL_ERROR_H
+#define MAIL_ERROR_H
+
+/* Some error strings that should be used everywhere to avoid
+ permissions checks from revealing mailbox's existence */
+#define MAIL_ERRSTR_MAILBOX_NOT_FOUND "Mailbox doesn't exist: %s"
+#define MAIL_ERRSTR_NO_PERMISSION "Permission denied"
+
+/* And just for making error strings consistent: */
+#define MAIL_ERRSTR_NO_QUOTA "Not enough disk quota"
+#define MAIL_ERRSTR_LOCK_TIMEOUT "Timeout while waiting for lock"
+
+/* Message to show to users when critical error occurs */
+#define MAIL_ERRSTR_CRITICAL_MSG \
+ "Internal error occurred. Refer to server log for more information."
+#define MAIL_ERRSTR_CRITICAL_MSG_STAMP \
+ MAIL_ERRSTR_CRITICAL_MSG " [%Y-%m-%d %H:%M:%S]"
+
+#define T_MAIL_ERR_MAILBOX_NOT_FOUND(name) \
+ t_strdup_printf(MAIL_ERRSTR_MAILBOX_NOT_FOUND, name)
+
+enum mail_error {
+ MAIL_ERROR_NONE = 0,
+
+ /* Temporary internal error */
+ MAIL_ERROR_TEMP,
+ /* Temporary failure because a subsystem is down */
+ MAIL_ERROR_UNAVAILABLE,
+ /* It's not possible to do the wanted operation */
+ MAIL_ERROR_NOTPOSSIBLE,
+ /* Invalid parameters (eg. mailbox name not valid) */
+ MAIL_ERROR_PARAMS,
+ /* No permission to do the request */
+ MAIL_ERROR_PERM,
+ /* Out of disk quota for user */
+ MAIL_ERROR_NOQUOTA,
+ /* Item (e.g. mailbox) doesn't exist or it's not visible to us */
+ MAIL_ERROR_NOTFOUND,
+ /* Item (e.g. mailbox) already exists */
+ MAIL_ERROR_EXISTS,
+ /* Tried to access an expunged message */
+ MAIL_ERROR_EXPUNGED,
+ /* Operation cannot be done because another session prevents it
+ (e.g. lock timeout) */
+ MAIL_ERROR_INUSE,
+ /* Can't do the requested data conversion (e.g. IMAP BINARY's
+ UNKNOWN-CTE code) */
+ MAIL_ERROR_CONVERSION,
+ /* Can't do the requested data conversion because the original data
+ isn't valid. */
+ MAIL_ERROR_INVALIDDATA,
+ /* Operation ran against some kind of a limit. */
+ MAIL_ERROR_LIMIT,
+ /* Operation couldn't be finished as efficiently as required by
+ mail.lookup_abort. */
+ MAIL_ERROR_LOOKUP_ABORTED,
+};
+
+/* Convert errno to mail_error and an error string. Returns TRUE if successful,
+ FALSE if we couldn't handle the errno. */
+bool mail_error_from_errno(enum mail_error *error_r,
+ const char **error_string_r);
+
+/* Build a helpful error message for a failed EACCES syscall. */
+const char *mail_error_eacces_msg(const char *func, const char *path);
+/* Build a helpful error message for a failed EACCES syscall that tried to
+ write to directory (create, rename, etc). */
+const char *mail_error_create_eacces_msg(const char *func, const char *path);
+
+#endif
diff --git a/src/lib-storage/mail-lua.c b/src/lib-storage/mail-lua.c
new file mode 100644
index 0000000..a559f98
--- /dev/null
+++ b/src/lib-storage/mail-lua.c
@@ -0,0 +1,143 @@
+/* Copyright (c) 2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "str.h"
+#include "istream.h"
+#include "array.h"
+#include "var-expand.h"
+#include "dlua-script.h"
+#include "dlua-script-private.h"
+#include "mail-storage.h"
+#include "mailbox-attribute.h"
+#include "mail-storage-lua.h"
+#include "mail-storage-lua-private.h"
+#include "mail-user.h"
+
+#define LUA_STORAGE_MAIL "struct mail"
+
+void dlua_push_mail(lua_State *L, struct mail *mail)
+{
+ luaL_checkstack(L, 20, "out of memory");
+ /* create a table for holding few things */
+ lua_createtable(L, 0, 20);
+ luaL_setmetatable(L, LUA_STORAGE_MAIL);
+
+ lua_pushlightuserdata(L, mail);
+ lua_setfield(L, -2, "item");
+
+#undef LUA_TABLE_SET_NUMBER
+#define LUA_TABLE_SET_NUMBER(field) \
+ lua_pushnumber(L, mail->field); \
+ lua_setfield(L, -2, #field);
+#undef LUA_TABLE_SET_BOOL
+#define LUA_TABLE_SET_BOOL(field) \
+ lua_pushboolean(L, mail->field); \
+ lua_setfield(L, -2, #field);
+
+ LUA_TABLE_SET_NUMBER(seq);
+ LUA_TABLE_SET_NUMBER(uid);
+ LUA_TABLE_SET_BOOL(expunged);
+
+ dlua_push_mailbox(L, mail->box);
+ lua_setfield(L, -2, "mailbox");
+
+}
+
+static struct mail *
+lua_check_storage_mail(lua_State *L, int arg)
+{
+ if (!lua_istable(L, arg)) {
+ (void)luaL_error(L, "Bad argument #%d, expected %s got %s",
+ arg, LUA_STORAGE_MAIL,
+ lua_typename(L, lua_type(L, arg)));
+ }
+ lua_pushliteral(L, "item");
+ lua_rawget(L, arg);
+ void *bp = (void*)lua_touserdata(L, -1);
+ lua_pop(L, 1);
+ return (struct mail*)bp;
+}
+
+static int lua_storage_mail_tostring(lua_State *L)
+{
+ DLUA_REQUIRE_ARGS(L, 1);
+ struct mail *mail = lua_check_storage_mail(L, 1);
+
+ const char *str =
+ t_strdup_printf("<%s:UID %u>", mailbox_get_vname(mail->box),
+ mail->uid);
+ lua_pushstring(L, str);
+ return 1;
+}
+
+static int lua_storage_mail_eq(lua_State *L)
+{
+ DLUA_REQUIRE_ARGS(L, 2);
+ struct mail *mail = lua_check_storage_mail(L, 1);
+ struct mail *mail2 = lua_check_storage_mail(L, 2);
+
+ if (!DLUA_MAILBOX_EQUALS(mail->box, mail2->box))
+ lua_pushboolean(L, FALSE);
+ else
+ lua_pushboolean(L, mail->uid != mail2->uid);
+ return 1;
+}
+
+static int lua_storage_mail_lt(lua_State *L)
+{
+ DLUA_REQUIRE_ARGS(L, 2);
+ struct mail *mail = lua_check_storage_mail(L, 1);
+ struct mail *mail2 = lua_check_storage_mail(L, 2);
+
+ if (!DLUA_MAILBOX_EQUALS(mail->box, mail2->box))
+ return luaL_error(L,
+ "For lt, Mail can only be compared within same mailbox");
+ else
+ lua_pushboolean(L, mail->uid < mail2->uid);
+ return 1;
+}
+
+static int lua_storage_mail_le(lua_State *L)
+{
+ DLUA_REQUIRE_ARGS(L, 2);
+ struct mail *mail = lua_check_storage_mail(L, 1);
+ struct mail *mail2 = lua_check_storage_mail(L, 2);
+
+ if (!DLUA_MAILBOX_EQUALS(mail->box, mail2->box))
+ return luaL_error(L,
+ "For le, mails can only be within same mailbox");
+ else
+ lua_pushboolean(L, mail->uid <= mail2->uid);
+
+ return 1;
+}
+
+static int lua_storage_mail_gc(lua_State *L)
+{
+ (void)lua_check_storage_mail(L, 1);
+
+ /* reset value to NULL */
+ lua_pushliteral(L, "item");
+ lua_pushnil(L);
+ lua_rawset(L, 1);
+
+ return 0;
+}
+
+static luaL_Reg lua_storage_mail_methods[] = {
+ { "__tostring", lua_storage_mail_tostring },
+ { "__eq", lua_storage_mail_eq },
+ { "__lt", lua_storage_mail_lt },
+ { "__le", lua_storage_mail_le },
+ { "__gc", lua_storage_mail_gc },
+ { NULL, NULL }
+};
+
+void lua_storage_mail_register(struct dlua_script *script)
+{
+ luaL_newmetatable(script->L, LUA_STORAGE_MAIL);
+ lua_pushvalue(script->L, -1);
+ lua_setfield(script->L, -2, "__index");
+ luaL_setfuncs(script->L, lua_storage_mail_methods, 0);
+ lua_pop(script->L, 1);
+}
diff --git a/src/lib-storage/mail-namespace.c b/src/lib-storage/mail-namespace.c
new file mode 100644
index 0000000..0b3d458
--- /dev/null
+++ b/src/lib-storage/mail-namespace.c
@@ -0,0 +1,866 @@
+/* Copyright (c) 2005-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "str.h"
+#include "file-lock.h"
+#include "settings-parser.h"
+#include "mailbox-list-private.h"
+#include "mail-storage-private.h"
+#include "mail-storage-settings.h"
+#include "mail-namespace.h"
+
+
+static struct mail_namespace_settings prefixless_ns_unexpanded_set = {
+ .name = "",
+ .type = "private",
+ .separator = "",
+ .prefix = "0",
+ .location = "0fail::LAYOUT=none",
+ .alias_for = NULL,
+
+ .inbox = FALSE,
+ .hidden = TRUE,
+ .list = "no",
+ .subscriptions = FALSE,
+ .ignore_on_failure = FALSE,
+ .disabled = FALSE,
+
+ .mailboxes = ARRAY_INIT
+};
+static struct mail_namespace_settings prefixless_ns_set;
+
+void mail_namespace_add_storage(struct mail_namespace *ns,
+ struct mail_storage *storage)
+{
+ if (ns->storage == NULL)
+ ns->storage = storage;
+ array_push_back(&ns->all_storages, &storage);
+
+ if (storage->v.add_list != NULL)
+ storage->v.add_list(storage, ns->list);
+ hook_mail_namespace_storage_added(ns);
+}
+
+void mail_namespace_finish_list_init(struct mail_namespace *ns,
+ struct mailbox_list *list)
+{
+ ns->list = list;
+ ns->prefix_len = strlen(ns->prefix);
+}
+
+static void mail_namespace_free(struct mail_namespace *ns)
+{
+ struct mail_storage *storage;
+
+ if (array_is_created(&ns->all_storages)) {
+ array_foreach_elem(&ns->all_storages, storage)
+ mail_storage_unref(&storage);
+ array_free(&ns->all_storages);
+ }
+ if (ns->list != NULL)
+ mailbox_list_destroy(&ns->list);
+
+ if (ns->owner != ns->user && ns->owner != NULL)
+ mail_user_unref(&ns->owner);
+ i_free(ns->prefix);
+ i_free(ns);
+}
+
+static bool
+namespace_has_special_use_mailboxes(struct mail_namespace_settings *ns_set)
+{
+ struct mailbox_settings *box_set;
+
+ if (!array_is_created(&ns_set->mailboxes))
+ return FALSE;
+
+ array_foreach_elem(&ns_set->mailboxes, box_set) {
+ if (box_set->special_use[0] != '\0')
+ return TRUE;
+ }
+ return FALSE;
+}
+
+int mail_namespace_alloc(struct mail_user *user,
+ void *user_all_settings,
+ struct mail_namespace_settings *ns_set,
+ struct mail_namespace_settings *unexpanded_set,
+ struct mail_namespace **ns_r,
+ const char **error_r)
+{
+ struct mail_namespace *ns;
+
+ ns = i_new(struct mail_namespace, 1);
+ ns->refcount = 1;
+ ns->user = user;
+ ns->prefix = i_strdup(ns_set->prefix);
+ ns->set = ns_set;
+ ns->unexpanded_set = unexpanded_set;
+ ns->user_set = user_all_settings;
+ ns->mail_set = mail_user_set_get_driver_settings(user->set_info,
+ ns->user_set, MAIL_STORAGE_SET_DRIVER_NAME);
+ i_array_init(&ns->all_storages, 2);
+
+ if (strcmp(ns_set->type, "private") == 0) {
+ ns->owner = user;
+ ns->type = MAIL_NAMESPACE_TYPE_PRIVATE;
+ } else if (strcmp(ns_set->type, "shared") == 0)
+ ns->type = MAIL_NAMESPACE_TYPE_SHARED;
+ else if (strcmp(ns_set->type, "public") == 0)
+ ns->type = MAIL_NAMESPACE_TYPE_PUBLIC;
+ else {
+ *error_r = t_strdup_printf("Unknown namespace type: %s",
+ ns_set->type);
+ mail_namespace_free(ns);
+ return -1;
+ }
+
+ if (strcmp(ns_set->list, "children") == 0)
+ ns->flags |= NAMESPACE_FLAG_LIST_CHILDREN;
+ else if (strcmp(ns_set->list, "yes") == 0)
+ ns->flags |= NAMESPACE_FLAG_LIST_PREFIX;
+ else if (strcmp(ns_set->list, "no") != 0) {
+ *error_r = t_strdup_printf("Invalid list setting value: %s",
+ ns_set->list);
+ mail_namespace_free(ns);
+ return -1;
+ }
+
+ if (ns_set->inbox) {
+ ns->flags |= NAMESPACE_FLAG_INBOX_USER |
+ NAMESPACE_FLAG_INBOX_ANY;
+ }
+ if (ns_set->hidden)
+ ns->flags |= NAMESPACE_FLAG_HIDDEN;
+ if (ns_set->subscriptions)
+ ns->flags |= NAMESPACE_FLAG_SUBSCRIPTIONS;
+
+ *ns_r = ns;
+
+ return 0;
+}
+
+int mail_namespaces_init_add(struct mail_user *user,
+ struct mail_namespace_settings *ns_set,
+ struct mail_namespace_settings *unexpanded_ns_set,
+ struct mail_namespace **ns_p, const char **error_r)
+{
+ const struct mail_storage_settings *mail_set =
+ mail_user_set_get_storage_set(user);
+ struct mail_namespace *ns;
+ const char *driver, *error;
+ int ret;
+
+ if (*ns_set->location == '\0')
+ ns_set->location = mail_set->mail_location;
+
+ e_debug(user->event, "Namespace %s: type=%s, prefix=%s, sep=%s, "
+ "inbox=%s, hidden=%s, list=%s, subscriptions=%s "
+ "location=%s",
+ ns_set->name, ns_set->type, ns_set->prefix,
+ ns_set->separator == NULL ? "" : ns_set->separator,
+ ns_set->inbox ? "yes" : "no",
+ ns_set->hidden ? "yes" : "no",
+ ns_set->list,
+ ns_set->subscriptions ? "yes" : "no", ns_set->location);
+
+ if ((ret = mail_namespace_alloc(user, user->set,
+ ns_set, unexpanded_ns_set,
+ &ns, error_r)) < 0)
+ return ret;
+
+ if (ns_set == &prefixless_ns_set) {
+ /* autocreated prefix="" namespace */
+ ns->flags |= NAMESPACE_FLAG_UNUSABLE |
+ NAMESPACE_FLAG_AUTOCREATED;
+ }
+
+ ns->special_use_mailboxes = namespace_has_special_use_mailboxes(ns_set);
+
+ if (ns->type == MAIL_NAMESPACE_TYPE_SHARED &&
+ (strchr(ns->prefix, '%') != NULL ||
+ strchr(ns->set->location, '%') != NULL)) {
+ /* dynamic shared namespace. the above check catches wrong
+ mixed %% usage, but still allows for specifying a shared
+ namespace to an explicit location without any %% */
+ ns->flags |= NAMESPACE_FLAG_NOQUOTA | NAMESPACE_FLAG_NOACL;
+ driver = MAIL_SHARED_STORAGE_NAME;
+ } else {
+ driver = NULL;
+ }
+
+ if (mail_storage_create(ns, driver, 0, &error) < 0) {
+ *error_r = t_strdup_printf("Namespace '%s': %s",
+ ns->prefix, error);
+ mail_namespace_free(ns);
+ return -1;
+ }
+
+ *ns_p = ns;
+ return 0;
+}
+
+static bool namespace_is_valid_alias_storage(struct mail_namespace *ns,
+ const char **error_r)
+{
+ if (strcmp(ns->storage->name, ns->alias_for->storage->name) != 0) {
+ *error_r = t_strdup_printf(
+ "Namespace %s can't have alias_for=%s "
+ "to a different storage type (%s vs %s)",
+ ns->prefix, ns->alias_for->prefix,
+ ns->storage->name, ns->alias_for->storage->name);
+ return FALSE;
+ }
+
+ if ((ns->storage->class_flags & MAIL_STORAGE_CLASS_FLAG_UNIQUE_ROOT) != 0 &&
+ ns->storage != ns->alias_for->storage) {
+ *error_r = t_strdup_printf(
+ "Namespace %s can't have alias_for=%s "
+ "to a different storage (different root dirs)",
+ ns->prefix, ns->alias_for->prefix);
+ return FALSE;
+ }
+ return TRUE;
+}
+
+static int
+namespace_set_alias_for(struct mail_namespace *ns,
+ struct mail_namespace *all_namespaces,
+ const char **error_r)
+{
+ if (ns->set->alias_for != NULL) {
+ ns->alias_for = mail_namespace_find_prefix(all_namespaces,
+ ns->set->alias_for);
+ if (ns->alias_for == NULL) {
+ *error_r = t_strdup_printf("Invalid namespace alias_for: %s",
+ ns->set->alias_for);
+ return -1;
+ }
+ if (ns->alias_for->alias_for != NULL) {
+ *error_r = t_strdup_printf("Chained namespace alias_for: %s",
+ ns->set->alias_for);
+ return -1;
+ }
+ if (!namespace_is_valid_alias_storage(ns, error_r))
+ return -1;
+
+ if ((ns->alias_for->flags & NAMESPACE_FLAG_INBOX_USER) != 0) {
+ /* copy inbox=yes */
+ ns->flags |= NAMESPACE_FLAG_INBOX_USER;
+ }
+
+ ns->alias_chain_next = ns->alias_for->alias_chain_next;
+ ns->alias_for->alias_chain_next = ns;
+ }
+ return 0;
+}
+
+static bool get_listindex_path(struct mail_namespace *ns, const char **path_r)
+{
+ const char *root;
+
+ if (ns->list->set.list_index_fname[0] == '\0' ||
+ !mailbox_list_get_root_path(ns->list,
+ MAILBOX_LIST_PATH_TYPE_LIST_INDEX,
+ &root))
+ return FALSE;
+
+ *path_r = t_strconcat(root, "/", ns->list->set.list_index_fname, NULL);
+ return TRUE;
+}
+
+static bool
+namespace_has_duplicate_listindex(struct mail_namespace *ns,
+ const char **error_r)
+{
+ struct mail_namespace *ns2;
+ const char *ns_list_index_path, *ns_mailboxes_root;
+ const char *ns2_list_index_path, *ns2_mailboxes_root;
+
+ if (!ns->mail_set->mailbox_list_index) {
+ /* mailbox list indexes not in use */
+ return FALSE;
+ }
+
+ if (!get_listindex_path(ns, &ns_list_index_path) ||
+ !mailbox_list_get_root_path(ns->list, MAILBOX_LIST_PATH_TYPE_MAILBOX,
+ &ns_mailboxes_root))
+ return FALSE;
+
+ for (ns2 = ns->next; ns2 != NULL; ns2 = ns2->next) {
+ if (!get_listindex_path(ns2, &ns2_list_index_path) ||
+ !mailbox_list_get_root_path(ns2->list, MAILBOX_LIST_PATH_TYPE_MAILBOX,
+ &ns2_mailboxes_root))
+ continue;
+
+ if (strcmp(ns_list_index_path, ns2_list_index_path) == 0 &&
+ strcmp(ns_mailboxes_root, ns2_mailboxes_root) != 0) {
+ *error_r = t_strdup_printf(
+ "Namespaces '%s' and '%s' have different mailboxes paths, but duplicate LISTINDEX path. "
+ "Add a unique LISTINDEX=<fname>",
+ ns->prefix, ns2->prefix);
+ return TRUE;
+ }
+ }
+ return FALSE;
+}
+
+static bool
+namespaces_check(struct mail_namespace *namespaces, const char **error_r)
+{
+ struct mail_namespace *ns, *inbox_ns = NULL;
+ unsigned int subscriptions_count = 0;
+ bool visible_namespaces = FALSE, have_list_yes = FALSE;
+ char ns_sep, list_sep = '\0';
+
+ for (ns = namespaces; ns != NULL; ns = ns->next) {
+ ns_sep = mail_namespace_get_sep(ns);
+ if (mail_namespace_find_prefix(ns->next, ns->prefix) != NULL) {
+ *error_r = t_strdup_printf(
+ "Duplicate namespace prefix: \"%s\"",
+ ns->prefix);
+ return FALSE;
+ }
+ if ((ns->flags & NAMESPACE_FLAG_HIDDEN) == 0)
+ visible_namespaces = TRUE;
+ /* check the inbox=yes status before alias_for changes it */
+ if ((ns->flags & NAMESPACE_FLAG_INBOX_USER) != 0) {
+ if (inbox_ns != NULL) {
+ *error_r = "There can be only one namespace with "
+ "inbox=yes";
+ return FALSE;
+ }
+ inbox_ns = ns;
+ }
+ if (namespace_set_alias_for(ns, namespaces, error_r) < 0)
+ return FALSE;
+ if (namespace_has_duplicate_listindex(ns, error_r))
+ return FALSE;
+
+ if (*ns->prefix != '\0' &&
+ (ns->flags & (NAMESPACE_FLAG_LIST_PREFIX |
+ NAMESPACE_FLAG_LIST_CHILDREN)) != 0 &&
+ ns->prefix[strlen(ns->prefix)-1] != ns_sep) {
+ *error_r = t_strdup_printf(
+ "list=yes requires prefix=%s "
+ "to end with separator %c", ns->prefix, ns_sep);
+ return FALSE;
+ }
+ if (*ns->prefix != '\0' &&
+ (ns->flags & (NAMESPACE_FLAG_LIST_PREFIX |
+ NAMESPACE_FLAG_LIST_CHILDREN)) != 0 &&
+ ns->prefix[0] == ns_sep) {
+ *error_r = t_strdup_printf(
+ "list=yes requires prefix=%s "
+ "not to start with separator", ns->prefix);
+ return FALSE;
+ }
+ if ((ns->flags & (NAMESPACE_FLAG_LIST_PREFIX |
+ NAMESPACE_FLAG_LIST_CHILDREN)) != 0) {
+ if ((ns->flags & NAMESPACE_FLAG_LIST_PREFIX) != 0)
+ have_list_yes = TRUE;
+ if (list_sep == '\0')
+ list_sep = ns_sep;
+ else if (list_sep != ns_sep) {
+ *error_r = "All list=yes namespaces must use "
+ "the same separator";
+ return FALSE;
+ }
+ }
+ if ((ns->flags & NAMESPACE_FLAG_SUBSCRIPTIONS) != 0)
+ subscriptions_count++;
+ }
+
+ if (inbox_ns == NULL) {
+ *error_r = "inbox=yes namespace missing";
+ return FALSE;
+ }
+ if (!have_list_yes) {
+ *error_r = "list=yes namespace missing";
+ return FALSE;
+ }
+ if (!visible_namespaces) {
+ *error_r = "hidden=no namespace missing";
+ return FALSE;
+ }
+ if (subscriptions_count == 0) {
+ *error_r = "subscriptions=yes namespace missing";
+ return FALSE;
+ }
+ return TRUE;
+}
+
+int mail_namespaces_init_finish(struct mail_namespace *namespaces,
+ const char **error_r)
+{
+ struct mail_namespace *ns;
+ bool prefixless_found = FALSE;
+
+ i_assert(namespaces != NULL);
+
+ for (ns = namespaces; ns != NULL; ns = ns->next) {
+ if (ns->prefix_len == 0)
+ prefixless_found = TRUE;
+ }
+ if (!prefixless_found) {
+ prefixless_ns_set = prefixless_ns_unexpanded_set;
+ /* a pretty evil way to expand the values */
+ prefixless_ns_set.prefix++;
+ prefixless_ns_set.location++;
+
+ if (mail_namespaces_init_add(namespaces->user,
+ &prefixless_ns_set,
+ &prefixless_ns_unexpanded_set,
+ &ns, error_r) < 0)
+ i_unreached();
+ ns->next = namespaces;
+ namespaces = ns;
+ }
+ if (namespaces->user->autocreated) {
+ /* e.g. raw user - don't check namespaces' validity */
+ } else if (!namespaces_check(namespaces, error_r)) {
+ namespaces->user->error =
+ t_strconcat("namespace configuration error: ",
+ *error_r, NULL);
+ }
+
+ if (namespaces->user->error == NULL) {
+ mail_user_add_namespace(namespaces->user, &namespaces);
+ T_BEGIN {
+ hook_mail_namespaces_created(namespaces);
+ } T_END;
+ }
+
+ /* allow namespace hooks to return failure via the user error */
+ if (namespaces->user->error != NULL) {
+ namespaces->user->namespaces = NULL;
+ *error_r = t_strdup(namespaces->user->error);
+ while (namespaces != NULL) {
+ ns = namespaces;
+ namespaces = ns->next;
+ mail_namespace_free(ns);
+ }
+ return -1;
+ }
+
+ namespaces->user->namespaces_created = TRUE;
+ return 0;
+}
+
+int mail_namespaces_init(struct mail_user *user, const char **error_r)
+{
+ struct mail_namespace_settings *const *ns_set;
+ struct mail_namespace_settings *const *unexpanded_ns_set;
+ struct mail_namespace *namespaces, **ns_p;
+ unsigned int i, count, count2;
+
+ i_assert(user->initialized);
+
+ namespaces = NULL; ns_p = &namespaces;
+
+ if (array_is_created(&user->set->namespaces)) {
+ ns_set = array_get(&user->set->namespaces, &count);
+ unexpanded_ns_set =
+ array_get(&user->unexpanded_set->namespaces, &count2);
+ i_assert(count == count2);
+ } else {
+ ns_set = unexpanded_ns_set = NULL;
+ count = 0;
+ }
+ for (i = 0; i < count; i++) {
+ if (ns_set[i]->disabled)
+ continue;
+
+ if (mail_namespaces_init_add(user, ns_set[i],
+ unexpanded_ns_set[i],
+ ns_p, error_r) < 0) {
+ if (!ns_set[i]->ignore_on_failure) {
+ mail_namespaces_deinit(&namespaces);
+ return -1;
+ }
+ e_debug(user->event, "Skipping namespace %s: %s",
+ ns_set[i]->prefix, *error_r);
+ } else {
+ ns_p = &(*ns_p)->next;
+ }
+ }
+
+ if (namespaces == NULL) {
+ /* no namespaces defined, create a default one */
+ return mail_namespaces_init_location(user, NULL, error_r);
+ }
+ return mail_namespaces_init_finish(namespaces, error_r);
+}
+
+int mail_namespaces_init_location(struct mail_user *user, const char *location,
+ const char **error_r)
+{
+ struct mail_namespace_settings *inbox_set, *unexpanded_inbox_set;
+ struct mail_namespace *ns;
+ const struct mail_storage_settings *mail_set;
+ const char *error, *driver, *location_source;
+ bool default_location = FALSE;
+ int ret;
+
+ i_assert(location == NULL || *location != '\0');
+
+ inbox_set = p_new(user->pool, struct mail_namespace_settings, 1);
+ *inbox_set = mail_namespace_default_settings;
+ inbox_set->inbox = TRUE;
+ /* enums must be changed */
+ inbox_set->type = "private";
+ inbox_set->list = "yes";
+
+ unexpanded_inbox_set = p_new(user->pool, struct mail_namespace_settings, 1);
+ *unexpanded_inbox_set = *inbox_set;
+
+ driver = NULL;
+ mail_set = mail_user_set_get_storage_set(user);
+ if (location != NULL) {
+ inbox_set->location = p_strdup(user->pool, location);
+ location_source = "mail_location parameter";
+ } else if (*mail_set->mail_location != '\0') {
+ location_source = "mail_location setting";
+ inbox_set->location = mail_set->mail_location;
+ default_location = TRUE;
+ } else {
+ location_source = "environment MAIL";
+ inbox_set->location = getenv("MAIL");
+ }
+ if (inbox_set->location == NULL) {
+ /* support also maildir-specific environment */
+ inbox_set->location = getenv("MAILDIR");
+ if (inbox_set->location == NULL)
+ inbox_set->location = "";
+ else {
+ driver = "maildir";
+ location_source = "environment MAILDIR";
+ }
+ }
+ if (default_location) {
+ /* treat this the same as if a namespace was created with
+ default settings. dsync relies on finding a namespace
+ without explicit location setting. */
+ unexpanded_inbox_set->location = SETTING_STRVAR_UNEXPANDED;
+ } else {
+ unexpanded_inbox_set->location =
+ p_strconcat(user->pool, SETTING_STRVAR_EXPANDED,
+ inbox_set->location, NULL);
+ }
+
+ if ((ret = mail_namespace_alloc(user, user->set,
+ inbox_set, unexpanded_inbox_set,
+ &ns, error_r)) < 0)
+ return ret;
+
+ if (mail_storage_create(ns, driver, 0, &error) < 0) {
+ if (*inbox_set->location != '\0') {
+ *error_r = t_strdup_printf(
+ "Initializing mail storage from %s "
+ "failed: %s", location_source, error);
+ } else {
+ *error_r = t_strdup_printf("mail_location not set and "
+ "autodetection failed: %s", error);
+ }
+ mail_namespace_free(ns);
+ return -1;
+ }
+ return mail_namespaces_init_finish(ns, error_r);
+}
+
+struct mail_namespace *mail_namespaces_init_empty(struct mail_user *user)
+{
+ struct mail_namespace *ns;
+
+ ns = i_new(struct mail_namespace, 1);
+ ns->refcount = 1;
+ ns->user = user;
+ ns->owner = user;
+ ns->prefix = i_strdup("");
+ ns->flags = NAMESPACE_FLAG_INBOX_USER | NAMESPACE_FLAG_INBOX_ANY |
+ NAMESPACE_FLAG_LIST_PREFIX | NAMESPACE_FLAG_SUBSCRIPTIONS;
+ ns->user_set = user->set;
+ ns->mail_set = mail_user_set_get_storage_set(user);
+ i_array_init(&ns->all_storages, 2);
+ return ns;
+}
+
+void mail_namespaces_deinit(struct mail_namespace **_namespaces)
+{
+ struct mail_namespace *ns, *next;
+
+ /* update *_namespaces as needed, instead of immediately setting it
+ to NULL. for example mdbox_storage.destroy() wants to go through
+ user's namespaces. */
+ while (*_namespaces != NULL) {
+ ns = *_namespaces;
+ next = ns->next;
+
+ mail_namespace_free(ns);
+ *_namespaces = next;
+ }
+}
+
+void mail_namespaces_set_storage_callbacks(struct mail_namespace *namespaces,
+ struct mail_storage_callbacks *callbacks,
+ void *context)
+{
+ struct mail_namespace *ns;
+ struct mail_storage *storage;
+
+ for (ns = namespaces; ns != NULL; ns = ns->next) {
+ array_foreach_elem(&ns->all_storages, storage)
+ mail_storage_set_callbacks(storage, callbacks, context);
+ }
+}
+
+void mail_namespace_ref(struct mail_namespace *ns)
+{
+ i_assert(ns->refcount > 0);
+
+ ns->refcount++;
+}
+
+void mail_namespace_unref(struct mail_namespace **_ns)
+{
+ struct mail_namespace *ns = *_ns;
+
+ i_assert(ns->refcount > 0);
+
+ *_ns = NULL;
+
+ if (--ns->refcount > 0)
+ return;
+
+ i_assert(ns->destroyed);
+ mail_namespace_free(ns);
+}
+
+void mail_namespace_destroy(struct mail_namespace *ns)
+{
+ struct mail_namespace **nsp;
+
+ i_assert(!ns->destroyed);
+
+ /* remove from user's namespaces list */
+ for (nsp = &ns->user->namespaces; *nsp != NULL; nsp = &(*nsp)->next) {
+ if (*nsp == ns) {
+ *nsp = ns->next;
+ break;
+ }
+ }
+ ns->destroyed = TRUE;
+
+ mail_namespace_unref(&ns);
+}
+
+struct mail_storage *
+mail_namespace_get_default_storage(struct mail_namespace *ns)
+{
+ return ns->storage;
+}
+
+char mail_namespace_get_sep(struct mail_namespace *ns)
+{
+ return *ns->set->separator != '\0' ? *ns->set->separator :
+ mailbox_list_get_hierarchy_sep(ns->list);
+}
+
+char mail_namespaces_get_root_sep(struct mail_namespace *namespaces)
+{
+ while ((namespaces->flags & NAMESPACE_FLAG_LIST_PREFIX) == 0)
+ namespaces = namespaces->next;
+ return mail_namespace_get_sep(namespaces);
+}
+
+static bool mail_namespace_is_usable_prefix(struct mail_namespace *ns,
+ const char *mailbox, bool inbox)
+{
+ if (strncmp(ns->prefix, mailbox, ns->prefix_len) == 0) {
+ /* true exact prefix match */
+ return TRUE;
+ }
+
+ if (inbox && str_begins(ns->prefix, "INBOX") &&
+ strncmp(ns->prefix+5, mailbox+5, ns->prefix_len-5) == 0) {
+ /* we already checked that mailbox begins with case-insensitive
+ INBOX. this namespace also begins with INBOX and the rest
+ of the prefix matches too. */
+ return TRUE;
+ }
+
+ if (strncmp(ns->prefix, mailbox, ns->prefix_len-1) == 0 &&
+ mailbox[ns->prefix_len-1] == '\0' &&
+ ns->prefix[ns->prefix_len-1] == mail_namespace_get_sep(ns)) {
+ /* we're trying to access the namespace prefix itself */
+ return TRUE;
+ }
+ return FALSE;
+}
+
+static struct mail_namespace *
+mail_namespace_find_mask(struct mail_namespace *namespaces, const char *box,
+ enum namespace_flags flags,
+ enum namespace_flags mask)
+{
+ struct mail_namespace *ns = namespaces;
+ struct mail_namespace *best = NULL;
+ size_t best_len = 0;
+ bool inbox;
+
+ inbox = strncasecmp(box, "INBOX", 5) == 0;
+ if (inbox && box[5] == '\0') {
+ /* find the INBOX namespace */
+ while (ns != NULL) {
+ if ((ns->flags & NAMESPACE_FLAG_INBOX_USER) != 0 &&
+ (ns->flags & mask) == flags)
+ return ns;
+ if (*ns->prefix == '\0')
+ best = ns;
+ ns = ns->next;
+ }
+ return best;
+ }
+
+ for (; ns != NULL; ns = ns->next) {
+ if (ns->prefix_len >= best_len && (ns->flags & mask) == flags &&
+ mail_namespace_is_usable_prefix(ns, box, inbox)) {
+ best = ns;
+ best_len = ns->prefix_len;
+ }
+ }
+ return best;
+}
+
+static struct mail_namespace *
+mail_namespace_find_shared(struct mail_namespace *ns, const char *mailbox)
+{
+ struct mailbox_list *list = ns->list;
+ struct mail_storage *storage;
+
+ if (mailbox_list_get_storage(&list, mailbox, &storage) < 0)
+ return ns;
+
+ return mailbox_list_get_namespace(list);
+}
+
+struct mail_namespace *
+mail_namespace_find(struct mail_namespace *namespaces, const char *mailbox)
+{
+ struct mail_namespace *ns;
+
+ ns = mail_namespace_find_mask(namespaces, mailbox, 0, 0);
+ i_assert(ns != NULL);
+
+ if (mail_namespace_is_shared_user_root(ns)) {
+ /* see if we need to autocreate a namespace for shared user */
+ if (strchr(mailbox, mail_namespace_get_sep(ns)) != NULL)
+ return mail_namespace_find_shared(ns, mailbox);
+ }
+ return ns;
+}
+
+struct mail_namespace *
+mail_namespace_find_unalias(struct mail_namespace *namespaces,
+ const char **mailbox)
+{
+ struct mail_namespace *ns;
+ const char *storage_name;
+
+ ns = mail_namespace_find(namespaces, *mailbox);
+ if (ns->alias_for != NULL) {
+ storage_name =
+ mailbox_list_get_storage_name(ns->list, *mailbox);
+ ns = ns->alias_for;
+ *mailbox = mailbox_list_get_vname(ns->list, storage_name);
+ }
+ return ns;
+}
+
+struct mail_namespace *
+mail_namespace_find_visible(struct mail_namespace *namespaces,
+ const char *mailbox)
+{
+ return mail_namespace_find_mask(namespaces, mailbox, 0,
+ NAMESPACE_FLAG_HIDDEN);
+}
+
+struct mail_namespace *
+mail_namespace_find_subscribable(struct mail_namespace *namespaces,
+ const char *mailbox)
+{
+ return mail_namespace_find_mask(namespaces, mailbox,
+ NAMESPACE_FLAG_SUBSCRIPTIONS,
+ NAMESPACE_FLAG_SUBSCRIPTIONS);
+}
+
+struct mail_namespace *
+mail_namespace_find_unsubscribable(struct mail_namespace *namespaces,
+ const char *mailbox)
+{
+ return mail_namespace_find_mask(namespaces, mailbox,
+ 0, NAMESPACE_FLAG_SUBSCRIPTIONS);
+}
+
+struct mail_namespace *
+mail_namespace_find_inbox(struct mail_namespace *namespaces)
+{
+ i_assert(namespaces != NULL);
+
+ /* there should always be an INBOX */
+ while ((namespaces->flags & NAMESPACE_FLAG_INBOX_USER) == 0) {
+ namespaces = namespaces->next;
+ i_assert(namespaces != NULL);
+ }
+ return namespaces;
+}
+
+struct mail_namespace *
+mail_namespace_find_prefix(struct mail_namespace *namespaces,
+ const char *prefix)
+{
+ struct mail_namespace *ns;
+ size_t len = strlen(prefix);
+
+ for (ns = namespaces; ns != NULL; ns = ns->next) {
+ if (ns->prefix_len == len &&
+ strcmp(ns->prefix, prefix) == 0)
+ return ns;
+ }
+ return NULL;
+}
+
+struct mail_namespace *
+mail_namespace_find_prefix_nosep(struct mail_namespace *namespaces,
+ const char *prefix)
+{
+ struct mail_namespace *ns;
+ size_t len = strlen(prefix);
+
+ for (ns = namespaces; ns != NULL; ns = ns->next) {
+ if (ns->prefix_len == len + 1 &&
+ strncmp(ns->prefix, prefix, len) == 0 &&
+ ns->prefix[len] == mail_namespace_get_sep(ns))
+ return ns;
+ }
+ return NULL;
+}
+
+bool mail_namespace_is_shared_user_root(struct mail_namespace *ns)
+{
+ struct mail_storage *storage;
+
+ if (ns->type != MAIL_NAMESPACE_TYPE_SHARED)
+ return FALSE;
+ if ((ns->flags & NAMESPACE_FLAG_AUTOCREATED) != 0) {
+ /* child of the shared root */
+ return FALSE;
+ }
+ /* if we have driver=shared storage, we're a real shared root */
+ array_foreach_elem(&ns->all_storages, storage) {
+ if (strcmp(storage->name, MAIL_SHARED_STORAGE_NAME) == 0)
+ return TRUE;
+ }
+ return FALSE;
+}
diff --git a/src/lib-storage/mail-namespace.h b/src/lib-storage/mail-namespace.h
new file mode 100644
index 0000000..ae54f59
--- /dev/null
+++ b/src/lib-storage/mail-namespace.h
@@ -0,0 +1,221 @@
+#ifndef MAIL_NAMESPACE_H
+#define MAIL_NAMESPACE_H
+
+#include "mail-user.h"
+
+struct mail_storage_callbacks;
+
+enum mail_namespace_type {
+ MAIL_NAMESPACE_TYPE_PRIVATE = 0x01,
+ MAIL_NAMESPACE_TYPE_SHARED = 0x02,
+ MAIL_NAMESPACE_TYPE_PUBLIC = 0x04
+#define MAIL_NAMESPACE_TYPE_MASK_ALL \
+ (MAIL_NAMESPACE_TYPE_PRIVATE | MAIL_NAMESPACE_TYPE_SHARED | \
+ MAIL_NAMESPACE_TYPE_PUBLIC)
+};
+
+enum namespace_flags {
+ /* Namespace contains the user's INBOX mailbox. Normally only a single
+ namespace has this flag set, but when using alias_for for the INBOX
+ namespace the flag gets copied to the alias namespace as well */
+ NAMESPACE_FLAG_INBOX_USER = 0x01,
+ /* Namespace contains someone's INBOX. This is set for both user's
+ INBOX namespace and also for any other users' shared namespaces. */
+ NAMESPACE_FLAG_INBOX_ANY = 0x02,
+ /* Namespace is visible only by explicitly using its full prefix */
+ NAMESPACE_FLAG_HIDDEN = 0x04,
+ /* Namespace prefix is visible with LIST */
+ NAMESPACE_FLAG_LIST_PREFIX = 0x08,
+ /* Namespace prefix isn't visible with LIST, but child mailboxes are */
+ NAMESPACE_FLAG_LIST_CHILDREN = 0x10,
+ /* Namespace uses its own subscriptions. */
+ NAMESPACE_FLAG_SUBSCRIPTIONS = 0x20,
+
+ /* Namespace was created automatically (for shared mailboxes) */
+ NAMESPACE_FLAG_AUTOCREATED = 0x1000,
+ /* Namespace has at least some usable mailboxes. Autocreated namespaces
+ that don't have usable mailboxes may be removed automatically. */
+ NAMESPACE_FLAG_USABLE = 0x2000,
+ /* Automatically created namespace for a user that doesn't exist. */
+ NAMESPACE_FLAG_UNUSABLE = 0x4000,
+ /* Don't track quota for this namespace */
+ NAMESPACE_FLAG_NOQUOTA = 0x8000,
+ /* Don't enforce ACLs for this namespace */
+ NAMESPACE_FLAG_NOACL = 0x10000
+};
+
+struct mail_namespace {
+ /* Namespaces are sorted by their prefix length, "" comes first */
+ struct mail_namespace *next;
+ int refcount;
+
+ enum mail_namespace_type type;
+ enum namespace_flags flags;
+
+ char *prefix;
+ size_t prefix_len;
+
+ /* If non-NULL, this points to a namespace with identical mail location
+ and it should be considered as the primary way to access the
+ mailboxes. This allows for example FTS plugin to avoid duplicating
+ indexes for same mailboxes when they're accessed via different
+ namespaces. */
+ struct mail_namespace *alias_for;
+ /* alias_for->alias_chain_next starts each chain. The chain goes
+ through all namespaces that have the same alias_for. */
+ struct mail_namespace *alias_chain_next;
+
+ struct mail_user *user, *owner;
+ struct mailbox_list *list;
+ struct mail_storage *storage; /* default storage */
+ ARRAY(struct mail_storage *) all_storages;
+
+ /* This may point to user->set, but it may also point to
+ namespace-specific settings. When accessing namespace-specific
+ settings it should be done through here instead of through the
+ mail_user. */
+ struct mail_user_settings *user_set;
+
+ const struct mail_namespace_settings *set, *unexpanded_set;
+ const struct mail_storage_settings *mail_set;
+
+ bool special_use_mailboxes:1;
+ bool destroyed:1;
+};
+
+/* Returns TRUE when namespace can be removed without consequence. */
+static inline bool mail_namespace_is_removable(const struct mail_namespace *ns)
+{
+ return ((ns->flags & NAMESPACE_FLAG_USABLE) == 0 &&
+ (ns->flags & NAMESPACE_FLAG_AUTOCREATED) != 0);
+}
+
+/* Allocate a new namespace, and fill it based on the passed in settings.
+ This is the most low-level namespace creation function. The storage isn't
+ initialized for the namespace.
+
+ user_all_settings normally points to user->set. If you want to override
+ settings for the created namespace, you can duplicate the user's settings
+ and provide a pointer to it here. Note that the pointer must contain
+ ALL the settings, including the dynamic driver-specific settings, so it
+ needs to created via settings-parser API. */
+int mail_namespace_alloc(struct mail_user *user,
+ void *user_all_settings,
+ struct mail_namespace_settings *ns_set,
+ struct mail_namespace_settings *unexpanded_set,
+ struct mail_namespace **ns_r,
+ const char **error_r);
+
+/* Add and initialize namespaces to user based on namespace settings. */
+int mail_namespaces_init(struct mail_user *user, const char **error_r);
+/* Add and initialize INBOX namespace to user based on the given location. */
+int mail_namespaces_init_location(struct mail_user *user, const char *location,
+ const char **error_r) ATTR_NULL(2);
+/* Add an empty namespace to user. */
+struct mail_namespace *mail_namespaces_init_empty(struct mail_user *user);
+/* Deinitialize all namespaces. mail_user_deinit() calls this automatically
+ for user's namespaces. */
+void mail_namespaces_deinit(struct mail_namespace **namespaces);
+
+/* Allocate a new namespace and initialize it. This is called automatically by
+ mail_namespaces_init(). */
+int mail_namespaces_init_add(struct mail_user *user,
+ struct mail_namespace_settings *ns_set,
+ struct mail_namespace_settings *unexpanded_ns_set,
+ struct mail_namespace **ns_p, const char **error_r);
+int mail_namespaces_init_finish(struct mail_namespace *namespaces,
+ const char **error_r);
+
+void mail_namespace_ref(struct mail_namespace *ns);
+void mail_namespace_unref(struct mail_namespace **ns);
+
+/* Set storage callback functions to use in all namespaces. */
+void mail_namespaces_set_storage_callbacks(struct mail_namespace *namespaces,
+ struct mail_storage_callbacks *callbacks,
+ void *context);
+
+/* Add a new storage to namespace. */
+void mail_namespace_add_storage(struct mail_namespace *ns,
+ struct mail_storage *storage);
+/* Destroy a single namespace and remove it from user's namespaces list. */
+void mail_namespace_destroy(struct mail_namespace *ns);
+
+/* Returns the default storage to use for newly created mailboxes. */
+struct mail_storage *
+mail_namespace_get_default_storage(struct mail_namespace *ns);
+
+/* Return namespace's hierarchy separator. */
+char mail_namespace_get_sep(struct mail_namespace *ns);
+/* Returns the hierarchy separator for mailboxes that are listed at root. */
+char mail_namespaces_get_root_sep(struct mail_namespace *namespaces)
+ ATTR_PURE;
+
+/* Returns namespace based on the mailbox name's prefix. Note that there is
+ always a prefix="" namespace, so for this function NULL is never returned. */
+struct mail_namespace *
+mail_namespace_find(struct mail_namespace *namespaces, const char *mailbox);
+/* Same as mail_namespace_find(), but if the namespace has alias_for set,
+ return that namespace instead and change mailbox name to be a valid
+ inside it. */
+struct mail_namespace *
+mail_namespace_find_unalias(struct mail_namespace *namespaces,
+ const char **mailbox);
+
+/* Like mail_namespace_find(), but ignore hidden namespaces. */
+struct mail_namespace *
+mail_namespace_find_visible(struct mail_namespace *namespaces,
+ const char *mailbox);
+/* Like mail_namespace_find(), but find only from namespaces with
+ subscriptions=yes. */
+struct mail_namespace *
+mail_namespace_find_subscribable(struct mail_namespace *namespaces,
+ const char *mailbox);
+/* Like mail_namespace_find(), but find only from namespaces with
+ subscriptions=no. */
+struct mail_namespace *
+mail_namespace_find_unsubscribable(struct mail_namespace *namespaces,
+ const char *mailbox);
+/* Returns the INBOX namespace. It always exists, so NULL is never returned. */
+struct mail_namespace *
+mail_namespace_find_inbox(struct mail_namespace *namespaces);
+/* Find a namespace with given prefix. */
+struct mail_namespace *
+mail_namespace_find_prefix(struct mail_namespace *namespaces,
+ const char *prefix);
+/* Like _find_prefix(), but ignore trailing separator */
+struct mail_namespace *
+mail_namespace_find_prefix_nosep(struct mail_namespace *namespaces,
+ const char *prefix);
+
+/* Called internally by mailbox_list_create(). */
+void mail_namespace_finish_list_init(struct mail_namespace *ns,
+ struct mailbox_list *list);
+
+/* Returns TRUE if this is the root of a type=shared namespace that is actually
+ used for accessing shared users' mailboxes (as opposed to marking a
+ type=public namespace "wrong"). */
+bool mail_namespace_is_shared_user_root(struct mail_namespace *ns);
+
+/* Returns TRUE if namespace includes INBOX that should be \Noinferiors.
+ This happens when the namespace has a prefix, which is not empty and not
+ "INBOX". This happens, because if storage_name=INBOX/foo it would be
+ converted to vname=prefix/INBOX/foo. */
+static inline bool
+mail_namespace_is_inbox_noinferiors(struct mail_namespace *ns)
+{
+ return (ns->flags & NAMESPACE_FLAG_INBOX_USER) != 0 &&
+ ns->prefix_len > 0 &&
+ strncmp(ns->prefix, "INBOX", ns->prefix_len-1) != 0;
+}
+
+/* Returns TRUE if namespace prefix is INBOX. */
+static inline bool
+mail_namespace_prefix_is_inbox(struct mail_namespace *ns)
+{
+ return (ns->flags & NAMESPACE_FLAG_INBOX_USER) != 0 &&
+ (ns->prefix_len == 6) &&
+ (strncasecmp(ns->prefix, "INBOX", 5) == 0) &&
+ (ns->prefix[5] == mail_namespace_get_sep(ns));
+}
+
+#endif
diff --git a/src/lib-storage/mail-search-args-cmdline.c b/src/lib-storage/mail-search-args-cmdline.c
new file mode 100644
index 0000000..5b1c47f
--- /dev/null
+++ b/src/lib-storage/mail-search-args-cmdline.c
@@ -0,0 +1,106 @@
+/* Copyright (c) 2015-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "str.h"
+#include "imap-quote.h"
+#include "mail-search.h"
+
+static void
+mail_search_arg_to_cmdline(string_t *dest, const struct mail_search_arg *arg);
+
+static void
+mail_search_subargs_to_cmdline(string_t *dest, const struct mail_search_arg *args,
+ const char *middle)
+{
+ const struct mail_search_arg *arg;
+
+ str_append(dest, "( ");
+ for (arg = args; arg != NULL; arg = arg->next) {
+ mail_search_arg_to_cmdline(dest, arg);
+ if (arg->next != NULL)
+ str_append(dest, middle);
+ }
+ str_append(dest, " )");
+}
+
+static void
+mail_search_arg_to_cmdline(string_t *dest, const struct mail_search_arg *arg)
+{
+ struct mail_search_arg new_arg;
+ const char *error;
+
+ if (arg->match_not)
+ str_append(dest, "NOT ");
+ switch (arg->type) {
+ case SEARCH_OR:
+ mail_search_subargs_to_cmdline(dest, arg->value.subargs, " OR ");
+ return;
+ case SEARCH_SUB:
+ mail_search_subargs_to_cmdline(dest, arg->value.subargs, " ");
+ return;
+ case SEARCH_FLAGS:
+ case SEARCH_KEYWORDS: {
+ size_t pos = str_len(dest);
+
+ new_arg = *arg;
+ new_arg.match_not = FALSE;
+ if (!mail_search_arg_to_imap(dest, &new_arg, &error))
+ i_unreached();
+ if (str_c(dest)[pos] == '(') {
+ str_insert(dest, pos+1, " ");
+ str_insert(dest, str_len(dest)-1, " ");
+ }
+ return;
+ }
+ case SEARCH_INTHREAD:
+ str_append(dest, "INTHREAD ");
+ imap_append_astring(dest, mail_thread_type_to_str(arg->value.thread_type));
+ str_append_c(dest, ' ');
+ mail_search_subargs_to_cmdline(dest, arg->value.subargs, " ");
+ break;
+ case SEARCH_MAILBOX:
+ case SEARCH_MAILBOX_GLOB:
+ str_append(dest, "MAILBOX ");
+ imap_append_astring(dest, arg->value.str);
+ return;
+ case SEARCH_MAILBOX_GUID:
+ str_append(dest, "MAILBOX-GUID ");
+ imap_append_astring(dest, arg->value.str);
+ return;
+ case SEARCH_ALL:
+ case SEARCH_SEQSET:
+ case SEARCH_UIDSET:
+ case SEARCH_BEFORE:
+ case SEARCH_ON:
+ case SEARCH_SINCE:
+ case SEARCH_SMALLER:
+ case SEARCH_LARGER:
+ case SEARCH_HEADER:
+ case SEARCH_HEADER_ADDRESS:
+ case SEARCH_HEADER_COMPRESS_LWSP:
+ case SEARCH_BODY:
+ case SEARCH_TEXT:
+ case SEARCH_MODSEQ:
+ case SEARCH_SAVEDATESUPPORTED:
+ case SEARCH_GUID:
+ case SEARCH_REAL_UID:
+ case SEARCH_MIMEPART:
+ break;
+ }
+ new_arg = *arg;
+ new_arg.match_not = FALSE;
+ if (!mail_search_arg_to_imap(dest, &new_arg, &error))
+ i_panic("mail_search_args_to_cmdline(): Missing handler: %s", error);
+}
+
+void mail_search_args_to_cmdline(string_t *dest,
+ const struct mail_search_arg *args)
+{
+ const struct mail_search_arg *arg;
+
+ for (arg = args; arg != NULL; arg = arg->next) {
+ mail_search_arg_to_cmdline(dest, arg);
+ if (arg->next != NULL)
+ str_append_c(dest, ' ');
+ }
+}
diff --git a/src/lib-storage/mail-search-args-imap.c b/src/lib-storage/mail-search-args-imap.c
new file mode 100644
index 0000000..e89e119
--- /dev/null
+++ b/src/lib-storage/mail-search-args-imap.c
@@ -0,0 +1,326 @@
+/* Copyright (c) 2015-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "ioloop.h"
+#include "array.h"
+#include "str.h"
+#include "utc-offset.h"
+#include "mail-index.h"
+#include "imap-date.h"
+#include "imap-util.h"
+#include "imap-quote.h"
+#include "mail-search.h"
+#include "mail-search-mime.h"
+
+#include <time.h>
+
+static bool
+mail_search_subargs_to_imap(string_t *dest, const struct mail_search_arg *args,
+ const char *prefix, const char **error_r)
+{
+ const struct mail_search_arg *arg;
+
+ if (prefix[0] == '\0')
+ str_append_c(dest, '(');
+ for (arg = args; arg != NULL; arg = arg->next) {
+ if (arg->next != NULL)
+ str_append(dest, prefix);
+ if (!mail_search_arg_to_imap(dest, arg, error_r))
+ return FALSE;
+ if (arg->next != NULL)
+ str_append_c(dest, ' ');
+ }
+ if (prefix[0] == '\0')
+ str_append_c(dest, ')');
+ return TRUE;
+}
+
+static bool
+mail_search_arg_to_imap_date(string_t *dest, const struct mail_search_arg *arg)
+{
+ time_t timestamp = arg->value.time;
+ const char *str;
+
+ if ((arg->value.search_flags &
+ MAIL_SEARCH_ARG_FLAG_UTC_TIMES) == 0) {
+ struct tm *tm = localtime(&timestamp);
+ int tz_offset = utc_offset(tm, timestamp);
+ timestamp -= tz_offset * 60;
+ }
+ if (!imap_to_date(timestamp, &str))
+ return FALSE;
+ str_printfa(dest, " \"%s\"", str);
+ return TRUE;
+}
+
+static void
+mail_search_arg_to_imap_flags(string_t *dest, enum mail_flags flags)
+{
+ static const char *flag_names[] = {
+ "ANSWERED", "FLAGGED", "DELETED", "SEEN", "DRAFT", "RECENT"
+ };
+
+ i_assert(flags != 0);
+
+ if (!bits_is_power_of_two(flags))
+ str_append_c(dest, '(');
+ for (unsigned int i = 0; i < N_ELEMENTS(flag_names); i++) {
+ if ((flags & (1 << i)) != 0) {
+ str_append(dest, flag_names[i]);
+ str_append_c(dest, ' ');
+ }
+ }
+
+ str_truncate(dest, str_len(dest)-1);
+ if (!bits_is_power_of_two(flags))
+ str_append_c(dest, ')');
+}
+
+bool mail_search_arg_to_imap(string_t *dest, const struct mail_search_arg *arg,
+ const char **error_r)
+{
+ unsigned int start_pos;
+
+ if (arg->match_not)
+ str_append(dest, "NOT ");
+ start_pos = str_len(dest);
+ switch (arg->type) {
+ case SEARCH_OR:
+ if (!mail_search_subargs_to_imap(dest, arg->value.subargs,
+ "OR ", error_r))
+ return FALSE;
+ break;
+ case SEARCH_SUB:
+ if (!mail_search_subargs_to_imap(dest, arg->value.subargs,
+ "", error_r))
+ return FALSE;
+ break;
+ case SEARCH_ALL:
+ str_append(dest, "ALL");
+ break;
+ case SEARCH_SEQSET:
+ imap_write_seq_range(dest, &arg->value.seqset);
+ break;
+ case SEARCH_UIDSET:
+ str_append(dest, "UID ");
+ imap_write_seq_range(dest, &arg->value.seqset);
+ break;
+ case SEARCH_FLAGS:
+ mail_search_arg_to_imap_flags(dest, arg->value.flags);
+ break;
+ case SEARCH_KEYWORDS: {
+ const struct mail_keywords *kw = arg->initialized.keywords;
+ const ARRAY_TYPE(keywords) *names_arr;
+ const char *name;
+ unsigned int i;
+
+ if (kw == NULL || kw->count == 0) {
+ /* uninitialized / invalid keyword */
+ str_printfa(dest, "KEYWORD %s", arg->value.str);
+ break;
+ }
+
+ names_arr = mail_index_get_keywords(kw->index);
+
+ if (kw->count > 1)
+ str_append_c(dest, '(');
+ for (i = 0; i < kw->count; i++) {
+ name = array_idx_elem(names_arr, kw->idx[i]);
+ if (i > 0)
+ str_append_c(dest, ' ');
+ str_printfa(dest, "KEYWORD %s", name);
+ }
+ if (kw->count > 1)
+ str_append_c(dest, ')');
+ break;
+ }
+
+ case SEARCH_BEFORE:
+ switch (arg->value.date_type) {
+ case MAIL_SEARCH_DATE_TYPE_SENT:
+ str_append(dest, "SENTBEFORE");
+ break;
+ case MAIL_SEARCH_DATE_TYPE_RECEIVED:
+ str_append(dest, "BEFORE");
+ break;
+ case MAIL_SEARCH_DATE_TYPE_SAVED:
+ str_append(dest, "SAVEDBEFORE");
+ break;
+ }
+ if (mail_search_arg_to_imap_date(dest, arg))
+ ;
+ else if (arg->value.date_type != MAIL_SEARCH_DATE_TYPE_RECEIVED ||
+ arg->value.time > ioloop_time) {
+ *error_r = t_strdup_printf(
+ "SEARCH_BEFORE can't be written as IMAP for timestamp %ld (type=%d, utc_times=%d)",
+ (long)arg->value.time, arg->value.date_type,
+ (arg->value.search_flags & MAIL_SEARCH_ARG_FLAG_UTC_TIMES) != 0);
+ return FALSE;
+ } else {
+ str_truncate(dest, start_pos);
+ str_printfa(dest, "OLDER %u",
+ (unsigned int)(ioloop_time - arg->value.time + 1));
+ }
+ break;
+ case SEARCH_ON:
+ switch (arg->value.date_type) {
+ case MAIL_SEARCH_DATE_TYPE_SENT:
+ str_append(dest, "SENTON");
+ break;
+ case MAIL_SEARCH_DATE_TYPE_RECEIVED:
+ str_append(dest, "ON");
+ break;
+ case MAIL_SEARCH_DATE_TYPE_SAVED:
+ str_append(dest, "SAVEDON");
+ break;
+ }
+ if (!mail_search_arg_to_imap_date(dest, arg)) {
+ *error_r = t_strdup_printf(
+ "SEARCH_ON can't be written as IMAP for timestamp %ld (type=%d, utc_times=%d)",
+ (long)arg->value.time, arg->value.date_type,
+ (arg->value.search_flags & MAIL_SEARCH_ARG_FLAG_UTC_TIMES) != 0);
+ return FALSE;
+ }
+ break;
+ case SEARCH_SINCE:
+ switch (arg->value.date_type) {
+ case MAIL_SEARCH_DATE_TYPE_SENT:
+ str_append(dest, "SENTSINCE");
+ break;
+ case MAIL_SEARCH_DATE_TYPE_RECEIVED:
+ str_append(dest, "SINCE");
+ break;
+ case MAIL_SEARCH_DATE_TYPE_SAVED:
+ str_append(dest, "SAVEDSINCE");
+ break;
+ }
+ if (mail_search_arg_to_imap_date(dest, arg))
+ ;
+ else if (arg->value.date_type != MAIL_SEARCH_DATE_TYPE_RECEIVED ||
+ arg->value.time >= ioloop_time) {
+ *error_r = t_strdup_printf(
+ "SEARCH_SINCE can't be written as IMAP for timestamp %ld (type=%d, utc_times=%d)",
+ (long)arg->value.time, arg->value.date_type,
+ (arg->value.search_flags & MAIL_SEARCH_ARG_FLAG_UTC_TIMES) != 0);
+ return FALSE;
+ } else {
+ str_truncate(dest, start_pos);
+ str_printfa(dest, "YOUNGER %u",
+ (unsigned int)(ioloop_time - arg->value.time));
+ }
+ break;
+ case SEARCH_SMALLER:
+ str_printfa(dest, "SMALLER %"PRIuUOFF_T, arg->value.size);
+ break;
+ case SEARCH_LARGER:
+ str_printfa(dest, "LARGER %"PRIuUOFF_T, arg->value.size);
+ break;
+ case SEARCH_HEADER:
+ case SEARCH_HEADER_ADDRESS:
+ case SEARCH_HEADER_COMPRESS_LWSP:
+ if (strcasecmp(arg->hdr_field_name, "From") == 0 ||
+ strcasecmp(arg->hdr_field_name, "To") == 0 ||
+ strcasecmp(arg->hdr_field_name, "Cc") == 0 ||
+ strcasecmp(arg->hdr_field_name, "Bcc") == 0 ||
+ strcasecmp(arg->hdr_field_name, "Subject") == 0)
+ str_append(dest, t_str_ucase(arg->hdr_field_name));
+ else {
+ str_append(dest, "HEADER ");
+ imap_append_astring(dest, arg->hdr_field_name);
+ }
+ str_append_c(dest, ' ');
+ imap_append_astring(dest, arg->value.str);
+ break;
+
+ case SEARCH_BODY:
+ str_append(dest, "BODY ");
+ imap_append_astring(dest, arg->value.str);
+ break;
+ case SEARCH_TEXT:
+ str_append(dest, "TEXT ");
+ imap_append_astring(dest, arg->value.str);
+ break;
+
+ /* extensions */
+ case SEARCH_MODSEQ: {
+ bool extended_output = FALSE;
+
+ str_append(dest, "MODSEQ ");
+ if (arg->value.str != NULL) {
+ str_printfa(dest, "/flags/%s", arg->value.str);
+ extended_output = TRUE;
+ } else if (arg->value.flags != 0) {
+ str_append(dest, "/flags/");
+ imap_write_flags(dest, arg->value.flags, NULL);
+ extended_output = TRUE;
+ }
+ if (extended_output) {
+ str_append_c(dest, ' ');
+ switch (arg->value.modseq->type) {
+ case MAIL_SEARCH_MODSEQ_TYPE_ANY:
+ str_append(dest, "all");
+ break;
+ case MAIL_SEARCH_MODSEQ_TYPE_PRIVATE:
+ str_append(dest, "priv");
+ break;
+ case MAIL_SEARCH_MODSEQ_TYPE_SHARED:
+ str_append(dest, "shared");
+ break;
+ }
+ str_append_c(dest, ' ');
+ }
+ str_printfa(dest, "%"PRIu64, arg->value.modseq->modseq);
+ break;
+ }
+ case SEARCH_SAVEDATESUPPORTED:
+ str_append(dest, "SAVEDATESUPPORTED");
+ break;
+ case SEARCH_INTHREAD:
+ str_append(dest, "INTHREAD ");
+ imap_append_astring(dest, mail_thread_type_to_str(arg->value.thread_type));
+ str_append_c(dest, ' ');
+ if (!mail_search_subargs_to_imap(dest, arg->value.subargs,
+ "", error_r))
+ return FALSE;
+ break;
+ case SEARCH_GUID:
+ str_append(dest, "X-GUID ");
+ imap_append_astring(dest, arg->value.str);
+ break;
+ case SEARCH_MAILBOX:
+ *error_r = "SEARCH_MAILBOX can't be written as IMAP";
+ return FALSE;
+ case SEARCH_MAILBOX_GUID:
+ *error_r = "SEARCH_MAILBOX_GUID can't be written as IMAP";
+ return FALSE;
+ case SEARCH_MAILBOX_GLOB:
+ str_append(dest, "X-MAILBOX ");
+ imap_append_astring(dest, arg->value.str);
+ break;
+ case SEARCH_REAL_UID:
+ str_append(dest, "X-REAL-UID ");
+ imap_write_seq_range(dest, &arg->value.seqset);
+ break;
+ case SEARCH_MIMEPART:
+ str_append(dest, "MIMEPART ");
+ if (!mail_search_mime_part_to_imap(dest,
+ arg->value.mime_part, error_r))
+ return FALSE;
+ break;
+ }
+ return TRUE;
+}
+
+bool mail_search_args_to_imap(string_t *dest, const struct mail_search_arg *args,
+ const char **error_r)
+{
+ const struct mail_search_arg *arg;
+
+ for (arg = args; arg != NULL; arg = arg->next) {
+ if (!mail_search_arg_to_imap(dest, arg, error_r))
+ return FALSE;
+ if (arg->next != NULL)
+ str_append_c(dest, ' ');
+ }
+ return TRUE;
+}
diff --git a/src/lib-storage/mail-search-args-simplify.c b/src/lib-storage/mail-search-args-simplify.c
new file mode 100644
index 0000000..9c86811
--- /dev/null
+++ b/src/lib-storage/mail-search-args-simplify.c
@@ -0,0 +1,798 @@
+/* Copyright (c) 2002-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "hash.h"
+#include "mail-search.h"
+
+struct mail_search_simplify_prev_arg {
+ struct {
+ enum mail_search_arg_type type;
+ enum mail_search_arg_flag search_flags;
+ enum mail_search_date_type date_type;
+ enum mail_flags mail_flags;
+ bool match_not;
+ bool fuzzy;
+ } bin_mask;
+ const char *hdr_field_name_mask;
+ const char *str_mask;
+
+ struct mail_search_arg *prev_arg;
+};
+
+struct mail_search_simplify_ctx {
+ pool_t pool;
+ /* arg mask => prev_arg */
+ HASH_TABLE(struct mail_search_simplify_prev_arg *,
+ struct mail_search_simplify_prev_arg *) prev_args;
+ bool parent_and;
+ bool removals;
+ bool initialized;
+};
+
+static int
+mail_search_simplify_prev_arg_cmp(const struct mail_search_simplify_prev_arg *arg1,
+ const struct mail_search_simplify_prev_arg *arg2)
+{
+ int ret;
+
+ ret = memcmp(&arg1->bin_mask, &arg2->bin_mask, sizeof(arg1->bin_mask));
+ if (ret == 0)
+ ret = null_strcmp(arg1->hdr_field_name_mask, arg2->hdr_field_name_mask);
+ if (ret == 0)
+ ret = null_strcmp(arg1->str_mask, arg2->str_mask);
+ return ret;
+}
+
+static unsigned int
+mail_search_simplify_prev_arg_hash(const struct mail_search_simplify_prev_arg *arg)
+{
+ unsigned int hash;
+
+ hash = mem_hash(&arg->bin_mask, sizeof(arg->bin_mask));
+ if (arg->hdr_field_name_mask != NULL)
+ hash ^= str_hash(arg->hdr_field_name_mask);
+ if (arg->str_mask != NULL)
+ hash ^= str_hash(arg->str_mask);
+ return hash;
+}
+
+static void mail_search_arg_get_base_mask(const struct mail_search_arg *arg,
+ struct mail_search_simplify_prev_arg *mask_r)
+{
+ i_zero(mask_r);
+ mask_r->bin_mask.type = arg->type;
+ mask_r->bin_mask.fuzzy = arg->fuzzy;
+ mask_r->bin_mask.search_flags = arg->value.search_flags;
+}
+
+static struct mail_search_arg **
+mail_search_args_simplify_get_prev_argp(struct mail_search_simplify_ctx *ctx,
+ const struct mail_search_simplify_prev_arg *mask)
+{
+ struct mail_search_simplify_prev_arg *prev_arg;
+
+ prev_arg = hash_table_lookup(ctx->prev_args, mask);
+ if (prev_arg == NULL) {
+ prev_arg = p_new(ctx->pool, struct mail_search_simplify_prev_arg, 1);
+ prev_arg->bin_mask = mask->bin_mask;
+ prev_arg->hdr_field_name_mask =
+ p_strdup(ctx->pool, mask->hdr_field_name_mask);
+ prev_arg->str_mask =
+ p_strdup(ctx->pool, mask->str_mask);
+ hash_table_insert(ctx->prev_args, prev_arg, prev_arg);
+ }
+ return &prev_arg->prev_arg;
+}
+
+static bool
+mail_search_args_merge_mask(struct mail_search_simplify_ctx *ctx,
+ struct mail_search_arg *args,
+ const struct mail_search_simplify_prev_arg *mask)
+{
+ struct mail_search_arg **prev_argp;
+
+ prev_argp = mail_search_args_simplify_get_prev_argp(ctx, mask);
+ if (*prev_argp == NULL) {
+ *prev_argp = args;
+ return FALSE;
+ }
+
+ if (ctx->initialized)
+ mail_search_arg_one_deinit(args);
+
+ if ((*prev_argp)->match_not != args->match_not) {
+ /* a && !a = 0 */
+ if (ctx->initialized)
+ mail_search_arg_one_deinit(*prev_argp);
+ (*prev_argp)->type = SEARCH_ALL;
+ (*prev_argp)->match_not = ctx->parent_and;
+ }
+ /* duplicate keyword. */
+ return TRUE;
+}
+
+static bool mail_search_args_merge_flags(struct mail_search_simplify_ctx *ctx,
+ struct mail_search_arg *args)
+{
+ struct mail_search_simplify_prev_arg mask;
+
+ mail_search_arg_get_base_mask(args, &mask);
+ mask.bin_mask.mail_flags = args->value.flags;
+ return mail_search_args_merge_mask(ctx, args, &mask);
+}
+
+static bool
+mail_search_args_merge_keywords(struct mail_search_simplify_ctx *ctx,
+ struct mail_search_arg *args)
+{
+ struct mail_search_simplify_prev_arg mask;
+
+ mail_search_arg_get_base_mask(args, &mask);
+ mask.str_mask = args->value.str;
+ return mail_search_args_merge_mask(ctx, args, &mask);
+}
+
+static void mail_search_args_simplify_set(struct mail_search_arg *args)
+{
+ const struct seq_range *seqset;
+ unsigned int count;
+
+ if (args->match_not) {
+ /* invert the set to drop the NOT. Note that (uint32_t)-1
+ matches the last existing mail, which we don't know at this
+ point. lib-imap/imap-seqset.c has similar code that
+ disallows using (uint32_t)-1 as a real UID. */
+ if (seq_range_exists(&args->value.seqset, (uint32_t)-1))
+ return;
+ args->match_not = FALSE;
+ seq_range_array_invert(&args->value.seqset, 1, (uint32_t)-2);
+ }
+ seqset = array_get(&args->value.seqset, &count);
+ if (count == 1 && seqset->seq1 == 1 && seqset->seq2 >= (uint32_t)-2) {
+ /* 1:* is the same as ALL. */
+ args->type = SEARCH_ALL;
+ } else if (count == 0) {
+ /* empty set is the same as NOT ALL. this is mainly coming
+ from mail_search_args_merge_set() intersection. */
+ args->type = SEARCH_ALL;
+ args->match_not = TRUE;
+ }
+}
+
+static bool mail_search_args_merge_set(struct mail_search_simplify_ctx *ctx,
+ struct mail_search_arg *args)
+{
+ struct mail_search_simplify_prev_arg mask;
+ struct mail_search_arg **prev_argp;
+
+ if (args->match_not) {
+ /* "*" used - can't simplify it */
+ return FALSE;
+ }
+
+ mail_search_arg_get_base_mask(args, &mask);
+ mask.bin_mask.match_not = args->match_not;
+ prev_argp = mail_search_args_simplify_get_prev_argp(ctx, &mask);
+
+ if (*prev_argp == NULL) {
+ *prev_argp = args;
+ return FALSE;
+ } else if (ctx->parent_and) {
+ seq_range_array_intersect(&(*prev_argp)->value.seqset,
+ &args->value.seqset);
+ return TRUE;
+ } else {
+ seq_range_array_merge(&(*prev_argp)->value.seqset,
+ &args->value.seqset);
+ return TRUE;
+ }
+}
+
+static bool mail_search_args_merge_time(struct mail_search_simplify_ctx *ctx,
+ struct mail_search_arg *args)
+{
+ struct mail_search_simplify_prev_arg mask;
+ struct mail_search_arg **prev_argp, *prev_arg;
+
+ mail_search_arg_get_base_mask(args, &mask);
+ mask.bin_mask.match_not = args->match_not;
+ mask.bin_mask.date_type = args->value.date_type;
+ prev_argp = mail_search_args_simplify_get_prev_argp(ctx, &mask);
+
+ if (*prev_argp == NULL) {
+ *prev_argp = args;
+ return FALSE;
+ }
+
+ prev_arg = *prev_argp;
+ switch (args->type) {
+ case SEARCH_BEFORE:
+ if (ctx->parent_and) {
+ if (prev_arg->value.time < args->value.time) {
+ /* prev_arg < 5 AND arg < 10 */
+ } else {
+ /* prev_arg < 10 AND arg < 5 */
+ prev_arg->value.time = args->value.time;
+ }
+ } else {
+ if (prev_arg->value.time < args->value.time) {
+ /* prev_arg < 5 OR arg < 10 */
+ prev_arg->value.time = args->value.time;
+ } else {
+ /* prev_arg < 10 OR arg < 5 */
+ }
+ }
+ return TRUE;
+ case SEARCH_ON:
+ if (prev_arg->value.time == args->value.time)
+ return TRUE;
+ return FALSE;
+ case SEARCH_SINCE:
+ if (ctx->parent_and) {
+ if (prev_arg->value.time < args->value.time) {
+ /* prev_arg >= 5 AND arg >= 10 */
+ prev_arg->value.time = args->value.time;
+ } else {
+ /* prev_arg >= 10 AND arg >= 5 */
+ }
+ } else {
+ if (prev_arg->value.time < args->value.time) {
+ /* prev_arg >= 5 OR arg >= 10 */
+ } else {
+ /* prev_arg >= 10 OR arg >= 5 */
+ prev_arg->value.time = args->value.time;
+ }
+ }
+ return TRUE;
+ default:
+ break;
+ }
+ return FALSE;
+}
+
+static bool mail_search_args_merge_size(struct mail_search_simplify_ctx *ctx,
+ struct mail_search_arg *args)
+{
+ struct mail_search_simplify_prev_arg mask;
+ struct mail_search_arg **prev_argp, *prev_arg;
+
+ mail_search_arg_get_base_mask(args, &mask);
+ mask.bin_mask.match_not = args->match_not;
+ prev_argp = mail_search_args_simplify_get_prev_argp(ctx, &mask);
+
+ if (*prev_argp == NULL) {
+ *prev_argp = args;
+ return FALSE;
+ }
+
+ prev_arg = *prev_argp;
+ switch (args->type) {
+ case SEARCH_SMALLER:
+ if (ctx->parent_and) {
+ if (prev_arg->value.size < args->value.size) {
+ /* prev_arg < 5 AND arg < 10 */
+ } else {
+ /* prev_arg < 10 AND arg < 5 */
+ prev_arg->value.size = args->value.size;
+ }
+ } else {
+ if (prev_arg->value.size < args->value.size) {
+ /* prev_arg < 5 OR arg < 10 */
+ prev_arg->value.size = args->value.size;
+ } else {
+ /* prev_arg < 10 OR arg < 5 */
+ }
+ }
+ return TRUE;
+ case SEARCH_LARGER:
+ if (ctx->parent_and) {
+ if (prev_arg->value.size < args->value.size) {
+ /* prev_arg >= 5 AND arg >= 10 */
+ prev_arg->value.size = args->value.size;
+ } else {
+ /* prev_arg >= 10 AND arg >= 5 */
+ }
+ } else {
+ if (prev_arg->value.size < args->value.size) {
+ /* prev_arg >= 5 OR arg >= 10 */
+ } else {
+ /* prev_arg >= 10 OR arg >= 5 */
+ prev_arg->value.size = args->value.size;
+ }
+ }
+ return TRUE;
+ default:
+ break;
+ }
+ return FALSE;
+}
+
+static bool mail_search_args_merge_text(struct mail_search_simplify_ctx *ctx,
+ struct mail_search_arg *args)
+{
+ struct mail_search_simplify_prev_arg mask;
+
+ mail_search_arg_get_base_mask(args, &mask);
+ mask.hdr_field_name_mask = args->hdr_field_name;
+ mask.str_mask = args->value.str;
+ return mail_search_args_merge_mask(ctx, args, &mask);
+}
+
+static bool
+mail_search_args_have_equal(const struct mail_search_arg *args,
+ const struct mail_search_arg *wanted_arg)
+{
+ const struct mail_search_arg *arg;
+
+ for (arg = args; arg != NULL; arg = arg->next) {
+ if (mail_search_arg_one_equals(arg, wanted_arg))
+ return TRUE;
+ }
+ return FALSE;
+}
+
+static bool
+mail_search_args_remove_equal(struct mail_search_args *all_args,
+ struct mail_search_arg **argsp,
+ const struct mail_search_arg *wanted_arg,
+ bool check_subs)
+{
+ struct mail_search_arg **argp;
+ bool found = FALSE;
+
+ for (argp = argsp; (*argp) != NULL; ) {
+ if (mail_search_arg_one_equals(*argp, wanted_arg)) {
+ if (all_args->init_refcount > 0)
+ mail_search_arg_one_deinit(*argp);
+ *argp = (*argp)->next;
+ found = TRUE;
+ } else if (check_subs) {
+ i_assert((*argp)->type == SEARCH_SUB ||
+ (*argp)->type == SEARCH_OR);
+ if (!mail_search_args_remove_equal(all_args, &(*argp)->value.subargs, wanted_arg, FALSE)) {
+ /* we already verified that this should have
+ existed. */
+ i_unreached();
+ }
+ if ((*argp)->value.subargs == NULL)
+ *argp = (*argp)->next;
+ else
+ argp = &(*argp)->next;
+ found = TRUE;
+ } else {
+ argp = &(*argp)->next;
+ }
+ }
+ return found;
+}
+
+static bool
+mail_search_args_have_all_equal(struct mail_search_arg *parent_arg,
+ const struct mail_search_arg *wanted_args)
+{
+ const struct mail_search_arg *arg;
+
+ i_assert(parent_arg->type == SEARCH_SUB ||
+ parent_arg->type == SEARCH_OR);
+
+ for (arg = wanted_args; arg != NULL; arg = arg->next) {
+ if (!mail_search_args_have_equal(parent_arg->value.subargs, arg))
+ return FALSE;
+ }
+ return TRUE;
+}
+
+static unsigned int
+mail_search_args_count(const struct mail_search_arg *args)
+{
+ unsigned int count;
+
+ for (count = 0; args != NULL; count++)
+ args = args->next;
+ return count;
+}
+
+static bool
+mail_search_args_simplify_drop_redundant_args(struct mail_search_args *all_args,
+ struct mail_search_arg **argsp,
+ bool and_arg)
+{
+ struct mail_search_arg *arg, **argp, one_arg, *lowest_arg = NULL;
+ enum mail_search_arg_type child_subargs_type;
+ unsigned int count, lowest_count = UINT_MAX;
+ bool ret = FALSE;
+
+ if (*argsp == NULL)
+ return FALSE;
+
+ child_subargs_type = and_arg ? SEARCH_OR : SEARCH_SUB;
+
+ /* find the arg which has the lowest number of child args */
+ for (arg = *argsp; arg != NULL; arg = arg->next) {
+ if (arg->type != child_subargs_type) {
+ one_arg = *arg;
+ one_arg.next = NULL;
+ lowest_arg = &one_arg;
+ break;
+ }
+ count = mail_search_args_count(arg->value.subargs);
+ if (count < lowest_count) {
+ lowest_arg = arg->value.subargs;
+ lowest_count = count;
+ }
+ }
+ i_assert(lowest_arg != NULL);
+
+ /* if there are any args that include lowest_arg, drop the arg since
+ it's redundant. (non-SUB duplicates are dropped elsewhere.) */
+ for (argp = argsp; *argp != NULL; ) {
+ if (*argp != lowest_arg && (*argp)->type == child_subargs_type &&
+ (*argp)->value.subargs != lowest_arg &&
+ mail_search_args_have_all_equal(*argp, lowest_arg)) {
+ if (all_args->init_refcount > 0)
+ mail_search_arg_one_deinit(*argp);
+ *argp = (*argp)->next;
+ ret = TRUE;
+ } else {
+ argp = &(*argp)->next;
+ }
+ }
+ return ret;
+}
+
+static bool
+mail_search_args_simplify_extract_common(struct mail_search_args *all_args,
+ struct mail_search_arg **argsp,
+ pool_t pool, bool and_arg)
+{
+ /* Simple SUB example:
+ (a AND b) OR (a AND c) -> a AND (b OR c)
+
+ More complicated example:
+ (c1 AND c2 AND u1 AND u2) OR (c1 AND c2 AND u3 AND u4) ->
+ c1 AND c2 AND ((u1 AND u2) OR (u3 AND u4))
+
+ Similarly for ORs:
+ (a OR b) AND (a OR c) -> a OR (b AND c)
+
+ (c1 OR c2 OR u1 OR u2) AND (c1 OR c2 OR u3 OR u4) ->
+ c1 OR c2 OR ((u1 OR u2) AND (u3 OR u4))
+
+ */
+ struct mail_search_arg *arg, *sub_arg, *sub_next;
+ struct mail_search_arg *new_arg, *child_arg, *common_args = NULL;
+ enum mail_search_arg_type child_subargs_type;
+
+ if (*argsp == NULL || (*argsp)->next == NULL) {
+ /* single arg, nothing to extract */
+ return FALSE;
+ }
+
+ child_subargs_type = and_arg ? SEARCH_OR : SEARCH_SUB;
+
+ /* find the first arg with child_subargs_type */
+ for (arg = *argsp; arg != NULL; arg = arg->next) {
+ if (arg->type == child_subargs_type)
+ break;
+ }
+ if (arg == NULL)
+ return FALSE;
+
+ for (sub_arg = arg->value.subargs; sub_arg != NULL; sub_arg = sub_next) {
+ sub_next = sub_arg->next;
+
+ /* check if sub_arg is found from all the args */
+ for (arg = *argsp; arg != NULL; arg = arg->next) {
+ if (mail_search_arg_one_equals(arg, sub_arg)) {
+ /* the whole arg matches */
+ } else if (arg->type == child_subargs_type &&
+ mail_search_args_have_equal(arg->value.subargs, sub_arg)) {
+ /* exists as subarg */
+ } else {
+ break;
+ }
+ }
+ if (arg != NULL)
+ continue;
+
+ /* extract the arg and put it to common_args */
+ mail_search_args_remove_equal(all_args, argsp, sub_arg, TRUE);
+ sub_arg->next = common_args;
+ common_args = sub_arg;
+ }
+ if (common_args == NULL)
+ return FALSE;
+
+ /* replace all the original args with a single new SUB/OR arg */
+ new_arg = p_new(pool, struct mail_search_arg, 1);
+ new_arg->type = child_subargs_type;
+ if (*argsp == NULL) {
+ /* there are only common args */
+ new_arg->value.subargs = common_args;
+ } else {
+ /* replace OR arg with AND(OR(non_common_args), common_args)
+ or
+ replace AND arg with OR(AND(non_common_args), common_args) */
+ child_arg = p_new(pool, struct mail_search_arg, 1);
+ child_arg->type = and_arg ? SEARCH_SUB : SEARCH_OR;
+ child_arg->value.subargs = *argsp;
+ child_arg->next = common_args;
+ new_arg->value.subargs = child_arg;
+ }
+ *argsp = new_arg;
+ return TRUE;
+}
+
+static bool
+mail_search_args_simplify_sub(struct mail_search_args *all_args, pool_t pool,
+ struct mail_search_arg **argsp, bool parent_and)
+{
+ struct mail_search_simplify_ctx ctx;
+ struct mail_search_arg *sub, **all_argsp = argsp;
+ bool merged;
+
+ i_zero(&ctx);
+ ctx.initialized = all_args->init_refcount > 0;
+ ctx.parent_and = parent_and;
+ ctx.pool = pool_alloconly_create("mail search args simplify", 1024);
+ hash_table_create(&ctx.prev_args, ctx.pool, 0,
+ mail_search_simplify_prev_arg_hash,
+ mail_search_simplify_prev_arg_cmp);
+
+ while (*argsp != NULL) {
+ struct mail_search_arg *args = *argsp;
+
+ if (args->match_not && (args->type == SEARCH_SUB ||
+ args->type == SEARCH_OR)) {
+ /* neg(p and q and ..) == neg(p) or neg(q) or ..
+ neg(p or q or ..) == neg(p) and neg(q) and .. */
+ args->type = args->type == SEARCH_SUB ?
+ SEARCH_OR : SEARCH_SUB;
+ args->match_not = FALSE;
+ sub = args->value.subargs;
+ do {
+ sub->match_not = !sub->match_not;
+ sub = sub->next;
+ } while (sub != NULL);
+ }
+
+ if ((args->type == SEARCH_SUB && parent_and) ||
+ (args->type == SEARCH_OR && !parent_and) ||
+ ((args->type == SEARCH_SUB || args->type == SEARCH_OR) &&
+ args->value.subargs->next == NULL)) {
+ /* p and (q and ..) == p and q and ..
+ p or (q or ..) == p or q or ..
+ (p) = p */
+ sub = args->value.subargs;
+ for (; sub->next != NULL; sub = sub->next) ;
+ sub->next = args->next;
+ *args = *args->value.subargs;
+ ctx.removals = TRUE;
+ continue;
+ }
+
+ if (args->type == SEARCH_SUB ||
+ args->type == SEARCH_OR ||
+ args->type == SEARCH_INTHREAD) {
+ i_assert(!args->match_not);
+
+ if (args->type != SEARCH_INTHREAD) {
+ bool and_arg = args->type == SEARCH_SUB;
+
+ if (mail_search_args_simplify_drop_redundant_args(all_args, &args->value.subargs, and_arg))
+ ctx.removals = TRUE;
+ if (mail_search_args_simplify_extract_common(all_args, &args->value.subargs, pool, and_arg))
+ ctx.removals = TRUE;
+ }
+ if (mail_search_args_simplify_sub(all_args, pool, &args->value.subargs,
+ args->type != SEARCH_OR))
+ ctx.removals = TRUE;
+ }
+ if (args->type == SEARCH_SEQSET ||
+ args->type == SEARCH_UIDSET)
+ mail_search_args_simplify_set(args);
+
+ /* try to merge arguments */
+ merged = FALSE;
+ switch (args->type) {
+ case SEARCH_ALL: {
+ if (*all_argsp == args && args->next == NULL) {
+ /* this arg has no siblings - no merging */
+ break;
+ }
+ if ((parent_and && !args->match_not) ||
+ (!parent_and && args->match_not)) {
+ /* .. AND ALL ..
+ .. OR NOT ALL ..
+ This arg is irrelevant -> drop */
+ merged = TRUE;
+ break;
+ }
+ /* .. AND NOT ALL ..
+ .. OR ALL ..
+ The other args are irrelevant -> drop them */
+ *all_argsp = args;
+ args->next = NULL;
+ ctx.removals = TRUE;
+ break;
+ }
+ case SEARCH_FLAGS:
+ merged = mail_search_args_merge_flags(&ctx, args);
+ break;
+ case SEARCH_KEYWORDS:
+ merged = mail_search_args_merge_keywords(&ctx, args);
+ break;
+ case SEARCH_SEQSET:
+ case SEARCH_UIDSET:
+ merged = mail_search_args_merge_set(&ctx, args);
+ break;
+ case SEARCH_BEFORE:
+ case SEARCH_ON:
+ case SEARCH_SINCE:
+ merged = mail_search_args_merge_time(&ctx, args);
+ break;
+ case SEARCH_SMALLER:
+ case SEARCH_LARGER:
+ merged = mail_search_args_merge_size(&ctx, args);
+ break;
+ case SEARCH_BODY:
+ case SEARCH_TEXT:
+ if (args->value.str[0] == '\0') {
+ /* BODY "" and TEXT "" matches everything */
+ args->type = SEARCH_ALL;
+ ctx.removals = TRUE;
+ break;
+ }
+ /* fall through */
+ case SEARCH_HEADER:
+ case SEARCH_HEADER_ADDRESS:
+ case SEARCH_HEADER_COMPRESS_LWSP:
+ merged = mail_search_args_merge_text(&ctx, args);
+ break;
+ default:
+ break;
+ }
+ if (merged) {
+ *argsp = args->next;
+ ctx.removals = TRUE;
+ continue;
+ }
+
+ argsp = &args->next;
+ }
+ hash_table_destroy(&ctx.prev_args);
+ pool_unref(&ctx.pool);
+ return ctx.removals;
+}
+
+static bool
+mail_search_args_simplify_merge_flags(struct mail_search_arg **argsp,
+ bool parent_and)
+{
+ struct mail_search_arg *prev_flags = NULL;
+ bool removals = FALSE;
+
+ while (*argsp != NULL) {
+ struct mail_search_arg *args = *argsp;
+
+ if (args->type == SEARCH_SUB ||
+ args->type == SEARCH_OR ||
+ args->type == SEARCH_INTHREAD) {
+ if (mail_search_args_simplify_merge_flags(&args->value.subargs,
+ args->type != SEARCH_OR))
+ removals = TRUE;
+ } else if (args->type != SEARCH_FLAGS) {
+ /* ignore non-flags */
+ } else if (!((!args->match_not && parent_and) ||
+ (args->match_not && !parent_and))) {
+ /* can't merge these flags args */
+ } else if (prev_flags == NULL) {
+ /* first flags arg */
+ prev_flags = args;
+ } else {
+ /* merge to previous arg */
+ prev_flags->value.flags |= args->value.flags;
+ *argsp = args->next;
+ removals = TRUE;
+ continue;
+ }
+ argsp = &args->next;
+ }
+ return removals;
+}
+
+static bool
+mail_search_args_unnest_inthreads(struct mail_search_args *args,
+ struct mail_search_arg **argp,
+ bool parent_inthreads, bool parent_and)
+{
+ struct mail_search_arg *arg, *thread_arg, *or_arg;
+ bool child_inthreads = FALSE, non_inthreads = FALSE;
+
+ for (arg = *argp; arg != NULL; arg = arg->next) {
+ switch (arg->type) {
+ case SEARCH_SUB:
+ case SEARCH_OR:
+ if (!mail_search_args_unnest_inthreads(args,
+ &arg->value.subargs, parent_inthreads,
+ arg->type != SEARCH_OR)) {
+ arg->result = 1;
+ child_inthreads = TRUE;
+ } else {
+ arg->result = 0;
+ non_inthreads = TRUE;
+ }
+ break;
+ case SEARCH_INTHREAD:
+ if (mail_search_args_unnest_inthreads(args,
+ &arg->value.subargs, TRUE, TRUE)) {
+ /* children converted to SEARCH_INTHREADs */
+ arg->type = SEARCH_SUB;
+ }
+ args->have_inthreads = TRUE;
+ arg->result = 1;
+ child_inthreads = TRUE;
+ break;
+ default:
+ arg->result = 0;
+ non_inthreads = TRUE;
+ break;
+ }
+ }
+
+ if (!parent_inthreads || !child_inthreads || !non_inthreads)
+ return FALSE;
+
+ /* put all non-INTHREADs under a single INTHREAD */
+ thread_arg = p_new(args->pool, struct mail_search_arg, 1);
+ thread_arg->type = SEARCH_INTHREAD;
+
+ while (*argp != NULL) {
+ arg = *argp;
+ argp = &(*argp)->next;
+
+ if (arg->result == 0) {
+ /* not an INTHREAD or a SUB/OR with only INTHREADs */
+ arg->next = thread_arg->value.subargs;
+ thread_arg->value.subargs = arg;
+ }
+ }
+ if (!parent_and) {
+ /* We want to OR the args */
+ or_arg = p_new(args->pool, struct mail_search_arg, 1);
+ or_arg->type = SEARCH_OR;
+ or_arg->value.subargs = thread_arg->value.subargs;
+ thread_arg->value.subargs = or_arg;
+ }
+ return TRUE;
+}
+
+void mail_search_args_simplify(struct mail_search_args *args)
+{
+ bool removals;
+
+ args->simplified = TRUE;
+
+ removals = mail_search_args_simplify_sub(args, args->pool, &args->args, TRUE);
+ if (mail_search_args_unnest_inthreads(args, &args->args,
+ FALSE, TRUE)) {
+ /* we may have added some extra SUBs that could be dropped */
+ if (mail_search_args_simplify_sub(args, args->pool, &args->args, TRUE))
+ removals = TRUE;
+ }
+ do {
+ if (mail_search_args_simplify_drop_redundant_args(args, &args->args, TRUE))
+ removals = TRUE;
+ if (mail_search_args_simplify_extract_common(args, &args->args, args->pool, TRUE))
+ removals = TRUE;
+ if (removals)
+ removals = mail_search_args_simplify_sub(args, args->pool, &args->args, TRUE);
+ /* do the flag merging into a single arg only at the end.
+ up until then they're treated as any other search args,
+ which simplifies their handling. after the flags merging is
+ done, further simplifications are still possible. */
+ if (mail_search_args_simplify_merge_flags(&args->args, TRUE))
+ removals = TRUE;
+ } while (removals);
+}
diff --git a/src/lib-storage/mail-search-build.c b/src/lib-storage/mail-search-build.c
new file mode 100644
index 0000000..7c86ee3
--- /dev/null
+++ b/src/lib-storage/mail-search-build.c
@@ -0,0 +1,250 @@
+/* Copyright (c) 2002-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "str.h"
+#include "charset-utf8.h"
+#include "mail-storage-private.h"
+#include "mail-search-register.h"
+#include "mail-search-parser.h"
+#include "mail-search-build.h"
+
+
+static int mail_search_build_list(struct mail_search_build_context *ctx,
+ struct mail_search_arg **arg_r);
+
+struct mail_search_arg *
+mail_search_build_new(struct mail_search_build_context *ctx,
+ enum mail_search_arg_type type)
+{
+ struct mail_search_arg *arg;
+
+ arg = p_new(ctx->pool, struct mail_search_arg, 1);
+ arg->type = type;
+ return arg;
+}
+
+struct mail_search_arg *
+mail_search_build_str(struct mail_search_build_context *ctx,
+ enum mail_search_arg_type type)
+{
+ struct mail_search_arg *sarg;
+ const char *value;
+
+ sarg = mail_search_build_new(ctx, type);
+ if (mail_search_parse_string(ctx->parser, &value) < 0)
+ return NULL;
+ sarg->value.str = p_strdup(ctx->pool, value);
+ return sarg;
+}
+
+static int
+mail_search_build_key_int(struct mail_search_build_context *ctx,
+ struct mail_search_arg *parent,
+ struct mail_search_arg **arg_r)
+{
+ struct mail_search_arg *sarg;
+ struct mail_search_arg *old_parent = ctx->parent;
+ const char *key;
+ const struct mail_search_register_arg *reg_arg;
+ mail_search_register_fallback_t *fallback;
+ int ret;
+
+ ctx->parent = parent;
+
+ if ((ret = mail_search_parse_key(ctx->parser, &key)) <= 0)
+ return ret;
+
+ if (strcmp(key, MAIL_SEARCH_PARSER_KEY_LIST) == 0) {
+ if (mail_search_build_list(ctx, &sarg) < 0)
+ return -1;
+ if (sarg->value.subargs == NULL) {
+ ctx->_error = "No search parameters inside list";
+ return -1;
+ }
+
+ ctx->parent = old_parent;
+ *arg_r = sarg;
+ return 1;
+ }
+ key = t_str_ucase(key);
+
+ reg_arg = mail_search_register_find(ctx->reg, key);
+ if (reg_arg != NULL)
+ sarg = reg_arg->build(ctx);
+ else if (mail_search_register_get_fallback(ctx->reg, &fallback))
+ sarg = fallback(ctx, key);
+ else {
+ sarg = NULL;
+ ctx->_error = p_strconcat(ctx->pool, "Unknown argument ",
+ key, NULL);
+ }
+
+ ctx->parent = old_parent;
+ *arg_r = sarg;
+ return sarg == NULL ? -1 : 1;
+}
+
+int mail_search_build_key(struct mail_search_build_context *ctx,
+ struct mail_search_arg *parent,
+ struct mail_search_arg **arg_r)
+{
+ int ret;
+
+ ret = mail_search_build_key_int(ctx, parent, arg_r);
+ if (ret <= 0) {
+ if (ret == 0)
+ ctx->_error = "Missing argument";
+ return -1;
+ }
+ return 0;
+}
+
+static int mail_search_build_list(struct mail_search_build_context *ctx,
+ struct mail_search_arg **arg_r)
+{
+ struct mail_search_arg *sarg, **subargs;
+ enum mail_search_arg_type cur_type = SEARCH_SUB;
+ int ret;
+
+ sarg = p_new(ctx->pool, struct mail_search_arg, 1);
+ sarg->type = cur_type;
+
+ subargs = &sarg->value.subargs;
+ while ((ret = mail_search_build_key_int(ctx, sarg, subargs)) > 0) {
+ if (cur_type == sarg->type) {
+ /* expected type */
+ } else if (cur_type == SEARCH_SUB) {
+ /* type changed. everything in this list must now
+ belong to this type. */
+ cur_type = sarg->type;
+ } else {
+ ctx->_error =
+ "Use parenthesis when mixing ANDs and ORs";
+ return -1;
+ }
+ subargs = &(*subargs)->next;
+ sarg->type = SEARCH_SUB;
+ }
+ if (ret < 0)
+ return -1;
+ sarg->type = cur_type;
+ *arg_r = sarg;
+ return 0;
+}
+
+int mail_search_build(struct mail_search_register *reg,
+ struct mail_search_parser *parser, const char **charset,
+ struct mail_search_args **args_r,
+ const char **client_error_r)
+{
+ struct mail_search_build_context ctx;
+ struct mail_search_args *args;
+ struct mail_search_arg *root;
+ const char *str;
+ int ret;
+
+ *args_r = NULL;
+ *client_error_r = NULL;
+
+ i_zero(&ctx);
+ ctx.args = args = mail_search_build_init();
+ ctx.pool = args->pool;
+ ctx.reg = reg;
+ ctx.parser = parser;
+ ctx.charset = p_strdup(ctx.pool, *charset);
+
+ ret = mail_search_build_list(&ctx, &root);
+ if (!ctx.charset_checked && ret == 0) {
+ /* make sure we give an error message if charset is invalid */
+ ret = mail_search_build_get_utf8(&ctx, "", &str);
+ }
+ if (ret < 0) {
+ *client_error_r = ctx._error != NULL ? t_strdup(ctx._error) :
+ t_strdup(mail_search_parser_get_error(parser));
+ if (ctx.unknown_charset)
+ *charset = NULL;
+ pool_unref(&args->pool);
+ return -1;
+ }
+
+ if (root->type == SEARCH_SUB && !root->match_not) {
+ /* simple SUB root */
+ args->args = root->value.subargs;
+ } else {
+ args->args = root;
+ }
+
+ *args_r = args;
+ return 0;
+}
+
+struct mail_search_args *mail_search_build_init(void)
+{
+ struct mail_search_args *args;
+ pool_t pool;
+
+ pool = pool_alloconly_create("mail search args", 4096);
+ args = p_new(pool, struct mail_search_args, 1);
+ args->pool = pool;
+ args->refcount = 1;
+ return args;
+}
+
+struct mail_search_arg *
+mail_search_build_add(struct mail_search_args *args,
+ enum mail_search_arg_type type)
+{
+ struct mail_search_arg *arg;
+
+ arg = p_new(args->pool, struct mail_search_arg, 1);
+ arg->type = type;
+
+ arg->next = args->args;
+ args->args = arg;
+ return arg;
+}
+
+void mail_search_build_add_all(struct mail_search_args *args)
+{
+ (void)mail_search_build_add(args, SEARCH_ALL);
+}
+
+void mail_search_build_add_seqset(struct mail_search_args *args,
+ uint32_t seq1, uint32_t seq2)
+{
+ struct mail_search_arg *arg;
+
+ arg = mail_search_build_add(args, SEARCH_SEQSET);
+
+ p_array_init(&arg->value.seqset, args->pool, 1);
+ seq_range_array_add_range(&arg->value.seqset, seq1, seq2);
+}
+
+int mail_search_build_get_utf8(struct mail_search_build_context *ctx,
+ const char *input, const char **output_r)
+{
+ int ret;
+
+ T_BEGIN {
+ string_t *utf8 = t_str_new(128);
+ enum charset_result result;
+
+ if (charset_to_utf8_str(ctx->charset, NULL,
+ input, utf8, &result) < 0) {
+ /* unknown charset */
+ ctx->_error = "Unknown charset";
+ ctx->unknown_charset = TRUE;
+ ret = -1;
+ } else if (result != CHARSET_RET_OK) {
+ /* invalid key */
+ ctx->_error = "Invalid search key";
+ ret = -1;
+ } else {
+ *output_r = p_strdup(ctx->pool, str_c(utf8));
+ ret = 0;
+ }
+ } T_END;
+
+ ctx->charset_checked = TRUE;
+ return ret;
+}
diff --git a/src/lib-storage/mail-search-build.h b/src/lib-storage/mail-search-build.h
new file mode 100644
index 0000000..7bba1ce
--- /dev/null
+++ b/src/lib-storage/mail-search-build.h
@@ -0,0 +1,59 @@
+#ifndef MAIL_SEARCH_BUILD_H
+#define MAIL_SEARCH_BUILD_H
+
+#include "mail-search.h"
+#include "mail-search-register.h"
+
+struct mailbox;
+
+struct mail_search_build_context {
+ pool_t pool;
+ struct mail_search_args *args;
+ struct mail_search_register *reg;
+ struct mail_search_parser *parser;
+ const char *charset;
+
+ struct mail_search_arg *parent;
+ /* error is either here or in parser */
+ const char *_error;
+ bool charset_checked;
+ bool unknown_charset;
+};
+
+/* Start building a new search query. Use mail_search_args_unref() to
+ free it. */
+struct mail_search_args *mail_search_build_init(void);
+
+/* Convert IMAP SEARCH command compatible parameters to mail_search_args.
+ If charset is unknown, it's changed to NULL. */
+int mail_search_build(struct mail_search_register *reg,
+ struct mail_search_parser *parser, const char **charset,
+ struct mail_search_args **args_r,
+ const char **client_error_r);
+
+/* Add new search arg with given type. */
+struct mail_search_arg *
+mail_search_build_add(struct mail_search_args *args,
+ enum mail_search_arg_type type);
+/* Add SEARCH_ALL to search args. */
+void mail_search_build_add_all(struct mail_search_args *args);
+/* Add a sequence set to search args. */
+void mail_search_build_add_seqset(struct mail_search_args *args,
+ uint32_t seq1, uint32_t seq2);
+
+/* Convert input string into UTF-8 */
+int mail_search_build_get_utf8(struct mail_search_build_context *ctx,
+ const char *input, const char **output_r);
+
+struct mail_search_arg *
+mail_search_build_new(struct mail_search_build_context *ctx,
+ enum mail_search_arg_type type);
+struct mail_search_arg *
+mail_search_build_str(struct mail_search_build_context *ctx,
+ enum mail_search_arg_type type);
+/* Returns 0 if arg is returned, -1 if error. */
+int mail_search_build_key(struct mail_search_build_context *ctx,
+ struct mail_search_arg *parent,
+ struct mail_search_arg **arg_r);
+
+#endif
diff --git a/src/lib-storage/mail-search-mime-build.c b/src/lib-storage/mail-search-mime-build.c
new file mode 100644
index 0000000..cdec68f
--- /dev/null
+++ b/src/lib-storage/mail-search-mime-build.c
@@ -0,0 +1,173 @@
+/* Copyright (c) 2016-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "str.h"
+#include "charset-utf8.h"
+#include "mail-storage-private.h"
+#include "mail-search-parser.h"
+#include "mail-search-mime-register.h"
+#include "mail-search-mime-build.h"
+
+static int mail_search_mime_build_list(struct mail_search_mime_build_context *ctx,
+ struct mail_search_mime_arg **arg_r);
+
+struct mail_search_mime_arg *
+mail_search_mime_build_new(struct mail_search_mime_build_context *ctx,
+ enum mail_search_mime_arg_type type)
+{
+ struct mail_search_mime_arg *arg;
+
+ arg = p_new(ctx->ctx->pool, struct mail_search_mime_arg, 1);
+ arg->type = type;
+ return arg;
+}
+
+struct mail_search_mime_arg *
+mail_search_mime_build_str(struct mail_search_mime_build_context *ctx,
+ enum mail_search_mime_arg_type type)
+{
+ struct mail_search_mime_arg *sarg;
+ const char *value;
+
+ sarg = mail_search_mime_build_new(ctx, type);
+ if (mail_search_parse_string(ctx->ctx->parser, &value) < 0)
+ return NULL;
+ sarg->value.str = p_strdup(ctx->ctx->pool, value);
+ return sarg;
+}
+
+static int
+mail_search_mime_build_key_int(struct mail_search_mime_build_context *ctx,
+ struct mail_search_mime_arg *parent,
+ struct mail_search_mime_arg **arg_r)
+{
+ struct mail_search_mime_arg *sarg;
+ struct mail_search_mime_arg *old_parent = ctx->parent;
+ const char *key;
+ const struct mail_search_mime_register_arg *reg_arg;
+ int ret;
+
+ ctx->parent = parent;
+
+ if ((ret = mail_search_parse_key(ctx->ctx->parser, &key)) <= 0)
+ return ret;
+
+ if (strcmp(key, MAIL_SEARCH_PARSER_KEY_LIST) == 0) {
+ if (mail_search_mime_build_list(ctx, &sarg) < 0)
+ return -1;
+ if (sarg->value.subargs == NULL) {
+ ctx->ctx->_error = "No MIMEPART keys inside list";
+ return -1;
+ }
+
+ ctx->parent = old_parent;
+ *arg_r = sarg;
+ return 1;
+ }
+ key = t_str_ucase(key);
+
+ reg_arg = mail_search_mime_register_find(key);
+ if (reg_arg != NULL)
+ sarg = reg_arg->build(ctx);
+ else {
+ sarg = NULL;
+ ctx->ctx->_error = p_strconcat
+ (ctx->ctx->pool, "Unknown MIMEPART key ", key, NULL);
+ }
+
+ ctx->parent = old_parent;
+ *arg_r = sarg;
+ return sarg == NULL ? -1 : 1;
+}
+
+int mail_search_mime_build_key(struct mail_search_mime_build_context *ctx,
+ struct mail_search_mime_arg *parent,
+ struct mail_search_mime_arg **arg_r)
+{
+ int ret;
+
+ ret = mail_search_mime_build_key_int(ctx, parent, arg_r);
+ if (ret <= 0) {
+ if (ret == 0)
+ ctx->ctx->_error = "Missing MIMEPART key";
+ return -1;
+ }
+ return 0;
+}
+
+static int mail_search_mime_build_list(struct mail_search_mime_build_context *ctx,
+ struct mail_search_mime_arg **arg_r)
+{
+ struct mail_search_mime_arg *sarg, **subargs;
+ enum mail_search_mime_arg_type cur_type = SEARCH_MIME_SUB;
+ int ret;
+
+ sarg = p_new(ctx->ctx->pool, struct mail_search_mime_arg, 1);
+ sarg->type = cur_type;
+
+ subargs = &sarg->value.subargs;
+ while ((ret = mail_search_mime_build_key_int(ctx, sarg, subargs)) > 0) {
+ if (cur_type == sarg->type) {
+ /* expected type */
+ } else if (cur_type == SEARCH_MIME_SUB) {
+ /* type changed. everything in this list must now
+ belong to this type. */
+ cur_type = sarg->type;
+ } else {
+ ctx->ctx->_error =
+ "Use parenthesis when mixing ANDs and ORs";
+ return -1;
+ }
+ subargs = &(*subargs)->next;
+ sarg->type = SEARCH_MIME_SUB;
+ }
+ if (ret < 0)
+ return -1;
+ sarg->type = cur_type;
+ *arg_r = sarg;
+ return 0;
+}
+
+int mail_search_mime_build(struct mail_search_build_context *bctx,
+ struct mail_search_mime_part **mpart_r)
+{
+ struct mail_search_mime_build_context ctx;
+ struct mail_search_mime_part *mpart;
+ struct mail_search_mime_arg *root;
+ int ret;
+
+ *mpart_r = NULL;
+
+ i_zero(&ctx);
+ ctx.ctx = bctx;
+ ctx.mime_part = mpart =
+ p_new(bctx->pool, struct mail_search_mime_part, 1);
+
+ if ((ret=mail_search_mime_build_key(&ctx, NULL, &root)) < 0)
+ return ret;
+
+ if (root->type == SEARCH_MIME_SUB && !root->match_not) {
+ /* simple SUB root */
+ mpart->args = root->value.subargs;
+ } else {
+ mpart->args = root;
+ }
+
+ *mpart_r = mpart;
+ return 0;
+}
+
+struct mail_search_mime_arg *
+mail_search_mime_build_add(pool_t pool,
+ struct mail_search_mime_part *mpart,
+ enum mail_search_mime_arg_type type)
+{
+ struct mail_search_mime_arg *arg;
+
+ arg = p_new(pool, struct mail_search_mime_arg, 1);
+ arg->type = type;
+
+ arg->next = mpart->args;
+ mpart->args = arg;
+ return arg;
+}
diff --git a/src/lib-storage/mail-search-mime-build.h b/src/lib-storage/mail-search-mime-build.h
new file mode 100644
index 0000000..e690454
--- /dev/null
+++ b/src/lib-storage/mail-search-mime-build.h
@@ -0,0 +1,44 @@
+#ifndef MAIL_SEARCH_MIME_BUILD_H
+#define MAIL_SEARCH_MIME_BUILD_H
+
+#include "mail-search.h"
+#include "mail-search-build.h"
+#include "mail-search-register.h"
+#include "mail-search-mime.h"
+
+struct mailbox;
+
+struct mail_search_mime_build_context {
+ struct mail_search_build_context *ctx;
+ struct mail_search_mime_part *mime_part;
+
+ struct mail_search_mime_arg *parent;
+};
+
+/* Start building a new MIMPART search key. Use mail_search_mime_args_unref()
+ to free it. */
+struct mail_search_mime_part *mail_search_mime_build_init(void);
+
+/* Convert IMAP SEARCH command compatible parameters to
+ mail_search_mime_args. */
+int mail_search_mime_build(struct mail_search_build_context *bctx,
+ struct mail_search_mime_part **mpart_r);
+
+/* Add new search arg with given type. */
+struct mail_search_mime_arg *
+mail_search_mime_build_add(pool_t pool,
+ struct mail_search_mime_part *mpart,
+ enum mail_search_mime_arg_type type);
+
+struct mail_search_mime_arg *
+mail_search_mime_build_new(struct mail_search_mime_build_context *ctx,
+ enum mail_search_mime_arg_type type);
+struct mail_search_mime_arg *
+mail_search_mime_build_str(struct mail_search_mime_build_context *ctx,
+ enum mail_search_mime_arg_type type);
+/* Returns 0 if arg is returned, -1 if error. */
+int mail_search_mime_build_key(struct mail_search_mime_build_context *ctx,
+ struct mail_search_mime_arg *parent,
+ struct mail_search_mime_arg **arg_r);
+
+#endif
diff --git a/src/lib-storage/mail-search-mime-register.c b/src/lib-storage/mail-search-mime-register.c
new file mode 100644
index 0000000..c0bda3d
--- /dev/null
+++ b/src/lib-storage/mail-search-mime-register.c
@@ -0,0 +1,547 @@
+/* Copyright (c) 2016-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "ioloop.h"
+#include "array.h"
+#include "str.h"
+#include "imap-date.h"
+#include "imap-seqset.h"
+#include "imap-utf7.h"
+#include "imap-util.h"
+#include "mail-search-parser.h"
+#include "mail-search-mime-register.h"
+#include "mail-search-mime-build.h"
+#include "mail-search-mime.h"
+
+struct mail_search_mime_register {
+ ARRAY(struct mail_search_mime_register_arg) args;
+
+ bool args_sorted:1;
+};
+
+struct mail_search_mime_register *mail_search_mime_register = NULL;
+
+static void
+mail_search_register_add_default(void);
+
+/*
+ * Register
+ */
+
+static struct mail_search_mime_register *
+mail_search_mime_register_init(void)
+{
+ struct mail_search_mime_register *reg =
+ mail_search_mime_register;
+ if (reg == NULL) {
+ reg = i_new(struct mail_search_mime_register, 1);
+ i_array_init(&reg->args, 64);
+
+ mail_search_mime_register = reg;
+ mail_search_register_add_default();
+ }
+
+ return reg;
+}
+
+void mail_search_mime_register_deinit(void)
+{
+ struct mail_search_mime_register *reg =
+ mail_search_mime_register;
+
+ mail_search_mime_register = NULL;
+ if (reg == NULL)
+ return;
+
+ array_free(&reg->args);
+ i_free(reg);
+}
+
+void mail_search_mime_register_add(
+ const struct mail_search_mime_register_arg *arg,
+ unsigned int count)
+{
+ struct mail_search_mime_register *reg =
+ mail_search_mime_register_init();
+
+ array_append(&reg->args, arg, count);
+ reg->args_sorted = FALSE;
+}
+
+static int
+mail_search_mime_register_arg_cmp(
+ const struct mail_search_mime_register_arg *arg1,
+ const struct mail_search_mime_register_arg *arg2)
+{
+ return strcmp(arg1->key, arg2->key);
+}
+
+const struct mail_search_mime_register_arg *
+mail_search_mime_register_get(unsigned int *count_r)
+{
+ struct mail_search_mime_register *reg =
+ mail_search_mime_register_init();
+
+ if (!reg->args_sorted) {
+ array_sort(&reg->args, mail_search_mime_register_arg_cmp);
+ reg->args_sorted = TRUE;
+ }
+
+ return array_get(&reg->args, count_r);
+}
+
+const struct mail_search_mime_register_arg *
+mail_search_mime_register_find(const char *key)
+{
+ struct mail_search_mime_register_arg arg;
+ struct mail_search_mime_register *reg =
+ mail_search_mime_register_init();
+
+ if (!reg->args_sorted) {
+ array_sort(&reg->args, mail_search_mime_register_arg_cmp);
+ reg->args_sorted = TRUE;
+ }
+
+ arg.key = key;
+ return array_bsearch(&reg->args, &arg, mail_search_mime_register_arg_cmp);
+}
+
+/*
+ * Default MIMEPART args
+ */
+
+static struct mail_search_mime_arg *
+mail_search_mime_not(struct mail_search_mime_build_context *ctx)
+{
+ struct mail_search_mime_arg *smarg;
+
+ if (mail_search_mime_build_key(ctx, ctx->parent, &smarg) < 0)
+ return NULL;
+
+ smarg->match_not = !smarg->match_not;
+ return smarg;
+}
+
+static struct mail_search_mime_arg *
+mail_search_mime_or(struct mail_search_mime_build_context *ctx)
+{
+ struct mail_search_mime_arg *smarg, **subargs;
+
+ /* <search-key1> <search-key2> */
+ smarg = mail_search_mime_build_new(ctx, SEARCH_MIME_OR);
+
+ subargs = &smarg->value.subargs;
+ do {
+ if (mail_search_mime_build_key(ctx, smarg, subargs) < 0)
+ return NULL;
+ subargs = &(*subargs)->next;
+
+ /* <key> OR <key> OR ... <key> - put them all
+ under one SEARCH_MIME_OR list. */
+ } while (mail_search_parse_skip_next(ctx->ctx->parser, "OR"));
+
+ if (mail_search_mime_build_key(ctx, smarg, subargs) < 0)
+ return NULL;
+ return smarg;
+}
+
+#define CALLBACK_STR(_func, _type) \
+static struct mail_search_mime_arg *\
+mail_search_mime_##_func(struct mail_search_mime_build_context *ctx) \
+{ \
+ return mail_search_mime_build_str(ctx, _type); \
+}
+
+static struct mail_search_mime_arg *
+arg_new_date(struct mail_search_mime_build_context *ctx,
+ enum mail_search_mime_arg_type type)
+{
+ struct mail_search_mime_arg *smarg;
+ const char *value;
+
+ smarg = mail_search_mime_build_new(ctx, type);
+ if (mail_search_parse_string(ctx->ctx->parser, &value) < 0)
+ return NULL;
+ if (!imap_parse_date(value, &smarg->value.time)) {
+ ctx->ctx->_error = "Invalid search date parameter";
+ return NULL;
+ }
+ return smarg;
+}
+
+#define CALLBACK_DATE(_func, _type) \
+static struct mail_search_mime_arg *\
+mail_search_mime_##_func(struct mail_search_mime_build_context *ctx) \
+{ \
+ return arg_new_date(ctx, _type); \
+}
+CALLBACK_DATE(sentbefore, SEARCH_MIME_SENTBEFORE)
+CALLBACK_DATE(senton, SEARCH_MIME_SENTON)
+CALLBACK_DATE(sentsince, SEARCH_MIME_SENTSINCE)
+
+static struct mail_search_mime_arg *
+mail_search_mime_size(struct mail_search_mime_build_context *ctx)
+{
+ struct mail_search_mime_arg *smarg;
+ enum mail_search_mime_arg_type type;
+ const char *key, *value;
+ uoff_t size;
+
+ if (mail_search_parse_key(ctx->ctx->parser, &key) <= 0) {
+ ctx->ctx->_error = "Invalid MIMEPART SIZE key type";
+ return NULL;
+ }
+
+ key = t_str_ucase(key);
+ if (strcmp(key, "LARGER") == 0)
+ type = SEARCH_MIME_SIZE_LARGER;
+ else if (strcmp(key, "SMALLER") == 0)
+ type = SEARCH_MIME_SIZE_SMALLER;
+ else {
+ type = SEARCH_MIME_SIZE_EQUAL;
+ value = key;
+ }
+
+ if (type != SEARCH_MIME_SIZE_EQUAL &&
+ mail_search_parse_string(ctx->ctx->parser, &value) < 0) {
+ ctx->ctx->_error = "Invalid MIMEPART SIZE value";
+ return NULL;
+ }
+
+ if (str_to_uoff(value, &size) < 0) {
+ ctx->ctx->_error = "Invalid MIMEPART SIZE value";
+ return NULL;
+ }
+
+ smarg = mail_search_mime_build_new(ctx, type);
+ smarg->value.size = size;
+ return smarg;
+}
+
+CALLBACK_STR(description, SEARCH_MIME_DESCRIPTION)
+CALLBACK_STR(encoding, SEARCH_MIME_ENCODING)
+CALLBACK_STR(id, SEARCH_MIME_ID)
+CALLBACK_STR(language, SEARCH_MIME_LANGUAGE)
+CALLBACK_STR(location, SEARCH_MIME_LOCATION)
+CALLBACK_STR(md5, SEARCH_MIME_MD5)
+
+CALLBACK_STR(type, SEARCH_MIME_TYPE)
+CALLBACK_STR(subtype, SEARCH_MIME_SUBTYPE)
+
+CALLBACK_STR(bcc, SEARCH_MIME_BCC)
+CALLBACK_STR(cc, SEARCH_MIME_CC)
+CALLBACK_STR(from, SEARCH_MIME_FROM)
+CALLBACK_STR(in_reply_to, SEARCH_MIME_IN_REPLY_TO)
+CALLBACK_STR(message_id, SEARCH_MIME_MESSAGE_ID)
+CALLBACK_STR(reply_to, SEARCH_MIME_REPLY_TO)
+CALLBACK_STR(sender, SEARCH_MIME_SENDER)
+CALLBACK_STR(subject, SEARCH_MIME_SUBJECT)
+CALLBACK_STR(to, SEARCH_MIME_TO)
+
+static struct mail_search_mime_arg *
+arg_new_field(struct mail_search_mime_build_context *ctx,
+ enum mail_search_mime_arg_type type)
+{
+ struct mail_search_mime_arg *smarg;
+ const char *field_name, *value;
+
+ /* <field-name> <string> */
+ if (mail_search_parse_string(ctx->ctx->parser, &field_name) < 0)
+ return NULL;
+ if (mail_search_build_get_utf8(ctx->ctx, field_name, &field_name) < 0)
+ return NULL;
+ if (mail_search_parse_string(ctx->ctx->parser, &value) < 0)
+ return NULL;
+ if (mail_search_build_get_utf8(ctx->ctx, value, &value) < 0)
+ return NULL;
+
+ smarg = mail_search_mime_build_new(ctx, type);
+ smarg->field_name = str_ucase(p_strdup(ctx->ctx->pool, field_name));
+ smarg->value.str = value;
+
+ return smarg;
+}
+
+static struct mail_search_mime_arg *
+mail_search_mime_param(struct mail_search_mime_build_context *ctx)
+{
+ return arg_new_field
+ (ctx, SEARCH_MIME_PARAM);
+}
+
+static struct mail_search_mime_arg *
+mail_search_mime_header(struct mail_search_mime_build_context *ctx)
+{
+ return arg_new_field
+ (ctx, SEARCH_MIME_HEADER);
+}
+
+static struct mail_search_mime_arg *
+arg_new_body(struct mail_search_mime_build_context *ctx,
+ enum mail_search_mime_arg_type type)
+{
+ struct mail_search_mime_arg *smarg;
+
+ smarg = mail_search_mime_build_str(ctx, type);
+ if (smarg == NULL)
+ return NULL;
+
+ if (mail_search_build_get_utf8(ctx->ctx, smarg->value.str,
+ &smarg->value.str) < 0)
+ return NULL;
+ return smarg;
+}
+
+#define CALLBACK_BODY(_func, _type) \
+static struct mail_search_mime_arg *\
+mail_search_mime_##_func(struct mail_search_mime_build_context *ctx) \
+{ \
+ return arg_new_body(ctx, _type); \
+}
+CALLBACK_BODY(body, SEARCH_MIME_BODY)
+CALLBACK_BODY(text, SEARCH_MIME_TEXT)
+
+static struct mail_search_mime_arg *
+mail_search_mime_disposition(struct mail_search_mime_build_context *ctx)
+{
+ struct mail_search_mime_arg *smarg;
+ const char *key, *value;
+
+ if (mail_search_parse_key(ctx->ctx->parser, &key) <= 0) {
+ ctx->ctx->_error = "Invalid MIMEPART DISPOSITION key type";
+ return NULL;
+ }
+
+ key = t_str_ucase(key);
+ if (strcmp(key, "TYPE") == 0) {
+ if (mail_search_parse_string(ctx->ctx->parser, &value) < 0) {
+ ctx->ctx->_error = "Invalid MIMEPART DISPOSITION TYPE value";
+ return NULL;
+ }
+ smarg = mail_search_mime_build_new
+ (ctx, SEARCH_MIME_DISPOSITION_TYPE);
+ smarg->value.str = p_strdup(ctx->ctx->pool, value);
+ return smarg;
+ } else if (strcmp(key, "PARAM") == 0) {
+ return arg_new_field
+ (ctx, SEARCH_MIME_DISPOSITION_PARAM);
+ }
+
+ ctx->ctx->_error = "Invalid MIMEPART DISPOSITION key type";
+ return NULL;
+}
+
+static struct mail_search_mime_arg *
+mail_search_mime_depth(struct mail_search_mime_build_context *ctx)
+{
+ struct mail_search_mime_arg *smarg;
+ enum mail_search_mime_arg_type type;
+ const char *key, *value;
+ unsigned int depth;
+
+ if (mail_search_parse_key(ctx->ctx->parser, &key) <= 0) {
+ ctx->ctx->_error = "Invalid MIMEPART DEPTH key";
+ return NULL;
+ }
+
+ key = t_str_ucase(key);
+ if (strcmp(key, "MIN") == 0)
+ type = SEARCH_MIME_DEPTH_MIN;
+ else if (strcmp(key, "MAX") == 0)
+ type = SEARCH_MIME_DEPTH_MAX;
+ else {
+ type = SEARCH_MIME_DEPTH_EQUAL;
+ value = key;
+ }
+
+ if (type != SEARCH_MIME_DEPTH_EQUAL &&
+ mail_search_parse_string(ctx->ctx->parser, &value) < 0) {
+ ctx->ctx->_error = "Invalid MIMEPART DEPTH value";
+ return NULL;
+ }
+
+ if (str_to_uint(value, &depth) < 0) {
+ ctx->ctx->_error = "Invalid MIMEPART DEPTH level";
+ return NULL;
+ }
+
+ smarg = mail_search_mime_build_new(ctx, type);
+ smarg->value.number = depth;
+ return smarg;
+}
+
+static struct mail_search_mime_arg *
+mail_search_mime_index(struct mail_search_mime_build_context *ctx)
+{
+ struct mail_search_mime_arg *smarg;
+ const char *value;
+ unsigned int index;
+
+ if (mail_search_parse_string(ctx->ctx->parser, &value) < 0) {
+ ctx->ctx->_error = "Invalid MIMEPART INDEX value";
+ return NULL;
+ }
+
+ if (str_to_uint(value, &index) < 0) {
+ ctx->ctx->_error = "Invalid MIMEPART INDEX number";
+ return NULL;
+ }
+
+ smarg = mail_search_mime_build_new
+ (ctx, SEARCH_MIME_INDEX);
+ smarg->value.number = index;
+ return smarg;
+}
+
+static struct mail_search_mime_arg *
+mail_search_mime_filename(struct mail_search_mime_build_context *ctx)
+{
+ struct mail_search_mime_arg *smarg;
+ enum mail_search_mime_arg_type type;
+ const char *key, *value;
+
+ if (mail_search_parse_key(ctx->ctx->parser, &key) <= 0) {
+ ctx->ctx->_error = "Invalid MIMEPART FILENAME match type";
+ return NULL;
+ }
+
+ key = t_str_ucase(key);
+ if (strcmp(key, "IS") == 0)
+ type = SEARCH_MIME_FILENAME_IS;
+ else if (strcmp(key, "CONTAINS") == 0)
+ type = SEARCH_MIME_FILENAME_CONTAINS;
+ else if (strcmp(key, "BEGINS") == 0)
+ type = SEARCH_MIME_FILENAME_BEGINS;
+ else if (strcmp(key, "ENDS") == 0)
+ type = SEARCH_MIME_FILENAME_ENDS;
+ else {
+ ctx->ctx->_error = "Invalid MIMEPART FILENAME match type";
+ return NULL;
+ }
+
+ if (mail_search_parse_string(ctx->ctx->parser, &value) < 0) {
+ ctx->ctx->_error = "Invalid MIMEPART FILENAME string value";
+ return NULL;
+ }
+
+ if (mail_search_build_get_utf8(ctx->ctx, value, &value) < 0) {
+ ctx->ctx->_error = "Invalid MIMEPART FILENAME stromg value";
+ return NULL;
+ }
+
+ smarg = mail_search_mime_build_new(ctx, type);
+ smarg->value.str = value;
+ return smarg;
+}
+
+static struct mail_search_mime_arg *
+mail_search_mime_parent(struct mail_search_mime_build_context *ctx)
+{
+ struct mail_search_mime_arg *smarg, *subargs;
+
+ smarg = mail_search_mime_build_new(ctx, SEARCH_MIME_PARENT);
+ if (mail_search_mime_build_key(ctx, smarg, &subargs) < 0)
+ return NULL;
+ if (subargs == smarg)
+ smarg->value.subargs = NULL;
+ else if (subargs->type == SEARCH_MIME_SUB)
+ smarg->value.subargs = subargs->value.subargs;
+ else
+ smarg->value.subargs = subargs;
+ return smarg;
+}
+
+static struct mail_search_mime_arg *
+mail_search_mime_child(struct mail_search_mime_build_context *ctx)
+{
+ struct mail_search_mime_arg *smarg, *subargs;
+
+ smarg = mail_search_mime_build_new(ctx, SEARCH_MIME_CHILD);
+ if (mail_search_mime_build_key(ctx, smarg, &subargs) < 0)
+ return NULL;
+ if (subargs == smarg)
+ smarg->value.subargs = NULL;
+ else if (subargs->type == SEARCH_MIME_SUB)
+ smarg->value.subargs = subargs->value.subargs;
+ else
+ smarg->value.subargs = subargs;
+ return smarg;
+}
+
+static struct mail_search_mime_arg *
+mail_search_mime_exists(struct mail_search_mime_build_context *ctx)
+{
+ if (ctx->parent == NULL ||
+ (ctx->parent->type != SEARCH_MIME_PARENT &&
+ ctx->parent->type != SEARCH_MIME_CHILD)) {
+ ctx->ctx->_error = "EXISTS key can only be used with PARENT or CHILD";
+ return NULL;
+ }
+ return ctx->parent;
+}
+
+static const struct mail_search_mime_register_arg
+mime_register_args[] = {
+ /* argument set operations */
+ { "NOT", mail_search_mime_not },
+ { "OR", mail_search_mime_or },
+
+ /* dates */
+ { "SENTBEFORE", mail_search_mime_sentbefore },
+ { "SENTON", mail_search_mime_senton },
+ { "SENTSINCE", mail_search_mime_sentsince },
+
+ /* size */
+ { "SIZE", mail_search_mime_size },
+
+ /* part properties */
+ { "DESCRIPTION", mail_search_mime_description },
+ { "DISPOSITION", mail_search_mime_disposition },
+ { "ENCODING", mail_search_mime_encoding },
+ { "ID", mail_search_mime_id },
+ { "LANGUAGE", mail_search_mime_language },
+ { "LOCATION", mail_search_mime_location },
+ { "MD5", mail_search_mime_md5 },
+
+ /* content-type */
+ { "TYPE", mail_search_mime_type },
+ { "SUBTYPE", mail_search_mime_subtype },
+ { "PARAM", mail_search_mime_param },
+
+ /* headers */
+ { "HEADER", mail_search_mime_header },
+
+ /* message */
+ { "BCC", mail_search_mime_bcc },
+ { "CC", mail_search_mime_cc },
+ { "FROM", mail_search_mime_from },
+ { "IN-REPLY-TO", mail_search_mime_in_reply_to },
+ { "MESSAGE-ID", mail_search_mime_message_id },
+ { "REPLY-TO", mail_search_mime_reply_to },
+ { "SENDER", mail_search_mime_sender },
+ { "SUBJECT", mail_search_mime_subject },
+ { "TO", mail_search_mime_to },
+
+ /* body */
+ { "BODY", mail_search_mime_body },
+ { "TEXT", mail_search_mime_text },
+
+ /* position */
+ { "DEPTH", mail_search_mime_depth },
+ { "INDEX", mail_search_mime_index },
+
+ /* relations */
+ { "PARENT", mail_search_mime_parent },
+ { "CHILD", mail_search_mime_child },
+ { "EXISTS", mail_search_mime_exists },
+
+ /* filename */
+ { "FILENAME", mail_search_mime_filename },
+};
+
+static void
+mail_search_register_add_default(void)
+{
+ mail_search_mime_register_add(mime_register_args,
+ N_ELEMENTS(mime_register_args));
+}
diff --git a/src/lib-storage/mail-search-mime-register.h b/src/lib-storage/mail-search-mime-register.h
new file mode 100644
index 0000000..70bd5fd
--- /dev/null
+++ b/src/lib-storage/mail-search-mime-register.h
@@ -0,0 +1,30 @@
+#ifndef MAIL_SEARCH_MIME_REGISTER_H
+#define MAIL_SEARCH_MIME_REGISTER_H
+
+struct mail_search_mime_arg;
+struct mail_search_mime_build_context;
+
+struct mail_search_mime_register_arg {
+ const char *key;
+
+ /* returns parsed arg or NULL if error. error message is set to ctx->ctx. */
+ struct mail_search_mime_arg *
+ (*build)(struct mail_search_mime_build_context *ctx);
+};
+
+void mail_search_mime_register_deinit(void);
+
+void mail_search_mime_register_add(
+ const struct mail_search_mime_register_arg *arg,
+ unsigned int count);
+
+/* Return all registered args sorted. */
+const struct mail_search_mime_register_arg *
+mail_search_mime_register_get(unsigned int *count_r);
+
+/* Find key's registered arg, or NULL if not found. */
+const struct mail_search_mime_register_arg *
+mail_search_mime_register_find(const char *key);
+
+
+#endif
diff --git a/src/lib-storage/mail-search-mime.c b/src/lib-storage/mail-search-mime.c
new file mode 100644
index 0000000..39b1f41
--- /dev/null
+++ b/src/lib-storage/mail-search-mime.c
@@ -0,0 +1,609 @@
+/* Copyright (c) 2016-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "ioloop.h"
+#include "str.h"
+#include "utc-offset.h"
+#include "imap-date.h"
+#include "imap-util.h"
+#include "imap-quote.h"
+#include "mail-search.h"
+#include "mail-search-mime.h"
+
+/*
+ *
+ */
+
+static struct mail_search_mime_arg *
+mail_search_mime_arg_dup_one(pool_t pool,
+ const struct mail_search_mime_arg *arg)
+{
+ struct mail_search_mime_arg *new_arg;
+
+ new_arg = p_new(pool, struct mail_search_mime_arg, 1);
+ new_arg->type = arg->type;
+ new_arg->match_not = arg->match_not;
+ new_arg->match_always = arg->match_always;
+ new_arg->nonmatch_always = arg->nonmatch_always;
+
+ switch (arg->type) {
+ case SEARCH_MIME_OR:
+ case SEARCH_MIME_SUB:
+ new_arg->value.subargs =
+ mail_search_mime_arg_dup(pool, arg->value.subargs);
+ break;
+ case SEARCH_MIME_SIZE_EQUAL:
+ case SEARCH_MIME_SIZE_LARGER:
+ case SEARCH_MIME_SIZE_SMALLER:
+ new_arg->value.size = arg->value.size;
+ break;
+ case SEARCH_MIME_HEADER:
+ new_arg->field_name = p_strdup(pool, arg->field_name);
+ /* fall through */
+ case SEARCH_MIME_DESCRIPTION:
+ case SEARCH_MIME_DISPOSITION_TYPE:
+ case SEARCH_MIME_DISPOSITION_PARAM:
+ case SEARCH_MIME_ENCODING:
+ case SEARCH_MIME_ID:
+ case SEARCH_MIME_LANGUAGE:
+ case SEARCH_MIME_LOCATION:
+ case SEARCH_MIME_MD5:
+ case SEARCH_MIME_TYPE:
+ case SEARCH_MIME_SUBTYPE:
+ case SEARCH_MIME_PARAM:
+ case SEARCH_MIME_BODY:
+ case SEARCH_MIME_TEXT:
+ case SEARCH_MIME_CC:
+ case SEARCH_MIME_BCC:
+ case SEARCH_MIME_FROM:
+ case SEARCH_MIME_IN_REPLY_TO:
+ case SEARCH_MIME_MESSAGE_ID:
+ case SEARCH_MIME_REPLY_TO:
+ case SEARCH_MIME_SENDER:
+ case SEARCH_MIME_SUBJECT:
+ case SEARCH_MIME_TO:
+ case SEARCH_MIME_FILENAME_IS:
+ case SEARCH_MIME_FILENAME_CONTAINS:
+ case SEARCH_MIME_FILENAME_BEGINS:
+ case SEARCH_MIME_FILENAME_ENDS:
+ new_arg->value.str =
+ p_strdup(pool, arg->value.str);
+ break;
+ case SEARCH_MIME_SENTBEFORE:
+ case SEARCH_MIME_SENTON:
+ case SEARCH_MIME_SENTSINCE:
+ new_arg->value.time = arg->value.time;
+ break;
+ case SEARCH_MIME_PARENT:
+ case SEARCH_MIME_CHILD:
+ if (new_arg->value.subargs != NULL) {
+ new_arg->value.subargs =
+ mail_search_mime_arg_dup(pool, arg->value.subargs);
+ }
+ break;
+ case SEARCH_MIME_DEPTH_EQUAL:
+ case SEARCH_MIME_DEPTH_MIN:
+ case SEARCH_MIME_DEPTH_MAX:
+ case SEARCH_MIME_INDEX:
+ new_arg->value.number = arg->value.number;
+ break;
+ }
+ return new_arg;
+}
+
+struct mail_search_mime_arg *
+mail_search_mime_arg_dup(pool_t pool,
+ const struct mail_search_mime_arg *arg)
+{
+ struct mail_search_mime_arg *new_arg = NULL, **dest = &new_arg;
+
+ for (; arg != NULL; arg = arg->next) {
+ *dest = mail_search_mime_arg_dup_one(pool, arg);
+ dest = &(*dest)->next;
+ }
+ return new_arg;
+}
+
+struct mail_search_mime_part *
+mail_search_mime_part_dup(pool_t pool,
+ const struct mail_search_mime_part *mpart)
+{
+ struct mail_search_mime_part *new_mpart;
+
+ new_mpart = p_new(pool, struct mail_search_mime_part, 1);
+ new_mpart->simplified = mpart->simplified;
+ new_mpart->args = mail_search_mime_arg_dup(pool, mpart->args);
+ return new_mpart;
+}
+
+/*
+ *
+ */
+
+void mail_search_mime_args_reset(struct mail_search_mime_arg *args,
+ bool full_reset)
+{
+ while (args != NULL) {
+ if (args->type == SEARCH_MIME_OR || args->type == SEARCH_MIME_SUB)
+ mail_search_mime_args_reset(args->value.subargs, full_reset);
+
+ if (args->match_always) {
+ if (!full_reset)
+ args->result = 1;
+ else {
+ args->match_always = FALSE;
+ args->result = -1;
+ }
+ } else if (args->nonmatch_always) {
+ if (!full_reset)
+ args->result = 0;
+ else {
+ args->nonmatch_always = FALSE;
+ args->result = -1;
+ }
+ } else {
+ args->result = -1;
+ }
+
+ args = args->next;
+ }
+}
+
+static void search_mime_arg_foreach(struct mail_search_mime_arg *arg,
+ mail_search_mime_foreach_callback_t *callback,
+ void *context)
+{
+ struct mail_search_mime_arg *subarg;
+
+ if (arg->result != -1)
+ return;
+
+ if (arg->type == SEARCH_MIME_SUB) {
+ /* sublist of conditions */
+ i_assert(arg->value.subargs != NULL);
+
+ arg->result = 1;
+ subarg = arg->value.subargs;
+ while (subarg != NULL) {
+ if (subarg->result == -1)
+ search_mime_arg_foreach(subarg, callback, context);
+
+ if (subarg->result == -1)
+ arg->result = -1;
+ else if (subarg->result == 0) {
+ /* didn't match */
+ arg->result = 0;
+ break;
+ }
+
+ subarg = subarg->next;
+ }
+ if (arg->match_not && arg->result != -1)
+ arg->result = arg->result > 0 ? 0 : 1;
+ } else if (arg->type == SEARCH_MIME_OR) {
+ /* OR-list of conditions */
+ i_assert(arg->value.subargs != NULL);
+
+ subarg = arg->value.subargs;
+ arg->result = 0;
+ while (subarg != NULL) {
+ if (subarg->result == -1)
+ search_mime_arg_foreach(subarg, callback, context);
+
+ if (subarg->result == -1)
+ arg->result = -1;
+ else if (subarg->result > 0) {
+ /* matched */
+ arg->result = 1;
+ break;
+ }
+
+ subarg = subarg->next;
+ }
+ if (arg->match_not && arg->result != -1)
+ arg->result = arg->result > 0 ? 0 : 1;
+ } else {
+ /* just a single condition */
+ callback(arg, context);
+ }
+}
+
+#undef mail_search_mime_args_foreach
+int mail_search_mime_args_foreach(struct mail_search_mime_arg *args,
+ mail_search_mime_foreach_callback_t *callback,
+ void *context)
+{
+ int result;
+
+ result = 1;
+ for (; args != NULL; args = args->next) {
+ search_mime_arg_foreach(args, callback, context);
+
+ if (args->result == 0) {
+ /* didn't match */
+ return 0;
+ }
+
+ if (args->result == -1)
+ result = -1;
+ }
+
+ return result;
+}
+
+/*
+ *
+ */
+
+bool mail_search_mime_arg_one_equals(const struct mail_search_mime_arg *arg1,
+ const struct mail_search_mime_arg *arg2)
+{
+ if (arg1->type != arg2->type ||
+ arg1->match_not != arg2->match_not)
+ return FALSE;
+
+ switch (arg1->type) {
+ case SEARCH_MIME_OR:
+ case SEARCH_MIME_SUB:
+ return mail_search_mime_arg_equals(arg1->value.subargs,
+ arg2->value.subargs);
+
+ case SEARCH_MIME_SIZE_EQUAL:
+ case SEARCH_MIME_SIZE_LARGER:
+ case SEARCH_MIME_SIZE_SMALLER:
+ return arg1->value.size == arg2->value.size;
+
+ case SEARCH_MIME_HEADER:
+ case SEARCH_MIME_DISPOSITION_PARAM:
+ case SEARCH_MIME_PARAM:
+ if (strcasecmp(arg1->field_name, arg2->field_name) != 0)
+ return FALSE;
+ /* fall through */
+ case SEARCH_MIME_DESCRIPTION:
+ case SEARCH_MIME_DISPOSITION_TYPE:
+ case SEARCH_MIME_ENCODING:
+ case SEARCH_MIME_ID:
+ case SEARCH_MIME_LANGUAGE:
+ case SEARCH_MIME_LOCATION:
+ case SEARCH_MIME_MD5:
+ case SEARCH_MIME_TYPE:
+ case SEARCH_MIME_SUBTYPE:
+ case SEARCH_MIME_BODY:
+ case SEARCH_MIME_TEXT:
+ case SEARCH_MIME_CC:
+ case SEARCH_MIME_BCC:
+ case SEARCH_MIME_FROM:
+ case SEARCH_MIME_IN_REPLY_TO:
+ case SEARCH_MIME_MESSAGE_ID:
+ case SEARCH_MIME_REPLY_TO:
+ case SEARCH_MIME_SENDER:
+ case SEARCH_MIME_SUBJECT:
+ case SEARCH_MIME_TO:
+ case SEARCH_MIME_FILENAME_IS:
+ case SEARCH_MIME_FILENAME_CONTAINS:
+ case SEARCH_MIME_FILENAME_BEGINS:
+ case SEARCH_MIME_FILENAME_ENDS:
+ /* don't bother doing case-insensitive comparison. we should support
+ full i18n case-insensitivity (or the active comparator
+ in future). */
+ return strcmp(arg1->value.str, arg2->value.str) == 0;
+
+ case SEARCH_MIME_SENTBEFORE:
+ case SEARCH_MIME_SENTON:
+ case SEARCH_MIME_SENTSINCE:
+ return arg1->value.time == arg2->value.time;
+
+ case SEARCH_MIME_PARENT:
+ case SEARCH_MIME_CHILD:
+ if (arg1->value.subargs == NULL)
+ return arg2->value.subargs == NULL;
+ if (arg2->value.subargs == NULL)
+ return FALSE;
+ return mail_search_mime_arg_equals(arg1->value.subargs,
+ arg2->value.subargs);
+
+ case SEARCH_MIME_DEPTH_EQUAL:
+ case SEARCH_MIME_DEPTH_MIN:
+ case SEARCH_MIME_DEPTH_MAX:
+ case SEARCH_MIME_INDEX:
+ return arg1->value.number == arg2->value.number;
+ }
+ i_unreached();
+}
+
+bool mail_search_mime_arg_equals(const struct mail_search_mime_arg *arg1,
+ const struct mail_search_mime_arg *arg2)
+{
+ while (arg1 != NULL && arg2 != NULL) {
+ if (!mail_search_mime_arg_one_equals(arg1, arg2))
+ return FALSE;
+ arg1 = arg1->next;
+ arg2 = arg2->next;
+ }
+ return arg1 == NULL && arg2 == NULL;
+}
+
+bool mail_search_mime_parts_equal(const struct mail_search_mime_part *mpart1,
+ const struct mail_search_mime_part *mpart2)
+{
+ i_assert(mpart1->simplified == mpart2->simplified);
+
+ return mail_search_mime_arg_equals(mpart1->args, mpart2->args);
+}
+
+/*
+ *
+ */
+
+void mail_search_mime_simplify(struct mail_search_mime_part *mpart)
+{
+ mpart->simplified = TRUE;
+
+ // FIXME: implement and use
+}
+
+/*
+ *
+ */
+
+static bool
+mail_search_mime_subargs_to_imap(string_t *dest,
+ const struct mail_search_mime_arg *args,
+ const char *prefix, const char **error_r)
+{
+ const struct mail_search_mime_arg *arg;
+
+ if (prefix[0] == '\0')
+ str_append_c(dest, '(');
+ for (arg = args; arg != NULL; arg = arg->next) {
+ if (arg->next != NULL)
+ str_append(dest, prefix);
+ if (!mail_search_mime_arg_to_imap(dest, arg, error_r))
+ return FALSE;
+ if (arg->next != NULL)
+ str_append_c(dest, ' ');
+ }
+ if (prefix[0] == '\0')
+ str_append_c(dest, ')');
+ return TRUE;
+}
+
+static bool
+mail_search_mime_arg_to_imap_date(string_t *dest,
+ const struct mail_search_mime_arg *arg)
+{
+ time_t timestamp = arg->value.time;
+ const char *str;
+ struct tm *tm;
+ int tz_offset;
+
+ tm = localtime(&timestamp);
+ tz_offset = utc_offset(tm, timestamp);
+ timestamp -= tz_offset * 60;
+
+ if (!imap_to_date(timestamp, &str))
+ return FALSE;
+ str_printfa(dest, " \"%s\"", str);
+ return TRUE;
+}
+
+bool mail_search_mime_arg_to_imap(string_t *dest,
+ const struct mail_search_mime_arg *arg, const char **error_r)
+{
+ if (arg->match_not)
+ str_append(dest, "NOT ");
+ switch (arg->type) {
+ case SEARCH_MIME_OR:
+ if (!mail_search_mime_subargs_to_imap
+ (dest, arg->value.subargs, "OR ", error_r))
+ return FALSE;
+ break;
+ case SEARCH_MIME_SUB:
+ if (!mail_search_mime_subargs_to_imap
+ (dest, arg->value.subargs, "", error_r))
+ return FALSE;
+ break;
+ case SEARCH_MIME_SIZE_EQUAL:
+ str_printfa(dest, "SIZE %"PRIuUOFF_T, arg->value.size);
+ break;
+ case SEARCH_MIME_SIZE_LARGER:
+ str_printfa(dest, "SIZE LARGER %"PRIuUOFF_T, arg->value.size);
+ break;
+ case SEARCH_MIME_SIZE_SMALLER:
+ str_printfa(dest, "SIZE SMALLER %"PRIuUOFF_T, arg->value.size);
+ break;
+ case SEARCH_MIME_DESCRIPTION:
+ str_append(dest, "DESCRIPTION ");
+ imap_append_astring(dest, arg->value.str);
+ break;
+ case SEARCH_MIME_DISPOSITION_TYPE:
+ str_append(dest, "DISPOSITION TYPE ");
+ imap_append_astring(dest, arg->value.str);
+ break;
+ case SEARCH_MIME_DISPOSITION_PARAM:
+ str_append(dest, "DISPOSITION PARAM ");
+ imap_append_astring(dest, arg->field_name);
+ str_append_c(dest, ' ');
+ imap_append_astring(dest, arg->value.str);
+ break;
+ case SEARCH_MIME_ENCODING:
+ str_append(dest, "ENCODING ");
+ imap_append_astring(dest, arg->value.str);
+ break;
+ case SEARCH_MIME_ID:
+ str_append(dest, "ID ");
+ imap_append_astring(dest, arg->value.str);
+ break;
+ case SEARCH_MIME_LANGUAGE:
+ str_append(dest, "LANGUAGE ");
+ imap_append_astring(dest, arg->value.str);
+ break;
+ case SEARCH_MIME_LOCATION:
+ str_append(dest, "LOCATION ");
+ imap_append_astring(dest, arg->value.str);
+ break;
+ case SEARCH_MIME_MD5:
+ str_append(dest, "MD5 ");
+ imap_append_astring(dest, arg->value.str);
+ break;
+ case SEARCH_MIME_TYPE:
+ str_append(dest, "TYPE ");
+ imap_append_astring(dest, arg->value.str);
+ break;
+ case SEARCH_MIME_SUBTYPE:
+ str_append(dest, "SUBTYPE ");
+ imap_append_astring(dest, arg->value.str);
+ break;
+ case SEARCH_MIME_PARAM:
+ str_append(dest, "PARAM ");
+ imap_append_astring(dest, arg->field_name);
+ str_append_c(dest, ' ');
+ imap_append_astring(dest, arg->value.str);
+ break;
+ case SEARCH_MIME_HEADER:
+ str_append(dest, "HEADER ");
+ imap_append_astring(dest, arg->field_name);
+ str_append_c(dest, ' ');
+ imap_append_astring(dest, arg->value.str);
+ break;
+ case SEARCH_MIME_BODY:
+ str_append(dest, "BODY ");
+ imap_append_astring(dest, arg->value.str);
+ break;
+ case SEARCH_MIME_TEXT:
+ str_append(dest, "TEXT ");
+ imap_append_astring(dest, arg->value.str);
+ break;
+ case SEARCH_MIME_CC:
+ str_append(dest, "CC ");
+ imap_append_astring(dest, arg->value.str);
+ break;
+ case SEARCH_MIME_BCC:
+ str_append(dest, "BCC ");
+ imap_append_astring(dest, arg->value.str);
+ break;
+ case SEARCH_MIME_FROM:
+ str_append(dest, "FROM ");
+ imap_append_astring(dest, arg->value.str);
+ break;
+ case SEARCH_MIME_IN_REPLY_TO:
+ str_append(dest, "IN-REPLY-TO ");
+ imap_append_astring(dest, arg->value.str);
+ break;
+ case SEARCH_MIME_MESSAGE_ID:
+ str_append(dest, "MESSAGE-ID ");
+ imap_append_astring(dest, arg->value.str);
+ break;
+ case SEARCH_MIME_REPLY_TO:
+ str_append(dest, "REPLY-TO ");
+ imap_append_astring(dest, arg->value.str);
+ break;
+ case SEARCH_MIME_SENDER:
+ str_append(dest, "SENDER ");
+ imap_append_astring(dest, arg->value.str);
+ break;
+ case SEARCH_MIME_SENTBEFORE:
+ str_append(dest, "SENTBEFORE");
+ if (!mail_search_mime_arg_to_imap_date(dest, arg)) {
+ *error_r = t_strdup_printf(
+ "SENTBEFORE can't be written as IMAP MIMEPART key "
+ "for timestamp %"PRIdTIME_T, arg->value.time);
+ return FALSE;
+ }
+ break;
+ case SEARCH_MIME_SENTON:
+ str_append(dest, "SENTON");
+ if (!mail_search_mime_arg_to_imap_date(dest, arg)) {
+ *error_r = t_strdup_printf(
+ "SENTON can't be written as IMAP MIMEPART key "
+ "for timestamp %"PRIdTIME_T, arg->value.time);
+ return FALSE;
+ }
+ break;
+ case SEARCH_MIME_SENTSINCE:
+ str_append(dest, "SENTSINCE");
+ if (!mail_search_mime_arg_to_imap_date(dest, arg)) {
+ *error_r = t_strdup_printf(
+ "SENTSINCE can't be written as IMAP MIMEPART key "
+ "for timestamp %"PRIdTIME_T, arg->value.time);
+ return FALSE;
+ }
+ break;
+ case SEARCH_MIME_SUBJECT:
+ str_append(dest, "SUBJECT ");
+ imap_append_astring(dest, arg->value.str);
+ break;
+ case SEARCH_MIME_TO:
+ str_append(dest, "TO ");
+ imap_append_astring(dest, arg->value.str);
+ break;
+ case SEARCH_MIME_DEPTH_EQUAL:
+ str_printfa(dest, "DEPTH %u", arg->value.number);
+ break;
+ case SEARCH_MIME_DEPTH_MIN:
+ str_printfa(dest, "DEPTH MIN %u", arg->value.number);
+ break;
+ case SEARCH_MIME_DEPTH_MAX:
+ str_printfa(dest, "DEPTH MAX %u", arg->value.number);
+ break;
+ case SEARCH_MIME_INDEX:
+ str_printfa(dest, "INDEX %u", arg->value.number);
+ break;
+ case SEARCH_MIME_PARENT:
+ str_append(dest, "PARENT ");
+ if (arg->value.subargs == NULL)
+ str_append(dest, "EXISTS");
+ else if (!mail_search_mime_subargs_to_imap
+ (dest, arg->value.subargs, "", error_r))
+ return FALSE;
+ break;
+ case SEARCH_MIME_CHILD:
+ str_append(dest, "CHILD ");
+ if (arg->value.subargs == NULL)
+ str_append(dest, "EXISTS");
+ else if (!mail_search_mime_subargs_to_imap
+ (dest, arg->value.subargs, "", error_r))
+ return FALSE;
+ break;
+ case SEARCH_MIME_FILENAME_IS:
+ str_append(dest, "FILENAME IS ");
+ imap_append_astring(dest, arg->value.str);
+ break;
+ case SEARCH_MIME_FILENAME_CONTAINS:
+ str_append(dest, "FILENAME CONTAINS ");
+ imap_append_astring(dest, arg->value.str);
+ break;
+ case SEARCH_MIME_FILENAME_BEGINS:
+ str_append(dest, "FILENAME BEGINS ");
+ imap_append_astring(dest, arg->value.str);
+ break;
+ case SEARCH_MIME_FILENAME_ENDS:
+ str_append(dest, "FILENAME ENDS ");
+ imap_append_astring(dest, arg->value.str);
+ break;
+ }
+ return TRUE;
+}
+
+bool mail_search_mime_part_to_imap(string_t *dest,
+ const struct mail_search_mime_part *mpart, const char **error_r)
+{
+ const struct mail_search_mime_arg *arg;
+
+ i_assert(mpart->args != NULL);
+ if (mpart->args->next == NULL) {
+ if (!mail_search_mime_arg_to_imap(dest, mpart->args, error_r))
+ return FALSE;
+ } else {
+ str_append_c(dest, '(');
+ for (arg = mpart->args; arg != NULL; arg = arg->next) {
+ if (!mail_search_mime_arg_to_imap(dest, arg, error_r))
+ return FALSE;
+ if (arg->next != NULL)
+ str_append_c(dest, ' ');
+ }
+ str_append_c(dest, ')');
+ }
+ return TRUE;
+}
+
diff --git a/src/lib-storage/mail-search-mime.h b/src/lib-storage/mail-search-mime.h
new file mode 100644
index 0000000..d3754ed
--- /dev/null
+++ b/src/lib-storage/mail-search-mime.h
@@ -0,0 +1,146 @@
+#ifndef MAIL_SEARCH_MIMEPART_H
+#define MAIL_SEARCH_MIMEPART_H
+
+enum mail_search_mime_arg_type {
+ SEARCH_MIME_OR,
+ SEARCH_MIME_SUB,
+
+ /* sizes */
+ SEARCH_MIME_SIZE_EQUAL,
+ SEARCH_MIME_SIZE_LARGER,
+ SEARCH_MIME_SIZE_SMALLER,
+
+ /* part properties */
+ SEARCH_MIME_DESCRIPTION,
+ SEARCH_MIME_DISPOSITION_TYPE,
+ SEARCH_MIME_DISPOSITION_PARAM,
+ SEARCH_MIME_ENCODING,
+ SEARCH_MIME_ID,
+ SEARCH_MIME_LANGUAGE,
+ SEARCH_MIME_LOCATION,
+ SEARCH_MIME_MD5,
+
+ /* content-type */
+ SEARCH_MIME_TYPE,
+ SEARCH_MIME_SUBTYPE,
+ SEARCH_MIME_PARAM,
+
+ /* headers */
+ SEARCH_MIME_HEADER,
+
+ /* body */
+ SEARCH_MIME_BODY,
+ SEARCH_MIME_TEXT,
+
+ /* message */
+ SEARCH_MIME_CC,
+ SEARCH_MIME_BCC,
+ SEARCH_MIME_FROM,
+ SEARCH_MIME_IN_REPLY_TO,
+ SEARCH_MIME_MESSAGE_ID,
+ SEARCH_MIME_REPLY_TO,
+ SEARCH_MIME_SENDER,
+ SEARCH_MIME_SENTBEFORE,
+ SEARCH_MIME_SENTON, /* time must point to beginning of the day */
+ SEARCH_MIME_SENTSINCE,
+ SEARCH_MIME_SUBJECT,
+ SEARCH_MIME_TO,
+
+ /* relations */
+ SEARCH_MIME_PARENT,
+ SEARCH_MIME_CHILD,
+
+ /* position */
+ SEARCH_MIME_DEPTH_EQUAL,
+ SEARCH_MIME_DEPTH_MIN,
+ SEARCH_MIME_DEPTH_MAX,
+ SEARCH_MIME_INDEX,
+
+ /* filename */
+ SEARCH_MIME_FILENAME_IS,
+ SEARCH_MIME_FILENAME_CONTAINS,
+ SEARCH_MIME_FILENAME_BEGINS,
+ SEARCH_MIME_FILENAME_ENDS
+};
+
+struct mail_search_mime_arg {
+ /* NOTE: when adding new fields, make sure mail_search_mime_arg_dup_one()
+ and mail_search_mime_arg_one_equals() are updated. */
+ struct mail_search_mime_arg *next;
+
+ enum mail_search_mime_arg_type type;
+ union {
+ struct mail_search_mime_arg *subargs;
+ const char *str;
+ time_t time;
+ uoff_t size;
+ unsigned int number;
+ } value;
+
+ void *context;
+ const char *field_name; /* for SEARCH_HEADER* */
+ bool match_not:1; /* result = !result */
+ bool match_always:1; /* result = 1 always */
+ bool nonmatch_always:1; /* result = 0 always */
+
+ int result; /* -1 = unknown, 0 = unmatched, 1 = matched */
+};
+
+struct mail_search_mime_part {
+ struct mail_search_mime_arg *args;
+
+ bool simplified:1;
+};
+
+typedef void
+mail_search_mime_foreach_callback_t(struct mail_search_mime_arg *arg,
+ void *context);
+
+/* Returns TRUE if the two mimepart search keys are fully compatible. */
+bool mail_search_mime_parts_equal(const struct mail_search_mime_part *mpart1,
+ const struct mail_search_mime_part *mpart2);
+/* Same as mail_search_mime_part_equal(), but for individual
+ mail_search_mime_arg structs. All the siblings of arg1 and arg2 are
+ also compared. */
+bool mail_search_mime_arg_equals(const struct mail_search_mime_arg *arg1,
+ const struct mail_search_mime_arg *arg2);
+/* Same as mail_search_mime_arg_equals(), but don't compare siblings. */
+bool mail_search_mime_arg_one_equals(const struct mail_search_mime_arg *arg1,
+ const struct mail_search_mime_arg *arg2);
+
+struct mail_search_mime_part *
+mail_search_mime_part_dup(pool_t pool,
+ const struct mail_search_mime_part *mpart);
+struct mail_search_mime_arg *
+mail_search_mime_arg_dup(pool_t pool,
+ const struct mail_search_mime_arg *arg);
+
+/* Reset the results in search arguments. match_always is reset only if
+ full_reset is TRUE. */
+void mail_search_mime_args_reset(struct mail_search_mime_arg *args,
+ bool full_reset);
+
+/* goes through arguments in list that don't have a result yet.
+ Returns 1 = search matched, 0 = search unmatched, -1 = don't know yet */
+int mail_search_mime_args_foreach(struct mail_search_mime_arg *args,
+ mail_search_mime_foreach_callback_t *callback,
+ void *context) ATTR_NULL(3);
+#define mail_search_mime_args_foreach(args, callback, context) \
+ mail_search_mime_args_foreach(args - \
+ CALLBACK_TYPECHECK(callback, void (*)( \
+ struct mail_search_mime_arg *, typeof(context))), \
+ (mail_search_mime_foreach_callback_t *)callback, context)
+
+/* Simplify/optimize search arguments. Afterwards all OR/SUB args are
+ guaranteed to have match_not=FALSE. */
+void mail_search_mime_simplify(struct mail_search_mime_part *args);
+
+/* Appends MIMEPART search key to the dest string and returns TRUE. */
+bool mail_search_mime_part_to_imap(string_t *dest,
+ const struct mail_search_mime_part *mpart, const char **error_r);
+/* Like mail_search_mime_part_to_imap(), but append only a single MIMEPART
+ key. */
+bool mail_search_mime_arg_to_imap(string_t *dest,
+ const struct mail_search_mime_arg *arg, const char **error_r);
+
+#endif
diff --git a/src/lib-storage/mail-search-parser-cmdline.c b/src/lib-storage/mail-search-parser-cmdline.c
new file mode 100644
index 0000000..f2dc883
--- /dev/null
+++ b/src/lib-storage/mail-search-parser-cmdline.c
@@ -0,0 +1,98 @@
+/* Copyright (c) 2010-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "mail-search-parser-private.h"
+
+struct cmdline_mail_search_parser {
+ struct mail_search_parser parser;
+
+ const char *const *args;
+ unsigned int list_level;
+};
+
+static int cmdline_search_parse_key(struct mail_search_parser *_parser,
+ const char **key_r)
+{
+ struct cmdline_mail_search_parser *parser =
+ (struct cmdline_mail_search_parser *)_parser;
+
+ if (parser->args[0] == NULL) {
+ if (parser->list_level != 0) {
+ _parser->error = "Missing ')'";
+ return -1;
+ }
+ return 0;
+ }
+
+ if (strcmp(parser->args[0], "(") == 0) {
+ parser->list_level++;
+ parser->args++;
+ *key_r = MAIL_SEARCH_PARSER_KEY_LIST;
+ return 1;
+ } else if (strcmp(parser->args[0], ")") == 0) {
+ if (parser->list_level == 0) {
+ _parser->error = "Unexpected ')'";
+ return -1;
+ }
+ parser->list_level--;
+ parser->args++;
+ *key_r = MAIL_SEARCH_PARSER_KEY_LIST;
+ return 0;
+ } else {
+ *key_r = parser->args[0];
+ parser->args++;
+ return 1;
+ }
+}
+
+static int cmdline_search_parse_string(struct mail_search_parser *_parser,
+ const char **value_r)
+{
+ struct cmdline_mail_search_parser *parser =
+ (struct cmdline_mail_search_parser *)_parser;
+
+ if (parser->args[0] == NULL) {
+ _parser->error = "Missing parameter for search key";
+ return -1;
+ }
+ *value_r = parser->args[0];
+
+ parser->args++;
+ return 1;
+}
+
+static bool
+cmdline_search_parse_skip_next(struct mail_search_parser *_parser,
+ const char *str)
+{
+ struct cmdline_mail_search_parser *parser =
+ (struct cmdline_mail_search_parser *)_parser;
+
+ if (parser->args[0] == NULL)
+ return FALSE;
+ if (strcasecmp(parser->args[0], str) != 0)
+ return FALSE;
+
+ parser->args++;
+ return TRUE;
+}
+
+static const struct mail_search_parser_vfuncs mail_search_parser_cmdline_vfuncs = {
+ cmdline_search_parse_key,
+ cmdline_search_parse_string,
+ cmdline_search_parse_skip_next
+};
+
+struct mail_search_parser *
+mail_search_parser_init_cmdline(const char *const args[])
+{
+ struct cmdline_mail_search_parser *parser;
+ pool_t pool;
+
+ pool = pool_alloconly_create("cmdline search parser", 1024);
+ parser = p_new(pool, struct cmdline_mail_search_parser, 1);
+ parser->parser.pool = pool;
+ parser->parser.v = mail_search_parser_cmdline_vfuncs;
+ parser->args = args;
+ return &parser->parser;
+}
diff --git a/src/lib-storage/mail-search-parser-imap.c b/src/lib-storage/mail-search-parser-imap.c
new file mode 100644
index 0000000..12f48b3
--- /dev/null
+++ b/src/lib-storage/mail-search-parser-imap.c
@@ -0,0 +1,122 @@
+/* Copyright (c) 2010-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "imap-arg.h"
+#include "mail-search-parser-private.h"
+
+struct imap_arg_stack {
+ struct imap_arg_stack *prev;
+
+ const struct imap_arg *args;
+};
+
+struct imap_mail_search_parser {
+ struct mail_search_parser parser;
+
+ struct imap_arg_stack root, *cur;
+};
+
+static int imap_search_parse_key(struct mail_search_parser *_parser,
+ const char **key_r)
+{
+ struct imap_mail_search_parser *parser =
+ (struct imap_mail_search_parser *)_parser;
+ const struct imap_arg *arg = parser->cur->args;
+ struct imap_arg_stack *stack;
+
+ switch (arg->type) {
+ case IMAP_ARG_NIL:
+ case IMAP_ARG_ATOM:
+ *key_r = imap_arg_as_astring(arg);
+ break;
+ case IMAP_ARG_STRING:
+ case IMAP_ARG_LITERAL:
+ _parser->error = t_strconcat(
+ "Unexpected string as search key: ",
+ imap_arg_as_astring(arg), NULL);
+ return -1;
+ case IMAP_ARG_LIST:
+ stack = p_new(_parser->pool, struct imap_arg_stack, 1);
+ stack->prev = parser->cur;
+ stack->args = imap_arg_as_list(arg);
+
+ parser->cur->args++;
+ parser->cur = stack;
+
+ *key_r = MAIL_SEARCH_PARSER_KEY_LIST;
+ return 1;
+ case IMAP_ARG_EOL:
+ parser->cur = parser->cur->prev;
+ return 0;
+ case IMAP_ARG_LITERAL_SIZE:
+ case IMAP_ARG_LITERAL_SIZE_NONSYNC:
+ i_unreached();
+ }
+ parser->cur->args++;
+ return 1;
+}
+
+static int imap_search_parse_string(struct mail_search_parser *_parser,
+ const char **value_r)
+{
+ struct imap_mail_search_parser *parser =
+ (struct imap_mail_search_parser *)_parser;
+ const struct imap_arg *arg = parser->cur->args;
+
+ switch (arg->type) {
+ case IMAP_ARG_NIL:
+ case IMAP_ARG_ATOM:
+ case IMAP_ARG_STRING:
+ case IMAP_ARG_LITERAL:
+ *value_r = imap_arg_as_astring(arg);
+ break;
+ case IMAP_ARG_LIST:
+ _parser->error = "Unexpected (";
+ return -1;
+ case IMAP_ARG_EOL:
+ _parser->error = "Missing parameter for search key";
+ return -1;
+ case IMAP_ARG_LITERAL_SIZE:
+ case IMAP_ARG_LITERAL_SIZE_NONSYNC:
+ i_unreached();
+ }
+ parser->cur->args++;
+ return 1;
+}
+
+static bool
+imap_search_parse_skip_next(struct mail_search_parser *_parser, const char *str)
+{
+ struct imap_mail_search_parser *parser =
+ (struct imap_mail_search_parser *)_parser;
+ const char *arg;
+
+ if (!imap_arg_get_astring(parser->cur->args, &arg))
+ return FALSE;
+ if (strcasecmp(arg, str) != 0)
+ return FALSE;
+
+ parser->cur->args++;
+ return TRUE;
+}
+
+static const struct mail_search_parser_vfuncs mail_search_parser_imap_vfuncs = {
+ imap_search_parse_key,
+ imap_search_parse_string,
+ imap_search_parse_skip_next
+};
+
+struct mail_search_parser *
+mail_search_parser_init_imap(const struct imap_arg *args)
+{
+ struct imap_mail_search_parser *parser;
+ pool_t pool;
+
+ pool = pool_alloconly_create("imap search parser", 1024);
+ parser = p_new(pool, struct imap_mail_search_parser, 1);
+ parser->parser.pool = pool;
+ parser->parser.v = mail_search_parser_imap_vfuncs;
+ parser->root.args = args;
+ parser->cur = &parser->root;
+ return &parser->parser;
+}
diff --git a/src/lib-storage/mail-search-parser-private.h b/src/lib-storage/mail-search-parser-private.h
new file mode 100644
index 0000000..1e7af1f
--- /dev/null
+++ b/src/lib-storage/mail-search-parser-private.h
@@ -0,0 +1,22 @@
+#ifndef MAIL_SEARCH_PARSER_PRIVATE_H
+#define MAIL_SEARCH_PARSER_PRIVATE_H
+
+#include "mail-search-parser.h"
+
+struct mail_search_parser_vfuncs {
+ int (*parse_key)(struct mail_search_parser *parser, const char **key_r);
+ int (*parse_string)(struct mail_search_parser *parser,
+ const char **value_r);
+ bool (*parse_skip_next)(struct mail_search_parser *parser,
+ const char *str);
+};
+
+struct mail_search_parser {
+ struct mail_search_parser_vfuncs v;
+
+ pool_t pool;
+ const char *cur_key;
+ const char *error;
+};
+
+#endif
diff --git a/src/lib-storage/mail-search-parser.c b/src/lib-storage/mail-search-parser.c
new file mode 100644
index 0000000..4569d44
--- /dev/null
+++ b/src/lib-storage/mail-search-parser.c
@@ -0,0 +1,49 @@
+/* Copyright (c) 2010-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "mail-search-parser-private.h"
+
+void mail_search_parser_deinit(struct mail_search_parser **_parser)
+{
+ struct mail_search_parser *parser = *_parser;
+
+ *_parser = NULL;
+ pool_unref(&parser->pool);
+}
+
+int mail_search_parse_key(struct mail_search_parser *parser,
+ const char **key_r)
+{
+ int ret;
+
+ if ((ret = parser->v.parse_key(parser, key_r)) <= 0)
+ return ret;
+
+ parser->cur_key = *key_r;
+ return 1;
+}
+
+int mail_search_parse_string(struct mail_search_parser *parser,
+ const char **value_r)
+{
+ int ret;
+
+ ret = parser->v.parse_string(parser, value_r);
+ if (ret < 0 && parser->cur_key != NULL) {
+ parser->error = p_strdup_printf(parser->pool,
+ "%s (for search key: %s)",
+ parser->error, t_str_ucase(parser->cur_key));
+ }
+ return ret;
+}
+
+bool mail_search_parse_skip_next(struct mail_search_parser *parser,
+ const char *str)
+{
+ return parser->v.parse_skip_next(parser, str);
+}
+
+const char *mail_search_parser_get_error(struct mail_search_parser *parser)
+{
+ return parser->error;
+}
diff --git a/src/lib-storage/mail-search-parser.h b/src/lib-storage/mail-search-parser.h
new file mode 100644
index 0000000..0535587
--- /dev/null
+++ b/src/lib-storage/mail-search-parser.h
@@ -0,0 +1,34 @@
+#ifndef MAIL_SEARCH_PARSER_H
+#define MAIL_SEARCH_PARSER_H
+
+#define MAIL_SEARCH_PARSER_KEY_LIST "("
+
+struct imap_arg;
+
+/* Build a parser parsing the given imap args. NOTE: args must not be freed
+ until this parser is destroyed. */
+struct mail_search_parser *
+mail_search_parser_init_imap(const struct imap_arg *args);
+/* Build a parser parsing the given command line args. */
+struct mail_search_parser *
+mail_search_parser_init_cmdline(const char *const args[]);
+
+void mail_search_parser_deinit(struct mail_search_parser **parser);
+
+/* Key is set to the next search key, or MAIL_SEARCH_PARSER_KEY_LIST for
+ beginning of a list. Returns 1 if ok, 0 if no more keys in this
+ list/query, -1 if parsing error. */
+int mail_search_parse_key(struct mail_search_parser *parser,
+ const char **key_r);
+/* Get the next string. Returns 0 if ok, -1 if parsing error. */
+int mail_search_parse_string(struct mail_search_parser *parser,
+ const char **value_r);
+/* If next parameter equals to the given string case-insensitively, skip over
+ it and return TRUE. Otherwise do nothing and return FALSE. */
+bool mail_search_parse_skip_next(struct mail_search_parser *parser,
+ const char *str);
+
+/* Returns the reason string for parsing error. */
+const char *mail_search_parser_get_error(struct mail_search_parser *parser);
+
+#endif
diff --git a/src/lib-storage/mail-search-register-human.c b/src/lib-storage/mail-search-register-human.c
new file mode 100644
index 0000000..8e82bdc
--- /dev/null
+++ b/src/lib-storage/mail-search-register-human.c
@@ -0,0 +1,232 @@
+/* Copyright (c) 2010-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "ioloop.h"
+#include "array.h"
+#include "str.h"
+#include "unichar.h"
+#include "settings-parser.h"
+#include "mail-storage.h"
+#include "mail-search-register.h"
+#include "mail-search-parser.h"
+#include "mail-search-build.h"
+
+#include <time.h>
+#include <ctype.h>
+
+struct mail_search_register *mail_search_register_human;
+
+static struct mail_search_arg *
+human_search_or(struct mail_search_build_context *ctx)
+{
+ struct mail_search_arg *sarg;
+
+ /* this changes the parent arg to be an OR block instead of AND block */
+ ctx->parent->type = SEARCH_OR;
+ if (mail_search_build_key(ctx, ctx->parent, &sarg) < 0)
+ return NULL;
+ return sarg;
+}
+
+static struct mail_search_arg *
+arg_new_human_date(struct mail_search_build_context *ctx,
+ enum mail_search_arg_type type,
+ enum mail_search_date_type date_type)
+{
+ struct mail_search_arg *sarg;
+ const char *value;
+ bool utc;
+
+ sarg = mail_search_build_new(ctx, type);
+ if (mail_search_parse_string(ctx->parser, &value) < 0)
+ return NULL;
+
+ if (mail_parse_human_timestamp(value, &sarg->value.time, &utc) < 0)
+ sarg->value.time = (time_t)-1;
+ if (utc)
+ sarg->value.search_flags = MAIL_SEARCH_ARG_FLAG_UTC_TIMES;
+
+ if (sarg->value.time == (time_t)-1) {
+ ctx->_error = p_strconcat(ctx->pool,
+ "Invalid search date parameter: ", value, NULL);
+ return NULL;
+ }
+ sarg->value.date_type = date_type;
+ return sarg;
+}
+
+#define CALLBACK_DATE(_func, _type, _date_type) \
+static struct mail_search_arg *\
+human_search_##_func(struct mail_search_build_context *ctx) \
+{ \
+ return arg_new_human_date(ctx, _type, _date_type); \
+}
+CALLBACK_DATE(before, SEARCH_BEFORE, MAIL_SEARCH_DATE_TYPE_RECEIVED)
+CALLBACK_DATE(on, SEARCH_ON, MAIL_SEARCH_DATE_TYPE_RECEIVED)
+CALLBACK_DATE(since, SEARCH_SINCE, MAIL_SEARCH_DATE_TYPE_RECEIVED)
+
+CALLBACK_DATE(sentbefore, SEARCH_BEFORE, MAIL_SEARCH_DATE_TYPE_SENT)
+CALLBACK_DATE(senton, SEARCH_ON, MAIL_SEARCH_DATE_TYPE_SENT)
+CALLBACK_DATE(sentsince, SEARCH_SINCE, MAIL_SEARCH_DATE_TYPE_SENT)
+
+CALLBACK_DATE(savedbefore, SEARCH_BEFORE, MAIL_SEARCH_DATE_TYPE_SAVED)
+CALLBACK_DATE(savedon, SEARCH_ON, MAIL_SEARCH_DATE_TYPE_SAVED)
+CALLBACK_DATE(savedsince, SEARCH_SINCE, MAIL_SEARCH_DATE_TYPE_SAVED)
+
+static struct mail_search_arg *
+human_search_savedatesupported(struct mail_search_build_context *ctx)
+{
+ return mail_search_build_new(ctx, SEARCH_SAVEDATESUPPORTED);
+}
+
+static struct mail_search_arg *
+arg_new_human_size(struct mail_search_build_context *ctx,
+ enum mail_search_arg_type type)
+{
+ struct mail_search_arg *sarg;
+ const char *value, *error;
+
+ sarg = mail_search_build_new(ctx, type);
+ if (mail_search_parse_string(ctx->parser, &value) < 0)
+ return NULL;
+
+ if (settings_get_size(value, &sarg->value.size, &error) < 0) {
+ ctx->_error = p_strdup(ctx->pool, error);
+ return NULL;
+ }
+ return sarg;
+}
+
+static struct mail_search_arg *
+human_search_larger(struct mail_search_build_context *ctx)
+{
+ return arg_new_human_size(ctx, SEARCH_LARGER);
+}
+
+static struct mail_search_arg *
+human_search_smaller(struct mail_search_build_context *ctx)
+{
+ return arg_new_human_size(ctx, SEARCH_SMALLER);
+}
+
+static struct mail_search_arg *
+human_search_guid(struct mail_search_build_context *ctx)
+{
+ return mail_search_build_str(ctx, SEARCH_GUID);
+}
+
+static struct mail_search_arg *
+human_search_mailbox(struct mail_search_build_context *ctx)
+{
+ struct mail_search_arg *sarg;
+
+ sarg = mail_search_build_str(ctx, SEARCH_MAILBOX);
+ if (sarg == NULL)
+ return NULL;
+
+ if (strchr(sarg->value.str, '*') != NULL ||
+ strchr(sarg->value.str, '%') != NULL)
+ sarg->type = SEARCH_MAILBOX_GLOB;
+
+ if (!uni_utf8_str_is_valid(sarg->value.str)) {
+ ctx->_error = p_strconcat(ctx->pool,
+ "Mailbox name not valid UTF-8: ",
+ sarg->value.str, NULL);
+ return NULL;
+ }
+ return sarg;
+}
+
+static struct mail_search_arg *
+human_search_mailbox_guid(struct mail_search_build_context *ctx)
+{
+ return mail_search_build_str(ctx, SEARCH_MAILBOX_GUID);
+}
+
+static struct mail_search_arg *
+human_search_oldestonly(struct mail_search_build_context *ctx)
+{
+ ctx->args->stop_on_nonmatch = TRUE;
+ return mail_search_build_new(ctx, SEARCH_ALL);
+}
+
+static const struct mail_search_register_arg human_register_args[] = {
+ { "OR", human_search_or },
+
+ /* dates */
+ { "BEFORE", human_search_before },
+ { "ON", human_search_on },
+ { "SINCE", human_search_since },
+ { "SENTBEFORE", human_search_sentbefore },
+ { "SENTON", human_search_senton },
+ { "SENTSINCE", human_search_sentsince },
+ { "SAVEDBEFORE", human_search_savedbefore },
+ { "SAVEDON", human_search_savedon },
+ { "SAVEDSINCE", human_search_savedsince },
+ { "SAVEDATESUPPORTED", human_search_savedatesupported },
+ { "X-SAVEDBEFORE", human_search_savedbefore },
+ { "X-SAVEDON", human_search_savedon },
+ { "X-SAVEDSINCE", human_search_savedsince },
+
+ /* sizes */
+ { "LARGER", human_search_larger },
+ { "SMALLER", human_search_smaller },
+
+ /* Other Dovecot extensions: */
+ { "GUID", human_search_guid },
+ { "MAILBOX", human_search_mailbox },
+ { "MAILBOX-GUID", human_search_mailbox_guid },
+ { "OLDESTONLY", human_search_oldestonly }
+};
+
+static struct mail_search_register *
+mail_search_register_init_human(struct mail_search_register *imap_register)
+{
+ struct mail_search_register *reg;
+ mail_search_register_fallback_t *fallback;
+ ARRAY(struct mail_search_register_arg) copy_args;
+ const struct mail_search_register_arg *human_args, *imap_args;
+ unsigned int i, j, human_count, imap_count;
+ int ret;
+
+ reg = mail_search_register_init();
+ mail_search_register_add(reg, human_register_args,
+ N_ELEMENTS(human_register_args));
+
+ /* find and register args in imap that don't exist in human */
+ imap_args = mail_search_register_get(imap_register, &imap_count);
+ human_args = mail_search_register_get(reg, &human_count);
+ t_array_init(&copy_args, imap_count);
+ for (i = j = 0; i < imap_count && j < human_count; ) {
+ ret = strcmp(imap_args[i].key, human_args[j].key);
+ if (ret < 0) {
+ array_push_back(&copy_args, &imap_args[i]);
+ i++;
+ } else if (ret > 0) {
+ j++;
+ } else {
+ i++; j++;
+ }
+ }
+ for (; i < imap_count; i++)
+ array_push_back(&copy_args, &imap_args[i]);
+
+ imap_args = array_get(&copy_args, &imap_count);
+ mail_search_register_add(reg, imap_args, imap_count);
+
+ if (mail_search_register_get_fallback(imap_register, &fallback))
+ mail_search_register_fallback(reg, fallback);
+ return reg;
+}
+
+struct mail_search_register *mail_search_register_get_human(void)
+{
+ if (mail_search_register_human == NULL) {
+ struct mail_search_register *imap_reg =
+ mail_search_register_get_imap();
+
+ mail_search_register_human =
+ mail_search_register_init_human(imap_reg);
+ }
+ return mail_search_register_human;
+}
diff --git a/src/lib-storage/mail-search-register-imap.c b/src/lib-storage/mail-search-register-imap.c
new file mode 100644
index 0000000..c888b9e
--- /dev/null
+++ b/src/lib-storage/mail-search-register-imap.c
@@ -0,0 +1,638 @@
+/* Copyright (c) 2010-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "ioloop.h"
+#include "array.h"
+#include "str.h"
+#include "imap-date.h"
+#include "imap-seqset.h"
+#include "imap-utf7.h"
+#include "imap-util.h"
+#include "mail-search-register.h"
+#include "mail-search-parser.h"
+#include "mail-search-build.h"
+#include "mail-search-build.h"
+#include "mail-search-mime-build.h"
+
+struct mail_search_register *mail_search_register_imap;
+
+static struct mail_search_arg *
+imap_search_fallback(struct mail_search_build_context *ctx,
+ const char *key)
+{
+ struct mail_search_arg *sarg;
+
+ if (*key == '*' || (*key >= '0' && *key <= '9')) {
+ /* <message-set> */
+ sarg = mail_search_build_new(ctx, SEARCH_SEQSET);
+ p_array_init(&sarg->value.seqset, ctx->pool, 16);
+ if (imap_seq_set_parse(key, &sarg->value.seqset) < 0) {
+ ctx->_error = "Invalid messageset";
+ return NULL;
+ }
+ return sarg;
+ }
+ ctx->_error = p_strconcat(ctx->pool, "Unknown argument ", key, NULL);
+ return NULL;
+}
+
+static struct mail_search_arg *
+imap_search_not(struct mail_search_build_context *ctx)
+{
+ struct mail_search_arg *sarg;
+
+ if (mail_search_build_key(ctx, ctx->parent, &sarg) < 0)
+ return NULL;
+
+ sarg->match_not = !sarg->match_not;
+ return sarg;
+}
+
+static struct mail_search_arg *
+imap_search_or(struct mail_search_build_context *ctx)
+{
+ struct mail_search_arg *sarg, **subargs;
+
+ /* <search-key1> <search-key2> */
+ sarg = mail_search_build_new(ctx, SEARCH_OR);
+
+ subargs = &sarg->value.subargs;
+ do {
+ if (mail_search_build_key(ctx, sarg, subargs) < 0)
+ return NULL;
+ subargs = &(*subargs)->next;
+
+ /* <key> OR <key> OR ... <key> - put them all
+ under one SEARCH_OR list. */
+ } while (mail_search_parse_skip_next(ctx->parser, "OR"));
+
+ if (mail_search_build_key(ctx, sarg, subargs) < 0)
+ return NULL;
+ return sarg;
+}
+
+#define CALLBACK_STR(_func, _type) \
+static struct mail_search_arg *\
+imap_search_##_func(struct mail_search_build_context *ctx) \
+{ \
+ return mail_search_build_str(ctx, _type); \
+}
+static struct mail_search_arg *
+imap_search_all(struct mail_search_build_context *ctx)
+{
+ return mail_search_build_new(ctx, SEARCH_ALL);
+}
+
+static struct mail_search_arg *
+imap_search_uid(struct mail_search_build_context *ctx)
+{
+ struct mail_search_arg *sarg;
+
+ /* <message set> */
+ sarg = mail_search_build_str(ctx, SEARCH_UIDSET);
+ if (sarg == NULL)
+ return NULL;
+
+ p_array_init(&sarg->value.seqset, ctx->pool, 16);
+ if (strcmp(sarg->value.str, "$") == 0) {
+ /* SEARCHRES: delay initialization */
+ } else {
+ if (imap_seq_set_parse(sarg->value.str,
+ &sarg->value.seqset) < 0) {
+ ctx->_error = "Invalid UID messageset";
+ return NULL;
+ }
+ }
+ return sarg;
+}
+
+#define CALLBACK_FLAG(_func, _flag, _not) \
+static struct mail_search_arg *\
+imap_search_##_func(struct mail_search_build_context *ctx) \
+{ \
+ struct mail_search_arg *sarg; \
+ sarg = mail_search_build_new(ctx, SEARCH_FLAGS); \
+ sarg->value.flags = _flag; \
+ sarg->match_not = _not; \
+ return sarg; \
+}
+CALLBACK_FLAG(answered, MAIL_ANSWERED, FALSE)
+CALLBACK_FLAG(unanswered, MAIL_ANSWERED, TRUE)
+CALLBACK_FLAG(deleted, MAIL_DELETED, FALSE)
+CALLBACK_FLAG(undeleted, MAIL_DELETED, TRUE)
+CALLBACK_FLAG(draft, MAIL_DRAFT, FALSE)
+CALLBACK_FLAG(undraft, MAIL_DRAFT, TRUE)
+CALLBACK_FLAG(flagged, MAIL_FLAGGED, FALSE)
+CALLBACK_FLAG(unflagged, MAIL_FLAGGED, TRUE)
+CALLBACK_FLAG(seen, MAIL_SEEN, FALSE)
+CALLBACK_FLAG(unseen, MAIL_SEEN, TRUE)
+CALLBACK_FLAG(recent, MAIL_RECENT, FALSE)
+CALLBACK_FLAG(old, MAIL_RECENT, TRUE)
+
+static struct mail_search_arg *
+imap_search_new(struct mail_search_build_context *ctx)
+{
+ struct mail_search_arg *sarg;
+
+ /* NEW == (RECENT UNSEEN) */
+ sarg = mail_search_build_new(ctx, SEARCH_SUB);
+ sarg->value.subargs = imap_search_recent(ctx);
+ sarg->value.subargs->next = imap_search_unseen(ctx);
+ return sarg;
+}
+
+CALLBACK_STR(keyword, SEARCH_KEYWORDS)
+
+static struct mail_search_arg *
+imap_search_unkeyword(struct mail_search_build_context *ctx)
+{
+ struct mail_search_arg *sarg;
+
+ sarg = imap_search_keyword(ctx);
+ if (sarg != NULL)
+ sarg->match_not = TRUE;
+ return sarg;
+}
+
+static struct mail_search_arg *
+arg_new_date(struct mail_search_build_context *ctx,
+ enum mail_search_arg_type type,
+ enum mail_search_date_type date_type)
+{
+ struct mail_search_arg *sarg;
+ const char *value;
+
+ sarg = mail_search_build_new(ctx, type);
+ if (mail_search_parse_string(ctx->parser, &value) < 0)
+ return NULL;
+ if (!imap_parse_date(value, &sarg->value.time)) {
+ ctx->_error = "Invalid search date parameter";
+ return NULL;
+ }
+ sarg->value.date_type = date_type;
+ return sarg;
+}
+
+#define CALLBACK_DATE(_func, _type, _date_type) \
+static struct mail_search_arg *\
+imap_search_##_func(struct mail_search_build_context *ctx) \
+{ \
+ return arg_new_date(ctx, _type, _date_type); \
+}
+CALLBACK_DATE(before, SEARCH_BEFORE, MAIL_SEARCH_DATE_TYPE_RECEIVED)
+CALLBACK_DATE(on, SEARCH_ON, MAIL_SEARCH_DATE_TYPE_RECEIVED)
+CALLBACK_DATE(since, SEARCH_SINCE, MAIL_SEARCH_DATE_TYPE_RECEIVED)
+
+CALLBACK_DATE(sentbefore, SEARCH_BEFORE, MAIL_SEARCH_DATE_TYPE_SENT)
+CALLBACK_DATE(senton, SEARCH_ON, MAIL_SEARCH_DATE_TYPE_SENT)
+CALLBACK_DATE(sentsince, SEARCH_SINCE, MAIL_SEARCH_DATE_TYPE_SENT)
+
+CALLBACK_DATE(savedbefore, SEARCH_BEFORE, MAIL_SEARCH_DATE_TYPE_SAVED)
+CALLBACK_DATE(savedon, SEARCH_ON, MAIL_SEARCH_DATE_TYPE_SAVED)
+CALLBACK_DATE(savedsince, SEARCH_SINCE, MAIL_SEARCH_DATE_TYPE_SAVED)
+
+CALLBACK_DATE(x_savedbefore, SEARCH_BEFORE, MAIL_SEARCH_DATE_TYPE_SAVED)
+CALLBACK_DATE(x_savedon, SEARCH_ON, MAIL_SEARCH_DATE_TYPE_SAVED)
+CALLBACK_DATE(x_savedsince, SEARCH_SINCE, MAIL_SEARCH_DATE_TYPE_SAVED)
+
+static struct mail_search_arg *
+imap_search_savedatesupported(struct mail_search_build_context *ctx)
+{
+ return mail_search_build_new(ctx, SEARCH_SAVEDATESUPPORTED);
+}
+
+static struct mail_search_arg *
+arg_new_size(struct mail_search_build_context *ctx,
+ enum mail_search_arg_type type)
+{
+ struct mail_search_arg *sarg;
+ const char *value;
+
+ sarg = mail_search_build_new(ctx, type);
+ if (mail_search_parse_string(ctx->parser, &value) < 0)
+ return NULL;
+
+ if (str_to_uoff(value, &sarg->value.size) < 0) {
+ ctx->_error = "Invalid search size parameter";
+ return NULL;
+ }
+ return sarg;
+}
+
+static struct mail_search_arg *
+imap_search_larger(struct mail_search_build_context *ctx)
+{
+ return arg_new_size(ctx, SEARCH_LARGER);
+}
+
+static struct mail_search_arg *
+imap_search_smaller(struct mail_search_build_context *ctx)
+{
+ return arg_new_size(ctx, SEARCH_SMALLER);
+}
+
+static struct mail_search_arg *
+arg_new_header(struct mail_search_build_context *ctx,
+ enum mail_search_arg_type type, const char *hdr_name)
+{
+ struct mail_search_arg *sarg;
+ const char *value;
+
+ sarg = mail_search_build_new(ctx, type);
+ if (mail_search_parse_string(ctx->parser, &value) < 0)
+ return NULL;
+
+ if (mail_search_build_get_utf8(ctx, value, &sarg->value.str) < 0)
+ return NULL;
+
+ sarg->hdr_field_name = p_strdup(ctx->pool, hdr_name);
+ return sarg;
+}
+
+#define CALLBACK_HDR(_name, _type) \
+static struct mail_search_arg *\
+imap_search_##_name(struct mail_search_build_context *ctx) \
+{ \
+ return arg_new_header(ctx, _type, #_name); \
+}
+CALLBACK_HDR(bcc, SEARCH_HEADER_ADDRESS)
+CALLBACK_HDR(cc, SEARCH_HEADER_ADDRESS)
+CALLBACK_HDR(from, SEARCH_HEADER_ADDRESS)
+CALLBACK_HDR(to, SEARCH_HEADER_ADDRESS)
+CALLBACK_HDR(subject, SEARCH_HEADER_COMPRESS_LWSP)
+
+static struct mail_search_arg *
+imap_search_header(struct mail_search_build_context *ctx)
+{
+ const char *hdr_name;
+
+ /* <hdr-name> <string> */
+ if (mail_search_parse_string(ctx->parser, &hdr_name) < 0)
+ return NULL;
+ if (mail_search_build_get_utf8(ctx, hdr_name, &hdr_name) < 0)
+ return NULL;
+
+ return arg_new_header(ctx, SEARCH_HEADER, t_str_ucase(hdr_name));
+}
+
+static struct mail_search_arg *
+arg_new_body(struct mail_search_build_context *ctx,
+ enum mail_search_arg_type type)
+{
+ struct mail_search_arg *sarg;
+
+ sarg = mail_search_build_str(ctx, type);
+ if (sarg == NULL)
+ return NULL;
+
+ if (mail_search_build_get_utf8(ctx, sarg->value.str,
+ &sarg->value.str) < 0)
+ return NULL;
+ return sarg;
+}
+
+#define CALLBACK_BODY(_func, _type) \
+static struct mail_search_arg *\
+imap_search_##_func(struct mail_search_build_context *ctx) \
+{ \
+ return arg_new_body(ctx, _type); \
+}
+CALLBACK_BODY(body, SEARCH_BODY)
+CALLBACK_BODY(text, SEARCH_TEXT)
+
+static struct mail_search_arg *
+arg_new_interval(struct mail_search_build_context *ctx,
+ enum mail_search_arg_type type)
+{
+ struct mail_search_arg *sarg;
+ const char *value;
+ uint32_t interval;
+
+ sarg = mail_search_build_new(ctx, type);
+ if (mail_search_parse_string(ctx->parser, &value) < 0)
+ return NULL;
+
+ if (str_to_uint32(value, &interval) < 0 || interval == 0) {
+ ctx->_error = "Invalid search interval parameter";
+ return NULL;
+ }
+ sarg->value.search_flags = MAIL_SEARCH_ARG_FLAG_UTC_TIMES;
+ sarg->value.time = ioloop_time - interval;
+ sarg->value.date_type = MAIL_SEARCH_DATE_TYPE_RECEIVED;
+ return sarg;
+}
+
+static struct mail_search_arg *
+imap_search_older(struct mail_search_build_context *ctx)
+{
+ struct mail_search_arg *sarg;
+
+ sarg = arg_new_interval(ctx, SEARCH_BEFORE);
+ if (sarg == NULL)
+ return NULL;
+
+ /* we need to match also equal, but SEARCH_BEFORE compares with "<" */
+ sarg->value.time++;
+ return sarg;
+}
+
+static struct mail_search_arg *
+imap_search_younger(struct mail_search_build_context *ctx)
+{
+ return arg_new_interval(ctx, SEARCH_SINCE);
+}
+
+static int
+arg_modseq_set_type(struct mail_search_build_context *ctx,
+ struct mail_search_modseq *modseq, const char *name)
+{
+ if (strcasecmp(name, "all") == 0)
+ modseq->type = MAIL_SEARCH_MODSEQ_TYPE_ANY;
+ else if (strcasecmp(name, "priv") == 0)
+ modseq->type = MAIL_SEARCH_MODSEQ_TYPE_PRIVATE;
+ else if (strcasecmp(name, "shared") == 0)
+ modseq->type = MAIL_SEARCH_MODSEQ_TYPE_SHARED;
+ else {
+ ctx->_error = "Invalid MODSEQ type";
+ return -1;
+ }
+ return 0;
+}
+
+static int
+arg_modseq_set_ext(struct mail_search_build_context *ctx,
+ struct mail_search_arg *sarg, const char *name)
+{
+ const char *value;
+
+ name = t_str_lcase(name);
+ if (!str_begins(name, "/flags/"))
+ return 0;
+ name += 7;
+
+ /* set name */
+ if (*name == '\\') {
+ /* system flag */
+ sarg->value.flags = imap_parse_system_flag(name);
+ if (sarg->value.flags == 0 ||
+ sarg->value.flags == MAIL_RECENT) {
+ ctx->_error = "Invalid MODSEQ system flag";
+ return -1;
+ }
+ } else {
+ sarg->value.str = p_strdup(ctx->pool, name);
+ }
+
+ /* set type */
+ if (mail_search_parse_string(ctx->parser, &value) < 0)
+ return -1;
+ if (arg_modseq_set_type(ctx, sarg->value.modseq, value) < 0)
+ return -1;
+ return 1;
+}
+
+static struct mail_search_arg *
+imap_search_modseq(struct mail_search_build_context *ctx)
+{
+ struct mail_search_arg *sarg;
+ const char *value;
+ int ret;
+
+ /* [<name> <type>] <modseq> */
+ sarg = mail_search_build_new(ctx, SEARCH_MODSEQ);
+ sarg->value.modseq = p_new(ctx->pool, struct mail_search_modseq, 1);
+
+ if (mail_search_parse_string(ctx->parser, &value) < 0)
+ return NULL;
+
+ if ((ret = arg_modseq_set_ext(ctx, sarg, value)) < 0)
+ return NULL;
+ if (ret > 0) {
+ /* extension data used */
+ if (mail_search_parse_string(ctx->parser, &value) < 0)
+ return NULL;
+ }
+
+ if (str_to_uint64(value, &sarg->value.modseq->modseq) < 0) {
+ ctx->_error = "Invalid MODSEQ value";
+ return NULL;
+ }
+ return sarg;
+}
+
+static struct mail_search_arg *
+imap_search_last_result(struct mail_search_build_context *ctx)
+{
+ struct mail_search_arg *sarg;
+
+ /* SEARCHRES: delay initialization */
+ sarg = mail_search_build_new(ctx, SEARCH_UIDSET);
+ sarg->value.str = "$";
+ p_array_init(&sarg->value.seqset, ctx->pool, 16);
+ return sarg;
+}
+
+static void mail_search_arg_set_fuzzy(struct mail_search_arg *sarg)
+{
+ for (; sarg != NULL; sarg = sarg->next) {
+ sarg->fuzzy = TRUE;
+ switch (sarg->type) {
+ case SEARCH_OR:
+ case SEARCH_SUB:
+ case SEARCH_INTHREAD:
+ mail_search_arg_set_fuzzy(sarg->value.subargs);
+ break;
+ default:
+ break;
+ }
+ }
+}
+
+static struct mail_search_arg *
+imap_search_fuzzy(struct mail_search_build_context *ctx)
+{
+ struct mail_search_arg *sarg;
+
+ if (mail_search_build_key(ctx, ctx->parent, &sarg) < 0)
+ return NULL;
+ i_assert(sarg->next == NULL);
+
+ mail_search_arg_set_fuzzy(sarg);
+ return sarg;
+}
+
+static struct mail_search_arg *
+imap_search_mimepart(struct mail_search_build_context *ctx)
+{
+ struct mail_search_arg *sarg;
+
+ sarg = mail_search_build_new(ctx, SEARCH_MIMEPART);
+ if (mail_search_mime_build(ctx, &sarg->value.mime_part) < 0)
+ return NULL;
+ return sarg;
+}
+
+static struct mail_search_arg *
+imap_search_inthread(struct mail_search_build_context *ctx)
+{
+ struct mail_search_arg *sarg;
+
+ /* <algorithm> <search key> */
+ enum mail_thread_type thread_type;
+ const char *algorithm;
+
+ if (mail_search_parse_string(ctx->parser, &algorithm) < 0)
+ return NULL;
+ if (!mail_thread_type_parse(algorithm, &thread_type)) {
+ ctx->_error = "Unknown thread algorithm";
+ return NULL;
+ }
+
+ sarg = mail_search_build_new(ctx, SEARCH_INTHREAD);
+ sarg->value.thread_type = thread_type;
+ if (mail_search_build_key(ctx, sarg, &sarg->value.subargs) < 0)
+ return NULL;
+ return sarg;
+}
+
+CALLBACK_STR(x_guid, SEARCH_GUID)
+
+static struct mail_search_arg *
+imap_search_x_mailbox(struct mail_search_build_context *ctx)
+{
+ struct mail_search_arg *sarg;
+ string_t *utf8_name;
+
+ sarg = mail_search_build_str(ctx, SEARCH_MAILBOX_GLOB);
+ if (sarg == NULL)
+ return NULL;
+
+ utf8_name = t_str_new(strlen(sarg->value.str));
+ if (imap_utf7_to_utf8(sarg->value.str, utf8_name) < 0) {
+ ctx->_error = "X-MAILBOX name not mUTF-7";
+ return NULL;
+ }
+ sarg->value.str = p_strdup(ctx->pool, str_c(utf8_name));
+ return sarg;
+}
+
+static struct mail_search_arg *
+imap_search_x_real_uid(struct mail_search_build_context *ctx)
+{
+ struct mail_search_arg *sarg;
+
+ /* <message set> */
+ sarg = mail_search_build_str(ctx, SEARCH_REAL_UID);
+ if (sarg == NULL)
+ return NULL;
+
+ p_array_init(&sarg->value.seqset, ctx->pool, 16);
+ if (imap_seq_set_parse(sarg->value.str,
+ &sarg->value.seqset) < 0) {
+ ctx->_error = "Invalid X-REAL-UID messageset";
+ return NULL;
+ }
+ return sarg;
+}
+
+static const struct mail_search_register_arg imap_register_args[] = {
+ /* argument set operations */
+ { "NOT", imap_search_not },
+ { "OR", imap_search_or },
+
+ /* message sets */
+ { "ALL", imap_search_all },
+ { "UID", imap_search_uid },
+
+ /* flags */
+ { "ANSWERED", imap_search_answered },
+ { "UNANSWERED", imap_search_unanswered },
+ { "DELETED", imap_search_deleted },
+ { "UNDELETED", imap_search_undeleted },
+ { "DRAFT", imap_search_draft },
+ { "UNDRAFT", imap_search_undraft },
+ { "FLAGGED", imap_search_flagged },
+ { "UNFLAGGED", imap_search_unflagged },
+ { "SEEN", imap_search_seen },
+ { "UNSEEN", imap_search_unseen },
+ { "RECENT", imap_search_recent },
+ { "OLD", imap_search_old },
+ { "NEW", imap_search_new },
+
+ /* keywords */
+ { "KEYWORD", imap_search_keyword },
+ { "UNKEYWORD", imap_search_unkeyword },
+
+ /* dates */
+ { "BEFORE", imap_search_before },
+ { "ON", imap_search_on },
+ { "SINCE", imap_search_since },
+ { "SENTBEFORE", imap_search_sentbefore },
+ { "SENTON", imap_search_senton },
+ { "SENTSINCE", imap_search_sentsince },
+ { "SAVEDBEFORE", imap_search_savedbefore },
+ { "SAVEDON", imap_search_savedon },
+ { "SAVEDSINCE", imap_search_savedsince },
+ { "SAVEDATESUPPORTED", imap_search_savedatesupported },
+ /* FIXME: remove these in v2.4: */
+ { "X-SAVEDBEFORE", imap_search_x_savedbefore },
+ { "X-SAVEDON", imap_search_x_savedon },
+ { "X-SAVEDSINCE", imap_search_x_savedsince },
+
+ /* sizes */
+ { "LARGER", imap_search_larger },
+ { "SMALLER", imap_search_smaller },
+
+ /* headers */
+ { "BCC", imap_search_bcc },
+ { "CC", imap_search_cc },
+ { "FROM", imap_search_from },
+ { "TO", imap_search_to },
+ { "SUBJECT", imap_search_subject },
+ { "HEADER", imap_search_header },
+
+ /* body */
+ { "BODY", imap_search_body },
+ { "TEXT", imap_search_text },
+
+ /* WITHIN extension: */
+ { "OLDER", imap_search_older },
+ { "YOUNGER", imap_search_younger },
+
+ /* CONDSTORE extension: */
+ { "MODSEQ", imap_search_modseq },
+
+ /* SEARCHRES extension: */
+ { "$", imap_search_last_result },
+
+ /* FUZZY extension: */
+ { "FUZZY", imap_search_fuzzy },
+
+ /* SEARCH=MIMEPART extension: */
+ { "MIMEPART", imap_search_mimepart },
+
+ /* Other Dovecot extensions: */
+ { "INTHREAD", imap_search_inthread },
+ { "X-GUID", imap_search_x_guid },
+ { "X-MAILBOX", imap_search_x_mailbox },
+ { "X-REAL-UID", imap_search_x_real_uid }
+};
+
+static struct mail_search_register *mail_search_register_init_imap(void)
+{
+ struct mail_search_register *reg;
+
+ reg = mail_search_register_init();
+ mail_search_register_add(reg, imap_register_args,
+ N_ELEMENTS(imap_register_args));
+ mail_search_register_fallback(reg, imap_search_fallback);
+ return reg;
+}
+
+struct mail_search_register *
+mail_search_register_get_imap(void)
+{
+ if (mail_search_register_imap == NULL)
+ mail_search_register_imap = mail_search_register_init_imap();
+ return mail_search_register_imap;
+}
diff --git a/src/lib-storage/mail-search-register.c b/src/lib-storage/mail-search-register.c
new file mode 100644
index 0000000..b0f9098
--- /dev/null
+++ b/src/lib-storage/mail-search-register.c
@@ -0,0 +1,89 @@
+/* Copyright (c) 2010-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "mail-search.h"
+#include "mail-search-register.h"
+
+struct mail_search_register {
+ ARRAY(struct mail_search_register_arg) args;
+ mail_search_register_fallback_t *fallback;
+
+ bool args_sorted:1;
+};
+
+struct mail_search_register *mail_search_register_init(void)
+{
+ struct mail_search_register *reg;
+
+ reg = i_new(struct mail_search_register, 1);
+ i_array_init(&reg->args, 64);
+ return reg;
+}
+
+void mail_search_register_deinit(struct mail_search_register **_reg)
+{
+ struct mail_search_register *reg = *_reg;
+
+ *_reg = NULL;
+
+ array_free(&reg->args);
+ i_free(reg);
+}
+
+void mail_search_register_add(struct mail_search_register *reg,
+ const struct mail_search_register_arg *arg,
+ unsigned int count)
+{
+ array_append(&reg->args, arg, count);
+ reg->args_sorted = FALSE;
+}
+
+void mail_search_register_fallback(struct mail_search_register *reg,
+ mail_search_register_fallback_t *fallback)
+{
+ reg->fallback = fallback;
+}
+
+static int
+mail_search_register_arg_cmp(const struct mail_search_register_arg *arg1,
+ const struct mail_search_register_arg *arg2)
+{
+ return strcmp(arg1->key, arg2->key);
+}
+
+const struct mail_search_register_arg *
+mail_search_register_get(struct mail_search_register *reg,
+ unsigned int *count_r)
+{
+ if (!reg->args_sorted) {
+ array_sort(&reg->args, mail_search_register_arg_cmp);
+ reg->args_sorted = TRUE;
+ }
+
+ return array_get(&reg->args, count_r);
+}
+
+const struct mail_search_register_arg *
+mail_search_register_find(struct mail_search_register *reg, const char *key)
+{
+ struct mail_search_register_arg arg;
+
+ if (!reg->args_sorted) {
+ array_sort(&reg->args, mail_search_register_arg_cmp);
+ reg->args_sorted = TRUE;
+ }
+
+ arg.key = key;
+ return array_bsearch(&reg->args, &arg, mail_search_register_arg_cmp);
+}
+
+bool mail_search_register_get_fallback(struct mail_search_register *reg,
+ mail_search_register_fallback_t **fallback_r)
+{
+ if (reg->fallback == NULL)
+ return FALSE;
+
+ *fallback_r = reg->fallback;
+ return TRUE;
+}
diff --git a/src/lib-storage/mail-search-register.h b/src/lib-storage/mail-search-register.h
new file mode 100644
index 0000000..16e9427
--- /dev/null
+++ b/src/lib-storage/mail-search-register.h
@@ -0,0 +1,46 @@
+#ifndef MAIL_SEARCH_REGISTER_H
+#define MAIL_SEARCH_REGISTER_H
+
+struct mail_search_arg;
+struct mail_search_build_context;
+
+struct mail_search_register_arg {
+ const char *key;
+
+ /* returns parsed arg or NULL if error. error message is set to ctx. */
+ struct mail_search_arg *
+ (*build)(struct mail_search_build_context *ctx);
+};
+
+typedef struct mail_search_arg *
+mail_search_register_fallback_t(struct mail_search_build_context *ctx,
+ const char *key);
+
+struct mail_search_register *mail_search_register_init(void);
+void mail_search_register_deinit(struct mail_search_register **reg);
+
+void mail_search_register_add(struct mail_search_register *reg,
+ const struct mail_search_register_arg *arg,
+ unsigned int count);
+/* Register a fallback handler. It's responsible for giving also the
+ "unknown key" error. */
+void mail_search_register_fallback(struct mail_search_register *reg,
+ mail_search_register_fallback_t *fallback);
+
+/* Return all registered args sorted. */
+const struct mail_search_register_arg *
+mail_search_register_get(struct mail_search_register *reg,
+ unsigned int *count_r);
+
+/* Find key's registered arg, or NULL if not found. */
+const struct mail_search_register_arg *
+mail_search_register_find(struct mail_search_register *reg, const char *key);
+/* Get registered fallback arg. Returns FALSE if fallback hasn't been
+ registered. */
+bool mail_search_register_get_fallback(struct mail_search_register *reg,
+ mail_search_register_fallback_t **fallback_r);
+
+struct mail_search_register *mail_search_register_get_imap(void);
+struct mail_search_register *mail_search_register_get_human(void);
+
+#endif
diff --git a/src/lib-storage/mail-search.c b/src/lib-storage/mail-search.c
new file mode 100644
index 0000000..1b14b67
--- /dev/null
+++ b/src/lib-storage/mail-search.c
@@ -0,0 +1,806 @@
+/* Copyright (c) 2002-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "imap-match.h"
+#include "mail-index.h"
+#include "mail-storage.h"
+#include "mail-namespace.h"
+#include "mail-search-build.h"
+#include "mail-search.h"
+#include "mail-search-mime.h"
+
+static void
+mailbox_uidset_change(struct mail_search_arg *arg, struct mailbox *box,
+ const ARRAY_TYPE(seq_range) *search_saved_uidset)
+{
+ struct seq_range *uids;
+ unsigned int i, count;
+ uint32_t seq1, seq2;
+
+ if (arg->value.str != NULL && strcmp(arg->value.str, "$") == 0) {
+ /* SEARCHRES: Replace with saved uidset */
+ array_clear(&arg->value.seqset);
+ if (search_saved_uidset == NULL ||
+ !array_is_created(search_saved_uidset))
+ return;
+
+ array_append_array(&arg->value.seqset, search_saved_uidset);
+ return;
+ }
+
+ arg->type = SEARCH_SEQSET;
+
+ /* make a copy of the UIDs */
+ count = array_count(&arg->value.seqset);
+ if (count == 0) {
+ /* empty set, keep it */
+ return;
+ }
+ uids = t_new(struct seq_range, count);
+ memcpy(uids, array_front(&arg->value.seqset), sizeof(*uids) * count);
+
+ /* put them back to the range as sequences */
+ array_clear(&arg->value.seqset);
+ for (i = 0; i < count; i++) {
+ mailbox_get_seq_range(box, uids[i].seq1, uids[i].seq2,
+ &seq1, &seq2);
+ if (seq1 != 0) {
+ seq_range_array_add_range(&arg->value.seqset,
+ seq1, seq2);
+ }
+ if (uids[i].seq2 == (uint32_t)-1) {
+ /* make sure the last message is in the range */
+ mailbox_get_seq_range(box, 1, (uint32_t)-1,
+ &seq1, &seq2);
+ if (seq2 != 0)
+ seq_range_array_add(&arg->value.seqset, seq2);
+ }
+ }
+}
+
+static void
+mailbox_seqset_change(struct mail_search_arg *arg, struct mailbox *box)
+{
+ const struct seq_range *seqset;
+ unsigned int count;
+ uint32_t seq1, seq2;
+
+ seqset = array_get(&arg->value.seqset, &count);
+ if (count > 0 && seqset[count-1].seq2 == (uint32_t)-1) {
+ /* n:* -> n:maxseq. */
+ mailbox_get_seq_range(box, 1, (uint32_t)-1,
+ &seq1, &seq2);
+ if (seq2 == 0) {
+ /* no mails in mailbox - nothing can match */
+ array_clear(&arg->value.seqset);
+ } else if (seqset[count-1].seq1 == (uint32_t)-1) {
+ /* "*" alone needs a bit special handling
+ NOTE: This could be e.g. 5,* so use
+ seqset[last] */
+ seq_range_array_remove(&arg->value.seqset, (uint32_t)-1);
+ seq_range_array_add(&arg->value.seqset, seq2);
+ } else {
+ seq_range_array_remove_range(&arg->value.seqset,
+ seq2+1, (uint32_t)-1);
+ }
+ }
+}
+
+static void
+mail_search_arg_change_sets(struct mail_search_args *args,
+ struct mail_search_arg *arg,
+ const ARRAY_TYPE(seq_range) *search_saved_uidset)
+{
+ for (; arg != NULL; arg = arg->next) {
+ switch (arg->type) {
+ case SEARCH_SEQSET:
+ mailbox_seqset_change(arg, args->box);
+ break;
+ case SEARCH_UIDSET:
+ T_BEGIN {
+ mailbox_uidset_change(arg, args->box,
+ search_saved_uidset);
+ } T_END;
+ break;
+ case SEARCH_INTHREAD:
+ case SEARCH_SUB:
+ case SEARCH_OR:
+ mail_search_arg_change_sets(args, arg->value.subargs,
+ search_saved_uidset);
+ break;
+ default:
+ break;
+ }
+ }
+}
+
+void mail_search_arg_init(struct mail_search_args *args,
+ struct mail_search_arg *arg)
+{
+ struct mail_search_args *thread_args;
+ const char *keywords[2];
+
+ for (; arg != NULL; arg = arg->next) {
+ switch (arg->type) {
+ case SEARCH_MODSEQ:
+ if (arg->value.str == NULL)
+ break;
+ /* fall through - modseq with keyword */
+ case SEARCH_KEYWORDS:
+ keywords[0] = arg->value.str;
+ keywords[1] = NULL;
+
+ i_assert(arg->initialized.keywords == NULL);
+ arg->initialized.keywords =
+ mailbox_keywords_create_valid(args->box,
+ keywords);
+ break;
+
+ case SEARCH_MAILBOX_GLOB: {
+ struct mail_namespace *ns =
+ mailbox_get_namespace(args->box);
+
+ arg->initialized.mailbox_glob =
+ imap_match_init(default_pool, arg->value.str,
+ TRUE, mail_namespace_get_sep(ns));
+ break;
+ }
+ case SEARCH_INTHREAD:
+ thread_args = arg->initialized.search_args;
+ if (thread_args == NULL) {
+ arg->initialized.search_args = thread_args =
+ p_new(args->pool,
+ struct mail_search_args, 1);
+ thread_args->pool = args->pool;
+ thread_args->args = arg->value.subargs;
+ thread_args->simplified = TRUE;
+ thread_args->init_refcount = 1;
+ /* simplification should have unnested all
+ inthreads, so we'll assume that
+ have_inthreads=FALSE */
+ }
+ thread_args->refcount++;
+ thread_args->box = args->box;
+ /* fall through */
+ case SEARCH_SUB:
+ case SEARCH_OR:
+ mail_search_arg_init(args, arg->value.subargs);
+ break;
+ default:
+ break;
+ }
+ }
+}
+
+void mail_search_args_init(struct mail_search_args *args,
+ struct mailbox *box, bool change_sets,
+ const ARRAY_TYPE(seq_range) *search_saved_uidset)
+{
+ i_assert(args->init_refcount <= args->refcount);
+
+ if (args->init_refcount++ > 0) {
+ i_assert(args->box == box);
+ return;
+ }
+
+ args->box = box;
+ if (change_sets) {
+ /* Change seqsets/uidsets before simplifying the args, since it
+ can't handle search_saved_uidset. */
+ mail_search_arg_change_sets(args, args->args,
+ search_saved_uidset);
+ }
+ if (!args->simplified)
+ mail_search_args_simplify(args);
+ mail_search_arg_init(args, args->args);
+}
+
+void mail_search_arg_deinit(struct mail_search_arg *arg)
+{
+ for (; arg != NULL; arg = arg->next)
+ mail_search_arg_one_deinit(arg);
+}
+
+void mail_search_arg_one_deinit(struct mail_search_arg *arg)
+{
+ switch (arg->type) {
+ case SEARCH_MODSEQ:
+ case SEARCH_KEYWORDS:
+ if (arg->initialized.keywords == NULL)
+ break;
+ mailbox_keywords_unref(&arg->initialized.keywords);
+ break;
+ case SEARCH_MAILBOX_GLOB:
+ if (arg->initialized.mailbox_glob == NULL)
+ break;
+
+ imap_match_deinit(&arg->initialized.mailbox_glob);
+ break;
+ case SEARCH_INTHREAD:
+ i_assert(arg->initialized.search_args->refcount > 0);
+ if (arg->value.search_result != NULL)
+ mailbox_search_result_free(&arg->value.search_result);
+ arg->initialized.search_args->refcount--;
+ arg->initialized.search_args->box = NULL;
+ /* fall through */
+ case SEARCH_SUB:
+ case SEARCH_OR:
+ mail_search_arg_deinit(arg->value.subargs);
+ break;
+ default:
+ break;
+ }
+}
+
+void mail_search_args_deinit(struct mail_search_args *args)
+{
+ if (--args->init_refcount > 0)
+ return;
+
+ mail_search_arg_deinit(args->args);
+ args->box = NULL;
+}
+
+static void mail_search_args_seq2uid_sub(struct mail_search_args *args,
+ struct mail_search_arg *arg,
+ ARRAY_TYPE(seq_range) *uids)
+{
+ for (; arg != NULL; arg = arg->next) {
+ switch (arg->type) {
+ case SEARCH_SEQSET:
+ array_clear(uids);
+ mailbox_get_uid_range(args->box,
+ &arg->value.seqset, uids);
+
+ /* replace sequences with UIDs in the existing array.
+ this way it's possible to switch between uidsets and
+ seqsets constantly without leaking memory */
+ arg->type = SEARCH_UIDSET;
+ array_clear(&arg->value.seqset);
+ array_append_array(&arg->value.seqset, uids);
+ break;
+ case SEARCH_SUB:
+ case SEARCH_OR:
+ case SEARCH_INTHREAD:
+ mail_search_args_seq2uid_sub(args, arg->value.subargs,
+ uids);
+ break;
+ default:
+ break;
+ }
+ }
+}
+
+void mail_search_args_seq2uid(struct mail_search_args *args)
+{
+ T_BEGIN {
+ ARRAY_TYPE(seq_range) uids;
+
+ t_array_init(&uids, 128);
+ mail_search_args_seq2uid_sub(args, args->args, &uids);
+ } T_END;
+}
+
+void mail_search_args_ref(struct mail_search_args *args)
+{
+ i_assert(args->refcount > 0);
+
+ args->refcount++;
+}
+
+void mail_search_args_unref(struct mail_search_args **_args)
+{
+ struct mail_search_args *args = *_args;
+
+ i_assert(args->refcount > 0);
+
+ *_args = NULL;
+ if (--args->refcount > 0) {
+ i_assert(args->init_refcount <= args->refcount);
+ return;
+ }
+ i_assert(args->init_refcount <= 1);
+ if (args->init_refcount == 1)
+ mail_search_args_deinit(args);
+ pool_unref(&args->pool);
+}
+
+static struct mail_search_arg *
+mail_search_arg_dup_one(pool_t pool, const struct mail_search_arg *arg)
+{
+ struct mail_search_arg *new_arg;
+
+ new_arg = p_new(pool, struct mail_search_arg, 1);
+ new_arg->type = arg->type;
+ new_arg->match_not = arg->match_not;
+ new_arg->match_always = arg->match_always;
+ new_arg->nonmatch_always = arg->nonmatch_always;
+ new_arg->fuzzy = arg->fuzzy;
+ new_arg->value.search_flags = arg->value.search_flags;
+
+ switch (arg->type) {
+ case SEARCH_INTHREAD:
+ new_arg->value.thread_type = arg->value.thread_type;
+ /* fall through */
+ case SEARCH_OR:
+ case SEARCH_SUB:
+ new_arg->value.subargs =
+ mail_search_arg_dup(pool, arg->value.subargs);
+ break;
+ case SEARCH_ALL:
+ case SEARCH_SAVEDATESUPPORTED:
+ break;
+ case SEARCH_SEQSET:
+ case SEARCH_UIDSET:
+ case SEARCH_REAL_UID:
+ p_array_init(&new_arg->value.seqset, pool,
+ array_count(&arg->value.seqset));
+ array_append_array(&new_arg->value.seqset, &arg->value.seqset);
+ break;
+ case SEARCH_FLAGS:
+ new_arg->value.flags = arg->value.flags;
+ break;
+ case SEARCH_BEFORE:
+ case SEARCH_ON:
+ case SEARCH_SINCE:
+ new_arg->value.time = arg->value.time;
+ new_arg->value.date_type = arg->value.date_type;
+ break;
+ case SEARCH_SMALLER:
+ case SEARCH_LARGER:
+ new_arg->value.size = arg->value.size;
+ break;
+ case SEARCH_HEADER:
+ case SEARCH_HEADER_ADDRESS:
+ case SEARCH_HEADER_COMPRESS_LWSP:
+ new_arg->hdr_field_name = p_strdup(pool, arg->hdr_field_name);
+ /* fall through */
+ case SEARCH_KEYWORDS:
+ case SEARCH_BODY:
+ case SEARCH_TEXT:
+ case SEARCH_GUID:
+ case SEARCH_MAILBOX:
+ case SEARCH_MAILBOX_GUID:
+ case SEARCH_MAILBOX_GLOB:
+ new_arg->value.str = p_strdup(pool, arg->value.str);
+ break;
+ case SEARCH_MODSEQ:
+ new_arg->value.modseq =
+ p_new(pool, struct mail_search_modseq, 1);
+ *new_arg->value.modseq = *arg->value.modseq;
+ break;
+ case SEARCH_MIMEPART:
+ new_arg->value.mime_part =
+ mail_search_mime_part_dup(pool, arg->value.mime_part);
+ break;
+ }
+ return new_arg;
+}
+
+struct mail_search_arg *
+mail_search_arg_dup(pool_t pool, const struct mail_search_arg *arg)
+{
+ struct mail_search_arg *new_arg = NULL, **dest = &new_arg;
+
+ for (; arg != NULL; arg = arg->next) {
+ *dest = mail_search_arg_dup_one(pool, arg);
+ dest = &(*dest)->next;
+ }
+ return new_arg;
+}
+
+struct mail_search_args *
+mail_search_args_dup(const struct mail_search_args *args)
+{
+ struct mail_search_args *new_args;
+
+ new_args = mail_search_build_init();
+ new_args->simplified = args->simplified;
+ new_args->have_inthreads = args->have_inthreads;
+ new_args->args = mail_search_arg_dup(new_args->pool, args->args);
+ return new_args;
+}
+
+void mail_search_args_reset(struct mail_search_arg *args, bool full_reset)
+{
+ while (args != NULL) {
+ if (args->type == SEARCH_OR || args->type == SEARCH_SUB)
+ mail_search_args_reset(args->value.subargs, full_reset);
+
+ if (args->match_always) {
+ if (!full_reset)
+ args->result = 1;
+ else {
+ args->match_always = FALSE;
+ args->result = -1;
+ }
+ } else if (args->nonmatch_always) {
+ if (!full_reset)
+ args->result = 0;
+ else {
+ args->nonmatch_always = FALSE;
+ args->result = -1;
+ }
+ } else {
+ args->result = -1;
+ }
+
+ args = args->next;
+ }
+}
+
+static void search_arg_foreach(struct mail_search_arg *arg,
+ mail_search_foreach_callback_t *callback,
+ void *context)
+{
+ struct mail_search_arg *subarg;
+
+ if (arg->result != -1)
+ return;
+
+ if (arg->type == SEARCH_SUB) {
+ /* sublist of conditions */
+ i_assert(arg->value.subargs != NULL);
+
+ arg->result = 1;
+ subarg = arg->value.subargs;
+ while (subarg != NULL) {
+ if (subarg->result == -1)
+ search_arg_foreach(subarg, callback, context);
+
+ if (subarg->result == -1)
+ arg->result = -1;
+ else if (subarg->result == 0) {
+ /* didn't match */
+ arg->result = 0;
+ break;
+ }
+
+ subarg = subarg->next;
+ }
+ if (arg->match_not && arg->result != -1)
+ arg->result = arg->result > 0 ? 0 : 1;
+ } else if (arg->type == SEARCH_OR) {
+ /* OR-list of conditions */
+ i_assert(arg->value.subargs != NULL);
+
+ subarg = arg->value.subargs;
+ arg->result = 0;
+ while (subarg != NULL) {
+ if (subarg->result == -1)
+ search_arg_foreach(subarg, callback, context);
+
+ if (subarg->result == -1)
+ arg->result = -1;
+ else if (subarg->result > 0) {
+ /* matched */
+ arg->result = 1;
+ break;
+ }
+
+ subarg = subarg->next;
+ }
+ if (arg->match_not && arg->result != -1)
+ arg->result = arg->result > 0 ? 0 : 1;
+ } else {
+ /* just a single condition */
+ callback(arg, context);
+ }
+}
+
+#undef mail_search_args_foreach
+int mail_search_args_foreach(struct mail_search_arg *args,
+ mail_search_foreach_callback_t *callback,
+ void *context)
+{
+ int result;
+
+ result = 1;
+ for (; args != NULL; args = args->next) {
+ search_arg_foreach(args, callback, context);
+
+ if (args->result == 0) {
+ /* didn't match */
+ return 0;
+ }
+
+ if (args->result == -1)
+ result = -1;
+ }
+
+ return result;
+}
+
+static void
+search_arg_analyze(struct mail_search_arg *arg, buffer_t *headers,
+ bool *have_body, bool *have_text)
+{
+ static const char *date_hdr = "Date";
+ struct mail_search_arg *subarg;
+
+ if (arg->result != -1)
+ return;
+
+ switch (arg->type) {
+ case SEARCH_OR:
+ case SEARCH_SUB:
+ subarg = arg->value.subargs;
+ while (subarg != NULL) {
+ if (subarg->result == -1) {
+ search_arg_analyze(subarg, headers,
+ have_body, have_text);
+ }
+
+ subarg = subarg->next;
+ }
+ break;
+ case SEARCH_BEFORE:
+ case SEARCH_ON:
+ case SEARCH_SINCE:
+ if (arg->value.date_type == MAIL_SEARCH_DATE_TYPE_SENT)
+ buffer_append(headers, &date_hdr, sizeof(const char *));
+ break;
+ case SEARCH_HEADER:
+ case SEARCH_HEADER_ADDRESS:
+ case SEARCH_HEADER_COMPRESS_LWSP:
+ buffer_append(headers, &arg->hdr_field_name,
+ sizeof(const char *));
+ break;
+ case SEARCH_BODY:
+ *have_body = TRUE;
+ break;
+ case SEARCH_TEXT:
+ *have_text = TRUE;
+ *have_body = TRUE;
+ break;
+ default:
+ break;
+ }
+}
+
+const char *const *
+mail_search_args_analyze(struct mail_search_arg *args,
+ bool *have_headers, bool *have_body)
+{
+ const char *null = NULL;
+ buffer_t *headers;
+ bool have_text;
+
+ *have_headers = *have_body = have_text = FALSE;
+
+ headers = t_buffer_create(128);
+ for (; args != NULL; args = args->next)
+ search_arg_analyze(args, headers, have_body, &have_text);
+
+ *have_headers = have_text || headers->used != 0;
+
+ if (headers->used == 0)
+ return NULL;
+
+ buffer_append(headers, &null, sizeof(const char *));
+ return headers->data;
+}
+
+static bool
+mail_search_args_match_mailbox_arg(const struct mail_search_arg *arg,
+ const char *vname, char sep)
+{
+ const struct mail_search_arg *subarg;
+ bool ret;
+
+ switch (arg->type) {
+ case SEARCH_OR:
+ subarg = arg->value.subargs;
+ for (; subarg != NULL; subarg = subarg->next) {
+ if (mail_search_args_match_mailbox_arg(subarg,
+ vname, sep))
+ return TRUE;
+ }
+ return FALSE;
+ case SEARCH_SUB:
+ case SEARCH_INTHREAD:
+ subarg = arg->value.subargs;
+ for (; subarg != NULL; subarg = subarg->next) {
+ if (!mail_search_args_match_mailbox_arg(subarg,
+ vname, sep))
+ return FALSE;
+ }
+ return TRUE;
+ case SEARCH_MAILBOX:
+ ret = strcmp(arg->value.str, vname) == 0;
+ return ret != arg->match_not;
+ case SEARCH_MAILBOX_GLOB: {
+ T_BEGIN {
+ struct imap_match_glob *glob;
+
+ glob = imap_match_init(pool_datastack_create(),
+ arg->value.str, TRUE, sep);
+ ret = imap_match(glob, vname) == IMAP_MATCH_YES;
+ } T_END;
+ return ret != arg->match_not;
+ }
+ default:
+ break;
+ }
+ return TRUE;
+}
+
+bool mail_search_args_match_mailbox(struct mail_search_args *args,
+ const char *vname, char sep)
+{
+ const struct mail_search_arg *arg;
+
+ if (!args->simplified)
+ mail_search_args_simplify(args);
+
+ for (arg = args->args; arg != NULL; arg = arg->next) {
+ if (!mail_search_args_match_mailbox_arg(arg, vname, sep))
+ return FALSE;
+ }
+ return TRUE;
+}
+
+bool mail_search_arg_one_equals(const struct mail_search_arg *arg1,
+ const struct mail_search_arg *arg2)
+{
+ if (arg1->type != arg2->type ||
+ arg1->match_not != arg2->match_not ||
+ arg1->fuzzy != arg2->fuzzy ||
+ arg1->value.search_flags != arg2->value.search_flags)
+ return FALSE;
+
+ switch (arg1->type) {
+ case SEARCH_OR:
+ case SEARCH_SUB:
+ return mail_search_arg_equals(arg1->value.subargs,
+ arg2->value.subargs);
+
+ case SEARCH_ALL:
+ case SEARCH_SAVEDATESUPPORTED:
+ return TRUE;
+ case SEARCH_SEQSET:
+ /* sequences may point to different messages at different times,
+ never assume they match */
+ return FALSE;
+ case SEARCH_UIDSET:
+ return array_cmp(&arg1->value.seqset, &arg2->value.seqset);
+ case SEARCH_REAL_UID:
+ return array_cmp(&arg1->value.seqset, &arg2->value.seqset);
+
+ case SEARCH_FLAGS:
+ return arg1->value.flags == arg2->value.flags;
+ case SEARCH_KEYWORDS:
+ return strcasecmp(arg1->value.str, arg2->value.str) == 0;
+
+ case SEARCH_BEFORE:
+ case SEARCH_ON:
+ case SEARCH_SINCE:
+ return arg1->value.time == arg2->value.time &&
+ arg1->value.date_type == arg2->value.date_type;
+
+ case SEARCH_SMALLER:
+ case SEARCH_LARGER:
+ return arg1->value.size == arg2->value.size;
+
+ case SEARCH_HEADER:
+ case SEARCH_HEADER_ADDRESS:
+ case SEARCH_HEADER_COMPRESS_LWSP:
+ if (strcasecmp(arg1->hdr_field_name, arg2->hdr_field_name) != 0)
+ return FALSE;
+ /* fall through */
+ case SEARCH_BODY:
+ case SEARCH_TEXT:
+ case SEARCH_GUID:
+ case SEARCH_MAILBOX:
+ case SEARCH_MAILBOX_GUID:
+ case SEARCH_MAILBOX_GLOB:
+ /* don't bother doing case-insensitive comparison. it must not
+ be done for guid/mailbox, and for others we should support
+ full i18n case-insensitivity (or the active comparator
+ in future). */
+ return strcmp(arg1->value.str, arg2->value.str) == 0;
+
+ case SEARCH_MODSEQ: {
+ const struct mail_search_modseq *m1 = arg1->value.modseq;
+ const struct mail_search_modseq *m2 = arg2->value.modseq;
+
+ return m1->modseq == m2->modseq &&
+ m1->type == m2->type;
+ }
+ case SEARCH_INTHREAD:
+ if (arg1->value.thread_type != arg2->value.thread_type)
+ return FALSE;
+ return mail_search_arg_equals(arg1->value.subargs,
+ arg2->value.subargs);
+ case SEARCH_MIMEPART:
+ return mail_search_mime_parts_equal(arg1->value.mime_part,
+ arg2->value.mime_part);
+
+ }
+ i_unreached();
+}
+
+bool mail_search_arg_equals(const struct mail_search_arg *arg1,
+ const struct mail_search_arg *arg2)
+{
+ while (arg1 != NULL && arg2 != NULL) {
+ if (!mail_search_arg_one_equals(arg1, arg2))
+ return FALSE;
+ arg1 = arg1->next;
+ arg2 = arg2->next;
+ }
+ return arg1 == NULL && arg2 == NULL;
+}
+
+bool mail_search_args_equal(const struct mail_search_args *args1,
+ const struct mail_search_args *args2)
+{
+ i_assert(args1->simplified == args2->simplified);
+ i_assert(args1->box == args2->box);
+
+ return mail_search_arg_equals(args1->args, args2->args);
+}
+
+static void
+mail_search_args_result_serialize_arg(const struct mail_search_arg *arg,
+ buffer_t *dest)
+{
+ const struct mail_search_arg *subarg;
+
+ buffer_append_c(dest, arg->result < 0 ? 0xff : arg->result);
+
+ switch (arg->type) {
+ case SEARCH_OR:
+ case SEARCH_SUB:
+ case SEARCH_INTHREAD:
+ subarg = arg->value.subargs;
+ for (; subarg != NULL; subarg = subarg->next)
+ mail_search_args_result_serialize_arg(subarg, dest);
+ default:
+ break;
+ }
+}
+
+void mail_search_args_result_serialize(const struct mail_search_args *args,
+ buffer_t *dest)
+{
+ const struct mail_search_arg *arg;
+
+ for (arg = args->args; arg != NULL; arg = arg->next)
+ mail_search_args_result_serialize_arg(arg, dest);
+}
+
+static void
+mail_search_args_result_deserialize_arg(struct mail_search_arg *arg,
+ const unsigned char **data,
+ size_t *size)
+{
+ struct mail_search_arg *subarg;
+
+ i_assert(*size > 0);
+ arg->result = **data == 0xff ? -1 : **data;
+ *data += 1; *size -= 1;
+
+ switch (arg->type) {
+ case SEARCH_OR:
+ case SEARCH_SUB:
+ case SEARCH_INTHREAD:
+ subarg = arg->value.subargs;
+ for (; subarg != NULL; subarg = subarg->next) {
+ mail_search_args_result_deserialize_arg(subarg,
+ data, size);
+ }
+ default:
+ break;
+ }
+}
+
+void mail_search_args_result_deserialize(struct mail_search_args *args,
+ const unsigned char *data, size_t size)
+{
+ struct mail_search_arg *arg;
+
+ for (arg = args->args; arg != NULL; arg = arg->next)
+ mail_search_args_result_deserialize_arg(arg, &data, &size);
+}
diff --git a/src/lib-storage/mail-search.h b/src/lib-storage/mail-search.h
new file mode 100644
index 0000000..2147175
--- /dev/null
+++ b/src/lib-storage/mail-search.h
@@ -0,0 +1,272 @@
+#ifndef MAIL_SEARCH_H
+#define MAIL_SEARCH_H
+
+#include "seq-range-array.h"
+#include "mail-types.h"
+#include "mail-thread.h"
+
+struct mail_search_mime_part;
+
+enum mail_search_arg_type {
+ SEARCH_OR,
+ SEARCH_SUB,
+
+ /* sequence sets */
+ SEARCH_ALL,
+ SEARCH_SEQSET,
+ SEARCH_UIDSET,
+
+ /* flags */
+ SEARCH_FLAGS,
+ SEARCH_KEYWORDS,
+
+ /* dates (date_type required) */
+ SEARCH_BEFORE,
+ SEARCH_ON, /* time must point to beginning of the day */
+ SEARCH_SINCE,
+
+ /* sizes */
+ SEARCH_SMALLER,
+ SEARCH_LARGER,
+
+ /* headers */
+ SEARCH_HEADER,
+ SEARCH_HEADER_ADDRESS,
+ SEARCH_HEADER_COMPRESS_LWSP,
+
+ /* body */
+ SEARCH_BODY,
+ SEARCH_TEXT,
+
+ /* extensions */
+ SEARCH_MODSEQ,
+ SEARCH_SAVEDATESUPPORTED,
+ SEARCH_INTHREAD,
+ SEARCH_GUID,
+ SEARCH_MAILBOX,
+ SEARCH_MAILBOX_GUID,
+ SEARCH_MAILBOX_GLOB,
+ SEARCH_REAL_UID,
+ SEARCH_MIMEPART
+};
+
+enum mail_search_date_type {
+ MAIL_SEARCH_DATE_TYPE_SENT = 1,
+ MAIL_SEARCH_DATE_TYPE_RECEIVED,
+ MAIL_SEARCH_DATE_TYPE_SAVED
+};
+
+enum mail_search_arg_flag {
+ /* Used by *BEFORE/SINCE/ON searches.
+
+ When NOT set: Adjust search timestamps so that the email's timezone
+ is included in the comparisons. For example
+ "04-Nov-2016 00:00:00 +0200" would match 4th day. This allows
+ searching for mails with dates from the email sender's point of
+ view. For received/saved dates there is no known timezone, and
+ without this flag the dates are compared using the server's local
+ timezone.
+
+ When set: Compare the timestamp as UTC. For example
+ "04-Nov-2016 00:00:00 +0200" would be treated as
+ "03-Nov-2016 22:00:00 UTC" and would match 3rd day. This allows
+ searching for mails within precise time interval. Since imap-dates
+ don't allow specifying timezone this isn't really possible with IMAP
+ protocol, except using OLDER/YOUNGER searches. */
+ MAIL_SEARCH_ARG_FLAG_UTC_TIMES = 0x01,
+};
+
+enum mail_search_modseq_type {
+ MAIL_SEARCH_MODSEQ_TYPE_ANY = 0,
+ MAIL_SEARCH_MODSEQ_TYPE_PRIVATE,
+ MAIL_SEARCH_MODSEQ_TYPE_SHARED
+};
+
+struct mail_search_modseq {
+ uint64_t modseq;
+ enum mail_search_modseq_type type;
+};
+
+struct mail_search_arg {
+ /* NOTE: when adding new fields, make sure mail_search_arg_dup_one()
+ and mail_search_arg_one_equals() are updated. */
+ struct mail_search_arg *next;
+
+ enum mail_search_arg_type type;
+ struct {
+ struct mail_search_arg *subargs;
+ ARRAY_TYPE(seq_range) seqset;
+ const char *str;
+ time_t time;
+ uoff_t size;
+ enum mail_flags flags;
+ enum mail_search_arg_flag search_flags;
+ enum mail_search_date_type date_type;
+ enum mail_thread_type thread_type;
+ struct mail_search_modseq *modseq;
+ struct mail_search_result *search_result;
+ struct mail_search_mime_part *mime_part;
+ } value;
+ /* set by mail_search_args_init(): */
+ struct {
+ struct mail_search_args *search_args;
+ /* Note that initialized keywords may be empty if the keyword
+ wasn't valid in this mailbox. */
+ struct mail_keywords *keywords;
+ struct imap_match_glob *mailbox_glob;
+ } initialized;
+
+ void *context;
+ const char *hdr_field_name; /* for SEARCH_HEADER* */
+ bool match_not:1; /* result = !result */
+ bool match_always:1; /* result = 1 always */
+ bool nonmatch_always:1; /* result = 0 always */
+ bool fuzzy:1; /* use fuzzy matching for this arg */
+ bool no_fts:1; /* do NOT call FTS */
+
+ int result; /* -1 = unknown, 0 = unmatched, 1 = matched */
+};
+
+struct mail_search_args {
+ /* There are two types of refcount:
+
+ 1) The normal refcount tracks the lifetime of the struct itself.
+ This allows using the same args for multiple search queries, even
+ across different mailboxes.
+
+ 2) The init_refcount tracks how many times mail_search_args_init() has
+ been called. This can happen when the same mail_search_args have been
+ shared by referencing them in different parts of the code. Only after
+ each one of them has called mail_search_args_deinit() the init_refcount
+ drops to 0 and it can really be deinitialized.
+
+ Note that all of the inits must be within the same mailbox - attempting
+ to init the same args in different mailboxes at the same time will
+ result in assert-crash. */
+ int refcount, init_refcount;
+
+ pool_t pool;
+ struct mailbox *box;
+ struct mail_search_arg *args;
+
+ bool simplified:1;
+ bool have_inthreads:1;
+ /* Stop mail_search_next() when finding a non-matching mail.
+ (Could be useful when wanting to find only the oldest mails.) */
+ bool stop_on_nonmatch:1;
+ /* fts plugin has already expanded the search args - no need to do
+ it again. */
+ bool fts_expanded:1;
+};
+
+#define ARG_SET_RESULT(arg, res) \
+ STMT_START { \
+ (arg)->result = !(arg)->match_not ? (res) : \
+ ((res) == -1 ? -1 : ((res) == 0 ? 1 : 0)); \
+ } STMT_END
+
+typedef void mail_search_foreach_callback_t(struct mail_search_arg *arg,
+ void *context);
+
+/* Fully initialize and optimize the args for searching within the specified
+ mailbox. This should always be called before the args are actually used
+ for searching. After search is finished, the args must be deinitialized.
+ It's possible to initialize the same args multiple times, as long as it's
+ done within the same mailbox. This would allow multiple concurrent searches
+ to be done within the shared search args.
+
+ This will implicitly call mail_search_args_simplify() if it wasn't called
+ yet. It also allocates any necessary per-mailbox data like keywords.
+
+ If change_sets is TRUE, change uidsets to seqsets and convert "*" in seqsets
+ to the current highest message sequence. */
+void mail_search_args_init(struct mail_search_args *args,
+ struct mailbox *box, bool change_sets,
+ const ARRAY_TYPE(seq_range) *search_saved_uidset)
+ ATTR_NULL(4);
+/* Initialize arg and its children. args is used for getting mailbox and
+ pool. */
+void mail_search_arg_init(struct mail_search_args *args,
+ struct mail_search_arg *arg);
+/* Free memory allocated by mail_search_args_init(). The args can initialized
+ afterwards again if needed. The args can be reused for other queries after
+ calling this. */
+void mail_search_args_deinit(struct mail_search_args *args);
+/* Free arg and its siblings and children. */
+void mail_search_arg_deinit(struct mail_search_arg *arg);
+/* Free arg and its children, but not its siblings. */
+void mail_search_arg_one_deinit(struct mail_search_arg *arg);
+/* Convert sequence sets in args to UIDs. */
+void mail_search_args_seq2uid(struct mail_search_args *args);
+/* Returns TRUE if the two search arguments are fully compatible.
+ Always returns FALSE if there are seqsets, since they may point to different
+ messages depending on when the search is run. */
+bool mail_search_args_equal(const struct mail_search_args *args1,
+ const struct mail_search_args *args2);
+/* Same as mail_search_args_equal(), but for individual mail_search_arg
+ structs. All the siblings of arg1 and arg2 are also compared. */
+bool mail_search_arg_equals(const struct mail_search_arg *arg1,
+ const struct mail_search_arg *arg2);
+/* Same as mail_search_arg_equals(), but don't compare siblings. */
+bool mail_search_arg_one_equals(const struct mail_search_arg *arg1,
+ const struct mail_search_arg *arg2);
+
+void mail_search_args_ref(struct mail_search_args *args);
+void mail_search_args_unref(struct mail_search_args **args);
+
+struct mail_search_args *
+mail_search_args_dup(const struct mail_search_args *args);
+struct mail_search_arg *
+mail_search_arg_dup(pool_t pool, const struct mail_search_arg *arg);
+
+/* Reset the results in search arguments. match_always is reset only if
+ full_reset is TRUE. */
+void mail_search_args_reset(struct mail_search_arg *args, bool full_reset);
+
+/* goes through arguments in list that don't have a result yet.
+ Returns 1 = search matched, 0 = search unmatched, -1 = don't know yet */
+int mail_search_args_foreach(struct mail_search_arg *args,
+ mail_search_foreach_callback_t *callback,
+ void *context) ATTR_NULL(3);
+#define mail_search_args_foreach(args, callback, context) \
+ mail_search_args_foreach(args - \
+ CALLBACK_TYPECHECK(callback, void (*)( \
+ struct mail_search_arg *, typeof(context))), \
+ (mail_search_foreach_callback_t *)callback, context)
+
+/* Fills have_headers and have_body based on if such search argument exists
+ that needs to be checked. Returns the headers that we're searching for, or
+ NULL if we're searching for TEXT. */
+const char *const *
+mail_search_args_analyze(struct mail_search_arg *args,
+ bool *have_headers, bool *have_body);
+
+/* Returns FALSE if search query contains MAILBOX[_GLOB] args such that the
+ query can never match any messages in the given mailbox. */
+bool mail_search_args_match_mailbox(struct mail_search_args *args,
+ const char *vname, char sep);
+
+/* Simplify/optimize search arguments. Afterwards all OR/SUB args are
+ guaranteed to have match_not=FALSE. */
+void mail_search_args_simplify(struct mail_search_args *args);
+
+/* Append all args as IMAP SEARCH AND-query to the dest string and returns TRUE.
+ If some search arg can't be written as IMAP SEARCH parameter, error_r is set
+ and FALSE is returned. */
+bool mail_search_args_to_imap(string_t *dest, const struct mail_search_arg *args,
+ const char **error_r);
+/* Like mail_search_args_to_imap(), but append only a single arg. */
+bool mail_search_arg_to_imap(string_t *dest, const struct mail_search_arg *arg,
+ const char **error_r);
+/* Write all args to dest string as cmdline/human compatible input. */
+void mail_search_args_to_cmdline(string_t *dest,
+ const struct mail_search_arg *args);
+
+/* Serialization for search args' results. */
+void mail_search_args_result_serialize(const struct mail_search_args *args,
+ buffer_t *dest);
+void mail_search_args_result_deserialize(struct mail_search_args *args,
+ const unsigned char *data,
+ size_t size);
+
+#endif
diff --git a/src/lib-storage/mail-storage-hooks.c b/src/lib-storage/mail-storage-hooks.c
new file mode 100644
index 0000000..d368bd7
--- /dev/null
+++ b/src/lib-storage/mail-storage-hooks.c
@@ -0,0 +1,291 @@
+/* Copyright (c) 2009-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "hook-build.h"
+#include "llist.h"
+#include "module-dir.h"
+#include "mail-user.h"
+#include "mail-namespace.h"
+#include "mail-storage-private.h"
+#include "mailbox-list-private.h"
+
+struct mail_storage_module_hooks {
+ struct module *module;
+ const struct mail_storage_hooks *hooks;
+ bool forced;
+};
+
+static ARRAY(struct mail_storage_module_hooks) module_hooks = ARRAY_INIT;
+static ARRAY(const struct mail_storage_hooks *) internal_hooks = ARRAY_INIT;
+
+void mail_storage_hooks_init(void)
+{
+ if (!array_is_created(&module_hooks))
+ i_array_init(&module_hooks, 32);
+ i_array_init(&internal_hooks, 8);
+}
+
+void mail_storage_hooks_deinit(void)
+{
+ /* allow calling this even if mail_storage_hooks_init() hasn't been
+ called, because e.g. doveadm plugins could call
+ mail_storage_hooks_add() even though mail storage is never
+ initialized. */
+ if (array_is_created(&internal_hooks))
+ array_free(&internal_hooks);
+ if (array_is_created(&module_hooks))
+ array_free(&module_hooks);
+}
+
+void mail_storage_hooks_add(struct module *module,
+ const struct mail_storage_hooks *hooks)
+{
+ struct mail_storage_module_hooks new_hook;
+
+ i_zero(&new_hook);
+ new_hook.module = module;
+ new_hook.hooks = hooks;
+
+ /* allow adding hooks before mail_storage_hooks_init() */
+ if (!array_is_created(&module_hooks))
+ i_array_init(&module_hooks, 32);
+ array_push_back(&module_hooks, &new_hook);
+}
+
+void mail_storage_hooks_add_forced(struct module *module,
+ const struct mail_storage_hooks *hooks)
+{
+ struct mail_storage_module_hooks *hook;
+
+ mail_storage_hooks_add(module, hooks);
+ hook = array_back_modifiable(&module_hooks);
+ hook->forced = TRUE;
+}
+
+void mail_storage_hooks_remove(const struct mail_storage_hooks *hooks)
+{
+ const struct mail_storage_module_hooks *module_hook;
+ unsigned int idx = UINT_MAX;
+
+ array_foreach(&module_hooks, module_hook) {
+ if (module_hook->hooks == hooks) {
+ idx = array_foreach_idx(&module_hooks, module_hook);
+ break;
+ }
+ }
+ i_assert(idx != UINT_MAX);
+
+ array_delete(&module_hooks, idx, 1);
+}
+
+void mail_storage_hooks_add_internal(const struct mail_storage_hooks *hooks)
+{
+ const struct mail_storage_hooks *existing_hooks;
+
+ /* make sure we don't add duplicate hooks */
+ array_foreach_elem(&internal_hooks, existing_hooks)
+ i_assert(existing_hooks != hooks);
+ array_push_back(&internal_hooks, &hooks);
+}
+
+void mail_storage_hooks_remove_internal(const struct mail_storage_hooks *hooks)
+{
+ const struct mail_storage_hooks *const *old_hooks;
+ unsigned int idx = UINT_MAX;
+
+ array_foreach(&internal_hooks, old_hooks) {
+ if (*old_hooks == hooks) {
+ idx = array_foreach_idx(&internal_hooks, old_hooks);
+ break;
+ }
+ }
+ i_assert(idx != UINT_MAX);
+
+ array_delete(&internal_hooks, idx, 1);
+}
+
+static int
+mail_storage_module_hooks_cmp(const struct mail_storage_module_hooks *h1,
+ const struct mail_storage_module_hooks *h2)
+{
+ const char *s1 = h1->module->path, *s2 = h2->module->path;
+ const char *p;
+
+ p = strrchr(s1, '/');
+ if (p != NULL) s1 = p+1;
+ p = strrchr(s2, '/');
+ if (p != NULL) s2 = p+1;
+
+ if (str_begins(s1, "lib"))
+ s1 += 3;
+ if (str_begins(s2, "lib"))
+ s2 += 3;
+
+ return strcmp(s1, s2);
+}
+
+static void mail_user_add_plugin_hooks(struct mail_user *user)
+{
+ const struct mail_storage_module_hooks *module_hook;
+ ARRAY(struct mail_storage_module_hooks) tmp_hooks;
+ const char *const *plugins, *name;
+
+ /* first get all hooks wanted by the user */
+ t_array_init(&tmp_hooks, array_count(&module_hooks));
+ plugins = t_strsplit_spaces(user->set->mail_plugins, ", ");
+ array_foreach(&module_hooks, module_hook) {
+ if (!module_hook->forced) {
+ name = module_get_plugin_name(module_hook->module);
+ if (!str_array_find(plugins, name))
+ continue;
+ }
+ array_push_back(&tmp_hooks, module_hook);
+ }
+
+ /* next we have to sort them by the modules' priority (based on name) */
+ array_sort(&tmp_hooks, mail_storage_module_hooks_cmp);
+
+ /* now that we have them in order, save them to user's hooks */
+ p_array_init(&user->hooks, user->pool,
+ array_count(&tmp_hooks) + array_count(&internal_hooks));
+ array_foreach(&tmp_hooks, module_hook)
+ array_push_back(&user->hooks, &module_hook->hooks);
+ array_append_array(&user->hooks, &internal_hooks);
+}
+
+void hook_mail_user_created(struct mail_user *user)
+{
+ const struct mail_storage_hooks *hooks;
+ struct hook_build_context *ctx;
+
+ mail_user_add_plugin_hooks(user);
+
+ ctx = hook_build_init((void *)&user->v, sizeof(user->v));
+ user->vlast = &user->v;
+ array_foreach_elem(&user->hooks, hooks) {
+ if (hooks->mail_user_created != NULL) T_BEGIN {
+ hooks->mail_user_created(user);
+ hook_build_update(ctx, user->vlast);
+ } T_END;
+ }
+ user->vlast = NULL;
+ hook_build_deinit(&ctx);
+}
+
+void hook_mail_namespace_storage_added(struct mail_namespace *ns)
+{
+ const struct mail_storage_hooks *hooks;
+
+ array_foreach_elem(&ns->user->hooks, hooks) {
+ if (hooks->mail_namespace_storage_added != NULL) T_BEGIN {
+ hooks->mail_namespace_storage_added(ns);
+ } T_END;
+ }
+}
+
+void hook_mail_namespaces_created(struct mail_namespace *namespaces)
+{
+ const struct mail_storage_hooks *hooks;
+
+ array_foreach_elem(&namespaces->user->hooks, hooks) {
+ if (namespaces->user->error != NULL)
+ break;
+ if (hooks->mail_namespaces_created != NULL) T_BEGIN {
+ hooks->mail_namespaces_created(namespaces);
+ } T_END;
+ }
+}
+
+void hook_mail_namespaces_added(struct mail_namespace *namespaces)
+{
+ const struct mail_storage_hooks *hooks;
+
+ array_foreach_elem(&namespaces->user->hooks, hooks) {
+ if (namespaces->user->error != NULL)
+ break;
+ if (hooks->mail_namespaces_added != NULL) T_BEGIN {
+ hooks->mail_namespaces_added(namespaces);
+ } T_END;
+ }
+}
+
+void hook_mail_storage_created(struct mail_storage *storage)
+{
+ const struct mail_storage_hooks *hooks;
+ struct hook_build_context *ctx;
+
+ ctx = hook_build_init((void *)&storage->v, sizeof(storage->v));
+ storage->vlast = &storage->v;
+ array_foreach_elem(&storage->user->hooks, hooks) {
+ if (hooks->mail_storage_created != NULL) T_BEGIN {
+ hooks->mail_storage_created(storage);
+ hook_build_update(ctx, storage->vlast);
+ } T_END;
+ }
+ storage->vlast = NULL;
+ hook_build_deinit(&ctx);
+}
+
+void hook_mailbox_list_created(struct mailbox_list *list)
+{
+ const struct mail_storage_hooks *hooks;
+ struct hook_build_context *ctx;
+
+ ctx = hook_build_init((void *)&list->v, sizeof(list->v));
+ list->vlast = &list->v;
+ array_foreach_elem(&list->ns->user->hooks, hooks) {
+ if (hooks->mailbox_list_created != NULL) T_BEGIN {
+ hooks->mailbox_list_created(list);
+ hook_build_update(ctx, list->vlast);
+ } T_END;
+ }
+ list->vlast = NULL;
+ hook_build_deinit(&ctx);
+}
+
+void hook_mailbox_allocated(struct mailbox *box)
+{
+ const struct mail_storage_hooks *hooks;
+ struct hook_build_context *ctx;
+
+ ctx = hook_build_init((void *)&box->v, sizeof(box->v));
+ box->vlast = &box->v;
+ array_foreach_elem(&box->storage->user->hooks, hooks) {
+ if (hooks->mailbox_allocated != NULL) T_BEGIN {
+ hooks->mailbox_allocated(box);
+ hook_build_update(ctx, box->vlast);
+ } T_END;
+ }
+ box->vlast = NULL;
+ hook_build_deinit(&ctx);
+}
+
+void hook_mailbox_opened(struct mailbox *box)
+{
+ const struct mail_storage_hooks *hooks;
+
+ array_foreach_elem(&box->storage->user->hooks, hooks) {
+ if (hooks->mailbox_opened != NULL) T_BEGIN {
+ hooks->mailbox_opened(box);
+ } T_END;
+ }
+}
+
+void hook_mail_allocated(struct mail *mail)
+{
+ const struct mail_storage_hooks *hooks;
+ struct mail_private *pmail = (struct mail_private *)mail;
+ struct hook_build_context *ctx;
+
+ ctx = hook_build_init((void *)&pmail->v, sizeof(pmail->v));
+ pmail->vlast = &pmail->v;
+ array_foreach_elem(&mail->box->storage->user->hooks, hooks) {
+ if (hooks->mail_allocated != NULL) T_BEGIN {
+ hooks->mail_allocated(mail);
+ hook_build_update(ctx, pmail->vlast);
+ } T_END;
+ }
+ pmail->vlast = NULL;
+ hook_build_deinit(&ctx);
+}
diff --git a/src/lib-storage/mail-storage-hooks.h b/src/lib-storage/mail-storage-hooks.h
new file mode 100644
index 0000000..beb3fa2
--- /dev/null
+++ b/src/lib-storage/mail-storage-hooks.h
@@ -0,0 +1,53 @@
+#ifndef MAIL_STORAGE_HOOKS_H
+#define MAIL_STORAGE_HOOKS_H
+
+struct module;
+struct mail_user;
+struct mail_storage;
+struct mail_namespace;
+struct mailbox_list;
+struct mailbox;
+struct mail;
+
+struct mail_storage_hooks {
+ void (*mail_user_created)(struct mail_user *user);
+ void (*mail_namespace_storage_added)(struct mail_namespace *ns);
+ /* called the first time user's initial namespaces were added */
+ void (*mail_namespaces_created)(struct mail_namespace *namespaces);
+ /* called every time namespaces are added. most importantly called
+ when shared mailbox accesses trigger creating new namespaces.
+ this is called before mail_namespaces_created() at startup.
+ The namespaces parameter contains all of the current namespaces. */
+ void (*mail_namespaces_added)(struct mail_namespace *namespaces);
+ void (*mail_storage_created)(struct mail_storage *storage);
+ void (*mailbox_list_created)(struct mailbox_list *list);
+ void (*mailbox_allocated)(struct mailbox *box);
+ void (*mailbox_opened)(struct mailbox *box);
+ void (*mail_allocated)(struct mail *mail);
+};
+
+void mail_storage_hooks_init(void);
+void mail_storage_hooks_deinit(void);
+
+void mail_storage_hooks_add(struct module *module,
+ const struct mail_storage_hooks *hooks);
+/* Add hooks to this plugin regardless of whether it exists in user's
+ mail_plugins setting. */
+void mail_storage_hooks_add_forced(struct module *module,
+ const struct mail_storage_hooks *hooks);
+void mail_storage_hooks_remove(const struct mail_storage_hooks *hooks);
+
+void mail_storage_hooks_add_internal(const struct mail_storage_hooks *hooks);
+void mail_storage_hooks_remove_internal(const struct mail_storage_hooks *hooks);
+
+void hook_mail_user_created(struct mail_user *user);
+void hook_mail_namespace_storage_added(struct mail_namespace *ns);
+void hook_mail_namespaces_created(struct mail_namespace *namespaces);
+void hook_mail_namespaces_added(struct mail_namespace *namespaces);
+void hook_mail_storage_created(struct mail_storage *storage);
+void hook_mailbox_list_created(struct mailbox_list *list);
+void hook_mailbox_allocated(struct mailbox *box);
+void hook_mailbox_opened(struct mailbox *box);
+void hook_mail_allocated(struct mail *mail);
+
+#endif
diff --git a/src/lib-storage/mail-storage-lua-private.h b/src/lib-storage/mail-storage-lua-private.h
new file mode 100644
index 0000000..3ac169e
--- /dev/null
+++ b/src/lib-storage/mail-storage-lua-private.h
@@ -0,0 +1,31 @@
+#ifndef MAIL_STORAGE_LUA_PRIVATE_H
+#define MAIL_STORAGE_LUA_PRIVATE_H 1
+
+#define DLUA_MAILBOX_EQUALS(a, b) \
+ mailbox_equals((a), mailbox_get_namespace(b), mailbox_get_vname(b))
+
+struct lua_storage_keyvalue {
+ const char *key;
+ const char *value;
+ size_t value_len;
+};
+
+ARRAY_DEFINE_TYPE(lua_storage_keyvalue, struct lua_storage_keyvalue);
+
+void lua_storage_mail_register(struct dlua_script *script);
+void lua_storage_mail_user_register(struct dlua_script *script);
+void lua_storage_mailbox_register(struct dlua_script *script);
+
+int lua_storage_cmp(lua_State *L);
+
+int lua_storage_mailbox_attribute_get(struct mailbox *box, const char *key,
+ const char **value_r, size_t *value_len_r,
+ const char **error_r);
+int lua_storage_mailbox_attribute_set(struct mailbox *box, const char *key,
+ const char *value, size_t value_len,
+ const char **error_r);
+int lua_storage_mailbox_attribute_list(struct mailbox *box, const char *prefix,
+ ARRAY_TYPE(lua_storage_keyvalue) *items_r,
+ const char **error_r);
+
+#endif
diff --git a/src/lib-storage/mail-storage-lua.c b/src/lib-storage/mail-storage-lua.c
new file mode 100644
index 0000000..02808bd
--- /dev/null
+++ b/src/lib-storage/mail-storage-lua.c
@@ -0,0 +1,91 @@
+/* Copyright (c) 2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "str.h"
+#include "istream.h"
+#include "array.h"
+#include "var-expand.h"
+#include "dlua-script.h"
+#include "dlua-script-private.h"
+#include "mail-storage.h"
+#include "mailbox-attribute.h"
+#include "mail-storage-lua.h"
+#include "mail-storage-lua-private.h"
+#include "mail-user.h"
+
+#define LUA_SCRIPT_STORAGE "storage"
+
+static struct dlua_table_values lua_storage_mail_storage_flags[] = {
+ DLUA_TABLE_ENUM(STATUS_MESSAGES),
+ DLUA_TABLE_ENUM(STATUS_RECENT),
+ DLUA_TABLE_ENUM(STATUS_UIDNEXT),
+ DLUA_TABLE_ENUM(STATUS_UIDVALIDITY),
+ DLUA_TABLE_ENUM(STATUS_UNSEEN),
+ DLUA_TABLE_ENUM(STATUS_FIRST_UNSEEN_SEQ),
+ DLUA_TABLE_ENUM(STATUS_KEYWORDS),
+ DLUA_TABLE_ENUM(STATUS_HIGHESTMODSEQ),
+ DLUA_TABLE_ENUM(STATUS_PERMANENT_FLAGS),
+ DLUA_TABLE_ENUM(STATUS_FIRST_RECENT_UID),
+ DLUA_TABLE_ENUM(STATUS_HIGHESTPVTMODSEQ),
+
+ DLUA_TABLE_ENUM(MAILBOX_FLAG_READONLY),
+ DLUA_TABLE_ENUM(MAILBOX_FLAG_SAVEONLY),
+ DLUA_TABLE_ENUM(MAILBOX_FLAG_DROP_RECENT),
+ DLUA_TABLE_ENUM(MAILBOX_FLAG_NO_INDEX_FILES),
+ DLUA_TABLE_ENUM(MAILBOX_FLAG_KEEP_LOCKED),
+ DLUA_TABLE_ENUM(MAILBOX_FLAG_IGNORE_ACLS),
+ DLUA_TABLE_ENUM(MAILBOX_FLAG_AUTO_CREATE),
+ DLUA_TABLE_ENUM(MAILBOX_FLAG_AUTO_SUBSCRIBE),
+
+ DLUA_TABLE_ENUM(MAILBOX_SYNC_FLAG_FULL_READ),
+ DLUA_TABLE_ENUM(MAILBOX_SYNC_FLAG_FULL_WRITE),
+ DLUA_TABLE_ENUM(MAILBOX_SYNC_FLAG_FAST),
+ DLUA_TABLE_ENUM(MAILBOX_SYNC_FLAG_NO_EXPUNGES),
+ DLUA_TABLE_ENUM(MAILBOX_SYNC_FLAG_FIX_INCONSISTENT),
+ DLUA_TABLE_ENUM(MAILBOX_SYNC_FLAG_EXPUNGE),
+ DLUA_TABLE_ENUM(MAILBOX_SYNC_FLAG_FORCE_RESYNC),
+
+ DLUA_TABLE_STRING("MAILBOX_ATTRIBUTE_PREFIX_DOVECOT",
+ MAILBOX_ATTRIBUTE_PREFIX_DOVECOT),
+ DLUA_TABLE_STRING("MAILBOX_ATTRIBUTE_PREFIX_DOVECOT_PVT",
+ MAILBOX_ATTRIBUTE_PREFIX_DOVECOT_PVT),
+ DLUA_TABLE_STRING("MAILBOX_ATTRIBUTE_PREFIX_DOVECOT_PVT_SERVER",
+ MAILBOX_ATTRIBUTE_PREFIX_DOVECOT_PVT_SERVER),
+
+ DLUA_TABLE_END
+};
+
+static luaL_Reg lua_storage_methods[] = {
+ { NULL, NULL }
+};
+
+void dlua_register_mail_storage(struct dlua_script *script)
+{
+ /* get dlua_dovecot */
+ dlua_get_dovecot(script->L);
+
+ /* Create table for holding values */
+ lua_newtable(script->L);
+
+ dlua_set_members(script->L, lua_storage_mail_storage_flags, -1);
+
+ /* push new metatable to stack */
+ luaL_newmetatable(script->L, LUA_SCRIPT_STORAGE);
+ /* this will register functions to the metatable itself */
+ luaL_setfuncs(script->L, lua_storage_methods, 0);
+ /* point __index to self */
+ lua_pushvalue(script->L, -1);
+ lua_setfield(script->L, -1, "__index");
+ /* set table's metatable, pops stack */
+ lua_setmetatable(script->L, -2);
+
+ /* register table as member of dovecot */
+ lua_setfield(script->L, -2, LUA_SCRIPT_STORAGE);
+
+ lua_storage_mail_user_register(script);
+ lua_storage_mailbox_register(script);
+ lua_storage_mail_register(script);
+
+ /* pop dlua_dovecot from stack */
+ lua_pop(script->L, 1);
+}
diff --git a/src/lib-storage/mail-storage-lua.h b/src/lib-storage/mail-storage-lua.h
new file mode 100644
index 0000000..38738c9
--- /dev/null
+++ b/src/lib-storage/mail-storage-lua.h
@@ -0,0 +1,17 @@
+#ifndef MAIL_STORAGE_LUA_H
+#define MAIL_STORAGE_LUA_H 1
+
+#include "dlua-script.h"
+#include "dlua-script-private.h"
+
+struct mail_user;
+struct mailbox;
+struct mail;
+struct dlua_script;
+
+void dlua_register_mail_storage(struct dlua_script *script);
+void dlua_push_mail_user(lua_State *L, struct mail_user *user);
+void dlua_push_mailbox(lua_State *L, struct mailbox *box);
+void dlua_push_mail(lua_State *L, struct mail *mail);
+
+#endif
diff --git a/src/lib-storage/mail-storage-private.h b/src/lib-storage/mail-storage-private.h
new file mode 100644
index 0000000..bc462bb
--- /dev/null
+++ b/src/lib-storage/mail-storage-private.h
@@ -0,0 +1,913 @@
+#ifndef MAIL_STORAGE_PRIVATE_H
+#define MAIL_STORAGE_PRIVATE_H
+
+#include "module-context.h"
+#include "unichar.h"
+#include "file-lock.h"
+#include "mail-storage.h"
+#include "mail-storage-hooks.h"
+#include "mail-storage-settings.h"
+#include "mailbox-attribute-private.h"
+#include "mail-index-private.h"
+
+struct file_lock;
+struct file_create_settings;
+struct fs;
+
+/* Default prefix for indexes */
+#define MAIL_INDEX_PREFIX "dovecot.index"
+
+/* Block size when read()ing message header. */
+#define MAIL_READ_HDR_BLOCK_SIZE (1024*4)
+/* Block size when read()ing message (header and) body. */
+#define MAIL_READ_FULL_BLOCK_SIZE IO_BLOCK_SIZE
+
+#define MAIL_SHARED_STORAGE_NAME "shared"
+
+#define MAIL_STORAGE_LOST_MAILBOX_PREFIX "recovered-lost-folder-"
+
+enum mail_storage_list_index_rebuild_reason {
+ /* Mailbox list index was found to be corrupted. */
+ MAIL_STORAGE_LIST_INDEX_REBUILD_REASON_CORRUPTED,
+ /* Mailbox list index doesn't have INBOX in an inbox=yes namespace.
+ Rebuild is done to verify whether the user really is an empty new
+ user, or if an existing user's mailbox list index was lost. Because
+ this is called in non-error conditions, the callback shouldn't log
+ any errors or warnings if it didn't find any missing mailboxes. */
+ MAIL_STORAGE_LIST_INDEX_REBUILD_REASON_NO_INBOX,
+ /* MAILBOX_SYNC_FLAG_FORCE_RESYNC is run. This is called only once
+ per list, so that doveadm force-resync '*' won't cause it to run for
+ every mailbox. */
+ MAIL_STORAGE_LIST_INDEX_REBUILD_REASON_FORCE_RESYNC,
+};
+
+struct mail_storage_module_register {
+ unsigned int id;
+};
+
+struct mail_module_register {
+ unsigned int id;
+};
+
+struct mail_storage_vfuncs {
+ const struct setting_parser_info *(*get_setting_parser_info)(void);
+
+ struct mail_storage *(*alloc)(void);
+ int (*create)(struct mail_storage *storage, struct mail_namespace *ns,
+ const char **error_r);
+ void (*destroy)(struct mail_storage *storage);
+ void (*add_list)(struct mail_storage *storage,
+ struct mailbox_list *list);
+
+ void (*get_list_settings)(const struct mail_namespace *ns,
+ struct mailbox_list_settings *set);
+ bool (*autodetect)(const struct mail_namespace *ns,
+ struct mailbox_list_settings *set);
+
+ struct mailbox *(*mailbox_alloc)(struct mail_storage *storage,
+ struct mailbox_list *list,
+ const char *vname,
+ enum mailbox_flags flags);
+ int (*purge)(struct mail_storage *storage);
+ /* Called when mailbox list index rebuild is requested.
+ The callback should add any missing mailboxes to the list index.
+ Returns 0 on success, -1 on temporary failure that didn't properly
+ rebuild the index. */
+ int (*list_index_rebuild)(struct mail_storage *storage,
+ enum mail_storage_list_index_rebuild_reason reason);
+};
+
+union mail_storage_module_context {
+ struct mail_storage_vfuncs super;
+ struct mail_storage_module_register *reg;
+};
+
+enum mail_storage_class_flags {
+ /* mailboxes are files, not directories */
+ MAIL_STORAGE_CLASS_FLAG_MAILBOX_IS_FILE = 0x01,
+ /* root_dir points to a unique directory */
+ MAIL_STORAGE_CLASS_FLAG_UNIQUE_ROOT = 0x02,
+ /* mailbox_open_stream() is supported */
+ MAIL_STORAGE_CLASS_FLAG_OPEN_STREAMS = 0x04,
+ /* never use quota for this storage (e.g. virtual mailboxes) */
+ MAIL_STORAGE_CLASS_FLAG_NOQUOTA = 0x08,
+ /* Storage doesn't need a mail root directory */
+ MAIL_STORAGE_CLASS_FLAG_NO_ROOT = 0x10,
+ /* Storage uses one file per message */
+ MAIL_STORAGE_CLASS_FLAG_FILE_PER_MSG = 0x20,
+ /* Messages have GUIDs (always set mailbox_status.have_guids=TRUE) */
+ MAIL_STORAGE_CLASS_FLAG_HAVE_MAIL_GUIDS = 0x40,
+ /* mailbox_save_set_guid() works (always set
+ mailbox_status.have_save_guids=TRUE) */
+ MAIL_STORAGE_CLASS_FLAG_HAVE_MAIL_SAVE_GUIDS = 0x80,
+ /* message content can be unstructured binary data
+ (e.g. zlib plugin is allowed to compress/decompress mails) */
+ MAIL_STORAGE_CLASS_FLAG_BINARY_DATA = 0x100,
+ /* Message GUIDs can only be 128bit (always set
+ mailbox_status.have_only_guid128) */
+ MAIL_STORAGE_CLASS_FLAG_HAVE_MAIL_GUID128 = 0x200,
+ /* Storage deletes all files internally - mailbox list's
+ delete_mailbox() shouldn't delete anything itself. */
+ MAIL_STORAGE_CLASS_FLAG_NO_LIST_DELETES = 0x400,
+ /* Storage creates a secondary index */
+ MAIL_STORAGE_CLASS_FLAG_SECONDARY_INDEX = 0x800,
+};
+
+struct mail_binary_cache {
+ struct timeout *to;
+ struct mailbox *box;
+ uint32_t uid;
+
+ uoff_t orig_physical_pos;
+ bool include_hdr;
+ struct istream *input;
+ uoff_t size;
+};
+
+struct mail_storage_error {
+ char *error_string;
+ enum mail_error error;
+ char *last_internal_error;
+ bool last_error_is_internal;
+};
+
+struct mail_storage {
+ const char *name;
+ enum mail_storage_class_flags class_flags;
+ /* Fields that the storage backend can get by other means than parsing
+ the message header/body. For example the imapc backend can lookup
+ MAIL_FETCH_IMAP_BODYSTRUCTURE from the remote server. Adding fields
+ here avoids adding them to index_mail_data.access_part. */
+ enum mail_fetch_field nonbody_access_fields;
+ struct event_category *event_category;
+
+ struct mail_storage_vfuncs v, *vlast;
+
+/* private: */
+ pool_t pool;
+ struct mail_storage *prev, *next;
+ /* counting number of times mail_storage_create() has returned this
+ same storage. */
+ int refcount;
+ /* counting number of objects (e.g. mailbox) that have a pointer
+ to this storage. */
+ int obj_refcount;
+ /* Linked list of all mailboxes in the storage */
+ struct mailbox *mailboxes;
+ /* A "root dir" to enable storage sharing. It is only ever used for
+ * uniqueness checking (via strcmp) and never used as a path. */
+ const char *unique_root_dir;
+
+ /* prefix for lost mailbox */
+ const char *lost_mailbox_prefix;
+
+ /* Last error set in mail_storage_set_critical(). */
+ char *last_internal_error;
+
+ char *error_string;
+ enum mail_error error;
+ ARRAY(struct mail_storage_error) error_stack;
+ struct event *event;
+
+ const struct mail_storage *storage_class;
+ struct mail_user *user;
+ const char *temp_path_prefix;
+ const struct mail_storage_settings *set;
+
+ enum mail_storage_flags flags;
+
+ struct mail_storage_callbacks callbacks;
+ void *callback_context;
+
+ struct mail_binary_cache binary_cache;
+ /* Filled lazily by mailbox_attribute_*() when accessing shared
+ attributes. */
+ struct dict *_shared_attr_dict;
+
+ /* optional fs-api object for accessing mailboxes */
+ struct fs *mailboxes_fs;
+
+ /* Module-specific contexts. See mail_storage_module_id. */
+ ARRAY(union mail_storage_module_context *) module_contexts;
+
+ /* Failed to create shared attribute dict, don't try again */
+ bool shared_attr_dict_failed:1;
+ bool last_error_is_internal:1;
+ bool rebuilding_list_index:1;
+ bool rebuild_list_index:1;
+};
+
+struct mail_attachment_part {
+ struct message_part *part;
+ const char *content_type, *content_disposition;
+};
+
+struct virtual_mailbox_vfuncs {
+ /* convert backend UIDs to virtual UIDs. if some backend UID doesn't
+ exist in mailbox, it's simply ignored */
+ void (*get_virtual_uids)(struct mailbox *box,
+ struct mailbox *backend_mailbox,
+ const ARRAY_TYPE(seq_range) *backend_uids,
+ ARRAY_TYPE(seq_range) *virtual_uids_r);
+ /* like get_virtual_uids(), but if a backend UID doesn't exist,
+ convert it to 0. */
+ void (*get_virtual_uid_map)(struct mailbox *box,
+ struct mailbox *backend_mailbox,
+ const ARRAY_TYPE(seq_range) *backend_uids,
+ ARRAY_TYPE(uint32_t) *virtual_uids_r);
+ void (*get_virtual_backend_boxes)(struct mailbox *box,
+ ARRAY_TYPE(mailboxes) *mailboxes,
+ bool only_with_msgs);
+};
+
+struct mailbox_vfuncs {
+ bool (*is_readonly)(struct mailbox *box);
+
+ int (*enable)(struct mailbox *box, enum mailbox_feature features);
+ int (*exists)(struct mailbox *box, bool auto_boxes,
+ enum mailbox_existence *existence_r);
+ int (*open)(struct mailbox *box);
+ void (*close)(struct mailbox *box);
+ void (*free)(struct mailbox *box);
+
+ int (*create_box)(struct mailbox *box,
+ const struct mailbox_update *update, bool directory);
+ int (*update_box)(struct mailbox *box,
+ const struct mailbox_update *update);
+ int (*delete_box)(struct mailbox *box);
+ int (*rename_box)(struct mailbox *src, struct mailbox *dest);
+
+ int (*get_status)(struct mailbox *box, enum mailbox_status_items items,
+ struct mailbox_status *status_r);
+ int (*get_metadata)(struct mailbox *box,
+ enum mailbox_metadata_items items,
+ struct mailbox_metadata *metadata_r);
+ int (*set_subscribed)(struct mailbox *box, bool set);
+
+ int (*attribute_set)(struct mailbox_transaction_context *t,
+ enum mail_attribute_type type_flags,
+ const char *key,
+ const struct mail_attribute_value *value);
+ int (*attribute_get)(struct mailbox *box,
+ enum mail_attribute_type type_flags,
+ const char *key,
+ struct mail_attribute_value *value_r);
+ struct mailbox_attribute_iter *
+ (*attribute_iter_init)(struct mailbox *box,
+ enum mail_attribute_type type_flags,
+ const char *prefix);
+ const char *(*attribute_iter_next)(struct mailbox_attribute_iter *iter);
+ int (*attribute_iter_deinit)(struct mailbox_attribute_iter *iter);
+
+ /* Lookup sync extension record and figure out if it mailbox has
+ changed since. Returns 1 = yes, 0 = no, -1 = error. if quick==TRUE,
+ return 1 if it's too costly to find out exactly. The reason_r is
+ set if 1 is returned. */
+ int (*list_index_has_changed)(struct mailbox *box,
+ struct mail_index_view *list_view,
+ uint32_t seq, bool quick,
+ const char **reason_r);
+ /* Update the sync extension record. */
+ void (*list_index_update_sync)(struct mailbox *box,
+ struct mail_index_transaction *trans,
+ uint32_t seq);
+
+ struct mailbox_sync_context *
+ (*sync_init)(struct mailbox *box,
+ enum mailbox_sync_flags flags);
+ bool (*sync_next)(struct mailbox_sync_context *ctx,
+ struct mailbox_sync_rec *sync_rec_r);
+ int (*sync_deinit)(struct mailbox_sync_context *ctx,
+ struct mailbox_sync_status *status_r);
+
+ /* Called once for each expunge. Called one or more times for
+ flag/keyword changes. Once the sync is finished, called with
+ uid=0 and sync_type=0. */
+ void (*sync_notify)(struct mailbox *box, uint32_t uid,
+ enum mailbox_sync_type sync_type);
+
+ void (*notify_changes)(struct mailbox *box);
+
+ struct mailbox_transaction_context *
+ (*transaction_begin)(struct mailbox *box,
+ enum mailbox_transaction_flags flags,
+ const char *reason);
+ int (*transaction_commit)(struct mailbox_transaction_context *t,
+ struct mail_transaction_commit_changes *changes_r);
+ void (*transaction_rollback)(struct mailbox_transaction_context *t);
+
+ enum mail_flags (*get_private_flags_mask)(struct mailbox *box);
+
+ struct mail *
+ (*mail_alloc)(struct mailbox_transaction_context *t,
+ enum mail_fetch_field wanted_fields,
+ struct mailbox_header_lookup_ctx *wanted_headers);
+
+ struct mail_search_context *
+ (*search_init)(struct mailbox_transaction_context *t,
+ struct mail_search_args *args,
+ const enum mail_sort_type *sort_program,
+ enum mail_fetch_field wanted_fields,
+ struct mailbox_header_lookup_ctx *wanted_headers);
+ int (*search_deinit)(struct mail_search_context *ctx);
+ bool (*search_next_nonblock)(struct mail_search_context *ctx,
+ struct mail **mail_r, bool *tryagain_r);
+ /* Internally used by the search to find the next mail that potentially
+ matches the search query. The function performs "quick checks" on
+ the search query that can be done without initializing the mail
+ struct. For example it can filter out non-matching mails based on
+ SEARCH_SEQSET and SEARCH_UIDSET. Once a potentially matching mail is
+ found, ctx->seq is updated to it.
+
+ Plugins can use this function to skip over any mails that can't
+ match the search query. Plugins can also set
+ mail_search_arg.[non]match_always=TRUE
+ for search args that it already definitely knows will/won't match,
+ which prevents the standard search from attempting to match them
+ again.
+
+ This is used by the FTS plugin to skip over non-matching mails and
+ to initialize the [non]match_always=TRUE for the search args that
+ were matched against the FTS indexes. This prevents the standard
+ search from having to open any email bodies. */
+ bool (*search_next_update_seq)(struct mail_search_context *ctx);
+ int (*search_next_match_mail)(struct mail_search_context *ctx,
+ struct mail *mail);
+
+ struct mail_save_context *
+ (*save_alloc)(struct mailbox_transaction_context *t);
+ int (*save_begin)(struct mail_save_context *ctx, struct istream *input);
+ int (*save_continue)(struct mail_save_context *ctx);
+ int (*save_finish)(struct mail_save_context *ctx);
+ void (*save_cancel)(struct mail_save_context *ctx);
+ int (*copy)(struct mail_save_context *ctx, struct mail *mail);
+
+ /* Called during transaction commit/rollback if saving was done */
+ int (*transaction_save_commit_pre)(struct mail_save_context *save_ctx);
+ void (*transaction_save_commit_post)
+ (struct mail_save_context *save_ctx,
+ struct mail_index_transaction_commit_result *result_r);
+ void (*transaction_save_rollback)(struct mail_save_context *save_ctx);
+
+ bool (*is_inconsistent)(struct mailbox *box);
+};
+
+union mailbox_module_context {
+ struct mailbox_vfuncs super;
+ struct mail_storage_module_register *reg;
+};
+
+struct mail_msgpart_partial_cache {
+ uint32_t uid;
+ uoff_t physical_start;
+ uoff_t physical_pos, virtual_pos;
+};
+
+struct mailbox_index_vsize {
+ uint64_t vsize;
+ uint32_t highest_uid;
+ uint32_t message_count;
+};
+
+struct mailbox_index_pop3_uidl {
+ uint32_t max_uid_with_pop3_uidl;
+};
+
+struct mailbox_index_first_saved {
+ uint32_t uid;
+ uint32_t timestamp;
+};
+
+struct mailbox {
+ const char *name;
+ /* mailbox's virtual name (from mail_namespace_get_vname()) */
+ const char *vname;
+ struct mail_storage *storage;
+ struct mailbox_list *list;
+ struct event *event;
+
+ struct mailbox_vfuncs v, *vlast;
+ /* virtual mailboxes: */
+ const struct virtual_mailbox_vfuncs *virtual_vfuncs;
+/* private: */
+ pool_t pool;
+ /* Linked list of all mailboxes in this storage */
+ struct mailbox *prev, *next;
+
+ /* these won't be set until mailbox is opened: */
+ struct mail_index *index;
+ struct mail_index_view *view;
+ struct mail_cache *cache;
+ /* Private per-user index/view for shared mailboxes. These are synced
+ against the primary index and used to store per-user flags.
+ These are non-NULL only when mailbox has per-user flags. */
+ struct mail_index *index_pvt;
+ struct mail_index_view *view_pvt;
+ /* Filled lazily by mailbox_get_permissions() */
+ struct mailbox_permissions _perm;
+ /* Filled lazily when mailbox is opened, use mailbox_get_path()
+ to access it */
+ const char *_path;
+ /* Filled lazily when mailbox is opened, use mailbox_get_index_path()
+ to access it */
+ const char *_index_path;
+
+ /* default vfuncs for new struct mails. */
+ const struct mail_vfuncs *mail_vfuncs;
+ /* Mailbox settings, or NULL if defaults */
+ const struct mailbox_settings *set;
+
+ /* If non-zero, fail mailbox_open() with this error. mailbox_alloc()
+ can set this to force open to fail. */
+ enum mail_error open_error;
+
+ struct istream *input;
+ const char *index_prefix;
+ enum mailbox_flags flags;
+ unsigned int transaction_count;
+ unsigned int attribute_iter_count;
+ enum mailbox_feature enabled_features;
+ struct mail_msgpart_partial_cache partial_cache;
+ uint32_t vsize_hdr_ext_id;
+ uint32_t pop3_uidl_hdr_ext_id;
+ uint32_t box_name_hdr_ext_id;
+ uint32_t box_last_rename_stamp_ext_id;
+ uint32_t mail_vsize_ext_id;
+
+ /* MAIL_RECENT flags handling */
+ ARRAY_TYPE(seq_range) recent_flags;
+ uint32_t recent_flags_prev_uid;
+ uint32_t recent_flags_count;
+
+ struct mail_index_view *tmp_sync_view;
+
+ /* Mailbox notification settings: */
+ mailbox_notify_callback_t *notify_callback;
+ void *notify_context;
+ struct timeout *to_notify, *to_notify_delay;
+ struct mailbox_notify_file *notify_files;
+
+ /* Increased by one for each new struct mailbox. */
+ unsigned int generation_sequence;
+
+ /* Saved search results */
+ ARRAY(struct mail_search_result *) search_results;
+
+ /* Module-specific contexts. See mail_storage_module_id. */
+ ARRAY(union mailbox_module_context *) module_contexts;
+
+ /* When FAST open flag is used, the mailbox isn't actually opened until
+ it's synced for the first time. */
+ bool opened:1;
+ /* Mailbox was deleted while we had it open. */
+ bool mailbox_deleted:1;
+ /* Mailbox is being created */
+ bool creating:1;
+ /* Mailbox is being deleted */
+ bool deleting:1;
+ /* Mailbox is being undeleted */
+ bool mailbox_undeleting:1;
+ /* Don't use MAIL_INDEX_SYNC_FLAG_DELETING_INDEX for sync flag */
+ bool delete_sync_check:1;
+ /* Delete mailbox only if it's empty */
+ bool deleting_must_be_empty:1;
+ /* The backend wants to skip checking if there are 0 messages before
+ calling mailbox_list.delete_mailbox() */
+ bool delete_skip_empty_check:1;
+ /* Mailbox was already marked as deleted within this allocation. */
+ bool marked_deleted:1;
+ /* TRUE if this is an INBOX for this user */
+ bool inbox_user:1;
+ /* TRUE if this is an INBOX for this namespace (user or shared) */
+ bool inbox_any:1;
+ /* When copying to this mailbox, require that mailbox_copy() uses
+ mailbox_save_*() to actually save a new physical copy rather than
+ simply incrementing a reference count (e.g. via hard link) */
+ bool disable_reflink_copy_to:1;
+ /* Don't allow creating any new keywords */
+ bool disallow_new_keywords:1;
+ /* Mailbox has been synced at least once */
+ bool synced:1;
+ /* Updating cache file is disabled */
+ bool mail_cache_disabled:1;
+ /* Update first_saved field to mailbox list index. */
+ bool update_first_saved:1;
+ /* mailbox_verify_create_name() only checks for mailbox_verify_name() */
+ bool skip_create_name_restrictions:1;
+ /* Using LAYOUT=index and mailbox is being opened with a corrupted
+ mailbox name. Try to revert to the previously known good name. */
+ bool corrupted_mailbox_name:1;
+ /* mailbox_open() returned MAIL_ERROR_NOTFOUND because the mailbox
+ doesn't have the LOOKUP ACL right. */
+ bool acl_no_lookup_right:1;
+};
+
+struct mail_vfuncs {
+ void (*close)(struct mail *mail);
+ void (*free)(struct mail *mail);
+ void (*set_seq)(struct mail *mail, uint32_t seq, bool saving);
+ bool (*set_uid)(struct mail *mail, uint32_t uid);
+ void (*set_uid_cache_updates)(struct mail *mail, bool set);
+ bool (*prefetch)(struct mail *mail);
+ int (*precache)(struct mail *mail);
+ void (*add_temp_wanted_fields)(struct mail *mail,
+ enum mail_fetch_field fields,
+ struct mailbox_header_lookup_ctx *headers);
+
+ enum mail_flags (*get_flags)(struct mail *mail);
+ const char *const *(*get_keywords)(struct mail *mail);
+ const ARRAY_TYPE(keyword_indexes) *
+ (*get_keyword_indexes)(struct mail *mail);
+ uint64_t (*get_modseq)(struct mail *mail);
+ uint64_t (*get_pvt_modseq)(struct mail *mail);
+
+ int (*get_parts)(struct mail *mail,
+ struct message_part **parts_r);
+ int (*get_date)(struct mail *mail, time_t *date_r, int *timezone_r);
+ int (*get_received_date)(struct mail *mail, time_t *date_r);
+ int (*get_save_date)(struct mail *mail, time_t *date_r);
+ int (*get_virtual_size)(struct mail *mail, uoff_t *size_r);
+ int (*get_physical_size)(struct mail *mail, uoff_t *size_r);
+
+ int (*get_first_header)(struct mail *mail, const char *field,
+ bool decode_to_utf8, const char **value_r);
+ int (*get_headers)(struct mail *mail, const char *field,
+ bool decode_to_utf8, const char *const **value_r);
+ int (*get_header_stream)(struct mail *mail,
+ struct mailbox_header_lookup_ctx *headers,
+ struct istream **stream_r);
+ int (*get_stream)(struct mail *mail, bool get_body,
+ struct message_size *hdr_size,
+ struct message_size *body_size,
+ struct istream **stream_r);
+ int (*get_binary_stream)(struct mail *mail,
+ const struct message_part *part,
+ bool include_hdr, uoff_t *size_r,
+ unsigned int *lines_r, bool *binary_r,
+ struct istream **stream_r);
+
+ int (*get_special)(struct mail *mail, enum mail_fetch_field field,
+ const char **value_r);
+ int (*get_backend_mail)(struct mail *mail, struct mail **real_mail_r);
+
+ void (*update_flags)(struct mail *mail, enum modify_type modify_type,
+ enum mail_flags flags);
+ void (*update_keywords)(struct mail *mail, enum modify_type modify_type,
+ struct mail_keywords *keywords);
+ void (*update_modseq)(struct mail *mail, uint64_t min_modseq);
+ void (*update_pvt_modseq)(struct mail *mail, uint64_t min_pvt_modseq);
+ void (*update_pop3_uidl)(struct mail *mail, const char *uidl);
+ void (*expunge)(struct mail *mail);
+ void (*set_cache_corrupted)(struct mail *mail,
+ enum mail_fetch_field field,
+ const char *reason);
+ int (*istream_opened)(struct mail *mail, struct istream **input);
+};
+
+union mail_module_context {
+ struct mail_vfuncs super;
+ struct mail_module_register *reg;
+};
+
+struct mail_private {
+ struct mail mail;
+ struct mail_vfuncs v, *vlast;
+ /* normally NULL, but in case this is a "backend mail" for a mail
+ created by virtual storage, this points back to the original virtual
+ mail. at least mailbox_copy() bypasses the virtual storage, so this
+ allows mail_log plugin to log the copy operation using the original
+ mailbox name. */
+ struct mail *vmail;
+ /* Event is created lazily. Use mail_event() to access it. */
+ struct event *_event;
+
+ uint32_t seq_pvt;
+
+ /* initial wanted fields/headers, set by mail_alloc(): */
+ enum mail_fetch_field wanted_fields;
+ struct mailbox_header_lookup_ctx *wanted_headers;
+
+ pool_t pool, data_pool;
+ ARRAY(union mail_module_context *) module_contexts;
+
+ const char *get_stream_reason;
+
+ bool autoexpunged:1;
+ /* mail created by mailbox_search_*() */
+ bool search_mail:1;
+};
+
+struct mailbox_list_context {
+ struct mail_storage *storage;
+ enum mailbox_list_flags flags;
+ bool failed;
+};
+
+union mailbox_transaction_module_context {
+ struct mail_storage_module_register *reg;
+};
+
+struct mailbox_transaction_stats {
+ unsigned long open_lookup_count;
+ unsigned long stat_lookup_count;
+ unsigned long fstat_lookup_count;
+ /* number of files we've opened and read */
+ unsigned long files_read_count;
+ /* number of bytes we've had to read from files */
+ unsigned long long files_read_bytes;
+ /* number of cache lookup hits */
+ unsigned long cache_hit_count;
+};
+
+struct mail_save_private_changes {
+ /* first saved mail is 0, second is 1, etc. we'll map these to UIDs
+ using struct mail_transaction_commit_changes. */
+ unsigned int mailnum;
+ enum mail_flags flags;
+};
+
+struct mailbox_transaction_context {
+ struct mailbox *box;
+ enum mailbox_transaction_flags flags;
+ char *reason;
+
+ union mail_index_transaction_module_context module_ctx;
+ struct mail_index_transaction_vfuncs super;
+ int mail_ref_count;
+
+ struct mail_index_transaction *itrans;
+ struct dict_transaction_context *attr_pvt_trans, *attr_shared_trans;
+ /* view contains all changes done within this transaction */
+ struct mail_index_view *view;
+
+ /* for private index updates: */
+ struct mail_index_transaction *itrans_pvt;
+ struct mail_index_view *view_pvt;
+
+ struct mail_cache_view *cache_view;
+ struct mail_cache_transaction_ctx *cache_trans;
+
+ struct mail_transaction_commit_changes *changes;
+ ARRAY(union mailbox_transaction_module_context *) module_contexts;
+
+ uint32_t prev_pop3_uidl_tracking_seq;
+ uint32_t highest_pop3_uidl_uid;
+
+ struct mail_save_context *save_ctx;
+ /* number of mails saved/copied within this transaction. */
+ unsigned int save_count;
+ /* List of private flags added with save/copy. These are added to the
+ private index after committing the mails to the shared index. */
+ ARRAY(struct mail_save_private_changes) pvt_saves;
+
+ /* these statistics are never reset by mail-storage API: */
+ struct mailbox_transaction_stats stats;
+ /* Set to TRUE to update stats_* fields */
+ bool stats_track:1;
+};
+
+union mail_search_module_context {
+ struct mail_storage_module_register *reg;
+};
+
+struct mail_search_context {
+ struct mailbox_transaction_context *transaction;
+
+ struct mail_search_args *args;
+ struct mail_search_sort_program *sort_program;
+ enum mail_fetch_field wanted_fields;
+ struct mailbox_header_lookup_ctx *wanted_headers;
+ normalizer_func_t *normalizer;
+
+ /* if non-NULL, specifies that a search resulting is being updated.
+ this can be used as a search optimization: if searched message
+ already exists in search result, it's not necessary to check if
+ static data matches. */
+ struct mail_search_result *update_result;
+ /* add matches to these search results */
+ ARRAY(struct mail_search_result *) results;
+
+ uint32_t seq;
+ uint32_t progress_cur, progress_max;
+
+ ARRAY(struct mail *) mails;
+ unsigned int unused_mail_idx;
+ unsigned int max_mails;
+
+ ARRAY(union mail_search_module_context *) module_contexts;
+
+ bool seen_lost_data:1;
+ bool progress_hidden:1;
+};
+
+struct mail_save_data {
+ enum mail_flags flags;
+ enum mail_flags pvt_flags;
+ struct mail_keywords *keywords;
+ uint64_t min_modseq;
+
+ time_t received_date, save_date;
+ int received_tz_offset;
+
+ uint32_t uid;
+ char *guid, *pop3_uidl, *from_envelope;
+ uint32_t pop3_order;
+
+ struct ostream *output;
+ struct mail_save_attachment *attach;
+};
+
+struct mail_save_context {
+ struct mailbox_transaction_context *transaction;
+ struct mail *dest_mail;
+ /* Set during mailbox_copy(). This is useful when copying is
+ implemented via save, and the save_*() methods want to access the
+ source mail. */
+ struct mail *copy_src_mail;
+
+ /* data that changes for each saved mail */
+ struct mail_save_data data;
+
+ /* returns TRUE if message part is an attachment. */
+ bool (*part_is_attachment)(struct mail_save_context *ctx,
+ const struct mail_attachment_part *part);
+
+ /* mailbox_save_alloc() called, but finish/cancel not.
+ the same context is usually returned by the backends for reuse. */
+ bool unfinished:1;
+ /* mailbox_save_finish() or mailbox_copy() is being called. */
+ bool finishing:1;
+ /* mail was copied or moved using saving (requires:
+ copying_or_moving==TRUE). */
+ bool copying_via_save:1;
+ /* mail is being saved, not copied. However, this is set also with
+ mailbox_save_using_mail() and then copying_or_moving==TRUE. */
+ bool saving:1;
+ /* mail is being moved - ignore quota (requires:
+ copying_or_moving==TRUE && saving==FALSE). */
+ bool moving:1;
+ /* mail is being copied or moved. However, this is set also with
+ mailbox_save_using_mail() and then saving==TRUE. */
+ bool copying_or_moving:1;
+};
+
+struct mailbox_sync_context {
+ struct mailbox *box;
+ enum mailbox_sync_flags flags;
+ bool open_failed;
+};
+
+struct mailbox_header_lookup_ctx {
+ struct mailbox *box;
+ pool_t pool;
+ int refcount;
+
+ unsigned int count;
+ const char *const *name;
+ unsigned int *idx;
+};
+
+/* Modules should use do "my_id = mail_storage_module_id++" and
+ use objects' module_contexts[id] for their own purposes. */
+extern struct mail_storage_module_register mail_storage_module_register;
+
+/* Storage's module_id for mail_index. */
+extern struct mail_module_register mail_module_register;
+
+extern struct event_category event_category_storage;
+extern struct event_category event_category_mailbox;
+extern struct event_category event_category_mail;
+
+#define MAIL_STORAGE_CONTEXT(obj) \
+ MODULE_CONTEXT(obj, mail_storage_mail_index_module)
+#define MAIL_STORAGE_CONTEXT_REQUIRE(obj) \
+ MODULE_CONTEXT_REQUIRE(obj, mail_storage_mail_index_module)
+extern MODULE_CONTEXT_DEFINE(mail_storage_mail_index_module,
+ &mail_index_module_register);
+
+void mail_storage_obj_ref(struct mail_storage *storage);
+void mail_storage_obj_unref(struct mail_storage *storage);
+
+/* Set error message in storage. Critical errors are logged with i_error(),
+ but user sees only "internal error" message. */
+void mail_storage_clear_error(struct mail_storage *storage);
+void mail_storage_set_error(struct mail_storage *storage,
+ enum mail_error error, const char *string);
+void mail_storage_set_critical(struct mail_storage *storage,
+ const char *fmt, ...) ATTR_FORMAT(2, 3);
+void mailbox_set_critical(struct mailbox *box,
+ const char *fmt, ...) ATTR_FORMAT(2, 3);
+void mail_set_critical(struct mail *mail,
+ const char *fmt, ...) ATTR_FORMAT(2, 3);
+void mail_storage_set_internal_error(struct mail_storage *storage);
+void mailbox_set_index_error(struct mailbox *box);
+void mail_storage_set_index_error(struct mail_storage *storage,
+ struct mail_index *index);
+bool mail_storage_set_error_from_errno(struct mail_storage *storage);
+void mail_storage_copy_list_error(struct mail_storage *storage,
+ struct mailbox_list *list);
+void mail_storage_copy_error(struct mail_storage *dest,
+ struct mail_storage *src);
+/* set record in mail cache corrupted */
+void mail_set_mail_cache_corrupted(struct mail *mail, const char *fmt, ...)
+ ATTR_FORMAT(2, 3);
+
+/* Indicate mail being expunged by autoexpunge */
+void mail_autoexpunge(struct mail *mail);
+
+void mail_event_create(struct mail *mail);
+/* Returns TRUE if everything should already be in memory after this call
+ or if prefetching is not supported, i.e. the caller shouldn't do more
+ prefetching before this message is handled. */
+bool mail_prefetch(struct mail *mail);
+void mail_set_aborted(struct mail *mail);
+void mail_set_expunged(struct mail *mail);
+void mail_set_seq_saving(struct mail *mail, uint32_t seq);
+/* Returns true IF and only IF the mail has EITHER one of the
+ attachment keywords set. If it has both, or none, it will return FALSE. */
+bool mail_has_attachment_keywords(struct mail *mail);
+/* Sets attachment keywords. Returns -1 on error, 0 when no attachment(s) found,
+ and 1 if attachment was found. */
+int mail_set_attachment_keywords(struct mail *mail);
+
+/* Attempt to start accessing the mail stream. Returns TRUE is ok, FALSE if
+ prevented by mail->lookup_abort. */
+bool mail_stream_access_start(struct mail *mail);
+/* Attempt to start accessing the mail metadata. Returns TRUE is ok, FALSE if
+ prevented by mail->lookup_abort. */
+bool mail_metadata_access_start(struct mail *mail);
+/* Emit mail opened events */
+void mail_opened_event(struct mail *mail);
+
+/* Emit mail expunge_requested event */
+void mail_expunge_requested_event(struct mail *mail);
+
+void mailbox_set_deleted(struct mailbox *box);
+int mailbox_mark_index_deleted(struct mailbox *box, bool del);
+/* Easy wrapper for getting mailbox's MAILBOX_LIST_PATH_TYPE_MAILBOX.
+ The mailbox must already be opened and the caller must know that the
+ storage has mailbox files (i.e. NULL/empty path is never returned). */
+const char *mailbox_get_path(struct mailbox *box) ATTR_PURE;
+/* Similar to mailbox_get_path() but for MAILBOX_LIST_PATH_TYPE_INDEX. */
+const char *mailbox_get_index_path(struct mailbox *box) ATTR_PURE;
+/* Wrapper to mailbox_list_get_path() */
+int mailbox_get_path_to(struct mailbox *box, enum mailbox_list_path_type type,
+ const char **path_r);
+/* Get mailbox permissions. */
+const struct mailbox_permissions *mailbox_get_permissions(struct mailbox *box);
+/* Force permissions to be refreshed on next lookup */
+void mailbox_refresh_permissions(struct mailbox *box);
+
+/* Open private index files for mailbox. Returns 1 if opened, 0 if there
+ are no private indexes (or flags) in this mailbox, -1 if error. */
+int mailbox_open_index_pvt(struct mailbox *box);
+/* Create path's directory with proper permissions. The root directory is also
+ created if necessary. Returns 1 if created, 0 if it already existed,
+ -1 if error. */
+int mailbox_mkdir(struct mailbox *box, const char *path,
+ enum mailbox_list_path_type type);
+/* Create a non-mailbox type directory for mailbox if it's missing (e.g. index).
+ Optimized for case where the directory usually exists. */
+int mailbox_create_missing_dir(struct mailbox *box,
+ enum mailbox_list_path_type type);
+/* Returns TRUE if mailbox is autocreated. */
+bool mailbox_is_autocreated(struct mailbox *box);
+/* Returns TRUE if mailbox is autosubscribed. */
+bool mailbox_is_autosubscribed(struct mailbox *box);
+
+/* Returns -1 if error, 0 if failed with EEXIST, 1 if ok */
+int mailbox_create_fd(struct mailbox *box, const char *path, int flags,
+ int *fd_r);
+/* Create a lock file with the given path and settings. If it succeeds,
+ returns 1 and lock_r, which needs to be freed once finished with the lock.
+ If lock_set->lock_timeout_secs is reached, returns 0 and error_r. Returns
+ -1 and sets error_r on other errors. */
+int mail_storage_lock_create(const char *lock_path,
+ const struct file_create_settings *lock_set,
+ const struct mail_storage_settings *mail_set,
+ struct file_lock **lock_r, const char **error_r);
+/* Create a lock file to the mailbox with the given filename. Returns the same
+ as mail_storage_lock_create(). */
+int mailbox_lock_file_create(struct mailbox *box, const char *lock_fname,
+ unsigned int lock_secs, struct file_lock **lock_r,
+ const char **error_r);
+unsigned int mail_storage_get_lock_timeout(struct mail_storage *storage,
+ unsigned int secs);
+void mail_storage_free_binary_cache(struct mail_storage *storage);
+
+enum mail_index_open_flags
+mail_storage_settings_to_index_flags(const struct mail_storage_settings *set);
+void mailbox_save_context_deinit(struct mail_save_context *ctx);
+
+/* Notify that a sync should be done. */
+void mailbox_sync_notify(struct mailbox *box, uint32_t uid,
+ enum mailbox_sync_type sync_type);
+
+/* for unit testing */
+int mailbox_verify_name(struct mailbox *box);
+
+int mail_storage_list_index_rebuild_and_set_uncorrupted(struct mail_storage *storage);
+int mail_storage_list_index_rebuild(struct mail_storage *storage,
+ enum mail_storage_list_index_rebuild_reason reason);
+
+#endif
diff --git a/src/lib-storage/mail-storage-register.c b/src/lib-storage/mail-storage-register.c
new file mode 100644
index 0000000..60bff70
--- /dev/null
+++ b/src/lib-storage/mail-storage-register.c
@@ -0,0 +1,31 @@
+/* Copyright (c) 2017-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "mail-storage.h"
+
+extern struct mail_storage shared_storage;
+extern struct mail_storage dbox_storage;
+extern struct mail_storage mdbox_storage;
+extern struct mail_storage mdbox_deleted_storage;
+extern struct mail_storage sdbox_storage;
+extern struct mail_storage maildir_storage;
+extern struct mail_storage mbox_storage;
+extern struct mail_storage imapc_storage;
+extern struct mail_storage pop3c_storage;
+extern struct mail_storage raw_storage;
+extern struct mail_storage fail_storage;
+
+void mail_storage_register_all(void)
+{
+ mail_storage_class_register(&shared_storage);
+ mail_storage_class_register(&dbox_storage);
+ mail_storage_class_register(&mdbox_storage);
+ mail_storage_class_register(&mdbox_deleted_storage);
+ mail_storage_class_register(&sdbox_storage);
+ mail_storage_class_register(&maildir_storage);
+ mail_storage_class_register(&mbox_storage);
+ mail_storage_class_register(&imapc_storage);
+ mail_storage_class_register(&pop3c_storage);
+ mail_storage_class_register(&raw_storage);
+ mail_storage_class_register(&fail_storage);
+}
diff --git a/src/lib-storage/mail-storage-service.c b/src/lib-storage/mail-storage-service.c
new file mode 100644
index 0000000..567ecb8
--- /dev/null
+++ b/src/lib-storage/mail-storage-service.c
@@ -0,0 +1,1807 @@
+/* Copyright (c) 2009-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "ioloop.h"
+#include "array.h"
+#include "base64.h"
+#include "hostpid.h"
+#include "module-dir.h"
+#include "restrict-access.h"
+#include "eacces-error.h"
+#include "ipwd.h"
+#include "str.h"
+#include "time-util.h"
+#include "sleep.h"
+#include "var-expand.h"
+#include "dict.h"
+#include "settings-parser.h"
+#include "auth-master.h"
+#include "master-service-private.h"
+#include "master-service-settings.h"
+#include "master-service-ssl-settings.h"
+#include "master-service-settings-cache.h"
+#include "mail-user.h"
+#include "mail-namespace.h"
+#include "mail-storage.h"
+#include "mail-storage-service.h"
+
+#include <sys/stat.h>
+#include <time.h>
+
+#ifdef HAVE_SYS_TIME_H
+# include <sys/time.h>
+#endif
+#ifdef HAVE_SYS_RESOURCE_H
+# include <sys/resource.h>
+#endif
+
+/* If time moves backwards more than this, kill ourself instead of sleeping. */
+#define MAX_TIME_BACKWARDS_SLEEP_MSECS (5*1000)
+#define MAX_NOWARN_FORWARD_MSECS (10*1000)
+
+#define ERRSTR_INVALID_USER_SETTINGS \
+ "Invalid user settings. Refer to server log for more information."
+
+struct mail_storage_service_privileges {
+ uid_t uid;
+ gid_t gid;
+ const char *uid_source, *gid_source;
+
+ const char *home;
+ const char *chroot;
+};
+
+struct mail_storage_service_ctx {
+ pool_t pool;
+ struct master_service *service;
+ const char *default_log_prefix;
+
+ struct auth_master_connection *conn, *iter_conn;
+ struct auth_master_user_list_ctx *auth_list;
+ const struct setting_parser_info **set_roots;
+ enum mail_storage_service_flags flags;
+
+ const char *set_cache_module, *set_cache_service;
+ struct master_service_settings_cache *set_cache;
+
+ pool_t userdb_next_pool;
+ const char *const **userdb_next_fieldsp;
+
+ bool debug:1;
+ bool log_initialized:1;
+ bool config_permission_denied:1;
+};
+
+struct mail_storage_service_user {
+ pool_t pool;
+ int refcount;
+
+ struct mail_storage_service_ctx *service_ctx;
+ struct mail_storage_service_input input;
+ enum mail_storage_service_flags flags;
+
+ struct event *event;
+ struct ioloop_context *ioloop_ctx;
+ const char *log_prefix, *auth_mech, *auth_token, *auth_user;
+
+ const char *system_groups_user, *uid_source, *gid_source;
+ const char *chdir_path;
+ const struct mail_user_settings *user_set;
+ const struct master_service_ssl_settings *ssl_set;
+ const struct setting_parser_info *user_info;
+ struct setting_parser_context *set_parser;
+
+ unsigned int session_id_counter;
+
+ bool anonymous:1;
+ bool admin:1;
+};
+
+struct module *mail_storage_service_modules = NULL;
+static struct mail_storage_service_ctx *storage_service_global = NULL;
+
+static int
+mail_storage_service_var_expand(struct mail_storage_service_ctx *ctx,
+ string_t *str, const char *format,
+ struct mail_storage_service_user *user,
+ const struct mail_storage_service_input *input,
+ const struct mail_storage_service_privileges *priv,
+ const char **error_r);
+
+static bool
+mail_user_set_get_mail_debug(const struct setting_parser_info *user_info,
+ const struct mail_user_settings *user_set)
+{
+ const struct mail_storage_settings *mail_set;
+
+ mail_set = mail_user_set_get_driver_settings(user_info, user_set,
+ MAIL_STORAGE_SET_DRIVER_NAME);
+ return mail_set->mail_debug;
+}
+
+static void set_keyval(struct mail_storage_service_ctx *ctx,
+ struct mail_storage_service_user *user,
+ const char *key, const char *value)
+{
+ struct setting_parser_context *set_parser = user->set_parser;
+
+ if (master_service_set_has_config_override(ctx->service, key)) {
+ /* this setting was already overridden with -o parameter */
+ e_debug(user->event,
+ "Ignoring overridden (-o) userdb setting: %s",
+ key);
+ return;
+ }
+
+ if (settings_parse_keyvalue(set_parser, key, value) < 0) {
+ i_fatal("Invalid userdb input %s=%s: %s", key, value,
+ settings_parser_get_error(set_parser));
+ }
+}
+
+static int set_line(struct mail_storage_service_ctx *ctx,
+ struct mail_storage_service_user *user,
+ const char *line)
+{
+ struct setting_parser_context *set_parser = user->set_parser;
+ const char *key, *orig_key, *append_value = NULL;
+ size_t len;
+ int ret;
+
+ if (strchr(line, '=') == NULL)
+ line = t_strconcat(line, "=yes", NULL);
+ orig_key = key = t_strcut(line, '=');
+
+ len = strlen(key);
+ if (len > 0 && key[len-1] == '+') {
+ /* key+=value */
+ append_value = line + len + 1;
+ key = t_strndup(key, len-1);
+ }
+
+ if (!settings_parse_is_valid_key(set_parser, key)) {
+ /* assume it's a plugin setting */
+ key = t_strconcat("plugin/", key, NULL);
+ line = t_strconcat("plugin/", line, NULL);
+ }
+
+ if (master_service_set_has_config_override(ctx->service, key)) {
+ /* this setting was already overridden with -o parameter */
+ e_debug(user->event, "Ignoring overridden (-o) userdb setting: %s",
+ key);
+ return 1;
+ }
+
+ if (append_value != NULL) {
+ const void *value;
+ enum setting_type type;
+
+ value = settings_parse_get_value(set_parser, key, &type);
+ if (value != NULL && type == SET_STR) {
+ const char *const *strp = value;
+
+ line = t_strdup_printf("%s=%s%s",
+ key, *strp, append_value);
+ } else {
+ e_error(user->event, "Ignoring %s userdb setting. "
+ "'+' can only be used for strings.", orig_key);
+ }
+ }
+
+ ret = settings_parse_line(set_parser, line);
+ if (ret >= 0) {
+ if (strstr(key, "pass") != NULL) {
+ /* possibly a password field (e.g. imapc_password).
+ hide the value. */
+ line = t_strconcat(key, "=<hidden>", NULL);
+ }
+ e_debug(user->event, ret == 0 ?
+ "Unknown userdb setting: %s" :
+ "Added userdb setting: %s", line);
+ }
+ return ret;
+}
+
+static bool validate_chroot(const struct mail_user_settings *user_set,
+ const char *dir)
+{
+ const char *const *chroot_dirs;
+
+ if (*dir == '\0')
+ return FALSE;
+
+ if (*user_set->valid_chroot_dirs == '\0')
+ return FALSE;
+
+ chroot_dirs = t_strsplit(user_set->valid_chroot_dirs, ":");
+ while (*chroot_dirs != NULL) {
+ if (**chroot_dirs != '\0' &&
+ str_begins(dir, *chroot_dirs))
+ return TRUE;
+ chroot_dirs++;
+ }
+ return FALSE;
+}
+
+static int
+user_reply_handle(struct mail_storage_service_ctx *ctx,
+ struct mail_storage_service_user *user,
+ const struct auth_user_reply *reply,
+ const char **error_r)
+{
+ const char *home = reply->home;
+ const char *chroot = reply->chroot;
+ const char *const *str, *line, *p;
+ unsigned int i, count;
+ int ret = 0;
+
+ if (reply->uid != (uid_t)-1) {
+ if (reply->uid == 0) {
+ *error_r = "userdb returned 0 as uid";
+ return -1;
+ }
+ user->uid_source = "userdb lookup";
+ set_keyval(ctx, user, "mail_uid", dec2str(reply->uid));
+ }
+ if (reply->gid != (uid_t)-1) {
+ user->gid_source = "userdb lookup";
+ set_keyval(ctx, user, "mail_gid", dec2str(reply->gid));
+ }
+
+ if (home != NULL && chroot == NULL &&
+ *user->user_set->valid_chroot_dirs != '\0' &&
+ (p = strstr(home, "/./")) != NULL) {
+ /* wu-ftpd like <chroot>/./<home> - check only if there's even
+ a possibility of using them (non-empty valid_chroot_dirs) */
+ chroot = t_strdup_until(home, p);
+ home = p + 2;
+ }
+
+ if (home != NULL)
+ set_keyval(ctx, user, "mail_home", home);
+
+ if (chroot != NULL) {
+ if (!validate_chroot(user->user_set, chroot)) {
+ *error_r = t_strdup_printf(
+ "userdb returned invalid chroot directory: %s "
+ "(see valid_chroot_dirs setting)", chroot);
+ return -1;
+ }
+ set_keyval(ctx, user, "mail_chroot", chroot);
+ }
+
+ user->anonymous = reply->anonymous;
+
+ str = array_get(&reply->extra_fields, &count);
+ for (i = 0; i < count; i++) {
+ line = str[i];
+ if (str_begins(line, "system_groups_user=")) {
+ user->system_groups_user =
+ p_strdup(user->pool, line + 19);
+ } else if (str_begins(line, "chdir=")) {
+ user->chdir_path = p_strdup(user->pool, line+6);
+ } else if (str_begins(line, "nice=")) {
+#ifdef HAVE_SETPRIORITY
+ int n;
+ if (str_to_int(line + 5, &n) < 0) {
+ e_error(user->event,
+ "userdb returned invalid nice value %s",
+ line + 5);
+ } else if (n != 0) {
+ if (setpriority(PRIO_PROCESS, 0, n) < 0)
+ e_error(user->event,
+ "setpriority(%d) failed: %m", n);
+ }
+#endif
+ } else if (str_begins(line, "auth_mech=")) {
+ user->auth_mech = p_strdup(user->pool, line+10);
+ } else if (str_begins(line, "auth_token=")) {
+ user->auth_token = p_strdup(user->pool, line+11);
+ } else if (str_begins(line, "auth_user=")) {
+ user->auth_user = p_strdup(user->pool, line+10);
+ } else if (str_begins(line, "admin=")) {
+ user->admin = line[6] == 'y' || line[6] == 'Y' ||
+ line[6] == '1';
+ } else T_BEGIN {
+ ret = set_line(ctx, user, line);
+ } T_END;
+ if (ret < 0)
+ break;
+ }
+
+ if (ret < 0) {
+ *error_r = t_strdup_printf("Invalid userdb input '%s': %s",
+ str[i], settings_parser_get_error(user->set_parser));
+ }
+ return ret;
+}
+
+static int
+service_auth_userdb_lookup(struct mail_storage_service_ctx *ctx,
+ const struct mail_storage_service_input *input,
+ pool_t pool, const char **user,
+ const char *const **fields_r,
+ const char **error_r)
+{
+ struct auth_user_info info;
+ const char *new_username;
+ int ret;
+
+ i_zero(&info);
+ info.service = input->service != NULL ? input->service :
+ ctx->service->name;
+ info.local_ip = input->local_ip;
+ info.remote_ip = input->remote_ip;
+ info.local_port = input->local_port;
+ info.remote_port = input->remote_port;
+ info.forward_fields = input->forward_fields;
+ info.debug = input->debug;
+
+ ret = auth_master_user_lookup(ctx->conn, *user, &info, pool,
+ &new_username, fields_r);
+ if (ret > 0) {
+ if (strcmp(*user, new_username) != 0) {
+ if (ctx->debug)
+ i_debug("changed username to %s", new_username);
+ *user = t_strdup(new_username);
+ }
+ *user = new_username;
+ } else if (ret == 0)
+ *error_r = "Unknown user";
+ else if (**fields_r != NULL) {
+ *error_r = t_strdup(**fields_r);
+ ret = -2;
+ } else {
+ *error_r = MAIL_ERRSTR_CRITICAL_MSG;
+ }
+ return ret;
+}
+
+static bool parse_uid(const char *str, uid_t *uid_r, const char **error_r)
+{
+ struct passwd pw;
+
+ if (str_to_uid(str, uid_r) == 0)
+ return TRUE;
+
+ switch (i_getpwnam(str, &pw)) {
+ case -1:
+ *error_r = t_strdup_printf("getpwnam(%s) failed: %m", str);
+ return FALSE;
+ case 0:
+ *error_r = t_strconcat("Unknown UNIX UID user: ", str, NULL);
+ return FALSE;
+ default:
+ *uid_r = pw.pw_uid;
+ return TRUE;
+ }
+}
+
+static bool parse_gid(const char *str, gid_t *gid_r, const char **error_r)
+{
+ struct group gr;
+
+ if (str_to_gid(str, gid_r) == 0)
+ return TRUE;
+
+ switch (i_getgrnam(str, &gr)) {
+ case -1:
+ *error_r = t_strdup_printf("getgrnam(%s) failed: %m", str);
+ return FALSE;
+ case 0:
+ *error_r = t_strconcat("Unknown UNIX GID group: ", str, NULL);
+ return FALSE;
+ default:
+ *gid_r = gr.gr_gid;
+ return TRUE;
+ }
+}
+
+static const struct var_expand_table *
+get_var_expand_table(struct master_service *service,
+ struct mail_storage_service_user *user,
+ const struct mail_storage_service_input *input,
+ const struct mail_storage_service_privileges *priv)
+{
+ const char *username = t_strcut(input->username, '@');
+ const char *domain = i_strchr_to_next(input->username, '@');
+ const char *uid = priv == NULL ? NULL :
+ dec2str(priv->uid == (uid_t)-1 ? geteuid() : priv->uid);
+ const char *gid = priv == NULL ? NULL :
+ dec2str(priv->gid == (gid_t)-1 ? getegid() : priv->gid);
+
+ const char *auth_user, *auth_username, *auth_domain;
+ if (user == NULL || user->auth_user == NULL) {
+ auth_user = input->username;
+ auth_username = username;
+ auth_domain = domain;
+ } else {
+ auth_user = user->auth_user;
+ auth_username = t_strcut(user->auth_user, '@');
+ auth_domain = i_strchr_to_next(user->auth_user, '@');
+ }
+
+ const struct var_expand_table stack_tab[] = {
+ { 'u', input->username, "user" },
+ { 'n', username, "username" },
+ { 'd', domain, "domain" },
+ { 's', service->name, "service" },
+ { 'l', net_ip2addr(&input->local_ip), "lip" },
+ { 'r', net_ip2addr(&input->remote_ip), "rip" },
+ { 'p', my_pid, "pid" },
+ { 'i', uid, "uid" },
+ { '\0', gid, "gid" },
+ { '\0', input->session_id, "session" },
+ { '\0', auth_user, "auth_user" },
+ { '\0', auth_username, "auth_username" },
+ { '\0', auth_domain, "auth_domain" },
+ /* aliases: */
+ { '\0', net_ip2addr(&input->local_ip), "local_ip" },
+ { '\0', net_ip2addr(&input->remote_ip), "remote_ip" },
+ { '\0', NULL, NULL }
+ };
+ struct var_expand_table *tab;
+
+ tab = t_malloc_no0(sizeof(stack_tab));
+ memcpy(tab, stack_tab, sizeof(stack_tab));
+ return tab;
+}
+
+const struct var_expand_table *
+mail_storage_service_get_var_expand_table(struct mail_storage_service_ctx *ctx,
+ struct mail_storage_service_input *input)
+{
+ struct mail_storage_service_privileges priv;
+
+ i_zero(&priv);
+ priv.uid = (uid_t)-1;
+ priv.gid = (gid_t)-1;
+ return get_var_expand_table(ctx->service, NULL, input, &priv);
+}
+
+static bool
+user_expand_varstr(struct mail_storage_service_ctx *ctx,
+ struct mail_storage_service_user *user,
+ struct mail_storage_service_privileges *priv,
+ const char *str, const char **value_r, const char **error_r)
+{
+ string_t *value;
+ int ret;
+
+ if (*str == SETTING_STRVAR_EXPANDED[0]) {
+ *value_r = str + 1;
+ return TRUE;
+ }
+
+ i_assert(*str == SETTING_STRVAR_UNEXPANDED[0]);
+
+ value = t_str_new(256);
+ ret = mail_storage_service_var_expand(ctx, value, str + 1, user,
+ &user->input, priv, error_r);
+ *value_r = str_c(value);
+ return ret > 0;
+}
+
+static int
+service_parse_privileges(struct mail_storage_service_ctx *ctx,
+ struct mail_storage_service_user *user,
+ struct mail_storage_service_privileges *priv_r,
+ const char **error_r)
+{
+ const struct mail_user_settings *set = user->user_set;
+ uid_t uid = (uid_t)-1;
+ gid_t gid = (gid_t)-1;
+ const char *error;
+
+ i_zero(priv_r);
+ if (*set->mail_uid != '\0') {
+ if (!parse_uid(set->mail_uid, &uid, error_r)) {
+ *error_r = t_strdup_printf("%s (from %s)", *error_r,
+ user->uid_source);
+ return -1;
+ }
+ if (uid < (uid_t)set->first_valid_uid ||
+ (set->last_valid_uid != 0 &&
+ uid > (uid_t)set->last_valid_uid)) {
+ *error_r = t_strdup_printf(
+ "Mail access for users with UID %s not permitted "
+ "(see first_valid_uid in config file, uid from %s).",
+ dec2str(uid), user->uid_source);
+ return -1;
+ }
+ }
+ priv_r->uid = uid;
+ priv_r->uid_source = user->uid_source;
+
+ if (*set->mail_gid != '\0') {
+ if (!parse_gid(set->mail_gid, &gid, error_r)) {
+ *error_r = t_strdup_printf("%s (from %s)", *error_r,
+ user->gid_source);
+ return -1;
+ }
+ if (gid < (gid_t)set->first_valid_gid ||
+ (set->last_valid_gid != 0 &&
+ gid > (gid_t)set->last_valid_gid)) {
+ *error_r = t_strdup_printf(
+ "Mail access for users with GID %s not permitted "
+ "(see first_valid_gid in config file, gid from %s).",
+ dec2str(gid), user->gid_source);
+ return -1;
+ }
+ }
+ priv_r->gid = gid;
+ priv_r->gid_source = user->gid_source;
+
+ /* variable strings are expanded in mail_user_init(),
+ but we need the home and chroot sooner so do them separately here. */
+ if (!user_expand_varstr(ctx, user, priv_r, user->user_set->mail_home,
+ &priv_r->home, &error)) {
+ *error_r = t_strdup_printf(
+ "Failed to expand mail_home '%s': %s",
+ user->user_set->mail_home, error);
+ return -1;
+ }
+ if (!user_expand_varstr(ctx, user, priv_r, user->user_set->mail_chroot,
+ &priv_r->chroot, &error)) {
+ *error_r = t_strdup_printf(
+ "Failed to expand mail_chroot '%s': %s",
+ user->user_set->mail_chroot, error);
+ return -1;
+ }
+ return 0;
+}
+
+static void mail_storage_service_seteuid_root(void)
+{
+ if (seteuid(0) < 0) {
+ i_fatal("mail-storage-service: "
+ "Failed to restore temporarily dropped root privileges: "
+ "seteuid(0) failed: %m");
+ }
+}
+
+static int
+service_drop_privileges(struct mail_storage_service_user *user,
+ struct mail_storage_service_privileges *priv,
+ bool allow_root, bool keep_setuid_root,
+ bool setenv_only, const char **error_r)
+{
+ const struct mail_user_settings *set = user->user_set;
+ struct restrict_access_settings rset;
+ uid_t current_euid, setuid_uid = 0;
+ const char *cur_chroot, *error;
+
+ current_euid = geteuid();
+ restrict_access_init(&rset);
+ restrict_access_get_env(&rset);
+ rset.allow_setuid_root = keep_setuid_root;
+ if (priv->uid != (uid_t)-1) {
+ rset.uid = priv->uid;
+ rset.uid_source = priv->uid_source;
+ } else if (rset.uid == (uid_t)-1 &&
+ !allow_root && current_euid == 0) {
+ *error_r = "User is missing UID (see mail_uid setting)";
+ return -1;
+ }
+ if (priv->gid != (gid_t)-1) {
+ rset.gid = priv->gid;
+ rset.gid_source = priv->gid_source;
+ } else if (rset.gid == (gid_t)-1 && !allow_root &&
+ set->first_valid_gid > 0 && getegid() == 0) {
+ *error_r = "User is missing GID (see mail_gid setting)";
+ return -1;
+ }
+ if (*set->mail_privileged_group != '\0') {
+ if (!parse_gid(set->mail_privileged_group, &rset.privileged_gid,
+ &error)) {
+ *error_r = t_strdup_printf(
+ "%s (in mail_privileged_group setting)", error);
+ return -1;
+ }
+ }
+ if (*set->mail_access_groups != '\0') {
+ rset.extra_groups = t_strconcat(set->mail_access_groups, ",",
+ rset.extra_groups, NULL);
+ }
+
+ rset.first_valid_gid = set->first_valid_gid;
+ rset.last_valid_gid = set->last_valid_gid;
+ rset.chroot_dir = *priv->chroot == '\0' ? NULL : priv->chroot;
+ rset.system_groups_user = user->system_groups_user;
+
+ cur_chroot = restrict_access_get_current_chroot();
+ if (cur_chroot != NULL) {
+ /* we're already chrooted. make sure the chroots are equal. */
+ if (rset.chroot_dir == NULL) {
+ *error_r = "Process is already chrooted, "
+ "can't un-chroot for this user";
+ return -1;
+ }
+ if (strcmp(rset.chroot_dir, cur_chroot) != 0) {
+ *error_r = t_strdup_printf(
+ "Process is already chrooted to %s, "
+ "can't chroot to %s", cur_chroot, priv->chroot);
+ return -1;
+ }
+ /* chrooting to same directory where we're already chrooted */
+ rset.chroot_dir = NULL;
+ }
+
+ if (!allow_root &&
+ (rset.uid == 0 || (rset.uid == (uid_t)-1 && current_euid == 0))) {
+ *error_r = "Mail access not allowed for root";
+ return -1;
+ }
+
+ if (keep_setuid_root) {
+ if (current_euid != rset.uid && rset.uid != (uid_t)-1) {
+ if (current_euid != 0) {
+ /* we're changing the UID,
+ switch back to root first */
+ mail_storage_service_seteuid_root();
+ }
+ setuid_uid = rset.uid;
+ }
+ rset.uid = (uid_t)-1;
+ allow_root = TRUE;
+ }
+ if (!setenv_only) {
+ restrict_access(&rset, allow_root ? RESTRICT_ACCESS_FLAG_ALLOW_ROOT : 0,
+ *priv->home == '\0' ? NULL : priv->home);
+ } else {
+ restrict_access_set_env(&rset);
+ }
+ if (setuid_uid != 0 && !setenv_only) {
+ if (seteuid(setuid_uid) < 0)
+ i_fatal("mail-storage-service: seteuid(%s) failed: %m",
+ dec2str(setuid_uid));
+ }
+ return 0;
+}
+
+static int
+mail_storage_service_init_post(struct mail_storage_service_ctx *ctx,
+ struct mail_storage_service_user *user,
+ struct mail_storage_service_privileges *priv,
+ const char *session_id_suffix,
+ struct mail_user **mail_user_r,
+ const char **error_r)
+{
+ const char *home = priv->home;
+ struct mail_user_connection_data conn_data;
+ struct mail_user *mail_user;
+ int ret;
+
+ i_zero(&conn_data);
+ conn_data.local_ip = &user->input.local_ip;
+ conn_data.remote_ip = &user->input.remote_ip;
+ conn_data.local_port = user->input.local_port;
+ conn_data.remote_port = user->input.remote_port;
+ conn_data.secured = user->input.conn_secured;
+ conn_data.ssl_secured = user->input.conn_ssl_secured;
+
+ /* NOTE: if more user initialization is added, add it also to
+ mail_user_dup() */
+ mail_user = mail_user_alloc_nodup_set(user->event, user->input.username,
+ user->user_info, user->user_set);
+ mail_user->_service_user = user;
+ mail_storage_service_user_ref(user);
+ mail_user_set_home(mail_user, *home == '\0' ? NULL : home);
+ mail_user_set_vars(mail_user, ctx->service->name, &conn_data);
+ mail_user->uid = priv->uid == (uid_t)-1 ? geteuid() : priv->uid;
+ mail_user->gid = priv->gid == (gid_t)-1 ? getegid() : priv->gid;
+ mail_user->anonymous = user->anonymous;
+ mail_user->admin = user->admin;
+ mail_user->auth_mech = p_strdup(mail_user->pool, user->auth_mech);
+ mail_user->auth_token = p_strdup(mail_user->pool, user->auth_token);
+ mail_user->auth_user = p_strdup(mail_user->pool, user->auth_user);
+ if (user->input.session_create_time != 0) {
+ mail_user->session_create_time =
+ user->input.session_create_time;
+ mail_user->session_restored = TRUE;
+ }
+
+ if (session_id_suffix == NULL) {
+ if (user->session_id_counter++ == 0) {
+ mail_user->session_id =
+ p_strdup(mail_user->pool, user->input.session_id);
+ } else {
+ mail_user->session_id =
+ p_strdup_printf(mail_user->pool, "%s:%u",
+ user->input.session_id,
+ user->session_id_counter);
+ }
+ } else
+ mail_user->session_id =
+ p_strdup_printf(mail_user->pool, "%s:%s",
+ user->input.session_id,
+ session_id_suffix);
+ event_add_str(user->event, "session", mail_user->session_id);
+
+ mail_user->userdb_fields = user->input.userdb_fields == NULL ? NULL :
+ p_strarray_dup(mail_user->pool, user->input.userdb_fields);
+
+ string_t *str = t_str_new(64);
+
+ str_printfa(str, "Effective uid=%s, gid=%s, home=%s",
+ dec2str(geteuid()), dec2str(getegid()), home);
+ if (*priv->chroot != '\0')
+ str_printfa(str, ", chroot=%s", priv->chroot);
+ e_debug(mail_user->event, "%s", str_c(str));
+
+ if ((user->flags & MAIL_STORAGE_SERVICE_FLAG_TEMP_PRIV_DROP) != 0 &&
+ (user->flags & MAIL_STORAGE_SERVICE_FLAG_ENABLE_CORE_DUMPS) == 0) {
+ /* we don't want to write core files to any users' home
+ directories since they could contain information about other
+ users' mails as well. so do no chdiring to home. */
+ } else if ((user->flags & MAIL_STORAGE_SERVICE_FLAG_NO_CHDIR) == 0) {
+ /* If possible chdir to home directory, so that core file
+ could be written in case we crash.
+
+ fallback to chdir()ing to root directory. this is needed
+ because the current directory may not be accessible after
+ dropping privileges, and for example unlink_directory()
+ requires ability to open the current directory. */
+ const char *chdir_path = user->chdir_path != NULL ?
+ user->chdir_path : home;
+
+ if (chdir_path[0] == '\0') {
+ if (chdir("/") < 0)
+ e_error(user->event, "chdir(/) failed: %m");
+ } else if (chdir(chdir_path) < 0) {
+ if (errno == EACCES) {
+ e_error(user->event, "%s",
+ eacces_error_get("chdir",
+ t_strconcat(chdir_path, "/", NULL)));
+ } else if (errno != ENOENT)
+ e_error(user->event, "chdir(%s) failed: %m",
+ chdir_path);
+ else
+ e_debug(mail_user->event, "Home dir not found: %s", chdir_path);
+
+ if (chdir("/") < 0)
+ e_error(user->event, "chdir(/) failed: %m");
+ }
+ }
+
+ T_BEGIN {
+ ret = mail_user_init(mail_user, error_r);
+ } T_END_PASS_STR_IF(ret < 0, error_r);
+ if (ret < 0) {
+ mail_user_unref(&mail_user);
+ return -1;
+ }
+ if ((user->flags & MAIL_STORAGE_SERVICE_FLAG_NO_NAMESPACES) == 0) {
+ if (mail_namespaces_init(mail_user, error_r) < 0) {
+ mail_user_deinit(&mail_user);
+ return -1;
+ }
+ }
+
+ *mail_user_r = mail_user;
+ return 0;
+}
+
+void mail_storage_service_io_activate_user(struct mail_storage_service_user *user)
+{
+ io_loop_context_activate(user->ioloop_ctx);
+}
+
+void mail_storage_service_io_deactivate_user(struct mail_storage_service_user *user)
+{
+ io_loop_context_deactivate(user->ioloop_ctx);
+}
+
+static void
+mail_storage_service_io_activate_user_cb(struct mail_storage_service_user *user)
+{
+ if (user->log_prefix != NULL)
+ i_set_failure_prefix("%s", user->log_prefix);
+}
+
+static void
+mail_storage_service_io_deactivate_user_cb(struct mail_storage_service_user *user)
+{
+ if (user->log_prefix != NULL)
+ i_set_failure_prefix("%s", user->service_ctx->default_log_prefix);
+}
+
+static const char *field_get_default(const char *data)
+{
+ const char *p;
+
+ p = strchr(data, ':');
+ if (p == NULL)
+ return "";
+ else {
+ /* default value given */
+ return p+1;
+ }
+}
+
+const char *mail_storage_service_fields_var_expand(const char *data,
+ const char *const *fields)
+{
+ const char *field_name = t_strcut(data, ':');
+ unsigned int i;
+ size_t field_name_len;
+
+ if (fields == NULL)
+ return field_get_default(data);
+
+ field_name_len = strlen(field_name);
+ for (i = 0; fields[i] != NULL; i++) {
+ if (strncmp(fields[i], field_name, field_name_len) == 0 &&
+ fields[i][field_name_len] == '=')
+ return fields[i] + field_name_len+1;
+ }
+ return field_get_default(data);
+}
+
+static int
+mail_storage_service_input_var_userdb(const char *data, void *context,
+ const char **value_r,
+ const char **error_r ATTR_UNUSED)
+{
+ struct mail_storage_service_user *user = context;
+
+ *value_r = mail_storage_service_fields_var_expand(data,
+ user == NULL ? NULL : user->input.userdb_fields);
+ return 1;
+}
+
+static int
+mail_storage_service_var_expand(struct mail_storage_service_ctx *ctx,
+ string_t *str, const char *format,
+ struct mail_storage_service_user *user,
+ const struct mail_storage_service_input *input,
+ const struct mail_storage_service_privileges *priv,
+ const char **error_r)
+{
+ static const struct var_expand_func_table func_table[] = {
+ { "userdb", mail_storage_service_input_var_userdb },
+ { NULL, NULL }
+ };
+ return var_expand_with_funcs(str, format,
+ get_var_expand_table(ctx->service, user, input, priv),
+ func_table, user, error_r);
+}
+
+const char *
+mail_storage_service_user_get_log_prefix(struct mail_storage_service_user *user)
+{
+ i_assert(user->log_prefix != NULL);
+ return user->log_prefix;
+}
+
+static void
+mail_storage_service_init_log(struct mail_storage_service_ctx *ctx,
+ struct mail_storage_service_user *user,
+ struct mail_storage_service_privileges *priv)
+{
+ const char *error;
+
+ T_BEGIN {
+ string_t *str;
+
+ str = t_str_new(256);
+ (void)mail_storage_service_var_expand(ctx, str,
+ user->user_set->mail_log_prefix,
+ user, &user->input, priv, &error);
+ user->log_prefix = p_strdup(user->pool, str_c(str));
+ } T_END;
+ if ((user->flags & MAIL_STORAGE_SERVICE_FLAG_NO_LOG_INIT) != 0)
+ return;
+
+ ctx->log_initialized = TRUE;
+ master_service_init_log_with_prefix(ctx->service, user->log_prefix);
+ /* replace the whole log prefix with mail_log_prefix */
+ event_replace_log_prefix(user->event, user->log_prefix);
+
+ if (master_service_get_client_limit(master_service) == 1)
+ i_set_failure_send_prefix(user->log_prefix);
+}
+
+static void
+mail_storage_service_time_moved(const struct timeval *old_time,
+ const struct timeval *new_time)
+{
+ long long diff = timeval_diff_usecs(new_time, old_time);
+
+ if (diff > 0) {
+ if ((diff / 1000) > MAX_NOWARN_FORWARD_MSECS)
+ i_warning("Time jumped forwards %lld.%06lld seconds",
+ diff / 1000000, diff % 1000000);
+ return;
+ }
+ diff = -diff;
+
+ if ((diff / 1000) > MAX_TIME_BACKWARDS_SLEEP_MSECS) {
+ i_fatal("Time just moved backwards by %lld.%06lld seconds. "
+ "This might cause a lot of problems, "
+ "so I'll just kill myself now. "
+ "http://wiki2.dovecot.org/TimeMovedBackwards",
+ diff / 1000000, diff % 1000000);
+ } else {
+ i_error("Time just moved backwards by %lld.%06lld seconds. "
+ "I'll sleep now until we're back in present. "
+ "http://wiki2.dovecot.org/TimeMovedBackwards",
+ diff / 1000000, diff % 1000000);
+
+ i_sleep_usecs(diff);
+ }
+}
+
+struct mail_storage_service_ctx *
+mail_storage_service_init(struct master_service *service,
+ const struct setting_parser_info *set_roots[],
+ enum mail_storage_service_flags flags)
+{
+ struct mail_storage_service_ctx *ctx;
+ const char *version;
+ pool_t pool;
+ unsigned int count;
+
+ version = master_service_get_version_string(service);
+ if (version != NULL && strcmp(version, PACKAGE_VERSION) != 0) {
+ i_fatal("Version mismatch: libdovecot-storage.so is '%s', "
+ "while the running Dovecot binary is '%s'",
+ PACKAGE_VERSION, version);
+ }
+
+ if ((flags & MAIL_STORAGE_SERVICE_FLAG_TEMP_PRIV_DROP) != 0 &&
+ getuid() != 0) {
+ /* service { user } isn't root. the permission drop can't be
+ temporary. */
+ flags &= ENUM_NEGATE(MAIL_STORAGE_SERVICE_FLAG_TEMP_PRIV_DROP);
+ }
+
+ (void)umask(0077);
+ io_loop_set_time_moved_callback(current_ioloop,
+ mail_storage_service_time_moved);
+
+ mail_storage_init();
+
+ pool = pool_alloconly_create("mail storage service", 2048);
+ ctx = p_new(pool, struct mail_storage_service_ctx, 1);
+ ctx->pool = pool;
+ ctx->service = service;
+ ctx->flags = flags;
+
+ /* @UNSAFE */
+ if (set_roots == NULL)
+ count = 0;
+ else
+ for (count = 0; set_roots[count] != NULL; count++) ;
+ ctx->set_roots =
+ p_new(pool, const struct setting_parser_info *, count + 2);
+ ctx->set_roots[0] = &mail_user_setting_parser_info;
+ if (set_roots != NULL) {
+ memcpy(ctx->set_roots + 1, set_roots,
+ sizeof(*ctx->set_roots) * count);
+ }
+
+ /* note: we may not have read any settings yet, so this logging
+ may still be going to wrong location */
+ const char *configured_name =
+ master_service_get_configured_name(service);
+ ctx->default_log_prefix =
+ p_strdup_printf(pool, "%s(%s): ", configured_name, my_pid);
+
+ /* do all the global initialization. delay initializing plugins until
+ we drop privileges the first time. */
+ if ((flags & MAIL_STORAGE_SERVICE_FLAG_NO_LOG_INIT) == 0)
+ master_service_init_log_with_prefix(service, ctx->default_log_prefix);
+ dict_drivers_register_builtin();
+ if (storage_service_global == NULL)
+ storage_service_global = ctx;
+ return ctx;
+}
+
+struct auth_master_connection *
+mail_storage_service_get_auth_conn(struct mail_storage_service_ctx *ctx)
+{
+ i_assert(ctx->conn != NULL);
+ return ctx->conn;
+}
+
+static enum mail_storage_service_flags
+mail_storage_service_input_get_flags(struct mail_storage_service_ctx *ctx,
+ const struct mail_storage_service_input *input)
+{
+ enum mail_storage_service_flags flags;
+
+ flags = (ctx->flags & ENUM_NEGATE(input->flags_override_remove)) |
+ input->flags_override_add;
+ if (input->no_userdb_lookup) {
+ /* FIXME: for API backwards compatibility only */
+ flags &= ENUM_NEGATE(MAIL_STORAGE_SERVICE_FLAG_USERDB_LOOKUP);
+ }
+ return flags;
+}
+
+int mail_storage_service_read_settings(struct mail_storage_service_ctx *ctx,
+ const struct mail_storage_service_input *input,
+ pool_t pool,
+ const struct setting_parser_info **user_info_r,
+ const struct setting_parser_context **parser_r,
+ const char **error_r)
+{
+ struct master_service_settings_input set_input;
+ const struct setting_parser_info *const *roots;
+ struct master_service_settings_output set_output;
+ const struct dynamic_settings_parser *dyn_parsers;
+ enum mail_storage_service_flags flags;
+ unsigned int i;
+
+ ctx->config_permission_denied = FALSE;
+
+ flags = input == NULL ? ctx->flags :
+ mail_storage_service_input_get_flags(ctx, input);
+
+ i_zero(&set_input);
+ set_input.roots = ctx->set_roots;
+ set_input.preserve_user = TRUE;
+ /* settings reader may exec doveconf, which is going to clear
+ environment, and if we're not doing a userdb lookup we want to
+ use $HOME */
+ set_input.preserve_home =
+ (flags & MAIL_STORAGE_SERVICE_FLAG_USERDB_LOOKUP) == 0;
+ set_input.use_sysexits =
+ (flags & MAIL_STORAGE_SERVICE_FLAG_USE_SYSEXITS) != 0;
+ set_input.no_ssl_ca =
+ (flags & MAIL_STORAGE_SERVICE_FLAG_NO_SSL_CA) != 0;
+
+ if (input != NULL) {
+ set_input.module = input->module;
+ set_input.service = input->service;
+ set_input.username = input->username;
+ set_input.local_ip = input->local_ip;
+ set_input.remote_ip = input->remote_ip;
+ }
+ if (input == NULL) {
+ /* global settings read - don't create a cache for thi */
+ } else if (ctx->set_cache == NULL) {
+ ctx->set_cache_module = p_strdup(ctx->pool, set_input.module);
+ ctx->set_cache_service = p_strdup(ctx->pool, set_input.service);
+ ctx->set_cache = master_service_settings_cache_init(
+ ctx->service, set_input.module, set_input.service);
+ } else {
+ /* already looked up settings at least once.
+ we really shouldn't be execing anymore. */
+ set_input.never_exec = TRUE;
+ }
+
+ dyn_parsers = mail_storage_get_dynamic_parsers(pool);
+ if (null_strcmp(set_input.module, ctx->set_cache_module) == 0 &&
+ null_strcmp(set_input.service, ctx->set_cache_service) == 0 &&
+ ctx->set_cache != NULL) {
+ if (master_service_settings_cache_read(ctx->set_cache,
+ &set_input, dyn_parsers,
+ parser_r, error_r) < 0) {
+ *error_r = t_strdup_printf(
+ "Error reading configuration: %s", *error_r);
+ return -1;
+ }
+ } else {
+ settings_parser_dyn_update(pool, &set_input.roots, dyn_parsers);
+ if (master_service_settings_read(ctx->service, &set_input,
+ &set_output, error_r) < 0) {
+ *error_r = t_strdup_printf(
+ "Error reading configuration: %s", *error_r);
+ ctx->config_permission_denied =
+ set_output.permission_denied;
+ return -1;
+ }
+ *parser_r = ctx->service->set_parser;
+ }
+
+ roots = settings_parser_get_roots(*parser_r);
+ for (i = 0; roots[i] != NULL; i++) {
+ if (strcmp(roots[i]->module_name,
+ mail_user_setting_parser_info.module_name) == 0) {
+ *user_info_r = roots[i];
+ return 0;
+ }
+ }
+ i_unreached();
+ return -1;
+}
+
+void mail_storage_service_set_auth_conn(struct mail_storage_service_ctx *ctx,
+ struct auth_master_connection *conn)
+{
+ i_assert(ctx->conn == NULL);
+ i_assert(mail_user_auth_master_conn == NULL);
+
+ ctx->conn = conn;
+ mail_user_auth_master_conn = conn;
+}
+
+static void
+mail_storage_service_first_init(struct mail_storage_service_ctx *ctx,
+ const struct setting_parser_info *user_info,
+ const struct mail_user_settings *user_set,
+ enum mail_storage_service_flags service_flags)
+{
+ enum auth_master_flags flags = 0;
+
+ ctx->debug = mail_user_set_get_mail_debug(user_info, user_set) ||
+ (service_flags & MAIL_STORAGE_SERVICE_FLAG_DEBUG) != 0;
+ if (ctx->debug)
+ flags |= AUTH_MASTER_FLAG_DEBUG;
+ if ((service_flags & MAIL_STORAGE_SERVICE_FLAG_NO_IDLE_TIMEOUT) != 0)
+ flags |= AUTH_MASTER_FLAG_NO_IDLE_TIMEOUT;
+ mail_storage_service_set_auth_conn(ctx,
+ auth_master_init(user_set->auth_socket_path, flags));
+}
+
+static int
+mail_storage_service_load_modules(struct mail_storage_service_ctx *ctx,
+ const struct setting_parser_info *user_info,
+ const struct mail_user_settings *user_set,
+ const char **error_r)
+{
+ struct module_dir_load_settings mod_set;
+
+ if (*user_set->mail_plugins == '\0')
+ return 0;
+ if ((ctx->flags & MAIL_STORAGE_SERVICE_FLAG_NO_PLUGINS) != 0)
+ return 0;
+
+ i_zero(&mod_set);
+ mod_set.abi_version = DOVECOT_ABI_VERSION;
+ mod_set.binary_name = master_service_get_name(ctx->service);
+ mod_set.setting_name = "mail_plugins";
+ mod_set.require_init_funcs = TRUE;
+ mod_set.debug = mail_user_set_get_mail_debug(user_info, user_set);
+
+ return module_dir_try_load_missing(&mail_storage_service_modules,
+ user_set->mail_plugin_dir,
+ user_set->mail_plugins,
+ &mod_set, error_r);
+}
+
+static int extra_field_key_cmp_p(const char *const *s1, const char *const *s2)
+{
+ const char *p1 = *s1, *p2 = *s2;
+
+ for (; *p1 == *p2; p1++, p2++) {
+ if (*p1 == '\0')
+ return 0;
+ }
+ if (*p1 == '=')
+ return -1;
+ if (*p2 == '=')
+ return 1;
+ return *p1 - *p2;
+}
+
+static void
+mail_storage_service_set_log_prefix(struct mail_storage_service_ctx *ctx,
+ const struct mail_user_settings *user_set,
+ struct mail_storage_service_user *user,
+ const struct mail_storage_service_input *input,
+ const struct mail_storage_service_privileges *priv)
+{
+ string_t *str;
+ const char *error;
+
+ str = t_str_new(256);
+ (void)mail_storage_service_var_expand(ctx, str, user_set->mail_log_prefix,
+ user, input, priv, &error);
+ i_set_failure_prefix("%s", str_c(str));
+}
+
+static const char *
+mail_storage_service_generate_session_id(pool_t pool, const char *prefix)
+{
+ guid_128_t guid;
+ size_t prefix_len = prefix == NULL ? 0 : strlen(prefix);
+ string_t *str = str_new(pool, MAX_BASE64_ENCODED_SIZE(prefix_len + 1 + sizeof(guid)));
+
+ if (prefix != NULL)
+ str_printfa(str, "%s:", prefix);
+
+ guid_128_generate(guid);
+ base64_encode(guid, sizeof(guid), str);
+ /* remove the trailing "==" */
+ i_assert(str_data(str)[str_len(str)-2] == '=');
+ str_truncate(str, str_len(str)-2);
+ return str_c(str);
+
+}
+
+static int
+mail_storage_service_lookup_real(struct mail_storage_service_ctx *ctx,
+ const struct mail_storage_service_input *input,
+ bool update_log_prefix,
+ struct mail_storage_service_user **user_r,
+ const char **error_r)
+{
+ enum mail_storage_service_flags flags;
+ struct mail_storage_service_user *user;
+ const char *username = input->username;
+ const struct setting_parser_info *user_info;
+ const struct mail_user_settings *user_set;
+ const char *const *userdb_fields, *error;
+ struct auth_user_reply reply;
+ const struct setting_parser_context *set_parser;
+ void **sets;
+ pool_t user_pool, temp_pool;
+ int ret = 1;
+
+ user_pool = pool_alloconly_create(MEMPOOL_GROWING"mail storage service user", 1024*6);
+ flags = mail_storage_service_input_get_flags(ctx, input);
+
+ if ((flags & MAIL_STORAGE_SERVICE_FLAG_TEMP_PRIV_DROP) != 0 &&
+ geteuid() != 0) {
+ /* we dropped privileges only temporarily. switch back to root
+ before reading settings, so we'll definitely have enough
+ permissions to connect to the config socket. */
+ mail_storage_service_seteuid_root();
+ }
+
+ if (mail_storage_service_read_settings(ctx, input, user_pool,
+ &user_info, &set_parser,
+ error_r) < 0) {
+ if (ctx->config_permission_denied) {
+ /* just restart and maybe next time we will open the
+ config socket before dropping privileges */
+ i_fatal("%s", *error_r);
+ }
+ pool_unref(&user_pool);
+ return -1;
+ }
+
+ if ((flags & MAIL_STORAGE_SERVICE_FLAG_NO_LOG_INIT) == 0 &&
+ !ctx->log_initialized) {
+ /* initialize logging again, in case we only read the
+ settings for the first above */
+ ctx->log_initialized = TRUE;
+ master_service_init_log_with_prefix(ctx->service,
+ ctx->default_log_prefix);
+ update_log_prefix = TRUE;
+ }
+ sets = master_service_settings_parser_get_others(master_service,
+ set_parser);
+ user_set = sets[0];
+
+ if (update_log_prefix)
+ mail_storage_service_set_log_prefix(ctx, user_set, NULL, input, NULL);
+
+ if (ctx->conn == NULL)
+ mail_storage_service_first_init(ctx, user_info, user_set, flags);
+ /* load global plugins */
+ if (mail_storage_service_load_modules(ctx, user_info, user_set, error_r) < 0) {
+ pool_unref(&user_pool);
+ return -1;
+ }
+
+ if (ctx->userdb_next_pool == NULL)
+ temp_pool = pool_alloconly_create("userdb lookup", 2048);
+ else {
+ temp_pool = ctx->userdb_next_pool;
+ ctx->userdb_next_pool = NULL;
+ pool_ref(temp_pool);
+ }
+ if ((flags & MAIL_STORAGE_SERVICE_FLAG_USERDB_LOOKUP) != 0) {
+ ret = service_auth_userdb_lookup(ctx, input, temp_pool,
+ &username, &userdb_fields, error_r);
+ if (ret <= 0) {
+ pool_unref(&temp_pool);
+ pool_unref(&user_pool);
+ return ret;
+ }
+ if (ctx->userdb_next_fieldsp != NULL)
+ *ctx->userdb_next_fieldsp = userdb_fields;
+ } else {
+ userdb_fields = input->userdb_fields;
+ }
+
+ user = p_new(user_pool, struct mail_storage_service_user, 1);
+ user->refcount = 1;
+ user->service_ctx = ctx;
+ user->pool = user_pool;
+ user->input = *input;
+ user->input.userdb_fields = userdb_fields == NULL ? NULL :
+ p_strarray_dup(user_pool, userdb_fields);
+ user->input.username = p_strdup(user_pool, username);
+ user->input.session_id = p_strdup(user_pool, input->session_id);
+ if (user->input.session_id == NULL) {
+ user->input.session_id =
+ mail_storage_service_generate_session_id(user_pool,
+ input->session_id_prefix);
+ }
+ user->input.session_create_time = input->session_create_time;
+ user->user_info = user_info;
+ user->flags = flags;
+
+ user->set_parser = settings_parser_dup(set_parser, user_pool);
+
+ sets = master_service_settings_parser_get_others(master_service,
+ user->set_parser);
+ user->user_set = sets[0];
+ user->ssl_set = master_service_ssl_settings_get_from_parser(user->set_parser);
+ user->gid_source = "mail_gid setting";
+ user->uid_source = "mail_uid setting";
+ /* Create an event that will be used as the default event for logging.
+ This event won't be a parent to any other events - mail_user.event
+ will be used for that. */
+ user->event = event_create(input->event_parent);
+ event_set_forced_debug(user->event,
+ user->service_ctx->debug || (flags & MAIL_STORAGE_SERVICE_FLAG_DEBUG) != 0);
+ event_add_fields(user->event, (const struct event_add_field []){
+ { .key = "user", .value = user->input.username },
+ { .key = "session", .value = user->input.session_id },
+ { .key = NULL }
+ });
+
+ if ((flags & MAIL_STORAGE_SERVICE_FLAG_DEBUG) != 0)
+ (void)settings_parse_line(user->set_parser, "mail_debug=yes");
+
+ if ((flags & MAIL_STORAGE_SERVICE_FLAG_USERDB_LOOKUP) == 0) {
+ const char *home = getenv("HOME");
+ if (home != NULL)
+ set_keyval(ctx, user, "mail_home", home);
+ }
+
+ if (userdb_fields != NULL) {
+ auth_user_fields_parse(userdb_fields, temp_pool, &reply);
+ array_sort(&reply.extra_fields, extra_field_key_cmp_p);
+ if (user_reply_handle(ctx, user, &reply, &error) < 0) {
+ *error_r = t_strdup_printf(
+ "Invalid settings in userdb: %s", error);
+ ret = -2;
+ }
+ }
+ if (ret > 0 && !settings_parser_check(user->set_parser, user_pool, &error)) {
+ *error_r = t_strdup_printf(
+ "Invalid settings (probably caused by userdb): %s", error);
+ ret = -2;
+ }
+ pool_unref(&temp_pool);
+
+ /* load per-user plugins */
+ if (ret > 0) {
+ if (mail_storage_service_load_modules(ctx, user_info,
+ user->user_set,
+ error_r) < 0) {
+ ret = -2;
+ }
+ }
+ if ((ctx->flags & MAIL_STORAGE_SERVICE_FLAG_NO_PLUGINS) != 0 &&
+ user_set->mail_plugins[0] != '\0') {
+ /* mail_storage_service_load_modules() already avoids loading
+ plugins when the _NO_PLUGINS flag is set. However, it's
+ possible that the plugins are already loaded, because the
+ plugin loading is a global state. This is especially true
+ with doveadm, which loads the mail_plugins immediately at
+ startup so it can find commands registered by plugins. It's
+ fine that extra plugins are loaded - we'll just need to
+ prevent any of their hooks from being called. One easy way
+ to do this is just to clear out the mail_plugins setting: */
+ (void)settings_parse_line(user->set_parser, "mail_plugins=");
+ }
+
+ if (ret < 0)
+ mail_storage_service_user_unref(&user);
+ *user_r = user;
+ return ret;
+}
+
+int mail_storage_service_lookup(struct mail_storage_service_ctx *ctx,
+ const struct mail_storage_service_input *input,
+ struct mail_storage_service_user **user_r,
+ const char **error_r)
+{
+ char *old_log_prefix = i_strdup(i_get_failure_prefix());
+ bool update_log_prefix;
+ int ret;
+
+ if (io_loop_get_current_context(current_ioloop) == NULL) {
+ /* no user yet. log prefix should be just "imap:" or something
+ equally unhelpful. we don't know the proper log format yet,
+ but initialize it to something better until we know it. */
+ const char *session_id =
+ input->session_id != NULL ? input->session_id :
+ (input->session_id_prefix != NULL ?
+ input->session_id_prefix : NULL);
+ i_set_failure_prefix("%s(%s%s%s): ",
+ master_service_get_name(ctx->service), input->username,
+ session_id == NULL ? "" : t_strdup_printf(",%s", session_id),
+ input->remote_ip.family == 0 ? "" :
+ t_strdup_printf(",%s", net_ip2addr(&input->remote_ip)));
+ update_log_prefix = TRUE;
+ } else {
+ /* we might be here because we're doing a user lookup for a
+ shared user. the log prefix is likely already usable, so
+ just append our own without replacing the whole thing. */
+ i_set_failure_prefix("%suser-lookup(%s): ",
+ old_log_prefix, input->username);
+ update_log_prefix = FALSE;
+ }
+
+ T_BEGIN {
+ ret = mail_storage_service_lookup_real(ctx, input,
+ update_log_prefix, user_r, error_r);
+ } T_END_PASS_STR_IF(ret < 0, error_r);
+ i_set_failure_prefix("%s", old_log_prefix);
+ i_free(old_log_prefix);
+ return ret;
+}
+
+void mail_storage_service_save_userdb_fields(struct mail_storage_service_ctx *ctx,
+ pool_t pool, const char *const **userdb_fields_r)
+{
+ i_assert(pool != NULL);
+ i_assert(userdb_fields_r != NULL);
+
+ ctx->userdb_next_pool = pool;
+ ctx->userdb_next_fieldsp = userdb_fields_r;
+ *userdb_fields_r = NULL;
+}
+
+static int
+mail_storage_service_next_real(struct mail_storage_service_ctx *ctx,
+ struct mail_storage_service_user *user,
+ const char *session_id_suffix,
+ struct mail_user **mail_user_r,
+ const char **error_r)
+{
+ struct mail_storage_service_privileges priv;
+ const char *error;
+ size_t len;
+ bool allow_root =
+ (user->flags & MAIL_STORAGE_SERVICE_FLAG_ALLOW_ROOT) != 0;
+ bool temp_priv_drop =
+ (user->flags & MAIL_STORAGE_SERVICE_FLAG_TEMP_PRIV_DROP) != 0;
+ bool use_chroot;
+
+ if (service_parse_privileges(ctx, user, &priv, error_r) < 0)
+ return -2;
+
+ if (*priv.home != '/' && *priv.home != '\0') {
+ *error_r = t_strdup_printf(
+ "Relative home directory paths not supported: %s",
+ priv.home);
+ return -2;
+ }
+
+ /* we can't chroot if we want to switch between users. there's
+ not much point either (from security point of view). but if we're
+ already chrooted, we'll just have to continue and hope that the
+ current chroot is the same as the wanted chroot */
+ use_chroot = !temp_priv_drop ||
+ restrict_access_get_current_chroot() != NULL;
+
+ len = strlen(priv.chroot);
+ if (len > 2 && strcmp(priv.chroot + len - 2, "/.") == 0 &&
+ strncmp(priv.home, priv.chroot, len - 2) == 0) {
+ /* mail_chroot = /chroot/. means that the home dir already
+ contains the chroot dir. remove it from home. */
+ if (use_chroot) {
+ priv.home += len - 2;
+ if (*priv.home == '\0')
+ priv.home = "/";
+ priv.chroot = t_strndup(priv.chroot, len - 2);
+
+ set_keyval(ctx, user, "mail_home", priv.home);
+ set_keyval(ctx, user, "mail_chroot", priv.chroot);
+ }
+ } else if (len > 0 && !use_chroot) {
+ /* we're not going to chroot. fix home directory so we can
+ access it. */
+ if (*priv.home == '\0' || strcmp(priv.home, "/") == 0)
+ priv.home = priv.chroot;
+ else
+ priv.home = t_strconcat(priv.chroot, priv.home, NULL);
+ priv.chroot = "";
+ set_keyval(ctx, user, "mail_home", priv.home);
+ }
+
+ mail_storage_service_init_log(ctx, user, &priv);
+
+ /* create ioloop context regardless of logging. it's also used by
+ stats plugin. */
+ if (user->ioloop_ctx == NULL) {
+ user->ioloop_ctx = io_loop_context_new(current_ioloop);
+ io_loop_context_add_callbacks(user->ioloop_ctx,
+ mail_storage_service_io_activate_user_cb,
+ mail_storage_service_io_deactivate_user_cb,
+ user);
+ }
+ io_loop_context_switch(user->ioloop_ctx);
+
+ if ((user->flags & MAIL_STORAGE_SERVICE_FLAG_NO_RESTRICT_ACCESS) == 0) {
+ if (service_drop_privileges(user, &priv,
+ allow_root, temp_priv_drop,
+ FALSE, &error) < 0) {
+ *error_r = t_strdup_printf(
+ "Couldn't drop privileges: %s", error);
+ mail_storage_service_io_deactivate_user(user);
+ return -1;
+ }
+ if (!temp_priv_drop ||
+ (user->flags & MAIL_STORAGE_SERVICE_FLAG_ENABLE_CORE_DUMPS) != 0)
+ restrict_access_allow_coredumps(TRUE);
+ }
+
+ /* privileges are dropped. initialize plugins that haven't been
+ initialized yet. */
+ module_dir_init(mail_storage_service_modules);
+
+ if (mail_storage_service_init_post(ctx, user, &priv,
+ session_id_suffix,
+ mail_user_r, error_r) < 0) {
+ mail_storage_service_io_deactivate_user(user);
+ return -2;
+ }
+ return 0;
+}
+
+int mail_storage_service_next(struct mail_storage_service_ctx *ctx,
+ struct mail_storage_service_user *user,
+ struct mail_user **mail_user_r,
+ const char **error_r)
+{
+ return mail_storage_service_next_with_session_suffix(ctx,
+ user,
+ NULL,
+ mail_user_r,
+ error_r);
+}
+
+int mail_storage_service_next_with_session_suffix(struct mail_storage_service_ctx *ctx,
+ struct mail_storage_service_user *user,
+ const char *session_id_suffix,
+ struct mail_user **mail_user_r,
+ const char **error_r)
+{
+ char *old_log_prefix = i_strdup(i_get_failure_prefix());
+ int ret;
+
+ mail_storage_service_set_log_prefix(ctx, user->user_set, user,
+ &user->input, NULL);
+ i_set_failure_prefix("%s", old_log_prefix);
+ T_BEGIN {
+ ret = mail_storage_service_next_real(ctx, user,
+ session_id_suffix,
+ mail_user_r, error_r);
+ } T_END_PASS_STR_IF(ret < 0, error_r);
+ if ((user->flags & MAIL_STORAGE_SERVICE_FLAG_NO_LOG_INIT) != 0)
+ i_set_failure_prefix("%s", old_log_prefix);
+ i_free(old_log_prefix);
+ return ret;
+}
+
+void mail_storage_service_restrict_setenv(struct mail_storage_service_ctx *ctx,
+ struct mail_storage_service_user *user)
+{
+ struct mail_storage_service_privileges priv;
+ const char *error;
+
+ if (service_parse_privileges(ctx, user, &priv, &error) < 0)
+ i_fatal("user %s: %s", user->input.username, error);
+ if (service_drop_privileges(user, &priv,
+ TRUE, FALSE, TRUE, &error) < 0)
+ i_fatal("user %s: %s", user->input.username, error);
+}
+
+int mail_storage_service_lookup_next(struct mail_storage_service_ctx *ctx,
+ const struct mail_storage_service_input *input,
+ struct mail_storage_service_user **user_r,
+ struct mail_user **mail_user_r,
+ const char **error_r)
+{
+ struct mail_storage_service_user *user;
+ int ret;
+
+ ret = mail_storage_service_lookup(ctx, input, &user, error_r);
+ if (ret <= 0)
+ return ret;
+
+ ret = mail_storage_service_next(ctx, user, mail_user_r, error_r);
+ if (ret < 0) {
+ mail_storage_service_user_unref(&user);
+ return ret;
+ }
+ *user_r = user;
+ return 1;
+}
+
+void mail_storage_service_user_ref(struct mail_storage_service_user *user)
+{
+ i_assert(user->refcount > 0);
+ user->refcount++;
+}
+
+void mail_storage_service_user_unref(struct mail_storage_service_user **_user)
+{
+ struct mail_storage_service_user *user = *_user;
+
+ *_user = NULL;
+
+ i_assert(user->refcount > 0);
+ if (--user->refcount > 0)
+ return;
+
+ if (user->ioloop_ctx != NULL) {
+ if (io_loop_get_current_context(current_ioloop) == user->ioloop_ctx)
+ mail_storage_service_io_deactivate_user(user);
+ io_loop_context_remove_callbacks(user->ioloop_ctx,
+ mail_storage_service_io_activate_user_cb,
+ mail_storage_service_io_deactivate_user_cb, user);
+ io_loop_context_unref(&user->ioloop_ctx);
+ }
+
+ settings_parser_deinit(&user->set_parser);
+ event_unref(&user->event);
+ pool_unref(&user->pool);
+}
+
+void mail_storage_service_init_settings(struct mail_storage_service_ctx *ctx,
+ const struct mail_storage_service_input *input)
+{
+ const struct setting_parser_info *user_info;
+ const struct mail_user_settings *user_set;
+ const struct setting_parser_context *set_parser;
+ const char *error;
+ pool_t temp_pool;
+ void **sets;
+
+ if (ctx->conn != NULL)
+ return;
+
+ temp_pool = pool_alloconly_create("service all settings", 4096);
+ if (mail_storage_service_read_settings(ctx, input, temp_pool,
+ &user_info, &set_parser,
+ &error) < 0)
+ i_fatal("%s", error);
+ sets = master_service_settings_parser_get_others(master_service,
+ set_parser);
+ user_set = sets[0];
+
+ mail_storage_service_first_init(ctx, user_info, user_set, ctx->flags);
+ pool_unref(&temp_pool);
+}
+
+static int
+mail_storage_service_all_iter_deinit(struct mail_storage_service_ctx *ctx)
+{
+ int ret = 0;
+
+ if (ctx->auth_list != NULL) {
+ ret = auth_master_user_list_deinit(&ctx->auth_list);
+ auth_master_deinit(&ctx->iter_conn);
+ }
+ return ret;
+}
+
+void mail_storage_service_all_init_mask(struct mail_storage_service_ctx *ctx,
+ const char *user_mask_hint)
+{
+ enum auth_master_flags flags = 0;
+
+ (void)mail_storage_service_all_iter_deinit(ctx);
+ mail_storage_service_init_settings(ctx, NULL);
+
+ /* create a new connection, because the iteration might take a while
+ and we might want to do USER lookups during it, which don't mix
+ well in the same connection. */
+ if (ctx->debug)
+ flags |= AUTH_MASTER_FLAG_DEBUG;
+ ctx->iter_conn = auth_master_init(auth_master_get_socket_path(ctx->conn),
+ flags);
+ ctx->auth_list = auth_master_user_list_init(ctx->iter_conn,
+ user_mask_hint, NULL);
+}
+
+int mail_storage_service_all_next(struct mail_storage_service_ctx *ctx,
+ const char **username_r)
+{
+ i_assert((ctx->flags & MAIL_STORAGE_SERVICE_FLAG_USERDB_LOOKUP) != 0);
+
+ *username_r = auth_master_user_list_next(ctx->auth_list);
+ if (*username_r != NULL)
+ return 1;
+ return mail_storage_service_all_iter_deinit(ctx);
+}
+
+void mail_storage_service_deinit(struct mail_storage_service_ctx **_ctx)
+{
+ struct mail_storage_service_ctx *ctx = *_ctx;
+
+ *_ctx = NULL;
+ (void)mail_storage_service_all_iter_deinit(ctx);
+ if (ctx->conn != NULL) {
+ if (mail_user_auth_master_conn == ctx->conn)
+ mail_user_auth_master_conn = NULL;
+ auth_master_deinit(&ctx->conn);
+ }
+ if (ctx->set_cache != NULL)
+ master_service_settings_cache_deinit(&ctx->set_cache);
+
+ if (storage_service_global == ctx)
+ storage_service_global = NULL;
+ pool_unref(&ctx->pool);
+
+ module_dir_unload(&mail_storage_service_modules);
+ mail_storage_deinit();
+ dict_drivers_unregister_builtin();
+}
+
+struct mail_storage_service_ctx *mail_storage_service_get_global(void)
+{
+ return storage_service_global;
+}
+
+void **mail_storage_service_user_get_set(struct mail_storage_service_user *user)
+{
+ return master_service_settings_parser_get_others(master_service,
+ user->set_parser);
+}
+
+const struct mail_storage_settings *
+mail_storage_service_user_get_mail_set(struct mail_storage_service_user *user)
+{
+ return mail_user_set_get_driver_settings(
+ user->user_info, user->user_set,
+ MAIL_STORAGE_SET_DRIVER_NAME);
+}
+
+const struct mail_storage_service_input *
+mail_storage_service_user_get_input(struct mail_storage_service_user *user)
+{
+ return &user->input;
+}
+
+struct setting_parser_context *
+mail_storage_service_user_get_settings_parser(struct mail_storage_service_user *user)
+{
+ return user->set_parser;
+}
+
+const struct master_service_ssl_settings *
+mail_storage_service_user_get_ssl_settings(struct mail_storage_service_user *user)
+{
+ return user->ssl_set;
+}
+
+struct mail_storage_service_ctx *
+mail_storage_service_user_get_service_ctx(struct mail_storage_service_user *user)
+{
+ return user->service_ctx;
+}
+
+pool_t mail_storage_service_user_get_pool(struct mail_storage_service_user *user)
+{
+ return user->pool;
+}
+
+void *mail_storage_service_get_settings(struct master_service *service)
+{
+ void **sets, *set;
+
+ T_BEGIN {
+ sets = master_service_settings_get_others(service);
+ set = sets[1];
+ } T_END;
+ return set;
+}
+
+int mail_storage_service_user_set_setting(struct mail_storage_service_user *user,
+ const char *key,
+ const char *value,
+ const char **error_r)
+{
+ int ret = settings_parse_keyvalue(user->set_parser, key, value);
+ *error_r = settings_parser_get_error(user->set_parser);
+ return ret;
+}
+
+const char *
+mail_storage_service_get_log_prefix(struct mail_storage_service_ctx *ctx)
+{
+ return ctx->default_log_prefix;
+}
diff --git a/src/lib-storage/mail-storage-service.h b/src/lib-storage/mail-storage-service.h
new file mode 100644
index 0000000..31508e8
--- /dev/null
+++ b/src/lib-storage/mail-storage-service.h
@@ -0,0 +1,187 @@
+#ifndef MAIL_STORAGE_SERVICE_H
+#define MAIL_STORAGE_SERVICE_H
+
+#include "net.h"
+
+struct master_service;
+struct mail_user;
+struct setting_parser_context;
+struct setting_parser_info;
+struct mail_storage_service_user;
+
+enum mail_storage_service_flags {
+ /* Allow not dropping root privileges */
+ MAIL_STORAGE_SERVICE_FLAG_ALLOW_ROOT = 0x01,
+ /* Lookup user from userdb */
+ MAIL_STORAGE_SERVICE_FLAG_USERDB_LOOKUP = 0x02,
+ /* Force mail_debug=yes */
+ MAIL_STORAGE_SERVICE_FLAG_DEBUG = 0x04,
+ /* Keep the current process permissions */
+ MAIL_STORAGE_SERVICE_FLAG_NO_RESTRICT_ACCESS = 0x08,
+ /* Don't chdir() to user's home */
+ MAIL_STORAGE_SERVICE_FLAG_NO_CHDIR = 0x10,
+ /* Drop privileges only temporarily (keep running as setuid-root) */
+ MAIL_STORAGE_SERVICE_FLAG_TEMP_PRIV_DROP = 0x20,
+ /* Enable core dumps even when dropping privileges temporarily */
+ MAIL_STORAGE_SERVICE_FLAG_ENABLE_CORE_DUMPS = 0x40,
+ /* Don't initialize logging or change log prefixes */
+ MAIL_STORAGE_SERVICE_FLAG_NO_LOG_INIT = 0x80,
+ /* Don't load plugins in _service_lookup() */
+ MAIL_STORAGE_SERVICE_FLAG_NO_PLUGINS = 0x100,
+ /* Don't close auth connections because of idling. */
+ MAIL_STORAGE_SERVICE_FLAG_NO_IDLE_TIMEOUT = 0x200,
+ /* When executing doveconf, tell it to use sysexits codes */
+ MAIL_STORAGE_SERVICE_FLAG_USE_SYSEXITS = 0x400,
+ /* Don't create namespaces, only the user. */
+ MAIL_STORAGE_SERVICE_FLAG_NO_NAMESPACES = 0x800,
+ /* Disable reading ssl_ca setting to save memory. */
+ MAIL_STORAGE_SERVICE_FLAG_NO_SSL_CA = 0x1000,
+};
+
+struct mail_storage_service_input {
+ struct event *event_parent;
+
+ const char *module;
+ const char *service;
+ const char *username;
+ /* If set, use this string as the session ID */
+ const char *session_id;
+ /* If set, use this string as the session ID prefix, but also append
+ a unique session ID suffix to it. */
+ const char *session_id_prefix;
+ /* If non-zero, override timestamp when session was created and set
+ mail_user.session_restored=TRUE */
+ time_t session_create_time;
+
+ struct ip_addr local_ip, remote_ip;
+ in_port_t local_port, remote_port;
+
+ const char *const *userdb_fields;
+
+ const char *forward_fields;
+
+ /* Override specified global flags */
+ enum mail_storage_service_flags flags_override_add;
+ enum mail_storage_service_flags flags_override_remove;
+
+ /* override MAIL_STORAGE_SERVICE_FLAG_USERDB_LOOKUP for this lookup */
+ bool no_userdb_lookup:1;
+ /* Enable auth_debug=yes for this lookup */
+ bool debug:1;
+ /* Connection is secure (SSL or just trusted) */
+ bool conn_secured:1;
+ /* Connection is secured using SSL specifically */
+ bool conn_ssl_secured:1;
+};
+
+extern struct module *mail_storage_service_modules;
+
+struct mail_storage_service_ctx *
+mail_storage_service_init(struct master_service *service,
+ const struct setting_parser_info *set_roots[],
+ enum mail_storage_service_flags flags) ATTR_NULL(2);
+struct auth_master_connection *
+mail_storage_service_get_auth_conn(struct mail_storage_service_ctx *ctx);
+/* Set auth connection (instead of creating a new one automatically). */
+void mail_storage_service_set_auth_conn(struct mail_storage_service_ctx *ctx,
+ struct auth_master_connection *conn);
+int mail_storage_service_read_settings(struct mail_storage_service_ctx *ctx,
+ const struct mail_storage_service_input *input,
+ pool_t pool,
+ const struct setting_parser_info **user_info_r,
+ const struct setting_parser_context **parser_r,
+ const char **error_r) ATTR_NULL(2);
+/* Read settings and initialize context to use them. Do nothing if service is
+ already initialized. This is mainly necessary when calling _get_auth_conn()
+ or _all_init(). */
+void mail_storage_service_init_settings(struct mail_storage_service_ctx *ctx,
+ const struct mail_storage_service_input *input)
+ ATTR_NULL(2);
+/* Returns 1 if ok, 0 if user wasn't found, -1 if fatal error,
+ -2 if error is user-specific (e.g. invalid settings). */
+int mail_storage_service_lookup(struct mail_storage_service_ctx *ctx,
+ const struct mail_storage_service_input *input,
+ struct mail_storage_service_user **user_r,
+ const char **error_r);
+/* The next mail_storage_service_lookup() will save the userdb fields into the
+ given pointer, allocated from the given pool. */
+void mail_storage_service_save_userdb_fields(struct mail_storage_service_ctx *ctx,
+ pool_t pool, const char *const **userdb_fields_r);
+/* Returns 0 if ok, -1 if fatal error, -2 if error is user-specific. */
+int mail_storage_service_next(struct mail_storage_service_ctx *ctx,
+ struct mail_storage_service_user *user,
+ struct mail_user **mail_user_r,
+ const char **error_r);
+/* Returns 0 if ok, -1 if fatal error, -2 if error is user-specific. */
+int mail_storage_service_next_with_session_suffix(struct mail_storage_service_ctx *ctx,
+ struct mail_storage_service_user *user,
+ const char *session_id_postfix,
+ struct mail_user **mail_user_r,
+ const char **error_r);
+void mail_storage_service_restrict_setenv(struct mail_storage_service_ctx *ctx,
+ struct mail_storage_service_user *user);
+/* Combine lookup() and next() into one call. */
+int mail_storage_service_lookup_next(struct mail_storage_service_ctx *ctx,
+ const struct mail_storage_service_input *input,
+ struct mail_storage_service_user **user_r,
+ struct mail_user **mail_user_r,
+ const char **error_r);
+void mail_storage_service_user_ref(struct mail_storage_service_user *user);
+void mail_storage_service_user_unref(struct mail_storage_service_user **user);
+/* Initialize iterating through all users. */
+void mail_storage_service_all_init(struct mail_storage_service_ctx *ctx);
+/* Initialize iterating through all users with a user mask hint to the
+ userdb iteration lookup. This itself isn't yet guaranteed to filter out any
+ usernames. */
+void mail_storage_service_all_init_mask(struct mail_storage_service_ctx *ctx,
+ const char *user_mask_hint);
+/* Iterate through all usernames. Returns 1 if username was returned, 0 if
+ there are no more users, -1 if error. */
+int mail_storage_service_all_next(struct mail_storage_service_ctx *ctx,
+ const char **username_r);
+void mail_storage_service_deinit(struct mail_storage_service_ctx **ctx);
+/* Returns the first created service context. If it gets freed, NULL is
+ returned until the next time mail_storage_service_init() is called. */
+struct mail_storage_service_ctx *mail_storage_service_get_global(void);
+
+/* Activate user context. Normally this is called automatically by the ioloop,
+ but e.g. during loops at deinit where all users are being destroyed, it's
+ useful to call this to set the correct user-specific log prefix. */
+void mail_storage_service_io_activate_user(struct mail_storage_service_user *user);
+/* Deactivate user context. This only switches back to non-user-specific
+ log prefix. */
+void mail_storage_service_io_deactivate_user(struct mail_storage_service_user *user);
+
+/* Return the settings pointed to by set_root parameter in _init().
+ The settings contain all the changes done by userdb lookups. */
+void **mail_storage_service_user_get_set(struct mail_storage_service_user *user);
+const struct mail_storage_settings *
+mail_storage_service_user_get_mail_set(struct mail_storage_service_user *user);
+const struct mail_storage_service_input *
+mail_storage_service_user_get_input(struct mail_storage_service_user *user);
+struct setting_parser_context *
+mail_storage_service_user_get_settings_parser(struct mail_storage_service_user *user);
+const struct master_service_ssl_settings *
+mail_storage_service_user_get_ssl_settings(struct mail_storage_service_user *user);
+struct mail_storage_service_ctx *
+mail_storage_service_user_get_service_ctx(struct mail_storage_service_user *user);
+pool_t mail_storage_service_user_get_pool(struct mail_storage_service_user *user);
+const char *
+mail_storage_service_user_get_log_prefix(struct mail_storage_service_user *user);
+
+const char *
+mail_storage_service_get_log_prefix(struct mail_storage_service_ctx *ctx);
+const struct var_expand_table *
+mail_storage_service_get_var_expand_table(struct mail_storage_service_ctx *ctx,
+ struct mail_storage_service_input *input);
+const char *mail_storage_service_fields_var_expand(const char *data,
+ const char *const *fields);
+/* Return the settings pointed to by set_root parameter in _init() */
+void *mail_storage_service_get_settings(struct master_service *service);
+/* Updates settings for storage service user, forwards return value of settings_parse_keyvalue() */
+int mail_storage_service_user_set_setting(struct mail_storage_service_user *user,
+ const char *key,
+ const char *value,
+ const char **error_r);
+
+#endif
diff --git a/src/lib-storage/mail-storage-settings.c b/src/lib-storage/mail-storage-settings.c
new file mode 100644
index 0000000..9421956
--- /dev/null
+++ b/src/lib-storage/mail-storage-settings.c
@@ -0,0 +1,809 @@
+/* Copyright (c) 2005-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "hash-format.h"
+#include "var-expand.h"
+#include "unichar.h"
+#include "hostpid.h"
+#include "settings-parser.h"
+#include "message-address.h"
+#include "message-header-parser.h"
+#include "smtp-address.h"
+#include "mail-index.h"
+#include "mail-user.h"
+#include "mail-namespace.h"
+#include "mail-storage-private.h"
+#include "mail-storage-settings.h"
+#include "iostream-ssl.h"
+
+#include <stddef.h>
+
+static bool mail_storage_settings_check(void *_set, pool_t pool, const char **error_r);
+static bool namespace_settings_check(void *_set, pool_t pool, const char **error_r);
+static bool mailbox_settings_check(void *_set, pool_t pool, const char **error_r);
+static bool mail_user_settings_check(void *_set, pool_t pool, const char **error_r);
+static bool mail_user_settings_expand_check(void *_set, pool_t pool ATTR_UNUSED, const char **error_r);
+
+#undef DEF
+#define DEF(type, name) \
+ SETTING_DEFINE_STRUCT_##type(#name, name, struct mail_storage_settings)
+
+static const struct setting_define mail_storage_setting_defines[] = {
+ DEF(STR_VARS, mail_location),
+ { .type = SET_ALIAS, .key = "mail" },
+ DEF(STR_VARS, mail_attachment_fs),
+ DEF(STR_VARS, mail_attachment_dir),
+ DEF(STR, mail_attachment_hash),
+ DEF(SIZE, mail_attachment_min_size),
+ DEF(STR, mail_attachment_detection_options),
+ DEF(STR_VARS, mail_attribute_dict),
+ DEF(UINT, mail_prefetch_count),
+ DEF(STR, mail_cache_fields),
+ DEF(STR, mail_always_cache_fields),
+ DEF(STR, mail_never_cache_fields),
+ DEF(STR, mail_server_comment),
+ DEF(STR, mail_server_admin),
+ DEF(TIME_HIDDEN, mail_cache_unaccessed_field_drop),
+ DEF(SIZE_HIDDEN, mail_cache_record_max_size),
+ DEF(SIZE_HIDDEN, mail_cache_max_size),
+ DEF(UINT_HIDDEN, mail_cache_min_mail_count),
+ DEF(SIZE_HIDDEN, mail_cache_purge_min_size),
+ DEF(UINT_HIDDEN, mail_cache_purge_delete_percentage),
+ DEF(UINT_HIDDEN, mail_cache_purge_continued_percentage),
+ DEF(UINT_HIDDEN, mail_cache_purge_header_continue_count),
+ DEF(SIZE_HIDDEN, mail_index_rewrite_min_log_bytes),
+ DEF(SIZE_HIDDEN, mail_index_rewrite_max_log_bytes),
+ DEF(SIZE_HIDDEN, mail_index_log_rotate_min_size),
+ DEF(SIZE_HIDDEN, mail_index_log_rotate_max_size),
+ DEF(TIME_HIDDEN, mail_index_log_rotate_min_age),
+ DEF(TIME_HIDDEN, mail_index_log2_max_age),
+ DEF(TIME, mailbox_idle_check_interval),
+ DEF(UINT, mail_max_keyword_length),
+ DEF(TIME, mail_max_lock_timeout),
+ DEF(TIME, mail_temp_scan_interval),
+ DEF(UINT, mail_vsize_bg_after_count),
+ DEF(UINT, mail_sort_max_read_count),
+ DEF(BOOL, mail_save_crlf),
+ DEF(ENUM, mail_fsync),
+ DEF(BOOL, mmap_disable),
+ DEF(BOOL, dotlock_use_excl),
+ DEF(BOOL, mail_nfs_storage),
+ DEF(BOOL, mail_nfs_index),
+ DEF(BOOL, mailbox_list_index),
+ DEF(BOOL, mailbox_list_index_very_dirty_syncs),
+ DEF(BOOL, mailbox_list_index_include_inbox),
+ DEF(BOOL, mail_debug),
+ DEF(BOOL, mail_full_filesystem_access),
+ DEF(BOOL, maildir_stat_dirs),
+ DEF(BOOL, mail_shared_explicit_inbox),
+ DEF(ENUM, lock_method),
+ DEF(STR, pop3_uidl_format),
+
+ DEF(STR, hostname),
+ DEF(STR, recipient_delimiter),
+
+ SETTING_DEFINE_LIST_END
+};
+
+const struct mail_storage_settings mail_storage_default_settings = {
+ .mail_location = "",
+ .mail_attachment_fs = "sis posix",
+ .mail_attachment_dir = "",
+ .mail_attachment_hash = "%{sha1}",
+ .mail_attachment_min_size = 1024*128,
+ .mail_attachment_detection_options = "",
+ .mail_attribute_dict = "",
+ .mail_prefetch_count = 0,
+ .mail_cache_fields = "flags",
+ .mail_always_cache_fields = "",
+ .mail_never_cache_fields = "imap.envelope",
+ .mail_server_comment = "",
+ .mail_server_admin = "",
+ .mail_cache_min_mail_count = 0,
+ .mail_cache_unaccessed_field_drop = 60*60*24*30,
+ .mail_cache_record_max_size = 64 * 1024,
+ .mail_cache_max_size = 1024 * 1024 * 1024,
+ .mail_cache_purge_min_size = 32 * 1024,
+ .mail_cache_purge_delete_percentage = 20,
+ .mail_cache_purge_continued_percentage = 200,
+ .mail_cache_purge_header_continue_count = 4,
+ .mail_index_rewrite_min_log_bytes = 8 * 1024,
+ .mail_index_rewrite_max_log_bytes = 128 * 1024,
+ .mail_index_log_rotate_min_size = 32 * 1024,
+ .mail_index_log_rotate_max_size = 1024 * 1024,
+ .mail_index_log_rotate_min_age = 5 * 60,
+ .mail_index_log2_max_age = 3600 * 24 * 2,
+ .mailbox_idle_check_interval = 30,
+ .mail_max_keyword_length = 50,
+ .mail_max_lock_timeout = 0,
+ .mail_temp_scan_interval = 7*24*60*60,
+ .mail_vsize_bg_after_count = 0,
+ .mail_sort_max_read_count = 0,
+ .mail_save_crlf = FALSE,
+ .mail_fsync = "optimized:never:always",
+ .mmap_disable = FALSE,
+ .dotlock_use_excl = TRUE,
+ .mail_nfs_storage = FALSE,
+ .mail_nfs_index = FALSE,
+ .mailbox_list_index = TRUE,
+ .mailbox_list_index_very_dirty_syncs = FALSE,
+ .mailbox_list_index_include_inbox = FALSE,
+ .mail_debug = FALSE,
+ .mail_full_filesystem_access = FALSE,
+ .maildir_stat_dirs = FALSE,
+ .mail_shared_explicit_inbox = FALSE,
+ .lock_method = "fcntl:flock:dotlock",
+ .pop3_uidl_format = "%08Xu%08Xv",
+
+ .hostname = "",
+ .recipient_delimiter = "+",
+};
+
+const struct setting_parser_info mail_storage_setting_parser_info = {
+ .module_name = "mail",
+ .defines = mail_storage_setting_defines,
+ .defaults = &mail_storage_default_settings,
+
+ .type_offset = SIZE_MAX,
+ .struct_size = sizeof(struct mail_storage_settings),
+
+ .parent_offset = SIZE_MAX,
+ .parent = &mail_user_setting_parser_info,
+
+ .check_func = mail_storage_settings_check,
+};
+
+#undef DEF
+#define DEF(type, name) \
+ SETTING_DEFINE_STRUCT_##type(#name, name, struct mailbox_settings)
+
+static const struct setting_define mailbox_setting_defines[] = {
+ DEF(STR, name),
+ { .type = SET_ENUM, .key = "auto",
+ .offset = offsetof(struct mailbox_settings, autocreate) } ,
+ DEF(STR, special_use),
+ DEF(STR, driver),
+ DEF(STR, comment),
+ DEF(TIME, autoexpunge),
+ DEF(UINT, autoexpunge_max_mails),
+
+ SETTING_DEFINE_LIST_END
+};
+
+const struct mailbox_settings mailbox_default_settings = {
+ .name = "",
+ .autocreate = MAILBOX_SET_AUTO_NO":"
+ MAILBOX_SET_AUTO_CREATE":"
+ MAILBOX_SET_AUTO_SUBSCRIBE,
+ .special_use = "",
+ .driver = "",
+ .comment = "",
+ .autoexpunge = 0,
+ .autoexpunge_max_mails = 0
+};
+
+const struct setting_parser_info mailbox_setting_parser_info = {
+ .defines = mailbox_setting_defines,
+ .defaults = &mailbox_default_settings,
+
+ .type_offset = offsetof(struct mailbox_settings, name),
+ .struct_size = sizeof(struct mailbox_settings),
+
+ .parent_offset = SIZE_MAX,
+ .parent = &mail_user_setting_parser_info,
+
+ .check_func = mailbox_settings_check
+};
+
+#undef DEF
+#undef DEFLIST_UNIQUE
+#define DEF(type, name) \
+ SETTING_DEFINE_STRUCT_##type(#name, name, struct mail_namespace_settings)
+#define DEFLIST_UNIQUE(field, name, defines) \
+ { .type = SET_DEFLIST_UNIQUE, .key = name, \
+ .offset = offsetof(struct mail_namespace_settings, field), \
+ .list_info = defines }
+
+static const struct setting_define mail_namespace_setting_defines[] = {
+ DEF(STR, name),
+ DEF(ENUM, type),
+ DEF(STR, separator),
+ DEF(STR_VARS, prefix),
+ DEF(STR_VARS, location),
+ { .type = SET_ALIAS, .key = "mail" },
+ { .type = SET_ALIAS, .key = "mail_location" },
+ DEF(STR_VARS, alias_for),
+
+ DEF(BOOL, inbox),
+ DEF(BOOL, hidden),
+ DEF(ENUM, list),
+ DEF(BOOL, subscriptions),
+ DEF(BOOL, ignore_on_failure),
+ DEF(BOOL, disabled),
+ DEF(UINT, order),
+
+ DEFLIST_UNIQUE(mailboxes, "mailbox", &mailbox_setting_parser_info),
+
+ SETTING_DEFINE_LIST_END
+};
+
+const struct mail_namespace_settings mail_namespace_default_settings = {
+ .name = "",
+ .type = "private:shared:public",
+ .separator = "",
+ .prefix = "",
+ .location = "",
+ .alias_for = NULL,
+
+ .inbox = FALSE,
+ .hidden = FALSE,
+ .list = "yes:no:children",
+ .subscriptions = TRUE,
+ .ignore_on_failure = FALSE,
+ .disabled = FALSE,
+ .order = 0,
+
+ .mailboxes = ARRAY_INIT
+};
+
+const struct setting_parser_info mail_namespace_setting_parser_info = {
+ .defines = mail_namespace_setting_defines,
+ .defaults = &mail_namespace_default_settings,
+
+ .type_offset = offsetof(struct mail_namespace_settings, name),
+ .struct_size = sizeof(struct mail_namespace_settings),
+
+ .parent_offset = offsetof(struct mail_namespace_settings, user_set),
+ .parent = &mail_user_setting_parser_info,
+
+ .check_func = namespace_settings_check
+};
+
+#undef DEF
+#undef DEFLIST_UNIQUE
+#define DEF(type, name) \
+ SETTING_DEFINE_STRUCT_##type(#name, name, struct mail_user_settings)
+#define DEFLIST_UNIQUE(field, name, defines) \
+ { .type = SET_DEFLIST_UNIQUE, .key = name, \
+ .offset = offsetof(struct mail_user_settings, field), \
+ .list_info = defines }
+
+static const struct setting_define mail_user_setting_defines[] = {
+ DEF(STR, base_dir),
+ DEF(STR, auth_socket_path),
+ DEF(STR_VARS, mail_temp_dir),
+
+ DEF(STR, mail_uid),
+ DEF(STR, mail_gid),
+ DEF(STR_VARS, mail_home),
+ DEF(STR_VARS, mail_chroot),
+ DEF(STR, mail_access_groups),
+ DEF(STR, mail_privileged_group),
+ DEF(STR, valid_chroot_dirs),
+
+ DEF(UINT, first_valid_uid),
+ DEF(UINT, last_valid_uid),
+ DEF(UINT, first_valid_gid),
+ DEF(UINT, last_valid_gid),
+
+ DEF(STR, mail_plugins),
+ DEF(STR, mail_plugin_dir),
+
+ DEF(STR, mail_log_prefix),
+
+ DEF(STR, hostname),
+ DEF(STR_VARS, postmaster_address),
+
+ DEFLIST_UNIQUE(namespaces, "namespace", &mail_namespace_setting_parser_info),
+ { .type = SET_STRLIST, .key = "plugin",
+ .offset = offsetof(struct mail_user_settings, plugin_envs) },
+
+ SETTING_DEFINE_LIST_END
+};
+
+static const struct mail_user_settings mail_user_default_settings = {
+ .base_dir = PKG_RUNDIR,
+ .auth_socket_path = "auth-userdb",
+ .mail_temp_dir = "/tmp",
+
+ .mail_uid = "",
+ .mail_gid = "",
+ .mail_home = "",
+ .mail_chroot = "",
+ .mail_access_groups = "",
+ .mail_privileged_group = "",
+ .valid_chroot_dirs = "",
+
+ .first_valid_uid = 500,
+ .last_valid_uid = 0,
+ .first_valid_gid = 1,
+ .last_valid_gid = 0,
+
+ .mail_plugins = "",
+ .mail_plugin_dir = MODULEDIR,
+
+ .mail_log_prefix = "%s(%u)<%{pid}><%{session}>: ",
+
+ .hostname = "",
+ .postmaster_address = "postmaster@%{if;%d;ne;;%d;%{hostname}}",
+
+ .namespaces = ARRAY_INIT,
+ .plugin_envs = ARRAY_INIT
+};
+
+const struct setting_parser_info mail_user_setting_parser_info = {
+ .module_name = "mail",
+ .defines = mail_user_setting_defines,
+ .defaults = &mail_user_default_settings,
+
+ .type_offset = SIZE_MAX,
+ .struct_size = sizeof(struct mail_user_settings),
+
+ .parent_offset = SIZE_MAX,
+
+ .check_func = mail_user_settings_check,
+#ifndef CONFIG_BINARY
+ .expand_check_func = mail_user_settings_expand_check,
+#endif
+};
+
+const void *
+mail_user_set_get_driver_settings(const struct setting_parser_info *info,
+ const struct mail_user_settings *set,
+ const char *driver)
+{
+ const void *dset;
+
+ dset = settings_find_dynamic(info, set, driver);
+ if (dset == NULL) {
+ i_panic("Default settings not found for storage driver %s",
+ driver);
+ }
+ return dset;
+}
+
+const struct mail_storage_settings *
+mail_user_set_get_storage_set(struct mail_user *user)
+{
+ return mail_user_set_get_driver_settings(user->set_info, user->set,
+ MAIL_STORAGE_SET_DRIVER_NAME);
+}
+
+const void *mail_namespace_get_driver_settings(struct mail_namespace *ns,
+ struct mail_storage *storage)
+{
+ return mail_user_set_get_driver_settings(storage->user->set_info,
+ ns->user_set, storage->name);
+}
+
+const struct dynamic_settings_parser *
+mail_storage_get_dynamic_parsers(pool_t pool)
+{
+ struct dynamic_settings_parser *parsers;
+ struct mail_storage *const *storages;
+ unsigned int i, j, count;
+
+ storages = array_get(&mail_storage_classes, &count);
+ parsers = p_new(pool, struct dynamic_settings_parser, 1 + count + 1);
+ parsers[0].name = MAIL_STORAGE_SET_DRIVER_NAME;
+ parsers[0].info = &mail_storage_setting_parser_info;
+
+ for (i = 0, j = 1; i < count; i++) {
+ if (storages[i]->v.get_setting_parser_info == NULL)
+ continue;
+
+ parsers[j].name = storages[i]->name;
+ parsers[j].info = storages[i]->v.get_setting_parser_info();
+ j++;
+ }
+ parsers[j].name = NULL;
+ return parsers;
+}
+
+static void
+fix_base_path(struct mail_user_settings *set, pool_t pool, const char **str)
+{
+ if (*str != NULL && **str != '\0' && **str != '/')
+ *str = p_strconcat(pool, set->base_dir, "/", *str, NULL);
+}
+
+/* <settings checks> */
+static bool mail_cache_fields_parse(const char *key, const char *value,
+ const char **error_r)
+{
+ const char *const *arr;
+
+ for (arr = t_strsplit_spaces(value, " ,"); *arr != NULL; arr++) {
+ const char *name = *arr;
+
+ if (strncasecmp(name, "hdr.", 4) == 0 &&
+ !message_header_name_is_valid(name+4)) {
+ *error_r = t_strdup_printf(
+ "Invalid %s: %s is not a valid header name",
+ key, name);
+ return FALSE;
+ }
+ }
+ return TRUE;
+}
+
+static bool mail_storage_settings_check(void *_set, pool_t pool,
+ const char **error_r)
+{
+ struct mail_storage_settings *set = _set;
+ struct hash_format *format;
+ const char *p, *error;
+ bool uidl_format_ok;
+ char c;
+
+ if (set->mailbox_idle_check_interval == 0) {
+ *error_r = "mailbox_idle_check_interval must not be 0";
+ return FALSE;
+ }
+
+ if (strcmp(set->mail_fsync, "optimized") == 0)
+ set->parsed_fsync_mode = FSYNC_MODE_OPTIMIZED;
+ else if (strcmp(set->mail_fsync, "never") == 0)
+ set->parsed_fsync_mode = FSYNC_MODE_NEVER;
+ else if (strcmp(set->mail_fsync, "always") == 0)
+ set->parsed_fsync_mode = FSYNC_MODE_ALWAYS;
+ else {
+ *error_r = t_strdup_printf("Unknown mail_fsync: %s",
+ set->mail_fsync);
+ return FALSE;
+ }
+
+ if (set->mail_nfs_index && !set->mmap_disable) {
+ *error_r = "mail_nfs_index=yes requires mmap_disable=yes";
+ return FALSE;
+ }
+ if (set->mail_nfs_index &&
+ set->parsed_fsync_mode != FSYNC_MODE_ALWAYS) {
+ *error_r = "mail_nfs_index=yes requires mail_fsync=always";
+ return FALSE;
+ }
+
+ if (!file_lock_method_parse(set->lock_method,
+ &set->parsed_lock_method)) {
+ *error_r = t_strdup_printf("Unknown lock_method: %s",
+ set->lock_method);
+ return FALSE;
+ }
+
+ if (set->mail_cache_max_size > 1024 * 1024 * 1024) {
+ *error_r = "mail_cache_max_size can't be over 1 GB";
+ return FALSE;
+ }
+ if (set->mail_cache_purge_delete_percentage > 100) {
+ *error_r = "mail_cache_purge_delete_percentage can't be over 100";
+ return FALSE;
+ }
+
+ uidl_format_ok = FALSE;
+ for (p = set->pop3_uidl_format; *p != '\0'; p++) {
+ if (p[0] != '%' || p[1] == '\0')
+ continue;
+
+ c = var_get_key(++p);
+ switch (c) {
+ case 'v':
+ case 'u':
+ case 'm':
+ case 'f':
+ case 'g':
+ uidl_format_ok = TRUE;
+ break;
+ case '%':
+ break;
+ default:
+ *error_r = t_strdup_printf(
+ "Unknown pop3_uidl_format variable: %%%c", c);
+ return FALSE;
+ }
+ }
+ if (!uidl_format_ok) {
+ *error_r = "pop3_uidl_format setting doesn't contain any "
+ "%% variables.";
+ return FALSE;
+ }
+
+ if (strchr(set->mail_attachment_hash, '/') != NULL) {
+ *error_r = "mail_attachment_hash setting "
+ "must not contain '/' characters";
+ return FALSE;
+ }
+ if (hash_format_init(set->mail_attachment_hash, &format, &error) < 0) {
+ *error_r = t_strconcat("Invalid mail_attachment_hash setting: ",
+ error, NULL);
+ return FALSE;
+ }
+ if (strchr(set->mail_attachment_hash, '-') != NULL) {
+ *error_r = "mail_attachment_hash setting "
+ "must not contain '-' characters";
+ return FALSE;
+ }
+ hash_format_deinit_free(&format);
+
+ // FIXME: check set->mail_server_admin syntax (RFC 5464, Section 6.2.2)
+
+#ifndef CONFIG_BINARY
+ if (*set->hostname == '\0')
+ set->hostname = p_strdup(pool, my_hostdomain());
+#endif
+
+ /* parse mail_attachment_indicator_options */
+ if (*set->mail_attachment_detection_options != '\0') {
+ ARRAY_TYPE(const_string) content_types;
+ p_array_init(&content_types, pool, 2);
+
+ const char *const *options =
+ t_strsplit_spaces(set->mail_attachment_detection_options, " ");
+
+ while(*options != NULL) {
+ const char *opt = *options;
+
+ if (strcmp(opt, "add-flags") == 0 ||
+ strcmp(opt, "add-flags-on-save") == 0) {
+ set->parsed_mail_attachment_detection_add_flags = TRUE;
+ } else if (strcmp(opt, "no-flags-on-fetch") == 0) {
+ set->parsed_mail_attachment_detection_no_flags_on_fetch = TRUE;
+ } else if (strcmp(opt, "exclude-inlined") == 0) {
+ set->parsed_mail_attachment_exclude_inlined = TRUE;
+ } else if (str_begins(opt, "content-type=")) {
+ const char *value = p_strdup(pool, opt+13);
+ array_push_back(&content_types, &value);
+ } else {
+ *error_r = t_strdup_printf("mail_attachment_detection_options: "
+ "Unknown option: %s", opt);
+ return FALSE;
+ }
+ options++;
+ }
+
+ array_append_zero(&content_types);
+ set->parsed_mail_attachment_content_type_filter = array_front(&content_types);
+ }
+
+ if (!mail_cache_fields_parse("mail_cache_fields",
+ set->mail_cache_fields, error_r))
+ return FALSE;
+ if (!mail_cache_fields_parse("mail_always_cache_fields",
+ set->mail_always_cache_fields, error_r))
+ return FALSE;
+ if (!mail_cache_fields_parse("mail_never_cache_fields",
+ set->mail_never_cache_fields, error_r))
+ return FALSE;
+ return TRUE;
+}
+
+static bool namespace_settings_check(void *_set, pool_t pool ATTR_UNUSED,
+ const char **error_r)
+{
+ struct mail_namespace_settings *ns = _set;
+ struct mail_namespace_settings *const *namespaces;
+ const char *name;
+ unsigned int i, count;
+
+ name = ns->prefix != NULL ? ns->prefix : "";
+
+ if (ns->separator[0] != '\0' && ns->separator[1] != '\0') {
+ *error_r = t_strdup_printf("Namespace '%s': "
+ "Hierarchy separator must be only one character long",
+ name);
+ return FALSE;
+ }
+ if (!uni_utf8_str_is_valid(name)) {
+ *error_r = t_strdup_printf("Namespace prefix not valid UTF8: %s",
+ name);
+ return FALSE;
+ }
+
+ if (ns->alias_for != NULL && !ns->disabled) {
+ if (array_is_created(&ns->user_set->namespaces)) {
+ namespaces = array_get(&ns->user_set->namespaces,
+ &count);
+ } else {
+ namespaces = NULL;
+ count = 0;
+ }
+ for (i = 0; i < count; i++) {
+ if (strcmp(namespaces[i]->prefix, ns->alias_for) == 0)
+ break;
+ }
+ if (i == count) {
+ *error_r = t_strdup_printf(
+ "Namespace '%s': alias_for points to "
+ "unknown namespace: %s", name, ns->alias_for);
+ return FALSE;
+ }
+ if (namespaces[i]->alias_for != NULL) {
+ *error_r = t_strdup_printf(
+ "Namespace '%s': alias_for chaining isn't "
+ "allowed: %s -> %s", name, ns->alias_for,
+ namespaces[i]->alias_for);
+ return FALSE;
+ }
+ }
+ return TRUE;
+}
+
+static bool mailbox_special_use_exists(const char *name)
+{
+ if (name[0] != '\\')
+ return FALSE;
+ name++;
+
+ if (strcasecmp(name, "All") == 0)
+ return TRUE;
+ if (strcasecmp(name, "Archive") == 0)
+ return TRUE;
+ if (strcasecmp(name, "Drafts") == 0)
+ return TRUE;
+ if (strcasecmp(name, "Flagged") == 0)
+ return TRUE;
+ if (strcasecmp(name, "Important") == 0)
+ return TRUE;
+ if (strcasecmp(name, "Junk") == 0)
+ return TRUE;
+ if (strcasecmp(name, "Sent") == 0)
+ return TRUE;
+ if (strcasecmp(name, "Trash") == 0)
+ return TRUE;
+ return FALSE;
+}
+
+static bool
+mailbox_special_use_check(struct mailbox_settings *set, pool_t pool,
+ const char **error_r)
+{
+ const char *const *uses, *str;
+ unsigned int i;
+
+ uses = t_strsplit_spaces(set->special_use, " ");
+ for (i = 0; uses[i] != NULL; i++) {
+ if (!mailbox_special_use_exists(uses[i])) {
+ *error_r = t_strdup_printf(
+ "mailbox %s: unknown special_use: %s",
+ set->name, uses[i]);
+ return FALSE;
+ }
+ }
+ /* make sure there are no extra spaces */
+ str = t_strarray_join(uses, " ");
+ if (strcmp(str, set->special_use) != 0)
+ set->special_use = p_strdup(pool, str);
+ return TRUE;
+}
+
+static bool mailbox_settings_check(void *_set, pool_t pool,
+ const char **error_r)
+{
+ struct mailbox_settings *set = _set;
+
+ if (!uni_utf8_str_is_valid(set->name)) {
+ *error_r = t_strdup_printf("mailbox %s: name isn't valid UTF-8",
+ set->name);
+ return FALSE;
+ }
+ if (*set->special_use != '\0') {
+ if (!mailbox_special_use_check(set, pool, error_r))
+ return FALSE;
+ }
+ return TRUE;
+}
+
+static bool mail_user_settings_check(void *_set, pool_t pool ATTR_UNUSED,
+ const char **error_r ATTR_UNUSED)
+{
+ struct mail_user_settings *set = _set;
+
+#ifndef CONFIG_BINARY
+ fix_base_path(set, pool, &set->auth_socket_path);
+
+ if (*set->hostname == '\0')
+ set->hostname = p_strdup(pool, my_hostdomain());
+ if (set->postmaster_address[0] == SETTING_STRVAR_UNEXPANDED[0] &&
+ set->postmaster_address[1] == '\0') {
+ /* check for valid looking fqdn in hostname */
+ if (strchr(set->hostname, '.') == NULL) {
+ *error_r = "postmaster_address setting not given";
+ return FALSE;
+ }
+ set->postmaster_address =
+ p_strconcat(pool, SETTING_STRVAR_UNEXPANDED,
+ "postmaster@", set->hostname, NULL);
+ }
+#else
+ if (*set->mail_plugins != '\0' &&
+ access(set->mail_plugin_dir, R_OK | X_OK) < 0) {
+ *error_r = t_strdup_printf(
+ "mail_plugin_dir: access(%s) failed: %m",
+ set->mail_plugin_dir);
+ return FALSE;
+ }
+#endif
+ return TRUE;
+}
+
+#ifndef CONFIG_BINARY
+static bool parse_postmaster_address(const char *address, pool_t pool,
+ struct mail_user_settings *set,
+ const char **error_r) ATTR_NULL(3)
+{
+ struct message_address *addr;
+ struct smtp_address *smtp_addr;
+
+ addr = message_address_parse(pool,
+ (const unsigned char *)address,
+ strlen(address), 2, 0);
+ if (addr == NULL || addr->domain == NULL || addr->invalid_syntax ||
+ smtp_address_create_from_msg(pool, addr, &smtp_addr) < 0) {
+ *error_r = t_strdup_printf(
+ "invalid address `%s' specified for the "
+ "postmaster_address setting", address);
+ return FALSE;
+ }
+ if (addr->next != NULL) {
+ *error_r = "more than one address specified for the "
+ "postmaster_address setting";
+ return FALSE;
+ }
+ if (addr->name == NULL || *addr->name == '\0')
+ addr->name = "Postmaster";
+ if (set != NULL) {
+ set->_parsed_postmaster_address = addr;
+ set->_parsed_postmaster_address_smtp = smtp_addr;
+ }
+ return TRUE;
+}
+
+static bool
+mail_user_settings_expand_check(void *_set, pool_t pool,
+ const char **error_r ATTR_UNUSED)
+{
+ struct mail_user_settings *set = _set;
+ const char *error;
+
+ /* Parse if possible. Perform error handling later. */
+ (void)parse_postmaster_address(set->postmaster_address, pool,
+ set, &error);
+ return TRUE;
+}
+#endif
+
+/* </settings checks> */
+
+static void
+get_postmaster_address_error(const struct mail_user_settings *set,
+ const char **error_r)
+{
+ if (parse_postmaster_address(set->postmaster_address,
+ pool_datastack_create(), NULL, error_r))
+ i_panic("postmaster_address='%s' parsing succeeded unexpectedly after it had already failed",
+ set->postmaster_address);
+}
+
+bool mail_user_set_get_postmaster_address(const struct mail_user_settings *set,
+ const struct message_address **address_r,
+ const char **error_r)
+{
+ *address_r = set->_parsed_postmaster_address;
+ if (*address_r != NULL)
+ return TRUE;
+ /* parsing failed - do it again to get the error */
+ get_postmaster_address_error(set, error_r);
+ return FALSE;
+}
+
+bool mail_user_set_get_postmaster_smtp(const struct mail_user_settings *set,
+ const struct smtp_address **address_r,
+ const char **error_r)
+{
+ *address_r = set->_parsed_postmaster_address_smtp;
+ if (*address_r != NULL)
+ return TRUE;
+ /* parsing failed - do it again to get the error */
+ get_postmaster_address_error(set, error_r);
+ return FALSE;
+}
diff --git a/src/lib-storage/mail-storage-settings.h b/src/lib-storage/mail-storage-settings.h
new file mode 100644
index 0000000..9dc8f5d
--- /dev/null
+++ b/src/lib-storage/mail-storage-settings.h
@@ -0,0 +1,175 @@
+#ifndef MAIL_STORAGE_SETTINGS_H
+#define MAIL_STORAGE_SETTINGS_H
+
+#include "file-lock.h"
+#include "fsync-mode.h"
+
+#define MAIL_STORAGE_SET_DRIVER_NAME "MAIL"
+
+struct mail_user;
+struct mail_namespace;
+struct mail_storage;
+struct message_address;
+struct smtp_address;
+
+struct mail_storage_settings {
+ const char *mail_location;
+ const char *mail_attachment_fs;
+ const char *mail_attachment_dir;
+ const char *mail_attachment_hash;
+ uoff_t mail_attachment_min_size;
+ const char *mail_attribute_dict;
+ unsigned int mail_prefetch_count;
+ const char *mail_cache_fields;
+ const char *mail_always_cache_fields;
+ const char *mail_never_cache_fields;
+ const char *mail_server_comment;
+ const char *mail_server_admin;
+ unsigned int mail_cache_min_mail_count;
+ unsigned int mail_cache_unaccessed_field_drop;
+ uoff_t mail_cache_record_max_size;
+ uoff_t mail_cache_max_size;
+ uoff_t mail_cache_purge_min_size;
+ unsigned int mail_cache_purge_delete_percentage;
+ unsigned int mail_cache_purge_continued_percentage;
+ unsigned int mail_cache_purge_header_continue_count;
+ uoff_t mail_index_rewrite_min_log_bytes;
+ uoff_t mail_index_rewrite_max_log_bytes;
+ uoff_t mail_index_log_rotate_min_size;
+ uoff_t mail_index_log_rotate_max_size;
+ unsigned int mail_index_log_rotate_min_age;
+ unsigned int mail_index_log2_max_age;
+ unsigned int mailbox_idle_check_interval;
+ unsigned int mail_max_keyword_length;
+ unsigned int mail_max_lock_timeout;
+ unsigned int mail_temp_scan_interval;
+ unsigned int mail_vsize_bg_after_count;
+ unsigned int mail_sort_max_read_count;
+ bool mail_save_crlf;
+ const char *mail_fsync;
+ bool mmap_disable;
+ bool dotlock_use_excl;
+ bool mail_nfs_storage;
+ bool mail_nfs_index;
+ bool mailbox_list_index;
+ bool mailbox_list_index_very_dirty_syncs;
+ bool mailbox_list_index_include_inbox;
+ bool mail_debug;
+ bool mail_full_filesystem_access;
+ bool maildir_stat_dirs;
+ bool mail_shared_explicit_inbox;
+ const char *lock_method;
+ const char *pop3_uidl_format;
+
+ const char *hostname;
+ const char *recipient_delimiter;
+
+ const char *mail_attachment_detection_options;
+
+ enum file_lock_method parsed_lock_method;
+ enum fsync_mode parsed_fsync_mode;
+
+ const char *const *parsed_mail_attachment_content_type_filter;
+ bool parsed_mail_attachment_exclude_inlined;
+ bool parsed_mail_attachment_detection_add_flags;
+ bool parsed_mail_attachment_detection_no_flags_on_fetch;
+};
+
+struct mail_namespace_settings {
+ const char *name;
+ const char *type;
+ const char *separator;
+ const char *prefix;
+ const char *location;
+ const char *alias_for;
+
+ bool inbox;
+ bool hidden;
+ const char *list;
+ bool subscriptions;
+ bool ignore_on_failure;
+ bool disabled;
+ unsigned int order;
+
+ ARRAY(struct mailbox_settings *) mailboxes;
+ struct mail_user_settings *user_set;
+};
+
+/* <settings checks> */
+#define MAILBOX_SET_AUTO_NO "no"
+#define MAILBOX_SET_AUTO_CREATE "create"
+#define MAILBOX_SET_AUTO_SUBSCRIBE "subscribe"
+/* </settings checks> */
+struct mailbox_settings {
+ const char *name;
+ const char *autocreate;
+ const char *special_use;
+ const char *driver;
+ const char *comment;
+ unsigned int autoexpunge;
+ unsigned int autoexpunge_max_mails;
+};
+
+struct mail_user_settings {
+ const char *base_dir;
+ const char *auth_socket_path;
+ const char *mail_temp_dir;
+
+ const char *mail_uid;
+ const char *mail_gid;
+ const char *mail_home;
+ const char *mail_chroot;
+ const char *mail_access_groups;
+ const char *mail_privileged_group;
+ const char *valid_chroot_dirs;
+
+ unsigned int first_valid_uid, last_valid_uid;
+ unsigned int first_valid_gid, last_valid_gid;
+
+ const char *mail_plugins;
+ const char *mail_plugin_dir;
+
+ const char *mail_log_prefix;
+
+ const char *hostname;
+ const char *postmaster_address;
+
+ ARRAY(struct mail_namespace_settings *) namespaces;
+ ARRAY(const char *) plugin_envs;
+
+ /* May be NULL - use mail_storage_get_postmaster_address() instead of
+ directly accessing this. */
+ const struct message_address *_parsed_postmaster_address;
+ const struct smtp_address *_parsed_postmaster_address_smtp;
+};
+
+extern const struct setting_parser_info mail_user_setting_parser_info;
+extern const struct setting_parser_info mail_namespace_setting_parser_info;
+extern const struct setting_parser_info mail_storage_setting_parser_info;
+extern const struct mail_namespace_settings mail_namespace_default_settings;
+extern const struct mailbox_settings mailbox_default_settings;
+
+struct ssl_iostream_settings;
+
+const void *
+mail_user_set_get_driver_settings(const struct setting_parser_info *info,
+ const struct mail_user_settings *set,
+ const char *driver);
+
+const struct mail_storage_settings *
+mail_user_set_get_storage_set(struct mail_user *user);
+/* Get storage-specific settings, which may be namespace-specific. */
+const void *mail_namespace_get_driver_settings(struct mail_namespace *ns,
+ struct mail_storage *storage);
+
+const struct dynamic_settings_parser *
+mail_storage_get_dynamic_parsers(pool_t pool);
+
+bool mail_user_set_get_postmaster_address(const struct mail_user_settings *set,
+ const struct message_address **address_r,
+ const char **error_r);
+bool mail_user_set_get_postmaster_smtp(const struct mail_user_settings *set,
+ const struct smtp_address **address_r,
+ const char **error_r);
+
+#endif
diff --git a/src/lib-storage/mail-storage.c b/src/lib-storage/mail-storage.c
new file mode 100644
index 0000000..0a71426
--- /dev/null
+++ b/src/lib-storage/mail-storage.c
@@ -0,0 +1,3288 @@
+/* Copyright (c) 2002-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "ioloop.h"
+#include "array.h"
+#include "llist.h"
+#include "mail-storage.h"
+#include "str.h"
+#include "str-sanitize.h"
+#include "sha1.h"
+#include "unichar.h"
+#include "hex-binary.h"
+#include "fs-api.h"
+#include "iostream-ssl.h"
+#include "file-dotlock.h"
+#include "file-create-locked.h"
+#include "istream.h"
+#include "eacces-error.h"
+#include "mkdir-parents.h"
+#include "time-util.h"
+#include "var-expand.h"
+#include "dsasl-client.h"
+#include "imap-date.h"
+#include "settings-parser.h"
+#include "mail-index-private.h"
+#include "mail-index-alloc-cache.h"
+#include "mailbox-tree.h"
+#include "mailbox-list-private.h"
+#include "mail-storage-private.h"
+#include "mail-storage-settings.h"
+#include "mail-namespace.h"
+#include "mail-search.h"
+#include "mail-search-register.h"
+#include "mail-search-mime-register.h"
+#include "mailbox-search-result-private.h"
+#include "mailbox-guid-cache.h"
+#include "mail-cache.h"
+
+#include <ctype.h>
+
+#define MAILBOX_DELETE_RETRY_SECS 30
+#define MAILBOX_MAX_HIERARCHY_NAME_LENGTH 255
+
+extern struct mail_search_register *mail_search_register_imap;
+extern struct mail_search_register *mail_search_register_human;
+
+struct event_category event_category_storage = {
+ .name = "storage",
+};
+struct event_category event_category_mailbox = {
+ .parent = &event_category_storage,
+ .name = "mailbox",
+};
+struct event_category event_category_mail = {
+ .parent = &event_category_mailbox,
+ .name = "mail",
+};
+
+struct mail_storage_module_register mail_storage_module_register = { 0 };
+struct mail_module_register mail_module_register = { 0 };
+
+struct mail_storage_mail_index_module mail_storage_mail_index_module =
+ MODULE_CONTEXT_INIT(&mail_index_module_register);
+ARRAY_TYPE(mail_storage) mail_storage_classes;
+
+static int mail_storage_init_refcount = 0;
+
+void mail_storage_init(void)
+{
+ if (mail_storage_init_refcount++ > 0)
+ return;
+ dsasl_clients_init();
+ mailbox_attributes_init();
+ mailbox_lists_init();
+ mail_storage_hooks_init();
+ i_array_init(&mail_storage_classes, 8);
+ mail_storage_register_all();
+ mailbox_list_register_all();
+}
+
+void mail_storage_deinit(void)
+{
+ i_assert(mail_storage_init_refcount > 0);
+ if (--mail_storage_init_refcount > 0)
+ return;
+ if (mail_search_register_human != NULL)
+ mail_search_register_deinit(&mail_search_register_human);
+ if (mail_search_register_imap != NULL)
+ mail_search_register_deinit(&mail_search_register_imap);
+ mail_search_mime_register_deinit();
+ if (array_is_created(&mail_storage_classes))
+ array_free(&mail_storage_classes);
+ mail_storage_hooks_deinit();
+ mailbox_lists_deinit();
+ mailbox_attributes_deinit();
+ dsasl_clients_deinit();
+}
+
+void mail_storage_class_register(struct mail_storage *storage_class)
+{
+ i_assert(mail_storage_find_class(storage_class->name) == NULL);
+
+ /* append it after the list, so the autodetection order is correct */
+ array_push_back(&mail_storage_classes, &storage_class);
+}
+
+void mail_storage_class_unregister(struct mail_storage *storage_class)
+{
+ struct mail_storage *const *classes;
+ unsigned int i, count;
+
+ classes = array_get(&mail_storage_classes, &count);
+ for (i = 0; i < count; i++) {
+ if (classes[i] == storage_class) {
+ array_delete(&mail_storage_classes, i, 1);
+ break;
+ }
+ }
+}
+
+struct mail_storage *mail_storage_find_class(const char *name)
+{
+ struct mail_storage *const *classes;
+ unsigned int i, count;
+
+ i_assert(name != NULL);
+
+ classes = array_get(&mail_storage_classes, &count);
+ for (i = 0; i < count; i++) {
+ if (strcasecmp(classes[i]->name, name) == 0)
+ return classes[i];
+ }
+ return NULL;
+}
+
+static struct mail_storage *
+mail_storage_autodetect(const struct mail_namespace *ns,
+ struct mailbox_list_settings *set)
+{
+ struct mail_storage *const *classes;
+ unsigned int i, count;
+
+ classes = array_get(&mail_storage_classes, &count);
+ for (i = 0; i < count; i++) {
+ if (classes[i]->v.autodetect != NULL) {
+ if (classes[i]->v.autodetect(ns, set))
+ return classes[i];
+ }
+ }
+ return NULL;
+}
+
+static void
+mail_storage_set_autodetection(const char **data, const char **driver)
+{
+ const char *p;
+
+ /* check if data is in driver:data format (eg. mbox:~/mail) */
+ p = *data;
+ while (i_isalnum(*p) || *p == '_') p++;
+
+ if (*p == ':' && p != *data) {
+ /* no autodetection if the storage driver is given. */
+ *driver = t_strdup_until(*data, p);
+ *data = p + 1;
+ }
+}
+
+static struct mail_storage *
+mail_storage_get_class(struct mail_namespace *ns, const char *driver,
+ struct mailbox_list_settings *list_set,
+ enum mail_storage_flags flags, const char **error_r)
+{
+ struct mail_storage *storage_class = NULL;
+ const char *home;
+
+ if (driver == NULL) {
+ /* no mail_location, autodetect */
+ } else if (strcmp(driver, "auto") == 0) {
+ /* explicit autodetection with "auto" driver. */
+ if (list_set->root_dir != NULL &&
+ *list_set->root_dir == '\0') {
+ /* handle the same as with driver=NULL */
+ list_set->root_dir = NULL;
+ }
+ } else {
+ storage_class = mail_user_get_storage_class(ns->user, driver);
+ if (storage_class == NULL) {
+ *error_r = t_strdup_printf(
+ "Unknown mail storage driver %s", driver);
+ return NULL;
+ }
+ }
+
+ if (list_set->root_dir == NULL || *list_set->root_dir == '\0') {
+ /* no root directory given. is this allowed? */
+ const struct mailbox_list *list;
+
+ list = list_set->layout == NULL ? NULL :
+ mailbox_list_find_class(list_set->layout);
+ if (storage_class == NULL &&
+ (flags & MAIL_STORAGE_FLAG_NO_AUTODETECTION) == 0) {
+ /* autodetection should take care of this */
+ } else if (storage_class != NULL &&
+ (storage_class->class_flags & MAIL_STORAGE_CLASS_FLAG_NO_ROOT) != 0) {
+ /* root not required for this storage */
+ } else if (list != NULL &&
+ (list->props & MAILBOX_LIST_PROP_NO_ROOT) != 0) {
+ /* root not required for this layout */
+ } else {
+ *error_r = "Root mail directory not given";
+ return NULL;
+ }
+ }
+
+ if (storage_class != NULL) {
+ storage_class->v.get_list_settings(ns, list_set);
+ return storage_class;
+ }
+
+ storage_class = mail_storage_autodetect(ns, list_set);
+ if (storage_class != NULL)
+ return storage_class;
+
+ (void)mail_user_get_home(ns->user, &home);
+ if (home == NULL || *home == '\0') home = "(not set)";
+
+ if (ns->set->location == NULL || *ns->set->location == '\0') {
+ *error_r = t_strdup_printf(
+ "Mail storage autodetection failed with home=%s", home);
+ } else if (str_begins(ns->set->location, "auto:")) {
+ *error_r = t_strdup_printf(
+ "Autodetection failed for %s (home=%s)",
+ ns->set->location, home);
+ } else {
+ *error_r = t_strdup_printf(
+ "Ambiguous mail location setting, "
+ "don't know what to do with it: %s "
+ "(try prefixing it with mbox: or maildir:)",
+ ns->set->location);
+ }
+ return NULL;
+}
+
+static int
+mail_storage_verify_root(const char *root_dir, const char *dir_type,
+ const char **error_r)
+{
+ struct stat st;
+
+ if (stat(root_dir, &st) == 0) {
+ /* exists */
+ if (S_ISDIR(st.st_mode))
+ return 0;
+ *error_r = t_strdup_printf(
+ "Root mail directory is a file: %s", root_dir);
+ return -1;
+ } else if (errno == EACCES) {
+ *error_r = mail_error_eacces_msg("stat", root_dir);
+ return -1;
+ } else if (errno != ENOENT) {
+ *error_r = t_strdup_printf("stat(%s) failed: %m", root_dir);
+ return -1;
+ } else {
+ *error_r = t_strdup_printf(
+ "Root %s directory doesn't exist: %s", dir_type, root_dir);
+ return -1;
+ }
+}
+
+static int
+mail_storage_create_root(struct mailbox_list *list,
+ enum mail_storage_flags flags, const char **error_r)
+{
+ const char *root_dir, *type_name, *error;
+ enum mailbox_list_path_type type;
+
+ if (list->set.iter_from_index_dir) {
+ type = MAILBOX_LIST_PATH_TYPE_INDEX;
+ type_name = "index";
+ } else {
+ type = MAILBOX_LIST_PATH_TYPE_MAILBOX;
+ type_name = "mail";
+ }
+ if (!mailbox_list_get_root_path(list, type, &root_dir)) {
+ /* storage doesn't use directories (e.g. shared root) */
+ return 0;
+ }
+
+ if ((flags & MAIL_STORAGE_FLAG_NO_AUTOVERIFY) != 0) {
+ if (!event_want_debug_log(list->ns->user->event))
+ return 0;
+
+ /* we don't need to verify, but since debugging is
+ enabled, check and log if the root doesn't exist */
+ if (mail_storage_verify_root(root_dir, type_name, &error) < 0) {
+ e_debug(list->ns->user->event,
+ "Namespace %s: Creating storage despite: %s",
+ list->ns->prefix, error);
+ }
+ return 0;
+ }
+
+ if ((flags & MAIL_STORAGE_FLAG_NO_AUTOCREATE) == 0) {
+ /* If the directories don't exist, we'll just autocreate them
+ later. */
+ return 0;
+ }
+ return mail_storage_verify_root(root_dir, type_name, error_r);
+}
+
+static bool
+mail_storage_match_class(struct mail_storage *storage,
+ const struct mail_storage *storage_class,
+ const struct mailbox_list_settings *set)
+{
+ if (strcmp(storage->name, storage_class->name) != 0)
+ return FALSE;
+
+ if ((storage->class_flags & MAIL_STORAGE_CLASS_FLAG_UNIQUE_ROOT) != 0 &&
+ strcmp(storage->unique_root_dir,
+ (set->root_dir != NULL ? set->root_dir : "")) != 0)
+ return FALSE;
+
+ if (strcmp(storage->name, "shared") == 0) {
+ /* allow multiple independent shared namespaces */
+ return FALSE;
+ }
+ return TRUE;
+}
+
+static struct mail_storage *
+mail_storage_find(struct mail_user *user,
+ const struct mail_storage *storage_class,
+ const struct mailbox_list_settings *set)
+{
+ struct mail_storage *storage = user->storages;
+
+ for (; storage != NULL; storage = storage->next) {
+ if (mail_storage_match_class(storage, storage_class, set))
+ return storage;
+ }
+ return NULL;
+}
+
+int mail_storage_create_full(struct mail_namespace *ns, const char *driver,
+ const char *data, enum mail_storage_flags flags,
+ struct mail_storage **storage_r,
+ const char **error_r)
+{
+ struct mail_storage *storage_class, *storage = NULL;
+ struct mailbox_list *list;
+ struct mailbox_list_settings list_set;
+ enum mailbox_list_flags list_flags = 0;
+ const char *p;
+
+ if ((flags & MAIL_STORAGE_FLAG_KEEP_HEADER_MD5) == 0 &&
+ ns->mail_set->pop3_uidl_format != NULL) {
+ /* if pop3_uidl_format contains %m, we want to keep the
+ header MD5 sums stored even if we're not running POP3
+ right now. */
+ p = ns->mail_set->pop3_uidl_format;
+ while ((p = strchr(p, '%')) != NULL) {
+ if (p[1] == '%')
+ p += 2;
+ else if (var_get_key(++p) == 'm') {
+ flags |= MAIL_STORAGE_FLAG_KEEP_HEADER_MD5;
+ break;
+ }
+ }
+ }
+
+ mailbox_list_settings_init_defaults(&list_set);
+ if (data == NULL) {
+ /* autodetect */
+ } else if (driver != NULL && strcmp(driver, "shared") == 0) {
+ /* internal shared namespace */
+ list_set.root_dir = ns->user->set->base_dir;
+ } else {
+ if (driver == NULL)
+ mail_storage_set_autodetection(&data, &driver);
+ if (mailbox_list_settings_parse(ns->user, data, &list_set,
+ error_r) < 0)
+ return -1;
+ }
+
+ storage_class = mail_storage_get_class(ns, driver, &list_set, flags,
+ error_r);
+ if (storage_class == NULL)
+ return -1;
+ i_assert(list_set.layout != NULL);
+
+ if (ns->list == NULL) {
+ /* first storage for namespace */
+ if (mail_storage_is_mailbox_file(storage_class))
+ list_flags |= MAILBOX_LIST_FLAG_MAILBOX_FILES;
+ if ((storage_class->class_flags & MAIL_STORAGE_CLASS_FLAG_NO_ROOT) != 0)
+ list_flags |= MAILBOX_LIST_FLAG_NO_MAIL_FILES;
+ if ((storage_class->class_flags & MAIL_STORAGE_CLASS_FLAG_NO_LIST_DELETES) != 0)
+ list_flags |= MAILBOX_LIST_FLAG_NO_DELETES;
+ if (mailbox_list_create(list_set.layout, ns, &list_set,
+ list_flags, &list, error_r) < 0) {
+ *error_r = t_strdup_printf("Mailbox list driver %s: %s",
+ list_set.layout, *error_r);
+ return -1;
+ }
+ if ((storage_class->class_flags & MAIL_STORAGE_CLASS_FLAG_NO_ROOT) == 0) {
+ if (mail_storage_create_root(ns->list, flags, error_r) < 0)
+ return -1;
+ }
+ }
+
+ storage = mail_storage_find(ns->user, storage_class, &list_set);
+ if (storage != NULL) {
+ /* using an existing storage */
+ storage->refcount++;
+ mail_namespace_add_storage(ns, storage);
+ *storage_r = storage;
+ return 0;
+ }
+
+ storage = storage_class->v.alloc();
+ if (storage->lost_mailbox_prefix == NULL)
+ storage->lost_mailbox_prefix = MAIL_STORAGE_LOST_MAILBOX_PREFIX;
+ storage->refcount = 1;
+ storage->storage_class = storage_class;
+ storage->user = ns->user;
+ storage->set = ns->mail_set;
+ storage->flags = flags;
+ storage->event = event_create(ns->user->event);
+ if (storage_class->event_category != NULL)
+ event_add_category(storage->event, storage_class->event_category);
+ p_array_init(&storage->module_contexts, storage->pool, 5);
+
+ if (storage->v.create != NULL &&
+ storage->v.create(storage, ns, error_r) < 0) {
+ *error_r = t_strdup_printf("%s: %s", storage->name, *error_r);
+ event_unref(&storage->event);
+ pool_unref(&storage->pool);
+ return -1;
+ }
+
+ /* If storage supports list index rebuild,
+ provide default mailboxes_fs unless storage
+ wants to use its own. */
+ if (storage->v.list_index_rebuild != NULL &&
+ storage->mailboxes_fs == NULL) {
+ struct fs_settings fs_set;
+ struct ssl_iostream_settings ssl_set;
+ const char *error;
+ i_zero(&fs_set);
+
+ mail_user_init_fs_settings(storage->user, &fs_set, &ssl_set);
+ if (fs_init("posix", "", &fs_set, &storage->mailboxes_fs,
+ &error) < 0) {
+ *error_r = t_strdup_printf("fs_init(posix) failed: %s", error);
+ storage->v.destroy(storage);
+ return -1;
+ }
+ }
+
+ T_BEGIN {
+ hook_mail_storage_created(storage);
+ } T_END;
+
+ i_assert(storage->unique_root_dir != NULL ||
+ (storage->class_flags & MAIL_STORAGE_CLASS_FLAG_UNIQUE_ROOT) == 0);
+ DLLIST_PREPEND(&ns->user->storages, storage);
+ mail_namespace_add_storage(ns, storage);
+ *storage_r = storage;
+ return 0;
+}
+
+int mail_storage_create(struct mail_namespace *ns, const char *driver,
+ enum mail_storage_flags flags, const char **error_r)
+{
+ struct mail_storage *storage;
+
+ return mail_storage_create_full(ns, driver, ns->set->location,
+ flags, &storage, error_r);
+}
+
+void mail_storage_unref(struct mail_storage **_storage)
+{
+ struct mail_storage *storage = *_storage;
+
+ i_assert(storage->refcount > 0);
+
+ /* set *_storage=NULL only after calling destroy() callback.
+ for example mdbox wants to access ns->storage */
+ if (--storage->refcount > 0) {
+ *_storage = NULL;
+ return;
+ }
+
+ if (storage->mailboxes != NULL) {
+ i_panic("Trying to deinit storage without freeing mailbox %s",
+ storage->mailboxes->vname);
+ }
+ if (storage->obj_refcount != 0)
+ i_panic("Trying to deinit storage before freeing its objects");
+
+ DLLIST_REMOVE(&storage->user->storages, storage);
+
+ storage->v.destroy(storage);
+ i_free(storage->last_internal_error);
+ i_free(storage->error_string);
+ if (array_is_created(&storage->error_stack)) {
+ i_assert(array_count(&storage->error_stack) == 0);
+ array_free(&storage->error_stack);
+ }
+ fs_unref(&storage->mailboxes_fs);
+ event_unref(&storage->event);
+
+ *_storage = NULL;
+ pool_unref(&storage->pool);
+
+ mail_index_alloc_cache_destroy_unrefed();
+}
+
+void mail_storage_obj_ref(struct mail_storage *storage)
+{
+ i_assert(storage->refcount > 0);
+
+ if (storage->obj_refcount++ == 0)
+ mail_user_ref(storage->user);
+}
+
+void mail_storage_obj_unref(struct mail_storage *storage)
+{
+ i_assert(storage->refcount > 0);
+ i_assert(storage->obj_refcount > 0);
+
+ if (--storage->obj_refcount == 0) {
+ struct mail_user *user = storage->user;
+ mail_user_unref(&user);
+ }
+}
+
+void mail_storage_clear_error(struct mail_storage *storage)
+{
+ i_free_and_null(storage->error_string);
+
+ i_free(storage->last_internal_error);
+ storage->last_error_is_internal = FALSE;
+ storage->error = MAIL_ERROR_NONE;
+}
+
+void mail_storage_set_error(struct mail_storage *storage,
+ enum mail_error error, const char *string)
+{
+ if (storage->error_string != string) {
+ i_free(storage->error_string);
+ storage->error_string = i_strdup(string);
+ }
+ storage->last_error_is_internal = FALSE;
+ storage->error = error;
+}
+
+void mail_storage_set_internal_error(struct mail_storage *storage)
+{
+ const char *str;
+
+ str = t_strflocaltime(MAIL_ERRSTR_CRITICAL_MSG_STAMP, ioloop_time);
+
+ i_free(storage->error_string);
+ storage->error_string = i_strdup(str);
+ storage->error = MAIL_ERROR_TEMP;
+
+ /* this function doesn't set last_internal_error, so
+ last_error_is_internal can't be TRUE. */
+ storage->last_error_is_internal = FALSE;
+ i_free(storage->last_internal_error);
+}
+
+void mail_storage_set_critical(struct mail_storage *storage,
+ const char *fmt, ...)
+{
+ char *old_error = storage->error_string;
+ char *old_internal_error = storage->last_internal_error;
+ va_list va;
+
+ storage->error_string = NULL;
+ storage->last_internal_error = NULL;
+ /* critical errors may contain sensitive data, so let user
+ see only "Internal error" with a timestamp to make it
+ easier to look from log files the actual error message. */
+ mail_storage_set_internal_error(storage);
+
+ va_start(va, fmt);
+ storage->last_internal_error = i_strdup_vprintf(fmt, va);
+ va_end(va);
+ storage->last_error_is_internal = TRUE;
+ e_error(storage->event, "%s", storage->last_internal_error);
+
+ /* free the old_error and old_internal_error only after the new error
+ is generated, because they may be one of the parameters. */
+ i_free(old_error);
+ i_free(old_internal_error);
+}
+
+void mailbox_set_critical(struct mailbox *box, const char *fmt, ...)
+{
+ va_list va;
+
+ va_start(va, fmt);
+ T_BEGIN {
+ mail_storage_set_critical(box->storage, "Mailbox %s: %s",
+ box->vname, t_strdup_vprintf(fmt, va));
+ } T_END;
+ va_end(va);
+}
+
+void mail_set_critical(struct mail *mail, const char *fmt, ...)
+{
+ va_list va;
+
+ va_start(va, fmt);
+ T_BEGIN {
+ if (mail->saving) {
+ mailbox_set_critical(mail->box, "Saving mail: %s",
+ t_strdup_vprintf(fmt, va));
+ } else {
+ mailbox_set_critical(mail->box, "UID=%u: %s",
+ mail->uid, t_strdup_vprintf(fmt, va));
+ }
+ } T_END;
+ va_end(va);
+}
+
+const char *mail_storage_get_last_internal_error(struct mail_storage *storage,
+ enum mail_error *error_r)
+{
+ if (error_r != NULL)
+ *error_r = storage->error;
+ if (storage->last_error_is_internal) {
+ i_assert(storage->last_internal_error != NULL);
+ return storage->last_internal_error;
+ }
+ return mail_storage_get_last_error(storage, error_r);
+}
+
+const char *mailbox_get_last_internal_error(struct mailbox *box,
+ enum mail_error *error_r)
+{
+ return mail_storage_get_last_internal_error(mailbox_get_storage(box),
+ error_r);
+}
+
+void mail_storage_copy_error(struct mail_storage *dest,
+ struct mail_storage *src)
+{
+ const char *str;
+ enum mail_error error;
+
+ if (src == dest)
+ return;
+
+ str = mail_storage_get_last_error(src, &error);
+ mail_storage_set_error(dest, error, str);
+}
+
+void mail_storage_copy_list_error(struct mail_storage *storage,
+ struct mailbox_list *list)
+{
+ const char *str;
+ enum mail_error error;
+
+ str = mailbox_list_get_last_error(list, &error);
+ mail_storage_set_error(storage, error, str);
+}
+
+void mailbox_set_index_error(struct mailbox *box)
+{
+ if (mail_index_is_deleted(box->index)) {
+ mailbox_set_deleted(box);
+ mail_index_reset_error(box->index);
+ } else {
+ mail_storage_set_index_error(box->storage, box->index);
+ }
+}
+
+void mail_storage_set_index_error(struct mail_storage *storage,
+ struct mail_index *index)
+{
+ const char *index_error;
+
+ mail_storage_set_internal_error(storage);
+ /* use the lib-index's error as our internal error string */
+ index_error = mail_index_get_error_message(index);
+ if (index_error == NULL)
+ index_error = "BUG: Unknown internal index error";
+ storage->last_internal_error = i_strdup(index_error);
+ storage->last_error_is_internal = TRUE;
+ mail_index_reset_error(index);
+}
+
+const struct mail_storage_settings *
+mail_storage_get_settings(struct mail_storage *storage)
+{
+ return storage->set;
+}
+
+struct mail_user *mail_storage_get_user(struct mail_storage *storage)
+{
+ return storage->user;
+}
+
+void mail_storage_set_callbacks(struct mail_storage *storage,
+ struct mail_storage_callbacks *callbacks,
+ void *context)
+{
+ storage->callbacks = *callbacks;
+ storage->callback_context = context;
+}
+
+int mail_storage_purge(struct mail_storage *storage)
+{
+ return storage->v.purge == NULL ? 0 :
+ storage->v.purge(storage);
+}
+
+const char *mail_storage_get_last_error(struct mail_storage *storage,
+ enum mail_error *error_r)
+{
+ /* We get here only in error situations, so we have to return some
+ error. If storage->error is NONE, it means we forgot to set it at
+ some point.. */
+ if (storage->error == MAIL_ERROR_NONE) {
+ if (error_r != NULL)
+ *error_r = MAIL_ERROR_TEMP;
+ return storage->error_string != NULL ? storage->error_string :
+ "BUG: Unknown internal error";
+ }
+
+ if (storage->error_string == NULL) {
+ /* This shouldn't happen.. */
+ storage->error_string =
+ i_strdup_printf("BUG: Unknown 0x%x error",
+ storage->error);
+ }
+
+ if (error_r != NULL)
+ *error_r = storage->error;
+ return storage->error_string;
+}
+
+const char *mailbox_get_last_error(struct mailbox *box,
+ enum mail_error *error_r)
+{
+ return mail_storage_get_last_error(box->storage, error_r);
+}
+
+enum mail_error mailbox_get_last_mail_error(struct mailbox *box)
+{
+ enum mail_error error;
+
+ mail_storage_get_last_error(box->storage, &error);
+ return error;
+}
+
+void mail_storage_last_error_push(struct mail_storage *storage)
+{
+ struct mail_storage_error *err;
+
+ if (!array_is_created(&storage->error_stack))
+ i_array_init(&storage->error_stack, 2);
+ err = array_append_space(&storage->error_stack);
+ err->error_string = i_strdup(storage->error_string);
+ err->error = storage->error;
+ err->last_error_is_internal = storage->last_error_is_internal;
+ if (err->last_error_is_internal)
+ err->last_internal_error = i_strdup(storage->last_internal_error);
+}
+
+void mail_storage_last_error_pop(struct mail_storage *storage)
+{
+ unsigned int count = array_count(&storage->error_stack);
+ const struct mail_storage_error *err =
+ array_idx(&storage->error_stack, count-1);
+
+ i_free(storage->error_string);
+ i_free(storage->last_internal_error);
+ storage->error_string = err->error_string;
+ storage->error = err->error;
+ storage->last_error_is_internal = err->last_error_is_internal;
+ storage->last_internal_error = err->last_internal_error;
+ array_delete(&storage->error_stack, count-1, 1);
+}
+
+bool mail_storage_is_mailbox_file(struct mail_storage *storage)
+{
+ return (storage->class_flags &
+ MAIL_STORAGE_CLASS_FLAG_MAILBOX_IS_FILE) != 0;
+}
+
+bool mail_storage_set_error_from_errno(struct mail_storage *storage)
+{
+ const char *error_string;
+ enum mail_error error;
+
+ if (!mail_error_from_errno(&error, &error_string))
+ return FALSE;
+ if (event_want_debug_log(storage->event) && error != MAIL_ERROR_NOTFOUND) {
+ /* debugging is enabled - admin may be debugging a
+ (permission) problem, so return FALSE to get the caller to
+ log the full error message. */
+ return FALSE;
+ }
+
+ mail_storage_set_error(storage, error, error_string);
+ return TRUE;
+}
+
+const struct mailbox_settings *
+mailbox_settings_find(struct mail_namespace *ns, const char *vname)
+{
+ struct mailbox_settings *box_set;
+
+ if (!array_is_created(&ns->set->mailboxes))
+ return NULL;
+
+ if (ns->prefix_len > 0 &&
+ strncmp(ns->prefix, vname, ns->prefix_len-1) == 0) {
+ if (vname[ns->prefix_len-1] == mail_namespace_get_sep(ns))
+ vname += ns->prefix_len;
+ else if (vname[ns->prefix_len-1] == '\0') {
+ /* namespace prefix itself */
+ vname = "";
+ }
+ }
+ array_foreach_elem(&ns->set->mailboxes, box_set) {
+ if (strcmp(box_set->name, vname) == 0)
+ return box_set;
+ }
+ return NULL;
+}
+
+struct mailbox *mailbox_alloc(struct mailbox_list *list, const char *vname,
+ enum mailbox_flags flags)
+{
+ struct mailbox_list *new_list = list;
+ struct mail_storage *storage;
+ struct mailbox *box;
+ enum mail_error open_error = 0;
+ const char *errstr = NULL;
+
+ i_assert(uni_utf8_str_is_valid(vname));
+
+ if (strncasecmp(vname, "INBOX", 5) == 0 &&
+ !str_begins(vname, "INBOX")) {
+ /* make sure INBOX shows up in uppercase everywhere. do this
+ regardless of whether we're in inbox=yes namespace, because
+ clients expect INBOX to be case insensitive regardless of
+ server's internal configuration. */
+ if (vname[5] == '\0')
+ vname = "INBOX";
+ else if (vname[5] != mail_namespace_get_sep(list->ns))
+ /* not INBOX prefix */ ;
+ else if (strncasecmp(list->ns->prefix, vname, 6) == 0 &&
+ !str_begins(list->ns->prefix, "INBOX")) {
+ mailbox_list_set_critical(list,
+ "Invalid server configuration: "
+ "Namespace prefix=%s must be uppercase INBOX",
+ list->ns->prefix);
+ open_error = MAIL_ERROR_TEMP;
+ } else {
+ vname = t_strconcat("INBOX", vname + 5, NULL);
+ }
+ }
+
+ T_BEGIN {
+ if (mailbox_list_get_storage(&new_list, vname, &storage) < 0) {
+ /* do a delayed failure at mailbox_open() */
+ storage = mail_namespace_get_default_storage(list->ns);
+ errstr = mailbox_list_get_last_error(new_list, &open_error);
+ errstr = t_strdup(errstr);
+ }
+
+ box = storage->v.mailbox_alloc(storage, new_list, vname, flags);
+ box->set = mailbox_settings_find(new_list->ns, vname);
+ box->open_error = open_error;
+ if (open_error != 0)
+ mail_storage_set_error(storage, open_error, errstr);
+ hook_mailbox_allocated(box);
+ } T_END;
+
+ DLLIST_PREPEND(&box->storage->mailboxes, box);
+ mail_storage_obj_ref(box->storage);
+ return box;
+}
+
+struct mailbox *mailbox_alloc_guid(struct mailbox_list *list,
+ const guid_128_t guid,
+ enum mailbox_flags flags)
+{
+ struct mailbox *box = NULL;
+ struct mailbox_metadata metadata;
+ enum mail_error open_error = MAIL_ERROR_TEMP;
+ const char *vname;
+
+ if (mailbox_guid_cache_find(list, guid, &vname) < 0) {
+ vname = NULL;
+ } else if (vname != NULL) {
+ box = mailbox_alloc(list, vname, flags);
+ if (mailbox_get_metadata(box, MAILBOX_METADATA_GUID,
+ &metadata) < 0) {
+ } else if (memcmp(metadata.guid, guid,
+ sizeof(metadata.guid)) != 0) {
+ /* GUID mismatch, refresh cache and try again */
+ mailbox_free(&box);
+ mailbox_guid_cache_refresh(list);
+ return mailbox_alloc_guid(list, guid, flags);
+ } else {
+ /* successfully opened the correct mailbox */
+ return box;
+ }
+ e_error(list->ns->user->event, "mailbox_alloc_guid(%s): "
+ "Couldn't verify mailbox GUID: %s",
+ guid_128_to_string(guid),
+ mailbox_get_last_internal_error(box, NULL));
+ vname = NULL;
+ mailbox_free(&box);
+ } else {
+ vname = t_strdup_printf("(nonexistent mailbox with GUID=%s)",
+ guid_128_to_string(guid));
+ open_error = MAIL_ERROR_NOTFOUND;
+ }
+
+ if (vname == NULL) {
+ vname = t_strdup_printf("(error in mailbox with GUID=%s)",
+ guid_128_to_string(guid));
+ }
+ box = mailbox_alloc(list, vname, flags);
+ box->open_error = open_error;
+ return box;
+}
+
+static bool
+str_contains_special_use(const char *str, const char *special_use)
+{
+ const char *const *uses;
+
+ i_assert(special_use != NULL);
+ if (*special_use != '\\')
+ return FALSE;
+
+ bool ret;
+ T_BEGIN {
+ uses = t_strsplit_spaces(str, " ");
+ ret = str_array_icase_find(uses, special_use);
+ } T_END;
+ return ret;
+}
+
+static int
+namespace_find_special_use(struct mail_namespace *ns, const char *special_use,
+ const char **vname_r, enum mail_error *error_code_r)
+{
+ struct mailbox_list *list = ns->list;
+ struct mailbox_list_iterate_context *ctx;
+ const struct mailbox_info *info;
+ int ret = 0;
+
+ *vname_r = NULL;
+ *error_code_r = MAIL_ERROR_NONE;
+
+ if (!ns->special_use_mailboxes)
+ return 0;
+ if (!HAS_ALL_BITS(ns->type, MAIL_NAMESPACE_TYPE_PRIVATE))
+ return 0;
+
+ ctx = mailbox_list_iter_init(list, "*",
+ MAILBOX_LIST_ITER_SELECT_SPECIALUSE |
+ MAILBOX_LIST_ITER_RETURN_SPECIALUSE);
+ while ((info = mailbox_list_iter_next(ctx)) != NULL) {
+ if ((info->flags &
+ (MAILBOX_NOSELECT | MAILBOX_NONEXISTENT)) != 0)
+ continue;
+ /* iter can only return mailboxes that have non-empty
+ special-use */
+ i_assert(info->special_use != NULL &&
+ *info->special_use != '\0');
+
+ if (str_contains_special_use(info->special_use, special_use)) {
+ *vname_r = t_strdup(info->vname);
+ ret = 1;
+ break;
+ }
+ }
+ if (mailbox_list_iter_deinit(&ctx) < 0) {
+ const char *error;
+
+ error = mailbox_list_get_last_error(ns->list, error_code_r);
+ e_error(ns->user->event,
+ "Failed to find mailbox with SPECIAL-USE flag '%s' "
+ "in namespace '%s': %s",
+ special_use, ns->prefix, error);
+ return -1;
+ }
+ return ret;
+}
+
+static int
+namespaces_find_special_use(struct mail_namespace *namespaces,
+ const char *special_use,
+ struct mail_namespace **ns_r,
+ const char **vname_r, enum mail_error *error_code_r)
+{
+ struct mail_namespace *ns_inbox;
+ int ret;
+
+ *error_code_r = MAIL_ERROR_NONE;
+ *vname_r = NULL;
+
+ /* check user's INBOX namespace first */
+ *ns_r = ns_inbox = mail_namespace_find_inbox(namespaces);
+ ret = namespace_find_special_use(*ns_r, special_use,
+ vname_r, error_code_r);
+ if (ret != 0)
+ return ret;
+
+ /* check other namespaces */
+ for (*ns_r = namespaces; *ns_r != NULL; *ns_r = (*ns_r)->next) {
+ if (*ns_r == ns_inbox) {
+ /* already checked */
+ continue;
+ }
+ ret = namespace_find_special_use(*ns_r, special_use,
+ vname_r, error_code_r);
+ if (ret != 0)
+ return ret;
+ }
+
+ *ns_r = ns_inbox;
+ return 0;
+}
+
+struct mailbox *
+mailbox_alloc_for_user(struct mail_user *user, const char *mname,
+ enum mailbox_flags flags)
+{
+ struct mail_namespace *ns;
+ struct mailbox *box;
+ const char *vname;
+ enum mail_error open_error = MAIL_ERROR_NONE;
+ int ret;
+
+ if (HAS_ALL_BITS(flags, MAILBOX_FLAG_SPECIAL_USE)) {
+ ret = namespaces_find_special_use(user->namespaces, mname,
+ &ns, &vname, &open_error);
+ if (ret < 0) {
+ i_assert(open_error != MAIL_ERROR_NONE);
+ vname = t_strdup_printf(
+ "(error finding mailbox with SPECIAL-USE=%s)",
+ mname);
+ } else if (ret == 0) {
+ i_assert(open_error == MAIL_ERROR_NONE);
+ vname = t_strdup_printf(
+ "(nonexistent mailbox with SPECIAL-USE=%s)",
+ mname);
+ open_error = MAIL_ERROR_NOTFOUND;
+ }
+ } else {
+ vname = mname;
+ ns = mail_namespace_find(user->namespaces, mname);
+ }
+
+ if (HAS_ALL_BITS(flags, MAILBOX_FLAG_POST_SESSION)) {
+ flags |= MAILBOX_FLAG_SAVEONLY;
+
+ if (strcmp(vname, ns->prefix) == 0 &&
+ (ns->flags & NAMESPACE_FLAG_INBOX_USER) != 0) {
+ /* delivering to a namespace prefix means we actually
+ want to deliver to the INBOX instead */
+ vname = "INBOX";
+ ns = mail_namespace_find_inbox(user->namespaces);
+ }
+
+ if (strcasecmp(vname, "INBOX") == 0) {
+ /* deliveries to INBOX must always succeed,
+ regardless of ACLs */
+ flags |= MAILBOX_FLAG_IGNORE_ACLS;
+ }
+ }
+
+ i_assert(ns != NULL);
+ box = mailbox_alloc(ns->list, vname, flags);
+ if (open_error != MAIL_ERROR_NONE)
+ box->open_error = open_error;
+ return box;
+}
+
+bool mailbox_is_autocreated(struct mailbox *box)
+{
+ if (box->inbox_user)
+ return TRUE;
+ if ((box->flags & MAILBOX_FLAG_AUTO_CREATE) != 0)
+ return TRUE;
+ return box->set != NULL &&
+ strcmp(box->set->autocreate, MAILBOX_SET_AUTO_NO) != 0;
+}
+
+bool mailbox_is_autosubscribed(struct mailbox *box)
+{
+ if ((box->flags & MAILBOX_FLAG_AUTO_SUBSCRIBE) != 0)
+ return TRUE;
+ return box->set != NULL &&
+ strcmp(box->set->autocreate, MAILBOX_SET_AUTO_SUBSCRIBE) == 0;
+}
+
+static int mailbox_autocreate(struct mailbox *box)
+{
+ const char *errstr;
+ enum mail_error error;
+
+ if (mailbox_create(box, NULL, FALSE) < 0) {
+ errstr = mailbox_get_last_internal_error(box, &error);
+ if (error == MAIL_ERROR_NOTFOUND && box->acl_no_lookup_right) {
+ /* ACL prevents creating this mailbox */
+ return -1;
+ }
+ if (error != MAIL_ERROR_EXISTS) {
+ mailbox_set_critical(box,
+ "Failed to autocreate mailbox: %s",
+ errstr);
+ return -1;
+ }
+ } else if (mailbox_is_autosubscribed(box)) {
+ if (mailbox_set_subscribed(box, TRUE) < 0) {
+ mailbox_set_critical(box,
+ "Failed to autosubscribe to mailbox: %s",
+ mailbox_get_last_internal_error(box, NULL));
+ return -1;
+ }
+ }
+ return 0;
+}
+
+static int mailbox_autocreate_and_reopen(struct mailbox *box)
+{
+ int ret;
+
+ if (mailbox_autocreate(box) < 0)
+ return -1;
+ mailbox_close(box);
+
+ ret = box->v.open(box);
+ if (ret < 0 && box->inbox_user && !box->acl_no_lookup_right &&
+ !box->storage->user->inbox_open_error_logged) {
+ box->storage->user->inbox_open_error_logged = TRUE;
+ mailbox_set_critical(box,
+ "Opening INBOX failed: %s",
+ mailbox_get_last_internal_error(box, NULL));
+ }
+ return ret;
+}
+
+static bool
+mailbox_name_verify_extra_separators(const char *vname, char sep,
+ const char **error_r)
+{
+ unsigned int i;
+ bool prev_sep = FALSE;
+
+ /* Make sure the vname doesn't have extra separators:
+
+ 1) Must not have adjacent separators. If we allow these, these could
+ end up pointing to existing mailboxes due to kernel ignoring
+ duplicate '/' in paths. However, this might cause us to handle some
+ of our own checks wrong, such as skipping ACLs.
+
+ 2) Must not end with separator. Similar reasoning as above.
+ */
+ for (i = 0; vname[i] != '\0'; i++) {
+ if (vname[i] == sep) {
+ if (prev_sep) {
+ *error_r = "Has adjacent hierarchy separators";
+ return FALSE;
+ }
+ prev_sep = TRUE;
+ } else {
+ prev_sep = FALSE;
+ }
+ }
+ if (prev_sep && i > 0) {
+ *error_r = "Ends with hierarchy separator";
+ return FALSE;
+ }
+ return TRUE;
+}
+
+static bool
+mailbox_verify_name_prefix(struct mail_namespace *ns, const char **vnamep,
+ const char **error_r)
+{
+ const char *vname = *vnamep;
+
+ if (ns->prefix_len == 0)
+ return TRUE;
+
+ /* vname is either "namespace/box" or "namespace" */
+ if (strncmp(vname, ns->prefix, ns->prefix_len-1) != 0 ||
+ (vname[ns->prefix_len-1] != '\0' &&
+ vname[ns->prefix_len-1] != ns->prefix[ns->prefix_len-1])) {
+ /* User input shouldn't normally be able to get us in
+ here. The main reason this isn't an assert is to
+ allow any input at all to mailbox_verify_*_name()
+ without crashing. */
+ *error_r = t_strdup_printf("Missing namespace prefix '%s'",
+ ns->prefix);
+ return FALSE;
+ }
+ vname += ns->prefix_len - 1;
+ if (vname[0] != '\0') {
+ i_assert(vname[0] == ns->prefix[ns->prefix_len-1]);
+ vname++;
+
+ if (vname[0] == '\0') {
+ /* "namespace/" isn't a valid mailbox name. */
+ *error_r = "Ends with hierarchy separator";
+ return FALSE;
+ }
+ }
+ *vnamep = vname;
+ return TRUE;
+}
+
+static int mailbox_verify_name_int(struct mailbox *box)
+{
+ struct mail_namespace *ns = box->list->ns;
+ const char *error, *vname = box->vname;
+ char list_sep, ns_sep;
+
+ if (box->inbox_user) {
+ /* this is INBOX - don't bother with further checks */
+ return 0;
+ }
+
+ /* Verify the namespace prefix here. Change vname to skip the prefix
+ for the following checks. */
+ if (!mailbox_verify_name_prefix(box->list->ns, &vname, &error)) {
+ mail_storage_set_error(box->storage, MAIL_ERROR_PARAMS,
+ t_strdup_printf("Invalid mailbox name '%s': %s",
+ str_sanitize(vname, 80), error));
+ return -1;
+ }
+
+ list_sep = mailbox_list_get_hierarchy_sep(box->list);
+ ns_sep = mail_namespace_get_sep(ns);
+
+ /* If namespace { separator } differs from the mailbox_list separator,
+ the list separator can't actually be used in the mailbox name
+ unless it's escaped with storage_name_escape_char. For example if
+ namespace separator is '/' and LAYOUT=Maildir++ has '.' as the
+ separator, there's no way to use '.' in the mailbox name (without
+ escaping) because it would end up becoming a hierarchy separator. */
+ if (ns_sep != list_sep &&
+ box->list->set.storage_name_escape_char == '\0' &&
+ strchr(vname, list_sep) != NULL) {
+ mail_storage_set_error(box->storage, MAIL_ERROR_PARAMS, t_strdup_printf(
+ "Character not allowed in mailbox name: '%c'", list_sep));
+ return -1;
+ }
+ /* vname must not begin with the hierarchy separator normally.
+ For example we don't want to allow accessing /etc/passwd. However,
+ if mail_full_filesystem_access=yes, we do actually want to allow
+ that. */
+ if (vname[0] == ns_sep &&
+ !box->storage->set->mail_full_filesystem_access) {
+ mail_storage_set_error(box->storage, MAIL_ERROR_PARAMS,
+ "Invalid mailbox name: Begins with hierarchy separator");
+ return -1;
+ }
+
+ if (!mailbox_name_verify_extra_separators(vname, ns_sep, &error) ||
+ !mailbox_list_is_valid_name(box->list, box->name, &error)) {
+ mail_storage_set_error(box->storage, MAIL_ERROR_PARAMS,
+ t_strdup_printf("Invalid mailbox name: %s", error));
+ return -1;
+ }
+ return 0;
+}
+
+int mailbox_verify_name(struct mailbox *box)
+{
+ int ret;
+ T_BEGIN {
+ ret = mailbox_verify_name_int(box);
+ } T_END;
+ return ret;
+}
+
+static int mailbox_verify_existing_name_int(struct mailbox *box)
+{
+ const char *path;
+
+ if (box->opened)
+ return 0;
+
+ if (mailbox_verify_name(box) < 0)
+ return -1;
+
+ /* Make sure box->_path is set, so mailbox_get_path() works from
+ now on. Note that this may also fail with some backends if the
+ mailbox doesn't exist. */
+ if (mailbox_get_path_to(box, MAILBOX_LIST_PATH_TYPE_MAILBOX, &path) < 0) {
+ if (box->storage->error != MAIL_ERROR_NOTFOUND ||
+ !mailbox_is_autocreated(box))
+ return -1;
+ /* if this is an autocreated mailbox, create it now */
+ if (mailbox_autocreate(box) < 0)
+ return -1;
+ mailbox_close(box);
+ if (mailbox_get_path_to(box, MAILBOX_LIST_PATH_TYPE_MAILBOX,
+ &path) < 0)
+ return -1;
+ }
+ return 0;
+}
+
+static int mailbox_verify_existing_name(struct mailbox *box)
+{
+ int ret;
+ T_BEGIN {
+ ret = mailbox_verify_existing_name_int(box);
+ } T_END;
+ return ret;
+}
+
+static bool mailbox_name_has_control_chars(const char *name)
+{
+ const char *p;
+
+ for (p = name; *p != '\0'; p++) {
+ if ((unsigned char)*p < ' ')
+ return TRUE;
+ }
+ return FALSE;
+}
+
+void mailbox_skip_create_name_restrictions(struct mailbox *box, bool set)
+{
+ box->skip_create_name_restrictions = set;
+}
+
+int mailbox_verify_create_name(struct mailbox *box)
+{
+ /* mailbox_alloc() already checks that vname is valid UTF8,
+ so we don't need to verify that.
+
+ check vname instead of storage name, because vname is what is
+ visible to users, while storage name may be a fixed length GUID. */
+ if (mailbox_verify_name(box) < 0)
+ return -1;
+ if (box->skip_create_name_restrictions)
+ return 0;
+ if (mailbox_name_has_control_chars(box->vname)) {
+ mail_storage_set_error(box->storage, MAIL_ERROR_PARAMS,
+ "Control characters not allowed in new mailbox names");
+ return -1;
+ }
+ if (strlen(box->vname) > MAILBOX_LIST_NAME_MAX_LENGTH) {
+ mail_storage_set_error(box->storage, MAIL_ERROR_PARAMS,
+ "Mailbox name too long");
+ return -1;
+ }
+ /* check individual component names, too */
+ const char *old_name = box->name;
+ const char *name;
+ const char sep = mailbox_list_get_hierarchy_sep(box->list);
+ while((name = strchr(old_name, sep)) != NULL) {
+ if (name - old_name > MAILBOX_MAX_HIERARCHY_NAME_LENGTH) {
+ mail_storage_set_error(box->storage, MAIL_ERROR_PARAMS,
+ "Mailbox name too long");
+ return -1;
+ }
+ name++;
+ old_name = name;
+ }
+ if (strlen(old_name) > MAILBOX_MAX_HIERARCHY_NAME_LENGTH) {
+ mail_storage_set_error(box->storage, MAIL_ERROR_PARAMS,
+ "Mailbox name too long");
+ return -1;
+ }
+ return 0;
+}
+
+static bool have_listable_namespace_prefix(struct mail_namespace *ns,
+ const char *name)
+{
+ size_t name_len = strlen(name);
+
+ for (; ns != NULL; ns = ns->next) {
+ if ((ns->flags & (NAMESPACE_FLAG_LIST_PREFIX |
+ NAMESPACE_FLAG_LIST_CHILDREN)) == 0)
+ continue;
+
+ if (ns->prefix_len <= name_len)
+ continue;
+
+ /* if prefix has multiple hierarchies, match
+ any of the hierarchies */
+ if (strncmp(ns->prefix, name, name_len) == 0 &&
+ ns->prefix[name_len] == mail_namespace_get_sep(ns))
+ return TRUE;
+ }
+ return FALSE;
+}
+
+int mailbox_exists(struct mailbox *box, bool auto_boxes,
+ enum mailbox_existence *existence_r)
+{
+ switch (box->open_error) {
+ case 0:
+ break;
+ case MAIL_ERROR_NOTFOUND:
+ *existence_r = MAILBOX_EXISTENCE_NONE;
+ return 0;
+ default:
+ /* unsure if this exists or not */
+ return -1;
+ }
+ if (mailbox_verify_name(box) < 0) {
+ /* the mailbox name is invalid. we don't know if it currently
+ exists or not, but since it can never be accessed in any way
+ report it as if it didn't exist. */
+ *existence_r = MAILBOX_EXISTENCE_NONE;
+ return 0;
+ }
+
+ if (box->v.exists(box, auto_boxes, existence_r) < 0)
+ return -1;
+
+ if (!box->inbox_user && *existence_r == MAILBOX_EXISTENCE_NOSELECT &&
+ have_listable_namespace_prefix(box->storage->user->namespaces,
+ box->vname)) {
+ /* listable namespace prefix always exists. */
+ *existence_r = MAILBOX_EXISTENCE_NOSELECT;
+ return 0;
+ }
+
+ /* if this is a shared namespace with only INBOX and
+ mail_shared_explicit_inbox=no, we'll need to mark the namespace as
+ usable here since nothing else will. */
+ box->list->ns->flags |= NAMESPACE_FLAG_USABLE;
+ return 0;
+}
+
+static int ATTR_NULL(2)
+mailbox_open_full(struct mailbox *box, struct istream *input)
+{
+ int ret;
+
+ if (box->opened)
+ return 0;
+
+ switch (box->open_error) {
+ case 0:
+ e_debug(box->event, "Mailbox opened");
+ break;
+ case MAIL_ERROR_NOTFOUND:
+ mail_storage_set_error(box->storage, MAIL_ERROR_NOTFOUND,
+ T_MAIL_ERR_MAILBOX_NOT_FOUND(box->vname));
+ return -1;
+ default:
+ mail_storage_set_internal_error(box->storage);
+ box->storage->error = box->open_error;
+ return -1;
+ }
+
+ if (mailbox_verify_existing_name(box) < 0)
+ return -1;
+
+ if (input != NULL) {
+ if ((box->storage->class_flags &
+ MAIL_STORAGE_CLASS_FLAG_OPEN_STREAMS) == 0) {
+ mailbox_set_critical(box,
+ "Storage doesn't support streamed mailboxes");
+ return -1;
+ }
+ box->input = input;
+ box->flags |= MAILBOX_FLAG_READONLY;
+ i_stream_ref(box->input);
+ }
+
+ T_BEGIN {
+ ret = box->v.open(box);
+ } T_END;
+
+ if (ret < 0 && box->storage->error == MAIL_ERROR_NOTFOUND &&
+ !box->deleting && !box->creating &&
+ box->input == NULL && mailbox_is_autocreated(box)) T_BEGIN {
+ ret = mailbox_autocreate_and_reopen(box);
+ } T_END;
+
+ if (ret < 0) {
+ if (box->input != NULL)
+ i_stream_unref(&box->input);
+ return -1;
+ }
+
+ box->list->ns->flags |= NAMESPACE_FLAG_USABLE;
+ return 0;
+}
+
+static bool mailbox_try_undelete(struct mailbox *box)
+{
+ time_t mtime;
+
+ i_assert(!box->mailbox_undeleting);
+
+ if ((box->flags & MAILBOX_FLAG_READONLY) != 0) {
+ /* most importantly we don't do this because we want to avoid
+ a loop: mdbox storage rebuild -> mailbox_open() ->
+ mailbox_mark_index_deleted() -> mailbox_sync() ->
+ mdbox storage rebuild. */
+ return FALSE;
+ }
+ if (mail_index_get_modification_time(box->index, &mtime) < 0)
+ return FALSE;
+ if (mtime + MAILBOX_DELETE_RETRY_SECS > time(NULL))
+ return FALSE;
+
+ box->mailbox_undeleting = TRUE;
+ int ret = mailbox_mark_index_deleted(box, FALSE);
+ box->mailbox_undeleting = FALSE;
+ if (ret < 0)
+ return FALSE;
+ box->mailbox_deleted = FALSE;
+ return TRUE;
+}
+
+int mailbox_open(struct mailbox *box)
+{
+ if (mailbox_open_full(box, NULL) < 0) {
+ if (!box->mailbox_deleted || box->mailbox_undeleting)
+ return -1;
+
+ /* mailbox has been marked as deleted. if this deletion
+ started (and crashed) a long time ago, it can be confusing
+ to user that the mailbox can't be opened. so we'll just
+ undelete it and reopen. */
+ if(!mailbox_try_undelete(box))
+ return -1;
+
+ /* make sure we close the mailbox in the middle. some backends
+ may not have fully opened the mailbox while it was being
+ undeleted. */
+ mailbox_close(box);
+ if (mailbox_open_full(box, NULL) < 0)
+ return -1;
+ }
+ return 0;
+}
+
+static int mailbox_alloc_index_pvt(struct mailbox *box)
+{
+ const char *index_dir;
+ int ret;
+
+ if (box->index_pvt != NULL)
+ return 1;
+
+ ret = mailbox_get_path_to(box, MAILBOX_LIST_PATH_TYPE_INDEX_PRIVATE,
+ &index_dir);
+ if (ret <= 0)
+ return ret; /* error / no private indexes */
+
+ if (mailbox_create_missing_dir(box, MAILBOX_LIST_PATH_TYPE_INDEX_PRIVATE) < 0)
+ return -1;
+
+ /* Note that this may cause box->event to live longer than box */
+ box->index_pvt = mail_index_alloc_cache_get(box->event,
+ NULL, index_dir, t_strconcat(box->index_prefix, ".pvt", NULL));
+ mail_index_set_fsync_mode(box->index_pvt,
+ box->storage->set->parsed_fsync_mode, 0);
+ mail_index_set_lock_method(box->index_pvt,
+ box->storage->set->parsed_lock_method,
+ mail_storage_get_lock_timeout(box->storage, UINT_MAX));
+ return 1;
+}
+
+int mailbox_open_index_pvt(struct mailbox *box)
+{
+ enum mail_index_open_flags index_flags;
+ int ret;
+
+ if (box->view_pvt != NULL)
+ return 1;
+ if (mailbox_get_private_flags_mask(box) == 0)
+ return 0;
+
+ if ((ret = mailbox_alloc_index_pvt(box)) <= 0)
+ return ret;
+ index_flags = MAIL_INDEX_OPEN_FLAG_CREATE |
+ mail_storage_settings_to_index_flags(box->storage->set);
+ if ((box->flags & MAILBOX_FLAG_SAVEONLY) != 0)
+ index_flags |= MAIL_INDEX_OPEN_FLAG_SAVEONLY;
+ if (mail_index_open(box->index_pvt, index_flags) < 0)
+ return -1;
+ box->view_pvt = mail_index_view_open(box->index_pvt);
+ return 1;
+}
+
+int mailbox_open_stream(struct mailbox *box, struct istream *input)
+{
+ return mailbox_open_full(box, input);
+}
+
+int mailbox_enable(struct mailbox *box, enum mailbox_feature features)
+{
+ if (mailbox_verify_name(box) < 0)
+ return -1;
+ return box->v.enable(box, features);
+}
+
+enum mailbox_feature mailbox_get_enabled_features(struct mailbox *box)
+{
+ return box->enabled_features;
+}
+
+void mail_storage_free_binary_cache(struct mail_storage *storage)
+{
+ if (storage->binary_cache.box == NULL)
+ return;
+
+ timeout_remove(&storage->binary_cache.to);
+ i_stream_destroy(&storage->binary_cache.input);
+ i_zero(&storage->binary_cache);
+}
+
+void mailbox_close(struct mailbox *box)
+{
+ if (!box->opened)
+ return;
+
+ if (box->transaction_count != 0) {
+ i_panic("Trying to close mailbox %s with open transactions",
+ box->name);
+ }
+ box->v.close(box);
+
+ if (box->storage->binary_cache.box == box)
+ mail_storage_free_binary_cache(box->storage);
+ box->opened = FALSE;
+ box->mailbox_deleted = FALSE;
+ array_clear(&box->search_results);
+
+ if (array_is_created(&box->recent_flags))
+ array_free(&box->recent_flags);
+ box->recent_flags_prev_uid = 0;
+ box->recent_flags_count = 0;
+}
+
+void mailbox_free(struct mailbox **_box)
+{
+ struct mailbox *box = *_box;
+
+ *_box = NULL;
+
+ mailbox_close(box);
+ box->v.free(box);
+
+ if (box->attribute_iter_count != 0) {
+ i_panic("Trying to free mailbox %s with %u open attribute iterators",
+ box->name, box->attribute_iter_count);
+ }
+
+ DLLIST_REMOVE(&box->storage->mailboxes, box);
+ mail_storage_obj_unref(box->storage);
+ pool_unref(&box->pool);
+}
+
+bool mailbox_equals(const struct mailbox *box1,
+ const struct mail_namespace *ns2, const char *vname2)
+{
+ struct mail_namespace *ns1 = mailbox_get_namespace(box1);
+ const char *name1;
+
+ if (ns1 != ns2)
+ return FALSE;
+
+ name1 = mailbox_get_vname(box1);
+ if (strcmp(name1, vname2) == 0)
+ return TRUE;
+
+ return strcasecmp(name1, "INBOX") == 0 &&
+ strcasecmp(vname2, "INBOX") == 0;
+}
+
+bool mailbox_is_any_inbox(struct mailbox *box)
+{
+ return box->inbox_any;
+}
+
+bool mailbox_has_special_use(struct mailbox *box, const char *special_use)
+{
+ if (box->set == NULL)
+ return FALSE;
+ return str_contains_special_use(box->set->special_use, special_use);
+}
+
+static void mailbox_copy_cache_decisions_from_inbox(struct mailbox *box)
+{
+ struct mail_namespace *ns =
+ mail_namespace_find_inbox(box->storage->user->namespaces);
+ struct mailbox *inbox =
+ mailbox_alloc(ns->list, "INBOX", MAILBOX_FLAG_READONLY);
+ enum mailbox_existence existence;
+
+ /* this should be NoSelect but since inbox can never be
+ NoSelect we use EXISTENCE_NONE to avoid creating inbox by accident */
+ if (mailbox_exists(inbox, FALSE, &existence) == 0 &&
+ existence != MAILBOX_EXISTENCE_NONE &&
+ mailbox_open(inbox) == 0 &&
+ mailbox_open(box) == 0) {
+ /* we can't do much about errors here */
+ (void)mail_cache_decisions_copy(inbox->cache, box->cache);
+ }
+
+ mailbox_free(&inbox);
+}
+
+int mailbox_create(struct mailbox *box, const struct mailbox_update *update,
+ bool directory)
+{
+ int ret;
+
+ if (mailbox_verify_create_name(box) < 0)
+ return -1;
+
+ struct event_reason *reason = event_reason_begin("mailbox:create");
+
+ /* Avoid race conditions by keeping mailbox list locked during changes.
+ This especially fixes a race during INBOX creation with LAYOUT=index
+ because it scans for missing mailboxes if INBOX doesn't exist. The
+ second process's scan can find a half-created INBOX and add it,
+ causing the first process to become confused. */
+ if (mailbox_list_lock(box->list) < 0) {
+ mail_storage_copy_list_error(box->storage, box->list);
+ event_reason_end(&reason);
+ return -1;
+ }
+ box->creating = TRUE;
+ T_BEGIN {
+ ret = box->v.create_box(box, update, directory);
+ } T_END;
+ box->creating = FALSE;
+ mailbox_list_unlock(box->list);
+
+ if (ret == 0) {
+ box->list->guid_cache_updated = TRUE;
+ if (!box->inbox_any) T_BEGIN {
+ mailbox_copy_cache_decisions_from_inbox(box);
+ } T_END;
+ } else if (box->opened) {
+ /* Creation failed after (partially) opening the mailbox.
+ It may not be in a valid state, so close it. */
+ mail_storage_last_error_push(box->storage);
+ mailbox_close(box);
+ mail_storage_last_error_pop(box->storage);
+ }
+ event_reason_end(&reason);
+ return ret;
+}
+
+int mailbox_update(struct mailbox *box, const struct mailbox_update *update)
+{
+ int ret;
+
+ i_assert(update->min_next_uid == 0 ||
+ update->min_first_recent_uid == 0 ||
+ update->min_first_recent_uid <= update->min_next_uid);
+
+ if (mailbox_verify_existing_name(box) < 0)
+ return -1;
+
+ struct event_reason *reason = event_reason_begin("mailbox:update");
+ ret = box->v.update_box(box, update);
+ if (!guid_128_is_empty(update->mailbox_guid))
+ box->list->guid_cache_invalidated = TRUE;
+ event_reason_end(&reason);
+ return ret;
+}
+
+int mailbox_mark_index_deleted(struct mailbox *box, bool del)
+{
+ struct mail_index_transaction *trans;
+ enum mail_index_transaction_flags trans_flags = 0;
+ enum mailbox_flags old_flag;
+ int ret;
+
+ e_debug(box->event, "Attempting to %s mailbox", del ?
+ "delete" : "undelete");
+
+ if (box->marked_deleted && del) {
+ /* we already marked it deleted. this allows plugins to
+ "lock" the deletion earlier. */
+ return 0;
+ }
+
+ old_flag = box->flags & MAILBOX_FLAG_OPEN_DELETED;
+ box->flags |= MAILBOX_FLAG_OPEN_DELETED;
+ ret = mailbox_open(box);
+ box->flags = (box->flags & ENUM_NEGATE(MAILBOX_FLAG_OPEN_DELETED)) | old_flag;
+ if (ret < 0)
+ return -1;
+
+ trans_flags = del ? 0 : MAIL_INDEX_TRANSACTION_FLAG_EXTERNAL;
+ trans = mail_index_transaction_begin(box->view, trans_flags);
+ if (del)
+ mail_index_set_deleted(trans);
+ else
+ mail_index_set_undeleted(trans);
+ if (mail_index_transaction_commit(&trans) < 0) {
+ mailbox_set_index_error(box);
+ return -1;
+ }
+
+ if (del) {
+ /* sync the mailbox. this finishes the index deletion and it
+ can succeed only for a single session. we do it here, so the
+ rest of the deletion code doesn't have to worry about race
+ conditions. */
+ box->delete_sync_check = TRUE;
+ ret = mailbox_sync(box, MAILBOX_SYNC_FLAG_FULL_READ);
+ box->delete_sync_check = FALSE;
+ if (ret < 0)
+ return -1;
+ }
+
+ box->marked_deleted = del;
+ return 0;
+}
+
+static void mailbox_close_reset_path(struct mailbox *box)
+{
+ i_zero(&box->_perm);
+ box->_path = NULL;
+ box->_index_path = NULL;
+}
+
+static int mailbox_delete_real(struct mailbox *box)
+{
+ bool list_locked;
+ int ret;
+
+ if (*box->name == '\0') {
+ mail_storage_set_error(box->storage, MAIL_ERROR_PARAMS,
+ "Storage root can't be deleted");
+ return -1;
+ }
+
+ struct event_reason *reason = event_reason_begin("mailbox:delete");
+
+ box->deleting = TRUE;
+ if (mailbox_open(box) < 0) {
+ if (mailbox_get_last_mail_error(box) != MAIL_ERROR_NOTFOUND &&
+ !box->mailbox_deleted) {
+ event_reason_end(&reason);
+ return -1;
+ }
+ /* might be a \noselect mailbox, so continue deletion */
+ }
+
+ if (mailbox_list_lock(box->list) < 0) {
+ mail_storage_copy_list_error(box->storage, box->list);
+ list_locked = FALSE;
+ ret = -1;
+ } else {
+ list_locked = TRUE;
+ ret = box->v.delete_box(box);
+ }
+ if (ret < 0 && box->marked_deleted) {
+ /* deletion failed. revert the mark so it can maybe be
+ tried again later. */
+ if (mailbox_mark_index_deleted(box, FALSE) < 0)
+ ret = -1;
+ }
+ if (list_locked)
+ mailbox_list_unlock(box->list);
+
+ box->deleting = FALSE;
+ mailbox_close(box);
+
+ /* if mailbox is reopened, its path may be different with
+ LAYOUT=index */
+ mailbox_close_reset_path(box);
+ event_reason_end(&reason);
+ return ret;
+}
+
+int mailbox_delete(struct mailbox *box)
+{
+ int ret;
+ T_BEGIN {
+ ret = mailbox_delete_real(box);
+ } T_END;
+ return ret;
+}
+
+int mailbox_delete_empty(struct mailbox *box)
+{
+ int ret;
+
+ /* FIXME: should be a parameter to delete(), but since it changes API
+ don't do it for now */
+ box->deleting_must_be_empty = TRUE;
+ ret = mailbox_delete(box);
+ box->deleting_must_be_empty = FALSE;
+ return ret;
+}
+
+static bool
+mail_storages_rename_compatible(struct mail_storage *storage1,
+ struct mail_storage *storage2,
+ const char **error_r)
+{
+ if (storage1 == storage2)
+ return TRUE;
+
+ if (strcmp(storage1->name, storage2->name) != 0) {
+ *error_r = t_strdup_printf("storage %s != %s",
+ storage1->name, storage2->name);
+ return FALSE;
+ }
+ if ((storage1->class_flags & MAIL_STORAGE_CLASS_FLAG_UNIQUE_ROOT) != 0) {
+ /* e.g. mdbox where all mails are in storage/ directory and
+ they can't be easily moved from there. */
+ *error_r = t_strdup_printf("storage %s uses unique root",
+ storage1->name);
+ return FALSE;
+ }
+ return TRUE;
+}
+
+static bool nullequals(const void *p1, const void *p2)
+{
+ return (p1 == NULL && p2 == NULL) || (p1 != NULL && p2 != NULL);
+}
+
+static bool
+mailbox_lists_rename_compatible(struct mailbox_list *list1,
+ struct mailbox_list *list2,
+ const char **error_r)
+{
+ if (!nullequals(list1->set.alt_dir, list2->set.alt_dir)) {
+ *error_r = t_strdup_printf("Namespace %s has alt dir, %s doesn't",
+ list1->ns->prefix, list2->ns->prefix);
+ return FALSE;
+ }
+ if (!nullequals(list1->set.index_dir, list2->set.index_dir)) {
+ *error_r = t_strdup_printf("Namespace %s has index dir, %s doesn't",
+ list1->ns->prefix, list2->ns->prefix);
+ return FALSE;
+ }
+ if (!nullequals(list1->set.index_cache_dir, list2->set.index_cache_dir)) {
+ *error_r = t_strdup_printf("Namespace %s has index cache dir, %s doesn't",
+ list1->ns->prefix, list2->ns->prefix);
+ return FALSE;
+ }
+ if (!nullequals(list1->set.control_dir, list2->set.control_dir)) {
+ *error_r = t_strdup_printf("Namespace %s has control dir, %s doesn't",
+ list1->ns->prefix, list2->ns->prefix);
+ return FALSE;
+ }
+ return TRUE;
+}
+
+static
+int mailbox_rename_check_children(struct mailbox *src, struct mailbox *dest)
+{
+ int ret = 0;
+ size_t src_prefix_len = strlen(src->vname)+1; /* include separator */
+ size_t dest_prefix_len = strlen(dest->vname)+1;
+ /* this can return folders with * in their name, that are not
+ actually our children */
+ char ns_sep = mail_namespace_get_sep(src->list->ns);
+ const char *pattern = t_strdup_printf("%s%c*", src->vname, ns_sep);
+
+ struct mailbox_list_iterate_context *iter = mailbox_list_iter_init(src->list, pattern,
+ MAILBOX_LIST_ITER_RETURN_NO_FLAGS);
+
+ const struct mailbox_info *child;
+ while((child = mailbox_list_iter_next(iter)) != NULL) {
+ if (strncmp(child->vname, src->vname, src_prefix_len-1) != 0 ||
+ child->vname[src_prefix_len-1] != ns_sep)
+ continue; /* not our child */
+ /* if total length of new name exceeds the limit, fail */
+ if (strlen(child->vname + src_prefix_len)+dest_prefix_len > MAILBOX_LIST_NAME_MAX_LENGTH) {
+ mail_storage_set_error(src->storage, MAIL_ERROR_PARAMS,
+ "Mailbox or child name too long");
+ ret = -1;
+ break;
+ }
+ }
+
+ /* something went bad */
+ if (mailbox_list_iter_deinit(&iter) < 0) {
+ mail_storage_copy_list_error(src->storage, src->list);
+ ret = -1;
+ }
+ return ret;
+}
+
+static int mailbox_rename_real(struct mailbox *src, struct mailbox *dest)
+{
+ const char *error = NULL;
+
+ /* Check only name validity, \Noselect don't necessarily exist. */
+ if (mailbox_verify_name(src) < 0)
+ return -1;
+ if (*src->name == '\0') {
+ mail_storage_set_error(src->storage, MAIL_ERROR_PARAMS,
+ "Can't rename mailbox root");
+ return -1;
+ }
+ if (mailbox_verify_create_name(dest) < 0) {
+ mail_storage_copy_error(src->storage, dest->storage);
+ return -1;
+ }
+ if (mailbox_rename_check_children(src, dest) != 0) {
+ return -1;
+ }
+
+ if (!mail_storages_rename_compatible(src->storage,
+ dest->storage, &error) ||
+ !mailbox_lists_rename_compatible(src->list,
+ dest->list, &error)) {
+ e_debug(src->event,
+ "Can't rename '%s' to '%s': %s",
+ src->vname, dest->vname, error);
+ mail_storage_set_error(src->storage, MAIL_ERROR_NOTPOSSIBLE,
+ "Can't rename mailboxes across specified storages.");
+ return -1;
+ }
+ if (src->list != dest->list &&
+ (src->list->ns->type != MAIL_NAMESPACE_TYPE_PRIVATE ||
+ dest->list->ns->type != MAIL_NAMESPACE_TYPE_PRIVATE)) {
+ mail_storage_set_error(src->storage, MAIL_ERROR_NOTPOSSIBLE,
+ "Renaming not supported across non-private namespaces.");
+ return -1;
+ }
+ if (src->list == dest->list && strcmp(src->name, dest->name) == 0) {
+ mail_storage_set_error(src->storage, MAIL_ERROR_EXISTS,
+ "Can't rename mailbox to itself.");
+ return -1;
+ }
+
+ /* It would be safer to lock both source and destination, but that
+ could lead to deadlocks. So at least for now lets just lock only the
+ destination list. */
+ if (mailbox_list_lock(dest->list) < 0) {
+ mail_storage_copy_list_error(src->storage, dest->list);
+ return -1;
+ }
+ int ret = src->v.rename_box(src, dest);
+ mailbox_list_unlock(dest->list);
+ if (ret < 0)
+ return -1;
+ src->list->guid_cache_invalidated = TRUE;
+ dest->list->guid_cache_invalidated = TRUE;
+ return 0;
+}
+
+int mailbox_rename(struct mailbox *src, struct mailbox *dest)
+{
+ int ret;
+ T_BEGIN {
+ struct event_reason *reason =
+ event_reason_begin("mailbox:rename");
+ ret = mailbox_rename_real(src, dest);
+ event_reason_end(&reason);
+ } T_END;
+ return ret;
+}
+
+int mailbox_set_subscribed(struct mailbox *box, bool set)
+{
+ int ret;
+
+ if (mailbox_verify_name(box) < 0)
+ return -1;
+
+ struct event_reason *reason =
+ event_reason_begin(set ? "mailbox:subscribe" :
+ "mailbox:unsubscribe");
+ if (mailbox_list_iter_subscriptions_refresh(box->list) < 0) {
+ mail_storage_copy_list_error(box->storage, box->list);
+ ret = -1;
+ } else if (mailbox_is_subscribed(box) == set)
+ ret = 0;
+ else
+ ret = box->v.set_subscribed(box, set);
+ event_reason_end(&reason);
+ return ret;
+}
+
+bool mailbox_is_subscribed(struct mailbox *box)
+{
+ struct mailbox_node *node;
+
+ i_assert(box->list->subscriptions != NULL);
+
+ node = mailbox_tree_lookup(box->list->subscriptions, box->vname);
+ return node != NULL && (node->flags & MAILBOX_SUBSCRIBED) != 0;
+}
+
+struct mail_storage *mailbox_get_storage(const struct mailbox *box)
+{
+ return box->storage;
+}
+
+struct mail_namespace *
+mailbox_get_namespace(const struct mailbox *box)
+{
+ return box->list->ns;
+}
+
+const struct mail_storage_settings *mailbox_get_settings(struct mailbox *box)
+{
+ return box->storage->set;
+}
+
+const char *mailbox_get_name(const struct mailbox *box)
+{
+ return box->name;
+}
+
+const char *mailbox_get_vname(const struct mailbox *box)
+{
+ return box->vname;
+}
+
+bool mailbox_is_readonly(struct mailbox *box)
+{
+ i_assert(box->opened);
+
+ return box->v.is_readonly(box);
+}
+
+bool mailbox_backends_equal(const struct mailbox *box1,
+ const struct mailbox *box2)
+{
+ struct mail_namespace *ns1 = box1->list->ns, *ns2 = box2->list->ns;
+
+ if (strcmp(box1->name, box2->name) != 0)
+ return FALSE;
+
+ while (ns1->alias_for != NULL)
+ ns1 = ns1->alias_for;
+ while (ns2->alias_for != NULL)
+ ns2 = ns2->alias_for;
+ return ns1 == ns2;
+}
+
+static void
+mailbox_get_status_set_defaults(struct mailbox *box,
+ struct mailbox_status *status_r)
+{
+ i_zero(status_r);
+ if ((box->storage->class_flags & MAIL_STORAGE_CLASS_FLAG_HAVE_MAIL_GUIDS) != 0)
+ status_r->have_guids = TRUE;
+ if ((box->storage->class_flags & MAIL_STORAGE_CLASS_FLAG_HAVE_MAIL_SAVE_GUIDS) != 0)
+ status_r->have_save_guids = TRUE;
+ if ((box->storage->class_flags & MAIL_STORAGE_CLASS_FLAG_HAVE_MAIL_GUID128) != 0)
+ status_r->have_only_guid128 = TRUE;
+}
+
+int mailbox_get_status(struct mailbox *box,
+ enum mailbox_status_items items,
+ struct mailbox_status *status_r)
+{
+ mailbox_get_status_set_defaults(box, status_r);
+ if (mailbox_verify_existing_name(box) < 0)
+ return -1;
+
+ if (box->v.get_status(box, items, status_r) < 0)
+ return -1;
+ i_assert(status_r->have_guids || !status_r->have_save_guids);
+ return 0;
+}
+
+void mailbox_get_open_status(struct mailbox *box,
+ enum mailbox_status_items items,
+ struct mailbox_status *status_r)
+{
+ i_assert(box->opened);
+ i_assert((items & MAILBOX_STATUS_FAILING_ITEMS) == 0);
+
+ mailbox_get_status_set_defaults(box, status_r);
+ if (box->v.get_status(box, items, status_r) < 0)
+ i_unreached();
+}
+
+int mailbox_get_metadata(struct mailbox *box, enum mailbox_metadata_items items,
+ struct mailbox_metadata *metadata_r)
+{
+ i_zero(metadata_r);
+ if (mailbox_verify_existing_name(box) < 0)
+ return -1;
+
+ if (box->v.get_metadata(box, items, metadata_r) < 0)
+ return -1;
+
+ i_assert((items & MAILBOX_METADATA_GUID) == 0 ||
+ !guid_128_is_empty(metadata_r->guid));
+ return 0;
+}
+
+enum mail_flags mailbox_get_private_flags_mask(struct mailbox *box)
+{
+ if (box->v.get_private_flags_mask != NULL)
+ return box->v.get_private_flags_mask(box);
+ else if (box->list->set.index_pvt_dir != NULL)
+ return MAIL_SEEN; /* FIXME */
+ else
+ return 0;
+}
+
+struct mailbox_sync_context *
+mailbox_sync_init(struct mailbox *box, enum mailbox_sync_flags flags)
+{
+ struct mailbox_sync_context *ctx;
+
+ if (box->transaction_count != 0) {
+ i_panic("Trying to sync mailbox %s with open transactions",
+ box->name);
+ }
+ if (!box->opened) {
+ if (mailbox_open(box) < 0) {
+ ctx = i_new(struct mailbox_sync_context, 1);
+ ctx->box = box;
+ ctx->flags = flags;
+ ctx->open_failed = TRUE;
+ return ctx;
+ }
+ }
+ T_BEGIN {
+ ctx = box->v.sync_init(box, flags);
+ } T_END;
+ return ctx;
+}
+
+bool mailbox_sync_next(struct mailbox_sync_context *ctx,
+ struct mailbox_sync_rec *sync_rec_r)
+{
+ if (ctx->open_failed)
+ return FALSE;
+ return ctx->box->v.sync_next(ctx, sync_rec_r);
+}
+
+int mailbox_sync_deinit(struct mailbox_sync_context **_ctx,
+ struct mailbox_sync_status *status_r)
+{
+ struct mailbox_sync_context *ctx = *_ctx;
+ struct mailbox *box = ctx->box;
+ const char *errormsg;
+ enum mail_error error;
+ int ret;
+
+ *_ctx = NULL;
+
+ i_zero(status_r);
+
+ if (!ctx->open_failed)
+ ret = box->v.sync_deinit(ctx, status_r);
+ else {
+ i_free(ctx);
+ ret = -1;
+ }
+ if (ret < 0 && box->inbox_user &&
+ !box->storage->user->inbox_open_error_logged) {
+ errormsg = mailbox_get_last_internal_error(box, &error);
+ if (error == MAIL_ERROR_NOTPOSSIBLE) {
+ box->storage->user->inbox_open_error_logged = TRUE;
+ e_error(box->event, "Syncing INBOX failed: %s", errormsg);
+ }
+ }
+ if (ret == 0)
+ box->synced = TRUE;
+ return ret;
+}
+
+int mailbox_sync(struct mailbox *box, enum mailbox_sync_flags flags)
+{
+ struct mailbox_sync_context *ctx;
+ struct mailbox_sync_status status;
+
+ if (array_count(&box->search_results) == 0) {
+ /* we don't care about mailbox's current state, so we might
+ as well fix inconsistency state */
+ flags |= MAILBOX_SYNC_FLAG_FIX_INCONSISTENT;
+ }
+
+ ctx = mailbox_sync_init(box, flags);
+ return mailbox_sync_deinit(&ctx, &status);
+}
+
+#undef mailbox_notify_changes
+void mailbox_notify_changes(struct mailbox *box,
+ mailbox_notify_callback_t *callback, void *context)
+{
+ i_assert(box->opened);
+
+ box->notify_callback = callback;
+ box->notify_context = context;
+
+ box->v.notify_changes(box);
+}
+
+void mailbox_notify_changes_stop(struct mailbox *box)
+{
+ i_assert(box->opened);
+
+ box->notify_callback = NULL;
+ box->notify_context = NULL;
+
+ box->v.notify_changes(box);
+}
+
+struct mail_search_context *
+mailbox_search_init(struct mailbox_transaction_context *t,
+ struct mail_search_args *args,
+ const enum mail_sort_type *sort_program,
+ enum mail_fetch_field wanted_fields,
+ struct mailbox_header_lookup_ctx *wanted_headers)
+{
+ i_assert(wanted_headers == NULL || wanted_headers->box == t->box);
+
+ mail_search_args_ref(args);
+ if (!args->simplified)
+ mail_search_args_simplify(args);
+ return t->box->v.search_init(t, args, sort_program,
+ wanted_fields, wanted_headers);
+}
+
+int mailbox_search_deinit(struct mail_search_context **_ctx)
+{
+ struct mail_search_context *ctx = *_ctx;
+ struct mail_search_args *args = ctx->args;
+ int ret;
+
+ *_ctx = NULL;
+ mailbox_search_results_initial_done(ctx);
+ ret = ctx->transaction->box->v.search_deinit(ctx);
+ mail_search_args_unref(&args);
+ return ret;
+}
+
+bool mailbox_search_next(struct mail_search_context *ctx, struct mail **mail_r)
+{
+ bool tryagain;
+
+ while (!mailbox_search_next_nonblock(ctx, mail_r, &tryagain)) {
+ if (!tryagain)
+ return FALSE;
+ }
+ return TRUE;
+}
+
+bool mailbox_search_next_nonblock(struct mail_search_context *ctx,
+ struct mail **mail_r, bool *tryagain_r)
+{
+ struct mailbox *box = ctx->transaction->box;
+
+ *mail_r = NULL;
+ *tryagain_r = FALSE;
+
+ if (!box->v.search_next_nonblock(ctx, mail_r, tryagain_r))
+ return FALSE;
+ else {
+ mailbox_search_results_add(ctx, (*mail_r)->uid);
+ return TRUE;
+ }
+}
+
+bool mailbox_search_seen_lost_data(struct mail_search_context *ctx)
+{
+ return ctx->seen_lost_data;
+}
+
+void mailbox_search_mail_detach(struct mail_search_context *ctx,
+ struct mail *mail)
+{
+ struct mail_private *pmail =
+ container_of(mail, struct mail_private, mail);
+ struct mail *const *mailp;
+
+ array_foreach(&ctx->mails, mailp) {
+ if (*mailp == mail) {
+ pmail->search_mail = FALSE;
+ array_delete(&ctx->mails,
+ array_foreach_idx(&ctx->mails, mailp), 1);
+ return;
+ }
+ }
+ i_unreached();
+}
+
+int mailbox_search_result_build(struct mailbox_transaction_context *t,
+ struct mail_search_args *args,
+ enum mailbox_search_result_flags flags,
+ struct mail_search_result **result_r)
+{
+ struct mail_search_context *ctx;
+ struct mail *mail;
+ int ret;
+
+ ctx = mailbox_search_init(t, args, NULL, 0, NULL);
+ *result_r = mailbox_search_result_save(ctx, flags);
+ while (mailbox_search_next(ctx, &mail)) ;
+
+ ret = mailbox_search_deinit(&ctx);
+ if (ret < 0)
+ mailbox_search_result_free(result_r);
+ return ret;
+}
+
+struct mailbox_transaction_context *
+mailbox_transaction_begin(struct mailbox *box,
+ enum mailbox_transaction_flags flags,
+ const char *reason)
+{
+ struct mailbox_transaction_context *trans;
+
+ i_assert(box->opened);
+
+ box->transaction_count++;
+ trans = box->v.transaction_begin(box, flags, reason);
+ i_assert(trans->reason != NULL);
+ return trans;
+}
+
+int mailbox_transaction_commit(struct mailbox_transaction_context **t)
+{
+ struct mail_transaction_commit_changes changes;
+ int ret;
+
+ /* Store changes temporarily so that plugins overriding
+ transaction_commit() can look at them. */
+ ret = mailbox_transaction_commit_get_changes(t, &changes);
+ pool_unref(&changes.pool);
+ return ret;
+}
+
+int mailbox_transaction_commit_get_changes(
+ struct mailbox_transaction_context **_t,
+ struct mail_transaction_commit_changes *changes_r)
+{
+ struct mailbox_transaction_context *t = *_t;
+ struct mailbox *box = t->box;
+ unsigned int save_count = t->save_count;
+ struct event_reason *reason = NULL;
+ int ret;
+
+ changes_r->pool = NULL;
+
+ *_t = NULL;
+
+ if (t->itrans->attribute_updates != NULL &&
+ t->itrans->attribute_updates->used > 0) {
+ /* attribute changes are also done directly via lib-index
+ by ACL and Sieve */
+ reason = event_reason_begin("mailbox:attributes_changed");
+ }
+ T_BEGIN {
+ ret = box->v.transaction_commit(t, changes_r);
+ } T_END;
+ /* either all the saved messages get UIDs or none, because a) we
+ failed, b) MAILBOX_TRANSACTION_FLAG_ASSIGN_UIDS not set,
+ c) backend doesn't support it (e.g. virtual plugin) */
+ i_assert(ret < 0 ||
+ seq_range_count(&changes_r->saved_uids) == save_count ||
+ array_count(&changes_r->saved_uids) == 0);
+ /* decrease the transaction count only after transaction_commit().
+ that way if it creates and destroys transactions internally, we
+ don't see transaction_count=0 until the parent transaction is fully
+ finished */
+ box->transaction_count--;
+ event_reason_end(&reason);
+ if (ret < 0 && changes_r->pool != NULL)
+ pool_unref(&changes_r->pool);
+ return ret;
+}
+
+void mailbox_transaction_rollback(struct mailbox_transaction_context **_t)
+{
+ struct mailbox_transaction_context *t = *_t;
+ struct mailbox *box = t->box;
+
+ *_t = NULL;
+ box->v.transaction_rollback(t);
+ box->transaction_count--;
+}
+
+unsigned int mailbox_transaction_get_count(const struct mailbox *box)
+{
+ return box->transaction_count;
+}
+
+void mailbox_transaction_set_max_modseq(struct mailbox_transaction_context *t,
+ uint64_t max_modseq,
+ ARRAY_TYPE(seq_range) *seqs)
+{
+ mail_index_transaction_set_max_modseq(t->itrans, max_modseq, seqs);
+}
+
+struct mailbox *
+mailbox_transaction_get_mailbox(const struct mailbox_transaction_context *t)
+{
+ return t->box;
+}
+
+static void mailbox_save_dest_mail_close(struct mail_save_context *ctx)
+{
+ struct mail_private *mail = (struct mail_private *)ctx->dest_mail;
+
+ mail->v.close(&mail->mail);
+}
+
+struct mail_save_context *
+mailbox_save_alloc(struct mailbox_transaction_context *t)
+{
+ struct mail_save_context *ctx;
+ T_BEGIN {
+ ctx = t->box->v.save_alloc(t);
+ } T_END;
+ i_assert(!ctx->unfinished);
+ ctx->unfinished = TRUE;
+ ctx->data.received_date = (time_t)-1;
+ ctx->data.save_date = (time_t)-1;
+
+ /* Always have a dest_mail available. A lot of plugins make use
+ of this. */
+ if (ctx->dest_mail == NULL)
+ ctx->dest_mail = mail_alloc(t, 0, NULL);
+ else {
+ /* make sure the mail isn't used before mail_set_seq_saving() */
+ mailbox_save_dest_mail_close(ctx);
+ }
+
+ return ctx;
+}
+
+void mailbox_save_context_deinit(struct mail_save_context *ctx)
+{
+ i_assert(ctx->dest_mail != NULL);
+
+ mail_free(&ctx->dest_mail);
+}
+
+void mailbox_save_set_flags(struct mail_save_context *ctx,
+ enum mail_flags flags,
+ struct mail_keywords *keywords)
+{
+ struct mailbox *box = ctx->transaction->box;
+
+ if (ctx->data.keywords != NULL)
+ mailbox_keywords_unref(&ctx->data.keywords);
+
+ ctx->data.flags = flags & ENUM_NEGATE(mailbox_get_private_flags_mask(box));
+ ctx->data.pvt_flags = flags & mailbox_get_private_flags_mask(box);
+ ctx->data.keywords = keywords;
+ if (keywords != NULL)
+ mailbox_keywords_ref(keywords);
+}
+
+void mailbox_save_copy_flags(struct mail_save_context *ctx, struct mail *mail)
+{
+ const char *const *keywords_list;
+ struct mail_keywords *keywords;
+
+ keywords_list = mail_get_keywords(mail);
+ keywords = str_array_length(keywords_list) == 0 ? NULL :
+ mailbox_keywords_create_valid(ctx->transaction->box,
+ keywords_list);
+ mailbox_save_set_flags(ctx, mail_get_flags(mail), keywords);
+ if (keywords != NULL)
+ mailbox_keywords_unref(&keywords);
+}
+
+void mailbox_save_set_min_modseq(struct mail_save_context *ctx,
+ uint64_t min_modseq)
+{
+ ctx->data.min_modseq = min_modseq;
+}
+
+void mailbox_save_set_received_date(struct mail_save_context *ctx,
+ time_t received_date, int timezone_offset)
+{
+ ctx->data.received_date = received_date;
+ ctx->data.received_tz_offset = timezone_offset;
+}
+
+void mailbox_save_set_save_date(struct mail_save_context *ctx,
+ time_t save_date)
+{
+ ctx->data.save_date = save_date;
+}
+
+void mailbox_save_set_from_envelope(struct mail_save_context *ctx,
+ const char *envelope)
+{
+ i_free(ctx->data.from_envelope);
+ ctx->data.from_envelope = i_strdup(envelope);
+}
+
+void mailbox_save_set_uid(struct mail_save_context *ctx, uint32_t uid)
+{
+ ctx->data.uid = uid;
+}
+
+void mailbox_save_set_guid(struct mail_save_context *ctx, const char *guid)
+{
+ i_assert(guid == NULL || *guid != '\0');
+
+ i_free(ctx->data.guid);
+ ctx->data.guid = i_strdup(guid);
+}
+
+void mailbox_save_set_pop3_uidl(struct mail_save_context *ctx, const char *uidl)
+{
+ i_assert(*uidl != '\0');
+ i_assert(strchr(uidl, '\n') == NULL);
+
+ i_free(ctx->data.pop3_uidl);
+ ctx->data.pop3_uidl = i_strdup(uidl);
+}
+
+void mailbox_save_set_pop3_order(struct mail_save_context *ctx,
+ unsigned int order)
+{
+ i_assert(order > 0);
+
+ ctx->data.pop3_order = order;
+}
+
+struct mail *mailbox_save_get_dest_mail(struct mail_save_context *ctx)
+{
+ return ctx->dest_mail;
+}
+
+int mailbox_save_begin(struct mail_save_context **ctx, struct istream *input)
+{
+ struct mailbox *box = (*ctx)->transaction->box;
+ int ret;
+
+ if (mail_index_is_deleted(box->index)) {
+ mailbox_set_deleted(box);
+ mailbox_save_cancel(ctx);
+ return -1;
+ }
+
+ /* make sure parts get parsed early on */
+ const struct mail_storage_settings *mail_set =
+ mailbox_get_settings(box);
+ if (mail_set->parsed_mail_attachment_detection_add_flags)
+ mail_add_temp_wanted_fields((*ctx)->dest_mail,
+ MAIL_FETCH_MESSAGE_PARTS, NULL);
+
+ if (!(*ctx)->copying_or_moving) {
+ /* We're actually saving the mail. We're not being called by
+ mail_storage_copy() because backend didn't support fast
+ copying. */
+ i_assert(!(*ctx)->copying_via_save);
+ (*ctx)->saving = TRUE;
+ } else {
+ i_assert((*ctx)->copying_via_save);
+ }
+ if (box->v.save_begin == NULL) {
+ mail_storage_set_error(box->storage, MAIL_ERROR_NOTPOSSIBLE,
+ "Saving messages not supported");
+ ret = -1;
+ } else T_BEGIN {
+ ret = box->v.save_begin(*ctx, input);
+ } T_END;
+
+ if (ret < 0) {
+ mailbox_save_cancel(ctx);
+ return -1;
+ }
+ return 0;
+}
+
+int mailbox_save_continue(struct mail_save_context *ctx)
+{
+ int ret;
+
+ T_BEGIN {
+ ret = ctx->transaction->box->v.save_continue(ctx);
+ } T_END;
+ return ret;
+}
+
+static void
+mailbox_save_add_pvt_flags(struct mailbox_transaction_context *t,
+ enum mail_flags pvt_flags)
+{
+ struct mail_save_private_changes *save;
+
+ if (!array_is_created(&t->pvt_saves))
+ i_array_init(&t->pvt_saves, 8);
+ save = array_append_space(&t->pvt_saves);
+ save->mailnum = t->save_count;
+ save->flags = pvt_flags;
+}
+
+static void
+mailbox_save_context_reset(struct mail_save_context *ctx, bool success)
+{
+ i_assert(!ctx->unfinished);
+ if (!ctx->copying_or_moving) {
+ /* we're finishing a save (not copy/move). Note that we could
+ have come here also from mailbox_save_cancel(), in which
+ case ctx->saving may be FALSE. */
+ i_assert(!ctx->copying_via_save);
+ i_assert(ctx->saving || !success);
+ ctx->saving = FALSE;
+ } else {
+ i_assert(ctx->copying_via_save || !success);
+ /* We came from mailbox_copy(). saving==TRUE is possible here
+ if we also came from mailbox_save_using_mail(). Don't set
+ saving=FALSE yet in that case, because copy() is still
+ running. */
+ }
+}
+
+int mailbox_save_finish(struct mail_save_context **_ctx)
+{
+ struct mail_save_context *ctx = *_ctx;
+ struct mailbox_transaction_context *t = ctx->transaction;
+ /* we need to keep a copy of this because save_finish implementations
+ will likely zero the data structure during cleanup */
+ enum mail_flags pvt_flags = ctx->data.pvt_flags;
+ bool copying_via_save = ctx->copying_via_save;
+ int ret;
+
+ /* Do one final continue. The caller may not have done it if the
+ input stream's offset already matched the number of bytes that
+ were wanted to be saved. But due to nested istreams some of the
+ underlying ones may not have seen the EOF yet, and haven't flushed
+ out the pending data. */
+ if (mailbox_save_continue(ctx) < 0) {
+ mailbox_save_cancel(_ctx);
+ return -1;
+ }
+ *_ctx = NULL;
+
+ ctx->finishing = TRUE;
+ T_BEGIN {
+ ret = t->box->v.save_finish(ctx);
+ } T_END;
+ ctx->finishing = FALSE;
+
+ if (ret == 0 && !copying_via_save) {
+ if (pvt_flags != 0)
+ mailbox_save_add_pvt_flags(t, pvt_flags);
+ t->save_count++;
+ }
+
+ mailbox_save_context_reset(ctx, TRUE);
+ return ret;
+}
+
+void mailbox_save_cancel(struct mail_save_context **_ctx)
+{
+ struct mail_save_context *ctx = *_ctx;
+
+ *_ctx = NULL;
+ T_BEGIN {
+ ctx->transaction->box->v.save_cancel(ctx);
+ } T_END;
+
+ /* the dest_mail is no longer valid. if we're still saving
+ more mails, the mail sequence may get reused. make sure
+ the mail gets reset in between */
+ mailbox_save_dest_mail_close(ctx);
+
+ mailbox_save_context_reset(ctx, FALSE);
+}
+
+struct mailbox_transaction_context *
+mailbox_save_get_transaction(struct mail_save_context *ctx)
+{
+ return ctx->transaction;
+}
+
+static int mailbox_copy_int(struct mail_save_context **_ctx, struct mail *mail)
+{
+ struct mail_save_context *ctx = *_ctx;
+ struct mailbox_transaction_context *t = ctx->transaction;
+ enum mail_flags pvt_flags = ctx->data.pvt_flags;
+ struct mail *backend_mail;
+ int ret;
+
+ *_ctx = NULL;
+
+ if (mail_index_is_deleted(t->box->index)) {
+ mailbox_set_deleted(t->box);
+ mailbox_save_cancel(&ctx);
+ return -1;
+ }
+
+ /* bypass virtual storage, so hard linking can be used whenever
+ possible */
+ if (mail_get_backend_mail(mail, &backend_mail) < 0) {
+ mailbox_save_cancel(&ctx);
+ return -1;
+ }
+
+ i_assert(!ctx->copying_or_moving);
+ i_assert(ctx->copy_src_mail == NULL);
+ ctx->copying_or_moving = TRUE;
+ ctx->copy_src_mail = mail;
+ ctx->finishing = TRUE;
+ T_BEGIN {
+ ret = t->box->v.copy(ctx, backend_mail);
+ } T_END;
+ ctx->finishing = FALSE;
+ if (ret == 0) {
+ if (pvt_flags != 0)
+ mailbox_save_add_pvt_flags(t, pvt_flags);
+ t->save_count++;
+ }
+ i_assert(!ctx->unfinished);
+
+ ctx->copy_src_mail = NULL;
+ ctx->copying_via_save = FALSE;
+ ctx->copying_or_moving = FALSE;
+ ctx->saving = FALSE; /* if we came from mailbox_save_using_mail() */
+ return ret;
+}
+
+int mailbox_copy(struct mail_save_context **_ctx, struct mail *mail)
+{
+ struct mail_save_context *ctx = *_ctx;
+
+ i_assert(!ctx->saving);
+ i_assert(!ctx->moving);
+
+ int ret;
+ T_BEGIN {
+ ret = mailbox_copy_int(_ctx, mail);
+ } T_END;
+
+ return ret;
+}
+
+int mailbox_move(struct mail_save_context **_ctx, struct mail *mail)
+{
+ struct mail_save_context *ctx = *_ctx;
+ int ret;
+
+ i_assert(!ctx->saving);
+ i_assert(!ctx->moving);
+
+ ctx->moving = TRUE;
+ T_BEGIN {
+ if ((ret = mailbox_copy_int(_ctx, mail)) == 0)
+ mail_expunge(mail);
+ } T_END;
+ ctx->moving = FALSE;
+ return ret;
+}
+
+int mailbox_save_using_mail(struct mail_save_context **_ctx, struct mail *mail)
+{
+ struct mail_save_context *ctx = *_ctx;
+
+ i_assert(!ctx->saving);
+ i_assert(!ctx->moving);
+
+ ctx->saving = TRUE;
+ return mailbox_copy_int(_ctx, mail);
+}
+
+bool mailbox_is_inconsistent(struct mailbox *box)
+{
+ return box->mailbox_deleted || box->v.is_inconsistent(box);
+}
+
+void mailbox_set_deleted(struct mailbox *box)
+{
+ mail_storage_set_error(box->storage, MAIL_ERROR_NOTFOUND,
+ "Mailbox was deleted under us");
+ box->mailbox_deleted = TRUE;
+}
+
+static int get_path_to(struct mailbox *box, enum mailbox_list_path_type type,
+ const char **internal_path, const char **path_r)
+{
+ int ret;
+
+ if (internal_path != NULL && *internal_path != NULL) {
+ if ((*internal_path)[0] == '\0') {
+ *path_r = NULL;
+ return 0;
+ }
+ *path_r = *internal_path;
+ return 1;
+ }
+ ret = mailbox_list_get_path(box->list, box->name, type, path_r);
+ if (ret < 0) {
+ mail_storage_copy_list_error(box->storage, box->list);
+ return -1;
+ }
+ if (internal_path != NULL && *internal_path == NULL)
+ *internal_path = ret == 0 ? "" : p_strdup(box->pool, *path_r);
+ return ret;
+}
+
+int mailbox_get_path_to(struct mailbox *box, enum mailbox_list_path_type type,
+ const char **path_r)
+{
+ if (type == MAILBOX_LIST_PATH_TYPE_MAILBOX)
+ return get_path_to(box, type, &box->_path, path_r);
+ if (type == MAILBOX_LIST_PATH_TYPE_INDEX)
+ return get_path_to(box, type, &box->_index_path, path_r);
+ return get_path_to(box, type, NULL, path_r);
+}
+
+const char *mailbox_get_path(struct mailbox *box)
+{
+ i_assert(box->_path != NULL);
+ i_assert(box->_path[0] != '\0');
+ return box->_path;
+}
+
+const char *mailbox_get_index_path(struct mailbox *box)
+{
+ i_assert(box->_index_path != NULL);
+ i_assert(box->_index_path[0] != '\0');
+ return box->_index_path;
+}
+
+static void mailbox_get_permissions_if_not_set(struct mailbox *box)
+{
+ if (box->_perm.file_create_mode != 0)
+ return;
+
+ if (box->input != NULL) {
+ box->_perm.file_uid = geteuid();
+ box->_perm.file_create_mode = 0600;
+ box->_perm.dir_create_mode = 0700;
+ box->_perm.file_create_gid = (gid_t)-1;
+ box->_perm.file_create_gid_origin = "defaults";
+ return;
+ }
+
+ struct mailbox_permissions perm;
+ mailbox_list_get_permissions(box->list, box->name, &perm);
+ mailbox_permissions_copy(&box->_perm, &perm, box->pool);
+}
+
+const struct mailbox_permissions *mailbox_get_permissions(struct mailbox *box)
+{
+ mailbox_get_permissions_if_not_set(box);
+
+ if (!box->_perm.mail_index_permissions_set && box->index != NULL) {
+ box->_perm.mail_index_permissions_set = TRUE;
+ mail_index_set_permissions(box->index,
+ box->_perm.file_create_mode,
+ box->_perm.file_create_gid,
+ box->_perm.file_create_gid_origin);
+ }
+ return &box->_perm;
+}
+
+void mailbox_refresh_permissions(struct mailbox *box)
+{
+ i_zero(&box->_perm);
+ (void)mailbox_get_permissions(box);
+}
+
+int mailbox_create_fd(struct mailbox *box, const char *path, int flags,
+ int *fd_r)
+{
+ const struct mailbox_permissions *perm = mailbox_get_permissions(box);
+ mode_t old_mask;
+ int fd;
+
+ i_assert((flags & O_CREAT) != 0);
+
+ *fd_r = -1;
+
+ old_mask = umask(0);
+ fd = open(path, flags, perm->file_create_mode);
+ umask(old_mask);
+
+ if (fd != -1) {
+ /* ok */
+ } else if (errno == EEXIST) {
+ /* O_EXCL used, caller will handle this error */
+ return 0;
+ } else if (errno == ENOENT) {
+ mailbox_set_deleted(box);
+ return -1;
+ } else if (errno == ENOTDIR) {
+ mail_storage_set_error(box->storage, MAIL_ERROR_NOTPOSSIBLE,
+ "Mailbox doesn't allow inferior mailboxes");
+ return -1;
+ } else if (mail_storage_set_error_from_errno(box->storage)) {
+ return -1;
+ } else {
+ mailbox_set_critical(box, "open(%s, O_CREAT) failed: %m", path);
+ return -1;
+ }
+
+ if (perm->file_create_gid != (gid_t)-1) {
+ if (fchown(fd, (uid_t)-1, perm->file_create_gid) == 0) {
+ /* ok */
+ } else if (errno == EPERM) {
+ mailbox_set_critical(box, "%s",
+ eperm_error_get_chgrp("fchown", path,
+ perm->file_create_gid,
+ perm->file_create_gid_origin));
+ } else {
+ mailbox_set_critical(box,
+ "fchown(%s) failed: %m", path);
+ }
+ }
+ *fd_r = fd;
+ return 1;
+}
+
+int mailbox_mkdir(struct mailbox *box, const char *path,
+ enum mailbox_list_path_type type)
+{
+ const struct mailbox_permissions *perm = mailbox_get_permissions(box);
+ const char *root_dir;
+
+ if (!perm->gid_origin_is_mailbox_path) {
+ /* mailbox root directory doesn't exist, create it */
+ root_dir = mailbox_list_get_root_forced(box->list, type);
+ if (mailbox_list_mkdir_root(box->list, root_dir, type) < 0) {
+ mail_storage_copy_list_error(box->storage, box->list);
+ return -1;
+ }
+ }
+
+ if (mkdir_parents_chgrp(path, perm->dir_create_mode,
+ perm->file_create_gid,
+ perm->file_create_gid_origin) == 0)
+ return 1;
+ else if (errno == EEXIST)
+ return 0;
+ else if (errno == ENOTDIR) {
+ mail_storage_set_error(box->storage, MAIL_ERROR_NOTPOSSIBLE,
+ "Mailbox doesn't allow inferior mailboxes");
+ return -1;
+ } else if (mail_storage_set_error_from_errno(box->storage)) {
+ return -1;
+ } else {
+ mailbox_set_critical(box, "mkdir_parents(%s) failed: %m", path);
+ return -1;
+ }
+}
+
+int mailbox_create_missing_dir(struct mailbox *box,
+ enum mailbox_list_path_type type)
+{
+ const char *mail_dir, *dir;
+ struct stat st;
+ int ret;
+
+ if ((ret = mailbox_get_path_to(box, type, &dir)) <= 0)
+ return ret;
+ if (mailbox_get_path_to(box, MAILBOX_LIST_PATH_TYPE_MAILBOX,
+ &mail_dir) < 0)
+ return -1;
+ if (null_strcmp(dir, mail_dir) != 0) {
+ /* Mailbox directory is different - create a missing dir */
+ } else if ((box->list->props & MAILBOX_LIST_PROP_AUTOCREATE_DIRS) != 0) {
+ /* This layout (e.g. imapc) wants to autocreate missing mailbox
+ directories as well. */
+ } else {
+ /* If the mailbox directory doesn't exist, the mailbox
+ shouldn't exist at all. So just assume that it's already
+ created and if there's a race condition just fail later. */
+ return 0;
+ }
+
+ /* we call this function even when the directory exists, so first do a
+ quick check to see if we need to mkdir anything */
+ if (stat(dir, &st) == 0)
+ return 0;
+
+ if ((box->storage->class_flags & MAIL_STORAGE_CLASS_FLAG_NO_ROOT) == 0 &&
+ null_strcmp(dir, mail_dir) != 0 && mail_dir != NULL &&
+ stat(mail_dir, &st) < 0 && (errno == ENOENT || errno == ENOTDIR)) {
+ /* Race condition - mail root directory doesn't exist
+ anymore either. We shouldn't create this directory
+ anymore. */
+ mailbox_set_deleted(box);
+ return -1;
+ }
+
+ return mailbox_mkdir(box, dir, type);
+}
+
+unsigned int mail_storage_get_lock_timeout(struct mail_storage *storage,
+ unsigned int secs)
+{
+ return storage->set->mail_max_lock_timeout == 0 ? secs :
+ I_MIN(secs, storage->set->mail_max_lock_timeout);
+}
+
+enum mail_index_open_flags
+mail_storage_settings_to_index_flags(const struct mail_storage_settings *set)
+{
+ enum mail_index_open_flags index_flags = 0;
+
+#ifndef MMAP_CONFLICTS_WRITE
+ if (set->mmap_disable)
+#endif
+ index_flags |= MAIL_INDEX_OPEN_FLAG_MMAP_DISABLE;
+ if (set->dotlock_use_excl)
+ index_flags |= MAIL_INDEX_OPEN_FLAG_DOTLOCK_USE_EXCL;
+ if (set->mail_nfs_index)
+ index_flags |= MAIL_INDEX_OPEN_FLAG_NFS_FLUSH;
+ return index_flags;
+}
+
+int mail_parse_human_timestamp(const char *str, time_t *timestamp_r,
+ bool *utc_r)
+{
+ struct tm tm;
+ unsigned int secs;
+ const char *error;
+
+ if (i_isdigit(str[0]) && i_isdigit(str[1]) &&
+ i_isdigit(str[2]) && i_isdigit(str[3]) && str[4] == '-' &&
+ i_isdigit(str[5]) && i_isdigit(str[6]) && str[7] == '-' &&
+ i_isdigit(str[8]) && i_isdigit(str[9]) && str[10] == '\0') {
+ /* yyyy-mm-dd */
+ i_zero(&tm);
+ tm.tm_year = (str[0]-'0') * 1000 + (str[1]-'0') * 100 +
+ (str[2]-'0') * 10 + (str[3]-'0') - 1900;
+ tm.tm_mon = (str[5]-'0') * 10 + (str[6]-'0') - 1;
+ tm.tm_mday = (str[8]-'0') * 10 + (str[9]-'0');
+ *timestamp_r = mktime(&tm);
+ *utc_r = FALSE;
+ return 0;
+ } else if (imap_parse_date(str, timestamp_r)) {
+ /* imap date */
+ *utc_r = FALSE;
+ return 0;
+ } else if (str_to_time(str, timestamp_r) == 0) {
+ /* unix timestamp */
+ *utc_r = TRUE;
+ return 0;
+ } else if (settings_get_time(str, &secs, &error) == 0) {
+ *timestamp_r = ioloop_time - secs;
+ *utc_r = TRUE;
+ return 0;
+ } else {
+ return -1;
+ }
+}
+
+void mail_set_mail_cache_corrupted(struct mail *mail, const char *fmt, ...)
+{
+ struct mail_cache_view *cache_view =
+ mail->transaction->cache_view;
+
+ i_assert(cache_view != NULL);
+
+ va_list va;
+ va_start(va, fmt);
+
+ T_BEGIN {
+ mail_cache_set_seq_corrupted_reason(cache_view, mail->seq,
+ t_strdup_printf("UID %u: %s",
+ mail->uid,
+ t_strdup_vprintf(fmt, va)));
+ } T_END;
+
+ /* update also the storage's internal error */
+ mailbox_set_index_error(mail->box);
+
+ va_end(va);
+}
+
+static int
+mail_storage_dotlock_create(const char *lock_path,
+ const struct file_create_settings *lock_set,
+ const struct mail_storage_settings *mail_set,
+ struct file_lock **lock_r, const char **error_r)
+{
+ const struct dotlock_settings dotlock_set = {
+ .timeout = lock_set->lock_timeout_secs,
+ .stale_timeout = I_MAX(60*5, lock_set->lock_timeout_secs),
+ .lock_suffix = "",
+
+ .use_excl_lock = mail_set->dotlock_use_excl,
+ .nfs_flush = mail_set->mail_nfs_storage,
+ .use_io_notify = TRUE,
+ };
+ struct dotlock *dotlock;
+ int ret = file_dotlock_create(&dotlock_set, lock_path, 0, &dotlock);
+ if (ret <= 0) {
+ *error_r = t_strdup_printf("file_dotlock_create(%s) failed: %m",
+ lock_path);
+ return ret;
+ }
+ *lock_r = file_lock_from_dotlock(&dotlock);
+ return 1;
+}
+
+int mail_storage_lock_create(const char *lock_path,
+ const struct file_create_settings *lock_set,
+ const struct mail_storage_settings *mail_set,
+ struct file_lock **lock_r, const char **error_r)
+{
+ struct file_create_settings lock_set_new = *lock_set;
+ bool created;
+
+ if (lock_set->lock_settings.lock_method == FILE_LOCK_METHOD_DOTLOCK)
+ return mail_storage_dotlock_create(lock_path, lock_set, mail_set, lock_r, error_r);
+
+ lock_set_new.lock_settings.close_on_free = TRUE;
+ lock_set_new.lock_settings.unlink_on_free = TRUE;
+ if (file_create_locked(lock_path, &lock_set_new, lock_r,
+ &created, error_r) == -1) {
+ *error_r = t_strdup_printf("file_create_locked(%s) failed: %s",
+ lock_path, *error_r);
+ return errno == EAGAIN ? 0 : -1;
+ }
+ return 1;
+}
+
+int mailbox_lock_file_create(struct mailbox *box, const char *lock_fname,
+ unsigned int lock_secs, struct file_lock **lock_r,
+ const char **error_r)
+{
+ const struct mailbox_permissions *perm;
+ struct file_create_settings set;
+ const char *lock_path;
+
+ perm = mailbox_get_permissions(box);
+ i_zero(&set);
+ set.lock_timeout_secs =
+ mail_storage_get_lock_timeout(box->storage, lock_secs);
+ set.lock_settings.lock_method = box->storage->set->parsed_lock_method;
+ set.mode = perm->file_create_mode;
+ set.gid = perm->file_create_gid;
+ set.gid_origin = perm->file_create_gid_origin;
+
+ if (box->list->set.volatile_dir == NULL)
+ lock_path = t_strdup_printf("%s/%s", box->index->dir, lock_fname);
+ else {
+ unsigned char box_name_sha1[SHA1_RESULTLEN];
+ string_t *str = t_str_new(128);
+
+ /* Keep this simple: Use the lock_fname with a SHA1 of the
+ mailbox name as the suffix. The mailbox name itself could
+ be too large as a filename and creating the full directory
+ structure would be pretty troublesome. It would also make
+ it more difficult to perform the automated deletion of empty
+ lock directories. */
+ str_printfa(str, "%s/%s.", box->list->set.volatile_dir,
+ lock_fname);
+ sha1_get_digest(box->name, strlen(box->name), box_name_sha1);
+ binary_to_hex_append(str, box_name_sha1, sizeof(box_name_sha1));
+ lock_path = str_c(str);
+ set.mkdir_mode = 0700;
+ }
+
+ return mail_storage_lock_create(lock_path, &set,
+ box->storage->set, lock_r, error_r);
+}
+
+void mailbox_sync_notify(struct mailbox *box, uint32_t uid,
+ enum mailbox_sync_type sync_type)
+{
+ if (box->v.sync_notify != NULL)
+ box->v.sync_notify(box, uid, sync_type);
+
+ /* Send an event for expunged mail. */
+ if (sync_type == MAILBOX_SYNC_TYPE_EXPUNGE) {
+ e_debug(event_create_passthrough(box->event)->
+ set_name("mail_expunged")->
+ add_int("uid", uid)->event(),
+ "UID %u: Mail expunged", uid);
+ }
+}
diff --git a/src/lib-storage/mail-storage.h b/src/lib-storage/mail-storage.h
new file mode 100644
index 0000000..64e0e49
--- /dev/null
+++ b/src/lib-storage/mail-storage.h
@@ -0,0 +1,1027 @@
+#ifndef MAIL_STORAGE_H
+#define MAIL_STORAGE_H
+
+struct message_size;
+
+#include "seq-range-array.h"
+#include "file-lock.h"
+#include "guid.h"
+#include "mail-types.h"
+#include "mail-error.h"
+#include "mail-index.h"
+#include "mail-namespace.h"
+#include "mailbox-list.h"
+#include "mailbox-attribute.h"
+
+/* If some operation is taking long, call notify_ok every n seconds. */
+#define MAIL_STORAGE_STAYALIVE_SECS 15
+
+#define MAIL_KEYWORD_HAS_ATTACHMENT "$HasAttachment"
+#define MAIL_KEYWORD_HAS_NO_ATTACHMENT "$HasNoAttachment"
+
+enum mail_storage_flags {
+ /* Remember message headers' MD5 sum */
+ MAIL_STORAGE_FLAG_KEEP_HEADER_MD5 = 0x01,
+ /* Don't try to autodetect anything, require that the given data
+ contains all the necessary information. */
+ MAIL_STORAGE_FLAG_NO_AUTODETECTION = 0x02,
+ /* Don't autocreate any directories. If they don't exist,
+ fail to create the storage. */
+ MAIL_STORAGE_FLAG_NO_AUTOCREATE = 0x04,
+ /* Don't verify existence or accessibility of any directories.
+ Create the storage in any case. */
+ MAIL_STORAGE_FLAG_NO_AUTOVERIFY = 0x08
+};
+
+enum mailbox_flags {
+ /* Mailbox must not be modified even if asked */
+ MAILBOX_FLAG_READONLY = 0x01,
+ /* Only saving/copying mails to mailbox works. */
+ MAILBOX_FLAG_SAVEONLY = 0x02,
+ /* Remove MAIL_RECENT flags when syncing */
+ MAILBOX_FLAG_DROP_RECENT = 0x04,
+ /* Don't create index files for the mailbox */
+ MAILBOX_FLAG_NO_INDEX_FILES = 0x10,
+ /* Keep mailbox exclusively locked all the time while it's open */
+ MAILBOX_FLAG_KEEP_LOCKED = 0x20,
+ /* Enable if mailbox is used for serving POP3. This allows making
+ better caching decisions. */
+ MAILBOX_FLAG_POP3_SESSION = 0x40,
+ /* Enable if mailbox is used for saving a mail delivery using MDA.
+ This causes ACL plugin to use POST right rather than INSERT. */
+ MAILBOX_FLAG_POST_SESSION = 0x80,
+ /* Force opening mailbox and ignoring any ACLs */
+ MAILBOX_FLAG_IGNORE_ACLS = 0x100,
+ /* Open mailbox even if it's already marked as deleted */
+ MAILBOX_FLAG_OPEN_DELETED = 0x200,
+ /* Mailbox is opened for deletion, which should be performed as
+ efficiently as possible, even allowing the mailbox state to become
+ inconsistent. For example this disables lazy_expunge plugin and
+ quota updates (possibly resulting in broken quota). and This is
+ useful for example when deleting entire user accounts. */
+ MAILBOX_FLAG_DELETE_UNSAFE = 0x400,
+ /* Mailbox is created implicitly if it does not exist. */
+ MAILBOX_FLAG_AUTO_CREATE = 0x1000,
+ /* Mailbox is subscribed to implicitly when it is created automatically */
+ MAILBOX_FLAG_AUTO_SUBSCRIBE = 0x2000,
+ /* Run fsck for mailbox index before doing anything else. This may be
+ useful in fixing index corruption errors that aren't otherwise
+ detected and that are causing the full mailbox opening to fail. */
+ MAILBOX_FLAG_FSCK = 0x4000,
+ /* Interpret name argument for mailbox_alloc_for_user() as a SPECIAL-USE
+ flag. */
+ MAILBOX_FLAG_SPECIAL_USE = 0x8000,
+ /* Mailbox is opened for reading/writing attributes. This allows ACL
+ plugin to determine correctly whether the mailbox should be allowed
+ to be opened. */
+ MAILBOX_FLAG_ATTRIBUTE_SESSION = 0x10000,
+};
+
+enum mailbox_feature {
+ /* Enable tracking modsequences */
+ MAILBOX_FEATURE_CONDSTORE = 0x01,
+};
+
+enum mailbox_existence {
+ MAILBOX_EXISTENCE_NONE,
+ MAILBOX_EXISTENCE_NOSELECT,
+ MAILBOX_EXISTENCE_SELECT
+};
+
+enum mailbox_status_items {
+ STATUS_MESSAGES = 0x01,
+ STATUS_RECENT = 0x02,
+ STATUS_UIDNEXT = 0x04,
+ STATUS_UIDVALIDITY = 0x08,
+ STATUS_UNSEEN = 0x10,
+ STATUS_FIRST_UNSEEN_SEQ = 0x20,
+ STATUS_KEYWORDS = 0x40,
+ STATUS_HIGHESTMODSEQ = 0x80,
+ STATUS_PERMANENT_FLAGS = 0x200,
+ STATUS_FIRST_RECENT_UID = 0x400,
+ STATUS_LAST_CACHED_SEQ = 0x800,
+ STATUS_CHECK_OVER_QUOTA = 0x1000, /* return error if over quota */
+ STATUS_HIGHESTPVTMODSEQ = 0x2000,
+ /* status items that must not be looked up with
+ mailbox_get_open_status(), because they can return failure. */
+#define MAILBOX_STATUS_FAILING_ITEMS \
+ (STATUS_LAST_CACHED_SEQ | STATUS_CHECK_OVER_QUOTA)
+};
+
+enum mailbox_metadata_items {
+ MAILBOX_METADATA_GUID = 0x01,
+ MAILBOX_METADATA_VIRTUAL_SIZE = 0x02,
+ MAILBOX_METADATA_CACHE_FIELDS = 0x04,
+ MAILBOX_METADATA_PRECACHE_FIELDS = 0x08,
+ MAILBOX_METADATA_BACKEND_NAMESPACE = 0x10,
+ MAILBOX_METADATA_PHYSICAL_SIZE = 0x20,
+ MAILBOX_METADATA_FIRST_SAVE_DATE = 0x40
+ /* metadata items that require mailbox to be synced at least once. */
+#define MAILBOX_METADATA_SYNC_ITEMS \
+ (MAILBOX_METADATA_VIRTUAL_SIZE | MAILBOX_METADATA_PHYSICAL_SIZE | \
+ MAILBOX_METADATA_FIRST_SAVE_DATE)
+};
+
+enum mailbox_search_result_flags {
+ /* Update search results whenever the mailbox view is synced.
+ Expunged messages are removed even without this flag. */
+ MAILBOX_SEARCH_RESULT_FLAG_UPDATE = 0x01,
+ /* Queue changes so _sync() can be used. */
+ MAILBOX_SEARCH_RESULT_FLAG_QUEUE_SYNC = 0x02
+};
+
+enum mail_sort_type {
+ MAIL_SORT_ARRIVAL = 0x0001,
+ MAIL_SORT_CC = 0x0002,
+ MAIL_SORT_DATE = 0x0004,
+ MAIL_SORT_FROM = 0x0008,
+ MAIL_SORT_SIZE = 0x0010,
+ MAIL_SORT_SUBJECT = 0x0020,
+ MAIL_SORT_TO = 0x0040,
+ MAIL_SORT_RELEVANCY = 0x0080,
+ MAIL_SORT_DISPLAYFROM = 0x0100,
+ MAIL_SORT_DISPLAYTO = 0x0200,
+ MAIL_SORT_POP3_ORDER = 0x0400,
+/* Maximum size for sort program (each one separately + END) */
+#define MAX_SORT_PROGRAM_SIZE (11 + 1)
+
+ MAIL_SORT_MASK = 0x0fff,
+ MAIL_SORT_FLAG_REVERSE = 0x1000, /* reverse this mask type */
+
+ MAIL_SORT_END = 0x0000 /* ends sort program */
+};
+
+enum mail_fetch_field {
+ MAIL_FETCH_FLAGS = 0x00000001,
+ MAIL_FETCH_MESSAGE_PARTS = 0x00000002,
+
+ MAIL_FETCH_STREAM_HEADER = 0x00000004,
+ MAIL_FETCH_STREAM_BODY = 0x00000008,
+
+ MAIL_FETCH_DATE = 0x00000010,
+ MAIL_FETCH_RECEIVED_DATE = 0x00000020,
+ MAIL_FETCH_SAVE_DATE = 0x00000040,
+ MAIL_FETCH_PHYSICAL_SIZE = 0x00000080,
+ MAIL_FETCH_VIRTUAL_SIZE = 0x00000100,
+
+ /* Set has_nuls / has_no_nuls fields */
+ MAIL_FETCH_NUL_STATE = 0x00000200,
+
+ MAIL_FETCH_STREAM_BINARY = 0x00000400,
+
+ /* specials: */
+ MAIL_FETCH_IMAP_BODY = 0x00001000,
+ MAIL_FETCH_IMAP_BODYSTRUCTURE = 0x00002000,
+ MAIL_FETCH_IMAP_ENVELOPE = 0x00004000,
+ MAIL_FETCH_FROM_ENVELOPE = 0x00008000,
+ MAIL_FETCH_HEADER_MD5 = 0x00010000,
+ MAIL_FETCH_STORAGE_ID = 0x00020000,
+ MAIL_FETCH_UIDL_BACKEND = 0x00040000,
+ MAIL_FETCH_MAILBOX_NAME = 0x00080000,
+ MAIL_FETCH_SEARCH_RELEVANCY = 0x00100000,
+ MAIL_FETCH_GUID = 0x00200000,
+ MAIL_FETCH_POP3_ORDER = 0x00400000,
+ MAIL_FETCH_REFCOUNT = 0x00800000,
+ MAIL_FETCH_BODY_SNIPPET = 0x01000000,
+ MAIL_FETCH_REFCOUNT_ID = 0x02000000,
+};
+
+enum mailbox_transaction_flags {
+ /* Hide changes done in this transaction from next view sync */
+ MAILBOX_TRANSACTION_FLAG_HIDE = 0x01,
+ /* External transaction. Should be used for copying and appends,
+ but nothing else. */
+ MAILBOX_TRANSACTION_FLAG_EXTERNAL = 0x02,
+ /* Always assign UIDs to messages when saving/copying. Normally this
+ is done only if it can be done easily. */
+ MAILBOX_TRANSACTION_FLAG_ASSIGN_UIDS = 0x04,
+ /* Refresh the index so lookups return latest flags/modseqs */
+ MAILBOX_TRANSACTION_FLAG_REFRESH = 0x08,
+ /* Don't update caching decisions no matter what we do in this
+ transaction (useful for e.g. precaching) */
+ MAILBOX_TRANSACTION_FLAG_NO_CACHE_DEC = 0x10,
+ /* Sync transaction describes changes to mailbox that already happened
+ to another mailbox with whom we're syncing with (dsync) */
+ MAILBOX_TRANSACTION_FLAG_SYNC = 0x20,
+ /* Don't trigger any notifications for this transaction. This
+ especially means the notify plugin. This would normally be used only
+ with _FLAG_SYNC. */
+ MAILBOX_TRANSACTION_FLAG_NO_NOTIFY = 0x40,
+};
+
+enum mailbox_sync_flags {
+ /* Make sure we sync all external changes done to mailbox */
+ MAILBOX_SYNC_FLAG_FULL_READ = 0x01,
+ /* Make sure we write all our internal changes into the mailbox */
+ MAILBOX_SYNC_FLAG_FULL_WRITE = 0x02,
+ /* If it's not too much trouble, check if there are some changes */
+ MAILBOX_SYNC_FLAG_FAST = 0x04,
+
+ /* Don't sync expunges from our view */
+ MAILBOX_SYNC_FLAG_NO_EXPUNGES = 0x08,
+ /* If mailbox is currently inconsistent, fix it instead of failing. */
+ MAILBOX_SYNC_FLAG_FIX_INCONSISTENT = 0x40,
+ /* Syncing after an EXPUNGE command. This is just an informational
+ flag for plugins. */
+ MAILBOX_SYNC_FLAG_EXPUNGE = 0x80,
+ /* Force doing a full resync of indexes. */
+ MAILBOX_SYNC_FLAG_FORCE_RESYNC = 0x100,
+ /* FIXME: kludge until something better comes along:
+ Request full text search index optimization */
+ MAILBOX_SYNC_FLAG_OPTIMIZE = 0x400
+};
+
+enum mailbox_sync_type {
+ MAILBOX_SYNC_TYPE_EXPUNGE = 0x01,
+ MAILBOX_SYNC_TYPE_FLAGS = 0x02,
+ MAILBOX_SYNC_TYPE_MODSEQ = 0x04
+};
+
+struct message_part;
+struct mail_namespace;
+struct mail_storage;
+struct mail_search_args;
+struct mail_search_result;
+struct mail_keywords;
+struct mail_save_context;
+struct mailbox;
+struct mailbox_transaction_context;
+
+struct mailbox_status {
+ uint32_t messages; /* STATUS_MESSAGES */
+ uint32_t recent; /* STATUS_RECENT */
+ uint32_t unseen; /* STATUS_UNSEEN */
+
+ uint32_t uidvalidity; /* STATUS_UIDVALIDITY */
+ uint32_t uidnext; /* STATUS_UIDNEXT */
+
+ uint32_t first_unseen_seq; /* STATUS_FIRST_UNSEEN_SEQ */
+ uint32_t first_recent_uid; /* STATUS_FIRST_RECENT_UID */
+ uint32_t last_cached_seq; /* STATUS_LAST_CACHED_SEQ */
+ uint64_t highest_modseq; /* STATUS_HIGHESTMODSEQ */
+ /* 0 if no private index (STATUS_HIGHESTPVTMODSEQ) */
+ uint64_t highest_pvt_modseq;
+
+ /* NULL-terminated array of keywords (STATUS_KEYWORDS) */
+ const ARRAY_TYPE(keywords) *keywords;
+
+ /* These flags can be permanently modified (STATUS_PERMANENT_FLAGS) */
+ enum mail_flags permanent_flags;
+ /* These flags can be modified (STATUS_PERMANENT_FLAGS) */
+ enum mail_flags flags;
+
+ /* All keywords can be permanently modified (STATUS_PERMANENT_FLAGS) */
+ bool permanent_keywords:1;
+ /* More keywords can be created (STATUS_PERMANENT_FLAGS) */
+ bool allow_new_keywords:1;
+ /* Modseqs aren't permanent (index is in memory) (STATUS_HIGHESTMODSEQ) */
+ bool nonpermanent_modseqs:1;
+ /* Modseq tracking has never been enabled for this mailbox
+ yet. (STATUS_HIGHESTMODSEQ) */
+ bool no_modseq_tracking:1;
+
+ /* Messages have GUIDs (always set) */
+ bool have_guids:1;
+ /* mailbox_save_set_guid() works (always set) */
+ bool have_save_guids:1;
+ /* GUIDs are always 128bit (always set) */
+ bool have_only_guid128:1;
+};
+
+struct mailbox_cache_field {
+ const char *name;
+ int decision; /* enum mail_cache_decision_type */
+ /* last_used is unchanged, if it's (time_t)-1 */
+ time_t last_used;
+};
+ARRAY_DEFINE_TYPE(mailbox_cache_field, struct mailbox_cache_field);
+
+struct mailbox_metadata {
+ guid_128_t guid;
+ /* sum of virtual size of all messages in mailbox */
+ uint64_t virtual_size;
+ /* sum of physical size of all messages in mailbox */
+ uint64_t physical_size;
+ /* timestamp of when the first message was saved.
+ (time_t)-1 if there are no mails in the mailbox. */
+ time_t first_save_date;
+
+ /* Fields that have "temp" or "yes" caching decision. */
+ const ARRAY_TYPE(mailbox_cache_field) *cache_fields;
+ /* Fields that should be precached */
+ enum mail_fetch_field precache_fields;
+
+ /* imapc backend returns this based on the remote NAMESPACE reply,
+ while currently other backends return "" and type the same as the
+ mailbox's real namespace type */
+ const char *backend_ns_prefix;
+ enum mail_namespace_type backend_ns_type;
+};
+
+struct mailbox_update {
+ /* All non-zero fields are changed. */
+ guid_128_t mailbox_guid;
+ uint32_t uid_validity;
+ uint32_t min_next_uid;
+ uint32_t min_first_recent_uid;
+ uint64_t min_highest_modseq;
+ uint64_t min_highest_pvt_modseq;
+ /* Modify caching decisions, terminated by name=NULL */
+ const struct mailbox_cache_field *cache_updates;
+};
+
+struct mail_transaction_commit_changes {
+ /* Unreference the pool to free memory used by these changes. */
+ pool_t pool;
+
+ /* UIDVALIDITY for assigned UIDs. */
+ uint32_t uid_validity;
+ /* UIDs assigned to saved messages. Not necessarily ascending.
+ If UID assignment wasn't required (e.g. LDA), this array may also be
+ empty. Otherwise all of the saved mails got an UID. */
+ ARRAY_TYPE(seq_range) saved_uids;
+
+ /* number of modseq changes that couldn't be changed as requested */
+ unsigned int ignored_modseq_changes;
+
+ /* Changes that occurred within this transaction */
+ enum mail_index_transaction_change changes_mask;
+ /* User doesn't have read ACL for the mailbox, so don't show the
+ uid_validity / saved_uids. */
+ bool no_read_perm;
+};
+
+struct mailbox_sync_rec {
+ uint32_t seq1, seq2;
+ enum mailbox_sync_type type;
+};
+struct mailbox_sync_status {
+ /* There are expunges that haven't been synced yet */
+ bool sync_delayed_expunges:1;
+};
+
+struct mailbox_expunge_rec {
+ /* IMAP UID */
+ uint32_t uid;
+ /* 128 bit GUID. If the actual GUID has a different size, this
+ contains last bits of its SHA1 sum. */
+ guid_128_t guid_128;
+};
+ARRAY_DEFINE_TYPE(mailbox_expunge_rec, struct mailbox_expunge_rec);
+
+enum mail_lookup_abort {
+ /* Perform everything no matter what it takes */
+ MAIL_LOOKUP_ABORT_NEVER = 0,
+ /* Abort if the operation would require reading message header/body or
+ otherwise opening the mail file (e.g. with dbox metadata is read by
+ opening and reading the file). This still allows somewhat fast
+ operations to be performed, such as stat()ing a file. */
+ MAIL_LOOKUP_ABORT_READ_MAIL,
+ /* Abort if the operation can't be done fully using cache file */
+ MAIL_LOOKUP_ABORT_NOT_IN_CACHE,
+ /* Abort if the operation can't be done fully using cache file.
+ * During this lookup all cache lookups that have "no" decision
+ * will be changed to "tmp". This way the field will start to be
+ * cached in the future. */
+ MAIL_LOOKUP_ABORT_NOT_IN_CACHE_START_CACHING,
+};
+
+enum mail_access_type {
+ MAIL_ACCESS_TYPE_DEFAULT = 0,
+ /* Mail is being used for searching */
+ MAIL_ACCESS_TYPE_SEARCH,
+ /* Mail is being used for sorting results */
+ MAIL_ACCESS_TYPE_SORT,
+};
+
+struct mail {
+ /* always set */
+ struct mailbox *box;
+ struct mailbox_transaction_context *transaction;
+ uint32_t seq, uid;
+
+ bool expunged:1;
+ bool saving:1; /* This mail is still being saved */
+ bool has_nuls:1; /* message data is known to contain NULs */
+ bool has_no_nuls:1; /* -''- known to not contain NULs */
+
+ /* Mail's header/body stream was opened (or attempted to be opened)
+ within this request. If lookup_abort!=MAIL_LOOKUP_ABORT_NEVER, this
+ can't become TRUE. */
+ bool mail_stream_accessed:1;
+ /* Mail's fast metadata was accessed within this request, e.g. the mail
+ file was stat()ed. If mail_stream_opened==TRUE, this value isn't
+ accurate anymore, because some backends may always set this when
+ stream is opened and some don't. If lookup_abort is
+ MAIL_LOOKUP_ABORT_NOT_IN_CACHE, this can't become TRUE. */
+ bool mail_metadata_accessed:1;
+
+ enum mail_access_type access_type;
+
+ /* If the lookup is aborted, error is set to MAIL_ERROR_NOTPOSSIBLE */
+ enum mail_lookup_abort lookup_abort;
+};
+
+struct mail_storage_callbacks {
+ /* "* OK <text>" */
+ void (*notify_ok)(struct mailbox *mailbox, const char *text,
+ void *context);
+ /* "* NO <text>" */
+ void (*notify_no)(struct mailbox *mailbox, const char *text,
+ void *context);
+
+};
+
+struct mailbox_virtual_pattern {
+ struct mail_namespace *ns;
+ const char *pattern;
+};
+ARRAY_DEFINE_TYPE(mailbox_virtual_patterns, struct mailbox_virtual_pattern);
+ARRAY_DEFINE_TYPE(mail_storage, struct mail_storage *);
+ARRAY_DEFINE_TYPE(mailboxes, struct mailbox *);
+
+extern ARRAY_TYPE(mail_storage) mail_storage_classes;
+
+typedef void mailbox_notify_callback_t(struct mailbox *box, void *context);
+
+void mail_storage_init(void);
+void mail_storage_deinit(void);
+
+/* register all mail storages */
+void mail_storage_register_all(void);
+
+/* Register mail storage class with given name - all methods that are NULL
+ are set to default methods */
+void mail_storage_class_register(struct mail_storage *storage_class);
+void mail_storage_class_unregister(struct mail_storage *storage_class);
+/* Find mail storage class by name */
+struct mail_storage *mail_storage_find_class(const char *name);
+
+/* Create a new instance of registered mail storage class with given
+ storage-specific data. If driver is NULL, it's tried to be autodetected
+ from ns location. If ns location is NULL, it uses the first storage that
+ exists. The storage is put into ns->storage. */
+int mail_storage_create(struct mail_namespace *ns, const char *driver,
+ enum mail_storage_flags flags, const char **error_r)
+ ATTR_NULL(2);
+int mail_storage_create_full(struct mail_namespace *ns, const char *driver,
+ const char *data, enum mail_storage_flags flags,
+ struct mail_storage **storage_r,
+ const char **error_r) ATTR_NULL(2);
+void mail_storage_unref(struct mail_storage **storage);
+
+/* Returns the mail storage settings. */
+const struct mail_storage_settings *
+mail_storage_get_settings(struct mail_storage *storage) ATTR_PURE;
+struct mail_user *mail_storage_get_user(struct mail_storage *storage) ATTR_PURE;
+
+/* Set storage callback functions to use. */
+void mail_storage_set_callbacks(struct mail_storage *storage,
+ struct mail_storage_callbacks *callbacks,
+ void *context) ATTR_NULL(3);
+
+/* Purge storage's mailboxes (freeing disk space from expunged mails),
+ if supported by the storage. Otherwise just a no-op. */
+int mail_storage_purge(struct mail_storage *storage);
+
+/* Returns the error message of last occurred error. */
+const char * ATTR_NOWARN_UNUSED_RESULT
+mail_storage_get_last_error(struct mail_storage *storage,
+ enum mail_error *error_r) ATTR_NULL(2);
+/* Wrapper for mail_storage_get_last_error(); */
+const char * ATTR_NOWARN_UNUSED_RESULT
+mailbox_get_last_error(struct mailbox *box, enum mail_error *error_r)
+ ATTR_NULL(2);
+/* Wrapper for mail_storage_get_last_error(); */
+enum mail_error mailbox_get_last_mail_error(struct mailbox *box);
+
+const char * ATTR_NOWARN_UNUSED_RESULT
+mail_storage_get_last_internal_error(struct mail_storage *storage,
+ enum mail_error *error_r) ATTR_NULL(2);
+/* Wrapper for mail_storage_get_last_internal_error(); */
+const char * ATTR_NOWARN_UNUSED_RESULT
+mailbox_get_last_internal_error(struct mailbox *box,
+ enum mail_error *error_r) ATTR_NULL(2);
+
+/* Save the last error until it's popped. This is useful for cases where the
+ storage has already failed, but the cleanup code path changes the error to
+ something else unwanted. */
+void mail_storage_last_error_push(struct mail_storage *storage);
+void mail_storage_last_error_pop(struct mail_storage *storage);
+
+/* Returns TRUE if mailboxes are files. */
+bool mail_storage_is_mailbox_file(struct mail_storage *storage) ATTR_PURE;
+
+/* Initialize mailbox without actually opening any files or verifying that
+ it exists. Note that append and copy may open the selected mailbox again
+ with possibly different readonly-state. */
+struct mailbox *mailbox_alloc(struct mailbox_list *list, const char *vname,
+ enum mailbox_flags flags);
+/* Like mailbox_alloc(), but use mailbox GUID. */
+struct mailbox *mailbox_alloc_guid(struct mailbox_list *list,
+ const guid_128_t guid,
+ enum mailbox_flags flags);
+/* Initialize mailbox for a particular user without actually opening any files
+ or verifying that it exists. The mname parameter is normally equal to the
+ mailbox vname, except when the MAILBOX_FLAG_SPECIAL_USE flag is set, in which
+ case it is the special-use flag. */
+struct mailbox *
+mailbox_alloc_for_user(struct mail_user *user, const char *mname,
+ enum mailbox_flags flags);
+
+/* Get mailbox existence state. If auto_boxes=FALSE, return
+ MAILBOX_EXISTENCE_NONE for autocreated mailboxes that haven't been
+ physically created yet */
+int mailbox_exists(struct mailbox *box, bool auto_boxes,
+ enum mailbox_existence *existence_r);
+/* Open the mailbox. If this function isn't called explicitly, it's also called
+ internally by lib-storage when necessary. */
+int mailbox_open(struct mailbox *box);
+/* Open mailbox as read-only using the given stream as input. */
+int mailbox_open_stream(struct mailbox *box, struct istream *input);
+/* Close mailbox. Same as if mailbox was freed and re-allocated. */
+void mailbox_close(struct mailbox *box);
+/* Close and free the mailbox. */
+void mailbox_free(struct mailbox **box);
+
+/* Returns TRUE if box1 points to the same mailbox as ns2/vname2. */
+bool mailbox_equals(const struct mailbox *box1,
+ const struct mail_namespace *ns2,
+ const char *vname2) ATTR_PURE;
+/* Returns TRUE if the mailbox is user's INBOX or another user's shared INBOX */
+bool mailbox_is_any_inbox(struct mailbox *box);
+
+/* Returns TRUE if the mailbox has the specified special use flag assigned. */
+bool mailbox_has_special_use(struct mailbox *box, const char *special_use);
+
+/* Change mailbox_verify_create_name() to not verify new mailbox name
+ restrictions (but still check that it's a valid existing name). This is
+ mainly used by dsync to make sure the sync works even though the original
+ name isn't valid anymore. */
+void mailbox_skip_create_name_restrictions(struct mailbox *box, bool set);
+/* Returns -1 if mailbox_create() is guaranteed to fail because the mailbox
+ name is invalid, 0 not. The error message contains a reason. */
+int mailbox_verify_create_name(struct mailbox *box);
+/* Create a mailbox. Returns failure if it already exists. Mailbox name is
+ allowed to contain multiple new nonexistent hierarchy levels. If directory
+ is TRUE, the mailbox should be created so that it can contain children. The
+ mailbox itself doesn't have to be created as long as it shows up in LIST.
+ If update is non-NULL, its contents are used to set initial mailbox
+ metadata. */
+int mailbox_create(struct mailbox *box, const struct mailbox_update *update,
+ bool directory) ATTR_NULL(2);
+/* Update existing mailbox's metadata. */
+int mailbox_update(struct mailbox *box, const struct mailbox_update *update);
+/* Delete mailbox (and its parent directory, if it has no siblings) */
+int mailbox_delete(struct mailbox *box);
+/* Delete mailbox, but only if it's empty. If it's not, fails with
+ MAIL_ERROR_EXISTS. */
+int mailbox_delete_empty(struct mailbox *box);
+/* Rename mailbox (and its children). Renaming across different mailbox lists
+ is possible only between private namespaces and storages of the same type.
+ If the rename fails, the error is set to src's storage. */
+int mailbox_rename(struct mailbox *src, struct mailbox *dest);
+/* Subscribe/unsubscribe mailbox. Subscribing to
+ nonexistent mailboxes is optional. */
+int mailbox_set_subscribed(struct mailbox *box, bool set);
+/* Returns TRUE if mailbox is subscribed, FALSE if not. This function
+ doesn't refresh the subscriptions list, but assumes that it's been done by
+ e.g. mailbox_list_iter*(). */
+bool mailbox_is_subscribed(struct mailbox *box);
+
+/* Enable the given feature for the mailbox. */
+int mailbox_enable(struct mailbox *box, enum mailbox_feature features);
+/* Returns all enabled features. */
+enum mailbox_feature
+mailbox_get_enabled_features(struct mailbox *box) ATTR_PURE;
+
+/* Returns storage of given mailbox */
+struct mail_storage *mailbox_get_storage(const struct mailbox *box) ATTR_PURE;
+/* Return namespace of given mailbox. */
+struct mail_namespace *
+mailbox_get_namespace(const struct mailbox *box) ATTR_PURE;
+/* Returns the storage's settings. */
+const struct mail_storage_settings *
+mailbox_get_settings(struct mailbox *box) ATTR_PURE;
+/* Returns the mailbox's settings, or NULL if there are none. */
+const struct mailbox_settings *
+mailbox_settings_find(struct mail_namespace *ns, const char *vname);
+
+/* Returns the (virtual) name of the given mailbox. */
+const char *mailbox_get_vname(const struct mailbox *box) ATTR_PURE;
+/* Returns the backend name of given mailbox. */
+const char *mailbox_get_name(const struct mailbox *box) ATTR_PURE;
+
+/* Returns TRUE if mailbox is read-only. */
+bool mailbox_is_readonly(struct mailbox *box);
+/* Returns TRUE if two mailboxes point to the same physical mailbox. */
+bool mailbox_backends_equal(const struct mailbox *box1,
+ const struct mailbox *box2);
+/* Returns TRUE if mailbox is now in inconsistent state, meaning that
+ the message IDs etc. may have changed - only way to recover this
+ would be to fully close the mailbox and reopen it. With IMAP
+ connection this would mean a forced disconnection since we can't
+ do forced CLOSE. */
+bool mailbox_is_inconsistent(struct mailbox *box);
+
+/* Gets the mailbox status information. If mailbox isn't opened yet, try to
+ return the results from mailbox list indexes. Otherwise the mailbox is
+ opened and synced. If the mailbox is already opened, no syncing is done
+ automatically. */
+int mailbox_get_status(struct mailbox *box, enum mailbox_status_items items,
+ struct mailbox_status *status_r);
+/* Gets the mailbox status, requires that mailbox is already opened. */
+void mailbox_get_open_status(struct mailbox *box,
+ enum mailbox_status_items items,
+ struct mailbox_status *status_r);
+/* Gets mailbox metadata */
+int mailbox_get_metadata(struct mailbox *box, enum mailbox_metadata_items items,
+ struct mailbox_metadata *metadata_r);
+/* Returns a mask of flags that are private to user in this mailbox
+ (as opposed to flags shared between users). */
+enum mail_flags mailbox_get_private_flags_mask(struct mailbox *box);
+
+/* Synchronize the mailbox. */
+struct mailbox_sync_context *
+mailbox_sync_init(struct mailbox *box, enum mailbox_sync_flags flags);
+bool mailbox_sync_next(struct mailbox_sync_context *ctx,
+ struct mailbox_sync_rec *sync_rec_r);
+int mailbox_sync_deinit(struct mailbox_sync_context **ctx,
+ struct mailbox_sync_status *status_r);
+/* One-step mailbox synchronization. Use this if you don't care about
+ changes. */
+int mailbox_sync(struct mailbox *box, enum mailbox_sync_flags flags);
+
+/* Call given callback function when something changes in the mailbox. */
+void mailbox_notify_changes(struct mailbox *box,
+ mailbox_notify_callback_t *callback, void *context)
+ ATTR_NULL(3);
+#define mailbox_notify_changes(box, callback, context) \
+ mailbox_notify_changes(box, (mailbox_notify_callback_t *)callback, \
+ (void *)((char *)context - CALLBACK_TYPECHECK(callback, \
+ void (*)(struct mailbox *, typeof(context)))))
+void mailbox_notify_changes_stop(struct mailbox *box);
+
+struct mailbox_transaction_context *
+mailbox_transaction_begin(struct mailbox *box,
+ enum mailbox_transaction_flags flags,
+ const char *reason);
+int mailbox_transaction_commit(struct mailbox_transaction_context **t);
+int mailbox_transaction_commit_get_changes(
+ struct mailbox_transaction_context **t,
+ struct mail_transaction_commit_changes *changes_r);
+void mailbox_transaction_rollback(struct mailbox_transaction_context **t);
+/* Return the number of active transactions for the mailbox. */
+unsigned int mailbox_transaction_get_count(const struct mailbox *box) ATTR_PURE;
+/* When committing transaction, drop flag/keyword updates for messages whose
+ modseq is larger than max_modseq. Save those messages' sequences to the
+ given array. */
+void mailbox_transaction_set_max_modseq(struct mailbox_transaction_context *t,
+ uint64_t max_modseq,
+ ARRAY_TYPE(seq_range) *seqs);
+
+struct mailbox *
+mailbox_transaction_get_mailbox(const struct mailbox_transaction_context *t)
+ ATTR_PURE;
+
+/* Convert uid range to sequence range. */
+void mailbox_get_seq_range(struct mailbox *box, uint32_t uid1, uint32_t uid2,
+ uint32_t *seq1_r, uint32_t *seq2_r);
+/* Convert sequence range to uid range. If sequences contain
+ (uint32_t)-1 to specify "*", they're preserved. */
+void mailbox_get_uid_range(struct mailbox *box,
+ const ARRAY_TYPE(seq_range) *seqs,
+ ARRAY_TYPE(seq_range) *uids);
+/* Get list of messages' that have been expunged after prev_modseq and that
+ exist in uids_filter range. UIDs that have been expunged after the last
+ mailbox sync aren't returned. Returns TRUE if ok, FALSE if modseq is lower
+ than we can check for (but expunged_uids is still set as best as it can). */
+bool mailbox_get_expunges(struct mailbox *box, uint64_t prev_modseq,
+ const ARRAY_TYPE(seq_range) *uids_filter,
+ ARRAY_TYPE(mailbox_expunge_rec) *expunges);
+/* Same as mailbox_get_expunges(), but return only list of UIDs. Not caring
+ about GUIDs is slightly faster. */
+bool mailbox_get_expunged_uids(struct mailbox *box, uint64_t prev_modseq,
+ const ARRAY_TYPE(seq_range) *uids_filter,
+ ARRAY_TYPE(seq_range) *expunged_uids);
+
+/* Initialize header lookup for given headers. */
+struct mailbox_header_lookup_ctx *
+mailbox_header_lookup_init(struct mailbox *box, const char *const headers[]);
+void mailbox_header_lookup_ref(struct mailbox_header_lookup_ctx *ctx);
+void mailbox_header_lookup_unref(struct mailbox_header_lookup_ctx **ctx);
+/* Merge two header lookups. */
+struct mailbox_header_lookup_ctx *
+mailbox_header_lookup_merge(const struct mailbox_header_lookup_ctx *hdr1,
+ const struct mailbox_header_lookup_ctx *hdr2);
+
+/* Initialize new search request. If sort_program is non-NULL, the messages are
+ returned in the requested order, otherwise from first to last. */
+struct mail_search_context * ATTR_NULL(3, 5)
+mailbox_search_init(struct mailbox_transaction_context *t,
+ struct mail_search_args *args,
+ const enum mail_sort_type *sort_program,
+ enum mail_fetch_field wanted_fields,
+ struct mailbox_header_lookup_ctx *wanted_headers);
+/* Deinitialize search request. */
+int mailbox_search_deinit(struct mail_search_context **ctx);
+/* Search the next message. Returns TRUE if found, FALSE if not. */
+bool mailbox_search_next(struct mail_search_context *ctx, struct mail **mail_r);
+/* Like mailbox_search_next(), but don't spend too much time searching.
+ Returns FALSE with tryagain_r=FALSE if finished, and tryagain_r=TRUE if
+ more results will be returned by calling the function again. */
+bool mailbox_search_next_nonblock(struct mail_search_context *ctx,
+ struct mail **mail_r, bool *tryagain_r);
+/* Returns TRUE if some messages were already expunged and we couldn't
+ determine correctly if those messages should have been returned in this
+ search. */
+bool mailbox_search_seen_lost_data(struct mail_search_context *ctx);
+/* Detach the given mail from the search context. This allows the mail to live
+ even after mail_search_context has been freed. */
+void mailbox_search_mail_detach(struct mail_search_context *ctx,
+ struct mail *mail);
+
+/* Remember the search result for future use. This must be called before the
+ first mailbox_search_next*() call. */
+struct mail_search_result *
+mailbox_search_result_save(struct mail_search_context *ctx,
+ enum mailbox_search_result_flags flags);
+/* Free memory used by search result. */
+void mailbox_search_result_free(struct mail_search_result **result);
+/* A simplified API for searching and saving the result. */
+int mailbox_search_result_build(struct mailbox_transaction_context *t,
+ struct mail_search_args *args,
+ enum mailbox_search_result_flags flags,
+ struct mail_search_result **result_r);
+/* Return all messages' UIDs in the search result. */
+const ARRAY_TYPE(seq_range) *
+mailbox_search_result_get(struct mail_search_result *result);
+/* Return messages that have been removed and added since the last sync call.
+ This function must not be called if search result wasn't saved with
+ _QUEUE_SYNC flag. */
+void mailbox_search_result_sync(struct mail_search_result *result,
+ ARRAY_TYPE(seq_range) *removed_uids,
+ ARRAY_TYPE(seq_range) *added_uids);
+
+/* Build mail_keywords from NULL-terminated keywords list. Any duplicate
+ keywords are removed. Returns 0 if successful, -1 if there are invalid
+ keywords (error is set). */
+int mailbox_keywords_create(struct mailbox *box, const char *const keywords[],
+ struct mail_keywords **keywords_r);
+/* Like mailbox_keywords_create(), except ignore invalid keywords. */
+struct mail_keywords *
+mailbox_keywords_create_valid(struct mailbox *box,
+ const char *const keywords[]);
+struct mail_keywords *
+mailbox_keywords_create_from_indexes(struct mailbox *box,
+ const ARRAY_TYPE(keyword_indexes) *idx);
+/* Return union of two mail_keywords. They must be created in the same
+ mailbox. */
+struct mail_keywords *mailbox_keywords_merge(struct mail_keywords *keywords1,
+ struct mail_keywords *keywords2);
+void mailbox_keywords_ref(struct mail_keywords *keywords);
+void mailbox_keywords_unref(struct mail_keywords **keywords);
+/* Returns TRUE if keyword is valid, FALSE and error if not. */
+bool mailbox_keyword_is_valid(struct mailbox *box, const char *keyword,
+ const char **error_r);
+
+/* Initialize saving a new mail. You must not try to save more than one mail
+ at a time. */
+struct mail_save_context *
+mailbox_save_alloc(struct mailbox_transaction_context *t);
+/* Set the flags and keywords. Nothing is set by default. */
+void mailbox_save_set_flags(struct mail_save_context *ctx,
+ enum mail_flags flags,
+ struct mail_keywords *keywords);
+/* Copy flags and keywords from given mail. */
+void mailbox_save_copy_flags(struct mail_save_context *ctx, struct mail *mail);
+/* Set message's modseq to be at least min_modseq. */
+void mailbox_save_set_min_modseq(struct mail_save_context *ctx,
+ uint64_t min_modseq);
+/* If received date isn't specified the current time is used. timezone_offset
+ specifies the preferred timezone in minutes, but it may be ignored if
+ backend doesn't support storing it. */
+void mailbox_save_set_received_date(struct mail_save_context *ctx,
+ time_t received_date, int timezone_offset);
+/* Set the "message saved" date. This should be set only when you're
+ replicating/restoring an existing mailbox. */
+void mailbox_save_set_save_date(struct mail_save_context *ctx,
+ time_t save_date);
+/* Set the envelope sender. This is currently used only with mbox files to
+ specify the address in From_-line. */
+void mailbox_save_set_from_envelope(struct mail_save_context *ctx,
+ const char *envelope);
+/* Set message's UID. If UID is smaller than the current next_uid, it's given
+ a new UID anyway. */
+void mailbox_save_set_uid(struct mail_save_context *ctx, uint32_t uid);
+/* Set globally unique ID for the saved mail. A new GUID is generated by
+ default. This function should usually be called only when copying an
+ existing mail (or restoring a mail from backup). */
+void mailbox_save_set_guid(struct mail_save_context *ctx, const char *guid);
+/* Set message's POP3 UIDL, if the backend supports it. */
+void mailbox_save_set_pop3_uidl(struct mail_save_context *ctx,
+ const char *uidl);
+/* Specify ordering for POP3 messages. The default is to add them to the end
+ of the mailbox. Not all backends support this. */
+void mailbox_save_set_pop3_order(struct mail_save_context *ctx,
+ unsigned int order);
+/* Returns the destination mail */
+struct mail *mailbox_save_get_dest_mail(struct mail_save_context *ctx);
+/* Begin saving the message. All mail_save_set_*() calls must have been called
+ before this function. If the save initialization fails, the context is freed
+ and -1 is returned. After beginning the save you should keep calling
+ i_stream_read() and calling mailbox_save_continue() as long as there's
+ more input. */
+int mailbox_save_begin(struct mail_save_context **ctx, struct istream *input);
+int mailbox_save_continue(struct mail_save_context *ctx);
+int mailbox_save_finish(struct mail_save_context **ctx);
+void mailbox_save_cancel(struct mail_save_context **ctx);
+
+struct mailbox_transaction_context *
+mailbox_save_get_transaction(struct mail_save_context *ctx);
+
+/* Copy the given message. You'll need to specify the flags etc. using the
+ mailbox_save_*() functions. */
+int mailbox_copy(struct mail_save_context **ctx, struct mail *mail);
+/* Move the given message. This is usually equivalent to copy+expunge,
+ but without enforcing quota. */
+int mailbox_move(struct mail_save_context **ctx, struct mail *mail);
+/* Same as mailbox_copy(), but treat the message as if it's being saved,
+ not copied. (For example: New mail delivered to multiple maildirs, with
+ each mails being hard link copies.) */
+int mailbox_save_using_mail(struct mail_save_context **ctx, struct mail *mail);
+
+struct mail *mail_alloc(struct mailbox_transaction_context *t,
+ enum mail_fetch_field wanted_fields,
+ struct mailbox_header_lookup_ctx *wanted_headers)
+ ATTR_NULL(3);
+void mail_free(struct mail **mail);
+void mail_set_seq(struct mail *mail, uint32_t seq);
+/* Returns TRUE if successful, FALSE if message doesn't exist.
+ mail_*() functions shouldn't be called if FALSE is returned. */
+bool mail_set_uid(struct mail *mail, uint32_t uid);
+
+/* Add wanted fields/headers on top of existing ones. These will be forgotten
+ after the next mail_set_seq/uid() that closes the existing mail. Note that
+ it's valid to call this function while there is no mail assigned
+ (mail->seq==0), i.e. this is called before any mail_set_seq/uid() or after
+ mail.close(). */
+void mail_add_temp_wanted_fields(struct mail *mail,
+ enum mail_fetch_field fields,
+ struct mailbox_header_lookup_ctx *headers)
+ ATTR_NULL(3);
+
+/* Returns mail's event. This lazily creates the event when called for the
+ first time for the mail. */
+struct event *mail_event(struct mail *mail);
+/* Returns message's flags */
+enum mail_flags mail_get_flags(struct mail *mail);
+/* Returns message's keywords */
+const char *const *mail_get_keywords(struct mail *mail);
+/* Returns message's keywords */
+const ARRAY_TYPE(keyword_indexes) *mail_get_keyword_indexes(struct mail *mail);
+/* Returns message's modseq */
+uint64_t mail_get_modseq(struct mail *mail);
+/* Returns message's private modseq, or 0 if message hasn't had any
+ private flag changes. This is useful only for shared mailboxes that have
+ a private index defined. */
+uint64_t mail_get_pvt_modseq(struct mail *mail);
+
+/* Returns message's MIME parts */
+int mail_get_parts(struct mail *mail, struct message_part **parts_r);
+
+/* Get the Date-header of the mail. Timezone is in minutes. date=0 if it
+ wasn't found or it was invalid. */
+int mail_get_date(struct mail *mail, time_t *date_r, int *timezone_r);
+/* Get the time when the mail was received (IMAP INTERNALDATE). */
+int mail_get_received_date(struct mail *mail, time_t *date_r);
+/* Get the time when the mail was saved into this mailbox. This returns -1 on
+ error, 0 if a real save date is not supported and a fall-back date is used,
+ and 1 when a save date was successfully retrieved. */
+int mail_get_save_date(struct mail *mail, time_t *date_r);
+
+/* Get the space used by the mail as seen by the reader. Linefeeds are always
+ counted as being CR+LF. */
+int mail_get_virtual_size(struct mail *mail, uoff_t *size_r);
+/* Get the size of the stream returned by mail_get_stream(). */
+int mail_get_physical_size(struct mail *mail, uoff_t *size_r);
+
+/* Get value for single header field, or NULL if header wasn't found.
+ Returns 1 if header was found, 0 if not, -1 if error. */
+int mail_get_first_header(struct mail *mail, const char *field,
+ const char **value_r);
+/* Like mail_get_first_header(), but decode MIME encoded words to UTF-8.
+ Also multiline headers are returned unfolded.
+
+ Do not use this function for getting structured fields (e.g. address fields),
+ because decoding may break the structuring. Instead parse them first and
+ only afterwards decode the encoded words. */
+int mail_get_first_header_utf8(struct mail *mail, const char *field,
+ const char **value_r);
+/* Return a NULL-terminated list of values for each found field.
+ Returns 1 if headers were found, 0 if not (value_r[0]==NULL) or
+ -1 if error. */
+int mail_get_headers(struct mail *mail, const char *field,
+ const char *const **value_r);
+/* Like mail_get_headers(), but decode MIME encoded words to UTF-8.
+ Also multiline headers are returned unfolded.
+ Do not use for structured fields (see mail_get_first_header_utf8()). */
+int mail_get_headers_utf8(struct mail *mail, const char *field,
+ const char *const **value_r);
+/* Returns stream containing specified headers. The returned stream will be
+ automatically freed when the mail is closed, or when another
+ mail_get_header_stream() call is made (so you can't have multiple header
+ streams open at the same time). */
+int mail_get_header_stream(struct mail *mail,
+ struct mailbox_header_lookup_ctx *headers,
+ struct istream **stream_r);
+/* Returns input stream pointing to beginning of message header.
+ hdr_size and body_size are updated unless they're NULL. The returned stream
+ is destroyed automatically, don't unreference it. */
+int mail_get_stream(struct mail *mail, struct message_size *hdr_size,
+ struct message_size *body_size, struct istream **stream_r)
+ ATTR_NULL(2, 3);
+/* Same as mail_get_stream(), but specify a reason why the mail is being read.
+ This can be useful for debugging purposes. */
+int mail_get_stream_because(struct mail *mail, struct message_size *hdr_size,
+ struct message_size *body_size,
+ const char *reason, struct istream **stream_r)
+ ATTR_NULL(2, 3);
+/* Similar to mail_get_stream(), but the stream may or may not contain the
+ message body. */
+int mail_get_hdr_stream(struct mail *mail, struct message_size *hdr_size,
+ struct istream **stream_r) ATTR_NULL(2);
+/* Same as mail_get_hdr_stream(), but specify a reason why the header is being
+ read. This can be useful for debugging purposes. */
+int mail_get_hdr_stream_because(struct mail *mail,
+ struct message_size *hdr_size,
+ const char *reason, struct istream **stream_r);
+/* Returns the message part's body decoded to 8bit binary. If the
+ Content-Transfer-Encoding isn't supported, returns -1 and sets error to
+ MAIL_ERROR_CONVERSION. If the part refers to a multipart, all of its
+ children are returned decoded. Note that the returned stream must be
+ unreferenced, unlike mail_get_*stream*() which automatically free it. */
+int mail_get_binary_stream(struct mail *mail, const struct message_part *part,
+ bool include_hdr, uoff_t *size_r,
+ bool *binary_r, struct istream **stream_r);
+/* Like mail_get_binary_stream(), but only return the size. */
+int mail_get_binary_size(struct mail *mail, const struct message_part *part,
+ bool include_hdr, uoff_t *size_r,
+ unsigned int *lines_r);
+
+/* Get any of the "special" fields. Unhandled specials are returned as "". */
+int mail_get_special(struct mail *mail, enum mail_fetch_field field,
+ const char **value_r);
+/* Returns the mail for the physical message. Normally this is the mail itself,
+ but in virtual mailboxes it points to the backend mailbox. */
+int mail_get_backend_mail(struct mail *mail, struct mail **real_mail_r);
+
+/* Retrieve and parse the value of the Message-ID header field. Returns 1 if the
+ header was found and it contains a valid message ID, 0 if the header was not
+ found or no valid message ID was contained in it, and -1 if an error occurred
+ while retrieving the header. Returns the message ID value including '<' and
+ '>' in the *value_r return parameter or NULL if the header wasn't found or
+ its value was invalid. */
+int mail_get_message_id(struct mail *mail, const char **value_r);
+
+/* Update message flags. */
+void mail_update_flags(struct mail *mail, enum modify_type modify_type,
+ enum mail_flags flags);
+/* Update message keywords. */
+void mail_update_keywords(struct mail *mail, enum modify_type modify_type,
+ struct mail_keywords *keywords);
+/* Update message's modseq to be at least min_modseq. */
+void mail_update_modseq(struct mail *mail, uint64_t min_modseq);
+/* Update message's private modseq to be at least min_pvt_modseq. */
+void mail_update_pvt_modseq(struct mail *mail, uint64_t min_pvt_modseq);
+
+/* Update message's POP3 UIDL (if possible). */
+void mail_update_pop3_uidl(struct mail *mail, const char *uidl);
+/* Expunge this message. Sequence numbers don't change until commit. */
+void mail_expunge(struct mail *mail);
+
+/* Add missing fields to cache. */
+int mail_precache(struct mail *mail);
+/* Mark a cached field corrupted and have it recalculated. */
+void mail_set_cache_corrupted(struct mail *mail,
+ enum mail_fetch_field field,
+ const char *reason);
+
+/* Return 128 bit GUID using input string. If guid is already 128 bit hex
+ encoded, it's returned as-is. Otherwise SHA1 sum is taken and its last
+ 128 bits are returned. */
+void mail_generate_guid_128_hash(const char *guid, guid_128_t guid_128_r);
+
+/* Parse a human-writable string into a timestamp. utc_r controls whether
+ the returned timestamp should be treated as an exact UTC time (TRUE), or
+ whether this is a human-given date where the timestamp could be adjusted
+ by the matched mails' timezones (see MAIL_SEARCH_ARG_FLAG_USE_TZ).
+
+ Returns 0 and timestamp on success, -1 if the string couldn't be parsed.
+ Currently supported string formats: yyyy-mm-dd (utc=FALSE),
+ imap date (utc=FALSE), unix timestamp (utc=TRUE), interval (e.g. n days,
+ utc=TRUE). */
+int mail_parse_human_timestamp(const char *str, time_t *timestamp_r,
+ bool *utc_r);
+
+#endif
diff --git a/src/lib-storage/mail-thread.c b/src/lib-storage/mail-thread.c
new file mode 100644
index 0000000..9426d68
--- /dev/null
+++ b/src/lib-storage/mail-thread.c
@@ -0,0 +1,37 @@
+/* Copyright (c) 2009-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "mail-thread.h"
+
+struct {
+ const char *name;
+ enum mail_thread_type type;
+} mail_thread_type_strings[] = {
+ { "REFERENCES", MAIL_THREAD_REFERENCES },
+ { "REFS", MAIL_THREAD_REFS },
+ { "ORDEREDSUBJECT", MAIL_THREAD_ORDEREDSUBJECT }
+};
+
+bool mail_thread_type_parse(const char *str, enum mail_thread_type *type_r)
+{
+ unsigned int i;
+
+ for (i = 0; i < N_ELEMENTS(mail_thread_type_strings); i++) {
+ if (strcasecmp(str, mail_thread_type_strings[i].name) == 0) {
+ *type_r = mail_thread_type_strings[i].type;
+ return TRUE;
+ }
+ }
+ return FALSE;
+}
+
+const char *mail_thread_type_to_str(enum mail_thread_type type)
+{
+ unsigned int i;
+
+ for (i = 0; i < N_ELEMENTS(mail_thread_type_strings); i++) {
+ if (mail_thread_type_strings[i].type == type)
+ return mail_thread_type_strings[i].name;
+ }
+ i_panic("Unknown mail_thread_type %d", type);
+}
diff --git a/src/lib-storage/mail-thread.h b/src/lib-storage/mail-thread.h
new file mode 100644
index 0000000..fb2e5ed
--- /dev/null
+++ b/src/lib-storage/mail-thread.h
@@ -0,0 +1,54 @@
+#ifndef MAIL_THREAD_H
+#define MAIL_THREAD_H
+
+struct mailbox;
+struct mail_search_args;
+struct mail_thread_context;
+
+enum mail_thread_type {
+ MAIL_THREAD_NONE,
+ MAIL_THREAD_ORDEREDSUBJECT,
+ MAIL_THREAD_REFERENCES,
+ MAIL_THREAD_REFS
+};
+
+struct mail_thread_child_node {
+ /* Node's index in mail hash transaction */
+ uint32_t idx;
+ /* UID or sequence */
+ uint32_t uid;
+ /* Timestamp node was sorted with (depends on thread algorithm) */
+ time_t sort_date;
+};
+ARRAY_DEFINE_TYPE(mail_thread_child_node, struct mail_thread_child_node);
+
+/* Convert thread type string to enum. Returns TRUE if ok, FALSE if type is
+ unknown. */
+bool mail_thread_type_parse(const char *str, enum mail_thread_type *type_r);
+/* Return thread type as string. */
+const char *mail_thread_type_to_str(enum mail_thread_type type);
+
+/* Build thread from given search arguments. args=NULL searches everything. */
+int mail_thread_init(struct mailbox *box, struct mail_search_args *args,
+ struct mail_thread_context **ctx_r) ATTR_NULL(2);
+void mail_thread_deinit(struct mail_thread_context **ctx);
+
+/* Iterate through thread tree. If write_seqs=TRUE, sequences are returned in
+ mail_thread_child_node.uid instead of UIDs. */
+struct mail_thread_iterate_context *
+mail_thread_iterate_init(struct mail_thread_context *ctx,
+ enum mail_thread_type thread_type, bool write_seqs);
+/* If child_iter_r is not NULL, it's set to contain another iterator if the
+ returned node contains children. The returned iterator must be freed
+ explicitly. */
+const struct mail_thread_child_node *
+mail_thread_iterate_next(struct mail_thread_iterate_context *iter,
+ struct mail_thread_iterate_context **child_iter_r);
+/* Returns number of nodes in the current iterator. */
+unsigned int
+mail_thread_iterate_count(struct mail_thread_iterate_context *iter);
+/* Free the iterator. Iterators don't reference other iterators, so it doesn't
+ matter in which order they're freed. */
+int mail_thread_iterate_deinit(struct mail_thread_iterate_context **iter);
+
+#endif
diff --git a/src/lib-storage/mail-user-lua.c b/src/lib-storage/mail-user-lua.c
new file mode 100644
index 0000000..529bd75
--- /dev/null
+++ b/src/lib-storage/mail-user-lua.c
@@ -0,0 +1,408 @@
+/* Copyright (c) 2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "str.h"
+#include "istream.h"
+#include "array.h"
+#include "var-expand.h"
+#include "dlua-script.h"
+#include "dlua-script-private.h"
+#include "mail-storage.h"
+#include "mailbox-attribute.h"
+#include "mail-storage-lua.h"
+#include "mail-storage-lua-private.h"
+#include "mail-user.h"
+
+#define LUA_STORAGE_MAIL_USER "struct mail_user"
+
+static int lua_storage_mail_user_unref(lua_State *L);
+
+void dlua_push_mail_user(lua_State *L, struct mail_user *user)
+{
+ luaL_checkstack(L, 20, "out of memory");
+ /* create a table for holding few things */
+ lua_createtable(L, 0, 20);
+ luaL_setmetatable(L, LUA_STORAGE_MAIL_USER);
+
+ mail_user_ref(user);
+ struct mail_user **ptr = lua_newuserdata(L, sizeof(struct mail_user*));
+ *ptr = user;
+ lua_createtable(L, 0, 1);
+ lua_pushcfunction(L, lua_storage_mail_user_unref);
+ lua_setfield(L, -2, "__gc");
+ lua_setmetatable(L, -2);
+ lua_setfield(L, -2, "item");
+
+#undef LUA_TABLE_SET_NUMBER
+#define LUA_TABLE_SET_NUMBER(field) \
+ lua_pushnumber(L, user->field); \
+ lua_setfield(L, -2, #field);
+#undef LUA_TABLE_SET_BOOL
+#define LUA_TABLE_SET_BOOL(field) \
+ lua_pushboolean(L, user->field); \
+ lua_setfield(L, -2, #field);
+#undef LUA_TABLE_SET_STRING
+#define LUA_TABLE_SET_STRING(field) \
+ lua_pushstring(L, user->field); \
+ lua_setfield(L, -2, #field);
+
+ const char *home = NULL;
+ (void)mail_user_get_home(user, &home);
+
+ lua_pushstring(L, home);
+ lua_setfield(L, -2, "home");
+
+ LUA_TABLE_SET_STRING(username);
+ LUA_TABLE_SET_NUMBER(uid);
+ LUA_TABLE_SET_NUMBER(gid);
+ LUA_TABLE_SET_STRING(service);
+ LUA_TABLE_SET_STRING(session_id);
+ LUA_TABLE_SET_NUMBER(session_create_time);
+
+ LUA_TABLE_SET_BOOL(nonexistent);
+ LUA_TABLE_SET_BOOL(anonymous);
+ LUA_TABLE_SET_BOOL(autocreated);
+ LUA_TABLE_SET_BOOL(mail_debug);
+ LUA_TABLE_SET_BOOL(fuzzy_search);
+ LUA_TABLE_SET_BOOL(dsyncing);
+ LUA_TABLE_SET_BOOL(admin);
+ LUA_TABLE_SET_BOOL(session_restored);
+}
+
+static struct mail_user *
+lua_check_storage_mail_user(lua_State *L, int arg)
+{
+ if (!lua_istable(L, arg)) {
+ (void)luaL_error(L, "Bad argument #%d, expected %s got %s",
+ arg, LUA_STORAGE_MAIL_USER,
+ lua_typename(L, lua_type(L, arg)));
+ }
+ lua_pushliteral(L, "item");
+ lua_rawget(L, arg);
+ struct mail_user **bp = lua_touserdata(L, -1);
+ lua_pop(L, 1);
+ return *bp;
+}
+
+static int lua_storage_mail_user_tostring(lua_State *L)
+{
+ DLUA_REQUIRE_ARGS(L, 1);
+ struct mail_user *user = lua_check_storage_mail_user(L, 1);
+
+ lua_pushstring(L, user->username);
+ return 1;
+}
+
+int lua_storage_cmp(lua_State *L)
+{
+ const char *name_a, *name_b;
+ name_a = lua_tostring(L, 1);
+ name_b = lua_tostring(L, 2);
+
+ return strcmp(name_a, name_b);
+}
+
+static int lua_storage_mail_user_eq(lua_State *L)
+{
+ DLUA_REQUIRE_ARGS(L, 2);
+ bool res = lua_storage_cmp(L) == 0;
+ lua_pushboolean(L, res);
+ return 1;
+}
+
+static int lua_storage_mail_user_lt(lua_State *L)
+{
+ DLUA_REQUIRE_ARGS(L, 2);
+ bool res = lua_storage_cmp(L) <= 0;
+ lua_pushboolean(L, res);
+ return 1;
+}
+
+static int lua_storage_mail_user_le(lua_State *L)
+{
+ DLUA_REQUIRE_ARGS(L, 2);
+ bool res = lua_storage_cmp(L) < 0;
+ lua_pushboolean(L, res);
+ return 1;
+}
+
+static int lua_storage_mail_user_var_expand(lua_State *L)
+{
+ DLUA_REQUIRE_ARGS(L, 2);
+ struct mail_user *user = lua_check_storage_mail_user(L, 1);
+ const char *error;
+ const char *format = luaL_checkstring(L, 2);
+ const struct var_expand_table *table = mail_user_var_expand_table(user);
+ string_t *str = t_str_new(128);
+ if (var_expand_with_funcs(str, format, table, mail_user_var_expand_func_table,
+ user, &error) < 0) {
+ return luaL_error(L, "var_expand(%s) failed: %s",
+ format, error);
+ }
+ lua_pushlstring(L, str->data, str->used);
+ return 1;
+}
+
+static int lua_storage_mail_user_plugin_getenv(lua_State *L)
+{
+ DLUA_REQUIRE_ARGS(L, 2);
+ struct mail_user *user = lua_check_storage_mail_user(L, 1);
+ const char *set = lua_tostring(L, 2);
+ const char *val = mail_user_plugin_getenv(user, set);
+ lua_pushstring(L, val);
+ return 1;
+}
+
+static int lua_storage_mail_user_mailbox_alloc(lua_State *L)
+{
+ DLUA_REQUIRE_ARGS_IN(L, 2, 3);
+ struct mail_user *user = lua_check_storage_mail_user(L, 1);
+ const char *mboxname = luaL_checkstring(L, 2);
+ enum mailbox_flags flags = 0;
+ if (lua_gettop(L) >= 3)
+ flags = luaL_checkinteger(L, 3);
+ struct mail_namespace *ns = mail_namespace_find(user->namespaces, mboxname);
+ if (ns == NULL) {
+ return luaL_error(L, "No namespace found for mailbox %s",
+ mboxname);
+ }
+ struct mailbox *mbox = mailbox_alloc(ns->list, mboxname, flags);
+ dlua_push_mailbox(L, mbox);
+ return 1;
+}
+
+static int lua_storage_mail_user_unref(lua_State *L)
+{
+ struct mail_user **ptr = lua_touserdata(L, 1);
+ if (*ptr != NULL)
+ mail_user_unref(ptr);
+ *ptr = NULL;
+ return 0;
+}
+
+static const char *lua_storage_mail_user_metadata_key(const char *key)
+{
+ if (str_begins(key, "/private/")) {
+ return t_strdup_printf("/private/%s%s",
+ MAILBOX_ATTRIBUTE_PREFIX_DOVECOT_PVT_SERVER,
+ key + 9);
+ } else if (str_begins(key, "/shared/")) {
+ return t_strdup_printf("/shared/%s%s",
+ MAILBOX_ATTRIBUTE_PREFIX_DOVECOT_PVT_SERVER,
+ key + 8);
+ }
+ return NULL;
+}
+
+static int lua_storage_mail_user_metadata_get(lua_State *L)
+{
+ if (lua_gettop(L) < 2)
+ return luaL_error(L, "expecting at least 1 parameter");
+ struct mail_user *user = lua_check_storage_mail_user(L, 1);
+
+ const char *value, *error;
+ size_t value_len;
+ int ret, i, top = lua_gettop(L);
+
+ /* fetch INBOX, as user metadata is stored there */
+ struct mail_namespace *ns = mail_namespace_find_inbox(user->namespaces);
+ struct mailbox *mbox = mailbox_alloc(ns->list, "INBOX",
+ MAILBOX_FLAG_READONLY);
+
+ if (mailbox_open(mbox) < 0) {
+ error = mailbox_get_last_error(mbox, NULL);
+ mailbox_free(&mbox);
+ return luaL_error(L, "Cannot open INBOX: %s", error);
+ }
+
+ ret = 0;
+ for(i = 2; i <= top; i++) {
+ /* reformat key */
+ const char *key = lua_tostring(L, i);
+
+ if (key == NULL) {
+ ret = -1;
+ error = t_strdup_printf("expected string at #%d", i);
+ break;
+ }
+
+ if ((key = lua_storage_mail_user_metadata_key(key)) == NULL) {
+ ret = -1;
+ error = "Invalid key prefix, must be "
+ "/private/ or /shared/";
+ break;
+ }
+
+ if ((ret = lua_storage_mailbox_attribute_get(mbox, key, &value,
+ &value_len, &error)) < 0) {
+ break;
+ } else if (ret == 0) {
+ lua_pushnil(L);
+ } else {
+ lua_pushlstring(L, value, value_len);
+ }
+ }
+
+ mailbox_free(&mbox);
+
+ if (ret < 0)
+ return luaL_error(L, "%s", error);
+
+ i_assert(i>=2);
+ return i-2;
+}
+
+static int
+lua_storage_mail_user_set_metadata_unset(lua_State *L, struct mail_user *user,
+ const char *key, const char *value,
+ size_t value_len)
+{
+ const char *error;
+
+ /* reformat key */
+ if ((key = lua_storage_mail_user_metadata_key(key)) == NULL) {
+ return luaL_error(L, "Invalid key prefix, must be "
+ "/private/ or /shared/");
+ }
+
+ /* fetch INBOX, as user metadata is stored there */
+ struct mail_namespace *ns = mail_namespace_find_inbox(user->namespaces);
+ struct mailbox *mbox = mailbox_alloc(ns->list, "INBOX", 0);
+
+ if (mailbox_open(mbox) < 0) {
+ error = mailbox_get_last_error(mbox, NULL);
+ mailbox_free(&mbox);
+ return luaL_error(L, "Cannot open INBOX: %s", error);
+ }
+
+ if (lua_storage_mailbox_attribute_set(mbox, key, value, value_len,
+ &error) < 0) {
+ mailbox_free(&mbox);
+ return luaL_error(L, "Cannot get attribute: %s", error);
+ }
+
+ mailbox_free(&mbox);
+ return 0;
+}
+
+static int lua_storage_mail_user_metadata_set(lua_State *L)
+{
+ DLUA_REQUIRE_ARGS(L, 3);
+ struct mail_user *user = lua_check_storage_mail_user(L, 1);
+ const char *key = luaL_checkstring(L, 2);
+ const char *value;
+ size_t value_len;
+
+ value = lua_tolstring(L, 3, &value_len);
+
+ return lua_storage_mail_user_set_metadata_unset(L, user, key,
+ value, value_len);
+}
+
+static int lua_storage_mail_user_metadata_unset(lua_State *L)
+{
+ DLUA_REQUIRE_ARGS(L, 2);
+ struct mail_user *user = lua_check_storage_mail_user(L, 1);
+ const char *key = luaL_checkstring(L, 2);
+
+ return lua_storage_mail_user_set_metadata_unset(L, user, key, NULL, 0);
+}
+
+static int lua_storage_mail_user_metadata_list(lua_State *L)
+{
+ if (lua_gettop(L) < 2)
+ return luaL_error(L, "expecting at least 1 parameter");
+ struct mail_user *user = lua_check_storage_mail_user(L, 1);
+ const struct lua_storage_keyvalue *item;
+ const char *error;
+ ARRAY_TYPE(lua_storage_keyvalue) items;
+ int i, ret;
+
+ /* fetch INBOX, as user metadata is stored there */
+ struct mail_namespace *ns = mail_namespace_find_inbox(user->namespaces);
+ struct mailbox *mbox = mailbox_alloc(ns->list, "INBOX", 0);
+
+ if (mailbox_open(mbox) < 0) {
+ error = mailbox_get_last_error(mbox, NULL);
+ mailbox_free(&mbox);
+ return luaL_error(L, "Cannot open INBOX: %s", error);
+ }
+
+ T_BEGIN {
+ t_array_init(&items, 1);
+
+ ret = 0;
+ for(i = 2; i <= lua_gettop(L); i++) {
+ const char *key = lua_tostring(L, i);
+
+ if (key == NULL) {
+ ret = -1;
+ error = t_strdup_printf("expected string at #%d", i);
+ break;
+ }
+
+ if ((key = lua_storage_mail_user_metadata_key(key)) == NULL) {
+ ret = -1;
+ error = "Invalid key prefix, must be "
+ "/private/ or /shared/";
+ break;
+ }
+
+ if (lua_storage_mailbox_attribute_list(mbox, key, &items,
+ &error) < 0) {
+ ret = -1;
+ break;
+ }
+ }
+
+ if (ret == 0) {
+ lua_createtable(L, 0, array_count(&items));
+ array_foreach(&items, item) {
+ char *ptr;
+ char *key = t_strdup_noconst(item->key);
+ if ((ptr = strstr(key, MAILBOX_ATTRIBUTE_PREFIX_DOVECOT_PVT_SERVER)) != NULL) {
+ const char *endp = ptr+strlen(MAILBOX_ATTRIBUTE_PREFIX_DOVECOT_PVT_SERVER);
+ memmove(ptr, endp, strlen(endp));
+ memset(ptr+strlen(endp), '\0', 1);
+ }
+ /* push value */
+ lua_pushlstring(L, item->value,
+ item->value_len);
+ /* set field */
+ lua_setfield(L, -2, key);
+ }
+ }
+ } T_END;
+
+ mailbox_free(&mbox);
+
+ if (ret == -1)
+ return luaL_error(L, "%s", error);
+
+ /* stack should have table with items */
+ return 1;
+}
+
+static luaL_Reg lua_storage_mail_user_methods[] = {
+ { "__tostring", lua_storage_mail_user_tostring },
+ { "__eq", lua_storage_mail_user_eq },
+ { "__lt", lua_storage_mail_user_lt },
+ { "__le", lua_storage_mail_user_le },
+ { "plugin_getenv", lua_storage_mail_user_plugin_getenv },
+ { "var_expand", lua_storage_mail_user_var_expand },
+ { "mailbox", lua_storage_mail_user_mailbox_alloc },
+ { "metadata_get", lua_storage_mail_user_metadata_get },
+ { "metadata_set", lua_storage_mail_user_metadata_set },
+ { "metadata_unset", lua_storage_mail_user_metadata_unset },
+ { "metadata_list", lua_storage_mail_user_metadata_list },
+ { NULL, NULL }
+};
+
+void lua_storage_mail_user_register(struct dlua_script *script)
+{
+ luaL_newmetatable(script->L, LUA_STORAGE_MAIL_USER);
+ lua_pushvalue(script->L, -1);
+ lua_setfield(script->L, -2, "__index");
+ luaL_setfuncs(script->L, lua_storage_mail_user_methods, 0);
+ lua_pop(script->L, 1);
+}
diff --git a/src/lib-storage/mail-user.c b/src/lib-storage/mail-user.c
new file mode 100644
index 0000000..9cf6042
--- /dev/null
+++ b/src/lib-storage/mail-user.c
@@ -0,0 +1,848 @@
+/* Copyright (c) 2008-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "hostpid.h"
+#include "ioloop.h"
+#include "net.h"
+#include "module-dir.h"
+#include "home-expand.h"
+#include "file-create-locked.h"
+#include "mkdir-parents.h"
+#include "safe-mkstemp.h"
+#include "str.h"
+#include "strescape.h"
+#include "var-expand.h"
+#include "settings-parser.h"
+#include "iostream-ssl.h"
+#include "fs-api.h"
+#include "auth-master.h"
+#include "master-service.h"
+#include "master-service-ssl-settings.h"
+#include "dict.h"
+#include "mail-storage-settings.h"
+#include "mail-storage-private.h"
+#include "mail-storage-service.h"
+#include "mail-namespace.h"
+#include "mail-storage.h"
+#include "mailbox-list-private.h"
+#include "mail-autoexpunge.h"
+#include "mail-user.h"
+
+
+struct mail_user_module_register mail_user_module_register = { 0 };
+struct auth_master_connection *mail_user_auth_master_conn;
+
+static void mail_user_deinit_base(struct mail_user *user)
+{
+ if (user->_attr_dict != NULL) {
+ dict_wait(user->_attr_dict);
+ dict_deinit(&user->_attr_dict);
+ }
+ mail_namespaces_deinit(&user->namespaces);
+ if (user->_service_user != NULL)
+ mail_storage_service_user_unref(&user->_service_user);
+}
+
+static void mail_user_deinit_pre_base(struct mail_user *user ATTR_UNUSED)
+{
+}
+
+static void mail_user_stats_fill_base(struct mail_user *user ATTR_UNUSED,
+ struct stats *stats ATTR_UNUSED)
+{
+}
+
+static struct mail_user *
+mail_user_alloc_int(struct event *parent_event,
+ const char *username,
+ const struct setting_parser_info *set_info,
+ const struct mail_user_settings *set, pool_t pool)
+{
+ struct mail_user *user;
+ const char *error;
+
+ i_assert(username != NULL);
+ i_assert(*username != '\0');
+
+ user = p_new(pool, struct mail_user, 1);
+ user->pool = pool;
+ user->refcount = 1;
+ user->username = p_strdup(pool, username);
+ user->set_info = set_info;
+ user->unexpanded_set = set;
+ user->set = settings_dup_with_pointers(set_info, user->unexpanded_set, pool);
+ user->service = master_service_get_name(master_service);
+ user->default_normalizer = uni_utf8_to_decomposed_titlecase;
+ user->session_create_time = ioloop_time;
+ user->event = event_create(parent_event);
+ event_add_category(user->event, &event_category_storage);
+ event_add_str(user->event, "user", username);
+
+ /* check settings so that the duplicated structure will again
+ contain the parsed fields */
+ if (!settings_check(set_info, pool, user->set, &error))
+ i_panic("Settings check unexpectedly failed: %s", error);
+
+ user->v.deinit = mail_user_deinit_base;
+ user->v.deinit_pre = mail_user_deinit_pre_base;
+ user->v.stats_fill = mail_user_stats_fill_base;
+ p_array_init(&user->module_contexts, user->pool, 5);
+ return user;
+}
+
+struct mail_user *
+mail_user_alloc_nodup_set(struct event *parent_event,
+ const char *username,
+ const struct setting_parser_info *set_info,
+ const struct mail_user_settings *set)
+{
+ pool_t pool;
+
+ pool = pool_alloconly_create(MEMPOOL_GROWING"mail user", 16*1024);
+ return mail_user_alloc_int(parent_event, username, set_info, set, pool);
+}
+
+struct mail_user *mail_user_alloc(struct event *parent_event,
+ const char *username,
+ const struct setting_parser_info *set_info,
+ const struct mail_user_settings *set)
+{
+ pool_t pool;
+
+ pool = pool_alloconly_create(MEMPOOL_GROWING"mail user", 16*1024);
+ return mail_user_alloc_int(parent_event, username, set_info,
+ settings_dup(set_info, set, pool), pool);
+}
+
+static void
+mail_user_expand_plugins_envs(struct mail_user *user)
+{
+ const char **envs, *home, *error;
+ string_t *str;
+ unsigned int i, count;
+
+ if (!array_is_created(&user->set->plugin_envs))
+ return;
+
+ str = t_str_new(256);
+ envs = array_get_modifiable(&user->set->plugin_envs, &count);
+ i_assert((count % 2) == 0);
+ for (i = 0; i < count; i += 2) {
+ if (user->_home == NULL &&
+ var_has_key(envs[i+1], 'h', "home") &&
+ mail_user_get_home(user, &home) <= 0) {
+ user->error = p_strdup_printf(user->pool,
+ "userdb didn't return a home directory, "
+ "but plugin setting %s used it (%%h): %s",
+ envs[i], envs[i+1]);
+ return;
+ }
+ str_truncate(str, 0);
+ if (var_expand_with_funcs(str, envs[i+1],
+ mail_user_var_expand_table(user),
+ mail_user_var_expand_func_table, user,
+ &error) <= 0) {
+ user->error = p_strdup_printf(user->pool,
+ "Failed to expand plugin setting %s = '%s': %s",
+ envs[i], envs[i+1], error);
+ return;
+ }
+ envs[i+1] = p_strdup(user->pool, str_c(str));
+ }
+}
+
+int mail_user_init(struct mail_user *user, const char **error_r)
+{
+ const struct mail_storage_settings *mail_set;
+ const char *home, *key, *value, *error;
+ bool need_home_dir;
+
+ need_home_dir = user->_home == NULL &&
+ settings_vars_have_key(user->set_info, user->set,
+ 'h', "home", &key, &value);
+ if (need_home_dir && mail_user_get_home(user, &home) <= 0) {
+ user->error = p_strdup_printf(user->pool,
+ "userdb didn't return a home directory, "
+ "but %s used it (%%h): %s", key, value);
+ }
+
+ /* expand settings after we can expand %h */
+ if (settings_var_expand_with_funcs(user->set_info, user->set,
+ user->pool, mail_user_var_expand_table(user),
+ mail_user_var_expand_func_table, user,
+ &error) <= 0) {
+ user->error = p_strdup_printf(user->pool,
+ "Failed to expand settings: %s", error);
+ }
+ user->settings_expanded = TRUE;
+ mail_user_expand_plugins_envs(user);
+
+ /* autocreated users for shared mailboxes need to be fully initialized
+ if they don't exist, since they're going to be used anyway */
+ if (user->error == NULL || user->nonexistent) {
+ mail_set = mail_user_set_get_storage_set(user);
+ user->mail_debug = mail_set->mail_debug;
+
+ user->initialized = TRUE;
+ hook_mail_user_created(user);
+ }
+
+ if (user->error != NULL) {
+ *error_r = t_strdup(user->error);
+ return -1;
+ }
+ process_stat_read_start(&user->proc_stat, user->event);
+ return 0;
+}
+
+void mail_user_ref(struct mail_user *user)
+{
+ i_assert(user->refcount > 0);
+
+ user->refcount++;
+}
+
+void mail_user_unref(struct mail_user **_user)
+{
+ struct mail_user *user = *_user;
+
+ i_assert(user->refcount > 0);
+
+ *_user = NULL;
+ if (user->refcount > 1) {
+ user->refcount--;
+ return;
+ }
+
+ user->deinitializing = TRUE;
+
+ /* call deinit() and deinit_pre() with refcount=1, otherwise we may
+ assert-crash in mail_user_ref() that is called by some handlers. */
+ T_BEGIN {
+ user->v.deinit_pre(user);
+ user->v.deinit(user);
+ } T_END;
+ event_unref(&user->event);
+ i_assert(user->refcount == 1);
+ pool_unref(&user->pool);
+}
+
+static void mail_user_session_finished(struct mail_user *user)
+{
+ struct event *ev = user->event;
+ struct process_stat *stat = &user->proc_stat;
+
+ process_stat_read_finish(stat, ev);
+
+ struct event_passthrough *e = event_create_passthrough(ev)->
+ set_name("mail_user_session_finished")->
+ add_int_nonzero("utime", stat->utime)->
+ add_int_nonzero("stime", stat->stime)->
+ add_int_nonzero("minor_faults", stat->minor_faults)->
+ add_int_nonzero("major_faults", stat->major_faults)->
+ add_int_nonzero("vol_cs", stat->vol_cs)->
+ add_int_nonzero("invol_cs", stat->invol_cs)->
+ add_int_nonzero("rss", stat->rss)->
+ add_int_nonzero("vsz", stat->vsz)->
+ add_int_nonzero("rchar", stat->rchar)->
+ add_int_nonzero("wchar", stat->wchar)->
+ add_int_nonzero("syscr", stat->syscr)->
+ add_int_nonzero("syscw", stat->syscw);
+ e_debug(e->event(), "User session is finished");
+}
+
+void mail_user_deinit(struct mail_user **user)
+{
+ mail_user_session_finished(*user);
+ i_assert((*user)->refcount == 1);
+ mail_user_unref(user);
+}
+
+struct mail_user *mail_user_find(struct mail_user *user, const char *name)
+{
+ struct mail_namespace *ns;
+
+ for (ns = user->namespaces; ns != NULL; ns = ns->next) {
+ if (ns->owner != NULL && strcmp(ns->owner->username, name) == 0)
+ return ns->owner;
+ }
+ return NULL;
+}
+
+static void
+mail_user_connection_init_from(struct mail_user_connection_data *conn,
+ pool_t pool, const struct mail_user_connection_data *src)
+{
+ *conn = *src;
+
+ if (src->local_ip != NULL && src->local_ip->family != 0) {
+ conn->local_ip = p_new(pool, struct ip_addr, 1);
+ *conn->local_ip = *src->local_ip;
+ }
+ if (src->remote_ip != NULL && src->remote_ip->family != 0) {
+ conn->remote_ip = p_new(pool, struct ip_addr, 1);
+ *conn->remote_ip = *src->remote_ip;
+ }
+}
+
+void mail_user_set_vars(struct mail_user *user, const char *service,
+ const struct mail_user_connection_data *conn)
+{
+ i_assert(service != NULL);
+
+ user->service = p_strdup(user->pool, service);
+ mail_user_connection_init_from(&user->conn, user->pool, conn);
+}
+
+const struct var_expand_table *
+mail_user_var_expand_table(struct mail_user *user)
+{
+ /* use a cached table, unless home directory has been set afterwards */
+ if (user->var_expand_table != NULL &&
+ user->var_expand_table[4].value == user->_home)
+ return user->var_expand_table;
+
+ const char *username =
+ p_strdup(user->pool, t_strcut(user->username, '@'));
+ const char *domain = i_strchr_to_next(user->username, '@');
+ const char *local_ip = user->conn.local_ip == NULL ? NULL :
+ p_strdup(user->pool, net_ip2addr(user->conn.local_ip));
+ const char *remote_ip = user->conn.remote_ip == NULL ? NULL :
+ p_strdup(user->pool, net_ip2addr(user->conn.remote_ip));
+
+ const char *auth_user, *auth_username, *auth_domain;
+ if (user->auth_user == NULL) {
+ auth_user = user->username;
+ auth_username = username;
+ auth_domain = domain;
+ } else {
+ auth_user = user->auth_user;
+ auth_username =
+ p_strdup(user->pool, t_strcut(user->auth_user, '@'));
+ auth_domain = i_strchr_to_next(user->auth_user, '@');
+ }
+
+ const struct var_expand_table stack_tab[] = {
+ { 'u', user->username, "user" },
+ { 'n', username, "username" },
+ { 'd', domain, "domain" },
+ { 's', user->service, "service" },
+ { 'h', user->_home /* don't look it up unless we need it */, "home" },
+ { 'l', local_ip, "lip" },
+ { 'r', remote_ip, "rip" },
+ { 'p', my_pid, "pid" },
+ { 'i', p_strdup(user->pool, dec2str(user->uid)), "uid" },
+ { '\0', p_strdup(user->pool, dec2str(user->gid)), "gid" },
+ { '\0', user->session_id, "session" },
+ { '\0', auth_user, "auth_user" },
+ { '\0', auth_username, "auth_username" },
+ { '\0', auth_domain, "auth_domain" },
+ { '\0', user->set->hostname, "hostname" },
+ /* aliases: */
+ { '\0', local_ip, "local_ip" },
+ { '\0', remote_ip, "remote_ip" },
+ /* NOTE: keep this synced with imap-hibernate's
+ imap_client_var_expand_table() */
+ { '\0', NULL, NULL }
+ };
+ struct var_expand_table *tab;
+
+ tab = p_malloc(user->pool, sizeof(stack_tab));
+ memcpy(tab, stack_tab, sizeof(stack_tab));
+
+ user->var_expand_table = tab;
+ return user->var_expand_table;
+}
+
+static int
+mail_user_var_expand_func_userdb(const char *data, void *context,
+ const char **value_r,
+ const char **error_r ATTR_UNUSED)
+{
+ struct mail_user *user = context;
+
+ *value_r = mail_storage_service_fields_var_expand(data, user->userdb_fields);
+ return 1;
+}
+
+void mail_user_set_home(struct mail_user *user, const char *home)
+{
+ user->_home = p_strdup(user->pool, home);
+ user->home_looked_up = TRUE;
+}
+
+void mail_user_add_namespace(struct mail_user *user,
+ struct mail_namespace **namespaces)
+{
+ struct mail_namespace **tmp, *next, *ns = *namespaces;
+
+ for (; ns != NULL; ns = next) {
+ next = ns->next;
+
+ tmp = &user->namespaces;
+ for (; *tmp != NULL; tmp = &(*tmp)->next) {
+ i_assert(*tmp != ns);
+ if (strlen(ns->prefix) < strlen((*tmp)->prefix))
+ break;
+ }
+ ns->next = *tmp;
+ *tmp = ns;
+ }
+ *namespaces = user->namespaces;
+
+ T_BEGIN {
+ hook_mail_namespaces_added(user->namespaces);
+ } T_END;
+}
+
+void mail_user_drop_useless_namespaces(struct mail_user *user)
+{
+ struct mail_namespace *ns, *next;
+
+ /* drop all autocreated unusable (typically shared) namespaces.
+ don't drop the autocreated prefix="" namespace that we explicitly
+ created for being the fallback namespace. */
+ for (ns = user->namespaces; ns != NULL; ns = next) {
+ next = ns->next;
+
+ if (mail_namespace_is_removable(ns) && ns->prefix_len > 0)
+ mail_namespace_destroy(ns);
+ }
+}
+
+const char *mail_user_home_expand(struct mail_user *user, const char *path)
+{
+ (void)mail_user_try_home_expand(user, &path);
+ return path;
+}
+
+static int mail_user_userdb_lookup_home(struct mail_user *user)
+{
+ struct auth_user_info info;
+ struct auth_user_reply reply;
+ pool_t userdb_pool;
+ const char *username, *const *fields;
+ int ret;
+
+ i_assert(!user->home_looked_up);
+
+ i_zero(&info);
+ info.service = user->service;
+ if (user->conn.local_ip != NULL)
+ info.local_ip = *user->conn.local_ip;
+ if (user->conn.remote_ip != NULL)
+ info.remote_ip = *user->conn.remote_ip;
+
+ userdb_pool = pool_alloconly_create("userdb lookup", 2048);
+ ret = auth_master_user_lookup(mail_user_auth_master_conn,
+ user->username, &info, userdb_pool,
+ &username, &fields);
+ if (ret > 0) {
+ auth_user_fields_parse(fields, userdb_pool, &reply);
+ user->_home = p_strdup(user->pool, reply.home);
+ }
+ pool_unref(&userdb_pool);
+ return ret;
+}
+
+static bool mail_user_get_mail_home(struct mail_user *user)
+{
+ const char *error, *home = user->set->mail_home;
+ string_t *str;
+
+ if (user->settings_expanded) {
+ user->_home = home[0] != '\0' ? home : NULL;
+ return TRUE;
+ }
+ /* we're still initializing user. need to do the expansion ourself. */
+ i_assert(home[0] == SETTING_STRVAR_UNEXPANDED[0]);
+ home++;
+ if (home[0] == '\0')
+ return TRUE;
+
+ str = t_str_new(128);
+ if (var_expand_with_funcs(str, home,
+ mail_user_var_expand_table(user),
+ mail_user_var_expand_func_table, user,
+ &error) <= 0) {
+ e_error(user->event, "Failed to expand mail_home=%s: %s",
+ home, error);
+ return FALSE;
+ }
+ user->_home = p_strdup(user->pool, str_c(str));
+ return TRUE;
+}
+
+int mail_user_get_home(struct mail_user *user, const char **home_r)
+{
+ int ret;
+
+ if (user->home_looked_up) {
+ *home_r = user->_home;
+ return user->_home != NULL ? 1 : 0;
+ }
+
+ if (mail_user_auth_master_conn == NULL) {
+ /* no userdb connection. we can only use mail_home setting. */
+ if (!mail_user_get_mail_home(user))
+ return -1;
+ } else if ((ret = mail_user_userdb_lookup_home(user)) < 0) {
+ /* userdb lookup failed */
+ return -1;
+ } else if (ret == 0) {
+ /* user doesn't exist */
+ user->nonexistent = TRUE;
+ } else if (user->_home == NULL) {
+ /* no home returned by userdb lookup, fallback to
+ mail_home setting. */
+ if (!mail_user_get_mail_home(user))
+ return -1;
+ }
+ user->home_looked_up = TRUE;
+
+ *home_r = user->_home;
+ return user->_home != NULL ? 1 : 0;
+}
+
+bool mail_user_is_plugin_loaded(struct mail_user *user, struct module *module)
+{
+ const char *const *plugins;
+ bool ret;
+
+ T_BEGIN {
+ plugins = t_strsplit_spaces(user->set->mail_plugins, ", ");
+ ret = str_array_find(plugins, module_get_plugin_name(module));
+ } T_END;
+ return ret;
+}
+
+bool mail_user_plugin_getenv_bool(struct mail_user *user, const char *name)
+{
+ return mail_user_set_plugin_getenv_bool(user->set, name);
+}
+
+bool mail_user_set_plugin_getenv_bool(const struct mail_user_settings *set,
+ const char *name)
+{
+ const char *env = mail_user_set_plugin_getenv(set, name);
+
+ if (env == NULL)
+ return FALSE;
+ switch (env[0]) {
+ case 'n':
+ case 'N':
+ case '0':
+ case 'f':
+ case 'F':
+ return FALSE;
+ }
+
+ //any other value including empty string will be treated as TRUE.
+ return TRUE;
+}
+
+const char *mail_user_plugin_getenv(struct mail_user *user, const char *name)
+{
+ return mail_user_set_plugin_getenv(user->set, name);
+}
+
+const char *mail_user_set_plugin_getenv(const struct mail_user_settings *set,
+ const char *name)
+{
+ const char *const *envs;
+ unsigned int i, count;
+
+ if (!array_is_created(&set->plugin_envs))
+ return NULL;
+
+ envs = array_get(&set->plugin_envs, &count);
+ for (i = 0; i < count; i += 2) {
+ if (strcmp(envs[i], name) == 0)
+ return envs[i+1];
+ }
+ return NULL;
+}
+
+int mail_user_try_home_expand(struct mail_user *user, const char **pathp)
+{
+ const char *home, *path = *pathp;
+
+ if (*path != '~') {
+ /* no need to expand home */
+ return 0;
+ }
+
+ if (mail_user_get_home(user, &home) <= 0)
+ return -1;
+
+ path = home_expand_tilde(path, home);
+ if (path == NULL)
+ return -1;
+
+ *pathp = path;
+ return 0;
+}
+
+void mail_user_set_get_temp_prefix(string_t *dest,
+ const struct mail_user_settings *set)
+{
+ str_append(dest, set->mail_temp_dir);
+ str_append(dest, "/dovecot.");
+ str_append(dest, master_service_get_name(master_service));
+ str_append_c(dest, '.');
+}
+
+const char *mail_user_get_volatile_dir(struct mail_user *user)
+{
+ struct mailbox_list *inbox_list =
+ mail_namespace_find_inbox(user->namespaces)->list;
+
+ return inbox_list->set.volatile_dir;
+}
+
+int mail_user_lock_file_create(struct mail_user *user, const char *lock_fname,
+ unsigned int lock_secs,
+ struct file_lock **lock_r, const char **error_r)
+{
+ const char *home, *path;
+ int ret;
+
+ if ((ret = mail_user_get_home(user, &home)) < 0) {
+ /* home lookup failed - shouldn't really happen */
+ *error_r = "Failed to lookup home directory";
+ errno = EINVAL;
+ return -1;
+ }
+ if (ret == 0) {
+ *error_r = "User has no home directory";
+ errno = EINVAL;
+ return -1;
+ }
+
+ const struct mail_storage_settings *mail_set =
+ mail_user_set_get_storage_set(user);
+ struct file_create_settings lock_set = {
+ .lock_timeout_secs = lock_secs,
+ .lock_settings = {
+ .lock_method = mail_set->parsed_lock_method,
+ },
+ };
+ struct mailbox_list *inbox_list =
+ mail_namespace_find_inbox(user->namespaces)->list;
+ if (inbox_list->set.volatile_dir == NULL)
+ path = t_strdup_printf("%s/%s", home, lock_fname);
+ else {
+ path = t_strdup_printf("%s/%s", inbox_list->set.volatile_dir,
+ lock_fname);
+ lock_set.mkdir_mode = 0700;
+ }
+ return mail_storage_lock_create(path, &lock_set, mail_set, lock_r, error_r);
+}
+
+const char *mail_user_get_anvil_userip_ident(struct mail_user *user)
+{
+ if (user->conn.remote_ip == NULL)
+ return NULL;
+ return t_strconcat(net_ip2addr(user->conn.remote_ip), "/",
+ str_tabescape(user->username), NULL);
+}
+
+static void
+mail_user_try_load_class_plugin(struct mail_user *user, const char *name)
+{
+ struct module_dir_load_settings mod_set;
+ struct module *module;
+ size_t name_len = strlen(name);
+
+ i_zero(&mod_set);
+ mod_set.abi_version = DOVECOT_ABI_VERSION;
+ mod_set.binary_name = master_service_get_name(master_service);
+ mod_set.setting_name = "<built-in storage lookup>";
+ mod_set.require_init_funcs = TRUE;
+ mod_set.debug = user->mail_debug;
+
+ mail_storage_service_modules =
+ module_dir_load_missing(mail_storage_service_modules,
+ user->set->mail_plugin_dir,
+ name, &mod_set);
+ /* initialize the module (and only this module!) immediately so that
+ the class gets registered */
+ for (module = mail_storage_service_modules; module != NULL; module = module->next) {
+ if (strncmp(module->name, name, name_len) == 0 &&
+ strcmp(module->name + name_len, "_plugin") == 0) {
+ if (!module->initialized) {
+ module->initialized = TRUE;
+ module->init(module);
+ }
+ break;
+ }
+ }
+}
+
+struct mail_storage *
+mail_user_get_storage_class(struct mail_user *user, const char *name)
+{
+ struct mail_storage *storage;
+
+ storage = mail_storage_find_class(name);
+ if (storage == NULL || storage->v.alloc != NULL)
+ return storage;
+
+ /* it's implemented by a plugin. load it and check again. */
+ mail_user_try_load_class_plugin(user, name);
+
+ storage = mail_storage_find_class(name);
+ if (storage != NULL && storage->v.alloc == NULL) {
+ e_error(user->event, "Storage driver '%s' exists as a stub, "
+ "but its plugin couldn't be loaded", name);
+ return NULL;
+ }
+ return storage;
+}
+
+struct mail_user *mail_user_dup(struct mail_user *user)
+{
+ struct mail_user *user2;
+
+ user2 = mail_user_alloc(event_get_parent(user->event), user->username,
+ user->set_info, user->unexpanded_set);
+ if (user2->_service_user != NULL) {
+ user2->_service_user = user->_service_user;
+ mail_storage_service_user_ref(user2->_service_user);
+ }
+ if (user->_home != NULL)
+ mail_user_set_home(user2, user->_home);
+ mail_user_set_vars(user2, user->service, &user->conn);
+ user2->uid = user->uid;
+ user2->gid = user->gid;
+ user2->anonymous = user->anonymous;
+ user2->admin = user->admin;
+ user2->auth_mech = p_strdup(user2->pool, user->auth_mech);
+ user2->auth_token = p_strdup(user2->pool, user->auth_token);
+ user2->auth_user = p_strdup(user2->pool, user->auth_user);
+ user2->session_id = p_strdup(user2->pool, user->session_id);
+ user2->session_create_time = user->session_create_time;
+ user2->userdb_fields = user->userdb_fields == NULL ? NULL :
+ p_strarray_dup(user2->pool, user->userdb_fields);
+ return user2;
+}
+
+void mail_user_init_ssl_client_settings(struct mail_user *user,
+ struct ssl_iostream_settings *ssl_set_r)
+{
+ if (user->_service_user == NULL) {
+ /* Internal test user that should never actually need any
+ SSL settings. */
+ i_zero(ssl_set_r);
+ return;
+ }
+
+ const struct master_service_ssl_settings *ssl_set =
+ mail_storage_service_user_get_ssl_settings(user->_service_user);
+
+ master_service_ssl_client_settings_to_iostream_set(ssl_set,
+ pool_datastack_create(), ssl_set_r);
+}
+
+void mail_user_init_fs_settings(struct mail_user *user,
+ struct fs_settings *fs_set,
+ struct ssl_iostream_settings *ssl_set_r)
+{
+ fs_set->event_parent = user->event;
+ fs_set->username = user->username;
+ fs_set->session_id = user->session_id;
+ fs_set->base_dir = user->set->base_dir;
+ fs_set->temp_dir = user->set->mail_temp_dir;
+ fs_set->debug = user->mail_debug;
+ fs_set->enable_timing = user->stats_enabled;
+
+ fs_set->ssl_client_set = ssl_set_r;
+ mail_user_init_ssl_client_settings(user, ssl_set_r);
+}
+
+void mail_user_stats_fill(struct mail_user *user, struct stats *stats)
+{
+ user->v.stats_fill(user, stats);
+}
+
+static int
+mail_user_home_mkdir_try_ns(struct mail_namespace *ns, const char *home)
+{
+ const enum mailbox_list_path_type types[] = {
+ MAILBOX_LIST_PATH_TYPE_DIR,
+ MAILBOX_LIST_PATH_TYPE_ALT_DIR,
+ MAILBOX_LIST_PATH_TYPE_CONTROL,
+ MAILBOX_LIST_PATH_TYPE_INDEX,
+ MAILBOX_LIST_PATH_TYPE_INDEX_PRIVATE,
+ MAILBOX_LIST_PATH_TYPE_INDEX_CACHE,
+ MAILBOX_LIST_PATH_TYPE_LIST_INDEX,
+ };
+ size_t home_len = strlen(home);
+ const char *path;
+
+ for (unsigned int i = 0; i < N_ELEMENTS(types); i++) {
+ if (!mailbox_list_get_root_path(ns->list, types[i], &path))
+ continue;
+ if (strncmp(path, home, home_len) == 0 &&
+ (path[home_len] == '\0' || path[home_len] == '/')) {
+ return mailbox_list_mkdir_root(ns->list, path,
+ types[i]) < 0 ? -1 : 1;
+ }
+ }
+ return 0;
+}
+
+int mail_user_home_mkdir(struct mail_user *user)
+{
+ struct mail_namespace *ns;
+ const char *home;
+ int ret;
+
+ if ((ret = mail_user_get_home(user, &home)) <= 0) {
+ /* If user has no home directory, just return success. */
+ return ret;
+ }
+
+ /* Try to create the home directory by creating the root directory for
+ a namespace that exists under the home. This way we end up in the
+ special mkdir() code in mailbox_list_try_mkdir_root_parent().
+ Start from INBOX, since that's usually the correct place. */
+ ns = mail_namespace_find_inbox(user->namespaces);
+ if ((ret = mail_user_home_mkdir_try_ns(ns, home)) != 0)
+ return ret < 0 ? -1 : 0;
+ /* try other namespaces */
+ for (ns = user->namespaces; ns != NULL; ns = ns->next) {
+ if ((ns->flags & NAMESPACE_FLAG_INBOX_USER) != 0) {
+ /* already tried the INBOX namespace */
+ continue;
+ }
+ if ((ret = mail_user_home_mkdir_try_ns(ns, home)) != 0)
+ return ret < 0 ? -1 : 0;
+ }
+ /* fallback to a safe mkdir() with 0700 mode */
+ if (mkdir_parents(home, 0700) < 0 && errno != EEXIST) {
+ e_error(user->event, "mkdir_parents(%s) failed: %m", home);
+ return -1;
+ }
+ return 0;
+}
+
+const struct dict_op_settings *
+mail_user_get_dict_op_settings(struct mail_user *user)
+{
+ if (user->dict_op_set == NULL) {
+ user->dict_op_set = p_new(user->pool, struct dict_op_settings, 1);
+ user->dict_op_set->username = p_strdup(user->pool, user->username);
+ if (mail_user_get_home(user, &user->dict_op_set->home_dir) <= 0)
+ user->dict_op_set->home_dir = NULL;
+ }
+ return user->dict_op_set;
+}
+
+static const struct var_expand_func_table mail_user_var_expand_func_table_arr[] = {
+ { "userdb", mail_user_var_expand_func_userdb },
+ { NULL, NULL }
+};
+const struct var_expand_func_table *mail_user_var_expand_func_table =
+ mail_user_var_expand_func_table_arr;
diff --git a/src/lib-storage/mail-user.h b/src/lib-storage/mail-user.h
new file mode 100644
index 0000000..ef833d1
--- /dev/null
+++ b/src/lib-storage/mail-user.h
@@ -0,0 +1,259 @@
+#ifndef MAIL_USER_H
+#define MAIL_USER_H
+
+#include "net.h"
+#include "unichar.h"
+#include "mail-storage-settings.h"
+#include "process-stat.h"
+
+struct module;
+struct stats;
+struct fs_settings;
+struct ssl_iostream_settings;
+struct mail_user;
+struct dict_op_settings;
+
+struct mail_user_vfuncs {
+ void (*deinit)(struct mail_user *user);
+ void (*deinit_pre)(struct mail_user *user);
+ void (*stats_fill)(struct mail_user *user, struct stats *stats);
+};
+
+struct mail_user_connection_data {
+ struct ip_addr *local_ip, *remote_ip;
+ in_port_t local_port, remote_port;
+
+ bool secured:1;
+ bool ssl_secured:1;
+};
+
+struct mail_user {
+ pool_t pool;
+ struct mail_user_vfuncs v, *vlast;
+ int refcount;
+
+ struct event *event;
+ /* User's creator if such exists. For example for autocreated shared
+ mailbox users their creator is the logged in user. */
+ struct mail_user *creator;
+ /* Set if user was created via mail_storage_service. */
+ struct mail_storage_service_user *_service_user;
+
+ const char *username;
+ /* don't access the home directly. It may be set lazily. */
+ const char *_home;
+
+ uid_t uid;
+ gid_t gid;
+ const char *service;
+ const char *session_id;
+ struct mail_user_connection_data conn;
+ const char *auth_mech, *auth_token, *auth_user;
+ const char *const *userdb_fields;
+ /* Timestamp when this session was initially created. Most importantly
+ this stays the same after IMAP client is hibernated and restored. */
+ time_t session_create_time;
+
+ const struct var_expand_table *var_expand_table;
+ /* If non-NULL, fail the user initialization with this error.
+ This could be set by plugins that need to fail the initialization. */
+ const char *error;
+
+ const struct setting_parser_info *set_info;
+ const struct mail_user_settings *unexpanded_set;
+ struct mail_user_settings *set;
+ struct mail_namespace *namespaces;
+ struct mail_storage *storages;
+ struct dict_op_settings *dict_op_set;
+ ARRAY(const struct mail_storage_hooks *) hooks;
+
+ normalizer_func_t *default_normalizer;
+ /* Filled lazily by mailbox_attribute_*() when accessing attributes. */
+ struct dict *_attr_dict;
+
+ /* Module-specific contexts. See mail_storage_module_id. */
+ ARRAY(union mail_user_module_context *) module_contexts;
+
+ struct process_stat proc_stat;
+
+ /* User doesn't exist (as reported by userdb lookup when looking
+ up home) */
+ bool nonexistent:1;
+ /* Either home is set or there is no home for the user. */
+ bool home_looked_up:1;
+ /* User is anonymous */
+ bool anonymous:1;
+ /* This is an autocreated user (e.g. for shared namespace or
+ lda raw storage) */
+ bool autocreated:1;
+ /* mail_user_init() has been called */
+ bool initialized:1;
+ /* The initial namespaces have been created and
+ hook_mail_namespaces_created() has been called. */
+ bool namespaces_created:1;
+ /* SET_STR_VARS in user's all settings have been expanded.
+ This happens near the beginning of the user initialization,
+ so this is rarely needed to be checked. */
+ bool settings_expanded:1;
+ /* Shortcut to mail_storage_settings.mail_debug */
+ bool mail_debug:1;
+ /* If INBOX can't be opened, log an error, but only once. */
+ bool inbox_open_error_logged:1;
+ /* Fuzzy search works for this user (FTS enabled) */
+ bool fuzzy_search:1;
+ /* We're running dsync */
+ bool dsyncing:1;
+ /* Failed to create attribute dict, don't try again */
+ bool attr_dict_failed:1;
+ /* We're deinitializing the user */
+ bool deinitializing:1;
+ /* Enable administrator user commands for the user */
+ bool admin:1;
+ /* Enable all statistics gathering */
+ bool stats_enabled:1;
+ /* This session was restored (e.g. IMAP unhibernation) */
+ bool session_restored:1;
+};
+
+struct mail_user_module_register {
+ unsigned int id;
+};
+
+union mail_user_module_context {
+ struct mail_user_vfuncs super;
+ struct mail_user_module_register *reg;
+};
+extern struct mail_user_module_register mail_user_module_register;
+extern struct auth_master_connection *mail_user_auth_master_conn;
+extern const struct var_expand_func_table *mail_user_var_expand_func_table;
+
+struct mail_user *mail_user_alloc(struct event *parent_event,
+ const char *username,
+ const struct setting_parser_info *set_info,
+ const struct mail_user_settings *set);
+struct mail_user *
+mail_user_alloc_nodup_set(struct event *parent_event,
+ const char *username,
+ const struct setting_parser_info *set_info,
+ const struct mail_user_settings *set);
+/* Returns -1 if settings were invalid. */
+int mail_user_init(struct mail_user *user, const char **error_r);
+
+void mail_user_ref(struct mail_user *user);
+void mail_user_unref(struct mail_user **user);
+/* Assert that this is the last reference for the user and unref it. */
+void mail_user_deinit(struct mail_user **user);
+
+/* Duplicate a mail_user. mail_user_init() and mail_namespaces_init() need to
+ be called before the user is usable. */
+struct mail_user *mail_user_dup(struct mail_user *user);
+
+/* Find another user from the given user's namespaces. */
+struct mail_user *mail_user_find(struct mail_user *user, const char *name);
+
+/* Specify mail location %variable expansion data. */
+void mail_user_set_vars(struct mail_user *user, const char *service,
+ const struct mail_user_connection_data *conn);
+/* Return %variable expansion table for the user. */
+const struct var_expand_table *
+mail_user_var_expand_table(struct mail_user *user);
+
+/* Specify the user's home directory. This should be called also when it's
+ known that the user doesn't have a home directory to avoid the internal
+ lookup. */
+void mail_user_set_home(struct mail_user *user, const char *home);
+/* Get the home directory for the user. Returns 1 if home directory looked up
+ successfully, 0 if there is no home directory (either user doesn't exist or
+ has no home directory) or -1 if lookup failed. The returned home string
+ is valid until the user is freed. */
+int mail_user_get_home(struct mail_user *user, const char **home_r);
+/* Appends path + file prefix for creating a temporary file.
+ The file prefix doesn't contain any uniqueness. */
+void mail_user_set_get_temp_prefix(string_t *dest,
+ const struct mail_user_settings *set);
+/* Get volatile directory from INBOX namespace if configured. Returns NULL if
+ none is configured. */
+const char *mail_user_get_volatile_dir(struct mail_user *user);
+/* Returns 1 on success, 0 if lock_secs is reached, -1 on error */
+int mail_user_lock_file_create(struct mail_user *user, const char *lock_fname,
+ unsigned int lock_secs,
+ struct file_lock **lock_r, const char **error_r);
+
+/* Returns TRUE if plugin is loaded for the user. */
+bool mail_user_is_plugin_loaded(struct mail_user *user, struct module *module);
+/* If name exists in plugin_envs, return its value. */
+const char *mail_user_plugin_getenv(struct mail_user *user, const char *name);
+bool mail_user_plugin_getenv_bool(struct mail_user *user, const char *name);
+const char *mail_user_set_plugin_getenv(const struct mail_user_settings *set,
+ const char *name);
+bool mail_user_set_plugin_getenv_bool(const struct mail_user_settings *set,
+ const char *name);
+
+/* Add more namespaces to user's namespaces. The ->next pointers may be
+ changed, so the namespaces pointer will be updated to user->namespaces. */
+void mail_user_add_namespace(struct mail_user *user,
+ struct mail_namespace **namespaces);
+/* Drop autocreated shared namespaces that don't have any "usable" mailboxes. */
+void mail_user_drop_useless_namespaces(struct mail_user *user);
+
+/* Replace ~/ at the beginning of the path with the user's home directory. */
+const char *mail_user_home_expand(struct mail_user *user, const char *path);
+/* Returns 0 if ok, -1 if home directory isn't set. */
+int mail_user_try_home_expand(struct mail_user *user, const char **path);
+/* Returns unique user+ip identifier for anvil. */
+const char *mail_user_get_anvil_userip_ident(struct mail_user *user);
+
+/* Basically the same as mail_storage_find_class(), except automatically load
+ storage plugins when needed. */
+struct mail_storage *
+mail_user_get_storage_class(struct mail_user *user, const char *name);
+
+/* Initialize SSL client settings from mail_user settings. */
+void mail_user_init_ssl_client_settings(struct mail_user *user,
+ struct ssl_iostream_settings *ssl_set_r);
+
+/* Initialize fs_settings from mail_user settings. */
+void mail_user_init_fs_settings(struct mail_user *user,
+ struct fs_settings *fs_set,
+ struct ssl_iostream_settings *ssl_set_r);
+
+/* Fill statistics for user. By default there are no statistics, so stats
+ plugin must be loaded to have anything filled. */
+void mail_user_stats_fill(struct mail_user *user, struct stats *stats);
+
+/* Try to mkdir() user's home directory. Ideally this should be called only
+ after the caller tries to create a file to the home directory, but it fails
+ with ENOENT. This way it avoids unnecessary disk IO to the home. */
+int mail_user_home_mkdir(struct mail_user *user);
+
+/* Return dict_op_settings for the user. The returned settings are valid until
+ the user is freed. */
+const struct dict_op_settings *
+mail_user_get_dict_op_settings(struct mail_user *user);
+
+
+/* Obtain the postmaster address to be used for this user as an RFC 5322 (IMF)
+ address. Returns false if the configured postmaster address is invalid in
+ which case error_r contains the error message. */
+static inline bool
+mail_user_get_postmaster_address(struct mail_user *user,
+ const struct message_address **address_r,
+ const char **error_r)
+{
+ return mail_user_set_get_postmaster_address(user->set, address_r,
+ error_r);
+}
+
+/* Obtain the postmaster address to be used for this user as an RFC 5321 (SMTP)
+ address. Returns false if the configured postmaster address is invalid in
+ which case error_r contains the error message. */
+static inline bool
+mail_user_get_postmaster_smtp(struct mail_user *user,
+ const struct smtp_address **address_r,
+ const char **error_r)
+{
+ return mail_user_set_get_postmaster_smtp(user->set, address_r,
+ error_r);
+}
+
+#endif
diff --git a/src/lib-storage/mail.c b/src/lib-storage/mail.c
new file mode 100644
index 0000000..b3991d6
--- /dev/null
+++ b/src/lib-storage/mail.c
@@ -0,0 +1,694 @@
+/* Copyright (c) 2002-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "ioloop.h"
+#include "buffer.h"
+#include "hash.h"
+#include "hex-binary.h"
+#include "crc32.h"
+#include "sha1.h"
+#include "hostpid.h"
+#include "istream.h"
+#include "mail-cache.h"
+#include "mail-storage-private.h"
+#include "message-id.h"
+#include "message-part-data.h"
+#include "imap-bodystructure.h"
+
+#include <time.h>
+
+struct mail *mail_alloc(struct mailbox_transaction_context *t,
+ enum mail_fetch_field wanted_fields,
+ struct mailbox_header_lookup_ctx *wanted_headers)
+{
+ struct mail *mail;
+
+ i_assert(wanted_headers == NULL || wanted_headers->box == t->box);
+
+ T_BEGIN {
+ mail = t->box->v.mail_alloc(t, wanted_fields, wanted_headers);
+ hook_mail_allocated(mail);
+ } T_END;
+
+ return mail;
+}
+
+void mail_free(struct mail **mail)
+{
+ struct mail_private *p = (struct mail_private *)*mail;
+
+ /* make sure mailbox_search_*() users don't try to free the mail
+ directly */
+ i_assert(!p->search_mail);
+
+ p->v.free(*mail);
+ *mail = NULL;
+}
+
+void mail_set_seq(struct mail *mail, uint32_t seq)
+{
+ struct mail_private *p = (struct mail_private *)mail;
+
+ T_BEGIN {
+ p->v.set_seq(mail, seq, FALSE);
+ } T_END;
+}
+
+void mail_set_seq_saving(struct mail *mail, uint32_t seq)
+{
+ struct mail_private *p = (struct mail_private *)mail;
+
+ T_BEGIN {
+ p->v.set_seq(mail, seq, TRUE);
+ } T_END;
+}
+
+bool mail_set_uid(struct mail *mail, uint32_t uid)
+{
+ struct mail_private *p = (struct mail_private *)mail;
+ bool ret;
+
+ T_BEGIN {
+ ret = p->v.set_uid(mail, uid);
+ } T_END;
+ return ret;
+}
+
+bool mail_prefetch(struct mail *mail)
+{
+ struct mail_private *p = (struct mail_private *)mail;
+ bool ret;
+
+ T_BEGIN {
+ ret = p->v.prefetch(mail);
+ } T_END;
+ return ret;
+}
+
+void mail_add_temp_wanted_fields(struct mail *mail,
+ enum mail_fetch_field fields,
+ struct mailbox_header_lookup_ctx *headers)
+{
+ struct mail_private *p = (struct mail_private *)mail;
+
+ i_assert(headers == NULL || headers->box == mail->box);
+
+ p->v.add_temp_wanted_fields(mail, fields, headers);
+}
+
+static bool index_mail_get_age_days(struct mail *mail, int *days_r)
+{
+ int age_days;
+ const struct mail_index_header *hdr =
+ mail_index_get_header(mail->transaction->view);
+ int n_days = N_ELEMENTS(hdr->day_first_uid);
+
+ for (age_days = 0; age_days < n_days; age_days++) {
+ if (mail->uid >= hdr->day_first_uid[age_days])
+ break;
+ }
+
+ if (age_days == n_days) {
+ /* mail is too old, cannot determine its age from
+ day_first_uid[]. */
+ return FALSE;
+ }
+
+ if (hdr->day_stamp != 0) {
+ /* offset for hdr->day_stamp */
+ age_days += (ioloop_time - hdr->day_stamp) / (3600 * 24);
+ }
+ *days_r = age_days;
+ return TRUE;
+}
+
+void mail_event_create(struct mail *mail)
+{
+ struct mail_private *p = (struct mail_private *)mail;
+ int age_days;
+
+ if (p->_event != NULL)
+ return;
+ p->_event = event_create(mail->box->event);
+ event_add_category(p->_event, &event_category_mail);
+ event_add_int(p->_event, "seq", mail->seq);
+ event_add_int(p->_event, "uid", mail->uid);
+ /* Add mail age field to event. */
+ if (index_mail_get_age_days(mail, &age_days))
+ event_add_int(p->_event, "mail_age_days", age_days);
+
+ char uid_buf[MAX_INT_STRLEN];
+ const char *prefix = t_strconcat(
+ p->mail.saving ? "saving UID " : "UID ",
+ dec2str_buf(uid_buf, p->mail.uid), ": ", NULL);
+ event_set_append_log_prefix(p->_event, prefix);
+}
+
+struct event *mail_event(struct mail *mail)
+{
+ struct mail_private *p = (struct mail_private *)mail;
+
+ mail_event_create(mail);
+ return p->_event;
+}
+
+enum mail_flags mail_get_flags(struct mail *mail)
+{
+ struct mail_private *p = (struct mail_private *)mail;
+
+ return p->v.get_flags(mail);
+}
+
+uint64_t mail_get_modseq(struct mail *mail)
+{
+ struct mail_private *p = (struct mail_private *)mail;
+
+ return p->v.get_modseq(mail);
+}
+
+uint64_t mail_get_pvt_modseq(struct mail *mail)
+{
+ struct mail_private *p = (struct mail_private *)mail;
+
+ return p->v.get_pvt_modseq(mail);
+}
+
+const char *const *mail_get_keywords(struct mail *mail)
+{
+ struct mail_private *p = (struct mail_private *)mail;
+
+ return p->v.get_keywords(mail);
+}
+
+const ARRAY_TYPE(keyword_indexes) *mail_get_keyword_indexes(struct mail *mail)
+{
+ struct mail_private *p = (struct mail_private *)mail;
+
+ return p->v.get_keyword_indexes(mail);
+}
+
+int mail_get_parts(struct mail *mail, struct message_part **parts_r)
+{
+ struct mail_private *p = (struct mail_private *)mail;
+ int ret;
+
+ T_BEGIN {
+ ret = p->v.get_parts(mail, parts_r);
+ } T_END;
+ return ret;
+}
+
+int mail_get_date(struct mail *mail, time_t *date_r, int *timezone_r)
+{
+ struct mail_private *p = (struct mail_private *)mail;
+ int ret;
+
+ T_BEGIN {
+ ret = p->v.get_date(mail, date_r, timezone_r);
+ } T_END;
+ return ret;
+}
+
+int mail_get_received_date(struct mail *mail, time_t *date_r)
+{
+ struct mail_private *p = (struct mail_private *)mail;
+ int ret;
+
+ T_BEGIN {
+ ret = p->v.get_received_date(mail, date_r);
+ } T_END;
+ return ret;
+}
+
+int mail_get_save_date(struct mail *mail, time_t *date_r)
+{
+ struct mail_private *p = (struct mail_private *)mail;
+ int ret;
+
+ T_BEGIN {
+ ret = p->v.get_save_date(mail, date_r);
+ } T_END;
+ return ret;
+}
+
+int mail_get_virtual_size(struct mail *mail, uoff_t *size_r)
+{
+ struct mail_private *p = (struct mail_private *)mail;
+ int ret;
+
+ T_BEGIN {
+ ret = p->v.get_virtual_size(mail, size_r);
+ } T_END;
+ return ret;
+}
+
+int mail_get_physical_size(struct mail *mail, uoff_t *size_r)
+{
+ struct mail_private *p = (struct mail_private *)mail;
+ int ret;
+
+ T_BEGIN {
+ ret = p->v.get_physical_size(mail, size_r);
+ } T_END;
+ return ret;
+}
+
+int mail_get_first_header(struct mail *mail, const char *field,
+ const char **value_r)
+{
+ struct mail_private *p = (struct mail_private *)mail;
+ int ret;
+
+ T_BEGIN {
+ ret = p->v.get_first_header(mail, field, FALSE, value_r);
+ } T_END;
+ return ret;
+}
+
+int mail_get_first_header_utf8(struct mail *mail, const char *field,
+ const char **value_r)
+{
+ struct mail_private *p = (struct mail_private *)mail;
+ int ret;
+
+ T_BEGIN {
+ ret = p->v.get_first_header(mail, field, TRUE, value_r);
+ } T_END;
+ return ret;
+}
+
+int mail_get_headers(struct mail *mail, const char *field,
+ const char *const **value_r)
+{
+ struct mail_private *p = (struct mail_private *)mail;
+ int ret;
+
+ T_BEGIN {
+ ret = p->v.get_headers(mail, field, FALSE, value_r);
+ } T_END;
+ return ret;
+}
+
+int mail_get_headers_utf8(struct mail *mail, const char *field,
+ const char *const **value_r)
+{
+ struct mail_private *p = (struct mail_private *)mail;
+ int ret;
+
+ T_BEGIN {
+ ret = p->v.get_headers(mail, field, TRUE, value_r);
+ } T_END;
+ return ret;
+}
+
+int mail_get_header_stream(struct mail *mail,
+ struct mailbox_header_lookup_ctx *headers,
+ struct istream **stream_r)
+{
+ struct mail_private *p = (struct mail_private *)mail;
+ int ret;
+
+ i_assert(headers->count > 0);
+ i_assert(headers->box == mail->box);
+
+ T_BEGIN {
+ ret = p->v.get_header_stream(mail, headers, stream_r);
+ } T_END;
+ return ret;
+}
+
+void mail_set_aborted(struct mail *mail)
+{
+ mail_storage_set_error(mail->box->storage, MAIL_ERROR_LOOKUP_ABORTED,
+ "Mail field not cached");
+}
+
+int mail_get_stream(struct mail *mail, struct message_size *hdr_size,
+ struct message_size *body_size, struct istream **stream_r)
+{
+ return mail_get_stream_because(mail, hdr_size, body_size,
+ "mail stream", stream_r);
+}
+
+int mail_get_stream_because(struct mail *mail, struct message_size *hdr_size,
+ struct message_size *body_size,
+ const char *reason, struct istream **stream_r)
+{
+ struct mail_private *p = (struct mail_private *)mail;
+ int ret;
+
+ if (mail->lookup_abort != MAIL_LOOKUP_ABORT_NEVER) {
+ mail_set_aborted(mail);
+ return -1;
+ }
+ T_BEGIN {
+ p->get_stream_reason = reason;
+ ret = p->v.get_stream(mail, TRUE, hdr_size, body_size, stream_r);
+ p->get_stream_reason = "";
+ } T_END;
+ i_assert(ret < 0 || (*stream_r)->blocking);
+ return ret;
+}
+
+int mail_get_hdr_stream(struct mail *mail, struct message_size *hdr_size,
+ struct istream **stream_r)
+{
+ return mail_get_hdr_stream_because(mail, hdr_size, "header stream", stream_r);
+}
+
+int mail_get_hdr_stream_because(struct mail *mail,
+ struct message_size *hdr_size,
+ const char *reason, struct istream **stream_r)
+{
+ struct mail_private *p = (struct mail_private *)mail;
+ int ret;
+
+ if (mail->lookup_abort != MAIL_LOOKUP_ABORT_NEVER) {
+ mail_set_aborted(mail);
+ return -1;
+ }
+ T_BEGIN {
+ p->get_stream_reason = reason;
+ ret = p->v.get_stream(mail, FALSE, hdr_size, NULL, stream_r);
+ p->get_stream_reason = "";
+ } T_END;
+ i_assert(ret < 0 || (*stream_r)->blocking);
+ return ret;
+}
+
+int mail_get_binary_stream(struct mail *mail, const struct message_part *part,
+ bool include_hdr, uoff_t *size_r,
+ bool *binary_r, struct istream **stream_r)
+{
+ struct mail_private *p = (struct mail_private *)mail;
+ int ret;
+
+ if (mail->lookup_abort != MAIL_LOOKUP_ABORT_NEVER) {
+ mail_set_aborted(mail);
+ return -1;
+ }
+ T_BEGIN {
+ ret = p->v.get_binary_stream(mail, part, include_hdr,
+ size_r, NULL, binary_r, stream_r);
+ } T_END;
+ i_assert(ret < 0 || (*stream_r)->blocking);
+ return ret;
+}
+
+int mail_get_binary_size(struct mail *mail, const struct message_part *part,
+ bool include_hdr, uoff_t *size_r,
+ unsigned int *lines_r)
+{
+ struct mail_private *p = (struct mail_private *)mail;
+ bool binary;
+ int ret;
+
+ T_BEGIN {
+ ret = p->v.get_binary_stream(mail, part, include_hdr,
+ size_r, lines_r, &binary, NULL);
+ } T_END;
+ return ret;
+}
+
+int mail_get_special(struct mail *mail, enum mail_fetch_field field,
+ const char **value_r)
+{
+ struct mail_private *p = (struct mail_private *)mail;
+
+ if (p->v.get_special(mail, field, value_r) < 0)
+ return -1;
+ i_assert(*value_r != NULL);
+ return 0;
+}
+
+int mail_get_backend_mail(struct mail *mail, struct mail **real_mail_r)
+{
+ struct mail_private *p = (struct mail_private *)mail;
+ return p->v.get_backend_mail(mail, real_mail_r);
+}
+
+int mail_get_message_id(struct mail *mail, const char **value_r)
+{
+ const char *hdr_value, *msgid_bare;
+ int ret;
+
+ *value_r = NULL;
+
+ ret = mail_get_first_header(mail, "Message-ID", &hdr_value);
+ if (ret <= 0)
+ return ret;
+
+ msgid_bare = message_id_get_next(&hdr_value);
+ if (msgid_bare == NULL)
+ return 0;
+
+ /* Complete the message ID with surrounding `<' and `>'. */
+ *value_r = t_strconcat("<", msgid_bare, ">", NULL);
+ return 1;
+}
+
+void mail_update_flags(struct mail *mail, enum modify_type modify_type,
+ enum mail_flags flags)
+{
+ struct mail_private *p = (struct mail_private *)mail;
+
+ p->v.update_flags(mail, modify_type, flags);
+}
+
+void mail_update_keywords(struct mail *mail, enum modify_type modify_type,
+ struct mail_keywords *keywords)
+{
+ struct mail_private *p = (struct mail_private *)mail;
+
+ p->v.update_keywords(mail, modify_type, keywords);
+}
+
+void mail_update_modseq(struct mail *mail, uint64_t min_modseq)
+{
+ struct mail_private *p = (struct mail_private *)mail;
+
+ p->v.update_modseq(mail, min_modseq);
+}
+
+void mail_update_pvt_modseq(struct mail *mail, uint64_t min_pvt_modseq)
+{
+ struct mail_private *p = (struct mail_private *)mail;
+
+ p->v.update_pvt_modseq(mail, min_pvt_modseq);
+}
+
+void mail_update_pop3_uidl(struct mail *mail, const char *uidl)
+{
+ struct mail_private *p = (struct mail_private *)mail;
+
+ if (p->v.update_pop3_uidl != NULL)
+ p->v.update_pop3_uidl(mail, uidl);
+}
+
+void mail_expunge(struct mail *mail)
+{
+ struct mail_private *p = (struct mail_private *)mail;
+
+ T_BEGIN {
+ p->v.expunge(mail);
+ } T_END;
+ mail_expunge_requested_event(mail);
+}
+
+void mail_autoexpunge(struct mail *mail)
+{
+ struct mail_private *p = (struct mail_private *)mail;
+ p->autoexpunged = TRUE;
+ mail_expunge(mail);
+ p->autoexpunged = FALSE;
+}
+
+void mail_set_expunged(struct mail *mail)
+{
+ mail_storage_set_error(mail->box->storage, MAIL_ERROR_EXPUNGED,
+ "Message was expunged");
+ mail->expunged = TRUE;
+}
+
+int mail_precache(struct mail *mail)
+{
+ struct mail_private *p = (struct mail_private *)mail;
+ int ret;
+
+ T_BEGIN {
+ ret = p->v.precache(mail);
+ } T_END;
+ return ret;
+}
+
+void mail_set_cache_corrupted(struct mail *mail,
+ enum mail_fetch_field field,
+ const char *reason)
+{
+ struct mail_private *p = (struct mail_private *)mail;
+ p->v.set_cache_corrupted(mail, field, reason);
+}
+
+void mail_generate_guid_128_hash(const char *guid, guid_128_t guid_128_r)
+{
+ unsigned char sha1_sum[SHA1_RESULTLEN];
+ buffer_t buf;
+
+ if (guid_128_from_string(guid, guid_128_r) < 0) {
+ /* not 128bit hex. use a hash of it instead. */
+ buffer_create_from_data(&buf, guid_128_r, GUID_128_SIZE);
+ buffer_set_used_size(&buf, 0);
+ sha1_get_digest(guid, strlen(guid), sha1_sum);
+#if SHA1_RESULTLEN < GUID_128_SIZE
+# error not possible
+#endif
+ buffer_append(&buf,
+ sha1_sum + SHA1_RESULTLEN - GUID_128_SIZE,
+ GUID_128_SIZE);
+ }
+}
+
+static bool
+mail_message_has_attachment(struct message_part *part,
+ const struct message_part_attachment_settings *set)
+{
+ for (; part != NULL; part = part->next) {
+ if (message_part_is_attachment(part, set) ||
+ mail_message_has_attachment(part->children, set))
+ return TRUE;
+ }
+
+ return FALSE;
+}
+
+bool mail_has_attachment_keywords(struct mail *mail)
+{
+ const char *const *kw = mail_get_keywords(mail);
+ return (str_array_icase_find(kw, MAIL_KEYWORD_HAS_ATTACHMENT) !=
+ str_array_icase_find(kw, MAIL_KEYWORD_HAS_NO_ATTACHMENT));
+}
+
+static int mail_parse_parts(struct mail *mail, struct message_part **parts_r)
+{
+ const char *structure, *error;
+ struct mail_private *pmail = (struct mail_private*)mail;
+
+ /* need to get bodystructure first */
+ if (mail_get_special(mail, MAIL_FETCH_IMAP_BODYSTRUCTURE,
+ &structure) < 0) {
+ /* Don't bother logging an error. See
+ mail_set_attachment_keywords(). */
+ return -1;
+ }
+ if (imap_bodystructure_parse_full(structure, pmail->data_pool, parts_r,
+ &error) < 0) {
+ mail_set_cache_corrupted(mail, MAIL_FETCH_IMAP_BODYSTRUCTURE,
+ error);
+ return -1;
+ }
+ return 0;
+}
+
+int mail_set_attachment_keywords(struct mail *mail)
+{
+ int ret;
+ const struct mail_storage_settings *mail_set =
+ mail_storage_get_settings(mailbox_get_storage(mail->box));
+
+ const char *const keyword_has_attachment[] = {
+ MAIL_KEYWORD_HAS_ATTACHMENT,
+ NULL,
+ };
+ const char *const keyword_has_no_attachment[] = {
+ MAIL_KEYWORD_HAS_NO_ATTACHMENT,
+ NULL
+ };
+ struct message_part_attachment_settings set = {
+ .content_type_filter =
+ mail_set->parsed_mail_attachment_content_type_filter,
+ .exclude_inlined =
+ mail_set->parsed_mail_attachment_exclude_inlined,
+ };
+ struct mail_keywords *kw_has = NULL, *kw_has_not = NULL;
+
+ /* walk all parts and see if there is an attachment */
+ struct message_part *parts;
+ if (mail_get_parts(mail, &parts) < 0) {
+ /* Callers don't really care about the exact error, and
+ critical errors were already logged. Most importantly we
+ don't want to log MAIL_ERROR_LOOKUP_ABORTED since that is
+ an expected error. */
+ ret = -1;
+ } else if (parts->data == NULL &&
+ mail_parse_parts(mail, &parts) < 0) {
+ ret = -1;
+ } else if (mailbox_keywords_create(mail->box, keyword_has_attachment, &kw_has) < 0 ||
+ mailbox_keywords_create(mail->box, keyword_has_no_attachment, &kw_has_not) < 0) {
+ mail_set_critical(mail, "Failed to add attachment keywords: "
+ "mailbox_keywords_create(%s) failed: %s",
+ mailbox_get_vname(mail->box),
+ mail_storage_get_last_internal_error(mail->box->storage, NULL));
+ ret = -1;
+ } else {
+ bool has_attachment = mail_message_has_attachment(parts, &set);
+
+ /* make sure only one of the keywords gets set */
+ mail_update_keywords(mail, MODIFY_REMOVE, has_attachment ? kw_has_not : kw_has);
+ mail_update_keywords(mail, MODIFY_ADD, has_attachment ? kw_has : kw_has_not);
+ ret = has_attachment ? 1 : 0;
+ }
+
+ if (kw_has != NULL)
+ mailbox_keywords_unref(&kw_has);
+ if (kw_has_not != NULL)
+ mailbox_keywords_unref(&kw_has_not);
+
+ return ret;
+}
+
+bool mail_stream_access_start(struct mail *mail)
+{
+ if (mail->lookup_abort != MAIL_LOOKUP_ABORT_NEVER) {
+ mail_set_aborted(mail);
+ return FALSE;
+ }
+ mail->mail_stream_accessed = TRUE;
+ mail_event_create(mail);
+ return TRUE;
+}
+
+bool mail_metadata_access_start(struct mail *mail)
+{
+ if (mail->lookup_abort >= MAIL_LOOKUP_ABORT_NOT_IN_CACHE) {
+ mail_set_aborted(mail);
+ return FALSE;
+ }
+ mail->mail_metadata_accessed = TRUE;
+ mail_event_create(mail);
+ return TRUE;
+}
+
+void mail_opened_event(struct mail *mail)
+{
+ struct mail_private *pmail =
+ container_of(mail, struct mail_private, mail);
+ struct event_passthrough *e =
+ event_create_passthrough(mail_event(mail))->
+ set_name("mail_opened")->
+ add_str("reason", pmail->get_stream_reason);
+ if (pmail->get_stream_reason != NULL)
+ e_debug(e->event(), "Opened mail because: %s",
+ pmail->get_stream_reason);
+ else
+ e_debug(e->event(), "Opened mail");
+}
+
+void mail_expunge_requested_event(struct mail *mail)
+{
+ struct event_passthrough *e =
+ event_create_passthrough(mail_event(mail))->
+ set_name("mail_expunge_requested")->
+ add_int("uid", mail->uid)->
+ add_int("seq", mail->seq);
+ e_debug(e->event(), "Expunge requested");
+}
diff --git a/src/lib-storage/mailbox-attribute-internal.c b/src/lib-storage/mailbox-attribute-internal.c
new file mode 100644
index 0000000..d5d0265
--- /dev/null
+++ b/src/lib-storage/mailbox-attribute-internal.c
@@ -0,0 +1,149 @@
+/* Copyright (c) 2003-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "mail-storage-private.h"
+#include "mailbox-attribute-internal.h"
+
+/*
+ * Internal mailbox attributes
+ */
+
+ /* /private/specialuse (RFC 6154) */
+
+static int
+mailbox_attribute_specialuse_get(struct mailbox *box,
+ const char *key ATTR_UNUSED,
+ struct mail_attribute_value *value_r)
+{
+ const struct mailbox_settings *set = box->set;
+
+ if (set == NULL || *set->special_use == '\0')
+ return 0;
+
+ value_r->value = set->special_use;
+ return 1;
+}
+
+static struct mailbox_attribute_internal
+iattr_mbox_prv_special_use = {
+ .type = MAIL_ATTRIBUTE_TYPE_PRIVATE,
+ .key = MAILBOX_ATTRIBUTE_SPECIALUSE,
+ .rank = MAIL_ATTRIBUTE_INTERNAL_RANK_AUTHORITY,
+ .flags = MAIL_ATTRIBUTE_INTERNAL_FLAG_VALIDATED,
+
+ .get = mailbox_attribute_specialuse_get
+};
+
+/* /private/comment, /shared/comment (RFC 5464) */
+
+static int
+mailbox_attribute_comment_get(struct mailbox *box,
+ const char *key ATTR_UNUSED,
+ struct mail_attribute_value *value_r)
+{
+ const struct mailbox_settings *set = box->set;
+
+ if (set == NULL || *set->comment == '\0')
+ return 0;
+ value_r->value = set->comment;
+ return 1;
+}
+
+static struct mailbox_attribute_internal
+iattr_mbox_prv_comment = {
+ .type = MAIL_ATTRIBUTE_TYPE_PRIVATE,
+ .key = MAILBOX_ATTRIBUTE_COMMENT,
+ .rank = MAIL_ATTRIBUTE_INTERNAL_RANK_DEFAULT,
+
+ .get = mailbox_attribute_comment_get
+};
+
+static struct mailbox_attribute_internal
+iattr_mbox_shd_comment = {
+ .type = MAIL_ATTRIBUTE_TYPE_SHARED,
+ .key = MAILBOX_ATTRIBUTE_COMMENT,
+ .rank = MAIL_ATTRIBUTE_INTERNAL_RANK_DEFAULT,
+
+ .get = mailbox_attribute_comment_get
+};
+
+/*
+ * Internal server attributes
+ */
+
+/* /shared/comment (RFC 5464) */
+
+static int
+server_attribute_comment_get(struct mailbox *box,
+ const char *key ATTR_UNUSED,
+ struct mail_attribute_value *value_r)
+{
+ const struct mail_storage_settings *set = box->storage->set;
+
+ if (*set->mail_server_comment == '\0')
+ return 0;
+ value_r->value = set->mail_server_comment;
+ return 1;
+}
+
+static struct mailbox_attribute_internal
+iattr_serv_shd_comment = {
+ .type = MAIL_ATTRIBUTE_TYPE_SHARED,
+ .key = MAILBOX_ATTRIBUTE_PREFIX_DOVECOT_PVT_SERVER
+ MAIL_SERVER_ATTRIBUTE_COMMENT,
+ .rank = MAIL_ATTRIBUTE_INTERNAL_RANK_AUTHORITY,
+
+ .get = server_attribute_comment_get
+};
+
+/* /shared/admin (RFC 5464) */
+
+static int
+server_attribute_admin_get(struct mailbox *box,
+ const char *key ATTR_UNUSED,
+ struct mail_attribute_value *value_r)
+{
+ const struct mail_storage_settings *set = box->storage->set;
+
+ if (*set->mail_server_admin == '\0')
+ return 0;
+ value_r->value = set->mail_server_admin;
+ return 1;
+}
+
+static struct mailbox_attribute_internal
+iattr_serv_shd_admin = {
+ .type = MAIL_ATTRIBUTE_TYPE_SHARED,
+ .key = MAILBOX_ATTRIBUTE_PREFIX_DOVECOT_PVT_SERVER
+ MAIL_SERVER_ATTRIBUTE_ADMIN,
+ .rank = MAIL_ATTRIBUTE_INTERNAL_RANK_AUTHORITY,
+
+ .get = server_attribute_admin_get
+};
+
+/*
+ * Registry
+ */
+
+void mailbox_attributes_internal_init(void)
+{
+ /*
+ * Internal mailbox attributes
+ */
+
+ /* /private/specialuse (RFC 6154) */
+ mailbox_attribute_register_internal(&iattr_mbox_prv_special_use);
+ /* /private/comment (RFC 5464) */
+ mailbox_attribute_register_internal(&iattr_mbox_prv_comment);
+ /* /shared/comment (RFC 5464) */
+ mailbox_attribute_register_internal(&iattr_mbox_shd_comment);
+
+ /*
+ * internal server attributes
+ */
+
+ /* /shared/comment (RFC 5464) */
+ mailbox_attribute_register_internal(&iattr_serv_shd_comment);
+ /* /shared/admin (RFC 5464) */
+ mailbox_attribute_register_internal(&iattr_serv_shd_admin);
+}
diff --git a/src/lib-storage/mailbox-attribute-internal.h b/src/lib-storage/mailbox-attribute-internal.h
new file mode 100644
index 0000000..3c9b635
--- /dev/null
+++ b/src/lib-storage/mailbox-attribute-internal.h
@@ -0,0 +1,14 @@
+#ifndef MAILBOX_ATTRIBUTE_INTERNAL_H
+#define MAILBOX_ATTRIBUTE_INTERNAL_H
+
+/* RFC 5464, Section 3.2.1.2: Mailbox entries */
+#define MAILBOX_ATTRIBUTE_COMMENT "comment"
+/* RFC 6154, Section 4: IMAP METADATA Entry for Special-Use Attributes */
+#define MAILBOX_ATTRIBUTE_SPECIALUSE "specialuse"
+/* RFC 5464, Section 3.2.1.1: Server entries */
+#define MAIL_SERVER_ATTRIBUTE_COMMENT "comment"
+#define MAIL_SERVER_ATTRIBUTE_ADMIN "admin"
+
+void mailbox_attributes_internal_init(void);
+
+#endif
diff --git a/src/lib-storage/mailbox-attribute-lua.c b/src/lib-storage/mailbox-attribute-lua.c
new file mode 100644
index 0000000..8d615a0
--- /dev/null
+++ b/src/lib-storage/mailbox-attribute-lua.c
@@ -0,0 +1,163 @@
+/* Copyright (c) 2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "str.h"
+#include "istream.h"
+#include "array.h"
+#include "var-expand.h"
+#include "dlua-script.h"
+#include "dlua-script-private.h"
+#include "mail-storage.h"
+#include "mailbox-attribute.h"
+#include "mail-storage-lua.h"
+#include "mail-storage-lua-private.h"
+#include "mail-user.h"
+
+/* lookup mailbox attribute */
+int lua_storage_mailbox_attribute_get(struct mailbox *box, const char *key,
+ const char **value_r, size_t *value_len_r,
+ const char **error_r)
+{
+ struct mail_attribute_value value;
+ enum mail_attribute_type attr_type;
+ int ret;
+
+ if (str_begins(key, "/private/")) {
+ attr_type = MAIL_ATTRIBUTE_TYPE_PRIVATE;
+ key += 9;
+ } else if (str_begins(key, "/shared/")) {
+ attr_type = MAIL_ATTRIBUTE_TYPE_SHARED;
+ key += 8;
+ } else {
+ *error_r = "Invalid key prefix, must be /private/ or /shared/";
+ return -1;
+ }
+
+ /* get the attribute */
+ if ((ret = mailbox_attribute_get_stream(box, attr_type, key, &value)) < 0) {
+ *error_r = mailbox_get_last_error(box, NULL);
+ return ret;
+ } else if (ret == 0) {
+ /* was not found */
+ *value_r = NULL;
+ *value_len_r = 0;
+ return 0;
+ }
+
+ if (value.value_stream != NULL) {
+ string_t *str = t_str_new(128);
+ const unsigned char *data;
+ size_t siz;
+ while((ret = i_stream_read_more(value.value_stream, &data, &siz))>0) {
+ str_append_data(str, data, siz);
+ i_stream_skip(value.value_stream, siz);
+ }
+ i_assert(ret != 0);
+ if (ret == -1 && !value.value_stream->eof) {
+ /* we could not read the stream */
+ *error_r = i_stream_get_error(value.value_stream);
+ ret = -1;
+ } else {
+ *value_r = str->data;
+ *value_len_r = str->used;
+ ret = 1;
+ }
+ i_stream_unref(&value.value_stream);
+ return ret;
+ }
+
+ *value_r = value.value;
+ if (value.value != NULL)
+ *value_len_r = strlen(value.value);
+ else
+ *value_len_r = 0;
+ return 1;
+}
+
+int lua_storage_mailbox_attribute_set(struct mailbox *box, const char *key,
+ const char *value, size_t value_len,
+ const char **error_r)
+{
+ struct mail_attribute_value attr_value;
+ enum mail_attribute_type attr_type;
+ int ret;
+
+ i_assert(value != NULL || value_len == 0);
+
+ if (str_begins(key, "/private/")) {
+ attr_type = MAIL_ATTRIBUTE_TYPE_PRIVATE;
+ key += 9;
+ } else if (str_begins(key, "/shared/")) {
+ attr_type = MAIL_ATTRIBUTE_TYPE_SHARED;
+ key += 8;
+ } else {
+ *error_r = "Invalid key prefix, must be /private/ or /shared/";
+ return -1;
+ }
+
+ struct mailbox_transaction_context *t =
+ mailbox_transaction_begin(box, MAILBOX_TRANSACTION_FLAG_NO_NOTIFY, __func__);
+ i_zero(&attr_value);
+
+ if (value != NULL) {
+ /* use stream API to allow NULs in data */
+ attr_value.value_stream = i_stream_create_from_data(value, value_len);
+ }
+
+ ret = mailbox_attribute_set(t, attr_type, key, &attr_value);
+
+ if (ret < 0) {
+ *error_r = mailbox_get_last_error(box, NULL);
+ mailbox_transaction_rollback(&t);
+ } else if ((ret = mailbox_transaction_commit(&t)) < 0) {
+ *error_r = mailbox_get_last_error(box, NULL);
+ }
+
+ if (attr_value.value_stream != NULL)
+ i_stream_unref(&attr_value.value_stream);
+
+ return ret;
+}
+
+int lua_storage_mailbox_attribute_list(struct mailbox *box, const char *prefix,
+ ARRAY_TYPE(lua_storage_keyvalue) *items_r,
+ const char **error_r)
+{
+ const char *key, *orig_prefix = prefix;
+ enum mail_attribute_type attr_type;
+ int ret;
+
+ if (str_begins(prefix, "/private/")) {
+ attr_type = MAIL_ATTRIBUTE_TYPE_PRIVATE;
+ prefix += 9;
+ } else if (str_begins(prefix, "/shared/")) {
+ attr_type = MAIL_ATTRIBUTE_TYPE_SHARED;
+ prefix += 8;
+ } else {
+ *error_r = "Invalid key prefix, must be /private/ or /shared/";
+ return -1;
+ }
+
+ struct mailbox_attribute_iter *iter =
+ mailbox_attribute_iter_init(box, attr_type, prefix);
+
+ ret = 0;
+ *error_r = NULL;
+ while((key = mailbox_attribute_iter_next(iter)) != NULL) {
+ struct lua_storage_keyvalue *item = array_append_space(items_r);
+ item->key = t_strdup_printf("%s%s", orig_prefix, key);
+ if (lua_storage_mailbox_attribute_get(box, item->key, &item->value,
+ &item->value_len, error_r) < 0) {
+ ret = -1;
+ break;
+ }
+ }
+
+ if (mailbox_attribute_iter_deinit(&iter) < 0 || ret == -1) {
+ if (*error_r == NULL)
+ *error_r = mailbox_get_last_error(box, NULL);
+ return -1;
+ }
+
+ return 0;
+}
diff --git a/src/lib-storage/mailbox-attribute-private.h b/src/lib-storage/mailbox-attribute-private.h
new file mode 100644
index 0000000..0296cdd
--- /dev/null
+++ b/src/lib-storage/mailbox-attribute-private.h
@@ -0,0 +1,15 @@
+#ifndef MAILBOX_ATTRIBUTE_PRIVATE_H
+#define MAILBOX_ATTRIBUTE_PRIVATE_H
+
+struct mailbox_attribute_iter {
+ struct mailbox *box;
+};
+
+void mailbox_attributes_init(void);
+void mailbox_attributes_deinit(void);
+
+int mailbox_attribute_value_to_string(struct mail_storage *storage,
+ const struct mail_attribute_value *value,
+ const char **str_r);
+
+#endif
diff --git a/src/lib-storage/mailbox-attribute.c b/src/lib-storage/mailbox-attribute.c
new file mode 100644
index 0000000..5bd8c1c
--- /dev/null
+++ b/src/lib-storage/mailbox-attribute.c
@@ -0,0 +1,582 @@
+/* Copyright (c) 2003-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "str.h"
+#include "istream.h"
+#include "mail-storage-private.h"
+#include "bsearch-insert-pos.h"
+#include "mailbox-attribute-internal.h"
+
+static ARRAY(struct mailbox_attribute_internal) mailbox_internal_attributes;
+static pool_t mailbox_attribute_pool;
+
+void mailbox_attributes_init(void)
+{
+ mailbox_attribute_pool =
+ pool_alloconly_create("mailbox attributes", 2048);
+ i_array_init(&mailbox_internal_attributes, 32);
+
+ /* internal mailbox attributes */
+ mailbox_attributes_internal_init();
+}
+
+void mailbox_attributes_deinit(void)
+{
+ pool_unref(&mailbox_attribute_pool);
+ array_free(&mailbox_internal_attributes);
+}
+
+/*
+ * Internal attributes
+ */
+
+static int
+mailbox_attribute_internal_cmp(
+ const struct mailbox_attribute_internal *reg1,
+ const struct mailbox_attribute_internal *reg2)
+{
+ if (reg1->type != reg2->type)
+ return (int)reg1->type - (int)reg2->type;
+ return strcmp(reg1->key, reg2->key);
+}
+
+void mailbox_attribute_register_internal(
+ const struct mailbox_attribute_internal *iattr)
+{
+ struct mailbox_attribute_internal ireg;
+ unsigned int insert_idx;
+
+ /* Validated attributes must have a set() callback that validates the
+ provided values. Also read-only _RANK_AUTHORITY attributes don't
+ need validation. */
+ i_assert((iattr->flags & MAIL_ATTRIBUTE_INTERNAL_FLAG_VALIDATED) == 0 ||
+ iattr->set != NULL ||
+ iattr->rank == MAIL_ATTRIBUTE_INTERNAL_RANK_AUTHORITY);
+
+ (void)array_bsearch_insert_pos(&mailbox_internal_attributes,
+ iattr, mailbox_attribute_internal_cmp, &insert_idx);
+
+ ireg = *iattr;
+ ireg.key = p_strdup(mailbox_attribute_pool, iattr->key);
+ array_insert(&mailbox_internal_attributes, insert_idx, &ireg, 1);
+}
+
+void mailbox_attribute_register_internals(
+ const struct mailbox_attribute_internal *iattrs, unsigned int count)
+{
+ unsigned int i;
+
+ for (i = 0; i < count; i++)
+ mailbox_attribute_register_internal(&iattrs[i]);
+}
+
+void mailbox_attribute_unregister_internal(
+ const struct mailbox_attribute_internal *iattr)
+{
+ unsigned int idx;
+
+ if (!array_bsearch_insert_pos(&mailbox_internal_attributes,
+ iattr, mailbox_attribute_internal_cmp, &idx)) {
+ i_panic("mailbox_attribute_unregister_internal(%s): "
+ "key not found", iattr->key);
+ }
+
+ array_delete(&mailbox_internal_attributes, idx, 1);
+}
+
+void mailbox_attribute_unregister_internals(
+ const struct mailbox_attribute_internal *iattrs, unsigned int count)
+{
+ unsigned int i;
+
+ for (i = 0; i < count; i++)
+ mailbox_attribute_unregister_internal(&iattrs[i]);
+}
+
+static const struct mailbox_attribute_internal *
+mailbox_internal_attribute_get_int(enum mail_attribute_type type_flags,
+ const char *key)
+{
+ const struct mailbox_attribute_internal *iattr;
+ struct mailbox_attribute_internal dreg;
+ unsigned int insert_idx;
+
+ i_zero(&dreg);
+ dreg.type = type_flags & MAIL_ATTRIBUTE_TYPE_MASK;
+ dreg.key = key;
+
+ if (array_bsearch_insert_pos(&mailbox_internal_attributes,
+ &dreg, mailbox_attribute_internal_cmp,
+ &insert_idx)) {
+ /* exact match */
+ return array_idx(&mailbox_internal_attributes, insert_idx);
+ }
+ if (insert_idx == 0) {
+ /* not found at all */
+ return NULL;
+ }
+ iattr = array_idx(&mailbox_internal_attributes, insert_idx-1);
+ if (!str_begins(key, iattr->key)) {
+ /* iattr isn't a prefix of key */
+ return NULL;
+ } else if ((iattr->flags & MAIL_ATTRIBUTE_INTERNAL_FLAG_CHILDREN) != 0) {
+ /* iattr is a prefix of key and it wants to handle the key */
+ return iattr;
+ } else {
+ return NULL;
+ }
+}
+
+static const struct mailbox_attribute_internal *
+mailbox_internal_attribute_get(enum mail_attribute_type type_flags,
+ const char *key)
+{
+ const struct mailbox_attribute_internal *iattr;
+
+ iattr = mailbox_internal_attribute_get_int(type_flags, key);
+ if ((type_flags & MAIL_ATTRIBUTE_TYPE_FLAG_VALIDATED) != 0 &&
+ iattr != NULL &&
+ (iattr->flags & MAIL_ATTRIBUTE_INTERNAL_FLAG_VALIDATED) == 0) {
+ /* only validated attributes can be accessed */
+ iattr = NULL;
+ }
+ return iattr;
+}
+
+static void
+mailbox_internal_attributes_add_prefixes(ARRAY_TYPE(const_string) *attrs,
+ pool_t pool, unsigned int old_count,
+ const char *key)
+{
+ unsigned int new_count;
+
+ if (key[0] == '\0')
+ return;
+ new_count = array_count(attrs);
+ for (unsigned int i = old_count; i < new_count; i++) {
+ const char *old_key = array_idx_elem(attrs, i);
+ const char *prefixed_key;
+
+ if (old_key[0] == '\0')
+ prefixed_key = p_strndup(pool, key, strlen(key)-1);
+ else
+ prefixed_key = p_strconcat(pool, key, old_key, NULL);
+ array_idx_set(attrs, i, &prefixed_key);
+ }
+}
+
+static int
+mailbox_internal_attributes_get(struct mailbox *box,
+ enum mail_attribute_type type_flags, const char *prefix,
+ pool_t attr_pool, bool have_dict, ARRAY_TYPE(const_string) *attrs)
+{
+ const struct mailbox_attribute_internal *regs;
+ struct mailbox_attribute_internal dreg;
+ char *bare_prefix;
+ size_t plen;
+ unsigned int count, i, j;
+ int ret = 0;
+
+ bare_prefix = t_strdup_noconst(prefix);
+ plen = strlen(bare_prefix);
+ if (plen > 0 && bare_prefix[plen-1] == '/') {
+ bare_prefix[plen-1] = '\0';
+ plen--;
+ }
+
+ i_zero(&dreg);
+ dreg.type = type_flags & MAIL_ATTRIBUTE_TYPE_MASK;
+ dreg.key = bare_prefix;
+
+ (void)array_bsearch_insert_pos(&mailbox_internal_attributes,
+ &dreg, mailbox_attribute_internal_cmp, &i);
+
+ /* iterate attributes that might have children whose keys begins with
+ the prefix */
+ regs = array_get(&mailbox_internal_attributes, &count);
+ for (j = i; j > 0; j--) {
+ const struct mailbox_attribute_internal *attr = &regs[j-1];
+
+ if ((attr->flags & MAIL_ATTRIBUTE_INTERNAL_FLAG_CHILDREN) == 0 ||
+ !str_begins(bare_prefix, attr->key))
+ break;
+
+ /* For example: bare_prefix="foo/bar" and attr->key="foo/", so
+ iter() is called with key_prefix="bar". It could add to
+ attrs: { "", "baz" }, which means with the full prefix:
+ { "foo/bar", "foo/bar/baz" } */
+ if (attr->iter != NULL &&
+ attr->iter(box, bare_prefix + strlen(attr->key),
+ attr_pool, attrs) < 0)
+ ret = -1;
+ }
+
+ /* iterate attributes whose key begins with the prefix */
+ for (; i < count; i++) {
+ const char *key = regs[i].key;
+
+ if (regs[i].type != dreg.type)
+ return ret;
+ if ((type_flags & MAIL_ATTRIBUTE_TYPE_FLAG_VALIDATED) != 0 &&
+ (regs[i].flags & MAIL_ATTRIBUTE_INTERNAL_FLAG_VALIDATED) == 0)
+ continue;
+
+ if (plen > 0) {
+ if (strncmp(key, bare_prefix, plen) != 0)
+ return ret;
+ if (key[plen] == '/') {
+ /* remove prefix */
+ key += plen + 1;
+ } else if (key[plen] == '\0') {
+ /* list the key itself, so this becomes an
+ empty key string. it's the same as how the
+ dict backend works too. */
+ key += plen;
+ } else {
+ return ret;
+ }
+ }
+ if (regs[i].iter != NULL) {
+ /* For example: bare_prefix="foo" and
+ attr->key="foo/bar/", so key="bar/". iter() is
+ always called with key_prefix="", so we're also
+ responsible for adding the "bar/" prefix to the
+ attrs that iter() returns. */
+ unsigned int old_count = array_count(attrs);
+ if (regs[i].iter(box, "", attr_pool, attrs) < 0)
+ ret = -1;
+ mailbox_internal_attributes_add_prefixes(attrs, attr_pool,
+ old_count, key);
+ } else if (have_dict || regs[i].rank == MAIL_ATTRIBUTE_INTERNAL_RANK_AUTHORITY)
+ array_push_back(attrs, &key);
+ }
+ return ret;
+}
+
+/*
+ * Attribute API
+ */
+
+static int
+mailbox_attribute_set_common(struct mailbox_transaction_context *t,
+ enum mail_attribute_type type_flags,
+ const char *key,
+ const struct mail_attribute_value *value)
+{
+ enum mail_attribute_type type =
+ type_flags & MAIL_ATTRIBUTE_TYPE_MASK;
+ const struct mailbox_attribute_internal *iattr;
+ int ret;
+
+ iattr = mailbox_internal_attribute_get(type_flags, key);
+
+ /* allow internal server attribute only for inbox */
+ if (iattr != NULL && !t->box->inbox_any &&
+ str_begins(key, MAILBOX_ATTRIBUTE_PREFIX_DOVECOT_PVT_SERVER))
+ iattr = NULL;
+
+ /* handle internal attribute */
+ if (iattr != NULL) {
+ switch (iattr->rank) {
+ case MAIL_ATTRIBUTE_INTERNAL_RANK_DEFAULT:
+ case MAIL_ATTRIBUTE_INTERNAL_RANK_OVERRIDE:
+ /* notify about assignment */
+ if (iattr->set != NULL && iattr->set(t, key, value) < 0)
+ return -1;
+ break;
+ case MAIL_ATTRIBUTE_INTERNAL_RANK_AUTHORITY:
+ if (iattr->set == NULL) {
+ mail_storage_set_error(t->box->storage, MAIL_ERROR_NOTPOSSIBLE, t_strdup_printf(
+ "The /%s/%s attribute cannot be changed",
+ (type == MAIL_ATTRIBUTE_TYPE_SHARED ? "shared" : "private"), key));
+ return -1;
+ }
+ /* assign internal attribute */
+ return iattr->set(t, key, value);
+ default:
+ i_unreached();
+ }
+ /* the value was validated. */
+ type_flags &= ENUM_NEGATE(MAIL_ATTRIBUTE_TYPE_FLAG_VALIDATED);
+ }
+
+ ret = t->box->v.attribute_set(t, type_flags, key, value);
+ return ret;
+}
+
+int mailbox_attribute_set(struct mailbox_transaction_context *t,
+ enum mail_attribute_type type_flags, const char *key,
+ const struct mail_attribute_value *value)
+{
+ return mailbox_attribute_set_common(t, type_flags, key, value);
+}
+
+int mailbox_attribute_unset(struct mailbox_transaction_context *t,
+ enum mail_attribute_type type_flags, const char *key)
+{
+ struct mail_attribute_value value;
+
+ i_zero(&value);
+ return mailbox_attribute_set_common(t, type_flags, key, &value);
+}
+
+int mailbox_attribute_value_to_string(struct mail_storage *storage,
+ const struct mail_attribute_value *value,
+ const char **str_r)
+{
+ string_t *str;
+ const unsigned char *data;
+ size_t size;
+
+ if (value->value_stream == NULL) {
+ *str_r = value->value;
+ return 0;
+ }
+ str = t_str_new(128);
+ i_stream_seek(value->value_stream, 0);
+ while (i_stream_read_more(value->value_stream, &data, &size) > 0) {
+ if (memchr(data, '\0', size) != NULL) {
+ mail_storage_set_error(storage, MAIL_ERROR_PARAMS,
+ "Attribute string value has NULs");
+ return -1;
+ }
+ str_append_data(str, data, size);
+ i_stream_skip(value->value_stream, size);
+ }
+ if (value->value_stream->stream_errno != 0) {
+ mail_storage_set_critical(storage, "read(%s) failed: %s",
+ i_stream_get_name(value->value_stream),
+ i_stream_get_error(value->value_stream));
+ return -1;
+ }
+ i_assert(value->value_stream->eof);
+ *str_r = str_c(str);
+ return 0;
+}
+
+static int
+mailbox_attribute_get_common(struct mailbox *box,
+ enum mail_attribute_type type_flags,
+ const char *key,
+ struct mail_attribute_value *value_r)
+{
+ const struct mailbox_attribute_internal *iattr;
+ int ret;
+
+ iattr = mailbox_internal_attribute_get(type_flags, key);
+
+ /* allow internal server attributes only for the inbox */
+ if (iattr != NULL && !box->inbox_user &&
+ str_begins(key, MAILBOX_ATTRIBUTE_PREFIX_DOVECOT_PVT_SERVER))
+ iattr = NULL;
+
+ /* internal attribute */
+ if (iattr != NULL) {
+ switch (iattr->rank) {
+ case MAIL_ATTRIBUTE_INTERNAL_RANK_OVERRIDE:
+ /* we already checked that this attribute has
+ validated-flag */
+ type_flags &= ENUM_NEGATE(MAIL_ATTRIBUTE_TYPE_FLAG_VALIDATED);
+
+ if (iattr->get == NULL)
+ break;
+ if ((ret = iattr->get(box, key, value_r)) != 0) {
+ if (ret < 0)
+ return -1;
+ value_r->flags |= MAIL_ATTRIBUTE_VALUE_FLAG_READONLY;
+ return 1;
+ }
+ break;
+ case MAIL_ATTRIBUTE_INTERNAL_RANK_DEFAULT:
+ break;
+ case MAIL_ATTRIBUTE_INTERNAL_RANK_AUTHORITY:
+ if ((ret = iattr->get(box, key, value_r)) <= 0)
+ return ret;
+ value_r->flags |= MAIL_ATTRIBUTE_VALUE_FLAG_READONLY;
+ return 1;
+ default:
+ i_unreached();
+ }
+ }
+
+ ret = box->v.attribute_get(box, type_flags, key, value_r);
+ if (ret != 0)
+ return ret;
+
+ /* default entries */
+ if (iattr != NULL) {
+ switch (iattr->rank) {
+ case MAIL_ATTRIBUTE_INTERNAL_RANK_DEFAULT:
+ if (iattr->get == NULL)
+ ret = 0;
+ else {
+ if ((ret = iattr->get(box, key, value_r)) < 0)
+ return ret;
+ }
+ if (ret > 0) {
+ value_r->flags |= MAIL_ATTRIBUTE_VALUE_FLAG_READONLY;
+ return 1;
+ }
+ break;
+ case MAIL_ATTRIBUTE_INTERNAL_RANK_OVERRIDE:
+ break;
+ default:
+ i_unreached();
+ }
+ }
+ return 0;
+}
+
+int mailbox_attribute_get(struct mailbox *box,
+ enum mail_attribute_type type_flags, const char *key,
+ struct mail_attribute_value *value_r)
+{
+ int ret;
+ i_zero(value_r);
+ if ((ret = mailbox_attribute_get_common(box, type_flags, key,
+ value_r)) <= 0)
+ return ret;
+ i_assert(value_r->value != NULL);
+ return 1;
+}
+
+int mailbox_attribute_get_stream(struct mailbox *box,
+ enum mail_attribute_type type_flags,
+ const char *key,
+ struct mail_attribute_value *value_r)
+{
+ int ret;
+
+ i_zero(value_r);
+ value_r->flags |= MAIL_ATTRIBUTE_VALUE_FLAG_INT_STREAMS;
+ if ((ret = mailbox_attribute_get_common(box, type_flags, key,
+ value_r)) <= 0)
+ return ret;
+ i_assert(value_r->value != NULL || value_r->value_stream != NULL);
+ return 1;
+}
+
+struct mailbox_attribute_internal_iter {
+ struct mailbox_attribute_iter iter;
+ pool_t pool;
+
+ ARRAY_TYPE(const_string) extra_attrs;
+ unsigned int extra_attr_idx;
+
+ struct mailbox_attribute_iter *real_iter;
+ bool iter_failed;
+};
+
+struct mailbox_attribute_iter *
+mailbox_attribute_iter_init(struct mailbox *box,
+ enum mail_attribute_type type_flags,
+ const char *prefix)
+{
+ struct mailbox_attribute_internal_iter *intiter;
+ struct mailbox_attribute_iter *iter;
+ ARRAY_TYPE(const_string) extra_attrs;
+ const char *const *attr;
+ pool_t pool;
+ bool have_dict, failed = FALSE;
+
+ iter = box->v.attribute_iter_init(box, type_flags, prefix);
+ i_assert(iter->box != NULL);
+ box->attribute_iter_count++;
+
+ /* check which internal attributes may apply */
+ t_array_init(&extra_attrs, 4);
+ have_dict = box->storage->set->mail_attribute_dict[0] != '\0';
+ pool = pool_alloconly_create("mailbox internal attribute iter", 128);
+ if (mailbox_internal_attributes_get(box, type_flags, prefix, pool,
+ have_dict, &extra_attrs) < 0)
+ failed = TRUE;
+
+ /* any extra internal attributes to add? */
+ if (array_count(&extra_attrs) == 0 && !failed) {
+ /* no */
+ pool_unref(&pool);
+ return iter;
+ }
+
+ /* yes */
+ intiter = p_new(pool, struct mailbox_attribute_internal_iter, 1);
+ intiter->pool = pool;
+ intiter->real_iter = iter;
+ intiter->iter_failed = failed;
+ p_array_init(&intiter->extra_attrs, pool, 4);
+
+ /* copy relevant attributes */
+ array_foreach(&extra_attrs, attr) {
+ /* skip internal server attributes unless we're iterating inbox */
+ if (!box->inbox_user &&
+ strncmp(*attr, MAILBOX_ATTRIBUTE_PREFIX_DOVECOT_PVT_SERVER,
+ strlen(MAILBOX_ATTRIBUTE_PREFIX_DOVECOT_PVT_SERVER)) == 0)
+ continue;
+ array_push_back(&intiter->extra_attrs, attr);
+ }
+ return &intiter->iter;
+}
+
+const char *mailbox_attribute_iter_next(struct mailbox_attribute_iter *iter)
+{
+ struct mailbox_attribute_internal_iter *intiter;
+ const char *const *attrs;
+ unsigned int count, i;
+ const char *result;
+
+ if (iter->box != NULL) {
+ /* no internal attributes to add */
+ return iter->box->v.attribute_iter_next(iter);
+ }
+
+ /* filter out duplicate results */
+ intiter = (struct mailbox_attribute_internal_iter *)iter;
+ attrs = array_get(&intiter->extra_attrs, &count);
+ while ((result = intiter->real_iter->box->
+ v.attribute_iter_next(intiter->real_iter)) != NULL) {
+ for (i = 0; i < count; i++) {
+ if (strcasecmp(attrs[i], result) == 0)
+ break;
+ }
+ if (i == count) {
+ /* return normally */
+ return result;
+ }
+ /* this attribute name is also to be returned as extra;
+ skip now */
+ }
+
+ /* return extra attributes at the end */
+ if (intiter->extra_attr_idx < count)
+ return attrs[intiter->extra_attr_idx++];
+ return NULL;
+}
+
+int mailbox_attribute_iter_deinit(struct mailbox_attribute_iter **_iter)
+{
+ struct mailbox_attribute_iter *iter = *_iter;
+ struct mailbox_attribute_internal_iter *intiter;
+ int ret;
+
+ *_iter = NULL;
+
+ if (iter->box != NULL) {
+ /* not wrapped */
+ i_assert(iter->box->attribute_iter_count > 0);
+ iter->box->attribute_iter_count--;
+ return iter->box->v.attribute_iter_deinit(iter);
+ }
+
+ /* wrapped */
+ intiter = (struct mailbox_attribute_internal_iter *)iter;
+
+ i_assert(intiter->real_iter->box->attribute_iter_count > 0);
+ intiter->real_iter->box->attribute_iter_count--;
+
+ ret = intiter->real_iter->box->v.attribute_iter_deinit(intiter->real_iter);
+ if (intiter->iter_failed)
+ ret = -1;
+ pool_unref(&intiter->pool);
+ return ret;
+}
diff --git a/src/lib-storage/mailbox-attribute.h b/src/lib-storage/mailbox-attribute.h
new file mode 100644
index 0000000..9a70580
--- /dev/null
+++ b/src/lib-storage/mailbox-attribute.h
@@ -0,0 +1,316 @@
+#ifndef MAILBOX_ATTRIBUTE_H
+#define MAILBOX_ATTRIBUTE_H
+
+/*
+ * Attribute Handling in Dovecot
+ * =============================
+ *
+ * What IMAP & doveadm users see gets translated into one of several things
+ * depending on if we're operating on a mailbox or on server metadata (""
+ * mailbox in IMAP parlance). Consider these examples:
+ *
+ * /private/foo
+ * /shared/foo
+ *
+ * Here "foo" can be any RFC defined attribute name, or a vendor-prefixed
+ * non-standard name. (Our vendor prefix is "vendor/vendor.dovecot".)
+ *
+ * In all cases, the "/private" and "/shared" user visible prefixes get
+ * replaced by priv/<GUID> and shared/<GUID>, respectively. (Here, <GUID>
+ * is the GUID of the mailbox with which the attribute is associated.) This
+ * way, attributes for all mailboxes can be stored in a single dict. For
+ * example, the above examples would map to:
+ *
+ * priv/<GUID>/foo
+ * shared/<GUID>/foo
+ *
+ * More concrete examples:
+ *
+ * /private/comment
+ * /private/vendor/vendor.dovecot/abc
+ *
+ * turn into:
+ *
+ * priv/<GUID>/comment
+ * priv/<GUID>/vendor/vendor.dovecot/abc
+ *
+ * Server attributes, that is attributes not associated with a mailbox, are
+ * stored in the INBOX mailbox with a special prefix -
+ * vendor/vendor.dovecot/pvt/server. For example, the server attribute
+ * /private/comment gets mapped to:
+ *
+ * priv/<INBOX GUID>/vendor/vendor.dovecot/pvt/server/comment
+ *
+ * This means that if we set a /private/comment server attribute as well as
+ * /private/comment INBOX mailbox attribute, we'll see the following paths
+ * used in the dict:
+ *
+ * priv/<INBOX GUID>/comment <- mailbox attr
+ * priv/<INBOX GUID>/vendor/vendor.dovecot/pvt/server/comment <- server attr
+ *
+ * The case of vendor specific server attributes is a bit confusing, but
+ * consistent. For example, this server attribute:
+ *
+ * /private/vendor/vendor.dovecot/abc
+ *
+ * It will get mapped to:
+ *
+ * priv/<INBOX GUID>/vendor/vendor.dovecot/pvt/server/vendor/vendor.dovecot/abc
+ * | | | |
+ * \----- server attr prefix -----/ \-- server attr name ---/
+ *
+ *
+ * Internal Attributes
+ * -------------------
+ *
+ * The final aspect of attribute handling in Dovecot are the so called
+ * "internal attributes".
+ *
+ * The easiest way to explain internal attributes is to summarize attributes
+ * in general. Attributes are just <key,value> pairs that are stored in a
+ * dict. The key is mangled according to the above rules before passed to
+ * the dict code. That is, the key already encodes whether the attribute is
+ * private or shared, the GUID of the mailbox (or of INBOX for server
+ * attributes), etc. There is no processing of the value. It is stored and
+ * returned to clients verbatim.
+ *
+ * Internal attributes, on the other hand, are special cased attributes.
+ * That is, the code contains a list of specific attribute names and how to
+ * handle them. Each internal attribute is defined by a struct
+ * mailbox_attribute_internal. It contains the pre-parsed name of the
+ * attribute (type, key, and flags), and how to handle getting and setting
+ * of the attribute (rank, get, and set).
+ *
+ * The values for these attributes may come from two places - from the
+ * attributes dict, or from the get function pointer. Which source to use
+ * is identified by the rank (MAIL_ATTRIBUTE_INTERNAL_RANK_*).
+ *
+ *
+ * Access
+ * ------
+ *
+ * In general, a user (IMAP or doveadm) can access all attributes for a
+ * mailbox. The one exception are attributes under:
+ *
+ * /private/vendor/vendor.dovecot/pvt
+ * /shared/vendor/vendor.dovecot/pvt
+ *
+ * Which as you may recall map to:
+ *
+ * priv/<GUID>/vendor/vendor.dovecot/pvt
+ * shared/<GUID>/vendor/vendor.dovecot/pvt
+ *
+ * These are deemed internal to Dovecot, and therefore of no concern to the
+ * user.
+ *
+ * Server attributes have a similar restriction. That is, attributes
+ * beginning with the following are not accessible:
+ *
+ * /private/vendor/vendor.dovecot/pvt
+ * /shared/vendor/vendor.dovecot/pvt
+ *
+ * However since server attributes are stored under the INBOX mailbox, these
+ * paths map to:
+ *
+ * priv/<INBOX GUID>/vendor/vendor.dovecot/pvt/server/vendor/vendor.dovecot/pvt
+ * shared/<INBOX GUID>/vendor/vendor.dovecot/pvt/server/vendor/vendor.dovecot/pvt
+ *
+ * As a result, the code performs access checks via the
+ * MAILBOX_ATTRIBUTE_KEY_IS_USER_ACCESSIBLE() macro to make sure that the
+ * user is allowed access to the attribute.
+ *
+ *
+ * Nicknames
+ * ---------
+ *
+ * Since every path stored in the dict begins with priv/<GUID> or
+ * shared/<GUID>, these prefixes are often omitted. This also matches the
+ * internal implementation where the priv/ or shared/ prefix is specified
+ * using an enum, and only the path after the GUID is handled as a string.
+ * For example:
+ *
+ * priv/<GUID>/vendor/vendor.dovecot/pvt/server/foo
+ *
+ * would be referred to as:
+ *
+ * vendor/vendor.dovecot/pvt/server/foo
+ *
+ * Since some of the generated paths are very long, developers often use a
+ * shorthand to refer to some of these paths. For example,
+ *
+ * pvt/server/pvt
+ *
+ * is really:
+ *
+ * vendor/vendor.dovecot/pvt/server/vendor/vendor.dovecot/pvt
+ *
+ * Which when fully specified with a type and INBOX's GUID would turn into
+ * one of the following:
+ *
+ * priv/<GUID>/vendor/vendor.dovecot/pvt/server/vendor/vendor.dovecot/pvt
+ * shared/<GUID>/vendor/vendor.dovecot/pvt/server/vendor/vendor.dovecot/pvt
+ */
+
+struct mailbox;
+struct mailbox_transaction_context;
+
+/* RFC 5464 specifies that this is vendor/<vendor-token>/. The registered
+ vendor-tokens always begin with "vendor." so there's some redundancy.. */
+#define MAILBOX_ATTRIBUTE_PREFIX_DOVECOT "vendor/vendor.dovecot/"
+/* Prefix used for attributes reserved for Dovecot's internal use. Normal
+ users cannot access these in any way. */
+#define MAILBOX_ATTRIBUTE_PREFIX_DOVECOT_PVT \
+ MAILBOX_ATTRIBUTE_PREFIX_DOVECOT"pvt/"
+/* Server attributes are currently stored in INBOX under this private prefix.
+ They're under the pvt/ prefix so they won't be listed as regular INBOX
+ attributes, but unlike other pvt/ attributes it's actually possible to
+ access these attributes as regular users.
+
+ If INBOX is deleted, attributes under this prefix are preserved. */
+#define MAILBOX_ATTRIBUTE_PREFIX_DOVECOT_PVT_SERVER \
+ MAILBOX_ATTRIBUTE_PREFIX_DOVECOT_PVT"server/"
+
+/* User can get/set all non-pvt/ attributes and also pvt/server/
+ (but not pvt/server/pvt/) attributes. */
+#define MAILBOX_ATTRIBUTE_KEY_IS_USER_ACCESSIBLE(key) \
+ (!str_begins(key, MAILBOX_ATTRIBUTE_PREFIX_DOVECOT_PVT) || \
+ (str_begins(key, MAILBOX_ATTRIBUTE_PREFIX_DOVECOT_PVT_SERVER) && \
+ strncmp(key, MAILBOX_ATTRIBUTE_PREFIX_DOVECOT_PVT_SERVER MAILBOX_ATTRIBUTE_PREFIX_DOVECOT_PVT, \
+ strlen(MAILBOX_ATTRIBUTE_PREFIX_DOVECOT_PVT_SERVER MAILBOX_ATTRIBUTE_PREFIX_DOVECOT_PVT)) != 0))
+
+enum mail_attribute_type {
+ MAIL_ATTRIBUTE_TYPE_PRIVATE,
+ MAIL_ATTRIBUTE_TYPE_SHARED
+};
+#define MAIL_ATTRIBUTE_TYPE_MASK 0x0f
+/* Allow accessing only attributes with
+ MAIL_ATTRIBUTE_INTERNAL_FLAG_VALIDATED. */
+#define MAIL_ATTRIBUTE_TYPE_FLAG_VALIDATED 0x80
+
+enum mail_attribute_value_flags {
+ MAIL_ATTRIBUTE_VALUE_FLAG_READONLY = 0x01,
+ MAIL_ATTRIBUTE_VALUE_FLAG_INT_STREAMS = 0x02
+};
+
+struct mail_attribute_value {
+ /* mailbox_attribute_set() can set either value or value_stream.
+ mailbox_attribute_get() returns only values, but
+ mailbox_attribute_get_stream() may return either value or
+ value_stream. The caller must unreference the returned streams. */
+ const char *value;
+ struct istream *value_stream;
+
+ /* Last time the attribute was changed (0 = unknown). This may be
+ returned even for values that don't exist anymore. */
+ time_t last_change;
+
+ enum mail_attribute_value_flags flags;
+};
+
+/*
+ * Internal attribute
+ */
+
+enum mail_attribute_internal_rank {
+ /* The internal attribute serves only as a source for a default value
+ when the normal mailbox attribute storage has no entry for this
+ attribute. Otherwise it is ignored. The `set' function is called
+ only as a notification, not with the intention to store the value.
+ The value is always assigned to the normal mailbox attribute storage.
+ */
+ MAIL_ATTRIBUTE_INTERNAL_RANK_DEFAULT = 0,
+ /* The internal attribute serves as the main source of the attribute
+ value. If the `get' function returns 0, the normal mailbox attribute
+ storage is attempted to obtain the value. The `set' function is
+ called only as a notification, not with the intention to store the
+ value. The value is assigned to the normal mailbox attribute storage.
+ */
+ MAIL_ATTRIBUTE_INTERNAL_RANK_OVERRIDE,
+ /* The value for the internal attribute is never read from the normal
+ mailbox attribute storage. If the `set' function is NULL, the
+ attribute is read-only. If it is not NULL it is used to assign the
+ attribute value; it is not assigned to the normal mailbox attribute
+ storage.
+ */
+ MAIL_ATTRIBUTE_INTERNAL_RANK_AUTHORITY
+};
+
+enum mail_attribute_internal_flags {
+ /* Apply this attribute to the given key and its children. */
+ MAIL_ATTRIBUTE_INTERNAL_FLAG_CHILDREN = 0x01,
+ /* This attribute can be set/get even without generic METADATA support.
+ These attributes don't count towards any quotas either, so the set()
+ callback should validate that the value isn't excessively large. */
+ MAIL_ATTRIBUTE_INTERNAL_FLAG_VALIDATED = 0x02,
+};
+
+struct mailbox_attribute_internal {
+ enum mail_attribute_type type;
+ const char *key; /* relative to the GUID, e.g., "comment" */
+ enum mail_attribute_internal_rank rank;
+ enum mail_attribute_internal_flags flags;
+
+ /* Get the value of this internal attribute */
+ int (*get)(struct mailbox *box, const char *key,
+ struct mail_attribute_value *value_r);
+ /* Set the value of this internal attribute */
+ int (*set)(struct mailbox_transaction_context *t, const char *key,
+ const struct mail_attribute_value *value);
+ /* If non-NULL, the function is responsible for iterating the
+ attribute. Typically this would be used for attributes with
+ MAIL_ATTRIBUTE_INTERNAL_FLAG_CHILDREN to get the children
+ iterated. If key_prefix is "", all keys should be returned.
+ Otherwise only the keys beginning with key_prefix should be
+ returned. The key_prefix is already relative to the
+ mailbox_attribute_internal.key. */
+ int (*iter)(struct mailbox *box, const char *key_prefix,
+ pool_t pool, ARRAY_TYPE(const_string) *keys);
+};
+
+void mailbox_attribute_register_internal(
+ const struct mailbox_attribute_internal *iattr);
+void mailbox_attribute_register_internals(
+ const struct mailbox_attribute_internal *iattrs, unsigned int count);
+
+void mailbox_attribute_unregister_internal(
+ const struct mailbox_attribute_internal *iattr);
+void mailbox_attribute_unregister_internals(
+ const struct mailbox_attribute_internal *iattrs, unsigned int count);
+
+/*
+ * Attribute API
+ */
+
+/* Set mailbox attribute key to value. The key should be compatible with
+ IMAP METADATA, so for Dovecot-specific keys use
+ MAILBOX_ATTRIBUTE_PREFIX_DOVECOT. */
+int mailbox_attribute_set(struct mailbox_transaction_context *t,
+ enum mail_attribute_type type_flags, const char *key,
+ const struct mail_attribute_value *value);
+/* Delete mailbox attribute key. This is just a wrapper to
+ mailbox_attribute_set() with value->value=NULL. */
+int mailbox_attribute_unset(struct mailbox_transaction_context *t,
+ enum mail_attribute_type type_flags, const char *key);
+/* Returns value for mailbox attribute key. Returns 1 if value was returned,
+ 0 if value wasn't found (set to NULL), -1 if error */
+int mailbox_attribute_get(struct mailbox *box,
+ enum mail_attribute_type type_flags, const char *key,
+ struct mail_attribute_value *value_r);
+/* Same as mailbox_attribute_get(), but the returned value may be either an
+ input stream or a string. */
+int mailbox_attribute_get_stream(struct mailbox *box,
+ enum mail_attribute_type type_flags,
+ const char *key,
+ struct mail_attribute_value *value_r);
+
+/* Iterate through mailbox attributes of the given type. The prefix can be used
+ to restrict what attributes are returned. */
+struct mailbox_attribute_iter *
+mailbox_attribute_iter_init(struct mailbox *box,
+ enum mail_attribute_type type_flags,
+ const char *prefix);
+/* Returns the attribute key or NULL if there are no more attributes. */
+const char *mailbox_attribute_iter_next(struct mailbox_attribute_iter *iter);
+int mailbox_attribute_iter_deinit(struct mailbox_attribute_iter **iter);
+
+#endif
diff --git a/src/lib-storage/mailbox-get.c b/src/lib-storage/mailbox-get.c
new file mode 100644
index 0000000..84a14ed
--- /dev/null
+++ b/src/lib-storage/mailbox-get.c
@@ -0,0 +1,239 @@
+/* Copyright (c) 2002-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "mail-index-modseq.h"
+#include "mail-storage-private.h"
+
+void mailbox_get_seq_range(struct mailbox *box, uint32_t uid1, uint32_t uid2,
+ uint32_t *seq1_r, uint32_t *seq2_r)
+{
+ (void)mail_index_lookup_seq_range(box->view, uid1, uid2, seq1_r, seq2_r);
+}
+
+void mailbox_get_uid_range(struct mailbox *box,
+ const ARRAY_TYPE(seq_range) *seqs,
+ ARRAY_TYPE(seq_range) *uids)
+{
+ const struct seq_range *range;
+ unsigned int i, count;
+ uint32_t seq, uid, uid2;
+
+ range = array_get(seqs, &count);
+ for (i = 0; i < count; i++) {
+ if (range[i].seq2 == (uint32_t)-1) {
+ i_assert(i == count-1);
+ mail_index_lookup_uid(box->view,
+ mail_index_view_get_messages_count(box->view),
+ &uid2);
+ if (range[i].seq1 == (uint32_t)-1)
+ uid = uid2;
+ else
+ mail_index_lookup_uid(box->view, range[i].seq1, &uid);
+ seq_range_array_add_range(uids, uid, uid2);
+ break;
+ }
+ for (seq = range[i].seq1; seq <= range[i].seq2; seq++) {
+ mail_index_lookup_uid(box->view, seq, &uid);
+ seq_range_array_add(uids, uid);
+ }
+ }
+}
+
+static void
+add_expunges(ARRAY_TYPE(seq_range) *expunged_uids, uint32_t min_uid,
+ const struct mail_transaction_expunge *src, size_t src_size)
+{
+ const struct mail_transaction_expunge *end;
+
+ end = src + src_size / sizeof(*src);
+ for (; src != end; src++) {
+ if (src->uid2 >= min_uid) {
+ seq_range_array_add_range(expunged_uids,
+ src->uid1, src->uid2);
+ }
+ }
+}
+
+static void
+add_guid_expunges(ARRAY_TYPE(seq_range) *expunged_uids, uint32_t min_uid,
+ const struct mail_transaction_expunge_guid *src,
+ size_t src_size)
+{
+ const struct mail_transaction_expunge_guid *end;
+
+ end = src + src_size / sizeof(*src);
+ for (; src != end; src++) {
+ if (src->uid >= min_uid)
+ seq_range_array_add(expunged_uids, src->uid);
+ }
+}
+
+static int
+mailbox_get_expunges_init(struct mailbox *box, uint64_t prev_modseq,
+ struct mail_transaction_log_view **log_view_r,
+ bool *modseq_too_old_r)
+{
+ struct mail_transaction_log_view *log_view;
+ uint32_t log_seq, tail_seq;
+ uoff_t log_offset;
+ const char *reason;
+ bool reset;
+ int ret;
+
+ *modseq_too_old_r = FALSE;
+
+ if (!mail_index_modseq_get_next_log_offset(box->view, prev_modseq,
+ &log_seq, &log_offset)) {
+ log_seq = 1;
+ log_offset = 0;
+ *modseq_too_old_r = TRUE;
+ }
+ if (log_seq > box->view->log_file_head_seq ||
+ (log_seq == box->view->log_file_head_seq &&
+ log_offset >= box->view->log_file_head_offset)) {
+ /* we haven't seen this high expunges at all */
+ return 1;
+ }
+
+ log_view = mail_transaction_log_view_open(box->index->log);
+ ret = mail_transaction_log_view_set(log_view, log_seq, log_offset,
+ box->view->log_file_head_seq,
+ box->view->log_file_head_offset,
+ &reset, &reason);
+ if (ret == 0) {
+ mail_transaction_log_get_tail(box->index->log, &tail_seq);
+ if (tail_seq <= box->view->log_file_head_seq) {
+ i_assert(tail_seq > log_seq);
+ ret = mail_transaction_log_view_set(log_view, tail_seq, 0,
+ box->view->log_file_head_seq,
+ box->view->log_file_head_offset,
+ &reset, &reason);
+ }
+ *modseq_too_old_r = TRUE;
+ }
+ if (ret <= 0) {
+ mail_transaction_log_view_close(&log_view);
+ return -1;
+ }
+
+ *log_view_r = log_view;
+ return 0;
+}
+
+static void
+mailbox_get_expunged_guids(struct mail_transaction_log_view *log_view,
+ ARRAY_TYPE(seq_range) *expunged_uids,
+ ARRAY_TYPE(mailbox_expunge_rec) *expunges)
+{
+ const struct mail_transaction_header *thdr;
+ const void *tdata;
+ const struct mail_transaction_expunge_guid *rec, *end;
+ struct mailbox_expunge_rec *expunge;
+ struct seq_range_iter iter;
+ unsigned int n;
+ uint32_t uid;
+
+ while (mail_transaction_log_view_next(log_view, &thdr, &tdata) > 0) {
+ if ((thdr->type & MAIL_TRANSACTION_TYPE_MASK) !=
+ MAIL_TRANSACTION_EXPUNGE_GUID)
+ continue;
+
+ rec = tdata;
+ end = rec + thdr->size / sizeof(*rec);
+ for (; rec != end; rec++) {
+ if (!seq_range_exists(expunged_uids, rec->uid))
+ continue;
+ seq_range_array_remove(expunged_uids, rec->uid);
+
+ expunge = array_append_space(expunges);
+ expunge->uid = rec->uid;
+ memcpy(expunge->guid_128, rec->guid_128,
+ sizeof(expunge->guid_128));
+ }
+ }
+
+ /* everything left in expunged_uids didn't get a GUID */
+ seq_range_array_iter_init(&iter, expunged_uids); n = 0;
+ while (seq_range_array_iter_nth(&iter, n++, &uid)) {
+ expunge = array_append_space(expunges);
+ expunge->uid = uid;
+ }
+}
+
+static bool ATTR_NULL(4, 5)
+mailbox_get_expunges_full(struct mailbox *box, uint64_t prev_modseq,
+ const ARRAY_TYPE(seq_range) *uids_filter,
+ ARRAY_TYPE(seq_range) *expunged_uids,
+ ARRAY_TYPE(mailbox_expunge_rec) *expunges)
+{
+ struct mail_transaction_log_view *log_view;
+ ARRAY_TYPE(seq_range) tmp_expunged_uids = ARRAY_INIT;
+ const struct mail_transaction_header *thdr;
+ const struct seq_range *range;
+ const void *tdata;
+ uint32_t min_uid;
+ bool modseq_too_old;
+ int ret;
+
+ i_assert(array_count(uids_filter) > 0);
+ i_assert(expunged_uids == NULL || expunges == NULL);
+
+ ret = mailbox_get_expunges_init(box, prev_modseq, &log_view, &modseq_too_old);
+ if (ret != 0)
+ return ret > 0;
+
+ range = array_front(uids_filter);
+ min_uid = range->seq1;
+
+ /* first get UIDs of all actual expunges */
+ if (expunged_uids == NULL) {
+ i_array_init(&tmp_expunged_uids, 64);
+ expunged_uids = &tmp_expunged_uids;
+ }
+ mail_transaction_log_view_mark(log_view);
+ while ((ret = mail_transaction_log_view_next(log_view,
+ &thdr, &tdata)) > 0) {
+ if ((thdr->type & MAIL_TRANSACTION_EXTERNAL) == 0) {
+ /* skip expunge requests */
+ continue;
+ }
+ switch (thdr->type & MAIL_TRANSACTION_TYPE_MASK) {
+ case MAIL_TRANSACTION_EXPUNGE:
+ add_expunges(expunged_uids, min_uid, tdata, thdr->size);
+ break;
+ case MAIL_TRANSACTION_EXPUNGE_GUID:
+ add_guid_expunges(expunged_uids, min_uid,
+ tdata, thdr->size);
+ break;
+ }
+ }
+ mail_transaction_log_view_rewind(log_view);
+
+ /* drop UIDs that don't match the filter */
+ seq_range_array_intersect(expunged_uids, uids_filter);
+
+ if (expunges != NULL) {
+ mailbox_get_expunged_guids(log_view, expunged_uids, expunges);
+ array_free(&tmp_expunged_uids);
+ }
+
+ mail_transaction_log_view_close(&log_view);
+ return ret < 0 || modseq_too_old ? FALSE : TRUE;
+}
+
+bool mailbox_get_expunges(struct mailbox *box, uint64_t prev_modseq,
+ const ARRAY_TYPE(seq_range) *uids_filter,
+ ARRAY_TYPE(mailbox_expunge_rec) *expunges)
+{
+ return mailbox_get_expunges_full(box, prev_modseq,
+ uids_filter, NULL, expunges);
+}
+
+bool mailbox_get_expunged_uids(struct mailbox *box, uint64_t prev_modseq,
+ const ARRAY_TYPE(seq_range) *uids_filter,
+ ARRAY_TYPE(seq_range) *expunged_uids)
+{
+ return mailbox_get_expunges_full(box, prev_modseq,
+ uids_filter, expunged_uids, NULL);
+}
diff --git a/src/lib-storage/mailbox-guid-cache.c b/src/lib-storage/mailbox-guid-cache.c
new file mode 100644
index 0000000..e35a358
--- /dev/null
+++ b/src/lib-storage/mailbox-guid-cache.c
@@ -0,0 +1,120 @@
+/* Copyright (c) 2005-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "ioloop.h"
+#include "hash.h"
+#include "mail-storage-private.h"
+#include "mailbox-list-private.h"
+#include "mailbox-guid-cache.h"
+
+struct mailbox_guid_cache_rec {
+ guid_128_t guid;
+ const char *vname;
+};
+
+int mailbox_guid_cache_find(struct mailbox_list *list,
+ const guid_128_t guid, const char **vname_r)
+{
+ const struct mailbox_guid_cache_rec *rec;
+ const uint8_t *guid_p = guid;
+
+ if (!hash_table_is_created(list->guid_cache) ||
+ list->guid_cache_invalidated) {
+ mailbox_guid_cache_refresh(list);
+ rec = hash_table_lookup(list->guid_cache, guid_p);
+ } else {
+ rec = hash_table_lookup(list->guid_cache, guid_p);
+ if (rec == NULL && list->guid_cache_updated) {
+ mailbox_guid_cache_refresh(list);
+ rec = hash_table_lookup(list->guid_cache, guid_p);
+ }
+ }
+ if (rec == NULL) {
+ *vname_r = NULL;
+ return list->guid_cache_errors ? -1 : 0;
+ }
+ *vname_r = rec->vname;
+ return 0;
+}
+
+static void mailbox_guid_cache_add_mailbox(struct mailbox_list *list,
+ const struct mailbox_info *info)
+{
+ struct mailbox *box;
+ struct mailbox_metadata metadata;
+ struct mailbox_guid_cache_rec *rec;
+ uint8_t *guid_p;
+
+ box = mailbox_alloc(list, info->vname, 0);
+ if (mailbox_get_metadata(box, MAILBOX_METADATA_GUID,
+ &metadata) < 0) {
+ e_error(box->event, "Couldn't get mailbox GUID: %s",
+ mailbox_get_last_internal_error(box, NULL));
+ list->guid_cache_errors = TRUE;
+ } else if ((rec = hash_table_lookup(list->guid_cache,
+ (const uint8_t *)metadata.guid)) != NULL) {
+ e_warning(list->ns->user->event,
+ "Mailbox %s has duplicate GUID with %s: %s",
+ info->vname, rec->vname,
+ guid_128_to_string(metadata.guid));
+ } else {
+ rec = p_new(list->guid_cache_pool,
+ struct mailbox_guid_cache_rec, 1);
+ memcpy(rec->guid, metadata.guid, sizeof(rec->guid));
+ rec->vname = p_strdup(list->guid_cache_pool, info->vname);
+ guid_p = rec->guid;
+ hash_table_insert(list->guid_cache, guid_p, rec);
+ }
+ mailbox_free(&box);
+}
+
+void mailbox_guid_cache_refresh(struct mailbox_list *list)
+{
+ struct mailbox_list_iterate_context *ctx;
+ const struct mailbox_info *info;
+
+ if (!hash_table_is_created(list->guid_cache)) {
+ list->guid_cache_pool =
+ pool_alloconly_create("guid cache", 1024*16);
+ hash_table_create(&list->guid_cache, list->guid_cache_pool, 0,
+ guid_128_hash, guid_128_cmp);
+ } else {
+ hash_table_clear(list->guid_cache, TRUE);
+ p_clear(list->guid_cache_pool);
+ }
+ list->guid_cache_invalidated = FALSE;
+ list->guid_cache_updated = FALSE;
+ list->guid_cache_errors = FALSE;
+
+ ctx = mailbox_list_iter_init(list, "*",
+ MAILBOX_LIST_ITER_SKIP_ALIASES |
+ MAILBOX_LIST_ITER_NO_AUTO_BOXES);
+ while ((info = mailbox_list_iter_next(ctx)) != NULL) {
+ if ((info->flags &
+ (MAILBOX_NOSELECT | MAILBOX_NONEXISTENT)) != 0)
+ continue;
+ T_BEGIN {
+ mailbox_guid_cache_add_mailbox(list, info);
+ } T_END;
+ }
+ if ((list->ns->prefix_len > 0) && !mail_namespace_prefix_is_inbox(list->ns)) {
+ /* Also check if namespace prefix is a selectable mailbox
+ and add it to cache. Does not need to include INBOX since
+ it is added separately by mailbox_list_iter_init above. */
+ const char *ns_vname = t_strndup(list->ns->prefix,
+ list->ns->prefix_len-1);
+ const struct mailbox_info ns_info = {
+ .vname = ns_vname,
+ .ns = list->ns,
+ };
+ struct mailbox *box = mailbox_alloc(list, ns_vname, 0);
+ enum mailbox_existence existence;
+ if (mailbox_exists(box, FALSE, &existence) == 0 &&
+ existence == MAILBOX_EXISTENCE_SELECT)
+ mailbox_guid_cache_add_mailbox(list, &ns_info);
+ mailbox_free(&box);
+ }
+
+ if (mailbox_list_iter_deinit(&ctx) < 0)
+ list->guid_cache_errors = TRUE;
+}
diff --git a/src/lib-storage/mailbox-guid-cache.h b/src/lib-storage/mailbox-guid-cache.h
new file mode 100644
index 0000000..6d445f8
--- /dev/null
+++ b/src/lib-storage/mailbox-guid-cache.h
@@ -0,0 +1,8 @@
+#ifndef MAILBOX_GUID_CACHE_H
+#define MAILBOX_GUID_CACHE_H
+
+int mailbox_guid_cache_find(struct mailbox_list *list, const guid_128_t guid,
+ const char **vname_r);
+void mailbox_guid_cache_refresh(struct mailbox_list *list);
+
+#endif
diff --git a/src/lib-storage/mailbox-header.c b/src/lib-storage/mailbox-header.c
new file mode 100644
index 0000000..b276fc9
--- /dev/null
+++ b/src/lib-storage/mailbox-header.c
@@ -0,0 +1,113 @@
+/* Copyright (c) 2003-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "sort.h"
+#include "mail-cache.h"
+#include "mail-storage-private.h"
+
+
+static struct mailbox_header_lookup_ctx *
+mailbox_header_lookup_init_real(struct mailbox *box,
+ const char *const headers[])
+{
+ struct mail_cache_field *fields, header_field = {
+ .type = MAIL_CACHE_FIELD_HEADER,
+ .decision = MAIL_CACHE_DECISION_NO,
+ };
+ struct mailbox_header_lookup_ctx *ctx;
+ const char *const *name;
+ const char **sorted_headers, **dest_name;
+ pool_t pool;
+ unsigned int i, j, count;
+
+ i_assert(*headers != NULL);
+
+ for (count = 0, name = headers; *name != NULL; name++)
+ count++;
+
+ /* @UNSAFE: headers need to be sorted for filter stream. */
+ sorted_headers = t_new(const char *, count);
+ memcpy(sorted_headers, headers, count * sizeof(*sorted_headers));
+ i_qsort(sorted_headers, count, sizeof(*sorted_headers), i_strcasecmp_p);
+ headers = sorted_headers;
+
+ /* @UNSAFE */
+ fields = t_new(struct mail_cache_field, count);
+ for (i = j = 0; i < count; i++) {
+ if (i > 0 && strcasecmp(headers[i-1], headers[i]) == 0)
+ continue;
+ header_field.name = t_strconcat("hdr.", headers[i], NULL);
+ fields[j++] = header_field;
+ }
+ count = j;
+ mail_cache_register_fields(box->cache, fields, count);
+
+ pool = pool_alloconly_create("mailbox_header_lookup_ctx", 1024);
+ ctx = p_new(pool, struct mailbox_header_lookup_ctx, 1);
+ ctx->box = box;
+ ctx->refcount = 1;
+ ctx->pool = pool;
+ ctx->count = count;
+
+ ctx->idx = p_new(pool, unsigned int, count);
+
+ /* @UNSAFE */
+ dest_name = p_new(pool, const char *, count + 1);
+ for (i = 0; i < count; i++) {
+ ctx->idx[i] = fields[i].idx;
+ dest_name[i] = p_strdup(pool, fields[i].name + strlen("hdr."));
+ }
+ ctx->name = dest_name;
+ return ctx;
+}
+
+struct mailbox_header_lookup_ctx *
+mailbox_header_lookup_init(struct mailbox *box, const char *const headers[])
+{
+ struct mailbox_header_lookup_ctx *ctx;
+
+ T_BEGIN {
+ ctx = mailbox_header_lookup_init_real(box, headers);
+ } T_END;
+ return ctx;
+}
+
+void mailbox_header_lookup_ref(struct mailbox_header_lookup_ctx *ctx)
+{
+ i_assert(ctx->refcount > 0);
+ ctx->refcount++;
+}
+
+void mailbox_header_lookup_unref(struct mailbox_header_lookup_ctx **_ctx)
+{
+ struct mailbox_header_lookup_ctx *ctx = *_ctx;
+
+ if (ctx == NULL)
+ return;
+
+ *_ctx = NULL;
+
+ i_assert(ctx->refcount > 0);
+ if (--ctx->refcount > 0)
+ return;
+
+ pool_unref(&ctx->pool);
+}
+
+struct mailbox_header_lookup_ctx *
+mailbox_header_lookup_merge(const struct mailbox_header_lookup_ctx *hdr1,
+ const struct mailbox_header_lookup_ctx *hdr2)
+{
+ ARRAY_TYPE(const_string) names;
+ unsigned int i;
+
+ i_assert(hdr1->box == hdr2->box);
+
+ t_array_init(&names, 32);
+ for (i = 0; i < hdr1->count; i++)
+ array_push_back(&names, &hdr1->name[i]);
+ for (i = 0; i < hdr2->count; i++)
+ array_push_back(&names, &hdr2->name[i]);
+ array_append_zero(&names);
+ return mailbox_header_lookup_init(hdr1->box, array_front(&names));
+}
diff --git a/src/lib-storage/mailbox-keywords.c b/src/lib-storage/mailbox-keywords.c
new file mode 100644
index 0000000..78e8529
--- /dev/null
+++ b/src/lib-storage/mailbox-keywords.c
@@ -0,0 +1,148 @@
+/* Copyright (c) 2002-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "imap-arg.h"
+#include "mail-storage-private.h"
+
+static struct mail_keywords *
+mailbox_keywords_create_skip(struct mailbox *box,
+ const char *const keywords[])
+{
+ struct mail_keywords *kw;
+
+ T_BEGIN {
+ ARRAY(const char *) valid_keywords;
+ const char *error;
+
+ t_array_init(&valid_keywords, 32);
+ for (; *keywords != NULL; keywords++) {
+ if (mailbox_keyword_is_valid(box, *keywords, &error))
+ array_push_back(&valid_keywords, keywords);
+ }
+ array_append_zero(&valid_keywords); /* NULL-terminate */
+ kw = mail_index_keywords_create(box->index, keywords);
+ } T_END;
+ return kw;
+}
+
+static bool
+mailbox_keywords_are_valid(struct mailbox *box, const char *const keywords[],
+ const char **error_r)
+{
+ unsigned int i;
+
+ for (i = 0; keywords[i] != NULL; i++) {
+ if (!mailbox_keyword_is_valid(box, keywords[i], error_r))
+ return FALSE;
+ }
+ return TRUE;
+}
+
+int mailbox_keywords_create(struct mailbox *box, const char *const keywords[],
+ struct mail_keywords **keywords_r)
+{
+ const char *error, *empty_keyword_list = NULL;
+
+ i_assert(box->opened);
+
+ if (keywords == NULL)
+ keywords = &empty_keyword_list;
+ if (!mailbox_keywords_are_valid(box, keywords, &error)) {
+ mail_storage_set_error(box->storage, MAIL_ERROR_PARAMS, error);
+ return -1;
+ }
+
+ *keywords_r = mail_index_keywords_create(box->index, keywords);
+ return 0;
+}
+
+struct mail_keywords *
+mailbox_keywords_create_valid(struct mailbox *box,
+ const char *const keywords[])
+{
+ const char *empty_keyword_list = NULL;
+ const char *error;
+
+ i_assert(box->opened);
+
+ if (keywords == NULL)
+ keywords = &empty_keyword_list;
+ if (mailbox_keywords_are_valid(box, keywords, &error))
+ return mail_index_keywords_create(box->index, keywords);
+ else {
+ /* found invalid keywords, do this the slow way */
+ return mailbox_keywords_create_skip(box, keywords);
+ }
+}
+
+struct mail_keywords *
+mailbox_keywords_create_from_indexes(struct mailbox *box,
+ const ARRAY_TYPE(keyword_indexes) *idx)
+{
+ i_assert(box->opened);
+
+ return mail_index_keywords_create_from_indexes(box->index, idx);
+}
+
+struct mail_keywords *mailbox_keywords_merge(struct mail_keywords *keywords1,
+ struct mail_keywords *keywords2)
+{
+ ARRAY_TYPE(keyword_indexes) keywords_merged;
+
+ i_assert(keywords1->index == keywords2->index);
+
+ t_array_init(&keywords_merged, keywords1->count + keywords2->count);
+ /* duplicates are dropped by mail_index_keywords_create() */
+ array_append(&keywords_merged, keywords1->idx, keywords1->count);
+ array_append(&keywords_merged, keywords2->idx, keywords2->count);
+ return mail_index_keywords_create_from_indexes(keywords1->index,
+ &keywords_merged);
+}
+
+void mailbox_keywords_ref(struct mail_keywords *keywords)
+{
+ mail_index_keywords_ref(keywords);
+}
+
+void mailbox_keywords_unref(struct mail_keywords **keywords)
+{
+ mail_index_keywords_unref(keywords);
+}
+
+bool mailbox_keyword_is_valid(struct mailbox *box, const char *keyword,
+ const char **error_r)
+{
+ unsigned int i, idx;
+
+ i_assert(box->opened);
+
+ /* if it already exists, skip validity checks */
+ if (mail_index_keyword_lookup(box->index, keyword, &idx))
+ return TRUE;
+
+ if (*keyword == '\0') {
+ *error_r = "Empty keywords not allowed";
+ return FALSE;
+ }
+ if (box->disallow_new_keywords) {
+ *error_r = "Can't create new keywords";
+ return FALSE;
+ }
+
+ /* these are IMAP-specific restrictions, but for now IMAP is all we
+ care about */
+ for (i = 0; keyword[i] != '\0'; i++) {
+ if (!IS_ATOM_CHAR(keyword[i])) {
+ if ((unsigned char)keyword[i] < 0x80)
+ *error_r = "Invalid characters in keyword";
+ else
+ *error_r = "8bit characters in keyword";
+ return FALSE;
+ }
+ }
+ if (i > box->storage->set->mail_max_keyword_length) {
+ *error_r = "Keyword length too long";
+ return FALSE;
+ }
+ return TRUE;
+}
diff --git a/src/lib-storage/mailbox-list-iter.h b/src/lib-storage/mailbox-list-iter.h
new file mode 100644
index 0000000..ec60313
--- /dev/null
+++ b/src/lib-storage/mailbox-list-iter.h
@@ -0,0 +1,89 @@
+#ifndef MAILBOX_LIST_ITER_H
+#define MAILBOX_LIST_ITER_H
+
+#include "mail-namespace.h"
+#include "mailbox-list.h"
+
+enum mailbox_list_iter_flags {
+ /* Ignore index file and ACLs (used by ACL plugin internally) */
+ MAILBOX_LIST_ITER_RAW_LIST = 0x000001,
+ /* Don't list autocreated mailboxes (e.g. INBOX) unless they
+ physically exist */
+ MAILBOX_LIST_ITER_NO_AUTO_BOXES = 0x000004,
+
+ /* Skip all kinds of mailbox aliases. This typically includes symlinks
+ that point to the same directory. Also when iterating with
+ mailbox_list_iter_init_namespaces() skip namespaces that
+ have alias_for set. */
+ MAILBOX_LIST_ITER_SKIP_ALIASES = 0x000008,
+ /* For mailbox_list_iter_init_namespaces(): '*' in a pattern doesn't
+ match beyond namespace boundary (e.g. "foo*" or "*o" doesn't match
+ "foo." namespace's mailboxes, but "*.*" does). also '%' can't match
+ namespace prefixes, if there exists a parent namespace whose children
+ it matches. */
+ MAILBOX_LIST_ITER_STAR_WITHIN_NS = 0x000010,
+
+ /* List only subscribed mailboxes */
+ MAILBOX_LIST_ITER_SELECT_SUBSCRIBED = 0x000100,
+ /* Return MAILBOX_CHILD_* if mailbox's children match selection
+ criteria, even if the mailbox itself wouldn't match. */
+ MAILBOX_LIST_ITER_SELECT_RECURSIVEMATCH = 0x000200,
+ /* Return only mailboxes that have special use flags */
+ MAILBOX_LIST_ITER_SELECT_SPECIALUSE = 0x000400,
+
+ /* Don't return any flags unless it can be done without cost */
+ MAILBOX_LIST_ITER_RETURN_NO_FLAGS = 0x001000,
+ /* Return MAILBOX_SUBSCRIBED flag */
+ MAILBOX_LIST_ITER_RETURN_SUBSCRIBED = 0x002000,
+ /* Return children flags */
+ MAILBOX_LIST_ITER_RETURN_CHILDREN = 0x004000,
+ /* Return IMAP special use flags */
+ MAILBOX_LIST_ITER_RETURN_SPECIALUSE = 0x008000
+};
+
+struct mailbox_info {
+ const char *vname;
+ const char *special_use;
+ enum mailbox_info_flags flags;
+
+ struct mail_namespace *ns;
+};
+
+/* Returns a single pattern from given reference and pattern. */
+const char *mailbox_list_join_refpattern(struct mailbox_list *list,
+ const char *ref, const char *pattern);
+
+/* Initialize new mailbox list request. Pattern may contain '%' and '*'
+ wildcards as defined by RFC-3501. */
+struct mailbox_list_iterate_context *
+mailbox_list_iter_init(struct mailbox_list *list, const char *pattern,
+ enum mailbox_list_iter_flags flags);
+/* Like mailbox_list_iter_init(), but support multiple patterns. Patterns is
+ a NULL-terminated list of strings. It must contain at least one pattern. */
+struct mailbox_list_iterate_context *
+mailbox_list_iter_init_multiple(struct mailbox_list *list,
+ const char *const *patterns,
+ enum mailbox_list_iter_flags flags);
+/* Like mailbox_list_iter_init_multiple(), but list mailboxes from all the
+ specified namespaces. If it fails, the error message is set to the first
+ namespaces->list. */
+struct mailbox_list_iterate_context *
+mailbox_list_iter_init_namespaces(struct mail_namespace *namespaces,
+ const char *const *patterns,
+ enum mail_namespace_type type_mask,
+ enum mailbox_list_iter_flags flags);
+/* Get next mailbox. Returns the mailbox name */
+const struct mailbox_info *
+mailbox_list_iter_next(struct mailbox_list_iterate_context *ctx);
+/* Deinitialize mailbox list request. Returns -1 if some error
+ occurred while listing. The error string can be looked up with
+ mailbox_list_get_last_error(). */
+int mailbox_list_iter_deinit(struct mailbox_list_iterate_context **ctx);
+/* List one mailbox. Returns 1 if info returned, 0 if mailbox doesn't exist,
+ -1 if error. */
+int mailbox_list_mailbox(struct mailbox_list *list, const char *name,
+ enum mailbox_info_flags *flags_r);
+/* Returns 1 if mailbox has children, 0 if not, -1 if error. */
+int mailbox_has_children(struct mailbox_list *list, const char *name);
+
+#endif
diff --git a/src/lib-storage/mailbox-list-notify.c b/src/lib-storage/mailbox-list-notify.c
new file mode 100644
index 0000000..9ca83ac
--- /dev/null
+++ b/src/lib-storage/mailbox-list-notify.c
@@ -0,0 +1,42 @@
+/* Copyright (c) 2013-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "mailbox-list-private.h"
+#include "mailbox-list-notify.h"
+
+int mailbox_list_notify_init(struct mailbox_list *list,
+ enum mailbox_list_notify_event mask,
+ struct mailbox_list_notify **notify_r)
+{
+ if (list->v.notify_init == NULL)
+ return -1;
+ return list->v.notify_init(list, mask, notify_r);
+}
+
+void mailbox_list_notify_deinit(struct mailbox_list_notify **_notify)
+{
+ struct mailbox_list_notify *notify = *_notify;
+
+ *_notify = NULL;
+
+ notify->list->v.notify_deinit(notify);
+}
+
+int mailbox_list_notify_next(struct mailbox_list_notify *notify,
+ const struct mailbox_list_notify_rec **rec_r)
+{
+ return notify->list->v.notify_next(notify, rec_r);
+}
+
+#undef mailbox_list_notify_wait
+void mailbox_list_notify_wait(struct mailbox_list_notify *notify,
+ void (*callback)(void *context), void *context)
+{
+ notify->list->v.notify_wait(notify, callback, context);
+}
+
+void mailbox_list_notify_flush(struct mailbox_list_notify *notify)
+{
+ if (notify->list->v.notify_flush != NULL)
+ notify->list->v.notify_flush(notify);
+}
diff --git a/src/lib-storage/mailbox-list-notify.h b/src/lib-storage/mailbox-list-notify.h
new file mode 100644
index 0000000..ed8f0e5
--- /dev/null
+++ b/src/lib-storage/mailbox-list-notify.h
@@ -0,0 +1,67 @@
+#ifndef MAILBOX_LIST_NOTIFY_H
+#define MAILBOX_LIST_NOTIFY_H
+
+#include "guid.h"
+
+struct mailbox_list_notify;
+
+enum mailbox_list_notify_event {
+ MAILBOX_LIST_NOTIFY_CREATE = 0x01,
+ MAILBOX_LIST_NOTIFY_DELETE = 0x02,
+ MAILBOX_LIST_NOTIFY_RENAME = 0x04,
+ MAILBOX_LIST_NOTIFY_SUBSCRIBE = 0x08,
+ MAILBOX_LIST_NOTIFY_UNSUBSCRIBE = 0x10,
+
+ MAILBOX_LIST_NOTIFY_UIDVALIDITY = 0x20,
+ MAILBOX_LIST_NOTIFY_APPENDS = 0x40,
+ MAILBOX_LIST_NOTIFY_EXPUNGES = 0x80,
+ MAILBOX_LIST_NOTIFY_SEEN_CHANGES = 0x100,
+ MAILBOX_LIST_NOTIFY_MODSEQ_CHANGES = 0x200
+#define MAILBOX_LIST_NOTIFY_STATUS \
+ (MAILBOX_LIST_NOTIFY_APPENDS | \
+ MAILBOX_LIST_NOTIFY_EXPUNGES | \
+ MAILBOX_LIST_NOTIFY_SEEN_CHANGES | \
+ MAILBOX_LIST_NOTIFY_MODSEQ_CHANGES)
+};
+
+struct mailbox_list_notify {
+ struct mailbox_list *list;
+ enum mailbox_list_notify_event mask;
+};
+
+struct mailbox_list_notify_rec {
+ /* Each record can contain multiple events */
+ enum mailbox_list_notify_event events;
+
+ /* For all events: */
+ const char *storage_name, *vname;
+ /* For selectable mailboxes: */
+ guid_128_t guid;
+
+ /* For rename: */
+ const char *old_vname;
+};
+
+typedef void mailbox_list_notify_callback_t(void *);
+
+/* Monitor for specified changes in the mailbox list.
+ Returns 0 if ok, -1 if notifications aren't supported. */
+int mailbox_list_notify_init(struct mailbox_list *list,
+ enum mailbox_list_notify_event mask,
+ struct mailbox_list_notify **notify_r);
+void mailbox_list_notify_deinit(struct mailbox_list_notify **notify);
+
+/* Get the next change. Returns 1 if record was returned, 0 if there are no
+ more changes currently or -1 if some error occurred */
+int mailbox_list_notify_next(struct mailbox_list_notify *notify,
+ const struct mailbox_list_notify_rec **rec_r);
+/* Call the specified callback when something changes. */
+void mailbox_list_notify_wait(struct mailbox_list_notify *notify,
+ mailbox_list_notify_callback_t *callback, void *context);
+#define mailbox_list_notify_wait(notify, callback, context) \
+ mailbox_list_notify_wait(notify - CALLBACK_TYPECHECK(callback, void (*)(typeof(context))), \
+ (mailbox_list_notify_callback_t*)callback, context);
+/* Flush any delayed notifications now. */
+void mailbox_list_notify_flush(struct mailbox_list_notify *notify);
+
+#endif
diff --git a/src/lib-storage/mailbox-list-private.h b/src/lib-storage/mailbox-list-private.h
new file mode 100644
index 0000000..1454db3
--- /dev/null
+++ b/src/lib-storage/mailbox-list-private.h
@@ -0,0 +1,249 @@
+#ifndef MAILBOX_LIST_PRIVATE_H
+#define MAILBOX_LIST_PRIVATE_H
+
+#include "mailbox-log.h"
+#include "mailbox-list-notify.h"
+#include "mail-namespace.h"
+#include "mailbox-list.h"
+#include "mailbox-list-iter.h"
+#include "mail-storage-settings.h"
+
+#define MAILBOX_LIST_NAME_MAILDIRPLUSPLUS "maildir++"
+#define MAILBOX_LIST_NAME_IMAPDIR "imapdir"
+#define MAILBOX_LIST_NAME_FS "fs"
+#define MAILBOX_LIST_NAME_INDEX "index"
+#define MAILBOX_LIST_NAME_NONE "none"
+
+#define MAILBOX_LIST_INDEX_DEFAULT_PREFIX "dovecot.list.index"
+#define MAILBOX_LOG_FILE_NAME "dovecot.mailbox.log"
+
+#define T_MAILBOX_LIST_ERR_NOT_FOUND(list, name) \
+ t_strdup_printf(MAIL_ERRSTR_MAILBOX_NOT_FOUND, \
+ mailbox_list_get_vname(list, name))
+
+struct stat;
+struct dirent;
+struct fs;
+struct imap_match_glob;
+struct mailbox_tree_context;
+struct mailbox_list_notify;
+struct mailbox_list_notify_rec;
+
+#define MAILBOX_INFO_FLAGS_FINISHED(flags) \
+ (((flags) & (MAILBOX_SELECT | MAILBOX_NOSELECT | \
+ MAILBOX_NONEXISTENT)) != 0)
+
+struct mailbox_list_vfuncs {
+ struct mailbox_list *(*alloc)(void);
+ int (*init)(struct mailbox_list *list, const char **error_r);
+ void (*deinit)(struct mailbox_list *list);
+
+ int (*get_storage)(struct mailbox_list **list, const char *vname,
+ struct mail_storage **storage_r);
+
+ char (*get_hierarchy_sep)(struct mailbox_list *list);
+ const char *(*get_vname)(struct mailbox_list *list,
+ const char *storage_name);
+ const char *(*get_storage_name)(struct mailbox_list *list,
+ const char *vname);
+ int (*get_path)(struct mailbox_list *list, const char *name,
+ enum mailbox_list_path_type type, const char **path_r);
+
+ const char *(*get_temp_prefix)(struct mailbox_list *list, bool global);
+ const char *(*join_refpattern)(struct mailbox_list *list,
+ const char *ref, const char *pattern);
+
+ struct mailbox_list_iterate_context *
+ (*iter_init)(struct mailbox_list *list,
+ const char *const *patterns,
+ enum mailbox_list_iter_flags flags);
+ const struct mailbox_info *
+ (*iter_next)(struct mailbox_list_iterate_context *ctx);
+ int (*iter_deinit)(struct mailbox_list_iterate_context *ctx);
+
+ int (*get_mailbox_flags)(struct mailbox_list *list,
+ const char *dir, const char *fname,
+ enum mailbox_list_file_type type,
+ enum mailbox_info_flags *flags_r);
+ /* Returns TRUE if name is mailbox's internal file/directory.
+ If it does, mailbox deletion assumes it can safely delete it. */
+ bool (*is_internal_name)(struct mailbox_list *list, const char *name);
+
+ /* Read subscriptions from src_list, but place them into
+ dest_list->subscriptions. Set errors to dest_list. */
+ int (*subscriptions_refresh)(struct mailbox_list *src_list,
+ struct mailbox_list *dest_list);
+ int (*set_subscribed)(struct mailbox_list *list,
+ const char *name, bool set);
+ int (*delete_mailbox)(struct mailbox_list *list, const char *name);
+ int (*delete_dir)(struct mailbox_list *list, const char *name);
+ int (*delete_symlink)(struct mailbox_list *list, const char *name);
+ int (*rename_mailbox)(struct mailbox_list *oldlist, const char *oldname,
+ struct mailbox_list *newlist, const char *newname);
+
+ int (*notify_init)(struct mailbox_list *list,
+ enum mailbox_list_notify_event mask,
+ struct mailbox_list_notify **notify_r);
+ int (*notify_next)(struct mailbox_list_notify *notify,
+ const struct mailbox_list_notify_rec **rec_r);
+ void (*notify_deinit)(struct mailbox_list_notify *notify);
+ void (*notify_wait)(struct mailbox_list_notify *notify,
+ void (*callback)(void *context), void *context);
+ void (*notify_flush)(struct mailbox_list_notify *notify);
+};
+
+struct mailbox_list_module_register {
+ unsigned int id;
+};
+
+union mailbox_list_module_context {
+ struct mailbox_list_vfuncs super;
+ struct mailbox_list_module_register *reg;
+};
+
+struct mailbox_list {
+ const char *name;
+ enum mailbox_list_properties props;
+ size_t mailbox_name_max_length;
+
+ struct mailbox_list_vfuncs v, *vlast;
+
+/* private: */
+ pool_t pool;
+ struct mail_namespace *ns;
+ struct mailbox_list_settings set;
+ const struct mail_storage_settings *mail_set;
+ enum mailbox_list_flags flags;
+
+ /* may not be set yet, use mailbox_list_get_permissions() to access */
+ struct mailbox_permissions root_permissions;
+
+ struct mailbox_tree_context *subscriptions;
+ time_t subscriptions_mtime, subscriptions_read_time;
+
+ struct mailbox_log *changelog;
+ time_t changelog_timestamp;
+
+ struct file_lock *lock;
+ int lock_refcount;
+
+ pool_t guid_cache_pool;
+ HASH_TABLE(uint8_t *, struct mailbox_guid_cache_rec *) guid_cache;
+ bool guid_cache_errors;
+
+ /* Last error set in mailbox_list_set_critical(). */
+ char *last_internal_error;
+
+ char *error_string;
+ enum mail_error error;
+ bool temporary_error;
+ ARRAY(struct mail_storage_error) error_stack;
+
+ ARRAY(union mailbox_list_module_context *) module_contexts;
+
+ bool index_root_dir_created:1;
+ bool list_index_root_dir_created:1;
+ bool guid_cache_updated:1;
+ bool disable_rebuild_on_corruption:1;
+ bool guid_cache_invalidated:1;
+ bool last_error_is_internal:1;
+};
+
+union mailbox_list_iterate_module_context {
+ struct mailbox_list_module_register *reg;
+};
+
+struct mailbox_list_iterate_context {
+ struct mailbox_list *list;
+ pool_t pool;
+ enum mailbox_list_iter_flags flags;
+ bool failed;
+ bool index_iteration;
+
+ struct imap_match_glob *glob;
+ struct mailbox_list_autocreate_iterate_context *autocreate_ctx;
+ struct mailbox_info specialuse_info;
+
+ ARRAY(union mailbox_list_iterate_module_context *) module_contexts;
+};
+
+struct mailbox_list_iter_update_context {
+ struct mailbox_list_iterate_context *iter_ctx;
+ struct mailbox_tree_context *tree_ctx;
+
+ struct imap_match_glob *glob;
+ enum mailbox_info_flags leaf_flags, parent_flags;
+
+ bool update_only:1;
+ bool match_parents:1;
+};
+
+/* Modules should use do "my_id = mailbox_list_module_id++" and
+ use objects' module_contexts[id] for their own purposes. */
+extern struct mailbox_list_module_register mailbox_list_module_register;
+
+void mailbox_lists_init(void);
+void mailbox_lists_deinit(void);
+
+void mailbox_list_settings_init_defaults(struct mailbox_list_settings *set_r);
+int mailbox_list_settings_parse(struct mail_user *user, const char *data,
+ struct mailbox_list_settings *set_r,
+ const char **error_r);
+const char *
+mailbox_list_escape_name_params(const char *vname, const char *ns_prefix,
+ char ns_sep, char list_sep, char escape_char,
+ const char *maildir_name);
+const char *
+mailbox_list_unescape_name_params(const char *src, const char *ns_prefix,
+ char ns_sep, char list_sep, char escape_char);
+
+const char *mailbox_list_default_get_storage_name(struct mailbox_list *list,
+ const char *vname);
+const char *mailbox_list_default_get_vname(struct mailbox_list *list,
+ const char *storage_name);
+const char *mailbox_list_get_unexpanded_path(struct mailbox_list *list,
+ enum mailbox_list_path_type type);
+bool mailbox_list_set_get_root_path(const struct mailbox_list_settings *set,
+ enum mailbox_list_path_type type,
+ const char **path_r);
+
+int mailbox_list_delete_index_control(struct mailbox_list *list,
+ const char *name);
+
+void mailbox_list_iter_update(struct mailbox_list_iter_update_context *ctx,
+ const char *name);
+int mailbox_list_iter_subscriptions_refresh(struct mailbox_list *list);
+const struct mailbox_info *
+mailbox_list_iter_default_next(struct mailbox_list_iterate_context *ctx);
+
+enum mailbox_list_file_type mailbox_list_get_file_type(const struct dirent *d);
+int mailbox_list_dirent_is_alias_symlink(struct mailbox_list *list,
+ const char *dir_path,
+ const struct dirent *d);
+bool mailbox_list_try_get_absolute_path(struct mailbox_list *list,
+ const char **name);
+void mailbox_permissions_copy(struct mailbox_permissions *dest,
+ const struct mailbox_permissions *src,
+ pool_t pool);
+
+void mailbox_list_add_change(struct mailbox_list *list,
+ enum mailbox_log_record_type type,
+ const guid_128_t guid_128);
+void mailbox_name_get_sha128(const char *name, guid_128_t guid_128_r);
+
+void mailbox_list_clear_error(struct mailbox_list *list);
+void mailbox_list_set_error(struct mailbox_list *list,
+ enum mail_error error, const char *string);
+void mailbox_list_set_critical(struct mailbox_list *list, const char *fmt, ...)
+ ATTR_FORMAT(2, 3);
+void mailbox_list_set_internal_error(struct mailbox_list *list);
+bool mailbox_list_set_error_from_errno(struct mailbox_list *list);
+
+const struct mailbox_info *
+mailbox_list_iter_autocreate_filter(struct mailbox_list_iterate_context *ctx,
+ const struct mailbox_info *_info);
+
+int mailbox_list_lock(struct mailbox_list *list);
+void mailbox_list_unlock(struct mailbox_list *list);
+
+#endif
diff --git a/src/lib-storage/mailbox-list-register.c b/src/lib-storage/mailbox-list-register.c
new file mode 100644
index 0000000..6dc94f3
--- /dev/null
+++ b/src/lib-storage/mailbox-list-register.c
@@ -0,0 +1,26 @@
+/* Copyright (c) 2017-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "mailbox-list.h"
+
+extern struct mailbox_list maildir_mailbox_list;
+extern struct mailbox_list imapdir_mailbox_list;
+extern struct mailbox_list fs_mailbox_list;
+extern struct mailbox_list index_mailbox_list;
+extern struct mailbox_list imapc_mailbox_list;
+extern struct mailbox_list none_mailbox_list;
+extern struct mailbox_list shared_mailbox_list;
+
+void mailbox_list_index_init(void);
+
+void mailbox_list_register_all(void)
+{
+ mailbox_list_register(&maildir_mailbox_list);
+ mailbox_list_register(&imapdir_mailbox_list);
+ mailbox_list_register(&fs_mailbox_list);
+ mailbox_list_register(&index_mailbox_list);
+ mailbox_list_register(&imapc_mailbox_list);
+ mailbox_list_register(&none_mailbox_list);
+ mailbox_list_register(&shared_mailbox_list);
+ mailbox_list_index_init();
+}
diff --git a/src/lib-storage/mailbox-list.c b/src/lib-storage/mailbox-list.c
new file mode 100644
index 0000000..c8f5d44
--- /dev/null
+++ b/src/lib-storage/mailbox-list.c
@@ -0,0 +1,2164 @@
+/* Copyright (c) 2006-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "path-util.h"
+#include "ioloop.h"
+#include "file-create-locked.h"
+#include "mkdir-parents.h"
+#include "hex-binary.h"
+#include "str.h"
+#include "sha1.h"
+#include "hash.h"
+#include "home-expand.h"
+#include "time-util.h"
+#include "unichar.h"
+#include "settings-parser.h"
+#include "iostream-ssl.h"
+#include "fs-api-private.h"
+#include "imap-utf7.h"
+#include "mailbox-log.h"
+#include "mailbox-tree.h"
+#include "mail-storage-private.h"
+#include "mail-storage-hooks.h"
+#include "mailbox-list-private.h"
+
+#include <time.h>
+#include <ctype.h>
+#include <unistd.h>
+#include <dirent.h>
+#include <sys/stat.h>
+
+#define MAILBOX_LIST_LOCK_FNAME "mailboxes.lock"
+#define MAILBOX_LIST_LOCK_SECS 60
+
+#define MAILBOX_LIST_FS_CONTEXT(obj) \
+ MODULE_CONTEXT(obj, mailbox_list_fs_module)
+
+struct mailbox_list_fs_context {
+ union fs_api_module_context module_ctx;
+ struct mailbox_list *list;
+};
+
+struct mailbox_list_module_register mailbox_list_module_register = { 0 };
+
+static ARRAY(const struct mailbox_list *) mailbox_list_drivers;
+static MODULE_CONTEXT_DEFINE_INIT(mailbox_list_fs_module,
+ &fs_api_module_register);
+
+void mailbox_lists_init(void)
+{
+ i_array_init(&mailbox_list_drivers, 4);
+}
+
+void mailbox_lists_deinit(void)
+{
+ array_free(&mailbox_list_drivers);
+}
+
+static bool mailbox_list_driver_find(const char *name, unsigned int *idx_r)
+{
+ const struct mailbox_list *const *drivers;
+ unsigned int i, count;
+
+ drivers = array_get(&mailbox_list_drivers, &count);
+ for (i = 0; i < count; i++) {
+ if (strcasecmp(drivers[i]->name, name) == 0) {
+ *idx_r = i;
+ return TRUE;
+ }
+ }
+ return FALSE;
+}
+
+void mailbox_list_register(const struct mailbox_list *list)
+{
+ unsigned int idx;
+
+ if (mailbox_list_driver_find(list->name, &idx)) {
+ i_fatal("mailbox_list_register(%s): duplicate driver",
+ list->name);
+ }
+
+ array_push_back(&mailbox_list_drivers, &list);
+}
+
+void mailbox_list_unregister(const struct mailbox_list *list)
+{
+ unsigned int idx;
+
+ if (!mailbox_list_driver_find(list->name, &idx)) {
+ i_fatal("mailbox_list_unregister(%s): unknown driver",
+ list->name);
+ }
+ array_delete(&mailbox_list_drivers, idx, 1);
+}
+
+const struct mailbox_list *
+mailbox_list_find_class(const char *driver)
+{
+ unsigned int idx;
+
+ if (!mailbox_list_driver_find(driver, &idx))
+ return NULL;
+
+ return array_idx_elem(&mailbox_list_drivers, idx);
+}
+
+int mailbox_list_create(const char *driver, struct mail_namespace *ns,
+ const struct mailbox_list_settings *set,
+ enum mailbox_list_flags flags,
+ struct mailbox_list **list_r, const char **error_r)
+{
+ const struct mailbox_list *class;
+ struct mailbox_list *list;
+
+ i_assert(ns->list == NULL ||
+ (flags & MAILBOX_LIST_FLAG_SECONDARY) != 0);
+
+ i_assert(set->subscription_fname == NULL ||
+ *set->subscription_fname != '\0');
+
+ if ((class = mailbox_list_find_class(driver)) == NULL) {
+ *error_r = "Unknown driver name";
+ return -1;
+ }
+
+ if ((class->props & MAILBOX_LIST_PROP_NO_MAILDIR_NAME) != 0 &&
+ *set->maildir_name != '\0') {
+ *error_r = "maildir_name not supported by this driver";
+ return -1;
+ }
+ if ((class->props & MAILBOX_LIST_PROP_NO_ALT_DIR) != 0 &&
+ set->alt_dir != NULL) {
+ *error_r = "alt_dir not supported by this driver";
+ return -1;
+ }
+
+ i_assert(set->root_dir == NULL || *set->root_dir != '\0' ||
+ (class->props & MAILBOX_LIST_PROP_NO_ROOT) != 0);
+
+ list = class->v.alloc();
+ array_create(&list->module_contexts, list->pool, sizeof(void *), 5);
+
+ list->ns = ns;
+ list->mail_set = ns->mail_set;
+ list->flags = flags;
+ list->root_permissions.file_create_mode = (mode_t)-1;
+ list->root_permissions.dir_create_mode = (mode_t)-1;
+ list->root_permissions.file_create_gid = (gid_t)-1;
+ list->changelog_timestamp = (time_t)-1;
+ if (set->no_noselect)
+ list->props |= MAILBOX_LIST_PROP_NO_NOSELECT;
+
+ /* copy settings */
+ if (set->root_dir != NULL) {
+ list->set.root_dir = p_strdup(list->pool, set->root_dir);
+ list->set.index_dir = set->index_dir == NULL ||
+ strcmp(set->index_dir, set->root_dir) == 0 ? NULL :
+ p_strdup(list->pool, set->index_dir);
+ list->set.index_pvt_dir = set->index_pvt_dir == NULL ||
+ strcmp(set->index_pvt_dir, set->root_dir) == 0 ? NULL :
+ p_strdup(list->pool, set->index_pvt_dir);
+ list->set.index_cache_dir = set->index_cache_dir == NULL ||
+ strcmp(set->index_cache_dir, set->root_dir) == 0 ? NULL :
+ p_strdup(list->pool, set->index_cache_dir);
+ list->set.control_dir = set->control_dir == NULL ||
+ strcmp(set->control_dir, set->root_dir) == 0 ? NULL :
+ p_strdup(list->pool, set->control_dir);
+ }
+
+ list->set.inbox_path = p_strdup(list->pool, set->inbox_path);
+ list->set.subscription_fname =
+ p_strdup(list->pool, set->subscription_fname);
+ list->set.list_index_fname =
+ p_strdup(list->pool, set->list_index_fname);
+ list->set.list_index_dir =
+ p_strdup(list->pool, set->list_index_dir);
+ list->set.maildir_name =
+ p_strdup(list->pool, set->maildir_name);
+ list->set.mailbox_dir_name =
+ p_strdup(list->pool, set->mailbox_dir_name);
+ list->set.alt_dir = p_strdup(list->pool, set->alt_dir);
+ list->set.alt_dir_nocheck = set->alt_dir_nocheck;
+ list->set.volatile_dir = p_strdup(list->pool, set->volatile_dir);
+ list->set.index_control_use_maildir_name =
+ set->index_control_use_maildir_name;
+ list->set.iter_from_index_dir = set->iter_from_index_dir;
+ list->set.no_noselect = set->no_noselect;
+ list->set.no_fs_validation = set->no_fs_validation;
+
+ if (*set->mailbox_dir_name == '\0')
+ list->set.mailbox_dir_name = "";
+ else if (set->mailbox_dir_name[strlen(set->mailbox_dir_name)-1] == '/') {
+ list->set.mailbox_dir_name =
+ p_strdup(list->pool, set->mailbox_dir_name);
+ } else {
+ list->set.mailbox_dir_name =
+ p_strconcat(list->pool, set->mailbox_dir_name, "/", NULL);
+ }
+ if (set->storage_name_escape_char != '\0')
+ list->set.storage_name_escape_char = set->storage_name_escape_char;
+ list->set.vname_escape_char = set->vname_escape_char;
+ list->set.utf8 = set->utf8;
+
+ if (list->v.init != NULL) {
+ if (list->v.init(list, error_r) < 0) {
+ list->v.deinit(list);
+ return -1;
+ }
+ }
+
+ e_debug(ns->user->event,
+ "%s: root=%s, index=%s, indexpvt=%s, control=%s, inbox=%s, alt=%s",
+ list->name,
+ list->set.root_dir == NULL ? "" : list->set.root_dir,
+ list->set.index_dir == NULL ? "" : list->set.index_dir,
+ list->set.index_pvt_dir == NULL ? "" : list->set.index_pvt_dir,
+ list->set.control_dir == NULL ?
+ "" : list->set.control_dir,
+ list->set.inbox_path == NULL ?
+ "" : list->set.inbox_path,
+ list->set.alt_dir == NULL ? "" : list->set.alt_dir);
+ if ((flags & MAILBOX_LIST_FLAG_SECONDARY) == 0)
+ mail_namespace_finish_list_init(ns, list);
+
+ *list_r = list;
+
+ hook_mailbox_list_created(list);
+ return 0;
+}
+
+static int fix_path(struct mail_user *user, const char *path, bool expand_home,
+ const char **path_r, const char **error_r)
+{
+ size_t len = strlen(path);
+
+ if (len > 1 && path[len-1] == '/')
+ path = t_strndup(path, len-1);
+ if (!expand_home) {
+ /* no ~ expansion */
+ } else if (path[0] == '~' && path[1] != '/' && path[1] != '\0') {
+ /* ~otheruser/dir */
+ if (home_try_expand(&path) < 0) {
+ *error_r = t_strconcat(
+ "No home directory for system user. "
+ "Can't expand ", t_strcut(path, '/'),
+ " for ", NULL);
+ return -1;
+ }
+ } else {
+ if (mail_user_try_home_expand(user, &path) < 0) {
+ *error_r = "Home directory not set for user. "
+ "Can't expand ~/ for ";
+ return -1;
+ }
+ }
+ *path_r = path;
+ return 0;
+}
+
+static const char *split_next_arg(const char *const **_args)
+{
+ const char *const *args = *_args;
+ const char *str = args[0];
+
+ args++;
+ while (*args != NULL && **args == '\0') {
+ args++;
+ if (*args == NULL) {
+ /* string ends with ":", just ignore it. */
+ break;
+ }
+ str = t_strconcat(str, ":", *args, NULL);
+ args++;
+ }
+ *_args = args;
+ return str;
+}
+
+void mailbox_list_settings_init_defaults(struct mailbox_list_settings *set_r)
+{
+ i_zero(set_r);
+ set_r->mailbox_dir_name = "";
+ set_r->maildir_name = "";
+ set_r->list_index_fname = MAILBOX_LIST_INDEX_DEFAULT_PREFIX;
+}
+
+static int
+mailbox_list_settings_parse_full(struct mail_user *user, const char *data,
+ bool expand_home,
+ struct mailbox_list_settings *set_r,
+ const char **error_r)
+{
+ const char *const *tmp, *key, *value, **dest, *str, *fname, *error;
+
+ *error_r = NULL;
+
+ mailbox_list_settings_init_defaults(set_r);
+ if (*data == '\0')
+ return 0;
+
+ /* <root dir> */
+ tmp = t_strsplit(data, ":");
+ str = split_next_arg(&tmp);
+ if (fix_path(user, str, expand_home, &set_r->root_dir, &error) < 0) {
+ *error_r = t_strconcat(error, "mail root dir in: ", data, NULL);
+ return -1;
+ }
+ if (str_begins(set_r->root_dir, "INBOX=")) {
+ /* probably mbox user trying to avoid root_dir */
+ *error_r = t_strconcat("Mail root directory not given: ",
+ data, NULL);
+ return -1;
+ }
+
+ while (*tmp != NULL) {
+ str = split_next_arg(&tmp);
+ if (strcmp(str, "UTF-8") == 0) {
+ set_r->utf8 = TRUE;
+ continue;
+ }
+
+ value = strchr(str, '=');
+ if (value == NULL) {
+ key = str;
+ value = "";
+ } else {
+ key = t_strdup_until(str, value);
+ value++;
+ }
+
+ if (strcmp(key, "INBOX") == 0)
+ dest = &set_r->inbox_path;
+ else if (strcmp(key, "INDEX") == 0)
+ dest = &set_r->index_dir;
+ else if (strcmp(key, "INDEXPVT") == 0)
+ dest = &set_r->index_pvt_dir;
+ else if (strcmp(key, "INDEXCACHE") == 0)
+ dest = &set_r->index_cache_dir;
+ else if (strcmp(key, "CONTROL") == 0)
+ dest = &set_r->control_dir;
+ else if (strcmp(key, "ALT") == 0)
+ dest = &set_r->alt_dir;
+ else if (strcmp(key, "ALTNOCHECK") == 0) {
+ set_r->alt_dir_nocheck = TRUE;
+ continue;
+ } else if (strcmp(key, "LAYOUT") == 0)
+ dest = &set_r->layout;
+ else if (strcmp(key, "SUBSCRIPTIONS") == 0)
+ dest = &set_r->subscription_fname;
+ else if (strcmp(key, "DIRNAME") == 0)
+ dest = &set_r->maildir_name;
+ else if (strcmp(key, "MAILBOXDIR") == 0)
+ dest = &set_r->mailbox_dir_name;
+ else if (strcmp(key, "VOLATILEDIR") == 0)
+ dest = &set_r->volatile_dir;
+ else if (strcmp(key, "LISTINDEX") == 0)
+ dest = &set_r->list_index_fname;
+ else if (strcmp(key, "FULLDIRNAME") == 0) {
+ set_r->index_control_use_maildir_name = TRUE;
+ dest = &set_r->maildir_name;
+ } else if (strcmp(key, "BROKENCHAR") == 0) {
+ if (strlen(value) != 1) {
+ *error_r = "BROKENCHAR value must be a single character";
+ return -1;
+ }
+ set_r->vname_escape_char = value[0];
+ continue;
+ } else if (strcmp(key, "ITERINDEX") == 0) {
+ set_r->iter_from_index_dir = TRUE;
+ continue;
+ } else if (strcmp(key, "NO-NOSELECT") == 0) {
+ set_r->no_noselect = TRUE;
+ continue;
+ } else if (strcmp(key, "NO-FS-VALIDATION") == 0) {
+ set_r->no_fs_validation = TRUE;
+ continue;
+ } else {
+ *error_r = t_strdup_printf("Unknown setting: %s", key);
+ return -1;
+ }
+ if (fix_path(user, value, expand_home, dest, &error) < 0) {
+ *error_r = t_strconcat(error, key, " in: ", data, NULL);
+ return -1;
+ }
+ }
+
+ if (set_r->index_dir != NULL && strcmp(set_r->index_dir, "MEMORY") == 0)
+ set_r->index_dir = "";
+ if (set_r->iter_from_index_dir &&
+ (set_r->index_dir == NULL || set_r->index_dir[0] == '\0')) {
+ *error_r = "ITERINDEX requires INDEX to be explicitly set";
+ return -1;
+ }
+ if (set_r->list_index_fname != NULL &&
+ (fname = strrchr(set_r->list_index_fname, '/')) != NULL) {
+ /* non-default LISTINDEX directory */
+ set_r->list_index_dir =
+ t_strdup_until(set_r->list_index_fname, fname);
+ set_r->list_index_fname = fname+1;
+ if (set_r->list_index_dir[0] != '/' &&
+ set_r->index_dir != NULL && set_r->index_dir[0] == '\0') {
+ *error_r = "LISTINDEX directory is relative but INDEX=MEMORY";
+ return -1;
+ }
+ }
+ return 0;
+}
+
+int mailbox_list_settings_parse(struct mail_user *user, const char *data,
+ struct mailbox_list_settings *set_r,
+ const char **error_r)
+{
+ return mailbox_list_settings_parse_full(user, data, TRUE,
+ set_r, error_r);
+}
+
+const char *mailbox_list_get_unexpanded_path(struct mailbox_list *list,
+ enum mailbox_list_path_type type)
+{
+ const struct mail_storage_settings *mail_set;
+ const char *location = list->ns->unexpanded_set->location;
+ struct mail_user *user = list->ns->user;
+ struct mailbox_list_settings set;
+ const char *p, *path, *error;
+
+ if (*location == SETTING_STRVAR_EXPANDED[0]) {
+ /* set using -o or userdb lookup. */
+ return "";
+ }
+
+ i_assert(*location == SETTING_STRVAR_UNEXPANDED[0]);
+ location++;
+
+ if (*location == '\0') {
+ mail_set = mail_user_set_get_driver_settings(user->set_info,
+ user->unexpanded_set, MAIL_STORAGE_SET_DRIVER_NAME);
+ i_assert(mail_set != NULL);
+ location = mail_set->mail_location;
+ if (*location == SETTING_STRVAR_EXPANDED[0])
+ return "";
+ i_assert(*location == SETTING_STRVAR_UNEXPANDED[0]);
+ location++;
+ }
+
+ /* type:settings */
+ p = strchr(location, ':');
+ if (p == NULL)
+ return "";
+
+ if (mailbox_list_settings_parse_full(user, p + 1, FALSE,
+ &set, &error) < 0)
+ return "";
+ if (!mailbox_list_set_get_root_path(&set, type, &path))
+ return "";
+ return path;
+}
+
+static bool need_escape_dirstart(const char *vname, const char *maildir_name)
+{
+ size_t len;
+
+ if (vname[0] == '.') {
+ if (vname[1] == '\0' || vname[1] == '/')
+ return TRUE; /* "." */
+ if (vname[1] == '.' && (vname[2] == '\0' || vname[2] == '/'))
+ return TRUE; /* ".." */
+ }
+ if (*maildir_name != '\0') {
+ len = strlen(maildir_name);
+ if (str_begins(vname, maildir_name) &&
+ (vname[len] == '\0' || vname[len] == '/'))
+ return TRUE; /* e.g. dbox-Mails */
+ }
+ return FALSE;
+}
+
+const char *
+mailbox_list_escape_name_params(const char *vname, const char *ns_prefix,
+ char ns_sep, char list_sep, char escape_char,
+ const char *maildir_name)
+{
+ size_t ns_prefix_len = strlen(ns_prefix);
+ string_t *escaped_name = t_str_new(64);
+ bool dirstart = TRUE;
+
+ i_assert(escape_char != '\0');
+
+ /* no escaping of namespace prefix */
+ if (str_begins(vname, ns_prefix)) {
+ str_append_data(escaped_name, vname, ns_prefix_len);
+ vname += ns_prefix_len;
+ }
+
+ /* escape the mailbox name */
+ if (*vname == '~') {
+ str_printfa(escaped_name, "%c%02x", escape_char, *vname);
+ vname++;
+ dirstart = FALSE;
+ }
+ for (; *vname != '\0'; vname++) {
+ if (*vname == ns_sep)
+ str_append_c(escaped_name, list_sep);
+ else if (*vname == list_sep ||
+ *vname == escape_char ||
+ *vname == '/' ||
+ (dirstart &&
+ need_escape_dirstart(vname, maildir_name))) {
+ str_printfa(escaped_name, "%c%02x",
+ escape_char, *vname);
+ } else {
+ str_append_c(escaped_name, *vname);
+ }
+ dirstart = *vname == '/';
+ }
+ return str_c(escaped_name);
+}
+
+void mailbox_list_name_unescape(const char **_name, char escape_char)
+{
+ const char *p, *name = *_name;
+ unsigned char chr;
+
+ if ((p = strchr(name, escape_char)) == NULL)
+ return;
+
+ string_t *str = t_str_new(strlen(name)*2);
+ str_append_data(str, name, p - name);
+ while (*p != '\0') {
+ if (*p == escape_char &&
+ imap_escaped_utf8_hex_to_char(p+1, &chr) == 0) {
+ str_append_c(str, chr);
+ p += 3;
+ } else {
+ str_append_c(str, *p++);
+ }
+ }
+ *_name = str_c(str);
+}
+
+static bool
+mailbox_list_vname_prepare(struct mailbox_list *list, const char **_vname)
+{
+ struct mail_namespace *ns = list->ns;
+ const char *vname = *_vname;
+
+ if (strcasecmp(vname, "INBOX") == 0 &&
+ (list->ns->flags & NAMESPACE_FLAG_INBOX_USER) != 0) {
+ /* INBOX is case sensitive. Normalize it into "INBOX". */
+ vname = "INBOX";
+ } else if (ns->prefix_len > 0) {
+ /* skip namespace prefix, except if this is INBOX */
+ if (strncmp(ns->prefix, vname, ns->prefix_len) == 0) {
+ vname += ns->prefix_len;
+ if (strcmp(vname, "INBOX") == 0 &&
+ (list->ns->flags & NAMESPACE_FLAG_INBOX_USER) != 0 &&
+ list->set.storage_name_escape_char != '\0') {
+ /* prefix/INBOX - this is troublesome, because
+ it ends up conflicting with the INBOX name.
+ Handle this in a bit kludgy way by escaping
+ the initial "I" character. */
+ *_vname = t_strdup_printf("%c49NBOX",
+ list->set.storage_name_escape_char);
+ return TRUE;
+ }
+ } else if (strncmp(ns->prefix, vname, ns->prefix_len-1) == 0 &&
+ strlen(vname) == ns->prefix_len-1 &&
+ ns->prefix[ns->prefix_len-1] == mail_namespace_get_sep(ns)) {
+ /* trying to access the namespace prefix itself */
+ vname = "";
+ } else {
+ /* we're converting a nonexistent mailbox name,
+ such as a LIST pattern. */
+ }
+ }
+ if (*vname == '\0' && ns->type == MAIL_NAMESPACE_TYPE_SHARED &&
+ (ns->flags & NAMESPACE_FLAG_INBOX_ANY) != 0 &&
+ !list->mail_set->mail_shared_explicit_inbox) {
+ /* opening shared/$user. it's the same as INBOX. */
+ vname = "INBOX";
+ }
+ *_vname = vname;
+ return FALSE;
+}
+
+static const char *
+mailbox_list_default_get_storage_name_part(struct mailbox_list *list,
+ const char *vname_part)
+{
+ const char *storage_name = vname_part;
+ string_t *str;
+
+ if (!list->set.utf8) {
+ /* UTF-8 -> mUTF-7 conversion */
+ str = t_str_new(strlen(storage_name)*2);
+ if (imap_escaped_utf8_to_utf7(storage_name,
+ list->set.vname_escape_char,
+ str) < 0)
+ i_panic("Mailbox name not UTF-8: %s", vname_part);
+ storage_name = str_c(str);
+ } else if (list->set.vname_escape_char != '\0') {
+ mailbox_list_name_unescape(&storage_name,
+ list->set.vname_escape_char);
+ }
+ if (list->set.storage_name_escape_char != '\0') {
+ storage_name = mailbox_list_escape_name_params(storage_name,
+ list->ns->prefix,
+ '\0', /* no separator conversion */
+ mailbox_list_get_hierarchy_sep(list),
+ list->set.storage_name_escape_char,
+ list->set.maildir_name);
+ }
+ return storage_name;
+}
+
+const char *mailbox_list_default_get_storage_name(struct mailbox_list *list,
+ const char *vname)
+{
+ const char *prepared_name = vname;
+ const char list_sep = mailbox_list_get_hierarchy_sep(list);
+ const char ns_sep = mail_namespace_get_sep(list->ns);
+
+ if (mailbox_list_vname_prepare(list, &prepared_name))
+ return prepared_name;
+ if (list->ns->type == MAIL_NAMESPACE_TYPE_SHARED &&
+ (list->ns->flags & NAMESPACE_FLAG_AUTOCREATED) == 0 &&
+ list_sep != ns_sep &&
+ list->set.storage_name_escape_char == '\0') {
+ /* Accessing shared namespace root. This is just the initial
+ lookup that ends up as parameter to
+ shared_storage_get_namespace(). That then finds/creates the
+ actual shared namespace, which gets used to generate the
+ proper storage_name. So the only thing that's really
+ necessary here is to just skip over the shared namespace
+ prefix and leave the rest of the name untouched. The only
+ exception is if there is a storage_name_escape_char set, in
+ this case the storage name must be handled. */
+ return prepared_name;
+ }
+
+ const char sep[] = { ns_sep, '\0' };
+ const char *const *parts = t_strsplit(prepared_name, sep);
+ string_t *storage_name = t_str_new(128);
+ for (unsigned int i = 0; parts[i] != NULL; i++) {
+ if (i > 0)
+ str_append_c(storage_name, list_sep);
+ str_append(storage_name,
+ mailbox_list_default_get_storage_name_part(list, parts[i]));
+ }
+ return str_c(storage_name);
+}
+
+const char *mailbox_list_get_storage_name(struct mailbox_list *list,
+ const char *vname)
+{
+ return list->v.get_storage_name(list, vname);
+}
+
+const char *
+mailbox_list_unescape_name_params(const char *src, const char *ns_prefix,
+ char ns_sep, char list_sep, char escape_char)
+{
+ size_t ns_prefix_len = strlen(ns_prefix);
+ string_t *dest = t_str_new(strlen(src));
+ unsigned int num;
+
+ if (str_begins(src, ns_prefix)) {
+ str_append_data(dest, src, ns_prefix_len);
+ src += ns_prefix_len;
+ }
+
+ for (; *src != '\0'; src++) {
+ if (*src == escape_char &&
+ i_isxdigit(src[1]) && i_isxdigit(src[2])) {
+ if (src[1] >= '0' && src[1] <= '9')
+ num = src[1] - '0';
+ else
+ num = i_toupper(src[1]) - 'A' + 10;
+ num *= 16;
+ if (src[2] >= '0' && src[2] <= '9')
+ num += src[2] - '0';
+ else
+ num += i_toupper(src[2]) - 'A' + 10;
+
+ str_append_c(dest, num);
+ src += 2;
+ } else if (*src == list_sep)
+ str_append_c(dest, ns_sep);
+ else
+ str_append_c(dest, *src);
+ }
+ return str_c(dest);
+}
+
+static bool
+mailbox_list_storage_name_prepare(struct mailbox_list *list,
+ const char **_storage_name)
+{
+ const char *name = *_storage_name;
+
+ if ((list->ns->flags & NAMESPACE_FLAG_INBOX_USER) != 0 &&
+ strcmp(name, "INBOX") == 0 &&
+ list->ns->user == list->ns->owner) {
+ /* user's INBOX - use as-is. NOTE: don't do case-insensitive
+ comparison, otherwise we can't differentiate between INBOX
+ and <ns prefix>/inBox. */
+ return TRUE;
+ }
+ if (strcmp(name, "INBOX") == 0 &&
+ list->ns->type == MAIL_NAMESPACE_TYPE_SHARED &&
+ (list->ns->flags & NAMESPACE_FLAG_INBOX_ANY) != 0 &&
+ !list->mail_set->mail_shared_explicit_inbox) {
+ /* convert to shared/$user, we don't really care about the
+ INBOX suffix here. */
+ name = "";
+ }
+ if (name[0] == '\0') {
+ /* return namespace prefix without the separator */
+ if (list->ns->prefix_len == 0)
+ *_storage_name = list->ns->prefix;
+ else {
+ *_storage_name =
+ t_strndup(list->ns->prefix,
+ list->ns->prefix_len - 1);
+ }
+ return TRUE;
+ }
+ return FALSE;
+}
+
+void mailbox_list_name_escape(const char *name, const char *escape_chars,
+ string_t *dest)
+{
+ for (unsigned int i = 0; name[i] != '\0'; i++) {
+ if (strchr(escape_chars, name[i]) != NULL)
+ str_printfa(dest, "%c%02x", escape_chars[0], name[i]);
+ else
+ str_append_c(dest, name[i]);
+ }
+}
+
+static const char *
+mailbox_list_default_get_vname_part(struct mailbox_list *list,
+ const char *storage_name_part)
+{
+ const char *vname = storage_name_part;
+ char escape_chars[] = {
+ list->set.vname_escape_char,
+ mail_namespace_get_sep(list->ns),
+ '\0'
+ };
+
+ if (list->set.storage_name_escape_char != '\0') {
+ vname = mailbox_list_unescape_name_params(vname,
+ list->ns->prefix,
+ '\0', '\0', /* no separator conversion */
+ list->set.storage_name_escape_char);
+ }
+
+ if (!list->set.utf8) {
+ /* mUTF-7 -> UTF-8 conversion */
+ string_t *str = t_str_new(strlen(vname));
+ if (escape_chars[0] != '\0') {
+ imap_utf7_to_utf8_escaped(vname, escape_chars, str);
+ vname = str_c(str);
+ } else if (imap_utf7_to_utf8(vname, str) == 0)
+ vname = str_c(str);
+ else {
+ /* Invalid mUTF7, but no escape character. This mailbox
+ can't be accessible, so just return it as the
+ original mUTF7 name. */
+ }
+ } else if (list->set.vname_escape_char != '\0') {
+ string_t *str = t_str_new(strlen(vname));
+ mailbox_list_name_escape(vname, escape_chars, str);
+ vname = str_c(str);
+ }
+ return vname;
+}
+
+const char *mailbox_list_default_get_vname(struct mailbox_list *list,
+ const char *storage_name)
+{
+ if (mailbox_list_storage_name_prepare(list, &storage_name))
+ return storage_name;
+
+ char ns_sep = mail_namespace_get_sep(list->ns);
+ char sep[] = { mailbox_list_get_hierarchy_sep(list), '\0' };
+ const char *const *parts = t_strsplit(storage_name, sep);
+ string_t *vname = t_str_new(128);
+ str_append(vname, list->ns->prefix);
+ for (unsigned int i = 0; parts[i] != NULL; i++) {
+ if (i > 0)
+ str_append_c(vname, ns_sep);
+ str_append(vname,
+ mailbox_list_default_get_vname_part(list, parts[i]));
+ }
+ return str_c(vname);
+}
+
+const char *mailbox_list_get_vname(struct mailbox_list *list, const char *name)
+{
+ return list->v.get_vname(list, name);
+}
+
+void mailbox_list_destroy(struct mailbox_list **_list)
+{
+ struct mailbox_list *list = *_list;
+
+ *_list = NULL;
+ i_free_and_null(list->error_string);
+ i_free(list->last_internal_error);
+
+ if (hash_table_is_created(list->guid_cache)) {
+ hash_table_destroy(&list->guid_cache);
+ pool_unref(&list->guid_cache_pool);
+ }
+
+ if (list->subscriptions != NULL)
+ mailbox_tree_deinit(&list->subscriptions);
+ if (list->changelog != NULL)
+ mailbox_log_free(&list->changelog);
+
+ if (array_is_created(&list->error_stack)) {
+ i_assert(array_count(&list->error_stack) == 0);
+ array_free(&list->error_stack);
+ }
+ list->v.deinit(list);
+}
+
+const char *mailbox_list_get_driver_name(const struct mailbox_list *list)
+{
+ return list->name;
+}
+
+const struct mailbox_list_settings *
+mailbox_list_get_settings(const struct mailbox_list *list)
+{
+ return &list->set;
+}
+
+enum mailbox_list_flags mailbox_list_get_flags(const struct mailbox_list *list)
+{
+ return list->flags;
+}
+
+struct mail_namespace *
+mailbox_list_get_namespace(const struct mailbox_list *list)
+{
+ return list->ns;
+}
+
+static mode_t get_dir_mode(mode_t mode)
+{
+ /* add the execute bit if either read or write bit is set */
+ if ((mode & 0600) != 0) mode |= 0100;
+ if ((mode & 0060) != 0) mode |= 0010;
+ if ((mode & 0006) != 0) mode |= 0001;
+ return mode;
+}
+
+struct mail_user *
+mailbox_list_get_user(const struct mailbox_list *list)
+{
+ return list->ns->user;
+}
+
+static int
+mailbox_list_get_storage_driver(struct mailbox_list *list, const char *driver,
+ struct mail_storage **storage_r)
+{
+ struct mail_storage *storage;
+ const char *error, *data;
+
+ array_foreach_elem(&list->ns->all_storages, storage) {
+ if (strcmp(storage->name, driver) == 0) {
+ *storage_r = storage;
+ return 0;
+ }
+ }
+
+ data = i_strchr_to_next(list->ns->set->location, ':');
+ if (data == NULL)
+ data = "";
+ if (mail_storage_create_full(list->ns, driver, data, 0,
+ storage_r, &error) < 0) {
+ mailbox_list_set_critical(list,
+ "Namespace %s: Failed to create storage '%s': %s",
+ list->ns->prefix, driver, error);
+ return -1;
+ }
+ return 0;
+}
+
+int mailbox_list_get_storage(struct mailbox_list **list, const char *vname,
+ struct mail_storage **storage_r)
+{
+ const struct mailbox_settings *set;
+
+ if ((*list)->v.get_storage != NULL)
+ return (*list)->v.get_storage(list, vname, storage_r);
+
+ set = mailbox_settings_find((*list)->ns, vname);
+ if (set != NULL && set->driver != NULL && set->driver[0] != '\0') {
+ return mailbox_list_get_storage_driver(*list, set->driver,
+ storage_r);
+ }
+ *storage_r = mail_namespace_get_default_storage((*list)->ns);
+ return 0;
+}
+
+void mailbox_list_get_default_storage(struct mailbox_list *list,
+ struct mail_storage **storage)
+{
+ *storage = mail_namespace_get_default_storage(list->ns);
+}
+
+char mailbox_list_get_hierarchy_sep(struct mailbox_list *list)
+{
+ /* the current API doesn't allow returning an error, so imap code
+ looks at the list's last error. make sure the error is cleared
+ so the error-check doesn't return something irrelevant */
+ mailbox_list_clear_error(list);
+ return list->v.get_hierarchy_sep(list);
+}
+
+static bool
+mailbox_list_get_permissions_stat(struct mailbox_list *list, const char *path,
+ struct mailbox_permissions *permissions_r)
+{
+ struct stat st;
+
+ if (stat(path, &st) < 0) {
+ if (errno == EACCES) {
+ mailbox_list_set_critical(list, "%s",
+ mail_error_eacces_msg("stat", path));
+ } else if (!ENOTFOUND(errno)) {
+ mailbox_list_set_critical(list, "stat(%s) failed: %m",
+ path);
+ } else {
+ e_debug(list->ns->user->event,
+ "Namespace %s: %s doesn't exist yet, "
+ "using default permissions",
+ list->ns->prefix, path);
+ }
+ return FALSE;
+ }
+
+ permissions_r->file_uid = st.st_uid;
+ permissions_r->file_gid = st.st_gid;
+ permissions_r->file_create_mode = (st.st_mode & 0666) | 0600;
+ permissions_r->dir_create_mode = (st.st_mode & 0777) | 0700;
+ permissions_r->file_create_gid_origin = path;
+
+ if (!S_ISDIR(st.st_mode)) {
+ /* we're getting permissions from a file.
+ apply +x modes as necessary. */
+ permissions_r->dir_create_mode =
+ get_dir_mode(permissions_r->dir_create_mode);
+ }
+
+ if (S_ISDIR(st.st_mode) && (st.st_mode & S_ISGID) != 0) {
+ /* directory's GID is used automatically for new files */
+ permissions_r->file_create_gid = (gid_t)-1;
+ } else if ((st.st_mode & 0070) >> 3 == (st.st_mode & 0007)) {
+ /* group has same permissions as world, so don't bother
+ changing it */
+ permissions_r->file_create_gid = (gid_t)-1;
+ } else if (getegid() == st.st_gid) {
+ /* using our own gid, no need to change it */
+ permissions_r->file_create_gid = (gid_t)-1;
+ } else {
+ permissions_r->file_create_gid = st.st_gid;
+ }
+ if (!S_ISDIR(st.st_mode) &&
+ permissions_r->file_create_gid != (gid_t)-1) {
+ /* we need to stat() the parent directory to see if
+ it has setgid-bit set */
+ const char *p = strrchr(path, '/');
+ const char *parent_path = p == NULL ? NULL :
+ t_strdup_until(path, p);
+ if (parent_path != NULL &&
+ stat(parent_path, &st) == 0 &&
+ (st.st_mode & S_ISGID) != 0) {
+ /* directory's GID is used automatically for
+ new files */
+ permissions_r->file_create_gid = (gid_t)-1;
+ }
+ }
+ return TRUE;
+}
+
+static void ATTR_NULL(2)
+mailbox_list_get_permissions_internal(struct mailbox_list *list,
+ const char *name,
+ struct mailbox_permissions *permissions_r)
+{
+ const char *path = NULL, *parent_name, *p;
+
+ i_zero(permissions_r);
+
+ /* use safe defaults */
+ permissions_r->file_uid = (uid_t)-1;
+ permissions_r->file_gid = (gid_t)-1;
+ permissions_r->file_create_mode = 0600;
+ permissions_r->dir_create_mode = 0700;
+ permissions_r->file_create_gid = (gid_t)-1;
+ permissions_r->file_create_gid_origin = "defaults";
+
+ if (list->set.iter_from_index_dir ||
+ (list->flags & MAILBOX_LIST_FLAG_NO_MAIL_FILES) != 0) {
+ /* a) iterating from index dir. Use the index dir's permissions
+ as well, since they might be in a faster storage.
+
+ b) mail files don't exist in storage, but index files
+ might. */
+ (void)mailbox_list_get_path(list, name,
+ MAILBOX_LIST_PATH_TYPE_INDEX, &path);
+ }
+
+ if (name != NULL && path == NULL) {
+ if (mailbox_list_get_path(list, name, MAILBOX_LIST_PATH_TYPE_DIR,
+ &path) < 0)
+ name = NULL;
+ }
+ if (name == NULL && path == NULL) {
+ (void)mailbox_list_get_root_path(list, MAILBOX_LIST_PATH_TYPE_DIR,
+ &path);
+ }
+
+ if (path == NULL) {
+ /* no filesystem support in storage */
+ } else if (mailbox_list_get_permissions_stat(list, path, permissions_r)) {
+ /* got permissions from the given path */
+ permissions_r->gid_origin_is_mailbox_path = name != NULL;
+ } else if (name != NULL) {
+ /* path couldn't be stat()ed, try parent mailbox */
+ p = strrchr(name, mailbox_list_get_hierarchy_sep(list));
+ if (p == NULL) {
+ /* return root defaults */
+ parent_name = NULL;
+ } else {
+ parent_name = t_strdup_until(name, p);
+ }
+ mailbox_list_get_permissions(list, parent_name,
+ permissions_r);
+ return;
+ } else {
+ /* assume current defaults for mailboxes that don't exist or
+ can't be looked up for some other reason */
+ permissions_r->file_uid = geteuid();
+ permissions_r->file_gid = getegid();
+ }
+ if (name == NULL) {
+ mailbox_permissions_copy(&list->root_permissions, permissions_r,
+ list->pool);
+ }
+
+ if (name == NULL) {
+ e_debug(list->ns->user->event,
+ "Namespace %s: Using permissions from %s: "
+ "mode=0%o gid=%s", list->ns->prefix,
+ path != NULL ? path : "",
+ (int)permissions_r->dir_create_mode,
+ permissions_r->file_create_gid == (gid_t)-1 ? "default" :
+ dec2str(permissions_r->file_create_gid));
+ }
+}
+
+void mailbox_list_get_permissions(struct mailbox_list *list, const char *name,
+ struct mailbox_permissions *permissions_r)
+{
+ mailbox_list_get_permissions_internal(list, name, permissions_r);
+}
+
+void mailbox_list_get_root_permissions(struct mailbox_list *list,
+ struct mailbox_permissions *permissions_r)
+{
+ if (list->root_permissions.file_create_mode != (mode_t)-1)
+ *permissions_r = list->root_permissions;
+ else {
+ mailbox_list_get_permissions_internal(list, NULL,
+ permissions_r);
+ }
+}
+
+void mailbox_permissions_copy(struct mailbox_permissions *dest,
+ const struct mailbox_permissions *src,
+ pool_t pool)
+{
+ *dest = *src;
+ dest->file_create_gid_origin =
+ p_strdup(pool, src->file_create_gid_origin);
+}
+
+static const char *
+get_expanded_path(const char *unexpanded_start, const char *unexpanded_stop,
+ const char *expanded_full)
+{
+ const char *ret;
+ unsigned int i, slash_count = 0, slash2_count = 0;
+
+ /* get the expanded path up to the same amount of '/' characters.
+ if there isn't the same amount of '/' characters, it means %variable
+ expansion added more of them and we can't handle this. */
+ for (i = 0; unexpanded_start+i != unexpanded_stop; i++) {
+ if (unexpanded_start[i] == '/')
+ slash_count++;
+ }
+ for (; unexpanded_start[i] != '\0'; i++) {
+ if (unexpanded_start[i] == '/')
+ slash2_count++;
+ }
+
+ for (i = 0; expanded_full[i] != '\0'; i++) {
+ if (expanded_full[i] == '/') {
+ if (slash_count == 0)
+ break;
+ slash_count--;
+ }
+ }
+ if (slash_count != 0)
+ return "";
+
+ ret = t_strndup(expanded_full, i);
+ for (; expanded_full[i] != '\0'; i++) {
+ if (expanded_full[i] == '/') {
+ if (slash2_count == 0)
+ return "";
+ slash2_count--;
+ }
+ }
+ if (slash2_count != 0)
+ return "";
+ return ret;
+}
+
+static int
+mailbox_list_try_mkdir_root_parent(struct mailbox_list *list,
+ enum mailbox_list_path_type type,
+ struct mailbox_permissions *perm,
+ const char **error_r)
+{
+ const char *expanded, *unexpanded, *root_dir, *p;
+ struct stat st;
+ bool home = FALSE;
+
+ /* get the directory path up to last %variable. for example
+ unexpanded path may be "/var/mail/%d/%2n/%n/Maildir", and we want
+ to get expanded="/var/mail/domain/nn" */
+ unexpanded = mailbox_list_get_unexpanded_path(list, type);
+ p = strrchr(unexpanded, '%');
+ if ((p == unexpanded && p[1] == 'h') ||
+ (p == NULL && unexpanded[0] == '~')) {
+ /* home directory used */
+ if (!mailbox_list_get_root_path(list, type, &expanded))
+ i_unreached();
+ home = TRUE;
+ } else if (p == NULL) {
+ return 0;
+ } else {
+ while (p != unexpanded && *p != '/') p--;
+ if (p == unexpanded)
+ return 0;
+
+ if (!mailbox_list_get_root_path(list, type, &expanded))
+ i_unreached();
+ expanded = get_expanded_path(unexpanded, p, expanded);
+ if (*expanded == '\0')
+ return 0;
+ }
+
+ /* get the first existing parent directory's permissions */
+ if (stat_first_parent(expanded, &root_dir, &st) < 0) {
+ *error_r = errno == EACCES ?
+ mail_error_eacces_msg("stat", root_dir) :
+ t_strdup_printf("stat(%s) failed: %m", root_dir);
+ return -1;
+ }
+
+ /* if the parent directory doesn't have setgid-bit enabled, we don't
+ copy any permissions from it. */
+ if ((st.st_mode & S_ISGID) == 0)
+ return 0;
+
+ if (!home) {
+ /* assuming we have e.g. /var/vmail/%d/%n directory, here we
+ want to create up to /var/vmail/%d with permissions from
+ the parent directory. we never want to create the %n
+ directory itself. */
+ if (root_dir == expanded) {
+ /* this is the %n directory */
+ } else {
+ if (mkdir_parents_chgrp(expanded, st.st_mode,
+ (gid_t)-1, root_dir) < 0 &&
+ errno != EEXIST) {
+ *error_r = t_strdup_printf(
+ "mkdir(%s) failed: %m", expanded);
+ return -1;
+ }
+ }
+ if (perm->file_create_gid == (gid_t)-1 &&
+ (perm->dir_create_mode & S_ISGID) == 0) {
+ /* change the group for user directories */
+ perm->dir_create_mode |= S_ISGID;
+ perm->file_create_gid = getegid();
+ perm->file_create_gid_origin = "egid";
+ perm->gid_origin_is_mailbox_path = FALSE;
+ }
+ } else {
+ /* when using %h and the parent has setgid-bit,
+ copy the permissions from it for the home we're creating */
+ perm->file_create_mode = st.st_mode & 0666;
+ perm->dir_create_mode = st.st_mode;
+ perm->file_create_gid = (gid_t)-1;
+ perm->file_create_gid_origin = "parent";
+ perm->gid_origin_is_mailbox_path = FALSE;
+ }
+ return 0;
+}
+
+int mailbox_list_try_mkdir_root(struct mailbox_list *list, const char *path,
+ enum mailbox_list_path_type type,
+ const char **error_r)
+{
+ const char *root_dir;
+ struct stat st;
+ struct mailbox_permissions perm;
+
+ if (stat(path, &st) == 0) {
+ /* looks like it already exists, don't bother checking
+ further. */
+ if (!S_ISDIR(st.st_mode)) {
+ *error_r = t_strdup_printf(
+ "Root directory is a file: %s", path);
+ return -1;
+ }
+ return 0;
+ }
+
+ mailbox_list_get_root_permissions(list, &perm);
+
+ if (!mailbox_list_get_root_path(list, type, &root_dir))
+ i_unreached();
+ i_assert(str_begins(path, root_dir));
+ if (strcmp(root_dir, path) != 0 && stat(root_dir, &st) == 0) {
+ /* creating a subdirectory under an already existing root dir.
+ use the root's permissions */
+ } else {
+ if (mailbox_list_try_mkdir_root_parent(list, type,
+ &perm, error_r) < 0)
+ return -1;
+ }
+
+ /* the rest of the directories exist only for one user. create them
+ with default directory permissions */
+ if (mkdir_parents_chgrp(path, perm.dir_create_mode,
+ perm.file_create_gid,
+ perm.file_create_gid_origin) < 0 &&
+ errno != EEXIST) {
+ if (errno == EACCES)
+ *error_r = mail_error_create_eacces_msg("mkdir", path);
+ else
+ *error_r = t_strdup_printf("mkdir(%s) failed: %m", path);
+ return -1;
+ }
+ return 0;
+}
+
+int mailbox_list_mkdir_root(struct mailbox_list *list, const char *path,
+ enum mailbox_list_path_type type)
+{
+ const char *error;
+
+ if (mailbox_list_try_mkdir_root(list, path, type, &error) < 0) {
+ mailbox_list_set_critical(list, "%s", error);
+ return -1;
+ }
+ if (type == MAILBOX_LIST_PATH_TYPE_INDEX)
+ list->index_root_dir_created = TRUE;
+ return 0;
+}
+
+static bool
+mailbox_list_is_valid_fs_name(struct mailbox_list *list, const char *name,
+ const char **error_r)
+{
+ bool ret, allow_internal_dirs;
+
+ *error_r = NULL;
+
+ if (list->mail_set->mail_full_filesystem_access ||
+ list->set.no_fs_validation)
+ return TRUE;
+
+ /* either the list backend uses '/' as the hierarchy separator or
+ it doesn't use filesystem at all (PROP_NO_ROOT) */
+ if ((list->props & MAILBOX_LIST_PROP_NO_ROOT) == 0 &&
+ mailbox_list_get_hierarchy_sep(list) != '/' &&
+ strchr(name, '/') != NULL) {
+ *error_r = "Name must not have '/' characters";
+ return FALSE;
+ }
+
+ /* make sure it's not absolute path */
+ if (*name == '/') {
+ *error_r = "Begins with '/'";
+ return FALSE;
+ }
+ if (*name == '~') {
+ *error_r = "Begins with '~'";
+ return FALSE;
+ }
+
+ /* make sure the mailbox name doesn't contain any foolishness:
+ "../" could give access outside the mailbox directory.
+ "./" and "//" could fool ACL checks.
+
+ some mailbox formats have reserved directory names, such as
+ Maildir's cur/new/tmp. if any of those would conflict with the
+ mailbox directory name, it's not valid. */
+ allow_internal_dirs = list->v.is_internal_name == NULL ||
+ *list->set.maildir_name != '\0' ||
+ (list->props & MAILBOX_LIST_PROP_NO_INTERNAL_NAMES) != 0;
+ T_BEGIN {
+ const char *const *names;
+
+ names = t_strsplit(name, "/");
+ for (; *names != NULL; names++) {
+ const char *n = *names;
+
+ if (*n == '\0') {
+ *error_r = "Has adjacent '/' chars";
+ break; /* // */
+ }
+ if (*n == '.') {
+ if (n[1] == '\0') {
+ *error_r = "Contains '.' part";
+ break; /* ./ */
+ }
+ if (n[1] == '.' && n[2] == '\0') {
+ *error_r = "Contains '..' part";
+ break; /* ../ */
+ }
+ }
+ if (*list->set.maildir_name != '\0' &&
+ strcmp(list->set.maildir_name, n) == 0) {
+ /* don't allow maildir_name to be used as part
+ of the mailbox name */
+ *error_r = "Contains reserved name";
+ break;
+ }
+ if (!allow_internal_dirs &&
+ list->v.is_internal_name(list, n)) {
+ *error_r = "Contains reserved name";
+ break;
+ }
+ }
+ ret = *names == NULL;
+ } T_END;
+
+ return ret;
+}
+
+
+bool mailbox_list_is_valid_name(struct mailbox_list *list,
+ const char *name, const char **error_r)
+{
+ if (*name == '\0') {
+ if (*list->ns->prefix != '\0') {
+ /* an ugly way to get to mailbox root (e.g. Maildir/
+ when it's not the INBOX) */
+ return TRUE;
+ }
+ *error_r = "Name is empty";
+ return FALSE;
+ }
+
+ return mailbox_list_is_valid_fs_name(list, name, error_r);
+}
+
+int mailbox_list_get_path(struct mailbox_list *list, const char *name,
+ enum mailbox_list_path_type type,
+ const char **path_r)
+{
+ int ret;
+
+ if ((ret = list->v.get_path(list, name, type, path_r)) <= 0)
+ *path_r = NULL;
+ else
+ i_assert(*path_r != NULL);
+ return ret;
+}
+
+bool mailbox_list_get_root_path(struct mailbox_list *list,
+ enum mailbox_list_path_type type,
+ const char **path_r)
+{
+ int ret;
+
+ if ((ret = list->v.get_path(list, NULL, type, path_r)) < 0)
+ i_unreached();
+ if (ret == 0)
+ *path_r = NULL;
+ else
+ i_assert(*path_r != NULL);
+ return ret > 0;
+}
+
+const char *mailbox_list_get_root_forced(struct mailbox_list *list,
+ enum mailbox_list_path_type type)
+{
+ const char *path;
+
+ if (!mailbox_list_get_root_path(list, type, &path))
+ i_unreached();
+ return path;
+}
+
+bool mailbox_list_set_get_root_path(const struct mailbox_list_settings *set,
+ enum mailbox_list_path_type type,
+ const char **path_r)
+{
+ const char *path = NULL;
+
+ switch (type) {
+ case MAILBOX_LIST_PATH_TYPE_DIR:
+ path = set->root_dir;
+ break;
+ case MAILBOX_LIST_PATH_TYPE_ALT_DIR:
+ path = set->alt_dir;
+ break;
+ case MAILBOX_LIST_PATH_TYPE_MAILBOX:
+ if (*set->mailbox_dir_name == '\0')
+ path = set->root_dir;
+ else {
+ path = t_strconcat(set->root_dir, "/",
+ set->mailbox_dir_name, NULL);
+ path = t_strndup(path, strlen(path)-1);
+ }
+ break;
+ case MAILBOX_LIST_PATH_TYPE_ALT_MAILBOX:
+ if (*set->mailbox_dir_name == '\0')
+ path = set->alt_dir;
+ else if (set->alt_dir != NULL) {
+ path = t_strconcat(set->alt_dir, "/",
+ set->mailbox_dir_name, NULL);
+ path = t_strndup(path, strlen(path)-1);
+ }
+ break;
+ case MAILBOX_LIST_PATH_TYPE_CONTROL:
+ path = set->control_dir != NULL ?
+ set->control_dir : set->root_dir;
+ break;
+ case MAILBOX_LIST_PATH_TYPE_LIST_INDEX:
+ if (set->list_index_dir != NULL) {
+ if (set->list_index_dir[0] == '/') {
+ path = set->list_index_dir;
+ break;
+ }
+ /* relative path */
+ if (!mailbox_list_set_get_root_path(set,
+ MAILBOX_LIST_PATH_TYPE_INDEX, &path))
+ i_unreached();
+ path = t_strconcat(path, "/", set->list_index_dir, NULL);
+ break;
+ }
+ /* fall through - default to index directory */
+ case MAILBOX_LIST_PATH_TYPE_INDEX_CACHE:
+ if (set->index_cache_dir != NULL &&
+ type == MAILBOX_LIST_PATH_TYPE_INDEX_CACHE) {
+ path = set->index_cache_dir;
+ break;
+ }
+ /* fall through */
+ case MAILBOX_LIST_PATH_TYPE_INDEX:
+ if (set->index_dir != NULL) {
+ if (set->index_dir[0] == '\0') {
+ /* in-memory indexes */
+ return 0;
+ }
+ path = set->index_dir;
+ } else {
+ path = set->root_dir;
+ }
+ break;
+ case MAILBOX_LIST_PATH_TYPE_INDEX_PRIVATE:
+ path = set->index_pvt_dir;
+ break;
+ }
+ *path_r = path;
+ return path != NULL;
+}
+
+const char *mailbox_list_get_temp_prefix(struct mailbox_list *list)
+{
+ return list->v.get_temp_prefix(list, FALSE);
+}
+
+const char *mailbox_list_get_global_temp_prefix(struct mailbox_list *list)
+{
+ return list->v.get_temp_prefix(list, TRUE);
+}
+
+const char *mailbox_list_join_refpattern(struct mailbox_list *list,
+ const char *ref, const char *pattern)
+{
+ if (list->v.join_refpattern != NULL)
+ return list->v.join_refpattern(list, ref, pattern);
+
+ /* the default implementation: */
+ if (*ref != '\0') {
+ /* merge reference and pattern */
+ pattern = t_strconcat(ref, pattern, NULL);
+ }
+ return pattern;
+}
+
+int mailbox_has_children(struct mailbox_list *list, const char *name)
+{
+ struct mailbox_list_iterate_context *iter;
+ const char *pattern;
+ int ret;
+
+ pattern = t_strdup_printf("%s%c%%", name,
+ mail_namespace_get_sep(list->ns));
+ iter = mailbox_list_iter_init(list, pattern,
+ MAILBOX_LIST_ITER_RETURN_NO_FLAGS);
+ ret = mailbox_list_iter_next(iter) != NULL ? 1 : 0;
+ if (mailbox_list_iter_deinit(&iter) < 0)
+ ret = -1;
+ return ret;
+}
+
+int mailbox_list_mailbox(struct mailbox_list *list, const char *name,
+ enum mailbox_info_flags *flags_r)
+{
+ const char *path, *fname, *rootdir, *dir, *inbox;
+ size_t len;
+
+ *flags_r = 0;
+
+ if ((list->ns->flags & NAMESPACE_FLAG_INBOX_USER) != 0 &&
+ strcasecmp(name, "INBOX") == 0) {
+ /* special handling for INBOX, mainly because with Maildir++
+ layout it needs to check if the cur/ directory exists,
+ which the Maildir++ layout backend itself can't do.. */
+ struct mailbox *box;
+ enum mailbox_existence existence;
+ int ret;
+
+ /* kludge: with imapc backend we can get here with
+ list=Maildir++ (for indexes), but list->ns->list=imapc */
+ box = mailbox_alloc(list->ns->list, "INBOX", 0);
+ ret = mailbox_exists(box, FALSE, &existence);
+ if (ret < 0) {
+ const char *errstr;
+ enum mail_error error;
+
+ /* internal error or with imapc we can get here with
+ login failures */
+ errstr = mailbox_get_last_error(box, &error);
+ mailbox_list_set_error(list, error, errstr);
+ }
+ mailbox_free(&box);
+ if (ret < 0)
+ return -1;
+ switch (existence) {
+ case MAILBOX_EXISTENCE_NONE:
+ case MAILBOX_EXISTENCE_NOSELECT:
+ *flags_r |= MAILBOX_NONEXISTENT;
+ return 0;
+ case MAILBOX_EXISTENCE_SELECT:
+ break;
+ }
+ return 1;
+ }
+
+ if (list->v.get_mailbox_flags == NULL) {
+ /* can't do this optimized. do it the slow way. */
+ struct mailbox_list_iterate_context *iter;
+ const struct mailbox_info *info;
+ const char *vname;
+
+ vname = mailbox_list_get_vname(list, name);
+ iter = mailbox_list_iter_init(list, vname, 0);
+ info = mailbox_list_iter_next(iter);
+ if (info == NULL)
+ *flags_r = MAILBOX_NONEXISTENT;
+ else
+ *flags_r = info->flags;
+ return mailbox_list_iter_deinit(&iter);
+ }
+
+ if (!list->set.iter_from_index_dir) {
+ rootdir = mailbox_list_get_root_forced(list, MAILBOX_LIST_PATH_TYPE_MAILBOX);
+ if (mailbox_list_get_path(list, name, MAILBOX_LIST_PATH_TYPE_DIR, &path) <= 0)
+ i_unreached();
+ } else {
+ rootdir = mailbox_list_get_root_forced(list, MAILBOX_LIST_PATH_TYPE_INDEX);
+ if (mailbox_list_get_path(list, name, MAILBOX_LIST_PATH_TYPE_INDEX, &path) <= 0)
+ i_unreached();
+ }
+
+ fname = strrchr(path, '/');
+ if (fname == NULL) {
+ fname = path;
+ dir = "/";
+ } else {
+ dir = t_strdup_until(path, fname);
+ fname++;
+ }
+
+ len = strlen(rootdir);
+ if (str_begins(path, rootdir) && path[len] == '/') {
+ /* looking up a regular mailbox under mail root dir */
+ } else if ((list->ns->flags & NAMESPACE_FLAG_INBOX_USER) != 0 &&
+ strcasecmp(name, "INBOX") == 0) {
+ /* looking up INBOX that's elsewhere */
+ } else {
+ /* looking up the root dir itself */
+ dir = path;
+ fname = "";
+ }
+ if (*fname == '\0' && *name == '\0' &&
+ (list->ns->flags & NAMESPACE_FLAG_INBOX_USER) != 0) {
+ /* if INBOX is in e.g. ~/Maildir, it shouldn't be possible to
+ access it also via namespace prefix. */
+ if (mailbox_list_get_path(list, "INBOX",
+ MAILBOX_LIST_PATH_TYPE_MAILBOX,
+ &inbox) <= 0)
+ i_unreached();
+ if (strcmp(inbox, dir) == 0) {
+ *flags_r |= MAILBOX_NONEXISTENT;
+ return 0;
+ }
+ }
+ return list->v.get_mailbox_flags(list, dir, fname,
+ MAILBOX_LIST_FILE_TYPE_UNKNOWN,
+ flags_r);
+}
+
+static bool mailbox_list_init_changelog(struct mailbox_list *list)
+{
+ struct mailbox_permissions perm;
+ const char *path;
+
+ if (list->changelog != NULL)
+ return TRUE;
+
+ /* don't do this in mailbox_list_create(), because _get_path() might be
+ overridden by storage (mbox). */
+ if (!mailbox_list_get_root_path(list, MAILBOX_LIST_PATH_TYPE_INDEX, &path))
+ return FALSE;
+
+ path = t_strconcat(path, "/"MAILBOX_LOG_FILE_NAME, NULL);
+ list->changelog = mailbox_log_alloc(list->ns->user->event, path);
+
+ mailbox_list_get_root_permissions(list, &perm);
+ mailbox_log_set_permissions(list->changelog, perm.file_create_mode,
+ perm.file_create_gid,
+ perm.file_create_gid_origin);
+ return TRUE;
+}
+
+int mailbox_list_mkdir_missing_index_root(struct mailbox_list *list)
+{
+ const char *index_dir;
+
+ if (list->index_root_dir_created)
+ return 1;
+
+ /* If index root dir hasn't been created yet, do it now.
+ Do this here even if the index directory is the same as mail root
+ directory, because it may not have been created elsewhere either. */
+ if (!mailbox_list_get_root_path(list, MAILBOX_LIST_PATH_TYPE_INDEX,
+ &index_dir))
+ return 0;
+
+ if (mailbox_list_mkdir_root(list, index_dir,
+ MAILBOX_LIST_PATH_TYPE_INDEX) < 0)
+ return -1;
+ list->index_root_dir_created = TRUE;
+ return 1;
+}
+
+int mailbox_list_mkdir_missing_list_index_root(struct mailbox_list *list)
+{
+ const char *index_dir;
+
+ if (list->set.list_index_dir == NULL)
+ return mailbox_list_mkdir_missing_index_root(list);
+
+ /* LISTINDEX points outside the index root directory */
+ if (list->list_index_root_dir_created)
+ return 1;
+
+ if (!mailbox_list_get_root_path(list, MAILBOX_LIST_PATH_TYPE_LIST_INDEX,
+ &index_dir))
+ return 0;
+ if (mailbox_list_mkdir_root(list, index_dir,
+ MAILBOX_LIST_PATH_TYPE_LIST_INDEX) < 0)
+ return -1;
+ list->list_index_root_dir_created = TRUE;
+ return 1;
+}
+
+void mailbox_list_add_change(struct mailbox_list *list,
+ enum mailbox_log_record_type type,
+ const guid_128_t mailbox_guid)
+{
+ struct mailbox_log_record rec;
+ time_t stamp;
+
+ if (!mailbox_list_init_changelog(list) ||
+ guid_128_is_empty(mailbox_guid))
+ return;
+
+ if (mailbox_list_mkdir_missing_index_root(list) <= 0)
+ return;
+
+ stamp = list->changelog_timestamp != (time_t)-1 ?
+ list->changelog_timestamp : ioloop_time;
+
+ i_zero(&rec);
+ rec.type = type;
+ memcpy(rec.mailbox_guid, mailbox_guid, sizeof(rec.mailbox_guid));
+ mailbox_log_record_set_timestamp(&rec, stamp);
+ (void)mailbox_log_append(list->changelog, &rec);
+}
+
+int mailbox_list_set_subscribed(struct mailbox_list *list,
+ const char *name, bool set)
+{
+ int ret;
+
+ /* make sure we'll refresh the file on next list */
+ list->subscriptions_mtime = (time_t)-1;
+
+ if ((ret = list->v.set_subscribed(list, name, set)) <= 0)
+ return ret;
+ return 0;
+}
+
+int mailbox_list_delete_dir(struct mailbox_list *list, const char *name)
+{
+ const char *error;
+
+ if (!mailbox_list_is_valid_name(list, name, &error) || *name == '\0') {
+ mailbox_list_set_error(list, MAIL_ERROR_PARAMS,
+ "Invalid mailbox name");
+ return -1;
+ }
+ return list->v.delete_dir(list, name);
+}
+
+int mailbox_list_delete_symlink(struct mailbox_list *list, const char *name)
+{
+ const char *error;
+
+ if (!mailbox_list_is_valid_name(list, name, &error) || *name == '\0') {
+ mailbox_list_set_error(list, MAIL_ERROR_PARAMS,
+ "Invalid mailbox name");
+ return -1;
+ }
+ return list->v.delete_symlink(list, name);
+}
+
+void mailbox_name_get_sha128(const char *name, guid_128_t guid_128_r)
+{
+ unsigned char sha[SHA1_RESULTLEN];
+
+ sha1_get_digest(name, strlen(name), sha);
+ memcpy(guid_128_r, sha, I_MIN(GUID_128_SIZE, sizeof(sha)));
+}
+
+struct mailbox_log *mailbox_list_get_changelog(struct mailbox_list *list)
+{
+ return !mailbox_list_init_changelog(list) ? NULL : list->changelog;
+}
+
+void mailbox_list_set_changelog_timestamp(struct mailbox_list *list,
+ time_t stamp)
+{
+ list->changelog_timestamp = stamp;
+}
+
+enum mailbox_list_file_type
+mailbox_list_get_file_type(const struct dirent *d ATTR_UNUSED)
+{
+ enum mailbox_list_file_type type;
+
+#ifdef HAVE_DIRENT_D_TYPE
+ switch (d->d_type) {
+ case DT_UNKNOWN:
+ type = MAILBOX_LIST_FILE_TYPE_UNKNOWN;
+ break;
+ case DT_REG:
+ type = MAILBOX_LIST_FILE_TYPE_FILE;
+ break;
+ case DT_DIR:
+ type = MAILBOX_LIST_FILE_TYPE_DIR;
+ break;
+ case DT_LNK:
+ type = MAILBOX_LIST_FILE_TYPE_SYMLINK;
+ break;
+ default:
+ type = MAILBOX_LIST_FILE_TYPE_OTHER;
+ break;
+ }
+#else
+ type = MAILBOX_LIST_FILE_TYPE_UNKNOWN;
+#endif
+ return type;
+}
+
+int mailbox_list_dirent_is_alias_symlink(struct mailbox_list *list,
+ const char *dir_path,
+ const struct dirent *d)
+{
+ struct stat st;
+ int ret;
+
+ if (mailbox_list_get_file_type(d) == MAILBOX_LIST_FILE_TYPE_SYMLINK)
+ return 1;
+
+ T_BEGIN {
+ const char *path, *linkpath, *error;
+
+ path = t_strconcat(dir_path, "/", d->d_name, NULL);
+ if (lstat(path, &st) < 0) {
+ mailbox_list_set_critical(list,
+ "lstat(%s) failed: %m", path);
+ ret = -1;
+ } else if (!S_ISLNK(st.st_mode)) {
+ ret = 0;
+ } else if (t_readlink(path, &linkpath, &error) < 0) {
+ e_error(list->ns->user->event,
+ "t_readlink(%s) failed: %s", path, error);
+ ret = -1;
+ } else {
+ /* it's an alias only if it points to the same
+ directory */
+ ret = strchr(linkpath, '/') == NULL ? 1 : 0;
+ }
+ } T_END;
+ return ret;
+}
+
+
+static bool
+mailbox_list_try_get_home_path(struct mailbox_list *list, const char **name)
+{
+ if ((*name)[1] == '/') {
+ /* ~/dir - use the configured home directory */
+ if (mail_user_try_home_expand(list->ns->user, name) < 0)
+ return FALSE;
+ } else {
+ /* ~otheruser/dir - assume we're using system users */
+ if (home_try_expand(name) < 0)
+ return FALSE;
+ }
+ return TRUE;
+}
+
+bool mailbox_list_try_get_absolute_path(struct mailbox_list *list,
+ const char **name)
+{
+ const char *root_dir, *path, *mailbox_name;
+ size_t len;
+
+ if (!list->mail_set->mail_full_filesystem_access)
+ return FALSE;
+
+ if (**name == '~') {
+ /* try to expand home directory */
+ if (!mailbox_list_try_get_home_path(list, name)) {
+ /* fallback to using actual "~name" mailbox */
+ return FALSE;
+ }
+ } else {
+ if (**name != '/')
+ return FALSE;
+ }
+
+ /* okay, we have an absolute path now. but check first if it points to
+ same directory as one of our regular mailboxes. */
+ root_dir = mailbox_list_get_root_forced(list, MAILBOX_LIST_PATH_TYPE_MAILBOX);
+ len = strlen(root_dir);
+ if (str_begins(*name, root_dir) && (*name)[len] == '/') {
+ mailbox_name = *name + len + 1;
+ if (mailbox_list_get_path(list, mailbox_name,
+ MAILBOX_LIST_PATH_TYPE_MAILBOX,
+ &path) <= 0)
+ return FALSE;
+ if (strcmp(path, *name) == 0) {
+ /* yeah, we can replace the full path with mailbox
+ name. this way we can use indexes. */
+ *name = mailbox_name;
+ return FALSE;
+ }
+ }
+ return TRUE;
+}
+
+const char *mailbox_list_get_last_error(struct mailbox_list *list,
+ enum mail_error *error_r)
+{
+ if (list->error == MAIL_ERROR_NONE) {
+ if (error_r != NULL)
+ *error_r = MAIL_ERROR_TEMP;
+ return list->error_string != NULL ? list->error_string :
+ "BUG: Unknown internal list error";
+ }
+
+ if (list->error_string == NULL) {
+ /* This shouldn't happen.. */
+ list->error_string =
+ i_strdup_printf("BUG: Unknown 0x%x list error",
+ list->error);
+ }
+
+ if (error_r != NULL)
+ *error_r = list->error;
+ return list->error_string;
+}
+
+enum mail_error mailbox_list_get_last_mail_error(struct mailbox_list *list)
+{
+ return list->error;
+}
+
+const char *mailbox_list_get_last_internal_error(struct mailbox_list *list,
+ enum mail_error *error_r)
+{
+ if (error_r != NULL)
+ *error_r = list->error;
+ if (list->last_error_is_internal) {
+ i_assert(list->last_internal_error != NULL);
+ return list->last_internal_error;
+ }
+ return mailbox_list_get_last_error(list, error_r);
+}
+
+void mailbox_list_clear_error(struct mailbox_list *list)
+{
+ i_free_and_null(list->error_string);
+
+ i_free(list->last_internal_error);
+ list->last_error_is_internal = FALSE;
+ list->error = MAIL_ERROR_NONE;
+}
+
+void mailbox_list_set_error(struct mailbox_list *list,
+ enum mail_error error, const char *string)
+{
+ if (list->error_string != string) {
+ i_free(list->error_string);
+ list->error_string = i_strdup(string);
+ }
+
+ list->last_error_is_internal = FALSE;
+ list->error = error;
+}
+
+void mailbox_list_set_internal_error(struct mailbox_list *list)
+{
+ const char *str;
+
+ str = t_strflocaltime(MAIL_ERRSTR_CRITICAL_MSG_STAMP, ioloop_time);
+ i_free(list->error_string);
+ list->error_string = i_strdup(str);
+ list->error = MAIL_ERROR_TEMP;
+
+ /* this function doesn't set last_internal_error, so
+ last_error_is_internal can't be TRUE. */
+ list->last_error_is_internal = FALSE;
+ i_free(list->last_internal_error);
+}
+
+void mailbox_list_set_critical(struct mailbox_list *list, const char *fmt, ...)
+{
+ char *old_error = list->error_string;
+ char *old_internal_error = list->last_internal_error;
+ va_list va;
+
+ list->error_string = NULL;
+ list->last_internal_error = NULL;
+ /* critical errors may contain sensitive data, so let user
+ see only "Internal error" with a timestamp to make it
+ easier to look from log files the actual error message. */
+ mailbox_list_set_internal_error(list);
+
+ va_start(va, fmt);
+ list->last_internal_error = i_strdup_vprintf(fmt, va);
+ va_end(va);
+ list->last_error_is_internal = TRUE;
+ e_error(list->ns->user->event, "%s", list->last_internal_error);
+
+ /* free the old_error and old_internal_error only after the new error
+ is generated, because they may be one of the parameters. */
+ i_free(old_error);
+ i_free(old_internal_error);
+}
+
+bool mailbox_list_set_error_from_errno(struct mailbox_list *list)
+{
+ const char *error_string;
+ enum mail_error error;
+
+ if (!mail_error_from_errno(&error, &error_string))
+ return FALSE;
+
+ mailbox_list_set_error(list, error, error_string);
+ return TRUE;
+}
+
+void mailbox_list_last_error_push(struct mailbox_list *list)
+{
+ struct mail_storage_error *err;
+
+ if (!array_is_created(&list->error_stack))
+ i_array_init(&list->error_stack, 2);
+ err = array_append_space(&list->error_stack);
+ err->error_string = i_strdup(list->error_string);
+ err->error = list->error;
+ err->last_error_is_internal = list->last_error_is_internal;
+ if (err->last_error_is_internal)
+ err->last_internal_error = i_strdup(list->last_internal_error);
+}
+
+void mailbox_list_last_error_pop(struct mailbox_list *list)
+{
+ unsigned int count = array_count(&list->error_stack);
+ const struct mail_storage_error *err =
+ array_idx(&list->error_stack, count-1);
+
+ i_free(list->error_string);
+ i_free(list->last_internal_error);
+ list->error_string = err->error_string;
+ list->error = err->error;
+ list->last_error_is_internal = err->last_error_is_internal;
+ list->last_internal_error = err->last_internal_error;
+ array_delete(&list->error_stack, count-1, 1);
+}
+
+int mailbox_list_init_fs(struct mailbox_list *list, struct event *event_parent,
+ const char *driver,
+ const char *args, const char *root_dir,
+ struct fs **fs_r, const char **error_r)
+{
+ struct fs_settings fs_set;
+ struct ssl_iostream_settings ssl_set;
+ struct mailbox_list_fs_context *ctx;
+ struct fs *parent_fs;
+
+ i_zero(&fs_set);
+ mail_user_init_fs_settings(list->ns->user, &fs_set, &ssl_set);
+ /* fs_set.event_parent points to user->event by default */
+ if (event_parent != NULL)
+ fs_set.event_parent = event_parent;
+ fs_set.root_path = root_dir;
+ fs_set.temp_file_prefix = mailbox_list_get_global_temp_prefix(list);
+
+ if (fs_init(driver, args, &fs_set, fs_r, error_r) < 0)
+ return -1;
+
+ /* add mailbox_list context to the parent fs, which allows
+ mailbox_list_fs_get_list() to work */
+ for (parent_fs = *fs_r; parent_fs->parent != NULL;
+ parent_fs = parent_fs->parent) ;
+
+ ctx = p_new(list->pool, struct mailbox_list_fs_context, 1);
+ ctx->list = list;
+ MODULE_CONTEXT_SET(parent_fs, mailbox_list_fs_module, ctx);
+
+ /* a bit kludgy notification to the fs that we're now finished setting
+ up the module context. */
+ (void)fs_get_properties(*fs_r);
+ return 0;
+}
+
+struct mailbox_list *mailbox_list_fs_get_list(struct fs *fs)
+{
+ struct mailbox_list_fs_context *ctx;
+
+ while (fs->parent != NULL)
+ fs = fs->parent;
+
+ ctx = MAILBOX_LIST_FS_CONTEXT(fs);
+ return ctx == NULL ? NULL : ctx->list;
+}
+
+int mailbox_list_lock(struct mailbox_list *list)
+{
+ struct mailbox_permissions perm;
+ struct file_create_settings set;
+ const char *lock_dir, *lock_fname, *lock_path, *error;
+
+ if (list->lock_refcount > 0) {
+ list->lock_refcount++;
+ return 0;
+ }
+
+ mailbox_list_get_root_permissions(list, &perm);
+ i_zero(&set);
+ set.lock_timeout_secs = list->mail_set->mail_max_lock_timeout == 0 ?
+ MAILBOX_LIST_LOCK_SECS :
+ I_MIN(MAILBOX_LIST_LOCK_SECS, list->mail_set->mail_max_lock_timeout);
+ set.lock_settings.lock_method = list->mail_set->parsed_lock_method;
+ set.mode = perm.file_create_mode;
+ set.gid = perm.file_create_gid;
+ set.gid_origin = perm.file_create_gid_origin;
+
+ lock_fname = MAILBOX_LIST_LOCK_FNAME;
+ if (list->set.volatile_dir != NULL) {
+ /* Use VOLATILEDIR. It's shared with all mailbox_lists, so use
+ hash of the namespace prefix as a way to make this lock name
+ unique across the namespaces. */
+ unsigned char ns_prefix_hash[SHA1_RESULTLEN];
+ sha1_get_digest(list->ns->prefix, list->ns->prefix_len,
+ ns_prefix_hash);
+ lock_fname = t_strconcat(MAILBOX_LIST_LOCK_FNAME,
+ binary_to_hex(ns_prefix_hash, sizeof(ns_prefix_hash)), NULL);
+ lock_dir = list->set.volatile_dir;
+ set.mkdir_mode = 0700;
+ } else if (mailbox_list_get_root_path(list, MAILBOX_LIST_PATH_TYPE_INDEX,
+ &lock_dir)) {
+ /* use index root directory */
+ if (mailbox_list_mkdir_missing_index_root(list) < 0)
+ return -1;
+ } else if (mailbox_list_get_root_path(list, MAILBOX_LIST_PATH_TYPE_DIR,
+ &lock_dir)) {
+ /* use mailbox root directory */
+ if (mailbox_list_mkdir_root(list, lock_dir,
+ MAILBOX_LIST_PATH_TYPE_DIR) < 0)
+ return -1;
+ } else {
+ /* No filesystem used by mailbox list (e.g. imapc).
+ Just assume it's locked */
+ list->lock_refcount = 1;
+ return 0;
+ }
+ lock_path = t_strdup_printf("%s/%s", lock_dir, lock_fname);
+ if (mail_storage_lock_create(lock_path, &set, list->mail_set,
+ &list->lock, &error) <= 0) {
+ mailbox_list_set_critical(list,
+ "Couldn't create mailbox list lock %s: %s",
+ lock_path, error);
+ return -1;
+ }
+
+ list->lock_refcount = 1;
+ return 0;
+}
+
+void mailbox_list_unlock(struct mailbox_list *list)
+{
+ i_assert(list->lock_refcount > 0);
+ if (--list->lock_refcount > 0)
+ return;
+
+ file_lock_free(&list->lock);
+}
diff --git a/src/lib-storage/mailbox-list.h b/src/lib-storage/mailbox-list.h
new file mode 100644
index 0000000..a329896
--- /dev/null
+++ b/src/lib-storage/mailbox-list.h
@@ -0,0 +1,339 @@
+#ifndef MAILBOX_LIST_H
+#define MAILBOX_LIST_H
+
+#include "mail-error.h"
+
+#ifdef PATH_MAX
+# define MAILBOX_LIST_NAME_MAX_LENGTH PATH_MAX
+#else
+# define MAILBOX_LIST_NAME_MAX_LENGTH 4096
+#endif
+
+struct fs;
+struct mail_namespace;
+struct mail_storage;
+struct mailbox_list;
+
+enum mailbox_list_properties {
+ /* maildir_name must always be empty */
+ MAILBOX_LIST_PROP_NO_MAILDIR_NAME = 0x01,
+ /* alt directories not supported */
+ MAILBOX_LIST_PROP_NO_ALT_DIR = 0x02,
+ /* no support for \noselect directories, only mailboxes */
+ MAILBOX_LIST_PROP_NO_NOSELECT = 0x04,
+ /* mail root directory isn't required */
+ MAILBOX_LIST_PROP_NO_ROOT = 0x08,
+ /* Automatically create mailbox directories when needed. Normally it's
+ assumed that if a mailbox directory doesn't exist, the mailbox
+ doesn't exist either. */
+ MAILBOX_LIST_PROP_AUTOCREATE_DIRS = 0x10,
+ /* Explicitly disable mailbox list index */
+ MAILBOX_LIST_PROP_NO_LIST_INDEX = 0x20,
+ /* Disable checking mailbox_list.is_internal_name(). The layout is
+ implemented in a way that there aren't any such reserved internal
+ names. For example Maildir++ prefixes all mailboxes with "." */
+ MAILBOX_LIST_PROP_NO_INTERNAL_NAMES = 0x40,
+};
+
+enum mailbox_list_flags {
+ /* Mailboxes are files, not directories. */
+ MAILBOX_LIST_FLAG_MAILBOX_FILES = 0x01,
+ /* Namespace already has a mailbox list, don't assign this
+ mailbox list to it. */
+ MAILBOX_LIST_FLAG_SECONDARY = 0x02,
+ /* There are no mail files, only index and/or control files. */
+ MAILBOX_LIST_FLAG_NO_MAIL_FILES = 0x04,
+ /* LAYOUT=index: Don't delete any files in delete_mailbox(). */
+ MAILBOX_LIST_FLAG_NO_DELETES = 0x08
+};
+
+enum mailbox_info_flags {
+ MAILBOX_NOSELECT = 0x001,
+ MAILBOX_NONEXISTENT = 0x002,
+ MAILBOX_CHILDREN = 0x004,
+ MAILBOX_NOCHILDREN = 0x008,
+ MAILBOX_NOINFERIORS = 0x010,
+ MAILBOX_MARKED = 0x020,
+ MAILBOX_UNMARKED = 0x040,
+ MAILBOX_SUBSCRIBED = 0x080,
+ MAILBOX_CHILD_SUBSCRIBED = 0x100,
+ MAILBOX_CHILD_SPECIALUSE = 0x200,
+
+ /* Internally used by lib-storage, use mailbox_info.special_use
+ to actually access these: */
+ MAILBOX_SPECIALUSE_ALL = 0x00010000,
+ MAILBOX_SPECIALUSE_ARCHIVE = 0x00020000,
+ MAILBOX_SPECIALUSE_DRAFTS = 0x00040000,
+ MAILBOX_SPECIALUSE_FLAGGED = 0x00080000,
+ MAILBOX_SPECIALUSE_JUNK = 0x00100000,
+ MAILBOX_SPECIALUSE_SENT = 0x00200000,
+ MAILBOX_SPECIALUSE_TRASH = 0x00400000,
+ MAILBOX_SPECIALUSE_IMPORTANT = 0x00800000,
+#define MAILBOX_SPECIALUSE_MASK 0x00ff0000
+
+ /* Internally used by lib-storage: */
+ MAILBOX_SELECT = 0x20000000,
+ MAILBOX_MATCHED = 0x40000000
+};
+
+enum mailbox_list_path_type {
+ /* Return directory's path (eg. ~/dbox/INBOX) */
+ MAILBOX_LIST_PATH_TYPE_DIR,
+ MAILBOX_LIST_PATH_TYPE_ALT_DIR,
+ /* Return mailbox path (eg. ~/dbox/INBOX/dbox-Mails) */
+ MAILBOX_LIST_PATH_TYPE_MAILBOX,
+ MAILBOX_LIST_PATH_TYPE_ALT_MAILBOX,
+ /* Return control directory */
+ MAILBOX_LIST_PATH_TYPE_CONTROL,
+ /* Return index directory ("" for in-memory) */
+ MAILBOX_LIST_PATH_TYPE_INDEX,
+ /* Return the private index directory (NULL if none) */
+ MAILBOX_LIST_PATH_TYPE_INDEX_PRIVATE,
+ /* Return the index cache directory (usually same as
+ MAILBOX_LIST_PATH_TYPE_INDEX) */
+ MAILBOX_LIST_PATH_TYPE_INDEX_CACHE,
+ /* Return mailbox list index directory (usually same as
+ MAILBOX_LIST_PATH_TYPE_INDEX) */
+ MAILBOX_LIST_PATH_TYPE_LIST_INDEX,
+};
+
+enum mailbox_list_file_type {
+ MAILBOX_LIST_FILE_TYPE_UNKNOWN = 0,
+ MAILBOX_LIST_FILE_TYPE_FILE,
+ MAILBOX_LIST_FILE_TYPE_DIR,
+ MAILBOX_LIST_FILE_TYPE_SYMLINK,
+ MAILBOX_LIST_FILE_TYPE_OTHER
+};
+
+struct mailbox_list_settings {
+ const char *layout; /* FIXME: shouldn't be here */
+ const char *root_dir;
+ const char *index_dir;
+ const char *index_pvt_dir;
+ const char *index_cache_dir;
+ const char *control_dir;
+ const char *alt_dir; /* FIXME: dbox-specific.. */
+ /* Backend-local directory where volatile data, such as lock files,
+ can be temporarily created. This setting allows specifying a
+ separate directory for them to reduce disk I/O on the real storage.
+ The volatile_dir can point to an in-memory filesystem. */
+ const char *volatile_dir;
+
+ const char *inbox_path;
+ const char *subscription_fname;
+ const char *list_index_fname;
+ /* Mailbox list index directory. NULL defaults to index directory.
+ The path may be relative to the index directory. */
+ const char *list_index_dir;
+ /* If non-empty, it means that mails exist in a maildir_name
+ subdirectory. eg. if you have a directory containing directories:
+
+ mail/
+ mail/foo/
+ mail/foo/Maildir
+
+ If mailbox_name is empty, you have mailboxes "mail", "mail/foo" and
+ "mail/foo/Maildir".
+
+ If mailbox_name is "Maildir", you have a non-selectable mailbox
+ "mail" and a selectable mailbox "mail/foo". */
+ const char *maildir_name;
+ /* if set, store mailboxes under root_dir/mailbox_dir_name/.
+ this setting contains either "" or "dir/". */
+ const char *mailbox_dir_name;
+
+ /* Used for escaping the mailbox name in storage (storage_name). If the
+ UTF-8 vname has characters that can't reversibly (or safely) be
+ converted to storage_name and back, encode the problematic parts
+ using <storage_name_escape_char><hex>. The storage_name_escape_char
+ itself also has to be encoded the same way. For example
+ { vname="A/B.C%D", storage_name_escape_char='%', namespace_sep='/',
+ storage_sep='.' } -> storage_name="A.B%2eC%25D". */
+ char storage_name_escape_char;
+ /* Used for escaping the user/client-visible UTF-8 vname. If the
+ storage_name can't be converted reversibly to the vname and back,
+ encode the problematic parts using <vname_escape_char><hex>. The
+ vname_escape_char itself also has to be encoded the same way. For
+ example { storage_name="A/B.C%D", vname_escape_char='%',
+ namespace_sep='/', storage_sep='.' } -> vname="A%2fB/C%25D".
+
+ Note that it's possible for escape_char and broken_char to be the
+ same character. They're just used for different directions in
+ conversion. */
+ char vname_escape_char;
+ /* Use UTF-8 mailbox names on filesystem instead of mUTF-7 */
+ bool utf8;
+ /* Don't check/create the alt-dir symlink. */
+ bool alt_dir_nocheck;
+ /* Use maildir_name also for index/control directories. This should
+ have been the default since the beginning, but for backwards
+ compatibility it had to be made an option. */
+ bool index_control_use_maildir_name;
+ /* Perform mailbox iteration using the index directory instead of the
+ mail root directory. This can be helpful if the indexes are on a
+ faster storage. This could perhaps be made the default at some point,
+ but for now since it's less tested it's optional. */
+ bool iter_from_index_dir;
+ /* Avoid creating or listing \NoSelect mailboxes. */
+ bool no_noselect;
+ /* Do not validate names as fs names (allows weird names) */
+ bool no_fs_validation;
+};
+
+struct mailbox_permissions {
+ /* The actual uid/gid of the mailbox */
+ uid_t file_uid;
+ gid_t file_gid;
+
+ /* mode and GID to use for newly created files/dirs.
+ (gid_t)-1 is used if the default GID can be used. */
+ mode_t file_create_mode, dir_create_mode;
+ gid_t file_create_gid;
+ /* origin (e.g. path) where the file_create_gid was got from */
+ const char *file_create_gid_origin;
+
+ bool gid_origin_is_mailbox_path;
+ bool mail_index_permissions_set;
+};
+
+/* register all drivers */
+void mailbox_list_register_all(void);
+
+void mailbox_list_register(const struct mailbox_list *list);
+void mailbox_list_unregister(const struct mailbox_list *list);
+
+const struct mailbox_list *
+mailbox_list_find_class(const char *driver);
+
+/* Returns 0 if ok, -1 if driver was unknown. */
+int mailbox_list_create(const char *driver, struct mail_namespace *ns,
+ const struct mailbox_list_settings *set,
+ enum mailbox_list_flags flags,
+ struct mailbox_list **list_r, const char **error_r);
+void mailbox_list_destroy(struct mailbox_list **list);
+
+const char *
+mailbox_list_get_driver_name(const struct mailbox_list *list) ATTR_PURE;
+const struct mailbox_list_settings *
+mailbox_list_get_settings(const struct mailbox_list *list) ATTR_PURE;
+enum mailbox_list_flags
+mailbox_list_get_flags(const struct mailbox_list *list) ATTR_PURE;
+struct mail_namespace *
+mailbox_list_get_namespace(const struct mailbox_list *list) ATTR_PURE;
+struct mail_user *
+mailbox_list_get_user(const struct mailbox_list *list) ATTR_PURE;
+int mailbox_list_get_storage(struct mailbox_list **list, const char *vname,
+ struct mail_storage **storage_r);
+void mailbox_list_get_default_storage(struct mailbox_list *list,
+ struct mail_storage **storage);
+char mailbox_list_get_hierarchy_sep(struct mailbox_list *list);
+
+/* Returns the mode and GID that should be used when creating new files and
+ directories to the specified mailbox. (gid_t)-1 is returned if it's not
+ necessary to change the default gid. */
+void mailbox_list_get_permissions(struct mailbox_list *list, const char *name,
+ struct mailbox_permissions *permissions_r);
+/* Like mailbox_list_get_permissions(), but for creating files/dirs to the
+ mail root directory (or even the root dir itself). */
+void mailbox_list_get_root_permissions(struct mailbox_list *list,
+ struct mailbox_permissions *permissions_r);
+/* mkdir() a root directory of given type with proper permissions. The path can
+ be either the root itself or point to a directory under the root. */
+int mailbox_list_mkdir_root(struct mailbox_list *list, const char *path,
+ enum mailbox_list_path_type type);
+/* Like mailbox_list_mkdir_root(), but don't log an error if it fails. */
+int mailbox_list_try_mkdir_root(struct mailbox_list *list, const char *path,
+ enum mailbox_list_path_type type,
+ const char **error_r);
+/* Call mailbox_list_mkdir_root() for index, unless the index root is the
+ same as mailbox root. Returns 1 if ok, 0 if there are no indexes, -1 if
+ error. Calling this multiple times does the check only once. */
+int mailbox_list_mkdir_missing_index_root(struct mailbox_list *list);
+/* Like mailbox_list_mkdir_missing_index_root(), but for mailbox list
+ index root. */
+int mailbox_list_mkdir_missing_list_index_root(struct mailbox_list *list);
+
+/* Returns TRUE if name is ok, FALSE if it can't be safely passed to
+ mailbox_list_*() functions */
+bool mailbox_list_is_valid_name(struct mailbox_list *list,
+ const char *name, const char **error_r);
+
+const char *mailbox_list_get_storage_name(struct mailbox_list *list,
+ const char *vname);
+const char *mailbox_list_get_vname(struct mailbox_list *list, const char *name);
+
+/* Get path to specified type of files in mailbox. Returns -1 if an error
+ occurred (e.g. mailbox no longer exists), 0 if there are no files of this
+ type (in-memory index, no alt dir, storage with no files), 1 if path was
+ returned successfully. The path is set to NULL when returning -1/0. */
+int mailbox_list_get_path(struct mailbox_list *list, const char *name,
+ enum mailbox_list_path_type type,
+ const char **path_r);
+/* Get path to the root directory for files of specified type. Returns TRUE
+ if path was returned, FALSE if there are no files of this type. */
+bool mailbox_list_get_root_path(struct mailbox_list *list,
+ enum mailbox_list_path_type type,
+ const char **path_r);
+/* Like mailbox_list_get_root_path(), but assume that the root directory
+ exists (assert crash if not) */
+const char *mailbox_list_get_root_forced(struct mailbox_list *list,
+ enum mailbox_list_path_type type);
+/* Returns mailbox's change log, or NULL if it doesn't have one. */
+struct mailbox_log *mailbox_list_get_changelog(struct mailbox_list *list);
+/* Specify timestamp to use when writing mailbox changes to changelog.
+ The same timestamp is used until stamp is set to (time_t)-1, after which
+ current time is used */
+void mailbox_list_set_changelog_timestamp(struct mailbox_list *list,
+ time_t stamp);
+
+/* Returns a prefix that temporary files should use without conflicting
+ with the namespace. */
+const char *mailbox_list_get_temp_prefix(struct mailbox_list *list);
+/* Returns prefix that's common to all get_temp_prefix() calls.
+ Typically this returns either "temp." or ".temp.". */
+const char *mailbox_list_get_global_temp_prefix(struct mailbox_list *list);
+
+/* Subscribe/unsubscribe mailbox. There should be no error when
+ subscribing to already subscribed mailbox. Subscribing to
+ unexisting mailboxes is optional. */
+int mailbox_list_set_subscribed(struct mailbox_list *list,
+ const char *name, bool set);
+
+/* Delete a non-selectable mailbox. Fail if the mailbox is selectable. */
+int mailbox_list_delete_dir(struct mailbox_list *list, const char *name);
+/* Delete a symlinked mailbox. Fail if the mailbox isn't a symlink. */
+int mailbox_list_delete_symlink(struct mailbox_list *list, const char *name);
+
+/* Returns the error message of last occurred error. */
+const char * ATTR_NOWARN_UNUSED_RESULT
+mailbox_list_get_last_error(struct mailbox_list *list,
+ enum mail_error *error_r);
+/* Wrapper for mailbox_list_get_last_error() */
+enum mail_error mailbox_list_get_last_mail_error(struct mailbox_list *list);
+
+const char * ATTR_NOWARN_UNUSED_RESULT
+mailbox_list_get_last_internal_error(struct mailbox_list *list,
+ enum mail_error *error_r);
+
+/* Save the last error until it's popped. This is useful for cases where the
+ list operation has already failed, but the cleanup code path changes the
+ error to something else unwanted. */
+void mailbox_list_last_error_push(struct mailbox_list *list);
+void mailbox_list_last_error_pop(struct mailbox_list *list);
+
+/* Create a fs based on the settings in the given mailbox_list. If event_parent
+ is NULL, use user->event as the parent. */
+int mailbox_list_init_fs(struct mailbox_list *list, struct event *event_parent,
+ const char *driver,
+ const char *args, const char *root_dir,
+ struct fs **fs_r, const char **error_r);
+/* Return mailbox_list that was used to create the fs via
+ mailbox_list_init_fs(). */
+struct mailbox_list *mailbox_list_fs_get_list(struct fs *fs);
+
+/* Escape/Unescape mailbox name in place. */
+void mailbox_list_name_unescape(const char **name, char escape_char);
+void mailbox_list_name_escape(const char *name, const char *escape_chars,
+ string_t *dest);
+
+#endif
diff --git a/src/lib-storage/mailbox-lua.c b/src/lib-storage/mailbox-lua.c
new file mode 100644
index 0000000..19327ba
--- /dev/null
+++ b/src/lib-storage/mailbox-lua.c
@@ -0,0 +1,366 @@
+/* Copyright (c) 2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "str.h"
+#include "istream.h"
+#include "array.h"
+#include "var-expand.h"
+#include "mail-storage.h"
+#include "mailbox-attribute.h"
+#include "mail-storage-lua.h"
+#include "mail-storage-lua-private.h"
+#include "mail-user.h"
+
+#define LUA_STORAGE_MAILBOX "struct mailbox"
+
+static int lua_storage_mailbox_gc(lua_State *L);
+
+void dlua_push_mailbox(lua_State *L, struct mailbox *box)
+{
+ luaL_checkstack(L, 4, "out of memory");
+ /* create a table for holding few things */
+ lua_createtable(L, 0, 0);
+ luaL_setmetatable(L, LUA_STORAGE_MAILBOX);
+
+ struct mailbox **ptr = lua_newuserdata(L, sizeof(struct mailbox*));
+ *ptr = box;
+ lua_createtable(L, 0, 1);
+ lua_pushcfunction(L, lua_storage_mailbox_gc);
+ lua_setfield(L, -2, "__gc");
+ lua_setmetatable(L, -2);
+ lua_setfield(L, -2, "item");
+
+ luaL_checkstack(L, 2, "out of memory");
+ lua_pushstring(L, mailbox_get_vname(box));
+ lua_setfield(L, -2, "vname");
+
+ lua_pushstring(L, mailbox_get_name(box));
+ lua_setfield(L, -2, "name");
+}
+
+static struct mailbox *
+lua_check_storage_mailbox(lua_State *L, int arg)
+{
+ if (!lua_istable(L, arg)) {
+ (void)luaL_error(L, "Bad argument #%d, expected %s got %s",
+ arg, LUA_STORAGE_MAILBOX,
+ lua_typename(L, lua_type(L, arg)));
+ }
+ lua_pushliteral(L, "item");
+ lua_rawget(L, arg);
+ struct mailbox **bp = lua_touserdata(L, -1);
+ lua_pop(L, 1);
+ return *bp;
+}
+
+static int lua_storage_mailbox_tostring(lua_State *L)
+{
+ DLUA_REQUIRE_ARGS(L, 1);
+ struct mailbox *mbox = lua_check_storage_mailbox(L, 1);
+
+ lua_pushstring(L, mailbox_get_vname(mbox));
+ return 1;
+}
+
+/* special case, we want to ensure this is eq when mailboxes
+ are really equal */
+static int lua_storage_mailbox_eq(lua_State *L)
+{
+ DLUA_REQUIRE_ARGS(L, 2);
+ struct mailbox *mbox = lua_check_storage_mailbox(L, 1);
+ struct mailbox *mbox2 = lua_check_storage_mailbox(L, 2);
+ lua_pushboolean(L, DLUA_MAILBOX_EQUALS(mbox, mbox2));
+ return 1;
+}
+
+/* these compare based to mailbox vname */
+static int lua_storage_mailbox_lt(lua_State *L)
+{
+ DLUA_REQUIRE_ARGS(L, 2);
+ bool res = lua_storage_cmp(L) <= 0;
+ lua_pushboolean(L, res);
+ return 1;
+}
+
+static int lua_storage_mailbox_le(lua_State *L)
+{
+ DLUA_REQUIRE_ARGS(L, 2);
+ bool res = lua_storage_cmp(L) < 0;
+ lua_pushboolean(L, res);
+ return 1;
+}
+
+static int lua_storage_mailbox_unref(lua_State *L)
+{
+ DLUA_REQUIRE_ARGS(L, 1);
+ /* fetch item from table */
+ lua_pushliteral(L, "item");
+ lua_rawget(L, 1);
+ struct mailbox **mbox = lua_touserdata(L, -1);
+ if (*mbox != NULL)
+ mailbox_free(mbox);
+ *mbox = NULL;
+ lua_pop(L, 1);
+ return 0;
+}
+
+static int lua_storage_mailbox_gc(lua_State *L)
+{
+ struct mailbox **mbox = lua_touserdata(L, 1);
+
+ if (*mbox != NULL)
+ mailbox_free(mbox);
+
+ return 0;
+}
+
+static int lua_storage_mailbox_open(lua_State *L)
+{
+ DLUA_REQUIRE_ARGS(L, 1);
+ struct mailbox *mbox = lua_check_storage_mailbox(L, 1);
+
+ /* try to open the box */
+ if (mailbox_open(mbox) < 0) {
+ return luaL_error(L, "mailbox_open(%s) failed: %s",
+ mailbox_get_vname(mbox),
+ mailbox_get_last_error(mbox, NULL));
+ }
+
+ return 0;
+}
+
+static int lua_storage_mailbox_close(lua_State *L)
+{
+ DLUA_REQUIRE_ARGS(L, 1);
+ struct mailbox *mbox = lua_check_storage_mailbox(L, 1);
+
+ mailbox_close(mbox);
+
+ return 0;
+}
+
+static int lua_storage_mailbox_sync(lua_State *L)
+{
+ DLUA_REQUIRE_ARGS_IN(L, 1, 2);
+ struct mailbox *mbox = lua_check_storage_mailbox(L, 1);
+ enum mailbox_sync_flags flags = 0;
+
+ if (lua_gettop(L) >= 2)
+ flags = luaL_checkinteger(L, 2);
+
+ if (mailbox_sync(mbox, flags) < 0) {
+ const char *error = mailbox_get_last_error(mbox, NULL);
+ return luaL_error(L, "mailbox_sync(%s) failed: %s",
+ mailbox_get_vname(mbox), error);
+ }
+
+ return 0;
+}
+
+static int lua_storage_mailbox_status(lua_State *L)
+{
+ struct mailbox_status status;
+ const char *keyword;
+ struct mailbox *mbox = lua_check_storage_mailbox(L, 1);
+ /* get items as list of parameters */
+ enum mailbox_status_items items = 0;
+
+ if (lua_gettop(L) < 2)
+ return luaL_error(L, "expecting at least 1 parameter");
+ for(int i = 2; i <= lua_gettop(L); i++)
+ items |= (unsigned int)luaL_checkinteger(L, i);
+
+ i_zero(&status);
+ if (mailbox_get_status(mbox, items, &status) < 0) {
+ const char *error = mailbox_get_last_error(mbox, NULL);
+ return luaL_error(L, "mailbox_get_status(%s, %u) failed: %s",
+ mailbox_get_vname(mbox), items, error);
+ }
+ /* returns a table */
+ lua_createtable(L, 0, 20);
+
+ lua_pushstring(L, mailbox_get_vname(mbox));
+ lua_setfield(L, -2, "mailbox");
+
+#undef LUA_TABLE_SET_NUMBER
+#define LUA_TABLE_SET_NUMBER(field) \
+ lua_pushnumber(L, status.field); \
+ lua_setfield(L, -2, #field);
+#undef LUA_TABLE_SET_BOOL
+#define LUA_TABLE_SET_BOOL(field) \
+ lua_pushboolean(L, status.field); \
+ lua_setfield(L, -2, #field);
+
+ LUA_TABLE_SET_NUMBER(messages);
+ LUA_TABLE_SET_NUMBER(recent);
+ LUA_TABLE_SET_NUMBER(unseen);
+ LUA_TABLE_SET_NUMBER(uidvalidity);
+ LUA_TABLE_SET_NUMBER(uidnext);
+ LUA_TABLE_SET_NUMBER(first_unseen_seq);
+ LUA_TABLE_SET_NUMBER(first_recent_uid);
+ LUA_TABLE_SET_NUMBER(highest_modseq);
+ LUA_TABLE_SET_NUMBER(highest_pvt_modseq);
+
+ LUA_TABLE_SET_NUMBER(permanent_flags);
+ LUA_TABLE_SET_NUMBER(flags);
+
+ LUA_TABLE_SET_BOOL(permanent_keywords);
+ LUA_TABLE_SET_BOOL(allow_new_keywords);
+ LUA_TABLE_SET_BOOL(nonpermanent_modseqs);
+ LUA_TABLE_SET_BOOL(no_modseq_tracking);
+ LUA_TABLE_SET_BOOL(have_guids);
+ LUA_TABLE_SET_BOOL(have_save_guids);
+ LUA_TABLE_SET_BOOL(have_only_guid128);
+
+ if (status.keywords != NULL && array_is_created(status.keywords)) {
+ int i = 1;
+ lua_createtable(L, array_count(status.keywords), 0);
+ array_foreach_elem(status.keywords, keyword) {
+ lua_pushstring(L, keyword);
+ lua_rawseti(L, -2, i++);
+ }
+ lua_setfield(L, -2, "keywords");
+ }
+
+ return 1;
+}
+
+static int lua_storage_mailbox_metadata_get(lua_State *L)
+{
+ if (lua_gettop(L) < 2)
+ return luaL_error(L, "expecting at least 1 parameter");
+ struct mailbox *mbox = lua_check_storage_mailbox(L, 1);
+ const char *value, *error;
+ size_t value_len;
+ int ret, i, top = lua_gettop(L);
+
+ ret = 0;
+ for(i = 2; i <= top; i++) {
+ const char *key = lua_tostring(L, i);
+ if (key == NULL) {
+ ret = -1;
+ error = t_strdup_printf("expected string at #%d", i);
+ break;
+ }
+
+ if ((ret = lua_storage_mailbox_attribute_get(mbox, key, &value,
+ &value_len, &error)) < 0) {
+ break;
+ } else if (ret == 0) {
+ lua_pushnil(L);
+ } else {
+ lua_pushlstring(L, value, value_len);
+ }
+ }
+
+ if (ret < 0)
+ return luaL_error(L, "%s", error);
+
+ /* return number of pushed items */
+ i_assert(i>=2);
+ return i-2;
+}
+
+static int lua_storage_mailbox_metadata_set(lua_State *L)
+{
+ DLUA_REQUIRE_ARGS(L, 3);
+ struct mailbox *mbox = lua_check_storage_mailbox(L, 1);
+ const char *key = luaL_checkstring(L, 2);
+ const char *value, *error;
+ size_t value_len;
+
+ value = lua_tolstring(L, 3, &value_len);
+
+ if (lua_storage_mailbox_attribute_set(mbox, key, value, value_len, &error) < 0)
+ return luaL_error(L, "Cannot set attribute: %s", error);
+
+ return 0;
+}
+
+static int lua_storage_mailbox_metadata_unset(lua_State *L)
+{
+ DLUA_REQUIRE_ARGS(L, 2);
+ struct mailbox *mbox = lua_check_storage_mailbox(L, 1);
+ const char *key = luaL_checkstring(L, 2);
+ const char *error;
+
+ if (lua_storage_mailbox_attribute_set(mbox, key, NULL, 0, &error) < 0)
+ return luaL_error(L, "Cannot unset attribute: %s", error);
+
+ return 0;
+}
+
+static int lua_storage_mailbox_metadata_list(lua_State *L)
+{
+ if (lua_gettop(L) < 2)
+ return luaL_error(L, "expecting at least 1 parameter");
+ struct mailbox *mbox = lua_check_storage_mailbox(L, 1);
+ const struct lua_storage_keyvalue *item;
+ const char *error;
+ ARRAY_TYPE(lua_storage_keyvalue) items;
+ int i, ret;
+
+ T_BEGIN {
+ t_array_init(&items, 1);
+
+ ret = 0;
+ for(i = 2; i <= lua_gettop(L); i++) {
+ const char *key = lua_tostring(L, i);
+
+ if (key == NULL) {
+ ret = -1;
+ error = t_strdup_printf("expected string at #%d", i);
+ break;
+ }
+
+ if (lua_storage_mailbox_attribute_list(mbox, key, &items,
+ &error) < 0) {
+ ret = -1;
+ break;
+ }
+ }
+
+ if (ret == 0) {
+ lua_createtable(L, 0, array_count(&items));
+ array_foreach(&items, item) {
+ /* push value */
+ lua_pushlstring(L, item->value,
+ item->value_len);
+ /* set field */
+ lua_setfield(L, -2, item->key);
+ }
+ }
+ } T_END;
+
+ if (ret == -1)
+ return luaL_error(L, "%s", error);
+
+ /* stack should have table with items */
+ return 1;
+}
+
+static luaL_Reg lua_storage_mailbox_methods[] = {
+ { "__tostring", lua_storage_mailbox_tostring },
+ { "__eq", lua_storage_mailbox_eq },
+ { "__lt", lua_storage_mailbox_lt },
+ { "__le", lua_storage_mailbox_le },
+ { "free", lua_storage_mailbox_unref },
+ { "status", lua_storage_mailbox_status },
+ { "open", lua_storage_mailbox_open },
+ { "close", lua_storage_mailbox_close },
+ { "sync", lua_storage_mailbox_sync },
+ { "metadata_get", lua_storage_mailbox_metadata_get },
+ { "metadata_set", lua_storage_mailbox_metadata_set },
+ { "metadata_unset", lua_storage_mailbox_metadata_unset },
+ { "metadata_list", lua_storage_mailbox_metadata_list },
+ { NULL, NULL }
+};
+
+void lua_storage_mailbox_register(struct dlua_script *script)
+{
+ luaL_newmetatable(script->L, LUA_STORAGE_MAILBOX);
+ lua_pushvalue(script->L, -1);
+ lua_setfield(script->L, -2, "__index");
+ luaL_setfuncs(script->L, lua_storage_mailbox_methods, 0);
+ lua_pop(script->L, 1);
+}
diff --git a/src/lib-storage/mailbox-match-plugin.c b/src/lib-storage/mailbox-match-plugin.c
new file mode 100644
index 0000000..276d134
--- /dev/null
+++ b/src/lib-storage/mailbox-match-plugin.c
@@ -0,0 +1,83 @@
+/* Copyright (c) 2021 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "str.h"
+#include "wildcard-match.h"
+#include "mail-storage-private.h"
+#include "mailbox-match-plugin.h"
+
+struct mailbox_match_plugin {
+ ARRAY_TYPE(const_string) patterns;
+};
+
+struct mailbox_match_plugin *
+mailbox_match_plugin_init(struct mail_user *user, const char *set_prefix)
+{
+ struct mailbox_match_plugin *match;
+ string_t *str;
+ const char *value;
+
+ match = i_new(struct mailbox_match_plugin, 1);
+
+ value = mail_user_plugin_getenv(user, set_prefix);
+ if (value == NULL)
+ return match;
+
+ i_array_init(&match->patterns, 16);
+ str = t_str_new(128);
+ for (unsigned int i = 2; value != NULL; i++) {
+ /* value points to user's settings, so there's no need to
+ strdup() it. */
+ array_push_back(&match->patterns, &value);
+
+ str_truncate(str, 0);
+ str_printfa(str, "%s%u", set_prefix, i);
+
+ value = mail_user_plugin_getenv(user, str_c(str));
+ }
+
+ return match;
+}
+
+bool mailbox_match_plugin_exclude(struct mailbox_match_plugin *match,
+ struct mailbox *box)
+{
+ const struct mailbox_settings *set;
+ const char *const *special_use;
+ const char *str;
+
+ if (!array_is_created(&match->patterns))
+ return FALSE;
+
+ set = mailbox_settings_find(mailbox_get_namespace(box),
+ mailbox_get_vname(box));
+ special_use = set == NULL ? NULL :
+ t_strsplit_spaces(set->special_use, " ");
+
+ array_foreach_elem(&match->patterns, str) {
+ if (str[0] == '\\') {
+ /* \Special-use flag */
+ if (special_use != NULL &&
+ str_array_icase_find(special_use, str))
+ return TRUE;
+ } else {
+ /* mailbox name with wildcards */
+ if (wildcard_match(box->name, str))
+ return TRUE;
+ }
+ }
+ return FALSE;
+}
+
+void mailbox_match_plugin_deinit(struct mailbox_match_plugin **_match)
+{
+ struct mailbox_match_plugin *match = *_match;
+
+ if (match == NULL)
+ return;
+ *_match = NULL;
+
+ array_free(&match->patterns);
+ i_free(match);
+}
diff --git a/src/lib-storage/mailbox-match-plugin.h b/src/lib-storage/mailbox-match-plugin.h
new file mode 100644
index 0000000..c6dbfe9
--- /dev/null
+++ b/src/lib-storage/mailbox-match-plugin.h
@@ -0,0 +1,16 @@
+#ifndef MAILBOX_MATCH_PLUGIN_H
+#define MAILBOX_MATCH_PLUGIN_H
+
+struct mailbox;
+
+/* Utility library to allow a Dovecot plugin an easy way to configure a list
+ of mailbox patterns and special-use flags that can be matched against. */
+
+struct mailbox_match_plugin *
+mailbox_match_plugin_init(struct mail_user *user, const char *set_prefix);
+void mailbox_match_plugin_deinit(struct mailbox_match_plugin **match);
+
+bool mailbox_match_plugin_exclude(struct mailbox_match_plugin *match,
+ struct mailbox *box);
+
+#endif
diff --git a/src/lib-storage/mailbox-recent-flags.c b/src/lib-storage/mailbox-recent-flags.c
new file mode 100644
index 0000000..5129809
--- /dev/null
+++ b/src/lib-storage/mailbox-recent-flags.c
@@ -0,0 +1,106 @@
+/* Copyright (c) 2002-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "mail-storage-private.h"
+#include "mailbox-recent-flags.h"
+
+void mailbox_recent_flags_set_uid(struct mailbox *box, uint32_t uid)
+{
+ if (uid <= box->recent_flags_prev_uid) {
+ if (seq_range_exists(&box->recent_flags, uid))
+ return;
+
+ mailbox_set_critical(box, "Recent flags state corrupted");
+ array_clear(&box->recent_flags);
+ box->recent_flags_count = 0;
+ }
+ mailbox_recent_flags_set_uid_forced(box, uid);
+}
+
+void mailbox_recent_flags_set_uid_forced(struct mailbox *box, uint32_t uid)
+{
+ box->recent_flags_prev_uid = uid;
+
+ if (!mailbox_recent_flags_have_uid(box, uid)) {
+ seq_range_array_add_with_init(&box->recent_flags, 64, uid);
+ box->recent_flags_count++;
+ }
+}
+
+void mailbox_recent_flags_set_seqs(struct mailbox *box,
+ struct mail_index_view *view,
+ uint32_t seq1, uint32_t seq2)
+{
+ uint32_t uid;
+
+ for (; seq1 <= seq2; seq1++) {
+ mail_index_lookup_uid(view, seq1, &uid);
+ mailbox_recent_flags_set_uid(box, uid);
+ }
+}
+
+bool mailbox_recent_flags_have_uid(struct mailbox *box, uint32_t uid)
+{
+ return array_is_created(&box->recent_flags) &&
+ seq_range_exists(&box->recent_flags, uid);
+}
+
+void mailbox_recent_flags_reset(struct mailbox *box)
+{
+ if (array_is_created(&box->recent_flags))
+ array_clear(&box->recent_flags);
+ box->recent_flags_count = 0;
+ box->recent_flags_prev_uid = 0;
+}
+
+unsigned int mailbox_recent_flags_count(struct mailbox *box)
+{
+ const struct mail_index_header *hdr;
+ const struct seq_range *range;
+ unsigned int i, count, recent_count;
+
+ if (!array_is_created(&box->recent_flags))
+ return 0;
+
+ hdr = mail_index_get_header(box->view);
+ recent_count = box->recent_flags_count;
+ range = array_get(&box->recent_flags, &count);
+ for (i = count; i > 0; ) {
+ i--;
+ if (range[i].seq2 < hdr->next_uid)
+ break;
+
+ if (range[i].seq1 >= hdr->next_uid) {
+ /* completely invisible to this view */
+ recent_count -= range[i].seq2 - range[i].seq1 + 1;
+ } else {
+ /* partially invisible */
+ recent_count -= range[i].seq2 - hdr->next_uid + 1;
+ break;
+ }
+ }
+ return recent_count;
+}
+
+void mailbox_recent_flags_expunge_seqs(struct mailbox *box,
+ uint32_t seq1, uint32_t seq2)
+{
+ uint32_t uid;
+
+ if (!array_is_created(&box->recent_flags))
+ return;
+
+ for (; seq1 <= seq2; seq1++) {
+ mail_index_lookup_uid(box->view, seq1, &uid);
+ if (seq_range_array_remove(&box->recent_flags, uid))
+ box->recent_flags_count--;
+ }
+}
+
+void mailbox_recent_flags_expunge_uid(struct mailbox *box, uint32_t uid)
+{
+ if (array_is_created(&box->recent_flags)) {
+ if (seq_range_array_remove(&box->recent_flags, uid))
+ box->recent_flags_count--;
+ }
+}
diff --git a/src/lib-storage/mailbox-recent-flags.h b/src/lib-storage/mailbox-recent-flags.h
new file mode 100644
index 0000000..d0cf389
--- /dev/null
+++ b/src/lib-storage/mailbox-recent-flags.h
@@ -0,0 +1,19 @@
+#ifndef MAILBOX_RECENT_FLAGS
+#define MAILBOX_RECENT_FLAGS
+
+struct mailbox;
+struct mail_index_view;
+
+void mailbox_recent_flags_set_uid(struct mailbox *box, uint32_t uid);
+void mailbox_recent_flags_set_uid_forced(struct mailbox *box, uint32_t uid);
+void mailbox_recent_flags_set_seqs(struct mailbox *box,
+ struct mail_index_view *view,
+ uint32_t seq1, uint32_t seq2);
+bool mailbox_recent_flags_have_uid(struct mailbox *box, uint32_t uid);
+void mailbox_recent_flags_reset(struct mailbox *box);
+unsigned int mailbox_recent_flags_count(struct mailbox *box);
+void mailbox_recent_flags_expunge_seqs(struct mailbox *box,
+ uint32_t seq1, uint32_t seq2);
+void mailbox_recent_flags_expunge_uid(struct mailbox *box, uint32_t uid);
+
+#endif
diff --git a/src/lib-storage/mailbox-search-result-private.h b/src/lib-storage/mailbox-search-result-private.h
new file mode 100644
index 0000000..983ac3c
--- /dev/null
+++ b/src/lib-storage/mailbox-search-result-private.h
@@ -0,0 +1,41 @@
+#ifndef MAILBOX_SEARCH_RESULT_PRIVATE_H
+#define MAILBOX_SEARCH_RESULT_PRIVATE_H
+
+#include "mail-storage.h"
+
+struct mail_search_result {
+ struct mailbox *box;
+ enum mailbox_search_result_flags flags;
+ struct mail_search_args *search_args;
+
+ /* UIDs of messages currently in the result */
+ ARRAY_TYPE(seq_range) uids;
+ /* UIDs of messages that will never match the result */
+ ARRAY_TYPE(seq_range) never_uids;
+ ARRAY_TYPE(seq_range) removed_uids, added_uids;
+
+ bool args_have_flags:1;
+ bool args_have_keywords:1;
+ bool args_have_modseq:1;
+};
+
+struct mail_search_result *
+mailbox_search_result_alloc(struct mailbox *box, struct mail_search_args *args,
+ enum mailbox_search_result_flags flags);
+
+/* called when initial search is done. */
+void mailbox_search_result_initial_done(struct mail_search_result *result);
+void mailbox_search_results_initial_done(struct mail_search_context *ctx);
+
+void mailbox_search_result_add(struct mail_search_result *result, uint32_t uid);
+void mailbox_search_result_remove(struct mail_search_result *result,
+ uint32_t uid);
+void mailbox_search_results_add(struct mail_search_context *ctx, uint32_t uid);
+void mailbox_search_results_remove(struct mailbox *box, uint32_t uid);
+
+void mailbox_search_result_never(struct mail_search_result *result,
+ uint32_t uid);
+void mailbox_search_results_never(struct mail_search_context *ctx,
+ uint32_t uid);
+
+#endif
diff --git a/src/lib-storage/mailbox-search-result.c b/src/lib-storage/mailbox-search-result.c
new file mode 100644
index 0000000..d239c72
--- /dev/null
+++ b/src/lib-storage/mailbox-search-result.c
@@ -0,0 +1,200 @@
+/* Copyright (c) 2008-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "mail-storage-private.h"
+#include "mail-search.h"
+#include "mailbox-search-result-private.h"
+
+static void
+mailbox_search_result_analyze_args(struct mail_search_result *result,
+ struct mail_search_arg *arg)
+{
+ for (; arg != NULL; arg = arg->next) {
+ switch (arg->type) {
+ case SEARCH_OR:
+ case SEARCH_SUB:
+ mailbox_search_result_analyze_args(result,
+ arg->value.subargs);
+ break;
+ case SEARCH_FLAGS:
+ result->args_have_flags = TRUE;
+ break;
+ case SEARCH_KEYWORDS:
+ result->args_have_keywords = TRUE;
+ break;
+ case SEARCH_MODSEQ:
+ result->args_have_modseq = TRUE;
+ break;
+ default:
+ break;
+ }
+ }
+}
+
+struct mail_search_result *
+mailbox_search_result_alloc(struct mailbox *box, struct mail_search_args *args,
+ enum mailbox_search_result_flags flags)
+{
+ struct mail_search_result *result;
+
+ result = i_new(struct mail_search_result, 1);
+ result->box = box;
+ result->flags = flags;
+ i_array_init(&result->uids, 32);
+ i_array_init(&result->never_uids, 128);
+
+ if ((result->flags & MAILBOX_SEARCH_RESULT_FLAG_UPDATE) != 0) {
+ result->search_args = args;
+ mail_search_args_ref(result->search_args);
+ mailbox_search_result_analyze_args(result, args->args);
+ }
+
+ array_push_back(&result->box->search_results, &result);
+ return result;
+}
+
+void mailbox_search_result_free(struct mail_search_result **_result)
+{
+ struct mail_search_result *result = *_result;
+ struct mail_search_result *const *results;
+ unsigned int i, count;
+
+ *_result = NULL;
+
+ results = array_get(&result->box->search_results, &count);
+ for (i = 0; i < count; i++) {
+ if (results[i] == result) {
+ array_delete(&result->box->search_results, i, 1);
+ break;
+ }
+ }
+ i_assert(i != count);
+
+ if (result->search_args != NULL)
+ mail_search_args_unref(&result->search_args);
+
+ array_free(&result->uids);
+ array_free(&result->never_uids);
+ if (array_is_created(&result->removed_uids)) {
+ array_free(&result->removed_uids);
+ array_free(&result->added_uids);
+ }
+ i_free(result);
+}
+
+struct mail_search_result *
+mailbox_search_result_save(struct mail_search_context *ctx,
+ enum mailbox_search_result_flags flags)
+{
+ struct mail_search_result *result;
+
+ result = mailbox_search_result_alloc(ctx->transaction->box,
+ ctx->args, flags);
+ array_push_back(&ctx->results, &result);
+ return result;
+}
+
+void mailbox_search_result_initial_done(struct mail_search_result *result)
+{
+ if ((result->flags & MAILBOX_SEARCH_RESULT_FLAG_QUEUE_SYNC) != 0) {
+ i_array_init(&result->removed_uids, 32);
+ i_array_init(&result->added_uids, 32);
+ }
+ mail_search_args_seq2uid(result->search_args);
+}
+
+void mailbox_search_results_initial_done(struct mail_search_context *ctx)
+{
+ struct mail_search_result *const *results;
+ unsigned int i, count;
+
+ results = array_get(&ctx->results, &count);
+ for (i = 0; i < count; i++)
+ mailbox_search_result_initial_done(results[i]);
+}
+
+void mailbox_search_result_add(struct mail_search_result *result, uint32_t uid)
+{
+ i_assert(uid > 0);
+
+ if (seq_range_exists(&result->uids, uid))
+ return;
+
+ seq_range_array_add(&result->uids, uid);
+ if (array_is_created(&result->added_uids)) {
+ seq_range_array_add(&result->added_uids, uid);
+ seq_range_array_remove(&result->removed_uids, uid);
+ }
+}
+
+void mailbox_search_result_remove(struct mail_search_result *result,
+ uint32_t uid)
+{
+ if (seq_range_array_remove(&result->uids, uid)) {
+ if (array_is_created(&result->removed_uids)) {
+ seq_range_array_add(&result->removed_uids, uid);
+ seq_range_array_remove(&result->added_uids, uid);
+ }
+ }
+}
+
+void mailbox_search_results_add(struct mail_search_context *ctx, uint32_t uid)
+{
+ struct mail_search_result *const *results;
+ unsigned int i, count;
+
+ results = array_get(&ctx->results, &count);
+ for (i = 0; i < count; i++)
+ mailbox_search_result_add(results[i], uid);
+}
+
+void mailbox_search_results_remove(struct mailbox *box, uint32_t uid)
+{
+ struct mail_search_result *const *results;
+ unsigned int i, count;
+
+ results = array_get(&box->search_results, &count);
+ for (i = 0; i < count; i++)
+ mailbox_search_result_remove(results[i], uid);
+}
+
+void mailbox_search_result_never(struct mail_search_result *result,
+ uint32_t uid)
+{
+ seq_range_array_add(&result->never_uids, uid);
+}
+
+void mailbox_search_results_never(struct mail_search_context *ctx,
+ uint32_t uid)
+{
+ struct mail_search_result *const *results;
+ unsigned int i, count;
+
+ if (ctx->update_result != NULL)
+ mailbox_search_result_never(ctx->update_result, uid);
+
+ results = array_get(&ctx->results, &count);
+ for (i = 0; i < count; i++)
+ mailbox_search_result_never(results[i], uid);
+}
+
+const ARRAY_TYPE(seq_range) *
+mailbox_search_result_get(struct mail_search_result *result)
+{
+ return &result->uids;
+}
+
+void mailbox_search_result_sync(struct mail_search_result *result,
+ ARRAY_TYPE(seq_range) *removed_uids,
+ ARRAY_TYPE(seq_range) *added_uids)
+{
+ array_clear(removed_uids);
+ array_clear(added_uids);
+
+ array_append_array(removed_uids, &result->removed_uids);
+ array_append_array(added_uids, &result->added_uids);
+
+ array_clear(&result->removed_uids);
+ array_clear(&result->added_uids);
+}
diff --git a/src/lib-storage/mailbox-tree.c b/src/lib-storage/mailbox-tree.c
new file mode 100644
index 0000000..9333fb6
--- /dev/null
+++ b/src/lib-storage/mailbox-tree.c
@@ -0,0 +1,345 @@
+/* Copyright (c) 2003-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "str.h"
+#include "mailbox-tree.h"
+
+struct mailbox_tree_context {
+ pool_t pool;
+ char separator;
+ bool parents_nonexistent;
+ bool sorted;
+ unsigned int node_size;
+
+ struct mailbox_node *nodes;
+};
+
+struct mailbox_tree_iterate_context {
+ struct mailbox_node *root, *next_node;
+ unsigned int flags_mask;
+
+ char separator;
+
+ ARRAY(struct mailbox_node *) node_path;
+ string_t *path_str;
+ size_t parent_pos;
+
+ bool first_child:1;
+};
+
+struct mailbox_tree_context *mailbox_tree_init(char separator)
+{
+ return mailbox_tree_init_size(separator, sizeof(struct mailbox_node));
+}
+
+struct mailbox_tree_context *
+mailbox_tree_init_size(char separator, unsigned int mailbox_node_size)
+{
+ struct mailbox_tree_context *tree;
+
+ i_assert(mailbox_node_size >= sizeof(struct mailbox_node));
+
+ tree = i_new(struct mailbox_tree_context, 1);
+ tree->pool = pool_alloconly_create(MEMPOOL_GROWING"mailbox_tree", 10240);
+ tree->separator = separator;
+ tree->node_size = mailbox_node_size;
+ return tree;
+}
+
+void mailbox_tree_deinit(struct mailbox_tree_context **_tree)
+{
+ struct mailbox_tree_context *tree = *_tree;
+
+ *_tree = NULL;
+ pool_unref(&tree->pool);
+ i_free(tree);
+}
+
+void mailbox_tree_set_separator(struct mailbox_tree_context *tree,
+ char separator)
+{
+ tree->separator = separator;
+}
+
+void mailbox_tree_set_parents_nonexistent(struct mailbox_tree_context *tree)
+{
+ tree->parents_nonexistent = TRUE;
+}
+
+void mailbox_tree_clear(struct mailbox_tree_context *tree)
+{
+ p_clear(tree->pool);
+ tree->nodes = NULL;
+}
+
+pool_t mailbox_tree_get_pool(struct mailbox_tree_context *tree)
+{
+ return tree->pool;
+}
+
+static struct mailbox_node * ATTR_NULL(2)
+mailbox_tree_traverse(struct mailbox_tree_context *tree, const char *path,
+ bool create, bool *created_r)
+{
+ struct mailbox_node **node, *parent;
+ const char *name;
+ string_t *str;
+
+ *created_r = FALSE;
+
+ if (path == NULL)
+ return tree->nodes;
+
+ if (strncasecmp(path, "INBOX", 5) == 0 &&
+ (path[5] == '\0' || path[5] == tree->separator))
+ path = t_strdup_printf("INBOX%s", path+5);
+
+ parent = NULL;
+ node = &tree->nodes;
+
+ str = t_str_new(strlen(path)+1);
+ for (name = path;; path++) {
+ if (*path != tree->separator && *path != '\0')
+ continue;
+
+ str_truncate(str, 0);
+ str_append_data(str, name, (size_t) (path - name));
+ name = str_c(str);
+
+ /* find the node */
+ while (*node != NULL) {
+ if (strcmp((*node)->name, name) == 0)
+ break;
+
+ node = &(*node)->next;
+ }
+
+ if (*node == NULL) {
+ /* not found, create it */
+ if (!create)
+ break;
+
+ *node = p_malloc(tree->pool, tree->node_size);
+ (*node)->parent = parent;
+ (*node)->name = p_strdup(tree->pool, name);
+ if (tree->parents_nonexistent)
+ (*node)->flags = MAILBOX_NONEXISTENT;
+ tree->sorted = FALSE;
+ *created_r = TRUE;
+ }
+
+ if (*path == '\0')
+ break;
+
+ name = path+1;
+
+ parent = *node;
+ node = &(*node)->children;
+ }
+
+ return *node;
+}
+
+struct mailbox_node *
+mailbox_tree_get(struct mailbox_tree_context *tree, const char *path,
+ bool *created_r)
+{
+ struct mailbox_node *node;
+ bool created;
+
+ T_BEGIN {
+ node = mailbox_tree_traverse(tree, path, TRUE, &created);
+ } T_END;
+ if (created && tree->parents_nonexistent)
+ node->flags = 0;
+
+ *created_r = created;
+ return node;
+}
+
+struct mailbox_node *
+mailbox_tree_lookup(struct mailbox_tree_context *tree, const char *path)
+{
+ struct mailbox_node *node;
+ bool created;
+
+ i_assert(tree != NULL);
+
+ T_BEGIN {
+ node = mailbox_tree_traverse(tree, path, FALSE, &created);
+ } T_END;
+ return node;
+}
+
+struct mailbox_tree_iterate_context *
+mailbox_tree_iterate_init(struct mailbox_tree_context *tree,
+ struct mailbox_node *root, unsigned int flags_mask)
+{
+ struct mailbox_tree_iterate_context *ctx;
+
+ ctx = i_new(struct mailbox_tree_iterate_context, 1);
+ ctx->separator = tree->separator;
+ ctx->root = root != NULL ? root : tree->nodes;
+ ctx->flags_mask = flags_mask;
+ ctx->path_str = str_new(default_pool, 256);
+ i_array_init(&ctx->node_path, 16);
+
+ ctx->next_node = ctx->root;
+ return ctx;
+}
+
+static void
+mailbox_tree_iterate_set_next_node(struct mailbox_tree_iterate_context *ctx)
+{
+ struct mailbox_node *node = ctx->next_node;
+ struct mailbox_node *const *nodes;
+ unsigned int i, count;
+
+ if (node->children != NULL) {
+ array_push_back(&ctx->node_path, &node);
+ ctx->parent_pos = str_len(ctx->path_str);
+ node = node->children;
+ ctx->first_child = TRUE;
+ } else if (node->next != NULL) {
+ node = node->next;
+ } else {
+ nodes = array_get(&ctx->node_path, &count);
+ node = NULL;
+ for (i = count; i != 0; i--) {
+ size_t len = strlen(nodes[i-1]->name) + 1;
+
+ i_assert(len <= ctx->parent_pos);
+ ctx->parent_pos -= len;
+ if (nodes[i-1]->next != NULL) {
+ node = nodes[i-1]->next;
+ ctx->first_child = TRUE;
+ i--;
+
+ if (ctx->parent_pos != 0)
+ ctx->parent_pos--;
+ break;
+ }
+ }
+ array_delete(&ctx->node_path, i, count - i);
+ }
+
+ ctx->next_node = node;
+}
+
+struct mailbox_node *
+mailbox_tree_iterate_next(struct mailbox_tree_iterate_context *ctx,
+ const char **path_r)
+{
+ struct mailbox_node *node;
+
+ do {
+ node = ctx->next_node;
+ if (node == NULL)
+ return NULL;
+
+ str_truncate(ctx->path_str, ctx->parent_pos);
+ if (ctx->first_child) {
+ ctx->first_child = FALSE;
+ if (node->parent != NULL) {
+ str_append_c(ctx->path_str, ctx->separator);
+ ctx->parent_pos++;
+ }
+ }
+ str_append(ctx->path_str, node->name);
+
+ mailbox_tree_iterate_set_next_node(ctx);
+ } while ((node->flags & ctx->flags_mask) != ctx->flags_mask);
+
+ *path_r = str_c(ctx->path_str);
+ return node;
+}
+
+void mailbox_tree_iterate_deinit(struct mailbox_tree_iterate_context **_ctx)
+{
+ struct mailbox_tree_iterate_context *ctx = *_ctx;
+
+ *_ctx = NULL;
+ str_free(&ctx->path_str);
+ array_free(&ctx->node_path);
+ i_free(ctx);
+}
+
+static struct mailbox_node * ATTR_NULL(1, 2)
+mailbox_tree_dup_branch(struct mailbox_tree_context *dest_tree,
+ struct mailbox_node *dest_parent,
+ const struct mailbox_node *src)
+{
+ struct mailbox_node *node, *dest_nodes = NULL, **dest = &dest_nodes;
+
+ for (; src != NULL; src = src->next) {
+ *dest = node = p_malloc(dest_tree->pool, dest_tree->node_size);
+ node->name = p_strdup(dest_tree->pool, src->name);
+ node->flags = src->flags;
+
+ node->parent = dest_parent;
+ node->children = mailbox_tree_dup_branch(dest_tree, node,
+ src->children);
+ dest = &node->next;
+ }
+ return dest_nodes;
+}
+
+struct mailbox_tree_context *mailbox_tree_dup(struct mailbox_tree_context *src)
+{
+ struct mailbox_tree_context *dest;
+
+ /* for now we don't need to support extra data */
+ i_assert(src->node_size == sizeof(struct mailbox_node));
+
+ dest = mailbox_tree_init_size(src->separator, src->node_size);
+ dest->nodes = mailbox_tree_dup_branch(dest, NULL, src->nodes);
+ return dest;
+}
+
+static int mailbox_node_name_cmp(struct mailbox_node *const *node1,
+ struct mailbox_node *const *node2)
+{
+ return strcmp((*node1)->name, (*node2)->name);
+}
+
+static void mailbox_tree_sort_branch(struct mailbox_node **nodes,
+ ARRAY_TYPE(mailbox_node) *tmparr)
+{
+ struct mailbox_node *node, **dest;
+
+ if (*nodes == NULL)
+ return;
+
+ /* first put the nodes into an array and sort it */
+ array_clear(tmparr);
+ for (node = *nodes; node != NULL; node = node->next)
+ array_push_back(tmparr, &node);
+ array_sort(tmparr, mailbox_node_name_cmp);
+
+ /* update the node pointers */
+ dest = nodes;
+ array_foreach_elem(tmparr, node) {
+ *dest = node;
+ dest = &(*dest)->next;
+ }
+ *dest = NULL;
+
+ /* sort the children */
+ for (node = *nodes; node != NULL; node = node->next)
+ mailbox_tree_sort_branch(&node->children, tmparr);
+}
+
+void mailbox_tree_sort(struct mailbox_tree_context *tree)
+{
+ if (tree->sorted)
+ return;
+ tree->sorted = TRUE;
+
+ T_BEGIN {
+ ARRAY_TYPE(mailbox_node) tmparr;
+
+ t_array_init(&tmparr, 32);
+ mailbox_tree_sort_branch(&tree->nodes, &tmparr);
+ } T_END;
+}
diff --git a/src/lib-storage/mailbox-tree.h b/src/lib-storage/mailbox-tree.h
new file mode 100644
index 0000000..aeb45c4
--- /dev/null
+++ b/src/lib-storage/mailbox-tree.h
@@ -0,0 +1,45 @@
+#ifndef MAILBOX_TREE_H
+#define MAILBOX_TREE_H
+
+#include "mailbox-list.h"
+
+struct mailbox_node {
+ struct mailbox_node *parent;
+ struct mailbox_node *next;
+ struct mailbox_node *children;
+
+ char *name;
+ enum mailbox_info_flags flags;
+};
+ARRAY_DEFINE_TYPE(mailbox_node, struct mailbox_node *);
+
+struct mailbox_tree_context *mailbox_tree_init(char separator);
+struct mailbox_tree_context *
+mailbox_tree_init_size(char separator, unsigned int mailbox_node_size);
+void mailbox_tree_deinit(struct mailbox_tree_context **tree);
+
+void mailbox_tree_set_separator(struct mailbox_tree_context *tree,
+ char separator);
+void mailbox_tree_set_parents_nonexistent(struct mailbox_tree_context *tree);
+void mailbox_tree_clear(struct mailbox_tree_context *tree);
+pool_t mailbox_tree_get_pool(struct mailbox_tree_context *tree);
+
+struct mailbox_node *
+mailbox_tree_get(struct mailbox_tree_context *tree, const char *path,
+ bool *created_r);
+
+struct mailbox_node *
+mailbox_tree_lookup(struct mailbox_tree_context *tree, const char *path);
+
+struct mailbox_tree_iterate_context * ATTR_NULL(2)
+mailbox_tree_iterate_init(struct mailbox_tree_context *tree,
+ struct mailbox_node *root, unsigned int flags_mask);
+struct mailbox_node *
+mailbox_tree_iterate_next(struct mailbox_tree_iterate_context *ctx,
+ const char **path_r);
+void mailbox_tree_iterate_deinit(struct mailbox_tree_iterate_context **ctx);
+
+struct mailbox_tree_context *mailbox_tree_dup(struct mailbox_tree_context *src);
+void mailbox_tree_sort(struct mailbox_tree_context *tree);
+
+#endif
diff --git a/src/lib-storage/mailbox-uidvalidity.c b/src/lib-storage/mailbox-uidvalidity.c
new file mode 100644
index 0000000..74a9dd2
--- /dev/null
+++ b/src/lib-storage/mailbox-uidvalidity.c
@@ -0,0 +1,249 @@
+/* Copyright (c) 2008-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "ioloop.h"
+#include "str.h"
+#include "read-full.h"
+#include "write-full.h"
+#include "eacces-error.h"
+#include "mail-user.h"
+#include "mailbox-list.h"
+#include "mailbox-uidvalidity.h"
+
+#include <stdio.h>
+#include <unistd.h>
+#include <dirent.h>
+#include <fcntl.h>
+#include <sys/stat.h>
+
+#define RETRY_COUNT 10
+
+static uint32_t mailbox_uidvalidity_next_fallback(void)
+{
+ static uint32_t uid_validity = 0;
+
+ /* we failed to use the uidvalidity file. don't fail the mailbox
+ creation because of it though, most of the time it's safe enough
+ to use the current time as the uidvalidity value. */
+ if (uid_validity < (uint32_t)ioloop_time)
+ uid_validity = (uint32_t)ioloop_time;
+ else
+ uid_validity++;
+ if (uid_validity == 0)
+ uid_validity = 1;
+ return uid_validity;
+}
+
+static void mailbox_uidvalidity_write(struct mailbox_list *list,
+ const char *path, uint32_t uid_validity)
+{
+ struct mail_user *user = mailbox_list_get_user(list);
+ char buf[8+1];
+ int fd;
+ struct mailbox_permissions perm;
+ mode_t old_mask;
+
+ mailbox_list_get_root_permissions(list, &perm);
+
+ old_mask = umask(0666 & ~perm.file_create_mode);
+ fd = open(path, O_RDWR | O_CREAT, 0666);
+ umask(old_mask);
+ if (fd == -1) {
+ e_error(user->event, "open(%s) failed: %m", path);
+ return;
+ }
+ if (perm.file_create_gid != (gid_t)-1 &&
+ fchown(fd, (uid_t)-1, perm.file_create_gid) < 0) {
+ if (errno == EPERM) {
+ e_error(user->event, "%s",
+ eperm_error_get_chgrp("fchown", path,
+ perm.file_create_gid,
+ perm.file_create_gid_origin));
+ } else {
+ e_error(mailbox_list_get_user(list)->event,
+ "fchown(%s, -1, %ld) failed: %m",
+ path, (long)perm.file_create_gid);
+ }
+ }
+
+ if (i_snprintf(buf, sizeof(buf), "%08x", uid_validity) < 0)
+ i_unreached();
+ if (pwrite_full(fd, buf, strlen(buf), 0) < 0)
+ e_error(user->event, "write(%s) failed: %m", path);
+ if (close(fd) < 0)
+ e_error(user->event, "close(%s) failed: %m", path);
+}
+
+static int
+mailbox_uidvalidity_rename(struct mailbox_list *list, const char *path,
+ uint32_t *uid_validity, bool log_enoent)
+{
+ string_t *src, *dest;
+ unsigned int i;
+ size_t prefix_len;
+ int ret;
+
+ src = t_str_new(256);
+ str_append(src, path);
+ dest = t_str_new(256);
+ str_append(dest, path);
+ prefix_len = str_len(src);
+
+ for (i = 0; i < RETRY_COUNT; i++) {
+ str_truncate(src, prefix_len);
+ str_truncate(dest, prefix_len);
+
+ str_printfa(src, ".%08x", *uid_validity);
+ *uid_validity += 1;
+ if (*uid_validity == 0)
+ *uid_validity += 1;
+ str_printfa(dest, ".%08x", *uid_validity);
+
+ if ((ret = rename(str_c(src), str_c(dest))) == 0 ||
+ errno != ENOENT)
+ break;
+
+ /* possibly a race condition. try the next value. */
+ }
+ if (ret < 0 && (errno != ENOENT || log_enoent))
+ e_error(mailbox_list_get_user(list)->event,
+ "rename(%s, %s) failed: %m", str_c(src), str_c(dest));
+ return ret;
+}
+
+static uint32_t
+mailbox_uidvalidity_next_rescan(struct mailbox_list *list, const char *path)
+{
+ DIR *d;
+ struct dirent *dp;
+ const char *fname, *dir, *prefix, *tmp;
+ unsigned int i;
+ size_t prefix_len;
+ uint32_t cur_value, min_value, max_value;
+ mode_t old_mask;
+ int fd;
+
+ fname = strrchr(path, '/');
+ if (fname == NULL) {
+ dir = ".";
+ fname = path;
+ } else {
+ dir = t_strdup_until(path, fname);
+ fname++;
+ }
+
+ d = opendir(dir);
+ if (d == NULL && errno == ENOENT) {
+ /* FIXME: the PATH_TYPE_CONTROL should come as a parameter, but
+ that's an API change, do it in v2.3. it's not really a
+ problem though, since currently all backends use control
+ dirs for the uidvalidity file. */
+ (void)mailbox_list_mkdir_root(list, dir, MAILBOX_LIST_PATH_TYPE_CONTROL);
+ d = opendir(dir);
+ }
+ if (d == NULL) {
+ e_error(mailbox_list_get_user(list)->event,
+ "opendir(%s) failed: %m", dir);
+ return mailbox_uidvalidity_next_fallback();
+ }
+ prefix = t_strconcat(fname, ".", NULL);
+ prefix_len = strlen(prefix);
+
+ /* just in case there happens to be multiple matching uidvalidity
+ files, track the min/max values. use the max value and delete the
+ min value file. */
+ max_value = 0; min_value = (uint32_t)-1;
+ while ((dp = readdir(d)) != NULL) {
+ if (strncmp(dp->d_name, prefix, prefix_len) == 0) {
+ if (str_to_uint32_hex(dp->d_name + prefix_len, &cur_value) >= 0) {
+ if (min_value > cur_value)
+ min_value = cur_value;
+ if (max_value < cur_value)
+ max_value = cur_value;
+ }
+ }
+ }
+ if (closedir(d) < 0)
+ e_error(mailbox_list_get_user(list)->event,
+ "closedir(%s) failed: %m", dir);
+
+ if (max_value == 0) {
+ /* no uidvalidity files. create one. */
+ for (i = 0; i < RETRY_COUNT; i++) {
+ cur_value = mailbox_uidvalidity_next_fallback();
+ tmp = t_strdup_printf("%s.%08x", path, cur_value);
+ /* the file is empty, don't bother with permissions */
+ old_mask = umask(0);
+ fd = open(tmp, O_RDWR | O_CREAT | O_EXCL, 0444);
+ umask(old_mask);
+ if (fd != -1 || errno != EEXIST)
+ break;
+ /* already exists. although it's quite unlikely we'll
+ hit this race condition. more likely we'll create
+ a duplicate file.. */
+ }
+ if (fd == -1) {
+ e_error(mailbox_list_get_user(list)->event,
+ "creat(%s) failed: %m", tmp);
+ return cur_value;
+ }
+ i_close_fd(&fd);
+ mailbox_uidvalidity_write(list, path, cur_value);
+ return cur_value;
+ }
+ if (min_value != max_value) {
+ /* duplicate uidvalidity files, delete the oldest */
+ tmp = t_strdup_printf("%s.%08x", path, min_value);
+ i_unlink_if_exists(tmp);
+ }
+
+ cur_value = max_value;
+ if (mailbox_uidvalidity_rename(list, path, &cur_value, TRUE) < 0)
+ return mailbox_uidvalidity_next_fallback();
+ mailbox_uidvalidity_write(list, path, cur_value);
+ return cur_value;
+}
+
+uint32_t mailbox_uidvalidity_next(struct mailbox_list *list, const char *path)
+{
+ struct mail_user *user = mailbox_list_get_user(list);
+ char buf[8+1];
+ uint32_t cur_value;
+ int fd, ret;
+
+ fd = open(path, O_RDWR);
+ if (fd == -1) {
+ if (errno != ENOENT)
+ e_error(user->event, "open(%s) failed: %m", path);
+ return mailbox_uidvalidity_next_rescan(list, path);
+ }
+ ret = read_full(fd, buf, sizeof(buf)-1);
+ if (ret < 0) {
+ e_error(user->event, "read(%s) failed: %m", path);
+ i_close_fd(&fd);
+ return mailbox_uidvalidity_next_rescan(list, path);
+ }
+ buf[sizeof(buf)-1] = 0;
+ if (ret == 0 || str_to_uint32_hex(buf, &cur_value) < 0 ||
+ cur_value == 0) {
+ /* broken value */
+ i_close_fd(&fd);
+ return mailbox_uidvalidity_next_rescan(list, path);
+ }
+
+ /* we now have the current uidvalidity value that's hopefully correct */
+ if (mailbox_uidvalidity_rename(list, path, &cur_value, FALSE) < 0) {
+ i_close_fd(&fd);
+ return mailbox_uidvalidity_next_rescan(list, path);
+ }
+
+ /* fast path succeeded. write the current value to the main
+ uidvalidity file. */
+ if (i_snprintf(buf, sizeof(buf), "%08x", cur_value) < 0)
+ i_unreached();
+ if (pwrite_full(fd, buf, strlen(buf), 0) < 0)
+ e_error(user->event, "write(%s) failed: %m", path);
+ if (close(fd) < 0)
+ e_error(user->event, "close(%s) failed: %m", path);
+ return cur_value;
+}
diff --git a/src/lib-storage/mailbox-uidvalidity.h b/src/lib-storage/mailbox-uidvalidity.h
new file mode 100644
index 0000000..388d78f
--- /dev/null
+++ b/src/lib-storage/mailbox-uidvalidity.h
@@ -0,0 +1,8 @@
+#ifndef MAILBOX_UIDVALIDITY_H
+#define MAILBOX_UIDVALIDITY_H
+
+struct mailbox_list;
+
+uint32_t mailbox_uidvalidity_next(struct mailbox_list *list, const char *path);
+
+#endif
diff --git a/src/lib-storage/mailbox-watch.c b/src/lib-storage/mailbox-watch.c
new file mode 100644
index 0000000..659cab3
--- /dev/null
+++ b/src/lib-storage/mailbox-watch.c
@@ -0,0 +1,152 @@
+/* Copyright (c) 2002-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "ioloop.h"
+#include "mail-storage-private.h"
+#include "mailbox-watch.h"
+
+#include <unistd.h>
+#include <fcntl.h>
+#include <sys/stat.h>
+
+#define NOTIFY_DELAY_MSECS 500
+
+struct mailbox_notify_file {
+ struct mailbox_notify_file *next;
+
+ char *path;
+ struct stat last_st;
+ struct io *io_notify;
+};
+
+static void notify_delay_callback(struct mailbox *box)
+{
+ timeout_remove(&box->to_notify_delay);
+ box->notify_callback(box, box->notify_context);
+}
+
+static void notify_timeout(struct mailbox *box)
+{
+ struct mailbox_notify_file *file;
+ struct stat st;
+ bool notify = FALSE;
+
+ for (file = box->notify_files; file != NULL; file = file->next) {
+ if (stat(file->path, &st) == 0 &&
+ ST_CHANGED(file->last_st, st)) {
+ file->last_st = st;
+ notify = TRUE;
+ }
+ }
+
+ if (notify)
+ notify_delay_callback(box);
+}
+
+static void notify_callback(struct mailbox *box)
+{
+ timeout_reset(box->to_notify);
+
+ if (box->to_notify_delay == NULL) {
+ box->to_notify_delay =
+ timeout_add_short(NOTIFY_DELAY_MSECS,
+ notify_delay_callback, box);
+ }
+}
+
+void mailbox_watch_add(struct mailbox *box, const char *path)
+{
+ const struct mail_storage_settings *set = box->storage->set;
+ struct mailbox_notify_file *file;
+ struct stat st;
+ struct io *io = NULL;
+
+ i_assert(set->mailbox_idle_check_interval > 0);
+
+ (void)io_add_notify(path, notify_callback, box, &io);
+
+ file = i_new(struct mailbox_notify_file, 1);
+ file->path = i_strdup(path);
+ if (stat(path, &st) == 0)
+ file->last_st = st;
+ file->io_notify = io;
+
+ file->next = box->notify_files;
+ box->notify_files = file;
+
+ /* we still add a timeout if we don't have one already,
+ * because we don't know what happens with [di]notify
+ * when the filesystem is remote (NFS, ...) */
+ if (box->to_notify == NULL) {
+ box->to_notify =
+ timeout_add(set->mailbox_idle_check_interval * 1000,
+ notify_timeout, box);
+ }
+}
+
+void mailbox_watch_remove_all(struct mailbox *box)
+{
+ struct mailbox_notify_file *file;
+
+ while (box->notify_files != NULL) {
+ file = box->notify_files;
+ box->notify_files = file->next;
+
+ io_remove(&file->io_notify);
+ i_free(file->path);
+ i_free(file);
+ }
+
+ timeout_remove(&box->to_notify_delay);
+ timeout_remove(&box->to_notify);
+}
+
+static void notify_extract_callback(struct mailbox *box ATTR_UNUSED)
+{
+ i_unreached();
+}
+
+int mailbox_watch_extract_notify_fd(struct mailbox *box, const char **reason_r)
+{
+ struct ioloop *ioloop;
+ struct mailbox_notify_file *file;
+ struct io *io;
+ ARRAY(struct io *) temp_ios;
+ int ret;
+ bool failed = FALSE;
+
+ /* add all the notify IOs to a new ioloop. */
+ ioloop = io_loop_create();
+
+ t_array_init(&temp_ios, 8);
+ for (file = box->notify_files; file != NULL && !failed; file = file->next) {
+ switch (io_add_notify(file->path, notify_extract_callback, box, &io)) {
+ case IO_NOTIFY_ADDED:
+ array_push_back(&temp_ios, &io);
+ break;
+ case IO_NOTIFY_NOTFOUND:
+ *reason_r = t_strdup_printf(
+ "%s not found - can't watch it", file->path);
+ failed = TRUE;
+ break;
+ case IO_NOTIFY_NOSUPPORT:
+ *reason_r = "Filesystem notifications not supported";
+ failed = TRUE;
+ break;
+ }
+ }
+ if (failed)
+ ret = -1;
+ else if (array_count(&temp_ios) == 0) {
+ *reason_r = "Mailbox has no IO notifications";
+ ret = -1;
+ } else {
+ ret = io_loop_extract_notify_fd(ioloop);
+ if (ret == -1)
+ *reason_r = "Couldn't extra notify fd";
+ }
+ array_foreach_elem(&temp_ios, io)
+ io_remove(&io);
+ io_loop_destroy(&ioloop);
+ return ret;
+}
diff --git a/src/lib-storage/mailbox-watch.h b/src/lib-storage/mailbox-watch.h
new file mode 100644
index 0000000..54e48b7
--- /dev/null
+++ b/src/lib-storage/mailbox-watch.h
@@ -0,0 +1,11 @@
+#ifndef MAILBOX_WATCH_H
+#define MAILBOX_WATCH_H
+
+void mailbox_watch_add(struct mailbox *box, const char *path);
+void mailbox_watch_remove_all(struct mailbox *box);
+
+/* Create a new temporary ioloop, add all the watches back and call
+ io_loop_extract_notify_fd() on it. Returns fd on success, -1 on error. */
+int mailbox_watch_extract_notify_fd(struct mailbox *box, const char **reason_r);
+
+#endif
diff --git a/src/lib-storage/test-mail-search-args-imap.c b/src/lib-storage/test-mail-search-args-imap.c
new file mode 100644
index 0000000..c6cda9c
--- /dev/null
+++ b/src/lib-storage/test-mail-search-args-imap.c
@@ -0,0 +1,179 @@
+/* Copyright (c) 2015-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "ioloop.h"
+#include "str.h"
+#include "test-common.h"
+#include "mail-search-build.h"
+#include "mail-search-parser.h"
+#include "mail-search.h"
+
+#define CURRENT_UNIX_TIME 1000000
+
+static const struct {
+ const char *input, *output;
+} tests[] = {
+ { "ALL", NULL },
+ { "1,5:6,10:15", NULL },
+ { "UID 1,5:6,10:15", NULL },
+ { "ANSWERED FLAGGED DELETED SEEN DRAFT RECENT",
+ "ANSWERED FLAGGED DELETED SEEN DRAFT RECENT" },
+ { "KEYWORD foo KEYWORD bar", NULL },
+ { "BEFORE 20-May-2015", "BEFORE \"20-May-2015\"" },
+ { "ON 20-May-2015", "ON \"20-May-2015\"" },
+ { "SINCE 20-May-2015", "SINCE \"20-May-2015\"" },
+ { "SENTBEFORE 20-May-2015", "SENTBEFORE \"20-May-2015\"" },
+ { "SENTON 20-May-2015", "SENTON \"20-May-2015\"" },
+ { "SENTSINCE 20-May-2015", "SENTSINCE \"20-May-2015\"" },
+ { "SAVEDBEFORE 20-May-2015", "SAVEDBEFORE \"20-May-2015\"" },
+ { "SAVEDON 20-May-2015", "SAVEDON \"20-May-2015\"" },
+ { "SAVEDSINCE 20-May-2015", "SAVEDSINCE \"20-May-2015\"" },
+ { "X-SAVEDBEFORE 20-May-2015", "SAVEDBEFORE \"20-May-2015\"" },
+ { "X-SAVEDON 20-May-2015", "SAVEDON \"20-May-2015\"" },
+ { "X-SAVEDSINCE 20-May-2015", "SAVEDSINCE \"20-May-2015\"" },
+ { "OLDER 1", NULL },
+ { "OLDER 1000", NULL },
+ { "YOUNGER 1", NULL },
+ { "YOUNGER 1000", NULL },
+ { "SMALLER 0", NULL },
+ { "SMALLER 1", NULL },
+ { "SMALLER 4294967295", NULL },
+ { "LARGER 0", NULL },
+ { "LARGER 1", NULL },
+ { "LARGER 4294967295", NULL },
+ { "FROM foo", NULL },
+ { "TO foo", NULL },
+ { "CC foo", NULL },
+ { "BCC foo", NULL },
+ { "SUBJECT foo", NULL },
+ { "HEADER subjecT foo", "SUBJECT foo" },
+ { "HEADER subjecT2 foo", "HEADER SUBJECT2 foo" },
+ { "BODY foo", NULL },
+ { "TEXT foo", NULL },
+ { "MODSEQ 0", NULL },
+ { "MODSEQ 1", NULL },
+ { "MODSEQ 18446744073709551615", NULL },
+ { "MODSEQ /flags/keyword all 0", NULL },
+ { "MODSEQ /flags/\\Seen all 0", NULL },
+ { "MODSEQ /flags/\\Seen priv 0", NULL },
+ { "MODSEQ /flags/\\Seen shared 0", NULL },
+ { "INTHREAD REFERENCES seen", "INTHREAD REFERENCES (SEEN)" },
+ { "INTHREAD ORDEREDSUBJECT seen", "INTHREAD ORDEREDSUBJECT (SEEN)" },
+ { "INTHREAD REFS seen", "INTHREAD REFS (SEEN)" },
+ { "INTHREAD REFS ( OR text foo OR keyword bar seen )",
+ "INTHREAD REFS ((OR TEXT foo OR KEYWORD bar SEEN))" },
+ { "X-GUID foo", NULL },
+ { "X-MAILBOX foo", NULL },
+ { "X-REAL-UID 1,5:6,10:15", NULL },
+ /* SEARCH=X-MIMEPART */
+ { "MIMEPART CHILD EXISTS", NULL },
+ { "MIMEPART ( CHILD EXISTS )",
+ "MIMEPART CHILD EXISTS" },
+ { "MIMEPART ( CHILD EXISTS HEADER Comment Hopla )",
+ "MIMEPART (CHILD EXISTS HEADER COMMENT Hopla)" },
+ { "MIMEPART ( DESCRIPTION Frop ENCODING base64 )",
+ "MIMEPART (DESCRIPTION Frop ENCODING base64)" },
+ { "MIMEPART ( DISPOSITION TYPE attachment "
+ "DISPOSITION PARAM FILENAME frop.txt )",
+ "MIMEPART (DISPOSITION TYPE attachment "
+ "DISPOSITION PARAM FILENAME frop.txt)" },
+ { "MIMEPART ( ID <frop.example.com> LANGUAGE en )",
+ "MIMEPART (ID <frop.example.com> LANGUAGE en)" },
+ { "MIMEPART ( LOCATION http://www.dovecot.org )",
+ "MIMEPART LOCATION http://www.dovecot.org" },
+ { "MIMEPART NOT MD5 373def35afde6378efd6172dfeadfd", NULL },
+ { "MIMEPART OR PARAM charset utf-8 TYPE text",
+ "MIMEPART OR PARAM CHARSET utf-8 TYPE text" },
+ { "MIMEPART ( OR SIZE LARGER 25 SIZE SMALLER 1023 )",
+ "MIMEPART OR SIZE LARGER 25 SIZE SMALLER 1023" },
+ { "MIMEPART ( TYPE video SUBTYPE mpeg )",
+ "MIMEPART (TYPE video SUBTYPE mpeg)" },
+ { "( OR MIMEPART ( DEPTH 2 INDEX 1 ) MIMEPART ( DEPTH MAX 4 INDEX 3 ) )",
+ "(OR MIMEPART (DEPTH 2 INDEX 1) MIMEPART (DEPTH MAX 4 INDEX 3))" },
+ { "MIMEPART FILENAME IS frop.txt", NULL },
+ { "MIMEPART FILENAME BEGINS frop", NULL },
+ { "MIMEPART FILENAME ENDS .txt", NULL },
+ { "MIMEPART FILENAME CONTAINS frop", NULL },
+ { "MIMEPART BODY frop MIMEPART TEXT frop", NULL },
+ { "MIMEPART ( CC appie BCC theo FROM leo REPLY-TO henk SENDER arie )",
+ "MIMEPART (CC appie BCC theo FROM leo REPLY-TO henk SENDER arie)" },
+ { "MIMEPART ( MESSAGE-ID <frop4222> IN-REPLY-TO <frop421> )",
+ "MIMEPART (MESSAGE-ID <frop4222> IN-REPLY-TO <frop421>)" },
+ { "MIMEPART ( SUBJECT Frop TO henkie SENTON 20-Feb-2017 )",
+ "MIMEPART (SUBJECT Frop TO henkie SENTON \"20-Feb-2017\")" },
+ { "MIMEPART ( OR SENTBEFORE 20-May-2015 SENTSINCE 20-Feb-2017 )",
+ "MIMEPART OR SENTBEFORE \"20-May-2015\" SENTSINCE \"20-Feb-2017\"" },
+ { "MIMEPART ( ID <frop> PARENT ID <friep> )",
+ "MIMEPART (ID <frop> PARENT (ID <friep>))" },
+ { "MIMEPART ( ID <frop> CHILD ( DESCRIPTION frop ID friep ) )",
+ "MIMEPART (ID <frop> CHILD (DESCRIPTION frop ID friep))" },
+ { "MIMEPART CHILD EXISTS MIMEPART PARENT EXISTS", NULL },
+};
+
+static struct mail_search_arg test_failures[] = {
+ { .type = SEARCH_MAILBOX },
+ { .type = SEARCH_MAILBOX_GUID },
+ { .type = SEARCH_BEFORE, .value = {
+ .date_type = MAIL_SEARCH_DATE_TYPE_SENT, .time = 86400-1 } },
+ { .type = SEARCH_ON, .value = {
+ .date_type = MAIL_SEARCH_DATE_TYPE_SENT, .time = 86400-1 } },
+ { .type = SEARCH_SINCE, .value = {
+ .date_type = MAIL_SEARCH_DATE_TYPE_SENT, .time = 86400-1 } },
+ { .type = SEARCH_ON, .value = {
+ .date_type = MAIL_SEARCH_DATE_TYPE_RECEIVED, .time = 86400-1 } },
+ { .type = SEARCH_BEFORE, .value = {
+ .date_type = MAIL_SEARCH_DATE_TYPE_SAVED, .time = 86400-1 } },
+ { .type = SEARCH_ON, .value = {
+ .date_type = MAIL_SEARCH_DATE_TYPE_SAVED, .time = 86400-1 } },
+ { .type = SEARCH_SINCE, .value = {
+ .date_type = MAIL_SEARCH_DATE_TYPE_SAVED, .time = 86400-1 } }
+};
+
+static struct mail_search_args *
+test_build_search_args(const char *args)
+{
+ struct mail_search_parser *parser;
+ struct mail_search_args *sargs;
+ const char *error, *charset = "UTF-8";
+
+ parser = mail_search_parser_init_cmdline(t_strsplit(args, " "));
+ if (mail_search_build(mail_search_register_get_imap(),
+ parser, &charset, &sargs, &error) < 0)
+ i_panic("%s", error);
+ mail_search_parser_deinit(&parser);
+ return sargs;
+}
+
+static void test_mail_search_args_imap(void)
+{
+ struct mail_search_args *args;
+ string_t *str = t_str_new(256);
+ const char *output, *error;
+ unsigned int i;
+
+ ioloop_time = CURRENT_UNIX_TIME; /* YOUNGER/OLDER tests need this */
+
+ test_begin("mail search args imap");
+ for (i = 0; i < N_ELEMENTS(tests); i++) {
+ args = test_build_search_args(tests[i].input);
+ output = tests[i].output != NULL ?
+ tests[i].output : tests[i].input;
+ str_truncate(str, 0);
+ test_assert_idx(mail_search_args_to_imap(str, args->args, &error), i);
+ test_assert_idx(strcmp(str_c(str), output) == 0, i);
+ mail_search_args_unref(&args);
+ }
+ for (i = 0; i < N_ELEMENTS(test_failures); i++)
+ test_assert_idx(!mail_search_args_to_imap(str, &test_failures[i], &error), i);
+ test_end();
+}
+
+int main(void)
+{
+ static void (*const test_functions[])(void) = {
+ test_mail_search_args_imap,
+ NULL
+ };
+
+ return test_run(test_functions);
+}
diff --git a/src/lib-storage/test-mail-search-args-simplify.c b/src/lib-storage/test-mail-search-args-simplify.c
new file mode 100644
index 0000000..a8b165b
--- /dev/null
+++ b/src/lib-storage/test-mail-search-args-simplify.c
@@ -0,0 +1,324 @@
+/* Copyright (c) 2015-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "str.h"
+#include "test-common.h"
+#include "mail-storage-private.h"
+#include "mail-search-build.h"
+#include "mail-search-parser.h"
+#include "mail-search.h"
+
+static const struct {
+ const char *input;
+ const char *output;
+} tests[] = {
+ { "ALL", "ALL" },
+ { "NOT ALL", "NOT ALL" },
+ { "ALL NOT ALL", "NOT ALL" },
+ { "ALL NOT ALL TEXT foo", "NOT ALL" },
+ { "OR ALL NOT ALL", "ALL" },
+ { "OR ALL OR NOT ALL TEXT foo", "ALL" },
+ { "OR ALL OR TEXT foo TEXT bar", "ALL" },
+ { "OR TEXT FOO ( ALL NOT ALL )", "TEXT FOO" },
+ { "TEXT FOO OR ALL NOT ALL", "TEXT FOO" },
+
+ { "TEXT foo", "TEXT foo" },
+ { "( TEXT foo )", "TEXT foo" },
+ { "( ( TEXT foo ) )", "TEXT foo" },
+ { "( ( TEXT foo ) ( TEXT bar ) )", "TEXT foo TEXT bar" },
+
+ { "OR ( TEXT foo ) ( TEXT bar )", "OR TEXT foo TEXT bar" },
+ { "OR ( TEXT foo ) OR ( TEXT bar ) ( TEXT baz )",
+ "OR TEXT foo OR TEXT bar TEXT baz" },
+ { "OR ( ( TEXT foo TEXT foo2 ) ) ( ( TEXT bar ( TEXT baz ) ) )",
+ "OR (TEXT foo TEXT foo2) (TEXT bar TEXT baz)" },
+
+ { "NOT ( TEXT foo )", "NOT TEXT foo" },
+ { "NOT ( NOT ( TEXT foo ) )", "TEXT foo" },
+ { "NOT OR ( TEXT foo ) ( TEXT bar )", "NOT TEXT foo NOT TEXT bar" },
+ { "NOT ( OR ( TEXT foo ) ( TEXT bar ) )", "NOT TEXT foo NOT TEXT bar" },
+ { "NOT ( TEXT foo TEXT bar )", "OR NOT TEXT foo NOT TEXT bar" },
+
+ { "ANSWERED FLAGGED SEEN", "(ANSWERED FLAGGED SEEN)" },
+ { "OR ( ANSWERED FLAGGED SEEN ) DRAFT", "OR (ANSWERED FLAGGED SEEN) DRAFT" },
+ { "ANSWERED TEXT foo FLAGGED SEEN", "(ANSWERED FLAGGED SEEN) TEXT foo" },
+ { "NOT ( ANSWERED FLAGGED SEEN )", "NOT (ANSWERED FLAGGED SEEN)" },
+ { "OR NOT ANSWERED OR NOT FLAGGED NOT SEEN", "NOT (ANSWERED FLAGGED SEEN)" },
+ { "OR NOT ANSWERED OR NOT FLAGGED SEEN", "OR NOT (ANSWERED FLAGGED) SEEN" },
+ { "OR NOT ANSWERED OR FLAGGED NOT SEEN", "OR NOT (ANSWERED SEEN) FLAGGED" },
+ { "NOT ANSWERED OR FLAGGED NOT SEEN", "NOT ANSWERED OR FLAGGED NOT SEEN" },
+ { "NOT ANSWERED OR NOT FLAGGED NOT SEEN", "NOT ANSWERED NOT (FLAGGED SEEN)" },
+ { "ANSWERED NOT FLAGGED SEEN NOT DRAFT", "(ANSWERED SEEN) NOT FLAGGED NOT DRAFT" },
+ { "OR NOT ANSWERED NOT SEEN", "NOT (ANSWERED SEEN)" },
+ { "OR NOT ANSWERED OR NOT SEEN TEXT foo", "OR NOT (ANSWERED SEEN) TEXT foo" },
+
+ { "ANSWERED ANSWERED", "ANSWERED" },
+ { "ANSWERED NOT ANSWERED", "NOT ALL" },
+ { "ANSWERED ANSWERED NOT ANSWERED", "NOT ALL" },
+ { "ANSWERED NOT ANSWERED ANSWERED NOT ANSWERED", "NOT ALL" },
+ { "NOT ANSWERED NOT ANSWERED", "NOT ANSWERED" },
+ { "NOT SEEN NOT ANSWERED NOT ANSWERED", "NOT SEEN NOT ANSWERED" },
+ { "OR NOT SEEN OR NOT ANSWERED NOT ANSWERED", "NOT (ANSWERED SEEN)" },
+
+ { "KEYWORD foo", "KEYWORD foo" },
+ { "KEYWORD foo KEYWORD bar", "KEYWORD foo KEYWORD bar" },
+ { "NOT KEYWORD foo", "NOT KEYWORD foo" },
+ { "NOT KEYWORD foo NOT KEYWORD bar", "NOT KEYWORD foo NOT KEYWORD bar" },
+ { "OR KEYWORD foo KEYWORD bar", "OR KEYWORD foo KEYWORD bar" },
+ { "OR NOT KEYWORD foo NOT KEYWORD bar", "OR NOT KEYWORD foo NOT KEYWORD bar" },
+
+ { "KEYWORD foo KEYWORD foo", "KEYWORD foo" },
+ { "KEYWORD foo NOT KEYWORD foo", "NOT ALL" },
+ { "OR KEYWORD foo NOT KEYWORD foo", "ALL" },
+ { "OR KEYWORD foo KEYWORD foo", "KEYWORD foo" },
+ { "NOT KEYWORD foo NOT KEYWORD foo", "NOT KEYWORD foo" },
+
+ { "1:* 1:*", "ALL" },
+ { "OR 1:5 6:*", "ALL" },
+
+ { "UID 1:* UID 1:*", "ALL" },
+ { "OR UID 1:5 UID 6:*", "ALL" },
+
+ { "2:* 2:*", "2:4294967295" },
+ { "OR 2:* 2:*", "2:4294967295" },
+
+ { "UID 2:* UID 2:*", "UID 2:4294967295" },
+ { "OR UID 2:* UID 2:*", "UID 2:4294967295" },
+
+ { "1:5 6:7", "NOT ALL" },
+ { "1:5 3:7", "3:5" },
+ { "1:5 3:7 4:9", "4:5" },
+ { "1:5 OR 3:4 4:6", "3:5" },
+ { "OR 1 2", "1:2" },
+ { "NOT 1,3:5", "2,6:4294967294" },
+ { "NOT 1:100 NOT 50:200", "201:4294967294" },
+ { "OR NOT 1:100 NOT 50:200", "1:49,101:4294967294" },
+
+ { "UID 1:5 UID 6:7", "NOT ALL" },
+ { "UID 1:5 UID 3:7", "UID 3:5" },
+ { "OR UID 1 UID 2", "UID 1:2" },
+ { "NOT UID 1,3:5", "UID 2,6:4294967294" },
+
+ { "1:5 UID 10:20", "1:5 UID 10:20" },
+ { "1:5 NOT UID 10:20", "1:5 UID 1:9,21:4294967294" },
+
+ { "ALL NOT UID 3:*", "NOT UID 3:4294967295" },
+ { "NOT 1:10 NOT *", "11:4294967294 NOT 4294967295" },
+
+ { "BEFORE 03-Aug-2014 BEFORE 01-Aug-2014 BEFORE 02-Aug-2014", "BEFORE \"01-Aug-2014\"" },
+ { "OR BEFORE 01-Aug-2014 BEFORE 02-Aug-2014", "BEFORE \"02-Aug-2014\"" },
+ { "OR BEFORE 01-Aug-2014 OR BEFORE 03-Aug-2014 BEFORE 02-Aug-2014", "BEFORE \"03-Aug-2014\"" },
+ { "BEFORE 03-Aug-2014 NOT BEFORE 01-Aug-2014 BEFORE 02-Aug-2014", "BEFORE \"02-Aug-2014\" NOT BEFORE \"01-Aug-2014\"" },
+ { "SENTBEFORE 03-Aug-2014 SENTBEFORE 01-Aug-2014 SENTBEFORE 02-Aug-2014", "SENTBEFORE \"01-Aug-2014\"" },
+ { "SENTBEFORE 03-Aug-2014 BEFORE 01-Aug-2014 SENTBEFORE 02-Aug-2014", "SENTBEFORE \"02-Aug-2014\" BEFORE \"01-Aug-2014\"" },
+
+ { "ON 03-Aug-2014 ON 03-Aug-2014", "ON \"03-Aug-2014\"" },
+ { "ON 03-Aug-2014 ON 04-Aug-2014", "ON \"03-Aug-2014\" ON \"04-Aug-2014\"" }, /* this could be replaced with e.g. NOT ALL */
+ { "OR ON 03-Aug-2014 ON 04-Aug-2014", "OR ON \"03-Aug-2014\" ON \"04-Aug-2014\"" },
+
+ { "SINCE 03-Aug-2014 SINCE 01-Aug-2014 SINCE 02-Aug-2014", "SINCE \"03-Aug-2014\"" },
+ { "OR SINCE 01-Aug-2014 SINCE 02-Aug-2014", "SINCE \"01-Aug-2014\"" },
+ { "OR SINCE 01-Aug-2014 OR SINCE 03-Aug-2014 SINCE 02-Aug-2014", "SINCE \"01-Aug-2014\"" },
+ { "SINCE 03-Aug-2014 NOT SINCE 01-Aug-2014 SINCE 02-Aug-2014", "SINCE \"03-Aug-2014\" NOT SINCE \"01-Aug-2014\"" },
+ { "SENTSINCE 03-Aug-2014 SENTSINCE 01-Aug-2014 SENTSINCE 02-Aug-2014", "SENTSINCE \"03-Aug-2014\"" },
+ { "SENTSINCE 03-Aug-2014 SINCE 01-Aug-2014 SENTSINCE 02-Aug-2014", "SENTSINCE \"03-Aug-2014\" SINCE \"01-Aug-2014\"" },
+
+ { "SMALLER 1 SMALLER 2", "SMALLER 1" },
+ { "OR SMALLER 1 SMALLER 2", "SMALLER 2" },
+ { "OR SMALLER 1 OR SMALLER 3 SMALLER 2", "SMALLER 3" },
+ { "SMALLER 3 NOT SMALLER 1 SMALLER 2", "SMALLER 2 NOT SMALLER 1" },
+ { "SMALLER 3 LARGER 5", "SMALLER 3 LARGER 5" }, /* this could be replaced with e.g. NOT ALL */
+ { "OR SMALLER 3 LARGER 5", "OR SMALLER 3 LARGER 5" },
+
+ { "LARGER 3 LARGER 1 LARGER 2", "LARGER 3" },
+ { "OR LARGER 1 LARGER 2", "LARGER 1" },
+ { "OR LARGER 1 OR LARGER 3 LARGER 2", "LARGER 1" },
+ { "LARGER 3 NOT LARGER 1 LARGER 2", "LARGER 3 NOT LARGER 1" },
+
+ { "SUBJECT foo SUBJECT foo", "SUBJECT foo" },
+ { "SUBJECT foo NOT SUBJECT foo", "NOT ALL" },
+ { "OR SUBJECT foo NOT SUBJECT foo", "ALL" },
+ { "SUBJECT foo SUBJECT foob", "SUBJECT foo SUBJECT foob" },
+ { "OR SUBJECT foo SUBJECT foo", "SUBJECT foo" },
+ { "FROM foo FROM foo", "FROM foo" },
+ { "FROM foo NOT FROM foo", "NOT ALL" },
+ { "OR FROM foo NOT FROM foo", "ALL" },
+ { "FROM foo FROM bar", "FROM foo FROM bar" },
+ { "FROM foo TO foo", "FROM foo TO foo" },
+
+ { "TEXT foo TEXT foo", "TEXT foo" },
+ { "TEXT foo TEXT foob", "TEXT foo TEXT foob" },
+ { "OR TEXT foo TEXT foo", "TEXT foo" },
+ { "OR NOT TEXT foo TEXT foo", "ALL" },
+ { "OR TEXT foo NOT TEXT foo", "ALL" },
+ { "TEXT foo NOT TEXT foo", "NOT ALL" },
+ { "NOT TEXT foo TEXT foo", "NOT ALL" },
+ { "BODY foo BODY foo", "BODY foo" },
+ { "BODY foo NOT BODY foo", "NOT ALL" },
+ { "OR BODY foo NOT BODY foo", "ALL" },
+ { "OR BODY foo BODY foo", "BODY foo" },
+ { "TEXT foo BODY foo", "TEXT foo BODY foo" },
+ { "OR ( TEXT foo OR TEXT foo TEXT foo ) ( TEXT foo ( TEXT foo ) )", "TEXT foo" },
+
+ /* value="" tests */
+ { "HEADER foo ", "HEADER FOO \"\"" },
+ { "SUBJECT ", "SUBJECT \"\"" },
+ { "BODY ", "ALL" },
+ { "TEXT ", "ALL" },
+ { "HEADER foo .", "HEADER FOO ." },
+ { "SUBJECT .", "SUBJECT ." },
+ { "BODY .", "BODY ." },
+ { "TEXT .", "TEXT ." },
+
+ /* OR: drop redundant args */
+ { "OR ( TEXT common1 TEXT unique1 ) TEXT common1", "TEXT common1" },
+ { "OR ( TEXT unique1 TEXT common1 ) TEXT common1", "TEXT common1" },
+ { "OR TEXT common1 ( TEXT common1 TEXT unique1 )", "TEXT common1" },
+ { "OR TEXT common1 ( TEXT unique1 TEXT common1 )", "TEXT common1" },
+ { "OR ( TEXT common1 TEXT common2 ) ( TEXT common1 TEXT common2 TEXT unique1 )", "TEXT common1 TEXT common2" },
+ { "OR TEXT common1 OR ( TEXT unique1 TEXT common1 ) ( TEXT unique3 TEXT common1 )", "TEXT common1" },
+
+ /* OR: extract common AND */
+ { "OR ( TEXT common1 TEXT unique1 ) ( TEXT common1 TEXT unique2 )", "OR TEXT unique1 TEXT unique2 TEXT common1" },
+ { "OR ( TEXT unique1 TEXT common1 ) ( TEXT unique2 TEXT common1 )", "OR TEXT unique1 TEXT unique2 TEXT common1" },
+ { "OR ( TEXT common1 TEXT unique1 ) ( TEXT unique2 TEXT common1 )", "OR TEXT unique1 TEXT unique2 TEXT common1" },
+ { "OR ( TEXT unique1 TEXT common1 ) ( TEXT common1 TEXT unique2 )", "OR TEXT unique1 TEXT unique2 TEXT common1" },
+
+ { "OR ( TEXT unique1 TEXT common1 ) ( TEXT common1 TEXT unique2 TEXT unique3 )", "OR TEXT unique1 (TEXT unique2 TEXT unique3) TEXT common1" },
+ { "OR ( TEXT common1 TEXT common2 TEXT unique1 ) ( TEXT common1 TEXT common2 TEXT unique2 )", "OR TEXT unique1 TEXT unique2 TEXT common2 TEXT common1" },
+ { "OR ( TEXT common1 TEXT common2 TEXT unique1 TEXT unique2 ) ( TEXT common1 TEXT common2 TEXT unique3 TEXT unique4 )", "OR (TEXT unique1 TEXT unique2) (TEXT unique3 TEXT unique4) TEXT common2 TEXT common1" },
+
+ /* non-matching cases */
+ { "OR ( TEXT unique1 TEXT unique2 ) TEXT unique3", "OR (TEXT unique1 TEXT unique2) TEXT unique3" },
+ { "OR ( TEXT unique1 TEXT unique2 ) ( TEXT unique3 TEXT unique4 )", "OR (TEXT unique1 TEXT unique2) (TEXT unique3 TEXT unique4)" },
+ { "OR ( TEXT common1 TEXT unique1 ) OR ( TEXT common1 TEXT unique2 ) TEXT unique3", "OR (TEXT common1 TEXT unique1) OR (TEXT common1 TEXT unique2) TEXT unique3" },
+ { "OR ( TEXT common1 TEXT unique1 ) OR ( TEXT common1 TEXT common2 ) ( TEXT common2 TEXT unique2 )", "OR (TEXT common1 TEXT unique1) OR (TEXT common1 TEXT common2) (TEXT common2 TEXT unique2)" },
+
+ /* SUB: drop redundant args */
+ { "( OR TEXT common1 TEXT unique1 ) TEXT common1", "TEXT common1" },
+ { "( OR TEXT unique1 TEXT common1 ) TEXT common1", "TEXT common1" },
+ { "TEXT common1 ( OR TEXT common1 TEXT unique1 )", "TEXT common1" },
+ { "TEXT common1 ( OR TEXT unique1 TEXT common1 )", "TEXT common1" },
+ { "( OR TEXT common1 TEXT common2 ) ( OR TEXT common1 OR TEXT common2 TEXT unique1 )", "OR TEXT common1 TEXT common2" },
+ { "TEXT common1 ( OR TEXT unique1 TEXT common1 ) ( OR TEXT unique3 TEXT common1 )", "TEXT common1" },
+ { "OR ( TEXT common1 ( OR TEXT unique1 TEXT common1 ) ) TEXT unique1", "OR TEXT common1 TEXT unique1" },
+
+ /* SUB: extract common OR */
+ { "( OR TEXT common1 TEXT unique1 ) ( OR TEXT common1 TEXT unique2 )", "OR (TEXT unique1 TEXT unique2) TEXT common1" },
+ { "( OR TEXT unique1 TEXT common1 ) ( OR TEXT unique2 TEXT common1 )", "OR (TEXT unique1 TEXT unique2) TEXT common1" },
+ { "( OR TEXT common1 TEXT unique1 ) ( OR TEXT unique2 TEXT common1 )", "OR (TEXT unique1 TEXT unique2) TEXT common1" },
+ { "( OR TEXT unique1 TEXT common1 ) ( OR TEXT common1 TEXT unique2 )", "OR (TEXT unique1 TEXT unique2) TEXT common1" },
+
+ { "( OR TEXT unique1 TEXT common1 ) ( OR TEXT common1 OR TEXT unique2 TEXT unique3 )", "OR (TEXT unique1 OR TEXT unique2 TEXT unique3) TEXT common1" },
+ { "( OR TEXT common1 OR TEXT common2 TEXT unique1 ) ( OR TEXT common1 OR TEXT common2 TEXT unique2 )", "OR (TEXT unique1 TEXT unique2) OR TEXT common2 TEXT common1" },
+ { "( OR TEXT common1 OR TEXT common2 OR TEXT unique1 TEXT unique2 ) ( OR TEXT common1 OR TEXT common2 OR TEXT unique3 TEXT unique4 )", "OR (OR TEXT unique1 TEXT unique2 OR TEXT unique3 TEXT unique4) OR TEXT common2 TEXT common1" },
+
+ /* non-matching cases */
+ { "( OR TEXT unique1 TEXT unique2 ) TEXT unique3", "OR TEXT unique1 TEXT unique2 TEXT unique3" },
+ { "( OR TEXT unique1 TEXT unique2 ) ( OR TEXT unique3 TEXT unique4 )", "OR TEXT unique1 TEXT unique2 OR TEXT unique3 TEXT unique4" },
+ { "( OR TEXT common1 TEXT unique1 ) ( OR TEXT common1 TEXT unique2 ) TEXT unique3", "OR TEXT common1 TEXT unique1 OR TEXT common1 TEXT unique2 TEXT unique3" },
+ { "( OR TEXT common1 TEXT unique1 ) ( OR TEXT common1 TEXT common2 ) ( OR TEXT common2 TEXT unique2 )", "OR TEXT common1 TEXT unique1 OR TEXT common1 TEXT common2 OR TEXT common2 TEXT unique2" },
+};
+
+static struct mail_search_args *
+test_build_search_args(const char *args)
+{
+ struct mail_search_parser *parser;
+ struct mail_search_args *sargs;
+ const char *error, *charset = "UTF-8";
+
+ parser = mail_search_parser_init_cmdline(t_strsplit(args, " "));
+ if (mail_search_build(mail_search_register_get_imap(),
+ parser, &charset, &sargs, &error) < 0)
+ i_panic("%s", error);
+ mail_search_parser_deinit(&parser);
+ return sargs;
+}
+
+static bool test_search_args_are_initialized(struct mail_search_arg *arg)
+{
+ for (; arg != NULL; arg = arg->next) {
+ switch (arg->type) {
+ case SEARCH_MODSEQ:
+ if (arg->value.str != NULL &&
+ arg->initialized.keywords == NULL)
+ return FALSE;
+ break;
+ case SEARCH_KEYWORDS:
+ if (arg->initialized.keywords == NULL)
+ return FALSE;
+ break;
+ case SEARCH_MAILBOX_GLOB:
+ if (arg->initialized.mailbox_glob == NULL)
+ return FALSE;
+ break;
+ case SEARCH_INTHREAD:
+ case SEARCH_SUB:
+ case SEARCH_OR:
+ if (!test_search_args_are_initialized(arg->value.subargs))
+ return FALSE;
+ break;
+ default:
+ break;
+ }
+ }
+ return TRUE;
+}
+
+static void test_mail_search_args_simplify(void)
+{
+ struct mail_search_args *args;
+ struct mail_storage_settings set = { .mail_max_keyword_length = 100 };
+ struct mail_storage storage = { .set = &set };
+ struct mailbox box = { .opened = TRUE, .storage = &storage };
+ string_t *str = t_str_new(256);
+ const char *error;
+ unsigned int i;
+
+ test_begin("mail search args simplify");
+ box.index = mail_index_alloc(NULL, NULL, "dovecot.index.");
+ for (i = 0; i < N_ELEMENTS(tests); i++) {
+ args = test_build_search_args(tests[i].input);
+ /* delay simplification until after init. that way we can test
+ that the simplification works correctly when working on
+ already-initialized args. */
+ args->simplified = TRUE;
+ mail_search_args_init(args, &box, FALSE, NULL);
+ mail_search_args_simplify(args);
+
+ str_truncate(str, 0);
+ test_assert(mail_search_args_to_imap(str, args->args, &error));
+ test_assert_idx(strcmp(str_c(str), tests[i].output) == 0, i);
+
+ test_assert_idx(test_search_args_are_initialized(args->args), i);
+ mail_search_args_unref(&args);
+ }
+ mail_index_free(&box.index);
+ test_end();
+}
+
+static void test_mail_search_args_simplify_empty_lists(void)
+{
+ struct mail_search_args *args;
+
+ test_begin("mail search args simplify empty args");
+
+ args = mail_search_build_init();
+ mail_search_args_simplify(args);
+ mail_search_args_unref(&args);
+
+ test_end();
+}
+
+int main(void)
+{
+ static void (*const test_functions[])(void) = {
+ mail_storage_init,
+ test_mail_search_args_simplify,
+ test_mail_search_args_simplify_empty_lists,
+ mail_storage_deinit,
+ NULL
+ };
+
+ return test_run(test_functions);
+}
diff --git a/src/lib-storage/test-mail-storage-common.c b/src/lib-storage/test-mail-storage-common.c
new file mode 100644
index 0000000..6c77ec1
--- /dev/null
+++ b/src/lib-storage/test-mail-storage-common.c
@@ -0,0 +1,118 @@
+/* Copyright (c) 2017-2020 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "ioloop.h"
+#include "mkdir-parents.h"
+#include "unlink-directory.h"
+#include "path-util.h"
+#include "master-service.h"
+#include "mail-storage-service.h"
+#include "test-mail-storage-common.h"
+
+struct test_mail_storage_ctx *test_mail_storage_init(void)
+{
+ struct test_mail_storage_ctx *ctx;
+ const char *current_dir, *error;
+ pool_t pool;
+
+ pool = pool_allocfree_create("test pool");
+ ctx = p_new(pool, struct test_mail_storage_ctx, 1);
+ ctx->pool = pool;
+
+ if (t_get_working_dir(&current_dir, &error) < 0)
+ i_fatal("Failed to get current directory: %s", error);
+ ctx->home_root = p_strdup_printf(ctx->pool, "%s/.test-home/",
+ current_dir);
+
+ if (unlink_directory(ctx->home_root, UNLINK_DIRECTORY_FLAG_RMDIR, &error) < 0 &&
+ errno != ENOENT)
+ i_warning("unlink_directory(%s) failed: %s", ctx->home_root, error);
+
+ ctx->ioloop = io_loop_create();
+
+ ctx->storage_service = mail_storage_service_init(master_service, NULL,
+ MAIL_STORAGE_SERVICE_FLAG_NO_RESTRICT_ACCESS |
+ MAIL_STORAGE_SERVICE_FLAG_NO_LOG_INIT |
+ MAIL_STORAGE_SERVICE_FLAG_NO_PLUGINS);
+ return ctx;
+}
+
+void test_mail_storage_deinit(struct test_mail_storage_ctx **_ctx)
+{
+ struct test_mail_storage_ctx *ctx = *_ctx;
+ const char *error;
+ mail_storage_service_deinit(&ctx->storage_service);
+
+ *_ctx = NULL;
+
+ if (chdir(ctx->home_root) < 0)
+ i_fatal("chdir(%s) failed: %m", ctx->home_root);
+ if (chdir("..") < 0)
+ i_fatal("chdir(..) failed: %m");
+
+ if (unlink_directory(ctx->home_root, UNLINK_DIRECTORY_FLAG_RMDIR,
+ &error) < 0)
+ i_error("unlink_directory(%s) failed: %s", ctx->home_root, error);
+
+ io_loop_destroy(&ctx->ioloop);
+
+ pool_unref(&ctx->pool);
+}
+
+void test_mail_storage_init_user(struct test_mail_storage_ctx *ctx,
+ const struct test_mail_storage_settings *set)
+{
+ const char *username = set->username != NULL ?
+ set->username : "testuser";
+ const char *error, *home;
+ ARRAY_TYPE(const_string) opts;
+
+ home = t_strdup_printf("%s%s", ctx->home_root, username);
+
+ const char *const default_input[] = {
+ t_strdup_printf("mail=%s:~/%s", set->driver,
+ set->driver_opts == NULL ? "" : set->driver_opts),
+ "postmaster_address=postmaster@localhost",
+ "namespace=inbox",
+ "namespace/inbox/prefix=",
+ "namespace/inbox/inbox=yes",
+ t_strdup_printf("home=%s/%s", home, username),
+ };
+
+ if (unlink_directory(home, UNLINK_DIRECTORY_FLAG_RMDIR, &error) < 0)
+ i_error("%s", error);
+ i_assert(mkdir_parents(home, S_IRWXU)==0 || errno == EEXIST);
+
+ t_array_init(&opts, 20);
+ array_append(&opts, default_input, N_ELEMENTS(default_input));
+ if (set->hierarchy_sep != NULL) {
+ const char *opt =
+ t_strdup_printf("namespace/inbox/separator=%s",
+ set->hierarchy_sep);
+ array_push_back(&opts, &opt);
+ }
+ if (set->extra_input != NULL)
+ array_append(&opts, set->extra_input,
+ str_array_length(set->extra_input));
+
+ array_append_zero(&opts);
+ struct mail_storage_service_input input = {
+ .userdb_fields = array_front(&opts),
+ .username = username,
+ .no_userdb_lookup = TRUE,
+ .debug = FALSE,
+ };
+
+ if (mail_storage_service_lookup_next(ctx->storage_service, &input,
+ &ctx->service_user, &ctx->user,
+ &error) < 0) {
+ i_fatal("mail_storage_service_lookup_next(%s) failed: %s",
+ username, error);
+ }
+}
+
+void test_mail_storage_deinit_user(struct test_mail_storage_ctx *ctx)
+{
+ mail_user_deinit(&ctx->user);
+ mail_storage_service_user_unref(&ctx->service_user);
+}
diff --git a/src/lib-storage/test-mail-storage-common.h b/src/lib-storage/test-mail-storage-common.h
new file mode 100644
index 0000000..161fb3f
--- /dev/null
+++ b/src/lib-storage/test-mail-storage-common.h
@@ -0,0 +1,30 @@
+#ifndef TEST_MAIL_STORAGE_H
+#define TEST_MAIL_STORAGE_H
+
+#include "mail-storage-private.h"
+
+struct test_mail_storage_ctx {
+ pool_t pool;
+ struct mail_storage_service_ctx *storage_service;
+ struct mail_user *user;
+ struct mail_storage_service_user *service_user;
+ struct ioloop *ioloop;
+ const char *home_root;
+};
+
+struct test_mail_storage_settings {
+ const char *username;
+ const char *driver;
+ const char *driver_opts;
+ const char *hierarchy_sep;
+ const char *const *extra_input;
+};
+
+struct test_mail_storage_ctx *test_mail_storage_init(void);
+void test_mail_storage_deinit(struct test_mail_storage_ctx **ctx);
+
+void test_mail_storage_init_user(struct test_mail_storage_ctx *ctx,
+ const struct test_mail_storage_settings *set);
+void test_mail_storage_deinit_user(struct test_mail_storage_ctx *ctx);
+
+#endif
diff --git a/src/lib-storage/test-mail-storage.c b/src/lib-storage/test-mail-storage.c
new file mode 100644
index 0000000..0a6b906
--- /dev/null
+++ b/src/lib-storage/test-mail-storage.c
@@ -0,0 +1,652 @@
+/* Copyright (c) 2017-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "test-common.h"
+#include "master-service.h"
+#include "test-mail-storage-common.h"
+
+static void test_init_storage(struct mail_storage *storage_r)
+{
+ i_zero(storage_r);
+ storage_r->user = t_new(struct mail_user, 1);
+ storage_r->user->event = event_create(NULL);
+ storage_r->event = event_create(storage_r->user->event);
+}
+
+static void test_deinit_storage(struct mail_storage *storage)
+{
+ mail_storage_clear_error(storage);
+ if (array_is_created(&storage->error_stack)) {
+ mail_storage_clear_error(storage);
+ i_assert(array_count(&storage->error_stack) == 0);
+ array_free(&storage->error_stack);
+ }
+ event_unref(&storage->event);
+ event_unref(&storage->user->event);
+}
+
+static void test_mail_storage_errors(void)
+{
+ /* NOTE: keep in sync with test-mailbox-list.c */
+ struct mail_storage storage;
+ enum mail_error mail_error;
+ const char *errstr;
+
+ test_begin("mail storage errors");
+ test_init_storage(&storage);
+
+ /* try a regular error */
+ mail_storage_set_error(&storage, MAIL_ERROR_PERM, "error1");
+ test_assert(strcmp(mail_storage_get_last_error(&storage, &mail_error),
+ "error1") == 0);
+ test_assert(mail_error == MAIL_ERROR_PERM);
+ test_assert(strcmp(mail_storage_get_last_internal_error(&storage, &mail_error),
+ "error1") == 0);
+ test_assert(mail_error == MAIL_ERROR_PERM);
+ test_assert(!storage.last_error_is_internal);
+
+ /* set the error to itself */
+ mail_storage_set_error(&storage, MAIL_ERROR_PARAMS,
+ mail_storage_get_last_error(&storage, &mail_error));
+ test_assert(strcmp(mail_storage_get_last_error(&storage, &mail_error),
+ "error1") == 0);
+ test_assert(mail_error == MAIL_ERROR_PARAMS);
+ test_assert(strcmp(mail_storage_get_last_internal_error(&storage, &mail_error),
+ "error1") == 0);
+ test_assert(mail_error == MAIL_ERROR_PARAMS);
+ test_assert(!storage.last_error_is_internal);
+
+ /* clear the error - asking for it afterwards is a bug */
+ mail_storage_clear_error(&storage);
+ test_assert(strcmp(mail_storage_get_last_error(&storage, &mail_error),
+ "BUG: Unknown internal error") == 0);
+ test_assert(mail_error == MAIL_ERROR_TEMP);
+ test_assert(strcmp(mail_storage_get_last_internal_error(&storage, &mail_error),
+ "BUG: Unknown internal error") == 0);
+ test_assert(mail_error == MAIL_ERROR_TEMP);
+ test_assert(!storage.last_error_is_internal);
+
+ /* set internal error in preparation for the next test */
+ test_expect_error_string("critical0");
+ mail_storage_set_critical(&storage, "critical0");
+ test_expect_no_more_errors();
+ test_assert(strstr(mail_storage_get_last_error(&storage, &mail_error),
+ MAIL_ERRSTR_CRITICAL_MSG) != NULL);
+ test_assert(mail_error == MAIL_ERROR_TEMP);
+ test_assert(strcmp(mail_storage_get_last_internal_error(&storage, &mail_error),
+ "critical0") == 0);
+ test_assert(mail_error == MAIL_ERROR_TEMP);
+ test_assert(storage.last_error_is_internal);
+
+ /* internal error without specifying what it is. this needs to clear
+ the previous internal error. */
+ mail_storage_set_internal_error(&storage);
+ test_assert(strstr(mail_storage_get_last_error(&storage, &mail_error),
+ MAIL_ERRSTR_CRITICAL_MSG) != NULL);
+ test_assert(mail_error == MAIL_ERROR_TEMP);
+ test_assert(strstr(mail_storage_get_last_internal_error(&storage, &mail_error),
+ MAIL_ERRSTR_CRITICAL_MSG) != NULL);
+ test_assert(mail_error == MAIL_ERROR_TEMP);
+ test_assert(!storage.last_error_is_internal);
+
+ /* proper internal error */
+ test_expect_error_string("critical1");
+ mail_storage_set_critical(&storage, "critical1");
+ test_expect_no_more_errors();
+ test_assert(strstr(mail_storage_get_last_error(&storage, &mail_error),
+ MAIL_ERRSTR_CRITICAL_MSG) != NULL);
+ test_assert(mail_error == MAIL_ERROR_TEMP);
+ test_assert(strcmp(mail_storage_get_last_internal_error(&storage, &mail_error),
+ "critical1") == 0);
+ test_assert(mail_error == MAIL_ERROR_TEMP);
+ test_assert(storage.last_error_is_internal);
+
+ /* use it in the following internal error */
+ test_expect_error_string("critical2: critical1");
+ mail_storage_set_critical(&storage, "critical2: %s",
+ mail_storage_get_last_internal_error(&storage, &mail_error));
+ test_expect_no_more_errors();
+ test_assert(strstr(mail_storage_get_last_error(&storage, &mail_error),
+ MAIL_ERRSTR_CRITICAL_MSG) != NULL);
+ test_assert(mail_error == MAIL_ERROR_TEMP);
+ test_assert(strcmp(mail_storage_get_last_internal_error(&storage, &mail_error),
+ "critical2: critical1") == 0);
+ test_assert(mail_error == MAIL_ERROR_TEMP);
+ test_assert(storage.last_error_is_internal);
+
+ /* use the previous non-internal error as part of the internal error */
+ test_expect_error_string("critical3: "MAIL_ERRSTR_CRITICAL_MSG);
+ mail_storage_set_critical(&storage, "critical3: %s",
+ mail_storage_get_last_error(&storage, &mail_error));
+ test_expect_no_more_errors();
+ test_assert(strstr(mail_storage_get_last_error(&storage, &mail_error),
+ MAIL_ERRSTR_CRITICAL_MSG) != NULL);
+ test_assert(mail_error == MAIL_ERROR_TEMP);
+ errstr = mail_storage_get_last_internal_error(&storage, &mail_error);
+ test_assert(str_begins(errstr, "critical3: "));
+ test_assert(strstr(errstr+11, MAIL_ERRSTR_CRITICAL_MSG) != NULL);
+ test_assert(mail_error == MAIL_ERROR_TEMP);
+ test_assert(storage.last_error_is_internal);
+
+ /* clear the error again and check that all is as expected */
+ mail_storage_clear_error(&storage);
+ test_assert(strcmp(mail_storage_get_last_error(&storage, &mail_error),
+ "BUG: Unknown internal error") == 0);
+ test_assert(mail_error == MAIL_ERROR_TEMP);
+ test_assert(strcmp(mail_storage_get_last_internal_error(&storage, &mail_error),
+ "BUG: Unknown internal error") == 0);
+ test_assert(mail_error == MAIL_ERROR_TEMP);
+ test_assert(!storage.last_error_is_internal);
+
+ /* use internal error as a regular error (although that really
+ shouldn't be done) */
+ test_expect_error_string("critical4");
+ mail_storage_set_critical(&storage, "critical4");
+ mail_storage_set_error(&storage, MAIL_ERROR_PARAMS,
+ mail_storage_get_last_internal_error(&storage, &mail_error));
+ test_expect_no_more_errors();
+ test_assert(strcmp(mail_storage_get_last_error(&storage, &mail_error),
+ "critical4") == 0);
+ test_assert(mail_error == MAIL_ERROR_PARAMS);
+ test_assert(strcmp(mail_storage_get_last_internal_error(&storage, &mail_error),
+ "critical4") == 0);
+ test_assert(mail_error == MAIL_ERROR_PARAMS);
+ test_assert(!storage.last_error_is_internal);
+
+ test_deinit_storage(&storage);
+ test_end();
+}
+
+static void test_mail_storage_last_error_push_pop(void)
+{
+ /* NOTE: keep in sync with test-mailbox-list.c */
+ struct mail_storage storage;
+ enum mail_error mail_error;
+
+ test_begin("mail_storage_last_error_push/pop()");
+ test_init_storage(&storage);
+
+ /* regular error 1 */
+ mail_storage_set_error(&storage, MAIL_ERROR_PERM, "regular error 1");
+ mail_storage_last_error_push(&storage);
+
+ /* critical error 1 */
+ test_expect_error_string("critical error 1");
+ mail_storage_set_critical(&storage, "critical error 1");
+ test_expect_no_more_errors();
+ mail_storage_last_error_push(&storage);
+
+ /* regular error 2 */
+ mail_storage_set_error(&storage, MAIL_ERROR_PARAMS, "regular error 2");
+ mail_storage_last_error_push(&storage);
+
+ /* critical error 2 */
+ test_expect_error_string("critical error 2");
+ mail_storage_set_critical(&storage, "critical error 2");
+ test_expect_no_more_errors();
+ mail_storage_last_error_push(&storage);
+
+ /* -- clear all errors -- */
+ mail_storage_clear_error(&storage);
+
+ /* critical error 2 pop */
+ mail_storage_last_error_pop(&storage);
+ test_assert(strstr(mail_storage_get_last_error(&storage, &mail_error),
+ MAIL_ERRSTR_CRITICAL_MSG) != NULL);
+ test_assert(mail_error == MAIL_ERROR_TEMP);
+ test_assert(strcmp(mail_storage_get_last_internal_error(&storage, &mail_error),
+ "critical error 2") == 0);
+ test_assert(mail_error == MAIL_ERROR_TEMP);
+ test_assert(storage.last_error_is_internal);
+
+ /* regular error 2 pop */
+ mail_storage_last_error_pop(&storage);
+ test_assert(strcmp(mail_storage_get_last_error(&storage, &mail_error),
+ "regular error 2") == 0);
+ test_assert(mail_error == MAIL_ERROR_PARAMS);
+ test_assert(strcmp(mail_storage_get_last_internal_error(&storage, &mail_error),
+ "regular error 2") == 0);
+ test_assert(mail_error == MAIL_ERROR_PARAMS);
+ test_assert(!storage.last_error_is_internal);
+
+ /* critical error 1 pop */
+ mail_storage_last_error_pop(&storage);
+ test_assert(strstr(mail_storage_get_last_error(&storage, &mail_error),
+ MAIL_ERRSTR_CRITICAL_MSG) != NULL);
+ test_assert(mail_error == MAIL_ERROR_TEMP);
+ test_assert(strcmp(mail_storage_get_last_internal_error(&storage, &mail_error),
+ "critical error 1") == 0);
+ test_assert(mail_error == MAIL_ERROR_TEMP);
+ test_assert(storage.last_error_is_internal);
+
+ /* regular error 1 pop */
+ mail_storage_last_error_pop(&storage);
+ test_assert(strcmp(mail_storage_get_last_error(&storage, &mail_error),
+ "regular error 1") == 0);
+ test_assert(mail_error == MAIL_ERROR_PERM);
+ test_assert(strcmp(mail_storage_get_last_internal_error(&storage, &mail_error),
+ "regular error 1") == 0);
+ test_assert(mail_error == MAIL_ERROR_PERM);
+ test_assert(!storage.last_error_is_internal);
+
+ test_deinit_storage(&storage);
+ test_end();
+}
+
+struct mailbox_verify_test_cases {
+ char ns_sep;
+ char list_sep;
+ const char *box;
+ int ret;
+} test_cases[] = {
+ { '\0', '\0', "INBOX", 0 },
+ { '/', '/', ".DUMPSTER", 0 },
+ { '\0', '\0', "DUMPSTER", 0 },
+ { '\0', '\0', "~DUMPSTER", -1 },
+ { '/', '.', "INBOX/INBOX", 0 },
+ { '/', '/', "INBOX/INBOX", 0 },
+ { '.', '.', "INBOX/INBOX", 0 },
+ { '.', '/', "INBOX/INBOX", -1 },
+ { '\0', '\0', "/etc/passwd", -1 },
+ { '.', '.', "foo.bar", 0 },
+ { '/', '.', "foo.bar", -1 },
+ { '.', '/', "foo.bar", 0 },
+ { '/', '/', "foo.bar", 0 },
+ { '/', '\0', "/foo", -1 },
+ { '/', '\0', "foo/", -1 },
+ { '/', '\0', "foo//bar", -1 },
+ { '.', '/', "/foo", -1 },
+ { '.', '/', "foo/", -1 },
+ { '.', '/', "foo//bar", -1 },
+ { '.', '.', ".foo", -1 },
+ { '.', '.', "foo.", -1 },
+ { '.', '.', "foo..bar", -1 },
+ { '.', '/', ".foo", -1 },
+ { '.', '/', "foo.", -1 },
+ { '.', '/', "foo..bar", -1 },
+ { '.', '/', "/", -1 },
+ { '.', '.', ".", -1 },
+ { '/', '\0', "/", -1 },
+ { '\0', '/', "/", -1 },
+ { '\0', '\0', "", -1 },
+};
+
+struct mailbox_verify_test_cases layout_index_test_cases[] = {
+ { '\0', '\0', "INBOX", 0 },
+ { '/', '/', ".DUMPSTER", 0 },
+ { '\0', '\0', "DUMPSTER", 0 },
+ { '\0', '\0', "~DUMPSTER", 0 },
+ { '\0', '\0', "^DUMPSTER", 0 },
+ { '\0', '\0', "%DUMPSTER", 0 },
+ { '/', '.', "INBOX/INBOX", 0 },
+ { '/', '/', "INBOX/INBOX", 0 },
+ { '.', '.', "INBOX/INBOX", 0 },
+ { '.', '/', "INBOX/INBOX", -1 },
+ { '/', '\0', "/etc/passwd", -1 },
+ { '.', '\0', "/etc/passwd", 0 },
+ { '.', '.', "foo.bar", 0 },
+ { '/', '.', "foo.bar", -1 },
+ { '.', '/', "foo.bar", 0 },
+ { '/', '/', "foo.bar", 0 },
+ { '/', '\0', "/foo", -1 },
+ { '/', '\0', "foo/", -1 },
+ { '/', '\0', "foo//bar", -1 },
+ { '.', '/', "/foo", -1 },
+ { '.', '/', "foo/", -1 },
+ { '.', '/', "foo//bar", -1 },
+ { '.', '.', ".foo", -1 },
+ { '.', '.', "foo.", -1 },
+ { '.', '.', "foo..bar", -1 },
+ { '.', '/', ".foo", -1 },
+ { '.', '/', "foo.", -1 },
+ { '.', '/', "foo..bar", -1 },
+ { '.', '/', "/", -1 },
+ { '.', '.', ".", -1 },
+ { '/', '\0', "/", -1 },
+ { '\0', '/', "/", -1 },
+ { '\0', '\0', "", -1 },
+};
+
+static void
+test_mailbox_verify_name_one(struct mailbox_verify_test_cases *test_case,
+ struct mail_namespace *ns,
+ size_t i)
+{
+ struct mailbox *box;
+ int ret;
+
+ box = mailbox_alloc(ns->list, test_case->box, 0);
+ ret = mailbox_verify_name(box);
+#ifdef DEBUG
+ if (ret != test_case->ret) {
+ i_debug("%c == %c %c == %c",
+ test_case->ns_sep, mail_namespace_get_sep(ns),
+ test_case->list_sep, mailbox_list_get_hierarchy_sep(ns->list));
+ const char *error = "should have failed";
+ if (ret < 0)
+ error = mailbox_get_last_error(box, NULL);
+ i_debug("Failed test for mailbox %s: %s", test_case->box, error);
+ }
+#endif
+ test_assert_idx(ret == test_case->ret, i);
+
+ /* Cannot rename to INBOX */
+ if (strcmp(test_case->box, "INBOX") == 0) {
+ ret = mailbox_create(box, NULL, FALSE);
+ test_assert_idx(ret == 0, i);
+ mailbox_delete(box);
+ mailbox_free(&box);
+ return;
+ }
+
+ struct mailbox *src = mailbox_alloc(ns->list, "RENAME", 0);
+ enum mailbox_existence exists;
+ /* check if the mailbox exists */
+ ret = mailbox_exists(src, FALSE, &exists);
+ test_assert_idx(ret == 0, i);
+ if (ret != 0) {
+ mailbox_free(&box);
+ mailbox_free(&src);
+ return;
+ }
+ if (exists == MAILBOX_EXISTENCE_NONE)
+ (void)mailbox_create(src, NULL, FALSE);
+ ret = mailbox_rename(src, box);
+ #ifdef DEBUG
+ if (ret != test_case->ret) {
+ i_debug("%c == %c %c == %c",
+ test_case->ns_sep, mail_namespace_get_sep(ns),
+ test_case->list_sep, mailbox_list_get_hierarchy_sep(ns->list));
+ const char *error = "should have failed";
+ if (ret < 0)
+ error = mailbox_get_last_error(box, NULL);
+ i_debug("Failed test for mailbox %s: %s", test_case->box, error);
+ }
+#endif
+ test_assert_idx(ret == test_case->ret, i);
+ mailbox_delete(box);
+ mailbox_free(&box);
+ mailbox_free(&src);
+}
+
+static void
+test_mailbox_verify_name_continue(struct mailbox_verify_test_cases *test_cases,
+ size_t ncases, struct test_mail_storage_ctx *ctx)
+{
+ struct mail_namespace *ns =
+ mail_namespace_find_inbox(ctx->user->namespaces);
+
+ for(size_t i = 0; i < ncases; i++) {
+ if ((test_cases[i].ns_sep != '\0' &&
+ (test_cases[i].ns_sep != mail_namespace_get_sep(ns))) ||
+ (test_cases[i].list_sep != '\0' &&
+ test_cases[i].list_sep != mailbox_list_get_hierarchy_sep(ns->list)))
+ continue;
+ test_mailbox_verify_name_one(&test_cases[i], ns, i);
+ }
+}
+
+static void test_mailbox_verify_name_driver_slash(const char *driver,
+ const char *driver_opts,
+ struct test_mail_storage_ctx *ctx)
+{
+ const char *const ns2[] = {
+ "namespace=subspace",
+ "namespace/subspace/separator=/",
+ "namespace/subspace/prefix=SubSpace/",
+ NULL
+ };
+ struct test_mail_storage_settings set = {
+ .driver = driver,
+ .driver_opts = driver_opts,
+ .hierarchy_sep = "/",
+ .extra_input = ns2,
+ };
+ test_mail_storage_init_user(ctx, &set);
+
+ if (strcmp(driver_opts, ":LAYOUT=INDEX") == 0)
+ test_mailbox_verify_name_continue(layout_index_test_cases, N_ELEMENTS(layout_index_test_cases), ctx);
+ else
+ test_mailbox_verify_name_continue(test_cases, N_ELEMENTS(test_cases), ctx);
+
+ test_mail_storage_deinit_user(ctx);
+}
+
+static void test_mailbox_verify_name_driver_dot(const char *driver,
+ const char *driver_opts,
+ struct test_mail_storage_ctx *ctx)
+{
+ const char *const ns2[] = {
+ "namespace=subspace",
+ "namespace/subspace/separator=.",
+ "namespace/subspace/prefix=SubSpace.",
+ NULL
+ };
+ struct test_mail_storage_settings set = {
+ .driver = driver,
+ .driver_opts = driver_opts,
+ .hierarchy_sep = ".",
+ .extra_input = ns2,
+ };
+ test_mail_storage_init_user(ctx, &set);
+
+ if (strcmp(driver_opts, ":LAYOUT=INDEX") == 0)
+ test_mailbox_verify_name_continue(layout_index_test_cases, N_ELEMENTS(layout_index_test_cases), ctx);
+ else
+ test_mailbox_verify_name_continue(test_cases, N_ELEMENTS(test_cases), ctx);
+
+ test_mail_storage_deinit_user(ctx);
+}
+
+static void test_mailbox_verify_name(void)
+{
+ struct {
+ const char *name;
+ const char *driver;
+ const char *opts;
+ } test_cases[] = {
+ { "mbox", "mbox", "" },
+ { "mbox LAYOUT=FS", "mbox", ":LAYOUT=FS" },
+ { "mbox LAYOUT=INDEX", "mbox", ":LAYOUT=INDEX" },
+ { "maildir LAYOUT=INDEX", "maildir", ":LAYOUT=INDEX" },
+ { "sdbox", "sdbox", "" },
+ { "sdbox LAYOUT=FS", "sdbox", ":LAYOUT=FS" },
+ { "sdbox LAYOUT=INDEX", "sdbox", ":LAYOUT=INDEX" },
+ { "mdbox", "mdbox", "" },
+ { "mdbox LAYOUT=FS", "mdbox", ":LAYOUT=FS" },
+ { "mdbox LAYOUT=INDEX", "mdbox", ":LAYOUT=INDEX" },
+ };
+ struct test_mail_storage_ctx *ctx = test_mail_storage_init();
+
+ for(unsigned int i = 0; i < N_ELEMENTS(test_cases); i++) T_BEGIN {
+ test_begin(t_strdup_printf("mailbox_verify_name (%s SEP=.)", test_cases[i].name));
+ test_mailbox_verify_name_driver_dot(test_cases[i].driver, test_cases[i].opts, ctx);
+ test_end();
+ test_begin(t_strdup_printf("mailbox_verify_name (%s SEP=/)", test_cases[i].name));
+ test_mailbox_verify_name_driver_slash(test_cases[i].driver, test_cases[i].opts, ctx);
+ test_end();
+ } T_END;
+
+ test_mail_storage_deinit(&ctx);
+}
+
+static void test_mailbox_list_maildir_continue(struct test_mail_storage_ctx *ctx)
+{
+ struct mailbox_verify_test_cases test_cases[] = {
+ { '\0', '\0', "INBOX", 0 },
+ { '/', '/', ".DUMPSTER", 0 },
+ { '\0', '\0', "DUMPSTER", 0 },
+ { '\0', '\0', "~DUMPSTER", -1 },
+ { '\0', '/', "INBOX/new", -1 },
+ { '\0', '/', "INBOX/cur", -1 },
+ { '\0', '/', "INBOX/tmp", -1 },
+ { '\0', '\0', "/etc/passwd", -1 },
+ { '\0', '/', "SubSpace/new", -1 },
+ { '\0', '/', "SubSpace/cur", -1 },
+ { '\0', '/', "SubSpace/tmp", -1 },
+ { '.', '/', "INBOX.new", -1 },
+ { '.', '/', "INBOX.cur", -1 },
+ { '.', '/', "INBOX.tmp", -1 },
+ { '.', '/', "SubSpace.new", -1 },
+ { '.', '/', "SubSpace.cur", -1 },
+ { '.', '/', "SubSpace.tmp", -1 },
+ { '/', '.', "INBOX/INBOX", 0 },
+ { '/', '/', "INBOX/INBOX", 0 },
+ { '.', '.', "INBOX/INBOX", -1 },
+ { '.', '/', "INBOX/INBOX", -1 },
+ { '.', '.', "foo.bar", 0 },
+ { '/', '.', "foo.bar", -1 },
+ { '.', '/', "foo.bar", 0 },
+ { '/', '/', "foo.bar", 0 },
+ { '/', '\0', "/foo", -1 },
+ { '/', '\0', "foo/", -1 },
+ { '/', '\0', "foo//bar", -1 },
+ { '.', '/', "/foo", -1 },
+ { '.', '/', "foo/", -1 },
+ { '.', '/', "foo//bar", -1 },
+ { '.', '.', ".foo", -1 },
+ { '.', '.', "foo.", -1 },
+ { '.', '.', "foo..bar", -1 },
+ { '.', '/', ".foo", -1 },
+ { '.', '/', "foo.", -1 },
+ { '.', '/', "foo..bar", -1 },
+ { '.', '/', "/", -1 },
+ { '.', '.', ".", -1 },
+ { '/', '\0', "/", -1 },
+ { '\0', '/', "/", -1 },
+ { '\0', '\0', "", -1 },
+ };
+
+ test_mailbox_verify_name_continue(test_cases, N_ELEMENTS(test_cases), ctx);
+}
+
+static void test_mailbox_list_maildir_init(struct test_mail_storage_ctx *ctx,
+ const char *driver_opts, const char *sep)
+{
+ const char *error ATTR_UNUSED;
+ const char *const ns2[] = {
+ "namespace=subspace",
+ t_strdup_printf("namespace/subspace/separator=%s", sep),
+ t_strdup_printf("namespace/subspace/prefix=SubSpace%s", sep),
+ NULL
+ };
+
+ struct test_mail_storage_settings set = {
+ .driver = "maildir",
+ .driver_opts = driver_opts,
+ .hierarchy_sep = sep,
+ .extra_input = ns2,
+ };
+ test_mail_storage_init_user(ctx, &set);
+ test_mailbox_list_maildir_continue(ctx);
+
+ struct mail_namespace *ns =
+ mail_namespace_find_prefix(ctx->user->namespaces,
+ t_strdup_printf("SubSpace%s", sep));
+
+ struct mailbox *box = mailbox_alloc(ns->list, "SubSpace", 0);
+ int ret = mailbox_verify_name(box);
+ test_assert(ret == 0);
+#ifdef DEBUG
+ if (ret < 0) {
+ error = mailbox_get_last_error(box, NULL);
+ i_debug("Failed test for mailbox %s: %s",
+ mailbox_get_vname(box), error);
+ }
+#endif
+ mailbox_free(&box);
+ box = mailbox_alloc(ns->list, t_strdup_printf("SubSpace%sInner", sep), 0);
+ ret = mailbox_verify_name(box);
+ test_assert(ret == 0);
+#ifdef DEBUG
+ if (ret < 0) {
+ error = mailbox_get_last_error(box, NULL);
+ i_debug("Failed test for mailbox %s: %s",
+ mailbox_get_vname(box), error);
+ }
+#endif
+ mailbox_free(&box);
+
+ test_mail_storage_deinit_user(ctx);
+}
+
+static void test_mailbox_list_maildir(void)
+{
+ struct test_mail_storage_ctx *ctx = test_mail_storage_init();
+
+ test_begin("mailbox_verify_name (maildir SEP=.)");
+ test_mailbox_list_maildir_init(ctx, "", ".");
+ test_end();
+
+ test_begin("mailbox_verify_name (maildir SEP=/)");
+ test_mailbox_list_maildir_init(ctx, "", "/");
+ test_end();
+
+ test_begin("mailbox_verify_name (maildir SEP=. LAYOUT=FS)");
+ test_mailbox_list_maildir_init(ctx, "LAYOUT=FS", ".");
+ test_end();
+
+ test_begin("mailbox_verify_name (maildir SEP=/ LAYOUT=FS)");
+ test_mailbox_list_maildir_init(ctx, "LAYOUT=FS", "/");
+ test_end();
+
+ test_mail_storage_deinit(&ctx);
+}
+
+static void test_mailbox_list_mbox(void)
+{
+ struct test_mail_storage_ctx *ctx;
+ struct mailbox_verify_test_cases test_case;
+ struct mail_namespace *ns;
+
+ test_begin("mailbox_list_mbox");
+
+ ctx = test_mail_storage_init();
+
+ /* check that .lock cannot be used */
+ struct test_mail_storage_settings set = {
+ .driver = "mbox",
+ .hierarchy_sep = ".",
+ };
+ test_mail_storage_init_user(ctx, &set);
+
+ test_case.list_sep = '/';
+ test_case.ns_sep = '.';
+ test_case.box = "INBOX/.lock";
+ test_case.ret = -1;
+
+ ns = mail_namespace_find_inbox(ctx->user->namespaces);
+ test_mailbox_verify_name_one(&test_case, ns, 0);
+
+ test_mail_storage_deinit_user(ctx);
+ test_mail_storage_deinit(&ctx);
+
+ test_end();
+}
+
+int main(int argc, char **argv)
+{
+ int ret;
+ void (*const tests[])(void) = {
+ test_mail_storage_errors,
+ test_mail_storage_last_error_push_pop,
+ test_mailbox_verify_name,
+ test_mailbox_list_maildir,
+ test_mailbox_list_mbox,
+ NULL
+ };
+
+ master_service = master_service_init("test-mail-storage",
+ MASTER_SERVICE_FLAG_STANDALONE |
+ MASTER_SERVICE_FLAG_DONT_SEND_STATS |
+ MASTER_SERVICE_FLAG_NO_CONFIG_SETTINGS |
+ MASTER_SERVICE_FLAG_NO_SSL_INIT |
+ MASTER_SERVICE_FLAG_NO_INIT_DATASTACK_FRAME,
+ &argc, &argv, "");
+
+ ret = test_run(tests);
+
+ master_service_deinit(&master_service);
+
+ return ret;
+}
diff --git a/src/lib-storage/test-mail.c b/src/lib-storage/test-mail.c
new file mode 100644
index 0000000..d68fda7
--- /dev/null
+++ b/src/lib-storage/test-mail.c
@@ -0,0 +1,506 @@
+/* Copyright (c) 2020 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "test-common.h"
+#include "istream.h"
+#include "master-service.h"
+#include "message-size.h"
+#include "test-mail-storage-common.h"
+
+static struct event *test_event;
+
+static int
+test_mail_save_trans(struct mailbox_transaction_context *trans,
+ struct istream *input)
+{
+ struct mail_save_context *save_ctx;
+ int ret;
+
+ save_ctx = mailbox_save_alloc(trans);
+ if (mailbox_save_begin(&save_ctx, input) < 0)
+ return -1;
+ do {
+ if (mailbox_save_continue(save_ctx) < 0) {
+ mailbox_save_cancel(&save_ctx);
+ return -1;
+ }
+ } while ((ret = i_stream_read(input)) > 0);
+ i_assert(ret == -1);
+ i_assert(input->stream_errno == 0);
+
+ return mailbox_save_finish(&save_ctx);
+}
+
+static void test_mail_save(struct mailbox *box, const char *mail_input)
+{
+ struct mailbox_transaction_context *trans;
+ struct istream *input;
+ int ret;
+
+ input = i_stream_create_from_data(mail_input, strlen(mail_input));
+ trans = mailbox_transaction_begin(box,
+ MAILBOX_TRANSACTION_FLAG_EXTERNAL, __func__);
+ ret = test_mail_save_trans(trans, input);
+ i_stream_unref(&input);
+ if (ret < 0)
+ mailbox_transaction_rollback(&trans);
+ else
+ ret = mailbox_transaction_commit(&trans);
+ if (ret < 0) {
+ i_fatal("Failed to save mail: %s",
+ mailbox_get_last_internal_error(box, NULL));
+ }
+ if (mailbox_sync(box, 0) < 0)
+ i_fatal("Failed to sync mailbox: %s",
+ mailbox_get_last_internal_error(box, NULL));
+}
+
+static void test_mail_remove_keywords(struct mailbox *box)
+{
+ struct mailbox_transaction_context *trans;
+ const char *keywords[] = { NULL };
+ struct mail_keywords *kw;
+ struct mail *mail;
+
+ trans = mailbox_transaction_begin(box, 0, __func__);
+ mail = mail_alloc(trans, 0, NULL);
+ mail_set_seq(mail, 1);
+ if (mailbox_keywords_create(box, keywords, &kw) < 0)
+ i_fatal("mailbox_keywords_create() failed: %s",
+ mailbox_get_last_internal_error(box, NULL));
+ mail_update_keywords(mail, MODIFY_REPLACE, kw);
+ mailbox_keywords_unref(&kw);
+ mail_free(&mail);
+ if (mailbox_transaction_commit(&trans) < 0) {
+ i_fatal("Failed to update flags: %s",
+ mailbox_get_last_internal_error(box, NULL));
+ }
+ if (mailbox_sync(box, 0) < 0)
+ i_fatal("Failed to sync mailbox: %s",
+ mailbox_get_last_internal_error(box, NULL));
+}
+
+static struct mailbox_header_lookup_ctx *
+test_mail_fetch_get_random_headers(struct mailbox *box)
+{
+ ARRAY_TYPE(const_string) headers_arr;
+ const char *potential_headers[] = {
+ "From", "To", "Subject", "Nonexistent",
+ };
+
+ t_array_init(&headers_arr, N_ELEMENTS(potential_headers)+1);
+ do {
+ for (unsigned int i = 0; i < N_ELEMENTS(potential_headers); i++) {
+ if (i_rand_limit(2) == 0)
+ array_push_back(&headers_arr, &potential_headers[i]);
+ }
+ } while (array_count(&headers_arr) == 0);
+
+ array_append_zero(&headers_arr);
+ return mailbox_header_lookup_init(box, array_idx(&headers_arr, 0));
+}
+
+static void
+test_mail_fetch_field(struct mail *mail, enum mail_fetch_field field)
+{
+ struct message_part *parts;
+ struct message_size hdr_size, body_size;
+ struct istream *input;
+ const char *str;
+ time_t t;
+ uoff_t size;
+ unsigned int lines;
+ bool binary;
+ int tz, ret = 0;
+
+ e_debug(test_event, "field=0x%x", field);
+ switch (field) {
+ case MAIL_FETCH_FLAGS:
+ (void)mail_get_flags(mail);
+ (void)mail_get_keywords(mail);
+ break;
+ case MAIL_FETCH_MESSAGE_PARTS:
+ ret = mail_get_parts(mail, &parts);
+ break;
+ case MAIL_FETCH_STREAM_HEADER:
+ ret = mail_get_hdr_stream(mail,
+ i_rand_limit(2) == 0 ? &hdr_size : NULL, &input);
+ break;
+ case MAIL_FETCH_STREAM_BODY:
+ ret = mail_get_stream(mail,
+ i_rand_limit(2) == 0 ? &hdr_size : NULL,
+ i_rand_limit(2) == 0 ? &body_size : NULL, &input);
+ break;
+ case MAIL_FETCH_DATE:
+ ret = mail_get_date(mail, &t, &tz);
+ break;
+ case MAIL_FETCH_RECEIVED_DATE:
+ ret = mail_get_received_date(mail, &t);
+ break;
+ case MAIL_FETCH_SAVE_DATE:
+ ret = mail_get_save_date(mail, &t);
+ break;
+ case MAIL_FETCH_PHYSICAL_SIZE:
+ ret = mail_get_physical_size(mail, &size);
+ break;
+ case MAIL_FETCH_VIRTUAL_SIZE:
+ ret = mail_get_virtual_size(mail, &size);
+ break;
+ case MAIL_FETCH_NUL_STATE:
+ /* nothing to do */
+ break;
+ case MAIL_FETCH_STREAM_BINARY:
+ if ((ret = mail_get_parts(mail, &parts)) < 0)
+ break;
+
+ if (i_rand_limit(2) == 0) {
+ ret = mail_get_binary_stream(mail, parts,
+ i_rand_limit(2) == 0,
+ &size, &binary, &input);
+ if (ret == 0)
+ i_stream_unref(&input);
+ } else {
+ ret = mail_get_binary_size(mail, parts,
+ i_rand_limit(2) == 0,
+ &size, &lines);
+ }
+ break;
+ case MAIL_FETCH_IMAP_BODY:
+ case MAIL_FETCH_IMAP_BODYSTRUCTURE:
+ case MAIL_FETCH_IMAP_ENVELOPE:
+ case MAIL_FETCH_FROM_ENVELOPE:
+ case MAIL_FETCH_HEADER_MD5:
+ case MAIL_FETCH_STORAGE_ID:
+ case MAIL_FETCH_UIDL_BACKEND:
+ case MAIL_FETCH_MAILBOX_NAME:
+ case MAIL_FETCH_SEARCH_RELEVANCY:
+ case MAIL_FETCH_GUID:
+ case MAIL_FETCH_POP3_ORDER:
+ case MAIL_FETCH_REFCOUNT:
+ case MAIL_FETCH_BODY_SNIPPET:
+ case MAIL_FETCH_REFCOUNT_ID:
+ ret = mail_get_special(mail, field, &str);
+ break;
+ }
+ if (ret < 0) {
+ const char *errstr;
+ enum mail_error error;
+
+ errstr = mailbox_get_last_internal_error(mail->box, &error);
+ if (error != MAIL_ERROR_LOOKUP_ABORTED)
+ i_error("Failed to fetch field 0x%x: %s", field, errstr);
+ }
+}
+
+static void
+test_mail_fetch_headers(struct mail *mail,
+ struct mailbox_header_lookup_ctx *headers)
+{
+ struct istream *input;
+
+ e_debug(test_event, "header fields");
+ if (mail_get_header_stream(mail, headers, &input) < 0) {
+ const char *errstr;
+ enum mail_error error;
+
+ errstr = mailbox_get_last_internal_error(mail->box, &error);
+ if (error != MAIL_ERROR_LOOKUP_ABORTED)
+ i_error("Failed to fetch headers: %s", errstr);
+ return;
+ }
+ while (i_stream_read(input) > 0)
+ i_stream_skip(input, i_stream_get_data_size(input));
+}
+
+static void test_mail_random_fetch(struct mailbox *box, uint32_t seq)
+{
+ const enum mail_fetch_field potential_fields[] = {
+ MAIL_FETCH_FLAGS,
+ MAIL_FETCH_MESSAGE_PARTS,
+ MAIL_FETCH_STREAM_HEADER,
+ MAIL_FETCH_STREAM_BODY,
+ MAIL_FETCH_DATE,
+ MAIL_FETCH_RECEIVED_DATE,
+ MAIL_FETCH_SAVE_DATE,
+ MAIL_FETCH_PHYSICAL_SIZE,
+ MAIL_FETCH_VIRTUAL_SIZE,
+ MAIL_FETCH_NUL_STATE,
+ MAIL_FETCH_STREAM_BINARY,
+ MAIL_FETCH_IMAP_BODY,
+ MAIL_FETCH_IMAP_BODYSTRUCTURE,
+ MAIL_FETCH_IMAP_ENVELOPE,
+ MAIL_FETCH_FROM_ENVELOPE,
+ MAIL_FETCH_HEADER_MD5,
+ MAIL_FETCH_STORAGE_ID,
+ MAIL_FETCH_UIDL_BACKEND,
+ MAIL_FETCH_MAILBOX_NAME,
+ MAIL_FETCH_SEARCH_RELEVANCY,
+ MAIL_FETCH_GUID,
+ MAIL_FETCH_POP3_ORDER,
+ MAIL_FETCH_REFCOUNT,
+ MAIL_FETCH_BODY_SNIPPET,
+ MAIL_FETCH_REFCOUNT_ID,
+ };
+ struct mailbox_transaction_context *trans;
+ struct mail *mail;
+ enum mail_fetch_field wanted_fields = 0;
+ struct mailbox_header_lookup_ctx *headers = NULL;
+
+ if (i_rand_limit(2) == 0)
+ wanted_fields = i_rand();
+ if (i_rand_limit(2) == 0)
+ headers = test_mail_fetch_get_random_headers(box);
+ trans = mailbox_transaction_begin(box, 0, __func__);
+ e_debug(test_event, "wanted_fields=%u wanted_headers=%p", wanted_fields, headers);
+ mail = mail_alloc(trans, wanted_fields, headers);
+ mailbox_header_lookup_unref(&headers);
+ mail_set_seq(mail, seq);
+
+ for (unsigned int i = 0; i < 5; i++) {
+ unsigned int fetch_field_idx =
+ i_rand_limit(N_ELEMENTS(potential_fields) + 1);
+
+ if (i_rand_limit(3) == 0)
+ mail->lookup_abort = 1+i_rand_limit(MAIL_LOOKUP_ABORT_NOT_IN_CACHE_START_CACHING);
+ if (fetch_field_idx < N_ELEMENTS(potential_fields)) {
+ test_mail_fetch_field(mail,
+ potential_fields[fetch_field_idx]);
+ } else {
+ headers = test_mail_fetch_get_random_headers(box);
+ test_mail_fetch_headers(mail, headers);
+ mailbox_header_lookup_unref(&headers);
+ }
+ mail->lookup_abort = MAIL_LOOKUP_ABORT_NEVER;
+ }
+ mail_free(&mail);
+ if (mailbox_transaction_commit(&trans) < 0) {
+ i_fatal("Failed to commit transaction: %s",
+ mailbox_get_last_internal_error(box, NULL));
+ }
+}
+
+static void test_mail_random_access(void)
+{
+ struct test_mail_storage_ctx *ctx;
+ const char *const potential_never_cache_fields[] = {
+ "",
+ "flags",
+ "mime.parts",
+ "imap.body",
+ "imap.bodystructure",
+ };
+ unsigned int never_cache_field_idx =
+ i_rand_limit(N_ELEMENTS(potential_never_cache_fields));
+ const char *never_cache_fields =
+ potential_never_cache_fields[never_cache_field_idx];
+ struct test_mail_storage_settings set = {
+ .driver = "sdbox",
+ .extra_input = (const char *const[]) {
+ "mail_attachment_detection_options=add-flags",
+ t_strconcat("mail_never_cache_fields=",
+ never_cache_fields, NULL),
+ NULL
+ },
+ };
+ struct mailbox *box;
+
+ test_begin("mail");
+ ctx = test_mail_storage_init();
+ test_mail_storage_init_user(ctx, &set);
+ e_debug(test_event, "mail_never_cache_fields=%s", never_cache_fields);
+ for (unsigned int i = 0; i < 20; i++) {
+ box = mailbox_alloc(ctx->user->namespaces->list, "INBOX", 0);
+ if (mailbox_open(box) < 0)
+ i_fatal("Failed to open mailbox: %s",
+ mailbox_get_last_internal_error(box, NULL));
+ test_mail_save(box,
+ "From: <test1@example.com>\n"
+ "To: <test1-dest@example.com>\n"
+ "Subject: test subject\n"
+ "\n"
+ "test body\n");
+ test_mail_remove_keywords(box);
+ e_debug(test_event, "--------------");
+ for (unsigned int j = 0; j < 3; j++)
+ test_mail_random_fetch(box, 1);
+ if (mailbox_delete(box) < 0)
+ i_fatal("Failed to delete mailbox: %s",
+ mailbox_get_last_internal_error(box, NULL));
+ mailbox_free(&box);
+ }
+ test_mail_storage_deinit_user(ctx);
+ test_mail_storage_deinit(&ctx);
+ test_end();
+}
+
+static void test_attachment_flags_during_header_fetch(void)
+{
+ struct test_mail_storage_ctx *ctx;
+ struct test_mail_storage_settings set = {
+ .driver = "sdbox",
+ .extra_input = (const char *const[]) {
+ "mail_attachment_detection_options=add-flags",
+ "mail_never_cache_fields=mime.parts",
+ NULL
+ },
+ };
+
+ test_begin("mail attachment flags during header fetch");
+ ctx = test_mail_storage_init();
+ test_mail_storage_init_user(ctx, &set);
+
+ struct mailbox *box =
+ mailbox_alloc(ctx->user->namespaces->list, "INBOX", 0);
+ test_assert(mailbox_open(box) == 0);
+
+#define TEST_HDR_FROM "From: <test1@example.com>\r\n"
+ test_mail_save(box,
+ TEST_HDR_FROM
+ "\r\n"
+ "test body\n");
+ /* Remove the $HasNoAttachment keyword */
+ test_mail_remove_keywords(box);
+
+ struct mailbox_transaction_context *trans =
+ mailbox_transaction_begin(box, 0, __func__);
+ struct mail *mail = mail_alloc(trans, 0, NULL);
+ mail_set_seq(mail, 1);
+
+ const char *from_headers[] = { "From", NULL };
+ struct mailbox_header_lookup_ctx *headers =
+ mailbox_header_lookup_init(box, from_headers);
+
+ struct istream *input;
+ const unsigned char *data;
+ size_t size;
+ test_assert(mail_get_header_stream(mail, headers, &input) == 0);
+ test_assert(i_stream_read_more(input, &data, &size) == 1);
+ /* TEST_HDR_FROM */
+ test_assert(size == strlen(TEST_HDR_FROM) &&
+ memcmp(data, TEST_HDR_FROM, strlen(TEST_HDR_FROM)) == 0);
+ i_stream_skip(input, size);
+ test_assert(i_stream_read_more(input, &data, &size) == 1);
+ test_assert(size == 2 && memcmp(data, "\r\n", 2) == 0);
+ i_stream_skip(input, size);
+ test_assert(i_stream_read_more(input, &data, &size) == -1);
+
+ mailbox_header_lookup_unref(&headers);
+ mail_free(&mail);
+ test_assert(mailbox_transaction_commit(&trans) == 0);
+ mailbox_free(&box);
+ test_mail_storage_deinit_user(ctx);
+ test_mail_storage_deinit(&ctx);
+ test_end();
+}
+
+static void test_bodystructure_reparsing(void)
+{
+ struct test_mail_storage_ctx *ctx;
+ struct test_mail_storage_settings set = {
+ .driver = "sdbox",
+ .extra_input = (const char *const[]) {
+ "mail_attachment_detection_options=add-flags",
+ "mail_never_cache_fields=flags",
+ NULL
+ },
+ };
+ const char *value;
+
+ test_begin("mail bodystructure reparsing");
+ ctx = test_mail_storage_init();
+ test_mail_storage_init_user(ctx, &set);
+
+ struct mailbox *box =
+ mailbox_alloc(ctx->user->namespaces->list, "INBOX", 0);
+ test_assert(mailbox_open(box) == 0);
+
+ test_mail_save(box,
+ "From: <test1@example.com>\r\n"
+ "\r\n"
+ "test body\n");
+
+ struct mailbox_transaction_context *trans =
+ mailbox_transaction_begin(box, 0, __func__);
+ struct mail *mail = mail_alloc(trans, MAIL_FETCH_IMAP_BODYSTRUCTURE, NULL);
+ mail_set_seq(mail, 1);
+
+ /* start parsing header */
+ test_assert(mail_get_first_header(mail, "From", &value) == 1);
+ /* fetching snippet triggers re-parsing the header */
+ test_assert(mail_get_special(mail, MAIL_FETCH_BODY_SNIPPET, &value) == 0);
+
+ mail_free(&mail);
+ test_assert(mailbox_transaction_commit(&trans) == 0);
+ mailbox_free(&box);
+ test_mail_storage_deinit_user(ctx);
+ test_mail_storage_deinit(&ctx);
+ test_end();
+}
+
+static void test_bodystructure_corruption_reparsing(void)
+{
+ struct test_mail_storage_ctx *ctx;
+ struct test_mail_storage_settings set = {
+ .driver = "sdbox",
+ };
+ const char *value;
+
+ test_begin("bodystructure corruption reparsing");
+ ctx = test_mail_storage_init();
+ test_mail_storage_init_user(ctx, &set);
+
+ struct mailbox *box =
+ mailbox_alloc(ctx->user->namespaces->list, "INBOX", 0);
+ test_assert(mailbox_open(box) == 0);
+
+ test_mail_save(box,
+ "From: <test1@example.com>\r\n"
+ "\r\n"
+ "test body\n");
+
+ struct mailbox_transaction_context *trans =
+ mailbox_transaction_begin(box, 0, __func__);
+ struct mail *mail = mail_alloc(trans, 0, NULL);
+ mail_set_seq(mail, 1);
+
+ test_assert(mail_get_special(mail, MAIL_FETCH_IMAP_BODY, &value) == 0);
+ test_expect_error_string("Mailbox INBOX: Deleting corrupted cache record uid=1: UID 1: Broken MIME parts in mailbox INBOX: test");
+ mail_set_cache_corrupted(mail, MAIL_FETCH_MESSAGE_PARTS, "test");
+ test_expect_no_more_errors();
+ test_assert(mail_get_special(mail, MAIL_FETCH_IMAP_BODYSTRUCTURE, &value) == 0);
+
+ mail_free(&mail);
+ test_assert(mailbox_transaction_commit(&trans) == 0);
+ mailbox_free(&box);
+ test_mail_storage_deinit_user(ctx);
+ test_mail_storage_deinit(&ctx);
+ test_end();
+}
+
+int main(int argc, char **argv)
+{
+ void (*const tests[])(void) = {
+ test_mail_random_access,
+ test_attachment_flags_during_header_fetch,
+ test_bodystructure_reparsing,
+ test_bodystructure_corruption_reparsing,
+ NULL
+ };
+ int ret;
+
+ master_service = master_service_init("test-mail",
+ MASTER_SERVICE_FLAG_STANDALONE |
+ MASTER_SERVICE_FLAG_DONT_SEND_STATS |
+ MASTER_SERVICE_FLAG_NO_CONFIG_SETTINGS |
+ MASTER_SERVICE_FLAG_NO_SSL_INIT |
+ MASTER_SERVICE_FLAG_NO_INIT_DATASTACK_FRAME,
+ &argc, &argv, "");
+
+ test_event = event_create(NULL);
+ if (null_strcmp(argv[1], "-D") == 0)
+ event_set_forced_debug(test_event, TRUE);
+ ret = test_run(tests);
+ event_unref(&test_event);
+ master_service_deinit(&master_service);
+ return ret;
+}
diff --git a/src/lib-storage/test-mailbox-get.c b/src/lib-storage/test-mailbox-get.c
new file mode 100644
index 0000000..42d5a32
--- /dev/null
+++ b/src/lib-storage/test-mailbox-get.c
@@ -0,0 +1,162 @@
+/* Copyright (c) 2009-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "test-common.h"
+#include "mail-index-modseq.h"
+#include "mail-storage-private.h"
+
+static uint32_t expunge_uids[] = { 25, 15, 7, 3, 11, 1, 53, 33 };
+static guid_128_t mail_guids[N_ELEMENTS(expunge_uids)];
+static unsigned int expunge_idx;
+static unsigned int nonexternal_idx;
+
+uint32_t mail_index_view_get_messages_count(struct mail_index_view *view ATTR_UNUSED)
+{
+ return 0;
+}
+
+void mail_index_lookup_uid(struct mail_index_view *view ATTR_UNUSED,
+ uint32_t seq, uint32_t *uid_r)
+{
+ *uid_r = seq;
+}
+
+bool mail_index_lookup_seq_range(struct mail_index_view *view ATTR_UNUSED,
+ uint32_t first_uid, uint32_t last_uid,
+ uint32_t *first_seq_r, uint32_t *last_seq_r)
+{
+ *first_seq_r = first_uid;
+ *last_seq_r = last_uid;
+ return TRUE;
+}
+
+bool mail_index_modseq_get_next_log_offset(struct mail_index_view *view ATTR_UNUSED,
+ uint64_t modseq, uint32_t *log_seq_r,
+ uoff_t *log_offset_r)
+{
+ *log_seq_r = modseq >> 32;
+ *log_offset_r = modseq & 0xfffffff;
+ return TRUE;
+}
+
+struct mail_transaction_log_view *
+mail_transaction_log_view_open(struct mail_transaction_log *log ATTR_UNUSED) { return NULL; }
+int mail_transaction_log_view_set(struct mail_transaction_log_view *view ATTR_UNUSED,
+ uint32_t min_file_seq ATTR_UNUSED, uoff_t min_file_offset ATTR_UNUSED,
+ uint32_t max_file_seq ATTR_UNUSED, uoff_t max_file_offset ATTR_UNUSED,
+ bool *reset_r ATTR_UNUSED, const char **reason_r ATTR_UNUSED) {
+ if (min_file_seq < 99)
+ return 0;
+ return 1;
+}
+
+void mail_transaction_log_view_close(struct mail_transaction_log_view **view ATTR_UNUSED) { }
+
+void mail_transaction_log_get_tail(struct mail_transaction_log *log ATTR_UNUSED,
+ uint32_t *file_seq_r)
+{
+ *file_seq_r = 100;
+}
+
+int mail_transaction_log_view_next(struct mail_transaction_log_view *view ATTR_UNUSED,
+ const struct mail_transaction_header **hdr_r,
+ const void **data_r)
+{
+ static struct mail_transaction_header hdr;
+ static struct mail_transaction_expunge_guid exp;
+ static struct mail_transaction_expunge old_exp;
+
+ if (expunge_idx == N_ELEMENTS(expunge_uids))
+ return 0;
+
+ if (mail_guids[expunge_idx][0] == 0) {
+ old_exp.uid1 = old_exp.uid2 = expunge_uids[expunge_idx];
+ hdr.type = MAIL_TRANSACTION_EXPUNGE;
+ hdr.size = sizeof(old_exp);
+ *data_r = &old_exp;
+ } else {
+ exp.uid = expunge_uids[expunge_idx];
+ memcpy(exp.guid_128, mail_guids[expunge_idx], sizeof(exp.guid_128));
+ hdr.type = MAIL_TRANSACTION_EXPUNGE_GUID;
+ hdr.size = sizeof(exp);
+ *data_r = &exp;
+ }
+ if (expunge_idx != nonexternal_idx)
+ hdr.type |= MAIL_TRANSACTION_EXTERNAL;
+
+ *hdr_r = &hdr;
+ expunge_idx++;
+ return 1;
+}
+
+void mail_transaction_log_view_mark(struct mail_transaction_log_view *view ATTR_UNUSED)
+{
+}
+
+void mail_transaction_log_view_rewind(struct mail_transaction_log_view *view ATTR_UNUSED)
+{
+ expunge_idx = 0;
+}
+
+static void test_mailbox_get_expunges(void)
+{
+ struct mailbox *box;
+ ARRAY_TYPE(seq_range) uids_filter;
+ ARRAY_TYPE(mailbox_expunge_rec) expunges;
+ const struct mailbox_expunge_rec *exp;
+ unsigned int count;
+ uint64_t modseq;
+
+ box = t_new(struct mailbox, 1);
+ box->index = t_new(struct mail_index, 1);
+ box->view = t_new(struct mail_index_view, 1);
+
+ box->view->log_file_head_seq = 101;
+ box->view->log_file_head_offset = 1024;
+
+ test_begin("mailbox get expunges");
+
+ nonexternal_idx = 1;
+ memset(mail_guids + 2, 0, GUID_128_SIZE);
+ memset(mail_guids + 4, 0, GUID_128_SIZE);
+
+ t_array_init(&uids_filter, 32);
+ seq_range_array_add_range(&uids_filter, 1, 20);
+ seq_range_array_add_range(&uids_filter, 53, 53);
+
+ t_array_init(&expunges, 32);
+ modseq = 98ULL << 32;
+ test_assert(!mailbox_get_expunges(box, modseq, &uids_filter,
+ &expunges));
+
+ exp = array_get(&expunges, &count);
+ test_assert(count == 5);
+ test_assert(exp[0].uid == 3);
+ test_assert(memcmp(exp[0].guid_128, mail_guids[3], GUID_128_SIZE) == 0);
+ test_assert(exp[1].uid == 1);
+ test_assert(memcmp(exp[1].guid_128, mail_guids[5], GUID_128_SIZE) == 0);
+ test_assert(exp[2].uid == 53);
+ test_assert(memcmp(exp[2].guid_128, mail_guids[6], GUID_128_SIZE) == 0);
+ test_assert(exp[3].uid == 7);
+ test_assert(memcmp(exp[3].guid_128, mail_guids[2], GUID_128_SIZE) == 0);
+ test_assert(exp[4].uid == 11);
+ test_assert(memcmp(exp[4].guid_128, mail_guids[4], GUID_128_SIZE) == 0);
+
+ test_end();
+}
+
+int main(void)
+{
+ static void (*const test_functions[])(void) = {
+ test_mailbox_get_expunges,
+ NULL
+ };
+ unsigned int i, j;
+
+ for (i = 0; i < N_ELEMENTS(mail_guids); i++) {
+ for (j = 0; j < GUID_128_SIZE; j++)
+ mail_guids[i][j] = j + i + 1;
+ }
+ return test_run(test_functions);
+}
diff --git a/src/lib-storage/test-mailbox-list.c b/src/lib-storage/test-mailbox-list.c
new file mode 100644
index 0000000..aa8f62a
--- /dev/null
+++ b/src/lib-storage/test-mailbox-list.c
@@ -0,0 +1,512 @@
+/* Copyright (c) 2019 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "str.h"
+#include "test-common.h"
+#include "mailbox-list-private.h"
+
+enum test_flags {
+ TEST_FLAG_NO_VNAME = BIT(0),
+ TEST_FLAG_NO_STORAGE_NAME = BIT(1),
+ TEST_FLAG_NO_MUTF7 = BIT(2),
+ TEST_FLAG_NO_UTF8 = BIT(3),
+};
+
+struct test_mailbox_list_name {
+ const char *vname;
+ const char *storage_name;
+ enum test_flags flags;
+ char *ns_prefix;
+ enum namespace_flags ns_flags;
+ char ns_sep;
+ char list_sep;
+ char vname_escape_char;
+ char storage_name_escape_char;
+ const char *maildir_name;
+};
+
+static char list_hierarchy_sep;
+static char ns_sep[2] = { '\0', '\0' };
+
+static void test_init_list(struct mailbox_list *list_r)
+{
+ i_zero(list_r);
+ list_r->ns = t_new(struct mail_namespace, 1);
+ list_r->ns->user = t_new(struct mail_user, 1);
+ list_r->ns->user->event = event_create(NULL);
+}
+
+static void test_deinit_list(struct mailbox_list *list)
+{
+ mailbox_list_clear_error(list);
+ if (array_is_created(&list->error_stack)) {
+ mailbox_list_clear_error(list);
+ i_assert(array_count(&list->error_stack) == 0);
+ array_free(&list->error_stack);
+ }
+ event_unref(&list->ns->user->event);
+}
+
+static void test_mailbox_list_errors(void)
+{
+ /* NOTE: keep in sync with test-mail-storage.c */
+ struct mailbox_list list;
+ enum mail_error mail_error;
+ const char *errstr;
+
+ test_begin("mail list errors");
+ test_init_list(&list);
+
+ /* try a regular error */
+ mailbox_list_set_error(&list, MAIL_ERROR_PERM, "error1");
+ test_assert(strcmp(mailbox_list_get_last_error(&list, &mail_error),
+ "error1") == 0);
+ test_assert(mail_error == MAIL_ERROR_PERM);
+ test_assert(strcmp(mailbox_list_get_last_internal_error(&list, &mail_error),
+ "error1") == 0);
+ test_assert(mail_error == MAIL_ERROR_PERM);
+ test_assert(!list.last_error_is_internal);
+
+ /* set the error to itself */
+ mailbox_list_set_error(&list, MAIL_ERROR_PARAMS,
+ mailbox_list_get_last_error(&list, &mail_error));
+ test_assert(strcmp(mailbox_list_get_last_error(&list, &mail_error),
+ "error1") == 0);
+ test_assert(mail_error == MAIL_ERROR_PARAMS);
+ test_assert(strcmp(mailbox_list_get_last_internal_error(&list, &mail_error),
+ "error1") == 0);
+ test_assert(mail_error == MAIL_ERROR_PARAMS);
+ test_assert(!list.last_error_is_internal);
+
+ /* clear the error - asking for it afterwards is a bug */
+ mailbox_list_clear_error(&list);
+ test_assert(strcmp(mailbox_list_get_last_error(&list, &mail_error),
+ "BUG: Unknown internal list error") == 0);
+ test_assert(mail_error == MAIL_ERROR_TEMP);
+ test_assert(strcmp(mailbox_list_get_last_internal_error(&list, &mail_error),
+ "BUG: Unknown internal list error") == 0);
+ test_assert(mail_error == MAIL_ERROR_TEMP);
+ test_assert(!list.last_error_is_internal);
+
+ /* set internal error in preparation for the next test */
+ test_expect_error_string("critical0");
+ mailbox_list_set_critical(&list, "critical0");
+ test_expect_no_more_errors();
+ test_assert(strstr(mailbox_list_get_last_error(&list, &mail_error),
+ MAIL_ERRSTR_CRITICAL_MSG) != NULL);
+ test_assert(mail_error == MAIL_ERROR_TEMP);
+ test_assert(strcmp(mailbox_list_get_last_internal_error(&list, &mail_error),
+ "critical0") == 0);
+ test_assert(mail_error == MAIL_ERROR_TEMP);
+ test_assert(list.last_error_is_internal);
+
+ /* internal error without specifying what it is. this needs to clear
+ the previous internal error. */
+ mailbox_list_set_internal_error(&list);
+ test_assert(strstr(mailbox_list_get_last_error(&list, &mail_error),
+ MAIL_ERRSTR_CRITICAL_MSG) != NULL);
+ test_assert(mail_error == MAIL_ERROR_TEMP);
+ test_assert(strstr(mailbox_list_get_last_internal_error(&list, &mail_error),
+ MAIL_ERRSTR_CRITICAL_MSG) != NULL);
+ test_assert(mail_error == MAIL_ERROR_TEMP);
+ test_assert(!list.last_error_is_internal);
+
+ /* proper internal error */
+ test_expect_error_string("critical1");
+ mailbox_list_set_critical(&list, "critical1");
+ test_expect_no_more_errors();
+ test_assert(strstr(mailbox_list_get_last_error(&list, &mail_error),
+ MAIL_ERRSTR_CRITICAL_MSG) != NULL);
+ test_assert(mail_error == MAIL_ERROR_TEMP);
+ test_assert(strcmp(mailbox_list_get_last_internal_error(&list, &mail_error),
+ "critical1") == 0);
+ test_assert(mail_error == MAIL_ERROR_TEMP);
+ test_assert(list.last_error_is_internal);
+
+ /* use it in the following internal error */
+ test_expect_error_string("critical2: critical1");
+ mailbox_list_set_critical(&list, "critical2: %s",
+ mailbox_list_get_last_internal_error(&list, &mail_error));
+ test_expect_no_more_errors();
+ test_assert(strstr(mailbox_list_get_last_error(&list, &mail_error),
+ MAIL_ERRSTR_CRITICAL_MSG) != NULL);
+ test_assert(mail_error == MAIL_ERROR_TEMP);
+ test_assert(strcmp(mailbox_list_get_last_internal_error(&list, &mail_error),
+ "critical2: critical1") == 0);
+ test_assert(mail_error == MAIL_ERROR_TEMP);
+ test_assert(list.last_error_is_internal);
+
+ /* use the previous non-internal error as part of the internal error */
+ test_expect_error_string("critical3: "MAIL_ERRSTR_CRITICAL_MSG);
+ mailbox_list_set_critical(&list, "critical3: %s",
+ mailbox_list_get_last_error(&list, &mail_error));
+ test_expect_no_more_errors();
+ test_assert(strstr(mailbox_list_get_last_error(&list, &mail_error),
+ MAIL_ERRSTR_CRITICAL_MSG) != NULL);
+ test_assert(mail_error == MAIL_ERROR_TEMP);
+ errstr = mailbox_list_get_last_internal_error(&list, &mail_error);
+ test_assert(str_begins(errstr, "critical3: "));
+ test_assert(strstr(errstr+11, MAIL_ERRSTR_CRITICAL_MSG) != NULL);
+ test_assert(mail_error == MAIL_ERROR_TEMP);
+ test_assert(list.last_error_is_internal);
+
+ /* clear the error again and check that all is as expected */
+ mailbox_list_clear_error(&list);
+ test_assert(strcmp(mailbox_list_get_last_error(&list, &mail_error),
+ "BUG: Unknown internal list error") == 0);
+ test_assert(mail_error == MAIL_ERROR_TEMP);
+ test_assert(strcmp(mailbox_list_get_last_internal_error(&list, &mail_error),
+ "BUG: Unknown internal list error") == 0);
+ test_assert(mail_error == MAIL_ERROR_TEMP);
+ test_assert(!list.last_error_is_internal);
+
+ /* use internal error as a regular error (although that really
+ shouldn't be done) */
+ test_expect_error_string("critical4");
+ mailbox_list_set_critical(&list, "critical4");
+ mailbox_list_set_error(&list, MAIL_ERROR_PARAMS,
+ mailbox_list_get_last_internal_error(&list, &mail_error));
+ test_expect_no_more_errors();
+ test_assert(strcmp(mailbox_list_get_last_error(&list, &mail_error),
+ "critical4") == 0);
+ test_assert(mail_error == MAIL_ERROR_PARAMS);
+ test_assert(strcmp(mailbox_list_get_last_internal_error(&list, &mail_error),
+ "critical4") == 0);
+ test_assert(mail_error == MAIL_ERROR_PARAMS);
+ test_assert(!list.last_error_is_internal);
+
+ test_deinit_list(&list);
+ test_end();
+}
+
+static void test_mailbox_list_last_error_push_pop(void)
+{
+ /* NOTE: keep in sync with test-mail-storage.c */
+ struct mailbox_list list;
+ enum mail_error mail_error;
+
+ test_begin("mailbox_list_last_error_push/pop()");
+ test_init_list(&list);
+
+ /* regular error 1 */
+ mailbox_list_set_error(&list, MAIL_ERROR_PERM, "regular error 1");
+ mailbox_list_last_error_push(&list);
+
+ /* critical error 1 */
+ test_expect_error_string("critical error 1");
+ mailbox_list_set_critical(&list, "critical error 1");
+ test_expect_no_more_errors();
+ mailbox_list_last_error_push(&list);
+
+ /* regular error 2 */
+ mailbox_list_set_error(&list, MAIL_ERROR_PARAMS, "regular error 2");
+ mailbox_list_last_error_push(&list);
+
+ /* critical error 2 */
+ test_expect_error_string("critical error 2");
+ mailbox_list_set_critical(&list, "critical error 2");
+ test_expect_no_more_errors();
+ mailbox_list_last_error_push(&list);
+
+ /* -- clear all errors -- */
+ mailbox_list_clear_error(&list);
+
+ /* critical error 2 pop */
+ mailbox_list_last_error_pop(&list);
+ test_assert(strstr(mailbox_list_get_last_error(&list, &mail_error),
+ MAIL_ERRSTR_CRITICAL_MSG) != NULL);
+ test_assert(mail_error == MAIL_ERROR_TEMP);
+ test_assert(strcmp(mailbox_list_get_last_internal_error(&list, &mail_error),
+ "critical error 2") == 0);
+ test_assert(mail_error == MAIL_ERROR_TEMP);
+ test_assert(list.last_error_is_internal);
+
+ /* regular error 2 pop */
+ mailbox_list_last_error_pop(&list);
+ test_assert(strcmp(mailbox_list_get_last_error(&list, &mail_error),
+ "regular error 2") == 0);
+ test_assert(mail_error == MAIL_ERROR_PARAMS);
+ test_assert(strcmp(mailbox_list_get_last_internal_error(&list, &mail_error),
+ "regular error 2") == 0);
+ test_assert(mail_error == MAIL_ERROR_PARAMS);
+ test_assert(!list.last_error_is_internal);
+
+ /* critical error 1 pop */
+ mailbox_list_last_error_pop(&list);
+ test_assert(strstr(mailbox_list_get_last_error(&list, &mail_error),
+ MAIL_ERRSTR_CRITICAL_MSG) != NULL);
+ test_assert(mail_error == MAIL_ERROR_TEMP);
+ test_assert(strcmp(mailbox_list_get_last_internal_error(&list, &mail_error),
+ "critical error 1") == 0);
+ test_assert(mail_error == MAIL_ERROR_TEMP);
+ test_assert(list.last_error_is_internal);
+
+ /* regular error 1 pop */
+ mailbox_list_last_error_pop(&list);
+ test_assert(strcmp(mailbox_list_get_last_error(&list, &mail_error),
+ "regular error 1") == 0);
+ test_assert(mail_error == MAIL_ERROR_PERM);
+ test_assert(strcmp(mailbox_list_get_last_internal_error(&list, &mail_error),
+ "regular error 1") == 0);
+ test_assert(mail_error == MAIL_ERROR_PERM);
+ test_assert(!list.last_error_is_internal);
+
+ test_deinit_list(&list);
+ test_end();
+}
+
+static char
+test_mailbox_list_get_hierarchy_sep(struct mailbox_list *list ATTR_UNUSED)
+{
+ return list_hierarchy_sep;
+}
+
+static void
+test_maibox_list_name_init(struct mailbox_list *list,
+ const struct test_mailbox_list_name *test,
+ bool mutf7)
+{
+ list->ns->prefix = test->ns_prefix == NULL ? "" :
+ test->ns_prefix;
+ list->ns->prefix_len = strlen(list->ns->prefix);
+ list->ns->flags = test->ns_flags;
+ ns_sep[0] = test->ns_sep;
+ list_hierarchy_sep = test->list_sep;
+ list->set.utf8 = !mutf7;
+ list->set.vname_escape_char = test->vname_escape_char;
+ list->set.storage_name_escape_char =
+ test->storage_name_escape_char;
+ list->set.maildir_name = test->maildir_name == NULL ? "" :
+ test->maildir_name;
+}
+
+static void test_mailbox_list_get_names(void)
+{
+ const struct test_mailbox_list_name tests[] = {
+ { .vname = "parent/child",
+ .storage_name = "parent/child",
+ .ns_sep = '/', .list_sep = '/' },
+ { .vname = "parent/child",
+ .storage_name = "parent.child",
+ .ns_sep = '/', .list_sep = '.' },
+ { .vname = "ns_prefix/parent/child",
+ .storage_name = "parent.child",
+ .ns_prefix = "ns_prefix/", .ns_sep = '/', .list_sep = '.' },
+ { .vname = "ns/prefix/parent/child",
+ .storage_name = "parent.child",
+ .ns_prefix = "ns/prefix/", .ns_sep = '/', .list_sep = '.' },
+ { .vname = "ns/prefix",
+ .storage_name = "",
+ .ns_prefix = "ns/prefix/", .ns_sep = '/', .list_sep = '.' },
+ { .vname = "\xC3\xA4/\xC3\xB6",
+ .storage_name = "&APY-",
+ .flags = TEST_FLAG_NO_UTF8,
+ .ns_prefix = "\xC3\xA4/", .ns_sep = '/', .list_sep = '.' },
+ { .vname = "\xC3\xA4/\xC3\xB6&test",
+ .storage_name = "\xC3\xB6&test",
+ .flags = TEST_FLAG_NO_MUTF7,
+ .ns_prefix = "\xC3\xA4/", .ns_sep = '/', .list_sep = '.' },
+
+ /* storage_name escaping: */
+ { .vname = "~home",
+ .storage_name = "%7ehome",
+ .ns_sep = '/', .list_sep = '.',
+ .storage_name_escape_char = '%' },
+ { .vname = "es%cape%",
+ .storage_name = "es%25cape%25",
+ .ns_sep = '/', .list_sep = '.',
+ .storage_name_escape_char = '%' },
+ { .vname = "slash/",
+ .storage_name = "slash%2f",
+ .ns_sep = '^', .list_sep = '.',
+ .storage_name_escape_char = '%' },
+ { .vname = "list.separator",
+ .storage_name = "list%2eseparator",
+ .ns_sep = '/', .list_sep = '.',
+ .storage_name_escape_char = '%' },
+ { .vname = "Maildir",
+ .storage_name = "%4daildir",
+ .ns_sep = '^', .list_sep = '.',
+ .storage_name_escape_char = '%',
+ .maildir_name = "Maildir" },
+ { .vname = "~Maildir",
+ .storage_name = "%7eMaildir",
+ .ns_sep = '^', .list_sep = '.',
+ .storage_name_escape_char = '%',
+ .maildir_name = "Maildir" },
+ { .vname = "Maildir/suffix",
+ .storage_name = "%4daildir%2fsuffix",
+ .ns_sep = '^', .list_sep = '.',
+ .storage_name_escape_char = '%',
+ .maildir_name = "Maildir" },
+ { .vname = "prefix/Maildir",
+ .storage_name = "prefix%2f%4daildir",
+ .ns_sep = '^', .list_sep = '.',
+ .storage_name_escape_char = '%',
+ .maildir_name = "Maildir" },
+ { .vname = "sep/Maildir/sep",
+ .storage_name = "sep.%4daildir.sep",
+ .ns_sep = '/', .list_sep = '.',
+ .storage_name_escape_char = '%',
+ .maildir_name = "Maildir" },
+ { .vname = "~/.%--Maildir",
+ .storage_name = "%4daildir",
+ .ns_prefix = "~/.%--",
+ .ns_sep = '/', .list_sep = '.',
+ .storage_name_escape_char = '%',
+ .maildir_name = "Maildir" },
+ { .vname = "%foo",
+ .storage_name = "%foo",
+ .flags = TEST_FLAG_NO_STORAGE_NAME,
+ .ns_sep = '/', .list_sep = '.',
+ .storage_name_escape_char = '%' },
+ { .vname = "A/B.C%D",
+ .storage_name = "A.B%2eC%25D",
+ .storage_name_escape_char='%',
+ .ns_sep = '/', .list_sep = '.' },
+
+ /* vname escaping: */
+ { .vname = "%7c|child",
+ .storage_name = "|.child",
+ .ns_sep = '|', .list_sep = '.',
+ .vname_escape_char = '%' },
+ { .vname = "%7c|child.",
+ .storage_name = "|.child+2e",
+ .ns_sep = '|', .list_sep = '.',
+ .storage_name_escape_char = '+',
+ .vname_escape_char = '%' },
+ { .vname = "%7c|child.",
+ .storage_name = "|.child%2e",
+ .ns_sep = '|', .list_sep = '.',
+ .storage_name_escape_char = '%',
+ .vname_escape_char = '%' },
+ { .vname = "%2f/child",
+ .storage_name = "/.child",
+ .ns_sep = '/', .list_sep = '.',
+ .vname_escape_char = '%' },
+ { .vname = "%2f/child.",
+ .storage_name = "+2f.child+2e",
+ .ns_sep = '/', .list_sep = '.',
+ .storage_name_escape_char = '+',
+ .vname_escape_char = '%' },
+ { .vname = "%2f/child.",
+ .storage_name = "%2f.child%2e",
+ .ns_sep = '/', .list_sep = '.',
+ .storage_name_escape_char = '%',
+ .vname_escape_char = '%' },
+ { .vname = "%25escaped+",
+ .storage_name = "%escaped+2b",
+ .ns_sep = '/', .list_sep = '.',
+ .storage_name_escape_char = '+',
+ .vname_escape_char = '%' },
+ { .vname = "x%2666-y",
+ .storage_name = "x&66-y",
+ .flags = TEST_FLAG_NO_UTF8,
+ .ns_sep = '/', .list_sep = '.',
+ .vname_escape_char = '%' },
+ { .vname = "p\xC3\xA4iv\xC3\xA4 & y%26APY %ff m\xC3\xB6h",
+ .storage_name = "p&AOQ-iv&AOQ- &- y&APY \xff m&APY-h",
+ .flags = TEST_FLAG_NO_UTF8,
+ .ns_sep = '/', .list_sep = '.',
+ .vname_escape_char = '%' },
+ { .vname = "%foo",
+ .storage_name = "%foo",
+ .flags = TEST_FLAG_NO_VNAME,
+ .ns_sep = '/', .list_sep = '.',
+ .vname_escape_char = '%' },
+ { .vname = "A%2fB/C%25D",
+ .storage_name = "A/B.C%D",
+ .ns_sep = '/', .list_sep = '.',
+ .vname_escape_char = '%' },
+
+ /* INBOX: */
+ { .vname = "inBox",
+ .storage_name = "inBox",
+ .ns_sep = '/', .list_sep = '.' },
+ { .vname = "inBox",
+ .storage_name = "INBOX",
+ .flags = TEST_FLAG_NO_VNAME,
+ .ns_flags = NAMESPACE_FLAG_INBOX_USER,
+ .ns_sep = '/', .list_sep = '.' },
+ { .vname = "inBox",
+ .storage_name = "INBOX",
+ .flags = TEST_FLAG_NO_VNAME,
+ .ns_flags = NAMESPACE_FLAG_INBOX_USER,
+ .ns_prefix = "prefix/", .ns_sep = '/', .list_sep = '.' },
+ { .vname = "prefix/inBox",
+ .storage_name = "inBox",
+ .ns_flags = NAMESPACE_FLAG_INBOX_USER,
+ .ns_prefix = "prefix/", .ns_sep = '/', .list_sep = '.' },
+ { .vname = "prefix/INBOX",
+ .storage_name = "+49NBOX",
+ .ns_flags = NAMESPACE_FLAG_INBOX_USER,
+ .ns_prefix = "prefix/", .ns_sep = '/', .list_sep = '.',
+ .storage_name_escape_char = '+' },
+
+ /* Problematic cases - not reversible: */
+ { .vname = "parent.child",
+ .storage_name = "parent.child",
+ .flags = TEST_FLAG_NO_VNAME,
+ .ns_sep = '/', .list_sep = '.' },
+ { .vname = "prefix/INBOX",
+ .storage_name = "INBOX",
+ .flags = TEST_FLAG_NO_VNAME,
+ .ns_flags = NAMESPACE_FLAG_INBOX_USER,
+ .ns_prefix = "prefix/", .ns_sep = '/', .list_sep = '.' },
+ { .vname = "invalid&mutf7",
+ .storage_name = "invalid&mutf7",
+ .flags = TEST_FLAG_NO_STORAGE_NAME,
+ .ns_sep = '/', .list_sep = '.' },
+ };
+ struct mail_namespace_settings ns_set = {
+ .separator = ns_sep,
+ };
+ struct mail_namespace ns = {
+ .set = &ns_set,
+ };
+ struct mailbox_list list = {
+ .ns = &ns,
+ .v = {
+ .get_hierarchy_sep = test_mailbox_list_get_hierarchy_sep,
+ },
+ };
+
+ test_begin("mailbox list get names");
+ for (unsigned int i = 0; i < N_ELEMENTS(tests); i++) {
+ if ((tests[i].flags & TEST_FLAG_NO_MUTF7) == 0) {
+ test_maibox_list_name_init(&list, &tests[i], TRUE);
+ if ((tests[i].flags & TEST_FLAG_NO_STORAGE_NAME) == 0) {
+ test_assert_strcmp_idx(mailbox_list_default_get_storage_name(&list, tests[i].vname),
+ tests[i].storage_name, i);
+ }
+ if ((tests[i].flags & TEST_FLAG_NO_VNAME) == 0) {
+ test_assert_strcmp_idx(mailbox_list_default_get_vname(&list, tests[i].storage_name),
+ tests[i].vname, i);
+ }
+ }
+ if ((tests[i].flags & TEST_FLAG_NO_UTF8) == 0) {
+ test_maibox_list_name_init(&list, &tests[i], FALSE);
+ if ((tests[i].flags & TEST_FLAG_NO_STORAGE_NAME) == 0) {
+ test_assert_strcmp_idx(mailbox_list_default_get_storage_name(&list, tests[i].vname),
+ tests[i].storage_name, i);
+ }
+ if ((tests[i].flags & TEST_FLAG_NO_VNAME) == 0) {
+ test_assert_strcmp_idx(mailbox_list_default_get_vname(&list, tests[i].storage_name),
+ tests[i].vname, i);
+ }
+ }
+ }
+ test_end();
+}
+
+int main(void)
+{
+ void (*const tests[])(void) = {
+ test_mailbox_list_errors,
+ test_mailbox_list_last_error_push_pop,
+ test_mailbox_list_get_names,
+ NULL
+ };
+ return test_run(tests);
+}
diff --git a/src/lib-test/Makefile.am b/src/lib-test/Makefile.am
new file mode 100644
index 0000000..ce7417d
--- /dev/null
+++ b/src/lib-test/Makefile.am
@@ -0,0 +1,20 @@
+noinst_LTLIBRARIES = libtest.la
+
+AM_CPPFLAGS = \
+ -I$(top_srcdir)/src/lib \
+ -I$(top_srcdir)/src/lib-charset
+
+libtest_la_SOURCES = \
+ fuzzer.c \
+ test-common.c \
+ test-istream.c \
+ test-ostream.c \
+ test-subprocess.c
+
+headers = \
+ fuzzer.h \
+ test-common.h \
+ test-subprocess.h
+
+pkginc_libdir=$(pkgincludedir)
+pkginc_lib_HEADERS = $(headers)
diff --git a/src/lib-test/Makefile.in b/src/lib-test/Makefile.in
new file mode 100644
index 0000000..db7e3a2
--- /dev/null
+++ b/src/lib-test/Makefile.in
@@ -0,0 +1,829 @@
+# Makefile.in generated by automake 1.16.1 from Makefile.am.
+# @configure_input@
+
+# Copyright (C) 1994-2018 Free Software Foundation, Inc.
+
+# This Makefile.in is free software; the Free Software Foundation
+# gives unlimited permission to copy and/or distribute it,
+# with or without modifications, as long as this notice is preserved.
+
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY, to the extent permitted by law; without
+# even the implied warranty of MERCHANTABILITY or FITNESS FOR A
+# PARTICULAR PURPOSE.
+
+@SET_MAKE@
+
+
+VPATH = @srcdir@
+am__is_gnu_make = { \
+ if test -z '$(MAKELEVEL)'; then \
+ false; \
+ elif test -n '$(MAKE_HOST)'; then \
+ true; \
+ elif test -n '$(MAKE_VERSION)' && test -n '$(CURDIR)'; then \
+ true; \
+ else \
+ false; \
+ fi; \
+}
+am__make_running_with_option = \
+ case $${target_option-} in \
+ ?) ;; \
+ *) echo "am__make_running_with_option: internal error: invalid" \
+ "target option '$${target_option-}' specified" >&2; \
+ exit 1;; \
+ esac; \
+ has_opt=no; \
+ sane_makeflags=$$MAKEFLAGS; \
+ if $(am__is_gnu_make); then \
+ sane_makeflags=$$MFLAGS; \
+ else \
+ case $$MAKEFLAGS in \
+ *\\[\ \ ]*) \
+ bs=\\; \
+ sane_makeflags=`printf '%s\n' "$$MAKEFLAGS" \
+ | sed "s/$$bs$$bs[$$bs $$bs ]*//g"`;; \
+ esac; \
+ fi; \
+ skip_next=no; \
+ strip_trailopt () \
+ { \
+ flg=`printf '%s\n' "$$flg" | sed "s/$$1.*$$//"`; \
+ }; \
+ for flg in $$sane_makeflags; do \
+ test $$skip_next = yes && { skip_next=no; continue; }; \
+ case $$flg in \
+ *=*|--*) continue;; \
+ -*I) strip_trailopt 'I'; skip_next=yes;; \
+ -*I?*) strip_trailopt 'I';; \
+ -*O) strip_trailopt 'O'; skip_next=yes;; \
+ -*O?*) strip_trailopt 'O';; \
+ -*l) strip_trailopt 'l'; skip_next=yes;; \
+ -*l?*) strip_trailopt 'l';; \
+ -[dEDm]) skip_next=yes;; \
+ -[JT]) skip_next=yes;; \
+ esac; \
+ case $$flg in \
+ *$$target_option*) has_opt=yes; break;; \
+ esac; \
+ done; \
+ test $$has_opt = yes
+am__make_dryrun = (target_option=n; $(am__make_running_with_option))
+am__make_keepgoing = (target_option=k; $(am__make_running_with_option))
+pkgdatadir = $(datadir)/@PACKAGE@
+pkgincludedir = $(includedir)/@PACKAGE@
+pkglibdir = $(libdir)/@PACKAGE@
+pkglibexecdir = $(libexecdir)/@PACKAGE@
+am__cd = CDPATH="$${ZSH_VERSION+.}$(PATH_SEPARATOR)" && cd
+install_sh_DATA = $(install_sh) -c -m 644
+install_sh_PROGRAM = $(install_sh) -c
+install_sh_SCRIPT = $(install_sh) -c
+INSTALL_HEADER = $(INSTALL_DATA)
+transform = $(program_transform_name)
+NORMAL_INSTALL = :
+PRE_INSTALL = :
+POST_INSTALL = :
+NORMAL_UNINSTALL = :
+PRE_UNINSTALL = :
+POST_UNINSTALL = :
+build_triplet = @build@
+host_triplet = @host@
+subdir = src/lib-test
+ACLOCAL_M4 = $(top_srcdir)/aclocal.m4
+am__aclocal_m4_deps = $(top_srcdir)/m4/ac_checktype2.m4 \
+ $(top_srcdir)/m4/ac_typeof.m4 $(top_srcdir)/m4/arc4random.m4 \
+ $(top_srcdir)/m4/blockdev.m4 $(top_srcdir)/m4/c99_vsnprintf.m4 \
+ $(top_srcdir)/m4/clock_gettime.m4 $(top_srcdir)/m4/crypt.m4 \
+ $(top_srcdir)/m4/crypt_xpg6.m4 $(top_srcdir)/m4/dbqlk.m4 \
+ $(top_srcdir)/m4/dirent_dtype.m4 $(top_srcdir)/m4/dovecot.m4 \
+ $(top_srcdir)/m4/fd_passing.m4 $(top_srcdir)/m4/fdatasync.m4 \
+ $(top_srcdir)/m4/flexible_array_member.m4 \
+ $(top_srcdir)/m4/glibc.m4 $(top_srcdir)/m4/gmtime_max.m4 \
+ $(top_srcdir)/m4/gmtime_tm_gmtoff.m4 \
+ $(top_srcdir)/m4/ioloop.m4 $(top_srcdir)/m4/iovec.m4 \
+ $(top_srcdir)/m4/ipv6.m4 $(top_srcdir)/m4/libcap.m4 \
+ $(top_srcdir)/m4/libtool.m4 $(top_srcdir)/m4/libwrap.m4 \
+ $(top_srcdir)/m4/linux_mremap.m4 $(top_srcdir)/m4/ltoptions.m4 \
+ $(top_srcdir)/m4/ltsugar.m4 $(top_srcdir)/m4/ltversion.m4 \
+ $(top_srcdir)/m4/lt~obsolete.m4 $(top_srcdir)/m4/mmap_write.m4 \
+ $(top_srcdir)/m4/mntctl.m4 $(top_srcdir)/m4/modules.m4 \
+ $(top_srcdir)/m4/notify.m4 $(top_srcdir)/m4/nsl.m4 \
+ $(top_srcdir)/m4/off_t_max.m4 $(top_srcdir)/m4/pkg.m4 \
+ $(top_srcdir)/m4/pr_set_dumpable.m4 \
+ $(top_srcdir)/m4/q_quotactl.m4 $(top_srcdir)/m4/quota.m4 \
+ $(top_srcdir)/m4/random.m4 $(top_srcdir)/m4/rlimit.m4 \
+ $(top_srcdir)/m4/sendfile.m4 $(top_srcdir)/m4/size_t_signed.m4 \
+ $(top_srcdir)/m4/sockpeercred.m4 $(top_srcdir)/m4/sql.m4 \
+ $(top_srcdir)/m4/ssl.m4 $(top_srcdir)/m4/st_tim.m4 \
+ $(top_srcdir)/m4/static_array.m4 $(top_srcdir)/m4/test_with.m4 \
+ $(top_srcdir)/m4/time_t.m4 $(top_srcdir)/m4/typeof.m4 \
+ $(top_srcdir)/m4/typeof_dev_t.m4 \
+ $(top_srcdir)/m4/uoff_t_max.m4 $(top_srcdir)/m4/vararg.m4 \
+ $(top_srcdir)/m4/want_apparmor.m4 \
+ $(top_srcdir)/m4/want_bsdauth.m4 \
+ $(top_srcdir)/m4/want_bzlib.m4 \
+ $(top_srcdir)/m4/want_cassandra.m4 \
+ $(top_srcdir)/m4/want_cdb.m4 \
+ $(top_srcdir)/m4/want_checkpassword.m4 \
+ $(top_srcdir)/m4/want_clucene.m4 $(top_srcdir)/m4/want_db.m4 \
+ $(top_srcdir)/m4/want_gssapi.m4 $(top_srcdir)/m4/want_icu.m4 \
+ $(top_srcdir)/m4/want_ldap.m4 $(top_srcdir)/m4/want_lua.m4 \
+ $(top_srcdir)/m4/want_lz4.m4 $(top_srcdir)/m4/want_lzma.m4 \
+ $(top_srcdir)/m4/want_mysql.m4 $(top_srcdir)/m4/want_pam.m4 \
+ $(top_srcdir)/m4/want_passwd.m4 $(top_srcdir)/m4/want_pgsql.m4 \
+ $(top_srcdir)/m4/want_prefetch.m4 \
+ $(top_srcdir)/m4/want_shadow.m4 \
+ $(top_srcdir)/m4/want_sodium.m4 $(top_srcdir)/m4/want_solr.m4 \
+ $(top_srcdir)/m4/want_sqlite.m4 \
+ $(top_srcdir)/m4/want_stemmer.m4 \
+ $(top_srcdir)/m4/want_systemd.m4 \
+ $(top_srcdir)/m4/want_textcat.m4 \
+ $(top_srcdir)/m4/want_unwind.m4 $(top_srcdir)/m4/want_zlib.m4 \
+ $(top_srcdir)/m4/want_zstd.m4 $(top_srcdir)/configure.ac
+am__configure_deps = $(am__aclocal_m4_deps) $(CONFIGURE_DEPENDENCIES) \
+ $(ACLOCAL_M4)
+DIST_COMMON = $(srcdir)/Makefile.am $(pkginc_lib_HEADERS) \
+ $(am__DIST_COMMON)
+mkinstalldirs = $(install_sh) -d
+CONFIG_HEADER = $(top_builddir)/config.h
+CONFIG_CLEAN_FILES =
+CONFIG_CLEAN_VPATH_FILES =
+LTLIBRARIES = $(noinst_LTLIBRARIES)
+libtest_la_LIBADD =
+am_libtest_la_OBJECTS = fuzzer.lo test-common.lo test-istream.lo \
+ test-ostream.lo test-subprocess.lo
+libtest_la_OBJECTS = $(am_libtest_la_OBJECTS)
+AM_V_lt = $(am__v_lt_@AM_V@)
+am__v_lt_ = $(am__v_lt_@AM_DEFAULT_V@)
+am__v_lt_0 = --silent
+am__v_lt_1 =
+AM_V_P = $(am__v_P_@AM_V@)
+am__v_P_ = $(am__v_P_@AM_DEFAULT_V@)
+am__v_P_0 = false
+am__v_P_1 = :
+AM_V_GEN = $(am__v_GEN_@AM_V@)
+am__v_GEN_ = $(am__v_GEN_@AM_DEFAULT_V@)
+am__v_GEN_0 = @echo " GEN " $@;
+am__v_GEN_1 =
+AM_V_at = $(am__v_at_@AM_V@)
+am__v_at_ = $(am__v_at_@AM_DEFAULT_V@)
+am__v_at_0 = @
+am__v_at_1 =
+DEFAULT_INCLUDES = -I.@am__isrc@ -I$(top_builddir)
+depcomp = $(SHELL) $(top_srcdir)/depcomp
+am__maybe_remake_depfiles = depfiles
+am__depfiles_remade = ./$(DEPDIR)/fuzzer.Plo \
+ ./$(DEPDIR)/test-common.Plo ./$(DEPDIR)/test-istream.Plo \
+ ./$(DEPDIR)/test-ostream.Plo ./$(DEPDIR)/test-subprocess.Plo
+am__mv = mv -f
+COMPILE = $(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) \
+ $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS)
+LTCOMPILE = $(LIBTOOL) $(AM_V_lt) --tag=CC $(AM_LIBTOOLFLAGS) \
+ $(LIBTOOLFLAGS) --mode=compile $(CC) $(DEFS) \
+ $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) $(CPPFLAGS) \
+ $(AM_CFLAGS) $(CFLAGS)
+AM_V_CC = $(am__v_CC_@AM_V@)
+am__v_CC_ = $(am__v_CC_@AM_DEFAULT_V@)
+am__v_CC_0 = @echo " CC " $@;
+am__v_CC_1 =
+CCLD = $(CC)
+LINK = $(LIBTOOL) $(AM_V_lt) --tag=CC $(AM_LIBTOOLFLAGS) \
+ $(LIBTOOLFLAGS) --mode=link $(CCLD) $(AM_CFLAGS) $(CFLAGS) \
+ $(AM_LDFLAGS) $(LDFLAGS) -o $@
+AM_V_CCLD = $(am__v_CCLD_@AM_V@)
+am__v_CCLD_ = $(am__v_CCLD_@AM_DEFAULT_V@)
+am__v_CCLD_0 = @echo " CCLD " $@;
+am__v_CCLD_1 =
+SOURCES = $(libtest_la_SOURCES)
+DIST_SOURCES = $(libtest_la_SOURCES)
+am__can_run_installinfo = \
+ case $$AM_UPDATE_INFO_DIR in \
+ n|no|NO) false;; \
+ *) (install-info --version) >/dev/null 2>&1;; \
+ esac
+am__vpath_adj_setup = srcdirstrip=`echo "$(srcdir)" | sed 's|.|.|g'`;
+am__vpath_adj = case $$p in \
+ $(srcdir)/*) f=`echo "$$p" | sed "s|^$$srcdirstrip/||"`;; \
+ *) f=$$p;; \
+ esac;
+am__strip_dir = f=`echo $$p | sed -e 's|^.*/||'`;
+am__install_max = 40
+am__nobase_strip_setup = \
+ srcdirstrip=`echo "$(srcdir)" | sed 's/[].[^$$\\*|]/\\\\&/g'`
+am__nobase_strip = \
+ for p in $$list; do echo "$$p"; done | sed -e "s|$$srcdirstrip/||"
+am__nobase_list = $(am__nobase_strip_setup); \
+ for p in $$list; do echo "$$p $$p"; done | \
+ sed "s| $$srcdirstrip/| |;"' / .*\//!s/ .*/ ./; s,\( .*\)/[^/]*$$,\1,' | \
+ $(AWK) 'BEGIN { files["."] = "" } { files[$$2] = files[$$2] " " $$1; \
+ if (++n[$$2] == $(am__install_max)) \
+ { print $$2, files[$$2]; n[$$2] = 0; files[$$2] = "" } } \
+ END { for (dir in files) print dir, files[dir] }'
+am__base_list = \
+ sed '$$!N;$$!N;$$!N;$$!N;$$!N;$$!N;$$!N;s/\n/ /g' | \
+ sed '$$!N;$$!N;$$!N;$$!N;s/\n/ /g'
+am__uninstall_files_from_dir = { \
+ test -z "$$files" \
+ || { test ! -d "$$dir" && test ! -f "$$dir" && test ! -r "$$dir"; } \
+ || { echo " ( cd '$$dir' && rm -f" $$files ")"; \
+ $(am__cd) "$$dir" && rm -f $$files; }; \
+ }
+am__installdirs = "$(DESTDIR)$(pkginc_libdir)"
+HEADERS = $(pkginc_lib_HEADERS)
+am__tagged_files = $(HEADERS) $(SOURCES) $(TAGS_FILES) $(LISP)
+# Read a list of newline-separated strings from the standard input,
+# and print each of them once, without duplicates. Input order is
+# *not* preserved.
+am__uniquify_input = $(AWK) '\
+ BEGIN { nonempty = 0; } \
+ { items[$$0] = 1; nonempty = 1; } \
+ END { if (nonempty) { for (i in items) print i; }; } \
+'
+# Make sure the list of sources is unique. This is necessary because,
+# e.g., the same source file might be shared among _SOURCES variables
+# for different programs/libraries.
+am__define_uniq_tagged_files = \
+ list='$(am__tagged_files)'; \
+ unique=`for i in $$list; do \
+ if test -f "$$i"; then echo $$i; else echo $(srcdir)/$$i; fi; \
+ done | $(am__uniquify_input)`
+ETAGS = etags
+CTAGS = ctags
+am__DIST_COMMON = $(srcdir)/Makefile.in $(top_srcdir)/depcomp
+DISTFILES = $(DIST_COMMON) $(DIST_SOURCES) $(TEXINFOS) $(EXTRA_DIST)
+ACLOCAL = @ACLOCAL@
+ACLOCAL_AMFLAGS = @ACLOCAL_AMFLAGS@
+AMTAR = @AMTAR@
+AM_DEFAULT_VERBOSITY = @AM_DEFAULT_VERBOSITY@
+APPARMOR_LIBS = @APPARMOR_LIBS@
+AR = @AR@
+AUTH_CFLAGS = @AUTH_CFLAGS@
+AUTH_LIBS = @AUTH_LIBS@
+AUTOCONF = @AUTOCONF@
+AUTOHEADER = @AUTOHEADER@
+AUTOMAKE = @AUTOMAKE@
+AWK = @AWK@
+BINARY_CFLAGS = @BINARY_CFLAGS@
+BINARY_LDFLAGS = @BINARY_LDFLAGS@
+BISON = @BISON@
+CASSANDRA_CFLAGS = @CASSANDRA_CFLAGS@
+CASSANDRA_LIBS = @CASSANDRA_LIBS@
+CC = @CC@
+CCDEPMODE = @CCDEPMODE@
+CDB_LIBS = @CDB_LIBS@
+CFLAGS = @CFLAGS@
+CLUCENE_CFLAGS = @CLUCENE_CFLAGS@
+CLUCENE_LIBS = @CLUCENE_LIBS@
+COMPRESS_LIBS = @COMPRESS_LIBS@
+CPP = @CPP@
+CPPFLAGS = @CPPFLAGS@
+CRYPT_LIBS = @CRYPT_LIBS@
+CXX = @CXX@
+CXXCPP = @CXXCPP@
+CXXDEPMODE = @CXXDEPMODE@
+CXXFLAGS = @CXXFLAGS@
+CYGPATH_W = @CYGPATH_W@
+DEFS = @DEFS@
+DEPDIR = @DEPDIR@
+DICT_LIBS = @DICT_LIBS@
+DLLIB = @DLLIB@
+DLLTOOL = @DLLTOOL@
+DSYMUTIL = @DSYMUTIL@
+DUMPBIN = @DUMPBIN@
+ECHO_C = @ECHO_C@
+ECHO_N = @ECHO_N@
+ECHO_T = @ECHO_T@
+EGREP = @EGREP@
+EXEEXT = @EXEEXT@
+FGREP = @FGREP@
+FLEX = @FLEX@
+FUZZER_CPPFLAGS = @FUZZER_CPPFLAGS@
+FUZZER_LDFLAGS = @FUZZER_LDFLAGS@
+GREP = @GREP@
+INSTALL = @INSTALL@
+INSTALL_DATA = @INSTALL_DATA@
+INSTALL_PROGRAM = @INSTALL_PROGRAM@
+INSTALL_SCRIPT = @INSTALL_SCRIPT@
+INSTALL_STRIP_PROGRAM = @INSTALL_STRIP_PROGRAM@
+KRB5CONFIG = @KRB5CONFIG@
+KRB5_CFLAGS = @KRB5_CFLAGS@
+KRB5_LIBS = @KRB5_LIBS@
+LD = @LD@
+LDAP_LIBS = @LDAP_LIBS@
+LDFLAGS = @LDFLAGS@
+LD_NO_WHOLE_ARCHIVE = @LD_NO_WHOLE_ARCHIVE@
+LD_WHOLE_ARCHIVE = @LD_WHOLE_ARCHIVE@
+LIBCAP = @LIBCAP@
+LIBDOVECOT = @LIBDOVECOT@
+LIBDOVECOT_COMPRESS = @LIBDOVECOT_COMPRESS@
+LIBDOVECOT_DEPS = @LIBDOVECOT_DEPS@
+LIBDOVECOT_DSYNC = @LIBDOVECOT_DSYNC@
+LIBDOVECOT_LA_LIBS = @LIBDOVECOT_LA_LIBS@
+LIBDOVECOT_LDA = @LIBDOVECOT_LDA@
+LIBDOVECOT_LDAP = @LIBDOVECOT_LDAP@
+LIBDOVECOT_LIBFTS = @LIBDOVECOT_LIBFTS@
+LIBDOVECOT_LIBFTS_DEPS = @LIBDOVECOT_LIBFTS_DEPS@
+LIBDOVECOT_LOGIN = @LIBDOVECOT_LOGIN@
+LIBDOVECOT_LUA = @LIBDOVECOT_LUA@
+LIBDOVECOT_LUA_DEPS = @LIBDOVECOT_LUA_DEPS@
+LIBDOVECOT_SQL = @LIBDOVECOT_SQL@
+LIBDOVECOT_STORAGE = @LIBDOVECOT_STORAGE@
+LIBDOVECOT_STORAGE_DEPS = @LIBDOVECOT_STORAGE_DEPS@
+LIBEXTTEXTCAT_CFLAGS = @LIBEXTTEXTCAT_CFLAGS@
+LIBEXTTEXTCAT_LIBS = @LIBEXTTEXTCAT_LIBS@
+LIBICONV = @LIBICONV@
+LIBICU_CFLAGS = @LIBICU_CFLAGS@
+LIBICU_LIBS = @LIBICU_LIBS@
+LIBOBJS = @LIBOBJS@
+LIBS = @LIBS@
+LIBSODIUM_CFLAGS = @LIBSODIUM_CFLAGS@
+LIBSODIUM_LIBS = @LIBSODIUM_LIBS@
+LIBTIRPC_CFLAGS = @LIBTIRPC_CFLAGS@
+LIBTIRPC_LIBS = @LIBTIRPC_LIBS@
+LIBTOOL = @LIBTOOL@
+LIBUNWIND_CFLAGS = @LIBUNWIND_CFLAGS@
+LIBUNWIND_LIBS = @LIBUNWIND_LIBS@
+LIBWRAP_LIBS = @LIBWRAP_LIBS@
+LINKED_STORAGE_LDADD = @LINKED_STORAGE_LDADD@
+LIPO = @LIPO@
+LN_S = @LN_S@
+LTLIBICONV = @LTLIBICONV@
+LTLIBOBJS = @LTLIBOBJS@
+LT_SYS_LIBRARY_PATH = @LT_SYS_LIBRARY_PATH@
+LUA_CFLAGS = @LUA_CFLAGS@
+LUA_LIBS = @LUA_LIBS@
+MAINT = @MAINT@
+MAKEINFO = @MAKEINFO@
+MANIFEST_TOOL = @MANIFEST_TOOL@
+MKDIR_P = @MKDIR_P@
+MODULE_LIBS = @MODULE_LIBS@
+MODULE_SUFFIX = @MODULE_SUFFIX@
+MYSQL_CFLAGS = @MYSQL_CFLAGS@
+MYSQL_CONFIG = @MYSQL_CONFIG@
+MYSQL_LIBS = @MYSQL_LIBS@
+NM = @NM@
+NMEDIT = @NMEDIT@
+NOPLUGIN_LDFLAGS = @NOPLUGIN_LDFLAGS@
+OBJDUMP = @OBJDUMP@
+OBJEXT = @OBJEXT@
+OTOOL = @OTOOL@
+OTOOL64 = @OTOOL64@
+PACKAGE = @PACKAGE@
+PACKAGE_BUGREPORT = @PACKAGE_BUGREPORT@
+PACKAGE_NAME = @PACKAGE_NAME@
+PACKAGE_STRING = @PACKAGE_STRING@
+PACKAGE_TARNAME = @PACKAGE_TARNAME@
+PACKAGE_URL = @PACKAGE_URL@
+PACKAGE_VERSION = @PACKAGE_VERSION@
+PANDOC = @PANDOC@
+PATH_SEPARATOR = @PATH_SEPARATOR@
+PGSQL_CFLAGS = @PGSQL_CFLAGS@
+PGSQL_LIBS = @PGSQL_LIBS@
+PG_CONFIG = @PG_CONFIG@
+PIE_CFLAGS = @PIE_CFLAGS@
+PIE_LDFLAGS = @PIE_LDFLAGS@
+PKG_CONFIG = @PKG_CONFIG@
+PKG_CONFIG_LIBDIR = @PKG_CONFIG_LIBDIR@
+PKG_CONFIG_PATH = @PKG_CONFIG_PATH@
+QUOTA_LIBS = @QUOTA_LIBS@
+RANLIB = @RANLIB@
+RELRO_LDFLAGS = @RELRO_LDFLAGS@
+RPCGEN = @RPCGEN@
+RUN_TEST = @RUN_TEST@
+SED = @SED@
+SETTING_FILES = @SETTING_FILES@
+SET_MAKE = @SET_MAKE@
+SHELL = @SHELL@
+SQLITE_CFLAGS = @SQLITE_CFLAGS@
+SQLITE_LIBS = @SQLITE_LIBS@
+SQL_CFLAGS = @SQL_CFLAGS@
+SQL_LIBS = @SQL_LIBS@
+SSL_CFLAGS = @SSL_CFLAGS@
+SSL_LIBS = @SSL_LIBS@
+STRIP = @STRIP@
+SYSTEMD_CFLAGS = @SYSTEMD_CFLAGS@
+SYSTEMD_LIBS = @SYSTEMD_LIBS@
+VALGRIND = @VALGRIND@
+VERSION = @VERSION@
+ZSTD_CFLAGS = @ZSTD_CFLAGS@
+ZSTD_LIBS = @ZSTD_LIBS@
+abs_builddir = @abs_builddir@
+abs_srcdir = @abs_srcdir@
+abs_top_builddir = @abs_top_builddir@
+abs_top_srcdir = @abs_top_srcdir@
+ac_ct_AR = @ac_ct_AR@
+ac_ct_CC = @ac_ct_CC@
+ac_ct_CXX = @ac_ct_CXX@
+ac_ct_DUMPBIN = @ac_ct_DUMPBIN@
+am__include = @am__include@
+am__leading_dot = @am__leading_dot@
+am__quote = @am__quote@
+am__tar = @am__tar@
+am__untar = @am__untar@
+bindir = @bindir@
+build = @build@
+build_alias = @build_alias@
+build_cpu = @build_cpu@
+build_os = @build_os@
+build_vendor = @build_vendor@
+builddir = @builddir@
+datadir = @datadir@
+datarootdir = @datarootdir@
+dict_drivers = @dict_drivers@
+docdir = @docdir@
+dvidir = @dvidir@
+exec_prefix = @exec_prefix@
+host = @host@
+host_alias = @host_alias@
+host_cpu = @host_cpu@
+host_os = @host_os@
+host_vendor = @host_vendor@
+htmldir = @htmldir@
+includedir = @includedir@
+infodir = @infodir@
+install_sh = @install_sh@
+libdir = @libdir@
+libexecdir = @libexecdir@
+localedir = @localedir@
+localstatedir = @localstatedir@
+mandir = @mandir@
+mkdir_p = @mkdir_p@
+moduledir = @moduledir@
+oldincludedir = @oldincludedir@
+pdfdir = @pdfdir@
+prefix = @prefix@
+program_transform_name = @program_transform_name@
+psdir = @psdir@
+rundir = @rundir@
+runstatedir = @runstatedir@
+sbindir = @sbindir@
+sharedstatedir = @sharedstatedir@
+sql_drivers = @sql_drivers@
+srcdir = @srcdir@
+ssldir = @ssldir@
+statedir = @statedir@
+sysconfdir = @sysconfdir@
+systemdservicetype = @systemdservicetype@
+systemdsystemunitdir = @systemdsystemunitdir@
+target_alias = @target_alias@
+top_build_prefix = @top_build_prefix@
+top_builddir = @top_builddir@
+top_srcdir = @top_srcdir@
+noinst_LTLIBRARIES = libtest.la
+AM_CPPFLAGS = \
+ -I$(top_srcdir)/src/lib \
+ -I$(top_srcdir)/src/lib-charset
+
+libtest_la_SOURCES = \
+ fuzzer.c \
+ test-common.c \
+ test-istream.c \
+ test-ostream.c \
+ test-subprocess.c
+
+headers = \
+ fuzzer.h \
+ test-common.h \
+ test-subprocess.h
+
+pkginc_libdir = $(pkgincludedir)
+pkginc_lib_HEADERS = $(headers)
+all: all-am
+
+.SUFFIXES:
+.SUFFIXES: .c .lo .o .obj
+$(srcdir)/Makefile.in: @MAINTAINER_MODE_TRUE@ $(srcdir)/Makefile.am $(am__configure_deps)
+ @for dep in $?; do \
+ case '$(am__configure_deps)' in \
+ *$$dep*) \
+ ( cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh ) \
+ && { if test -f $@; then exit 0; else break; fi; }; \
+ exit 1;; \
+ esac; \
+ done; \
+ echo ' cd $(top_srcdir) && $(AUTOMAKE) --foreign src/lib-test/Makefile'; \
+ $(am__cd) $(top_srcdir) && \
+ $(AUTOMAKE) --foreign src/lib-test/Makefile
+Makefile: $(srcdir)/Makefile.in $(top_builddir)/config.status
+ @case '$?' in \
+ *config.status*) \
+ cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh;; \
+ *) \
+ echo ' cd $(top_builddir) && $(SHELL) ./config.status $(subdir)/$@ $(am__maybe_remake_depfiles)'; \
+ cd $(top_builddir) && $(SHELL) ./config.status $(subdir)/$@ $(am__maybe_remake_depfiles);; \
+ esac;
+
+$(top_builddir)/config.status: $(top_srcdir)/configure $(CONFIG_STATUS_DEPENDENCIES)
+ cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh
+
+$(top_srcdir)/configure: @MAINTAINER_MODE_TRUE@ $(am__configure_deps)
+ cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh
+$(ACLOCAL_M4): @MAINTAINER_MODE_TRUE@ $(am__aclocal_m4_deps)
+ cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh
+$(am__aclocal_m4_deps):
+
+clean-noinstLTLIBRARIES:
+ -test -z "$(noinst_LTLIBRARIES)" || rm -f $(noinst_LTLIBRARIES)
+ @list='$(noinst_LTLIBRARIES)'; \
+ locs=`for p in $$list; do echo $$p; done | \
+ sed 's|^[^/]*$$|.|; s|/[^/]*$$||; s|$$|/so_locations|' | \
+ sort -u`; \
+ test -z "$$locs" || { \
+ echo rm -f $${locs}; \
+ rm -f $${locs}; \
+ }
+
+libtest.la: $(libtest_la_OBJECTS) $(libtest_la_DEPENDENCIES) $(EXTRA_libtest_la_DEPENDENCIES)
+ $(AM_V_CCLD)$(LINK) $(libtest_la_OBJECTS) $(libtest_la_LIBADD) $(LIBS)
+
+mostlyclean-compile:
+ -rm -f *.$(OBJEXT)
+
+distclean-compile:
+ -rm -f *.tab.c
+
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/fuzzer.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/test-common.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/test-istream.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/test-ostream.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/test-subprocess.Plo@am__quote@ # am--include-marker
+
+$(am__depfiles_remade):
+ @$(MKDIR_P) $(@D)
+ @echo '# dummy' >$@-t && $(am__mv) $@-t $@
+
+am--depfiles: $(am__depfiles_remade)
+
+.c.o:
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(COMPILE) -MT $@ -MD -MP -MF $(DEPDIR)/$*.Tpo -c -o $@ $<
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/$*.Tpo $(DEPDIR)/$*.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='$<' object='$@' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(COMPILE) -c -o $@ $<
+
+.c.obj:
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(COMPILE) -MT $@ -MD -MP -MF $(DEPDIR)/$*.Tpo -c -o $@ `$(CYGPATH_W) '$<'`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/$*.Tpo $(DEPDIR)/$*.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='$<' object='$@' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(COMPILE) -c -o $@ `$(CYGPATH_W) '$<'`
+
+.c.lo:
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(LTCOMPILE) -MT $@ -MD -MP -MF $(DEPDIR)/$*.Tpo -c -o $@ $<
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/$*.Tpo $(DEPDIR)/$*.Plo
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='$<' object='$@' libtool=yes @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(LTCOMPILE) -c -o $@ $<
+
+mostlyclean-libtool:
+ -rm -f *.lo
+
+clean-libtool:
+ -rm -rf .libs _libs
+install-pkginc_libHEADERS: $(pkginc_lib_HEADERS)
+ @$(NORMAL_INSTALL)
+ @list='$(pkginc_lib_HEADERS)'; test -n "$(pkginc_libdir)" || list=; \
+ if test -n "$$list"; then \
+ echo " $(MKDIR_P) '$(DESTDIR)$(pkginc_libdir)'"; \
+ $(MKDIR_P) "$(DESTDIR)$(pkginc_libdir)" || exit 1; \
+ fi; \
+ for p in $$list; do \
+ if test -f "$$p"; then d=; else d="$(srcdir)/"; fi; \
+ echo "$$d$$p"; \
+ done | $(am__base_list) | \
+ while read files; do \
+ echo " $(INSTALL_HEADER) $$files '$(DESTDIR)$(pkginc_libdir)'"; \
+ $(INSTALL_HEADER) $$files "$(DESTDIR)$(pkginc_libdir)" || exit $$?; \
+ done
+
+uninstall-pkginc_libHEADERS:
+ @$(NORMAL_UNINSTALL)
+ @list='$(pkginc_lib_HEADERS)'; test -n "$(pkginc_libdir)" || list=; \
+ files=`for p in $$list; do echo $$p; done | sed -e 's|^.*/||'`; \
+ dir='$(DESTDIR)$(pkginc_libdir)'; $(am__uninstall_files_from_dir)
+
+ID: $(am__tagged_files)
+ $(am__define_uniq_tagged_files); mkid -fID $$unique
+tags: tags-am
+TAGS: tags
+
+tags-am: $(TAGS_DEPENDENCIES) $(am__tagged_files)
+ set x; \
+ here=`pwd`; \
+ $(am__define_uniq_tagged_files); \
+ shift; \
+ if test -z "$(ETAGS_ARGS)$$*$$unique"; then :; else \
+ test -n "$$unique" || unique=$$empty_fix; \
+ if test $$# -gt 0; then \
+ $(ETAGS) $(ETAGSFLAGS) $(AM_ETAGSFLAGS) $(ETAGS_ARGS) \
+ "$$@" $$unique; \
+ else \
+ $(ETAGS) $(ETAGSFLAGS) $(AM_ETAGSFLAGS) $(ETAGS_ARGS) \
+ $$unique; \
+ fi; \
+ fi
+ctags: ctags-am
+
+CTAGS: ctags
+ctags-am: $(TAGS_DEPENDENCIES) $(am__tagged_files)
+ $(am__define_uniq_tagged_files); \
+ test -z "$(CTAGS_ARGS)$$unique" \
+ || $(CTAGS) $(CTAGSFLAGS) $(AM_CTAGSFLAGS) $(CTAGS_ARGS) \
+ $$unique
+
+GTAGS:
+ here=`$(am__cd) $(top_builddir) && pwd` \
+ && $(am__cd) $(top_srcdir) \
+ && gtags -i $(GTAGS_ARGS) "$$here"
+cscopelist: cscopelist-am
+
+cscopelist-am: $(am__tagged_files)
+ list='$(am__tagged_files)'; \
+ case "$(srcdir)" in \
+ [\\/]* | ?:[\\/]*) sdir="$(srcdir)" ;; \
+ *) sdir=$(subdir)/$(srcdir) ;; \
+ esac; \
+ for i in $$list; do \
+ if test -f "$$i"; then \
+ echo "$(subdir)/$$i"; \
+ else \
+ echo "$$sdir/$$i"; \
+ fi; \
+ done >> $(top_builddir)/cscope.files
+
+distclean-tags:
+ -rm -f TAGS ID GTAGS GRTAGS GSYMS GPATH tags
+
+distdir: $(BUILT_SOURCES)
+ $(MAKE) $(AM_MAKEFLAGS) distdir-am
+
+distdir-am: $(DISTFILES)
+ @srcdirstrip=`echo "$(srcdir)" | sed 's/[].[^$$\\*]/\\\\&/g'`; \
+ topsrcdirstrip=`echo "$(top_srcdir)" | sed 's/[].[^$$\\*]/\\\\&/g'`; \
+ list='$(DISTFILES)'; \
+ dist_files=`for file in $$list; do echo $$file; done | \
+ sed -e "s|^$$srcdirstrip/||;t" \
+ -e "s|^$$topsrcdirstrip/|$(top_builddir)/|;t"`; \
+ case $$dist_files in \
+ */*) $(MKDIR_P) `echo "$$dist_files" | \
+ sed '/\//!d;s|^|$(distdir)/|;s,/[^/]*$$,,' | \
+ sort -u` ;; \
+ esac; \
+ for file in $$dist_files; do \
+ if test -f $$file || test -d $$file; then d=.; else d=$(srcdir); fi; \
+ if test -d $$d/$$file; then \
+ dir=`echo "/$$file" | sed -e 's,/[^/]*$$,,'`; \
+ if test -d "$(distdir)/$$file"; then \
+ find "$(distdir)/$$file" -type d ! -perm -700 -exec chmod u+rwx {} \;; \
+ fi; \
+ if test -d $(srcdir)/$$file && test $$d != $(srcdir); then \
+ cp -fpR $(srcdir)/$$file "$(distdir)$$dir" || exit 1; \
+ find "$(distdir)/$$file" -type d ! -perm -700 -exec chmod u+rwx {} \;; \
+ fi; \
+ cp -fpR $$d/$$file "$(distdir)$$dir" || exit 1; \
+ else \
+ test -f "$(distdir)/$$file" \
+ || cp -p $$d/$$file "$(distdir)/$$file" \
+ || exit 1; \
+ fi; \
+ done
+check-am: all-am
+check: check-am
+all-am: Makefile $(LTLIBRARIES) $(HEADERS)
+installdirs:
+ for dir in "$(DESTDIR)$(pkginc_libdir)"; do \
+ test -z "$$dir" || $(MKDIR_P) "$$dir"; \
+ done
+install: install-am
+install-exec: install-exec-am
+install-data: install-data-am
+uninstall: uninstall-am
+
+install-am: all-am
+ @$(MAKE) $(AM_MAKEFLAGS) install-exec-am install-data-am
+
+installcheck: installcheck-am
+install-strip:
+ if test -z '$(STRIP)'; then \
+ $(MAKE) $(AM_MAKEFLAGS) INSTALL_PROGRAM="$(INSTALL_STRIP_PROGRAM)" \
+ install_sh_PROGRAM="$(INSTALL_STRIP_PROGRAM)" INSTALL_STRIP_FLAG=-s \
+ install; \
+ else \
+ $(MAKE) $(AM_MAKEFLAGS) INSTALL_PROGRAM="$(INSTALL_STRIP_PROGRAM)" \
+ install_sh_PROGRAM="$(INSTALL_STRIP_PROGRAM)" INSTALL_STRIP_FLAG=-s \
+ "INSTALL_PROGRAM_ENV=STRIPPROG='$(STRIP)'" install; \
+ fi
+mostlyclean-generic:
+
+clean-generic:
+
+distclean-generic:
+ -test -z "$(CONFIG_CLEAN_FILES)" || rm -f $(CONFIG_CLEAN_FILES)
+ -test . = "$(srcdir)" || test -z "$(CONFIG_CLEAN_VPATH_FILES)" || rm -f $(CONFIG_CLEAN_VPATH_FILES)
+
+maintainer-clean-generic:
+ @echo "This command is intended for maintainers to use"
+ @echo "it deletes files that may require special tools to rebuild."
+clean: clean-am
+
+clean-am: clean-generic clean-libtool clean-noinstLTLIBRARIES \
+ mostlyclean-am
+
+distclean: distclean-am
+ -rm -f ./$(DEPDIR)/fuzzer.Plo
+ -rm -f ./$(DEPDIR)/test-common.Plo
+ -rm -f ./$(DEPDIR)/test-istream.Plo
+ -rm -f ./$(DEPDIR)/test-ostream.Plo
+ -rm -f ./$(DEPDIR)/test-subprocess.Plo
+ -rm -f Makefile
+distclean-am: clean-am distclean-compile distclean-generic \
+ distclean-tags
+
+dvi: dvi-am
+
+dvi-am:
+
+html: html-am
+
+html-am:
+
+info: info-am
+
+info-am:
+
+install-data-am: install-pkginc_libHEADERS
+
+install-dvi: install-dvi-am
+
+install-dvi-am:
+
+install-exec-am:
+
+install-html: install-html-am
+
+install-html-am:
+
+install-info: install-info-am
+
+install-info-am:
+
+install-man:
+
+install-pdf: install-pdf-am
+
+install-pdf-am:
+
+install-ps: install-ps-am
+
+install-ps-am:
+
+installcheck-am:
+
+maintainer-clean: maintainer-clean-am
+ -rm -f ./$(DEPDIR)/fuzzer.Plo
+ -rm -f ./$(DEPDIR)/test-common.Plo
+ -rm -f ./$(DEPDIR)/test-istream.Plo
+ -rm -f ./$(DEPDIR)/test-ostream.Plo
+ -rm -f ./$(DEPDIR)/test-subprocess.Plo
+ -rm -f Makefile
+maintainer-clean-am: distclean-am maintainer-clean-generic
+
+mostlyclean: mostlyclean-am
+
+mostlyclean-am: mostlyclean-compile mostlyclean-generic \
+ mostlyclean-libtool
+
+pdf: pdf-am
+
+pdf-am:
+
+ps: ps-am
+
+ps-am:
+
+uninstall-am: uninstall-pkginc_libHEADERS
+
+.MAKE: install-am install-strip
+
+.PHONY: CTAGS GTAGS TAGS all all-am am--depfiles check check-am clean \
+ clean-generic clean-libtool clean-noinstLTLIBRARIES \
+ cscopelist-am ctags ctags-am distclean distclean-compile \
+ distclean-generic distclean-libtool distclean-tags distdir dvi \
+ dvi-am html html-am info info-am install install-am \
+ install-data install-data-am install-dvi install-dvi-am \
+ install-exec install-exec-am install-html install-html-am \
+ install-info install-info-am install-man install-pdf \
+ install-pdf-am install-pkginc_libHEADERS install-ps \
+ install-ps-am install-strip installcheck installcheck-am \
+ installdirs maintainer-clean maintainer-clean-generic \
+ mostlyclean mostlyclean-compile mostlyclean-generic \
+ mostlyclean-libtool pdf pdf-am ps ps-am tags tags-am uninstall \
+ uninstall-am uninstall-pkginc_libHEADERS
+
+.PRECIOUS: Makefile
+
+
+# Tell versions [3.59,3.63) of GNU make to not export all variables.
+# Otherwise a system limit (for SysV at least) may be exceeded.
+.NOEXPORT:
diff --git a/src/lib-test/fuzzer.c b/src/lib-test/fuzzer.c
new file mode 100644
index 0000000..c15fee7
--- /dev/null
+++ b/src/lib-test/fuzzer.c
@@ -0,0 +1,87 @@
+/* Copyright (c) 2020 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "lib-signals.h"
+#include "net.h"
+#include "istream.h"
+#include "ostream.h"
+#include "iostream-pump.h"
+#include "fuzzer.h"
+
+#include <sys/socket.h>
+#include <unistd.h>
+
+void fuzzer_init(struct fuzzer_context *fuzz_ctx)
+{
+ i_zero(fuzz_ctx);
+ if (!lib_is_initialized()) {
+ lib_init();
+ lib_signals_init();
+ lib_signals_ignore(SIGPIPE, TRUE);
+ }
+ fuzz_ctx->fd = -1;
+}
+
+void fuzzer_deinit(struct fuzzer_context *fuzz_ctx)
+{
+ iostream_pump_destroy(&fuzz_ctx->pump);
+ /* ensure fd gets closed, we don't care
+ if this fails. */
+ if (fuzz_ctx->fd > -1)
+ (void)close(fuzz_ctx->fd);
+ if (fuzz_ctx->fd_pump > -1)
+ (void)close(fuzz_ctx->fd_pump);
+ if (fuzz_ctx->ioloop != NULL)
+ io_loop_destroy(&fuzz_ctx->ioloop);
+}
+
+static void pump_finished(enum iostream_pump_status status ATTR_UNUSED,
+ struct fuzzer_context *fuzz_ctx)
+{
+ struct istream *input = iostream_pump_get_input(fuzz_ctx->pump);
+ struct ostream *output = iostream_pump_get_output(fuzz_ctx->pump);
+
+ switch (status) {
+ case IOSTREAM_PUMP_STATUS_INPUT_EOF:
+ break;
+ case IOSTREAM_PUMP_STATUS_INPUT_ERROR:
+ i_error("read(%s) failed: %s", i_stream_get_name(input),
+ i_stream_get_error(input));
+ break;
+ case IOSTREAM_PUMP_STATUS_OUTPUT_ERROR:
+ i_error("write(%s) failed: %s", o_stream_get_name(output),
+ o_stream_get_error(output));
+ break;
+ };
+
+ if (shutdown(o_stream_get_fd(output), SHUT_WR) < 0)
+ i_fatal("shutdown() failed: %m");
+ iostream_pump_destroy(&fuzz_ctx->pump);
+}
+
+int fuzzer_io_as_fd(struct fuzzer_context *fuzz_ctx,
+ const uint8_t *data, size_t size)
+{
+ int sfd[2];
+
+ if (socketpair(AF_UNIX, SOCK_STREAM, 0, sfd) < 0)
+ i_fatal("socketpair() failed: %m");
+ net_set_nonblock(sfd[0], TRUE);
+ net_set_nonblock(sfd[1], TRUE);
+
+ struct istream *input = i_stream_create_from_data(data, size);
+ struct ostream *output = o_stream_create_fd(sfd[0], IO_BLOCK_SIZE);
+ i_stream_set_name(input, "(fuzzer data)");
+ o_stream_set_name(output, "(fuzzer input to program)");
+ o_stream_set_no_error_handling(output, TRUE);
+
+ fuzz_ctx->pump = iostream_pump_create(input, output);
+ fuzz_ctx->fd_pump = sfd[0];
+ fuzz_ctx->fd = sfd[1];
+ iostream_pump_set_completion_callback(fuzz_ctx->pump, pump_finished,
+ fuzz_ctx);
+ i_stream_unref(&input);
+ o_stream_unref(&output);
+ iostream_pump_start(fuzz_ctx->pump);
+ return sfd[1];
+}
diff --git a/src/lib-test/fuzzer.h b/src/lib-test/fuzzer.h
new file mode 100644
index 0000000..e20ba13
--- /dev/null
+++ b/src/lib-test/fuzzer.h
@@ -0,0 +1,37 @@
+#ifndef FUZZER_H
+#define FUZZER_H
+
+struct iostream_pump;
+struct ioloop;
+
+struct fuzzer_context {
+ int fd, fd_pump;
+ struct iostream_pump *pump;
+ struct ioloop *ioloop;
+};
+
+#define FUZZ_BEGIN_DATA(data_arg, size_arg) \
+ int LLVMFuzzerTestOneInput(data_arg, size_arg); \
+ int LLVMFuzzerTestOneInput(data_arg, size_arg) { \
+ struct fuzzer_context fuzz_ctx; \
+ fuzzer_init(&fuzz_ctx); T_BEGIN {
+
+#define FUZZ_BEGIN_STR(str_arg) \
+ FUZZ_BEGIN_DATA(const uint8_t *_param_data, size_t _param_size) \
+ str_arg = t_strndup(_param_data, _param_size);
+
+#define FUZZ_BEGIN_FD \
+ FUZZ_BEGIN_DATA(const uint8_t *_param_data, size_t _param_size) \
+ fuzz_ctx.ioloop = io_loop_create(); \
+ (void)fuzzer_io_as_fd(&fuzz_ctx, _param_data, _param_size);
+
+#define FUZZ_END \
+ } T_END; fuzzer_deinit(&fuzz_ctx); return 0; }
+
+void fuzzer_init(struct fuzzer_context *fuzz_ctx);
+void fuzzer_deinit(struct fuzzer_context *fuzz_ctx);
+
+int fuzzer_io_as_fd(struct fuzzer_context *fuzz_ctx,
+ const uint8_t *data, size_t size);
+
+#endif
diff --git a/src/lib-test/test-common.c b/src/lib-test/test-common.c
new file mode 100644
index 0000000..feea7ec
--- /dev/null
+++ b/src/lib-test/test-common.c
@@ -0,0 +1,465 @@
+/* Copyright (c) 2007-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "test-common.h"
+
+#include <stdio.h>
+#include <setjmp.h> /* for fatal tests */
+
+static bool test_deinit_lib;
+
+/* To test the firing of i_assert, we need non-local jumps, i.e. setjmp */
+static volatile bool expecting_fatal = FALSE;
+static jmp_buf fatal_jmpbuf;
+
+#define OUT_NAME_ALIGN 70
+
+static char *test_prefix;
+static bool test_success;
+static unsigned int failure_count;
+static unsigned int total_count;
+static unsigned int expected_errors;
+static char *expected_error_str, *expected_fatal_str;
+static test_fatal_callback_t *test_fatal_callback;
+static void *test_fatal_context;
+
+void test_begin(const char *name)
+{
+ test_success = TRUE;
+ expected_errors = 0;
+ if (!expecting_fatal)
+ i_assert(test_prefix == NULL);
+ else
+ test_assert((test_success = (test_prefix == NULL)));
+ test_prefix = i_strdup(name);
+}
+
+bool test_has_failed(void)
+{
+ return !test_success;
+}
+
+void test_assert_failed(const char *code, const char *file, unsigned int line)
+{
+ printf("%s:%u: Assert failed: %s\n", file, line, code);
+ fflush(stdout);
+ test_success = FALSE;
+#ifdef STATIC_CHECKER
+ i_unreached();
+#endif
+}
+
+void test_assert_failed_idx(const char *code, const char *file, unsigned int line, long long i)
+{
+ printf("%s:%u: Assert(#%lld) failed: %s\n", file, line, i, code);
+ fflush(stdout);
+ test_success = FALSE;
+#ifdef STATIC_CHECKER
+ i_unreached();
+#endif
+}
+
+void test_assert_failed_strcmp_idx(const char *code, const char *file, unsigned int line,
+ const char * src, const char * dst, long long i)
+{
+ printf("%s:%u: Assert", file, line);
+ if (i == LLONG_MIN)
+ printf(" failed: %s\n", code);
+ else
+ printf("(#%lld) failed: %s\n", i, code);
+ if (src != NULL)
+ printf(" \"%s\" != ", src);
+ else
+ printf(" NULL != ");
+ if (dst != NULL)
+ printf("\"%s\"\n", dst);
+ else
+ printf("NULL\n");
+ fflush(stdout);
+ test_success = FALSE;
+#ifdef STATIC_CHECKER
+ i_unreached();
+#endif
+}
+
+void test_assert_failed_cmp_intmax_idx(const char *code, const char *file,
+ unsigned int line,
+ intmax_t src, intmax_t dst,
+ const char *op, long long i)
+{
+ printf("%s:%u: Assert", file, line);
+ if (i == LLONG_MIN)
+ printf(" failed: %s\n", code);
+ else
+ printf("(#%lld) failed: %s\n", i, code);
+ printf(" %jd %s %jd is not true\n", src, op, dst);
+ fflush(stdout);
+ test_success = FALSE;
+#ifdef STATIC_CHECKER
+ i_unreached();
+#endif
+}
+
+void test_assert_failed_ucmp_intmax_idx(const char *code, const char *file,
+ unsigned int line,
+ uintmax_t src, uintmax_t dst,
+ const char *op, long long i)
+{
+ printf("%s:%u: Assert", file, line);
+ if (i == LLONG_MIN)
+ printf(" failed: %s\n", code);
+ else
+ printf("(#%lld) failed: %s\n", i, code);
+ printf(" %ju %s %ju is not true\n", src, op, dst);
+ fflush(stdout);
+ test_success = FALSE;
+#ifdef STATIC_CHECKER
+ i_unreached();
+#endif
+}
+
+#ifdef DEBUG
+#include "randgen.h"
+static void
+test_dump_rand_state(void)
+{
+ static int64_t seen_seed = -1;
+ unsigned int seed;
+ if (rand_get_last_seed(&seed) < 0) {
+ if (seen_seed == -1) {
+ printf("test: random sequence not reproduceable, use DOVECOT_SRAND=kiss\n");
+ seen_seed = -2;
+ }
+ return;
+ }
+ if (seed == seen_seed)
+ return;
+ seen_seed = seed;
+ printf("test: DOVECOT_SRAND random seed was %u\n", seed);
+}
+#else
+static inline void test_dump_rand_state(void) { }
+#endif
+
+void test_end(void)
+{
+ if (!expecting_fatal)
+ i_assert(test_prefix != NULL);
+ else
+ test_assert(test_prefix != NULL);
+
+ test_out("", test_success);
+ if (!test_success)
+ test_dump_rand_state();
+ i_free_and_null(test_prefix);
+ test_success = FALSE;
+}
+
+void test_out(const char *name, bool success)
+{
+ test_out_reason(name, success, NULL);
+}
+
+void test_out_quiet(const char *name, bool success)
+{
+ if (success) {
+ total_count++;
+ return;
+ }
+ test_out(name, success);
+}
+
+void test_out_reason(const char *name, bool success, const char *reason)
+{
+ int i = 0;
+
+ if (test_prefix != NULL) {
+ fputs(test_prefix, stdout);
+ i += strlen(test_prefix);
+ if (*name != '\0') {
+ putchar(':');
+ i++;
+ }
+ putchar(' ');
+ i++;
+ }
+ if (*name != '\0') {
+ fputs(name, stdout);
+ putchar(' ');
+ i += strlen(name) + 1;
+ }
+ for (; i < OUT_NAME_ALIGN; i++)
+ putchar('.');
+ fputs(" : ", stdout);
+ if (success)
+ fputs("ok", stdout);
+ else {
+ fputs("FAILED", stdout);
+ test_success = FALSE;
+ failure_count++;
+ }
+ if (reason != NULL && *reason != '\0')
+ printf(": %s", reason);
+ putchar('\n');
+ fflush(stdout);
+ total_count++;
+}
+
+void
+test_expect_error_string_n_times(const char *substr, unsigned int times)
+{
+ i_assert(expected_errors == 0);
+ expected_errors = times;
+ expected_error_str = i_strdup(substr);
+}
+void
+test_expect_error_string(const char *substr)
+{
+ test_expect_error_string_n_times(substr, 1);
+}
+void
+test_expect_errors(unsigned int expected)
+{
+ i_assert(expected_errors == 0);
+ expected_errors = expected;
+}
+void
+test_expect_no_more_errors(void)
+{
+ test_assert(expected_errors == 0 && expected_error_str == NULL);
+ i_free_and_null(expected_error_str);
+ expected_errors = 0;
+}
+
+static bool ATTR_FORMAT(2, 0)
+expect_error_check(char **error_strp, const char *format, va_list args)
+{
+ if (*error_strp == NULL)
+ return TRUE;
+
+ bool suppress;
+ T_BEGIN {
+ /* test_assert() will reset test_success if need be. */
+ const char *str = t_strdup_vprintf(format, args);
+ suppress = strstr(str, *error_strp) != NULL;
+ test_assert(suppress == TRUE);
+ i_free_and_null(*error_strp);
+ } T_END;
+ return suppress;
+}
+
+static void ATTR_FORMAT(2, 0)
+test_error_handler(const struct failure_context *ctx,
+ const char *format, va_list args)
+{
+ bool suppress = FALSE;
+
+ if (expected_errors > 0) {
+ va_list args2;
+ VA_COPY(args2, args);
+ suppress = expect_error_check(&expected_error_str, format, args2);
+ expected_errors--;
+ va_end(args2);
+ } else {
+ test_success = FALSE;
+ }
+
+ if (!suppress) {
+ test_dump_rand_state();
+ default_error_handler(ctx, format, args);
+ }
+}
+
+void test_expect_fatal_string(const char *substr)
+{
+ i_free(expected_fatal_str);
+ expected_fatal_str = i_strdup(substr);
+}
+
+#undef test_fatal_set_callback
+void test_fatal_set_callback(test_fatal_callback_t *callback, void *context)
+{
+ i_assert(test_fatal_callback == NULL);
+ test_fatal_callback = callback;
+ test_fatal_context = context;
+}
+
+static void ATTR_FORMAT(2, 0) ATTR_NORETURN
+test_fatal_handler(const struct failure_context *ctx,
+ const char *format, va_list args)
+{
+ /* Prevent recursion, we can't handle our own errors */
+ i_set_fatal_handler(default_fatal_handler);
+ i_assert(expecting_fatal); /* if not at the right time, bail */
+
+ va_list args2;
+ VA_COPY(args2, args);
+ bool suppress = expect_error_check(&expected_fatal_str, format, args2);
+ va_end(args);
+
+ if (suppress) {
+ if (test_fatal_callback != NULL) {
+ test_fatal_callback(test_fatal_context);
+ test_fatal_callback = NULL;
+ test_fatal_context = NULL;
+ }
+
+ i_set_fatal_handler(test_fatal_handler);
+ longjmp(fatal_jmpbuf, 1);
+ } else {
+ default_fatal_handler(ctx, format, args);
+ }
+ i_unreached(); /* we simply can't get here */
+}
+
+static void test_init(void)
+{
+ test_prefix = NULL;
+ failure_count = 0;
+ total_count = 0;
+
+ if (!lib_is_initialized()) {
+ lib_init();
+ test_deinit_lib = TRUE;
+ } else
+ test_deinit_lib = FALSE;
+
+ i_set_error_handler(test_error_handler);
+ /* Don't set fatal handler until actually needed for fatal testing */
+}
+
+static int test_deinit(void)
+{
+ i_assert(test_prefix == NULL);
+ printf("%u / %u tests failed\n", failure_count, total_count);
+ if (test_deinit_lib)
+ lib_deinit();
+ return failure_count == 0 ? 0 : 1;
+}
+
+static void test_run_funcs(void (*const test_functions[])(void))
+{
+ unsigned int i;
+
+ for (i = 0; test_functions[i] != NULL; i++) {
+ T_BEGIN {
+ test_functions[i]();
+ } T_END;
+ }
+}
+static void test_run_named_funcs(const struct named_test tests[],
+ const char *match)
+{
+ unsigned int i;
+
+ for (i = 0; tests[i].func != NULL; i++) {
+ if (strstr(tests[i].name, match) != NULL) T_BEGIN {
+ tests[i].func();
+ } T_END;
+ }
+}
+
+static void run_one_fatal(test_fatal_func_t *fatal_function)
+{
+ static unsigned int index = 0;
+ for (;;) {
+ volatile int jumped = setjmp(fatal_jmpbuf);
+ if (jumped == 0) {
+ /* normal flow */
+ expecting_fatal = TRUE;
+ enum fatal_test_state ret = fatal_function(index);
+ expecting_fatal = FALSE;
+ if (ret == FATAL_TEST_FINISHED) {
+ /* ran out of tests - good */
+ index = 0;
+ break;
+ } else if (ret == FATAL_TEST_FAILURE) {
+ /* failed to fire assert - bad, but can continue */
+ test_success = FALSE;
+ i_error("Desired assert failed to fire at step %i", index);
+ index++;
+ } else { /* FATAL_TEST_ABORT or other value */
+ test_success = FALSE;
+ test_end();
+ index = 0;
+ break;
+ }
+ } else {
+ /* assert fired, continue with next test */
+ index++;
+ }
+ }
+}
+static void test_run_fatals(test_fatal_func_t *const fatal_functions[])
+{
+ unsigned int i;
+
+ for (i = 0; fatal_functions[i] != NULL; i++) {
+ T_BEGIN {
+ run_one_fatal(fatal_functions[i]);
+ } T_END;
+ }
+}
+static void test_run_named_fatals(const struct named_fatal fatals[], const char *match)
+{
+ unsigned int i;
+
+ for (i = 0; fatals[i].func != NULL; i++) {
+ if (strstr(fatals[i].name, match) != NULL) T_BEGIN {
+ run_one_fatal(fatals[i].func);
+ } T_END;
+ }
+}
+
+int test_run(void (*const test_functions[])(void))
+{
+ test_init();
+ test_run_funcs(test_functions);
+ return test_deinit();
+}
+int test_run_named(const struct named_test tests[], const char *match)
+{
+ test_init();
+ test_run_named_funcs(tests, match);
+ return test_deinit();
+}
+int test_run_with_fatals(void (*const test_functions[])(void),
+ test_fatal_func_t *const fatal_functions[])
+{
+ test_init();
+ test_run_funcs(test_functions);
+ i_set_fatal_handler(test_fatal_handler);
+ test_run_fatals(fatal_functions);
+ return test_deinit();
+}
+int test_run_named_with_fatals(const char *match, const struct named_test tests[],
+ const struct named_fatal fatals[])
+{
+ test_init();
+ test_run_named_funcs(tests, match);
+ i_set_fatal_handler(test_fatal_handler);
+ test_run_named_fatals(fatals, match);
+ return test_deinit();
+}
+
+void test_forked_end(void)
+{
+ i_set_error_handler(default_error_handler);
+ i_set_fatal_handler(default_fatal_handler);
+
+ i_free_and_null(expected_error_str);
+ i_free_and_null(expected_fatal_str);
+ i_free_and_null(test_prefix);
+ t_pop_last_unsafe(); /* as we were within a T_BEGIN { tests[i].func(); } T_END */
+}
+
+void ATTR_NORETURN
+test_exit(int status)
+{
+ i_free_and_null(expected_error_str);
+ i_free_and_null(expected_fatal_str);
+ i_free_and_null(test_prefix);
+ t_pop_last_unsafe(); /* as we were within a T_BEGIN { tests[i].func(); } T_END */
+ lib_deinit();
+ lib_exit(status);
+}
diff --git a/src/lib-test/test-common.h b/src/lib-test/test-common.h
new file mode 100644
index 0000000..f845c0d
--- /dev/null
+++ b/src/lib-test/test-common.h
@@ -0,0 +1,165 @@
+#ifndef TEST_COMMON_H
+#define TEST_COMMON_H
+
+struct istream *test_istream_create(const char *data);
+struct istream *test_istream_create_data(const void *data, size_t size);
+void test_istream_set_size(struct istream *input, uoff_t size);
+void test_istream_set_allow_eof(struct istream *input, bool allow);
+void test_istream_set_max_buffer_size(struct istream *input, size_t size);
+
+struct ostream *test_ostream_create(buffer_t *output);
+struct ostream *test_ostream_create_nonblocking(buffer_t *output,
+ size_t max_internal_buffer_size);
+/* When output->used reaches max_size, start buffering output internally.
+ When internal buffer reaches max_internal_buffer_size, start returning 0 for
+ o_stream_send*(). */
+void test_ostream_set_max_output_size(struct ostream *output, size_t max_size);
+
+void test_begin(const char *name);
+#define test_assert(code) STMT_START { \
+ if (!(code)) test_assert_failed(#code, __FILE__, __LINE__); \
+ } STMT_END
+/* Additional parameter may be int or unsigned int, to indicate which of
+ * a barrage of tests have failed (such as in a loop).
+ */
+#define test_assert_idx(code, i) STMT_START { \
+ if (!(code)) test_assert_failed_idx(#code, __FILE__, __LINE__, i); \
+ } STMT_END
+/* Additional parameters are s1 (source) and s2 (destination) string
+ * in strcmp().
+ */
+#define test_assert_strcmp(s1, s2) STMT_START { \
+ test_assert_strcmp_idx(s1, s2, LLONG_MIN); \
+ } STMT_END
+
+/* Same as test_assert_strcmp expect that it takes an additional i as input.
+ * When i is greater than or equals 0 it is used to identify the barrage of
+ * tests failed like in test_assert_idx.
+*/
+#define test_assert_strcmp_idx(_s1, _s2, i) STMT_START { \
+ const char *_temp_s1 = (_s1); \
+ const char *_temp_s2 = (_s2); \
+ if ((null_strcmp(_temp_s1,_temp_s2) != 0)) \
+ test_assert_failed_strcmp_idx("strcmp(" #_s1 "," #_s2 ")", \
+ __FILE__, __LINE__, _temp_s1, _temp_s2, i); \
+ } STMT_END
+
+#define test_assert_cmp(_value1, _op, _value2) \
+ test_assert_cmp_idx(_value1, _op, _value2, LLONG_MIN)
+#define test_assert_cmp_idx(_value1, _op, _value2, _idx) STMT_START { \
+ intmax_t _temp_value1 = (_value1); \
+ intmax_t _temp_value2 = (_value2); \
+ if (!(_value1 _op _value2)) \
+ test_assert_failed_cmp_intmax_idx( \
+ #_value1 " " #_op " " #_value2, \
+ __FILE__, __LINE__, _temp_value1, _temp_value2, \
+ #_op, _idx); \
+ } STMT_END
+
+#define test_assert_ucmp(_value1, _op, _value2) \
+ test_assert_ucmp_idx(_value1, _op, _value2, LLONG_MIN)
+#define test_assert_ucmp_idx(_value1, _op, _value2, _idx) STMT_START { \
+ uintmax_t _temp_value1 = (_value1); \
+ uintmax_t _temp_value2 = (_value2); \
+ if (!(_value1 _op _value2)) \
+ test_assert_failed_ucmp_intmax_idx( \
+ #_value1 " " #_op " " #_value2, \
+ __FILE__, __LINE__, _temp_value1, _temp_value2, \
+ #_op, _idx); \
+ } STMT_END
+
+#ifdef STATIC_CHECKER
+# define ATTR_STATIC_CHECKER_NORETURN ATTR_NORETURN
+#else
+# define ATTR_STATIC_CHECKER_NORETURN
+#endif
+
+void test_assert_failed(const char *code, const char *file, unsigned int line)
+ ATTR_STATIC_CHECKER_NORETURN;
+void test_assert_failed_idx(const char *code, const char *file, unsigned int line, long long i)
+ ATTR_STATIC_CHECKER_NORETURN;
+void test_assert_failed_strcmp_idx(const char *code, const char *file, unsigned int line,
+ const char * src, const char * dst, long long i)
+ ATTR_STATIC_CHECKER_NORETURN;
+void test_assert_failed_cmp_intmax_idx(const char *code, const char *file,
+ unsigned int line,
+ intmax_t src, intmax_t dst,
+ const char *op, long long i)
+ ATTR_STATIC_CHECKER_NORETURN;
+void test_assert_failed_ucmp_intmax_idx(const char *code, const char *file,
+ unsigned int line,
+ uintmax_t src, uintmax_t dst,
+ const char *op, long long i)
+ ATTR_STATIC_CHECKER_NORETURN;
+bool test_has_failed(void);
+/* If you're testing nasty cases which you want to warn, surround the noisy op with these */
+void test_expect_errors(unsigned int expected);
+void test_expect_error_string(const char *substr); /* expect just 1 message matching the printf format */
+void test_expect_error_string_n_times(const char *substr, unsigned int times); /* expect just n messages matching the printf format */
+void test_expect_no_more_errors(void);
+/* Note that test_expect_error{s,_string}() effectively begin with a check equivalent
+ to test_expect_no_more_errors(), so you don't need the latter explicitly if following
+ it with either of the former.*/
+
+void test_end(void);
+
+void test_out(const char *name, bool success);
+void test_out_quiet(const char *name, bool success); /* only prints failures */
+void test_out_reason(const char *name, bool success, const char *reason)
+ ATTR_NULL(3);
+
+int test_run(void (*const test_functions[])(void)) ATTR_WARN_UNUSED_RESULT;
+struct named_test {
+ const char *name;
+ void (*func)(void);
+};
+int test_run_named(const struct named_test tests[], const char *match) ATTR_WARN_UNUSED_RESULT;
+
+#define TEST_DECL(x) void x(void);
+#define TEST_NAMELESS(x) x, /* Were you to want to use the X trick but not name the tests */
+#define TEST_NAMED(x) { .name = #x , .func = x },
+
+enum fatal_test_state {
+ FATAL_TEST_FINISHED, /* no more test stages, don't call again */
+ FATAL_TEST_FAILURE, /* single stage has failed, continue */
+ FATAL_TEST_ABORT, /* something's gone horrifically wrong */
+};
+/* The fatal function is called first with stage=0. After each call the stage
+ is increased by 1. The idea is that each stage would be running an
+ individual test that is supposed to crash. The function is called until
+ FATAL_TEST_FINISHED or FATAL_TEST_ABORT is returned. */
+typedef enum fatal_test_state test_fatal_func_t(unsigned int stage);
+
+typedef void test_fatal_callback_t(void *context);
+
+struct named_fatal {
+ const char *name;
+ test_fatal_func_t *func;
+};
+int test_run_with_fatals(void (*const test_functions[])(void),
+ test_fatal_func_t *const fatal_functions[]);
+int test_run_named_with_fatals(const char *match, const struct named_test tests[],
+ const struct named_fatal fatals[]);
+
+/* Require the Fatal/Panic string to match this or the fatal test fails. */
+void test_expect_fatal_string(const char *substr);
+/* Call the specified callback when a fatal is being triggered. This is mainly
+ intended to allow freeing memory so valgrind won't complain about memory
+ leaks. */
+void test_fatal_set_callback(test_fatal_callback_t *callback, void *context);
+#define test_fatal_set_callback(callback, context) \
+ test_fatal_set_callback(1 ? (test_fatal_callback_t *)callback : \
+ CALLBACK_TYPECHECK(callback, void (*)(typeof(context))), \
+ context)
+
+#define FATAL_DECL(x) enum fatal_test_state x(unsigned int);
+#define FATAL_NAMELESS(x) x, /* Were you to want to use the X trick but not name the tests */
+#define FATAL_NAMED(x) { .name = #x , .func = x },
+
+/* If a child is forked within test context, but wants to operate outside it,
+ then this will avoid valgrind leak errors */
+void test_forked_end(void);
+/* If a fork() wants to exit(), then this will avoid valgrind leak errors */
+void test_exit(int status) ATTR_NORETURN;
+
+#endif
diff --git a/src/lib-test/test-istream.c b/src/lib-test/test-istream.c
new file mode 100644
index 0000000..1d20748
--- /dev/null
+++ b/src/lib-test/test-istream.c
@@ -0,0 +1,166 @@
+/* Copyright (c) 2007-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "memarea.h"
+#include "istream-private.h"
+#include "test-common.h"
+
+struct test_istream {
+ struct istream_private istream;
+ const void *orig_buffer;
+ unsigned int skip_diff;
+ size_t max_pos;
+ bool allow_eof;
+};
+
+static void test_buffer_free(unsigned char *buf)
+{
+ i_free(buf);
+}
+
+static ssize_t test_read(struct istream_private *stream)
+{
+ struct test_istream *tstream = (struct test_istream *)stream;
+ unsigned int new_skip_diff;
+ size_t cur_max;
+ ssize_t ret;
+
+ i_assert(stream->skip <= stream->pos);
+
+ if (stream->pos - stream->skip >= tstream->istream.max_buffer_size) {
+ i_assert(stream->skip != stream->pos);
+ return -2;
+ }
+
+ if (tstream->max_pos < stream->pos) {
+ /* we seeked past the end of file. */
+ ret = 0;
+ } else {
+ /* copy data to a buffer in somewhat random place. this could
+ help catch bugs. */
+ new_skip_diff = i_rand_limit(128);
+ stream->skip = (stream->skip - tstream->skip_diff) +
+ new_skip_diff;
+ stream->pos = (stream->pos - tstream->skip_diff) +
+ new_skip_diff;
+ tstream->max_pos = (tstream->max_pos - tstream->skip_diff) +
+ new_skip_diff;
+ tstream->skip_diff = new_skip_diff;
+
+ cur_max = tstream->max_pos;
+ if (stream->max_buffer_size < SIZE_MAX - stream->skip &&
+ cur_max > stream->skip + stream->max_buffer_size)
+ cur_max = stream->skip + stream->max_buffer_size;
+
+ /* Reallocate the memory area if needed. Use exactly correct
+ buffer size so valgrind can catch read overflows. If a
+ correctly sized memarea already exists, use it only if
+ its refcount is 1. Otherwise with refcount>1 we could be
+ moving data within an existing memarea, which breaks
+ snapshots. */
+ if (cur_max > 0 && (stream->buffer_size != cur_max ||
+ stream->memarea == NULL ||
+ memarea_get_refcount(stream->memarea) > 1)) {
+ void *old_w_buffer = stream->w_buffer;
+ stream->w_buffer = i_malloc(cur_max);
+ if (stream->buffer_size != 0) {
+ memcpy(stream->w_buffer, old_w_buffer,
+ I_MIN(stream->buffer_size, cur_max));
+ }
+ stream->buffer = stream->w_buffer;
+ stream->buffer_size = cur_max;
+
+ if (stream->memarea != NULL)
+ memarea_unref(&stream->memarea);
+ stream->memarea = memarea_init(stream->w_buffer,
+ stream->buffer_size,
+ test_buffer_free,
+ stream->w_buffer);
+ }
+ ssize_t size = cur_max - new_skip_diff;
+ if (size > 0)
+ memcpy(stream->w_buffer + new_skip_diff,
+ tstream->orig_buffer, (size_t)size);
+
+ ret = cur_max - stream->pos;
+ stream->pos = cur_max;
+ }
+
+ if (ret > 0)
+ return ret;
+ else if (!tstream->allow_eof ||
+ stream->pos - tstream->skip_diff < (uoff_t)stream->statbuf.st_size)
+ return 0;
+ else {
+ stream->istream.eof = TRUE;
+ return -1;
+ }
+}
+
+static void test_seek(struct istream_private *stream, uoff_t v_offset,
+ bool mark ATTR_UNUSED)
+{
+ struct test_istream *tstream = (struct test_istream *)stream;
+
+ stream->istream.v_offset = v_offset;
+ stream->skip = v_offset + tstream->skip_diff;
+ stream->pos = stream->skip;
+}
+
+struct istream *test_istream_create_data(const void *data, size_t size)
+{
+ struct test_istream *tstream;
+
+ tstream = i_new(struct test_istream, 1);
+ tstream->orig_buffer = data;
+
+ tstream->istream.read = test_read;
+ tstream->istream.seek = test_seek;
+
+ tstream->istream.istream.blocking = FALSE;
+ tstream->istream.istream.seekable = TRUE;
+ i_stream_create(&tstream->istream, NULL, -1, 0);
+ tstream->istream.statbuf.st_size = tstream->max_pos = size;
+ tstream->allow_eof = TRUE;
+ tstream->istream.max_buffer_size = SIZE_MAX;
+ return &tstream->istream.istream;
+}
+
+struct istream *test_istream_create(const char *data)
+{
+ return test_istream_create_data(data, strlen(data));
+}
+
+static struct test_istream *test_istream_find(struct istream *input)
+{
+ struct istream *in;
+
+ for (in = input; in != NULL; in = in->real_stream->parent) {
+ if (in->real_stream->read == test_read)
+ return (struct test_istream *)in->real_stream;
+ }
+ i_panic("%s isn't test-istream", i_stream_get_name(input));
+}
+
+void test_istream_set_allow_eof(struct istream *input, bool allow)
+{
+ struct test_istream *tstream = test_istream_find(input);
+
+ tstream->allow_eof = allow;
+}
+
+void test_istream_set_max_buffer_size(struct istream *input, size_t size)
+{
+ struct test_istream *tstream = test_istream_find(input);
+
+ tstream->istream.max_buffer_size = size;
+}
+
+void test_istream_set_size(struct istream *input, uoff_t size)
+{
+ struct test_istream *tstream = test_istream_find(input);
+
+ if (size > (uoff_t)tstream->istream.statbuf.st_size)
+ size = (uoff_t)tstream->istream.statbuf.st_size;
+ tstream->max_pos = size + tstream->skip_diff;
+}
diff --git a/src/lib-test/test-ostream.c b/src/lib-test/test-ostream.c
new file mode 100644
index 0000000..4ca3a4a
--- /dev/null
+++ b/src/lib-test/test-ostream.c
@@ -0,0 +1,194 @@
+/* Copyright (c) 2016-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "buffer.h"
+#include "ostream-private.h"
+#include "test-common.h"
+
+struct test_ostream {
+ struct ostream_private ostream;
+ buffer_t *internal_buf;
+ buffer_t *output_buf;
+ size_t max_output_size;
+ struct timeout *to;
+ bool flush_pending;
+};
+
+static void o_stream_test_destroy(struct iostream_private *stream)
+{
+ struct test_ostream *tstream = (struct test_ostream *)stream;
+
+ timeout_remove(&tstream->to);
+ buffer_free(&tstream->internal_buf);
+}
+
+static int o_stream_test_flush(struct ostream_private *stream)
+{
+ struct test_ostream *tstream = (struct test_ostream *)stream;
+
+ if (tstream->internal_buf == NULL || tstream->internal_buf->used == 0)
+ return 1;
+ if (tstream->output_buf->used >= tstream->max_output_size)
+ return 0;
+
+ size_t left = tstream->max_output_size - tstream->output_buf->used;
+ size_t n = I_MIN(left, tstream->internal_buf->used);
+ buffer_append(tstream->output_buf, tstream->internal_buf->data, n);
+ buffer_delete(tstream->internal_buf, 0, n);
+ return tstream->internal_buf->used == 0 ? 1 : 0;
+}
+
+static ssize_t
+o_stream_test_sendv(struct ostream_private *stream,
+ const struct const_iovec *iov, unsigned int iov_count)
+{
+ struct test_ostream *tstream = (struct test_ostream *)stream;
+ struct const_iovec cur_iov = { NULL, 0 };
+ size_t left, n;
+ ssize_t ret = 0;
+ unsigned int i;
+
+ /* first we need to try to flush the internal buffer */
+ if ((ret = o_stream_test_flush(stream)) <= 0)
+ return ret;
+
+ /* append to output_buf until max_output_size is reached */
+ ret = 0;
+ for (i = 0; i < iov_count; i++) {
+ left = tstream->max_output_size < tstream->output_buf->used ? 0 :
+ tstream->max_output_size - tstream->output_buf->used;
+ n = I_MIN(left, iov[i].iov_len);
+ buffer_append(tstream->output_buf, iov[i].iov_base, n);
+ stream->ostream.offset += n;
+ ret += n;
+ if (n != iov[i].iov_len) {
+ cur_iov.iov_base = CONST_PTR_OFFSET(iov[i].iov_base, n);
+ cur_iov.iov_len = iov[i].iov_len - n;
+ break;
+ }
+ }
+ /* if we've internal_buf, append to it until max_buffer_size is
+ reached */
+ if (i == iov_count || tstream->internal_buf == NULL)
+ return ret;
+ do {
+ left = tstream->ostream.max_buffer_size -
+ tstream->internal_buf->used;
+ n = I_MIN(left, cur_iov.iov_len);
+ buffer_append(tstream->internal_buf, cur_iov.iov_base, n);
+ stream->ostream.offset += n;
+ ret += n;
+ if (n != cur_iov.iov_len)
+ break;
+ if (++i < iov_count)
+ cur_iov = iov[i];
+ } while (i < iov_count);
+
+ tstream->flush_pending = TRUE;
+ return ret;
+}
+
+static void test_ostream_send_more(struct test_ostream *tstream)
+{
+ struct ostream *ostream = &tstream->ostream.ostream;
+ int ret;
+
+ o_stream_ref(ostream);
+ tstream->flush_pending = FALSE;
+ if (tstream->ostream.callback != NULL)
+ ret = tstream->ostream.callback(tstream->ostream.context);
+ else
+ ret = o_stream_test_flush(&tstream->ostream);
+ if (ret == 0 || (tstream->internal_buf != NULL &&
+ tstream->internal_buf->used > 0))
+ tstream->flush_pending = TRUE;
+ if (!tstream->flush_pending ||
+ tstream->output_buf->used >= tstream->max_output_size)
+ timeout_remove(&tstream->to);
+ o_stream_unref(&ostream);
+}
+
+static void test_ostream_set_send_more_timeout(struct test_ostream *tstream)
+{
+ if (tstream->to == NULL && tstream->flush_pending &&
+ tstream->output_buf->used < tstream->max_output_size)
+ tstream->to = timeout_add_short(0, test_ostream_send_more, tstream);
+}
+
+static void
+o_stream_test_flush_pending(struct ostream_private *stream, bool set)
+{
+ struct test_ostream *tstream = (struct test_ostream *)stream;
+
+ if (tstream->internal_buf != NULL && tstream->internal_buf->used > 0) {
+ /* we have internal data, won't reset flush_pending */
+ i_assert(tstream->flush_pending);
+ } else {
+ tstream->flush_pending = set;
+ }
+ if (set)
+ test_ostream_set_send_more_timeout(tstream);
+}
+
+static size_t
+o_stream_test_get_buffer_used_size(const struct ostream_private *stream)
+{
+ const struct test_ostream *tstream =
+ (const struct test_ostream *)stream;
+
+ return tstream->internal_buf == NULL ? 0 :
+ tstream->internal_buf->used;
+}
+
+struct ostream *test_ostream_create(buffer_t *output)
+{
+ struct test_ostream *tstream;
+ struct ostream *ostream;
+
+ tstream = i_new(struct test_ostream, 1);
+ tstream->ostream.max_buffer_size = SIZE_MAX;
+ tstream->ostream.iostream.destroy = o_stream_test_destroy;
+ tstream->ostream.sendv = o_stream_test_sendv;
+ tstream->ostream.flush = o_stream_test_flush;
+ tstream->ostream.flush_pending = o_stream_test_flush_pending;
+ tstream->ostream.get_buffer_used_size =
+ o_stream_test_get_buffer_used_size;
+ tstream->ostream.ostream.blocking = TRUE;
+
+ tstream->output_buf = output;
+ tstream->max_output_size = SIZE_MAX;
+ ostream = o_stream_create(&tstream->ostream, NULL, -1);
+ o_stream_set_name(ostream, "(test-ostream)");
+ return ostream;
+}
+
+struct ostream *test_ostream_create_nonblocking(buffer_t *output,
+ size_t max_internal_buffer_size)
+{
+ struct test_ostream *tstream;
+
+ tstream = (struct test_ostream *)test_ostream_create(output)->real_stream;
+ tstream->internal_buf = buffer_create_dynamic(default_pool, 128);
+ tstream->ostream.ostream.blocking = FALSE;
+ tstream->ostream.max_buffer_size = max_internal_buffer_size;
+ return &tstream->ostream.ostream;
+}
+
+static struct test_ostream *test_ostream_find(struct ostream *output)
+{
+ struct ostream *out;
+
+ for (out = output; out != NULL; out = out->real_stream->parent) {
+ if (out->real_stream->sendv == o_stream_test_sendv)
+ return (struct test_ostream *)out->real_stream;
+ }
+ i_panic("%s isn't test-ostream", o_stream_get_name(output));
+}
+
+void test_ostream_set_max_output_size(struct ostream *output, size_t max_size)
+{
+ struct test_ostream *tstream = test_ostream_find(output);
+
+ tstream->max_output_size = max_size;
+ test_ostream_set_send_more_timeout(tstream);
+}
diff --git a/src/lib-test/test-subprocess.c b/src/lib-test/test-subprocess.c
new file mode 100644
index 0000000..eeaae80
--- /dev/null
+++ b/src/lib-test/test-subprocess.c
@@ -0,0 +1,392 @@
+/* Copyright (c) 2020 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "lib-signals.h"
+#include "hostpid.h"
+#include "array.h"
+#include "ioloop.h"
+#include "sleep.h"
+#include "test-common.h"
+#include "test-subprocess.h"
+
+#include <sys/types.h>
+#include <sys/wait.h>
+#include <unistd.h>
+
+struct test_subprocess {
+ pid_t pid;
+};
+
+volatile sig_atomic_t test_subprocess_is_child = 0;
+static bool test_subprocess_lib_init = FALSE;
+static volatile bool test_subprocess_notification_signal_received[SIGUSR1 + 1];
+static struct event *test_subprocess_event = NULL;
+static ARRAY(struct test_subprocess *) test_subprocesses = ARRAY_INIT;
+static void (*test_subprocess_cleanup_callback)(void) = NULL;
+
+static void
+test_subprocess_signal(const siginfo_t *si ATTR_UNUSED,
+ void *context ATTR_UNUSED)
+{
+ io_loop_stop(current_ioloop);
+}
+
+static void test_subprocess_free_all(void)
+{
+ struct test_subprocess *subp;
+
+ array_foreach_elem(&test_subprocesses, subp)
+ i_free(subp);
+ array_free(&test_subprocesses);
+}
+
+static void ATTR_NORETURN
+test_subprocess_child(int (*func)(void *context), void *context,
+ bool continue_test)
+{
+ int ret;
+
+ if (!continue_test)
+ test_forked_end();
+
+ hostpid_init();
+
+ lib_signals_deinit();
+ lib_signals_init();
+
+ lib_signals_set_handler(SIGTERM,
+ LIBSIG_FLAG_DELAYED | LIBSIG_FLAG_IOLOOP_AUTOMOVE,
+ test_subprocess_signal, NULL);
+ lib_signals_set_handler(SIGQUIT,
+ LIBSIG_FLAG_DELAYED | LIBSIG_FLAG_IOLOOP_AUTOMOVE,
+ test_subprocess_signal, NULL);
+ lib_signals_set_handler(SIGINT,
+ LIBSIG_FLAG_DELAYED | LIBSIG_FLAG_IOLOOP_AUTOMOVE,
+ test_subprocess_signal, NULL);
+
+ ret = func(context);
+
+ /* Prevent race condition */
+ lib_signals_clear_handlers_and_ignore(SIGTERM);
+
+ event_unref(&test_subprocess_event);
+ lib_signals_deinit();
+
+ if (!continue_test) {
+ lib_deinit();
+ lib_exit(ret);
+ }
+ test_exit((test_has_failed() ? 1 : 0));
+}
+
+#undef test_subprocess_fork
+void test_subprocess_fork(int (*func)(void *context), void *context,
+ bool continue_test)
+{
+ struct test_subprocess *subprocess;
+
+ subprocess = i_new(struct test_subprocess, 1);
+
+ lib_signals_ioloop_detach();
+
+ /* avoid races: fork the child process with test_subprocess_is_child
+ set to 1 in case it immediately receives a signal. */
+ test_subprocess_is_child = 1;
+ if ((subprocess->pid = fork()) == (pid_t)-1)
+ i_fatal("test: sub-process: fork() failed: %m");
+ if (subprocess->pid == 0) {
+ /* cannot include this in the list to avoid accidental
+ * kill of PID 0, so just free it here explicitly. */
+ i_free(subprocess);
+ test_subprocess_free_all();
+
+ test_subprocess_child(func, context, continue_test);
+ i_unreached();
+ }
+ test_subprocess_is_child = 0;
+
+ array_push_back(&test_subprocesses, &subprocess);
+ lib_signals_ioloop_attach();
+}
+
+static void test_subprocess_verify_exit_status(int status)
+{
+ test_out_quiet("sub-process ended properly",
+ WIFEXITED(status) && WEXITSTATUS(status) == 0);
+ if (WIFEXITED(status)) {
+ if (WEXITSTATUS(status) != 0) {
+ e_warning(test_subprocess_event,
+ "Sub-process exited with status %d",
+ WEXITSTATUS(status));
+ }
+ } else if (WIFSIGNALED(status)) {
+ e_warning(test_subprocess_event,
+ "Sub-process forcibly terminated with signal %d",
+ WTERMSIG(status));
+ } else if (WIFSTOPPED(status)) {
+ e_warning(test_subprocess_event,
+ "Sub-process stopped with signal %d",
+ WSTOPSIG(status));
+ } else {
+ e_warning(test_subprocess_event,
+ "Sub-process terminated abnormally with status %d",
+ status);
+ }
+}
+
+static void test_subprocess_kill_forced(struct test_subprocess *subp)
+{
+ i_assert(subp->pid > 0);
+ (void)kill(subp->pid, SIGKILL);
+ (void)waitpid(subp->pid, NULL, 0);
+}
+
+void test_subprocess_kill_all(unsigned int timeout_secs)
+{
+ struct test_subprocess **subps;
+ unsigned int subps_count, subps_left, i;
+
+ subps = array_get_modifiable(&test_subprocesses, &subps_count);
+
+ /* Request children to terminate gently */
+ for (i = 0; i < subps_count; i++) {
+ if (subps[i] == NULL || subps[i]->pid == (pid_t)-1)
+ continue;
+
+ e_debug(test_subprocess_event,
+ "Terminating sub-process [%u]", i);
+ if (kill(subps[i]->pid, SIGTERM) < 0) {
+ e_error(test_subprocess_event,
+ "Failed to kill sub-process [%u] with SIGTERM: "
+ "%m", i);
+ }
+ }
+
+ /* Wait for children */
+ subps_left = subps_count;
+ while (subps_left > 0) {
+ int status;
+ pid_t wret = (pid_t)-1;
+
+ alarm(timeout_secs);
+ wret = waitpid(-1, &status, 0);
+ alarm(0);
+
+ test_assert(wret > 0);
+ if (wret < 0 && errno == EINTR)
+ e_warning(test_subprocess_event,
+ "Wait for sub-processes timed out");
+ if (wret > 0)
+ test_subprocess_verify_exit_status(status);
+
+ if (wret == 0)
+ break;
+ if (wret < 0) {
+ if (errno == ECHILD)
+ continue;
+ e_warning(test_subprocess_event,
+ "Wait for sub-processes failed: %m");
+ break;
+ }
+ for (i = 0; i < subps_count; i++) {
+ if (subps[i] == NULL || subps[i]->pid != wret)
+ continue;
+ e_debug(test_subprocess_event,
+ "Terminated sub-process [%u]", i);
+ i_free(subps[i]);
+ subps_left--;
+ }
+ }
+
+ /* Kill disobedient ones with fire */
+ for (i = 0; i < subps_count; i++) {
+ if (subps[i] == NULL || subps[i]->pid == (pid_t)-1)
+ continue;
+ e_warning(test_subprocess_event,
+ "Forcibly killed sub-process [%u]", i);
+ test_subprocess_kill_forced(subps[i]);
+ i_assert(subps_left > 0);
+ i_free(subps[i]);
+ subps_left--;
+ }
+ i_assert(subps_left == 0);
+
+ array_clear(&test_subprocesses);
+}
+
+static void test_subprocess_kill_all_forced(void)
+{
+ struct test_subprocess **subps;
+ unsigned int subps_count, i;
+
+ if (!array_is_created(&test_subprocesses))
+ return;
+
+ /* This is also called from signal handler context, so no debug logging
+ here.
+ */
+
+ subps = array_get_modifiable(&test_subprocesses, &subps_count);
+
+ if (subps_count == 0)
+ return;
+
+ for (i = 0; i < subps_count; i++) {
+ if (subps[i] == NULL || subps[i]->pid == (pid_t)-1)
+ continue;
+ test_subprocess_kill_forced(subps[i]);
+ subps[i]->pid = (pid_t)-1;
+ }
+}
+
+/*
+ * Main
+ */
+
+volatile sig_atomic_t terminating = 0;
+
+static void test_subprocess_cleanup(void)
+{
+ if (test_subprocess_is_child != 0) {
+ /* Child processes must not execute the cleanups */
+ return;
+ }
+
+ /* We get here when the test ended normally, badly failed, crashed,
+ terminated, or executed exit() unexpectedly. The cleanups performed
+ here are important and must be executed at all times. */
+
+ /* While any unfreed memory will be handled by the system, lingering
+ child processes will not be handled so well. So, we need to make sure
+ here that we don't leave any pesky child processes alive. */
+ test_subprocess_kill_all_forced();
+
+ /* Perform any additional important cleanup specific to the test. */
+ if (test_subprocess_cleanup_callback != NULL)
+ test_subprocess_cleanup_callback();
+}
+
+static void
+test_subprocess_alarm(const siginfo_t *si ATTR_UNUSED,
+ void *context ATTR_UNUSED)
+{
+ /* We use alarm() to implement a simple timeout on waitpid(), which will
+ exit with EINTR when SIGALRM is received. This handler overrides the
+ default SIGALRM handler, so that the process is not killed and no
+ messages are printed to terminal.
+ */
+}
+
+static void
+test_subprocess_terminate(const siginfo_t *si, void *context ATTR_UNUSED)
+{
+ int signo = si->si_signo;
+
+ if (terminating != 0)
+ raise(signo);
+ terminating = 1;
+
+ /* Perform important cleanups */
+ test_subprocess_cleanup();
+
+ (void)signal(signo, SIG_DFL);
+ if (signo == SIGTERM)
+ _exit(0);
+ else
+ raise(signo);
+}
+
+static void test_atexit(void)
+{
+ /* NOTICE: This is also called by children, so be careful. */
+
+ /* Perform important cleanups */
+ test_subprocess_cleanup();
+}
+
+void test_subprocess_set_cleanup_callback(void (*callback)(void))
+{
+ test_subprocess_cleanup_callback = callback;
+}
+
+void test_subprocess_notify_signal_send(int signo, pid_t pid)
+{
+ if (kill(pid, signo) < 0)
+ i_fatal("kill(%ld, SIGHUP) failed: %m", (long)pid);
+}
+
+void test_subprocess_notify_signal_send_parent(int signo)
+{
+ test_subprocess_notify_signal_send(signo, getppid());
+}
+
+void test_subprocess_notify_signal_reset(int signo)
+{
+ i_assert(signo >= 0 &&
+ (unsigned int)signo < N_ELEMENTS(test_subprocess_notification_signal_received));
+ test_subprocess_notification_signal_received[signo] = FALSE;
+}
+
+void test_subprocess_notify_signal_wait(int signo, unsigned int timeout_msecs)
+{
+ unsigned int i, count = timeout_msecs / 10;
+
+ for (i = 0; i < count; i++) {
+ if (test_subprocess_notification_signal_received[signo])
+ return;
+ i_sleep_msecs(10);
+ }
+ i_fatal("Didn't receive wait notification signal from server");
+}
+
+static void
+test_subprocess_notification_signal(const siginfo_t *si,
+ void *context ATTR_UNUSED)
+{
+ int signo = si->si_signo;
+
+ i_assert(signo >= 0 &&
+ (unsigned int)signo < N_ELEMENTS(test_subprocess_notification_signal_received));
+ test_subprocess_notification_signal_received[signo] = TRUE;
+}
+
+void test_subprocesses_init(bool debug)
+{
+ if (!lib_is_initialized()) {
+ lib_init();
+ test_subprocess_lib_init = TRUE;
+ }
+ lib_signals_init();
+
+ atexit(test_atexit);
+ lib_signals_ignore(SIGPIPE, TRUE);
+ lib_signals_set_handler(SIGALRM, 0, test_subprocess_alarm, NULL);
+ lib_signals_set_handler(SIGTERM, 0, test_subprocess_terminate, NULL);
+ lib_signals_set_handler(SIGQUIT, 0, test_subprocess_terminate, NULL);
+ lib_signals_set_handler(SIGINT, 0, test_subprocess_terminate, NULL);
+ lib_signals_set_handler(SIGSEGV, 0, test_subprocess_terminate, NULL);
+ lib_signals_set_handler(SIGABRT, 0, test_subprocess_terminate, NULL);
+ lib_signals_set_handler(SIGHUP, LIBSIG_FLAG_RESTART,
+ test_subprocess_notification_signal, NULL);
+ lib_signals_set_handler(SIGUSR1, LIBSIG_FLAG_RESTART,
+ test_subprocess_notification_signal, NULL);
+
+ i_array_init(&test_subprocesses, 8);
+
+ test_subprocess_event = event_create(NULL);
+ event_set_forced_debug(test_subprocess_event, debug);
+ event_set_append_log_prefix(test_subprocess_event, "test: ");
+}
+
+void test_subprocesses_deinit(void)
+{
+ test_subprocess_cleanup();
+ test_subprocess_free_all();
+ array_free(&test_subprocesses);
+
+ event_unref(&test_subprocess_event);
+ lib_signals_deinit();
+
+ if (test_subprocess_lib_init)
+ lib_deinit();
+}
diff --git a/src/lib-test/test-subprocess.h b/src/lib-test/test-subprocess.h
new file mode 100644
index 0000000..1a8d174
--- /dev/null
+++ b/src/lib-test/test-subprocess.h
@@ -0,0 +1,41 @@
+#ifndef TEST_SUBPROCESS_H
+#define TEST_SUBPROCESS_H
+
+struct test_subprocess;
+
+/* Fork a sub-process for this test. The func is the main function for the
+ forked sub-process. The provided context is passed to the provided function.
+ When continue_test=FALSE, the test is ended immediately in the sub-process,
+ otherwise, the test continues and its result is used to set the exit code
+ when the process ends gracefully. */
+void test_subprocess_fork(int (*func)(void *), void *context,
+ bool continue_test);
+#define test_subprocess_fork(func, context, continue_test) \
+ test_subprocess_fork( \
+ (int(*)(void*))func, \
+ (TRUE ? context : \
+ CALLBACK_TYPECHECK(func, int(*)(typeof(context)))), \
+ continue_test)
+
+void test_subprocess_kill_all(unsigned int timeout_secs);
+
+/* Set a cleanup callback that is executed even when the test program crashes or
+ exit()s unexpectedly. Note that this may be run in signal context. */
+void test_subprocess_set_cleanup_callback(void (*callback)(void));
+
+/* Send a notification signal (SIGHUP) to the given PID */
+void test_subprocess_notify_signal_send(int signo, pid_t pid);
+/* Send a notificatino signal to the parent process. */
+void test_subprocess_notify_signal_send_parent(int signo);
+/* Reset any previously sent notification signals. */
+void test_subprocess_notify_signal_reset(int signo);
+/* Wait until a notification signal is sent, or return immediately if it was
+ already sent. test_subprocess_notify_signal_reset() should be called before
+ this to make sure it's not returning due to a previously sent signal.
+ If the timeout is reached, i_fatal() is called. */
+void test_subprocess_notify_signal_wait(int signo, unsigned int timeout_msecs);
+
+void test_subprocesses_init(bool debug);
+void test_subprocesses_deinit(void);
+
+#endif
diff --git a/src/lib/Makefile.am b/src/lib/Makefile.am
new file mode 100644
index 0000000..ac5c62b
--- /dev/null
+++ b/src/lib/Makefile.am
@@ -0,0 +1,481 @@
+AM_CPPFLAGS = \
+ $(LIBUNWIND_CFLAGS)
+
+noinst_LTLIBRARIES = liblib.la
+
+BUILT_SOURCES = $(srcdir)/unicodemap.c \
+ event-filter-lexer.c \
+ event-filter-parser.c \
+ event-filter-parser.h
+
+EXTRA_DIST = unicodemap.c unicodemap.pl UnicodeData.txt
+
+# Squelch autoconf error about using .[ly] sources but not defining $(LEX)
+# and $(YACC). Using false here avoids accidental use.
+LEX=/bin/false
+YACC=/bin/false
+
+# We use custom rules here because we want to use flex and bison instead
+# of lex and yacc (or bison in yacc-compatibility mode). Both flex and
+# bison can handle properly naming the generated files, and it is simpler
+# and cleaner to make this rule ourselves instead of working around ylwrap
+# and yywrap's antiquated notion of what is hapenning.
+.l.c:
+ $(AM_V_GEN)$(FLEX) -o $@ $<
+
+.y.c:
+ $(AM_V_GEN)$(BISON) -o $@ $<
+
+# Bison generates both a header and a .c file. Without the following
+# dependency, anything including the header will race the bison process.
+event-filter-parser.h: event-filter-parser.c
+
+$(srcdir)/UnicodeData.txt:
+ test -f $@ || wget -O $@ https://dovecot.org/res/UnicodeData.txt
+
+$(srcdir)/unicodemap.c: $(srcdir)/unicodemap.pl $(srcdir)/UnicodeData.txt
+ perl $(srcdir)/unicodemap.pl < $(srcdir)/UnicodeData.txt > $@
+
+liblib_la_LIBADD = $(LIBUNWIND_LIBS)
+liblib_la_SOURCES = \
+ array.c \
+ aqueue.c \
+ askpass.c \
+ backtrace-string.c \
+ base32.c \
+ base64.c \
+ bits.c \
+ bsearch-insert-pos.c \
+ buffer.c \
+ buffer-istream.c \
+ child-wait.c \
+ compat.c \
+ connection.c \
+ cpu-limit.c \
+ crc32.c \
+ data-stack.c \
+ eacces-error.c \
+ env-util.c \
+ event-filter.c \
+ event-filter-lexer.l \
+ event-filter-parser.y \
+ event-log.c \
+ execv-const.c \
+ failures.c \
+ fd-util.c \
+ fdatasync-path.c \
+ fdpass.c \
+ file-cache.c \
+ file-create-locked.c \
+ file-copy.c \
+ file-dotlock.c \
+ file-lock.c \
+ file-set-size.c \
+ guid.c \
+ hash.c \
+ hash-format.c \
+ hash-method.c \
+ hash2.c \
+ hex-binary.c \
+ hex-dec.c \
+ hmac.c \
+ hmac-cram-md5.c \
+ home-expand.c \
+ hook-build.c \
+ hostpid.c \
+ imem.c \
+ ipwd.c \
+ iostream.c \
+ iostream-pump.c \
+ iostream-proxy.c \
+ iostream-rawlog.c \
+ iostream-temp.c \
+ iso8601-date.c \
+ istream.c \
+ istream-base64-decoder.c \
+ istream-base64-encoder.c \
+ istream-callback.c \
+ istream-chain.c \
+ istream-concat.c \
+ istream-crlf.c \
+ istream-data.c \
+ istream-failure-at.c \
+ istream-file.c \
+ istream-hash.c \
+ istream-jsonstr.c \
+ istream-limit.c \
+ istream-multiplex.c \
+ istream-rawlog.c \
+ istream-seekable.c \
+ istream-sized.c \
+ istream-tee.c \
+ istream-try.c \
+ istream-timeout.c \
+ istream-unix.c \
+ ioloop.c \
+ ioloop-iolist.c \
+ ioloop-notify-none.c \
+ ioloop-notify-fd.c \
+ ioloop-notify-inotify.c \
+ ioloop-notify-kqueue.c \
+ ioloop-poll.c \
+ ioloop-select.c \
+ ioloop-epoll.c \
+ ioloop-kqueue.c \
+ json-parser.c \
+ json-tree.c \
+ lib.c \
+ lib-event.c \
+ lib-signals.c \
+ log-throttle.c \
+ md4.c \
+ md5.c \
+ memarea.c \
+ mempool.c \
+ mempool-allocfree.c \
+ mempool-alloconly.c \
+ mempool-datastack.c \
+ mempool-system.c \
+ mempool-unsafe-datastack.c \
+ mkdir-parents.c \
+ mmap-anon.c \
+ mmap-util.c \
+ module-dir.c \
+ mountpoint.c \
+ net.c \
+ nfs-workarounds.c \
+ numpack.c \
+ ostream.c \
+ ostream-buffer.c \
+ ostream-failure-at.c \
+ ostream-file.c \
+ ostream-hash.c \
+ ostream-multiplex.c \
+ ostream-null.c \
+ ostream-rawlog.c \
+ ostream-unix.c \
+ ostream-wrapper.c \
+ path-util.c \
+ pkcs5.c \
+ primes.c \
+ printf-format-fix.c \
+ process-stat.c \
+ process-title.c \
+ priorityq.c \
+ randgen.c \
+ rand.c \
+ read-full.c \
+ restrict-access.c \
+ restrict-process-size.c \
+ safe-memset.c \
+ safe-mkdir.c \
+ safe-mkstemp.c \
+ sendfile-util.c \
+ seq-range-array.c \
+ seq-set-builder.c \
+ sha1.c \
+ sha2.c \
+ sha3.c \
+ sleep.c \
+ sort.c \
+ stats-dist.c \
+ str.c \
+ str-find.c \
+ str-sanitize.c \
+ str-table.c \
+ strescape.c \
+ strfuncs.c \
+ strnum.c \
+ time-util.c \
+ unix-socket-create.c \
+ unlink-directory.c \
+ unlink-old-files.c \
+ unichar.c \
+ uri-util.c \
+ utc-offset.c \
+ utc-mktime.c \
+ var-expand.c \
+ var-expand-if.c \
+ wildcard-match.c \
+ write-full.c
+
+headers = \
+ aqueue.h \
+ array.h \
+ array-decl.h \
+ askpass.h \
+ backtrace-string.h \
+ base32.h \
+ base64.h \
+ bits.h \
+ bsearch-insert-pos.h \
+ buffer.h \
+ byteorder.h \
+ child-wait.h \
+ compat.h \
+ connection.h \
+ cpu-limit.h \
+ crc32.h \
+ data-stack.h \
+ eacces-error.h \
+ env-util.h \
+ event-filter.h \
+ event-filter-parser.h \
+ event-filter-private.h \
+ event-log.h \
+ execv-const.h \
+ failures.h \
+ failures-private.h \
+ fd-util.h \
+ fdatasync-path.h \
+ fdpass.h \
+ file-cache.h \
+ file-create-locked.h \
+ file-copy.h \
+ file-dotlock.h \
+ file-lock.h \
+ file-set-size.h \
+ fsync-mode.h \
+ guid.h \
+ hash.h \
+ hash-decl.h \
+ hash-format.h \
+ hash-method.h \
+ hash2.h \
+ hex-binary.h \
+ hex-dec.h \
+ hmac.h \
+ hmac-cram-md5.h \
+ home-expand.h \
+ hook-build.h \
+ hostpid.h \
+ imem.h \
+ ipwd.h \
+ iostream.h \
+ iostream-private.h \
+ iostream-pump.h \
+ iostream-proxy.h \
+ iostream-rawlog.h \
+ iostream-rawlog-private.h \
+ iostream-temp.h \
+ iso8601-date.h \
+ istream.h \
+ istream-base64.h \
+ istream-callback.h \
+ istream-chain.h \
+ istream-concat.h \
+ istream-crlf.h \
+ istream-failure-at.h \
+ istream-file-private.h \
+ istream-hash.h \
+ istream-jsonstr.h \
+ istream-multiplex.h \
+ istream-private.h \
+ istream-rawlog.h \
+ istream-seekable.h \
+ istream-sized.h \
+ istream-tee.h \
+ istream-try.h \
+ istream-timeout.h \
+ istream-unix.h \
+ ioloop.h \
+ ioloop-iolist.h \
+ ioloop-private.h \
+ ioloop-notify-fd.h \
+ json-parser.h \
+ json-tree.h \
+ lib.h \
+ lib-event.h \
+ lib-event-private.h \
+ lib-signals.h \
+ llist.h \
+ log-throttle.h \
+ macros.h \
+ md4.h \
+ md5.h \
+ malloc-overflow.h \
+ memarea.h \
+ mempool.h \
+ mkdir-parents.h \
+ mmap-util.h \
+ module-context.h \
+ module-dir.h \
+ mountpoint.h \
+ net.h \
+ nfs-workarounds.h \
+ numpack.h \
+ ostream.h \
+ ostream-failure-at.h \
+ ostream-file-private.h \
+ ostream-hash.h \
+ ostream-multiplex.h \
+ ostream-private.h \
+ ostream-null.h \
+ ostream-rawlog.h \
+ ostream-unix.h \
+ ostream-wrapper.h \
+ path-util.h \
+ pkcs5.h \
+ primes.h \
+ printf-format-fix.h \
+ process-stat.h \
+ process-title.h \
+ priorityq.h \
+ randgen.h \
+ read-full.h \
+ restrict-access.h \
+ restrict-process-size.h \
+ safe-memset.h \
+ safe-mkdir.h \
+ safe-mkstemp.h \
+ sendfile-util.h \
+ seq-range-array.h \
+ seq-set-builder.h \
+ sha-common.h \
+ sha1.h \
+ sha2.h \
+ sha3.h \
+ sleep.h \
+ sort.h \
+ stats-dist.h \
+ str.h \
+ str-find.h \
+ str-sanitize.h \
+ str-table.h \
+ strescape.h \
+ strfuncs.h \
+ strnum.h \
+ time-util.h \
+ unix-socket-create.h \
+ unlink-directory.h \
+ unlink-old-files.h \
+ unichar.h \
+ uri-util.h \
+ utc-offset.h \
+ utc-mktime.h \
+ var-expand.h \
+ var-expand-private.h \
+ wildcard-match.h \
+ write-full.h
+
+test_programs = test-lib
+noinst_PROGRAMS = $(test_programs)
+
+test_lib_CPPFLAGS = \
+ -I$(top_srcdir)/src/lib-test
+
+test_libs = \
+ ../lib-test/libtest.la \
+ liblib.la
+
+test_lib_SOURCES = \
+ test-lib.c \
+ test-array.c \
+ test-aqueue.c \
+ test-backtrace.c \
+ test-base32.c \
+ test-base64.c \
+ test-bits.c \
+ test-bsearch-insert-pos.c \
+ test-buffer.c \
+ test-buffer-istream.c \
+ test-byteorder.c \
+ test-connection.c \
+ test-crc32.c \
+ test-cpu-limit.c \
+ test-data-stack.c \
+ test-env-util.c \
+ test-event-category-register.c \
+ test-event-filter.c \
+ test-event-filter-expr.c \
+ test-event-filter-merge.c \
+ test-event-filter-parser.c \
+ test-event-flatten.c \
+ test-event-log.c \
+ test-failures.c \
+ test-fd-util.c \
+ test-file-cache.c \
+ test-file-create-locked.c \
+ test-guid.c \
+ test-hash.c \
+ test-hash-format.c \
+ test-hash-method.c \
+ test-hmac.c \
+ test-hex-binary.c \
+ test-imem.c \
+ test-ioloop.c \
+ test-iso8601-date.c \
+ test-iostream-pump.c \
+ test-iostream-proxy.c \
+ test-iostream-temp.c \
+ test-istream.c \
+ test-istream-base64-decoder.c \
+ test-istream-base64-encoder.c \
+ test-istream-chain.c \
+ test-istream-concat.c \
+ test-istream-crlf.c \
+ test-istream-failure-at.c \
+ test-istream-jsonstr.c \
+ test-istream-multiplex.c \
+ test-istream-seekable.c \
+ test-istream-sized.c \
+ test-istream-tee.c \
+ test-istream-try.c \
+ test-istream-unix.c \
+ test-json-parser.c \
+ test-json-tree.c \
+ test-lib-event.c \
+ test-lib-signals.c \
+ test-llist.c \
+ test-log-throttle.c \
+ test-macros.c \
+ test-malloc-overflow.c \
+ test-memarea.c \
+ test-mempool.c \
+ test-mempool-allocfree.c \
+ test-mempool-alloconly.c \
+ test-pkcs5.c \
+ test-net.c \
+ test-numpack.c \
+ test-ostream-buffer.c \
+ test-ostream-failure-at.c \
+ test-ostream-file.c \
+ test-ostream-multiplex.c \
+ test-multiplex.c \
+ test-path-util.c \
+ test-primes.c \
+ test-printf-format-fix.c \
+ test-priorityq.c \
+ test-random.c \
+ test-seq-range-array.c \
+ test-seq-set-builder.c \
+ test-stats-dist.c \
+ test-str.c \
+ test-strescape.c \
+ test-strfuncs.c \
+ test-strnum.c \
+ test-str-find.c \
+ test-str-sanitize.c \
+ test-str-table.c \
+ test-time-util.c \
+ test-unichar.c \
+ test-utc-mktime.c \
+ test-uri.c \
+ test-var-expand.c \
+ test-wildcard-match.c
+
+test_headers = \
+ test-lib.h \
+ test-lib.inc
+
+test_lib_LDADD = $(test_libs) -lm
+test_lib_DEPENDENCIES = $(test_libs)
+
+check-local:
+ for bin in $(test_programs); do \
+ if ! $(RUN_TEST) ./$$bin; then exit 1; fi; \
+ done
+
+pkginc_libdir=$(pkgincludedir)
+pkginc_lib_HEADERS = $(headers)
+noinst_HEADERS = $(test_headers)
diff --git a/src/lib/Makefile.in b/src/lib/Makefile.in
new file mode 100644
index 0000000..a7d8e77
--- /dev/null
+++ b/src/lib/Makefile.in
@@ -0,0 +1,3709 @@
+# Makefile.in generated by automake 1.16.1 from Makefile.am.
+# @configure_input@
+
+# Copyright (C) 1994-2018 Free Software Foundation, Inc.
+
+# This Makefile.in is free software; the Free Software Foundation
+# gives unlimited permission to copy and/or distribute it,
+# with or without modifications, as long as this notice is preserved.
+
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY, to the extent permitted by law; without
+# even the implied warranty of MERCHANTABILITY or FITNESS FOR A
+# PARTICULAR PURPOSE.
+
+@SET_MAKE@
+
+
+
+VPATH = @srcdir@
+am__is_gnu_make = { \
+ if test -z '$(MAKELEVEL)'; then \
+ false; \
+ elif test -n '$(MAKE_HOST)'; then \
+ true; \
+ elif test -n '$(MAKE_VERSION)' && test -n '$(CURDIR)'; then \
+ true; \
+ else \
+ false; \
+ fi; \
+}
+am__make_running_with_option = \
+ case $${target_option-} in \
+ ?) ;; \
+ *) echo "am__make_running_with_option: internal error: invalid" \
+ "target option '$${target_option-}' specified" >&2; \
+ exit 1;; \
+ esac; \
+ has_opt=no; \
+ sane_makeflags=$$MAKEFLAGS; \
+ if $(am__is_gnu_make); then \
+ sane_makeflags=$$MFLAGS; \
+ else \
+ case $$MAKEFLAGS in \
+ *\\[\ \ ]*) \
+ bs=\\; \
+ sane_makeflags=`printf '%s\n' "$$MAKEFLAGS" \
+ | sed "s/$$bs$$bs[$$bs $$bs ]*//g"`;; \
+ esac; \
+ fi; \
+ skip_next=no; \
+ strip_trailopt () \
+ { \
+ flg=`printf '%s\n' "$$flg" | sed "s/$$1.*$$//"`; \
+ }; \
+ for flg in $$sane_makeflags; do \
+ test $$skip_next = yes && { skip_next=no; continue; }; \
+ case $$flg in \
+ *=*|--*) continue;; \
+ -*I) strip_trailopt 'I'; skip_next=yes;; \
+ -*I?*) strip_trailopt 'I';; \
+ -*O) strip_trailopt 'O'; skip_next=yes;; \
+ -*O?*) strip_trailopt 'O';; \
+ -*l) strip_trailopt 'l'; skip_next=yes;; \
+ -*l?*) strip_trailopt 'l';; \
+ -[dEDm]) skip_next=yes;; \
+ -[JT]) skip_next=yes;; \
+ esac; \
+ case $$flg in \
+ *$$target_option*) has_opt=yes; break;; \
+ esac; \
+ done; \
+ test $$has_opt = yes
+am__make_dryrun = (target_option=n; $(am__make_running_with_option))
+am__make_keepgoing = (target_option=k; $(am__make_running_with_option))
+pkgdatadir = $(datadir)/@PACKAGE@
+pkgincludedir = $(includedir)/@PACKAGE@
+pkglibdir = $(libdir)/@PACKAGE@
+pkglibexecdir = $(libexecdir)/@PACKAGE@
+am__cd = CDPATH="$${ZSH_VERSION+.}$(PATH_SEPARATOR)" && cd
+install_sh_DATA = $(install_sh) -c -m 644
+install_sh_PROGRAM = $(install_sh) -c
+install_sh_SCRIPT = $(install_sh) -c
+INSTALL_HEADER = $(INSTALL_DATA)
+transform = $(program_transform_name)
+NORMAL_INSTALL = :
+PRE_INSTALL = :
+POST_INSTALL = :
+NORMAL_UNINSTALL = :
+PRE_UNINSTALL = :
+POST_UNINSTALL = :
+build_triplet = @build@
+host_triplet = @host@
+noinst_PROGRAMS = $(am__EXEEXT_1)
+subdir = src/lib
+ACLOCAL_M4 = $(top_srcdir)/aclocal.m4
+am__aclocal_m4_deps = $(top_srcdir)/m4/ac_checktype2.m4 \
+ $(top_srcdir)/m4/ac_typeof.m4 $(top_srcdir)/m4/arc4random.m4 \
+ $(top_srcdir)/m4/blockdev.m4 $(top_srcdir)/m4/c99_vsnprintf.m4 \
+ $(top_srcdir)/m4/clock_gettime.m4 $(top_srcdir)/m4/crypt.m4 \
+ $(top_srcdir)/m4/crypt_xpg6.m4 $(top_srcdir)/m4/dbqlk.m4 \
+ $(top_srcdir)/m4/dirent_dtype.m4 $(top_srcdir)/m4/dovecot.m4 \
+ $(top_srcdir)/m4/fd_passing.m4 $(top_srcdir)/m4/fdatasync.m4 \
+ $(top_srcdir)/m4/flexible_array_member.m4 \
+ $(top_srcdir)/m4/glibc.m4 $(top_srcdir)/m4/gmtime_max.m4 \
+ $(top_srcdir)/m4/gmtime_tm_gmtoff.m4 \
+ $(top_srcdir)/m4/ioloop.m4 $(top_srcdir)/m4/iovec.m4 \
+ $(top_srcdir)/m4/ipv6.m4 $(top_srcdir)/m4/libcap.m4 \
+ $(top_srcdir)/m4/libtool.m4 $(top_srcdir)/m4/libwrap.m4 \
+ $(top_srcdir)/m4/linux_mremap.m4 $(top_srcdir)/m4/ltoptions.m4 \
+ $(top_srcdir)/m4/ltsugar.m4 $(top_srcdir)/m4/ltversion.m4 \
+ $(top_srcdir)/m4/lt~obsolete.m4 $(top_srcdir)/m4/mmap_write.m4 \
+ $(top_srcdir)/m4/mntctl.m4 $(top_srcdir)/m4/modules.m4 \
+ $(top_srcdir)/m4/notify.m4 $(top_srcdir)/m4/nsl.m4 \
+ $(top_srcdir)/m4/off_t_max.m4 $(top_srcdir)/m4/pkg.m4 \
+ $(top_srcdir)/m4/pr_set_dumpable.m4 \
+ $(top_srcdir)/m4/q_quotactl.m4 $(top_srcdir)/m4/quota.m4 \
+ $(top_srcdir)/m4/random.m4 $(top_srcdir)/m4/rlimit.m4 \
+ $(top_srcdir)/m4/sendfile.m4 $(top_srcdir)/m4/size_t_signed.m4 \
+ $(top_srcdir)/m4/sockpeercred.m4 $(top_srcdir)/m4/sql.m4 \
+ $(top_srcdir)/m4/ssl.m4 $(top_srcdir)/m4/st_tim.m4 \
+ $(top_srcdir)/m4/static_array.m4 $(top_srcdir)/m4/test_with.m4 \
+ $(top_srcdir)/m4/time_t.m4 $(top_srcdir)/m4/typeof.m4 \
+ $(top_srcdir)/m4/typeof_dev_t.m4 \
+ $(top_srcdir)/m4/uoff_t_max.m4 $(top_srcdir)/m4/vararg.m4 \
+ $(top_srcdir)/m4/want_apparmor.m4 \
+ $(top_srcdir)/m4/want_bsdauth.m4 \
+ $(top_srcdir)/m4/want_bzlib.m4 \
+ $(top_srcdir)/m4/want_cassandra.m4 \
+ $(top_srcdir)/m4/want_cdb.m4 \
+ $(top_srcdir)/m4/want_checkpassword.m4 \
+ $(top_srcdir)/m4/want_clucene.m4 $(top_srcdir)/m4/want_db.m4 \
+ $(top_srcdir)/m4/want_gssapi.m4 $(top_srcdir)/m4/want_icu.m4 \
+ $(top_srcdir)/m4/want_ldap.m4 $(top_srcdir)/m4/want_lua.m4 \
+ $(top_srcdir)/m4/want_lz4.m4 $(top_srcdir)/m4/want_lzma.m4 \
+ $(top_srcdir)/m4/want_mysql.m4 $(top_srcdir)/m4/want_pam.m4 \
+ $(top_srcdir)/m4/want_passwd.m4 $(top_srcdir)/m4/want_pgsql.m4 \
+ $(top_srcdir)/m4/want_prefetch.m4 \
+ $(top_srcdir)/m4/want_shadow.m4 \
+ $(top_srcdir)/m4/want_sodium.m4 $(top_srcdir)/m4/want_solr.m4 \
+ $(top_srcdir)/m4/want_sqlite.m4 \
+ $(top_srcdir)/m4/want_stemmer.m4 \
+ $(top_srcdir)/m4/want_systemd.m4 \
+ $(top_srcdir)/m4/want_textcat.m4 \
+ $(top_srcdir)/m4/want_unwind.m4 $(top_srcdir)/m4/want_zlib.m4 \
+ $(top_srcdir)/m4/want_zstd.m4 $(top_srcdir)/configure.ac
+am__configure_deps = $(am__aclocal_m4_deps) $(CONFIGURE_DEPENDENCIES) \
+ $(ACLOCAL_M4)
+DIST_COMMON = $(srcdir)/Makefile.am $(noinst_HEADERS) \
+ $(pkginc_lib_HEADERS) $(am__DIST_COMMON)
+mkinstalldirs = $(install_sh) -d
+CONFIG_HEADER = $(top_builddir)/config.h
+CONFIG_CLEAN_FILES =
+CONFIG_CLEAN_VPATH_FILES =
+am__EXEEXT_1 = test-lib$(EXEEXT)
+PROGRAMS = $(noinst_PROGRAMS)
+LTLIBRARIES = $(noinst_LTLIBRARIES)
+am__DEPENDENCIES_1 =
+liblib_la_DEPENDENCIES = $(am__DEPENDENCIES_1)
+am_liblib_la_OBJECTS = array.lo aqueue.lo askpass.lo \
+ backtrace-string.lo base32.lo base64.lo bits.lo \
+ bsearch-insert-pos.lo buffer.lo buffer-istream.lo \
+ child-wait.lo compat.lo connection.lo cpu-limit.lo crc32.lo \
+ data-stack.lo eacces-error.lo env-util.lo event-filter.lo \
+ event-filter-lexer.lo event-filter-parser.lo event-log.lo \
+ execv-const.lo failures.lo fd-util.lo fdatasync-path.lo \
+ fdpass.lo file-cache.lo file-create-locked.lo file-copy.lo \
+ file-dotlock.lo file-lock.lo file-set-size.lo guid.lo hash.lo \
+ hash-format.lo hash-method.lo hash2.lo hex-binary.lo \
+ hex-dec.lo hmac.lo hmac-cram-md5.lo home-expand.lo \
+ hook-build.lo hostpid.lo imem.lo ipwd.lo iostream.lo \
+ iostream-pump.lo iostream-proxy.lo iostream-rawlog.lo \
+ iostream-temp.lo iso8601-date.lo istream.lo \
+ istream-base64-decoder.lo istream-base64-encoder.lo \
+ istream-callback.lo istream-chain.lo istream-concat.lo \
+ istream-crlf.lo istream-data.lo istream-failure-at.lo \
+ istream-file.lo istream-hash.lo istream-jsonstr.lo \
+ istream-limit.lo istream-multiplex.lo istream-rawlog.lo \
+ istream-seekable.lo istream-sized.lo istream-tee.lo \
+ istream-try.lo istream-timeout.lo istream-unix.lo ioloop.lo \
+ ioloop-iolist.lo ioloop-notify-none.lo ioloop-notify-fd.lo \
+ ioloop-notify-inotify.lo ioloop-notify-kqueue.lo \
+ ioloop-poll.lo ioloop-select.lo ioloop-epoll.lo \
+ ioloop-kqueue.lo json-parser.lo json-tree.lo lib.lo \
+ lib-event.lo lib-signals.lo log-throttle.lo md4.lo md5.lo \
+ memarea.lo mempool.lo mempool-allocfree.lo \
+ mempool-alloconly.lo mempool-datastack.lo mempool-system.lo \
+ mempool-unsafe-datastack.lo mkdir-parents.lo mmap-anon.lo \
+ mmap-util.lo module-dir.lo mountpoint.lo net.lo \
+ nfs-workarounds.lo numpack.lo ostream.lo ostream-buffer.lo \
+ ostream-failure-at.lo ostream-file.lo ostream-hash.lo \
+ ostream-multiplex.lo ostream-null.lo ostream-rawlog.lo \
+ ostream-unix.lo ostream-wrapper.lo path-util.lo pkcs5.lo \
+ primes.lo printf-format-fix.lo process-stat.lo \
+ process-title.lo priorityq.lo randgen.lo rand.lo read-full.lo \
+ restrict-access.lo restrict-process-size.lo safe-memset.lo \
+ safe-mkdir.lo safe-mkstemp.lo sendfile-util.lo \
+ seq-range-array.lo seq-set-builder.lo sha1.lo sha2.lo sha3.lo \
+ sleep.lo sort.lo stats-dist.lo str.lo str-find.lo \
+ str-sanitize.lo str-table.lo strescape.lo strfuncs.lo \
+ strnum.lo time-util.lo unix-socket-create.lo \
+ unlink-directory.lo unlink-old-files.lo unichar.lo uri-util.lo \
+ utc-offset.lo utc-mktime.lo var-expand.lo var-expand-if.lo \
+ wildcard-match.lo write-full.lo
+liblib_la_OBJECTS = $(am_liblib_la_OBJECTS)
+AM_V_lt = $(am__v_lt_@AM_V@)
+am__v_lt_ = $(am__v_lt_@AM_DEFAULT_V@)
+am__v_lt_0 = --silent
+am__v_lt_1 =
+am_test_lib_OBJECTS = test_lib-test-lib.$(OBJEXT) \
+ test_lib-test-array.$(OBJEXT) test_lib-test-aqueue.$(OBJEXT) \
+ test_lib-test-backtrace.$(OBJEXT) \
+ test_lib-test-base32.$(OBJEXT) test_lib-test-base64.$(OBJEXT) \
+ test_lib-test-bits.$(OBJEXT) \
+ test_lib-test-bsearch-insert-pos.$(OBJEXT) \
+ test_lib-test-buffer.$(OBJEXT) \
+ test_lib-test-buffer-istream.$(OBJEXT) \
+ test_lib-test-byteorder.$(OBJEXT) \
+ test_lib-test-connection.$(OBJEXT) \
+ test_lib-test-crc32.$(OBJEXT) \
+ test_lib-test-cpu-limit.$(OBJEXT) \
+ test_lib-test-data-stack.$(OBJEXT) \
+ test_lib-test-env-util.$(OBJEXT) \
+ test_lib-test-event-category-register.$(OBJEXT) \
+ test_lib-test-event-filter.$(OBJEXT) \
+ test_lib-test-event-filter-expr.$(OBJEXT) \
+ test_lib-test-event-filter-merge.$(OBJEXT) \
+ test_lib-test-event-filter-parser.$(OBJEXT) \
+ test_lib-test-event-flatten.$(OBJEXT) \
+ test_lib-test-event-log.$(OBJEXT) \
+ test_lib-test-failures.$(OBJEXT) \
+ test_lib-test-fd-util.$(OBJEXT) \
+ test_lib-test-file-cache.$(OBJEXT) \
+ test_lib-test-file-create-locked.$(OBJEXT) \
+ test_lib-test-guid.$(OBJEXT) test_lib-test-hash.$(OBJEXT) \
+ test_lib-test-hash-format.$(OBJEXT) \
+ test_lib-test-hash-method.$(OBJEXT) \
+ test_lib-test-hmac.$(OBJEXT) \
+ test_lib-test-hex-binary.$(OBJEXT) \
+ test_lib-test-imem.$(OBJEXT) test_lib-test-ioloop.$(OBJEXT) \
+ test_lib-test-iso8601-date.$(OBJEXT) \
+ test_lib-test-iostream-pump.$(OBJEXT) \
+ test_lib-test-iostream-proxy.$(OBJEXT) \
+ test_lib-test-iostream-temp.$(OBJEXT) \
+ test_lib-test-istream.$(OBJEXT) \
+ test_lib-test-istream-base64-decoder.$(OBJEXT) \
+ test_lib-test-istream-base64-encoder.$(OBJEXT) \
+ test_lib-test-istream-chain.$(OBJEXT) \
+ test_lib-test-istream-concat.$(OBJEXT) \
+ test_lib-test-istream-crlf.$(OBJEXT) \
+ test_lib-test-istream-failure-at.$(OBJEXT) \
+ test_lib-test-istream-jsonstr.$(OBJEXT) \
+ test_lib-test-istream-multiplex.$(OBJEXT) \
+ test_lib-test-istream-seekable.$(OBJEXT) \
+ test_lib-test-istream-sized.$(OBJEXT) \
+ test_lib-test-istream-tee.$(OBJEXT) \
+ test_lib-test-istream-try.$(OBJEXT) \
+ test_lib-test-istream-unix.$(OBJEXT) \
+ test_lib-test-json-parser.$(OBJEXT) \
+ test_lib-test-json-tree.$(OBJEXT) \
+ test_lib-test-lib-event.$(OBJEXT) \
+ test_lib-test-lib-signals.$(OBJEXT) \
+ test_lib-test-llist.$(OBJEXT) \
+ test_lib-test-log-throttle.$(OBJEXT) \
+ test_lib-test-macros.$(OBJEXT) \
+ test_lib-test-malloc-overflow.$(OBJEXT) \
+ test_lib-test-memarea.$(OBJEXT) \
+ test_lib-test-mempool.$(OBJEXT) \
+ test_lib-test-mempool-allocfree.$(OBJEXT) \
+ test_lib-test-mempool-alloconly.$(OBJEXT) \
+ test_lib-test-pkcs5.$(OBJEXT) test_lib-test-net.$(OBJEXT) \
+ test_lib-test-numpack.$(OBJEXT) \
+ test_lib-test-ostream-buffer.$(OBJEXT) \
+ test_lib-test-ostream-failure-at.$(OBJEXT) \
+ test_lib-test-ostream-file.$(OBJEXT) \
+ test_lib-test-ostream-multiplex.$(OBJEXT) \
+ test_lib-test-multiplex.$(OBJEXT) \
+ test_lib-test-path-util.$(OBJEXT) \
+ test_lib-test-primes.$(OBJEXT) \
+ test_lib-test-printf-format-fix.$(OBJEXT) \
+ test_lib-test-priorityq.$(OBJEXT) \
+ test_lib-test-random.$(OBJEXT) \
+ test_lib-test-seq-range-array.$(OBJEXT) \
+ test_lib-test-seq-set-builder.$(OBJEXT) \
+ test_lib-test-stats-dist.$(OBJEXT) test_lib-test-str.$(OBJEXT) \
+ test_lib-test-strescape.$(OBJEXT) \
+ test_lib-test-strfuncs.$(OBJEXT) \
+ test_lib-test-strnum.$(OBJEXT) \
+ test_lib-test-str-find.$(OBJEXT) \
+ test_lib-test-str-sanitize.$(OBJEXT) \
+ test_lib-test-str-table.$(OBJEXT) \
+ test_lib-test-time-util.$(OBJEXT) \
+ test_lib-test-unichar.$(OBJEXT) \
+ test_lib-test-utc-mktime.$(OBJEXT) test_lib-test-uri.$(OBJEXT) \
+ test_lib-test-var-expand.$(OBJEXT) \
+ test_lib-test-wildcard-match.$(OBJEXT)
+test_lib_OBJECTS = $(am_test_lib_OBJECTS)
+AM_V_P = $(am__v_P_@AM_V@)
+am__v_P_ = $(am__v_P_@AM_DEFAULT_V@)
+am__v_P_0 = false
+am__v_P_1 = :
+AM_V_GEN = $(am__v_GEN_@AM_V@)
+am__v_GEN_ = $(am__v_GEN_@AM_DEFAULT_V@)
+am__v_GEN_0 = @echo " GEN " $@;
+am__v_GEN_1 =
+AM_V_at = $(am__v_at_@AM_V@)
+am__v_at_ = $(am__v_at_@AM_DEFAULT_V@)
+am__v_at_0 = @
+am__v_at_1 =
+DEFAULT_INCLUDES = -I.@am__isrc@ -I$(top_builddir)
+depcomp = $(SHELL) $(top_srcdir)/depcomp
+am__maybe_remake_depfiles = depfiles
+am__depfiles_remade = ./$(DEPDIR)/aqueue.Plo ./$(DEPDIR)/array.Plo \
+ ./$(DEPDIR)/askpass.Plo ./$(DEPDIR)/backtrace-string.Plo \
+ ./$(DEPDIR)/base32.Plo ./$(DEPDIR)/base64.Plo \
+ ./$(DEPDIR)/bits.Plo ./$(DEPDIR)/bsearch-insert-pos.Plo \
+ ./$(DEPDIR)/buffer-istream.Plo ./$(DEPDIR)/buffer.Plo \
+ ./$(DEPDIR)/child-wait.Plo ./$(DEPDIR)/compat.Plo \
+ ./$(DEPDIR)/connection.Plo ./$(DEPDIR)/cpu-limit.Plo \
+ ./$(DEPDIR)/crc32.Plo ./$(DEPDIR)/data-stack.Plo \
+ ./$(DEPDIR)/eacces-error.Plo ./$(DEPDIR)/env-util.Plo \
+ ./$(DEPDIR)/event-filter-lexer.Plo \
+ ./$(DEPDIR)/event-filter-parser.Plo \
+ ./$(DEPDIR)/event-filter.Plo ./$(DEPDIR)/event-log.Plo \
+ ./$(DEPDIR)/execv-const.Plo ./$(DEPDIR)/failures.Plo \
+ ./$(DEPDIR)/fd-util.Plo ./$(DEPDIR)/fdatasync-path.Plo \
+ ./$(DEPDIR)/fdpass.Plo ./$(DEPDIR)/file-cache.Plo \
+ ./$(DEPDIR)/file-copy.Plo ./$(DEPDIR)/file-create-locked.Plo \
+ ./$(DEPDIR)/file-dotlock.Plo ./$(DEPDIR)/file-lock.Plo \
+ ./$(DEPDIR)/file-set-size.Plo ./$(DEPDIR)/guid.Plo \
+ ./$(DEPDIR)/hash-format.Plo ./$(DEPDIR)/hash-method.Plo \
+ ./$(DEPDIR)/hash.Plo ./$(DEPDIR)/hash2.Plo \
+ ./$(DEPDIR)/hex-binary.Plo ./$(DEPDIR)/hex-dec.Plo \
+ ./$(DEPDIR)/hmac-cram-md5.Plo ./$(DEPDIR)/hmac.Plo \
+ ./$(DEPDIR)/home-expand.Plo ./$(DEPDIR)/hook-build.Plo \
+ ./$(DEPDIR)/hostpid.Plo ./$(DEPDIR)/imem.Plo \
+ ./$(DEPDIR)/ioloop-epoll.Plo ./$(DEPDIR)/ioloop-iolist.Plo \
+ ./$(DEPDIR)/ioloop-kqueue.Plo ./$(DEPDIR)/ioloop-notify-fd.Plo \
+ ./$(DEPDIR)/ioloop-notify-inotify.Plo \
+ ./$(DEPDIR)/ioloop-notify-kqueue.Plo \
+ ./$(DEPDIR)/ioloop-notify-none.Plo ./$(DEPDIR)/ioloop-poll.Plo \
+ ./$(DEPDIR)/ioloop-select.Plo ./$(DEPDIR)/ioloop.Plo \
+ ./$(DEPDIR)/iostream-proxy.Plo ./$(DEPDIR)/iostream-pump.Plo \
+ ./$(DEPDIR)/iostream-rawlog.Plo ./$(DEPDIR)/iostream-temp.Plo \
+ ./$(DEPDIR)/iostream.Plo ./$(DEPDIR)/ipwd.Plo \
+ ./$(DEPDIR)/iso8601-date.Plo \
+ ./$(DEPDIR)/istream-base64-decoder.Plo \
+ ./$(DEPDIR)/istream-base64-encoder.Plo \
+ ./$(DEPDIR)/istream-callback.Plo ./$(DEPDIR)/istream-chain.Plo \
+ ./$(DEPDIR)/istream-concat.Plo ./$(DEPDIR)/istream-crlf.Plo \
+ ./$(DEPDIR)/istream-data.Plo \
+ ./$(DEPDIR)/istream-failure-at.Plo \
+ ./$(DEPDIR)/istream-file.Plo ./$(DEPDIR)/istream-hash.Plo \
+ ./$(DEPDIR)/istream-jsonstr.Plo ./$(DEPDIR)/istream-limit.Plo \
+ ./$(DEPDIR)/istream-multiplex.Plo \
+ ./$(DEPDIR)/istream-rawlog.Plo \
+ ./$(DEPDIR)/istream-seekable.Plo ./$(DEPDIR)/istream-sized.Plo \
+ ./$(DEPDIR)/istream-tee.Plo ./$(DEPDIR)/istream-timeout.Plo \
+ ./$(DEPDIR)/istream-try.Plo ./$(DEPDIR)/istream-unix.Plo \
+ ./$(DEPDIR)/istream.Plo ./$(DEPDIR)/json-parser.Plo \
+ ./$(DEPDIR)/json-tree.Plo ./$(DEPDIR)/lib-event.Plo \
+ ./$(DEPDIR)/lib-signals.Plo ./$(DEPDIR)/lib.Plo \
+ ./$(DEPDIR)/log-throttle.Plo ./$(DEPDIR)/md4.Plo \
+ ./$(DEPDIR)/md5.Plo ./$(DEPDIR)/memarea.Plo \
+ ./$(DEPDIR)/mempool-allocfree.Plo \
+ ./$(DEPDIR)/mempool-alloconly.Plo \
+ ./$(DEPDIR)/mempool-datastack.Plo \
+ ./$(DEPDIR)/mempool-system.Plo \
+ ./$(DEPDIR)/mempool-unsafe-datastack.Plo \
+ ./$(DEPDIR)/mempool.Plo ./$(DEPDIR)/mkdir-parents.Plo \
+ ./$(DEPDIR)/mmap-anon.Plo ./$(DEPDIR)/mmap-util.Plo \
+ ./$(DEPDIR)/module-dir.Plo ./$(DEPDIR)/mountpoint.Plo \
+ ./$(DEPDIR)/net.Plo ./$(DEPDIR)/nfs-workarounds.Plo \
+ ./$(DEPDIR)/numpack.Plo ./$(DEPDIR)/ostream-buffer.Plo \
+ ./$(DEPDIR)/ostream-failure-at.Plo \
+ ./$(DEPDIR)/ostream-file.Plo ./$(DEPDIR)/ostream-hash.Plo \
+ ./$(DEPDIR)/ostream-multiplex.Plo ./$(DEPDIR)/ostream-null.Plo \
+ ./$(DEPDIR)/ostream-rawlog.Plo ./$(DEPDIR)/ostream-unix.Plo \
+ ./$(DEPDIR)/ostream-wrapper.Plo ./$(DEPDIR)/ostream.Plo \
+ ./$(DEPDIR)/path-util.Plo ./$(DEPDIR)/pkcs5.Plo \
+ ./$(DEPDIR)/primes.Plo ./$(DEPDIR)/printf-format-fix.Plo \
+ ./$(DEPDIR)/priorityq.Plo ./$(DEPDIR)/process-stat.Plo \
+ ./$(DEPDIR)/process-title.Plo ./$(DEPDIR)/rand.Plo \
+ ./$(DEPDIR)/randgen.Plo ./$(DEPDIR)/read-full.Plo \
+ ./$(DEPDIR)/restrict-access.Plo \
+ ./$(DEPDIR)/restrict-process-size.Plo \
+ ./$(DEPDIR)/safe-memset.Plo ./$(DEPDIR)/safe-mkdir.Plo \
+ ./$(DEPDIR)/safe-mkstemp.Plo ./$(DEPDIR)/sendfile-util.Plo \
+ ./$(DEPDIR)/seq-range-array.Plo \
+ ./$(DEPDIR)/seq-set-builder.Plo ./$(DEPDIR)/sha1.Plo \
+ ./$(DEPDIR)/sha2.Plo ./$(DEPDIR)/sha3.Plo \
+ ./$(DEPDIR)/sleep.Plo ./$(DEPDIR)/sort.Plo \
+ ./$(DEPDIR)/stats-dist.Plo ./$(DEPDIR)/str-find.Plo \
+ ./$(DEPDIR)/str-sanitize.Plo ./$(DEPDIR)/str-table.Plo \
+ ./$(DEPDIR)/str.Plo ./$(DEPDIR)/strescape.Plo \
+ ./$(DEPDIR)/strfuncs.Plo ./$(DEPDIR)/strnum.Plo \
+ ./$(DEPDIR)/test_lib-test-aqueue.Po \
+ ./$(DEPDIR)/test_lib-test-array.Po \
+ ./$(DEPDIR)/test_lib-test-backtrace.Po \
+ ./$(DEPDIR)/test_lib-test-base32.Po \
+ ./$(DEPDIR)/test_lib-test-base64.Po \
+ ./$(DEPDIR)/test_lib-test-bits.Po \
+ ./$(DEPDIR)/test_lib-test-bsearch-insert-pos.Po \
+ ./$(DEPDIR)/test_lib-test-buffer-istream.Po \
+ ./$(DEPDIR)/test_lib-test-buffer.Po \
+ ./$(DEPDIR)/test_lib-test-byteorder.Po \
+ ./$(DEPDIR)/test_lib-test-connection.Po \
+ ./$(DEPDIR)/test_lib-test-cpu-limit.Po \
+ ./$(DEPDIR)/test_lib-test-crc32.Po \
+ ./$(DEPDIR)/test_lib-test-data-stack.Po \
+ ./$(DEPDIR)/test_lib-test-env-util.Po \
+ ./$(DEPDIR)/test_lib-test-event-category-register.Po \
+ ./$(DEPDIR)/test_lib-test-event-filter-expr.Po \
+ ./$(DEPDIR)/test_lib-test-event-filter-merge.Po \
+ ./$(DEPDIR)/test_lib-test-event-filter-parser.Po \
+ ./$(DEPDIR)/test_lib-test-event-filter.Po \
+ ./$(DEPDIR)/test_lib-test-event-flatten.Po \
+ ./$(DEPDIR)/test_lib-test-event-log.Po \
+ ./$(DEPDIR)/test_lib-test-failures.Po \
+ ./$(DEPDIR)/test_lib-test-fd-util.Po \
+ ./$(DEPDIR)/test_lib-test-file-cache.Po \
+ ./$(DEPDIR)/test_lib-test-file-create-locked.Po \
+ ./$(DEPDIR)/test_lib-test-guid.Po \
+ ./$(DEPDIR)/test_lib-test-hash-format.Po \
+ ./$(DEPDIR)/test_lib-test-hash-method.Po \
+ ./$(DEPDIR)/test_lib-test-hash.Po \
+ ./$(DEPDIR)/test_lib-test-hex-binary.Po \
+ ./$(DEPDIR)/test_lib-test-hmac.Po \
+ ./$(DEPDIR)/test_lib-test-imem.Po \
+ ./$(DEPDIR)/test_lib-test-ioloop.Po \
+ ./$(DEPDIR)/test_lib-test-iostream-proxy.Po \
+ ./$(DEPDIR)/test_lib-test-iostream-pump.Po \
+ ./$(DEPDIR)/test_lib-test-iostream-temp.Po \
+ ./$(DEPDIR)/test_lib-test-iso8601-date.Po \
+ ./$(DEPDIR)/test_lib-test-istream-base64-decoder.Po \
+ ./$(DEPDIR)/test_lib-test-istream-base64-encoder.Po \
+ ./$(DEPDIR)/test_lib-test-istream-chain.Po \
+ ./$(DEPDIR)/test_lib-test-istream-concat.Po \
+ ./$(DEPDIR)/test_lib-test-istream-crlf.Po \
+ ./$(DEPDIR)/test_lib-test-istream-failure-at.Po \
+ ./$(DEPDIR)/test_lib-test-istream-jsonstr.Po \
+ ./$(DEPDIR)/test_lib-test-istream-multiplex.Po \
+ ./$(DEPDIR)/test_lib-test-istream-seekable.Po \
+ ./$(DEPDIR)/test_lib-test-istream-sized.Po \
+ ./$(DEPDIR)/test_lib-test-istream-tee.Po \
+ ./$(DEPDIR)/test_lib-test-istream-try.Po \
+ ./$(DEPDIR)/test_lib-test-istream-unix.Po \
+ ./$(DEPDIR)/test_lib-test-istream.Po \
+ ./$(DEPDIR)/test_lib-test-json-parser.Po \
+ ./$(DEPDIR)/test_lib-test-json-tree.Po \
+ ./$(DEPDIR)/test_lib-test-lib-event.Po \
+ ./$(DEPDIR)/test_lib-test-lib-signals.Po \
+ ./$(DEPDIR)/test_lib-test-lib.Po \
+ ./$(DEPDIR)/test_lib-test-llist.Po \
+ ./$(DEPDIR)/test_lib-test-log-throttle.Po \
+ ./$(DEPDIR)/test_lib-test-macros.Po \
+ ./$(DEPDIR)/test_lib-test-malloc-overflow.Po \
+ ./$(DEPDIR)/test_lib-test-memarea.Po \
+ ./$(DEPDIR)/test_lib-test-mempool-allocfree.Po \
+ ./$(DEPDIR)/test_lib-test-mempool-alloconly.Po \
+ ./$(DEPDIR)/test_lib-test-mempool.Po \
+ ./$(DEPDIR)/test_lib-test-multiplex.Po \
+ ./$(DEPDIR)/test_lib-test-net.Po \
+ ./$(DEPDIR)/test_lib-test-numpack.Po \
+ ./$(DEPDIR)/test_lib-test-ostream-buffer.Po \
+ ./$(DEPDIR)/test_lib-test-ostream-failure-at.Po \
+ ./$(DEPDIR)/test_lib-test-ostream-file.Po \
+ ./$(DEPDIR)/test_lib-test-ostream-multiplex.Po \
+ ./$(DEPDIR)/test_lib-test-path-util.Po \
+ ./$(DEPDIR)/test_lib-test-pkcs5.Po \
+ ./$(DEPDIR)/test_lib-test-primes.Po \
+ ./$(DEPDIR)/test_lib-test-printf-format-fix.Po \
+ ./$(DEPDIR)/test_lib-test-priorityq.Po \
+ ./$(DEPDIR)/test_lib-test-random.Po \
+ ./$(DEPDIR)/test_lib-test-seq-range-array.Po \
+ ./$(DEPDIR)/test_lib-test-seq-set-builder.Po \
+ ./$(DEPDIR)/test_lib-test-stats-dist.Po \
+ ./$(DEPDIR)/test_lib-test-str-find.Po \
+ ./$(DEPDIR)/test_lib-test-str-sanitize.Po \
+ ./$(DEPDIR)/test_lib-test-str-table.Po \
+ ./$(DEPDIR)/test_lib-test-str.Po \
+ ./$(DEPDIR)/test_lib-test-strescape.Po \
+ ./$(DEPDIR)/test_lib-test-strfuncs.Po \
+ ./$(DEPDIR)/test_lib-test-strnum.Po \
+ ./$(DEPDIR)/test_lib-test-time-util.Po \
+ ./$(DEPDIR)/test_lib-test-unichar.Po \
+ ./$(DEPDIR)/test_lib-test-uri.Po \
+ ./$(DEPDIR)/test_lib-test-utc-mktime.Po \
+ ./$(DEPDIR)/test_lib-test-var-expand.Po \
+ ./$(DEPDIR)/test_lib-test-wildcard-match.Po \
+ ./$(DEPDIR)/time-util.Plo ./$(DEPDIR)/unichar.Plo \
+ ./$(DEPDIR)/unix-socket-create.Plo \
+ ./$(DEPDIR)/unlink-directory.Plo \
+ ./$(DEPDIR)/unlink-old-files.Plo ./$(DEPDIR)/uri-util.Plo \
+ ./$(DEPDIR)/utc-mktime.Plo ./$(DEPDIR)/utc-offset.Plo \
+ ./$(DEPDIR)/var-expand-if.Plo ./$(DEPDIR)/var-expand.Plo \
+ ./$(DEPDIR)/wildcard-match.Plo ./$(DEPDIR)/write-full.Plo
+am__mv = mv -f
+COMPILE = $(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) \
+ $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS)
+LTCOMPILE = $(LIBTOOL) $(AM_V_lt) --tag=CC $(AM_LIBTOOLFLAGS) \
+ $(LIBTOOLFLAGS) --mode=compile $(CC) $(DEFS) \
+ $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) $(CPPFLAGS) \
+ $(AM_CFLAGS) $(CFLAGS)
+AM_V_CC = $(am__v_CC_@AM_V@)
+am__v_CC_ = $(am__v_CC_@AM_DEFAULT_V@)
+am__v_CC_0 = @echo " CC " $@;
+am__v_CC_1 =
+CCLD = $(CC)
+LINK = $(LIBTOOL) $(AM_V_lt) --tag=CC $(AM_LIBTOOLFLAGS) \
+ $(LIBTOOLFLAGS) --mode=link $(CCLD) $(AM_CFLAGS) $(CFLAGS) \
+ $(AM_LDFLAGS) $(LDFLAGS) -o $@
+AM_V_CCLD = $(am__v_CCLD_@AM_V@)
+am__v_CCLD_ = $(am__v_CCLD_@AM_DEFAULT_V@)
+am__v_CCLD_0 = @echo " CCLD " $@;
+am__v_CCLD_1 =
+@MAINTAINER_MODE_FALSE@am__skiplex = test -f $@ ||
+LEXCOMPILE = $(LEX) $(AM_LFLAGS) $(LFLAGS)
+LTLEXCOMPILE = $(LIBTOOL) $(AM_V_lt) $(AM_LIBTOOLFLAGS) \
+ $(LIBTOOLFLAGS) --mode=compile $(LEX) $(AM_LFLAGS) $(LFLAGS)
+AM_V_LEX = $(am__v_LEX_@AM_V@)
+am__v_LEX_ = $(am__v_LEX_@AM_DEFAULT_V@)
+am__v_LEX_0 = @echo " LEX " $@;
+am__v_LEX_1 =
+YLWRAP = $(top_srcdir)/ylwrap
+@MAINTAINER_MODE_FALSE@am__skipyacc = test -f $@ ||
+am__yacc_c2h = sed -e s/cc$$/hh/ -e s/cpp$$/hpp/ -e s/cxx$$/hxx/ \
+ -e s/c++$$/h++/ -e s/c$$/h/
+YACCCOMPILE = $(YACC) $(AM_YFLAGS) $(YFLAGS)
+LTYACCCOMPILE = $(LIBTOOL) $(AM_V_lt) $(AM_LIBTOOLFLAGS) \
+ $(LIBTOOLFLAGS) --mode=compile $(YACC) $(AM_YFLAGS) $(YFLAGS)
+AM_V_YACC = $(am__v_YACC_@AM_V@)
+am__v_YACC_ = $(am__v_YACC_@AM_DEFAULT_V@)
+am__v_YACC_0 = @echo " YACC " $@;
+am__v_YACC_1 =
+SOURCES = $(liblib_la_SOURCES) $(test_lib_SOURCES)
+DIST_SOURCES = $(liblib_la_SOURCES) $(test_lib_SOURCES)
+am__can_run_installinfo = \
+ case $$AM_UPDATE_INFO_DIR in \
+ n|no|NO) false;; \
+ *) (install-info --version) >/dev/null 2>&1;; \
+ esac
+am__vpath_adj_setup = srcdirstrip=`echo "$(srcdir)" | sed 's|.|.|g'`;
+am__vpath_adj = case $$p in \
+ $(srcdir)/*) f=`echo "$$p" | sed "s|^$$srcdirstrip/||"`;; \
+ *) f=$$p;; \
+ esac;
+am__strip_dir = f=`echo $$p | sed -e 's|^.*/||'`;
+am__install_max = 40
+am__nobase_strip_setup = \
+ srcdirstrip=`echo "$(srcdir)" | sed 's/[].[^$$\\*|]/\\\\&/g'`
+am__nobase_strip = \
+ for p in $$list; do echo "$$p"; done | sed -e "s|$$srcdirstrip/||"
+am__nobase_list = $(am__nobase_strip_setup); \
+ for p in $$list; do echo "$$p $$p"; done | \
+ sed "s| $$srcdirstrip/| |;"' / .*\//!s/ .*/ ./; s,\( .*\)/[^/]*$$,\1,' | \
+ $(AWK) 'BEGIN { files["."] = "" } { files[$$2] = files[$$2] " " $$1; \
+ if (++n[$$2] == $(am__install_max)) \
+ { print $$2, files[$$2]; n[$$2] = 0; files[$$2] = "" } } \
+ END { for (dir in files) print dir, files[dir] }'
+am__base_list = \
+ sed '$$!N;$$!N;$$!N;$$!N;$$!N;$$!N;$$!N;s/\n/ /g' | \
+ sed '$$!N;$$!N;$$!N;$$!N;s/\n/ /g'
+am__uninstall_files_from_dir = { \
+ test -z "$$files" \
+ || { test ! -d "$$dir" && test ! -f "$$dir" && test ! -r "$$dir"; } \
+ || { echo " ( cd '$$dir' && rm -f" $$files ")"; \
+ $(am__cd) "$$dir" && rm -f $$files; }; \
+ }
+am__installdirs = "$(DESTDIR)$(pkginc_libdir)"
+HEADERS = $(noinst_HEADERS) $(pkginc_lib_HEADERS)
+am__tagged_files = $(HEADERS) $(SOURCES) $(TAGS_FILES) $(LISP)
+# Read a list of newline-separated strings from the standard input,
+# and print each of them once, without duplicates. Input order is
+# *not* preserved.
+am__uniquify_input = $(AWK) '\
+ BEGIN { nonempty = 0; } \
+ { items[$$0] = 1; nonempty = 1; } \
+ END { if (nonempty) { for (i in items) print i; }; } \
+'
+# Make sure the list of sources is unique. This is necessary because,
+# e.g., the same source file might be shared among _SOURCES variables
+# for different programs/libraries.
+am__define_uniq_tagged_files = \
+ list='$(am__tagged_files)'; \
+ unique=`for i in $$list; do \
+ if test -f "$$i"; then echo $$i; else echo $(srcdir)/$$i; fi; \
+ done | $(am__uniquify_input)`
+ETAGS = etags
+CTAGS = ctags
+am__DIST_COMMON = $(srcdir)/Makefile.in $(top_srcdir)/depcomp \
+ $(top_srcdir)/ylwrap event-filter-lexer.c \
+ event-filter-parser.c
+DISTFILES = $(DIST_COMMON) $(DIST_SOURCES) $(TEXINFOS) $(EXTRA_DIST)
+ACLOCAL = @ACLOCAL@
+ACLOCAL_AMFLAGS = @ACLOCAL_AMFLAGS@
+AMTAR = @AMTAR@
+AM_DEFAULT_VERBOSITY = @AM_DEFAULT_VERBOSITY@
+APPARMOR_LIBS = @APPARMOR_LIBS@
+AR = @AR@
+AUTH_CFLAGS = @AUTH_CFLAGS@
+AUTH_LIBS = @AUTH_LIBS@
+AUTOCONF = @AUTOCONF@
+AUTOHEADER = @AUTOHEADER@
+AUTOMAKE = @AUTOMAKE@
+AWK = @AWK@
+BINARY_CFLAGS = @BINARY_CFLAGS@
+BINARY_LDFLAGS = @BINARY_LDFLAGS@
+BISON = @BISON@
+CASSANDRA_CFLAGS = @CASSANDRA_CFLAGS@
+CASSANDRA_LIBS = @CASSANDRA_LIBS@
+CC = @CC@
+CCDEPMODE = @CCDEPMODE@
+CDB_LIBS = @CDB_LIBS@
+CFLAGS = @CFLAGS@
+CLUCENE_CFLAGS = @CLUCENE_CFLAGS@
+CLUCENE_LIBS = @CLUCENE_LIBS@
+COMPRESS_LIBS = @COMPRESS_LIBS@
+CPP = @CPP@
+CPPFLAGS = @CPPFLAGS@
+CRYPT_LIBS = @CRYPT_LIBS@
+CXX = @CXX@
+CXXCPP = @CXXCPP@
+CXXDEPMODE = @CXXDEPMODE@
+CXXFLAGS = @CXXFLAGS@
+CYGPATH_W = @CYGPATH_W@
+DEFS = @DEFS@
+DEPDIR = @DEPDIR@
+DICT_LIBS = @DICT_LIBS@
+DLLIB = @DLLIB@
+DLLTOOL = @DLLTOOL@
+DSYMUTIL = @DSYMUTIL@
+DUMPBIN = @DUMPBIN@
+ECHO_C = @ECHO_C@
+ECHO_N = @ECHO_N@
+ECHO_T = @ECHO_T@
+EGREP = @EGREP@
+EXEEXT = @EXEEXT@
+FGREP = @FGREP@
+FLEX = @FLEX@
+FUZZER_CPPFLAGS = @FUZZER_CPPFLAGS@
+FUZZER_LDFLAGS = @FUZZER_LDFLAGS@
+GREP = @GREP@
+INSTALL = @INSTALL@
+INSTALL_DATA = @INSTALL_DATA@
+INSTALL_PROGRAM = @INSTALL_PROGRAM@
+INSTALL_SCRIPT = @INSTALL_SCRIPT@
+INSTALL_STRIP_PROGRAM = @INSTALL_STRIP_PROGRAM@
+KRB5CONFIG = @KRB5CONFIG@
+KRB5_CFLAGS = @KRB5_CFLAGS@
+KRB5_LIBS = @KRB5_LIBS@
+LD = @LD@
+LDAP_LIBS = @LDAP_LIBS@
+LDFLAGS = @LDFLAGS@
+LD_NO_WHOLE_ARCHIVE = @LD_NO_WHOLE_ARCHIVE@
+LD_WHOLE_ARCHIVE = @LD_WHOLE_ARCHIVE@
+LIBCAP = @LIBCAP@
+LIBDOVECOT = @LIBDOVECOT@
+LIBDOVECOT_COMPRESS = @LIBDOVECOT_COMPRESS@
+LIBDOVECOT_DEPS = @LIBDOVECOT_DEPS@
+LIBDOVECOT_DSYNC = @LIBDOVECOT_DSYNC@
+LIBDOVECOT_LA_LIBS = @LIBDOVECOT_LA_LIBS@
+LIBDOVECOT_LDA = @LIBDOVECOT_LDA@
+LIBDOVECOT_LDAP = @LIBDOVECOT_LDAP@
+LIBDOVECOT_LIBFTS = @LIBDOVECOT_LIBFTS@
+LIBDOVECOT_LIBFTS_DEPS = @LIBDOVECOT_LIBFTS_DEPS@
+LIBDOVECOT_LOGIN = @LIBDOVECOT_LOGIN@
+LIBDOVECOT_LUA = @LIBDOVECOT_LUA@
+LIBDOVECOT_LUA_DEPS = @LIBDOVECOT_LUA_DEPS@
+LIBDOVECOT_SQL = @LIBDOVECOT_SQL@
+LIBDOVECOT_STORAGE = @LIBDOVECOT_STORAGE@
+LIBDOVECOT_STORAGE_DEPS = @LIBDOVECOT_STORAGE_DEPS@
+LIBEXTTEXTCAT_CFLAGS = @LIBEXTTEXTCAT_CFLAGS@
+LIBEXTTEXTCAT_LIBS = @LIBEXTTEXTCAT_LIBS@
+LIBICONV = @LIBICONV@
+LIBICU_CFLAGS = @LIBICU_CFLAGS@
+LIBICU_LIBS = @LIBICU_LIBS@
+LIBOBJS = @LIBOBJS@
+LIBS = @LIBS@
+LIBSODIUM_CFLAGS = @LIBSODIUM_CFLAGS@
+LIBSODIUM_LIBS = @LIBSODIUM_LIBS@
+LIBTIRPC_CFLAGS = @LIBTIRPC_CFLAGS@
+LIBTIRPC_LIBS = @LIBTIRPC_LIBS@
+LIBTOOL = @LIBTOOL@
+LIBUNWIND_CFLAGS = @LIBUNWIND_CFLAGS@
+LIBUNWIND_LIBS = @LIBUNWIND_LIBS@
+LIBWRAP_LIBS = @LIBWRAP_LIBS@
+LINKED_STORAGE_LDADD = @LINKED_STORAGE_LDADD@
+LIPO = @LIPO@
+LN_S = @LN_S@
+LTLIBICONV = @LTLIBICONV@
+LTLIBOBJS = @LTLIBOBJS@
+LT_SYS_LIBRARY_PATH = @LT_SYS_LIBRARY_PATH@
+LUA_CFLAGS = @LUA_CFLAGS@
+LUA_LIBS = @LUA_LIBS@
+MAINT = @MAINT@
+MAKEINFO = @MAKEINFO@
+MANIFEST_TOOL = @MANIFEST_TOOL@
+MKDIR_P = @MKDIR_P@
+MODULE_LIBS = @MODULE_LIBS@
+MODULE_SUFFIX = @MODULE_SUFFIX@
+MYSQL_CFLAGS = @MYSQL_CFLAGS@
+MYSQL_CONFIG = @MYSQL_CONFIG@
+MYSQL_LIBS = @MYSQL_LIBS@
+NM = @NM@
+NMEDIT = @NMEDIT@
+NOPLUGIN_LDFLAGS = @NOPLUGIN_LDFLAGS@
+OBJDUMP = @OBJDUMP@
+OBJEXT = @OBJEXT@
+OTOOL = @OTOOL@
+OTOOL64 = @OTOOL64@
+PACKAGE = @PACKAGE@
+PACKAGE_BUGREPORT = @PACKAGE_BUGREPORT@
+PACKAGE_NAME = @PACKAGE_NAME@
+PACKAGE_STRING = @PACKAGE_STRING@
+PACKAGE_TARNAME = @PACKAGE_TARNAME@
+PACKAGE_URL = @PACKAGE_URL@
+PACKAGE_VERSION = @PACKAGE_VERSION@
+PANDOC = @PANDOC@
+PATH_SEPARATOR = @PATH_SEPARATOR@
+PGSQL_CFLAGS = @PGSQL_CFLAGS@
+PGSQL_LIBS = @PGSQL_LIBS@
+PG_CONFIG = @PG_CONFIG@
+PIE_CFLAGS = @PIE_CFLAGS@
+PIE_LDFLAGS = @PIE_LDFLAGS@
+PKG_CONFIG = @PKG_CONFIG@
+PKG_CONFIG_LIBDIR = @PKG_CONFIG_LIBDIR@
+PKG_CONFIG_PATH = @PKG_CONFIG_PATH@
+QUOTA_LIBS = @QUOTA_LIBS@
+RANLIB = @RANLIB@
+RELRO_LDFLAGS = @RELRO_LDFLAGS@
+RPCGEN = @RPCGEN@
+RUN_TEST = @RUN_TEST@
+SED = @SED@
+SETTING_FILES = @SETTING_FILES@
+SET_MAKE = @SET_MAKE@
+SHELL = @SHELL@
+SQLITE_CFLAGS = @SQLITE_CFLAGS@
+SQLITE_LIBS = @SQLITE_LIBS@
+SQL_CFLAGS = @SQL_CFLAGS@
+SQL_LIBS = @SQL_LIBS@
+SSL_CFLAGS = @SSL_CFLAGS@
+SSL_LIBS = @SSL_LIBS@
+STRIP = @STRIP@
+SYSTEMD_CFLAGS = @SYSTEMD_CFLAGS@
+SYSTEMD_LIBS = @SYSTEMD_LIBS@
+VALGRIND = @VALGRIND@
+VERSION = @VERSION@
+ZSTD_CFLAGS = @ZSTD_CFLAGS@
+ZSTD_LIBS = @ZSTD_LIBS@
+abs_builddir = @abs_builddir@
+abs_srcdir = @abs_srcdir@
+abs_top_builddir = @abs_top_builddir@
+abs_top_srcdir = @abs_top_srcdir@
+ac_ct_AR = @ac_ct_AR@
+ac_ct_CC = @ac_ct_CC@
+ac_ct_CXX = @ac_ct_CXX@
+ac_ct_DUMPBIN = @ac_ct_DUMPBIN@
+am__include = @am__include@
+am__leading_dot = @am__leading_dot@
+am__quote = @am__quote@
+am__tar = @am__tar@
+am__untar = @am__untar@
+bindir = @bindir@
+build = @build@
+build_alias = @build_alias@
+build_cpu = @build_cpu@
+build_os = @build_os@
+build_vendor = @build_vendor@
+builddir = @builddir@
+datadir = @datadir@
+datarootdir = @datarootdir@
+dict_drivers = @dict_drivers@
+docdir = @docdir@
+dvidir = @dvidir@
+exec_prefix = @exec_prefix@
+host = @host@
+host_alias = @host_alias@
+host_cpu = @host_cpu@
+host_os = @host_os@
+host_vendor = @host_vendor@
+htmldir = @htmldir@
+includedir = @includedir@
+infodir = @infodir@
+install_sh = @install_sh@
+libdir = @libdir@
+libexecdir = @libexecdir@
+localedir = @localedir@
+localstatedir = @localstatedir@
+mandir = @mandir@
+mkdir_p = @mkdir_p@
+moduledir = @moduledir@
+oldincludedir = @oldincludedir@
+pdfdir = @pdfdir@
+prefix = @prefix@
+program_transform_name = @program_transform_name@
+psdir = @psdir@
+rundir = @rundir@
+runstatedir = @runstatedir@
+sbindir = @sbindir@
+sharedstatedir = @sharedstatedir@
+sql_drivers = @sql_drivers@
+srcdir = @srcdir@
+ssldir = @ssldir@
+statedir = @statedir@
+sysconfdir = @sysconfdir@
+systemdservicetype = @systemdservicetype@
+systemdsystemunitdir = @systemdsystemunitdir@
+target_alias = @target_alias@
+top_build_prefix = @top_build_prefix@
+top_builddir = @top_builddir@
+top_srcdir = @top_srcdir@
+AM_CPPFLAGS = \
+ $(LIBUNWIND_CFLAGS)
+
+noinst_LTLIBRARIES = liblib.la
+BUILT_SOURCES = $(srcdir)/unicodemap.c \
+ event-filter-lexer.c \
+ event-filter-parser.c \
+ event-filter-parser.h
+
+EXTRA_DIST = unicodemap.c unicodemap.pl UnicodeData.txt
+
+# Squelch autoconf error about using .[ly] sources but not defining $(LEX)
+# and $(YACC). Using false here avoids accidental use.
+LEX = /bin/false
+YACC = /bin/false
+liblib_la_LIBADD = $(LIBUNWIND_LIBS)
+liblib_la_SOURCES = \
+ array.c \
+ aqueue.c \
+ askpass.c \
+ backtrace-string.c \
+ base32.c \
+ base64.c \
+ bits.c \
+ bsearch-insert-pos.c \
+ buffer.c \
+ buffer-istream.c \
+ child-wait.c \
+ compat.c \
+ connection.c \
+ cpu-limit.c \
+ crc32.c \
+ data-stack.c \
+ eacces-error.c \
+ env-util.c \
+ event-filter.c \
+ event-filter-lexer.l \
+ event-filter-parser.y \
+ event-log.c \
+ execv-const.c \
+ failures.c \
+ fd-util.c \
+ fdatasync-path.c \
+ fdpass.c \
+ file-cache.c \
+ file-create-locked.c \
+ file-copy.c \
+ file-dotlock.c \
+ file-lock.c \
+ file-set-size.c \
+ guid.c \
+ hash.c \
+ hash-format.c \
+ hash-method.c \
+ hash2.c \
+ hex-binary.c \
+ hex-dec.c \
+ hmac.c \
+ hmac-cram-md5.c \
+ home-expand.c \
+ hook-build.c \
+ hostpid.c \
+ imem.c \
+ ipwd.c \
+ iostream.c \
+ iostream-pump.c \
+ iostream-proxy.c \
+ iostream-rawlog.c \
+ iostream-temp.c \
+ iso8601-date.c \
+ istream.c \
+ istream-base64-decoder.c \
+ istream-base64-encoder.c \
+ istream-callback.c \
+ istream-chain.c \
+ istream-concat.c \
+ istream-crlf.c \
+ istream-data.c \
+ istream-failure-at.c \
+ istream-file.c \
+ istream-hash.c \
+ istream-jsonstr.c \
+ istream-limit.c \
+ istream-multiplex.c \
+ istream-rawlog.c \
+ istream-seekable.c \
+ istream-sized.c \
+ istream-tee.c \
+ istream-try.c \
+ istream-timeout.c \
+ istream-unix.c \
+ ioloop.c \
+ ioloop-iolist.c \
+ ioloop-notify-none.c \
+ ioloop-notify-fd.c \
+ ioloop-notify-inotify.c \
+ ioloop-notify-kqueue.c \
+ ioloop-poll.c \
+ ioloop-select.c \
+ ioloop-epoll.c \
+ ioloop-kqueue.c \
+ json-parser.c \
+ json-tree.c \
+ lib.c \
+ lib-event.c \
+ lib-signals.c \
+ log-throttle.c \
+ md4.c \
+ md5.c \
+ memarea.c \
+ mempool.c \
+ mempool-allocfree.c \
+ mempool-alloconly.c \
+ mempool-datastack.c \
+ mempool-system.c \
+ mempool-unsafe-datastack.c \
+ mkdir-parents.c \
+ mmap-anon.c \
+ mmap-util.c \
+ module-dir.c \
+ mountpoint.c \
+ net.c \
+ nfs-workarounds.c \
+ numpack.c \
+ ostream.c \
+ ostream-buffer.c \
+ ostream-failure-at.c \
+ ostream-file.c \
+ ostream-hash.c \
+ ostream-multiplex.c \
+ ostream-null.c \
+ ostream-rawlog.c \
+ ostream-unix.c \
+ ostream-wrapper.c \
+ path-util.c \
+ pkcs5.c \
+ primes.c \
+ printf-format-fix.c \
+ process-stat.c \
+ process-title.c \
+ priorityq.c \
+ randgen.c \
+ rand.c \
+ read-full.c \
+ restrict-access.c \
+ restrict-process-size.c \
+ safe-memset.c \
+ safe-mkdir.c \
+ safe-mkstemp.c \
+ sendfile-util.c \
+ seq-range-array.c \
+ seq-set-builder.c \
+ sha1.c \
+ sha2.c \
+ sha3.c \
+ sleep.c \
+ sort.c \
+ stats-dist.c \
+ str.c \
+ str-find.c \
+ str-sanitize.c \
+ str-table.c \
+ strescape.c \
+ strfuncs.c \
+ strnum.c \
+ time-util.c \
+ unix-socket-create.c \
+ unlink-directory.c \
+ unlink-old-files.c \
+ unichar.c \
+ uri-util.c \
+ utc-offset.c \
+ utc-mktime.c \
+ var-expand.c \
+ var-expand-if.c \
+ wildcard-match.c \
+ write-full.c
+
+headers = \
+ aqueue.h \
+ array.h \
+ array-decl.h \
+ askpass.h \
+ backtrace-string.h \
+ base32.h \
+ base64.h \
+ bits.h \
+ bsearch-insert-pos.h \
+ buffer.h \
+ byteorder.h \
+ child-wait.h \
+ compat.h \
+ connection.h \
+ cpu-limit.h \
+ crc32.h \
+ data-stack.h \
+ eacces-error.h \
+ env-util.h \
+ event-filter.h \
+ event-filter-parser.h \
+ event-filter-private.h \
+ event-log.h \
+ execv-const.h \
+ failures.h \
+ failures-private.h \
+ fd-util.h \
+ fdatasync-path.h \
+ fdpass.h \
+ file-cache.h \
+ file-create-locked.h \
+ file-copy.h \
+ file-dotlock.h \
+ file-lock.h \
+ file-set-size.h \
+ fsync-mode.h \
+ guid.h \
+ hash.h \
+ hash-decl.h \
+ hash-format.h \
+ hash-method.h \
+ hash2.h \
+ hex-binary.h \
+ hex-dec.h \
+ hmac.h \
+ hmac-cram-md5.h \
+ home-expand.h \
+ hook-build.h \
+ hostpid.h \
+ imem.h \
+ ipwd.h \
+ iostream.h \
+ iostream-private.h \
+ iostream-pump.h \
+ iostream-proxy.h \
+ iostream-rawlog.h \
+ iostream-rawlog-private.h \
+ iostream-temp.h \
+ iso8601-date.h \
+ istream.h \
+ istream-base64.h \
+ istream-callback.h \
+ istream-chain.h \
+ istream-concat.h \
+ istream-crlf.h \
+ istream-failure-at.h \
+ istream-file-private.h \
+ istream-hash.h \
+ istream-jsonstr.h \
+ istream-multiplex.h \
+ istream-private.h \
+ istream-rawlog.h \
+ istream-seekable.h \
+ istream-sized.h \
+ istream-tee.h \
+ istream-try.h \
+ istream-timeout.h \
+ istream-unix.h \
+ ioloop.h \
+ ioloop-iolist.h \
+ ioloop-private.h \
+ ioloop-notify-fd.h \
+ json-parser.h \
+ json-tree.h \
+ lib.h \
+ lib-event.h \
+ lib-event-private.h \
+ lib-signals.h \
+ llist.h \
+ log-throttle.h \
+ macros.h \
+ md4.h \
+ md5.h \
+ malloc-overflow.h \
+ memarea.h \
+ mempool.h \
+ mkdir-parents.h \
+ mmap-util.h \
+ module-context.h \
+ module-dir.h \
+ mountpoint.h \
+ net.h \
+ nfs-workarounds.h \
+ numpack.h \
+ ostream.h \
+ ostream-failure-at.h \
+ ostream-file-private.h \
+ ostream-hash.h \
+ ostream-multiplex.h \
+ ostream-private.h \
+ ostream-null.h \
+ ostream-rawlog.h \
+ ostream-unix.h \
+ ostream-wrapper.h \
+ path-util.h \
+ pkcs5.h \
+ primes.h \
+ printf-format-fix.h \
+ process-stat.h \
+ process-title.h \
+ priorityq.h \
+ randgen.h \
+ read-full.h \
+ restrict-access.h \
+ restrict-process-size.h \
+ safe-memset.h \
+ safe-mkdir.h \
+ safe-mkstemp.h \
+ sendfile-util.h \
+ seq-range-array.h \
+ seq-set-builder.h \
+ sha-common.h \
+ sha1.h \
+ sha2.h \
+ sha3.h \
+ sleep.h \
+ sort.h \
+ stats-dist.h \
+ str.h \
+ str-find.h \
+ str-sanitize.h \
+ str-table.h \
+ strescape.h \
+ strfuncs.h \
+ strnum.h \
+ time-util.h \
+ unix-socket-create.h \
+ unlink-directory.h \
+ unlink-old-files.h \
+ unichar.h \
+ uri-util.h \
+ utc-offset.h \
+ utc-mktime.h \
+ var-expand.h \
+ var-expand-private.h \
+ wildcard-match.h \
+ write-full.h
+
+test_programs = test-lib
+test_lib_CPPFLAGS = \
+ -I$(top_srcdir)/src/lib-test
+
+test_libs = \
+ ../lib-test/libtest.la \
+ liblib.la
+
+test_lib_SOURCES = \
+ test-lib.c \
+ test-array.c \
+ test-aqueue.c \
+ test-backtrace.c \
+ test-base32.c \
+ test-base64.c \
+ test-bits.c \
+ test-bsearch-insert-pos.c \
+ test-buffer.c \
+ test-buffer-istream.c \
+ test-byteorder.c \
+ test-connection.c \
+ test-crc32.c \
+ test-cpu-limit.c \
+ test-data-stack.c \
+ test-env-util.c \
+ test-event-category-register.c \
+ test-event-filter.c \
+ test-event-filter-expr.c \
+ test-event-filter-merge.c \
+ test-event-filter-parser.c \
+ test-event-flatten.c \
+ test-event-log.c \
+ test-failures.c \
+ test-fd-util.c \
+ test-file-cache.c \
+ test-file-create-locked.c \
+ test-guid.c \
+ test-hash.c \
+ test-hash-format.c \
+ test-hash-method.c \
+ test-hmac.c \
+ test-hex-binary.c \
+ test-imem.c \
+ test-ioloop.c \
+ test-iso8601-date.c \
+ test-iostream-pump.c \
+ test-iostream-proxy.c \
+ test-iostream-temp.c \
+ test-istream.c \
+ test-istream-base64-decoder.c \
+ test-istream-base64-encoder.c \
+ test-istream-chain.c \
+ test-istream-concat.c \
+ test-istream-crlf.c \
+ test-istream-failure-at.c \
+ test-istream-jsonstr.c \
+ test-istream-multiplex.c \
+ test-istream-seekable.c \
+ test-istream-sized.c \
+ test-istream-tee.c \
+ test-istream-try.c \
+ test-istream-unix.c \
+ test-json-parser.c \
+ test-json-tree.c \
+ test-lib-event.c \
+ test-lib-signals.c \
+ test-llist.c \
+ test-log-throttle.c \
+ test-macros.c \
+ test-malloc-overflow.c \
+ test-memarea.c \
+ test-mempool.c \
+ test-mempool-allocfree.c \
+ test-mempool-alloconly.c \
+ test-pkcs5.c \
+ test-net.c \
+ test-numpack.c \
+ test-ostream-buffer.c \
+ test-ostream-failure-at.c \
+ test-ostream-file.c \
+ test-ostream-multiplex.c \
+ test-multiplex.c \
+ test-path-util.c \
+ test-primes.c \
+ test-printf-format-fix.c \
+ test-priorityq.c \
+ test-random.c \
+ test-seq-range-array.c \
+ test-seq-set-builder.c \
+ test-stats-dist.c \
+ test-str.c \
+ test-strescape.c \
+ test-strfuncs.c \
+ test-strnum.c \
+ test-str-find.c \
+ test-str-sanitize.c \
+ test-str-table.c \
+ test-time-util.c \
+ test-unichar.c \
+ test-utc-mktime.c \
+ test-uri.c \
+ test-var-expand.c \
+ test-wildcard-match.c
+
+test_headers = \
+ test-lib.h \
+ test-lib.inc
+
+test_lib_LDADD = $(test_libs) -lm
+test_lib_DEPENDENCIES = $(test_libs)
+pkginc_libdir = $(pkgincludedir)
+pkginc_lib_HEADERS = $(headers)
+noinst_HEADERS = $(test_headers)
+all: $(BUILT_SOURCES)
+ $(MAKE) $(AM_MAKEFLAGS) all-am
+
+.SUFFIXES:
+.SUFFIXES: .c .l .lo .o .obj .y
+$(srcdir)/Makefile.in: @MAINTAINER_MODE_TRUE@ $(srcdir)/Makefile.am $(am__configure_deps)
+ @for dep in $?; do \
+ case '$(am__configure_deps)' in \
+ *$$dep*) \
+ ( cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh ) \
+ && { if test -f $@; then exit 0; else break; fi; }; \
+ exit 1;; \
+ esac; \
+ done; \
+ echo ' cd $(top_srcdir) && $(AUTOMAKE) --foreign src/lib/Makefile'; \
+ $(am__cd) $(top_srcdir) && \
+ $(AUTOMAKE) --foreign src/lib/Makefile
+Makefile: $(srcdir)/Makefile.in $(top_builddir)/config.status
+ @case '$?' in \
+ *config.status*) \
+ cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh;; \
+ *) \
+ echo ' cd $(top_builddir) && $(SHELL) ./config.status $(subdir)/$@ $(am__maybe_remake_depfiles)'; \
+ cd $(top_builddir) && $(SHELL) ./config.status $(subdir)/$@ $(am__maybe_remake_depfiles);; \
+ esac;
+
+$(top_builddir)/config.status: $(top_srcdir)/configure $(CONFIG_STATUS_DEPENDENCIES)
+ cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh
+
+$(top_srcdir)/configure: @MAINTAINER_MODE_TRUE@ $(am__configure_deps)
+ cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh
+$(ACLOCAL_M4): @MAINTAINER_MODE_TRUE@ $(am__aclocal_m4_deps)
+ cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh
+$(am__aclocal_m4_deps):
+
+clean-noinstPROGRAMS:
+ @list='$(noinst_PROGRAMS)'; test -n "$$list" || exit 0; \
+ echo " rm -f" $$list; \
+ rm -f $$list || exit $$?; \
+ test -n "$(EXEEXT)" || exit 0; \
+ list=`for p in $$list; do echo "$$p"; done | sed 's/$(EXEEXT)$$//'`; \
+ echo " rm -f" $$list; \
+ rm -f $$list
+
+clean-noinstLTLIBRARIES:
+ -test -z "$(noinst_LTLIBRARIES)" || rm -f $(noinst_LTLIBRARIES)
+ @list='$(noinst_LTLIBRARIES)'; \
+ locs=`for p in $$list; do echo $$p; done | \
+ sed 's|^[^/]*$$|.|; s|/[^/]*$$||; s|$$|/so_locations|' | \
+ sort -u`; \
+ test -z "$$locs" || { \
+ echo rm -f $${locs}; \
+ rm -f $${locs}; \
+ }
+
+liblib.la: $(liblib_la_OBJECTS) $(liblib_la_DEPENDENCIES) $(EXTRA_liblib_la_DEPENDENCIES)
+ $(AM_V_CCLD)$(LINK) $(liblib_la_OBJECTS) $(liblib_la_LIBADD) $(LIBS)
+
+test-lib$(EXEEXT): $(test_lib_OBJECTS) $(test_lib_DEPENDENCIES) $(EXTRA_test_lib_DEPENDENCIES)
+ @rm -f test-lib$(EXEEXT)
+ $(AM_V_CCLD)$(LINK) $(test_lib_OBJECTS) $(test_lib_LDADD) $(LIBS)
+
+mostlyclean-compile:
+ -rm -f *.$(OBJEXT)
+
+distclean-compile:
+ -rm -f *.tab.c
+
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/aqueue.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/array.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/askpass.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/backtrace-string.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/base32.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/base64.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/bits.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/bsearch-insert-pos.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/buffer-istream.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/buffer.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/child-wait.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/compat.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/connection.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/cpu-limit.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/crc32.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/data-stack.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/eacces-error.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/env-util.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/event-filter-lexer.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/event-filter-parser.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/event-filter.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/event-log.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/execv-const.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/failures.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/fd-util.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/fdatasync-path.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/fdpass.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/file-cache.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/file-copy.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/file-create-locked.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/file-dotlock.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/file-lock.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/file-set-size.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/guid.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/hash-format.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/hash-method.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/hash.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/hash2.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/hex-binary.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/hex-dec.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/hmac-cram-md5.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/hmac.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/home-expand.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/hook-build.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/hostpid.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/imem.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/ioloop-epoll.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/ioloop-iolist.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/ioloop-kqueue.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/ioloop-notify-fd.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/ioloop-notify-inotify.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/ioloop-notify-kqueue.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/ioloop-notify-none.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/ioloop-poll.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/ioloop-select.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/ioloop.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/iostream-proxy.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/iostream-pump.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/iostream-rawlog.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/iostream-temp.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/iostream.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/ipwd.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/iso8601-date.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/istream-base64-decoder.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/istream-base64-encoder.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/istream-callback.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/istream-chain.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/istream-concat.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/istream-crlf.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/istream-data.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/istream-failure-at.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/istream-file.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/istream-hash.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/istream-jsonstr.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/istream-limit.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/istream-multiplex.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/istream-rawlog.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/istream-seekable.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/istream-sized.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/istream-tee.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/istream-timeout.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/istream-try.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/istream-unix.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/istream.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/json-parser.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/json-tree.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/lib-event.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/lib-signals.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/lib.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/log-throttle.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/md4.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/md5.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/memarea.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/mempool-allocfree.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/mempool-alloconly.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/mempool-datastack.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/mempool-system.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/mempool-unsafe-datastack.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/mempool.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/mkdir-parents.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/mmap-anon.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/mmap-util.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/module-dir.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/mountpoint.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/net.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/nfs-workarounds.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/numpack.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/ostream-buffer.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/ostream-failure-at.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/ostream-file.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/ostream-hash.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/ostream-multiplex.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/ostream-null.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/ostream-rawlog.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/ostream-unix.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/ostream-wrapper.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/ostream.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/path-util.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/pkcs5.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/primes.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/printf-format-fix.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/priorityq.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/process-stat.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/process-title.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/rand.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/randgen.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/read-full.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/restrict-access.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/restrict-process-size.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/safe-memset.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/safe-mkdir.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/safe-mkstemp.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/sendfile-util.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/seq-range-array.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/seq-set-builder.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/sha1.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/sha2.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/sha3.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/sleep.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/sort.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/stats-dist.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/str-find.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/str-sanitize.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/str-table.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/str.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/strescape.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/strfuncs.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/strnum.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/test_lib-test-aqueue.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/test_lib-test-array.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/test_lib-test-backtrace.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/test_lib-test-base32.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/test_lib-test-base64.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/test_lib-test-bits.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/test_lib-test-bsearch-insert-pos.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/test_lib-test-buffer-istream.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/test_lib-test-buffer.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/test_lib-test-byteorder.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/test_lib-test-connection.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/test_lib-test-cpu-limit.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/test_lib-test-crc32.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/test_lib-test-data-stack.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/test_lib-test-env-util.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/test_lib-test-event-category-register.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/test_lib-test-event-filter-expr.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/test_lib-test-event-filter-merge.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/test_lib-test-event-filter-parser.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/test_lib-test-event-filter.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/test_lib-test-event-flatten.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/test_lib-test-event-log.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/test_lib-test-failures.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/test_lib-test-fd-util.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/test_lib-test-file-cache.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/test_lib-test-file-create-locked.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/test_lib-test-guid.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/test_lib-test-hash-format.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/test_lib-test-hash-method.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/test_lib-test-hash.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/test_lib-test-hex-binary.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/test_lib-test-hmac.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/test_lib-test-imem.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/test_lib-test-ioloop.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/test_lib-test-iostream-proxy.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/test_lib-test-iostream-pump.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/test_lib-test-iostream-temp.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/test_lib-test-iso8601-date.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/test_lib-test-istream-base64-decoder.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/test_lib-test-istream-base64-encoder.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/test_lib-test-istream-chain.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/test_lib-test-istream-concat.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/test_lib-test-istream-crlf.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/test_lib-test-istream-failure-at.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/test_lib-test-istream-jsonstr.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/test_lib-test-istream-multiplex.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/test_lib-test-istream-seekable.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/test_lib-test-istream-sized.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/test_lib-test-istream-tee.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/test_lib-test-istream-try.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/test_lib-test-istream-unix.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/test_lib-test-istream.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/test_lib-test-json-parser.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/test_lib-test-json-tree.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/test_lib-test-lib-event.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/test_lib-test-lib-signals.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/test_lib-test-lib.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/test_lib-test-llist.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/test_lib-test-log-throttle.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/test_lib-test-macros.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/test_lib-test-malloc-overflow.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/test_lib-test-memarea.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/test_lib-test-mempool-allocfree.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/test_lib-test-mempool-alloconly.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/test_lib-test-mempool.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/test_lib-test-multiplex.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/test_lib-test-net.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/test_lib-test-numpack.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/test_lib-test-ostream-buffer.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/test_lib-test-ostream-failure-at.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/test_lib-test-ostream-file.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/test_lib-test-ostream-multiplex.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/test_lib-test-path-util.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/test_lib-test-pkcs5.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/test_lib-test-primes.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/test_lib-test-printf-format-fix.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/test_lib-test-priorityq.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/test_lib-test-random.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/test_lib-test-seq-range-array.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/test_lib-test-seq-set-builder.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/test_lib-test-stats-dist.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/test_lib-test-str-find.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/test_lib-test-str-sanitize.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/test_lib-test-str-table.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/test_lib-test-str.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/test_lib-test-strescape.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/test_lib-test-strfuncs.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/test_lib-test-strnum.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/test_lib-test-time-util.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/test_lib-test-unichar.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/test_lib-test-uri.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/test_lib-test-utc-mktime.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/test_lib-test-var-expand.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/test_lib-test-wildcard-match.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/time-util.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/unichar.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/unix-socket-create.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/unlink-directory.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/unlink-old-files.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/uri-util.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/utc-mktime.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/utc-offset.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/var-expand-if.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/var-expand.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/wildcard-match.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/write-full.Plo@am__quote@ # am--include-marker
+
+$(am__depfiles_remade):
+ @$(MKDIR_P) $(@D)
+ @echo '# dummy' >$@-t && $(am__mv) $@-t $@
+
+am--depfiles: $(am__depfiles_remade)
+
+.c.o:
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(COMPILE) -MT $@ -MD -MP -MF $(DEPDIR)/$*.Tpo -c -o $@ $<
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/$*.Tpo $(DEPDIR)/$*.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='$<' object='$@' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(COMPILE) -c -o $@ $<
+
+.c.obj:
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(COMPILE) -MT $@ -MD -MP -MF $(DEPDIR)/$*.Tpo -c -o $@ `$(CYGPATH_W) '$<'`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/$*.Tpo $(DEPDIR)/$*.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='$<' object='$@' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(COMPILE) -c -o $@ `$(CYGPATH_W) '$<'`
+
+.c.lo:
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(LTCOMPILE) -MT $@ -MD -MP -MF $(DEPDIR)/$*.Tpo -c -o $@ $<
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/$*.Tpo $(DEPDIR)/$*.Plo
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='$<' object='$@' libtool=yes @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(LTCOMPILE) -c -o $@ $<
+
+test_lib-test-lib.o: test-lib.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT test_lib-test-lib.o -MD -MP -MF $(DEPDIR)/test_lib-test-lib.Tpo -c -o test_lib-test-lib.o `test -f 'test-lib.c' || echo '$(srcdir)/'`test-lib.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/test_lib-test-lib.Tpo $(DEPDIR)/test_lib-test-lib.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='test-lib.c' object='test_lib-test-lib.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o test_lib-test-lib.o `test -f 'test-lib.c' || echo '$(srcdir)/'`test-lib.c
+
+test_lib-test-lib.obj: test-lib.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT test_lib-test-lib.obj -MD -MP -MF $(DEPDIR)/test_lib-test-lib.Tpo -c -o test_lib-test-lib.obj `if test -f 'test-lib.c'; then $(CYGPATH_W) 'test-lib.c'; else $(CYGPATH_W) '$(srcdir)/test-lib.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/test_lib-test-lib.Tpo $(DEPDIR)/test_lib-test-lib.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='test-lib.c' object='test_lib-test-lib.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o test_lib-test-lib.obj `if test -f 'test-lib.c'; then $(CYGPATH_W) 'test-lib.c'; else $(CYGPATH_W) '$(srcdir)/test-lib.c'; fi`
+
+test_lib-test-array.o: test-array.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT test_lib-test-array.o -MD -MP -MF $(DEPDIR)/test_lib-test-array.Tpo -c -o test_lib-test-array.o `test -f 'test-array.c' || echo '$(srcdir)/'`test-array.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/test_lib-test-array.Tpo $(DEPDIR)/test_lib-test-array.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='test-array.c' object='test_lib-test-array.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o test_lib-test-array.o `test -f 'test-array.c' || echo '$(srcdir)/'`test-array.c
+
+test_lib-test-array.obj: test-array.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT test_lib-test-array.obj -MD -MP -MF $(DEPDIR)/test_lib-test-array.Tpo -c -o test_lib-test-array.obj `if test -f 'test-array.c'; then $(CYGPATH_W) 'test-array.c'; else $(CYGPATH_W) '$(srcdir)/test-array.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/test_lib-test-array.Tpo $(DEPDIR)/test_lib-test-array.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='test-array.c' object='test_lib-test-array.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o test_lib-test-array.obj `if test -f 'test-array.c'; then $(CYGPATH_W) 'test-array.c'; else $(CYGPATH_W) '$(srcdir)/test-array.c'; fi`
+
+test_lib-test-aqueue.o: test-aqueue.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT test_lib-test-aqueue.o -MD -MP -MF $(DEPDIR)/test_lib-test-aqueue.Tpo -c -o test_lib-test-aqueue.o `test -f 'test-aqueue.c' || echo '$(srcdir)/'`test-aqueue.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/test_lib-test-aqueue.Tpo $(DEPDIR)/test_lib-test-aqueue.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='test-aqueue.c' object='test_lib-test-aqueue.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o test_lib-test-aqueue.o `test -f 'test-aqueue.c' || echo '$(srcdir)/'`test-aqueue.c
+
+test_lib-test-aqueue.obj: test-aqueue.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT test_lib-test-aqueue.obj -MD -MP -MF $(DEPDIR)/test_lib-test-aqueue.Tpo -c -o test_lib-test-aqueue.obj `if test -f 'test-aqueue.c'; then $(CYGPATH_W) 'test-aqueue.c'; else $(CYGPATH_W) '$(srcdir)/test-aqueue.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/test_lib-test-aqueue.Tpo $(DEPDIR)/test_lib-test-aqueue.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='test-aqueue.c' object='test_lib-test-aqueue.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o test_lib-test-aqueue.obj `if test -f 'test-aqueue.c'; then $(CYGPATH_W) 'test-aqueue.c'; else $(CYGPATH_W) '$(srcdir)/test-aqueue.c'; fi`
+
+test_lib-test-backtrace.o: test-backtrace.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT test_lib-test-backtrace.o -MD -MP -MF $(DEPDIR)/test_lib-test-backtrace.Tpo -c -o test_lib-test-backtrace.o `test -f 'test-backtrace.c' || echo '$(srcdir)/'`test-backtrace.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/test_lib-test-backtrace.Tpo $(DEPDIR)/test_lib-test-backtrace.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='test-backtrace.c' object='test_lib-test-backtrace.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o test_lib-test-backtrace.o `test -f 'test-backtrace.c' || echo '$(srcdir)/'`test-backtrace.c
+
+test_lib-test-backtrace.obj: test-backtrace.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT test_lib-test-backtrace.obj -MD -MP -MF $(DEPDIR)/test_lib-test-backtrace.Tpo -c -o test_lib-test-backtrace.obj `if test -f 'test-backtrace.c'; then $(CYGPATH_W) 'test-backtrace.c'; else $(CYGPATH_W) '$(srcdir)/test-backtrace.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/test_lib-test-backtrace.Tpo $(DEPDIR)/test_lib-test-backtrace.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='test-backtrace.c' object='test_lib-test-backtrace.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o test_lib-test-backtrace.obj `if test -f 'test-backtrace.c'; then $(CYGPATH_W) 'test-backtrace.c'; else $(CYGPATH_W) '$(srcdir)/test-backtrace.c'; fi`
+
+test_lib-test-base32.o: test-base32.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT test_lib-test-base32.o -MD -MP -MF $(DEPDIR)/test_lib-test-base32.Tpo -c -o test_lib-test-base32.o `test -f 'test-base32.c' || echo '$(srcdir)/'`test-base32.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/test_lib-test-base32.Tpo $(DEPDIR)/test_lib-test-base32.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='test-base32.c' object='test_lib-test-base32.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o test_lib-test-base32.o `test -f 'test-base32.c' || echo '$(srcdir)/'`test-base32.c
+
+test_lib-test-base32.obj: test-base32.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT test_lib-test-base32.obj -MD -MP -MF $(DEPDIR)/test_lib-test-base32.Tpo -c -o test_lib-test-base32.obj `if test -f 'test-base32.c'; then $(CYGPATH_W) 'test-base32.c'; else $(CYGPATH_W) '$(srcdir)/test-base32.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/test_lib-test-base32.Tpo $(DEPDIR)/test_lib-test-base32.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='test-base32.c' object='test_lib-test-base32.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o test_lib-test-base32.obj `if test -f 'test-base32.c'; then $(CYGPATH_W) 'test-base32.c'; else $(CYGPATH_W) '$(srcdir)/test-base32.c'; fi`
+
+test_lib-test-base64.o: test-base64.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT test_lib-test-base64.o -MD -MP -MF $(DEPDIR)/test_lib-test-base64.Tpo -c -o test_lib-test-base64.o `test -f 'test-base64.c' || echo '$(srcdir)/'`test-base64.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/test_lib-test-base64.Tpo $(DEPDIR)/test_lib-test-base64.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='test-base64.c' object='test_lib-test-base64.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o test_lib-test-base64.o `test -f 'test-base64.c' || echo '$(srcdir)/'`test-base64.c
+
+test_lib-test-base64.obj: test-base64.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT test_lib-test-base64.obj -MD -MP -MF $(DEPDIR)/test_lib-test-base64.Tpo -c -o test_lib-test-base64.obj `if test -f 'test-base64.c'; then $(CYGPATH_W) 'test-base64.c'; else $(CYGPATH_W) '$(srcdir)/test-base64.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/test_lib-test-base64.Tpo $(DEPDIR)/test_lib-test-base64.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='test-base64.c' object='test_lib-test-base64.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o test_lib-test-base64.obj `if test -f 'test-base64.c'; then $(CYGPATH_W) 'test-base64.c'; else $(CYGPATH_W) '$(srcdir)/test-base64.c'; fi`
+
+test_lib-test-bits.o: test-bits.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT test_lib-test-bits.o -MD -MP -MF $(DEPDIR)/test_lib-test-bits.Tpo -c -o test_lib-test-bits.o `test -f 'test-bits.c' || echo '$(srcdir)/'`test-bits.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/test_lib-test-bits.Tpo $(DEPDIR)/test_lib-test-bits.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='test-bits.c' object='test_lib-test-bits.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o test_lib-test-bits.o `test -f 'test-bits.c' || echo '$(srcdir)/'`test-bits.c
+
+test_lib-test-bits.obj: test-bits.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT test_lib-test-bits.obj -MD -MP -MF $(DEPDIR)/test_lib-test-bits.Tpo -c -o test_lib-test-bits.obj `if test -f 'test-bits.c'; then $(CYGPATH_W) 'test-bits.c'; else $(CYGPATH_W) '$(srcdir)/test-bits.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/test_lib-test-bits.Tpo $(DEPDIR)/test_lib-test-bits.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='test-bits.c' object='test_lib-test-bits.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o test_lib-test-bits.obj `if test -f 'test-bits.c'; then $(CYGPATH_W) 'test-bits.c'; else $(CYGPATH_W) '$(srcdir)/test-bits.c'; fi`
+
+test_lib-test-bsearch-insert-pos.o: test-bsearch-insert-pos.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT test_lib-test-bsearch-insert-pos.o -MD -MP -MF $(DEPDIR)/test_lib-test-bsearch-insert-pos.Tpo -c -o test_lib-test-bsearch-insert-pos.o `test -f 'test-bsearch-insert-pos.c' || echo '$(srcdir)/'`test-bsearch-insert-pos.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/test_lib-test-bsearch-insert-pos.Tpo $(DEPDIR)/test_lib-test-bsearch-insert-pos.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='test-bsearch-insert-pos.c' object='test_lib-test-bsearch-insert-pos.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o test_lib-test-bsearch-insert-pos.o `test -f 'test-bsearch-insert-pos.c' || echo '$(srcdir)/'`test-bsearch-insert-pos.c
+
+test_lib-test-bsearch-insert-pos.obj: test-bsearch-insert-pos.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT test_lib-test-bsearch-insert-pos.obj -MD -MP -MF $(DEPDIR)/test_lib-test-bsearch-insert-pos.Tpo -c -o test_lib-test-bsearch-insert-pos.obj `if test -f 'test-bsearch-insert-pos.c'; then $(CYGPATH_W) 'test-bsearch-insert-pos.c'; else $(CYGPATH_W) '$(srcdir)/test-bsearch-insert-pos.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/test_lib-test-bsearch-insert-pos.Tpo $(DEPDIR)/test_lib-test-bsearch-insert-pos.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='test-bsearch-insert-pos.c' object='test_lib-test-bsearch-insert-pos.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o test_lib-test-bsearch-insert-pos.obj `if test -f 'test-bsearch-insert-pos.c'; then $(CYGPATH_W) 'test-bsearch-insert-pos.c'; else $(CYGPATH_W) '$(srcdir)/test-bsearch-insert-pos.c'; fi`
+
+test_lib-test-buffer.o: test-buffer.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT test_lib-test-buffer.o -MD -MP -MF $(DEPDIR)/test_lib-test-buffer.Tpo -c -o test_lib-test-buffer.o `test -f 'test-buffer.c' || echo '$(srcdir)/'`test-buffer.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/test_lib-test-buffer.Tpo $(DEPDIR)/test_lib-test-buffer.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='test-buffer.c' object='test_lib-test-buffer.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o test_lib-test-buffer.o `test -f 'test-buffer.c' || echo '$(srcdir)/'`test-buffer.c
+
+test_lib-test-buffer.obj: test-buffer.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT test_lib-test-buffer.obj -MD -MP -MF $(DEPDIR)/test_lib-test-buffer.Tpo -c -o test_lib-test-buffer.obj `if test -f 'test-buffer.c'; then $(CYGPATH_W) 'test-buffer.c'; else $(CYGPATH_W) '$(srcdir)/test-buffer.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/test_lib-test-buffer.Tpo $(DEPDIR)/test_lib-test-buffer.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='test-buffer.c' object='test_lib-test-buffer.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o test_lib-test-buffer.obj `if test -f 'test-buffer.c'; then $(CYGPATH_W) 'test-buffer.c'; else $(CYGPATH_W) '$(srcdir)/test-buffer.c'; fi`
+
+test_lib-test-buffer-istream.o: test-buffer-istream.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT test_lib-test-buffer-istream.o -MD -MP -MF $(DEPDIR)/test_lib-test-buffer-istream.Tpo -c -o test_lib-test-buffer-istream.o `test -f 'test-buffer-istream.c' || echo '$(srcdir)/'`test-buffer-istream.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/test_lib-test-buffer-istream.Tpo $(DEPDIR)/test_lib-test-buffer-istream.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='test-buffer-istream.c' object='test_lib-test-buffer-istream.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o test_lib-test-buffer-istream.o `test -f 'test-buffer-istream.c' || echo '$(srcdir)/'`test-buffer-istream.c
+
+test_lib-test-buffer-istream.obj: test-buffer-istream.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT test_lib-test-buffer-istream.obj -MD -MP -MF $(DEPDIR)/test_lib-test-buffer-istream.Tpo -c -o test_lib-test-buffer-istream.obj `if test -f 'test-buffer-istream.c'; then $(CYGPATH_W) 'test-buffer-istream.c'; else $(CYGPATH_W) '$(srcdir)/test-buffer-istream.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/test_lib-test-buffer-istream.Tpo $(DEPDIR)/test_lib-test-buffer-istream.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='test-buffer-istream.c' object='test_lib-test-buffer-istream.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o test_lib-test-buffer-istream.obj `if test -f 'test-buffer-istream.c'; then $(CYGPATH_W) 'test-buffer-istream.c'; else $(CYGPATH_W) '$(srcdir)/test-buffer-istream.c'; fi`
+
+test_lib-test-byteorder.o: test-byteorder.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT test_lib-test-byteorder.o -MD -MP -MF $(DEPDIR)/test_lib-test-byteorder.Tpo -c -o test_lib-test-byteorder.o `test -f 'test-byteorder.c' || echo '$(srcdir)/'`test-byteorder.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/test_lib-test-byteorder.Tpo $(DEPDIR)/test_lib-test-byteorder.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='test-byteorder.c' object='test_lib-test-byteorder.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o test_lib-test-byteorder.o `test -f 'test-byteorder.c' || echo '$(srcdir)/'`test-byteorder.c
+
+test_lib-test-byteorder.obj: test-byteorder.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT test_lib-test-byteorder.obj -MD -MP -MF $(DEPDIR)/test_lib-test-byteorder.Tpo -c -o test_lib-test-byteorder.obj `if test -f 'test-byteorder.c'; then $(CYGPATH_W) 'test-byteorder.c'; else $(CYGPATH_W) '$(srcdir)/test-byteorder.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/test_lib-test-byteorder.Tpo $(DEPDIR)/test_lib-test-byteorder.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='test-byteorder.c' object='test_lib-test-byteorder.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o test_lib-test-byteorder.obj `if test -f 'test-byteorder.c'; then $(CYGPATH_W) 'test-byteorder.c'; else $(CYGPATH_W) '$(srcdir)/test-byteorder.c'; fi`
+
+test_lib-test-connection.o: test-connection.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT test_lib-test-connection.o -MD -MP -MF $(DEPDIR)/test_lib-test-connection.Tpo -c -o test_lib-test-connection.o `test -f 'test-connection.c' || echo '$(srcdir)/'`test-connection.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/test_lib-test-connection.Tpo $(DEPDIR)/test_lib-test-connection.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='test-connection.c' object='test_lib-test-connection.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o test_lib-test-connection.o `test -f 'test-connection.c' || echo '$(srcdir)/'`test-connection.c
+
+test_lib-test-connection.obj: test-connection.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT test_lib-test-connection.obj -MD -MP -MF $(DEPDIR)/test_lib-test-connection.Tpo -c -o test_lib-test-connection.obj `if test -f 'test-connection.c'; then $(CYGPATH_W) 'test-connection.c'; else $(CYGPATH_W) '$(srcdir)/test-connection.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/test_lib-test-connection.Tpo $(DEPDIR)/test_lib-test-connection.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='test-connection.c' object='test_lib-test-connection.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o test_lib-test-connection.obj `if test -f 'test-connection.c'; then $(CYGPATH_W) 'test-connection.c'; else $(CYGPATH_W) '$(srcdir)/test-connection.c'; fi`
+
+test_lib-test-crc32.o: test-crc32.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT test_lib-test-crc32.o -MD -MP -MF $(DEPDIR)/test_lib-test-crc32.Tpo -c -o test_lib-test-crc32.o `test -f 'test-crc32.c' || echo '$(srcdir)/'`test-crc32.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/test_lib-test-crc32.Tpo $(DEPDIR)/test_lib-test-crc32.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='test-crc32.c' object='test_lib-test-crc32.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o test_lib-test-crc32.o `test -f 'test-crc32.c' || echo '$(srcdir)/'`test-crc32.c
+
+test_lib-test-crc32.obj: test-crc32.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT test_lib-test-crc32.obj -MD -MP -MF $(DEPDIR)/test_lib-test-crc32.Tpo -c -o test_lib-test-crc32.obj `if test -f 'test-crc32.c'; then $(CYGPATH_W) 'test-crc32.c'; else $(CYGPATH_W) '$(srcdir)/test-crc32.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/test_lib-test-crc32.Tpo $(DEPDIR)/test_lib-test-crc32.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='test-crc32.c' object='test_lib-test-crc32.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o test_lib-test-crc32.obj `if test -f 'test-crc32.c'; then $(CYGPATH_W) 'test-crc32.c'; else $(CYGPATH_W) '$(srcdir)/test-crc32.c'; fi`
+
+test_lib-test-cpu-limit.o: test-cpu-limit.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT test_lib-test-cpu-limit.o -MD -MP -MF $(DEPDIR)/test_lib-test-cpu-limit.Tpo -c -o test_lib-test-cpu-limit.o `test -f 'test-cpu-limit.c' || echo '$(srcdir)/'`test-cpu-limit.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/test_lib-test-cpu-limit.Tpo $(DEPDIR)/test_lib-test-cpu-limit.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='test-cpu-limit.c' object='test_lib-test-cpu-limit.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o test_lib-test-cpu-limit.o `test -f 'test-cpu-limit.c' || echo '$(srcdir)/'`test-cpu-limit.c
+
+test_lib-test-cpu-limit.obj: test-cpu-limit.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT test_lib-test-cpu-limit.obj -MD -MP -MF $(DEPDIR)/test_lib-test-cpu-limit.Tpo -c -o test_lib-test-cpu-limit.obj `if test -f 'test-cpu-limit.c'; then $(CYGPATH_W) 'test-cpu-limit.c'; else $(CYGPATH_W) '$(srcdir)/test-cpu-limit.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/test_lib-test-cpu-limit.Tpo $(DEPDIR)/test_lib-test-cpu-limit.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='test-cpu-limit.c' object='test_lib-test-cpu-limit.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o test_lib-test-cpu-limit.obj `if test -f 'test-cpu-limit.c'; then $(CYGPATH_W) 'test-cpu-limit.c'; else $(CYGPATH_W) '$(srcdir)/test-cpu-limit.c'; fi`
+
+test_lib-test-data-stack.o: test-data-stack.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT test_lib-test-data-stack.o -MD -MP -MF $(DEPDIR)/test_lib-test-data-stack.Tpo -c -o test_lib-test-data-stack.o `test -f 'test-data-stack.c' || echo '$(srcdir)/'`test-data-stack.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/test_lib-test-data-stack.Tpo $(DEPDIR)/test_lib-test-data-stack.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='test-data-stack.c' object='test_lib-test-data-stack.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o test_lib-test-data-stack.o `test -f 'test-data-stack.c' || echo '$(srcdir)/'`test-data-stack.c
+
+test_lib-test-data-stack.obj: test-data-stack.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT test_lib-test-data-stack.obj -MD -MP -MF $(DEPDIR)/test_lib-test-data-stack.Tpo -c -o test_lib-test-data-stack.obj `if test -f 'test-data-stack.c'; then $(CYGPATH_W) 'test-data-stack.c'; else $(CYGPATH_W) '$(srcdir)/test-data-stack.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/test_lib-test-data-stack.Tpo $(DEPDIR)/test_lib-test-data-stack.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='test-data-stack.c' object='test_lib-test-data-stack.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o test_lib-test-data-stack.obj `if test -f 'test-data-stack.c'; then $(CYGPATH_W) 'test-data-stack.c'; else $(CYGPATH_W) '$(srcdir)/test-data-stack.c'; fi`
+
+test_lib-test-env-util.o: test-env-util.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT test_lib-test-env-util.o -MD -MP -MF $(DEPDIR)/test_lib-test-env-util.Tpo -c -o test_lib-test-env-util.o `test -f 'test-env-util.c' || echo '$(srcdir)/'`test-env-util.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/test_lib-test-env-util.Tpo $(DEPDIR)/test_lib-test-env-util.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='test-env-util.c' object='test_lib-test-env-util.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o test_lib-test-env-util.o `test -f 'test-env-util.c' || echo '$(srcdir)/'`test-env-util.c
+
+test_lib-test-env-util.obj: test-env-util.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT test_lib-test-env-util.obj -MD -MP -MF $(DEPDIR)/test_lib-test-env-util.Tpo -c -o test_lib-test-env-util.obj `if test -f 'test-env-util.c'; then $(CYGPATH_W) 'test-env-util.c'; else $(CYGPATH_W) '$(srcdir)/test-env-util.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/test_lib-test-env-util.Tpo $(DEPDIR)/test_lib-test-env-util.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='test-env-util.c' object='test_lib-test-env-util.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o test_lib-test-env-util.obj `if test -f 'test-env-util.c'; then $(CYGPATH_W) 'test-env-util.c'; else $(CYGPATH_W) '$(srcdir)/test-env-util.c'; fi`
+
+test_lib-test-event-category-register.o: test-event-category-register.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT test_lib-test-event-category-register.o -MD -MP -MF $(DEPDIR)/test_lib-test-event-category-register.Tpo -c -o test_lib-test-event-category-register.o `test -f 'test-event-category-register.c' || echo '$(srcdir)/'`test-event-category-register.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/test_lib-test-event-category-register.Tpo $(DEPDIR)/test_lib-test-event-category-register.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='test-event-category-register.c' object='test_lib-test-event-category-register.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o test_lib-test-event-category-register.o `test -f 'test-event-category-register.c' || echo '$(srcdir)/'`test-event-category-register.c
+
+test_lib-test-event-category-register.obj: test-event-category-register.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT test_lib-test-event-category-register.obj -MD -MP -MF $(DEPDIR)/test_lib-test-event-category-register.Tpo -c -o test_lib-test-event-category-register.obj `if test -f 'test-event-category-register.c'; then $(CYGPATH_W) 'test-event-category-register.c'; else $(CYGPATH_W) '$(srcdir)/test-event-category-register.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/test_lib-test-event-category-register.Tpo $(DEPDIR)/test_lib-test-event-category-register.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='test-event-category-register.c' object='test_lib-test-event-category-register.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o test_lib-test-event-category-register.obj `if test -f 'test-event-category-register.c'; then $(CYGPATH_W) 'test-event-category-register.c'; else $(CYGPATH_W) '$(srcdir)/test-event-category-register.c'; fi`
+
+test_lib-test-event-filter.o: test-event-filter.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT test_lib-test-event-filter.o -MD -MP -MF $(DEPDIR)/test_lib-test-event-filter.Tpo -c -o test_lib-test-event-filter.o `test -f 'test-event-filter.c' || echo '$(srcdir)/'`test-event-filter.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/test_lib-test-event-filter.Tpo $(DEPDIR)/test_lib-test-event-filter.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='test-event-filter.c' object='test_lib-test-event-filter.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o test_lib-test-event-filter.o `test -f 'test-event-filter.c' || echo '$(srcdir)/'`test-event-filter.c
+
+test_lib-test-event-filter.obj: test-event-filter.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT test_lib-test-event-filter.obj -MD -MP -MF $(DEPDIR)/test_lib-test-event-filter.Tpo -c -o test_lib-test-event-filter.obj `if test -f 'test-event-filter.c'; then $(CYGPATH_W) 'test-event-filter.c'; else $(CYGPATH_W) '$(srcdir)/test-event-filter.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/test_lib-test-event-filter.Tpo $(DEPDIR)/test_lib-test-event-filter.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='test-event-filter.c' object='test_lib-test-event-filter.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o test_lib-test-event-filter.obj `if test -f 'test-event-filter.c'; then $(CYGPATH_W) 'test-event-filter.c'; else $(CYGPATH_W) '$(srcdir)/test-event-filter.c'; fi`
+
+test_lib-test-event-filter-expr.o: test-event-filter-expr.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT test_lib-test-event-filter-expr.o -MD -MP -MF $(DEPDIR)/test_lib-test-event-filter-expr.Tpo -c -o test_lib-test-event-filter-expr.o `test -f 'test-event-filter-expr.c' || echo '$(srcdir)/'`test-event-filter-expr.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/test_lib-test-event-filter-expr.Tpo $(DEPDIR)/test_lib-test-event-filter-expr.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='test-event-filter-expr.c' object='test_lib-test-event-filter-expr.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o test_lib-test-event-filter-expr.o `test -f 'test-event-filter-expr.c' || echo '$(srcdir)/'`test-event-filter-expr.c
+
+test_lib-test-event-filter-expr.obj: test-event-filter-expr.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT test_lib-test-event-filter-expr.obj -MD -MP -MF $(DEPDIR)/test_lib-test-event-filter-expr.Tpo -c -o test_lib-test-event-filter-expr.obj `if test -f 'test-event-filter-expr.c'; then $(CYGPATH_W) 'test-event-filter-expr.c'; else $(CYGPATH_W) '$(srcdir)/test-event-filter-expr.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/test_lib-test-event-filter-expr.Tpo $(DEPDIR)/test_lib-test-event-filter-expr.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='test-event-filter-expr.c' object='test_lib-test-event-filter-expr.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o test_lib-test-event-filter-expr.obj `if test -f 'test-event-filter-expr.c'; then $(CYGPATH_W) 'test-event-filter-expr.c'; else $(CYGPATH_W) '$(srcdir)/test-event-filter-expr.c'; fi`
+
+test_lib-test-event-filter-merge.o: test-event-filter-merge.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT test_lib-test-event-filter-merge.o -MD -MP -MF $(DEPDIR)/test_lib-test-event-filter-merge.Tpo -c -o test_lib-test-event-filter-merge.o `test -f 'test-event-filter-merge.c' || echo '$(srcdir)/'`test-event-filter-merge.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/test_lib-test-event-filter-merge.Tpo $(DEPDIR)/test_lib-test-event-filter-merge.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='test-event-filter-merge.c' object='test_lib-test-event-filter-merge.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o test_lib-test-event-filter-merge.o `test -f 'test-event-filter-merge.c' || echo '$(srcdir)/'`test-event-filter-merge.c
+
+test_lib-test-event-filter-merge.obj: test-event-filter-merge.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT test_lib-test-event-filter-merge.obj -MD -MP -MF $(DEPDIR)/test_lib-test-event-filter-merge.Tpo -c -o test_lib-test-event-filter-merge.obj `if test -f 'test-event-filter-merge.c'; then $(CYGPATH_W) 'test-event-filter-merge.c'; else $(CYGPATH_W) '$(srcdir)/test-event-filter-merge.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/test_lib-test-event-filter-merge.Tpo $(DEPDIR)/test_lib-test-event-filter-merge.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='test-event-filter-merge.c' object='test_lib-test-event-filter-merge.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o test_lib-test-event-filter-merge.obj `if test -f 'test-event-filter-merge.c'; then $(CYGPATH_W) 'test-event-filter-merge.c'; else $(CYGPATH_W) '$(srcdir)/test-event-filter-merge.c'; fi`
+
+test_lib-test-event-filter-parser.o: test-event-filter-parser.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT test_lib-test-event-filter-parser.o -MD -MP -MF $(DEPDIR)/test_lib-test-event-filter-parser.Tpo -c -o test_lib-test-event-filter-parser.o `test -f 'test-event-filter-parser.c' || echo '$(srcdir)/'`test-event-filter-parser.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/test_lib-test-event-filter-parser.Tpo $(DEPDIR)/test_lib-test-event-filter-parser.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='test-event-filter-parser.c' object='test_lib-test-event-filter-parser.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o test_lib-test-event-filter-parser.o `test -f 'test-event-filter-parser.c' || echo '$(srcdir)/'`test-event-filter-parser.c
+
+test_lib-test-event-filter-parser.obj: test-event-filter-parser.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT test_lib-test-event-filter-parser.obj -MD -MP -MF $(DEPDIR)/test_lib-test-event-filter-parser.Tpo -c -o test_lib-test-event-filter-parser.obj `if test -f 'test-event-filter-parser.c'; then $(CYGPATH_W) 'test-event-filter-parser.c'; else $(CYGPATH_W) '$(srcdir)/test-event-filter-parser.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/test_lib-test-event-filter-parser.Tpo $(DEPDIR)/test_lib-test-event-filter-parser.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='test-event-filter-parser.c' object='test_lib-test-event-filter-parser.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o test_lib-test-event-filter-parser.obj `if test -f 'test-event-filter-parser.c'; then $(CYGPATH_W) 'test-event-filter-parser.c'; else $(CYGPATH_W) '$(srcdir)/test-event-filter-parser.c'; fi`
+
+test_lib-test-event-flatten.o: test-event-flatten.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT test_lib-test-event-flatten.o -MD -MP -MF $(DEPDIR)/test_lib-test-event-flatten.Tpo -c -o test_lib-test-event-flatten.o `test -f 'test-event-flatten.c' || echo '$(srcdir)/'`test-event-flatten.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/test_lib-test-event-flatten.Tpo $(DEPDIR)/test_lib-test-event-flatten.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='test-event-flatten.c' object='test_lib-test-event-flatten.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o test_lib-test-event-flatten.o `test -f 'test-event-flatten.c' || echo '$(srcdir)/'`test-event-flatten.c
+
+test_lib-test-event-flatten.obj: test-event-flatten.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT test_lib-test-event-flatten.obj -MD -MP -MF $(DEPDIR)/test_lib-test-event-flatten.Tpo -c -o test_lib-test-event-flatten.obj `if test -f 'test-event-flatten.c'; then $(CYGPATH_W) 'test-event-flatten.c'; else $(CYGPATH_W) '$(srcdir)/test-event-flatten.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/test_lib-test-event-flatten.Tpo $(DEPDIR)/test_lib-test-event-flatten.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='test-event-flatten.c' object='test_lib-test-event-flatten.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o test_lib-test-event-flatten.obj `if test -f 'test-event-flatten.c'; then $(CYGPATH_W) 'test-event-flatten.c'; else $(CYGPATH_W) '$(srcdir)/test-event-flatten.c'; fi`
+
+test_lib-test-event-log.o: test-event-log.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT test_lib-test-event-log.o -MD -MP -MF $(DEPDIR)/test_lib-test-event-log.Tpo -c -o test_lib-test-event-log.o `test -f 'test-event-log.c' || echo '$(srcdir)/'`test-event-log.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/test_lib-test-event-log.Tpo $(DEPDIR)/test_lib-test-event-log.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='test-event-log.c' object='test_lib-test-event-log.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o test_lib-test-event-log.o `test -f 'test-event-log.c' || echo '$(srcdir)/'`test-event-log.c
+
+test_lib-test-event-log.obj: test-event-log.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT test_lib-test-event-log.obj -MD -MP -MF $(DEPDIR)/test_lib-test-event-log.Tpo -c -o test_lib-test-event-log.obj `if test -f 'test-event-log.c'; then $(CYGPATH_W) 'test-event-log.c'; else $(CYGPATH_W) '$(srcdir)/test-event-log.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/test_lib-test-event-log.Tpo $(DEPDIR)/test_lib-test-event-log.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='test-event-log.c' object='test_lib-test-event-log.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o test_lib-test-event-log.obj `if test -f 'test-event-log.c'; then $(CYGPATH_W) 'test-event-log.c'; else $(CYGPATH_W) '$(srcdir)/test-event-log.c'; fi`
+
+test_lib-test-failures.o: test-failures.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT test_lib-test-failures.o -MD -MP -MF $(DEPDIR)/test_lib-test-failures.Tpo -c -o test_lib-test-failures.o `test -f 'test-failures.c' || echo '$(srcdir)/'`test-failures.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/test_lib-test-failures.Tpo $(DEPDIR)/test_lib-test-failures.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='test-failures.c' object='test_lib-test-failures.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o test_lib-test-failures.o `test -f 'test-failures.c' || echo '$(srcdir)/'`test-failures.c
+
+test_lib-test-failures.obj: test-failures.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT test_lib-test-failures.obj -MD -MP -MF $(DEPDIR)/test_lib-test-failures.Tpo -c -o test_lib-test-failures.obj `if test -f 'test-failures.c'; then $(CYGPATH_W) 'test-failures.c'; else $(CYGPATH_W) '$(srcdir)/test-failures.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/test_lib-test-failures.Tpo $(DEPDIR)/test_lib-test-failures.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='test-failures.c' object='test_lib-test-failures.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o test_lib-test-failures.obj `if test -f 'test-failures.c'; then $(CYGPATH_W) 'test-failures.c'; else $(CYGPATH_W) '$(srcdir)/test-failures.c'; fi`
+
+test_lib-test-fd-util.o: test-fd-util.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT test_lib-test-fd-util.o -MD -MP -MF $(DEPDIR)/test_lib-test-fd-util.Tpo -c -o test_lib-test-fd-util.o `test -f 'test-fd-util.c' || echo '$(srcdir)/'`test-fd-util.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/test_lib-test-fd-util.Tpo $(DEPDIR)/test_lib-test-fd-util.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='test-fd-util.c' object='test_lib-test-fd-util.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o test_lib-test-fd-util.o `test -f 'test-fd-util.c' || echo '$(srcdir)/'`test-fd-util.c
+
+test_lib-test-fd-util.obj: test-fd-util.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT test_lib-test-fd-util.obj -MD -MP -MF $(DEPDIR)/test_lib-test-fd-util.Tpo -c -o test_lib-test-fd-util.obj `if test -f 'test-fd-util.c'; then $(CYGPATH_W) 'test-fd-util.c'; else $(CYGPATH_W) '$(srcdir)/test-fd-util.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/test_lib-test-fd-util.Tpo $(DEPDIR)/test_lib-test-fd-util.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='test-fd-util.c' object='test_lib-test-fd-util.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o test_lib-test-fd-util.obj `if test -f 'test-fd-util.c'; then $(CYGPATH_W) 'test-fd-util.c'; else $(CYGPATH_W) '$(srcdir)/test-fd-util.c'; fi`
+
+test_lib-test-file-cache.o: test-file-cache.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT test_lib-test-file-cache.o -MD -MP -MF $(DEPDIR)/test_lib-test-file-cache.Tpo -c -o test_lib-test-file-cache.o `test -f 'test-file-cache.c' || echo '$(srcdir)/'`test-file-cache.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/test_lib-test-file-cache.Tpo $(DEPDIR)/test_lib-test-file-cache.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='test-file-cache.c' object='test_lib-test-file-cache.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o test_lib-test-file-cache.o `test -f 'test-file-cache.c' || echo '$(srcdir)/'`test-file-cache.c
+
+test_lib-test-file-cache.obj: test-file-cache.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT test_lib-test-file-cache.obj -MD -MP -MF $(DEPDIR)/test_lib-test-file-cache.Tpo -c -o test_lib-test-file-cache.obj `if test -f 'test-file-cache.c'; then $(CYGPATH_W) 'test-file-cache.c'; else $(CYGPATH_W) '$(srcdir)/test-file-cache.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/test_lib-test-file-cache.Tpo $(DEPDIR)/test_lib-test-file-cache.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='test-file-cache.c' object='test_lib-test-file-cache.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o test_lib-test-file-cache.obj `if test -f 'test-file-cache.c'; then $(CYGPATH_W) 'test-file-cache.c'; else $(CYGPATH_W) '$(srcdir)/test-file-cache.c'; fi`
+
+test_lib-test-file-create-locked.o: test-file-create-locked.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT test_lib-test-file-create-locked.o -MD -MP -MF $(DEPDIR)/test_lib-test-file-create-locked.Tpo -c -o test_lib-test-file-create-locked.o `test -f 'test-file-create-locked.c' || echo '$(srcdir)/'`test-file-create-locked.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/test_lib-test-file-create-locked.Tpo $(DEPDIR)/test_lib-test-file-create-locked.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='test-file-create-locked.c' object='test_lib-test-file-create-locked.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o test_lib-test-file-create-locked.o `test -f 'test-file-create-locked.c' || echo '$(srcdir)/'`test-file-create-locked.c
+
+test_lib-test-file-create-locked.obj: test-file-create-locked.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT test_lib-test-file-create-locked.obj -MD -MP -MF $(DEPDIR)/test_lib-test-file-create-locked.Tpo -c -o test_lib-test-file-create-locked.obj `if test -f 'test-file-create-locked.c'; then $(CYGPATH_W) 'test-file-create-locked.c'; else $(CYGPATH_W) '$(srcdir)/test-file-create-locked.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/test_lib-test-file-create-locked.Tpo $(DEPDIR)/test_lib-test-file-create-locked.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='test-file-create-locked.c' object='test_lib-test-file-create-locked.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o test_lib-test-file-create-locked.obj `if test -f 'test-file-create-locked.c'; then $(CYGPATH_W) 'test-file-create-locked.c'; else $(CYGPATH_W) '$(srcdir)/test-file-create-locked.c'; fi`
+
+test_lib-test-guid.o: test-guid.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT test_lib-test-guid.o -MD -MP -MF $(DEPDIR)/test_lib-test-guid.Tpo -c -o test_lib-test-guid.o `test -f 'test-guid.c' || echo '$(srcdir)/'`test-guid.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/test_lib-test-guid.Tpo $(DEPDIR)/test_lib-test-guid.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='test-guid.c' object='test_lib-test-guid.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o test_lib-test-guid.o `test -f 'test-guid.c' || echo '$(srcdir)/'`test-guid.c
+
+test_lib-test-guid.obj: test-guid.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT test_lib-test-guid.obj -MD -MP -MF $(DEPDIR)/test_lib-test-guid.Tpo -c -o test_lib-test-guid.obj `if test -f 'test-guid.c'; then $(CYGPATH_W) 'test-guid.c'; else $(CYGPATH_W) '$(srcdir)/test-guid.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/test_lib-test-guid.Tpo $(DEPDIR)/test_lib-test-guid.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='test-guid.c' object='test_lib-test-guid.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o test_lib-test-guid.obj `if test -f 'test-guid.c'; then $(CYGPATH_W) 'test-guid.c'; else $(CYGPATH_W) '$(srcdir)/test-guid.c'; fi`
+
+test_lib-test-hash.o: test-hash.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT test_lib-test-hash.o -MD -MP -MF $(DEPDIR)/test_lib-test-hash.Tpo -c -o test_lib-test-hash.o `test -f 'test-hash.c' || echo '$(srcdir)/'`test-hash.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/test_lib-test-hash.Tpo $(DEPDIR)/test_lib-test-hash.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='test-hash.c' object='test_lib-test-hash.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o test_lib-test-hash.o `test -f 'test-hash.c' || echo '$(srcdir)/'`test-hash.c
+
+test_lib-test-hash.obj: test-hash.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT test_lib-test-hash.obj -MD -MP -MF $(DEPDIR)/test_lib-test-hash.Tpo -c -o test_lib-test-hash.obj `if test -f 'test-hash.c'; then $(CYGPATH_W) 'test-hash.c'; else $(CYGPATH_W) '$(srcdir)/test-hash.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/test_lib-test-hash.Tpo $(DEPDIR)/test_lib-test-hash.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='test-hash.c' object='test_lib-test-hash.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o test_lib-test-hash.obj `if test -f 'test-hash.c'; then $(CYGPATH_W) 'test-hash.c'; else $(CYGPATH_W) '$(srcdir)/test-hash.c'; fi`
+
+test_lib-test-hash-format.o: test-hash-format.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT test_lib-test-hash-format.o -MD -MP -MF $(DEPDIR)/test_lib-test-hash-format.Tpo -c -o test_lib-test-hash-format.o `test -f 'test-hash-format.c' || echo '$(srcdir)/'`test-hash-format.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/test_lib-test-hash-format.Tpo $(DEPDIR)/test_lib-test-hash-format.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='test-hash-format.c' object='test_lib-test-hash-format.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o test_lib-test-hash-format.o `test -f 'test-hash-format.c' || echo '$(srcdir)/'`test-hash-format.c
+
+test_lib-test-hash-format.obj: test-hash-format.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT test_lib-test-hash-format.obj -MD -MP -MF $(DEPDIR)/test_lib-test-hash-format.Tpo -c -o test_lib-test-hash-format.obj `if test -f 'test-hash-format.c'; then $(CYGPATH_W) 'test-hash-format.c'; else $(CYGPATH_W) '$(srcdir)/test-hash-format.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/test_lib-test-hash-format.Tpo $(DEPDIR)/test_lib-test-hash-format.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='test-hash-format.c' object='test_lib-test-hash-format.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o test_lib-test-hash-format.obj `if test -f 'test-hash-format.c'; then $(CYGPATH_W) 'test-hash-format.c'; else $(CYGPATH_W) '$(srcdir)/test-hash-format.c'; fi`
+
+test_lib-test-hash-method.o: test-hash-method.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT test_lib-test-hash-method.o -MD -MP -MF $(DEPDIR)/test_lib-test-hash-method.Tpo -c -o test_lib-test-hash-method.o `test -f 'test-hash-method.c' || echo '$(srcdir)/'`test-hash-method.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/test_lib-test-hash-method.Tpo $(DEPDIR)/test_lib-test-hash-method.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='test-hash-method.c' object='test_lib-test-hash-method.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o test_lib-test-hash-method.o `test -f 'test-hash-method.c' || echo '$(srcdir)/'`test-hash-method.c
+
+test_lib-test-hash-method.obj: test-hash-method.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT test_lib-test-hash-method.obj -MD -MP -MF $(DEPDIR)/test_lib-test-hash-method.Tpo -c -o test_lib-test-hash-method.obj `if test -f 'test-hash-method.c'; then $(CYGPATH_W) 'test-hash-method.c'; else $(CYGPATH_W) '$(srcdir)/test-hash-method.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/test_lib-test-hash-method.Tpo $(DEPDIR)/test_lib-test-hash-method.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='test-hash-method.c' object='test_lib-test-hash-method.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o test_lib-test-hash-method.obj `if test -f 'test-hash-method.c'; then $(CYGPATH_W) 'test-hash-method.c'; else $(CYGPATH_W) '$(srcdir)/test-hash-method.c'; fi`
+
+test_lib-test-hmac.o: test-hmac.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT test_lib-test-hmac.o -MD -MP -MF $(DEPDIR)/test_lib-test-hmac.Tpo -c -o test_lib-test-hmac.o `test -f 'test-hmac.c' || echo '$(srcdir)/'`test-hmac.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/test_lib-test-hmac.Tpo $(DEPDIR)/test_lib-test-hmac.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='test-hmac.c' object='test_lib-test-hmac.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o test_lib-test-hmac.o `test -f 'test-hmac.c' || echo '$(srcdir)/'`test-hmac.c
+
+test_lib-test-hmac.obj: test-hmac.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT test_lib-test-hmac.obj -MD -MP -MF $(DEPDIR)/test_lib-test-hmac.Tpo -c -o test_lib-test-hmac.obj `if test -f 'test-hmac.c'; then $(CYGPATH_W) 'test-hmac.c'; else $(CYGPATH_W) '$(srcdir)/test-hmac.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/test_lib-test-hmac.Tpo $(DEPDIR)/test_lib-test-hmac.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='test-hmac.c' object='test_lib-test-hmac.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o test_lib-test-hmac.obj `if test -f 'test-hmac.c'; then $(CYGPATH_W) 'test-hmac.c'; else $(CYGPATH_W) '$(srcdir)/test-hmac.c'; fi`
+
+test_lib-test-hex-binary.o: test-hex-binary.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT test_lib-test-hex-binary.o -MD -MP -MF $(DEPDIR)/test_lib-test-hex-binary.Tpo -c -o test_lib-test-hex-binary.o `test -f 'test-hex-binary.c' || echo '$(srcdir)/'`test-hex-binary.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/test_lib-test-hex-binary.Tpo $(DEPDIR)/test_lib-test-hex-binary.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='test-hex-binary.c' object='test_lib-test-hex-binary.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o test_lib-test-hex-binary.o `test -f 'test-hex-binary.c' || echo '$(srcdir)/'`test-hex-binary.c
+
+test_lib-test-hex-binary.obj: test-hex-binary.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT test_lib-test-hex-binary.obj -MD -MP -MF $(DEPDIR)/test_lib-test-hex-binary.Tpo -c -o test_lib-test-hex-binary.obj `if test -f 'test-hex-binary.c'; then $(CYGPATH_W) 'test-hex-binary.c'; else $(CYGPATH_W) '$(srcdir)/test-hex-binary.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/test_lib-test-hex-binary.Tpo $(DEPDIR)/test_lib-test-hex-binary.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='test-hex-binary.c' object='test_lib-test-hex-binary.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o test_lib-test-hex-binary.obj `if test -f 'test-hex-binary.c'; then $(CYGPATH_W) 'test-hex-binary.c'; else $(CYGPATH_W) '$(srcdir)/test-hex-binary.c'; fi`
+
+test_lib-test-imem.o: test-imem.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT test_lib-test-imem.o -MD -MP -MF $(DEPDIR)/test_lib-test-imem.Tpo -c -o test_lib-test-imem.o `test -f 'test-imem.c' || echo '$(srcdir)/'`test-imem.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/test_lib-test-imem.Tpo $(DEPDIR)/test_lib-test-imem.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='test-imem.c' object='test_lib-test-imem.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o test_lib-test-imem.o `test -f 'test-imem.c' || echo '$(srcdir)/'`test-imem.c
+
+test_lib-test-imem.obj: test-imem.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT test_lib-test-imem.obj -MD -MP -MF $(DEPDIR)/test_lib-test-imem.Tpo -c -o test_lib-test-imem.obj `if test -f 'test-imem.c'; then $(CYGPATH_W) 'test-imem.c'; else $(CYGPATH_W) '$(srcdir)/test-imem.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/test_lib-test-imem.Tpo $(DEPDIR)/test_lib-test-imem.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='test-imem.c' object='test_lib-test-imem.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o test_lib-test-imem.obj `if test -f 'test-imem.c'; then $(CYGPATH_W) 'test-imem.c'; else $(CYGPATH_W) '$(srcdir)/test-imem.c'; fi`
+
+test_lib-test-ioloop.o: test-ioloop.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT test_lib-test-ioloop.o -MD -MP -MF $(DEPDIR)/test_lib-test-ioloop.Tpo -c -o test_lib-test-ioloop.o `test -f 'test-ioloop.c' || echo '$(srcdir)/'`test-ioloop.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/test_lib-test-ioloop.Tpo $(DEPDIR)/test_lib-test-ioloop.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='test-ioloop.c' object='test_lib-test-ioloop.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o test_lib-test-ioloop.o `test -f 'test-ioloop.c' || echo '$(srcdir)/'`test-ioloop.c
+
+test_lib-test-ioloop.obj: test-ioloop.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT test_lib-test-ioloop.obj -MD -MP -MF $(DEPDIR)/test_lib-test-ioloop.Tpo -c -o test_lib-test-ioloop.obj `if test -f 'test-ioloop.c'; then $(CYGPATH_W) 'test-ioloop.c'; else $(CYGPATH_W) '$(srcdir)/test-ioloop.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/test_lib-test-ioloop.Tpo $(DEPDIR)/test_lib-test-ioloop.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='test-ioloop.c' object='test_lib-test-ioloop.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o test_lib-test-ioloop.obj `if test -f 'test-ioloop.c'; then $(CYGPATH_W) 'test-ioloop.c'; else $(CYGPATH_W) '$(srcdir)/test-ioloop.c'; fi`
+
+test_lib-test-iso8601-date.o: test-iso8601-date.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT test_lib-test-iso8601-date.o -MD -MP -MF $(DEPDIR)/test_lib-test-iso8601-date.Tpo -c -o test_lib-test-iso8601-date.o `test -f 'test-iso8601-date.c' || echo '$(srcdir)/'`test-iso8601-date.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/test_lib-test-iso8601-date.Tpo $(DEPDIR)/test_lib-test-iso8601-date.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='test-iso8601-date.c' object='test_lib-test-iso8601-date.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o test_lib-test-iso8601-date.o `test -f 'test-iso8601-date.c' || echo '$(srcdir)/'`test-iso8601-date.c
+
+test_lib-test-iso8601-date.obj: test-iso8601-date.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT test_lib-test-iso8601-date.obj -MD -MP -MF $(DEPDIR)/test_lib-test-iso8601-date.Tpo -c -o test_lib-test-iso8601-date.obj `if test -f 'test-iso8601-date.c'; then $(CYGPATH_W) 'test-iso8601-date.c'; else $(CYGPATH_W) '$(srcdir)/test-iso8601-date.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/test_lib-test-iso8601-date.Tpo $(DEPDIR)/test_lib-test-iso8601-date.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='test-iso8601-date.c' object='test_lib-test-iso8601-date.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o test_lib-test-iso8601-date.obj `if test -f 'test-iso8601-date.c'; then $(CYGPATH_W) 'test-iso8601-date.c'; else $(CYGPATH_W) '$(srcdir)/test-iso8601-date.c'; fi`
+
+test_lib-test-iostream-pump.o: test-iostream-pump.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT test_lib-test-iostream-pump.o -MD -MP -MF $(DEPDIR)/test_lib-test-iostream-pump.Tpo -c -o test_lib-test-iostream-pump.o `test -f 'test-iostream-pump.c' || echo '$(srcdir)/'`test-iostream-pump.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/test_lib-test-iostream-pump.Tpo $(DEPDIR)/test_lib-test-iostream-pump.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='test-iostream-pump.c' object='test_lib-test-iostream-pump.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o test_lib-test-iostream-pump.o `test -f 'test-iostream-pump.c' || echo '$(srcdir)/'`test-iostream-pump.c
+
+test_lib-test-iostream-pump.obj: test-iostream-pump.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT test_lib-test-iostream-pump.obj -MD -MP -MF $(DEPDIR)/test_lib-test-iostream-pump.Tpo -c -o test_lib-test-iostream-pump.obj `if test -f 'test-iostream-pump.c'; then $(CYGPATH_W) 'test-iostream-pump.c'; else $(CYGPATH_W) '$(srcdir)/test-iostream-pump.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/test_lib-test-iostream-pump.Tpo $(DEPDIR)/test_lib-test-iostream-pump.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='test-iostream-pump.c' object='test_lib-test-iostream-pump.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o test_lib-test-iostream-pump.obj `if test -f 'test-iostream-pump.c'; then $(CYGPATH_W) 'test-iostream-pump.c'; else $(CYGPATH_W) '$(srcdir)/test-iostream-pump.c'; fi`
+
+test_lib-test-iostream-proxy.o: test-iostream-proxy.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT test_lib-test-iostream-proxy.o -MD -MP -MF $(DEPDIR)/test_lib-test-iostream-proxy.Tpo -c -o test_lib-test-iostream-proxy.o `test -f 'test-iostream-proxy.c' || echo '$(srcdir)/'`test-iostream-proxy.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/test_lib-test-iostream-proxy.Tpo $(DEPDIR)/test_lib-test-iostream-proxy.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='test-iostream-proxy.c' object='test_lib-test-iostream-proxy.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o test_lib-test-iostream-proxy.o `test -f 'test-iostream-proxy.c' || echo '$(srcdir)/'`test-iostream-proxy.c
+
+test_lib-test-iostream-proxy.obj: test-iostream-proxy.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT test_lib-test-iostream-proxy.obj -MD -MP -MF $(DEPDIR)/test_lib-test-iostream-proxy.Tpo -c -o test_lib-test-iostream-proxy.obj `if test -f 'test-iostream-proxy.c'; then $(CYGPATH_W) 'test-iostream-proxy.c'; else $(CYGPATH_W) '$(srcdir)/test-iostream-proxy.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/test_lib-test-iostream-proxy.Tpo $(DEPDIR)/test_lib-test-iostream-proxy.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='test-iostream-proxy.c' object='test_lib-test-iostream-proxy.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o test_lib-test-iostream-proxy.obj `if test -f 'test-iostream-proxy.c'; then $(CYGPATH_W) 'test-iostream-proxy.c'; else $(CYGPATH_W) '$(srcdir)/test-iostream-proxy.c'; fi`
+
+test_lib-test-iostream-temp.o: test-iostream-temp.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT test_lib-test-iostream-temp.o -MD -MP -MF $(DEPDIR)/test_lib-test-iostream-temp.Tpo -c -o test_lib-test-iostream-temp.o `test -f 'test-iostream-temp.c' || echo '$(srcdir)/'`test-iostream-temp.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/test_lib-test-iostream-temp.Tpo $(DEPDIR)/test_lib-test-iostream-temp.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='test-iostream-temp.c' object='test_lib-test-iostream-temp.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o test_lib-test-iostream-temp.o `test -f 'test-iostream-temp.c' || echo '$(srcdir)/'`test-iostream-temp.c
+
+test_lib-test-iostream-temp.obj: test-iostream-temp.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT test_lib-test-iostream-temp.obj -MD -MP -MF $(DEPDIR)/test_lib-test-iostream-temp.Tpo -c -o test_lib-test-iostream-temp.obj `if test -f 'test-iostream-temp.c'; then $(CYGPATH_W) 'test-iostream-temp.c'; else $(CYGPATH_W) '$(srcdir)/test-iostream-temp.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/test_lib-test-iostream-temp.Tpo $(DEPDIR)/test_lib-test-iostream-temp.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='test-iostream-temp.c' object='test_lib-test-iostream-temp.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o test_lib-test-iostream-temp.obj `if test -f 'test-iostream-temp.c'; then $(CYGPATH_W) 'test-iostream-temp.c'; else $(CYGPATH_W) '$(srcdir)/test-iostream-temp.c'; fi`
+
+test_lib-test-istream.o: test-istream.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT test_lib-test-istream.o -MD -MP -MF $(DEPDIR)/test_lib-test-istream.Tpo -c -o test_lib-test-istream.o `test -f 'test-istream.c' || echo '$(srcdir)/'`test-istream.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/test_lib-test-istream.Tpo $(DEPDIR)/test_lib-test-istream.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='test-istream.c' object='test_lib-test-istream.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o test_lib-test-istream.o `test -f 'test-istream.c' || echo '$(srcdir)/'`test-istream.c
+
+test_lib-test-istream.obj: test-istream.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT test_lib-test-istream.obj -MD -MP -MF $(DEPDIR)/test_lib-test-istream.Tpo -c -o test_lib-test-istream.obj `if test -f 'test-istream.c'; then $(CYGPATH_W) 'test-istream.c'; else $(CYGPATH_W) '$(srcdir)/test-istream.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/test_lib-test-istream.Tpo $(DEPDIR)/test_lib-test-istream.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='test-istream.c' object='test_lib-test-istream.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o test_lib-test-istream.obj `if test -f 'test-istream.c'; then $(CYGPATH_W) 'test-istream.c'; else $(CYGPATH_W) '$(srcdir)/test-istream.c'; fi`
+
+test_lib-test-istream-base64-decoder.o: test-istream-base64-decoder.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT test_lib-test-istream-base64-decoder.o -MD -MP -MF $(DEPDIR)/test_lib-test-istream-base64-decoder.Tpo -c -o test_lib-test-istream-base64-decoder.o `test -f 'test-istream-base64-decoder.c' || echo '$(srcdir)/'`test-istream-base64-decoder.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/test_lib-test-istream-base64-decoder.Tpo $(DEPDIR)/test_lib-test-istream-base64-decoder.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='test-istream-base64-decoder.c' object='test_lib-test-istream-base64-decoder.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o test_lib-test-istream-base64-decoder.o `test -f 'test-istream-base64-decoder.c' || echo '$(srcdir)/'`test-istream-base64-decoder.c
+
+test_lib-test-istream-base64-decoder.obj: test-istream-base64-decoder.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT test_lib-test-istream-base64-decoder.obj -MD -MP -MF $(DEPDIR)/test_lib-test-istream-base64-decoder.Tpo -c -o test_lib-test-istream-base64-decoder.obj `if test -f 'test-istream-base64-decoder.c'; then $(CYGPATH_W) 'test-istream-base64-decoder.c'; else $(CYGPATH_W) '$(srcdir)/test-istream-base64-decoder.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/test_lib-test-istream-base64-decoder.Tpo $(DEPDIR)/test_lib-test-istream-base64-decoder.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='test-istream-base64-decoder.c' object='test_lib-test-istream-base64-decoder.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o test_lib-test-istream-base64-decoder.obj `if test -f 'test-istream-base64-decoder.c'; then $(CYGPATH_W) 'test-istream-base64-decoder.c'; else $(CYGPATH_W) '$(srcdir)/test-istream-base64-decoder.c'; fi`
+
+test_lib-test-istream-base64-encoder.o: test-istream-base64-encoder.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT test_lib-test-istream-base64-encoder.o -MD -MP -MF $(DEPDIR)/test_lib-test-istream-base64-encoder.Tpo -c -o test_lib-test-istream-base64-encoder.o `test -f 'test-istream-base64-encoder.c' || echo '$(srcdir)/'`test-istream-base64-encoder.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/test_lib-test-istream-base64-encoder.Tpo $(DEPDIR)/test_lib-test-istream-base64-encoder.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='test-istream-base64-encoder.c' object='test_lib-test-istream-base64-encoder.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o test_lib-test-istream-base64-encoder.o `test -f 'test-istream-base64-encoder.c' || echo '$(srcdir)/'`test-istream-base64-encoder.c
+
+test_lib-test-istream-base64-encoder.obj: test-istream-base64-encoder.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT test_lib-test-istream-base64-encoder.obj -MD -MP -MF $(DEPDIR)/test_lib-test-istream-base64-encoder.Tpo -c -o test_lib-test-istream-base64-encoder.obj `if test -f 'test-istream-base64-encoder.c'; then $(CYGPATH_W) 'test-istream-base64-encoder.c'; else $(CYGPATH_W) '$(srcdir)/test-istream-base64-encoder.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/test_lib-test-istream-base64-encoder.Tpo $(DEPDIR)/test_lib-test-istream-base64-encoder.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='test-istream-base64-encoder.c' object='test_lib-test-istream-base64-encoder.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o test_lib-test-istream-base64-encoder.obj `if test -f 'test-istream-base64-encoder.c'; then $(CYGPATH_W) 'test-istream-base64-encoder.c'; else $(CYGPATH_W) '$(srcdir)/test-istream-base64-encoder.c'; fi`
+
+test_lib-test-istream-chain.o: test-istream-chain.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT test_lib-test-istream-chain.o -MD -MP -MF $(DEPDIR)/test_lib-test-istream-chain.Tpo -c -o test_lib-test-istream-chain.o `test -f 'test-istream-chain.c' || echo '$(srcdir)/'`test-istream-chain.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/test_lib-test-istream-chain.Tpo $(DEPDIR)/test_lib-test-istream-chain.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='test-istream-chain.c' object='test_lib-test-istream-chain.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o test_lib-test-istream-chain.o `test -f 'test-istream-chain.c' || echo '$(srcdir)/'`test-istream-chain.c
+
+test_lib-test-istream-chain.obj: test-istream-chain.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT test_lib-test-istream-chain.obj -MD -MP -MF $(DEPDIR)/test_lib-test-istream-chain.Tpo -c -o test_lib-test-istream-chain.obj `if test -f 'test-istream-chain.c'; then $(CYGPATH_W) 'test-istream-chain.c'; else $(CYGPATH_W) '$(srcdir)/test-istream-chain.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/test_lib-test-istream-chain.Tpo $(DEPDIR)/test_lib-test-istream-chain.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='test-istream-chain.c' object='test_lib-test-istream-chain.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o test_lib-test-istream-chain.obj `if test -f 'test-istream-chain.c'; then $(CYGPATH_W) 'test-istream-chain.c'; else $(CYGPATH_W) '$(srcdir)/test-istream-chain.c'; fi`
+
+test_lib-test-istream-concat.o: test-istream-concat.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT test_lib-test-istream-concat.o -MD -MP -MF $(DEPDIR)/test_lib-test-istream-concat.Tpo -c -o test_lib-test-istream-concat.o `test -f 'test-istream-concat.c' || echo '$(srcdir)/'`test-istream-concat.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/test_lib-test-istream-concat.Tpo $(DEPDIR)/test_lib-test-istream-concat.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='test-istream-concat.c' object='test_lib-test-istream-concat.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o test_lib-test-istream-concat.o `test -f 'test-istream-concat.c' || echo '$(srcdir)/'`test-istream-concat.c
+
+test_lib-test-istream-concat.obj: test-istream-concat.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT test_lib-test-istream-concat.obj -MD -MP -MF $(DEPDIR)/test_lib-test-istream-concat.Tpo -c -o test_lib-test-istream-concat.obj `if test -f 'test-istream-concat.c'; then $(CYGPATH_W) 'test-istream-concat.c'; else $(CYGPATH_W) '$(srcdir)/test-istream-concat.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/test_lib-test-istream-concat.Tpo $(DEPDIR)/test_lib-test-istream-concat.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='test-istream-concat.c' object='test_lib-test-istream-concat.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o test_lib-test-istream-concat.obj `if test -f 'test-istream-concat.c'; then $(CYGPATH_W) 'test-istream-concat.c'; else $(CYGPATH_W) '$(srcdir)/test-istream-concat.c'; fi`
+
+test_lib-test-istream-crlf.o: test-istream-crlf.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT test_lib-test-istream-crlf.o -MD -MP -MF $(DEPDIR)/test_lib-test-istream-crlf.Tpo -c -o test_lib-test-istream-crlf.o `test -f 'test-istream-crlf.c' || echo '$(srcdir)/'`test-istream-crlf.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/test_lib-test-istream-crlf.Tpo $(DEPDIR)/test_lib-test-istream-crlf.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='test-istream-crlf.c' object='test_lib-test-istream-crlf.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o test_lib-test-istream-crlf.o `test -f 'test-istream-crlf.c' || echo '$(srcdir)/'`test-istream-crlf.c
+
+test_lib-test-istream-crlf.obj: test-istream-crlf.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT test_lib-test-istream-crlf.obj -MD -MP -MF $(DEPDIR)/test_lib-test-istream-crlf.Tpo -c -o test_lib-test-istream-crlf.obj `if test -f 'test-istream-crlf.c'; then $(CYGPATH_W) 'test-istream-crlf.c'; else $(CYGPATH_W) '$(srcdir)/test-istream-crlf.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/test_lib-test-istream-crlf.Tpo $(DEPDIR)/test_lib-test-istream-crlf.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='test-istream-crlf.c' object='test_lib-test-istream-crlf.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o test_lib-test-istream-crlf.obj `if test -f 'test-istream-crlf.c'; then $(CYGPATH_W) 'test-istream-crlf.c'; else $(CYGPATH_W) '$(srcdir)/test-istream-crlf.c'; fi`
+
+test_lib-test-istream-failure-at.o: test-istream-failure-at.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT test_lib-test-istream-failure-at.o -MD -MP -MF $(DEPDIR)/test_lib-test-istream-failure-at.Tpo -c -o test_lib-test-istream-failure-at.o `test -f 'test-istream-failure-at.c' || echo '$(srcdir)/'`test-istream-failure-at.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/test_lib-test-istream-failure-at.Tpo $(DEPDIR)/test_lib-test-istream-failure-at.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='test-istream-failure-at.c' object='test_lib-test-istream-failure-at.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o test_lib-test-istream-failure-at.o `test -f 'test-istream-failure-at.c' || echo '$(srcdir)/'`test-istream-failure-at.c
+
+test_lib-test-istream-failure-at.obj: test-istream-failure-at.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT test_lib-test-istream-failure-at.obj -MD -MP -MF $(DEPDIR)/test_lib-test-istream-failure-at.Tpo -c -o test_lib-test-istream-failure-at.obj `if test -f 'test-istream-failure-at.c'; then $(CYGPATH_W) 'test-istream-failure-at.c'; else $(CYGPATH_W) '$(srcdir)/test-istream-failure-at.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/test_lib-test-istream-failure-at.Tpo $(DEPDIR)/test_lib-test-istream-failure-at.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='test-istream-failure-at.c' object='test_lib-test-istream-failure-at.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o test_lib-test-istream-failure-at.obj `if test -f 'test-istream-failure-at.c'; then $(CYGPATH_W) 'test-istream-failure-at.c'; else $(CYGPATH_W) '$(srcdir)/test-istream-failure-at.c'; fi`
+
+test_lib-test-istream-jsonstr.o: test-istream-jsonstr.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT test_lib-test-istream-jsonstr.o -MD -MP -MF $(DEPDIR)/test_lib-test-istream-jsonstr.Tpo -c -o test_lib-test-istream-jsonstr.o `test -f 'test-istream-jsonstr.c' || echo '$(srcdir)/'`test-istream-jsonstr.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/test_lib-test-istream-jsonstr.Tpo $(DEPDIR)/test_lib-test-istream-jsonstr.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='test-istream-jsonstr.c' object='test_lib-test-istream-jsonstr.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o test_lib-test-istream-jsonstr.o `test -f 'test-istream-jsonstr.c' || echo '$(srcdir)/'`test-istream-jsonstr.c
+
+test_lib-test-istream-jsonstr.obj: test-istream-jsonstr.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT test_lib-test-istream-jsonstr.obj -MD -MP -MF $(DEPDIR)/test_lib-test-istream-jsonstr.Tpo -c -o test_lib-test-istream-jsonstr.obj `if test -f 'test-istream-jsonstr.c'; then $(CYGPATH_W) 'test-istream-jsonstr.c'; else $(CYGPATH_W) '$(srcdir)/test-istream-jsonstr.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/test_lib-test-istream-jsonstr.Tpo $(DEPDIR)/test_lib-test-istream-jsonstr.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='test-istream-jsonstr.c' object='test_lib-test-istream-jsonstr.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o test_lib-test-istream-jsonstr.obj `if test -f 'test-istream-jsonstr.c'; then $(CYGPATH_W) 'test-istream-jsonstr.c'; else $(CYGPATH_W) '$(srcdir)/test-istream-jsonstr.c'; fi`
+
+test_lib-test-istream-multiplex.o: test-istream-multiplex.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT test_lib-test-istream-multiplex.o -MD -MP -MF $(DEPDIR)/test_lib-test-istream-multiplex.Tpo -c -o test_lib-test-istream-multiplex.o `test -f 'test-istream-multiplex.c' || echo '$(srcdir)/'`test-istream-multiplex.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/test_lib-test-istream-multiplex.Tpo $(DEPDIR)/test_lib-test-istream-multiplex.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='test-istream-multiplex.c' object='test_lib-test-istream-multiplex.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o test_lib-test-istream-multiplex.o `test -f 'test-istream-multiplex.c' || echo '$(srcdir)/'`test-istream-multiplex.c
+
+test_lib-test-istream-multiplex.obj: test-istream-multiplex.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT test_lib-test-istream-multiplex.obj -MD -MP -MF $(DEPDIR)/test_lib-test-istream-multiplex.Tpo -c -o test_lib-test-istream-multiplex.obj `if test -f 'test-istream-multiplex.c'; then $(CYGPATH_W) 'test-istream-multiplex.c'; else $(CYGPATH_W) '$(srcdir)/test-istream-multiplex.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/test_lib-test-istream-multiplex.Tpo $(DEPDIR)/test_lib-test-istream-multiplex.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='test-istream-multiplex.c' object='test_lib-test-istream-multiplex.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o test_lib-test-istream-multiplex.obj `if test -f 'test-istream-multiplex.c'; then $(CYGPATH_W) 'test-istream-multiplex.c'; else $(CYGPATH_W) '$(srcdir)/test-istream-multiplex.c'; fi`
+
+test_lib-test-istream-seekable.o: test-istream-seekable.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT test_lib-test-istream-seekable.o -MD -MP -MF $(DEPDIR)/test_lib-test-istream-seekable.Tpo -c -o test_lib-test-istream-seekable.o `test -f 'test-istream-seekable.c' || echo '$(srcdir)/'`test-istream-seekable.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/test_lib-test-istream-seekable.Tpo $(DEPDIR)/test_lib-test-istream-seekable.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='test-istream-seekable.c' object='test_lib-test-istream-seekable.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o test_lib-test-istream-seekable.o `test -f 'test-istream-seekable.c' || echo '$(srcdir)/'`test-istream-seekable.c
+
+test_lib-test-istream-seekable.obj: test-istream-seekable.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT test_lib-test-istream-seekable.obj -MD -MP -MF $(DEPDIR)/test_lib-test-istream-seekable.Tpo -c -o test_lib-test-istream-seekable.obj `if test -f 'test-istream-seekable.c'; then $(CYGPATH_W) 'test-istream-seekable.c'; else $(CYGPATH_W) '$(srcdir)/test-istream-seekable.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/test_lib-test-istream-seekable.Tpo $(DEPDIR)/test_lib-test-istream-seekable.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='test-istream-seekable.c' object='test_lib-test-istream-seekable.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o test_lib-test-istream-seekable.obj `if test -f 'test-istream-seekable.c'; then $(CYGPATH_W) 'test-istream-seekable.c'; else $(CYGPATH_W) '$(srcdir)/test-istream-seekable.c'; fi`
+
+test_lib-test-istream-sized.o: test-istream-sized.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT test_lib-test-istream-sized.o -MD -MP -MF $(DEPDIR)/test_lib-test-istream-sized.Tpo -c -o test_lib-test-istream-sized.o `test -f 'test-istream-sized.c' || echo '$(srcdir)/'`test-istream-sized.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/test_lib-test-istream-sized.Tpo $(DEPDIR)/test_lib-test-istream-sized.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='test-istream-sized.c' object='test_lib-test-istream-sized.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o test_lib-test-istream-sized.o `test -f 'test-istream-sized.c' || echo '$(srcdir)/'`test-istream-sized.c
+
+test_lib-test-istream-sized.obj: test-istream-sized.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT test_lib-test-istream-sized.obj -MD -MP -MF $(DEPDIR)/test_lib-test-istream-sized.Tpo -c -o test_lib-test-istream-sized.obj `if test -f 'test-istream-sized.c'; then $(CYGPATH_W) 'test-istream-sized.c'; else $(CYGPATH_W) '$(srcdir)/test-istream-sized.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/test_lib-test-istream-sized.Tpo $(DEPDIR)/test_lib-test-istream-sized.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='test-istream-sized.c' object='test_lib-test-istream-sized.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o test_lib-test-istream-sized.obj `if test -f 'test-istream-sized.c'; then $(CYGPATH_W) 'test-istream-sized.c'; else $(CYGPATH_W) '$(srcdir)/test-istream-sized.c'; fi`
+
+test_lib-test-istream-tee.o: test-istream-tee.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT test_lib-test-istream-tee.o -MD -MP -MF $(DEPDIR)/test_lib-test-istream-tee.Tpo -c -o test_lib-test-istream-tee.o `test -f 'test-istream-tee.c' || echo '$(srcdir)/'`test-istream-tee.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/test_lib-test-istream-tee.Tpo $(DEPDIR)/test_lib-test-istream-tee.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='test-istream-tee.c' object='test_lib-test-istream-tee.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o test_lib-test-istream-tee.o `test -f 'test-istream-tee.c' || echo '$(srcdir)/'`test-istream-tee.c
+
+test_lib-test-istream-tee.obj: test-istream-tee.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT test_lib-test-istream-tee.obj -MD -MP -MF $(DEPDIR)/test_lib-test-istream-tee.Tpo -c -o test_lib-test-istream-tee.obj `if test -f 'test-istream-tee.c'; then $(CYGPATH_W) 'test-istream-tee.c'; else $(CYGPATH_W) '$(srcdir)/test-istream-tee.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/test_lib-test-istream-tee.Tpo $(DEPDIR)/test_lib-test-istream-tee.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='test-istream-tee.c' object='test_lib-test-istream-tee.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o test_lib-test-istream-tee.obj `if test -f 'test-istream-tee.c'; then $(CYGPATH_W) 'test-istream-tee.c'; else $(CYGPATH_W) '$(srcdir)/test-istream-tee.c'; fi`
+
+test_lib-test-istream-try.o: test-istream-try.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT test_lib-test-istream-try.o -MD -MP -MF $(DEPDIR)/test_lib-test-istream-try.Tpo -c -o test_lib-test-istream-try.o `test -f 'test-istream-try.c' || echo '$(srcdir)/'`test-istream-try.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/test_lib-test-istream-try.Tpo $(DEPDIR)/test_lib-test-istream-try.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='test-istream-try.c' object='test_lib-test-istream-try.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o test_lib-test-istream-try.o `test -f 'test-istream-try.c' || echo '$(srcdir)/'`test-istream-try.c
+
+test_lib-test-istream-try.obj: test-istream-try.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT test_lib-test-istream-try.obj -MD -MP -MF $(DEPDIR)/test_lib-test-istream-try.Tpo -c -o test_lib-test-istream-try.obj `if test -f 'test-istream-try.c'; then $(CYGPATH_W) 'test-istream-try.c'; else $(CYGPATH_W) '$(srcdir)/test-istream-try.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/test_lib-test-istream-try.Tpo $(DEPDIR)/test_lib-test-istream-try.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='test-istream-try.c' object='test_lib-test-istream-try.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o test_lib-test-istream-try.obj `if test -f 'test-istream-try.c'; then $(CYGPATH_W) 'test-istream-try.c'; else $(CYGPATH_W) '$(srcdir)/test-istream-try.c'; fi`
+
+test_lib-test-istream-unix.o: test-istream-unix.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT test_lib-test-istream-unix.o -MD -MP -MF $(DEPDIR)/test_lib-test-istream-unix.Tpo -c -o test_lib-test-istream-unix.o `test -f 'test-istream-unix.c' || echo '$(srcdir)/'`test-istream-unix.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/test_lib-test-istream-unix.Tpo $(DEPDIR)/test_lib-test-istream-unix.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='test-istream-unix.c' object='test_lib-test-istream-unix.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o test_lib-test-istream-unix.o `test -f 'test-istream-unix.c' || echo '$(srcdir)/'`test-istream-unix.c
+
+test_lib-test-istream-unix.obj: test-istream-unix.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT test_lib-test-istream-unix.obj -MD -MP -MF $(DEPDIR)/test_lib-test-istream-unix.Tpo -c -o test_lib-test-istream-unix.obj `if test -f 'test-istream-unix.c'; then $(CYGPATH_W) 'test-istream-unix.c'; else $(CYGPATH_W) '$(srcdir)/test-istream-unix.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/test_lib-test-istream-unix.Tpo $(DEPDIR)/test_lib-test-istream-unix.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='test-istream-unix.c' object='test_lib-test-istream-unix.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o test_lib-test-istream-unix.obj `if test -f 'test-istream-unix.c'; then $(CYGPATH_W) 'test-istream-unix.c'; else $(CYGPATH_W) '$(srcdir)/test-istream-unix.c'; fi`
+
+test_lib-test-json-parser.o: test-json-parser.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT test_lib-test-json-parser.o -MD -MP -MF $(DEPDIR)/test_lib-test-json-parser.Tpo -c -o test_lib-test-json-parser.o `test -f 'test-json-parser.c' || echo '$(srcdir)/'`test-json-parser.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/test_lib-test-json-parser.Tpo $(DEPDIR)/test_lib-test-json-parser.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='test-json-parser.c' object='test_lib-test-json-parser.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o test_lib-test-json-parser.o `test -f 'test-json-parser.c' || echo '$(srcdir)/'`test-json-parser.c
+
+test_lib-test-json-parser.obj: test-json-parser.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT test_lib-test-json-parser.obj -MD -MP -MF $(DEPDIR)/test_lib-test-json-parser.Tpo -c -o test_lib-test-json-parser.obj `if test -f 'test-json-parser.c'; then $(CYGPATH_W) 'test-json-parser.c'; else $(CYGPATH_W) '$(srcdir)/test-json-parser.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/test_lib-test-json-parser.Tpo $(DEPDIR)/test_lib-test-json-parser.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='test-json-parser.c' object='test_lib-test-json-parser.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o test_lib-test-json-parser.obj `if test -f 'test-json-parser.c'; then $(CYGPATH_W) 'test-json-parser.c'; else $(CYGPATH_W) '$(srcdir)/test-json-parser.c'; fi`
+
+test_lib-test-json-tree.o: test-json-tree.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT test_lib-test-json-tree.o -MD -MP -MF $(DEPDIR)/test_lib-test-json-tree.Tpo -c -o test_lib-test-json-tree.o `test -f 'test-json-tree.c' || echo '$(srcdir)/'`test-json-tree.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/test_lib-test-json-tree.Tpo $(DEPDIR)/test_lib-test-json-tree.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='test-json-tree.c' object='test_lib-test-json-tree.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o test_lib-test-json-tree.o `test -f 'test-json-tree.c' || echo '$(srcdir)/'`test-json-tree.c
+
+test_lib-test-json-tree.obj: test-json-tree.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT test_lib-test-json-tree.obj -MD -MP -MF $(DEPDIR)/test_lib-test-json-tree.Tpo -c -o test_lib-test-json-tree.obj `if test -f 'test-json-tree.c'; then $(CYGPATH_W) 'test-json-tree.c'; else $(CYGPATH_W) '$(srcdir)/test-json-tree.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/test_lib-test-json-tree.Tpo $(DEPDIR)/test_lib-test-json-tree.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='test-json-tree.c' object='test_lib-test-json-tree.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o test_lib-test-json-tree.obj `if test -f 'test-json-tree.c'; then $(CYGPATH_W) 'test-json-tree.c'; else $(CYGPATH_W) '$(srcdir)/test-json-tree.c'; fi`
+
+test_lib-test-lib-event.o: test-lib-event.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT test_lib-test-lib-event.o -MD -MP -MF $(DEPDIR)/test_lib-test-lib-event.Tpo -c -o test_lib-test-lib-event.o `test -f 'test-lib-event.c' || echo '$(srcdir)/'`test-lib-event.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/test_lib-test-lib-event.Tpo $(DEPDIR)/test_lib-test-lib-event.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='test-lib-event.c' object='test_lib-test-lib-event.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o test_lib-test-lib-event.o `test -f 'test-lib-event.c' || echo '$(srcdir)/'`test-lib-event.c
+
+test_lib-test-lib-event.obj: test-lib-event.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT test_lib-test-lib-event.obj -MD -MP -MF $(DEPDIR)/test_lib-test-lib-event.Tpo -c -o test_lib-test-lib-event.obj `if test -f 'test-lib-event.c'; then $(CYGPATH_W) 'test-lib-event.c'; else $(CYGPATH_W) '$(srcdir)/test-lib-event.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/test_lib-test-lib-event.Tpo $(DEPDIR)/test_lib-test-lib-event.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='test-lib-event.c' object='test_lib-test-lib-event.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o test_lib-test-lib-event.obj `if test -f 'test-lib-event.c'; then $(CYGPATH_W) 'test-lib-event.c'; else $(CYGPATH_W) '$(srcdir)/test-lib-event.c'; fi`
+
+test_lib-test-lib-signals.o: test-lib-signals.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT test_lib-test-lib-signals.o -MD -MP -MF $(DEPDIR)/test_lib-test-lib-signals.Tpo -c -o test_lib-test-lib-signals.o `test -f 'test-lib-signals.c' || echo '$(srcdir)/'`test-lib-signals.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/test_lib-test-lib-signals.Tpo $(DEPDIR)/test_lib-test-lib-signals.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='test-lib-signals.c' object='test_lib-test-lib-signals.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o test_lib-test-lib-signals.o `test -f 'test-lib-signals.c' || echo '$(srcdir)/'`test-lib-signals.c
+
+test_lib-test-lib-signals.obj: test-lib-signals.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT test_lib-test-lib-signals.obj -MD -MP -MF $(DEPDIR)/test_lib-test-lib-signals.Tpo -c -o test_lib-test-lib-signals.obj `if test -f 'test-lib-signals.c'; then $(CYGPATH_W) 'test-lib-signals.c'; else $(CYGPATH_W) '$(srcdir)/test-lib-signals.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/test_lib-test-lib-signals.Tpo $(DEPDIR)/test_lib-test-lib-signals.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='test-lib-signals.c' object='test_lib-test-lib-signals.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o test_lib-test-lib-signals.obj `if test -f 'test-lib-signals.c'; then $(CYGPATH_W) 'test-lib-signals.c'; else $(CYGPATH_W) '$(srcdir)/test-lib-signals.c'; fi`
+
+test_lib-test-llist.o: test-llist.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT test_lib-test-llist.o -MD -MP -MF $(DEPDIR)/test_lib-test-llist.Tpo -c -o test_lib-test-llist.o `test -f 'test-llist.c' || echo '$(srcdir)/'`test-llist.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/test_lib-test-llist.Tpo $(DEPDIR)/test_lib-test-llist.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='test-llist.c' object='test_lib-test-llist.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o test_lib-test-llist.o `test -f 'test-llist.c' || echo '$(srcdir)/'`test-llist.c
+
+test_lib-test-llist.obj: test-llist.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT test_lib-test-llist.obj -MD -MP -MF $(DEPDIR)/test_lib-test-llist.Tpo -c -o test_lib-test-llist.obj `if test -f 'test-llist.c'; then $(CYGPATH_W) 'test-llist.c'; else $(CYGPATH_W) '$(srcdir)/test-llist.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/test_lib-test-llist.Tpo $(DEPDIR)/test_lib-test-llist.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='test-llist.c' object='test_lib-test-llist.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o test_lib-test-llist.obj `if test -f 'test-llist.c'; then $(CYGPATH_W) 'test-llist.c'; else $(CYGPATH_W) '$(srcdir)/test-llist.c'; fi`
+
+test_lib-test-log-throttle.o: test-log-throttle.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT test_lib-test-log-throttle.o -MD -MP -MF $(DEPDIR)/test_lib-test-log-throttle.Tpo -c -o test_lib-test-log-throttle.o `test -f 'test-log-throttle.c' || echo '$(srcdir)/'`test-log-throttle.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/test_lib-test-log-throttle.Tpo $(DEPDIR)/test_lib-test-log-throttle.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='test-log-throttle.c' object='test_lib-test-log-throttle.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o test_lib-test-log-throttle.o `test -f 'test-log-throttle.c' || echo '$(srcdir)/'`test-log-throttle.c
+
+test_lib-test-log-throttle.obj: test-log-throttle.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT test_lib-test-log-throttle.obj -MD -MP -MF $(DEPDIR)/test_lib-test-log-throttle.Tpo -c -o test_lib-test-log-throttle.obj `if test -f 'test-log-throttle.c'; then $(CYGPATH_W) 'test-log-throttle.c'; else $(CYGPATH_W) '$(srcdir)/test-log-throttle.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/test_lib-test-log-throttle.Tpo $(DEPDIR)/test_lib-test-log-throttle.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='test-log-throttle.c' object='test_lib-test-log-throttle.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o test_lib-test-log-throttle.obj `if test -f 'test-log-throttle.c'; then $(CYGPATH_W) 'test-log-throttle.c'; else $(CYGPATH_W) '$(srcdir)/test-log-throttle.c'; fi`
+
+test_lib-test-macros.o: test-macros.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT test_lib-test-macros.o -MD -MP -MF $(DEPDIR)/test_lib-test-macros.Tpo -c -o test_lib-test-macros.o `test -f 'test-macros.c' || echo '$(srcdir)/'`test-macros.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/test_lib-test-macros.Tpo $(DEPDIR)/test_lib-test-macros.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='test-macros.c' object='test_lib-test-macros.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o test_lib-test-macros.o `test -f 'test-macros.c' || echo '$(srcdir)/'`test-macros.c
+
+test_lib-test-macros.obj: test-macros.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT test_lib-test-macros.obj -MD -MP -MF $(DEPDIR)/test_lib-test-macros.Tpo -c -o test_lib-test-macros.obj `if test -f 'test-macros.c'; then $(CYGPATH_W) 'test-macros.c'; else $(CYGPATH_W) '$(srcdir)/test-macros.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/test_lib-test-macros.Tpo $(DEPDIR)/test_lib-test-macros.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='test-macros.c' object='test_lib-test-macros.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o test_lib-test-macros.obj `if test -f 'test-macros.c'; then $(CYGPATH_W) 'test-macros.c'; else $(CYGPATH_W) '$(srcdir)/test-macros.c'; fi`
+
+test_lib-test-malloc-overflow.o: test-malloc-overflow.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT test_lib-test-malloc-overflow.o -MD -MP -MF $(DEPDIR)/test_lib-test-malloc-overflow.Tpo -c -o test_lib-test-malloc-overflow.o `test -f 'test-malloc-overflow.c' || echo '$(srcdir)/'`test-malloc-overflow.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/test_lib-test-malloc-overflow.Tpo $(DEPDIR)/test_lib-test-malloc-overflow.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='test-malloc-overflow.c' object='test_lib-test-malloc-overflow.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o test_lib-test-malloc-overflow.o `test -f 'test-malloc-overflow.c' || echo '$(srcdir)/'`test-malloc-overflow.c
+
+test_lib-test-malloc-overflow.obj: test-malloc-overflow.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT test_lib-test-malloc-overflow.obj -MD -MP -MF $(DEPDIR)/test_lib-test-malloc-overflow.Tpo -c -o test_lib-test-malloc-overflow.obj `if test -f 'test-malloc-overflow.c'; then $(CYGPATH_W) 'test-malloc-overflow.c'; else $(CYGPATH_W) '$(srcdir)/test-malloc-overflow.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/test_lib-test-malloc-overflow.Tpo $(DEPDIR)/test_lib-test-malloc-overflow.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='test-malloc-overflow.c' object='test_lib-test-malloc-overflow.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o test_lib-test-malloc-overflow.obj `if test -f 'test-malloc-overflow.c'; then $(CYGPATH_W) 'test-malloc-overflow.c'; else $(CYGPATH_W) '$(srcdir)/test-malloc-overflow.c'; fi`
+
+test_lib-test-memarea.o: test-memarea.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT test_lib-test-memarea.o -MD -MP -MF $(DEPDIR)/test_lib-test-memarea.Tpo -c -o test_lib-test-memarea.o `test -f 'test-memarea.c' || echo '$(srcdir)/'`test-memarea.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/test_lib-test-memarea.Tpo $(DEPDIR)/test_lib-test-memarea.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='test-memarea.c' object='test_lib-test-memarea.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o test_lib-test-memarea.o `test -f 'test-memarea.c' || echo '$(srcdir)/'`test-memarea.c
+
+test_lib-test-memarea.obj: test-memarea.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT test_lib-test-memarea.obj -MD -MP -MF $(DEPDIR)/test_lib-test-memarea.Tpo -c -o test_lib-test-memarea.obj `if test -f 'test-memarea.c'; then $(CYGPATH_W) 'test-memarea.c'; else $(CYGPATH_W) '$(srcdir)/test-memarea.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/test_lib-test-memarea.Tpo $(DEPDIR)/test_lib-test-memarea.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='test-memarea.c' object='test_lib-test-memarea.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o test_lib-test-memarea.obj `if test -f 'test-memarea.c'; then $(CYGPATH_W) 'test-memarea.c'; else $(CYGPATH_W) '$(srcdir)/test-memarea.c'; fi`
+
+test_lib-test-mempool.o: test-mempool.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT test_lib-test-mempool.o -MD -MP -MF $(DEPDIR)/test_lib-test-mempool.Tpo -c -o test_lib-test-mempool.o `test -f 'test-mempool.c' || echo '$(srcdir)/'`test-mempool.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/test_lib-test-mempool.Tpo $(DEPDIR)/test_lib-test-mempool.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='test-mempool.c' object='test_lib-test-mempool.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o test_lib-test-mempool.o `test -f 'test-mempool.c' || echo '$(srcdir)/'`test-mempool.c
+
+test_lib-test-mempool.obj: test-mempool.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT test_lib-test-mempool.obj -MD -MP -MF $(DEPDIR)/test_lib-test-mempool.Tpo -c -o test_lib-test-mempool.obj `if test -f 'test-mempool.c'; then $(CYGPATH_W) 'test-mempool.c'; else $(CYGPATH_W) '$(srcdir)/test-mempool.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/test_lib-test-mempool.Tpo $(DEPDIR)/test_lib-test-mempool.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='test-mempool.c' object='test_lib-test-mempool.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o test_lib-test-mempool.obj `if test -f 'test-mempool.c'; then $(CYGPATH_W) 'test-mempool.c'; else $(CYGPATH_W) '$(srcdir)/test-mempool.c'; fi`
+
+test_lib-test-mempool-allocfree.o: test-mempool-allocfree.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT test_lib-test-mempool-allocfree.o -MD -MP -MF $(DEPDIR)/test_lib-test-mempool-allocfree.Tpo -c -o test_lib-test-mempool-allocfree.o `test -f 'test-mempool-allocfree.c' || echo '$(srcdir)/'`test-mempool-allocfree.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/test_lib-test-mempool-allocfree.Tpo $(DEPDIR)/test_lib-test-mempool-allocfree.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='test-mempool-allocfree.c' object='test_lib-test-mempool-allocfree.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o test_lib-test-mempool-allocfree.o `test -f 'test-mempool-allocfree.c' || echo '$(srcdir)/'`test-mempool-allocfree.c
+
+test_lib-test-mempool-allocfree.obj: test-mempool-allocfree.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT test_lib-test-mempool-allocfree.obj -MD -MP -MF $(DEPDIR)/test_lib-test-mempool-allocfree.Tpo -c -o test_lib-test-mempool-allocfree.obj `if test -f 'test-mempool-allocfree.c'; then $(CYGPATH_W) 'test-mempool-allocfree.c'; else $(CYGPATH_W) '$(srcdir)/test-mempool-allocfree.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/test_lib-test-mempool-allocfree.Tpo $(DEPDIR)/test_lib-test-mempool-allocfree.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='test-mempool-allocfree.c' object='test_lib-test-mempool-allocfree.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o test_lib-test-mempool-allocfree.obj `if test -f 'test-mempool-allocfree.c'; then $(CYGPATH_W) 'test-mempool-allocfree.c'; else $(CYGPATH_W) '$(srcdir)/test-mempool-allocfree.c'; fi`
+
+test_lib-test-mempool-alloconly.o: test-mempool-alloconly.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT test_lib-test-mempool-alloconly.o -MD -MP -MF $(DEPDIR)/test_lib-test-mempool-alloconly.Tpo -c -o test_lib-test-mempool-alloconly.o `test -f 'test-mempool-alloconly.c' || echo '$(srcdir)/'`test-mempool-alloconly.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/test_lib-test-mempool-alloconly.Tpo $(DEPDIR)/test_lib-test-mempool-alloconly.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='test-mempool-alloconly.c' object='test_lib-test-mempool-alloconly.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o test_lib-test-mempool-alloconly.o `test -f 'test-mempool-alloconly.c' || echo '$(srcdir)/'`test-mempool-alloconly.c
+
+test_lib-test-mempool-alloconly.obj: test-mempool-alloconly.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT test_lib-test-mempool-alloconly.obj -MD -MP -MF $(DEPDIR)/test_lib-test-mempool-alloconly.Tpo -c -o test_lib-test-mempool-alloconly.obj `if test -f 'test-mempool-alloconly.c'; then $(CYGPATH_W) 'test-mempool-alloconly.c'; else $(CYGPATH_W) '$(srcdir)/test-mempool-alloconly.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/test_lib-test-mempool-alloconly.Tpo $(DEPDIR)/test_lib-test-mempool-alloconly.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='test-mempool-alloconly.c' object='test_lib-test-mempool-alloconly.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o test_lib-test-mempool-alloconly.obj `if test -f 'test-mempool-alloconly.c'; then $(CYGPATH_W) 'test-mempool-alloconly.c'; else $(CYGPATH_W) '$(srcdir)/test-mempool-alloconly.c'; fi`
+
+test_lib-test-pkcs5.o: test-pkcs5.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT test_lib-test-pkcs5.o -MD -MP -MF $(DEPDIR)/test_lib-test-pkcs5.Tpo -c -o test_lib-test-pkcs5.o `test -f 'test-pkcs5.c' || echo '$(srcdir)/'`test-pkcs5.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/test_lib-test-pkcs5.Tpo $(DEPDIR)/test_lib-test-pkcs5.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='test-pkcs5.c' object='test_lib-test-pkcs5.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o test_lib-test-pkcs5.o `test -f 'test-pkcs5.c' || echo '$(srcdir)/'`test-pkcs5.c
+
+test_lib-test-pkcs5.obj: test-pkcs5.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT test_lib-test-pkcs5.obj -MD -MP -MF $(DEPDIR)/test_lib-test-pkcs5.Tpo -c -o test_lib-test-pkcs5.obj `if test -f 'test-pkcs5.c'; then $(CYGPATH_W) 'test-pkcs5.c'; else $(CYGPATH_W) '$(srcdir)/test-pkcs5.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/test_lib-test-pkcs5.Tpo $(DEPDIR)/test_lib-test-pkcs5.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='test-pkcs5.c' object='test_lib-test-pkcs5.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o test_lib-test-pkcs5.obj `if test -f 'test-pkcs5.c'; then $(CYGPATH_W) 'test-pkcs5.c'; else $(CYGPATH_W) '$(srcdir)/test-pkcs5.c'; fi`
+
+test_lib-test-net.o: test-net.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT test_lib-test-net.o -MD -MP -MF $(DEPDIR)/test_lib-test-net.Tpo -c -o test_lib-test-net.o `test -f 'test-net.c' || echo '$(srcdir)/'`test-net.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/test_lib-test-net.Tpo $(DEPDIR)/test_lib-test-net.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='test-net.c' object='test_lib-test-net.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o test_lib-test-net.o `test -f 'test-net.c' || echo '$(srcdir)/'`test-net.c
+
+test_lib-test-net.obj: test-net.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT test_lib-test-net.obj -MD -MP -MF $(DEPDIR)/test_lib-test-net.Tpo -c -o test_lib-test-net.obj `if test -f 'test-net.c'; then $(CYGPATH_W) 'test-net.c'; else $(CYGPATH_W) '$(srcdir)/test-net.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/test_lib-test-net.Tpo $(DEPDIR)/test_lib-test-net.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='test-net.c' object='test_lib-test-net.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o test_lib-test-net.obj `if test -f 'test-net.c'; then $(CYGPATH_W) 'test-net.c'; else $(CYGPATH_W) '$(srcdir)/test-net.c'; fi`
+
+test_lib-test-numpack.o: test-numpack.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT test_lib-test-numpack.o -MD -MP -MF $(DEPDIR)/test_lib-test-numpack.Tpo -c -o test_lib-test-numpack.o `test -f 'test-numpack.c' || echo '$(srcdir)/'`test-numpack.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/test_lib-test-numpack.Tpo $(DEPDIR)/test_lib-test-numpack.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='test-numpack.c' object='test_lib-test-numpack.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o test_lib-test-numpack.o `test -f 'test-numpack.c' || echo '$(srcdir)/'`test-numpack.c
+
+test_lib-test-numpack.obj: test-numpack.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT test_lib-test-numpack.obj -MD -MP -MF $(DEPDIR)/test_lib-test-numpack.Tpo -c -o test_lib-test-numpack.obj `if test -f 'test-numpack.c'; then $(CYGPATH_W) 'test-numpack.c'; else $(CYGPATH_W) '$(srcdir)/test-numpack.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/test_lib-test-numpack.Tpo $(DEPDIR)/test_lib-test-numpack.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='test-numpack.c' object='test_lib-test-numpack.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o test_lib-test-numpack.obj `if test -f 'test-numpack.c'; then $(CYGPATH_W) 'test-numpack.c'; else $(CYGPATH_W) '$(srcdir)/test-numpack.c'; fi`
+
+test_lib-test-ostream-buffer.o: test-ostream-buffer.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT test_lib-test-ostream-buffer.o -MD -MP -MF $(DEPDIR)/test_lib-test-ostream-buffer.Tpo -c -o test_lib-test-ostream-buffer.o `test -f 'test-ostream-buffer.c' || echo '$(srcdir)/'`test-ostream-buffer.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/test_lib-test-ostream-buffer.Tpo $(DEPDIR)/test_lib-test-ostream-buffer.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='test-ostream-buffer.c' object='test_lib-test-ostream-buffer.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o test_lib-test-ostream-buffer.o `test -f 'test-ostream-buffer.c' || echo '$(srcdir)/'`test-ostream-buffer.c
+
+test_lib-test-ostream-buffer.obj: test-ostream-buffer.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT test_lib-test-ostream-buffer.obj -MD -MP -MF $(DEPDIR)/test_lib-test-ostream-buffer.Tpo -c -o test_lib-test-ostream-buffer.obj `if test -f 'test-ostream-buffer.c'; then $(CYGPATH_W) 'test-ostream-buffer.c'; else $(CYGPATH_W) '$(srcdir)/test-ostream-buffer.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/test_lib-test-ostream-buffer.Tpo $(DEPDIR)/test_lib-test-ostream-buffer.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='test-ostream-buffer.c' object='test_lib-test-ostream-buffer.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o test_lib-test-ostream-buffer.obj `if test -f 'test-ostream-buffer.c'; then $(CYGPATH_W) 'test-ostream-buffer.c'; else $(CYGPATH_W) '$(srcdir)/test-ostream-buffer.c'; fi`
+
+test_lib-test-ostream-failure-at.o: test-ostream-failure-at.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT test_lib-test-ostream-failure-at.o -MD -MP -MF $(DEPDIR)/test_lib-test-ostream-failure-at.Tpo -c -o test_lib-test-ostream-failure-at.o `test -f 'test-ostream-failure-at.c' || echo '$(srcdir)/'`test-ostream-failure-at.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/test_lib-test-ostream-failure-at.Tpo $(DEPDIR)/test_lib-test-ostream-failure-at.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='test-ostream-failure-at.c' object='test_lib-test-ostream-failure-at.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o test_lib-test-ostream-failure-at.o `test -f 'test-ostream-failure-at.c' || echo '$(srcdir)/'`test-ostream-failure-at.c
+
+test_lib-test-ostream-failure-at.obj: test-ostream-failure-at.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT test_lib-test-ostream-failure-at.obj -MD -MP -MF $(DEPDIR)/test_lib-test-ostream-failure-at.Tpo -c -o test_lib-test-ostream-failure-at.obj `if test -f 'test-ostream-failure-at.c'; then $(CYGPATH_W) 'test-ostream-failure-at.c'; else $(CYGPATH_W) '$(srcdir)/test-ostream-failure-at.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/test_lib-test-ostream-failure-at.Tpo $(DEPDIR)/test_lib-test-ostream-failure-at.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='test-ostream-failure-at.c' object='test_lib-test-ostream-failure-at.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o test_lib-test-ostream-failure-at.obj `if test -f 'test-ostream-failure-at.c'; then $(CYGPATH_W) 'test-ostream-failure-at.c'; else $(CYGPATH_W) '$(srcdir)/test-ostream-failure-at.c'; fi`
+
+test_lib-test-ostream-file.o: test-ostream-file.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT test_lib-test-ostream-file.o -MD -MP -MF $(DEPDIR)/test_lib-test-ostream-file.Tpo -c -o test_lib-test-ostream-file.o `test -f 'test-ostream-file.c' || echo '$(srcdir)/'`test-ostream-file.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/test_lib-test-ostream-file.Tpo $(DEPDIR)/test_lib-test-ostream-file.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='test-ostream-file.c' object='test_lib-test-ostream-file.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o test_lib-test-ostream-file.o `test -f 'test-ostream-file.c' || echo '$(srcdir)/'`test-ostream-file.c
+
+test_lib-test-ostream-file.obj: test-ostream-file.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT test_lib-test-ostream-file.obj -MD -MP -MF $(DEPDIR)/test_lib-test-ostream-file.Tpo -c -o test_lib-test-ostream-file.obj `if test -f 'test-ostream-file.c'; then $(CYGPATH_W) 'test-ostream-file.c'; else $(CYGPATH_W) '$(srcdir)/test-ostream-file.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/test_lib-test-ostream-file.Tpo $(DEPDIR)/test_lib-test-ostream-file.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='test-ostream-file.c' object='test_lib-test-ostream-file.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o test_lib-test-ostream-file.obj `if test -f 'test-ostream-file.c'; then $(CYGPATH_W) 'test-ostream-file.c'; else $(CYGPATH_W) '$(srcdir)/test-ostream-file.c'; fi`
+
+test_lib-test-ostream-multiplex.o: test-ostream-multiplex.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT test_lib-test-ostream-multiplex.o -MD -MP -MF $(DEPDIR)/test_lib-test-ostream-multiplex.Tpo -c -o test_lib-test-ostream-multiplex.o `test -f 'test-ostream-multiplex.c' || echo '$(srcdir)/'`test-ostream-multiplex.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/test_lib-test-ostream-multiplex.Tpo $(DEPDIR)/test_lib-test-ostream-multiplex.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='test-ostream-multiplex.c' object='test_lib-test-ostream-multiplex.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o test_lib-test-ostream-multiplex.o `test -f 'test-ostream-multiplex.c' || echo '$(srcdir)/'`test-ostream-multiplex.c
+
+test_lib-test-ostream-multiplex.obj: test-ostream-multiplex.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT test_lib-test-ostream-multiplex.obj -MD -MP -MF $(DEPDIR)/test_lib-test-ostream-multiplex.Tpo -c -o test_lib-test-ostream-multiplex.obj `if test -f 'test-ostream-multiplex.c'; then $(CYGPATH_W) 'test-ostream-multiplex.c'; else $(CYGPATH_W) '$(srcdir)/test-ostream-multiplex.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/test_lib-test-ostream-multiplex.Tpo $(DEPDIR)/test_lib-test-ostream-multiplex.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='test-ostream-multiplex.c' object='test_lib-test-ostream-multiplex.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o test_lib-test-ostream-multiplex.obj `if test -f 'test-ostream-multiplex.c'; then $(CYGPATH_W) 'test-ostream-multiplex.c'; else $(CYGPATH_W) '$(srcdir)/test-ostream-multiplex.c'; fi`
+
+test_lib-test-multiplex.o: test-multiplex.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT test_lib-test-multiplex.o -MD -MP -MF $(DEPDIR)/test_lib-test-multiplex.Tpo -c -o test_lib-test-multiplex.o `test -f 'test-multiplex.c' || echo '$(srcdir)/'`test-multiplex.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/test_lib-test-multiplex.Tpo $(DEPDIR)/test_lib-test-multiplex.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='test-multiplex.c' object='test_lib-test-multiplex.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o test_lib-test-multiplex.o `test -f 'test-multiplex.c' || echo '$(srcdir)/'`test-multiplex.c
+
+test_lib-test-multiplex.obj: test-multiplex.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT test_lib-test-multiplex.obj -MD -MP -MF $(DEPDIR)/test_lib-test-multiplex.Tpo -c -o test_lib-test-multiplex.obj `if test -f 'test-multiplex.c'; then $(CYGPATH_W) 'test-multiplex.c'; else $(CYGPATH_W) '$(srcdir)/test-multiplex.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/test_lib-test-multiplex.Tpo $(DEPDIR)/test_lib-test-multiplex.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='test-multiplex.c' object='test_lib-test-multiplex.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o test_lib-test-multiplex.obj `if test -f 'test-multiplex.c'; then $(CYGPATH_W) 'test-multiplex.c'; else $(CYGPATH_W) '$(srcdir)/test-multiplex.c'; fi`
+
+test_lib-test-path-util.o: test-path-util.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT test_lib-test-path-util.o -MD -MP -MF $(DEPDIR)/test_lib-test-path-util.Tpo -c -o test_lib-test-path-util.o `test -f 'test-path-util.c' || echo '$(srcdir)/'`test-path-util.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/test_lib-test-path-util.Tpo $(DEPDIR)/test_lib-test-path-util.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='test-path-util.c' object='test_lib-test-path-util.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o test_lib-test-path-util.o `test -f 'test-path-util.c' || echo '$(srcdir)/'`test-path-util.c
+
+test_lib-test-path-util.obj: test-path-util.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT test_lib-test-path-util.obj -MD -MP -MF $(DEPDIR)/test_lib-test-path-util.Tpo -c -o test_lib-test-path-util.obj `if test -f 'test-path-util.c'; then $(CYGPATH_W) 'test-path-util.c'; else $(CYGPATH_W) '$(srcdir)/test-path-util.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/test_lib-test-path-util.Tpo $(DEPDIR)/test_lib-test-path-util.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='test-path-util.c' object='test_lib-test-path-util.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o test_lib-test-path-util.obj `if test -f 'test-path-util.c'; then $(CYGPATH_W) 'test-path-util.c'; else $(CYGPATH_W) '$(srcdir)/test-path-util.c'; fi`
+
+test_lib-test-primes.o: test-primes.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT test_lib-test-primes.o -MD -MP -MF $(DEPDIR)/test_lib-test-primes.Tpo -c -o test_lib-test-primes.o `test -f 'test-primes.c' || echo '$(srcdir)/'`test-primes.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/test_lib-test-primes.Tpo $(DEPDIR)/test_lib-test-primes.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='test-primes.c' object='test_lib-test-primes.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o test_lib-test-primes.o `test -f 'test-primes.c' || echo '$(srcdir)/'`test-primes.c
+
+test_lib-test-primes.obj: test-primes.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT test_lib-test-primes.obj -MD -MP -MF $(DEPDIR)/test_lib-test-primes.Tpo -c -o test_lib-test-primes.obj `if test -f 'test-primes.c'; then $(CYGPATH_W) 'test-primes.c'; else $(CYGPATH_W) '$(srcdir)/test-primes.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/test_lib-test-primes.Tpo $(DEPDIR)/test_lib-test-primes.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='test-primes.c' object='test_lib-test-primes.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o test_lib-test-primes.obj `if test -f 'test-primes.c'; then $(CYGPATH_W) 'test-primes.c'; else $(CYGPATH_W) '$(srcdir)/test-primes.c'; fi`
+
+test_lib-test-printf-format-fix.o: test-printf-format-fix.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT test_lib-test-printf-format-fix.o -MD -MP -MF $(DEPDIR)/test_lib-test-printf-format-fix.Tpo -c -o test_lib-test-printf-format-fix.o `test -f 'test-printf-format-fix.c' || echo '$(srcdir)/'`test-printf-format-fix.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/test_lib-test-printf-format-fix.Tpo $(DEPDIR)/test_lib-test-printf-format-fix.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='test-printf-format-fix.c' object='test_lib-test-printf-format-fix.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o test_lib-test-printf-format-fix.o `test -f 'test-printf-format-fix.c' || echo '$(srcdir)/'`test-printf-format-fix.c
+
+test_lib-test-printf-format-fix.obj: test-printf-format-fix.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT test_lib-test-printf-format-fix.obj -MD -MP -MF $(DEPDIR)/test_lib-test-printf-format-fix.Tpo -c -o test_lib-test-printf-format-fix.obj `if test -f 'test-printf-format-fix.c'; then $(CYGPATH_W) 'test-printf-format-fix.c'; else $(CYGPATH_W) '$(srcdir)/test-printf-format-fix.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/test_lib-test-printf-format-fix.Tpo $(DEPDIR)/test_lib-test-printf-format-fix.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='test-printf-format-fix.c' object='test_lib-test-printf-format-fix.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o test_lib-test-printf-format-fix.obj `if test -f 'test-printf-format-fix.c'; then $(CYGPATH_W) 'test-printf-format-fix.c'; else $(CYGPATH_W) '$(srcdir)/test-printf-format-fix.c'; fi`
+
+test_lib-test-priorityq.o: test-priorityq.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT test_lib-test-priorityq.o -MD -MP -MF $(DEPDIR)/test_lib-test-priorityq.Tpo -c -o test_lib-test-priorityq.o `test -f 'test-priorityq.c' || echo '$(srcdir)/'`test-priorityq.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/test_lib-test-priorityq.Tpo $(DEPDIR)/test_lib-test-priorityq.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='test-priorityq.c' object='test_lib-test-priorityq.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o test_lib-test-priorityq.o `test -f 'test-priorityq.c' || echo '$(srcdir)/'`test-priorityq.c
+
+test_lib-test-priorityq.obj: test-priorityq.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT test_lib-test-priorityq.obj -MD -MP -MF $(DEPDIR)/test_lib-test-priorityq.Tpo -c -o test_lib-test-priorityq.obj `if test -f 'test-priorityq.c'; then $(CYGPATH_W) 'test-priorityq.c'; else $(CYGPATH_W) '$(srcdir)/test-priorityq.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/test_lib-test-priorityq.Tpo $(DEPDIR)/test_lib-test-priorityq.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='test-priorityq.c' object='test_lib-test-priorityq.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o test_lib-test-priorityq.obj `if test -f 'test-priorityq.c'; then $(CYGPATH_W) 'test-priorityq.c'; else $(CYGPATH_W) '$(srcdir)/test-priorityq.c'; fi`
+
+test_lib-test-random.o: test-random.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT test_lib-test-random.o -MD -MP -MF $(DEPDIR)/test_lib-test-random.Tpo -c -o test_lib-test-random.o `test -f 'test-random.c' || echo '$(srcdir)/'`test-random.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/test_lib-test-random.Tpo $(DEPDIR)/test_lib-test-random.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='test-random.c' object='test_lib-test-random.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o test_lib-test-random.o `test -f 'test-random.c' || echo '$(srcdir)/'`test-random.c
+
+test_lib-test-random.obj: test-random.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT test_lib-test-random.obj -MD -MP -MF $(DEPDIR)/test_lib-test-random.Tpo -c -o test_lib-test-random.obj `if test -f 'test-random.c'; then $(CYGPATH_W) 'test-random.c'; else $(CYGPATH_W) '$(srcdir)/test-random.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/test_lib-test-random.Tpo $(DEPDIR)/test_lib-test-random.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='test-random.c' object='test_lib-test-random.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o test_lib-test-random.obj `if test -f 'test-random.c'; then $(CYGPATH_W) 'test-random.c'; else $(CYGPATH_W) '$(srcdir)/test-random.c'; fi`
+
+test_lib-test-seq-range-array.o: test-seq-range-array.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT test_lib-test-seq-range-array.o -MD -MP -MF $(DEPDIR)/test_lib-test-seq-range-array.Tpo -c -o test_lib-test-seq-range-array.o `test -f 'test-seq-range-array.c' || echo '$(srcdir)/'`test-seq-range-array.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/test_lib-test-seq-range-array.Tpo $(DEPDIR)/test_lib-test-seq-range-array.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='test-seq-range-array.c' object='test_lib-test-seq-range-array.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o test_lib-test-seq-range-array.o `test -f 'test-seq-range-array.c' || echo '$(srcdir)/'`test-seq-range-array.c
+
+test_lib-test-seq-range-array.obj: test-seq-range-array.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT test_lib-test-seq-range-array.obj -MD -MP -MF $(DEPDIR)/test_lib-test-seq-range-array.Tpo -c -o test_lib-test-seq-range-array.obj `if test -f 'test-seq-range-array.c'; then $(CYGPATH_W) 'test-seq-range-array.c'; else $(CYGPATH_W) '$(srcdir)/test-seq-range-array.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/test_lib-test-seq-range-array.Tpo $(DEPDIR)/test_lib-test-seq-range-array.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='test-seq-range-array.c' object='test_lib-test-seq-range-array.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o test_lib-test-seq-range-array.obj `if test -f 'test-seq-range-array.c'; then $(CYGPATH_W) 'test-seq-range-array.c'; else $(CYGPATH_W) '$(srcdir)/test-seq-range-array.c'; fi`
+
+test_lib-test-seq-set-builder.o: test-seq-set-builder.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT test_lib-test-seq-set-builder.o -MD -MP -MF $(DEPDIR)/test_lib-test-seq-set-builder.Tpo -c -o test_lib-test-seq-set-builder.o `test -f 'test-seq-set-builder.c' || echo '$(srcdir)/'`test-seq-set-builder.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/test_lib-test-seq-set-builder.Tpo $(DEPDIR)/test_lib-test-seq-set-builder.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='test-seq-set-builder.c' object='test_lib-test-seq-set-builder.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o test_lib-test-seq-set-builder.o `test -f 'test-seq-set-builder.c' || echo '$(srcdir)/'`test-seq-set-builder.c
+
+test_lib-test-seq-set-builder.obj: test-seq-set-builder.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT test_lib-test-seq-set-builder.obj -MD -MP -MF $(DEPDIR)/test_lib-test-seq-set-builder.Tpo -c -o test_lib-test-seq-set-builder.obj `if test -f 'test-seq-set-builder.c'; then $(CYGPATH_W) 'test-seq-set-builder.c'; else $(CYGPATH_W) '$(srcdir)/test-seq-set-builder.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/test_lib-test-seq-set-builder.Tpo $(DEPDIR)/test_lib-test-seq-set-builder.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='test-seq-set-builder.c' object='test_lib-test-seq-set-builder.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o test_lib-test-seq-set-builder.obj `if test -f 'test-seq-set-builder.c'; then $(CYGPATH_W) 'test-seq-set-builder.c'; else $(CYGPATH_W) '$(srcdir)/test-seq-set-builder.c'; fi`
+
+test_lib-test-stats-dist.o: test-stats-dist.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT test_lib-test-stats-dist.o -MD -MP -MF $(DEPDIR)/test_lib-test-stats-dist.Tpo -c -o test_lib-test-stats-dist.o `test -f 'test-stats-dist.c' || echo '$(srcdir)/'`test-stats-dist.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/test_lib-test-stats-dist.Tpo $(DEPDIR)/test_lib-test-stats-dist.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='test-stats-dist.c' object='test_lib-test-stats-dist.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o test_lib-test-stats-dist.o `test -f 'test-stats-dist.c' || echo '$(srcdir)/'`test-stats-dist.c
+
+test_lib-test-stats-dist.obj: test-stats-dist.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT test_lib-test-stats-dist.obj -MD -MP -MF $(DEPDIR)/test_lib-test-stats-dist.Tpo -c -o test_lib-test-stats-dist.obj `if test -f 'test-stats-dist.c'; then $(CYGPATH_W) 'test-stats-dist.c'; else $(CYGPATH_W) '$(srcdir)/test-stats-dist.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/test_lib-test-stats-dist.Tpo $(DEPDIR)/test_lib-test-stats-dist.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='test-stats-dist.c' object='test_lib-test-stats-dist.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o test_lib-test-stats-dist.obj `if test -f 'test-stats-dist.c'; then $(CYGPATH_W) 'test-stats-dist.c'; else $(CYGPATH_W) '$(srcdir)/test-stats-dist.c'; fi`
+
+test_lib-test-str.o: test-str.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT test_lib-test-str.o -MD -MP -MF $(DEPDIR)/test_lib-test-str.Tpo -c -o test_lib-test-str.o `test -f 'test-str.c' || echo '$(srcdir)/'`test-str.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/test_lib-test-str.Tpo $(DEPDIR)/test_lib-test-str.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='test-str.c' object='test_lib-test-str.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o test_lib-test-str.o `test -f 'test-str.c' || echo '$(srcdir)/'`test-str.c
+
+test_lib-test-str.obj: test-str.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT test_lib-test-str.obj -MD -MP -MF $(DEPDIR)/test_lib-test-str.Tpo -c -o test_lib-test-str.obj `if test -f 'test-str.c'; then $(CYGPATH_W) 'test-str.c'; else $(CYGPATH_W) '$(srcdir)/test-str.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/test_lib-test-str.Tpo $(DEPDIR)/test_lib-test-str.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='test-str.c' object='test_lib-test-str.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o test_lib-test-str.obj `if test -f 'test-str.c'; then $(CYGPATH_W) 'test-str.c'; else $(CYGPATH_W) '$(srcdir)/test-str.c'; fi`
+
+test_lib-test-strescape.o: test-strescape.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT test_lib-test-strescape.o -MD -MP -MF $(DEPDIR)/test_lib-test-strescape.Tpo -c -o test_lib-test-strescape.o `test -f 'test-strescape.c' || echo '$(srcdir)/'`test-strescape.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/test_lib-test-strescape.Tpo $(DEPDIR)/test_lib-test-strescape.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='test-strescape.c' object='test_lib-test-strescape.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o test_lib-test-strescape.o `test -f 'test-strescape.c' || echo '$(srcdir)/'`test-strescape.c
+
+test_lib-test-strescape.obj: test-strescape.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT test_lib-test-strescape.obj -MD -MP -MF $(DEPDIR)/test_lib-test-strescape.Tpo -c -o test_lib-test-strescape.obj `if test -f 'test-strescape.c'; then $(CYGPATH_W) 'test-strescape.c'; else $(CYGPATH_W) '$(srcdir)/test-strescape.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/test_lib-test-strescape.Tpo $(DEPDIR)/test_lib-test-strescape.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='test-strescape.c' object='test_lib-test-strescape.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o test_lib-test-strescape.obj `if test -f 'test-strescape.c'; then $(CYGPATH_W) 'test-strescape.c'; else $(CYGPATH_W) '$(srcdir)/test-strescape.c'; fi`
+
+test_lib-test-strfuncs.o: test-strfuncs.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT test_lib-test-strfuncs.o -MD -MP -MF $(DEPDIR)/test_lib-test-strfuncs.Tpo -c -o test_lib-test-strfuncs.o `test -f 'test-strfuncs.c' || echo '$(srcdir)/'`test-strfuncs.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/test_lib-test-strfuncs.Tpo $(DEPDIR)/test_lib-test-strfuncs.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='test-strfuncs.c' object='test_lib-test-strfuncs.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o test_lib-test-strfuncs.o `test -f 'test-strfuncs.c' || echo '$(srcdir)/'`test-strfuncs.c
+
+test_lib-test-strfuncs.obj: test-strfuncs.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT test_lib-test-strfuncs.obj -MD -MP -MF $(DEPDIR)/test_lib-test-strfuncs.Tpo -c -o test_lib-test-strfuncs.obj `if test -f 'test-strfuncs.c'; then $(CYGPATH_W) 'test-strfuncs.c'; else $(CYGPATH_W) '$(srcdir)/test-strfuncs.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/test_lib-test-strfuncs.Tpo $(DEPDIR)/test_lib-test-strfuncs.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='test-strfuncs.c' object='test_lib-test-strfuncs.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o test_lib-test-strfuncs.obj `if test -f 'test-strfuncs.c'; then $(CYGPATH_W) 'test-strfuncs.c'; else $(CYGPATH_W) '$(srcdir)/test-strfuncs.c'; fi`
+
+test_lib-test-strnum.o: test-strnum.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT test_lib-test-strnum.o -MD -MP -MF $(DEPDIR)/test_lib-test-strnum.Tpo -c -o test_lib-test-strnum.o `test -f 'test-strnum.c' || echo '$(srcdir)/'`test-strnum.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/test_lib-test-strnum.Tpo $(DEPDIR)/test_lib-test-strnum.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='test-strnum.c' object='test_lib-test-strnum.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o test_lib-test-strnum.o `test -f 'test-strnum.c' || echo '$(srcdir)/'`test-strnum.c
+
+test_lib-test-strnum.obj: test-strnum.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT test_lib-test-strnum.obj -MD -MP -MF $(DEPDIR)/test_lib-test-strnum.Tpo -c -o test_lib-test-strnum.obj `if test -f 'test-strnum.c'; then $(CYGPATH_W) 'test-strnum.c'; else $(CYGPATH_W) '$(srcdir)/test-strnum.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/test_lib-test-strnum.Tpo $(DEPDIR)/test_lib-test-strnum.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='test-strnum.c' object='test_lib-test-strnum.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o test_lib-test-strnum.obj `if test -f 'test-strnum.c'; then $(CYGPATH_W) 'test-strnum.c'; else $(CYGPATH_W) '$(srcdir)/test-strnum.c'; fi`
+
+test_lib-test-str-find.o: test-str-find.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT test_lib-test-str-find.o -MD -MP -MF $(DEPDIR)/test_lib-test-str-find.Tpo -c -o test_lib-test-str-find.o `test -f 'test-str-find.c' || echo '$(srcdir)/'`test-str-find.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/test_lib-test-str-find.Tpo $(DEPDIR)/test_lib-test-str-find.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='test-str-find.c' object='test_lib-test-str-find.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o test_lib-test-str-find.o `test -f 'test-str-find.c' || echo '$(srcdir)/'`test-str-find.c
+
+test_lib-test-str-find.obj: test-str-find.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT test_lib-test-str-find.obj -MD -MP -MF $(DEPDIR)/test_lib-test-str-find.Tpo -c -o test_lib-test-str-find.obj `if test -f 'test-str-find.c'; then $(CYGPATH_W) 'test-str-find.c'; else $(CYGPATH_W) '$(srcdir)/test-str-find.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/test_lib-test-str-find.Tpo $(DEPDIR)/test_lib-test-str-find.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='test-str-find.c' object='test_lib-test-str-find.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o test_lib-test-str-find.obj `if test -f 'test-str-find.c'; then $(CYGPATH_W) 'test-str-find.c'; else $(CYGPATH_W) '$(srcdir)/test-str-find.c'; fi`
+
+test_lib-test-str-sanitize.o: test-str-sanitize.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT test_lib-test-str-sanitize.o -MD -MP -MF $(DEPDIR)/test_lib-test-str-sanitize.Tpo -c -o test_lib-test-str-sanitize.o `test -f 'test-str-sanitize.c' || echo '$(srcdir)/'`test-str-sanitize.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/test_lib-test-str-sanitize.Tpo $(DEPDIR)/test_lib-test-str-sanitize.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='test-str-sanitize.c' object='test_lib-test-str-sanitize.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o test_lib-test-str-sanitize.o `test -f 'test-str-sanitize.c' || echo '$(srcdir)/'`test-str-sanitize.c
+
+test_lib-test-str-sanitize.obj: test-str-sanitize.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT test_lib-test-str-sanitize.obj -MD -MP -MF $(DEPDIR)/test_lib-test-str-sanitize.Tpo -c -o test_lib-test-str-sanitize.obj `if test -f 'test-str-sanitize.c'; then $(CYGPATH_W) 'test-str-sanitize.c'; else $(CYGPATH_W) '$(srcdir)/test-str-sanitize.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/test_lib-test-str-sanitize.Tpo $(DEPDIR)/test_lib-test-str-sanitize.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='test-str-sanitize.c' object='test_lib-test-str-sanitize.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o test_lib-test-str-sanitize.obj `if test -f 'test-str-sanitize.c'; then $(CYGPATH_W) 'test-str-sanitize.c'; else $(CYGPATH_W) '$(srcdir)/test-str-sanitize.c'; fi`
+
+test_lib-test-str-table.o: test-str-table.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT test_lib-test-str-table.o -MD -MP -MF $(DEPDIR)/test_lib-test-str-table.Tpo -c -o test_lib-test-str-table.o `test -f 'test-str-table.c' || echo '$(srcdir)/'`test-str-table.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/test_lib-test-str-table.Tpo $(DEPDIR)/test_lib-test-str-table.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='test-str-table.c' object='test_lib-test-str-table.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o test_lib-test-str-table.o `test -f 'test-str-table.c' || echo '$(srcdir)/'`test-str-table.c
+
+test_lib-test-str-table.obj: test-str-table.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT test_lib-test-str-table.obj -MD -MP -MF $(DEPDIR)/test_lib-test-str-table.Tpo -c -o test_lib-test-str-table.obj `if test -f 'test-str-table.c'; then $(CYGPATH_W) 'test-str-table.c'; else $(CYGPATH_W) '$(srcdir)/test-str-table.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/test_lib-test-str-table.Tpo $(DEPDIR)/test_lib-test-str-table.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='test-str-table.c' object='test_lib-test-str-table.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o test_lib-test-str-table.obj `if test -f 'test-str-table.c'; then $(CYGPATH_W) 'test-str-table.c'; else $(CYGPATH_W) '$(srcdir)/test-str-table.c'; fi`
+
+test_lib-test-time-util.o: test-time-util.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT test_lib-test-time-util.o -MD -MP -MF $(DEPDIR)/test_lib-test-time-util.Tpo -c -o test_lib-test-time-util.o `test -f 'test-time-util.c' || echo '$(srcdir)/'`test-time-util.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/test_lib-test-time-util.Tpo $(DEPDIR)/test_lib-test-time-util.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='test-time-util.c' object='test_lib-test-time-util.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o test_lib-test-time-util.o `test -f 'test-time-util.c' || echo '$(srcdir)/'`test-time-util.c
+
+test_lib-test-time-util.obj: test-time-util.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT test_lib-test-time-util.obj -MD -MP -MF $(DEPDIR)/test_lib-test-time-util.Tpo -c -o test_lib-test-time-util.obj `if test -f 'test-time-util.c'; then $(CYGPATH_W) 'test-time-util.c'; else $(CYGPATH_W) '$(srcdir)/test-time-util.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/test_lib-test-time-util.Tpo $(DEPDIR)/test_lib-test-time-util.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='test-time-util.c' object='test_lib-test-time-util.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o test_lib-test-time-util.obj `if test -f 'test-time-util.c'; then $(CYGPATH_W) 'test-time-util.c'; else $(CYGPATH_W) '$(srcdir)/test-time-util.c'; fi`
+
+test_lib-test-unichar.o: test-unichar.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT test_lib-test-unichar.o -MD -MP -MF $(DEPDIR)/test_lib-test-unichar.Tpo -c -o test_lib-test-unichar.o `test -f 'test-unichar.c' || echo '$(srcdir)/'`test-unichar.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/test_lib-test-unichar.Tpo $(DEPDIR)/test_lib-test-unichar.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='test-unichar.c' object='test_lib-test-unichar.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o test_lib-test-unichar.o `test -f 'test-unichar.c' || echo '$(srcdir)/'`test-unichar.c
+
+test_lib-test-unichar.obj: test-unichar.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT test_lib-test-unichar.obj -MD -MP -MF $(DEPDIR)/test_lib-test-unichar.Tpo -c -o test_lib-test-unichar.obj `if test -f 'test-unichar.c'; then $(CYGPATH_W) 'test-unichar.c'; else $(CYGPATH_W) '$(srcdir)/test-unichar.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/test_lib-test-unichar.Tpo $(DEPDIR)/test_lib-test-unichar.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='test-unichar.c' object='test_lib-test-unichar.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o test_lib-test-unichar.obj `if test -f 'test-unichar.c'; then $(CYGPATH_W) 'test-unichar.c'; else $(CYGPATH_W) '$(srcdir)/test-unichar.c'; fi`
+
+test_lib-test-utc-mktime.o: test-utc-mktime.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT test_lib-test-utc-mktime.o -MD -MP -MF $(DEPDIR)/test_lib-test-utc-mktime.Tpo -c -o test_lib-test-utc-mktime.o `test -f 'test-utc-mktime.c' || echo '$(srcdir)/'`test-utc-mktime.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/test_lib-test-utc-mktime.Tpo $(DEPDIR)/test_lib-test-utc-mktime.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='test-utc-mktime.c' object='test_lib-test-utc-mktime.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o test_lib-test-utc-mktime.o `test -f 'test-utc-mktime.c' || echo '$(srcdir)/'`test-utc-mktime.c
+
+test_lib-test-utc-mktime.obj: test-utc-mktime.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT test_lib-test-utc-mktime.obj -MD -MP -MF $(DEPDIR)/test_lib-test-utc-mktime.Tpo -c -o test_lib-test-utc-mktime.obj `if test -f 'test-utc-mktime.c'; then $(CYGPATH_W) 'test-utc-mktime.c'; else $(CYGPATH_W) '$(srcdir)/test-utc-mktime.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/test_lib-test-utc-mktime.Tpo $(DEPDIR)/test_lib-test-utc-mktime.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='test-utc-mktime.c' object='test_lib-test-utc-mktime.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o test_lib-test-utc-mktime.obj `if test -f 'test-utc-mktime.c'; then $(CYGPATH_W) 'test-utc-mktime.c'; else $(CYGPATH_W) '$(srcdir)/test-utc-mktime.c'; fi`
+
+test_lib-test-uri.o: test-uri.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT test_lib-test-uri.o -MD -MP -MF $(DEPDIR)/test_lib-test-uri.Tpo -c -o test_lib-test-uri.o `test -f 'test-uri.c' || echo '$(srcdir)/'`test-uri.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/test_lib-test-uri.Tpo $(DEPDIR)/test_lib-test-uri.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='test-uri.c' object='test_lib-test-uri.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o test_lib-test-uri.o `test -f 'test-uri.c' || echo '$(srcdir)/'`test-uri.c
+
+test_lib-test-uri.obj: test-uri.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT test_lib-test-uri.obj -MD -MP -MF $(DEPDIR)/test_lib-test-uri.Tpo -c -o test_lib-test-uri.obj `if test -f 'test-uri.c'; then $(CYGPATH_W) 'test-uri.c'; else $(CYGPATH_W) '$(srcdir)/test-uri.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/test_lib-test-uri.Tpo $(DEPDIR)/test_lib-test-uri.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='test-uri.c' object='test_lib-test-uri.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o test_lib-test-uri.obj `if test -f 'test-uri.c'; then $(CYGPATH_W) 'test-uri.c'; else $(CYGPATH_W) '$(srcdir)/test-uri.c'; fi`
+
+test_lib-test-var-expand.o: test-var-expand.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT test_lib-test-var-expand.o -MD -MP -MF $(DEPDIR)/test_lib-test-var-expand.Tpo -c -o test_lib-test-var-expand.o `test -f 'test-var-expand.c' || echo '$(srcdir)/'`test-var-expand.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/test_lib-test-var-expand.Tpo $(DEPDIR)/test_lib-test-var-expand.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='test-var-expand.c' object='test_lib-test-var-expand.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o test_lib-test-var-expand.o `test -f 'test-var-expand.c' || echo '$(srcdir)/'`test-var-expand.c
+
+test_lib-test-var-expand.obj: test-var-expand.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT test_lib-test-var-expand.obj -MD -MP -MF $(DEPDIR)/test_lib-test-var-expand.Tpo -c -o test_lib-test-var-expand.obj `if test -f 'test-var-expand.c'; then $(CYGPATH_W) 'test-var-expand.c'; else $(CYGPATH_W) '$(srcdir)/test-var-expand.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/test_lib-test-var-expand.Tpo $(DEPDIR)/test_lib-test-var-expand.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='test-var-expand.c' object='test_lib-test-var-expand.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o test_lib-test-var-expand.obj `if test -f 'test-var-expand.c'; then $(CYGPATH_W) 'test-var-expand.c'; else $(CYGPATH_W) '$(srcdir)/test-var-expand.c'; fi`
+
+test_lib-test-wildcard-match.o: test-wildcard-match.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT test_lib-test-wildcard-match.o -MD -MP -MF $(DEPDIR)/test_lib-test-wildcard-match.Tpo -c -o test_lib-test-wildcard-match.o `test -f 'test-wildcard-match.c' || echo '$(srcdir)/'`test-wildcard-match.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/test_lib-test-wildcard-match.Tpo $(DEPDIR)/test_lib-test-wildcard-match.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='test-wildcard-match.c' object='test_lib-test-wildcard-match.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o test_lib-test-wildcard-match.o `test -f 'test-wildcard-match.c' || echo '$(srcdir)/'`test-wildcard-match.c
+
+test_lib-test-wildcard-match.obj: test-wildcard-match.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT test_lib-test-wildcard-match.obj -MD -MP -MF $(DEPDIR)/test_lib-test-wildcard-match.Tpo -c -o test_lib-test-wildcard-match.obj `if test -f 'test-wildcard-match.c'; then $(CYGPATH_W) 'test-wildcard-match.c'; else $(CYGPATH_W) '$(srcdir)/test-wildcard-match.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/test_lib-test-wildcard-match.Tpo $(DEPDIR)/test_lib-test-wildcard-match.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='test-wildcard-match.c' object='test_lib-test-wildcard-match.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_lib_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o test_lib-test-wildcard-match.obj `if test -f 'test-wildcard-match.c'; then $(CYGPATH_W) 'test-wildcard-match.c'; else $(CYGPATH_W) '$(srcdir)/test-wildcard-match.c'; fi`
+
+mostlyclean-libtool:
+ -rm -f *.lo
+
+clean-libtool:
+ -rm -rf .libs _libs
+install-pkginc_libHEADERS: $(pkginc_lib_HEADERS)
+ @$(NORMAL_INSTALL)
+ @list='$(pkginc_lib_HEADERS)'; test -n "$(pkginc_libdir)" || list=; \
+ if test -n "$$list"; then \
+ echo " $(MKDIR_P) '$(DESTDIR)$(pkginc_libdir)'"; \
+ $(MKDIR_P) "$(DESTDIR)$(pkginc_libdir)" || exit 1; \
+ fi; \
+ for p in $$list; do \
+ if test -f "$$p"; then d=; else d="$(srcdir)/"; fi; \
+ echo "$$d$$p"; \
+ done | $(am__base_list) | \
+ while read files; do \
+ echo " $(INSTALL_HEADER) $$files '$(DESTDIR)$(pkginc_libdir)'"; \
+ $(INSTALL_HEADER) $$files "$(DESTDIR)$(pkginc_libdir)" || exit $$?; \
+ done
+
+uninstall-pkginc_libHEADERS:
+ @$(NORMAL_UNINSTALL)
+ @list='$(pkginc_lib_HEADERS)'; test -n "$(pkginc_libdir)" || list=; \
+ files=`for p in $$list; do echo $$p; done | sed -e 's|^.*/||'`; \
+ dir='$(DESTDIR)$(pkginc_libdir)'; $(am__uninstall_files_from_dir)
+
+ID: $(am__tagged_files)
+ $(am__define_uniq_tagged_files); mkid -fID $$unique
+tags: tags-am
+TAGS: tags
+
+tags-am: $(TAGS_DEPENDENCIES) $(am__tagged_files)
+ set x; \
+ here=`pwd`; \
+ $(am__define_uniq_tagged_files); \
+ shift; \
+ if test -z "$(ETAGS_ARGS)$$*$$unique"; then :; else \
+ test -n "$$unique" || unique=$$empty_fix; \
+ if test $$# -gt 0; then \
+ $(ETAGS) $(ETAGSFLAGS) $(AM_ETAGSFLAGS) $(ETAGS_ARGS) \
+ "$$@" $$unique; \
+ else \
+ $(ETAGS) $(ETAGSFLAGS) $(AM_ETAGSFLAGS) $(ETAGS_ARGS) \
+ $$unique; \
+ fi; \
+ fi
+ctags: ctags-am
+
+CTAGS: ctags
+ctags-am: $(TAGS_DEPENDENCIES) $(am__tagged_files)
+ $(am__define_uniq_tagged_files); \
+ test -z "$(CTAGS_ARGS)$$unique" \
+ || $(CTAGS) $(CTAGSFLAGS) $(AM_CTAGSFLAGS) $(CTAGS_ARGS) \
+ $$unique
+
+GTAGS:
+ here=`$(am__cd) $(top_builddir) && pwd` \
+ && $(am__cd) $(top_srcdir) \
+ && gtags -i $(GTAGS_ARGS) "$$here"
+cscopelist: cscopelist-am
+
+cscopelist-am: $(am__tagged_files)
+ list='$(am__tagged_files)'; \
+ case "$(srcdir)" in \
+ [\\/]* | ?:[\\/]*) sdir="$(srcdir)" ;; \
+ *) sdir=$(subdir)/$(srcdir) ;; \
+ esac; \
+ for i in $$list; do \
+ if test -f "$$i"; then \
+ echo "$(subdir)/$$i"; \
+ else \
+ echo "$$sdir/$$i"; \
+ fi; \
+ done >> $(top_builddir)/cscope.files
+
+distclean-tags:
+ -rm -f TAGS ID GTAGS GRTAGS GSYMS GPATH tags
+
+distdir: $(BUILT_SOURCES)
+ $(MAKE) $(AM_MAKEFLAGS) distdir-am
+
+distdir-am: $(DISTFILES)
+ @srcdirstrip=`echo "$(srcdir)" | sed 's/[].[^$$\\*]/\\\\&/g'`; \
+ topsrcdirstrip=`echo "$(top_srcdir)" | sed 's/[].[^$$\\*]/\\\\&/g'`; \
+ list='$(DISTFILES)'; \
+ dist_files=`for file in $$list; do echo $$file; done | \
+ sed -e "s|^$$srcdirstrip/||;t" \
+ -e "s|^$$topsrcdirstrip/|$(top_builddir)/|;t"`; \
+ case $$dist_files in \
+ */*) $(MKDIR_P) `echo "$$dist_files" | \
+ sed '/\//!d;s|^|$(distdir)/|;s,/[^/]*$$,,' | \
+ sort -u` ;; \
+ esac; \
+ for file in $$dist_files; do \
+ if test -f $$file || test -d $$file; then d=.; else d=$(srcdir); fi; \
+ if test -d $$d/$$file; then \
+ dir=`echo "/$$file" | sed -e 's,/[^/]*$$,,'`; \
+ if test -d "$(distdir)/$$file"; then \
+ find "$(distdir)/$$file" -type d ! -perm -700 -exec chmod u+rwx {} \;; \
+ fi; \
+ if test -d $(srcdir)/$$file && test $$d != $(srcdir); then \
+ cp -fpR $(srcdir)/$$file "$(distdir)$$dir" || exit 1; \
+ find "$(distdir)/$$file" -type d ! -perm -700 -exec chmod u+rwx {} \;; \
+ fi; \
+ cp -fpR $$d/$$file "$(distdir)$$dir" || exit 1; \
+ else \
+ test -f "$(distdir)/$$file" \
+ || cp -p $$d/$$file "$(distdir)/$$file" \
+ || exit 1; \
+ fi; \
+ done
+check-am: all-am
+ $(MAKE) $(AM_MAKEFLAGS) check-local
+check: $(BUILT_SOURCES)
+ $(MAKE) $(AM_MAKEFLAGS) check-am
+all-am: Makefile $(PROGRAMS) $(LTLIBRARIES) $(HEADERS)
+installdirs:
+ for dir in "$(DESTDIR)$(pkginc_libdir)"; do \
+ test -z "$$dir" || $(MKDIR_P) "$$dir"; \
+ done
+install: $(BUILT_SOURCES)
+ $(MAKE) $(AM_MAKEFLAGS) install-am
+install-exec: install-exec-am
+install-data: install-data-am
+uninstall: uninstall-am
+
+install-am: all-am
+ @$(MAKE) $(AM_MAKEFLAGS) install-exec-am install-data-am
+
+installcheck: installcheck-am
+install-strip:
+ if test -z '$(STRIP)'; then \
+ $(MAKE) $(AM_MAKEFLAGS) INSTALL_PROGRAM="$(INSTALL_STRIP_PROGRAM)" \
+ install_sh_PROGRAM="$(INSTALL_STRIP_PROGRAM)" INSTALL_STRIP_FLAG=-s \
+ install; \
+ else \
+ $(MAKE) $(AM_MAKEFLAGS) INSTALL_PROGRAM="$(INSTALL_STRIP_PROGRAM)" \
+ install_sh_PROGRAM="$(INSTALL_STRIP_PROGRAM)" INSTALL_STRIP_FLAG=-s \
+ "INSTALL_PROGRAM_ENV=STRIPPROG='$(STRIP)'" install; \
+ fi
+mostlyclean-generic:
+
+clean-generic:
+
+distclean-generic:
+ -test -z "$(CONFIG_CLEAN_FILES)" || rm -f $(CONFIG_CLEAN_FILES)
+ -test . = "$(srcdir)" || test -z "$(CONFIG_CLEAN_VPATH_FILES)" || rm -f $(CONFIG_CLEAN_VPATH_FILES)
+
+maintainer-clean-generic:
+ @echo "This command is intended for maintainers to use"
+ @echo "it deletes files that may require special tools to rebuild."
+ -rm -f event-filter-lexer.c
+ -rm -f event-filter-parser.c
+ -test -z "$(BUILT_SOURCES)" || rm -f $(BUILT_SOURCES)
+clean: clean-am
+
+clean-am: clean-generic clean-libtool clean-noinstLTLIBRARIES \
+ clean-noinstPROGRAMS mostlyclean-am
+
+distclean: distclean-am
+ -rm -f ./$(DEPDIR)/aqueue.Plo
+ -rm -f ./$(DEPDIR)/array.Plo
+ -rm -f ./$(DEPDIR)/askpass.Plo
+ -rm -f ./$(DEPDIR)/backtrace-string.Plo
+ -rm -f ./$(DEPDIR)/base32.Plo
+ -rm -f ./$(DEPDIR)/base64.Plo
+ -rm -f ./$(DEPDIR)/bits.Plo
+ -rm -f ./$(DEPDIR)/bsearch-insert-pos.Plo
+ -rm -f ./$(DEPDIR)/buffer-istream.Plo
+ -rm -f ./$(DEPDIR)/buffer.Plo
+ -rm -f ./$(DEPDIR)/child-wait.Plo
+ -rm -f ./$(DEPDIR)/compat.Plo
+ -rm -f ./$(DEPDIR)/connection.Plo
+ -rm -f ./$(DEPDIR)/cpu-limit.Plo
+ -rm -f ./$(DEPDIR)/crc32.Plo
+ -rm -f ./$(DEPDIR)/data-stack.Plo
+ -rm -f ./$(DEPDIR)/eacces-error.Plo
+ -rm -f ./$(DEPDIR)/env-util.Plo
+ -rm -f ./$(DEPDIR)/event-filter-lexer.Plo
+ -rm -f ./$(DEPDIR)/event-filter-parser.Plo
+ -rm -f ./$(DEPDIR)/event-filter.Plo
+ -rm -f ./$(DEPDIR)/event-log.Plo
+ -rm -f ./$(DEPDIR)/execv-const.Plo
+ -rm -f ./$(DEPDIR)/failures.Plo
+ -rm -f ./$(DEPDIR)/fd-util.Plo
+ -rm -f ./$(DEPDIR)/fdatasync-path.Plo
+ -rm -f ./$(DEPDIR)/fdpass.Plo
+ -rm -f ./$(DEPDIR)/file-cache.Plo
+ -rm -f ./$(DEPDIR)/file-copy.Plo
+ -rm -f ./$(DEPDIR)/file-create-locked.Plo
+ -rm -f ./$(DEPDIR)/file-dotlock.Plo
+ -rm -f ./$(DEPDIR)/file-lock.Plo
+ -rm -f ./$(DEPDIR)/file-set-size.Plo
+ -rm -f ./$(DEPDIR)/guid.Plo
+ -rm -f ./$(DEPDIR)/hash-format.Plo
+ -rm -f ./$(DEPDIR)/hash-method.Plo
+ -rm -f ./$(DEPDIR)/hash.Plo
+ -rm -f ./$(DEPDIR)/hash2.Plo
+ -rm -f ./$(DEPDIR)/hex-binary.Plo
+ -rm -f ./$(DEPDIR)/hex-dec.Plo
+ -rm -f ./$(DEPDIR)/hmac-cram-md5.Plo
+ -rm -f ./$(DEPDIR)/hmac.Plo
+ -rm -f ./$(DEPDIR)/home-expand.Plo
+ -rm -f ./$(DEPDIR)/hook-build.Plo
+ -rm -f ./$(DEPDIR)/hostpid.Plo
+ -rm -f ./$(DEPDIR)/imem.Plo
+ -rm -f ./$(DEPDIR)/ioloop-epoll.Plo
+ -rm -f ./$(DEPDIR)/ioloop-iolist.Plo
+ -rm -f ./$(DEPDIR)/ioloop-kqueue.Plo
+ -rm -f ./$(DEPDIR)/ioloop-notify-fd.Plo
+ -rm -f ./$(DEPDIR)/ioloop-notify-inotify.Plo
+ -rm -f ./$(DEPDIR)/ioloop-notify-kqueue.Plo
+ -rm -f ./$(DEPDIR)/ioloop-notify-none.Plo
+ -rm -f ./$(DEPDIR)/ioloop-poll.Plo
+ -rm -f ./$(DEPDIR)/ioloop-select.Plo
+ -rm -f ./$(DEPDIR)/ioloop.Plo
+ -rm -f ./$(DEPDIR)/iostream-proxy.Plo
+ -rm -f ./$(DEPDIR)/iostream-pump.Plo
+ -rm -f ./$(DEPDIR)/iostream-rawlog.Plo
+ -rm -f ./$(DEPDIR)/iostream-temp.Plo
+ -rm -f ./$(DEPDIR)/iostream.Plo
+ -rm -f ./$(DEPDIR)/ipwd.Plo
+ -rm -f ./$(DEPDIR)/iso8601-date.Plo
+ -rm -f ./$(DEPDIR)/istream-base64-decoder.Plo
+ -rm -f ./$(DEPDIR)/istream-base64-encoder.Plo
+ -rm -f ./$(DEPDIR)/istream-callback.Plo
+ -rm -f ./$(DEPDIR)/istream-chain.Plo
+ -rm -f ./$(DEPDIR)/istream-concat.Plo
+ -rm -f ./$(DEPDIR)/istream-crlf.Plo
+ -rm -f ./$(DEPDIR)/istream-data.Plo
+ -rm -f ./$(DEPDIR)/istream-failure-at.Plo
+ -rm -f ./$(DEPDIR)/istream-file.Plo
+ -rm -f ./$(DEPDIR)/istream-hash.Plo
+ -rm -f ./$(DEPDIR)/istream-jsonstr.Plo
+ -rm -f ./$(DEPDIR)/istream-limit.Plo
+ -rm -f ./$(DEPDIR)/istream-multiplex.Plo
+ -rm -f ./$(DEPDIR)/istream-rawlog.Plo
+ -rm -f ./$(DEPDIR)/istream-seekable.Plo
+ -rm -f ./$(DEPDIR)/istream-sized.Plo
+ -rm -f ./$(DEPDIR)/istream-tee.Plo
+ -rm -f ./$(DEPDIR)/istream-timeout.Plo
+ -rm -f ./$(DEPDIR)/istream-try.Plo
+ -rm -f ./$(DEPDIR)/istream-unix.Plo
+ -rm -f ./$(DEPDIR)/istream.Plo
+ -rm -f ./$(DEPDIR)/json-parser.Plo
+ -rm -f ./$(DEPDIR)/json-tree.Plo
+ -rm -f ./$(DEPDIR)/lib-event.Plo
+ -rm -f ./$(DEPDIR)/lib-signals.Plo
+ -rm -f ./$(DEPDIR)/lib.Plo
+ -rm -f ./$(DEPDIR)/log-throttle.Plo
+ -rm -f ./$(DEPDIR)/md4.Plo
+ -rm -f ./$(DEPDIR)/md5.Plo
+ -rm -f ./$(DEPDIR)/memarea.Plo
+ -rm -f ./$(DEPDIR)/mempool-allocfree.Plo
+ -rm -f ./$(DEPDIR)/mempool-alloconly.Plo
+ -rm -f ./$(DEPDIR)/mempool-datastack.Plo
+ -rm -f ./$(DEPDIR)/mempool-system.Plo
+ -rm -f ./$(DEPDIR)/mempool-unsafe-datastack.Plo
+ -rm -f ./$(DEPDIR)/mempool.Plo
+ -rm -f ./$(DEPDIR)/mkdir-parents.Plo
+ -rm -f ./$(DEPDIR)/mmap-anon.Plo
+ -rm -f ./$(DEPDIR)/mmap-util.Plo
+ -rm -f ./$(DEPDIR)/module-dir.Plo
+ -rm -f ./$(DEPDIR)/mountpoint.Plo
+ -rm -f ./$(DEPDIR)/net.Plo
+ -rm -f ./$(DEPDIR)/nfs-workarounds.Plo
+ -rm -f ./$(DEPDIR)/numpack.Plo
+ -rm -f ./$(DEPDIR)/ostream-buffer.Plo
+ -rm -f ./$(DEPDIR)/ostream-failure-at.Plo
+ -rm -f ./$(DEPDIR)/ostream-file.Plo
+ -rm -f ./$(DEPDIR)/ostream-hash.Plo
+ -rm -f ./$(DEPDIR)/ostream-multiplex.Plo
+ -rm -f ./$(DEPDIR)/ostream-null.Plo
+ -rm -f ./$(DEPDIR)/ostream-rawlog.Plo
+ -rm -f ./$(DEPDIR)/ostream-unix.Plo
+ -rm -f ./$(DEPDIR)/ostream-wrapper.Plo
+ -rm -f ./$(DEPDIR)/ostream.Plo
+ -rm -f ./$(DEPDIR)/path-util.Plo
+ -rm -f ./$(DEPDIR)/pkcs5.Plo
+ -rm -f ./$(DEPDIR)/primes.Plo
+ -rm -f ./$(DEPDIR)/printf-format-fix.Plo
+ -rm -f ./$(DEPDIR)/priorityq.Plo
+ -rm -f ./$(DEPDIR)/process-stat.Plo
+ -rm -f ./$(DEPDIR)/process-title.Plo
+ -rm -f ./$(DEPDIR)/rand.Plo
+ -rm -f ./$(DEPDIR)/randgen.Plo
+ -rm -f ./$(DEPDIR)/read-full.Plo
+ -rm -f ./$(DEPDIR)/restrict-access.Plo
+ -rm -f ./$(DEPDIR)/restrict-process-size.Plo
+ -rm -f ./$(DEPDIR)/safe-memset.Plo
+ -rm -f ./$(DEPDIR)/safe-mkdir.Plo
+ -rm -f ./$(DEPDIR)/safe-mkstemp.Plo
+ -rm -f ./$(DEPDIR)/sendfile-util.Plo
+ -rm -f ./$(DEPDIR)/seq-range-array.Plo
+ -rm -f ./$(DEPDIR)/seq-set-builder.Plo
+ -rm -f ./$(DEPDIR)/sha1.Plo
+ -rm -f ./$(DEPDIR)/sha2.Plo
+ -rm -f ./$(DEPDIR)/sha3.Plo
+ -rm -f ./$(DEPDIR)/sleep.Plo
+ -rm -f ./$(DEPDIR)/sort.Plo
+ -rm -f ./$(DEPDIR)/stats-dist.Plo
+ -rm -f ./$(DEPDIR)/str-find.Plo
+ -rm -f ./$(DEPDIR)/str-sanitize.Plo
+ -rm -f ./$(DEPDIR)/str-table.Plo
+ -rm -f ./$(DEPDIR)/str.Plo
+ -rm -f ./$(DEPDIR)/strescape.Plo
+ -rm -f ./$(DEPDIR)/strfuncs.Plo
+ -rm -f ./$(DEPDIR)/strnum.Plo
+ -rm -f ./$(DEPDIR)/test_lib-test-aqueue.Po
+ -rm -f ./$(DEPDIR)/test_lib-test-array.Po
+ -rm -f ./$(DEPDIR)/test_lib-test-backtrace.Po
+ -rm -f ./$(DEPDIR)/test_lib-test-base32.Po
+ -rm -f ./$(DEPDIR)/test_lib-test-base64.Po
+ -rm -f ./$(DEPDIR)/test_lib-test-bits.Po
+ -rm -f ./$(DEPDIR)/test_lib-test-bsearch-insert-pos.Po
+ -rm -f ./$(DEPDIR)/test_lib-test-buffer-istream.Po
+ -rm -f ./$(DEPDIR)/test_lib-test-buffer.Po
+ -rm -f ./$(DEPDIR)/test_lib-test-byteorder.Po
+ -rm -f ./$(DEPDIR)/test_lib-test-connection.Po
+ -rm -f ./$(DEPDIR)/test_lib-test-cpu-limit.Po
+ -rm -f ./$(DEPDIR)/test_lib-test-crc32.Po
+ -rm -f ./$(DEPDIR)/test_lib-test-data-stack.Po
+ -rm -f ./$(DEPDIR)/test_lib-test-env-util.Po
+ -rm -f ./$(DEPDIR)/test_lib-test-event-category-register.Po
+ -rm -f ./$(DEPDIR)/test_lib-test-event-filter-expr.Po
+ -rm -f ./$(DEPDIR)/test_lib-test-event-filter-merge.Po
+ -rm -f ./$(DEPDIR)/test_lib-test-event-filter-parser.Po
+ -rm -f ./$(DEPDIR)/test_lib-test-event-filter.Po
+ -rm -f ./$(DEPDIR)/test_lib-test-event-flatten.Po
+ -rm -f ./$(DEPDIR)/test_lib-test-event-log.Po
+ -rm -f ./$(DEPDIR)/test_lib-test-failures.Po
+ -rm -f ./$(DEPDIR)/test_lib-test-fd-util.Po
+ -rm -f ./$(DEPDIR)/test_lib-test-file-cache.Po
+ -rm -f ./$(DEPDIR)/test_lib-test-file-create-locked.Po
+ -rm -f ./$(DEPDIR)/test_lib-test-guid.Po
+ -rm -f ./$(DEPDIR)/test_lib-test-hash-format.Po
+ -rm -f ./$(DEPDIR)/test_lib-test-hash-method.Po
+ -rm -f ./$(DEPDIR)/test_lib-test-hash.Po
+ -rm -f ./$(DEPDIR)/test_lib-test-hex-binary.Po
+ -rm -f ./$(DEPDIR)/test_lib-test-hmac.Po
+ -rm -f ./$(DEPDIR)/test_lib-test-imem.Po
+ -rm -f ./$(DEPDIR)/test_lib-test-ioloop.Po
+ -rm -f ./$(DEPDIR)/test_lib-test-iostream-proxy.Po
+ -rm -f ./$(DEPDIR)/test_lib-test-iostream-pump.Po
+ -rm -f ./$(DEPDIR)/test_lib-test-iostream-temp.Po
+ -rm -f ./$(DEPDIR)/test_lib-test-iso8601-date.Po
+ -rm -f ./$(DEPDIR)/test_lib-test-istream-base64-decoder.Po
+ -rm -f ./$(DEPDIR)/test_lib-test-istream-base64-encoder.Po
+ -rm -f ./$(DEPDIR)/test_lib-test-istream-chain.Po
+ -rm -f ./$(DEPDIR)/test_lib-test-istream-concat.Po
+ -rm -f ./$(DEPDIR)/test_lib-test-istream-crlf.Po
+ -rm -f ./$(DEPDIR)/test_lib-test-istream-failure-at.Po
+ -rm -f ./$(DEPDIR)/test_lib-test-istream-jsonstr.Po
+ -rm -f ./$(DEPDIR)/test_lib-test-istream-multiplex.Po
+ -rm -f ./$(DEPDIR)/test_lib-test-istream-seekable.Po
+ -rm -f ./$(DEPDIR)/test_lib-test-istream-sized.Po
+ -rm -f ./$(DEPDIR)/test_lib-test-istream-tee.Po
+ -rm -f ./$(DEPDIR)/test_lib-test-istream-try.Po
+ -rm -f ./$(DEPDIR)/test_lib-test-istream-unix.Po
+ -rm -f ./$(DEPDIR)/test_lib-test-istream.Po
+ -rm -f ./$(DEPDIR)/test_lib-test-json-parser.Po
+ -rm -f ./$(DEPDIR)/test_lib-test-json-tree.Po
+ -rm -f ./$(DEPDIR)/test_lib-test-lib-event.Po
+ -rm -f ./$(DEPDIR)/test_lib-test-lib-signals.Po
+ -rm -f ./$(DEPDIR)/test_lib-test-lib.Po
+ -rm -f ./$(DEPDIR)/test_lib-test-llist.Po
+ -rm -f ./$(DEPDIR)/test_lib-test-log-throttle.Po
+ -rm -f ./$(DEPDIR)/test_lib-test-macros.Po
+ -rm -f ./$(DEPDIR)/test_lib-test-malloc-overflow.Po
+ -rm -f ./$(DEPDIR)/test_lib-test-memarea.Po
+ -rm -f ./$(DEPDIR)/test_lib-test-mempool-allocfree.Po
+ -rm -f ./$(DEPDIR)/test_lib-test-mempool-alloconly.Po
+ -rm -f ./$(DEPDIR)/test_lib-test-mempool.Po
+ -rm -f ./$(DEPDIR)/test_lib-test-multiplex.Po
+ -rm -f ./$(DEPDIR)/test_lib-test-net.Po
+ -rm -f ./$(DEPDIR)/test_lib-test-numpack.Po
+ -rm -f ./$(DEPDIR)/test_lib-test-ostream-buffer.Po
+ -rm -f ./$(DEPDIR)/test_lib-test-ostream-failure-at.Po
+ -rm -f ./$(DEPDIR)/test_lib-test-ostream-file.Po
+ -rm -f ./$(DEPDIR)/test_lib-test-ostream-multiplex.Po
+ -rm -f ./$(DEPDIR)/test_lib-test-path-util.Po
+ -rm -f ./$(DEPDIR)/test_lib-test-pkcs5.Po
+ -rm -f ./$(DEPDIR)/test_lib-test-primes.Po
+ -rm -f ./$(DEPDIR)/test_lib-test-printf-format-fix.Po
+ -rm -f ./$(DEPDIR)/test_lib-test-priorityq.Po
+ -rm -f ./$(DEPDIR)/test_lib-test-random.Po
+ -rm -f ./$(DEPDIR)/test_lib-test-seq-range-array.Po
+ -rm -f ./$(DEPDIR)/test_lib-test-seq-set-builder.Po
+ -rm -f ./$(DEPDIR)/test_lib-test-stats-dist.Po
+ -rm -f ./$(DEPDIR)/test_lib-test-str-find.Po
+ -rm -f ./$(DEPDIR)/test_lib-test-str-sanitize.Po
+ -rm -f ./$(DEPDIR)/test_lib-test-str-table.Po
+ -rm -f ./$(DEPDIR)/test_lib-test-str.Po
+ -rm -f ./$(DEPDIR)/test_lib-test-strescape.Po
+ -rm -f ./$(DEPDIR)/test_lib-test-strfuncs.Po
+ -rm -f ./$(DEPDIR)/test_lib-test-strnum.Po
+ -rm -f ./$(DEPDIR)/test_lib-test-time-util.Po
+ -rm -f ./$(DEPDIR)/test_lib-test-unichar.Po
+ -rm -f ./$(DEPDIR)/test_lib-test-uri.Po
+ -rm -f ./$(DEPDIR)/test_lib-test-utc-mktime.Po
+ -rm -f ./$(DEPDIR)/test_lib-test-var-expand.Po
+ -rm -f ./$(DEPDIR)/test_lib-test-wildcard-match.Po
+ -rm -f ./$(DEPDIR)/time-util.Plo
+ -rm -f ./$(DEPDIR)/unichar.Plo
+ -rm -f ./$(DEPDIR)/unix-socket-create.Plo
+ -rm -f ./$(DEPDIR)/unlink-directory.Plo
+ -rm -f ./$(DEPDIR)/unlink-old-files.Plo
+ -rm -f ./$(DEPDIR)/uri-util.Plo
+ -rm -f ./$(DEPDIR)/utc-mktime.Plo
+ -rm -f ./$(DEPDIR)/utc-offset.Plo
+ -rm -f ./$(DEPDIR)/var-expand-if.Plo
+ -rm -f ./$(DEPDIR)/var-expand.Plo
+ -rm -f ./$(DEPDIR)/wildcard-match.Plo
+ -rm -f ./$(DEPDIR)/write-full.Plo
+ -rm -f Makefile
+distclean-am: clean-am distclean-compile distclean-generic \
+ distclean-tags
+
+dvi: dvi-am
+
+dvi-am:
+
+html: html-am
+
+html-am:
+
+info: info-am
+
+info-am:
+
+install-data-am: install-pkginc_libHEADERS
+
+install-dvi: install-dvi-am
+
+install-dvi-am:
+
+install-exec-am:
+
+install-html: install-html-am
+
+install-html-am:
+
+install-info: install-info-am
+
+install-info-am:
+
+install-man:
+
+install-pdf: install-pdf-am
+
+install-pdf-am:
+
+install-ps: install-ps-am
+
+install-ps-am:
+
+installcheck-am:
+
+maintainer-clean: maintainer-clean-am
+ -rm -f ./$(DEPDIR)/aqueue.Plo
+ -rm -f ./$(DEPDIR)/array.Plo
+ -rm -f ./$(DEPDIR)/askpass.Plo
+ -rm -f ./$(DEPDIR)/backtrace-string.Plo
+ -rm -f ./$(DEPDIR)/base32.Plo
+ -rm -f ./$(DEPDIR)/base64.Plo
+ -rm -f ./$(DEPDIR)/bits.Plo
+ -rm -f ./$(DEPDIR)/bsearch-insert-pos.Plo
+ -rm -f ./$(DEPDIR)/buffer-istream.Plo
+ -rm -f ./$(DEPDIR)/buffer.Plo
+ -rm -f ./$(DEPDIR)/child-wait.Plo
+ -rm -f ./$(DEPDIR)/compat.Plo
+ -rm -f ./$(DEPDIR)/connection.Plo
+ -rm -f ./$(DEPDIR)/cpu-limit.Plo
+ -rm -f ./$(DEPDIR)/crc32.Plo
+ -rm -f ./$(DEPDIR)/data-stack.Plo
+ -rm -f ./$(DEPDIR)/eacces-error.Plo
+ -rm -f ./$(DEPDIR)/env-util.Plo
+ -rm -f ./$(DEPDIR)/event-filter-lexer.Plo
+ -rm -f ./$(DEPDIR)/event-filter-parser.Plo
+ -rm -f ./$(DEPDIR)/event-filter.Plo
+ -rm -f ./$(DEPDIR)/event-log.Plo
+ -rm -f ./$(DEPDIR)/execv-const.Plo
+ -rm -f ./$(DEPDIR)/failures.Plo
+ -rm -f ./$(DEPDIR)/fd-util.Plo
+ -rm -f ./$(DEPDIR)/fdatasync-path.Plo
+ -rm -f ./$(DEPDIR)/fdpass.Plo
+ -rm -f ./$(DEPDIR)/file-cache.Plo
+ -rm -f ./$(DEPDIR)/file-copy.Plo
+ -rm -f ./$(DEPDIR)/file-create-locked.Plo
+ -rm -f ./$(DEPDIR)/file-dotlock.Plo
+ -rm -f ./$(DEPDIR)/file-lock.Plo
+ -rm -f ./$(DEPDIR)/file-set-size.Plo
+ -rm -f ./$(DEPDIR)/guid.Plo
+ -rm -f ./$(DEPDIR)/hash-format.Plo
+ -rm -f ./$(DEPDIR)/hash-method.Plo
+ -rm -f ./$(DEPDIR)/hash.Plo
+ -rm -f ./$(DEPDIR)/hash2.Plo
+ -rm -f ./$(DEPDIR)/hex-binary.Plo
+ -rm -f ./$(DEPDIR)/hex-dec.Plo
+ -rm -f ./$(DEPDIR)/hmac-cram-md5.Plo
+ -rm -f ./$(DEPDIR)/hmac.Plo
+ -rm -f ./$(DEPDIR)/home-expand.Plo
+ -rm -f ./$(DEPDIR)/hook-build.Plo
+ -rm -f ./$(DEPDIR)/hostpid.Plo
+ -rm -f ./$(DEPDIR)/imem.Plo
+ -rm -f ./$(DEPDIR)/ioloop-epoll.Plo
+ -rm -f ./$(DEPDIR)/ioloop-iolist.Plo
+ -rm -f ./$(DEPDIR)/ioloop-kqueue.Plo
+ -rm -f ./$(DEPDIR)/ioloop-notify-fd.Plo
+ -rm -f ./$(DEPDIR)/ioloop-notify-inotify.Plo
+ -rm -f ./$(DEPDIR)/ioloop-notify-kqueue.Plo
+ -rm -f ./$(DEPDIR)/ioloop-notify-none.Plo
+ -rm -f ./$(DEPDIR)/ioloop-poll.Plo
+ -rm -f ./$(DEPDIR)/ioloop-select.Plo
+ -rm -f ./$(DEPDIR)/ioloop.Plo
+ -rm -f ./$(DEPDIR)/iostream-proxy.Plo
+ -rm -f ./$(DEPDIR)/iostream-pump.Plo
+ -rm -f ./$(DEPDIR)/iostream-rawlog.Plo
+ -rm -f ./$(DEPDIR)/iostream-temp.Plo
+ -rm -f ./$(DEPDIR)/iostream.Plo
+ -rm -f ./$(DEPDIR)/ipwd.Plo
+ -rm -f ./$(DEPDIR)/iso8601-date.Plo
+ -rm -f ./$(DEPDIR)/istream-base64-decoder.Plo
+ -rm -f ./$(DEPDIR)/istream-base64-encoder.Plo
+ -rm -f ./$(DEPDIR)/istream-callback.Plo
+ -rm -f ./$(DEPDIR)/istream-chain.Plo
+ -rm -f ./$(DEPDIR)/istream-concat.Plo
+ -rm -f ./$(DEPDIR)/istream-crlf.Plo
+ -rm -f ./$(DEPDIR)/istream-data.Plo
+ -rm -f ./$(DEPDIR)/istream-failure-at.Plo
+ -rm -f ./$(DEPDIR)/istream-file.Plo
+ -rm -f ./$(DEPDIR)/istream-hash.Plo
+ -rm -f ./$(DEPDIR)/istream-jsonstr.Plo
+ -rm -f ./$(DEPDIR)/istream-limit.Plo
+ -rm -f ./$(DEPDIR)/istream-multiplex.Plo
+ -rm -f ./$(DEPDIR)/istream-rawlog.Plo
+ -rm -f ./$(DEPDIR)/istream-seekable.Plo
+ -rm -f ./$(DEPDIR)/istream-sized.Plo
+ -rm -f ./$(DEPDIR)/istream-tee.Plo
+ -rm -f ./$(DEPDIR)/istream-timeout.Plo
+ -rm -f ./$(DEPDIR)/istream-try.Plo
+ -rm -f ./$(DEPDIR)/istream-unix.Plo
+ -rm -f ./$(DEPDIR)/istream.Plo
+ -rm -f ./$(DEPDIR)/json-parser.Plo
+ -rm -f ./$(DEPDIR)/json-tree.Plo
+ -rm -f ./$(DEPDIR)/lib-event.Plo
+ -rm -f ./$(DEPDIR)/lib-signals.Plo
+ -rm -f ./$(DEPDIR)/lib.Plo
+ -rm -f ./$(DEPDIR)/log-throttle.Plo
+ -rm -f ./$(DEPDIR)/md4.Plo
+ -rm -f ./$(DEPDIR)/md5.Plo
+ -rm -f ./$(DEPDIR)/memarea.Plo
+ -rm -f ./$(DEPDIR)/mempool-allocfree.Plo
+ -rm -f ./$(DEPDIR)/mempool-alloconly.Plo
+ -rm -f ./$(DEPDIR)/mempool-datastack.Plo
+ -rm -f ./$(DEPDIR)/mempool-system.Plo
+ -rm -f ./$(DEPDIR)/mempool-unsafe-datastack.Plo
+ -rm -f ./$(DEPDIR)/mempool.Plo
+ -rm -f ./$(DEPDIR)/mkdir-parents.Plo
+ -rm -f ./$(DEPDIR)/mmap-anon.Plo
+ -rm -f ./$(DEPDIR)/mmap-util.Plo
+ -rm -f ./$(DEPDIR)/module-dir.Plo
+ -rm -f ./$(DEPDIR)/mountpoint.Plo
+ -rm -f ./$(DEPDIR)/net.Plo
+ -rm -f ./$(DEPDIR)/nfs-workarounds.Plo
+ -rm -f ./$(DEPDIR)/numpack.Plo
+ -rm -f ./$(DEPDIR)/ostream-buffer.Plo
+ -rm -f ./$(DEPDIR)/ostream-failure-at.Plo
+ -rm -f ./$(DEPDIR)/ostream-file.Plo
+ -rm -f ./$(DEPDIR)/ostream-hash.Plo
+ -rm -f ./$(DEPDIR)/ostream-multiplex.Plo
+ -rm -f ./$(DEPDIR)/ostream-null.Plo
+ -rm -f ./$(DEPDIR)/ostream-rawlog.Plo
+ -rm -f ./$(DEPDIR)/ostream-unix.Plo
+ -rm -f ./$(DEPDIR)/ostream-wrapper.Plo
+ -rm -f ./$(DEPDIR)/ostream.Plo
+ -rm -f ./$(DEPDIR)/path-util.Plo
+ -rm -f ./$(DEPDIR)/pkcs5.Plo
+ -rm -f ./$(DEPDIR)/primes.Plo
+ -rm -f ./$(DEPDIR)/printf-format-fix.Plo
+ -rm -f ./$(DEPDIR)/priorityq.Plo
+ -rm -f ./$(DEPDIR)/process-stat.Plo
+ -rm -f ./$(DEPDIR)/process-title.Plo
+ -rm -f ./$(DEPDIR)/rand.Plo
+ -rm -f ./$(DEPDIR)/randgen.Plo
+ -rm -f ./$(DEPDIR)/read-full.Plo
+ -rm -f ./$(DEPDIR)/restrict-access.Plo
+ -rm -f ./$(DEPDIR)/restrict-process-size.Plo
+ -rm -f ./$(DEPDIR)/safe-memset.Plo
+ -rm -f ./$(DEPDIR)/safe-mkdir.Plo
+ -rm -f ./$(DEPDIR)/safe-mkstemp.Plo
+ -rm -f ./$(DEPDIR)/sendfile-util.Plo
+ -rm -f ./$(DEPDIR)/seq-range-array.Plo
+ -rm -f ./$(DEPDIR)/seq-set-builder.Plo
+ -rm -f ./$(DEPDIR)/sha1.Plo
+ -rm -f ./$(DEPDIR)/sha2.Plo
+ -rm -f ./$(DEPDIR)/sha3.Plo
+ -rm -f ./$(DEPDIR)/sleep.Plo
+ -rm -f ./$(DEPDIR)/sort.Plo
+ -rm -f ./$(DEPDIR)/stats-dist.Plo
+ -rm -f ./$(DEPDIR)/str-find.Plo
+ -rm -f ./$(DEPDIR)/str-sanitize.Plo
+ -rm -f ./$(DEPDIR)/str-table.Plo
+ -rm -f ./$(DEPDIR)/str.Plo
+ -rm -f ./$(DEPDIR)/strescape.Plo
+ -rm -f ./$(DEPDIR)/strfuncs.Plo
+ -rm -f ./$(DEPDIR)/strnum.Plo
+ -rm -f ./$(DEPDIR)/test_lib-test-aqueue.Po
+ -rm -f ./$(DEPDIR)/test_lib-test-array.Po
+ -rm -f ./$(DEPDIR)/test_lib-test-backtrace.Po
+ -rm -f ./$(DEPDIR)/test_lib-test-base32.Po
+ -rm -f ./$(DEPDIR)/test_lib-test-base64.Po
+ -rm -f ./$(DEPDIR)/test_lib-test-bits.Po
+ -rm -f ./$(DEPDIR)/test_lib-test-bsearch-insert-pos.Po
+ -rm -f ./$(DEPDIR)/test_lib-test-buffer-istream.Po
+ -rm -f ./$(DEPDIR)/test_lib-test-buffer.Po
+ -rm -f ./$(DEPDIR)/test_lib-test-byteorder.Po
+ -rm -f ./$(DEPDIR)/test_lib-test-connection.Po
+ -rm -f ./$(DEPDIR)/test_lib-test-cpu-limit.Po
+ -rm -f ./$(DEPDIR)/test_lib-test-crc32.Po
+ -rm -f ./$(DEPDIR)/test_lib-test-data-stack.Po
+ -rm -f ./$(DEPDIR)/test_lib-test-env-util.Po
+ -rm -f ./$(DEPDIR)/test_lib-test-event-category-register.Po
+ -rm -f ./$(DEPDIR)/test_lib-test-event-filter-expr.Po
+ -rm -f ./$(DEPDIR)/test_lib-test-event-filter-merge.Po
+ -rm -f ./$(DEPDIR)/test_lib-test-event-filter-parser.Po
+ -rm -f ./$(DEPDIR)/test_lib-test-event-filter.Po
+ -rm -f ./$(DEPDIR)/test_lib-test-event-flatten.Po
+ -rm -f ./$(DEPDIR)/test_lib-test-event-log.Po
+ -rm -f ./$(DEPDIR)/test_lib-test-failures.Po
+ -rm -f ./$(DEPDIR)/test_lib-test-fd-util.Po
+ -rm -f ./$(DEPDIR)/test_lib-test-file-cache.Po
+ -rm -f ./$(DEPDIR)/test_lib-test-file-create-locked.Po
+ -rm -f ./$(DEPDIR)/test_lib-test-guid.Po
+ -rm -f ./$(DEPDIR)/test_lib-test-hash-format.Po
+ -rm -f ./$(DEPDIR)/test_lib-test-hash-method.Po
+ -rm -f ./$(DEPDIR)/test_lib-test-hash.Po
+ -rm -f ./$(DEPDIR)/test_lib-test-hex-binary.Po
+ -rm -f ./$(DEPDIR)/test_lib-test-hmac.Po
+ -rm -f ./$(DEPDIR)/test_lib-test-imem.Po
+ -rm -f ./$(DEPDIR)/test_lib-test-ioloop.Po
+ -rm -f ./$(DEPDIR)/test_lib-test-iostream-proxy.Po
+ -rm -f ./$(DEPDIR)/test_lib-test-iostream-pump.Po
+ -rm -f ./$(DEPDIR)/test_lib-test-iostream-temp.Po
+ -rm -f ./$(DEPDIR)/test_lib-test-iso8601-date.Po
+ -rm -f ./$(DEPDIR)/test_lib-test-istream-base64-decoder.Po
+ -rm -f ./$(DEPDIR)/test_lib-test-istream-base64-encoder.Po
+ -rm -f ./$(DEPDIR)/test_lib-test-istream-chain.Po
+ -rm -f ./$(DEPDIR)/test_lib-test-istream-concat.Po
+ -rm -f ./$(DEPDIR)/test_lib-test-istream-crlf.Po
+ -rm -f ./$(DEPDIR)/test_lib-test-istream-failure-at.Po
+ -rm -f ./$(DEPDIR)/test_lib-test-istream-jsonstr.Po
+ -rm -f ./$(DEPDIR)/test_lib-test-istream-multiplex.Po
+ -rm -f ./$(DEPDIR)/test_lib-test-istream-seekable.Po
+ -rm -f ./$(DEPDIR)/test_lib-test-istream-sized.Po
+ -rm -f ./$(DEPDIR)/test_lib-test-istream-tee.Po
+ -rm -f ./$(DEPDIR)/test_lib-test-istream-try.Po
+ -rm -f ./$(DEPDIR)/test_lib-test-istream-unix.Po
+ -rm -f ./$(DEPDIR)/test_lib-test-istream.Po
+ -rm -f ./$(DEPDIR)/test_lib-test-json-parser.Po
+ -rm -f ./$(DEPDIR)/test_lib-test-json-tree.Po
+ -rm -f ./$(DEPDIR)/test_lib-test-lib-event.Po
+ -rm -f ./$(DEPDIR)/test_lib-test-lib-signals.Po
+ -rm -f ./$(DEPDIR)/test_lib-test-lib.Po
+ -rm -f ./$(DEPDIR)/test_lib-test-llist.Po
+ -rm -f ./$(DEPDIR)/test_lib-test-log-throttle.Po
+ -rm -f ./$(DEPDIR)/test_lib-test-macros.Po
+ -rm -f ./$(DEPDIR)/test_lib-test-malloc-overflow.Po
+ -rm -f ./$(DEPDIR)/test_lib-test-memarea.Po
+ -rm -f ./$(DEPDIR)/test_lib-test-mempool-allocfree.Po
+ -rm -f ./$(DEPDIR)/test_lib-test-mempool-alloconly.Po
+ -rm -f ./$(DEPDIR)/test_lib-test-mempool.Po
+ -rm -f ./$(DEPDIR)/test_lib-test-multiplex.Po
+ -rm -f ./$(DEPDIR)/test_lib-test-net.Po
+ -rm -f ./$(DEPDIR)/test_lib-test-numpack.Po
+ -rm -f ./$(DEPDIR)/test_lib-test-ostream-buffer.Po
+ -rm -f ./$(DEPDIR)/test_lib-test-ostream-failure-at.Po
+ -rm -f ./$(DEPDIR)/test_lib-test-ostream-file.Po
+ -rm -f ./$(DEPDIR)/test_lib-test-ostream-multiplex.Po
+ -rm -f ./$(DEPDIR)/test_lib-test-path-util.Po
+ -rm -f ./$(DEPDIR)/test_lib-test-pkcs5.Po
+ -rm -f ./$(DEPDIR)/test_lib-test-primes.Po
+ -rm -f ./$(DEPDIR)/test_lib-test-printf-format-fix.Po
+ -rm -f ./$(DEPDIR)/test_lib-test-priorityq.Po
+ -rm -f ./$(DEPDIR)/test_lib-test-random.Po
+ -rm -f ./$(DEPDIR)/test_lib-test-seq-range-array.Po
+ -rm -f ./$(DEPDIR)/test_lib-test-seq-set-builder.Po
+ -rm -f ./$(DEPDIR)/test_lib-test-stats-dist.Po
+ -rm -f ./$(DEPDIR)/test_lib-test-str-find.Po
+ -rm -f ./$(DEPDIR)/test_lib-test-str-sanitize.Po
+ -rm -f ./$(DEPDIR)/test_lib-test-str-table.Po
+ -rm -f ./$(DEPDIR)/test_lib-test-str.Po
+ -rm -f ./$(DEPDIR)/test_lib-test-strescape.Po
+ -rm -f ./$(DEPDIR)/test_lib-test-strfuncs.Po
+ -rm -f ./$(DEPDIR)/test_lib-test-strnum.Po
+ -rm -f ./$(DEPDIR)/test_lib-test-time-util.Po
+ -rm -f ./$(DEPDIR)/test_lib-test-unichar.Po
+ -rm -f ./$(DEPDIR)/test_lib-test-uri.Po
+ -rm -f ./$(DEPDIR)/test_lib-test-utc-mktime.Po
+ -rm -f ./$(DEPDIR)/test_lib-test-var-expand.Po
+ -rm -f ./$(DEPDIR)/test_lib-test-wildcard-match.Po
+ -rm -f ./$(DEPDIR)/time-util.Plo
+ -rm -f ./$(DEPDIR)/unichar.Plo
+ -rm -f ./$(DEPDIR)/unix-socket-create.Plo
+ -rm -f ./$(DEPDIR)/unlink-directory.Plo
+ -rm -f ./$(DEPDIR)/unlink-old-files.Plo
+ -rm -f ./$(DEPDIR)/uri-util.Plo
+ -rm -f ./$(DEPDIR)/utc-mktime.Plo
+ -rm -f ./$(DEPDIR)/utc-offset.Plo
+ -rm -f ./$(DEPDIR)/var-expand-if.Plo
+ -rm -f ./$(DEPDIR)/var-expand.Plo
+ -rm -f ./$(DEPDIR)/wildcard-match.Plo
+ -rm -f ./$(DEPDIR)/write-full.Plo
+ -rm -f Makefile
+maintainer-clean-am: distclean-am maintainer-clean-generic
+
+mostlyclean: mostlyclean-am
+
+mostlyclean-am: mostlyclean-compile mostlyclean-generic \
+ mostlyclean-libtool
+
+pdf: pdf-am
+
+pdf-am:
+
+ps: ps-am
+
+ps-am:
+
+uninstall-am: uninstall-pkginc_libHEADERS
+
+.MAKE: all check check-am install install-am install-strip
+
+.PHONY: CTAGS GTAGS TAGS all all-am am--depfiles check check-am \
+ check-local clean clean-generic clean-libtool \
+ clean-noinstLTLIBRARIES clean-noinstPROGRAMS cscopelist-am \
+ ctags ctags-am distclean distclean-compile distclean-generic \
+ distclean-libtool distclean-tags distdir dvi dvi-am html \
+ html-am info info-am install install-am install-data \
+ install-data-am install-dvi install-dvi-am install-exec \
+ install-exec-am install-html install-html-am install-info \
+ install-info-am install-man install-pdf install-pdf-am \
+ install-pkginc_libHEADERS install-ps install-ps-am \
+ install-strip installcheck installcheck-am installdirs \
+ maintainer-clean maintainer-clean-generic mostlyclean \
+ mostlyclean-compile mostlyclean-generic mostlyclean-libtool \
+ pdf pdf-am ps ps-am tags tags-am uninstall uninstall-am \
+ uninstall-pkginc_libHEADERS
+
+.PRECIOUS: Makefile
+
+
+# We use custom rules here because we want to use flex and bison instead
+# of lex and yacc (or bison in yacc-compatibility mode). Both flex and
+# bison can handle properly naming the generated files, and it is simpler
+# and cleaner to make this rule ourselves instead of working around ylwrap
+# and yywrap's antiquated notion of what is hapenning.
+.l.c:
+ $(AM_V_GEN)$(FLEX) -o $@ $<
+
+.y.c:
+ $(AM_V_GEN)$(BISON) -o $@ $<
+
+# Bison generates both a header and a .c file. Without the following
+# dependency, anything including the header will race the bison process.
+event-filter-parser.h: event-filter-parser.c
+
+$(srcdir)/UnicodeData.txt:
+ test -f $@ || wget -O $@ https://dovecot.org/res/UnicodeData.txt
+
+$(srcdir)/unicodemap.c: $(srcdir)/unicodemap.pl $(srcdir)/UnicodeData.txt
+ perl $(srcdir)/unicodemap.pl < $(srcdir)/UnicodeData.txt > $@
+
+check-local:
+ for bin in $(test_programs); do \
+ if ! $(RUN_TEST) ./$$bin; then exit 1; fi; \
+ done
+
+# Tell versions [3.59,3.63) of GNU make to not export all variables.
+# Otherwise a system limit (for SysV at least) may be exceeded.
+.NOEXPORT:
diff --git a/src/lib/UnicodeData.txt b/src/lib/UnicodeData.txt
new file mode 100644
index 0000000..a756976
--- /dev/null
+++ b/src/lib/UnicodeData.txt
@@ -0,0 +1,30592 @@
+0000;<control>;Cc;0;BN;;;;;N;NULL;;;;
+0001;<control>;Cc;0;BN;;;;;N;START OF HEADING;;;;
+0002;<control>;Cc;0;BN;;;;;N;START OF TEXT;;;;
+0003;<control>;Cc;0;BN;;;;;N;END OF TEXT;;;;
+0004;<control>;Cc;0;BN;;;;;N;END OF TRANSMISSION;;;;
+0005;<control>;Cc;0;BN;;;;;N;ENQUIRY;;;;
+0006;<control>;Cc;0;BN;;;;;N;ACKNOWLEDGE;;;;
+0007;<control>;Cc;0;BN;;;;;N;BELL;;;;
+0008;<control>;Cc;0;BN;;;;;N;BACKSPACE;;;;
+0009;<control>;Cc;0;S;;;;;N;CHARACTER TABULATION;;;;
+000A;<control>;Cc;0;B;;;;;N;LINE FEED (LF);;;;
+000B;<control>;Cc;0;S;;;;;N;LINE TABULATION;;;;
+000C;<control>;Cc;0;WS;;;;;N;FORM FEED (FF);;;;
+000D;<control>;Cc;0;B;;;;;N;CARRIAGE RETURN (CR);;;;
+000E;<control>;Cc;0;BN;;;;;N;SHIFT OUT;;;;
+000F;<control>;Cc;0;BN;;;;;N;SHIFT IN;;;;
+0010;<control>;Cc;0;BN;;;;;N;DATA LINK ESCAPE;;;;
+0011;<control>;Cc;0;BN;;;;;N;DEVICE CONTROL ONE;;;;
+0012;<control>;Cc;0;BN;;;;;N;DEVICE CONTROL TWO;;;;
+0013;<control>;Cc;0;BN;;;;;N;DEVICE CONTROL THREE;;;;
+0014;<control>;Cc;0;BN;;;;;N;DEVICE CONTROL FOUR;;;;
+0015;<control>;Cc;0;BN;;;;;N;NEGATIVE ACKNOWLEDGE;;;;
+0016;<control>;Cc;0;BN;;;;;N;SYNCHRONOUS IDLE;;;;
+0017;<control>;Cc;0;BN;;;;;N;END OF TRANSMISSION BLOCK;;;;
+0018;<control>;Cc;0;BN;;;;;N;CANCEL;;;;
+0019;<control>;Cc;0;BN;;;;;N;END OF MEDIUM;;;;
+001A;<control>;Cc;0;BN;;;;;N;SUBSTITUTE;;;;
+001B;<control>;Cc;0;BN;;;;;N;ESCAPE;;;;
+001C;<control>;Cc;0;B;;;;;N;INFORMATION SEPARATOR FOUR;;;;
+001D;<control>;Cc;0;B;;;;;N;INFORMATION SEPARATOR THREE;;;;
+001E;<control>;Cc;0;B;;;;;N;INFORMATION SEPARATOR TWO;;;;
+001F;<control>;Cc;0;S;;;;;N;INFORMATION SEPARATOR ONE;;;;
+0020;SPACE;Zs;0;WS;;;;;N;;;;;
+0021;EXCLAMATION MARK;Po;0;ON;;;;;N;;;;;
+0022;QUOTATION MARK;Po;0;ON;;;;;N;;;;;
+0023;NUMBER SIGN;Po;0;ET;;;;;N;;;;;
+0024;DOLLAR SIGN;Sc;0;ET;;;;;N;;;;;
+0025;PERCENT SIGN;Po;0;ET;;;;;N;;;;;
+0026;AMPERSAND;Po;0;ON;;;;;N;;;;;
+0027;APOSTROPHE;Po;0;ON;;;;;N;APOSTROPHE-QUOTE;;;;
+0028;LEFT PARENTHESIS;Ps;0;ON;;;;;Y;OPENING PARENTHESIS;;;;
+0029;RIGHT PARENTHESIS;Pe;0;ON;;;;;Y;CLOSING PARENTHESIS;;;;
+002A;ASTERISK;Po;0;ON;;;;;N;;;;;
+002B;PLUS SIGN;Sm;0;ES;;;;;N;;;;;
+002C;COMMA;Po;0;CS;;;;;N;;;;;
+002D;HYPHEN-MINUS;Pd;0;ES;;;;;N;;;;;
+002E;FULL STOP;Po;0;CS;;;;;N;PERIOD;;;;
+002F;SOLIDUS;Po;0;CS;;;;;N;SLASH;;;;
+0030;DIGIT ZERO;Nd;0;EN;;0;0;0;N;;;;;
+0031;DIGIT ONE;Nd;0;EN;;1;1;1;N;;;;;
+0032;DIGIT TWO;Nd;0;EN;;2;2;2;N;;;;;
+0033;DIGIT THREE;Nd;0;EN;;3;3;3;N;;;;;
+0034;DIGIT FOUR;Nd;0;EN;;4;4;4;N;;;;;
+0035;DIGIT FIVE;Nd;0;EN;;5;5;5;N;;;;;
+0036;DIGIT SIX;Nd;0;EN;;6;6;6;N;;;;;
+0037;DIGIT SEVEN;Nd;0;EN;;7;7;7;N;;;;;
+0038;DIGIT EIGHT;Nd;0;EN;;8;8;8;N;;;;;
+0039;DIGIT NINE;Nd;0;EN;;9;9;9;N;;;;;
+003A;COLON;Po;0;CS;;;;;N;;;;;
+003B;SEMICOLON;Po;0;ON;;;;;N;;;;;
+003C;LESS-THAN SIGN;Sm;0;ON;;;;;Y;;;;;
+003D;EQUALS SIGN;Sm;0;ON;;;;;N;;;;;
+003E;GREATER-THAN SIGN;Sm;0;ON;;;;;Y;;;;;
+003F;QUESTION MARK;Po;0;ON;;;;;N;;;;;
+0040;COMMERCIAL AT;Po;0;ON;;;;;N;;;;;
+0041;LATIN CAPITAL LETTER A;Lu;0;L;;;;;N;;;;0061;
+0042;LATIN CAPITAL LETTER B;Lu;0;L;;;;;N;;;;0062;
+0043;LATIN CAPITAL LETTER C;Lu;0;L;;;;;N;;;;0063;
+0044;LATIN CAPITAL LETTER D;Lu;0;L;;;;;N;;;;0064;
+0045;LATIN CAPITAL LETTER E;Lu;0;L;;;;;N;;;;0065;
+0046;LATIN CAPITAL LETTER F;Lu;0;L;;;;;N;;;;0066;
+0047;LATIN CAPITAL LETTER G;Lu;0;L;;;;;N;;;;0067;
+0048;LATIN CAPITAL LETTER H;Lu;0;L;;;;;N;;;;0068;
+0049;LATIN CAPITAL LETTER I;Lu;0;L;;;;;N;;;;0069;
+004A;LATIN CAPITAL LETTER J;Lu;0;L;;;;;N;;;;006A;
+004B;LATIN CAPITAL LETTER K;Lu;0;L;;;;;N;;;;006B;
+004C;LATIN CAPITAL LETTER L;Lu;0;L;;;;;N;;;;006C;
+004D;LATIN CAPITAL LETTER M;Lu;0;L;;;;;N;;;;006D;
+004E;LATIN CAPITAL LETTER N;Lu;0;L;;;;;N;;;;006E;
+004F;LATIN CAPITAL LETTER O;Lu;0;L;;;;;N;;;;006F;
+0050;LATIN CAPITAL LETTER P;Lu;0;L;;;;;N;;;;0070;
+0051;LATIN CAPITAL LETTER Q;Lu;0;L;;;;;N;;;;0071;
+0052;LATIN CAPITAL LETTER R;Lu;0;L;;;;;N;;;;0072;
+0053;LATIN CAPITAL LETTER S;Lu;0;L;;;;;N;;;;0073;
+0054;LATIN CAPITAL LETTER T;Lu;0;L;;;;;N;;;;0074;
+0055;LATIN CAPITAL LETTER U;Lu;0;L;;;;;N;;;;0075;
+0056;LATIN CAPITAL LETTER V;Lu;0;L;;;;;N;;;;0076;
+0057;LATIN CAPITAL LETTER W;Lu;0;L;;;;;N;;;;0077;
+0058;LATIN CAPITAL LETTER X;Lu;0;L;;;;;N;;;;0078;
+0059;LATIN CAPITAL LETTER Y;Lu;0;L;;;;;N;;;;0079;
+005A;LATIN CAPITAL LETTER Z;Lu;0;L;;;;;N;;;;007A;
+005B;LEFT SQUARE BRACKET;Ps;0;ON;;;;;Y;OPENING SQUARE BRACKET;;;;
+005C;REVERSE SOLIDUS;Po;0;ON;;;;;N;BACKSLASH;;;;
+005D;RIGHT SQUARE BRACKET;Pe;0;ON;;;;;Y;CLOSING SQUARE BRACKET;;;;
+005E;CIRCUMFLEX ACCENT;Sk;0;ON;;;;;N;SPACING CIRCUMFLEX;;;;
+005F;LOW LINE;Pc;0;ON;;;;;N;SPACING UNDERSCORE;;;;
+0060;GRAVE ACCENT;Sk;0;ON;;;;;N;SPACING GRAVE;;;;
+0061;LATIN SMALL LETTER A;Ll;0;L;;;;;N;;;0041;;0041
+0062;LATIN SMALL LETTER B;Ll;0;L;;;;;N;;;0042;;0042
+0063;LATIN SMALL LETTER C;Ll;0;L;;;;;N;;;0043;;0043
+0064;LATIN SMALL LETTER D;Ll;0;L;;;;;N;;;0044;;0044
+0065;LATIN SMALL LETTER E;Ll;0;L;;;;;N;;;0045;;0045
+0066;LATIN SMALL LETTER F;Ll;0;L;;;;;N;;;0046;;0046
+0067;LATIN SMALL LETTER G;Ll;0;L;;;;;N;;;0047;;0047
+0068;LATIN SMALL LETTER H;Ll;0;L;;;;;N;;;0048;;0048
+0069;LATIN SMALL LETTER I;Ll;0;L;;;;;N;;;0049;;0049
+006A;LATIN SMALL LETTER J;Ll;0;L;;;;;N;;;004A;;004A
+006B;LATIN SMALL LETTER K;Ll;0;L;;;;;N;;;004B;;004B
+006C;LATIN SMALL LETTER L;Ll;0;L;;;;;N;;;004C;;004C
+006D;LATIN SMALL LETTER M;Ll;0;L;;;;;N;;;004D;;004D
+006E;LATIN SMALL LETTER N;Ll;0;L;;;;;N;;;004E;;004E
+006F;LATIN SMALL LETTER O;Ll;0;L;;;;;N;;;004F;;004F
+0070;LATIN SMALL LETTER P;Ll;0;L;;;;;N;;;0050;;0050
+0071;LATIN SMALL LETTER Q;Ll;0;L;;;;;N;;;0051;;0051
+0072;LATIN SMALL LETTER R;Ll;0;L;;;;;N;;;0052;;0052
+0073;LATIN SMALL LETTER S;Ll;0;L;;;;;N;;;0053;;0053
+0074;LATIN SMALL LETTER T;Ll;0;L;;;;;N;;;0054;;0054
+0075;LATIN SMALL LETTER U;Ll;0;L;;;;;N;;;0055;;0055
+0076;LATIN SMALL LETTER V;Ll;0;L;;;;;N;;;0056;;0056
+0077;LATIN SMALL LETTER W;Ll;0;L;;;;;N;;;0057;;0057
+0078;LATIN SMALL LETTER X;Ll;0;L;;;;;N;;;0058;;0058
+0079;LATIN SMALL LETTER Y;Ll;0;L;;;;;N;;;0059;;0059
+007A;LATIN SMALL LETTER Z;Ll;0;L;;;;;N;;;005A;;005A
+007B;LEFT CURLY BRACKET;Ps;0;ON;;;;;Y;OPENING CURLY BRACKET;;;;
+007C;VERTICAL LINE;Sm;0;ON;;;;;N;VERTICAL BAR;;;;
+007D;RIGHT CURLY BRACKET;Pe;0;ON;;;;;Y;CLOSING CURLY BRACKET;;;;
+007E;TILDE;Sm;0;ON;;;;;N;;;;;
+007F;<control>;Cc;0;BN;;;;;N;DELETE;;;;
+0080;<control>;Cc;0;BN;;;;;N;;;;;
+0081;<control>;Cc;0;BN;;;;;N;;;;;
+0082;<control>;Cc;0;BN;;;;;N;BREAK PERMITTED HERE;;;;
+0083;<control>;Cc;0;BN;;;;;N;NO BREAK HERE;;;;
+0084;<control>;Cc;0;BN;;;;;N;;;;;
+0085;<control>;Cc;0;B;;;;;N;NEXT LINE (NEL);;;;
+0086;<control>;Cc;0;BN;;;;;N;START OF SELECTED AREA;;;;
+0087;<control>;Cc;0;BN;;;;;N;END OF SELECTED AREA;;;;
+0088;<control>;Cc;0;BN;;;;;N;CHARACTER TABULATION SET;;;;
+0089;<control>;Cc;0;BN;;;;;N;CHARACTER TABULATION WITH JUSTIFICATION;;;;
+008A;<control>;Cc;0;BN;;;;;N;LINE TABULATION SET;;;;
+008B;<control>;Cc;0;BN;;;;;N;PARTIAL LINE FORWARD;;;;
+008C;<control>;Cc;0;BN;;;;;N;PARTIAL LINE BACKWARD;;;;
+008D;<control>;Cc;0;BN;;;;;N;REVERSE LINE FEED;;;;
+008E;<control>;Cc;0;BN;;;;;N;SINGLE SHIFT TWO;;;;
+008F;<control>;Cc;0;BN;;;;;N;SINGLE SHIFT THREE;;;;
+0090;<control>;Cc;0;BN;;;;;N;DEVICE CONTROL STRING;;;;
+0091;<control>;Cc;0;BN;;;;;N;PRIVATE USE ONE;;;;
+0092;<control>;Cc;0;BN;;;;;N;PRIVATE USE TWO;;;;
+0093;<control>;Cc;0;BN;;;;;N;SET TRANSMIT STATE;;;;
+0094;<control>;Cc;0;BN;;;;;N;CANCEL CHARACTER;;;;
+0095;<control>;Cc;0;BN;;;;;N;MESSAGE WAITING;;;;
+0096;<control>;Cc;0;BN;;;;;N;START OF GUARDED AREA;;;;
+0097;<control>;Cc;0;BN;;;;;N;END OF GUARDED AREA;;;;
+0098;<control>;Cc;0;BN;;;;;N;START OF STRING;;;;
+0099;<control>;Cc;0;BN;;;;;N;;;;;
+009A;<control>;Cc;0;BN;;;;;N;SINGLE CHARACTER INTRODUCER;;;;
+009B;<control>;Cc;0;BN;;;;;N;CONTROL SEQUENCE INTRODUCER;;;;
+009C;<control>;Cc;0;BN;;;;;N;STRING TERMINATOR;;;;
+009D;<control>;Cc;0;BN;;;;;N;OPERATING SYSTEM COMMAND;;;;
+009E;<control>;Cc;0;BN;;;;;N;PRIVACY MESSAGE;;;;
+009F;<control>;Cc;0;BN;;;;;N;APPLICATION PROGRAM COMMAND;;;;
+00A0;NO-BREAK SPACE;Zs;0;CS;<noBreak> 0020;;;;N;NON-BREAKING SPACE;;;;
+00A1;INVERTED EXCLAMATION MARK;Po;0;ON;;;;;N;;;;;
+00A2;CENT SIGN;Sc;0;ET;;;;;N;;;;;
+00A3;POUND SIGN;Sc;0;ET;;;;;N;;;;;
+00A4;CURRENCY SIGN;Sc;0;ET;;;;;N;;;;;
+00A5;YEN SIGN;Sc;0;ET;;;;;N;;;;;
+00A6;BROKEN BAR;So;0;ON;;;;;N;BROKEN VERTICAL BAR;;;;
+00A7;SECTION SIGN;Po;0;ON;;;;;N;;;;;
+00A8;DIAERESIS;Sk;0;ON;<compat> 0020 0308;;;;N;SPACING DIAERESIS;;;;
+00A9;COPYRIGHT SIGN;So;0;ON;;;;;N;;;;;
+00AA;FEMININE ORDINAL INDICATOR;Lo;0;L;<super> 0061;;;;N;;;;;
+00AB;LEFT-POINTING DOUBLE ANGLE QUOTATION MARK;Pi;0;ON;;;;;Y;LEFT POINTING GUILLEMET;;;;
+00AC;NOT SIGN;Sm;0;ON;;;;;N;;;;;
+00AD;SOFT HYPHEN;Cf;0;BN;;;;;N;;;;;
+00AE;REGISTERED SIGN;So;0;ON;;;;;N;REGISTERED TRADE MARK SIGN;;;;
+00AF;MACRON;Sk;0;ON;<compat> 0020 0304;;;;N;SPACING MACRON;;;;
+00B0;DEGREE SIGN;So;0;ET;;;;;N;;;;;
+00B1;PLUS-MINUS SIGN;Sm;0;ET;;;;;N;PLUS-OR-MINUS SIGN;;;;
+00B2;SUPERSCRIPT TWO;No;0;EN;<super> 0032;;2;2;N;SUPERSCRIPT DIGIT TWO;;;;
+00B3;SUPERSCRIPT THREE;No;0;EN;<super> 0033;;3;3;N;SUPERSCRIPT DIGIT THREE;;;;
+00B4;ACUTE ACCENT;Sk;0;ON;<compat> 0020 0301;;;;N;SPACING ACUTE;;;;
+00B5;MICRO SIGN;Ll;0;L;<compat> 03BC;;;;N;;;039C;;039C
+00B6;PILCROW SIGN;Po;0;ON;;;;;N;PARAGRAPH SIGN;;;;
+00B7;MIDDLE DOT;Po;0;ON;;;;;N;;;;;
+00B8;CEDILLA;Sk;0;ON;<compat> 0020 0327;;;;N;SPACING CEDILLA;;;;
+00B9;SUPERSCRIPT ONE;No;0;EN;<super> 0031;;1;1;N;SUPERSCRIPT DIGIT ONE;;;;
+00BA;MASCULINE ORDINAL INDICATOR;Lo;0;L;<super> 006F;;;;N;;;;;
+00BB;RIGHT-POINTING DOUBLE ANGLE QUOTATION MARK;Pf;0;ON;;;;;Y;RIGHT POINTING GUILLEMET;;;;
+00BC;VULGAR FRACTION ONE QUARTER;No;0;ON;<fraction> 0031 2044 0034;;;1/4;N;FRACTION ONE QUARTER;;;;
+00BD;VULGAR FRACTION ONE HALF;No;0;ON;<fraction> 0031 2044 0032;;;1/2;N;FRACTION ONE HALF;;;;
+00BE;VULGAR FRACTION THREE QUARTERS;No;0;ON;<fraction> 0033 2044 0034;;;3/4;N;FRACTION THREE QUARTERS;;;;
+00BF;INVERTED QUESTION MARK;Po;0;ON;;;;;N;;;;;
+00C0;LATIN CAPITAL LETTER A WITH GRAVE;Lu;0;L;0041 0300;;;;N;LATIN CAPITAL LETTER A GRAVE;;;00E0;
+00C1;LATIN CAPITAL LETTER A WITH ACUTE;Lu;0;L;0041 0301;;;;N;LATIN CAPITAL LETTER A ACUTE;;;00E1;
+00C2;LATIN CAPITAL LETTER A WITH CIRCUMFLEX;Lu;0;L;0041 0302;;;;N;LATIN CAPITAL LETTER A CIRCUMFLEX;;;00E2;
+00C3;LATIN CAPITAL LETTER A WITH TILDE;Lu;0;L;0041 0303;;;;N;LATIN CAPITAL LETTER A TILDE;;;00E3;
+00C4;LATIN CAPITAL LETTER A WITH DIAERESIS;Lu;0;L;0041 0308;;;;N;LATIN CAPITAL LETTER A DIAERESIS;;;00E4;
+00C5;LATIN CAPITAL LETTER A WITH RING ABOVE;Lu;0;L;0041 030A;;;;N;LATIN CAPITAL LETTER A RING;;;00E5;
+00C6;LATIN CAPITAL LETTER AE;Lu;0;L;;;;;N;LATIN CAPITAL LETTER A E;;;00E6;
+00C7;LATIN CAPITAL LETTER C WITH CEDILLA;Lu;0;L;0043 0327;;;;N;LATIN CAPITAL LETTER C CEDILLA;;;00E7;
+00C8;LATIN CAPITAL LETTER E WITH GRAVE;Lu;0;L;0045 0300;;;;N;LATIN CAPITAL LETTER E GRAVE;;;00E8;
+00C9;LATIN CAPITAL LETTER E WITH ACUTE;Lu;0;L;0045 0301;;;;N;LATIN CAPITAL LETTER E ACUTE;;;00E9;
+00CA;LATIN CAPITAL LETTER E WITH CIRCUMFLEX;Lu;0;L;0045 0302;;;;N;LATIN CAPITAL LETTER E CIRCUMFLEX;;;00EA;
+00CB;LATIN CAPITAL LETTER E WITH DIAERESIS;Lu;0;L;0045 0308;;;;N;LATIN CAPITAL LETTER E DIAERESIS;;;00EB;
+00CC;LATIN CAPITAL LETTER I WITH GRAVE;Lu;0;L;0049 0300;;;;N;LATIN CAPITAL LETTER I GRAVE;;;00EC;
+00CD;LATIN CAPITAL LETTER I WITH ACUTE;Lu;0;L;0049 0301;;;;N;LATIN CAPITAL LETTER I ACUTE;;;00ED;
+00CE;LATIN CAPITAL LETTER I WITH CIRCUMFLEX;Lu;0;L;0049 0302;;;;N;LATIN CAPITAL LETTER I CIRCUMFLEX;;;00EE;
+00CF;LATIN CAPITAL LETTER I WITH DIAERESIS;Lu;0;L;0049 0308;;;;N;LATIN CAPITAL LETTER I DIAERESIS;;;00EF;
+00D0;LATIN CAPITAL LETTER ETH;Lu;0;L;;;;;N;;;;00F0;
+00D1;LATIN CAPITAL LETTER N WITH TILDE;Lu;0;L;004E 0303;;;;N;LATIN CAPITAL LETTER N TILDE;;;00F1;
+00D2;LATIN CAPITAL LETTER O WITH GRAVE;Lu;0;L;004F 0300;;;;N;LATIN CAPITAL LETTER O GRAVE;;;00F2;
+00D3;LATIN CAPITAL LETTER O WITH ACUTE;Lu;0;L;004F 0301;;;;N;LATIN CAPITAL LETTER O ACUTE;;;00F3;
+00D4;LATIN CAPITAL LETTER O WITH CIRCUMFLEX;Lu;0;L;004F 0302;;;;N;LATIN CAPITAL LETTER O CIRCUMFLEX;;;00F4;
+00D5;LATIN CAPITAL LETTER O WITH TILDE;Lu;0;L;004F 0303;;;;N;LATIN CAPITAL LETTER O TILDE;;;00F5;
+00D6;LATIN CAPITAL LETTER O WITH DIAERESIS;Lu;0;L;004F 0308;;;;N;LATIN CAPITAL LETTER O DIAERESIS;;;00F6;
+00D7;MULTIPLICATION SIGN;Sm;0;ON;;;;;N;;;;;
+00D8;LATIN CAPITAL LETTER O WITH STROKE;Lu;0;L;;;;;N;LATIN CAPITAL LETTER O SLASH;;;00F8;
+00D9;LATIN CAPITAL LETTER U WITH GRAVE;Lu;0;L;0055 0300;;;;N;LATIN CAPITAL LETTER U GRAVE;;;00F9;
+00DA;LATIN CAPITAL LETTER U WITH ACUTE;Lu;0;L;0055 0301;;;;N;LATIN CAPITAL LETTER U ACUTE;;;00FA;
+00DB;LATIN CAPITAL LETTER U WITH CIRCUMFLEX;Lu;0;L;0055 0302;;;;N;LATIN CAPITAL LETTER U CIRCUMFLEX;;;00FB;
+00DC;LATIN CAPITAL LETTER U WITH DIAERESIS;Lu;0;L;0055 0308;;;;N;LATIN CAPITAL LETTER U DIAERESIS;;;00FC;
+00DD;LATIN CAPITAL LETTER Y WITH ACUTE;Lu;0;L;0059 0301;;;;N;LATIN CAPITAL LETTER Y ACUTE;;;00FD;
+00DE;LATIN CAPITAL LETTER THORN;Lu;0;L;;;;;N;;;;00FE;
+00DF;LATIN SMALL LETTER SHARP S;Ll;0;L;;;;;N;;;;;
+00E0;LATIN SMALL LETTER A WITH GRAVE;Ll;0;L;0061 0300;;;;N;LATIN SMALL LETTER A GRAVE;;00C0;;00C0
+00E1;LATIN SMALL LETTER A WITH ACUTE;Ll;0;L;0061 0301;;;;N;LATIN SMALL LETTER A ACUTE;;00C1;;00C1
+00E2;LATIN SMALL LETTER A WITH CIRCUMFLEX;Ll;0;L;0061 0302;;;;N;LATIN SMALL LETTER A CIRCUMFLEX;;00C2;;00C2
+00E3;LATIN SMALL LETTER A WITH TILDE;Ll;0;L;0061 0303;;;;N;LATIN SMALL LETTER A TILDE;;00C3;;00C3
+00E4;LATIN SMALL LETTER A WITH DIAERESIS;Ll;0;L;0061 0308;;;;N;LATIN SMALL LETTER A DIAERESIS;;00C4;;00C4
+00E5;LATIN SMALL LETTER A WITH RING ABOVE;Ll;0;L;0061 030A;;;;N;LATIN SMALL LETTER A RING;;00C5;;00C5
+00E6;LATIN SMALL LETTER AE;Ll;0;L;;;;;N;LATIN SMALL LETTER A E;;00C6;;00C6
+00E7;LATIN SMALL LETTER C WITH CEDILLA;Ll;0;L;0063 0327;;;;N;LATIN SMALL LETTER C CEDILLA;;00C7;;00C7
+00E8;LATIN SMALL LETTER E WITH GRAVE;Ll;0;L;0065 0300;;;;N;LATIN SMALL LETTER E GRAVE;;00C8;;00C8
+00E9;LATIN SMALL LETTER E WITH ACUTE;Ll;0;L;0065 0301;;;;N;LATIN SMALL LETTER E ACUTE;;00C9;;00C9
+00EA;LATIN SMALL LETTER E WITH CIRCUMFLEX;Ll;0;L;0065 0302;;;;N;LATIN SMALL LETTER E CIRCUMFLEX;;00CA;;00CA
+00EB;LATIN SMALL LETTER E WITH DIAERESIS;Ll;0;L;0065 0308;;;;N;LATIN SMALL LETTER E DIAERESIS;;00CB;;00CB
+00EC;LATIN SMALL LETTER I WITH GRAVE;Ll;0;L;0069 0300;;;;N;LATIN SMALL LETTER I GRAVE;;00CC;;00CC
+00ED;LATIN SMALL LETTER I WITH ACUTE;Ll;0;L;0069 0301;;;;N;LATIN SMALL LETTER I ACUTE;;00CD;;00CD
+00EE;LATIN SMALL LETTER I WITH CIRCUMFLEX;Ll;0;L;0069 0302;;;;N;LATIN SMALL LETTER I CIRCUMFLEX;;00CE;;00CE
+00EF;LATIN SMALL LETTER I WITH DIAERESIS;Ll;0;L;0069 0308;;;;N;LATIN SMALL LETTER I DIAERESIS;;00CF;;00CF
+00F0;LATIN SMALL LETTER ETH;Ll;0;L;;;;;N;;;00D0;;00D0
+00F1;LATIN SMALL LETTER N WITH TILDE;Ll;0;L;006E 0303;;;;N;LATIN SMALL LETTER N TILDE;;00D1;;00D1
+00F2;LATIN SMALL LETTER O WITH GRAVE;Ll;0;L;006F 0300;;;;N;LATIN SMALL LETTER O GRAVE;;00D2;;00D2
+00F3;LATIN SMALL LETTER O WITH ACUTE;Ll;0;L;006F 0301;;;;N;LATIN SMALL LETTER O ACUTE;;00D3;;00D3
+00F4;LATIN SMALL LETTER O WITH CIRCUMFLEX;Ll;0;L;006F 0302;;;;N;LATIN SMALL LETTER O CIRCUMFLEX;;00D4;;00D4
+00F5;LATIN SMALL LETTER O WITH TILDE;Ll;0;L;006F 0303;;;;N;LATIN SMALL LETTER O TILDE;;00D5;;00D5
+00F6;LATIN SMALL LETTER O WITH DIAERESIS;Ll;0;L;006F 0308;;;;N;LATIN SMALL LETTER O DIAERESIS;;00D6;;00D6
+00F7;DIVISION SIGN;Sm;0;ON;;;;;N;;;;;
+00F8;LATIN SMALL LETTER O WITH STROKE;Ll;0;L;;;;;N;LATIN SMALL LETTER O SLASH;;00D8;;00D8
+00F9;LATIN SMALL LETTER U WITH GRAVE;Ll;0;L;0075 0300;;;;N;LATIN SMALL LETTER U GRAVE;;00D9;;00D9
+00FA;LATIN SMALL LETTER U WITH ACUTE;Ll;0;L;0075 0301;;;;N;LATIN SMALL LETTER U ACUTE;;00DA;;00DA
+00FB;LATIN SMALL LETTER U WITH CIRCUMFLEX;Ll;0;L;0075 0302;;;;N;LATIN SMALL LETTER U CIRCUMFLEX;;00DB;;00DB
+00FC;LATIN SMALL LETTER U WITH DIAERESIS;Ll;0;L;0075 0308;;;;N;LATIN SMALL LETTER U DIAERESIS;;00DC;;00DC
+00FD;LATIN SMALL LETTER Y WITH ACUTE;Ll;0;L;0079 0301;;;;N;LATIN SMALL LETTER Y ACUTE;;00DD;;00DD
+00FE;LATIN SMALL LETTER THORN;Ll;0;L;;;;;N;;;00DE;;00DE
+00FF;LATIN SMALL LETTER Y WITH DIAERESIS;Ll;0;L;0079 0308;;;;N;LATIN SMALL LETTER Y DIAERESIS;;0178;;0178
+0100;LATIN CAPITAL LETTER A WITH MACRON;Lu;0;L;0041 0304;;;;N;LATIN CAPITAL LETTER A MACRON;;;0101;
+0101;LATIN SMALL LETTER A WITH MACRON;Ll;0;L;0061 0304;;;;N;LATIN SMALL LETTER A MACRON;;0100;;0100
+0102;LATIN CAPITAL LETTER A WITH BREVE;Lu;0;L;0041 0306;;;;N;LATIN CAPITAL LETTER A BREVE;;;0103;
+0103;LATIN SMALL LETTER A WITH BREVE;Ll;0;L;0061 0306;;;;N;LATIN SMALL LETTER A BREVE;;0102;;0102
+0104;LATIN CAPITAL LETTER A WITH OGONEK;Lu;0;L;0041 0328;;;;N;LATIN CAPITAL LETTER A OGONEK;;;0105;
+0105;LATIN SMALL LETTER A WITH OGONEK;Ll;0;L;0061 0328;;;;N;LATIN SMALL LETTER A OGONEK;;0104;;0104
+0106;LATIN CAPITAL LETTER C WITH ACUTE;Lu;0;L;0043 0301;;;;N;LATIN CAPITAL LETTER C ACUTE;;;0107;
+0107;LATIN SMALL LETTER C WITH ACUTE;Ll;0;L;0063 0301;;;;N;LATIN SMALL LETTER C ACUTE;;0106;;0106
+0108;LATIN CAPITAL LETTER C WITH CIRCUMFLEX;Lu;0;L;0043 0302;;;;N;LATIN CAPITAL LETTER C CIRCUMFLEX;;;0109;
+0109;LATIN SMALL LETTER C WITH CIRCUMFLEX;Ll;0;L;0063 0302;;;;N;LATIN SMALL LETTER C CIRCUMFLEX;;0108;;0108
+010A;LATIN CAPITAL LETTER C WITH DOT ABOVE;Lu;0;L;0043 0307;;;;N;LATIN CAPITAL LETTER C DOT;;;010B;
+010B;LATIN SMALL LETTER C WITH DOT ABOVE;Ll;0;L;0063 0307;;;;N;LATIN SMALL LETTER C DOT;;010A;;010A
+010C;LATIN CAPITAL LETTER C WITH CARON;Lu;0;L;0043 030C;;;;N;LATIN CAPITAL LETTER C HACEK;;;010D;
+010D;LATIN SMALL LETTER C WITH CARON;Ll;0;L;0063 030C;;;;N;LATIN SMALL LETTER C HACEK;;010C;;010C
+010E;LATIN CAPITAL LETTER D WITH CARON;Lu;0;L;0044 030C;;;;N;LATIN CAPITAL LETTER D HACEK;;;010F;
+010F;LATIN SMALL LETTER D WITH CARON;Ll;0;L;0064 030C;;;;N;LATIN SMALL LETTER D HACEK;;010E;;010E
+0110;LATIN CAPITAL LETTER D WITH STROKE;Lu;0;L;;;;;N;LATIN CAPITAL LETTER D BAR;;;0111;
+0111;LATIN SMALL LETTER D WITH STROKE;Ll;0;L;;;;;N;LATIN SMALL LETTER D BAR;;0110;;0110
+0112;LATIN CAPITAL LETTER E WITH MACRON;Lu;0;L;0045 0304;;;;N;LATIN CAPITAL LETTER E MACRON;;;0113;
+0113;LATIN SMALL LETTER E WITH MACRON;Ll;0;L;0065 0304;;;;N;LATIN SMALL LETTER E MACRON;;0112;;0112
+0114;LATIN CAPITAL LETTER E WITH BREVE;Lu;0;L;0045 0306;;;;N;LATIN CAPITAL LETTER E BREVE;;;0115;
+0115;LATIN SMALL LETTER E WITH BREVE;Ll;0;L;0065 0306;;;;N;LATIN SMALL LETTER E BREVE;;0114;;0114
+0116;LATIN CAPITAL LETTER E WITH DOT ABOVE;Lu;0;L;0045 0307;;;;N;LATIN CAPITAL LETTER E DOT;;;0117;
+0117;LATIN SMALL LETTER E WITH DOT ABOVE;Ll;0;L;0065 0307;;;;N;LATIN SMALL LETTER E DOT;;0116;;0116
+0118;LATIN CAPITAL LETTER E WITH OGONEK;Lu;0;L;0045 0328;;;;N;LATIN CAPITAL LETTER E OGONEK;;;0119;
+0119;LATIN SMALL LETTER E WITH OGONEK;Ll;0;L;0065 0328;;;;N;LATIN SMALL LETTER E OGONEK;;0118;;0118
+011A;LATIN CAPITAL LETTER E WITH CARON;Lu;0;L;0045 030C;;;;N;LATIN CAPITAL LETTER E HACEK;;;011B;
+011B;LATIN SMALL LETTER E WITH CARON;Ll;0;L;0065 030C;;;;N;LATIN SMALL LETTER E HACEK;;011A;;011A
+011C;LATIN CAPITAL LETTER G WITH CIRCUMFLEX;Lu;0;L;0047 0302;;;;N;LATIN CAPITAL LETTER G CIRCUMFLEX;;;011D;
+011D;LATIN SMALL LETTER G WITH CIRCUMFLEX;Ll;0;L;0067 0302;;;;N;LATIN SMALL LETTER G CIRCUMFLEX;;011C;;011C
+011E;LATIN CAPITAL LETTER G WITH BREVE;Lu;0;L;0047 0306;;;;N;LATIN CAPITAL LETTER G BREVE;;;011F;
+011F;LATIN SMALL LETTER G WITH BREVE;Ll;0;L;0067 0306;;;;N;LATIN SMALL LETTER G BREVE;;011E;;011E
+0120;LATIN CAPITAL LETTER G WITH DOT ABOVE;Lu;0;L;0047 0307;;;;N;LATIN CAPITAL LETTER G DOT;;;0121;
+0121;LATIN SMALL LETTER G WITH DOT ABOVE;Ll;0;L;0067 0307;;;;N;LATIN SMALL LETTER G DOT;;0120;;0120
+0122;LATIN CAPITAL LETTER G WITH CEDILLA;Lu;0;L;0047 0327;;;;N;LATIN CAPITAL LETTER G CEDILLA;;;0123;
+0123;LATIN SMALL LETTER G WITH CEDILLA;Ll;0;L;0067 0327;;;;N;LATIN SMALL LETTER G CEDILLA;;0122;;0122
+0124;LATIN CAPITAL LETTER H WITH CIRCUMFLEX;Lu;0;L;0048 0302;;;;N;LATIN CAPITAL LETTER H CIRCUMFLEX;;;0125;
+0125;LATIN SMALL LETTER H WITH CIRCUMFLEX;Ll;0;L;0068 0302;;;;N;LATIN SMALL LETTER H CIRCUMFLEX;;0124;;0124
+0126;LATIN CAPITAL LETTER H WITH STROKE;Lu;0;L;;;;;N;LATIN CAPITAL LETTER H BAR;;;0127;
+0127;LATIN SMALL LETTER H WITH STROKE;Ll;0;L;;;;;N;LATIN SMALL LETTER H BAR;;0126;;0126
+0128;LATIN CAPITAL LETTER I WITH TILDE;Lu;0;L;0049 0303;;;;N;LATIN CAPITAL LETTER I TILDE;;;0129;
+0129;LATIN SMALL LETTER I WITH TILDE;Ll;0;L;0069 0303;;;;N;LATIN SMALL LETTER I TILDE;;0128;;0128
+012A;LATIN CAPITAL LETTER I WITH MACRON;Lu;0;L;0049 0304;;;;N;LATIN CAPITAL LETTER I MACRON;;;012B;
+012B;LATIN SMALL LETTER I WITH MACRON;Ll;0;L;0069 0304;;;;N;LATIN SMALL LETTER I MACRON;;012A;;012A
+012C;LATIN CAPITAL LETTER I WITH BREVE;Lu;0;L;0049 0306;;;;N;LATIN CAPITAL LETTER I BREVE;;;012D;
+012D;LATIN SMALL LETTER I WITH BREVE;Ll;0;L;0069 0306;;;;N;LATIN SMALL LETTER I BREVE;;012C;;012C
+012E;LATIN CAPITAL LETTER I WITH OGONEK;Lu;0;L;0049 0328;;;;N;LATIN CAPITAL LETTER I OGONEK;;;012F;
+012F;LATIN SMALL LETTER I WITH OGONEK;Ll;0;L;0069 0328;;;;N;LATIN SMALL LETTER I OGONEK;;012E;;012E
+0130;LATIN CAPITAL LETTER I WITH DOT ABOVE;Lu;0;L;0049 0307;;;;N;LATIN CAPITAL LETTER I DOT;;;0069;
+0131;LATIN SMALL LETTER DOTLESS I;Ll;0;L;;;;;N;;;0049;;0049
+0132;LATIN CAPITAL LIGATURE IJ;Lu;0;L;<compat> 0049 004A;;;;N;LATIN CAPITAL LETTER I J;;;0133;
+0133;LATIN SMALL LIGATURE IJ;Ll;0;L;<compat> 0069 006A;;;;N;LATIN SMALL LETTER I J;;0132;;0132
+0134;LATIN CAPITAL LETTER J WITH CIRCUMFLEX;Lu;0;L;004A 0302;;;;N;LATIN CAPITAL LETTER J CIRCUMFLEX;;;0135;
+0135;LATIN SMALL LETTER J WITH CIRCUMFLEX;Ll;0;L;006A 0302;;;;N;LATIN SMALL LETTER J CIRCUMFLEX;;0134;;0134
+0136;LATIN CAPITAL LETTER K WITH CEDILLA;Lu;0;L;004B 0327;;;;N;LATIN CAPITAL LETTER K CEDILLA;;;0137;
+0137;LATIN SMALL LETTER K WITH CEDILLA;Ll;0;L;006B 0327;;;;N;LATIN SMALL LETTER K CEDILLA;;0136;;0136
+0138;LATIN SMALL LETTER KRA;Ll;0;L;;;;;N;;;;;
+0139;LATIN CAPITAL LETTER L WITH ACUTE;Lu;0;L;004C 0301;;;;N;LATIN CAPITAL LETTER L ACUTE;;;013A;
+013A;LATIN SMALL LETTER L WITH ACUTE;Ll;0;L;006C 0301;;;;N;LATIN SMALL LETTER L ACUTE;;0139;;0139
+013B;LATIN CAPITAL LETTER L WITH CEDILLA;Lu;0;L;004C 0327;;;;N;LATIN CAPITAL LETTER L CEDILLA;;;013C;
+013C;LATIN SMALL LETTER L WITH CEDILLA;Ll;0;L;006C 0327;;;;N;LATIN SMALL LETTER L CEDILLA;;013B;;013B
+013D;LATIN CAPITAL LETTER L WITH CARON;Lu;0;L;004C 030C;;;;N;LATIN CAPITAL LETTER L HACEK;;;013E;
+013E;LATIN SMALL LETTER L WITH CARON;Ll;0;L;006C 030C;;;;N;LATIN SMALL LETTER L HACEK;;013D;;013D
+013F;LATIN CAPITAL LETTER L WITH MIDDLE DOT;Lu;0;L;<compat> 004C 00B7;;;;N;;;;0140;
+0140;LATIN SMALL LETTER L WITH MIDDLE DOT;Ll;0;L;<compat> 006C 00B7;;;;N;;;013F;;013F
+0141;LATIN CAPITAL LETTER L WITH STROKE;Lu;0;L;;;;;N;LATIN CAPITAL LETTER L SLASH;;;0142;
+0142;LATIN SMALL LETTER L WITH STROKE;Ll;0;L;;;;;N;LATIN SMALL LETTER L SLASH;;0141;;0141
+0143;LATIN CAPITAL LETTER N WITH ACUTE;Lu;0;L;004E 0301;;;;N;LATIN CAPITAL LETTER N ACUTE;;;0144;
+0144;LATIN SMALL LETTER N WITH ACUTE;Ll;0;L;006E 0301;;;;N;LATIN SMALL LETTER N ACUTE;;0143;;0143
+0145;LATIN CAPITAL LETTER N WITH CEDILLA;Lu;0;L;004E 0327;;;;N;LATIN CAPITAL LETTER N CEDILLA;;;0146;
+0146;LATIN SMALL LETTER N WITH CEDILLA;Ll;0;L;006E 0327;;;;N;LATIN SMALL LETTER N CEDILLA;;0145;;0145
+0147;LATIN CAPITAL LETTER N WITH CARON;Lu;0;L;004E 030C;;;;N;LATIN CAPITAL LETTER N HACEK;;;0148;
+0148;LATIN SMALL LETTER N WITH CARON;Ll;0;L;006E 030C;;;;N;LATIN SMALL LETTER N HACEK;;0147;;0147
+0149;LATIN SMALL LETTER N PRECEDED BY APOSTROPHE;Ll;0;L;<compat> 02BC 006E;;;;N;LATIN SMALL LETTER APOSTROPHE N;;;;
+014A;LATIN CAPITAL LETTER ENG;Lu;0;L;;;;;N;;;;014B;
+014B;LATIN SMALL LETTER ENG;Ll;0;L;;;;;N;;;014A;;014A
+014C;LATIN CAPITAL LETTER O WITH MACRON;Lu;0;L;004F 0304;;;;N;LATIN CAPITAL LETTER O MACRON;;;014D;
+014D;LATIN SMALL LETTER O WITH MACRON;Ll;0;L;006F 0304;;;;N;LATIN SMALL LETTER O MACRON;;014C;;014C
+014E;LATIN CAPITAL LETTER O WITH BREVE;Lu;0;L;004F 0306;;;;N;LATIN CAPITAL LETTER O BREVE;;;014F;
+014F;LATIN SMALL LETTER O WITH BREVE;Ll;0;L;006F 0306;;;;N;LATIN SMALL LETTER O BREVE;;014E;;014E
+0150;LATIN CAPITAL LETTER O WITH DOUBLE ACUTE;Lu;0;L;004F 030B;;;;N;LATIN CAPITAL LETTER O DOUBLE ACUTE;;;0151;
+0151;LATIN SMALL LETTER O WITH DOUBLE ACUTE;Ll;0;L;006F 030B;;;;N;LATIN SMALL LETTER O DOUBLE ACUTE;;0150;;0150
+0152;LATIN CAPITAL LIGATURE OE;Lu;0;L;;;;;N;LATIN CAPITAL LETTER O E;;;0153;
+0153;LATIN SMALL LIGATURE OE;Ll;0;L;;;;;N;LATIN SMALL LETTER O E;;0152;;0152
+0154;LATIN CAPITAL LETTER R WITH ACUTE;Lu;0;L;0052 0301;;;;N;LATIN CAPITAL LETTER R ACUTE;;;0155;
+0155;LATIN SMALL LETTER R WITH ACUTE;Ll;0;L;0072 0301;;;;N;LATIN SMALL LETTER R ACUTE;;0154;;0154
+0156;LATIN CAPITAL LETTER R WITH CEDILLA;Lu;0;L;0052 0327;;;;N;LATIN CAPITAL LETTER R CEDILLA;;;0157;
+0157;LATIN SMALL LETTER R WITH CEDILLA;Ll;0;L;0072 0327;;;;N;LATIN SMALL LETTER R CEDILLA;;0156;;0156
+0158;LATIN CAPITAL LETTER R WITH CARON;Lu;0;L;0052 030C;;;;N;LATIN CAPITAL LETTER R HACEK;;;0159;
+0159;LATIN SMALL LETTER R WITH CARON;Ll;0;L;0072 030C;;;;N;LATIN SMALL LETTER R HACEK;;0158;;0158
+015A;LATIN CAPITAL LETTER S WITH ACUTE;Lu;0;L;0053 0301;;;;N;LATIN CAPITAL LETTER S ACUTE;;;015B;
+015B;LATIN SMALL LETTER S WITH ACUTE;Ll;0;L;0073 0301;;;;N;LATIN SMALL LETTER S ACUTE;;015A;;015A
+015C;LATIN CAPITAL LETTER S WITH CIRCUMFLEX;Lu;0;L;0053 0302;;;;N;LATIN CAPITAL LETTER S CIRCUMFLEX;;;015D;
+015D;LATIN SMALL LETTER S WITH CIRCUMFLEX;Ll;0;L;0073 0302;;;;N;LATIN SMALL LETTER S CIRCUMFLEX;;015C;;015C
+015E;LATIN CAPITAL LETTER S WITH CEDILLA;Lu;0;L;0053 0327;;;;N;LATIN CAPITAL LETTER S CEDILLA;;;015F;
+015F;LATIN SMALL LETTER S WITH CEDILLA;Ll;0;L;0073 0327;;;;N;LATIN SMALL LETTER S CEDILLA;;015E;;015E
+0160;LATIN CAPITAL LETTER S WITH CARON;Lu;0;L;0053 030C;;;;N;LATIN CAPITAL LETTER S HACEK;;;0161;
+0161;LATIN SMALL LETTER S WITH CARON;Ll;0;L;0073 030C;;;;N;LATIN SMALL LETTER S HACEK;;0160;;0160
+0162;LATIN CAPITAL LETTER T WITH CEDILLA;Lu;0;L;0054 0327;;;;N;LATIN CAPITAL LETTER T CEDILLA;;;0163;
+0163;LATIN SMALL LETTER T WITH CEDILLA;Ll;0;L;0074 0327;;;;N;LATIN SMALL LETTER T CEDILLA;;0162;;0162
+0164;LATIN CAPITAL LETTER T WITH CARON;Lu;0;L;0054 030C;;;;N;LATIN CAPITAL LETTER T HACEK;;;0165;
+0165;LATIN SMALL LETTER T WITH CARON;Ll;0;L;0074 030C;;;;N;LATIN SMALL LETTER T HACEK;;0164;;0164
+0166;LATIN CAPITAL LETTER T WITH STROKE;Lu;0;L;;;;;N;LATIN CAPITAL LETTER T BAR;;;0167;
+0167;LATIN SMALL LETTER T WITH STROKE;Ll;0;L;;;;;N;LATIN SMALL LETTER T BAR;;0166;;0166
+0168;LATIN CAPITAL LETTER U WITH TILDE;Lu;0;L;0055 0303;;;;N;LATIN CAPITAL LETTER U TILDE;;;0169;
+0169;LATIN SMALL LETTER U WITH TILDE;Ll;0;L;0075 0303;;;;N;LATIN SMALL LETTER U TILDE;;0168;;0168
+016A;LATIN CAPITAL LETTER U WITH MACRON;Lu;0;L;0055 0304;;;;N;LATIN CAPITAL LETTER U MACRON;;;016B;
+016B;LATIN SMALL LETTER U WITH MACRON;Ll;0;L;0075 0304;;;;N;LATIN SMALL LETTER U MACRON;;016A;;016A
+016C;LATIN CAPITAL LETTER U WITH BREVE;Lu;0;L;0055 0306;;;;N;LATIN CAPITAL LETTER U BREVE;;;016D;
+016D;LATIN SMALL LETTER U WITH BREVE;Ll;0;L;0075 0306;;;;N;LATIN SMALL LETTER U BREVE;;016C;;016C
+016E;LATIN CAPITAL LETTER U WITH RING ABOVE;Lu;0;L;0055 030A;;;;N;LATIN CAPITAL LETTER U RING;;;016F;
+016F;LATIN SMALL LETTER U WITH RING ABOVE;Ll;0;L;0075 030A;;;;N;LATIN SMALL LETTER U RING;;016E;;016E
+0170;LATIN CAPITAL LETTER U WITH DOUBLE ACUTE;Lu;0;L;0055 030B;;;;N;LATIN CAPITAL LETTER U DOUBLE ACUTE;;;0171;
+0171;LATIN SMALL LETTER U WITH DOUBLE ACUTE;Ll;0;L;0075 030B;;;;N;LATIN SMALL LETTER U DOUBLE ACUTE;;0170;;0170
+0172;LATIN CAPITAL LETTER U WITH OGONEK;Lu;0;L;0055 0328;;;;N;LATIN CAPITAL LETTER U OGONEK;;;0173;
+0173;LATIN SMALL LETTER U WITH OGONEK;Ll;0;L;0075 0328;;;;N;LATIN SMALL LETTER U OGONEK;;0172;;0172
+0174;LATIN CAPITAL LETTER W WITH CIRCUMFLEX;Lu;0;L;0057 0302;;;;N;LATIN CAPITAL LETTER W CIRCUMFLEX;;;0175;
+0175;LATIN SMALL LETTER W WITH CIRCUMFLEX;Ll;0;L;0077 0302;;;;N;LATIN SMALL LETTER W CIRCUMFLEX;;0174;;0174
+0176;LATIN CAPITAL LETTER Y WITH CIRCUMFLEX;Lu;0;L;0059 0302;;;;N;LATIN CAPITAL LETTER Y CIRCUMFLEX;;;0177;
+0177;LATIN SMALL LETTER Y WITH CIRCUMFLEX;Ll;0;L;0079 0302;;;;N;LATIN SMALL LETTER Y CIRCUMFLEX;;0176;;0176
+0178;LATIN CAPITAL LETTER Y WITH DIAERESIS;Lu;0;L;0059 0308;;;;N;LATIN CAPITAL LETTER Y DIAERESIS;;;00FF;
+0179;LATIN CAPITAL LETTER Z WITH ACUTE;Lu;0;L;005A 0301;;;;N;LATIN CAPITAL LETTER Z ACUTE;;;017A;
+017A;LATIN SMALL LETTER Z WITH ACUTE;Ll;0;L;007A 0301;;;;N;LATIN SMALL LETTER Z ACUTE;;0179;;0179
+017B;LATIN CAPITAL LETTER Z WITH DOT ABOVE;Lu;0;L;005A 0307;;;;N;LATIN CAPITAL LETTER Z DOT;;;017C;
+017C;LATIN SMALL LETTER Z WITH DOT ABOVE;Ll;0;L;007A 0307;;;;N;LATIN SMALL LETTER Z DOT;;017B;;017B
+017D;LATIN CAPITAL LETTER Z WITH CARON;Lu;0;L;005A 030C;;;;N;LATIN CAPITAL LETTER Z HACEK;;;017E;
+017E;LATIN SMALL LETTER Z WITH CARON;Ll;0;L;007A 030C;;;;N;LATIN SMALL LETTER Z HACEK;;017D;;017D
+017F;LATIN SMALL LETTER LONG S;Ll;0;L;<compat> 0073;;;;N;;;0053;;0053
+0180;LATIN SMALL LETTER B WITH STROKE;Ll;0;L;;;;;N;LATIN SMALL LETTER B BAR;;0243;;0243
+0181;LATIN CAPITAL LETTER B WITH HOOK;Lu;0;L;;;;;N;LATIN CAPITAL LETTER B HOOK;;;0253;
+0182;LATIN CAPITAL LETTER B WITH TOPBAR;Lu;0;L;;;;;N;LATIN CAPITAL LETTER B TOPBAR;;;0183;
+0183;LATIN SMALL LETTER B WITH TOPBAR;Ll;0;L;;;;;N;LATIN SMALL LETTER B TOPBAR;;0182;;0182
+0184;LATIN CAPITAL LETTER TONE SIX;Lu;0;L;;;;;N;;;;0185;
+0185;LATIN SMALL LETTER TONE SIX;Ll;0;L;;;;;N;;;0184;;0184
+0186;LATIN CAPITAL LETTER OPEN O;Lu;0;L;;;;;N;;;;0254;
+0187;LATIN CAPITAL LETTER C WITH HOOK;Lu;0;L;;;;;N;LATIN CAPITAL LETTER C HOOK;;;0188;
+0188;LATIN SMALL LETTER C WITH HOOK;Ll;0;L;;;;;N;LATIN SMALL LETTER C HOOK;;0187;;0187
+0189;LATIN CAPITAL LETTER AFRICAN D;Lu;0;L;;;;;N;;;;0256;
+018A;LATIN CAPITAL LETTER D WITH HOOK;Lu;0;L;;;;;N;LATIN CAPITAL LETTER D HOOK;;;0257;
+018B;LATIN CAPITAL LETTER D WITH TOPBAR;Lu;0;L;;;;;N;LATIN CAPITAL LETTER D TOPBAR;;;018C;
+018C;LATIN SMALL LETTER D WITH TOPBAR;Ll;0;L;;;;;N;LATIN SMALL LETTER D TOPBAR;;018B;;018B
+018D;LATIN SMALL LETTER TURNED DELTA;Ll;0;L;;;;;N;;;;;
+018E;LATIN CAPITAL LETTER REVERSED E;Lu;0;L;;;;;N;LATIN CAPITAL LETTER TURNED E;;;01DD;
+018F;LATIN CAPITAL LETTER SCHWA;Lu;0;L;;;;;N;;;;0259;
+0190;LATIN CAPITAL LETTER OPEN E;Lu;0;L;;;;;N;LATIN CAPITAL LETTER EPSILON;;;025B;
+0191;LATIN CAPITAL LETTER F WITH HOOK;Lu;0;L;;;;;N;LATIN CAPITAL LETTER F HOOK;;;0192;
+0192;LATIN SMALL LETTER F WITH HOOK;Ll;0;L;;;;;N;LATIN SMALL LETTER SCRIPT F;;0191;;0191
+0193;LATIN CAPITAL LETTER G WITH HOOK;Lu;0;L;;;;;N;LATIN CAPITAL LETTER G HOOK;;;0260;
+0194;LATIN CAPITAL LETTER GAMMA;Lu;0;L;;;;;N;;;;0263;
+0195;LATIN SMALL LETTER HV;Ll;0;L;;;;;N;LATIN SMALL LETTER H V;;01F6;;01F6
+0196;LATIN CAPITAL LETTER IOTA;Lu;0;L;;;;;N;;;;0269;
+0197;LATIN CAPITAL LETTER I WITH STROKE;Lu;0;L;;;;;N;LATIN CAPITAL LETTER BARRED I;;;0268;
+0198;LATIN CAPITAL LETTER K WITH HOOK;Lu;0;L;;;;;N;LATIN CAPITAL LETTER K HOOK;;;0199;
+0199;LATIN SMALL LETTER K WITH HOOK;Ll;0;L;;;;;N;LATIN SMALL LETTER K HOOK;;0198;;0198
+019A;LATIN SMALL LETTER L WITH BAR;Ll;0;L;;;;;N;LATIN SMALL LETTER BARRED L;;023D;;023D
+019B;LATIN SMALL LETTER LAMBDA WITH STROKE;Ll;0;L;;;;;N;LATIN SMALL LETTER BARRED LAMBDA;;;;
+019C;LATIN CAPITAL LETTER TURNED M;Lu;0;L;;;;;N;;;;026F;
+019D;LATIN CAPITAL LETTER N WITH LEFT HOOK;Lu;0;L;;;;;N;LATIN CAPITAL LETTER N HOOK;;;0272;
+019E;LATIN SMALL LETTER N WITH LONG RIGHT LEG;Ll;0;L;;;;;N;;;0220;;0220
+019F;LATIN CAPITAL LETTER O WITH MIDDLE TILDE;Lu;0;L;;;;;N;LATIN CAPITAL LETTER BARRED O;;;0275;
+01A0;LATIN CAPITAL LETTER O WITH HORN;Lu;0;L;004F 031B;;;;N;LATIN CAPITAL LETTER O HORN;;;01A1;
+01A1;LATIN SMALL LETTER O WITH HORN;Ll;0;L;006F 031B;;;;N;LATIN SMALL LETTER O HORN;;01A0;;01A0
+01A2;LATIN CAPITAL LETTER OI;Lu;0;L;;;;;N;LATIN CAPITAL LETTER O I;;;01A3;
+01A3;LATIN SMALL LETTER OI;Ll;0;L;;;;;N;LATIN SMALL LETTER O I;;01A2;;01A2
+01A4;LATIN CAPITAL LETTER P WITH HOOK;Lu;0;L;;;;;N;LATIN CAPITAL LETTER P HOOK;;;01A5;
+01A5;LATIN SMALL LETTER P WITH HOOK;Ll;0;L;;;;;N;LATIN SMALL LETTER P HOOK;;01A4;;01A4
+01A6;LATIN LETTER YR;Lu;0;L;;;;;N;LATIN LETTER Y R;;;0280;
+01A7;LATIN CAPITAL LETTER TONE TWO;Lu;0;L;;;;;N;;;;01A8;
+01A8;LATIN SMALL LETTER TONE TWO;Ll;0;L;;;;;N;;;01A7;;01A7
+01A9;LATIN CAPITAL LETTER ESH;Lu;0;L;;;;;N;;;;0283;
+01AA;LATIN LETTER REVERSED ESH LOOP;Ll;0;L;;;;;N;;;;;
+01AB;LATIN SMALL LETTER T WITH PALATAL HOOK;Ll;0;L;;;;;N;LATIN SMALL LETTER T PALATAL HOOK;;;;
+01AC;LATIN CAPITAL LETTER T WITH HOOK;Lu;0;L;;;;;N;LATIN CAPITAL LETTER T HOOK;;;01AD;
+01AD;LATIN SMALL LETTER T WITH HOOK;Ll;0;L;;;;;N;LATIN SMALL LETTER T HOOK;;01AC;;01AC
+01AE;LATIN CAPITAL LETTER T WITH RETROFLEX HOOK;Lu;0;L;;;;;N;LATIN CAPITAL LETTER T RETROFLEX HOOK;;;0288;
+01AF;LATIN CAPITAL LETTER U WITH HORN;Lu;0;L;0055 031B;;;;N;LATIN CAPITAL LETTER U HORN;;;01B0;
+01B0;LATIN SMALL LETTER U WITH HORN;Ll;0;L;0075 031B;;;;N;LATIN SMALL LETTER U HORN;;01AF;;01AF
+01B1;LATIN CAPITAL LETTER UPSILON;Lu;0;L;;;;;N;;;;028A;
+01B2;LATIN CAPITAL LETTER V WITH HOOK;Lu;0;L;;;;;N;LATIN CAPITAL LETTER SCRIPT V;;;028B;
+01B3;LATIN CAPITAL LETTER Y WITH HOOK;Lu;0;L;;;;;N;LATIN CAPITAL LETTER Y HOOK;;;01B4;
+01B4;LATIN SMALL LETTER Y WITH HOOK;Ll;0;L;;;;;N;LATIN SMALL LETTER Y HOOK;;01B3;;01B3
+01B5;LATIN CAPITAL LETTER Z WITH STROKE;Lu;0;L;;;;;N;LATIN CAPITAL LETTER Z BAR;;;01B6;
+01B6;LATIN SMALL LETTER Z WITH STROKE;Ll;0;L;;;;;N;LATIN SMALL LETTER Z BAR;;01B5;;01B5
+01B7;LATIN CAPITAL LETTER EZH;Lu;0;L;;;;;N;LATIN CAPITAL LETTER YOGH;;;0292;
+01B8;LATIN CAPITAL LETTER EZH REVERSED;Lu;0;L;;;;;N;LATIN CAPITAL LETTER REVERSED YOGH;;;01B9;
+01B9;LATIN SMALL LETTER EZH REVERSED;Ll;0;L;;;;;N;LATIN SMALL LETTER REVERSED YOGH;;01B8;;01B8
+01BA;LATIN SMALL LETTER EZH WITH TAIL;Ll;0;L;;;;;N;LATIN SMALL LETTER YOGH WITH TAIL;;;;
+01BB;LATIN LETTER TWO WITH STROKE;Lo;0;L;;;;;N;LATIN LETTER TWO BAR;;;;
+01BC;LATIN CAPITAL LETTER TONE FIVE;Lu;0;L;;;;;N;;;;01BD;
+01BD;LATIN SMALL LETTER TONE FIVE;Ll;0;L;;;;;N;;;01BC;;01BC
+01BE;LATIN LETTER INVERTED GLOTTAL STOP WITH STROKE;Ll;0;L;;;;;N;LATIN LETTER INVERTED GLOTTAL STOP BAR;;;;
+01BF;LATIN LETTER WYNN;Ll;0;L;;;;;N;;;01F7;;01F7
+01C0;LATIN LETTER DENTAL CLICK;Lo;0;L;;;;;N;LATIN LETTER PIPE;;;;
+01C1;LATIN LETTER LATERAL CLICK;Lo;0;L;;;;;N;LATIN LETTER DOUBLE PIPE;;;;
+01C2;LATIN LETTER ALVEOLAR CLICK;Lo;0;L;;;;;N;LATIN LETTER PIPE DOUBLE BAR;;;;
+01C3;LATIN LETTER RETROFLEX CLICK;Lo;0;L;;;;;N;LATIN LETTER EXCLAMATION MARK;;;;
+01C4;LATIN CAPITAL LETTER DZ WITH CARON;Lu;0;L;<compat> 0044 017D;;;;N;LATIN CAPITAL LETTER D Z HACEK;;;01C6;01C5
+01C5;LATIN CAPITAL LETTER D WITH SMALL LETTER Z WITH CARON;Lt;0;L;<compat> 0044 017E;;;;N;LATIN LETTER CAPITAL D SMALL Z HACEK;;01C4;01C6;01C5
+01C6;LATIN SMALL LETTER DZ WITH CARON;Ll;0;L;<compat> 0064 017E;;;;N;LATIN SMALL LETTER D Z HACEK;;01C4;;01C5
+01C7;LATIN CAPITAL LETTER LJ;Lu;0;L;<compat> 004C 004A;;;;N;LATIN CAPITAL LETTER L J;;;01C9;01C8
+01C8;LATIN CAPITAL LETTER L WITH SMALL LETTER J;Lt;0;L;<compat> 004C 006A;;;;N;LATIN LETTER CAPITAL L SMALL J;;01C7;01C9;01C8
+01C9;LATIN SMALL LETTER LJ;Ll;0;L;<compat> 006C 006A;;;;N;LATIN SMALL LETTER L J;;01C7;;01C8
+01CA;LATIN CAPITAL LETTER NJ;Lu;0;L;<compat> 004E 004A;;;;N;LATIN CAPITAL LETTER N J;;;01CC;01CB
+01CB;LATIN CAPITAL LETTER N WITH SMALL LETTER J;Lt;0;L;<compat> 004E 006A;;;;N;LATIN LETTER CAPITAL N SMALL J;;01CA;01CC;01CB
+01CC;LATIN SMALL LETTER NJ;Ll;0;L;<compat> 006E 006A;;;;N;LATIN SMALL LETTER N J;;01CA;;01CB
+01CD;LATIN CAPITAL LETTER A WITH CARON;Lu;0;L;0041 030C;;;;N;LATIN CAPITAL LETTER A HACEK;;;01CE;
+01CE;LATIN SMALL LETTER A WITH CARON;Ll;0;L;0061 030C;;;;N;LATIN SMALL LETTER A HACEK;;01CD;;01CD
+01CF;LATIN CAPITAL LETTER I WITH CARON;Lu;0;L;0049 030C;;;;N;LATIN CAPITAL LETTER I HACEK;;;01D0;
+01D0;LATIN SMALL LETTER I WITH CARON;Ll;0;L;0069 030C;;;;N;LATIN SMALL LETTER I HACEK;;01CF;;01CF
+01D1;LATIN CAPITAL LETTER O WITH CARON;Lu;0;L;004F 030C;;;;N;LATIN CAPITAL LETTER O HACEK;;;01D2;
+01D2;LATIN SMALL LETTER O WITH CARON;Ll;0;L;006F 030C;;;;N;LATIN SMALL LETTER O HACEK;;01D1;;01D1
+01D3;LATIN CAPITAL LETTER U WITH CARON;Lu;0;L;0055 030C;;;;N;LATIN CAPITAL LETTER U HACEK;;;01D4;
+01D4;LATIN SMALL LETTER U WITH CARON;Ll;0;L;0075 030C;;;;N;LATIN SMALL LETTER U HACEK;;01D3;;01D3
+01D5;LATIN CAPITAL LETTER U WITH DIAERESIS AND MACRON;Lu;0;L;00DC 0304;;;;N;LATIN CAPITAL LETTER U DIAERESIS MACRON;;;01D6;
+01D6;LATIN SMALL LETTER U WITH DIAERESIS AND MACRON;Ll;0;L;00FC 0304;;;;N;LATIN SMALL LETTER U DIAERESIS MACRON;;01D5;;01D5
+01D7;LATIN CAPITAL LETTER U WITH DIAERESIS AND ACUTE;Lu;0;L;00DC 0301;;;;N;LATIN CAPITAL LETTER U DIAERESIS ACUTE;;;01D8;
+01D8;LATIN SMALL LETTER U WITH DIAERESIS AND ACUTE;Ll;0;L;00FC 0301;;;;N;LATIN SMALL LETTER U DIAERESIS ACUTE;;01D7;;01D7
+01D9;LATIN CAPITAL LETTER U WITH DIAERESIS AND CARON;Lu;0;L;00DC 030C;;;;N;LATIN CAPITAL LETTER U DIAERESIS HACEK;;;01DA;
+01DA;LATIN SMALL LETTER U WITH DIAERESIS AND CARON;Ll;0;L;00FC 030C;;;;N;LATIN SMALL LETTER U DIAERESIS HACEK;;01D9;;01D9
+01DB;LATIN CAPITAL LETTER U WITH DIAERESIS AND GRAVE;Lu;0;L;00DC 0300;;;;N;LATIN CAPITAL LETTER U DIAERESIS GRAVE;;;01DC;
+01DC;LATIN SMALL LETTER U WITH DIAERESIS AND GRAVE;Ll;0;L;00FC 0300;;;;N;LATIN SMALL LETTER U DIAERESIS GRAVE;;01DB;;01DB
+01DD;LATIN SMALL LETTER TURNED E;Ll;0;L;;;;;N;;;018E;;018E
+01DE;LATIN CAPITAL LETTER A WITH DIAERESIS AND MACRON;Lu;0;L;00C4 0304;;;;N;LATIN CAPITAL LETTER A DIAERESIS MACRON;;;01DF;
+01DF;LATIN SMALL LETTER A WITH DIAERESIS AND MACRON;Ll;0;L;00E4 0304;;;;N;LATIN SMALL LETTER A DIAERESIS MACRON;;01DE;;01DE
+01E0;LATIN CAPITAL LETTER A WITH DOT ABOVE AND MACRON;Lu;0;L;0226 0304;;;;N;LATIN CAPITAL LETTER A DOT MACRON;;;01E1;
+01E1;LATIN SMALL LETTER A WITH DOT ABOVE AND MACRON;Ll;0;L;0227 0304;;;;N;LATIN SMALL LETTER A DOT MACRON;;01E0;;01E0
+01E2;LATIN CAPITAL LETTER AE WITH MACRON;Lu;0;L;00C6 0304;;;;N;LATIN CAPITAL LETTER A E MACRON;;;01E3;
+01E3;LATIN SMALL LETTER AE WITH MACRON;Ll;0;L;00E6 0304;;;;N;LATIN SMALL LETTER A E MACRON;;01E2;;01E2
+01E4;LATIN CAPITAL LETTER G WITH STROKE;Lu;0;L;;;;;N;LATIN CAPITAL LETTER G BAR;;;01E5;
+01E5;LATIN SMALL LETTER G WITH STROKE;Ll;0;L;;;;;N;LATIN SMALL LETTER G BAR;;01E4;;01E4
+01E6;LATIN CAPITAL LETTER G WITH CARON;Lu;0;L;0047 030C;;;;N;LATIN CAPITAL LETTER G HACEK;;;01E7;
+01E7;LATIN SMALL LETTER G WITH CARON;Ll;0;L;0067 030C;;;;N;LATIN SMALL LETTER G HACEK;;01E6;;01E6
+01E8;LATIN CAPITAL LETTER K WITH CARON;Lu;0;L;004B 030C;;;;N;LATIN CAPITAL LETTER K HACEK;;;01E9;
+01E9;LATIN SMALL LETTER K WITH CARON;Ll;0;L;006B 030C;;;;N;LATIN SMALL LETTER K HACEK;;01E8;;01E8
+01EA;LATIN CAPITAL LETTER O WITH OGONEK;Lu;0;L;004F 0328;;;;N;LATIN CAPITAL LETTER O OGONEK;;;01EB;
+01EB;LATIN SMALL LETTER O WITH OGONEK;Ll;0;L;006F 0328;;;;N;LATIN SMALL LETTER O OGONEK;;01EA;;01EA
+01EC;LATIN CAPITAL LETTER O WITH OGONEK AND MACRON;Lu;0;L;01EA 0304;;;;N;LATIN CAPITAL LETTER O OGONEK MACRON;;;01ED;
+01ED;LATIN SMALL LETTER O WITH OGONEK AND MACRON;Ll;0;L;01EB 0304;;;;N;LATIN SMALL LETTER O OGONEK MACRON;;01EC;;01EC
+01EE;LATIN CAPITAL LETTER EZH WITH CARON;Lu;0;L;01B7 030C;;;;N;LATIN CAPITAL LETTER YOGH HACEK;;;01EF;
+01EF;LATIN SMALL LETTER EZH WITH CARON;Ll;0;L;0292 030C;;;;N;LATIN SMALL LETTER YOGH HACEK;;01EE;;01EE
+01F0;LATIN SMALL LETTER J WITH CARON;Ll;0;L;006A 030C;;;;N;LATIN SMALL LETTER J HACEK;;;;
+01F1;LATIN CAPITAL LETTER DZ;Lu;0;L;<compat> 0044 005A;;;;N;;;;01F3;01F2
+01F2;LATIN CAPITAL LETTER D WITH SMALL LETTER Z;Lt;0;L;<compat> 0044 007A;;;;N;;;01F1;01F3;01F2
+01F3;LATIN SMALL LETTER DZ;Ll;0;L;<compat> 0064 007A;;;;N;;;01F1;;01F2
+01F4;LATIN CAPITAL LETTER G WITH ACUTE;Lu;0;L;0047 0301;;;;N;;;;01F5;
+01F5;LATIN SMALL LETTER G WITH ACUTE;Ll;0;L;0067 0301;;;;N;;;01F4;;01F4
+01F6;LATIN CAPITAL LETTER HWAIR;Lu;0;L;;;;;N;;;;0195;
+01F7;LATIN CAPITAL LETTER WYNN;Lu;0;L;;;;;N;;;;01BF;
+01F8;LATIN CAPITAL LETTER N WITH GRAVE;Lu;0;L;004E 0300;;;;N;;;;01F9;
+01F9;LATIN SMALL LETTER N WITH GRAVE;Ll;0;L;006E 0300;;;;N;;;01F8;;01F8
+01FA;LATIN CAPITAL LETTER A WITH RING ABOVE AND ACUTE;Lu;0;L;00C5 0301;;;;N;;;;01FB;
+01FB;LATIN SMALL LETTER A WITH RING ABOVE AND ACUTE;Ll;0;L;00E5 0301;;;;N;;;01FA;;01FA
+01FC;LATIN CAPITAL LETTER AE WITH ACUTE;Lu;0;L;00C6 0301;;;;N;;;;01FD;
+01FD;LATIN SMALL LETTER AE WITH ACUTE;Ll;0;L;00E6 0301;;;;N;;;01FC;;01FC
+01FE;LATIN CAPITAL LETTER O WITH STROKE AND ACUTE;Lu;0;L;00D8 0301;;;;N;;;;01FF;
+01FF;LATIN SMALL LETTER O WITH STROKE AND ACUTE;Ll;0;L;00F8 0301;;;;N;;;01FE;;01FE
+0200;LATIN CAPITAL LETTER A WITH DOUBLE GRAVE;Lu;0;L;0041 030F;;;;N;;;;0201;
+0201;LATIN SMALL LETTER A WITH DOUBLE GRAVE;Ll;0;L;0061 030F;;;;N;;;0200;;0200
+0202;LATIN CAPITAL LETTER A WITH INVERTED BREVE;Lu;0;L;0041 0311;;;;N;;;;0203;
+0203;LATIN SMALL LETTER A WITH INVERTED BREVE;Ll;0;L;0061 0311;;;;N;;;0202;;0202
+0204;LATIN CAPITAL LETTER E WITH DOUBLE GRAVE;Lu;0;L;0045 030F;;;;N;;;;0205;
+0205;LATIN SMALL LETTER E WITH DOUBLE GRAVE;Ll;0;L;0065 030F;;;;N;;;0204;;0204
+0206;LATIN CAPITAL LETTER E WITH INVERTED BREVE;Lu;0;L;0045 0311;;;;N;;;;0207;
+0207;LATIN SMALL LETTER E WITH INVERTED BREVE;Ll;0;L;0065 0311;;;;N;;;0206;;0206
+0208;LATIN CAPITAL LETTER I WITH DOUBLE GRAVE;Lu;0;L;0049 030F;;;;N;;;;0209;
+0209;LATIN SMALL LETTER I WITH DOUBLE GRAVE;Ll;0;L;0069 030F;;;;N;;;0208;;0208
+020A;LATIN CAPITAL LETTER I WITH INVERTED BREVE;Lu;0;L;0049 0311;;;;N;;;;020B;
+020B;LATIN SMALL LETTER I WITH INVERTED BREVE;Ll;0;L;0069 0311;;;;N;;;020A;;020A
+020C;LATIN CAPITAL LETTER O WITH DOUBLE GRAVE;Lu;0;L;004F 030F;;;;N;;;;020D;
+020D;LATIN SMALL LETTER O WITH DOUBLE GRAVE;Ll;0;L;006F 030F;;;;N;;;020C;;020C
+020E;LATIN CAPITAL LETTER O WITH INVERTED BREVE;Lu;0;L;004F 0311;;;;N;;;;020F;
+020F;LATIN SMALL LETTER O WITH INVERTED BREVE;Ll;0;L;006F 0311;;;;N;;;020E;;020E
+0210;LATIN CAPITAL LETTER R WITH DOUBLE GRAVE;Lu;0;L;0052 030F;;;;N;;;;0211;
+0211;LATIN SMALL LETTER R WITH DOUBLE GRAVE;Ll;0;L;0072 030F;;;;N;;;0210;;0210
+0212;LATIN CAPITAL LETTER R WITH INVERTED BREVE;Lu;0;L;0052 0311;;;;N;;;;0213;
+0213;LATIN SMALL LETTER R WITH INVERTED BREVE;Ll;0;L;0072 0311;;;;N;;;0212;;0212
+0214;LATIN CAPITAL LETTER U WITH DOUBLE GRAVE;Lu;0;L;0055 030F;;;;N;;;;0215;
+0215;LATIN SMALL LETTER U WITH DOUBLE GRAVE;Ll;0;L;0075 030F;;;;N;;;0214;;0214
+0216;LATIN CAPITAL LETTER U WITH INVERTED BREVE;Lu;0;L;0055 0311;;;;N;;;;0217;
+0217;LATIN SMALL LETTER U WITH INVERTED BREVE;Ll;0;L;0075 0311;;;;N;;;0216;;0216
+0218;LATIN CAPITAL LETTER S WITH COMMA BELOW;Lu;0;L;0053 0326;;;;N;;;;0219;
+0219;LATIN SMALL LETTER S WITH COMMA BELOW;Ll;0;L;0073 0326;;;;N;;;0218;;0218
+021A;LATIN CAPITAL LETTER T WITH COMMA BELOW;Lu;0;L;0054 0326;;;;N;;;;021B;
+021B;LATIN SMALL LETTER T WITH COMMA BELOW;Ll;0;L;0074 0326;;;;N;;;021A;;021A
+021C;LATIN CAPITAL LETTER YOGH;Lu;0;L;;;;;N;;;;021D;
+021D;LATIN SMALL LETTER YOGH;Ll;0;L;;;;;N;;;021C;;021C
+021E;LATIN CAPITAL LETTER H WITH CARON;Lu;0;L;0048 030C;;;;N;;;;021F;
+021F;LATIN SMALL LETTER H WITH CARON;Ll;0;L;0068 030C;;;;N;;;021E;;021E
+0220;LATIN CAPITAL LETTER N WITH LONG RIGHT LEG;Lu;0;L;;;;;N;;;;019E;
+0221;LATIN SMALL LETTER D WITH CURL;Ll;0;L;;;;;N;;;;;
+0222;LATIN CAPITAL LETTER OU;Lu;0;L;;;;;N;;;;0223;
+0223;LATIN SMALL LETTER OU;Ll;0;L;;;;;N;;;0222;;0222
+0224;LATIN CAPITAL LETTER Z WITH HOOK;Lu;0;L;;;;;N;;;;0225;
+0225;LATIN SMALL LETTER Z WITH HOOK;Ll;0;L;;;;;N;;;0224;;0224
+0226;LATIN CAPITAL LETTER A WITH DOT ABOVE;Lu;0;L;0041 0307;;;;N;;;;0227;
+0227;LATIN SMALL LETTER A WITH DOT ABOVE;Ll;0;L;0061 0307;;;;N;;;0226;;0226
+0228;LATIN CAPITAL LETTER E WITH CEDILLA;Lu;0;L;0045 0327;;;;N;;;;0229;
+0229;LATIN SMALL LETTER E WITH CEDILLA;Ll;0;L;0065 0327;;;;N;;;0228;;0228
+022A;LATIN CAPITAL LETTER O WITH DIAERESIS AND MACRON;Lu;0;L;00D6 0304;;;;N;;;;022B;
+022B;LATIN SMALL LETTER O WITH DIAERESIS AND MACRON;Ll;0;L;00F6 0304;;;;N;;;022A;;022A
+022C;LATIN CAPITAL LETTER O WITH TILDE AND MACRON;Lu;0;L;00D5 0304;;;;N;;;;022D;
+022D;LATIN SMALL LETTER O WITH TILDE AND MACRON;Ll;0;L;00F5 0304;;;;N;;;022C;;022C
+022E;LATIN CAPITAL LETTER O WITH DOT ABOVE;Lu;0;L;004F 0307;;;;N;;;;022F;
+022F;LATIN SMALL LETTER O WITH DOT ABOVE;Ll;0;L;006F 0307;;;;N;;;022E;;022E
+0230;LATIN CAPITAL LETTER O WITH DOT ABOVE AND MACRON;Lu;0;L;022E 0304;;;;N;;;;0231;
+0231;LATIN SMALL LETTER O WITH DOT ABOVE AND MACRON;Ll;0;L;022F 0304;;;;N;;;0230;;0230
+0232;LATIN CAPITAL LETTER Y WITH MACRON;Lu;0;L;0059 0304;;;;N;;;;0233;
+0233;LATIN SMALL LETTER Y WITH MACRON;Ll;0;L;0079 0304;;;;N;;;0232;;0232
+0234;LATIN SMALL LETTER L WITH CURL;Ll;0;L;;;;;N;;;;;
+0235;LATIN SMALL LETTER N WITH CURL;Ll;0;L;;;;;N;;;;;
+0236;LATIN SMALL LETTER T WITH CURL;Ll;0;L;;;;;N;;;;;
+0237;LATIN SMALL LETTER DOTLESS J;Ll;0;L;;;;;N;;;;;
+0238;LATIN SMALL LETTER DB DIGRAPH;Ll;0;L;;;;;N;;;;;
+0239;LATIN SMALL LETTER QP DIGRAPH;Ll;0;L;;;;;N;;;;;
+023A;LATIN CAPITAL LETTER A WITH STROKE;Lu;0;L;;;;;N;;;;2C65;
+023B;LATIN CAPITAL LETTER C WITH STROKE;Lu;0;L;;;;;N;;;;023C;
+023C;LATIN SMALL LETTER C WITH STROKE;Ll;0;L;;;;;N;;;023B;;023B
+023D;LATIN CAPITAL LETTER L WITH BAR;Lu;0;L;;;;;N;;;;019A;
+023E;LATIN CAPITAL LETTER T WITH DIAGONAL STROKE;Lu;0;L;;;;;N;;;;2C66;
+023F;LATIN SMALL LETTER S WITH SWASH TAIL;Ll;0;L;;;;;N;;;2C7E;;2C7E
+0240;LATIN SMALL LETTER Z WITH SWASH TAIL;Ll;0;L;;;;;N;;;2C7F;;2C7F
+0241;LATIN CAPITAL LETTER GLOTTAL STOP;Lu;0;L;;;;;N;;;;0242;
+0242;LATIN SMALL LETTER GLOTTAL STOP;Ll;0;L;;;;;N;;;0241;;0241
+0243;LATIN CAPITAL LETTER B WITH STROKE;Lu;0;L;;;;;N;;;;0180;
+0244;LATIN CAPITAL LETTER U BAR;Lu;0;L;;;;;N;;;;0289;
+0245;LATIN CAPITAL LETTER TURNED V;Lu;0;L;;;;;N;;;;028C;
+0246;LATIN CAPITAL LETTER E WITH STROKE;Lu;0;L;;;;;N;;;;0247;
+0247;LATIN SMALL LETTER E WITH STROKE;Ll;0;L;;;;;N;;;0246;;0246
+0248;LATIN CAPITAL LETTER J WITH STROKE;Lu;0;L;;;;;N;;;;0249;
+0249;LATIN SMALL LETTER J WITH STROKE;Ll;0;L;;;;;N;;;0248;;0248
+024A;LATIN CAPITAL LETTER SMALL Q WITH HOOK TAIL;Lu;0;L;;;;;N;;;;024B;
+024B;LATIN SMALL LETTER Q WITH HOOK TAIL;Ll;0;L;;;;;N;;;024A;;024A
+024C;LATIN CAPITAL LETTER R WITH STROKE;Lu;0;L;;;;;N;;;;024D;
+024D;LATIN SMALL LETTER R WITH STROKE;Ll;0;L;;;;;N;;;024C;;024C
+024E;LATIN CAPITAL LETTER Y WITH STROKE;Lu;0;L;;;;;N;;;;024F;
+024F;LATIN SMALL LETTER Y WITH STROKE;Ll;0;L;;;;;N;;;024E;;024E
+0250;LATIN SMALL LETTER TURNED A;Ll;0;L;;;;;N;;;2C6F;;2C6F
+0251;LATIN SMALL LETTER ALPHA;Ll;0;L;;;;;N;LATIN SMALL LETTER SCRIPT A;;2C6D;;2C6D
+0252;LATIN SMALL LETTER TURNED ALPHA;Ll;0;L;;;;;N;LATIN SMALL LETTER TURNED SCRIPT A;;2C70;;2C70
+0253;LATIN SMALL LETTER B WITH HOOK;Ll;0;L;;;;;N;LATIN SMALL LETTER B HOOK;;0181;;0181
+0254;LATIN SMALL LETTER OPEN O;Ll;0;L;;;;;N;;;0186;;0186
+0255;LATIN SMALL LETTER C WITH CURL;Ll;0;L;;;;;N;LATIN SMALL LETTER C CURL;;;;
+0256;LATIN SMALL LETTER D WITH TAIL;Ll;0;L;;;;;N;LATIN SMALL LETTER D RETROFLEX HOOK;;0189;;0189
+0257;LATIN SMALL LETTER D WITH HOOK;Ll;0;L;;;;;N;LATIN SMALL LETTER D HOOK;;018A;;018A
+0258;LATIN SMALL LETTER REVERSED E;Ll;0;L;;;;;N;;;;;
+0259;LATIN SMALL LETTER SCHWA;Ll;0;L;;;;;N;;;018F;;018F
+025A;LATIN SMALL LETTER SCHWA WITH HOOK;Ll;0;L;;;;;N;LATIN SMALL LETTER SCHWA HOOK;;;;
+025B;LATIN SMALL LETTER OPEN E;Ll;0;L;;;;;N;LATIN SMALL LETTER EPSILON;;0190;;0190
+025C;LATIN SMALL LETTER REVERSED OPEN E;Ll;0;L;;;;;N;LATIN SMALL LETTER REVERSED EPSILON;;A7AB;;A7AB
+025D;LATIN SMALL LETTER REVERSED OPEN E WITH HOOK;Ll;0;L;;;;;N;LATIN SMALL LETTER REVERSED EPSILON HOOK;;;;
+025E;LATIN SMALL LETTER CLOSED REVERSED OPEN E;Ll;0;L;;;;;N;LATIN SMALL LETTER CLOSED REVERSED EPSILON;;;;
+025F;LATIN SMALL LETTER DOTLESS J WITH STROKE;Ll;0;L;;;;;N;LATIN SMALL LETTER DOTLESS J BAR;;;;
+0260;LATIN SMALL LETTER G WITH HOOK;Ll;0;L;;;;;N;LATIN SMALL LETTER G HOOK;;0193;;0193
+0261;LATIN SMALL LETTER SCRIPT G;Ll;0;L;;;;;N;;;A7AC;;A7AC
+0262;LATIN LETTER SMALL CAPITAL G;Ll;0;L;;;;;N;;;;;
+0263;LATIN SMALL LETTER GAMMA;Ll;0;L;;;;;N;;;0194;;0194
+0264;LATIN SMALL LETTER RAMS HORN;Ll;0;L;;;;;N;LATIN SMALL LETTER BABY GAMMA;;;;
+0265;LATIN SMALL LETTER TURNED H;Ll;0;L;;;;;N;;;A78D;;A78D
+0266;LATIN SMALL LETTER H WITH HOOK;Ll;0;L;;;;;N;LATIN SMALL LETTER H HOOK;;A7AA;;A7AA
+0267;LATIN SMALL LETTER HENG WITH HOOK;Ll;0;L;;;;;N;LATIN SMALL LETTER HENG HOOK;;;;
+0268;LATIN SMALL LETTER I WITH STROKE;Ll;0;L;;;;;N;LATIN SMALL LETTER BARRED I;;0197;;0197
+0269;LATIN SMALL LETTER IOTA;Ll;0;L;;;;;N;;;0196;;0196
+026A;LATIN LETTER SMALL CAPITAL I;Ll;0;L;;;;;N;;;A7AE;;A7AE
+026B;LATIN SMALL LETTER L WITH MIDDLE TILDE;Ll;0;L;;;;;N;;;2C62;;2C62
+026C;LATIN SMALL LETTER L WITH BELT;Ll;0;L;;;;;N;LATIN SMALL LETTER L BELT;;A7AD;;A7AD
+026D;LATIN SMALL LETTER L WITH RETROFLEX HOOK;Ll;0;L;;;;;N;LATIN SMALL LETTER L RETROFLEX HOOK;;;;
+026E;LATIN SMALL LETTER LEZH;Ll;0;L;;;;;N;LATIN SMALL LETTER L YOGH;;;;
+026F;LATIN SMALL LETTER TURNED M;Ll;0;L;;;;;N;;;019C;;019C
+0270;LATIN SMALL LETTER TURNED M WITH LONG LEG;Ll;0;L;;;;;N;;;;;
+0271;LATIN SMALL LETTER M WITH HOOK;Ll;0;L;;;;;N;LATIN SMALL LETTER M HOOK;;2C6E;;2C6E
+0272;LATIN SMALL LETTER N WITH LEFT HOOK;Ll;0;L;;;;;N;LATIN SMALL LETTER N HOOK;;019D;;019D
+0273;LATIN SMALL LETTER N WITH RETROFLEX HOOK;Ll;0;L;;;;;N;LATIN SMALL LETTER N RETROFLEX HOOK;;;;
+0274;LATIN LETTER SMALL CAPITAL N;Ll;0;L;;;;;N;;;;;
+0275;LATIN SMALL LETTER BARRED O;Ll;0;L;;;;;N;;;019F;;019F
+0276;LATIN LETTER SMALL CAPITAL OE;Ll;0;L;;;;;N;LATIN LETTER SMALL CAPITAL O E;;;;
+0277;LATIN SMALL LETTER CLOSED OMEGA;Ll;0;L;;;;;N;;;;;
+0278;LATIN SMALL LETTER PHI;Ll;0;L;;;;;N;;;;;
+0279;LATIN SMALL LETTER TURNED R;Ll;0;L;;;;;N;;;;;
+027A;LATIN SMALL LETTER TURNED R WITH LONG LEG;Ll;0;L;;;;;N;;;;;
+027B;LATIN SMALL LETTER TURNED R WITH HOOK;Ll;0;L;;;;;N;LATIN SMALL LETTER TURNED R HOOK;;;;
+027C;LATIN SMALL LETTER R WITH LONG LEG;Ll;0;L;;;;;N;;;;;
+027D;LATIN SMALL LETTER R WITH TAIL;Ll;0;L;;;;;N;LATIN SMALL LETTER R HOOK;;2C64;;2C64
+027E;LATIN SMALL LETTER R WITH FISHHOOK;Ll;0;L;;;;;N;LATIN SMALL LETTER FISHHOOK R;;;;
+027F;LATIN SMALL LETTER REVERSED R WITH FISHHOOK;Ll;0;L;;;;;N;LATIN SMALL LETTER REVERSED FISHHOOK R;;;;
+0280;LATIN LETTER SMALL CAPITAL R;Ll;0;L;;;;;N;;;01A6;;01A6
+0281;LATIN LETTER SMALL CAPITAL INVERTED R;Ll;0;L;;;;;N;;;;;
+0282;LATIN SMALL LETTER S WITH HOOK;Ll;0;L;;;;;N;LATIN SMALL LETTER S HOOK;;;;
+0283;LATIN SMALL LETTER ESH;Ll;0;L;;;;;N;;;01A9;;01A9
+0284;LATIN SMALL LETTER DOTLESS J WITH STROKE AND HOOK;Ll;0;L;;;;;N;LATIN SMALL LETTER DOTLESS J BAR HOOK;;;;
+0285;LATIN SMALL LETTER SQUAT REVERSED ESH;Ll;0;L;;;;;N;;;;;
+0286;LATIN SMALL LETTER ESH WITH CURL;Ll;0;L;;;;;N;LATIN SMALL LETTER ESH CURL;;;;
+0287;LATIN SMALL LETTER TURNED T;Ll;0;L;;;;;N;;;A7B1;;A7B1
+0288;LATIN SMALL LETTER T WITH RETROFLEX HOOK;Ll;0;L;;;;;N;LATIN SMALL LETTER T RETROFLEX HOOK;;01AE;;01AE
+0289;LATIN SMALL LETTER U BAR;Ll;0;L;;;;;N;;;0244;;0244
+028A;LATIN SMALL LETTER UPSILON;Ll;0;L;;;;;N;;;01B1;;01B1
+028B;LATIN SMALL LETTER V WITH HOOK;Ll;0;L;;;;;N;LATIN SMALL LETTER SCRIPT V;;01B2;;01B2
+028C;LATIN SMALL LETTER TURNED V;Ll;0;L;;;;;N;;;0245;;0245
+028D;LATIN SMALL LETTER TURNED W;Ll;0;L;;;;;N;;;;;
+028E;LATIN SMALL LETTER TURNED Y;Ll;0;L;;;;;N;;;;;
+028F;LATIN LETTER SMALL CAPITAL Y;Ll;0;L;;;;;N;;;;;
+0290;LATIN SMALL LETTER Z WITH RETROFLEX HOOK;Ll;0;L;;;;;N;LATIN SMALL LETTER Z RETROFLEX HOOK;;;;
+0291;LATIN SMALL LETTER Z WITH CURL;Ll;0;L;;;;;N;LATIN SMALL LETTER Z CURL;;;;
+0292;LATIN SMALL LETTER EZH;Ll;0;L;;;;;N;LATIN SMALL LETTER YOGH;;01B7;;01B7
+0293;LATIN SMALL LETTER EZH WITH CURL;Ll;0;L;;;;;N;LATIN SMALL LETTER YOGH CURL;;;;
+0294;LATIN LETTER GLOTTAL STOP;Lo;0;L;;;;;N;;;;;
+0295;LATIN LETTER PHARYNGEAL VOICED FRICATIVE;Ll;0;L;;;;;N;LATIN LETTER REVERSED GLOTTAL STOP;;;;
+0296;LATIN LETTER INVERTED GLOTTAL STOP;Ll;0;L;;;;;N;;;;;
+0297;LATIN LETTER STRETCHED C;Ll;0;L;;;;;N;;;;;
+0298;LATIN LETTER BILABIAL CLICK;Ll;0;L;;;;;N;LATIN LETTER BULLSEYE;;;;
+0299;LATIN LETTER SMALL CAPITAL B;Ll;0;L;;;;;N;;;;;
+029A;LATIN SMALL LETTER CLOSED OPEN E;Ll;0;L;;;;;N;LATIN SMALL LETTER CLOSED EPSILON;;;;
+029B;LATIN LETTER SMALL CAPITAL G WITH HOOK;Ll;0;L;;;;;N;LATIN LETTER SMALL CAPITAL G HOOK;;;;
+029C;LATIN LETTER SMALL CAPITAL H;Ll;0;L;;;;;N;;;;;
+029D;LATIN SMALL LETTER J WITH CROSSED-TAIL;Ll;0;L;;;;;N;LATIN SMALL LETTER CROSSED-TAIL J;;A7B2;;A7B2
+029E;LATIN SMALL LETTER TURNED K;Ll;0;L;;;;;N;;;A7B0;;A7B0
+029F;LATIN LETTER SMALL CAPITAL L;Ll;0;L;;;;;N;;;;;
+02A0;LATIN SMALL LETTER Q WITH HOOK;Ll;0;L;;;;;N;LATIN SMALL LETTER Q HOOK;;;;
+02A1;LATIN LETTER GLOTTAL STOP WITH STROKE;Ll;0;L;;;;;N;LATIN LETTER GLOTTAL STOP BAR;;;;
+02A2;LATIN LETTER REVERSED GLOTTAL STOP WITH STROKE;Ll;0;L;;;;;N;LATIN LETTER REVERSED GLOTTAL STOP BAR;;;;
+02A3;LATIN SMALL LETTER DZ DIGRAPH;Ll;0;L;;;;;N;LATIN SMALL LETTER D Z;;;;
+02A4;LATIN SMALL LETTER DEZH DIGRAPH;Ll;0;L;;;;;N;LATIN SMALL LETTER D YOGH;;;;
+02A5;LATIN SMALL LETTER DZ DIGRAPH WITH CURL;Ll;0;L;;;;;N;LATIN SMALL LETTER D Z CURL;;;;
+02A6;LATIN SMALL LETTER TS DIGRAPH;Ll;0;L;;;;;N;LATIN SMALL LETTER T S;;;;
+02A7;LATIN SMALL LETTER TESH DIGRAPH;Ll;0;L;;;;;N;LATIN SMALL LETTER T ESH;;;;
+02A8;LATIN SMALL LETTER TC DIGRAPH WITH CURL;Ll;0;L;;;;;N;LATIN SMALL LETTER T C CURL;;;;
+02A9;LATIN SMALL LETTER FENG DIGRAPH;Ll;0;L;;;;;N;;;;;
+02AA;LATIN SMALL LETTER LS DIGRAPH;Ll;0;L;;;;;N;;;;;
+02AB;LATIN SMALL LETTER LZ DIGRAPH;Ll;0;L;;;;;N;;;;;
+02AC;LATIN LETTER BILABIAL PERCUSSIVE;Ll;0;L;;;;;N;;;;;
+02AD;LATIN LETTER BIDENTAL PERCUSSIVE;Ll;0;L;;;;;N;;;;;
+02AE;LATIN SMALL LETTER TURNED H WITH FISHHOOK;Ll;0;L;;;;;N;;;;;
+02AF;LATIN SMALL LETTER TURNED H WITH FISHHOOK AND TAIL;Ll;0;L;;;;;N;;;;;
+02B0;MODIFIER LETTER SMALL H;Lm;0;L;<super> 0068;;;;N;;;;;
+02B1;MODIFIER LETTER SMALL H WITH HOOK;Lm;0;L;<super> 0266;;;;N;MODIFIER LETTER SMALL H HOOK;;;;
+02B2;MODIFIER LETTER SMALL J;Lm;0;L;<super> 006A;;;;N;;;;;
+02B3;MODIFIER LETTER SMALL R;Lm;0;L;<super> 0072;;;;N;;;;;
+02B4;MODIFIER LETTER SMALL TURNED R;Lm;0;L;<super> 0279;;;;N;;;;;
+02B5;MODIFIER LETTER SMALL TURNED R WITH HOOK;Lm;0;L;<super> 027B;;;;N;MODIFIER LETTER SMALL TURNED R HOOK;;;;
+02B6;MODIFIER LETTER SMALL CAPITAL INVERTED R;Lm;0;L;<super> 0281;;;;N;;;;;
+02B7;MODIFIER LETTER SMALL W;Lm;0;L;<super> 0077;;;;N;;;;;
+02B8;MODIFIER LETTER SMALL Y;Lm;0;L;<super> 0079;;;;N;;;;;
+02B9;MODIFIER LETTER PRIME;Lm;0;ON;;;;;N;;;;;
+02BA;MODIFIER LETTER DOUBLE PRIME;Lm;0;ON;;;;;N;;;;;
+02BB;MODIFIER LETTER TURNED COMMA;Lm;0;L;;;;;N;;;;;
+02BC;MODIFIER LETTER APOSTROPHE;Lm;0;L;;;;;N;;;;;
+02BD;MODIFIER LETTER REVERSED COMMA;Lm;0;L;;;;;N;;;;;
+02BE;MODIFIER LETTER RIGHT HALF RING;Lm;0;L;;;;;N;;;;;
+02BF;MODIFIER LETTER LEFT HALF RING;Lm;0;L;;;;;N;;;;;
+02C0;MODIFIER LETTER GLOTTAL STOP;Lm;0;L;;;;;N;;;;;
+02C1;MODIFIER LETTER REVERSED GLOTTAL STOP;Lm;0;L;;;;;N;;;;;
+02C2;MODIFIER LETTER LEFT ARROWHEAD;Sk;0;ON;;;;;N;;;;;
+02C3;MODIFIER LETTER RIGHT ARROWHEAD;Sk;0;ON;;;;;N;;;;;
+02C4;MODIFIER LETTER UP ARROWHEAD;Sk;0;ON;;;;;N;;;;;
+02C5;MODIFIER LETTER DOWN ARROWHEAD;Sk;0;ON;;;;;N;;;;;
+02C6;MODIFIER LETTER CIRCUMFLEX ACCENT;Lm;0;ON;;;;;N;MODIFIER LETTER CIRCUMFLEX;;;;
+02C7;CARON;Lm;0;ON;;;;;N;MODIFIER LETTER HACEK;;;;
+02C8;MODIFIER LETTER VERTICAL LINE;Lm;0;ON;;;;;N;;;;;
+02C9;MODIFIER LETTER MACRON;Lm;0;ON;;;;;N;;;;;
+02CA;MODIFIER LETTER ACUTE ACCENT;Lm;0;ON;;;;;N;MODIFIER LETTER ACUTE;;;;
+02CB;MODIFIER LETTER GRAVE ACCENT;Lm;0;ON;;;;;N;MODIFIER LETTER GRAVE;;;;
+02CC;MODIFIER LETTER LOW VERTICAL LINE;Lm;0;ON;;;;;N;;;;;
+02CD;MODIFIER LETTER LOW MACRON;Lm;0;ON;;;;;N;;;;;
+02CE;MODIFIER LETTER LOW GRAVE ACCENT;Lm;0;ON;;;;;N;MODIFIER LETTER LOW GRAVE;;;;
+02CF;MODIFIER LETTER LOW ACUTE ACCENT;Lm;0;ON;;;;;N;MODIFIER LETTER LOW ACUTE;;;;
+02D0;MODIFIER LETTER TRIANGULAR COLON;Lm;0;L;;;;;N;;;;;
+02D1;MODIFIER LETTER HALF TRIANGULAR COLON;Lm;0;L;;;;;N;;;;;
+02D2;MODIFIER LETTER CENTRED RIGHT HALF RING;Sk;0;ON;;;;;N;MODIFIER LETTER CENTERED RIGHT HALF RING;;;;
+02D3;MODIFIER LETTER CENTRED LEFT HALF RING;Sk;0;ON;;;;;N;MODIFIER LETTER CENTERED LEFT HALF RING;;;;
+02D4;MODIFIER LETTER UP TACK;Sk;0;ON;;;;;N;;;;;
+02D5;MODIFIER LETTER DOWN TACK;Sk;0;ON;;;;;N;;;;;
+02D6;MODIFIER LETTER PLUS SIGN;Sk;0;ON;;;;;N;;;;;
+02D7;MODIFIER LETTER MINUS SIGN;Sk;0;ON;;;;;N;;;;;
+02D8;BREVE;Sk;0;ON;<compat> 0020 0306;;;;N;SPACING BREVE;;;;
+02D9;DOT ABOVE;Sk;0;ON;<compat> 0020 0307;;;;N;SPACING DOT ABOVE;;;;
+02DA;RING ABOVE;Sk;0;ON;<compat> 0020 030A;;;;N;SPACING RING ABOVE;;;;
+02DB;OGONEK;Sk;0;ON;<compat> 0020 0328;;;;N;SPACING OGONEK;;;;
+02DC;SMALL TILDE;Sk;0;ON;<compat> 0020 0303;;;;N;SPACING TILDE;;;;
+02DD;DOUBLE ACUTE ACCENT;Sk;0;ON;<compat> 0020 030B;;;;N;SPACING DOUBLE ACUTE;;;;
+02DE;MODIFIER LETTER RHOTIC HOOK;Sk;0;ON;;;;;N;;;;;
+02DF;MODIFIER LETTER CROSS ACCENT;Sk;0;ON;;;;;N;;;;;
+02E0;MODIFIER LETTER SMALL GAMMA;Lm;0;L;<super> 0263;;;;N;;;;;
+02E1;MODIFIER LETTER SMALL L;Lm;0;L;<super> 006C;;;;N;;;;;
+02E2;MODIFIER LETTER SMALL S;Lm;0;L;<super> 0073;;;;N;;;;;
+02E3;MODIFIER LETTER SMALL X;Lm;0;L;<super> 0078;;;;N;;;;;
+02E4;MODIFIER LETTER SMALL REVERSED GLOTTAL STOP;Lm;0;L;<super> 0295;;;;N;;;;;
+02E5;MODIFIER LETTER EXTRA-HIGH TONE BAR;Sk;0;ON;;;;;N;;;;;
+02E6;MODIFIER LETTER HIGH TONE BAR;Sk;0;ON;;;;;N;;;;;
+02E7;MODIFIER LETTER MID TONE BAR;Sk;0;ON;;;;;N;;;;;
+02E8;MODIFIER LETTER LOW TONE BAR;Sk;0;ON;;;;;N;;;;;
+02E9;MODIFIER LETTER EXTRA-LOW TONE BAR;Sk;0;ON;;;;;N;;;;;
+02EA;MODIFIER LETTER YIN DEPARTING TONE MARK;Sk;0;ON;;;;;N;;;;;
+02EB;MODIFIER LETTER YANG DEPARTING TONE MARK;Sk;0;ON;;;;;N;;;;;
+02EC;MODIFIER LETTER VOICING;Lm;0;ON;;;;;N;;;;;
+02ED;MODIFIER LETTER UNASPIRATED;Sk;0;ON;;;;;N;;;;;
+02EE;MODIFIER LETTER DOUBLE APOSTROPHE;Lm;0;L;;;;;N;;;;;
+02EF;MODIFIER LETTER LOW DOWN ARROWHEAD;Sk;0;ON;;;;;N;;;;;
+02F0;MODIFIER LETTER LOW UP ARROWHEAD;Sk;0;ON;;;;;N;;;;;
+02F1;MODIFIER LETTER LOW LEFT ARROWHEAD;Sk;0;ON;;;;;N;;;;;
+02F2;MODIFIER LETTER LOW RIGHT ARROWHEAD;Sk;0;ON;;;;;N;;;;;
+02F3;MODIFIER LETTER LOW RING;Sk;0;ON;;;;;N;;;;;
+02F4;MODIFIER LETTER MIDDLE GRAVE ACCENT;Sk;0;ON;;;;;N;;;;;
+02F5;MODIFIER LETTER MIDDLE DOUBLE GRAVE ACCENT;Sk;0;ON;;;;;N;;;;;
+02F6;MODIFIER LETTER MIDDLE DOUBLE ACUTE ACCENT;Sk;0;ON;;;;;N;;;;;
+02F7;MODIFIER LETTER LOW TILDE;Sk;0;ON;;;;;N;;;;;
+02F8;MODIFIER LETTER RAISED COLON;Sk;0;ON;;;;;N;;;;;
+02F9;MODIFIER LETTER BEGIN HIGH TONE;Sk;0;ON;;;;;N;;;;;
+02FA;MODIFIER LETTER END HIGH TONE;Sk;0;ON;;;;;N;;;;;
+02FB;MODIFIER LETTER BEGIN LOW TONE;Sk;0;ON;;;;;N;;;;;
+02FC;MODIFIER LETTER END LOW TONE;Sk;0;ON;;;;;N;;;;;
+02FD;MODIFIER LETTER SHELF;Sk;0;ON;;;;;N;;;;;
+02FE;MODIFIER LETTER OPEN SHELF;Sk;0;ON;;;;;N;;;;;
+02FF;MODIFIER LETTER LOW LEFT ARROW;Sk;0;ON;;;;;N;;;;;
+0300;COMBINING GRAVE ACCENT;Mn;230;NSM;;;;;N;NON-SPACING GRAVE;;;;
+0301;COMBINING ACUTE ACCENT;Mn;230;NSM;;;;;N;NON-SPACING ACUTE;;;;
+0302;COMBINING CIRCUMFLEX ACCENT;Mn;230;NSM;;;;;N;NON-SPACING CIRCUMFLEX;;;;
+0303;COMBINING TILDE;Mn;230;NSM;;;;;N;NON-SPACING TILDE;;;;
+0304;COMBINING MACRON;Mn;230;NSM;;;;;N;NON-SPACING MACRON;;;;
+0305;COMBINING OVERLINE;Mn;230;NSM;;;;;N;NON-SPACING OVERSCORE;;;;
+0306;COMBINING BREVE;Mn;230;NSM;;;;;N;NON-SPACING BREVE;;;;
+0307;COMBINING DOT ABOVE;Mn;230;NSM;;;;;N;NON-SPACING DOT ABOVE;;;;
+0308;COMBINING DIAERESIS;Mn;230;NSM;;;;;N;NON-SPACING DIAERESIS;;;;
+0309;COMBINING HOOK ABOVE;Mn;230;NSM;;;;;N;NON-SPACING HOOK ABOVE;;;;
+030A;COMBINING RING ABOVE;Mn;230;NSM;;;;;N;NON-SPACING RING ABOVE;;;;
+030B;COMBINING DOUBLE ACUTE ACCENT;Mn;230;NSM;;;;;N;NON-SPACING DOUBLE ACUTE;;;;
+030C;COMBINING CARON;Mn;230;NSM;;;;;N;NON-SPACING HACEK;;;;
+030D;COMBINING VERTICAL LINE ABOVE;Mn;230;NSM;;;;;N;NON-SPACING VERTICAL LINE ABOVE;;;;
+030E;COMBINING DOUBLE VERTICAL LINE ABOVE;Mn;230;NSM;;;;;N;NON-SPACING DOUBLE VERTICAL LINE ABOVE;;;;
+030F;COMBINING DOUBLE GRAVE ACCENT;Mn;230;NSM;;;;;N;NON-SPACING DOUBLE GRAVE;;;;
+0310;COMBINING CANDRABINDU;Mn;230;NSM;;;;;N;NON-SPACING CANDRABINDU;;;;
+0311;COMBINING INVERTED BREVE;Mn;230;NSM;;;;;N;NON-SPACING INVERTED BREVE;;;;
+0312;COMBINING TURNED COMMA ABOVE;Mn;230;NSM;;;;;N;NON-SPACING TURNED COMMA ABOVE;;;;
+0313;COMBINING COMMA ABOVE;Mn;230;NSM;;;;;N;NON-SPACING COMMA ABOVE;;;;
+0314;COMBINING REVERSED COMMA ABOVE;Mn;230;NSM;;;;;N;NON-SPACING REVERSED COMMA ABOVE;;;;
+0315;COMBINING COMMA ABOVE RIGHT;Mn;232;NSM;;;;;N;NON-SPACING COMMA ABOVE RIGHT;;;;
+0316;COMBINING GRAVE ACCENT BELOW;Mn;220;NSM;;;;;N;NON-SPACING GRAVE BELOW;;;;
+0317;COMBINING ACUTE ACCENT BELOW;Mn;220;NSM;;;;;N;NON-SPACING ACUTE BELOW;;;;
+0318;COMBINING LEFT TACK BELOW;Mn;220;NSM;;;;;N;NON-SPACING LEFT TACK BELOW;;;;
+0319;COMBINING RIGHT TACK BELOW;Mn;220;NSM;;;;;N;NON-SPACING RIGHT TACK BELOW;;;;
+031A;COMBINING LEFT ANGLE ABOVE;Mn;232;NSM;;;;;N;NON-SPACING LEFT ANGLE ABOVE;;;;
+031B;COMBINING HORN;Mn;216;NSM;;;;;N;NON-SPACING HORN;;;;
+031C;COMBINING LEFT HALF RING BELOW;Mn;220;NSM;;;;;N;NON-SPACING LEFT HALF RING BELOW;;;;
+031D;COMBINING UP TACK BELOW;Mn;220;NSM;;;;;N;NON-SPACING UP TACK BELOW;;;;
+031E;COMBINING DOWN TACK BELOW;Mn;220;NSM;;;;;N;NON-SPACING DOWN TACK BELOW;;;;
+031F;COMBINING PLUS SIGN BELOW;Mn;220;NSM;;;;;N;NON-SPACING PLUS SIGN BELOW;;;;
+0320;COMBINING MINUS SIGN BELOW;Mn;220;NSM;;;;;N;NON-SPACING MINUS SIGN BELOW;;;;
+0321;COMBINING PALATALIZED HOOK BELOW;Mn;202;NSM;;;;;N;NON-SPACING PALATALIZED HOOK BELOW;;;;
+0322;COMBINING RETROFLEX HOOK BELOW;Mn;202;NSM;;;;;N;NON-SPACING RETROFLEX HOOK BELOW;;;;
+0323;COMBINING DOT BELOW;Mn;220;NSM;;;;;N;NON-SPACING DOT BELOW;;;;
+0324;COMBINING DIAERESIS BELOW;Mn;220;NSM;;;;;N;NON-SPACING DOUBLE DOT BELOW;;;;
+0325;COMBINING RING BELOW;Mn;220;NSM;;;;;N;NON-SPACING RING BELOW;;;;
+0326;COMBINING COMMA BELOW;Mn;220;NSM;;;;;N;NON-SPACING COMMA BELOW;;;;
+0327;COMBINING CEDILLA;Mn;202;NSM;;;;;N;NON-SPACING CEDILLA;;;;
+0328;COMBINING OGONEK;Mn;202;NSM;;;;;N;NON-SPACING OGONEK;;;;
+0329;COMBINING VERTICAL LINE BELOW;Mn;220;NSM;;;;;N;NON-SPACING VERTICAL LINE BELOW;;;;
+032A;COMBINING BRIDGE BELOW;Mn;220;NSM;;;;;N;NON-SPACING BRIDGE BELOW;;;;
+032B;COMBINING INVERTED DOUBLE ARCH BELOW;Mn;220;NSM;;;;;N;NON-SPACING INVERTED DOUBLE ARCH BELOW;;;;
+032C;COMBINING CARON BELOW;Mn;220;NSM;;;;;N;NON-SPACING HACEK BELOW;;;;
+032D;COMBINING CIRCUMFLEX ACCENT BELOW;Mn;220;NSM;;;;;N;NON-SPACING CIRCUMFLEX BELOW;;;;
+032E;COMBINING BREVE BELOW;Mn;220;NSM;;;;;N;NON-SPACING BREVE BELOW;;;;
+032F;COMBINING INVERTED BREVE BELOW;Mn;220;NSM;;;;;N;NON-SPACING INVERTED BREVE BELOW;;;;
+0330;COMBINING TILDE BELOW;Mn;220;NSM;;;;;N;NON-SPACING TILDE BELOW;;;;
+0331;COMBINING MACRON BELOW;Mn;220;NSM;;;;;N;NON-SPACING MACRON BELOW;;;;
+0332;COMBINING LOW LINE;Mn;220;NSM;;;;;N;NON-SPACING UNDERSCORE;;;;
+0333;COMBINING DOUBLE LOW LINE;Mn;220;NSM;;;;;N;NON-SPACING DOUBLE UNDERSCORE;;;;
+0334;COMBINING TILDE OVERLAY;Mn;1;NSM;;;;;N;NON-SPACING TILDE OVERLAY;;;;
+0335;COMBINING SHORT STROKE OVERLAY;Mn;1;NSM;;;;;N;NON-SPACING SHORT BAR OVERLAY;;;;
+0336;COMBINING LONG STROKE OVERLAY;Mn;1;NSM;;;;;N;NON-SPACING LONG BAR OVERLAY;;;;
+0337;COMBINING SHORT SOLIDUS OVERLAY;Mn;1;NSM;;;;;N;NON-SPACING SHORT SLASH OVERLAY;;;;
+0338;COMBINING LONG SOLIDUS OVERLAY;Mn;1;NSM;;;;;N;NON-SPACING LONG SLASH OVERLAY;;;;
+0339;COMBINING RIGHT HALF RING BELOW;Mn;220;NSM;;;;;N;NON-SPACING RIGHT HALF RING BELOW;;;;
+033A;COMBINING INVERTED BRIDGE BELOW;Mn;220;NSM;;;;;N;NON-SPACING INVERTED BRIDGE BELOW;;;;
+033B;COMBINING SQUARE BELOW;Mn;220;NSM;;;;;N;NON-SPACING SQUARE BELOW;;;;
+033C;COMBINING SEAGULL BELOW;Mn;220;NSM;;;;;N;NON-SPACING SEAGULL BELOW;;;;
+033D;COMBINING X ABOVE;Mn;230;NSM;;;;;N;NON-SPACING X ABOVE;;;;
+033E;COMBINING VERTICAL TILDE;Mn;230;NSM;;;;;N;NON-SPACING VERTICAL TILDE;;;;
+033F;COMBINING DOUBLE OVERLINE;Mn;230;NSM;;;;;N;NON-SPACING DOUBLE OVERSCORE;;;;
+0340;COMBINING GRAVE TONE MARK;Mn;230;NSM;0300;;;;N;NON-SPACING GRAVE TONE MARK;;;;
+0341;COMBINING ACUTE TONE MARK;Mn;230;NSM;0301;;;;N;NON-SPACING ACUTE TONE MARK;;;;
+0342;COMBINING GREEK PERISPOMENI;Mn;230;NSM;;;;;N;;;;;
+0343;COMBINING GREEK KORONIS;Mn;230;NSM;0313;;;;N;;;;;
+0344;COMBINING GREEK DIALYTIKA TONOS;Mn;230;NSM;0308 0301;;;;N;GREEK NON-SPACING DIAERESIS TONOS;;;;
+0345;COMBINING GREEK YPOGEGRAMMENI;Mn;240;NSM;;;;;N;GREEK NON-SPACING IOTA BELOW;;0399;;0399
+0346;COMBINING BRIDGE ABOVE;Mn;230;NSM;;;;;N;;;;;
+0347;COMBINING EQUALS SIGN BELOW;Mn;220;NSM;;;;;N;;;;;
+0348;COMBINING DOUBLE VERTICAL LINE BELOW;Mn;220;NSM;;;;;N;;;;;
+0349;COMBINING LEFT ANGLE BELOW;Mn;220;NSM;;;;;N;;;;;
+034A;COMBINING NOT TILDE ABOVE;Mn;230;NSM;;;;;N;;;;;
+034B;COMBINING HOMOTHETIC ABOVE;Mn;230;NSM;;;;;N;;;;;
+034C;COMBINING ALMOST EQUAL TO ABOVE;Mn;230;NSM;;;;;N;;;;;
+034D;COMBINING LEFT RIGHT ARROW BELOW;Mn;220;NSM;;;;;N;;;;;
+034E;COMBINING UPWARDS ARROW BELOW;Mn;220;NSM;;;;;N;;;;;
+034F;COMBINING GRAPHEME JOINER;Mn;0;NSM;;;;;N;;;;;
+0350;COMBINING RIGHT ARROWHEAD ABOVE;Mn;230;NSM;;;;;N;;;;;
+0351;COMBINING LEFT HALF RING ABOVE;Mn;230;NSM;;;;;N;;;;;
+0352;COMBINING FERMATA;Mn;230;NSM;;;;;N;;;;;
+0353;COMBINING X BELOW;Mn;220;NSM;;;;;N;;;;;
+0354;COMBINING LEFT ARROWHEAD BELOW;Mn;220;NSM;;;;;N;;;;;
+0355;COMBINING RIGHT ARROWHEAD BELOW;Mn;220;NSM;;;;;N;;;;;
+0356;COMBINING RIGHT ARROWHEAD AND UP ARROWHEAD BELOW;Mn;220;NSM;;;;;N;;;;;
+0357;COMBINING RIGHT HALF RING ABOVE;Mn;230;NSM;;;;;N;;;;;
+0358;COMBINING DOT ABOVE RIGHT;Mn;232;NSM;;;;;N;;;;;
+0359;COMBINING ASTERISK BELOW;Mn;220;NSM;;;;;N;;;;;
+035A;COMBINING DOUBLE RING BELOW;Mn;220;NSM;;;;;N;;;;;
+035B;COMBINING ZIGZAG ABOVE;Mn;230;NSM;;;;;N;;;;;
+035C;COMBINING DOUBLE BREVE BELOW;Mn;233;NSM;;;;;N;;;;;
+035D;COMBINING DOUBLE BREVE;Mn;234;NSM;;;;;N;;;;;
+035E;COMBINING DOUBLE MACRON;Mn;234;NSM;;;;;N;;;;;
+035F;COMBINING DOUBLE MACRON BELOW;Mn;233;NSM;;;;;N;;;;;
+0360;COMBINING DOUBLE TILDE;Mn;234;NSM;;;;;N;;;;;
+0361;COMBINING DOUBLE INVERTED BREVE;Mn;234;NSM;;;;;N;;;;;
+0362;COMBINING DOUBLE RIGHTWARDS ARROW BELOW;Mn;233;NSM;;;;;N;;;;;
+0363;COMBINING LATIN SMALL LETTER A;Mn;230;NSM;;;;;N;;;;;
+0364;COMBINING LATIN SMALL LETTER E;Mn;230;NSM;;;;;N;;;;;
+0365;COMBINING LATIN SMALL LETTER I;Mn;230;NSM;;;;;N;;;;;
+0366;COMBINING LATIN SMALL LETTER O;Mn;230;NSM;;;;;N;;;;;
+0367;COMBINING LATIN SMALL LETTER U;Mn;230;NSM;;;;;N;;;;;
+0368;COMBINING LATIN SMALL LETTER C;Mn;230;NSM;;;;;N;;;;;
+0369;COMBINING LATIN SMALL LETTER D;Mn;230;NSM;;;;;N;;;;;
+036A;COMBINING LATIN SMALL LETTER H;Mn;230;NSM;;;;;N;;;;;
+036B;COMBINING LATIN SMALL LETTER M;Mn;230;NSM;;;;;N;;;;;
+036C;COMBINING LATIN SMALL LETTER R;Mn;230;NSM;;;;;N;;;;;
+036D;COMBINING LATIN SMALL LETTER T;Mn;230;NSM;;;;;N;;;;;
+036E;COMBINING LATIN SMALL LETTER V;Mn;230;NSM;;;;;N;;;;;
+036F;COMBINING LATIN SMALL LETTER X;Mn;230;NSM;;;;;N;;;;;
+0370;GREEK CAPITAL LETTER HETA;Lu;0;L;;;;;N;;;;0371;
+0371;GREEK SMALL LETTER HETA;Ll;0;L;;;;;N;;;0370;;0370
+0372;GREEK CAPITAL LETTER ARCHAIC SAMPI;Lu;0;L;;;;;N;;;;0373;
+0373;GREEK SMALL LETTER ARCHAIC SAMPI;Ll;0;L;;;;;N;;;0372;;0372
+0374;GREEK NUMERAL SIGN;Lm;0;ON;02B9;;;;N;GREEK UPPER NUMERAL SIGN;;;;
+0375;GREEK LOWER NUMERAL SIGN;Sk;0;ON;;;;;N;;;;;
+0376;GREEK CAPITAL LETTER PAMPHYLIAN DIGAMMA;Lu;0;L;;;;;N;;;;0377;
+0377;GREEK SMALL LETTER PAMPHYLIAN DIGAMMA;Ll;0;L;;;;;N;;;0376;;0376
+037A;GREEK YPOGEGRAMMENI;Lm;0;L;<compat> 0020 0345;;;;N;GREEK SPACING IOTA BELOW;;;;
+037B;GREEK SMALL REVERSED LUNATE SIGMA SYMBOL;Ll;0;L;;;;;N;;;03FD;;03FD
+037C;GREEK SMALL DOTTED LUNATE SIGMA SYMBOL;Ll;0;L;;;;;N;;;03FE;;03FE
+037D;GREEK SMALL REVERSED DOTTED LUNATE SIGMA SYMBOL;Ll;0;L;;;;;N;;;03FF;;03FF
+037E;GREEK QUESTION MARK;Po;0;ON;003B;;;;N;;;;;
+037F;GREEK CAPITAL LETTER YOT;Lu;0;L;;;;;N;;;;03F3;
+0384;GREEK TONOS;Sk;0;ON;<compat> 0020 0301;;;;N;GREEK SPACING TONOS;;;;
+0385;GREEK DIALYTIKA TONOS;Sk;0;ON;00A8 0301;;;;N;GREEK SPACING DIAERESIS TONOS;;;;
+0386;GREEK CAPITAL LETTER ALPHA WITH TONOS;Lu;0;L;0391 0301;;;;N;GREEK CAPITAL LETTER ALPHA TONOS;;;03AC;
+0387;GREEK ANO TELEIA;Po;0;ON;00B7;;;;N;;;;;
+0388;GREEK CAPITAL LETTER EPSILON WITH TONOS;Lu;0;L;0395 0301;;;;N;GREEK CAPITAL LETTER EPSILON TONOS;;;03AD;
+0389;GREEK CAPITAL LETTER ETA WITH TONOS;Lu;0;L;0397 0301;;;;N;GREEK CAPITAL LETTER ETA TONOS;;;03AE;
+038A;GREEK CAPITAL LETTER IOTA WITH TONOS;Lu;0;L;0399 0301;;;;N;GREEK CAPITAL LETTER IOTA TONOS;;;03AF;
+038C;GREEK CAPITAL LETTER OMICRON WITH TONOS;Lu;0;L;039F 0301;;;;N;GREEK CAPITAL LETTER OMICRON TONOS;;;03CC;
+038E;GREEK CAPITAL LETTER UPSILON WITH TONOS;Lu;0;L;03A5 0301;;;;N;GREEK CAPITAL LETTER UPSILON TONOS;;;03CD;
+038F;GREEK CAPITAL LETTER OMEGA WITH TONOS;Lu;0;L;03A9 0301;;;;N;GREEK CAPITAL LETTER OMEGA TONOS;;;03CE;
+0390;GREEK SMALL LETTER IOTA WITH DIALYTIKA AND TONOS;Ll;0;L;03CA 0301;;;;N;GREEK SMALL LETTER IOTA DIAERESIS TONOS;;;;
+0391;GREEK CAPITAL LETTER ALPHA;Lu;0;L;;;;;N;;;;03B1;
+0392;GREEK CAPITAL LETTER BETA;Lu;0;L;;;;;N;;;;03B2;
+0393;GREEK CAPITAL LETTER GAMMA;Lu;0;L;;;;;N;;;;03B3;
+0394;GREEK CAPITAL LETTER DELTA;Lu;0;L;;;;;N;;;;03B4;
+0395;GREEK CAPITAL LETTER EPSILON;Lu;0;L;;;;;N;;;;03B5;
+0396;GREEK CAPITAL LETTER ZETA;Lu;0;L;;;;;N;;;;03B6;
+0397;GREEK CAPITAL LETTER ETA;Lu;0;L;;;;;N;;;;03B7;
+0398;GREEK CAPITAL LETTER THETA;Lu;0;L;;;;;N;;;;03B8;
+0399;GREEK CAPITAL LETTER IOTA;Lu;0;L;;;;;N;;;;03B9;
+039A;GREEK CAPITAL LETTER KAPPA;Lu;0;L;;;;;N;;;;03BA;
+039B;GREEK CAPITAL LETTER LAMDA;Lu;0;L;;;;;N;GREEK CAPITAL LETTER LAMBDA;;;03BB;
+039C;GREEK CAPITAL LETTER MU;Lu;0;L;;;;;N;;;;03BC;
+039D;GREEK CAPITAL LETTER NU;Lu;0;L;;;;;N;;;;03BD;
+039E;GREEK CAPITAL LETTER XI;Lu;0;L;;;;;N;;;;03BE;
+039F;GREEK CAPITAL LETTER OMICRON;Lu;0;L;;;;;N;;;;03BF;
+03A0;GREEK CAPITAL LETTER PI;Lu;0;L;;;;;N;;;;03C0;
+03A1;GREEK CAPITAL LETTER RHO;Lu;0;L;;;;;N;;;;03C1;
+03A3;GREEK CAPITAL LETTER SIGMA;Lu;0;L;;;;;N;;;;03C3;
+03A4;GREEK CAPITAL LETTER TAU;Lu;0;L;;;;;N;;;;03C4;
+03A5;GREEK CAPITAL LETTER UPSILON;Lu;0;L;;;;;N;;;;03C5;
+03A6;GREEK CAPITAL LETTER PHI;Lu;0;L;;;;;N;;;;03C6;
+03A7;GREEK CAPITAL LETTER CHI;Lu;0;L;;;;;N;;;;03C7;
+03A8;GREEK CAPITAL LETTER PSI;Lu;0;L;;;;;N;;;;03C8;
+03A9;GREEK CAPITAL LETTER OMEGA;Lu;0;L;;;;;N;;;;03C9;
+03AA;GREEK CAPITAL LETTER IOTA WITH DIALYTIKA;Lu;0;L;0399 0308;;;;N;GREEK CAPITAL LETTER IOTA DIAERESIS;;;03CA;
+03AB;GREEK CAPITAL LETTER UPSILON WITH DIALYTIKA;Lu;0;L;03A5 0308;;;;N;GREEK CAPITAL LETTER UPSILON DIAERESIS;;;03CB;
+03AC;GREEK SMALL LETTER ALPHA WITH TONOS;Ll;0;L;03B1 0301;;;;N;GREEK SMALL LETTER ALPHA TONOS;;0386;;0386
+03AD;GREEK SMALL LETTER EPSILON WITH TONOS;Ll;0;L;03B5 0301;;;;N;GREEK SMALL LETTER EPSILON TONOS;;0388;;0388
+03AE;GREEK SMALL LETTER ETA WITH TONOS;Ll;0;L;03B7 0301;;;;N;GREEK SMALL LETTER ETA TONOS;;0389;;0389
+03AF;GREEK SMALL LETTER IOTA WITH TONOS;Ll;0;L;03B9 0301;;;;N;GREEK SMALL LETTER IOTA TONOS;;038A;;038A
+03B0;GREEK SMALL LETTER UPSILON WITH DIALYTIKA AND TONOS;Ll;0;L;03CB 0301;;;;N;GREEK SMALL LETTER UPSILON DIAERESIS TONOS;;;;
+03B1;GREEK SMALL LETTER ALPHA;Ll;0;L;;;;;N;;;0391;;0391
+03B2;GREEK SMALL LETTER BETA;Ll;0;L;;;;;N;;;0392;;0392
+03B3;GREEK SMALL LETTER GAMMA;Ll;0;L;;;;;N;;;0393;;0393
+03B4;GREEK SMALL LETTER DELTA;Ll;0;L;;;;;N;;;0394;;0394
+03B5;GREEK SMALL LETTER EPSILON;Ll;0;L;;;;;N;;;0395;;0395
+03B6;GREEK SMALL LETTER ZETA;Ll;0;L;;;;;N;;;0396;;0396
+03B7;GREEK SMALL LETTER ETA;Ll;0;L;;;;;N;;;0397;;0397
+03B8;GREEK SMALL LETTER THETA;Ll;0;L;;;;;N;;;0398;;0398
+03B9;GREEK SMALL LETTER IOTA;Ll;0;L;;;;;N;;;0399;;0399
+03BA;GREEK SMALL LETTER KAPPA;Ll;0;L;;;;;N;;;039A;;039A
+03BB;GREEK SMALL LETTER LAMDA;Ll;0;L;;;;;N;GREEK SMALL LETTER LAMBDA;;039B;;039B
+03BC;GREEK SMALL LETTER MU;Ll;0;L;;;;;N;;;039C;;039C
+03BD;GREEK SMALL LETTER NU;Ll;0;L;;;;;N;;;039D;;039D
+03BE;GREEK SMALL LETTER XI;Ll;0;L;;;;;N;;;039E;;039E
+03BF;GREEK SMALL LETTER OMICRON;Ll;0;L;;;;;N;;;039F;;039F
+03C0;GREEK SMALL LETTER PI;Ll;0;L;;;;;N;;;03A0;;03A0
+03C1;GREEK SMALL LETTER RHO;Ll;0;L;;;;;N;;;03A1;;03A1
+03C2;GREEK SMALL LETTER FINAL SIGMA;Ll;0;L;;;;;N;;;03A3;;03A3
+03C3;GREEK SMALL LETTER SIGMA;Ll;0;L;;;;;N;;;03A3;;03A3
+03C4;GREEK SMALL LETTER TAU;Ll;0;L;;;;;N;;;03A4;;03A4
+03C5;GREEK SMALL LETTER UPSILON;Ll;0;L;;;;;N;;;03A5;;03A5
+03C6;GREEK SMALL LETTER PHI;Ll;0;L;;;;;N;;;03A6;;03A6
+03C7;GREEK SMALL LETTER CHI;Ll;0;L;;;;;N;;;03A7;;03A7
+03C8;GREEK SMALL LETTER PSI;Ll;0;L;;;;;N;;;03A8;;03A8
+03C9;GREEK SMALL LETTER OMEGA;Ll;0;L;;;;;N;;;03A9;;03A9
+03CA;GREEK SMALL LETTER IOTA WITH DIALYTIKA;Ll;0;L;03B9 0308;;;;N;GREEK SMALL LETTER IOTA DIAERESIS;;03AA;;03AA
+03CB;GREEK SMALL LETTER UPSILON WITH DIALYTIKA;Ll;0;L;03C5 0308;;;;N;GREEK SMALL LETTER UPSILON DIAERESIS;;03AB;;03AB
+03CC;GREEK SMALL LETTER OMICRON WITH TONOS;Ll;0;L;03BF 0301;;;;N;GREEK SMALL LETTER OMICRON TONOS;;038C;;038C
+03CD;GREEK SMALL LETTER UPSILON WITH TONOS;Ll;0;L;03C5 0301;;;;N;GREEK SMALL LETTER UPSILON TONOS;;038E;;038E
+03CE;GREEK SMALL LETTER OMEGA WITH TONOS;Ll;0;L;03C9 0301;;;;N;GREEK SMALL LETTER OMEGA TONOS;;038F;;038F
+03CF;GREEK CAPITAL KAI SYMBOL;Lu;0;L;;;;;N;;;;03D7;
+03D0;GREEK BETA SYMBOL;Ll;0;L;<compat> 03B2;;;;N;GREEK SMALL LETTER CURLED BETA;;0392;;0392
+03D1;GREEK THETA SYMBOL;Ll;0;L;<compat> 03B8;;;;N;GREEK SMALL LETTER SCRIPT THETA;;0398;;0398
+03D2;GREEK UPSILON WITH HOOK SYMBOL;Lu;0;L;<compat> 03A5;;;;N;GREEK CAPITAL LETTER UPSILON HOOK;;;;
+03D3;GREEK UPSILON WITH ACUTE AND HOOK SYMBOL;Lu;0;L;03D2 0301;;;;N;GREEK CAPITAL LETTER UPSILON HOOK TONOS;;;;
+03D4;GREEK UPSILON WITH DIAERESIS AND HOOK SYMBOL;Lu;0;L;03D2 0308;;;;N;GREEK CAPITAL LETTER UPSILON HOOK DIAERESIS;;;;
+03D5;GREEK PHI SYMBOL;Ll;0;L;<compat> 03C6;;;;N;GREEK SMALL LETTER SCRIPT PHI;;03A6;;03A6
+03D6;GREEK PI SYMBOL;Ll;0;L;<compat> 03C0;;;;N;GREEK SMALL LETTER OMEGA PI;;03A0;;03A0
+03D7;GREEK KAI SYMBOL;Ll;0;L;;;;;N;;;03CF;;03CF
+03D8;GREEK LETTER ARCHAIC KOPPA;Lu;0;L;;;;;N;;;;03D9;
+03D9;GREEK SMALL LETTER ARCHAIC KOPPA;Ll;0;L;;;;;N;;;03D8;;03D8
+03DA;GREEK LETTER STIGMA;Lu;0;L;;;;;N;GREEK CAPITAL LETTER STIGMA;;;03DB;
+03DB;GREEK SMALL LETTER STIGMA;Ll;0;L;;;;;N;;;03DA;;03DA
+03DC;GREEK LETTER DIGAMMA;Lu;0;L;;;;;N;GREEK CAPITAL LETTER DIGAMMA;;;03DD;
+03DD;GREEK SMALL LETTER DIGAMMA;Ll;0;L;;;;;N;;;03DC;;03DC
+03DE;GREEK LETTER KOPPA;Lu;0;L;;;;;N;GREEK CAPITAL LETTER KOPPA;;;03DF;
+03DF;GREEK SMALL LETTER KOPPA;Ll;0;L;;;;;N;;;03DE;;03DE
+03E0;GREEK LETTER SAMPI;Lu;0;L;;;;;N;GREEK CAPITAL LETTER SAMPI;;;03E1;
+03E1;GREEK SMALL LETTER SAMPI;Ll;0;L;;;;;N;;;03E0;;03E0
+03E2;COPTIC CAPITAL LETTER SHEI;Lu;0;L;;;;;N;GREEK CAPITAL LETTER SHEI;;;03E3;
+03E3;COPTIC SMALL LETTER SHEI;Ll;0;L;;;;;N;GREEK SMALL LETTER SHEI;;03E2;;03E2
+03E4;COPTIC CAPITAL LETTER FEI;Lu;0;L;;;;;N;GREEK CAPITAL LETTER FEI;;;03E5;
+03E5;COPTIC SMALL LETTER FEI;Ll;0;L;;;;;N;GREEK SMALL LETTER FEI;;03E4;;03E4
+03E6;COPTIC CAPITAL LETTER KHEI;Lu;0;L;;;;;N;GREEK CAPITAL LETTER KHEI;;;03E7;
+03E7;COPTIC SMALL LETTER KHEI;Ll;0;L;;;;;N;GREEK SMALL LETTER KHEI;;03E6;;03E6
+03E8;COPTIC CAPITAL LETTER HORI;Lu;0;L;;;;;N;GREEK CAPITAL LETTER HORI;;;03E9;
+03E9;COPTIC SMALL LETTER HORI;Ll;0;L;;;;;N;GREEK SMALL LETTER HORI;;03E8;;03E8
+03EA;COPTIC CAPITAL LETTER GANGIA;Lu;0;L;;;;;N;GREEK CAPITAL LETTER GANGIA;;;03EB;
+03EB;COPTIC SMALL LETTER GANGIA;Ll;0;L;;;;;N;GREEK SMALL LETTER GANGIA;;03EA;;03EA
+03EC;COPTIC CAPITAL LETTER SHIMA;Lu;0;L;;;;;N;GREEK CAPITAL LETTER SHIMA;;;03ED;
+03ED;COPTIC SMALL LETTER SHIMA;Ll;0;L;;;;;N;GREEK SMALL LETTER SHIMA;;03EC;;03EC
+03EE;COPTIC CAPITAL LETTER DEI;Lu;0;L;;;;;N;GREEK CAPITAL LETTER DEI;;;03EF;
+03EF;COPTIC SMALL LETTER DEI;Ll;0;L;;;;;N;GREEK SMALL LETTER DEI;;03EE;;03EE
+03F0;GREEK KAPPA SYMBOL;Ll;0;L;<compat> 03BA;;;;N;GREEK SMALL LETTER SCRIPT KAPPA;;039A;;039A
+03F1;GREEK RHO SYMBOL;Ll;0;L;<compat> 03C1;;;;N;GREEK SMALL LETTER TAILED RHO;;03A1;;03A1
+03F2;GREEK LUNATE SIGMA SYMBOL;Ll;0;L;<compat> 03C2;;;;N;GREEK SMALL LETTER LUNATE SIGMA;;03F9;;03F9
+03F3;GREEK LETTER YOT;Ll;0;L;;;;;N;;;037F;;037F
+03F4;GREEK CAPITAL THETA SYMBOL;Lu;0;L;<compat> 0398;;;;N;;;;03B8;
+03F5;GREEK LUNATE EPSILON SYMBOL;Ll;0;L;<compat> 03B5;;;;N;;;0395;;0395
+03F6;GREEK REVERSED LUNATE EPSILON SYMBOL;Sm;0;ON;;;;;N;;;;;
+03F7;GREEK CAPITAL LETTER SHO;Lu;0;L;;;;;N;;;;03F8;
+03F8;GREEK SMALL LETTER SHO;Ll;0;L;;;;;N;;;03F7;;03F7
+03F9;GREEK CAPITAL LUNATE SIGMA SYMBOL;Lu;0;L;<compat> 03A3;;;;N;;;;03F2;
+03FA;GREEK CAPITAL LETTER SAN;Lu;0;L;;;;;N;;;;03FB;
+03FB;GREEK SMALL LETTER SAN;Ll;0;L;;;;;N;;;03FA;;03FA
+03FC;GREEK RHO WITH STROKE SYMBOL;Ll;0;L;;;;;N;;;;;
+03FD;GREEK CAPITAL REVERSED LUNATE SIGMA SYMBOL;Lu;0;L;;;;;N;;;;037B;
+03FE;GREEK CAPITAL DOTTED LUNATE SIGMA SYMBOL;Lu;0;L;;;;;N;;;;037C;
+03FF;GREEK CAPITAL REVERSED DOTTED LUNATE SIGMA SYMBOL;Lu;0;L;;;;;N;;;;037D;
+0400;CYRILLIC CAPITAL LETTER IE WITH GRAVE;Lu;0;L;0415 0300;;;;N;;;;0450;
+0401;CYRILLIC CAPITAL LETTER IO;Lu;0;L;0415 0308;;;;N;;;;0451;
+0402;CYRILLIC CAPITAL LETTER DJE;Lu;0;L;;;;;N;;;;0452;
+0403;CYRILLIC CAPITAL LETTER GJE;Lu;0;L;0413 0301;;;;N;;;;0453;
+0404;CYRILLIC CAPITAL LETTER UKRAINIAN IE;Lu;0;L;;;;;N;CYRILLIC CAPITAL LETTER E;;;0454;
+0405;CYRILLIC CAPITAL LETTER DZE;Lu;0;L;;;;;N;;;;0455;
+0406;CYRILLIC CAPITAL LETTER BYELORUSSIAN-UKRAINIAN I;Lu;0;L;;;;;N;CYRILLIC CAPITAL LETTER I;;;0456;
+0407;CYRILLIC CAPITAL LETTER YI;Lu;0;L;0406 0308;;;;N;;;;0457;
+0408;CYRILLIC CAPITAL LETTER JE;Lu;0;L;;;;;N;;;;0458;
+0409;CYRILLIC CAPITAL LETTER LJE;Lu;0;L;;;;;N;;;;0459;
+040A;CYRILLIC CAPITAL LETTER NJE;Lu;0;L;;;;;N;;;;045A;
+040B;CYRILLIC CAPITAL LETTER TSHE;Lu;0;L;;;;;N;;;;045B;
+040C;CYRILLIC CAPITAL LETTER KJE;Lu;0;L;041A 0301;;;;N;;;;045C;
+040D;CYRILLIC CAPITAL LETTER I WITH GRAVE;Lu;0;L;0418 0300;;;;N;;;;045D;
+040E;CYRILLIC CAPITAL LETTER SHORT U;Lu;0;L;0423 0306;;;;N;;;;045E;
+040F;CYRILLIC CAPITAL LETTER DZHE;Lu;0;L;;;;;N;;;;045F;
+0410;CYRILLIC CAPITAL LETTER A;Lu;0;L;;;;;N;;;;0430;
+0411;CYRILLIC CAPITAL LETTER BE;Lu;0;L;;;;;N;;;;0431;
+0412;CYRILLIC CAPITAL LETTER VE;Lu;0;L;;;;;N;;;;0432;
+0413;CYRILLIC CAPITAL LETTER GHE;Lu;0;L;;;;;N;CYRILLIC CAPITAL LETTER GE;;;0433;
+0414;CYRILLIC CAPITAL LETTER DE;Lu;0;L;;;;;N;;;;0434;
+0415;CYRILLIC CAPITAL LETTER IE;Lu;0;L;;;;;N;;;;0435;
+0416;CYRILLIC CAPITAL LETTER ZHE;Lu;0;L;;;;;N;;;;0436;
+0417;CYRILLIC CAPITAL LETTER ZE;Lu;0;L;;;;;N;;;;0437;
+0418;CYRILLIC CAPITAL LETTER I;Lu;0;L;;;;;N;CYRILLIC CAPITAL LETTER II;;;0438;
+0419;CYRILLIC CAPITAL LETTER SHORT I;Lu;0;L;0418 0306;;;;N;CYRILLIC CAPITAL LETTER SHORT II;;;0439;
+041A;CYRILLIC CAPITAL LETTER KA;Lu;0;L;;;;;N;;;;043A;
+041B;CYRILLIC CAPITAL LETTER EL;Lu;0;L;;;;;N;;;;043B;
+041C;CYRILLIC CAPITAL LETTER EM;Lu;0;L;;;;;N;;;;043C;
+041D;CYRILLIC CAPITAL LETTER EN;Lu;0;L;;;;;N;;;;043D;
+041E;CYRILLIC CAPITAL LETTER O;Lu;0;L;;;;;N;;;;043E;
+041F;CYRILLIC CAPITAL LETTER PE;Lu;0;L;;;;;N;;;;043F;
+0420;CYRILLIC CAPITAL LETTER ER;Lu;0;L;;;;;N;;;;0440;
+0421;CYRILLIC CAPITAL LETTER ES;Lu;0;L;;;;;N;;;;0441;
+0422;CYRILLIC CAPITAL LETTER TE;Lu;0;L;;;;;N;;;;0442;
+0423;CYRILLIC CAPITAL LETTER U;Lu;0;L;;;;;N;;;;0443;
+0424;CYRILLIC CAPITAL LETTER EF;Lu;0;L;;;;;N;;;;0444;
+0425;CYRILLIC CAPITAL LETTER HA;Lu;0;L;;;;;N;CYRILLIC CAPITAL LETTER KHA;;;0445;
+0426;CYRILLIC CAPITAL LETTER TSE;Lu;0;L;;;;;N;;;;0446;
+0427;CYRILLIC CAPITAL LETTER CHE;Lu;0;L;;;;;N;;;;0447;
+0428;CYRILLIC CAPITAL LETTER SHA;Lu;0;L;;;;;N;;;;0448;
+0429;CYRILLIC CAPITAL LETTER SHCHA;Lu;0;L;;;;;N;;;;0449;
+042A;CYRILLIC CAPITAL LETTER HARD SIGN;Lu;0;L;;;;;N;;;;044A;
+042B;CYRILLIC CAPITAL LETTER YERU;Lu;0;L;;;;;N;CYRILLIC CAPITAL LETTER YERI;;;044B;
+042C;CYRILLIC CAPITAL LETTER SOFT SIGN;Lu;0;L;;;;;N;;;;044C;
+042D;CYRILLIC CAPITAL LETTER E;Lu;0;L;;;;;N;CYRILLIC CAPITAL LETTER REVERSED E;;;044D;
+042E;CYRILLIC CAPITAL LETTER YU;Lu;0;L;;;;;N;CYRILLIC CAPITAL LETTER IU;;;044E;
+042F;CYRILLIC CAPITAL LETTER YA;Lu;0;L;;;;;N;CYRILLIC CAPITAL LETTER IA;;;044F;
+0430;CYRILLIC SMALL LETTER A;Ll;0;L;;;;;N;;;0410;;0410
+0431;CYRILLIC SMALL LETTER BE;Ll;0;L;;;;;N;;;0411;;0411
+0432;CYRILLIC SMALL LETTER VE;Ll;0;L;;;;;N;;;0412;;0412
+0433;CYRILLIC SMALL LETTER GHE;Ll;0;L;;;;;N;CYRILLIC SMALL LETTER GE;;0413;;0413
+0434;CYRILLIC SMALL LETTER DE;Ll;0;L;;;;;N;;;0414;;0414
+0435;CYRILLIC SMALL LETTER IE;Ll;0;L;;;;;N;;;0415;;0415
+0436;CYRILLIC SMALL LETTER ZHE;Ll;0;L;;;;;N;;;0416;;0416
+0437;CYRILLIC SMALL LETTER ZE;Ll;0;L;;;;;N;;;0417;;0417
+0438;CYRILLIC SMALL LETTER I;Ll;0;L;;;;;N;CYRILLIC SMALL LETTER II;;0418;;0418
+0439;CYRILLIC SMALL LETTER SHORT I;Ll;0;L;0438 0306;;;;N;CYRILLIC SMALL LETTER SHORT II;;0419;;0419
+043A;CYRILLIC SMALL LETTER KA;Ll;0;L;;;;;N;;;041A;;041A
+043B;CYRILLIC SMALL LETTER EL;Ll;0;L;;;;;N;;;041B;;041B
+043C;CYRILLIC SMALL LETTER EM;Ll;0;L;;;;;N;;;041C;;041C
+043D;CYRILLIC SMALL LETTER EN;Ll;0;L;;;;;N;;;041D;;041D
+043E;CYRILLIC SMALL LETTER O;Ll;0;L;;;;;N;;;041E;;041E
+043F;CYRILLIC SMALL LETTER PE;Ll;0;L;;;;;N;;;041F;;041F
+0440;CYRILLIC SMALL LETTER ER;Ll;0;L;;;;;N;;;0420;;0420
+0441;CYRILLIC SMALL LETTER ES;Ll;0;L;;;;;N;;;0421;;0421
+0442;CYRILLIC SMALL LETTER TE;Ll;0;L;;;;;N;;;0422;;0422
+0443;CYRILLIC SMALL LETTER U;Ll;0;L;;;;;N;;;0423;;0423
+0444;CYRILLIC SMALL LETTER EF;Ll;0;L;;;;;N;;;0424;;0424
+0445;CYRILLIC SMALL LETTER HA;Ll;0;L;;;;;N;CYRILLIC SMALL LETTER KHA;;0425;;0425
+0446;CYRILLIC SMALL LETTER TSE;Ll;0;L;;;;;N;;;0426;;0426
+0447;CYRILLIC SMALL LETTER CHE;Ll;0;L;;;;;N;;;0427;;0427
+0448;CYRILLIC SMALL LETTER SHA;Ll;0;L;;;;;N;;;0428;;0428
+0449;CYRILLIC SMALL LETTER SHCHA;Ll;0;L;;;;;N;;;0429;;0429
+044A;CYRILLIC SMALL LETTER HARD SIGN;Ll;0;L;;;;;N;;;042A;;042A
+044B;CYRILLIC SMALL LETTER YERU;Ll;0;L;;;;;N;CYRILLIC SMALL LETTER YERI;;042B;;042B
+044C;CYRILLIC SMALL LETTER SOFT SIGN;Ll;0;L;;;;;N;;;042C;;042C
+044D;CYRILLIC SMALL LETTER E;Ll;0;L;;;;;N;CYRILLIC SMALL LETTER REVERSED E;;042D;;042D
+044E;CYRILLIC SMALL LETTER YU;Ll;0;L;;;;;N;CYRILLIC SMALL LETTER IU;;042E;;042E
+044F;CYRILLIC SMALL LETTER YA;Ll;0;L;;;;;N;CYRILLIC SMALL LETTER IA;;042F;;042F
+0450;CYRILLIC SMALL LETTER IE WITH GRAVE;Ll;0;L;0435 0300;;;;N;;;0400;;0400
+0451;CYRILLIC SMALL LETTER IO;Ll;0;L;0435 0308;;;;N;;;0401;;0401
+0452;CYRILLIC SMALL LETTER DJE;Ll;0;L;;;;;N;;;0402;;0402
+0453;CYRILLIC SMALL LETTER GJE;Ll;0;L;0433 0301;;;;N;;;0403;;0403
+0454;CYRILLIC SMALL LETTER UKRAINIAN IE;Ll;0;L;;;;;N;CYRILLIC SMALL LETTER E;;0404;;0404
+0455;CYRILLIC SMALL LETTER DZE;Ll;0;L;;;;;N;;;0405;;0405
+0456;CYRILLIC SMALL LETTER BYELORUSSIAN-UKRAINIAN I;Ll;0;L;;;;;N;CYRILLIC SMALL LETTER I;;0406;;0406
+0457;CYRILLIC SMALL LETTER YI;Ll;0;L;0456 0308;;;;N;;;0407;;0407
+0458;CYRILLIC SMALL LETTER JE;Ll;0;L;;;;;N;;;0408;;0408
+0459;CYRILLIC SMALL LETTER LJE;Ll;0;L;;;;;N;;;0409;;0409
+045A;CYRILLIC SMALL LETTER NJE;Ll;0;L;;;;;N;;;040A;;040A
+045B;CYRILLIC SMALL LETTER TSHE;Ll;0;L;;;;;N;;;040B;;040B
+045C;CYRILLIC SMALL LETTER KJE;Ll;0;L;043A 0301;;;;N;;;040C;;040C
+045D;CYRILLIC SMALL LETTER I WITH GRAVE;Ll;0;L;0438 0300;;;;N;;;040D;;040D
+045E;CYRILLIC SMALL LETTER SHORT U;Ll;0;L;0443 0306;;;;N;;;040E;;040E
+045F;CYRILLIC SMALL LETTER DZHE;Ll;0;L;;;;;N;;;040F;;040F
+0460;CYRILLIC CAPITAL LETTER OMEGA;Lu;0;L;;;;;N;;;;0461;
+0461;CYRILLIC SMALL LETTER OMEGA;Ll;0;L;;;;;N;;;0460;;0460
+0462;CYRILLIC CAPITAL LETTER YAT;Lu;0;L;;;;;N;;;;0463;
+0463;CYRILLIC SMALL LETTER YAT;Ll;0;L;;;;;N;;;0462;;0462
+0464;CYRILLIC CAPITAL LETTER IOTIFIED E;Lu;0;L;;;;;N;;;;0465;
+0465;CYRILLIC SMALL LETTER IOTIFIED E;Ll;0;L;;;;;N;;;0464;;0464
+0466;CYRILLIC CAPITAL LETTER LITTLE YUS;Lu;0;L;;;;;N;;;;0467;
+0467;CYRILLIC SMALL LETTER LITTLE YUS;Ll;0;L;;;;;N;;;0466;;0466
+0468;CYRILLIC CAPITAL LETTER IOTIFIED LITTLE YUS;Lu;0;L;;;;;N;;;;0469;
+0469;CYRILLIC SMALL LETTER IOTIFIED LITTLE YUS;Ll;0;L;;;;;N;;;0468;;0468
+046A;CYRILLIC CAPITAL LETTER BIG YUS;Lu;0;L;;;;;N;;;;046B;
+046B;CYRILLIC SMALL LETTER BIG YUS;Ll;0;L;;;;;N;;;046A;;046A
+046C;CYRILLIC CAPITAL LETTER IOTIFIED BIG YUS;Lu;0;L;;;;;N;;;;046D;
+046D;CYRILLIC SMALL LETTER IOTIFIED BIG YUS;Ll;0;L;;;;;N;;;046C;;046C
+046E;CYRILLIC CAPITAL LETTER KSI;Lu;0;L;;;;;N;;;;046F;
+046F;CYRILLIC SMALL LETTER KSI;Ll;0;L;;;;;N;;;046E;;046E
+0470;CYRILLIC CAPITAL LETTER PSI;Lu;0;L;;;;;N;;;;0471;
+0471;CYRILLIC SMALL LETTER PSI;Ll;0;L;;;;;N;;;0470;;0470
+0472;CYRILLIC CAPITAL LETTER FITA;Lu;0;L;;;;;N;;;;0473;
+0473;CYRILLIC SMALL LETTER FITA;Ll;0;L;;;;;N;;;0472;;0472
+0474;CYRILLIC CAPITAL LETTER IZHITSA;Lu;0;L;;;;;N;;;;0475;
+0475;CYRILLIC SMALL LETTER IZHITSA;Ll;0;L;;;;;N;;;0474;;0474
+0476;CYRILLIC CAPITAL LETTER IZHITSA WITH DOUBLE GRAVE ACCENT;Lu;0;L;0474 030F;;;;N;CYRILLIC CAPITAL LETTER IZHITSA DOUBLE GRAVE;;;0477;
+0477;CYRILLIC SMALL LETTER IZHITSA WITH DOUBLE GRAVE ACCENT;Ll;0;L;0475 030F;;;;N;CYRILLIC SMALL LETTER IZHITSA DOUBLE GRAVE;;0476;;0476
+0478;CYRILLIC CAPITAL LETTER UK;Lu;0;L;;;;;N;CYRILLIC CAPITAL LETTER UK DIGRAPH;;;0479;
+0479;CYRILLIC SMALL LETTER UK;Ll;0;L;;;;;N;CYRILLIC SMALL LETTER UK DIGRAPH;;0478;;0478
+047A;CYRILLIC CAPITAL LETTER ROUND OMEGA;Lu;0;L;;;;;N;;;;047B;
+047B;CYRILLIC SMALL LETTER ROUND OMEGA;Ll;0;L;;;;;N;;;047A;;047A
+047C;CYRILLIC CAPITAL LETTER OMEGA WITH TITLO;Lu;0;L;;;;;N;CYRILLIC CAPITAL LETTER OMEGA TITLO;;;047D;
+047D;CYRILLIC SMALL LETTER OMEGA WITH TITLO;Ll;0;L;;;;;N;CYRILLIC SMALL LETTER OMEGA TITLO;;047C;;047C
+047E;CYRILLIC CAPITAL LETTER OT;Lu;0;L;;;;;N;;;;047F;
+047F;CYRILLIC SMALL LETTER OT;Ll;0;L;;;;;N;;;047E;;047E
+0480;CYRILLIC CAPITAL LETTER KOPPA;Lu;0;L;;;;;N;;;;0481;
+0481;CYRILLIC SMALL LETTER KOPPA;Ll;0;L;;;;;N;;;0480;;0480
+0482;CYRILLIC THOUSANDS SIGN;So;0;L;;;;;N;;;;;
+0483;COMBINING CYRILLIC TITLO;Mn;230;NSM;;;;;N;CYRILLIC NON-SPACING TITLO;;;;
+0484;COMBINING CYRILLIC PALATALIZATION;Mn;230;NSM;;;;;N;CYRILLIC NON-SPACING PALATALIZATION;;;;
+0485;COMBINING CYRILLIC DASIA PNEUMATA;Mn;230;NSM;;;;;N;CYRILLIC NON-SPACING DASIA PNEUMATA;;;;
+0486;COMBINING CYRILLIC PSILI PNEUMATA;Mn;230;NSM;;;;;N;CYRILLIC NON-SPACING PSILI PNEUMATA;;;;
+0487;COMBINING CYRILLIC POKRYTIE;Mn;230;NSM;;;;;N;;;;;
+0488;COMBINING CYRILLIC HUNDRED THOUSANDS SIGN;Me;0;NSM;;;;;N;;;;;
+0489;COMBINING CYRILLIC MILLIONS SIGN;Me;0;NSM;;;;;N;;;;;
+048A;CYRILLIC CAPITAL LETTER SHORT I WITH TAIL;Lu;0;L;;;;;N;;;;048B;
+048B;CYRILLIC SMALL LETTER SHORT I WITH TAIL;Ll;0;L;;;;;N;;;048A;;048A
+048C;CYRILLIC CAPITAL LETTER SEMISOFT SIGN;Lu;0;L;;;;;N;;;;048D;
+048D;CYRILLIC SMALL LETTER SEMISOFT SIGN;Ll;0;L;;;;;N;;;048C;;048C
+048E;CYRILLIC CAPITAL LETTER ER WITH TICK;Lu;0;L;;;;;N;;;;048F;
+048F;CYRILLIC SMALL LETTER ER WITH TICK;Ll;0;L;;;;;N;;;048E;;048E
+0490;CYRILLIC CAPITAL LETTER GHE WITH UPTURN;Lu;0;L;;;;;N;CYRILLIC CAPITAL LETTER GE WITH UPTURN;;;0491;
+0491;CYRILLIC SMALL LETTER GHE WITH UPTURN;Ll;0;L;;;;;N;CYRILLIC SMALL LETTER GE WITH UPTURN;;0490;;0490
+0492;CYRILLIC CAPITAL LETTER GHE WITH STROKE;Lu;0;L;;;;;N;CYRILLIC CAPITAL LETTER GE BAR;;;0493;
+0493;CYRILLIC SMALL LETTER GHE WITH STROKE;Ll;0;L;;;;;N;CYRILLIC SMALL LETTER GE BAR;;0492;;0492
+0494;CYRILLIC CAPITAL LETTER GHE WITH MIDDLE HOOK;Lu;0;L;;;;;N;CYRILLIC CAPITAL LETTER GE HOOK;;;0495;
+0495;CYRILLIC SMALL LETTER GHE WITH MIDDLE HOOK;Ll;0;L;;;;;N;CYRILLIC SMALL LETTER GE HOOK;;0494;;0494
+0496;CYRILLIC CAPITAL LETTER ZHE WITH DESCENDER;Lu;0;L;;;;;N;CYRILLIC CAPITAL LETTER ZHE WITH RIGHT DESCENDER;;;0497;
+0497;CYRILLIC SMALL LETTER ZHE WITH DESCENDER;Ll;0;L;;;;;N;CYRILLIC SMALL LETTER ZHE WITH RIGHT DESCENDER;;0496;;0496
+0498;CYRILLIC CAPITAL LETTER ZE WITH DESCENDER;Lu;0;L;;;;;N;CYRILLIC CAPITAL LETTER ZE CEDILLA;;;0499;
+0499;CYRILLIC SMALL LETTER ZE WITH DESCENDER;Ll;0;L;;;;;N;CYRILLIC SMALL LETTER ZE CEDILLA;;0498;;0498
+049A;CYRILLIC CAPITAL LETTER KA WITH DESCENDER;Lu;0;L;;;;;N;CYRILLIC CAPITAL LETTER KA WITH RIGHT DESCENDER;;;049B;
+049B;CYRILLIC SMALL LETTER KA WITH DESCENDER;Ll;0;L;;;;;N;CYRILLIC SMALL LETTER KA WITH RIGHT DESCENDER;;049A;;049A
+049C;CYRILLIC CAPITAL LETTER KA WITH VERTICAL STROKE;Lu;0;L;;;;;N;CYRILLIC CAPITAL LETTER KA VERTICAL BAR;;;049D;
+049D;CYRILLIC SMALL LETTER KA WITH VERTICAL STROKE;Ll;0;L;;;;;N;CYRILLIC SMALL LETTER KA VERTICAL BAR;;049C;;049C
+049E;CYRILLIC CAPITAL LETTER KA WITH STROKE;Lu;0;L;;;;;N;CYRILLIC CAPITAL LETTER KA BAR;;;049F;
+049F;CYRILLIC SMALL LETTER KA WITH STROKE;Ll;0;L;;;;;N;CYRILLIC SMALL LETTER KA BAR;;049E;;049E
+04A0;CYRILLIC CAPITAL LETTER BASHKIR KA;Lu;0;L;;;;;N;CYRILLIC CAPITAL LETTER REVERSED GE KA;;;04A1;
+04A1;CYRILLIC SMALL LETTER BASHKIR KA;Ll;0;L;;;;;N;CYRILLIC SMALL LETTER REVERSED GE KA;;04A0;;04A0
+04A2;CYRILLIC CAPITAL LETTER EN WITH DESCENDER;Lu;0;L;;;;;N;CYRILLIC CAPITAL LETTER EN WITH RIGHT DESCENDER;;;04A3;
+04A3;CYRILLIC SMALL LETTER EN WITH DESCENDER;Ll;0;L;;;;;N;CYRILLIC SMALL LETTER EN WITH RIGHT DESCENDER;;04A2;;04A2
+04A4;CYRILLIC CAPITAL LIGATURE EN GHE;Lu;0;L;;;;;N;CYRILLIC CAPITAL LETTER EN GE;;;04A5;
+04A5;CYRILLIC SMALL LIGATURE EN GHE;Ll;0;L;;;;;N;CYRILLIC SMALL LETTER EN GE;;04A4;;04A4
+04A6;CYRILLIC CAPITAL LETTER PE WITH MIDDLE HOOK;Lu;0;L;;;;;N;CYRILLIC CAPITAL LETTER PE HOOK;;;04A7;
+04A7;CYRILLIC SMALL LETTER PE WITH MIDDLE HOOK;Ll;0;L;;;;;N;CYRILLIC SMALL LETTER PE HOOK;;04A6;;04A6
+04A8;CYRILLIC CAPITAL LETTER ABKHASIAN HA;Lu;0;L;;;;;N;CYRILLIC CAPITAL LETTER O HOOK;;;04A9;
+04A9;CYRILLIC SMALL LETTER ABKHASIAN HA;Ll;0;L;;;;;N;CYRILLIC SMALL LETTER O HOOK;;04A8;;04A8
+04AA;CYRILLIC CAPITAL LETTER ES WITH DESCENDER;Lu;0;L;;;;;N;CYRILLIC CAPITAL LETTER ES CEDILLA;;;04AB;
+04AB;CYRILLIC SMALL LETTER ES WITH DESCENDER;Ll;0;L;;;;;N;CYRILLIC SMALL LETTER ES CEDILLA;;04AA;;04AA
+04AC;CYRILLIC CAPITAL LETTER TE WITH DESCENDER;Lu;0;L;;;;;N;CYRILLIC CAPITAL LETTER TE WITH RIGHT DESCENDER;;;04AD;
+04AD;CYRILLIC SMALL LETTER TE WITH DESCENDER;Ll;0;L;;;;;N;CYRILLIC SMALL LETTER TE WITH RIGHT DESCENDER;;04AC;;04AC
+04AE;CYRILLIC CAPITAL LETTER STRAIGHT U;Lu;0;L;;;;;N;;;;04AF;
+04AF;CYRILLIC SMALL LETTER STRAIGHT U;Ll;0;L;;;;;N;;;04AE;;04AE
+04B0;CYRILLIC CAPITAL LETTER STRAIGHT U WITH STROKE;Lu;0;L;;;;;N;CYRILLIC CAPITAL LETTER STRAIGHT U BAR;;;04B1;
+04B1;CYRILLIC SMALL LETTER STRAIGHT U WITH STROKE;Ll;0;L;;;;;N;CYRILLIC SMALL LETTER STRAIGHT U BAR;;04B0;;04B0
+04B2;CYRILLIC CAPITAL LETTER HA WITH DESCENDER;Lu;0;L;;;;;N;CYRILLIC CAPITAL LETTER KHA WITH RIGHT DESCENDER;;;04B3;
+04B3;CYRILLIC SMALL LETTER HA WITH DESCENDER;Ll;0;L;;;;;N;CYRILLIC SMALL LETTER KHA WITH RIGHT DESCENDER;;04B2;;04B2
+04B4;CYRILLIC CAPITAL LIGATURE TE TSE;Lu;0;L;;;;;N;CYRILLIC CAPITAL LETTER TE TSE;;;04B5;
+04B5;CYRILLIC SMALL LIGATURE TE TSE;Ll;0;L;;;;;N;CYRILLIC SMALL LETTER TE TSE;;04B4;;04B4
+04B6;CYRILLIC CAPITAL LETTER CHE WITH DESCENDER;Lu;0;L;;;;;N;CYRILLIC CAPITAL LETTER CHE WITH RIGHT DESCENDER;;;04B7;
+04B7;CYRILLIC SMALL LETTER CHE WITH DESCENDER;Ll;0;L;;;;;N;CYRILLIC SMALL LETTER CHE WITH RIGHT DESCENDER;;04B6;;04B6
+04B8;CYRILLIC CAPITAL LETTER CHE WITH VERTICAL STROKE;Lu;0;L;;;;;N;CYRILLIC CAPITAL LETTER CHE VERTICAL BAR;;;04B9;
+04B9;CYRILLIC SMALL LETTER CHE WITH VERTICAL STROKE;Ll;0;L;;;;;N;CYRILLIC SMALL LETTER CHE VERTICAL BAR;;04B8;;04B8
+04BA;CYRILLIC CAPITAL LETTER SHHA;Lu;0;L;;;;;N;CYRILLIC CAPITAL LETTER H;;;04BB;
+04BB;CYRILLIC SMALL LETTER SHHA;Ll;0;L;;;;;N;CYRILLIC SMALL LETTER H;;04BA;;04BA
+04BC;CYRILLIC CAPITAL LETTER ABKHASIAN CHE;Lu;0;L;;;;;N;CYRILLIC CAPITAL LETTER IE HOOK;;;04BD;
+04BD;CYRILLIC SMALL LETTER ABKHASIAN CHE;Ll;0;L;;;;;N;CYRILLIC SMALL LETTER IE HOOK;;04BC;;04BC
+04BE;CYRILLIC CAPITAL LETTER ABKHASIAN CHE WITH DESCENDER;Lu;0;L;;;;;N;CYRILLIC CAPITAL LETTER IE HOOK OGONEK;;;04BF;
+04BF;CYRILLIC SMALL LETTER ABKHASIAN CHE WITH DESCENDER;Ll;0;L;;;;;N;CYRILLIC SMALL LETTER IE HOOK OGONEK;;04BE;;04BE
+04C0;CYRILLIC LETTER PALOCHKA;Lu;0;L;;;;;N;CYRILLIC LETTER I;;;04CF;
+04C1;CYRILLIC CAPITAL LETTER ZHE WITH BREVE;Lu;0;L;0416 0306;;;;N;CYRILLIC CAPITAL LETTER SHORT ZHE;;;04C2;
+04C2;CYRILLIC SMALL LETTER ZHE WITH BREVE;Ll;0;L;0436 0306;;;;N;CYRILLIC SMALL LETTER SHORT ZHE;;04C1;;04C1
+04C3;CYRILLIC CAPITAL LETTER KA WITH HOOK;Lu;0;L;;;;;N;CYRILLIC CAPITAL LETTER KA HOOK;;;04C4;
+04C4;CYRILLIC SMALL LETTER KA WITH HOOK;Ll;0;L;;;;;N;CYRILLIC SMALL LETTER KA HOOK;;04C3;;04C3
+04C5;CYRILLIC CAPITAL LETTER EL WITH TAIL;Lu;0;L;;;;;N;;;;04C6;
+04C6;CYRILLIC SMALL LETTER EL WITH TAIL;Ll;0;L;;;;;N;;;04C5;;04C5
+04C7;CYRILLIC CAPITAL LETTER EN WITH HOOK;Lu;0;L;;;;;N;CYRILLIC CAPITAL LETTER EN HOOK;;;04C8;
+04C8;CYRILLIC SMALL LETTER EN WITH HOOK;Ll;0;L;;;;;N;CYRILLIC SMALL LETTER EN HOOK;;04C7;;04C7
+04C9;CYRILLIC CAPITAL LETTER EN WITH TAIL;Lu;0;L;;;;;N;;;;04CA;
+04CA;CYRILLIC SMALL LETTER EN WITH TAIL;Ll;0;L;;;;;N;;;04C9;;04C9
+04CB;CYRILLIC CAPITAL LETTER KHAKASSIAN CHE;Lu;0;L;;;;;N;CYRILLIC CAPITAL LETTER CHE WITH LEFT DESCENDER;;;04CC;
+04CC;CYRILLIC SMALL LETTER KHAKASSIAN CHE;Ll;0;L;;;;;N;CYRILLIC SMALL LETTER CHE WITH LEFT DESCENDER;;04CB;;04CB
+04CD;CYRILLIC CAPITAL LETTER EM WITH TAIL;Lu;0;L;;;;;N;;;;04CE;
+04CE;CYRILLIC SMALL LETTER EM WITH TAIL;Ll;0;L;;;;;N;;;04CD;;04CD
+04CF;CYRILLIC SMALL LETTER PALOCHKA;Ll;0;L;;;;;N;;;04C0;;04C0
+04D0;CYRILLIC CAPITAL LETTER A WITH BREVE;Lu;0;L;0410 0306;;;;N;;;;04D1;
+04D1;CYRILLIC SMALL LETTER A WITH BREVE;Ll;0;L;0430 0306;;;;N;;;04D0;;04D0
+04D2;CYRILLIC CAPITAL LETTER A WITH DIAERESIS;Lu;0;L;0410 0308;;;;N;;;;04D3;
+04D3;CYRILLIC SMALL LETTER A WITH DIAERESIS;Ll;0;L;0430 0308;;;;N;;;04D2;;04D2
+04D4;CYRILLIC CAPITAL LIGATURE A IE;Lu;0;L;;;;;N;;;;04D5;
+04D5;CYRILLIC SMALL LIGATURE A IE;Ll;0;L;;;;;N;;;04D4;;04D4
+04D6;CYRILLIC CAPITAL LETTER IE WITH BREVE;Lu;0;L;0415 0306;;;;N;;;;04D7;
+04D7;CYRILLIC SMALL LETTER IE WITH BREVE;Ll;0;L;0435 0306;;;;N;;;04D6;;04D6
+04D8;CYRILLIC CAPITAL LETTER SCHWA;Lu;0;L;;;;;N;;;;04D9;
+04D9;CYRILLIC SMALL LETTER SCHWA;Ll;0;L;;;;;N;;;04D8;;04D8
+04DA;CYRILLIC CAPITAL LETTER SCHWA WITH DIAERESIS;Lu;0;L;04D8 0308;;;;N;;;;04DB;
+04DB;CYRILLIC SMALL LETTER SCHWA WITH DIAERESIS;Ll;0;L;04D9 0308;;;;N;;;04DA;;04DA
+04DC;CYRILLIC CAPITAL LETTER ZHE WITH DIAERESIS;Lu;0;L;0416 0308;;;;N;;;;04DD;
+04DD;CYRILLIC SMALL LETTER ZHE WITH DIAERESIS;Ll;0;L;0436 0308;;;;N;;;04DC;;04DC
+04DE;CYRILLIC CAPITAL LETTER ZE WITH DIAERESIS;Lu;0;L;0417 0308;;;;N;;;;04DF;
+04DF;CYRILLIC SMALL LETTER ZE WITH DIAERESIS;Ll;0;L;0437 0308;;;;N;;;04DE;;04DE
+04E0;CYRILLIC CAPITAL LETTER ABKHASIAN DZE;Lu;0;L;;;;;N;;;;04E1;
+04E1;CYRILLIC SMALL LETTER ABKHASIAN DZE;Ll;0;L;;;;;N;;;04E0;;04E0
+04E2;CYRILLIC CAPITAL LETTER I WITH MACRON;Lu;0;L;0418 0304;;;;N;;;;04E3;
+04E3;CYRILLIC SMALL LETTER I WITH MACRON;Ll;0;L;0438 0304;;;;N;;;04E2;;04E2
+04E4;CYRILLIC CAPITAL LETTER I WITH DIAERESIS;Lu;0;L;0418 0308;;;;N;;;;04E5;
+04E5;CYRILLIC SMALL LETTER I WITH DIAERESIS;Ll;0;L;0438 0308;;;;N;;;04E4;;04E4
+04E6;CYRILLIC CAPITAL LETTER O WITH DIAERESIS;Lu;0;L;041E 0308;;;;N;;;;04E7;
+04E7;CYRILLIC SMALL LETTER O WITH DIAERESIS;Ll;0;L;043E 0308;;;;N;;;04E6;;04E6
+04E8;CYRILLIC CAPITAL LETTER BARRED O;Lu;0;L;;;;;N;;;;04E9;
+04E9;CYRILLIC SMALL LETTER BARRED O;Ll;0;L;;;;;N;;;04E8;;04E8
+04EA;CYRILLIC CAPITAL LETTER BARRED O WITH DIAERESIS;Lu;0;L;04E8 0308;;;;N;;;;04EB;
+04EB;CYRILLIC SMALL LETTER BARRED O WITH DIAERESIS;Ll;0;L;04E9 0308;;;;N;;;04EA;;04EA
+04EC;CYRILLIC CAPITAL LETTER E WITH DIAERESIS;Lu;0;L;042D 0308;;;;N;;;;04ED;
+04ED;CYRILLIC SMALL LETTER E WITH DIAERESIS;Ll;0;L;044D 0308;;;;N;;;04EC;;04EC
+04EE;CYRILLIC CAPITAL LETTER U WITH MACRON;Lu;0;L;0423 0304;;;;N;;;;04EF;
+04EF;CYRILLIC SMALL LETTER U WITH MACRON;Ll;0;L;0443 0304;;;;N;;;04EE;;04EE
+04F0;CYRILLIC CAPITAL LETTER U WITH DIAERESIS;Lu;0;L;0423 0308;;;;N;;;;04F1;
+04F1;CYRILLIC SMALL LETTER U WITH DIAERESIS;Ll;0;L;0443 0308;;;;N;;;04F0;;04F0
+04F2;CYRILLIC CAPITAL LETTER U WITH DOUBLE ACUTE;Lu;0;L;0423 030B;;;;N;;;;04F3;
+04F3;CYRILLIC SMALL LETTER U WITH DOUBLE ACUTE;Ll;0;L;0443 030B;;;;N;;;04F2;;04F2
+04F4;CYRILLIC CAPITAL LETTER CHE WITH DIAERESIS;Lu;0;L;0427 0308;;;;N;;;;04F5;
+04F5;CYRILLIC SMALL LETTER CHE WITH DIAERESIS;Ll;0;L;0447 0308;;;;N;;;04F4;;04F4
+04F6;CYRILLIC CAPITAL LETTER GHE WITH DESCENDER;Lu;0;L;;;;;N;;;;04F7;
+04F7;CYRILLIC SMALL LETTER GHE WITH DESCENDER;Ll;0;L;;;;;N;;;04F6;;04F6
+04F8;CYRILLIC CAPITAL LETTER YERU WITH DIAERESIS;Lu;0;L;042B 0308;;;;N;;;;04F9;
+04F9;CYRILLIC SMALL LETTER YERU WITH DIAERESIS;Ll;0;L;044B 0308;;;;N;;;04F8;;04F8
+04FA;CYRILLIC CAPITAL LETTER GHE WITH STROKE AND HOOK;Lu;0;L;;;;;N;;;;04FB;
+04FB;CYRILLIC SMALL LETTER GHE WITH STROKE AND HOOK;Ll;0;L;;;;;N;;;04FA;;04FA
+04FC;CYRILLIC CAPITAL LETTER HA WITH HOOK;Lu;0;L;;;;;N;;;;04FD;
+04FD;CYRILLIC SMALL LETTER HA WITH HOOK;Ll;0;L;;;;;N;;;04FC;;04FC
+04FE;CYRILLIC CAPITAL LETTER HA WITH STROKE;Lu;0;L;;;;;N;;;;04FF;
+04FF;CYRILLIC SMALL LETTER HA WITH STROKE;Ll;0;L;;;;;N;;;04FE;;04FE
+0500;CYRILLIC CAPITAL LETTER KOMI DE;Lu;0;L;;;;;N;;;;0501;
+0501;CYRILLIC SMALL LETTER KOMI DE;Ll;0;L;;;;;N;;;0500;;0500
+0502;CYRILLIC CAPITAL LETTER KOMI DJE;Lu;0;L;;;;;N;;;;0503;
+0503;CYRILLIC SMALL LETTER KOMI DJE;Ll;0;L;;;;;N;;;0502;;0502
+0504;CYRILLIC CAPITAL LETTER KOMI ZJE;Lu;0;L;;;;;N;;;;0505;
+0505;CYRILLIC SMALL LETTER KOMI ZJE;Ll;0;L;;;;;N;;;0504;;0504
+0506;CYRILLIC CAPITAL LETTER KOMI DZJE;Lu;0;L;;;;;N;;;;0507;
+0507;CYRILLIC SMALL LETTER KOMI DZJE;Ll;0;L;;;;;N;;;0506;;0506
+0508;CYRILLIC CAPITAL LETTER KOMI LJE;Lu;0;L;;;;;N;;;;0509;
+0509;CYRILLIC SMALL LETTER KOMI LJE;Ll;0;L;;;;;N;;;0508;;0508
+050A;CYRILLIC CAPITAL LETTER KOMI NJE;Lu;0;L;;;;;N;;;;050B;
+050B;CYRILLIC SMALL LETTER KOMI NJE;Ll;0;L;;;;;N;;;050A;;050A
+050C;CYRILLIC CAPITAL LETTER KOMI SJE;Lu;0;L;;;;;N;;;;050D;
+050D;CYRILLIC SMALL LETTER KOMI SJE;Ll;0;L;;;;;N;;;050C;;050C
+050E;CYRILLIC CAPITAL LETTER KOMI TJE;Lu;0;L;;;;;N;;;;050F;
+050F;CYRILLIC SMALL LETTER KOMI TJE;Ll;0;L;;;;;N;;;050E;;050E
+0510;CYRILLIC CAPITAL LETTER REVERSED ZE;Lu;0;L;;;;;N;;;;0511;
+0511;CYRILLIC SMALL LETTER REVERSED ZE;Ll;0;L;;;;;N;;;0510;;0510
+0512;CYRILLIC CAPITAL LETTER EL WITH HOOK;Lu;0;L;;;;;N;;;;0513;
+0513;CYRILLIC SMALL LETTER EL WITH HOOK;Ll;0;L;;;;;N;;;0512;;0512
+0514;CYRILLIC CAPITAL LETTER LHA;Lu;0;L;;;;;N;;;;0515;
+0515;CYRILLIC SMALL LETTER LHA;Ll;0;L;;;;;N;;;0514;;0514
+0516;CYRILLIC CAPITAL LETTER RHA;Lu;0;L;;;;;N;;;;0517;
+0517;CYRILLIC SMALL LETTER RHA;Ll;0;L;;;;;N;;;0516;;0516
+0518;CYRILLIC CAPITAL LETTER YAE;Lu;0;L;;;;;N;;;;0519;
+0519;CYRILLIC SMALL LETTER YAE;Ll;0;L;;;;;N;;;0518;;0518
+051A;CYRILLIC CAPITAL LETTER QA;Lu;0;L;;;;;N;;;;051B;
+051B;CYRILLIC SMALL LETTER QA;Ll;0;L;;;;;N;;;051A;;051A
+051C;CYRILLIC CAPITAL LETTER WE;Lu;0;L;;;;;N;;;;051D;
+051D;CYRILLIC SMALL LETTER WE;Ll;0;L;;;;;N;;;051C;;051C
+051E;CYRILLIC CAPITAL LETTER ALEUT KA;Lu;0;L;;;;;N;;;;051F;
+051F;CYRILLIC SMALL LETTER ALEUT KA;Ll;0;L;;;;;N;;;051E;;051E
+0520;CYRILLIC CAPITAL LETTER EL WITH MIDDLE HOOK;Lu;0;L;;;;;N;;;;0521;
+0521;CYRILLIC SMALL LETTER EL WITH MIDDLE HOOK;Ll;0;L;;;;;N;;;0520;;0520
+0522;CYRILLIC CAPITAL LETTER EN WITH MIDDLE HOOK;Lu;0;L;;;;;N;;;;0523;
+0523;CYRILLIC SMALL LETTER EN WITH MIDDLE HOOK;Ll;0;L;;;;;N;;;0522;;0522
+0524;CYRILLIC CAPITAL LETTER PE WITH DESCENDER;Lu;0;L;;;;;N;;;;0525;
+0525;CYRILLIC SMALL LETTER PE WITH DESCENDER;Ll;0;L;;;;;N;;;0524;;0524
+0526;CYRILLIC CAPITAL LETTER SHHA WITH DESCENDER;Lu;0;L;;;;;N;;;;0527;
+0527;CYRILLIC SMALL LETTER SHHA WITH DESCENDER;Ll;0;L;;;;;N;;;0526;;0526
+0528;CYRILLIC CAPITAL LETTER EN WITH LEFT HOOK;Lu;0;L;;;;;N;;;;0529;
+0529;CYRILLIC SMALL LETTER EN WITH LEFT HOOK;Ll;0;L;;;;;N;;;0528;;0528
+052A;CYRILLIC CAPITAL LETTER DZZHE;Lu;0;L;;;;;N;;;;052B;
+052B;CYRILLIC SMALL LETTER DZZHE;Ll;0;L;;;;;N;;;052A;;052A
+052C;CYRILLIC CAPITAL LETTER DCHE;Lu;0;L;;;;;N;;;;052D;
+052D;CYRILLIC SMALL LETTER DCHE;Ll;0;L;;;;;N;;;052C;;052C
+052E;CYRILLIC CAPITAL LETTER EL WITH DESCENDER;Lu;0;L;;;;;N;;;;052F;
+052F;CYRILLIC SMALL LETTER EL WITH DESCENDER;Ll;0;L;;;;;N;;;052E;;052E
+0531;ARMENIAN CAPITAL LETTER AYB;Lu;0;L;;;;;N;;;;0561;
+0532;ARMENIAN CAPITAL LETTER BEN;Lu;0;L;;;;;N;;;;0562;
+0533;ARMENIAN CAPITAL LETTER GIM;Lu;0;L;;;;;N;;;;0563;
+0534;ARMENIAN CAPITAL LETTER DA;Lu;0;L;;;;;N;;;;0564;
+0535;ARMENIAN CAPITAL LETTER ECH;Lu;0;L;;;;;N;;;;0565;
+0536;ARMENIAN CAPITAL LETTER ZA;Lu;0;L;;;;;N;;;;0566;
+0537;ARMENIAN CAPITAL LETTER EH;Lu;0;L;;;;;N;;;;0567;
+0538;ARMENIAN CAPITAL LETTER ET;Lu;0;L;;;;;N;;;;0568;
+0539;ARMENIAN CAPITAL LETTER TO;Lu;0;L;;;;;N;;;;0569;
+053A;ARMENIAN CAPITAL LETTER ZHE;Lu;0;L;;;;;N;;;;056A;
+053B;ARMENIAN CAPITAL LETTER INI;Lu;0;L;;;;;N;;;;056B;
+053C;ARMENIAN CAPITAL LETTER LIWN;Lu;0;L;;;;;N;;;;056C;
+053D;ARMENIAN CAPITAL LETTER XEH;Lu;0;L;;;;;N;;;;056D;
+053E;ARMENIAN CAPITAL LETTER CA;Lu;0;L;;;;;N;;;;056E;
+053F;ARMENIAN CAPITAL LETTER KEN;Lu;0;L;;;;;N;;;;056F;
+0540;ARMENIAN CAPITAL LETTER HO;Lu;0;L;;;;;N;;;;0570;
+0541;ARMENIAN CAPITAL LETTER JA;Lu;0;L;;;;;N;;;;0571;
+0542;ARMENIAN CAPITAL LETTER GHAD;Lu;0;L;;;;;N;ARMENIAN CAPITAL LETTER LAD;;;0572;
+0543;ARMENIAN CAPITAL LETTER CHEH;Lu;0;L;;;;;N;;;;0573;
+0544;ARMENIAN CAPITAL LETTER MEN;Lu;0;L;;;;;N;;;;0574;
+0545;ARMENIAN CAPITAL LETTER YI;Lu;0;L;;;;;N;;;;0575;
+0546;ARMENIAN CAPITAL LETTER NOW;Lu;0;L;;;;;N;;;;0576;
+0547;ARMENIAN CAPITAL LETTER SHA;Lu;0;L;;;;;N;;;;0577;
+0548;ARMENIAN CAPITAL LETTER VO;Lu;0;L;;;;;N;;;;0578;
+0549;ARMENIAN CAPITAL LETTER CHA;Lu;0;L;;;;;N;;;;0579;
+054A;ARMENIAN CAPITAL LETTER PEH;Lu;0;L;;;;;N;;;;057A;
+054B;ARMENIAN CAPITAL LETTER JHEH;Lu;0;L;;;;;N;;;;057B;
+054C;ARMENIAN CAPITAL LETTER RA;Lu;0;L;;;;;N;;;;057C;
+054D;ARMENIAN CAPITAL LETTER SEH;Lu;0;L;;;;;N;;;;057D;
+054E;ARMENIAN CAPITAL LETTER VEW;Lu;0;L;;;;;N;;;;057E;
+054F;ARMENIAN CAPITAL LETTER TIWN;Lu;0;L;;;;;N;;;;057F;
+0550;ARMENIAN CAPITAL LETTER REH;Lu;0;L;;;;;N;;;;0580;
+0551;ARMENIAN CAPITAL LETTER CO;Lu;0;L;;;;;N;;;;0581;
+0552;ARMENIAN CAPITAL LETTER YIWN;Lu;0;L;;;;;N;;;;0582;
+0553;ARMENIAN CAPITAL LETTER PIWR;Lu;0;L;;;;;N;;;;0583;
+0554;ARMENIAN CAPITAL LETTER KEH;Lu;0;L;;;;;N;;;;0584;
+0555;ARMENIAN CAPITAL LETTER OH;Lu;0;L;;;;;N;;;;0585;
+0556;ARMENIAN CAPITAL LETTER FEH;Lu;0;L;;;;;N;;;;0586;
+0559;ARMENIAN MODIFIER LETTER LEFT HALF RING;Lm;0;L;;;;;N;;;;;
+055A;ARMENIAN APOSTROPHE;Po;0;L;;;;;N;ARMENIAN MODIFIER LETTER RIGHT HALF RING;;;;
+055B;ARMENIAN EMPHASIS MARK;Po;0;L;;;;;N;;;;;
+055C;ARMENIAN EXCLAMATION MARK;Po;0;L;;;;;N;;;;;
+055D;ARMENIAN COMMA;Po;0;L;;;;;N;;;;;
+055E;ARMENIAN QUESTION MARK;Po;0;L;;;;;N;;;;;
+055F;ARMENIAN ABBREVIATION MARK;Po;0;L;;;;;N;;;;;
+0561;ARMENIAN SMALL LETTER AYB;Ll;0;L;;;;;N;;;0531;;0531
+0562;ARMENIAN SMALL LETTER BEN;Ll;0;L;;;;;N;;;0532;;0532
+0563;ARMENIAN SMALL LETTER GIM;Ll;0;L;;;;;N;;;0533;;0533
+0564;ARMENIAN SMALL LETTER DA;Ll;0;L;;;;;N;;;0534;;0534
+0565;ARMENIAN SMALL LETTER ECH;Ll;0;L;;;;;N;;;0535;;0535
+0566;ARMENIAN SMALL LETTER ZA;Ll;0;L;;;;;N;;;0536;;0536
+0567;ARMENIAN SMALL LETTER EH;Ll;0;L;;;;;N;;;0537;;0537
+0568;ARMENIAN SMALL LETTER ET;Ll;0;L;;;;;N;;;0538;;0538
+0569;ARMENIAN SMALL LETTER TO;Ll;0;L;;;;;N;;;0539;;0539
+056A;ARMENIAN SMALL LETTER ZHE;Ll;0;L;;;;;N;;;053A;;053A
+056B;ARMENIAN SMALL LETTER INI;Ll;0;L;;;;;N;;;053B;;053B
+056C;ARMENIAN SMALL LETTER LIWN;Ll;0;L;;;;;N;;;053C;;053C
+056D;ARMENIAN SMALL LETTER XEH;Ll;0;L;;;;;N;;;053D;;053D
+056E;ARMENIAN SMALL LETTER CA;Ll;0;L;;;;;N;;;053E;;053E
+056F;ARMENIAN SMALL LETTER KEN;Ll;0;L;;;;;N;;;053F;;053F
+0570;ARMENIAN SMALL LETTER HO;Ll;0;L;;;;;N;;;0540;;0540
+0571;ARMENIAN SMALL LETTER JA;Ll;0;L;;;;;N;;;0541;;0541
+0572;ARMENIAN SMALL LETTER GHAD;Ll;0;L;;;;;N;ARMENIAN SMALL LETTER LAD;;0542;;0542
+0573;ARMENIAN SMALL LETTER CHEH;Ll;0;L;;;;;N;;;0543;;0543
+0574;ARMENIAN SMALL LETTER MEN;Ll;0;L;;;;;N;;;0544;;0544
+0575;ARMENIAN SMALL LETTER YI;Ll;0;L;;;;;N;;;0545;;0545
+0576;ARMENIAN SMALL LETTER NOW;Ll;0;L;;;;;N;;;0546;;0546
+0577;ARMENIAN SMALL LETTER SHA;Ll;0;L;;;;;N;;;0547;;0547
+0578;ARMENIAN SMALL LETTER VO;Ll;0;L;;;;;N;;;0548;;0548
+0579;ARMENIAN SMALL LETTER CHA;Ll;0;L;;;;;N;;;0549;;0549
+057A;ARMENIAN SMALL LETTER PEH;Ll;0;L;;;;;N;;;054A;;054A
+057B;ARMENIAN SMALL LETTER JHEH;Ll;0;L;;;;;N;;;054B;;054B
+057C;ARMENIAN SMALL LETTER RA;Ll;0;L;;;;;N;;;054C;;054C
+057D;ARMENIAN SMALL LETTER SEH;Ll;0;L;;;;;N;;;054D;;054D
+057E;ARMENIAN SMALL LETTER VEW;Ll;0;L;;;;;N;;;054E;;054E
+057F;ARMENIAN SMALL LETTER TIWN;Ll;0;L;;;;;N;;;054F;;054F
+0580;ARMENIAN SMALL LETTER REH;Ll;0;L;;;;;N;;;0550;;0550
+0581;ARMENIAN SMALL LETTER CO;Ll;0;L;;;;;N;;;0551;;0551
+0582;ARMENIAN SMALL LETTER YIWN;Ll;0;L;;;;;N;;;0552;;0552
+0583;ARMENIAN SMALL LETTER PIWR;Ll;0;L;;;;;N;;;0553;;0553
+0584;ARMENIAN SMALL LETTER KEH;Ll;0;L;;;;;N;;;0554;;0554
+0585;ARMENIAN SMALL LETTER OH;Ll;0;L;;;;;N;;;0555;;0555
+0586;ARMENIAN SMALL LETTER FEH;Ll;0;L;;;;;N;;;0556;;0556
+0587;ARMENIAN SMALL LIGATURE ECH YIWN;Ll;0;L;<compat> 0565 0582;;;;N;;;;;
+0589;ARMENIAN FULL STOP;Po;0;L;;;;;N;ARMENIAN PERIOD;;;;
+058A;ARMENIAN HYPHEN;Pd;0;ON;;;;;N;;;;;
+058D;RIGHT-FACING ARMENIAN ETERNITY SIGN;So;0;ON;;;;;N;;;;;
+058E;LEFT-FACING ARMENIAN ETERNITY SIGN;So;0;ON;;;;;N;;;;;
+058F;ARMENIAN DRAM SIGN;Sc;0;ET;;;;;N;;;;;
+0591;HEBREW ACCENT ETNAHTA;Mn;220;NSM;;;;;N;;;;;
+0592;HEBREW ACCENT SEGOL;Mn;230;NSM;;;;;N;;;;;
+0593;HEBREW ACCENT SHALSHELET;Mn;230;NSM;;;;;N;;;;;
+0594;HEBREW ACCENT ZAQEF QATAN;Mn;230;NSM;;;;;N;;;;;
+0595;HEBREW ACCENT ZAQEF GADOL;Mn;230;NSM;;;;;N;;;;;
+0596;HEBREW ACCENT TIPEHA;Mn;220;NSM;;;;;N;;;;;
+0597;HEBREW ACCENT REVIA;Mn;230;NSM;;;;;N;;;;;
+0598;HEBREW ACCENT ZARQA;Mn;230;NSM;;;;;N;;;;;
+0599;HEBREW ACCENT PASHTA;Mn;230;NSM;;;;;N;;;;;
+059A;HEBREW ACCENT YETIV;Mn;222;NSM;;;;;N;;;;;
+059B;HEBREW ACCENT TEVIR;Mn;220;NSM;;;;;N;;;;;
+059C;HEBREW ACCENT GERESH;Mn;230;NSM;;;;;N;;;;;
+059D;HEBREW ACCENT GERESH MUQDAM;Mn;230;NSM;;;;;N;;;;;
+059E;HEBREW ACCENT GERSHAYIM;Mn;230;NSM;;;;;N;;;;;
+059F;HEBREW ACCENT QARNEY PARA;Mn;230;NSM;;;;;N;;;;;
+05A0;HEBREW ACCENT TELISHA GEDOLA;Mn;230;NSM;;;;;N;;;;;
+05A1;HEBREW ACCENT PAZER;Mn;230;NSM;;;;;N;;;;;
+05A2;HEBREW ACCENT ATNAH HAFUKH;Mn;220;NSM;;;;;N;;;;;
+05A3;HEBREW ACCENT MUNAH;Mn;220;NSM;;;;;N;;;;;
+05A4;HEBREW ACCENT MAHAPAKH;Mn;220;NSM;;;;;N;;;;;
+05A5;HEBREW ACCENT MERKHA;Mn;220;NSM;;;;;N;;;;;
+05A6;HEBREW ACCENT MERKHA KEFULA;Mn;220;NSM;;;;;N;;;;;
+05A7;HEBREW ACCENT DARGA;Mn;220;NSM;;;;;N;;;;;
+05A8;HEBREW ACCENT QADMA;Mn;230;NSM;;;;;N;;;;;
+05A9;HEBREW ACCENT TELISHA QETANA;Mn;230;NSM;;;;;N;;;;;
+05AA;HEBREW ACCENT YERAH BEN YOMO;Mn;220;NSM;;;;;N;;;;;
+05AB;HEBREW ACCENT OLE;Mn;230;NSM;;;;;N;;;;;
+05AC;HEBREW ACCENT ILUY;Mn;230;NSM;;;;;N;;;;;
+05AD;HEBREW ACCENT DEHI;Mn;222;NSM;;;;;N;;;;;
+05AE;HEBREW ACCENT ZINOR;Mn;228;NSM;;;;;N;;;;;
+05AF;HEBREW MARK MASORA CIRCLE;Mn;230;NSM;;;;;N;;;;;
+05B0;HEBREW POINT SHEVA;Mn;10;NSM;;;;;N;;;;;
+05B1;HEBREW POINT HATAF SEGOL;Mn;11;NSM;;;;;N;;;;;
+05B2;HEBREW POINT HATAF PATAH;Mn;12;NSM;;;;;N;;;;;
+05B3;HEBREW POINT HATAF QAMATS;Mn;13;NSM;;;;;N;;;;;
+05B4;HEBREW POINT HIRIQ;Mn;14;NSM;;;;;N;;;;;
+05B5;HEBREW POINT TSERE;Mn;15;NSM;;;;;N;;;;;
+05B6;HEBREW POINT SEGOL;Mn;16;NSM;;;;;N;;;;;
+05B7;HEBREW POINT PATAH;Mn;17;NSM;;;;;N;;;;;
+05B8;HEBREW POINT QAMATS;Mn;18;NSM;;;;;N;;;;;
+05B9;HEBREW POINT HOLAM;Mn;19;NSM;;;;;N;;;;;
+05BA;HEBREW POINT HOLAM HASER FOR VAV;Mn;19;NSM;;;;;N;;;;;
+05BB;HEBREW POINT QUBUTS;Mn;20;NSM;;;;;N;;;;;
+05BC;HEBREW POINT DAGESH OR MAPIQ;Mn;21;NSM;;;;;N;HEBREW POINT DAGESH;;;;
+05BD;HEBREW POINT METEG;Mn;22;NSM;;;;;N;;;;;
+05BE;HEBREW PUNCTUATION MAQAF;Pd;0;R;;;;;N;;;;;
+05BF;HEBREW POINT RAFE;Mn;23;NSM;;;;;N;;;;;
+05C0;HEBREW PUNCTUATION PASEQ;Po;0;R;;;;;N;HEBREW POINT PASEQ;;;;
+05C1;HEBREW POINT SHIN DOT;Mn;24;NSM;;;;;N;;;;;
+05C2;HEBREW POINT SIN DOT;Mn;25;NSM;;;;;N;;;;;
+05C3;HEBREW PUNCTUATION SOF PASUQ;Po;0;R;;;;;N;;;;;
+05C4;HEBREW MARK UPPER DOT;Mn;230;NSM;;;;;N;;;;;
+05C5;HEBREW MARK LOWER DOT;Mn;220;NSM;;;;;N;;;;;
+05C6;HEBREW PUNCTUATION NUN HAFUKHA;Po;0;R;;;;;N;;;;;
+05C7;HEBREW POINT QAMATS QATAN;Mn;18;NSM;;;;;N;;;;;
+05D0;HEBREW LETTER ALEF;Lo;0;R;;;;;N;;;;;
+05D1;HEBREW LETTER BET;Lo;0;R;;;;;N;;;;;
+05D2;HEBREW LETTER GIMEL;Lo;0;R;;;;;N;;;;;
+05D3;HEBREW LETTER DALET;Lo;0;R;;;;;N;;;;;
+05D4;HEBREW LETTER HE;Lo;0;R;;;;;N;;;;;
+05D5;HEBREW LETTER VAV;Lo;0;R;;;;;N;;;;;
+05D6;HEBREW LETTER ZAYIN;Lo;0;R;;;;;N;;;;;
+05D7;HEBREW LETTER HET;Lo;0;R;;;;;N;;;;;
+05D8;HEBREW LETTER TET;Lo;0;R;;;;;N;;;;;
+05D9;HEBREW LETTER YOD;Lo;0;R;;;;;N;;;;;
+05DA;HEBREW LETTER FINAL KAF;Lo;0;R;;;;;N;;;;;
+05DB;HEBREW LETTER KAF;Lo;0;R;;;;;N;;;;;
+05DC;HEBREW LETTER LAMED;Lo;0;R;;;;;N;;;;;
+05DD;HEBREW LETTER FINAL MEM;Lo;0;R;;;;;N;;;;;
+05DE;HEBREW LETTER MEM;Lo;0;R;;;;;N;;;;;
+05DF;HEBREW LETTER FINAL NUN;Lo;0;R;;;;;N;;;;;
+05E0;HEBREW LETTER NUN;Lo;0;R;;;;;N;;;;;
+05E1;HEBREW LETTER SAMEKH;Lo;0;R;;;;;N;;;;;
+05E2;HEBREW LETTER AYIN;Lo;0;R;;;;;N;;;;;
+05E3;HEBREW LETTER FINAL PE;Lo;0;R;;;;;N;;;;;
+05E4;HEBREW LETTER PE;Lo;0;R;;;;;N;;;;;
+05E5;HEBREW LETTER FINAL TSADI;Lo;0;R;;;;;N;;;;;
+05E6;HEBREW LETTER TSADI;Lo;0;R;;;;;N;;;;;
+05E7;HEBREW LETTER QOF;Lo;0;R;;;;;N;;;;;
+05E8;HEBREW LETTER RESH;Lo;0;R;;;;;N;;;;;
+05E9;HEBREW LETTER SHIN;Lo;0;R;;;;;N;;;;;
+05EA;HEBREW LETTER TAV;Lo;0;R;;;;;N;;;;;
+05F0;HEBREW LIGATURE YIDDISH DOUBLE VAV;Lo;0;R;;;;;N;HEBREW LETTER DOUBLE VAV;;;;
+05F1;HEBREW LIGATURE YIDDISH VAV YOD;Lo;0;R;;;;;N;HEBREW LETTER VAV YOD;;;;
+05F2;HEBREW LIGATURE YIDDISH DOUBLE YOD;Lo;0;R;;;;;N;HEBREW LETTER DOUBLE YOD;;;;
+05F3;HEBREW PUNCTUATION GERESH;Po;0;R;;;;;N;;;;;
+05F4;HEBREW PUNCTUATION GERSHAYIM;Po;0;R;;;;;N;;;;;
+0600;ARABIC NUMBER SIGN;Cf;0;AN;;;;;N;;;;;
+0601;ARABIC SIGN SANAH;Cf;0;AN;;;;;N;;;;;
+0602;ARABIC FOOTNOTE MARKER;Cf;0;AN;;;;;N;;;;;
+0603;ARABIC SIGN SAFHA;Cf;0;AN;;;;;N;;;;;
+0604;ARABIC SIGN SAMVAT;Cf;0;AN;;;;;N;;;;;
+0605;ARABIC NUMBER MARK ABOVE;Cf;0;AN;;;;;N;;;;;
+0606;ARABIC-INDIC CUBE ROOT;Sm;0;ON;;;;;N;;;;;
+0607;ARABIC-INDIC FOURTH ROOT;Sm;0;ON;;;;;N;;;;;
+0608;ARABIC RAY;Sm;0;AL;;;;;N;;;;;
+0609;ARABIC-INDIC PER MILLE SIGN;Po;0;ET;;;;;N;;;;;
+060A;ARABIC-INDIC PER TEN THOUSAND SIGN;Po;0;ET;;;;;N;;;;;
+060B;AFGHANI SIGN;Sc;0;AL;;;;;N;;;;;
+060C;ARABIC COMMA;Po;0;CS;;;;;N;;;;;
+060D;ARABIC DATE SEPARATOR;Po;0;AL;;;;;N;;;;;
+060E;ARABIC POETIC VERSE SIGN;So;0;ON;;;;;N;;;;;
+060F;ARABIC SIGN MISRA;So;0;ON;;;;;N;;;;;
+0610;ARABIC SIGN SALLALLAHOU ALAYHE WASSALLAM;Mn;230;NSM;;;;;N;;;;;
+0611;ARABIC SIGN ALAYHE ASSALLAM;Mn;230;NSM;;;;;N;;;;;
+0612;ARABIC SIGN RAHMATULLAH ALAYHE;Mn;230;NSM;;;;;N;;;;;
+0613;ARABIC SIGN RADI ALLAHOU ANHU;Mn;230;NSM;;;;;N;;;;;
+0614;ARABIC SIGN TAKHALLUS;Mn;230;NSM;;;;;N;;;;;
+0615;ARABIC SMALL HIGH TAH;Mn;230;NSM;;;;;N;;;;;
+0616;ARABIC SMALL HIGH LIGATURE ALEF WITH LAM WITH YEH;Mn;230;NSM;;;;;N;;;;;
+0617;ARABIC SMALL HIGH ZAIN;Mn;230;NSM;;;;;N;;;;;
+0618;ARABIC SMALL FATHA;Mn;30;NSM;;;;;N;;;;;
+0619;ARABIC SMALL DAMMA;Mn;31;NSM;;;;;N;;;;;
+061A;ARABIC SMALL KASRA;Mn;32;NSM;;;;;N;;;;;
+061B;ARABIC SEMICOLON;Po;0;AL;;;;;N;;;;;
+061C;ARABIC LETTER MARK;Cf;0;AL;;;;;N;;;;;
+061E;ARABIC TRIPLE DOT PUNCTUATION MARK;Po;0;AL;;;;;N;;;;;
+061F;ARABIC QUESTION MARK;Po;0;AL;;;;;N;;;;;
+0620;ARABIC LETTER KASHMIRI YEH;Lo;0;AL;;;;;N;;;;;
+0621;ARABIC LETTER HAMZA;Lo;0;AL;;;;;N;ARABIC LETTER HAMZAH;;;;
+0622;ARABIC LETTER ALEF WITH MADDA ABOVE;Lo;0;AL;0627 0653;;;;N;ARABIC LETTER MADDAH ON ALEF;;;;
+0623;ARABIC LETTER ALEF WITH HAMZA ABOVE;Lo;0;AL;0627 0654;;;;N;ARABIC LETTER HAMZAH ON ALEF;;;;
+0624;ARABIC LETTER WAW WITH HAMZA ABOVE;Lo;0;AL;0648 0654;;;;N;ARABIC LETTER HAMZAH ON WAW;;;;
+0625;ARABIC LETTER ALEF WITH HAMZA BELOW;Lo;0;AL;0627 0655;;;;N;ARABIC LETTER HAMZAH UNDER ALEF;;;;
+0626;ARABIC LETTER YEH WITH HAMZA ABOVE;Lo;0;AL;064A 0654;;;;N;ARABIC LETTER HAMZAH ON YA;;;;
+0627;ARABIC LETTER ALEF;Lo;0;AL;;;;;N;;;;;
+0628;ARABIC LETTER BEH;Lo;0;AL;;;;;N;ARABIC LETTER BAA;;;;
+0629;ARABIC LETTER TEH MARBUTA;Lo;0;AL;;;;;N;ARABIC LETTER TAA MARBUTAH;;;;
+062A;ARABIC LETTER TEH;Lo;0;AL;;;;;N;ARABIC LETTER TAA;;;;
+062B;ARABIC LETTER THEH;Lo;0;AL;;;;;N;ARABIC LETTER THAA;;;;
+062C;ARABIC LETTER JEEM;Lo;0;AL;;;;;N;;;;;
+062D;ARABIC LETTER HAH;Lo;0;AL;;;;;N;ARABIC LETTER HAA;;;;
+062E;ARABIC LETTER KHAH;Lo;0;AL;;;;;N;ARABIC LETTER KHAA;;;;
+062F;ARABIC LETTER DAL;Lo;0;AL;;;;;N;;;;;
+0630;ARABIC LETTER THAL;Lo;0;AL;;;;;N;;;;;
+0631;ARABIC LETTER REH;Lo;0;AL;;;;;N;ARABIC LETTER RA;;;;
+0632;ARABIC LETTER ZAIN;Lo;0;AL;;;;;N;;;;;
+0633;ARABIC LETTER SEEN;Lo;0;AL;;;;;N;;;;;
+0634;ARABIC LETTER SHEEN;Lo;0;AL;;;;;N;;;;;
+0635;ARABIC LETTER SAD;Lo;0;AL;;;;;N;;;;;
+0636;ARABIC LETTER DAD;Lo;0;AL;;;;;N;;;;;
+0637;ARABIC LETTER TAH;Lo;0;AL;;;;;N;;;;;
+0638;ARABIC LETTER ZAH;Lo;0;AL;;;;;N;ARABIC LETTER DHAH;;;;
+0639;ARABIC LETTER AIN;Lo;0;AL;;;;;N;;;;;
+063A;ARABIC LETTER GHAIN;Lo;0;AL;;;;;N;;;;;
+063B;ARABIC LETTER KEHEH WITH TWO DOTS ABOVE;Lo;0;AL;;;;;N;;;;;
+063C;ARABIC LETTER KEHEH WITH THREE DOTS BELOW;Lo;0;AL;;;;;N;;;;;
+063D;ARABIC LETTER FARSI YEH WITH INVERTED V;Lo;0;AL;;;;;N;;;;;
+063E;ARABIC LETTER FARSI YEH WITH TWO DOTS ABOVE;Lo;0;AL;;;;;N;;;;;
+063F;ARABIC LETTER FARSI YEH WITH THREE DOTS ABOVE;Lo;0;AL;;;;;N;;;;;
+0640;ARABIC TATWEEL;Lm;0;AL;;;;;N;;;;;
+0641;ARABIC LETTER FEH;Lo;0;AL;;;;;N;ARABIC LETTER FA;;;;
+0642;ARABIC LETTER QAF;Lo;0;AL;;;;;N;;;;;
+0643;ARABIC LETTER KAF;Lo;0;AL;;;;;N;ARABIC LETTER CAF;;;;
+0644;ARABIC LETTER LAM;Lo;0;AL;;;;;N;;;;;
+0645;ARABIC LETTER MEEM;Lo;0;AL;;;;;N;;;;;
+0646;ARABIC LETTER NOON;Lo;0;AL;;;;;N;;;;;
+0647;ARABIC LETTER HEH;Lo;0;AL;;;;;N;ARABIC LETTER HA;;;;
+0648;ARABIC LETTER WAW;Lo;0;AL;;;;;N;;;;;
+0649;ARABIC LETTER ALEF MAKSURA;Lo;0;AL;;;;;N;ARABIC LETTER ALEF MAQSURAH;;;;
+064A;ARABIC LETTER YEH;Lo;0;AL;;;;;N;ARABIC LETTER YA;;;;
+064B;ARABIC FATHATAN;Mn;27;NSM;;;;;N;;;;;
+064C;ARABIC DAMMATAN;Mn;28;NSM;;;;;N;;;;;
+064D;ARABIC KASRATAN;Mn;29;NSM;;;;;N;;;;;
+064E;ARABIC FATHA;Mn;30;NSM;;;;;N;ARABIC FATHAH;;;;
+064F;ARABIC DAMMA;Mn;31;NSM;;;;;N;ARABIC DAMMAH;;;;
+0650;ARABIC KASRA;Mn;32;NSM;;;;;N;ARABIC KASRAH;;;;
+0651;ARABIC SHADDA;Mn;33;NSM;;;;;N;ARABIC SHADDAH;;;;
+0652;ARABIC SUKUN;Mn;34;NSM;;;;;N;;;;;
+0653;ARABIC MADDAH ABOVE;Mn;230;NSM;;;;;N;;;;;
+0654;ARABIC HAMZA ABOVE;Mn;230;NSM;;;;;N;;;;;
+0655;ARABIC HAMZA BELOW;Mn;220;NSM;;;;;N;;;;;
+0656;ARABIC SUBSCRIPT ALEF;Mn;220;NSM;;;;;N;;;;;
+0657;ARABIC INVERTED DAMMA;Mn;230;NSM;;;;;N;;;;;
+0658;ARABIC MARK NOON GHUNNA;Mn;230;NSM;;;;;N;;;;;
+0659;ARABIC ZWARAKAY;Mn;230;NSM;;;;;N;;;;;
+065A;ARABIC VOWEL SIGN SMALL V ABOVE;Mn;230;NSM;;;;;N;;;;;
+065B;ARABIC VOWEL SIGN INVERTED SMALL V ABOVE;Mn;230;NSM;;;;;N;;;;;
+065C;ARABIC VOWEL SIGN DOT BELOW;Mn;220;NSM;;;;;N;;;;;
+065D;ARABIC REVERSED DAMMA;Mn;230;NSM;;;;;N;;;;;
+065E;ARABIC FATHA WITH TWO DOTS;Mn;230;NSM;;;;;N;;;;;
+065F;ARABIC WAVY HAMZA BELOW;Mn;220;NSM;;;;;N;;;;;
+0660;ARABIC-INDIC DIGIT ZERO;Nd;0;AN;;0;0;0;N;;;;;
+0661;ARABIC-INDIC DIGIT ONE;Nd;0;AN;;1;1;1;N;;;;;
+0662;ARABIC-INDIC DIGIT TWO;Nd;0;AN;;2;2;2;N;;;;;
+0663;ARABIC-INDIC DIGIT THREE;Nd;0;AN;;3;3;3;N;;;;;
+0664;ARABIC-INDIC DIGIT FOUR;Nd;0;AN;;4;4;4;N;;;;;
+0665;ARABIC-INDIC DIGIT FIVE;Nd;0;AN;;5;5;5;N;;;;;
+0666;ARABIC-INDIC DIGIT SIX;Nd;0;AN;;6;6;6;N;;;;;
+0667;ARABIC-INDIC DIGIT SEVEN;Nd;0;AN;;7;7;7;N;;;;;
+0668;ARABIC-INDIC DIGIT EIGHT;Nd;0;AN;;8;8;8;N;;;;;
+0669;ARABIC-INDIC DIGIT NINE;Nd;0;AN;;9;9;9;N;;;;;
+066A;ARABIC PERCENT SIGN;Po;0;ET;;;;;N;;;;;
+066B;ARABIC DECIMAL SEPARATOR;Po;0;AN;;;;;N;;;;;
+066C;ARABIC THOUSANDS SEPARATOR;Po;0;AN;;;;;N;;;;;
+066D;ARABIC FIVE POINTED STAR;Po;0;AL;;;;;N;;;;;
+066E;ARABIC LETTER DOTLESS BEH;Lo;0;AL;;;;;N;;;;;
+066F;ARABIC LETTER DOTLESS QAF;Lo;0;AL;;;;;N;;;;;
+0670;ARABIC LETTER SUPERSCRIPT ALEF;Mn;35;NSM;;;;;N;ARABIC ALEF ABOVE;;;;
+0671;ARABIC LETTER ALEF WASLA;Lo;0;AL;;;;;N;ARABIC LETTER HAMZAT WASL ON ALEF;;;;
+0672;ARABIC LETTER ALEF WITH WAVY HAMZA ABOVE;Lo;0;AL;;;;;N;ARABIC LETTER WAVY HAMZAH ON ALEF;;;;
+0673;ARABIC LETTER ALEF WITH WAVY HAMZA BELOW;Lo;0;AL;;;;;N;ARABIC LETTER WAVY HAMZAH UNDER ALEF;;;;
+0674;ARABIC LETTER HIGH HAMZA;Lo;0;AL;;;;;N;ARABIC LETTER HIGH HAMZAH;;;;
+0675;ARABIC LETTER HIGH HAMZA ALEF;Lo;0;AL;<compat> 0627 0674;;;;N;ARABIC LETTER HIGH HAMZAH ALEF;;;;
+0676;ARABIC LETTER HIGH HAMZA WAW;Lo;0;AL;<compat> 0648 0674;;;;N;ARABIC LETTER HIGH HAMZAH WAW;;;;
+0677;ARABIC LETTER U WITH HAMZA ABOVE;Lo;0;AL;<compat> 06C7 0674;;;;N;ARABIC LETTER HIGH HAMZAH WAW WITH DAMMAH;;;;
+0678;ARABIC LETTER HIGH HAMZA YEH;Lo;0;AL;<compat> 064A 0674;;;;N;ARABIC LETTER HIGH HAMZAH YA;;;;
+0679;ARABIC LETTER TTEH;Lo;0;AL;;;;;N;ARABIC LETTER TAA WITH SMALL TAH;;;;
+067A;ARABIC LETTER TTEHEH;Lo;0;AL;;;;;N;ARABIC LETTER TAA WITH TWO DOTS VERTICAL ABOVE;;;;
+067B;ARABIC LETTER BEEH;Lo;0;AL;;;;;N;ARABIC LETTER BAA WITH TWO DOTS VERTICAL BELOW;;;;
+067C;ARABIC LETTER TEH WITH RING;Lo;0;AL;;;;;N;ARABIC LETTER TAA WITH RING;;;;
+067D;ARABIC LETTER TEH WITH THREE DOTS ABOVE DOWNWARDS;Lo;0;AL;;;;;N;ARABIC LETTER TAA WITH THREE DOTS ABOVE DOWNWARD;;;;
+067E;ARABIC LETTER PEH;Lo;0;AL;;;;;N;ARABIC LETTER TAA WITH THREE DOTS BELOW;;;;
+067F;ARABIC LETTER TEHEH;Lo;0;AL;;;;;N;ARABIC LETTER TAA WITH FOUR DOTS ABOVE;;;;
+0680;ARABIC LETTER BEHEH;Lo;0;AL;;;;;N;ARABIC LETTER BAA WITH FOUR DOTS BELOW;;;;
+0681;ARABIC LETTER HAH WITH HAMZA ABOVE;Lo;0;AL;;;;;N;ARABIC LETTER HAMZAH ON HAA;;;;
+0682;ARABIC LETTER HAH WITH TWO DOTS VERTICAL ABOVE;Lo;0;AL;;;;;N;ARABIC LETTER HAA WITH TWO DOTS VERTICAL ABOVE;;;;
+0683;ARABIC LETTER NYEH;Lo;0;AL;;;;;N;ARABIC LETTER HAA WITH MIDDLE TWO DOTS;;;;
+0684;ARABIC LETTER DYEH;Lo;0;AL;;;;;N;ARABIC LETTER HAA WITH MIDDLE TWO DOTS VERTICAL;;;;
+0685;ARABIC LETTER HAH WITH THREE DOTS ABOVE;Lo;0;AL;;;;;N;ARABIC LETTER HAA WITH THREE DOTS ABOVE;;;;
+0686;ARABIC LETTER TCHEH;Lo;0;AL;;;;;N;ARABIC LETTER HAA WITH MIDDLE THREE DOTS DOWNWARD;;;;
+0687;ARABIC LETTER TCHEHEH;Lo;0;AL;;;;;N;ARABIC LETTER HAA WITH MIDDLE FOUR DOTS;;;;
+0688;ARABIC LETTER DDAL;Lo;0;AL;;;;;N;ARABIC LETTER DAL WITH SMALL TAH;;;;
+0689;ARABIC LETTER DAL WITH RING;Lo;0;AL;;;;;N;;;;;
+068A;ARABIC LETTER DAL WITH DOT BELOW;Lo;0;AL;;;;;N;;;;;
+068B;ARABIC LETTER DAL WITH DOT BELOW AND SMALL TAH;Lo;0;AL;;;;;N;;;;;
+068C;ARABIC LETTER DAHAL;Lo;0;AL;;;;;N;ARABIC LETTER DAL WITH TWO DOTS ABOVE;;;;
+068D;ARABIC LETTER DDAHAL;Lo;0;AL;;;;;N;ARABIC LETTER DAL WITH TWO DOTS BELOW;;;;
+068E;ARABIC LETTER DUL;Lo;0;AL;;;;;N;ARABIC LETTER DAL WITH THREE DOTS ABOVE;;;;
+068F;ARABIC LETTER DAL WITH THREE DOTS ABOVE DOWNWARDS;Lo;0;AL;;;;;N;ARABIC LETTER DAL WITH THREE DOTS ABOVE DOWNWARD;;;;
+0690;ARABIC LETTER DAL WITH FOUR DOTS ABOVE;Lo;0;AL;;;;;N;;;;;
+0691;ARABIC LETTER RREH;Lo;0;AL;;;;;N;ARABIC LETTER RA WITH SMALL TAH;;;;
+0692;ARABIC LETTER REH WITH SMALL V;Lo;0;AL;;;;;N;ARABIC LETTER RA WITH SMALL V;;;;
+0693;ARABIC LETTER REH WITH RING;Lo;0;AL;;;;;N;ARABIC LETTER RA WITH RING;;;;
+0694;ARABIC LETTER REH WITH DOT BELOW;Lo;0;AL;;;;;N;ARABIC LETTER RA WITH DOT BELOW;;;;
+0695;ARABIC LETTER REH WITH SMALL V BELOW;Lo;0;AL;;;;;N;ARABIC LETTER RA WITH SMALL V BELOW;;;;
+0696;ARABIC LETTER REH WITH DOT BELOW AND DOT ABOVE;Lo;0;AL;;;;;N;ARABIC LETTER RA WITH DOT BELOW AND DOT ABOVE;;;;
+0697;ARABIC LETTER REH WITH TWO DOTS ABOVE;Lo;0;AL;;;;;N;ARABIC LETTER RA WITH TWO DOTS ABOVE;;;;
+0698;ARABIC LETTER JEH;Lo;0;AL;;;;;N;ARABIC LETTER RA WITH THREE DOTS ABOVE;;;;
+0699;ARABIC LETTER REH WITH FOUR DOTS ABOVE;Lo;0;AL;;;;;N;ARABIC LETTER RA WITH FOUR DOTS ABOVE;;;;
+069A;ARABIC LETTER SEEN WITH DOT BELOW AND DOT ABOVE;Lo;0;AL;;;;;N;;;;;
+069B;ARABIC LETTER SEEN WITH THREE DOTS BELOW;Lo;0;AL;;;;;N;;;;;
+069C;ARABIC LETTER SEEN WITH THREE DOTS BELOW AND THREE DOTS ABOVE;Lo;0;AL;;;;;N;;;;;
+069D;ARABIC LETTER SAD WITH TWO DOTS BELOW;Lo;0;AL;;;;;N;;;;;
+069E;ARABIC LETTER SAD WITH THREE DOTS ABOVE;Lo;0;AL;;;;;N;;;;;
+069F;ARABIC LETTER TAH WITH THREE DOTS ABOVE;Lo;0;AL;;;;;N;;;;;
+06A0;ARABIC LETTER AIN WITH THREE DOTS ABOVE;Lo;0;AL;;;;;N;;;;;
+06A1;ARABIC LETTER DOTLESS FEH;Lo;0;AL;;;;;N;ARABIC LETTER DOTLESS FA;;;;
+06A2;ARABIC LETTER FEH WITH DOT MOVED BELOW;Lo;0;AL;;;;;N;ARABIC LETTER FA WITH DOT MOVED BELOW;;;;
+06A3;ARABIC LETTER FEH WITH DOT BELOW;Lo;0;AL;;;;;N;ARABIC LETTER FA WITH DOT BELOW;;;;
+06A4;ARABIC LETTER VEH;Lo;0;AL;;;;;N;ARABIC LETTER FA WITH THREE DOTS ABOVE;;;;
+06A5;ARABIC LETTER FEH WITH THREE DOTS BELOW;Lo;0;AL;;;;;N;ARABIC LETTER FA WITH THREE DOTS BELOW;;;;
+06A6;ARABIC LETTER PEHEH;Lo;0;AL;;;;;N;ARABIC LETTER FA WITH FOUR DOTS ABOVE;;;;
+06A7;ARABIC LETTER QAF WITH DOT ABOVE;Lo;0;AL;;;;;N;;;;;
+06A8;ARABIC LETTER QAF WITH THREE DOTS ABOVE;Lo;0;AL;;;;;N;;;;;
+06A9;ARABIC LETTER KEHEH;Lo;0;AL;;;;;N;ARABIC LETTER OPEN CAF;;;;
+06AA;ARABIC LETTER SWASH KAF;Lo;0;AL;;;;;N;ARABIC LETTER SWASH CAF;;;;
+06AB;ARABIC LETTER KAF WITH RING;Lo;0;AL;;;;;N;ARABIC LETTER CAF WITH RING;;;;
+06AC;ARABIC LETTER KAF WITH DOT ABOVE;Lo;0;AL;;;;;N;ARABIC LETTER CAF WITH DOT ABOVE;;;;
+06AD;ARABIC LETTER NG;Lo;0;AL;;;;;N;ARABIC LETTER CAF WITH THREE DOTS ABOVE;;;;
+06AE;ARABIC LETTER KAF WITH THREE DOTS BELOW;Lo;0;AL;;;;;N;ARABIC LETTER CAF WITH THREE DOTS BELOW;;;;
+06AF;ARABIC LETTER GAF;Lo;0;AL;;;;;N;;;;;
+06B0;ARABIC LETTER GAF WITH RING;Lo;0;AL;;;;;N;;;;;
+06B1;ARABIC LETTER NGOEH;Lo;0;AL;;;;;N;ARABIC LETTER GAF WITH TWO DOTS ABOVE;;;;
+06B2;ARABIC LETTER GAF WITH TWO DOTS BELOW;Lo;0;AL;;;;;N;;;;;
+06B3;ARABIC LETTER GUEH;Lo;0;AL;;;;;N;ARABIC LETTER GAF WITH TWO DOTS VERTICAL BELOW;;;;
+06B4;ARABIC LETTER GAF WITH THREE DOTS ABOVE;Lo;0;AL;;;;;N;;;;;
+06B5;ARABIC LETTER LAM WITH SMALL V;Lo;0;AL;;;;;N;;;;;
+06B6;ARABIC LETTER LAM WITH DOT ABOVE;Lo;0;AL;;;;;N;;;;;
+06B7;ARABIC LETTER LAM WITH THREE DOTS ABOVE;Lo;0;AL;;;;;N;;;;;
+06B8;ARABIC LETTER LAM WITH THREE DOTS BELOW;Lo;0;AL;;;;;N;;;;;
+06B9;ARABIC LETTER NOON WITH DOT BELOW;Lo;0;AL;;;;;N;;;;;
+06BA;ARABIC LETTER NOON GHUNNA;Lo;0;AL;;;;;N;ARABIC LETTER DOTLESS NOON;;;;
+06BB;ARABIC LETTER RNOON;Lo;0;AL;;;;;N;ARABIC LETTER DOTLESS NOON WITH SMALL TAH;;;;
+06BC;ARABIC LETTER NOON WITH RING;Lo;0;AL;;;;;N;;;;;
+06BD;ARABIC LETTER NOON WITH THREE DOTS ABOVE;Lo;0;AL;;;;;N;;;;;
+06BE;ARABIC LETTER HEH DOACHASHMEE;Lo;0;AL;;;;;N;ARABIC LETTER KNOTTED HA;;;;
+06BF;ARABIC LETTER TCHEH WITH DOT ABOVE;Lo;0;AL;;;;;N;;;;;
+06C0;ARABIC LETTER HEH WITH YEH ABOVE;Lo;0;AL;06D5 0654;;;;N;ARABIC LETTER HAMZAH ON HA;;;;
+06C1;ARABIC LETTER HEH GOAL;Lo;0;AL;;;;;N;ARABIC LETTER HA GOAL;;;;
+06C2;ARABIC LETTER HEH GOAL WITH HAMZA ABOVE;Lo;0;AL;06C1 0654;;;;N;ARABIC LETTER HAMZAH ON HA GOAL;;;;
+06C3;ARABIC LETTER TEH MARBUTA GOAL;Lo;0;AL;;;;;N;ARABIC LETTER TAA MARBUTAH GOAL;;;;
+06C4;ARABIC LETTER WAW WITH RING;Lo;0;AL;;;;;N;;;;;
+06C5;ARABIC LETTER KIRGHIZ OE;Lo;0;AL;;;;;N;ARABIC LETTER WAW WITH BAR;;;;
+06C6;ARABIC LETTER OE;Lo;0;AL;;;;;N;ARABIC LETTER WAW WITH SMALL V;;;;
+06C7;ARABIC LETTER U;Lo;0;AL;;;;;N;ARABIC LETTER WAW WITH DAMMAH;;;;
+06C8;ARABIC LETTER YU;Lo;0;AL;;;;;N;ARABIC LETTER WAW WITH ALEF ABOVE;;;;
+06C9;ARABIC LETTER KIRGHIZ YU;Lo;0;AL;;;;;N;ARABIC LETTER WAW WITH INVERTED SMALL V;;;;
+06CA;ARABIC LETTER WAW WITH TWO DOTS ABOVE;Lo;0;AL;;;;;N;;;;;
+06CB;ARABIC LETTER VE;Lo;0;AL;;;;;N;ARABIC LETTER WAW WITH THREE DOTS ABOVE;;;;
+06CC;ARABIC LETTER FARSI YEH;Lo;0;AL;;;;;N;ARABIC LETTER DOTLESS YA;;;;
+06CD;ARABIC LETTER YEH WITH TAIL;Lo;0;AL;;;;;N;ARABIC LETTER YA WITH TAIL;;;;
+06CE;ARABIC LETTER YEH WITH SMALL V;Lo;0;AL;;;;;N;ARABIC LETTER YA WITH SMALL V;;;;
+06CF;ARABIC LETTER WAW WITH DOT ABOVE;Lo;0;AL;;;;;N;;;;;
+06D0;ARABIC LETTER E;Lo;0;AL;;;;;N;ARABIC LETTER YA WITH TWO DOTS VERTICAL BELOW;;;;
+06D1;ARABIC LETTER YEH WITH THREE DOTS BELOW;Lo;0;AL;;;;;N;ARABIC LETTER YA WITH THREE DOTS BELOW;;;;
+06D2;ARABIC LETTER YEH BARREE;Lo;0;AL;;;;;N;ARABIC LETTER YA BARREE;;;;
+06D3;ARABIC LETTER YEH BARREE WITH HAMZA ABOVE;Lo;0;AL;06D2 0654;;;;N;ARABIC LETTER HAMZAH ON YA BARREE;;;;
+06D4;ARABIC FULL STOP;Po;0;AL;;;;;N;ARABIC PERIOD;;;;
+06D5;ARABIC LETTER AE;Lo;0;AL;;;;;N;;;;;
+06D6;ARABIC SMALL HIGH LIGATURE SAD WITH LAM WITH ALEF MAKSURA;Mn;230;NSM;;;;;N;;;;;
+06D7;ARABIC SMALL HIGH LIGATURE QAF WITH LAM WITH ALEF MAKSURA;Mn;230;NSM;;;;;N;;;;;
+06D8;ARABIC SMALL HIGH MEEM INITIAL FORM;Mn;230;NSM;;;;;N;;;;;
+06D9;ARABIC SMALL HIGH LAM ALEF;Mn;230;NSM;;;;;N;;;;;
+06DA;ARABIC SMALL HIGH JEEM;Mn;230;NSM;;;;;N;;;;;
+06DB;ARABIC SMALL HIGH THREE DOTS;Mn;230;NSM;;;;;N;;;;;
+06DC;ARABIC SMALL HIGH SEEN;Mn;230;NSM;;;;;N;;;;;
+06DD;ARABIC END OF AYAH;Cf;0;AN;;;;;N;;;;;
+06DE;ARABIC START OF RUB EL HIZB;So;0;ON;;;;;N;;;;;
+06DF;ARABIC SMALL HIGH ROUNDED ZERO;Mn;230;NSM;;;;;N;;;;;
+06E0;ARABIC SMALL HIGH UPRIGHT RECTANGULAR ZERO;Mn;230;NSM;;;;;N;;;;;
+06E1;ARABIC SMALL HIGH DOTLESS HEAD OF KHAH;Mn;230;NSM;;;;;N;;;;;
+06E2;ARABIC SMALL HIGH MEEM ISOLATED FORM;Mn;230;NSM;;;;;N;;;;;
+06E3;ARABIC SMALL LOW SEEN;Mn;220;NSM;;;;;N;;;;;
+06E4;ARABIC SMALL HIGH MADDA;Mn;230;NSM;;;;;N;;;;;
+06E5;ARABIC SMALL WAW;Lm;0;AL;;;;;N;;;;;
+06E6;ARABIC SMALL YEH;Lm;0;AL;;;;;N;;;;;
+06E7;ARABIC SMALL HIGH YEH;Mn;230;NSM;;;;;N;;;;;
+06E8;ARABIC SMALL HIGH NOON;Mn;230;NSM;;;;;N;;;;;
+06E9;ARABIC PLACE OF SAJDAH;So;0;ON;;;;;N;;;;;
+06EA;ARABIC EMPTY CENTRE LOW STOP;Mn;220;NSM;;;;;N;;;;;
+06EB;ARABIC EMPTY CENTRE HIGH STOP;Mn;230;NSM;;;;;N;;;;;
+06EC;ARABIC ROUNDED HIGH STOP WITH FILLED CENTRE;Mn;230;NSM;;;;;N;;;;;
+06ED;ARABIC SMALL LOW MEEM;Mn;220;NSM;;;;;N;;;;;
+06EE;ARABIC LETTER DAL WITH INVERTED V;Lo;0;AL;;;;;N;;;;;
+06EF;ARABIC LETTER REH WITH INVERTED V;Lo;0;AL;;;;;N;;;;;
+06F0;EXTENDED ARABIC-INDIC DIGIT ZERO;Nd;0;EN;;0;0;0;N;EASTERN ARABIC-INDIC DIGIT ZERO;;;;
+06F1;EXTENDED ARABIC-INDIC DIGIT ONE;Nd;0;EN;;1;1;1;N;EASTERN ARABIC-INDIC DIGIT ONE;;;;
+06F2;EXTENDED ARABIC-INDIC DIGIT TWO;Nd;0;EN;;2;2;2;N;EASTERN ARABIC-INDIC DIGIT TWO;;;;
+06F3;EXTENDED ARABIC-INDIC DIGIT THREE;Nd;0;EN;;3;3;3;N;EASTERN ARABIC-INDIC DIGIT THREE;;;;
+06F4;EXTENDED ARABIC-INDIC DIGIT FOUR;Nd;0;EN;;4;4;4;N;EASTERN ARABIC-INDIC DIGIT FOUR;;;;
+06F5;EXTENDED ARABIC-INDIC DIGIT FIVE;Nd;0;EN;;5;5;5;N;EASTERN ARABIC-INDIC DIGIT FIVE;;;;
+06F6;EXTENDED ARABIC-INDIC DIGIT SIX;Nd;0;EN;;6;6;6;N;EASTERN ARABIC-INDIC DIGIT SIX;;;;
+06F7;EXTENDED ARABIC-INDIC DIGIT SEVEN;Nd;0;EN;;7;7;7;N;EASTERN ARABIC-INDIC DIGIT SEVEN;;;;
+06F8;EXTENDED ARABIC-INDIC DIGIT EIGHT;Nd;0;EN;;8;8;8;N;EASTERN ARABIC-INDIC DIGIT EIGHT;;;;
+06F9;EXTENDED ARABIC-INDIC DIGIT NINE;Nd;0;EN;;9;9;9;N;EASTERN ARABIC-INDIC DIGIT NINE;;;;
+06FA;ARABIC LETTER SHEEN WITH DOT BELOW;Lo;0;AL;;;;;N;;;;;
+06FB;ARABIC LETTER DAD WITH DOT BELOW;Lo;0;AL;;;;;N;;;;;
+06FC;ARABIC LETTER GHAIN WITH DOT BELOW;Lo;0;AL;;;;;N;;;;;
+06FD;ARABIC SIGN SINDHI AMPERSAND;So;0;AL;;;;;N;;;;;
+06FE;ARABIC SIGN SINDHI POSTPOSITION MEN;So;0;AL;;;;;N;;;;;
+06FF;ARABIC LETTER HEH WITH INVERTED V;Lo;0;AL;;;;;N;;;;;
+0700;SYRIAC END OF PARAGRAPH;Po;0;AL;;;;;N;;;;;
+0701;SYRIAC SUPRALINEAR FULL STOP;Po;0;AL;;;;;N;;;;;
+0702;SYRIAC SUBLINEAR FULL STOP;Po;0;AL;;;;;N;;;;;
+0703;SYRIAC SUPRALINEAR COLON;Po;0;AL;;;;;N;;;;;
+0704;SYRIAC SUBLINEAR COLON;Po;0;AL;;;;;N;;;;;
+0705;SYRIAC HORIZONTAL COLON;Po;0;AL;;;;;N;;;;;
+0706;SYRIAC COLON SKEWED LEFT;Po;0;AL;;;;;N;;;;;
+0707;SYRIAC COLON SKEWED RIGHT;Po;0;AL;;;;;N;;;;;
+0708;SYRIAC SUPRALINEAR COLON SKEWED LEFT;Po;0;AL;;;;;N;;;;;
+0709;SYRIAC SUBLINEAR COLON SKEWED RIGHT;Po;0;AL;;;;;N;;;;;
+070A;SYRIAC CONTRACTION;Po;0;AL;;;;;N;;;;;
+070B;SYRIAC HARKLEAN OBELUS;Po;0;AL;;;;;N;;;;;
+070C;SYRIAC HARKLEAN METOBELUS;Po;0;AL;;;;;N;;;;;
+070D;SYRIAC HARKLEAN ASTERISCUS;Po;0;AL;;;;;N;;;;;
+070F;SYRIAC ABBREVIATION MARK;Cf;0;AL;;;;;N;;;;;
+0710;SYRIAC LETTER ALAPH;Lo;0;AL;;;;;N;;;;;
+0711;SYRIAC LETTER SUPERSCRIPT ALAPH;Mn;36;NSM;;;;;N;;;;;
+0712;SYRIAC LETTER BETH;Lo;0;AL;;;;;N;;;;;
+0713;SYRIAC LETTER GAMAL;Lo;0;AL;;;;;N;;;;;
+0714;SYRIAC LETTER GAMAL GARSHUNI;Lo;0;AL;;;;;N;;;;;
+0715;SYRIAC LETTER DALATH;Lo;0;AL;;;;;N;;;;;
+0716;SYRIAC LETTER DOTLESS DALATH RISH;Lo;0;AL;;;;;N;;;;;
+0717;SYRIAC LETTER HE;Lo;0;AL;;;;;N;;;;;
+0718;SYRIAC LETTER WAW;Lo;0;AL;;;;;N;;;;;
+0719;SYRIAC LETTER ZAIN;Lo;0;AL;;;;;N;;;;;
+071A;SYRIAC LETTER HETH;Lo;0;AL;;;;;N;;;;;
+071B;SYRIAC LETTER TETH;Lo;0;AL;;;;;N;;;;;
+071C;SYRIAC LETTER TETH GARSHUNI;Lo;0;AL;;;;;N;;;;;
+071D;SYRIAC LETTER YUDH;Lo;0;AL;;;;;N;;;;;
+071E;SYRIAC LETTER YUDH HE;Lo;0;AL;;;;;N;;;;;
+071F;SYRIAC LETTER KAPH;Lo;0;AL;;;;;N;;;;;
+0720;SYRIAC LETTER LAMADH;Lo;0;AL;;;;;N;;;;;
+0721;SYRIAC LETTER MIM;Lo;0;AL;;;;;N;;;;;
+0722;SYRIAC LETTER NUN;Lo;0;AL;;;;;N;;;;;
+0723;SYRIAC LETTER SEMKATH;Lo;0;AL;;;;;N;;;;;
+0724;SYRIAC LETTER FINAL SEMKATH;Lo;0;AL;;;;;N;;;;;
+0725;SYRIAC LETTER E;Lo;0;AL;;;;;N;;;;;
+0726;SYRIAC LETTER PE;Lo;0;AL;;;;;N;;;;;
+0727;SYRIAC LETTER REVERSED PE;Lo;0;AL;;;;;N;;;;;
+0728;SYRIAC LETTER SADHE;Lo;0;AL;;;;;N;;;;;
+0729;SYRIAC LETTER QAPH;Lo;0;AL;;;;;N;;;;;
+072A;SYRIAC LETTER RISH;Lo;0;AL;;;;;N;;;;;
+072B;SYRIAC LETTER SHIN;Lo;0;AL;;;;;N;;;;;
+072C;SYRIAC LETTER TAW;Lo;0;AL;;;;;N;;;;;
+072D;SYRIAC LETTER PERSIAN BHETH;Lo;0;AL;;;;;N;;;;;
+072E;SYRIAC LETTER PERSIAN GHAMAL;Lo;0;AL;;;;;N;;;;;
+072F;SYRIAC LETTER PERSIAN DHALATH;Lo;0;AL;;;;;N;;;;;
+0730;SYRIAC PTHAHA ABOVE;Mn;230;NSM;;;;;N;;;;;
+0731;SYRIAC PTHAHA BELOW;Mn;220;NSM;;;;;N;;;;;
+0732;SYRIAC PTHAHA DOTTED;Mn;230;NSM;;;;;N;;;;;
+0733;SYRIAC ZQAPHA ABOVE;Mn;230;NSM;;;;;N;;;;;
+0734;SYRIAC ZQAPHA BELOW;Mn;220;NSM;;;;;N;;;;;
+0735;SYRIAC ZQAPHA DOTTED;Mn;230;NSM;;;;;N;;;;;
+0736;SYRIAC RBASA ABOVE;Mn;230;NSM;;;;;N;;;;;
+0737;SYRIAC RBASA BELOW;Mn;220;NSM;;;;;N;;;;;
+0738;SYRIAC DOTTED ZLAMA HORIZONTAL;Mn;220;NSM;;;;;N;;;;;
+0739;SYRIAC DOTTED ZLAMA ANGULAR;Mn;220;NSM;;;;;N;;;;;
+073A;SYRIAC HBASA ABOVE;Mn;230;NSM;;;;;N;;;;;
+073B;SYRIAC HBASA BELOW;Mn;220;NSM;;;;;N;;;;;
+073C;SYRIAC HBASA-ESASA DOTTED;Mn;220;NSM;;;;;N;;;;;
+073D;SYRIAC ESASA ABOVE;Mn;230;NSM;;;;;N;;;;;
+073E;SYRIAC ESASA BELOW;Mn;220;NSM;;;;;N;;;;;
+073F;SYRIAC RWAHA;Mn;230;NSM;;;;;N;;;;;
+0740;SYRIAC FEMININE DOT;Mn;230;NSM;;;;;N;;;;;
+0741;SYRIAC QUSHSHAYA;Mn;230;NSM;;;;;N;;;;;
+0742;SYRIAC RUKKAKHA;Mn;220;NSM;;;;;N;;;;;
+0743;SYRIAC TWO VERTICAL DOTS ABOVE;Mn;230;NSM;;;;;N;;;;;
+0744;SYRIAC TWO VERTICAL DOTS BELOW;Mn;220;NSM;;;;;N;;;;;
+0745;SYRIAC THREE DOTS ABOVE;Mn;230;NSM;;;;;N;;;;;
+0746;SYRIAC THREE DOTS BELOW;Mn;220;NSM;;;;;N;;;;;
+0747;SYRIAC OBLIQUE LINE ABOVE;Mn;230;NSM;;;;;N;;;;;
+0748;SYRIAC OBLIQUE LINE BELOW;Mn;220;NSM;;;;;N;;;;;
+0749;SYRIAC MUSIC;Mn;230;NSM;;;;;N;;;;;
+074A;SYRIAC BARREKH;Mn;230;NSM;;;;;N;;;;;
+074D;SYRIAC LETTER SOGDIAN ZHAIN;Lo;0;AL;;;;;N;;;;;
+074E;SYRIAC LETTER SOGDIAN KHAPH;Lo;0;AL;;;;;N;;;;;
+074F;SYRIAC LETTER SOGDIAN FE;Lo;0;AL;;;;;N;;;;;
+0750;ARABIC LETTER BEH WITH THREE DOTS HORIZONTALLY BELOW;Lo;0;AL;;;;;N;;;;;
+0751;ARABIC LETTER BEH WITH DOT BELOW AND THREE DOTS ABOVE;Lo;0;AL;;;;;N;;;;;
+0752;ARABIC LETTER BEH WITH THREE DOTS POINTING UPWARDS BELOW;Lo;0;AL;;;;;N;;;;;
+0753;ARABIC LETTER BEH WITH THREE DOTS POINTING UPWARDS BELOW AND TWO DOTS ABOVE;Lo;0;AL;;;;;N;;;;;
+0754;ARABIC LETTER BEH WITH TWO DOTS BELOW AND DOT ABOVE;Lo;0;AL;;;;;N;;;;;
+0755;ARABIC LETTER BEH WITH INVERTED SMALL V BELOW;Lo;0;AL;;;;;N;;;;;
+0756;ARABIC LETTER BEH WITH SMALL V;Lo;0;AL;;;;;N;;;;;
+0757;ARABIC LETTER HAH WITH TWO DOTS ABOVE;Lo;0;AL;;;;;N;;;;;
+0758;ARABIC LETTER HAH WITH THREE DOTS POINTING UPWARDS BELOW;Lo;0;AL;;;;;N;;;;;
+0759;ARABIC LETTER DAL WITH TWO DOTS VERTICALLY BELOW AND SMALL TAH;Lo;0;AL;;;;;N;;;;;
+075A;ARABIC LETTER DAL WITH INVERTED SMALL V BELOW;Lo;0;AL;;;;;N;;;;;
+075B;ARABIC LETTER REH WITH STROKE;Lo;0;AL;;;;;N;;;;;
+075C;ARABIC LETTER SEEN WITH FOUR DOTS ABOVE;Lo;0;AL;;;;;N;;;;;
+075D;ARABIC LETTER AIN WITH TWO DOTS ABOVE;Lo;0;AL;;;;;N;;;;;
+075E;ARABIC LETTER AIN WITH THREE DOTS POINTING DOWNWARDS ABOVE;Lo;0;AL;;;;;N;;;;;
+075F;ARABIC LETTER AIN WITH TWO DOTS VERTICALLY ABOVE;Lo;0;AL;;;;;N;;;;;
+0760;ARABIC LETTER FEH WITH TWO DOTS BELOW;Lo;0;AL;;;;;N;;;;;
+0761;ARABIC LETTER FEH WITH THREE DOTS POINTING UPWARDS BELOW;Lo;0;AL;;;;;N;;;;;
+0762;ARABIC LETTER KEHEH WITH DOT ABOVE;Lo;0;AL;;;;;N;;;;;
+0763;ARABIC LETTER KEHEH WITH THREE DOTS ABOVE;Lo;0;AL;;;;;N;;;;;
+0764;ARABIC LETTER KEHEH WITH THREE DOTS POINTING UPWARDS BELOW;Lo;0;AL;;;;;N;;;;;
+0765;ARABIC LETTER MEEM WITH DOT ABOVE;Lo;0;AL;;;;;N;;;;;
+0766;ARABIC LETTER MEEM WITH DOT BELOW;Lo;0;AL;;;;;N;;;;;
+0767;ARABIC LETTER NOON WITH TWO DOTS BELOW;Lo;0;AL;;;;;N;;;;;
+0768;ARABIC LETTER NOON WITH SMALL TAH;Lo;0;AL;;;;;N;;;;;
+0769;ARABIC LETTER NOON WITH SMALL V;Lo;0;AL;;;;;N;;;;;
+076A;ARABIC LETTER LAM WITH BAR;Lo;0;AL;;;;;N;;;;;
+076B;ARABIC LETTER REH WITH TWO DOTS VERTICALLY ABOVE;Lo;0;AL;;;;;N;;;;;
+076C;ARABIC LETTER REH WITH HAMZA ABOVE;Lo;0;AL;;;;;N;;;;;
+076D;ARABIC LETTER SEEN WITH TWO DOTS VERTICALLY ABOVE;Lo;0;AL;;;;;N;;;;;
+076E;ARABIC LETTER HAH WITH SMALL ARABIC LETTER TAH BELOW;Lo;0;AL;;;;;N;;;;;
+076F;ARABIC LETTER HAH WITH SMALL ARABIC LETTER TAH AND TWO DOTS;Lo;0;AL;;;;;N;;;;;
+0770;ARABIC LETTER SEEN WITH SMALL ARABIC LETTER TAH AND TWO DOTS;Lo;0;AL;;;;;N;;;;;
+0771;ARABIC LETTER REH WITH SMALL ARABIC LETTER TAH AND TWO DOTS;Lo;0;AL;;;;;N;;;;;
+0772;ARABIC LETTER HAH WITH SMALL ARABIC LETTER TAH ABOVE;Lo;0;AL;;;;;N;;;;;
+0773;ARABIC LETTER ALEF WITH EXTENDED ARABIC-INDIC DIGIT TWO ABOVE;Lo;0;AL;;;;;N;;;;;
+0774;ARABIC LETTER ALEF WITH EXTENDED ARABIC-INDIC DIGIT THREE ABOVE;Lo;0;AL;;;;;N;;;;;
+0775;ARABIC LETTER FARSI YEH WITH EXTENDED ARABIC-INDIC DIGIT TWO ABOVE;Lo;0;AL;;;;;N;;;;;
+0776;ARABIC LETTER FARSI YEH WITH EXTENDED ARABIC-INDIC DIGIT THREE ABOVE;Lo;0;AL;;;;;N;;;;;
+0777;ARABIC LETTER FARSI YEH WITH EXTENDED ARABIC-INDIC DIGIT FOUR BELOW;Lo;0;AL;;;;;N;;;;;
+0778;ARABIC LETTER WAW WITH EXTENDED ARABIC-INDIC DIGIT TWO ABOVE;Lo;0;AL;;;;;N;;;;;
+0779;ARABIC LETTER WAW WITH EXTENDED ARABIC-INDIC DIGIT THREE ABOVE;Lo;0;AL;;;;;N;;;;;
+077A;ARABIC LETTER YEH BARREE WITH EXTENDED ARABIC-INDIC DIGIT TWO ABOVE;Lo;0;AL;;;;;N;;;;;
+077B;ARABIC LETTER YEH BARREE WITH EXTENDED ARABIC-INDIC DIGIT THREE ABOVE;Lo;0;AL;;;;;N;;;;;
+077C;ARABIC LETTER HAH WITH EXTENDED ARABIC-INDIC DIGIT FOUR BELOW;Lo;0;AL;;;;;N;;;;;
+077D;ARABIC LETTER SEEN WITH EXTENDED ARABIC-INDIC DIGIT FOUR ABOVE;Lo;0;AL;;;;;N;;;;;
+077E;ARABIC LETTER SEEN WITH INVERTED V;Lo;0;AL;;;;;N;;;;;
+077F;ARABIC LETTER KAF WITH TWO DOTS ABOVE;Lo;0;AL;;;;;N;;;;;
+0780;THAANA LETTER HAA;Lo;0;AL;;;;;N;;;;;
+0781;THAANA LETTER SHAVIYANI;Lo;0;AL;;;;;N;;;;;
+0782;THAANA LETTER NOONU;Lo;0;AL;;;;;N;;;;;
+0783;THAANA LETTER RAA;Lo;0;AL;;;;;N;;;;;
+0784;THAANA LETTER BAA;Lo;0;AL;;;;;N;;;;;
+0785;THAANA LETTER LHAVIYANI;Lo;0;AL;;;;;N;;;;;
+0786;THAANA LETTER KAAFU;Lo;0;AL;;;;;N;;;;;
+0787;THAANA LETTER ALIFU;Lo;0;AL;;;;;N;;;;;
+0788;THAANA LETTER VAAVU;Lo;0;AL;;;;;N;;;;;
+0789;THAANA LETTER MEEMU;Lo;0;AL;;;;;N;;;;;
+078A;THAANA LETTER FAAFU;Lo;0;AL;;;;;N;;;;;
+078B;THAANA LETTER DHAALU;Lo;0;AL;;;;;N;;;;;
+078C;THAANA LETTER THAA;Lo;0;AL;;;;;N;;;;;
+078D;THAANA LETTER LAAMU;Lo;0;AL;;;;;N;;;;;
+078E;THAANA LETTER GAAFU;Lo;0;AL;;;;;N;;;;;
+078F;THAANA LETTER GNAVIYANI;Lo;0;AL;;;;;N;;;;;
+0790;THAANA LETTER SEENU;Lo;0;AL;;;;;N;;;;;
+0791;THAANA LETTER DAVIYANI;Lo;0;AL;;;;;N;;;;;
+0792;THAANA LETTER ZAVIYANI;Lo;0;AL;;;;;N;;;;;
+0793;THAANA LETTER TAVIYANI;Lo;0;AL;;;;;N;;;;;
+0794;THAANA LETTER YAA;Lo;0;AL;;;;;N;;;;;
+0795;THAANA LETTER PAVIYANI;Lo;0;AL;;;;;N;;;;;
+0796;THAANA LETTER JAVIYANI;Lo;0;AL;;;;;N;;;;;
+0797;THAANA LETTER CHAVIYANI;Lo;0;AL;;;;;N;;;;;
+0798;THAANA LETTER TTAA;Lo;0;AL;;;;;N;;;;;
+0799;THAANA LETTER HHAA;Lo;0;AL;;;;;N;;;;;
+079A;THAANA LETTER KHAA;Lo;0;AL;;;;;N;;;;;
+079B;THAANA LETTER THAALU;Lo;0;AL;;;;;N;;;;;
+079C;THAANA LETTER ZAA;Lo;0;AL;;;;;N;;;;;
+079D;THAANA LETTER SHEENU;Lo;0;AL;;;;;N;;;;;
+079E;THAANA LETTER SAADHU;Lo;0;AL;;;;;N;;;;;
+079F;THAANA LETTER DAADHU;Lo;0;AL;;;;;N;;;;;
+07A0;THAANA LETTER TO;Lo;0;AL;;;;;N;;;;;
+07A1;THAANA LETTER ZO;Lo;0;AL;;;;;N;;;;;
+07A2;THAANA LETTER AINU;Lo;0;AL;;;;;N;;;;;
+07A3;THAANA LETTER GHAINU;Lo;0;AL;;;;;N;;;;;
+07A4;THAANA LETTER QAAFU;Lo;0;AL;;;;;N;;;;;
+07A5;THAANA LETTER WAAVU;Lo;0;AL;;;;;N;;;;;
+07A6;THAANA ABAFILI;Mn;0;NSM;;;;;N;;;;;
+07A7;THAANA AABAAFILI;Mn;0;NSM;;;;;N;;;;;
+07A8;THAANA IBIFILI;Mn;0;NSM;;;;;N;;;;;
+07A9;THAANA EEBEEFILI;Mn;0;NSM;;;;;N;;;;;
+07AA;THAANA UBUFILI;Mn;0;NSM;;;;;N;;;;;
+07AB;THAANA OOBOOFILI;Mn;0;NSM;;;;;N;;;;;
+07AC;THAANA EBEFILI;Mn;0;NSM;;;;;N;;;;;
+07AD;THAANA EYBEYFILI;Mn;0;NSM;;;;;N;;;;;
+07AE;THAANA OBOFILI;Mn;0;NSM;;;;;N;;;;;
+07AF;THAANA OABOAFILI;Mn;0;NSM;;;;;N;;;;;
+07B0;THAANA SUKUN;Mn;0;NSM;;;;;N;;;;;
+07B1;THAANA LETTER NAA;Lo;0;AL;;;;;N;;;;;
+07C0;NKO DIGIT ZERO;Nd;0;R;;0;0;0;N;;;;;
+07C1;NKO DIGIT ONE;Nd;0;R;;1;1;1;N;;;;;
+07C2;NKO DIGIT TWO;Nd;0;R;;2;2;2;N;;;;;
+07C3;NKO DIGIT THREE;Nd;0;R;;3;3;3;N;;;;;
+07C4;NKO DIGIT FOUR;Nd;0;R;;4;4;4;N;;;;;
+07C5;NKO DIGIT FIVE;Nd;0;R;;5;5;5;N;;;;;
+07C6;NKO DIGIT SIX;Nd;0;R;;6;6;6;N;;;;;
+07C7;NKO DIGIT SEVEN;Nd;0;R;;7;7;7;N;;;;;
+07C8;NKO DIGIT EIGHT;Nd;0;R;;8;8;8;N;;;;;
+07C9;NKO DIGIT NINE;Nd;0;R;;9;9;9;N;;;;;
+07CA;NKO LETTER A;Lo;0;R;;;;;N;;;;;
+07CB;NKO LETTER EE;Lo;0;R;;;;;N;;;;;
+07CC;NKO LETTER I;Lo;0;R;;;;;N;;;;;
+07CD;NKO LETTER E;Lo;0;R;;;;;N;;;;;
+07CE;NKO LETTER U;Lo;0;R;;;;;N;;;;;
+07CF;NKO LETTER OO;Lo;0;R;;;;;N;;;;;
+07D0;NKO LETTER O;Lo;0;R;;;;;N;;;;;
+07D1;NKO LETTER DAGBASINNA;Lo;0;R;;;;;N;;;;;
+07D2;NKO LETTER N;Lo;0;R;;;;;N;;;;;
+07D3;NKO LETTER BA;Lo;0;R;;;;;N;;;;;
+07D4;NKO LETTER PA;Lo;0;R;;;;;N;;;;;
+07D5;NKO LETTER TA;Lo;0;R;;;;;N;;;;;
+07D6;NKO LETTER JA;Lo;0;R;;;;;N;;;;;
+07D7;NKO LETTER CHA;Lo;0;R;;;;;N;;;;;
+07D8;NKO LETTER DA;Lo;0;R;;;;;N;;;;;
+07D9;NKO LETTER RA;Lo;0;R;;;;;N;;;;;
+07DA;NKO LETTER RRA;Lo;0;R;;;;;N;;;;;
+07DB;NKO LETTER SA;Lo;0;R;;;;;N;;;;;
+07DC;NKO LETTER GBA;Lo;0;R;;;;;N;;;;;
+07DD;NKO LETTER FA;Lo;0;R;;;;;N;;;;;
+07DE;NKO LETTER KA;Lo;0;R;;;;;N;;;;;
+07DF;NKO LETTER LA;Lo;0;R;;;;;N;;;;;
+07E0;NKO LETTER NA WOLOSO;Lo;0;R;;;;;N;;;;;
+07E1;NKO LETTER MA;Lo;0;R;;;;;N;;;;;
+07E2;NKO LETTER NYA;Lo;0;R;;;;;N;;;;;
+07E3;NKO LETTER NA;Lo;0;R;;;;;N;;;;;
+07E4;NKO LETTER HA;Lo;0;R;;;;;N;;;;;
+07E5;NKO LETTER WA;Lo;0;R;;;;;N;;;;;
+07E6;NKO LETTER YA;Lo;0;R;;;;;N;;;;;
+07E7;NKO LETTER NYA WOLOSO;Lo;0;R;;;;;N;;;;;
+07E8;NKO LETTER JONA JA;Lo;0;R;;;;;N;;;;;
+07E9;NKO LETTER JONA CHA;Lo;0;R;;;;;N;;;;;
+07EA;NKO LETTER JONA RA;Lo;0;R;;;;;N;;;;;
+07EB;NKO COMBINING SHORT HIGH TONE;Mn;230;NSM;;;;;N;;;;;
+07EC;NKO COMBINING SHORT LOW TONE;Mn;230;NSM;;;;;N;;;;;
+07ED;NKO COMBINING SHORT RISING TONE;Mn;230;NSM;;;;;N;;;;;
+07EE;NKO COMBINING LONG DESCENDING TONE;Mn;230;NSM;;;;;N;;;;;
+07EF;NKO COMBINING LONG HIGH TONE;Mn;230;NSM;;;;;N;;;;;
+07F0;NKO COMBINING LONG LOW TONE;Mn;230;NSM;;;;;N;;;;;
+07F1;NKO COMBINING LONG RISING TONE;Mn;230;NSM;;;;;N;;;;;
+07F2;NKO COMBINING NASALIZATION MARK;Mn;220;NSM;;;;;N;;;;;
+07F3;NKO COMBINING DOUBLE DOT ABOVE;Mn;230;NSM;;;;;N;;;;;
+07F4;NKO HIGH TONE APOSTROPHE;Lm;0;R;;;;;N;;;;;
+07F5;NKO LOW TONE APOSTROPHE;Lm;0;R;;;;;N;;;;;
+07F6;NKO SYMBOL OO DENNEN;So;0;ON;;;;;N;;;;;
+07F7;NKO SYMBOL GBAKURUNEN;Po;0;ON;;;;;N;;;;;
+07F8;NKO COMMA;Po;0;ON;;;;;N;;;;;
+07F9;NKO EXCLAMATION MARK;Po;0;ON;;;;;N;;;;;
+07FA;NKO LAJANYALAN;Lm;0;R;;;;;N;;;;;
+0800;SAMARITAN LETTER ALAF;Lo;0;R;;;;;N;;;;;
+0801;SAMARITAN LETTER BIT;Lo;0;R;;;;;N;;;;;
+0802;SAMARITAN LETTER GAMAN;Lo;0;R;;;;;N;;;;;
+0803;SAMARITAN LETTER DALAT;Lo;0;R;;;;;N;;;;;
+0804;SAMARITAN LETTER IY;Lo;0;R;;;;;N;;;;;
+0805;SAMARITAN LETTER BAA;Lo;0;R;;;;;N;;;;;
+0806;SAMARITAN LETTER ZEN;Lo;0;R;;;;;N;;;;;
+0807;SAMARITAN LETTER IT;Lo;0;R;;;;;N;;;;;
+0808;SAMARITAN LETTER TIT;Lo;0;R;;;;;N;;;;;
+0809;SAMARITAN LETTER YUT;Lo;0;R;;;;;N;;;;;
+080A;SAMARITAN LETTER KAAF;Lo;0;R;;;;;N;;;;;
+080B;SAMARITAN LETTER LABAT;Lo;0;R;;;;;N;;;;;
+080C;SAMARITAN LETTER MIM;Lo;0;R;;;;;N;;;;;
+080D;SAMARITAN LETTER NUN;Lo;0;R;;;;;N;;;;;
+080E;SAMARITAN LETTER SINGAAT;Lo;0;R;;;;;N;;;;;
+080F;SAMARITAN LETTER IN;Lo;0;R;;;;;N;;;;;
+0810;SAMARITAN LETTER FI;Lo;0;R;;;;;N;;;;;
+0811;SAMARITAN LETTER TSAADIY;Lo;0;R;;;;;N;;;;;
+0812;SAMARITAN LETTER QUF;Lo;0;R;;;;;N;;;;;
+0813;SAMARITAN LETTER RISH;Lo;0;R;;;;;N;;;;;
+0814;SAMARITAN LETTER SHAN;Lo;0;R;;;;;N;;;;;
+0815;SAMARITAN LETTER TAAF;Lo;0;R;;;;;N;;;;;
+0816;SAMARITAN MARK IN;Mn;230;NSM;;;;;N;;;;;
+0817;SAMARITAN MARK IN-ALAF;Mn;230;NSM;;;;;N;;;;;
+0818;SAMARITAN MARK OCCLUSION;Mn;230;NSM;;;;;N;;;;;
+0819;SAMARITAN MARK DAGESH;Mn;230;NSM;;;;;N;;;;;
+081A;SAMARITAN MODIFIER LETTER EPENTHETIC YUT;Lm;0;R;;;;;N;;;;;
+081B;SAMARITAN MARK EPENTHETIC YUT;Mn;230;NSM;;;;;N;;;;;
+081C;SAMARITAN VOWEL SIGN LONG E;Mn;230;NSM;;;;;N;;;;;
+081D;SAMARITAN VOWEL SIGN E;Mn;230;NSM;;;;;N;;;;;
+081E;SAMARITAN VOWEL SIGN OVERLONG AA;Mn;230;NSM;;;;;N;;;;;
+081F;SAMARITAN VOWEL SIGN LONG AA;Mn;230;NSM;;;;;N;;;;;
+0820;SAMARITAN VOWEL SIGN AA;Mn;230;NSM;;;;;N;;;;;
+0821;SAMARITAN VOWEL SIGN OVERLONG A;Mn;230;NSM;;;;;N;;;;;
+0822;SAMARITAN VOWEL SIGN LONG A;Mn;230;NSM;;;;;N;;;;;
+0823;SAMARITAN VOWEL SIGN A;Mn;230;NSM;;;;;N;;;;;
+0824;SAMARITAN MODIFIER LETTER SHORT A;Lm;0;R;;;;;N;;;;;
+0825;SAMARITAN VOWEL SIGN SHORT A;Mn;230;NSM;;;;;N;;;;;
+0826;SAMARITAN VOWEL SIGN LONG U;Mn;230;NSM;;;;;N;;;;;
+0827;SAMARITAN VOWEL SIGN U;Mn;230;NSM;;;;;N;;;;;
+0828;SAMARITAN MODIFIER LETTER I;Lm;0;R;;;;;N;;;;;
+0829;SAMARITAN VOWEL SIGN LONG I;Mn;230;NSM;;;;;N;;;;;
+082A;SAMARITAN VOWEL SIGN I;Mn;230;NSM;;;;;N;;;;;
+082B;SAMARITAN VOWEL SIGN O;Mn;230;NSM;;;;;N;;;;;
+082C;SAMARITAN VOWEL SIGN SUKUN;Mn;230;NSM;;;;;N;;;;;
+082D;SAMARITAN MARK NEQUDAA;Mn;230;NSM;;;;;N;;;;;
+0830;SAMARITAN PUNCTUATION NEQUDAA;Po;0;R;;;;;N;;;;;
+0831;SAMARITAN PUNCTUATION AFSAAQ;Po;0;R;;;;;N;;;;;
+0832;SAMARITAN PUNCTUATION ANGED;Po;0;R;;;;;N;;;;;
+0833;SAMARITAN PUNCTUATION BAU;Po;0;R;;;;;N;;;;;
+0834;SAMARITAN PUNCTUATION ATMAAU;Po;0;R;;;;;N;;;;;
+0835;SAMARITAN PUNCTUATION SHIYYAALAA;Po;0;R;;;;;N;;;;;
+0836;SAMARITAN ABBREVIATION MARK;Po;0;R;;;;;N;;;;;
+0837;SAMARITAN PUNCTUATION MELODIC QITSA;Po;0;R;;;;;N;;;;;
+0838;SAMARITAN PUNCTUATION ZIQAA;Po;0;R;;;;;N;;;;;
+0839;SAMARITAN PUNCTUATION QITSA;Po;0;R;;;;;N;;;;;
+083A;SAMARITAN PUNCTUATION ZAEF;Po;0;R;;;;;N;;;;;
+083B;SAMARITAN PUNCTUATION TURU;Po;0;R;;;;;N;;;;;
+083C;SAMARITAN PUNCTUATION ARKAANU;Po;0;R;;;;;N;;;;;
+083D;SAMARITAN PUNCTUATION SOF MASHFAAT;Po;0;R;;;;;N;;;;;
+083E;SAMARITAN PUNCTUATION ANNAAU;Po;0;R;;;;;N;;;;;
+0840;MANDAIC LETTER HALQA;Lo;0;R;;;;;N;;;;;
+0841;MANDAIC LETTER AB;Lo;0;R;;;;;N;;;;;
+0842;MANDAIC LETTER AG;Lo;0;R;;;;;N;;;;;
+0843;MANDAIC LETTER AD;Lo;0;R;;;;;N;;;;;
+0844;MANDAIC LETTER AH;Lo;0;R;;;;;N;;;;;
+0845;MANDAIC LETTER USHENNA;Lo;0;R;;;;;N;;;;;
+0846;MANDAIC LETTER AZ;Lo;0;R;;;;;N;;;;;
+0847;MANDAIC LETTER IT;Lo;0;R;;;;;N;;;;;
+0848;MANDAIC LETTER ATT;Lo;0;R;;;;;N;;;;;
+0849;MANDAIC LETTER AKSA;Lo;0;R;;;;;N;;;;;
+084A;MANDAIC LETTER AK;Lo;0;R;;;;;N;;;;;
+084B;MANDAIC LETTER AL;Lo;0;R;;;;;N;;;;;
+084C;MANDAIC LETTER AM;Lo;0;R;;;;;N;;;;;
+084D;MANDAIC LETTER AN;Lo;0;R;;;;;N;;;;;
+084E;MANDAIC LETTER AS;Lo;0;R;;;;;N;;;;;
+084F;MANDAIC LETTER IN;Lo;0;R;;;;;N;;;;;
+0850;MANDAIC LETTER AP;Lo;0;R;;;;;N;;;;;
+0851;MANDAIC LETTER ASZ;Lo;0;R;;;;;N;;;;;
+0852;MANDAIC LETTER AQ;Lo;0;R;;;;;N;;;;;
+0853;MANDAIC LETTER AR;Lo;0;R;;;;;N;;;;;
+0854;MANDAIC LETTER ASH;Lo;0;R;;;;;N;;;;;
+0855;MANDAIC LETTER AT;Lo;0;R;;;;;N;;;;;
+0856;MANDAIC LETTER DUSHENNA;Lo;0;R;;;;;N;;;;;
+0857;MANDAIC LETTER KAD;Lo;0;R;;;;;N;;;;;
+0858;MANDAIC LETTER AIN;Lo;0;R;;;;;N;;;;;
+0859;MANDAIC AFFRICATION MARK;Mn;220;NSM;;;;;N;;;;;
+085A;MANDAIC VOCALIZATION MARK;Mn;220;NSM;;;;;N;;;;;
+085B;MANDAIC GEMINATION MARK;Mn;220;NSM;;;;;N;;;;;
+085E;MANDAIC PUNCTUATION;Po;0;R;;;;;N;;;;;
+08A0;ARABIC LETTER BEH WITH SMALL V BELOW;Lo;0;AL;;;;;N;;;;;
+08A1;ARABIC LETTER BEH WITH HAMZA ABOVE;Lo;0;AL;;;;;N;;;;;
+08A2;ARABIC LETTER JEEM WITH TWO DOTS ABOVE;Lo;0;AL;;;;;N;;;;;
+08A3;ARABIC LETTER TAH WITH TWO DOTS ABOVE;Lo;0;AL;;;;;N;;;;;
+08A4;ARABIC LETTER FEH WITH DOT BELOW AND THREE DOTS ABOVE;Lo;0;AL;;;;;N;;;;;
+08A5;ARABIC LETTER QAF WITH DOT BELOW;Lo;0;AL;;;;;N;;;;;
+08A6;ARABIC LETTER LAM WITH DOUBLE BAR;Lo;0;AL;;;;;N;;;;;
+08A7;ARABIC LETTER MEEM WITH THREE DOTS ABOVE;Lo;0;AL;;;;;N;;;;;
+08A8;ARABIC LETTER YEH WITH TWO DOTS BELOW AND HAMZA ABOVE;Lo;0;AL;;;;;N;;;;;
+08A9;ARABIC LETTER YEH WITH TWO DOTS BELOW AND DOT ABOVE;Lo;0;AL;;;;;N;;;;;
+08AA;ARABIC LETTER REH WITH LOOP;Lo;0;AL;;;;;N;;;;;
+08AB;ARABIC LETTER WAW WITH DOT WITHIN;Lo;0;AL;;;;;N;;;;;
+08AC;ARABIC LETTER ROHINGYA YEH;Lo;0;AL;;;;;N;;;;;
+08AD;ARABIC LETTER LOW ALEF;Lo;0;AL;;;;;N;;;;;
+08AE;ARABIC LETTER DAL WITH THREE DOTS BELOW;Lo;0;AL;;;;;N;;;;;
+08AF;ARABIC LETTER SAD WITH THREE DOTS BELOW;Lo;0;AL;;;;;N;;;;;
+08B0;ARABIC LETTER GAF WITH INVERTED STROKE;Lo;0;AL;;;;;N;;;;;
+08B1;ARABIC LETTER STRAIGHT WAW;Lo;0;AL;;;;;N;;;;;
+08B2;ARABIC LETTER ZAIN WITH INVERTED V ABOVE;Lo;0;AL;;;;;N;;;;;
+08B3;ARABIC LETTER AIN WITH THREE DOTS BELOW;Lo;0;AL;;;;;N;;;;;
+08B4;ARABIC LETTER KAF WITH DOT BELOW;Lo;0;AL;;;;;N;;;;;
+08B6;ARABIC LETTER BEH WITH SMALL MEEM ABOVE;Lo;0;AL;;;;;N;;;;;
+08B7;ARABIC LETTER PEH WITH SMALL MEEM ABOVE;Lo;0;AL;;;;;N;;;;;
+08B8;ARABIC LETTER TEH WITH SMALL TEH ABOVE;Lo;0;AL;;;;;N;;;;;
+08B9;ARABIC LETTER REH WITH SMALL NOON ABOVE;Lo;0;AL;;;;;N;;;;;
+08BA;ARABIC LETTER YEH WITH TWO DOTS BELOW AND SMALL NOON ABOVE;Lo;0;AL;;;;;N;;;;;
+08BB;ARABIC LETTER AFRICAN FEH;Lo;0;AL;;;;;N;;;;;
+08BC;ARABIC LETTER AFRICAN QAF;Lo;0;AL;;;;;N;;;;;
+08BD;ARABIC LETTER AFRICAN NOON;Lo;0;AL;;;;;N;;;;;
+08D4;ARABIC SMALL HIGH WORD AR-RUB;Mn;230;NSM;;;;;N;;;;;
+08D5;ARABIC SMALL HIGH SAD;Mn;230;NSM;;;;;N;;;;;
+08D6;ARABIC SMALL HIGH AIN;Mn;230;NSM;;;;;N;;;;;
+08D7;ARABIC SMALL HIGH QAF;Mn;230;NSM;;;;;N;;;;;
+08D8;ARABIC SMALL HIGH NOON WITH KASRA;Mn;230;NSM;;;;;N;;;;;
+08D9;ARABIC SMALL LOW NOON WITH KASRA;Mn;230;NSM;;;;;N;;;;;
+08DA;ARABIC SMALL HIGH WORD ATH-THALATHA;Mn;230;NSM;;;;;N;;;;;
+08DB;ARABIC SMALL HIGH WORD AS-SAJDA;Mn;230;NSM;;;;;N;;;;;
+08DC;ARABIC SMALL HIGH WORD AN-NISF;Mn;230;NSM;;;;;N;;;;;
+08DD;ARABIC SMALL HIGH WORD SAKTA;Mn;230;NSM;;;;;N;;;;;
+08DE;ARABIC SMALL HIGH WORD QIF;Mn;230;NSM;;;;;N;;;;;
+08DF;ARABIC SMALL HIGH WORD WAQFA;Mn;230;NSM;;;;;N;;;;;
+08E0;ARABIC SMALL HIGH FOOTNOTE MARKER;Mn;230;NSM;;;;;N;;;;;
+08E1;ARABIC SMALL HIGH SIGN SAFHA;Mn;230;NSM;;;;;N;;;;;
+08E2;ARABIC DISPUTED END OF AYAH;Cf;0;AN;;;;;N;;;;;
+08E3;ARABIC TURNED DAMMA BELOW;Mn;220;NSM;;;;;N;;;;;
+08E4;ARABIC CURLY FATHA;Mn;230;NSM;;;;;N;;;;;
+08E5;ARABIC CURLY DAMMA;Mn;230;NSM;;;;;N;;;;;
+08E6;ARABIC CURLY KASRA;Mn;220;NSM;;;;;N;;;;;
+08E7;ARABIC CURLY FATHATAN;Mn;230;NSM;;;;;N;;;;;
+08E8;ARABIC CURLY DAMMATAN;Mn;230;NSM;;;;;N;;;;;
+08E9;ARABIC CURLY KASRATAN;Mn;220;NSM;;;;;N;;;;;
+08EA;ARABIC TONE ONE DOT ABOVE;Mn;230;NSM;;;;;N;;;;;
+08EB;ARABIC TONE TWO DOTS ABOVE;Mn;230;NSM;;;;;N;;;;;
+08EC;ARABIC TONE LOOP ABOVE;Mn;230;NSM;;;;;N;;;;;
+08ED;ARABIC TONE ONE DOT BELOW;Mn;220;NSM;;;;;N;;;;;
+08EE;ARABIC TONE TWO DOTS BELOW;Mn;220;NSM;;;;;N;;;;;
+08EF;ARABIC TONE LOOP BELOW;Mn;220;NSM;;;;;N;;;;;
+08F0;ARABIC OPEN FATHATAN;Mn;27;NSM;;;;;N;;;;;
+08F1;ARABIC OPEN DAMMATAN;Mn;28;NSM;;;;;N;;;;;
+08F2;ARABIC OPEN KASRATAN;Mn;29;NSM;;;;;N;;;;;
+08F3;ARABIC SMALL HIGH WAW;Mn;230;NSM;;;;;N;;;;;
+08F4;ARABIC FATHA WITH RING;Mn;230;NSM;;;;;N;;;;;
+08F5;ARABIC FATHA WITH DOT ABOVE;Mn;230;NSM;;;;;N;;;;;
+08F6;ARABIC KASRA WITH DOT BELOW;Mn;220;NSM;;;;;N;;;;;
+08F7;ARABIC LEFT ARROWHEAD ABOVE;Mn;230;NSM;;;;;N;;;;;
+08F8;ARABIC RIGHT ARROWHEAD ABOVE;Mn;230;NSM;;;;;N;;;;;
+08F9;ARABIC LEFT ARROWHEAD BELOW;Mn;220;NSM;;;;;N;;;;;
+08FA;ARABIC RIGHT ARROWHEAD BELOW;Mn;220;NSM;;;;;N;;;;;
+08FB;ARABIC DOUBLE RIGHT ARROWHEAD ABOVE;Mn;230;NSM;;;;;N;;;;;
+08FC;ARABIC DOUBLE RIGHT ARROWHEAD ABOVE WITH DOT;Mn;230;NSM;;;;;N;;;;;
+08FD;ARABIC RIGHT ARROWHEAD ABOVE WITH DOT;Mn;230;NSM;;;;;N;;;;;
+08FE;ARABIC DAMMA WITH DOT;Mn;230;NSM;;;;;N;;;;;
+08FF;ARABIC MARK SIDEWAYS NOON GHUNNA;Mn;230;NSM;;;;;N;;;;;
+0900;DEVANAGARI SIGN INVERTED CANDRABINDU;Mn;0;NSM;;;;;N;;;;;
+0901;DEVANAGARI SIGN CANDRABINDU;Mn;0;NSM;;;;;N;;;;;
+0902;DEVANAGARI SIGN ANUSVARA;Mn;0;NSM;;;;;N;;;;;
+0903;DEVANAGARI SIGN VISARGA;Mc;0;L;;;;;N;;;;;
+0904;DEVANAGARI LETTER SHORT A;Lo;0;L;;;;;N;;;;;
+0905;DEVANAGARI LETTER A;Lo;0;L;;;;;N;;;;;
+0906;DEVANAGARI LETTER AA;Lo;0;L;;;;;N;;;;;
+0907;DEVANAGARI LETTER I;Lo;0;L;;;;;N;;;;;
+0908;DEVANAGARI LETTER II;Lo;0;L;;;;;N;;;;;
+0909;DEVANAGARI LETTER U;Lo;0;L;;;;;N;;;;;
+090A;DEVANAGARI LETTER UU;Lo;0;L;;;;;N;;;;;
+090B;DEVANAGARI LETTER VOCALIC R;Lo;0;L;;;;;N;;;;;
+090C;DEVANAGARI LETTER VOCALIC L;Lo;0;L;;;;;N;;;;;
+090D;DEVANAGARI LETTER CANDRA E;Lo;0;L;;;;;N;;;;;
+090E;DEVANAGARI LETTER SHORT E;Lo;0;L;;;;;N;;;;;
+090F;DEVANAGARI LETTER E;Lo;0;L;;;;;N;;;;;
+0910;DEVANAGARI LETTER AI;Lo;0;L;;;;;N;;;;;
+0911;DEVANAGARI LETTER CANDRA O;Lo;0;L;;;;;N;;;;;
+0912;DEVANAGARI LETTER SHORT O;Lo;0;L;;;;;N;;;;;
+0913;DEVANAGARI LETTER O;Lo;0;L;;;;;N;;;;;
+0914;DEVANAGARI LETTER AU;Lo;0;L;;;;;N;;;;;
+0915;DEVANAGARI LETTER KA;Lo;0;L;;;;;N;;;;;
+0916;DEVANAGARI LETTER KHA;Lo;0;L;;;;;N;;;;;
+0917;DEVANAGARI LETTER GA;Lo;0;L;;;;;N;;;;;
+0918;DEVANAGARI LETTER GHA;Lo;0;L;;;;;N;;;;;
+0919;DEVANAGARI LETTER NGA;Lo;0;L;;;;;N;;;;;
+091A;DEVANAGARI LETTER CA;Lo;0;L;;;;;N;;;;;
+091B;DEVANAGARI LETTER CHA;Lo;0;L;;;;;N;;;;;
+091C;DEVANAGARI LETTER JA;Lo;0;L;;;;;N;;;;;
+091D;DEVANAGARI LETTER JHA;Lo;0;L;;;;;N;;;;;
+091E;DEVANAGARI LETTER NYA;Lo;0;L;;;;;N;;;;;
+091F;DEVANAGARI LETTER TTA;Lo;0;L;;;;;N;;;;;
+0920;DEVANAGARI LETTER TTHA;Lo;0;L;;;;;N;;;;;
+0921;DEVANAGARI LETTER DDA;Lo;0;L;;;;;N;;;;;
+0922;DEVANAGARI LETTER DDHA;Lo;0;L;;;;;N;;;;;
+0923;DEVANAGARI LETTER NNA;Lo;0;L;;;;;N;;;;;
+0924;DEVANAGARI LETTER TA;Lo;0;L;;;;;N;;;;;
+0925;DEVANAGARI LETTER THA;Lo;0;L;;;;;N;;;;;
+0926;DEVANAGARI LETTER DA;Lo;0;L;;;;;N;;;;;
+0927;DEVANAGARI LETTER DHA;Lo;0;L;;;;;N;;;;;
+0928;DEVANAGARI LETTER NA;Lo;0;L;;;;;N;;;;;
+0929;DEVANAGARI LETTER NNNA;Lo;0;L;0928 093C;;;;N;;;;;
+092A;DEVANAGARI LETTER PA;Lo;0;L;;;;;N;;;;;
+092B;DEVANAGARI LETTER PHA;Lo;0;L;;;;;N;;;;;
+092C;DEVANAGARI LETTER BA;Lo;0;L;;;;;N;;;;;
+092D;DEVANAGARI LETTER BHA;Lo;0;L;;;;;N;;;;;
+092E;DEVANAGARI LETTER MA;Lo;0;L;;;;;N;;;;;
+092F;DEVANAGARI LETTER YA;Lo;0;L;;;;;N;;;;;
+0930;DEVANAGARI LETTER RA;Lo;0;L;;;;;N;;;;;
+0931;DEVANAGARI LETTER RRA;Lo;0;L;0930 093C;;;;N;;;;;
+0932;DEVANAGARI LETTER LA;Lo;0;L;;;;;N;;;;;
+0933;DEVANAGARI LETTER LLA;Lo;0;L;;;;;N;;;;;
+0934;DEVANAGARI LETTER LLLA;Lo;0;L;0933 093C;;;;N;;;;;
+0935;DEVANAGARI LETTER VA;Lo;0;L;;;;;N;;;;;
+0936;DEVANAGARI LETTER SHA;Lo;0;L;;;;;N;;;;;
+0937;DEVANAGARI LETTER SSA;Lo;0;L;;;;;N;;;;;
+0938;DEVANAGARI LETTER SA;Lo;0;L;;;;;N;;;;;
+0939;DEVANAGARI LETTER HA;Lo;0;L;;;;;N;;;;;
+093A;DEVANAGARI VOWEL SIGN OE;Mn;0;NSM;;;;;N;;;;;
+093B;DEVANAGARI VOWEL SIGN OOE;Mc;0;L;;;;;N;;;;;
+093C;DEVANAGARI SIGN NUKTA;Mn;7;NSM;;;;;N;;;;;
+093D;DEVANAGARI SIGN AVAGRAHA;Lo;0;L;;;;;N;;;;;
+093E;DEVANAGARI VOWEL SIGN AA;Mc;0;L;;;;;N;;;;;
+093F;DEVANAGARI VOWEL SIGN I;Mc;0;L;;;;;N;;;;;
+0940;DEVANAGARI VOWEL SIGN II;Mc;0;L;;;;;N;;;;;
+0941;DEVANAGARI VOWEL SIGN U;Mn;0;NSM;;;;;N;;;;;
+0942;DEVANAGARI VOWEL SIGN UU;Mn;0;NSM;;;;;N;;;;;
+0943;DEVANAGARI VOWEL SIGN VOCALIC R;Mn;0;NSM;;;;;N;;;;;
+0944;DEVANAGARI VOWEL SIGN VOCALIC RR;Mn;0;NSM;;;;;N;;;;;
+0945;DEVANAGARI VOWEL SIGN CANDRA E;Mn;0;NSM;;;;;N;;;;;
+0946;DEVANAGARI VOWEL SIGN SHORT E;Mn;0;NSM;;;;;N;;;;;
+0947;DEVANAGARI VOWEL SIGN E;Mn;0;NSM;;;;;N;;;;;
+0948;DEVANAGARI VOWEL SIGN AI;Mn;0;NSM;;;;;N;;;;;
+0949;DEVANAGARI VOWEL SIGN CANDRA O;Mc;0;L;;;;;N;;;;;
+094A;DEVANAGARI VOWEL SIGN SHORT O;Mc;0;L;;;;;N;;;;;
+094B;DEVANAGARI VOWEL SIGN O;Mc;0;L;;;;;N;;;;;
+094C;DEVANAGARI VOWEL SIGN AU;Mc;0;L;;;;;N;;;;;
+094D;DEVANAGARI SIGN VIRAMA;Mn;9;NSM;;;;;N;;;;;
+094E;DEVANAGARI VOWEL SIGN PRISHTHAMATRA E;Mc;0;L;;;;;N;;;;;
+094F;DEVANAGARI VOWEL SIGN AW;Mc;0;L;;;;;N;;;;;
+0950;DEVANAGARI OM;Lo;0;L;;;;;N;;;;;
+0951;DEVANAGARI STRESS SIGN UDATTA;Mn;230;NSM;;;;;N;;;;;
+0952;DEVANAGARI STRESS SIGN ANUDATTA;Mn;220;NSM;;;;;N;;;;;
+0953;DEVANAGARI GRAVE ACCENT;Mn;230;NSM;;;;;N;;;;;
+0954;DEVANAGARI ACUTE ACCENT;Mn;230;NSM;;;;;N;;;;;
+0955;DEVANAGARI VOWEL SIGN CANDRA LONG E;Mn;0;NSM;;;;;N;;;;;
+0956;DEVANAGARI VOWEL SIGN UE;Mn;0;NSM;;;;;N;;;;;
+0957;DEVANAGARI VOWEL SIGN UUE;Mn;0;NSM;;;;;N;;;;;
+0958;DEVANAGARI LETTER QA;Lo;0;L;0915 093C;;;;N;;;;;
+0959;DEVANAGARI LETTER KHHA;Lo;0;L;0916 093C;;;;N;;;;;
+095A;DEVANAGARI LETTER GHHA;Lo;0;L;0917 093C;;;;N;;;;;
+095B;DEVANAGARI LETTER ZA;Lo;0;L;091C 093C;;;;N;;;;;
+095C;DEVANAGARI LETTER DDDHA;Lo;0;L;0921 093C;;;;N;;;;;
+095D;DEVANAGARI LETTER RHA;Lo;0;L;0922 093C;;;;N;;;;;
+095E;DEVANAGARI LETTER FA;Lo;0;L;092B 093C;;;;N;;;;;
+095F;DEVANAGARI LETTER YYA;Lo;0;L;092F 093C;;;;N;;;;;
+0960;DEVANAGARI LETTER VOCALIC RR;Lo;0;L;;;;;N;;;;;
+0961;DEVANAGARI LETTER VOCALIC LL;Lo;0;L;;;;;N;;;;;
+0962;DEVANAGARI VOWEL SIGN VOCALIC L;Mn;0;NSM;;;;;N;;;;;
+0963;DEVANAGARI VOWEL SIGN VOCALIC LL;Mn;0;NSM;;;;;N;;;;;
+0964;DEVANAGARI DANDA;Po;0;L;;;;;N;;;;;
+0965;DEVANAGARI DOUBLE DANDA;Po;0;L;;;;;N;;;;;
+0966;DEVANAGARI DIGIT ZERO;Nd;0;L;;0;0;0;N;;;;;
+0967;DEVANAGARI DIGIT ONE;Nd;0;L;;1;1;1;N;;;;;
+0968;DEVANAGARI DIGIT TWO;Nd;0;L;;2;2;2;N;;;;;
+0969;DEVANAGARI DIGIT THREE;Nd;0;L;;3;3;3;N;;;;;
+096A;DEVANAGARI DIGIT FOUR;Nd;0;L;;4;4;4;N;;;;;
+096B;DEVANAGARI DIGIT FIVE;Nd;0;L;;5;5;5;N;;;;;
+096C;DEVANAGARI DIGIT SIX;Nd;0;L;;6;6;6;N;;;;;
+096D;DEVANAGARI DIGIT SEVEN;Nd;0;L;;7;7;7;N;;;;;
+096E;DEVANAGARI DIGIT EIGHT;Nd;0;L;;8;8;8;N;;;;;
+096F;DEVANAGARI DIGIT NINE;Nd;0;L;;9;9;9;N;;;;;
+0970;DEVANAGARI ABBREVIATION SIGN;Po;0;L;;;;;N;;;;;
+0971;DEVANAGARI SIGN HIGH SPACING DOT;Lm;0;L;;;;;N;;;;;
+0972;DEVANAGARI LETTER CANDRA A;Lo;0;L;;;;;N;;;;;
+0973;DEVANAGARI LETTER OE;Lo;0;L;;;;;N;;;;;
+0974;DEVANAGARI LETTER OOE;Lo;0;L;;;;;N;;;;;
+0975;DEVANAGARI LETTER AW;Lo;0;L;;;;;N;;;;;
+0976;DEVANAGARI LETTER UE;Lo;0;L;;;;;N;;;;;
+0977;DEVANAGARI LETTER UUE;Lo;0;L;;;;;N;;;;;
+0978;DEVANAGARI LETTER MARWARI DDA;Lo;0;L;;;;;N;;;;;
+0979;DEVANAGARI LETTER ZHA;Lo;0;L;;;;;N;;;;;
+097A;DEVANAGARI LETTER HEAVY YA;Lo;0;L;;;;;N;;;;;
+097B;DEVANAGARI LETTER GGA;Lo;0;L;;;;;N;;;;;
+097C;DEVANAGARI LETTER JJA;Lo;0;L;;;;;N;;;;;
+097D;DEVANAGARI LETTER GLOTTAL STOP;Lo;0;L;;;;;N;;;;;
+097E;DEVANAGARI LETTER DDDA;Lo;0;L;;;;;N;;;;;
+097F;DEVANAGARI LETTER BBA;Lo;0;L;;;;;N;;;;;
+0980;BENGALI ANJI;Lo;0;L;;;;;N;;;;;
+0981;BENGALI SIGN CANDRABINDU;Mn;0;NSM;;;;;N;;;;;
+0982;BENGALI SIGN ANUSVARA;Mc;0;L;;;;;N;;;;;
+0983;BENGALI SIGN VISARGA;Mc;0;L;;;;;N;;;;;
+0985;BENGALI LETTER A;Lo;0;L;;;;;N;;;;;
+0986;BENGALI LETTER AA;Lo;0;L;;;;;N;;;;;
+0987;BENGALI LETTER I;Lo;0;L;;;;;N;;;;;
+0988;BENGALI LETTER II;Lo;0;L;;;;;N;;;;;
+0989;BENGALI LETTER U;Lo;0;L;;;;;N;;;;;
+098A;BENGALI LETTER UU;Lo;0;L;;;;;N;;;;;
+098B;BENGALI LETTER VOCALIC R;Lo;0;L;;;;;N;;;;;
+098C;BENGALI LETTER VOCALIC L;Lo;0;L;;;;;N;;;;;
+098F;BENGALI LETTER E;Lo;0;L;;;;;N;;;;;
+0990;BENGALI LETTER AI;Lo;0;L;;;;;N;;;;;
+0993;BENGALI LETTER O;Lo;0;L;;;;;N;;;;;
+0994;BENGALI LETTER AU;Lo;0;L;;;;;N;;;;;
+0995;BENGALI LETTER KA;Lo;0;L;;;;;N;;;;;
+0996;BENGALI LETTER KHA;Lo;0;L;;;;;N;;;;;
+0997;BENGALI LETTER GA;Lo;0;L;;;;;N;;;;;
+0998;BENGALI LETTER GHA;Lo;0;L;;;;;N;;;;;
+0999;BENGALI LETTER NGA;Lo;0;L;;;;;N;;;;;
+099A;BENGALI LETTER CA;Lo;0;L;;;;;N;;;;;
+099B;BENGALI LETTER CHA;Lo;0;L;;;;;N;;;;;
+099C;BENGALI LETTER JA;Lo;0;L;;;;;N;;;;;
+099D;BENGALI LETTER JHA;Lo;0;L;;;;;N;;;;;
+099E;BENGALI LETTER NYA;Lo;0;L;;;;;N;;;;;
+099F;BENGALI LETTER TTA;Lo;0;L;;;;;N;;;;;
+09A0;BENGALI LETTER TTHA;Lo;0;L;;;;;N;;;;;
+09A1;BENGALI LETTER DDA;Lo;0;L;;;;;N;;;;;
+09A2;BENGALI LETTER DDHA;Lo;0;L;;;;;N;;;;;
+09A3;BENGALI LETTER NNA;Lo;0;L;;;;;N;;;;;
+09A4;BENGALI LETTER TA;Lo;0;L;;;;;N;;;;;
+09A5;BENGALI LETTER THA;Lo;0;L;;;;;N;;;;;
+09A6;BENGALI LETTER DA;Lo;0;L;;;;;N;;;;;
+09A7;BENGALI LETTER DHA;Lo;0;L;;;;;N;;;;;
+09A8;BENGALI LETTER NA;Lo;0;L;;;;;N;;;;;
+09AA;BENGALI LETTER PA;Lo;0;L;;;;;N;;;;;
+09AB;BENGALI LETTER PHA;Lo;0;L;;;;;N;;;;;
+09AC;BENGALI LETTER BA;Lo;0;L;;;;;N;;;;;
+09AD;BENGALI LETTER BHA;Lo;0;L;;;;;N;;;;;
+09AE;BENGALI LETTER MA;Lo;0;L;;;;;N;;;;;
+09AF;BENGALI LETTER YA;Lo;0;L;;;;;N;;;;;
+09B0;BENGALI LETTER RA;Lo;0;L;;;;;N;;;;;
+09B2;BENGALI LETTER LA;Lo;0;L;;;;;N;;;;;
+09B6;BENGALI LETTER SHA;Lo;0;L;;;;;N;;;;;
+09B7;BENGALI LETTER SSA;Lo;0;L;;;;;N;;;;;
+09B8;BENGALI LETTER SA;Lo;0;L;;;;;N;;;;;
+09B9;BENGALI LETTER HA;Lo;0;L;;;;;N;;;;;
+09BC;BENGALI SIGN NUKTA;Mn;7;NSM;;;;;N;;;;;
+09BD;BENGALI SIGN AVAGRAHA;Lo;0;L;;;;;N;;;;;
+09BE;BENGALI VOWEL SIGN AA;Mc;0;L;;;;;N;;;;;
+09BF;BENGALI VOWEL SIGN I;Mc;0;L;;;;;N;;;;;
+09C0;BENGALI VOWEL SIGN II;Mc;0;L;;;;;N;;;;;
+09C1;BENGALI VOWEL SIGN U;Mn;0;NSM;;;;;N;;;;;
+09C2;BENGALI VOWEL SIGN UU;Mn;0;NSM;;;;;N;;;;;
+09C3;BENGALI VOWEL SIGN VOCALIC R;Mn;0;NSM;;;;;N;;;;;
+09C4;BENGALI VOWEL SIGN VOCALIC RR;Mn;0;NSM;;;;;N;;;;;
+09C7;BENGALI VOWEL SIGN E;Mc;0;L;;;;;N;;;;;
+09C8;BENGALI VOWEL SIGN AI;Mc;0;L;;;;;N;;;;;
+09CB;BENGALI VOWEL SIGN O;Mc;0;L;09C7 09BE;;;;N;;;;;
+09CC;BENGALI VOWEL SIGN AU;Mc;0;L;09C7 09D7;;;;N;;;;;
+09CD;BENGALI SIGN VIRAMA;Mn;9;NSM;;;;;N;;;;;
+09CE;BENGALI LETTER KHANDA TA;Lo;0;L;;;;;N;;;;;
+09D7;BENGALI AU LENGTH MARK;Mc;0;L;;;;;N;;;;;
+09DC;BENGALI LETTER RRA;Lo;0;L;09A1 09BC;;;;N;;;;;
+09DD;BENGALI LETTER RHA;Lo;0;L;09A2 09BC;;;;N;;;;;
+09DF;BENGALI LETTER YYA;Lo;0;L;09AF 09BC;;;;N;;;;;
+09E0;BENGALI LETTER VOCALIC RR;Lo;0;L;;;;;N;;;;;
+09E1;BENGALI LETTER VOCALIC LL;Lo;0;L;;;;;N;;;;;
+09E2;BENGALI VOWEL SIGN VOCALIC L;Mn;0;NSM;;;;;N;;;;;
+09E3;BENGALI VOWEL SIGN VOCALIC LL;Mn;0;NSM;;;;;N;;;;;
+09E6;BENGALI DIGIT ZERO;Nd;0;L;;0;0;0;N;;;;;
+09E7;BENGALI DIGIT ONE;Nd;0;L;;1;1;1;N;;;;;
+09E8;BENGALI DIGIT TWO;Nd;0;L;;2;2;2;N;;;;;
+09E9;BENGALI DIGIT THREE;Nd;0;L;;3;3;3;N;;;;;
+09EA;BENGALI DIGIT FOUR;Nd;0;L;;4;4;4;N;;;;;
+09EB;BENGALI DIGIT FIVE;Nd;0;L;;5;5;5;N;;;;;
+09EC;BENGALI DIGIT SIX;Nd;0;L;;6;6;6;N;;;;;
+09ED;BENGALI DIGIT SEVEN;Nd;0;L;;7;7;7;N;;;;;
+09EE;BENGALI DIGIT EIGHT;Nd;0;L;;8;8;8;N;;;;;
+09EF;BENGALI DIGIT NINE;Nd;0;L;;9;9;9;N;;;;;
+09F0;BENGALI LETTER RA WITH MIDDLE DIAGONAL;Lo;0;L;;;;;N;;;;;
+09F1;BENGALI LETTER RA WITH LOWER DIAGONAL;Lo;0;L;;;;;N;BENGALI LETTER VA WITH LOWER DIAGONAL;;;;
+09F2;BENGALI RUPEE MARK;Sc;0;ET;;;;;N;;;;;
+09F3;BENGALI RUPEE SIGN;Sc;0;ET;;;;;N;;;;;
+09F4;BENGALI CURRENCY NUMERATOR ONE;No;0;L;;;;1/16;N;;;;;
+09F5;BENGALI CURRENCY NUMERATOR TWO;No;0;L;;;;1/8;N;;;;;
+09F6;BENGALI CURRENCY NUMERATOR THREE;No;0;L;;;;3/16;N;;;;;
+09F7;BENGALI CURRENCY NUMERATOR FOUR;No;0;L;;;;1/4;N;;;;;
+09F8;BENGALI CURRENCY NUMERATOR ONE LESS THAN THE DENOMINATOR;No;0;L;;;;3/4;N;;;;;
+09F9;BENGALI CURRENCY DENOMINATOR SIXTEEN;No;0;L;;;;16;N;;;;;
+09FA;BENGALI ISSHAR;So;0;L;;;;;N;;;;;
+09FB;BENGALI GANDA MARK;Sc;0;ET;;;;;N;;;;;
+0A01;GURMUKHI SIGN ADAK BINDI;Mn;0;NSM;;;;;N;;;;;
+0A02;GURMUKHI SIGN BINDI;Mn;0;NSM;;;;;N;;;;;
+0A03;GURMUKHI SIGN VISARGA;Mc;0;L;;;;;N;;;;;
+0A05;GURMUKHI LETTER A;Lo;0;L;;;;;N;;;;;
+0A06;GURMUKHI LETTER AA;Lo;0;L;;;;;N;;;;;
+0A07;GURMUKHI LETTER I;Lo;0;L;;;;;N;;;;;
+0A08;GURMUKHI LETTER II;Lo;0;L;;;;;N;;;;;
+0A09;GURMUKHI LETTER U;Lo;0;L;;;;;N;;;;;
+0A0A;GURMUKHI LETTER UU;Lo;0;L;;;;;N;;;;;
+0A0F;GURMUKHI LETTER EE;Lo;0;L;;;;;N;;;;;
+0A10;GURMUKHI LETTER AI;Lo;0;L;;;;;N;;;;;
+0A13;GURMUKHI LETTER OO;Lo;0;L;;;;;N;;;;;
+0A14;GURMUKHI LETTER AU;Lo;0;L;;;;;N;;;;;
+0A15;GURMUKHI LETTER KA;Lo;0;L;;;;;N;;;;;
+0A16;GURMUKHI LETTER KHA;Lo;0;L;;;;;N;;;;;
+0A17;GURMUKHI LETTER GA;Lo;0;L;;;;;N;;;;;
+0A18;GURMUKHI LETTER GHA;Lo;0;L;;;;;N;;;;;
+0A19;GURMUKHI LETTER NGA;Lo;0;L;;;;;N;;;;;
+0A1A;GURMUKHI LETTER CA;Lo;0;L;;;;;N;;;;;
+0A1B;GURMUKHI LETTER CHA;Lo;0;L;;;;;N;;;;;
+0A1C;GURMUKHI LETTER JA;Lo;0;L;;;;;N;;;;;
+0A1D;GURMUKHI LETTER JHA;Lo;0;L;;;;;N;;;;;
+0A1E;GURMUKHI LETTER NYA;Lo;0;L;;;;;N;;;;;
+0A1F;GURMUKHI LETTER TTA;Lo;0;L;;;;;N;;;;;
+0A20;GURMUKHI LETTER TTHA;Lo;0;L;;;;;N;;;;;
+0A21;GURMUKHI LETTER DDA;Lo;0;L;;;;;N;;;;;
+0A22;GURMUKHI LETTER DDHA;Lo;0;L;;;;;N;;;;;
+0A23;GURMUKHI LETTER NNA;Lo;0;L;;;;;N;;;;;
+0A24;GURMUKHI LETTER TA;Lo;0;L;;;;;N;;;;;
+0A25;GURMUKHI LETTER THA;Lo;0;L;;;;;N;;;;;
+0A26;GURMUKHI LETTER DA;Lo;0;L;;;;;N;;;;;
+0A27;GURMUKHI LETTER DHA;Lo;0;L;;;;;N;;;;;
+0A28;GURMUKHI LETTER NA;Lo;0;L;;;;;N;;;;;
+0A2A;GURMUKHI LETTER PA;Lo;0;L;;;;;N;;;;;
+0A2B;GURMUKHI LETTER PHA;Lo;0;L;;;;;N;;;;;
+0A2C;GURMUKHI LETTER BA;Lo;0;L;;;;;N;;;;;
+0A2D;GURMUKHI LETTER BHA;Lo;0;L;;;;;N;;;;;
+0A2E;GURMUKHI LETTER MA;Lo;0;L;;;;;N;;;;;
+0A2F;GURMUKHI LETTER YA;Lo;0;L;;;;;N;;;;;
+0A30;GURMUKHI LETTER RA;Lo;0;L;;;;;N;;;;;
+0A32;GURMUKHI LETTER LA;Lo;0;L;;;;;N;;;;;
+0A33;GURMUKHI LETTER LLA;Lo;0;L;0A32 0A3C;;;;N;;;;;
+0A35;GURMUKHI LETTER VA;Lo;0;L;;;;;N;;;;;
+0A36;GURMUKHI LETTER SHA;Lo;0;L;0A38 0A3C;;;;N;;;;;
+0A38;GURMUKHI LETTER SA;Lo;0;L;;;;;N;;;;;
+0A39;GURMUKHI LETTER HA;Lo;0;L;;;;;N;;;;;
+0A3C;GURMUKHI SIGN NUKTA;Mn;7;NSM;;;;;N;;;;;
+0A3E;GURMUKHI VOWEL SIGN AA;Mc;0;L;;;;;N;;;;;
+0A3F;GURMUKHI VOWEL SIGN I;Mc;0;L;;;;;N;;;;;
+0A40;GURMUKHI VOWEL SIGN II;Mc;0;L;;;;;N;;;;;
+0A41;GURMUKHI VOWEL SIGN U;Mn;0;NSM;;;;;N;;;;;
+0A42;GURMUKHI VOWEL SIGN UU;Mn;0;NSM;;;;;N;;;;;
+0A47;GURMUKHI VOWEL SIGN EE;Mn;0;NSM;;;;;N;;;;;
+0A48;GURMUKHI VOWEL SIGN AI;Mn;0;NSM;;;;;N;;;;;
+0A4B;GURMUKHI VOWEL SIGN OO;Mn;0;NSM;;;;;N;;;;;
+0A4C;GURMUKHI VOWEL SIGN AU;Mn;0;NSM;;;;;N;;;;;
+0A4D;GURMUKHI SIGN VIRAMA;Mn;9;NSM;;;;;N;;;;;
+0A51;GURMUKHI SIGN UDAAT;Mn;0;NSM;;;;;N;;;;;
+0A59;GURMUKHI LETTER KHHA;Lo;0;L;0A16 0A3C;;;;N;;;;;
+0A5A;GURMUKHI LETTER GHHA;Lo;0;L;0A17 0A3C;;;;N;;;;;
+0A5B;GURMUKHI LETTER ZA;Lo;0;L;0A1C 0A3C;;;;N;;;;;
+0A5C;GURMUKHI LETTER RRA;Lo;0;L;;;;;N;;;;;
+0A5E;GURMUKHI LETTER FA;Lo;0;L;0A2B 0A3C;;;;N;;;;;
+0A66;GURMUKHI DIGIT ZERO;Nd;0;L;;0;0;0;N;;;;;
+0A67;GURMUKHI DIGIT ONE;Nd;0;L;;1;1;1;N;;;;;
+0A68;GURMUKHI DIGIT TWO;Nd;0;L;;2;2;2;N;;;;;
+0A69;GURMUKHI DIGIT THREE;Nd;0;L;;3;3;3;N;;;;;
+0A6A;GURMUKHI DIGIT FOUR;Nd;0;L;;4;4;4;N;;;;;
+0A6B;GURMUKHI DIGIT FIVE;Nd;0;L;;5;5;5;N;;;;;
+0A6C;GURMUKHI DIGIT SIX;Nd;0;L;;6;6;6;N;;;;;
+0A6D;GURMUKHI DIGIT SEVEN;Nd;0;L;;7;7;7;N;;;;;
+0A6E;GURMUKHI DIGIT EIGHT;Nd;0;L;;8;8;8;N;;;;;
+0A6F;GURMUKHI DIGIT NINE;Nd;0;L;;9;9;9;N;;;;;
+0A70;GURMUKHI TIPPI;Mn;0;NSM;;;;;N;;;;;
+0A71;GURMUKHI ADDAK;Mn;0;NSM;;;;;N;;;;;
+0A72;GURMUKHI IRI;Lo;0;L;;;;;N;;;;;
+0A73;GURMUKHI URA;Lo;0;L;;;;;N;;;;;
+0A74;GURMUKHI EK ONKAR;Lo;0;L;;;;;N;;;;;
+0A75;GURMUKHI SIGN YAKASH;Mn;0;NSM;;;;;N;;;;;
+0A81;GUJARATI SIGN CANDRABINDU;Mn;0;NSM;;;;;N;;;;;
+0A82;GUJARATI SIGN ANUSVARA;Mn;0;NSM;;;;;N;;;;;
+0A83;GUJARATI SIGN VISARGA;Mc;0;L;;;;;N;;;;;
+0A85;GUJARATI LETTER A;Lo;0;L;;;;;N;;;;;
+0A86;GUJARATI LETTER AA;Lo;0;L;;;;;N;;;;;
+0A87;GUJARATI LETTER I;Lo;0;L;;;;;N;;;;;
+0A88;GUJARATI LETTER II;Lo;0;L;;;;;N;;;;;
+0A89;GUJARATI LETTER U;Lo;0;L;;;;;N;;;;;
+0A8A;GUJARATI LETTER UU;Lo;0;L;;;;;N;;;;;
+0A8B;GUJARATI LETTER VOCALIC R;Lo;0;L;;;;;N;;;;;
+0A8C;GUJARATI LETTER VOCALIC L;Lo;0;L;;;;;N;;;;;
+0A8D;GUJARATI VOWEL CANDRA E;Lo;0;L;;;;;N;;;;;
+0A8F;GUJARATI LETTER E;Lo;0;L;;;;;N;;;;;
+0A90;GUJARATI LETTER AI;Lo;0;L;;;;;N;;;;;
+0A91;GUJARATI VOWEL CANDRA O;Lo;0;L;;;;;N;;;;;
+0A93;GUJARATI LETTER O;Lo;0;L;;;;;N;;;;;
+0A94;GUJARATI LETTER AU;Lo;0;L;;;;;N;;;;;
+0A95;GUJARATI LETTER KA;Lo;0;L;;;;;N;;;;;
+0A96;GUJARATI LETTER KHA;Lo;0;L;;;;;N;;;;;
+0A97;GUJARATI LETTER GA;Lo;0;L;;;;;N;;;;;
+0A98;GUJARATI LETTER GHA;Lo;0;L;;;;;N;;;;;
+0A99;GUJARATI LETTER NGA;Lo;0;L;;;;;N;;;;;
+0A9A;GUJARATI LETTER CA;Lo;0;L;;;;;N;;;;;
+0A9B;GUJARATI LETTER CHA;Lo;0;L;;;;;N;;;;;
+0A9C;GUJARATI LETTER JA;Lo;0;L;;;;;N;;;;;
+0A9D;GUJARATI LETTER JHA;Lo;0;L;;;;;N;;;;;
+0A9E;GUJARATI LETTER NYA;Lo;0;L;;;;;N;;;;;
+0A9F;GUJARATI LETTER TTA;Lo;0;L;;;;;N;;;;;
+0AA0;GUJARATI LETTER TTHA;Lo;0;L;;;;;N;;;;;
+0AA1;GUJARATI LETTER DDA;Lo;0;L;;;;;N;;;;;
+0AA2;GUJARATI LETTER DDHA;Lo;0;L;;;;;N;;;;;
+0AA3;GUJARATI LETTER NNA;Lo;0;L;;;;;N;;;;;
+0AA4;GUJARATI LETTER TA;Lo;0;L;;;;;N;;;;;
+0AA5;GUJARATI LETTER THA;Lo;0;L;;;;;N;;;;;
+0AA6;GUJARATI LETTER DA;Lo;0;L;;;;;N;;;;;
+0AA7;GUJARATI LETTER DHA;Lo;0;L;;;;;N;;;;;
+0AA8;GUJARATI LETTER NA;Lo;0;L;;;;;N;;;;;
+0AAA;GUJARATI LETTER PA;Lo;0;L;;;;;N;;;;;
+0AAB;GUJARATI LETTER PHA;Lo;0;L;;;;;N;;;;;
+0AAC;GUJARATI LETTER BA;Lo;0;L;;;;;N;;;;;
+0AAD;GUJARATI LETTER BHA;Lo;0;L;;;;;N;;;;;
+0AAE;GUJARATI LETTER MA;Lo;0;L;;;;;N;;;;;
+0AAF;GUJARATI LETTER YA;Lo;0;L;;;;;N;;;;;
+0AB0;GUJARATI LETTER RA;Lo;0;L;;;;;N;;;;;
+0AB2;GUJARATI LETTER LA;Lo;0;L;;;;;N;;;;;
+0AB3;GUJARATI LETTER LLA;Lo;0;L;;;;;N;;;;;
+0AB5;GUJARATI LETTER VA;Lo;0;L;;;;;N;;;;;
+0AB6;GUJARATI LETTER SHA;Lo;0;L;;;;;N;;;;;
+0AB7;GUJARATI LETTER SSA;Lo;0;L;;;;;N;;;;;
+0AB8;GUJARATI LETTER SA;Lo;0;L;;;;;N;;;;;
+0AB9;GUJARATI LETTER HA;Lo;0;L;;;;;N;;;;;
+0ABC;GUJARATI SIGN NUKTA;Mn;7;NSM;;;;;N;;;;;
+0ABD;GUJARATI SIGN AVAGRAHA;Lo;0;L;;;;;N;;;;;
+0ABE;GUJARATI VOWEL SIGN AA;Mc;0;L;;;;;N;;;;;
+0ABF;GUJARATI VOWEL SIGN I;Mc;0;L;;;;;N;;;;;
+0AC0;GUJARATI VOWEL SIGN II;Mc;0;L;;;;;N;;;;;
+0AC1;GUJARATI VOWEL SIGN U;Mn;0;NSM;;;;;N;;;;;
+0AC2;GUJARATI VOWEL SIGN UU;Mn;0;NSM;;;;;N;;;;;
+0AC3;GUJARATI VOWEL SIGN VOCALIC R;Mn;0;NSM;;;;;N;;;;;
+0AC4;GUJARATI VOWEL SIGN VOCALIC RR;Mn;0;NSM;;;;;N;;;;;
+0AC5;GUJARATI VOWEL SIGN CANDRA E;Mn;0;NSM;;;;;N;;;;;
+0AC7;GUJARATI VOWEL SIGN E;Mn;0;NSM;;;;;N;;;;;
+0AC8;GUJARATI VOWEL SIGN AI;Mn;0;NSM;;;;;N;;;;;
+0AC9;GUJARATI VOWEL SIGN CANDRA O;Mc;0;L;;;;;N;;;;;
+0ACB;GUJARATI VOWEL SIGN O;Mc;0;L;;;;;N;;;;;
+0ACC;GUJARATI VOWEL SIGN AU;Mc;0;L;;;;;N;;;;;
+0ACD;GUJARATI SIGN VIRAMA;Mn;9;NSM;;;;;N;;;;;
+0AD0;GUJARATI OM;Lo;0;L;;;;;N;;;;;
+0AE0;GUJARATI LETTER VOCALIC RR;Lo;0;L;;;;;N;;;;;
+0AE1;GUJARATI LETTER VOCALIC LL;Lo;0;L;;;;;N;;;;;
+0AE2;GUJARATI VOWEL SIGN VOCALIC L;Mn;0;NSM;;;;;N;;;;;
+0AE3;GUJARATI VOWEL SIGN VOCALIC LL;Mn;0;NSM;;;;;N;;;;;
+0AE6;GUJARATI DIGIT ZERO;Nd;0;L;;0;0;0;N;;;;;
+0AE7;GUJARATI DIGIT ONE;Nd;0;L;;1;1;1;N;;;;;
+0AE8;GUJARATI DIGIT TWO;Nd;0;L;;2;2;2;N;;;;;
+0AE9;GUJARATI DIGIT THREE;Nd;0;L;;3;3;3;N;;;;;
+0AEA;GUJARATI DIGIT FOUR;Nd;0;L;;4;4;4;N;;;;;
+0AEB;GUJARATI DIGIT FIVE;Nd;0;L;;5;5;5;N;;;;;
+0AEC;GUJARATI DIGIT SIX;Nd;0;L;;6;6;6;N;;;;;
+0AED;GUJARATI DIGIT SEVEN;Nd;0;L;;7;7;7;N;;;;;
+0AEE;GUJARATI DIGIT EIGHT;Nd;0;L;;8;8;8;N;;;;;
+0AEF;GUJARATI DIGIT NINE;Nd;0;L;;9;9;9;N;;;;;
+0AF0;GUJARATI ABBREVIATION SIGN;Po;0;L;;;;;N;;;;;
+0AF1;GUJARATI RUPEE SIGN;Sc;0;ET;;;;;N;;;;;
+0AF9;GUJARATI LETTER ZHA;Lo;0;L;;;;;N;;;;;
+0B01;ORIYA SIGN CANDRABINDU;Mn;0;NSM;;;;;N;;;;;
+0B02;ORIYA SIGN ANUSVARA;Mc;0;L;;;;;N;;;;;
+0B03;ORIYA SIGN VISARGA;Mc;0;L;;;;;N;;;;;
+0B05;ORIYA LETTER A;Lo;0;L;;;;;N;;;;;
+0B06;ORIYA LETTER AA;Lo;0;L;;;;;N;;;;;
+0B07;ORIYA LETTER I;Lo;0;L;;;;;N;;;;;
+0B08;ORIYA LETTER II;Lo;0;L;;;;;N;;;;;
+0B09;ORIYA LETTER U;Lo;0;L;;;;;N;;;;;
+0B0A;ORIYA LETTER UU;Lo;0;L;;;;;N;;;;;
+0B0B;ORIYA LETTER VOCALIC R;Lo;0;L;;;;;N;;;;;
+0B0C;ORIYA LETTER VOCALIC L;Lo;0;L;;;;;N;;;;;
+0B0F;ORIYA LETTER E;Lo;0;L;;;;;N;;;;;
+0B10;ORIYA LETTER AI;Lo;0;L;;;;;N;;;;;
+0B13;ORIYA LETTER O;Lo;0;L;;;;;N;;;;;
+0B14;ORIYA LETTER AU;Lo;0;L;;;;;N;;;;;
+0B15;ORIYA LETTER KA;Lo;0;L;;;;;N;;;;;
+0B16;ORIYA LETTER KHA;Lo;0;L;;;;;N;;;;;
+0B17;ORIYA LETTER GA;Lo;0;L;;;;;N;;;;;
+0B18;ORIYA LETTER GHA;Lo;0;L;;;;;N;;;;;
+0B19;ORIYA LETTER NGA;Lo;0;L;;;;;N;;;;;
+0B1A;ORIYA LETTER CA;Lo;0;L;;;;;N;;;;;
+0B1B;ORIYA LETTER CHA;Lo;0;L;;;;;N;;;;;
+0B1C;ORIYA LETTER JA;Lo;0;L;;;;;N;;;;;
+0B1D;ORIYA LETTER JHA;Lo;0;L;;;;;N;;;;;
+0B1E;ORIYA LETTER NYA;Lo;0;L;;;;;N;;;;;
+0B1F;ORIYA LETTER TTA;Lo;0;L;;;;;N;;;;;
+0B20;ORIYA LETTER TTHA;Lo;0;L;;;;;N;;;;;
+0B21;ORIYA LETTER DDA;Lo;0;L;;;;;N;;;;;
+0B22;ORIYA LETTER DDHA;Lo;0;L;;;;;N;;;;;
+0B23;ORIYA LETTER NNA;Lo;0;L;;;;;N;;;;;
+0B24;ORIYA LETTER TA;Lo;0;L;;;;;N;;;;;
+0B25;ORIYA LETTER THA;Lo;0;L;;;;;N;;;;;
+0B26;ORIYA LETTER DA;Lo;0;L;;;;;N;;;;;
+0B27;ORIYA LETTER DHA;Lo;0;L;;;;;N;;;;;
+0B28;ORIYA LETTER NA;Lo;0;L;;;;;N;;;;;
+0B2A;ORIYA LETTER PA;Lo;0;L;;;;;N;;;;;
+0B2B;ORIYA LETTER PHA;Lo;0;L;;;;;N;;;;;
+0B2C;ORIYA LETTER BA;Lo;0;L;;;;;N;;;;;
+0B2D;ORIYA LETTER BHA;Lo;0;L;;;;;N;;;;;
+0B2E;ORIYA LETTER MA;Lo;0;L;;;;;N;;;;;
+0B2F;ORIYA LETTER YA;Lo;0;L;;;;;N;;;;;
+0B30;ORIYA LETTER RA;Lo;0;L;;;;;N;;;;;
+0B32;ORIYA LETTER LA;Lo;0;L;;;;;N;;;;;
+0B33;ORIYA LETTER LLA;Lo;0;L;;;;;N;;;;;
+0B35;ORIYA LETTER VA;Lo;0;L;;;;;N;;;;;
+0B36;ORIYA LETTER SHA;Lo;0;L;;;;;N;;;;;
+0B37;ORIYA LETTER SSA;Lo;0;L;;;;;N;;;;;
+0B38;ORIYA LETTER SA;Lo;0;L;;;;;N;;;;;
+0B39;ORIYA LETTER HA;Lo;0;L;;;;;N;;;;;
+0B3C;ORIYA SIGN NUKTA;Mn;7;NSM;;;;;N;;;;;
+0B3D;ORIYA SIGN AVAGRAHA;Lo;0;L;;;;;N;;;;;
+0B3E;ORIYA VOWEL SIGN AA;Mc;0;L;;;;;N;;;;;
+0B3F;ORIYA VOWEL SIGN I;Mn;0;NSM;;;;;N;;;;;
+0B40;ORIYA VOWEL SIGN II;Mc;0;L;;;;;N;;;;;
+0B41;ORIYA VOWEL SIGN U;Mn;0;NSM;;;;;N;;;;;
+0B42;ORIYA VOWEL SIGN UU;Mn;0;NSM;;;;;N;;;;;
+0B43;ORIYA VOWEL SIGN VOCALIC R;Mn;0;NSM;;;;;N;;;;;
+0B44;ORIYA VOWEL SIGN VOCALIC RR;Mn;0;NSM;;;;;N;;;;;
+0B47;ORIYA VOWEL SIGN E;Mc;0;L;;;;;N;;;;;
+0B48;ORIYA VOWEL SIGN AI;Mc;0;L;0B47 0B56;;;;N;;;;;
+0B4B;ORIYA VOWEL SIGN O;Mc;0;L;0B47 0B3E;;;;N;;;;;
+0B4C;ORIYA VOWEL SIGN AU;Mc;0;L;0B47 0B57;;;;N;;;;;
+0B4D;ORIYA SIGN VIRAMA;Mn;9;NSM;;;;;N;;;;;
+0B56;ORIYA AI LENGTH MARK;Mn;0;NSM;;;;;N;;;;;
+0B57;ORIYA AU LENGTH MARK;Mc;0;L;;;;;N;;;;;
+0B5C;ORIYA LETTER RRA;Lo;0;L;0B21 0B3C;;;;N;;;;;
+0B5D;ORIYA LETTER RHA;Lo;0;L;0B22 0B3C;;;;N;;;;;
+0B5F;ORIYA LETTER YYA;Lo;0;L;;;;;N;;;;;
+0B60;ORIYA LETTER VOCALIC RR;Lo;0;L;;;;;N;;;;;
+0B61;ORIYA LETTER VOCALIC LL;Lo;0;L;;;;;N;;;;;
+0B62;ORIYA VOWEL SIGN VOCALIC L;Mn;0;NSM;;;;;N;;;;;
+0B63;ORIYA VOWEL SIGN VOCALIC LL;Mn;0;NSM;;;;;N;;;;;
+0B66;ORIYA DIGIT ZERO;Nd;0;L;;0;0;0;N;;;;;
+0B67;ORIYA DIGIT ONE;Nd;0;L;;1;1;1;N;;;;;
+0B68;ORIYA DIGIT TWO;Nd;0;L;;2;2;2;N;;;;;
+0B69;ORIYA DIGIT THREE;Nd;0;L;;3;3;3;N;;;;;
+0B6A;ORIYA DIGIT FOUR;Nd;0;L;;4;4;4;N;;;;;
+0B6B;ORIYA DIGIT FIVE;Nd;0;L;;5;5;5;N;;;;;
+0B6C;ORIYA DIGIT SIX;Nd;0;L;;6;6;6;N;;;;;
+0B6D;ORIYA DIGIT SEVEN;Nd;0;L;;7;7;7;N;;;;;
+0B6E;ORIYA DIGIT EIGHT;Nd;0;L;;8;8;8;N;;;;;
+0B6F;ORIYA DIGIT NINE;Nd;0;L;;9;9;9;N;;;;;
+0B70;ORIYA ISSHAR;So;0;L;;;;;N;;;;;
+0B71;ORIYA LETTER WA;Lo;0;L;;;;;N;;;;;
+0B72;ORIYA FRACTION ONE QUARTER;No;0;L;;;;1/4;N;;;;;
+0B73;ORIYA FRACTION ONE HALF;No;0;L;;;;1/2;N;;;;;
+0B74;ORIYA FRACTION THREE QUARTERS;No;0;L;;;;3/4;N;;;;;
+0B75;ORIYA FRACTION ONE SIXTEENTH;No;0;L;;;;1/16;N;;;;;
+0B76;ORIYA FRACTION ONE EIGHTH;No;0;L;;;;1/8;N;;;;;
+0B77;ORIYA FRACTION THREE SIXTEENTHS;No;0;L;;;;3/16;N;;;;;
+0B82;TAMIL SIGN ANUSVARA;Mn;0;NSM;;;;;N;;;;;
+0B83;TAMIL SIGN VISARGA;Lo;0;L;;;;;N;;;;;
+0B85;TAMIL LETTER A;Lo;0;L;;;;;N;;;;;
+0B86;TAMIL LETTER AA;Lo;0;L;;;;;N;;;;;
+0B87;TAMIL LETTER I;Lo;0;L;;;;;N;;;;;
+0B88;TAMIL LETTER II;Lo;0;L;;;;;N;;;;;
+0B89;TAMIL LETTER U;Lo;0;L;;;;;N;;;;;
+0B8A;TAMIL LETTER UU;Lo;0;L;;;;;N;;;;;
+0B8E;TAMIL LETTER E;Lo;0;L;;;;;N;;;;;
+0B8F;TAMIL LETTER EE;Lo;0;L;;;;;N;;;;;
+0B90;TAMIL LETTER AI;Lo;0;L;;;;;N;;;;;
+0B92;TAMIL LETTER O;Lo;0;L;;;;;N;;;;;
+0B93;TAMIL LETTER OO;Lo;0;L;;;;;N;;;;;
+0B94;TAMIL LETTER AU;Lo;0;L;0B92 0BD7;;;;N;;;;;
+0B95;TAMIL LETTER KA;Lo;0;L;;;;;N;;;;;
+0B99;TAMIL LETTER NGA;Lo;0;L;;;;;N;;;;;
+0B9A;TAMIL LETTER CA;Lo;0;L;;;;;N;;;;;
+0B9C;TAMIL LETTER JA;Lo;0;L;;;;;N;;;;;
+0B9E;TAMIL LETTER NYA;Lo;0;L;;;;;N;;;;;
+0B9F;TAMIL LETTER TTA;Lo;0;L;;;;;N;;;;;
+0BA3;TAMIL LETTER NNA;Lo;0;L;;;;;N;;;;;
+0BA4;TAMIL LETTER TA;Lo;0;L;;;;;N;;;;;
+0BA8;TAMIL LETTER NA;Lo;0;L;;;;;N;;;;;
+0BA9;TAMIL LETTER NNNA;Lo;0;L;;;;;N;;;;;
+0BAA;TAMIL LETTER PA;Lo;0;L;;;;;N;;;;;
+0BAE;TAMIL LETTER MA;Lo;0;L;;;;;N;;;;;
+0BAF;TAMIL LETTER YA;Lo;0;L;;;;;N;;;;;
+0BB0;TAMIL LETTER RA;Lo;0;L;;;;;N;;;;;
+0BB1;TAMIL LETTER RRA;Lo;0;L;;;;;N;;;;;
+0BB2;TAMIL LETTER LA;Lo;0;L;;;;;N;;;;;
+0BB3;TAMIL LETTER LLA;Lo;0;L;;;;;N;;;;;
+0BB4;TAMIL LETTER LLLA;Lo;0;L;;;;;N;;;;;
+0BB5;TAMIL LETTER VA;Lo;0;L;;;;;N;;;;;
+0BB6;TAMIL LETTER SHA;Lo;0;L;;;;;N;;;;;
+0BB7;TAMIL LETTER SSA;Lo;0;L;;;;;N;;;;;
+0BB8;TAMIL LETTER SA;Lo;0;L;;;;;N;;;;;
+0BB9;TAMIL LETTER HA;Lo;0;L;;;;;N;;;;;
+0BBE;TAMIL VOWEL SIGN AA;Mc;0;L;;;;;N;;;;;
+0BBF;TAMIL VOWEL SIGN I;Mc;0;L;;;;;N;;;;;
+0BC0;TAMIL VOWEL SIGN II;Mn;0;NSM;;;;;N;;;;;
+0BC1;TAMIL VOWEL SIGN U;Mc;0;L;;;;;N;;;;;
+0BC2;TAMIL VOWEL SIGN UU;Mc;0;L;;;;;N;;;;;
+0BC6;TAMIL VOWEL SIGN E;Mc;0;L;;;;;N;;;;;
+0BC7;TAMIL VOWEL SIGN EE;Mc;0;L;;;;;N;;;;;
+0BC8;TAMIL VOWEL SIGN AI;Mc;0;L;;;;;N;;;;;
+0BCA;TAMIL VOWEL SIGN O;Mc;0;L;0BC6 0BBE;;;;N;;;;;
+0BCB;TAMIL VOWEL SIGN OO;Mc;0;L;0BC7 0BBE;;;;N;;;;;
+0BCC;TAMIL VOWEL SIGN AU;Mc;0;L;0BC6 0BD7;;;;N;;;;;
+0BCD;TAMIL SIGN VIRAMA;Mn;9;NSM;;;;;N;;;;;
+0BD0;TAMIL OM;Lo;0;L;;;;;N;;;;;
+0BD7;TAMIL AU LENGTH MARK;Mc;0;L;;;;;N;;;;;
+0BE6;TAMIL DIGIT ZERO;Nd;0;L;;0;0;0;N;;;;;
+0BE7;TAMIL DIGIT ONE;Nd;0;L;;1;1;1;N;;;;;
+0BE8;TAMIL DIGIT TWO;Nd;0;L;;2;2;2;N;;;;;
+0BE9;TAMIL DIGIT THREE;Nd;0;L;;3;3;3;N;;;;;
+0BEA;TAMIL DIGIT FOUR;Nd;0;L;;4;4;4;N;;;;;
+0BEB;TAMIL DIGIT FIVE;Nd;0;L;;5;5;5;N;;;;;
+0BEC;TAMIL DIGIT SIX;Nd;0;L;;6;6;6;N;;;;;
+0BED;TAMIL DIGIT SEVEN;Nd;0;L;;7;7;7;N;;;;;
+0BEE;TAMIL DIGIT EIGHT;Nd;0;L;;8;8;8;N;;;;;
+0BEF;TAMIL DIGIT NINE;Nd;0;L;;9;9;9;N;;;;;
+0BF0;TAMIL NUMBER TEN;No;0;L;;;;10;N;;;;;
+0BF1;TAMIL NUMBER ONE HUNDRED;No;0;L;;;;100;N;;;;;
+0BF2;TAMIL NUMBER ONE THOUSAND;No;0;L;;;;1000;N;;;;;
+0BF3;TAMIL DAY SIGN;So;0;ON;;;;;N;;;;;
+0BF4;TAMIL MONTH SIGN;So;0;ON;;;;;N;;;;;
+0BF5;TAMIL YEAR SIGN;So;0;ON;;;;;N;;;;;
+0BF6;TAMIL DEBIT SIGN;So;0;ON;;;;;N;;;;;
+0BF7;TAMIL CREDIT SIGN;So;0;ON;;;;;N;;;;;
+0BF8;TAMIL AS ABOVE SIGN;So;0;ON;;;;;N;;;;;
+0BF9;TAMIL RUPEE SIGN;Sc;0;ET;;;;;N;;;;;
+0BFA;TAMIL NUMBER SIGN;So;0;ON;;;;;N;;;;;
+0C00;TELUGU SIGN COMBINING CANDRABINDU ABOVE;Mn;0;NSM;;;;;N;;;;;
+0C01;TELUGU SIGN CANDRABINDU;Mc;0;L;;;;;N;;;;;
+0C02;TELUGU SIGN ANUSVARA;Mc;0;L;;;;;N;;;;;
+0C03;TELUGU SIGN VISARGA;Mc;0;L;;;;;N;;;;;
+0C05;TELUGU LETTER A;Lo;0;L;;;;;N;;;;;
+0C06;TELUGU LETTER AA;Lo;0;L;;;;;N;;;;;
+0C07;TELUGU LETTER I;Lo;0;L;;;;;N;;;;;
+0C08;TELUGU LETTER II;Lo;0;L;;;;;N;;;;;
+0C09;TELUGU LETTER U;Lo;0;L;;;;;N;;;;;
+0C0A;TELUGU LETTER UU;Lo;0;L;;;;;N;;;;;
+0C0B;TELUGU LETTER VOCALIC R;Lo;0;L;;;;;N;;;;;
+0C0C;TELUGU LETTER VOCALIC L;Lo;0;L;;;;;N;;;;;
+0C0E;TELUGU LETTER E;Lo;0;L;;;;;N;;;;;
+0C0F;TELUGU LETTER EE;Lo;0;L;;;;;N;;;;;
+0C10;TELUGU LETTER AI;Lo;0;L;;;;;N;;;;;
+0C12;TELUGU LETTER O;Lo;0;L;;;;;N;;;;;
+0C13;TELUGU LETTER OO;Lo;0;L;;;;;N;;;;;
+0C14;TELUGU LETTER AU;Lo;0;L;;;;;N;;;;;
+0C15;TELUGU LETTER KA;Lo;0;L;;;;;N;;;;;
+0C16;TELUGU LETTER KHA;Lo;0;L;;;;;N;;;;;
+0C17;TELUGU LETTER GA;Lo;0;L;;;;;N;;;;;
+0C18;TELUGU LETTER GHA;Lo;0;L;;;;;N;;;;;
+0C19;TELUGU LETTER NGA;Lo;0;L;;;;;N;;;;;
+0C1A;TELUGU LETTER CA;Lo;0;L;;;;;N;;;;;
+0C1B;TELUGU LETTER CHA;Lo;0;L;;;;;N;;;;;
+0C1C;TELUGU LETTER JA;Lo;0;L;;;;;N;;;;;
+0C1D;TELUGU LETTER JHA;Lo;0;L;;;;;N;;;;;
+0C1E;TELUGU LETTER NYA;Lo;0;L;;;;;N;;;;;
+0C1F;TELUGU LETTER TTA;Lo;0;L;;;;;N;;;;;
+0C20;TELUGU LETTER TTHA;Lo;0;L;;;;;N;;;;;
+0C21;TELUGU LETTER DDA;Lo;0;L;;;;;N;;;;;
+0C22;TELUGU LETTER DDHA;Lo;0;L;;;;;N;;;;;
+0C23;TELUGU LETTER NNA;Lo;0;L;;;;;N;;;;;
+0C24;TELUGU LETTER TA;Lo;0;L;;;;;N;;;;;
+0C25;TELUGU LETTER THA;Lo;0;L;;;;;N;;;;;
+0C26;TELUGU LETTER DA;Lo;0;L;;;;;N;;;;;
+0C27;TELUGU LETTER DHA;Lo;0;L;;;;;N;;;;;
+0C28;TELUGU LETTER NA;Lo;0;L;;;;;N;;;;;
+0C2A;TELUGU LETTER PA;Lo;0;L;;;;;N;;;;;
+0C2B;TELUGU LETTER PHA;Lo;0;L;;;;;N;;;;;
+0C2C;TELUGU LETTER BA;Lo;0;L;;;;;N;;;;;
+0C2D;TELUGU LETTER BHA;Lo;0;L;;;;;N;;;;;
+0C2E;TELUGU LETTER MA;Lo;0;L;;;;;N;;;;;
+0C2F;TELUGU LETTER YA;Lo;0;L;;;;;N;;;;;
+0C30;TELUGU LETTER RA;Lo;0;L;;;;;N;;;;;
+0C31;TELUGU LETTER RRA;Lo;0;L;;;;;N;;;;;
+0C32;TELUGU LETTER LA;Lo;0;L;;;;;N;;;;;
+0C33;TELUGU LETTER LLA;Lo;0;L;;;;;N;;;;;
+0C34;TELUGU LETTER LLLA;Lo;0;L;;;;;N;;;;;
+0C35;TELUGU LETTER VA;Lo;0;L;;;;;N;;;;;
+0C36;TELUGU LETTER SHA;Lo;0;L;;;;;N;;;;;
+0C37;TELUGU LETTER SSA;Lo;0;L;;;;;N;;;;;
+0C38;TELUGU LETTER SA;Lo;0;L;;;;;N;;;;;
+0C39;TELUGU LETTER HA;Lo;0;L;;;;;N;;;;;
+0C3D;TELUGU SIGN AVAGRAHA;Lo;0;L;;;;;N;;;;;
+0C3E;TELUGU VOWEL SIGN AA;Mn;0;NSM;;;;;N;;;;;
+0C3F;TELUGU VOWEL SIGN I;Mn;0;NSM;;;;;N;;;;;
+0C40;TELUGU VOWEL SIGN II;Mn;0;NSM;;;;;N;;;;;
+0C41;TELUGU VOWEL SIGN U;Mc;0;L;;;;;N;;;;;
+0C42;TELUGU VOWEL SIGN UU;Mc;0;L;;;;;N;;;;;
+0C43;TELUGU VOWEL SIGN VOCALIC R;Mc;0;L;;;;;N;;;;;
+0C44;TELUGU VOWEL SIGN VOCALIC RR;Mc;0;L;;;;;N;;;;;
+0C46;TELUGU VOWEL SIGN E;Mn;0;NSM;;;;;N;;;;;
+0C47;TELUGU VOWEL SIGN EE;Mn;0;NSM;;;;;N;;;;;
+0C48;TELUGU VOWEL SIGN AI;Mn;0;NSM;0C46 0C56;;;;N;;;;;
+0C4A;TELUGU VOWEL SIGN O;Mn;0;NSM;;;;;N;;;;;
+0C4B;TELUGU VOWEL SIGN OO;Mn;0;NSM;;;;;N;;;;;
+0C4C;TELUGU VOWEL SIGN AU;Mn;0;NSM;;;;;N;;;;;
+0C4D;TELUGU SIGN VIRAMA;Mn;9;NSM;;;;;N;;;;;
+0C55;TELUGU LENGTH MARK;Mn;84;NSM;;;;;N;;;;;
+0C56;TELUGU AI LENGTH MARK;Mn;91;NSM;;;;;N;;;;;
+0C58;TELUGU LETTER TSA;Lo;0;L;;;;;N;;;;;
+0C59;TELUGU LETTER DZA;Lo;0;L;;;;;N;;;;;
+0C5A;TELUGU LETTER RRRA;Lo;0;L;;;;;N;;;;;
+0C60;TELUGU LETTER VOCALIC RR;Lo;0;L;;;;;N;;;;;
+0C61;TELUGU LETTER VOCALIC LL;Lo;0;L;;;;;N;;;;;
+0C62;TELUGU VOWEL SIGN VOCALIC L;Mn;0;NSM;;;;;N;;;;;
+0C63;TELUGU VOWEL SIGN VOCALIC LL;Mn;0;NSM;;;;;N;;;;;
+0C66;TELUGU DIGIT ZERO;Nd;0;L;;0;0;0;N;;;;;
+0C67;TELUGU DIGIT ONE;Nd;0;L;;1;1;1;N;;;;;
+0C68;TELUGU DIGIT TWO;Nd;0;L;;2;2;2;N;;;;;
+0C69;TELUGU DIGIT THREE;Nd;0;L;;3;3;3;N;;;;;
+0C6A;TELUGU DIGIT FOUR;Nd;0;L;;4;4;4;N;;;;;
+0C6B;TELUGU DIGIT FIVE;Nd;0;L;;5;5;5;N;;;;;
+0C6C;TELUGU DIGIT SIX;Nd;0;L;;6;6;6;N;;;;;
+0C6D;TELUGU DIGIT SEVEN;Nd;0;L;;7;7;7;N;;;;;
+0C6E;TELUGU DIGIT EIGHT;Nd;0;L;;8;8;8;N;;;;;
+0C6F;TELUGU DIGIT NINE;Nd;0;L;;9;9;9;N;;;;;
+0C78;TELUGU FRACTION DIGIT ZERO FOR ODD POWERS OF FOUR;No;0;ON;;;;0;N;;;;;
+0C79;TELUGU FRACTION DIGIT ONE FOR ODD POWERS OF FOUR;No;0;ON;;;;1;N;;;;;
+0C7A;TELUGU FRACTION DIGIT TWO FOR ODD POWERS OF FOUR;No;0;ON;;;;2;N;;;;;
+0C7B;TELUGU FRACTION DIGIT THREE FOR ODD POWERS OF FOUR;No;0;ON;;;;3;N;;;;;
+0C7C;TELUGU FRACTION DIGIT ONE FOR EVEN POWERS OF FOUR;No;0;ON;;;;1;N;;;;;
+0C7D;TELUGU FRACTION DIGIT TWO FOR EVEN POWERS OF FOUR;No;0;ON;;;;2;N;;;;;
+0C7E;TELUGU FRACTION DIGIT THREE FOR EVEN POWERS OF FOUR;No;0;ON;;;;3;N;;;;;
+0C7F;TELUGU SIGN TUUMU;So;0;L;;;;;N;;;;;
+0C80;KANNADA SIGN SPACING CANDRABINDU;Lo;0;L;;;;;N;;;;;
+0C81;KANNADA SIGN CANDRABINDU;Mn;0;NSM;;;;;N;;;;;
+0C82;KANNADA SIGN ANUSVARA;Mc;0;L;;;;;N;;;;;
+0C83;KANNADA SIGN VISARGA;Mc;0;L;;;;;N;;;;;
+0C85;KANNADA LETTER A;Lo;0;L;;;;;N;;;;;
+0C86;KANNADA LETTER AA;Lo;0;L;;;;;N;;;;;
+0C87;KANNADA LETTER I;Lo;0;L;;;;;N;;;;;
+0C88;KANNADA LETTER II;Lo;0;L;;;;;N;;;;;
+0C89;KANNADA LETTER U;Lo;0;L;;;;;N;;;;;
+0C8A;KANNADA LETTER UU;Lo;0;L;;;;;N;;;;;
+0C8B;KANNADA LETTER VOCALIC R;Lo;0;L;;;;;N;;;;;
+0C8C;KANNADA LETTER VOCALIC L;Lo;0;L;;;;;N;;;;;
+0C8E;KANNADA LETTER E;Lo;0;L;;;;;N;;;;;
+0C8F;KANNADA LETTER EE;Lo;0;L;;;;;N;;;;;
+0C90;KANNADA LETTER AI;Lo;0;L;;;;;N;;;;;
+0C92;KANNADA LETTER O;Lo;0;L;;;;;N;;;;;
+0C93;KANNADA LETTER OO;Lo;0;L;;;;;N;;;;;
+0C94;KANNADA LETTER AU;Lo;0;L;;;;;N;;;;;
+0C95;KANNADA LETTER KA;Lo;0;L;;;;;N;;;;;
+0C96;KANNADA LETTER KHA;Lo;0;L;;;;;N;;;;;
+0C97;KANNADA LETTER GA;Lo;0;L;;;;;N;;;;;
+0C98;KANNADA LETTER GHA;Lo;0;L;;;;;N;;;;;
+0C99;KANNADA LETTER NGA;Lo;0;L;;;;;N;;;;;
+0C9A;KANNADA LETTER CA;Lo;0;L;;;;;N;;;;;
+0C9B;KANNADA LETTER CHA;Lo;0;L;;;;;N;;;;;
+0C9C;KANNADA LETTER JA;Lo;0;L;;;;;N;;;;;
+0C9D;KANNADA LETTER JHA;Lo;0;L;;;;;N;;;;;
+0C9E;KANNADA LETTER NYA;Lo;0;L;;;;;N;;;;;
+0C9F;KANNADA LETTER TTA;Lo;0;L;;;;;N;;;;;
+0CA0;KANNADA LETTER TTHA;Lo;0;L;;;;;N;;;;;
+0CA1;KANNADA LETTER DDA;Lo;0;L;;;;;N;;;;;
+0CA2;KANNADA LETTER DDHA;Lo;0;L;;;;;N;;;;;
+0CA3;KANNADA LETTER NNA;Lo;0;L;;;;;N;;;;;
+0CA4;KANNADA LETTER TA;Lo;0;L;;;;;N;;;;;
+0CA5;KANNADA LETTER THA;Lo;0;L;;;;;N;;;;;
+0CA6;KANNADA LETTER DA;Lo;0;L;;;;;N;;;;;
+0CA7;KANNADA LETTER DHA;Lo;0;L;;;;;N;;;;;
+0CA8;KANNADA LETTER NA;Lo;0;L;;;;;N;;;;;
+0CAA;KANNADA LETTER PA;Lo;0;L;;;;;N;;;;;
+0CAB;KANNADA LETTER PHA;Lo;0;L;;;;;N;;;;;
+0CAC;KANNADA LETTER BA;Lo;0;L;;;;;N;;;;;
+0CAD;KANNADA LETTER BHA;Lo;0;L;;;;;N;;;;;
+0CAE;KANNADA LETTER MA;Lo;0;L;;;;;N;;;;;
+0CAF;KANNADA LETTER YA;Lo;0;L;;;;;N;;;;;
+0CB0;KANNADA LETTER RA;Lo;0;L;;;;;N;;;;;
+0CB1;KANNADA LETTER RRA;Lo;0;L;;;;;N;;;;;
+0CB2;KANNADA LETTER LA;Lo;0;L;;;;;N;;;;;
+0CB3;KANNADA LETTER LLA;Lo;0;L;;;;;N;;;;;
+0CB5;KANNADA LETTER VA;Lo;0;L;;;;;N;;;;;
+0CB6;KANNADA LETTER SHA;Lo;0;L;;;;;N;;;;;
+0CB7;KANNADA LETTER SSA;Lo;0;L;;;;;N;;;;;
+0CB8;KANNADA LETTER SA;Lo;0;L;;;;;N;;;;;
+0CB9;KANNADA LETTER HA;Lo;0;L;;;;;N;;;;;
+0CBC;KANNADA SIGN NUKTA;Mn;7;NSM;;;;;N;;;;;
+0CBD;KANNADA SIGN AVAGRAHA;Lo;0;L;;;;;N;;;;;
+0CBE;KANNADA VOWEL SIGN AA;Mc;0;L;;;;;N;;;;;
+0CBF;KANNADA VOWEL SIGN I;Mn;0;L;;;;;N;;;;;
+0CC0;KANNADA VOWEL SIGN II;Mc;0;L;0CBF 0CD5;;;;N;;;;;
+0CC1;KANNADA VOWEL SIGN U;Mc;0;L;;;;;N;;;;;
+0CC2;KANNADA VOWEL SIGN UU;Mc;0;L;;;;;N;;;;;
+0CC3;KANNADA VOWEL SIGN VOCALIC R;Mc;0;L;;;;;N;;;;;
+0CC4;KANNADA VOWEL SIGN VOCALIC RR;Mc;0;L;;;;;N;;;;;
+0CC6;KANNADA VOWEL SIGN E;Mn;0;L;;;;;N;;;;;
+0CC7;KANNADA VOWEL SIGN EE;Mc;0;L;0CC6 0CD5;;;;N;;;;;
+0CC8;KANNADA VOWEL SIGN AI;Mc;0;L;0CC6 0CD6;;;;N;;;;;
+0CCA;KANNADA VOWEL SIGN O;Mc;0;L;0CC6 0CC2;;;;N;;;;;
+0CCB;KANNADA VOWEL SIGN OO;Mc;0;L;0CCA 0CD5;;;;N;;;;;
+0CCC;KANNADA VOWEL SIGN AU;Mn;0;NSM;;;;;N;;;;;
+0CCD;KANNADA SIGN VIRAMA;Mn;9;NSM;;;;;N;;;;;
+0CD5;KANNADA LENGTH MARK;Mc;0;L;;;;;N;;;;;
+0CD6;KANNADA AI LENGTH MARK;Mc;0;L;;;;;N;;;;;
+0CDE;KANNADA LETTER FA;Lo;0;L;;;;;N;;;;;
+0CE0;KANNADA LETTER VOCALIC RR;Lo;0;L;;;;;N;;;;;
+0CE1;KANNADA LETTER VOCALIC LL;Lo;0;L;;;;;N;;;;;
+0CE2;KANNADA VOWEL SIGN VOCALIC L;Mn;0;NSM;;;;;N;;;;;
+0CE3;KANNADA VOWEL SIGN VOCALIC LL;Mn;0;NSM;;;;;N;;;;;
+0CE6;KANNADA DIGIT ZERO;Nd;0;L;;0;0;0;N;;;;;
+0CE7;KANNADA DIGIT ONE;Nd;0;L;;1;1;1;N;;;;;
+0CE8;KANNADA DIGIT TWO;Nd;0;L;;2;2;2;N;;;;;
+0CE9;KANNADA DIGIT THREE;Nd;0;L;;3;3;3;N;;;;;
+0CEA;KANNADA DIGIT FOUR;Nd;0;L;;4;4;4;N;;;;;
+0CEB;KANNADA DIGIT FIVE;Nd;0;L;;5;5;5;N;;;;;
+0CEC;KANNADA DIGIT SIX;Nd;0;L;;6;6;6;N;;;;;
+0CED;KANNADA DIGIT SEVEN;Nd;0;L;;7;7;7;N;;;;;
+0CEE;KANNADA DIGIT EIGHT;Nd;0;L;;8;8;8;N;;;;;
+0CEF;KANNADA DIGIT NINE;Nd;0;L;;9;9;9;N;;;;;
+0CF1;KANNADA SIGN JIHVAMULIYA;Lo;0;L;;;;;N;;;;;
+0CF2;KANNADA SIGN UPADHMANIYA;Lo;0;L;;;;;N;;;;;
+0D01;MALAYALAM SIGN CANDRABINDU;Mn;0;NSM;;;;;N;;;;;
+0D02;MALAYALAM SIGN ANUSVARA;Mc;0;L;;;;;N;;;;;
+0D03;MALAYALAM SIGN VISARGA;Mc;0;L;;;;;N;;;;;
+0D05;MALAYALAM LETTER A;Lo;0;L;;;;;N;;;;;
+0D06;MALAYALAM LETTER AA;Lo;0;L;;;;;N;;;;;
+0D07;MALAYALAM LETTER I;Lo;0;L;;;;;N;;;;;
+0D08;MALAYALAM LETTER II;Lo;0;L;;;;;N;;;;;
+0D09;MALAYALAM LETTER U;Lo;0;L;;;;;N;;;;;
+0D0A;MALAYALAM LETTER UU;Lo;0;L;;;;;N;;;;;
+0D0B;MALAYALAM LETTER VOCALIC R;Lo;0;L;;;;;N;;;;;
+0D0C;MALAYALAM LETTER VOCALIC L;Lo;0;L;;;;;N;;;;;
+0D0E;MALAYALAM LETTER E;Lo;0;L;;;;;N;;;;;
+0D0F;MALAYALAM LETTER EE;Lo;0;L;;;;;N;;;;;
+0D10;MALAYALAM LETTER AI;Lo;0;L;;;;;N;;;;;
+0D12;MALAYALAM LETTER O;Lo;0;L;;;;;N;;;;;
+0D13;MALAYALAM LETTER OO;Lo;0;L;;;;;N;;;;;
+0D14;MALAYALAM LETTER AU;Lo;0;L;;;;;N;;;;;
+0D15;MALAYALAM LETTER KA;Lo;0;L;;;;;N;;;;;
+0D16;MALAYALAM LETTER KHA;Lo;0;L;;;;;N;;;;;
+0D17;MALAYALAM LETTER GA;Lo;0;L;;;;;N;;;;;
+0D18;MALAYALAM LETTER GHA;Lo;0;L;;;;;N;;;;;
+0D19;MALAYALAM LETTER NGA;Lo;0;L;;;;;N;;;;;
+0D1A;MALAYALAM LETTER CA;Lo;0;L;;;;;N;;;;;
+0D1B;MALAYALAM LETTER CHA;Lo;0;L;;;;;N;;;;;
+0D1C;MALAYALAM LETTER JA;Lo;0;L;;;;;N;;;;;
+0D1D;MALAYALAM LETTER JHA;Lo;0;L;;;;;N;;;;;
+0D1E;MALAYALAM LETTER NYA;Lo;0;L;;;;;N;;;;;
+0D1F;MALAYALAM LETTER TTA;Lo;0;L;;;;;N;;;;;
+0D20;MALAYALAM LETTER TTHA;Lo;0;L;;;;;N;;;;;
+0D21;MALAYALAM LETTER DDA;Lo;0;L;;;;;N;;;;;
+0D22;MALAYALAM LETTER DDHA;Lo;0;L;;;;;N;;;;;
+0D23;MALAYALAM LETTER NNA;Lo;0;L;;;;;N;;;;;
+0D24;MALAYALAM LETTER TA;Lo;0;L;;;;;N;;;;;
+0D25;MALAYALAM LETTER THA;Lo;0;L;;;;;N;;;;;
+0D26;MALAYALAM LETTER DA;Lo;0;L;;;;;N;;;;;
+0D27;MALAYALAM LETTER DHA;Lo;0;L;;;;;N;;;;;
+0D28;MALAYALAM LETTER NA;Lo;0;L;;;;;N;;;;;
+0D29;MALAYALAM LETTER NNNA;Lo;0;L;;;;;N;;;;;
+0D2A;MALAYALAM LETTER PA;Lo;0;L;;;;;N;;;;;
+0D2B;MALAYALAM LETTER PHA;Lo;0;L;;;;;N;;;;;
+0D2C;MALAYALAM LETTER BA;Lo;0;L;;;;;N;;;;;
+0D2D;MALAYALAM LETTER BHA;Lo;0;L;;;;;N;;;;;
+0D2E;MALAYALAM LETTER MA;Lo;0;L;;;;;N;;;;;
+0D2F;MALAYALAM LETTER YA;Lo;0;L;;;;;N;;;;;
+0D30;MALAYALAM LETTER RA;Lo;0;L;;;;;N;;;;;
+0D31;MALAYALAM LETTER RRA;Lo;0;L;;;;;N;;;;;
+0D32;MALAYALAM LETTER LA;Lo;0;L;;;;;N;;;;;
+0D33;MALAYALAM LETTER LLA;Lo;0;L;;;;;N;;;;;
+0D34;MALAYALAM LETTER LLLA;Lo;0;L;;;;;N;;;;;
+0D35;MALAYALAM LETTER VA;Lo;0;L;;;;;N;;;;;
+0D36;MALAYALAM LETTER SHA;Lo;0;L;;;;;N;;;;;
+0D37;MALAYALAM LETTER SSA;Lo;0;L;;;;;N;;;;;
+0D38;MALAYALAM LETTER SA;Lo;0;L;;;;;N;;;;;
+0D39;MALAYALAM LETTER HA;Lo;0;L;;;;;N;;;;;
+0D3A;MALAYALAM LETTER TTTA;Lo;0;L;;;;;N;;;;;
+0D3D;MALAYALAM SIGN AVAGRAHA;Lo;0;L;;;;;N;;;;;
+0D3E;MALAYALAM VOWEL SIGN AA;Mc;0;L;;;;;N;;;;;
+0D3F;MALAYALAM VOWEL SIGN I;Mc;0;L;;;;;N;;;;;
+0D40;MALAYALAM VOWEL SIGN II;Mc;0;L;;;;;N;;;;;
+0D41;MALAYALAM VOWEL SIGN U;Mn;0;NSM;;;;;N;;;;;
+0D42;MALAYALAM VOWEL SIGN UU;Mn;0;NSM;;;;;N;;;;;
+0D43;MALAYALAM VOWEL SIGN VOCALIC R;Mn;0;NSM;;;;;N;;;;;
+0D44;MALAYALAM VOWEL SIGN VOCALIC RR;Mn;0;NSM;;;;;N;;;;;
+0D46;MALAYALAM VOWEL SIGN E;Mc;0;L;;;;;N;;;;;
+0D47;MALAYALAM VOWEL SIGN EE;Mc;0;L;;;;;N;;;;;
+0D48;MALAYALAM VOWEL SIGN AI;Mc;0;L;;;;;N;;;;;
+0D4A;MALAYALAM VOWEL SIGN O;Mc;0;L;0D46 0D3E;;;;N;;;;;
+0D4B;MALAYALAM VOWEL SIGN OO;Mc;0;L;0D47 0D3E;;;;N;;;;;
+0D4C;MALAYALAM VOWEL SIGN AU;Mc;0;L;0D46 0D57;;;;N;;;;;
+0D4D;MALAYALAM SIGN VIRAMA;Mn;9;NSM;;;;;N;;;;;
+0D4E;MALAYALAM LETTER DOT REPH;Lo;0;L;;;;;N;;;;;
+0D4F;MALAYALAM SIGN PARA;So;0;L;;;;;N;;;;;
+0D54;MALAYALAM LETTER CHILLU M;Lo;0;L;;;;;N;;;;;
+0D55;MALAYALAM LETTER CHILLU Y;Lo;0;L;;;;;N;;;;;
+0D56;MALAYALAM LETTER CHILLU LLL;Lo;0;L;;;;;N;;;;;
+0D57;MALAYALAM AU LENGTH MARK;Mc;0;L;;;;;N;;;;;
+0D58;MALAYALAM FRACTION ONE ONE-HUNDRED-AND-SIXTIETH;No;0;L;;;;1/160;N;;;;;
+0D59;MALAYALAM FRACTION ONE FORTIETH;No;0;L;;;;1/40;N;;;;;
+0D5A;MALAYALAM FRACTION THREE EIGHTIETHS;No;0;L;;;;3/80;N;;;;;
+0D5B;MALAYALAM FRACTION ONE TWENTIETH;No;0;L;;;;1/20;N;;;;;
+0D5C;MALAYALAM FRACTION ONE TENTH;No;0;L;;;;1/10;N;;;;;
+0D5D;MALAYALAM FRACTION THREE TWENTIETHS;No;0;L;;;;3/20;N;;;;;
+0D5E;MALAYALAM FRACTION ONE FIFTH;No;0;L;;;;1/5;N;;;;;
+0D5F;MALAYALAM LETTER ARCHAIC II;Lo;0;L;;;;;N;;;;;
+0D60;MALAYALAM LETTER VOCALIC RR;Lo;0;L;;;;;N;;;;;
+0D61;MALAYALAM LETTER VOCALIC LL;Lo;0;L;;;;;N;;;;;
+0D62;MALAYALAM VOWEL SIGN VOCALIC L;Mn;0;NSM;;;;;N;;;;;
+0D63;MALAYALAM VOWEL SIGN VOCALIC LL;Mn;0;NSM;;;;;N;;;;;
+0D66;MALAYALAM DIGIT ZERO;Nd;0;L;;0;0;0;N;;;;;
+0D67;MALAYALAM DIGIT ONE;Nd;0;L;;1;1;1;N;;;;;
+0D68;MALAYALAM DIGIT TWO;Nd;0;L;;2;2;2;N;;;;;
+0D69;MALAYALAM DIGIT THREE;Nd;0;L;;3;3;3;N;;;;;
+0D6A;MALAYALAM DIGIT FOUR;Nd;0;L;;4;4;4;N;;;;;
+0D6B;MALAYALAM DIGIT FIVE;Nd;0;L;;5;5;5;N;;;;;
+0D6C;MALAYALAM DIGIT SIX;Nd;0;L;;6;6;6;N;;;;;
+0D6D;MALAYALAM DIGIT SEVEN;Nd;0;L;;7;7;7;N;;;;;
+0D6E;MALAYALAM DIGIT EIGHT;Nd;0;L;;8;8;8;N;;;;;
+0D6F;MALAYALAM DIGIT NINE;Nd;0;L;;9;9;9;N;;;;;
+0D70;MALAYALAM NUMBER TEN;No;0;L;;;;10;N;;;;;
+0D71;MALAYALAM NUMBER ONE HUNDRED;No;0;L;;;;100;N;;;;;
+0D72;MALAYALAM NUMBER ONE THOUSAND;No;0;L;;;;1000;N;;;;;
+0D73;MALAYALAM FRACTION ONE QUARTER;No;0;L;;;;1/4;N;;;;;
+0D74;MALAYALAM FRACTION ONE HALF;No;0;L;;;;1/2;N;;;;;
+0D75;MALAYALAM FRACTION THREE QUARTERS;No;0;L;;;;3/4;N;;;;;
+0D76;MALAYALAM FRACTION ONE SIXTEENTH;No;0;L;;;;1/16;N;;;;;
+0D77;MALAYALAM FRACTION ONE EIGHTH;No;0;L;;;;1/8;N;;;;;
+0D78;MALAYALAM FRACTION THREE SIXTEENTHS;No;0;L;;;;3/16;N;;;;;
+0D79;MALAYALAM DATE MARK;So;0;L;;;;;N;;;;;
+0D7A;MALAYALAM LETTER CHILLU NN;Lo;0;L;;;;;N;;;;;
+0D7B;MALAYALAM LETTER CHILLU N;Lo;0;L;;;;;N;;;;;
+0D7C;MALAYALAM LETTER CHILLU RR;Lo;0;L;;;;;N;;;;;
+0D7D;MALAYALAM LETTER CHILLU L;Lo;0;L;;;;;N;;;;;
+0D7E;MALAYALAM LETTER CHILLU LL;Lo;0;L;;;;;N;;;;;
+0D7F;MALAYALAM LETTER CHILLU K;Lo;0;L;;;;;N;;;;;
+0D82;SINHALA SIGN ANUSVARAYA;Mc;0;L;;;;;N;;;;;
+0D83;SINHALA SIGN VISARGAYA;Mc;0;L;;;;;N;;;;;
+0D85;SINHALA LETTER AYANNA;Lo;0;L;;;;;N;;;;;
+0D86;SINHALA LETTER AAYANNA;Lo;0;L;;;;;N;;;;;
+0D87;SINHALA LETTER AEYANNA;Lo;0;L;;;;;N;;;;;
+0D88;SINHALA LETTER AEEYANNA;Lo;0;L;;;;;N;;;;;
+0D89;SINHALA LETTER IYANNA;Lo;0;L;;;;;N;;;;;
+0D8A;SINHALA LETTER IIYANNA;Lo;0;L;;;;;N;;;;;
+0D8B;SINHALA LETTER UYANNA;Lo;0;L;;;;;N;;;;;
+0D8C;SINHALA LETTER UUYANNA;Lo;0;L;;;;;N;;;;;
+0D8D;SINHALA LETTER IRUYANNA;Lo;0;L;;;;;N;;;;;
+0D8E;SINHALA LETTER IRUUYANNA;Lo;0;L;;;;;N;;;;;
+0D8F;SINHALA LETTER ILUYANNA;Lo;0;L;;;;;N;;;;;
+0D90;SINHALA LETTER ILUUYANNA;Lo;0;L;;;;;N;;;;;
+0D91;SINHALA LETTER EYANNA;Lo;0;L;;;;;N;;;;;
+0D92;SINHALA LETTER EEYANNA;Lo;0;L;;;;;N;;;;;
+0D93;SINHALA LETTER AIYANNA;Lo;0;L;;;;;N;;;;;
+0D94;SINHALA LETTER OYANNA;Lo;0;L;;;;;N;;;;;
+0D95;SINHALA LETTER OOYANNA;Lo;0;L;;;;;N;;;;;
+0D96;SINHALA LETTER AUYANNA;Lo;0;L;;;;;N;;;;;
+0D9A;SINHALA LETTER ALPAPRAANA KAYANNA;Lo;0;L;;;;;N;;;;;
+0D9B;SINHALA LETTER MAHAAPRAANA KAYANNA;Lo;0;L;;;;;N;;;;;
+0D9C;SINHALA LETTER ALPAPRAANA GAYANNA;Lo;0;L;;;;;N;;;;;
+0D9D;SINHALA LETTER MAHAAPRAANA GAYANNA;Lo;0;L;;;;;N;;;;;
+0D9E;SINHALA LETTER KANTAJA NAASIKYAYA;Lo;0;L;;;;;N;;;;;
+0D9F;SINHALA LETTER SANYAKA GAYANNA;Lo;0;L;;;;;N;;;;;
+0DA0;SINHALA LETTER ALPAPRAANA CAYANNA;Lo;0;L;;;;;N;;;;;
+0DA1;SINHALA LETTER MAHAAPRAANA CAYANNA;Lo;0;L;;;;;N;;;;;
+0DA2;SINHALA LETTER ALPAPRAANA JAYANNA;Lo;0;L;;;;;N;;;;;
+0DA3;SINHALA LETTER MAHAAPRAANA JAYANNA;Lo;0;L;;;;;N;;;;;
+0DA4;SINHALA LETTER TAALUJA NAASIKYAYA;Lo;0;L;;;;;N;;;;;
+0DA5;SINHALA LETTER TAALUJA SANYOOGA NAAKSIKYAYA;Lo;0;L;;;;;N;;;;;
+0DA6;SINHALA LETTER SANYAKA JAYANNA;Lo;0;L;;;;;N;;;;;
+0DA7;SINHALA LETTER ALPAPRAANA TTAYANNA;Lo;0;L;;;;;N;;;;;
+0DA8;SINHALA LETTER MAHAAPRAANA TTAYANNA;Lo;0;L;;;;;N;;;;;
+0DA9;SINHALA LETTER ALPAPRAANA DDAYANNA;Lo;0;L;;;;;N;;;;;
+0DAA;SINHALA LETTER MAHAAPRAANA DDAYANNA;Lo;0;L;;;;;N;;;;;
+0DAB;SINHALA LETTER MUURDHAJA NAYANNA;Lo;0;L;;;;;N;;;;;
+0DAC;SINHALA LETTER SANYAKA DDAYANNA;Lo;0;L;;;;;N;;;;;
+0DAD;SINHALA LETTER ALPAPRAANA TAYANNA;Lo;0;L;;;;;N;;;;;
+0DAE;SINHALA LETTER MAHAAPRAANA TAYANNA;Lo;0;L;;;;;N;;;;;
+0DAF;SINHALA LETTER ALPAPRAANA DAYANNA;Lo;0;L;;;;;N;;;;;
+0DB0;SINHALA LETTER MAHAAPRAANA DAYANNA;Lo;0;L;;;;;N;;;;;
+0DB1;SINHALA LETTER DANTAJA NAYANNA;Lo;0;L;;;;;N;;;;;
+0DB3;SINHALA LETTER SANYAKA DAYANNA;Lo;0;L;;;;;N;;;;;
+0DB4;SINHALA LETTER ALPAPRAANA PAYANNA;Lo;0;L;;;;;N;;;;;
+0DB5;SINHALA LETTER MAHAAPRAANA PAYANNA;Lo;0;L;;;;;N;;;;;
+0DB6;SINHALA LETTER ALPAPRAANA BAYANNA;Lo;0;L;;;;;N;;;;;
+0DB7;SINHALA LETTER MAHAAPRAANA BAYANNA;Lo;0;L;;;;;N;;;;;
+0DB8;SINHALA LETTER MAYANNA;Lo;0;L;;;;;N;;;;;
+0DB9;SINHALA LETTER AMBA BAYANNA;Lo;0;L;;;;;N;;;;;
+0DBA;SINHALA LETTER YAYANNA;Lo;0;L;;;;;N;;;;;
+0DBB;SINHALA LETTER RAYANNA;Lo;0;L;;;;;N;;;;;
+0DBD;SINHALA LETTER DANTAJA LAYANNA;Lo;0;L;;;;;N;;;;;
+0DC0;SINHALA LETTER VAYANNA;Lo;0;L;;;;;N;;;;;
+0DC1;SINHALA LETTER TAALUJA SAYANNA;Lo;0;L;;;;;N;;;;;
+0DC2;SINHALA LETTER MUURDHAJA SAYANNA;Lo;0;L;;;;;N;;;;;
+0DC3;SINHALA LETTER DANTAJA SAYANNA;Lo;0;L;;;;;N;;;;;
+0DC4;SINHALA LETTER HAYANNA;Lo;0;L;;;;;N;;;;;
+0DC5;SINHALA LETTER MUURDHAJA LAYANNA;Lo;0;L;;;;;N;;;;;
+0DC6;SINHALA LETTER FAYANNA;Lo;0;L;;;;;N;;;;;
+0DCA;SINHALA SIGN AL-LAKUNA;Mn;9;NSM;;;;;N;;;;;
+0DCF;SINHALA VOWEL SIGN AELA-PILLA;Mc;0;L;;;;;N;;;;;
+0DD0;SINHALA VOWEL SIGN KETTI AEDA-PILLA;Mc;0;L;;;;;N;;;;;
+0DD1;SINHALA VOWEL SIGN DIGA AEDA-PILLA;Mc;0;L;;;;;N;;;;;
+0DD2;SINHALA VOWEL SIGN KETTI IS-PILLA;Mn;0;NSM;;;;;N;;;;;
+0DD3;SINHALA VOWEL SIGN DIGA IS-PILLA;Mn;0;NSM;;;;;N;;;;;
+0DD4;SINHALA VOWEL SIGN KETTI PAA-PILLA;Mn;0;NSM;;;;;N;;;;;
+0DD6;SINHALA VOWEL SIGN DIGA PAA-PILLA;Mn;0;NSM;;;;;N;;;;;
+0DD8;SINHALA VOWEL SIGN GAETTA-PILLA;Mc;0;L;;;;;N;;;;;
+0DD9;SINHALA VOWEL SIGN KOMBUVA;Mc;0;L;;;;;N;;;;;
+0DDA;SINHALA VOWEL SIGN DIGA KOMBUVA;Mc;0;L;0DD9 0DCA;;;;N;;;;;
+0DDB;SINHALA VOWEL SIGN KOMBU DEKA;Mc;0;L;;;;;N;;;;;
+0DDC;SINHALA VOWEL SIGN KOMBUVA HAA AELA-PILLA;Mc;0;L;0DD9 0DCF;;;;N;;;;;
+0DDD;SINHALA VOWEL SIGN KOMBUVA HAA DIGA AELA-PILLA;Mc;0;L;0DDC 0DCA;;;;N;;;;;
+0DDE;SINHALA VOWEL SIGN KOMBUVA HAA GAYANUKITTA;Mc;0;L;0DD9 0DDF;;;;N;;;;;
+0DDF;SINHALA VOWEL SIGN GAYANUKITTA;Mc;0;L;;;;;N;;;;;
+0DE6;SINHALA LITH DIGIT ZERO;Nd;0;L;;0;0;0;N;;;;;
+0DE7;SINHALA LITH DIGIT ONE;Nd;0;L;;1;1;1;N;;;;;
+0DE8;SINHALA LITH DIGIT TWO;Nd;0;L;;2;2;2;N;;;;;
+0DE9;SINHALA LITH DIGIT THREE;Nd;0;L;;3;3;3;N;;;;;
+0DEA;SINHALA LITH DIGIT FOUR;Nd;0;L;;4;4;4;N;;;;;
+0DEB;SINHALA LITH DIGIT FIVE;Nd;0;L;;5;5;5;N;;;;;
+0DEC;SINHALA LITH DIGIT SIX;Nd;0;L;;6;6;6;N;;;;;
+0DED;SINHALA LITH DIGIT SEVEN;Nd;0;L;;7;7;7;N;;;;;
+0DEE;SINHALA LITH DIGIT EIGHT;Nd;0;L;;8;8;8;N;;;;;
+0DEF;SINHALA LITH DIGIT NINE;Nd;0;L;;9;9;9;N;;;;;
+0DF2;SINHALA VOWEL SIGN DIGA GAETTA-PILLA;Mc;0;L;;;;;N;;;;;
+0DF3;SINHALA VOWEL SIGN DIGA GAYANUKITTA;Mc;0;L;;;;;N;;;;;
+0DF4;SINHALA PUNCTUATION KUNDDALIYA;Po;0;L;;;;;N;;;;;
+0E01;THAI CHARACTER KO KAI;Lo;0;L;;;;;N;THAI LETTER KO KAI;;;;
+0E02;THAI CHARACTER KHO KHAI;Lo;0;L;;;;;N;THAI LETTER KHO KHAI;;;;
+0E03;THAI CHARACTER KHO KHUAT;Lo;0;L;;;;;N;THAI LETTER KHO KHUAT;;;;
+0E04;THAI CHARACTER KHO KHWAI;Lo;0;L;;;;;N;THAI LETTER KHO KHWAI;;;;
+0E05;THAI CHARACTER KHO KHON;Lo;0;L;;;;;N;THAI LETTER KHO KHON;;;;
+0E06;THAI CHARACTER KHO RAKHANG;Lo;0;L;;;;;N;THAI LETTER KHO RAKHANG;;;;
+0E07;THAI CHARACTER NGO NGU;Lo;0;L;;;;;N;THAI LETTER NGO NGU;;;;
+0E08;THAI CHARACTER CHO CHAN;Lo;0;L;;;;;N;THAI LETTER CHO CHAN;;;;
+0E09;THAI CHARACTER CHO CHING;Lo;0;L;;;;;N;THAI LETTER CHO CHING;;;;
+0E0A;THAI CHARACTER CHO CHANG;Lo;0;L;;;;;N;THAI LETTER CHO CHANG;;;;
+0E0B;THAI CHARACTER SO SO;Lo;0;L;;;;;N;THAI LETTER SO SO;;;;
+0E0C;THAI CHARACTER CHO CHOE;Lo;0;L;;;;;N;THAI LETTER CHO CHOE;;;;
+0E0D;THAI CHARACTER YO YING;Lo;0;L;;;;;N;THAI LETTER YO YING;;;;
+0E0E;THAI CHARACTER DO CHADA;Lo;0;L;;;;;N;THAI LETTER DO CHADA;;;;
+0E0F;THAI CHARACTER TO PATAK;Lo;0;L;;;;;N;THAI LETTER TO PATAK;;;;
+0E10;THAI CHARACTER THO THAN;Lo;0;L;;;;;N;THAI LETTER THO THAN;;;;
+0E11;THAI CHARACTER THO NANGMONTHO;Lo;0;L;;;;;N;THAI LETTER THO NANGMONTHO;;;;
+0E12;THAI CHARACTER THO PHUTHAO;Lo;0;L;;;;;N;THAI LETTER THO PHUTHAO;;;;
+0E13;THAI CHARACTER NO NEN;Lo;0;L;;;;;N;THAI LETTER NO NEN;;;;
+0E14;THAI CHARACTER DO DEK;Lo;0;L;;;;;N;THAI LETTER DO DEK;;;;
+0E15;THAI CHARACTER TO TAO;Lo;0;L;;;;;N;THAI LETTER TO TAO;;;;
+0E16;THAI CHARACTER THO THUNG;Lo;0;L;;;;;N;THAI LETTER THO THUNG;;;;
+0E17;THAI CHARACTER THO THAHAN;Lo;0;L;;;;;N;THAI LETTER THO THAHAN;;;;
+0E18;THAI CHARACTER THO THONG;Lo;0;L;;;;;N;THAI LETTER THO THONG;;;;
+0E19;THAI CHARACTER NO NU;Lo;0;L;;;;;N;THAI LETTER NO NU;;;;
+0E1A;THAI CHARACTER BO BAIMAI;Lo;0;L;;;;;N;THAI LETTER BO BAIMAI;;;;
+0E1B;THAI CHARACTER PO PLA;Lo;0;L;;;;;N;THAI LETTER PO PLA;;;;
+0E1C;THAI CHARACTER PHO PHUNG;Lo;0;L;;;;;N;THAI LETTER PHO PHUNG;;;;
+0E1D;THAI CHARACTER FO FA;Lo;0;L;;;;;N;THAI LETTER FO FA;;;;
+0E1E;THAI CHARACTER PHO PHAN;Lo;0;L;;;;;N;THAI LETTER PHO PHAN;;;;
+0E1F;THAI CHARACTER FO FAN;Lo;0;L;;;;;N;THAI LETTER FO FAN;;;;
+0E20;THAI CHARACTER PHO SAMPHAO;Lo;0;L;;;;;N;THAI LETTER PHO SAMPHAO;;;;
+0E21;THAI CHARACTER MO MA;Lo;0;L;;;;;N;THAI LETTER MO MA;;;;
+0E22;THAI CHARACTER YO YAK;Lo;0;L;;;;;N;THAI LETTER YO YAK;;;;
+0E23;THAI CHARACTER RO RUA;Lo;0;L;;;;;N;THAI LETTER RO RUA;;;;
+0E24;THAI CHARACTER RU;Lo;0;L;;;;;N;THAI LETTER RU;;;;
+0E25;THAI CHARACTER LO LING;Lo;0;L;;;;;N;THAI LETTER LO LING;;;;
+0E26;THAI CHARACTER LU;Lo;0;L;;;;;N;THAI LETTER LU;;;;
+0E27;THAI CHARACTER WO WAEN;Lo;0;L;;;;;N;THAI LETTER WO WAEN;;;;
+0E28;THAI CHARACTER SO SALA;Lo;0;L;;;;;N;THAI LETTER SO SALA;;;;
+0E29;THAI CHARACTER SO RUSI;Lo;0;L;;;;;N;THAI LETTER SO RUSI;;;;
+0E2A;THAI CHARACTER SO SUA;Lo;0;L;;;;;N;THAI LETTER SO SUA;;;;
+0E2B;THAI CHARACTER HO HIP;Lo;0;L;;;;;N;THAI LETTER HO HIP;;;;
+0E2C;THAI CHARACTER LO CHULA;Lo;0;L;;;;;N;THAI LETTER LO CHULA;;;;
+0E2D;THAI CHARACTER O ANG;Lo;0;L;;;;;N;THAI LETTER O ANG;;;;
+0E2E;THAI CHARACTER HO NOKHUK;Lo;0;L;;;;;N;THAI LETTER HO NOK HUK;;;;
+0E2F;THAI CHARACTER PAIYANNOI;Lo;0;L;;;;;N;THAI PAI YAN NOI;;;;
+0E30;THAI CHARACTER SARA A;Lo;0;L;;;;;N;THAI VOWEL SIGN SARA A;;;;
+0E31;THAI CHARACTER MAI HAN-AKAT;Mn;0;NSM;;;;;N;THAI VOWEL SIGN MAI HAN-AKAT;;;;
+0E32;THAI CHARACTER SARA AA;Lo;0;L;;;;;N;THAI VOWEL SIGN SARA AA;;;;
+0E33;THAI CHARACTER SARA AM;Lo;0;L;<compat> 0E4D 0E32;;;;N;THAI VOWEL SIGN SARA AM;;;;
+0E34;THAI CHARACTER SARA I;Mn;0;NSM;;;;;N;THAI VOWEL SIGN SARA I;;;;
+0E35;THAI CHARACTER SARA II;Mn;0;NSM;;;;;N;THAI VOWEL SIGN SARA II;;;;
+0E36;THAI CHARACTER SARA UE;Mn;0;NSM;;;;;N;THAI VOWEL SIGN SARA UE;;;;
+0E37;THAI CHARACTER SARA UEE;Mn;0;NSM;;;;;N;THAI VOWEL SIGN SARA UEE;;;;
+0E38;THAI CHARACTER SARA U;Mn;103;NSM;;;;;N;THAI VOWEL SIGN SARA U;;;;
+0E39;THAI CHARACTER SARA UU;Mn;103;NSM;;;;;N;THAI VOWEL SIGN SARA UU;;;;
+0E3A;THAI CHARACTER PHINTHU;Mn;9;NSM;;;;;N;THAI VOWEL SIGN PHINTHU;;;;
+0E3F;THAI CURRENCY SYMBOL BAHT;Sc;0;ET;;;;;N;THAI BAHT SIGN;;;;
+0E40;THAI CHARACTER SARA E;Lo;0;L;;;;;N;THAI VOWEL SIGN SARA E;;;;
+0E41;THAI CHARACTER SARA AE;Lo;0;L;;;;;N;THAI VOWEL SIGN SARA AE;;;;
+0E42;THAI CHARACTER SARA O;Lo;0;L;;;;;N;THAI VOWEL SIGN SARA O;;;;
+0E43;THAI CHARACTER SARA AI MAIMUAN;Lo;0;L;;;;;N;THAI VOWEL SIGN SARA MAI MUAN;;;;
+0E44;THAI CHARACTER SARA AI MAIMALAI;Lo;0;L;;;;;N;THAI VOWEL SIGN SARA MAI MALAI;;;;
+0E45;THAI CHARACTER LAKKHANGYAO;Lo;0;L;;;;;N;THAI LAK KHANG YAO;;;;
+0E46;THAI CHARACTER MAIYAMOK;Lm;0;L;;;;;N;THAI MAI YAMOK;;;;
+0E47;THAI CHARACTER MAITAIKHU;Mn;0;NSM;;;;;N;THAI VOWEL SIGN MAI TAI KHU;;;;
+0E48;THAI CHARACTER MAI EK;Mn;107;NSM;;;;;N;THAI TONE MAI EK;;;;
+0E49;THAI CHARACTER MAI THO;Mn;107;NSM;;;;;N;THAI TONE MAI THO;;;;
+0E4A;THAI CHARACTER MAI TRI;Mn;107;NSM;;;;;N;THAI TONE MAI TRI;;;;
+0E4B;THAI CHARACTER MAI CHATTAWA;Mn;107;NSM;;;;;N;THAI TONE MAI CHATTAWA;;;;
+0E4C;THAI CHARACTER THANTHAKHAT;Mn;0;NSM;;;;;N;THAI THANTHAKHAT;;;;
+0E4D;THAI CHARACTER NIKHAHIT;Mn;0;NSM;;;;;N;THAI NIKKHAHIT;;;;
+0E4E;THAI CHARACTER YAMAKKAN;Mn;0;NSM;;;;;N;THAI YAMAKKAN;;;;
+0E4F;THAI CHARACTER FONGMAN;Po;0;L;;;;;N;THAI FONGMAN;;;;
+0E50;THAI DIGIT ZERO;Nd;0;L;;0;0;0;N;;;;;
+0E51;THAI DIGIT ONE;Nd;0;L;;1;1;1;N;;;;;
+0E52;THAI DIGIT TWO;Nd;0;L;;2;2;2;N;;;;;
+0E53;THAI DIGIT THREE;Nd;0;L;;3;3;3;N;;;;;
+0E54;THAI DIGIT FOUR;Nd;0;L;;4;4;4;N;;;;;
+0E55;THAI DIGIT FIVE;Nd;0;L;;5;5;5;N;;;;;
+0E56;THAI DIGIT SIX;Nd;0;L;;6;6;6;N;;;;;
+0E57;THAI DIGIT SEVEN;Nd;0;L;;7;7;7;N;;;;;
+0E58;THAI DIGIT EIGHT;Nd;0;L;;8;8;8;N;;;;;
+0E59;THAI DIGIT NINE;Nd;0;L;;9;9;9;N;;;;;
+0E5A;THAI CHARACTER ANGKHANKHU;Po;0;L;;;;;N;THAI ANGKHANKHU;;;;
+0E5B;THAI CHARACTER KHOMUT;Po;0;L;;;;;N;THAI KHOMUT;;;;
+0E81;LAO LETTER KO;Lo;0;L;;;;;N;;;;;
+0E82;LAO LETTER KHO SUNG;Lo;0;L;;;;;N;;;;;
+0E84;LAO LETTER KHO TAM;Lo;0;L;;;;;N;;;;;
+0E87;LAO LETTER NGO;Lo;0;L;;;;;N;;;;;
+0E88;LAO LETTER CO;Lo;0;L;;;;;N;;;;;
+0E8A;LAO LETTER SO TAM;Lo;0;L;;;;;N;;;;;
+0E8D;LAO LETTER NYO;Lo;0;L;;;;;N;;;;;
+0E94;LAO LETTER DO;Lo;0;L;;;;;N;;;;;
+0E95;LAO LETTER TO;Lo;0;L;;;;;N;;;;;
+0E96;LAO LETTER THO SUNG;Lo;0;L;;;;;N;;;;;
+0E97;LAO LETTER THO TAM;Lo;0;L;;;;;N;;;;;
+0E99;LAO LETTER NO;Lo;0;L;;;;;N;;;;;
+0E9A;LAO LETTER BO;Lo;0;L;;;;;N;;;;;
+0E9B;LAO LETTER PO;Lo;0;L;;;;;N;;;;;
+0E9C;LAO LETTER PHO SUNG;Lo;0;L;;;;;N;;;;;
+0E9D;LAO LETTER FO TAM;Lo;0;L;;;;;N;;;;;
+0E9E;LAO LETTER PHO TAM;Lo;0;L;;;;;N;;;;;
+0E9F;LAO LETTER FO SUNG;Lo;0;L;;;;;N;;;;;
+0EA1;LAO LETTER MO;Lo;0;L;;;;;N;;;;;
+0EA2;LAO LETTER YO;Lo;0;L;;;;;N;;;;;
+0EA3;LAO LETTER LO LING;Lo;0;L;;;;;N;;;;;
+0EA5;LAO LETTER LO LOOT;Lo;0;L;;;;;N;;;;;
+0EA7;LAO LETTER WO;Lo;0;L;;;;;N;;;;;
+0EAA;LAO LETTER SO SUNG;Lo;0;L;;;;;N;;;;;
+0EAB;LAO LETTER HO SUNG;Lo;0;L;;;;;N;;;;;
+0EAD;LAO LETTER O;Lo;0;L;;;;;N;;;;;
+0EAE;LAO LETTER HO TAM;Lo;0;L;;;;;N;;;;;
+0EAF;LAO ELLIPSIS;Lo;0;L;;;;;N;;;;;
+0EB0;LAO VOWEL SIGN A;Lo;0;L;;;;;N;;;;;
+0EB1;LAO VOWEL SIGN MAI KAN;Mn;0;NSM;;;;;N;;;;;
+0EB2;LAO VOWEL SIGN AA;Lo;0;L;;;;;N;;;;;
+0EB3;LAO VOWEL SIGN AM;Lo;0;L;<compat> 0ECD 0EB2;;;;N;;;;;
+0EB4;LAO VOWEL SIGN I;Mn;0;NSM;;;;;N;;;;;
+0EB5;LAO VOWEL SIGN II;Mn;0;NSM;;;;;N;;;;;
+0EB6;LAO VOWEL SIGN Y;Mn;0;NSM;;;;;N;;;;;
+0EB7;LAO VOWEL SIGN YY;Mn;0;NSM;;;;;N;;;;;
+0EB8;LAO VOWEL SIGN U;Mn;118;NSM;;;;;N;;;;;
+0EB9;LAO VOWEL SIGN UU;Mn;118;NSM;;;;;N;;;;;
+0EBB;LAO VOWEL SIGN MAI KON;Mn;0;NSM;;;;;N;;;;;
+0EBC;LAO SEMIVOWEL SIGN LO;Mn;0;NSM;;;;;N;;;;;
+0EBD;LAO SEMIVOWEL SIGN NYO;Lo;0;L;;;;;N;;;;;
+0EC0;LAO VOWEL SIGN E;Lo;0;L;;;;;N;;;;;
+0EC1;LAO VOWEL SIGN EI;Lo;0;L;;;;;N;;;;;
+0EC2;LAO VOWEL SIGN O;Lo;0;L;;;;;N;;;;;
+0EC3;LAO VOWEL SIGN AY;Lo;0;L;;;;;N;;;;;
+0EC4;LAO VOWEL SIGN AI;Lo;0;L;;;;;N;;;;;
+0EC6;LAO KO LA;Lm;0;L;;;;;N;;;;;
+0EC8;LAO TONE MAI EK;Mn;122;NSM;;;;;N;;;;;
+0EC9;LAO TONE MAI THO;Mn;122;NSM;;;;;N;;;;;
+0ECA;LAO TONE MAI TI;Mn;122;NSM;;;;;N;;;;;
+0ECB;LAO TONE MAI CATAWA;Mn;122;NSM;;;;;N;;;;;
+0ECC;LAO CANCELLATION MARK;Mn;0;NSM;;;;;N;;;;;
+0ECD;LAO NIGGAHITA;Mn;0;NSM;;;;;N;;;;;
+0ED0;LAO DIGIT ZERO;Nd;0;L;;0;0;0;N;;;;;
+0ED1;LAO DIGIT ONE;Nd;0;L;;1;1;1;N;;;;;
+0ED2;LAO DIGIT TWO;Nd;0;L;;2;2;2;N;;;;;
+0ED3;LAO DIGIT THREE;Nd;0;L;;3;3;3;N;;;;;
+0ED4;LAO DIGIT FOUR;Nd;0;L;;4;4;4;N;;;;;
+0ED5;LAO DIGIT FIVE;Nd;0;L;;5;5;5;N;;;;;
+0ED6;LAO DIGIT SIX;Nd;0;L;;6;6;6;N;;;;;
+0ED7;LAO DIGIT SEVEN;Nd;0;L;;7;7;7;N;;;;;
+0ED8;LAO DIGIT EIGHT;Nd;0;L;;8;8;8;N;;;;;
+0ED9;LAO DIGIT NINE;Nd;0;L;;9;9;9;N;;;;;
+0EDC;LAO HO NO;Lo;0;L;<compat> 0EAB 0E99;;;;N;;;;;
+0EDD;LAO HO MO;Lo;0;L;<compat> 0EAB 0EA1;;;;N;;;;;
+0EDE;LAO LETTER KHMU GO;Lo;0;L;;;;;N;;;;;
+0EDF;LAO LETTER KHMU NYO;Lo;0;L;;;;;N;;;;;
+0F00;TIBETAN SYLLABLE OM;Lo;0;L;;;;;N;;;;;
+0F01;TIBETAN MARK GTER YIG MGO TRUNCATED A;So;0;L;;;;;N;;;;;
+0F02;TIBETAN MARK GTER YIG MGO -UM RNAM BCAD MA;So;0;L;;;;;N;;;;;
+0F03;TIBETAN MARK GTER YIG MGO -UM GTER TSHEG MA;So;0;L;;;;;N;;;;;
+0F04;TIBETAN MARK INITIAL YIG MGO MDUN MA;Po;0;L;;;;;N;TIBETAN SINGLE ORNAMENT;;;;
+0F05;TIBETAN MARK CLOSING YIG MGO SGAB MA;Po;0;L;;;;;N;;;;;
+0F06;TIBETAN MARK CARET YIG MGO PHUR SHAD MA;Po;0;L;;;;;N;;;;;
+0F07;TIBETAN MARK YIG MGO TSHEG SHAD MA;Po;0;L;;;;;N;;;;;
+0F08;TIBETAN MARK SBRUL SHAD;Po;0;L;;;;;N;TIBETAN RGYANSHAD;;;;
+0F09;TIBETAN MARK BSKUR YIG MGO;Po;0;L;;;;;N;;;;;
+0F0A;TIBETAN MARK BKA- SHOG YIG MGO;Po;0;L;;;;;N;;;;;
+0F0B;TIBETAN MARK INTERSYLLABIC TSHEG;Po;0;L;;;;;N;TIBETAN TSEG;;;;
+0F0C;TIBETAN MARK DELIMITER TSHEG BSTAR;Po;0;L;<noBreak> 0F0B;;;;N;;;;;
+0F0D;TIBETAN MARK SHAD;Po;0;L;;;;;N;TIBETAN SHAD;;;;
+0F0E;TIBETAN MARK NYIS SHAD;Po;0;L;;;;;N;TIBETAN DOUBLE SHAD;;;;
+0F0F;TIBETAN MARK TSHEG SHAD;Po;0;L;;;;;N;;;;;
+0F10;TIBETAN MARK NYIS TSHEG SHAD;Po;0;L;;;;;N;;;;;
+0F11;TIBETAN MARK RIN CHEN SPUNGS SHAD;Po;0;L;;;;;N;TIBETAN RINCHANPHUNGSHAD;;;;
+0F12;TIBETAN MARK RGYA GRAM SHAD;Po;0;L;;;;;N;;;;;
+0F13;TIBETAN MARK CARET -DZUD RTAGS ME LONG CAN;So;0;L;;;;;N;;;;;
+0F14;TIBETAN MARK GTER TSHEG;Po;0;L;;;;;N;TIBETAN COMMA;;;;
+0F15;TIBETAN LOGOTYPE SIGN CHAD RTAGS;So;0;L;;;;;N;;;;;
+0F16;TIBETAN LOGOTYPE SIGN LHAG RTAGS;So;0;L;;;;;N;;;;;
+0F17;TIBETAN ASTROLOGICAL SIGN SGRA GCAN -CHAR RTAGS;So;0;L;;;;;N;;;;;
+0F18;TIBETAN ASTROLOGICAL SIGN -KHYUD PA;Mn;220;NSM;;;;;N;;;;;
+0F19;TIBETAN ASTROLOGICAL SIGN SDONG TSHUGS;Mn;220;NSM;;;;;N;;;;;
+0F1A;TIBETAN SIGN RDEL DKAR GCIG;So;0;L;;;;;N;;;;;
+0F1B;TIBETAN SIGN RDEL DKAR GNYIS;So;0;L;;;;;N;;;;;
+0F1C;TIBETAN SIGN RDEL DKAR GSUM;So;0;L;;;;;N;;;;;
+0F1D;TIBETAN SIGN RDEL NAG GCIG;So;0;L;;;;;N;;;;;
+0F1E;TIBETAN SIGN RDEL NAG GNYIS;So;0;L;;;;;N;;;;;
+0F1F;TIBETAN SIGN RDEL DKAR RDEL NAG;So;0;L;;;;;N;;;;;
+0F20;TIBETAN DIGIT ZERO;Nd;0;L;;0;0;0;N;;;;;
+0F21;TIBETAN DIGIT ONE;Nd;0;L;;1;1;1;N;;;;;
+0F22;TIBETAN DIGIT TWO;Nd;0;L;;2;2;2;N;;;;;
+0F23;TIBETAN DIGIT THREE;Nd;0;L;;3;3;3;N;;;;;
+0F24;TIBETAN DIGIT FOUR;Nd;0;L;;4;4;4;N;;;;;
+0F25;TIBETAN DIGIT FIVE;Nd;0;L;;5;5;5;N;;;;;
+0F26;TIBETAN DIGIT SIX;Nd;0;L;;6;6;6;N;;;;;
+0F27;TIBETAN DIGIT SEVEN;Nd;0;L;;7;7;7;N;;;;;
+0F28;TIBETAN DIGIT EIGHT;Nd;0;L;;8;8;8;N;;;;;
+0F29;TIBETAN DIGIT NINE;Nd;0;L;;9;9;9;N;;;;;
+0F2A;TIBETAN DIGIT HALF ONE;No;0;L;;;;1/2;N;;;;;
+0F2B;TIBETAN DIGIT HALF TWO;No;0;L;;;;3/2;N;;;;;
+0F2C;TIBETAN DIGIT HALF THREE;No;0;L;;;;5/2;N;;;;;
+0F2D;TIBETAN DIGIT HALF FOUR;No;0;L;;;;7/2;N;;;;;
+0F2E;TIBETAN DIGIT HALF FIVE;No;0;L;;;;9/2;N;;;;;
+0F2F;TIBETAN DIGIT HALF SIX;No;0;L;;;;11/2;N;;;;;
+0F30;TIBETAN DIGIT HALF SEVEN;No;0;L;;;;13/2;N;;;;;
+0F31;TIBETAN DIGIT HALF EIGHT;No;0;L;;;;15/2;N;;;;;
+0F32;TIBETAN DIGIT HALF NINE;No;0;L;;;;17/2;N;;;;;
+0F33;TIBETAN DIGIT HALF ZERO;No;0;L;;;;-1/2;N;;;;;
+0F34;TIBETAN MARK BSDUS RTAGS;So;0;L;;;;;N;;;;;
+0F35;TIBETAN MARK NGAS BZUNG NYI ZLA;Mn;220;NSM;;;;;N;TIBETAN HONORIFIC UNDER RING;;;;
+0F36;TIBETAN MARK CARET -DZUD RTAGS BZHI MIG CAN;So;0;L;;;;;N;;;;;
+0F37;TIBETAN MARK NGAS BZUNG SGOR RTAGS;Mn;220;NSM;;;;;N;TIBETAN UNDER RING;;;;
+0F38;TIBETAN MARK CHE MGO;So;0;L;;;;;N;;;;;
+0F39;TIBETAN MARK TSA -PHRU;Mn;216;NSM;;;;;N;TIBETAN LENITION MARK;;;;
+0F3A;TIBETAN MARK GUG RTAGS GYON;Ps;0;ON;;;;;Y;;;;;
+0F3B;TIBETAN MARK GUG RTAGS GYAS;Pe;0;ON;;;;;Y;;;;;
+0F3C;TIBETAN MARK ANG KHANG GYON;Ps;0;ON;;;;;Y;TIBETAN LEFT BRACE;;;;
+0F3D;TIBETAN MARK ANG KHANG GYAS;Pe;0;ON;;;;;Y;TIBETAN RIGHT BRACE;;;;
+0F3E;TIBETAN SIGN YAR TSHES;Mc;0;L;;;;;N;;;;;
+0F3F;TIBETAN SIGN MAR TSHES;Mc;0;L;;;;;N;;;;;
+0F40;TIBETAN LETTER KA;Lo;0;L;;;;;N;;;;;
+0F41;TIBETAN LETTER KHA;Lo;0;L;;;;;N;;;;;
+0F42;TIBETAN LETTER GA;Lo;0;L;;;;;N;;;;;
+0F43;TIBETAN LETTER GHA;Lo;0;L;0F42 0FB7;;;;N;;;;;
+0F44;TIBETAN LETTER NGA;Lo;0;L;;;;;N;;;;;
+0F45;TIBETAN LETTER CA;Lo;0;L;;;;;N;;;;;
+0F46;TIBETAN LETTER CHA;Lo;0;L;;;;;N;;;;;
+0F47;TIBETAN LETTER JA;Lo;0;L;;;;;N;;;;;
+0F49;TIBETAN LETTER NYA;Lo;0;L;;;;;N;;;;;
+0F4A;TIBETAN LETTER TTA;Lo;0;L;;;;;N;TIBETAN LETTER REVERSED TA;;;;
+0F4B;TIBETAN LETTER TTHA;Lo;0;L;;;;;N;TIBETAN LETTER REVERSED THA;;;;
+0F4C;TIBETAN LETTER DDA;Lo;0;L;;;;;N;TIBETAN LETTER REVERSED DA;;;;
+0F4D;TIBETAN LETTER DDHA;Lo;0;L;0F4C 0FB7;;;;N;;;;;
+0F4E;TIBETAN LETTER NNA;Lo;0;L;;;;;N;TIBETAN LETTER REVERSED NA;;;;
+0F4F;TIBETAN LETTER TA;Lo;0;L;;;;;N;;;;;
+0F50;TIBETAN LETTER THA;Lo;0;L;;;;;N;;;;;
+0F51;TIBETAN LETTER DA;Lo;0;L;;;;;N;;;;;
+0F52;TIBETAN LETTER DHA;Lo;0;L;0F51 0FB7;;;;N;;;;;
+0F53;TIBETAN LETTER NA;Lo;0;L;;;;;N;;;;;
+0F54;TIBETAN LETTER PA;Lo;0;L;;;;;N;;;;;
+0F55;TIBETAN LETTER PHA;Lo;0;L;;;;;N;;;;;
+0F56;TIBETAN LETTER BA;Lo;0;L;;;;;N;;;;;
+0F57;TIBETAN LETTER BHA;Lo;0;L;0F56 0FB7;;;;N;;;;;
+0F58;TIBETAN LETTER MA;Lo;0;L;;;;;N;;;;;
+0F59;TIBETAN LETTER TSA;Lo;0;L;;;;;N;;;;;
+0F5A;TIBETAN LETTER TSHA;Lo;0;L;;;;;N;;;;;
+0F5B;TIBETAN LETTER DZA;Lo;0;L;;;;;N;;;;;
+0F5C;TIBETAN LETTER DZHA;Lo;0;L;0F5B 0FB7;;;;N;;;;;
+0F5D;TIBETAN LETTER WA;Lo;0;L;;;;;N;;;;;
+0F5E;TIBETAN LETTER ZHA;Lo;0;L;;;;;N;;;;;
+0F5F;TIBETAN LETTER ZA;Lo;0;L;;;;;N;;;;;
+0F60;TIBETAN LETTER -A;Lo;0;L;;;;;N;TIBETAN LETTER AA;;;;
+0F61;TIBETAN LETTER YA;Lo;0;L;;;;;N;;;;;
+0F62;TIBETAN LETTER RA;Lo;0;L;;;;;N;;;;;
+0F63;TIBETAN LETTER LA;Lo;0;L;;;;;N;;;;;
+0F64;TIBETAN LETTER SHA;Lo;0;L;;;;;N;;;;;
+0F65;TIBETAN LETTER SSA;Lo;0;L;;;;;N;TIBETAN LETTER REVERSED SHA;;;;
+0F66;TIBETAN LETTER SA;Lo;0;L;;;;;N;;;;;
+0F67;TIBETAN LETTER HA;Lo;0;L;;;;;N;;;;;
+0F68;TIBETAN LETTER A;Lo;0;L;;;;;N;;;;;
+0F69;TIBETAN LETTER KSSA;Lo;0;L;0F40 0FB5;;;;N;;;;;
+0F6A;TIBETAN LETTER FIXED-FORM RA;Lo;0;L;;;;;N;;;;;
+0F6B;TIBETAN LETTER KKA;Lo;0;L;;;;;N;;;;;
+0F6C;TIBETAN LETTER RRA;Lo;0;L;;;;;N;;;;;
+0F71;TIBETAN VOWEL SIGN AA;Mn;129;NSM;;;;;N;;;;;
+0F72;TIBETAN VOWEL SIGN I;Mn;130;NSM;;;;;N;;;;;
+0F73;TIBETAN VOWEL SIGN II;Mn;0;NSM;0F71 0F72;;;;N;;;;;
+0F74;TIBETAN VOWEL SIGN U;Mn;132;NSM;;;;;N;;;;;
+0F75;TIBETAN VOWEL SIGN UU;Mn;0;NSM;0F71 0F74;;;;N;;;;;
+0F76;TIBETAN VOWEL SIGN VOCALIC R;Mn;0;NSM;0FB2 0F80;;;;N;;;;;
+0F77;TIBETAN VOWEL SIGN VOCALIC RR;Mn;0;NSM;<compat> 0FB2 0F81;;;;N;;;;;
+0F78;TIBETAN VOWEL SIGN VOCALIC L;Mn;0;NSM;0FB3 0F80;;;;N;;;;;
+0F79;TIBETAN VOWEL SIGN VOCALIC LL;Mn;0;NSM;<compat> 0FB3 0F81;;;;N;;;;;
+0F7A;TIBETAN VOWEL SIGN E;Mn;130;NSM;;;;;N;;;;;
+0F7B;TIBETAN VOWEL SIGN EE;Mn;130;NSM;;;;;N;TIBETAN VOWEL SIGN AI;;;;
+0F7C;TIBETAN VOWEL SIGN O;Mn;130;NSM;;;;;N;;;;;
+0F7D;TIBETAN VOWEL SIGN OO;Mn;130;NSM;;;;;N;TIBETAN VOWEL SIGN AU;;;;
+0F7E;TIBETAN SIGN RJES SU NGA RO;Mn;0;NSM;;;;;N;TIBETAN ANUSVARA;;;;
+0F7F;TIBETAN SIGN RNAM BCAD;Mc;0;L;;;;;N;TIBETAN VISARGA;;;;
+0F80;TIBETAN VOWEL SIGN REVERSED I;Mn;130;NSM;;;;;N;TIBETAN VOWEL SIGN SHORT I;;;;
+0F81;TIBETAN VOWEL SIGN REVERSED II;Mn;0;NSM;0F71 0F80;;;;N;;;;;
+0F82;TIBETAN SIGN NYI ZLA NAA DA;Mn;230;NSM;;;;;N;TIBETAN CANDRABINDU WITH ORNAMENT;;;;
+0F83;TIBETAN SIGN SNA LDAN;Mn;230;NSM;;;;;N;TIBETAN CANDRABINDU;;;;
+0F84;TIBETAN MARK HALANTA;Mn;9;NSM;;;;;N;TIBETAN VIRAMA;;;;
+0F85;TIBETAN MARK PALUTA;Po;0;L;;;;;N;TIBETAN CHUCHENYIGE;;;;
+0F86;TIBETAN SIGN LCI RTAGS;Mn;230;NSM;;;;;N;;;;;
+0F87;TIBETAN SIGN YANG RTAGS;Mn;230;NSM;;;;;N;;;;;
+0F88;TIBETAN SIGN LCE TSA CAN;Lo;0;L;;;;;N;;;;;
+0F89;TIBETAN SIGN MCHU CAN;Lo;0;L;;;;;N;;;;;
+0F8A;TIBETAN SIGN GRU CAN RGYINGS;Lo;0;L;;;;;N;;;;;
+0F8B;TIBETAN SIGN GRU MED RGYINGS;Lo;0;L;;;;;N;;;;;
+0F8C;TIBETAN SIGN INVERTED MCHU CAN;Lo;0;L;;;;;N;;;;;
+0F8D;TIBETAN SUBJOINED SIGN LCE TSA CAN;Mn;0;NSM;;;;;N;;;;;
+0F8E;TIBETAN SUBJOINED SIGN MCHU CAN;Mn;0;NSM;;;;;N;;;;;
+0F8F;TIBETAN SUBJOINED SIGN INVERTED MCHU CAN;Mn;0;NSM;;;;;N;;;;;
+0F90;TIBETAN SUBJOINED LETTER KA;Mn;0;NSM;;;;;N;;;;;
+0F91;TIBETAN SUBJOINED LETTER KHA;Mn;0;NSM;;;;;N;;;;;
+0F92;TIBETAN SUBJOINED LETTER GA;Mn;0;NSM;;;;;N;;;;;
+0F93;TIBETAN SUBJOINED LETTER GHA;Mn;0;NSM;0F92 0FB7;;;;N;;;;;
+0F94;TIBETAN SUBJOINED LETTER NGA;Mn;0;NSM;;;;;N;;;;;
+0F95;TIBETAN SUBJOINED LETTER CA;Mn;0;NSM;;;;;N;;;;;
+0F96;TIBETAN SUBJOINED LETTER CHA;Mn;0;NSM;;;;;N;;;;;
+0F97;TIBETAN SUBJOINED LETTER JA;Mn;0;NSM;;;;;N;;;;;
+0F99;TIBETAN SUBJOINED LETTER NYA;Mn;0;NSM;;;;;N;;;;;
+0F9A;TIBETAN SUBJOINED LETTER TTA;Mn;0;NSM;;;;;N;;;;;
+0F9B;TIBETAN SUBJOINED LETTER TTHA;Mn;0;NSM;;;;;N;;;;;
+0F9C;TIBETAN SUBJOINED LETTER DDA;Mn;0;NSM;;;;;N;;;;;
+0F9D;TIBETAN SUBJOINED LETTER DDHA;Mn;0;NSM;0F9C 0FB7;;;;N;;;;;
+0F9E;TIBETAN SUBJOINED LETTER NNA;Mn;0;NSM;;;;;N;;;;;
+0F9F;TIBETAN SUBJOINED LETTER TA;Mn;0;NSM;;;;;N;;;;;
+0FA0;TIBETAN SUBJOINED LETTER THA;Mn;0;NSM;;;;;N;;;;;
+0FA1;TIBETAN SUBJOINED LETTER DA;Mn;0;NSM;;;;;N;;;;;
+0FA2;TIBETAN SUBJOINED LETTER DHA;Mn;0;NSM;0FA1 0FB7;;;;N;;;;;
+0FA3;TIBETAN SUBJOINED LETTER NA;Mn;0;NSM;;;;;N;;;;;
+0FA4;TIBETAN SUBJOINED LETTER PA;Mn;0;NSM;;;;;N;;;;;
+0FA5;TIBETAN SUBJOINED LETTER PHA;Mn;0;NSM;;;;;N;;;;;
+0FA6;TIBETAN SUBJOINED LETTER BA;Mn;0;NSM;;;;;N;;;;;
+0FA7;TIBETAN SUBJOINED LETTER BHA;Mn;0;NSM;0FA6 0FB7;;;;N;;;;;
+0FA8;TIBETAN SUBJOINED LETTER MA;Mn;0;NSM;;;;;N;;;;;
+0FA9;TIBETAN SUBJOINED LETTER TSA;Mn;0;NSM;;;;;N;;;;;
+0FAA;TIBETAN SUBJOINED LETTER TSHA;Mn;0;NSM;;;;;N;;;;;
+0FAB;TIBETAN SUBJOINED LETTER DZA;Mn;0;NSM;;;;;N;;;;;
+0FAC;TIBETAN SUBJOINED LETTER DZHA;Mn;0;NSM;0FAB 0FB7;;;;N;;;;;
+0FAD;TIBETAN SUBJOINED LETTER WA;Mn;0;NSM;;;;;N;;;;;
+0FAE;TIBETAN SUBJOINED LETTER ZHA;Mn;0;NSM;;;;;N;;;;;
+0FAF;TIBETAN SUBJOINED LETTER ZA;Mn;0;NSM;;;;;N;;;;;
+0FB0;TIBETAN SUBJOINED LETTER -A;Mn;0;NSM;;;;;N;;;;;
+0FB1;TIBETAN SUBJOINED LETTER YA;Mn;0;NSM;;;;;N;;;;;
+0FB2;TIBETAN SUBJOINED LETTER RA;Mn;0;NSM;;;;;N;;;;;
+0FB3;TIBETAN SUBJOINED LETTER LA;Mn;0;NSM;;;;;N;;;;;
+0FB4;TIBETAN SUBJOINED LETTER SHA;Mn;0;NSM;;;;;N;;;;;
+0FB5;TIBETAN SUBJOINED LETTER SSA;Mn;0;NSM;;;;;N;;;;;
+0FB6;TIBETAN SUBJOINED LETTER SA;Mn;0;NSM;;;;;N;;;;;
+0FB7;TIBETAN SUBJOINED LETTER HA;Mn;0;NSM;;;;;N;;;;;
+0FB8;TIBETAN SUBJOINED LETTER A;Mn;0;NSM;;;;;N;;;;;
+0FB9;TIBETAN SUBJOINED LETTER KSSA;Mn;0;NSM;0F90 0FB5;;;;N;;;;;
+0FBA;TIBETAN SUBJOINED LETTER FIXED-FORM WA;Mn;0;NSM;;;;;N;;;;;
+0FBB;TIBETAN SUBJOINED LETTER FIXED-FORM YA;Mn;0;NSM;;;;;N;;;;;
+0FBC;TIBETAN SUBJOINED LETTER FIXED-FORM RA;Mn;0;NSM;;;;;N;;;;;
+0FBE;TIBETAN KU RU KHA;So;0;L;;;;;N;;;;;
+0FBF;TIBETAN KU RU KHA BZHI MIG CAN;So;0;L;;;;;N;;;;;
+0FC0;TIBETAN CANTILLATION SIGN HEAVY BEAT;So;0;L;;;;;N;;;;;
+0FC1;TIBETAN CANTILLATION SIGN LIGHT BEAT;So;0;L;;;;;N;;;;;
+0FC2;TIBETAN CANTILLATION SIGN CANG TE-U;So;0;L;;;;;N;;;;;
+0FC3;TIBETAN CANTILLATION SIGN SBUB -CHAL;So;0;L;;;;;N;;;;;
+0FC4;TIBETAN SYMBOL DRIL BU;So;0;L;;;;;N;;;;;
+0FC5;TIBETAN SYMBOL RDO RJE;So;0;L;;;;;N;;;;;
+0FC6;TIBETAN SYMBOL PADMA GDAN;Mn;220;NSM;;;;;N;;;;;
+0FC7;TIBETAN SYMBOL RDO RJE RGYA GRAM;So;0;L;;;;;N;;;;;
+0FC8;TIBETAN SYMBOL PHUR PA;So;0;L;;;;;N;;;;;
+0FC9;TIBETAN SYMBOL NOR BU;So;0;L;;;;;N;;;;;
+0FCA;TIBETAN SYMBOL NOR BU NYIS -KHYIL;So;0;L;;;;;N;;;;;
+0FCB;TIBETAN SYMBOL NOR BU GSUM -KHYIL;So;0;L;;;;;N;;;;;
+0FCC;TIBETAN SYMBOL NOR BU BZHI -KHYIL;So;0;L;;;;;N;;;;;
+0FCE;TIBETAN SIGN RDEL NAG RDEL DKAR;So;0;L;;;;;N;;;;;
+0FCF;TIBETAN SIGN RDEL NAG GSUM;So;0;L;;;;;N;;;;;
+0FD0;TIBETAN MARK BSKA- SHOG GI MGO RGYAN;Po;0;L;;;;;N;;;;;
+0FD1;TIBETAN MARK MNYAM YIG GI MGO RGYAN;Po;0;L;;;;;N;;;;;
+0FD2;TIBETAN MARK NYIS TSHEG;Po;0;L;;;;;N;;;;;
+0FD3;TIBETAN MARK INITIAL BRDA RNYING YIG MGO MDUN MA;Po;0;L;;;;;N;;;;;
+0FD4;TIBETAN MARK CLOSING BRDA RNYING YIG MGO SGAB MA;Po;0;L;;;;;N;;;;;
+0FD5;RIGHT-FACING SVASTI SIGN;So;0;L;;;;;N;;;;;
+0FD6;LEFT-FACING SVASTI SIGN;So;0;L;;;;;N;;;;;
+0FD7;RIGHT-FACING SVASTI SIGN WITH DOTS;So;0;L;;;;;N;;;;;
+0FD8;LEFT-FACING SVASTI SIGN WITH DOTS;So;0;L;;;;;N;;;;;
+0FD9;TIBETAN MARK LEADING MCHAN RTAGS;Po;0;L;;;;;N;;;;;
+0FDA;TIBETAN MARK TRAILING MCHAN RTAGS;Po;0;L;;;;;N;;;;;
+1000;MYANMAR LETTER KA;Lo;0;L;;;;;N;;;;;
+1001;MYANMAR LETTER KHA;Lo;0;L;;;;;N;;;;;
+1002;MYANMAR LETTER GA;Lo;0;L;;;;;N;;;;;
+1003;MYANMAR LETTER GHA;Lo;0;L;;;;;N;;;;;
+1004;MYANMAR LETTER NGA;Lo;0;L;;;;;N;;;;;
+1005;MYANMAR LETTER CA;Lo;0;L;;;;;N;;;;;
+1006;MYANMAR LETTER CHA;Lo;0;L;;;;;N;;;;;
+1007;MYANMAR LETTER JA;Lo;0;L;;;;;N;;;;;
+1008;MYANMAR LETTER JHA;Lo;0;L;;;;;N;;;;;
+1009;MYANMAR LETTER NYA;Lo;0;L;;;;;N;;;;;
+100A;MYANMAR LETTER NNYA;Lo;0;L;;;;;N;;;;;
+100B;MYANMAR LETTER TTA;Lo;0;L;;;;;N;;;;;
+100C;MYANMAR LETTER TTHA;Lo;0;L;;;;;N;;;;;
+100D;MYANMAR LETTER DDA;Lo;0;L;;;;;N;;;;;
+100E;MYANMAR LETTER DDHA;Lo;0;L;;;;;N;;;;;
+100F;MYANMAR LETTER NNA;Lo;0;L;;;;;N;;;;;
+1010;MYANMAR LETTER TA;Lo;0;L;;;;;N;;;;;
+1011;MYANMAR LETTER THA;Lo;0;L;;;;;N;;;;;
+1012;MYANMAR LETTER DA;Lo;0;L;;;;;N;;;;;
+1013;MYANMAR LETTER DHA;Lo;0;L;;;;;N;;;;;
+1014;MYANMAR LETTER NA;Lo;0;L;;;;;N;;;;;
+1015;MYANMAR LETTER PA;Lo;0;L;;;;;N;;;;;
+1016;MYANMAR LETTER PHA;Lo;0;L;;;;;N;;;;;
+1017;MYANMAR LETTER BA;Lo;0;L;;;;;N;;;;;
+1018;MYANMAR LETTER BHA;Lo;0;L;;;;;N;;;;;
+1019;MYANMAR LETTER MA;Lo;0;L;;;;;N;;;;;
+101A;MYANMAR LETTER YA;Lo;0;L;;;;;N;;;;;
+101B;MYANMAR LETTER RA;Lo;0;L;;;;;N;;;;;
+101C;MYANMAR LETTER LA;Lo;0;L;;;;;N;;;;;
+101D;MYANMAR LETTER WA;Lo;0;L;;;;;N;;;;;
+101E;MYANMAR LETTER SA;Lo;0;L;;;;;N;;;;;
+101F;MYANMAR LETTER HA;Lo;0;L;;;;;N;;;;;
+1020;MYANMAR LETTER LLA;Lo;0;L;;;;;N;;;;;
+1021;MYANMAR LETTER A;Lo;0;L;;;;;N;;;;;
+1022;MYANMAR LETTER SHAN A;Lo;0;L;;;;;N;;;;;
+1023;MYANMAR LETTER I;Lo;0;L;;;;;N;;;;;
+1024;MYANMAR LETTER II;Lo;0;L;;;;;N;;;;;
+1025;MYANMAR LETTER U;Lo;0;L;;;;;N;;;;;
+1026;MYANMAR LETTER UU;Lo;0;L;1025 102E;;;;N;;;;;
+1027;MYANMAR LETTER E;Lo;0;L;;;;;N;;;;;
+1028;MYANMAR LETTER MON E;Lo;0;L;;;;;N;;;;;
+1029;MYANMAR LETTER O;Lo;0;L;;;;;N;;;;;
+102A;MYANMAR LETTER AU;Lo;0;L;;;;;N;;;;;
+102B;MYANMAR VOWEL SIGN TALL AA;Mc;0;L;;;;;N;;;;;
+102C;MYANMAR VOWEL SIGN AA;Mc;0;L;;;;;N;;;;;
+102D;MYANMAR VOWEL SIGN I;Mn;0;NSM;;;;;N;;;;;
+102E;MYANMAR VOWEL SIGN II;Mn;0;NSM;;;;;N;;;;;
+102F;MYANMAR VOWEL SIGN U;Mn;0;NSM;;;;;N;;;;;
+1030;MYANMAR VOWEL SIGN UU;Mn;0;NSM;;;;;N;;;;;
+1031;MYANMAR VOWEL SIGN E;Mc;0;L;;;;;N;;;;;
+1032;MYANMAR VOWEL SIGN AI;Mn;0;NSM;;;;;N;;;;;
+1033;MYANMAR VOWEL SIGN MON II;Mn;0;NSM;;;;;N;;;;;
+1034;MYANMAR VOWEL SIGN MON O;Mn;0;NSM;;;;;N;;;;;
+1035;MYANMAR VOWEL SIGN E ABOVE;Mn;0;NSM;;;;;N;;;;;
+1036;MYANMAR SIGN ANUSVARA;Mn;0;NSM;;;;;N;;;;;
+1037;MYANMAR SIGN DOT BELOW;Mn;7;NSM;;;;;N;;;;;
+1038;MYANMAR SIGN VISARGA;Mc;0;L;;;;;N;;;;;
+1039;MYANMAR SIGN VIRAMA;Mn;9;NSM;;;;;N;;;;;
+103A;MYANMAR SIGN ASAT;Mn;9;NSM;;;;;N;;;;;
+103B;MYANMAR CONSONANT SIGN MEDIAL YA;Mc;0;L;;;;;N;;;;;
+103C;MYANMAR CONSONANT SIGN MEDIAL RA;Mc;0;L;;;;;N;;;;;
+103D;MYANMAR CONSONANT SIGN MEDIAL WA;Mn;0;NSM;;;;;N;;;;;
+103E;MYANMAR CONSONANT SIGN MEDIAL HA;Mn;0;NSM;;;;;N;;;;;
+103F;MYANMAR LETTER GREAT SA;Lo;0;L;;;;;N;;;;;
+1040;MYANMAR DIGIT ZERO;Nd;0;L;;0;0;0;N;;;;;
+1041;MYANMAR DIGIT ONE;Nd;0;L;;1;1;1;N;;;;;
+1042;MYANMAR DIGIT TWO;Nd;0;L;;2;2;2;N;;;;;
+1043;MYANMAR DIGIT THREE;Nd;0;L;;3;3;3;N;;;;;
+1044;MYANMAR DIGIT FOUR;Nd;0;L;;4;4;4;N;;;;;
+1045;MYANMAR DIGIT FIVE;Nd;0;L;;5;5;5;N;;;;;
+1046;MYANMAR DIGIT SIX;Nd;0;L;;6;6;6;N;;;;;
+1047;MYANMAR DIGIT SEVEN;Nd;0;L;;7;7;7;N;;;;;
+1048;MYANMAR DIGIT EIGHT;Nd;0;L;;8;8;8;N;;;;;
+1049;MYANMAR DIGIT NINE;Nd;0;L;;9;9;9;N;;;;;
+104A;MYANMAR SIGN LITTLE SECTION;Po;0;L;;;;;N;;;;;
+104B;MYANMAR SIGN SECTION;Po;0;L;;;;;N;;;;;
+104C;MYANMAR SYMBOL LOCATIVE;Po;0;L;;;;;N;;;;;
+104D;MYANMAR SYMBOL COMPLETED;Po;0;L;;;;;N;;;;;
+104E;MYANMAR SYMBOL AFOREMENTIONED;Po;0;L;;;;;N;;;;;
+104F;MYANMAR SYMBOL GENITIVE;Po;0;L;;;;;N;;;;;
+1050;MYANMAR LETTER SHA;Lo;0;L;;;;;N;;;;;
+1051;MYANMAR LETTER SSA;Lo;0;L;;;;;N;;;;;
+1052;MYANMAR LETTER VOCALIC R;Lo;0;L;;;;;N;;;;;
+1053;MYANMAR LETTER VOCALIC RR;Lo;0;L;;;;;N;;;;;
+1054;MYANMAR LETTER VOCALIC L;Lo;0;L;;;;;N;;;;;
+1055;MYANMAR LETTER VOCALIC LL;Lo;0;L;;;;;N;;;;;
+1056;MYANMAR VOWEL SIGN VOCALIC R;Mc;0;L;;;;;N;;;;;
+1057;MYANMAR VOWEL SIGN VOCALIC RR;Mc;0;L;;;;;N;;;;;
+1058;MYANMAR VOWEL SIGN VOCALIC L;Mn;0;NSM;;;;;N;;;;;
+1059;MYANMAR VOWEL SIGN VOCALIC LL;Mn;0;NSM;;;;;N;;;;;
+105A;MYANMAR LETTER MON NGA;Lo;0;L;;;;;N;;;;;
+105B;MYANMAR LETTER MON JHA;Lo;0;L;;;;;N;;;;;
+105C;MYANMAR LETTER MON BBA;Lo;0;L;;;;;N;;;;;
+105D;MYANMAR LETTER MON BBE;Lo;0;L;;;;;N;;;;;
+105E;MYANMAR CONSONANT SIGN MON MEDIAL NA;Mn;0;NSM;;;;;N;;;;;
+105F;MYANMAR CONSONANT SIGN MON MEDIAL MA;Mn;0;NSM;;;;;N;;;;;
+1060;MYANMAR CONSONANT SIGN MON MEDIAL LA;Mn;0;NSM;;;;;N;;;;;
+1061;MYANMAR LETTER SGAW KAREN SHA;Lo;0;L;;;;;N;;;;;
+1062;MYANMAR VOWEL SIGN SGAW KAREN EU;Mc;0;L;;;;;N;;;;;
+1063;MYANMAR TONE MARK SGAW KAREN HATHI;Mc;0;L;;;;;N;;;;;
+1064;MYANMAR TONE MARK SGAW KAREN KE PHO;Mc;0;L;;;;;N;;;;;
+1065;MYANMAR LETTER WESTERN PWO KAREN THA;Lo;0;L;;;;;N;;;;;
+1066;MYANMAR LETTER WESTERN PWO KAREN PWA;Lo;0;L;;;;;N;;;;;
+1067;MYANMAR VOWEL SIGN WESTERN PWO KAREN EU;Mc;0;L;;;;;N;;;;;
+1068;MYANMAR VOWEL SIGN WESTERN PWO KAREN UE;Mc;0;L;;;;;N;;;;;
+1069;MYANMAR SIGN WESTERN PWO KAREN TONE-1;Mc;0;L;;;;;N;;;;;
+106A;MYANMAR SIGN WESTERN PWO KAREN TONE-2;Mc;0;L;;;;;N;;;;;
+106B;MYANMAR SIGN WESTERN PWO KAREN TONE-3;Mc;0;L;;;;;N;;;;;
+106C;MYANMAR SIGN WESTERN PWO KAREN TONE-4;Mc;0;L;;;;;N;;;;;
+106D;MYANMAR SIGN WESTERN PWO KAREN TONE-5;Mc;0;L;;;;;N;;;;;
+106E;MYANMAR LETTER EASTERN PWO KAREN NNA;Lo;0;L;;;;;N;;;;;
+106F;MYANMAR LETTER EASTERN PWO KAREN YWA;Lo;0;L;;;;;N;;;;;
+1070;MYANMAR LETTER EASTERN PWO KAREN GHWA;Lo;0;L;;;;;N;;;;;
+1071;MYANMAR VOWEL SIGN GEBA KAREN I;Mn;0;NSM;;;;;N;;;;;
+1072;MYANMAR VOWEL SIGN KAYAH OE;Mn;0;NSM;;;;;N;;;;;
+1073;MYANMAR VOWEL SIGN KAYAH U;Mn;0;NSM;;;;;N;;;;;
+1074;MYANMAR VOWEL SIGN KAYAH EE;Mn;0;NSM;;;;;N;;;;;
+1075;MYANMAR LETTER SHAN KA;Lo;0;L;;;;;N;;;;;
+1076;MYANMAR LETTER SHAN KHA;Lo;0;L;;;;;N;;;;;
+1077;MYANMAR LETTER SHAN GA;Lo;0;L;;;;;N;;;;;
+1078;MYANMAR LETTER SHAN CA;Lo;0;L;;;;;N;;;;;
+1079;MYANMAR LETTER SHAN ZA;Lo;0;L;;;;;N;;;;;
+107A;MYANMAR LETTER SHAN NYA;Lo;0;L;;;;;N;;;;;
+107B;MYANMAR LETTER SHAN DA;Lo;0;L;;;;;N;;;;;
+107C;MYANMAR LETTER SHAN NA;Lo;0;L;;;;;N;;;;;
+107D;MYANMAR LETTER SHAN PHA;Lo;0;L;;;;;N;;;;;
+107E;MYANMAR LETTER SHAN FA;Lo;0;L;;;;;N;;;;;
+107F;MYANMAR LETTER SHAN BA;Lo;0;L;;;;;N;;;;;
+1080;MYANMAR LETTER SHAN THA;Lo;0;L;;;;;N;;;;;
+1081;MYANMAR LETTER SHAN HA;Lo;0;L;;;;;N;;;;;
+1082;MYANMAR CONSONANT SIGN SHAN MEDIAL WA;Mn;0;NSM;;;;;N;;;;;
+1083;MYANMAR VOWEL SIGN SHAN AA;Mc;0;L;;;;;N;;;;;
+1084;MYANMAR VOWEL SIGN SHAN E;Mc;0;L;;;;;N;;;;;
+1085;MYANMAR VOWEL SIGN SHAN E ABOVE;Mn;0;NSM;;;;;N;;;;;
+1086;MYANMAR VOWEL SIGN SHAN FINAL Y;Mn;0;NSM;;;;;N;;;;;
+1087;MYANMAR SIGN SHAN TONE-2;Mc;0;L;;;;;N;;;;;
+1088;MYANMAR SIGN SHAN TONE-3;Mc;0;L;;;;;N;;;;;
+1089;MYANMAR SIGN SHAN TONE-5;Mc;0;L;;;;;N;;;;;
+108A;MYANMAR SIGN SHAN TONE-6;Mc;0;L;;;;;N;;;;;
+108B;MYANMAR SIGN SHAN COUNCIL TONE-2;Mc;0;L;;;;;N;;;;;
+108C;MYANMAR SIGN SHAN COUNCIL TONE-3;Mc;0;L;;;;;N;;;;;
+108D;MYANMAR SIGN SHAN COUNCIL EMPHATIC TONE;Mn;220;NSM;;;;;N;;;;;
+108E;MYANMAR LETTER RUMAI PALAUNG FA;Lo;0;L;;;;;N;;;;;
+108F;MYANMAR SIGN RUMAI PALAUNG TONE-5;Mc;0;L;;;;;N;;;;;
+1090;MYANMAR SHAN DIGIT ZERO;Nd;0;L;;0;0;0;N;;;;;
+1091;MYANMAR SHAN DIGIT ONE;Nd;0;L;;1;1;1;N;;;;;
+1092;MYANMAR SHAN DIGIT TWO;Nd;0;L;;2;2;2;N;;;;;
+1093;MYANMAR SHAN DIGIT THREE;Nd;0;L;;3;3;3;N;;;;;
+1094;MYANMAR SHAN DIGIT FOUR;Nd;0;L;;4;4;4;N;;;;;
+1095;MYANMAR SHAN DIGIT FIVE;Nd;0;L;;5;5;5;N;;;;;
+1096;MYANMAR SHAN DIGIT SIX;Nd;0;L;;6;6;6;N;;;;;
+1097;MYANMAR SHAN DIGIT SEVEN;Nd;0;L;;7;7;7;N;;;;;
+1098;MYANMAR SHAN DIGIT EIGHT;Nd;0;L;;8;8;8;N;;;;;
+1099;MYANMAR SHAN DIGIT NINE;Nd;0;L;;9;9;9;N;;;;;
+109A;MYANMAR SIGN KHAMTI TONE-1;Mc;0;L;;;;;N;;;;;
+109B;MYANMAR SIGN KHAMTI TONE-3;Mc;0;L;;;;;N;;;;;
+109C;MYANMAR VOWEL SIGN AITON A;Mc;0;L;;;;;N;;;;;
+109D;MYANMAR VOWEL SIGN AITON AI;Mn;0;NSM;;;;;N;;;;;
+109E;MYANMAR SYMBOL SHAN ONE;So;0;L;;;;;N;;;;;
+109F;MYANMAR SYMBOL SHAN EXCLAMATION;So;0;L;;;;;N;;;;;
+10A0;GEORGIAN CAPITAL LETTER AN;Lu;0;L;;;;;N;;;;2D00;
+10A1;GEORGIAN CAPITAL LETTER BAN;Lu;0;L;;;;;N;;;;2D01;
+10A2;GEORGIAN CAPITAL LETTER GAN;Lu;0;L;;;;;N;;;;2D02;
+10A3;GEORGIAN CAPITAL LETTER DON;Lu;0;L;;;;;N;;;;2D03;
+10A4;GEORGIAN CAPITAL LETTER EN;Lu;0;L;;;;;N;;;;2D04;
+10A5;GEORGIAN CAPITAL LETTER VIN;Lu;0;L;;;;;N;;;;2D05;
+10A6;GEORGIAN CAPITAL LETTER ZEN;Lu;0;L;;;;;N;;;;2D06;
+10A7;GEORGIAN CAPITAL LETTER TAN;Lu;0;L;;;;;N;;;;2D07;
+10A8;GEORGIAN CAPITAL LETTER IN;Lu;0;L;;;;;N;;;;2D08;
+10A9;GEORGIAN CAPITAL LETTER KAN;Lu;0;L;;;;;N;;;;2D09;
+10AA;GEORGIAN CAPITAL LETTER LAS;Lu;0;L;;;;;N;;;;2D0A;
+10AB;GEORGIAN CAPITAL LETTER MAN;Lu;0;L;;;;;N;;;;2D0B;
+10AC;GEORGIAN CAPITAL LETTER NAR;Lu;0;L;;;;;N;;;;2D0C;
+10AD;GEORGIAN CAPITAL LETTER ON;Lu;0;L;;;;;N;;;;2D0D;
+10AE;GEORGIAN CAPITAL LETTER PAR;Lu;0;L;;;;;N;;;;2D0E;
+10AF;GEORGIAN CAPITAL LETTER ZHAR;Lu;0;L;;;;;N;;;;2D0F;
+10B0;GEORGIAN CAPITAL LETTER RAE;Lu;0;L;;;;;N;;;;2D10;
+10B1;GEORGIAN CAPITAL LETTER SAN;Lu;0;L;;;;;N;;;;2D11;
+10B2;GEORGIAN CAPITAL LETTER TAR;Lu;0;L;;;;;N;;;;2D12;
+10B3;GEORGIAN CAPITAL LETTER UN;Lu;0;L;;;;;N;;;;2D13;
+10B4;GEORGIAN CAPITAL LETTER PHAR;Lu;0;L;;;;;N;;;;2D14;
+10B5;GEORGIAN CAPITAL LETTER KHAR;Lu;0;L;;;;;N;;;;2D15;
+10B6;GEORGIAN CAPITAL LETTER GHAN;Lu;0;L;;;;;N;;;;2D16;
+10B7;GEORGIAN CAPITAL LETTER QAR;Lu;0;L;;;;;N;;;;2D17;
+10B8;GEORGIAN CAPITAL LETTER SHIN;Lu;0;L;;;;;N;;;;2D18;
+10B9;GEORGIAN CAPITAL LETTER CHIN;Lu;0;L;;;;;N;;;;2D19;
+10BA;GEORGIAN CAPITAL LETTER CAN;Lu;0;L;;;;;N;;;;2D1A;
+10BB;GEORGIAN CAPITAL LETTER JIL;Lu;0;L;;;;;N;;;;2D1B;
+10BC;GEORGIAN CAPITAL LETTER CIL;Lu;0;L;;;;;N;;;;2D1C;
+10BD;GEORGIAN CAPITAL LETTER CHAR;Lu;0;L;;;;;N;;;;2D1D;
+10BE;GEORGIAN CAPITAL LETTER XAN;Lu;0;L;;;;;N;;;;2D1E;
+10BF;GEORGIAN CAPITAL LETTER JHAN;Lu;0;L;;;;;N;;;;2D1F;
+10C0;GEORGIAN CAPITAL LETTER HAE;Lu;0;L;;;;;N;;;;2D20;
+10C1;GEORGIAN CAPITAL LETTER HE;Lu;0;L;;;;;N;;;;2D21;
+10C2;GEORGIAN CAPITAL LETTER HIE;Lu;0;L;;;;;N;;;;2D22;
+10C3;GEORGIAN CAPITAL LETTER WE;Lu;0;L;;;;;N;;;;2D23;
+10C4;GEORGIAN CAPITAL LETTER HAR;Lu;0;L;;;;;N;;;;2D24;
+10C5;GEORGIAN CAPITAL LETTER HOE;Lu;0;L;;;;;N;;;;2D25;
+10C7;GEORGIAN CAPITAL LETTER YN;Lu;0;L;;;;;N;;;;2D27;
+10CD;GEORGIAN CAPITAL LETTER AEN;Lu;0;L;;;;;N;;;;2D2D;
+10D0;GEORGIAN LETTER AN;Lo;0;L;;;;;N;GEORGIAN SMALL LETTER AN;;;;
+10D1;GEORGIAN LETTER BAN;Lo;0;L;;;;;N;GEORGIAN SMALL LETTER BAN;;;;
+10D2;GEORGIAN LETTER GAN;Lo;0;L;;;;;N;GEORGIAN SMALL LETTER GAN;;;;
+10D3;GEORGIAN LETTER DON;Lo;0;L;;;;;N;GEORGIAN SMALL LETTER DON;;;;
+10D4;GEORGIAN LETTER EN;Lo;0;L;;;;;N;GEORGIAN SMALL LETTER EN;;;;
+10D5;GEORGIAN LETTER VIN;Lo;0;L;;;;;N;GEORGIAN SMALL LETTER VIN;;;;
+10D6;GEORGIAN LETTER ZEN;Lo;0;L;;;;;N;GEORGIAN SMALL LETTER ZEN;;;;
+10D7;GEORGIAN LETTER TAN;Lo;0;L;;;;;N;GEORGIAN SMALL LETTER TAN;;;;
+10D8;GEORGIAN LETTER IN;Lo;0;L;;;;;N;GEORGIAN SMALL LETTER IN;;;;
+10D9;GEORGIAN LETTER KAN;Lo;0;L;;;;;N;GEORGIAN SMALL LETTER KAN;;;;
+10DA;GEORGIAN LETTER LAS;Lo;0;L;;;;;N;GEORGIAN SMALL LETTER LAS;;;;
+10DB;GEORGIAN LETTER MAN;Lo;0;L;;;;;N;GEORGIAN SMALL LETTER MAN;;;;
+10DC;GEORGIAN LETTER NAR;Lo;0;L;;;;;N;GEORGIAN SMALL LETTER NAR;;;;
+10DD;GEORGIAN LETTER ON;Lo;0;L;;;;;N;GEORGIAN SMALL LETTER ON;;;;
+10DE;GEORGIAN LETTER PAR;Lo;0;L;;;;;N;GEORGIAN SMALL LETTER PAR;;;;
+10DF;GEORGIAN LETTER ZHAR;Lo;0;L;;;;;N;GEORGIAN SMALL LETTER ZHAR;;;;
+10E0;GEORGIAN LETTER RAE;Lo;0;L;;;;;N;GEORGIAN SMALL LETTER RAE;;;;
+10E1;GEORGIAN LETTER SAN;Lo;0;L;;;;;N;GEORGIAN SMALL LETTER SAN;;;;
+10E2;GEORGIAN LETTER TAR;Lo;0;L;;;;;N;GEORGIAN SMALL LETTER TAR;;;;
+10E3;GEORGIAN LETTER UN;Lo;0;L;;;;;N;GEORGIAN SMALL LETTER UN;;;;
+10E4;GEORGIAN LETTER PHAR;Lo;0;L;;;;;N;GEORGIAN SMALL LETTER PHAR;;;;
+10E5;GEORGIAN LETTER KHAR;Lo;0;L;;;;;N;GEORGIAN SMALL LETTER KHAR;;;;
+10E6;GEORGIAN LETTER GHAN;Lo;0;L;;;;;N;GEORGIAN SMALL LETTER GHAN;;;;
+10E7;GEORGIAN LETTER QAR;Lo;0;L;;;;;N;GEORGIAN SMALL LETTER QAR;;;;
+10E8;GEORGIAN LETTER SHIN;Lo;0;L;;;;;N;GEORGIAN SMALL LETTER SHIN;;;;
+10E9;GEORGIAN LETTER CHIN;Lo;0;L;;;;;N;GEORGIAN SMALL LETTER CHIN;;;;
+10EA;GEORGIAN LETTER CAN;Lo;0;L;;;;;N;GEORGIAN SMALL LETTER CAN;;;;
+10EB;GEORGIAN LETTER JIL;Lo;0;L;;;;;N;GEORGIAN SMALL LETTER JIL;;;;
+10EC;GEORGIAN LETTER CIL;Lo;0;L;;;;;N;GEORGIAN SMALL LETTER CIL;;;;
+10ED;GEORGIAN LETTER CHAR;Lo;0;L;;;;;N;GEORGIAN SMALL LETTER CHAR;;;;
+10EE;GEORGIAN LETTER XAN;Lo;0;L;;;;;N;GEORGIAN SMALL LETTER XAN;;;;
+10EF;GEORGIAN LETTER JHAN;Lo;0;L;;;;;N;GEORGIAN SMALL LETTER JHAN;;;;
+10F0;GEORGIAN LETTER HAE;Lo;0;L;;;;;N;GEORGIAN SMALL LETTER HAE;;;;
+10F1;GEORGIAN LETTER HE;Lo;0;L;;;;;N;GEORGIAN SMALL LETTER HE;;;;
+10F2;GEORGIAN LETTER HIE;Lo;0;L;;;;;N;GEORGIAN SMALL LETTER HIE;;;;
+10F3;GEORGIAN LETTER WE;Lo;0;L;;;;;N;GEORGIAN SMALL LETTER WE;;;;
+10F4;GEORGIAN LETTER HAR;Lo;0;L;;;;;N;GEORGIAN SMALL LETTER HAR;;;;
+10F5;GEORGIAN LETTER HOE;Lo;0;L;;;;;N;GEORGIAN SMALL LETTER HOE;;;;
+10F6;GEORGIAN LETTER FI;Lo;0;L;;;;;N;GEORGIAN SMALL LETTER FI;;;;
+10F7;GEORGIAN LETTER YN;Lo;0;L;;;;;N;;;;;
+10F8;GEORGIAN LETTER ELIFI;Lo;0;L;;;;;N;;;;;
+10F9;GEORGIAN LETTER TURNED GAN;Lo;0;L;;;;;N;;;;;
+10FA;GEORGIAN LETTER AIN;Lo;0;L;;;;;N;;;;;
+10FB;GEORGIAN PARAGRAPH SEPARATOR;Po;0;L;;;;;N;;;;;
+10FC;MODIFIER LETTER GEORGIAN NAR;Lm;0;L;<super> 10DC;;;;N;;;;;
+10FD;GEORGIAN LETTER AEN;Lo;0;L;;;;;N;;;;;
+10FE;GEORGIAN LETTER HARD SIGN;Lo;0;L;;;;;N;;;;;
+10FF;GEORGIAN LETTER LABIAL SIGN;Lo;0;L;;;;;N;;;;;
+1100;HANGUL CHOSEONG KIYEOK;Lo;0;L;;;;;N;;;;;
+1101;HANGUL CHOSEONG SSANGKIYEOK;Lo;0;L;;;;;N;;;;;
+1102;HANGUL CHOSEONG NIEUN;Lo;0;L;;;;;N;;;;;
+1103;HANGUL CHOSEONG TIKEUT;Lo;0;L;;;;;N;;;;;
+1104;HANGUL CHOSEONG SSANGTIKEUT;Lo;0;L;;;;;N;;;;;
+1105;HANGUL CHOSEONG RIEUL;Lo;0;L;;;;;N;;;;;
+1106;HANGUL CHOSEONG MIEUM;Lo;0;L;;;;;N;;;;;
+1107;HANGUL CHOSEONG PIEUP;Lo;0;L;;;;;N;;;;;
+1108;HANGUL CHOSEONG SSANGPIEUP;Lo;0;L;;;;;N;;;;;
+1109;HANGUL CHOSEONG SIOS;Lo;0;L;;;;;N;;;;;
+110A;HANGUL CHOSEONG SSANGSIOS;Lo;0;L;;;;;N;;;;;
+110B;HANGUL CHOSEONG IEUNG;Lo;0;L;;;;;N;;;;;
+110C;HANGUL CHOSEONG CIEUC;Lo;0;L;;;;;N;;;;;
+110D;HANGUL CHOSEONG SSANGCIEUC;Lo;0;L;;;;;N;;;;;
+110E;HANGUL CHOSEONG CHIEUCH;Lo;0;L;;;;;N;;;;;
+110F;HANGUL CHOSEONG KHIEUKH;Lo;0;L;;;;;N;;;;;
+1110;HANGUL CHOSEONG THIEUTH;Lo;0;L;;;;;N;;;;;
+1111;HANGUL CHOSEONG PHIEUPH;Lo;0;L;;;;;N;;;;;
+1112;HANGUL CHOSEONG HIEUH;Lo;0;L;;;;;N;;;;;
+1113;HANGUL CHOSEONG NIEUN-KIYEOK;Lo;0;L;;;;;N;;;;;
+1114;HANGUL CHOSEONG SSANGNIEUN;Lo;0;L;;;;;N;;;;;
+1115;HANGUL CHOSEONG NIEUN-TIKEUT;Lo;0;L;;;;;N;;;;;
+1116;HANGUL CHOSEONG NIEUN-PIEUP;Lo;0;L;;;;;N;;;;;
+1117;HANGUL CHOSEONG TIKEUT-KIYEOK;Lo;0;L;;;;;N;;;;;
+1118;HANGUL CHOSEONG RIEUL-NIEUN;Lo;0;L;;;;;N;;;;;
+1119;HANGUL CHOSEONG SSANGRIEUL;Lo;0;L;;;;;N;;;;;
+111A;HANGUL CHOSEONG RIEUL-HIEUH;Lo;0;L;;;;;N;;;;;
+111B;HANGUL CHOSEONG KAPYEOUNRIEUL;Lo;0;L;;;;;N;;;;;
+111C;HANGUL CHOSEONG MIEUM-PIEUP;Lo;0;L;;;;;N;;;;;
+111D;HANGUL CHOSEONG KAPYEOUNMIEUM;Lo;0;L;;;;;N;;;;;
+111E;HANGUL CHOSEONG PIEUP-KIYEOK;Lo;0;L;;;;;N;;;;;
+111F;HANGUL CHOSEONG PIEUP-NIEUN;Lo;0;L;;;;;N;;;;;
+1120;HANGUL CHOSEONG PIEUP-TIKEUT;Lo;0;L;;;;;N;;;;;
+1121;HANGUL CHOSEONG PIEUP-SIOS;Lo;0;L;;;;;N;;;;;
+1122;HANGUL CHOSEONG PIEUP-SIOS-KIYEOK;Lo;0;L;;;;;N;;;;;
+1123;HANGUL CHOSEONG PIEUP-SIOS-TIKEUT;Lo;0;L;;;;;N;;;;;
+1124;HANGUL CHOSEONG PIEUP-SIOS-PIEUP;Lo;0;L;;;;;N;;;;;
+1125;HANGUL CHOSEONG PIEUP-SSANGSIOS;Lo;0;L;;;;;N;;;;;
+1126;HANGUL CHOSEONG PIEUP-SIOS-CIEUC;Lo;0;L;;;;;N;;;;;
+1127;HANGUL CHOSEONG PIEUP-CIEUC;Lo;0;L;;;;;N;;;;;
+1128;HANGUL CHOSEONG PIEUP-CHIEUCH;Lo;0;L;;;;;N;;;;;
+1129;HANGUL CHOSEONG PIEUP-THIEUTH;Lo;0;L;;;;;N;;;;;
+112A;HANGUL CHOSEONG PIEUP-PHIEUPH;Lo;0;L;;;;;N;;;;;
+112B;HANGUL CHOSEONG KAPYEOUNPIEUP;Lo;0;L;;;;;N;;;;;
+112C;HANGUL CHOSEONG KAPYEOUNSSANGPIEUP;Lo;0;L;;;;;N;;;;;
+112D;HANGUL CHOSEONG SIOS-KIYEOK;Lo;0;L;;;;;N;;;;;
+112E;HANGUL CHOSEONG SIOS-NIEUN;Lo;0;L;;;;;N;;;;;
+112F;HANGUL CHOSEONG SIOS-TIKEUT;Lo;0;L;;;;;N;;;;;
+1130;HANGUL CHOSEONG SIOS-RIEUL;Lo;0;L;;;;;N;;;;;
+1131;HANGUL CHOSEONG SIOS-MIEUM;Lo;0;L;;;;;N;;;;;
+1132;HANGUL CHOSEONG SIOS-PIEUP;Lo;0;L;;;;;N;;;;;
+1133;HANGUL CHOSEONG SIOS-PIEUP-KIYEOK;Lo;0;L;;;;;N;;;;;
+1134;HANGUL CHOSEONG SIOS-SSANGSIOS;Lo;0;L;;;;;N;;;;;
+1135;HANGUL CHOSEONG SIOS-IEUNG;Lo;0;L;;;;;N;;;;;
+1136;HANGUL CHOSEONG SIOS-CIEUC;Lo;0;L;;;;;N;;;;;
+1137;HANGUL CHOSEONG SIOS-CHIEUCH;Lo;0;L;;;;;N;;;;;
+1138;HANGUL CHOSEONG SIOS-KHIEUKH;Lo;0;L;;;;;N;;;;;
+1139;HANGUL CHOSEONG SIOS-THIEUTH;Lo;0;L;;;;;N;;;;;
+113A;HANGUL CHOSEONG SIOS-PHIEUPH;Lo;0;L;;;;;N;;;;;
+113B;HANGUL CHOSEONG SIOS-HIEUH;Lo;0;L;;;;;N;;;;;
+113C;HANGUL CHOSEONG CHITUEUMSIOS;Lo;0;L;;;;;N;;;;;
+113D;HANGUL CHOSEONG CHITUEUMSSANGSIOS;Lo;0;L;;;;;N;;;;;
+113E;HANGUL CHOSEONG CEONGCHIEUMSIOS;Lo;0;L;;;;;N;;;;;
+113F;HANGUL CHOSEONG CEONGCHIEUMSSANGSIOS;Lo;0;L;;;;;N;;;;;
+1140;HANGUL CHOSEONG PANSIOS;Lo;0;L;;;;;N;;;;;
+1141;HANGUL CHOSEONG IEUNG-KIYEOK;Lo;0;L;;;;;N;;;;;
+1142;HANGUL CHOSEONG IEUNG-TIKEUT;Lo;0;L;;;;;N;;;;;
+1143;HANGUL CHOSEONG IEUNG-MIEUM;Lo;0;L;;;;;N;;;;;
+1144;HANGUL CHOSEONG IEUNG-PIEUP;Lo;0;L;;;;;N;;;;;
+1145;HANGUL CHOSEONG IEUNG-SIOS;Lo;0;L;;;;;N;;;;;
+1146;HANGUL CHOSEONG IEUNG-PANSIOS;Lo;0;L;;;;;N;;;;;
+1147;HANGUL CHOSEONG SSANGIEUNG;Lo;0;L;;;;;N;;;;;
+1148;HANGUL CHOSEONG IEUNG-CIEUC;Lo;0;L;;;;;N;;;;;
+1149;HANGUL CHOSEONG IEUNG-CHIEUCH;Lo;0;L;;;;;N;;;;;
+114A;HANGUL CHOSEONG IEUNG-THIEUTH;Lo;0;L;;;;;N;;;;;
+114B;HANGUL CHOSEONG IEUNG-PHIEUPH;Lo;0;L;;;;;N;;;;;
+114C;HANGUL CHOSEONG YESIEUNG;Lo;0;L;;;;;N;;;;;
+114D;HANGUL CHOSEONG CIEUC-IEUNG;Lo;0;L;;;;;N;;;;;
+114E;HANGUL CHOSEONG CHITUEUMCIEUC;Lo;0;L;;;;;N;;;;;
+114F;HANGUL CHOSEONG CHITUEUMSSANGCIEUC;Lo;0;L;;;;;N;;;;;
+1150;HANGUL CHOSEONG CEONGCHIEUMCIEUC;Lo;0;L;;;;;N;;;;;
+1151;HANGUL CHOSEONG CEONGCHIEUMSSANGCIEUC;Lo;0;L;;;;;N;;;;;
+1152;HANGUL CHOSEONG CHIEUCH-KHIEUKH;Lo;0;L;;;;;N;;;;;
+1153;HANGUL CHOSEONG CHIEUCH-HIEUH;Lo;0;L;;;;;N;;;;;
+1154;HANGUL CHOSEONG CHITUEUMCHIEUCH;Lo;0;L;;;;;N;;;;;
+1155;HANGUL CHOSEONG CEONGCHIEUMCHIEUCH;Lo;0;L;;;;;N;;;;;
+1156;HANGUL CHOSEONG PHIEUPH-PIEUP;Lo;0;L;;;;;N;;;;;
+1157;HANGUL CHOSEONG KAPYEOUNPHIEUPH;Lo;0;L;;;;;N;;;;;
+1158;HANGUL CHOSEONG SSANGHIEUH;Lo;0;L;;;;;N;;;;;
+1159;HANGUL CHOSEONG YEORINHIEUH;Lo;0;L;;;;;N;;;;;
+115A;HANGUL CHOSEONG KIYEOK-TIKEUT;Lo;0;L;;;;;N;;;;;
+115B;HANGUL CHOSEONG NIEUN-SIOS;Lo;0;L;;;;;N;;;;;
+115C;HANGUL CHOSEONG NIEUN-CIEUC;Lo;0;L;;;;;N;;;;;
+115D;HANGUL CHOSEONG NIEUN-HIEUH;Lo;0;L;;;;;N;;;;;
+115E;HANGUL CHOSEONG TIKEUT-RIEUL;Lo;0;L;;;;;N;;;;;
+115F;HANGUL CHOSEONG FILLER;Lo;0;L;;;;;N;;;;;
+1160;HANGUL JUNGSEONG FILLER;Lo;0;L;;;;;N;;;;;
+1161;HANGUL JUNGSEONG A;Lo;0;L;;;;;N;;;;;
+1162;HANGUL JUNGSEONG AE;Lo;0;L;;;;;N;;;;;
+1163;HANGUL JUNGSEONG YA;Lo;0;L;;;;;N;;;;;
+1164;HANGUL JUNGSEONG YAE;Lo;0;L;;;;;N;;;;;
+1165;HANGUL JUNGSEONG EO;Lo;0;L;;;;;N;;;;;
+1166;HANGUL JUNGSEONG E;Lo;0;L;;;;;N;;;;;
+1167;HANGUL JUNGSEONG YEO;Lo;0;L;;;;;N;;;;;
+1168;HANGUL JUNGSEONG YE;Lo;0;L;;;;;N;;;;;
+1169;HANGUL JUNGSEONG O;Lo;0;L;;;;;N;;;;;
+116A;HANGUL JUNGSEONG WA;Lo;0;L;;;;;N;;;;;
+116B;HANGUL JUNGSEONG WAE;Lo;0;L;;;;;N;;;;;
+116C;HANGUL JUNGSEONG OE;Lo;0;L;;;;;N;;;;;
+116D;HANGUL JUNGSEONG YO;Lo;0;L;;;;;N;;;;;
+116E;HANGUL JUNGSEONG U;Lo;0;L;;;;;N;;;;;
+116F;HANGUL JUNGSEONG WEO;Lo;0;L;;;;;N;;;;;
+1170;HANGUL JUNGSEONG WE;Lo;0;L;;;;;N;;;;;
+1171;HANGUL JUNGSEONG WI;Lo;0;L;;;;;N;;;;;
+1172;HANGUL JUNGSEONG YU;Lo;0;L;;;;;N;;;;;
+1173;HANGUL JUNGSEONG EU;Lo;0;L;;;;;N;;;;;
+1174;HANGUL JUNGSEONG YI;Lo;0;L;;;;;N;;;;;
+1175;HANGUL JUNGSEONG I;Lo;0;L;;;;;N;;;;;
+1176;HANGUL JUNGSEONG A-O;Lo;0;L;;;;;N;;;;;
+1177;HANGUL JUNGSEONG A-U;Lo;0;L;;;;;N;;;;;
+1178;HANGUL JUNGSEONG YA-O;Lo;0;L;;;;;N;;;;;
+1179;HANGUL JUNGSEONG YA-YO;Lo;0;L;;;;;N;;;;;
+117A;HANGUL JUNGSEONG EO-O;Lo;0;L;;;;;N;;;;;
+117B;HANGUL JUNGSEONG EO-U;Lo;0;L;;;;;N;;;;;
+117C;HANGUL JUNGSEONG EO-EU;Lo;0;L;;;;;N;;;;;
+117D;HANGUL JUNGSEONG YEO-O;Lo;0;L;;;;;N;;;;;
+117E;HANGUL JUNGSEONG YEO-U;Lo;0;L;;;;;N;;;;;
+117F;HANGUL JUNGSEONG O-EO;Lo;0;L;;;;;N;;;;;
+1180;HANGUL JUNGSEONG O-E;Lo;0;L;;;;;N;;;;;
+1181;HANGUL JUNGSEONG O-YE;Lo;0;L;;;;;N;;;;;
+1182;HANGUL JUNGSEONG O-O;Lo;0;L;;;;;N;;;;;
+1183;HANGUL JUNGSEONG O-U;Lo;0;L;;;;;N;;;;;
+1184;HANGUL JUNGSEONG YO-YA;Lo;0;L;;;;;N;;;;;
+1185;HANGUL JUNGSEONG YO-YAE;Lo;0;L;;;;;N;;;;;
+1186;HANGUL JUNGSEONG YO-YEO;Lo;0;L;;;;;N;;;;;
+1187;HANGUL JUNGSEONG YO-O;Lo;0;L;;;;;N;;;;;
+1188;HANGUL JUNGSEONG YO-I;Lo;0;L;;;;;N;;;;;
+1189;HANGUL JUNGSEONG U-A;Lo;0;L;;;;;N;;;;;
+118A;HANGUL JUNGSEONG U-AE;Lo;0;L;;;;;N;;;;;
+118B;HANGUL JUNGSEONG U-EO-EU;Lo;0;L;;;;;N;;;;;
+118C;HANGUL JUNGSEONG U-YE;Lo;0;L;;;;;N;;;;;
+118D;HANGUL JUNGSEONG U-U;Lo;0;L;;;;;N;;;;;
+118E;HANGUL JUNGSEONG YU-A;Lo;0;L;;;;;N;;;;;
+118F;HANGUL JUNGSEONG YU-EO;Lo;0;L;;;;;N;;;;;
+1190;HANGUL JUNGSEONG YU-E;Lo;0;L;;;;;N;;;;;
+1191;HANGUL JUNGSEONG YU-YEO;Lo;0;L;;;;;N;;;;;
+1192;HANGUL JUNGSEONG YU-YE;Lo;0;L;;;;;N;;;;;
+1193;HANGUL JUNGSEONG YU-U;Lo;0;L;;;;;N;;;;;
+1194;HANGUL JUNGSEONG YU-I;Lo;0;L;;;;;N;;;;;
+1195;HANGUL JUNGSEONG EU-U;Lo;0;L;;;;;N;;;;;
+1196;HANGUL JUNGSEONG EU-EU;Lo;0;L;;;;;N;;;;;
+1197;HANGUL JUNGSEONG YI-U;Lo;0;L;;;;;N;;;;;
+1198;HANGUL JUNGSEONG I-A;Lo;0;L;;;;;N;;;;;
+1199;HANGUL JUNGSEONG I-YA;Lo;0;L;;;;;N;;;;;
+119A;HANGUL JUNGSEONG I-O;Lo;0;L;;;;;N;;;;;
+119B;HANGUL JUNGSEONG I-U;Lo;0;L;;;;;N;;;;;
+119C;HANGUL JUNGSEONG I-EU;Lo;0;L;;;;;N;;;;;
+119D;HANGUL JUNGSEONG I-ARAEA;Lo;0;L;;;;;N;;;;;
+119E;HANGUL JUNGSEONG ARAEA;Lo;0;L;;;;;N;;;;;
+119F;HANGUL JUNGSEONG ARAEA-EO;Lo;0;L;;;;;N;;;;;
+11A0;HANGUL JUNGSEONG ARAEA-U;Lo;0;L;;;;;N;;;;;
+11A1;HANGUL JUNGSEONG ARAEA-I;Lo;0;L;;;;;N;;;;;
+11A2;HANGUL JUNGSEONG SSANGARAEA;Lo;0;L;;;;;N;;;;;
+11A3;HANGUL JUNGSEONG A-EU;Lo;0;L;;;;;N;;;;;
+11A4;HANGUL JUNGSEONG YA-U;Lo;0;L;;;;;N;;;;;
+11A5;HANGUL JUNGSEONG YEO-YA;Lo;0;L;;;;;N;;;;;
+11A6;HANGUL JUNGSEONG O-YA;Lo;0;L;;;;;N;;;;;
+11A7;HANGUL JUNGSEONG O-YAE;Lo;0;L;;;;;N;;;;;
+11A8;HANGUL JONGSEONG KIYEOK;Lo;0;L;;;;;N;;;;;
+11A9;HANGUL JONGSEONG SSANGKIYEOK;Lo;0;L;;;;;N;;;;;
+11AA;HANGUL JONGSEONG KIYEOK-SIOS;Lo;0;L;;;;;N;;;;;
+11AB;HANGUL JONGSEONG NIEUN;Lo;0;L;;;;;N;;;;;
+11AC;HANGUL JONGSEONG NIEUN-CIEUC;Lo;0;L;;;;;N;;;;;
+11AD;HANGUL JONGSEONG NIEUN-HIEUH;Lo;0;L;;;;;N;;;;;
+11AE;HANGUL JONGSEONG TIKEUT;Lo;0;L;;;;;N;;;;;
+11AF;HANGUL JONGSEONG RIEUL;Lo;0;L;;;;;N;;;;;
+11B0;HANGUL JONGSEONG RIEUL-KIYEOK;Lo;0;L;;;;;N;;;;;
+11B1;HANGUL JONGSEONG RIEUL-MIEUM;Lo;0;L;;;;;N;;;;;
+11B2;HANGUL JONGSEONG RIEUL-PIEUP;Lo;0;L;;;;;N;;;;;
+11B3;HANGUL JONGSEONG RIEUL-SIOS;Lo;0;L;;;;;N;;;;;
+11B4;HANGUL JONGSEONG RIEUL-THIEUTH;Lo;0;L;;;;;N;;;;;
+11B5;HANGUL JONGSEONG RIEUL-PHIEUPH;Lo;0;L;;;;;N;;;;;
+11B6;HANGUL JONGSEONG RIEUL-HIEUH;Lo;0;L;;;;;N;;;;;
+11B7;HANGUL JONGSEONG MIEUM;Lo;0;L;;;;;N;;;;;
+11B8;HANGUL JONGSEONG PIEUP;Lo;0;L;;;;;N;;;;;
+11B9;HANGUL JONGSEONG PIEUP-SIOS;Lo;0;L;;;;;N;;;;;
+11BA;HANGUL JONGSEONG SIOS;Lo;0;L;;;;;N;;;;;
+11BB;HANGUL JONGSEONG SSANGSIOS;Lo;0;L;;;;;N;;;;;
+11BC;HANGUL JONGSEONG IEUNG;Lo;0;L;;;;;N;;;;;
+11BD;HANGUL JONGSEONG CIEUC;Lo;0;L;;;;;N;;;;;
+11BE;HANGUL JONGSEONG CHIEUCH;Lo;0;L;;;;;N;;;;;
+11BF;HANGUL JONGSEONG KHIEUKH;Lo;0;L;;;;;N;;;;;
+11C0;HANGUL JONGSEONG THIEUTH;Lo;0;L;;;;;N;;;;;
+11C1;HANGUL JONGSEONG PHIEUPH;Lo;0;L;;;;;N;;;;;
+11C2;HANGUL JONGSEONG HIEUH;Lo;0;L;;;;;N;;;;;
+11C3;HANGUL JONGSEONG KIYEOK-RIEUL;Lo;0;L;;;;;N;;;;;
+11C4;HANGUL JONGSEONG KIYEOK-SIOS-KIYEOK;Lo;0;L;;;;;N;;;;;
+11C5;HANGUL JONGSEONG NIEUN-KIYEOK;Lo;0;L;;;;;N;;;;;
+11C6;HANGUL JONGSEONG NIEUN-TIKEUT;Lo;0;L;;;;;N;;;;;
+11C7;HANGUL JONGSEONG NIEUN-SIOS;Lo;0;L;;;;;N;;;;;
+11C8;HANGUL JONGSEONG NIEUN-PANSIOS;Lo;0;L;;;;;N;;;;;
+11C9;HANGUL JONGSEONG NIEUN-THIEUTH;Lo;0;L;;;;;N;;;;;
+11CA;HANGUL JONGSEONG TIKEUT-KIYEOK;Lo;0;L;;;;;N;;;;;
+11CB;HANGUL JONGSEONG TIKEUT-RIEUL;Lo;0;L;;;;;N;;;;;
+11CC;HANGUL JONGSEONG RIEUL-KIYEOK-SIOS;Lo;0;L;;;;;N;;;;;
+11CD;HANGUL JONGSEONG RIEUL-NIEUN;Lo;0;L;;;;;N;;;;;
+11CE;HANGUL JONGSEONG RIEUL-TIKEUT;Lo;0;L;;;;;N;;;;;
+11CF;HANGUL JONGSEONG RIEUL-TIKEUT-HIEUH;Lo;0;L;;;;;N;;;;;
+11D0;HANGUL JONGSEONG SSANGRIEUL;Lo;0;L;;;;;N;;;;;
+11D1;HANGUL JONGSEONG RIEUL-MIEUM-KIYEOK;Lo;0;L;;;;;N;;;;;
+11D2;HANGUL JONGSEONG RIEUL-MIEUM-SIOS;Lo;0;L;;;;;N;;;;;
+11D3;HANGUL JONGSEONG RIEUL-PIEUP-SIOS;Lo;0;L;;;;;N;;;;;
+11D4;HANGUL JONGSEONG RIEUL-PIEUP-HIEUH;Lo;0;L;;;;;N;;;;;
+11D5;HANGUL JONGSEONG RIEUL-KAPYEOUNPIEUP;Lo;0;L;;;;;N;;;;;
+11D6;HANGUL JONGSEONG RIEUL-SSANGSIOS;Lo;0;L;;;;;N;;;;;
+11D7;HANGUL JONGSEONG RIEUL-PANSIOS;Lo;0;L;;;;;N;;;;;
+11D8;HANGUL JONGSEONG RIEUL-KHIEUKH;Lo;0;L;;;;;N;;;;;
+11D9;HANGUL JONGSEONG RIEUL-YEORINHIEUH;Lo;0;L;;;;;N;;;;;
+11DA;HANGUL JONGSEONG MIEUM-KIYEOK;Lo;0;L;;;;;N;;;;;
+11DB;HANGUL JONGSEONG MIEUM-RIEUL;Lo;0;L;;;;;N;;;;;
+11DC;HANGUL JONGSEONG MIEUM-PIEUP;Lo;0;L;;;;;N;;;;;
+11DD;HANGUL JONGSEONG MIEUM-SIOS;Lo;0;L;;;;;N;;;;;
+11DE;HANGUL JONGSEONG MIEUM-SSANGSIOS;Lo;0;L;;;;;N;;;;;
+11DF;HANGUL JONGSEONG MIEUM-PANSIOS;Lo;0;L;;;;;N;;;;;
+11E0;HANGUL JONGSEONG MIEUM-CHIEUCH;Lo;0;L;;;;;N;;;;;
+11E1;HANGUL JONGSEONG MIEUM-HIEUH;Lo;0;L;;;;;N;;;;;
+11E2;HANGUL JONGSEONG KAPYEOUNMIEUM;Lo;0;L;;;;;N;;;;;
+11E3;HANGUL JONGSEONG PIEUP-RIEUL;Lo;0;L;;;;;N;;;;;
+11E4;HANGUL JONGSEONG PIEUP-PHIEUPH;Lo;0;L;;;;;N;;;;;
+11E5;HANGUL JONGSEONG PIEUP-HIEUH;Lo;0;L;;;;;N;;;;;
+11E6;HANGUL JONGSEONG KAPYEOUNPIEUP;Lo;0;L;;;;;N;;;;;
+11E7;HANGUL JONGSEONG SIOS-KIYEOK;Lo;0;L;;;;;N;;;;;
+11E8;HANGUL JONGSEONG SIOS-TIKEUT;Lo;0;L;;;;;N;;;;;
+11E9;HANGUL JONGSEONG SIOS-RIEUL;Lo;0;L;;;;;N;;;;;
+11EA;HANGUL JONGSEONG SIOS-PIEUP;Lo;0;L;;;;;N;;;;;
+11EB;HANGUL JONGSEONG PANSIOS;Lo;0;L;;;;;N;;;;;
+11EC;HANGUL JONGSEONG IEUNG-KIYEOK;Lo;0;L;;;;;N;;;;;
+11ED;HANGUL JONGSEONG IEUNG-SSANGKIYEOK;Lo;0;L;;;;;N;;;;;
+11EE;HANGUL JONGSEONG SSANGIEUNG;Lo;0;L;;;;;N;;;;;
+11EF;HANGUL JONGSEONG IEUNG-KHIEUKH;Lo;0;L;;;;;N;;;;;
+11F0;HANGUL JONGSEONG YESIEUNG;Lo;0;L;;;;;N;;;;;
+11F1;HANGUL JONGSEONG YESIEUNG-SIOS;Lo;0;L;;;;;N;;;;;
+11F2;HANGUL JONGSEONG YESIEUNG-PANSIOS;Lo;0;L;;;;;N;;;;;
+11F3;HANGUL JONGSEONG PHIEUPH-PIEUP;Lo;0;L;;;;;N;;;;;
+11F4;HANGUL JONGSEONG KAPYEOUNPHIEUPH;Lo;0;L;;;;;N;;;;;
+11F5;HANGUL JONGSEONG HIEUH-NIEUN;Lo;0;L;;;;;N;;;;;
+11F6;HANGUL JONGSEONG HIEUH-RIEUL;Lo;0;L;;;;;N;;;;;
+11F7;HANGUL JONGSEONG HIEUH-MIEUM;Lo;0;L;;;;;N;;;;;
+11F8;HANGUL JONGSEONG HIEUH-PIEUP;Lo;0;L;;;;;N;;;;;
+11F9;HANGUL JONGSEONG YEORINHIEUH;Lo;0;L;;;;;N;;;;;
+11FA;HANGUL JONGSEONG KIYEOK-NIEUN;Lo;0;L;;;;;N;;;;;
+11FB;HANGUL JONGSEONG KIYEOK-PIEUP;Lo;0;L;;;;;N;;;;;
+11FC;HANGUL JONGSEONG KIYEOK-CHIEUCH;Lo;0;L;;;;;N;;;;;
+11FD;HANGUL JONGSEONG KIYEOK-KHIEUKH;Lo;0;L;;;;;N;;;;;
+11FE;HANGUL JONGSEONG KIYEOK-HIEUH;Lo;0;L;;;;;N;;;;;
+11FF;HANGUL JONGSEONG SSANGNIEUN;Lo;0;L;;;;;N;;;;;
+1200;ETHIOPIC SYLLABLE HA;Lo;0;L;;;;;N;;;;;
+1201;ETHIOPIC SYLLABLE HU;Lo;0;L;;;;;N;;;;;
+1202;ETHIOPIC SYLLABLE HI;Lo;0;L;;;;;N;;;;;
+1203;ETHIOPIC SYLLABLE HAA;Lo;0;L;;;;;N;;;;;
+1204;ETHIOPIC SYLLABLE HEE;Lo;0;L;;;;;N;;;;;
+1205;ETHIOPIC SYLLABLE HE;Lo;0;L;;;;;N;;;;;
+1206;ETHIOPIC SYLLABLE HO;Lo;0;L;;;;;N;;;;;
+1207;ETHIOPIC SYLLABLE HOA;Lo;0;L;;;;;N;;;;;
+1208;ETHIOPIC SYLLABLE LA;Lo;0;L;;;;;N;;;;;
+1209;ETHIOPIC SYLLABLE LU;Lo;0;L;;;;;N;;;;;
+120A;ETHIOPIC SYLLABLE LI;Lo;0;L;;;;;N;;;;;
+120B;ETHIOPIC SYLLABLE LAA;Lo;0;L;;;;;N;;;;;
+120C;ETHIOPIC SYLLABLE LEE;Lo;0;L;;;;;N;;;;;
+120D;ETHIOPIC SYLLABLE LE;Lo;0;L;;;;;N;;;;;
+120E;ETHIOPIC SYLLABLE LO;Lo;0;L;;;;;N;;;;;
+120F;ETHIOPIC SYLLABLE LWA;Lo;0;L;;;;;N;;;;;
+1210;ETHIOPIC SYLLABLE HHA;Lo;0;L;;;;;N;;;;;
+1211;ETHIOPIC SYLLABLE HHU;Lo;0;L;;;;;N;;;;;
+1212;ETHIOPIC SYLLABLE HHI;Lo;0;L;;;;;N;;;;;
+1213;ETHIOPIC SYLLABLE HHAA;Lo;0;L;;;;;N;;;;;
+1214;ETHIOPIC SYLLABLE HHEE;Lo;0;L;;;;;N;;;;;
+1215;ETHIOPIC SYLLABLE HHE;Lo;0;L;;;;;N;;;;;
+1216;ETHIOPIC SYLLABLE HHO;Lo;0;L;;;;;N;;;;;
+1217;ETHIOPIC SYLLABLE HHWA;Lo;0;L;;;;;N;;;;;
+1218;ETHIOPIC SYLLABLE MA;Lo;0;L;;;;;N;;;;;
+1219;ETHIOPIC SYLLABLE MU;Lo;0;L;;;;;N;;;;;
+121A;ETHIOPIC SYLLABLE MI;Lo;0;L;;;;;N;;;;;
+121B;ETHIOPIC SYLLABLE MAA;Lo;0;L;;;;;N;;;;;
+121C;ETHIOPIC SYLLABLE MEE;Lo;0;L;;;;;N;;;;;
+121D;ETHIOPIC SYLLABLE ME;Lo;0;L;;;;;N;;;;;
+121E;ETHIOPIC SYLLABLE MO;Lo;0;L;;;;;N;;;;;
+121F;ETHIOPIC SYLLABLE MWA;Lo;0;L;;;;;N;;;;;
+1220;ETHIOPIC SYLLABLE SZA;Lo;0;L;;;;;N;;;;;
+1221;ETHIOPIC SYLLABLE SZU;Lo;0;L;;;;;N;;;;;
+1222;ETHIOPIC SYLLABLE SZI;Lo;0;L;;;;;N;;;;;
+1223;ETHIOPIC SYLLABLE SZAA;Lo;0;L;;;;;N;;;;;
+1224;ETHIOPIC SYLLABLE SZEE;Lo;0;L;;;;;N;;;;;
+1225;ETHIOPIC SYLLABLE SZE;Lo;0;L;;;;;N;;;;;
+1226;ETHIOPIC SYLLABLE SZO;Lo;0;L;;;;;N;;;;;
+1227;ETHIOPIC SYLLABLE SZWA;Lo;0;L;;;;;N;;;;;
+1228;ETHIOPIC SYLLABLE RA;Lo;0;L;;;;;N;;;;;
+1229;ETHIOPIC SYLLABLE RU;Lo;0;L;;;;;N;;;;;
+122A;ETHIOPIC SYLLABLE RI;Lo;0;L;;;;;N;;;;;
+122B;ETHIOPIC SYLLABLE RAA;Lo;0;L;;;;;N;;;;;
+122C;ETHIOPIC SYLLABLE REE;Lo;0;L;;;;;N;;;;;
+122D;ETHIOPIC SYLLABLE RE;Lo;0;L;;;;;N;;;;;
+122E;ETHIOPIC SYLLABLE RO;Lo;0;L;;;;;N;;;;;
+122F;ETHIOPIC SYLLABLE RWA;Lo;0;L;;;;;N;;;;;
+1230;ETHIOPIC SYLLABLE SA;Lo;0;L;;;;;N;;;;;
+1231;ETHIOPIC SYLLABLE SU;Lo;0;L;;;;;N;;;;;
+1232;ETHIOPIC SYLLABLE SI;Lo;0;L;;;;;N;;;;;
+1233;ETHIOPIC SYLLABLE SAA;Lo;0;L;;;;;N;;;;;
+1234;ETHIOPIC SYLLABLE SEE;Lo;0;L;;;;;N;;;;;
+1235;ETHIOPIC SYLLABLE SE;Lo;0;L;;;;;N;;;;;
+1236;ETHIOPIC SYLLABLE SO;Lo;0;L;;;;;N;;;;;
+1237;ETHIOPIC SYLLABLE SWA;Lo;0;L;;;;;N;;;;;
+1238;ETHIOPIC SYLLABLE SHA;Lo;0;L;;;;;N;;;;;
+1239;ETHIOPIC SYLLABLE SHU;Lo;0;L;;;;;N;;;;;
+123A;ETHIOPIC SYLLABLE SHI;Lo;0;L;;;;;N;;;;;
+123B;ETHIOPIC SYLLABLE SHAA;Lo;0;L;;;;;N;;;;;
+123C;ETHIOPIC SYLLABLE SHEE;Lo;0;L;;;;;N;;;;;
+123D;ETHIOPIC SYLLABLE SHE;Lo;0;L;;;;;N;;;;;
+123E;ETHIOPIC SYLLABLE SHO;Lo;0;L;;;;;N;;;;;
+123F;ETHIOPIC SYLLABLE SHWA;Lo;0;L;;;;;N;;;;;
+1240;ETHIOPIC SYLLABLE QA;Lo;0;L;;;;;N;;;;;
+1241;ETHIOPIC SYLLABLE QU;Lo;0;L;;;;;N;;;;;
+1242;ETHIOPIC SYLLABLE QI;Lo;0;L;;;;;N;;;;;
+1243;ETHIOPIC SYLLABLE QAA;Lo;0;L;;;;;N;;;;;
+1244;ETHIOPIC SYLLABLE QEE;Lo;0;L;;;;;N;;;;;
+1245;ETHIOPIC SYLLABLE QE;Lo;0;L;;;;;N;;;;;
+1246;ETHIOPIC SYLLABLE QO;Lo;0;L;;;;;N;;;;;
+1247;ETHIOPIC SYLLABLE QOA;Lo;0;L;;;;;N;;;;;
+1248;ETHIOPIC SYLLABLE QWA;Lo;0;L;;;;;N;;;;;
+124A;ETHIOPIC SYLLABLE QWI;Lo;0;L;;;;;N;;;;;
+124B;ETHIOPIC SYLLABLE QWAA;Lo;0;L;;;;;N;;;;;
+124C;ETHIOPIC SYLLABLE QWEE;Lo;0;L;;;;;N;;;;;
+124D;ETHIOPIC SYLLABLE QWE;Lo;0;L;;;;;N;;;;;
+1250;ETHIOPIC SYLLABLE QHA;Lo;0;L;;;;;N;;;;;
+1251;ETHIOPIC SYLLABLE QHU;Lo;0;L;;;;;N;;;;;
+1252;ETHIOPIC SYLLABLE QHI;Lo;0;L;;;;;N;;;;;
+1253;ETHIOPIC SYLLABLE QHAA;Lo;0;L;;;;;N;;;;;
+1254;ETHIOPIC SYLLABLE QHEE;Lo;0;L;;;;;N;;;;;
+1255;ETHIOPIC SYLLABLE QHE;Lo;0;L;;;;;N;;;;;
+1256;ETHIOPIC SYLLABLE QHO;Lo;0;L;;;;;N;;;;;
+1258;ETHIOPIC SYLLABLE QHWA;Lo;0;L;;;;;N;;;;;
+125A;ETHIOPIC SYLLABLE QHWI;Lo;0;L;;;;;N;;;;;
+125B;ETHIOPIC SYLLABLE QHWAA;Lo;0;L;;;;;N;;;;;
+125C;ETHIOPIC SYLLABLE QHWEE;Lo;0;L;;;;;N;;;;;
+125D;ETHIOPIC SYLLABLE QHWE;Lo;0;L;;;;;N;;;;;
+1260;ETHIOPIC SYLLABLE BA;Lo;0;L;;;;;N;;;;;
+1261;ETHIOPIC SYLLABLE BU;Lo;0;L;;;;;N;;;;;
+1262;ETHIOPIC SYLLABLE BI;Lo;0;L;;;;;N;;;;;
+1263;ETHIOPIC SYLLABLE BAA;Lo;0;L;;;;;N;;;;;
+1264;ETHIOPIC SYLLABLE BEE;Lo;0;L;;;;;N;;;;;
+1265;ETHIOPIC SYLLABLE BE;Lo;0;L;;;;;N;;;;;
+1266;ETHIOPIC SYLLABLE BO;Lo;0;L;;;;;N;;;;;
+1267;ETHIOPIC SYLLABLE BWA;Lo;0;L;;;;;N;;;;;
+1268;ETHIOPIC SYLLABLE VA;Lo;0;L;;;;;N;;;;;
+1269;ETHIOPIC SYLLABLE VU;Lo;0;L;;;;;N;;;;;
+126A;ETHIOPIC SYLLABLE VI;Lo;0;L;;;;;N;;;;;
+126B;ETHIOPIC SYLLABLE VAA;Lo;0;L;;;;;N;;;;;
+126C;ETHIOPIC SYLLABLE VEE;Lo;0;L;;;;;N;;;;;
+126D;ETHIOPIC SYLLABLE VE;Lo;0;L;;;;;N;;;;;
+126E;ETHIOPIC SYLLABLE VO;Lo;0;L;;;;;N;;;;;
+126F;ETHIOPIC SYLLABLE VWA;Lo;0;L;;;;;N;;;;;
+1270;ETHIOPIC SYLLABLE TA;Lo;0;L;;;;;N;;;;;
+1271;ETHIOPIC SYLLABLE TU;Lo;0;L;;;;;N;;;;;
+1272;ETHIOPIC SYLLABLE TI;Lo;0;L;;;;;N;;;;;
+1273;ETHIOPIC SYLLABLE TAA;Lo;0;L;;;;;N;;;;;
+1274;ETHIOPIC SYLLABLE TEE;Lo;0;L;;;;;N;;;;;
+1275;ETHIOPIC SYLLABLE TE;Lo;0;L;;;;;N;;;;;
+1276;ETHIOPIC SYLLABLE TO;Lo;0;L;;;;;N;;;;;
+1277;ETHIOPIC SYLLABLE TWA;Lo;0;L;;;;;N;;;;;
+1278;ETHIOPIC SYLLABLE CA;Lo;0;L;;;;;N;;;;;
+1279;ETHIOPIC SYLLABLE CU;Lo;0;L;;;;;N;;;;;
+127A;ETHIOPIC SYLLABLE CI;Lo;0;L;;;;;N;;;;;
+127B;ETHIOPIC SYLLABLE CAA;Lo;0;L;;;;;N;;;;;
+127C;ETHIOPIC SYLLABLE CEE;Lo;0;L;;;;;N;;;;;
+127D;ETHIOPIC SYLLABLE CE;Lo;0;L;;;;;N;;;;;
+127E;ETHIOPIC SYLLABLE CO;Lo;0;L;;;;;N;;;;;
+127F;ETHIOPIC SYLLABLE CWA;Lo;0;L;;;;;N;;;;;
+1280;ETHIOPIC SYLLABLE XA;Lo;0;L;;;;;N;;;;;
+1281;ETHIOPIC SYLLABLE XU;Lo;0;L;;;;;N;;;;;
+1282;ETHIOPIC SYLLABLE XI;Lo;0;L;;;;;N;;;;;
+1283;ETHIOPIC SYLLABLE XAA;Lo;0;L;;;;;N;;;;;
+1284;ETHIOPIC SYLLABLE XEE;Lo;0;L;;;;;N;;;;;
+1285;ETHIOPIC SYLLABLE XE;Lo;0;L;;;;;N;;;;;
+1286;ETHIOPIC SYLLABLE XO;Lo;0;L;;;;;N;;;;;
+1287;ETHIOPIC SYLLABLE XOA;Lo;0;L;;;;;N;;;;;
+1288;ETHIOPIC SYLLABLE XWA;Lo;0;L;;;;;N;;;;;
+128A;ETHIOPIC SYLLABLE XWI;Lo;0;L;;;;;N;;;;;
+128B;ETHIOPIC SYLLABLE XWAA;Lo;0;L;;;;;N;;;;;
+128C;ETHIOPIC SYLLABLE XWEE;Lo;0;L;;;;;N;;;;;
+128D;ETHIOPIC SYLLABLE XWE;Lo;0;L;;;;;N;;;;;
+1290;ETHIOPIC SYLLABLE NA;Lo;0;L;;;;;N;;;;;
+1291;ETHIOPIC SYLLABLE NU;Lo;0;L;;;;;N;;;;;
+1292;ETHIOPIC SYLLABLE NI;Lo;0;L;;;;;N;;;;;
+1293;ETHIOPIC SYLLABLE NAA;Lo;0;L;;;;;N;;;;;
+1294;ETHIOPIC SYLLABLE NEE;Lo;0;L;;;;;N;;;;;
+1295;ETHIOPIC SYLLABLE NE;Lo;0;L;;;;;N;;;;;
+1296;ETHIOPIC SYLLABLE NO;Lo;0;L;;;;;N;;;;;
+1297;ETHIOPIC SYLLABLE NWA;Lo;0;L;;;;;N;;;;;
+1298;ETHIOPIC SYLLABLE NYA;Lo;0;L;;;;;N;;;;;
+1299;ETHIOPIC SYLLABLE NYU;Lo;0;L;;;;;N;;;;;
+129A;ETHIOPIC SYLLABLE NYI;Lo;0;L;;;;;N;;;;;
+129B;ETHIOPIC SYLLABLE NYAA;Lo;0;L;;;;;N;;;;;
+129C;ETHIOPIC SYLLABLE NYEE;Lo;0;L;;;;;N;;;;;
+129D;ETHIOPIC SYLLABLE NYE;Lo;0;L;;;;;N;;;;;
+129E;ETHIOPIC SYLLABLE NYO;Lo;0;L;;;;;N;;;;;
+129F;ETHIOPIC SYLLABLE NYWA;Lo;0;L;;;;;N;;;;;
+12A0;ETHIOPIC SYLLABLE GLOTTAL A;Lo;0;L;;;;;N;;;;;
+12A1;ETHIOPIC SYLLABLE GLOTTAL U;Lo;0;L;;;;;N;;;;;
+12A2;ETHIOPIC SYLLABLE GLOTTAL I;Lo;0;L;;;;;N;;;;;
+12A3;ETHIOPIC SYLLABLE GLOTTAL AA;Lo;0;L;;;;;N;;;;;
+12A4;ETHIOPIC SYLLABLE GLOTTAL EE;Lo;0;L;;;;;N;;;;;
+12A5;ETHIOPIC SYLLABLE GLOTTAL E;Lo;0;L;;;;;N;;;;;
+12A6;ETHIOPIC SYLLABLE GLOTTAL O;Lo;0;L;;;;;N;;;;;
+12A7;ETHIOPIC SYLLABLE GLOTTAL WA;Lo;0;L;;;;;N;;;;;
+12A8;ETHIOPIC SYLLABLE KA;Lo;0;L;;;;;N;;;;;
+12A9;ETHIOPIC SYLLABLE KU;Lo;0;L;;;;;N;;;;;
+12AA;ETHIOPIC SYLLABLE KI;Lo;0;L;;;;;N;;;;;
+12AB;ETHIOPIC SYLLABLE KAA;Lo;0;L;;;;;N;;;;;
+12AC;ETHIOPIC SYLLABLE KEE;Lo;0;L;;;;;N;;;;;
+12AD;ETHIOPIC SYLLABLE KE;Lo;0;L;;;;;N;;;;;
+12AE;ETHIOPIC SYLLABLE KO;Lo;0;L;;;;;N;;;;;
+12AF;ETHIOPIC SYLLABLE KOA;Lo;0;L;;;;;N;;;;;
+12B0;ETHIOPIC SYLLABLE KWA;Lo;0;L;;;;;N;;;;;
+12B2;ETHIOPIC SYLLABLE KWI;Lo;0;L;;;;;N;;;;;
+12B3;ETHIOPIC SYLLABLE KWAA;Lo;0;L;;;;;N;;;;;
+12B4;ETHIOPIC SYLLABLE KWEE;Lo;0;L;;;;;N;;;;;
+12B5;ETHIOPIC SYLLABLE KWE;Lo;0;L;;;;;N;;;;;
+12B8;ETHIOPIC SYLLABLE KXA;Lo;0;L;;;;;N;;;;;
+12B9;ETHIOPIC SYLLABLE KXU;Lo;0;L;;;;;N;;;;;
+12BA;ETHIOPIC SYLLABLE KXI;Lo;0;L;;;;;N;;;;;
+12BB;ETHIOPIC SYLLABLE KXAA;Lo;0;L;;;;;N;;;;;
+12BC;ETHIOPIC SYLLABLE KXEE;Lo;0;L;;;;;N;;;;;
+12BD;ETHIOPIC SYLLABLE KXE;Lo;0;L;;;;;N;;;;;
+12BE;ETHIOPIC SYLLABLE KXO;Lo;0;L;;;;;N;;;;;
+12C0;ETHIOPIC SYLLABLE KXWA;Lo;0;L;;;;;N;;;;;
+12C2;ETHIOPIC SYLLABLE KXWI;Lo;0;L;;;;;N;;;;;
+12C3;ETHIOPIC SYLLABLE KXWAA;Lo;0;L;;;;;N;;;;;
+12C4;ETHIOPIC SYLLABLE KXWEE;Lo;0;L;;;;;N;;;;;
+12C5;ETHIOPIC SYLLABLE KXWE;Lo;0;L;;;;;N;;;;;
+12C8;ETHIOPIC SYLLABLE WA;Lo;0;L;;;;;N;;;;;
+12C9;ETHIOPIC SYLLABLE WU;Lo;0;L;;;;;N;;;;;
+12CA;ETHIOPIC SYLLABLE WI;Lo;0;L;;;;;N;;;;;
+12CB;ETHIOPIC SYLLABLE WAA;Lo;0;L;;;;;N;;;;;
+12CC;ETHIOPIC SYLLABLE WEE;Lo;0;L;;;;;N;;;;;
+12CD;ETHIOPIC SYLLABLE WE;Lo;0;L;;;;;N;;;;;
+12CE;ETHIOPIC SYLLABLE WO;Lo;0;L;;;;;N;;;;;
+12CF;ETHIOPIC SYLLABLE WOA;Lo;0;L;;;;;N;;;;;
+12D0;ETHIOPIC SYLLABLE PHARYNGEAL A;Lo;0;L;;;;;N;;;;;
+12D1;ETHIOPIC SYLLABLE PHARYNGEAL U;Lo;0;L;;;;;N;;;;;
+12D2;ETHIOPIC SYLLABLE PHARYNGEAL I;Lo;0;L;;;;;N;;;;;
+12D3;ETHIOPIC SYLLABLE PHARYNGEAL AA;Lo;0;L;;;;;N;;;;;
+12D4;ETHIOPIC SYLLABLE PHARYNGEAL EE;Lo;0;L;;;;;N;;;;;
+12D5;ETHIOPIC SYLLABLE PHARYNGEAL E;Lo;0;L;;;;;N;;;;;
+12D6;ETHIOPIC SYLLABLE PHARYNGEAL O;Lo;0;L;;;;;N;;;;;
+12D8;ETHIOPIC SYLLABLE ZA;Lo;0;L;;;;;N;;;;;
+12D9;ETHIOPIC SYLLABLE ZU;Lo;0;L;;;;;N;;;;;
+12DA;ETHIOPIC SYLLABLE ZI;Lo;0;L;;;;;N;;;;;
+12DB;ETHIOPIC SYLLABLE ZAA;Lo;0;L;;;;;N;;;;;
+12DC;ETHIOPIC SYLLABLE ZEE;Lo;0;L;;;;;N;;;;;
+12DD;ETHIOPIC SYLLABLE ZE;Lo;0;L;;;;;N;;;;;
+12DE;ETHIOPIC SYLLABLE ZO;Lo;0;L;;;;;N;;;;;
+12DF;ETHIOPIC SYLLABLE ZWA;Lo;0;L;;;;;N;;;;;
+12E0;ETHIOPIC SYLLABLE ZHA;Lo;0;L;;;;;N;;;;;
+12E1;ETHIOPIC SYLLABLE ZHU;Lo;0;L;;;;;N;;;;;
+12E2;ETHIOPIC SYLLABLE ZHI;Lo;0;L;;;;;N;;;;;
+12E3;ETHIOPIC SYLLABLE ZHAA;Lo;0;L;;;;;N;;;;;
+12E4;ETHIOPIC SYLLABLE ZHEE;Lo;0;L;;;;;N;;;;;
+12E5;ETHIOPIC SYLLABLE ZHE;Lo;0;L;;;;;N;;;;;
+12E6;ETHIOPIC SYLLABLE ZHO;Lo;0;L;;;;;N;;;;;
+12E7;ETHIOPIC SYLLABLE ZHWA;Lo;0;L;;;;;N;;;;;
+12E8;ETHIOPIC SYLLABLE YA;Lo;0;L;;;;;N;;;;;
+12E9;ETHIOPIC SYLLABLE YU;Lo;0;L;;;;;N;;;;;
+12EA;ETHIOPIC SYLLABLE YI;Lo;0;L;;;;;N;;;;;
+12EB;ETHIOPIC SYLLABLE YAA;Lo;0;L;;;;;N;;;;;
+12EC;ETHIOPIC SYLLABLE YEE;Lo;0;L;;;;;N;;;;;
+12ED;ETHIOPIC SYLLABLE YE;Lo;0;L;;;;;N;;;;;
+12EE;ETHIOPIC SYLLABLE YO;Lo;0;L;;;;;N;;;;;
+12EF;ETHIOPIC SYLLABLE YOA;Lo;0;L;;;;;N;;;;;
+12F0;ETHIOPIC SYLLABLE DA;Lo;0;L;;;;;N;;;;;
+12F1;ETHIOPIC SYLLABLE DU;Lo;0;L;;;;;N;;;;;
+12F2;ETHIOPIC SYLLABLE DI;Lo;0;L;;;;;N;;;;;
+12F3;ETHIOPIC SYLLABLE DAA;Lo;0;L;;;;;N;;;;;
+12F4;ETHIOPIC SYLLABLE DEE;Lo;0;L;;;;;N;;;;;
+12F5;ETHIOPIC SYLLABLE DE;Lo;0;L;;;;;N;;;;;
+12F6;ETHIOPIC SYLLABLE DO;Lo;0;L;;;;;N;;;;;
+12F7;ETHIOPIC SYLLABLE DWA;Lo;0;L;;;;;N;;;;;
+12F8;ETHIOPIC SYLLABLE DDA;Lo;0;L;;;;;N;;;;;
+12F9;ETHIOPIC SYLLABLE DDU;Lo;0;L;;;;;N;;;;;
+12FA;ETHIOPIC SYLLABLE DDI;Lo;0;L;;;;;N;;;;;
+12FB;ETHIOPIC SYLLABLE DDAA;Lo;0;L;;;;;N;;;;;
+12FC;ETHIOPIC SYLLABLE DDEE;Lo;0;L;;;;;N;;;;;
+12FD;ETHIOPIC SYLLABLE DDE;Lo;0;L;;;;;N;;;;;
+12FE;ETHIOPIC SYLLABLE DDO;Lo;0;L;;;;;N;;;;;
+12FF;ETHIOPIC SYLLABLE DDWA;Lo;0;L;;;;;N;;;;;
+1300;ETHIOPIC SYLLABLE JA;Lo;0;L;;;;;N;;;;;
+1301;ETHIOPIC SYLLABLE JU;Lo;0;L;;;;;N;;;;;
+1302;ETHIOPIC SYLLABLE JI;Lo;0;L;;;;;N;;;;;
+1303;ETHIOPIC SYLLABLE JAA;Lo;0;L;;;;;N;;;;;
+1304;ETHIOPIC SYLLABLE JEE;Lo;0;L;;;;;N;;;;;
+1305;ETHIOPIC SYLLABLE JE;Lo;0;L;;;;;N;;;;;
+1306;ETHIOPIC SYLLABLE JO;Lo;0;L;;;;;N;;;;;
+1307;ETHIOPIC SYLLABLE JWA;Lo;0;L;;;;;N;;;;;
+1308;ETHIOPIC SYLLABLE GA;Lo;0;L;;;;;N;;;;;
+1309;ETHIOPIC SYLLABLE GU;Lo;0;L;;;;;N;;;;;
+130A;ETHIOPIC SYLLABLE GI;Lo;0;L;;;;;N;;;;;
+130B;ETHIOPIC SYLLABLE GAA;Lo;0;L;;;;;N;;;;;
+130C;ETHIOPIC SYLLABLE GEE;Lo;0;L;;;;;N;;;;;
+130D;ETHIOPIC SYLLABLE GE;Lo;0;L;;;;;N;;;;;
+130E;ETHIOPIC SYLLABLE GO;Lo;0;L;;;;;N;;;;;
+130F;ETHIOPIC SYLLABLE GOA;Lo;0;L;;;;;N;;;;;
+1310;ETHIOPIC SYLLABLE GWA;Lo;0;L;;;;;N;;;;;
+1312;ETHIOPIC SYLLABLE GWI;Lo;0;L;;;;;N;;;;;
+1313;ETHIOPIC SYLLABLE GWAA;Lo;0;L;;;;;N;;;;;
+1314;ETHIOPIC SYLLABLE GWEE;Lo;0;L;;;;;N;;;;;
+1315;ETHIOPIC SYLLABLE GWE;Lo;0;L;;;;;N;;;;;
+1318;ETHIOPIC SYLLABLE GGA;Lo;0;L;;;;;N;;;;;
+1319;ETHIOPIC SYLLABLE GGU;Lo;0;L;;;;;N;;;;;
+131A;ETHIOPIC SYLLABLE GGI;Lo;0;L;;;;;N;;;;;
+131B;ETHIOPIC SYLLABLE GGAA;Lo;0;L;;;;;N;;;;;
+131C;ETHIOPIC SYLLABLE GGEE;Lo;0;L;;;;;N;;;;;
+131D;ETHIOPIC SYLLABLE GGE;Lo;0;L;;;;;N;;;;;
+131E;ETHIOPIC SYLLABLE GGO;Lo;0;L;;;;;N;;;;;
+131F;ETHIOPIC SYLLABLE GGWAA;Lo;0;L;;;;;N;;;;;
+1320;ETHIOPIC SYLLABLE THA;Lo;0;L;;;;;N;;;;;
+1321;ETHIOPIC SYLLABLE THU;Lo;0;L;;;;;N;;;;;
+1322;ETHIOPIC SYLLABLE THI;Lo;0;L;;;;;N;;;;;
+1323;ETHIOPIC SYLLABLE THAA;Lo;0;L;;;;;N;;;;;
+1324;ETHIOPIC SYLLABLE THEE;Lo;0;L;;;;;N;;;;;
+1325;ETHIOPIC SYLLABLE THE;Lo;0;L;;;;;N;;;;;
+1326;ETHIOPIC SYLLABLE THO;Lo;0;L;;;;;N;;;;;
+1327;ETHIOPIC SYLLABLE THWA;Lo;0;L;;;;;N;;;;;
+1328;ETHIOPIC SYLLABLE CHA;Lo;0;L;;;;;N;;;;;
+1329;ETHIOPIC SYLLABLE CHU;Lo;0;L;;;;;N;;;;;
+132A;ETHIOPIC SYLLABLE CHI;Lo;0;L;;;;;N;;;;;
+132B;ETHIOPIC SYLLABLE CHAA;Lo;0;L;;;;;N;;;;;
+132C;ETHIOPIC SYLLABLE CHEE;Lo;0;L;;;;;N;;;;;
+132D;ETHIOPIC SYLLABLE CHE;Lo;0;L;;;;;N;;;;;
+132E;ETHIOPIC SYLLABLE CHO;Lo;0;L;;;;;N;;;;;
+132F;ETHIOPIC SYLLABLE CHWA;Lo;0;L;;;;;N;;;;;
+1330;ETHIOPIC SYLLABLE PHA;Lo;0;L;;;;;N;;;;;
+1331;ETHIOPIC SYLLABLE PHU;Lo;0;L;;;;;N;;;;;
+1332;ETHIOPIC SYLLABLE PHI;Lo;0;L;;;;;N;;;;;
+1333;ETHIOPIC SYLLABLE PHAA;Lo;0;L;;;;;N;;;;;
+1334;ETHIOPIC SYLLABLE PHEE;Lo;0;L;;;;;N;;;;;
+1335;ETHIOPIC SYLLABLE PHE;Lo;0;L;;;;;N;;;;;
+1336;ETHIOPIC SYLLABLE PHO;Lo;0;L;;;;;N;;;;;
+1337;ETHIOPIC SYLLABLE PHWA;Lo;0;L;;;;;N;;;;;
+1338;ETHIOPIC SYLLABLE TSA;Lo;0;L;;;;;N;;;;;
+1339;ETHIOPIC SYLLABLE TSU;Lo;0;L;;;;;N;;;;;
+133A;ETHIOPIC SYLLABLE TSI;Lo;0;L;;;;;N;;;;;
+133B;ETHIOPIC SYLLABLE TSAA;Lo;0;L;;;;;N;;;;;
+133C;ETHIOPIC SYLLABLE TSEE;Lo;0;L;;;;;N;;;;;
+133D;ETHIOPIC SYLLABLE TSE;Lo;0;L;;;;;N;;;;;
+133E;ETHIOPIC SYLLABLE TSO;Lo;0;L;;;;;N;;;;;
+133F;ETHIOPIC SYLLABLE TSWA;Lo;0;L;;;;;N;;;;;
+1340;ETHIOPIC SYLLABLE TZA;Lo;0;L;;;;;N;;;;;
+1341;ETHIOPIC SYLLABLE TZU;Lo;0;L;;;;;N;;;;;
+1342;ETHIOPIC SYLLABLE TZI;Lo;0;L;;;;;N;;;;;
+1343;ETHIOPIC SYLLABLE TZAA;Lo;0;L;;;;;N;;;;;
+1344;ETHIOPIC SYLLABLE TZEE;Lo;0;L;;;;;N;;;;;
+1345;ETHIOPIC SYLLABLE TZE;Lo;0;L;;;;;N;;;;;
+1346;ETHIOPIC SYLLABLE TZO;Lo;0;L;;;;;N;;;;;
+1347;ETHIOPIC SYLLABLE TZOA;Lo;0;L;;;;;N;;;;;
+1348;ETHIOPIC SYLLABLE FA;Lo;0;L;;;;;N;;;;;
+1349;ETHIOPIC SYLLABLE FU;Lo;0;L;;;;;N;;;;;
+134A;ETHIOPIC SYLLABLE FI;Lo;0;L;;;;;N;;;;;
+134B;ETHIOPIC SYLLABLE FAA;Lo;0;L;;;;;N;;;;;
+134C;ETHIOPIC SYLLABLE FEE;Lo;0;L;;;;;N;;;;;
+134D;ETHIOPIC SYLLABLE FE;Lo;0;L;;;;;N;;;;;
+134E;ETHIOPIC SYLLABLE FO;Lo;0;L;;;;;N;;;;;
+134F;ETHIOPIC SYLLABLE FWA;Lo;0;L;;;;;N;;;;;
+1350;ETHIOPIC SYLLABLE PA;Lo;0;L;;;;;N;;;;;
+1351;ETHIOPIC SYLLABLE PU;Lo;0;L;;;;;N;;;;;
+1352;ETHIOPIC SYLLABLE PI;Lo;0;L;;;;;N;;;;;
+1353;ETHIOPIC SYLLABLE PAA;Lo;0;L;;;;;N;;;;;
+1354;ETHIOPIC SYLLABLE PEE;Lo;0;L;;;;;N;;;;;
+1355;ETHIOPIC SYLLABLE PE;Lo;0;L;;;;;N;;;;;
+1356;ETHIOPIC SYLLABLE PO;Lo;0;L;;;;;N;;;;;
+1357;ETHIOPIC SYLLABLE PWA;Lo;0;L;;;;;N;;;;;
+1358;ETHIOPIC SYLLABLE RYA;Lo;0;L;;;;;N;;;;;
+1359;ETHIOPIC SYLLABLE MYA;Lo;0;L;;;;;N;;;;;
+135A;ETHIOPIC SYLLABLE FYA;Lo;0;L;;;;;N;;;;;
+135D;ETHIOPIC COMBINING GEMINATION AND VOWEL LENGTH MARK;Mn;230;NSM;;;;;N;;;;;
+135E;ETHIOPIC COMBINING VOWEL LENGTH MARK;Mn;230;NSM;;;;;N;;;;;
+135F;ETHIOPIC COMBINING GEMINATION MARK;Mn;230;NSM;;;;;N;;;;;
+1360;ETHIOPIC SECTION MARK;Po;0;L;;;;;N;;;;;
+1361;ETHIOPIC WORDSPACE;Po;0;L;;;;;N;;;;;
+1362;ETHIOPIC FULL STOP;Po;0;L;;;;;N;;;;;
+1363;ETHIOPIC COMMA;Po;0;L;;;;;N;;;;;
+1364;ETHIOPIC SEMICOLON;Po;0;L;;;;;N;;;;;
+1365;ETHIOPIC COLON;Po;0;L;;;;;N;;;;;
+1366;ETHIOPIC PREFACE COLON;Po;0;L;;;;;N;;;;;
+1367;ETHIOPIC QUESTION MARK;Po;0;L;;;;;N;;;;;
+1368;ETHIOPIC PARAGRAPH SEPARATOR;Po;0;L;;;;;N;;;;;
+1369;ETHIOPIC DIGIT ONE;No;0;L;;;1;1;N;;;;;
+136A;ETHIOPIC DIGIT TWO;No;0;L;;;2;2;N;;;;;
+136B;ETHIOPIC DIGIT THREE;No;0;L;;;3;3;N;;;;;
+136C;ETHIOPIC DIGIT FOUR;No;0;L;;;4;4;N;;;;;
+136D;ETHIOPIC DIGIT FIVE;No;0;L;;;5;5;N;;;;;
+136E;ETHIOPIC DIGIT SIX;No;0;L;;;6;6;N;;;;;
+136F;ETHIOPIC DIGIT SEVEN;No;0;L;;;7;7;N;;;;;
+1370;ETHIOPIC DIGIT EIGHT;No;0;L;;;8;8;N;;;;;
+1371;ETHIOPIC DIGIT NINE;No;0;L;;;9;9;N;;;;;
+1372;ETHIOPIC NUMBER TEN;No;0;L;;;;10;N;;;;;
+1373;ETHIOPIC NUMBER TWENTY;No;0;L;;;;20;N;;;;;
+1374;ETHIOPIC NUMBER THIRTY;No;0;L;;;;30;N;;;;;
+1375;ETHIOPIC NUMBER FORTY;No;0;L;;;;40;N;;;;;
+1376;ETHIOPIC NUMBER FIFTY;No;0;L;;;;50;N;;;;;
+1377;ETHIOPIC NUMBER SIXTY;No;0;L;;;;60;N;;;;;
+1378;ETHIOPIC NUMBER SEVENTY;No;0;L;;;;70;N;;;;;
+1379;ETHIOPIC NUMBER EIGHTY;No;0;L;;;;80;N;;;;;
+137A;ETHIOPIC NUMBER NINETY;No;0;L;;;;90;N;;;;;
+137B;ETHIOPIC NUMBER HUNDRED;No;0;L;;;;100;N;;;;;
+137C;ETHIOPIC NUMBER TEN THOUSAND;No;0;L;;;;10000;N;;;;;
+1380;ETHIOPIC SYLLABLE SEBATBEIT MWA;Lo;0;L;;;;;N;;;;;
+1381;ETHIOPIC SYLLABLE MWI;Lo;0;L;;;;;N;;;;;
+1382;ETHIOPIC SYLLABLE MWEE;Lo;0;L;;;;;N;;;;;
+1383;ETHIOPIC SYLLABLE MWE;Lo;0;L;;;;;N;;;;;
+1384;ETHIOPIC SYLLABLE SEBATBEIT BWA;Lo;0;L;;;;;N;;;;;
+1385;ETHIOPIC SYLLABLE BWI;Lo;0;L;;;;;N;;;;;
+1386;ETHIOPIC SYLLABLE BWEE;Lo;0;L;;;;;N;;;;;
+1387;ETHIOPIC SYLLABLE BWE;Lo;0;L;;;;;N;;;;;
+1388;ETHIOPIC SYLLABLE SEBATBEIT FWA;Lo;0;L;;;;;N;;;;;
+1389;ETHIOPIC SYLLABLE FWI;Lo;0;L;;;;;N;;;;;
+138A;ETHIOPIC SYLLABLE FWEE;Lo;0;L;;;;;N;;;;;
+138B;ETHIOPIC SYLLABLE FWE;Lo;0;L;;;;;N;;;;;
+138C;ETHIOPIC SYLLABLE SEBATBEIT PWA;Lo;0;L;;;;;N;;;;;
+138D;ETHIOPIC SYLLABLE PWI;Lo;0;L;;;;;N;;;;;
+138E;ETHIOPIC SYLLABLE PWEE;Lo;0;L;;;;;N;;;;;
+138F;ETHIOPIC SYLLABLE PWE;Lo;0;L;;;;;N;;;;;
+1390;ETHIOPIC TONAL MARK YIZET;So;0;ON;;;;;N;;;;;
+1391;ETHIOPIC TONAL MARK DERET;So;0;ON;;;;;N;;;;;
+1392;ETHIOPIC TONAL MARK RIKRIK;So;0;ON;;;;;N;;;;;
+1393;ETHIOPIC TONAL MARK SHORT RIKRIK;So;0;ON;;;;;N;;;;;
+1394;ETHIOPIC TONAL MARK DIFAT;So;0;ON;;;;;N;;;;;
+1395;ETHIOPIC TONAL MARK KENAT;So;0;ON;;;;;N;;;;;
+1396;ETHIOPIC TONAL MARK CHIRET;So;0;ON;;;;;N;;;;;
+1397;ETHIOPIC TONAL MARK HIDET;So;0;ON;;;;;N;;;;;
+1398;ETHIOPIC TONAL MARK DERET-HIDET;So;0;ON;;;;;N;;;;;
+1399;ETHIOPIC TONAL MARK KURT;So;0;ON;;;;;N;;;;;
+13A0;CHEROKEE LETTER A;Lu;0;L;;;;;N;;;;AB70;
+13A1;CHEROKEE LETTER E;Lu;0;L;;;;;N;;;;AB71;
+13A2;CHEROKEE LETTER I;Lu;0;L;;;;;N;;;;AB72;
+13A3;CHEROKEE LETTER O;Lu;0;L;;;;;N;;;;AB73;
+13A4;CHEROKEE LETTER U;Lu;0;L;;;;;N;;;;AB74;
+13A5;CHEROKEE LETTER V;Lu;0;L;;;;;N;;;;AB75;
+13A6;CHEROKEE LETTER GA;Lu;0;L;;;;;N;;;;AB76;
+13A7;CHEROKEE LETTER KA;Lu;0;L;;;;;N;;;;AB77;
+13A8;CHEROKEE LETTER GE;Lu;0;L;;;;;N;;;;AB78;
+13A9;CHEROKEE LETTER GI;Lu;0;L;;;;;N;;;;AB79;
+13AA;CHEROKEE LETTER GO;Lu;0;L;;;;;N;;;;AB7A;
+13AB;CHEROKEE LETTER GU;Lu;0;L;;;;;N;;;;AB7B;
+13AC;CHEROKEE LETTER GV;Lu;0;L;;;;;N;;;;AB7C;
+13AD;CHEROKEE LETTER HA;Lu;0;L;;;;;N;;;;AB7D;
+13AE;CHEROKEE LETTER HE;Lu;0;L;;;;;N;;;;AB7E;
+13AF;CHEROKEE LETTER HI;Lu;0;L;;;;;N;;;;AB7F;
+13B0;CHEROKEE LETTER HO;Lu;0;L;;;;;N;;;;AB80;
+13B1;CHEROKEE LETTER HU;Lu;0;L;;;;;N;;;;AB81;
+13B2;CHEROKEE LETTER HV;Lu;0;L;;;;;N;;;;AB82;
+13B3;CHEROKEE LETTER LA;Lu;0;L;;;;;N;;;;AB83;
+13B4;CHEROKEE LETTER LE;Lu;0;L;;;;;N;;;;AB84;
+13B5;CHEROKEE LETTER LI;Lu;0;L;;;;;N;;;;AB85;
+13B6;CHEROKEE LETTER LO;Lu;0;L;;;;;N;;;;AB86;
+13B7;CHEROKEE LETTER LU;Lu;0;L;;;;;N;;;;AB87;
+13B8;CHEROKEE LETTER LV;Lu;0;L;;;;;N;;;;AB88;
+13B9;CHEROKEE LETTER MA;Lu;0;L;;;;;N;;;;AB89;
+13BA;CHEROKEE LETTER ME;Lu;0;L;;;;;N;;;;AB8A;
+13BB;CHEROKEE LETTER MI;Lu;0;L;;;;;N;;;;AB8B;
+13BC;CHEROKEE LETTER MO;Lu;0;L;;;;;N;;;;AB8C;
+13BD;CHEROKEE LETTER MU;Lu;0;L;;;;;N;;;;AB8D;
+13BE;CHEROKEE LETTER NA;Lu;0;L;;;;;N;;;;AB8E;
+13BF;CHEROKEE LETTER HNA;Lu;0;L;;;;;N;;;;AB8F;
+13C0;CHEROKEE LETTER NAH;Lu;0;L;;;;;N;;;;AB90;
+13C1;CHEROKEE LETTER NE;Lu;0;L;;;;;N;;;;AB91;
+13C2;CHEROKEE LETTER NI;Lu;0;L;;;;;N;;;;AB92;
+13C3;CHEROKEE LETTER NO;Lu;0;L;;;;;N;;;;AB93;
+13C4;CHEROKEE LETTER NU;Lu;0;L;;;;;N;;;;AB94;
+13C5;CHEROKEE LETTER NV;Lu;0;L;;;;;N;;;;AB95;
+13C6;CHEROKEE LETTER QUA;Lu;0;L;;;;;N;;;;AB96;
+13C7;CHEROKEE LETTER QUE;Lu;0;L;;;;;N;;;;AB97;
+13C8;CHEROKEE LETTER QUI;Lu;0;L;;;;;N;;;;AB98;
+13C9;CHEROKEE LETTER QUO;Lu;0;L;;;;;N;;;;AB99;
+13CA;CHEROKEE LETTER QUU;Lu;0;L;;;;;N;;;;AB9A;
+13CB;CHEROKEE LETTER QUV;Lu;0;L;;;;;N;;;;AB9B;
+13CC;CHEROKEE LETTER SA;Lu;0;L;;;;;N;;;;AB9C;
+13CD;CHEROKEE LETTER S;Lu;0;L;;;;;N;;;;AB9D;
+13CE;CHEROKEE LETTER SE;Lu;0;L;;;;;N;;;;AB9E;
+13CF;CHEROKEE LETTER SI;Lu;0;L;;;;;N;;;;AB9F;
+13D0;CHEROKEE LETTER SO;Lu;0;L;;;;;N;;;;ABA0;
+13D1;CHEROKEE LETTER SU;Lu;0;L;;;;;N;;;;ABA1;
+13D2;CHEROKEE LETTER SV;Lu;0;L;;;;;N;;;;ABA2;
+13D3;CHEROKEE LETTER DA;Lu;0;L;;;;;N;;;;ABA3;
+13D4;CHEROKEE LETTER TA;Lu;0;L;;;;;N;;;;ABA4;
+13D5;CHEROKEE LETTER DE;Lu;0;L;;;;;N;;;;ABA5;
+13D6;CHEROKEE LETTER TE;Lu;0;L;;;;;N;;;;ABA6;
+13D7;CHEROKEE LETTER DI;Lu;0;L;;;;;N;;;;ABA7;
+13D8;CHEROKEE LETTER TI;Lu;0;L;;;;;N;;;;ABA8;
+13D9;CHEROKEE LETTER DO;Lu;0;L;;;;;N;;;;ABA9;
+13DA;CHEROKEE LETTER DU;Lu;0;L;;;;;N;;;;ABAA;
+13DB;CHEROKEE LETTER DV;Lu;0;L;;;;;N;;;;ABAB;
+13DC;CHEROKEE LETTER DLA;Lu;0;L;;;;;N;;;;ABAC;
+13DD;CHEROKEE LETTER TLA;Lu;0;L;;;;;N;;;;ABAD;
+13DE;CHEROKEE LETTER TLE;Lu;0;L;;;;;N;;;;ABAE;
+13DF;CHEROKEE LETTER TLI;Lu;0;L;;;;;N;;;;ABAF;
+13E0;CHEROKEE LETTER TLO;Lu;0;L;;;;;N;;;;ABB0;
+13E1;CHEROKEE LETTER TLU;Lu;0;L;;;;;N;;;;ABB1;
+13E2;CHEROKEE LETTER TLV;Lu;0;L;;;;;N;;;;ABB2;
+13E3;CHEROKEE LETTER TSA;Lu;0;L;;;;;N;;;;ABB3;
+13E4;CHEROKEE LETTER TSE;Lu;0;L;;;;;N;;;;ABB4;
+13E5;CHEROKEE LETTER TSI;Lu;0;L;;;;;N;;;;ABB5;
+13E6;CHEROKEE LETTER TSO;Lu;0;L;;;;;N;;;;ABB6;
+13E7;CHEROKEE LETTER TSU;Lu;0;L;;;;;N;;;;ABB7;
+13E8;CHEROKEE LETTER TSV;Lu;0;L;;;;;N;;;;ABB8;
+13E9;CHEROKEE LETTER WA;Lu;0;L;;;;;N;;;;ABB9;
+13EA;CHEROKEE LETTER WE;Lu;0;L;;;;;N;;;;ABBA;
+13EB;CHEROKEE LETTER WI;Lu;0;L;;;;;N;;;;ABBB;
+13EC;CHEROKEE LETTER WO;Lu;0;L;;;;;N;;;;ABBC;
+13ED;CHEROKEE LETTER WU;Lu;0;L;;;;;N;;;;ABBD;
+13EE;CHEROKEE LETTER WV;Lu;0;L;;;;;N;;;;ABBE;
+13EF;CHEROKEE LETTER YA;Lu;0;L;;;;;N;;;;ABBF;
+13F0;CHEROKEE LETTER YE;Lu;0;L;;;;;N;;;;13F8;
+13F1;CHEROKEE LETTER YI;Lu;0;L;;;;;N;;;;13F9;
+13F2;CHEROKEE LETTER YO;Lu;0;L;;;;;N;;;;13FA;
+13F3;CHEROKEE LETTER YU;Lu;0;L;;;;;N;;;;13FB;
+13F4;CHEROKEE LETTER YV;Lu;0;L;;;;;N;;;;13FC;
+13F5;CHEROKEE LETTER MV;Lu;0;L;;;;;N;;;;13FD;
+13F8;CHEROKEE SMALL LETTER YE;Ll;0;L;;;;;N;;;13F0;;13F0
+13F9;CHEROKEE SMALL LETTER YI;Ll;0;L;;;;;N;;;13F1;;13F1
+13FA;CHEROKEE SMALL LETTER YO;Ll;0;L;;;;;N;;;13F2;;13F2
+13FB;CHEROKEE SMALL LETTER YU;Ll;0;L;;;;;N;;;13F3;;13F3
+13FC;CHEROKEE SMALL LETTER YV;Ll;0;L;;;;;N;;;13F4;;13F4
+13FD;CHEROKEE SMALL LETTER MV;Ll;0;L;;;;;N;;;13F5;;13F5
+1400;CANADIAN SYLLABICS HYPHEN;Pd;0;ON;;;;;N;;;;;
+1401;CANADIAN SYLLABICS E;Lo;0;L;;;;;N;;;;;
+1402;CANADIAN SYLLABICS AAI;Lo;0;L;;;;;N;;;;;
+1403;CANADIAN SYLLABICS I;Lo;0;L;;;;;N;;;;;
+1404;CANADIAN SYLLABICS II;Lo;0;L;;;;;N;;;;;
+1405;CANADIAN SYLLABICS O;Lo;0;L;;;;;N;;;;;
+1406;CANADIAN SYLLABICS OO;Lo;0;L;;;;;N;;;;;
+1407;CANADIAN SYLLABICS Y-CREE OO;Lo;0;L;;;;;N;;;;;
+1408;CANADIAN SYLLABICS CARRIER EE;Lo;0;L;;;;;N;;;;;
+1409;CANADIAN SYLLABICS CARRIER I;Lo;0;L;;;;;N;;;;;
+140A;CANADIAN SYLLABICS A;Lo;0;L;;;;;N;;;;;
+140B;CANADIAN SYLLABICS AA;Lo;0;L;;;;;N;;;;;
+140C;CANADIAN SYLLABICS WE;Lo;0;L;;;;;N;;;;;
+140D;CANADIAN SYLLABICS WEST-CREE WE;Lo;0;L;;;;;N;;;;;
+140E;CANADIAN SYLLABICS WI;Lo;0;L;;;;;N;;;;;
+140F;CANADIAN SYLLABICS WEST-CREE WI;Lo;0;L;;;;;N;;;;;
+1410;CANADIAN SYLLABICS WII;Lo;0;L;;;;;N;;;;;
+1411;CANADIAN SYLLABICS WEST-CREE WII;Lo;0;L;;;;;N;;;;;
+1412;CANADIAN SYLLABICS WO;Lo;0;L;;;;;N;;;;;
+1413;CANADIAN SYLLABICS WEST-CREE WO;Lo;0;L;;;;;N;;;;;
+1414;CANADIAN SYLLABICS WOO;Lo;0;L;;;;;N;;;;;
+1415;CANADIAN SYLLABICS WEST-CREE WOO;Lo;0;L;;;;;N;;;;;
+1416;CANADIAN SYLLABICS NASKAPI WOO;Lo;0;L;;;;;N;;;;;
+1417;CANADIAN SYLLABICS WA;Lo;0;L;;;;;N;;;;;
+1418;CANADIAN SYLLABICS WEST-CREE WA;Lo;0;L;;;;;N;;;;;
+1419;CANADIAN SYLLABICS WAA;Lo;0;L;;;;;N;;;;;
+141A;CANADIAN SYLLABICS WEST-CREE WAA;Lo;0;L;;;;;N;;;;;
+141B;CANADIAN SYLLABICS NASKAPI WAA;Lo;0;L;;;;;N;;;;;
+141C;CANADIAN SYLLABICS AI;Lo;0;L;;;;;N;;;;;
+141D;CANADIAN SYLLABICS Y-CREE W;Lo;0;L;;;;;N;;;;;
+141E;CANADIAN SYLLABICS GLOTTAL STOP;Lo;0;L;;;;;N;;;;;
+141F;CANADIAN SYLLABICS FINAL ACUTE;Lo;0;L;;;;;N;;;;;
+1420;CANADIAN SYLLABICS FINAL GRAVE;Lo;0;L;;;;;N;;;;;
+1421;CANADIAN SYLLABICS FINAL BOTTOM HALF RING;Lo;0;L;;;;;N;;;;;
+1422;CANADIAN SYLLABICS FINAL TOP HALF RING;Lo;0;L;;;;;N;;;;;
+1423;CANADIAN SYLLABICS FINAL RIGHT HALF RING;Lo;0;L;;;;;N;;;;;
+1424;CANADIAN SYLLABICS FINAL RING;Lo;0;L;;;;;N;;;;;
+1425;CANADIAN SYLLABICS FINAL DOUBLE ACUTE;Lo;0;L;;;;;N;;;;;
+1426;CANADIAN SYLLABICS FINAL DOUBLE SHORT VERTICAL STROKES;Lo;0;L;;;;;N;;;;;
+1427;CANADIAN SYLLABICS FINAL MIDDLE DOT;Lo;0;L;;;;;N;;;;;
+1428;CANADIAN SYLLABICS FINAL SHORT HORIZONTAL STROKE;Lo;0;L;;;;;N;;;;;
+1429;CANADIAN SYLLABICS FINAL PLUS;Lo;0;L;;;;;N;;;;;
+142A;CANADIAN SYLLABICS FINAL DOWN TACK;Lo;0;L;;;;;N;;;;;
+142B;CANADIAN SYLLABICS EN;Lo;0;L;;;;;N;;;;;
+142C;CANADIAN SYLLABICS IN;Lo;0;L;;;;;N;;;;;
+142D;CANADIAN SYLLABICS ON;Lo;0;L;;;;;N;;;;;
+142E;CANADIAN SYLLABICS AN;Lo;0;L;;;;;N;;;;;
+142F;CANADIAN SYLLABICS PE;Lo;0;L;;;;;N;;;;;
+1430;CANADIAN SYLLABICS PAAI;Lo;0;L;;;;;N;;;;;
+1431;CANADIAN SYLLABICS PI;Lo;0;L;;;;;N;;;;;
+1432;CANADIAN SYLLABICS PII;Lo;0;L;;;;;N;;;;;
+1433;CANADIAN SYLLABICS PO;Lo;0;L;;;;;N;;;;;
+1434;CANADIAN SYLLABICS POO;Lo;0;L;;;;;N;;;;;
+1435;CANADIAN SYLLABICS Y-CREE POO;Lo;0;L;;;;;N;;;;;
+1436;CANADIAN SYLLABICS CARRIER HEE;Lo;0;L;;;;;N;;;;;
+1437;CANADIAN SYLLABICS CARRIER HI;Lo;0;L;;;;;N;;;;;
+1438;CANADIAN SYLLABICS PA;Lo;0;L;;;;;N;;;;;
+1439;CANADIAN SYLLABICS PAA;Lo;0;L;;;;;N;;;;;
+143A;CANADIAN SYLLABICS PWE;Lo;0;L;;;;;N;;;;;
+143B;CANADIAN SYLLABICS WEST-CREE PWE;Lo;0;L;;;;;N;;;;;
+143C;CANADIAN SYLLABICS PWI;Lo;0;L;;;;;N;;;;;
+143D;CANADIAN SYLLABICS WEST-CREE PWI;Lo;0;L;;;;;N;;;;;
+143E;CANADIAN SYLLABICS PWII;Lo;0;L;;;;;N;;;;;
+143F;CANADIAN SYLLABICS WEST-CREE PWII;Lo;0;L;;;;;N;;;;;
+1440;CANADIAN SYLLABICS PWO;Lo;0;L;;;;;N;;;;;
+1441;CANADIAN SYLLABICS WEST-CREE PWO;Lo;0;L;;;;;N;;;;;
+1442;CANADIAN SYLLABICS PWOO;Lo;0;L;;;;;N;;;;;
+1443;CANADIAN SYLLABICS WEST-CREE PWOO;Lo;0;L;;;;;N;;;;;
+1444;CANADIAN SYLLABICS PWA;Lo;0;L;;;;;N;;;;;
+1445;CANADIAN SYLLABICS WEST-CREE PWA;Lo;0;L;;;;;N;;;;;
+1446;CANADIAN SYLLABICS PWAA;Lo;0;L;;;;;N;;;;;
+1447;CANADIAN SYLLABICS WEST-CREE PWAA;Lo;0;L;;;;;N;;;;;
+1448;CANADIAN SYLLABICS Y-CREE PWAA;Lo;0;L;;;;;N;;;;;
+1449;CANADIAN SYLLABICS P;Lo;0;L;;;;;N;;;;;
+144A;CANADIAN SYLLABICS WEST-CREE P;Lo;0;L;;;;;N;;;;;
+144B;CANADIAN SYLLABICS CARRIER H;Lo;0;L;;;;;N;;;;;
+144C;CANADIAN SYLLABICS TE;Lo;0;L;;;;;N;;;;;
+144D;CANADIAN SYLLABICS TAAI;Lo;0;L;;;;;N;;;;;
+144E;CANADIAN SYLLABICS TI;Lo;0;L;;;;;N;;;;;
+144F;CANADIAN SYLLABICS TII;Lo;0;L;;;;;N;;;;;
+1450;CANADIAN SYLLABICS TO;Lo;0;L;;;;;N;;;;;
+1451;CANADIAN SYLLABICS TOO;Lo;0;L;;;;;N;;;;;
+1452;CANADIAN SYLLABICS Y-CREE TOO;Lo;0;L;;;;;N;;;;;
+1453;CANADIAN SYLLABICS CARRIER DEE;Lo;0;L;;;;;N;;;;;
+1454;CANADIAN SYLLABICS CARRIER DI;Lo;0;L;;;;;N;;;;;
+1455;CANADIAN SYLLABICS TA;Lo;0;L;;;;;N;;;;;
+1456;CANADIAN SYLLABICS TAA;Lo;0;L;;;;;N;;;;;
+1457;CANADIAN SYLLABICS TWE;Lo;0;L;;;;;N;;;;;
+1458;CANADIAN SYLLABICS WEST-CREE TWE;Lo;0;L;;;;;N;;;;;
+1459;CANADIAN SYLLABICS TWI;Lo;0;L;;;;;N;;;;;
+145A;CANADIAN SYLLABICS WEST-CREE TWI;Lo;0;L;;;;;N;;;;;
+145B;CANADIAN SYLLABICS TWII;Lo;0;L;;;;;N;;;;;
+145C;CANADIAN SYLLABICS WEST-CREE TWII;Lo;0;L;;;;;N;;;;;
+145D;CANADIAN SYLLABICS TWO;Lo;0;L;;;;;N;;;;;
+145E;CANADIAN SYLLABICS WEST-CREE TWO;Lo;0;L;;;;;N;;;;;
+145F;CANADIAN SYLLABICS TWOO;Lo;0;L;;;;;N;;;;;
+1460;CANADIAN SYLLABICS WEST-CREE TWOO;Lo;0;L;;;;;N;;;;;
+1461;CANADIAN SYLLABICS TWA;Lo;0;L;;;;;N;;;;;
+1462;CANADIAN SYLLABICS WEST-CREE TWA;Lo;0;L;;;;;N;;;;;
+1463;CANADIAN SYLLABICS TWAA;Lo;0;L;;;;;N;;;;;
+1464;CANADIAN SYLLABICS WEST-CREE TWAA;Lo;0;L;;;;;N;;;;;
+1465;CANADIAN SYLLABICS NASKAPI TWAA;Lo;0;L;;;;;N;;;;;
+1466;CANADIAN SYLLABICS T;Lo;0;L;;;;;N;;;;;
+1467;CANADIAN SYLLABICS TTE;Lo;0;L;;;;;N;;;;;
+1468;CANADIAN SYLLABICS TTI;Lo;0;L;;;;;N;;;;;
+1469;CANADIAN SYLLABICS TTO;Lo;0;L;;;;;N;;;;;
+146A;CANADIAN SYLLABICS TTA;Lo;0;L;;;;;N;;;;;
+146B;CANADIAN SYLLABICS KE;Lo;0;L;;;;;N;;;;;
+146C;CANADIAN SYLLABICS KAAI;Lo;0;L;;;;;N;;;;;
+146D;CANADIAN SYLLABICS KI;Lo;0;L;;;;;N;;;;;
+146E;CANADIAN SYLLABICS KII;Lo;0;L;;;;;N;;;;;
+146F;CANADIAN SYLLABICS KO;Lo;0;L;;;;;N;;;;;
+1470;CANADIAN SYLLABICS KOO;Lo;0;L;;;;;N;;;;;
+1471;CANADIAN SYLLABICS Y-CREE KOO;Lo;0;L;;;;;N;;;;;
+1472;CANADIAN SYLLABICS KA;Lo;0;L;;;;;N;;;;;
+1473;CANADIAN SYLLABICS KAA;Lo;0;L;;;;;N;;;;;
+1474;CANADIAN SYLLABICS KWE;Lo;0;L;;;;;N;;;;;
+1475;CANADIAN SYLLABICS WEST-CREE KWE;Lo;0;L;;;;;N;;;;;
+1476;CANADIAN SYLLABICS KWI;Lo;0;L;;;;;N;;;;;
+1477;CANADIAN SYLLABICS WEST-CREE KWI;Lo;0;L;;;;;N;;;;;
+1478;CANADIAN SYLLABICS KWII;Lo;0;L;;;;;N;;;;;
+1479;CANADIAN SYLLABICS WEST-CREE KWII;Lo;0;L;;;;;N;;;;;
+147A;CANADIAN SYLLABICS KWO;Lo;0;L;;;;;N;;;;;
+147B;CANADIAN SYLLABICS WEST-CREE KWO;Lo;0;L;;;;;N;;;;;
+147C;CANADIAN SYLLABICS KWOO;Lo;0;L;;;;;N;;;;;
+147D;CANADIAN SYLLABICS WEST-CREE KWOO;Lo;0;L;;;;;N;;;;;
+147E;CANADIAN SYLLABICS KWA;Lo;0;L;;;;;N;;;;;
+147F;CANADIAN SYLLABICS WEST-CREE KWA;Lo;0;L;;;;;N;;;;;
+1480;CANADIAN SYLLABICS KWAA;Lo;0;L;;;;;N;;;;;
+1481;CANADIAN SYLLABICS WEST-CREE KWAA;Lo;0;L;;;;;N;;;;;
+1482;CANADIAN SYLLABICS NASKAPI KWAA;Lo;0;L;;;;;N;;;;;
+1483;CANADIAN SYLLABICS K;Lo;0;L;;;;;N;;;;;
+1484;CANADIAN SYLLABICS KW;Lo;0;L;;;;;N;;;;;
+1485;CANADIAN SYLLABICS SOUTH-SLAVEY KEH;Lo;0;L;;;;;N;;;;;
+1486;CANADIAN SYLLABICS SOUTH-SLAVEY KIH;Lo;0;L;;;;;N;;;;;
+1487;CANADIAN SYLLABICS SOUTH-SLAVEY KOH;Lo;0;L;;;;;N;;;;;
+1488;CANADIAN SYLLABICS SOUTH-SLAVEY KAH;Lo;0;L;;;;;N;;;;;
+1489;CANADIAN SYLLABICS CE;Lo;0;L;;;;;N;;;;;
+148A;CANADIAN SYLLABICS CAAI;Lo;0;L;;;;;N;;;;;
+148B;CANADIAN SYLLABICS CI;Lo;0;L;;;;;N;;;;;
+148C;CANADIAN SYLLABICS CII;Lo;0;L;;;;;N;;;;;
+148D;CANADIAN SYLLABICS CO;Lo;0;L;;;;;N;;;;;
+148E;CANADIAN SYLLABICS COO;Lo;0;L;;;;;N;;;;;
+148F;CANADIAN SYLLABICS Y-CREE COO;Lo;0;L;;;;;N;;;;;
+1490;CANADIAN SYLLABICS CA;Lo;0;L;;;;;N;;;;;
+1491;CANADIAN SYLLABICS CAA;Lo;0;L;;;;;N;;;;;
+1492;CANADIAN SYLLABICS CWE;Lo;0;L;;;;;N;;;;;
+1493;CANADIAN SYLLABICS WEST-CREE CWE;Lo;0;L;;;;;N;;;;;
+1494;CANADIAN SYLLABICS CWI;Lo;0;L;;;;;N;;;;;
+1495;CANADIAN SYLLABICS WEST-CREE CWI;Lo;0;L;;;;;N;;;;;
+1496;CANADIAN SYLLABICS CWII;Lo;0;L;;;;;N;;;;;
+1497;CANADIAN SYLLABICS WEST-CREE CWII;Lo;0;L;;;;;N;;;;;
+1498;CANADIAN SYLLABICS CWO;Lo;0;L;;;;;N;;;;;
+1499;CANADIAN SYLLABICS WEST-CREE CWO;Lo;0;L;;;;;N;;;;;
+149A;CANADIAN SYLLABICS CWOO;Lo;0;L;;;;;N;;;;;
+149B;CANADIAN SYLLABICS WEST-CREE CWOO;Lo;0;L;;;;;N;;;;;
+149C;CANADIAN SYLLABICS CWA;Lo;0;L;;;;;N;;;;;
+149D;CANADIAN SYLLABICS WEST-CREE CWA;Lo;0;L;;;;;N;;;;;
+149E;CANADIAN SYLLABICS CWAA;Lo;0;L;;;;;N;;;;;
+149F;CANADIAN SYLLABICS WEST-CREE CWAA;Lo;0;L;;;;;N;;;;;
+14A0;CANADIAN SYLLABICS NASKAPI CWAA;Lo;0;L;;;;;N;;;;;
+14A1;CANADIAN SYLLABICS C;Lo;0;L;;;;;N;;;;;
+14A2;CANADIAN SYLLABICS SAYISI TH;Lo;0;L;;;;;N;;;;;
+14A3;CANADIAN SYLLABICS ME;Lo;0;L;;;;;N;;;;;
+14A4;CANADIAN SYLLABICS MAAI;Lo;0;L;;;;;N;;;;;
+14A5;CANADIAN SYLLABICS MI;Lo;0;L;;;;;N;;;;;
+14A6;CANADIAN SYLLABICS MII;Lo;0;L;;;;;N;;;;;
+14A7;CANADIAN SYLLABICS MO;Lo;0;L;;;;;N;;;;;
+14A8;CANADIAN SYLLABICS MOO;Lo;0;L;;;;;N;;;;;
+14A9;CANADIAN SYLLABICS Y-CREE MOO;Lo;0;L;;;;;N;;;;;
+14AA;CANADIAN SYLLABICS MA;Lo;0;L;;;;;N;;;;;
+14AB;CANADIAN SYLLABICS MAA;Lo;0;L;;;;;N;;;;;
+14AC;CANADIAN SYLLABICS MWE;Lo;0;L;;;;;N;;;;;
+14AD;CANADIAN SYLLABICS WEST-CREE MWE;Lo;0;L;;;;;N;;;;;
+14AE;CANADIAN SYLLABICS MWI;Lo;0;L;;;;;N;;;;;
+14AF;CANADIAN SYLLABICS WEST-CREE MWI;Lo;0;L;;;;;N;;;;;
+14B0;CANADIAN SYLLABICS MWII;Lo;0;L;;;;;N;;;;;
+14B1;CANADIAN SYLLABICS WEST-CREE MWII;Lo;0;L;;;;;N;;;;;
+14B2;CANADIAN SYLLABICS MWO;Lo;0;L;;;;;N;;;;;
+14B3;CANADIAN SYLLABICS WEST-CREE MWO;Lo;0;L;;;;;N;;;;;
+14B4;CANADIAN SYLLABICS MWOO;Lo;0;L;;;;;N;;;;;
+14B5;CANADIAN SYLLABICS WEST-CREE MWOO;Lo;0;L;;;;;N;;;;;
+14B6;CANADIAN SYLLABICS MWA;Lo;0;L;;;;;N;;;;;
+14B7;CANADIAN SYLLABICS WEST-CREE MWA;Lo;0;L;;;;;N;;;;;
+14B8;CANADIAN SYLLABICS MWAA;Lo;0;L;;;;;N;;;;;
+14B9;CANADIAN SYLLABICS WEST-CREE MWAA;Lo;0;L;;;;;N;;;;;
+14BA;CANADIAN SYLLABICS NASKAPI MWAA;Lo;0;L;;;;;N;;;;;
+14BB;CANADIAN SYLLABICS M;Lo;0;L;;;;;N;;;;;
+14BC;CANADIAN SYLLABICS WEST-CREE M;Lo;0;L;;;;;N;;;;;
+14BD;CANADIAN SYLLABICS MH;Lo;0;L;;;;;N;;;;;
+14BE;CANADIAN SYLLABICS ATHAPASCAN M;Lo;0;L;;;;;N;;;;;
+14BF;CANADIAN SYLLABICS SAYISI M;Lo;0;L;;;;;N;;;;;
+14C0;CANADIAN SYLLABICS NE;Lo;0;L;;;;;N;;;;;
+14C1;CANADIAN SYLLABICS NAAI;Lo;0;L;;;;;N;;;;;
+14C2;CANADIAN SYLLABICS NI;Lo;0;L;;;;;N;;;;;
+14C3;CANADIAN SYLLABICS NII;Lo;0;L;;;;;N;;;;;
+14C4;CANADIAN SYLLABICS NO;Lo;0;L;;;;;N;;;;;
+14C5;CANADIAN SYLLABICS NOO;Lo;0;L;;;;;N;;;;;
+14C6;CANADIAN SYLLABICS Y-CREE NOO;Lo;0;L;;;;;N;;;;;
+14C7;CANADIAN SYLLABICS NA;Lo;0;L;;;;;N;;;;;
+14C8;CANADIAN SYLLABICS NAA;Lo;0;L;;;;;N;;;;;
+14C9;CANADIAN SYLLABICS NWE;Lo;0;L;;;;;N;;;;;
+14CA;CANADIAN SYLLABICS WEST-CREE NWE;Lo;0;L;;;;;N;;;;;
+14CB;CANADIAN SYLLABICS NWA;Lo;0;L;;;;;N;;;;;
+14CC;CANADIAN SYLLABICS WEST-CREE NWA;Lo;0;L;;;;;N;;;;;
+14CD;CANADIAN SYLLABICS NWAA;Lo;0;L;;;;;N;;;;;
+14CE;CANADIAN SYLLABICS WEST-CREE NWAA;Lo;0;L;;;;;N;;;;;
+14CF;CANADIAN SYLLABICS NASKAPI NWAA;Lo;0;L;;;;;N;;;;;
+14D0;CANADIAN SYLLABICS N;Lo;0;L;;;;;N;;;;;
+14D1;CANADIAN SYLLABICS CARRIER NG;Lo;0;L;;;;;N;;;;;
+14D2;CANADIAN SYLLABICS NH;Lo;0;L;;;;;N;;;;;
+14D3;CANADIAN SYLLABICS LE;Lo;0;L;;;;;N;;;;;
+14D4;CANADIAN SYLLABICS LAAI;Lo;0;L;;;;;N;;;;;
+14D5;CANADIAN SYLLABICS LI;Lo;0;L;;;;;N;;;;;
+14D6;CANADIAN SYLLABICS LII;Lo;0;L;;;;;N;;;;;
+14D7;CANADIAN SYLLABICS LO;Lo;0;L;;;;;N;;;;;
+14D8;CANADIAN SYLLABICS LOO;Lo;0;L;;;;;N;;;;;
+14D9;CANADIAN SYLLABICS Y-CREE LOO;Lo;0;L;;;;;N;;;;;
+14DA;CANADIAN SYLLABICS LA;Lo;0;L;;;;;N;;;;;
+14DB;CANADIAN SYLLABICS LAA;Lo;0;L;;;;;N;;;;;
+14DC;CANADIAN SYLLABICS LWE;Lo;0;L;;;;;N;;;;;
+14DD;CANADIAN SYLLABICS WEST-CREE LWE;Lo;0;L;;;;;N;;;;;
+14DE;CANADIAN SYLLABICS LWI;Lo;0;L;;;;;N;;;;;
+14DF;CANADIAN SYLLABICS WEST-CREE LWI;Lo;0;L;;;;;N;;;;;
+14E0;CANADIAN SYLLABICS LWII;Lo;0;L;;;;;N;;;;;
+14E1;CANADIAN SYLLABICS WEST-CREE LWII;Lo;0;L;;;;;N;;;;;
+14E2;CANADIAN SYLLABICS LWO;Lo;0;L;;;;;N;;;;;
+14E3;CANADIAN SYLLABICS WEST-CREE LWO;Lo;0;L;;;;;N;;;;;
+14E4;CANADIAN SYLLABICS LWOO;Lo;0;L;;;;;N;;;;;
+14E5;CANADIAN SYLLABICS WEST-CREE LWOO;Lo;0;L;;;;;N;;;;;
+14E6;CANADIAN SYLLABICS LWA;Lo;0;L;;;;;N;;;;;
+14E7;CANADIAN SYLLABICS WEST-CREE LWA;Lo;0;L;;;;;N;;;;;
+14E8;CANADIAN SYLLABICS LWAA;Lo;0;L;;;;;N;;;;;
+14E9;CANADIAN SYLLABICS WEST-CREE LWAA;Lo;0;L;;;;;N;;;;;
+14EA;CANADIAN SYLLABICS L;Lo;0;L;;;;;N;;;;;
+14EB;CANADIAN SYLLABICS WEST-CREE L;Lo;0;L;;;;;N;;;;;
+14EC;CANADIAN SYLLABICS MEDIAL L;Lo;0;L;;;;;N;;;;;
+14ED;CANADIAN SYLLABICS SE;Lo;0;L;;;;;N;;;;;
+14EE;CANADIAN SYLLABICS SAAI;Lo;0;L;;;;;N;;;;;
+14EF;CANADIAN SYLLABICS SI;Lo;0;L;;;;;N;;;;;
+14F0;CANADIAN SYLLABICS SII;Lo;0;L;;;;;N;;;;;
+14F1;CANADIAN SYLLABICS SO;Lo;0;L;;;;;N;;;;;
+14F2;CANADIAN SYLLABICS SOO;Lo;0;L;;;;;N;;;;;
+14F3;CANADIAN SYLLABICS Y-CREE SOO;Lo;0;L;;;;;N;;;;;
+14F4;CANADIAN SYLLABICS SA;Lo;0;L;;;;;N;;;;;
+14F5;CANADIAN SYLLABICS SAA;Lo;0;L;;;;;N;;;;;
+14F6;CANADIAN SYLLABICS SWE;Lo;0;L;;;;;N;;;;;
+14F7;CANADIAN SYLLABICS WEST-CREE SWE;Lo;0;L;;;;;N;;;;;
+14F8;CANADIAN SYLLABICS SWI;Lo;0;L;;;;;N;;;;;
+14F9;CANADIAN SYLLABICS WEST-CREE SWI;Lo;0;L;;;;;N;;;;;
+14FA;CANADIAN SYLLABICS SWII;Lo;0;L;;;;;N;;;;;
+14FB;CANADIAN SYLLABICS WEST-CREE SWII;Lo;0;L;;;;;N;;;;;
+14FC;CANADIAN SYLLABICS SWO;Lo;0;L;;;;;N;;;;;
+14FD;CANADIAN SYLLABICS WEST-CREE SWO;Lo;0;L;;;;;N;;;;;
+14FE;CANADIAN SYLLABICS SWOO;Lo;0;L;;;;;N;;;;;
+14FF;CANADIAN SYLLABICS WEST-CREE SWOO;Lo;0;L;;;;;N;;;;;
+1500;CANADIAN SYLLABICS SWA;Lo;0;L;;;;;N;;;;;
+1501;CANADIAN SYLLABICS WEST-CREE SWA;Lo;0;L;;;;;N;;;;;
+1502;CANADIAN SYLLABICS SWAA;Lo;0;L;;;;;N;;;;;
+1503;CANADIAN SYLLABICS WEST-CREE SWAA;Lo;0;L;;;;;N;;;;;
+1504;CANADIAN SYLLABICS NASKAPI SWAA;Lo;0;L;;;;;N;;;;;
+1505;CANADIAN SYLLABICS S;Lo;0;L;;;;;N;;;;;
+1506;CANADIAN SYLLABICS ATHAPASCAN S;Lo;0;L;;;;;N;;;;;
+1507;CANADIAN SYLLABICS SW;Lo;0;L;;;;;N;;;;;
+1508;CANADIAN SYLLABICS BLACKFOOT S;Lo;0;L;;;;;N;;;;;
+1509;CANADIAN SYLLABICS MOOSE-CREE SK;Lo;0;L;;;;;N;;;;;
+150A;CANADIAN SYLLABICS NASKAPI SKW;Lo;0;L;;;;;N;;;;;
+150B;CANADIAN SYLLABICS NASKAPI S-W;Lo;0;L;;;;;N;;;;;
+150C;CANADIAN SYLLABICS NASKAPI SPWA;Lo;0;L;;;;;N;;;;;
+150D;CANADIAN SYLLABICS NASKAPI STWA;Lo;0;L;;;;;N;;;;;
+150E;CANADIAN SYLLABICS NASKAPI SKWA;Lo;0;L;;;;;N;;;;;
+150F;CANADIAN SYLLABICS NASKAPI SCWA;Lo;0;L;;;;;N;;;;;
+1510;CANADIAN SYLLABICS SHE;Lo;0;L;;;;;N;;;;;
+1511;CANADIAN SYLLABICS SHI;Lo;0;L;;;;;N;;;;;
+1512;CANADIAN SYLLABICS SHII;Lo;0;L;;;;;N;;;;;
+1513;CANADIAN SYLLABICS SHO;Lo;0;L;;;;;N;;;;;
+1514;CANADIAN SYLLABICS SHOO;Lo;0;L;;;;;N;;;;;
+1515;CANADIAN SYLLABICS SHA;Lo;0;L;;;;;N;;;;;
+1516;CANADIAN SYLLABICS SHAA;Lo;0;L;;;;;N;;;;;
+1517;CANADIAN SYLLABICS SHWE;Lo;0;L;;;;;N;;;;;
+1518;CANADIAN SYLLABICS WEST-CREE SHWE;Lo;0;L;;;;;N;;;;;
+1519;CANADIAN SYLLABICS SHWI;Lo;0;L;;;;;N;;;;;
+151A;CANADIAN SYLLABICS WEST-CREE SHWI;Lo;0;L;;;;;N;;;;;
+151B;CANADIAN SYLLABICS SHWII;Lo;0;L;;;;;N;;;;;
+151C;CANADIAN SYLLABICS WEST-CREE SHWII;Lo;0;L;;;;;N;;;;;
+151D;CANADIAN SYLLABICS SHWO;Lo;0;L;;;;;N;;;;;
+151E;CANADIAN SYLLABICS WEST-CREE SHWO;Lo;0;L;;;;;N;;;;;
+151F;CANADIAN SYLLABICS SHWOO;Lo;0;L;;;;;N;;;;;
+1520;CANADIAN SYLLABICS WEST-CREE SHWOO;Lo;0;L;;;;;N;;;;;
+1521;CANADIAN SYLLABICS SHWA;Lo;0;L;;;;;N;;;;;
+1522;CANADIAN SYLLABICS WEST-CREE SHWA;Lo;0;L;;;;;N;;;;;
+1523;CANADIAN SYLLABICS SHWAA;Lo;0;L;;;;;N;;;;;
+1524;CANADIAN SYLLABICS WEST-CREE SHWAA;Lo;0;L;;;;;N;;;;;
+1525;CANADIAN SYLLABICS SH;Lo;0;L;;;;;N;;;;;
+1526;CANADIAN SYLLABICS YE;Lo;0;L;;;;;N;;;;;
+1527;CANADIAN SYLLABICS YAAI;Lo;0;L;;;;;N;;;;;
+1528;CANADIAN SYLLABICS YI;Lo;0;L;;;;;N;;;;;
+1529;CANADIAN SYLLABICS YII;Lo;0;L;;;;;N;;;;;
+152A;CANADIAN SYLLABICS YO;Lo;0;L;;;;;N;;;;;
+152B;CANADIAN SYLLABICS YOO;Lo;0;L;;;;;N;;;;;
+152C;CANADIAN SYLLABICS Y-CREE YOO;Lo;0;L;;;;;N;;;;;
+152D;CANADIAN SYLLABICS YA;Lo;0;L;;;;;N;;;;;
+152E;CANADIAN SYLLABICS YAA;Lo;0;L;;;;;N;;;;;
+152F;CANADIAN SYLLABICS YWE;Lo;0;L;;;;;N;;;;;
+1530;CANADIAN SYLLABICS WEST-CREE YWE;Lo;0;L;;;;;N;;;;;
+1531;CANADIAN SYLLABICS YWI;Lo;0;L;;;;;N;;;;;
+1532;CANADIAN SYLLABICS WEST-CREE YWI;Lo;0;L;;;;;N;;;;;
+1533;CANADIAN SYLLABICS YWII;Lo;0;L;;;;;N;;;;;
+1534;CANADIAN SYLLABICS WEST-CREE YWII;Lo;0;L;;;;;N;;;;;
+1535;CANADIAN SYLLABICS YWO;Lo;0;L;;;;;N;;;;;
+1536;CANADIAN SYLLABICS WEST-CREE YWO;Lo;0;L;;;;;N;;;;;
+1537;CANADIAN SYLLABICS YWOO;Lo;0;L;;;;;N;;;;;
+1538;CANADIAN SYLLABICS WEST-CREE YWOO;Lo;0;L;;;;;N;;;;;
+1539;CANADIAN SYLLABICS YWA;Lo;0;L;;;;;N;;;;;
+153A;CANADIAN SYLLABICS WEST-CREE YWA;Lo;0;L;;;;;N;;;;;
+153B;CANADIAN SYLLABICS YWAA;Lo;0;L;;;;;N;;;;;
+153C;CANADIAN SYLLABICS WEST-CREE YWAA;Lo;0;L;;;;;N;;;;;
+153D;CANADIAN SYLLABICS NASKAPI YWAA;Lo;0;L;;;;;N;;;;;
+153E;CANADIAN SYLLABICS Y;Lo;0;L;;;;;N;;;;;
+153F;CANADIAN SYLLABICS BIBLE-CREE Y;Lo;0;L;;;;;N;;;;;
+1540;CANADIAN SYLLABICS WEST-CREE Y;Lo;0;L;;;;;N;;;;;
+1541;CANADIAN SYLLABICS SAYISI YI;Lo;0;L;;;;;N;;;;;
+1542;CANADIAN SYLLABICS RE;Lo;0;L;;;;;N;;;;;
+1543;CANADIAN SYLLABICS R-CREE RE;Lo;0;L;;;;;N;;;;;
+1544;CANADIAN SYLLABICS WEST-CREE LE;Lo;0;L;;;;;N;;;;;
+1545;CANADIAN SYLLABICS RAAI;Lo;0;L;;;;;N;;;;;
+1546;CANADIAN SYLLABICS RI;Lo;0;L;;;;;N;;;;;
+1547;CANADIAN SYLLABICS RII;Lo;0;L;;;;;N;;;;;
+1548;CANADIAN SYLLABICS RO;Lo;0;L;;;;;N;;;;;
+1549;CANADIAN SYLLABICS ROO;Lo;0;L;;;;;N;;;;;
+154A;CANADIAN SYLLABICS WEST-CREE LO;Lo;0;L;;;;;N;;;;;
+154B;CANADIAN SYLLABICS RA;Lo;0;L;;;;;N;;;;;
+154C;CANADIAN SYLLABICS RAA;Lo;0;L;;;;;N;;;;;
+154D;CANADIAN SYLLABICS WEST-CREE LA;Lo;0;L;;;;;N;;;;;
+154E;CANADIAN SYLLABICS RWAA;Lo;0;L;;;;;N;;;;;
+154F;CANADIAN SYLLABICS WEST-CREE RWAA;Lo;0;L;;;;;N;;;;;
+1550;CANADIAN SYLLABICS R;Lo;0;L;;;;;N;;;;;
+1551;CANADIAN SYLLABICS WEST-CREE R;Lo;0;L;;;;;N;;;;;
+1552;CANADIAN SYLLABICS MEDIAL R;Lo;0;L;;;;;N;;;;;
+1553;CANADIAN SYLLABICS FE;Lo;0;L;;;;;N;;;;;
+1554;CANADIAN SYLLABICS FAAI;Lo;0;L;;;;;N;;;;;
+1555;CANADIAN SYLLABICS FI;Lo;0;L;;;;;N;;;;;
+1556;CANADIAN SYLLABICS FII;Lo;0;L;;;;;N;;;;;
+1557;CANADIAN SYLLABICS FO;Lo;0;L;;;;;N;;;;;
+1558;CANADIAN SYLLABICS FOO;Lo;0;L;;;;;N;;;;;
+1559;CANADIAN SYLLABICS FA;Lo;0;L;;;;;N;;;;;
+155A;CANADIAN SYLLABICS FAA;Lo;0;L;;;;;N;;;;;
+155B;CANADIAN SYLLABICS FWAA;Lo;0;L;;;;;N;;;;;
+155C;CANADIAN SYLLABICS WEST-CREE FWAA;Lo;0;L;;;;;N;;;;;
+155D;CANADIAN SYLLABICS F;Lo;0;L;;;;;N;;;;;
+155E;CANADIAN SYLLABICS THE;Lo;0;L;;;;;N;;;;;
+155F;CANADIAN SYLLABICS N-CREE THE;Lo;0;L;;;;;N;;;;;
+1560;CANADIAN SYLLABICS THI;Lo;0;L;;;;;N;;;;;
+1561;CANADIAN SYLLABICS N-CREE THI;Lo;0;L;;;;;N;;;;;
+1562;CANADIAN SYLLABICS THII;Lo;0;L;;;;;N;;;;;
+1563;CANADIAN SYLLABICS N-CREE THII;Lo;0;L;;;;;N;;;;;
+1564;CANADIAN SYLLABICS THO;Lo;0;L;;;;;N;;;;;
+1565;CANADIAN SYLLABICS THOO;Lo;0;L;;;;;N;;;;;
+1566;CANADIAN SYLLABICS THA;Lo;0;L;;;;;N;;;;;
+1567;CANADIAN SYLLABICS THAA;Lo;0;L;;;;;N;;;;;
+1568;CANADIAN SYLLABICS THWAA;Lo;0;L;;;;;N;;;;;
+1569;CANADIAN SYLLABICS WEST-CREE THWAA;Lo;0;L;;;;;N;;;;;
+156A;CANADIAN SYLLABICS TH;Lo;0;L;;;;;N;;;;;
+156B;CANADIAN SYLLABICS TTHE;Lo;0;L;;;;;N;;;;;
+156C;CANADIAN SYLLABICS TTHI;Lo;0;L;;;;;N;;;;;
+156D;CANADIAN SYLLABICS TTHO;Lo;0;L;;;;;N;;;;;
+156E;CANADIAN SYLLABICS TTHA;Lo;0;L;;;;;N;;;;;
+156F;CANADIAN SYLLABICS TTH;Lo;0;L;;;;;N;;;;;
+1570;CANADIAN SYLLABICS TYE;Lo;0;L;;;;;N;;;;;
+1571;CANADIAN SYLLABICS TYI;Lo;0;L;;;;;N;;;;;
+1572;CANADIAN SYLLABICS TYO;Lo;0;L;;;;;N;;;;;
+1573;CANADIAN SYLLABICS TYA;Lo;0;L;;;;;N;;;;;
+1574;CANADIAN SYLLABICS NUNAVIK HE;Lo;0;L;;;;;N;;;;;
+1575;CANADIAN SYLLABICS NUNAVIK HI;Lo;0;L;;;;;N;;;;;
+1576;CANADIAN SYLLABICS NUNAVIK HII;Lo;0;L;;;;;N;;;;;
+1577;CANADIAN SYLLABICS NUNAVIK HO;Lo;0;L;;;;;N;;;;;
+1578;CANADIAN SYLLABICS NUNAVIK HOO;Lo;0;L;;;;;N;;;;;
+1579;CANADIAN SYLLABICS NUNAVIK HA;Lo;0;L;;;;;N;;;;;
+157A;CANADIAN SYLLABICS NUNAVIK HAA;Lo;0;L;;;;;N;;;;;
+157B;CANADIAN SYLLABICS NUNAVIK H;Lo;0;L;;;;;N;;;;;
+157C;CANADIAN SYLLABICS NUNAVUT H;Lo;0;L;;;;;N;;;;;
+157D;CANADIAN SYLLABICS HK;Lo;0;L;;;;;N;;;;;
+157E;CANADIAN SYLLABICS QAAI;Lo;0;L;;;;;N;;;;;
+157F;CANADIAN SYLLABICS QI;Lo;0;L;;;;;N;;;;;
+1580;CANADIAN SYLLABICS QII;Lo;0;L;;;;;N;;;;;
+1581;CANADIAN SYLLABICS QO;Lo;0;L;;;;;N;;;;;
+1582;CANADIAN SYLLABICS QOO;Lo;0;L;;;;;N;;;;;
+1583;CANADIAN SYLLABICS QA;Lo;0;L;;;;;N;;;;;
+1584;CANADIAN SYLLABICS QAA;Lo;0;L;;;;;N;;;;;
+1585;CANADIAN SYLLABICS Q;Lo;0;L;;;;;N;;;;;
+1586;CANADIAN SYLLABICS TLHE;Lo;0;L;;;;;N;;;;;
+1587;CANADIAN SYLLABICS TLHI;Lo;0;L;;;;;N;;;;;
+1588;CANADIAN SYLLABICS TLHO;Lo;0;L;;;;;N;;;;;
+1589;CANADIAN SYLLABICS TLHA;Lo;0;L;;;;;N;;;;;
+158A;CANADIAN SYLLABICS WEST-CREE RE;Lo;0;L;;;;;N;;;;;
+158B;CANADIAN SYLLABICS WEST-CREE RI;Lo;0;L;;;;;N;;;;;
+158C;CANADIAN SYLLABICS WEST-CREE RO;Lo;0;L;;;;;N;;;;;
+158D;CANADIAN SYLLABICS WEST-CREE RA;Lo;0;L;;;;;N;;;;;
+158E;CANADIAN SYLLABICS NGAAI;Lo;0;L;;;;;N;;;;;
+158F;CANADIAN SYLLABICS NGI;Lo;0;L;;;;;N;;;;;
+1590;CANADIAN SYLLABICS NGII;Lo;0;L;;;;;N;;;;;
+1591;CANADIAN SYLLABICS NGO;Lo;0;L;;;;;N;;;;;
+1592;CANADIAN SYLLABICS NGOO;Lo;0;L;;;;;N;;;;;
+1593;CANADIAN SYLLABICS NGA;Lo;0;L;;;;;N;;;;;
+1594;CANADIAN SYLLABICS NGAA;Lo;0;L;;;;;N;;;;;
+1595;CANADIAN SYLLABICS NG;Lo;0;L;;;;;N;;;;;
+1596;CANADIAN SYLLABICS NNG;Lo;0;L;;;;;N;;;;;
+1597;CANADIAN SYLLABICS SAYISI SHE;Lo;0;L;;;;;N;;;;;
+1598;CANADIAN SYLLABICS SAYISI SHI;Lo;0;L;;;;;N;;;;;
+1599;CANADIAN SYLLABICS SAYISI SHO;Lo;0;L;;;;;N;;;;;
+159A;CANADIAN SYLLABICS SAYISI SHA;Lo;0;L;;;;;N;;;;;
+159B;CANADIAN SYLLABICS WOODS-CREE THE;Lo;0;L;;;;;N;;;;;
+159C;CANADIAN SYLLABICS WOODS-CREE THI;Lo;0;L;;;;;N;;;;;
+159D;CANADIAN SYLLABICS WOODS-CREE THO;Lo;0;L;;;;;N;;;;;
+159E;CANADIAN SYLLABICS WOODS-CREE THA;Lo;0;L;;;;;N;;;;;
+159F;CANADIAN SYLLABICS WOODS-CREE TH;Lo;0;L;;;;;N;;;;;
+15A0;CANADIAN SYLLABICS LHI;Lo;0;L;;;;;N;;;;;
+15A1;CANADIAN SYLLABICS LHII;Lo;0;L;;;;;N;;;;;
+15A2;CANADIAN SYLLABICS LHO;Lo;0;L;;;;;N;;;;;
+15A3;CANADIAN SYLLABICS LHOO;Lo;0;L;;;;;N;;;;;
+15A4;CANADIAN SYLLABICS LHA;Lo;0;L;;;;;N;;;;;
+15A5;CANADIAN SYLLABICS LHAA;Lo;0;L;;;;;N;;;;;
+15A6;CANADIAN SYLLABICS LH;Lo;0;L;;;;;N;;;;;
+15A7;CANADIAN SYLLABICS TH-CREE THE;Lo;0;L;;;;;N;;;;;
+15A8;CANADIAN SYLLABICS TH-CREE THI;Lo;0;L;;;;;N;;;;;
+15A9;CANADIAN SYLLABICS TH-CREE THII;Lo;0;L;;;;;N;;;;;
+15AA;CANADIAN SYLLABICS TH-CREE THO;Lo;0;L;;;;;N;;;;;
+15AB;CANADIAN SYLLABICS TH-CREE THOO;Lo;0;L;;;;;N;;;;;
+15AC;CANADIAN SYLLABICS TH-CREE THA;Lo;0;L;;;;;N;;;;;
+15AD;CANADIAN SYLLABICS TH-CREE THAA;Lo;0;L;;;;;N;;;;;
+15AE;CANADIAN SYLLABICS TH-CREE TH;Lo;0;L;;;;;N;;;;;
+15AF;CANADIAN SYLLABICS AIVILIK B;Lo;0;L;;;;;N;;;;;
+15B0;CANADIAN SYLLABICS BLACKFOOT E;Lo;0;L;;;;;N;;;;;
+15B1;CANADIAN SYLLABICS BLACKFOOT I;Lo;0;L;;;;;N;;;;;
+15B2;CANADIAN SYLLABICS BLACKFOOT O;Lo;0;L;;;;;N;;;;;
+15B3;CANADIAN SYLLABICS BLACKFOOT A;Lo;0;L;;;;;N;;;;;
+15B4;CANADIAN SYLLABICS BLACKFOOT WE;Lo;0;L;;;;;N;;;;;
+15B5;CANADIAN SYLLABICS BLACKFOOT WI;Lo;0;L;;;;;N;;;;;
+15B6;CANADIAN SYLLABICS BLACKFOOT WO;Lo;0;L;;;;;N;;;;;
+15B7;CANADIAN SYLLABICS BLACKFOOT WA;Lo;0;L;;;;;N;;;;;
+15B8;CANADIAN SYLLABICS BLACKFOOT NE;Lo;0;L;;;;;N;;;;;
+15B9;CANADIAN SYLLABICS BLACKFOOT NI;Lo;0;L;;;;;N;;;;;
+15BA;CANADIAN SYLLABICS BLACKFOOT NO;Lo;0;L;;;;;N;;;;;
+15BB;CANADIAN SYLLABICS BLACKFOOT NA;Lo;0;L;;;;;N;;;;;
+15BC;CANADIAN SYLLABICS BLACKFOOT KE;Lo;0;L;;;;;N;;;;;
+15BD;CANADIAN SYLLABICS BLACKFOOT KI;Lo;0;L;;;;;N;;;;;
+15BE;CANADIAN SYLLABICS BLACKFOOT KO;Lo;0;L;;;;;N;;;;;
+15BF;CANADIAN SYLLABICS BLACKFOOT KA;Lo;0;L;;;;;N;;;;;
+15C0;CANADIAN SYLLABICS SAYISI HE;Lo;0;L;;;;;N;;;;;
+15C1;CANADIAN SYLLABICS SAYISI HI;Lo;0;L;;;;;N;;;;;
+15C2;CANADIAN SYLLABICS SAYISI HO;Lo;0;L;;;;;N;;;;;
+15C3;CANADIAN SYLLABICS SAYISI HA;Lo;0;L;;;;;N;;;;;
+15C4;CANADIAN SYLLABICS CARRIER GHU;Lo;0;L;;;;;N;;;;;
+15C5;CANADIAN SYLLABICS CARRIER GHO;Lo;0;L;;;;;N;;;;;
+15C6;CANADIAN SYLLABICS CARRIER GHE;Lo;0;L;;;;;N;;;;;
+15C7;CANADIAN SYLLABICS CARRIER GHEE;Lo;0;L;;;;;N;;;;;
+15C8;CANADIAN SYLLABICS CARRIER GHI;Lo;0;L;;;;;N;;;;;
+15C9;CANADIAN SYLLABICS CARRIER GHA;Lo;0;L;;;;;N;;;;;
+15CA;CANADIAN SYLLABICS CARRIER RU;Lo;0;L;;;;;N;;;;;
+15CB;CANADIAN SYLLABICS CARRIER RO;Lo;0;L;;;;;N;;;;;
+15CC;CANADIAN SYLLABICS CARRIER RE;Lo;0;L;;;;;N;;;;;
+15CD;CANADIAN SYLLABICS CARRIER REE;Lo;0;L;;;;;N;;;;;
+15CE;CANADIAN SYLLABICS CARRIER RI;Lo;0;L;;;;;N;;;;;
+15CF;CANADIAN SYLLABICS CARRIER RA;Lo;0;L;;;;;N;;;;;
+15D0;CANADIAN SYLLABICS CARRIER WU;Lo;0;L;;;;;N;;;;;
+15D1;CANADIAN SYLLABICS CARRIER WO;Lo;0;L;;;;;N;;;;;
+15D2;CANADIAN SYLLABICS CARRIER WE;Lo;0;L;;;;;N;;;;;
+15D3;CANADIAN SYLLABICS CARRIER WEE;Lo;0;L;;;;;N;;;;;
+15D4;CANADIAN SYLLABICS CARRIER WI;Lo;0;L;;;;;N;;;;;
+15D5;CANADIAN SYLLABICS CARRIER WA;Lo;0;L;;;;;N;;;;;
+15D6;CANADIAN SYLLABICS CARRIER HWU;Lo;0;L;;;;;N;;;;;
+15D7;CANADIAN SYLLABICS CARRIER HWO;Lo;0;L;;;;;N;;;;;
+15D8;CANADIAN SYLLABICS CARRIER HWE;Lo;0;L;;;;;N;;;;;
+15D9;CANADIAN SYLLABICS CARRIER HWEE;Lo;0;L;;;;;N;;;;;
+15DA;CANADIAN SYLLABICS CARRIER HWI;Lo;0;L;;;;;N;;;;;
+15DB;CANADIAN SYLLABICS CARRIER HWA;Lo;0;L;;;;;N;;;;;
+15DC;CANADIAN SYLLABICS CARRIER THU;Lo;0;L;;;;;N;;;;;
+15DD;CANADIAN SYLLABICS CARRIER THO;Lo;0;L;;;;;N;;;;;
+15DE;CANADIAN SYLLABICS CARRIER THE;Lo;0;L;;;;;N;;;;;
+15DF;CANADIAN SYLLABICS CARRIER THEE;Lo;0;L;;;;;N;;;;;
+15E0;CANADIAN SYLLABICS CARRIER THI;Lo;0;L;;;;;N;;;;;
+15E1;CANADIAN SYLLABICS CARRIER THA;Lo;0;L;;;;;N;;;;;
+15E2;CANADIAN SYLLABICS CARRIER TTU;Lo;0;L;;;;;N;;;;;
+15E3;CANADIAN SYLLABICS CARRIER TTO;Lo;0;L;;;;;N;;;;;
+15E4;CANADIAN SYLLABICS CARRIER TTE;Lo;0;L;;;;;N;;;;;
+15E5;CANADIAN SYLLABICS CARRIER TTEE;Lo;0;L;;;;;N;;;;;
+15E6;CANADIAN SYLLABICS CARRIER TTI;Lo;0;L;;;;;N;;;;;
+15E7;CANADIAN SYLLABICS CARRIER TTA;Lo;0;L;;;;;N;;;;;
+15E8;CANADIAN SYLLABICS CARRIER PU;Lo;0;L;;;;;N;;;;;
+15E9;CANADIAN SYLLABICS CARRIER PO;Lo;0;L;;;;;N;;;;;
+15EA;CANADIAN SYLLABICS CARRIER PE;Lo;0;L;;;;;N;;;;;
+15EB;CANADIAN SYLLABICS CARRIER PEE;Lo;0;L;;;;;N;;;;;
+15EC;CANADIAN SYLLABICS CARRIER PI;Lo;0;L;;;;;N;;;;;
+15ED;CANADIAN SYLLABICS CARRIER PA;Lo;0;L;;;;;N;;;;;
+15EE;CANADIAN SYLLABICS CARRIER P;Lo;0;L;;;;;N;;;;;
+15EF;CANADIAN SYLLABICS CARRIER GU;Lo;0;L;;;;;N;;;;;
+15F0;CANADIAN SYLLABICS CARRIER GO;Lo;0;L;;;;;N;;;;;
+15F1;CANADIAN SYLLABICS CARRIER GE;Lo;0;L;;;;;N;;;;;
+15F2;CANADIAN SYLLABICS CARRIER GEE;Lo;0;L;;;;;N;;;;;
+15F3;CANADIAN SYLLABICS CARRIER GI;Lo;0;L;;;;;N;;;;;
+15F4;CANADIAN SYLLABICS CARRIER GA;Lo;0;L;;;;;N;;;;;
+15F5;CANADIAN SYLLABICS CARRIER KHU;Lo;0;L;;;;;N;;;;;
+15F6;CANADIAN SYLLABICS CARRIER KHO;Lo;0;L;;;;;N;;;;;
+15F7;CANADIAN SYLLABICS CARRIER KHE;Lo;0;L;;;;;N;;;;;
+15F8;CANADIAN SYLLABICS CARRIER KHEE;Lo;0;L;;;;;N;;;;;
+15F9;CANADIAN SYLLABICS CARRIER KHI;Lo;0;L;;;;;N;;;;;
+15FA;CANADIAN SYLLABICS CARRIER KHA;Lo;0;L;;;;;N;;;;;
+15FB;CANADIAN SYLLABICS CARRIER KKU;Lo;0;L;;;;;N;;;;;
+15FC;CANADIAN SYLLABICS CARRIER KKO;Lo;0;L;;;;;N;;;;;
+15FD;CANADIAN SYLLABICS CARRIER KKE;Lo;0;L;;;;;N;;;;;
+15FE;CANADIAN SYLLABICS CARRIER KKEE;Lo;0;L;;;;;N;;;;;
+15FF;CANADIAN SYLLABICS CARRIER KKI;Lo;0;L;;;;;N;;;;;
+1600;CANADIAN SYLLABICS CARRIER KKA;Lo;0;L;;;;;N;;;;;
+1601;CANADIAN SYLLABICS CARRIER KK;Lo;0;L;;;;;N;;;;;
+1602;CANADIAN SYLLABICS CARRIER NU;Lo;0;L;;;;;N;;;;;
+1603;CANADIAN SYLLABICS CARRIER NO;Lo;0;L;;;;;N;;;;;
+1604;CANADIAN SYLLABICS CARRIER NE;Lo;0;L;;;;;N;;;;;
+1605;CANADIAN SYLLABICS CARRIER NEE;Lo;0;L;;;;;N;;;;;
+1606;CANADIAN SYLLABICS CARRIER NI;Lo;0;L;;;;;N;;;;;
+1607;CANADIAN SYLLABICS CARRIER NA;Lo;0;L;;;;;N;;;;;
+1608;CANADIAN SYLLABICS CARRIER MU;Lo;0;L;;;;;N;;;;;
+1609;CANADIAN SYLLABICS CARRIER MO;Lo;0;L;;;;;N;;;;;
+160A;CANADIAN SYLLABICS CARRIER ME;Lo;0;L;;;;;N;;;;;
+160B;CANADIAN SYLLABICS CARRIER MEE;Lo;0;L;;;;;N;;;;;
+160C;CANADIAN SYLLABICS CARRIER MI;Lo;0;L;;;;;N;;;;;
+160D;CANADIAN SYLLABICS CARRIER MA;Lo;0;L;;;;;N;;;;;
+160E;CANADIAN SYLLABICS CARRIER YU;Lo;0;L;;;;;N;;;;;
+160F;CANADIAN SYLLABICS CARRIER YO;Lo;0;L;;;;;N;;;;;
+1610;CANADIAN SYLLABICS CARRIER YE;Lo;0;L;;;;;N;;;;;
+1611;CANADIAN SYLLABICS CARRIER YEE;Lo;0;L;;;;;N;;;;;
+1612;CANADIAN SYLLABICS CARRIER YI;Lo;0;L;;;;;N;;;;;
+1613;CANADIAN SYLLABICS CARRIER YA;Lo;0;L;;;;;N;;;;;
+1614;CANADIAN SYLLABICS CARRIER JU;Lo;0;L;;;;;N;;;;;
+1615;CANADIAN SYLLABICS SAYISI JU;Lo;0;L;;;;;N;;;;;
+1616;CANADIAN SYLLABICS CARRIER JO;Lo;0;L;;;;;N;;;;;
+1617;CANADIAN SYLLABICS CARRIER JE;Lo;0;L;;;;;N;;;;;
+1618;CANADIAN SYLLABICS CARRIER JEE;Lo;0;L;;;;;N;;;;;
+1619;CANADIAN SYLLABICS CARRIER JI;Lo;0;L;;;;;N;;;;;
+161A;CANADIAN SYLLABICS SAYISI JI;Lo;0;L;;;;;N;;;;;
+161B;CANADIAN SYLLABICS CARRIER JA;Lo;0;L;;;;;N;;;;;
+161C;CANADIAN SYLLABICS CARRIER JJU;Lo;0;L;;;;;N;;;;;
+161D;CANADIAN SYLLABICS CARRIER JJO;Lo;0;L;;;;;N;;;;;
+161E;CANADIAN SYLLABICS CARRIER JJE;Lo;0;L;;;;;N;;;;;
+161F;CANADIAN SYLLABICS CARRIER JJEE;Lo;0;L;;;;;N;;;;;
+1620;CANADIAN SYLLABICS CARRIER JJI;Lo;0;L;;;;;N;;;;;
+1621;CANADIAN SYLLABICS CARRIER JJA;Lo;0;L;;;;;N;;;;;
+1622;CANADIAN SYLLABICS CARRIER LU;Lo;0;L;;;;;N;;;;;
+1623;CANADIAN SYLLABICS CARRIER LO;Lo;0;L;;;;;N;;;;;
+1624;CANADIAN SYLLABICS CARRIER LE;Lo;0;L;;;;;N;;;;;
+1625;CANADIAN SYLLABICS CARRIER LEE;Lo;0;L;;;;;N;;;;;
+1626;CANADIAN SYLLABICS CARRIER LI;Lo;0;L;;;;;N;;;;;
+1627;CANADIAN SYLLABICS CARRIER LA;Lo;0;L;;;;;N;;;;;
+1628;CANADIAN SYLLABICS CARRIER DLU;Lo;0;L;;;;;N;;;;;
+1629;CANADIAN SYLLABICS CARRIER DLO;Lo;0;L;;;;;N;;;;;
+162A;CANADIAN SYLLABICS CARRIER DLE;Lo;0;L;;;;;N;;;;;
+162B;CANADIAN SYLLABICS CARRIER DLEE;Lo;0;L;;;;;N;;;;;
+162C;CANADIAN SYLLABICS CARRIER DLI;Lo;0;L;;;;;N;;;;;
+162D;CANADIAN SYLLABICS CARRIER DLA;Lo;0;L;;;;;N;;;;;
+162E;CANADIAN SYLLABICS CARRIER LHU;Lo;0;L;;;;;N;;;;;
+162F;CANADIAN SYLLABICS CARRIER LHO;Lo;0;L;;;;;N;;;;;
+1630;CANADIAN SYLLABICS CARRIER LHE;Lo;0;L;;;;;N;;;;;
+1631;CANADIAN SYLLABICS CARRIER LHEE;Lo;0;L;;;;;N;;;;;
+1632;CANADIAN SYLLABICS CARRIER LHI;Lo;0;L;;;;;N;;;;;
+1633;CANADIAN SYLLABICS CARRIER LHA;Lo;0;L;;;;;N;;;;;
+1634;CANADIAN SYLLABICS CARRIER TLHU;Lo;0;L;;;;;N;;;;;
+1635;CANADIAN SYLLABICS CARRIER TLHO;Lo;0;L;;;;;N;;;;;
+1636;CANADIAN SYLLABICS CARRIER TLHE;Lo;0;L;;;;;N;;;;;
+1637;CANADIAN SYLLABICS CARRIER TLHEE;Lo;0;L;;;;;N;;;;;
+1638;CANADIAN SYLLABICS CARRIER TLHI;Lo;0;L;;;;;N;;;;;
+1639;CANADIAN SYLLABICS CARRIER TLHA;Lo;0;L;;;;;N;;;;;
+163A;CANADIAN SYLLABICS CARRIER TLU;Lo;0;L;;;;;N;;;;;
+163B;CANADIAN SYLLABICS CARRIER TLO;Lo;0;L;;;;;N;;;;;
+163C;CANADIAN SYLLABICS CARRIER TLE;Lo;0;L;;;;;N;;;;;
+163D;CANADIAN SYLLABICS CARRIER TLEE;Lo;0;L;;;;;N;;;;;
+163E;CANADIAN SYLLABICS CARRIER TLI;Lo;0;L;;;;;N;;;;;
+163F;CANADIAN SYLLABICS CARRIER TLA;Lo;0;L;;;;;N;;;;;
+1640;CANADIAN SYLLABICS CARRIER ZU;Lo;0;L;;;;;N;;;;;
+1641;CANADIAN SYLLABICS CARRIER ZO;Lo;0;L;;;;;N;;;;;
+1642;CANADIAN SYLLABICS CARRIER ZE;Lo;0;L;;;;;N;;;;;
+1643;CANADIAN SYLLABICS CARRIER ZEE;Lo;0;L;;;;;N;;;;;
+1644;CANADIAN SYLLABICS CARRIER ZI;Lo;0;L;;;;;N;;;;;
+1645;CANADIAN SYLLABICS CARRIER ZA;Lo;0;L;;;;;N;;;;;
+1646;CANADIAN SYLLABICS CARRIER Z;Lo;0;L;;;;;N;;;;;
+1647;CANADIAN SYLLABICS CARRIER INITIAL Z;Lo;0;L;;;;;N;;;;;
+1648;CANADIAN SYLLABICS CARRIER DZU;Lo;0;L;;;;;N;;;;;
+1649;CANADIAN SYLLABICS CARRIER DZO;Lo;0;L;;;;;N;;;;;
+164A;CANADIAN SYLLABICS CARRIER DZE;Lo;0;L;;;;;N;;;;;
+164B;CANADIAN SYLLABICS CARRIER DZEE;Lo;0;L;;;;;N;;;;;
+164C;CANADIAN SYLLABICS CARRIER DZI;Lo;0;L;;;;;N;;;;;
+164D;CANADIAN SYLLABICS CARRIER DZA;Lo;0;L;;;;;N;;;;;
+164E;CANADIAN SYLLABICS CARRIER SU;Lo;0;L;;;;;N;;;;;
+164F;CANADIAN SYLLABICS CARRIER SO;Lo;0;L;;;;;N;;;;;
+1650;CANADIAN SYLLABICS CARRIER SE;Lo;0;L;;;;;N;;;;;
+1651;CANADIAN SYLLABICS CARRIER SEE;Lo;0;L;;;;;N;;;;;
+1652;CANADIAN SYLLABICS CARRIER SI;Lo;0;L;;;;;N;;;;;
+1653;CANADIAN SYLLABICS CARRIER SA;Lo;0;L;;;;;N;;;;;
+1654;CANADIAN SYLLABICS CARRIER SHU;Lo;0;L;;;;;N;;;;;
+1655;CANADIAN SYLLABICS CARRIER SHO;Lo;0;L;;;;;N;;;;;
+1656;CANADIAN SYLLABICS CARRIER SHE;Lo;0;L;;;;;N;;;;;
+1657;CANADIAN SYLLABICS CARRIER SHEE;Lo;0;L;;;;;N;;;;;
+1658;CANADIAN SYLLABICS CARRIER SHI;Lo;0;L;;;;;N;;;;;
+1659;CANADIAN SYLLABICS CARRIER SHA;Lo;0;L;;;;;N;;;;;
+165A;CANADIAN SYLLABICS CARRIER SH;Lo;0;L;;;;;N;;;;;
+165B;CANADIAN SYLLABICS CARRIER TSU;Lo;0;L;;;;;N;;;;;
+165C;CANADIAN SYLLABICS CARRIER TSO;Lo;0;L;;;;;N;;;;;
+165D;CANADIAN SYLLABICS CARRIER TSE;Lo;0;L;;;;;N;;;;;
+165E;CANADIAN SYLLABICS CARRIER TSEE;Lo;0;L;;;;;N;;;;;
+165F;CANADIAN SYLLABICS CARRIER TSI;Lo;0;L;;;;;N;;;;;
+1660;CANADIAN SYLLABICS CARRIER TSA;Lo;0;L;;;;;N;;;;;
+1661;CANADIAN SYLLABICS CARRIER CHU;Lo;0;L;;;;;N;;;;;
+1662;CANADIAN SYLLABICS CARRIER CHO;Lo;0;L;;;;;N;;;;;
+1663;CANADIAN SYLLABICS CARRIER CHE;Lo;0;L;;;;;N;;;;;
+1664;CANADIAN SYLLABICS CARRIER CHEE;Lo;0;L;;;;;N;;;;;
+1665;CANADIAN SYLLABICS CARRIER CHI;Lo;0;L;;;;;N;;;;;
+1666;CANADIAN SYLLABICS CARRIER CHA;Lo;0;L;;;;;N;;;;;
+1667;CANADIAN SYLLABICS CARRIER TTSU;Lo;0;L;;;;;N;;;;;
+1668;CANADIAN SYLLABICS CARRIER TTSO;Lo;0;L;;;;;N;;;;;
+1669;CANADIAN SYLLABICS CARRIER TTSE;Lo;0;L;;;;;N;;;;;
+166A;CANADIAN SYLLABICS CARRIER TTSEE;Lo;0;L;;;;;N;;;;;
+166B;CANADIAN SYLLABICS CARRIER TTSI;Lo;0;L;;;;;N;;;;;
+166C;CANADIAN SYLLABICS CARRIER TTSA;Lo;0;L;;;;;N;;;;;
+166D;CANADIAN SYLLABICS CHI SIGN;Po;0;L;;;;;N;;;;;
+166E;CANADIAN SYLLABICS FULL STOP;Po;0;L;;;;;N;;;;;
+166F;CANADIAN SYLLABICS QAI;Lo;0;L;;;;;N;;;;;
+1670;CANADIAN SYLLABICS NGAI;Lo;0;L;;;;;N;;;;;
+1671;CANADIAN SYLLABICS NNGI;Lo;0;L;;;;;N;;;;;
+1672;CANADIAN SYLLABICS NNGII;Lo;0;L;;;;;N;;;;;
+1673;CANADIAN SYLLABICS NNGO;Lo;0;L;;;;;N;;;;;
+1674;CANADIAN SYLLABICS NNGOO;Lo;0;L;;;;;N;;;;;
+1675;CANADIAN SYLLABICS NNGA;Lo;0;L;;;;;N;;;;;
+1676;CANADIAN SYLLABICS NNGAA;Lo;0;L;;;;;N;;;;;
+1677;CANADIAN SYLLABICS WOODS-CREE THWEE;Lo;0;L;;;;;N;;;;;
+1678;CANADIAN SYLLABICS WOODS-CREE THWI;Lo;0;L;;;;;N;;;;;
+1679;CANADIAN SYLLABICS WOODS-CREE THWII;Lo;0;L;;;;;N;;;;;
+167A;CANADIAN SYLLABICS WOODS-CREE THWO;Lo;0;L;;;;;N;;;;;
+167B;CANADIAN SYLLABICS WOODS-CREE THWOO;Lo;0;L;;;;;N;;;;;
+167C;CANADIAN SYLLABICS WOODS-CREE THWA;Lo;0;L;;;;;N;;;;;
+167D;CANADIAN SYLLABICS WOODS-CREE THWAA;Lo;0;L;;;;;N;;;;;
+167E;CANADIAN SYLLABICS WOODS-CREE FINAL TH;Lo;0;L;;;;;N;;;;;
+167F;CANADIAN SYLLABICS BLACKFOOT W;Lo;0;L;;;;;N;;;;;
+1680;OGHAM SPACE MARK;Zs;0;WS;;;;;N;;;;;
+1681;OGHAM LETTER BEITH;Lo;0;L;;;;;N;;;;;
+1682;OGHAM LETTER LUIS;Lo;0;L;;;;;N;;;;;
+1683;OGHAM LETTER FEARN;Lo;0;L;;;;;N;;;;;
+1684;OGHAM LETTER SAIL;Lo;0;L;;;;;N;;;;;
+1685;OGHAM LETTER NION;Lo;0;L;;;;;N;;;;;
+1686;OGHAM LETTER UATH;Lo;0;L;;;;;N;;;;;
+1687;OGHAM LETTER DAIR;Lo;0;L;;;;;N;;;;;
+1688;OGHAM LETTER TINNE;Lo;0;L;;;;;N;;;;;
+1689;OGHAM LETTER COLL;Lo;0;L;;;;;N;;;;;
+168A;OGHAM LETTER CEIRT;Lo;0;L;;;;;N;;;;;
+168B;OGHAM LETTER MUIN;Lo;0;L;;;;;N;;;;;
+168C;OGHAM LETTER GORT;Lo;0;L;;;;;N;;;;;
+168D;OGHAM LETTER NGEADAL;Lo;0;L;;;;;N;;;;;
+168E;OGHAM LETTER STRAIF;Lo;0;L;;;;;N;;;;;
+168F;OGHAM LETTER RUIS;Lo;0;L;;;;;N;;;;;
+1690;OGHAM LETTER AILM;Lo;0;L;;;;;N;;;;;
+1691;OGHAM LETTER ONN;Lo;0;L;;;;;N;;;;;
+1692;OGHAM LETTER UR;Lo;0;L;;;;;N;;;;;
+1693;OGHAM LETTER EADHADH;Lo;0;L;;;;;N;;;;;
+1694;OGHAM LETTER IODHADH;Lo;0;L;;;;;N;;;;;
+1695;OGHAM LETTER EABHADH;Lo;0;L;;;;;N;;;;;
+1696;OGHAM LETTER OR;Lo;0;L;;;;;N;;;;;
+1697;OGHAM LETTER UILLEANN;Lo;0;L;;;;;N;;;;;
+1698;OGHAM LETTER IFIN;Lo;0;L;;;;;N;;;;;
+1699;OGHAM LETTER EAMHANCHOLL;Lo;0;L;;;;;N;;;;;
+169A;OGHAM LETTER PEITH;Lo;0;L;;;;;N;;;;;
+169B;OGHAM FEATHER MARK;Ps;0;ON;;;;;Y;;;;;
+169C;OGHAM REVERSED FEATHER MARK;Pe;0;ON;;;;;Y;;;;;
+16A0;RUNIC LETTER FEHU FEOH FE F;Lo;0;L;;;;;N;;;;;
+16A1;RUNIC LETTER V;Lo;0;L;;;;;N;;;;;
+16A2;RUNIC LETTER URUZ UR U;Lo;0;L;;;;;N;;;;;
+16A3;RUNIC LETTER YR;Lo;0;L;;;;;N;;;;;
+16A4;RUNIC LETTER Y;Lo;0;L;;;;;N;;;;;
+16A5;RUNIC LETTER W;Lo;0;L;;;;;N;;;;;
+16A6;RUNIC LETTER THURISAZ THURS THORN;Lo;0;L;;;;;N;;;;;
+16A7;RUNIC LETTER ETH;Lo;0;L;;;;;N;;;;;
+16A8;RUNIC LETTER ANSUZ A;Lo;0;L;;;;;N;;;;;
+16A9;RUNIC LETTER OS O;Lo;0;L;;;;;N;;;;;
+16AA;RUNIC LETTER AC A;Lo;0;L;;;;;N;;;;;
+16AB;RUNIC LETTER AESC;Lo;0;L;;;;;N;;;;;
+16AC;RUNIC LETTER LONG-BRANCH-OSS O;Lo;0;L;;;;;N;;;;;
+16AD;RUNIC LETTER SHORT-TWIG-OSS O;Lo;0;L;;;;;N;;;;;
+16AE;RUNIC LETTER O;Lo;0;L;;;;;N;;;;;
+16AF;RUNIC LETTER OE;Lo;0;L;;;;;N;;;;;
+16B0;RUNIC LETTER ON;Lo;0;L;;;;;N;;;;;
+16B1;RUNIC LETTER RAIDO RAD REID R;Lo;0;L;;;;;N;;;;;
+16B2;RUNIC LETTER KAUNA;Lo;0;L;;;;;N;;;;;
+16B3;RUNIC LETTER CEN;Lo;0;L;;;;;N;;;;;
+16B4;RUNIC LETTER KAUN K;Lo;0;L;;;;;N;;;;;
+16B5;RUNIC LETTER G;Lo;0;L;;;;;N;;;;;
+16B6;RUNIC LETTER ENG;Lo;0;L;;;;;N;;;;;
+16B7;RUNIC LETTER GEBO GYFU G;Lo;0;L;;;;;N;;;;;
+16B8;RUNIC LETTER GAR;Lo;0;L;;;;;N;;;;;
+16B9;RUNIC LETTER WUNJO WYNN W;Lo;0;L;;;;;N;;;;;
+16BA;RUNIC LETTER HAGLAZ H;Lo;0;L;;;;;N;;;;;
+16BB;RUNIC LETTER HAEGL H;Lo;0;L;;;;;N;;;;;
+16BC;RUNIC LETTER LONG-BRANCH-HAGALL H;Lo;0;L;;;;;N;;;;;
+16BD;RUNIC LETTER SHORT-TWIG-HAGALL H;Lo;0;L;;;;;N;;;;;
+16BE;RUNIC LETTER NAUDIZ NYD NAUD N;Lo;0;L;;;;;N;;;;;
+16BF;RUNIC LETTER SHORT-TWIG-NAUD N;Lo;0;L;;;;;N;;;;;
+16C0;RUNIC LETTER DOTTED-N;Lo;0;L;;;;;N;;;;;
+16C1;RUNIC LETTER ISAZ IS ISS I;Lo;0;L;;;;;N;;;;;
+16C2;RUNIC LETTER E;Lo;0;L;;;;;N;;;;;
+16C3;RUNIC LETTER JERAN J;Lo;0;L;;;;;N;;;;;
+16C4;RUNIC LETTER GER;Lo;0;L;;;;;N;;;;;
+16C5;RUNIC LETTER LONG-BRANCH-AR AE;Lo;0;L;;;;;N;;;;;
+16C6;RUNIC LETTER SHORT-TWIG-AR A;Lo;0;L;;;;;N;;;;;
+16C7;RUNIC LETTER IWAZ EOH;Lo;0;L;;;;;N;;;;;
+16C8;RUNIC LETTER PERTHO PEORTH P;Lo;0;L;;;;;N;;;;;
+16C9;RUNIC LETTER ALGIZ EOLHX;Lo;0;L;;;;;N;;;;;
+16CA;RUNIC LETTER SOWILO S;Lo;0;L;;;;;N;;;;;
+16CB;RUNIC LETTER SIGEL LONG-BRANCH-SOL S;Lo;0;L;;;;;N;;;;;
+16CC;RUNIC LETTER SHORT-TWIG-SOL S;Lo;0;L;;;;;N;;;;;
+16CD;RUNIC LETTER C;Lo;0;L;;;;;N;;;;;
+16CE;RUNIC LETTER Z;Lo;0;L;;;;;N;;;;;
+16CF;RUNIC LETTER TIWAZ TIR TYR T;Lo;0;L;;;;;N;;;;;
+16D0;RUNIC LETTER SHORT-TWIG-TYR T;Lo;0;L;;;;;N;;;;;
+16D1;RUNIC LETTER D;Lo;0;L;;;;;N;;;;;
+16D2;RUNIC LETTER BERKANAN BEORC BJARKAN B;Lo;0;L;;;;;N;;;;;
+16D3;RUNIC LETTER SHORT-TWIG-BJARKAN B;Lo;0;L;;;;;N;;;;;
+16D4;RUNIC LETTER DOTTED-P;Lo;0;L;;;;;N;;;;;
+16D5;RUNIC LETTER OPEN-P;Lo;0;L;;;;;N;;;;;
+16D6;RUNIC LETTER EHWAZ EH E;Lo;0;L;;;;;N;;;;;
+16D7;RUNIC LETTER MANNAZ MAN M;Lo;0;L;;;;;N;;;;;
+16D8;RUNIC LETTER LONG-BRANCH-MADR M;Lo;0;L;;;;;N;;;;;
+16D9;RUNIC LETTER SHORT-TWIG-MADR M;Lo;0;L;;;;;N;;;;;
+16DA;RUNIC LETTER LAUKAZ LAGU LOGR L;Lo;0;L;;;;;N;;;;;
+16DB;RUNIC LETTER DOTTED-L;Lo;0;L;;;;;N;;;;;
+16DC;RUNIC LETTER INGWAZ;Lo;0;L;;;;;N;;;;;
+16DD;RUNIC LETTER ING;Lo;0;L;;;;;N;;;;;
+16DE;RUNIC LETTER DAGAZ DAEG D;Lo;0;L;;;;;N;;;;;
+16DF;RUNIC LETTER OTHALAN ETHEL O;Lo;0;L;;;;;N;;;;;
+16E0;RUNIC LETTER EAR;Lo;0;L;;;;;N;;;;;
+16E1;RUNIC LETTER IOR;Lo;0;L;;;;;N;;;;;
+16E2;RUNIC LETTER CWEORTH;Lo;0;L;;;;;N;;;;;
+16E3;RUNIC LETTER CALC;Lo;0;L;;;;;N;;;;;
+16E4;RUNIC LETTER CEALC;Lo;0;L;;;;;N;;;;;
+16E5;RUNIC LETTER STAN;Lo;0;L;;;;;N;;;;;
+16E6;RUNIC LETTER LONG-BRANCH-YR;Lo;0;L;;;;;N;;;;;
+16E7;RUNIC LETTER SHORT-TWIG-YR;Lo;0;L;;;;;N;;;;;
+16E8;RUNIC LETTER ICELANDIC-YR;Lo;0;L;;;;;N;;;;;
+16E9;RUNIC LETTER Q;Lo;0;L;;;;;N;;;;;
+16EA;RUNIC LETTER X;Lo;0;L;;;;;N;;;;;
+16EB;RUNIC SINGLE PUNCTUATION;Po;0;L;;;;;N;;;;;
+16EC;RUNIC MULTIPLE PUNCTUATION;Po;0;L;;;;;N;;;;;
+16ED;RUNIC CROSS PUNCTUATION;Po;0;L;;;;;N;;;;;
+16EE;RUNIC ARLAUG SYMBOL;Nl;0;L;;;;17;N;;;;;
+16EF;RUNIC TVIMADUR SYMBOL;Nl;0;L;;;;18;N;;;;;
+16F0;RUNIC BELGTHOR SYMBOL;Nl;0;L;;;;19;N;;;;;
+16F1;RUNIC LETTER K;Lo;0;L;;;;;N;;;;;
+16F2;RUNIC LETTER SH;Lo;0;L;;;;;N;;;;;
+16F3;RUNIC LETTER OO;Lo;0;L;;;;;N;;;;;
+16F4;RUNIC LETTER FRANKS CASKET OS;Lo;0;L;;;;;N;;;;;
+16F5;RUNIC LETTER FRANKS CASKET IS;Lo;0;L;;;;;N;;;;;
+16F6;RUNIC LETTER FRANKS CASKET EH;Lo;0;L;;;;;N;;;;;
+16F7;RUNIC LETTER FRANKS CASKET AC;Lo;0;L;;;;;N;;;;;
+16F8;RUNIC LETTER FRANKS CASKET AESC;Lo;0;L;;;;;N;;;;;
+1700;TAGALOG LETTER A;Lo;0;L;;;;;N;;;;;
+1701;TAGALOG LETTER I;Lo;0;L;;;;;N;;;;;
+1702;TAGALOG LETTER U;Lo;0;L;;;;;N;;;;;
+1703;TAGALOG LETTER KA;Lo;0;L;;;;;N;;;;;
+1704;TAGALOG LETTER GA;Lo;0;L;;;;;N;;;;;
+1705;TAGALOG LETTER NGA;Lo;0;L;;;;;N;;;;;
+1706;TAGALOG LETTER TA;Lo;0;L;;;;;N;;;;;
+1707;TAGALOG LETTER DA;Lo;0;L;;;;;N;;;;;
+1708;TAGALOG LETTER NA;Lo;0;L;;;;;N;;;;;
+1709;TAGALOG LETTER PA;Lo;0;L;;;;;N;;;;;
+170A;TAGALOG LETTER BA;Lo;0;L;;;;;N;;;;;
+170B;TAGALOG LETTER MA;Lo;0;L;;;;;N;;;;;
+170C;TAGALOG LETTER YA;Lo;0;L;;;;;N;;;;;
+170E;TAGALOG LETTER LA;Lo;0;L;;;;;N;;;;;
+170F;TAGALOG LETTER WA;Lo;0;L;;;;;N;;;;;
+1710;TAGALOG LETTER SA;Lo;0;L;;;;;N;;;;;
+1711;TAGALOG LETTER HA;Lo;0;L;;;;;N;;;;;
+1712;TAGALOG VOWEL SIGN I;Mn;0;NSM;;;;;N;;;;;
+1713;TAGALOG VOWEL SIGN U;Mn;0;NSM;;;;;N;;;;;
+1714;TAGALOG SIGN VIRAMA;Mn;9;NSM;;;;;N;;;;;
+1720;HANUNOO LETTER A;Lo;0;L;;;;;N;;;;;
+1721;HANUNOO LETTER I;Lo;0;L;;;;;N;;;;;
+1722;HANUNOO LETTER U;Lo;0;L;;;;;N;;;;;
+1723;HANUNOO LETTER KA;Lo;0;L;;;;;N;;;;;
+1724;HANUNOO LETTER GA;Lo;0;L;;;;;N;;;;;
+1725;HANUNOO LETTER NGA;Lo;0;L;;;;;N;;;;;
+1726;HANUNOO LETTER TA;Lo;0;L;;;;;N;;;;;
+1727;HANUNOO LETTER DA;Lo;0;L;;;;;N;;;;;
+1728;HANUNOO LETTER NA;Lo;0;L;;;;;N;;;;;
+1729;HANUNOO LETTER PA;Lo;0;L;;;;;N;;;;;
+172A;HANUNOO LETTER BA;Lo;0;L;;;;;N;;;;;
+172B;HANUNOO LETTER MA;Lo;0;L;;;;;N;;;;;
+172C;HANUNOO LETTER YA;Lo;0;L;;;;;N;;;;;
+172D;HANUNOO LETTER RA;Lo;0;L;;;;;N;;;;;
+172E;HANUNOO LETTER LA;Lo;0;L;;;;;N;;;;;
+172F;HANUNOO LETTER WA;Lo;0;L;;;;;N;;;;;
+1730;HANUNOO LETTER SA;Lo;0;L;;;;;N;;;;;
+1731;HANUNOO LETTER HA;Lo;0;L;;;;;N;;;;;
+1732;HANUNOO VOWEL SIGN I;Mn;0;NSM;;;;;N;;;;;
+1733;HANUNOO VOWEL SIGN U;Mn;0;NSM;;;;;N;;;;;
+1734;HANUNOO SIGN PAMUDPOD;Mn;9;NSM;;;;;N;;;;;
+1735;PHILIPPINE SINGLE PUNCTUATION;Po;0;L;;;;;N;;;;;
+1736;PHILIPPINE DOUBLE PUNCTUATION;Po;0;L;;;;;N;;;;;
+1740;BUHID LETTER A;Lo;0;L;;;;;N;;;;;
+1741;BUHID LETTER I;Lo;0;L;;;;;N;;;;;
+1742;BUHID LETTER U;Lo;0;L;;;;;N;;;;;
+1743;BUHID LETTER KA;Lo;0;L;;;;;N;;;;;
+1744;BUHID LETTER GA;Lo;0;L;;;;;N;;;;;
+1745;BUHID LETTER NGA;Lo;0;L;;;;;N;;;;;
+1746;BUHID LETTER TA;Lo;0;L;;;;;N;;;;;
+1747;BUHID LETTER DA;Lo;0;L;;;;;N;;;;;
+1748;BUHID LETTER NA;Lo;0;L;;;;;N;;;;;
+1749;BUHID LETTER PA;Lo;0;L;;;;;N;;;;;
+174A;BUHID LETTER BA;Lo;0;L;;;;;N;;;;;
+174B;BUHID LETTER MA;Lo;0;L;;;;;N;;;;;
+174C;BUHID LETTER YA;Lo;0;L;;;;;N;;;;;
+174D;BUHID LETTER RA;Lo;0;L;;;;;N;;;;;
+174E;BUHID LETTER LA;Lo;0;L;;;;;N;;;;;
+174F;BUHID LETTER WA;Lo;0;L;;;;;N;;;;;
+1750;BUHID LETTER SA;Lo;0;L;;;;;N;;;;;
+1751;BUHID LETTER HA;Lo;0;L;;;;;N;;;;;
+1752;BUHID VOWEL SIGN I;Mn;0;NSM;;;;;N;;;;;
+1753;BUHID VOWEL SIGN U;Mn;0;NSM;;;;;N;;;;;
+1760;TAGBANWA LETTER A;Lo;0;L;;;;;N;;;;;
+1761;TAGBANWA LETTER I;Lo;0;L;;;;;N;;;;;
+1762;TAGBANWA LETTER U;Lo;0;L;;;;;N;;;;;
+1763;TAGBANWA LETTER KA;Lo;0;L;;;;;N;;;;;
+1764;TAGBANWA LETTER GA;Lo;0;L;;;;;N;;;;;
+1765;TAGBANWA LETTER NGA;Lo;0;L;;;;;N;;;;;
+1766;TAGBANWA LETTER TA;Lo;0;L;;;;;N;;;;;
+1767;TAGBANWA LETTER DA;Lo;0;L;;;;;N;;;;;
+1768;TAGBANWA LETTER NA;Lo;0;L;;;;;N;;;;;
+1769;TAGBANWA LETTER PA;Lo;0;L;;;;;N;;;;;
+176A;TAGBANWA LETTER BA;Lo;0;L;;;;;N;;;;;
+176B;TAGBANWA LETTER MA;Lo;0;L;;;;;N;;;;;
+176C;TAGBANWA LETTER YA;Lo;0;L;;;;;N;;;;;
+176E;TAGBANWA LETTER LA;Lo;0;L;;;;;N;;;;;
+176F;TAGBANWA LETTER WA;Lo;0;L;;;;;N;;;;;
+1770;TAGBANWA LETTER SA;Lo;0;L;;;;;N;;;;;
+1772;TAGBANWA VOWEL SIGN I;Mn;0;NSM;;;;;N;;;;;
+1773;TAGBANWA VOWEL SIGN U;Mn;0;NSM;;;;;N;;;;;
+1780;KHMER LETTER KA;Lo;0;L;;;;;N;;;;;
+1781;KHMER LETTER KHA;Lo;0;L;;;;;N;;;;;
+1782;KHMER LETTER KO;Lo;0;L;;;;;N;;;;;
+1783;KHMER LETTER KHO;Lo;0;L;;;;;N;;;;;
+1784;KHMER LETTER NGO;Lo;0;L;;;;;N;;;;;
+1785;KHMER LETTER CA;Lo;0;L;;;;;N;;;;;
+1786;KHMER LETTER CHA;Lo;0;L;;;;;N;;;;;
+1787;KHMER LETTER CO;Lo;0;L;;;;;N;;;;;
+1788;KHMER LETTER CHO;Lo;0;L;;;;;N;;;;;
+1789;KHMER LETTER NYO;Lo;0;L;;;;;N;;;;;
+178A;KHMER LETTER DA;Lo;0;L;;;;;N;;;;;
+178B;KHMER LETTER TTHA;Lo;0;L;;;;;N;;;;;
+178C;KHMER LETTER DO;Lo;0;L;;;;;N;;;;;
+178D;KHMER LETTER TTHO;Lo;0;L;;;;;N;;;;;
+178E;KHMER LETTER NNO;Lo;0;L;;;;;N;;;;;
+178F;KHMER LETTER TA;Lo;0;L;;;;;N;;;;;
+1790;KHMER LETTER THA;Lo;0;L;;;;;N;;;;;
+1791;KHMER LETTER TO;Lo;0;L;;;;;N;;;;;
+1792;KHMER LETTER THO;Lo;0;L;;;;;N;;;;;
+1793;KHMER LETTER NO;Lo;0;L;;;;;N;;;;;
+1794;KHMER LETTER BA;Lo;0;L;;;;;N;;;;;
+1795;KHMER LETTER PHA;Lo;0;L;;;;;N;;;;;
+1796;KHMER LETTER PO;Lo;0;L;;;;;N;;;;;
+1797;KHMER LETTER PHO;Lo;0;L;;;;;N;;;;;
+1798;KHMER LETTER MO;Lo;0;L;;;;;N;;;;;
+1799;KHMER LETTER YO;Lo;0;L;;;;;N;;;;;
+179A;KHMER LETTER RO;Lo;0;L;;;;;N;;;;;
+179B;KHMER LETTER LO;Lo;0;L;;;;;N;;;;;
+179C;KHMER LETTER VO;Lo;0;L;;;;;N;;;;;
+179D;KHMER LETTER SHA;Lo;0;L;;;;;N;;;;;
+179E;KHMER LETTER SSO;Lo;0;L;;;;;N;;;;;
+179F;KHMER LETTER SA;Lo;0;L;;;;;N;;;;;
+17A0;KHMER LETTER HA;Lo;0;L;;;;;N;;;;;
+17A1;KHMER LETTER LA;Lo;0;L;;;;;N;;;;;
+17A2;KHMER LETTER QA;Lo;0;L;;;;;N;;;;;
+17A3;KHMER INDEPENDENT VOWEL QAQ;Lo;0;L;;;;;N;;;;;
+17A4;KHMER INDEPENDENT VOWEL QAA;Lo;0;L;;;;;N;;;;;
+17A5;KHMER INDEPENDENT VOWEL QI;Lo;0;L;;;;;N;;;;;
+17A6;KHMER INDEPENDENT VOWEL QII;Lo;0;L;;;;;N;;;;;
+17A7;KHMER INDEPENDENT VOWEL QU;Lo;0;L;;;;;N;;;;;
+17A8;KHMER INDEPENDENT VOWEL QUK;Lo;0;L;;;;;N;;;;;
+17A9;KHMER INDEPENDENT VOWEL QUU;Lo;0;L;;;;;N;;;;;
+17AA;KHMER INDEPENDENT VOWEL QUUV;Lo;0;L;;;;;N;;;;;
+17AB;KHMER INDEPENDENT VOWEL RY;Lo;0;L;;;;;N;;;;;
+17AC;KHMER INDEPENDENT VOWEL RYY;Lo;0;L;;;;;N;;;;;
+17AD;KHMER INDEPENDENT VOWEL LY;Lo;0;L;;;;;N;;;;;
+17AE;KHMER INDEPENDENT VOWEL LYY;Lo;0;L;;;;;N;;;;;
+17AF;KHMER INDEPENDENT VOWEL QE;Lo;0;L;;;;;N;;;;;
+17B0;KHMER INDEPENDENT VOWEL QAI;Lo;0;L;;;;;N;;;;;
+17B1;KHMER INDEPENDENT VOWEL QOO TYPE ONE;Lo;0;L;;;;;N;;;;;
+17B2;KHMER INDEPENDENT VOWEL QOO TYPE TWO;Lo;0;L;;;;;N;;;;;
+17B3;KHMER INDEPENDENT VOWEL QAU;Lo;0;L;;;;;N;;;;;
+17B4;KHMER VOWEL INHERENT AQ;Mn;0;NSM;;;;;N;;;;;
+17B5;KHMER VOWEL INHERENT AA;Mn;0;NSM;;;;;N;;;;;
+17B6;KHMER VOWEL SIGN AA;Mc;0;L;;;;;N;;;;;
+17B7;KHMER VOWEL SIGN I;Mn;0;NSM;;;;;N;;;;;
+17B8;KHMER VOWEL SIGN II;Mn;0;NSM;;;;;N;;;;;
+17B9;KHMER VOWEL SIGN Y;Mn;0;NSM;;;;;N;;;;;
+17BA;KHMER VOWEL SIGN YY;Mn;0;NSM;;;;;N;;;;;
+17BB;KHMER VOWEL SIGN U;Mn;0;NSM;;;;;N;;;;;
+17BC;KHMER VOWEL SIGN UU;Mn;0;NSM;;;;;N;;;;;
+17BD;KHMER VOWEL SIGN UA;Mn;0;NSM;;;;;N;;;;;
+17BE;KHMER VOWEL SIGN OE;Mc;0;L;;;;;N;;;;;
+17BF;KHMER VOWEL SIGN YA;Mc;0;L;;;;;N;;;;;
+17C0;KHMER VOWEL SIGN IE;Mc;0;L;;;;;N;;;;;
+17C1;KHMER VOWEL SIGN E;Mc;0;L;;;;;N;;;;;
+17C2;KHMER VOWEL SIGN AE;Mc;0;L;;;;;N;;;;;
+17C3;KHMER VOWEL SIGN AI;Mc;0;L;;;;;N;;;;;
+17C4;KHMER VOWEL SIGN OO;Mc;0;L;;;;;N;;;;;
+17C5;KHMER VOWEL SIGN AU;Mc;0;L;;;;;N;;;;;
+17C6;KHMER SIGN NIKAHIT;Mn;0;NSM;;;;;N;;;;;
+17C7;KHMER SIGN REAHMUK;Mc;0;L;;;;;N;;;;;
+17C8;KHMER SIGN YUUKALEAPINTU;Mc;0;L;;;;;N;;;;;
+17C9;KHMER SIGN MUUSIKATOAN;Mn;0;NSM;;;;;N;;;;;
+17CA;KHMER SIGN TRIISAP;Mn;0;NSM;;;;;N;;;;;
+17CB;KHMER SIGN BANTOC;Mn;0;NSM;;;;;N;;;;;
+17CC;KHMER SIGN ROBAT;Mn;0;NSM;;;;;N;;;;;
+17CD;KHMER SIGN TOANDAKHIAT;Mn;0;NSM;;;;;N;;;;;
+17CE;KHMER SIGN KAKABAT;Mn;0;NSM;;;;;N;;;;;
+17CF;KHMER SIGN AHSDA;Mn;0;NSM;;;;;N;;;;;
+17D0;KHMER SIGN SAMYOK SANNYA;Mn;0;NSM;;;;;N;;;;;
+17D1;KHMER SIGN VIRIAM;Mn;0;NSM;;;;;N;;;;;
+17D2;KHMER SIGN COENG;Mn;9;NSM;;;;;N;;;;;
+17D3;KHMER SIGN BATHAMASAT;Mn;0;NSM;;;;;N;;;;;
+17D4;KHMER SIGN KHAN;Po;0;L;;;;;N;;;;;
+17D5;KHMER SIGN BARIYOOSAN;Po;0;L;;;;;N;;;;;
+17D6;KHMER SIGN CAMNUC PII KUUH;Po;0;L;;;;;N;;;;;
+17D7;KHMER SIGN LEK TOO;Lm;0;L;;;;;N;;;;;
+17D8;KHMER SIGN BEYYAL;Po;0;L;;;;;N;;;;;
+17D9;KHMER SIGN PHNAEK MUAN;Po;0;L;;;;;N;;;;;
+17DA;KHMER SIGN KOOMUUT;Po;0;L;;;;;N;;;;;
+17DB;KHMER CURRENCY SYMBOL RIEL;Sc;0;ET;;;;;N;;;;;
+17DC;KHMER SIGN AVAKRAHASANYA;Lo;0;L;;;;;N;;;;;
+17DD;KHMER SIGN ATTHACAN;Mn;230;NSM;;;;;N;;;;;
+17E0;KHMER DIGIT ZERO;Nd;0;L;;0;0;0;N;;;;;
+17E1;KHMER DIGIT ONE;Nd;0;L;;1;1;1;N;;;;;
+17E2;KHMER DIGIT TWO;Nd;0;L;;2;2;2;N;;;;;
+17E3;KHMER DIGIT THREE;Nd;0;L;;3;3;3;N;;;;;
+17E4;KHMER DIGIT FOUR;Nd;0;L;;4;4;4;N;;;;;
+17E5;KHMER DIGIT FIVE;Nd;0;L;;5;5;5;N;;;;;
+17E6;KHMER DIGIT SIX;Nd;0;L;;6;6;6;N;;;;;
+17E7;KHMER DIGIT SEVEN;Nd;0;L;;7;7;7;N;;;;;
+17E8;KHMER DIGIT EIGHT;Nd;0;L;;8;8;8;N;;;;;
+17E9;KHMER DIGIT NINE;Nd;0;L;;9;9;9;N;;;;;
+17F0;KHMER SYMBOL LEK ATTAK SON;No;0;ON;;;;0;N;;;;;
+17F1;KHMER SYMBOL LEK ATTAK MUOY;No;0;ON;;;;1;N;;;;;
+17F2;KHMER SYMBOL LEK ATTAK PII;No;0;ON;;;;2;N;;;;;
+17F3;KHMER SYMBOL LEK ATTAK BEI;No;0;ON;;;;3;N;;;;;
+17F4;KHMER SYMBOL LEK ATTAK BUON;No;0;ON;;;;4;N;;;;;
+17F5;KHMER SYMBOL LEK ATTAK PRAM;No;0;ON;;;;5;N;;;;;
+17F6;KHMER SYMBOL LEK ATTAK PRAM-MUOY;No;0;ON;;;;6;N;;;;;
+17F7;KHMER SYMBOL LEK ATTAK PRAM-PII;No;0;ON;;;;7;N;;;;;
+17F8;KHMER SYMBOL LEK ATTAK PRAM-BEI;No;0;ON;;;;8;N;;;;;
+17F9;KHMER SYMBOL LEK ATTAK PRAM-BUON;No;0;ON;;;;9;N;;;;;
+1800;MONGOLIAN BIRGA;Po;0;ON;;;;;N;;;;;
+1801;MONGOLIAN ELLIPSIS;Po;0;ON;;;;;N;;;;;
+1802;MONGOLIAN COMMA;Po;0;ON;;;;;N;;;;;
+1803;MONGOLIAN FULL STOP;Po;0;ON;;;;;N;;;;;
+1804;MONGOLIAN COLON;Po;0;ON;;;;;N;;;;;
+1805;MONGOLIAN FOUR DOTS;Po;0;ON;;;;;N;;;;;
+1806;MONGOLIAN TODO SOFT HYPHEN;Pd;0;ON;;;;;N;;;;;
+1807;MONGOLIAN SIBE SYLLABLE BOUNDARY MARKER;Po;0;ON;;;;;N;;;;;
+1808;MONGOLIAN MANCHU COMMA;Po;0;ON;;;;;N;;;;;
+1809;MONGOLIAN MANCHU FULL STOP;Po;0;ON;;;;;N;;;;;
+180A;MONGOLIAN NIRUGU;Po;0;ON;;;;;N;;;;;
+180B;MONGOLIAN FREE VARIATION SELECTOR ONE;Mn;0;NSM;;;;;N;;;;;
+180C;MONGOLIAN FREE VARIATION SELECTOR TWO;Mn;0;NSM;;;;;N;;;;;
+180D;MONGOLIAN FREE VARIATION SELECTOR THREE;Mn;0;NSM;;;;;N;;;;;
+180E;MONGOLIAN VOWEL SEPARATOR;Cf;0;BN;;;;;N;;;;;
+1810;MONGOLIAN DIGIT ZERO;Nd;0;L;;0;0;0;N;;;;;
+1811;MONGOLIAN DIGIT ONE;Nd;0;L;;1;1;1;N;;;;;
+1812;MONGOLIAN DIGIT TWO;Nd;0;L;;2;2;2;N;;;;;
+1813;MONGOLIAN DIGIT THREE;Nd;0;L;;3;3;3;N;;;;;
+1814;MONGOLIAN DIGIT FOUR;Nd;0;L;;4;4;4;N;;;;;
+1815;MONGOLIAN DIGIT FIVE;Nd;0;L;;5;5;5;N;;;;;
+1816;MONGOLIAN DIGIT SIX;Nd;0;L;;6;6;6;N;;;;;
+1817;MONGOLIAN DIGIT SEVEN;Nd;0;L;;7;7;7;N;;;;;
+1818;MONGOLIAN DIGIT EIGHT;Nd;0;L;;8;8;8;N;;;;;
+1819;MONGOLIAN DIGIT NINE;Nd;0;L;;9;9;9;N;;;;;
+1820;MONGOLIAN LETTER A;Lo;0;L;;;;;N;;;;;
+1821;MONGOLIAN LETTER E;Lo;0;L;;;;;N;;;;;
+1822;MONGOLIAN LETTER I;Lo;0;L;;;;;N;;;;;
+1823;MONGOLIAN LETTER O;Lo;0;L;;;;;N;;;;;
+1824;MONGOLIAN LETTER U;Lo;0;L;;;;;N;;;;;
+1825;MONGOLIAN LETTER OE;Lo;0;L;;;;;N;;;;;
+1826;MONGOLIAN LETTER UE;Lo;0;L;;;;;N;;;;;
+1827;MONGOLIAN LETTER EE;Lo;0;L;;;;;N;;;;;
+1828;MONGOLIAN LETTER NA;Lo;0;L;;;;;N;;;;;
+1829;MONGOLIAN LETTER ANG;Lo;0;L;;;;;N;;;;;
+182A;MONGOLIAN LETTER BA;Lo;0;L;;;;;N;;;;;
+182B;MONGOLIAN LETTER PA;Lo;0;L;;;;;N;;;;;
+182C;MONGOLIAN LETTER QA;Lo;0;L;;;;;N;;;;;
+182D;MONGOLIAN LETTER GA;Lo;0;L;;;;;N;;;;;
+182E;MONGOLIAN LETTER MA;Lo;0;L;;;;;N;;;;;
+182F;MONGOLIAN LETTER LA;Lo;0;L;;;;;N;;;;;
+1830;MONGOLIAN LETTER SA;Lo;0;L;;;;;N;;;;;
+1831;MONGOLIAN LETTER SHA;Lo;0;L;;;;;N;;;;;
+1832;MONGOLIAN LETTER TA;Lo;0;L;;;;;N;;;;;
+1833;MONGOLIAN LETTER DA;Lo;0;L;;;;;N;;;;;
+1834;MONGOLIAN LETTER CHA;Lo;0;L;;;;;N;;;;;
+1835;MONGOLIAN LETTER JA;Lo;0;L;;;;;N;;;;;
+1836;MONGOLIAN LETTER YA;Lo;0;L;;;;;N;;;;;
+1837;MONGOLIAN LETTER RA;Lo;0;L;;;;;N;;;;;
+1838;MONGOLIAN LETTER WA;Lo;0;L;;;;;N;;;;;
+1839;MONGOLIAN LETTER FA;Lo;0;L;;;;;N;;;;;
+183A;MONGOLIAN LETTER KA;Lo;0;L;;;;;N;;;;;
+183B;MONGOLIAN LETTER KHA;Lo;0;L;;;;;N;;;;;
+183C;MONGOLIAN LETTER TSA;Lo;0;L;;;;;N;;;;;
+183D;MONGOLIAN LETTER ZA;Lo;0;L;;;;;N;;;;;
+183E;MONGOLIAN LETTER HAA;Lo;0;L;;;;;N;;;;;
+183F;MONGOLIAN LETTER ZRA;Lo;0;L;;;;;N;;;;;
+1840;MONGOLIAN LETTER LHA;Lo;0;L;;;;;N;;;;;
+1841;MONGOLIAN LETTER ZHI;Lo;0;L;;;;;N;;;;;
+1842;MONGOLIAN LETTER CHI;Lo;0;L;;;;;N;;;;;
+1843;MONGOLIAN LETTER TODO LONG VOWEL SIGN;Lm;0;L;;;;;N;;;;;
+1844;MONGOLIAN LETTER TODO E;Lo;0;L;;;;;N;;;;;
+1845;MONGOLIAN LETTER TODO I;Lo;0;L;;;;;N;;;;;
+1846;MONGOLIAN LETTER TODO O;Lo;0;L;;;;;N;;;;;
+1847;MONGOLIAN LETTER TODO U;Lo;0;L;;;;;N;;;;;
+1848;MONGOLIAN LETTER TODO OE;Lo;0;L;;;;;N;;;;;
+1849;MONGOLIAN LETTER TODO UE;Lo;0;L;;;;;N;;;;;
+184A;MONGOLIAN LETTER TODO ANG;Lo;0;L;;;;;N;;;;;
+184B;MONGOLIAN LETTER TODO BA;Lo;0;L;;;;;N;;;;;
+184C;MONGOLIAN LETTER TODO PA;Lo;0;L;;;;;N;;;;;
+184D;MONGOLIAN LETTER TODO QA;Lo;0;L;;;;;N;;;;;
+184E;MONGOLIAN LETTER TODO GA;Lo;0;L;;;;;N;;;;;
+184F;MONGOLIAN LETTER TODO MA;Lo;0;L;;;;;N;;;;;
+1850;MONGOLIAN LETTER TODO TA;Lo;0;L;;;;;N;;;;;
+1851;MONGOLIAN LETTER TODO DA;Lo;0;L;;;;;N;;;;;
+1852;MONGOLIAN LETTER TODO CHA;Lo;0;L;;;;;N;;;;;
+1853;MONGOLIAN LETTER TODO JA;Lo;0;L;;;;;N;;;;;
+1854;MONGOLIAN LETTER TODO TSA;Lo;0;L;;;;;N;;;;;
+1855;MONGOLIAN LETTER TODO YA;Lo;0;L;;;;;N;;;;;
+1856;MONGOLIAN LETTER TODO WA;Lo;0;L;;;;;N;;;;;
+1857;MONGOLIAN LETTER TODO KA;Lo;0;L;;;;;N;;;;;
+1858;MONGOLIAN LETTER TODO GAA;Lo;0;L;;;;;N;;;;;
+1859;MONGOLIAN LETTER TODO HAA;Lo;0;L;;;;;N;;;;;
+185A;MONGOLIAN LETTER TODO JIA;Lo;0;L;;;;;N;;;;;
+185B;MONGOLIAN LETTER TODO NIA;Lo;0;L;;;;;N;;;;;
+185C;MONGOLIAN LETTER TODO DZA;Lo;0;L;;;;;N;;;;;
+185D;MONGOLIAN LETTER SIBE E;Lo;0;L;;;;;N;;;;;
+185E;MONGOLIAN LETTER SIBE I;Lo;0;L;;;;;N;;;;;
+185F;MONGOLIAN LETTER SIBE IY;Lo;0;L;;;;;N;;;;;
+1860;MONGOLIAN LETTER SIBE UE;Lo;0;L;;;;;N;;;;;
+1861;MONGOLIAN LETTER SIBE U;Lo;0;L;;;;;N;;;;;
+1862;MONGOLIAN LETTER SIBE ANG;Lo;0;L;;;;;N;;;;;
+1863;MONGOLIAN LETTER SIBE KA;Lo;0;L;;;;;N;;;;;
+1864;MONGOLIAN LETTER SIBE GA;Lo;0;L;;;;;N;;;;;
+1865;MONGOLIAN LETTER SIBE HA;Lo;0;L;;;;;N;;;;;
+1866;MONGOLIAN LETTER SIBE PA;Lo;0;L;;;;;N;;;;;
+1867;MONGOLIAN LETTER SIBE SHA;Lo;0;L;;;;;N;;;;;
+1868;MONGOLIAN LETTER SIBE TA;Lo;0;L;;;;;N;;;;;
+1869;MONGOLIAN LETTER SIBE DA;Lo;0;L;;;;;N;;;;;
+186A;MONGOLIAN LETTER SIBE JA;Lo;0;L;;;;;N;;;;;
+186B;MONGOLIAN LETTER SIBE FA;Lo;0;L;;;;;N;;;;;
+186C;MONGOLIAN LETTER SIBE GAA;Lo;0;L;;;;;N;;;;;
+186D;MONGOLIAN LETTER SIBE HAA;Lo;0;L;;;;;N;;;;;
+186E;MONGOLIAN LETTER SIBE TSA;Lo;0;L;;;;;N;;;;;
+186F;MONGOLIAN LETTER SIBE ZA;Lo;0;L;;;;;N;;;;;
+1870;MONGOLIAN LETTER SIBE RAA;Lo;0;L;;;;;N;;;;;
+1871;MONGOLIAN LETTER SIBE CHA;Lo;0;L;;;;;N;;;;;
+1872;MONGOLIAN LETTER SIBE ZHA;Lo;0;L;;;;;N;;;;;
+1873;MONGOLIAN LETTER MANCHU I;Lo;0;L;;;;;N;;;;;
+1874;MONGOLIAN LETTER MANCHU KA;Lo;0;L;;;;;N;;;;;
+1875;MONGOLIAN LETTER MANCHU RA;Lo;0;L;;;;;N;;;;;
+1876;MONGOLIAN LETTER MANCHU FA;Lo;0;L;;;;;N;;;;;
+1877;MONGOLIAN LETTER MANCHU ZHA;Lo;0;L;;;;;N;;;;;
+1880;MONGOLIAN LETTER ALI GALI ANUSVARA ONE;Lo;0;L;;;;;N;;;;;
+1881;MONGOLIAN LETTER ALI GALI VISARGA ONE;Lo;0;L;;;;;N;;;;;
+1882;MONGOLIAN LETTER ALI GALI DAMARU;Lo;0;L;;;;;N;;;;;
+1883;MONGOLIAN LETTER ALI GALI UBADAMA;Lo;0;L;;;;;N;;;;;
+1884;MONGOLIAN LETTER ALI GALI INVERTED UBADAMA;Lo;0;L;;;;;N;;;;;
+1885;MONGOLIAN LETTER ALI GALI BALUDA;Mn;0;NSM;;;;;N;;;;;
+1886;MONGOLIAN LETTER ALI GALI THREE BALUDA;Mn;0;NSM;;;;;N;;;;;
+1887;MONGOLIAN LETTER ALI GALI A;Lo;0;L;;;;;N;;;;;
+1888;MONGOLIAN LETTER ALI GALI I;Lo;0;L;;;;;N;;;;;
+1889;MONGOLIAN LETTER ALI GALI KA;Lo;0;L;;;;;N;;;;;
+188A;MONGOLIAN LETTER ALI GALI NGA;Lo;0;L;;;;;N;;;;;
+188B;MONGOLIAN LETTER ALI GALI CA;Lo;0;L;;;;;N;;;;;
+188C;MONGOLIAN LETTER ALI GALI TTA;Lo;0;L;;;;;N;;;;;
+188D;MONGOLIAN LETTER ALI GALI TTHA;Lo;0;L;;;;;N;;;;;
+188E;MONGOLIAN LETTER ALI GALI DDA;Lo;0;L;;;;;N;;;;;
+188F;MONGOLIAN LETTER ALI GALI NNA;Lo;0;L;;;;;N;;;;;
+1890;MONGOLIAN LETTER ALI GALI TA;Lo;0;L;;;;;N;;;;;
+1891;MONGOLIAN LETTER ALI GALI DA;Lo;0;L;;;;;N;;;;;
+1892;MONGOLIAN LETTER ALI GALI PA;Lo;0;L;;;;;N;;;;;
+1893;MONGOLIAN LETTER ALI GALI PHA;Lo;0;L;;;;;N;;;;;
+1894;MONGOLIAN LETTER ALI GALI SSA;Lo;0;L;;;;;N;;;;;
+1895;MONGOLIAN LETTER ALI GALI ZHA;Lo;0;L;;;;;N;;;;;
+1896;MONGOLIAN LETTER ALI GALI ZA;Lo;0;L;;;;;N;;;;;
+1897;MONGOLIAN LETTER ALI GALI AH;Lo;0;L;;;;;N;;;;;
+1898;MONGOLIAN LETTER TODO ALI GALI TA;Lo;0;L;;;;;N;;;;;
+1899;MONGOLIAN LETTER TODO ALI GALI ZHA;Lo;0;L;;;;;N;;;;;
+189A;MONGOLIAN LETTER MANCHU ALI GALI GHA;Lo;0;L;;;;;N;;;;;
+189B;MONGOLIAN LETTER MANCHU ALI GALI NGA;Lo;0;L;;;;;N;;;;;
+189C;MONGOLIAN LETTER MANCHU ALI GALI CA;Lo;0;L;;;;;N;;;;;
+189D;MONGOLIAN LETTER MANCHU ALI GALI JHA;Lo;0;L;;;;;N;;;;;
+189E;MONGOLIAN LETTER MANCHU ALI GALI TTA;Lo;0;L;;;;;N;;;;;
+189F;MONGOLIAN LETTER MANCHU ALI GALI DDHA;Lo;0;L;;;;;N;;;;;
+18A0;MONGOLIAN LETTER MANCHU ALI GALI TA;Lo;0;L;;;;;N;;;;;
+18A1;MONGOLIAN LETTER MANCHU ALI GALI DHA;Lo;0;L;;;;;N;;;;;
+18A2;MONGOLIAN LETTER MANCHU ALI GALI SSA;Lo;0;L;;;;;N;;;;;
+18A3;MONGOLIAN LETTER MANCHU ALI GALI CYA;Lo;0;L;;;;;N;;;;;
+18A4;MONGOLIAN LETTER MANCHU ALI GALI ZHA;Lo;0;L;;;;;N;;;;;
+18A5;MONGOLIAN LETTER MANCHU ALI GALI ZA;Lo;0;L;;;;;N;;;;;
+18A6;MONGOLIAN LETTER ALI GALI HALF U;Lo;0;L;;;;;N;;;;;
+18A7;MONGOLIAN LETTER ALI GALI HALF YA;Lo;0;L;;;;;N;;;;;
+18A8;MONGOLIAN LETTER MANCHU ALI GALI BHA;Lo;0;L;;;;;N;;;;;
+18A9;MONGOLIAN LETTER ALI GALI DAGALGA;Mn;228;NSM;;;;;N;;;;;
+18AA;MONGOLIAN LETTER MANCHU ALI GALI LHA;Lo;0;L;;;;;N;;;;;
+18B0;CANADIAN SYLLABICS OY;Lo;0;L;;;;;N;;;;;
+18B1;CANADIAN SYLLABICS AY;Lo;0;L;;;;;N;;;;;
+18B2;CANADIAN SYLLABICS AAY;Lo;0;L;;;;;N;;;;;
+18B3;CANADIAN SYLLABICS WAY;Lo;0;L;;;;;N;;;;;
+18B4;CANADIAN SYLLABICS POY;Lo;0;L;;;;;N;;;;;
+18B5;CANADIAN SYLLABICS PAY;Lo;0;L;;;;;N;;;;;
+18B6;CANADIAN SYLLABICS PWOY;Lo;0;L;;;;;N;;;;;
+18B7;CANADIAN SYLLABICS TAY;Lo;0;L;;;;;N;;;;;
+18B8;CANADIAN SYLLABICS KAY;Lo;0;L;;;;;N;;;;;
+18B9;CANADIAN SYLLABICS KWAY;Lo;0;L;;;;;N;;;;;
+18BA;CANADIAN SYLLABICS MAY;Lo;0;L;;;;;N;;;;;
+18BB;CANADIAN SYLLABICS NOY;Lo;0;L;;;;;N;;;;;
+18BC;CANADIAN SYLLABICS NAY;Lo;0;L;;;;;N;;;;;
+18BD;CANADIAN SYLLABICS LAY;Lo;0;L;;;;;N;;;;;
+18BE;CANADIAN SYLLABICS SOY;Lo;0;L;;;;;N;;;;;
+18BF;CANADIAN SYLLABICS SAY;Lo;0;L;;;;;N;;;;;
+18C0;CANADIAN SYLLABICS SHOY;Lo;0;L;;;;;N;;;;;
+18C1;CANADIAN SYLLABICS SHAY;Lo;0;L;;;;;N;;;;;
+18C2;CANADIAN SYLLABICS SHWOY;Lo;0;L;;;;;N;;;;;
+18C3;CANADIAN SYLLABICS YOY;Lo;0;L;;;;;N;;;;;
+18C4;CANADIAN SYLLABICS YAY;Lo;0;L;;;;;N;;;;;
+18C5;CANADIAN SYLLABICS RAY;Lo;0;L;;;;;N;;;;;
+18C6;CANADIAN SYLLABICS NWI;Lo;0;L;;;;;N;;;;;
+18C7;CANADIAN SYLLABICS OJIBWAY NWI;Lo;0;L;;;;;N;;;;;
+18C8;CANADIAN SYLLABICS NWII;Lo;0;L;;;;;N;;;;;
+18C9;CANADIAN SYLLABICS OJIBWAY NWII;Lo;0;L;;;;;N;;;;;
+18CA;CANADIAN SYLLABICS NWO;Lo;0;L;;;;;N;;;;;
+18CB;CANADIAN SYLLABICS OJIBWAY NWO;Lo;0;L;;;;;N;;;;;
+18CC;CANADIAN SYLLABICS NWOO;Lo;0;L;;;;;N;;;;;
+18CD;CANADIAN SYLLABICS OJIBWAY NWOO;Lo;0;L;;;;;N;;;;;
+18CE;CANADIAN SYLLABICS RWEE;Lo;0;L;;;;;N;;;;;
+18CF;CANADIAN SYLLABICS RWI;Lo;0;L;;;;;N;;;;;
+18D0;CANADIAN SYLLABICS RWII;Lo;0;L;;;;;N;;;;;
+18D1;CANADIAN SYLLABICS RWO;Lo;0;L;;;;;N;;;;;
+18D2;CANADIAN SYLLABICS RWOO;Lo;0;L;;;;;N;;;;;
+18D3;CANADIAN SYLLABICS RWA;Lo;0;L;;;;;N;;;;;
+18D4;CANADIAN SYLLABICS OJIBWAY P;Lo;0;L;;;;;N;;;;;
+18D5;CANADIAN SYLLABICS OJIBWAY T;Lo;0;L;;;;;N;;;;;
+18D6;CANADIAN SYLLABICS OJIBWAY K;Lo;0;L;;;;;N;;;;;
+18D7;CANADIAN SYLLABICS OJIBWAY C;Lo;0;L;;;;;N;;;;;
+18D8;CANADIAN SYLLABICS OJIBWAY M;Lo;0;L;;;;;N;;;;;
+18D9;CANADIAN SYLLABICS OJIBWAY N;Lo;0;L;;;;;N;;;;;
+18DA;CANADIAN SYLLABICS OJIBWAY S;Lo;0;L;;;;;N;;;;;
+18DB;CANADIAN SYLLABICS OJIBWAY SH;Lo;0;L;;;;;N;;;;;
+18DC;CANADIAN SYLLABICS EASTERN W;Lo;0;L;;;;;N;;;;;
+18DD;CANADIAN SYLLABICS WESTERN W;Lo;0;L;;;;;N;;;;;
+18DE;CANADIAN SYLLABICS FINAL SMALL RING;Lo;0;L;;;;;N;;;;;
+18DF;CANADIAN SYLLABICS FINAL RAISED DOT;Lo;0;L;;;;;N;;;;;
+18E0;CANADIAN SYLLABICS R-CREE RWE;Lo;0;L;;;;;N;;;;;
+18E1;CANADIAN SYLLABICS WEST-CREE LOO;Lo;0;L;;;;;N;;;;;
+18E2;CANADIAN SYLLABICS WEST-CREE LAA;Lo;0;L;;;;;N;;;;;
+18E3;CANADIAN SYLLABICS THWE;Lo;0;L;;;;;N;;;;;
+18E4;CANADIAN SYLLABICS THWA;Lo;0;L;;;;;N;;;;;
+18E5;CANADIAN SYLLABICS TTHWE;Lo;0;L;;;;;N;;;;;
+18E6;CANADIAN SYLLABICS TTHOO;Lo;0;L;;;;;N;;;;;
+18E7;CANADIAN SYLLABICS TTHAA;Lo;0;L;;;;;N;;;;;
+18E8;CANADIAN SYLLABICS TLHWE;Lo;0;L;;;;;N;;;;;
+18E9;CANADIAN SYLLABICS TLHOO;Lo;0;L;;;;;N;;;;;
+18EA;CANADIAN SYLLABICS SAYISI SHWE;Lo;0;L;;;;;N;;;;;
+18EB;CANADIAN SYLLABICS SAYISI SHOO;Lo;0;L;;;;;N;;;;;
+18EC;CANADIAN SYLLABICS SAYISI HOO;Lo;0;L;;;;;N;;;;;
+18ED;CANADIAN SYLLABICS CARRIER GWU;Lo;0;L;;;;;N;;;;;
+18EE;CANADIAN SYLLABICS CARRIER DENE GEE;Lo;0;L;;;;;N;;;;;
+18EF;CANADIAN SYLLABICS CARRIER GAA;Lo;0;L;;;;;N;;;;;
+18F0;CANADIAN SYLLABICS CARRIER GWA;Lo;0;L;;;;;N;;;;;
+18F1;CANADIAN SYLLABICS SAYISI JUU;Lo;0;L;;;;;N;;;;;
+18F2;CANADIAN SYLLABICS CARRIER JWA;Lo;0;L;;;;;N;;;;;
+18F3;CANADIAN SYLLABICS BEAVER DENE L;Lo;0;L;;;;;N;;;;;
+18F4;CANADIAN SYLLABICS BEAVER DENE R;Lo;0;L;;;;;N;;;;;
+18F5;CANADIAN SYLLABICS CARRIER DENTAL S;Lo;0;L;;;;;N;;;;;
+1900;LIMBU VOWEL-CARRIER LETTER;Lo;0;L;;;;;N;;;;;
+1901;LIMBU LETTER KA;Lo;0;L;;;;;N;;;;;
+1902;LIMBU LETTER KHA;Lo;0;L;;;;;N;;;;;
+1903;LIMBU LETTER GA;Lo;0;L;;;;;N;;;;;
+1904;LIMBU LETTER GHA;Lo;0;L;;;;;N;;;;;
+1905;LIMBU LETTER NGA;Lo;0;L;;;;;N;;;;;
+1906;LIMBU LETTER CA;Lo;0;L;;;;;N;;;;;
+1907;LIMBU LETTER CHA;Lo;0;L;;;;;N;;;;;
+1908;LIMBU LETTER JA;Lo;0;L;;;;;N;;;;;
+1909;LIMBU LETTER JHA;Lo;0;L;;;;;N;;;;;
+190A;LIMBU LETTER YAN;Lo;0;L;;;;;N;;;;;
+190B;LIMBU LETTER TA;Lo;0;L;;;;;N;;;;;
+190C;LIMBU LETTER THA;Lo;0;L;;;;;N;;;;;
+190D;LIMBU LETTER DA;Lo;0;L;;;;;N;;;;;
+190E;LIMBU LETTER DHA;Lo;0;L;;;;;N;;;;;
+190F;LIMBU LETTER NA;Lo;0;L;;;;;N;;;;;
+1910;LIMBU LETTER PA;Lo;0;L;;;;;N;;;;;
+1911;LIMBU LETTER PHA;Lo;0;L;;;;;N;;;;;
+1912;LIMBU LETTER BA;Lo;0;L;;;;;N;;;;;
+1913;LIMBU LETTER BHA;Lo;0;L;;;;;N;;;;;
+1914;LIMBU LETTER MA;Lo;0;L;;;;;N;;;;;
+1915;LIMBU LETTER YA;Lo;0;L;;;;;N;;;;;
+1916;LIMBU LETTER RA;Lo;0;L;;;;;N;;;;;
+1917;LIMBU LETTER LA;Lo;0;L;;;;;N;;;;;
+1918;LIMBU LETTER WA;Lo;0;L;;;;;N;;;;;
+1919;LIMBU LETTER SHA;Lo;0;L;;;;;N;;;;;
+191A;LIMBU LETTER SSA;Lo;0;L;;;;;N;;;;;
+191B;LIMBU LETTER SA;Lo;0;L;;;;;N;;;;;
+191C;LIMBU LETTER HA;Lo;0;L;;;;;N;;;;;
+191D;LIMBU LETTER GYAN;Lo;0;L;;;;;N;;;;;
+191E;LIMBU LETTER TRA;Lo;0;L;;;;;N;;;;;
+1920;LIMBU VOWEL SIGN A;Mn;0;NSM;;;;;N;;;;;
+1921;LIMBU VOWEL SIGN I;Mn;0;NSM;;;;;N;;;;;
+1922;LIMBU VOWEL SIGN U;Mn;0;NSM;;;;;N;;;;;
+1923;LIMBU VOWEL SIGN EE;Mc;0;L;;;;;N;;;;;
+1924;LIMBU VOWEL SIGN AI;Mc;0;L;;;;;N;;;;;
+1925;LIMBU VOWEL SIGN OO;Mc;0;L;;;;;N;;;;;
+1926;LIMBU VOWEL SIGN AU;Mc;0;L;;;;;N;;;;;
+1927;LIMBU VOWEL SIGN E;Mn;0;NSM;;;;;N;;;;;
+1928;LIMBU VOWEL SIGN O;Mn;0;NSM;;;;;N;;;;;
+1929;LIMBU SUBJOINED LETTER YA;Mc;0;L;;;;;N;;;;;
+192A;LIMBU SUBJOINED LETTER RA;Mc;0;L;;;;;N;;;;;
+192B;LIMBU SUBJOINED LETTER WA;Mc;0;L;;;;;N;;;;;
+1930;LIMBU SMALL LETTER KA;Mc;0;L;;;;;N;;;;;
+1931;LIMBU SMALL LETTER NGA;Mc;0;L;;;;;N;;;;;
+1932;LIMBU SMALL LETTER ANUSVARA;Mn;0;NSM;;;;;N;;;;;
+1933;LIMBU SMALL LETTER TA;Mc;0;L;;;;;N;;;;;
+1934;LIMBU SMALL LETTER NA;Mc;0;L;;;;;N;;;;;
+1935;LIMBU SMALL LETTER PA;Mc;0;L;;;;;N;;;;;
+1936;LIMBU SMALL LETTER MA;Mc;0;L;;;;;N;;;;;
+1937;LIMBU SMALL LETTER RA;Mc;0;L;;;;;N;;;;;
+1938;LIMBU SMALL LETTER LA;Mc;0;L;;;;;N;;;;;
+1939;LIMBU SIGN MUKPHRENG;Mn;222;NSM;;;;;N;;;;;
+193A;LIMBU SIGN KEMPHRENG;Mn;230;NSM;;;;;N;;;;;
+193B;LIMBU SIGN SA-I;Mn;220;NSM;;;;;N;;;;;
+1940;LIMBU SIGN LOO;So;0;ON;;;;;N;;;;;
+1944;LIMBU EXCLAMATION MARK;Po;0;ON;;;;;N;;;;;
+1945;LIMBU QUESTION MARK;Po;0;ON;;;;;N;;;;;
+1946;LIMBU DIGIT ZERO;Nd;0;L;;0;0;0;N;;;;;
+1947;LIMBU DIGIT ONE;Nd;0;L;;1;1;1;N;;;;;
+1948;LIMBU DIGIT TWO;Nd;0;L;;2;2;2;N;;;;;
+1949;LIMBU DIGIT THREE;Nd;0;L;;3;3;3;N;;;;;
+194A;LIMBU DIGIT FOUR;Nd;0;L;;4;4;4;N;;;;;
+194B;LIMBU DIGIT FIVE;Nd;0;L;;5;5;5;N;;;;;
+194C;LIMBU DIGIT SIX;Nd;0;L;;6;6;6;N;;;;;
+194D;LIMBU DIGIT SEVEN;Nd;0;L;;7;7;7;N;;;;;
+194E;LIMBU DIGIT EIGHT;Nd;0;L;;8;8;8;N;;;;;
+194F;LIMBU DIGIT NINE;Nd;0;L;;9;9;9;N;;;;;
+1950;TAI LE LETTER KA;Lo;0;L;;;;;N;;;;;
+1951;TAI LE LETTER XA;Lo;0;L;;;;;N;;;;;
+1952;TAI LE LETTER NGA;Lo;0;L;;;;;N;;;;;
+1953;TAI LE LETTER TSA;Lo;0;L;;;;;N;;;;;
+1954;TAI LE LETTER SA;Lo;0;L;;;;;N;;;;;
+1955;TAI LE LETTER YA;Lo;0;L;;;;;N;;;;;
+1956;TAI LE LETTER TA;Lo;0;L;;;;;N;;;;;
+1957;TAI LE LETTER THA;Lo;0;L;;;;;N;;;;;
+1958;TAI LE LETTER LA;Lo;0;L;;;;;N;;;;;
+1959;TAI LE LETTER PA;Lo;0;L;;;;;N;;;;;
+195A;TAI LE LETTER PHA;Lo;0;L;;;;;N;;;;;
+195B;TAI LE LETTER MA;Lo;0;L;;;;;N;;;;;
+195C;TAI LE LETTER FA;Lo;0;L;;;;;N;;;;;
+195D;TAI LE LETTER VA;Lo;0;L;;;;;N;;;;;
+195E;TAI LE LETTER HA;Lo;0;L;;;;;N;;;;;
+195F;TAI LE LETTER QA;Lo;0;L;;;;;N;;;;;
+1960;TAI LE LETTER KHA;Lo;0;L;;;;;N;;;;;
+1961;TAI LE LETTER TSHA;Lo;0;L;;;;;N;;;;;
+1962;TAI LE LETTER NA;Lo;0;L;;;;;N;;;;;
+1963;TAI LE LETTER A;Lo;0;L;;;;;N;;;;;
+1964;TAI LE LETTER I;Lo;0;L;;;;;N;;;;;
+1965;TAI LE LETTER EE;Lo;0;L;;;;;N;;;;;
+1966;TAI LE LETTER EH;Lo;0;L;;;;;N;;;;;
+1967;TAI LE LETTER U;Lo;0;L;;;;;N;;;;;
+1968;TAI LE LETTER OO;Lo;0;L;;;;;N;;;;;
+1969;TAI LE LETTER O;Lo;0;L;;;;;N;;;;;
+196A;TAI LE LETTER UE;Lo;0;L;;;;;N;;;;;
+196B;TAI LE LETTER E;Lo;0;L;;;;;N;;;;;
+196C;TAI LE LETTER AUE;Lo;0;L;;;;;N;;;;;
+196D;TAI LE LETTER AI;Lo;0;L;;;;;N;;;;;
+1970;TAI LE LETTER TONE-2;Lo;0;L;;;;;N;;;;;
+1971;TAI LE LETTER TONE-3;Lo;0;L;;;;;N;;;;;
+1972;TAI LE LETTER TONE-4;Lo;0;L;;;;;N;;;;;
+1973;TAI LE LETTER TONE-5;Lo;0;L;;;;;N;;;;;
+1974;TAI LE LETTER TONE-6;Lo;0;L;;;;;N;;;;;
+1980;NEW TAI LUE LETTER HIGH QA;Lo;0;L;;;;;N;;;;;
+1981;NEW TAI LUE LETTER LOW QA;Lo;0;L;;;;;N;;;;;
+1982;NEW TAI LUE LETTER HIGH KA;Lo;0;L;;;;;N;;;;;
+1983;NEW TAI LUE LETTER HIGH XA;Lo;0;L;;;;;N;;;;;
+1984;NEW TAI LUE LETTER HIGH NGA;Lo;0;L;;;;;N;;;;;
+1985;NEW TAI LUE LETTER LOW KA;Lo;0;L;;;;;N;;;;;
+1986;NEW TAI LUE LETTER LOW XA;Lo;0;L;;;;;N;;;;;
+1987;NEW TAI LUE LETTER LOW NGA;Lo;0;L;;;;;N;;;;;
+1988;NEW TAI LUE LETTER HIGH TSA;Lo;0;L;;;;;N;;;;;
+1989;NEW TAI LUE LETTER HIGH SA;Lo;0;L;;;;;N;;;;;
+198A;NEW TAI LUE LETTER HIGH YA;Lo;0;L;;;;;N;;;;;
+198B;NEW TAI LUE LETTER LOW TSA;Lo;0;L;;;;;N;;;;;
+198C;NEW TAI LUE LETTER LOW SA;Lo;0;L;;;;;N;;;;;
+198D;NEW TAI LUE LETTER LOW YA;Lo;0;L;;;;;N;;;;;
+198E;NEW TAI LUE LETTER HIGH TA;Lo;0;L;;;;;N;;;;;
+198F;NEW TAI LUE LETTER HIGH THA;Lo;0;L;;;;;N;;;;;
+1990;NEW TAI LUE LETTER HIGH NA;Lo;0;L;;;;;N;;;;;
+1991;NEW TAI LUE LETTER LOW TA;Lo;0;L;;;;;N;;;;;
+1992;NEW TAI LUE LETTER LOW THA;Lo;0;L;;;;;N;;;;;
+1993;NEW TAI LUE LETTER LOW NA;Lo;0;L;;;;;N;;;;;
+1994;NEW TAI LUE LETTER HIGH PA;Lo;0;L;;;;;N;;;;;
+1995;NEW TAI LUE LETTER HIGH PHA;Lo;0;L;;;;;N;;;;;
+1996;NEW TAI LUE LETTER HIGH MA;Lo;0;L;;;;;N;;;;;
+1997;NEW TAI LUE LETTER LOW PA;Lo;0;L;;;;;N;;;;;
+1998;NEW TAI LUE LETTER LOW PHA;Lo;0;L;;;;;N;;;;;
+1999;NEW TAI LUE LETTER LOW MA;Lo;0;L;;;;;N;;;;;
+199A;NEW TAI LUE LETTER HIGH FA;Lo;0;L;;;;;N;;;;;
+199B;NEW TAI LUE LETTER HIGH VA;Lo;0;L;;;;;N;;;;;
+199C;NEW TAI LUE LETTER HIGH LA;Lo;0;L;;;;;N;;;;;
+199D;NEW TAI LUE LETTER LOW FA;Lo;0;L;;;;;N;;;;;
+199E;NEW TAI LUE LETTER LOW VA;Lo;0;L;;;;;N;;;;;
+199F;NEW TAI LUE LETTER LOW LA;Lo;0;L;;;;;N;;;;;
+19A0;NEW TAI LUE LETTER HIGH HA;Lo;0;L;;;;;N;;;;;
+19A1;NEW TAI LUE LETTER HIGH DA;Lo;0;L;;;;;N;;;;;
+19A2;NEW TAI LUE LETTER HIGH BA;Lo;0;L;;;;;N;;;;;
+19A3;NEW TAI LUE LETTER LOW HA;Lo;0;L;;;;;N;;;;;
+19A4;NEW TAI LUE LETTER LOW DA;Lo;0;L;;;;;N;;;;;
+19A5;NEW TAI LUE LETTER LOW BA;Lo;0;L;;;;;N;;;;;
+19A6;NEW TAI LUE LETTER HIGH KVA;Lo;0;L;;;;;N;;;;;
+19A7;NEW TAI LUE LETTER HIGH XVA;Lo;0;L;;;;;N;;;;;
+19A8;NEW TAI LUE LETTER LOW KVA;Lo;0;L;;;;;N;;;;;
+19A9;NEW TAI LUE LETTER LOW XVA;Lo;0;L;;;;;N;;;;;
+19AA;NEW TAI LUE LETTER HIGH SUA;Lo;0;L;;;;;N;;;;;
+19AB;NEW TAI LUE LETTER LOW SUA;Lo;0;L;;;;;N;;;;;
+19B0;NEW TAI LUE VOWEL SIGN VOWEL SHORTENER;Lo;0;L;;;;;N;;;;;
+19B1;NEW TAI LUE VOWEL SIGN AA;Lo;0;L;;;;;N;;;;;
+19B2;NEW TAI LUE VOWEL SIGN II;Lo;0;L;;;;;N;;;;;
+19B3;NEW TAI LUE VOWEL SIGN U;Lo;0;L;;;;;N;;;;;
+19B4;NEW TAI LUE VOWEL SIGN UU;Lo;0;L;;;;;N;;;;;
+19B5;NEW TAI LUE VOWEL SIGN E;Lo;0;L;;;;;N;;;;;
+19B6;NEW TAI LUE VOWEL SIGN AE;Lo;0;L;;;;;N;;;;;
+19B7;NEW TAI LUE VOWEL SIGN O;Lo;0;L;;;;;N;;;;;
+19B8;NEW TAI LUE VOWEL SIGN OA;Lo;0;L;;;;;N;;;;;
+19B9;NEW TAI LUE VOWEL SIGN UE;Lo;0;L;;;;;N;;;;;
+19BA;NEW TAI LUE VOWEL SIGN AY;Lo;0;L;;;;;N;;;;;
+19BB;NEW TAI LUE VOWEL SIGN AAY;Lo;0;L;;;;;N;;;;;
+19BC;NEW TAI LUE VOWEL SIGN UY;Lo;0;L;;;;;N;;;;;
+19BD;NEW TAI LUE VOWEL SIGN OY;Lo;0;L;;;;;N;;;;;
+19BE;NEW TAI LUE VOWEL SIGN OAY;Lo;0;L;;;;;N;;;;;
+19BF;NEW TAI LUE VOWEL SIGN UEY;Lo;0;L;;;;;N;;;;;
+19C0;NEW TAI LUE VOWEL SIGN IY;Lo;0;L;;;;;N;;;;;
+19C1;NEW TAI LUE LETTER FINAL V;Lo;0;L;;;;;N;;;;;
+19C2;NEW TAI LUE LETTER FINAL NG;Lo;0;L;;;;;N;;;;;
+19C3;NEW TAI LUE LETTER FINAL N;Lo;0;L;;;;;N;;;;;
+19C4;NEW TAI LUE LETTER FINAL M;Lo;0;L;;;;;N;;;;;
+19C5;NEW TAI LUE LETTER FINAL K;Lo;0;L;;;;;N;;;;;
+19C6;NEW TAI LUE LETTER FINAL D;Lo;0;L;;;;;N;;;;;
+19C7;NEW TAI LUE LETTER FINAL B;Lo;0;L;;;;;N;;;;;
+19C8;NEW TAI LUE TONE MARK-1;Lo;0;L;;;;;N;;;;;
+19C9;NEW TAI LUE TONE MARK-2;Lo;0;L;;;;;N;;;;;
+19D0;NEW TAI LUE DIGIT ZERO;Nd;0;L;;0;0;0;N;;;;;
+19D1;NEW TAI LUE DIGIT ONE;Nd;0;L;;1;1;1;N;;;;;
+19D2;NEW TAI LUE DIGIT TWO;Nd;0;L;;2;2;2;N;;;;;
+19D3;NEW TAI LUE DIGIT THREE;Nd;0;L;;3;3;3;N;;;;;
+19D4;NEW TAI LUE DIGIT FOUR;Nd;0;L;;4;4;4;N;;;;;
+19D5;NEW TAI LUE DIGIT FIVE;Nd;0;L;;5;5;5;N;;;;;
+19D6;NEW TAI LUE DIGIT SIX;Nd;0;L;;6;6;6;N;;;;;
+19D7;NEW TAI LUE DIGIT SEVEN;Nd;0;L;;7;7;7;N;;;;;
+19D8;NEW TAI LUE DIGIT EIGHT;Nd;0;L;;8;8;8;N;;;;;
+19D9;NEW TAI LUE DIGIT NINE;Nd;0;L;;9;9;9;N;;;;;
+19DA;NEW TAI LUE THAM DIGIT ONE;No;0;L;;;1;1;N;;;;;
+19DE;NEW TAI LUE SIGN LAE;So;0;ON;;;;;N;;;;;
+19DF;NEW TAI LUE SIGN LAEV;So;0;ON;;;;;N;;;;;
+19E0;KHMER SYMBOL PATHAMASAT;So;0;ON;;;;;N;;;;;
+19E1;KHMER SYMBOL MUOY KOET;So;0;ON;;;;;N;;;;;
+19E2;KHMER SYMBOL PII KOET;So;0;ON;;;;;N;;;;;
+19E3;KHMER SYMBOL BEI KOET;So;0;ON;;;;;N;;;;;
+19E4;KHMER SYMBOL BUON KOET;So;0;ON;;;;;N;;;;;
+19E5;KHMER SYMBOL PRAM KOET;So;0;ON;;;;;N;;;;;
+19E6;KHMER SYMBOL PRAM-MUOY KOET;So;0;ON;;;;;N;;;;;
+19E7;KHMER SYMBOL PRAM-PII KOET;So;0;ON;;;;;N;;;;;
+19E8;KHMER SYMBOL PRAM-BEI KOET;So;0;ON;;;;;N;;;;;
+19E9;KHMER SYMBOL PRAM-BUON KOET;So;0;ON;;;;;N;;;;;
+19EA;KHMER SYMBOL DAP KOET;So;0;ON;;;;;N;;;;;
+19EB;KHMER SYMBOL DAP-MUOY KOET;So;0;ON;;;;;N;;;;;
+19EC;KHMER SYMBOL DAP-PII KOET;So;0;ON;;;;;N;;;;;
+19ED;KHMER SYMBOL DAP-BEI KOET;So;0;ON;;;;;N;;;;;
+19EE;KHMER SYMBOL DAP-BUON KOET;So;0;ON;;;;;N;;;;;
+19EF;KHMER SYMBOL DAP-PRAM KOET;So;0;ON;;;;;N;;;;;
+19F0;KHMER SYMBOL TUTEYASAT;So;0;ON;;;;;N;;;;;
+19F1;KHMER SYMBOL MUOY ROC;So;0;ON;;;;;N;;;;;
+19F2;KHMER SYMBOL PII ROC;So;0;ON;;;;;N;;;;;
+19F3;KHMER SYMBOL BEI ROC;So;0;ON;;;;;N;;;;;
+19F4;KHMER SYMBOL BUON ROC;So;0;ON;;;;;N;;;;;
+19F5;KHMER SYMBOL PRAM ROC;So;0;ON;;;;;N;;;;;
+19F6;KHMER SYMBOL PRAM-MUOY ROC;So;0;ON;;;;;N;;;;;
+19F7;KHMER SYMBOL PRAM-PII ROC;So;0;ON;;;;;N;;;;;
+19F8;KHMER SYMBOL PRAM-BEI ROC;So;0;ON;;;;;N;;;;;
+19F9;KHMER SYMBOL PRAM-BUON ROC;So;0;ON;;;;;N;;;;;
+19FA;KHMER SYMBOL DAP ROC;So;0;ON;;;;;N;;;;;
+19FB;KHMER SYMBOL DAP-MUOY ROC;So;0;ON;;;;;N;;;;;
+19FC;KHMER SYMBOL DAP-PII ROC;So;0;ON;;;;;N;;;;;
+19FD;KHMER SYMBOL DAP-BEI ROC;So;0;ON;;;;;N;;;;;
+19FE;KHMER SYMBOL DAP-BUON ROC;So;0;ON;;;;;N;;;;;
+19FF;KHMER SYMBOL DAP-PRAM ROC;So;0;ON;;;;;N;;;;;
+1A00;BUGINESE LETTER KA;Lo;0;L;;;;;N;;;;;
+1A01;BUGINESE LETTER GA;Lo;0;L;;;;;N;;;;;
+1A02;BUGINESE LETTER NGA;Lo;0;L;;;;;N;;;;;
+1A03;BUGINESE LETTER NGKA;Lo;0;L;;;;;N;;;;;
+1A04;BUGINESE LETTER PA;Lo;0;L;;;;;N;;;;;
+1A05;BUGINESE LETTER BA;Lo;0;L;;;;;N;;;;;
+1A06;BUGINESE LETTER MA;Lo;0;L;;;;;N;;;;;
+1A07;BUGINESE LETTER MPA;Lo;0;L;;;;;N;;;;;
+1A08;BUGINESE LETTER TA;Lo;0;L;;;;;N;;;;;
+1A09;BUGINESE LETTER DA;Lo;0;L;;;;;N;;;;;
+1A0A;BUGINESE LETTER NA;Lo;0;L;;;;;N;;;;;
+1A0B;BUGINESE LETTER NRA;Lo;0;L;;;;;N;;;;;
+1A0C;BUGINESE LETTER CA;Lo;0;L;;;;;N;;;;;
+1A0D;BUGINESE LETTER JA;Lo;0;L;;;;;N;;;;;
+1A0E;BUGINESE LETTER NYA;Lo;0;L;;;;;N;;;;;
+1A0F;BUGINESE LETTER NYCA;Lo;0;L;;;;;N;;;;;
+1A10;BUGINESE LETTER YA;Lo;0;L;;;;;N;;;;;
+1A11;BUGINESE LETTER RA;Lo;0;L;;;;;N;;;;;
+1A12;BUGINESE LETTER LA;Lo;0;L;;;;;N;;;;;
+1A13;BUGINESE LETTER VA;Lo;0;L;;;;;N;;;;;
+1A14;BUGINESE LETTER SA;Lo;0;L;;;;;N;;;;;
+1A15;BUGINESE LETTER A;Lo;0;L;;;;;N;;;;;
+1A16;BUGINESE LETTER HA;Lo;0;L;;;;;N;;;;;
+1A17;BUGINESE VOWEL SIGN I;Mn;230;NSM;;;;;N;;;;;
+1A18;BUGINESE VOWEL SIGN U;Mn;220;NSM;;;;;N;;;;;
+1A19;BUGINESE VOWEL SIGN E;Mc;0;L;;;;;N;;;;;
+1A1A;BUGINESE VOWEL SIGN O;Mc;0;L;;;;;N;;;;;
+1A1B;BUGINESE VOWEL SIGN AE;Mn;0;NSM;;;;;N;;;;;
+1A1E;BUGINESE PALLAWA;Po;0;L;;;;;N;;;;;
+1A1F;BUGINESE END OF SECTION;Po;0;L;;;;;N;;;;;
+1A20;TAI THAM LETTER HIGH KA;Lo;0;L;;;;;N;;;;;
+1A21;TAI THAM LETTER HIGH KHA;Lo;0;L;;;;;N;;;;;
+1A22;TAI THAM LETTER HIGH KXA;Lo;0;L;;;;;N;;;;;
+1A23;TAI THAM LETTER LOW KA;Lo;0;L;;;;;N;;;;;
+1A24;TAI THAM LETTER LOW KXA;Lo;0;L;;;;;N;;;;;
+1A25;TAI THAM LETTER LOW KHA;Lo;0;L;;;;;N;;;;;
+1A26;TAI THAM LETTER NGA;Lo;0;L;;;;;N;;;;;
+1A27;TAI THAM LETTER HIGH CA;Lo;0;L;;;;;N;;;;;
+1A28;TAI THAM LETTER HIGH CHA;Lo;0;L;;;;;N;;;;;
+1A29;TAI THAM LETTER LOW CA;Lo;0;L;;;;;N;;;;;
+1A2A;TAI THAM LETTER LOW SA;Lo;0;L;;;;;N;;;;;
+1A2B;TAI THAM LETTER LOW CHA;Lo;0;L;;;;;N;;;;;
+1A2C;TAI THAM LETTER NYA;Lo;0;L;;;;;N;;;;;
+1A2D;TAI THAM LETTER RATA;Lo;0;L;;;;;N;;;;;
+1A2E;TAI THAM LETTER HIGH RATHA;Lo;0;L;;;;;N;;;;;
+1A2F;TAI THAM LETTER DA;Lo;0;L;;;;;N;;;;;
+1A30;TAI THAM LETTER LOW RATHA;Lo;0;L;;;;;N;;;;;
+1A31;TAI THAM LETTER RANA;Lo;0;L;;;;;N;;;;;
+1A32;TAI THAM LETTER HIGH TA;Lo;0;L;;;;;N;;;;;
+1A33;TAI THAM LETTER HIGH THA;Lo;0;L;;;;;N;;;;;
+1A34;TAI THAM LETTER LOW TA;Lo;0;L;;;;;N;;;;;
+1A35;TAI THAM LETTER LOW THA;Lo;0;L;;;;;N;;;;;
+1A36;TAI THAM LETTER NA;Lo;0;L;;;;;N;;;;;
+1A37;TAI THAM LETTER BA;Lo;0;L;;;;;N;;;;;
+1A38;TAI THAM LETTER HIGH PA;Lo;0;L;;;;;N;;;;;
+1A39;TAI THAM LETTER HIGH PHA;Lo;0;L;;;;;N;;;;;
+1A3A;TAI THAM LETTER HIGH FA;Lo;0;L;;;;;N;;;;;
+1A3B;TAI THAM LETTER LOW PA;Lo;0;L;;;;;N;;;;;
+1A3C;TAI THAM LETTER LOW FA;Lo;0;L;;;;;N;;;;;
+1A3D;TAI THAM LETTER LOW PHA;Lo;0;L;;;;;N;;;;;
+1A3E;TAI THAM LETTER MA;Lo;0;L;;;;;N;;;;;
+1A3F;TAI THAM LETTER LOW YA;Lo;0;L;;;;;N;;;;;
+1A40;TAI THAM LETTER HIGH YA;Lo;0;L;;;;;N;;;;;
+1A41;TAI THAM LETTER RA;Lo;0;L;;;;;N;;;;;
+1A42;TAI THAM LETTER RUE;Lo;0;L;;;;;N;;;;;
+1A43;TAI THAM LETTER LA;Lo;0;L;;;;;N;;;;;
+1A44;TAI THAM LETTER LUE;Lo;0;L;;;;;N;;;;;
+1A45;TAI THAM LETTER WA;Lo;0;L;;;;;N;;;;;
+1A46;TAI THAM LETTER HIGH SHA;Lo;0;L;;;;;N;;;;;
+1A47;TAI THAM LETTER HIGH SSA;Lo;0;L;;;;;N;;;;;
+1A48;TAI THAM LETTER HIGH SA;Lo;0;L;;;;;N;;;;;
+1A49;TAI THAM LETTER HIGH HA;Lo;0;L;;;;;N;;;;;
+1A4A;TAI THAM LETTER LLA;Lo;0;L;;;;;N;;;;;
+1A4B;TAI THAM LETTER A;Lo;0;L;;;;;N;;;;;
+1A4C;TAI THAM LETTER LOW HA;Lo;0;L;;;;;N;;;;;
+1A4D;TAI THAM LETTER I;Lo;0;L;;;;;N;;;;;
+1A4E;TAI THAM LETTER II;Lo;0;L;;;;;N;;;;;
+1A4F;TAI THAM LETTER U;Lo;0;L;;;;;N;;;;;
+1A50;TAI THAM LETTER UU;Lo;0;L;;;;;N;;;;;
+1A51;TAI THAM LETTER EE;Lo;0;L;;;;;N;;;;;
+1A52;TAI THAM LETTER OO;Lo;0;L;;;;;N;;;;;
+1A53;TAI THAM LETTER LAE;Lo;0;L;;;;;N;;;;;
+1A54;TAI THAM LETTER GREAT SA;Lo;0;L;;;;;N;;;;;
+1A55;TAI THAM CONSONANT SIGN MEDIAL RA;Mc;0;L;;;;;N;;;;;
+1A56;TAI THAM CONSONANT SIGN MEDIAL LA;Mn;0;NSM;;;;;N;;;;;
+1A57;TAI THAM CONSONANT SIGN LA TANG LAI;Mc;0;L;;;;;N;;;;;
+1A58;TAI THAM SIGN MAI KANG LAI;Mn;0;NSM;;;;;N;;;;;
+1A59;TAI THAM CONSONANT SIGN FINAL NGA;Mn;0;NSM;;;;;N;;;;;
+1A5A;TAI THAM CONSONANT SIGN LOW PA;Mn;0;NSM;;;;;N;;;;;
+1A5B;TAI THAM CONSONANT SIGN HIGH RATHA OR LOW PA;Mn;0;NSM;;;;;N;;;;;
+1A5C;TAI THAM CONSONANT SIGN MA;Mn;0;NSM;;;;;N;;;;;
+1A5D;TAI THAM CONSONANT SIGN BA;Mn;0;NSM;;;;;N;;;;;
+1A5E;TAI THAM CONSONANT SIGN SA;Mn;0;NSM;;;;;N;;;;;
+1A60;TAI THAM SIGN SAKOT;Mn;9;NSM;;;;;N;;;;;
+1A61;TAI THAM VOWEL SIGN A;Mc;0;L;;;;;N;;;;;
+1A62;TAI THAM VOWEL SIGN MAI SAT;Mn;0;NSM;;;;;N;;;;;
+1A63;TAI THAM VOWEL SIGN AA;Mc;0;L;;;;;N;;;;;
+1A64;TAI THAM VOWEL SIGN TALL AA;Mc;0;L;;;;;N;;;;;
+1A65;TAI THAM VOWEL SIGN I;Mn;0;NSM;;;;;N;;;;;
+1A66;TAI THAM VOWEL SIGN II;Mn;0;NSM;;;;;N;;;;;
+1A67;TAI THAM VOWEL SIGN UE;Mn;0;NSM;;;;;N;;;;;
+1A68;TAI THAM VOWEL SIGN UUE;Mn;0;NSM;;;;;N;;;;;
+1A69;TAI THAM VOWEL SIGN U;Mn;0;NSM;;;;;N;;;;;
+1A6A;TAI THAM VOWEL SIGN UU;Mn;0;NSM;;;;;N;;;;;
+1A6B;TAI THAM VOWEL SIGN O;Mn;0;NSM;;;;;N;;;;;
+1A6C;TAI THAM VOWEL SIGN OA BELOW;Mn;0;NSM;;;;;N;;;;;
+1A6D;TAI THAM VOWEL SIGN OY;Mc;0;L;;;;;N;;;;;
+1A6E;TAI THAM VOWEL SIGN E;Mc;0;L;;;;;N;;;;;
+1A6F;TAI THAM VOWEL SIGN AE;Mc;0;L;;;;;N;;;;;
+1A70;TAI THAM VOWEL SIGN OO;Mc;0;L;;;;;N;;;;;
+1A71;TAI THAM VOWEL SIGN AI;Mc;0;L;;;;;N;;;;;
+1A72;TAI THAM VOWEL SIGN THAM AI;Mc;0;L;;;;;N;;;;;
+1A73;TAI THAM VOWEL SIGN OA ABOVE;Mn;0;NSM;;;;;N;;;;;
+1A74;TAI THAM SIGN MAI KANG;Mn;0;NSM;;;;;N;;;;;
+1A75;TAI THAM SIGN TONE-1;Mn;230;NSM;;;;;N;;;;;
+1A76;TAI THAM SIGN TONE-2;Mn;230;NSM;;;;;N;;;;;
+1A77;TAI THAM SIGN KHUEN TONE-3;Mn;230;NSM;;;;;N;;;;;
+1A78;TAI THAM SIGN KHUEN TONE-4;Mn;230;NSM;;;;;N;;;;;
+1A79;TAI THAM SIGN KHUEN TONE-5;Mn;230;NSM;;;;;N;;;;;
+1A7A;TAI THAM SIGN RA HAAM;Mn;230;NSM;;;;;N;;;;;
+1A7B;TAI THAM SIGN MAI SAM;Mn;230;NSM;;;;;N;;;;;
+1A7C;TAI THAM SIGN KHUEN-LUE KARAN;Mn;230;NSM;;;;;N;;;;;
+1A7F;TAI THAM COMBINING CRYPTOGRAMMIC DOT;Mn;220;NSM;;;;;N;;;;;
+1A80;TAI THAM HORA DIGIT ZERO;Nd;0;L;;0;0;0;N;;;;;
+1A81;TAI THAM HORA DIGIT ONE;Nd;0;L;;1;1;1;N;;;;;
+1A82;TAI THAM HORA DIGIT TWO;Nd;0;L;;2;2;2;N;;;;;
+1A83;TAI THAM HORA DIGIT THREE;Nd;0;L;;3;3;3;N;;;;;
+1A84;TAI THAM HORA DIGIT FOUR;Nd;0;L;;4;4;4;N;;;;;
+1A85;TAI THAM HORA DIGIT FIVE;Nd;0;L;;5;5;5;N;;;;;
+1A86;TAI THAM HORA DIGIT SIX;Nd;0;L;;6;6;6;N;;;;;
+1A87;TAI THAM HORA DIGIT SEVEN;Nd;0;L;;7;7;7;N;;;;;
+1A88;TAI THAM HORA DIGIT EIGHT;Nd;0;L;;8;8;8;N;;;;;
+1A89;TAI THAM HORA DIGIT NINE;Nd;0;L;;9;9;9;N;;;;;
+1A90;TAI THAM THAM DIGIT ZERO;Nd;0;L;;0;0;0;N;;;;;
+1A91;TAI THAM THAM DIGIT ONE;Nd;0;L;;1;1;1;N;;;;;
+1A92;TAI THAM THAM DIGIT TWO;Nd;0;L;;2;2;2;N;;;;;
+1A93;TAI THAM THAM DIGIT THREE;Nd;0;L;;3;3;3;N;;;;;
+1A94;TAI THAM THAM DIGIT FOUR;Nd;0;L;;4;4;4;N;;;;;
+1A95;TAI THAM THAM DIGIT FIVE;Nd;0;L;;5;5;5;N;;;;;
+1A96;TAI THAM THAM DIGIT SIX;Nd;0;L;;6;6;6;N;;;;;
+1A97;TAI THAM THAM DIGIT SEVEN;Nd;0;L;;7;7;7;N;;;;;
+1A98;TAI THAM THAM DIGIT EIGHT;Nd;0;L;;8;8;8;N;;;;;
+1A99;TAI THAM THAM DIGIT NINE;Nd;0;L;;9;9;9;N;;;;;
+1AA0;TAI THAM SIGN WIANG;Po;0;L;;;;;N;;;;;
+1AA1;TAI THAM SIGN WIANGWAAK;Po;0;L;;;;;N;;;;;
+1AA2;TAI THAM SIGN SAWAN;Po;0;L;;;;;N;;;;;
+1AA3;TAI THAM SIGN KEOW;Po;0;L;;;;;N;;;;;
+1AA4;TAI THAM SIGN HOY;Po;0;L;;;;;N;;;;;
+1AA5;TAI THAM SIGN DOKMAI;Po;0;L;;;;;N;;;;;
+1AA6;TAI THAM SIGN REVERSED ROTATED RANA;Po;0;L;;;;;N;;;;;
+1AA7;TAI THAM SIGN MAI YAMOK;Lm;0;L;;;;;N;;;;;
+1AA8;TAI THAM SIGN KAAN;Po;0;L;;;;;N;;;;;
+1AA9;TAI THAM SIGN KAANKUU;Po;0;L;;;;;N;;;;;
+1AAA;TAI THAM SIGN SATKAAN;Po;0;L;;;;;N;;;;;
+1AAB;TAI THAM SIGN SATKAANKUU;Po;0;L;;;;;N;;;;;
+1AAC;TAI THAM SIGN HANG;Po;0;L;;;;;N;;;;;
+1AAD;TAI THAM SIGN CAANG;Po;0;L;;;;;N;;;;;
+1AB0;COMBINING DOUBLED CIRCUMFLEX ACCENT;Mn;230;NSM;;;;;N;;;;;
+1AB1;COMBINING DIAERESIS-RING;Mn;230;NSM;;;;;N;;;;;
+1AB2;COMBINING INFINITY;Mn;230;NSM;;;;;N;;;;;
+1AB3;COMBINING DOWNWARDS ARROW;Mn;230;NSM;;;;;N;;;;;
+1AB4;COMBINING TRIPLE DOT;Mn;230;NSM;;;;;N;;;;;
+1AB5;COMBINING X-X BELOW;Mn;220;NSM;;;;;N;;;;;
+1AB6;COMBINING WIGGLY LINE BELOW;Mn;220;NSM;;;;;N;;;;;
+1AB7;COMBINING OPEN MARK BELOW;Mn;220;NSM;;;;;N;;;;;
+1AB8;COMBINING DOUBLE OPEN MARK BELOW;Mn;220;NSM;;;;;N;;;;;
+1AB9;COMBINING LIGHT CENTRALIZATION STROKE BELOW;Mn;220;NSM;;;;;N;;;;;
+1ABA;COMBINING STRONG CENTRALIZATION STROKE BELOW;Mn;220;NSM;;;;;N;;;;;
+1ABB;COMBINING PARENTHESES ABOVE;Mn;230;NSM;;;;;N;;;;;
+1ABC;COMBINING DOUBLE PARENTHESES ABOVE;Mn;230;NSM;;;;;N;;;;;
+1ABD;COMBINING PARENTHESES BELOW;Mn;220;NSM;;;;;N;;;;;
+1ABE;COMBINING PARENTHESES OVERLAY;Me;0;NSM;;;;;N;;;;;
+1B00;BALINESE SIGN ULU RICEM;Mn;0;NSM;;;;;N;;;;;
+1B01;BALINESE SIGN ULU CANDRA;Mn;0;NSM;;;;;N;;;;;
+1B02;BALINESE SIGN CECEK;Mn;0;NSM;;;;;N;;;;;
+1B03;BALINESE SIGN SURANG;Mn;0;NSM;;;;;N;;;;;
+1B04;BALINESE SIGN BISAH;Mc;0;L;;;;;N;;;;;
+1B05;BALINESE LETTER AKARA;Lo;0;L;;;;;N;;;;;
+1B06;BALINESE LETTER AKARA TEDUNG;Lo;0;L;1B05 1B35;;;;N;;;;;
+1B07;BALINESE LETTER IKARA;Lo;0;L;;;;;N;;;;;
+1B08;BALINESE LETTER IKARA TEDUNG;Lo;0;L;1B07 1B35;;;;N;;;;;
+1B09;BALINESE LETTER UKARA;Lo;0;L;;;;;N;;;;;
+1B0A;BALINESE LETTER UKARA TEDUNG;Lo;0;L;1B09 1B35;;;;N;;;;;
+1B0B;BALINESE LETTER RA REPA;Lo;0;L;;;;;N;;;;;
+1B0C;BALINESE LETTER RA REPA TEDUNG;Lo;0;L;1B0B 1B35;;;;N;;;;;
+1B0D;BALINESE LETTER LA LENGA;Lo;0;L;;;;;N;;;;;
+1B0E;BALINESE LETTER LA LENGA TEDUNG;Lo;0;L;1B0D 1B35;;;;N;;;;;
+1B0F;BALINESE LETTER EKARA;Lo;0;L;;;;;N;;;;;
+1B10;BALINESE LETTER AIKARA;Lo;0;L;;;;;N;;;;;
+1B11;BALINESE LETTER OKARA;Lo;0;L;;;;;N;;;;;
+1B12;BALINESE LETTER OKARA TEDUNG;Lo;0;L;1B11 1B35;;;;N;;;;;
+1B13;BALINESE LETTER KA;Lo;0;L;;;;;N;;;;;
+1B14;BALINESE LETTER KA MAHAPRANA;Lo;0;L;;;;;N;;;;;
+1B15;BALINESE LETTER GA;Lo;0;L;;;;;N;;;;;
+1B16;BALINESE LETTER GA GORA;Lo;0;L;;;;;N;;;;;
+1B17;BALINESE LETTER NGA;Lo;0;L;;;;;N;;;;;
+1B18;BALINESE LETTER CA;Lo;0;L;;;;;N;;;;;
+1B19;BALINESE LETTER CA LACA;Lo;0;L;;;;;N;;;;;
+1B1A;BALINESE LETTER JA;Lo;0;L;;;;;N;;;;;
+1B1B;BALINESE LETTER JA JERA;Lo;0;L;;;;;N;;;;;
+1B1C;BALINESE LETTER NYA;Lo;0;L;;;;;N;;;;;
+1B1D;BALINESE LETTER TA LATIK;Lo;0;L;;;;;N;;;;;
+1B1E;BALINESE LETTER TA MURDA MAHAPRANA;Lo;0;L;;;;;N;;;;;
+1B1F;BALINESE LETTER DA MURDA ALPAPRANA;Lo;0;L;;;;;N;;;;;
+1B20;BALINESE LETTER DA MURDA MAHAPRANA;Lo;0;L;;;;;N;;;;;
+1B21;BALINESE LETTER NA RAMBAT;Lo;0;L;;;;;N;;;;;
+1B22;BALINESE LETTER TA;Lo;0;L;;;;;N;;;;;
+1B23;BALINESE LETTER TA TAWA;Lo;0;L;;;;;N;;;;;
+1B24;BALINESE LETTER DA;Lo;0;L;;;;;N;;;;;
+1B25;BALINESE LETTER DA MADU;Lo;0;L;;;;;N;;;;;
+1B26;BALINESE LETTER NA;Lo;0;L;;;;;N;;;;;
+1B27;BALINESE LETTER PA;Lo;0;L;;;;;N;;;;;
+1B28;BALINESE LETTER PA KAPAL;Lo;0;L;;;;;N;;;;;
+1B29;BALINESE LETTER BA;Lo;0;L;;;;;N;;;;;
+1B2A;BALINESE LETTER BA KEMBANG;Lo;0;L;;;;;N;;;;;
+1B2B;BALINESE LETTER MA;Lo;0;L;;;;;N;;;;;
+1B2C;BALINESE LETTER YA;Lo;0;L;;;;;N;;;;;
+1B2D;BALINESE LETTER RA;Lo;0;L;;;;;N;;;;;
+1B2E;BALINESE LETTER LA;Lo;0;L;;;;;N;;;;;
+1B2F;BALINESE LETTER WA;Lo;0;L;;;;;N;;;;;
+1B30;BALINESE LETTER SA SAGA;Lo;0;L;;;;;N;;;;;
+1B31;BALINESE LETTER SA SAPA;Lo;0;L;;;;;N;;;;;
+1B32;BALINESE LETTER SA;Lo;0;L;;;;;N;;;;;
+1B33;BALINESE LETTER HA;Lo;0;L;;;;;N;;;;;
+1B34;BALINESE SIGN REREKAN;Mn;7;NSM;;;;;N;;;;;
+1B35;BALINESE VOWEL SIGN TEDUNG;Mc;0;L;;;;;N;;;;;
+1B36;BALINESE VOWEL SIGN ULU;Mn;0;NSM;;;;;N;;;;;
+1B37;BALINESE VOWEL SIGN ULU SARI;Mn;0;NSM;;;;;N;;;;;
+1B38;BALINESE VOWEL SIGN SUKU;Mn;0;NSM;;;;;N;;;;;
+1B39;BALINESE VOWEL SIGN SUKU ILUT;Mn;0;NSM;;;;;N;;;;;
+1B3A;BALINESE VOWEL SIGN RA REPA;Mn;0;NSM;;;;;N;;;;;
+1B3B;BALINESE VOWEL SIGN RA REPA TEDUNG;Mc;0;L;1B3A 1B35;;;;N;;;;;
+1B3C;BALINESE VOWEL SIGN LA LENGA;Mn;0;NSM;;;;;N;;;;;
+1B3D;BALINESE VOWEL SIGN LA LENGA TEDUNG;Mc;0;L;1B3C 1B35;;;;N;;;;;
+1B3E;BALINESE VOWEL SIGN TALING;Mc;0;L;;;;;N;;;;;
+1B3F;BALINESE VOWEL SIGN TALING REPA;Mc;0;L;;;;;N;;;;;
+1B40;BALINESE VOWEL SIGN TALING TEDUNG;Mc;0;L;1B3E 1B35;;;;N;;;;;
+1B41;BALINESE VOWEL SIGN TALING REPA TEDUNG;Mc;0;L;1B3F 1B35;;;;N;;;;;
+1B42;BALINESE VOWEL SIGN PEPET;Mn;0;NSM;;;;;N;;;;;
+1B43;BALINESE VOWEL SIGN PEPET TEDUNG;Mc;0;L;1B42 1B35;;;;N;;;;;
+1B44;BALINESE ADEG ADEG;Mc;9;L;;;;;N;;;;;
+1B45;BALINESE LETTER KAF SASAK;Lo;0;L;;;;;N;;;;;
+1B46;BALINESE LETTER KHOT SASAK;Lo;0;L;;;;;N;;;;;
+1B47;BALINESE LETTER TZIR SASAK;Lo;0;L;;;;;N;;;;;
+1B48;BALINESE LETTER EF SASAK;Lo;0;L;;;;;N;;;;;
+1B49;BALINESE LETTER VE SASAK;Lo;0;L;;;;;N;;;;;
+1B4A;BALINESE LETTER ZAL SASAK;Lo;0;L;;;;;N;;;;;
+1B4B;BALINESE LETTER ASYURA SASAK;Lo;0;L;;;;;N;;;;;
+1B50;BALINESE DIGIT ZERO;Nd;0;L;;0;0;0;N;;;;;
+1B51;BALINESE DIGIT ONE;Nd;0;L;;1;1;1;N;;;;;
+1B52;BALINESE DIGIT TWO;Nd;0;L;;2;2;2;N;;;;;
+1B53;BALINESE DIGIT THREE;Nd;0;L;;3;3;3;N;;;;;
+1B54;BALINESE DIGIT FOUR;Nd;0;L;;4;4;4;N;;;;;
+1B55;BALINESE DIGIT FIVE;Nd;0;L;;5;5;5;N;;;;;
+1B56;BALINESE DIGIT SIX;Nd;0;L;;6;6;6;N;;;;;
+1B57;BALINESE DIGIT SEVEN;Nd;0;L;;7;7;7;N;;;;;
+1B58;BALINESE DIGIT EIGHT;Nd;0;L;;8;8;8;N;;;;;
+1B59;BALINESE DIGIT NINE;Nd;0;L;;9;9;9;N;;;;;
+1B5A;BALINESE PANTI;Po;0;L;;;;;N;;;;;
+1B5B;BALINESE PAMADA;Po;0;L;;;;;N;;;;;
+1B5C;BALINESE WINDU;Po;0;L;;;;;N;;;;;
+1B5D;BALINESE CARIK PAMUNGKAH;Po;0;L;;;;;N;;;;;
+1B5E;BALINESE CARIK SIKI;Po;0;L;;;;;N;;;;;
+1B5F;BALINESE CARIK PAREREN;Po;0;L;;;;;N;;;;;
+1B60;BALINESE PAMENENG;Po;0;L;;;;;N;;;;;
+1B61;BALINESE MUSICAL SYMBOL DONG;So;0;L;;;;;N;;;;;
+1B62;BALINESE MUSICAL SYMBOL DENG;So;0;L;;;;;N;;;;;
+1B63;BALINESE MUSICAL SYMBOL DUNG;So;0;L;;;;;N;;;;;
+1B64;BALINESE MUSICAL SYMBOL DANG;So;0;L;;;;;N;;;;;
+1B65;BALINESE MUSICAL SYMBOL DANG SURANG;So;0;L;;;;;N;;;;;
+1B66;BALINESE MUSICAL SYMBOL DING;So;0;L;;;;;N;;;;;
+1B67;BALINESE MUSICAL SYMBOL DAENG;So;0;L;;;;;N;;;;;
+1B68;BALINESE MUSICAL SYMBOL DEUNG;So;0;L;;;;;N;;;;;
+1B69;BALINESE MUSICAL SYMBOL DAING;So;0;L;;;;;N;;;;;
+1B6A;BALINESE MUSICAL SYMBOL DANG GEDE;So;0;L;;;;;N;;;;;
+1B6B;BALINESE MUSICAL SYMBOL COMBINING TEGEH;Mn;230;NSM;;;;;N;;;;;
+1B6C;BALINESE MUSICAL SYMBOL COMBINING ENDEP;Mn;220;NSM;;;;;N;;;;;
+1B6D;BALINESE MUSICAL SYMBOL COMBINING KEMPUL;Mn;230;NSM;;;;;N;;;;;
+1B6E;BALINESE MUSICAL SYMBOL COMBINING KEMPLI;Mn;230;NSM;;;;;N;;;;;
+1B6F;BALINESE MUSICAL SYMBOL COMBINING JEGOGAN;Mn;230;NSM;;;;;N;;;;;
+1B70;BALINESE MUSICAL SYMBOL COMBINING KEMPUL WITH JEGOGAN;Mn;230;NSM;;;;;N;;;;;
+1B71;BALINESE MUSICAL SYMBOL COMBINING KEMPLI WITH JEGOGAN;Mn;230;NSM;;;;;N;;;;;
+1B72;BALINESE MUSICAL SYMBOL COMBINING BENDE;Mn;230;NSM;;;;;N;;;;;
+1B73;BALINESE MUSICAL SYMBOL COMBINING GONG;Mn;230;NSM;;;;;N;;;;;
+1B74;BALINESE MUSICAL SYMBOL RIGHT-HAND OPEN DUG;So;0;L;;;;;N;;;;;
+1B75;BALINESE MUSICAL SYMBOL RIGHT-HAND OPEN DAG;So;0;L;;;;;N;;;;;
+1B76;BALINESE MUSICAL SYMBOL RIGHT-HAND CLOSED TUK;So;0;L;;;;;N;;;;;
+1B77;BALINESE MUSICAL SYMBOL RIGHT-HAND CLOSED TAK;So;0;L;;;;;N;;;;;
+1B78;BALINESE MUSICAL SYMBOL LEFT-HAND OPEN PANG;So;0;L;;;;;N;;;;;
+1B79;BALINESE MUSICAL SYMBOL LEFT-HAND OPEN PUNG;So;0;L;;;;;N;;;;;
+1B7A;BALINESE MUSICAL SYMBOL LEFT-HAND CLOSED PLAK;So;0;L;;;;;N;;;;;
+1B7B;BALINESE MUSICAL SYMBOL LEFT-HAND CLOSED PLUK;So;0;L;;;;;N;;;;;
+1B7C;BALINESE MUSICAL SYMBOL LEFT-HAND OPEN PING;So;0;L;;;;;N;;;;;
+1B80;SUNDANESE SIGN PANYECEK;Mn;0;NSM;;;;;N;;;;;
+1B81;SUNDANESE SIGN PANGLAYAR;Mn;0;NSM;;;;;N;;;;;
+1B82;SUNDANESE SIGN PANGWISAD;Mc;0;L;;;;;N;;;;;
+1B83;SUNDANESE LETTER A;Lo;0;L;;;;;N;;;;;
+1B84;SUNDANESE LETTER I;Lo;0;L;;;;;N;;;;;
+1B85;SUNDANESE LETTER U;Lo;0;L;;;;;N;;;;;
+1B86;SUNDANESE LETTER AE;Lo;0;L;;;;;N;;;;;
+1B87;SUNDANESE LETTER O;Lo;0;L;;;;;N;;;;;
+1B88;SUNDANESE LETTER E;Lo;0;L;;;;;N;;;;;
+1B89;SUNDANESE LETTER EU;Lo;0;L;;;;;N;;;;;
+1B8A;SUNDANESE LETTER KA;Lo;0;L;;;;;N;;;;;
+1B8B;SUNDANESE LETTER QA;Lo;0;L;;;;;N;;;;;
+1B8C;SUNDANESE LETTER GA;Lo;0;L;;;;;N;;;;;
+1B8D;SUNDANESE LETTER NGA;Lo;0;L;;;;;N;;;;;
+1B8E;SUNDANESE LETTER CA;Lo;0;L;;;;;N;;;;;
+1B8F;SUNDANESE LETTER JA;Lo;0;L;;;;;N;;;;;
+1B90;SUNDANESE LETTER ZA;Lo;0;L;;;;;N;;;;;
+1B91;SUNDANESE LETTER NYA;Lo;0;L;;;;;N;;;;;
+1B92;SUNDANESE LETTER TA;Lo;0;L;;;;;N;;;;;
+1B93;SUNDANESE LETTER DA;Lo;0;L;;;;;N;;;;;
+1B94;SUNDANESE LETTER NA;Lo;0;L;;;;;N;;;;;
+1B95;SUNDANESE LETTER PA;Lo;0;L;;;;;N;;;;;
+1B96;SUNDANESE LETTER FA;Lo;0;L;;;;;N;;;;;
+1B97;SUNDANESE LETTER VA;Lo;0;L;;;;;N;;;;;
+1B98;SUNDANESE LETTER BA;Lo;0;L;;;;;N;;;;;
+1B99;SUNDANESE LETTER MA;Lo;0;L;;;;;N;;;;;
+1B9A;SUNDANESE LETTER YA;Lo;0;L;;;;;N;;;;;
+1B9B;SUNDANESE LETTER RA;Lo;0;L;;;;;N;;;;;
+1B9C;SUNDANESE LETTER LA;Lo;0;L;;;;;N;;;;;
+1B9D;SUNDANESE LETTER WA;Lo;0;L;;;;;N;;;;;
+1B9E;SUNDANESE LETTER SA;Lo;0;L;;;;;N;;;;;
+1B9F;SUNDANESE LETTER XA;Lo;0;L;;;;;N;;;;;
+1BA0;SUNDANESE LETTER HA;Lo;0;L;;;;;N;;;;;
+1BA1;SUNDANESE CONSONANT SIGN PAMINGKAL;Mc;0;L;;;;;N;;;;;
+1BA2;SUNDANESE CONSONANT SIGN PANYAKRA;Mn;0;NSM;;;;;N;;;;;
+1BA3;SUNDANESE CONSONANT SIGN PANYIKU;Mn;0;NSM;;;;;N;;;;;
+1BA4;SUNDANESE VOWEL SIGN PANGHULU;Mn;0;NSM;;;;;N;;;;;
+1BA5;SUNDANESE VOWEL SIGN PANYUKU;Mn;0;NSM;;;;;N;;;;;
+1BA6;SUNDANESE VOWEL SIGN PANAELAENG;Mc;0;L;;;;;N;;;;;
+1BA7;SUNDANESE VOWEL SIGN PANOLONG;Mc;0;L;;;;;N;;;;;
+1BA8;SUNDANESE VOWEL SIGN PAMEPET;Mn;0;NSM;;;;;N;;;;;
+1BA9;SUNDANESE VOWEL SIGN PANEULEUNG;Mn;0;NSM;;;;;N;;;;;
+1BAA;SUNDANESE SIGN PAMAAEH;Mc;9;L;;;;;N;;;;;
+1BAB;SUNDANESE SIGN VIRAMA;Mn;9;NSM;;;;;N;;;;;
+1BAC;SUNDANESE CONSONANT SIGN PASANGAN MA;Mn;0;NSM;;;;;N;;;;;
+1BAD;SUNDANESE CONSONANT SIGN PASANGAN WA;Mn;0;NSM;;;;;N;;;;;
+1BAE;SUNDANESE LETTER KHA;Lo;0;L;;;;;N;;;;;
+1BAF;SUNDANESE LETTER SYA;Lo;0;L;;;;;N;;;;;
+1BB0;SUNDANESE DIGIT ZERO;Nd;0;L;;0;0;0;N;;;;;
+1BB1;SUNDANESE DIGIT ONE;Nd;0;L;;1;1;1;N;;;;;
+1BB2;SUNDANESE DIGIT TWO;Nd;0;L;;2;2;2;N;;;;;
+1BB3;SUNDANESE DIGIT THREE;Nd;0;L;;3;3;3;N;;;;;
+1BB4;SUNDANESE DIGIT FOUR;Nd;0;L;;4;4;4;N;;;;;
+1BB5;SUNDANESE DIGIT FIVE;Nd;0;L;;5;5;5;N;;;;;
+1BB6;SUNDANESE DIGIT SIX;Nd;0;L;;6;6;6;N;;;;;
+1BB7;SUNDANESE DIGIT SEVEN;Nd;0;L;;7;7;7;N;;;;;
+1BB8;SUNDANESE DIGIT EIGHT;Nd;0;L;;8;8;8;N;;;;;
+1BB9;SUNDANESE DIGIT NINE;Nd;0;L;;9;9;9;N;;;;;
+1BBA;SUNDANESE AVAGRAHA;Lo;0;L;;;;;N;;;;;
+1BBB;SUNDANESE LETTER REU;Lo;0;L;;;;;N;;;;;
+1BBC;SUNDANESE LETTER LEU;Lo;0;L;;;;;N;;;;;
+1BBD;SUNDANESE LETTER BHA;Lo;0;L;;;;;N;;;;;
+1BBE;SUNDANESE LETTER FINAL K;Lo;0;L;;;;;N;;;;;
+1BBF;SUNDANESE LETTER FINAL M;Lo;0;L;;;;;N;;;;;
+1BC0;BATAK LETTER A;Lo;0;L;;;;;N;;;;;
+1BC1;BATAK LETTER SIMALUNGUN A;Lo;0;L;;;;;N;;;;;
+1BC2;BATAK LETTER HA;Lo;0;L;;;;;N;;;;;
+1BC3;BATAK LETTER SIMALUNGUN HA;Lo;0;L;;;;;N;;;;;
+1BC4;BATAK LETTER MANDAILING HA;Lo;0;L;;;;;N;;;;;
+1BC5;BATAK LETTER BA;Lo;0;L;;;;;N;;;;;
+1BC6;BATAK LETTER KARO BA;Lo;0;L;;;;;N;;;;;
+1BC7;BATAK LETTER PA;Lo;0;L;;;;;N;;;;;
+1BC8;BATAK LETTER SIMALUNGUN PA;Lo;0;L;;;;;N;;;;;
+1BC9;BATAK LETTER NA;Lo;0;L;;;;;N;;;;;
+1BCA;BATAK LETTER MANDAILING NA;Lo;0;L;;;;;N;;;;;
+1BCB;BATAK LETTER WA;Lo;0;L;;;;;N;;;;;
+1BCC;BATAK LETTER SIMALUNGUN WA;Lo;0;L;;;;;N;;;;;
+1BCD;BATAK LETTER PAKPAK WA;Lo;0;L;;;;;N;;;;;
+1BCE;BATAK LETTER GA;Lo;0;L;;;;;N;;;;;
+1BCF;BATAK LETTER SIMALUNGUN GA;Lo;0;L;;;;;N;;;;;
+1BD0;BATAK LETTER JA;Lo;0;L;;;;;N;;;;;
+1BD1;BATAK LETTER DA;Lo;0;L;;;;;N;;;;;
+1BD2;BATAK LETTER RA;Lo;0;L;;;;;N;;;;;
+1BD3;BATAK LETTER SIMALUNGUN RA;Lo;0;L;;;;;N;;;;;
+1BD4;BATAK LETTER MA;Lo;0;L;;;;;N;;;;;
+1BD5;BATAK LETTER SIMALUNGUN MA;Lo;0;L;;;;;N;;;;;
+1BD6;BATAK LETTER SOUTHERN TA;Lo;0;L;;;;;N;;;;;
+1BD7;BATAK LETTER NORTHERN TA;Lo;0;L;;;;;N;;;;;
+1BD8;BATAK LETTER SA;Lo;0;L;;;;;N;;;;;
+1BD9;BATAK LETTER SIMALUNGUN SA;Lo;0;L;;;;;N;;;;;
+1BDA;BATAK LETTER MANDAILING SA;Lo;0;L;;;;;N;;;;;
+1BDB;BATAK LETTER YA;Lo;0;L;;;;;N;;;;;
+1BDC;BATAK LETTER SIMALUNGUN YA;Lo;0;L;;;;;N;;;;;
+1BDD;BATAK LETTER NGA;Lo;0;L;;;;;N;;;;;
+1BDE;BATAK LETTER LA;Lo;0;L;;;;;N;;;;;
+1BDF;BATAK LETTER SIMALUNGUN LA;Lo;0;L;;;;;N;;;;;
+1BE0;BATAK LETTER NYA;Lo;0;L;;;;;N;;;;;
+1BE1;BATAK LETTER CA;Lo;0;L;;;;;N;;;;;
+1BE2;BATAK LETTER NDA;Lo;0;L;;;;;N;;;;;
+1BE3;BATAK LETTER MBA;Lo;0;L;;;;;N;;;;;
+1BE4;BATAK LETTER I;Lo;0;L;;;;;N;;;;;
+1BE5;BATAK LETTER U;Lo;0;L;;;;;N;;;;;
+1BE6;BATAK SIGN TOMPI;Mn;7;NSM;;;;;N;;;;;
+1BE7;BATAK VOWEL SIGN E;Mc;0;L;;;;;N;;;;;
+1BE8;BATAK VOWEL SIGN PAKPAK E;Mn;0;NSM;;;;;N;;;;;
+1BE9;BATAK VOWEL SIGN EE;Mn;0;NSM;;;;;N;;;;;
+1BEA;BATAK VOWEL SIGN I;Mc;0;L;;;;;N;;;;;
+1BEB;BATAK VOWEL SIGN KARO I;Mc;0;L;;;;;N;;;;;
+1BEC;BATAK VOWEL SIGN O;Mc;0;L;;;;;N;;;;;
+1BED;BATAK VOWEL SIGN KARO O;Mn;0;NSM;;;;;N;;;;;
+1BEE;BATAK VOWEL SIGN U;Mc;0;L;;;;;N;;;;;
+1BEF;BATAK VOWEL SIGN U FOR SIMALUNGUN SA;Mn;0;NSM;;;;;N;;;;;
+1BF0;BATAK CONSONANT SIGN NG;Mn;0;NSM;;;;;N;;;;;
+1BF1;BATAK CONSONANT SIGN H;Mn;0;NSM;;;;;N;;;;;
+1BF2;BATAK PANGOLAT;Mc;9;L;;;;;N;;;;;
+1BF3;BATAK PANONGONAN;Mc;9;L;;;;;N;;;;;
+1BFC;BATAK SYMBOL BINDU NA METEK;Po;0;L;;;;;N;;;;;
+1BFD;BATAK SYMBOL BINDU PINARBORAS;Po;0;L;;;;;N;;;;;
+1BFE;BATAK SYMBOL BINDU JUDUL;Po;0;L;;;;;N;;;;;
+1BFF;BATAK SYMBOL BINDU PANGOLAT;Po;0;L;;;;;N;;;;;
+1C00;LEPCHA LETTER KA;Lo;0;L;;;;;N;;;;;
+1C01;LEPCHA LETTER KLA;Lo;0;L;;;;;N;;;;;
+1C02;LEPCHA LETTER KHA;Lo;0;L;;;;;N;;;;;
+1C03;LEPCHA LETTER GA;Lo;0;L;;;;;N;;;;;
+1C04;LEPCHA LETTER GLA;Lo;0;L;;;;;N;;;;;
+1C05;LEPCHA LETTER NGA;Lo;0;L;;;;;N;;;;;
+1C06;LEPCHA LETTER CA;Lo;0;L;;;;;N;;;;;
+1C07;LEPCHA LETTER CHA;Lo;0;L;;;;;N;;;;;
+1C08;LEPCHA LETTER JA;Lo;0;L;;;;;N;;;;;
+1C09;LEPCHA LETTER NYA;Lo;0;L;;;;;N;;;;;
+1C0A;LEPCHA LETTER TA;Lo;0;L;;;;;N;;;;;
+1C0B;LEPCHA LETTER THA;Lo;0;L;;;;;N;;;;;
+1C0C;LEPCHA LETTER DA;Lo;0;L;;;;;N;;;;;
+1C0D;LEPCHA LETTER NA;Lo;0;L;;;;;N;;;;;
+1C0E;LEPCHA LETTER PA;Lo;0;L;;;;;N;;;;;
+1C0F;LEPCHA LETTER PLA;Lo;0;L;;;;;N;;;;;
+1C10;LEPCHA LETTER PHA;Lo;0;L;;;;;N;;;;;
+1C11;LEPCHA LETTER FA;Lo;0;L;;;;;N;;;;;
+1C12;LEPCHA LETTER FLA;Lo;0;L;;;;;N;;;;;
+1C13;LEPCHA LETTER BA;Lo;0;L;;;;;N;;;;;
+1C14;LEPCHA LETTER BLA;Lo;0;L;;;;;N;;;;;
+1C15;LEPCHA LETTER MA;Lo;0;L;;;;;N;;;;;
+1C16;LEPCHA LETTER MLA;Lo;0;L;;;;;N;;;;;
+1C17;LEPCHA LETTER TSA;Lo;0;L;;;;;N;;;;;
+1C18;LEPCHA LETTER TSHA;Lo;0;L;;;;;N;;;;;
+1C19;LEPCHA LETTER DZA;Lo;0;L;;;;;N;;;;;
+1C1A;LEPCHA LETTER YA;Lo;0;L;;;;;N;;;;;
+1C1B;LEPCHA LETTER RA;Lo;0;L;;;;;N;;;;;
+1C1C;LEPCHA LETTER LA;Lo;0;L;;;;;N;;;;;
+1C1D;LEPCHA LETTER HA;Lo;0;L;;;;;N;;;;;
+1C1E;LEPCHA LETTER HLA;Lo;0;L;;;;;N;;;;;
+1C1F;LEPCHA LETTER VA;Lo;0;L;;;;;N;;;;;
+1C20;LEPCHA LETTER SA;Lo;0;L;;;;;N;;;;;
+1C21;LEPCHA LETTER SHA;Lo;0;L;;;;;N;;;;;
+1C22;LEPCHA LETTER WA;Lo;0;L;;;;;N;;;;;
+1C23;LEPCHA LETTER A;Lo;0;L;;;;;N;;;;;
+1C24;LEPCHA SUBJOINED LETTER YA;Mc;0;L;;;;;N;;;;;
+1C25;LEPCHA SUBJOINED LETTER RA;Mc;0;L;;;;;N;;;;;
+1C26;LEPCHA VOWEL SIGN AA;Mc;0;L;;;;;N;;;;;
+1C27;LEPCHA VOWEL SIGN I;Mc;0;L;;;;;N;;;;;
+1C28;LEPCHA VOWEL SIGN O;Mc;0;L;;;;;N;;;;;
+1C29;LEPCHA VOWEL SIGN OO;Mc;0;L;;;;;N;;;;;
+1C2A;LEPCHA VOWEL SIGN U;Mc;0;L;;;;;N;;;;;
+1C2B;LEPCHA VOWEL SIGN UU;Mc;0;L;;;;;N;;;;;
+1C2C;LEPCHA VOWEL SIGN E;Mn;0;NSM;;;;;N;;;;;
+1C2D;LEPCHA CONSONANT SIGN K;Mn;0;NSM;;;;;N;;;;;
+1C2E;LEPCHA CONSONANT SIGN M;Mn;0;NSM;;;;;N;;;;;
+1C2F;LEPCHA CONSONANT SIGN L;Mn;0;NSM;;;;;N;;;;;
+1C30;LEPCHA CONSONANT SIGN N;Mn;0;NSM;;;;;N;;;;;
+1C31;LEPCHA CONSONANT SIGN P;Mn;0;NSM;;;;;N;;;;;
+1C32;LEPCHA CONSONANT SIGN R;Mn;0;NSM;;;;;N;;;;;
+1C33;LEPCHA CONSONANT SIGN T;Mn;0;NSM;;;;;N;;;;;
+1C34;LEPCHA CONSONANT SIGN NYIN-DO;Mc;0;L;;;;;N;;;;;
+1C35;LEPCHA CONSONANT SIGN KANG;Mc;0;L;;;;;N;;;;;
+1C36;LEPCHA SIGN RAN;Mn;0;NSM;;;;;N;;;;;
+1C37;LEPCHA SIGN NUKTA;Mn;7;NSM;;;;;N;;;;;
+1C3B;LEPCHA PUNCTUATION TA-ROL;Po;0;L;;;;;N;;;;;
+1C3C;LEPCHA PUNCTUATION NYET THYOOM TA-ROL;Po;0;L;;;;;N;;;;;
+1C3D;LEPCHA PUNCTUATION CER-WA;Po;0;L;;;;;N;;;;;
+1C3E;LEPCHA PUNCTUATION TSHOOK CER-WA;Po;0;L;;;;;N;;;;;
+1C3F;LEPCHA PUNCTUATION TSHOOK;Po;0;L;;;;;N;;;;;
+1C40;LEPCHA DIGIT ZERO;Nd;0;L;;0;0;0;N;;;;;
+1C41;LEPCHA DIGIT ONE;Nd;0;L;;1;1;1;N;;;;;
+1C42;LEPCHA DIGIT TWO;Nd;0;L;;2;2;2;N;;;;;
+1C43;LEPCHA DIGIT THREE;Nd;0;L;;3;3;3;N;;;;;
+1C44;LEPCHA DIGIT FOUR;Nd;0;L;;4;4;4;N;;;;;
+1C45;LEPCHA DIGIT FIVE;Nd;0;L;;5;5;5;N;;;;;
+1C46;LEPCHA DIGIT SIX;Nd;0;L;;6;6;6;N;;;;;
+1C47;LEPCHA DIGIT SEVEN;Nd;0;L;;7;7;7;N;;;;;
+1C48;LEPCHA DIGIT EIGHT;Nd;0;L;;8;8;8;N;;;;;
+1C49;LEPCHA DIGIT NINE;Nd;0;L;;9;9;9;N;;;;;
+1C4D;LEPCHA LETTER TTA;Lo;0;L;;;;;N;;;;;
+1C4E;LEPCHA LETTER TTHA;Lo;0;L;;;;;N;;;;;
+1C4F;LEPCHA LETTER DDA;Lo;0;L;;;;;N;;;;;
+1C50;OL CHIKI DIGIT ZERO;Nd;0;L;;0;0;0;N;;;;;
+1C51;OL CHIKI DIGIT ONE;Nd;0;L;;1;1;1;N;;;;;
+1C52;OL CHIKI DIGIT TWO;Nd;0;L;;2;2;2;N;;;;;
+1C53;OL CHIKI DIGIT THREE;Nd;0;L;;3;3;3;N;;;;;
+1C54;OL CHIKI DIGIT FOUR;Nd;0;L;;4;4;4;N;;;;;
+1C55;OL CHIKI DIGIT FIVE;Nd;0;L;;5;5;5;N;;;;;
+1C56;OL CHIKI DIGIT SIX;Nd;0;L;;6;6;6;N;;;;;
+1C57;OL CHIKI DIGIT SEVEN;Nd;0;L;;7;7;7;N;;;;;
+1C58;OL CHIKI DIGIT EIGHT;Nd;0;L;;8;8;8;N;;;;;
+1C59;OL CHIKI DIGIT NINE;Nd;0;L;;9;9;9;N;;;;;
+1C5A;OL CHIKI LETTER LA;Lo;0;L;;;;;N;;;;;
+1C5B;OL CHIKI LETTER AT;Lo;0;L;;;;;N;;;;;
+1C5C;OL CHIKI LETTER AG;Lo;0;L;;;;;N;;;;;
+1C5D;OL CHIKI LETTER ANG;Lo;0;L;;;;;N;;;;;
+1C5E;OL CHIKI LETTER AL;Lo;0;L;;;;;N;;;;;
+1C5F;OL CHIKI LETTER LAA;Lo;0;L;;;;;N;;;;;
+1C60;OL CHIKI LETTER AAK;Lo;0;L;;;;;N;;;;;
+1C61;OL CHIKI LETTER AAJ;Lo;0;L;;;;;N;;;;;
+1C62;OL CHIKI LETTER AAM;Lo;0;L;;;;;N;;;;;
+1C63;OL CHIKI LETTER AAW;Lo;0;L;;;;;N;;;;;
+1C64;OL CHIKI LETTER LI;Lo;0;L;;;;;N;;;;;
+1C65;OL CHIKI LETTER IS;Lo;0;L;;;;;N;;;;;
+1C66;OL CHIKI LETTER IH;Lo;0;L;;;;;N;;;;;
+1C67;OL CHIKI LETTER INY;Lo;0;L;;;;;N;;;;;
+1C68;OL CHIKI LETTER IR;Lo;0;L;;;;;N;;;;;
+1C69;OL CHIKI LETTER LU;Lo;0;L;;;;;N;;;;;
+1C6A;OL CHIKI LETTER UC;Lo;0;L;;;;;N;;;;;
+1C6B;OL CHIKI LETTER UD;Lo;0;L;;;;;N;;;;;
+1C6C;OL CHIKI LETTER UNN;Lo;0;L;;;;;N;;;;;
+1C6D;OL CHIKI LETTER UY;Lo;0;L;;;;;N;;;;;
+1C6E;OL CHIKI LETTER LE;Lo;0;L;;;;;N;;;;;
+1C6F;OL CHIKI LETTER EP;Lo;0;L;;;;;N;;;;;
+1C70;OL CHIKI LETTER EDD;Lo;0;L;;;;;N;;;;;
+1C71;OL CHIKI LETTER EN;Lo;0;L;;;;;N;;;;;
+1C72;OL CHIKI LETTER ERR;Lo;0;L;;;;;N;;;;;
+1C73;OL CHIKI LETTER LO;Lo;0;L;;;;;N;;;;;
+1C74;OL CHIKI LETTER OTT;Lo;0;L;;;;;N;;;;;
+1C75;OL CHIKI LETTER OB;Lo;0;L;;;;;N;;;;;
+1C76;OL CHIKI LETTER OV;Lo;0;L;;;;;N;;;;;
+1C77;OL CHIKI LETTER OH;Lo;0;L;;;;;N;;;;;
+1C78;OL CHIKI MU TTUDDAG;Lm;0;L;;;;;N;;;;;
+1C79;OL CHIKI GAAHLAA TTUDDAAG;Lm;0;L;;;;;N;;;;;
+1C7A;OL CHIKI MU-GAAHLAA TTUDDAAG;Lm;0;L;;;;;N;;;;;
+1C7B;OL CHIKI RELAA;Lm;0;L;;;;;N;;;;;
+1C7C;OL CHIKI PHAARKAA;Lm;0;L;;;;;N;;;;;
+1C7D;OL CHIKI AHAD;Lm;0;L;;;;;N;;;;;
+1C7E;OL CHIKI PUNCTUATION MUCAAD;Po;0;L;;;;;N;;;;;
+1C7F;OL CHIKI PUNCTUATION DOUBLE MUCAAD;Po;0;L;;;;;N;;;;;
+1C80;CYRILLIC SMALL LETTER ROUNDED VE;Ll;0;L;;;;;N;;;0412;;0412
+1C81;CYRILLIC SMALL LETTER LONG-LEGGED DE;Ll;0;L;;;;;N;;;0414;;0414
+1C82;CYRILLIC SMALL LETTER NARROW O;Ll;0;L;;;;;N;;;041E;;041E
+1C83;CYRILLIC SMALL LETTER WIDE ES;Ll;0;L;;;;;N;;;0421;;0421
+1C84;CYRILLIC SMALL LETTER TALL TE;Ll;0;L;;;;;N;;;0422;;0422
+1C85;CYRILLIC SMALL LETTER THREE-LEGGED TE;Ll;0;L;;;;;N;;;0422;;0422
+1C86;CYRILLIC SMALL LETTER TALL HARD SIGN;Ll;0;L;;;;;N;;;042A;;042A
+1C87;CYRILLIC SMALL LETTER TALL YAT;Ll;0;L;;;;;N;;;0462;;0462
+1C88;CYRILLIC SMALL LETTER UNBLENDED UK;Ll;0;L;;;;;N;;;A64A;;A64A
+1CC0;SUNDANESE PUNCTUATION BINDU SURYA;Po;0;L;;;;;N;;;;;
+1CC1;SUNDANESE PUNCTUATION BINDU PANGLONG;Po;0;L;;;;;N;;;;;
+1CC2;SUNDANESE PUNCTUATION BINDU PURNAMA;Po;0;L;;;;;N;;;;;
+1CC3;SUNDANESE PUNCTUATION BINDU CAKRA;Po;0;L;;;;;N;;;;;
+1CC4;SUNDANESE PUNCTUATION BINDU LEU SATANGA;Po;0;L;;;;;N;;;;;
+1CC5;SUNDANESE PUNCTUATION BINDU KA SATANGA;Po;0;L;;;;;N;;;;;
+1CC6;SUNDANESE PUNCTUATION BINDU DA SATANGA;Po;0;L;;;;;N;;;;;
+1CC7;SUNDANESE PUNCTUATION BINDU BA SATANGA;Po;0;L;;;;;N;;;;;
+1CD0;VEDIC TONE KARSHANA;Mn;230;NSM;;;;;N;;;;;
+1CD1;VEDIC TONE SHARA;Mn;230;NSM;;;;;N;;;;;
+1CD2;VEDIC TONE PRENKHA;Mn;230;NSM;;;;;N;;;;;
+1CD3;VEDIC SIGN NIHSHVASA;Po;0;L;;;;;N;;;;;
+1CD4;VEDIC SIGN YAJURVEDIC MIDLINE SVARITA;Mn;1;NSM;;;;;N;;;;;
+1CD5;VEDIC TONE YAJURVEDIC AGGRAVATED INDEPENDENT SVARITA;Mn;220;NSM;;;;;N;;;;;
+1CD6;VEDIC TONE YAJURVEDIC INDEPENDENT SVARITA;Mn;220;NSM;;;;;N;;;;;
+1CD7;VEDIC TONE YAJURVEDIC KATHAKA INDEPENDENT SVARITA;Mn;220;NSM;;;;;N;;;;;
+1CD8;VEDIC TONE CANDRA BELOW;Mn;220;NSM;;;;;N;;;;;
+1CD9;VEDIC TONE YAJURVEDIC KATHAKA INDEPENDENT SVARITA SCHROEDER;Mn;220;NSM;;;;;N;;;;;
+1CDA;VEDIC TONE DOUBLE SVARITA;Mn;230;NSM;;;;;N;;;;;
+1CDB;VEDIC TONE TRIPLE SVARITA;Mn;230;NSM;;;;;N;;;;;
+1CDC;VEDIC TONE KATHAKA ANUDATTA;Mn;220;NSM;;;;;N;;;;;
+1CDD;VEDIC TONE DOT BELOW;Mn;220;NSM;;;;;N;;;;;
+1CDE;VEDIC TONE TWO DOTS BELOW;Mn;220;NSM;;;;;N;;;;;
+1CDF;VEDIC TONE THREE DOTS BELOW;Mn;220;NSM;;;;;N;;;;;
+1CE0;VEDIC TONE RIGVEDIC KASHMIRI INDEPENDENT SVARITA;Mn;230;NSM;;;;;N;;;;;
+1CE1;VEDIC TONE ATHARVAVEDIC INDEPENDENT SVARITA;Mc;0;L;;;;;N;;;;;
+1CE2;VEDIC SIGN VISARGA SVARITA;Mn;1;NSM;;;;;N;;;;;
+1CE3;VEDIC SIGN VISARGA UDATTA;Mn;1;NSM;;;;;N;;;;;
+1CE4;VEDIC SIGN REVERSED VISARGA UDATTA;Mn;1;NSM;;;;;N;;;;;
+1CE5;VEDIC SIGN VISARGA ANUDATTA;Mn;1;NSM;;;;;N;;;;;
+1CE6;VEDIC SIGN REVERSED VISARGA ANUDATTA;Mn;1;NSM;;;;;N;;;;;
+1CE7;VEDIC SIGN VISARGA UDATTA WITH TAIL;Mn;1;NSM;;;;;N;;;;;
+1CE8;VEDIC SIGN VISARGA ANUDATTA WITH TAIL;Mn;1;NSM;;;;;N;;;;;
+1CE9;VEDIC SIGN ANUSVARA ANTARGOMUKHA;Lo;0;L;;;;;N;;;;;
+1CEA;VEDIC SIGN ANUSVARA BAHIRGOMUKHA;Lo;0;L;;;;;N;;;;;
+1CEB;VEDIC SIGN ANUSVARA VAMAGOMUKHA;Lo;0;L;;;;;N;;;;;
+1CEC;VEDIC SIGN ANUSVARA VAMAGOMUKHA WITH TAIL;Lo;0;L;;;;;N;;;;;
+1CED;VEDIC SIGN TIRYAK;Mn;220;NSM;;;;;N;;;;;
+1CEE;VEDIC SIGN HEXIFORM LONG ANUSVARA;Lo;0;L;;;;;N;;;;;
+1CEF;VEDIC SIGN LONG ANUSVARA;Lo;0;L;;;;;N;;;;;
+1CF0;VEDIC SIGN RTHANG LONG ANUSVARA;Lo;0;L;;;;;N;;;;;
+1CF1;VEDIC SIGN ANUSVARA UBHAYATO MUKHA;Lo;0;L;;;;;N;;;;;
+1CF2;VEDIC SIGN ARDHAVISARGA;Mc;0;L;;;;;N;;;;;
+1CF3;VEDIC SIGN ROTATED ARDHAVISARGA;Mc;0;L;;;;;N;;;;;
+1CF4;VEDIC TONE CANDRA ABOVE;Mn;230;NSM;;;;;N;;;;;
+1CF5;VEDIC SIGN JIHVAMULIYA;Lo;0;L;;;;;N;;;;;
+1CF6;VEDIC SIGN UPADHMANIYA;Lo;0;L;;;;;N;;;;;
+1CF8;VEDIC TONE RING ABOVE;Mn;230;NSM;;;;;N;;;;;
+1CF9;VEDIC TONE DOUBLE RING ABOVE;Mn;230;NSM;;;;;N;;;;;
+1D00;LATIN LETTER SMALL CAPITAL A;Ll;0;L;;;;;N;;;;;
+1D01;LATIN LETTER SMALL CAPITAL AE;Ll;0;L;;;;;N;;;;;
+1D02;LATIN SMALL LETTER TURNED AE;Ll;0;L;;;;;N;;;;;
+1D03;LATIN LETTER SMALL CAPITAL BARRED B;Ll;0;L;;;;;N;;;;;
+1D04;LATIN LETTER SMALL CAPITAL C;Ll;0;L;;;;;N;;;;;
+1D05;LATIN LETTER SMALL CAPITAL D;Ll;0;L;;;;;N;;;;;
+1D06;LATIN LETTER SMALL CAPITAL ETH;Ll;0;L;;;;;N;;;;;
+1D07;LATIN LETTER SMALL CAPITAL E;Ll;0;L;;;;;N;;;;;
+1D08;LATIN SMALL LETTER TURNED OPEN E;Ll;0;L;;;;;N;;;;;
+1D09;LATIN SMALL LETTER TURNED I;Ll;0;L;;;;;N;;;;;
+1D0A;LATIN LETTER SMALL CAPITAL J;Ll;0;L;;;;;N;;;;;
+1D0B;LATIN LETTER SMALL CAPITAL K;Ll;0;L;;;;;N;;;;;
+1D0C;LATIN LETTER SMALL CAPITAL L WITH STROKE;Ll;0;L;;;;;N;;;;;
+1D0D;LATIN LETTER SMALL CAPITAL M;Ll;0;L;;;;;N;;;;;
+1D0E;LATIN LETTER SMALL CAPITAL REVERSED N;Ll;0;L;;;;;N;;;;;
+1D0F;LATIN LETTER SMALL CAPITAL O;Ll;0;L;;;;;N;;;;;
+1D10;LATIN LETTER SMALL CAPITAL OPEN O;Ll;0;L;;;;;N;;;;;
+1D11;LATIN SMALL LETTER SIDEWAYS O;Ll;0;L;;;;;N;;;;;
+1D12;LATIN SMALL LETTER SIDEWAYS OPEN O;Ll;0;L;;;;;N;;;;;
+1D13;LATIN SMALL LETTER SIDEWAYS O WITH STROKE;Ll;0;L;;;;;N;;;;;
+1D14;LATIN SMALL LETTER TURNED OE;Ll;0;L;;;;;N;;;;;
+1D15;LATIN LETTER SMALL CAPITAL OU;Ll;0;L;;;;;N;;;;;
+1D16;LATIN SMALL LETTER TOP HALF O;Ll;0;L;;;;;N;;;;;
+1D17;LATIN SMALL LETTER BOTTOM HALF O;Ll;0;L;;;;;N;;;;;
+1D18;LATIN LETTER SMALL CAPITAL P;Ll;0;L;;;;;N;;;;;
+1D19;LATIN LETTER SMALL CAPITAL REVERSED R;Ll;0;L;;;;;N;;;;;
+1D1A;LATIN LETTER SMALL CAPITAL TURNED R;Ll;0;L;;;;;N;;;;;
+1D1B;LATIN LETTER SMALL CAPITAL T;Ll;0;L;;;;;N;;;;;
+1D1C;LATIN LETTER SMALL CAPITAL U;Ll;0;L;;;;;N;;;;;
+1D1D;LATIN SMALL LETTER SIDEWAYS U;Ll;0;L;;;;;N;;;;;
+1D1E;LATIN SMALL LETTER SIDEWAYS DIAERESIZED U;Ll;0;L;;;;;N;;;;;
+1D1F;LATIN SMALL LETTER SIDEWAYS TURNED M;Ll;0;L;;;;;N;;;;;
+1D20;LATIN LETTER SMALL CAPITAL V;Ll;0;L;;;;;N;;;;;
+1D21;LATIN LETTER SMALL CAPITAL W;Ll;0;L;;;;;N;;;;;
+1D22;LATIN LETTER SMALL CAPITAL Z;Ll;0;L;;;;;N;;;;;
+1D23;LATIN LETTER SMALL CAPITAL EZH;Ll;0;L;;;;;N;;;;;
+1D24;LATIN LETTER VOICED LARYNGEAL SPIRANT;Ll;0;L;;;;;N;;;;;
+1D25;LATIN LETTER AIN;Ll;0;L;;;;;N;;;;;
+1D26;GREEK LETTER SMALL CAPITAL GAMMA;Ll;0;L;;;;;N;;;;;
+1D27;GREEK LETTER SMALL CAPITAL LAMDA;Ll;0;L;;;;;N;;;;;
+1D28;GREEK LETTER SMALL CAPITAL PI;Ll;0;L;;;;;N;;;;;
+1D29;GREEK LETTER SMALL CAPITAL RHO;Ll;0;L;;;;;N;;;;;
+1D2A;GREEK LETTER SMALL CAPITAL PSI;Ll;0;L;;;;;N;;;;;
+1D2B;CYRILLIC LETTER SMALL CAPITAL EL;Ll;0;L;;;;;N;;;;;
+1D2C;MODIFIER LETTER CAPITAL A;Lm;0;L;<super> 0041;;;;N;;;;;
+1D2D;MODIFIER LETTER CAPITAL AE;Lm;0;L;<super> 00C6;;;;N;;;;;
+1D2E;MODIFIER LETTER CAPITAL B;Lm;0;L;<super> 0042;;;;N;;;;;
+1D2F;MODIFIER LETTER CAPITAL BARRED B;Lm;0;L;;;;;N;;;;;
+1D30;MODIFIER LETTER CAPITAL D;Lm;0;L;<super> 0044;;;;N;;;;;
+1D31;MODIFIER LETTER CAPITAL E;Lm;0;L;<super> 0045;;;;N;;;;;
+1D32;MODIFIER LETTER CAPITAL REVERSED E;Lm;0;L;<super> 018E;;;;N;;;;;
+1D33;MODIFIER LETTER CAPITAL G;Lm;0;L;<super> 0047;;;;N;;;;;
+1D34;MODIFIER LETTER CAPITAL H;Lm;0;L;<super> 0048;;;;N;;;;;
+1D35;MODIFIER LETTER CAPITAL I;Lm;0;L;<super> 0049;;;;N;;;;;
+1D36;MODIFIER LETTER CAPITAL J;Lm;0;L;<super> 004A;;;;N;;;;;
+1D37;MODIFIER LETTER CAPITAL K;Lm;0;L;<super> 004B;;;;N;;;;;
+1D38;MODIFIER LETTER CAPITAL L;Lm;0;L;<super> 004C;;;;N;;;;;
+1D39;MODIFIER LETTER CAPITAL M;Lm;0;L;<super> 004D;;;;N;;;;;
+1D3A;MODIFIER LETTER CAPITAL N;Lm;0;L;<super> 004E;;;;N;;;;;
+1D3B;MODIFIER LETTER CAPITAL REVERSED N;Lm;0;L;;;;;N;;;;;
+1D3C;MODIFIER LETTER CAPITAL O;Lm;0;L;<super> 004F;;;;N;;;;;
+1D3D;MODIFIER LETTER CAPITAL OU;Lm;0;L;<super> 0222;;;;N;;;;;
+1D3E;MODIFIER LETTER CAPITAL P;Lm;0;L;<super> 0050;;;;N;;;;;
+1D3F;MODIFIER LETTER CAPITAL R;Lm;0;L;<super> 0052;;;;N;;;;;
+1D40;MODIFIER LETTER CAPITAL T;Lm;0;L;<super> 0054;;;;N;;;;;
+1D41;MODIFIER LETTER CAPITAL U;Lm;0;L;<super> 0055;;;;N;;;;;
+1D42;MODIFIER LETTER CAPITAL W;Lm;0;L;<super> 0057;;;;N;;;;;
+1D43;MODIFIER LETTER SMALL A;Lm;0;L;<super> 0061;;;;N;;;;;
+1D44;MODIFIER LETTER SMALL TURNED A;Lm;0;L;<super> 0250;;;;N;;;;;
+1D45;MODIFIER LETTER SMALL ALPHA;Lm;0;L;<super> 0251;;;;N;;;;;
+1D46;MODIFIER LETTER SMALL TURNED AE;Lm;0;L;<super> 1D02;;;;N;;;;;
+1D47;MODIFIER LETTER SMALL B;Lm;0;L;<super> 0062;;;;N;;;;;
+1D48;MODIFIER LETTER SMALL D;Lm;0;L;<super> 0064;;;;N;;;;;
+1D49;MODIFIER LETTER SMALL E;Lm;0;L;<super> 0065;;;;N;;;;;
+1D4A;MODIFIER LETTER SMALL SCHWA;Lm;0;L;<super> 0259;;;;N;;;;;
+1D4B;MODIFIER LETTER SMALL OPEN E;Lm;0;L;<super> 025B;;;;N;;;;;
+1D4C;MODIFIER LETTER SMALL TURNED OPEN E;Lm;0;L;<super> 025C;;;;N;;;;;
+1D4D;MODIFIER LETTER SMALL G;Lm;0;L;<super> 0067;;;;N;;;;;
+1D4E;MODIFIER LETTER SMALL TURNED I;Lm;0;L;;;;;N;;;;;
+1D4F;MODIFIER LETTER SMALL K;Lm;0;L;<super> 006B;;;;N;;;;;
+1D50;MODIFIER LETTER SMALL M;Lm;0;L;<super> 006D;;;;N;;;;;
+1D51;MODIFIER LETTER SMALL ENG;Lm;0;L;<super> 014B;;;;N;;;;;
+1D52;MODIFIER LETTER SMALL O;Lm;0;L;<super> 006F;;;;N;;;;;
+1D53;MODIFIER LETTER SMALL OPEN O;Lm;0;L;<super> 0254;;;;N;;;;;
+1D54;MODIFIER LETTER SMALL TOP HALF O;Lm;0;L;<super> 1D16;;;;N;;;;;
+1D55;MODIFIER LETTER SMALL BOTTOM HALF O;Lm;0;L;<super> 1D17;;;;N;;;;;
+1D56;MODIFIER LETTER SMALL P;Lm;0;L;<super> 0070;;;;N;;;;;
+1D57;MODIFIER LETTER SMALL T;Lm;0;L;<super> 0074;;;;N;;;;;
+1D58;MODIFIER LETTER SMALL U;Lm;0;L;<super> 0075;;;;N;;;;;
+1D59;MODIFIER LETTER SMALL SIDEWAYS U;Lm;0;L;<super> 1D1D;;;;N;;;;;
+1D5A;MODIFIER LETTER SMALL TURNED M;Lm;0;L;<super> 026F;;;;N;;;;;
+1D5B;MODIFIER LETTER SMALL V;Lm;0;L;<super> 0076;;;;N;;;;;
+1D5C;MODIFIER LETTER SMALL AIN;Lm;0;L;<super> 1D25;;;;N;;;;;
+1D5D;MODIFIER LETTER SMALL BETA;Lm;0;L;<super> 03B2;;;;N;;;;;
+1D5E;MODIFIER LETTER SMALL GREEK GAMMA;Lm;0;L;<super> 03B3;;;;N;;;;;
+1D5F;MODIFIER LETTER SMALL DELTA;Lm;0;L;<super> 03B4;;;;N;;;;;
+1D60;MODIFIER LETTER SMALL GREEK PHI;Lm;0;L;<super> 03C6;;;;N;;;;;
+1D61;MODIFIER LETTER SMALL CHI;Lm;0;L;<super> 03C7;;;;N;;;;;
+1D62;LATIN SUBSCRIPT SMALL LETTER I;Lm;0;L;<sub> 0069;;;;N;;;;;
+1D63;LATIN SUBSCRIPT SMALL LETTER R;Lm;0;L;<sub> 0072;;;;N;;;;;
+1D64;LATIN SUBSCRIPT SMALL LETTER U;Lm;0;L;<sub> 0075;;;;N;;;;;
+1D65;LATIN SUBSCRIPT SMALL LETTER V;Lm;0;L;<sub> 0076;;;;N;;;;;
+1D66;GREEK SUBSCRIPT SMALL LETTER BETA;Lm;0;L;<sub> 03B2;;;;N;;;;;
+1D67;GREEK SUBSCRIPT SMALL LETTER GAMMA;Lm;0;L;<sub> 03B3;;;;N;;;;;
+1D68;GREEK SUBSCRIPT SMALL LETTER RHO;Lm;0;L;<sub> 03C1;;;;N;;;;;
+1D69;GREEK SUBSCRIPT SMALL LETTER PHI;Lm;0;L;<sub> 03C6;;;;N;;;;;
+1D6A;GREEK SUBSCRIPT SMALL LETTER CHI;Lm;0;L;<sub> 03C7;;;;N;;;;;
+1D6B;LATIN SMALL LETTER UE;Ll;0;L;;;;;N;;;;;
+1D6C;LATIN SMALL LETTER B WITH MIDDLE TILDE;Ll;0;L;;;;;N;;;;;
+1D6D;LATIN SMALL LETTER D WITH MIDDLE TILDE;Ll;0;L;;;;;N;;;;;
+1D6E;LATIN SMALL LETTER F WITH MIDDLE TILDE;Ll;0;L;;;;;N;;;;;
+1D6F;LATIN SMALL LETTER M WITH MIDDLE TILDE;Ll;0;L;;;;;N;;;;;
+1D70;LATIN SMALL LETTER N WITH MIDDLE TILDE;Ll;0;L;;;;;N;;;;;
+1D71;LATIN SMALL LETTER P WITH MIDDLE TILDE;Ll;0;L;;;;;N;;;;;
+1D72;LATIN SMALL LETTER R WITH MIDDLE TILDE;Ll;0;L;;;;;N;;;;;
+1D73;LATIN SMALL LETTER R WITH FISHHOOK AND MIDDLE TILDE;Ll;0;L;;;;;N;;;;;
+1D74;LATIN SMALL LETTER S WITH MIDDLE TILDE;Ll;0;L;;;;;N;;;;;
+1D75;LATIN SMALL LETTER T WITH MIDDLE TILDE;Ll;0;L;;;;;N;;;;;
+1D76;LATIN SMALL LETTER Z WITH MIDDLE TILDE;Ll;0;L;;;;;N;;;;;
+1D77;LATIN SMALL LETTER TURNED G;Ll;0;L;;;;;N;;;;;
+1D78;MODIFIER LETTER CYRILLIC EN;Lm;0;L;<super> 043D;;;;N;;;;;
+1D79;LATIN SMALL LETTER INSULAR G;Ll;0;L;;;;;N;;;A77D;;A77D
+1D7A;LATIN SMALL LETTER TH WITH STRIKETHROUGH;Ll;0;L;;;;;N;;;;;
+1D7B;LATIN SMALL CAPITAL LETTER I WITH STROKE;Ll;0;L;;;;;N;;;;;
+1D7C;LATIN SMALL LETTER IOTA WITH STROKE;Ll;0;L;;;;;N;;;;;
+1D7D;LATIN SMALL LETTER P WITH STROKE;Ll;0;L;;;;;N;;;2C63;;2C63
+1D7E;LATIN SMALL CAPITAL LETTER U WITH STROKE;Ll;0;L;;;;;N;;;;;
+1D7F;LATIN SMALL LETTER UPSILON WITH STROKE;Ll;0;L;;;;;N;;;;;
+1D80;LATIN SMALL LETTER B WITH PALATAL HOOK;Ll;0;L;;;;;N;;;;;
+1D81;LATIN SMALL LETTER D WITH PALATAL HOOK;Ll;0;L;;;;;N;;;;;
+1D82;LATIN SMALL LETTER F WITH PALATAL HOOK;Ll;0;L;;;;;N;;;;;
+1D83;LATIN SMALL LETTER G WITH PALATAL HOOK;Ll;0;L;;;;;N;;;;;
+1D84;LATIN SMALL LETTER K WITH PALATAL HOOK;Ll;0;L;;;;;N;;;;;
+1D85;LATIN SMALL LETTER L WITH PALATAL HOOK;Ll;0;L;;;;;N;;;;;
+1D86;LATIN SMALL LETTER M WITH PALATAL HOOK;Ll;0;L;;;;;N;;;;;
+1D87;LATIN SMALL LETTER N WITH PALATAL HOOK;Ll;0;L;;;;;N;;;;;
+1D88;LATIN SMALL LETTER P WITH PALATAL HOOK;Ll;0;L;;;;;N;;;;;
+1D89;LATIN SMALL LETTER R WITH PALATAL HOOK;Ll;0;L;;;;;N;;;;;
+1D8A;LATIN SMALL LETTER S WITH PALATAL HOOK;Ll;0;L;;;;;N;;;;;
+1D8B;LATIN SMALL LETTER ESH WITH PALATAL HOOK;Ll;0;L;;;;;N;;;;;
+1D8C;LATIN SMALL LETTER V WITH PALATAL HOOK;Ll;0;L;;;;;N;;;;;
+1D8D;LATIN SMALL LETTER X WITH PALATAL HOOK;Ll;0;L;;;;;N;;;;;
+1D8E;LATIN SMALL LETTER Z WITH PALATAL HOOK;Ll;0;L;;;;;N;;;;;
+1D8F;LATIN SMALL LETTER A WITH RETROFLEX HOOK;Ll;0;L;;;;;N;;;;;
+1D90;LATIN SMALL LETTER ALPHA WITH RETROFLEX HOOK;Ll;0;L;;;;;N;;;;;
+1D91;LATIN SMALL LETTER D WITH HOOK AND TAIL;Ll;0;L;;;;;N;;;;;
+1D92;LATIN SMALL LETTER E WITH RETROFLEX HOOK;Ll;0;L;;;;;N;;;;;
+1D93;LATIN SMALL LETTER OPEN E WITH RETROFLEX HOOK;Ll;0;L;;;;;N;;;;;
+1D94;LATIN SMALL LETTER REVERSED OPEN E WITH RETROFLEX HOOK;Ll;0;L;;;;;N;;;;;
+1D95;LATIN SMALL LETTER SCHWA WITH RETROFLEX HOOK;Ll;0;L;;;;;N;;;;;
+1D96;LATIN SMALL LETTER I WITH RETROFLEX HOOK;Ll;0;L;;;;;N;;;;;
+1D97;LATIN SMALL LETTER OPEN O WITH RETROFLEX HOOK;Ll;0;L;;;;;N;;;;;
+1D98;LATIN SMALL LETTER ESH WITH RETROFLEX HOOK;Ll;0;L;;;;;N;;;;;
+1D99;LATIN SMALL LETTER U WITH RETROFLEX HOOK;Ll;0;L;;;;;N;;;;;
+1D9A;LATIN SMALL LETTER EZH WITH RETROFLEX HOOK;Ll;0;L;;;;;N;;;;;
+1D9B;MODIFIER LETTER SMALL TURNED ALPHA;Lm;0;L;<super> 0252;;;;N;;;;;
+1D9C;MODIFIER LETTER SMALL C;Lm;0;L;<super> 0063;;;;N;;;;;
+1D9D;MODIFIER LETTER SMALL C WITH CURL;Lm;0;L;<super> 0255;;;;N;;;;;
+1D9E;MODIFIER LETTER SMALL ETH;Lm;0;L;<super> 00F0;;;;N;;;;;
+1D9F;MODIFIER LETTER SMALL REVERSED OPEN E;Lm;0;L;<super> 025C;;;;N;;;;;
+1DA0;MODIFIER LETTER SMALL F;Lm;0;L;<super> 0066;;;;N;;;;;
+1DA1;MODIFIER LETTER SMALL DOTLESS J WITH STROKE;Lm;0;L;<super> 025F;;;;N;;;;;
+1DA2;MODIFIER LETTER SMALL SCRIPT G;Lm;0;L;<super> 0261;;;;N;;;;;
+1DA3;MODIFIER LETTER SMALL TURNED H;Lm;0;L;<super> 0265;;;;N;;;;;
+1DA4;MODIFIER LETTER SMALL I WITH STROKE;Lm;0;L;<super> 0268;;;;N;;;;;
+1DA5;MODIFIER LETTER SMALL IOTA;Lm;0;L;<super> 0269;;;;N;;;;;
+1DA6;MODIFIER LETTER SMALL CAPITAL I;Lm;0;L;<super> 026A;;;;N;;;;;
+1DA7;MODIFIER LETTER SMALL CAPITAL I WITH STROKE;Lm;0;L;<super> 1D7B;;;;N;;;;;
+1DA8;MODIFIER LETTER SMALL J WITH CROSSED-TAIL;Lm;0;L;<super> 029D;;;;N;;;;;
+1DA9;MODIFIER LETTER SMALL L WITH RETROFLEX HOOK;Lm;0;L;<super> 026D;;;;N;;;;;
+1DAA;MODIFIER LETTER SMALL L WITH PALATAL HOOK;Lm;0;L;<super> 1D85;;;;N;;;;;
+1DAB;MODIFIER LETTER SMALL CAPITAL L;Lm;0;L;<super> 029F;;;;N;;;;;
+1DAC;MODIFIER LETTER SMALL M WITH HOOK;Lm;0;L;<super> 0271;;;;N;;;;;
+1DAD;MODIFIER LETTER SMALL TURNED M WITH LONG LEG;Lm;0;L;<super> 0270;;;;N;;;;;
+1DAE;MODIFIER LETTER SMALL N WITH LEFT HOOK;Lm;0;L;<super> 0272;;;;N;;;;;
+1DAF;MODIFIER LETTER SMALL N WITH RETROFLEX HOOK;Lm;0;L;<super> 0273;;;;N;;;;;
+1DB0;MODIFIER LETTER SMALL CAPITAL N;Lm;0;L;<super> 0274;;;;N;;;;;
+1DB1;MODIFIER LETTER SMALL BARRED O;Lm;0;L;<super> 0275;;;;N;;;;;
+1DB2;MODIFIER LETTER SMALL PHI;Lm;0;L;<super> 0278;;;;N;;;;;
+1DB3;MODIFIER LETTER SMALL S WITH HOOK;Lm;0;L;<super> 0282;;;;N;;;;;
+1DB4;MODIFIER LETTER SMALL ESH;Lm;0;L;<super> 0283;;;;N;;;;;
+1DB5;MODIFIER LETTER SMALL T WITH PALATAL HOOK;Lm;0;L;<super> 01AB;;;;N;;;;;
+1DB6;MODIFIER LETTER SMALL U BAR;Lm;0;L;<super> 0289;;;;N;;;;;
+1DB7;MODIFIER LETTER SMALL UPSILON;Lm;0;L;<super> 028A;;;;N;;;;;
+1DB8;MODIFIER LETTER SMALL CAPITAL U;Lm;0;L;<super> 1D1C;;;;N;;;;;
+1DB9;MODIFIER LETTER SMALL V WITH HOOK;Lm;0;L;<super> 028B;;;;N;;;;;
+1DBA;MODIFIER LETTER SMALL TURNED V;Lm;0;L;<super> 028C;;;;N;;;;;
+1DBB;MODIFIER LETTER SMALL Z;Lm;0;L;<super> 007A;;;;N;;;;;
+1DBC;MODIFIER LETTER SMALL Z WITH RETROFLEX HOOK;Lm;0;L;<super> 0290;;;;N;;;;;
+1DBD;MODIFIER LETTER SMALL Z WITH CURL;Lm;0;L;<super> 0291;;;;N;;;;;
+1DBE;MODIFIER LETTER SMALL EZH;Lm;0;L;<super> 0292;;;;N;;;;;
+1DBF;MODIFIER LETTER SMALL THETA;Lm;0;L;<super> 03B8;;;;N;;;;;
+1DC0;COMBINING DOTTED GRAVE ACCENT;Mn;230;NSM;;;;;N;;;;;
+1DC1;COMBINING DOTTED ACUTE ACCENT;Mn;230;NSM;;;;;N;;;;;
+1DC2;COMBINING SNAKE BELOW;Mn;220;NSM;;;;;N;;;;;
+1DC3;COMBINING SUSPENSION MARK;Mn;230;NSM;;;;;N;;;;;
+1DC4;COMBINING MACRON-ACUTE;Mn;230;NSM;;;;;N;;;;;
+1DC5;COMBINING GRAVE-MACRON;Mn;230;NSM;;;;;N;;;;;
+1DC6;COMBINING MACRON-GRAVE;Mn;230;NSM;;;;;N;;;;;
+1DC7;COMBINING ACUTE-MACRON;Mn;230;NSM;;;;;N;;;;;
+1DC8;COMBINING GRAVE-ACUTE-GRAVE;Mn;230;NSM;;;;;N;;;;;
+1DC9;COMBINING ACUTE-GRAVE-ACUTE;Mn;230;NSM;;;;;N;;;;;
+1DCA;COMBINING LATIN SMALL LETTER R BELOW;Mn;220;NSM;;;;;N;;;;;
+1DCB;COMBINING BREVE-MACRON;Mn;230;NSM;;;;;N;;;;;
+1DCC;COMBINING MACRON-BREVE;Mn;230;NSM;;;;;N;;;;;
+1DCD;COMBINING DOUBLE CIRCUMFLEX ABOVE;Mn;234;NSM;;;;;N;;;;;
+1DCE;COMBINING OGONEK ABOVE;Mn;214;NSM;;;;;N;;;;;
+1DCF;COMBINING ZIGZAG BELOW;Mn;220;NSM;;;;;N;;;;;
+1DD0;COMBINING IS BELOW;Mn;202;NSM;;;;;N;;;;;
+1DD1;COMBINING UR ABOVE;Mn;230;NSM;;;;;N;;;;;
+1DD2;COMBINING US ABOVE;Mn;230;NSM;;;;;N;;;;;
+1DD3;COMBINING LATIN SMALL LETTER FLATTENED OPEN A ABOVE;Mn;230;NSM;;;;;N;;;;;
+1DD4;COMBINING LATIN SMALL LETTER AE;Mn;230;NSM;;;;;N;;;;;
+1DD5;COMBINING LATIN SMALL LETTER AO;Mn;230;NSM;;;;;N;;;;;
+1DD6;COMBINING LATIN SMALL LETTER AV;Mn;230;NSM;;;;;N;;;;;
+1DD7;COMBINING LATIN SMALL LETTER C CEDILLA;Mn;230;NSM;;;;;N;;;;;
+1DD8;COMBINING LATIN SMALL LETTER INSULAR D;Mn;230;NSM;;;;;N;;;;;
+1DD9;COMBINING LATIN SMALL LETTER ETH;Mn;230;NSM;;;;;N;;;;;
+1DDA;COMBINING LATIN SMALL LETTER G;Mn;230;NSM;;;;;N;;;;;
+1DDB;COMBINING LATIN LETTER SMALL CAPITAL G;Mn;230;NSM;;;;;N;;;;;
+1DDC;COMBINING LATIN SMALL LETTER K;Mn;230;NSM;;;;;N;;;;;
+1DDD;COMBINING LATIN SMALL LETTER L;Mn;230;NSM;;;;;N;;;;;
+1DDE;COMBINING LATIN LETTER SMALL CAPITAL L;Mn;230;NSM;;;;;N;;;;;
+1DDF;COMBINING LATIN LETTER SMALL CAPITAL M;Mn;230;NSM;;;;;N;;;;;
+1DE0;COMBINING LATIN SMALL LETTER N;Mn;230;NSM;;;;;N;;;;;
+1DE1;COMBINING LATIN LETTER SMALL CAPITAL N;Mn;230;NSM;;;;;N;;;;;
+1DE2;COMBINING LATIN LETTER SMALL CAPITAL R;Mn;230;NSM;;;;;N;;;;;
+1DE3;COMBINING LATIN SMALL LETTER R ROTUNDA;Mn;230;NSM;;;;;N;;;;;
+1DE4;COMBINING LATIN SMALL LETTER S;Mn;230;NSM;;;;;N;;;;;
+1DE5;COMBINING LATIN SMALL LETTER LONG S;Mn;230;NSM;;;;;N;;;;;
+1DE6;COMBINING LATIN SMALL LETTER Z;Mn;230;NSM;;;;;N;;;;;
+1DE7;COMBINING LATIN SMALL LETTER ALPHA;Mn;230;NSM;;;;;N;;;;;
+1DE8;COMBINING LATIN SMALL LETTER B;Mn;230;NSM;;;;;N;;;;;
+1DE9;COMBINING LATIN SMALL LETTER BETA;Mn;230;NSM;;;;;N;;;;;
+1DEA;COMBINING LATIN SMALL LETTER SCHWA;Mn;230;NSM;;;;;N;;;;;
+1DEB;COMBINING LATIN SMALL LETTER F;Mn;230;NSM;;;;;N;;;;;
+1DEC;COMBINING LATIN SMALL LETTER L WITH DOUBLE MIDDLE TILDE;Mn;230;NSM;;;;;N;;;;;
+1DED;COMBINING LATIN SMALL LETTER O WITH LIGHT CENTRALIZATION STROKE;Mn;230;NSM;;;;;N;;;;;
+1DEE;COMBINING LATIN SMALL LETTER P;Mn;230;NSM;;;;;N;;;;;
+1DEF;COMBINING LATIN SMALL LETTER ESH;Mn;230;NSM;;;;;N;;;;;
+1DF0;COMBINING LATIN SMALL LETTER U WITH LIGHT CENTRALIZATION STROKE;Mn;230;NSM;;;;;N;;;;;
+1DF1;COMBINING LATIN SMALL LETTER W;Mn;230;NSM;;;;;N;;;;;
+1DF2;COMBINING LATIN SMALL LETTER A WITH DIAERESIS;Mn;230;NSM;;;;;N;;;;;
+1DF3;COMBINING LATIN SMALL LETTER O WITH DIAERESIS;Mn;230;NSM;;;;;N;;;;;
+1DF4;COMBINING LATIN SMALL LETTER U WITH DIAERESIS;Mn;230;NSM;;;;;N;;;;;
+1DF5;COMBINING UP TACK ABOVE;Mn;230;NSM;;;;;N;;;;;
+1DFB;COMBINING DELETION MARK;Mn;230;NSM;;;;;N;;;;;
+1DFC;COMBINING DOUBLE INVERTED BREVE BELOW;Mn;233;NSM;;;;;N;;;;;
+1DFD;COMBINING ALMOST EQUAL TO BELOW;Mn;220;NSM;;;;;N;;;;;
+1DFE;COMBINING LEFT ARROWHEAD ABOVE;Mn;230;NSM;;;;;N;;;;;
+1DFF;COMBINING RIGHT ARROWHEAD AND DOWN ARROWHEAD BELOW;Mn;220;NSM;;;;;N;;;;;
+1E00;LATIN CAPITAL LETTER A WITH RING BELOW;Lu;0;L;0041 0325;;;;N;;;;1E01;
+1E01;LATIN SMALL LETTER A WITH RING BELOW;Ll;0;L;0061 0325;;;;N;;;1E00;;1E00
+1E02;LATIN CAPITAL LETTER B WITH DOT ABOVE;Lu;0;L;0042 0307;;;;N;;;;1E03;
+1E03;LATIN SMALL LETTER B WITH DOT ABOVE;Ll;0;L;0062 0307;;;;N;;;1E02;;1E02
+1E04;LATIN CAPITAL LETTER B WITH DOT BELOW;Lu;0;L;0042 0323;;;;N;;;;1E05;
+1E05;LATIN SMALL LETTER B WITH DOT BELOW;Ll;0;L;0062 0323;;;;N;;;1E04;;1E04
+1E06;LATIN CAPITAL LETTER B WITH LINE BELOW;Lu;0;L;0042 0331;;;;N;;;;1E07;
+1E07;LATIN SMALL LETTER B WITH LINE BELOW;Ll;0;L;0062 0331;;;;N;;;1E06;;1E06
+1E08;LATIN CAPITAL LETTER C WITH CEDILLA AND ACUTE;Lu;0;L;00C7 0301;;;;N;;;;1E09;
+1E09;LATIN SMALL LETTER C WITH CEDILLA AND ACUTE;Ll;0;L;00E7 0301;;;;N;;;1E08;;1E08
+1E0A;LATIN CAPITAL LETTER D WITH DOT ABOVE;Lu;0;L;0044 0307;;;;N;;;;1E0B;
+1E0B;LATIN SMALL LETTER D WITH DOT ABOVE;Ll;0;L;0064 0307;;;;N;;;1E0A;;1E0A
+1E0C;LATIN CAPITAL LETTER D WITH DOT BELOW;Lu;0;L;0044 0323;;;;N;;;;1E0D;
+1E0D;LATIN SMALL LETTER D WITH DOT BELOW;Ll;0;L;0064 0323;;;;N;;;1E0C;;1E0C
+1E0E;LATIN CAPITAL LETTER D WITH LINE BELOW;Lu;0;L;0044 0331;;;;N;;;;1E0F;
+1E0F;LATIN SMALL LETTER D WITH LINE BELOW;Ll;0;L;0064 0331;;;;N;;;1E0E;;1E0E
+1E10;LATIN CAPITAL LETTER D WITH CEDILLA;Lu;0;L;0044 0327;;;;N;;;;1E11;
+1E11;LATIN SMALL LETTER D WITH CEDILLA;Ll;0;L;0064 0327;;;;N;;;1E10;;1E10
+1E12;LATIN CAPITAL LETTER D WITH CIRCUMFLEX BELOW;Lu;0;L;0044 032D;;;;N;;;;1E13;
+1E13;LATIN SMALL LETTER D WITH CIRCUMFLEX BELOW;Ll;0;L;0064 032D;;;;N;;;1E12;;1E12
+1E14;LATIN CAPITAL LETTER E WITH MACRON AND GRAVE;Lu;0;L;0112 0300;;;;N;;;;1E15;
+1E15;LATIN SMALL LETTER E WITH MACRON AND GRAVE;Ll;0;L;0113 0300;;;;N;;;1E14;;1E14
+1E16;LATIN CAPITAL LETTER E WITH MACRON AND ACUTE;Lu;0;L;0112 0301;;;;N;;;;1E17;
+1E17;LATIN SMALL LETTER E WITH MACRON AND ACUTE;Ll;0;L;0113 0301;;;;N;;;1E16;;1E16
+1E18;LATIN CAPITAL LETTER E WITH CIRCUMFLEX BELOW;Lu;0;L;0045 032D;;;;N;;;;1E19;
+1E19;LATIN SMALL LETTER E WITH CIRCUMFLEX BELOW;Ll;0;L;0065 032D;;;;N;;;1E18;;1E18
+1E1A;LATIN CAPITAL LETTER E WITH TILDE BELOW;Lu;0;L;0045 0330;;;;N;;;;1E1B;
+1E1B;LATIN SMALL LETTER E WITH TILDE BELOW;Ll;0;L;0065 0330;;;;N;;;1E1A;;1E1A
+1E1C;LATIN CAPITAL LETTER E WITH CEDILLA AND BREVE;Lu;0;L;0228 0306;;;;N;;;;1E1D;
+1E1D;LATIN SMALL LETTER E WITH CEDILLA AND BREVE;Ll;0;L;0229 0306;;;;N;;;1E1C;;1E1C
+1E1E;LATIN CAPITAL LETTER F WITH DOT ABOVE;Lu;0;L;0046 0307;;;;N;;;;1E1F;
+1E1F;LATIN SMALL LETTER F WITH DOT ABOVE;Ll;0;L;0066 0307;;;;N;;;1E1E;;1E1E
+1E20;LATIN CAPITAL LETTER G WITH MACRON;Lu;0;L;0047 0304;;;;N;;;;1E21;
+1E21;LATIN SMALL LETTER G WITH MACRON;Ll;0;L;0067 0304;;;;N;;;1E20;;1E20
+1E22;LATIN CAPITAL LETTER H WITH DOT ABOVE;Lu;0;L;0048 0307;;;;N;;;;1E23;
+1E23;LATIN SMALL LETTER H WITH DOT ABOVE;Ll;0;L;0068 0307;;;;N;;;1E22;;1E22
+1E24;LATIN CAPITAL LETTER H WITH DOT BELOW;Lu;0;L;0048 0323;;;;N;;;;1E25;
+1E25;LATIN SMALL LETTER H WITH DOT BELOW;Ll;0;L;0068 0323;;;;N;;;1E24;;1E24
+1E26;LATIN CAPITAL LETTER H WITH DIAERESIS;Lu;0;L;0048 0308;;;;N;;;;1E27;
+1E27;LATIN SMALL LETTER H WITH DIAERESIS;Ll;0;L;0068 0308;;;;N;;;1E26;;1E26
+1E28;LATIN CAPITAL LETTER H WITH CEDILLA;Lu;0;L;0048 0327;;;;N;;;;1E29;
+1E29;LATIN SMALL LETTER H WITH CEDILLA;Ll;0;L;0068 0327;;;;N;;;1E28;;1E28
+1E2A;LATIN CAPITAL LETTER H WITH BREVE BELOW;Lu;0;L;0048 032E;;;;N;;;;1E2B;
+1E2B;LATIN SMALL LETTER H WITH BREVE BELOW;Ll;0;L;0068 032E;;;;N;;;1E2A;;1E2A
+1E2C;LATIN CAPITAL LETTER I WITH TILDE BELOW;Lu;0;L;0049 0330;;;;N;;;;1E2D;
+1E2D;LATIN SMALL LETTER I WITH TILDE BELOW;Ll;0;L;0069 0330;;;;N;;;1E2C;;1E2C
+1E2E;LATIN CAPITAL LETTER I WITH DIAERESIS AND ACUTE;Lu;0;L;00CF 0301;;;;N;;;;1E2F;
+1E2F;LATIN SMALL LETTER I WITH DIAERESIS AND ACUTE;Ll;0;L;00EF 0301;;;;N;;;1E2E;;1E2E
+1E30;LATIN CAPITAL LETTER K WITH ACUTE;Lu;0;L;004B 0301;;;;N;;;;1E31;
+1E31;LATIN SMALL LETTER K WITH ACUTE;Ll;0;L;006B 0301;;;;N;;;1E30;;1E30
+1E32;LATIN CAPITAL LETTER K WITH DOT BELOW;Lu;0;L;004B 0323;;;;N;;;;1E33;
+1E33;LATIN SMALL LETTER K WITH DOT BELOW;Ll;0;L;006B 0323;;;;N;;;1E32;;1E32
+1E34;LATIN CAPITAL LETTER K WITH LINE BELOW;Lu;0;L;004B 0331;;;;N;;;;1E35;
+1E35;LATIN SMALL LETTER K WITH LINE BELOW;Ll;0;L;006B 0331;;;;N;;;1E34;;1E34
+1E36;LATIN CAPITAL LETTER L WITH DOT BELOW;Lu;0;L;004C 0323;;;;N;;;;1E37;
+1E37;LATIN SMALL LETTER L WITH DOT BELOW;Ll;0;L;006C 0323;;;;N;;;1E36;;1E36
+1E38;LATIN CAPITAL LETTER L WITH DOT BELOW AND MACRON;Lu;0;L;1E36 0304;;;;N;;;;1E39;
+1E39;LATIN SMALL LETTER L WITH DOT BELOW AND MACRON;Ll;0;L;1E37 0304;;;;N;;;1E38;;1E38
+1E3A;LATIN CAPITAL LETTER L WITH LINE BELOW;Lu;0;L;004C 0331;;;;N;;;;1E3B;
+1E3B;LATIN SMALL LETTER L WITH LINE BELOW;Ll;0;L;006C 0331;;;;N;;;1E3A;;1E3A
+1E3C;LATIN CAPITAL LETTER L WITH CIRCUMFLEX BELOW;Lu;0;L;004C 032D;;;;N;;;;1E3D;
+1E3D;LATIN SMALL LETTER L WITH CIRCUMFLEX BELOW;Ll;0;L;006C 032D;;;;N;;;1E3C;;1E3C
+1E3E;LATIN CAPITAL LETTER M WITH ACUTE;Lu;0;L;004D 0301;;;;N;;;;1E3F;
+1E3F;LATIN SMALL LETTER M WITH ACUTE;Ll;0;L;006D 0301;;;;N;;;1E3E;;1E3E
+1E40;LATIN CAPITAL LETTER M WITH DOT ABOVE;Lu;0;L;004D 0307;;;;N;;;;1E41;
+1E41;LATIN SMALL LETTER M WITH DOT ABOVE;Ll;0;L;006D 0307;;;;N;;;1E40;;1E40
+1E42;LATIN CAPITAL LETTER M WITH DOT BELOW;Lu;0;L;004D 0323;;;;N;;;;1E43;
+1E43;LATIN SMALL LETTER M WITH DOT BELOW;Ll;0;L;006D 0323;;;;N;;;1E42;;1E42
+1E44;LATIN CAPITAL LETTER N WITH DOT ABOVE;Lu;0;L;004E 0307;;;;N;;;;1E45;
+1E45;LATIN SMALL LETTER N WITH DOT ABOVE;Ll;0;L;006E 0307;;;;N;;;1E44;;1E44
+1E46;LATIN CAPITAL LETTER N WITH DOT BELOW;Lu;0;L;004E 0323;;;;N;;;;1E47;
+1E47;LATIN SMALL LETTER N WITH DOT BELOW;Ll;0;L;006E 0323;;;;N;;;1E46;;1E46
+1E48;LATIN CAPITAL LETTER N WITH LINE BELOW;Lu;0;L;004E 0331;;;;N;;;;1E49;
+1E49;LATIN SMALL LETTER N WITH LINE BELOW;Ll;0;L;006E 0331;;;;N;;;1E48;;1E48
+1E4A;LATIN CAPITAL LETTER N WITH CIRCUMFLEX BELOW;Lu;0;L;004E 032D;;;;N;;;;1E4B;
+1E4B;LATIN SMALL LETTER N WITH CIRCUMFLEX BELOW;Ll;0;L;006E 032D;;;;N;;;1E4A;;1E4A
+1E4C;LATIN CAPITAL LETTER O WITH TILDE AND ACUTE;Lu;0;L;00D5 0301;;;;N;;;;1E4D;
+1E4D;LATIN SMALL LETTER O WITH TILDE AND ACUTE;Ll;0;L;00F5 0301;;;;N;;;1E4C;;1E4C
+1E4E;LATIN CAPITAL LETTER O WITH TILDE AND DIAERESIS;Lu;0;L;00D5 0308;;;;N;;;;1E4F;
+1E4F;LATIN SMALL LETTER O WITH TILDE AND DIAERESIS;Ll;0;L;00F5 0308;;;;N;;;1E4E;;1E4E
+1E50;LATIN CAPITAL LETTER O WITH MACRON AND GRAVE;Lu;0;L;014C 0300;;;;N;;;;1E51;
+1E51;LATIN SMALL LETTER O WITH MACRON AND GRAVE;Ll;0;L;014D 0300;;;;N;;;1E50;;1E50
+1E52;LATIN CAPITAL LETTER O WITH MACRON AND ACUTE;Lu;0;L;014C 0301;;;;N;;;;1E53;
+1E53;LATIN SMALL LETTER O WITH MACRON AND ACUTE;Ll;0;L;014D 0301;;;;N;;;1E52;;1E52
+1E54;LATIN CAPITAL LETTER P WITH ACUTE;Lu;0;L;0050 0301;;;;N;;;;1E55;
+1E55;LATIN SMALL LETTER P WITH ACUTE;Ll;0;L;0070 0301;;;;N;;;1E54;;1E54
+1E56;LATIN CAPITAL LETTER P WITH DOT ABOVE;Lu;0;L;0050 0307;;;;N;;;;1E57;
+1E57;LATIN SMALL LETTER P WITH DOT ABOVE;Ll;0;L;0070 0307;;;;N;;;1E56;;1E56
+1E58;LATIN CAPITAL LETTER R WITH DOT ABOVE;Lu;0;L;0052 0307;;;;N;;;;1E59;
+1E59;LATIN SMALL LETTER R WITH DOT ABOVE;Ll;0;L;0072 0307;;;;N;;;1E58;;1E58
+1E5A;LATIN CAPITAL LETTER R WITH DOT BELOW;Lu;0;L;0052 0323;;;;N;;;;1E5B;
+1E5B;LATIN SMALL LETTER R WITH DOT BELOW;Ll;0;L;0072 0323;;;;N;;;1E5A;;1E5A
+1E5C;LATIN CAPITAL LETTER R WITH DOT BELOW AND MACRON;Lu;0;L;1E5A 0304;;;;N;;;;1E5D;
+1E5D;LATIN SMALL LETTER R WITH DOT BELOW AND MACRON;Ll;0;L;1E5B 0304;;;;N;;;1E5C;;1E5C
+1E5E;LATIN CAPITAL LETTER R WITH LINE BELOW;Lu;0;L;0052 0331;;;;N;;;;1E5F;
+1E5F;LATIN SMALL LETTER R WITH LINE BELOW;Ll;0;L;0072 0331;;;;N;;;1E5E;;1E5E
+1E60;LATIN CAPITAL LETTER S WITH DOT ABOVE;Lu;0;L;0053 0307;;;;N;;;;1E61;
+1E61;LATIN SMALL LETTER S WITH DOT ABOVE;Ll;0;L;0073 0307;;;;N;;;1E60;;1E60
+1E62;LATIN CAPITAL LETTER S WITH DOT BELOW;Lu;0;L;0053 0323;;;;N;;;;1E63;
+1E63;LATIN SMALL LETTER S WITH DOT BELOW;Ll;0;L;0073 0323;;;;N;;;1E62;;1E62
+1E64;LATIN CAPITAL LETTER S WITH ACUTE AND DOT ABOVE;Lu;0;L;015A 0307;;;;N;;;;1E65;
+1E65;LATIN SMALL LETTER S WITH ACUTE AND DOT ABOVE;Ll;0;L;015B 0307;;;;N;;;1E64;;1E64
+1E66;LATIN CAPITAL LETTER S WITH CARON AND DOT ABOVE;Lu;0;L;0160 0307;;;;N;;;;1E67;
+1E67;LATIN SMALL LETTER S WITH CARON AND DOT ABOVE;Ll;0;L;0161 0307;;;;N;;;1E66;;1E66
+1E68;LATIN CAPITAL LETTER S WITH DOT BELOW AND DOT ABOVE;Lu;0;L;1E62 0307;;;;N;;;;1E69;
+1E69;LATIN SMALL LETTER S WITH DOT BELOW AND DOT ABOVE;Ll;0;L;1E63 0307;;;;N;;;1E68;;1E68
+1E6A;LATIN CAPITAL LETTER T WITH DOT ABOVE;Lu;0;L;0054 0307;;;;N;;;;1E6B;
+1E6B;LATIN SMALL LETTER T WITH DOT ABOVE;Ll;0;L;0074 0307;;;;N;;;1E6A;;1E6A
+1E6C;LATIN CAPITAL LETTER T WITH DOT BELOW;Lu;0;L;0054 0323;;;;N;;;;1E6D;
+1E6D;LATIN SMALL LETTER T WITH DOT BELOW;Ll;0;L;0074 0323;;;;N;;;1E6C;;1E6C
+1E6E;LATIN CAPITAL LETTER T WITH LINE BELOW;Lu;0;L;0054 0331;;;;N;;;;1E6F;
+1E6F;LATIN SMALL LETTER T WITH LINE BELOW;Ll;0;L;0074 0331;;;;N;;;1E6E;;1E6E
+1E70;LATIN CAPITAL LETTER T WITH CIRCUMFLEX BELOW;Lu;0;L;0054 032D;;;;N;;;;1E71;
+1E71;LATIN SMALL LETTER T WITH CIRCUMFLEX BELOW;Ll;0;L;0074 032D;;;;N;;;1E70;;1E70
+1E72;LATIN CAPITAL LETTER U WITH DIAERESIS BELOW;Lu;0;L;0055 0324;;;;N;;;;1E73;
+1E73;LATIN SMALL LETTER U WITH DIAERESIS BELOW;Ll;0;L;0075 0324;;;;N;;;1E72;;1E72
+1E74;LATIN CAPITAL LETTER U WITH TILDE BELOW;Lu;0;L;0055 0330;;;;N;;;;1E75;
+1E75;LATIN SMALL LETTER U WITH TILDE BELOW;Ll;0;L;0075 0330;;;;N;;;1E74;;1E74
+1E76;LATIN CAPITAL LETTER U WITH CIRCUMFLEX BELOW;Lu;0;L;0055 032D;;;;N;;;;1E77;
+1E77;LATIN SMALL LETTER U WITH CIRCUMFLEX BELOW;Ll;0;L;0075 032D;;;;N;;;1E76;;1E76
+1E78;LATIN CAPITAL LETTER U WITH TILDE AND ACUTE;Lu;0;L;0168 0301;;;;N;;;;1E79;
+1E79;LATIN SMALL LETTER U WITH TILDE AND ACUTE;Ll;0;L;0169 0301;;;;N;;;1E78;;1E78
+1E7A;LATIN CAPITAL LETTER U WITH MACRON AND DIAERESIS;Lu;0;L;016A 0308;;;;N;;;;1E7B;
+1E7B;LATIN SMALL LETTER U WITH MACRON AND DIAERESIS;Ll;0;L;016B 0308;;;;N;;;1E7A;;1E7A
+1E7C;LATIN CAPITAL LETTER V WITH TILDE;Lu;0;L;0056 0303;;;;N;;;;1E7D;
+1E7D;LATIN SMALL LETTER V WITH TILDE;Ll;0;L;0076 0303;;;;N;;;1E7C;;1E7C
+1E7E;LATIN CAPITAL LETTER V WITH DOT BELOW;Lu;0;L;0056 0323;;;;N;;;;1E7F;
+1E7F;LATIN SMALL LETTER V WITH DOT BELOW;Ll;0;L;0076 0323;;;;N;;;1E7E;;1E7E
+1E80;LATIN CAPITAL LETTER W WITH GRAVE;Lu;0;L;0057 0300;;;;N;;;;1E81;
+1E81;LATIN SMALL LETTER W WITH GRAVE;Ll;0;L;0077 0300;;;;N;;;1E80;;1E80
+1E82;LATIN CAPITAL LETTER W WITH ACUTE;Lu;0;L;0057 0301;;;;N;;;;1E83;
+1E83;LATIN SMALL LETTER W WITH ACUTE;Ll;0;L;0077 0301;;;;N;;;1E82;;1E82
+1E84;LATIN CAPITAL LETTER W WITH DIAERESIS;Lu;0;L;0057 0308;;;;N;;;;1E85;
+1E85;LATIN SMALL LETTER W WITH DIAERESIS;Ll;0;L;0077 0308;;;;N;;;1E84;;1E84
+1E86;LATIN CAPITAL LETTER W WITH DOT ABOVE;Lu;0;L;0057 0307;;;;N;;;;1E87;
+1E87;LATIN SMALL LETTER W WITH DOT ABOVE;Ll;0;L;0077 0307;;;;N;;;1E86;;1E86
+1E88;LATIN CAPITAL LETTER W WITH DOT BELOW;Lu;0;L;0057 0323;;;;N;;;;1E89;
+1E89;LATIN SMALL LETTER W WITH DOT BELOW;Ll;0;L;0077 0323;;;;N;;;1E88;;1E88
+1E8A;LATIN CAPITAL LETTER X WITH DOT ABOVE;Lu;0;L;0058 0307;;;;N;;;;1E8B;
+1E8B;LATIN SMALL LETTER X WITH DOT ABOVE;Ll;0;L;0078 0307;;;;N;;;1E8A;;1E8A
+1E8C;LATIN CAPITAL LETTER X WITH DIAERESIS;Lu;0;L;0058 0308;;;;N;;;;1E8D;
+1E8D;LATIN SMALL LETTER X WITH DIAERESIS;Ll;0;L;0078 0308;;;;N;;;1E8C;;1E8C
+1E8E;LATIN CAPITAL LETTER Y WITH DOT ABOVE;Lu;0;L;0059 0307;;;;N;;;;1E8F;
+1E8F;LATIN SMALL LETTER Y WITH DOT ABOVE;Ll;0;L;0079 0307;;;;N;;;1E8E;;1E8E
+1E90;LATIN CAPITAL LETTER Z WITH CIRCUMFLEX;Lu;0;L;005A 0302;;;;N;;;;1E91;
+1E91;LATIN SMALL LETTER Z WITH CIRCUMFLEX;Ll;0;L;007A 0302;;;;N;;;1E90;;1E90
+1E92;LATIN CAPITAL LETTER Z WITH DOT BELOW;Lu;0;L;005A 0323;;;;N;;;;1E93;
+1E93;LATIN SMALL LETTER Z WITH DOT BELOW;Ll;0;L;007A 0323;;;;N;;;1E92;;1E92
+1E94;LATIN CAPITAL LETTER Z WITH LINE BELOW;Lu;0;L;005A 0331;;;;N;;;;1E95;
+1E95;LATIN SMALL LETTER Z WITH LINE BELOW;Ll;0;L;007A 0331;;;;N;;;1E94;;1E94
+1E96;LATIN SMALL LETTER H WITH LINE BELOW;Ll;0;L;0068 0331;;;;N;;;;;
+1E97;LATIN SMALL LETTER T WITH DIAERESIS;Ll;0;L;0074 0308;;;;N;;;;;
+1E98;LATIN SMALL LETTER W WITH RING ABOVE;Ll;0;L;0077 030A;;;;N;;;;;
+1E99;LATIN SMALL LETTER Y WITH RING ABOVE;Ll;0;L;0079 030A;;;;N;;;;;
+1E9A;LATIN SMALL LETTER A WITH RIGHT HALF RING;Ll;0;L;<compat> 0061 02BE;;;;N;;;;;
+1E9B;LATIN SMALL LETTER LONG S WITH DOT ABOVE;Ll;0;L;017F 0307;;;;N;;;1E60;;1E60
+1E9C;LATIN SMALL LETTER LONG S WITH DIAGONAL STROKE;Ll;0;L;;;;;N;;;;;
+1E9D;LATIN SMALL LETTER LONG S WITH HIGH STROKE;Ll;0;L;;;;;N;;;;;
+1E9E;LATIN CAPITAL LETTER SHARP S;Lu;0;L;;;;;N;;;;00DF;
+1E9F;LATIN SMALL LETTER DELTA;Ll;0;L;;;;;N;;;;;
+1EA0;LATIN CAPITAL LETTER A WITH DOT BELOW;Lu;0;L;0041 0323;;;;N;;;;1EA1;
+1EA1;LATIN SMALL LETTER A WITH DOT BELOW;Ll;0;L;0061 0323;;;;N;;;1EA0;;1EA0
+1EA2;LATIN CAPITAL LETTER A WITH HOOK ABOVE;Lu;0;L;0041 0309;;;;N;;;;1EA3;
+1EA3;LATIN SMALL LETTER A WITH HOOK ABOVE;Ll;0;L;0061 0309;;;;N;;;1EA2;;1EA2
+1EA4;LATIN CAPITAL LETTER A WITH CIRCUMFLEX AND ACUTE;Lu;0;L;00C2 0301;;;;N;;;;1EA5;
+1EA5;LATIN SMALL LETTER A WITH CIRCUMFLEX AND ACUTE;Ll;0;L;00E2 0301;;;;N;;;1EA4;;1EA4
+1EA6;LATIN CAPITAL LETTER A WITH CIRCUMFLEX AND GRAVE;Lu;0;L;00C2 0300;;;;N;;;;1EA7;
+1EA7;LATIN SMALL LETTER A WITH CIRCUMFLEX AND GRAVE;Ll;0;L;00E2 0300;;;;N;;;1EA6;;1EA6
+1EA8;LATIN CAPITAL LETTER A WITH CIRCUMFLEX AND HOOK ABOVE;Lu;0;L;00C2 0309;;;;N;;;;1EA9;
+1EA9;LATIN SMALL LETTER A WITH CIRCUMFLEX AND HOOK ABOVE;Ll;0;L;00E2 0309;;;;N;;;1EA8;;1EA8
+1EAA;LATIN CAPITAL LETTER A WITH CIRCUMFLEX AND TILDE;Lu;0;L;00C2 0303;;;;N;;;;1EAB;
+1EAB;LATIN SMALL LETTER A WITH CIRCUMFLEX AND TILDE;Ll;0;L;00E2 0303;;;;N;;;1EAA;;1EAA
+1EAC;LATIN CAPITAL LETTER A WITH CIRCUMFLEX AND DOT BELOW;Lu;0;L;1EA0 0302;;;;N;;;;1EAD;
+1EAD;LATIN SMALL LETTER A WITH CIRCUMFLEX AND DOT BELOW;Ll;0;L;1EA1 0302;;;;N;;;1EAC;;1EAC
+1EAE;LATIN CAPITAL LETTER A WITH BREVE AND ACUTE;Lu;0;L;0102 0301;;;;N;;;;1EAF;
+1EAF;LATIN SMALL LETTER A WITH BREVE AND ACUTE;Ll;0;L;0103 0301;;;;N;;;1EAE;;1EAE
+1EB0;LATIN CAPITAL LETTER A WITH BREVE AND GRAVE;Lu;0;L;0102 0300;;;;N;;;;1EB1;
+1EB1;LATIN SMALL LETTER A WITH BREVE AND GRAVE;Ll;0;L;0103 0300;;;;N;;;1EB0;;1EB0
+1EB2;LATIN CAPITAL LETTER A WITH BREVE AND HOOK ABOVE;Lu;0;L;0102 0309;;;;N;;;;1EB3;
+1EB3;LATIN SMALL LETTER A WITH BREVE AND HOOK ABOVE;Ll;0;L;0103 0309;;;;N;;;1EB2;;1EB2
+1EB4;LATIN CAPITAL LETTER A WITH BREVE AND TILDE;Lu;0;L;0102 0303;;;;N;;;;1EB5;
+1EB5;LATIN SMALL LETTER A WITH BREVE AND TILDE;Ll;0;L;0103 0303;;;;N;;;1EB4;;1EB4
+1EB6;LATIN CAPITAL LETTER A WITH BREVE AND DOT BELOW;Lu;0;L;1EA0 0306;;;;N;;;;1EB7;
+1EB7;LATIN SMALL LETTER A WITH BREVE AND DOT BELOW;Ll;0;L;1EA1 0306;;;;N;;;1EB6;;1EB6
+1EB8;LATIN CAPITAL LETTER E WITH DOT BELOW;Lu;0;L;0045 0323;;;;N;;;;1EB9;
+1EB9;LATIN SMALL LETTER E WITH DOT BELOW;Ll;0;L;0065 0323;;;;N;;;1EB8;;1EB8
+1EBA;LATIN CAPITAL LETTER E WITH HOOK ABOVE;Lu;0;L;0045 0309;;;;N;;;;1EBB;
+1EBB;LATIN SMALL LETTER E WITH HOOK ABOVE;Ll;0;L;0065 0309;;;;N;;;1EBA;;1EBA
+1EBC;LATIN CAPITAL LETTER E WITH TILDE;Lu;0;L;0045 0303;;;;N;;;;1EBD;
+1EBD;LATIN SMALL LETTER E WITH TILDE;Ll;0;L;0065 0303;;;;N;;;1EBC;;1EBC
+1EBE;LATIN CAPITAL LETTER E WITH CIRCUMFLEX AND ACUTE;Lu;0;L;00CA 0301;;;;N;;;;1EBF;
+1EBF;LATIN SMALL LETTER E WITH CIRCUMFLEX AND ACUTE;Ll;0;L;00EA 0301;;;;N;;;1EBE;;1EBE
+1EC0;LATIN CAPITAL LETTER E WITH CIRCUMFLEX AND GRAVE;Lu;0;L;00CA 0300;;;;N;;;;1EC1;
+1EC1;LATIN SMALL LETTER E WITH CIRCUMFLEX AND GRAVE;Ll;0;L;00EA 0300;;;;N;;;1EC0;;1EC0
+1EC2;LATIN CAPITAL LETTER E WITH CIRCUMFLEX AND HOOK ABOVE;Lu;0;L;00CA 0309;;;;N;;;;1EC3;
+1EC3;LATIN SMALL LETTER E WITH CIRCUMFLEX AND HOOK ABOVE;Ll;0;L;00EA 0309;;;;N;;;1EC2;;1EC2
+1EC4;LATIN CAPITAL LETTER E WITH CIRCUMFLEX AND TILDE;Lu;0;L;00CA 0303;;;;N;;;;1EC5;
+1EC5;LATIN SMALL LETTER E WITH CIRCUMFLEX AND TILDE;Ll;0;L;00EA 0303;;;;N;;;1EC4;;1EC4
+1EC6;LATIN CAPITAL LETTER E WITH CIRCUMFLEX AND DOT BELOW;Lu;0;L;1EB8 0302;;;;N;;;;1EC7;
+1EC7;LATIN SMALL LETTER E WITH CIRCUMFLEX AND DOT BELOW;Ll;0;L;1EB9 0302;;;;N;;;1EC6;;1EC6
+1EC8;LATIN CAPITAL LETTER I WITH HOOK ABOVE;Lu;0;L;0049 0309;;;;N;;;;1EC9;
+1EC9;LATIN SMALL LETTER I WITH HOOK ABOVE;Ll;0;L;0069 0309;;;;N;;;1EC8;;1EC8
+1ECA;LATIN CAPITAL LETTER I WITH DOT BELOW;Lu;0;L;0049 0323;;;;N;;;;1ECB;
+1ECB;LATIN SMALL LETTER I WITH DOT BELOW;Ll;0;L;0069 0323;;;;N;;;1ECA;;1ECA
+1ECC;LATIN CAPITAL LETTER O WITH DOT BELOW;Lu;0;L;004F 0323;;;;N;;;;1ECD;
+1ECD;LATIN SMALL LETTER O WITH DOT BELOW;Ll;0;L;006F 0323;;;;N;;;1ECC;;1ECC
+1ECE;LATIN CAPITAL LETTER O WITH HOOK ABOVE;Lu;0;L;004F 0309;;;;N;;;;1ECF;
+1ECF;LATIN SMALL LETTER O WITH HOOK ABOVE;Ll;0;L;006F 0309;;;;N;;;1ECE;;1ECE
+1ED0;LATIN CAPITAL LETTER O WITH CIRCUMFLEX AND ACUTE;Lu;0;L;00D4 0301;;;;N;;;;1ED1;
+1ED1;LATIN SMALL LETTER O WITH CIRCUMFLEX AND ACUTE;Ll;0;L;00F4 0301;;;;N;;;1ED0;;1ED0
+1ED2;LATIN CAPITAL LETTER O WITH CIRCUMFLEX AND GRAVE;Lu;0;L;00D4 0300;;;;N;;;;1ED3;
+1ED3;LATIN SMALL LETTER O WITH CIRCUMFLEX AND GRAVE;Ll;0;L;00F4 0300;;;;N;;;1ED2;;1ED2
+1ED4;LATIN CAPITAL LETTER O WITH CIRCUMFLEX AND HOOK ABOVE;Lu;0;L;00D4 0309;;;;N;;;;1ED5;
+1ED5;LATIN SMALL LETTER O WITH CIRCUMFLEX AND HOOK ABOVE;Ll;0;L;00F4 0309;;;;N;;;1ED4;;1ED4
+1ED6;LATIN CAPITAL LETTER O WITH CIRCUMFLEX AND TILDE;Lu;0;L;00D4 0303;;;;N;;;;1ED7;
+1ED7;LATIN SMALL LETTER O WITH CIRCUMFLEX AND TILDE;Ll;0;L;00F4 0303;;;;N;;;1ED6;;1ED6
+1ED8;LATIN CAPITAL LETTER O WITH CIRCUMFLEX AND DOT BELOW;Lu;0;L;1ECC 0302;;;;N;;;;1ED9;
+1ED9;LATIN SMALL LETTER O WITH CIRCUMFLEX AND DOT BELOW;Ll;0;L;1ECD 0302;;;;N;;;1ED8;;1ED8
+1EDA;LATIN CAPITAL LETTER O WITH HORN AND ACUTE;Lu;0;L;01A0 0301;;;;N;;;;1EDB;
+1EDB;LATIN SMALL LETTER O WITH HORN AND ACUTE;Ll;0;L;01A1 0301;;;;N;;;1EDA;;1EDA
+1EDC;LATIN CAPITAL LETTER O WITH HORN AND GRAVE;Lu;0;L;01A0 0300;;;;N;;;;1EDD;
+1EDD;LATIN SMALL LETTER O WITH HORN AND GRAVE;Ll;0;L;01A1 0300;;;;N;;;1EDC;;1EDC
+1EDE;LATIN CAPITAL LETTER O WITH HORN AND HOOK ABOVE;Lu;0;L;01A0 0309;;;;N;;;;1EDF;
+1EDF;LATIN SMALL LETTER O WITH HORN AND HOOK ABOVE;Ll;0;L;01A1 0309;;;;N;;;1EDE;;1EDE
+1EE0;LATIN CAPITAL LETTER O WITH HORN AND TILDE;Lu;0;L;01A0 0303;;;;N;;;;1EE1;
+1EE1;LATIN SMALL LETTER O WITH HORN AND TILDE;Ll;0;L;01A1 0303;;;;N;;;1EE0;;1EE0
+1EE2;LATIN CAPITAL LETTER O WITH HORN AND DOT BELOW;Lu;0;L;01A0 0323;;;;N;;;;1EE3;
+1EE3;LATIN SMALL LETTER O WITH HORN AND DOT BELOW;Ll;0;L;01A1 0323;;;;N;;;1EE2;;1EE2
+1EE4;LATIN CAPITAL LETTER U WITH DOT BELOW;Lu;0;L;0055 0323;;;;N;;;;1EE5;
+1EE5;LATIN SMALL LETTER U WITH DOT BELOW;Ll;0;L;0075 0323;;;;N;;;1EE4;;1EE4
+1EE6;LATIN CAPITAL LETTER U WITH HOOK ABOVE;Lu;0;L;0055 0309;;;;N;;;;1EE7;
+1EE7;LATIN SMALL LETTER U WITH HOOK ABOVE;Ll;0;L;0075 0309;;;;N;;;1EE6;;1EE6
+1EE8;LATIN CAPITAL LETTER U WITH HORN AND ACUTE;Lu;0;L;01AF 0301;;;;N;;;;1EE9;
+1EE9;LATIN SMALL LETTER U WITH HORN AND ACUTE;Ll;0;L;01B0 0301;;;;N;;;1EE8;;1EE8
+1EEA;LATIN CAPITAL LETTER U WITH HORN AND GRAVE;Lu;0;L;01AF 0300;;;;N;;;;1EEB;
+1EEB;LATIN SMALL LETTER U WITH HORN AND GRAVE;Ll;0;L;01B0 0300;;;;N;;;1EEA;;1EEA
+1EEC;LATIN CAPITAL LETTER U WITH HORN AND HOOK ABOVE;Lu;0;L;01AF 0309;;;;N;;;;1EED;
+1EED;LATIN SMALL LETTER U WITH HORN AND HOOK ABOVE;Ll;0;L;01B0 0309;;;;N;;;1EEC;;1EEC
+1EEE;LATIN CAPITAL LETTER U WITH HORN AND TILDE;Lu;0;L;01AF 0303;;;;N;;;;1EEF;
+1EEF;LATIN SMALL LETTER U WITH HORN AND TILDE;Ll;0;L;01B0 0303;;;;N;;;1EEE;;1EEE
+1EF0;LATIN CAPITAL LETTER U WITH HORN AND DOT BELOW;Lu;0;L;01AF 0323;;;;N;;;;1EF1;
+1EF1;LATIN SMALL LETTER U WITH HORN AND DOT BELOW;Ll;0;L;01B0 0323;;;;N;;;1EF0;;1EF0
+1EF2;LATIN CAPITAL LETTER Y WITH GRAVE;Lu;0;L;0059 0300;;;;N;;;;1EF3;
+1EF3;LATIN SMALL LETTER Y WITH GRAVE;Ll;0;L;0079 0300;;;;N;;;1EF2;;1EF2
+1EF4;LATIN CAPITAL LETTER Y WITH DOT BELOW;Lu;0;L;0059 0323;;;;N;;;;1EF5;
+1EF5;LATIN SMALL LETTER Y WITH DOT BELOW;Ll;0;L;0079 0323;;;;N;;;1EF4;;1EF4
+1EF6;LATIN CAPITAL LETTER Y WITH HOOK ABOVE;Lu;0;L;0059 0309;;;;N;;;;1EF7;
+1EF7;LATIN SMALL LETTER Y WITH HOOK ABOVE;Ll;0;L;0079 0309;;;;N;;;1EF6;;1EF6
+1EF8;LATIN CAPITAL LETTER Y WITH TILDE;Lu;0;L;0059 0303;;;;N;;;;1EF9;
+1EF9;LATIN SMALL LETTER Y WITH TILDE;Ll;0;L;0079 0303;;;;N;;;1EF8;;1EF8
+1EFA;LATIN CAPITAL LETTER MIDDLE-WELSH LL;Lu;0;L;;;;;N;;;;1EFB;
+1EFB;LATIN SMALL LETTER MIDDLE-WELSH LL;Ll;0;L;;;;;N;;;1EFA;;1EFA
+1EFC;LATIN CAPITAL LETTER MIDDLE-WELSH V;Lu;0;L;;;;;N;;;;1EFD;
+1EFD;LATIN SMALL LETTER MIDDLE-WELSH V;Ll;0;L;;;;;N;;;1EFC;;1EFC
+1EFE;LATIN CAPITAL LETTER Y WITH LOOP;Lu;0;L;;;;;N;;;;1EFF;
+1EFF;LATIN SMALL LETTER Y WITH LOOP;Ll;0;L;;;;;N;;;1EFE;;1EFE
+1F00;GREEK SMALL LETTER ALPHA WITH PSILI;Ll;0;L;03B1 0313;;;;N;;;1F08;;1F08
+1F01;GREEK SMALL LETTER ALPHA WITH DASIA;Ll;0;L;03B1 0314;;;;N;;;1F09;;1F09
+1F02;GREEK SMALL LETTER ALPHA WITH PSILI AND VARIA;Ll;0;L;1F00 0300;;;;N;;;1F0A;;1F0A
+1F03;GREEK SMALL LETTER ALPHA WITH DASIA AND VARIA;Ll;0;L;1F01 0300;;;;N;;;1F0B;;1F0B
+1F04;GREEK SMALL LETTER ALPHA WITH PSILI AND OXIA;Ll;0;L;1F00 0301;;;;N;;;1F0C;;1F0C
+1F05;GREEK SMALL LETTER ALPHA WITH DASIA AND OXIA;Ll;0;L;1F01 0301;;;;N;;;1F0D;;1F0D
+1F06;GREEK SMALL LETTER ALPHA WITH PSILI AND PERISPOMENI;Ll;0;L;1F00 0342;;;;N;;;1F0E;;1F0E
+1F07;GREEK SMALL LETTER ALPHA WITH DASIA AND PERISPOMENI;Ll;0;L;1F01 0342;;;;N;;;1F0F;;1F0F
+1F08;GREEK CAPITAL LETTER ALPHA WITH PSILI;Lu;0;L;0391 0313;;;;N;;;;1F00;
+1F09;GREEK CAPITAL LETTER ALPHA WITH DASIA;Lu;0;L;0391 0314;;;;N;;;;1F01;
+1F0A;GREEK CAPITAL LETTER ALPHA WITH PSILI AND VARIA;Lu;0;L;1F08 0300;;;;N;;;;1F02;
+1F0B;GREEK CAPITAL LETTER ALPHA WITH DASIA AND VARIA;Lu;0;L;1F09 0300;;;;N;;;;1F03;
+1F0C;GREEK CAPITAL LETTER ALPHA WITH PSILI AND OXIA;Lu;0;L;1F08 0301;;;;N;;;;1F04;
+1F0D;GREEK CAPITAL LETTER ALPHA WITH DASIA AND OXIA;Lu;0;L;1F09 0301;;;;N;;;;1F05;
+1F0E;GREEK CAPITAL LETTER ALPHA WITH PSILI AND PERISPOMENI;Lu;0;L;1F08 0342;;;;N;;;;1F06;
+1F0F;GREEK CAPITAL LETTER ALPHA WITH DASIA AND PERISPOMENI;Lu;0;L;1F09 0342;;;;N;;;;1F07;
+1F10;GREEK SMALL LETTER EPSILON WITH PSILI;Ll;0;L;03B5 0313;;;;N;;;1F18;;1F18
+1F11;GREEK SMALL LETTER EPSILON WITH DASIA;Ll;0;L;03B5 0314;;;;N;;;1F19;;1F19
+1F12;GREEK SMALL LETTER EPSILON WITH PSILI AND VARIA;Ll;0;L;1F10 0300;;;;N;;;1F1A;;1F1A
+1F13;GREEK SMALL LETTER EPSILON WITH DASIA AND VARIA;Ll;0;L;1F11 0300;;;;N;;;1F1B;;1F1B
+1F14;GREEK SMALL LETTER EPSILON WITH PSILI AND OXIA;Ll;0;L;1F10 0301;;;;N;;;1F1C;;1F1C
+1F15;GREEK SMALL LETTER EPSILON WITH DASIA AND OXIA;Ll;0;L;1F11 0301;;;;N;;;1F1D;;1F1D
+1F18;GREEK CAPITAL LETTER EPSILON WITH PSILI;Lu;0;L;0395 0313;;;;N;;;;1F10;
+1F19;GREEK CAPITAL LETTER EPSILON WITH DASIA;Lu;0;L;0395 0314;;;;N;;;;1F11;
+1F1A;GREEK CAPITAL LETTER EPSILON WITH PSILI AND VARIA;Lu;0;L;1F18 0300;;;;N;;;;1F12;
+1F1B;GREEK CAPITAL LETTER EPSILON WITH DASIA AND VARIA;Lu;0;L;1F19 0300;;;;N;;;;1F13;
+1F1C;GREEK CAPITAL LETTER EPSILON WITH PSILI AND OXIA;Lu;0;L;1F18 0301;;;;N;;;;1F14;
+1F1D;GREEK CAPITAL LETTER EPSILON WITH DASIA AND OXIA;Lu;0;L;1F19 0301;;;;N;;;;1F15;
+1F20;GREEK SMALL LETTER ETA WITH PSILI;Ll;0;L;03B7 0313;;;;N;;;1F28;;1F28
+1F21;GREEK SMALL LETTER ETA WITH DASIA;Ll;0;L;03B7 0314;;;;N;;;1F29;;1F29
+1F22;GREEK SMALL LETTER ETA WITH PSILI AND VARIA;Ll;0;L;1F20 0300;;;;N;;;1F2A;;1F2A
+1F23;GREEK SMALL LETTER ETA WITH DASIA AND VARIA;Ll;0;L;1F21 0300;;;;N;;;1F2B;;1F2B
+1F24;GREEK SMALL LETTER ETA WITH PSILI AND OXIA;Ll;0;L;1F20 0301;;;;N;;;1F2C;;1F2C
+1F25;GREEK SMALL LETTER ETA WITH DASIA AND OXIA;Ll;0;L;1F21 0301;;;;N;;;1F2D;;1F2D
+1F26;GREEK SMALL LETTER ETA WITH PSILI AND PERISPOMENI;Ll;0;L;1F20 0342;;;;N;;;1F2E;;1F2E
+1F27;GREEK SMALL LETTER ETA WITH DASIA AND PERISPOMENI;Ll;0;L;1F21 0342;;;;N;;;1F2F;;1F2F
+1F28;GREEK CAPITAL LETTER ETA WITH PSILI;Lu;0;L;0397 0313;;;;N;;;;1F20;
+1F29;GREEK CAPITAL LETTER ETA WITH DASIA;Lu;0;L;0397 0314;;;;N;;;;1F21;
+1F2A;GREEK CAPITAL LETTER ETA WITH PSILI AND VARIA;Lu;0;L;1F28 0300;;;;N;;;;1F22;
+1F2B;GREEK CAPITAL LETTER ETA WITH DASIA AND VARIA;Lu;0;L;1F29 0300;;;;N;;;;1F23;
+1F2C;GREEK CAPITAL LETTER ETA WITH PSILI AND OXIA;Lu;0;L;1F28 0301;;;;N;;;;1F24;
+1F2D;GREEK CAPITAL LETTER ETA WITH DASIA AND OXIA;Lu;0;L;1F29 0301;;;;N;;;;1F25;
+1F2E;GREEK CAPITAL LETTER ETA WITH PSILI AND PERISPOMENI;Lu;0;L;1F28 0342;;;;N;;;;1F26;
+1F2F;GREEK CAPITAL LETTER ETA WITH DASIA AND PERISPOMENI;Lu;0;L;1F29 0342;;;;N;;;;1F27;
+1F30;GREEK SMALL LETTER IOTA WITH PSILI;Ll;0;L;03B9 0313;;;;N;;;1F38;;1F38
+1F31;GREEK SMALL LETTER IOTA WITH DASIA;Ll;0;L;03B9 0314;;;;N;;;1F39;;1F39
+1F32;GREEK SMALL LETTER IOTA WITH PSILI AND VARIA;Ll;0;L;1F30 0300;;;;N;;;1F3A;;1F3A
+1F33;GREEK SMALL LETTER IOTA WITH DASIA AND VARIA;Ll;0;L;1F31 0300;;;;N;;;1F3B;;1F3B
+1F34;GREEK SMALL LETTER IOTA WITH PSILI AND OXIA;Ll;0;L;1F30 0301;;;;N;;;1F3C;;1F3C
+1F35;GREEK SMALL LETTER IOTA WITH DASIA AND OXIA;Ll;0;L;1F31 0301;;;;N;;;1F3D;;1F3D
+1F36;GREEK SMALL LETTER IOTA WITH PSILI AND PERISPOMENI;Ll;0;L;1F30 0342;;;;N;;;1F3E;;1F3E
+1F37;GREEK SMALL LETTER IOTA WITH DASIA AND PERISPOMENI;Ll;0;L;1F31 0342;;;;N;;;1F3F;;1F3F
+1F38;GREEK CAPITAL LETTER IOTA WITH PSILI;Lu;0;L;0399 0313;;;;N;;;;1F30;
+1F39;GREEK CAPITAL LETTER IOTA WITH DASIA;Lu;0;L;0399 0314;;;;N;;;;1F31;
+1F3A;GREEK CAPITAL LETTER IOTA WITH PSILI AND VARIA;Lu;0;L;1F38 0300;;;;N;;;;1F32;
+1F3B;GREEK CAPITAL LETTER IOTA WITH DASIA AND VARIA;Lu;0;L;1F39 0300;;;;N;;;;1F33;
+1F3C;GREEK CAPITAL LETTER IOTA WITH PSILI AND OXIA;Lu;0;L;1F38 0301;;;;N;;;;1F34;
+1F3D;GREEK CAPITAL LETTER IOTA WITH DASIA AND OXIA;Lu;0;L;1F39 0301;;;;N;;;;1F35;
+1F3E;GREEK CAPITAL LETTER IOTA WITH PSILI AND PERISPOMENI;Lu;0;L;1F38 0342;;;;N;;;;1F36;
+1F3F;GREEK CAPITAL LETTER IOTA WITH DASIA AND PERISPOMENI;Lu;0;L;1F39 0342;;;;N;;;;1F37;
+1F40;GREEK SMALL LETTER OMICRON WITH PSILI;Ll;0;L;03BF 0313;;;;N;;;1F48;;1F48
+1F41;GREEK SMALL LETTER OMICRON WITH DASIA;Ll;0;L;03BF 0314;;;;N;;;1F49;;1F49
+1F42;GREEK SMALL LETTER OMICRON WITH PSILI AND VARIA;Ll;0;L;1F40 0300;;;;N;;;1F4A;;1F4A
+1F43;GREEK SMALL LETTER OMICRON WITH DASIA AND VARIA;Ll;0;L;1F41 0300;;;;N;;;1F4B;;1F4B
+1F44;GREEK SMALL LETTER OMICRON WITH PSILI AND OXIA;Ll;0;L;1F40 0301;;;;N;;;1F4C;;1F4C
+1F45;GREEK SMALL LETTER OMICRON WITH DASIA AND OXIA;Ll;0;L;1F41 0301;;;;N;;;1F4D;;1F4D
+1F48;GREEK CAPITAL LETTER OMICRON WITH PSILI;Lu;0;L;039F 0313;;;;N;;;;1F40;
+1F49;GREEK CAPITAL LETTER OMICRON WITH DASIA;Lu;0;L;039F 0314;;;;N;;;;1F41;
+1F4A;GREEK CAPITAL LETTER OMICRON WITH PSILI AND VARIA;Lu;0;L;1F48 0300;;;;N;;;;1F42;
+1F4B;GREEK CAPITAL LETTER OMICRON WITH DASIA AND VARIA;Lu;0;L;1F49 0300;;;;N;;;;1F43;
+1F4C;GREEK CAPITAL LETTER OMICRON WITH PSILI AND OXIA;Lu;0;L;1F48 0301;;;;N;;;;1F44;
+1F4D;GREEK CAPITAL LETTER OMICRON WITH DASIA AND OXIA;Lu;0;L;1F49 0301;;;;N;;;;1F45;
+1F50;GREEK SMALL LETTER UPSILON WITH PSILI;Ll;0;L;03C5 0313;;;;N;;;;;
+1F51;GREEK SMALL LETTER UPSILON WITH DASIA;Ll;0;L;03C5 0314;;;;N;;;1F59;;1F59
+1F52;GREEK SMALL LETTER UPSILON WITH PSILI AND VARIA;Ll;0;L;1F50 0300;;;;N;;;;;
+1F53;GREEK SMALL LETTER UPSILON WITH DASIA AND VARIA;Ll;0;L;1F51 0300;;;;N;;;1F5B;;1F5B
+1F54;GREEK SMALL LETTER UPSILON WITH PSILI AND OXIA;Ll;0;L;1F50 0301;;;;N;;;;;
+1F55;GREEK SMALL LETTER UPSILON WITH DASIA AND OXIA;Ll;0;L;1F51 0301;;;;N;;;1F5D;;1F5D
+1F56;GREEK SMALL LETTER UPSILON WITH PSILI AND PERISPOMENI;Ll;0;L;1F50 0342;;;;N;;;;;
+1F57;GREEK SMALL LETTER UPSILON WITH DASIA AND PERISPOMENI;Ll;0;L;1F51 0342;;;;N;;;1F5F;;1F5F
+1F59;GREEK CAPITAL LETTER UPSILON WITH DASIA;Lu;0;L;03A5 0314;;;;N;;;;1F51;
+1F5B;GREEK CAPITAL LETTER UPSILON WITH DASIA AND VARIA;Lu;0;L;1F59 0300;;;;N;;;;1F53;
+1F5D;GREEK CAPITAL LETTER UPSILON WITH DASIA AND OXIA;Lu;0;L;1F59 0301;;;;N;;;;1F55;
+1F5F;GREEK CAPITAL LETTER UPSILON WITH DASIA AND PERISPOMENI;Lu;0;L;1F59 0342;;;;N;;;;1F57;
+1F60;GREEK SMALL LETTER OMEGA WITH PSILI;Ll;0;L;03C9 0313;;;;N;;;1F68;;1F68
+1F61;GREEK SMALL LETTER OMEGA WITH DASIA;Ll;0;L;03C9 0314;;;;N;;;1F69;;1F69
+1F62;GREEK SMALL LETTER OMEGA WITH PSILI AND VARIA;Ll;0;L;1F60 0300;;;;N;;;1F6A;;1F6A
+1F63;GREEK SMALL LETTER OMEGA WITH DASIA AND VARIA;Ll;0;L;1F61 0300;;;;N;;;1F6B;;1F6B
+1F64;GREEK SMALL LETTER OMEGA WITH PSILI AND OXIA;Ll;0;L;1F60 0301;;;;N;;;1F6C;;1F6C
+1F65;GREEK SMALL LETTER OMEGA WITH DASIA AND OXIA;Ll;0;L;1F61 0301;;;;N;;;1F6D;;1F6D
+1F66;GREEK SMALL LETTER OMEGA WITH PSILI AND PERISPOMENI;Ll;0;L;1F60 0342;;;;N;;;1F6E;;1F6E
+1F67;GREEK SMALL LETTER OMEGA WITH DASIA AND PERISPOMENI;Ll;0;L;1F61 0342;;;;N;;;1F6F;;1F6F
+1F68;GREEK CAPITAL LETTER OMEGA WITH PSILI;Lu;0;L;03A9 0313;;;;N;;;;1F60;
+1F69;GREEK CAPITAL LETTER OMEGA WITH DASIA;Lu;0;L;03A9 0314;;;;N;;;;1F61;
+1F6A;GREEK CAPITAL LETTER OMEGA WITH PSILI AND VARIA;Lu;0;L;1F68 0300;;;;N;;;;1F62;
+1F6B;GREEK CAPITAL LETTER OMEGA WITH DASIA AND VARIA;Lu;0;L;1F69 0300;;;;N;;;;1F63;
+1F6C;GREEK CAPITAL LETTER OMEGA WITH PSILI AND OXIA;Lu;0;L;1F68 0301;;;;N;;;;1F64;
+1F6D;GREEK CAPITAL LETTER OMEGA WITH DASIA AND OXIA;Lu;0;L;1F69 0301;;;;N;;;;1F65;
+1F6E;GREEK CAPITAL LETTER OMEGA WITH PSILI AND PERISPOMENI;Lu;0;L;1F68 0342;;;;N;;;;1F66;
+1F6F;GREEK CAPITAL LETTER OMEGA WITH DASIA AND PERISPOMENI;Lu;0;L;1F69 0342;;;;N;;;;1F67;
+1F70;GREEK SMALL LETTER ALPHA WITH VARIA;Ll;0;L;03B1 0300;;;;N;;;1FBA;;1FBA
+1F71;GREEK SMALL LETTER ALPHA WITH OXIA;Ll;0;L;03AC;;;;N;;;1FBB;;1FBB
+1F72;GREEK SMALL LETTER EPSILON WITH VARIA;Ll;0;L;03B5 0300;;;;N;;;1FC8;;1FC8
+1F73;GREEK SMALL LETTER EPSILON WITH OXIA;Ll;0;L;03AD;;;;N;;;1FC9;;1FC9
+1F74;GREEK SMALL LETTER ETA WITH VARIA;Ll;0;L;03B7 0300;;;;N;;;1FCA;;1FCA
+1F75;GREEK SMALL LETTER ETA WITH OXIA;Ll;0;L;03AE;;;;N;;;1FCB;;1FCB
+1F76;GREEK SMALL LETTER IOTA WITH VARIA;Ll;0;L;03B9 0300;;;;N;;;1FDA;;1FDA
+1F77;GREEK SMALL LETTER IOTA WITH OXIA;Ll;0;L;03AF;;;;N;;;1FDB;;1FDB
+1F78;GREEK SMALL LETTER OMICRON WITH VARIA;Ll;0;L;03BF 0300;;;;N;;;1FF8;;1FF8
+1F79;GREEK SMALL LETTER OMICRON WITH OXIA;Ll;0;L;03CC;;;;N;;;1FF9;;1FF9
+1F7A;GREEK SMALL LETTER UPSILON WITH VARIA;Ll;0;L;03C5 0300;;;;N;;;1FEA;;1FEA
+1F7B;GREEK SMALL LETTER UPSILON WITH OXIA;Ll;0;L;03CD;;;;N;;;1FEB;;1FEB
+1F7C;GREEK SMALL LETTER OMEGA WITH VARIA;Ll;0;L;03C9 0300;;;;N;;;1FFA;;1FFA
+1F7D;GREEK SMALL LETTER OMEGA WITH OXIA;Ll;0;L;03CE;;;;N;;;1FFB;;1FFB
+1F80;GREEK SMALL LETTER ALPHA WITH PSILI AND YPOGEGRAMMENI;Ll;0;L;1F00 0345;;;;N;;;1F88;;1F88
+1F81;GREEK SMALL LETTER ALPHA WITH DASIA AND YPOGEGRAMMENI;Ll;0;L;1F01 0345;;;;N;;;1F89;;1F89
+1F82;GREEK SMALL LETTER ALPHA WITH PSILI AND VARIA AND YPOGEGRAMMENI;Ll;0;L;1F02 0345;;;;N;;;1F8A;;1F8A
+1F83;GREEK SMALL LETTER ALPHA WITH DASIA AND VARIA AND YPOGEGRAMMENI;Ll;0;L;1F03 0345;;;;N;;;1F8B;;1F8B
+1F84;GREEK SMALL LETTER ALPHA WITH PSILI AND OXIA AND YPOGEGRAMMENI;Ll;0;L;1F04 0345;;;;N;;;1F8C;;1F8C
+1F85;GREEK SMALL LETTER ALPHA WITH DASIA AND OXIA AND YPOGEGRAMMENI;Ll;0;L;1F05 0345;;;;N;;;1F8D;;1F8D
+1F86;GREEK SMALL LETTER ALPHA WITH PSILI AND PERISPOMENI AND YPOGEGRAMMENI;Ll;0;L;1F06 0345;;;;N;;;1F8E;;1F8E
+1F87;GREEK SMALL LETTER ALPHA WITH DASIA AND PERISPOMENI AND YPOGEGRAMMENI;Ll;0;L;1F07 0345;;;;N;;;1F8F;;1F8F
+1F88;GREEK CAPITAL LETTER ALPHA WITH PSILI AND PROSGEGRAMMENI;Lt;0;L;1F08 0345;;;;N;;;;1F80;
+1F89;GREEK CAPITAL LETTER ALPHA WITH DASIA AND PROSGEGRAMMENI;Lt;0;L;1F09 0345;;;;N;;;;1F81;
+1F8A;GREEK CAPITAL LETTER ALPHA WITH PSILI AND VARIA AND PROSGEGRAMMENI;Lt;0;L;1F0A 0345;;;;N;;;;1F82;
+1F8B;GREEK CAPITAL LETTER ALPHA WITH DASIA AND VARIA AND PROSGEGRAMMENI;Lt;0;L;1F0B 0345;;;;N;;;;1F83;
+1F8C;GREEK CAPITAL LETTER ALPHA WITH PSILI AND OXIA AND PROSGEGRAMMENI;Lt;0;L;1F0C 0345;;;;N;;;;1F84;
+1F8D;GREEK CAPITAL LETTER ALPHA WITH DASIA AND OXIA AND PROSGEGRAMMENI;Lt;0;L;1F0D 0345;;;;N;;;;1F85;
+1F8E;GREEK CAPITAL LETTER ALPHA WITH PSILI AND PERISPOMENI AND PROSGEGRAMMENI;Lt;0;L;1F0E 0345;;;;N;;;;1F86;
+1F8F;GREEK CAPITAL LETTER ALPHA WITH DASIA AND PERISPOMENI AND PROSGEGRAMMENI;Lt;0;L;1F0F 0345;;;;N;;;;1F87;
+1F90;GREEK SMALL LETTER ETA WITH PSILI AND YPOGEGRAMMENI;Ll;0;L;1F20 0345;;;;N;;;1F98;;1F98
+1F91;GREEK SMALL LETTER ETA WITH DASIA AND YPOGEGRAMMENI;Ll;0;L;1F21 0345;;;;N;;;1F99;;1F99
+1F92;GREEK SMALL LETTER ETA WITH PSILI AND VARIA AND YPOGEGRAMMENI;Ll;0;L;1F22 0345;;;;N;;;1F9A;;1F9A
+1F93;GREEK SMALL LETTER ETA WITH DASIA AND VARIA AND YPOGEGRAMMENI;Ll;0;L;1F23 0345;;;;N;;;1F9B;;1F9B
+1F94;GREEK SMALL LETTER ETA WITH PSILI AND OXIA AND YPOGEGRAMMENI;Ll;0;L;1F24 0345;;;;N;;;1F9C;;1F9C
+1F95;GREEK SMALL LETTER ETA WITH DASIA AND OXIA AND YPOGEGRAMMENI;Ll;0;L;1F25 0345;;;;N;;;1F9D;;1F9D
+1F96;GREEK SMALL LETTER ETA WITH PSILI AND PERISPOMENI AND YPOGEGRAMMENI;Ll;0;L;1F26 0345;;;;N;;;1F9E;;1F9E
+1F97;GREEK SMALL LETTER ETA WITH DASIA AND PERISPOMENI AND YPOGEGRAMMENI;Ll;0;L;1F27 0345;;;;N;;;1F9F;;1F9F
+1F98;GREEK CAPITAL LETTER ETA WITH PSILI AND PROSGEGRAMMENI;Lt;0;L;1F28 0345;;;;N;;;;1F90;
+1F99;GREEK CAPITAL LETTER ETA WITH DASIA AND PROSGEGRAMMENI;Lt;0;L;1F29 0345;;;;N;;;;1F91;
+1F9A;GREEK CAPITAL LETTER ETA WITH PSILI AND VARIA AND PROSGEGRAMMENI;Lt;0;L;1F2A 0345;;;;N;;;;1F92;
+1F9B;GREEK CAPITAL LETTER ETA WITH DASIA AND VARIA AND PROSGEGRAMMENI;Lt;0;L;1F2B 0345;;;;N;;;;1F93;
+1F9C;GREEK CAPITAL LETTER ETA WITH PSILI AND OXIA AND PROSGEGRAMMENI;Lt;0;L;1F2C 0345;;;;N;;;;1F94;
+1F9D;GREEK CAPITAL LETTER ETA WITH DASIA AND OXIA AND PROSGEGRAMMENI;Lt;0;L;1F2D 0345;;;;N;;;;1F95;
+1F9E;GREEK CAPITAL LETTER ETA WITH PSILI AND PERISPOMENI AND PROSGEGRAMMENI;Lt;0;L;1F2E 0345;;;;N;;;;1F96;
+1F9F;GREEK CAPITAL LETTER ETA WITH DASIA AND PERISPOMENI AND PROSGEGRAMMENI;Lt;0;L;1F2F 0345;;;;N;;;;1F97;
+1FA0;GREEK SMALL LETTER OMEGA WITH PSILI AND YPOGEGRAMMENI;Ll;0;L;1F60 0345;;;;N;;;1FA8;;1FA8
+1FA1;GREEK SMALL LETTER OMEGA WITH DASIA AND YPOGEGRAMMENI;Ll;0;L;1F61 0345;;;;N;;;1FA9;;1FA9
+1FA2;GREEK SMALL LETTER OMEGA WITH PSILI AND VARIA AND YPOGEGRAMMENI;Ll;0;L;1F62 0345;;;;N;;;1FAA;;1FAA
+1FA3;GREEK SMALL LETTER OMEGA WITH DASIA AND VARIA AND YPOGEGRAMMENI;Ll;0;L;1F63 0345;;;;N;;;1FAB;;1FAB
+1FA4;GREEK SMALL LETTER OMEGA WITH PSILI AND OXIA AND YPOGEGRAMMENI;Ll;0;L;1F64 0345;;;;N;;;1FAC;;1FAC
+1FA5;GREEK SMALL LETTER OMEGA WITH DASIA AND OXIA AND YPOGEGRAMMENI;Ll;0;L;1F65 0345;;;;N;;;1FAD;;1FAD
+1FA6;GREEK SMALL LETTER OMEGA WITH PSILI AND PERISPOMENI AND YPOGEGRAMMENI;Ll;0;L;1F66 0345;;;;N;;;1FAE;;1FAE
+1FA7;GREEK SMALL LETTER OMEGA WITH DASIA AND PERISPOMENI AND YPOGEGRAMMENI;Ll;0;L;1F67 0345;;;;N;;;1FAF;;1FAF
+1FA8;GREEK CAPITAL LETTER OMEGA WITH PSILI AND PROSGEGRAMMENI;Lt;0;L;1F68 0345;;;;N;;;;1FA0;
+1FA9;GREEK CAPITAL LETTER OMEGA WITH DASIA AND PROSGEGRAMMENI;Lt;0;L;1F69 0345;;;;N;;;;1FA1;
+1FAA;GREEK CAPITAL LETTER OMEGA WITH PSILI AND VARIA AND PROSGEGRAMMENI;Lt;0;L;1F6A 0345;;;;N;;;;1FA2;
+1FAB;GREEK CAPITAL LETTER OMEGA WITH DASIA AND VARIA AND PROSGEGRAMMENI;Lt;0;L;1F6B 0345;;;;N;;;;1FA3;
+1FAC;GREEK CAPITAL LETTER OMEGA WITH PSILI AND OXIA AND PROSGEGRAMMENI;Lt;0;L;1F6C 0345;;;;N;;;;1FA4;
+1FAD;GREEK CAPITAL LETTER OMEGA WITH DASIA AND OXIA AND PROSGEGRAMMENI;Lt;0;L;1F6D 0345;;;;N;;;;1FA5;
+1FAE;GREEK CAPITAL LETTER OMEGA WITH PSILI AND PERISPOMENI AND PROSGEGRAMMENI;Lt;0;L;1F6E 0345;;;;N;;;;1FA6;
+1FAF;GREEK CAPITAL LETTER OMEGA WITH DASIA AND PERISPOMENI AND PROSGEGRAMMENI;Lt;0;L;1F6F 0345;;;;N;;;;1FA7;
+1FB0;GREEK SMALL LETTER ALPHA WITH VRACHY;Ll;0;L;03B1 0306;;;;N;;;1FB8;;1FB8
+1FB1;GREEK SMALL LETTER ALPHA WITH MACRON;Ll;0;L;03B1 0304;;;;N;;;1FB9;;1FB9
+1FB2;GREEK SMALL LETTER ALPHA WITH VARIA AND YPOGEGRAMMENI;Ll;0;L;1F70 0345;;;;N;;;;;
+1FB3;GREEK SMALL LETTER ALPHA WITH YPOGEGRAMMENI;Ll;0;L;03B1 0345;;;;N;;;1FBC;;1FBC
+1FB4;GREEK SMALL LETTER ALPHA WITH OXIA AND YPOGEGRAMMENI;Ll;0;L;03AC 0345;;;;N;;;;;
+1FB6;GREEK SMALL LETTER ALPHA WITH PERISPOMENI;Ll;0;L;03B1 0342;;;;N;;;;;
+1FB7;GREEK SMALL LETTER ALPHA WITH PERISPOMENI AND YPOGEGRAMMENI;Ll;0;L;1FB6 0345;;;;N;;;;;
+1FB8;GREEK CAPITAL LETTER ALPHA WITH VRACHY;Lu;0;L;0391 0306;;;;N;;;;1FB0;
+1FB9;GREEK CAPITAL LETTER ALPHA WITH MACRON;Lu;0;L;0391 0304;;;;N;;;;1FB1;
+1FBA;GREEK CAPITAL LETTER ALPHA WITH VARIA;Lu;0;L;0391 0300;;;;N;;;;1F70;
+1FBB;GREEK CAPITAL LETTER ALPHA WITH OXIA;Lu;0;L;0386;;;;N;;;;1F71;
+1FBC;GREEK CAPITAL LETTER ALPHA WITH PROSGEGRAMMENI;Lt;0;L;0391 0345;;;;N;;;;1FB3;
+1FBD;GREEK KORONIS;Sk;0;ON;<compat> 0020 0313;;;;N;;;;;
+1FBE;GREEK PROSGEGRAMMENI;Ll;0;L;03B9;;;;N;;;0399;;0399
+1FBF;GREEK PSILI;Sk;0;ON;<compat> 0020 0313;;;;N;;;;;
+1FC0;GREEK PERISPOMENI;Sk;0;ON;<compat> 0020 0342;;;;N;;;;;
+1FC1;GREEK DIALYTIKA AND PERISPOMENI;Sk;0;ON;00A8 0342;;;;N;;;;;
+1FC2;GREEK SMALL LETTER ETA WITH VARIA AND YPOGEGRAMMENI;Ll;0;L;1F74 0345;;;;N;;;;;
+1FC3;GREEK SMALL LETTER ETA WITH YPOGEGRAMMENI;Ll;0;L;03B7 0345;;;;N;;;1FCC;;1FCC
+1FC4;GREEK SMALL LETTER ETA WITH OXIA AND YPOGEGRAMMENI;Ll;0;L;03AE 0345;;;;N;;;;;
+1FC6;GREEK SMALL LETTER ETA WITH PERISPOMENI;Ll;0;L;03B7 0342;;;;N;;;;;
+1FC7;GREEK SMALL LETTER ETA WITH PERISPOMENI AND YPOGEGRAMMENI;Ll;0;L;1FC6 0345;;;;N;;;;;
+1FC8;GREEK CAPITAL LETTER EPSILON WITH VARIA;Lu;0;L;0395 0300;;;;N;;;;1F72;
+1FC9;GREEK CAPITAL LETTER EPSILON WITH OXIA;Lu;0;L;0388;;;;N;;;;1F73;
+1FCA;GREEK CAPITAL LETTER ETA WITH VARIA;Lu;0;L;0397 0300;;;;N;;;;1F74;
+1FCB;GREEK CAPITAL LETTER ETA WITH OXIA;Lu;0;L;0389;;;;N;;;;1F75;
+1FCC;GREEK CAPITAL LETTER ETA WITH PROSGEGRAMMENI;Lt;0;L;0397 0345;;;;N;;;;1FC3;
+1FCD;GREEK PSILI AND VARIA;Sk;0;ON;1FBF 0300;;;;N;;;;;
+1FCE;GREEK PSILI AND OXIA;Sk;0;ON;1FBF 0301;;;;N;;;;;
+1FCF;GREEK PSILI AND PERISPOMENI;Sk;0;ON;1FBF 0342;;;;N;;;;;
+1FD0;GREEK SMALL LETTER IOTA WITH VRACHY;Ll;0;L;03B9 0306;;;;N;;;1FD8;;1FD8
+1FD1;GREEK SMALL LETTER IOTA WITH MACRON;Ll;0;L;03B9 0304;;;;N;;;1FD9;;1FD9
+1FD2;GREEK SMALL LETTER IOTA WITH DIALYTIKA AND VARIA;Ll;0;L;03CA 0300;;;;N;;;;;
+1FD3;GREEK SMALL LETTER IOTA WITH DIALYTIKA AND OXIA;Ll;0;L;0390;;;;N;;;;;
+1FD6;GREEK SMALL LETTER IOTA WITH PERISPOMENI;Ll;0;L;03B9 0342;;;;N;;;;;
+1FD7;GREEK SMALL LETTER IOTA WITH DIALYTIKA AND PERISPOMENI;Ll;0;L;03CA 0342;;;;N;;;;;
+1FD8;GREEK CAPITAL LETTER IOTA WITH VRACHY;Lu;0;L;0399 0306;;;;N;;;;1FD0;
+1FD9;GREEK CAPITAL LETTER IOTA WITH MACRON;Lu;0;L;0399 0304;;;;N;;;;1FD1;
+1FDA;GREEK CAPITAL LETTER IOTA WITH VARIA;Lu;0;L;0399 0300;;;;N;;;;1F76;
+1FDB;GREEK CAPITAL LETTER IOTA WITH OXIA;Lu;0;L;038A;;;;N;;;;1F77;
+1FDD;GREEK DASIA AND VARIA;Sk;0;ON;1FFE 0300;;;;N;;;;;
+1FDE;GREEK DASIA AND OXIA;Sk;0;ON;1FFE 0301;;;;N;;;;;
+1FDF;GREEK DASIA AND PERISPOMENI;Sk;0;ON;1FFE 0342;;;;N;;;;;
+1FE0;GREEK SMALL LETTER UPSILON WITH VRACHY;Ll;0;L;03C5 0306;;;;N;;;1FE8;;1FE8
+1FE1;GREEK SMALL LETTER UPSILON WITH MACRON;Ll;0;L;03C5 0304;;;;N;;;1FE9;;1FE9
+1FE2;GREEK SMALL LETTER UPSILON WITH DIALYTIKA AND VARIA;Ll;0;L;03CB 0300;;;;N;;;;;
+1FE3;GREEK SMALL LETTER UPSILON WITH DIALYTIKA AND OXIA;Ll;0;L;03B0;;;;N;;;;;
+1FE4;GREEK SMALL LETTER RHO WITH PSILI;Ll;0;L;03C1 0313;;;;N;;;;;
+1FE5;GREEK SMALL LETTER RHO WITH DASIA;Ll;0;L;03C1 0314;;;;N;;;1FEC;;1FEC
+1FE6;GREEK SMALL LETTER UPSILON WITH PERISPOMENI;Ll;0;L;03C5 0342;;;;N;;;;;
+1FE7;GREEK SMALL LETTER UPSILON WITH DIALYTIKA AND PERISPOMENI;Ll;0;L;03CB 0342;;;;N;;;;;
+1FE8;GREEK CAPITAL LETTER UPSILON WITH VRACHY;Lu;0;L;03A5 0306;;;;N;;;;1FE0;
+1FE9;GREEK CAPITAL LETTER UPSILON WITH MACRON;Lu;0;L;03A5 0304;;;;N;;;;1FE1;
+1FEA;GREEK CAPITAL LETTER UPSILON WITH VARIA;Lu;0;L;03A5 0300;;;;N;;;;1F7A;
+1FEB;GREEK CAPITAL LETTER UPSILON WITH OXIA;Lu;0;L;038E;;;;N;;;;1F7B;
+1FEC;GREEK CAPITAL LETTER RHO WITH DASIA;Lu;0;L;03A1 0314;;;;N;;;;1FE5;
+1FED;GREEK DIALYTIKA AND VARIA;Sk;0;ON;00A8 0300;;;;N;;;;;
+1FEE;GREEK DIALYTIKA AND OXIA;Sk;0;ON;0385;;;;N;;;;;
+1FEF;GREEK VARIA;Sk;0;ON;0060;;;;N;;;;;
+1FF2;GREEK SMALL LETTER OMEGA WITH VARIA AND YPOGEGRAMMENI;Ll;0;L;1F7C 0345;;;;N;;;;;
+1FF3;GREEK SMALL LETTER OMEGA WITH YPOGEGRAMMENI;Ll;0;L;03C9 0345;;;;N;;;1FFC;;1FFC
+1FF4;GREEK SMALL LETTER OMEGA WITH OXIA AND YPOGEGRAMMENI;Ll;0;L;03CE 0345;;;;N;;;;;
+1FF6;GREEK SMALL LETTER OMEGA WITH PERISPOMENI;Ll;0;L;03C9 0342;;;;N;;;;;
+1FF7;GREEK SMALL LETTER OMEGA WITH PERISPOMENI AND YPOGEGRAMMENI;Ll;0;L;1FF6 0345;;;;N;;;;;
+1FF8;GREEK CAPITAL LETTER OMICRON WITH VARIA;Lu;0;L;039F 0300;;;;N;;;;1F78;
+1FF9;GREEK CAPITAL LETTER OMICRON WITH OXIA;Lu;0;L;038C;;;;N;;;;1F79;
+1FFA;GREEK CAPITAL LETTER OMEGA WITH VARIA;Lu;0;L;03A9 0300;;;;N;;;;1F7C;
+1FFB;GREEK CAPITAL LETTER OMEGA WITH OXIA;Lu;0;L;038F;;;;N;;;;1F7D;
+1FFC;GREEK CAPITAL LETTER OMEGA WITH PROSGEGRAMMENI;Lt;0;L;03A9 0345;;;;N;;;;1FF3;
+1FFD;GREEK OXIA;Sk;0;ON;00B4;;;;N;;;;;
+1FFE;GREEK DASIA;Sk;0;ON;<compat> 0020 0314;;;;N;;;;;
+2000;EN QUAD;Zs;0;WS;2002;;;;N;;;;;
+2001;EM QUAD;Zs;0;WS;2003;;;;N;;;;;
+2002;EN SPACE;Zs;0;WS;<compat> 0020;;;;N;;;;;
+2003;EM SPACE;Zs;0;WS;<compat> 0020;;;;N;;;;;
+2004;THREE-PER-EM SPACE;Zs;0;WS;<compat> 0020;;;;N;;;;;
+2005;FOUR-PER-EM SPACE;Zs;0;WS;<compat> 0020;;;;N;;;;;
+2006;SIX-PER-EM SPACE;Zs;0;WS;<compat> 0020;;;;N;;;;;
+2007;FIGURE SPACE;Zs;0;WS;<noBreak> 0020;;;;N;;;;;
+2008;PUNCTUATION SPACE;Zs;0;WS;<compat> 0020;;;;N;;;;;
+2009;THIN SPACE;Zs;0;WS;<compat> 0020;;;;N;;;;;
+200A;HAIR SPACE;Zs;0;WS;<compat> 0020;;;;N;;;;;
+200B;ZERO WIDTH SPACE;Cf;0;BN;;;;;N;;;;;
+200C;ZERO WIDTH NON-JOINER;Cf;0;BN;;;;;N;;;;;
+200D;ZERO WIDTH JOINER;Cf;0;BN;;;;;N;;;;;
+200E;LEFT-TO-RIGHT MARK;Cf;0;L;;;;;N;;;;;
+200F;RIGHT-TO-LEFT MARK;Cf;0;R;;;;;N;;;;;
+2010;HYPHEN;Pd;0;ON;;;;;N;;;;;
+2011;NON-BREAKING HYPHEN;Pd;0;ON;<noBreak> 2010;;;;N;;;;;
+2012;FIGURE DASH;Pd;0;ON;;;;;N;;;;;
+2013;EN DASH;Pd;0;ON;;;;;N;;;;;
+2014;EM DASH;Pd;0;ON;;;;;N;;;;;
+2015;HORIZONTAL BAR;Pd;0;ON;;;;;N;QUOTATION DASH;;;;
+2016;DOUBLE VERTICAL LINE;Po;0;ON;;;;;N;DOUBLE VERTICAL BAR;;;;
+2017;DOUBLE LOW LINE;Po;0;ON;<compat> 0020 0333;;;;N;SPACING DOUBLE UNDERSCORE;;;;
+2018;LEFT SINGLE QUOTATION MARK;Pi;0;ON;;;;;N;SINGLE TURNED COMMA QUOTATION MARK;;;;
+2019;RIGHT SINGLE QUOTATION MARK;Pf;0;ON;;;;;N;SINGLE COMMA QUOTATION MARK;;;;
+201A;SINGLE LOW-9 QUOTATION MARK;Ps;0;ON;;;;;N;LOW SINGLE COMMA QUOTATION MARK;;;;
+201B;SINGLE HIGH-REVERSED-9 QUOTATION MARK;Pi;0;ON;;;;;N;SINGLE REVERSED COMMA QUOTATION MARK;;;;
+201C;LEFT DOUBLE QUOTATION MARK;Pi;0;ON;;;;;N;DOUBLE TURNED COMMA QUOTATION MARK;;;;
+201D;RIGHT DOUBLE QUOTATION MARK;Pf;0;ON;;;;;N;DOUBLE COMMA QUOTATION MARK;;;;
+201E;DOUBLE LOW-9 QUOTATION MARK;Ps;0;ON;;;;;N;LOW DOUBLE COMMA QUOTATION MARK;;;;
+201F;DOUBLE HIGH-REVERSED-9 QUOTATION MARK;Pi;0;ON;;;;;N;DOUBLE REVERSED COMMA QUOTATION MARK;;;;
+2020;DAGGER;Po;0;ON;;;;;N;;;;;
+2021;DOUBLE DAGGER;Po;0;ON;;;;;N;;;;;
+2022;BULLET;Po;0;ON;;;;;N;;;;;
+2023;TRIANGULAR BULLET;Po;0;ON;;;;;N;;;;;
+2024;ONE DOT LEADER;Po;0;ON;<compat> 002E;;;;N;;;;;
+2025;TWO DOT LEADER;Po;0;ON;<compat> 002E 002E;;;;N;;;;;
+2026;HORIZONTAL ELLIPSIS;Po;0;ON;<compat> 002E 002E 002E;;;;N;;;;;
+2027;HYPHENATION POINT;Po;0;ON;;;;;N;;;;;
+2028;LINE SEPARATOR;Zl;0;WS;;;;;N;;;;;
+2029;PARAGRAPH SEPARATOR;Zp;0;B;;;;;N;;;;;
+202A;LEFT-TO-RIGHT EMBEDDING;Cf;0;LRE;;;;;N;;;;;
+202B;RIGHT-TO-LEFT EMBEDDING;Cf;0;RLE;;;;;N;;;;;
+202C;POP DIRECTIONAL FORMATTING;Cf;0;PDF;;;;;N;;;;;
+202D;LEFT-TO-RIGHT OVERRIDE;Cf;0;LRO;;;;;N;;;;;
+202E;RIGHT-TO-LEFT OVERRIDE;Cf;0;RLO;;;;;N;;;;;
+202F;NARROW NO-BREAK SPACE;Zs;0;CS;<noBreak> 0020;;;;N;;;;;
+2030;PER MILLE SIGN;Po;0;ET;;;;;N;;;;;
+2031;PER TEN THOUSAND SIGN;Po;0;ET;;;;;N;;;;;
+2032;PRIME;Po;0;ET;;;;;N;;;;;
+2033;DOUBLE PRIME;Po;0;ET;<compat> 2032 2032;;;;N;;;;;
+2034;TRIPLE PRIME;Po;0;ET;<compat> 2032 2032 2032;;;;N;;;;;
+2035;REVERSED PRIME;Po;0;ON;;;;;N;;;;;
+2036;REVERSED DOUBLE PRIME;Po;0;ON;<compat> 2035 2035;;;;N;;;;;
+2037;REVERSED TRIPLE PRIME;Po;0;ON;<compat> 2035 2035 2035;;;;N;;;;;
+2038;CARET;Po;0;ON;;;;;N;;;;;
+2039;SINGLE LEFT-POINTING ANGLE QUOTATION MARK;Pi;0;ON;;;;;Y;LEFT POINTING SINGLE GUILLEMET;;;;
+203A;SINGLE RIGHT-POINTING ANGLE QUOTATION MARK;Pf;0;ON;;;;;Y;RIGHT POINTING SINGLE GUILLEMET;;;;
+203B;REFERENCE MARK;Po;0;ON;;;;;N;;;;;
+203C;DOUBLE EXCLAMATION MARK;Po;0;ON;<compat> 0021 0021;;;;N;;;;;
+203D;INTERROBANG;Po;0;ON;;;;;N;;;;;
+203E;OVERLINE;Po;0;ON;<compat> 0020 0305;;;;N;SPACING OVERSCORE;;;;
+203F;UNDERTIE;Pc;0;ON;;;;;N;;;;;
+2040;CHARACTER TIE;Pc;0;ON;;;;;N;;;;;
+2041;CARET INSERTION POINT;Po;0;ON;;;;;N;;;;;
+2042;ASTERISM;Po;0;ON;;;;;N;;;;;
+2043;HYPHEN BULLET;Po;0;ON;;;;;N;;;;;
+2044;FRACTION SLASH;Sm;0;CS;;;;;N;;;;;
+2045;LEFT SQUARE BRACKET WITH QUILL;Ps;0;ON;;;;;Y;;;;;
+2046;RIGHT SQUARE BRACKET WITH QUILL;Pe;0;ON;;;;;Y;;;;;
+2047;DOUBLE QUESTION MARK;Po;0;ON;<compat> 003F 003F;;;;N;;;;;
+2048;QUESTION EXCLAMATION MARK;Po;0;ON;<compat> 003F 0021;;;;N;;;;;
+2049;EXCLAMATION QUESTION MARK;Po;0;ON;<compat> 0021 003F;;;;N;;;;;
+204A;TIRONIAN SIGN ET;Po;0;ON;;;;;N;;;;;
+204B;REVERSED PILCROW SIGN;Po;0;ON;;;;;N;;;;;
+204C;BLACK LEFTWARDS BULLET;Po;0;ON;;;;;N;;;;;
+204D;BLACK RIGHTWARDS BULLET;Po;0;ON;;;;;N;;;;;
+204E;LOW ASTERISK;Po;0;ON;;;;;N;;;;;
+204F;REVERSED SEMICOLON;Po;0;ON;;;;;N;;;;;
+2050;CLOSE UP;Po;0;ON;;;;;N;;;;;
+2051;TWO ASTERISKS ALIGNED VERTICALLY;Po;0;ON;;;;;N;;;;;
+2052;COMMERCIAL MINUS SIGN;Sm;0;ON;;;;;N;;;;;
+2053;SWUNG DASH;Po;0;ON;;;;;N;;;;;
+2054;INVERTED UNDERTIE;Pc;0;ON;;;;;N;;;;;
+2055;FLOWER PUNCTUATION MARK;Po;0;ON;;;;;N;;;;;
+2056;THREE DOT PUNCTUATION;Po;0;ON;;;;;N;;;;;
+2057;QUADRUPLE PRIME;Po;0;ON;<compat> 2032 2032 2032 2032;;;;N;;;;;
+2058;FOUR DOT PUNCTUATION;Po;0;ON;;;;;N;;;;;
+2059;FIVE DOT PUNCTUATION;Po;0;ON;;;;;N;;;;;
+205A;TWO DOT PUNCTUATION;Po;0;ON;;;;;N;;;;;
+205B;FOUR DOT MARK;Po;0;ON;;;;;N;;;;;
+205C;DOTTED CROSS;Po;0;ON;;;;;N;;;;;
+205D;TRICOLON;Po;0;ON;;;;;N;;;;;
+205E;VERTICAL FOUR DOTS;Po;0;ON;;;;;N;;;;;
+205F;MEDIUM MATHEMATICAL SPACE;Zs;0;WS;<compat> 0020;;;;N;;;;;
+2060;WORD JOINER;Cf;0;BN;;;;;N;;;;;
+2061;FUNCTION APPLICATION;Cf;0;BN;;;;;N;;;;;
+2062;INVISIBLE TIMES;Cf;0;BN;;;;;N;;;;;
+2063;INVISIBLE SEPARATOR;Cf;0;BN;;;;;N;;;;;
+2064;INVISIBLE PLUS;Cf;0;BN;;;;;N;;;;;
+2066;LEFT-TO-RIGHT ISOLATE;Cf;0;LRI;;;;;N;;;;;
+2067;RIGHT-TO-LEFT ISOLATE;Cf;0;RLI;;;;;N;;;;;
+2068;FIRST STRONG ISOLATE;Cf;0;FSI;;;;;N;;;;;
+2069;POP DIRECTIONAL ISOLATE;Cf;0;PDI;;;;;N;;;;;
+206A;INHIBIT SYMMETRIC SWAPPING;Cf;0;BN;;;;;N;;;;;
+206B;ACTIVATE SYMMETRIC SWAPPING;Cf;0;BN;;;;;N;;;;;
+206C;INHIBIT ARABIC FORM SHAPING;Cf;0;BN;;;;;N;;;;;
+206D;ACTIVATE ARABIC FORM SHAPING;Cf;0;BN;;;;;N;;;;;
+206E;NATIONAL DIGIT SHAPES;Cf;0;BN;;;;;N;;;;;
+206F;NOMINAL DIGIT SHAPES;Cf;0;BN;;;;;N;;;;;
+2070;SUPERSCRIPT ZERO;No;0;EN;<super> 0030;;0;0;N;SUPERSCRIPT DIGIT ZERO;;;;
+2071;SUPERSCRIPT LATIN SMALL LETTER I;Lm;0;L;<super> 0069;;;;N;;;;;
+2074;SUPERSCRIPT FOUR;No;0;EN;<super> 0034;;4;4;N;SUPERSCRIPT DIGIT FOUR;;;;
+2075;SUPERSCRIPT FIVE;No;0;EN;<super> 0035;;5;5;N;SUPERSCRIPT DIGIT FIVE;;;;
+2076;SUPERSCRIPT SIX;No;0;EN;<super> 0036;;6;6;N;SUPERSCRIPT DIGIT SIX;;;;
+2077;SUPERSCRIPT SEVEN;No;0;EN;<super> 0037;;7;7;N;SUPERSCRIPT DIGIT SEVEN;;;;
+2078;SUPERSCRIPT EIGHT;No;0;EN;<super> 0038;;8;8;N;SUPERSCRIPT DIGIT EIGHT;;;;
+2079;SUPERSCRIPT NINE;No;0;EN;<super> 0039;;9;9;N;SUPERSCRIPT DIGIT NINE;;;;
+207A;SUPERSCRIPT PLUS SIGN;Sm;0;ES;<super> 002B;;;;N;;;;;
+207B;SUPERSCRIPT MINUS;Sm;0;ES;<super> 2212;;;;N;SUPERSCRIPT HYPHEN-MINUS;;;;
+207C;SUPERSCRIPT EQUALS SIGN;Sm;0;ON;<super> 003D;;;;N;;;;;
+207D;SUPERSCRIPT LEFT PARENTHESIS;Ps;0;ON;<super> 0028;;;;Y;SUPERSCRIPT OPENING PARENTHESIS;;;;
+207E;SUPERSCRIPT RIGHT PARENTHESIS;Pe;0;ON;<super> 0029;;;;Y;SUPERSCRIPT CLOSING PARENTHESIS;;;;
+207F;SUPERSCRIPT LATIN SMALL LETTER N;Lm;0;L;<super> 006E;;;;N;;;;;
+2080;SUBSCRIPT ZERO;No;0;EN;<sub> 0030;;0;0;N;SUBSCRIPT DIGIT ZERO;;;;
+2081;SUBSCRIPT ONE;No;0;EN;<sub> 0031;;1;1;N;SUBSCRIPT DIGIT ONE;;;;
+2082;SUBSCRIPT TWO;No;0;EN;<sub> 0032;;2;2;N;SUBSCRIPT DIGIT TWO;;;;
+2083;SUBSCRIPT THREE;No;0;EN;<sub> 0033;;3;3;N;SUBSCRIPT DIGIT THREE;;;;
+2084;SUBSCRIPT FOUR;No;0;EN;<sub> 0034;;4;4;N;SUBSCRIPT DIGIT FOUR;;;;
+2085;SUBSCRIPT FIVE;No;0;EN;<sub> 0035;;5;5;N;SUBSCRIPT DIGIT FIVE;;;;
+2086;SUBSCRIPT SIX;No;0;EN;<sub> 0036;;6;6;N;SUBSCRIPT DIGIT SIX;;;;
+2087;SUBSCRIPT SEVEN;No;0;EN;<sub> 0037;;7;7;N;SUBSCRIPT DIGIT SEVEN;;;;
+2088;SUBSCRIPT EIGHT;No;0;EN;<sub> 0038;;8;8;N;SUBSCRIPT DIGIT EIGHT;;;;
+2089;SUBSCRIPT NINE;No;0;EN;<sub> 0039;;9;9;N;SUBSCRIPT DIGIT NINE;;;;
+208A;SUBSCRIPT PLUS SIGN;Sm;0;ES;<sub> 002B;;;;N;;;;;
+208B;SUBSCRIPT MINUS;Sm;0;ES;<sub> 2212;;;;N;SUBSCRIPT HYPHEN-MINUS;;;;
+208C;SUBSCRIPT EQUALS SIGN;Sm;0;ON;<sub> 003D;;;;N;;;;;
+208D;SUBSCRIPT LEFT PARENTHESIS;Ps;0;ON;<sub> 0028;;;;Y;SUBSCRIPT OPENING PARENTHESIS;;;;
+208E;SUBSCRIPT RIGHT PARENTHESIS;Pe;0;ON;<sub> 0029;;;;Y;SUBSCRIPT CLOSING PARENTHESIS;;;;
+2090;LATIN SUBSCRIPT SMALL LETTER A;Lm;0;L;<sub> 0061;;;;N;;;;;
+2091;LATIN SUBSCRIPT SMALL LETTER E;Lm;0;L;<sub> 0065;;;;N;;;;;
+2092;LATIN SUBSCRIPT SMALL LETTER O;Lm;0;L;<sub> 006F;;;;N;;;;;
+2093;LATIN SUBSCRIPT SMALL LETTER X;Lm;0;L;<sub> 0078;;;;N;;;;;
+2094;LATIN SUBSCRIPT SMALL LETTER SCHWA;Lm;0;L;<sub> 0259;;;;N;;;;;
+2095;LATIN SUBSCRIPT SMALL LETTER H;Lm;0;L;<sub> 0068;;;;N;;;;;
+2096;LATIN SUBSCRIPT SMALL LETTER K;Lm;0;L;<sub> 006B;;;;N;;;;;
+2097;LATIN SUBSCRIPT SMALL LETTER L;Lm;0;L;<sub> 006C;;;;N;;;;;
+2098;LATIN SUBSCRIPT SMALL LETTER M;Lm;0;L;<sub> 006D;;;;N;;;;;
+2099;LATIN SUBSCRIPT SMALL LETTER N;Lm;0;L;<sub> 006E;;;;N;;;;;
+209A;LATIN SUBSCRIPT SMALL LETTER P;Lm;0;L;<sub> 0070;;;;N;;;;;
+209B;LATIN SUBSCRIPT SMALL LETTER S;Lm;0;L;<sub> 0073;;;;N;;;;;
+209C;LATIN SUBSCRIPT SMALL LETTER T;Lm;0;L;<sub> 0074;;;;N;;;;;
+20A0;EURO-CURRENCY SIGN;Sc;0;ET;;;;;N;;;;;
+20A1;COLON SIGN;Sc;0;ET;;;;;N;;;;;
+20A2;CRUZEIRO SIGN;Sc;0;ET;;;;;N;;;;;
+20A3;FRENCH FRANC SIGN;Sc;0;ET;;;;;N;;;;;
+20A4;LIRA SIGN;Sc;0;ET;;;;;N;;;;;
+20A5;MILL SIGN;Sc;0;ET;;;;;N;;;;;
+20A6;NAIRA SIGN;Sc;0;ET;;;;;N;;;;;
+20A7;PESETA SIGN;Sc;0;ET;;;;;N;;;;;
+20A8;RUPEE SIGN;Sc;0;ET;<compat> 0052 0073;;;;N;;;;;
+20A9;WON SIGN;Sc;0;ET;;;;;N;;;;;
+20AA;NEW SHEQEL SIGN;Sc;0;ET;;;;;N;;;;;
+20AB;DONG SIGN;Sc;0;ET;;;;;N;;;;;
+20AC;EURO SIGN;Sc;0;ET;;;;;N;;;;;
+20AD;KIP SIGN;Sc;0;ET;;;;;N;;;;;
+20AE;TUGRIK SIGN;Sc;0;ET;;;;;N;;;;;
+20AF;DRACHMA SIGN;Sc;0;ET;;;;;N;;;;;
+20B0;GERMAN PENNY SIGN;Sc;0;ET;;;;;N;;;;;
+20B1;PESO SIGN;Sc;0;ET;;;;;N;;;;;
+20B2;GUARANI SIGN;Sc;0;ET;;;;;N;;;;;
+20B3;AUSTRAL SIGN;Sc;0;ET;;;;;N;;;;;
+20B4;HRYVNIA SIGN;Sc;0;ET;;;;;N;;;;;
+20B5;CEDI SIGN;Sc;0;ET;;;;;N;;;;;
+20B6;LIVRE TOURNOIS SIGN;Sc;0;ET;;;;;N;;;;;
+20B7;SPESMILO SIGN;Sc;0;ET;;;;;N;;;;;
+20B8;TENGE SIGN;Sc;0;ET;;;;;N;;;;;
+20B9;INDIAN RUPEE SIGN;Sc;0;ET;;;;;N;;;;;
+20BA;TURKISH LIRA SIGN;Sc;0;ET;;;;;N;;;;;
+20BB;NORDIC MARK SIGN;Sc;0;ET;;;;;N;;;;;
+20BC;MANAT SIGN;Sc;0;ET;;;;;N;;;;;
+20BD;RUBLE SIGN;Sc;0;ET;;;;;N;;;;;
+20BE;LARI SIGN;Sc;0;ET;;;;;N;;;;;
+20D0;COMBINING LEFT HARPOON ABOVE;Mn;230;NSM;;;;;N;NON-SPACING LEFT HARPOON ABOVE;;;;
+20D1;COMBINING RIGHT HARPOON ABOVE;Mn;230;NSM;;;;;N;NON-SPACING RIGHT HARPOON ABOVE;;;;
+20D2;COMBINING LONG VERTICAL LINE OVERLAY;Mn;1;NSM;;;;;N;NON-SPACING LONG VERTICAL BAR OVERLAY;;;;
+20D3;COMBINING SHORT VERTICAL LINE OVERLAY;Mn;1;NSM;;;;;N;NON-SPACING SHORT VERTICAL BAR OVERLAY;;;;
+20D4;COMBINING ANTICLOCKWISE ARROW ABOVE;Mn;230;NSM;;;;;N;NON-SPACING ANTICLOCKWISE ARROW ABOVE;;;;
+20D5;COMBINING CLOCKWISE ARROW ABOVE;Mn;230;NSM;;;;;N;NON-SPACING CLOCKWISE ARROW ABOVE;;;;
+20D6;COMBINING LEFT ARROW ABOVE;Mn;230;NSM;;;;;N;NON-SPACING LEFT ARROW ABOVE;;;;
+20D7;COMBINING RIGHT ARROW ABOVE;Mn;230;NSM;;;;;N;NON-SPACING RIGHT ARROW ABOVE;;;;
+20D8;COMBINING RING OVERLAY;Mn;1;NSM;;;;;N;NON-SPACING RING OVERLAY;;;;
+20D9;COMBINING CLOCKWISE RING OVERLAY;Mn;1;NSM;;;;;N;NON-SPACING CLOCKWISE RING OVERLAY;;;;
+20DA;COMBINING ANTICLOCKWISE RING OVERLAY;Mn;1;NSM;;;;;N;NON-SPACING ANTICLOCKWISE RING OVERLAY;;;;
+20DB;COMBINING THREE DOTS ABOVE;Mn;230;NSM;;;;;N;NON-SPACING THREE DOTS ABOVE;;;;
+20DC;COMBINING FOUR DOTS ABOVE;Mn;230;NSM;;;;;N;NON-SPACING FOUR DOTS ABOVE;;;;
+20DD;COMBINING ENCLOSING CIRCLE;Me;0;NSM;;;;;N;ENCLOSING CIRCLE;;;;
+20DE;COMBINING ENCLOSING SQUARE;Me;0;NSM;;;;;N;ENCLOSING SQUARE;;;;
+20DF;COMBINING ENCLOSING DIAMOND;Me;0;NSM;;;;;N;ENCLOSING DIAMOND;;;;
+20E0;COMBINING ENCLOSING CIRCLE BACKSLASH;Me;0;NSM;;;;;N;ENCLOSING CIRCLE SLASH;;;;
+20E1;COMBINING LEFT RIGHT ARROW ABOVE;Mn;230;NSM;;;;;N;NON-SPACING LEFT RIGHT ARROW ABOVE;;;;
+20E2;COMBINING ENCLOSING SCREEN;Me;0;NSM;;;;;N;;;;;
+20E3;COMBINING ENCLOSING KEYCAP;Me;0;NSM;;;;;N;;;;;
+20E4;COMBINING ENCLOSING UPWARD POINTING TRIANGLE;Me;0;NSM;;;;;N;;;;;
+20E5;COMBINING REVERSE SOLIDUS OVERLAY;Mn;1;NSM;;;;;N;;;;;
+20E6;COMBINING DOUBLE VERTICAL STROKE OVERLAY;Mn;1;NSM;;;;;N;;;;;
+20E7;COMBINING ANNUITY SYMBOL;Mn;230;NSM;;;;;N;;;;;
+20E8;COMBINING TRIPLE UNDERDOT;Mn;220;NSM;;;;;N;;;;;
+20E9;COMBINING WIDE BRIDGE ABOVE;Mn;230;NSM;;;;;N;;;;;
+20EA;COMBINING LEFTWARDS ARROW OVERLAY;Mn;1;NSM;;;;;N;;;;;
+20EB;COMBINING LONG DOUBLE SOLIDUS OVERLAY;Mn;1;NSM;;;;;N;;;;;
+20EC;COMBINING RIGHTWARDS HARPOON WITH BARB DOWNWARDS;Mn;220;NSM;;;;;N;;;;;
+20ED;COMBINING LEFTWARDS HARPOON WITH BARB DOWNWARDS;Mn;220;NSM;;;;;N;;;;;
+20EE;COMBINING LEFT ARROW BELOW;Mn;220;NSM;;;;;N;;;;;
+20EF;COMBINING RIGHT ARROW BELOW;Mn;220;NSM;;;;;N;;;;;
+20F0;COMBINING ASTERISK ABOVE;Mn;230;NSM;;;;;N;;;;;
+2100;ACCOUNT OF;So;0;ON;<compat> 0061 002F 0063;;;;N;;;;;
+2101;ADDRESSED TO THE SUBJECT;So;0;ON;<compat> 0061 002F 0073;;;;N;;;;;
+2102;DOUBLE-STRUCK CAPITAL C;Lu;0;L;<font> 0043;;;;N;DOUBLE-STRUCK C;;;;
+2103;DEGREE CELSIUS;So;0;ON;<compat> 00B0 0043;;;;N;DEGREES CENTIGRADE;;;;
+2104;CENTRE LINE SYMBOL;So;0;ON;;;;;N;C L SYMBOL;;;;
+2105;CARE OF;So;0;ON;<compat> 0063 002F 006F;;;;N;;;;;
+2106;CADA UNA;So;0;ON;<compat> 0063 002F 0075;;;;N;;;;;
+2107;EULER CONSTANT;Lu;0;L;<compat> 0190;;;;N;EULERS;;;;
+2108;SCRUPLE;So;0;ON;;;;;N;;;;;
+2109;DEGREE FAHRENHEIT;So;0;ON;<compat> 00B0 0046;;;;N;DEGREES FAHRENHEIT;;;;
+210A;SCRIPT SMALL G;Ll;0;L;<font> 0067;;;;N;;;;;
+210B;SCRIPT CAPITAL H;Lu;0;L;<font> 0048;;;;N;SCRIPT H;;;;
+210C;BLACK-LETTER CAPITAL H;Lu;0;L;<font> 0048;;;;N;BLACK-LETTER H;;;;
+210D;DOUBLE-STRUCK CAPITAL H;Lu;0;L;<font> 0048;;;;N;DOUBLE-STRUCK H;;;;
+210E;PLANCK CONSTANT;Ll;0;L;<font> 0068;;;;N;;;;;
+210F;PLANCK CONSTANT OVER TWO PI;Ll;0;L;<font> 0127;;;;N;PLANCK CONSTANT OVER 2 PI;;;;
+2110;SCRIPT CAPITAL I;Lu;0;L;<font> 0049;;;;N;SCRIPT I;;;;
+2111;BLACK-LETTER CAPITAL I;Lu;0;L;<font> 0049;;;;N;BLACK-LETTER I;;;;
+2112;SCRIPT CAPITAL L;Lu;0;L;<font> 004C;;;;N;SCRIPT L;;;;
+2113;SCRIPT SMALL L;Ll;0;L;<font> 006C;;;;N;;;;;
+2114;L B BAR SYMBOL;So;0;ON;;;;;N;;;;;
+2115;DOUBLE-STRUCK CAPITAL N;Lu;0;L;<font> 004E;;;;N;DOUBLE-STRUCK N;;;;
+2116;NUMERO SIGN;So;0;ON;<compat> 004E 006F;;;;N;NUMERO;;;;
+2117;SOUND RECORDING COPYRIGHT;So;0;ON;;;;;N;;;;;
+2118;SCRIPT CAPITAL P;Sm;0;ON;;;;;N;SCRIPT P;;;;
+2119;DOUBLE-STRUCK CAPITAL P;Lu;0;L;<font> 0050;;;;N;DOUBLE-STRUCK P;;;;
+211A;DOUBLE-STRUCK CAPITAL Q;Lu;0;L;<font> 0051;;;;N;DOUBLE-STRUCK Q;;;;
+211B;SCRIPT CAPITAL R;Lu;0;L;<font> 0052;;;;N;SCRIPT R;;;;
+211C;BLACK-LETTER CAPITAL R;Lu;0;L;<font> 0052;;;;N;BLACK-LETTER R;;;;
+211D;DOUBLE-STRUCK CAPITAL R;Lu;0;L;<font> 0052;;;;N;DOUBLE-STRUCK R;;;;
+211E;PRESCRIPTION TAKE;So;0;ON;;;;;N;;;;;
+211F;RESPONSE;So;0;ON;;;;;N;;;;;
+2120;SERVICE MARK;So;0;ON;<super> 0053 004D;;;;N;;;;;
+2121;TELEPHONE SIGN;So;0;ON;<compat> 0054 0045 004C;;;;N;T E L SYMBOL;;;;
+2122;TRADE MARK SIGN;So;0;ON;<super> 0054 004D;;;;N;TRADEMARK;;;;
+2123;VERSICLE;So;0;ON;;;;;N;;;;;
+2124;DOUBLE-STRUCK CAPITAL Z;Lu;0;L;<font> 005A;;;;N;DOUBLE-STRUCK Z;;;;
+2125;OUNCE SIGN;So;0;ON;;;;;N;OUNCE;;;;
+2126;OHM SIGN;Lu;0;L;03A9;;;;N;OHM;;;03C9;
+2127;INVERTED OHM SIGN;So;0;ON;;;;;N;MHO;;;;
+2128;BLACK-LETTER CAPITAL Z;Lu;0;L;<font> 005A;;;;N;BLACK-LETTER Z;;;;
+2129;TURNED GREEK SMALL LETTER IOTA;So;0;ON;;;;;N;;;;;
+212A;KELVIN SIGN;Lu;0;L;004B;;;;N;DEGREES KELVIN;;;006B;
+212B;ANGSTROM SIGN;Lu;0;L;00C5;;;;N;ANGSTROM UNIT;;;00E5;
+212C;SCRIPT CAPITAL B;Lu;0;L;<font> 0042;;;;N;SCRIPT B;;;;
+212D;BLACK-LETTER CAPITAL C;Lu;0;L;<font> 0043;;;;N;BLACK-LETTER C;;;;
+212E;ESTIMATED SYMBOL;So;0;ET;;;;;N;;;;;
+212F;SCRIPT SMALL E;Ll;0;L;<font> 0065;;;;N;;;;;
+2130;SCRIPT CAPITAL E;Lu;0;L;<font> 0045;;;;N;SCRIPT E;;;;
+2131;SCRIPT CAPITAL F;Lu;0;L;<font> 0046;;;;N;SCRIPT F;;;;
+2132;TURNED CAPITAL F;Lu;0;L;;;;;N;TURNED F;;;214E;
+2133;SCRIPT CAPITAL M;Lu;0;L;<font> 004D;;;;N;SCRIPT M;;;;
+2134;SCRIPT SMALL O;Ll;0;L;<font> 006F;;;;N;;;;;
+2135;ALEF SYMBOL;Lo;0;L;<compat> 05D0;;;;N;FIRST TRANSFINITE CARDINAL;;;;
+2136;BET SYMBOL;Lo;0;L;<compat> 05D1;;;;N;SECOND TRANSFINITE CARDINAL;;;;
+2137;GIMEL SYMBOL;Lo;0;L;<compat> 05D2;;;;N;THIRD TRANSFINITE CARDINAL;;;;
+2138;DALET SYMBOL;Lo;0;L;<compat> 05D3;;;;N;FOURTH TRANSFINITE CARDINAL;;;;
+2139;INFORMATION SOURCE;Ll;0;L;<font> 0069;;;;N;;;;;
+213A;ROTATED CAPITAL Q;So;0;ON;;;;;N;;;;;
+213B;FACSIMILE SIGN;So;0;ON;<compat> 0046 0041 0058;;;;N;;;;;
+213C;DOUBLE-STRUCK SMALL PI;Ll;0;L;<font> 03C0;;;;N;;;;;
+213D;DOUBLE-STRUCK SMALL GAMMA;Ll;0;L;<font> 03B3;;;;N;;;;;
+213E;DOUBLE-STRUCK CAPITAL GAMMA;Lu;0;L;<font> 0393;;;;N;;;;;
+213F;DOUBLE-STRUCK CAPITAL PI;Lu;0;L;<font> 03A0;;;;N;;;;;
+2140;DOUBLE-STRUCK N-ARY SUMMATION;Sm;0;ON;<font> 2211;;;;Y;;;;;
+2141;TURNED SANS-SERIF CAPITAL G;Sm;0;ON;;;;;N;;;;;
+2142;TURNED SANS-SERIF CAPITAL L;Sm;0;ON;;;;;N;;;;;
+2143;REVERSED SANS-SERIF CAPITAL L;Sm;0;ON;;;;;N;;;;;
+2144;TURNED SANS-SERIF CAPITAL Y;Sm;0;ON;;;;;N;;;;;
+2145;DOUBLE-STRUCK ITALIC CAPITAL D;Lu;0;L;<font> 0044;;;;N;;;;;
+2146;DOUBLE-STRUCK ITALIC SMALL D;Ll;0;L;<font> 0064;;;;N;;;;;
+2147;DOUBLE-STRUCK ITALIC SMALL E;Ll;0;L;<font> 0065;;;;N;;;;;
+2148;DOUBLE-STRUCK ITALIC SMALL I;Ll;0;L;<font> 0069;;;;N;;;;;
+2149;DOUBLE-STRUCK ITALIC SMALL J;Ll;0;L;<font> 006A;;;;N;;;;;
+214A;PROPERTY LINE;So;0;ON;;;;;N;;;;;
+214B;TURNED AMPERSAND;Sm;0;ON;;;;;N;;;;;
+214C;PER SIGN;So;0;ON;;;;;N;;;;;
+214D;AKTIESELSKAB;So;0;ON;;;;;N;;;;;
+214E;TURNED SMALL F;Ll;0;L;;;;;N;;;2132;;2132
+214F;SYMBOL FOR SAMARITAN SOURCE;So;0;L;;;;;N;;;;;
+2150;VULGAR FRACTION ONE SEVENTH;No;0;ON;<fraction> 0031 2044 0037;;;1/7;N;;;;;
+2151;VULGAR FRACTION ONE NINTH;No;0;ON;<fraction> 0031 2044 0039;;;1/9;N;;;;;
+2152;VULGAR FRACTION ONE TENTH;No;0;ON;<fraction> 0031 2044 0031 0030;;;1/10;N;;;;;
+2153;VULGAR FRACTION ONE THIRD;No;0;ON;<fraction> 0031 2044 0033;;;1/3;N;FRACTION ONE THIRD;;;;
+2154;VULGAR FRACTION TWO THIRDS;No;0;ON;<fraction> 0032 2044 0033;;;2/3;N;FRACTION TWO THIRDS;;;;
+2155;VULGAR FRACTION ONE FIFTH;No;0;ON;<fraction> 0031 2044 0035;;;1/5;N;FRACTION ONE FIFTH;;;;
+2156;VULGAR FRACTION TWO FIFTHS;No;0;ON;<fraction> 0032 2044 0035;;;2/5;N;FRACTION TWO FIFTHS;;;;
+2157;VULGAR FRACTION THREE FIFTHS;No;0;ON;<fraction> 0033 2044 0035;;;3/5;N;FRACTION THREE FIFTHS;;;;
+2158;VULGAR FRACTION FOUR FIFTHS;No;0;ON;<fraction> 0034 2044 0035;;;4/5;N;FRACTION FOUR FIFTHS;;;;
+2159;VULGAR FRACTION ONE SIXTH;No;0;ON;<fraction> 0031 2044 0036;;;1/6;N;FRACTION ONE SIXTH;;;;
+215A;VULGAR FRACTION FIVE SIXTHS;No;0;ON;<fraction> 0035 2044 0036;;;5/6;N;FRACTION FIVE SIXTHS;;;;
+215B;VULGAR FRACTION ONE EIGHTH;No;0;ON;<fraction> 0031 2044 0038;;;1/8;N;FRACTION ONE EIGHTH;;;;
+215C;VULGAR FRACTION THREE EIGHTHS;No;0;ON;<fraction> 0033 2044 0038;;;3/8;N;FRACTION THREE EIGHTHS;;;;
+215D;VULGAR FRACTION FIVE EIGHTHS;No;0;ON;<fraction> 0035 2044 0038;;;5/8;N;FRACTION FIVE EIGHTHS;;;;
+215E;VULGAR FRACTION SEVEN EIGHTHS;No;0;ON;<fraction> 0037 2044 0038;;;7/8;N;FRACTION SEVEN EIGHTHS;;;;
+215F;FRACTION NUMERATOR ONE;No;0;ON;<fraction> 0031 2044;;;1;N;;;;;
+2160;ROMAN NUMERAL ONE;Nl;0;L;<compat> 0049;;;1;N;;;;2170;
+2161;ROMAN NUMERAL TWO;Nl;0;L;<compat> 0049 0049;;;2;N;;;;2171;
+2162;ROMAN NUMERAL THREE;Nl;0;L;<compat> 0049 0049 0049;;;3;N;;;;2172;
+2163;ROMAN NUMERAL FOUR;Nl;0;L;<compat> 0049 0056;;;4;N;;;;2173;
+2164;ROMAN NUMERAL FIVE;Nl;0;L;<compat> 0056;;;5;N;;;;2174;
+2165;ROMAN NUMERAL SIX;Nl;0;L;<compat> 0056 0049;;;6;N;;;;2175;
+2166;ROMAN NUMERAL SEVEN;Nl;0;L;<compat> 0056 0049 0049;;;7;N;;;;2176;
+2167;ROMAN NUMERAL EIGHT;Nl;0;L;<compat> 0056 0049 0049 0049;;;8;N;;;;2177;
+2168;ROMAN NUMERAL NINE;Nl;0;L;<compat> 0049 0058;;;9;N;;;;2178;
+2169;ROMAN NUMERAL TEN;Nl;0;L;<compat> 0058;;;10;N;;;;2179;
+216A;ROMAN NUMERAL ELEVEN;Nl;0;L;<compat> 0058 0049;;;11;N;;;;217A;
+216B;ROMAN NUMERAL TWELVE;Nl;0;L;<compat> 0058 0049 0049;;;12;N;;;;217B;
+216C;ROMAN NUMERAL FIFTY;Nl;0;L;<compat> 004C;;;50;N;;;;217C;
+216D;ROMAN NUMERAL ONE HUNDRED;Nl;0;L;<compat> 0043;;;100;N;;;;217D;
+216E;ROMAN NUMERAL FIVE HUNDRED;Nl;0;L;<compat> 0044;;;500;N;;;;217E;
+216F;ROMAN NUMERAL ONE THOUSAND;Nl;0;L;<compat> 004D;;;1000;N;;;;217F;
+2170;SMALL ROMAN NUMERAL ONE;Nl;0;L;<compat> 0069;;;1;N;;;2160;;2160
+2171;SMALL ROMAN NUMERAL TWO;Nl;0;L;<compat> 0069 0069;;;2;N;;;2161;;2161
+2172;SMALL ROMAN NUMERAL THREE;Nl;0;L;<compat> 0069 0069 0069;;;3;N;;;2162;;2162
+2173;SMALL ROMAN NUMERAL FOUR;Nl;0;L;<compat> 0069 0076;;;4;N;;;2163;;2163
+2174;SMALL ROMAN NUMERAL FIVE;Nl;0;L;<compat> 0076;;;5;N;;;2164;;2164
+2175;SMALL ROMAN NUMERAL SIX;Nl;0;L;<compat> 0076 0069;;;6;N;;;2165;;2165
+2176;SMALL ROMAN NUMERAL SEVEN;Nl;0;L;<compat> 0076 0069 0069;;;7;N;;;2166;;2166
+2177;SMALL ROMAN NUMERAL EIGHT;Nl;0;L;<compat> 0076 0069 0069 0069;;;8;N;;;2167;;2167
+2178;SMALL ROMAN NUMERAL NINE;Nl;0;L;<compat> 0069 0078;;;9;N;;;2168;;2168
+2179;SMALL ROMAN NUMERAL TEN;Nl;0;L;<compat> 0078;;;10;N;;;2169;;2169
+217A;SMALL ROMAN NUMERAL ELEVEN;Nl;0;L;<compat> 0078 0069;;;11;N;;;216A;;216A
+217B;SMALL ROMAN NUMERAL TWELVE;Nl;0;L;<compat> 0078 0069 0069;;;12;N;;;216B;;216B
+217C;SMALL ROMAN NUMERAL FIFTY;Nl;0;L;<compat> 006C;;;50;N;;;216C;;216C
+217D;SMALL ROMAN NUMERAL ONE HUNDRED;Nl;0;L;<compat> 0063;;;100;N;;;216D;;216D
+217E;SMALL ROMAN NUMERAL FIVE HUNDRED;Nl;0;L;<compat> 0064;;;500;N;;;216E;;216E
+217F;SMALL ROMAN NUMERAL ONE THOUSAND;Nl;0;L;<compat> 006D;;;1000;N;;;216F;;216F
+2180;ROMAN NUMERAL ONE THOUSAND C D;Nl;0;L;;;;1000;N;;;;;
+2181;ROMAN NUMERAL FIVE THOUSAND;Nl;0;L;;;;5000;N;;;;;
+2182;ROMAN NUMERAL TEN THOUSAND;Nl;0;L;;;;10000;N;;;;;
+2183;ROMAN NUMERAL REVERSED ONE HUNDRED;Lu;0;L;;;;;N;;;;2184;
+2184;LATIN SMALL LETTER REVERSED C;Ll;0;L;;;;;N;;;2183;;2183
+2185;ROMAN NUMERAL SIX LATE FORM;Nl;0;L;;;;6;N;;;;;
+2186;ROMAN NUMERAL FIFTY EARLY FORM;Nl;0;L;;;;50;N;;;;;
+2187;ROMAN NUMERAL FIFTY THOUSAND;Nl;0;L;;;;50000;N;;;;;
+2188;ROMAN NUMERAL ONE HUNDRED THOUSAND;Nl;0;L;;;;100000;N;;;;;
+2189;VULGAR FRACTION ZERO THIRDS;No;0;ON;<fraction> 0030 2044 0033;;;0;N;;;;;
+218A;TURNED DIGIT TWO;So;0;ON;;;;;N;;;;;
+218B;TURNED DIGIT THREE;So;0;ON;;;;;N;;;;;
+2190;LEFTWARDS ARROW;Sm;0;ON;;;;;N;LEFT ARROW;;;;
+2191;UPWARDS ARROW;Sm;0;ON;;;;;N;UP ARROW;;;;
+2192;RIGHTWARDS ARROW;Sm;0;ON;;;;;N;RIGHT ARROW;;;;
+2193;DOWNWARDS ARROW;Sm;0;ON;;;;;N;DOWN ARROW;;;;
+2194;LEFT RIGHT ARROW;Sm;0;ON;;;;;N;;;;;
+2195;UP DOWN ARROW;So;0;ON;;;;;N;;;;;
+2196;NORTH WEST ARROW;So;0;ON;;;;;N;UPPER LEFT ARROW;;;;
+2197;NORTH EAST ARROW;So;0;ON;;;;;N;UPPER RIGHT ARROW;;;;
+2198;SOUTH EAST ARROW;So;0;ON;;;;;N;LOWER RIGHT ARROW;;;;
+2199;SOUTH WEST ARROW;So;0;ON;;;;;N;LOWER LEFT ARROW;;;;
+219A;LEFTWARDS ARROW WITH STROKE;Sm;0;ON;2190 0338;;;;N;LEFT ARROW WITH STROKE;;;;
+219B;RIGHTWARDS ARROW WITH STROKE;Sm;0;ON;2192 0338;;;;N;RIGHT ARROW WITH STROKE;;;;
+219C;LEFTWARDS WAVE ARROW;So;0;ON;;;;;N;LEFT WAVE ARROW;;;;
+219D;RIGHTWARDS WAVE ARROW;So;0;ON;;;;;N;RIGHT WAVE ARROW;;;;
+219E;LEFTWARDS TWO HEADED ARROW;So;0;ON;;;;;N;LEFT TWO HEADED ARROW;;;;
+219F;UPWARDS TWO HEADED ARROW;So;0;ON;;;;;N;UP TWO HEADED ARROW;;;;
+21A0;RIGHTWARDS TWO HEADED ARROW;Sm;0;ON;;;;;N;RIGHT TWO HEADED ARROW;;;;
+21A1;DOWNWARDS TWO HEADED ARROW;So;0;ON;;;;;N;DOWN TWO HEADED ARROW;;;;
+21A2;LEFTWARDS ARROW WITH TAIL;So;0;ON;;;;;N;LEFT ARROW WITH TAIL;;;;
+21A3;RIGHTWARDS ARROW WITH TAIL;Sm;0;ON;;;;;N;RIGHT ARROW WITH TAIL;;;;
+21A4;LEFTWARDS ARROW FROM BAR;So;0;ON;;;;;N;LEFT ARROW FROM BAR;;;;
+21A5;UPWARDS ARROW FROM BAR;So;0;ON;;;;;N;UP ARROW FROM BAR;;;;
+21A6;RIGHTWARDS ARROW FROM BAR;Sm;0;ON;;;;;N;RIGHT ARROW FROM BAR;;;;
+21A7;DOWNWARDS ARROW FROM BAR;So;0;ON;;;;;N;DOWN ARROW FROM BAR;;;;
+21A8;UP DOWN ARROW WITH BASE;So;0;ON;;;;;N;;;;;
+21A9;LEFTWARDS ARROW WITH HOOK;So;0;ON;;;;;N;LEFT ARROW WITH HOOK;;;;
+21AA;RIGHTWARDS ARROW WITH HOOK;So;0;ON;;;;;N;RIGHT ARROW WITH HOOK;;;;
+21AB;LEFTWARDS ARROW WITH LOOP;So;0;ON;;;;;N;LEFT ARROW WITH LOOP;;;;
+21AC;RIGHTWARDS ARROW WITH LOOP;So;0;ON;;;;;N;RIGHT ARROW WITH LOOP;;;;
+21AD;LEFT RIGHT WAVE ARROW;So;0;ON;;;;;N;;;;;
+21AE;LEFT RIGHT ARROW WITH STROKE;Sm;0;ON;2194 0338;;;;N;;;;;
+21AF;DOWNWARDS ZIGZAG ARROW;So;0;ON;;;;;N;DOWN ZIGZAG ARROW;;;;
+21B0;UPWARDS ARROW WITH TIP LEFTWARDS;So;0;ON;;;;;N;UP ARROW WITH TIP LEFT;;;;
+21B1;UPWARDS ARROW WITH TIP RIGHTWARDS;So;0;ON;;;;;N;UP ARROW WITH TIP RIGHT;;;;
+21B2;DOWNWARDS ARROW WITH TIP LEFTWARDS;So;0;ON;;;;;N;DOWN ARROW WITH TIP LEFT;;;;
+21B3;DOWNWARDS ARROW WITH TIP RIGHTWARDS;So;0;ON;;;;;N;DOWN ARROW WITH TIP RIGHT;;;;
+21B4;RIGHTWARDS ARROW WITH CORNER DOWNWARDS;So;0;ON;;;;;N;RIGHT ARROW WITH CORNER DOWN;;;;
+21B5;DOWNWARDS ARROW WITH CORNER LEFTWARDS;So;0;ON;;;;;N;DOWN ARROW WITH CORNER LEFT;;;;
+21B6;ANTICLOCKWISE TOP SEMICIRCLE ARROW;So;0;ON;;;;;N;;;;;
+21B7;CLOCKWISE TOP SEMICIRCLE ARROW;So;0;ON;;;;;N;;;;;
+21B8;NORTH WEST ARROW TO LONG BAR;So;0;ON;;;;;N;UPPER LEFT ARROW TO LONG BAR;;;;
+21B9;LEFTWARDS ARROW TO BAR OVER RIGHTWARDS ARROW TO BAR;So;0;ON;;;;;N;LEFT ARROW TO BAR OVER RIGHT ARROW TO BAR;;;;
+21BA;ANTICLOCKWISE OPEN CIRCLE ARROW;So;0;ON;;;;;N;;;;;
+21BB;CLOCKWISE OPEN CIRCLE ARROW;So;0;ON;;;;;N;;;;;
+21BC;LEFTWARDS HARPOON WITH BARB UPWARDS;So;0;ON;;;;;N;LEFT HARPOON WITH BARB UP;;;;
+21BD;LEFTWARDS HARPOON WITH BARB DOWNWARDS;So;0;ON;;;;;N;LEFT HARPOON WITH BARB DOWN;;;;
+21BE;UPWARDS HARPOON WITH BARB RIGHTWARDS;So;0;ON;;;;;N;UP HARPOON WITH BARB RIGHT;;;;
+21BF;UPWARDS HARPOON WITH BARB LEFTWARDS;So;0;ON;;;;;N;UP HARPOON WITH BARB LEFT;;;;
+21C0;RIGHTWARDS HARPOON WITH BARB UPWARDS;So;0;ON;;;;;N;RIGHT HARPOON WITH BARB UP;;;;
+21C1;RIGHTWARDS HARPOON WITH BARB DOWNWARDS;So;0;ON;;;;;N;RIGHT HARPOON WITH BARB DOWN;;;;
+21C2;DOWNWARDS HARPOON WITH BARB RIGHTWARDS;So;0;ON;;;;;N;DOWN HARPOON WITH BARB RIGHT;;;;
+21C3;DOWNWARDS HARPOON WITH BARB LEFTWARDS;So;0;ON;;;;;N;DOWN HARPOON WITH BARB LEFT;;;;
+21C4;RIGHTWARDS ARROW OVER LEFTWARDS ARROW;So;0;ON;;;;;N;RIGHT ARROW OVER LEFT ARROW;;;;
+21C5;UPWARDS ARROW LEFTWARDS OF DOWNWARDS ARROW;So;0;ON;;;;;N;UP ARROW LEFT OF DOWN ARROW;;;;
+21C6;LEFTWARDS ARROW OVER RIGHTWARDS ARROW;So;0;ON;;;;;N;LEFT ARROW OVER RIGHT ARROW;;;;
+21C7;LEFTWARDS PAIRED ARROWS;So;0;ON;;;;;N;LEFT PAIRED ARROWS;;;;
+21C8;UPWARDS PAIRED ARROWS;So;0;ON;;;;;N;UP PAIRED ARROWS;;;;
+21C9;RIGHTWARDS PAIRED ARROWS;So;0;ON;;;;;N;RIGHT PAIRED ARROWS;;;;
+21CA;DOWNWARDS PAIRED ARROWS;So;0;ON;;;;;N;DOWN PAIRED ARROWS;;;;
+21CB;LEFTWARDS HARPOON OVER RIGHTWARDS HARPOON;So;0;ON;;;;;N;LEFT HARPOON OVER RIGHT HARPOON;;;;
+21CC;RIGHTWARDS HARPOON OVER LEFTWARDS HARPOON;So;0;ON;;;;;N;RIGHT HARPOON OVER LEFT HARPOON;;;;
+21CD;LEFTWARDS DOUBLE ARROW WITH STROKE;So;0;ON;21D0 0338;;;;N;LEFT DOUBLE ARROW WITH STROKE;;;;
+21CE;LEFT RIGHT DOUBLE ARROW WITH STROKE;Sm;0;ON;21D4 0338;;;;N;;;;;
+21CF;RIGHTWARDS DOUBLE ARROW WITH STROKE;Sm;0;ON;21D2 0338;;;;N;RIGHT DOUBLE ARROW WITH STROKE;;;;
+21D0;LEFTWARDS DOUBLE ARROW;So;0;ON;;;;;N;LEFT DOUBLE ARROW;;;;
+21D1;UPWARDS DOUBLE ARROW;So;0;ON;;;;;N;UP DOUBLE ARROW;;;;
+21D2;RIGHTWARDS DOUBLE ARROW;Sm;0;ON;;;;;N;RIGHT DOUBLE ARROW;;;;
+21D3;DOWNWARDS DOUBLE ARROW;So;0;ON;;;;;N;DOWN DOUBLE ARROW;;;;
+21D4;LEFT RIGHT DOUBLE ARROW;Sm;0;ON;;;;;N;;;;;
+21D5;UP DOWN DOUBLE ARROW;So;0;ON;;;;;N;;;;;
+21D6;NORTH WEST DOUBLE ARROW;So;0;ON;;;;;N;UPPER LEFT DOUBLE ARROW;;;;
+21D7;NORTH EAST DOUBLE ARROW;So;0;ON;;;;;N;UPPER RIGHT DOUBLE ARROW;;;;
+21D8;SOUTH EAST DOUBLE ARROW;So;0;ON;;;;;N;LOWER RIGHT DOUBLE ARROW;;;;
+21D9;SOUTH WEST DOUBLE ARROW;So;0;ON;;;;;N;LOWER LEFT DOUBLE ARROW;;;;
+21DA;LEFTWARDS TRIPLE ARROW;So;0;ON;;;;;N;LEFT TRIPLE ARROW;;;;
+21DB;RIGHTWARDS TRIPLE ARROW;So;0;ON;;;;;N;RIGHT TRIPLE ARROW;;;;
+21DC;LEFTWARDS SQUIGGLE ARROW;So;0;ON;;;;;N;LEFT SQUIGGLE ARROW;;;;
+21DD;RIGHTWARDS SQUIGGLE ARROW;So;0;ON;;;;;N;RIGHT SQUIGGLE ARROW;;;;
+21DE;UPWARDS ARROW WITH DOUBLE STROKE;So;0;ON;;;;;N;UP ARROW WITH DOUBLE STROKE;;;;
+21DF;DOWNWARDS ARROW WITH DOUBLE STROKE;So;0;ON;;;;;N;DOWN ARROW WITH DOUBLE STROKE;;;;
+21E0;LEFTWARDS DASHED ARROW;So;0;ON;;;;;N;LEFT DASHED ARROW;;;;
+21E1;UPWARDS DASHED ARROW;So;0;ON;;;;;N;UP DASHED ARROW;;;;
+21E2;RIGHTWARDS DASHED ARROW;So;0;ON;;;;;N;RIGHT DASHED ARROW;;;;
+21E3;DOWNWARDS DASHED ARROW;So;0;ON;;;;;N;DOWN DASHED ARROW;;;;
+21E4;LEFTWARDS ARROW TO BAR;So;0;ON;;;;;N;LEFT ARROW TO BAR;;;;
+21E5;RIGHTWARDS ARROW TO BAR;So;0;ON;;;;;N;RIGHT ARROW TO BAR;;;;
+21E6;LEFTWARDS WHITE ARROW;So;0;ON;;;;;N;WHITE LEFT ARROW;;;;
+21E7;UPWARDS WHITE ARROW;So;0;ON;;;;;N;WHITE UP ARROW;;;;
+21E8;RIGHTWARDS WHITE ARROW;So;0;ON;;;;;N;WHITE RIGHT ARROW;;;;
+21E9;DOWNWARDS WHITE ARROW;So;0;ON;;;;;N;WHITE DOWN ARROW;;;;
+21EA;UPWARDS WHITE ARROW FROM BAR;So;0;ON;;;;;N;WHITE UP ARROW FROM BAR;;;;
+21EB;UPWARDS WHITE ARROW ON PEDESTAL;So;0;ON;;;;;N;;;;;
+21EC;UPWARDS WHITE ARROW ON PEDESTAL WITH HORIZONTAL BAR;So;0;ON;;;;;N;;;;;
+21ED;UPWARDS WHITE ARROW ON PEDESTAL WITH VERTICAL BAR;So;0;ON;;;;;N;;;;;
+21EE;UPWARDS WHITE DOUBLE ARROW;So;0;ON;;;;;N;;;;;
+21EF;UPWARDS WHITE DOUBLE ARROW ON PEDESTAL;So;0;ON;;;;;N;;;;;
+21F0;RIGHTWARDS WHITE ARROW FROM WALL;So;0;ON;;;;;N;;;;;
+21F1;NORTH WEST ARROW TO CORNER;So;0;ON;;;;;N;;;;;
+21F2;SOUTH EAST ARROW TO CORNER;So;0;ON;;;;;N;;;;;
+21F3;UP DOWN WHITE ARROW;So;0;ON;;;;;N;;;;;
+21F4;RIGHT ARROW WITH SMALL CIRCLE;Sm;0;ON;;;;;N;;;;;
+21F5;DOWNWARDS ARROW LEFTWARDS OF UPWARDS ARROW;Sm;0;ON;;;;;N;;;;;
+21F6;THREE RIGHTWARDS ARROWS;Sm;0;ON;;;;;N;;;;;
+21F7;LEFTWARDS ARROW WITH VERTICAL STROKE;Sm;0;ON;;;;;N;;;;;
+21F8;RIGHTWARDS ARROW WITH VERTICAL STROKE;Sm;0;ON;;;;;N;;;;;
+21F9;LEFT RIGHT ARROW WITH VERTICAL STROKE;Sm;0;ON;;;;;N;;;;;
+21FA;LEFTWARDS ARROW WITH DOUBLE VERTICAL STROKE;Sm;0;ON;;;;;N;;;;;
+21FB;RIGHTWARDS ARROW WITH DOUBLE VERTICAL STROKE;Sm;0;ON;;;;;N;;;;;
+21FC;LEFT RIGHT ARROW WITH DOUBLE VERTICAL STROKE;Sm;0;ON;;;;;N;;;;;
+21FD;LEFTWARDS OPEN-HEADED ARROW;Sm;0;ON;;;;;N;;;;;
+21FE;RIGHTWARDS OPEN-HEADED ARROW;Sm;0;ON;;;;;N;;;;;
+21FF;LEFT RIGHT OPEN-HEADED ARROW;Sm;0;ON;;;;;N;;;;;
+2200;FOR ALL;Sm;0;ON;;;;;N;;;;;
+2201;COMPLEMENT;Sm;0;ON;;;;;Y;;;;;
+2202;PARTIAL DIFFERENTIAL;Sm;0;ON;;;;;Y;;;;;
+2203;THERE EXISTS;Sm;0;ON;;;;;Y;;;;;
+2204;THERE DOES NOT EXIST;Sm;0;ON;2203 0338;;;;Y;;;;;
+2205;EMPTY SET;Sm;0;ON;;;;;N;;;;;
+2206;INCREMENT;Sm;0;ON;;;;;N;;;;;
+2207;NABLA;Sm;0;ON;;;;;N;;;;;
+2208;ELEMENT OF;Sm;0;ON;;;;;Y;;;;;
+2209;NOT AN ELEMENT OF;Sm;0;ON;2208 0338;;;;Y;;;;;
+220A;SMALL ELEMENT OF;Sm;0;ON;;;;;Y;;;;;
+220B;CONTAINS AS MEMBER;Sm;0;ON;;;;;Y;;;;;
+220C;DOES NOT CONTAIN AS MEMBER;Sm;0;ON;220B 0338;;;;Y;;;;;
+220D;SMALL CONTAINS AS MEMBER;Sm;0;ON;;;;;Y;;;;;
+220E;END OF PROOF;Sm;0;ON;;;;;N;;;;;
+220F;N-ARY PRODUCT;Sm;0;ON;;;;;N;;;;;
+2210;N-ARY COPRODUCT;Sm;0;ON;;;;;N;;;;;
+2211;N-ARY SUMMATION;Sm;0;ON;;;;;Y;;;;;
+2212;MINUS SIGN;Sm;0;ES;;;;;N;;;;;
+2213;MINUS-OR-PLUS SIGN;Sm;0;ET;;;;;N;;;;;
+2214;DOT PLUS;Sm;0;ON;;;;;N;;;;;
+2215;DIVISION SLASH;Sm;0;ON;;;;;Y;;;;;
+2216;SET MINUS;Sm;0;ON;;;;;Y;;;;;
+2217;ASTERISK OPERATOR;Sm;0;ON;;;;;N;;;;;
+2218;RING OPERATOR;Sm;0;ON;;;;;N;;;;;
+2219;BULLET OPERATOR;Sm;0;ON;;;;;N;;;;;
+221A;SQUARE ROOT;Sm;0;ON;;;;;Y;;;;;
+221B;CUBE ROOT;Sm;0;ON;;;;;Y;;;;;
+221C;FOURTH ROOT;Sm;0;ON;;;;;Y;;;;;
+221D;PROPORTIONAL TO;Sm;0;ON;;;;;Y;;;;;
+221E;INFINITY;Sm;0;ON;;;;;N;;;;;
+221F;RIGHT ANGLE;Sm;0;ON;;;;;Y;;;;;
+2220;ANGLE;Sm;0;ON;;;;;Y;;;;;
+2221;MEASURED ANGLE;Sm;0;ON;;;;;Y;;;;;
+2222;SPHERICAL ANGLE;Sm;0;ON;;;;;Y;;;;;
+2223;DIVIDES;Sm;0;ON;;;;;N;;;;;
+2224;DOES NOT DIVIDE;Sm;0;ON;2223 0338;;;;Y;;;;;
+2225;PARALLEL TO;Sm;0;ON;;;;;N;;;;;
+2226;NOT PARALLEL TO;Sm;0;ON;2225 0338;;;;Y;;;;;
+2227;LOGICAL AND;Sm;0;ON;;;;;N;;;;;
+2228;LOGICAL OR;Sm;0;ON;;;;;N;;;;;
+2229;INTERSECTION;Sm;0;ON;;;;;N;;;;;
+222A;UNION;Sm;0;ON;;;;;N;;;;;
+222B;INTEGRAL;Sm;0;ON;;;;;Y;;;;;
+222C;DOUBLE INTEGRAL;Sm;0;ON;<compat> 222B 222B;;;;Y;;;;;
+222D;TRIPLE INTEGRAL;Sm;0;ON;<compat> 222B 222B 222B;;;;Y;;;;;
+222E;CONTOUR INTEGRAL;Sm;0;ON;;;;;Y;;;;;
+222F;SURFACE INTEGRAL;Sm;0;ON;<compat> 222E 222E;;;;Y;;;;;
+2230;VOLUME INTEGRAL;Sm;0;ON;<compat> 222E 222E 222E;;;;Y;;;;;
+2231;CLOCKWISE INTEGRAL;Sm;0;ON;;;;;Y;;;;;
+2232;CLOCKWISE CONTOUR INTEGRAL;Sm;0;ON;;;;;Y;;;;;
+2233;ANTICLOCKWISE CONTOUR INTEGRAL;Sm;0;ON;;;;;Y;;;;;
+2234;THEREFORE;Sm;0;ON;;;;;N;;;;;
+2235;BECAUSE;Sm;0;ON;;;;;N;;;;;
+2236;RATIO;Sm;0;ON;;;;;N;;;;;
+2237;PROPORTION;Sm;0;ON;;;;;N;;;;;
+2238;DOT MINUS;Sm;0;ON;;;;;N;;;;;
+2239;EXCESS;Sm;0;ON;;;;;Y;;;;;
+223A;GEOMETRIC PROPORTION;Sm;0;ON;;;;;N;;;;;
+223B;HOMOTHETIC;Sm;0;ON;;;;;Y;;;;;
+223C;TILDE OPERATOR;Sm;0;ON;;;;;Y;;;;;
+223D;REVERSED TILDE;Sm;0;ON;;;;;Y;;;;;
+223E;INVERTED LAZY S;Sm;0;ON;;;;;Y;;;;;
+223F;SINE WAVE;Sm;0;ON;;;;;Y;;;;;
+2240;WREATH PRODUCT;Sm;0;ON;;;;;Y;;;;;
+2241;NOT TILDE;Sm;0;ON;223C 0338;;;;Y;;;;;
+2242;MINUS TILDE;Sm;0;ON;;;;;Y;;;;;
+2243;ASYMPTOTICALLY EQUAL TO;Sm;0;ON;;;;;Y;;;;;
+2244;NOT ASYMPTOTICALLY EQUAL TO;Sm;0;ON;2243 0338;;;;Y;;;;;
+2245;APPROXIMATELY EQUAL TO;Sm;0;ON;;;;;Y;;;;;
+2246;APPROXIMATELY BUT NOT ACTUALLY EQUAL TO;Sm;0;ON;;;;;Y;;;;;
+2247;NEITHER APPROXIMATELY NOR ACTUALLY EQUAL TO;Sm;0;ON;2245 0338;;;;Y;;;;;
+2248;ALMOST EQUAL TO;Sm;0;ON;;;;;Y;;;;;
+2249;NOT ALMOST EQUAL TO;Sm;0;ON;2248 0338;;;;Y;;;;;
+224A;ALMOST EQUAL OR EQUAL TO;Sm;0;ON;;;;;Y;;;;;
+224B;TRIPLE TILDE;Sm;0;ON;;;;;Y;;;;;
+224C;ALL EQUAL TO;Sm;0;ON;;;;;Y;;;;;
+224D;EQUIVALENT TO;Sm;0;ON;;;;;N;;;;;
+224E;GEOMETRICALLY EQUIVALENT TO;Sm;0;ON;;;;;N;;;;;
+224F;DIFFERENCE BETWEEN;Sm;0;ON;;;;;N;;;;;
+2250;APPROACHES THE LIMIT;Sm;0;ON;;;;;N;;;;;
+2251;GEOMETRICALLY EQUAL TO;Sm;0;ON;;;;;N;;;;;
+2252;APPROXIMATELY EQUAL TO OR THE IMAGE OF;Sm;0;ON;;;;;Y;;;;;
+2253;IMAGE OF OR APPROXIMATELY EQUAL TO;Sm;0;ON;;;;;Y;;;;;
+2254;COLON EQUALS;Sm;0;ON;;;;;Y;COLON EQUAL;;;;
+2255;EQUALS COLON;Sm;0;ON;;;;;Y;EQUAL COLON;;;;
+2256;RING IN EQUAL TO;Sm;0;ON;;;;;N;;;;;
+2257;RING EQUAL TO;Sm;0;ON;;;;;N;;;;;
+2258;CORRESPONDS TO;Sm;0;ON;;;;;N;;;;;
+2259;ESTIMATES;Sm;0;ON;;;;;N;;;;;
+225A;EQUIANGULAR TO;Sm;0;ON;;;;;N;;;;;
+225B;STAR EQUALS;Sm;0;ON;;;;;N;;;;;
+225C;DELTA EQUAL TO;Sm;0;ON;;;;;N;;;;;
+225D;EQUAL TO BY DEFINITION;Sm;0;ON;;;;;N;;;;;
+225E;MEASURED BY;Sm;0;ON;;;;;N;;;;;
+225F;QUESTIONED EQUAL TO;Sm;0;ON;;;;;Y;;;;;
+2260;NOT EQUAL TO;Sm;0;ON;003D 0338;;;;Y;;;;;
+2261;IDENTICAL TO;Sm;0;ON;;;;;N;;;;;
+2262;NOT IDENTICAL TO;Sm;0;ON;2261 0338;;;;Y;;;;;
+2263;STRICTLY EQUIVALENT TO;Sm;0;ON;;;;;N;;;;;
+2264;LESS-THAN OR EQUAL TO;Sm;0;ON;;;;;Y;LESS THAN OR EQUAL TO;;;;
+2265;GREATER-THAN OR EQUAL TO;Sm;0;ON;;;;;Y;GREATER THAN OR EQUAL TO;;;;
+2266;LESS-THAN OVER EQUAL TO;Sm;0;ON;;;;;Y;LESS THAN OVER EQUAL TO;;;;
+2267;GREATER-THAN OVER EQUAL TO;Sm;0;ON;;;;;Y;GREATER THAN OVER EQUAL TO;;;;
+2268;LESS-THAN BUT NOT EQUAL TO;Sm;0;ON;;;;;Y;LESS THAN BUT NOT EQUAL TO;;;;
+2269;GREATER-THAN BUT NOT EQUAL TO;Sm;0;ON;;;;;Y;GREATER THAN BUT NOT EQUAL TO;;;;
+226A;MUCH LESS-THAN;Sm;0;ON;;;;;Y;MUCH LESS THAN;;;;
+226B;MUCH GREATER-THAN;Sm;0;ON;;;;;Y;MUCH GREATER THAN;;;;
+226C;BETWEEN;Sm;0;ON;;;;;N;;;;;
+226D;NOT EQUIVALENT TO;Sm;0;ON;224D 0338;;;;N;;;;;
+226E;NOT LESS-THAN;Sm;0;ON;003C 0338;;;;Y;NOT LESS THAN;;;;
+226F;NOT GREATER-THAN;Sm;0;ON;003E 0338;;;;Y;NOT GREATER THAN;;;;
+2270;NEITHER LESS-THAN NOR EQUAL TO;Sm;0;ON;2264 0338;;;;Y;NEITHER LESS THAN NOR EQUAL TO;;;;
+2271;NEITHER GREATER-THAN NOR EQUAL TO;Sm;0;ON;2265 0338;;;;Y;NEITHER GREATER THAN NOR EQUAL TO;;;;
+2272;LESS-THAN OR EQUIVALENT TO;Sm;0;ON;;;;;Y;LESS THAN OR EQUIVALENT TO;;;;
+2273;GREATER-THAN OR EQUIVALENT TO;Sm;0;ON;;;;;Y;GREATER THAN OR EQUIVALENT TO;;;;
+2274;NEITHER LESS-THAN NOR EQUIVALENT TO;Sm;0;ON;2272 0338;;;;Y;NEITHER LESS THAN NOR EQUIVALENT TO;;;;
+2275;NEITHER GREATER-THAN NOR EQUIVALENT TO;Sm;0;ON;2273 0338;;;;Y;NEITHER GREATER THAN NOR EQUIVALENT TO;;;;
+2276;LESS-THAN OR GREATER-THAN;Sm;0;ON;;;;;Y;LESS THAN OR GREATER THAN;;;;
+2277;GREATER-THAN OR LESS-THAN;Sm;0;ON;;;;;Y;GREATER THAN OR LESS THAN;;;;
+2278;NEITHER LESS-THAN NOR GREATER-THAN;Sm;0;ON;2276 0338;;;;Y;NEITHER LESS THAN NOR GREATER THAN;;;;
+2279;NEITHER GREATER-THAN NOR LESS-THAN;Sm;0;ON;2277 0338;;;;Y;NEITHER GREATER THAN NOR LESS THAN;;;;
+227A;PRECEDES;Sm;0;ON;;;;;Y;;;;;
+227B;SUCCEEDS;Sm;0;ON;;;;;Y;;;;;
+227C;PRECEDES OR EQUAL TO;Sm;0;ON;;;;;Y;;;;;
+227D;SUCCEEDS OR EQUAL TO;Sm;0;ON;;;;;Y;;;;;
+227E;PRECEDES OR EQUIVALENT TO;Sm;0;ON;;;;;Y;;;;;
+227F;SUCCEEDS OR EQUIVALENT TO;Sm;0;ON;;;;;Y;;;;;
+2280;DOES NOT PRECEDE;Sm;0;ON;227A 0338;;;;Y;;;;;
+2281;DOES NOT SUCCEED;Sm;0;ON;227B 0338;;;;Y;;;;;
+2282;SUBSET OF;Sm;0;ON;;;;;Y;;;;;
+2283;SUPERSET OF;Sm;0;ON;;;;;Y;;;;;
+2284;NOT A SUBSET OF;Sm;0;ON;2282 0338;;;;Y;;;;;
+2285;NOT A SUPERSET OF;Sm;0;ON;2283 0338;;;;Y;;;;;
+2286;SUBSET OF OR EQUAL TO;Sm;0;ON;;;;;Y;;;;;
+2287;SUPERSET OF OR EQUAL TO;Sm;0;ON;;;;;Y;;;;;
+2288;NEITHER A SUBSET OF NOR EQUAL TO;Sm;0;ON;2286 0338;;;;Y;;;;;
+2289;NEITHER A SUPERSET OF NOR EQUAL TO;Sm;0;ON;2287 0338;;;;Y;;;;;
+228A;SUBSET OF WITH NOT EQUAL TO;Sm;0;ON;;;;;Y;SUBSET OF OR NOT EQUAL TO;;;;
+228B;SUPERSET OF WITH NOT EQUAL TO;Sm;0;ON;;;;;Y;SUPERSET OF OR NOT EQUAL TO;;;;
+228C;MULTISET;Sm;0;ON;;;;;Y;;;;;
+228D;MULTISET MULTIPLICATION;Sm;0;ON;;;;;N;;;;;
+228E;MULTISET UNION;Sm;0;ON;;;;;N;;;;;
+228F;SQUARE IMAGE OF;Sm;0;ON;;;;;Y;;;;;
+2290;SQUARE ORIGINAL OF;Sm;0;ON;;;;;Y;;;;;
+2291;SQUARE IMAGE OF OR EQUAL TO;Sm;0;ON;;;;;Y;;;;;
+2292;SQUARE ORIGINAL OF OR EQUAL TO;Sm;0;ON;;;;;Y;;;;;
+2293;SQUARE CAP;Sm;0;ON;;;;;N;;;;;
+2294;SQUARE CUP;Sm;0;ON;;;;;N;;;;;
+2295;CIRCLED PLUS;Sm;0;ON;;;;;N;;;;;
+2296;CIRCLED MINUS;Sm;0;ON;;;;;N;;;;;
+2297;CIRCLED TIMES;Sm;0;ON;;;;;N;;;;;
+2298;CIRCLED DIVISION SLASH;Sm;0;ON;;;;;Y;;;;;
+2299;CIRCLED DOT OPERATOR;Sm;0;ON;;;;;N;;;;;
+229A;CIRCLED RING OPERATOR;Sm;0;ON;;;;;N;;;;;
+229B;CIRCLED ASTERISK OPERATOR;Sm;0;ON;;;;;N;;;;;
+229C;CIRCLED EQUALS;Sm;0;ON;;;;;N;;;;;
+229D;CIRCLED DASH;Sm;0;ON;;;;;N;;;;;
+229E;SQUARED PLUS;Sm;0;ON;;;;;N;;;;;
+229F;SQUARED MINUS;Sm;0;ON;;;;;N;;;;;
+22A0;SQUARED TIMES;Sm;0;ON;;;;;N;;;;;
+22A1;SQUARED DOT OPERATOR;Sm;0;ON;;;;;N;;;;;
+22A2;RIGHT TACK;Sm;0;ON;;;;;Y;;;;;
+22A3;LEFT TACK;Sm;0;ON;;;;;Y;;;;;
+22A4;DOWN TACK;Sm;0;ON;;;;;N;;;;;
+22A5;UP TACK;Sm;0;ON;;;;;N;;;;;
+22A6;ASSERTION;Sm;0;ON;;;;;Y;;;;;
+22A7;MODELS;Sm;0;ON;;;;;Y;;;;;
+22A8;TRUE;Sm;0;ON;;;;;Y;;;;;
+22A9;FORCES;Sm;0;ON;;;;;Y;;;;;
+22AA;TRIPLE VERTICAL BAR RIGHT TURNSTILE;Sm;0;ON;;;;;Y;;;;;
+22AB;DOUBLE VERTICAL BAR DOUBLE RIGHT TURNSTILE;Sm;0;ON;;;;;Y;;;;;
+22AC;DOES NOT PROVE;Sm;0;ON;22A2 0338;;;;Y;;;;;
+22AD;NOT TRUE;Sm;0;ON;22A8 0338;;;;Y;;;;;
+22AE;DOES NOT FORCE;Sm;0;ON;22A9 0338;;;;Y;;;;;
+22AF;NEGATED DOUBLE VERTICAL BAR DOUBLE RIGHT TURNSTILE;Sm;0;ON;22AB 0338;;;;Y;;;;;
+22B0;PRECEDES UNDER RELATION;Sm;0;ON;;;;;Y;;;;;
+22B1;SUCCEEDS UNDER RELATION;Sm;0;ON;;;;;Y;;;;;
+22B2;NORMAL SUBGROUP OF;Sm;0;ON;;;;;Y;;;;;
+22B3;CONTAINS AS NORMAL SUBGROUP;Sm;0;ON;;;;;Y;;;;;
+22B4;NORMAL SUBGROUP OF OR EQUAL TO;Sm;0;ON;;;;;Y;;;;;
+22B5;CONTAINS AS NORMAL SUBGROUP OR EQUAL TO;Sm;0;ON;;;;;Y;;;;;
+22B6;ORIGINAL OF;Sm;0;ON;;;;;Y;;;;;
+22B7;IMAGE OF;Sm;0;ON;;;;;Y;;;;;
+22B8;MULTIMAP;Sm;0;ON;;;;;Y;;;;;
+22B9;HERMITIAN CONJUGATE MATRIX;Sm;0;ON;;;;;N;;;;;
+22BA;INTERCALATE;Sm;0;ON;;;;;N;;;;;
+22BB;XOR;Sm;0;ON;;;;;N;;;;;
+22BC;NAND;Sm;0;ON;;;;;N;;;;;
+22BD;NOR;Sm;0;ON;;;;;N;;;;;
+22BE;RIGHT ANGLE WITH ARC;Sm;0;ON;;;;;Y;;;;;
+22BF;RIGHT TRIANGLE;Sm;0;ON;;;;;Y;;;;;
+22C0;N-ARY LOGICAL AND;Sm;0;ON;;;;;N;;;;;
+22C1;N-ARY LOGICAL OR;Sm;0;ON;;;;;N;;;;;
+22C2;N-ARY INTERSECTION;Sm;0;ON;;;;;N;;;;;
+22C3;N-ARY UNION;Sm;0;ON;;;;;N;;;;;
+22C4;DIAMOND OPERATOR;Sm;0;ON;;;;;N;;;;;
+22C5;DOT OPERATOR;Sm;0;ON;;;;;N;;;;;
+22C6;STAR OPERATOR;Sm;0;ON;;;;;N;;;;;
+22C7;DIVISION TIMES;Sm;0;ON;;;;;N;;;;;
+22C8;BOWTIE;Sm;0;ON;;;;;N;;;;;
+22C9;LEFT NORMAL FACTOR SEMIDIRECT PRODUCT;Sm;0;ON;;;;;Y;;;;;
+22CA;RIGHT NORMAL FACTOR SEMIDIRECT PRODUCT;Sm;0;ON;;;;;Y;;;;;
+22CB;LEFT SEMIDIRECT PRODUCT;Sm;0;ON;;;;;Y;;;;;
+22CC;RIGHT SEMIDIRECT PRODUCT;Sm;0;ON;;;;;Y;;;;;
+22CD;REVERSED TILDE EQUALS;Sm;0;ON;;;;;Y;;;;;
+22CE;CURLY LOGICAL OR;Sm;0;ON;;;;;N;;;;;
+22CF;CURLY LOGICAL AND;Sm;0;ON;;;;;N;;;;;
+22D0;DOUBLE SUBSET;Sm;0;ON;;;;;Y;;;;;
+22D1;DOUBLE SUPERSET;Sm;0;ON;;;;;Y;;;;;
+22D2;DOUBLE INTERSECTION;Sm;0;ON;;;;;N;;;;;
+22D3;DOUBLE UNION;Sm;0;ON;;;;;N;;;;;
+22D4;PITCHFORK;Sm;0;ON;;;;;N;;;;;
+22D5;EQUAL AND PARALLEL TO;Sm;0;ON;;;;;N;;;;;
+22D6;LESS-THAN WITH DOT;Sm;0;ON;;;;;Y;LESS THAN WITH DOT;;;;
+22D7;GREATER-THAN WITH DOT;Sm;0;ON;;;;;Y;GREATER THAN WITH DOT;;;;
+22D8;VERY MUCH LESS-THAN;Sm;0;ON;;;;;Y;VERY MUCH LESS THAN;;;;
+22D9;VERY MUCH GREATER-THAN;Sm;0;ON;;;;;Y;VERY MUCH GREATER THAN;;;;
+22DA;LESS-THAN EQUAL TO OR GREATER-THAN;Sm;0;ON;;;;;Y;LESS THAN EQUAL TO OR GREATER THAN;;;;
+22DB;GREATER-THAN EQUAL TO OR LESS-THAN;Sm;0;ON;;;;;Y;GREATER THAN EQUAL TO OR LESS THAN;;;;
+22DC;EQUAL TO OR LESS-THAN;Sm;0;ON;;;;;Y;EQUAL TO OR LESS THAN;;;;
+22DD;EQUAL TO OR GREATER-THAN;Sm;0;ON;;;;;Y;EQUAL TO OR GREATER THAN;;;;
+22DE;EQUAL TO OR PRECEDES;Sm;0;ON;;;;;Y;;;;;
+22DF;EQUAL TO OR SUCCEEDS;Sm;0;ON;;;;;Y;;;;;
+22E0;DOES NOT PRECEDE OR EQUAL;Sm;0;ON;227C 0338;;;;Y;;;;;
+22E1;DOES NOT SUCCEED OR EQUAL;Sm;0;ON;227D 0338;;;;Y;;;;;
+22E2;NOT SQUARE IMAGE OF OR EQUAL TO;Sm;0;ON;2291 0338;;;;Y;;;;;
+22E3;NOT SQUARE ORIGINAL OF OR EQUAL TO;Sm;0;ON;2292 0338;;;;Y;;;;;
+22E4;SQUARE IMAGE OF OR NOT EQUAL TO;Sm;0;ON;;;;;Y;;;;;
+22E5;SQUARE ORIGINAL OF OR NOT EQUAL TO;Sm;0;ON;;;;;Y;;;;;
+22E6;LESS-THAN BUT NOT EQUIVALENT TO;Sm;0;ON;;;;;Y;LESS THAN BUT NOT EQUIVALENT TO;;;;
+22E7;GREATER-THAN BUT NOT EQUIVALENT TO;Sm;0;ON;;;;;Y;GREATER THAN BUT NOT EQUIVALENT TO;;;;
+22E8;PRECEDES BUT NOT EQUIVALENT TO;Sm;0;ON;;;;;Y;;;;;
+22E9;SUCCEEDS BUT NOT EQUIVALENT TO;Sm;0;ON;;;;;Y;;;;;
+22EA;NOT NORMAL SUBGROUP OF;Sm;0;ON;22B2 0338;;;;Y;;;;;
+22EB;DOES NOT CONTAIN AS NORMAL SUBGROUP;Sm;0;ON;22B3 0338;;;;Y;;;;;
+22EC;NOT NORMAL SUBGROUP OF OR EQUAL TO;Sm;0;ON;22B4 0338;;;;Y;;;;;
+22ED;DOES NOT CONTAIN AS NORMAL SUBGROUP OR EQUAL;Sm;0;ON;22B5 0338;;;;Y;;;;;
+22EE;VERTICAL ELLIPSIS;Sm;0;ON;;;;;N;;;;;
+22EF;MIDLINE HORIZONTAL ELLIPSIS;Sm;0;ON;;;;;N;;;;;
+22F0;UP RIGHT DIAGONAL ELLIPSIS;Sm;0;ON;;;;;Y;;;;;
+22F1;DOWN RIGHT DIAGONAL ELLIPSIS;Sm;0;ON;;;;;Y;;;;;
+22F2;ELEMENT OF WITH LONG HORIZONTAL STROKE;Sm;0;ON;;;;;Y;;;;;
+22F3;ELEMENT OF WITH VERTICAL BAR AT END OF HORIZONTAL STROKE;Sm;0;ON;;;;;Y;;;;;
+22F4;SMALL ELEMENT OF WITH VERTICAL BAR AT END OF HORIZONTAL STROKE;Sm;0;ON;;;;;Y;;;;;
+22F5;ELEMENT OF WITH DOT ABOVE;Sm;0;ON;;;;;Y;;;;;
+22F6;ELEMENT OF WITH OVERBAR;Sm;0;ON;;;;;Y;;;;;
+22F7;SMALL ELEMENT OF WITH OVERBAR;Sm;0;ON;;;;;Y;;;;;
+22F8;ELEMENT OF WITH UNDERBAR;Sm;0;ON;;;;;Y;;;;;
+22F9;ELEMENT OF WITH TWO HORIZONTAL STROKES;Sm;0;ON;;;;;Y;;;;;
+22FA;CONTAINS WITH LONG HORIZONTAL STROKE;Sm;0;ON;;;;;Y;;;;;
+22FB;CONTAINS WITH VERTICAL BAR AT END OF HORIZONTAL STROKE;Sm;0;ON;;;;;Y;;;;;
+22FC;SMALL CONTAINS WITH VERTICAL BAR AT END OF HORIZONTAL STROKE;Sm;0;ON;;;;;Y;;;;;
+22FD;CONTAINS WITH OVERBAR;Sm;0;ON;;;;;Y;;;;;
+22FE;SMALL CONTAINS WITH OVERBAR;Sm;0;ON;;;;;Y;;;;;
+22FF;Z NOTATION BAG MEMBERSHIP;Sm;0;ON;;;;;Y;;;;;
+2300;DIAMETER SIGN;So;0;ON;;;;;N;;;;;
+2301;ELECTRIC ARROW;So;0;ON;;;;;N;;;;;
+2302;HOUSE;So;0;ON;;;;;N;;;;;
+2303;UP ARROWHEAD;So;0;ON;;;;;N;;;;;
+2304;DOWN ARROWHEAD;So;0;ON;;;;;N;;;;;
+2305;PROJECTIVE;So;0;ON;;;;;N;;;;;
+2306;PERSPECTIVE;So;0;ON;;;;;N;;;;;
+2307;WAVY LINE;So;0;ON;;;;;N;;;;;
+2308;LEFT CEILING;Ps;0;ON;;;;;Y;;;;;
+2309;RIGHT CEILING;Pe;0;ON;;;;;Y;;;;;
+230A;LEFT FLOOR;Ps;0;ON;;;;;Y;;;;;
+230B;RIGHT FLOOR;Pe;0;ON;;;;;Y;;;;;
+230C;BOTTOM RIGHT CROP;So;0;ON;;;;;N;;;;;
+230D;BOTTOM LEFT CROP;So;0;ON;;;;;N;;;;;
+230E;TOP RIGHT CROP;So;0;ON;;;;;N;;;;;
+230F;TOP LEFT CROP;So;0;ON;;;;;N;;;;;
+2310;REVERSED NOT SIGN;So;0;ON;;;;;N;;;;;
+2311;SQUARE LOZENGE;So;0;ON;;;;;N;;;;;
+2312;ARC;So;0;ON;;;;;N;;;;;
+2313;SEGMENT;So;0;ON;;;;;N;;;;;
+2314;SECTOR;So;0;ON;;;;;N;;;;;
+2315;TELEPHONE RECORDER;So;0;ON;;;;;N;;;;;
+2316;POSITION INDICATOR;So;0;ON;;;;;N;;;;;
+2317;VIEWDATA SQUARE;So;0;ON;;;;;N;;;;;
+2318;PLACE OF INTEREST SIGN;So;0;ON;;;;;N;COMMAND KEY;;;;
+2319;TURNED NOT SIGN;So;0;ON;;;;;N;;;;;
+231A;WATCH;So;0;ON;;;;;N;;;;;
+231B;HOURGLASS;So;0;ON;;;;;N;;;;;
+231C;TOP LEFT CORNER;So;0;ON;;;;;N;;;;;
+231D;TOP RIGHT CORNER;So;0;ON;;;;;N;;;;;
+231E;BOTTOM LEFT CORNER;So;0;ON;;;;;N;;;;;
+231F;BOTTOM RIGHT CORNER;So;0;ON;;;;;N;;;;;
+2320;TOP HALF INTEGRAL;Sm;0;ON;;;;;Y;;;;;
+2321;BOTTOM HALF INTEGRAL;Sm;0;ON;;;;;Y;;;;;
+2322;FROWN;So;0;ON;;;;;N;;;;;
+2323;SMILE;So;0;ON;;;;;N;;;;;
+2324;UP ARROWHEAD BETWEEN TWO HORIZONTAL BARS;So;0;ON;;;;;N;ENTER KEY;;;;
+2325;OPTION KEY;So;0;ON;;;;;N;;;;;
+2326;ERASE TO THE RIGHT;So;0;ON;;;;;N;DELETE TO THE RIGHT KEY;;;;
+2327;X IN A RECTANGLE BOX;So;0;ON;;;;;N;CLEAR KEY;;;;
+2328;KEYBOARD;So;0;ON;;;;;N;;;;;
+2329;LEFT-POINTING ANGLE BRACKET;Ps;0;ON;3008;;;;Y;BRA;;;;
+232A;RIGHT-POINTING ANGLE BRACKET;Pe;0;ON;3009;;;;Y;KET;;;;
+232B;ERASE TO THE LEFT;So;0;ON;;;;;N;DELETE TO THE LEFT KEY;;;;
+232C;BENZENE RING;So;0;ON;;;;;N;;;;;
+232D;CYLINDRICITY;So;0;ON;;;;;N;;;;;
+232E;ALL AROUND-PROFILE;So;0;ON;;;;;N;;;;;
+232F;SYMMETRY;So;0;ON;;;;;N;;;;;
+2330;TOTAL RUNOUT;So;0;ON;;;;;N;;;;;
+2331;DIMENSION ORIGIN;So;0;ON;;;;;N;;;;;
+2332;CONICAL TAPER;So;0;ON;;;;;N;;;;;
+2333;SLOPE;So;0;ON;;;;;N;;;;;
+2334;COUNTERBORE;So;0;ON;;;;;N;;;;;
+2335;COUNTERSINK;So;0;ON;;;;;N;;;;;
+2336;APL FUNCTIONAL SYMBOL I-BEAM;So;0;L;;;;;N;;;;;
+2337;APL FUNCTIONAL SYMBOL SQUISH QUAD;So;0;L;;;;;N;;;;;
+2338;APL FUNCTIONAL SYMBOL QUAD EQUAL;So;0;L;;;;;N;;;;;
+2339;APL FUNCTIONAL SYMBOL QUAD DIVIDE;So;0;L;;;;;N;;;;;
+233A;APL FUNCTIONAL SYMBOL QUAD DIAMOND;So;0;L;;;;;N;;;;;
+233B;APL FUNCTIONAL SYMBOL QUAD JOT;So;0;L;;;;;N;;;;;
+233C;APL FUNCTIONAL SYMBOL QUAD CIRCLE;So;0;L;;;;;N;;;;;
+233D;APL FUNCTIONAL SYMBOL CIRCLE STILE;So;0;L;;;;;N;;;;;
+233E;APL FUNCTIONAL SYMBOL CIRCLE JOT;So;0;L;;;;;N;;;;;
+233F;APL FUNCTIONAL SYMBOL SLASH BAR;So;0;L;;;;;N;;;;;
+2340;APL FUNCTIONAL SYMBOL BACKSLASH BAR;So;0;L;;;;;N;;;;;
+2341;APL FUNCTIONAL SYMBOL QUAD SLASH;So;0;L;;;;;N;;;;;
+2342;APL FUNCTIONAL SYMBOL QUAD BACKSLASH;So;0;L;;;;;N;;;;;
+2343;APL FUNCTIONAL SYMBOL QUAD LESS-THAN;So;0;L;;;;;N;;;;;
+2344;APL FUNCTIONAL SYMBOL QUAD GREATER-THAN;So;0;L;;;;;N;;;;;
+2345;APL FUNCTIONAL SYMBOL LEFTWARDS VANE;So;0;L;;;;;N;;;;;
+2346;APL FUNCTIONAL SYMBOL RIGHTWARDS VANE;So;0;L;;;;;N;;;;;
+2347;APL FUNCTIONAL SYMBOL QUAD LEFTWARDS ARROW;So;0;L;;;;;N;;;;;
+2348;APL FUNCTIONAL SYMBOL QUAD RIGHTWARDS ARROW;So;0;L;;;;;N;;;;;
+2349;APL FUNCTIONAL SYMBOL CIRCLE BACKSLASH;So;0;L;;;;;N;;;;;
+234A;APL FUNCTIONAL SYMBOL DOWN TACK UNDERBAR;So;0;L;;;;;N;;;;;
+234B;APL FUNCTIONAL SYMBOL DELTA STILE;So;0;L;;;;;N;;;;;
+234C;APL FUNCTIONAL SYMBOL QUAD DOWN CARET;So;0;L;;;;;N;;;;;
+234D;APL FUNCTIONAL SYMBOL QUAD DELTA;So;0;L;;;;;N;;;;;
+234E;APL FUNCTIONAL SYMBOL DOWN TACK JOT;So;0;L;;;;;N;;;;;
+234F;APL FUNCTIONAL SYMBOL UPWARDS VANE;So;0;L;;;;;N;;;;;
+2350;APL FUNCTIONAL SYMBOL QUAD UPWARDS ARROW;So;0;L;;;;;N;;;;;
+2351;APL FUNCTIONAL SYMBOL UP TACK OVERBAR;So;0;L;;;;;N;;;;;
+2352;APL FUNCTIONAL SYMBOL DEL STILE;So;0;L;;;;;N;;;;;
+2353;APL FUNCTIONAL SYMBOL QUAD UP CARET;So;0;L;;;;;N;;;;;
+2354;APL FUNCTIONAL SYMBOL QUAD DEL;So;0;L;;;;;N;;;;;
+2355;APL FUNCTIONAL SYMBOL UP TACK JOT;So;0;L;;;;;N;;;;;
+2356;APL FUNCTIONAL SYMBOL DOWNWARDS VANE;So;0;L;;;;;N;;;;;
+2357;APL FUNCTIONAL SYMBOL QUAD DOWNWARDS ARROW;So;0;L;;;;;N;;;;;
+2358;APL FUNCTIONAL SYMBOL QUOTE UNDERBAR;So;0;L;;;;;N;;;;;
+2359;APL FUNCTIONAL SYMBOL DELTA UNDERBAR;So;0;L;;;;;N;;;;;
+235A;APL FUNCTIONAL SYMBOL DIAMOND UNDERBAR;So;0;L;;;;;N;;;;;
+235B;APL FUNCTIONAL SYMBOL JOT UNDERBAR;So;0;L;;;;;N;;;;;
+235C;APL FUNCTIONAL SYMBOL CIRCLE UNDERBAR;So;0;L;;;;;N;;;;;
+235D;APL FUNCTIONAL SYMBOL UP SHOE JOT;So;0;L;;;;;N;;;;;
+235E;APL FUNCTIONAL SYMBOL QUOTE QUAD;So;0;L;;;;;N;;;;;
+235F;APL FUNCTIONAL SYMBOL CIRCLE STAR;So;0;L;;;;;N;;;;;
+2360;APL FUNCTIONAL SYMBOL QUAD COLON;So;0;L;;;;;N;;;;;
+2361;APL FUNCTIONAL SYMBOL UP TACK DIAERESIS;So;0;L;;;;;N;;;;;
+2362;APL FUNCTIONAL SYMBOL DEL DIAERESIS;So;0;L;;;;;N;;;;;
+2363;APL FUNCTIONAL SYMBOL STAR DIAERESIS;So;0;L;;;;;N;;;;;
+2364;APL FUNCTIONAL SYMBOL JOT DIAERESIS;So;0;L;;;;;N;;;;;
+2365;APL FUNCTIONAL SYMBOL CIRCLE DIAERESIS;So;0;L;;;;;N;;;;;
+2366;APL FUNCTIONAL SYMBOL DOWN SHOE STILE;So;0;L;;;;;N;;;;;
+2367;APL FUNCTIONAL SYMBOL LEFT SHOE STILE;So;0;L;;;;;N;;;;;
+2368;APL FUNCTIONAL SYMBOL TILDE DIAERESIS;So;0;L;;;;;N;;;;;
+2369;APL FUNCTIONAL SYMBOL GREATER-THAN DIAERESIS;So;0;L;;;;;N;;;;;
+236A;APL FUNCTIONAL SYMBOL COMMA BAR;So;0;L;;;;;N;;;;;
+236B;APL FUNCTIONAL SYMBOL DEL TILDE;So;0;L;;;;;N;;;;;
+236C;APL FUNCTIONAL SYMBOL ZILDE;So;0;L;;;;;N;;;;;
+236D;APL FUNCTIONAL SYMBOL STILE TILDE;So;0;L;;;;;N;;;;;
+236E;APL FUNCTIONAL SYMBOL SEMICOLON UNDERBAR;So;0;L;;;;;N;;;;;
+236F;APL FUNCTIONAL SYMBOL QUAD NOT EQUAL;So;0;L;;;;;N;;;;;
+2370;APL FUNCTIONAL SYMBOL QUAD QUESTION;So;0;L;;;;;N;;;;;
+2371;APL FUNCTIONAL SYMBOL DOWN CARET TILDE;So;0;L;;;;;N;;;;;
+2372;APL FUNCTIONAL SYMBOL UP CARET TILDE;So;0;L;;;;;N;;;;;
+2373;APL FUNCTIONAL SYMBOL IOTA;So;0;L;;;;;N;;;;;
+2374;APL FUNCTIONAL SYMBOL RHO;So;0;L;;;;;N;;;;;
+2375;APL FUNCTIONAL SYMBOL OMEGA;So;0;L;;;;;N;;;;;
+2376;APL FUNCTIONAL SYMBOL ALPHA UNDERBAR;So;0;L;;;;;N;;;;;
+2377;APL FUNCTIONAL SYMBOL EPSILON UNDERBAR;So;0;L;;;;;N;;;;;
+2378;APL FUNCTIONAL SYMBOL IOTA UNDERBAR;So;0;L;;;;;N;;;;;
+2379;APL FUNCTIONAL SYMBOL OMEGA UNDERBAR;So;0;L;;;;;N;;;;;
+237A;APL FUNCTIONAL SYMBOL ALPHA;So;0;L;;;;;N;;;;;
+237B;NOT CHECK MARK;So;0;ON;;;;;N;;;;;
+237C;RIGHT ANGLE WITH DOWNWARDS ZIGZAG ARROW;Sm;0;ON;;;;;N;;;;;
+237D;SHOULDERED OPEN BOX;So;0;ON;;;;;N;;;;;
+237E;BELL SYMBOL;So;0;ON;;;;;N;;;;;
+237F;VERTICAL LINE WITH MIDDLE DOT;So;0;ON;;;;;N;;;;;
+2380;INSERTION SYMBOL;So;0;ON;;;;;N;;;;;
+2381;CONTINUOUS UNDERLINE SYMBOL;So;0;ON;;;;;N;;;;;
+2382;DISCONTINUOUS UNDERLINE SYMBOL;So;0;ON;;;;;N;;;;;
+2383;EMPHASIS SYMBOL;So;0;ON;;;;;N;;;;;
+2384;COMPOSITION SYMBOL;So;0;ON;;;;;N;;;;;
+2385;WHITE SQUARE WITH CENTRE VERTICAL LINE;So;0;ON;;;;;N;;;;;
+2386;ENTER SYMBOL;So;0;ON;;;;;N;;;;;
+2387;ALTERNATIVE KEY SYMBOL;So;0;ON;;;;;N;;;;;
+2388;HELM SYMBOL;So;0;ON;;;;;N;;;;;
+2389;CIRCLED HORIZONTAL BAR WITH NOTCH;So;0;ON;;;;;N;;;;;
+238A;CIRCLED TRIANGLE DOWN;So;0;ON;;;;;N;;;;;
+238B;BROKEN CIRCLE WITH NORTHWEST ARROW;So;0;ON;;;;;N;;;;;
+238C;UNDO SYMBOL;So;0;ON;;;;;N;;;;;
+238D;MONOSTABLE SYMBOL;So;0;ON;;;;;N;;;;;
+238E;HYSTERESIS SYMBOL;So;0;ON;;;;;N;;;;;
+238F;OPEN-CIRCUIT-OUTPUT H-TYPE SYMBOL;So;0;ON;;;;;N;;;;;
+2390;OPEN-CIRCUIT-OUTPUT L-TYPE SYMBOL;So;0;ON;;;;;N;;;;;
+2391;PASSIVE-PULL-DOWN-OUTPUT SYMBOL;So;0;ON;;;;;N;;;;;
+2392;PASSIVE-PULL-UP-OUTPUT SYMBOL;So;0;ON;;;;;N;;;;;
+2393;DIRECT CURRENT SYMBOL FORM TWO;So;0;ON;;;;;N;;;;;
+2394;SOFTWARE-FUNCTION SYMBOL;So;0;ON;;;;;N;;;;;
+2395;APL FUNCTIONAL SYMBOL QUAD;So;0;L;;;;;N;;;;;
+2396;DECIMAL SEPARATOR KEY SYMBOL;So;0;ON;;;;;N;;;;;
+2397;PREVIOUS PAGE;So;0;ON;;;;;N;;;;;
+2398;NEXT PAGE;So;0;ON;;;;;N;;;;;
+2399;PRINT SCREEN SYMBOL;So;0;ON;;;;;N;;;;;
+239A;CLEAR SCREEN SYMBOL;So;0;ON;;;;;N;;;;;
+239B;LEFT PARENTHESIS UPPER HOOK;Sm;0;ON;;;;;N;;;;;
+239C;LEFT PARENTHESIS EXTENSION;Sm;0;ON;;;;;N;;;;;
+239D;LEFT PARENTHESIS LOWER HOOK;Sm;0;ON;;;;;N;;;;;
+239E;RIGHT PARENTHESIS UPPER HOOK;Sm;0;ON;;;;;N;;;;;
+239F;RIGHT PARENTHESIS EXTENSION;Sm;0;ON;;;;;N;;;;;
+23A0;RIGHT PARENTHESIS LOWER HOOK;Sm;0;ON;;;;;N;;;;;
+23A1;LEFT SQUARE BRACKET UPPER CORNER;Sm;0;ON;;;;;N;;;;;
+23A2;LEFT SQUARE BRACKET EXTENSION;Sm;0;ON;;;;;N;;;;;
+23A3;LEFT SQUARE BRACKET LOWER CORNER;Sm;0;ON;;;;;N;;;;;
+23A4;RIGHT SQUARE BRACKET UPPER CORNER;Sm;0;ON;;;;;N;;;;;
+23A5;RIGHT SQUARE BRACKET EXTENSION;Sm;0;ON;;;;;N;;;;;
+23A6;RIGHT SQUARE BRACKET LOWER CORNER;Sm;0;ON;;;;;N;;;;;
+23A7;LEFT CURLY BRACKET UPPER HOOK;Sm;0;ON;;;;;N;;;;;
+23A8;LEFT CURLY BRACKET MIDDLE PIECE;Sm;0;ON;;;;;N;;;;;
+23A9;LEFT CURLY BRACKET LOWER HOOK;Sm;0;ON;;;;;N;;;;;
+23AA;CURLY BRACKET EXTENSION;Sm;0;ON;;;;;N;;;;;
+23AB;RIGHT CURLY BRACKET UPPER HOOK;Sm;0;ON;;;;;N;;;;;
+23AC;RIGHT CURLY BRACKET MIDDLE PIECE;Sm;0;ON;;;;;N;;;;;
+23AD;RIGHT CURLY BRACKET LOWER HOOK;Sm;0;ON;;;;;N;;;;;
+23AE;INTEGRAL EXTENSION;Sm;0;ON;;;;;N;;;;;
+23AF;HORIZONTAL LINE EXTENSION;Sm;0;ON;;;;;N;;;;;
+23B0;UPPER LEFT OR LOWER RIGHT CURLY BRACKET SECTION;Sm;0;ON;;;;;N;;;;;
+23B1;UPPER RIGHT OR LOWER LEFT CURLY BRACKET SECTION;Sm;0;ON;;;;;N;;;;;
+23B2;SUMMATION TOP;Sm;0;ON;;;;;N;;;;;
+23B3;SUMMATION BOTTOM;Sm;0;ON;;;;;N;;;;;
+23B4;TOP SQUARE BRACKET;So;0;ON;;;;;N;;;;;
+23B5;BOTTOM SQUARE BRACKET;So;0;ON;;;;;N;;;;;
+23B6;BOTTOM SQUARE BRACKET OVER TOP SQUARE BRACKET;So;0;ON;;;;;N;;;;;
+23B7;RADICAL SYMBOL BOTTOM;So;0;ON;;;;;N;;;;;
+23B8;LEFT VERTICAL BOX LINE;So;0;ON;;;;;N;;;;;
+23B9;RIGHT VERTICAL BOX LINE;So;0;ON;;;;;N;;;;;
+23BA;HORIZONTAL SCAN LINE-1;So;0;ON;;;;;N;;;;;
+23BB;HORIZONTAL SCAN LINE-3;So;0;ON;;;;;N;;;;;
+23BC;HORIZONTAL SCAN LINE-7;So;0;ON;;;;;N;;;;;
+23BD;HORIZONTAL SCAN LINE-9;So;0;ON;;;;;N;;;;;
+23BE;DENTISTRY SYMBOL LIGHT VERTICAL AND TOP RIGHT;So;0;ON;;;;;N;;;;;
+23BF;DENTISTRY SYMBOL LIGHT VERTICAL AND BOTTOM RIGHT;So;0;ON;;;;;N;;;;;
+23C0;DENTISTRY SYMBOL LIGHT VERTICAL WITH CIRCLE;So;0;ON;;;;;N;;;;;
+23C1;DENTISTRY SYMBOL LIGHT DOWN AND HORIZONTAL WITH CIRCLE;So;0;ON;;;;;N;;;;;
+23C2;DENTISTRY SYMBOL LIGHT UP AND HORIZONTAL WITH CIRCLE;So;0;ON;;;;;N;;;;;
+23C3;DENTISTRY SYMBOL LIGHT VERTICAL WITH TRIANGLE;So;0;ON;;;;;N;;;;;
+23C4;DENTISTRY SYMBOL LIGHT DOWN AND HORIZONTAL WITH TRIANGLE;So;0;ON;;;;;N;;;;;
+23C5;DENTISTRY SYMBOL LIGHT UP AND HORIZONTAL WITH TRIANGLE;So;0;ON;;;;;N;;;;;
+23C6;DENTISTRY SYMBOL LIGHT VERTICAL AND WAVE;So;0;ON;;;;;N;;;;;
+23C7;DENTISTRY SYMBOL LIGHT DOWN AND HORIZONTAL WITH WAVE;So;0;ON;;;;;N;;;;;
+23C8;DENTISTRY SYMBOL LIGHT UP AND HORIZONTAL WITH WAVE;So;0;ON;;;;;N;;;;;
+23C9;DENTISTRY SYMBOL LIGHT DOWN AND HORIZONTAL;So;0;ON;;;;;N;;;;;
+23CA;DENTISTRY SYMBOL LIGHT UP AND HORIZONTAL;So;0;ON;;;;;N;;;;;
+23CB;DENTISTRY SYMBOL LIGHT VERTICAL AND TOP LEFT;So;0;ON;;;;;N;;;;;
+23CC;DENTISTRY SYMBOL LIGHT VERTICAL AND BOTTOM LEFT;So;0;ON;;;;;N;;;;;
+23CD;SQUARE FOOT;So;0;ON;;;;;N;;;;;
+23CE;RETURN SYMBOL;So;0;ON;;;;;N;;;;;
+23CF;EJECT SYMBOL;So;0;ON;;;;;N;;;;;
+23D0;VERTICAL LINE EXTENSION;So;0;ON;;;;;N;;;;;
+23D1;METRICAL BREVE;So;0;ON;;;;;N;;;;;
+23D2;METRICAL LONG OVER SHORT;So;0;ON;;;;;N;;;;;
+23D3;METRICAL SHORT OVER LONG;So;0;ON;;;;;N;;;;;
+23D4;METRICAL LONG OVER TWO SHORTS;So;0;ON;;;;;N;;;;;
+23D5;METRICAL TWO SHORTS OVER LONG;So;0;ON;;;;;N;;;;;
+23D6;METRICAL TWO SHORTS JOINED;So;0;ON;;;;;N;;;;;
+23D7;METRICAL TRISEME;So;0;ON;;;;;N;;;;;
+23D8;METRICAL TETRASEME;So;0;ON;;;;;N;;;;;
+23D9;METRICAL PENTASEME;So;0;ON;;;;;N;;;;;
+23DA;EARTH GROUND;So;0;ON;;;;;N;;;;;
+23DB;FUSE;So;0;ON;;;;;N;;;;;
+23DC;TOP PARENTHESIS;Sm;0;ON;;;;;N;;;;;
+23DD;BOTTOM PARENTHESIS;Sm;0;ON;;;;;N;;;;;
+23DE;TOP CURLY BRACKET;Sm;0;ON;;;;;N;;;;;
+23DF;BOTTOM CURLY BRACKET;Sm;0;ON;;;;;N;;;;;
+23E0;TOP TORTOISE SHELL BRACKET;Sm;0;ON;;;;;N;;;;;
+23E1;BOTTOM TORTOISE SHELL BRACKET;Sm;0;ON;;;;;N;;;;;
+23E2;WHITE TRAPEZIUM;So;0;ON;;;;;N;;;;;
+23E3;BENZENE RING WITH CIRCLE;So;0;ON;;;;;N;;;;;
+23E4;STRAIGHTNESS;So;0;ON;;;;;N;;;;;
+23E5;FLATNESS;So;0;ON;;;;;N;;;;;
+23E6;AC CURRENT;So;0;ON;;;;;N;;;;;
+23E7;ELECTRICAL INTERSECTION;So;0;ON;;;;;N;;;;;
+23E8;DECIMAL EXPONENT SYMBOL;So;0;ON;;;;;N;;;;;
+23E9;BLACK RIGHT-POINTING DOUBLE TRIANGLE;So;0;ON;;;;;N;;;;;
+23EA;BLACK LEFT-POINTING DOUBLE TRIANGLE;So;0;ON;;;;;N;;;;;
+23EB;BLACK UP-POINTING DOUBLE TRIANGLE;So;0;ON;;;;;N;;;;;
+23EC;BLACK DOWN-POINTING DOUBLE TRIANGLE;So;0;ON;;;;;N;;;;;
+23ED;BLACK RIGHT-POINTING DOUBLE TRIANGLE WITH VERTICAL BAR;So;0;ON;;;;;N;;;;;
+23EE;BLACK LEFT-POINTING DOUBLE TRIANGLE WITH VERTICAL BAR;So;0;ON;;;;;N;;;;;
+23EF;BLACK RIGHT-POINTING TRIANGLE WITH DOUBLE VERTICAL BAR;So;0;ON;;;;;N;;;;;
+23F0;ALARM CLOCK;So;0;ON;;;;;N;;;;;
+23F1;STOPWATCH;So;0;ON;;;;;N;;;;;
+23F2;TIMER CLOCK;So;0;ON;;;;;N;;;;;
+23F3;HOURGLASS WITH FLOWING SAND;So;0;ON;;;;;N;;;;;
+23F4;BLACK MEDIUM LEFT-POINTING TRIANGLE;So;0;ON;;;;;N;;;;;
+23F5;BLACK MEDIUM RIGHT-POINTING TRIANGLE;So;0;ON;;;;;N;;;;;
+23F6;BLACK MEDIUM UP-POINTING TRIANGLE;So;0;ON;;;;;N;;;;;
+23F7;BLACK MEDIUM DOWN-POINTING TRIANGLE;So;0;ON;;;;;N;;;;;
+23F8;DOUBLE VERTICAL BAR;So;0;ON;;;;;N;;;;;
+23F9;BLACK SQUARE FOR STOP;So;0;ON;;;;;N;;;;;
+23FA;BLACK CIRCLE FOR RECORD;So;0;ON;;;;;N;;;;;
+23FB;POWER SYMBOL;So;0;ON;;;;;N;;;;;
+23FC;POWER ON-OFF SYMBOL;So;0;ON;;;;;N;;;;;
+23FD;POWER ON SYMBOL;So;0;ON;;;;;N;;;;;
+23FE;POWER SLEEP SYMBOL;So;0;ON;;;;;N;;;;;
+2400;SYMBOL FOR NULL;So;0;ON;;;;;N;GRAPHIC FOR NULL;;;;
+2401;SYMBOL FOR START OF HEADING;So;0;ON;;;;;N;GRAPHIC FOR START OF HEADING;;;;
+2402;SYMBOL FOR START OF TEXT;So;0;ON;;;;;N;GRAPHIC FOR START OF TEXT;;;;
+2403;SYMBOL FOR END OF TEXT;So;0;ON;;;;;N;GRAPHIC FOR END OF TEXT;;;;
+2404;SYMBOL FOR END OF TRANSMISSION;So;0;ON;;;;;N;GRAPHIC FOR END OF TRANSMISSION;;;;
+2405;SYMBOL FOR ENQUIRY;So;0;ON;;;;;N;GRAPHIC FOR ENQUIRY;;;;
+2406;SYMBOL FOR ACKNOWLEDGE;So;0;ON;;;;;N;GRAPHIC FOR ACKNOWLEDGE;;;;
+2407;SYMBOL FOR BELL;So;0;ON;;;;;N;GRAPHIC FOR BELL;;;;
+2408;SYMBOL FOR BACKSPACE;So;0;ON;;;;;N;GRAPHIC FOR BACKSPACE;;;;
+2409;SYMBOL FOR HORIZONTAL TABULATION;So;0;ON;;;;;N;GRAPHIC FOR HORIZONTAL TABULATION;;;;
+240A;SYMBOL FOR LINE FEED;So;0;ON;;;;;N;GRAPHIC FOR LINE FEED;;;;
+240B;SYMBOL FOR VERTICAL TABULATION;So;0;ON;;;;;N;GRAPHIC FOR VERTICAL TABULATION;;;;
+240C;SYMBOL FOR FORM FEED;So;0;ON;;;;;N;GRAPHIC FOR FORM FEED;;;;
+240D;SYMBOL FOR CARRIAGE RETURN;So;0;ON;;;;;N;GRAPHIC FOR CARRIAGE RETURN;;;;
+240E;SYMBOL FOR SHIFT OUT;So;0;ON;;;;;N;GRAPHIC FOR SHIFT OUT;;;;
+240F;SYMBOL FOR SHIFT IN;So;0;ON;;;;;N;GRAPHIC FOR SHIFT IN;;;;
+2410;SYMBOL FOR DATA LINK ESCAPE;So;0;ON;;;;;N;GRAPHIC FOR DATA LINK ESCAPE;;;;
+2411;SYMBOL FOR DEVICE CONTROL ONE;So;0;ON;;;;;N;GRAPHIC FOR DEVICE CONTROL ONE;;;;
+2412;SYMBOL FOR DEVICE CONTROL TWO;So;0;ON;;;;;N;GRAPHIC FOR DEVICE CONTROL TWO;;;;
+2413;SYMBOL FOR DEVICE CONTROL THREE;So;0;ON;;;;;N;GRAPHIC FOR DEVICE CONTROL THREE;;;;
+2414;SYMBOL FOR DEVICE CONTROL FOUR;So;0;ON;;;;;N;GRAPHIC FOR DEVICE CONTROL FOUR;;;;
+2415;SYMBOL FOR NEGATIVE ACKNOWLEDGE;So;0;ON;;;;;N;GRAPHIC FOR NEGATIVE ACKNOWLEDGE;;;;
+2416;SYMBOL FOR SYNCHRONOUS IDLE;So;0;ON;;;;;N;GRAPHIC FOR SYNCHRONOUS IDLE;;;;
+2417;SYMBOL FOR END OF TRANSMISSION BLOCK;So;0;ON;;;;;N;GRAPHIC FOR END OF TRANSMISSION BLOCK;;;;
+2418;SYMBOL FOR CANCEL;So;0;ON;;;;;N;GRAPHIC FOR CANCEL;;;;
+2419;SYMBOL FOR END OF MEDIUM;So;0;ON;;;;;N;GRAPHIC FOR END OF MEDIUM;;;;
+241A;SYMBOL FOR SUBSTITUTE;So;0;ON;;;;;N;GRAPHIC FOR SUBSTITUTE;;;;
+241B;SYMBOL FOR ESCAPE;So;0;ON;;;;;N;GRAPHIC FOR ESCAPE;;;;
+241C;SYMBOL FOR FILE SEPARATOR;So;0;ON;;;;;N;GRAPHIC FOR FILE SEPARATOR;;;;
+241D;SYMBOL FOR GROUP SEPARATOR;So;0;ON;;;;;N;GRAPHIC FOR GROUP SEPARATOR;;;;
+241E;SYMBOL FOR RECORD SEPARATOR;So;0;ON;;;;;N;GRAPHIC FOR RECORD SEPARATOR;;;;
+241F;SYMBOL FOR UNIT SEPARATOR;So;0;ON;;;;;N;GRAPHIC FOR UNIT SEPARATOR;;;;
+2420;SYMBOL FOR SPACE;So;0;ON;;;;;N;GRAPHIC FOR SPACE;;;;
+2421;SYMBOL FOR DELETE;So;0;ON;;;;;N;GRAPHIC FOR DELETE;;;;
+2422;BLANK SYMBOL;So;0;ON;;;;;N;BLANK;;;;
+2423;OPEN BOX;So;0;ON;;;;;N;;;;;
+2424;SYMBOL FOR NEWLINE;So;0;ON;;;;;N;GRAPHIC FOR NEWLINE;;;;
+2425;SYMBOL FOR DELETE FORM TWO;So;0;ON;;;;;N;;;;;
+2426;SYMBOL FOR SUBSTITUTE FORM TWO;So;0;ON;;;;;N;;;;;
+2440;OCR HOOK;So;0;ON;;;;;N;;;;;
+2441;OCR CHAIR;So;0;ON;;;;;N;;;;;
+2442;OCR FORK;So;0;ON;;;;;N;;;;;
+2443;OCR INVERTED FORK;So;0;ON;;;;;N;;;;;
+2444;OCR BELT BUCKLE;So;0;ON;;;;;N;;;;;
+2445;OCR BOW TIE;So;0;ON;;;;;N;;;;;
+2446;OCR BRANCH BANK IDENTIFICATION;So;0;ON;;;;;N;;;;;
+2447;OCR AMOUNT OF CHECK;So;0;ON;;;;;N;;;;;
+2448;OCR DASH;So;0;ON;;;;;N;;;;;
+2449;OCR CUSTOMER ACCOUNT NUMBER;So;0;ON;;;;;N;;;;;
+244A;OCR DOUBLE BACKSLASH;So;0;ON;;;;;N;;;;;
+2460;CIRCLED DIGIT ONE;No;0;ON;<circle> 0031;;1;1;N;;;;;
+2461;CIRCLED DIGIT TWO;No;0;ON;<circle> 0032;;2;2;N;;;;;
+2462;CIRCLED DIGIT THREE;No;0;ON;<circle> 0033;;3;3;N;;;;;
+2463;CIRCLED DIGIT FOUR;No;0;ON;<circle> 0034;;4;4;N;;;;;
+2464;CIRCLED DIGIT FIVE;No;0;ON;<circle> 0035;;5;5;N;;;;;
+2465;CIRCLED DIGIT SIX;No;0;ON;<circle> 0036;;6;6;N;;;;;
+2466;CIRCLED DIGIT SEVEN;No;0;ON;<circle> 0037;;7;7;N;;;;;
+2467;CIRCLED DIGIT EIGHT;No;0;ON;<circle> 0038;;8;8;N;;;;;
+2468;CIRCLED DIGIT NINE;No;0;ON;<circle> 0039;;9;9;N;;;;;
+2469;CIRCLED NUMBER TEN;No;0;ON;<circle> 0031 0030;;;10;N;;;;;
+246A;CIRCLED NUMBER ELEVEN;No;0;ON;<circle> 0031 0031;;;11;N;;;;;
+246B;CIRCLED NUMBER TWELVE;No;0;ON;<circle> 0031 0032;;;12;N;;;;;
+246C;CIRCLED NUMBER THIRTEEN;No;0;ON;<circle> 0031 0033;;;13;N;;;;;
+246D;CIRCLED NUMBER FOURTEEN;No;0;ON;<circle> 0031 0034;;;14;N;;;;;
+246E;CIRCLED NUMBER FIFTEEN;No;0;ON;<circle> 0031 0035;;;15;N;;;;;
+246F;CIRCLED NUMBER SIXTEEN;No;0;ON;<circle> 0031 0036;;;16;N;;;;;
+2470;CIRCLED NUMBER SEVENTEEN;No;0;ON;<circle> 0031 0037;;;17;N;;;;;
+2471;CIRCLED NUMBER EIGHTEEN;No;0;ON;<circle> 0031 0038;;;18;N;;;;;
+2472;CIRCLED NUMBER NINETEEN;No;0;ON;<circle> 0031 0039;;;19;N;;;;;
+2473;CIRCLED NUMBER TWENTY;No;0;ON;<circle> 0032 0030;;;20;N;;;;;
+2474;PARENTHESIZED DIGIT ONE;No;0;ON;<compat> 0028 0031 0029;;1;1;N;;;;;
+2475;PARENTHESIZED DIGIT TWO;No;0;ON;<compat> 0028 0032 0029;;2;2;N;;;;;
+2476;PARENTHESIZED DIGIT THREE;No;0;ON;<compat> 0028 0033 0029;;3;3;N;;;;;
+2477;PARENTHESIZED DIGIT FOUR;No;0;ON;<compat> 0028 0034 0029;;4;4;N;;;;;
+2478;PARENTHESIZED DIGIT FIVE;No;0;ON;<compat> 0028 0035 0029;;5;5;N;;;;;
+2479;PARENTHESIZED DIGIT SIX;No;0;ON;<compat> 0028 0036 0029;;6;6;N;;;;;
+247A;PARENTHESIZED DIGIT SEVEN;No;0;ON;<compat> 0028 0037 0029;;7;7;N;;;;;
+247B;PARENTHESIZED DIGIT EIGHT;No;0;ON;<compat> 0028 0038 0029;;8;8;N;;;;;
+247C;PARENTHESIZED DIGIT NINE;No;0;ON;<compat> 0028 0039 0029;;9;9;N;;;;;
+247D;PARENTHESIZED NUMBER TEN;No;0;ON;<compat> 0028 0031 0030 0029;;;10;N;;;;;
+247E;PARENTHESIZED NUMBER ELEVEN;No;0;ON;<compat> 0028 0031 0031 0029;;;11;N;;;;;
+247F;PARENTHESIZED NUMBER TWELVE;No;0;ON;<compat> 0028 0031 0032 0029;;;12;N;;;;;
+2480;PARENTHESIZED NUMBER THIRTEEN;No;0;ON;<compat> 0028 0031 0033 0029;;;13;N;;;;;
+2481;PARENTHESIZED NUMBER FOURTEEN;No;0;ON;<compat> 0028 0031 0034 0029;;;14;N;;;;;
+2482;PARENTHESIZED NUMBER FIFTEEN;No;0;ON;<compat> 0028 0031 0035 0029;;;15;N;;;;;
+2483;PARENTHESIZED NUMBER SIXTEEN;No;0;ON;<compat> 0028 0031 0036 0029;;;16;N;;;;;
+2484;PARENTHESIZED NUMBER SEVENTEEN;No;0;ON;<compat> 0028 0031 0037 0029;;;17;N;;;;;
+2485;PARENTHESIZED NUMBER EIGHTEEN;No;0;ON;<compat> 0028 0031 0038 0029;;;18;N;;;;;
+2486;PARENTHESIZED NUMBER NINETEEN;No;0;ON;<compat> 0028 0031 0039 0029;;;19;N;;;;;
+2487;PARENTHESIZED NUMBER TWENTY;No;0;ON;<compat> 0028 0032 0030 0029;;;20;N;;;;;
+2488;DIGIT ONE FULL STOP;No;0;EN;<compat> 0031 002E;;1;1;N;DIGIT ONE PERIOD;;;;
+2489;DIGIT TWO FULL STOP;No;0;EN;<compat> 0032 002E;;2;2;N;DIGIT TWO PERIOD;;;;
+248A;DIGIT THREE FULL STOP;No;0;EN;<compat> 0033 002E;;3;3;N;DIGIT THREE PERIOD;;;;
+248B;DIGIT FOUR FULL STOP;No;0;EN;<compat> 0034 002E;;4;4;N;DIGIT FOUR PERIOD;;;;
+248C;DIGIT FIVE FULL STOP;No;0;EN;<compat> 0035 002E;;5;5;N;DIGIT FIVE PERIOD;;;;
+248D;DIGIT SIX FULL STOP;No;0;EN;<compat> 0036 002E;;6;6;N;DIGIT SIX PERIOD;;;;
+248E;DIGIT SEVEN FULL STOP;No;0;EN;<compat> 0037 002E;;7;7;N;DIGIT SEVEN PERIOD;;;;
+248F;DIGIT EIGHT FULL STOP;No;0;EN;<compat> 0038 002E;;8;8;N;DIGIT EIGHT PERIOD;;;;
+2490;DIGIT NINE FULL STOP;No;0;EN;<compat> 0039 002E;;9;9;N;DIGIT NINE PERIOD;;;;
+2491;NUMBER TEN FULL STOP;No;0;EN;<compat> 0031 0030 002E;;;10;N;NUMBER TEN PERIOD;;;;
+2492;NUMBER ELEVEN FULL STOP;No;0;EN;<compat> 0031 0031 002E;;;11;N;NUMBER ELEVEN PERIOD;;;;
+2493;NUMBER TWELVE FULL STOP;No;0;EN;<compat> 0031 0032 002E;;;12;N;NUMBER TWELVE PERIOD;;;;
+2494;NUMBER THIRTEEN FULL STOP;No;0;EN;<compat> 0031 0033 002E;;;13;N;NUMBER THIRTEEN PERIOD;;;;
+2495;NUMBER FOURTEEN FULL STOP;No;0;EN;<compat> 0031 0034 002E;;;14;N;NUMBER FOURTEEN PERIOD;;;;
+2496;NUMBER FIFTEEN FULL STOP;No;0;EN;<compat> 0031 0035 002E;;;15;N;NUMBER FIFTEEN PERIOD;;;;
+2497;NUMBER SIXTEEN FULL STOP;No;0;EN;<compat> 0031 0036 002E;;;16;N;NUMBER SIXTEEN PERIOD;;;;
+2498;NUMBER SEVENTEEN FULL STOP;No;0;EN;<compat> 0031 0037 002E;;;17;N;NUMBER SEVENTEEN PERIOD;;;;
+2499;NUMBER EIGHTEEN FULL STOP;No;0;EN;<compat> 0031 0038 002E;;;18;N;NUMBER EIGHTEEN PERIOD;;;;
+249A;NUMBER NINETEEN FULL STOP;No;0;EN;<compat> 0031 0039 002E;;;19;N;NUMBER NINETEEN PERIOD;;;;
+249B;NUMBER TWENTY FULL STOP;No;0;EN;<compat> 0032 0030 002E;;;20;N;NUMBER TWENTY PERIOD;;;;
+249C;PARENTHESIZED LATIN SMALL LETTER A;So;0;L;<compat> 0028 0061 0029;;;;N;;;;;
+249D;PARENTHESIZED LATIN SMALL LETTER B;So;0;L;<compat> 0028 0062 0029;;;;N;;;;;
+249E;PARENTHESIZED LATIN SMALL LETTER C;So;0;L;<compat> 0028 0063 0029;;;;N;;;;;
+249F;PARENTHESIZED LATIN SMALL LETTER D;So;0;L;<compat> 0028 0064 0029;;;;N;;;;;
+24A0;PARENTHESIZED LATIN SMALL LETTER E;So;0;L;<compat> 0028 0065 0029;;;;N;;;;;
+24A1;PARENTHESIZED LATIN SMALL LETTER F;So;0;L;<compat> 0028 0066 0029;;;;N;;;;;
+24A2;PARENTHESIZED LATIN SMALL LETTER G;So;0;L;<compat> 0028 0067 0029;;;;N;;;;;
+24A3;PARENTHESIZED LATIN SMALL LETTER H;So;0;L;<compat> 0028 0068 0029;;;;N;;;;;
+24A4;PARENTHESIZED LATIN SMALL LETTER I;So;0;L;<compat> 0028 0069 0029;;;;N;;;;;
+24A5;PARENTHESIZED LATIN SMALL LETTER J;So;0;L;<compat> 0028 006A 0029;;;;N;;;;;
+24A6;PARENTHESIZED LATIN SMALL LETTER K;So;0;L;<compat> 0028 006B 0029;;;;N;;;;;
+24A7;PARENTHESIZED LATIN SMALL LETTER L;So;0;L;<compat> 0028 006C 0029;;;;N;;;;;
+24A8;PARENTHESIZED LATIN SMALL LETTER M;So;0;L;<compat> 0028 006D 0029;;;;N;;;;;
+24A9;PARENTHESIZED LATIN SMALL LETTER N;So;0;L;<compat> 0028 006E 0029;;;;N;;;;;
+24AA;PARENTHESIZED LATIN SMALL LETTER O;So;0;L;<compat> 0028 006F 0029;;;;N;;;;;
+24AB;PARENTHESIZED LATIN SMALL LETTER P;So;0;L;<compat> 0028 0070 0029;;;;N;;;;;
+24AC;PARENTHESIZED LATIN SMALL LETTER Q;So;0;L;<compat> 0028 0071 0029;;;;N;;;;;
+24AD;PARENTHESIZED LATIN SMALL LETTER R;So;0;L;<compat> 0028 0072 0029;;;;N;;;;;
+24AE;PARENTHESIZED LATIN SMALL LETTER S;So;0;L;<compat> 0028 0073 0029;;;;N;;;;;
+24AF;PARENTHESIZED LATIN SMALL LETTER T;So;0;L;<compat> 0028 0074 0029;;;;N;;;;;
+24B0;PARENTHESIZED LATIN SMALL LETTER U;So;0;L;<compat> 0028 0075 0029;;;;N;;;;;
+24B1;PARENTHESIZED LATIN SMALL LETTER V;So;0;L;<compat> 0028 0076 0029;;;;N;;;;;
+24B2;PARENTHESIZED LATIN SMALL LETTER W;So;0;L;<compat> 0028 0077 0029;;;;N;;;;;
+24B3;PARENTHESIZED LATIN SMALL LETTER X;So;0;L;<compat> 0028 0078 0029;;;;N;;;;;
+24B4;PARENTHESIZED LATIN SMALL LETTER Y;So;0;L;<compat> 0028 0079 0029;;;;N;;;;;
+24B5;PARENTHESIZED LATIN SMALL LETTER Z;So;0;L;<compat> 0028 007A 0029;;;;N;;;;;
+24B6;CIRCLED LATIN CAPITAL LETTER A;So;0;L;<circle> 0041;;;;N;;;;24D0;
+24B7;CIRCLED LATIN CAPITAL LETTER B;So;0;L;<circle> 0042;;;;N;;;;24D1;
+24B8;CIRCLED LATIN CAPITAL LETTER C;So;0;L;<circle> 0043;;;;N;;;;24D2;
+24B9;CIRCLED LATIN CAPITAL LETTER D;So;0;L;<circle> 0044;;;;N;;;;24D3;
+24BA;CIRCLED LATIN CAPITAL LETTER E;So;0;L;<circle> 0045;;;;N;;;;24D4;
+24BB;CIRCLED LATIN CAPITAL LETTER F;So;0;L;<circle> 0046;;;;N;;;;24D5;
+24BC;CIRCLED LATIN CAPITAL LETTER G;So;0;L;<circle> 0047;;;;N;;;;24D6;
+24BD;CIRCLED LATIN CAPITAL LETTER H;So;0;L;<circle> 0048;;;;N;;;;24D7;
+24BE;CIRCLED LATIN CAPITAL LETTER I;So;0;L;<circle> 0049;;;;N;;;;24D8;
+24BF;CIRCLED LATIN CAPITAL LETTER J;So;0;L;<circle> 004A;;;;N;;;;24D9;
+24C0;CIRCLED LATIN CAPITAL LETTER K;So;0;L;<circle> 004B;;;;N;;;;24DA;
+24C1;CIRCLED LATIN CAPITAL LETTER L;So;0;L;<circle> 004C;;;;N;;;;24DB;
+24C2;CIRCLED LATIN CAPITAL LETTER M;So;0;L;<circle> 004D;;;;N;;;;24DC;
+24C3;CIRCLED LATIN CAPITAL LETTER N;So;0;L;<circle> 004E;;;;N;;;;24DD;
+24C4;CIRCLED LATIN CAPITAL LETTER O;So;0;L;<circle> 004F;;;;N;;;;24DE;
+24C5;CIRCLED LATIN CAPITAL LETTER P;So;0;L;<circle> 0050;;;;N;;;;24DF;
+24C6;CIRCLED LATIN CAPITAL LETTER Q;So;0;L;<circle> 0051;;;;N;;;;24E0;
+24C7;CIRCLED LATIN CAPITAL LETTER R;So;0;L;<circle> 0052;;;;N;;;;24E1;
+24C8;CIRCLED LATIN CAPITAL LETTER S;So;0;L;<circle> 0053;;;;N;;;;24E2;
+24C9;CIRCLED LATIN CAPITAL LETTER T;So;0;L;<circle> 0054;;;;N;;;;24E3;
+24CA;CIRCLED LATIN CAPITAL LETTER U;So;0;L;<circle> 0055;;;;N;;;;24E4;
+24CB;CIRCLED LATIN CAPITAL LETTER V;So;0;L;<circle> 0056;;;;N;;;;24E5;
+24CC;CIRCLED LATIN CAPITAL LETTER W;So;0;L;<circle> 0057;;;;N;;;;24E6;
+24CD;CIRCLED LATIN CAPITAL LETTER X;So;0;L;<circle> 0058;;;;N;;;;24E7;
+24CE;CIRCLED LATIN CAPITAL LETTER Y;So;0;L;<circle> 0059;;;;N;;;;24E8;
+24CF;CIRCLED LATIN CAPITAL LETTER Z;So;0;L;<circle> 005A;;;;N;;;;24E9;
+24D0;CIRCLED LATIN SMALL LETTER A;So;0;L;<circle> 0061;;;;N;;;24B6;;24B6
+24D1;CIRCLED LATIN SMALL LETTER B;So;0;L;<circle> 0062;;;;N;;;24B7;;24B7
+24D2;CIRCLED LATIN SMALL LETTER C;So;0;L;<circle> 0063;;;;N;;;24B8;;24B8
+24D3;CIRCLED LATIN SMALL LETTER D;So;0;L;<circle> 0064;;;;N;;;24B9;;24B9
+24D4;CIRCLED LATIN SMALL LETTER E;So;0;L;<circle> 0065;;;;N;;;24BA;;24BA
+24D5;CIRCLED LATIN SMALL LETTER F;So;0;L;<circle> 0066;;;;N;;;24BB;;24BB
+24D6;CIRCLED LATIN SMALL LETTER G;So;0;L;<circle> 0067;;;;N;;;24BC;;24BC
+24D7;CIRCLED LATIN SMALL LETTER H;So;0;L;<circle> 0068;;;;N;;;24BD;;24BD
+24D8;CIRCLED LATIN SMALL LETTER I;So;0;L;<circle> 0069;;;;N;;;24BE;;24BE
+24D9;CIRCLED LATIN SMALL LETTER J;So;0;L;<circle> 006A;;;;N;;;24BF;;24BF
+24DA;CIRCLED LATIN SMALL LETTER K;So;0;L;<circle> 006B;;;;N;;;24C0;;24C0
+24DB;CIRCLED LATIN SMALL LETTER L;So;0;L;<circle> 006C;;;;N;;;24C1;;24C1
+24DC;CIRCLED LATIN SMALL LETTER M;So;0;L;<circle> 006D;;;;N;;;24C2;;24C2
+24DD;CIRCLED LATIN SMALL LETTER N;So;0;L;<circle> 006E;;;;N;;;24C3;;24C3
+24DE;CIRCLED LATIN SMALL LETTER O;So;0;L;<circle> 006F;;;;N;;;24C4;;24C4
+24DF;CIRCLED LATIN SMALL LETTER P;So;0;L;<circle> 0070;;;;N;;;24C5;;24C5
+24E0;CIRCLED LATIN SMALL LETTER Q;So;0;L;<circle> 0071;;;;N;;;24C6;;24C6
+24E1;CIRCLED LATIN SMALL LETTER R;So;0;L;<circle> 0072;;;;N;;;24C7;;24C7
+24E2;CIRCLED LATIN SMALL LETTER S;So;0;L;<circle> 0073;;;;N;;;24C8;;24C8
+24E3;CIRCLED LATIN SMALL LETTER T;So;0;L;<circle> 0074;;;;N;;;24C9;;24C9
+24E4;CIRCLED LATIN SMALL LETTER U;So;0;L;<circle> 0075;;;;N;;;24CA;;24CA
+24E5;CIRCLED LATIN SMALL LETTER V;So;0;L;<circle> 0076;;;;N;;;24CB;;24CB
+24E6;CIRCLED LATIN SMALL LETTER W;So;0;L;<circle> 0077;;;;N;;;24CC;;24CC
+24E7;CIRCLED LATIN SMALL LETTER X;So;0;L;<circle> 0078;;;;N;;;24CD;;24CD
+24E8;CIRCLED LATIN SMALL LETTER Y;So;0;L;<circle> 0079;;;;N;;;24CE;;24CE
+24E9;CIRCLED LATIN SMALL LETTER Z;So;0;L;<circle> 007A;;;;N;;;24CF;;24CF
+24EA;CIRCLED DIGIT ZERO;No;0;ON;<circle> 0030;;0;0;N;;;;;
+24EB;NEGATIVE CIRCLED NUMBER ELEVEN;No;0;ON;;;;11;N;;;;;
+24EC;NEGATIVE CIRCLED NUMBER TWELVE;No;0;ON;;;;12;N;;;;;
+24ED;NEGATIVE CIRCLED NUMBER THIRTEEN;No;0;ON;;;;13;N;;;;;
+24EE;NEGATIVE CIRCLED NUMBER FOURTEEN;No;0;ON;;;;14;N;;;;;
+24EF;NEGATIVE CIRCLED NUMBER FIFTEEN;No;0;ON;;;;15;N;;;;;
+24F0;NEGATIVE CIRCLED NUMBER SIXTEEN;No;0;ON;;;;16;N;;;;;
+24F1;NEGATIVE CIRCLED NUMBER SEVENTEEN;No;0;ON;;;;17;N;;;;;
+24F2;NEGATIVE CIRCLED NUMBER EIGHTEEN;No;0;ON;;;;18;N;;;;;
+24F3;NEGATIVE CIRCLED NUMBER NINETEEN;No;0;ON;;;;19;N;;;;;
+24F4;NEGATIVE CIRCLED NUMBER TWENTY;No;0;ON;;;;20;N;;;;;
+24F5;DOUBLE CIRCLED DIGIT ONE;No;0;ON;;;1;1;N;;;;;
+24F6;DOUBLE CIRCLED DIGIT TWO;No;0;ON;;;2;2;N;;;;;
+24F7;DOUBLE CIRCLED DIGIT THREE;No;0;ON;;;3;3;N;;;;;
+24F8;DOUBLE CIRCLED DIGIT FOUR;No;0;ON;;;4;4;N;;;;;
+24F9;DOUBLE CIRCLED DIGIT FIVE;No;0;ON;;;5;5;N;;;;;
+24FA;DOUBLE CIRCLED DIGIT SIX;No;0;ON;;;6;6;N;;;;;
+24FB;DOUBLE CIRCLED DIGIT SEVEN;No;0;ON;;;7;7;N;;;;;
+24FC;DOUBLE CIRCLED DIGIT EIGHT;No;0;ON;;;8;8;N;;;;;
+24FD;DOUBLE CIRCLED DIGIT NINE;No;0;ON;;;9;9;N;;;;;
+24FE;DOUBLE CIRCLED NUMBER TEN;No;0;ON;;;;10;N;;;;;
+24FF;NEGATIVE CIRCLED DIGIT ZERO;No;0;ON;;;0;0;N;;;;;
+2500;BOX DRAWINGS LIGHT HORIZONTAL;So;0;ON;;;;;N;FORMS LIGHT HORIZONTAL;;;;
+2501;BOX DRAWINGS HEAVY HORIZONTAL;So;0;ON;;;;;N;FORMS HEAVY HORIZONTAL;;;;
+2502;BOX DRAWINGS LIGHT VERTICAL;So;0;ON;;;;;N;FORMS LIGHT VERTICAL;;;;
+2503;BOX DRAWINGS HEAVY VERTICAL;So;0;ON;;;;;N;FORMS HEAVY VERTICAL;;;;
+2504;BOX DRAWINGS LIGHT TRIPLE DASH HORIZONTAL;So;0;ON;;;;;N;FORMS LIGHT TRIPLE DASH HORIZONTAL;;;;
+2505;BOX DRAWINGS HEAVY TRIPLE DASH HORIZONTAL;So;0;ON;;;;;N;FORMS HEAVY TRIPLE DASH HORIZONTAL;;;;
+2506;BOX DRAWINGS LIGHT TRIPLE DASH VERTICAL;So;0;ON;;;;;N;FORMS LIGHT TRIPLE DASH VERTICAL;;;;
+2507;BOX DRAWINGS HEAVY TRIPLE DASH VERTICAL;So;0;ON;;;;;N;FORMS HEAVY TRIPLE DASH VERTICAL;;;;
+2508;BOX DRAWINGS LIGHT QUADRUPLE DASH HORIZONTAL;So;0;ON;;;;;N;FORMS LIGHT QUADRUPLE DASH HORIZONTAL;;;;
+2509;BOX DRAWINGS HEAVY QUADRUPLE DASH HORIZONTAL;So;0;ON;;;;;N;FORMS HEAVY QUADRUPLE DASH HORIZONTAL;;;;
+250A;BOX DRAWINGS LIGHT QUADRUPLE DASH VERTICAL;So;0;ON;;;;;N;FORMS LIGHT QUADRUPLE DASH VERTICAL;;;;
+250B;BOX DRAWINGS HEAVY QUADRUPLE DASH VERTICAL;So;0;ON;;;;;N;FORMS HEAVY QUADRUPLE DASH VERTICAL;;;;
+250C;BOX DRAWINGS LIGHT DOWN AND RIGHT;So;0;ON;;;;;N;FORMS LIGHT DOWN AND RIGHT;;;;
+250D;BOX DRAWINGS DOWN LIGHT AND RIGHT HEAVY;So;0;ON;;;;;N;FORMS DOWN LIGHT AND RIGHT HEAVY;;;;
+250E;BOX DRAWINGS DOWN HEAVY AND RIGHT LIGHT;So;0;ON;;;;;N;FORMS DOWN HEAVY AND RIGHT LIGHT;;;;
+250F;BOX DRAWINGS HEAVY DOWN AND RIGHT;So;0;ON;;;;;N;FORMS HEAVY DOWN AND RIGHT;;;;
+2510;BOX DRAWINGS LIGHT DOWN AND LEFT;So;0;ON;;;;;N;FORMS LIGHT DOWN AND LEFT;;;;
+2511;BOX DRAWINGS DOWN LIGHT AND LEFT HEAVY;So;0;ON;;;;;N;FORMS DOWN LIGHT AND LEFT HEAVY;;;;
+2512;BOX DRAWINGS DOWN HEAVY AND LEFT LIGHT;So;0;ON;;;;;N;FORMS DOWN HEAVY AND LEFT LIGHT;;;;
+2513;BOX DRAWINGS HEAVY DOWN AND LEFT;So;0;ON;;;;;N;FORMS HEAVY DOWN AND LEFT;;;;
+2514;BOX DRAWINGS LIGHT UP AND RIGHT;So;0;ON;;;;;N;FORMS LIGHT UP AND RIGHT;;;;
+2515;BOX DRAWINGS UP LIGHT AND RIGHT HEAVY;So;0;ON;;;;;N;FORMS UP LIGHT AND RIGHT HEAVY;;;;
+2516;BOX DRAWINGS UP HEAVY AND RIGHT LIGHT;So;0;ON;;;;;N;FORMS UP HEAVY AND RIGHT LIGHT;;;;
+2517;BOX DRAWINGS HEAVY UP AND RIGHT;So;0;ON;;;;;N;FORMS HEAVY UP AND RIGHT;;;;
+2518;BOX DRAWINGS LIGHT UP AND LEFT;So;0;ON;;;;;N;FORMS LIGHT UP AND LEFT;;;;
+2519;BOX DRAWINGS UP LIGHT AND LEFT HEAVY;So;0;ON;;;;;N;FORMS UP LIGHT AND LEFT HEAVY;;;;
+251A;BOX DRAWINGS UP HEAVY AND LEFT LIGHT;So;0;ON;;;;;N;FORMS UP HEAVY AND LEFT LIGHT;;;;
+251B;BOX DRAWINGS HEAVY UP AND LEFT;So;0;ON;;;;;N;FORMS HEAVY UP AND LEFT;;;;
+251C;BOX DRAWINGS LIGHT VERTICAL AND RIGHT;So;0;ON;;;;;N;FORMS LIGHT VERTICAL AND RIGHT;;;;
+251D;BOX DRAWINGS VERTICAL LIGHT AND RIGHT HEAVY;So;0;ON;;;;;N;FORMS VERTICAL LIGHT AND RIGHT HEAVY;;;;
+251E;BOX DRAWINGS UP HEAVY AND RIGHT DOWN LIGHT;So;0;ON;;;;;N;FORMS UP HEAVY AND RIGHT DOWN LIGHT;;;;
+251F;BOX DRAWINGS DOWN HEAVY AND RIGHT UP LIGHT;So;0;ON;;;;;N;FORMS DOWN HEAVY AND RIGHT UP LIGHT;;;;
+2520;BOX DRAWINGS VERTICAL HEAVY AND RIGHT LIGHT;So;0;ON;;;;;N;FORMS VERTICAL HEAVY AND RIGHT LIGHT;;;;
+2521;BOX DRAWINGS DOWN LIGHT AND RIGHT UP HEAVY;So;0;ON;;;;;N;FORMS DOWN LIGHT AND RIGHT UP HEAVY;;;;
+2522;BOX DRAWINGS UP LIGHT AND RIGHT DOWN HEAVY;So;0;ON;;;;;N;FORMS UP LIGHT AND RIGHT DOWN HEAVY;;;;
+2523;BOX DRAWINGS HEAVY VERTICAL AND RIGHT;So;0;ON;;;;;N;FORMS HEAVY VERTICAL AND RIGHT;;;;
+2524;BOX DRAWINGS LIGHT VERTICAL AND LEFT;So;0;ON;;;;;N;FORMS LIGHT VERTICAL AND LEFT;;;;
+2525;BOX DRAWINGS VERTICAL LIGHT AND LEFT HEAVY;So;0;ON;;;;;N;FORMS VERTICAL LIGHT AND LEFT HEAVY;;;;
+2526;BOX DRAWINGS UP HEAVY AND LEFT DOWN LIGHT;So;0;ON;;;;;N;FORMS UP HEAVY AND LEFT DOWN LIGHT;;;;
+2527;BOX DRAWINGS DOWN HEAVY AND LEFT UP LIGHT;So;0;ON;;;;;N;FORMS DOWN HEAVY AND LEFT UP LIGHT;;;;
+2528;BOX DRAWINGS VERTICAL HEAVY AND LEFT LIGHT;So;0;ON;;;;;N;FORMS VERTICAL HEAVY AND LEFT LIGHT;;;;
+2529;BOX DRAWINGS DOWN LIGHT AND LEFT UP HEAVY;So;0;ON;;;;;N;FORMS DOWN LIGHT AND LEFT UP HEAVY;;;;
+252A;BOX DRAWINGS UP LIGHT AND LEFT DOWN HEAVY;So;0;ON;;;;;N;FORMS UP LIGHT AND LEFT DOWN HEAVY;;;;
+252B;BOX DRAWINGS HEAVY VERTICAL AND LEFT;So;0;ON;;;;;N;FORMS HEAVY VERTICAL AND LEFT;;;;
+252C;BOX DRAWINGS LIGHT DOWN AND HORIZONTAL;So;0;ON;;;;;N;FORMS LIGHT DOWN AND HORIZONTAL;;;;
+252D;BOX DRAWINGS LEFT HEAVY AND RIGHT DOWN LIGHT;So;0;ON;;;;;N;FORMS LEFT HEAVY AND RIGHT DOWN LIGHT;;;;
+252E;BOX DRAWINGS RIGHT HEAVY AND LEFT DOWN LIGHT;So;0;ON;;;;;N;FORMS RIGHT HEAVY AND LEFT DOWN LIGHT;;;;
+252F;BOX DRAWINGS DOWN LIGHT AND HORIZONTAL HEAVY;So;0;ON;;;;;N;FORMS DOWN LIGHT AND HORIZONTAL HEAVY;;;;
+2530;BOX DRAWINGS DOWN HEAVY AND HORIZONTAL LIGHT;So;0;ON;;;;;N;FORMS DOWN HEAVY AND HORIZONTAL LIGHT;;;;
+2531;BOX DRAWINGS RIGHT LIGHT AND LEFT DOWN HEAVY;So;0;ON;;;;;N;FORMS RIGHT LIGHT AND LEFT DOWN HEAVY;;;;
+2532;BOX DRAWINGS LEFT LIGHT AND RIGHT DOWN HEAVY;So;0;ON;;;;;N;FORMS LEFT LIGHT AND RIGHT DOWN HEAVY;;;;
+2533;BOX DRAWINGS HEAVY DOWN AND HORIZONTAL;So;0;ON;;;;;N;FORMS HEAVY DOWN AND HORIZONTAL;;;;
+2534;BOX DRAWINGS LIGHT UP AND HORIZONTAL;So;0;ON;;;;;N;FORMS LIGHT UP AND HORIZONTAL;;;;
+2535;BOX DRAWINGS LEFT HEAVY AND RIGHT UP LIGHT;So;0;ON;;;;;N;FORMS LEFT HEAVY AND RIGHT UP LIGHT;;;;
+2536;BOX DRAWINGS RIGHT HEAVY AND LEFT UP LIGHT;So;0;ON;;;;;N;FORMS RIGHT HEAVY AND LEFT UP LIGHT;;;;
+2537;BOX DRAWINGS UP LIGHT AND HORIZONTAL HEAVY;So;0;ON;;;;;N;FORMS UP LIGHT AND HORIZONTAL HEAVY;;;;
+2538;BOX DRAWINGS UP HEAVY AND HORIZONTAL LIGHT;So;0;ON;;;;;N;FORMS UP HEAVY AND HORIZONTAL LIGHT;;;;
+2539;BOX DRAWINGS RIGHT LIGHT AND LEFT UP HEAVY;So;0;ON;;;;;N;FORMS RIGHT LIGHT AND LEFT UP HEAVY;;;;
+253A;BOX DRAWINGS LEFT LIGHT AND RIGHT UP HEAVY;So;0;ON;;;;;N;FORMS LEFT LIGHT AND RIGHT UP HEAVY;;;;
+253B;BOX DRAWINGS HEAVY UP AND HORIZONTAL;So;0;ON;;;;;N;FORMS HEAVY UP AND HORIZONTAL;;;;
+253C;BOX DRAWINGS LIGHT VERTICAL AND HORIZONTAL;So;0;ON;;;;;N;FORMS LIGHT VERTICAL AND HORIZONTAL;;;;
+253D;BOX DRAWINGS LEFT HEAVY AND RIGHT VERTICAL LIGHT;So;0;ON;;;;;N;FORMS LEFT HEAVY AND RIGHT VERTICAL LIGHT;;;;
+253E;BOX DRAWINGS RIGHT HEAVY AND LEFT VERTICAL LIGHT;So;0;ON;;;;;N;FORMS RIGHT HEAVY AND LEFT VERTICAL LIGHT;;;;
+253F;BOX DRAWINGS VERTICAL LIGHT AND HORIZONTAL HEAVY;So;0;ON;;;;;N;FORMS VERTICAL LIGHT AND HORIZONTAL HEAVY;;;;
+2540;BOX DRAWINGS UP HEAVY AND DOWN HORIZONTAL LIGHT;So;0;ON;;;;;N;FORMS UP HEAVY AND DOWN HORIZONTAL LIGHT;;;;
+2541;BOX DRAWINGS DOWN HEAVY AND UP HORIZONTAL LIGHT;So;0;ON;;;;;N;FORMS DOWN HEAVY AND UP HORIZONTAL LIGHT;;;;
+2542;BOX DRAWINGS VERTICAL HEAVY AND HORIZONTAL LIGHT;So;0;ON;;;;;N;FORMS VERTICAL HEAVY AND HORIZONTAL LIGHT;;;;
+2543;BOX DRAWINGS LEFT UP HEAVY AND RIGHT DOWN LIGHT;So;0;ON;;;;;N;FORMS LEFT UP HEAVY AND RIGHT DOWN LIGHT;;;;
+2544;BOX DRAWINGS RIGHT UP HEAVY AND LEFT DOWN LIGHT;So;0;ON;;;;;N;FORMS RIGHT UP HEAVY AND LEFT DOWN LIGHT;;;;
+2545;BOX DRAWINGS LEFT DOWN HEAVY AND RIGHT UP LIGHT;So;0;ON;;;;;N;FORMS LEFT DOWN HEAVY AND RIGHT UP LIGHT;;;;
+2546;BOX DRAWINGS RIGHT DOWN HEAVY AND LEFT UP LIGHT;So;0;ON;;;;;N;FORMS RIGHT DOWN HEAVY AND LEFT UP LIGHT;;;;
+2547;BOX DRAWINGS DOWN LIGHT AND UP HORIZONTAL HEAVY;So;0;ON;;;;;N;FORMS DOWN LIGHT AND UP HORIZONTAL HEAVY;;;;
+2548;BOX DRAWINGS UP LIGHT AND DOWN HORIZONTAL HEAVY;So;0;ON;;;;;N;FORMS UP LIGHT AND DOWN HORIZONTAL HEAVY;;;;
+2549;BOX DRAWINGS RIGHT LIGHT AND LEFT VERTICAL HEAVY;So;0;ON;;;;;N;FORMS RIGHT LIGHT AND LEFT VERTICAL HEAVY;;;;
+254A;BOX DRAWINGS LEFT LIGHT AND RIGHT VERTICAL HEAVY;So;0;ON;;;;;N;FORMS LEFT LIGHT AND RIGHT VERTICAL HEAVY;;;;
+254B;BOX DRAWINGS HEAVY VERTICAL AND HORIZONTAL;So;0;ON;;;;;N;FORMS HEAVY VERTICAL AND HORIZONTAL;;;;
+254C;BOX DRAWINGS LIGHT DOUBLE DASH HORIZONTAL;So;0;ON;;;;;N;FORMS LIGHT DOUBLE DASH HORIZONTAL;;;;
+254D;BOX DRAWINGS HEAVY DOUBLE DASH HORIZONTAL;So;0;ON;;;;;N;FORMS HEAVY DOUBLE DASH HORIZONTAL;;;;
+254E;BOX DRAWINGS LIGHT DOUBLE DASH VERTICAL;So;0;ON;;;;;N;FORMS LIGHT DOUBLE DASH VERTICAL;;;;
+254F;BOX DRAWINGS HEAVY DOUBLE DASH VERTICAL;So;0;ON;;;;;N;FORMS HEAVY DOUBLE DASH VERTICAL;;;;
+2550;BOX DRAWINGS DOUBLE HORIZONTAL;So;0;ON;;;;;N;FORMS DOUBLE HORIZONTAL;;;;
+2551;BOX DRAWINGS DOUBLE VERTICAL;So;0;ON;;;;;N;FORMS DOUBLE VERTICAL;;;;
+2552;BOX DRAWINGS DOWN SINGLE AND RIGHT DOUBLE;So;0;ON;;;;;N;FORMS DOWN SINGLE AND RIGHT DOUBLE;;;;
+2553;BOX DRAWINGS DOWN DOUBLE AND RIGHT SINGLE;So;0;ON;;;;;N;FORMS DOWN DOUBLE AND RIGHT SINGLE;;;;
+2554;BOX DRAWINGS DOUBLE DOWN AND RIGHT;So;0;ON;;;;;N;FORMS DOUBLE DOWN AND RIGHT;;;;
+2555;BOX DRAWINGS DOWN SINGLE AND LEFT DOUBLE;So;0;ON;;;;;N;FORMS DOWN SINGLE AND LEFT DOUBLE;;;;
+2556;BOX DRAWINGS DOWN DOUBLE AND LEFT SINGLE;So;0;ON;;;;;N;FORMS DOWN DOUBLE AND LEFT SINGLE;;;;
+2557;BOX DRAWINGS DOUBLE DOWN AND LEFT;So;0;ON;;;;;N;FORMS DOUBLE DOWN AND LEFT;;;;
+2558;BOX DRAWINGS UP SINGLE AND RIGHT DOUBLE;So;0;ON;;;;;N;FORMS UP SINGLE AND RIGHT DOUBLE;;;;
+2559;BOX DRAWINGS UP DOUBLE AND RIGHT SINGLE;So;0;ON;;;;;N;FORMS UP DOUBLE AND RIGHT SINGLE;;;;
+255A;BOX DRAWINGS DOUBLE UP AND RIGHT;So;0;ON;;;;;N;FORMS DOUBLE UP AND RIGHT;;;;
+255B;BOX DRAWINGS UP SINGLE AND LEFT DOUBLE;So;0;ON;;;;;N;FORMS UP SINGLE AND LEFT DOUBLE;;;;
+255C;BOX DRAWINGS UP DOUBLE AND LEFT SINGLE;So;0;ON;;;;;N;FORMS UP DOUBLE AND LEFT SINGLE;;;;
+255D;BOX DRAWINGS DOUBLE UP AND LEFT;So;0;ON;;;;;N;FORMS DOUBLE UP AND LEFT;;;;
+255E;BOX DRAWINGS VERTICAL SINGLE AND RIGHT DOUBLE;So;0;ON;;;;;N;FORMS VERTICAL SINGLE AND RIGHT DOUBLE;;;;
+255F;BOX DRAWINGS VERTICAL DOUBLE AND RIGHT SINGLE;So;0;ON;;;;;N;FORMS VERTICAL DOUBLE AND RIGHT SINGLE;;;;
+2560;BOX DRAWINGS DOUBLE VERTICAL AND RIGHT;So;0;ON;;;;;N;FORMS DOUBLE VERTICAL AND RIGHT;;;;
+2561;BOX DRAWINGS VERTICAL SINGLE AND LEFT DOUBLE;So;0;ON;;;;;N;FORMS VERTICAL SINGLE AND LEFT DOUBLE;;;;
+2562;BOX DRAWINGS VERTICAL DOUBLE AND LEFT SINGLE;So;0;ON;;;;;N;FORMS VERTICAL DOUBLE AND LEFT SINGLE;;;;
+2563;BOX DRAWINGS DOUBLE VERTICAL AND LEFT;So;0;ON;;;;;N;FORMS DOUBLE VERTICAL AND LEFT;;;;
+2564;BOX DRAWINGS DOWN SINGLE AND HORIZONTAL DOUBLE;So;0;ON;;;;;N;FORMS DOWN SINGLE AND HORIZONTAL DOUBLE;;;;
+2565;BOX DRAWINGS DOWN DOUBLE AND HORIZONTAL SINGLE;So;0;ON;;;;;N;FORMS DOWN DOUBLE AND HORIZONTAL SINGLE;;;;
+2566;BOX DRAWINGS DOUBLE DOWN AND HORIZONTAL;So;0;ON;;;;;N;FORMS DOUBLE DOWN AND HORIZONTAL;;;;
+2567;BOX DRAWINGS UP SINGLE AND HORIZONTAL DOUBLE;So;0;ON;;;;;N;FORMS UP SINGLE AND HORIZONTAL DOUBLE;;;;
+2568;BOX DRAWINGS UP DOUBLE AND HORIZONTAL SINGLE;So;0;ON;;;;;N;FORMS UP DOUBLE AND HORIZONTAL SINGLE;;;;
+2569;BOX DRAWINGS DOUBLE UP AND HORIZONTAL;So;0;ON;;;;;N;FORMS DOUBLE UP AND HORIZONTAL;;;;
+256A;BOX DRAWINGS VERTICAL SINGLE AND HORIZONTAL DOUBLE;So;0;ON;;;;;N;FORMS VERTICAL SINGLE AND HORIZONTAL DOUBLE;;;;
+256B;BOX DRAWINGS VERTICAL DOUBLE AND HORIZONTAL SINGLE;So;0;ON;;;;;N;FORMS VERTICAL DOUBLE AND HORIZONTAL SINGLE;;;;
+256C;BOX DRAWINGS DOUBLE VERTICAL AND HORIZONTAL;So;0;ON;;;;;N;FORMS DOUBLE VERTICAL AND HORIZONTAL;;;;
+256D;BOX DRAWINGS LIGHT ARC DOWN AND RIGHT;So;0;ON;;;;;N;FORMS LIGHT ARC DOWN AND RIGHT;;;;
+256E;BOX DRAWINGS LIGHT ARC DOWN AND LEFT;So;0;ON;;;;;N;FORMS LIGHT ARC DOWN AND LEFT;;;;
+256F;BOX DRAWINGS LIGHT ARC UP AND LEFT;So;0;ON;;;;;N;FORMS LIGHT ARC UP AND LEFT;;;;
+2570;BOX DRAWINGS LIGHT ARC UP AND RIGHT;So;0;ON;;;;;N;FORMS LIGHT ARC UP AND RIGHT;;;;
+2571;BOX DRAWINGS LIGHT DIAGONAL UPPER RIGHT TO LOWER LEFT;So;0;ON;;;;;N;FORMS LIGHT DIAGONAL UPPER RIGHT TO LOWER LEFT;;;;
+2572;BOX DRAWINGS LIGHT DIAGONAL UPPER LEFT TO LOWER RIGHT;So;0;ON;;;;;N;FORMS LIGHT DIAGONAL UPPER LEFT TO LOWER RIGHT;;;;
+2573;BOX DRAWINGS LIGHT DIAGONAL CROSS;So;0;ON;;;;;N;FORMS LIGHT DIAGONAL CROSS;;;;
+2574;BOX DRAWINGS LIGHT LEFT;So;0;ON;;;;;N;FORMS LIGHT LEFT;;;;
+2575;BOX DRAWINGS LIGHT UP;So;0;ON;;;;;N;FORMS LIGHT UP;;;;
+2576;BOX DRAWINGS LIGHT RIGHT;So;0;ON;;;;;N;FORMS LIGHT RIGHT;;;;
+2577;BOX DRAWINGS LIGHT DOWN;So;0;ON;;;;;N;FORMS LIGHT DOWN;;;;
+2578;BOX DRAWINGS HEAVY LEFT;So;0;ON;;;;;N;FORMS HEAVY LEFT;;;;
+2579;BOX DRAWINGS HEAVY UP;So;0;ON;;;;;N;FORMS HEAVY UP;;;;
+257A;BOX DRAWINGS HEAVY RIGHT;So;0;ON;;;;;N;FORMS HEAVY RIGHT;;;;
+257B;BOX DRAWINGS HEAVY DOWN;So;0;ON;;;;;N;FORMS HEAVY DOWN;;;;
+257C;BOX DRAWINGS LIGHT LEFT AND HEAVY RIGHT;So;0;ON;;;;;N;FORMS LIGHT LEFT AND HEAVY RIGHT;;;;
+257D;BOX DRAWINGS LIGHT UP AND HEAVY DOWN;So;0;ON;;;;;N;FORMS LIGHT UP AND HEAVY DOWN;;;;
+257E;BOX DRAWINGS HEAVY LEFT AND LIGHT RIGHT;So;0;ON;;;;;N;FORMS HEAVY LEFT AND LIGHT RIGHT;;;;
+257F;BOX DRAWINGS HEAVY UP AND LIGHT DOWN;So;0;ON;;;;;N;FORMS HEAVY UP AND LIGHT DOWN;;;;
+2580;UPPER HALF BLOCK;So;0;ON;;;;;N;;;;;
+2581;LOWER ONE EIGHTH BLOCK;So;0;ON;;;;;N;;;;;
+2582;LOWER ONE QUARTER BLOCK;So;0;ON;;;;;N;;;;;
+2583;LOWER THREE EIGHTHS BLOCK;So;0;ON;;;;;N;;;;;
+2584;LOWER HALF BLOCK;So;0;ON;;;;;N;;;;;
+2585;LOWER FIVE EIGHTHS BLOCK;So;0;ON;;;;;N;;;;;
+2586;LOWER THREE QUARTERS BLOCK;So;0;ON;;;;;N;LOWER THREE QUARTER BLOCK;;;;
+2587;LOWER SEVEN EIGHTHS BLOCK;So;0;ON;;;;;N;;;;;
+2588;FULL BLOCK;So;0;ON;;;;;N;;;;;
+2589;LEFT SEVEN EIGHTHS BLOCK;So;0;ON;;;;;N;;;;;
+258A;LEFT THREE QUARTERS BLOCK;So;0;ON;;;;;N;LEFT THREE QUARTER BLOCK;;;;
+258B;LEFT FIVE EIGHTHS BLOCK;So;0;ON;;;;;N;;;;;
+258C;LEFT HALF BLOCK;So;0;ON;;;;;N;;;;;
+258D;LEFT THREE EIGHTHS BLOCK;So;0;ON;;;;;N;;;;;
+258E;LEFT ONE QUARTER BLOCK;So;0;ON;;;;;N;;;;;
+258F;LEFT ONE EIGHTH BLOCK;So;0;ON;;;;;N;;;;;
+2590;RIGHT HALF BLOCK;So;0;ON;;;;;N;;;;;
+2591;LIGHT SHADE;So;0;ON;;;;;N;;;;;
+2592;MEDIUM SHADE;So;0;ON;;;;;N;;;;;
+2593;DARK SHADE;So;0;ON;;;;;N;;;;;
+2594;UPPER ONE EIGHTH BLOCK;So;0;ON;;;;;N;;;;;
+2595;RIGHT ONE EIGHTH BLOCK;So;0;ON;;;;;N;;;;;
+2596;QUADRANT LOWER LEFT;So;0;ON;;;;;N;;;;;
+2597;QUADRANT LOWER RIGHT;So;0;ON;;;;;N;;;;;
+2598;QUADRANT UPPER LEFT;So;0;ON;;;;;N;;;;;
+2599;QUADRANT UPPER LEFT AND LOWER LEFT AND LOWER RIGHT;So;0;ON;;;;;N;;;;;
+259A;QUADRANT UPPER LEFT AND LOWER RIGHT;So;0;ON;;;;;N;;;;;
+259B;QUADRANT UPPER LEFT AND UPPER RIGHT AND LOWER LEFT;So;0;ON;;;;;N;;;;;
+259C;QUADRANT UPPER LEFT AND UPPER RIGHT AND LOWER RIGHT;So;0;ON;;;;;N;;;;;
+259D;QUADRANT UPPER RIGHT;So;0;ON;;;;;N;;;;;
+259E;QUADRANT UPPER RIGHT AND LOWER LEFT;So;0;ON;;;;;N;;;;;
+259F;QUADRANT UPPER RIGHT AND LOWER LEFT AND LOWER RIGHT;So;0;ON;;;;;N;;;;;
+25A0;BLACK SQUARE;So;0;ON;;;;;N;;;;;
+25A1;WHITE SQUARE;So;0;ON;;;;;N;;;;;
+25A2;WHITE SQUARE WITH ROUNDED CORNERS;So;0;ON;;;;;N;;;;;
+25A3;WHITE SQUARE CONTAINING BLACK SMALL SQUARE;So;0;ON;;;;;N;;;;;
+25A4;SQUARE WITH HORIZONTAL FILL;So;0;ON;;;;;N;;;;;
+25A5;SQUARE WITH VERTICAL FILL;So;0;ON;;;;;N;;;;;
+25A6;SQUARE WITH ORTHOGONAL CROSSHATCH FILL;So;0;ON;;;;;N;;;;;
+25A7;SQUARE WITH UPPER LEFT TO LOWER RIGHT FILL;So;0;ON;;;;;N;;;;;
+25A8;SQUARE WITH UPPER RIGHT TO LOWER LEFT FILL;So;0;ON;;;;;N;;;;;
+25A9;SQUARE WITH DIAGONAL CROSSHATCH FILL;So;0;ON;;;;;N;;;;;
+25AA;BLACK SMALL SQUARE;So;0;ON;;;;;N;;;;;
+25AB;WHITE SMALL SQUARE;So;0;ON;;;;;N;;;;;
+25AC;BLACK RECTANGLE;So;0;ON;;;;;N;;;;;
+25AD;WHITE RECTANGLE;So;0;ON;;;;;N;;;;;
+25AE;BLACK VERTICAL RECTANGLE;So;0;ON;;;;;N;;;;;
+25AF;WHITE VERTICAL RECTANGLE;So;0;ON;;;;;N;;;;;
+25B0;BLACK PARALLELOGRAM;So;0;ON;;;;;N;;;;;
+25B1;WHITE PARALLELOGRAM;So;0;ON;;;;;N;;;;;
+25B2;BLACK UP-POINTING TRIANGLE;So;0;ON;;;;;N;BLACK UP POINTING TRIANGLE;;;;
+25B3;WHITE UP-POINTING TRIANGLE;So;0;ON;;;;;N;WHITE UP POINTING TRIANGLE;;;;
+25B4;BLACK UP-POINTING SMALL TRIANGLE;So;0;ON;;;;;N;BLACK UP POINTING SMALL TRIANGLE;;;;
+25B5;WHITE UP-POINTING SMALL TRIANGLE;So;0;ON;;;;;N;WHITE UP POINTING SMALL TRIANGLE;;;;
+25B6;BLACK RIGHT-POINTING TRIANGLE;So;0;ON;;;;;N;BLACK RIGHT POINTING TRIANGLE;;;;
+25B7;WHITE RIGHT-POINTING TRIANGLE;Sm;0;ON;;;;;N;WHITE RIGHT POINTING TRIANGLE;;;;
+25B8;BLACK RIGHT-POINTING SMALL TRIANGLE;So;0;ON;;;;;N;BLACK RIGHT POINTING SMALL TRIANGLE;;;;
+25B9;WHITE RIGHT-POINTING SMALL TRIANGLE;So;0;ON;;;;;N;WHITE RIGHT POINTING SMALL TRIANGLE;;;;
+25BA;BLACK RIGHT-POINTING POINTER;So;0;ON;;;;;N;BLACK RIGHT POINTING POINTER;;;;
+25BB;WHITE RIGHT-POINTING POINTER;So;0;ON;;;;;N;WHITE RIGHT POINTING POINTER;;;;
+25BC;BLACK DOWN-POINTING TRIANGLE;So;0;ON;;;;;N;BLACK DOWN POINTING TRIANGLE;;;;
+25BD;WHITE DOWN-POINTING TRIANGLE;So;0;ON;;;;;N;WHITE DOWN POINTING TRIANGLE;;;;
+25BE;BLACK DOWN-POINTING SMALL TRIANGLE;So;0;ON;;;;;N;BLACK DOWN POINTING SMALL TRIANGLE;;;;
+25BF;WHITE DOWN-POINTING SMALL TRIANGLE;So;0;ON;;;;;N;WHITE DOWN POINTING SMALL TRIANGLE;;;;
+25C0;BLACK LEFT-POINTING TRIANGLE;So;0;ON;;;;;N;BLACK LEFT POINTING TRIANGLE;;;;
+25C1;WHITE LEFT-POINTING TRIANGLE;Sm;0;ON;;;;;N;WHITE LEFT POINTING TRIANGLE;;;;
+25C2;BLACK LEFT-POINTING SMALL TRIANGLE;So;0;ON;;;;;N;BLACK LEFT POINTING SMALL TRIANGLE;;;;
+25C3;WHITE LEFT-POINTING SMALL TRIANGLE;So;0;ON;;;;;N;WHITE LEFT POINTING SMALL TRIANGLE;;;;
+25C4;BLACK LEFT-POINTING POINTER;So;0;ON;;;;;N;BLACK LEFT POINTING POINTER;;;;
+25C5;WHITE LEFT-POINTING POINTER;So;0;ON;;;;;N;WHITE LEFT POINTING POINTER;;;;
+25C6;BLACK DIAMOND;So;0;ON;;;;;N;;;;;
+25C7;WHITE DIAMOND;So;0;ON;;;;;N;;;;;
+25C8;WHITE DIAMOND CONTAINING BLACK SMALL DIAMOND;So;0;ON;;;;;N;;;;;
+25C9;FISHEYE;So;0;ON;;;;;N;;;;;
+25CA;LOZENGE;So;0;ON;;;;;N;;;;;
+25CB;WHITE CIRCLE;So;0;ON;;;;;N;;;;;
+25CC;DOTTED CIRCLE;So;0;ON;;;;;N;;;;;
+25CD;CIRCLE WITH VERTICAL FILL;So;0;ON;;;;;N;;;;;
+25CE;BULLSEYE;So;0;ON;;;;;N;;;;;
+25CF;BLACK CIRCLE;So;0;ON;;;;;N;;;;;
+25D0;CIRCLE WITH LEFT HALF BLACK;So;0;ON;;;;;N;;;;;
+25D1;CIRCLE WITH RIGHT HALF BLACK;So;0;ON;;;;;N;;;;;
+25D2;CIRCLE WITH LOWER HALF BLACK;So;0;ON;;;;;N;;;;;
+25D3;CIRCLE WITH UPPER HALF BLACK;So;0;ON;;;;;N;;;;;
+25D4;CIRCLE WITH UPPER RIGHT QUADRANT BLACK;So;0;ON;;;;;N;;;;;
+25D5;CIRCLE WITH ALL BUT UPPER LEFT QUADRANT BLACK;So;0;ON;;;;;N;;;;;
+25D6;LEFT HALF BLACK CIRCLE;So;0;ON;;;;;N;;;;;
+25D7;RIGHT HALF BLACK CIRCLE;So;0;ON;;;;;N;;;;;
+25D8;INVERSE BULLET;So;0;ON;;;;;N;;;;;
+25D9;INVERSE WHITE CIRCLE;So;0;ON;;;;;N;;;;;
+25DA;UPPER HALF INVERSE WHITE CIRCLE;So;0;ON;;;;;N;;;;;
+25DB;LOWER HALF INVERSE WHITE CIRCLE;So;0;ON;;;;;N;;;;;
+25DC;UPPER LEFT QUADRANT CIRCULAR ARC;So;0;ON;;;;;N;;;;;
+25DD;UPPER RIGHT QUADRANT CIRCULAR ARC;So;0;ON;;;;;N;;;;;
+25DE;LOWER RIGHT QUADRANT CIRCULAR ARC;So;0;ON;;;;;N;;;;;
+25DF;LOWER LEFT QUADRANT CIRCULAR ARC;So;0;ON;;;;;N;;;;;
+25E0;UPPER HALF CIRCLE;So;0;ON;;;;;N;;;;;
+25E1;LOWER HALF CIRCLE;So;0;ON;;;;;N;;;;;
+25E2;BLACK LOWER RIGHT TRIANGLE;So;0;ON;;;;;N;;;;;
+25E3;BLACK LOWER LEFT TRIANGLE;So;0;ON;;;;;N;;;;;
+25E4;BLACK UPPER LEFT TRIANGLE;So;0;ON;;;;;N;;;;;
+25E5;BLACK UPPER RIGHT TRIANGLE;So;0;ON;;;;;N;;;;;
+25E6;WHITE BULLET;So;0;ON;;;;;N;;;;;
+25E7;SQUARE WITH LEFT HALF BLACK;So;0;ON;;;;;N;;;;;
+25E8;SQUARE WITH RIGHT HALF BLACK;So;0;ON;;;;;N;;;;;
+25E9;SQUARE WITH UPPER LEFT DIAGONAL HALF BLACK;So;0;ON;;;;;N;;;;;
+25EA;SQUARE WITH LOWER RIGHT DIAGONAL HALF BLACK;So;0;ON;;;;;N;;;;;
+25EB;WHITE SQUARE WITH VERTICAL BISECTING LINE;So;0;ON;;;;;N;;;;;
+25EC;WHITE UP-POINTING TRIANGLE WITH DOT;So;0;ON;;;;;N;WHITE UP POINTING TRIANGLE WITH DOT;;;;
+25ED;UP-POINTING TRIANGLE WITH LEFT HALF BLACK;So;0;ON;;;;;N;UP POINTING TRIANGLE WITH LEFT HALF BLACK;;;;
+25EE;UP-POINTING TRIANGLE WITH RIGHT HALF BLACK;So;0;ON;;;;;N;UP POINTING TRIANGLE WITH RIGHT HALF BLACK;;;;
+25EF;LARGE CIRCLE;So;0;ON;;;;;N;;;;;
+25F0;WHITE SQUARE WITH UPPER LEFT QUADRANT;So;0;ON;;;;;N;;;;;
+25F1;WHITE SQUARE WITH LOWER LEFT QUADRANT;So;0;ON;;;;;N;;;;;
+25F2;WHITE SQUARE WITH LOWER RIGHT QUADRANT;So;0;ON;;;;;N;;;;;
+25F3;WHITE SQUARE WITH UPPER RIGHT QUADRANT;So;0;ON;;;;;N;;;;;
+25F4;WHITE CIRCLE WITH UPPER LEFT QUADRANT;So;0;ON;;;;;N;;;;;
+25F5;WHITE CIRCLE WITH LOWER LEFT QUADRANT;So;0;ON;;;;;N;;;;;
+25F6;WHITE CIRCLE WITH LOWER RIGHT QUADRANT;So;0;ON;;;;;N;;;;;
+25F7;WHITE CIRCLE WITH UPPER RIGHT QUADRANT;So;0;ON;;;;;N;;;;;
+25F8;UPPER LEFT TRIANGLE;Sm;0;ON;;;;;N;;;;;
+25F9;UPPER RIGHT TRIANGLE;Sm;0;ON;;;;;N;;;;;
+25FA;LOWER LEFT TRIANGLE;Sm;0;ON;;;;;N;;;;;
+25FB;WHITE MEDIUM SQUARE;Sm;0;ON;;;;;N;;;;;
+25FC;BLACK MEDIUM SQUARE;Sm;0;ON;;;;;N;;;;;
+25FD;WHITE MEDIUM SMALL SQUARE;Sm;0;ON;;;;;N;;;;;
+25FE;BLACK MEDIUM SMALL SQUARE;Sm;0;ON;;;;;N;;;;;
+25FF;LOWER RIGHT TRIANGLE;Sm;0;ON;;;;;N;;;;;
+2600;BLACK SUN WITH RAYS;So;0;ON;;;;;N;;;;;
+2601;CLOUD;So;0;ON;;;;;N;;;;;
+2602;UMBRELLA;So;0;ON;;;;;N;;;;;
+2603;SNOWMAN;So;0;ON;;;;;N;;;;;
+2604;COMET;So;0;ON;;;;;N;;;;;
+2605;BLACK STAR;So;0;ON;;;;;N;;;;;
+2606;WHITE STAR;So;0;ON;;;;;N;;;;;
+2607;LIGHTNING;So;0;ON;;;;;N;;;;;
+2608;THUNDERSTORM;So;0;ON;;;;;N;;;;;
+2609;SUN;So;0;ON;;;;;N;;;;;
+260A;ASCENDING NODE;So;0;ON;;;;;N;;;;;
+260B;DESCENDING NODE;So;0;ON;;;;;N;;;;;
+260C;CONJUNCTION;So;0;ON;;;;;N;;;;;
+260D;OPPOSITION;So;0;ON;;;;;N;;;;;
+260E;BLACK TELEPHONE;So;0;ON;;;;;N;;;;;
+260F;WHITE TELEPHONE;So;0;ON;;;;;N;;;;;
+2610;BALLOT BOX;So;0;ON;;;;;N;;;;;
+2611;BALLOT BOX WITH CHECK;So;0;ON;;;;;N;;;;;
+2612;BALLOT BOX WITH X;So;0;ON;;;;;N;;;;;
+2613;SALTIRE;So;0;ON;;;;;N;;;;;
+2614;UMBRELLA WITH RAIN DROPS;So;0;ON;;;;;N;;;;;
+2615;HOT BEVERAGE;So;0;ON;;;;;N;;;;;
+2616;WHITE SHOGI PIECE;So;0;ON;;;;;N;;;;;
+2617;BLACK SHOGI PIECE;So;0;ON;;;;;N;;;;;
+2618;SHAMROCK;So;0;ON;;;;;N;;;;;
+2619;REVERSED ROTATED FLORAL HEART BULLET;So;0;ON;;;;;N;;;;;
+261A;BLACK LEFT POINTING INDEX;So;0;ON;;;;;N;;;;;
+261B;BLACK RIGHT POINTING INDEX;So;0;ON;;;;;N;;;;;
+261C;WHITE LEFT POINTING INDEX;So;0;ON;;;;;N;;;;;
+261D;WHITE UP POINTING INDEX;So;0;ON;;;;;N;;;;;
+261E;WHITE RIGHT POINTING INDEX;So;0;ON;;;;;N;;;;;
+261F;WHITE DOWN POINTING INDEX;So;0;ON;;;;;N;;;;;
+2620;SKULL AND CROSSBONES;So;0;ON;;;;;N;;;;;
+2621;CAUTION SIGN;So;0;ON;;;;;N;;;;;
+2622;RADIOACTIVE SIGN;So;0;ON;;;;;N;;;;;
+2623;BIOHAZARD SIGN;So;0;ON;;;;;N;;;;;
+2624;CADUCEUS;So;0;ON;;;;;N;;;;;
+2625;ANKH;So;0;ON;;;;;N;;;;;
+2626;ORTHODOX CROSS;So;0;ON;;;;;N;;;;;
+2627;CHI RHO;So;0;ON;;;;;N;;;;;
+2628;CROSS OF LORRAINE;So;0;ON;;;;;N;;;;;
+2629;CROSS OF JERUSALEM;So;0;ON;;;;;N;;;;;
+262A;STAR AND CRESCENT;So;0;ON;;;;;N;;;;;
+262B;FARSI SYMBOL;So;0;ON;;;;;N;SYMBOL OF IRAN;;;;
+262C;ADI SHAKTI;So;0;ON;;;;;N;;;;;
+262D;HAMMER AND SICKLE;So;0;ON;;;;;N;;;;;
+262E;PEACE SYMBOL;So;0;ON;;;;;N;;;;;
+262F;YIN YANG;So;0;ON;;;;;N;;;;;
+2630;TRIGRAM FOR HEAVEN;So;0;ON;;;;;N;;;;;
+2631;TRIGRAM FOR LAKE;So;0;ON;;;;;N;;;;;
+2632;TRIGRAM FOR FIRE;So;0;ON;;;;;N;;;;;
+2633;TRIGRAM FOR THUNDER;So;0;ON;;;;;N;;;;;
+2634;TRIGRAM FOR WIND;So;0;ON;;;;;N;;;;;
+2635;TRIGRAM FOR WATER;So;0;ON;;;;;N;;;;;
+2636;TRIGRAM FOR MOUNTAIN;So;0;ON;;;;;N;;;;;
+2637;TRIGRAM FOR EARTH;So;0;ON;;;;;N;;;;;
+2638;WHEEL OF DHARMA;So;0;ON;;;;;N;;;;;
+2639;WHITE FROWNING FACE;So;0;ON;;;;;N;;;;;
+263A;WHITE SMILING FACE;So;0;ON;;;;;N;;;;;
+263B;BLACK SMILING FACE;So;0;ON;;;;;N;;;;;
+263C;WHITE SUN WITH RAYS;So;0;ON;;;;;N;;;;;
+263D;FIRST QUARTER MOON;So;0;ON;;;;;N;;;;;
+263E;LAST QUARTER MOON;So;0;ON;;;;;N;;;;;
+263F;MERCURY;So;0;ON;;;;;N;;;;;
+2640;FEMALE SIGN;So;0;ON;;;;;N;;;;;
+2641;EARTH;So;0;ON;;;;;N;;;;;
+2642;MALE SIGN;So;0;ON;;;;;N;;;;;
+2643;JUPITER;So;0;ON;;;;;N;;;;;
+2644;SATURN;So;0;ON;;;;;N;;;;;
+2645;URANUS;So;0;ON;;;;;N;;;;;
+2646;NEPTUNE;So;0;ON;;;;;N;;;;;
+2647;PLUTO;So;0;ON;;;;;N;;;;;
+2648;ARIES;So;0;ON;;;;;N;;;;;
+2649;TAURUS;So;0;ON;;;;;N;;;;;
+264A;GEMINI;So;0;ON;;;;;N;;;;;
+264B;CANCER;So;0;ON;;;;;N;;;;;
+264C;LEO;So;0;ON;;;;;N;;;;;
+264D;VIRGO;So;0;ON;;;;;N;;;;;
+264E;LIBRA;So;0;ON;;;;;N;;;;;
+264F;SCORPIUS;So;0;ON;;;;;N;;;;;
+2650;SAGITTARIUS;So;0;ON;;;;;N;;;;;
+2651;CAPRICORN;So;0;ON;;;;;N;;;;;
+2652;AQUARIUS;So;0;ON;;;;;N;;;;;
+2653;PISCES;So;0;ON;;;;;N;;;;;
+2654;WHITE CHESS KING;So;0;ON;;;;;N;;;;;
+2655;WHITE CHESS QUEEN;So;0;ON;;;;;N;;;;;
+2656;WHITE CHESS ROOK;So;0;ON;;;;;N;;;;;
+2657;WHITE CHESS BISHOP;So;0;ON;;;;;N;;;;;
+2658;WHITE CHESS KNIGHT;So;0;ON;;;;;N;;;;;
+2659;WHITE CHESS PAWN;So;0;ON;;;;;N;;;;;
+265A;BLACK CHESS KING;So;0;ON;;;;;N;;;;;
+265B;BLACK CHESS QUEEN;So;0;ON;;;;;N;;;;;
+265C;BLACK CHESS ROOK;So;0;ON;;;;;N;;;;;
+265D;BLACK CHESS BISHOP;So;0;ON;;;;;N;;;;;
+265E;BLACK CHESS KNIGHT;So;0;ON;;;;;N;;;;;
+265F;BLACK CHESS PAWN;So;0;ON;;;;;N;;;;;
+2660;BLACK SPADE SUIT;So;0;ON;;;;;N;;;;;
+2661;WHITE HEART SUIT;So;0;ON;;;;;N;;;;;
+2662;WHITE DIAMOND SUIT;So;0;ON;;;;;N;;;;;
+2663;BLACK CLUB SUIT;So;0;ON;;;;;N;;;;;
+2664;WHITE SPADE SUIT;So;0;ON;;;;;N;;;;;
+2665;BLACK HEART SUIT;So;0;ON;;;;;N;;;;;
+2666;BLACK DIAMOND SUIT;So;0;ON;;;;;N;;;;;
+2667;WHITE CLUB SUIT;So;0;ON;;;;;N;;;;;
+2668;HOT SPRINGS;So;0;ON;;;;;N;;;;;
+2669;QUARTER NOTE;So;0;ON;;;;;N;;;;;
+266A;EIGHTH NOTE;So;0;ON;;;;;N;;;;;
+266B;BEAMED EIGHTH NOTES;So;0;ON;;;;;N;BARRED EIGHTH NOTES;;;;
+266C;BEAMED SIXTEENTH NOTES;So;0;ON;;;;;N;BARRED SIXTEENTH NOTES;;;;
+266D;MUSIC FLAT SIGN;So;0;ON;;;;;N;FLAT;;;;
+266E;MUSIC NATURAL SIGN;So;0;ON;;;;;N;NATURAL;;;;
+266F;MUSIC SHARP SIGN;Sm;0;ON;;;;;N;SHARP;;;;
+2670;WEST SYRIAC CROSS;So;0;ON;;;;;N;;;;;
+2671;EAST SYRIAC CROSS;So;0;ON;;;;;N;;;;;
+2672;UNIVERSAL RECYCLING SYMBOL;So;0;ON;;;;;N;;;;;
+2673;RECYCLING SYMBOL FOR TYPE-1 PLASTICS;So;0;ON;;;;;N;;;;;
+2674;RECYCLING SYMBOL FOR TYPE-2 PLASTICS;So;0;ON;;;;;N;;;;;
+2675;RECYCLING SYMBOL FOR TYPE-3 PLASTICS;So;0;ON;;;;;N;;;;;
+2676;RECYCLING SYMBOL FOR TYPE-4 PLASTICS;So;0;ON;;;;;N;;;;;
+2677;RECYCLING SYMBOL FOR TYPE-5 PLASTICS;So;0;ON;;;;;N;;;;;
+2678;RECYCLING SYMBOL FOR TYPE-6 PLASTICS;So;0;ON;;;;;N;;;;;
+2679;RECYCLING SYMBOL FOR TYPE-7 PLASTICS;So;0;ON;;;;;N;;;;;
+267A;RECYCLING SYMBOL FOR GENERIC MATERIALS;So;0;ON;;;;;N;;;;;
+267B;BLACK UNIVERSAL RECYCLING SYMBOL;So;0;ON;;;;;N;;;;;
+267C;RECYCLED PAPER SYMBOL;So;0;ON;;;;;N;;;;;
+267D;PARTIALLY-RECYCLED PAPER SYMBOL;So;0;ON;;;;;N;;;;;
+267E;PERMANENT PAPER SIGN;So;0;ON;;;;;N;;;;;
+267F;WHEELCHAIR SYMBOL;So;0;ON;;;;;N;;;;;
+2680;DIE FACE-1;So;0;ON;;;;;N;;;;;
+2681;DIE FACE-2;So;0;ON;;;;;N;;;;;
+2682;DIE FACE-3;So;0;ON;;;;;N;;;;;
+2683;DIE FACE-4;So;0;ON;;;;;N;;;;;
+2684;DIE FACE-5;So;0;ON;;;;;N;;;;;
+2685;DIE FACE-6;So;0;ON;;;;;N;;;;;
+2686;WHITE CIRCLE WITH DOT RIGHT;So;0;ON;;;;;N;;;;;
+2687;WHITE CIRCLE WITH TWO DOTS;So;0;ON;;;;;N;;;;;
+2688;BLACK CIRCLE WITH WHITE DOT RIGHT;So;0;ON;;;;;N;;;;;
+2689;BLACK CIRCLE WITH TWO WHITE DOTS;So;0;ON;;;;;N;;;;;
+268A;MONOGRAM FOR YANG;So;0;ON;;;;;N;;;;;
+268B;MONOGRAM FOR YIN;So;0;ON;;;;;N;;;;;
+268C;DIGRAM FOR GREATER YANG;So;0;ON;;;;;N;;;;;
+268D;DIGRAM FOR LESSER YIN;So;0;ON;;;;;N;;;;;
+268E;DIGRAM FOR LESSER YANG;So;0;ON;;;;;N;;;;;
+268F;DIGRAM FOR GREATER YIN;So;0;ON;;;;;N;;;;;
+2690;WHITE FLAG;So;0;ON;;;;;N;;;;;
+2691;BLACK FLAG;So;0;ON;;;;;N;;;;;
+2692;HAMMER AND PICK;So;0;ON;;;;;N;;;;;
+2693;ANCHOR;So;0;ON;;;;;N;;;;;
+2694;CROSSED SWORDS;So;0;ON;;;;;N;;;;;
+2695;STAFF OF AESCULAPIUS;So;0;ON;;;;;N;;;;;
+2696;SCALES;So;0;ON;;;;;N;;;;;
+2697;ALEMBIC;So;0;ON;;;;;N;;;;;
+2698;FLOWER;So;0;ON;;;;;N;;;;;
+2699;GEAR;So;0;ON;;;;;N;;;;;
+269A;STAFF OF HERMES;So;0;ON;;;;;N;;;;;
+269B;ATOM SYMBOL;So;0;ON;;;;;N;;;;;
+269C;FLEUR-DE-LIS;So;0;ON;;;;;N;;;;;
+269D;OUTLINED WHITE STAR;So;0;ON;;;;;N;;;;;
+269E;THREE LINES CONVERGING RIGHT;So;0;ON;;;;;N;;;;;
+269F;THREE LINES CONVERGING LEFT;So;0;ON;;;;;N;;;;;
+26A0;WARNING SIGN;So;0;ON;;;;;N;;;;;
+26A1;HIGH VOLTAGE SIGN;So;0;ON;;;;;N;;;;;
+26A2;DOUBLED FEMALE SIGN;So;0;ON;;;;;N;;;;;
+26A3;DOUBLED MALE SIGN;So;0;ON;;;;;N;;;;;
+26A4;INTERLOCKED FEMALE AND MALE SIGN;So;0;ON;;;;;N;;;;;
+26A5;MALE AND FEMALE SIGN;So;0;ON;;;;;N;;;;;
+26A6;MALE WITH STROKE SIGN;So;0;ON;;;;;N;;;;;
+26A7;MALE WITH STROKE AND MALE AND FEMALE SIGN;So;0;ON;;;;;N;;;;;
+26A8;VERTICAL MALE WITH STROKE SIGN;So;0;ON;;;;;N;;;;;
+26A9;HORIZONTAL MALE WITH STROKE SIGN;So;0;ON;;;;;N;;;;;
+26AA;MEDIUM WHITE CIRCLE;So;0;ON;;;;;N;;;;;
+26AB;MEDIUM BLACK CIRCLE;So;0;ON;;;;;N;;;;;
+26AC;MEDIUM SMALL WHITE CIRCLE;So;0;L;;;;;N;;;;;
+26AD;MARRIAGE SYMBOL;So;0;ON;;;;;N;;;;;
+26AE;DIVORCE SYMBOL;So;0;ON;;;;;N;;;;;
+26AF;UNMARRIED PARTNERSHIP SYMBOL;So;0;ON;;;;;N;;;;;
+26B0;COFFIN;So;0;ON;;;;;N;;;;;
+26B1;FUNERAL URN;So;0;ON;;;;;N;;;;;
+26B2;NEUTER;So;0;ON;;;;;N;;;;;
+26B3;CERES;So;0;ON;;;;;N;;;;;
+26B4;PALLAS;So;0;ON;;;;;N;;;;;
+26B5;JUNO;So;0;ON;;;;;N;;;;;
+26B6;VESTA;So;0;ON;;;;;N;;;;;
+26B7;CHIRON;So;0;ON;;;;;N;;;;;
+26B8;BLACK MOON LILITH;So;0;ON;;;;;N;;;;;
+26B9;SEXTILE;So;0;ON;;;;;N;;;;;
+26BA;SEMISEXTILE;So;0;ON;;;;;N;;;;;
+26BB;QUINCUNX;So;0;ON;;;;;N;;;;;
+26BC;SESQUIQUADRATE;So;0;ON;;;;;N;;;;;
+26BD;SOCCER BALL;So;0;ON;;;;;N;;;;;
+26BE;BASEBALL;So;0;ON;;;;;N;;;;;
+26BF;SQUARED KEY;So;0;ON;;;;;N;;;;;
+26C0;WHITE DRAUGHTS MAN;So;0;ON;;;;;N;;;;;
+26C1;WHITE DRAUGHTS KING;So;0;ON;;;;;N;;;;;
+26C2;BLACK DRAUGHTS MAN;So;0;ON;;;;;N;;;;;
+26C3;BLACK DRAUGHTS KING;So;0;ON;;;;;N;;;;;
+26C4;SNOWMAN WITHOUT SNOW;So;0;ON;;;;;N;;;;;
+26C5;SUN BEHIND CLOUD;So;0;ON;;;;;N;;;;;
+26C6;RAIN;So;0;ON;;;;;N;;;;;
+26C7;BLACK SNOWMAN;So;0;ON;;;;;N;;;;;
+26C8;THUNDER CLOUD AND RAIN;So;0;ON;;;;;N;;;;;
+26C9;TURNED WHITE SHOGI PIECE;So;0;ON;;;;;N;;;;;
+26CA;TURNED BLACK SHOGI PIECE;So;0;ON;;;;;N;;;;;
+26CB;WHITE DIAMOND IN SQUARE;So;0;ON;;;;;N;;;;;
+26CC;CROSSING LANES;So;0;ON;;;;;N;;;;;
+26CD;DISABLED CAR;So;0;ON;;;;;N;;;;;
+26CE;OPHIUCHUS;So;0;ON;;;;;N;;;;;
+26CF;PICK;So;0;ON;;;;;N;;;;;
+26D0;CAR SLIDING;So;0;ON;;;;;N;;;;;
+26D1;HELMET WITH WHITE CROSS;So;0;ON;;;;;N;;;;;
+26D2;CIRCLED CROSSING LANES;So;0;ON;;;;;N;;;;;
+26D3;CHAINS;So;0;ON;;;;;N;;;;;
+26D4;NO ENTRY;So;0;ON;;;;;N;;;;;
+26D5;ALTERNATE ONE-WAY LEFT WAY TRAFFIC;So;0;ON;;;;;N;;;;;
+26D6;BLACK TWO-WAY LEFT WAY TRAFFIC;So;0;ON;;;;;N;;;;;
+26D7;WHITE TWO-WAY LEFT WAY TRAFFIC;So;0;ON;;;;;N;;;;;
+26D8;BLACK LEFT LANE MERGE;So;0;ON;;;;;N;;;;;
+26D9;WHITE LEFT LANE MERGE;So;0;ON;;;;;N;;;;;
+26DA;DRIVE SLOW SIGN;So;0;ON;;;;;N;;;;;
+26DB;HEAVY WHITE DOWN-POINTING TRIANGLE;So;0;ON;;;;;N;;;;;
+26DC;LEFT CLOSED ENTRY;So;0;ON;;;;;N;;;;;
+26DD;SQUARED SALTIRE;So;0;ON;;;;;N;;;;;
+26DE;FALLING DIAGONAL IN WHITE CIRCLE IN BLACK SQUARE;So;0;ON;;;;;N;;;;;
+26DF;BLACK TRUCK;So;0;ON;;;;;N;;;;;
+26E0;RESTRICTED LEFT ENTRY-1;So;0;ON;;;;;N;;;;;
+26E1;RESTRICTED LEFT ENTRY-2;So;0;ON;;;;;N;;;;;
+26E2;ASTRONOMICAL SYMBOL FOR URANUS;So;0;ON;;;;;N;;;;;
+26E3;HEAVY CIRCLE WITH STROKE AND TWO DOTS ABOVE;So;0;ON;;;;;N;;;;;
+26E4;PENTAGRAM;So;0;ON;;;;;N;;;;;
+26E5;RIGHT-HANDED INTERLACED PENTAGRAM;So;0;ON;;;;;N;;;;;
+26E6;LEFT-HANDED INTERLACED PENTAGRAM;So;0;ON;;;;;N;;;;;
+26E7;INVERTED PENTAGRAM;So;0;ON;;;;;N;;;;;
+26E8;BLACK CROSS ON SHIELD;So;0;ON;;;;;N;;;;;
+26E9;SHINTO SHRINE;So;0;ON;;;;;N;;;;;
+26EA;CHURCH;So;0;ON;;;;;N;;;;;
+26EB;CASTLE;So;0;ON;;;;;N;;;;;
+26EC;HISTORIC SITE;So;0;ON;;;;;N;;;;;
+26ED;GEAR WITHOUT HUB;So;0;ON;;;;;N;;;;;
+26EE;GEAR WITH HANDLES;So;0;ON;;;;;N;;;;;
+26EF;MAP SYMBOL FOR LIGHTHOUSE;So;0;ON;;;;;N;;;;;
+26F0;MOUNTAIN;So;0;ON;;;;;N;;;;;
+26F1;UMBRELLA ON GROUND;So;0;ON;;;;;N;;;;;
+26F2;FOUNTAIN;So;0;ON;;;;;N;;;;;
+26F3;FLAG IN HOLE;So;0;ON;;;;;N;;;;;
+26F4;FERRY;So;0;ON;;;;;N;;;;;
+26F5;SAILBOAT;So;0;ON;;;;;N;;;;;
+26F6;SQUARE FOUR CORNERS;So;0;ON;;;;;N;;;;;
+26F7;SKIER;So;0;ON;;;;;N;;;;;
+26F8;ICE SKATE;So;0;ON;;;;;N;;;;;
+26F9;PERSON WITH BALL;So;0;ON;;;;;N;;;;;
+26FA;TENT;So;0;ON;;;;;N;;;;;
+26FB;JAPANESE BANK SYMBOL;So;0;ON;;;;;N;;;;;
+26FC;HEADSTONE GRAVEYARD SYMBOL;So;0;ON;;;;;N;;;;;
+26FD;FUEL PUMP;So;0;ON;;;;;N;;;;;
+26FE;CUP ON BLACK SQUARE;So;0;ON;;;;;N;;;;;
+26FF;WHITE FLAG WITH HORIZONTAL MIDDLE BLACK STRIPE;So;0;ON;;;;;N;;;;;
+2700;BLACK SAFETY SCISSORS;So;0;ON;;;;;N;;;;;
+2701;UPPER BLADE SCISSORS;So;0;ON;;;;;N;;;;;
+2702;BLACK SCISSORS;So;0;ON;;;;;N;;;;;
+2703;LOWER BLADE SCISSORS;So;0;ON;;;;;N;;;;;
+2704;WHITE SCISSORS;So;0;ON;;;;;N;;;;;
+2705;WHITE HEAVY CHECK MARK;So;0;ON;;;;;N;;;;;
+2706;TELEPHONE LOCATION SIGN;So;0;ON;;;;;N;;;;;
+2707;TAPE DRIVE;So;0;ON;;;;;N;;;;;
+2708;AIRPLANE;So;0;ON;;;;;N;;;;;
+2709;ENVELOPE;So;0;ON;;;;;N;;;;;
+270A;RAISED FIST;So;0;ON;;;;;N;;;;;
+270B;RAISED HAND;So;0;ON;;;;;N;;;;;
+270C;VICTORY HAND;So;0;ON;;;;;N;;;;;
+270D;WRITING HAND;So;0;ON;;;;;N;;;;;
+270E;LOWER RIGHT PENCIL;So;0;ON;;;;;N;;;;;
+270F;PENCIL;So;0;ON;;;;;N;;;;;
+2710;UPPER RIGHT PENCIL;So;0;ON;;;;;N;;;;;
+2711;WHITE NIB;So;0;ON;;;;;N;;;;;
+2712;BLACK NIB;So;0;ON;;;;;N;;;;;
+2713;CHECK MARK;So;0;ON;;;;;N;;;;;
+2714;HEAVY CHECK MARK;So;0;ON;;;;;N;;;;;
+2715;MULTIPLICATION X;So;0;ON;;;;;N;;;;;
+2716;HEAVY MULTIPLICATION X;So;0;ON;;;;;N;;;;;
+2717;BALLOT X;So;0;ON;;;;;N;;;;;
+2718;HEAVY BALLOT X;So;0;ON;;;;;N;;;;;
+2719;OUTLINED GREEK CROSS;So;0;ON;;;;;N;;;;;
+271A;HEAVY GREEK CROSS;So;0;ON;;;;;N;;;;;
+271B;OPEN CENTRE CROSS;So;0;ON;;;;;N;OPEN CENTER CROSS;;;;
+271C;HEAVY OPEN CENTRE CROSS;So;0;ON;;;;;N;HEAVY OPEN CENTER CROSS;;;;
+271D;LATIN CROSS;So;0;ON;;;;;N;;;;;
+271E;SHADOWED WHITE LATIN CROSS;So;0;ON;;;;;N;;;;;
+271F;OUTLINED LATIN CROSS;So;0;ON;;;;;N;;;;;
+2720;MALTESE CROSS;So;0;ON;;;;;N;;;;;
+2721;STAR OF DAVID;So;0;ON;;;;;N;;;;;
+2722;FOUR TEARDROP-SPOKED ASTERISK;So;0;ON;;;;;N;;;;;
+2723;FOUR BALLOON-SPOKED ASTERISK;So;0;ON;;;;;N;;;;;
+2724;HEAVY FOUR BALLOON-SPOKED ASTERISK;So;0;ON;;;;;N;;;;;
+2725;FOUR CLUB-SPOKED ASTERISK;So;0;ON;;;;;N;;;;;
+2726;BLACK FOUR POINTED STAR;So;0;ON;;;;;N;;;;;
+2727;WHITE FOUR POINTED STAR;So;0;ON;;;;;N;;;;;
+2728;SPARKLES;So;0;ON;;;;;N;;;;;
+2729;STRESS OUTLINED WHITE STAR;So;0;ON;;;;;N;;;;;
+272A;CIRCLED WHITE STAR;So;0;ON;;;;;N;;;;;
+272B;OPEN CENTRE BLACK STAR;So;0;ON;;;;;N;OPEN CENTER BLACK STAR;;;;
+272C;BLACK CENTRE WHITE STAR;So;0;ON;;;;;N;BLACK CENTER WHITE STAR;;;;
+272D;OUTLINED BLACK STAR;So;0;ON;;;;;N;;;;;
+272E;HEAVY OUTLINED BLACK STAR;So;0;ON;;;;;N;;;;;
+272F;PINWHEEL STAR;So;0;ON;;;;;N;;;;;
+2730;SHADOWED WHITE STAR;So;0;ON;;;;;N;;;;;
+2731;HEAVY ASTERISK;So;0;ON;;;;;N;;;;;
+2732;OPEN CENTRE ASTERISK;So;0;ON;;;;;N;OPEN CENTER ASTERISK;;;;
+2733;EIGHT SPOKED ASTERISK;So;0;ON;;;;;N;;;;;
+2734;EIGHT POINTED BLACK STAR;So;0;ON;;;;;N;;;;;
+2735;EIGHT POINTED PINWHEEL STAR;So;0;ON;;;;;N;;;;;
+2736;SIX POINTED BLACK STAR;So;0;ON;;;;;N;;;;;
+2737;EIGHT POINTED RECTILINEAR BLACK STAR;So;0;ON;;;;;N;;;;;
+2738;HEAVY EIGHT POINTED RECTILINEAR BLACK STAR;So;0;ON;;;;;N;;;;;
+2739;TWELVE POINTED BLACK STAR;So;0;ON;;;;;N;;;;;
+273A;SIXTEEN POINTED ASTERISK;So;0;ON;;;;;N;;;;;
+273B;TEARDROP-SPOKED ASTERISK;So;0;ON;;;;;N;;;;;
+273C;OPEN CENTRE TEARDROP-SPOKED ASTERISK;So;0;ON;;;;;N;OPEN CENTER TEARDROP-SPOKED ASTERISK;;;;
+273D;HEAVY TEARDROP-SPOKED ASTERISK;So;0;ON;;;;;N;;;;;
+273E;SIX PETALLED BLACK AND WHITE FLORETTE;So;0;ON;;;;;N;;;;;
+273F;BLACK FLORETTE;So;0;ON;;;;;N;;;;;
+2740;WHITE FLORETTE;So;0;ON;;;;;N;;;;;
+2741;EIGHT PETALLED OUTLINED BLACK FLORETTE;So;0;ON;;;;;N;;;;;
+2742;CIRCLED OPEN CENTRE EIGHT POINTED STAR;So;0;ON;;;;;N;CIRCLED OPEN CENTER EIGHT POINTED STAR;;;;
+2743;HEAVY TEARDROP-SPOKED PINWHEEL ASTERISK;So;0;ON;;;;;N;;;;;
+2744;SNOWFLAKE;So;0;ON;;;;;N;;;;;
+2745;TIGHT TRIFOLIATE SNOWFLAKE;So;0;ON;;;;;N;;;;;
+2746;HEAVY CHEVRON SNOWFLAKE;So;0;ON;;;;;N;;;;;
+2747;SPARKLE;So;0;ON;;;;;N;;;;;
+2748;HEAVY SPARKLE;So;0;ON;;;;;N;;;;;
+2749;BALLOON-SPOKED ASTERISK;So;0;ON;;;;;N;;;;;
+274A;EIGHT TEARDROP-SPOKED PROPELLER ASTERISK;So;0;ON;;;;;N;;;;;
+274B;HEAVY EIGHT TEARDROP-SPOKED PROPELLER ASTERISK;So;0;ON;;;;;N;;;;;
+274C;CROSS MARK;So;0;ON;;;;;N;;;;;
+274D;SHADOWED WHITE CIRCLE;So;0;ON;;;;;N;;;;;
+274E;NEGATIVE SQUARED CROSS MARK;So;0;ON;;;;;N;;;;;
+274F;LOWER RIGHT DROP-SHADOWED WHITE SQUARE;So;0;ON;;;;;N;;;;;
+2750;UPPER RIGHT DROP-SHADOWED WHITE SQUARE;So;0;ON;;;;;N;;;;;
+2751;LOWER RIGHT SHADOWED WHITE SQUARE;So;0;ON;;;;;N;;;;;
+2752;UPPER RIGHT SHADOWED WHITE SQUARE;So;0;ON;;;;;N;;;;;
+2753;BLACK QUESTION MARK ORNAMENT;So;0;ON;;;;;N;;;;;
+2754;WHITE QUESTION MARK ORNAMENT;So;0;ON;;;;;N;;;;;
+2755;WHITE EXCLAMATION MARK ORNAMENT;So;0;ON;;;;;N;;;;;
+2756;BLACK DIAMOND MINUS WHITE X;So;0;ON;;;;;N;;;;;
+2757;HEAVY EXCLAMATION MARK SYMBOL;So;0;ON;;;;;N;;;;;
+2758;LIGHT VERTICAL BAR;So;0;ON;;;;;N;;;;;
+2759;MEDIUM VERTICAL BAR;So;0;ON;;;;;N;;;;;
+275A;HEAVY VERTICAL BAR;So;0;ON;;;;;N;;;;;
+275B;HEAVY SINGLE TURNED COMMA QUOTATION MARK ORNAMENT;So;0;ON;;;;;N;;;;;
+275C;HEAVY SINGLE COMMA QUOTATION MARK ORNAMENT;So;0;ON;;;;;N;;;;;
+275D;HEAVY DOUBLE TURNED COMMA QUOTATION MARK ORNAMENT;So;0;ON;;;;;N;;;;;
+275E;HEAVY DOUBLE COMMA QUOTATION MARK ORNAMENT;So;0;ON;;;;;N;;;;;
+275F;HEAVY LOW SINGLE COMMA QUOTATION MARK ORNAMENT;So;0;ON;;;;;N;;;;;
+2760;HEAVY LOW DOUBLE COMMA QUOTATION MARK ORNAMENT;So;0;ON;;;;;N;;;;;
+2761;CURVED STEM PARAGRAPH SIGN ORNAMENT;So;0;ON;;;;;N;;;;;
+2762;HEAVY EXCLAMATION MARK ORNAMENT;So;0;ON;;;;;N;;;;;
+2763;HEAVY HEART EXCLAMATION MARK ORNAMENT;So;0;ON;;;;;N;;;;;
+2764;HEAVY BLACK HEART;So;0;ON;;;;;N;;;;;
+2765;ROTATED HEAVY BLACK HEART BULLET;So;0;ON;;;;;N;;;;;
+2766;FLORAL HEART;So;0;ON;;;;;N;;;;;
+2767;ROTATED FLORAL HEART BULLET;So;0;ON;;;;;N;;;;;
+2768;MEDIUM LEFT PARENTHESIS ORNAMENT;Ps;0;ON;;;;;Y;;;;;
+2769;MEDIUM RIGHT PARENTHESIS ORNAMENT;Pe;0;ON;;;;;Y;;;;;
+276A;MEDIUM FLATTENED LEFT PARENTHESIS ORNAMENT;Ps;0;ON;;;;;Y;;;;;
+276B;MEDIUM FLATTENED RIGHT PARENTHESIS ORNAMENT;Pe;0;ON;;;;;Y;;;;;
+276C;MEDIUM LEFT-POINTING ANGLE BRACKET ORNAMENT;Ps;0;ON;;;;;Y;;;;;
+276D;MEDIUM RIGHT-POINTING ANGLE BRACKET ORNAMENT;Pe;0;ON;;;;;Y;;;;;
+276E;HEAVY LEFT-POINTING ANGLE QUOTATION MARK ORNAMENT;Ps;0;ON;;;;;Y;;;;;
+276F;HEAVY RIGHT-POINTING ANGLE QUOTATION MARK ORNAMENT;Pe;0;ON;;;;;Y;;;;;
+2770;HEAVY LEFT-POINTING ANGLE BRACKET ORNAMENT;Ps;0;ON;;;;;Y;;;;;
+2771;HEAVY RIGHT-POINTING ANGLE BRACKET ORNAMENT;Pe;0;ON;;;;;Y;;;;;
+2772;LIGHT LEFT TORTOISE SHELL BRACKET ORNAMENT;Ps;0;ON;;;;;Y;;;;;
+2773;LIGHT RIGHT TORTOISE SHELL BRACKET ORNAMENT;Pe;0;ON;;;;;Y;;;;;
+2774;MEDIUM LEFT CURLY BRACKET ORNAMENT;Ps;0;ON;;;;;Y;;;;;
+2775;MEDIUM RIGHT CURLY BRACKET ORNAMENT;Pe;0;ON;;;;;Y;;;;;
+2776;DINGBAT NEGATIVE CIRCLED DIGIT ONE;No;0;ON;;;1;1;N;INVERSE CIRCLED DIGIT ONE;;;;
+2777;DINGBAT NEGATIVE CIRCLED DIGIT TWO;No;0;ON;;;2;2;N;INVERSE CIRCLED DIGIT TWO;;;;
+2778;DINGBAT NEGATIVE CIRCLED DIGIT THREE;No;0;ON;;;3;3;N;INVERSE CIRCLED DIGIT THREE;;;;
+2779;DINGBAT NEGATIVE CIRCLED DIGIT FOUR;No;0;ON;;;4;4;N;INVERSE CIRCLED DIGIT FOUR;;;;
+277A;DINGBAT NEGATIVE CIRCLED DIGIT FIVE;No;0;ON;;;5;5;N;INVERSE CIRCLED DIGIT FIVE;;;;
+277B;DINGBAT NEGATIVE CIRCLED DIGIT SIX;No;0;ON;;;6;6;N;INVERSE CIRCLED DIGIT SIX;;;;
+277C;DINGBAT NEGATIVE CIRCLED DIGIT SEVEN;No;0;ON;;;7;7;N;INVERSE CIRCLED DIGIT SEVEN;;;;
+277D;DINGBAT NEGATIVE CIRCLED DIGIT EIGHT;No;0;ON;;;8;8;N;INVERSE CIRCLED DIGIT EIGHT;;;;
+277E;DINGBAT NEGATIVE CIRCLED DIGIT NINE;No;0;ON;;;9;9;N;INVERSE CIRCLED DIGIT NINE;;;;
+277F;DINGBAT NEGATIVE CIRCLED NUMBER TEN;No;0;ON;;;;10;N;INVERSE CIRCLED NUMBER TEN;;;;
+2780;DINGBAT CIRCLED SANS-SERIF DIGIT ONE;No;0;ON;;;1;1;N;CIRCLED SANS-SERIF DIGIT ONE;;;;
+2781;DINGBAT CIRCLED SANS-SERIF DIGIT TWO;No;0;ON;;;2;2;N;CIRCLED SANS-SERIF DIGIT TWO;;;;
+2782;DINGBAT CIRCLED SANS-SERIF DIGIT THREE;No;0;ON;;;3;3;N;CIRCLED SANS-SERIF DIGIT THREE;;;;
+2783;DINGBAT CIRCLED SANS-SERIF DIGIT FOUR;No;0;ON;;;4;4;N;CIRCLED SANS-SERIF DIGIT FOUR;;;;
+2784;DINGBAT CIRCLED SANS-SERIF DIGIT FIVE;No;0;ON;;;5;5;N;CIRCLED SANS-SERIF DIGIT FIVE;;;;
+2785;DINGBAT CIRCLED SANS-SERIF DIGIT SIX;No;0;ON;;;6;6;N;CIRCLED SANS-SERIF DIGIT SIX;;;;
+2786;DINGBAT CIRCLED SANS-SERIF DIGIT SEVEN;No;0;ON;;;7;7;N;CIRCLED SANS-SERIF DIGIT SEVEN;;;;
+2787;DINGBAT CIRCLED SANS-SERIF DIGIT EIGHT;No;0;ON;;;8;8;N;CIRCLED SANS-SERIF DIGIT EIGHT;;;;
+2788;DINGBAT CIRCLED SANS-SERIF DIGIT NINE;No;0;ON;;;9;9;N;CIRCLED SANS-SERIF DIGIT NINE;;;;
+2789;DINGBAT CIRCLED SANS-SERIF NUMBER TEN;No;0;ON;;;;10;N;CIRCLED SANS-SERIF NUMBER TEN;;;;
+278A;DINGBAT NEGATIVE CIRCLED SANS-SERIF DIGIT ONE;No;0;ON;;;1;1;N;INVERSE CIRCLED SANS-SERIF DIGIT ONE;;;;
+278B;DINGBAT NEGATIVE CIRCLED SANS-SERIF DIGIT TWO;No;0;ON;;;2;2;N;INVERSE CIRCLED SANS-SERIF DIGIT TWO;;;;
+278C;DINGBAT NEGATIVE CIRCLED SANS-SERIF DIGIT THREE;No;0;ON;;;3;3;N;INVERSE CIRCLED SANS-SERIF DIGIT THREE;;;;
+278D;DINGBAT NEGATIVE CIRCLED SANS-SERIF DIGIT FOUR;No;0;ON;;;4;4;N;INVERSE CIRCLED SANS-SERIF DIGIT FOUR;;;;
+278E;DINGBAT NEGATIVE CIRCLED SANS-SERIF DIGIT FIVE;No;0;ON;;;5;5;N;INVERSE CIRCLED SANS-SERIF DIGIT FIVE;;;;
+278F;DINGBAT NEGATIVE CIRCLED SANS-SERIF DIGIT SIX;No;0;ON;;;6;6;N;INVERSE CIRCLED SANS-SERIF DIGIT SIX;;;;
+2790;DINGBAT NEGATIVE CIRCLED SANS-SERIF DIGIT SEVEN;No;0;ON;;;7;7;N;INVERSE CIRCLED SANS-SERIF DIGIT SEVEN;;;;
+2791;DINGBAT NEGATIVE CIRCLED SANS-SERIF DIGIT EIGHT;No;0;ON;;;8;8;N;INVERSE CIRCLED SANS-SERIF DIGIT EIGHT;;;;
+2792;DINGBAT NEGATIVE CIRCLED SANS-SERIF DIGIT NINE;No;0;ON;;;9;9;N;INVERSE CIRCLED SANS-SERIF DIGIT NINE;;;;
+2793;DINGBAT NEGATIVE CIRCLED SANS-SERIF NUMBER TEN;No;0;ON;;;;10;N;INVERSE CIRCLED SANS-SERIF NUMBER TEN;;;;
+2794;HEAVY WIDE-HEADED RIGHTWARDS ARROW;So;0;ON;;;;;N;HEAVY WIDE-HEADED RIGHT ARROW;;;;
+2795;HEAVY PLUS SIGN;So;0;ON;;;;;N;;;;;
+2796;HEAVY MINUS SIGN;So;0;ON;;;;;N;;;;;
+2797;HEAVY DIVISION SIGN;So;0;ON;;;;;N;;;;;
+2798;HEAVY SOUTH EAST ARROW;So;0;ON;;;;;N;HEAVY LOWER RIGHT ARROW;;;;
+2799;HEAVY RIGHTWARDS ARROW;So;0;ON;;;;;N;HEAVY RIGHT ARROW;;;;
+279A;HEAVY NORTH EAST ARROW;So;0;ON;;;;;N;HEAVY UPPER RIGHT ARROW;;;;
+279B;DRAFTING POINT RIGHTWARDS ARROW;So;0;ON;;;;;N;DRAFTING POINT RIGHT ARROW;;;;
+279C;HEAVY ROUND-TIPPED RIGHTWARDS ARROW;So;0;ON;;;;;N;HEAVY ROUND-TIPPED RIGHT ARROW;;;;
+279D;TRIANGLE-HEADED RIGHTWARDS ARROW;So;0;ON;;;;;N;TRIANGLE-HEADED RIGHT ARROW;;;;
+279E;HEAVY TRIANGLE-HEADED RIGHTWARDS ARROW;So;0;ON;;;;;N;HEAVY TRIANGLE-HEADED RIGHT ARROW;;;;
+279F;DASHED TRIANGLE-HEADED RIGHTWARDS ARROW;So;0;ON;;;;;N;DASHED TRIANGLE-HEADED RIGHT ARROW;;;;
+27A0;HEAVY DASHED TRIANGLE-HEADED RIGHTWARDS ARROW;So;0;ON;;;;;N;HEAVY DASHED TRIANGLE-HEADED RIGHT ARROW;;;;
+27A1;BLACK RIGHTWARDS ARROW;So;0;ON;;;;;N;BLACK RIGHT ARROW;;;;
+27A2;THREE-D TOP-LIGHTED RIGHTWARDS ARROWHEAD;So;0;ON;;;;;N;THREE-D TOP-LIGHTED RIGHT ARROWHEAD;;;;
+27A3;THREE-D BOTTOM-LIGHTED RIGHTWARDS ARROWHEAD;So;0;ON;;;;;N;THREE-D BOTTOM-LIGHTED RIGHT ARROWHEAD;;;;
+27A4;BLACK RIGHTWARDS ARROWHEAD;So;0;ON;;;;;N;BLACK RIGHT ARROWHEAD;;;;
+27A5;HEAVY BLACK CURVED DOWNWARDS AND RIGHTWARDS ARROW;So;0;ON;;;;;N;HEAVY BLACK CURVED DOWN AND RIGHT ARROW;;;;
+27A6;HEAVY BLACK CURVED UPWARDS AND RIGHTWARDS ARROW;So;0;ON;;;;;N;HEAVY BLACK CURVED UP AND RIGHT ARROW;;;;
+27A7;SQUAT BLACK RIGHTWARDS ARROW;So;0;ON;;;;;N;SQUAT BLACK RIGHT ARROW;;;;
+27A8;HEAVY CONCAVE-POINTED BLACK RIGHTWARDS ARROW;So;0;ON;;;;;N;HEAVY CONCAVE-POINTED BLACK RIGHT ARROW;;;;
+27A9;RIGHT-SHADED WHITE RIGHTWARDS ARROW;So;0;ON;;;;;N;RIGHT-SHADED WHITE RIGHT ARROW;;;;
+27AA;LEFT-SHADED WHITE RIGHTWARDS ARROW;So;0;ON;;;;;N;LEFT-SHADED WHITE RIGHT ARROW;;;;
+27AB;BACK-TILTED SHADOWED WHITE RIGHTWARDS ARROW;So;0;ON;;;;;N;BACK-TILTED SHADOWED WHITE RIGHT ARROW;;;;
+27AC;FRONT-TILTED SHADOWED WHITE RIGHTWARDS ARROW;So;0;ON;;;;;N;FRONT-TILTED SHADOWED WHITE RIGHT ARROW;;;;
+27AD;HEAVY LOWER RIGHT-SHADOWED WHITE RIGHTWARDS ARROW;So;0;ON;;;;;N;HEAVY LOWER RIGHT-SHADOWED WHITE RIGHT ARROW;;;;
+27AE;HEAVY UPPER RIGHT-SHADOWED WHITE RIGHTWARDS ARROW;So;0;ON;;;;;N;HEAVY UPPER RIGHT-SHADOWED WHITE RIGHT ARROW;;;;
+27AF;NOTCHED LOWER RIGHT-SHADOWED WHITE RIGHTWARDS ARROW;So;0;ON;;;;;N;NOTCHED LOWER RIGHT-SHADOWED WHITE RIGHT ARROW;;;;
+27B0;CURLY LOOP;So;0;ON;;;;;N;;;;;
+27B1;NOTCHED UPPER RIGHT-SHADOWED WHITE RIGHTWARDS ARROW;So;0;ON;;;;;N;NOTCHED UPPER RIGHT-SHADOWED WHITE RIGHT ARROW;;;;
+27B2;CIRCLED HEAVY WHITE RIGHTWARDS ARROW;So;0;ON;;;;;N;CIRCLED HEAVY WHITE RIGHT ARROW;;;;
+27B3;WHITE-FEATHERED RIGHTWARDS ARROW;So;0;ON;;;;;N;WHITE-FEATHERED RIGHT ARROW;;;;
+27B4;BLACK-FEATHERED SOUTH EAST ARROW;So;0;ON;;;;;N;BLACK-FEATHERED LOWER RIGHT ARROW;;;;
+27B5;BLACK-FEATHERED RIGHTWARDS ARROW;So;0;ON;;;;;N;BLACK-FEATHERED RIGHT ARROW;;;;
+27B6;BLACK-FEATHERED NORTH EAST ARROW;So;0;ON;;;;;N;BLACK-FEATHERED UPPER RIGHT ARROW;;;;
+27B7;HEAVY BLACK-FEATHERED SOUTH EAST ARROW;So;0;ON;;;;;N;HEAVY BLACK-FEATHERED LOWER RIGHT ARROW;;;;
+27B8;HEAVY BLACK-FEATHERED RIGHTWARDS ARROW;So;0;ON;;;;;N;HEAVY BLACK-FEATHERED RIGHT ARROW;;;;
+27B9;HEAVY BLACK-FEATHERED NORTH EAST ARROW;So;0;ON;;;;;N;HEAVY BLACK-FEATHERED UPPER RIGHT ARROW;;;;
+27BA;TEARDROP-BARBED RIGHTWARDS ARROW;So;0;ON;;;;;N;TEARDROP-BARBED RIGHT ARROW;;;;
+27BB;HEAVY TEARDROP-SHANKED RIGHTWARDS ARROW;So;0;ON;;;;;N;HEAVY TEARDROP-SHANKED RIGHT ARROW;;;;
+27BC;WEDGE-TAILED RIGHTWARDS ARROW;So;0;ON;;;;;N;WEDGE-TAILED RIGHT ARROW;;;;
+27BD;HEAVY WEDGE-TAILED RIGHTWARDS ARROW;So;0;ON;;;;;N;HEAVY WEDGE-TAILED RIGHT ARROW;;;;
+27BE;OPEN-OUTLINED RIGHTWARDS ARROW;So;0;ON;;;;;N;OPEN-OUTLINED RIGHT ARROW;;;;
+27BF;DOUBLE CURLY LOOP;So;0;ON;;;;;N;;;;;
+27C0;THREE DIMENSIONAL ANGLE;Sm;0;ON;;;;;Y;;;;;
+27C1;WHITE TRIANGLE CONTAINING SMALL WHITE TRIANGLE;Sm;0;ON;;;;;N;;;;;
+27C2;PERPENDICULAR;Sm;0;ON;;;;;N;;;;;
+27C3;OPEN SUBSET;Sm;0;ON;;;;;Y;;;;;
+27C4;OPEN SUPERSET;Sm;0;ON;;;;;Y;;;;;
+27C5;LEFT S-SHAPED BAG DELIMITER;Ps;0;ON;;;;;Y;;;;;
+27C6;RIGHT S-SHAPED BAG DELIMITER;Pe;0;ON;;;;;Y;;;;;
+27C7;OR WITH DOT INSIDE;Sm;0;ON;;;;;N;;;;;
+27C8;REVERSE SOLIDUS PRECEDING SUBSET;Sm;0;ON;;;;;Y;;;;;
+27C9;SUPERSET PRECEDING SOLIDUS;Sm;0;ON;;;;;Y;;;;;
+27CA;VERTICAL BAR WITH HORIZONTAL STROKE;Sm;0;ON;;;;;N;;;;;
+27CB;MATHEMATICAL RISING DIAGONAL;Sm;0;ON;;;;;Y;;;;;
+27CC;LONG DIVISION;Sm;0;ON;;;;;Y;;;;;
+27CD;MATHEMATICAL FALLING DIAGONAL;Sm;0;ON;;;;;Y;;;;;
+27CE;SQUARED LOGICAL AND;Sm;0;ON;;;;;N;;;;;
+27CF;SQUARED LOGICAL OR;Sm;0;ON;;;;;N;;;;;
+27D0;WHITE DIAMOND WITH CENTRED DOT;Sm;0;ON;;;;;N;;;;;
+27D1;AND WITH DOT;Sm;0;ON;;;;;N;;;;;
+27D2;ELEMENT OF OPENING UPWARDS;Sm;0;ON;;;;;N;;;;;
+27D3;LOWER RIGHT CORNER WITH DOT;Sm;0;ON;;;;;Y;;;;;
+27D4;UPPER LEFT CORNER WITH DOT;Sm;0;ON;;;;;Y;;;;;
+27D5;LEFT OUTER JOIN;Sm;0;ON;;;;;Y;;;;;
+27D6;RIGHT OUTER JOIN;Sm;0;ON;;;;;Y;;;;;
+27D7;FULL OUTER JOIN;Sm;0;ON;;;;;N;;;;;
+27D8;LARGE UP TACK;Sm;0;ON;;;;;N;;;;;
+27D9;LARGE DOWN TACK;Sm;0;ON;;;;;N;;;;;
+27DA;LEFT AND RIGHT DOUBLE TURNSTILE;Sm;0;ON;;;;;N;;;;;
+27DB;LEFT AND RIGHT TACK;Sm;0;ON;;;;;N;;;;;
+27DC;LEFT MULTIMAP;Sm;0;ON;;;;;Y;;;;;
+27DD;LONG RIGHT TACK;Sm;0;ON;;;;;Y;;;;;
+27DE;LONG LEFT TACK;Sm;0;ON;;;;;Y;;;;;
+27DF;UP TACK WITH CIRCLE ABOVE;Sm;0;ON;;;;;N;;;;;
+27E0;LOZENGE DIVIDED BY HORIZONTAL RULE;Sm;0;ON;;;;;N;;;;;
+27E1;WHITE CONCAVE-SIDED DIAMOND;Sm;0;ON;;;;;N;;;;;
+27E2;WHITE CONCAVE-SIDED DIAMOND WITH LEFTWARDS TICK;Sm;0;ON;;;;;Y;;;;;
+27E3;WHITE CONCAVE-SIDED DIAMOND WITH RIGHTWARDS TICK;Sm;0;ON;;;;;Y;;;;;
+27E4;WHITE SQUARE WITH LEFTWARDS TICK;Sm;0;ON;;;;;Y;;;;;
+27E5;WHITE SQUARE WITH RIGHTWARDS TICK;Sm;0;ON;;;;;Y;;;;;
+27E6;MATHEMATICAL LEFT WHITE SQUARE BRACKET;Ps;0;ON;;;;;Y;;;;;
+27E7;MATHEMATICAL RIGHT WHITE SQUARE BRACKET;Pe;0;ON;;;;;Y;;;;;
+27E8;MATHEMATICAL LEFT ANGLE BRACKET;Ps;0;ON;;;;;Y;;;;;
+27E9;MATHEMATICAL RIGHT ANGLE BRACKET;Pe;0;ON;;;;;Y;;;;;
+27EA;MATHEMATICAL LEFT DOUBLE ANGLE BRACKET;Ps;0;ON;;;;;Y;;;;;
+27EB;MATHEMATICAL RIGHT DOUBLE ANGLE BRACKET;Pe;0;ON;;;;;Y;;;;;
+27EC;MATHEMATICAL LEFT WHITE TORTOISE SHELL BRACKET;Ps;0;ON;;;;;Y;;;;;
+27ED;MATHEMATICAL RIGHT WHITE TORTOISE SHELL BRACKET;Pe;0;ON;;;;;Y;;;;;
+27EE;MATHEMATICAL LEFT FLATTENED PARENTHESIS;Ps;0;ON;;;;;Y;;;;;
+27EF;MATHEMATICAL RIGHT FLATTENED PARENTHESIS;Pe;0;ON;;;;;Y;;;;;
+27F0;UPWARDS QUADRUPLE ARROW;Sm;0;ON;;;;;N;;;;;
+27F1;DOWNWARDS QUADRUPLE ARROW;Sm;0;ON;;;;;N;;;;;
+27F2;ANTICLOCKWISE GAPPED CIRCLE ARROW;Sm;0;ON;;;;;N;;;;;
+27F3;CLOCKWISE GAPPED CIRCLE ARROW;Sm;0;ON;;;;;N;;;;;
+27F4;RIGHT ARROW WITH CIRCLED PLUS;Sm;0;ON;;;;;N;;;;;
+27F5;LONG LEFTWARDS ARROW;Sm;0;ON;;;;;N;;;;;
+27F6;LONG RIGHTWARDS ARROW;Sm;0;ON;;;;;N;;;;;
+27F7;LONG LEFT RIGHT ARROW;Sm;0;ON;;;;;N;;;;;
+27F8;LONG LEFTWARDS DOUBLE ARROW;Sm;0;ON;;;;;N;;;;;
+27F9;LONG RIGHTWARDS DOUBLE ARROW;Sm;0;ON;;;;;N;;;;;
+27FA;LONG LEFT RIGHT DOUBLE ARROW;Sm;0;ON;;;;;N;;;;;
+27FB;LONG LEFTWARDS ARROW FROM BAR;Sm;0;ON;;;;;N;;;;;
+27FC;LONG RIGHTWARDS ARROW FROM BAR;Sm;0;ON;;;;;N;;;;;
+27FD;LONG LEFTWARDS DOUBLE ARROW FROM BAR;Sm;0;ON;;;;;N;;;;;
+27FE;LONG RIGHTWARDS DOUBLE ARROW FROM BAR;Sm;0;ON;;;;;N;;;;;
+27FF;LONG RIGHTWARDS SQUIGGLE ARROW;Sm;0;ON;;;;;N;;;;;
+2800;BRAILLE PATTERN BLANK;So;0;L;;;;;N;;;;;
+2801;BRAILLE PATTERN DOTS-1;So;0;L;;;;;N;;;;;
+2802;BRAILLE PATTERN DOTS-2;So;0;L;;;;;N;;;;;
+2803;BRAILLE PATTERN DOTS-12;So;0;L;;;;;N;;;;;
+2804;BRAILLE PATTERN DOTS-3;So;0;L;;;;;N;;;;;
+2805;BRAILLE PATTERN DOTS-13;So;0;L;;;;;N;;;;;
+2806;BRAILLE PATTERN DOTS-23;So;0;L;;;;;N;;;;;
+2807;BRAILLE PATTERN DOTS-123;So;0;L;;;;;N;;;;;
+2808;BRAILLE PATTERN DOTS-4;So;0;L;;;;;N;;;;;
+2809;BRAILLE PATTERN DOTS-14;So;0;L;;;;;N;;;;;
+280A;BRAILLE PATTERN DOTS-24;So;0;L;;;;;N;;;;;
+280B;BRAILLE PATTERN DOTS-124;So;0;L;;;;;N;;;;;
+280C;BRAILLE PATTERN DOTS-34;So;0;L;;;;;N;;;;;
+280D;BRAILLE PATTERN DOTS-134;So;0;L;;;;;N;;;;;
+280E;BRAILLE PATTERN DOTS-234;So;0;L;;;;;N;;;;;
+280F;BRAILLE PATTERN DOTS-1234;So;0;L;;;;;N;;;;;
+2810;BRAILLE PATTERN DOTS-5;So;0;L;;;;;N;;;;;
+2811;BRAILLE PATTERN DOTS-15;So;0;L;;;;;N;;;;;
+2812;BRAILLE PATTERN DOTS-25;So;0;L;;;;;N;;;;;
+2813;BRAILLE PATTERN DOTS-125;So;0;L;;;;;N;;;;;
+2814;BRAILLE PATTERN DOTS-35;So;0;L;;;;;N;;;;;
+2815;BRAILLE PATTERN DOTS-135;So;0;L;;;;;N;;;;;
+2816;BRAILLE PATTERN DOTS-235;So;0;L;;;;;N;;;;;
+2817;BRAILLE PATTERN DOTS-1235;So;0;L;;;;;N;;;;;
+2818;BRAILLE PATTERN DOTS-45;So;0;L;;;;;N;;;;;
+2819;BRAILLE PATTERN DOTS-145;So;0;L;;;;;N;;;;;
+281A;BRAILLE PATTERN DOTS-245;So;0;L;;;;;N;;;;;
+281B;BRAILLE PATTERN DOTS-1245;So;0;L;;;;;N;;;;;
+281C;BRAILLE PATTERN DOTS-345;So;0;L;;;;;N;;;;;
+281D;BRAILLE PATTERN DOTS-1345;So;0;L;;;;;N;;;;;
+281E;BRAILLE PATTERN DOTS-2345;So;0;L;;;;;N;;;;;
+281F;BRAILLE PATTERN DOTS-12345;So;0;L;;;;;N;;;;;
+2820;BRAILLE PATTERN DOTS-6;So;0;L;;;;;N;;;;;
+2821;BRAILLE PATTERN DOTS-16;So;0;L;;;;;N;;;;;
+2822;BRAILLE PATTERN DOTS-26;So;0;L;;;;;N;;;;;
+2823;BRAILLE PATTERN DOTS-126;So;0;L;;;;;N;;;;;
+2824;BRAILLE PATTERN DOTS-36;So;0;L;;;;;N;;;;;
+2825;BRAILLE PATTERN DOTS-136;So;0;L;;;;;N;;;;;
+2826;BRAILLE PATTERN DOTS-236;So;0;L;;;;;N;;;;;
+2827;BRAILLE PATTERN DOTS-1236;So;0;L;;;;;N;;;;;
+2828;BRAILLE PATTERN DOTS-46;So;0;L;;;;;N;;;;;
+2829;BRAILLE PATTERN DOTS-146;So;0;L;;;;;N;;;;;
+282A;BRAILLE PATTERN DOTS-246;So;0;L;;;;;N;;;;;
+282B;BRAILLE PATTERN DOTS-1246;So;0;L;;;;;N;;;;;
+282C;BRAILLE PATTERN DOTS-346;So;0;L;;;;;N;;;;;
+282D;BRAILLE PATTERN DOTS-1346;So;0;L;;;;;N;;;;;
+282E;BRAILLE PATTERN DOTS-2346;So;0;L;;;;;N;;;;;
+282F;BRAILLE PATTERN DOTS-12346;So;0;L;;;;;N;;;;;
+2830;BRAILLE PATTERN DOTS-56;So;0;L;;;;;N;;;;;
+2831;BRAILLE PATTERN DOTS-156;So;0;L;;;;;N;;;;;
+2832;BRAILLE PATTERN DOTS-256;So;0;L;;;;;N;;;;;
+2833;BRAILLE PATTERN DOTS-1256;So;0;L;;;;;N;;;;;
+2834;BRAILLE PATTERN DOTS-356;So;0;L;;;;;N;;;;;
+2835;BRAILLE PATTERN DOTS-1356;So;0;L;;;;;N;;;;;
+2836;BRAILLE PATTERN DOTS-2356;So;0;L;;;;;N;;;;;
+2837;BRAILLE PATTERN DOTS-12356;So;0;L;;;;;N;;;;;
+2838;BRAILLE PATTERN DOTS-456;So;0;L;;;;;N;;;;;
+2839;BRAILLE PATTERN DOTS-1456;So;0;L;;;;;N;;;;;
+283A;BRAILLE PATTERN DOTS-2456;So;0;L;;;;;N;;;;;
+283B;BRAILLE PATTERN DOTS-12456;So;0;L;;;;;N;;;;;
+283C;BRAILLE PATTERN DOTS-3456;So;0;L;;;;;N;;;;;
+283D;BRAILLE PATTERN DOTS-13456;So;0;L;;;;;N;;;;;
+283E;BRAILLE PATTERN DOTS-23456;So;0;L;;;;;N;;;;;
+283F;BRAILLE PATTERN DOTS-123456;So;0;L;;;;;N;;;;;
+2840;BRAILLE PATTERN DOTS-7;So;0;L;;;;;N;;;;;
+2841;BRAILLE PATTERN DOTS-17;So;0;L;;;;;N;;;;;
+2842;BRAILLE PATTERN DOTS-27;So;0;L;;;;;N;;;;;
+2843;BRAILLE PATTERN DOTS-127;So;0;L;;;;;N;;;;;
+2844;BRAILLE PATTERN DOTS-37;So;0;L;;;;;N;;;;;
+2845;BRAILLE PATTERN DOTS-137;So;0;L;;;;;N;;;;;
+2846;BRAILLE PATTERN DOTS-237;So;0;L;;;;;N;;;;;
+2847;BRAILLE PATTERN DOTS-1237;So;0;L;;;;;N;;;;;
+2848;BRAILLE PATTERN DOTS-47;So;0;L;;;;;N;;;;;
+2849;BRAILLE PATTERN DOTS-147;So;0;L;;;;;N;;;;;
+284A;BRAILLE PATTERN DOTS-247;So;0;L;;;;;N;;;;;
+284B;BRAILLE PATTERN DOTS-1247;So;0;L;;;;;N;;;;;
+284C;BRAILLE PATTERN DOTS-347;So;0;L;;;;;N;;;;;
+284D;BRAILLE PATTERN DOTS-1347;So;0;L;;;;;N;;;;;
+284E;BRAILLE PATTERN DOTS-2347;So;0;L;;;;;N;;;;;
+284F;BRAILLE PATTERN DOTS-12347;So;0;L;;;;;N;;;;;
+2850;BRAILLE PATTERN DOTS-57;So;0;L;;;;;N;;;;;
+2851;BRAILLE PATTERN DOTS-157;So;0;L;;;;;N;;;;;
+2852;BRAILLE PATTERN DOTS-257;So;0;L;;;;;N;;;;;
+2853;BRAILLE PATTERN DOTS-1257;So;0;L;;;;;N;;;;;
+2854;BRAILLE PATTERN DOTS-357;So;0;L;;;;;N;;;;;
+2855;BRAILLE PATTERN DOTS-1357;So;0;L;;;;;N;;;;;
+2856;BRAILLE PATTERN DOTS-2357;So;0;L;;;;;N;;;;;
+2857;BRAILLE PATTERN DOTS-12357;So;0;L;;;;;N;;;;;
+2858;BRAILLE PATTERN DOTS-457;So;0;L;;;;;N;;;;;
+2859;BRAILLE PATTERN DOTS-1457;So;0;L;;;;;N;;;;;
+285A;BRAILLE PATTERN DOTS-2457;So;0;L;;;;;N;;;;;
+285B;BRAILLE PATTERN DOTS-12457;So;0;L;;;;;N;;;;;
+285C;BRAILLE PATTERN DOTS-3457;So;0;L;;;;;N;;;;;
+285D;BRAILLE PATTERN DOTS-13457;So;0;L;;;;;N;;;;;
+285E;BRAILLE PATTERN DOTS-23457;So;0;L;;;;;N;;;;;
+285F;BRAILLE PATTERN DOTS-123457;So;0;L;;;;;N;;;;;
+2860;BRAILLE PATTERN DOTS-67;So;0;L;;;;;N;;;;;
+2861;BRAILLE PATTERN DOTS-167;So;0;L;;;;;N;;;;;
+2862;BRAILLE PATTERN DOTS-267;So;0;L;;;;;N;;;;;
+2863;BRAILLE PATTERN DOTS-1267;So;0;L;;;;;N;;;;;
+2864;BRAILLE PATTERN DOTS-367;So;0;L;;;;;N;;;;;
+2865;BRAILLE PATTERN DOTS-1367;So;0;L;;;;;N;;;;;
+2866;BRAILLE PATTERN DOTS-2367;So;0;L;;;;;N;;;;;
+2867;BRAILLE PATTERN DOTS-12367;So;0;L;;;;;N;;;;;
+2868;BRAILLE PATTERN DOTS-467;So;0;L;;;;;N;;;;;
+2869;BRAILLE PATTERN DOTS-1467;So;0;L;;;;;N;;;;;
+286A;BRAILLE PATTERN DOTS-2467;So;0;L;;;;;N;;;;;
+286B;BRAILLE PATTERN DOTS-12467;So;0;L;;;;;N;;;;;
+286C;BRAILLE PATTERN DOTS-3467;So;0;L;;;;;N;;;;;
+286D;BRAILLE PATTERN DOTS-13467;So;0;L;;;;;N;;;;;
+286E;BRAILLE PATTERN DOTS-23467;So;0;L;;;;;N;;;;;
+286F;BRAILLE PATTERN DOTS-123467;So;0;L;;;;;N;;;;;
+2870;BRAILLE PATTERN DOTS-567;So;0;L;;;;;N;;;;;
+2871;BRAILLE PATTERN DOTS-1567;So;0;L;;;;;N;;;;;
+2872;BRAILLE PATTERN DOTS-2567;So;0;L;;;;;N;;;;;
+2873;BRAILLE PATTERN DOTS-12567;So;0;L;;;;;N;;;;;
+2874;BRAILLE PATTERN DOTS-3567;So;0;L;;;;;N;;;;;
+2875;BRAILLE PATTERN DOTS-13567;So;0;L;;;;;N;;;;;
+2876;BRAILLE PATTERN DOTS-23567;So;0;L;;;;;N;;;;;
+2877;BRAILLE PATTERN DOTS-123567;So;0;L;;;;;N;;;;;
+2878;BRAILLE PATTERN DOTS-4567;So;0;L;;;;;N;;;;;
+2879;BRAILLE PATTERN DOTS-14567;So;0;L;;;;;N;;;;;
+287A;BRAILLE PATTERN DOTS-24567;So;0;L;;;;;N;;;;;
+287B;BRAILLE PATTERN DOTS-124567;So;0;L;;;;;N;;;;;
+287C;BRAILLE PATTERN DOTS-34567;So;0;L;;;;;N;;;;;
+287D;BRAILLE PATTERN DOTS-134567;So;0;L;;;;;N;;;;;
+287E;BRAILLE PATTERN DOTS-234567;So;0;L;;;;;N;;;;;
+287F;BRAILLE PATTERN DOTS-1234567;So;0;L;;;;;N;;;;;
+2880;BRAILLE PATTERN DOTS-8;So;0;L;;;;;N;;;;;
+2881;BRAILLE PATTERN DOTS-18;So;0;L;;;;;N;;;;;
+2882;BRAILLE PATTERN DOTS-28;So;0;L;;;;;N;;;;;
+2883;BRAILLE PATTERN DOTS-128;So;0;L;;;;;N;;;;;
+2884;BRAILLE PATTERN DOTS-38;So;0;L;;;;;N;;;;;
+2885;BRAILLE PATTERN DOTS-138;So;0;L;;;;;N;;;;;
+2886;BRAILLE PATTERN DOTS-238;So;0;L;;;;;N;;;;;
+2887;BRAILLE PATTERN DOTS-1238;So;0;L;;;;;N;;;;;
+2888;BRAILLE PATTERN DOTS-48;So;0;L;;;;;N;;;;;
+2889;BRAILLE PATTERN DOTS-148;So;0;L;;;;;N;;;;;
+288A;BRAILLE PATTERN DOTS-248;So;0;L;;;;;N;;;;;
+288B;BRAILLE PATTERN DOTS-1248;So;0;L;;;;;N;;;;;
+288C;BRAILLE PATTERN DOTS-348;So;0;L;;;;;N;;;;;
+288D;BRAILLE PATTERN DOTS-1348;So;0;L;;;;;N;;;;;
+288E;BRAILLE PATTERN DOTS-2348;So;0;L;;;;;N;;;;;
+288F;BRAILLE PATTERN DOTS-12348;So;0;L;;;;;N;;;;;
+2890;BRAILLE PATTERN DOTS-58;So;0;L;;;;;N;;;;;
+2891;BRAILLE PATTERN DOTS-158;So;0;L;;;;;N;;;;;
+2892;BRAILLE PATTERN DOTS-258;So;0;L;;;;;N;;;;;
+2893;BRAILLE PATTERN DOTS-1258;So;0;L;;;;;N;;;;;
+2894;BRAILLE PATTERN DOTS-358;So;0;L;;;;;N;;;;;
+2895;BRAILLE PATTERN DOTS-1358;So;0;L;;;;;N;;;;;
+2896;BRAILLE PATTERN DOTS-2358;So;0;L;;;;;N;;;;;
+2897;BRAILLE PATTERN DOTS-12358;So;0;L;;;;;N;;;;;
+2898;BRAILLE PATTERN DOTS-458;So;0;L;;;;;N;;;;;
+2899;BRAILLE PATTERN DOTS-1458;So;0;L;;;;;N;;;;;
+289A;BRAILLE PATTERN DOTS-2458;So;0;L;;;;;N;;;;;
+289B;BRAILLE PATTERN DOTS-12458;So;0;L;;;;;N;;;;;
+289C;BRAILLE PATTERN DOTS-3458;So;0;L;;;;;N;;;;;
+289D;BRAILLE PATTERN DOTS-13458;So;0;L;;;;;N;;;;;
+289E;BRAILLE PATTERN DOTS-23458;So;0;L;;;;;N;;;;;
+289F;BRAILLE PATTERN DOTS-123458;So;0;L;;;;;N;;;;;
+28A0;BRAILLE PATTERN DOTS-68;So;0;L;;;;;N;;;;;
+28A1;BRAILLE PATTERN DOTS-168;So;0;L;;;;;N;;;;;
+28A2;BRAILLE PATTERN DOTS-268;So;0;L;;;;;N;;;;;
+28A3;BRAILLE PATTERN DOTS-1268;So;0;L;;;;;N;;;;;
+28A4;BRAILLE PATTERN DOTS-368;So;0;L;;;;;N;;;;;
+28A5;BRAILLE PATTERN DOTS-1368;So;0;L;;;;;N;;;;;
+28A6;BRAILLE PATTERN DOTS-2368;So;0;L;;;;;N;;;;;
+28A7;BRAILLE PATTERN DOTS-12368;So;0;L;;;;;N;;;;;
+28A8;BRAILLE PATTERN DOTS-468;So;0;L;;;;;N;;;;;
+28A9;BRAILLE PATTERN DOTS-1468;So;0;L;;;;;N;;;;;
+28AA;BRAILLE PATTERN DOTS-2468;So;0;L;;;;;N;;;;;
+28AB;BRAILLE PATTERN DOTS-12468;So;0;L;;;;;N;;;;;
+28AC;BRAILLE PATTERN DOTS-3468;So;0;L;;;;;N;;;;;
+28AD;BRAILLE PATTERN DOTS-13468;So;0;L;;;;;N;;;;;
+28AE;BRAILLE PATTERN DOTS-23468;So;0;L;;;;;N;;;;;
+28AF;BRAILLE PATTERN DOTS-123468;So;0;L;;;;;N;;;;;
+28B0;BRAILLE PATTERN DOTS-568;So;0;L;;;;;N;;;;;
+28B1;BRAILLE PATTERN DOTS-1568;So;0;L;;;;;N;;;;;
+28B2;BRAILLE PATTERN DOTS-2568;So;0;L;;;;;N;;;;;
+28B3;BRAILLE PATTERN DOTS-12568;So;0;L;;;;;N;;;;;
+28B4;BRAILLE PATTERN DOTS-3568;So;0;L;;;;;N;;;;;
+28B5;BRAILLE PATTERN DOTS-13568;So;0;L;;;;;N;;;;;
+28B6;BRAILLE PATTERN DOTS-23568;So;0;L;;;;;N;;;;;
+28B7;BRAILLE PATTERN DOTS-123568;So;0;L;;;;;N;;;;;
+28B8;BRAILLE PATTERN DOTS-4568;So;0;L;;;;;N;;;;;
+28B9;BRAILLE PATTERN DOTS-14568;So;0;L;;;;;N;;;;;
+28BA;BRAILLE PATTERN DOTS-24568;So;0;L;;;;;N;;;;;
+28BB;BRAILLE PATTERN DOTS-124568;So;0;L;;;;;N;;;;;
+28BC;BRAILLE PATTERN DOTS-34568;So;0;L;;;;;N;;;;;
+28BD;BRAILLE PATTERN DOTS-134568;So;0;L;;;;;N;;;;;
+28BE;BRAILLE PATTERN DOTS-234568;So;0;L;;;;;N;;;;;
+28BF;BRAILLE PATTERN DOTS-1234568;So;0;L;;;;;N;;;;;
+28C0;BRAILLE PATTERN DOTS-78;So;0;L;;;;;N;;;;;
+28C1;BRAILLE PATTERN DOTS-178;So;0;L;;;;;N;;;;;
+28C2;BRAILLE PATTERN DOTS-278;So;0;L;;;;;N;;;;;
+28C3;BRAILLE PATTERN DOTS-1278;So;0;L;;;;;N;;;;;
+28C4;BRAILLE PATTERN DOTS-378;So;0;L;;;;;N;;;;;
+28C5;BRAILLE PATTERN DOTS-1378;So;0;L;;;;;N;;;;;
+28C6;BRAILLE PATTERN DOTS-2378;So;0;L;;;;;N;;;;;
+28C7;BRAILLE PATTERN DOTS-12378;So;0;L;;;;;N;;;;;
+28C8;BRAILLE PATTERN DOTS-478;So;0;L;;;;;N;;;;;
+28C9;BRAILLE PATTERN DOTS-1478;So;0;L;;;;;N;;;;;
+28CA;BRAILLE PATTERN DOTS-2478;So;0;L;;;;;N;;;;;
+28CB;BRAILLE PATTERN DOTS-12478;So;0;L;;;;;N;;;;;
+28CC;BRAILLE PATTERN DOTS-3478;So;0;L;;;;;N;;;;;
+28CD;BRAILLE PATTERN DOTS-13478;So;0;L;;;;;N;;;;;
+28CE;BRAILLE PATTERN DOTS-23478;So;0;L;;;;;N;;;;;
+28CF;BRAILLE PATTERN DOTS-123478;So;0;L;;;;;N;;;;;
+28D0;BRAILLE PATTERN DOTS-578;So;0;L;;;;;N;;;;;
+28D1;BRAILLE PATTERN DOTS-1578;So;0;L;;;;;N;;;;;
+28D2;BRAILLE PATTERN DOTS-2578;So;0;L;;;;;N;;;;;
+28D3;BRAILLE PATTERN DOTS-12578;So;0;L;;;;;N;;;;;
+28D4;BRAILLE PATTERN DOTS-3578;So;0;L;;;;;N;;;;;
+28D5;BRAILLE PATTERN DOTS-13578;So;0;L;;;;;N;;;;;
+28D6;BRAILLE PATTERN DOTS-23578;So;0;L;;;;;N;;;;;
+28D7;BRAILLE PATTERN DOTS-123578;So;0;L;;;;;N;;;;;
+28D8;BRAILLE PATTERN DOTS-4578;So;0;L;;;;;N;;;;;
+28D9;BRAILLE PATTERN DOTS-14578;So;0;L;;;;;N;;;;;
+28DA;BRAILLE PATTERN DOTS-24578;So;0;L;;;;;N;;;;;
+28DB;BRAILLE PATTERN DOTS-124578;So;0;L;;;;;N;;;;;
+28DC;BRAILLE PATTERN DOTS-34578;So;0;L;;;;;N;;;;;
+28DD;BRAILLE PATTERN DOTS-134578;So;0;L;;;;;N;;;;;
+28DE;BRAILLE PATTERN DOTS-234578;So;0;L;;;;;N;;;;;
+28DF;BRAILLE PATTERN DOTS-1234578;So;0;L;;;;;N;;;;;
+28E0;BRAILLE PATTERN DOTS-678;So;0;L;;;;;N;;;;;
+28E1;BRAILLE PATTERN DOTS-1678;So;0;L;;;;;N;;;;;
+28E2;BRAILLE PATTERN DOTS-2678;So;0;L;;;;;N;;;;;
+28E3;BRAILLE PATTERN DOTS-12678;So;0;L;;;;;N;;;;;
+28E4;BRAILLE PATTERN DOTS-3678;So;0;L;;;;;N;;;;;
+28E5;BRAILLE PATTERN DOTS-13678;So;0;L;;;;;N;;;;;
+28E6;BRAILLE PATTERN DOTS-23678;So;0;L;;;;;N;;;;;
+28E7;BRAILLE PATTERN DOTS-123678;So;0;L;;;;;N;;;;;
+28E8;BRAILLE PATTERN DOTS-4678;So;0;L;;;;;N;;;;;
+28E9;BRAILLE PATTERN DOTS-14678;So;0;L;;;;;N;;;;;
+28EA;BRAILLE PATTERN DOTS-24678;So;0;L;;;;;N;;;;;
+28EB;BRAILLE PATTERN DOTS-124678;So;0;L;;;;;N;;;;;
+28EC;BRAILLE PATTERN DOTS-34678;So;0;L;;;;;N;;;;;
+28ED;BRAILLE PATTERN DOTS-134678;So;0;L;;;;;N;;;;;
+28EE;BRAILLE PATTERN DOTS-234678;So;0;L;;;;;N;;;;;
+28EF;BRAILLE PATTERN DOTS-1234678;So;0;L;;;;;N;;;;;
+28F0;BRAILLE PATTERN DOTS-5678;So;0;L;;;;;N;;;;;
+28F1;BRAILLE PATTERN DOTS-15678;So;0;L;;;;;N;;;;;
+28F2;BRAILLE PATTERN DOTS-25678;So;0;L;;;;;N;;;;;
+28F3;BRAILLE PATTERN DOTS-125678;So;0;L;;;;;N;;;;;
+28F4;BRAILLE PATTERN DOTS-35678;So;0;L;;;;;N;;;;;
+28F5;BRAILLE PATTERN DOTS-135678;So;0;L;;;;;N;;;;;
+28F6;BRAILLE PATTERN DOTS-235678;So;0;L;;;;;N;;;;;
+28F7;BRAILLE PATTERN DOTS-1235678;So;0;L;;;;;N;;;;;
+28F8;BRAILLE PATTERN DOTS-45678;So;0;L;;;;;N;;;;;
+28F9;BRAILLE PATTERN DOTS-145678;So;0;L;;;;;N;;;;;
+28FA;BRAILLE PATTERN DOTS-245678;So;0;L;;;;;N;;;;;
+28FB;BRAILLE PATTERN DOTS-1245678;So;0;L;;;;;N;;;;;
+28FC;BRAILLE PATTERN DOTS-345678;So;0;L;;;;;N;;;;;
+28FD;BRAILLE PATTERN DOTS-1345678;So;0;L;;;;;N;;;;;
+28FE;BRAILLE PATTERN DOTS-2345678;So;0;L;;;;;N;;;;;
+28FF;BRAILLE PATTERN DOTS-12345678;So;0;L;;;;;N;;;;;
+2900;RIGHTWARDS TWO-HEADED ARROW WITH VERTICAL STROKE;Sm;0;ON;;;;;N;;;;;
+2901;RIGHTWARDS TWO-HEADED ARROW WITH DOUBLE VERTICAL STROKE;Sm;0;ON;;;;;N;;;;;
+2902;LEFTWARDS DOUBLE ARROW WITH VERTICAL STROKE;Sm;0;ON;;;;;N;;;;;
+2903;RIGHTWARDS DOUBLE ARROW WITH VERTICAL STROKE;Sm;0;ON;;;;;N;;;;;
+2904;LEFT RIGHT DOUBLE ARROW WITH VERTICAL STROKE;Sm;0;ON;;;;;N;;;;;
+2905;RIGHTWARDS TWO-HEADED ARROW FROM BAR;Sm;0;ON;;;;;N;;;;;
+2906;LEFTWARDS DOUBLE ARROW FROM BAR;Sm;0;ON;;;;;N;;;;;
+2907;RIGHTWARDS DOUBLE ARROW FROM BAR;Sm;0;ON;;;;;N;;;;;
+2908;DOWNWARDS ARROW WITH HORIZONTAL STROKE;Sm;0;ON;;;;;N;;;;;
+2909;UPWARDS ARROW WITH HORIZONTAL STROKE;Sm;0;ON;;;;;N;;;;;
+290A;UPWARDS TRIPLE ARROW;Sm;0;ON;;;;;N;;;;;
+290B;DOWNWARDS TRIPLE ARROW;Sm;0;ON;;;;;N;;;;;
+290C;LEFTWARDS DOUBLE DASH ARROW;Sm;0;ON;;;;;N;;;;;
+290D;RIGHTWARDS DOUBLE DASH ARROW;Sm;0;ON;;;;;N;;;;;
+290E;LEFTWARDS TRIPLE DASH ARROW;Sm;0;ON;;;;;N;;;;;
+290F;RIGHTWARDS TRIPLE DASH ARROW;Sm;0;ON;;;;;N;;;;;
+2910;RIGHTWARDS TWO-HEADED TRIPLE DASH ARROW;Sm;0;ON;;;;;N;;;;;
+2911;RIGHTWARDS ARROW WITH DOTTED STEM;Sm;0;ON;;;;;N;;;;;
+2912;UPWARDS ARROW TO BAR;Sm;0;ON;;;;;N;;;;;
+2913;DOWNWARDS ARROW TO BAR;Sm;0;ON;;;;;N;;;;;
+2914;RIGHTWARDS ARROW WITH TAIL WITH VERTICAL STROKE;Sm;0;ON;;;;;N;;;;;
+2915;RIGHTWARDS ARROW WITH TAIL WITH DOUBLE VERTICAL STROKE;Sm;0;ON;;;;;N;;;;;
+2916;RIGHTWARDS TWO-HEADED ARROW WITH TAIL;Sm;0;ON;;;;;N;;;;;
+2917;RIGHTWARDS TWO-HEADED ARROW WITH TAIL WITH VERTICAL STROKE;Sm;0;ON;;;;;N;;;;;
+2918;RIGHTWARDS TWO-HEADED ARROW WITH TAIL WITH DOUBLE VERTICAL STROKE;Sm;0;ON;;;;;N;;;;;
+2919;LEFTWARDS ARROW-TAIL;Sm;0;ON;;;;;N;;;;;
+291A;RIGHTWARDS ARROW-TAIL;Sm;0;ON;;;;;N;;;;;
+291B;LEFTWARDS DOUBLE ARROW-TAIL;Sm;0;ON;;;;;N;;;;;
+291C;RIGHTWARDS DOUBLE ARROW-TAIL;Sm;0;ON;;;;;N;;;;;
+291D;LEFTWARDS ARROW TO BLACK DIAMOND;Sm;0;ON;;;;;N;;;;;
+291E;RIGHTWARDS ARROW TO BLACK DIAMOND;Sm;0;ON;;;;;N;;;;;
+291F;LEFTWARDS ARROW FROM BAR TO BLACK DIAMOND;Sm;0;ON;;;;;N;;;;;
+2920;RIGHTWARDS ARROW FROM BAR TO BLACK DIAMOND;Sm;0;ON;;;;;N;;;;;
+2921;NORTH WEST AND SOUTH EAST ARROW;Sm;0;ON;;;;;N;;;;;
+2922;NORTH EAST AND SOUTH WEST ARROW;Sm;0;ON;;;;;N;;;;;
+2923;NORTH WEST ARROW WITH HOOK;Sm;0;ON;;;;;N;;;;;
+2924;NORTH EAST ARROW WITH HOOK;Sm;0;ON;;;;;N;;;;;
+2925;SOUTH EAST ARROW WITH HOOK;Sm;0;ON;;;;;N;;;;;
+2926;SOUTH WEST ARROW WITH HOOK;Sm;0;ON;;;;;N;;;;;
+2927;NORTH WEST ARROW AND NORTH EAST ARROW;Sm;0;ON;;;;;N;;;;;
+2928;NORTH EAST ARROW AND SOUTH EAST ARROW;Sm;0;ON;;;;;N;;;;;
+2929;SOUTH EAST ARROW AND SOUTH WEST ARROW;Sm;0;ON;;;;;N;;;;;
+292A;SOUTH WEST ARROW AND NORTH WEST ARROW;Sm;0;ON;;;;;N;;;;;
+292B;RISING DIAGONAL CROSSING FALLING DIAGONAL;Sm;0;ON;;;;;N;;;;;
+292C;FALLING DIAGONAL CROSSING RISING DIAGONAL;Sm;0;ON;;;;;N;;;;;
+292D;SOUTH EAST ARROW CROSSING NORTH EAST ARROW;Sm;0;ON;;;;;N;;;;;
+292E;NORTH EAST ARROW CROSSING SOUTH EAST ARROW;Sm;0;ON;;;;;N;;;;;
+292F;FALLING DIAGONAL CROSSING NORTH EAST ARROW;Sm;0;ON;;;;;N;;;;;
+2930;RISING DIAGONAL CROSSING SOUTH EAST ARROW;Sm;0;ON;;;;;N;;;;;
+2931;NORTH EAST ARROW CROSSING NORTH WEST ARROW;Sm;0;ON;;;;;N;;;;;
+2932;NORTH WEST ARROW CROSSING NORTH EAST ARROW;Sm;0;ON;;;;;N;;;;;
+2933;WAVE ARROW POINTING DIRECTLY RIGHT;Sm;0;ON;;;;;N;;;;;
+2934;ARROW POINTING RIGHTWARDS THEN CURVING UPWARDS;Sm;0;ON;;;;;N;;;;;
+2935;ARROW POINTING RIGHTWARDS THEN CURVING DOWNWARDS;Sm;0;ON;;;;;N;;;;;
+2936;ARROW POINTING DOWNWARDS THEN CURVING LEFTWARDS;Sm;0;ON;;;;;N;;;;;
+2937;ARROW POINTING DOWNWARDS THEN CURVING RIGHTWARDS;Sm;0;ON;;;;;N;;;;;
+2938;RIGHT-SIDE ARC CLOCKWISE ARROW;Sm;0;ON;;;;;N;;;;;
+2939;LEFT-SIDE ARC ANTICLOCKWISE ARROW;Sm;0;ON;;;;;N;;;;;
+293A;TOP ARC ANTICLOCKWISE ARROW;Sm;0;ON;;;;;N;;;;;
+293B;BOTTOM ARC ANTICLOCKWISE ARROW;Sm;0;ON;;;;;N;;;;;
+293C;TOP ARC CLOCKWISE ARROW WITH MINUS;Sm;0;ON;;;;;N;;;;;
+293D;TOP ARC ANTICLOCKWISE ARROW WITH PLUS;Sm;0;ON;;;;;N;;;;;
+293E;LOWER RIGHT SEMICIRCULAR CLOCKWISE ARROW;Sm;0;ON;;;;;N;;;;;
+293F;LOWER LEFT SEMICIRCULAR ANTICLOCKWISE ARROW;Sm;0;ON;;;;;N;;;;;
+2940;ANTICLOCKWISE CLOSED CIRCLE ARROW;Sm;0;ON;;;;;N;;;;;
+2941;CLOCKWISE CLOSED CIRCLE ARROW;Sm;0;ON;;;;;N;;;;;
+2942;RIGHTWARDS ARROW ABOVE SHORT LEFTWARDS ARROW;Sm;0;ON;;;;;N;;;;;
+2943;LEFTWARDS ARROW ABOVE SHORT RIGHTWARDS ARROW;Sm;0;ON;;;;;N;;;;;
+2944;SHORT RIGHTWARDS ARROW ABOVE LEFTWARDS ARROW;Sm;0;ON;;;;;N;;;;;
+2945;RIGHTWARDS ARROW WITH PLUS BELOW;Sm;0;ON;;;;;N;;;;;
+2946;LEFTWARDS ARROW WITH PLUS BELOW;Sm;0;ON;;;;;N;;;;;
+2947;RIGHTWARDS ARROW THROUGH X;Sm;0;ON;;;;;N;;;;;
+2948;LEFT RIGHT ARROW THROUGH SMALL CIRCLE;Sm;0;ON;;;;;N;;;;;
+2949;UPWARDS TWO-HEADED ARROW FROM SMALL CIRCLE;Sm;0;ON;;;;;N;;;;;
+294A;LEFT BARB UP RIGHT BARB DOWN HARPOON;Sm;0;ON;;;;;N;;;;;
+294B;LEFT BARB DOWN RIGHT BARB UP HARPOON;Sm;0;ON;;;;;N;;;;;
+294C;UP BARB RIGHT DOWN BARB LEFT HARPOON;Sm;0;ON;;;;;N;;;;;
+294D;UP BARB LEFT DOWN BARB RIGHT HARPOON;Sm;0;ON;;;;;N;;;;;
+294E;LEFT BARB UP RIGHT BARB UP HARPOON;Sm;0;ON;;;;;N;;;;;
+294F;UP BARB RIGHT DOWN BARB RIGHT HARPOON;Sm;0;ON;;;;;N;;;;;
+2950;LEFT BARB DOWN RIGHT BARB DOWN HARPOON;Sm;0;ON;;;;;N;;;;;
+2951;UP BARB LEFT DOWN BARB LEFT HARPOON;Sm;0;ON;;;;;N;;;;;
+2952;LEFTWARDS HARPOON WITH BARB UP TO BAR;Sm;0;ON;;;;;N;;;;;
+2953;RIGHTWARDS HARPOON WITH BARB UP TO BAR;Sm;0;ON;;;;;N;;;;;
+2954;UPWARDS HARPOON WITH BARB RIGHT TO BAR;Sm;0;ON;;;;;N;;;;;
+2955;DOWNWARDS HARPOON WITH BARB RIGHT TO BAR;Sm;0;ON;;;;;N;;;;;
+2956;LEFTWARDS HARPOON WITH BARB DOWN TO BAR;Sm;0;ON;;;;;N;;;;;
+2957;RIGHTWARDS HARPOON WITH BARB DOWN TO BAR;Sm;0;ON;;;;;N;;;;;
+2958;UPWARDS HARPOON WITH BARB LEFT TO BAR;Sm;0;ON;;;;;N;;;;;
+2959;DOWNWARDS HARPOON WITH BARB LEFT TO BAR;Sm;0;ON;;;;;N;;;;;
+295A;LEFTWARDS HARPOON WITH BARB UP FROM BAR;Sm;0;ON;;;;;N;;;;;
+295B;RIGHTWARDS HARPOON WITH BARB UP FROM BAR;Sm;0;ON;;;;;N;;;;;
+295C;UPWARDS HARPOON WITH BARB RIGHT FROM BAR;Sm;0;ON;;;;;N;;;;;
+295D;DOWNWARDS HARPOON WITH BARB RIGHT FROM BAR;Sm;0;ON;;;;;N;;;;;
+295E;LEFTWARDS HARPOON WITH BARB DOWN FROM BAR;Sm;0;ON;;;;;N;;;;;
+295F;RIGHTWARDS HARPOON WITH BARB DOWN FROM BAR;Sm;0;ON;;;;;N;;;;;
+2960;UPWARDS HARPOON WITH BARB LEFT FROM BAR;Sm;0;ON;;;;;N;;;;;
+2961;DOWNWARDS HARPOON WITH BARB LEFT FROM BAR;Sm;0;ON;;;;;N;;;;;
+2962;LEFTWARDS HARPOON WITH BARB UP ABOVE LEFTWARDS HARPOON WITH BARB DOWN;Sm;0;ON;;;;;N;;;;;
+2963;UPWARDS HARPOON WITH BARB LEFT BESIDE UPWARDS HARPOON WITH BARB RIGHT;Sm;0;ON;;;;;N;;;;;
+2964;RIGHTWARDS HARPOON WITH BARB UP ABOVE RIGHTWARDS HARPOON WITH BARB DOWN;Sm;0;ON;;;;;N;;;;;
+2965;DOWNWARDS HARPOON WITH BARB LEFT BESIDE DOWNWARDS HARPOON WITH BARB RIGHT;Sm;0;ON;;;;;N;;;;;
+2966;LEFTWARDS HARPOON WITH BARB UP ABOVE RIGHTWARDS HARPOON WITH BARB UP;Sm;0;ON;;;;;N;;;;;
+2967;LEFTWARDS HARPOON WITH BARB DOWN ABOVE RIGHTWARDS HARPOON WITH BARB DOWN;Sm;0;ON;;;;;N;;;;;
+2968;RIGHTWARDS HARPOON WITH BARB UP ABOVE LEFTWARDS HARPOON WITH BARB UP;Sm;0;ON;;;;;N;;;;;
+2969;RIGHTWARDS HARPOON WITH BARB DOWN ABOVE LEFTWARDS HARPOON WITH BARB DOWN;Sm;0;ON;;;;;N;;;;;
+296A;LEFTWARDS HARPOON WITH BARB UP ABOVE LONG DASH;Sm;0;ON;;;;;N;;;;;
+296B;LEFTWARDS HARPOON WITH BARB DOWN BELOW LONG DASH;Sm;0;ON;;;;;N;;;;;
+296C;RIGHTWARDS HARPOON WITH BARB UP ABOVE LONG DASH;Sm;0;ON;;;;;N;;;;;
+296D;RIGHTWARDS HARPOON WITH BARB DOWN BELOW LONG DASH;Sm;0;ON;;;;;N;;;;;
+296E;UPWARDS HARPOON WITH BARB LEFT BESIDE DOWNWARDS HARPOON WITH BARB RIGHT;Sm;0;ON;;;;;N;;;;;
+296F;DOWNWARDS HARPOON WITH BARB LEFT BESIDE UPWARDS HARPOON WITH BARB RIGHT;Sm;0;ON;;;;;N;;;;;
+2970;RIGHT DOUBLE ARROW WITH ROUNDED HEAD;Sm;0;ON;;;;;N;;;;;
+2971;EQUALS SIGN ABOVE RIGHTWARDS ARROW;Sm;0;ON;;;;;N;;;;;
+2972;TILDE OPERATOR ABOVE RIGHTWARDS ARROW;Sm;0;ON;;;;;N;;;;;
+2973;LEFTWARDS ARROW ABOVE TILDE OPERATOR;Sm;0;ON;;;;;N;;;;;
+2974;RIGHTWARDS ARROW ABOVE TILDE OPERATOR;Sm;0;ON;;;;;N;;;;;
+2975;RIGHTWARDS ARROW ABOVE ALMOST EQUAL TO;Sm;0;ON;;;;;N;;;;;
+2976;LESS-THAN ABOVE LEFTWARDS ARROW;Sm;0;ON;;;;;N;;;;;
+2977;LEFTWARDS ARROW THROUGH LESS-THAN;Sm;0;ON;;;;;N;;;;;
+2978;GREATER-THAN ABOVE RIGHTWARDS ARROW;Sm;0;ON;;;;;N;;;;;
+2979;SUBSET ABOVE RIGHTWARDS ARROW;Sm;0;ON;;;;;N;;;;;
+297A;LEFTWARDS ARROW THROUGH SUBSET;Sm;0;ON;;;;;N;;;;;
+297B;SUPERSET ABOVE LEFTWARDS ARROW;Sm;0;ON;;;;;N;;;;;
+297C;LEFT FISH TAIL;Sm;0;ON;;;;;N;;;;;
+297D;RIGHT FISH TAIL;Sm;0;ON;;;;;N;;;;;
+297E;UP FISH TAIL;Sm;0;ON;;;;;N;;;;;
+297F;DOWN FISH TAIL;Sm;0;ON;;;;;N;;;;;
+2980;TRIPLE VERTICAL BAR DELIMITER;Sm;0;ON;;;;;N;;;;;
+2981;Z NOTATION SPOT;Sm;0;ON;;;;;N;;;;;
+2982;Z NOTATION TYPE COLON;Sm;0;ON;;;;;N;;;;;
+2983;LEFT WHITE CURLY BRACKET;Ps;0;ON;;;;;Y;;;;;
+2984;RIGHT WHITE CURLY BRACKET;Pe;0;ON;;;;;Y;;;;;
+2985;LEFT WHITE PARENTHESIS;Ps;0;ON;;;;;Y;;;;;
+2986;RIGHT WHITE PARENTHESIS;Pe;0;ON;;;;;Y;;;;;
+2987;Z NOTATION LEFT IMAGE BRACKET;Ps;0;ON;;;;;Y;;;;;
+2988;Z NOTATION RIGHT IMAGE BRACKET;Pe;0;ON;;;;;Y;;;;;
+2989;Z NOTATION LEFT BINDING BRACKET;Ps;0;ON;;;;;Y;;;;;
+298A;Z NOTATION RIGHT BINDING BRACKET;Pe;0;ON;;;;;Y;;;;;
+298B;LEFT SQUARE BRACKET WITH UNDERBAR;Ps;0;ON;;;;;Y;;;;;
+298C;RIGHT SQUARE BRACKET WITH UNDERBAR;Pe;0;ON;;;;;Y;;;;;
+298D;LEFT SQUARE BRACKET WITH TICK IN TOP CORNER;Ps;0;ON;;;;;Y;;;;;
+298E;RIGHT SQUARE BRACKET WITH TICK IN BOTTOM CORNER;Pe;0;ON;;;;;Y;;;;;
+298F;LEFT SQUARE BRACKET WITH TICK IN BOTTOM CORNER;Ps;0;ON;;;;;Y;;;;;
+2990;RIGHT SQUARE BRACKET WITH TICK IN TOP CORNER;Pe;0;ON;;;;;Y;;;;;
+2991;LEFT ANGLE BRACKET WITH DOT;Ps;0;ON;;;;;Y;;;;;
+2992;RIGHT ANGLE BRACKET WITH DOT;Pe;0;ON;;;;;Y;;;;;
+2993;LEFT ARC LESS-THAN BRACKET;Ps;0;ON;;;;;Y;;;;;
+2994;RIGHT ARC GREATER-THAN BRACKET;Pe;0;ON;;;;;Y;;;;;
+2995;DOUBLE LEFT ARC GREATER-THAN BRACKET;Ps;0;ON;;;;;Y;;;;;
+2996;DOUBLE RIGHT ARC LESS-THAN BRACKET;Pe;0;ON;;;;;Y;;;;;
+2997;LEFT BLACK TORTOISE SHELL BRACKET;Ps;0;ON;;;;;Y;;;;;
+2998;RIGHT BLACK TORTOISE SHELL BRACKET;Pe;0;ON;;;;;Y;;;;;
+2999;DOTTED FENCE;Sm;0;ON;;;;;N;;;;;
+299A;VERTICAL ZIGZAG LINE;Sm;0;ON;;;;;N;;;;;
+299B;MEASURED ANGLE OPENING LEFT;Sm;0;ON;;;;;Y;;;;;
+299C;RIGHT ANGLE VARIANT WITH SQUARE;Sm;0;ON;;;;;Y;;;;;
+299D;MEASURED RIGHT ANGLE WITH DOT;Sm;0;ON;;;;;Y;;;;;
+299E;ANGLE WITH S INSIDE;Sm;0;ON;;;;;Y;;;;;
+299F;ACUTE ANGLE;Sm;0;ON;;;;;Y;;;;;
+29A0;SPHERICAL ANGLE OPENING LEFT;Sm;0;ON;;;;;Y;;;;;
+29A1;SPHERICAL ANGLE OPENING UP;Sm;0;ON;;;;;Y;;;;;
+29A2;TURNED ANGLE;Sm;0;ON;;;;;Y;;;;;
+29A3;REVERSED ANGLE;Sm;0;ON;;;;;Y;;;;;
+29A4;ANGLE WITH UNDERBAR;Sm;0;ON;;;;;Y;;;;;
+29A5;REVERSED ANGLE WITH UNDERBAR;Sm;0;ON;;;;;Y;;;;;
+29A6;OBLIQUE ANGLE OPENING UP;Sm;0;ON;;;;;Y;;;;;
+29A7;OBLIQUE ANGLE OPENING DOWN;Sm;0;ON;;;;;Y;;;;;
+29A8;MEASURED ANGLE WITH OPEN ARM ENDING IN ARROW POINTING UP AND RIGHT;Sm;0;ON;;;;;Y;;;;;
+29A9;MEASURED ANGLE WITH OPEN ARM ENDING IN ARROW POINTING UP AND LEFT;Sm;0;ON;;;;;Y;;;;;
+29AA;MEASURED ANGLE WITH OPEN ARM ENDING IN ARROW POINTING DOWN AND RIGHT;Sm;0;ON;;;;;Y;;;;;
+29AB;MEASURED ANGLE WITH OPEN ARM ENDING IN ARROW POINTING DOWN AND LEFT;Sm;0;ON;;;;;Y;;;;;
+29AC;MEASURED ANGLE WITH OPEN ARM ENDING IN ARROW POINTING RIGHT AND UP;Sm;0;ON;;;;;Y;;;;;
+29AD;MEASURED ANGLE WITH OPEN ARM ENDING IN ARROW POINTING LEFT AND UP;Sm;0;ON;;;;;Y;;;;;
+29AE;MEASURED ANGLE WITH OPEN ARM ENDING IN ARROW POINTING RIGHT AND DOWN;Sm;0;ON;;;;;Y;;;;;
+29AF;MEASURED ANGLE WITH OPEN ARM ENDING IN ARROW POINTING LEFT AND DOWN;Sm;0;ON;;;;;Y;;;;;
+29B0;REVERSED EMPTY SET;Sm;0;ON;;;;;N;;;;;
+29B1;EMPTY SET WITH OVERBAR;Sm;0;ON;;;;;N;;;;;
+29B2;EMPTY SET WITH SMALL CIRCLE ABOVE;Sm;0;ON;;;;;N;;;;;
+29B3;EMPTY SET WITH RIGHT ARROW ABOVE;Sm;0;ON;;;;;N;;;;;
+29B4;EMPTY SET WITH LEFT ARROW ABOVE;Sm;0;ON;;;;;N;;;;;
+29B5;CIRCLE WITH HORIZONTAL BAR;Sm;0;ON;;;;;N;;;;;
+29B6;CIRCLED VERTICAL BAR;Sm;0;ON;;;;;N;;;;;
+29B7;CIRCLED PARALLEL;Sm;0;ON;;;;;N;;;;;
+29B8;CIRCLED REVERSE SOLIDUS;Sm;0;ON;;;;;Y;;;;;
+29B9;CIRCLED PERPENDICULAR;Sm;0;ON;;;;;N;;;;;
+29BA;CIRCLE DIVIDED BY HORIZONTAL BAR AND TOP HALF DIVIDED BY VERTICAL BAR;Sm;0;ON;;;;;N;;;;;
+29BB;CIRCLE WITH SUPERIMPOSED X;Sm;0;ON;;;;;N;;;;;
+29BC;CIRCLED ANTICLOCKWISE-ROTATED DIVISION SIGN;Sm;0;ON;;;;;N;;;;;
+29BD;UP ARROW THROUGH CIRCLE;Sm;0;ON;;;;;N;;;;;
+29BE;CIRCLED WHITE BULLET;Sm;0;ON;;;;;N;;;;;
+29BF;CIRCLED BULLET;Sm;0;ON;;;;;N;;;;;
+29C0;CIRCLED LESS-THAN;Sm;0;ON;;;;;Y;;;;;
+29C1;CIRCLED GREATER-THAN;Sm;0;ON;;;;;Y;;;;;
+29C2;CIRCLE WITH SMALL CIRCLE TO THE RIGHT;Sm;0;ON;;;;;Y;;;;;
+29C3;CIRCLE WITH TWO HORIZONTAL STROKES TO THE RIGHT;Sm;0;ON;;;;;Y;;;;;
+29C4;SQUARED RISING DIAGONAL SLASH;Sm;0;ON;;;;;Y;;;;;
+29C5;SQUARED FALLING DIAGONAL SLASH;Sm;0;ON;;;;;Y;;;;;
+29C6;SQUARED ASTERISK;Sm;0;ON;;;;;N;;;;;
+29C7;SQUARED SMALL CIRCLE;Sm;0;ON;;;;;N;;;;;
+29C8;SQUARED SQUARE;Sm;0;ON;;;;;N;;;;;
+29C9;TWO JOINED SQUARES;Sm;0;ON;;;;;Y;;;;;
+29CA;TRIANGLE WITH DOT ABOVE;Sm;0;ON;;;;;N;;;;;
+29CB;TRIANGLE WITH UNDERBAR;Sm;0;ON;;;;;N;;;;;
+29CC;S IN TRIANGLE;Sm;0;ON;;;;;N;;;;;
+29CD;TRIANGLE WITH SERIFS AT BOTTOM;Sm;0;ON;;;;;N;;;;;
+29CE;RIGHT TRIANGLE ABOVE LEFT TRIANGLE;Sm;0;ON;;;;;Y;;;;;
+29CF;LEFT TRIANGLE BESIDE VERTICAL BAR;Sm;0;ON;;;;;Y;;;;;
+29D0;VERTICAL BAR BESIDE RIGHT TRIANGLE;Sm;0;ON;;;;;Y;;;;;
+29D1;BOWTIE WITH LEFT HALF BLACK;Sm;0;ON;;;;;Y;;;;;
+29D2;BOWTIE WITH RIGHT HALF BLACK;Sm;0;ON;;;;;Y;;;;;
+29D3;BLACK BOWTIE;Sm;0;ON;;;;;N;;;;;
+29D4;TIMES WITH LEFT HALF BLACK;Sm;0;ON;;;;;Y;;;;;
+29D5;TIMES WITH RIGHT HALF BLACK;Sm;0;ON;;;;;Y;;;;;
+29D6;WHITE HOURGLASS;Sm;0;ON;;;;;N;;;;;
+29D7;BLACK HOURGLASS;Sm;0;ON;;;;;N;;;;;
+29D8;LEFT WIGGLY FENCE;Ps;0;ON;;;;;Y;;;;;
+29D9;RIGHT WIGGLY FENCE;Pe;0;ON;;;;;Y;;;;;
+29DA;LEFT DOUBLE WIGGLY FENCE;Ps;0;ON;;;;;Y;;;;;
+29DB;RIGHT DOUBLE WIGGLY FENCE;Pe;0;ON;;;;;Y;;;;;
+29DC;INCOMPLETE INFINITY;Sm;0;ON;;;;;Y;;;;;
+29DD;TIE OVER INFINITY;Sm;0;ON;;;;;N;;;;;
+29DE;INFINITY NEGATED WITH VERTICAL BAR;Sm;0;ON;;;;;N;;;;;
+29DF;DOUBLE-ENDED MULTIMAP;Sm;0;ON;;;;;N;;;;;
+29E0;SQUARE WITH CONTOURED OUTLINE;Sm;0;ON;;;;;N;;;;;
+29E1;INCREASES AS;Sm;0;ON;;;;;Y;;;;;
+29E2;SHUFFLE PRODUCT;Sm;0;ON;;;;;N;;;;;
+29E3;EQUALS SIGN AND SLANTED PARALLEL;Sm;0;ON;;;;;Y;;;;;
+29E4;EQUALS SIGN AND SLANTED PARALLEL WITH TILDE ABOVE;Sm;0;ON;;;;;Y;;;;;
+29E5;IDENTICAL TO AND SLANTED PARALLEL;Sm;0;ON;;;;;Y;;;;;
+29E6;GLEICH STARK;Sm;0;ON;;;;;N;;;;;
+29E7;THERMODYNAMIC;Sm;0;ON;;;;;N;;;;;
+29E8;DOWN-POINTING TRIANGLE WITH LEFT HALF BLACK;Sm;0;ON;;;;;Y;;;;;
+29E9;DOWN-POINTING TRIANGLE WITH RIGHT HALF BLACK;Sm;0;ON;;;;;Y;;;;;
+29EA;BLACK DIAMOND WITH DOWN ARROW;Sm;0;ON;;;;;N;;;;;
+29EB;BLACK LOZENGE;Sm;0;ON;;;;;N;;;;;
+29EC;WHITE CIRCLE WITH DOWN ARROW;Sm;0;ON;;;;;N;;;;;
+29ED;BLACK CIRCLE WITH DOWN ARROW;Sm;0;ON;;;;;N;;;;;
+29EE;ERROR-BARRED WHITE SQUARE;Sm;0;ON;;;;;N;;;;;
+29EF;ERROR-BARRED BLACK SQUARE;Sm;0;ON;;;;;N;;;;;
+29F0;ERROR-BARRED WHITE DIAMOND;Sm;0;ON;;;;;N;;;;;
+29F1;ERROR-BARRED BLACK DIAMOND;Sm;0;ON;;;;;N;;;;;
+29F2;ERROR-BARRED WHITE CIRCLE;Sm;0;ON;;;;;N;;;;;
+29F3;ERROR-BARRED BLACK CIRCLE;Sm;0;ON;;;;;N;;;;;
+29F4;RULE-DELAYED;Sm;0;ON;;;;;Y;;;;;
+29F5;REVERSE SOLIDUS OPERATOR;Sm;0;ON;;;;;Y;;;;;
+29F6;SOLIDUS WITH OVERBAR;Sm;0;ON;;;;;Y;;;;;
+29F7;REVERSE SOLIDUS WITH HORIZONTAL STROKE;Sm;0;ON;;;;;Y;;;;;
+29F8;BIG SOLIDUS;Sm;0;ON;;;;;Y;;;;;
+29F9;BIG REVERSE SOLIDUS;Sm;0;ON;;;;;Y;;;;;
+29FA;DOUBLE PLUS;Sm;0;ON;;;;;N;;;;;
+29FB;TRIPLE PLUS;Sm;0;ON;;;;;N;;;;;
+29FC;LEFT-POINTING CURVED ANGLE BRACKET;Ps;0;ON;;;;;Y;;;;;
+29FD;RIGHT-POINTING CURVED ANGLE BRACKET;Pe;0;ON;;;;;Y;;;;;
+29FE;TINY;Sm;0;ON;;;;;N;;;;;
+29FF;MINY;Sm;0;ON;;;;;N;;;;;
+2A00;N-ARY CIRCLED DOT OPERATOR;Sm;0;ON;;;;;N;;;;;
+2A01;N-ARY CIRCLED PLUS OPERATOR;Sm;0;ON;;;;;N;;;;;
+2A02;N-ARY CIRCLED TIMES OPERATOR;Sm;0;ON;;;;;N;;;;;
+2A03;N-ARY UNION OPERATOR WITH DOT;Sm;0;ON;;;;;N;;;;;
+2A04;N-ARY UNION OPERATOR WITH PLUS;Sm;0;ON;;;;;N;;;;;
+2A05;N-ARY SQUARE INTERSECTION OPERATOR;Sm;0;ON;;;;;N;;;;;
+2A06;N-ARY SQUARE UNION OPERATOR;Sm;0;ON;;;;;N;;;;;
+2A07;TWO LOGICAL AND OPERATOR;Sm;0;ON;;;;;N;;;;;
+2A08;TWO LOGICAL OR OPERATOR;Sm;0;ON;;;;;N;;;;;
+2A09;N-ARY TIMES OPERATOR;Sm;0;ON;;;;;N;;;;;
+2A0A;MODULO TWO SUM;Sm;0;ON;;;;;Y;;;;;
+2A0B;SUMMATION WITH INTEGRAL;Sm;0;ON;;;;;Y;;;;;
+2A0C;QUADRUPLE INTEGRAL OPERATOR;Sm;0;ON;<compat> 222B 222B 222B 222B;;;;Y;;;;;
+2A0D;FINITE PART INTEGRAL;Sm;0;ON;;;;;Y;;;;;
+2A0E;INTEGRAL WITH DOUBLE STROKE;Sm;0;ON;;;;;Y;;;;;
+2A0F;INTEGRAL AVERAGE WITH SLASH;Sm;0;ON;;;;;Y;;;;;
+2A10;CIRCULATION FUNCTION;Sm;0;ON;;;;;Y;;;;;
+2A11;ANTICLOCKWISE INTEGRATION;Sm;0;ON;;;;;Y;;;;;
+2A12;LINE INTEGRATION WITH RECTANGULAR PATH AROUND POLE;Sm;0;ON;;;;;Y;;;;;
+2A13;LINE INTEGRATION WITH SEMICIRCULAR PATH AROUND POLE;Sm;0;ON;;;;;Y;;;;;
+2A14;LINE INTEGRATION NOT INCLUDING THE POLE;Sm;0;ON;;;;;Y;;;;;
+2A15;INTEGRAL AROUND A POINT OPERATOR;Sm;0;ON;;;;;Y;;;;;
+2A16;QUATERNION INTEGRAL OPERATOR;Sm;0;ON;;;;;Y;;;;;
+2A17;INTEGRAL WITH LEFTWARDS ARROW WITH HOOK;Sm;0;ON;;;;;Y;;;;;
+2A18;INTEGRAL WITH TIMES SIGN;Sm;0;ON;;;;;Y;;;;;
+2A19;INTEGRAL WITH INTERSECTION;Sm;0;ON;;;;;Y;;;;;
+2A1A;INTEGRAL WITH UNION;Sm;0;ON;;;;;Y;;;;;
+2A1B;INTEGRAL WITH OVERBAR;Sm;0;ON;;;;;Y;;;;;
+2A1C;INTEGRAL WITH UNDERBAR;Sm;0;ON;;;;;Y;;;;;
+2A1D;JOIN;Sm;0;ON;;;;;N;;;;;
+2A1E;LARGE LEFT TRIANGLE OPERATOR;Sm;0;ON;;;;;Y;;;;;
+2A1F;Z NOTATION SCHEMA COMPOSITION;Sm;0;ON;;;;;Y;;;;;
+2A20;Z NOTATION SCHEMA PIPING;Sm;0;ON;;;;;Y;;;;;
+2A21;Z NOTATION SCHEMA PROJECTION;Sm;0;ON;;;;;Y;;;;;
+2A22;PLUS SIGN WITH SMALL CIRCLE ABOVE;Sm;0;ON;;;;;N;;;;;
+2A23;PLUS SIGN WITH CIRCUMFLEX ACCENT ABOVE;Sm;0;ON;;;;;N;;;;;
+2A24;PLUS SIGN WITH TILDE ABOVE;Sm;0;ON;;;;;Y;;;;;
+2A25;PLUS SIGN WITH DOT BELOW;Sm;0;ON;;;;;N;;;;;
+2A26;PLUS SIGN WITH TILDE BELOW;Sm;0;ON;;;;;Y;;;;;
+2A27;PLUS SIGN WITH SUBSCRIPT TWO;Sm;0;ON;;;;;N;;;;;
+2A28;PLUS SIGN WITH BLACK TRIANGLE;Sm;0;ON;;;;;N;;;;;
+2A29;MINUS SIGN WITH COMMA ABOVE;Sm;0;ON;;;;;Y;;;;;
+2A2A;MINUS SIGN WITH DOT BELOW;Sm;0;ON;;;;;N;;;;;
+2A2B;MINUS SIGN WITH FALLING DOTS;Sm;0;ON;;;;;Y;;;;;
+2A2C;MINUS SIGN WITH RISING DOTS;Sm;0;ON;;;;;Y;;;;;
+2A2D;PLUS SIGN IN LEFT HALF CIRCLE;Sm;0;ON;;;;;Y;;;;;
+2A2E;PLUS SIGN IN RIGHT HALF CIRCLE;Sm;0;ON;;;;;Y;;;;;
+2A2F;VECTOR OR CROSS PRODUCT;Sm;0;ON;;;;;N;;;;;
+2A30;MULTIPLICATION SIGN WITH DOT ABOVE;Sm;0;ON;;;;;N;;;;;
+2A31;MULTIPLICATION SIGN WITH UNDERBAR;Sm;0;ON;;;;;N;;;;;
+2A32;SEMIDIRECT PRODUCT WITH BOTTOM CLOSED;Sm;0;ON;;;;;N;;;;;
+2A33;SMASH PRODUCT;Sm;0;ON;;;;;N;;;;;
+2A34;MULTIPLICATION SIGN IN LEFT HALF CIRCLE;Sm;0;ON;;;;;Y;;;;;
+2A35;MULTIPLICATION SIGN IN RIGHT HALF CIRCLE;Sm;0;ON;;;;;Y;;;;;
+2A36;CIRCLED MULTIPLICATION SIGN WITH CIRCUMFLEX ACCENT;Sm;0;ON;;;;;N;;;;;
+2A37;MULTIPLICATION SIGN IN DOUBLE CIRCLE;Sm;0;ON;;;;;N;;;;;
+2A38;CIRCLED DIVISION SIGN;Sm;0;ON;;;;;N;;;;;
+2A39;PLUS SIGN IN TRIANGLE;Sm;0;ON;;;;;N;;;;;
+2A3A;MINUS SIGN IN TRIANGLE;Sm;0;ON;;;;;N;;;;;
+2A3B;MULTIPLICATION SIGN IN TRIANGLE;Sm;0;ON;;;;;N;;;;;
+2A3C;INTERIOR PRODUCT;Sm;0;ON;;;;;Y;;;;;
+2A3D;RIGHTHAND INTERIOR PRODUCT;Sm;0;ON;;;;;Y;;;;;
+2A3E;Z NOTATION RELATIONAL COMPOSITION;Sm;0;ON;;;;;Y;;;;;
+2A3F;AMALGAMATION OR COPRODUCT;Sm;0;ON;;;;;N;;;;;
+2A40;INTERSECTION WITH DOT;Sm;0;ON;;;;;N;;;;;
+2A41;UNION WITH MINUS SIGN;Sm;0;ON;;;;;N;;;;;
+2A42;UNION WITH OVERBAR;Sm;0;ON;;;;;N;;;;;
+2A43;INTERSECTION WITH OVERBAR;Sm;0;ON;;;;;N;;;;;
+2A44;INTERSECTION WITH LOGICAL AND;Sm;0;ON;;;;;N;;;;;
+2A45;UNION WITH LOGICAL OR;Sm;0;ON;;;;;N;;;;;
+2A46;UNION ABOVE INTERSECTION;Sm;0;ON;;;;;N;;;;;
+2A47;INTERSECTION ABOVE UNION;Sm;0;ON;;;;;N;;;;;
+2A48;UNION ABOVE BAR ABOVE INTERSECTION;Sm;0;ON;;;;;N;;;;;
+2A49;INTERSECTION ABOVE BAR ABOVE UNION;Sm;0;ON;;;;;N;;;;;
+2A4A;UNION BESIDE AND JOINED WITH UNION;Sm;0;ON;;;;;N;;;;;
+2A4B;INTERSECTION BESIDE AND JOINED WITH INTERSECTION;Sm;0;ON;;;;;N;;;;;
+2A4C;CLOSED UNION WITH SERIFS;Sm;0;ON;;;;;N;;;;;
+2A4D;CLOSED INTERSECTION WITH SERIFS;Sm;0;ON;;;;;N;;;;;
+2A4E;DOUBLE SQUARE INTERSECTION;Sm;0;ON;;;;;N;;;;;
+2A4F;DOUBLE SQUARE UNION;Sm;0;ON;;;;;N;;;;;
+2A50;CLOSED UNION WITH SERIFS AND SMASH PRODUCT;Sm;0;ON;;;;;N;;;;;
+2A51;LOGICAL AND WITH DOT ABOVE;Sm;0;ON;;;;;N;;;;;
+2A52;LOGICAL OR WITH DOT ABOVE;Sm;0;ON;;;;;N;;;;;
+2A53;DOUBLE LOGICAL AND;Sm;0;ON;;;;;N;;;;;
+2A54;DOUBLE LOGICAL OR;Sm;0;ON;;;;;N;;;;;
+2A55;TWO INTERSECTING LOGICAL AND;Sm;0;ON;;;;;N;;;;;
+2A56;TWO INTERSECTING LOGICAL OR;Sm;0;ON;;;;;N;;;;;
+2A57;SLOPING LARGE OR;Sm;0;ON;;;;;Y;;;;;
+2A58;SLOPING LARGE AND;Sm;0;ON;;;;;Y;;;;;
+2A59;LOGICAL OR OVERLAPPING LOGICAL AND;Sm;0;ON;;;;;N;;;;;
+2A5A;LOGICAL AND WITH MIDDLE STEM;Sm;0;ON;;;;;N;;;;;
+2A5B;LOGICAL OR WITH MIDDLE STEM;Sm;0;ON;;;;;N;;;;;
+2A5C;LOGICAL AND WITH HORIZONTAL DASH;Sm;0;ON;;;;;N;;;;;
+2A5D;LOGICAL OR WITH HORIZONTAL DASH;Sm;0;ON;;;;;N;;;;;
+2A5E;LOGICAL AND WITH DOUBLE OVERBAR;Sm;0;ON;;;;;N;;;;;
+2A5F;LOGICAL AND WITH UNDERBAR;Sm;0;ON;;;;;N;;;;;
+2A60;LOGICAL AND WITH DOUBLE UNDERBAR;Sm;0;ON;;;;;N;;;;;
+2A61;SMALL VEE WITH UNDERBAR;Sm;0;ON;;;;;N;;;;;
+2A62;LOGICAL OR WITH DOUBLE OVERBAR;Sm;0;ON;;;;;N;;;;;
+2A63;LOGICAL OR WITH DOUBLE UNDERBAR;Sm;0;ON;;;;;N;;;;;
+2A64;Z NOTATION DOMAIN ANTIRESTRICTION;Sm;0;ON;;;;;Y;;;;;
+2A65;Z NOTATION RANGE ANTIRESTRICTION;Sm;0;ON;;;;;Y;;;;;
+2A66;EQUALS SIGN WITH DOT BELOW;Sm;0;ON;;;;;N;;;;;
+2A67;IDENTICAL WITH DOT ABOVE;Sm;0;ON;;;;;N;;;;;
+2A68;TRIPLE HORIZONTAL BAR WITH DOUBLE VERTICAL STROKE;Sm;0;ON;;;;;N;;;;;
+2A69;TRIPLE HORIZONTAL BAR WITH TRIPLE VERTICAL STROKE;Sm;0;ON;;;;;N;;;;;
+2A6A;TILDE OPERATOR WITH DOT ABOVE;Sm;0;ON;;;;;Y;;;;;
+2A6B;TILDE OPERATOR WITH RISING DOTS;Sm;0;ON;;;;;Y;;;;;
+2A6C;SIMILAR MINUS SIMILAR;Sm;0;ON;;;;;Y;;;;;
+2A6D;CONGRUENT WITH DOT ABOVE;Sm;0;ON;;;;;Y;;;;;
+2A6E;EQUALS WITH ASTERISK;Sm;0;ON;;;;;N;;;;;
+2A6F;ALMOST EQUAL TO WITH CIRCUMFLEX ACCENT;Sm;0;ON;;;;;Y;;;;;
+2A70;APPROXIMATELY EQUAL OR EQUAL TO;Sm;0;ON;;;;;Y;;;;;
+2A71;EQUALS SIGN ABOVE PLUS SIGN;Sm;0;ON;;;;;N;;;;;
+2A72;PLUS SIGN ABOVE EQUALS SIGN;Sm;0;ON;;;;;N;;;;;
+2A73;EQUALS SIGN ABOVE TILDE OPERATOR;Sm;0;ON;;;;;Y;;;;;
+2A74;DOUBLE COLON EQUAL;Sm;0;ON;<compat> 003A 003A 003D;;;;Y;;;;;
+2A75;TWO CONSECUTIVE EQUALS SIGNS;Sm;0;ON;<compat> 003D 003D;;;;N;;;;;
+2A76;THREE CONSECUTIVE EQUALS SIGNS;Sm;0;ON;<compat> 003D 003D 003D;;;;N;;;;;
+2A77;EQUALS SIGN WITH TWO DOTS ABOVE AND TWO DOTS BELOW;Sm;0;ON;;;;;N;;;;;
+2A78;EQUIVALENT WITH FOUR DOTS ABOVE;Sm;0;ON;;;;;N;;;;;
+2A79;LESS-THAN WITH CIRCLE INSIDE;Sm;0;ON;;;;;Y;;;;;
+2A7A;GREATER-THAN WITH CIRCLE INSIDE;Sm;0;ON;;;;;Y;;;;;
+2A7B;LESS-THAN WITH QUESTION MARK ABOVE;Sm;0;ON;;;;;Y;;;;;
+2A7C;GREATER-THAN WITH QUESTION MARK ABOVE;Sm;0;ON;;;;;Y;;;;;
+2A7D;LESS-THAN OR SLANTED EQUAL TO;Sm;0;ON;;;;;Y;;;;;
+2A7E;GREATER-THAN OR SLANTED EQUAL TO;Sm;0;ON;;;;;Y;;;;;
+2A7F;LESS-THAN OR SLANTED EQUAL TO WITH DOT INSIDE;Sm;0;ON;;;;;Y;;;;;
+2A80;GREATER-THAN OR SLANTED EQUAL TO WITH DOT INSIDE;Sm;0;ON;;;;;Y;;;;;
+2A81;LESS-THAN OR SLANTED EQUAL TO WITH DOT ABOVE;Sm;0;ON;;;;;Y;;;;;
+2A82;GREATER-THAN OR SLANTED EQUAL TO WITH DOT ABOVE;Sm;0;ON;;;;;Y;;;;;
+2A83;LESS-THAN OR SLANTED EQUAL TO WITH DOT ABOVE RIGHT;Sm;0;ON;;;;;Y;;;;;
+2A84;GREATER-THAN OR SLANTED EQUAL TO WITH DOT ABOVE LEFT;Sm;0;ON;;;;;Y;;;;;
+2A85;LESS-THAN OR APPROXIMATE;Sm;0;ON;;;;;Y;;;;;
+2A86;GREATER-THAN OR APPROXIMATE;Sm;0;ON;;;;;Y;;;;;
+2A87;LESS-THAN AND SINGLE-LINE NOT EQUAL TO;Sm;0;ON;;;;;Y;;;;;
+2A88;GREATER-THAN AND SINGLE-LINE NOT EQUAL TO;Sm;0;ON;;;;;Y;;;;;
+2A89;LESS-THAN AND NOT APPROXIMATE;Sm;0;ON;;;;;Y;;;;;
+2A8A;GREATER-THAN AND NOT APPROXIMATE;Sm;0;ON;;;;;Y;;;;;
+2A8B;LESS-THAN ABOVE DOUBLE-LINE EQUAL ABOVE GREATER-THAN;Sm;0;ON;;;;;Y;;;;;
+2A8C;GREATER-THAN ABOVE DOUBLE-LINE EQUAL ABOVE LESS-THAN;Sm;0;ON;;;;;Y;;;;;
+2A8D;LESS-THAN ABOVE SIMILAR OR EQUAL;Sm;0;ON;;;;;Y;;;;;
+2A8E;GREATER-THAN ABOVE SIMILAR OR EQUAL;Sm;0;ON;;;;;Y;;;;;
+2A8F;LESS-THAN ABOVE SIMILAR ABOVE GREATER-THAN;Sm;0;ON;;;;;Y;;;;;
+2A90;GREATER-THAN ABOVE SIMILAR ABOVE LESS-THAN;Sm;0;ON;;;;;Y;;;;;
+2A91;LESS-THAN ABOVE GREATER-THAN ABOVE DOUBLE-LINE EQUAL;Sm;0;ON;;;;;Y;;;;;
+2A92;GREATER-THAN ABOVE LESS-THAN ABOVE DOUBLE-LINE EQUAL;Sm;0;ON;;;;;Y;;;;;
+2A93;LESS-THAN ABOVE SLANTED EQUAL ABOVE GREATER-THAN ABOVE SLANTED EQUAL;Sm;0;ON;;;;;Y;;;;;
+2A94;GREATER-THAN ABOVE SLANTED EQUAL ABOVE LESS-THAN ABOVE SLANTED EQUAL;Sm;0;ON;;;;;Y;;;;;
+2A95;SLANTED EQUAL TO OR LESS-THAN;Sm;0;ON;;;;;Y;;;;;
+2A96;SLANTED EQUAL TO OR GREATER-THAN;Sm;0;ON;;;;;Y;;;;;
+2A97;SLANTED EQUAL TO OR LESS-THAN WITH DOT INSIDE;Sm;0;ON;;;;;Y;;;;;
+2A98;SLANTED EQUAL TO OR GREATER-THAN WITH DOT INSIDE;Sm;0;ON;;;;;Y;;;;;
+2A99;DOUBLE-LINE EQUAL TO OR LESS-THAN;Sm;0;ON;;;;;Y;;;;;
+2A9A;DOUBLE-LINE EQUAL TO OR GREATER-THAN;Sm;0;ON;;;;;Y;;;;;
+2A9B;DOUBLE-LINE SLANTED EQUAL TO OR LESS-THAN;Sm;0;ON;;;;;Y;;;;;
+2A9C;DOUBLE-LINE SLANTED EQUAL TO OR GREATER-THAN;Sm;0;ON;;;;;Y;;;;;
+2A9D;SIMILAR OR LESS-THAN;Sm;0;ON;;;;;Y;;;;;
+2A9E;SIMILAR OR GREATER-THAN;Sm;0;ON;;;;;Y;;;;;
+2A9F;SIMILAR ABOVE LESS-THAN ABOVE EQUALS SIGN;Sm;0;ON;;;;;Y;;;;;
+2AA0;SIMILAR ABOVE GREATER-THAN ABOVE EQUALS SIGN;Sm;0;ON;;;;;Y;;;;;
+2AA1;DOUBLE NESTED LESS-THAN;Sm;0;ON;;;;;Y;;;;;
+2AA2;DOUBLE NESTED GREATER-THAN;Sm;0;ON;;;;;Y;;;;;
+2AA3;DOUBLE NESTED LESS-THAN WITH UNDERBAR;Sm;0;ON;;;;;Y;;;;;
+2AA4;GREATER-THAN OVERLAPPING LESS-THAN;Sm;0;ON;;;;;N;;;;;
+2AA5;GREATER-THAN BESIDE LESS-THAN;Sm;0;ON;;;;;N;;;;;
+2AA6;LESS-THAN CLOSED BY CURVE;Sm;0;ON;;;;;Y;;;;;
+2AA7;GREATER-THAN CLOSED BY CURVE;Sm;0;ON;;;;;Y;;;;;
+2AA8;LESS-THAN CLOSED BY CURVE ABOVE SLANTED EQUAL;Sm;0;ON;;;;;Y;;;;;
+2AA9;GREATER-THAN CLOSED BY CURVE ABOVE SLANTED EQUAL;Sm;0;ON;;;;;Y;;;;;
+2AAA;SMALLER THAN;Sm;0;ON;;;;;Y;;;;;
+2AAB;LARGER THAN;Sm;0;ON;;;;;Y;;;;;
+2AAC;SMALLER THAN OR EQUAL TO;Sm;0;ON;;;;;Y;;;;;
+2AAD;LARGER THAN OR EQUAL TO;Sm;0;ON;;;;;Y;;;;;
+2AAE;EQUALS SIGN WITH BUMPY ABOVE;Sm;0;ON;;;;;N;;;;;
+2AAF;PRECEDES ABOVE SINGLE-LINE EQUALS SIGN;Sm;0;ON;;;;;Y;;;;;
+2AB0;SUCCEEDS ABOVE SINGLE-LINE EQUALS SIGN;Sm;0;ON;;;;;Y;;;;;
+2AB1;PRECEDES ABOVE SINGLE-LINE NOT EQUAL TO;Sm;0;ON;;;;;Y;;;;;
+2AB2;SUCCEEDS ABOVE SINGLE-LINE NOT EQUAL TO;Sm;0;ON;;;;;Y;;;;;
+2AB3;PRECEDES ABOVE EQUALS SIGN;Sm;0;ON;;;;;Y;;;;;
+2AB4;SUCCEEDS ABOVE EQUALS SIGN;Sm;0;ON;;;;;Y;;;;;
+2AB5;PRECEDES ABOVE NOT EQUAL TO;Sm;0;ON;;;;;Y;;;;;
+2AB6;SUCCEEDS ABOVE NOT EQUAL TO;Sm;0;ON;;;;;Y;;;;;
+2AB7;PRECEDES ABOVE ALMOST EQUAL TO;Sm;0;ON;;;;;Y;;;;;
+2AB8;SUCCEEDS ABOVE ALMOST EQUAL TO;Sm;0;ON;;;;;Y;;;;;
+2AB9;PRECEDES ABOVE NOT ALMOST EQUAL TO;Sm;0;ON;;;;;Y;;;;;
+2ABA;SUCCEEDS ABOVE NOT ALMOST EQUAL TO;Sm;0;ON;;;;;Y;;;;;
+2ABB;DOUBLE PRECEDES;Sm;0;ON;;;;;Y;;;;;
+2ABC;DOUBLE SUCCEEDS;Sm;0;ON;;;;;Y;;;;;
+2ABD;SUBSET WITH DOT;Sm;0;ON;;;;;Y;;;;;
+2ABE;SUPERSET WITH DOT;Sm;0;ON;;;;;Y;;;;;
+2ABF;SUBSET WITH PLUS SIGN BELOW;Sm;0;ON;;;;;Y;;;;;
+2AC0;SUPERSET WITH PLUS SIGN BELOW;Sm;0;ON;;;;;Y;;;;;
+2AC1;SUBSET WITH MULTIPLICATION SIGN BELOW;Sm;0;ON;;;;;Y;;;;;
+2AC2;SUPERSET WITH MULTIPLICATION SIGN BELOW;Sm;0;ON;;;;;Y;;;;;
+2AC3;SUBSET OF OR EQUAL TO WITH DOT ABOVE;Sm;0;ON;;;;;Y;;;;;
+2AC4;SUPERSET OF OR EQUAL TO WITH DOT ABOVE;Sm;0;ON;;;;;Y;;;;;
+2AC5;SUBSET OF ABOVE EQUALS SIGN;Sm;0;ON;;;;;Y;;;;;
+2AC6;SUPERSET OF ABOVE EQUALS SIGN;Sm;0;ON;;;;;Y;;;;;
+2AC7;SUBSET OF ABOVE TILDE OPERATOR;Sm;0;ON;;;;;Y;;;;;
+2AC8;SUPERSET OF ABOVE TILDE OPERATOR;Sm;0;ON;;;;;Y;;;;;
+2AC9;SUBSET OF ABOVE ALMOST EQUAL TO;Sm;0;ON;;;;;Y;;;;;
+2ACA;SUPERSET OF ABOVE ALMOST EQUAL TO;Sm;0;ON;;;;;Y;;;;;
+2ACB;SUBSET OF ABOVE NOT EQUAL TO;Sm;0;ON;;;;;Y;;;;;
+2ACC;SUPERSET OF ABOVE NOT EQUAL TO;Sm;0;ON;;;;;Y;;;;;
+2ACD;SQUARE LEFT OPEN BOX OPERATOR;Sm;0;ON;;;;;Y;;;;;
+2ACE;SQUARE RIGHT OPEN BOX OPERATOR;Sm;0;ON;;;;;Y;;;;;
+2ACF;CLOSED SUBSET;Sm;0;ON;;;;;Y;;;;;
+2AD0;CLOSED SUPERSET;Sm;0;ON;;;;;Y;;;;;
+2AD1;CLOSED SUBSET OR EQUAL TO;Sm;0;ON;;;;;Y;;;;;
+2AD2;CLOSED SUPERSET OR EQUAL TO;Sm;0;ON;;;;;Y;;;;;
+2AD3;SUBSET ABOVE SUPERSET;Sm;0;ON;;;;;Y;;;;;
+2AD4;SUPERSET ABOVE SUBSET;Sm;0;ON;;;;;Y;;;;;
+2AD5;SUBSET ABOVE SUBSET;Sm;0;ON;;;;;Y;;;;;
+2AD6;SUPERSET ABOVE SUPERSET;Sm;0;ON;;;;;Y;;;;;
+2AD7;SUPERSET BESIDE SUBSET;Sm;0;ON;;;;;N;;;;;
+2AD8;SUPERSET BESIDE AND JOINED BY DASH WITH SUBSET;Sm;0;ON;;;;;N;;;;;
+2AD9;ELEMENT OF OPENING DOWNWARDS;Sm;0;ON;;;;;N;;;;;
+2ADA;PITCHFORK WITH TEE TOP;Sm;0;ON;;;;;N;;;;;
+2ADB;TRANSVERSAL INTERSECTION;Sm;0;ON;;;;;N;;;;;
+2ADC;FORKING;Sm;0;ON;2ADD 0338;;;;Y;;;;;
+2ADD;NONFORKING;Sm;0;ON;;;;;N;;;;;
+2ADE;SHORT LEFT TACK;Sm;0;ON;;;;;Y;;;;;
+2ADF;SHORT DOWN TACK;Sm;0;ON;;;;;N;;;;;
+2AE0;SHORT UP TACK;Sm;0;ON;;;;;N;;;;;
+2AE1;PERPENDICULAR WITH S;Sm;0;ON;;;;;N;;;;;
+2AE2;VERTICAL BAR TRIPLE RIGHT TURNSTILE;Sm;0;ON;;;;;Y;;;;;
+2AE3;DOUBLE VERTICAL BAR LEFT TURNSTILE;Sm;0;ON;;;;;Y;;;;;
+2AE4;VERTICAL BAR DOUBLE LEFT TURNSTILE;Sm;0;ON;;;;;Y;;;;;
+2AE5;DOUBLE VERTICAL BAR DOUBLE LEFT TURNSTILE;Sm;0;ON;;;;;Y;;;;;
+2AE6;LONG DASH FROM LEFT MEMBER OF DOUBLE VERTICAL;Sm;0;ON;;;;;Y;;;;;
+2AE7;SHORT DOWN TACK WITH OVERBAR;Sm;0;ON;;;;;N;;;;;
+2AE8;SHORT UP TACK WITH UNDERBAR;Sm;0;ON;;;;;N;;;;;
+2AE9;SHORT UP TACK ABOVE SHORT DOWN TACK;Sm;0;ON;;;;;N;;;;;
+2AEA;DOUBLE DOWN TACK;Sm;0;ON;;;;;N;;;;;
+2AEB;DOUBLE UP TACK;Sm;0;ON;;;;;N;;;;;
+2AEC;DOUBLE STROKE NOT SIGN;Sm;0;ON;;;;;Y;;;;;
+2AED;REVERSED DOUBLE STROKE NOT SIGN;Sm;0;ON;;;;;Y;;;;;
+2AEE;DOES NOT DIVIDE WITH REVERSED NEGATION SLASH;Sm;0;ON;;;;;Y;;;;;
+2AEF;VERTICAL LINE WITH CIRCLE ABOVE;Sm;0;ON;;;;;N;;;;;
+2AF0;VERTICAL LINE WITH CIRCLE BELOW;Sm;0;ON;;;;;N;;;;;
+2AF1;DOWN TACK WITH CIRCLE BELOW;Sm;0;ON;;;;;N;;;;;
+2AF2;PARALLEL WITH HORIZONTAL STROKE;Sm;0;ON;;;;;N;;;;;
+2AF3;PARALLEL WITH TILDE OPERATOR;Sm;0;ON;;;;;Y;;;;;
+2AF4;TRIPLE VERTICAL BAR BINARY RELATION;Sm;0;ON;;;;;N;;;;;
+2AF5;TRIPLE VERTICAL BAR WITH HORIZONTAL STROKE;Sm;0;ON;;;;;N;;;;;
+2AF6;TRIPLE COLON OPERATOR;Sm;0;ON;;;;;N;;;;;
+2AF7;TRIPLE NESTED LESS-THAN;Sm;0;ON;;;;;Y;;;;;
+2AF8;TRIPLE NESTED GREATER-THAN;Sm;0;ON;;;;;Y;;;;;
+2AF9;DOUBLE-LINE SLANTED LESS-THAN OR EQUAL TO;Sm;0;ON;;;;;Y;;;;;
+2AFA;DOUBLE-LINE SLANTED GREATER-THAN OR EQUAL TO;Sm;0;ON;;;;;Y;;;;;
+2AFB;TRIPLE SOLIDUS BINARY RELATION;Sm;0;ON;;;;;Y;;;;;
+2AFC;LARGE TRIPLE VERTICAL BAR OPERATOR;Sm;0;ON;;;;;N;;;;;
+2AFD;DOUBLE SOLIDUS OPERATOR;Sm;0;ON;;;;;Y;;;;;
+2AFE;WHITE VERTICAL BAR;Sm;0;ON;;;;;N;;;;;
+2AFF;N-ARY WHITE VERTICAL BAR;Sm;0;ON;;;;;N;;;;;
+2B00;NORTH EAST WHITE ARROW;So;0;ON;;;;;N;;;;;
+2B01;NORTH WEST WHITE ARROW;So;0;ON;;;;;N;;;;;
+2B02;SOUTH EAST WHITE ARROW;So;0;ON;;;;;N;;;;;
+2B03;SOUTH WEST WHITE ARROW;So;0;ON;;;;;N;;;;;
+2B04;LEFT RIGHT WHITE ARROW;So;0;ON;;;;;N;;;;;
+2B05;LEFTWARDS BLACK ARROW;So;0;ON;;;;;N;;;;;
+2B06;UPWARDS BLACK ARROW;So;0;ON;;;;;N;;;;;
+2B07;DOWNWARDS BLACK ARROW;So;0;ON;;;;;N;;;;;
+2B08;NORTH EAST BLACK ARROW;So;0;ON;;;;;N;;;;;
+2B09;NORTH WEST BLACK ARROW;So;0;ON;;;;;N;;;;;
+2B0A;SOUTH EAST BLACK ARROW;So;0;ON;;;;;N;;;;;
+2B0B;SOUTH WEST BLACK ARROW;So;0;ON;;;;;N;;;;;
+2B0C;LEFT RIGHT BLACK ARROW;So;0;ON;;;;;N;;;;;
+2B0D;UP DOWN BLACK ARROW;So;0;ON;;;;;N;;;;;
+2B0E;RIGHTWARDS ARROW WITH TIP DOWNWARDS;So;0;ON;;;;;N;;;;;
+2B0F;RIGHTWARDS ARROW WITH TIP UPWARDS;So;0;ON;;;;;N;;;;;
+2B10;LEFTWARDS ARROW WITH TIP DOWNWARDS;So;0;ON;;;;;N;;;;;
+2B11;LEFTWARDS ARROW WITH TIP UPWARDS;So;0;ON;;;;;N;;;;;
+2B12;SQUARE WITH TOP HALF BLACK;So;0;ON;;;;;N;;;;;
+2B13;SQUARE WITH BOTTOM HALF BLACK;So;0;ON;;;;;N;;;;;
+2B14;SQUARE WITH UPPER RIGHT DIAGONAL HALF BLACK;So;0;ON;;;;;N;;;;;
+2B15;SQUARE WITH LOWER LEFT DIAGONAL HALF BLACK;So;0;ON;;;;;N;;;;;
+2B16;DIAMOND WITH LEFT HALF BLACK;So;0;ON;;;;;N;;;;;
+2B17;DIAMOND WITH RIGHT HALF BLACK;So;0;ON;;;;;N;;;;;
+2B18;DIAMOND WITH TOP HALF BLACK;So;0;ON;;;;;N;;;;;
+2B19;DIAMOND WITH BOTTOM HALF BLACK;So;0;ON;;;;;N;;;;;
+2B1A;DOTTED SQUARE;So;0;ON;;;;;N;;;;;
+2B1B;BLACK LARGE SQUARE;So;0;ON;;;;;N;;;;;
+2B1C;WHITE LARGE SQUARE;So;0;ON;;;;;N;;;;;
+2B1D;BLACK VERY SMALL SQUARE;So;0;ON;;;;;N;;;;;
+2B1E;WHITE VERY SMALL SQUARE;So;0;ON;;;;;N;;;;;
+2B1F;BLACK PENTAGON;So;0;ON;;;;;N;;;;;
+2B20;WHITE PENTAGON;So;0;ON;;;;;N;;;;;
+2B21;WHITE HEXAGON;So;0;ON;;;;;N;;;;;
+2B22;BLACK HEXAGON;So;0;ON;;;;;N;;;;;
+2B23;HORIZONTAL BLACK HEXAGON;So;0;ON;;;;;N;;;;;
+2B24;BLACK LARGE CIRCLE;So;0;ON;;;;;N;;;;;
+2B25;BLACK MEDIUM DIAMOND;So;0;ON;;;;;N;;;;;
+2B26;WHITE MEDIUM DIAMOND;So;0;ON;;;;;N;;;;;
+2B27;BLACK MEDIUM LOZENGE;So;0;ON;;;;;N;;;;;
+2B28;WHITE MEDIUM LOZENGE;So;0;ON;;;;;N;;;;;
+2B29;BLACK SMALL DIAMOND;So;0;ON;;;;;N;;;;;
+2B2A;BLACK SMALL LOZENGE;So;0;ON;;;;;N;;;;;
+2B2B;WHITE SMALL LOZENGE;So;0;ON;;;;;N;;;;;
+2B2C;BLACK HORIZONTAL ELLIPSE;So;0;ON;;;;;N;;;;;
+2B2D;WHITE HORIZONTAL ELLIPSE;So;0;ON;;;;;N;;;;;
+2B2E;BLACK VERTICAL ELLIPSE;So;0;ON;;;;;N;;;;;
+2B2F;WHITE VERTICAL ELLIPSE;So;0;ON;;;;;N;;;;;
+2B30;LEFT ARROW WITH SMALL CIRCLE;Sm;0;ON;;;;;N;;;;;
+2B31;THREE LEFTWARDS ARROWS;Sm;0;ON;;;;;N;;;;;
+2B32;LEFT ARROW WITH CIRCLED PLUS;Sm;0;ON;;;;;N;;;;;
+2B33;LONG LEFTWARDS SQUIGGLE ARROW;Sm;0;ON;;;;;N;;;;;
+2B34;LEFTWARDS TWO-HEADED ARROW WITH VERTICAL STROKE;Sm;0;ON;;;;;N;;;;;
+2B35;LEFTWARDS TWO-HEADED ARROW WITH DOUBLE VERTICAL STROKE;Sm;0;ON;;;;;N;;;;;
+2B36;LEFTWARDS TWO-HEADED ARROW FROM BAR;Sm;0;ON;;;;;N;;;;;
+2B37;LEFTWARDS TWO-HEADED TRIPLE DASH ARROW;Sm;0;ON;;;;;N;;;;;
+2B38;LEFTWARDS ARROW WITH DOTTED STEM;Sm;0;ON;;;;;N;;;;;
+2B39;LEFTWARDS ARROW WITH TAIL WITH VERTICAL STROKE;Sm;0;ON;;;;;N;;;;;
+2B3A;LEFTWARDS ARROW WITH TAIL WITH DOUBLE VERTICAL STROKE;Sm;0;ON;;;;;N;;;;;
+2B3B;LEFTWARDS TWO-HEADED ARROW WITH TAIL;Sm;0;ON;;;;;N;;;;;
+2B3C;LEFTWARDS TWO-HEADED ARROW WITH TAIL WITH VERTICAL STROKE;Sm;0;ON;;;;;N;;;;;
+2B3D;LEFTWARDS TWO-HEADED ARROW WITH TAIL WITH DOUBLE VERTICAL STROKE;Sm;0;ON;;;;;N;;;;;
+2B3E;LEFTWARDS ARROW THROUGH X;Sm;0;ON;;;;;N;;;;;
+2B3F;WAVE ARROW POINTING DIRECTLY LEFT;Sm;0;ON;;;;;N;;;;;
+2B40;EQUALS SIGN ABOVE LEFTWARDS ARROW;Sm;0;ON;;;;;N;;;;;
+2B41;REVERSE TILDE OPERATOR ABOVE LEFTWARDS ARROW;Sm;0;ON;;;;;N;;;;;
+2B42;LEFTWARDS ARROW ABOVE REVERSE ALMOST EQUAL TO;Sm;0;ON;;;;;N;;;;;
+2B43;RIGHTWARDS ARROW THROUGH GREATER-THAN;Sm;0;ON;;;;;N;;;;;
+2B44;RIGHTWARDS ARROW THROUGH SUPERSET;Sm;0;ON;;;;;N;;;;;
+2B45;LEFTWARDS QUADRUPLE ARROW;So;0;ON;;;;;N;;;;;
+2B46;RIGHTWARDS QUADRUPLE ARROW;So;0;ON;;;;;N;;;;;
+2B47;REVERSE TILDE OPERATOR ABOVE RIGHTWARDS ARROW;Sm;0;ON;;;;;N;;;;;
+2B48;RIGHTWARDS ARROW ABOVE REVERSE ALMOST EQUAL TO;Sm;0;ON;;;;;N;;;;;
+2B49;TILDE OPERATOR ABOVE LEFTWARDS ARROW;Sm;0;ON;;;;;N;;;;;
+2B4A;LEFTWARDS ARROW ABOVE ALMOST EQUAL TO;Sm;0;ON;;;;;N;;;;;
+2B4B;LEFTWARDS ARROW ABOVE REVERSE TILDE OPERATOR;Sm;0;ON;;;;;N;;;;;
+2B4C;RIGHTWARDS ARROW ABOVE REVERSE TILDE OPERATOR;Sm;0;ON;;;;;N;;;;;
+2B4D;DOWNWARDS TRIANGLE-HEADED ZIGZAG ARROW;So;0;ON;;;;;N;;;;;
+2B4E;SHORT SLANTED NORTH ARROW;So;0;ON;;;;;N;;;;;
+2B4F;SHORT BACKSLANTED SOUTH ARROW;So;0;ON;;;;;N;;;;;
+2B50;WHITE MEDIUM STAR;So;0;ON;;;;;N;;;;;
+2B51;BLACK SMALL STAR;So;0;ON;;;;;N;;;;;
+2B52;WHITE SMALL STAR;So;0;ON;;;;;N;;;;;
+2B53;BLACK RIGHT-POINTING PENTAGON;So;0;ON;;;;;N;;;;;
+2B54;WHITE RIGHT-POINTING PENTAGON;So;0;ON;;;;;N;;;;;
+2B55;HEAVY LARGE CIRCLE;So;0;ON;;;;;N;;;;;
+2B56;HEAVY OVAL WITH OVAL INSIDE;So;0;ON;;;;;N;;;;;
+2B57;HEAVY CIRCLE WITH CIRCLE INSIDE;So;0;ON;;;;;N;;;;;
+2B58;HEAVY CIRCLE;So;0;ON;;;;;N;;;;;
+2B59;HEAVY CIRCLED SALTIRE;So;0;ON;;;;;N;;;;;
+2B5A;SLANTED NORTH ARROW WITH HOOKED HEAD;So;0;ON;;;;;N;;;;;
+2B5B;BACKSLANTED SOUTH ARROW WITH HOOKED TAIL;So;0;ON;;;;;N;;;;;
+2B5C;SLANTED NORTH ARROW WITH HORIZONTAL TAIL;So;0;ON;;;;;N;;;;;
+2B5D;BACKSLANTED SOUTH ARROW WITH HORIZONTAL TAIL;So;0;ON;;;;;N;;;;;
+2B5E;BENT ARROW POINTING DOWNWARDS THEN NORTH EAST;So;0;ON;;;;;N;;;;;
+2B5F;SHORT BENT ARROW POINTING DOWNWARDS THEN NORTH EAST;So;0;ON;;;;;N;;;;;
+2B60;LEFTWARDS TRIANGLE-HEADED ARROW;So;0;ON;;;;;N;;;;;
+2B61;UPWARDS TRIANGLE-HEADED ARROW;So;0;ON;;;;;N;;;;;
+2B62;RIGHTWARDS TRIANGLE-HEADED ARROW;So;0;ON;;;;;N;;;;;
+2B63;DOWNWARDS TRIANGLE-HEADED ARROW;So;0;ON;;;;;N;;;;;
+2B64;LEFT RIGHT TRIANGLE-HEADED ARROW;So;0;ON;;;;;N;;;;;
+2B65;UP DOWN TRIANGLE-HEADED ARROW;So;0;ON;;;;;N;;;;;
+2B66;NORTH WEST TRIANGLE-HEADED ARROW;So;0;ON;;;;;N;;;;;
+2B67;NORTH EAST TRIANGLE-HEADED ARROW;So;0;ON;;;;;N;;;;;
+2B68;SOUTH EAST TRIANGLE-HEADED ARROW;So;0;ON;;;;;N;;;;;
+2B69;SOUTH WEST TRIANGLE-HEADED ARROW;So;0;ON;;;;;N;;;;;
+2B6A;LEFTWARDS TRIANGLE-HEADED DASHED ARROW;So;0;ON;;;;;N;;;;;
+2B6B;UPWARDS TRIANGLE-HEADED DASHED ARROW;So;0;ON;;;;;N;;;;;
+2B6C;RIGHTWARDS TRIANGLE-HEADED DASHED ARROW;So;0;ON;;;;;N;;;;;
+2B6D;DOWNWARDS TRIANGLE-HEADED DASHED ARROW;So;0;ON;;;;;N;;;;;
+2B6E;CLOCKWISE TRIANGLE-HEADED OPEN CIRCLE ARROW;So;0;ON;;;;;N;;;;;
+2B6F;ANTICLOCKWISE TRIANGLE-HEADED OPEN CIRCLE ARROW;So;0;ON;;;;;N;;;;;
+2B70;LEFTWARDS TRIANGLE-HEADED ARROW TO BAR;So;0;ON;;;;;N;;;;;
+2B71;UPWARDS TRIANGLE-HEADED ARROW TO BAR;So;0;ON;;;;;N;;;;;
+2B72;RIGHTWARDS TRIANGLE-HEADED ARROW TO BAR;So;0;ON;;;;;N;;;;;
+2B73;DOWNWARDS TRIANGLE-HEADED ARROW TO BAR;So;0;ON;;;;;N;;;;;
+2B76;NORTH WEST TRIANGLE-HEADED ARROW TO BAR;So;0;ON;;;;;N;;;;;
+2B77;NORTH EAST TRIANGLE-HEADED ARROW TO BAR;So;0;ON;;;;;N;;;;;
+2B78;SOUTH EAST TRIANGLE-HEADED ARROW TO BAR;So;0;ON;;;;;N;;;;;
+2B79;SOUTH WEST TRIANGLE-HEADED ARROW TO BAR;So;0;ON;;;;;N;;;;;
+2B7A;LEFTWARDS TRIANGLE-HEADED ARROW WITH DOUBLE HORIZONTAL STROKE;So;0;ON;;;;;N;;;;;
+2B7B;UPWARDS TRIANGLE-HEADED ARROW WITH DOUBLE HORIZONTAL STROKE;So;0;ON;;;;;N;;;;;
+2B7C;RIGHTWARDS TRIANGLE-HEADED ARROW WITH DOUBLE HORIZONTAL STROKE;So;0;ON;;;;;N;;;;;
+2B7D;DOWNWARDS TRIANGLE-HEADED ARROW WITH DOUBLE HORIZONTAL STROKE;So;0;ON;;;;;N;;;;;
+2B7E;HORIZONTAL TAB KEY;So;0;ON;;;;;N;;;;;
+2B7F;VERTICAL TAB KEY;So;0;ON;;;;;N;;;;;
+2B80;LEFTWARDS TRIANGLE-HEADED ARROW OVER RIGHTWARDS TRIANGLE-HEADED ARROW;So;0;ON;;;;;N;;;;;
+2B81;UPWARDS TRIANGLE-HEADED ARROW LEFTWARDS OF DOWNWARDS TRIANGLE-HEADED ARROW;So;0;ON;;;;;N;;;;;
+2B82;RIGHTWARDS TRIANGLE-HEADED ARROW OVER LEFTWARDS TRIANGLE-HEADED ARROW;So;0;ON;;;;;N;;;;;
+2B83;DOWNWARDS TRIANGLE-HEADED ARROW LEFTWARDS OF UPWARDS TRIANGLE-HEADED ARROW;So;0;ON;;;;;N;;;;;
+2B84;LEFTWARDS TRIANGLE-HEADED PAIRED ARROWS;So;0;ON;;;;;N;;;;;
+2B85;UPWARDS TRIANGLE-HEADED PAIRED ARROWS;So;0;ON;;;;;N;;;;;
+2B86;RIGHTWARDS TRIANGLE-HEADED PAIRED ARROWS;So;0;ON;;;;;N;;;;;
+2B87;DOWNWARDS TRIANGLE-HEADED PAIRED ARROWS;So;0;ON;;;;;N;;;;;
+2B88;LEFTWARDS BLACK CIRCLED WHITE ARROW;So;0;ON;;;;;N;;;;;
+2B89;UPWARDS BLACK CIRCLED WHITE ARROW;So;0;ON;;;;;N;;;;;
+2B8A;RIGHTWARDS BLACK CIRCLED WHITE ARROW;So;0;ON;;;;;N;;;;;
+2B8B;DOWNWARDS BLACK CIRCLED WHITE ARROW;So;0;ON;;;;;N;;;;;
+2B8C;ANTICLOCKWISE TRIANGLE-HEADED RIGHT U-SHAPED ARROW;So;0;ON;;;;;N;;;;;
+2B8D;ANTICLOCKWISE TRIANGLE-HEADED BOTTOM U-SHAPED ARROW;So;0;ON;;;;;N;;;;;
+2B8E;ANTICLOCKWISE TRIANGLE-HEADED LEFT U-SHAPED ARROW;So;0;ON;;;;;N;;;;;
+2B8F;ANTICLOCKWISE TRIANGLE-HEADED TOP U-SHAPED ARROW;So;0;ON;;;;;N;;;;;
+2B90;RETURN LEFT;So;0;ON;;;;;N;;;;;
+2B91;RETURN RIGHT;So;0;ON;;;;;N;;;;;
+2B92;NEWLINE LEFT;So;0;ON;;;;;N;;;;;
+2B93;NEWLINE RIGHT;So;0;ON;;;;;N;;;;;
+2B94;FOUR CORNER ARROWS CIRCLING ANTICLOCKWISE;So;0;ON;;;;;N;;;;;
+2B95;RIGHTWARDS BLACK ARROW;So;0;ON;;;;;N;;;;;
+2B98;THREE-D TOP-LIGHTED LEFTWARDS EQUILATERAL ARROWHEAD;So;0;ON;;;;;N;;;;;
+2B99;THREE-D RIGHT-LIGHTED UPWARDS EQUILATERAL ARROWHEAD;So;0;ON;;;;;N;;;;;
+2B9A;THREE-D TOP-LIGHTED RIGHTWARDS EQUILATERAL ARROWHEAD;So;0;ON;;;;;N;;;;;
+2B9B;THREE-D LEFT-LIGHTED DOWNWARDS EQUILATERAL ARROWHEAD;So;0;ON;;;;;N;;;;;
+2B9C;BLACK LEFTWARDS EQUILATERAL ARROWHEAD;So;0;ON;;;;;N;;;;;
+2B9D;BLACK UPWARDS EQUILATERAL ARROWHEAD;So;0;ON;;;;;N;;;;;
+2B9E;BLACK RIGHTWARDS EQUILATERAL ARROWHEAD;So;0;ON;;;;;N;;;;;
+2B9F;BLACK DOWNWARDS EQUILATERAL ARROWHEAD;So;0;ON;;;;;N;;;;;
+2BA0;DOWNWARDS TRIANGLE-HEADED ARROW WITH LONG TIP LEFTWARDS;So;0;ON;;;;;N;;;;;
+2BA1;DOWNWARDS TRIANGLE-HEADED ARROW WITH LONG TIP RIGHTWARDS;So;0;ON;;;;;N;;;;;
+2BA2;UPWARDS TRIANGLE-HEADED ARROW WITH LONG TIP LEFTWARDS;So;0;ON;;;;;N;;;;;
+2BA3;UPWARDS TRIANGLE-HEADED ARROW WITH LONG TIP RIGHTWARDS;So;0;ON;;;;;N;;;;;
+2BA4;LEFTWARDS TRIANGLE-HEADED ARROW WITH LONG TIP UPWARDS;So;0;ON;;;;;N;;;;;
+2BA5;RIGHTWARDS TRIANGLE-HEADED ARROW WITH LONG TIP UPWARDS;So;0;ON;;;;;N;;;;;
+2BA6;LEFTWARDS TRIANGLE-HEADED ARROW WITH LONG TIP DOWNWARDS;So;0;ON;;;;;N;;;;;
+2BA7;RIGHTWARDS TRIANGLE-HEADED ARROW WITH LONG TIP DOWNWARDS;So;0;ON;;;;;N;;;;;
+2BA8;BLACK CURVED DOWNWARDS AND LEFTWARDS ARROW;So;0;ON;;;;;N;;;;;
+2BA9;BLACK CURVED DOWNWARDS AND RIGHTWARDS ARROW;So;0;ON;;;;;N;;;;;
+2BAA;BLACK CURVED UPWARDS AND LEFTWARDS ARROW;So;0;ON;;;;;N;;;;;
+2BAB;BLACK CURVED UPWARDS AND RIGHTWARDS ARROW;So;0;ON;;;;;N;;;;;
+2BAC;BLACK CURVED LEFTWARDS AND UPWARDS ARROW;So;0;ON;;;;;N;;;;;
+2BAD;BLACK CURVED RIGHTWARDS AND UPWARDS ARROW;So;0;ON;;;;;N;;;;;
+2BAE;BLACK CURVED LEFTWARDS AND DOWNWARDS ARROW;So;0;ON;;;;;N;;;;;
+2BAF;BLACK CURVED RIGHTWARDS AND DOWNWARDS ARROW;So;0;ON;;;;;N;;;;;
+2BB0;RIBBON ARROW DOWN LEFT;So;0;ON;;;;;N;;;;;
+2BB1;RIBBON ARROW DOWN RIGHT;So;0;ON;;;;;N;;;;;
+2BB2;RIBBON ARROW UP LEFT;So;0;ON;;;;;N;;;;;
+2BB3;RIBBON ARROW UP RIGHT;So;0;ON;;;;;N;;;;;
+2BB4;RIBBON ARROW LEFT UP;So;0;ON;;;;;N;;;;;
+2BB5;RIBBON ARROW RIGHT UP;So;0;ON;;;;;N;;;;;
+2BB6;RIBBON ARROW LEFT DOWN;So;0;ON;;;;;N;;;;;
+2BB7;RIBBON ARROW RIGHT DOWN;So;0;ON;;;;;N;;;;;
+2BB8;UPWARDS WHITE ARROW FROM BAR WITH HORIZONTAL BAR;So;0;ON;;;;;N;;;;;
+2BB9;UP ARROWHEAD IN A RECTANGLE BOX;So;0;ON;;;;;N;;;;;
+2BBD;BALLOT BOX WITH LIGHT X;So;0;ON;;;;;N;;;;;
+2BBE;CIRCLED X;So;0;ON;;;;;N;;;;;
+2BBF;CIRCLED BOLD X;So;0;ON;;;;;N;;;;;
+2BC0;BLACK SQUARE CENTRED;So;0;ON;;;;;N;;;;;
+2BC1;BLACK DIAMOND CENTRED;So;0;ON;;;;;N;;;;;
+2BC2;TURNED BLACK PENTAGON;So;0;ON;;;;;N;;;;;
+2BC3;HORIZONTAL BLACK OCTAGON;So;0;ON;;;;;N;;;;;
+2BC4;BLACK OCTAGON;So;0;ON;;;;;N;;;;;
+2BC5;BLACK MEDIUM UP-POINTING TRIANGLE CENTRED;So;0;ON;;;;;N;;;;;
+2BC6;BLACK MEDIUM DOWN-POINTING TRIANGLE CENTRED;So;0;ON;;;;;N;;;;;
+2BC7;BLACK MEDIUM LEFT-POINTING TRIANGLE CENTRED;So;0;ON;;;;;N;;;;;
+2BC8;BLACK MEDIUM RIGHT-POINTING TRIANGLE CENTRED;So;0;ON;;;;;N;;;;;
+2BCA;TOP HALF BLACK CIRCLE;So;0;ON;;;;;N;;;;;
+2BCB;BOTTOM HALF BLACK CIRCLE;So;0;ON;;;;;N;;;;;
+2BCC;LIGHT FOUR POINTED BLACK CUSP;So;0;ON;;;;;N;;;;;
+2BCD;ROTATED LIGHT FOUR POINTED BLACK CUSP;So;0;ON;;;;;N;;;;;
+2BCE;WHITE FOUR POINTED CUSP;So;0;ON;;;;;N;;;;;
+2BCF;ROTATED WHITE FOUR POINTED CUSP;So;0;ON;;;;;N;;;;;
+2BD0;SQUARE POSITION INDICATOR;So;0;ON;;;;;N;;;;;
+2BD1;UNCERTAINTY SIGN;So;0;ON;;;;;N;;;;;
+2BEC;LEFTWARDS TWO-HEADED ARROW WITH TRIANGLE ARROWHEADS;So;0;ON;;;;;N;;;;;
+2BED;UPWARDS TWO-HEADED ARROW WITH TRIANGLE ARROWHEADS;So;0;ON;;;;;N;;;;;
+2BEE;RIGHTWARDS TWO-HEADED ARROW WITH TRIANGLE ARROWHEADS;So;0;ON;;;;;N;;;;;
+2BEF;DOWNWARDS TWO-HEADED ARROW WITH TRIANGLE ARROWHEADS;So;0;ON;;;;;N;;;;;
+2C00;GLAGOLITIC CAPITAL LETTER AZU;Lu;0;L;;;;;N;;;;2C30;
+2C01;GLAGOLITIC CAPITAL LETTER BUKY;Lu;0;L;;;;;N;;;;2C31;
+2C02;GLAGOLITIC CAPITAL LETTER VEDE;Lu;0;L;;;;;N;;;;2C32;
+2C03;GLAGOLITIC CAPITAL LETTER GLAGOLI;Lu;0;L;;;;;N;;;;2C33;
+2C04;GLAGOLITIC CAPITAL LETTER DOBRO;Lu;0;L;;;;;N;;;;2C34;
+2C05;GLAGOLITIC CAPITAL LETTER YESTU;Lu;0;L;;;;;N;;;;2C35;
+2C06;GLAGOLITIC CAPITAL LETTER ZHIVETE;Lu;0;L;;;;;N;;;;2C36;
+2C07;GLAGOLITIC CAPITAL LETTER DZELO;Lu;0;L;;;;;N;;;;2C37;
+2C08;GLAGOLITIC CAPITAL LETTER ZEMLJA;Lu;0;L;;;;;N;;;;2C38;
+2C09;GLAGOLITIC CAPITAL LETTER IZHE;Lu;0;L;;;;;N;;;;2C39;
+2C0A;GLAGOLITIC CAPITAL LETTER INITIAL IZHE;Lu;0;L;;;;;N;;;;2C3A;
+2C0B;GLAGOLITIC CAPITAL LETTER I;Lu;0;L;;;;;N;;;;2C3B;
+2C0C;GLAGOLITIC CAPITAL LETTER DJERVI;Lu;0;L;;;;;N;;;;2C3C;
+2C0D;GLAGOLITIC CAPITAL LETTER KAKO;Lu;0;L;;;;;N;;;;2C3D;
+2C0E;GLAGOLITIC CAPITAL LETTER LJUDIJE;Lu;0;L;;;;;N;;;;2C3E;
+2C0F;GLAGOLITIC CAPITAL LETTER MYSLITE;Lu;0;L;;;;;N;;;;2C3F;
+2C10;GLAGOLITIC CAPITAL LETTER NASHI;Lu;0;L;;;;;N;;;;2C40;
+2C11;GLAGOLITIC CAPITAL LETTER ONU;Lu;0;L;;;;;N;;;;2C41;
+2C12;GLAGOLITIC CAPITAL LETTER POKOJI;Lu;0;L;;;;;N;;;;2C42;
+2C13;GLAGOLITIC CAPITAL LETTER RITSI;Lu;0;L;;;;;N;;;;2C43;
+2C14;GLAGOLITIC CAPITAL LETTER SLOVO;Lu;0;L;;;;;N;;;;2C44;
+2C15;GLAGOLITIC CAPITAL LETTER TVRIDO;Lu;0;L;;;;;N;;;;2C45;
+2C16;GLAGOLITIC CAPITAL LETTER UKU;Lu;0;L;;;;;N;;;;2C46;
+2C17;GLAGOLITIC CAPITAL LETTER FRITU;Lu;0;L;;;;;N;;;;2C47;
+2C18;GLAGOLITIC CAPITAL LETTER HERU;Lu;0;L;;;;;N;;;;2C48;
+2C19;GLAGOLITIC CAPITAL LETTER OTU;Lu;0;L;;;;;N;;;;2C49;
+2C1A;GLAGOLITIC CAPITAL LETTER PE;Lu;0;L;;;;;N;;;;2C4A;
+2C1B;GLAGOLITIC CAPITAL LETTER SHTA;Lu;0;L;;;;;N;;;;2C4B;
+2C1C;GLAGOLITIC CAPITAL LETTER TSI;Lu;0;L;;;;;N;;;;2C4C;
+2C1D;GLAGOLITIC CAPITAL LETTER CHRIVI;Lu;0;L;;;;;N;;;;2C4D;
+2C1E;GLAGOLITIC CAPITAL LETTER SHA;Lu;0;L;;;;;N;;;;2C4E;
+2C1F;GLAGOLITIC CAPITAL LETTER YERU;Lu;0;L;;;;;N;;;;2C4F;
+2C20;GLAGOLITIC CAPITAL LETTER YERI;Lu;0;L;;;;;N;;;;2C50;
+2C21;GLAGOLITIC CAPITAL LETTER YATI;Lu;0;L;;;;;N;;;;2C51;
+2C22;GLAGOLITIC CAPITAL LETTER SPIDERY HA;Lu;0;L;;;;;N;;;;2C52;
+2C23;GLAGOLITIC CAPITAL LETTER YU;Lu;0;L;;;;;N;;;;2C53;
+2C24;GLAGOLITIC CAPITAL LETTER SMALL YUS;Lu;0;L;;;;;N;;;;2C54;
+2C25;GLAGOLITIC CAPITAL LETTER SMALL YUS WITH TAIL;Lu;0;L;;;;;N;;;;2C55;
+2C26;GLAGOLITIC CAPITAL LETTER YO;Lu;0;L;;;;;N;;;;2C56;
+2C27;GLAGOLITIC CAPITAL LETTER IOTATED SMALL YUS;Lu;0;L;;;;;N;;;;2C57;
+2C28;GLAGOLITIC CAPITAL LETTER BIG YUS;Lu;0;L;;;;;N;;;;2C58;
+2C29;GLAGOLITIC CAPITAL LETTER IOTATED BIG YUS;Lu;0;L;;;;;N;;;;2C59;
+2C2A;GLAGOLITIC CAPITAL LETTER FITA;Lu;0;L;;;;;N;;;;2C5A;
+2C2B;GLAGOLITIC CAPITAL LETTER IZHITSA;Lu;0;L;;;;;N;;;;2C5B;
+2C2C;GLAGOLITIC CAPITAL LETTER SHTAPIC;Lu;0;L;;;;;N;;;;2C5C;
+2C2D;GLAGOLITIC CAPITAL LETTER TROKUTASTI A;Lu;0;L;;;;;N;;;;2C5D;
+2C2E;GLAGOLITIC CAPITAL LETTER LATINATE MYSLITE;Lu;0;L;;;;;N;;;;2C5E;
+2C30;GLAGOLITIC SMALL LETTER AZU;Ll;0;L;;;;;N;;;2C00;;2C00
+2C31;GLAGOLITIC SMALL LETTER BUKY;Ll;0;L;;;;;N;;;2C01;;2C01
+2C32;GLAGOLITIC SMALL LETTER VEDE;Ll;0;L;;;;;N;;;2C02;;2C02
+2C33;GLAGOLITIC SMALL LETTER GLAGOLI;Ll;0;L;;;;;N;;;2C03;;2C03
+2C34;GLAGOLITIC SMALL LETTER DOBRO;Ll;0;L;;;;;N;;;2C04;;2C04
+2C35;GLAGOLITIC SMALL LETTER YESTU;Ll;0;L;;;;;N;;;2C05;;2C05
+2C36;GLAGOLITIC SMALL LETTER ZHIVETE;Ll;0;L;;;;;N;;;2C06;;2C06
+2C37;GLAGOLITIC SMALL LETTER DZELO;Ll;0;L;;;;;N;;;2C07;;2C07
+2C38;GLAGOLITIC SMALL LETTER ZEMLJA;Ll;0;L;;;;;N;;;2C08;;2C08
+2C39;GLAGOLITIC SMALL LETTER IZHE;Ll;0;L;;;;;N;;;2C09;;2C09
+2C3A;GLAGOLITIC SMALL LETTER INITIAL IZHE;Ll;0;L;;;;;N;;;2C0A;;2C0A
+2C3B;GLAGOLITIC SMALL LETTER I;Ll;0;L;;;;;N;;;2C0B;;2C0B
+2C3C;GLAGOLITIC SMALL LETTER DJERVI;Ll;0;L;;;;;N;;;2C0C;;2C0C
+2C3D;GLAGOLITIC SMALL LETTER KAKO;Ll;0;L;;;;;N;;;2C0D;;2C0D
+2C3E;GLAGOLITIC SMALL LETTER LJUDIJE;Ll;0;L;;;;;N;;;2C0E;;2C0E
+2C3F;GLAGOLITIC SMALL LETTER MYSLITE;Ll;0;L;;;;;N;;;2C0F;;2C0F
+2C40;GLAGOLITIC SMALL LETTER NASHI;Ll;0;L;;;;;N;;;2C10;;2C10
+2C41;GLAGOLITIC SMALL LETTER ONU;Ll;0;L;;;;;N;;;2C11;;2C11
+2C42;GLAGOLITIC SMALL LETTER POKOJI;Ll;0;L;;;;;N;;;2C12;;2C12
+2C43;GLAGOLITIC SMALL LETTER RITSI;Ll;0;L;;;;;N;;;2C13;;2C13
+2C44;GLAGOLITIC SMALL LETTER SLOVO;Ll;0;L;;;;;N;;;2C14;;2C14
+2C45;GLAGOLITIC SMALL LETTER TVRIDO;Ll;0;L;;;;;N;;;2C15;;2C15
+2C46;GLAGOLITIC SMALL LETTER UKU;Ll;0;L;;;;;N;;;2C16;;2C16
+2C47;GLAGOLITIC SMALL LETTER FRITU;Ll;0;L;;;;;N;;;2C17;;2C17
+2C48;GLAGOLITIC SMALL LETTER HERU;Ll;0;L;;;;;N;;;2C18;;2C18
+2C49;GLAGOLITIC SMALL LETTER OTU;Ll;0;L;;;;;N;;;2C19;;2C19
+2C4A;GLAGOLITIC SMALL LETTER PE;Ll;0;L;;;;;N;;;2C1A;;2C1A
+2C4B;GLAGOLITIC SMALL LETTER SHTA;Ll;0;L;;;;;N;;;2C1B;;2C1B
+2C4C;GLAGOLITIC SMALL LETTER TSI;Ll;0;L;;;;;N;;;2C1C;;2C1C
+2C4D;GLAGOLITIC SMALL LETTER CHRIVI;Ll;0;L;;;;;N;;;2C1D;;2C1D
+2C4E;GLAGOLITIC SMALL LETTER SHA;Ll;0;L;;;;;N;;;2C1E;;2C1E
+2C4F;GLAGOLITIC SMALL LETTER YERU;Ll;0;L;;;;;N;;;2C1F;;2C1F
+2C50;GLAGOLITIC SMALL LETTER YERI;Ll;0;L;;;;;N;;;2C20;;2C20
+2C51;GLAGOLITIC SMALL LETTER YATI;Ll;0;L;;;;;N;;;2C21;;2C21
+2C52;GLAGOLITIC SMALL LETTER SPIDERY HA;Ll;0;L;;;;;N;;;2C22;;2C22
+2C53;GLAGOLITIC SMALL LETTER YU;Ll;0;L;;;;;N;;;2C23;;2C23
+2C54;GLAGOLITIC SMALL LETTER SMALL YUS;Ll;0;L;;;;;N;;;2C24;;2C24
+2C55;GLAGOLITIC SMALL LETTER SMALL YUS WITH TAIL;Ll;0;L;;;;;N;;;2C25;;2C25
+2C56;GLAGOLITIC SMALL LETTER YO;Ll;0;L;;;;;N;;;2C26;;2C26
+2C57;GLAGOLITIC SMALL LETTER IOTATED SMALL YUS;Ll;0;L;;;;;N;;;2C27;;2C27
+2C58;GLAGOLITIC SMALL LETTER BIG YUS;Ll;0;L;;;;;N;;;2C28;;2C28
+2C59;GLAGOLITIC SMALL LETTER IOTATED BIG YUS;Ll;0;L;;;;;N;;;2C29;;2C29
+2C5A;GLAGOLITIC SMALL LETTER FITA;Ll;0;L;;;;;N;;;2C2A;;2C2A
+2C5B;GLAGOLITIC SMALL LETTER IZHITSA;Ll;0;L;;;;;N;;;2C2B;;2C2B
+2C5C;GLAGOLITIC SMALL LETTER SHTAPIC;Ll;0;L;;;;;N;;;2C2C;;2C2C
+2C5D;GLAGOLITIC SMALL LETTER TROKUTASTI A;Ll;0;L;;;;;N;;;2C2D;;2C2D
+2C5E;GLAGOLITIC SMALL LETTER LATINATE MYSLITE;Ll;0;L;;;;;N;;;2C2E;;2C2E
+2C60;LATIN CAPITAL LETTER L WITH DOUBLE BAR;Lu;0;L;;;;;N;;;;2C61;
+2C61;LATIN SMALL LETTER L WITH DOUBLE BAR;Ll;0;L;;;;;N;;;2C60;;2C60
+2C62;LATIN CAPITAL LETTER L WITH MIDDLE TILDE;Lu;0;L;;;;;N;;;;026B;
+2C63;LATIN CAPITAL LETTER P WITH STROKE;Lu;0;L;;;;;N;;;;1D7D;
+2C64;LATIN CAPITAL LETTER R WITH TAIL;Lu;0;L;;;;;N;;;;027D;
+2C65;LATIN SMALL LETTER A WITH STROKE;Ll;0;L;;;;;N;;;023A;;023A
+2C66;LATIN SMALL LETTER T WITH DIAGONAL STROKE;Ll;0;L;;;;;N;;;023E;;023E
+2C67;LATIN CAPITAL LETTER H WITH DESCENDER;Lu;0;L;;;;;N;;;;2C68;
+2C68;LATIN SMALL LETTER H WITH DESCENDER;Ll;0;L;;;;;N;;;2C67;;2C67
+2C69;LATIN CAPITAL LETTER K WITH DESCENDER;Lu;0;L;;;;;N;;;;2C6A;
+2C6A;LATIN SMALL LETTER K WITH DESCENDER;Ll;0;L;;;;;N;;;2C69;;2C69
+2C6B;LATIN CAPITAL LETTER Z WITH DESCENDER;Lu;0;L;;;;;N;;;;2C6C;
+2C6C;LATIN SMALL LETTER Z WITH DESCENDER;Ll;0;L;;;;;N;;;2C6B;;2C6B
+2C6D;LATIN CAPITAL LETTER ALPHA;Lu;0;L;;;;;N;;;;0251;
+2C6E;LATIN CAPITAL LETTER M WITH HOOK;Lu;0;L;;;;;N;;;;0271;
+2C6F;LATIN CAPITAL LETTER TURNED A;Lu;0;L;;;;;N;;;;0250;
+2C70;LATIN CAPITAL LETTER TURNED ALPHA;Lu;0;L;;;;;N;;;;0252;
+2C71;LATIN SMALL LETTER V WITH RIGHT HOOK;Ll;0;L;;;;;N;;;;;
+2C72;LATIN CAPITAL LETTER W WITH HOOK;Lu;0;L;;;;;N;;;;2C73;
+2C73;LATIN SMALL LETTER W WITH HOOK;Ll;0;L;;;;;N;;;2C72;;2C72
+2C74;LATIN SMALL LETTER V WITH CURL;Ll;0;L;;;;;N;;;;;
+2C75;LATIN CAPITAL LETTER HALF H;Lu;0;L;;;;;N;;;;2C76;
+2C76;LATIN SMALL LETTER HALF H;Ll;0;L;;;;;N;;;2C75;;2C75
+2C77;LATIN SMALL LETTER TAILLESS PHI;Ll;0;L;;;;;N;;;;;
+2C78;LATIN SMALL LETTER E WITH NOTCH;Ll;0;L;;;;;N;;;;;
+2C79;LATIN SMALL LETTER TURNED R WITH TAIL;Ll;0;L;;;;;N;;;;;
+2C7A;LATIN SMALL LETTER O WITH LOW RING INSIDE;Ll;0;L;;;;;N;;;;;
+2C7B;LATIN LETTER SMALL CAPITAL TURNED E;Ll;0;L;;;;;N;;;;;
+2C7C;LATIN SUBSCRIPT SMALL LETTER J;Lm;0;L;<sub> 006A;;;;N;;;;;
+2C7D;MODIFIER LETTER CAPITAL V;Lm;0;L;<super> 0056;;;;N;;;;;
+2C7E;LATIN CAPITAL LETTER S WITH SWASH TAIL;Lu;0;L;;;;;N;;;;023F;
+2C7F;LATIN CAPITAL LETTER Z WITH SWASH TAIL;Lu;0;L;;;;;N;;;;0240;
+2C80;COPTIC CAPITAL LETTER ALFA;Lu;0;L;;;;;N;;;;2C81;
+2C81;COPTIC SMALL LETTER ALFA;Ll;0;L;;;;;N;;;2C80;;2C80
+2C82;COPTIC CAPITAL LETTER VIDA;Lu;0;L;;;;;N;;;;2C83;
+2C83;COPTIC SMALL LETTER VIDA;Ll;0;L;;;;;N;;;2C82;;2C82
+2C84;COPTIC CAPITAL LETTER GAMMA;Lu;0;L;;;;;N;;;;2C85;
+2C85;COPTIC SMALL LETTER GAMMA;Ll;0;L;;;;;N;;;2C84;;2C84
+2C86;COPTIC CAPITAL LETTER DALDA;Lu;0;L;;;;;N;;;;2C87;
+2C87;COPTIC SMALL LETTER DALDA;Ll;0;L;;;;;N;;;2C86;;2C86
+2C88;COPTIC CAPITAL LETTER EIE;Lu;0;L;;;;;N;;;;2C89;
+2C89;COPTIC SMALL LETTER EIE;Ll;0;L;;;;;N;;;2C88;;2C88
+2C8A;COPTIC CAPITAL LETTER SOU;Lu;0;L;;;;;N;;;;2C8B;
+2C8B;COPTIC SMALL LETTER SOU;Ll;0;L;;;;;N;;;2C8A;;2C8A
+2C8C;COPTIC CAPITAL LETTER ZATA;Lu;0;L;;;;;N;;;;2C8D;
+2C8D;COPTIC SMALL LETTER ZATA;Ll;0;L;;;;;N;;;2C8C;;2C8C
+2C8E;COPTIC CAPITAL LETTER HATE;Lu;0;L;;;;;N;;;;2C8F;
+2C8F;COPTIC SMALL LETTER HATE;Ll;0;L;;;;;N;;;2C8E;;2C8E
+2C90;COPTIC CAPITAL LETTER THETHE;Lu;0;L;;;;;N;;;;2C91;
+2C91;COPTIC SMALL LETTER THETHE;Ll;0;L;;;;;N;;;2C90;;2C90
+2C92;COPTIC CAPITAL LETTER IAUDA;Lu;0;L;;;;;N;;;;2C93;
+2C93;COPTIC SMALL LETTER IAUDA;Ll;0;L;;;;;N;;;2C92;;2C92
+2C94;COPTIC CAPITAL LETTER KAPA;Lu;0;L;;;;;N;;;;2C95;
+2C95;COPTIC SMALL LETTER KAPA;Ll;0;L;;;;;N;;;2C94;;2C94
+2C96;COPTIC CAPITAL LETTER LAULA;Lu;0;L;;;;;N;;;;2C97;
+2C97;COPTIC SMALL LETTER LAULA;Ll;0;L;;;;;N;;;2C96;;2C96
+2C98;COPTIC CAPITAL LETTER MI;Lu;0;L;;;;;N;;;;2C99;
+2C99;COPTIC SMALL LETTER MI;Ll;0;L;;;;;N;;;2C98;;2C98
+2C9A;COPTIC CAPITAL LETTER NI;Lu;0;L;;;;;N;;;;2C9B;
+2C9B;COPTIC SMALL LETTER NI;Ll;0;L;;;;;N;;;2C9A;;2C9A
+2C9C;COPTIC CAPITAL LETTER KSI;Lu;0;L;;;;;N;;;;2C9D;
+2C9D;COPTIC SMALL LETTER KSI;Ll;0;L;;;;;N;;;2C9C;;2C9C
+2C9E;COPTIC CAPITAL LETTER O;Lu;0;L;;;;;N;;;;2C9F;
+2C9F;COPTIC SMALL LETTER O;Ll;0;L;;;;;N;;;2C9E;;2C9E
+2CA0;COPTIC CAPITAL LETTER PI;Lu;0;L;;;;;N;;;;2CA1;
+2CA1;COPTIC SMALL LETTER PI;Ll;0;L;;;;;N;;;2CA0;;2CA0
+2CA2;COPTIC CAPITAL LETTER RO;Lu;0;L;;;;;N;;;;2CA3;
+2CA3;COPTIC SMALL LETTER RO;Ll;0;L;;;;;N;;;2CA2;;2CA2
+2CA4;COPTIC CAPITAL LETTER SIMA;Lu;0;L;;;;;N;;;;2CA5;
+2CA5;COPTIC SMALL LETTER SIMA;Ll;0;L;;;;;N;;;2CA4;;2CA4
+2CA6;COPTIC CAPITAL LETTER TAU;Lu;0;L;;;;;N;;;;2CA7;
+2CA7;COPTIC SMALL LETTER TAU;Ll;0;L;;;;;N;;;2CA6;;2CA6
+2CA8;COPTIC CAPITAL LETTER UA;Lu;0;L;;;;;N;;;;2CA9;
+2CA9;COPTIC SMALL LETTER UA;Ll;0;L;;;;;N;;;2CA8;;2CA8
+2CAA;COPTIC CAPITAL LETTER FI;Lu;0;L;;;;;N;;;;2CAB;
+2CAB;COPTIC SMALL LETTER FI;Ll;0;L;;;;;N;;;2CAA;;2CAA
+2CAC;COPTIC CAPITAL LETTER KHI;Lu;0;L;;;;;N;;;;2CAD;
+2CAD;COPTIC SMALL LETTER KHI;Ll;0;L;;;;;N;;;2CAC;;2CAC
+2CAE;COPTIC CAPITAL LETTER PSI;Lu;0;L;;;;;N;;;;2CAF;
+2CAF;COPTIC SMALL LETTER PSI;Ll;0;L;;;;;N;;;2CAE;;2CAE
+2CB0;COPTIC CAPITAL LETTER OOU;Lu;0;L;;;;;N;;;;2CB1;
+2CB1;COPTIC SMALL LETTER OOU;Ll;0;L;;;;;N;;;2CB0;;2CB0
+2CB2;COPTIC CAPITAL LETTER DIALECT-P ALEF;Lu;0;L;;;;;N;;;;2CB3;
+2CB3;COPTIC SMALL LETTER DIALECT-P ALEF;Ll;0;L;;;;;N;;;2CB2;;2CB2
+2CB4;COPTIC CAPITAL LETTER OLD COPTIC AIN;Lu;0;L;;;;;N;;;;2CB5;
+2CB5;COPTIC SMALL LETTER OLD COPTIC AIN;Ll;0;L;;;;;N;;;2CB4;;2CB4
+2CB6;COPTIC CAPITAL LETTER CRYPTOGRAMMIC EIE;Lu;0;L;;;;;N;;;;2CB7;
+2CB7;COPTIC SMALL LETTER CRYPTOGRAMMIC EIE;Ll;0;L;;;;;N;;;2CB6;;2CB6
+2CB8;COPTIC CAPITAL LETTER DIALECT-P KAPA;Lu;0;L;;;;;N;;;;2CB9;
+2CB9;COPTIC SMALL LETTER DIALECT-P KAPA;Ll;0;L;;;;;N;;;2CB8;;2CB8
+2CBA;COPTIC CAPITAL LETTER DIALECT-P NI;Lu;0;L;;;;;N;;;;2CBB;
+2CBB;COPTIC SMALL LETTER DIALECT-P NI;Ll;0;L;;;;;N;;;2CBA;;2CBA
+2CBC;COPTIC CAPITAL LETTER CRYPTOGRAMMIC NI;Lu;0;L;;;;;N;;;;2CBD;
+2CBD;COPTIC SMALL LETTER CRYPTOGRAMMIC NI;Ll;0;L;;;;;N;;;2CBC;;2CBC
+2CBE;COPTIC CAPITAL LETTER OLD COPTIC OOU;Lu;0;L;;;;;N;;;;2CBF;
+2CBF;COPTIC SMALL LETTER OLD COPTIC OOU;Ll;0;L;;;;;N;;;2CBE;;2CBE
+2CC0;COPTIC CAPITAL LETTER SAMPI;Lu;0;L;;;;;N;;;;2CC1;
+2CC1;COPTIC SMALL LETTER SAMPI;Ll;0;L;;;;;N;;;2CC0;;2CC0
+2CC2;COPTIC CAPITAL LETTER CROSSED SHEI;Lu;0;L;;;;;N;;;;2CC3;
+2CC3;COPTIC SMALL LETTER CROSSED SHEI;Ll;0;L;;;;;N;;;2CC2;;2CC2
+2CC4;COPTIC CAPITAL LETTER OLD COPTIC SHEI;Lu;0;L;;;;;N;;;;2CC5;
+2CC5;COPTIC SMALL LETTER OLD COPTIC SHEI;Ll;0;L;;;;;N;;;2CC4;;2CC4
+2CC6;COPTIC CAPITAL LETTER OLD COPTIC ESH;Lu;0;L;;;;;N;;;;2CC7;
+2CC7;COPTIC SMALL LETTER OLD COPTIC ESH;Ll;0;L;;;;;N;;;2CC6;;2CC6
+2CC8;COPTIC CAPITAL LETTER AKHMIMIC KHEI;Lu;0;L;;;;;N;;;;2CC9;
+2CC9;COPTIC SMALL LETTER AKHMIMIC KHEI;Ll;0;L;;;;;N;;;2CC8;;2CC8
+2CCA;COPTIC CAPITAL LETTER DIALECT-P HORI;Lu;0;L;;;;;N;;;;2CCB;
+2CCB;COPTIC SMALL LETTER DIALECT-P HORI;Ll;0;L;;;;;N;;;2CCA;;2CCA
+2CCC;COPTIC CAPITAL LETTER OLD COPTIC HORI;Lu;0;L;;;;;N;;;;2CCD;
+2CCD;COPTIC SMALL LETTER OLD COPTIC HORI;Ll;0;L;;;;;N;;;2CCC;;2CCC
+2CCE;COPTIC CAPITAL LETTER OLD COPTIC HA;Lu;0;L;;;;;N;;;;2CCF;
+2CCF;COPTIC SMALL LETTER OLD COPTIC HA;Ll;0;L;;;;;N;;;2CCE;;2CCE
+2CD0;COPTIC CAPITAL LETTER L-SHAPED HA;Lu;0;L;;;;;N;;;;2CD1;
+2CD1;COPTIC SMALL LETTER L-SHAPED HA;Ll;0;L;;;;;N;;;2CD0;;2CD0
+2CD2;COPTIC CAPITAL LETTER OLD COPTIC HEI;Lu;0;L;;;;;N;;;;2CD3;
+2CD3;COPTIC SMALL LETTER OLD COPTIC HEI;Ll;0;L;;;;;N;;;2CD2;;2CD2
+2CD4;COPTIC CAPITAL LETTER OLD COPTIC HAT;Lu;0;L;;;;;N;;;;2CD5;
+2CD5;COPTIC SMALL LETTER OLD COPTIC HAT;Ll;0;L;;;;;N;;;2CD4;;2CD4
+2CD6;COPTIC CAPITAL LETTER OLD COPTIC GANGIA;Lu;0;L;;;;;N;;;;2CD7;
+2CD7;COPTIC SMALL LETTER OLD COPTIC GANGIA;Ll;0;L;;;;;N;;;2CD6;;2CD6
+2CD8;COPTIC CAPITAL LETTER OLD COPTIC DJA;Lu;0;L;;;;;N;;;;2CD9;
+2CD9;COPTIC SMALL LETTER OLD COPTIC DJA;Ll;0;L;;;;;N;;;2CD8;;2CD8
+2CDA;COPTIC CAPITAL LETTER OLD COPTIC SHIMA;Lu;0;L;;;;;N;;;;2CDB;
+2CDB;COPTIC SMALL LETTER OLD COPTIC SHIMA;Ll;0;L;;;;;N;;;2CDA;;2CDA
+2CDC;COPTIC CAPITAL LETTER OLD NUBIAN SHIMA;Lu;0;L;;;;;N;;;;2CDD;
+2CDD;COPTIC SMALL LETTER OLD NUBIAN SHIMA;Ll;0;L;;;;;N;;;2CDC;;2CDC
+2CDE;COPTIC CAPITAL LETTER OLD NUBIAN NGI;Lu;0;L;;;;;N;;;;2CDF;
+2CDF;COPTIC SMALL LETTER OLD NUBIAN NGI;Ll;0;L;;;;;N;;;2CDE;;2CDE
+2CE0;COPTIC CAPITAL LETTER OLD NUBIAN NYI;Lu;0;L;;;;;N;;;;2CE1;
+2CE1;COPTIC SMALL LETTER OLD NUBIAN NYI;Ll;0;L;;;;;N;;;2CE0;;2CE0
+2CE2;COPTIC CAPITAL LETTER OLD NUBIAN WAU;Lu;0;L;;;;;N;;;;2CE3;
+2CE3;COPTIC SMALL LETTER OLD NUBIAN WAU;Ll;0;L;;;;;N;;;2CE2;;2CE2
+2CE4;COPTIC SYMBOL KAI;Ll;0;L;;;;;N;;;;;
+2CE5;COPTIC SYMBOL MI RO;So;0;ON;;;;;N;;;;;
+2CE6;COPTIC SYMBOL PI RO;So;0;ON;;;;;N;;;;;
+2CE7;COPTIC SYMBOL STAUROS;So;0;ON;;;;;N;;;;;
+2CE8;COPTIC SYMBOL TAU RO;So;0;ON;;;;;N;;;;;
+2CE9;COPTIC SYMBOL KHI RO;So;0;ON;;;;;N;;;;;
+2CEA;COPTIC SYMBOL SHIMA SIMA;So;0;ON;;;;;N;;;;;
+2CEB;COPTIC CAPITAL LETTER CRYPTOGRAMMIC SHEI;Lu;0;L;;;;;N;;;;2CEC;
+2CEC;COPTIC SMALL LETTER CRYPTOGRAMMIC SHEI;Ll;0;L;;;;;N;;;2CEB;;2CEB
+2CED;COPTIC CAPITAL LETTER CRYPTOGRAMMIC GANGIA;Lu;0;L;;;;;N;;;;2CEE;
+2CEE;COPTIC SMALL LETTER CRYPTOGRAMMIC GANGIA;Ll;0;L;;;;;N;;;2CED;;2CED
+2CEF;COPTIC COMBINING NI ABOVE;Mn;230;NSM;;;;;N;;;;;
+2CF0;COPTIC COMBINING SPIRITUS ASPER;Mn;230;NSM;;;;;N;;;;;
+2CF1;COPTIC COMBINING SPIRITUS LENIS;Mn;230;NSM;;;;;N;;;;;
+2CF2;COPTIC CAPITAL LETTER BOHAIRIC KHEI;Lu;0;L;;;;;N;;;;2CF3;
+2CF3;COPTIC SMALL LETTER BOHAIRIC KHEI;Ll;0;L;;;;;N;;;2CF2;;2CF2
+2CF9;COPTIC OLD NUBIAN FULL STOP;Po;0;ON;;;;;N;;;;;
+2CFA;COPTIC OLD NUBIAN DIRECT QUESTION MARK;Po;0;ON;;;;;N;;;;;
+2CFB;COPTIC OLD NUBIAN INDIRECT QUESTION MARK;Po;0;ON;;;;;N;;;;;
+2CFC;COPTIC OLD NUBIAN VERSE DIVIDER;Po;0;ON;;;;;N;;;;;
+2CFD;COPTIC FRACTION ONE HALF;No;0;ON;;;;1/2;N;;;;;
+2CFE;COPTIC FULL STOP;Po;0;ON;;;;;N;;;;;
+2CFF;COPTIC MORPHOLOGICAL DIVIDER;Po;0;ON;;;;;N;;;;;
+2D00;GEORGIAN SMALL LETTER AN;Ll;0;L;;;;;N;;;10A0;;10A0
+2D01;GEORGIAN SMALL LETTER BAN;Ll;0;L;;;;;N;;;10A1;;10A1
+2D02;GEORGIAN SMALL LETTER GAN;Ll;0;L;;;;;N;;;10A2;;10A2
+2D03;GEORGIAN SMALL LETTER DON;Ll;0;L;;;;;N;;;10A3;;10A3
+2D04;GEORGIAN SMALL LETTER EN;Ll;0;L;;;;;N;;;10A4;;10A4
+2D05;GEORGIAN SMALL LETTER VIN;Ll;0;L;;;;;N;;;10A5;;10A5
+2D06;GEORGIAN SMALL LETTER ZEN;Ll;0;L;;;;;N;;;10A6;;10A6
+2D07;GEORGIAN SMALL LETTER TAN;Ll;0;L;;;;;N;;;10A7;;10A7
+2D08;GEORGIAN SMALL LETTER IN;Ll;0;L;;;;;N;;;10A8;;10A8
+2D09;GEORGIAN SMALL LETTER KAN;Ll;0;L;;;;;N;;;10A9;;10A9
+2D0A;GEORGIAN SMALL LETTER LAS;Ll;0;L;;;;;N;;;10AA;;10AA
+2D0B;GEORGIAN SMALL LETTER MAN;Ll;0;L;;;;;N;;;10AB;;10AB
+2D0C;GEORGIAN SMALL LETTER NAR;Ll;0;L;;;;;N;;;10AC;;10AC
+2D0D;GEORGIAN SMALL LETTER ON;Ll;0;L;;;;;N;;;10AD;;10AD
+2D0E;GEORGIAN SMALL LETTER PAR;Ll;0;L;;;;;N;;;10AE;;10AE
+2D0F;GEORGIAN SMALL LETTER ZHAR;Ll;0;L;;;;;N;;;10AF;;10AF
+2D10;GEORGIAN SMALL LETTER RAE;Ll;0;L;;;;;N;;;10B0;;10B0
+2D11;GEORGIAN SMALL LETTER SAN;Ll;0;L;;;;;N;;;10B1;;10B1
+2D12;GEORGIAN SMALL LETTER TAR;Ll;0;L;;;;;N;;;10B2;;10B2
+2D13;GEORGIAN SMALL LETTER UN;Ll;0;L;;;;;N;;;10B3;;10B3
+2D14;GEORGIAN SMALL LETTER PHAR;Ll;0;L;;;;;N;;;10B4;;10B4
+2D15;GEORGIAN SMALL LETTER KHAR;Ll;0;L;;;;;N;;;10B5;;10B5
+2D16;GEORGIAN SMALL LETTER GHAN;Ll;0;L;;;;;N;;;10B6;;10B6
+2D17;GEORGIAN SMALL LETTER QAR;Ll;0;L;;;;;N;;;10B7;;10B7
+2D18;GEORGIAN SMALL LETTER SHIN;Ll;0;L;;;;;N;;;10B8;;10B8
+2D19;GEORGIAN SMALL LETTER CHIN;Ll;0;L;;;;;N;;;10B9;;10B9
+2D1A;GEORGIAN SMALL LETTER CAN;Ll;0;L;;;;;N;;;10BA;;10BA
+2D1B;GEORGIAN SMALL LETTER JIL;Ll;0;L;;;;;N;;;10BB;;10BB
+2D1C;GEORGIAN SMALL LETTER CIL;Ll;0;L;;;;;N;;;10BC;;10BC
+2D1D;GEORGIAN SMALL LETTER CHAR;Ll;0;L;;;;;N;;;10BD;;10BD
+2D1E;GEORGIAN SMALL LETTER XAN;Ll;0;L;;;;;N;;;10BE;;10BE
+2D1F;GEORGIAN SMALL LETTER JHAN;Ll;0;L;;;;;N;;;10BF;;10BF
+2D20;GEORGIAN SMALL LETTER HAE;Ll;0;L;;;;;N;;;10C0;;10C0
+2D21;GEORGIAN SMALL LETTER HE;Ll;0;L;;;;;N;;;10C1;;10C1
+2D22;GEORGIAN SMALL LETTER HIE;Ll;0;L;;;;;N;;;10C2;;10C2
+2D23;GEORGIAN SMALL LETTER WE;Ll;0;L;;;;;N;;;10C3;;10C3
+2D24;GEORGIAN SMALL LETTER HAR;Ll;0;L;;;;;N;;;10C4;;10C4
+2D25;GEORGIAN SMALL LETTER HOE;Ll;0;L;;;;;N;;;10C5;;10C5
+2D27;GEORGIAN SMALL LETTER YN;Ll;0;L;;;;;N;;;10C7;;10C7
+2D2D;GEORGIAN SMALL LETTER AEN;Ll;0;L;;;;;N;;;10CD;;10CD
+2D30;TIFINAGH LETTER YA;Lo;0;L;;;;;N;;;;;
+2D31;TIFINAGH LETTER YAB;Lo;0;L;;;;;N;;;;;
+2D32;TIFINAGH LETTER YABH;Lo;0;L;;;;;N;;;;;
+2D33;TIFINAGH LETTER YAG;Lo;0;L;;;;;N;;;;;
+2D34;TIFINAGH LETTER YAGHH;Lo;0;L;;;;;N;;;;;
+2D35;TIFINAGH LETTER BERBER ACADEMY YAJ;Lo;0;L;;;;;N;;;;;
+2D36;TIFINAGH LETTER YAJ;Lo;0;L;;;;;N;;;;;
+2D37;TIFINAGH LETTER YAD;Lo;0;L;;;;;N;;;;;
+2D38;TIFINAGH LETTER YADH;Lo;0;L;;;;;N;;;;;
+2D39;TIFINAGH LETTER YADD;Lo;0;L;;;;;N;;;;;
+2D3A;TIFINAGH LETTER YADDH;Lo;0;L;;;;;N;;;;;
+2D3B;TIFINAGH LETTER YEY;Lo;0;L;;;;;N;;;;;
+2D3C;TIFINAGH LETTER YAF;Lo;0;L;;;;;N;;;;;
+2D3D;TIFINAGH LETTER YAK;Lo;0;L;;;;;N;;;;;
+2D3E;TIFINAGH LETTER TUAREG YAK;Lo;0;L;;;;;N;;;;;
+2D3F;TIFINAGH LETTER YAKHH;Lo;0;L;;;;;N;;;;;
+2D40;TIFINAGH LETTER YAH;Lo;0;L;;;;;N;;;;;
+2D41;TIFINAGH LETTER BERBER ACADEMY YAH;Lo;0;L;;;;;N;;;;;
+2D42;TIFINAGH LETTER TUAREG YAH;Lo;0;L;;;;;N;;;;;
+2D43;TIFINAGH LETTER YAHH;Lo;0;L;;;;;N;;;;;
+2D44;TIFINAGH LETTER YAA;Lo;0;L;;;;;N;;;;;
+2D45;TIFINAGH LETTER YAKH;Lo;0;L;;;;;N;;;;;
+2D46;TIFINAGH LETTER TUAREG YAKH;Lo;0;L;;;;;N;;;;;
+2D47;TIFINAGH LETTER YAQ;Lo;0;L;;;;;N;;;;;
+2D48;TIFINAGH LETTER TUAREG YAQ;Lo;0;L;;;;;N;;;;;
+2D49;TIFINAGH LETTER YI;Lo;0;L;;;;;N;;;;;
+2D4A;TIFINAGH LETTER YAZH;Lo;0;L;;;;;N;;;;;
+2D4B;TIFINAGH LETTER AHAGGAR YAZH;Lo;0;L;;;;;N;;;;;
+2D4C;TIFINAGH LETTER TUAREG YAZH;Lo;0;L;;;;;N;;;;;
+2D4D;TIFINAGH LETTER YAL;Lo;0;L;;;;;N;;;;;
+2D4E;TIFINAGH LETTER YAM;Lo;0;L;;;;;N;;;;;
+2D4F;TIFINAGH LETTER YAN;Lo;0;L;;;;;N;;;;;
+2D50;TIFINAGH LETTER TUAREG YAGN;Lo;0;L;;;;;N;;;;;
+2D51;TIFINAGH LETTER TUAREG YANG;Lo;0;L;;;;;N;;;;;
+2D52;TIFINAGH LETTER YAP;Lo;0;L;;;;;N;;;;;
+2D53;TIFINAGH LETTER YU;Lo;0;L;;;;;N;;;;;
+2D54;TIFINAGH LETTER YAR;Lo;0;L;;;;;N;;;;;
+2D55;TIFINAGH LETTER YARR;Lo;0;L;;;;;N;;;;;
+2D56;TIFINAGH LETTER YAGH;Lo;0;L;;;;;N;;;;;
+2D57;TIFINAGH LETTER TUAREG YAGH;Lo;0;L;;;;;N;;;;;
+2D58;TIFINAGH LETTER AYER YAGH;Lo;0;L;;;;;N;;;;;
+2D59;TIFINAGH LETTER YAS;Lo;0;L;;;;;N;;;;;
+2D5A;TIFINAGH LETTER YASS;Lo;0;L;;;;;N;;;;;
+2D5B;TIFINAGH LETTER YASH;Lo;0;L;;;;;N;;;;;
+2D5C;TIFINAGH LETTER YAT;Lo;0;L;;;;;N;;;;;
+2D5D;TIFINAGH LETTER YATH;Lo;0;L;;;;;N;;;;;
+2D5E;TIFINAGH LETTER YACH;Lo;0;L;;;;;N;;;;;
+2D5F;TIFINAGH LETTER YATT;Lo;0;L;;;;;N;;;;;
+2D60;TIFINAGH LETTER YAV;Lo;0;L;;;;;N;;;;;
+2D61;TIFINAGH LETTER YAW;Lo;0;L;;;;;N;;;;;
+2D62;TIFINAGH LETTER YAY;Lo;0;L;;;;;N;;;;;
+2D63;TIFINAGH LETTER YAZ;Lo;0;L;;;;;N;;;;;
+2D64;TIFINAGH LETTER TAWELLEMET YAZ;Lo;0;L;;;;;N;;;;;
+2D65;TIFINAGH LETTER YAZZ;Lo;0;L;;;;;N;;;;;
+2D66;TIFINAGH LETTER YE;Lo;0;L;;;;;N;;;;;
+2D67;TIFINAGH LETTER YO;Lo;0;L;;;;;N;;;;;
+2D6F;TIFINAGH MODIFIER LETTER LABIALIZATION MARK;Lm;0;L;<super> 2D61;;;;N;;;;;
+2D70;TIFINAGH SEPARATOR MARK;Po;0;L;;;;;N;;;;;
+2D7F;TIFINAGH CONSONANT JOINER;Mn;9;NSM;;;;;N;;;;;
+2D80;ETHIOPIC SYLLABLE LOA;Lo;0;L;;;;;N;;;;;
+2D81;ETHIOPIC SYLLABLE MOA;Lo;0;L;;;;;N;;;;;
+2D82;ETHIOPIC SYLLABLE ROA;Lo;0;L;;;;;N;;;;;
+2D83;ETHIOPIC SYLLABLE SOA;Lo;0;L;;;;;N;;;;;
+2D84;ETHIOPIC SYLLABLE SHOA;Lo;0;L;;;;;N;;;;;
+2D85;ETHIOPIC SYLLABLE BOA;Lo;0;L;;;;;N;;;;;
+2D86;ETHIOPIC SYLLABLE TOA;Lo;0;L;;;;;N;;;;;
+2D87;ETHIOPIC SYLLABLE COA;Lo;0;L;;;;;N;;;;;
+2D88;ETHIOPIC SYLLABLE NOA;Lo;0;L;;;;;N;;;;;
+2D89;ETHIOPIC SYLLABLE NYOA;Lo;0;L;;;;;N;;;;;
+2D8A;ETHIOPIC SYLLABLE GLOTTAL OA;Lo;0;L;;;;;N;;;;;
+2D8B;ETHIOPIC SYLLABLE ZOA;Lo;0;L;;;;;N;;;;;
+2D8C;ETHIOPIC SYLLABLE DOA;Lo;0;L;;;;;N;;;;;
+2D8D;ETHIOPIC SYLLABLE DDOA;Lo;0;L;;;;;N;;;;;
+2D8E;ETHIOPIC SYLLABLE JOA;Lo;0;L;;;;;N;;;;;
+2D8F;ETHIOPIC SYLLABLE THOA;Lo;0;L;;;;;N;;;;;
+2D90;ETHIOPIC SYLLABLE CHOA;Lo;0;L;;;;;N;;;;;
+2D91;ETHIOPIC SYLLABLE PHOA;Lo;0;L;;;;;N;;;;;
+2D92;ETHIOPIC SYLLABLE POA;Lo;0;L;;;;;N;;;;;
+2D93;ETHIOPIC SYLLABLE GGWA;Lo;0;L;;;;;N;;;;;
+2D94;ETHIOPIC SYLLABLE GGWI;Lo;0;L;;;;;N;;;;;
+2D95;ETHIOPIC SYLLABLE GGWEE;Lo;0;L;;;;;N;;;;;
+2D96;ETHIOPIC SYLLABLE GGWE;Lo;0;L;;;;;N;;;;;
+2DA0;ETHIOPIC SYLLABLE SSA;Lo;0;L;;;;;N;;;;;
+2DA1;ETHIOPIC SYLLABLE SSU;Lo;0;L;;;;;N;;;;;
+2DA2;ETHIOPIC SYLLABLE SSI;Lo;0;L;;;;;N;;;;;
+2DA3;ETHIOPIC SYLLABLE SSAA;Lo;0;L;;;;;N;;;;;
+2DA4;ETHIOPIC SYLLABLE SSEE;Lo;0;L;;;;;N;;;;;
+2DA5;ETHIOPIC SYLLABLE SSE;Lo;0;L;;;;;N;;;;;
+2DA6;ETHIOPIC SYLLABLE SSO;Lo;0;L;;;;;N;;;;;
+2DA8;ETHIOPIC SYLLABLE CCA;Lo;0;L;;;;;N;;;;;
+2DA9;ETHIOPIC SYLLABLE CCU;Lo;0;L;;;;;N;;;;;
+2DAA;ETHIOPIC SYLLABLE CCI;Lo;0;L;;;;;N;;;;;
+2DAB;ETHIOPIC SYLLABLE CCAA;Lo;0;L;;;;;N;;;;;
+2DAC;ETHIOPIC SYLLABLE CCEE;Lo;0;L;;;;;N;;;;;
+2DAD;ETHIOPIC SYLLABLE CCE;Lo;0;L;;;;;N;;;;;
+2DAE;ETHIOPIC SYLLABLE CCO;Lo;0;L;;;;;N;;;;;
+2DB0;ETHIOPIC SYLLABLE ZZA;Lo;0;L;;;;;N;;;;;
+2DB1;ETHIOPIC SYLLABLE ZZU;Lo;0;L;;;;;N;;;;;
+2DB2;ETHIOPIC SYLLABLE ZZI;Lo;0;L;;;;;N;;;;;
+2DB3;ETHIOPIC SYLLABLE ZZAA;Lo;0;L;;;;;N;;;;;
+2DB4;ETHIOPIC SYLLABLE ZZEE;Lo;0;L;;;;;N;;;;;
+2DB5;ETHIOPIC SYLLABLE ZZE;Lo;0;L;;;;;N;;;;;
+2DB6;ETHIOPIC SYLLABLE ZZO;Lo;0;L;;;;;N;;;;;
+2DB8;ETHIOPIC SYLLABLE CCHA;Lo;0;L;;;;;N;;;;;
+2DB9;ETHIOPIC SYLLABLE CCHU;Lo;0;L;;;;;N;;;;;
+2DBA;ETHIOPIC SYLLABLE CCHI;Lo;0;L;;;;;N;;;;;
+2DBB;ETHIOPIC SYLLABLE CCHAA;Lo;0;L;;;;;N;;;;;
+2DBC;ETHIOPIC SYLLABLE CCHEE;Lo;0;L;;;;;N;;;;;
+2DBD;ETHIOPIC SYLLABLE CCHE;Lo;0;L;;;;;N;;;;;
+2DBE;ETHIOPIC SYLLABLE CCHO;Lo;0;L;;;;;N;;;;;
+2DC0;ETHIOPIC SYLLABLE QYA;Lo;0;L;;;;;N;;;;;
+2DC1;ETHIOPIC SYLLABLE QYU;Lo;0;L;;;;;N;;;;;
+2DC2;ETHIOPIC SYLLABLE QYI;Lo;0;L;;;;;N;;;;;
+2DC3;ETHIOPIC SYLLABLE QYAA;Lo;0;L;;;;;N;;;;;
+2DC4;ETHIOPIC SYLLABLE QYEE;Lo;0;L;;;;;N;;;;;
+2DC5;ETHIOPIC SYLLABLE QYE;Lo;0;L;;;;;N;;;;;
+2DC6;ETHIOPIC SYLLABLE QYO;Lo;0;L;;;;;N;;;;;
+2DC8;ETHIOPIC SYLLABLE KYA;Lo;0;L;;;;;N;;;;;
+2DC9;ETHIOPIC SYLLABLE KYU;Lo;0;L;;;;;N;;;;;
+2DCA;ETHIOPIC SYLLABLE KYI;Lo;0;L;;;;;N;;;;;
+2DCB;ETHIOPIC SYLLABLE KYAA;Lo;0;L;;;;;N;;;;;
+2DCC;ETHIOPIC SYLLABLE KYEE;Lo;0;L;;;;;N;;;;;
+2DCD;ETHIOPIC SYLLABLE KYE;Lo;0;L;;;;;N;;;;;
+2DCE;ETHIOPIC SYLLABLE KYO;Lo;0;L;;;;;N;;;;;
+2DD0;ETHIOPIC SYLLABLE XYA;Lo;0;L;;;;;N;;;;;
+2DD1;ETHIOPIC SYLLABLE XYU;Lo;0;L;;;;;N;;;;;
+2DD2;ETHIOPIC SYLLABLE XYI;Lo;0;L;;;;;N;;;;;
+2DD3;ETHIOPIC SYLLABLE XYAA;Lo;0;L;;;;;N;;;;;
+2DD4;ETHIOPIC SYLLABLE XYEE;Lo;0;L;;;;;N;;;;;
+2DD5;ETHIOPIC SYLLABLE XYE;Lo;0;L;;;;;N;;;;;
+2DD6;ETHIOPIC SYLLABLE XYO;Lo;0;L;;;;;N;;;;;
+2DD8;ETHIOPIC SYLLABLE GYA;Lo;0;L;;;;;N;;;;;
+2DD9;ETHIOPIC SYLLABLE GYU;Lo;0;L;;;;;N;;;;;
+2DDA;ETHIOPIC SYLLABLE GYI;Lo;0;L;;;;;N;;;;;
+2DDB;ETHIOPIC SYLLABLE GYAA;Lo;0;L;;;;;N;;;;;
+2DDC;ETHIOPIC SYLLABLE GYEE;Lo;0;L;;;;;N;;;;;
+2DDD;ETHIOPIC SYLLABLE GYE;Lo;0;L;;;;;N;;;;;
+2DDE;ETHIOPIC SYLLABLE GYO;Lo;0;L;;;;;N;;;;;
+2DE0;COMBINING CYRILLIC LETTER BE;Mn;230;NSM;;;;;N;;;;;
+2DE1;COMBINING CYRILLIC LETTER VE;Mn;230;NSM;;;;;N;;;;;
+2DE2;COMBINING CYRILLIC LETTER GHE;Mn;230;NSM;;;;;N;;;;;
+2DE3;COMBINING CYRILLIC LETTER DE;Mn;230;NSM;;;;;N;;;;;
+2DE4;COMBINING CYRILLIC LETTER ZHE;Mn;230;NSM;;;;;N;;;;;
+2DE5;COMBINING CYRILLIC LETTER ZE;Mn;230;NSM;;;;;N;;;;;
+2DE6;COMBINING CYRILLIC LETTER KA;Mn;230;NSM;;;;;N;;;;;
+2DE7;COMBINING CYRILLIC LETTER EL;Mn;230;NSM;;;;;N;;;;;
+2DE8;COMBINING CYRILLIC LETTER EM;Mn;230;NSM;;;;;N;;;;;
+2DE9;COMBINING CYRILLIC LETTER EN;Mn;230;NSM;;;;;N;;;;;
+2DEA;COMBINING CYRILLIC LETTER O;Mn;230;NSM;;;;;N;;;;;
+2DEB;COMBINING CYRILLIC LETTER PE;Mn;230;NSM;;;;;N;;;;;
+2DEC;COMBINING CYRILLIC LETTER ER;Mn;230;NSM;;;;;N;;;;;
+2DED;COMBINING CYRILLIC LETTER ES;Mn;230;NSM;;;;;N;;;;;
+2DEE;COMBINING CYRILLIC LETTER TE;Mn;230;NSM;;;;;N;;;;;
+2DEF;COMBINING CYRILLIC LETTER HA;Mn;230;NSM;;;;;N;;;;;
+2DF0;COMBINING CYRILLIC LETTER TSE;Mn;230;NSM;;;;;N;;;;;
+2DF1;COMBINING CYRILLIC LETTER CHE;Mn;230;NSM;;;;;N;;;;;
+2DF2;COMBINING CYRILLIC LETTER SHA;Mn;230;NSM;;;;;N;;;;;
+2DF3;COMBINING CYRILLIC LETTER SHCHA;Mn;230;NSM;;;;;N;;;;;
+2DF4;COMBINING CYRILLIC LETTER FITA;Mn;230;NSM;;;;;N;;;;;
+2DF5;COMBINING CYRILLIC LETTER ES-TE;Mn;230;NSM;;;;;N;;;;;
+2DF6;COMBINING CYRILLIC LETTER A;Mn;230;NSM;;;;;N;;;;;
+2DF7;COMBINING CYRILLIC LETTER IE;Mn;230;NSM;;;;;N;;;;;
+2DF8;COMBINING CYRILLIC LETTER DJERV;Mn;230;NSM;;;;;N;;;;;
+2DF9;COMBINING CYRILLIC LETTER MONOGRAPH UK;Mn;230;NSM;;;;;N;;;;;
+2DFA;COMBINING CYRILLIC LETTER YAT;Mn;230;NSM;;;;;N;;;;;
+2DFB;COMBINING CYRILLIC LETTER YU;Mn;230;NSM;;;;;N;;;;;
+2DFC;COMBINING CYRILLIC LETTER IOTIFIED A;Mn;230;NSM;;;;;N;;;;;
+2DFD;COMBINING CYRILLIC LETTER LITTLE YUS;Mn;230;NSM;;;;;N;;;;;
+2DFE;COMBINING CYRILLIC LETTER BIG YUS;Mn;230;NSM;;;;;N;;;;;
+2DFF;COMBINING CYRILLIC LETTER IOTIFIED BIG YUS;Mn;230;NSM;;;;;N;;;;;
+2E00;RIGHT ANGLE SUBSTITUTION MARKER;Po;0;ON;;;;;N;;;;;
+2E01;RIGHT ANGLE DOTTED SUBSTITUTION MARKER;Po;0;ON;;;;;N;;;;;
+2E02;LEFT SUBSTITUTION BRACKET;Pi;0;ON;;;;;Y;;;;;
+2E03;RIGHT SUBSTITUTION BRACKET;Pf;0;ON;;;;;Y;;;;;
+2E04;LEFT DOTTED SUBSTITUTION BRACKET;Pi;0;ON;;;;;Y;;;;;
+2E05;RIGHT DOTTED SUBSTITUTION BRACKET;Pf;0;ON;;;;;Y;;;;;
+2E06;RAISED INTERPOLATION MARKER;Po;0;ON;;;;;N;;;;;
+2E07;RAISED DOTTED INTERPOLATION MARKER;Po;0;ON;;;;;N;;;;;
+2E08;DOTTED TRANSPOSITION MARKER;Po;0;ON;;;;;N;;;;;
+2E09;LEFT TRANSPOSITION BRACKET;Pi;0;ON;;;;;Y;;;;;
+2E0A;RIGHT TRANSPOSITION BRACKET;Pf;0;ON;;;;;Y;;;;;
+2E0B;RAISED SQUARE;Po;0;ON;;;;;N;;;;;
+2E0C;LEFT RAISED OMISSION BRACKET;Pi;0;ON;;;;;Y;;;;;
+2E0D;RIGHT RAISED OMISSION BRACKET;Pf;0;ON;;;;;Y;;;;;
+2E0E;EDITORIAL CORONIS;Po;0;ON;;;;;N;;;;;
+2E0F;PARAGRAPHOS;Po;0;ON;;;;;N;;;;;
+2E10;FORKED PARAGRAPHOS;Po;0;ON;;;;;N;;;;;
+2E11;REVERSED FORKED PARAGRAPHOS;Po;0;ON;;;;;N;;;;;
+2E12;HYPODIASTOLE;Po;0;ON;;;;;N;;;;;
+2E13;DOTTED OBELOS;Po;0;ON;;;;;N;;;;;
+2E14;DOWNWARDS ANCORA;Po;0;ON;;;;;N;;;;;
+2E15;UPWARDS ANCORA;Po;0;ON;;;;;N;;;;;
+2E16;DOTTED RIGHT-POINTING ANGLE;Po;0;ON;;;;;N;;;;;
+2E17;DOUBLE OBLIQUE HYPHEN;Pd;0;ON;;;;;N;;;;;
+2E18;INVERTED INTERROBANG;Po;0;ON;;;;;N;;;;;
+2E19;PALM BRANCH;Po;0;ON;;;;;N;;;;;
+2E1A;HYPHEN WITH DIAERESIS;Pd;0;ON;;;;;N;;;;;
+2E1B;TILDE WITH RING ABOVE;Po;0;ON;;;;;N;;;;;
+2E1C;LEFT LOW PARAPHRASE BRACKET;Pi;0;ON;;;;;Y;;;;;
+2E1D;RIGHT LOW PARAPHRASE BRACKET;Pf;0;ON;;;;;Y;;;;;
+2E1E;TILDE WITH DOT ABOVE;Po;0;ON;;;;;N;;;;;
+2E1F;TILDE WITH DOT BELOW;Po;0;ON;;;;;N;;;;;
+2E20;LEFT VERTICAL BAR WITH QUILL;Pi;0;ON;;;;;Y;;;;;
+2E21;RIGHT VERTICAL BAR WITH QUILL;Pf;0;ON;;;;;Y;;;;;
+2E22;TOP LEFT HALF BRACKET;Ps;0;ON;;;;;Y;;;;;
+2E23;TOP RIGHT HALF BRACKET;Pe;0;ON;;;;;Y;;;;;
+2E24;BOTTOM LEFT HALF BRACKET;Ps;0;ON;;;;;Y;;;;;
+2E25;BOTTOM RIGHT HALF BRACKET;Pe;0;ON;;;;;Y;;;;;
+2E26;LEFT SIDEWAYS U BRACKET;Ps;0;ON;;;;;Y;;;;;
+2E27;RIGHT SIDEWAYS U BRACKET;Pe;0;ON;;;;;Y;;;;;
+2E28;LEFT DOUBLE PARENTHESIS;Ps;0;ON;;;;;Y;;;;;
+2E29;RIGHT DOUBLE PARENTHESIS;Pe;0;ON;;;;;Y;;;;;
+2E2A;TWO DOTS OVER ONE DOT PUNCTUATION;Po;0;ON;;;;;N;;;;;
+2E2B;ONE DOT OVER TWO DOTS PUNCTUATION;Po;0;ON;;;;;N;;;;;
+2E2C;SQUARED FOUR DOT PUNCTUATION;Po;0;ON;;;;;N;;;;;
+2E2D;FIVE DOT MARK;Po;0;ON;;;;;N;;;;;
+2E2E;REVERSED QUESTION MARK;Po;0;ON;;;;;N;;;;;
+2E2F;VERTICAL TILDE;Lm;0;ON;;;;;N;;;;;
+2E30;RING POINT;Po;0;ON;;;;;N;;;;;
+2E31;WORD SEPARATOR MIDDLE DOT;Po;0;ON;;;;;N;;;;;
+2E32;TURNED COMMA;Po;0;ON;;;;;N;;;;;
+2E33;RAISED DOT;Po;0;ON;;;;;N;;;;;
+2E34;RAISED COMMA;Po;0;ON;;;;;N;;;;;
+2E35;TURNED SEMICOLON;Po;0;ON;;;;;N;;;;;
+2E36;DAGGER WITH LEFT GUARD;Po;0;ON;;;;;N;;;;;
+2E37;DAGGER WITH RIGHT GUARD;Po;0;ON;;;;;N;;;;;
+2E38;TURNED DAGGER;Po;0;ON;;;;;N;;;;;
+2E39;TOP HALF SECTION SIGN;Po;0;ON;;;;;N;;;;;
+2E3A;TWO-EM DASH;Pd;0;ON;;;;;N;;;;;
+2E3B;THREE-EM DASH;Pd;0;ON;;;;;N;;;;;
+2E3C;STENOGRAPHIC FULL STOP;Po;0;ON;;;;;N;;;;;
+2E3D;VERTICAL SIX DOTS;Po;0;ON;;;;;N;;;;;
+2E3E;WIGGLY VERTICAL LINE;Po;0;ON;;;;;N;;;;;
+2E3F;CAPITULUM;Po;0;ON;;;;;N;;;;;
+2E40;DOUBLE HYPHEN;Pd;0;ON;;;;;N;;;;;
+2E41;REVERSED COMMA;Po;0;ON;;;;;N;;;;;
+2E42;DOUBLE LOW-REVERSED-9 QUOTATION MARK;Ps;0;ON;;;;;N;;;;;
+2E43;DASH WITH LEFT UPTURN;Po;0;ON;;;;;N;;;;;
+2E44;DOUBLE SUSPENSION MARK;Po;0;ON;;;;;N;;;;;
+2E80;CJK RADICAL REPEAT;So;0;ON;;;;;N;;;;;
+2E81;CJK RADICAL CLIFF;So;0;ON;;;;;N;;;;;
+2E82;CJK RADICAL SECOND ONE;So;0;ON;;;;;N;;;;;
+2E83;CJK RADICAL SECOND TWO;So;0;ON;;;;;N;;;;;
+2E84;CJK RADICAL SECOND THREE;So;0;ON;;;;;N;;;;;
+2E85;CJK RADICAL PERSON;So;0;ON;;;;;N;;;;;
+2E86;CJK RADICAL BOX;So;0;ON;;;;;N;;;;;
+2E87;CJK RADICAL TABLE;So;0;ON;;;;;N;;;;;
+2E88;CJK RADICAL KNIFE ONE;So;0;ON;;;;;N;;;;;
+2E89;CJK RADICAL KNIFE TWO;So;0;ON;;;;;N;;;;;
+2E8A;CJK RADICAL DIVINATION;So;0;ON;;;;;N;;;;;
+2E8B;CJK RADICAL SEAL;So;0;ON;;;;;N;;;;;
+2E8C;CJK RADICAL SMALL ONE;So;0;ON;;;;;N;;;;;
+2E8D;CJK RADICAL SMALL TWO;So;0;ON;;;;;N;;;;;
+2E8E;CJK RADICAL LAME ONE;So;0;ON;;;;;N;;;;;
+2E8F;CJK RADICAL LAME TWO;So;0;ON;;;;;N;;;;;
+2E90;CJK RADICAL LAME THREE;So;0;ON;;;;;N;;;;;
+2E91;CJK RADICAL LAME FOUR;So;0;ON;;;;;N;;;;;
+2E92;CJK RADICAL SNAKE;So;0;ON;;;;;N;;;;;
+2E93;CJK RADICAL THREAD;So;0;ON;;;;;N;;;;;
+2E94;CJK RADICAL SNOUT ONE;So;0;ON;;;;;N;;;;;
+2E95;CJK RADICAL SNOUT TWO;So;0;ON;;;;;N;;;;;
+2E96;CJK RADICAL HEART ONE;So;0;ON;;;;;N;;;;;
+2E97;CJK RADICAL HEART TWO;So;0;ON;;;;;N;;;;;
+2E98;CJK RADICAL HAND;So;0;ON;;;;;N;;;;;
+2E99;CJK RADICAL RAP;So;0;ON;;;;;N;;;;;
+2E9B;CJK RADICAL CHOKE;So;0;ON;;;;;N;;;;;
+2E9C;CJK RADICAL SUN;So;0;ON;;;;;N;;;;;
+2E9D;CJK RADICAL MOON;So;0;ON;;;;;N;;;;;
+2E9E;CJK RADICAL DEATH;So;0;ON;;;;;N;;;;;
+2E9F;CJK RADICAL MOTHER;So;0;ON;<compat> 6BCD;;;;N;;;;;
+2EA0;CJK RADICAL CIVILIAN;So;0;ON;;;;;N;;;;;
+2EA1;CJK RADICAL WATER ONE;So;0;ON;;;;;N;;;;;
+2EA2;CJK RADICAL WATER TWO;So;0;ON;;;;;N;;;;;
+2EA3;CJK RADICAL FIRE;So;0;ON;;;;;N;;;;;
+2EA4;CJK RADICAL PAW ONE;So;0;ON;;;;;N;;;;;
+2EA5;CJK RADICAL PAW TWO;So;0;ON;;;;;N;;;;;
+2EA6;CJK RADICAL SIMPLIFIED HALF TREE TRUNK;So;0;ON;;;;;N;;;;;
+2EA7;CJK RADICAL COW;So;0;ON;;;;;N;;;;;
+2EA8;CJK RADICAL DOG;So;0;ON;;;;;N;;;;;
+2EA9;CJK RADICAL JADE;So;0;ON;;;;;N;;;;;
+2EAA;CJK RADICAL BOLT OF CLOTH;So;0;ON;;;;;N;;;;;
+2EAB;CJK RADICAL EYE;So;0;ON;;;;;N;;;;;
+2EAC;CJK RADICAL SPIRIT ONE;So;0;ON;;;;;N;;;;;
+2EAD;CJK RADICAL SPIRIT TWO;So;0;ON;;;;;N;;;;;
+2EAE;CJK RADICAL BAMBOO;So;0;ON;;;;;N;;;;;
+2EAF;CJK RADICAL SILK;So;0;ON;;;;;N;;;;;
+2EB0;CJK RADICAL C-SIMPLIFIED SILK;So;0;ON;;;;;N;;;;;
+2EB1;CJK RADICAL NET ONE;So;0;ON;;;;;N;;;;;
+2EB2;CJK RADICAL NET TWO;So;0;ON;;;;;N;;;;;
+2EB3;CJK RADICAL NET THREE;So;0;ON;;;;;N;;;;;
+2EB4;CJK RADICAL NET FOUR;So;0;ON;;;;;N;;;;;
+2EB5;CJK RADICAL MESH;So;0;ON;;;;;N;;;;;
+2EB6;CJK RADICAL SHEEP;So;0;ON;;;;;N;;;;;
+2EB7;CJK RADICAL RAM;So;0;ON;;;;;N;;;;;
+2EB8;CJK RADICAL EWE;So;0;ON;;;;;N;;;;;
+2EB9;CJK RADICAL OLD;So;0;ON;;;;;N;;;;;
+2EBA;CJK RADICAL BRUSH ONE;So;0;ON;;;;;N;;;;;
+2EBB;CJK RADICAL BRUSH TWO;So;0;ON;;;;;N;;;;;
+2EBC;CJK RADICAL MEAT;So;0;ON;;;;;N;;;;;
+2EBD;CJK RADICAL MORTAR;So;0;ON;;;;;N;;;;;
+2EBE;CJK RADICAL GRASS ONE;So;0;ON;;;;;N;;;;;
+2EBF;CJK RADICAL GRASS TWO;So;0;ON;;;;;N;;;;;
+2EC0;CJK RADICAL GRASS THREE;So;0;ON;;;;;N;;;;;
+2EC1;CJK RADICAL TIGER;So;0;ON;;;;;N;;;;;
+2EC2;CJK RADICAL CLOTHES;So;0;ON;;;;;N;;;;;
+2EC3;CJK RADICAL WEST ONE;So;0;ON;;;;;N;;;;;
+2EC4;CJK RADICAL WEST TWO;So;0;ON;;;;;N;;;;;
+2EC5;CJK RADICAL C-SIMPLIFIED SEE;So;0;ON;;;;;N;;;;;
+2EC6;CJK RADICAL SIMPLIFIED HORN;So;0;ON;;;;;N;;;;;
+2EC7;CJK RADICAL HORN;So;0;ON;;;;;N;;;;;
+2EC8;CJK RADICAL C-SIMPLIFIED SPEECH;So;0;ON;;;;;N;;;;;
+2EC9;CJK RADICAL C-SIMPLIFIED SHELL;So;0;ON;;;;;N;;;;;
+2ECA;CJK RADICAL FOOT;So;0;ON;;;;;N;;;;;
+2ECB;CJK RADICAL C-SIMPLIFIED CART;So;0;ON;;;;;N;;;;;
+2ECC;CJK RADICAL SIMPLIFIED WALK;So;0;ON;;;;;N;;;;;
+2ECD;CJK RADICAL WALK ONE;So;0;ON;;;;;N;;;;;
+2ECE;CJK RADICAL WALK TWO;So;0;ON;;;;;N;;;;;
+2ECF;CJK RADICAL CITY;So;0;ON;;;;;N;;;;;
+2ED0;CJK RADICAL C-SIMPLIFIED GOLD;So;0;ON;;;;;N;;;;;
+2ED1;CJK RADICAL LONG ONE;So;0;ON;;;;;N;;;;;
+2ED2;CJK RADICAL LONG TWO;So;0;ON;;;;;N;;;;;
+2ED3;CJK RADICAL C-SIMPLIFIED LONG;So;0;ON;;;;;N;;;;;
+2ED4;CJK RADICAL C-SIMPLIFIED GATE;So;0;ON;;;;;N;;;;;
+2ED5;CJK RADICAL MOUND ONE;So;0;ON;;;;;N;;;;;
+2ED6;CJK RADICAL MOUND TWO;So;0;ON;;;;;N;;;;;
+2ED7;CJK RADICAL RAIN;So;0;ON;;;;;N;;;;;
+2ED8;CJK RADICAL BLUE;So;0;ON;;;;;N;;;;;
+2ED9;CJK RADICAL C-SIMPLIFIED TANNED LEATHER;So;0;ON;;;;;N;;;;;
+2EDA;CJK RADICAL C-SIMPLIFIED LEAF;So;0;ON;;;;;N;;;;;
+2EDB;CJK RADICAL C-SIMPLIFIED WIND;So;0;ON;;;;;N;;;;;
+2EDC;CJK RADICAL C-SIMPLIFIED FLY;So;0;ON;;;;;N;;;;;
+2EDD;CJK RADICAL EAT ONE;So;0;ON;;;;;N;;;;;
+2EDE;CJK RADICAL EAT TWO;So;0;ON;;;;;N;;;;;
+2EDF;CJK RADICAL EAT THREE;So;0;ON;;;;;N;;;;;
+2EE0;CJK RADICAL C-SIMPLIFIED EAT;So;0;ON;;;;;N;;;;;
+2EE1;CJK RADICAL HEAD;So;0;ON;;;;;N;;;;;
+2EE2;CJK RADICAL C-SIMPLIFIED HORSE;So;0;ON;;;;;N;;;;;
+2EE3;CJK RADICAL BONE;So;0;ON;;;;;N;;;;;
+2EE4;CJK RADICAL GHOST;So;0;ON;;;;;N;;;;;
+2EE5;CJK RADICAL C-SIMPLIFIED FISH;So;0;ON;;;;;N;;;;;
+2EE6;CJK RADICAL C-SIMPLIFIED BIRD;So;0;ON;;;;;N;;;;;
+2EE7;CJK RADICAL C-SIMPLIFIED SALT;So;0;ON;;;;;N;;;;;
+2EE8;CJK RADICAL SIMPLIFIED WHEAT;So;0;ON;;;;;N;;;;;
+2EE9;CJK RADICAL SIMPLIFIED YELLOW;So;0;ON;;;;;N;;;;;
+2EEA;CJK RADICAL C-SIMPLIFIED FROG;So;0;ON;;;;;N;;;;;
+2EEB;CJK RADICAL J-SIMPLIFIED EVEN;So;0;ON;;;;;N;;;;;
+2EEC;CJK RADICAL C-SIMPLIFIED EVEN;So;0;ON;;;;;N;;;;;
+2EED;CJK RADICAL J-SIMPLIFIED TOOTH;So;0;ON;;;;;N;;;;;
+2EEE;CJK RADICAL C-SIMPLIFIED TOOTH;So;0;ON;;;;;N;;;;;
+2EEF;CJK RADICAL J-SIMPLIFIED DRAGON;So;0;ON;;;;;N;;;;;
+2EF0;CJK RADICAL C-SIMPLIFIED DRAGON;So;0;ON;;;;;N;;;;;
+2EF1;CJK RADICAL TURTLE;So;0;ON;;;;;N;;;;;
+2EF2;CJK RADICAL J-SIMPLIFIED TURTLE;So;0;ON;;;;;N;;;;;
+2EF3;CJK RADICAL C-SIMPLIFIED TURTLE;So;0;ON;<compat> 9F9F;;;;N;;;;;
+2F00;KANGXI RADICAL ONE;So;0;ON;<compat> 4E00;;;;N;;;;;
+2F01;KANGXI RADICAL LINE;So;0;ON;<compat> 4E28;;;;N;;;;;
+2F02;KANGXI RADICAL DOT;So;0;ON;<compat> 4E36;;;;N;;;;;
+2F03;KANGXI RADICAL SLASH;So;0;ON;<compat> 4E3F;;;;N;;;;;
+2F04;KANGXI RADICAL SECOND;So;0;ON;<compat> 4E59;;;;N;;;;;
+2F05;KANGXI RADICAL HOOK;So;0;ON;<compat> 4E85;;;;N;;;;;
+2F06;KANGXI RADICAL TWO;So;0;ON;<compat> 4E8C;;;;N;;;;;
+2F07;KANGXI RADICAL LID;So;0;ON;<compat> 4EA0;;;;N;;;;;
+2F08;KANGXI RADICAL MAN;So;0;ON;<compat> 4EBA;;;;N;;;;;
+2F09;KANGXI RADICAL LEGS;So;0;ON;<compat> 513F;;;;N;;;;;
+2F0A;KANGXI RADICAL ENTER;So;0;ON;<compat> 5165;;;;N;;;;;
+2F0B;KANGXI RADICAL EIGHT;So;0;ON;<compat> 516B;;;;N;;;;;
+2F0C;KANGXI RADICAL DOWN BOX;So;0;ON;<compat> 5182;;;;N;;;;;
+2F0D;KANGXI RADICAL COVER;So;0;ON;<compat> 5196;;;;N;;;;;
+2F0E;KANGXI RADICAL ICE;So;0;ON;<compat> 51AB;;;;N;;;;;
+2F0F;KANGXI RADICAL TABLE;So;0;ON;<compat> 51E0;;;;N;;;;;
+2F10;KANGXI RADICAL OPEN BOX;So;0;ON;<compat> 51F5;;;;N;;;;;
+2F11;KANGXI RADICAL KNIFE;So;0;ON;<compat> 5200;;;;N;;;;;
+2F12;KANGXI RADICAL POWER;So;0;ON;<compat> 529B;;;;N;;;;;
+2F13;KANGXI RADICAL WRAP;So;0;ON;<compat> 52F9;;;;N;;;;;
+2F14;KANGXI RADICAL SPOON;So;0;ON;<compat> 5315;;;;N;;;;;
+2F15;KANGXI RADICAL RIGHT OPEN BOX;So;0;ON;<compat> 531A;;;;N;;;;;
+2F16;KANGXI RADICAL HIDING ENCLOSURE;So;0;ON;<compat> 5338;;;;N;;;;;
+2F17;KANGXI RADICAL TEN;So;0;ON;<compat> 5341;;;;N;;;;;
+2F18;KANGXI RADICAL DIVINATION;So;0;ON;<compat> 535C;;;;N;;;;;
+2F19;KANGXI RADICAL SEAL;So;0;ON;<compat> 5369;;;;N;;;;;
+2F1A;KANGXI RADICAL CLIFF;So;0;ON;<compat> 5382;;;;N;;;;;
+2F1B;KANGXI RADICAL PRIVATE;So;0;ON;<compat> 53B6;;;;N;;;;;
+2F1C;KANGXI RADICAL AGAIN;So;0;ON;<compat> 53C8;;;;N;;;;;
+2F1D;KANGXI RADICAL MOUTH;So;0;ON;<compat> 53E3;;;;N;;;;;
+2F1E;KANGXI RADICAL ENCLOSURE;So;0;ON;<compat> 56D7;;;;N;;;;;
+2F1F;KANGXI RADICAL EARTH;So;0;ON;<compat> 571F;;;;N;;;;;
+2F20;KANGXI RADICAL SCHOLAR;So;0;ON;<compat> 58EB;;;;N;;;;;
+2F21;KANGXI RADICAL GO;So;0;ON;<compat> 5902;;;;N;;;;;
+2F22;KANGXI RADICAL GO SLOWLY;So;0;ON;<compat> 590A;;;;N;;;;;
+2F23;KANGXI RADICAL EVENING;So;0;ON;<compat> 5915;;;;N;;;;;
+2F24;KANGXI RADICAL BIG;So;0;ON;<compat> 5927;;;;N;;;;;
+2F25;KANGXI RADICAL WOMAN;So;0;ON;<compat> 5973;;;;N;;;;;
+2F26;KANGXI RADICAL CHILD;So;0;ON;<compat> 5B50;;;;N;;;;;
+2F27;KANGXI RADICAL ROOF;So;0;ON;<compat> 5B80;;;;N;;;;;
+2F28;KANGXI RADICAL INCH;So;0;ON;<compat> 5BF8;;;;N;;;;;
+2F29;KANGXI RADICAL SMALL;So;0;ON;<compat> 5C0F;;;;N;;;;;
+2F2A;KANGXI RADICAL LAME;So;0;ON;<compat> 5C22;;;;N;;;;;
+2F2B;KANGXI RADICAL CORPSE;So;0;ON;<compat> 5C38;;;;N;;;;;
+2F2C;KANGXI RADICAL SPROUT;So;0;ON;<compat> 5C6E;;;;N;;;;;
+2F2D;KANGXI RADICAL MOUNTAIN;So;0;ON;<compat> 5C71;;;;N;;;;;
+2F2E;KANGXI RADICAL RIVER;So;0;ON;<compat> 5DDB;;;;N;;;;;
+2F2F;KANGXI RADICAL WORK;So;0;ON;<compat> 5DE5;;;;N;;;;;
+2F30;KANGXI RADICAL ONESELF;So;0;ON;<compat> 5DF1;;;;N;;;;;
+2F31;KANGXI RADICAL TURBAN;So;0;ON;<compat> 5DFE;;;;N;;;;;
+2F32;KANGXI RADICAL DRY;So;0;ON;<compat> 5E72;;;;N;;;;;
+2F33;KANGXI RADICAL SHORT THREAD;So;0;ON;<compat> 5E7A;;;;N;;;;;
+2F34;KANGXI RADICAL DOTTED CLIFF;So;0;ON;<compat> 5E7F;;;;N;;;;;
+2F35;KANGXI RADICAL LONG STRIDE;So;0;ON;<compat> 5EF4;;;;N;;;;;
+2F36;KANGXI RADICAL TWO HANDS;So;0;ON;<compat> 5EFE;;;;N;;;;;
+2F37;KANGXI RADICAL SHOOT;So;0;ON;<compat> 5F0B;;;;N;;;;;
+2F38;KANGXI RADICAL BOW;So;0;ON;<compat> 5F13;;;;N;;;;;
+2F39;KANGXI RADICAL SNOUT;So;0;ON;<compat> 5F50;;;;N;;;;;
+2F3A;KANGXI RADICAL BRISTLE;So;0;ON;<compat> 5F61;;;;N;;;;;
+2F3B;KANGXI RADICAL STEP;So;0;ON;<compat> 5F73;;;;N;;;;;
+2F3C;KANGXI RADICAL HEART;So;0;ON;<compat> 5FC3;;;;N;;;;;
+2F3D;KANGXI RADICAL HALBERD;So;0;ON;<compat> 6208;;;;N;;;;;
+2F3E;KANGXI RADICAL DOOR;So;0;ON;<compat> 6236;;;;N;;;;;
+2F3F;KANGXI RADICAL HAND;So;0;ON;<compat> 624B;;;;N;;;;;
+2F40;KANGXI RADICAL BRANCH;So;0;ON;<compat> 652F;;;;N;;;;;
+2F41;KANGXI RADICAL RAP;So;0;ON;<compat> 6534;;;;N;;;;;
+2F42;KANGXI RADICAL SCRIPT;So;0;ON;<compat> 6587;;;;N;;;;;
+2F43;KANGXI RADICAL DIPPER;So;0;ON;<compat> 6597;;;;N;;;;;
+2F44;KANGXI RADICAL AXE;So;0;ON;<compat> 65A4;;;;N;;;;;
+2F45;KANGXI RADICAL SQUARE;So;0;ON;<compat> 65B9;;;;N;;;;;
+2F46;KANGXI RADICAL NOT;So;0;ON;<compat> 65E0;;;;N;;;;;
+2F47;KANGXI RADICAL SUN;So;0;ON;<compat> 65E5;;;;N;;;;;
+2F48;KANGXI RADICAL SAY;So;0;ON;<compat> 66F0;;;;N;;;;;
+2F49;KANGXI RADICAL MOON;So;0;ON;<compat> 6708;;;;N;;;;;
+2F4A;KANGXI RADICAL TREE;So;0;ON;<compat> 6728;;;;N;;;;;
+2F4B;KANGXI RADICAL LACK;So;0;ON;<compat> 6B20;;;;N;;;;;
+2F4C;KANGXI RADICAL STOP;So;0;ON;<compat> 6B62;;;;N;;;;;
+2F4D;KANGXI RADICAL DEATH;So;0;ON;<compat> 6B79;;;;N;;;;;
+2F4E;KANGXI RADICAL WEAPON;So;0;ON;<compat> 6BB3;;;;N;;;;;
+2F4F;KANGXI RADICAL DO NOT;So;0;ON;<compat> 6BCB;;;;N;;;;;
+2F50;KANGXI RADICAL COMPARE;So;0;ON;<compat> 6BD4;;;;N;;;;;
+2F51;KANGXI RADICAL FUR;So;0;ON;<compat> 6BDB;;;;N;;;;;
+2F52;KANGXI RADICAL CLAN;So;0;ON;<compat> 6C0F;;;;N;;;;;
+2F53;KANGXI RADICAL STEAM;So;0;ON;<compat> 6C14;;;;N;;;;;
+2F54;KANGXI RADICAL WATER;So;0;ON;<compat> 6C34;;;;N;;;;;
+2F55;KANGXI RADICAL FIRE;So;0;ON;<compat> 706B;;;;N;;;;;
+2F56;KANGXI RADICAL CLAW;So;0;ON;<compat> 722A;;;;N;;;;;
+2F57;KANGXI RADICAL FATHER;So;0;ON;<compat> 7236;;;;N;;;;;
+2F58;KANGXI RADICAL DOUBLE X;So;0;ON;<compat> 723B;;;;N;;;;;
+2F59;KANGXI RADICAL HALF TREE TRUNK;So;0;ON;<compat> 723F;;;;N;;;;;
+2F5A;KANGXI RADICAL SLICE;So;0;ON;<compat> 7247;;;;N;;;;;
+2F5B;KANGXI RADICAL FANG;So;0;ON;<compat> 7259;;;;N;;;;;
+2F5C;KANGXI RADICAL COW;So;0;ON;<compat> 725B;;;;N;;;;;
+2F5D;KANGXI RADICAL DOG;So;0;ON;<compat> 72AC;;;;N;;;;;
+2F5E;KANGXI RADICAL PROFOUND;So;0;ON;<compat> 7384;;;;N;;;;;
+2F5F;KANGXI RADICAL JADE;So;0;ON;<compat> 7389;;;;N;;;;;
+2F60;KANGXI RADICAL MELON;So;0;ON;<compat> 74DC;;;;N;;;;;
+2F61;KANGXI RADICAL TILE;So;0;ON;<compat> 74E6;;;;N;;;;;
+2F62;KANGXI RADICAL SWEET;So;0;ON;<compat> 7518;;;;N;;;;;
+2F63;KANGXI RADICAL LIFE;So;0;ON;<compat> 751F;;;;N;;;;;
+2F64;KANGXI RADICAL USE;So;0;ON;<compat> 7528;;;;N;;;;;
+2F65;KANGXI RADICAL FIELD;So;0;ON;<compat> 7530;;;;N;;;;;
+2F66;KANGXI RADICAL BOLT OF CLOTH;So;0;ON;<compat> 758B;;;;N;;;;;
+2F67;KANGXI RADICAL SICKNESS;So;0;ON;<compat> 7592;;;;N;;;;;
+2F68;KANGXI RADICAL DOTTED TENT;So;0;ON;<compat> 7676;;;;N;;;;;
+2F69;KANGXI RADICAL WHITE;So;0;ON;<compat> 767D;;;;N;;;;;
+2F6A;KANGXI RADICAL SKIN;So;0;ON;<compat> 76AE;;;;N;;;;;
+2F6B;KANGXI RADICAL DISH;So;0;ON;<compat> 76BF;;;;N;;;;;
+2F6C;KANGXI RADICAL EYE;So;0;ON;<compat> 76EE;;;;N;;;;;
+2F6D;KANGXI RADICAL SPEAR;So;0;ON;<compat> 77DB;;;;N;;;;;
+2F6E;KANGXI RADICAL ARROW;So;0;ON;<compat> 77E2;;;;N;;;;;
+2F6F;KANGXI RADICAL STONE;So;0;ON;<compat> 77F3;;;;N;;;;;
+2F70;KANGXI RADICAL SPIRIT;So;0;ON;<compat> 793A;;;;N;;;;;
+2F71;KANGXI RADICAL TRACK;So;0;ON;<compat> 79B8;;;;N;;;;;
+2F72;KANGXI RADICAL GRAIN;So;0;ON;<compat> 79BE;;;;N;;;;;
+2F73;KANGXI RADICAL CAVE;So;0;ON;<compat> 7A74;;;;N;;;;;
+2F74;KANGXI RADICAL STAND;So;0;ON;<compat> 7ACB;;;;N;;;;;
+2F75;KANGXI RADICAL BAMBOO;So;0;ON;<compat> 7AF9;;;;N;;;;;
+2F76;KANGXI RADICAL RICE;So;0;ON;<compat> 7C73;;;;N;;;;;
+2F77;KANGXI RADICAL SILK;So;0;ON;<compat> 7CF8;;;;N;;;;;
+2F78;KANGXI RADICAL JAR;So;0;ON;<compat> 7F36;;;;N;;;;;
+2F79;KANGXI RADICAL NET;So;0;ON;<compat> 7F51;;;;N;;;;;
+2F7A;KANGXI RADICAL SHEEP;So;0;ON;<compat> 7F8A;;;;N;;;;;
+2F7B;KANGXI RADICAL FEATHER;So;0;ON;<compat> 7FBD;;;;N;;;;;
+2F7C;KANGXI RADICAL OLD;So;0;ON;<compat> 8001;;;;N;;;;;
+2F7D;KANGXI RADICAL AND;So;0;ON;<compat> 800C;;;;N;;;;;
+2F7E;KANGXI RADICAL PLOW;So;0;ON;<compat> 8012;;;;N;;;;;
+2F7F;KANGXI RADICAL EAR;So;0;ON;<compat> 8033;;;;N;;;;;
+2F80;KANGXI RADICAL BRUSH;So;0;ON;<compat> 807F;;;;N;;;;;
+2F81;KANGXI RADICAL MEAT;So;0;ON;<compat> 8089;;;;N;;;;;
+2F82;KANGXI RADICAL MINISTER;So;0;ON;<compat> 81E3;;;;N;;;;;
+2F83;KANGXI RADICAL SELF;So;0;ON;<compat> 81EA;;;;N;;;;;
+2F84;KANGXI RADICAL ARRIVE;So;0;ON;<compat> 81F3;;;;N;;;;;
+2F85;KANGXI RADICAL MORTAR;So;0;ON;<compat> 81FC;;;;N;;;;;
+2F86;KANGXI RADICAL TONGUE;So;0;ON;<compat> 820C;;;;N;;;;;
+2F87;KANGXI RADICAL OPPOSE;So;0;ON;<compat> 821B;;;;N;;;;;
+2F88;KANGXI RADICAL BOAT;So;0;ON;<compat> 821F;;;;N;;;;;
+2F89;KANGXI RADICAL STOPPING;So;0;ON;<compat> 826E;;;;N;;;;;
+2F8A;KANGXI RADICAL COLOR;So;0;ON;<compat> 8272;;;;N;;;;;
+2F8B;KANGXI RADICAL GRASS;So;0;ON;<compat> 8278;;;;N;;;;;
+2F8C;KANGXI RADICAL TIGER;So;0;ON;<compat> 864D;;;;N;;;;;
+2F8D;KANGXI RADICAL INSECT;So;0;ON;<compat> 866B;;;;N;;;;;
+2F8E;KANGXI RADICAL BLOOD;So;0;ON;<compat> 8840;;;;N;;;;;
+2F8F;KANGXI RADICAL WALK ENCLOSURE;So;0;ON;<compat> 884C;;;;N;;;;;
+2F90;KANGXI RADICAL CLOTHES;So;0;ON;<compat> 8863;;;;N;;;;;
+2F91;KANGXI RADICAL WEST;So;0;ON;<compat> 897E;;;;N;;;;;
+2F92;KANGXI RADICAL SEE;So;0;ON;<compat> 898B;;;;N;;;;;
+2F93;KANGXI RADICAL HORN;So;0;ON;<compat> 89D2;;;;N;;;;;
+2F94;KANGXI RADICAL SPEECH;So;0;ON;<compat> 8A00;;;;N;;;;;
+2F95;KANGXI RADICAL VALLEY;So;0;ON;<compat> 8C37;;;;N;;;;;
+2F96;KANGXI RADICAL BEAN;So;0;ON;<compat> 8C46;;;;N;;;;;
+2F97;KANGXI RADICAL PIG;So;0;ON;<compat> 8C55;;;;N;;;;;
+2F98;KANGXI RADICAL BADGER;So;0;ON;<compat> 8C78;;;;N;;;;;
+2F99;KANGXI RADICAL SHELL;So;0;ON;<compat> 8C9D;;;;N;;;;;
+2F9A;KANGXI RADICAL RED;So;0;ON;<compat> 8D64;;;;N;;;;;
+2F9B;KANGXI RADICAL RUN;So;0;ON;<compat> 8D70;;;;N;;;;;
+2F9C;KANGXI RADICAL FOOT;So;0;ON;<compat> 8DB3;;;;N;;;;;
+2F9D;KANGXI RADICAL BODY;So;0;ON;<compat> 8EAB;;;;N;;;;;
+2F9E;KANGXI RADICAL CART;So;0;ON;<compat> 8ECA;;;;N;;;;;
+2F9F;KANGXI RADICAL BITTER;So;0;ON;<compat> 8F9B;;;;N;;;;;
+2FA0;KANGXI RADICAL MORNING;So;0;ON;<compat> 8FB0;;;;N;;;;;
+2FA1;KANGXI RADICAL WALK;So;0;ON;<compat> 8FB5;;;;N;;;;;
+2FA2;KANGXI RADICAL CITY;So;0;ON;<compat> 9091;;;;N;;;;;
+2FA3;KANGXI RADICAL WINE;So;0;ON;<compat> 9149;;;;N;;;;;
+2FA4;KANGXI RADICAL DISTINGUISH;So;0;ON;<compat> 91C6;;;;N;;;;;
+2FA5;KANGXI RADICAL VILLAGE;So;0;ON;<compat> 91CC;;;;N;;;;;
+2FA6;KANGXI RADICAL GOLD;So;0;ON;<compat> 91D1;;;;N;;;;;
+2FA7;KANGXI RADICAL LONG;So;0;ON;<compat> 9577;;;;N;;;;;
+2FA8;KANGXI RADICAL GATE;So;0;ON;<compat> 9580;;;;N;;;;;
+2FA9;KANGXI RADICAL MOUND;So;0;ON;<compat> 961C;;;;N;;;;;
+2FAA;KANGXI RADICAL SLAVE;So;0;ON;<compat> 96B6;;;;N;;;;;
+2FAB;KANGXI RADICAL SHORT TAILED BIRD;So;0;ON;<compat> 96B9;;;;N;;;;;
+2FAC;KANGXI RADICAL RAIN;So;0;ON;<compat> 96E8;;;;N;;;;;
+2FAD;KANGXI RADICAL BLUE;So;0;ON;<compat> 9751;;;;N;;;;;
+2FAE;KANGXI RADICAL WRONG;So;0;ON;<compat> 975E;;;;N;;;;;
+2FAF;KANGXI RADICAL FACE;So;0;ON;<compat> 9762;;;;N;;;;;
+2FB0;KANGXI RADICAL LEATHER;So;0;ON;<compat> 9769;;;;N;;;;;
+2FB1;KANGXI RADICAL TANNED LEATHER;So;0;ON;<compat> 97CB;;;;N;;;;;
+2FB2;KANGXI RADICAL LEEK;So;0;ON;<compat> 97ED;;;;N;;;;;
+2FB3;KANGXI RADICAL SOUND;So;0;ON;<compat> 97F3;;;;N;;;;;
+2FB4;KANGXI RADICAL LEAF;So;0;ON;<compat> 9801;;;;N;;;;;
+2FB5;KANGXI RADICAL WIND;So;0;ON;<compat> 98A8;;;;N;;;;;
+2FB6;KANGXI RADICAL FLY;So;0;ON;<compat> 98DB;;;;N;;;;;
+2FB7;KANGXI RADICAL EAT;So;0;ON;<compat> 98DF;;;;N;;;;;
+2FB8;KANGXI RADICAL HEAD;So;0;ON;<compat> 9996;;;;N;;;;;
+2FB9;KANGXI RADICAL FRAGRANT;So;0;ON;<compat> 9999;;;;N;;;;;
+2FBA;KANGXI RADICAL HORSE;So;0;ON;<compat> 99AC;;;;N;;;;;
+2FBB;KANGXI RADICAL BONE;So;0;ON;<compat> 9AA8;;;;N;;;;;
+2FBC;KANGXI RADICAL TALL;So;0;ON;<compat> 9AD8;;;;N;;;;;
+2FBD;KANGXI RADICAL HAIR;So;0;ON;<compat> 9ADF;;;;N;;;;;
+2FBE;KANGXI RADICAL FIGHT;So;0;ON;<compat> 9B25;;;;N;;;;;
+2FBF;KANGXI RADICAL SACRIFICIAL WINE;So;0;ON;<compat> 9B2F;;;;N;;;;;
+2FC0;KANGXI RADICAL CAULDRON;So;0;ON;<compat> 9B32;;;;N;;;;;
+2FC1;KANGXI RADICAL GHOST;So;0;ON;<compat> 9B3C;;;;N;;;;;
+2FC2;KANGXI RADICAL FISH;So;0;ON;<compat> 9B5A;;;;N;;;;;
+2FC3;KANGXI RADICAL BIRD;So;0;ON;<compat> 9CE5;;;;N;;;;;
+2FC4;KANGXI RADICAL SALT;So;0;ON;<compat> 9E75;;;;N;;;;;
+2FC5;KANGXI RADICAL DEER;So;0;ON;<compat> 9E7F;;;;N;;;;;
+2FC6;KANGXI RADICAL WHEAT;So;0;ON;<compat> 9EA5;;;;N;;;;;
+2FC7;KANGXI RADICAL HEMP;So;0;ON;<compat> 9EBB;;;;N;;;;;
+2FC8;KANGXI RADICAL YELLOW;So;0;ON;<compat> 9EC3;;;;N;;;;;
+2FC9;KANGXI RADICAL MILLET;So;0;ON;<compat> 9ECD;;;;N;;;;;
+2FCA;KANGXI RADICAL BLACK;So;0;ON;<compat> 9ED1;;;;N;;;;;
+2FCB;KANGXI RADICAL EMBROIDERY;So;0;ON;<compat> 9EF9;;;;N;;;;;
+2FCC;KANGXI RADICAL FROG;So;0;ON;<compat> 9EFD;;;;N;;;;;
+2FCD;KANGXI RADICAL TRIPOD;So;0;ON;<compat> 9F0E;;;;N;;;;;
+2FCE;KANGXI RADICAL DRUM;So;0;ON;<compat> 9F13;;;;N;;;;;
+2FCF;KANGXI RADICAL RAT;So;0;ON;<compat> 9F20;;;;N;;;;;
+2FD0;KANGXI RADICAL NOSE;So;0;ON;<compat> 9F3B;;;;N;;;;;
+2FD1;KANGXI RADICAL EVEN;So;0;ON;<compat> 9F4A;;;;N;;;;;
+2FD2;KANGXI RADICAL TOOTH;So;0;ON;<compat> 9F52;;;;N;;;;;
+2FD3;KANGXI RADICAL DRAGON;So;0;ON;<compat> 9F8D;;;;N;;;;;
+2FD4;KANGXI RADICAL TURTLE;So;0;ON;<compat> 9F9C;;;;N;;;;;
+2FD5;KANGXI RADICAL FLUTE;So;0;ON;<compat> 9FA0;;;;N;;;;;
+2FF0;IDEOGRAPHIC DESCRIPTION CHARACTER LEFT TO RIGHT;So;0;ON;;;;;N;;;;;
+2FF1;IDEOGRAPHIC DESCRIPTION CHARACTER ABOVE TO BELOW;So;0;ON;;;;;N;;;;;
+2FF2;IDEOGRAPHIC DESCRIPTION CHARACTER LEFT TO MIDDLE AND RIGHT;So;0;ON;;;;;N;;;;;
+2FF3;IDEOGRAPHIC DESCRIPTION CHARACTER ABOVE TO MIDDLE AND BELOW;So;0;ON;;;;;N;;;;;
+2FF4;IDEOGRAPHIC DESCRIPTION CHARACTER FULL SURROUND;So;0;ON;;;;;N;;;;;
+2FF5;IDEOGRAPHIC DESCRIPTION CHARACTER SURROUND FROM ABOVE;So;0;ON;;;;;N;;;;;
+2FF6;IDEOGRAPHIC DESCRIPTION CHARACTER SURROUND FROM BELOW;So;0;ON;;;;;N;;;;;
+2FF7;IDEOGRAPHIC DESCRIPTION CHARACTER SURROUND FROM LEFT;So;0;ON;;;;;N;;;;;
+2FF8;IDEOGRAPHIC DESCRIPTION CHARACTER SURROUND FROM UPPER LEFT;So;0;ON;;;;;N;;;;;
+2FF9;IDEOGRAPHIC DESCRIPTION CHARACTER SURROUND FROM UPPER RIGHT;So;0;ON;;;;;N;;;;;
+2FFA;IDEOGRAPHIC DESCRIPTION CHARACTER SURROUND FROM LOWER LEFT;So;0;ON;;;;;N;;;;;
+2FFB;IDEOGRAPHIC DESCRIPTION CHARACTER OVERLAID;So;0;ON;;;;;N;;;;;
+3000;IDEOGRAPHIC SPACE;Zs;0;WS;<wide> 0020;;;;N;;;;;
+3001;IDEOGRAPHIC COMMA;Po;0;ON;;;;;N;;;;;
+3002;IDEOGRAPHIC FULL STOP;Po;0;ON;;;;;N;IDEOGRAPHIC PERIOD;;;;
+3003;DITTO MARK;Po;0;ON;;;;;N;;;;;
+3004;JAPANESE INDUSTRIAL STANDARD SYMBOL;So;0;ON;;;;;N;;;;;
+3005;IDEOGRAPHIC ITERATION MARK;Lm;0;L;;;;;N;;;;;
+3006;IDEOGRAPHIC CLOSING MARK;Lo;0;L;;;;;N;;;;;
+3007;IDEOGRAPHIC NUMBER ZERO;Nl;0;L;;;;0;N;;;;;
+3008;LEFT ANGLE BRACKET;Ps;0;ON;;;;;Y;OPENING ANGLE BRACKET;;;;
+3009;RIGHT ANGLE BRACKET;Pe;0;ON;;;;;Y;CLOSING ANGLE BRACKET;;;;
+300A;LEFT DOUBLE ANGLE BRACKET;Ps;0;ON;;;;;Y;OPENING DOUBLE ANGLE BRACKET;;;;
+300B;RIGHT DOUBLE ANGLE BRACKET;Pe;0;ON;;;;;Y;CLOSING DOUBLE ANGLE BRACKET;;;;
+300C;LEFT CORNER BRACKET;Ps;0;ON;;;;;Y;OPENING CORNER BRACKET;;;;
+300D;RIGHT CORNER BRACKET;Pe;0;ON;;;;;Y;CLOSING CORNER BRACKET;;;;
+300E;LEFT WHITE CORNER BRACKET;Ps;0;ON;;;;;Y;OPENING WHITE CORNER BRACKET;;;;
+300F;RIGHT WHITE CORNER BRACKET;Pe;0;ON;;;;;Y;CLOSING WHITE CORNER BRACKET;;;;
+3010;LEFT BLACK LENTICULAR BRACKET;Ps;0;ON;;;;;Y;OPENING BLACK LENTICULAR BRACKET;;;;
+3011;RIGHT BLACK LENTICULAR BRACKET;Pe;0;ON;;;;;Y;CLOSING BLACK LENTICULAR BRACKET;;;;
+3012;POSTAL MARK;So;0;ON;;;;;N;;;;;
+3013;GETA MARK;So;0;ON;;;;;N;;;;;
+3014;LEFT TORTOISE SHELL BRACKET;Ps;0;ON;;;;;Y;OPENING TORTOISE SHELL BRACKET;;;;
+3015;RIGHT TORTOISE SHELL BRACKET;Pe;0;ON;;;;;Y;CLOSING TORTOISE SHELL BRACKET;;;;
+3016;LEFT WHITE LENTICULAR BRACKET;Ps;0;ON;;;;;Y;OPENING WHITE LENTICULAR BRACKET;;;;
+3017;RIGHT WHITE LENTICULAR BRACKET;Pe;0;ON;;;;;Y;CLOSING WHITE LENTICULAR BRACKET;;;;
+3018;LEFT WHITE TORTOISE SHELL BRACKET;Ps;0;ON;;;;;Y;OPENING WHITE TORTOISE SHELL BRACKET;;;;
+3019;RIGHT WHITE TORTOISE SHELL BRACKET;Pe;0;ON;;;;;Y;CLOSING WHITE TORTOISE SHELL BRACKET;;;;
+301A;LEFT WHITE SQUARE BRACKET;Ps;0;ON;;;;;Y;OPENING WHITE SQUARE BRACKET;;;;
+301B;RIGHT WHITE SQUARE BRACKET;Pe;0;ON;;;;;Y;CLOSING WHITE SQUARE BRACKET;;;;
+301C;WAVE DASH;Pd;0;ON;;;;;N;;;;;
+301D;REVERSED DOUBLE PRIME QUOTATION MARK;Ps;0;ON;;;;;N;;;;;
+301E;DOUBLE PRIME QUOTATION MARK;Pe;0;ON;;;;;N;;;;;
+301F;LOW DOUBLE PRIME QUOTATION MARK;Pe;0;ON;;;;;N;;;;;
+3020;POSTAL MARK FACE;So;0;ON;;;;;N;;;;;
+3021;HANGZHOU NUMERAL ONE;Nl;0;L;;;;1;N;;;;;
+3022;HANGZHOU NUMERAL TWO;Nl;0;L;;;;2;N;;;;;
+3023;HANGZHOU NUMERAL THREE;Nl;0;L;;;;3;N;;;;;
+3024;HANGZHOU NUMERAL FOUR;Nl;0;L;;;;4;N;;;;;
+3025;HANGZHOU NUMERAL FIVE;Nl;0;L;;;;5;N;;;;;
+3026;HANGZHOU NUMERAL SIX;Nl;0;L;;;;6;N;;;;;
+3027;HANGZHOU NUMERAL SEVEN;Nl;0;L;;;;7;N;;;;;
+3028;HANGZHOU NUMERAL EIGHT;Nl;0;L;;;;8;N;;;;;
+3029;HANGZHOU NUMERAL NINE;Nl;0;L;;;;9;N;;;;;
+302A;IDEOGRAPHIC LEVEL TONE MARK;Mn;218;NSM;;;;;N;;;;;
+302B;IDEOGRAPHIC RISING TONE MARK;Mn;228;NSM;;;;;N;;;;;
+302C;IDEOGRAPHIC DEPARTING TONE MARK;Mn;232;NSM;;;;;N;;;;;
+302D;IDEOGRAPHIC ENTERING TONE MARK;Mn;222;NSM;;;;;N;;;;;
+302E;HANGUL SINGLE DOT TONE MARK;Mc;224;L;;;;;N;;;;;
+302F;HANGUL DOUBLE DOT TONE MARK;Mc;224;L;;;;;N;;;;;
+3030;WAVY DASH;Pd;0;ON;;;;;N;;;;;
+3031;VERTICAL KANA REPEAT MARK;Lm;0;L;;;;;N;;;;;
+3032;VERTICAL KANA REPEAT WITH VOICED SOUND MARK;Lm;0;L;;;;;N;;;;;
+3033;VERTICAL KANA REPEAT MARK UPPER HALF;Lm;0;L;;;;;N;;;;;
+3034;VERTICAL KANA REPEAT WITH VOICED SOUND MARK UPPER HALF;Lm;0;L;;;;;N;;;;;
+3035;VERTICAL KANA REPEAT MARK LOWER HALF;Lm;0;L;;;;;N;;;;;
+3036;CIRCLED POSTAL MARK;So;0;ON;<compat> 3012;;;;N;;;;;
+3037;IDEOGRAPHIC TELEGRAPH LINE FEED SEPARATOR SYMBOL;So;0;ON;;;;;N;;;;;
+3038;HANGZHOU NUMERAL TEN;Nl;0;L;<compat> 5341;;;10;N;;;;;
+3039;HANGZHOU NUMERAL TWENTY;Nl;0;L;<compat> 5344;;;20;N;;;;;
+303A;HANGZHOU NUMERAL THIRTY;Nl;0;L;<compat> 5345;;;30;N;;;;;
+303B;VERTICAL IDEOGRAPHIC ITERATION MARK;Lm;0;L;;;;;N;;;;;
+303C;MASU MARK;Lo;0;L;;;;;N;;;;;
+303D;PART ALTERNATION MARK;Po;0;ON;;;;;N;;;;;
+303E;IDEOGRAPHIC VARIATION INDICATOR;So;0;ON;;;;;N;;;;;
+303F;IDEOGRAPHIC HALF FILL SPACE;So;0;ON;;;;;N;;;;;
+3041;HIRAGANA LETTER SMALL A;Lo;0;L;;;;;N;;;;;
+3042;HIRAGANA LETTER A;Lo;0;L;;;;;N;;;;;
+3043;HIRAGANA LETTER SMALL I;Lo;0;L;;;;;N;;;;;
+3044;HIRAGANA LETTER I;Lo;0;L;;;;;N;;;;;
+3045;HIRAGANA LETTER SMALL U;Lo;0;L;;;;;N;;;;;
+3046;HIRAGANA LETTER U;Lo;0;L;;;;;N;;;;;
+3047;HIRAGANA LETTER SMALL E;Lo;0;L;;;;;N;;;;;
+3048;HIRAGANA LETTER E;Lo;0;L;;;;;N;;;;;
+3049;HIRAGANA LETTER SMALL O;Lo;0;L;;;;;N;;;;;
+304A;HIRAGANA LETTER O;Lo;0;L;;;;;N;;;;;
+304B;HIRAGANA LETTER KA;Lo;0;L;;;;;N;;;;;
+304C;HIRAGANA LETTER GA;Lo;0;L;304B 3099;;;;N;;;;;
+304D;HIRAGANA LETTER KI;Lo;0;L;;;;;N;;;;;
+304E;HIRAGANA LETTER GI;Lo;0;L;304D 3099;;;;N;;;;;
+304F;HIRAGANA LETTER KU;Lo;0;L;;;;;N;;;;;
+3050;HIRAGANA LETTER GU;Lo;0;L;304F 3099;;;;N;;;;;
+3051;HIRAGANA LETTER KE;Lo;0;L;;;;;N;;;;;
+3052;HIRAGANA LETTER GE;Lo;0;L;3051 3099;;;;N;;;;;
+3053;HIRAGANA LETTER KO;Lo;0;L;;;;;N;;;;;
+3054;HIRAGANA LETTER GO;Lo;0;L;3053 3099;;;;N;;;;;
+3055;HIRAGANA LETTER SA;Lo;0;L;;;;;N;;;;;
+3056;HIRAGANA LETTER ZA;Lo;0;L;3055 3099;;;;N;;;;;
+3057;HIRAGANA LETTER SI;Lo;0;L;;;;;N;;;;;
+3058;HIRAGANA LETTER ZI;Lo;0;L;3057 3099;;;;N;;;;;
+3059;HIRAGANA LETTER SU;Lo;0;L;;;;;N;;;;;
+305A;HIRAGANA LETTER ZU;Lo;0;L;3059 3099;;;;N;;;;;
+305B;HIRAGANA LETTER SE;Lo;0;L;;;;;N;;;;;
+305C;HIRAGANA LETTER ZE;Lo;0;L;305B 3099;;;;N;;;;;
+305D;HIRAGANA LETTER SO;Lo;0;L;;;;;N;;;;;
+305E;HIRAGANA LETTER ZO;Lo;0;L;305D 3099;;;;N;;;;;
+305F;HIRAGANA LETTER TA;Lo;0;L;;;;;N;;;;;
+3060;HIRAGANA LETTER DA;Lo;0;L;305F 3099;;;;N;;;;;
+3061;HIRAGANA LETTER TI;Lo;0;L;;;;;N;;;;;
+3062;HIRAGANA LETTER DI;Lo;0;L;3061 3099;;;;N;;;;;
+3063;HIRAGANA LETTER SMALL TU;Lo;0;L;;;;;N;;;;;
+3064;HIRAGANA LETTER TU;Lo;0;L;;;;;N;;;;;
+3065;HIRAGANA LETTER DU;Lo;0;L;3064 3099;;;;N;;;;;
+3066;HIRAGANA LETTER TE;Lo;0;L;;;;;N;;;;;
+3067;HIRAGANA LETTER DE;Lo;0;L;3066 3099;;;;N;;;;;
+3068;HIRAGANA LETTER TO;Lo;0;L;;;;;N;;;;;
+3069;HIRAGANA LETTER DO;Lo;0;L;3068 3099;;;;N;;;;;
+306A;HIRAGANA LETTER NA;Lo;0;L;;;;;N;;;;;
+306B;HIRAGANA LETTER NI;Lo;0;L;;;;;N;;;;;
+306C;HIRAGANA LETTER NU;Lo;0;L;;;;;N;;;;;
+306D;HIRAGANA LETTER NE;Lo;0;L;;;;;N;;;;;
+306E;HIRAGANA LETTER NO;Lo;0;L;;;;;N;;;;;
+306F;HIRAGANA LETTER HA;Lo;0;L;;;;;N;;;;;
+3070;HIRAGANA LETTER BA;Lo;0;L;306F 3099;;;;N;;;;;
+3071;HIRAGANA LETTER PA;Lo;0;L;306F 309A;;;;N;;;;;
+3072;HIRAGANA LETTER HI;Lo;0;L;;;;;N;;;;;
+3073;HIRAGANA LETTER BI;Lo;0;L;3072 3099;;;;N;;;;;
+3074;HIRAGANA LETTER PI;Lo;0;L;3072 309A;;;;N;;;;;
+3075;HIRAGANA LETTER HU;Lo;0;L;;;;;N;;;;;
+3076;HIRAGANA LETTER BU;Lo;0;L;3075 3099;;;;N;;;;;
+3077;HIRAGANA LETTER PU;Lo;0;L;3075 309A;;;;N;;;;;
+3078;HIRAGANA LETTER HE;Lo;0;L;;;;;N;;;;;
+3079;HIRAGANA LETTER BE;Lo;0;L;3078 3099;;;;N;;;;;
+307A;HIRAGANA LETTER PE;Lo;0;L;3078 309A;;;;N;;;;;
+307B;HIRAGANA LETTER HO;Lo;0;L;;;;;N;;;;;
+307C;HIRAGANA LETTER BO;Lo;0;L;307B 3099;;;;N;;;;;
+307D;HIRAGANA LETTER PO;Lo;0;L;307B 309A;;;;N;;;;;
+307E;HIRAGANA LETTER MA;Lo;0;L;;;;;N;;;;;
+307F;HIRAGANA LETTER MI;Lo;0;L;;;;;N;;;;;
+3080;HIRAGANA LETTER MU;Lo;0;L;;;;;N;;;;;
+3081;HIRAGANA LETTER ME;Lo;0;L;;;;;N;;;;;
+3082;HIRAGANA LETTER MO;Lo;0;L;;;;;N;;;;;
+3083;HIRAGANA LETTER SMALL YA;Lo;0;L;;;;;N;;;;;
+3084;HIRAGANA LETTER YA;Lo;0;L;;;;;N;;;;;
+3085;HIRAGANA LETTER SMALL YU;Lo;0;L;;;;;N;;;;;
+3086;HIRAGANA LETTER YU;Lo;0;L;;;;;N;;;;;
+3087;HIRAGANA LETTER SMALL YO;Lo;0;L;;;;;N;;;;;
+3088;HIRAGANA LETTER YO;Lo;0;L;;;;;N;;;;;
+3089;HIRAGANA LETTER RA;Lo;0;L;;;;;N;;;;;
+308A;HIRAGANA LETTER RI;Lo;0;L;;;;;N;;;;;
+308B;HIRAGANA LETTER RU;Lo;0;L;;;;;N;;;;;
+308C;HIRAGANA LETTER RE;Lo;0;L;;;;;N;;;;;
+308D;HIRAGANA LETTER RO;Lo;0;L;;;;;N;;;;;
+308E;HIRAGANA LETTER SMALL WA;Lo;0;L;;;;;N;;;;;
+308F;HIRAGANA LETTER WA;Lo;0;L;;;;;N;;;;;
+3090;HIRAGANA LETTER WI;Lo;0;L;;;;;N;;;;;
+3091;HIRAGANA LETTER WE;Lo;0;L;;;;;N;;;;;
+3092;HIRAGANA LETTER WO;Lo;0;L;;;;;N;;;;;
+3093;HIRAGANA LETTER N;Lo;0;L;;;;;N;;;;;
+3094;HIRAGANA LETTER VU;Lo;0;L;3046 3099;;;;N;;;;;
+3095;HIRAGANA LETTER SMALL KA;Lo;0;L;;;;;N;;;;;
+3096;HIRAGANA LETTER SMALL KE;Lo;0;L;;;;;N;;;;;
+3099;COMBINING KATAKANA-HIRAGANA VOICED SOUND MARK;Mn;8;NSM;;;;;N;NON-SPACING KATAKANA-HIRAGANA VOICED SOUND MARK;;;;
+309A;COMBINING KATAKANA-HIRAGANA SEMI-VOICED SOUND MARK;Mn;8;NSM;;;;;N;NON-SPACING KATAKANA-HIRAGANA SEMI-VOICED SOUND MARK;;;;
+309B;KATAKANA-HIRAGANA VOICED SOUND MARK;Sk;0;ON;<compat> 0020 3099;;;;N;;;;;
+309C;KATAKANA-HIRAGANA SEMI-VOICED SOUND MARK;Sk;0;ON;<compat> 0020 309A;;;;N;;;;;
+309D;HIRAGANA ITERATION MARK;Lm;0;L;;;;;N;;;;;
+309E;HIRAGANA VOICED ITERATION MARK;Lm;0;L;309D 3099;;;;N;;;;;
+309F;HIRAGANA DIGRAPH YORI;Lo;0;L;<vertical> 3088 308A;;;;N;;;;;
+30A0;KATAKANA-HIRAGANA DOUBLE HYPHEN;Pd;0;ON;;;;;N;;;;;
+30A1;KATAKANA LETTER SMALL A;Lo;0;L;;;;;N;;;;;
+30A2;KATAKANA LETTER A;Lo;0;L;;;;;N;;;;;
+30A3;KATAKANA LETTER SMALL I;Lo;0;L;;;;;N;;;;;
+30A4;KATAKANA LETTER I;Lo;0;L;;;;;N;;;;;
+30A5;KATAKANA LETTER SMALL U;Lo;0;L;;;;;N;;;;;
+30A6;KATAKANA LETTER U;Lo;0;L;;;;;N;;;;;
+30A7;KATAKANA LETTER SMALL E;Lo;0;L;;;;;N;;;;;
+30A8;KATAKANA LETTER E;Lo;0;L;;;;;N;;;;;
+30A9;KATAKANA LETTER SMALL O;Lo;0;L;;;;;N;;;;;
+30AA;KATAKANA LETTER O;Lo;0;L;;;;;N;;;;;
+30AB;KATAKANA LETTER KA;Lo;0;L;;;;;N;;;;;
+30AC;KATAKANA LETTER GA;Lo;0;L;30AB 3099;;;;N;;;;;
+30AD;KATAKANA LETTER KI;Lo;0;L;;;;;N;;;;;
+30AE;KATAKANA LETTER GI;Lo;0;L;30AD 3099;;;;N;;;;;
+30AF;KATAKANA LETTER KU;Lo;0;L;;;;;N;;;;;
+30B0;KATAKANA LETTER GU;Lo;0;L;30AF 3099;;;;N;;;;;
+30B1;KATAKANA LETTER KE;Lo;0;L;;;;;N;;;;;
+30B2;KATAKANA LETTER GE;Lo;0;L;30B1 3099;;;;N;;;;;
+30B3;KATAKANA LETTER KO;Lo;0;L;;;;;N;;;;;
+30B4;KATAKANA LETTER GO;Lo;0;L;30B3 3099;;;;N;;;;;
+30B5;KATAKANA LETTER SA;Lo;0;L;;;;;N;;;;;
+30B6;KATAKANA LETTER ZA;Lo;0;L;30B5 3099;;;;N;;;;;
+30B7;KATAKANA LETTER SI;Lo;0;L;;;;;N;;;;;
+30B8;KATAKANA LETTER ZI;Lo;0;L;30B7 3099;;;;N;;;;;
+30B9;KATAKANA LETTER SU;Lo;0;L;;;;;N;;;;;
+30BA;KATAKANA LETTER ZU;Lo;0;L;30B9 3099;;;;N;;;;;
+30BB;KATAKANA LETTER SE;Lo;0;L;;;;;N;;;;;
+30BC;KATAKANA LETTER ZE;Lo;0;L;30BB 3099;;;;N;;;;;
+30BD;KATAKANA LETTER SO;Lo;0;L;;;;;N;;;;;
+30BE;KATAKANA LETTER ZO;Lo;0;L;30BD 3099;;;;N;;;;;
+30BF;KATAKANA LETTER TA;Lo;0;L;;;;;N;;;;;
+30C0;KATAKANA LETTER DA;Lo;0;L;30BF 3099;;;;N;;;;;
+30C1;KATAKANA LETTER TI;Lo;0;L;;;;;N;;;;;
+30C2;KATAKANA LETTER DI;Lo;0;L;30C1 3099;;;;N;;;;;
+30C3;KATAKANA LETTER SMALL TU;Lo;0;L;;;;;N;;;;;
+30C4;KATAKANA LETTER TU;Lo;0;L;;;;;N;;;;;
+30C5;KATAKANA LETTER DU;Lo;0;L;30C4 3099;;;;N;;;;;
+30C6;KATAKANA LETTER TE;Lo;0;L;;;;;N;;;;;
+30C7;KATAKANA LETTER DE;Lo;0;L;30C6 3099;;;;N;;;;;
+30C8;KATAKANA LETTER TO;Lo;0;L;;;;;N;;;;;
+30C9;KATAKANA LETTER DO;Lo;0;L;30C8 3099;;;;N;;;;;
+30CA;KATAKANA LETTER NA;Lo;0;L;;;;;N;;;;;
+30CB;KATAKANA LETTER NI;Lo;0;L;;;;;N;;;;;
+30CC;KATAKANA LETTER NU;Lo;0;L;;;;;N;;;;;
+30CD;KATAKANA LETTER NE;Lo;0;L;;;;;N;;;;;
+30CE;KATAKANA LETTER NO;Lo;0;L;;;;;N;;;;;
+30CF;KATAKANA LETTER HA;Lo;0;L;;;;;N;;;;;
+30D0;KATAKANA LETTER BA;Lo;0;L;30CF 3099;;;;N;;;;;
+30D1;KATAKANA LETTER PA;Lo;0;L;30CF 309A;;;;N;;;;;
+30D2;KATAKANA LETTER HI;Lo;0;L;;;;;N;;;;;
+30D3;KATAKANA LETTER BI;Lo;0;L;30D2 3099;;;;N;;;;;
+30D4;KATAKANA LETTER PI;Lo;0;L;30D2 309A;;;;N;;;;;
+30D5;KATAKANA LETTER HU;Lo;0;L;;;;;N;;;;;
+30D6;KATAKANA LETTER BU;Lo;0;L;30D5 3099;;;;N;;;;;
+30D7;KATAKANA LETTER PU;Lo;0;L;30D5 309A;;;;N;;;;;
+30D8;KATAKANA LETTER HE;Lo;0;L;;;;;N;;;;;
+30D9;KATAKANA LETTER BE;Lo;0;L;30D8 3099;;;;N;;;;;
+30DA;KATAKANA LETTER PE;Lo;0;L;30D8 309A;;;;N;;;;;
+30DB;KATAKANA LETTER HO;Lo;0;L;;;;;N;;;;;
+30DC;KATAKANA LETTER BO;Lo;0;L;30DB 3099;;;;N;;;;;
+30DD;KATAKANA LETTER PO;Lo;0;L;30DB 309A;;;;N;;;;;
+30DE;KATAKANA LETTER MA;Lo;0;L;;;;;N;;;;;
+30DF;KATAKANA LETTER MI;Lo;0;L;;;;;N;;;;;
+30E0;KATAKANA LETTER MU;Lo;0;L;;;;;N;;;;;
+30E1;KATAKANA LETTER ME;Lo;0;L;;;;;N;;;;;
+30E2;KATAKANA LETTER MO;Lo;0;L;;;;;N;;;;;
+30E3;KATAKANA LETTER SMALL YA;Lo;0;L;;;;;N;;;;;
+30E4;KATAKANA LETTER YA;Lo;0;L;;;;;N;;;;;
+30E5;KATAKANA LETTER SMALL YU;Lo;0;L;;;;;N;;;;;
+30E6;KATAKANA LETTER YU;Lo;0;L;;;;;N;;;;;
+30E7;KATAKANA LETTER SMALL YO;Lo;0;L;;;;;N;;;;;
+30E8;KATAKANA LETTER YO;Lo;0;L;;;;;N;;;;;
+30E9;KATAKANA LETTER RA;Lo;0;L;;;;;N;;;;;
+30EA;KATAKANA LETTER RI;Lo;0;L;;;;;N;;;;;
+30EB;KATAKANA LETTER RU;Lo;0;L;;;;;N;;;;;
+30EC;KATAKANA LETTER RE;Lo;0;L;;;;;N;;;;;
+30ED;KATAKANA LETTER RO;Lo;0;L;;;;;N;;;;;
+30EE;KATAKANA LETTER SMALL WA;Lo;0;L;;;;;N;;;;;
+30EF;KATAKANA LETTER WA;Lo;0;L;;;;;N;;;;;
+30F0;KATAKANA LETTER WI;Lo;0;L;;;;;N;;;;;
+30F1;KATAKANA LETTER WE;Lo;0;L;;;;;N;;;;;
+30F2;KATAKANA LETTER WO;Lo;0;L;;;;;N;;;;;
+30F3;KATAKANA LETTER N;Lo;0;L;;;;;N;;;;;
+30F4;KATAKANA LETTER VU;Lo;0;L;30A6 3099;;;;N;;;;;
+30F5;KATAKANA LETTER SMALL KA;Lo;0;L;;;;;N;;;;;
+30F6;KATAKANA LETTER SMALL KE;Lo;0;L;;;;;N;;;;;
+30F7;KATAKANA LETTER VA;Lo;0;L;30EF 3099;;;;N;;;;;
+30F8;KATAKANA LETTER VI;Lo;0;L;30F0 3099;;;;N;;;;;
+30F9;KATAKANA LETTER VE;Lo;0;L;30F1 3099;;;;N;;;;;
+30FA;KATAKANA LETTER VO;Lo;0;L;30F2 3099;;;;N;;;;;
+30FB;KATAKANA MIDDLE DOT;Po;0;ON;;;;;N;;;;;
+30FC;KATAKANA-HIRAGANA PROLONGED SOUND MARK;Lm;0;L;;;;;N;;;;;
+30FD;KATAKANA ITERATION MARK;Lm;0;L;;;;;N;;;;;
+30FE;KATAKANA VOICED ITERATION MARK;Lm;0;L;30FD 3099;;;;N;;;;;
+30FF;KATAKANA DIGRAPH KOTO;Lo;0;L;<vertical> 30B3 30C8;;;;N;;;;;
+3105;BOPOMOFO LETTER B;Lo;0;L;;;;;N;;;;;
+3106;BOPOMOFO LETTER P;Lo;0;L;;;;;N;;;;;
+3107;BOPOMOFO LETTER M;Lo;0;L;;;;;N;;;;;
+3108;BOPOMOFO LETTER F;Lo;0;L;;;;;N;;;;;
+3109;BOPOMOFO LETTER D;Lo;0;L;;;;;N;;;;;
+310A;BOPOMOFO LETTER T;Lo;0;L;;;;;N;;;;;
+310B;BOPOMOFO LETTER N;Lo;0;L;;;;;N;;;;;
+310C;BOPOMOFO LETTER L;Lo;0;L;;;;;N;;;;;
+310D;BOPOMOFO LETTER G;Lo;0;L;;;;;N;;;;;
+310E;BOPOMOFO LETTER K;Lo;0;L;;;;;N;;;;;
+310F;BOPOMOFO LETTER H;Lo;0;L;;;;;N;;;;;
+3110;BOPOMOFO LETTER J;Lo;0;L;;;;;N;;;;;
+3111;BOPOMOFO LETTER Q;Lo;0;L;;;;;N;;;;;
+3112;BOPOMOFO LETTER X;Lo;0;L;;;;;N;;;;;
+3113;BOPOMOFO LETTER ZH;Lo;0;L;;;;;N;;;;;
+3114;BOPOMOFO LETTER CH;Lo;0;L;;;;;N;;;;;
+3115;BOPOMOFO LETTER SH;Lo;0;L;;;;;N;;;;;
+3116;BOPOMOFO LETTER R;Lo;0;L;;;;;N;;;;;
+3117;BOPOMOFO LETTER Z;Lo;0;L;;;;;N;;;;;
+3118;BOPOMOFO LETTER C;Lo;0;L;;;;;N;;;;;
+3119;BOPOMOFO LETTER S;Lo;0;L;;;;;N;;;;;
+311A;BOPOMOFO LETTER A;Lo;0;L;;;;;N;;;;;
+311B;BOPOMOFO LETTER O;Lo;0;L;;;;;N;;;;;
+311C;BOPOMOFO LETTER E;Lo;0;L;;;;;N;;;;;
+311D;BOPOMOFO LETTER EH;Lo;0;L;;;;;N;;;;;
+311E;BOPOMOFO LETTER AI;Lo;0;L;;;;;N;;;;;
+311F;BOPOMOFO LETTER EI;Lo;0;L;;;;;N;;;;;
+3120;BOPOMOFO LETTER AU;Lo;0;L;;;;;N;;;;;
+3121;BOPOMOFO LETTER OU;Lo;0;L;;;;;N;;;;;
+3122;BOPOMOFO LETTER AN;Lo;0;L;;;;;N;;;;;
+3123;BOPOMOFO LETTER EN;Lo;0;L;;;;;N;;;;;
+3124;BOPOMOFO LETTER ANG;Lo;0;L;;;;;N;;;;;
+3125;BOPOMOFO LETTER ENG;Lo;0;L;;;;;N;;;;;
+3126;BOPOMOFO LETTER ER;Lo;0;L;;;;;N;;;;;
+3127;BOPOMOFO LETTER I;Lo;0;L;;;;;N;;;;;
+3128;BOPOMOFO LETTER U;Lo;0;L;;;;;N;;;;;
+3129;BOPOMOFO LETTER IU;Lo;0;L;;;;;N;;;;;
+312A;BOPOMOFO LETTER V;Lo;0;L;;;;;N;;;;;
+312B;BOPOMOFO LETTER NG;Lo;0;L;;;;;N;;;;;
+312C;BOPOMOFO LETTER GN;Lo;0;L;;;;;N;;;;;
+312D;BOPOMOFO LETTER IH;Lo;0;L;;;;;N;;;;;
+3131;HANGUL LETTER KIYEOK;Lo;0;L;<compat> 1100;;;;N;HANGUL LETTER GIYEOG;;;;
+3132;HANGUL LETTER SSANGKIYEOK;Lo;0;L;<compat> 1101;;;;N;HANGUL LETTER SSANG GIYEOG;;;;
+3133;HANGUL LETTER KIYEOK-SIOS;Lo;0;L;<compat> 11AA;;;;N;HANGUL LETTER GIYEOG SIOS;;;;
+3134;HANGUL LETTER NIEUN;Lo;0;L;<compat> 1102;;;;N;;;;;
+3135;HANGUL LETTER NIEUN-CIEUC;Lo;0;L;<compat> 11AC;;;;N;HANGUL LETTER NIEUN JIEUJ;;;;
+3136;HANGUL LETTER NIEUN-HIEUH;Lo;0;L;<compat> 11AD;;;;N;HANGUL LETTER NIEUN HIEUH;;;;
+3137;HANGUL LETTER TIKEUT;Lo;0;L;<compat> 1103;;;;N;HANGUL LETTER DIGEUD;;;;
+3138;HANGUL LETTER SSANGTIKEUT;Lo;0;L;<compat> 1104;;;;N;HANGUL LETTER SSANG DIGEUD;;;;
+3139;HANGUL LETTER RIEUL;Lo;0;L;<compat> 1105;;;;N;HANGUL LETTER LIEUL;;;;
+313A;HANGUL LETTER RIEUL-KIYEOK;Lo;0;L;<compat> 11B0;;;;N;HANGUL LETTER LIEUL GIYEOG;;;;
+313B;HANGUL LETTER RIEUL-MIEUM;Lo;0;L;<compat> 11B1;;;;N;HANGUL LETTER LIEUL MIEUM;;;;
+313C;HANGUL LETTER RIEUL-PIEUP;Lo;0;L;<compat> 11B2;;;;N;HANGUL LETTER LIEUL BIEUB;;;;
+313D;HANGUL LETTER RIEUL-SIOS;Lo;0;L;<compat> 11B3;;;;N;HANGUL LETTER LIEUL SIOS;;;;
+313E;HANGUL LETTER RIEUL-THIEUTH;Lo;0;L;<compat> 11B4;;;;N;HANGUL LETTER LIEUL TIEUT;;;;
+313F;HANGUL LETTER RIEUL-PHIEUPH;Lo;0;L;<compat> 11B5;;;;N;HANGUL LETTER LIEUL PIEUP;;;;
+3140;HANGUL LETTER RIEUL-HIEUH;Lo;0;L;<compat> 111A;;;;N;HANGUL LETTER LIEUL HIEUH;;;;
+3141;HANGUL LETTER MIEUM;Lo;0;L;<compat> 1106;;;;N;;;;;
+3142;HANGUL LETTER PIEUP;Lo;0;L;<compat> 1107;;;;N;HANGUL LETTER BIEUB;;;;
+3143;HANGUL LETTER SSANGPIEUP;Lo;0;L;<compat> 1108;;;;N;HANGUL LETTER SSANG BIEUB;;;;
+3144;HANGUL LETTER PIEUP-SIOS;Lo;0;L;<compat> 1121;;;;N;HANGUL LETTER BIEUB SIOS;;;;
+3145;HANGUL LETTER SIOS;Lo;0;L;<compat> 1109;;;;N;;;;;
+3146;HANGUL LETTER SSANGSIOS;Lo;0;L;<compat> 110A;;;;N;HANGUL LETTER SSANG SIOS;;;;
+3147;HANGUL LETTER IEUNG;Lo;0;L;<compat> 110B;;;;N;;;;;
+3148;HANGUL LETTER CIEUC;Lo;0;L;<compat> 110C;;;;N;HANGUL LETTER JIEUJ;;;;
+3149;HANGUL LETTER SSANGCIEUC;Lo;0;L;<compat> 110D;;;;N;HANGUL LETTER SSANG JIEUJ;;;;
+314A;HANGUL LETTER CHIEUCH;Lo;0;L;<compat> 110E;;;;N;HANGUL LETTER CIEUC;;;;
+314B;HANGUL LETTER KHIEUKH;Lo;0;L;<compat> 110F;;;;N;HANGUL LETTER KIYEOK;;;;
+314C;HANGUL LETTER THIEUTH;Lo;0;L;<compat> 1110;;;;N;HANGUL LETTER TIEUT;;;;
+314D;HANGUL LETTER PHIEUPH;Lo;0;L;<compat> 1111;;;;N;HANGUL LETTER PIEUP;;;;
+314E;HANGUL LETTER HIEUH;Lo;0;L;<compat> 1112;;;;N;;;;;
+314F;HANGUL LETTER A;Lo;0;L;<compat> 1161;;;;N;;;;;
+3150;HANGUL LETTER AE;Lo;0;L;<compat> 1162;;;;N;;;;;
+3151;HANGUL LETTER YA;Lo;0;L;<compat> 1163;;;;N;;;;;
+3152;HANGUL LETTER YAE;Lo;0;L;<compat> 1164;;;;N;;;;;
+3153;HANGUL LETTER EO;Lo;0;L;<compat> 1165;;;;N;;;;;
+3154;HANGUL LETTER E;Lo;0;L;<compat> 1166;;;;N;;;;;
+3155;HANGUL LETTER YEO;Lo;0;L;<compat> 1167;;;;N;;;;;
+3156;HANGUL LETTER YE;Lo;0;L;<compat> 1168;;;;N;;;;;
+3157;HANGUL LETTER O;Lo;0;L;<compat> 1169;;;;N;;;;;
+3158;HANGUL LETTER WA;Lo;0;L;<compat> 116A;;;;N;;;;;
+3159;HANGUL LETTER WAE;Lo;0;L;<compat> 116B;;;;N;;;;;
+315A;HANGUL LETTER OE;Lo;0;L;<compat> 116C;;;;N;;;;;
+315B;HANGUL LETTER YO;Lo;0;L;<compat> 116D;;;;N;;;;;
+315C;HANGUL LETTER U;Lo;0;L;<compat> 116E;;;;N;;;;;
+315D;HANGUL LETTER WEO;Lo;0;L;<compat> 116F;;;;N;;;;;
+315E;HANGUL LETTER WE;Lo;0;L;<compat> 1170;;;;N;;;;;
+315F;HANGUL LETTER WI;Lo;0;L;<compat> 1171;;;;N;;;;;
+3160;HANGUL LETTER YU;Lo;0;L;<compat> 1172;;;;N;;;;;
+3161;HANGUL LETTER EU;Lo;0;L;<compat> 1173;;;;N;;;;;
+3162;HANGUL LETTER YI;Lo;0;L;<compat> 1174;;;;N;;;;;
+3163;HANGUL LETTER I;Lo;0;L;<compat> 1175;;;;N;;;;;
+3164;HANGUL FILLER;Lo;0;L;<compat> 1160;;;;N;HANGUL CAE OM;;;;
+3165;HANGUL LETTER SSANGNIEUN;Lo;0;L;<compat> 1114;;;;N;HANGUL LETTER SSANG NIEUN;;;;
+3166;HANGUL LETTER NIEUN-TIKEUT;Lo;0;L;<compat> 1115;;;;N;HANGUL LETTER NIEUN DIGEUD;;;;
+3167;HANGUL LETTER NIEUN-SIOS;Lo;0;L;<compat> 11C7;;;;N;HANGUL LETTER NIEUN SIOS;;;;
+3168;HANGUL LETTER NIEUN-PANSIOS;Lo;0;L;<compat> 11C8;;;;N;HANGUL LETTER NIEUN BAN CHI EUM;;;;
+3169;HANGUL LETTER RIEUL-KIYEOK-SIOS;Lo;0;L;<compat> 11CC;;;;N;HANGUL LETTER LIEUL GIYEOG SIOS;;;;
+316A;HANGUL LETTER RIEUL-TIKEUT;Lo;0;L;<compat> 11CE;;;;N;HANGUL LETTER LIEUL DIGEUD;;;;
+316B;HANGUL LETTER RIEUL-PIEUP-SIOS;Lo;0;L;<compat> 11D3;;;;N;HANGUL LETTER LIEUL BIEUB SIOS;;;;
+316C;HANGUL LETTER RIEUL-PANSIOS;Lo;0;L;<compat> 11D7;;;;N;HANGUL LETTER LIEUL BAN CHI EUM;;;;
+316D;HANGUL LETTER RIEUL-YEORINHIEUH;Lo;0;L;<compat> 11D9;;;;N;HANGUL LETTER LIEUL YEOLIN HIEUH;;;;
+316E;HANGUL LETTER MIEUM-PIEUP;Lo;0;L;<compat> 111C;;;;N;HANGUL LETTER MIEUM BIEUB;;;;
+316F;HANGUL LETTER MIEUM-SIOS;Lo;0;L;<compat> 11DD;;;;N;HANGUL LETTER MIEUM SIOS;;;;
+3170;HANGUL LETTER MIEUM-PANSIOS;Lo;0;L;<compat> 11DF;;;;N;HANGUL LETTER BIEUB BAN CHI EUM;;;;
+3171;HANGUL LETTER KAPYEOUNMIEUM;Lo;0;L;<compat> 111D;;;;N;HANGUL LETTER MIEUM SUN GYEONG EUM;;;;
+3172;HANGUL LETTER PIEUP-KIYEOK;Lo;0;L;<compat> 111E;;;;N;HANGUL LETTER BIEUB GIYEOG;;;;
+3173;HANGUL LETTER PIEUP-TIKEUT;Lo;0;L;<compat> 1120;;;;N;HANGUL LETTER BIEUB DIGEUD;;;;
+3174;HANGUL LETTER PIEUP-SIOS-KIYEOK;Lo;0;L;<compat> 1122;;;;N;HANGUL LETTER BIEUB SIOS GIYEOG;;;;
+3175;HANGUL LETTER PIEUP-SIOS-TIKEUT;Lo;0;L;<compat> 1123;;;;N;HANGUL LETTER BIEUB SIOS DIGEUD;;;;
+3176;HANGUL LETTER PIEUP-CIEUC;Lo;0;L;<compat> 1127;;;;N;HANGUL LETTER BIEUB JIEUJ;;;;
+3177;HANGUL LETTER PIEUP-THIEUTH;Lo;0;L;<compat> 1129;;;;N;HANGUL LETTER BIEUB TIEUT;;;;
+3178;HANGUL LETTER KAPYEOUNPIEUP;Lo;0;L;<compat> 112B;;;;N;HANGUL LETTER BIEUB SUN GYEONG EUM;;;;
+3179;HANGUL LETTER KAPYEOUNSSANGPIEUP;Lo;0;L;<compat> 112C;;;;N;HANGUL LETTER SSANG BIEUB SUN GYEONG EUM;;;;
+317A;HANGUL LETTER SIOS-KIYEOK;Lo;0;L;<compat> 112D;;;;N;HANGUL LETTER SIOS GIYEOG;;;;
+317B;HANGUL LETTER SIOS-NIEUN;Lo;0;L;<compat> 112E;;;;N;HANGUL LETTER SIOS NIEUN;;;;
+317C;HANGUL LETTER SIOS-TIKEUT;Lo;0;L;<compat> 112F;;;;N;HANGUL LETTER SIOS DIGEUD;;;;
+317D;HANGUL LETTER SIOS-PIEUP;Lo;0;L;<compat> 1132;;;;N;HANGUL LETTER SIOS BIEUB;;;;
+317E;HANGUL LETTER SIOS-CIEUC;Lo;0;L;<compat> 1136;;;;N;HANGUL LETTER SIOS JIEUJ;;;;
+317F;HANGUL LETTER PANSIOS;Lo;0;L;<compat> 1140;;;;N;HANGUL LETTER BAN CHI EUM;;;;
+3180;HANGUL LETTER SSANGIEUNG;Lo;0;L;<compat> 1147;;;;N;HANGUL LETTER SSANG IEUNG;;;;
+3181;HANGUL LETTER YESIEUNG;Lo;0;L;<compat> 114C;;;;N;HANGUL LETTER NGIEUNG;;;;
+3182;HANGUL LETTER YESIEUNG-SIOS;Lo;0;L;<compat> 11F1;;;;N;HANGUL LETTER NGIEUNG SIOS;;;;
+3183;HANGUL LETTER YESIEUNG-PANSIOS;Lo;0;L;<compat> 11F2;;;;N;HANGUL LETTER NGIEUNG BAN CHI EUM;;;;
+3184;HANGUL LETTER KAPYEOUNPHIEUPH;Lo;0;L;<compat> 1157;;;;N;HANGUL LETTER PIEUP SUN GYEONG EUM;;;;
+3185;HANGUL LETTER SSANGHIEUH;Lo;0;L;<compat> 1158;;;;N;HANGUL LETTER SSANG HIEUH;;;;
+3186;HANGUL LETTER YEORINHIEUH;Lo;0;L;<compat> 1159;;;;N;HANGUL LETTER YEOLIN HIEUH;;;;
+3187;HANGUL LETTER YO-YA;Lo;0;L;<compat> 1184;;;;N;HANGUL LETTER YOYA;;;;
+3188;HANGUL LETTER YO-YAE;Lo;0;L;<compat> 1185;;;;N;HANGUL LETTER YOYAE;;;;
+3189;HANGUL LETTER YO-I;Lo;0;L;<compat> 1188;;;;N;HANGUL LETTER YOI;;;;
+318A;HANGUL LETTER YU-YEO;Lo;0;L;<compat> 1191;;;;N;HANGUL LETTER YUYEO;;;;
+318B;HANGUL LETTER YU-YE;Lo;0;L;<compat> 1192;;;;N;HANGUL LETTER YUYE;;;;
+318C;HANGUL LETTER YU-I;Lo;0;L;<compat> 1194;;;;N;HANGUL LETTER YUI;;;;
+318D;HANGUL LETTER ARAEA;Lo;0;L;<compat> 119E;;;;N;HANGUL LETTER ALAE A;;;;
+318E;HANGUL LETTER ARAEAE;Lo;0;L;<compat> 11A1;;;;N;HANGUL LETTER ALAE AE;;;;
+3190;IDEOGRAPHIC ANNOTATION LINKING MARK;So;0;L;;;;;N;KANBUN TATETEN;;;;
+3191;IDEOGRAPHIC ANNOTATION REVERSE MARK;So;0;L;;;;;N;KAERITEN RE;;;;
+3192;IDEOGRAPHIC ANNOTATION ONE MARK;No;0;L;<super> 4E00;;;1;N;KAERITEN ITI;;;;
+3193;IDEOGRAPHIC ANNOTATION TWO MARK;No;0;L;<super> 4E8C;;;2;N;KAERITEN NI;;;;
+3194;IDEOGRAPHIC ANNOTATION THREE MARK;No;0;L;<super> 4E09;;;3;N;KAERITEN SAN;;;;
+3195;IDEOGRAPHIC ANNOTATION FOUR MARK;No;0;L;<super> 56DB;;;4;N;KAERITEN SI;;;;
+3196;IDEOGRAPHIC ANNOTATION TOP MARK;So;0;L;<super> 4E0A;;;;N;KAERITEN ZYOU;;;;
+3197;IDEOGRAPHIC ANNOTATION MIDDLE MARK;So;0;L;<super> 4E2D;;;;N;KAERITEN TYUU;;;;
+3198;IDEOGRAPHIC ANNOTATION BOTTOM MARK;So;0;L;<super> 4E0B;;;;N;KAERITEN GE;;;;
+3199;IDEOGRAPHIC ANNOTATION FIRST MARK;So;0;L;<super> 7532;;;;N;KAERITEN KOU;;;;
+319A;IDEOGRAPHIC ANNOTATION SECOND MARK;So;0;L;<super> 4E59;;;;N;KAERITEN OTU;;;;
+319B;IDEOGRAPHIC ANNOTATION THIRD MARK;So;0;L;<super> 4E19;;;;N;KAERITEN HEI;;;;
+319C;IDEOGRAPHIC ANNOTATION FOURTH MARK;So;0;L;<super> 4E01;;;;N;KAERITEN TEI;;;;
+319D;IDEOGRAPHIC ANNOTATION HEAVEN MARK;So;0;L;<super> 5929;;;;N;KAERITEN TEN;;;;
+319E;IDEOGRAPHIC ANNOTATION EARTH MARK;So;0;L;<super> 5730;;;;N;KAERITEN TI;;;;
+319F;IDEOGRAPHIC ANNOTATION MAN MARK;So;0;L;<super> 4EBA;;;;N;KAERITEN ZIN;;;;
+31A0;BOPOMOFO LETTER BU;Lo;0;L;;;;;N;;;;;
+31A1;BOPOMOFO LETTER ZI;Lo;0;L;;;;;N;;;;;
+31A2;BOPOMOFO LETTER JI;Lo;0;L;;;;;N;;;;;
+31A3;BOPOMOFO LETTER GU;Lo;0;L;;;;;N;;;;;
+31A4;BOPOMOFO LETTER EE;Lo;0;L;;;;;N;;;;;
+31A5;BOPOMOFO LETTER ENN;Lo;0;L;;;;;N;;;;;
+31A6;BOPOMOFO LETTER OO;Lo;0;L;;;;;N;;;;;
+31A7;BOPOMOFO LETTER ONN;Lo;0;L;;;;;N;;;;;
+31A8;BOPOMOFO LETTER IR;Lo;0;L;;;;;N;;;;;
+31A9;BOPOMOFO LETTER ANN;Lo;0;L;;;;;N;;;;;
+31AA;BOPOMOFO LETTER INN;Lo;0;L;;;;;N;;;;;
+31AB;BOPOMOFO LETTER UNN;Lo;0;L;;;;;N;;;;;
+31AC;BOPOMOFO LETTER IM;Lo;0;L;;;;;N;;;;;
+31AD;BOPOMOFO LETTER NGG;Lo;0;L;;;;;N;;;;;
+31AE;BOPOMOFO LETTER AINN;Lo;0;L;;;;;N;;;;;
+31AF;BOPOMOFO LETTER AUNN;Lo;0;L;;;;;N;;;;;
+31B0;BOPOMOFO LETTER AM;Lo;0;L;;;;;N;;;;;
+31B1;BOPOMOFO LETTER OM;Lo;0;L;;;;;N;;;;;
+31B2;BOPOMOFO LETTER ONG;Lo;0;L;;;;;N;;;;;
+31B3;BOPOMOFO LETTER INNN;Lo;0;L;;;;;N;;;;;
+31B4;BOPOMOFO FINAL LETTER P;Lo;0;L;;;;;N;;;;;
+31B5;BOPOMOFO FINAL LETTER T;Lo;0;L;;;;;N;;;;;
+31B6;BOPOMOFO FINAL LETTER K;Lo;0;L;;;;;N;;;;;
+31B7;BOPOMOFO FINAL LETTER H;Lo;0;L;;;;;N;;;;;
+31B8;BOPOMOFO LETTER GH;Lo;0;L;;;;;N;;;;;
+31B9;BOPOMOFO LETTER LH;Lo;0;L;;;;;N;;;;;
+31BA;BOPOMOFO LETTER ZY;Lo;0;L;;;;;N;;;;;
+31C0;CJK STROKE T;So;0;ON;;;;;N;;;;;
+31C1;CJK STROKE WG;So;0;ON;;;;;N;;;;;
+31C2;CJK STROKE XG;So;0;ON;;;;;N;;;;;
+31C3;CJK STROKE BXG;So;0;ON;;;;;N;;;;;
+31C4;CJK STROKE SW;So;0;ON;;;;;N;;;;;
+31C5;CJK STROKE HZZ;So;0;ON;;;;;N;;;;;
+31C6;CJK STROKE HZG;So;0;ON;;;;;N;;;;;
+31C7;CJK STROKE HP;So;0;ON;;;;;N;;;;;
+31C8;CJK STROKE HZWG;So;0;ON;;;;;N;;;;;
+31C9;CJK STROKE SZWG;So;0;ON;;;;;N;;;;;
+31CA;CJK STROKE HZT;So;0;ON;;;;;N;;;;;
+31CB;CJK STROKE HZZP;So;0;ON;;;;;N;;;;;
+31CC;CJK STROKE HPWG;So;0;ON;;;;;N;;;;;
+31CD;CJK STROKE HZW;So;0;ON;;;;;N;;;;;
+31CE;CJK STROKE HZZZ;So;0;ON;;;;;N;;;;;
+31CF;CJK STROKE N;So;0;ON;;;;;N;;;;;
+31D0;CJK STROKE H;So;0;ON;;;;;N;;;;;
+31D1;CJK STROKE S;So;0;ON;;;;;N;;;;;
+31D2;CJK STROKE P;So;0;ON;;;;;N;;;;;
+31D3;CJK STROKE SP;So;0;ON;;;;;N;;;;;
+31D4;CJK STROKE D;So;0;ON;;;;;N;;;;;
+31D5;CJK STROKE HZ;So;0;ON;;;;;N;;;;;
+31D6;CJK STROKE HG;So;0;ON;;;;;N;;;;;
+31D7;CJK STROKE SZ;So;0;ON;;;;;N;;;;;
+31D8;CJK STROKE SWZ;So;0;ON;;;;;N;;;;;
+31D9;CJK STROKE ST;So;0;ON;;;;;N;;;;;
+31DA;CJK STROKE SG;So;0;ON;;;;;N;;;;;
+31DB;CJK STROKE PD;So;0;ON;;;;;N;;;;;
+31DC;CJK STROKE PZ;So;0;ON;;;;;N;;;;;
+31DD;CJK STROKE TN;So;0;ON;;;;;N;;;;;
+31DE;CJK STROKE SZZ;So;0;ON;;;;;N;;;;;
+31DF;CJK STROKE SWG;So;0;ON;;;;;N;;;;;
+31E0;CJK STROKE HXWG;So;0;ON;;;;;N;;;;;
+31E1;CJK STROKE HZZZG;So;0;ON;;;;;N;;;;;
+31E2;CJK STROKE PG;So;0;ON;;;;;N;;;;;
+31E3;CJK STROKE Q;So;0;ON;;;;;N;;;;;
+31F0;KATAKANA LETTER SMALL KU;Lo;0;L;;;;;N;;;;;
+31F1;KATAKANA LETTER SMALL SI;Lo;0;L;;;;;N;;;;;
+31F2;KATAKANA LETTER SMALL SU;Lo;0;L;;;;;N;;;;;
+31F3;KATAKANA LETTER SMALL TO;Lo;0;L;;;;;N;;;;;
+31F4;KATAKANA LETTER SMALL NU;Lo;0;L;;;;;N;;;;;
+31F5;KATAKANA LETTER SMALL HA;Lo;0;L;;;;;N;;;;;
+31F6;KATAKANA LETTER SMALL HI;Lo;0;L;;;;;N;;;;;
+31F7;KATAKANA LETTER SMALL HU;Lo;0;L;;;;;N;;;;;
+31F8;KATAKANA LETTER SMALL HE;Lo;0;L;;;;;N;;;;;
+31F9;KATAKANA LETTER SMALL HO;Lo;0;L;;;;;N;;;;;
+31FA;KATAKANA LETTER SMALL MU;Lo;0;L;;;;;N;;;;;
+31FB;KATAKANA LETTER SMALL RA;Lo;0;L;;;;;N;;;;;
+31FC;KATAKANA LETTER SMALL RI;Lo;0;L;;;;;N;;;;;
+31FD;KATAKANA LETTER SMALL RU;Lo;0;L;;;;;N;;;;;
+31FE;KATAKANA LETTER SMALL RE;Lo;0;L;;;;;N;;;;;
+31FF;KATAKANA LETTER SMALL RO;Lo;0;L;;;;;N;;;;;
+3200;PARENTHESIZED HANGUL KIYEOK;So;0;L;<compat> 0028 1100 0029;;;;N;PARENTHESIZED HANGUL GIYEOG;;;;
+3201;PARENTHESIZED HANGUL NIEUN;So;0;L;<compat> 0028 1102 0029;;;;N;;;;;
+3202;PARENTHESIZED HANGUL TIKEUT;So;0;L;<compat> 0028 1103 0029;;;;N;PARENTHESIZED HANGUL DIGEUD;;;;
+3203;PARENTHESIZED HANGUL RIEUL;So;0;L;<compat> 0028 1105 0029;;;;N;PARENTHESIZED HANGUL LIEUL;;;;
+3204;PARENTHESIZED HANGUL MIEUM;So;0;L;<compat> 0028 1106 0029;;;;N;;;;;
+3205;PARENTHESIZED HANGUL PIEUP;So;0;L;<compat> 0028 1107 0029;;;;N;PARENTHESIZED HANGUL BIEUB;;;;
+3206;PARENTHESIZED HANGUL SIOS;So;0;L;<compat> 0028 1109 0029;;;;N;;;;;
+3207;PARENTHESIZED HANGUL IEUNG;So;0;L;<compat> 0028 110B 0029;;;;N;;;;;
+3208;PARENTHESIZED HANGUL CIEUC;So;0;L;<compat> 0028 110C 0029;;;;N;PARENTHESIZED HANGUL JIEUJ;;;;
+3209;PARENTHESIZED HANGUL CHIEUCH;So;0;L;<compat> 0028 110E 0029;;;;N;PARENTHESIZED HANGUL CIEUC;;;;
+320A;PARENTHESIZED HANGUL KHIEUKH;So;0;L;<compat> 0028 110F 0029;;;;N;PARENTHESIZED HANGUL KIYEOK;;;;
+320B;PARENTHESIZED HANGUL THIEUTH;So;0;L;<compat> 0028 1110 0029;;;;N;PARENTHESIZED HANGUL TIEUT;;;;
+320C;PARENTHESIZED HANGUL PHIEUPH;So;0;L;<compat> 0028 1111 0029;;;;N;PARENTHESIZED HANGUL PIEUP;;;;
+320D;PARENTHESIZED HANGUL HIEUH;So;0;L;<compat> 0028 1112 0029;;;;N;;;;;
+320E;PARENTHESIZED HANGUL KIYEOK A;So;0;L;<compat> 0028 1100 1161 0029;;;;N;PARENTHESIZED HANGUL GA;;;;
+320F;PARENTHESIZED HANGUL NIEUN A;So;0;L;<compat> 0028 1102 1161 0029;;;;N;PARENTHESIZED HANGUL NA;;;;
+3210;PARENTHESIZED HANGUL TIKEUT A;So;0;L;<compat> 0028 1103 1161 0029;;;;N;PARENTHESIZED HANGUL DA;;;;
+3211;PARENTHESIZED HANGUL RIEUL A;So;0;L;<compat> 0028 1105 1161 0029;;;;N;PARENTHESIZED HANGUL LA;;;;
+3212;PARENTHESIZED HANGUL MIEUM A;So;0;L;<compat> 0028 1106 1161 0029;;;;N;PARENTHESIZED HANGUL MA;;;;
+3213;PARENTHESIZED HANGUL PIEUP A;So;0;L;<compat> 0028 1107 1161 0029;;;;N;PARENTHESIZED HANGUL BA;;;;
+3214;PARENTHESIZED HANGUL SIOS A;So;0;L;<compat> 0028 1109 1161 0029;;;;N;PARENTHESIZED HANGUL SA;;;;
+3215;PARENTHESIZED HANGUL IEUNG A;So;0;L;<compat> 0028 110B 1161 0029;;;;N;PARENTHESIZED HANGUL A;;;;
+3216;PARENTHESIZED HANGUL CIEUC A;So;0;L;<compat> 0028 110C 1161 0029;;;;N;PARENTHESIZED HANGUL JA;;;;
+3217;PARENTHESIZED HANGUL CHIEUCH A;So;0;L;<compat> 0028 110E 1161 0029;;;;N;PARENTHESIZED HANGUL CA;;;;
+3218;PARENTHESIZED HANGUL KHIEUKH A;So;0;L;<compat> 0028 110F 1161 0029;;;;N;PARENTHESIZED HANGUL KA;;;;
+3219;PARENTHESIZED HANGUL THIEUTH A;So;0;L;<compat> 0028 1110 1161 0029;;;;N;PARENTHESIZED HANGUL TA;;;;
+321A;PARENTHESIZED HANGUL PHIEUPH A;So;0;L;<compat> 0028 1111 1161 0029;;;;N;PARENTHESIZED HANGUL PA;;;;
+321B;PARENTHESIZED HANGUL HIEUH A;So;0;L;<compat> 0028 1112 1161 0029;;;;N;PARENTHESIZED HANGUL HA;;;;
+321C;PARENTHESIZED HANGUL CIEUC U;So;0;L;<compat> 0028 110C 116E 0029;;;;N;PARENTHESIZED HANGUL JU;;;;
+321D;PARENTHESIZED KOREAN CHARACTER OJEON;So;0;ON;<compat> 0028 110B 1169 110C 1165 11AB 0029;;;;N;;;;;
+321E;PARENTHESIZED KOREAN CHARACTER O HU;So;0;ON;<compat> 0028 110B 1169 1112 116E 0029;;;;N;;;;;
+3220;PARENTHESIZED IDEOGRAPH ONE;No;0;L;<compat> 0028 4E00 0029;;;1;N;;;;;
+3221;PARENTHESIZED IDEOGRAPH TWO;No;0;L;<compat> 0028 4E8C 0029;;;2;N;;;;;
+3222;PARENTHESIZED IDEOGRAPH THREE;No;0;L;<compat> 0028 4E09 0029;;;3;N;;;;;
+3223;PARENTHESIZED IDEOGRAPH FOUR;No;0;L;<compat> 0028 56DB 0029;;;4;N;;;;;
+3224;PARENTHESIZED IDEOGRAPH FIVE;No;0;L;<compat> 0028 4E94 0029;;;5;N;;;;;
+3225;PARENTHESIZED IDEOGRAPH SIX;No;0;L;<compat> 0028 516D 0029;;;6;N;;;;;
+3226;PARENTHESIZED IDEOGRAPH SEVEN;No;0;L;<compat> 0028 4E03 0029;;;7;N;;;;;
+3227;PARENTHESIZED IDEOGRAPH EIGHT;No;0;L;<compat> 0028 516B 0029;;;8;N;;;;;
+3228;PARENTHESIZED IDEOGRAPH NINE;No;0;L;<compat> 0028 4E5D 0029;;;9;N;;;;;
+3229;PARENTHESIZED IDEOGRAPH TEN;No;0;L;<compat> 0028 5341 0029;;;10;N;;;;;
+322A;PARENTHESIZED IDEOGRAPH MOON;So;0;L;<compat> 0028 6708 0029;;;;N;;;;;
+322B;PARENTHESIZED IDEOGRAPH FIRE;So;0;L;<compat> 0028 706B 0029;;;;N;;;;;
+322C;PARENTHESIZED IDEOGRAPH WATER;So;0;L;<compat> 0028 6C34 0029;;;;N;;;;;
+322D;PARENTHESIZED IDEOGRAPH WOOD;So;0;L;<compat> 0028 6728 0029;;;;N;;;;;
+322E;PARENTHESIZED IDEOGRAPH METAL;So;0;L;<compat> 0028 91D1 0029;;;;N;;;;;
+322F;PARENTHESIZED IDEOGRAPH EARTH;So;0;L;<compat> 0028 571F 0029;;;;N;;;;;
+3230;PARENTHESIZED IDEOGRAPH SUN;So;0;L;<compat> 0028 65E5 0029;;;;N;;;;;
+3231;PARENTHESIZED IDEOGRAPH STOCK;So;0;L;<compat> 0028 682A 0029;;;;N;;;;;
+3232;PARENTHESIZED IDEOGRAPH HAVE;So;0;L;<compat> 0028 6709 0029;;;;N;;;;;
+3233;PARENTHESIZED IDEOGRAPH SOCIETY;So;0;L;<compat> 0028 793E 0029;;;;N;;;;;
+3234;PARENTHESIZED IDEOGRAPH NAME;So;0;L;<compat> 0028 540D 0029;;;;N;;;;;
+3235;PARENTHESIZED IDEOGRAPH SPECIAL;So;0;L;<compat> 0028 7279 0029;;;;N;;;;;
+3236;PARENTHESIZED IDEOGRAPH FINANCIAL;So;0;L;<compat> 0028 8CA1 0029;;;;N;;;;;
+3237;PARENTHESIZED IDEOGRAPH CONGRATULATION;So;0;L;<compat> 0028 795D 0029;;;;N;;;;;
+3238;PARENTHESIZED IDEOGRAPH LABOR;So;0;L;<compat> 0028 52B4 0029;;;;N;;;;;
+3239;PARENTHESIZED IDEOGRAPH REPRESENT;So;0;L;<compat> 0028 4EE3 0029;;;;N;;;;;
+323A;PARENTHESIZED IDEOGRAPH CALL;So;0;L;<compat> 0028 547C 0029;;;;N;;;;;
+323B;PARENTHESIZED IDEOGRAPH STUDY;So;0;L;<compat> 0028 5B66 0029;;;;N;;;;;
+323C;PARENTHESIZED IDEOGRAPH SUPERVISE;So;0;L;<compat> 0028 76E3 0029;;;;N;;;;;
+323D;PARENTHESIZED IDEOGRAPH ENTERPRISE;So;0;L;<compat> 0028 4F01 0029;;;;N;;;;;
+323E;PARENTHESIZED IDEOGRAPH RESOURCE;So;0;L;<compat> 0028 8CC7 0029;;;;N;;;;;
+323F;PARENTHESIZED IDEOGRAPH ALLIANCE;So;0;L;<compat> 0028 5354 0029;;;;N;;;;;
+3240;PARENTHESIZED IDEOGRAPH FESTIVAL;So;0;L;<compat> 0028 796D 0029;;;;N;;;;;
+3241;PARENTHESIZED IDEOGRAPH REST;So;0;L;<compat> 0028 4F11 0029;;;;N;;;;;
+3242;PARENTHESIZED IDEOGRAPH SELF;So;0;L;<compat> 0028 81EA 0029;;;;N;;;;;
+3243;PARENTHESIZED IDEOGRAPH REACH;So;0;L;<compat> 0028 81F3 0029;;;;N;;;;;
+3244;CIRCLED IDEOGRAPH QUESTION;So;0;L;<circle> 554F;;;;N;;;;;
+3245;CIRCLED IDEOGRAPH KINDERGARTEN;So;0;L;<circle> 5E7C;;;;N;;;;;
+3246;CIRCLED IDEOGRAPH SCHOOL;So;0;L;<circle> 6587;;;;N;;;;;
+3247;CIRCLED IDEOGRAPH KOTO;So;0;L;<circle> 7B8F;;;;N;;;;;
+3248;CIRCLED NUMBER TEN ON BLACK SQUARE;No;0;L;;;;10;N;;;;;
+3249;CIRCLED NUMBER TWENTY ON BLACK SQUARE;No;0;L;;;;20;N;;;;;
+324A;CIRCLED NUMBER THIRTY ON BLACK SQUARE;No;0;L;;;;30;N;;;;;
+324B;CIRCLED NUMBER FORTY ON BLACK SQUARE;No;0;L;;;;40;N;;;;;
+324C;CIRCLED NUMBER FIFTY ON BLACK SQUARE;No;0;L;;;;50;N;;;;;
+324D;CIRCLED NUMBER SIXTY ON BLACK SQUARE;No;0;L;;;;60;N;;;;;
+324E;CIRCLED NUMBER SEVENTY ON BLACK SQUARE;No;0;L;;;;70;N;;;;;
+324F;CIRCLED NUMBER EIGHTY ON BLACK SQUARE;No;0;L;;;;80;N;;;;;
+3250;PARTNERSHIP SIGN;So;0;ON;<square> 0050 0054 0045;;;;N;;;;;
+3251;CIRCLED NUMBER TWENTY ONE;No;0;ON;<circle> 0032 0031;;;21;N;;;;;
+3252;CIRCLED NUMBER TWENTY TWO;No;0;ON;<circle> 0032 0032;;;22;N;;;;;
+3253;CIRCLED NUMBER TWENTY THREE;No;0;ON;<circle> 0032 0033;;;23;N;;;;;
+3254;CIRCLED NUMBER TWENTY FOUR;No;0;ON;<circle> 0032 0034;;;24;N;;;;;
+3255;CIRCLED NUMBER TWENTY FIVE;No;0;ON;<circle> 0032 0035;;;25;N;;;;;
+3256;CIRCLED NUMBER TWENTY SIX;No;0;ON;<circle> 0032 0036;;;26;N;;;;;
+3257;CIRCLED NUMBER TWENTY SEVEN;No;0;ON;<circle> 0032 0037;;;27;N;;;;;
+3258;CIRCLED NUMBER TWENTY EIGHT;No;0;ON;<circle> 0032 0038;;;28;N;;;;;
+3259;CIRCLED NUMBER TWENTY NINE;No;0;ON;<circle> 0032 0039;;;29;N;;;;;
+325A;CIRCLED NUMBER THIRTY;No;0;ON;<circle> 0033 0030;;;30;N;;;;;
+325B;CIRCLED NUMBER THIRTY ONE;No;0;ON;<circle> 0033 0031;;;31;N;;;;;
+325C;CIRCLED NUMBER THIRTY TWO;No;0;ON;<circle> 0033 0032;;;32;N;;;;;
+325D;CIRCLED NUMBER THIRTY THREE;No;0;ON;<circle> 0033 0033;;;33;N;;;;;
+325E;CIRCLED NUMBER THIRTY FOUR;No;0;ON;<circle> 0033 0034;;;34;N;;;;;
+325F;CIRCLED NUMBER THIRTY FIVE;No;0;ON;<circle> 0033 0035;;;35;N;;;;;
+3260;CIRCLED HANGUL KIYEOK;So;0;L;<circle> 1100;;;;N;CIRCLED HANGUL GIYEOG;;;;
+3261;CIRCLED HANGUL NIEUN;So;0;L;<circle> 1102;;;;N;;;;;
+3262;CIRCLED HANGUL TIKEUT;So;0;L;<circle> 1103;;;;N;CIRCLED HANGUL DIGEUD;;;;
+3263;CIRCLED HANGUL RIEUL;So;0;L;<circle> 1105;;;;N;CIRCLED HANGUL LIEUL;;;;
+3264;CIRCLED HANGUL MIEUM;So;0;L;<circle> 1106;;;;N;;;;;
+3265;CIRCLED HANGUL PIEUP;So;0;L;<circle> 1107;;;;N;CIRCLED HANGUL BIEUB;;;;
+3266;CIRCLED HANGUL SIOS;So;0;L;<circle> 1109;;;;N;;;;;
+3267;CIRCLED HANGUL IEUNG;So;0;L;<circle> 110B;;;;N;;;;;
+3268;CIRCLED HANGUL CIEUC;So;0;L;<circle> 110C;;;;N;CIRCLED HANGUL JIEUJ;;;;
+3269;CIRCLED HANGUL CHIEUCH;So;0;L;<circle> 110E;;;;N;CIRCLED HANGUL CIEUC;;;;
+326A;CIRCLED HANGUL KHIEUKH;So;0;L;<circle> 110F;;;;N;CIRCLED HANGUL KIYEOK;;;;
+326B;CIRCLED HANGUL THIEUTH;So;0;L;<circle> 1110;;;;N;CIRCLED HANGUL TIEUT;;;;
+326C;CIRCLED HANGUL PHIEUPH;So;0;L;<circle> 1111;;;;N;CIRCLED HANGUL PIEUP;;;;
+326D;CIRCLED HANGUL HIEUH;So;0;L;<circle> 1112;;;;N;;;;;
+326E;CIRCLED HANGUL KIYEOK A;So;0;L;<circle> 1100 1161;;;;N;CIRCLED HANGUL GA;;;;
+326F;CIRCLED HANGUL NIEUN A;So;0;L;<circle> 1102 1161;;;;N;CIRCLED HANGUL NA;;;;
+3270;CIRCLED HANGUL TIKEUT A;So;0;L;<circle> 1103 1161;;;;N;CIRCLED HANGUL DA;;;;
+3271;CIRCLED HANGUL RIEUL A;So;0;L;<circle> 1105 1161;;;;N;CIRCLED HANGUL LA;;;;
+3272;CIRCLED HANGUL MIEUM A;So;0;L;<circle> 1106 1161;;;;N;CIRCLED HANGUL MA;;;;
+3273;CIRCLED HANGUL PIEUP A;So;0;L;<circle> 1107 1161;;;;N;CIRCLED HANGUL BA;;;;
+3274;CIRCLED HANGUL SIOS A;So;0;L;<circle> 1109 1161;;;;N;CIRCLED HANGUL SA;;;;
+3275;CIRCLED HANGUL IEUNG A;So;0;L;<circle> 110B 1161;;;;N;CIRCLED HANGUL A;;;;
+3276;CIRCLED HANGUL CIEUC A;So;0;L;<circle> 110C 1161;;;;N;CIRCLED HANGUL JA;;;;
+3277;CIRCLED HANGUL CHIEUCH A;So;0;L;<circle> 110E 1161;;;;N;CIRCLED HANGUL CA;;;;
+3278;CIRCLED HANGUL KHIEUKH A;So;0;L;<circle> 110F 1161;;;;N;CIRCLED HANGUL KA;;;;
+3279;CIRCLED HANGUL THIEUTH A;So;0;L;<circle> 1110 1161;;;;N;CIRCLED HANGUL TA;;;;
+327A;CIRCLED HANGUL PHIEUPH A;So;0;L;<circle> 1111 1161;;;;N;CIRCLED HANGUL PA;;;;
+327B;CIRCLED HANGUL HIEUH A;So;0;L;<circle> 1112 1161;;;;N;CIRCLED HANGUL HA;;;;
+327C;CIRCLED KOREAN CHARACTER CHAMKO;So;0;ON;<circle> 110E 1161 11B7 1100 1169;;;;N;;;;;
+327D;CIRCLED KOREAN CHARACTER JUEUI;So;0;ON;<circle> 110C 116E 110B 1174;;;;N;;;;;
+327E;CIRCLED HANGUL IEUNG U;So;0;ON;<circle> 110B 116E;;;;N;;;;;
+327F;KOREAN STANDARD SYMBOL;So;0;L;;;;;N;;;;;
+3280;CIRCLED IDEOGRAPH ONE;No;0;L;<circle> 4E00;;;1;N;;;;;
+3281;CIRCLED IDEOGRAPH TWO;No;0;L;<circle> 4E8C;;;2;N;;;;;
+3282;CIRCLED IDEOGRAPH THREE;No;0;L;<circle> 4E09;;;3;N;;;;;
+3283;CIRCLED IDEOGRAPH FOUR;No;0;L;<circle> 56DB;;;4;N;;;;;
+3284;CIRCLED IDEOGRAPH FIVE;No;0;L;<circle> 4E94;;;5;N;;;;;
+3285;CIRCLED IDEOGRAPH SIX;No;0;L;<circle> 516D;;;6;N;;;;;
+3286;CIRCLED IDEOGRAPH SEVEN;No;0;L;<circle> 4E03;;;7;N;;;;;
+3287;CIRCLED IDEOGRAPH EIGHT;No;0;L;<circle> 516B;;;8;N;;;;;
+3288;CIRCLED IDEOGRAPH NINE;No;0;L;<circle> 4E5D;;;9;N;;;;;
+3289;CIRCLED IDEOGRAPH TEN;No;0;L;<circle> 5341;;;10;N;;;;;
+328A;CIRCLED IDEOGRAPH MOON;So;0;L;<circle> 6708;;;;N;;;;;
+328B;CIRCLED IDEOGRAPH FIRE;So;0;L;<circle> 706B;;;;N;;;;;
+328C;CIRCLED IDEOGRAPH WATER;So;0;L;<circle> 6C34;;;;N;;;;;
+328D;CIRCLED IDEOGRAPH WOOD;So;0;L;<circle> 6728;;;;N;;;;;
+328E;CIRCLED IDEOGRAPH METAL;So;0;L;<circle> 91D1;;;;N;;;;;
+328F;CIRCLED IDEOGRAPH EARTH;So;0;L;<circle> 571F;;;;N;;;;;
+3290;CIRCLED IDEOGRAPH SUN;So;0;L;<circle> 65E5;;;;N;;;;;
+3291;CIRCLED IDEOGRAPH STOCK;So;0;L;<circle> 682A;;;;N;;;;;
+3292;CIRCLED IDEOGRAPH HAVE;So;0;L;<circle> 6709;;;;N;;;;;
+3293;CIRCLED IDEOGRAPH SOCIETY;So;0;L;<circle> 793E;;;;N;;;;;
+3294;CIRCLED IDEOGRAPH NAME;So;0;L;<circle> 540D;;;;N;;;;;
+3295;CIRCLED IDEOGRAPH SPECIAL;So;0;L;<circle> 7279;;;;N;;;;;
+3296;CIRCLED IDEOGRAPH FINANCIAL;So;0;L;<circle> 8CA1;;;;N;;;;;
+3297;CIRCLED IDEOGRAPH CONGRATULATION;So;0;L;<circle> 795D;;;;N;;;;;
+3298;CIRCLED IDEOGRAPH LABOR;So;0;L;<circle> 52B4;;;;N;;;;;
+3299;CIRCLED IDEOGRAPH SECRET;So;0;L;<circle> 79D8;;;;N;;;;;
+329A;CIRCLED IDEOGRAPH MALE;So;0;L;<circle> 7537;;;;N;;;;;
+329B;CIRCLED IDEOGRAPH FEMALE;So;0;L;<circle> 5973;;;;N;;;;;
+329C;CIRCLED IDEOGRAPH SUITABLE;So;0;L;<circle> 9069;;;;N;;;;;
+329D;CIRCLED IDEOGRAPH EXCELLENT;So;0;L;<circle> 512A;;;;N;;;;;
+329E;CIRCLED IDEOGRAPH PRINT;So;0;L;<circle> 5370;;;;N;;;;;
+329F;CIRCLED IDEOGRAPH ATTENTION;So;0;L;<circle> 6CE8;;;;N;;;;;
+32A0;CIRCLED IDEOGRAPH ITEM;So;0;L;<circle> 9805;;;;N;;;;;
+32A1;CIRCLED IDEOGRAPH REST;So;0;L;<circle> 4F11;;;;N;;;;;
+32A2;CIRCLED IDEOGRAPH COPY;So;0;L;<circle> 5199;;;;N;;;;;
+32A3;CIRCLED IDEOGRAPH CORRECT;So;0;L;<circle> 6B63;;;;N;;;;;
+32A4;CIRCLED IDEOGRAPH HIGH;So;0;L;<circle> 4E0A;;;;N;;;;;
+32A5;CIRCLED IDEOGRAPH CENTRE;So;0;L;<circle> 4E2D;;;;N;CIRCLED IDEOGRAPH CENTER;;;;
+32A6;CIRCLED IDEOGRAPH LOW;So;0;L;<circle> 4E0B;;;;N;;;;;
+32A7;CIRCLED IDEOGRAPH LEFT;So;0;L;<circle> 5DE6;;;;N;;;;;
+32A8;CIRCLED IDEOGRAPH RIGHT;So;0;L;<circle> 53F3;;;;N;;;;;
+32A9;CIRCLED IDEOGRAPH MEDICINE;So;0;L;<circle> 533B;;;;N;;;;;
+32AA;CIRCLED IDEOGRAPH RELIGION;So;0;L;<circle> 5B97;;;;N;;;;;
+32AB;CIRCLED IDEOGRAPH STUDY;So;0;L;<circle> 5B66;;;;N;;;;;
+32AC;CIRCLED IDEOGRAPH SUPERVISE;So;0;L;<circle> 76E3;;;;N;;;;;
+32AD;CIRCLED IDEOGRAPH ENTERPRISE;So;0;L;<circle> 4F01;;;;N;;;;;
+32AE;CIRCLED IDEOGRAPH RESOURCE;So;0;L;<circle> 8CC7;;;;N;;;;;
+32AF;CIRCLED IDEOGRAPH ALLIANCE;So;0;L;<circle> 5354;;;;N;;;;;
+32B0;CIRCLED IDEOGRAPH NIGHT;So;0;L;<circle> 591C;;;;N;;;;;
+32B1;CIRCLED NUMBER THIRTY SIX;No;0;ON;<circle> 0033 0036;;;36;N;;;;;
+32B2;CIRCLED NUMBER THIRTY SEVEN;No;0;ON;<circle> 0033 0037;;;37;N;;;;;
+32B3;CIRCLED NUMBER THIRTY EIGHT;No;0;ON;<circle> 0033 0038;;;38;N;;;;;
+32B4;CIRCLED NUMBER THIRTY NINE;No;0;ON;<circle> 0033 0039;;;39;N;;;;;
+32B5;CIRCLED NUMBER FORTY;No;0;ON;<circle> 0034 0030;;;40;N;;;;;
+32B6;CIRCLED NUMBER FORTY ONE;No;0;ON;<circle> 0034 0031;;;41;N;;;;;
+32B7;CIRCLED NUMBER FORTY TWO;No;0;ON;<circle> 0034 0032;;;42;N;;;;;
+32B8;CIRCLED NUMBER FORTY THREE;No;0;ON;<circle> 0034 0033;;;43;N;;;;;
+32B9;CIRCLED NUMBER FORTY FOUR;No;0;ON;<circle> 0034 0034;;;44;N;;;;;
+32BA;CIRCLED NUMBER FORTY FIVE;No;0;ON;<circle> 0034 0035;;;45;N;;;;;
+32BB;CIRCLED NUMBER FORTY SIX;No;0;ON;<circle> 0034 0036;;;46;N;;;;;
+32BC;CIRCLED NUMBER FORTY SEVEN;No;0;ON;<circle> 0034 0037;;;47;N;;;;;
+32BD;CIRCLED NUMBER FORTY EIGHT;No;0;ON;<circle> 0034 0038;;;48;N;;;;;
+32BE;CIRCLED NUMBER FORTY NINE;No;0;ON;<circle> 0034 0039;;;49;N;;;;;
+32BF;CIRCLED NUMBER FIFTY;No;0;ON;<circle> 0035 0030;;;50;N;;;;;
+32C0;IDEOGRAPHIC TELEGRAPH SYMBOL FOR JANUARY;So;0;L;<compat> 0031 6708;;;;N;;;;;
+32C1;IDEOGRAPHIC TELEGRAPH SYMBOL FOR FEBRUARY;So;0;L;<compat> 0032 6708;;;;N;;;;;
+32C2;IDEOGRAPHIC TELEGRAPH SYMBOL FOR MARCH;So;0;L;<compat> 0033 6708;;;;N;;;;;
+32C3;IDEOGRAPHIC TELEGRAPH SYMBOL FOR APRIL;So;0;L;<compat> 0034 6708;;;;N;;;;;
+32C4;IDEOGRAPHIC TELEGRAPH SYMBOL FOR MAY;So;0;L;<compat> 0035 6708;;;;N;;;;;
+32C5;IDEOGRAPHIC TELEGRAPH SYMBOL FOR JUNE;So;0;L;<compat> 0036 6708;;;;N;;;;;
+32C6;IDEOGRAPHIC TELEGRAPH SYMBOL FOR JULY;So;0;L;<compat> 0037 6708;;;;N;;;;;
+32C7;IDEOGRAPHIC TELEGRAPH SYMBOL FOR AUGUST;So;0;L;<compat> 0038 6708;;;;N;;;;;
+32C8;IDEOGRAPHIC TELEGRAPH SYMBOL FOR SEPTEMBER;So;0;L;<compat> 0039 6708;;;;N;;;;;
+32C9;IDEOGRAPHIC TELEGRAPH SYMBOL FOR OCTOBER;So;0;L;<compat> 0031 0030 6708;;;;N;;;;;
+32CA;IDEOGRAPHIC TELEGRAPH SYMBOL FOR NOVEMBER;So;0;L;<compat> 0031 0031 6708;;;;N;;;;;
+32CB;IDEOGRAPHIC TELEGRAPH SYMBOL FOR DECEMBER;So;0;L;<compat> 0031 0032 6708;;;;N;;;;;
+32CC;SQUARE HG;So;0;ON;<square> 0048 0067;;;;N;;;;;
+32CD;SQUARE ERG;So;0;ON;<square> 0065 0072 0067;;;;N;;;;;
+32CE;SQUARE EV;So;0;ON;<square> 0065 0056;;;;N;;;;;
+32CF;LIMITED LIABILITY SIGN;So;0;ON;<square> 004C 0054 0044;;;;N;;;;;
+32D0;CIRCLED KATAKANA A;So;0;L;<circle> 30A2;;;;N;;;;;
+32D1;CIRCLED KATAKANA I;So;0;L;<circle> 30A4;;;;N;;;;;
+32D2;CIRCLED KATAKANA U;So;0;L;<circle> 30A6;;;;N;;;;;
+32D3;CIRCLED KATAKANA E;So;0;L;<circle> 30A8;;;;N;;;;;
+32D4;CIRCLED KATAKANA O;So;0;L;<circle> 30AA;;;;N;;;;;
+32D5;CIRCLED KATAKANA KA;So;0;L;<circle> 30AB;;;;N;;;;;
+32D6;CIRCLED KATAKANA KI;So;0;L;<circle> 30AD;;;;N;;;;;
+32D7;CIRCLED KATAKANA KU;So;0;L;<circle> 30AF;;;;N;;;;;
+32D8;CIRCLED KATAKANA KE;So;0;L;<circle> 30B1;;;;N;;;;;
+32D9;CIRCLED KATAKANA KO;So;0;L;<circle> 30B3;;;;N;;;;;
+32DA;CIRCLED KATAKANA SA;So;0;L;<circle> 30B5;;;;N;;;;;
+32DB;CIRCLED KATAKANA SI;So;0;L;<circle> 30B7;;;;N;;;;;
+32DC;CIRCLED KATAKANA SU;So;0;L;<circle> 30B9;;;;N;;;;;
+32DD;CIRCLED KATAKANA SE;So;0;L;<circle> 30BB;;;;N;;;;;
+32DE;CIRCLED KATAKANA SO;So;0;L;<circle> 30BD;;;;N;;;;;
+32DF;CIRCLED KATAKANA TA;So;0;L;<circle> 30BF;;;;N;;;;;
+32E0;CIRCLED KATAKANA TI;So;0;L;<circle> 30C1;;;;N;;;;;
+32E1;CIRCLED KATAKANA TU;So;0;L;<circle> 30C4;;;;N;;;;;
+32E2;CIRCLED KATAKANA TE;So;0;L;<circle> 30C6;;;;N;;;;;
+32E3;CIRCLED KATAKANA TO;So;0;L;<circle> 30C8;;;;N;;;;;
+32E4;CIRCLED KATAKANA NA;So;0;L;<circle> 30CA;;;;N;;;;;
+32E5;CIRCLED KATAKANA NI;So;0;L;<circle> 30CB;;;;N;;;;;
+32E6;CIRCLED KATAKANA NU;So;0;L;<circle> 30CC;;;;N;;;;;
+32E7;CIRCLED KATAKANA NE;So;0;L;<circle> 30CD;;;;N;;;;;
+32E8;CIRCLED KATAKANA NO;So;0;L;<circle> 30CE;;;;N;;;;;
+32E9;CIRCLED KATAKANA HA;So;0;L;<circle> 30CF;;;;N;;;;;
+32EA;CIRCLED KATAKANA HI;So;0;L;<circle> 30D2;;;;N;;;;;
+32EB;CIRCLED KATAKANA HU;So;0;L;<circle> 30D5;;;;N;;;;;
+32EC;CIRCLED KATAKANA HE;So;0;L;<circle> 30D8;;;;N;;;;;
+32ED;CIRCLED KATAKANA HO;So;0;L;<circle> 30DB;;;;N;;;;;
+32EE;CIRCLED KATAKANA MA;So;0;L;<circle> 30DE;;;;N;;;;;
+32EF;CIRCLED KATAKANA MI;So;0;L;<circle> 30DF;;;;N;;;;;
+32F0;CIRCLED KATAKANA MU;So;0;L;<circle> 30E0;;;;N;;;;;
+32F1;CIRCLED KATAKANA ME;So;0;L;<circle> 30E1;;;;N;;;;;
+32F2;CIRCLED KATAKANA MO;So;0;L;<circle> 30E2;;;;N;;;;;
+32F3;CIRCLED KATAKANA YA;So;0;L;<circle> 30E4;;;;N;;;;;
+32F4;CIRCLED KATAKANA YU;So;0;L;<circle> 30E6;;;;N;;;;;
+32F5;CIRCLED KATAKANA YO;So;0;L;<circle> 30E8;;;;N;;;;;
+32F6;CIRCLED KATAKANA RA;So;0;L;<circle> 30E9;;;;N;;;;;
+32F7;CIRCLED KATAKANA RI;So;0;L;<circle> 30EA;;;;N;;;;;
+32F8;CIRCLED KATAKANA RU;So;0;L;<circle> 30EB;;;;N;;;;;
+32F9;CIRCLED KATAKANA RE;So;0;L;<circle> 30EC;;;;N;;;;;
+32FA;CIRCLED KATAKANA RO;So;0;L;<circle> 30ED;;;;N;;;;;
+32FB;CIRCLED KATAKANA WA;So;0;L;<circle> 30EF;;;;N;;;;;
+32FC;CIRCLED KATAKANA WI;So;0;L;<circle> 30F0;;;;N;;;;;
+32FD;CIRCLED KATAKANA WE;So;0;L;<circle> 30F1;;;;N;;;;;
+32FE;CIRCLED KATAKANA WO;So;0;L;<circle> 30F2;;;;N;;;;;
+3300;SQUARE APAATO;So;0;L;<square> 30A2 30D1 30FC 30C8;;;;N;SQUARED APAATO;;;;
+3301;SQUARE ARUHUA;So;0;L;<square> 30A2 30EB 30D5 30A1;;;;N;SQUARED ARUHUA;;;;
+3302;SQUARE ANPEA;So;0;L;<square> 30A2 30F3 30DA 30A2;;;;N;SQUARED ANPEA;;;;
+3303;SQUARE AARU;So;0;L;<square> 30A2 30FC 30EB;;;;N;SQUARED AARU;;;;
+3304;SQUARE ININGU;So;0;L;<square> 30A4 30CB 30F3 30B0;;;;N;SQUARED ININGU;;;;
+3305;SQUARE INTI;So;0;L;<square> 30A4 30F3 30C1;;;;N;SQUARED INTI;;;;
+3306;SQUARE UON;So;0;L;<square> 30A6 30A9 30F3;;;;N;SQUARED UON;;;;
+3307;SQUARE ESUKUUDO;So;0;L;<square> 30A8 30B9 30AF 30FC 30C9;;;;N;SQUARED ESUKUUDO;;;;
+3308;SQUARE EEKAA;So;0;L;<square> 30A8 30FC 30AB 30FC;;;;N;SQUARED EEKAA;;;;
+3309;SQUARE ONSU;So;0;L;<square> 30AA 30F3 30B9;;;;N;SQUARED ONSU;;;;
+330A;SQUARE OOMU;So;0;L;<square> 30AA 30FC 30E0;;;;N;SQUARED OOMU;;;;
+330B;SQUARE KAIRI;So;0;L;<square> 30AB 30A4 30EA;;;;N;SQUARED KAIRI;;;;
+330C;SQUARE KARATTO;So;0;L;<square> 30AB 30E9 30C3 30C8;;;;N;SQUARED KARATTO;;;;
+330D;SQUARE KARORII;So;0;L;<square> 30AB 30ED 30EA 30FC;;;;N;SQUARED KARORII;;;;
+330E;SQUARE GARON;So;0;L;<square> 30AC 30ED 30F3;;;;N;SQUARED GARON;;;;
+330F;SQUARE GANMA;So;0;L;<square> 30AC 30F3 30DE;;;;N;SQUARED GANMA;;;;
+3310;SQUARE GIGA;So;0;L;<square> 30AE 30AC;;;;N;SQUARED GIGA;;;;
+3311;SQUARE GINII;So;0;L;<square> 30AE 30CB 30FC;;;;N;SQUARED GINII;;;;
+3312;SQUARE KYURII;So;0;L;<square> 30AD 30E5 30EA 30FC;;;;N;SQUARED KYURII;;;;
+3313;SQUARE GIRUDAA;So;0;L;<square> 30AE 30EB 30C0 30FC;;;;N;SQUARED GIRUDAA;;;;
+3314;SQUARE KIRO;So;0;L;<square> 30AD 30ED;;;;N;SQUARED KIRO;;;;
+3315;SQUARE KIROGURAMU;So;0;L;<square> 30AD 30ED 30B0 30E9 30E0;;;;N;SQUARED KIROGURAMU;;;;
+3316;SQUARE KIROMEETORU;So;0;L;<square> 30AD 30ED 30E1 30FC 30C8 30EB;;;;N;SQUARED KIROMEETORU;;;;
+3317;SQUARE KIROWATTO;So;0;L;<square> 30AD 30ED 30EF 30C3 30C8;;;;N;SQUARED KIROWATTO;;;;
+3318;SQUARE GURAMU;So;0;L;<square> 30B0 30E9 30E0;;;;N;SQUARED GURAMU;;;;
+3319;SQUARE GURAMUTON;So;0;L;<square> 30B0 30E9 30E0 30C8 30F3;;;;N;SQUARED GURAMUTON;;;;
+331A;SQUARE KURUZEIRO;So;0;L;<square> 30AF 30EB 30BC 30A4 30ED;;;;N;SQUARED KURUZEIRO;;;;
+331B;SQUARE KUROONE;So;0;L;<square> 30AF 30ED 30FC 30CD;;;;N;SQUARED KUROONE;;;;
+331C;SQUARE KEESU;So;0;L;<square> 30B1 30FC 30B9;;;;N;SQUARED KEESU;;;;
+331D;SQUARE KORUNA;So;0;L;<square> 30B3 30EB 30CA;;;;N;SQUARED KORUNA;;;;
+331E;SQUARE KOOPO;So;0;L;<square> 30B3 30FC 30DD;;;;N;SQUARED KOOPO;;;;
+331F;SQUARE SAIKURU;So;0;L;<square> 30B5 30A4 30AF 30EB;;;;N;SQUARED SAIKURU;;;;
+3320;SQUARE SANTIIMU;So;0;L;<square> 30B5 30F3 30C1 30FC 30E0;;;;N;SQUARED SANTIIMU;;;;
+3321;SQUARE SIRINGU;So;0;L;<square> 30B7 30EA 30F3 30B0;;;;N;SQUARED SIRINGU;;;;
+3322;SQUARE SENTI;So;0;L;<square> 30BB 30F3 30C1;;;;N;SQUARED SENTI;;;;
+3323;SQUARE SENTO;So;0;L;<square> 30BB 30F3 30C8;;;;N;SQUARED SENTO;;;;
+3324;SQUARE DAASU;So;0;L;<square> 30C0 30FC 30B9;;;;N;SQUARED DAASU;;;;
+3325;SQUARE DESI;So;0;L;<square> 30C7 30B7;;;;N;SQUARED DESI;;;;
+3326;SQUARE DORU;So;0;L;<square> 30C9 30EB;;;;N;SQUARED DORU;;;;
+3327;SQUARE TON;So;0;L;<square> 30C8 30F3;;;;N;SQUARED TON;;;;
+3328;SQUARE NANO;So;0;L;<square> 30CA 30CE;;;;N;SQUARED NANO;;;;
+3329;SQUARE NOTTO;So;0;L;<square> 30CE 30C3 30C8;;;;N;SQUARED NOTTO;;;;
+332A;SQUARE HAITU;So;0;L;<square> 30CF 30A4 30C4;;;;N;SQUARED HAITU;;;;
+332B;SQUARE PAASENTO;So;0;L;<square> 30D1 30FC 30BB 30F3 30C8;;;;N;SQUARED PAASENTO;;;;
+332C;SQUARE PAATU;So;0;L;<square> 30D1 30FC 30C4;;;;N;SQUARED PAATU;;;;
+332D;SQUARE BAARERU;So;0;L;<square> 30D0 30FC 30EC 30EB;;;;N;SQUARED BAARERU;;;;
+332E;SQUARE PIASUTORU;So;0;L;<square> 30D4 30A2 30B9 30C8 30EB;;;;N;SQUARED PIASUTORU;;;;
+332F;SQUARE PIKURU;So;0;L;<square> 30D4 30AF 30EB;;;;N;SQUARED PIKURU;;;;
+3330;SQUARE PIKO;So;0;L;<square> 30D4 30B3;;;;N;SQUARED PIKO;;;;
+3331;SQUARE BIRU;So;0;L;<square> 30D3 30EB;;;;N;SQUARED BIRU;;;;
+3332;SQUARE HUARADDO;So;0;L;<square> 30D5 30A1 30E9 30C3 30C9;;;;N;SQUARED HUARADDO;;;;
+3333;SQUARE HUIITO;So;0;L;<square> 30D5 30A3 30FC 30C8;;;;N;SQUARED HUIITO;;;;
+3334;SQUARE BUSSYERU;So;0;L;<square> 30D6 30C3 30B7 30A7 30EB;;;;N;SQUARED BUSSYERU;;;;
+3335;SQUARE HURAN;So;0;L;<square> 30D5 30E9 30F3;;;;N;SQUARED HURAN;;;;
+3336;SQUARE HEKUTAARU;So;0;L;<square> 30D8 30AF 30BF 30FC 30EB;;;;N;SQUARED HEKUTAARU;;;;
+3337;SQUARE PESO;So;0;L;<square> 30DA 30BD;;;;N;SQUARED PESO;;;;
+3338;SQUARE PENIHI;So;0;L;<square> 30DA 30CB 30D2;;;;N;SQUARED PENIHI;;;;
+3339;SQUARE HERUTU;So;0;L;<square> 30D8 30EB 30C4;;;;N;SQUARED HERUTU;;;;
+333A;SQUARE PENSU;So;0;L;<square> 30DA 30F3 30B9;;;;N;SQUARED PENSU;;;;
+333B;SQUARE PEEZI;So;0;L;<square> 30DA 30FC 30B8;;;;N;SQUARED PEEZI;;;;
+333C;SQUARE BEETA;So;0;L;<square> 30D9 30FC 30BF;;;;N;SQUARED BEETA;;;;
+333D;SQUARE POINTO;So;0;L;<square> 30DD 30A4 30F3 30C8;;;;N;SQUARED POINTO;;;;
+333E;SQUARE BORUTO;So;0;L;<square> 30DC 30EB 30C8;;;;N;SQUARED BORUTO;;;;
+333F;SQUARE HON;So;0;L;<square> 30DB 30F3;;;;N;SQUARED HON;;;;
+3340;SQUARE PONDO;So;0;L;<square> 30DD 30F3 30C9;;;;N;SQUARED PONDO;;;;
+3341;SQUARE HOORU;So;0;L;<square> 30DB 30FC 30EB;;;;N;SQUARED HOORU;;;;
+3342;SQUARE HOON;So;0;L;<square> 30DB 30FC 30F3;;;;N;SQUARED HOON;;;;
+3343;SQUARE MAIKURO;So;0;L;<square> 30DE 30A4 30AF 30ED;;;;N;SQUARED MAIKURO;;;;
+3344;SQUARE MAIRU;So;0;L;<square> 30DE 30A4 30EB;;;;N;SQUARED MAIRU;;;;
+3345;SQUARE MAHHA;So;0;L;<square> 30DE 30C3 30CF;;;;N;SQUARED MAHHA;;;;
+3346;SQUARE MARUKU;So;0;L;<square> 30DE 30EB 30AF;;;;N;SQUARED MARUKU;;;;
+3347;SQUARE MANSYON;So;0;L;<square> 30DE 30F3 30B7 30E7 30F3;;;;N;SQUARED MANSYON;;;;
+3348;SQUARE MIKURON;So;0;L;<square> 30DF 30AF 30ED 30F3;;;;N;SQUARED MIKURON;;;;
+3349;SQUARE MIRI;So;0;L;<square> 30DF 30EA;;;;N;SQUARED MIRI;;;;
+334A;SQUARE MIRIBAARU;So;0;L;<square> 30DF 30EA 30D0 30FC 30EB;;;;N;SQUARED MIRIBAARU;;;;
+334B;SQUARE MEGA;So;0;L;<square> 30E1 30AC;;;;N;SQUARED MEGA;;;;
+334C;SQUARE MEGATON;So;0;L;<square> 30E1 30AC 30C8 30F3;;;;N;SQUARED MEGATON;;;;
+334D;SQUARE MEETORU;So;0;L;<square> 30E1 30FC 30C8 30EB;;;;N;SQUARED MEETORU;;;;
+334E;SQUARE YAADO;So;0;L;<square> 30E4 30FC 30C9;;;;N;SQUARED YAADO;;;;
+334F;SQUARE YAARU;So;0;L;<square> 30E4 30FC 30EB;;;;N;SQUARED YAARU;;;;
+3350;SQUARE YUAN;So;0;L;<square> 30E6 30A2 30F3;;;;N;SQUARED YUAN;;;;
+3351;SQUARE RITTORU;So;0;L;<square> 30EA 30C3 30C8 30EB;;;;N;SQUARED RITTORU;;;;
+3352;SQUARE RIRA;So;0;L;<square> 30EA 30E9;;;;N;SQUARED RIRA;;;;
+3353;SQUARE RUPII;So;0;L;<square> 30EB 30D4 30FC;;;;N;SQUARED RUPII;;;;
+3354;SQUARE RUUBURU;So;0;L;<square> 30EB 30FC 30D6 30EB;;;;N;SQUARED RUUBURU;;;;
+3355;SQUARE REMU;So;0;L;<square> 30EC 30E0;;;;N;SQUARED REMU;;;;
+3356;SQUARE RENTOGEN;So;0;L;<square> 30EC 30F3 30C8 30B2 30F3;;;;N;SQUARED RENTOGEN;;;;
+3357;SQUARE WATTO;So;0;L;<square> 30EF 30C3 30C8;;;;N;SQUARED WATTO;;;;
+3358;IDEOGRAPHIC TELEGRAPH SYMBOL FOR HOUR ZERO;So;0;L;<compat> 0030 70B9;;;;N;;;;;
+3359;IDEOGRAPHIC TELEGRAPH SYMBOL FOR HOUR ONE;So;0;L;<compat> 0031 70B9;;;;N;;;;;
+335A;IDEOGRAPHIC TELEGRAPH SYMBOL FOR HOUR TWO;So;0;L;<compat> 0032 70B9;;;;N;;;;;
+335B;IDEOGRAPHIC TELEGRAPH SYMBOL FOR HOUR THREE;So;0;L;<compat> 0033 70B9;;;;N;;;;;
+335C;IDEOGRAPHIC TELEGRAPH SYMBOL FOR HOUR FOUR;So;0;L;<compat> 0034 70B9;;;;N;;;;;
+335D;IDEOGRAPHIC TELEGRAPH SYMBOL FOR HOUR FIVE;So;0;L;<compat> 0035 70B9;;;;N;;;;;
+335E;IDEOGRAPHIC TELEGRAPH SYMBOL FOR HOUR SIX;So;0;L;<compat> 0036 70B9;;;;N;;;;;
+335F;IDEOGRAPHIC TELEGRAPH SYMBOL FOR HOUR SEVEN;So;0;L;<compat> 0037 70B9;;;;N;;;;;
+3360;IDEOGRAPHIC TELEGRAPH SYMBOL FOR HOUR EIGHT;So;0;L;<compat> 0038 70B9;;;;N;;;;;
+3361;IDEOGRAPHIC TELEGRAPH SYMBOL FOR HOUR NINE;So;0;L;<compat> 0039 70B9;;;;N;;;;;
+3362;IDEOGRAPHIC TELEGRAPH SYMBOL FOR HOUR TEN;So;0;L;<compat> 0031 0030 70B9;;;;N;;;;;
+3363;IDEOGRAPHIC TELEGRAPH SYMBOL FOR HOUR ELEVEN;So;0;L;<compat> 0031 0031 70B9;;;;N;;;;;
+3364;IDEOGRAPHIC TELEGRAPH SYMBOL FOR HOUR TWELVE;So;0;L;<compat> 0031 0032 70B9;;;;N;;;;;
+3365;IDEOGRAPHIC TELEGRAPH SYMBOL FOR HOUR THIRTEEN;So;0;L;<compat> 0031 0033 70B9;;;;N;;;;;
+3366;IDEOGRAPHIC TELEGRAPH SYMBOL FOR HOUR FOURTEEN;So;0;L;<compat> 0031 0034 70B9;;;;N;;;;;
+3367;IDEOGRAPHIC TELEGRAPH SYMBOL FOR HOUR FIFTEEN;So;0;L;<compat> 0031 0035 70B9;;;;N;;;;;
+3368;IDEOGRAPHIC TELEGRAPH SYMBOL FOR HOUR SIXTEEN;So;0;L;<compat> 0031 0036 70B9;;;;N;;;;;
+3369;IDEOGRAPHIC TELEGRAPH SYMBOL FOR HOUR SEVENTEEN;So;0;L;<compat> 0031 0037 70B9;;;;N;;;;;
+336A;IDEOGRAPHIC TELEGRAPH SYMBOL FOR HOUR EIGHTEEN;So;0;L;<compat> 0031 0038 70B9;;;;N;;;;;
+336B;IDEOGRAPHIC TELEGRAPH SYMBOL FOR HOUR NINETEEN;So;0;L;<compat> 0031 0039 70B9;;;;N;;;;;
+336C;IDEOGRAPHIC TELEGRAPH SYMBOL FOR HOUR TWENTY;So;0;L;<compat> 0032 0030 70B9;;;;N;;;;;
+336D;IDEOGRAPHIC TELEGRAPH SYMBOL FOR HOUR TWENTY-ONE;So;0;L;<compat> 0032 0031 70B9;;;;N;;;;;
+336E;IDEOGRAPHIC TELEGRAPH SYMBOL FOR HOUR TWENTY-TWO;So;0;L;<compat> 0032 0032 70B9;;;;N;;;;;
+336F;IDEOGRAPHIC TELEGRAPH SYMBOL FOR HOUR TWENTY-THREE;So;0;L;<compat> 0032 0033 70B9;;;;N;;;;;
+3370;IDEOGRAPHIC TELEGRAPH SYMBOL FOR HOUR TWENTY-FOUR;So;0;L;<compat> 0032 0034 70B9;;;;N;;;;;
+3371;SQUARE HPA;So;0;L;<square> 0068 0050 0061;;;;N;;;;;
+3372;SQUARE DA;So;0;L;<square> 0064 0061;;;;N;;;;;
+3373;SQUARE AU;So;0;L;<square> 0041 0055;;;;N;;;;;
+3374;SQUARE BAR;So;0;L;<square> 0062 0061 0072;;;;N;;;;;
+3375;SQUARE OV;So;0;L;<square> 006F 0056;;;;N;;;;;
+3376;SQUARE PC;So;0;L;<square> 0070 0063;;;;N;;;;;
+3377;SQUARE DM;So;0;ON;<square> 0064 006D;;;;N;;;;;
+3378;SQUARE DM SQUARED;So;0;ON;<square> 0064 006D 00B2;;;;N;;;;;
+3379;SQUARE DM CUBED;So;0;ON;<square> 0064 006D 00B3;;;;N;;;;;
+337A;SQUARE IU;So;0;ON;<square> 0049 0055;;;;N;;;;;
+337B;SQUARE ERA NAME HEISEI;So;0;L;<square> 5E73 6210;;;;N;SQUARED TWO IDEOGRAPHS ERA NAME HEISEI;;;;
+337C;SQUARE ERA NAME SYOUWA;So;0;L;<square> 662D 548C;;;;N;SQUARED TWO IDEOGRAPHS ERA NAME SYOUWA;;;;
+337D;SQUARE ERA NAME TAISYOU;So;0;L;<square> 5927 6B63;;;;N;SQUARED TWO IDEOGRAPHS ERA NAME TAISYOU;;;;
+337E;SQUARE ERA NAME MEIZI;So;0;L;<square> 660E 6CBB;;;;N;SQUARED TWO IDEOGRAPHS ERA NAME MEIZI;;;;
+337F;SQUARE CORPORATION;So;0;L;<square> 682A 5F0F 4F1A 793E;;;;N;SQUARED FOUR IDEOGRAPHS CORPORATION;;;;
+3380;SQUARE PA AMPS;So;0;L;<square> 0070 0041;;;;N;SQUARED PA AMPS;;;;
+3381;SQUARE NA;So;0;L;<square> 006E 0041;;;;N;SQUARED NA;;;;
+3382;SQUARE MU A;So;0;L;<square> 03BC 0041;;;;N;SQUARED MU A;;;;
+3383;SQUARE MA;So;0;L;<square> 006D 0041;;;;N;SQUARED MA;;;;
+3384;SQUARE KA;So;0;L;<square> 006B 0041;;;;N;SQUARED KA;;;;
+3385;SQUARE KB;So;0;L;<square> 004B 0042;;;;N;SQUARED KB;;;;
+3386;SQUARE MB;So;0;L;<square> 004D 0042;;;;N;SQUARED MB;;;;
+3387;SQUARE GB;So;0;L;<square> 0047 0042;;;;N;SQUARED GB;;;;
+3388;SQUARE CAL;So;0;L;<square> 0063 0061 006C;;;;N;SQUARED CAL;;;;
+3389;SQUARE KCAL;So;0;L;<square> 006B 0063 0061 006C;;;;N;SQUARED KCAL;;;;
+338A;SQUARE PF;So;0;L;<square> 0070 0046;;;;N;SQUARED PF;;;;
+338B;SQUARE NF;So;0;L;<square> 006E 0046;;;;N;SQUARED NF;;;;
+338C;SQUARE MU F;So;0;L;<square> 03BC 0046;;;;N;SQUARED MU F;;;;
+338D;SQUARE MU G;So;0;L;<square> 03BC 0067;;;;N;SQUARED MU G;;;;
+338E;SQUARE MG;So;0;L;<square> 006D 0067;;;;N;SQUARED MG;;;;
+338F;SQUARE KG;So;0;L;<square> 006B 0067;;;;N;SQUARED KG;;;;
+3390;SQUARE HZ;So;0;L;<square> 0048 007A;;;;N;SQUARED HZ;;;;
+3391;SQUARE KHZ;So;0;L;<square> 006B 0048 007A;;;;N;SQUARED KHZ;;;;
+3392;SQUARE MHZ;So;0;L;<square> 004D 0048 007A;;;;N;SQUARED MHZ;;;;
+3393;SQUARE GHZ;So;0;L;<square> 0047 0048 007A;;;;N;SQUARED GHZ;;;;
+3394;SQUARE THZ;So;0;L;<square> 0054 0048 007A;;;;N;SQUARED THZ;;;;
+3395;SQUARE MU L;So;0;L;<square> 03BC 2113;;;;N;SQUARED MU L;;;;
+3396;SQUARE ML;So;0;L;<square> 006D 2113;;;;N;SQUARED ML;;;;
+3397;SQUARE DL;So;0;L;<square> 0064 2113;;;;N;SQUARED DL;;;;
+3398;SQUARE KL;So;0;L;<square> 006B 2113;;;;N;SQUARED KL;;;;
+3399;SQUARE FM;So;0;L;<square> 0066 006D;;;;N;SQUARED FM;;;;
+339A;SQUARE NM;So;0;L;<square> 006E 006D;;;;N;SQUARED NM;;;;
+339B;SQUARE MU M;So;0;L;<square> 03BC 006D;;;;N;SQUARED MU M;;;;
+339C;SQUARE MM;So;0;L;<square> 006D 006D;;;;N;SQUARED MM;;;;
+339D;SQUARE CM;So;0;L;<square> 0063 006D;;;;N;SQUARED CM;;;;
+339E;SQUARE KM;So;0;L;<square> 006B 006D;;;;N;SQUARED KM;;;;
+339F;SQUARE MM SQUARED;So;0;L;<square> 006D 006D 00B2;;;;N;SQUARED MM SQUARED;;;;
+33A0;SQUARE CM SQUARED;So;0;L;<square> 0063 006D 00B2;;;;N;SQUARED CM SQUARED;;;;
+33A1;SQUARE M SQUARED;So;0;L;<square> 006D 00B2;;;;N;SQUARED M SQUARED;;;;
+33A2;SQUARE KM SQUARED;So;0;L;<square> 006B 006D 00B2;;;;N;SQUARED KM SQUARED;;;;
+33A3;SQUARE MM CUBED;So;0;L;<square> 006D 006D 00B3;;;;N;SQUARED MM CUBED;;;;
+33A4;SQUARE CM CUBED;So;0;L;<square> 0063 006D 00B3;;;;N;SQUARED CM CUBED;;;;
+33A5;SQUARE M CUBED;So;0;L;<square> 006D 00B3;;;;N;SQUARED M CUBED;;;;
+33A6;SQUARE KM CUBED;So;0;L;<square> 006B 006D 00B3;;;;N;SQUARED KM CUBED;;;;
+33A7;SQUARE M OVER S;So;0;L;<square> 006D 2215 0073;;;;N;SQUARED M OVER S;;;;
+33A8;SQUARE M OVER S SQUARED;So;0;L;<square> 006D 2215 0073 00B2;;;;N;SQUARED M OVER S SQUARED;;;;
+33A9;SQUARE PA;So;0;L;<square> 0050 0061;;;;N;SQUARED PA;;;;
+33AA;SQUARE KPA;So;0;L;<square> 006B 0050 0061;;;;N;SQUARED KPA;;;;
+33AB;SQUARE MPA;So;0;L;<square> 004D 0050 0061;;;;N;SQUARED MPA;;;;
+33AC;SQUARE GPA;So;0;L;<square> 0047 0050 0061;;;;N;SQUARED GPA;;;;
+33AD;SQUARE RAD;So;0;L;<square> 0072 0061 0064;;;;N;SQUARED RAD;;;;
+33AE;SQUARE RAD OVER S;So;0;L;<square> 0072 0061 0064 2215 0073;;;;N;SQUARED RAD OVER S;;;;
+33AF;SQUARE RAD OVER S SQUARED;So;0;L;<square> 0072 0061 0064 2215 0073 00B2;;;;N;SQUARED RAD OVER S SQUARED;;;;
+33B0;SQUARE PS;So;0;L;<square> 0070 0073;;;;N;SQUARED PS;;;;
+33B1;SQUARE NS;So;0;L;<square> 006E 0073;;;;N;SQUARED NS;;;;
+33B2;SQUARE MU S;So;0;L;<square> 03BC 0073;;;;N;SQUARED MU S;;;;
+33B3;SQUARE MS;So;0;L;<square> 006D 0073;;;;N;SQUARED MS;;;;
+33B4;SQUARE PV;So;0;L;<square> 0070 0056;;;;N;SQUARED PV;;;;
+33B5;SQUARE NV;So;0;L;<square> 006E 0056;;;;N;SQUARED NV;;;;
+33B6;SQUARE MU V;So;0;L;<square> 03BC 0056;;;;N;SQUARED MU V;;;;
+33B7;SQUARE MV;So;0;L;<square> 006D 0056;;;;N;SQUARED MV;;;;
+33B8;SQUARE KV;So;0;L;<square> 006B 0056;;;;N;SQUARED KV;;;;
+33B9;SQUARE MV MEGA;So;0;L;<square> 004D 0056;;;;N;SQUARED MV MEGA;;;;
+33BA;SQUARE PW;So;0;L;<square> 0070 0057;;;;N;SQUARED PW;;;;
+33BB;SQUARE NW;So;0;L;<square> 006E 0057;;;;N;SQUARED NW;;;;
+33BC;SQUARE MU W;So;0;L;<square> 03BC 0057;;;;N;SQUARED MU W;;;;
+33BD;SQUARE MW;So;0;L;<square> 006D 0057;;;;N;SQUARED MW;;;;
+33BE;SQUARE KW;So;0;L;<square> 006B 0057;;;;N;SQUARED KW;;;;
+33BF;SQUARE MW MEGA;So;0;L;<square> 004D 0057;;;;N;SQUARED MW MEGA;;;;
+33C0;SQUARE K OHM;So;0;L;<square> 006B 03A9;;;;N;SQUARED K OHM;;;;
+33C1;SQUARE M OHM;So;0;L;<square> 004D 03A9;;;;N;SQUARED M OHM;;;;
+33C2;SQUARE AM;So;0;L;<square> 0061 002E 006D 002E;;;;N;SQUARED AM;;;;
+33C3;SQUARE BQ;So;0;L;<square> 0042 0071;;;;N;SQUARED BQ;;;;
+33C4;SQUARE CC;So;0;L;<square> 0063 0063;;;;N;SQUARED CC;;;;
+33C5;SQUARE CD;So;0;L;<square> 0063 0064;;;;N;SQUARED CD;;;;
+33C6;SQUARE C OVER KG;So;0;L;<square> 0043 2215 006B 0067;;;;N;SQUARED C OVER KG;;;;
+33C7;SQUARE CO;So;0;L;<square> 0043 006F 002E;;;;N;SQUARED CO;;;;
+33C8;SQUARE DB;So;0;L;<square> 0064 0042;;;;N;SQUARED DB;;;;
+33C9;SQUARE GY;So;0;L;<square> 0047 0079;;;;N;SQUARED GY;;;;
+33CA;SQUARE HA;So;0;L;<square> 0068 0061;;;;N;SQUARED HA;;;;
+33CB;SQUARE HP;So;0;L;<square> 0048 0050;;;;N;SQUARED HP;;;;
+33CC;SQUARE IN;So;0;L;<square> 0069 006E;;;;N;SQUARED IN;;;;
+33CD;SQUARE KK;So;0;L;<square> 004B 004B;;;;N;SQUARED KK;;;;
+33CE;SQUARE KM CAPITAL;So;0;L;<square> 004B 004D;;;;N;SQUARED KM CAPITAL;;;;
+33CF;SQUARE KT;So;0;L;<square> 006B 0074;;;;N;SQUARED KT;;;;
+33D0;SQUARE LM;So;0;L;<square> 006C 006D;;;;N;SQUARED LM;;;;
+33D1;SQUARE LN;So;0;L;<square> 006C 006E;;;;N;SQUARED LN;;;;
+33D2;SQUARE LOG;So;0;L;<square> 006C 006F 0067;;;;N;SQUARED LOG;;;;
+33D3;SQUARE LX;So;0;L;<square> 006C 0078;;;;N;SQUARED LX;;;;
+33D4;SQUARE MB SMALL;So;0;L;<square> 006D 0062;;;;N;SQUARED MB SMALL;;;;
+33D5;SQUARE MIL;So;0;L;<square> 006D 0069 006C;;;;N;SQUARED MIL;;;;
+33D6;SQUARE MOL;So;0;L;<square> 006D 006F 006C;;;;N;SQUARED MOL;;;;
+33D7;SQUARE PH;So;0;L;<square> 0050 0048;;;;N;SQUARED PH;;;;
+33D8;SQUARE PM;So;0;L;<square> 0070 002E 006D 002E;;;;N;SQUARED PM;;;;
+33D9;SQUARE PPM;So;0;L;<square> 0050 0050 004D;;;;N;SQUARED PPM;;;;
+33DA;SQUARE PR;So;0;L;<square> 0050 0052;;;;N;SQUARED PR;;;;
+33DB;SQUARE SR;So;0;L;<square> 0073 0072;;;;N;SQUARED SR;;;;
+33DC;SQUARE SV;So;0;L;<square> 0053 0076;;;;N;SQUARED SV;;;;
+33DD;SQUARE WB;So;0;L;<square> 0057 0062;;;;N;SQUARED WB;;;;
+33DE;SQUARE V OVER M;So;0;ON;<square> 0056 2215 006D;;;;N;;;;;
+33DF;SQUARE A OVER M;So;0;ON;<square> 0041 2215 006D;;;;N;;;;;
+33E0;IDEOGRAPHIC TELEGRAPH SYMBOL FOR DAY ONE;So;0;L;<compat> 0031 65E5;;;;N;;;;;
+33E1;IDEOGRAPHIC TELEGRAPH SYMBOL FOR DAY TWO;So;0;L;<compat> 0032 65E5;;;;N;;;;;
+33E2;IDEOGRAPHIC TELEGRAPH SYMBOL FOR DAY THREE;So;0;L;<compat> 0033 65E5;;;;N;;;;;
+33E3;IDEOGRAPHIC TELEGRAPH SYMBOL FOR DAY FOUR;So;0;L;<compat> 0034 65E5;;;;N;;;;;
+33E4;IDEOGRAPHIC TELEGRAPH SYMBOL FOR DAY FIVE;So;0;L;<compat> 0035 65E5;;;;N;;;;;
+33E5;IDEOGRAPHIC TELEGRAPH SYMBOL FOR DAY SIX;So;0;L;<compat> 0036 65E5;;;;N;;;;;
+33E6;IDEOGRAPHIC TELEGRAPH SYMBOL FOR DAY SEVEN;So;0;L;<compat> 0037 65E5;;;;N;;;;;
+33E7;IDEOGRAPHIC TELEGRAPH SYMBOL FOR DAY EIGHT;So;0;L;<compat> 0038 65E5;;;;N;;;;;
+33E8;IDEOGRAPHIC TELEGRAPH SYMBOL FOR DAY NINE;So;0;L;<compat> 0039 65E5;;;;N;;;;;
+33E9;IDEOGRAPHIC TELEGRAPH SYMBOL FOR DAY TEN;So;0;L;<compat> 0031 0030 65E5;;;;N;;;;;
+33EA;IDEOGRAPHIC TELEGRAPH SYMBOL FOR DAY ELEVEN;So;0;L;<compat> 0031 0031 65E5;;;;N;;;;;
+33EB;IDEOGRAPHIC TELEGRAPH SYMBOL FOR DAY TWELVE;So;0;L;<compat> 0031 0032 65E5;;;;N;;;;;
+33EC;IDEOGRAPHIC TELEGRAPH SYMBOL FOR DAY THIRTEEN;So;0;L;<compat> 0031 0033 65E5;;;;N;;;;;
+33ED;IDEOGRAPHIC TELEGRAPH SYMBOL FOR DAY FOURTEEN;So;0;L;<compat> 0031 0034 65E5;;;;N;;;;;
+33EE;IDEOGRAPHIC TELEGRAPH SYMBOL FOR DAY FIFTEEN;So;0;L;<compat> 0031 0035 65E5;;;;N;;;;;
+33EF;IDEOGRAPHIC TELEGRAPH SYMBOL FOR DAY SIXTEEN;So;0;L;<compat> 0031 0036 65E5;;;;N;;;;;
+33F0;IDEOGRAPHIC TELEGRAPH SYMBOL FOR DAY SEVENTEEN;So;0;L;<compat> 0031 0037 65E5;;;;N;;;;;
+33F1;IDEOGRAPHIC TELEGRAPH SYMBOL FOR DAY EIGHTEEN;So;0;L;<compat> 0031 0038 65E5;;;;N;;;;;
+33F2;IDEOGRAPHIC TELEGRAPH SYMBOL FOR DAY NINETEEN;So;0;L;<compat> 0031 0039 65E5;;;;N;;;;;
+33F3;IDEOGRAPHIC TELEGRAPH SYMBOL FOR DAY TWENTY;So;0;L;<compat> 0032 0030 65E5;;;;N;;;;;
+33F4;IDEOGRAPHIC TELEGRAPH SYMBOL FOR DAY TWENTY-ONE;So;0;L;<compat> 0032 0031 65E5;;;;N;;;;;
+33F5;IDEOGRAPHIC TELEGRAPH SYMBOL FOR DAY TWENTY-TWO;So;0;L;<compat> 0032 0032 65E5;;;;N;;;;;
+33F6;IDEOGRAPHIC TELEGRAPH SYMBOL FOR DAY TWENTY-THREE;So;0;L;<compat> 0032 0033 65E5;;;;N;;;;;
+33F7;IDEOGRAPHIC TELEGRAPH SYMBOL FOR DAY TWENTY-FOUR;So;0;L;<compat> 0032 0034 65E5;;;;N;;;;;
+33F8;IDEOGRAPHIC TELEGRAPH SYMBOL FOR DAY TWENTY-FIVE;So;0;L;<compat> 0032 0035 65E5;;;;N;;;;;
+33F9;IDEOGRAPHIC TELEGRAPH SYMBOL FOR DAY TWENTY-SIX;So;0;L;<compat> 0032 0036 65E5;;;;N;;;;;
+33FA;IDEOGRAPHIC TELEGRAPH SYMBOL FOR DAY TWENTY-SEVEN;So;0;L;<compat> 0032 0037 65E5;;;;N;;;;;
+33FB;IDEOGRAPHIC TELEGRAPH SYMBOL FOR DAY TWENTY-EIGHT;So;0;L;<compat> 0032 0038 65E5;;;;N;;;;;
+33FC;IDEOGRAPHIC TELEGRAPH SYMBOL FOR DAY TWENTY-NINE;So;0;L;<compat> 0032 0039 65E5;;;;N;;;;;
+33FD;IDEOGRAPHIC TELEGRAPH SYMBOL FOR DAY THIRTY;So;0;L;<compat> 0033 0030 65E5;;;;N;;;;;
+33FE;IDEOGRAPHIC TELEGRAPH SYMBOL FOR DAY THIRTY-ONE;So;0;L;<compat> 0033 0031 65E5;;;;N;;;;;
+33FF;SQUARE GAL;So;0;ON;<square> 0067 0061 006C;;;;N;;;;;
+3400;<CJK Ideograph Extension A, First>;Lo;0;L;;;;;N;;;;;
+4DB5;<CJK Ideograph Extension A, Last>;Lo;0;L;;;;;N;;;;;
+4DC0;HEXAGRAM FOR THE CREATIVE HEAVEN;So;0;ON;;;;;N;;;;;
+4DC1;HEXAGRAM FOR THE RECEPTIVE EARTH;So;0;ON;;;;;N;;;;;
+4DC2;HEXAGRAM FOR DIFFICULTY AT THE BEGINNING;So;0;ON;;;;;N;;;;;
+4DC3;HEXAGRAM FOR YOUTHFUL FOLLY;So;0;ON;;;;;N;;;;;
+4DC4;HEXAGRAM FOR WAITING;So;0;ON;;;;;N;;;;;
+4DC5;HEXAGRAM FOR CONFLICT;So;0;ON;;;;;N;;;;;
+4DC6;HEXAGRAM FOR THE ARMY;So;0;ON;;;;;N;;;;;
+4DC7;HEXAGRAM FOR HOLDING TOGETHER;So;0;ON;;;;;N;;;;;
+4DC8;HEXAGRAM FOR SMALL TAMING;So;0;ON;;;;;N;;;;;
+4DC9;HEXAGRAM FOR TREADING;So;0;ON;;;;;N;;;;;
+4DCA;HEXAGRAM FOR PEACE;So;0;ON;;;;;N;;;;;
+4DCB;HEXAGRAM FOR STANDSTILL;So;0;ON;;;;;N;;;;;
+4DCC;HEXAGRAM FOR FELLOWSHIP;So;0;ON;;;;;N;;;;;
+4DCD;HEXAGRAM FOR GREAT POSSESSION;So;0;ON;;;;;N;;;;;
+4DCE;HEXAGRAM FOR MODESTY;So;0;ON;;;;;N;;;;;
+4DCF;HEXAGRAM FOR ENTHUSIASM;So;0;ON;;;;;N;;;;;
+4DD0;HEXAGRAM FOR FOLLOWING;So;0;ON;;;;;N;;;;;
+4DD1;HEXAGRAM FOR WORK ON THE DECAYED;So;0;ON;;;;;N;;;;;
+4DD2;HEXAGRAM FOR APPROACH;So;0;ON;;;;;N;;;;;
+4DD3;HEXAGRAM FOR CONTEMPLATION;So;0;ON;;;;;N;;;;;
+4DD4;HEXAGRAM FOR BITING THROUGH;So;0;ON;;;;;N;;;;;
+4DD5;HEXAGRAM FOR GRACE;So;0;ON;;;;;N;;;;;
+4DD6;HEXAGRAM FOR SPLITTING APART;So;0;ON;;;;;N;;;;;
+4DD7;HEXAGRAM FOR RETURN;So;0;ON;;;;;N;;;;;
+4DD8;HEXAGRAM FOR INNOCENCE;So;0;ON;;;;;N;;;;;
+4DD9;HEXAGRAM FOR GREAT TAMING;So;0;ON;;;;;N;;;;;
+4DDA;HEXAGRAM FOR MOUTH CORNERS;So;0;ON;;;;;N;;;;;
+4DDB;HEXAGRAM FOR GREAT PREPONDERANCE;So;0;ON;;;;;N;;;;;
+4DDC;HEXAGRAM FOR THE ABYSMAL WATER;So;0;ON;;;;;N;;;;;
+4DDD;HEXAGRAM FOR THE CLINGING FIRE;So;0;ON;;;;;N;;;;;
+4DDE;HEXAGRAM FOR INFLUENCE;So;0;ON;;;;;N;;;;;
+4DDF;HEXAGRAM FOR DURATION;So;0;ON;;;;;N;;;;;
+4DE0;HEXAGRAM FOR RETREAT;So;0;ON;;;;;N;;;;;
+4DE1;HEXAGRAM FOR GREAT POWER;So;0;ON;;;;;N;;;;;
+4DE2;HEXAGRAM FOR PROGRESS;So;0;ON;;;;;N;;;;;
+4DE3;HEXAGRAM FOR DARKENING OF THE LIGHT;So;0;ON;;;;;N;;;;;
+4DE4;HEXAGRAM FOR THE FAMILY;So;0;ON;;;;;N;;;;;
+4DE5;HEXAGRAM FOR OPPOSITION;So;0;ON;;;;;N;;;;;
+4DE6;HEXAGRAM FOR OBSTRUCTION;So;0;ON;;;;;N;;;;;
+4DE7;HEXAGRAM FOR DELIVERANCE;So;0;ON;;;;;N;;;;;
+4DE8;HEXAGRAM FOR DECREASE;So;0;ON;;;;;N;;;;;
+4DE9;HEXAGRAM FOR INCREASE;So;0;ON;;;;;N;;;;;
+4DEA;HEXAGRAM FOR BREAKTHROUGH;So;0;ON;;;;;N;;;;;
+4DEB;HEXAGRAM FOR COMING TO MEET;So;0;ON;;;;;N;;;;;
+4DEC;HEXAGRAM FOR GATHERING TOGETHER;So;0;ON;;;;;N;;;;;
+4DED;HEXAGRAM FOR PUSHING UPWARD;So;0;ON;;;;;N;;;;;
+4DEE;HEXAGRAM FOR OPPRESSION;So;0;ON;;;;;N;;;;;
+4DEF;HEXAGRAM FOR THE WELL;So;0;ON;;;;;N;;;;;
+4DF0;HEXAGRAM FOR REVOLUTION;So;0;ON;;;;;N;;;;;
+4DF1;HEXAGRAM FOR THE CAULDRON;So;0;ON;;;;;N;;;;;
+4DF2;HEXAGRAM FOR THE AROUSING THUNDER;So;0;ON;;;;;N;;;;;
+4DF3;HEXAGRAM FOR THE KEEPING STILL MOUNTAIN;So;0;ON;;;;;N;;;;;
+4DF4;HEXAGRAM FOR DEVELOPMENT;So;0;ON;;;;;N;;;;;
+4DF5;HEXAGRAM FOR THE MARRYING MAIDEN;So;0;ON;;;;;N;;;;;
+4DF6;HEXAGRAM FOR ABUNDANCE;So;0;ON;;;;;N;;;;;
+4DF7;HEXAGRAM FOR THE WANDERER;So;0;ON;;;;;N;;;;;
+4DF8;HEXAGRAM FOR THE GENTLE WIND;So;0;ON;;;;;N;;;;;
+4DF9;HEXAGRAM FOR THE JOYOUS LAKE;So;0;ON;;;;;N;;;;;
+4DFA;HEXAGRAM FOR DISPERSION;So;0;ON;;;;;N;;;;;
+4DFB;HEXAGRAM FOR LIMITATION;So;0;ON;;;;;N;;;;;
+4DFC;HEXAGRAM FOR INNER TRUTH;So;0;ON;;;;;N;;;;;
+4DFD;HEXAGRAM FOR SMALL PREPONDERANCE;So;0;ON;;;;;N;;;;;
+4DFE;HEXAGRAM FOR AFTER COMPLETION;So;0;ON;;;;;N;;;;;
+4DFF;HEXAGRAM FOR BEFORE COMPLETION;So;0;ON;;;;;N;;;;;
+4E00;<CJK Ideograph, First>;Lo;0;L;;;;;N;;;;;
+9FD5;<CJK Ideograph, Last>;Lo;0;L;;;;;N;;;;;
+A000;YI SYLLABLE IT;Lo;0;L;;;;;N;;;;;
+A001;YI SYLLABLE IX;Lo;0;L;;;;;N;;;;;
+A002;YI SYLLABLE I;Lo;0;L;;;;;N;;;;;
+A003;YI SYLLABLE IP;Lo;0;L;;;;;N;;;;;
+A004;YI SYLLABLE IET;Lo;0;L;;;;;N;;;;;
+A005;YI SYLLABLE IEX;Lo;0;L;;;;;N;;;;;
+A006;YI SYLLABLE IE;Lo;0;L;;;;;N;;;;;
+A007;YI SYLLABLE IEP;Lo;0;L;;;;;N;;;;;
+A008;YI SYLLABLE AT;Lo;0;L;;;;;N;;;;;
+A009;YI SYLLABLE AX;Lo;0;L;;;;;N;;;;;
+A00A;YI SYLLABLE A;Lo;0;L;;;;;N;;;;;
+A00B;YI SYLLABLE AP;Lo;0;L;;;;;N;;;;;
+A00C;YI SYLLABLE UOX;Lo;0;L;;;;;N;;;;;
+A00D;YI SYLLABLE UO;Lo;0;L;;;;;N;;;;;
+A00E;YI SYLLABLE UOP;Lo;0;L;;;;;N;;;;;
+A00F;YI SYLLABLE OT;Lo;0;L;;;;;N;;;;;
+A010;YI SYLLABLE OX;Lo;0;L;;;;;N;;;;;
+A011;YI SYLLABLE O;Lo;0;L;;;;;N;;;;;
+A012;YI SYLLABLE OP;Lo;0;L;;;;;N;;;;;
+A013;YI SYLLABLE EX;Lo;0;L;;;;;N;;;;;
+A014;YI SYLLABLE E;Lo;0;L;;;;;N;;;;;
+A015;YI SYLLABLE WU;Lm;0;L;;;;;N;;;;;
+A016;YI SYLLABLE BIT;Lo;0;L;;;;;N;;;;;
+A017;YI SYLLABLE BIX;Lo;0;L;;;;;N;;;;;
+A018;YI SYLLABLE BI;Lo;0;L;;;;;N;;;;;
+A019;YI SYLLABLE BIP;Lo;0;L;;;;;N;;;;;
+A01A;YI SYLLABLE BIET;Lo;0;L;;;;;N;;;;;
+A01B;YI SYLLABLE BIEX;Lo;0;L;;;;;N;;;;;
+A01C;YI SYLLABLE BIE;Lo;0;L;;;;;N;;;;;
+A01D;YI SYLLABLE BIEP;Lo;0;L;;;;;N;;;;;
+A01E;YI SYLLABLE BAT;Lo;0;L;;;;;N;;;;;
+A01F;YI SYLLABLE BAX;Lo;0;L;;;;;N;;;;;
+A020;YI SYLLABLE BA;Lo;0;L;;;;;N;;;;;
+A021;YI SYLLABLE BAP;Lo;0;L;;;;;N;;;;;
+A022;YI SYLLABLE BUOX;Lo;0;L;;;;;N;;;;;
+A023;YI SYLLABLE BUO;Lo;0;L;;;;;N;;;;;
+A024;YI SYLLABLE BUOP;Lo;0;L;;;;;N;;;;;
+A025;YI SYLLABLE BOT;Lo;0;L;;;;;N;;;;;
+A026;YI SYLLABLE BOX;Lo;0;L;;;;;N;;;;;
+A027;YI SYLLABLE BO;Lo;0;L;;;;;N;;;;;
+A028;YI SYLLABLE BOP;Lo;0;L;;;;;N;;;;;
+A029;YI SYLLABLE BEX;Lo;0;L;;;;;N;;;;;
+A02A;YI SYLLABLE BE;Lo;0;L;;;;;N;;;;;
+A02B;YI SYLLABLE BEP;Lo;0;L;;;;;N;;;;;
+A02C;YI SYLLABLE BUT;Lo;0;L;;;;;N;;;;;
+A02D;YI SYLLABLE BUX;Lo;0;L;;;;;N;;;;;
+A02E;YI SYLLABLE BU;Lo;0;L;;;;;N;;;;;
+A02F;YI SYLLABLE BUP;Lo;0;L;;;;;N;;;;;
+A030;YI SYLLABLE BURX;Lo;0;L;;;;;N;;;;;
+A031;YI SYLLABLE BUR;Lo;0;L;;;;;N;;;;;
+A032;YI SYLLABLE BYT;Lo;0;L;;;;;N;;;;;
+A033;YI SYLLABLE BYX;Lo;0;L;;;;;N;;;;;
+A034;YI SYLLABLE BY;Lo;0;L;;;;;N;;;;;
+A035;YI SYLLABLE BYP;Lo;0;L;;;;;N;;;;;
+A036;YI SYLLABLE BYRX;Lo;0;L;;;;;N;;;;;
+A037;YI SYLLABLE BYR;Lo;0;L;;;;;N;;;;;
+A038;YI SYLLABLE PIT;Lo;0;L;;;;;N;;;;;
+A039;YI SYLLABLE PIX;Lo;0;L;;;;;N;;;;;
+A03A;YI SYLLABLE PI;Lo;0;L;;;;;N;;;;;
+A03B;YI SYLLABLE PIP;Lo;0;L;;;;;N;;;;;
+A03C;YI SYLLABLE PIEX;Lo;0;L;;;;;N;;;;;
+A03D;YI SYLLABLE PIE;Lo;0;L;;;;;N;;;;;
+A03E;YI SYLLABLE PIEP;Lo;0;L;;;;;N;;;;;
+A03F;YI SYLLABLE PAT;Lo;0;L;;;;;N;;;;;
+A040;YI SYLLABLE PAX;Lo;0;L;;;;;N;;;;;
+A041;YI SYLLABLE PA;Lo;0;L;;;;;N;;;;;
+A042;YI SYLLABLE PAP;Lo;0;L;;;;;N;;;;;
+A043;YI SYLLABLE PUOX;Lo;0;L;;;;;N;;;;;
+A044;YI SYLLABLE PUO;Lo;0;L;;;;;N;;;;;
+A045;YI SYLLABLE PUOP;Lo;0;L;;;;;N;;;;;
+A046;YI SYLLABLE POT;Lo;0;L;;;;;N;;;;;
+A047;YI SYLLABLE POX;Lo;0;L;;;;;N;;;;;
+A048;YI SYLLABLE PO;Lo;0;L;;;;;N;;;;;
+A049;YI SYLLABLE POP;Lo;0;L;;;;;N;;;;;
+A04A;YI SYLLABLE PUT;Lo;0;L;;;;;N;;;;;
+A04B;YI SYLLABLE PUX;Lo;0;L;;;;;N;;;;;
+A04C;YI SYLLABLE PU;Lo;0;L;;;;;N;;;;;
+A04D;YI SYLLABLE PUP;Lo;0;L;;;;;N;;;;;
+A04E;YI SYLLABLE PURX;Lo;0;L;;;;;N;;;;;
+A04F;YI SYLLABLE PUR;Lo;0;L;;;;;N;;;;;
+A050;YI SYLLABLE PYT;Lo;0;L;;;;;N;;;;;
+A051;YI SYLLABLE PYX;Lo;0;L;;;;;N;;;;;
+A052;YI SYLLABLE PY;Lo;0;L;;;;;N;;;;;
+A053;YI SYLLABLE PYP;Lo;0;L;;;;;N;;;;;
+A054;YI SYLLABLE PYRX;Lo;0;L;;;;;N;;;;;
+A055;YI SYLLABLE PYR;Lo;0;L;;;;;N;;;;;
+A056;YI SYLLABLE BBIT;Lo;0;L;;;;;N;;;;;
+A057;YI SYLLABLE BBIX;Lo;0;L;;;;;N;;;;;
+A058;YI SYLLABLE BBI;Lo;0;L;;;;;N;;;;;
+A059;YI SYLLABLE BBIP;Lo;0;L;;;;;N;;;;;
+A05A;YI SYLLABLE BBIET;Lo;0;L;;;;;N;;;;;
+A05B;YI SYLLABLE BBIEX;Lo;0;L;;;;;N;;;;;
+A05C;YI SYLLABLE BBIE;Lo;0;L;;;;;N;;;;;
+A05D;YI SYLLABLE BBIEP;Lo;0;L;;;;;N;;;;;
+A05E;YI SYLLABLE BBAT;Lo;0;L;;;;;N;;;;;
+A05F;YI SYLLABLE BBAX;Lo;0;L;;;;;N;;;;;
+A060;YI SYLLABLE BBA;Lo;0;L;;;;;N;;;;;
+A061;YI SYLLABLE BBAP;Lo;0;L;;;;;N;;;;;
+A062;YI SYLLABLE BBUOX;Lo;0;L;;;;;N;;;;;
+A063;YI SYLLABLE BBUO;Lo;0;L;;;;;N;;;;;
+A064;YI SYLLABLE BBUOP;Lo;0;L;;;;;N;;;;;
+A065;YI SYLLABLE BBOT;Lo;0;L;;;;;N;;;;;
+A066;YI SYLLABLE BBOX;Lo;0;L;;;;;N;;;;;
+A067;YI SYLLABLE BBO;Lo;0;L;;;;;N;;;;;
+A068;YI SYLLABLE BBOP;Lo;0;L;;;;;N;;;;;
+A069;YI SYLLABLE BBEX;Lo;0;L;;;;;N;;;;;
+A06A;YI SYLLABLE BBE;Lo;0;L;;;;;N;;;;;
+A06B;YI SYLLABLE BBEP;Lo;0;L;;;;;N;;;;;
+A06C;YI SYLLABLE BBUT;Lo;0;L;;;;;N;;;;;
+A06D;YI SYLLABLE BBUX;Lo;0;L;;;;;N;;;;;
+A06E;YI SYLLABLE BBU;Lo;0;L;;;;;N;;;;;
+A06F;YI SYLLABLE BBUP;Lo;0;L;;;;;N;;;;;
+A070;YI SYLLABLE BBURX;Lo;0;L;;;;;N;;;;;
+A071;YI SYLLABLE BBUR;Lo;0;L;;;;;N;;;;;
+A072;YI SYLLABLE BBYT;Lo;0;L;;;;;N;;;;;
+A073;YI SYLLABLE BBYX;Lo;0;L;;;;;N;;;;;
+A074;YI SYLLABLE BBY;Lo;0;L;;;;;N;;;;;
+A075;YI SYLLABLE BBYP;Lo;0;L;;;;;N;;;;;
+A076;YI SYLLABLE NBIT;Lo;0;L;;;;;N;;;;;
+A077;YI SYLLABLE NBIX;Lo;0;L;;;;;N;;;;;
+A078;YI SYLLABLE NBI;Lo;0;L;;;;;N;;;;;
+A079;YI SYLLABLE NBIP;Lo;0;L;;;;;N;;;;;
+A07A;YI SYLLABLE NBIEX;Lo;0;L;;;;;N;;;;;
+A07B;YI SYLLABLE NBIE;Lo;0;L;;;;;N;;;;;
+A07C;YI SYLLABLE NBIEP;Lo;0;L;;;;;N;;;;;
+A07D;YI SYLLABLE NBAT;Lo;0;L;;;;;N;;;;;
+A07E;YI SYLLABLE NBAX;Lo;0;L;;;;;N;;;;;
+A07F;YI SYLLABLE NBA;Lo;0;L;;;;;N;;;;;
+A080;YI SYLLABLE NBAP;Lo;0;L;;;;;N;;;;;
+A081;YI SYLLABLE NBOT;Lo;0;L;;;;;N;;;;;
+A082;YI SYLLABLE NBOX;Lo;0;L;;;;;N;;;;;
+A083;YI SYLLABLE NBO;Lo;0;L;;;;;N;;;;;
+A084;YI SYLLABLE NBOP;Lo;0;L;;;;;N;;;;;
+A085;YI SYLLABLE NBUT;Lo;0;L;;;;;N;;;;;
+A086;YI SYLLABLE NBUX;Lo;0;L;;;;;N;;;;;
+A087;YI SYLLABLE NBU;Lo;0;L;;;;;N;;;;;
+A088;YI SYLLABLE NBUP;Lo;0;L;;;;;N;;;;;
+A089;YI SYLLABLE NBURX;Lo;0;L;;;;;N;;;;;
+A08A;YI SYLLABLE NBUR;Lo;0;L;;;;;N;;;;;
+A08B;YI SYLLABLE NBYT;Lo;0;L;;;;;N;;;;;
+A08C;YI SYLLABLE NBYX;Lo;0;L;;;;;N;;;;;
+A08D;YI SYLLABLE NBY;Lo;0;L;;;;;N;;;;;
+A08E;YI SYLLABLE NBYP;Lo;0;L;;;;;N;;;;;
+A08F;YI SYLLABLE NBYRX;Lo;0;L;;;;;N;;;;;
+A090;YI SYLLABLE NBYR;Lo;0;L;;;;;N;;;;;
+A091;YI SYLLABLE HMIT;Lo;0;L;;;;;N;;;;;
+A092;YI SYLLABLE HMIX;Lo;0;L;;;;;N;;;;;
+A093;YI SYLLABLE HMI;Lo;0;L;;;;;N;;;;;
+A094;YI SYLLABLE HMIP;Lo;0;L;;;;;N;;;;;
+A095;YI SYLLABLE HMIEX;Lo;0;L;;;;;N;;;;;
+A096;YI SYLLABLE HMIE;Lo;0;L;;;;;N;;;;;
+A097;YI SYLLABLE HMIEP;Lo;0;L;;;;;N;;;;;
+A098;YI SYLLABLE HMAT;Lo;0;L;;;;;N;;;;;
+A099;YI SYLLABLE HMAX;Lo;0;L;;;;;N;;;;;
+A09A;YI SYLLABLE HMA;Lo;0;L;;;;;N;;;;;
+A09B;YI SYLLABLE HMAP;Lo;0;L;;;;;N;;;;;
+A09C;YI SYLLABLE HMUOX;Lo;0;L;;;;;N;;;;;
+A09D;YI SYLLABLE HMUO;Lo;0;L;;;;;N;;;;;
+A09E;YI SYLLABLE HMUOP;Lo;0;L;;;;;N;;;;;
+A09F;YI SYLLABLE HMOT;Lo;0;L;;;;;N;;;;;
+A0A0;YI SYLLABLE HMOX;Lo;0;L;;;;;N;;;;;
+A0A1;YI SYLLABLE HMO;Lo;0;L;;;;;N;;;;;
+A0A2;YI SYLLABLE HMOP;Lo;0;L;;;;;N;;;;;
+A0A3;YI SYLLABLE HMUT;Lo;0;L;;;;;N;;;;;
+A0A4;YI SYLLABLE HMUX;Lo;0;L;;;;;N;;;;;
+A0A5;YI SYLLABLE HMU;Lo;0;L;;;;;N;;;;;
+A0A6;YI SYLLABLE HMUP;Lo;0;L;;;;;N;;;;;
+A0A7;YI SYLLABLE HMURX;Lo;0;L;;;;;N;;;;;
+A0A8;YI SYLLABLE HMUR;Lo;0;L;;;;;N;;;;;
+A0A9;YI SYLLABLE HMYX;Lo;0;L;;;;;N;;;;;
+A0AA;YI SYLLABLE HMY;Lo;0;L;;;;;N;;;;;
+A0AB;YI SYLLABLE HMYP;Lo;0;L;;;;;N;;;;;
+A0AC;YI SYLLABLE HMYRX;Lo;0;L;;;;;N;;;;;
+A0AD;YI SYLLABLE HMYR;Lo;0;L;;;;;N;;;;;
+A0AE;YI SYLLABLE MIT;Lo;0;L;;;;;N;;;;;
+A0AF;YI SYLLABLE MIX;Lo;0;L;;;;;N;;;;;
+A0B0;YI SYLLABLE MI;Lo;0;L;;;;;N;;;;;
+A0B1;YI SYLLABLE MIP;Lo;0;L;;;;;N;;;;;
+A0B2;YI SYLLABLE MIEX;Lo;0;L;;;;;N;;;;;
+A0B3;YI SYLLABLE MIE;Lo;0;L;;;;;N;;;;;
+A0B4;YI SYLLABLE MIEP;Lo;0;L;;;;;N;;;;;
+A0B5;YI SYLLABLE MAT;Lo;0;L;;;;;N;;;;;
+A0B6;YI SYLLABLE MAX;Lo;0;L;;;;;N;;;;;
+A0B7;YI SYLLABLE MA;Lo;0;L;;;;;N;;;;;
+A0B8;YI SYLLABLE MAP;Lo;0;L;;;;;N;;;;;
+A0B9;YI SYLLABLE MUOT;Lo;0;L;;;;;N;;;;;
+A0BA;YI SYLLABLE MUOX;Lo;0;L;;;;;N;;;;;
+A0BB;YI SYLLABLE MUO;Lo;0;L;;;;;N;;;;;
+A0BC;YI SYLLABLE MUOP;Lo;0;L;;;;;N;;;;;
+A0BD;YI SYLLABLE MOT;Lo;0;L;;;;;N;;;;;
+A0BE;YI SYLLABLE MOX;Lo;0;L;;;;;N;;;;;
+A0BF;YI SYLLABLE MO;Lo;0;L;;;;;N;;;;;
+A0C0;YI SYLLABLE MOP;Lo;0;L;;;;;N;;;;;
+A0C1;YI SYLLABLE MEX;Lo;0;L;;;;;N;;;;;
+A0C2;YI SYLLABLE ME;Lo;0;L;;;;;N;;;;;
+A0C3;YI SYLLABLE MUT;Lo;0;L;;;;;N;;;;;
+A0C4;YI SYLLABLE MUX;Lo;0;L;;;;;N;;;;;
+A0C5;YI SYLLABLE MU;Lo;0;L;;;;;N;;;;;
+A0C6;YI SYLLABLE MUP;Lo;0;L;;;;;N;;;;;
+A0C7;YI SYLLABLE MURX;Lo;0;L;;;;;N;;;;;
+A0C8;YI SYLLABLE MUR;Lo;0;L;;;;;N;;;;;
+A0C9;YI SYLLABLE MYT;Lo;0;L;;;;;N;;;;;
+A0CA;YI SYLLABLE MYX;Lo;0;L;;;;;N;;;;;
+A0CB;YI SYLLABLE MY;Lo;0;L;;;;;N;;;;;
+A0CC;YI SYLLABLE MYP;Lo;0;L;;;;;N;;;;;
+A0CD;YI SYLLABLE FIT;Lo;0;L;;;;;N;;;;;
+A0CE;YI SYLLABLE FIX;Lo;0;L;;;;;N;;;;;
+A0CF;YI SYLLABLE FI;Lo;0;L;;;;;N;;;;;
+A0D0;YI SYLLABLE FIP;Lo;0;L;;;;;N;;;;;
+A0D1;YI SYLLABLE FAT;Lo;0;L;;;;;N;;;;;
+A0D2;YI SYLLABLE FAX;Lo;0;L;;;;;N;;;;;
+A0D3;YI SYLLABLE FA;Lo;0;L;;;;;N;;;;;
+A0D4;YI SYLLABLE FAP;Lo;0;L;;;;;N;;;;;
+A0D5;YI SYLLABLE FOX;Lo;0;L;;;;;N;;;;;
+A0D6;YI SYLLABLE FO;Lo;0;L;;;;;N;;;;;
+A0D7;YI SYLLABLE FOP;Lo;0;L;;;;;N;;;;;
+A0D8;YI SYLLABLE FUT;Lo;0;L;;;;;N;;;;;
+A0D9;YI SYLLABLE FUX;Lo;0;L;;;;;N;;;;;
+A0DA;YI SYLLABLE FU;Lo;0;L;;;;;N;;;;;
+A0DB;YI SYLLABLE FUP;Lo;0;L;;;;;N;;;;;
+A0DC;YI SYLLABLE FURX;Lo;0;L;;;;;N;;;;;
+A0DD;YI SYLLABLE FUR;Lo;0;L;;;;;N;;;;;
+A0DE;YI SYLLABLE FYT;Lo;0;L;;;;;N;;;;;
+A0DF;YI SYLLABLE FYX;Lo;0;L;;;;;N;;;;;
+A0E0;YI SYLLABLE FY;Lo;0;L;;;;;N;;;;;
+A0E1;YI SYLLABLE FYP;Lo;0;L;;;;;N;;;;;
+A0E2;YI SYLLABLE VIT;Lo;0;L;;;;;N;;;;;
+A0E3;YI SYLLABLE VIX;Lo;0;L;;;;;N;;;;;
+A0E4;YI SYLLABLE VI;Lo;0;L;;;;;N;;;;;
+A0E5;YI SYLLABLE VIP;Lo;0;L;;;;;N;;;;;
+A0E6;YI SYLLABLE VIET;Lo;0;L;;;;;N;;;;;
+A0E7;YI SYLLABLE VIEX;Lo;0;L;;;;;N;;;;;
+A0E8;YI SYLLABLE VIE;Lo;0;L;;;;;N;;;;;
+A0E9;YI SYLLABLE VIEP;Lo;0;L;;;;;N;;;;;
+A0EA;YI SYLLABLE VAT;Lo;0;L;;;;;N;;;;;
+A0EB;YI SYLLABLE VAX;Lo;0;L;;;;;N;;;;;
+A0EC;YI SYLLABLE VA;Lo;0;L;;;;;N;;;;;
+A0ED;YI SYLLABLE VAP;Lo;0;L;;;;;N;;;;;
+A0EE;YI SYLLABLE VOT;Lo;0;L;;;;;N;;;;;
+A0EF;YI SYLLABLE VOX;Lo;0;L;;;;;N;;;;;
+A0F0;YI SYLLABLE VO;Lo;0;L;;;;;N;;;;;
+A0F1;YI SYLLABLE VOP;Lo;0;L;;;;;N;;;;;
+A0F2;YI SYLLABLE VEX;Lo;0;L;;;;;N;;;;;
+A0F3;YI SYLLABLE VEP;Lo;0;L;;;;;N;;;;;
+A0F4;YI SYLLABLE VUT;Lo;0;L;;;;;N;;;;;
+A0F5;YI SYLLABLE VUX;Lo;0;L;;;;;N;;;;;
+A0F6;YI SYLLABLE VU;Lo;0;L;;;;;N;;;;;
+A0F7;YI SYLLABLE VUP;Lo;0;L;;;;;N;;;;;
+A0F8;YI SYLLABLE VURX;Lo;0;L;;;;;N;;;;;
+A0F9;YI SYLLABLE VUR;Lo;0;L;;;;;N;;;;;
+A0FA;YI SYLLABLE VYT;Lo;0;L;;;;;N;;;;;
+A0FB;YI SYLLABLE VYX;Lo;0;L;;;;;N;;;;;
+A0FC;YI SYLLABLE VY;Lo;0;L;;;;;N;;;;;
+A0FD;YI SYLLABLE VYP;Lo;0;L;;;;;N;;;;;
+A0FE;YI SYLLABLE VYRX;Lo;0;L;;;;;N;;;;;
+A0FF;YI SYLLABLE VYR;Lo;0;L;;;;;N;;;;;
+A100;YI SYLLABLE DIT;Lo;0;L;;;;;N;;;;;
+A101;YI SYLLABLE DIX;Lo;0;L;;;;;N;;;;;
+A102;YI SYLLABLE DI;Lo;0;L;;;;;N;;;;;
+A103;YI SYLLABLE DIP;Lo;0;L;;;;;N;;;;;
+A104;YI SYLLABLE DIEX;Lo;0;L;;;;;N;;;;;
+A105;YI SYLLABLE DIE;Lo;0;L;;;;;N;;;;;
+A106;YI SYLLABLE DIEP;Lo;0;L;;;;;N;;;;;
+A107;YI SYLLABLE DAT;Lo;0;L;;;;;N;;;;;
+A108;YI SYLLABLE DAX;Lo;0;L;;;;;N;;;;;
+A109;YI SYLLABLE DA;Lo;0;L;;;;;N;;;;;
+A10A;YI SYLLABLE DAP;Lo;0;L;;;;;N;;;;;
+A10B;YI SYLLABLE DUOX;Lo;0;L;;;;;N;;;;;
+A10C;YI SYLLABLE DUO;Lo;0;L;;;;;N;;;;;
+A10D;YI SYLLABLE DOT;Lo;0;L;;;;;N;;;;;
+A10E;YI SYLLABLE DOX;Lo;0;L;;;;;N;;;;;
+A10F;YI SYLLABLE DO;Lo;0;L;;;;;N;;;;;
+A110;YI SYLLABLE DOP;Lo;0;L;;;;;N;;;;;
+A111;YI SYLLABLE DEX;Lo;0;L;;;;;N;;;;;
+A112;YI SYLLABLE DE;Lo;0;L;;;;;N;;;;;
+A113;YI SYLLABLE DEP;Lo;0;L;;;;;N;;;;;
+A114;YI SYLLABLE DUT;Lo;0;L;;;;;N;;;;;
+A115;YI SYLLABLE DUX;Lo;0;L;;;;;N;;;;;
+A116;YI SYLLABLE DU;Lo;0;L;;;;;N;;;;;
+A117;YI SYLLABLE DUP;Lo;0;L;;;;;N;;;;;
+A118;YI SYLLABLE DURX;Lo;0;L;;;;;N;;;;;
+A119;YI SYLLABLE DUR;Lo;0;L;;;;;N;;;;;
+A11A;YI SYLLABLE TIT;Lo;0;L;;;;;N;;;;;
+A11B;YI SYLLABLE TIX;Lo;0;L;;;;;N;;;;;
+A11C;YI SYLLABLE TI;Lo;0;L;;;;;N;;;;;
+A11D;YI SYLLABLE TIP;Lo;0;L;;;;;N;;;;;
+A11E;YI SYLLABLE TIEX;Lo;0;L;;;;;N;;;;;
+A11F;YI SYLLABLE TIE;Lo;0;L;;;;;N;;;;;
+A120;YI SYLLABLE TIEP;Lo;0;L;;;;;N;;;;;
+A121;YI SYLLABLE TAT;Lo;0;L;;;;;N;;;;;
+A122;YI SYLLABLE TAX;Lo;0;L;;;;;N;;;;;
+A123;YI SYLLABLE TA;Lo;0;L;;;;;N;;;;;
+A124;YI SYLLABLE TAP;Lo;0;L;;;;;N;;;;;
+A125;YI SYLLABLE TUOT;Lo;0;L;;;;;N;;;;;
+A126;YI SYLLABLE TUOX;Lo;0;L;;;;;N;;;;;
+A127;YI SYLLABLE TUO;Lo;0;L;;;;;N;;;;;
+A128;YI SYLLABLE TUOP;Lo;0;L;;;;;N;;;;;
+A129;YI SYLLABLE TOT;Lo;0;L;;;;;N;;;;;
+A12A;YI SYLLABLE TOX;Lo;0;L;;;;;N;;;;;
+A12B;YI SYLLABLE TO;Lo;0;L;;;;;N;;;;;
+A12C;YI SYLLABLE TOP;Lo;0;L;;;;;N;;;;;
+A12D;YI SYLLABLE TEX;Lo;0;L;;;;;N;;;;;
+A12E;YI SYLLABLE TE;Lo;0;L;;;;;N;;;;;
+A12F;YI SYLLABLE TEP;Lo;0;L;;;;;N;;;;;
+A130;YI SYLLABLE TUT;Lo;0;L;;;;;N;;;;;
+A131;YI SYLLABLE TUX;Lo;0;L;;;;;N;;;;;
+A132;YI SYLLABLE TU;Lo;0;L;;;;;N;;;;;
+A133;YI SYLLABLE TUP;Lo;0;L;;;;;N;;;;;
+A134;YI SYLLABLE TURX;Lo;0;L;;;;;N;;;;;
+A135;YI SYLLABLE TUR;Lo;0;L;;;;;N;;;;;
+A136;YI SYLLABLE DDIT;Lo;0;L;;;;;N;;;;;
+A137;YI SYLLABLE DDIX;Lo;0;L;;;;;N;;;;;
+A138;YI SYLLABLE DDI;Lo;0;L;;;;;N;;;;;
+A139;YI SYLLABLE DDIP;Lo;0;L;;;;;N;;;;;
+A13A;YI SYLLABLE DDIEX;Lo;0;L;;;;;N;;;;;
+A13B;YI SYLLABLE DDIE;Lo;0;L;;;;;N;;;;;
+A13C;YI SYLLABLE DDIEP;Lo;0;L;;;;;N;;;;;
+A13D;YI SYLLABLE DDAT;Lo;0;L;;;;;N;;;;;
+A13E;YI SYLLABLE DDAX;Lo;0;L;;;;;N;;;;;
+A13F;YI SYLLABLE DDA;Lo;0;L;;;;;N;;;;;
+A140;YI SYLLABLE DDAP;Lo;0;L;;;;;N;;;;;
+A141;YI SYLLABLE DDUOX;Lo;0;L;;;;;N;;;;;
+A142;YI SYLLABLE DDUO;Lo;0;L;;;;;N;;;;;
+A143;YI SYLLABLE DDUOP;Lo;0;L;;;;;N;;;;;
+A144;YI SYLLABLE DDOT;Lo;0;L;;;;;N;;;;;
+A145;YI SYLLABLE DDOX;Lo;0;L;;;;;N;;;;;
+A146;YI SYLLABLE DDO;Lo;0;L;;;;;N;;;;;
+A147;YI SYLLABLE DDOP;Lo;0;L;;;;;N;;;;;
+A148;YI SYLLABLE DDEX;Lo;0;L;;;;;N;;;;;
+A149;YI SYLLABLE DDE;Lo;0;L;;;;;N;;;;;
+A14A;YI SYLLABLE DDEP;Lo;0;L;;;;;N;;;;;
+A14B;YI SYLLABLE DDUT;Lo;0;L;;;;;N;;;;;
+A14C;YI SYLLABLE DDUX;Lo;0;L;;;;;N;;;;;
+A14D;YI SYLLABLE DDU;Lo;0;L;;;;;N;;;;;
+A14E;YI SYLLABLE DDUP;Lo;0;L;;;;;N;;;;;
+A14F;YI SYLLABLE DDURX;Lo;0;L;;;;;N;;;;;
+A150;YI SYLLABLE DDUR;Lo;0;L;;;;;N;;;;;
+A151;YI SYLLABLE NDIT;Lo;0;L;;;;;N;;;;;
+A152;YI SYLLABLE NDIX;Lo;0;L;;;;;N;;;;;
+A153;YI SYLLABLE NDI;Lo;0;L;;;;;N;;;;;
+A154;YI SYLLABLE NDIP;Lo;0;L;;;;;N;;;;;
+A155;YI SYLLABLE NDIEX;Lo;0;L;;;;;N;;;;;
+A156;YI SYLLABLE NDIE;Lo;0;L;;;;;N;;;;;
+A157;YI SYLLABLE NDAT;Lo;0;L;;;;;N;;;;;
+A158;YI SYLLABLE NDAX;Lo;0;L;;;;;N;;;;;
+A159;YI SYLLABLE NDA;Lo;0;L;;;;;N;;;;;
+A15A;YI SYLLABLE NDAP;Lo;0;L;;;;;N;;;;;
+A15B;YI SYLLABLE NDOT;Lo;0;L;;;;;N;;;;;
+A15C;YI SYLLABLE NDOX;Lo;0;L;;;;;N;;;;;
+A15D;YI SYLLABLE NDO;Lo;0;L;;;;;N;;;;;
+A15E;YI SYLLABLE NDOP;Lo;0;L;;;;;N;;;;;
+A15F;YI SYLLABLE NDEX;Lo;0;L;;;;;N;;;;;
+A160;YI SYLLABLE NDE;Lo;0;L;;;;;N;;;;;
+A161;YI SYLLABLE NDEP;Lo;0;L;;;;;N;;;;;
+A162;YI SYLLABLE NDUT;Lo;0;L;;;;;N;;;;;
+A163;YI SYLLABLE NDUX;Lo;0;L;;;;;N;;;;;
+A164;YI SYLLABLE NDU;Lo;0;L;;;;;N;;;;;
+A165;YI SYLLABLE NDUP;Lo;0;L;;;;;N;;;;;
+A166;YI SYLLABLE NDURX;Lo;0;L;;;;;N;;;;;
+A167;YI SYLLABLE NDUR;Lo;0;L;;;;;N;;;;;
+A168;YI SYLLABLE HNIT;Lo;0;L;;;;;N;;;;;
+A169;YI SYLLABLE HNIX;Lo;0;L;;;;;N;;;;;
+A16A;YI SYLLABLE HNI;Lo;0;L;;;;;N;;;;;
+A16B;YI SYLLABLE HNIP;Lo;0;L;;;;;N;;;;;
+A16C;YI SYLLABLE HNIET;Lo;0;L;;;;;N;;;;;
+A16D;YI SYLLABLE HNIEX;Lo;0;L;;;;;N;;;;;
+A16E;YI SYLLABLE HNIE;Lo;0;L;;;;;N;;;;;
+A16F;YI SYLLABLE HNIEP;Lo;0;L;;;;;N;;;;;
+A170;YI SYLLABLE HNAT;Lo;0;L;;;;;N;;;;;
+A171;YI SYLLABLE HNAX;Lo;0;L;;;;;N;;;;;
+A172;YI SYLLABLE HNA;Lo;0;L;;;;;N;;;;;
+A173;YI SYLLABLE HNAP;Lo;0;L;;;;;N;;;;;
+A174;YI SYLLABLE HNUOX;Lo;0;L;;;;;N;;;;;
+A175;YI SYLLABLE HNUO;Lo;0;L;;;;;N;;;;;
+A176;YI SYLLABLE HNOT;Lo;0;L;;;;;N;;;;;
+A177;YI SYLLABLE HNOX;Lo;0;L;;;;;N;;;;;
+A178;YI SYLLABLE HNOP;Lo;0;L;;;;;N;;;;;
+A179;YI SYLLABLE HNEX;Lo;0;L;;;;;N;;;;;
+A17A;YI SYLLABLE HNE;Lo;0;L;;;;;N;;;;;
+A17B;YI SYLLABLE HNEP;Lo;0;L;;;;;N;;;;;
+A17C;YI SYLLABLE HNUT;Lo;0;L;;;;;N;;;;;
+A17D;YI SYLLABLE NIT;Lo;0;L;;;;;N;;;;;
+A17E;YI SYLLABLE NIX;Lo;0;L;;;;;N;;;;;
+A17F;YI SYLLABLE NI;Lo;0;L;;;;;N;;;;;
+A180;YI SYLLABLE NIP;Lo;0;L;;;;;N;;;;;
+A181;YI SYLLABLE NIEX;Lo;0;L;;;;;N;;;;;
+A182;YI SYLLABLE NIE;Lo;0;L;;;;;N;;;;;
+A183;YI SYLLABLE NIEP;Lo;0;L;;;;;N;;;;;
+A184;YI SYLLABLE NAX;Lo;0;L;;;;;N;;;;;
+A185;YI SYLLABLE NA;Lo;0;L;;;;;N;;;;;
+A186;YI SYLLABLE NAP;Lo;0;L;;;;;N;;;;;
+A187;YI SYLLABLE NUOX;Lo;0;L;;;;;N;;;;;
+A188;YI SYLLABLE NUO;Lo;0;L;;;;;N;;;;;
+A189;YI SYLLABLE NUOP;Lo;0;L;;;;;N;;;;;
+A18A;YI SYLLABLE NOT;Lo;0;L;;;;;N;;;;;
+A18B;YI SYLLABLE NOX;Lo;0;L;;;;;N;;;;;
+A18C;YI SYLLABLE NO;Lo;0;L;;;;;N;;;;;
+A18D;YI SYLLABLE NOP;Lo;0;L;;;;;N;;;;;
+A18E;YI SYLLABLE NEX;Lo;0;L;;;;;N;;;;;
+A18F;YI SYLLABLE NE;Lo;0;L;;;;;N;;;;;
+A190;YI SYLLABLE NEP;Lo;0;L;;;;;N;;;;;
+A191;YI SYLLABLE NUT;Lo;0;L;;;;;N;;;;;
+A192;YI SYLLABLE NUX;Lo;0;L;;;;;N;;;;;
+A193;YI SYLLABLE NU;Lo;0;L;;;;;N;;;;;
+A194;YI SYLLABLE NUP;Lo;0;L;;;;;N;;;;;
+A195;YI SYLLABLE NURX;Lo;0;L;;;;;N;;;;;
+A196;YI SYLLABLE NUR;Lo;0;L;;;;;N;;;;;
+A197;YI SYLLABLE HLIT;Lo;0;L;;;;;N;;;;;
+A198;YI SYLLABLE HLIX;Lo;0;L;;;;;N;;;;;
+A199;YI SYLLABLE HLI;Lo;0;L;;;;;N;;;;;
+A19A;YI SYLLABLE HLIP;Lo;0;L;;;;;N;;;;;
+A19B;YI SYLLABLE HLIEX;Lo;0;L;;;;;N;;;;;
+A19C;YI SYLLABLE HLIE;Lo;0;L;;;;;N;;;;;
+A19D;YI SYLLABLE HLIEP;Lo;0;L;;;;;N;;;;;
+A19E;YI SYLLABLE HLAT;Lo;0;L;;;;;N;;;;;
+A19F;YI SYLLABLE HLAX;Lo;0;L;;;;;N;;;;;
+A1A0;YI SYLLABLE HLA;Lo;0;L;;;;;N;;;;;
+A1A1;YI SYLLABLE HLAP;Lo;0;L;;;;;N;;;;;
+A1A2;YI SYLLABLE HLUOX;Lo;0;L;;;;;N;;;;;
+A1A3;YI SYLLABLE HLUO;Lo;0;L;;;;;N;;;;;
+A1A4;YI SYLLABLE HLUOP;Lo;0;L;;;;;N;;;;;
+A1A5;YI SYLLABLE HLOX;Lo;0;L;;;;;N;;;;;
+A1A6;YI SYLLABLE HLO;Lo;0;L;;;;;N;;;;;
+A1A7;YI SYLLABLE HLOP;Lo;0;L;;;;;N;;;;;
+A1A8;YI SYLLABLE HLEX;Lo;0;L;;;;;N;;;;;
+A1A9;YI SYLLABLE HLE;Lo;0;L;;;;;N;;;;;
+A1AA;YI SYLLABLE HLEP;Lo;0;L;;;;;N;;;;;
+A1AB;YI SYLLABLE HLUT;Lo;0;L;;;;;N;;;;;
+A1AC;YI SYLLABLE HLUX;Lo;0;L;;;;;N;;;;;
+A1AD;YI SYLLABLE HLU;Lo;0;L;;;;;N;;;;;
+A1AE;YI SYLLABLE HLUP;Lo;0;L;;;;;N;;;;;
+A1AF;YI SYLLABLE HLURX;Lo;0;L;;;;;N;;;;;
+A1B0;YI SYLLABLE HLUR;Lo;0;L;;;;;N;;;;;
+A1B1;YI SYLLABLE HLYT;Lo;0;L;;;;;N;;;;;
+A1B2;YI SYLLABLE HLYX;Lo;0;L;;;;;N;;;;;
+A1B3;YI SYLLABLE HLY;Lo;0;L;;;;;N;;;;;
+A1B4;YI SYLLABLE HLYP;Lo;0;L;;;;;N;;;;;
+A1B5;YI SYLLABLE HLYRX;Lo;0;L;;;;;N;;;;;
+A1B6;YI SYLLABLE HLYR;Lo;0;L;;;;;N;;;;;
+A1B7;YI SYLLABLE LIT;Lo;0;L;;;;;N;;;;;
+A1B8;YI SYLLABLE LIX;Lo;0;L;;;;;N;;;;;
+A1B9;YI SYLLABLE LI;Lo;0;L;;;;;N;;;;;
+A1BA;YI SYLLABLE LIP;Lo;0;L;;;;;N;;;;;
+A1BB;YI SYLLABLE LIET;Lo;0;L;;;;;N;;;;;
+A1BC;YI SYLLABLE LIEX;Lo;0;L;;;;;N;;;;;
+A1BD;YI SYLLABLE LIE;Lo;0;L;;;;;N;;;;;
+A1BE;YI SYLLABLE LIEP;Lo;0;L;;;;;N;;;;;
+A1BF;YI SYLLABLE LAT;Lo;0;L;;;;;N;;;;;
+A1C0;YI SYLLABLE LAX;Lo;0;L;;;;;N;;;;;
+A1C1;YI SYLLABLE LA;Lo;0;L;;;;;N;;;;;
+A1C2;YI SYLLABLE LAP;Lo;0;L;;;;;N;;;;;
+A1C3;YI SYLLABLE LUOT;Lo;0;L;;;;;N;;;;;
+A1C4;YI SYLLABLE LUOX;Lo;0;L;;;;;N;;;;;
+A1C5;YI SYLLABLE LUO;Lo;0;L;;;;;N;;;;;
+A1C6;YI SYLLABLE LUOP;Lo;0;L;;;;;N;;;;;
+A1C7;YI SYLLABLE LOT;Lo;0;L;;;;;N;;;;;
+A1C8;YI SYLLABLE LOX;Lo;0;L;;;;;N;;;;;
+A1C9;YI SYLLABLE LO;Lo;0;L;;;;;N;;;;;
+A1CA;YI SYLLABLE LOP;Lo;0;L;;;;;N;;;;;
+A1CB;YI SYLLABLE LEX;Lo;0;L;;;;;N;;;;;
+A1CC;YI SYLLABLE LE;Lo;0;L;;;;;N;;;;;
+A1CD;YI SYLLABLE LEP;Lo;0;L;;;;;N;;;;;
+A1CE;YI SYLLABLE LUT;Lo;0;L;;;;;N;;;;;
+A1CF;YI SYLLABLE LUX;Lo;0;L;;;;;N;;;;;
+A1D0;YI SYLLABLE LU;Lo;0;L;;;;;N;;;;;
+A1D1;YI SYLLABLE LUP;Lo;0;L;;;;;N;;;;;
+A1D2;YI SYLLABLE LURX;Lo;0;L;;;;;N;;;;;
+A1D3;YI SYLLABLE LUR;Lo;0;L;;;;;N;;;;;
+A1D4;YI SYLLABLE LYT;Lo;0;L;;;;;N;;;;;
+A1D5;YI SYLLABLE LYX;Lo;0;L;;;;;N;;;;;
+A1D6;YI SYLLABLE LY;Lo;0;L;;;;;N;;;;;
+A1D7;YI SYLLABLE LYP;Lo;0;L;;;;;N;;;;;
+A1D8;YI SYLLABLE LYRX;Lo;0;L;;;;;N;;;;;
+A1D9;YI SYLLABLE LYR;Lo;0;L;;;;;N;;;;;
+A1DA;YI SYLLABLE GIT;Lo;0;L;;;;;N;;;;;
+A1DB;YI SYLLABLE GIX;Lo;0;L;;;;;N;;;;;
+A1DC;YI SYLLABLE GI;Lo;0;L;;;;;N;;;;;
+A1DD;YI SYLLABLE GIP;Lo;0;L;;;;;N;;;;;
+A1DE;YI SYLLABLE GIET;Lo;0;L;;;;;N;;;;;
+A1DF;YI SYLLABLE GIEX;Lo;0;L;;;;;N;;;;;
+A1E0;YI SYLLABLE GIE;Lo;0;L;;;;;N;;;;;
+A1E1;YI SYLLABLE GIEP;Lo;0;L;;;;;N;;;;;
+A1E2;YI SYLLABLE GAT;Lo;0;L;;;;;N;;;;;
+A1E3;YI SYLLABLE GAX;Lo;0;L;;;;;N;;;;;
+A1E4;YI SYLLABLE GA;Lo;0;L;;;;;N;;;;;
+A1E5;YI SYLLABLE GAP;Lo;0;L;;;;;N;;;;;
+A1E6;YI SYLLABLE GUOT;Lo;0;L;;;;;N;;;;;
+A1E7;YI SYLLABLE GUOX;Lo;0;L;;;;;N;;;;;
+A1E8;YI SYLLABLE GUO;Lo;0;L;;;;;N;;;;;
+A1E9;YI SYLLABLE GUOP;Lo;0;L;;;;;N;;;;;
+A1EA;YI SYLLABLE GOT;Lo;0;L;;;;;N;;;;;
+A1EB;YI SYLLABLE GOX;Lo;0;L;;;;;N;;;;;
+A1EC;YI SYLLABLE GO;Lo;0;L;;;;;N;;;;;
+A1ED;YI SYLLABLE GOP;Lo;0;L;;;;;N;;;;;
+A1EE;YI SYLLABLE GET;Lo;0;L;;;;;N;;;;;
+A1EF;YI SYLLABLE GEX;Lo;0;L;;;;;N;;;;;
+A1F0;YI SYLLABLE GE;Lo;0;L;;;;;N;;;;;
+A1F1;YI SYLLABLE GEP;Lo;0;L;;;;;N;;;;;
+A1F2;YI SYLLABLE GUT;Lo;0;L;;;;;N;;;;;
+A1F3;YI SYLLABLE GUX;Lo;0;L;;;;;N;;;;;
+A1F4;YI SYLLABLE GU;Lo;0;L;;;;;N;;;;;
+A1F5;YI SYLLABLE GUP;Lo;0;L;;;;;N;;;;;
+A1F6;YI SYLLABLE GURX;Lo;0;L;;;;;N;;;;;
+A1F7;YI SYLLABLE GUR;Lo;0;L;;;;;N;;;;;
+A1F8;YI SYLLABLE KIT;Lo;0;L;;;;;N;;;;;
+A1F9;YI SYLLABLE KIX;Lo;0;L;;;;;N;;;;;
+A1FA;YI SYLLABLE KI;Lo;0;L;;;;;N;;;;;
+A1FB;YI SYLLABLE KIP;Lo;0;L;;;;;N;;;;;
+A1FC;YI SYLLABLE KIEX;Lo;0;L;;;;;N;;;;;
+A1FD;YI SYLLABLE KIE;Lo;0;L;;;;;N;;;;;
+A1FE;YI SYLLABLE KIEP;Lo;0;L;;;;;N;;;;;
+A1FF;YI SYLLABLE KAT;Lo;0;L;;;;;N;;;;;
+A200;YI SYLLABLE KAX;Lo;0;L;;;;;N;;;;;
+A201;YI SYLLABLE KA;Lo;0;L;;;;;N;;;;;
+A202;YI SYLLABLE KAP;Lo;0;L;;;;;N;;;;;
+A203;YI SYLLABLE KUOX;Lo;0;L;;;;;N;;;;;
+A204;YI SYLLABLE KUO;Lo;0;L;;;;;N;;;;;
+A205;YI SYLLABLE KUOP;Lo;0;L;;;;;N;;;;;
+A206;YI SYLLABLE KOT;Lo;0;L;;;;;N;;;;;
+A207;YI SYLLABLE KOX;Lo;0;L;;;;;N;;;;;
+A208;YI SYLLABLE KO;Lo;0;L;;;;;N;;;;;
+A209;YI SYLLABLE KOP;Lo;0;L;;;;;N;;;;;
+A20A;YI SYLLABLE KET;Lo;0;L;;;;;N;;;;;
+A20B;YI SYLLABLE KEX;Lo;0;L;;;;;N;;;;;
+A20C;YI SYLLABLE KE;Lo;0;L;;;;;N;;;;;
+A20D;YI SYLLABLE KEP;Lo;0;L;;;;;N;;;;;
+A20E;YI SYLLABLE KUT;Lo;0;L;;;;;N;;;;;
+A20F;YI SYLLABLE KUX;Lo;0;L;;;;;N;;;;;
+A210;YI SYLLABLE KU;Lo;0;L;;;;;N;;;;;
+A211;YI SYLLABLE KUP;Lo;0;L;;;;;N;;;;;
+A212;YI SYLLABLE KURX;Lo;0;L;;;;;N;;;;;
+A213;YI SYLLABLE KUR;Lo;0;L;;;;;N;;;;;
+A214;YI SYLLABLE GGIT;Lo;0;L;;;;;N;;;;;
+A215;YI SYLLABLE GGIX;Lo;0;L;;;;;N;;;;;
+A216;YI SYLLABLE GGI;Lo;0;L;;;;;N;;;;;
+A217;YI SYLLABLE GGIEX;Lo;0;L;;;;;N;;;;;
+A218;YI SYLLABLE GGIE;Lo;0;L;;;;;N;;;;;
+A219;YI SYLLABLE GGIEP;Lo;0;L;;;;;N;;;;;
+A21A;YI SYLLABLE GGAT;Lo;0;L;;;;;N;;;;;
+A21B;YI SYLLABLE GGAX;Lo;0;L;;;;;N;;;;;
+A21C;YI SYLLABLE GGA;Lo;0;L;;;;;N;;;;;
+A21D;YI SYLLABLE GGAP;Lo;0;L;;;;;N;;;;;
+A21E;YI SYLLABLE GGUOT;Lo;0;L;;;;;N;;;;;
+A21F;YI SYLLABLE GGUOX;Lo;0;L;;;;;N;;;;;
+A220;YI SYLLABLE GGUO;Lo;0;L;;;;;N;;;;;
+A221;YI SYLLABLE GGUOP;Lo;0;L;;;;;N;;;;;
+A222;YI SYLLABLE GGOT;Lo;0;L;;;;;N;;;;;
+A223;YI SYLLABLE GGOX;Lo;0;L;;;;;N;;;;;
+A224;YI SYLLABLE GGO;Lo;0;L;;;;;N;;;;;
+A225;YI SYLLABLE GGOP;Lo;0;L;;;;;N;;;;;
+A226;YI SYLLABLE GGET;Lo;0;L;;;;;N;;;;;
+A227;YI SYLLABLE GGEX;Lo;0;L;;;;;N;;;;;
+A228;YI SYLLABLE GGE;Lo;0;L;;;;;N;;;;;
+A229;YI SYLLABLE GGEP;Lo;0;L;;;;;N;;;;;
+A22A;YI SYLLABLE GGUT;Lo;0;L;;;;;N;;;;;
+A22B;YI SYLLABLE GGUX;Lo;0;L;;;;;N;;;;;
+A22C;YI SYLLABLE GGU;Lo;0;L;;;;;N;;;;;
+A22D;YI SYLLABLE GGUP;Lo;0;L;;;;;N;;;;;
+A22E;YI SYLLABLE GGURX;Lo;0;L;;;;;N;;;;;
+A22F;YI SYLLABLE GGUR;Lo;0;L;;;;;N;;;;;
+A230;YI SYLLABLE MGIEX;Lo;0;L;;;;;N;;;;;
+A231;YI SYLLABLE MGIE;Lo;0;L;;;;;N;;;;;
+A232;YI SYLLABLE MGAT;Lo;0;L;;;;;N;;;;;
+A233;YI SYLLABLE MGAX;Lo;0;L;;;;;N;;;;;
+A234;YI SYLLABLE MGA;Lo;0;L;;;;;N;;;;;
+A235;YI SYLLABLE MGAP;Lo;0;L;;;;;N;;;;;
+A236;YI SYLLABLE MGUOX;Lo;0;L;;;;;N;;;;;
+A237;YI SYLLABLE MGUO;Lo;0;L;;;;;N;;;;;
+A238;YI SYLLABLE MGUOP;Lo;0;L;;;;;N;;;;;
+A239;YI SYLLABLE MGOT;Lo;0;L;;;;;N;;;;;
+A23A;YI SYLLABLE MGOX;Lo;0;L;;;;;N;;;;;
+A23B;YI SYLLABLE MGO;Lo;0;L;;;;;N;;;;;
+A23C;YI SYLLABLE MGOP;Lo;0;L;;;;;N;;;;;
+A23D;YI SYLLABLE MGEX;Lo;0;L;;;;;N;;;;;
+A23E;YI SYLLABLE MGE;Lo;0;L;;;;;N;;;;;
+A23F;YI SYLLABLE MGEP;Lo;0;L;;;;;N;;;;;
+A240;YI SYLLABLE MGUT;Lo;0;L;;;;;N;;;;;
+A241;YI SYLLABLE MGUX;Lo;0;L;;;;;N;;;;;
+A242;YI SYLLABLE MGU;Lo;0;L;;;;;N;;;;;
+A243;YI SYLLABLE MGUP;Lo;0;L;;;;;N;;;;;
+A244;YI SYLLABLE MGURX;Lo;0;L;;;;;N;;;;;
+A245;YI SYLLABLE MGUR;Lo;0;L;;;;;N;;;;;
+A246;YI SYLLABLE HXIT;Lo;0;L;;;;;N;;;;;
+A247;YI SYLLABLE HXIX;Lo;0;L;;;;;N;;;;;
+A248;YI SYLLABLE HXI;Lo;0;L;;;;;N;;;;;
+A249;YI SYLLABLE HXIP;Lo;0;L;;;;;N;;;;;
+A24A;YI SYLLABLE HXIET;Lo;0;L;;;;;N;;;;;
+A24B;YI SYLLABLE HXIEX;Lo;0;L;;;;;N;;;;;
+A24C;YI SYLLABLE HXIE;Lo;0;L;;;;;N;;;;;
+A24D;YI SYLLABLE HXIEP;Lo;0;L;;;;;N;;;;;
+A24E;YI SYLLABLE HXAT;Lo;0;L;;;;;N;;;;;
+A24F;YI SYLLABLE HXAX;Lo;0;L;;;;;N;;;;;
+A250;YI SYLLABLE HXA;Lo;0;L;;;;;N;;;;;
+A251;YI SYLLABLE HXAP;Lo;0;L;;;;;N;;;;;
+A252;YI SYLLABLE HXUOT;Lo;0;L;;;;;N;;;;;
+A253;YI SYLLABLE HXUOX;Lo;0;L;;;;;N;;;;;
+A254;YI SYLLABLE HXUO;Lo;0;L;;;;;N;;;;;
+A255;YI SYLLABLE HXUOP;Lo;0;L;;;;;N;;;;;
+A256;YI SYLLABLE HXOT;Lo;0;L;;;;;N;;;;;
+A257;YI SYLLABLE HXOX;Lo;0;L;;;;;N;;;;;
+A258;YI SYLLABLE HXO;Lo;0;L;;;;;N;;;;;
+A259;YI SYLLABLE HXOP;Lo;0;L;;;;;N;;;;;
+A25A;YI SYLLABLE HXEX;Lo;0;L;;;;;N;;;;;
+A25B;YI SYLLABLE HXE;Lo;0;L;;;;;N;;;;;
+A25C;YI SYLLABLE HXEP;Lo;0;L;;;;;N;;;;;
+A25D;YI SYLLABLE NGIEX;Lo;0;L;;;;;N;;;;;
+A25E;YI SYLLABLE NGIE;Lo;0;L;;;;;N;;;;;
+A25F;YI SYLLABLE NGIEP;Lo;0;L;;;;;N;;;;;
+A260;YI SYLLABLE NGAT;Lo;0;L;;;;;N;;;;;
+A261;YI SYLLABLE NGAX;Lo;0;L;;;;;N;;;;;
+A262;YI SYLLABLE NGA;Lo;0;L;;;;;N;;;;;
+A263;YI SYLLABLE NGAP;Lo;0;L;;;;;N;;;;;
+A264;YI SYLLABLE NGUOT;Lo;0;L;;;;;N;;;;;
+A265;YI SYLLABLE NGUOX;Lo;0;L;;;;;N;;;;;
+A266;YI SYLLABLE NGUO;Lo;0;L;;;;;N;;;;;
+A267;YI SYLLABLE NGOT;Lo;0;L;;;;;N;;;;;
+A268;YI SYLLABLE NGOX;Lo;0;L;;;;;N;;;;;
+A269;YI SYLLABLE NGO;Lo;0;L;;;;;N;;;;;
+A26A;YI SYLLABLE NGOP;Lo;0;L;;;;;N;;;;;
+A26B;YI SYLLABLE NGEX;Lo;0;L;;;;;N;;;;;
+A26C;YI SYLLABLE NGE;Lo;0;L;;;;;N;;;;;
+A26D;YI SYLLABLE NGEP;Lo;0;L;;;;;N;;;;;
+A26E;YI SYLLABLE HIT;Lo;0;L;;;;;N;;;;;
+A26F;YI SYLLABLE HIEX;Lo;0;L;;;;;N;;;;;
+A270;YI SYLLABLE HIE;Lo;0;L;;;;;N;;;;;
+A271;YI SYLLABLE HAT;Lo;0;L;;;;;N;;;;;
+A272;YI SYLLABLE HAX;Lo;0;L;;;;;N;;;;;
+A273;YI SYLLABLE HA;Lo;0;L;;;;;N;;;;;
+A274;YI SYLLABLE HAP;Lo;0;L;;;;;N;;;;;
+A275;YI SYLLABLE HUOT;Lo;0;L;;;;;N;;;;;
+A276;YI SYLLABLE HUOX;Lo;0;L;;;;;N;;;;;
+A277;YI SYLLABLE HUO;Lo;0;L;;;;;N;;;;;
+A278;YI SYLLABLE HUOP;Lo;0;L;;;;;N;;;;;
+A279;YI SYLLABLE HOT;Lo;0;L;;;;;N;;;;;
+A27A;YI SYLLABLE HOX;Lo;0;L;;;;;N;;;;;
+A27B;YI SYLLABLE HO;Lo;0;L;;;;;N;;;;;
+A27C;YI SYLLABLE HOP;Lo;0;L;;;;;N;;;;;
+A27D;YI SYLLABLE HEX;Lo;0;L;;;;;N;;;;;
+A27E;YI SYLLABLE HE;Lo;0;L;;;;;N;;;;;
+A27F;YI SYLLABLE HEP;Lo;0;L;;;;;N;;;;;
+A280;YI SYLLABLE WAT;Lo;0;L;;;;;N;;;;;
+A281;YI SYLLABLE WAX;Lo;0;L;;;;;N;;;;;
+A282;YI SYLLABLE WA;Lo;0;L;;;;;N;;;;;
+A283;YI SYLLABLE WAP;Lo;0;L;;;;;N;;;;;
+A284;YI SYLLABLE WUOX;Lo;0;L;;;;;N;;;;;
+A285;YI SYLLABLE WUO;Lo;0;L;;;;;N;;;;;
+A286;YI SYLLABLE WUOP;Lo;0;L;;;;;N;;;;;
+A287;YI SYLLABLE WOX;Lo;0;L;;;;;N;;;;;
+A288;YI SYLLABLE WO;Lo;0;L;;;;;N;;;;;
+A289;YI SYLLABLE WOP;Lo;0;L;;;;;N;;;;;
+A28A;YI SYLLABLE WEX;Lo;0;L;;;;;N;;;;;
+A28B;YI SYLLABLE WE;Lo;0;L;;;;;N;;;;;
+A28C;YI SYLLABLE WEP;Lo;0;L;;;;;N;;;;;
+A28D;YI SYLLABLE ZIT;Lo;0;L;;;;;N;;;;;
+A28E;YI SYLLABLE ZIX;Lo;0;L;;;;;N;;;;;
+A28F;YI SYLLABLE ZI;Lo;0;L;;;;;N;;;;;
+A290;YI SYLLABLE ZIP;Lo;0;L;;;;;N;;;;;
+A291;YI SYLLABLE ZIEX;Lo;0;L;;;;;N;;;;;
+A292;YI SYLLABLE ZIE;Lo;0;L;;;;;N;;;;;
+A293;YI SYLLABLE ZIEP;Lo;0;L;;;;;N;;;;;
+A294;YI SYLLABLE ZAT;Lo;0;L;;;;;N;;;;;
+A295;YI SYLLABLE ZAX;Lo;0;L;;;;;N;;;;;
+A296;YI SYLLABLE ZA;Lo;0;L;;;;;N;;;;;
+A297;YI SYLLABLE ZAP;Lo;0;L;;;;;N;;;;;
+A298;YI SYLLABLE ZUOX;Lo;0;L;;;;;N;;;;;
+A299;YI SYLLABLE ZUO;Lo;0;L;;;;;N;;;;;
+A29A;YI SYLLABLE ZUOP;Lo;0;L;;;;;N;;;;;
+A29B;YI SYLLABLE ZOT;Lo;0;L;;;;;N;;;;;
+A29C;YI SYLLABLE ZOX;Lo;0;L;;;;;N;;;;;
+A29D;YI SYLLABLE ZO;Lo;0;L;;;;;N;;;;;
+A29E;YI SYLLABLE ZOP;Lo;0;L;;;;;N;;;;;
+A29F;YI SYLLABLE ZEX;Lo;0;L;;;;;N;;;;;
+A2A0;YI SYLLABLE ZE;Lo;0;L;;;;;N;;;;;
+A2A1;YI SYLLABLE ZEP;Lo;0;L;;;;;N;;;;;
+A2A2;YI SYLLABLE ZUT;Lo;0;L;;;;;N;;;;;
+A2A3;YI SYLLABLE ZUX;Lo;0;L;;;;;N;;;;;
+A2A4;YI SYLLABLE ZU;Lo;0;L;;;;;N;;;;;
+A2A5;YI SYLLABLE ZUP;Lo;0;L;;;;;N;;;;;
+A2A6;YI SYLLABLE ZURX;Lo;0;L;;;;;N;;;;;
+A2A7;YI SYLLABLE ZUR;Lo;0;L;;;;;N;;;;;
+A2A8;YI SYLLABLE ZYT;Lo;0;L;;;;;N;;;;;
+A2A9;YI SYLLABLE ZYX;Lo;0;L;;;;;N;;;;;
+A2AA;YI SYLLABLE ZY;Lo;0;L;;;;;N;;;;;
+A2AB;YI SYLLABLE ZYP;Lo;0;L;;;;;N;;;;;
+A2AC;YI SYLLABLE ZYRX;Lo;0;L;;;;;N;;;;;
+A2AD;YI SYLLABLE ZYR;Lo;0;L;;;;;N;;;;;
+A2AE;YI SYLLABLE CIT;Lo;0;L;;;;;N;;;;;
+A2AF;YI SYLLABLE CIX;Lo;0;L;;;;;N;;;;;
+A2B0;YI SYLLABLE CI;Lo;0;L;;;;;N;;;;;
+A2B1;YI SYLLABLE CIP;Lo;0;L;;;;;N;;;;;
+A2B2;YI SYLLABLE CIET;Lo;0;L;;;;;N;;;;;
+A2B3;YI SYLLABLE CIEX;Lo;0;L;;;;;N;;;;;
+A2B4;YI SYLLABLE CIE;Lo;0;L;;;;;N;;;;;
+A2B5;YI SYLLABLE CIEP;Lo;0;L;;;;;N;;;;;
+A2B6;YI SYLLABLE CAT;Lo;0;L;;;;;N;;;;;
+A2B7;YI SYLLABLE CAX;Lo;0;L;;;;;N;;;;;
+A2B8;YI SYLLABLE CA;Lo;0;L;;;;;N;;;;;
+A2B9;YI SYLLABLE CAP;Lo;0;L;;;;;N;;;;;
+A2BA;YI SYLLABLE CUOX;Lo;0;L;;;;;N;;;;;
+A2BB;YI SYLLABLE CUO;Lo;0;L;;;;;N;;;;;
+A2BC;YI SYLLABLE CUOP;Lo;0;L;;;;;N;;;;;
+A2BD;YI SYLLABLE COT;Lo;0;L;;;;;N;;;;;
+A2BE;YI SYLLABLE COX;Lo;0;L;;;;;N;;;;;
+A2BF;YI SYLLABLE CO;Lo;0;L;;;;;N;;;;;
+A2C0;YI SYLLABLE COP;Lo;0;L;;;;;N;;;;;
+A2C1;YI SYLLABLE CEX;Lo;0;L;;;;;N;;;;;
+A2C2;YI SYLLABLE CE;Lo;0;L;;;;;N;;;;;
+A2C3;YI SYLLABLE CEP;Lo;0;L;;;;;N;;;;;
+A2C4;YI SYLLABLE CUT;Lo;0;L;;;;;N;;;;;
+A2C5;YI SYLLABLE CUX;Lo;0;L;;;;;N;;;;;
+A2C6;YI SYLLABLE CU;Lo;0;L;;;;;N;;;;;
+A2C7;YI SYLLABLE CUP;Lo;0;L;;;;;N;;;;;
+A2C8;YI SYLLABLE CURX;Lo;0;L;;;;;N;;;;;
+A2C9;YI SYLLABLE CUR;Lo;0;L;;;;;N;;;;;
+A2CA;YI SYLLABLE CYT;Lo;0;L;;;;;N;;;;;
+A2CB;YI SYLLABLE CYX;Lo;0;L;;;;;N;;;;;
+A2CC;YI SYLLABLE CY;Lo;0;L;;;;;N;;;;;
+A2CD;YI SYLLABLE CYP;Lo;0;L;;;;;N;;;;;
+A2CE;YI SYLLABLE CYRX;Lo;0;L;;;;;N;;;;;
+A2CF;YI SYLLABLE CYR;Lo;0;L;;;;;N;;;;;
+A2D0;YI SYLLABLE ZZIT;Lo;0;L;;;;;N;;;;;
+A2D1;YI SYLLABLE ZZIX;Lo;0;L;;;;;N;;;;;
+A2D2;YI SYLLABLE ZZI;Lo;0;L;;;;;N;;;;;
+A2D3;YI SYLLABLE ZZIP;Lo;0;L;;;;;N;;;;;
+A2D4;YI SYLLABLE ZZIET;Lo;0;L;;;;;N;;;;;
+A2D5;YI SYLLABLE ZZIEX;Lo;0;L;;;;;N;;;;;
+A2D6;YI SYLLABLE ZZIE;Lo;0;L;;;;;N;;;;;
+A2D7;YI SYLLABLE ZZIEP;Lo;0;L;;;;;N;;;;;
+A2D8;YI SYLLABLE ZZAT;Lo;0;L;;;;;N;;;;;
+A2D9;YI SYLLABLE ZZAX;Lo;0;L;;;;;N;;;;;
+A2DA;YI SYLLABLE ZZA;Lo;0;L;;;;;N;;;;;
+A2DB;YI SYLLABLE ZZAP;Lo;0;L;;;;;N;;;;;
+A2DC;YI SYLLABLE ZZOX;Lo;0;L;;;;;N;;;;;
+A2DD;YI SYLLABLE ZZO;Lo;0;L;;;;;N;;;;;
+A2DE;YI SYLLABLE ZZOP;Lo;0;L;;;;;N;;;;;
+A2DF;YI SYLLABLE ZZEX;Lo;0;L;;;;;N;;;;;
+A2E0;YI SYLLABLE ZZE;Lo;0;L;;;;;N;;;;;
+A2E1;YI SYLLABLE ZZEP;Lo;0;L;;;;;N;;;;;
+A2E2;YI SYLLABLE ZZUX;Lo;0;L;;;;;N;;;;;
+A2E3;YI SYLLABLE ZZU;Lo;0;L;;;;;N;;;;;
+A2E4;YI SYLLABLE ZZUP;Lo;0;L;;;;;N;;;;;
+A2E5;YI SYLLABLE ZZURX;Lo;0;L;;;;;N;;;;;
+A2E6;YI SYLLABLE ZZUR;Lo;0;L;;;;;N;;;;;
+A2E7;YI SYLLABLE ZZYT;Lo;0;L;;;;;N;;;;;
+A2E8;YI SYLLABLE ZZYX;Lo;0;L;;;;;N;;;;;
+A2E9;YI SYLLABLE ZZY;Lo;0;L;;;;;N;;;;;
+A2EA;YI SYLLABLE ZZYP;Lo;0;L;;;;;N;;;;;
+A2EB;YI SYLLABLE ZZYRX;Lo;0;L;;;;;N;;;;;
+A2EC;YI SYLLABLE ZZYR;Lo;0;L;;;;;N;;;;;
+A2ED;YI SYLLABLE NZIT;Lo;0;L;;;;;N;;;;;
+A2EE;YI SYLLABLE NZIX;Lo;0;L;;;;;N;;;;;
+A2EF;YI SYLLABLE NZI;Lo;0;L;;;;;N;;;;;
+A2F0;YI SYLLABLE NZIP;Lo;0;L;;;;;N;;;;;
+A2F1;YI SYLLABLE NZIEX;Lo;0;L;;;;;N;;;;;
+A2F2;YI SYLLABLE NZIE;Lo;0;L;;;;;N;;;;;
+A2F3;YI SYLLABLE NZIEP;Lo;0;L;;;;;N;;;;;
+A2F4;YI SYLLABLE NZAT;Lo;0;L;;;;;N;;;;;
+A2F5;YI SYLLABLE NZAX;Lo;0;L;;;;;N;;;;;
+A2F6;YI SYLLABLE NZA;Lo;0;L;;;;;N;;;;;
+A2F7;YI SYLLABLE NZAP;Lo;0;L;;;;;N;;;;;
+A2F8;YI SYLLABLE NZUOX;Lo;0;L;;;;;N;;;;;
+A2F9;YI SYLLABLE NZUO;Lo;0;L;;;;;N;;;;;
+A2FA;YI SYLLABLE NZOX;Lo;0;L;;;;;N;;;;;
+A2FB;YI SYLLABLE NZOP;Lo;0;L;;;;;N;;;;;
+A2FC;YI SYLLABLE NZEX;Lo;0;L;;;;;N;;;;;
+A2FD;YI SYLLABLE NZE;Lo;0;L;;;;;N;;;;;
+A2FE;YI SYLLABLE NZUX;Lo;0;L;;;;;N;;;;;
+A2FF;YI SYLLABLE NZU;Lo;0;L;;;;;N;;;;;
+A300;YI SYLLABLE NZUP;Lo;0;L;;;;;N;;;;;
+A301;YI SYLLABLE NZURX;Lo;0;L;;;;;N;;;;;
+A302;YI SYLLABLE NZUR;Lo;0;L;;;;;N;;;;;
+A303;YI SYLLABLE NZYT;Lo;0;L;;;;;N;;;;;
+A304;YI SYLLABLE NZYX;Lo;0;L;;;;;N;;;;;
+A305;YI SYLLABLE NZY;Lo;0;L;;;;;N;;;;;
+A306;YI SYLLABLE NZYP;Lo;0;L;;;;;N;;;;;
+A307;YI SYLLABLE NZYRX;Lo;0;L;;;;;N;;;;;
+A308;YI SYLLABLE NZYR;Lo;0;L;;;;;N;;;;;
+A309;YI SYLLABLE SIT;Lo;0;L;;;;;N;;;;;
+A30A;YI SYLLABLE SIX;Lo;0;L;;;;;N;;;;;
+A30B;YI SYLLABLE SI;Lo;0;L;;;;;N;;;;;
+A30C;YI SYLLABLE SIP;Lo;0;L;;;;;N;;;;;
+A30D;YI SYLLABLE SIEX;Lo;0;L;;;;;N;;;;;
+A30E;YI SYLLABLE SIE;Lo;0;L;;;;;N;;;;;
+A30F;YI SYLLABLE SIEP;Lo;0;L;;;;;N;;;;;
+A310;YI SYLLABLE SAT;Lo;0;L;;;;;N;;;;;
+A311;YI SYLLABLE SAX;Lo;0;L;;;;;N;;;;;
+A312;YI SYLLABLE SA;Lo;0;L;;;;;N;;;;;
+A313;YI SYLLABLE SAP;Lo;0;L;;;;;N;;;;;
+A314;YI SYLLABLE SUOX;Lo;0;L;;;;;N;;;;;
+A315;YI SYLLABLE SUO;Lo;0;L;;;;;N;;;;;
+A316;YI SYLLABLE SUOP;Lo;0;L;;;;;N;;;;;
+A317;YI SYLLABLE SOT;Lo;0;L;;;;;N;;;;;
+A318;YI SYLLABLE SOX;Lo;0;L;;;;;N;;;;;
+A319;YI SYLLABLE SO;Lo;0;L;;;;;N;;;;;
+A31A;YI SYLLABLE SOP;Lo;0;L;;;;;N;;;;;
+A31B;YI SYLLABLE SEX;Lo;0;L;;;;;N;;;;;
+A31C;YI SYLLABLE SE;Lo;0;L;;;;;N;;;;;
+A31D;YI SYLLABLE SEP;Lo;0;L;;;;;N;;;;;
+A31E;YI SYLLABLE SUT;Lo;0;L;;;;;N;;;;;
+A31F;YI SYLLABLE SUX;Lo;0;L;;;;;N;;;;;
+A320;YI SYLLABLE SU;Lo;0;L;;;;;N;;;;;
+A321;YI SYLLABLE SUP;Lo;0;L;;;;;N;;;;;
+A322;YI SYLLABLE SURX;Lo;0;L;;;;;N;;;;;
+A323;YI SYLLABLE SUR;Lo;0;L;;;;;N;;;;;
+A324;YI SYLLABLE SYT;Lo;0;L;;;;;N;;;;;
+A325;YI SYLLABLE SYX;Lo;0;L;;;;;N;;;;;
+A326;YI SYLLABLE SY;Lo;0;L;;;;;N;;;;;
+A327;YI SYLLABLE SYP;Lo;0;L;;;;;N;;;;;
+A328;YI SYLLABLE SYRX;Lo;0;L;;;;;N;;;;;
+A329;YI SYLLABLE SYR;Lo;0;L;;;;;N;;;;;
+A32A;YI SYLLABLE SSIT;Lo;0;L;;;;;N;;;;;
+A32B;YI SYLLABLE SSIX;Lo;0;L;;;;;N;;;;;
+A32C;YI SYLLABLE SSI;Lo;0;L;;;;;N;;;;;
+A32D;YI SYLLABLE SSIP;Lo;0;L;;;;;N;;;;;
+A32E;YI SYLLABLE SSIEX;Lo;0;L;;;;;N;;;;;
+A32F;YI SYLLABLE SSIE;Lo;0;L;;;;;N;;;;;
+A330;YI SYLLABLE SSIEP;Lo;0;L;;;;;N;;;;;
+A331;YI SYLLABLE SSAT;Lo;0;L;;;;;N;;;;;
+A332;YI SYLLABLE SSAX;Lo;0;L;;;;;N;;;;;
+A333;YI SYLLABLE SSA;Lo;0;L;;;;;N;;;;;
+A334;YI SYLLABLE SSAP;Lo;0;L;;;;;N;;;;;
+A335;YI SYLLABLE SSOT;Lo;0;L;;;;;N;;;;;
+A336;YI SYLLABLE SSOX;Lo;0;L;;;;;N;;;;;
+A337;YI SYLLABLE SSO;Lo;0;L;;;;;N;;;;;
+A338;YI SYLLABLE SSOP;Lo;0;L;;;;;N;;;;;
+A339;YI SYLLABLE SSEX;Lo;0;L;;;;;N;;;;;
+A33A;YI SYLLABLE SSE;Lo;0;L;;;;;N;;;;;
+A33B;YI SYLLABLE SSEP;Lo;0;L;;;;;N;;;;;
+A33C;YI SYLLABLE SSUT;Lo;0;L;;;;;N;;;;;
+A33D;YI SYLLABLE SSUX;Lo;0;L;;;;;N;;;;;
+A33E;YI SYLLABLE SSU;Lo;0;L;;;;;N;;;;;
+A33F;YI SYLLABLE SSUP;Lo;0;L;;;;;N;;;;;
+A340;YI SYLLABLE SSYT;Lo;0;L;;;;;N;;;;;
+A341;YI SYLLABLE SSYX;Lo;0;L;;;;;N;;;;;
+A342;YI SYLLABLE SSY;Lo;0;L;;;;;N;;;;;
+A343;YI SYLLABLE SSYP;Lo;0;L;;;;;N;;;;;
+A344;YI SYLLABLE SSYRX;Lo;0;L;;;;;N;;;;;
+A345;YI SYLLABLE SSYR;Lo;0;L;;;;;N;;;;;
+A346;YI SYLLABLE ZHAT;Lo;0;L;;;;;N;;;;;
+A347;YI SYLLABLE ZHAX;Lo;0;L;;;;;N;;;;;
+A348;YI SYLLABLE ZHA;Lo;0;L;;;;;N;;;;;
+A349;YI SYLLABLE ZHAP;Lo;0;L;;;;;N;;;;;
+A34A;YI SYLLABLE ZHUOX;Lo;0;L;;;;;N;;;;;
+A34B;YI SYLLABLE ZHUO;Lo;0;L;;;;;N;;;;;
+A34C;YI SYLLABLE ZHUOP;Lo;0;L;;;;;N;;;;;
+A34D;YI SYLLABLE ZHOT;Lo;0;L;;;;;N;;;;;
+A34E;YI SYLLABLE ZHOX;Lo;0;L;;;;;N;;;;;
+A34F;YI SYLLABLE ZHO;Lo;0;L;;;;;N;;;;;
+A350;YI SYLLABLE ZHOP;Lo;0;L;;;;;N;;;;;
+A351;YI SYLLABLE ZHET;Lo;0;L;;;;;N;;;;;
+A352;YI SYLLABLE ZHEX;Lo;0;L;;;;;N;;;;;
+A353;YI SYLLABLE ZHE;Lo;0;L;;;;;N;;;;;
+A354;YI SYLLABLE ZHEP;Lo;0;L;;;;;N;;;;;
+A355;YI SYLLABLE ZHUT;Lo;0;L;;;;;N;;;;;
+A356;YI SYLLABLE ZHUX;Lo;0;L;;;;;N;;;;;
+A357;YI SYLLABLE ZHU;Lo;0;L;;;;;N;;;;;
+A358;YI SYLLABLE ZHUP;Lo;0;L;;;;;N;;;;;
+A359;YI SYLLABLE ZHURX;Lo;0;L;;;;;N;;;;;
+A35A;YI SYLLABLE ZHUR;Lo;0;L;;;;;N;;;;;
+A35B;YI SYLLABLE ZHYT;Lo;0;L;;;;;N;;;;;
+A35C;YI SYLLABLE ZHYX;Lo;0;L;;;;;N;;;;;
+A35D;YI SYLLABLE ZHY;Lo;0;L;;;;;N;;;;;
+A35E;YI SYLLABLE ZHYP;Lo;0;L;;;;;N;;;;;
+A35F;YI SYLLABLE ZHYRX;Lo;0;L;;;;;N;;;;;
+A360;YI SYLLABLE ZHYR;Lo;0;L;;;;;N;;;;;
+A361;YI SYLLABLE CHAT;Lo;0;L;;;;;N;;;;;
+A362;YI SYLLABLE CHAX;Lo;0;L;;;;;N;;;;;
+A363;YI SYLLABLE CHA;Lo;0;L;;;;;N;;;;;
+A364;YI SYLLABLE CHAP;Lo;0;L;;;;;N;;;;;
+A365;YI SYLLABLE CHUOT;Lo;0;L;;;;;N;;;;;
+A366;YI SYLLABLE CHUOX;Lo;0;L;;;;;N;;;;;
+A367;YI SYLLABLE CHUO;Lo;0;L;;;;;N;;;;;
+A368;YI SYLLABLE CHUOP;Lo;0;L;;;;;N;;;;;
+A369;YI SYLLABLE CHOT;Lo;0;L;;;;;N;;;;;
+A36A;YI SYLLABLE CHOX;Lo;0;L;;;;;N;;;;;
+A36B;YI SYLLABLE CHO;Lo;0;L;;;;;N;;;;;
+A36C;YI SYLLABLE CHOP;Lo;0;L;;;;;N;;;;;
+A36D;YI SYLLABLE CHET;Lo;0;L;;;;;N;;;;;
+A36E;YI SYLLABLE CHEX;Lo;0;L;;;;;N;;;;;
+A36F;YI SYLLABLE CHE;Lo;0;L;;;;;N;;;;;
+A370;YI SYLLABLE CHEP;Lo;0;L;;;;;N;;;;;
+A371;YI SYLLABLE CHUX;Lo;0;L;;;;;N;;;;;
+A372;YI SYLLABLE CHU;Lo;0;L;;;;;N;;;;;
+A373;YI SYLLABLE CHUP;Lo;0;L;;;;;N;;;;;
+A374;YI SYLLABLE CHURX;Lo;0;L;;;;;N;;;;;
+A375;YI SYLLABLE CHUR;Lo;0;L;;;;;N;;;;;
+A376;YI SYLLABLE CHYT;Lo;0;L;;;;;N;;;;;
+A377;YI SYLLABLE CHYX;Lo;0;L;;;;;N;;;;;
+A378;YI SYLLABLE CHY;Lo;0;L;;;;;N;;;;;
+A379;YI SYLLABLE CHYP;Lo;0;L;;;;;N;;;;;
+A37A;YI SYLLABLE CHYRX;Lo;0;L;;;;;N;;;;;
+A37B;YI SYLLABLE CHYR;Lo;0;L;;;;;N;;;;;
+A37C;YI SYLLABLE RRAX;Lo;0;L;;;;;N;;;;;
+A37D;YI SYLLABLE RRA;Lo;0;L;;;;;N;;;;;
+A37E;YI SYLLABLE RRUOX;Lo;0;L;;;;;N;;;;;
+A37F;YI SYLLABLE RRUO;Lo;0;L;;;;;N;;;;;
+A380;YI SYLLABLE RROT;Lo;0;L;;;;;N;;;;;
+A381;YI SYLLABLE RROX;Lo;0;L;;;;;N;;;;;
+A382;YI SYLLABLE RRO;Lo;0;L;;;;;N;;;;;
+A383;YI SYLLABLE RROP;Lo;0;L;;;;;N;;;;;
+A384;YI SYLLABLE RRET;Lo;0;L;;;;;N;;;;;
+A385;YI SYLLABLE RREX;Lo;0;L;;;;;N;;;;;
+A386;YI SYLLABLE RRE;Lo;0;L;;;;;N;;;;;
+A387;YI SYLLABLE RREP;Lo;0;L;;;;;N;;;;;
+A388;YI SYLLABLE RRUT;Lo;0;L;;;;;N;;;;;
+A389;YI SYLLABLE RRUX;Lo;0;L;;;;;N;;;;;
+A38A;YI SYLLABLE RRU;Lo;0;L;;;;;N;;;;;
+A38B;YI SYLLABLE RRUP;Lo;0;L;;;;;N;;;;;
+A38C;YI SYLLABLE RRURX;Lo;0;L;;;;;N;;;;;
+A38D;YI SYLLABLE RRUR;Lo;0;L;;;;;N;;;;;
+A38E;YI SYLLABLE RRYT;Lo;0;L;;;;;N;;;;;
+A38F;YI SYLLABLE RRYX;Lo;0;L;;;;;N;;;;;
+A390;YI SYLLABLE RRY;Lo;0;L;;;;;N;;;;;
+A391;YI SYLLABLE RRYP;Lo;0;L;;;;;N;;;;;
+A392;YI SYLLABLE RRYRX;Lo;0;L;;;;;N;;;;;
+A393;YI SYLLABLE RRYR;Lo;0;L;;;;;N;;;;;
+A394;YI SYLLABLE NRAT;Lo;0;L;;;;;N;;;;;
+A395;YI SYLLABLE NRAX;Lo;0;L;;;;;N;;;;;
+A396;YI SYLLABLE NRA;Lo;0;L;;;;;N;;;;;
+A397;YI SYLLABLE NRAP;Lo;0;L;;;;;N;;;;;
+A398;YI SYLLABLE NROX;Lo;0;L;;;;;N;;;;;
+A399;YI SYLLABLE NRO;Lo;0;L;;;;;N;;;;;
+A39A;YI SYLLABLE NROP;Lo;0;L;;;;;N;;;;;
+A39B;YI SYLLABLE NRET;Lo;0;L;;;;;N;;;;;
+A39C;YI SYLLABLE NREX;Lo;0;L;;;;;N;;;;;
+A39D;YI SYLLABLE NRE;Lo;0;L;;;;;N;;;;;
+A39E;YI SYLLABLE NREP;Lo;0;L;;;;;N;;;;;
+A39F;YI SYLLABLE NRUT;Lo;0;L;;;;;N;;;;;
+A3A0;YI SYLLABLE NRUX;Lo;0;L;;;;;N;;;;;
+A3A1;YI SYLLABLE NRU;Lo;0;L;;;;;N;;;;;
+A3A2;YI SYLLABLE NRUP;Lo;0;L;;;;;N;;;;;
+A3A3;YI SYLLABLE NRURX;Lo;0;L;;;;;N;;;;;
+A3A4;YI SYLLABLE NRUR;Lo;0;L;;;;;N;;;;;
+A3A5;YI SYLLABLE NRYT;Lo;0;L;;;;;N;;;;;
+A3A6;YI SYLLABLE NRYX;Lo;0;L;;;;;N;;;;;
+A3A7;YI SYLLABLE NRY;Lo;0;L;;;;;N;;;;;
+A3A8;YI SYLLABLE NRYP;Lo;0;L;;;;;N;;;;;
+A3A9;YI SYLLABLE NRYRX;Lo;0;L;;;;;N;;;;;
+A3AA;YI SYLLABLE NRYR;Lo;0;L;;;;;N;;;;;
+A3AB;YI SYLLABLE SHAT;Lo;0;L;;;;;N;;;;;
+A3AC;YI SYLLABLE SHAX;Lo;0;L;;;;;N;;;;;
+A3AD;YI SYLLABLE SHA;Lo;0;L;;;;;N;;;;;
+A3AE;YI SYLLABLE SHAP;Lo;0;L;;;;;N;;;;;
+A3AF;YI SYLLABLE SHUOX;Lo;0;L;;;;;N;;;;;
+A3B0;YI SYLLABLE SHUO;Lo;0;L;;;;;N;;;;;
+A3B1;YI SYLLABLE SHUOP;Lo;0;L;;;;;N;;;;;
+A3B2;YI SYLLABLE SHOT;Lo;0;L;;;;;N;;;;;
+A3B3;YI SYLLABLE SHOX;Lo;0;L;;;;;N;;;;;
+A3B4;YI SYLLABLE SHO;Lo;0;L;;;;;N;;;;;
+A3B5;YI SYLLABLE SHOP;Lo;0;L;;;;;N;;;;;
+A3B6;YI SYLLABLE SHET;Lo;0;L;;;;;N;;;;;
+A3B7;YI SYLLABLE SHEX;Lo;0;L;;;;;N;;;;;
+A3B8;YI SYLLABLE SHE;Lo;0;L;;;;;N;;;;;
+A3B9;YI SYLLABLE SHEP;Lo;0;L;;;;;N;;;;;
+A3BA;YI SYLLABLE SHUT;Lo;0;L;;;;;N;;;;;
+A3BB;YI SYLLABLE SHUX;Lo;0;L;;;;;N;;;;;
+A3BC;YI SYLLABLE SHU;Lo;0;L;;;;;N;;;;;
+A3BD;YI SYLLABLE SHUP;Lo;0;L;;;;;N;;;;;
+A3BE;YI SYLLABLE SHURX;Lo;0;L;;;;;N;;;;;
+A3BF;YI SYLLABLE SHUR;Lo;0;L;;;;;N;;;;;
+A3C0;YI SYLLABLE SHYT;Lo;0;L;;;;;N;;;;;
+A3C1;YI SYLLABLE SHYX;Lo;0;L;;;;;N;;;;;
+A3C2;YI SYLLABLE SHY;Lo;0;L;;;;;N;;;;;
+A3C3;YI SYLLABLE SHYP;Lo;0;L;;;;;N;;;;;
+A3C4;YI SYLLABLE SHYRX;Lo;0;L;;;;;N;;;;;
+A3C5;YI SYLLABLE SHYR;Lo;0;L;;;;;N;;;;;
+A3C6;YI SYLLABLE RAT;Lo;0;L;;;;;N;;;;;
+A3C7;YI SYLLABLE RAX;Lo;0;L;;;;;N;;;;;
+A3C8;YI SYLLABLE RA;Lo;0;L;;;;;N;;;;;
+A3C9;YI SYLLABLE RAP;Lo;0;L;;;;;N;;;;;
+A3CA;YI SYLLABLE RUOX;Lo;0;L;;;;;N;;;;;
+A3CB;YI SYLLABLE RUO;Lo;0;L;;;;;N;;;;;
+A3CC;YI SYLLABLE RUOP;Lo;0;L;;;;;N;;;;;
+A3CD;YI SYLLABLE ROT;Lo;0;L;;;;;N;;;;;
+A3CE;YI SYLLABLE ROX;Lo;0;L;;;;;N;;;;;
+A3CF;YI SYLLABLE RO;Lo;0;L;;;;;N;;;;;
+A3D0;YI SYLLABLE ROP;Lo;0;L;;;;;N;;;;;
+A3D1;YI SYLLABLE REX;Lo;0;L;;;;;N;;;;;
+A3D2;YI SYLLABLE RE;Lo;0;L;;;;;N;;;;;
+A3D3;YI SYLLABLE REP;Lo;0;L;;;;;N;;;;;
+A3D4;YI SYLLABLE RUT;Lo;0;L;;;;;N;;;;;
+A3D5;YI SYLLABLE RUX;Lo;0;L;;;;;N;;;;;
+A3D6;YI SYLLABLE RU;Lo;0;L;;;;;N;;;;;
+A3D7;YI SYLLABLE RUP;Lo;0;L;;;;;N;;;;;
+A3D8;YI SYLLABLE RURX;Lo;0;L;;;;;N;;;;;
+A3D9;YI SYLLABLE RUR;Lo;0;L;;;;;N;;;;;
+A3DA;YI SYLLABLE RYT;Lo;0;L;;;;;N;;;;;
+A3DB;YI SYLLABLE RYX;Lo;0;L;;;;;N;;;;;
+A3DC;YI SYLLABLE RY;Lo;0;L;;;;;N;;;;;
+A3DD;YI SYLLABLE RYP;Lo;0;L;;;;;N;;;;;
+A3DE;YI SYLLABLE RYRX;Lo;0;L;;;;;N;;;;;
+A3DF;YI SYLLABLE RYR;Lo;0;L;;;;;N;;;;;
+A3E0;YI SYLLABLE JIT;Lo;0;L;;;;;N;;;;;
+A3E1;YI SYLLABLE JIX;Lo;0;L;;;;;N;;;;;
+A3E2;YI SYLLABLE JI;Lo;0;L;;;;;N;;;;;
+A3E3;YI SYLLABLE JIP;Lo;0;L;;;;;N;;;;;
+A3E4;YI SYLLABLE JIET;Lo;0;L;;;;;N;;;;;
+A3E5;YI SYLLABLE JIEX;Lo;0;L;;;;;N;;;;;
+A3E6;YI SYLLABLE JIE;Lo;0;L;;;;;N;;;;;
+A3E7;YI SYLLABLE JIEP;Lo;0;L;;;;;N;;;;;
+A3E8;YI SYLLABLE JUOT;Lo;0;L;;;;;N;;;;;
+A3E9;YI SYLLABLE JUOX;Lo;0;L;;;;;N;;;;;
+A3EA;YI SYLLABLE JUO;Lo;0;L;;;;;N;;;;;
+A3EB;YI SYLLABLE JUOP;Lo;0;L;;;;;N;;;;;
+A3EC;YI SYLLABLE JOT;Lo;0;L;;;;;N;;;;;
+A3ED;YI SYLLABLE JOX;Lo;0;L;;;;;N;;;;;
+A3EE;YI SYLLABLE JO;Lo;0;L;;;;;N;;;;;
+A3EF;YI SYLLABLE JOP;Lo;0;L;;;;;N;;;;;
+A3F0;YI SYLLABLE JUT;Lo;0;L;;;;;N;;;;;
+A3F1;YI SYLLABLE JUX;Lo;0;L;;;;;N;;;;;
+A3F2;YI SYLLABLE JU;Lo;0;L;;;;;N;;;;;
+A3F3;YI SYLLABLE JUP;Lo;0;L;;;;;N;;;;;
+A3F4;YI SYLLABLE JURX;Lo;0;L;;;;;N;;;;;
+A3F5;YI SYLLABLE JUR;Lo;0;L;;;;;N;;;;;
+A3F6;YI SYLLABLE JYT;Lo;0;L;;;;;N;;;;;
+A3F7;YI SYLLABLE JYX;Lo;0;L;;;;;N;;;;;
+A3F8;YI SYLLABLE JY;Lo;0;L;;;;;N;;;;;
+A3F9;YI SYLLABLE JYP;Lo;0;L;;;;;N;;;;;
+A3FA;YI SYLLABLE JYRX;Lo;0;L;;;;;N;;;;;
+A3FB;YI SYLLABLE JYR;Lo;0;L;;;;;N;;;;;
+A3FC;YI SYLLABLE QIT;Lo;0;L;;;;;N;;;;;
+A3FD;YI SYLLABLE QIX;Lo;0;L;;;;;N;;;;;
+A3FE;YI SYLLABLE QI;Lo;0;L;;;;;N;;;;;
+A3FF;YI SYLLABLE QIP;Lo;0;L;;;;;N;;;;;
+A400;YI SYLLABLE QIET;Lo;0;L;;;;;N;;;;;
+A401;YI SYLLABLE QIEX;Lo;0;L;;;;;N;;;;;
+A402;YI SYLLABLE QIE;Lo;0;L;;;;;N;;;;;
+A403;YI SYLLABLE QIEP;Lo;0;L;;;;;N;;;;;
+A404;YI SYLLABLE QUOT;Lo;0;L;;;;;N;;;;;
+A405;YI SYLLABLE QUOX;Lo;0;L;;;;;N;;;;;
+A406;YI SYLLABLE QUO;Lo;0;L;;;;;N;;;;;
+A407;YI SYLLABLE QUOP;Lo;0;L;;;;;N;;;;;
+A408;YI SYLLABLE QOT;Lo;0;L;;;;;N;;;;;
+A409;YI SYLLABLE QOX;Lo;0;L;;;;;N;;;;;
+A40A;YI SYLLABLE QO;Lo;0;L;;;;;N;;;;;
+A40B;YI SYLLABLE QOP;Lo;0;L;;;;;N;;;;;
+A40C;YI SYLLABLE QUT;Lo;0;L;;;;;N;;;;;
+A40D;YI SYLLABLE QUX;Lo;0;L;;;;;N;;;;;
+A40E;YI SYLLABLE QU;Lo;0;L;;;;;N;;;;;
+A40F;YI SYLLABLE QUP;Lo;0;L;;;;;N;;;;;
+A410;YI SYLLABLE QURX;Lo;0;L;;;;;N;;;;;
+A411;YI SYLLABLE QUR;Lo;0;L;;;;;N;;;;;
+A412;YI SYLLABLE QYT;Lo;0;L;;;;;N;;;;;
+A413;YI SYLLABLE QYX;Lo;0;L;;;;;N;;;;;
+A414;YI SYLLABLE QY;Lo;0;L;;;;;N;;;;;
+A415;YI SYLLABLE QYP;Lo;0;L;;;;;N;;;;;
+A416;YI SYLLABLE QYRX;Lo;0;L;;;;;N;;;;;
+A417;YI SYLLABLE QYR;Lo;0;L;;;;;N;;;;;
+A418;YI SYLLABLE JJIT;Lo;0;L;;;;;N;;;;;
+A419;YI SYLLABLE JJIX;Lo;0;L;;;;;N;;;;;
+A41A;YI SYLLABLE JJI;Lo;0;L;;;;;N;;;;;
+A41B;YI SYLLABLE JJIP;Lo;0;L;;;;;N;;;;;
+A41C;YI SYLLABLE JJIET;Lo;0;L;;;;;N;;;;;
+A41D;YI SYLLABLE JJIEX;Lo;0;L;;;;;N;;;;;
+A41E;YI SYLLABLE JJIE;Lo;0;L;;;;;N;;;;;
+A41F;YI SYLLABLE JJIEP;Lo;0;L;;;;;N;;;;;
+A420;YI SYLLABLE JJUOX;Lo;0;L;;;;;N;;;;;
+A421;YI SYLLABLE JJUO;Lo;0;L;;;;;N;;;;;
+A422;YI SYLLABLE JJUOP;Lo;0;L;;;;;N;;;;;
+A423;YI SYLLABLE JJOT;Lo;0;L;;;;;N;;;;;
+A424;YI SYLLABLE JJOX;Lo;0;L;;;;;N;;;;;
+A425;YI SYLLABLE JJO;Lo;0;L;;;;;N;;;;;
+A426;YI SYLLABLE JJOP;Lo;0;L;;;;;N;;;;;
+A427;YI SYLLABLE JJUT;Lo;0;L;;;;;N;;;;;
+A428;YI SYLLABLE JJUX;Lo;0;L;;;;;N;;;;;
+A429;YI SYLLABLE JJU;Lo;0;L;;;;;N;;;;;
+A42A;YI SYLLABLE JJUP;Lo;0;L;;;;;N;;;;;
+A42B;YI SYLLABLE JJURX;Lo;0;L;;;;;N;;;;;
+A42C;YI SYLLABLE JJUR;Lo;0;L;;;;;N;;;;;
+A42D;YI SYLLABLE JJYT;Lo;0;L;;;;;N;;;;;
+A42E;YI SYLLABLE JJYX;Lo;0;L;;;;;N;;;;;
+A42F;YI SYLLABLE JJY;Lo;0;L;;;;;N;;;;;
+A430;YI SYLLABLE JJYP;Lo;0;L;;;;;N;;;;;
+A431;YI SYLLABLE NJIT;Lo;0;L;;;;;N;;;;;
+A432;YI SYLLABLE NJIX;Lo;0;L;;;;;N;;;;;
+A433;YI SYLLABLE NJI;Lo;0;L;;;;;N;;;;;
+A434;YI SYLLABLE NJIP;Lo;0;L;;;;;N;;;;;
+A435;YI SYLLABLE NJIET;Lo;0;L;;;;;N;;;;;
+A436;YI SYLLABLE NJIEX;Lo;0;L;;;;;N;;;;;
+A437;YI SYLLABLE NJIE;Lo;0;L;;;;;N;;;;;
+A438;YI SYLLABLE NJIEP;Lo;0;L;;;;;N;;;;;
+A439;YI SYLLABLE NJUOX;Lo;0;L;;;;;N;;;;;
+A43A;YI SYLLABLE NJUO;Lo;0;L;;;;;N;;;;;
+A43B;YI SYLLABLE NJOT;Lo;0;L;;;;;N;;;;;
+A43C;YI SYLLABLE NJOX;Lo;0;L;;;;;N;;;;;
+A43D;YI SYLLABLE NJO;Lo;0;L;;;;;N;;;;;
+A43E;YI SYLLABLE NJOP;Lo;0;L;;;;;N;;;;;
+A43F;YI SYLLABLE NJUX;Lo;0;L;;;;;N;;;;;
+A440;YI SYLLABLE NJU;Lo;0;L;;;;;N;;;;;
+A441;YI SYLLABLE NJUP;Lo;0;L;;;;;N;;;;;
+A442;YI SYLLABLE NJURX;Lo;0;L;;;;;N;;;;;
+A443;YI SYLLABLE NJUR;Lo;0;L;;;;;N;;;;;
+A444;YI SYLLABLE NJYT;Lo;0;L;;;;;N;;;;;
+A445;YI SYLLABLE NJYX;Lo;0;L;;;;;N;;;;;
+A446;YI SYLLABLE NJY;Lo;0;L;;;;;N;;;;;
+A447;YI SYLLABLE NJYP;Lo;0;L;;;;;N;;;;;
+A448;YI SYLLABLE NJYRX;Lo;0;L;;;;;N;;;;;
+A449;YI SYLLABLE NJYR;Lo;0;L;;;;;N;;;;;
+A44A;YI SYLLABLE NYIT;Lo;0;L;;;;;N;;;;;
+A44B;YI SYLLABLE NYIX;Lo;0;L;;;;;N;;;;;
+A44C;YI SYLLABLE NYI;Lo;0;L;;;;;N;;;;;
+A44D;YI SYLLABLE NYIP;Lo;0;L;;;;;N;;;;;
+A44E;YI SYLLABLE NYIET;Lo;0;L;;;;;N;;;;;
+A44F;YI SYLLABLE NYIEX;Lo;0;L;;;;;N;;;;;
+A450;YI SYLLABLE NYIE;Lo;0;L;;;;;N;;;;;
+A451;YI SYLLABLE NYIEP;Lo;0;L;;;;;N;;;;;
+A452;YI SYLLABLE NYUOX;Lo;0;L;;;;;N;;;;;
+A453;YI SYLLABLE NYUO;Lo;0;L;;;;;N;;;;;
+A454;YI SYLLABLE NYUOP;Lo;0;L;;;;;N;;;;;
+A455;YI SYLLABLE NYOT;Lo;0;L;;;;;N;;;;;
+A456;YI SYLLABLE NYOX;Lo;0;L;;;;;N;;;;;
+A457;YI SYLLABLE NYO;Lo;0;L;;;;;N;;;;;
+A458;YI SYLLABLE NYOP;Lo;0;L;;;;;N;;;;;
+A459;YI SYLLABLE NYUT;Lo;0;L;;;;;N;;;;;
+A45A;YI SYLLABLE NYUX;Lo;0;L;;;;;N;;;;;
+A45B;YI SYLLABLE NYU;Lo;0;L;;;;;N;;;;;
+A45C;YI SYLLABLE NYUP;Lo;0;L;;;;;N;;;;;
+A45D;YI SYLLABLE XIT;Lo;0;L;;;;;N;;;;;
+A45E;YI SYLLABLE XIX;Lo;0;L;;;;;N;;;;;
+A45F;YI SYLLABLE XI;Lo;0;L;;;;;N;;;;;
+A460;YI SYLLABLE XIP;Lo;0;L;;;;;N;;;;;
+A461;YI SYLLABLE XIET;Lo;0;L;;;;;N;;;;;
+A462;YI SYLLABLE XIEX;Lo;0;L;;;;;N;;;;;
+A463;YI SYLLABLE XIE;Lo;0;L;;;;;N;;;;;
+A464;YI SYLLABLE XIEP;Lo;0;L;;;;;N;;;;;
+A465;YI SYLLABLE XUOX;Lo;0;L;;;;;N;;;;;
+A466;YI SYLLABLE XUO;Lo;0;L;;;;;N;;;;;
+A467;YI SYLLABLE XOT;Lo;0;L;;;;;N;;;;;
+A468;YI SYLLABLE XOX;Lo;0;L;;;;;N;;;;;
+A469;YI SYLLABLE XO;Lo;0;L;;;;;N;;;;;
+A46A;YI SYLLABLE XOP;Lo;0;L;;;;;N;;;;;
+A46B;YI SYLLABLE XYT;Lo;0;L;;;;;N;;;;;
+A46C;YI SYLLABLE XYX;Lo;0;L;;;;;N;;;;;
+A46D;YI SYLLABLE XY;Lo;0;L;;;;;N;;;;;
+A46E;YI SYLLABLE XYP;Lo;0;L;;;;;N;;;;;
+A46F;YI SYLLABLE XYRX;Lo;0;L;;;;;N;;;;;
+A470;YI SYLLABLE XYR;Lo;0;L;;;;;N;;;;;
+A471;YI SYLLABLE YIT;Lo;0;L;;;;;N;;;;;
+A472;YI SYLLABLE YIX;Lo;0;L;;;;;N;;;;;
+A473;YI SYLLABLE YI;Lo;0;L;;;;;N;;;;;
+A474;YI SYLLABLE YIP;Lo;0;L;;;;;N;;;;;
+A475;YI SYLLABLE YIET;Lo;0;L;;;;;N;;;;;
+A476;YI SYLLABLE YIEX;Lo;0;L;;;;;N;;;;;
+A477;YI SYLLABLE YIE;Lo;0;L;;;;;N;;;;;
+A478;YI SYLLABLE YIEP;Lo;0;L;;;;;N;;;;;
+A479;YI SYLLABLE YUOT;Lo;0;L;;;;;N;;;;;
+A47A;YI SYLLABLE YUOX;Lo;0;L;;;;;N;;;;;
+A47B;YI SYLLABLE YUO;Lo;0;L;;;;;N;;;;;
+A47C;YI SYLLABLE YUOP;Lo;0;L;;;;;N;;;;;
+A47D;YI SYLLABLE YOT;Lo;0;L;;;;;N;;;;;
+A47E;YI SYLLABLE YOX;Lo;0;L;;;;;N;;;;;
+A47F;YI SYLLABLE YO;Lo;0;L;;;;;N;;;;;
+A480;YI SYLLABLE YOP;Lo;0;L;;;;;N;;;;;
+A481;YI SYLLABLE YUT;Lo;0;L;;;;;N;;;;;
+A482;YI SYLLABLE YUX;Lo;0;L;;;;;N;;;;;
+A483;YI SYLLABLE YU;Lo;0;L;;;;;N;;;;;
+A484;YI SYLLABLE YUP;Lo;0;L;;;;;N;;;;;
+A485;YI SYLLABLE YURX;Lo;0;L;;;;;N;;;;;
+A486;YI SYLLABLE YUR;Lo;0;L;;;;;N;;;;;
+A487;YI SYLLABLE YYT;Lo;0;L;;;;;N;;;;;
+A488;YI SYLLABLE YYX;Lo;0;L;;;;;N;;;;;
+A489;YI SYLLABLE YY;Lo;0;L;;;;;N;;;;;
+A48A;YI SYLLABLE YYP;Lo;0;L;;;;;N;;;;;
+A48B;YI SYLLABLE YYRX;Lo;0;L;;;;;N;;;;;
+A48C;YI SYLLABLE YYR;Lo;0;L;;;;;N;;;;;
+A490;YI RADICAL QOT;So;0;ON;;;;;N;;;;;
+A491;YI RADICAL LI;So;0;ON;;;;;N;;;;;
+A492;YI RADICAL KIT;So;0;ON;;;;;N;;;;;
+A493;YI RADICAL NYIP;So;0;ON;;;;;N;;;;;
+A494;YI RADICAL CYP;So;0;ON;;;;;N;;;;;
+A495;YI RADICAL SSI;So;0;ON;;;;;N;;;;;
+A496;YI RADICAL GGOP;So;0;ON;;;;;N;;;;;
+A497;YI RADICAL GEP;So;0;ON;;;;;N;;;;;
+A498;YI RADICAL MI;So;0;ON;;;;;N;;;;;
+A499;YI RADICAL HXIT;So;0;ON;;;;;N;;;;;
+A49A;YI RADICAL LYR;So;0;ON;;;;;N;;;;;
+A49B;YI RADICAL BBUT;So;0;ON;;;;;N;;;;;
+A49C;YI RADICAL MOP;So;0;ON;;;;;N;;;;;
+A49D;YI RADICAL YO;So;0;ON;;;;;N;;;;;
+A49E;YI RADICAL PUT;So;0;ON;;;;;N;;;;;
+A49F;YI RADICAL HXUO;So;0;ON;;;;;N;;;;;
+A4A0;YI RADICAL TAT;So;0;ON;;;;;N;;;;;
+A4A1;YI RADICAL GA;So;0;ON;;;;;N;;;;;
+A4A2;YI RADICAL ZUP;So;0;ON;;;;;N;;;;;
+A4A3;YI RADICAL CYT;So;0;ON;;;;;N;;;;;
+A4A4;YI RADICAL DDUR;So;0;ON;;;;;N;;;;;
+A4A5;YI RADICAL BUR;So;0;ON;;;;;N;;;;;
+A4A6;YI RADICAL GGUO;So;0;ON;;;;;N;;;;;
+A4A7;YI RADICAL NYOP;So;0;ON;;;;;N;;;;;
+A4A8;YI RADICAL TU;So;0;ON;;;;;N;;;;;
+A4A9;YI RADICAL OP;So;0;ON;;;;;N;;;;;
+A4AA;YI RADICAL JJUT;So;0;ON;;;;;N;;;;;
+A4AB;YI RADICAL ZOT;So;0;ON;;;;;N;;;;;
+A4AC;YI RADICAL PYT;So;0;ON;;;;;N;;;;;
+A4AD;YI RADICAL HMO;So;0;ON;;;;;N;;;;;
+A4AE;YI RADICAL YIT;So;0;ON;;;;;N;;;;;
+A4AF;YI RADICAL VUR;So;0;ON;;;;;N;;;;;
+A4B0;YI RADICAL SHY;So;0;ON;;;;;N;;;;;
+A4B1;YI RADICAL VEP;So;0;ON;;;;;N;;;;;
+A4B2;YI RADICAL ZA;So;0;ON;;;;;N;;;;;
+A4B3;YI RADICAL JO;So;0;ON;;;;;N;;;;;
+A4B4;YI RADICAL NZUP;So;0;ON;;;;;N;;;;;
+A4B5;YI RADICAL JJY;So;0;ON;;;;;N;;;;;
+A4B6;YI RADICAL GOT;So;0;ON;;;;;N;;;;;
+A4B7;YI RADICAL JJIE;So;0;ON;;;;;N;;;;;
+A4B8;YI RADICAL WO;So;0;ON;;;;;N;;;;;
+A4B9;YI RADICAL DU;So;0;ON;;;;;N;;;;;
+A4BA;YI RADICAL SHUR;So;0;ON;;;;;N;;;;;
+A4BB;YI RADICAL LIE;So;0;ON;;;;;N;;;;;
+A4BC;YI RADICAL CY;So;0;ON;;;;;N;;;;;
+A4BD;YI RADICAL CUOP;So;0;ON;;;;;N;;;;;
+A4BE;YI RADICAL CIP;So;0;ON;;;;;N;;;;;
+A4BF;YI RADICAL HXOP;So;0;ON;;;;;N;;;;;
+A4C0;YI RADICAL SHAT;So;0;ON;;;;;N;;;;;
+A4C1;YI RADICAL ZUR;So;0;ON;;;;;N;;;;;
+A4C2;YI RADICAL SHOP;So;0;ON;;;;;N;;;;;
+A4C3;YI RADICAL CHE;So;0;ON;;;;;N;;;;;
+A4C4;YI RADICAL ZZIET;So;0;ON;;;;;N;;;;;
+A4C5;YI RADICAL NBIE;So;0;ON;;;;;N;;;;;
+A4C6;YI RADICAL KE;So;0;ON;;;;;N;;;;;
+A4D0;LISU LETTER BA;Lo;0;L;;;;;N;;;;;
+A4D1;LISU LETTER PA;Lo;0;L;;;;;N;;;;;
+A4D2;LISU LETTER PHA;Lo;0;L;;;;;N;;;;;
+A4D3;LISU LETTER DA;Lo;0;L;;;;;N;;;;;
+A4D4;LISU LETTER TA;Lo;0;L;;;;;N;;;;;
+A4D5;LISU LETTER THA;Lo;0;L;;;;;N;;;;;
+A4D6;LISU LETTER GA;Lo;0;L;;;;;N;;;;;
+A4D7;LISU LETTER KA;Lo;0;L;;;;;N;;;;;
+A4D8;LISU LETTER KHA;Lo;0;L;;;;;N;;;;;
+A4D9;LISU LETTER JA;Lo;0;L;;;;;N;;;;;
+A4DA;LISU LETTER CA;Lo;0;L;;;;;N;;;;;
+A4DB;LISU LETTER CHA;Lo;0;L;;;;;N;;;;;
+A4DC;LISU LETTER DZA;Lo;0;L;;;;;N;;;;;
+A4DD;LISU LETTER TSA;Lo;0;L;;;;;N;;;;;
+A4DE;LISU LETTER TSHA;Lo;0;L;;;;;N;;;;;
+A4DF;LISU LETTER MA;Lo;0;L;;;;;N;;;;;
+A4E0;LISU LETTER NA;Lo;0;L;;;;;N;;;;;
+A4E1;LISU LETTER LA;Lo;0;L;;;;;N;;;;;
+A4E2;LISU LETTER SA;Lo;0;L;;;;;N;;;;;
+A4E3;LISU LETTER ZHA;Lo;0;L;;;;;N;;;;;
+A4E4;LISU LETTER ZA;Lo;0;L;;;;;N;;;;;
+A4E5;LISU LETTER NGA;Lo;0;L;;;;;N;;;;;
+A4E6;LISU LETTER HA;Lo;0;L;;;;;N;;;;;
+A4E7;LISU LETTER XA;Lo;0;L;;;;;N;;;;;
+A4E8;LISU LETTER HHA;Lo;0;L;;;;;N;;;;;
+A4E9;LISU LETTER FA;Lo;0;L;;;;;N;;;;;
+A4EA;LISU LETTER WA;Lo;0;L;;;;;N;;;;;
+A4EB;LISU LETTER SHA;Lo;0;L;;;;;N;;;;;
+A4EC;LISU LETTER YA;Lo;0;L;;;;;N;;;;;
+A4ED;LISU LETTER GHA;Lo;0;L;;;;;N;;;;;
+A4EE;LISU LETTER A;Lo;0;L;;;;;N;;;;;
+A4EF;LISU LETTER AE;Lo;0;L;;;;;N;;;;;
+A4F0;LISU LETTER E;Lo;0;L;;;;;N;;;;;
+A4F1;LISU LETTER EU;Lo;0;L;;;;;N;;;;;
+A4F2;LISU LETTER I;Lo;0;L;;;;;N;;;;;
+A4F3;LISU LETTER O;Lo;0;L;;;;;N;;;;;
+A4F4;LISU LETTER U;Lo;0;L;;;;;N;;;;;
+A4F5;LISU LETTER UE;Lo;0;L;;;;;N;;;;;
+A4F6;LISU LETTER UH;Lo;0;L;;;;;N;;;;;
+A4F7;LISU LETTER OE;Lo;0;L;;;;;N;;;;;
+A4F8;LISU LETTER TONE MYA TI;Lm;0;L;;;;;N;;;;;
+A4F9;LISU LETTER TONE NA PO;Lm;0;L;;;;;N;;;;;
+A4FA;LISU LETTER TONE MYA CYA;Lm;0;L;;;;;N;;;;;
+A4FB;LISU LETTER TONE MYA BO;Lm;0;L;;;;;N;;;;;
+A4FC;LISU LETTER TONE MYA NA;Lm;0;L;;;;;N;;;;;
+A4FD;LISU LETTER TONE MYA JEU;Lm;0;L;;;;;N;;;;;
+A4FE;LISU PUNCTUATION COMMA;Po;0;L;;;;;N;;;;;
+A4FF;LISU PUNCTUATION FULL STOP;Po;0;L;;;;;N;;;;;
+A500;VAI SYLLABLE EE;Lo;0;L;;;;;N;;;;;
+A501;VAI SYLLABLE EEN;Lo;0;L;;;;;N;;;;;
+A502;VAI SYLLABLE HEE;Lo;0;L;;;;;N;;;;;
+A503;VAI SYLLABLE WEE;Lo;0;L;;;;;N;;;;;
+A504;VAI SYLLABLE WEEN;Lo;0;L;;;;;N;;;;;
+A505;VAI SYLLABLE PEE;Lo;0;L;;;;;N;;;;;
+A506;VAI SYLLABLE BHEE;Lo;0;L;;;;;N;;;;;
+A507;VAI SYLLABLE BEE;Lo;0;L;;;;;N;;;;;
+A508;VAI SYLLABLE MBEE;Lo;0;L;;;;;N;;;;;
+A509;VAI SYLLABLE KPEE;Lo;0;L;;;;;N;;;;;
+A50A;VAI SYLLABLE MGBEE;Lo;0;L;;;;;N;;;;;
+A50B;VAI SYLLABLE GBEE;Lo;0;L;;;;;N;;;;;
+A50C;VAI SYLLABLE FEE;Lo;0;L;;;;;N;;;;;
+A50D;VAI SYLLABLE VEE;Lo;0;L;;;;;N;;;;;
+A50E;VAI SYLLABLE TEE;Lo;0;L;;;;;N;;;;;
+A50F;VAI SYLLABLE THEE;Lo;0;L;;;;;N;;;;;
+A510;VAI SYLLABLE DHEE;Lo;0;L;;;;;N;;;;;
+A511;VAI SYLLABLE DHHEE;Lo;0;L;;;;;N;;;;;
+A512;VAI SYLLABLE LEE;Lo;0;L;;;;;N;;;;;
+A513;VAI SYLLABLE REE;Lo;0;L;;;;;N;;;;;
+A514;VAI SYLLABLE DEE;Lo;0;L;;;;;N;;;;;
+A515;VAI SYLLABLE NDEE;Lo;0;L;;;;;N;;;;;
+A516;VAI SYLLABLE SEE;Lo;0;L;;;;;N;;;;;
+A517;VAI SYLLABLE SHEE;Lo;0;L;;;;;N;;;;;
+A518;VAI SYLLABLE ZEE;Lo;0;L;;;;;N;;;;;
+A519;VAI SYLLABLE ZHEE;Lo;0;L;;;;;N;;;;;
+A51A;VAI SYLLABLE CEE;Lo;0;L;;;;;N;;;;;
+A51B;VAI SYLLABLE JEE;Lo;0;L;;;;;N;;;;;
+A51C;VAI SYLLABLE NJEE;Lo;0;L;;;;;N;;;;;
+A51D;VAI SYLLABLE YEE;Lo;0;L;;;;;N;;;;;
+A51E;VAI SYLLABLE KEE;Lo;0;L;;;;;N;;;;;
+A51F;VAI SYLLABLE NGGEE;Lo;0;L;;;;;N;;;;;
+A520;VAI SYLLABLE GEE;Lo;0;L;;;;;N;;;;;
+A521;VAI SYLLABLE MEE;Lo;0;L;;;;;N;;;;;
+A522;VAI SYLLABLE NEE;Lo;0;L;;;;;N;;;;;
+A523;VAI SYLLABLE NYEE;Lo;0;L;;;;;N;;;;;
+A524;VAI SYLLABLE I;Lo;0;L;;;;;N;;;;;
+A525;VAI SYLLABLE IN;Lo;0;L;;;;;N;;;;;
+A526;VAI SYLLABLE HI;Lo;0;L;;;;;N;;;;;
+A527;VAI SYLLABLE HIN;Lo;0;L;;;;;N;;;;;
+A528;VAI SYLLABLE WI;Lo;0;L;;;;;N;;;;;
+A529;VAI SYLLABLE WIN;Lo;0;L;;;;;N;;;;;
+A52A;VAI SYLLABLE PI;Lo;0;L;;;;;N;;;;;
+A52B;VAI SYLLABLE BHI;Lo;0;L;;;;;N;;;;;
+A52C;VAI SYLLABLE BI;Lo;0;L;;;;;N;;;;;
+A52D;VAI SYLLABLE MBI;Lo;0;L;;;;;N;;;;;
+A52E;VAI SYLLABLE KPI;Lo;0;L;;;;;N;;;;;
+A52F;VAI SYLLABLE MGBI;Lo;0;L;;;;;N;;;;;
+A530;VAI SYLLABLE GBI;Lo;0;L;;;;;N;;;;;
+A531;VAI SYLLABLE FI;Lo;0;L;;;;;N;;;;;
+A532;VAI SYLLABLE VI;Lo;0;L;;;;;N;;;;;
+A533;VAI SYLLABLE TI;Lo;0;L;;;;;N;;;;;
+A534;VAI SYLLABLE THI;Lo;0;L;;;;;N;;;;;
+A535;VAI SYLLABLE DHI;Lo;0;L;;;;;N;;;;;
+A536;VAI SYLLABLE DHHI;Lo;0;L;;;;;N;;;;;
+A537;VAI SYLLABLE LI;Lo;0;L;;;;;N;;;;;
+A538;VAI SYLLABLE RI;Lo;0;L;;;;;N;;;;;
+A539;VAI SYLLABLE DI;Lo;0;L;;;;;N;;;;;
+A53A;VAI SYLLABLE NDI;Lo;0;L;;;;;N;;;;;
+A53B;VAI SYLLABLE SI;Lo;0;L;;;;;N;;;;;
+A53C;VAI SYLLABLE SHI;Lo;0;L;;;;;N;;;;;
+A53D;VAI SYLLABLE ZI;Lo;0;L;;;;;N;;;;;
+A53E;VAI SYLLABLE ZHI;Lo;0;L;;;;;N;;;;;
+A53F;VAI SYLLABLE CI;Lo;0;L;;;;;N;;;;;
+A540;VAI SYLLABLE JI;Lo;0;L;;;;;N;;;;;
+A541;VAI SYLLABLE NJI;Lo;0;L;;;;;N;;;;;
+A542;VAI SYLLABLE YI;Lo;0;L;;;;;N;;;;;
+A543;VAI SYLLABLE KI;Lo;0;L;;;;;N;;;;;
+A544;VAI SYLLABLE NGGI;Lo;0;L;;;;;N;;;;;
+A545;VAI SYLLABLE GI;Lo;0;L;;;;;N;;;;;
+A546;VAI SYLLABLE MI;Lo;0;L;;;;;N;;;;;
+A547;VAI SYLLABLE NI;Lo;0;L;;;;;N;;;;;
+A548;VAI SYLLABLE NYI;Lo;0;L;;;;;N;;;;;
+A549;VAI SYLLABLE A;Lo;0;L;;;;;N;;;;;
+A54A;VAI SYLLABLE AN;Lo;0;L;;;;;N;;;;;
+A54B;VAI SYLLABLE NGAN;Lo;0;L;;;;;N;;;;;
+A54C;VAI SYLLABLE HA;Lo;0;L;;;;;N;;;;;
+A54D;VAI SYLLABLE HAN;Lo;0;L;;;;;N;;;;;
+A54E;VAI SYLLABLE WA;Lo;0;L;;;;;N;;;;;
+A54F;VAI SYLLABLE WAN;Lo;0;L;;;;;N;;;;;
+A550;VAI SYLLABLE PA;Lo;0;L;;;;;N;;;;;
+A551;VAI SYLLABLE BHA;Lo;0;L;;;;;N;;;;;
+A552;VAI SYLLABLE BA;Lo;0;L;;;;;N;;;;;
+A553;VAI SYLLABLE MBA;Lo;0;L;;;;;N;;;;;
+A554;VAI SYLLABLE KPA;Lo;0;L;;;;;N;;;;;
+A555;VAI SYLLABLE KPAN;Lo;0;L;;;;;N;;;;;
+A556;VAI SYLLABLE MGBA;Lo;0;L;;;;;N;;;;;
+A557;VAI SYLLABLE GBA;Lo;0;L;;;;;N;;;;;
+A558;VAI SYLLABLE FA;Lo;0;L;;;;;N;;;;;
+A559;VAI SYLLABLE VA;Lo;0;L;;;;;N;;;;;
+A55A;VAI SYLLABLE TA;Lo;0;L;;;;;N;;;;;
+A55B;VAI SYLLABLE THA;Lo;0;L;;;;;N;;;;;
+A55C;VAI SYLLABLE DHA;Lo;0;L;;;;;N;;;;;
+A55D;VAI SYLLABLE DHHA;Lo;0;L;;;;;N;;;;;
+A55E;VAI SYLLABLE LA;Lo;0;L;;;;;N;;;;;
+A55F;VAI SYLLABLE RA;Lo;0;L;;;;;N;;;;;
+A560;VAI SYLLABLE DA;Lo;0;L;;;;;N;;;;;
+A561;VAI SYLLABLE NDA;Lo;0;L;;;;;N;;;;;
+A562;VAI SYLLABLE SA;Lo;0;L;;;;;N;;;;;
+A563;VAI SYLLABLE SHA;Lo;0;L;;;;;N;;;;;
+A564;VAI SYLLABLE ZA;Lo;0;L;;;;;N;;;;;
+A565;VAI SYLLABLE ZHA;Lo;0;L;;;;;N;;;;;
+A566;VAI SYLLABLE CA;Lo;0;L;;;;;N;;;;;
+A567;VAI SYLLABLE JA;Lo;0;L;;;;;N;;;;;
+A568;VAI SYLLABLE NJA;Lo;0;L;;;;;N;;;;;
+A569;VAI SYLLABLE YA;Lo;0;L;;;;;N;;;;;
+A56A;VAI SYLLABLE KA;Lo;0;L;;;;;N;;;;;
+A56B;VAI SYLLABLE KAN;Lo;0;L;;;;;N;;;;;
+A56C;VAI SYLLABLE NGGA;Lo;0;L;;;;;N;;;;;
+A56D;VAI SYLLABLE GA;Lo;0;L;;;;;N;;;;;
+A56E;VAI SYLLABLE MA;Lo;0;L;;;;;N;;;;;
+A56F;VAI SYLLABLE NA;Lo;0;L;;;;;N;;;;;
+A570;VAI SYLLABLE NYA;Lo;0;L;;;;;N;;;;;
+A571;VAI SYLLABLE OO;Lo;0;L;;;;;N;;;;;
+A572;VAI SYLLABLE OON;Lo;0;L;;;;;N;;;;;
+A573;VAI SYLLABLE HOO;Lo;0;L;;;;;N;;;;;
+A574;VAI SYLLABLE WOO;Lo;0;L;;;;;N;;;;;
+A575;VAI SYLLABLE WOON;Lo;0;L;;;;;N;;;;;
+A576;VAI SYLLABLE POO;Lo;0;L;;;;;N;;;;;
+A577;VAI SYLLABLE BHOO;Lo;0;L;;;;;N;;;;;
+A578;VAI SYLLABLE BOO;Lo;0;L;;;;;N;;;;;
+A579;VAI SYLLABLE MBOO;Lo;0;L;;;;;N;;;;;
+A57A;VAI SYLLABLE KPOO;Lo;0;L;;;;;N;;;;;
+A57B;VAI SYLLABLE MGBOO;Lo;0;L;;;;;N;;;;;
+A57C;VAI SYLLABLE GBOO;Lo;0;L;;;;;N;;;;;
+A57D;VAI SYLLABLE FOO;Lo;0;L;;;;;N;;;;;
+A57E;VAI SYLLABLE VOO;Lo;0;L;;;;;N;;;;;
+A57F;VAI SYLLABLE TOO;Lo;0;L;;;;;N;;;;;
+A580;VAI SYLLABLE THOO;Lo;0;L;;;;;N;;;;;
+A581;VAI SYLLABLE DHOO;Lo;0;L;;;;;N;;;;;
+A582;VAI SYLLABLE DHHOO;Lo;0;L;;;;;N;;;;;
+A583;VAI SYLLABLE LOO;Lo;0;L;;;;;N;;;;;
+A584;VAI SYLLABLE ROO;Lo;0;L;;;;;N;;;;;
+A585;VAI SYLLABLE DOO;Lo;0;L;;;;;N;;;;;
+A586;VAI SYLLABLE NDOO;Lo;0;L;;;;;N;;;;;
+A587;VAI SYLLABLE SOO;Lo;0;L;;;;;N;;;;;
+A588;VAI SYLLABLE SHOO;Lo;0;L;;;;;N;;;;;
+A589;VAI SYLLABLE ZOO;Lo;0;L;;;;;N;;;;;
+A58A;VAI SYLLABLE ZHOO;Lo;0;L;;;;;N;;;;;
+A58B;VAI SYLLABLE COO;Lo;0;L;;;;;N;;;;;
+A58C;VAI SYLLABLE JOO;Lo;0;L;;;;;N;;;;;
+A58D;VAI SYLLABLE NJOO;Lo;0;L;;;;;N;;;;;
+A58E;VAI SYLLABLE YOO;Lo;0;L;;;;;N;;;;;
+A58F;VAI SYLLABLE KOO;Lo;0;L;;;;;N;;;;;
+A590;VAI SYLLABLE NGGOO;Lo;0;L;;;;;N;;;;;
+A591;VAI SYLLABLE GOO;Lo;0;L;;;;;N;;;;;
+A592;VAI SYLLABLE MOO;Lo;0;L;;;;;N;;;;;
+A593;VAI SYLLABLE NOO;Lo;0;L;;;;;N;;;;;
+A594;VAI SYLLABLE NYOO;Lo;0;L;;;;;N;;;;;
+A595;VAI SYLLABLE U;Lo;0;L;;;;;N;;;;;
+A596;VAI SYLLABLE UN;Lo;0;L;;;;;N;;;;;
+A597;VAI SYLLABLE HU;Lo;0;L;;;;;N;;;;;
+A598;VAI SYLLABLE HUN;Lo;0;L;;;;;N;;;;;
+A599;VAI SYLLABLE WU;Lo;0;L;;;;;N;;;;;
+A59A;VAI SYLLABLE WUN;Lo;0;L;;;;;N;;;;;
+A59B;VAI SYLLABLE PU;Lo;0;L;;;;;N;;;;;
+A59C;VAI SYLLABLE BHU;Lo;0;L;;;;;N;;;;;
+A59D;VAI SYLLABLE BU;Lo;0;L;;;;;N;;;;;
+A59E;VAI SYLLABLE MBU;Lo;0;L;;;;;N;;;;;
+A59F;VAI SYLLABLE KPU;Lo;0;L;;;;;N;;;;;
+A5A0;VAI SYLLABLE MGBU;Lo;0;L;;;;;N;;;;;
+A5A1;VAI SYLLABLE GBU;Lo;0;L;;;;;N;;;;;
+A5A2;VAI SYLLABLE FU;Lo;0;L;;;;;N;;;;;
+A5A3;VAI SYLLABLE VU;Lo;0;L;;;;;N;;;;;
+A5A4;VAI SYLLABLE TU;Lo;0;L;;;;;N;;;;;
+A5A5;VAI SYLLABLE THU;Lo;0;L;;;;;N;;;;;
+A5A6;VAI SYLLABLE DHU;Lo;0;L;;;;;N;;;;;
+A5A7;VAI SYLLABLE DHHU;Lo;0;L;;;;;N;;;;;
+A5A8;VAI SYLLABLE LU;Lo;0;L;;;;;N;;;;;
+A5A9;VAI SYLLABLE RU;Lo;0;L;;;;;N;;;;;
+A5AA;VAI SYLLABLE DU;Lo;0;L;;;;;N;;;;;
+A5AB;VAI SYLLABLE NDU;Lo;0;L;;;;;N;;;;;
+A5AC;VAI SYLLABLE SU;Lo;0;L;;;;;N;;;;;
+A5AD;VAI SYLLABLE SHU;Lo;0;L;;;;;N;;;;;
+A5AE;VAI SYLLABLE ZU;Lo;0;L;;;;;N;;;;;
+A5AF;VAI SYLLABLE ZHU;Lo;0;L;;;;;N;;;;;
+A5B0;VAI SYLLABLE CU;Lo;0;L;;;;;N;;;;;
+A5B1;VAI SYLLABLE JU;Lo;0;L;;;;;N;;;;;
+A5B2;VAI SYLLABLE NJU;Lo;0;L;;;;;N;;;;;
+A5B3;VAI SYLLABLE YU;Lo;0;L;;;;;N;;;;;
+A5B4;VAI SYLLABLE KU;Lo;0;L;;;;;N;;;;;
+A5B5;VAI SYLLABLE NGGU;Lo;0;L;;;;;N;;;;;
+A5B6;VAI SYLLABLE GU;Lo;0;L;;;;;N;;;;;
+A5B7;VAI SYLLABLE MU;Lo;0;L;;;;;N;;;;;
+A5B8;VAI SYLLABLE NU;Lo;0;L;;;;;N;;;;;
+A5B9;VAI SYLLABLE NYU;Lo;0;L;;;;;N;;;;;
+A5BA;VAI SYLLABLE O;Lo;0;L;;;;;N;;;;;
+A5BB;VAI SYLLABLE ON;Lo;0;L;;;;;N;;;;;
+A5BC;VAI SYLLABLE NGON;Lo;0;L;;;;;N;;;;;
+A5BD;VAI SYLLABLE HO;Lo;0;L;;;;;N;;;;;
+A5BE;VAI SYLLABLE HON;Lo;0;L;;;;;N;;;;;
+A5BF;VAI SYLLABLE WO;Lo;0;L;;;;;N;;;;;
+A5C0;VAI SYLLABLE WON;Lo;0;L;;;;;N;;;;;
+A5C1;VAI SYLLABLE PO;Lo;0;L;;;;;N;;;;;
+A5C2;VAI SYLLABLE BHO;Lo;0;L;;;;;N;;;;;
+A5C3;VAI SYLLABLE BO;Lo;0;L;;;;;N;;;;;
+A5C4;VAI SYLLABLE MBO;Lo;0;L;;;;;N;;;;;
+A5C5;VAI SYLLABLE KPO;Lo;0;L;;;;;N;;;;;
+A5C6;VAI SYLLABLE MGBO;Lo;0;L;;;;;N;;;;;
+A5C7;VAI SYLLABLE GBO;Lo;0;L;;;;;N;;;;;
+A5C8;VAI SYLLABLE GBON;Lo;0;L;;;;;N;;;;;
+A5C9;VAI SYLLABLE FO;Lo;0;L;;;;;N;;;;;
+A5CA;VAI SYLLABLE VO;Lo;0;L;;;;;N;;;;;
+A5CB;VAI SYLLABLE TO;Lo;0;L;;;;;N;;;;;
+A5CC;VAI SYLLABLE THO;Lo;0;L;;;;;N;;;;;
+A5CD;VAI SYLLABLE DHO;Lo;0;L;;;;;N;;;;;
+A5CE;VAI SYLLABLE DHHO;Lo;0;L;;;;;N;;;;;
+A5CF;VAI SYLLABLE LO;Lo;0;L;;;;;N;;;;;
+A5D0;VAI SYLLABLE RO;Lo;0;L;;;;;N;;;;;
+A5D1;VAI SYLLABLE DO;Lo;0;L;;;;;N;;;;;
+A5D2;VAI SYLLABLE NDO;Lo;0;L;;;;;N;;;;;
+A5D3;VAI SYLLABLE SO;Lo;0;L;;;;;N;;;;;
+A5D4;VAI SYLLABLE SHO;Lo;0;L;;;;;N;;;;;
+A5D5;VAI SYLLABLE ZO;Lo;0;L;;;;;N;;;;;
+A5D6;VAI SYLLABLE ZHO;Lo;0;L;;;;;N;;;;;
+A5D7;VAI SYLLABLE CO;Lo;0;L;;;;;N;;;;;
+A5D8;VAI SYLLABLE JO;Lo;0;L;;;;;N;;;;;
+A5D9;VAI SYLLABLE NJO;Lo;0;L;;;;;N;;;;;
+A5DA;VAI SYLLABLE YO;Lo;0;L;;;;;N;;;;;
+A5DB;VAI SYLLABLE KO;Lo;0;L;;;;;N;;;;;
+A5DC;VAI SYLLABLE NGGO;Lo;0;L;;;;;N;;;;;
+A5DD;VAI SYLLABLE GO;Lo;0;L;;;;;N;;;;;
+A5DE;VAI SYLLABLE MO;Lo;0;L;;;;;N;;;;;
+A5DF;VAI SYLLABLE NO;Lo;0;L;;;;;N;;;;;
+A5E0;VAI SYLLABLE NYO;Lo;0;L;;;;;N;;;;;
+A5E1;VAI SYLLABLE E;Lo;0;L;;;;;N;;;;;
+A5E2;VAI SYLLABLE EN;Lo;0;L;;;;;N;;;;;
+A5E3;VAI SYLLABLE NGEN;Lo;0;L;;;;;N;;;;;
+A5E4;VAI SYLLABLE HE;Lo;0;L;;;;;N;;;;;
+A5E5;VAI SYLLABLE HEN;Lo;0;L;;;;;N;;;;;
+A5E6;VAI SYLLABLE WE;Lo;0;L;;;;;N;;;;;
+A5E7;VAI SYLLABLE WEN;Lo;0;L;;;;;N;;;;;
+A5E8;VAI SYLLABLE PE;Lo;0;L;;;;;N;;;;;
+A5E9;VAI SYLLABLE BHE;Lo;0;L;;;;;N;;;;;
+A5EA;VAI SYLLABLE BE;Lo;0;L;;;;;N;;;;;
+A5EB;VAI SYLLABLE MBE;Lo;0;L;;;;;N;;;;;
+A5EC;VAI SYLLABLE KPE;Lo;0;L;;;;;N;;;;;
+A5ED;VAI SYLLABLE KPEN;Lo;0;L;;;;;N;;;;;
+A5EE;VAI SYLLABLE MGBE;Lo;0;L;;;;;N;;;;;
+A5EF;VAI SYLLABLE GBE;Lo;0;L;;;;;N;;;;;
+A5F0;VAI SYLLABLE GBEN;Lo;0;L;;;;;N;;;;;
+A5F1;VAI SYLLABLE FE;Lo;0;L;;;;;N;;;;;
+A5F2;VAI SYLLABLE VE;Lo;0;L;;;;;N;;;;;
+A5F3;VAI SYLLABLE TE;Lo;0;L;;;;;N;;;;;
+A5F4;VAI SYLLABLE THE;Lo;0;L;;;;;N;;;;;
+A5F5;VAI SYLLABLE DHE;Lo;0;L;;;;;N;;;;;
+A5F6;VAI SYLLABLE DHHE;Lo;0;L;;;;;N;;;;;
+A5F7;VAI SYLLABLE LE;Lo;0;L;;;;;N;;;;;
+A5F8;VAI SYLLABLE RE;Lo;0;L;;;;;N;;;;;
+A5F9;VAI SYLLABLE DE;Lo;0;L;;;;;N;;;;;
+A5FA;VAI SYLLABLE NDE;Lo;0;L;;;;;N;;;;;
+A5FB;VAI SYLLABLE SE;Lo;0;L;;;;;N;;;;;
+A5FC;VAI SYLLABLE SHE;Lo;0;L;;;;;N;;;;;
+A5FD;VAI SYLLABLE ZE;Lo;0;L;;;;;N;;;;;
+A5FE;VAI SYLLABLE ZHE;Lo;0;L;;;;;N;;;;;
+A5FF;VAI SYLLABLE CE;Lo;0;L;;;;;N;;;;;
+A600;VAI SYLLABLE JE;Lo;0;L;;;;;N;;;;;
+A601;VAI SYLLABLE NJE;Lo;0;L;;;;;N;;;;;
+A602;VAI SYLLABLE YE;Lo;0;L;;;;;N;;;;;
+A603;VAI SYLLABLE KE;Lo;0;L;;;;;N;;;;;
+A604;VAI SYLLABLE NGGE;Lo;0;L;;;;;N;;;;;
+A605;VAI SYLLABLE NGGEN;Lo;0;L;;;;;N;;;;;
+A606;VAI SYLLABLE GE;Lo;0;L;;;;;N;;;;;
+A607;VAI SYLLABLE GEN;Lo;0;L;;;;;N;;;;;
+A608;VAI SYLLABLE ME;Lo;0;L;;;;;N;;;;;
+A609;VAI SYLLABLE NE;Lo;0;L;;;;;N;;;;;
+A60A;VAI SYLLABLE NYE;Lo;0;L;;;;;N;;;;;
+A60B;VAI SYLLABLE NG;Lo;0;L;;;;;N;;;;;
+A60C;VAI SYLLABLE LENGTHENER;Lm;0;L;;;;;N;;;;;
+A60D;VAI COMMA;Po;0;ON;;;;;N;;;;;
+A60E;VAI FULL STOP;Po;0;ON;;;;;N;;;;;
+A60F;VAI QUESTION MARK;Po;0;ON;;;;;N;;;;;
+A610;VAI SYLLABLE NDOLE FA;Lo;0;L;;;;;N;;;;;
+A611;VAI SYLLABLE NDOLE KA;Lo;0;L;;;;;N;;;;;
+A612;VAI SYLLABLE NDOLE SOO;Lo;0;L;;;;;N;;;;;
+A613;VAI SYMBOL FEENG;Lo;0;L;;;;;N;;;;;
+A614;VAI SYMBOL KEENG;Lo;0;L;;;;;N;;;;;
+A615;VAI SYMBOL TING;Lo;0;L;;;;;N;;;;;
+A616;VAI SYMBOL NII;Lo;0;L;;;;;N;;;;;
+A617;VAI SYMBOL BANG;Lo;0;L;;;;;N;;;;;
+A618;VAI SYMBOL FAA;Lo;0;L;;;;;N;;;;;
+A619;VAI SYMBOL TAA;Lo;0;L;;;;;N;;;;;
+A61A;VAI SYMBOL DANG;Lo;0;L;;;;;N;;;;;
+A61B;VAI SYMBOL DOONG;Lo;0;L;;;;;N;;;;;
+A61C;VAI SYMBOL KUNG;Lo;0;L;;;;;N;;;;;
+A61D;VAI SYMBOL TONG;Lo;0;L;;;;;N;;;;;
+A61E;VAI SYMBOL DO-O;Lo;0;L;;;;;N;;;;;
+A61F;VAI SYMBOL JONG;Lo;0;L;;;;;N;;;;;
+A620;VAI DIGIT ZERO;Nd;0;L;;0;0;0;N;;;;;
+A621;VAI DIGIT ONE;Nd;0;L;;1;1;1;N;;;;;
+A622;VAI DIGIT TWO;Nd;0;L;;2;2;2;N;;;;;
+A623;VAI DIGIT THREE;Nd;0;L;;3;3;3;N;;;;;
+A624;VAI DIGIT FOUR;Nd;0;L;;4;4;4;N;;;;;
+A625;VAI DIGIT FIVE;Nd;0;L;;5;5;5;N;;;;;
+A626;VAI DIGIT SIX;Nd;0;L;;6;6;6;N;;;;;
+A627;VAI DIGIT SEVEN;Nd;0;L;;7;7;7;N;;;;;
+A628;VAI DIGIT EIGHT;Nd;0;L;;8;8;8;N;;;;;
+A629;VAI DIGIT NINE;Nd;0;L;;9;9;9;N;;;;;
+A62A;VAI SYLLABLE NDOLE MA;Lo;0;L;;;;;N;;;;;
+A62B;VAI SYLLABLE NDOLE DO;Lo;0;L;;;;;N;;;;;
+A640;CYRILLIC CAPITAL LETTER ZEMLYA;Lu;0;L;;;;;N;;;;A641;
+A641;CYRILLIC SMALL LETTER ZEMLYA;Ll;0;L;;;;;N;;;A640;;A640
+A642;CYRILLIC CAPITAL LETTER DZELO;Lu;0;L;;;;;N;;;;A643;
+A643;CYRILLIC SMALL LETTER DZELO;Ll;0;L;;;;;N;;;A642;;A642
+A644;CYRILLIC CAPITAL LETTER REVERSED DZE;Lu;0;L;;;;;N;;;;A645;
+A645;CYRILLIC SMALL LETTER REVERSED DZE;Ll;0;L;;;;;N;;;A644;;A644
+A646;CYRILLIC CAPITAL LETTER IOTA;Lu;0;L;;;;;N;;;;A647;
+A647;CYRILLIC SMALL LETTER IOTA;Ll;0;L;;;;;N;;;A646;;A646
+A648;CYRILLIC CAPITAL LETTER DJERV;Lu;0;L;;;;;N;;;;A649;
+A649;CYRILLIC SMALL LETTER DJERV;Ll;0;L;;;;;N;;;A648;;A648
+A64A;CYRILLIC CAPITAL LETTER MONOGRAPH UK;Lu;0;L;;;;;N;;;;A64B;
+A64B;CYRILLIC SMALL LETTER MONOGRAPH UK;Ll;0;L;;;;;N;;;A64A;;A64A
+A64C;CYRILLIC CAPITAL LETTER BROAD OMEGA;Lu;0;L;;;;;N;;;;A64D;
+A64D;CYRILLIC SMALL LETTER BROAD OMEGA;Ll;0;L;;;;;N;;;A64C;;A64C
+A64E;CYRILLIC CAPITAL LETTER NEUTRAL YER;Lu;0;L;;;;;N;;;;A64F;
+A64F;CYRILLIC SMALL LETTER NEUTRAL YER;Ll;0;L;;;;;N;;;A64E;;A64E
+A650;CYRILLIC CAPITAL LETTER YERU WITH BACK YER;Lu;0;L;;;;;N;;;;A651;
+A651;CYRILLIC SMALL LETTER YERU WITH BACK YER;Ll;0;L;;;;;N;;;A650;;A650
+A652;CYRILLIC CAPITAL LETTER IOTIFIED YAT;Lu;0;L;;;;;N;;;;A653;
+A653;CYRILLIC SMALL LETTER IOTIFIED YAT;Ll;0;L;;;;;N;;;A652;;A652
+A654;CYRILLIC CAPITAL LETTER REVERSED YU;Lu;0;L;;;;;N;;;;A655;
+A655;CYRILLIC SMALL LETTER REVERSED YU;Ll;0;L;;;;;N;;;A654;;A654
+A656;CYRILLIC CAPITAL LETTER IOTIFIED A;Lu;0;L;;;;;N;;;;A657;
+A657;CYRILLIC SMALL LETTER IOTIFIED A;Ll;0;L;;;;;N;;;A656;;A656
+A658;CYRILLIC CAPITAL LETTER CLOSED LITTLE YUS;Lu;0;L;;;;;N;;;;A659;
+A659;CYRILLIC SMALL LETTER CLOSED LITTLE YUS;Ll;0;L;;;;;N;;;A658;;A658
+A65A;CYRILLIC CAPITAL LETTER BLENDED YUS;Lu;0;L;;;;;N;;;;A65B;
+A65B;CYRILLIC SMALL LETTER BLENDED YUS;Ll;0;L;;;;;N;;;A65A;;A65A
+A65C;CYRILLIC CAPITAL LETTER IOTIFIED CLOSED LITTLE YUS;Lu;0;L;;;;;N;;;;A65D;
+A65D;CYRILLIC SMALL LETTER IOTIFIED CLOSED LITTLE YUS;Ll;0;L;;;;;N;;;A65C;;A65C
+A65E;CYRILLIC CAPITAL LETTER YN;Lu;0;L;;;;;N;;;;A65F;
+A65F;CYRILLIC SMALL LETTER YN;Ll;0;L;;;;;N;;;A65E;;A65E
+A660;CYRILLIC CAPITAL LETTER REVERSED TSE;Lu;0;L;;;;;N;;;;A661;
+A661;CYRILLIC SMALL LETTER REVERSED TSE;Ll;0;L;;;;;N;;;A660;;A660
+A662;CYRILLIC CAPITAL LETTER SOFT DE;Lu;0;L;;;;;N;;;;A663;
+A663;CYRILLIC SMALL LETTER SOFT DE;Ll;0;L;;;;;N;;;A662;;A662
+A664;CYRILLIC CAPITAL LETTER SOFT EL;Lu;0;L;;;;;N;;;;A665;
+A665;CYRILLIC SMALL LETTER SOFT EL;Ll;0;L;;;;;N;;;A664;;A664
+A666;CYRILLIC CAPITAL LETTER SOFT EM;Lu;0;L;;;;;N;;;;A667;
+A667;CYRILLIC SMALL LETTER SOFT EM;Ll;0;L;;;;;N;;;A666;;A666
+A668;CYRILLIC CAPITAL LETTER MONOCULAR O;Lu;0;L;;;;;N;;;;A669;
+A669;CYRILLIC SMALL LETTER MONOCULAR O;Ll;0;L;;;;;N;;;A668;;A668
+A66A;CYRILLIC CAPITAL LETTER BINOCULAR O;Lu;0;L;;;;;N;;;;A66B;
+A66B;CYRILLIC SMALL LETTER BINOCULAR O;Ll;0;L;;;;;N;;;A66A;;A66A
+A66C;CYRILLIC CAPITAL LETTER DOUBLE MONOCULAR O;Lu;0;L;;;;;N;;;;A66D;
+A66D;CYRILLIC SMALL LETTER DOUBLE MONOCULAR O;Ll;0;L;;;;;N;;;A66C;;A66C
+A66E;CYRILLIC LETTER MULTIOCULAR O;Lo;0;L;;;;;N;;;;;
+A66F;COMBINING CYRILLIC VZMET;Mn;230;NSM;;;;;N;;;;;
+A670;COMBINING CYRILLIC TEN MILLIONS SIGN;Me;0;NSM;;;;;N;;;;;
+A671;COMBINING CYRILLIC HUNDRED MILLIONS SIGN;Me;0;NSM;;;;;N;;;;;
+A672;COMBINING CYRILLIC THOUSAND MILLIONS SIGN;Me;0;NSM;;;;;N;;;;;
+A673;SLAVONIC ASTERISK;Po;0;ON;;;;;N;;;;;
+A674;COMBINING CYRILLIC LETTER UKRAINIAN IE;Mn;230;NSM;;;;;N;;;;;
+A675;COMBINING CYRILLIC LETTER I;Mn;230;NSM;;;;;N;;;;;
+A676;COMBINING CYRILLIC LETTER YI;Mn;230;NSM;;;;;N;;;;;
+A677;COMBINING CYRILLIC LETTER U;Mn;230;NSM;;;;;N;;;;;
+A678;COMBINING CYRILLIC LETTER HARD SIGN;Mn;230;NSM;;;;;N;;;;;
+A679;COMBINING CYRILLIC LETTER YERU;Mn;230;NSM;;;;;N;;;;;
+A67A;COMBINING CYRILLIC LETTER SOFT SIGN;Mn;230;NSM;;;;;N;;;;;
+A67B;COMBINING CYRILLIC LETTER OMEGA;Mn;230;NSM;;;;;N;;;;;
+A67C;COMBINING CYRILLIC KAVYKA;Mn;230;NSM;;;;;N;;;;;
+A67D;COMBINING CYRILLIC PAYEROK;Mn;230;NSM;;;;;N;;;;;
+A67E;CYRILLIC KAVYKA;Po;0;ON;;;;;N;;;;;
+A67F;CYRILLIC PAYEROK;Lm;0;ON;;;;;N;;;;;
+A680;CYRILLIC CAPITAL LETTER DWE;Lu;0;L;;;;;N;;;;A681;
+A681;CYRILLIC SMALL LETTER DWE;Ll;0;L;;;;;N;;;A680;;A680
+A682;CYRILLIC CAPITAL LETTER DZWE;Lu;0;L;;;;;N;;;;A683;
+A683;CYRILLIC SMALL LETTER DZWE;Ll;0;L;;;;;N;;;A682;;A682
+A684;CYRILLIC CAPITAL LETTER ZHWE;Lu;0;L;;;;;N;;;;A685;
+A685;CYRILLIC SMALL LETTER ZHWE;Ll;0;L;;;;;N;;;A684;;A684
+A686;CYRILLIC CAPITAL LETTER CCHE;Lu;0;L;;;;;N;;;;A687;
+A687;CYRILLIC SMALL LETTER CCHE;Ll;0;L;;;;;N;;;A686;;A686
+A688;CYRILLIC CAPITAL LETTER DZZE;Lu;0;L;;;;;N;;;;A689;
+A689;CYRILLIC SMALL LETTER DZZE;Ll;0;L;;;;;N;;;A688;;A688
+A68A;CYRILLIC CAPITAL LETTER TE WITH MIDDLE HOOK;Lu;0;L;;;;;N;;;;A68B;
+A68B;CYRILLIC SMALL LETTER TE WITH MIDDLE HOOK;Ll;0;L;;;;;N;;;A68A;;A68A
+A68C;CYRILLIC CAPITAL LETTER TWE;Lu;0;L;;;;;N;;;;A68D;
+A68D;CYRILLIC SMALL LETTER TWE;Ll;0;L;;;;;N;;;A68C;;A68C
+A68E;CYRILLIC CAPITAL LETTER TSWE;Lu;0;L;;;;;N;;;;A68F;
+A68F;CYRILLIC SMALL LETTER TSWE;Ll;0;L;;;;;N;;;A68E;;A68E
+A690;CYRILLIC CAPITAL LETTER TSSE;Lu;0;L;;;;;N;;;;A691;
+A691;CYRILLIC SMALL LETTER TSSE;Ll;0;L;;;;;N;;;A690;;A690
+A692;CYRILLIC CAPITAL LETTER TCHE;Lu;0;L;;;;;N;;;;A693;
+A693;CYRILLIC SMALL LETTER TCHE;Ll;0;L;;;;;N;;;A692;;A692
+A694;CYRILLIC CAPITAL LETTER HWE;Lu;0;L;;;;;N;;;;A695;
+A695;CYRILLIC SMALL LETTER HWE;Ll;0;L;;;;;N;;;A694;;A694
+A696;CYRILLIC CAPITAL LETTER SHWE;Lu;0;L;;;;;N;;;;A697;
+A697;CYRILLIC SMALL LETTER SHWE;Ll;0;L;;;;;N;;;A696;;A696
+A698;CYRILLIC CAPITAL LETTER DOUBLE O;Lu;0;L;;;;;N;;;;A699;
+A699;CYRILLIC SMALL LETTER DOUBLE O;Ll;0;L;;;;;N;;;A698;;A698
+A69A;CYRILLIC CAPITAL LETTER CROSSED O;Lu;0;L;;;;;N;;;;A69B;
+A69B;CYRILLIC SMALL LETTER CROSSED O;Ll;0;L;;;;;N;;;A69A;;A69A
+A69C;MODIFIER LETTER CYRILLIC HARD SIGN;Lm;0;L;<super> 044A;;;;N;;;;;
+A69D;MODIFIER LETTER CYRILLIC SOFT SIGN;Lm;0;L;<super> 044C;;;;N;;;;;
+A69E;COMBINING CYRILLIC LETTER EF;Mn;230;NSM;;;;;N;;;;;
+A69F;COMBINING CYRILLIC LETTER IOTIFIED E;Mn;230;NSM;;;;;N;;;;;
+A6A0;BAMUM LETTER A;Lo;0;L;;;;;N;;;;;
+A6A1;BAMUM LETTER KA;Lo;0;L;;;;;N;;;;;
+A6A2;BAMUM LETTER U;Lo;0;L;;;;;N;;;;;
+A6A3;BAMUM LETTER KU;Lo;0;L;;;;;N;;;;;
+A6A4;BAMUM LETTER EE;Lo;0;L;;;;;N;;;;;
+A6A5;BAMUM LETTER REE;Lo;0;L;;;;;N;;;;;
+A6A6;BAMUM LETTER TAE;Lo;0;L;;;;;N;;;;;
+A6A7;BAMUM LETTER O;Lo;0;L;;;;;N;;;;;
+A6A8;BAMUM LETTER NYI;Lo;0;L;;;;;N;;;;;
+A6A9;BAMUM LETTER I;Lo;0;L;;;;;N;;;;;
+A6AA;BAMUM LETTER LA;Lo;0;L;;;;;N;;;;;
+A6AB;BAMUM LETTER PA;Lo;0;L;;;;;N;;;;;
+A6AC;BAMUM LETTER RII;Lo;0;L;;;;;N;;;;;
+A6AD;BAMUM LETTER RIEE;Lo;0;L;;;;;N;;;;;
+A6AE;BAMUM LETTER LEEEE;Lo;0;L;;;;;N;;;;;
+A6AF;BAMUM LETTER MEEEE;Lo;0;L;;;;;N;;;;;
+A6B0;BAMUM LETTER TAA;Lo;0;L;;;;;N;;;;;
+A6B1;BAMUM LETTER NDAA;Lo;0;L;;;;;N;;;;;
+A6B2;BAMUM LETTER NJAEM;Lo;0;L;;;;;N;;;;;
+A6B3;BAMUM LETTER M;Lo;0;L;;;;;N;;;;;
+A6B4;BAMUM LETTER SUU;Lo;0;L;;;;;N;;;;;
+A6B5;BAMUM LETTER MU;Lo;0;L;;;;;N;;;;;
+A6B6;BAMUM LETTER SHII;Lo;0;L;;;;;N;;;;;
+A6B7;BAMUM LETTER SI;Lo;0;L;;;;;N;;;;;
+A6B8;BAMUM LETTER SHEUX;Lo;0;L;;;;;N;;;;;
+A6B9;BAMUM LETTER SEUX;Lo;0;L;;;;;N;;;;;
+A6BA;BAMUM LETTER KYEE;Lo;0;L;;;;;N;;;;;
+A6BB;BAMUM LETTER KET;Lo;0;L;;;;;N;;;;;
+A6BC;BAMUM LETTER NUAE;Lo;0;L;;;;;N;;;;;
+A6BD;BAMUM LETTER NU;Lo;0;L;;;;;N;;;;;
+A6BE;BAMUM LETTER NJUAE;Lo;0;L;;;;;N;;;;;
+A6BF;BAMUM LETTER YOQ;Lo;0;L;;;;;N;;;;;
+A6C0;BAMUM LETTER SHU;Lo;0;L;;;;;N;;;;;
+A6C1;BAMUM LETTER YUQ;Lo;0;L;;;;;N;;;;;
+A6C2;BAMUM LETTER YA;Lo;0;L;;;;;N;;;;;
+A6C3;BAMUM LETTER NSHA;Lo;0;L;;;;;N;;;;;
+A6C4;BAMUM LETTER KEUX;Lo;0;L;;;;;N;;;;;
+A6C5;BAMUM LETTER PEUX;Lo;0;L;;;;;N;;;;;
+A6C6;BAMUM LETTER NJEE;Lo;0;L;;;;;N;;;;;
+A6C7;BAMUM LETTER NTEE;Lo;0;L;;;;;N;;;;;
+A6C8;BAMUM LETTER PUE;Lo;0;L;;;;;N;;;;;
+A6C9;BAMUM LETTER WUE;Lo;0;L;;;;;N;;;;;
+A6CA;BAMUM LETTER PEE;Lo;0;L;;;;;N;;;;;
+A6CB;BAMUM LETTER FEE;Lo;0;L;;;;;N;;;;;
+A6CC;BAMUM LETTER RU;Lo;0;L;;;;;N;;;;;
+A6CD;BAMUM LETTER LU;Lo;0;L;;;;;N;;;;;
+A6CE;BAMUM LETTER MI;Lo;0;L;;;;;N;;;;;
+A6CF;BAMUM LETTER NI;Lo;0;L;;;;;N;;;;;
+A6D0;BAMUM LETTER REUX;Lo;0;L;;;;;N;;;;;
+A6D1;BAMUM LETTER RAE;Lo;0;L;;;;;N;;;;;
+A6D2;BAMUM LETTER KEN;Lo;0;L;;;;;N;;;;;
+A6D3;BAMUM LETTER NGKWAEN;Lo;0;L;;;;;N;;;;;
+A6D4;BAMUM LETTER NGGA;Lo;0;L;;;;;N;;;;;
+A6D5;BAMUM LETTER NGA;Lo;0;L;;;;;N;;;;;
+A6D6;BAMUM LETTER SHO;Lo;0;L;;;;;N;;;;;
+A6D7;BAMUM LETTER PUAE;Lo;0;L;;;;;N;;;;;
+A6D8;BAMUM LETTER FU;Lo;0;L;;;;;N;;;;;
+A6D9;BAMUM LETTER FOM;Lo;0;L;;;;;N;;;;;
+A6DA;BAMUM LETTER WA;Lo;0;L;;;;;N;;;;;
+A6DB;BAMUM LETTER NA;Lo;0;L;;;;;N;;;;;
+A6DC;BAMUM LETTER LI;Lo;0;L;;;;;N;;;;;
+A6DD;BAMUM LETTER PI;Lo;0;L;;;;;N;;;;;
+A6DE;BAMUM LETTER LOQ;Lo;0;L;;;;;N;;;;;
+A6DF;BAMUM LETTER KO;Lo;0;L;;;;;N;;;;;
+A6E0;BAMUM LETTER MBEN;Lo;0;L;;;;;N;;;;;
+A6E1;BAMUM LETTER REN;Lo;0;L;;;;;N;;;;;
+A6E2;BAMUM LETTER MEN;Lo;0;L;;;;;N;;;;;
+A6E3;BAMUM LETTER MA;Lo;0;L;;;;;N;;;;;
+A6E4;BAMUM LETTER TI;Lo;0;L;;;;;N;;;;;
+A6E5;BAMUM LETTER KI;Lo;0;L;;;;;N;;;;;
+A6E6;BAMUM LETTER MO;Nl;0;L;;;;1;N;;;;;
+A6E7;BAMUM LETTER MBAA;Nl;0;L;;;;2;N;;;;;
+A6E8;BAMUM LETTER TET;Nl;0;L;;;;3;N;;;;;
+A6E9;BAMUM LETTER KPA;Nl;0;L;;;;4;N;;;;;
+A6EA;BAMUM LETTER TEN;Nl;0;L;;;;5;N;;;;;
+A6EB;BAMUM LETTER NTUU;Nl;0;L;;;;6;N;;;;;
+A6EC;BAMUM LETTER SAMBA;Nl;0;L;;;;7;N;;;;;
+A6ED;BAMUM LETTER FAAMAE;Nl;0;L;;;;8;N;;;;;
+A6EE;BAMUM LETTER KOVUU;Nl;0;L;;;;9;N;;;;;
+A6EF;BAMUM LETTER KOGHOM;Nl;0;L;;;;0;N;;;;;
+A6F0;BAMUM COMBINING MARK KOQNDON;Mn;230;NSM;;;;;N;;;;;
+A6F1;BAMUM COMBINING MARK TUKWENTIS;Mn;230;NSM;;;;;N;;;;;
+A6F2;BAMUM NJAEMLI;Po;0;L;;;;;N;;;;;
+A6F3;BAMUM FULL STOP;Po;0;L;;;;;N;;;;;
+A6F4;BAMUM COLON;Po;0;L;;;;;N;;;;;
+A6F5;BAMUM COMMA;Po;0;L;;;;;N;;;;;
+A6F6;BAMUM SEMICOLON;Po;0;L;;;;;N;;;;;
+A6F7;BAMUM QUESTION MARK;Po;0;L;;;;;N;;;;;
+A700;MODIFIER LETTER CHINESE TONE YIN PING;Sk;0;ON;;;;;N;;;;;
+A701;MODIFIER LETTER CHINESE TONE YANG PING;Sk;0;ON;;;;;N;;;;;
+A702;MODIFIER LETTER CHINESE TONE YIN SHANG;Sk;0;ON;;;;;N;;;;;
+A703;MODIFIER LETTER CHINESE TONE YANG SHANG;Sk;0;ON;;;;;N;;;;;
+A704;MODIFIER LETTER CHINESE TONE YIN QU;Sk;0;ON;;;;;N;;;;;
+A705;MODIFIER LETTER CHINESE TONE YANG QU;Sk;0;ON;;;;;N;;;;;
+A706;MODIFIER LETTER CHINESE TONE YIN RU;Sk;0;ON;;;;;N;;;;;
+A707;MODIFIER LETTER CHINESE TONE YANG RU;Sk;0;ON;;;;;N;;;;;
+A708;MODIFIER LETTER EXTRA-HIGH DOTTED TONE BAR;Sk;0;ON;;;;;N;;;;;
+A709;MODIFIER LETTER HIGH DOTTED TONE BAR;Sk;0;ON;;;;;N;;;;;
+A70A;MODIFIER LETTER MID DOTTED TONE BAR;Sk;0;ON;;;;;N;;;;;
+A70B;MODIFIER LETTER LOW DOTTED TONE BAR;Sk;0;ON;;;;;N;;;;;
+A70C;MODIFIER LETTER EXTRA-LOW DOTTED TONE BAR;Sk;0;ON;;;;;N;;;;;
+A70D;MODIFIER LETTER EXTRA-HIGH DOTTED LEFT-STEM TONE BAR;Sk;0;ON;;;;;N;;;;;
+A70E;MODIFIER LETTER HIGH DOTTED LEFT-STEM TONE BAR;Sk;0;ON;;;;;N;;;;;
+A70F;MODIFIER LETTER MID DOTTED LEFT-STEM TONE BAR;Sk;0;ON;;;;;N;;;;;
+A710;MODIFIER LETTER LOW DOTTED LEFT-STEM TONE BAR;Sk;0;ON;;;;;N;;;;;
+A711;MODIFIER LETTER EXTRA-LOW DOTTED LEFT-STEM TONE BAR;Sk;0;ON;;;;;N;;;;;
+A712;MODIFIER LETTER EXTRA-HIGH LEFT-STEM TONE BAR;Sk;0;ON;;;;;N;;;;;
+A713;MODIFIER LETTER HIGH LEFT-STEM TONE BAR;Sk;0;ON;;;;;N;;;;;
+A714;MODIFIER LETTER MID LEFT-STEM TONE BAR;Sk;0;ON;;;;;N;;;;;
+A715;MODIFIER LETTER LOW LEFT-STEM TONE BAR;Sk;0;ON;;;;;N;;;;;
+A716;MODIFIER LETTER EXTRA-LOW LEFT-STEM TONE BAR;Sk;0;ON;;;;;N;;;;;
+A717;MODIFIER LETTER DOT VERTICAL BAR;Lm;0;ON;;;;;N;;;;;
+A718;MODIFIER LETTER DOT SLASH;Lm;0;ON;;;;;N;;;;;
+A719;MODIFIER LETTER DOT HORIZONTAL BAR;Lm;0;ON;;;;;N;;;;;
+A71A;MODIFIER LETTER LOWER RIGHT CORNER ANGLE;Lm;0;ON;;;;;N;;;;;
+A71B;MODIFIER LETTER RAISED UP ARROW;Lm;0;ON;;;;;N;;;;;
+A71C;MODIFIER LETTER RAISED DOWN ARROW;Lm;0;ON;;;;;N;;;;;
+A71D;MODIFIER LETTER RAISED EXCLAMATION MARK;Lm;0;ON;;;;;N;;;;;
+A71E;MODIFIER LETTER RAISED INVERTED EXCLAMATION MARK;Lm;0;ON;;;;;N;;;;;
+A71F;MODIFIER LETTER LOW INVERTED EXCLAMATION MARK;Lm;0;ON;;;;;N;;;;;
+A720;MODIFIER LETTER STRESS AND HIGH TONE;Sk;0;ON;;;;;N;;;;;
+A721;MODIFIER LETTER STRESS AND LOW TONE;Sk;0;ON;;;;;N;;;;;
+A722;LATIN CAPITAL LETTER EGYPTOLOGICAL ALEF;Lu;0;L;;;;;N;;;;A723;
+A723;LATIN SMALL LETTER EGYPTOLOGICAL ALEF;Ll;0;L;;;;;N;;;A722;;A722
+A724;LATIN CAPITAL LETTER EGYPTOLOGICAL AIN;Lu;0;L;;;;;N;;;;A725;
+A725;LATIN SMALL LETTER EGYPTOLOGICAL AIN;Ll;0;L;;;;;N;;;A724;;A724
+A726;LATIN CAPITAL LETTER HENG;Lu;0;L;;;;;N;;;;A727;
+A727;LATIN SMALL LETTER HENG;Ll;0;L;;;;;N;;;A726;;A726
+A728;LATIN CAPITAL LETTER TZ;Lu;0;L;;;;;N;;;;A729;
+A729;LATIN SMALL LETTER TZ;Ll;0;L;;;;;N;;;A728;;A728
+A72A;LATIN CAPITAL LETTER TRESILLO;Lu;0;L;;;;;N;;;;A72B;
+A72B;LATIN SMALL LETTER TRESILLO;Ll;0;L;;;;;N;;;A72A;;A72A
+A72C;LATIN CAPITAL LETTER CUATRILLO;Lu;0;L;;;;;N;;;;A72D;
+A72D;LATIN SMALL LETTER CUATRILLO;Ll;0;L;;;;;N;;;A72C;;A72C
+A72E;LATIN CAPITAL LETTER CUATRILLO WITH COMMA;Lu;0;L;;;;;N;;;;A72F;
+A72F;LATIN SMALL LETTER CUATRILLO WITH COMMA;Ll;0;L;;;;;N;;;A72E;;A72E
+A730;LATIN LETTER SMALL CAPITAL F;Ll;0;L;;;;;N;;;;;
+A731;LATIN LETTER SMALL CAPITAL S;Ll;0;L;;;;;N;;;;;
+A732;LATIN CAPITAL LETTER AA;Lu;0;L;;;;;N;;;;A733;
+A733;LATIN SMALL LETTER AA;Ll;0;L;;;;;N;;;A732;;A732
+A734;LATIN CAPITAL LETTER AO;Lu;0;L;;;;;N;;;;A735;
+A735;LATIN SMALL LETTER AO;Ll;0;L;;;;;N;;;A734;;A734
+A736;LATIN CAPITAL LETTER AU;Lu;0;L;;;;;N;;;;A737;
+A737;LATIN SMALL LETTER AU;Ll;0;L;;;;;N;;;A736;;A736
+A738;LATIN CAPITAL LETTER AV;Lu;0;L;;;;;N;;;;A739;
+A739;LATIN SMALL LETTER AV;Ll;0;L;;;;;N;;;A738;;A738
+A73A;LATIN CAPITAL LETTER AV WITH HORIZONTAL BAR;Lu;0;L;;;;;N;;;;A73B;
+A73B;LATIN SMALL LETTER AV WITH HORIZONTAL BAR;Ll;0;L;;;;;N;;;A73A;;A73A
+A73C;LATIN CAPITAL LETTER AY;Lu;0;L;;;;;N;;;;A73D;
+A73D;LATIN SMALL LETTER AY;Ll;0;L;;;;;N;;;A73C;;A73C
+A73E;LATIN CAPITAL LETTER REVERSED C WITH DOT;Lu;0;L;;;;;N;;;;A73F;
+A73F;LATIN SMALL LETTER REVERSED C WITH DOT;Ll;0;L;;;;;N;;;A73E;;A73E
+A740;LATIN CAPITAL LETTER K WITH STROKE;Lu;0;L;;;;;N;;;;A741;
+A741;LATIN SMALL LETTER K WITH STROKE;Ll;0;L;;;;;N;;;A740;;A740
+A742;LATIN CAPITAL LETTER K WITH DIAGONAL STROKE;Lu;0;L;;;;;N;;;;A743;
+A743;LATIN SMALL LETTER K WITH DIAGONAL STROKE;Ll;0;L;;;;;N;;;A742;;A742
+A744;LATIN CAPITAL LETTER K WITH STROKE AND DIAGONAL STROKE;Lu;0;L;;;;;N;;;;A745;
+A745;LATIN SMALL LETTER K WITH STROKE AND DIAGONAL STROKE;Ll;0;L;;;;;N;;;A744;;A744
+A746;LATIN CAPITAL LETTER BROKEN L;Lu;0;L;;;;;N;;;;A747;
+A747;LATIN SMALL LETTER BROKEN L;Ll;0;L;;;;;N;;;A746;;A746
+A748;LATIN CAPITAL LETTER L WITH HIGH STROKE;Lu;0;L;;;;;N;;;;A749;
+A749;LATIN SMALL LETTER L WITH HIGH STROKE;Ll;0;L;;;;;N;;;A748;;A748
+A74A;LATIN CAPITAL LETTER O WITH LONG STROKE OVERLAY;Lu;0;L;;;;;N;;;;A74B;
+A74B;LATIN SMALL LETTER O WITH LONG STROKE OVERLAY;Ll;0;L;;;;;N;;;A74A;;A74A
+A74C;LATIN CAPITAL LETTER O WITH LOOP;Lu;0;L;;;;;N;;;;A74D;
+A74D;LATIN SMALL LETTER O WITH LOOP;Ll;0;L;;;;;N;;;A74C;;A74C
+A74E;LATIN CAPITAL LETTER OO;Lu;0;L;;;;;N;;;;A74F;
+A74F;LATIN SMALL LETTER OO;Ll;0;L;;;;;N;;;A74E;;A74E
+A750;LATIN CAPITAL LETTER P WITH STROKE THROUGH DESCENDER;Lu;0;L;;;;;N;;;;A751;
+A751;LATIN SMALL LETTER P WITH STROKE THROUGH DESCENDER;Ll;0;L;;;;;N;;;A750;;A750
+A752;LATIN CAPITAL LETTER P WITH FLOURISH;Lu;0;L;;;;;N;;;;A753;
+A753;LATIN SMALL LETTER P WITH FLOURISH;Ll;0;L;;;;;N;;;A752;;A752
+A754;LATIN CAPITAL LETTER P WITH SQUIRREL TAIL;Lu;0;L;;;;;N;;;;A755;
+A755;LATIN SMALL LETTER P WITH SQUIRREL TAIL;Ll;0;L;;;;;N;;;A754;;A754
+A756;LATIN CAPITAL LETTER Q WITH STROKE THROUGH DESCENDER;Lu;0;L;;;;;N;;;;A757;
+A757;LATIN SMALL LETTER Q WITH STROKE THROUGH DESCENDER;Ll;0;L;;;;;N;;;A756;;A756
+A758;LATIN CAPITAL LETTER Q WITH DIAGONAL STROKE;Lu;0;L;;;;;N;;;;A759;
+A759;LATIN SMALL LETTER Q WITH DIAGONAL STROKE;Ll;0;L;;;;;N;;;A758;;A758
+A75A;LATIN CAPITAL LETTER R ROTUNDA;Lu;0;L;;;;;N;;;;A75B;
+A75B;LATIN SMALL LETTER R ROTUNDA;Ll;0;L;;;;;N;;;A75A;;A75A
+A75C;LATIN CAPITAL LETTER RUM ROTUNDA;Lu;0;L;;;;;N;;;;A75D;
+A75D;LATIN SMALL LETTER RUM ROTUNDA;Ll;0;L;;;;;N;;;A75C;;A75C
+A75E;LATIN CAPITAL LETTER V WITH DIAGONAL STROKE;Lu;0;L;;;;;N;;;;A75F;
+A75F;LATIN SMALL LETTER V WITH DIAGONAL STROKE;Ll;0;L;;;;;N;;;A75E;;A75E
+A760;LATIN CAPITAL LETTER VY;Lu;0;L;;;;;N;;;;A761;
+A761;LATIN SMALL LETTER VY;Ll;0;L;;;;;N;;;A760;;A760
+A762;LATIN CAPITAL LETTER VISIGOTHIC Z;Lu;0;L;;;;;N;;;;A763;
+A763;LATIN SMALL LETTER VISIGOTHIC Z;Ll;0;L;;;;;N;;;A762;;A762
+A764;LATIN CAPITAL LETTER THORN WITH STROKE;Lu;0;L;;;;;N;;;;A765;
+A765;LATIN SMALL LETTER THORN WITH STROKE;Ll;0;L;;;;;N;;;A764;;A764
+A766;LATIN CAPITAL LETTER THORN WITH STROKE THROUGH DESCENDER;Lu;0;L;;;;;N;;;;A767;
+A767;LATIN SMALL LETTER THORN WITH STROKE THROUGH DESCENDER;Ll;0;L;;;;;N;;;A766;;A766
+A768;LATIN CAPITAL LETTER VEND;Lu;0;L;;;;;N;;;;A769;
+A769;LATIN SMALL LETTER VEND;Ll;0;L;;;;;N;;;A768;;A768
+A76A;LATIN CAPITAL LETTER ET;Lu;0;L;;;;;N;;;;A76B;
+A76B;LATIN SMALL LETTER ET;Ll;0;L;;;;;N;;;A76A;;A76A
+A76C;LATIN CAPITAL LETTER IS;Lu;0;L;;;;;N;;;;A76D;
+A76D;LATIN SMALL LETTER IS;Ll;0;L;;;;;N;;;A76C;;A76C
+A76E;LATIN CAPITAL LETTER CON;Lu;0;L;;;;;N;;;;A76F;
+A76F;LATIN SMALL LETTER CON;Ll;0;L;;;;;N;;;A76E;;A76E
+A770;MODIFIER LETTER US;Lm;0;L;<super> A76F;;;;N;;;;;
+A771;LATIN SMALL LETTER DUM;Ll;0;L;;;;;N;;;;;
+A772;LATIN SMALL LETTER LUM;Ll;0;L;;;;;N;;;;;
+A773;LATIN SMALL LETTER MUM;Ll;0;L;;;;;N;;;;;
+A774;LATIN SMALL LETTER NUM;Ll;0;L;;;;;N;;;;;
+A775;LATIN SMALL LETTER RUM;Ll;0;L;;;;;N;;;;;
+A776;LATIN LETTER SMALL CAPITAL RUM;Ll;0;L;;;;;N;;;;;
+A777;LATIN SMALL LETTER TUM;Ll;0;L;;;;;N;;;;;
+A778;LATIN SMALL LETTER UM;Ll;0;L;;;;;N;;;;;
+A779;LATIN CAPITAL LETTER INSULAR D;Lu;0;L;;;;;N;;;;A77A;
+A77A;LATIN SMALL LETTER INSULAR D;Ll;0;L;;;;;N;;;A779;;A779
+A77B;LATIN CAPITAL LETTER INSULAR F;Lu;0;L;;;;;N;;;;A77C;
+A77C;LATIN SMALL LETTER INSULAR F;Ll;0;L;;;;;N;;;A77B;;A77B
+A77D;LATIN CAPITAL LETTER INSULAR G;Lu;0;L;;;;;N;;;;1D79;
+A77E;LATIN CAPITAL LETTER TURNED INSULAR G;Lu;0;L;;;;;N;;;;A77F;
+A77F;LATIN SMALL LETTER TURNED INSULAR G;Ll;0;L;;;;;N;;;A77E;;A77E
+A780;LATIN CAPITAL LETTER TURNED L;Lu;0;L;;;;;N;;;;A781;
+A781;LATIN SMALL LETTER TURNED L;Ll;0;L;;;;;N;;;A780;;A780
+A782;LATIN CAPITAL LETTER INSULAR R;Lu;0;L;;;;;N;;;;A783;
+A783;LATIN SMALL LETTER INSULAR R;Ll;0;L;;;;;N;;;A782;;A782
+A784;LATIN CAPITAL LETTER INSULAR S;Lu;0;L;;;;;N;;;;A785;
+A785;LATIN SMALL LETTER INSULAR S;Ll;0;L;;;;;N;;;A784;;A784
+A786;LATIN CAPITAL LETTER INSULAR T;Lu;0;L;;;;;N;;;;A787;
+A787;LATIN SMALL LETTER INSULAR T;Ll;0;L;;;;;N;;;A786;;A786
+A788;MODIFIER LETTER LOW CIRCUMFLEX ACCENT;Lm;0;ON;;;;;N;;;;;
+A789;MODIFIER LETTER COLON;Sk;0;L;;;;;N;;;;;
+A78A;MODIFIER LETTER SHORT EQUALS SIGN;Sk;0;L;;;;;N;;;;;
+A78B;LATIN CAPITAL LETTER SALTILLO;Lu;0;L;;;;;N;;;;A78C;
+A78C;LATIN SMALL LETTER SALTILLO;Ll;0;L;;;;;N;;;A78B;;A78B
+A78D;LATIN CAPITAL LETTER TURNED H;Lu;0;L;;;;;N;;;;0265;
+A78E;LATIN SMALL LETTER L WITH RETROFLEX HOOK AND BELT;Ll;0;L;;;;;N;;;;;
+A78F;LATIN LETTER SINOLOGICAL DOT;Lo;0;L;;;;;N;;;;;
+A790;LATIN CAPITAL LETTER N WITH DESCENDER;Lu;0;L;;;;;N;;;;A791;
+A791;LATIN SMALL LETTER N WITH DESCENDER;Ll;0;L;;;;;N;;;A790;;A790
+A792;LATIN CAPITAL LETTER C WITH BAR;Lu;0;L;;;;;N;;;;A793;
+A793;LATIN SMALL LETTER C WITH BAR;Ll;0;L;;;;;N;;;A792;;A792
+A794;LATIN SMALL LETTER C WITH PALATAL HOOK;Ll;0;L;;;;;N;;;;;
+A795;LATIN SMALL LETTER H WITH PALATAL HOOK;Ll;0;L;;;;;N;;;;;
+A796;LATIN CAPITAL LETTER B WITH FLOURISH;Lu;0;L;;;;;N;;;;A797;
+A797;LATIN SMALL LETTER B WITH FLOURISH;Ll;0;L;;;;;N;;;A796;;A796
+A798;LATIN CAPITAL LETTER F WITH STROKE;Lu;0;L;;;;;N;;;;A799;
+A799;LATIN SMALL LETTER F WITH STROKE;Ll;0;L;;;;;N;;;A798;;A798
+A79A;LATIN CAPITAL LETTER VOLAPUK AE;Lu;0;L;;;;;N;;;;A79B;
+A79B;LATIN SMALL LETTER VOLAPUK AE;Ll;0;L;;;;;N;;;A79A;;A79A
+A79C;LATIN CAPITAL LETTER VOLAPUK OE;Lu;0;L;;;;;N;;;;A79D;
+A79D;LATIN SMALL LETTER VOLAPUK OE;Ll;0;L;;;;;N;;;A79C;;A79C
+A79E;LATIN CAPITAL LETTER VOLAPUK UE;Lu;0;L;;;;;N;;;;A79F;
+A79F;LATIN SMALL LETTER VOLAPUK UE;Ll;0;L;;;;;N;;;A79E;;A79E
+A7A0;LATIN CAPITAL LETTER G WITH OBLIQUE STROKE;Lu;0;L;;;;;N;;;;A7A1;
+A7A1;LATIN SMALL LETTER G WITH OBLIQUE STROKE;Ll;0;L;;;;;N;;;A7A0;;A7A0
+A7A2;LATIN CAPITAL LETTER K WITH OBLIQUE STROKE;Lu;0;L;;;;;N;;;;A7A3;
+A7A3;LATIN SMALL LETTER K WITH OBLIQUE STROKE;Ll;0;L;;;;;N;;;A7A2;;A7A2
+A7A4;LATIN CAPITAL LETTER N WITH OBLIQUE STROKE;Lu;0;L;;;;;N;;;;A7A5;
+A7A5;LATIN SMALL LETTER N WITH OBLIQUE STROKE;Ll;0;L;;;;;N;;;A7A4;;A7A4
+A7A6;LATIN CAPITAL LETTER R WITH OBLIQUE STROKE;Lu;0;L;;;;;N;;;;A7A7;
+A7A7;LATIN SMALL LETTER R WITH OBLIQUE STROKE;Ll;0;L;;;;;N;;;A7A6;;A7A6
+A7A8;LATIN CAPITAL LETTER S WITH OBLIQUE STROKE;Lu;0;L;;;;;N;;;;A7A9;
+A7A9;LATIN SMALL LETTER S WITH OBLIQUE STROKE;Ll;0;L;;;;;N;;;A7A8;;A7A8
+A7AA;LATIN CAPITAL LETTER H WITH HOOK;Lu;0;L;;;;;N;;;;0266;
+A7AB;LATIN CAPITAL LETTER REVERSED OPEN E;Lu;0;L;;;;;N;;;;025C;
+A7AC;LATIN CAPITAL LETTER SCRIPT G;Lu;0;L;;;;;N;;;;0261;
+A7AD;LATIN CAPITAL LETTER L WITH BELT;Lu;0;L;;;;;N;;;;026C;
+A7AE;LATIN CAPITAL LETTER SMALL CAPITAL I;Lu;0;L;;;;;N;;;;026A;
+A7B0;LATIN CAPITAL LETTER TURNED K;Lu;0;L;;;;;N;;;;029E;
+A7B1;LATIN CAPITAL LETTER TURNED T;Lu;0;L;;;;;N;;;;0287;
+A7B2;LATIN CAPITAL LETTER J WITH CROSSED-TAIL;Lu;0;L;;;;;N;;;;029D;
+A7B3;LATIN CAPITAL LETTER CHI;Lu;0;L;;;;;N;;;;AB53;
+A7B4;LATIN CAPITAL LETTER BETA;Lu;0;L;;;;;N;;;;A7B5;
+A7B5;LATIN SMALL LETTER BETA;Ll;0;L;;;;;N;;;A7B4;;A7B4
+A7B6;LATIN CAPITAL LETTER OMEGA;Lu;0;L;;;;;N;;;;A7B7;
+A7B7;LATIN SMALL LETTER OMEGA;Ll;0;L;;;;;N;;;A7B6;;A7B6
+A7F7;LATIN EPIGRAPHIC LETTER SIDEWAYS I;Lo;0;L;;;;;N;;;;;
+A7F8;MODIFIER LETTER CAPITAL H WITH STROKE;Lm;0;L;<super> 0126;;;;N;;;;;
+A7F9;MODIFIER LETTER SMALL LIGATURE OE;Lm;0;L;<super> 0153;;;;N;;;;;
+A7FA;LATIN LETTER SMALL CAPITAL TURNED M;Ll;0;L;;;;;N;;;;;
+A7FB;LATIN EPIGRAPHIC LETTER REVERSED F;Lo;0;L;;;;;N;;;;;
+A7FC;LATIN EPIGRAPHIC LETTER REVERSED P;Lo;0;L;;;;;N;;;;;
+A7FD;LATIN EPIGRAPHIC LETTER INVERTED M;Lo;0;L;;;;;N;;;;;
+A7FE;LATIN EPIGRAPHIC LETTER I LONGA;Lo;0;L;;;;;N;;;;;
+A7FF;LATIN EPIGRAPHIC LETTER ARCHAIC M;Lo;0;L;;;;;N;;;;;
+A800;SYLOTI NAGRI LETTER A;Lo;0;L;;;;;N;;;;;
+A801;SYLOTI NAGRI LETTER I;Lo;0;L;;;;;N;;;;;
+A802;SYLOTI NAGRI SIGN DVISVARA;Mn;0;NSM;;;;;N;;;;;
+A803;SYLOTI NAGRI LETTER U;Lo;0;L;;;;;N;;;;;
+A804;SYLOTI NAGRI LETTER E;Lo;0;L;;;;;N;;;;;
+A805;SYLOTI NAGRI LETTER O;Lo;0;L;;;;;N;;;;;
+A806;SYLOTI NAGRI SIGN HASANTA;Mn;9;NSM;;;;;N;;;;;
+A807;SYLOTI NAGRI LETTER KO;Lo;0;L;;;;;N;;;;;
+A808;SYLOTI NAGRI LETTER KHO;Lo;0;L;;;;;N;;;;;
+A809;SYLOTI NAGRI LETTER GO;Lo;0;L;;;;;N;;;;;
+A80A;SYLOTI NAGRI LETTER GHO;Lo;0;L;;;;;N;;;;;
+A80B;SYLOTI NAGRI SIGN ANUSVARA;Mn;0;NSM;;;;;N;;;;;
+A80C;SYLOTI NAGRI LETTER CO;Lo;0;L;;;;;N;;;;;
+A80D;SYLOTI NAGRI LETTER CHO;Lo;0;L;;;;;N;;;;;
+A80E;SYLOTI NAGRI LETTER JO;Lo;0;L;;;;;N;;;;;
+A80F;SYLOTI NAGRI LETTER JHO;Lo;0;L;;;;;N;;;;;
+A810;SYLOTI NAGRI LETTER TTO;Lo;0;L;;;;;N;;;;;
+A811;SYLOTI NAGRI LETTER TTHO;Lo;0;L;;;;;N;;;;;
+A812;SYLOTI NAGRI LETTER DDO;Lo;0;L;;;;;N;;;;;
+A813;SYLOTI NAGRI LETTER DDHO;Lo;0;L;;;;;N;;;;;
+A814;SYLOTI NAGRI LETTER TO;Lo;0;L;;;;;N;;;;;
+A815;SYLOTI NAGRI LETTER THO;Lo;0;L;;;;;N;;;;;
+A816;SYLOTI NAGRI LETTER DO;Lo;0;L;;;;;N;;;;;
+A817;SYLOTI NAGRI LETTER DHO;Lo;0;L;;;;;N;;;;;
+A818;SYLOTI NAGRI LETTER NO;Lo;0;L;;;;;N;;;;;
+A819;SYLOTI NAGRI LETTER PO;Lo;0;L;;;;;N;;;;;
+A81A;SYLOTI NAGRI LETTER PHO;Lo;0;L;;;;;N;;;;;
+A81B;SYLOTI NAGRI LETTER BO;Lo;0;L;;;;;N;;;;;
+A81C;SYLOTI NAGRI LETTER BHO;Lo;0;L;;;;;N;;;;;
+A81D;SYLOTI NAGRI LETTER MO;Lo;0;L;;;;;N;;;;;
+A81E;SYLOTI NAGRI LETTER RO;Lo;0;L;;;;;N;;;;;
+A81F;SYLOTI NAGRI LETTER LO;Lo;0;L;;;;;N;;;;;
+A820;SYLOTI NAGRI LETTER RRO;Lo;0;L;;;;;N;;;;;
+A821;SYLOTI NAGRI LETTER SO;Lo;0;L;;;;;N;;;;;
+A822;SYLOTI NAGRI LETTER HO;Lo;0;L;;;;;N;;;;;
+A823;SYLOTI NAGRI VOWEL SIGN A;Mc;0;L;;;;;N;;;;;
+A824;SYLOTI NAGRI VOWEL SIGN I;Mc;0;L;;;;;N;;;;;
+A825;SYLOTI NAGRI VOWEL SIGN U;Mn;0;NSM;;;;;N;;;;;
+A826;SYLOTI NAGRI VOWEL SIGN E;Mn;0;NSM;;;;;N;;;;;
+A827;SYLOTI NAGRI VOWEL SIGN OO;Mc;0;L;;;;;N;;;;;
+A828;SYLOTI NAGRI POETRY MARK-1;So;0;ON;;;;;N;;;;;
+A829;SYLOTI NAGRI POETRY MARK-2;So;0;ON;;;;;N;;;;;
+A82A;SYLOTI NAGRI POETRY MARK-3;So;0;ON;;;;;N;;;;;
+A82B;SYLOTI NAGRI POETRY MARK-4;So;0;ON;;;;;N;;;;;
+A830;NORTH INDIC FRACTION ONE QUARTER;No;0;L;;;;1/4;N;;;;;
+A831;NORTH INDIC FRACTION ONE HALF;No;0;L;;;;1/2;N;;;;;
+A832;NORTH INDIC FRACTION THREE QUARTERS;No;0;L;;;;3/4;N;;;;;
+A833;NORTH INDIC FRACTION ONE SIXTEENTH;No;0;L;;;;1/16;N;;;;;
+A834;NORTH INDIC FRACTION ONE EIGHTH;No;0;L;;;;1/8;N;;;;;
+A835;NORTH INDIC FRACTION THREE SIXTEENTHS;No;0;L;;;;3/16;N;;;;;
+A836;NORTH INDIC QUARTER MARK;So;0;L;;;;;N;;;;;
+A837;NORTH INDIC PLACEHOLDER MARK;So;0;L;;;;;N;;;;;
+A838;NORTH INDIC RUPEE MARK;Sc;0;ET;;;;;N;;;;;
+A839;NORTH INDIC QUANTITY MARK;So;0;ET;;;;;N;;;;;
+A840;PHAGS-PA LETTER KA;Lo;0;L;;;;;N;;;;;
+A841;PHAGS-PA LETTER KHA;Lo;0;L;;;;;N;;;;;
+A842;PHAGS-PA LETTER GA;Lo;0;L;;;;;N;;;;;
+A843;PHAGS-PA LETTER NGA;Lo;0;L;;;;;N;;;;;
+A844;PHAGS-PA LETTER CA;Lo;0;L;;;;;N;;;;;
+A845;PHAGS-PA LETTER CHA;Lo;0;L;;;;;N;;;;;
+A846;PHAGS-PA LETTER JA;Lo;0;L;;;;;N;;;;;
+A847;PHAGS-PA LETTER NYA;Lo;0;L;;;;;N;;;;;
+A848;PHAGS-PA LETTER TA;Lo;0;L;;;;;N;;;;;
+A849;PHAGS-PA LETTER THA;Lo;0;L;;;;;N;;;;;
+A84A;PHAGS-PA LETTER DA;Lo;0;L;;;;;N;;;;;
+A84B;PHAGS-PA LETTER NA;Lo;0;L;;;;;N;;;;;
+A84C;PHAGS-PA LETTER PA;Lo;0;L;;;;;N;;;;;
+A84D;PHAGS-PA LETTER PHA;Lo;0;L;;;;;N;;;;;
+A84E;PHAGS-PA LETTER BA;Lo;0;L;;;;;N;;;;;
+A84F;PHAGS-PA LETTER MA;Lo;0;L;;;;;N;;;;;
+A850;PHAGS-PA LETTER TSA;Lo;0;L;;;;;N;;;;;
+A851;PHAGS-PA LETTER TSHA;Lo;0;L;;;;;N;;;;;
+A852;PHAGS-PA LETTER DZA;Lo;0;L;;;;;N;;;;;
+A853;PHAGS-PA LETTER WA;Lo;0;L;;;;;N;;;;;
+A854;PHAGS-PA LETTER ZHA;Lo;0;L;;;;;N;;;;;
+A855;PHAGS-PA LETTER ZA;Lo;0;L;;;;;N;;;;;
+A856;PHAGS-PA LETTER SMALL A;Lo;0;L;;;;;N;;;;;
+A857;PHAGS-PA LETTER YA;Lo;0;L;;;;;N;;;;;
+A858;PHAGS-PA LETTER RA;Lo;0;L;;;;;N;;;;;
+A859;PHAGS-PA LETTER LA;Lo;0;L;;;;;N;;;;;
+A85A;PHAGS-PA LETTER SHA;Lo;0;L;;;;;N;;;;;
+A85B;PHAGS-PA LETTER SA;Lo;0;L;;;;;N;;;;;
+A85C;PHAGS-PA LETTER HA;Lo;0;L;;;;;N;;;;;
+A85D;PHAGS-PA LETTER A;Lo;0;L;;;;;N;;;;;
+A85E;PHAGS-PA LETTER I;Lo;0;L;;;;;N;;;;;
+A85F;PHAGS-PA LETTER U;Lo;0;L;;;;;N;;;;;
+A860;PHAGS-PA LETTER E;Lo;0;L;;;;;N;;;;;
+A861;PHAGS-PA LETTER O;Lo;0;L;;;;;N;;;;;
+A862;PHAGS-PA LETTER QA;Lo;0;L;;;;;N;;;;;
+A863;PHAGS-PA LETTER XA;Lo;0;L;;;;;N;;;;;
+A864;PHAGS-PA LETTER FA;Lo;0;L;;;;;N;;;;;
+A865;PHAGS-PA LETTER GGA;Lo;0;L;;;;;N;;;;;
+A866;PHAGS-PA LETTER EE;Lo;0;L;;;;;N;;;;;
+A867;PHAGS-PA SUBJOINED LETTER WA;Lo;0;L;;;;;N;;;;;
+A868;PHAGS-PA SUBJOINED LETTER YA;Lo;0;L;;;;;N;;;;;
+A869;PHAGS-PA LETTER TTA;Lo;0;L;;;;;N;;;;;
+A86A;PHAGS-PA LETTER TTHA;Lo;0;L;;;;;N;;;;;
+A86B;PHAGS-PA LETTER DDA;Lo;0;L;;;;;N;;;;;
+A86C;PHAGS-PA LETTER NNA;Lo;0;L;;;;;N;;;;;
+A86D;PHAGS-PA LETTER ALTERNATE YA;Lo;0;L;;;;;N;;;;;
+A86E;PHAGS-PA LETTER VOICELESS SHA;Lo;0;L;;;;;N;;;;;
+A86F;PHAGS-PA LETTER VOICED HA;Lo;0;L;;;;;N;;;;;
+A870;PHAGS-PA LETTER ASPIRATED FA;Lo;0;L;;;;;N;;;;;
+A871;PHAGS-PA SUBJOINED LETTER RA;Lo;0;L;;;;;N;;;;;
+A872;PHAGS-PA SUPERFIXED LETTER RA;Lo;0;L;;;;;N;;;;;
+A873;PHAGS-PA LETTER CANDRABINDU;Lo;0;L;;;;;N;;;;;
+A874;PHAGS-PA SINGLE HEAD MARK;Po;0;ON;;;;;N;;;;;
+A875;PHAGS-PA DOUBLE HEAD MARK;Po;0;ON;;;;;N;;;;;
+A876;PHAGS-PA MARK SHAD;Po;0;ON;;;;;N;;;;;
+A877;PHAGS-PA MARK DOUBLE SHAD;Po;0;ON;;;;;N;;;;;
+A880;SAURASHTRA SIGN ANUSVARA;Mc;0;L;;;;;N;;;;;
+A881;SAURASHTRA SIGN VISARGA;Mc;0;L;;;;;N;;;;;
+A882;SAURASHTRA LETTER A;Lo;0;L;;;;;N;;;;;
+A883;SAURASHTRA LETTER AA;Lo;0;L;;;;;N;;;;;
+A884;SAURASHTRA LETTER I;Lo;0;L;;;;;N;;;;;
+A885;SAURASHTRA LETTER II;Lo;0;L;;;;;N;;;;;
+A886;SAURASHTRA LETTER U;Lo;0;L;;;;;N;;;;;
+A887;SAURASHTRA LETTER UU;Lo;0;L;;;;;N;;;;;
+A888;SAURASHTRA LETTER VOCALIC R;Lo;0;L;;;;;N;;;;;
+A889;SAURASHTRA LETTER VOCALIC RR;Lo;0;L;;;;;N;;;;;
+A88A;SAURASHTRA LETTER VOCALIC L;Lo;0;L;;;;;N;;;;;
+A88B;SAURASHTRA LETTER VOCALIC LL;Lo;0;L;;;;;N;;;;;
+A88C;SAURASHTRA LETTER E;Lo;0;L;;;;;N;;;;;
+A88D;SAURASHTRA LETTER EE;Lo;0;L;;;;;N;;;;;
+A88E;SAURASHTRA LETTER AI;Lo;0;L;;;;;N;;;;;
+A88F;SAURASHTRA LETTER O;Lo;0;L;;;;;N;;;;;
+A890;SAURASHTRA LETTER OO;Lo;0;L;;;;;N;;;;;
+A891;SAURASHTRA LETTER AU;Lo;0;L;;;;;N;;;;;
+A892;SAURASHTRA LETTER KA;Lo;0;L;;;;;N;;;;;
+A893;SAURASHTRA LETTER KHA;Lo;0;L;;;;;N;;;;;
+A894;SAURASHTRA LETTER GA;Lo;0;L;;;;;N;;;;;
+A895;SAURASHTRA LETTER GHA;Lo;0;L;;;;;N;;;;;
+A896;SAURASHTRA LETTER NGA;Lo;0;L;;;;;N;;;;;
+A897;SAURASHTRA LETTER CA;Lo;0;L;;;;;N;;;;;
+A898;SAURASHTRA LETTER CHA;Lo;0;L;;;;;N;;;;;
+A899;SAURASHTRA LETTER JA;Lo;0;L;;;;;N;;;;;
+A89A;SAURASHTRA LETTER JHA;Lo;0;L;;;;;N;;;;;
+A89B;SAURASHTRA LETTER NYA;Lo;0;L;;;;;N;;;;;
+A89C;SAURASHTRA LETTER TTA;Lo;0;L;;;;;N;;;;;
+A89D;SAURASHTRA LETTER TTHA;Lo;0;L;;;;;N;;;;;
+A89E;SAURASHTRA LETTER DDA;Lo;0;L;;;;;N;;;;;
+A89F;SAURASHTRA LETTER DDHA;Lo;0;L;;;;;N;;;;;
+A8A0;SAURASHTRA LETTER NNA;Lo;0;L;;;;;N;;;;;
+A8A1;SAURASHTRA LETTER TA;Lo;0;L;;;;;N;;;;;
+A8A2;SAURASHTRA LETTER THA;Lo;0;L;;;;;N;;;;;
+A8A3;SAURASHTRA LETTER DA;Lo;0;L;;;;;N;;;;;
+A8A4;SAURASHTRA LETTER DHA;Lo;0;L;;;;;N;;;;;
+A8A5;SAURASHTRA LETTER NA;Lo;0;L;;;;;N;;;;;
+A8A6;SAURASHTRA LETTER PA;Lo;0;L;;;;;N;;;;;
+A8A7;SAURASHTRA LETTER PHA;Lo;0;L;;;;;N;;;;;
+A8A8;SAURASHTRA LETTER BA;Lo;0;L;;;;;N;;;;;
+A8A9;SAURASHTRA LETTER BHA;Lo;0;L;;;;;N;;;;;
+A8AA;SAURASHTRA LETTER MA;Lo;0;L;;;;;N;;;;;
+A8AB;SAURASHTRA LETTER YA;Lo;0;L;;;;;N;;;;;
+A8AC;SAURASHTRA LETTER RA;Lo;0;L;;;;;N;;;;;
+A8AD;SAURASHTRA LETTER LA;Lo;0;L;;;;;N;;;;;
+A8AE;SAURASHTRA LETTER VA;Lo;0;L;;;;;N;;;;;
+A8AF;SAURASHTRA LETTER SHA;Lo;0;L;;;;;N;;;;;
+A8B0;SAURASHTRA LETTER SSA;Lo;0;L;;;;;N;;;;;
+A8B1;SAURASHTRA LETTER SA;Lo;0;L;;;;;N;;;;;
+A8B2;SAURASHTRA LETTER HA;Lo;0;L;;;;;N;;;;;
+A8B3;SAURASHTRA LETTER LLA;Lo;0;L;;;;;N;;;;;
+A8B4;SAURASHTRA CONSONANT SIGN HAARU;Mc;0;L;;;;;N;;;;;
+A8B5;SAURASHTRA VOWEL SIGN AA;Mc;0;L;;;;;N;;;;;
+A8B6;SAURASHTRA VOWEL SIGN I;Mc;0;L;;;;;N;;;;;
+A8B7;SAURASHTRA VOWEL SIGN II;Mc;0;L;;;;;N;;;;;
+A8B8;SAURASHTRA VOWEL SIGN U;Mc;0;L;;;;;N;;;;;
+A8B9;SAURASHTRA VOWEL SIGN UU;Mc;0;L;;;;;N;;;;;
+A8BA;SAURASHTRA VOWEL SIGN VOCALIC R;Mc;0;L;;;;;N;;;;;
+A8BB;SAURASHTRA VOWEL SIGN VOCALIC RR;Mc;0;L;;;;;N;;;;;
+A8BC;SAURASHTRA VOWEL SIGN VOCALIC L;Mc;0;L;;;;;N;;;;;
+A8BD;SAURASHTRA VOWEL SIGN VOCALIC LL;Mc;0;L;;;;;N;;;;;
+A8BE;SAURASHTRA VOWEL SIGN E;Mc;0;L;;;;;N;;;;;
+A8BF;SAURASHTRA VOWEL SIGN EE;Mc;0;L;;;;;N;;;;;
+A8C0;SAURASHTRA VOWEL SIGN AI;Mc;0;L;;;;;N;;;;;
+A8C1;SAURASHTRA VOWEL SIGN O;Mc;0;L;;;;;N;;;;;
+A8C2;SAURASHTRA VOWEL SIGN OO;Mc;0;L;;;;;N;;;;;
+A8C3;SAURASHTRA VOWEL SIGN AU;Mc;0;L;;;;;N;;;;;
+A8C4;SAURASHTRA SIGN VIRAMA;Mn;9;NSM;;;;;N;;;;;
+A8C5;SAURASHTRA SIGN CANDRABINDU;Mn;0;NSM;;;;;N;;;;;
+A8CE;SAURASHTRA DANDA;Po;0;L;;;;;N;;;;;
+A8CF;SAURASHTRA DOUBLE DANDA;Po;0;L;;;;;N;;;;;
+A8D0;SAURASHTRA DIGIT ZERO;Nd;0;L;;0;0;0;N;;;;;
+A8D1;SAURASHTRA DIGIT ONE;Nd;0;L;;1;1;1;N;;;;;
+A8D2;SAURASHTRA DIGIT TWO;Nd;0;L;;2;2;2;N;;;;;
+A8D3;SAURASHTRA DIGIT THREE;Nd;0;L;;3;3;3;N;;;;;
+A8D4;SAURASHTRA DIGIT FOUR;Nd;0;L;;4;4;4;N;;;;;
+A8D5;SAURASHTRA DIGIT FIVE;Nd;0;L;;5;5;5;N;;;;;
+A8D6;SAURASHTRA DIGIT SIX;Nd;0;L;;6;6;6;N;;;;;
+A8D7;SAURASHTRA DIGIT SEVEN;Nd;0;L;;7;7;7;N;;;;;
+A8D8;SAURASHTRA DIGIT EIGHT;Nd;0;L;;8;8;8;N;;;;;
+A8D9;SAURASHTRA DIGIT NINE;Nd;0;L;;9;9;9;N;;;;;
+A8E0;COMBINING DEVANAGARI DIGIT ZERO;Mn;230;NSM;;;;;N;;;;;
+A8E1;COMBINING DEVANAGARI DIGIT ONE;Mn;230;NSM;;;;;N;;;;;
+A8E2;COMBINING DEVANAGARI DIGIT TWO;Mn;230;NSM;;;;;N;;;;;
+A8E3;COMBINING DEVANAGARI DIGIT THREE;Mn;230;NSM;;;;;N;;;;;
+A8E4;COMBINING DEVANAGARI DIGIT FOUR;Mn;230;NSM;;;;;N;;;;;
+A8E5;COMBINING DEVANAGARI DIGIT FIVE;Mn;230;NSM;;;;;N;;;;;
+A8E6;COMBINING DEVANAGARI DIGIT SIX;Mn;230;NSM;;;;;N;;;;;
+A8E7;COMBINING DEVANAGARI DIGIT SEVEN;Mn;230;NSM;;;;;N;;;;;
+A8E8;COMBINING DEVANAGARI DIGIT EIGHT;Mn;230;NSM;;;;;N;;;;;
+A8E9;COMBINING DEVANAGARI DIGIT NINE;Mn;230;NSM;;;;;N;;;;;
+A8EA;COMBINING DEVANAGARI LETTER A;Mn;230;NSM;;;;;N;;;;;
+A8EB;COMBINING DEVANAGARI LETTER U;Mn;230;NSM;;;;;N;;;;;
+A8EC;COMBINING DEVANAGARI LETTER KA;Mn;230;NSM;;;;;N;;;;;
+A8ED;COMBINING DEVANAGARI LETTER NA;Mn;230;NSM;;;;;N;;;;;
+A8EE;COMBINING DEVANAGARI LETTER PA;Mn;230;NSM;;;;;N;;;;;
+A8EF;COMBINING DEVANAGARI LETTER RA;Mn;230;NSM;;;;;N;;;;;
+A8F0;COMBINING DEVANAGARI LETTER VI;Mn;230;NSM;;;;;N;;;;;
+A8F1;COMBINING DEVANAGARI SIGN AVAGRAHA;Mn;230;NSM;;;;;N;;;;;
+A8F2;DEVANAGARI SIGN SPACING CANDRABINDU;Lo;0;L;;;;;N;;;;;
+A8F3;DEVANAGARI SIGN CANDRABINDU VIRAMA;Lo;0;L;;;;;N;;;;;
+A8F4;DEVANAGARI SIGN DOUBLE CANDRABINDU VIRAMA;Lo;0;L;;;;;N;;;;;
+A8F5;DEVANAGARI SIGN CANDRABINDU TWO;Lo;0;L;;;;;N;;;;;
+A8F6;DEVANAGARI SIGN CANDRABINDU THREE;Lo;0;L;;;;;N;;;;;
+A8F7;DEVANAGARI SIGN CANDRABINDU AVAGRAHA;Lo;0;L;;;;;N;;;;;
+A8F8;DEVANAGARI SIGN PUSHPIKA;Po;0;L;;;;;N;;;;;
+A8F9;DEVANAGARI GAP FILLER;Po;0;L;;;;;N;;;;;
+A8FA;DEVANAGARI CARET;Po;0;L;;;;;N;;;;;
+A8FB;DEVANAGARI HEADSTROKE;Lo;0;L;;;;;N;;;;;
+A8FC;DEVANAGARI SIGN SIDDHAM;Po;0;L;;;;;N;;;;;
+A8FD;DEVANAGARI JAIN OM;Lo;0;L;;;;;N;;;;;
+A900;KAYAH LI DIGIT ZERO;Nd;0;L;;0;0;0;N;;;;;
+A901;KAYAH LI DIGIT ONE;Nd;0;L;;1;1;1;N;;;;;
+A902;KAYAH LI DIGIT TWO;Nd;0;L;;2;2;2;N;;;;;
+A903;KAYAH LI DIGIT THREE;Nd;0;L;;3;3;3;N;;;;;
+A904;KAYAH LI DIGIT FOUR;Nd;0;L;;4;4;4;N;;;;;
+A905;KAYAH LI DIGIT FIVE;Nd;0;L;;5;5;5;N;;;;;
+A906;KAYAH LI DIGIT SIX;Nd;0;L;;6;6;6;N;;;;;
+A907;KAYAH LI DIGIT SEVEN;Nd;0;L;;7;7;7;N;;;;;
+A908;KAYAH LI DIGIT EIGHT;Nd;0;L;;8;8;8;N;;;;;
+A909;KAYAH LI DIGIT NINE;Nd;0;L;;9;9;9;N;;;;;
+A90A;KAYAH LI LETTER KA;Lo;0;L;;;;;N;;;;;
+A90B;KAYAH LI LETTER KHA;Lo;0;L;;;;;N;;;;;
+A90C;KAYAH LI LETTER GA;Lo;0;L;;;;;N;;;;;
+A90D;KAYAH LI LETTER NGA;Lo;0;L;;;;;N;;;;;
+A90E;KAYAH LI LETTER SA;Lo;0;L;;;;;N;;;;;
+A90F;KAYAH LI LETTER SHA;Lo;0;L;;;;;N;;;;;
+A910;KAYAH LI LETTER ZA;Lo;0;L;;;;;N;;;;;
+A911;KAYAH LI LETTER NYA;Lo;0;L;;;;;N;;;;;
+A912;KAYAH LI LETTER TA;Lo;0;L;;;;;N;;;;;
+A913;KAYAH LI LETTER HTA;Lo;0;L;;;;;N;;;;;
+A914;KAYAH LI LETTER NA;Lo;0;L;;;;;N;;;;;
+A915;KAYAH LI LETTER PA;Lo;0;L;;;;;N;;;;;
+A916;KAYAH LI LETTER PHA;Lo;0;L;;;;;N;;;;;
+A917;KAYAH LI LETTER MA;Lo;0;L;;;;;N;;;;;
+A918;KAYAH LI LETTER DA;Lo;0;L;;;;;N;;;;;
+A919;KAYAH LI LETTER BA;Lo;0;L;;;;;N;;;;;
+A91A;KAYAH LI LETTER RA;Lo;0;L;;;;;N;;;;;
+A91B;KAYAH LI LETTER YA;Lo;0;L;;;;;N;;;;;
+A91C;KAYAH LI LETTER LA;Lo;0;L;;;;;N;;;;;
+A91D;KAYAH LI LETTER WA;Lo;0;L;;;;;N;;;;;
+A91E;KAYAH LI LETTER THA;Lo;0;L;;;;;N;;;;;
+A91F;KAYAH LI LETTER HA;Lo;0;L;;;;;N;;;;;
+A920;KAYAH LI LETTER VA;Lo;0;L;;;;;N;;;;;
+A921;KAYAH LI LETTER CA;Lo;0;L;;;;;N;;;;;
+A922;KAYAH LI LETTER A;Lo;0;L;;;;;N;;;;;
+A923;KAYAH LI LETTER OE;Lo;0;L;;;;;N;;;;;
+A924;KAYAH LI LETTER I;Lo;0;L;;;;;N;;;;;
+A925;KAYAH LI LETTER OO;Lo;0;L;;;;;N;;;;;
+A926;KAYAH LI VOWEL UE;Mn;0;NSM;;;;;N;;;;;
+A927;KAYAH LI VOWEL E;Mn;0;NSM;;;;;N;;;;;
+A928;KAYAH LI VOWEL U;Mn;0;NSM;;;;;N;;;;;
+A929;KAYAH LI VOWEL EE;Mn;0;NSM;;;;;N;;;;;
+A92A;KAYAH LI VOWEL O;Mn;0;NSM;;;;;N;;;;;
+A92B;KAYAH LI TONE PLOPHU;Mn;220;NSM;;;;;N;;;;;
+A92C;KAYAH LI TONE CALYA;Mn;220;NSM;;;;;N;;;;;
+A92D;KAYAH LI TONE CALYA PLOPHU;Mn;220;NSM;;;;;N;;;;;
+A92E;KAYAH LI SIGN CWI;Po;0;L;;;;;N;;;;;
+A92F;KAYAH LI SIGN SHYA;Po;0;L;;;;;N;;;;;
+A930;REJANG LETTER KA;Lo;0;L;;;;;N;;;;;
+A931;REJANG LETTER GA;Lo;0;L;;;;;N;;;;;
+A932;REJANG LETTER NGA;Lo;0;L;;;;;N;;;;;
+A933;REJANG LETTER TA;Lo;0;L;;;;;N;;;;;
+A934;REJANG LETTER DA;Lo;0;L;;;;;N;;;;;
+A935;REJANG LETTER NA;Lo;0;L;;;;;N;;;;;
+A936;REJANG LETTER PA;Lo;0;L;;;;;N;;;;;
+A937;REJANG LETTER BA;Lo;0;L;;;;;N;;;;;
+A938;REJANG LETTER MA;Lo;0;L;;;;;N;;;;;
+A939;REJANG LETTER CA;Lo;0;L;;;;;N;;;;;
+A93A;REJANG LETTER JA;Lo;0;L;;;;;N;;;;;
+A93B;REJANG LETTER NYA;Lo;0;L;;;;;N;;;;;
+A93C;REJANG LETTER SA;Lo;0;L;;;;;N;;;;;
+A93D;REJANG LETTER RA;Lo;0;L;;;;;N;;;;;
+A93E;REJANG LETTER LA;Lo;0;L;;;;;N;;;;;
+A93F;REJANG LETTER YA;Lo;0;L;;;;;N;;;;;
+A940;REJANG LETTER WA;Lo;0;L;;;;;N;;;;;
+A941;REJANG LETTER HA;Lo;0;L;;;;;N;;;;;
+A942;REJANG LETTER MBA;Lo;0;L;;;;;N;;;;;
+A943;REJANG LETTER NGGA;Lo;0;L;;;;;N;;;;;
+A944;REJANG LETTER NDA;Lo;0;L;;;;;N;;;;;
+A945;REJANG LETTER NYJA;Lo;0;L;;;;;N;;;;;
+A946;REJANG LETTER A;Lo;0;L;;;;;N;;;;;
+A947;REJANG VOWEL SIGN I;Mn;0;NSM;;;;;N;;;;;
+A948;REJANG VOWEL SIGN U;Mn;0;NSM;;;;;N;;;;;
+A949;REJANG VOWEL SIGN E;Mn;0;NSM;;;;;N;;;;;
+A94A;REJANG VOWEL SIGN AI;Mn;0;NSM;;;;;N;;;;;
+A94B;REJANG VOWEL SIGN O;Mn;0;NSM;;;;;N;;;;;
+A94C;REJANG VOWEL SIGN AU;Mn;0;NSM;;;;;N;;;;;
+A94D;REJANG VOWEL SIGN EU;Mn;0;NSM;;;;;N;;;;;
+A94E;REJANG VOWEL SIGN EA;Mn;0;NSM;;;;;N;;;;;
+A94F;REJANG CONSONANT SIGN NG;Mn;0;NSM;;;;;N;;;;;
+A950;REJANG CONSONANT SIGN N;Mn;0;NSM;;;;;N;;;;;
+A951;REJANG CONSONANT SIGN R;Mn;0;NSM;;;;;N;;;;;
+A952;REJANG CONSONANT SIGN H;Mc;0;L;;;;;N;;;;;
+A953;REJANG VIRAMA;Mc;9;L;;;;;N;;;;;
+A95F;REJANG SECTION MARK;Po;0;L;;;;;N;;;;;
+A960;HANGUL CHOSEONG TIKEUT-MIEUM;Lo;0;L;;;;;N;;;;;
+A961;HANGUL CHOSEONG TIKEUT-PIEUP;Lo;0;L;;;;;N;;;;;
+A962;HANGUL CHOSEONG TIKEUT-SIOS;Lo;0;L;;;;;N;;;;;
+A963;HANGUL CHOSEONG TIKEUT-CIEUC;Lo;0;L;;;;;N;;;;;
+A964;HANGUL CHOSEONG RIEUL-KIYEOK;Lo;0;L;;;;;N;;;;;
+A965;HANGUL CHOSEONG RIEUL-SSANGKIYEOK;Lo;0;L;;;;;N;;;;;
+A966;HANGUL CHOSEONG RIEUL-TIKEUT;Lo;0;L;;;;;N;;;;;
+A967;HANGUL CHOSEONG RIEUL-SSANGTIKEUT;Lo;0;L;;;;;N;;;;;
+A968;HANGUL CHOSEONG RIEUL-MIEUM;Lo;0;L;;;;;N;;;;;
+A969;HANGUL CHOSEONG RIEUL-PIEUP;Lo;0;L;;;;;N;;;;;
+A96A;HANGUL CHOSEONG RIEUL-SSANGPIEUP;Lo;0;L;;;;;N;;;;;
+A96B;HANGUL CHOSEONG RIEUL-KAPYEOUNPIEUP;Lo;0;L;;;;;N;;;;;
+A96C;HANGUL CHOSEONG RIEUL-SIOS;Lo;0;L;;;;;N;;;;;
+A96D;HANGUL CHOSEONG RIEUL-CIEUC;Lo;0;L;;;;;N;;;;;
+A96E;HANGUL CHOSEONG RIEUL-KHIEUKH;Lo;0;L;;;;;N;;;;;
+A96F;HANGUL CHOSEONG MIEUM-KIYEOK;Lo;0;L;;;;;N;;;;;
+A970;HANGUL CHOSEONG MIEUM-TIKEUT;Lo;0;L;;;;;N;;;;;
+A971;HANGUL CHOSEONG MIEUM-SIOS;Lo;0;L;;;;;N;;;;;
+A972;HANGUL CHOSEONG PIEUP-SIOS-THIEUTH;Lo;0;L;;;;;N;;;;;
+A973;HANGUL CHOSEONG PIEUP-KHIEUKH;Lo;0;L;;;;;N;;;;;
+A974;HANGUL CHOSEONG PIEUP-HIEUH;Lo;0;L;;;;;N;;;;;
+A975;HANGUL CHOSEONG SSANGSIOS-PIEUP;Lo;0;L;;;;;N;;;;;
+A976;HANGUL CHOSEONG IEUNG-RIEUL;Lo;0;L;;;;;N;;;;;
+A977;HANGUL CHOSEONG IEUNG-HIEUH;Lo;0;L;;;;;N;;;;;
+A978;HANGUL CHOSEONG SSANGCIEUC-HIEUH;Lo;0;L;;;;;N;;;;;
+A979;HANGUL CHOSEONG SSANGTHIEUTH;Lo;0;L;;;;;N;;;;;
+A97A;HANGUL CHOSEONG PHIEUPH-HIEUH;Lo;0;L;;;;;N;;;;;
+A97B;HANGUL CHOSEONG HIEUH-SIOS;Lo;0;L;;;;;N;;;;;
+A97C;HANGUL CHOSEONG SSANGYEORINHIEUH;Lo;0;L;;;;;N;;;;;
+A980;JAVANESE SIGN PANYANGGA;Mn;0;NSM;;;;;N;;;;;
+A981;JAVANESE SIGN CECAK;Mn;0;NSM;;;;;N;;;;;
+A982;JAVANESE SIGN LAYAR;Mn;0;NSM;;;;;N;;;;;
+A983;JAVANESE SIGN WIGNYAN;Mc;0;L;;;;;N;;;;;
+A984;JAVANESE LETTER A;Lo;0;L;;;;;N;;;;;
+A985;JAVANESE LETTER I KAWI;Lo;0;L;;;;;N;;;;;
+A986;JAVANESE LETTER I;Lo;0;L;;;;;N;;;;;
+A987;JAVANESE LETTER II;Lo;0;L;;;;;N;;;;;
+A988;JAVANESE LETTER U;Lo;0;L;;;;;N;;;;;
+A989;JAVANESE LETTER PA CEREK;Lo;0;L;;;;;N;;;;;
+A98A;JAVANESE LETTER NGA LELET;Lo;0;L;;;;;N;;;;;
+A98B;JAVANESE LETTER NGA LELET RASWADI;Lo;0;L;;;;;N;;;;;
+A98C;JAVANESE LETTER E;Lo;0;L;;;;;N;;;;;
+A98D;JAVANESE LETTER AI;Lo;0;L;;;;;N;;;;;
+A98E;JAVANESE LETTER O;Lo;0;L;;;;;N;;;;;
+A98F;JAVANESE LETTER KA;Lo;0;L;;;;;N;;;;;
+A990;JAVANESE LETTER KA SASAK;Lo;0;L;;;;;N;;;;;
+A991;JAVANESE LETTER KA MURDA;Lo;0;L;;;;;N;;;;;
+A992;JAVANESE LETTER GA;Lo;0;L;;;;;N;;;;;
+A993;JAVANESE LETTER GA MURDA;Lo;0;L;;;;;N;;;;;
+A994;JAVANESE LETTER NGA;Lo;0;L;;;;;N;;;;;
+A995;JAVANESE LETTER CA;Lo;0;L;;;;;N;;;;;
+A996;JAVANESE LETTER CA MURDA;Lo;0;L;;;;;N;;;;;
+A997;JAVANESE LETTER JA;Lo;0;L;;;;;N;;;;;
+A998;JAVANESE LETTER NYA MURDA;Lo;0;L;;;;;N;;;;;
+A999;JAVANESE LETTER JA MAHAPRANA;Lo;0;L;;;;;N;;;;;
+A99A;JAVANESE LETTER NYA;Lo;0;L;;;;;N;;;;;
+A99B;JAVANESE LETTER TTA;Lo;0;L;;;;;N;;;;;
+A99C;JAVANESE LETTER TTA MAHAPRANA;Lo;0;L;;;;;N;;;;;
+A99D;JAVANESE LETTER DDA;Lo;0;L;;;;;N;;;;;
+A99E;JAVANESE LETTER DDA MAHAPRANA;Lo;0;L;;;;;N;;;;;
+A99F;JAVANESE LETTER NA MURDA;Lo;0;L;;;;;N;;;;;
+A9A0;JAVANESE LETTER TA;Lo;0;L;;;;;N;;;;;
+A9A1;JAVANESE LETTER TA MURDA;Lo;0;L;;;;;N;;;;;
+A9A2;JAVANESE LETTER DA;Lo;0;L;;;;;N;;;;;
+A9A3;JAVANESE LETTER DA MAHAPRANA;Lo;0;L;;;;;N;;;;;
+A9A4;JAVANESE LETTER NA;Lo;0;L;;;;;N;;;;;
+A9A5;JAVANESE LETTER PA;Lo;0;L;;;;;N;;;;;
+A9A6;JAVANESE LETTER PA MURDA;Lo;0;L;;;;;N;;;;;
+A9A7;JAVANESE LETTER BA;Lo;0;L;;;;;N;;;;;
+A9A8;JAVANESE LETTER BA MURDA;Lo;0;L;;;;;N;;;;;
+A9A9;JAVANESE LETTER MA;Lo;0;L;;;;;N;;;;;
+A9AA;JAVANESE LETTER YA;Lo;0;L;;;;;N;;;;;
+A9AB;JAVANESE LETTER RA;Lo;0;L;;;;;N;;;;;
+A9AC;JAVANESE LETTER RA AGUNG;Lo;0;L;;;;;N;;;;;
+A9AD;JAVANESE LETTER LA;Lo;0;L;;;;;N;;;;;
+A9AE;JAVANESE LETTER WA;Lo;0;L;;;;;N;;;;;
+A9AF;JAVANESE LETTER SA MURDA;Lo;0;L;;;;;N;;;;;
+A9B0;JAVANESE LETTER SA MAHAPRANA;Lo;0;L;;;;;N;;;;;
+A9B1;JAVANESE LETTER SA;Lo;0;L;;;;;N;;;;;
+A9B2;JAVANESE LETTER HA;Lo;0;L;;;;;N;;;;;
+A9B3;JAVANESE SIGN CECAK TELU;Mn;7;NSM;;;;;N;;;;;
+A9B4;JAVANESE VOWEL SIGN TARUNG;Mc;0;L;;;;;N;;;;;
+A9B5;JAVANESE VOWEL SIGN TOLONG;Mc;0;L;;;;;N;;;;;
+A9B6;JAVANESE VOWEL SIGN WULU;Mn;0;NSM;;;;;N;;;;;
+A9B7;JAVANESE VOWEL SIGN WULU MELIK;Mn;0;NSM;;;;;N;;;;;
+A9B8;JAVANESE VOWEL SIGN SUKU;Mn;0;NSM;;;;;N;;;;;
+A9B9;JAVANESE VOWEL SIGN SUKU MENDUT;Mn;0;NSM;;;;;N;;;;;
+A9BA;JAVANESE VOWEL SIGN TALING;Mc;0;L;;;;;N;;;;;
+A9BB;JAVANESE VOWEL SIGN DIRGA MURE;Mc;0;L;;;;;N;;;;;
+A9BC;JAVANESE VOWEL SIGN PEPET;Mn;0;NSM;;;;;N;;;;;
+A9BD;JAVANESE CONSONANT SIGN KERET;Mc;0;L;;;;;N;;;;;
+A9BE;JAVANESE CONSONANT SIGN PENGKAL;Mc;0;L;;;;;N;;;;;
+A9BF;JAVANESE CONSONANT SIGN CAKRA;Mc;0;L;;;;;N;;;;;
+A9C0;JAVANESE PANGKON;Mc;9;L;;;;;N;;;;;
+A9C1;JAVANESE LEFT RERENGGAN;Po;0;L;;;;;N;;;;;
+A9C2;JAVANESE RIGHT RERENGGAN;Po;0;L;;;;;N;;;;;
+A9C3;JAVANESE PADA ANDAP;Po;0;L;;;;;N;;;;;
+A9C4;JAVANESE PADA MADYA;Po;0;L;;;;;N;;;;;
+A9C5;JAVANESE PADA LUHUR;Po;0;L;;;;;N;;;;;
+A9C6;JAVANESE PADA WINDU;Po;0;L;;;;;N;;;;;
+A9C7;JAVANESE PADA PANGKAT;Po;0;L;;;;;N;;;;;
+A9C8;JAVANESE PADA LINGSA;Po;0;L;;;;;N;;;;;
+A9C9;JAVANESE PADA LUNGSI;Po;0;L;;;;;N;;;;;
+A9CA;JAVANESE PADA ADEG;Po;0;L;;;;;N;;;;;
+A9CB;JAVANESE PADA ADEG ADEG;Po;0;L;;;;;N;;;;;
+A9CC;JAVANESE PADA PISELEH;Po;0;L;;;;;N;;;;;
+A9CD;JAVANESE TURNED PADA PISELEH;Po;0;L;;;;;N;;;;;
+A9CF;JAVANESE PANGRANGKEP;Lm;0;L;;;;;N;;;;;
+A9D0;JAVANESE DIGIT ZERO;Nd;0;L;;0;0;0;N;;;;;
+A9D1;JAVANESE DIGIT ONE;Nd;0;L;;1;1;1;N;;;;;
+A9D2;JAVANESE DIGIT TWO;Nd;0;L;;2;2;2;N;;;;;
+A9D3;JAVANESE DIGIT THREE;Nd;0;L;;3;3;3;N;;;;;
+A9D4;JAVANESE DIGIT FOUR;Nd;0;L;;4;4;4;N;;;;;
+A9D5;JAVANESE DIGIT FIVE;Nd;0;L;;5;5;5;N;;;;;
+A9D6;JAVANESE DIGIT SIX;Nd;0;L;;6;6;6;N;;;;;
+A9D7;JAVANESE DIGIT SEVEN;Nd;0;L;;7;7;7;N;;;;;
+A9D8;JAVANESE DIGIT EIGHT;Nd;0;L;;8;8;8;N;;;;;
+A9D9;JAVANESE DIGIT NINE;Nd;0;L;;9;9;9;N;;;;;
+A9DE;JAVANESE PADA TIRTA TUMETES;Po;0;L;;;;;N;;;;;
+A9DF;JAVANESE PADA ISEN-ISEN;Po;0;L;;;;;N;;;;;
+A9E0;MYANMAR LETTER SHAN GHA;Lo;0;L;;;;;N;;;;;
+A9E1;MYANMAR LETTER SHAN CHA;Lo;0;L;;;;;N;;;;;
+A9E2;MYANMAR LETTER SHAN JHA;Lo;0;L;;;;;N;;;;;
+A9E3;MYANMAR LETTER SHAN NNA;Lo;0;L;;;;;N;;;;;
+A9E4;MYANMAR LETTER SHAN BHA;Lo;0;L;;;;;N;;;;;
+A9E5;MYANMAR SIGN SHAN SAW;Mn;0;NSM;;;;;N;;;;;
+A9E6;MYANMAR MODIFIER LETTER SHAN REDUPLICATION;Lm;0;L;;;;;N;;;;;
+A9E7;MYANMAR LETTER TAI LAING NYA;Lo;0;L;;;;;N;;;;;
+A9E8;MYANMAR LETTER TAI LAING FA;Lo;0;L;;;;;N;;;;;
+A9E9;MYANMAR LETTER TAI LAING GA;Lo;0;L;;;;;N;;;;;
+A9EA;MYANMAR LETTER TAI LAING GHA;Lo;0;L;;;;;N;;;;;
+A9EB;MYANMAR LETTER TAI LAING JA;Lo;0;L;;;;;N;;;;;
+A9EC;MYANMAR LETTER TAI LAING JHA;Lo;0;L;;;;;N;;;;;
+A9ED;MYANMAR LETTER TAI LAING DDA;Lo;0;L;;;;;N;;;;;
+A9EE;MYANMAR LETTER TAI LAING DDHA;Lo;0;L;;;;;N;;;;;
+A9EF;MYANMAR LETTER TAI LAING NNA;Lo;0;L;;;;;N;;;;;
+A9F0;MYANMAR TAI LAING DIGIT ZERO;Nd;0;L;;0;0;0;N;;;;;
+A9F1;MYANMAR TAI LAING DIGIT ONE;Nd;0;L;;1;1;1;N;;;;;
+A9F2;MYANMAR TAI LAING DIGIT TWO;Nd;0;L;;2;2;2;N;;;;;
+A9F3;MYANMAR TAI LAING DIGIT THREE;Nd;0;L;;3;3;3;N;;;;;
+A9F4;MYANMAR TAI LAING DIGIT FOUR;Nd;0;L;;4;4;4;N;;;;;
+A9F5;MYANMAR TAI LAING DIGIT FIVE;Nd;0;L;;5;5;5;N;;;;;
+A9F6;MYANMAR TAI LAING DIGIT SIX;Nd;0;L;;6;6;6;N;;;;;
+A9F7;MYANMAR TAI LAING DIGIT SEVEN;Nd;0;L;;7;7;7;N;;;;;
+A9F8;MYANMAR TAI LAING DIGIT EIGHT;Nd;0;L;;8;8;8;N;;;;;
+A9F9;MYANMAR TAI LAING DIGIT NINE;Nd;0;L;;9;9;9;N;;;;;
+A9FA;MYANMAR LETTER TAI LAING LLA;Lo;0;L;;;;;N;;;;;
+A9FB;MYANMAR LETTER TAI LAING DA;Lo;0;L;;;;;N;;;;;
+A9FC;MYANMAR LETTER TAI LAING DHA;Lo;0;L;;;;;N;;;;;
+A9FD;MYANMAR LETTER TAI LAING BA;Lo;0;L;;;;;N;;;;;
+A9FE;MYANMAR LETTER TAI LAING BHA;Lo;0;L;;;;;N;;;;;
+AA00;CHAM LETTER A;Lo;0;L;;;;;N;;;;;
+AA01;CHAM LETTER I;Lo;0;L;;;;;N;;;;;
+AA02;CHAM LETTER U;Lo;0;L;;;;;N;;;;;
+AA03;CHAM LETTER E;Lo;0;L;;;;;N;;;;;
+AA04;CHAM LETTER AI;Lo;0;L;;;;;N;;;;;
+AA05;CHAM LETTER O;Lo;0;L;;;;;N;;;;;
+AA06;CHAM LETTER KA;Lo;0;L;;;;;N;;;;;
+AA07;CHAM LETTER KHA;Lo;0;L;;;;;N;;;;;
+AA08;CHAM LETTER GA;Lo;0;L;;;;;N;;;;;
+AA09;CHAM LETTER GHA;Lo;0;L;;;;;N;;;;;
+AA0A;CHAM LETTER NGUE;Lo;0;L;;;;;N;;;;;
+AA0B;CHAM LETTER NGA;Lo;0;L;;;;;N;;;;;
+AA0C;CHAM LETTER CHA;Lo;0;L;;;;;N;;;;;
+AA0D;CHAM LETTER CHHA;Lo;0;L;;;;;N;;;;;
+AA0E;CHAM LETTER JA;Lo;0;L;;;;;N;;;;;
+AA0F;CHAM LETTER JHA;Lo;0;L;;;;;N;;;;;
+AA10;CHAM LETTER NHUE;Lo;0;L;;;;;N;;;;;
+AA11;CHAM LETTER NHA;Lo;0;L;;;;;N;;;;;
+AA12;CHAM LETTER NHJA;Lo;0;L;;;;;N;;;;;
+AA13;CHAM LETTER TA;Lo;0;L;;;;;N;;;;;
+AA14;CHAM LETTER THA;Lo;0;L;;;;;N;;;;;
+AA15;CHAM LETTER DA;Lo;0;L;;;;;N;;;;;
+AA16;CHAM LETTER DHA;Lo;0;L;;;;;N;;;;;
+AA17;CHAM LETTER NUE;Lo;0;L;;;;;N;;;;;
+AA18;CHAM LETTER NA;Lo;0;L;;;;;N;;;;;
+AA19;CHAM LETTER DDA;Lo;0;L;;;;;N;;;;;
+AA1A;CHAM LETTER PA;Lo;0;L;;;;;N;;;;;
+AA1B;CHAM LETTER PPA;Lo;0;L;;;;;N;;;;;
+AA1C;CHAM LETTER PHA;Lo;0;L;;;;;N;;;;;
+AA1D;CHAM LETTER BA;Lo;0;L;;;;;N;;;;;
+AA1E;CHAM LETTER BHA;Lo;0;L;;;;;N;;;;;
+AA1F;CHAM LETTER MUE;Lo;0;L;;;;;N;;;;;
+AA20;CHAM LETTER MA;Lo;0;L;;;;;N;;;;;
+AA21;CHAM LETTER BBA;Lo;0;L;;;;;N;;;;;
+AA22;CHAM LETTER YA;Lo;0;L;;;;;N;;;;;
+AA23;CHAM LETTER RA;Lo;0;L;;;;;N;;;;;
+AA24;CHAM LETTER LA;Lo;0;L;;;;;N;;;;;
+AA25;CHAM LETTER VA;Lo;0;L;;;;;N;;;;;
+AA26;CHAM LETTER SSA;Lo;0;L;;;;;N;;;;;
+AA27;CHAM LETTER SA;Lo;0;L;;;;;N;;;;;
+AA28;CHAM LETTER HA;Lo;0;L;;;;;N;;;;;
+AA29;CHAM VOWEL SIGN AA;Mn;0;NSM;;;;;N;;;;;
+AA2A;CHAM VOWEL SIGN I;Mn;0;NSM;;;;;N;;;;;
+AA2B;CHAM VOWEL SIGN II;Mn;0;NSM;;;;;N;;;;;
+AA2C;CHAM VOWEL SIGN EI;Mn;0;NSM;;;;;N;;;;;
+AA2D;CHAM VOWEL SIGN U;Mn;0;NSM;;;;;N;;;;;
+AA2E;CHAM VOWEL SIGN OE;Mn;0;NSM;;;;;N;;;;;
+AA2F;CHAM VOWEL SIGN O;Mc;0;L;;;;;N;;;;;
+AA30;CHAM VOWEL SIGN AI;Mc;0;L;;;;;N;;;;;
+AA31;CHAM VOWEL SIGN AU;Mn;0;NSM;;;;;N;;;;;
+AA32;CHAM VOWEL SIGN UE;Mn;0;NSM;;;;;N;;;;;
+AA33;CHAM CONSONANT SIGN YA;Mc;0;L;;;;;N;;;;;
+AA34;CHAM CONSONANT SIGN RA;Mc;0;L;;;;;N;;;;;
+AA35;CHAM CONSONANT SIGN LA;Mn;0;NSM;;;;;N;;;;;
+AA36;CHAM CONSONANT SIGN WA;Mn;0;NSM;;;;;N;;;;;
+AA40;CHAM LETTER FINAL K;Lo;0;L;;;;;N;;;;;
+AA41;CHAM LETTER FINAL G;Lo;0;L;;;;;N;;;;;
+AA42;CHAM LETTER FINAL NG;Lo;0;L;;;;;N;;;;;
+AA43;CHAM CONSONANT SIGN FINAL NG;Mn;0;NSM;;;;;N;;;;;
+AA44;CHAM LETTER FINAL CH;Lo;0;L;;;;;N;;;;;
+AA45;CHAM LETTER FINAL T;Lo;0;L;;;;;N;;;;;
+AA46;CHAM LETTER FINAL N;Lo;0;L;;;;;N;;;;;
+AA47;CHAM LETTER FINAL P;Lo;0;L;;;;;N;;;;;
+AA48;CHAM LETTER FINAL Y;Lo;0;L;;;;;N;;;;;
+AA49;CHAM LETTER FINAL R;Lo;0;L;;;;;N;;;;;
+AA4A;CHAM LETTER FINAL L;Lo;0;L;;;;;N;;;;;
+AA4B;CHAM LETTER FINAL SS;Lo;0;L;;;;;N;;;;;
+AA4C;CHAM CONSONANT SIGN FINAL M;Mn;0;NSM;;;;;N;;;;;
+AA4D;CHAM CONSONANT SIGN FINAL H;Mc;0;L;;;;;N;;;;;
+AA50;CHAM DIGIT ZERO;Nd;0;L;;0;0;0;N;;;;;
+AA51;CHAM DIGIT ONE;Nd;0;L;;1;1;1;N;;;;;
+AA52;CHAM DIGIT TWO;Nd;0;L;;2;2;2;N;;;;;
+AA53;CHAM DIGIT THREE;Nd;0;L;;3;3;3;N;;;;;
+AA54;CHAM DIGIT FOUR;Nd;0;L;;4;4;4;N;;;;;
+AA55;CHAM DIGIT FIVE;Nd;0;L;;5;5;5;N;;;;;
+AA56;CHAM DIGIT SIX;Nd;0;L;;6;6;6;N;;;;;
+AA57;CHAM DIGIT SEVEN;Nd;0;L;;7;7;7;N;;;;;
+AA58;CHAM DIGIT EIGHT;Nd;0;L;;8;8;8;N;;;;;
+AA59;CHAM DIGIT NINE;Nd;0;L;;9;9;9;N;;;;;
+AA5C;CHAM PUNCTUATION SPIRAL;Po;0;L;;;;;N;;;;;
+AA5D;CHAM PUNCTUATION DANDA;Po;0;L;;;;;N;;;;;
+AA5E;CHAM PUNCTUATION DOUBLE DANDA;Po;0;L;;;;;N;;;;;
+AA5F;CHAM PUNCTUATION TRIPLE DANDA;Po;0;L;;;;;N;;;;;
+AA60;MYANMAR LETTER KHAMTI GA;Lo;0;L;;;;;N;;;;;
+AA61;MYANMAR LETTER KHAMTI CA;Lo;0;L;;;;;N;;;;;
+AA62;MYANMAR LETTER KHAMTI CHA;Lo;0;L;;;;;N;;;;;
+AA63;MYANMAR LETTER KHAMTI JA;Lo;0;L;;;;;N;;;;;
+AA64;MYANMAR LETTER KHAMTI JHA;Lo;0;L;;;;;N;;;;;
+AA65;MYANMAR LETTER KHAMTI NYA;Lo;0;L;;;;;N;;;;;
+AA66;MYANMAR LETTER KHAMTI TTA;Lo;0;L;;;;;N;;;;;
+AA67;MYANMAR LETTER KHAMTI TTHA;Lo;0;L;;;;;N;;;;;
+AA68;MYANMAR LETTER KHAMTI DDA;Lo;0;L;;;;;N;;;;;
+AA69;MYANMAR LETTER KHAMTI DDHA;Lo;0;L;;;;;N;;;;;
+AA6A;MYANMAR LETTER KHAMTI DHA;Lo;0;L;;;;;N;;;;;
+AA6B;MYANMAR LETTER KHAMTI NA;Lo;0;L;;;;;N;;;;;
+AA6C;MYANMAR LETTER KHAMTI SA;Lo;0;L;;;;;N;;;;;
+AA6D;MYANMAR LETTER KHAMTI HA;Lo;0;L;;;;;N;;;;;
+AA6E;MYANMAR LETTER KHAMTI HHA;Lo;0;L;;;;;N;;;;;
+AA6F;MYANMAR LETTER KHAMTI FA;Lo;0;L;;;;;N;;;;;
+AA70;MYANMAR MODIFIER LETTER KHAMTI REDUPLICATION;Lm;0;L;;;;;N;;;;;
+AA71;MYANMAR LETTER KHAMTI XA;Lo;0;L;;;;;N;;;;;
+AA72;MYANMAR LETTER KHAMTI ZA;Lo;0;L;;;;;N;;;;;
+AA73;MYANMAR LETTER KHAMTI RA;Lo;0;L;;;;;N;;;;;
+AA74;MYANMAR LOGOGRAM KHAMTI OAY;Lo;0;L;;;;;N;;;;;
+AA75;MYANMAR LOGOGRAM KHAMTI QN;Lo;0;L;;;;;N;;;;;
+AA76;MYANMAR LOGOGRAM KHAMTI HM;Lo;0;L;;;;;N;;;;;
+AA77;MYANMAR SYMBOL AITON EXCLAMATION;So;0;L;;;;;N;;;;;
+AA78;MYANMAR SYMBOL AITON ONE;So;0;L;;;;;N;;;;;
+AA79;MYANMAR SYMBOL AITON TWO;So;0;L;;;;;N;;;;;
+AA7A;MYANMAR LETTER AITON RA;Lo;0;L;;;;;N;;;;;
+AA7B;MYANMAR SIGN PAO KAREN TONE;Mc;0;L;;;;;N;;;;;
+AA7C;MYANMAR SIGN TAI LAING TONE-2;Mn;0;NSM;;;;;N;;;;;
+AA7D;MYANMAR SIGN TAI LAING TONE-5;Mc;0;L;;;;;N;;;;;
+AA7E;MYANMAR LETTER SHWE PALAUNG CHA;Lo;0;L;;;;;N;;;;;
+AA7F;MYANMAR LETTER SHWE PALAUNG SHA;Lo;0;L;;;;;N;;;;;
+AA80;TAI VIET LETTER LOW KO;Lo;0;L;;;;;N;;;;;
+AA81;TAI VIET LETTER HIGH KO;Lo;0;L;;;;;N;;;;;
+AA82;TAI VIET LETTER LOW KHO;Lo;0;L;;;;;N;;;;;
+AA83;TAI VIET LETTER HIGH KHO;Lo;0;L;;;;;N;;;;;
+AA84;TAI VIET LETTER LOW KHHO;Lo;0;L;;;;;N;;;;;
+AA85;TAI VIET LETTER HIGH KHHO;Lo;0;L;;;;;N;;;;;
+AA86;TAI VIET LETTER LOW GO;Lo;0;L;;;;;N;;;;;
+AA87;TAI VIET LETTER HIGH GO;Lo;0;L;;;;;N;;;;;
+AA88;TAI VIET LETTER LOW NGO;Lo;0;L;;;;;N;;;;;
+AA89;TAI VIET LETTER HIGH NGO;Lo;0;L;;;;;N;;;;;
+AA8A;TAI VIET LETTER LOW CO;Lo;0;L;;;;;N;;;;;
+AA8B;TAI VIET LETTER HIGH CO;Lo;0;L;;;;;N;;;;;
+AA8C;TAI VIET LETTER LOW CHO;Lo;0;L;;;;;N;;;;;
+AA8D;TAI VIET LETTER HIGH CHO;Lo;0;L;;;;;N;;;;;
+AA8E;TAI VIET LETTER LOW SO;Lo;0;L;;;;;N;;;;;
+AA8F;TAI VIET LETTER HIGH SO;Lo;0;L;;;;;N;;;;;
+AA90;TAI VIET LETTER LOW NYO;Lo;0;L;;;;;N;;;;;
+AA91;TAI VIET LETTER HIGH NYO;Lo;0;L;;;;;N;;;;;
+AA92;TAI VIET LETTER LOW DO;Lo;0;L;;;;;N;;;;;
+AA93;TAI VIET LETTER HIGH DO;Lo;0;L;;;;;N;;;;;
+AA94;TAI VIET LETTER LOW TO;Lo;0;L;;;;;N;;;;;
+AA95;TAI VIET LETTER HIGH TO;Lo;0;L;;;;;N;;;;;
+AA96;TAI VIET LETTER LOW THO;Lo;0;L;;;;;N;;;;;
+AA97;TAI VIET LETTER HIGH THO;Lo;0;L;;;;;N;;;;;
+AA98;TAI VIET LETTER LOW NO;Lo;0;L;;;;;N;;;;;
+AA99;TAI VIET LETTER HIGH NO;Lo;0;L;;;;;N;;;;;
+AA9A;TAI VIET LETTER LOW BO;Lo;0;L;;;;;N;;;;;
+AA9B;TAI VIET LETTER HIGH BO;Lo;0;L;;;;;N;;;;;
+AA9C;TAI VIET LETTER LOW PO;Lo;0;L;;;;;N;;;;;
+AA9D;TAI VIET LETTER HIGH PO;Lo;0;L;;;;;N;;;;;
+AA9E;TAI VIET LETTER LOW PHO;Lo;0;L;;;;;N;;;;;
+AA9F;TAI VIET LETTER HIGH PHO;Lo;0;L;;;;;N;;;;;
+AAA0;TAI VIET LETTER LOW FO;Lo;0;L;;;;;N;;;;;
+AAA1;TAI VIET LETTER HIGH FO;Lo;0;L;;;;;N;;;;;
+AAA2;TAI VIET LETTER LOW MO;Lo;0;L;;;;;N;;;;;
+AAA3;TAI VIET LETTER HIGH MO;Lo;0;L;;;;;N;;;;;
+AAA4;TAI VIET LETTER LOW YO;Lo;0;L;;;;;N;;;;;
+AAA5;TAI VIET LETTER HIGH YO;Lo;0;L;;;;;N;;;;;
+AAA6;TAI VIET LETTER LOW RO;Lo;0;L;;;;;N;;;;;
+AAA7;TAI VIET LETTER HIGH RO;Lo;0;L;;;;;N;;;;;
+AAA8;TAI VIET LETTER LOW LO;Lo;0;L;;;;;N;;;;;
+AAA9;TAI VIET LETTER HIGH LO;Lo;0;L;;;;;N;;;;;
+AAAA;TAI VIET LETTER LOW VO;Lo;0;L;;;;;N;;;;;
+AAAB;TAI VIET LETTER HIGH VO;Lo;0;L;;;;;N;;;;;
+AAAC;TAI VIET LETTER LOW HO;Lo;0;L;;;;;N;;;;;
+AAAD;TAI VIET LETTER HIGH HO;Lo;0;L;;;;;N;;;;;
+AAAE;TAI VIET LETTER LOW O;Lo;0;L;;;;;N;;;;;
+AAAF;TAI VIET LETTER HIGH O;Lo;0;L;;;;;N;;;;;
+AAB0;TAI VIET MAI KANG;Mn;230;NSM;;;;;N;;;;;
+AAB1;TAI VIET VOWEL AA;Lo;0;L;;;;;N;;;;;
+AAB2;TAI VIET VOWEL I;Mn;230;NSM;;;;;N;;;;;
+AAB3;TAI VIET VOWEL UE;Mn;230;NSM;;;;;N;;;;;
+AAB4;TAI VIET VOWEL U;Mn;220;NSM;;;;;N;;;;;
+AAB5;TAI VIET VOWEL E;Lo;0;L;;;;;N;;;;;
+AAB6;TAI VIET VOWEL O;Lo;0;L;;;;;N;;;;;
+AAB7;TAI VIET MAI KHIT;Mn;230;NSM;;;;;N;;;;;
+AAB8;TAI VIET VOWEL IA;Mn;230;NSM;;;;;N;;;;;
+AAB9;TAI VIET VOWEL UEA;Lo;0;L;;;;;N;;;;;
+AABA;TAI VIET VOWEL UA;Lo;0;L;;;;;N;;;;;
+AABB;TAI VIET VOWEL AUE;Lo;0;L;;;;;N;;;;;
+AABC;TAI VIET VOWEL AY;Lo;0;L;;;;;N;;;;;
+AABD;TAI VIET VOWEL AN;Lo;0;L;;;;;N;;;;;
+AABE;TAI VIET VOWEL AM;Mn;230;NSM;;;;;N;;;;;
+AABF;TAI VIET TONE MAI EK;Mn;230;NSM;;;;;N;;;;;
+AAC0;TAI VIET TONE MAI NUENG;Lo;0;L;;;;;N;;;;;
+AAC1;TAI VIET TONE MAI THO;Mn;230;NSM;;;;;N;;;;;
+AAC2;TAI VIET TONE MAI SONG;Lo;0;L;;;;;N;;;;;
+AADB;TAI VIET SYMBOL KON;Lo;0;L;;;;;N;;;;;
+AADC;TAI VIET SYMBOL NUENG;Lo;0;L;;;;;N;;;;;
+AADD;TAI VIET SYMBOL SAM;Lm;0;L;;;;;N;;;;;
+AADE;TAI VIET SYMBOL HO HOI;Po;0;L;;;;;N;;;;;
+AADF;TAI VIET SYMBOL KOI KOI;Po;0;L;;;;;N;;;;;
+AAE0;MEETEI MAYEK LETTER E;Lo;0;L;;;;;N;;;;;
+AAE1;MEETEI MAYEK LETTER O;Lo;0;L;;;;;N;;;;;
+AAE2;MEETEI MAYEK LETTER CHA;Lo;0;L;;;;;N;;;;;
+AAE3;MEETEI MAYEK LETTER NYA;Lo;0;L;;;;;N;;;;;
+AAE4;MEETEI MAYEK LETTER TTA;Lo;0;L;;;;;N;;;;;
+AAE5;MEETEI MAYEK LETTER TTHA;Lo;0;L;;;;;N;;;;;
+AAE6;MEETEI MAYEK LETTER DDA;Lo;0;L;;;;;N;;;;;
+AAE7;MEETEI MAYEK LETTER DDHA;Lo;0;L;;;;;N;;;;;
+AAE8;MEETEI MAYEK LETTER NNA;Lo;0;L;;;;;N;;;;;
+AAE9;MEETEI MAYEK LETTER SHA;Lo;0;L;;;;;N;;;;;
+AAEA;MEETEI MAYEK LETTER SSA;Lo;0;L;;;;;N;;;;;
+AAEB;MEETEI MAYEK VOWEL SIGN II;Mc;0;L;;;;;N;;;;;
+AAEC;MEETEI MAYEK VOWEL SIGN UU;Mn;0;NSM;;;;;N;;;;;
+AAED;MEETEI MAYEK VOWEL SIGN AAI;Mn;0;NSM;;;;;N;;;;;
+AAEE;MEETEI MAYEK VOWEL SIGN AU;Mc;0;L;;;;;N;;;;;
+AAEF;MEETEI MAYEK VOWEL SIGN AAU;Mc;0;L;;;;;N;;;;;
+AAF0;MEETEI MAYEK CHEIKHAN;Po;0;L;;;;;N;;;;;
+AAF1;MEETEI MAYEK AHANG KHUDAM;Po;0;L;;;;;N;;;;;
+AAF2;MEETEI MAYEK ANJI;Lo;0;L;;;;;N;;;;;
+AAF3;MEETEI MAYEK SYLLABLE REPETITION MARK;Lm;0;L;;;;;N;;;;;
+AAF4;MEETEI MAYEK WORD REPETITION MARK;Lm;0;L;;;;;N;;;;;
+AAF5;MEETEI MAYEK VOWEL SIGN VISARGA;Mc;0;L;;;;;N;;;;;
+AAF6;MEETEI MAYEK VIRAMA;Mn;9;NSM;;;;;N;;;;;
+AB01;ETHIOPIC SYLLABLE TTHU;Lo;0;L;;;;;N;;;;;
+AB02;ETHIOPIC SYLLABLE TTHI;Lo;0;L;;;;;N;;;;;
+AB03;ETHIOPIC SYLLABLE TTHAA;Lo;0;L;;;;;N;;;;;
+AB04;ETHIOPIC SYLLABLE TTHEE;Lo;0;L;;;;;N;;;;;
+AB05;ETHIOPIC SYLLABLE TTHE;Lo;0;L;;;;;N;;;;;
+AB06;ETHIOPIC SYLLABLE TTHO;Lo;0;L;;;;;N;;;;;
+AB09;ETHIOPIC SYLLABLE DDHU;Lo;0;L;;;;;N;;;;;
+AB0A;ETHIOPIC SYLLABLE DDHI;Lo;0;L;;;;;N;;;;;
+AB0B;ETHIOPIC SYLLABLE DDHAA;Lo;0;L;;;;;N;;;;;
+AB0C;ETHIOPIC SYLLABLE DDHEE;Lo;0;L;;;;;N;;;;;
+AB0D;ETHIOPIC SYLLABLE DDHE;Lo;0;L;;;;;N;;;;;
+AB0E;ETHIOPIC SYLLABLE DDHO;Lo;0;L;;;;;N;;;;;
+AB11;ETHIOPIC SYLLABLE DZU;Lo;0;L;;;;;N;;;;;
+AB12;ETHIOPIC SYLLABLE DZI;Lo;0;L;;;;;N;;;;;
+AB13;ETHIOPIC SYLLABLE DZAA;Lo;0;L;;;;;N;;;;;
+AB14;ETHIOPIC SYLLABLE DZEE;Lo;0;L;;;;;N;;;;;
+AB15;ETHIOPIC SYLLABLE DZE;Lo;0;L;;;;;N;;;;;
+AB16;ETHIOPIC SYLLABLE DZO;Lo;0;L;;;;;N;;;;;
+AB20;ETHIOPIC SYLLABLE CCHHA;Lo;0;L;;;;;N;;;;;
+AB21;ETHIOPIC SYLLABLE CCHHU;Lo;0;L;;;;;N;;;;;
+AB22;ETHIOPIC SYLLABLE CCHHI;Lo;0;L;;;;;N;;;;;
+AB23;ETHIOPIC SYLLABLE CCHHAA;Lo;0;L;;;;;N;;;;;
+AB24;ETHIOPIC SYLLABLE CCHHEE;Lo;0;L;;;;;N;;;;;
+AB25;ETHIOPIC SYLLABLE CCHHE;Lo;0;L;;;;;N;;;;;
+AB26;ETHIOPIC SYLLABLE CCHHO;Lo;0;L;;;;;N;;;;;
+AB28;ETHIOPIC SYLLABLE BBA;Lo;0;L;;;;;N;;;;;
+AB29;ETHIOPIC SYLLABLE BBU;Lo;0;L;;;;;N;;;;;
+AB2A;ETHIOPIC SYLLABLE BBI;Lo;0;L;;;;;N;;;;;
+AB2B;ETHIOPIC SYLLABLE BBAA;Lo;0;L;;;;;N;;;;;
+AB2C;ETHIOPIC SYLLABLE BBEE;Lo;0;L;;;;;N;;;;;
+AB2D;ETHIOPIC SYLLABLE BBE;Lo;0;L;;;;;N;;;;;
+AB2E;ETHIOPIC SYLLABLE BBO;Lo;0;L;;;;;N;;;;;
+AB30;LATIN SMALL LETTER BARRED ALPHA;Ll;0;L;;;;;N;;;;;
+AB31;LATIN SMALL LETTER A REVERSED-SCHWA;Ll;0;L;;;;;N;;;;;
+AB32;LATIN SMALL LETTER BLACKLETTER E;Ll;0;L;;;;;N;;;;;
+AB33;LATIN SMALL LETTER BARRED E;Ll;0;L;;;;;N;;;;;
+AB34;LATIN SMALL LETTER E WITH FLOURISH;Ll;0;L;;;;;N;;;;;
+AB35;LATIN SMALL LETTER LENIS F;Ll;0;L;;;;;N;;;;;
+AB36;LATIN SMALL LETTER SCRIPT G WITH CROSSED-TAIL;Ll;0;L;;;;;N;;;;;
+AB37;LATIN SMALL LETTER L WITH INVERTED LAZY S;Ll;0;L;;;;;N;;;;;
+AB38;LATIN SMALL LETTER L WITH DOUBLE MIDDLE TILDE;Ll;0;L;;;;;N;;;;;
+AB39;LATIN SMALL LETTER L WITH MIDDLE RING;Ll;0;L;;;;;N;;;;;
+AB3A;LATIN SMALL LETTER M WITH CROSSED-TAIL;Ll;0;L;;;;;N;;;;;
+AB3B;LATIN SMALL LETTER N WITH CROSSED-TAIL;Ll;0;L;;;;;N;;;;;
+AB3C;LATIN SMALL LETTER ENG WITH CROSSED-TAIL;Ll;0;L;;;;;N;;;;;
+AB3D;LATIN SMALL LETTER BLACKLETTER O;Ll;0;L;;;;;N;;;;;
+AB3E;LATIN SMALL LETTER BLACKLETTER O WITH STROKE;Ll;0;L;;;;;N;;;;;
+AB3F;LATIN SMALL LETTER OPEN O WITH STROKE;Ll;0;L;;;;;N;;;;;
+AB40;LATIN SMALL LETTER INVERTED OE;Ll;0;L;;;;;N;;;;;
+AB41;LATIN SMALL LETTER TURNED OE WITH STROKE;Ll;0;L;;;;;N;;;;;
+AB42;LATIN SMALL LETTER TURNED OE WITH HORIZONTAL STROKE;Ll;0;L;;;;;N;;;;;
+AB43;LATIN SMALL LETTER TURNED O OPEN-O;Ll;0;L;;;;;N;;;;;
+AB44;LATIN SMALL LETTER TURNED O OPEN-O WITH STROKE;Ll;0;L;;;;;N;;;;;
+AB45;LATIN SMALL LETTER STIRRUP R;Ll;0;L;;;;;N;;;;;
+AB46;LATIN LETTER SMALL CAPITAL R WITH RIGHT LEG;Ll;0;L;;;;;N;;;;;
+AB47;LATIN SMALL LETTER R WITHOUT HANDLE;Ll;0;L;;;;;N;;;;;
+AB48;LATIN SMALL LETTER DOUBLE R;Ll;0;L;;;;;N;;;;;
+AB49;LATIN SMALL LETTER R WITH CROSSED-TAIL;Ll;0;L;;;;;N;;;;;
+AB4A;LATIN SMALL LETTER DOUBLE R WITH CROSSED-TAIL;Ll;0;L;;;;;N;;;;;
+AB4B;LATIN SMALL LETTER SCRIPT R;Ll;0;L;;;;;N;;;;;
+AB4C;LATIN SMALL LETTER SCRIPT R WITH RING;Ll;0;L;;;;;N;;;;;
+AB4D;LATIN SMALL LETTER BASELINE ESH;Ll;0;L;;;;;N;;;;;
+AB4E;LATIN SMALL LETTER U WITH SHORT RIGHT LEG;Ll;0;L;;;;;N;;;;;
+AB4F;LATIN SMALL LETTER U BAR WITH SHORT RIGHT LEG;Ll;0;L;;;;;N;;;;;
+AB50;LATIN SMALL LETTER UI;Ll;0;L;;;;;N;;;;;
+AB51;LATIN SMALL LETTER TURNED UI;Ll;0;L;;;;;N;;;;;
+AB52;LATIN SMALL LETTER U WITH LEFT HOOK;Ll;0;L;;;;;N;;;;;
+AB53;LATIN SMALL LETTER CHI;Ll;0;L;;;;;N;;;A7B3;;A7B3
+AB54;LATIN SMALL LETTER CHI WITH LOW RIGHT RING;Ll;0;L;;;;;N;;;;;
+AB55;LATIN SMALL LETTER CHI WITH LOW LEFT SERIF;Ll;0;L;;;;;N;;;;;
+AB56;LATIN SMALL LETTER X WITH LOW RIGHT RING;Ll;0;L;;;;;N;;;;;
+AB57;LATIN SMALL LETTER X WITH LONG LEFT LEG;Ll;0;L;;;;;N;;;;;
+AB58;LATIN SMALL LETTER X WITH LONG LEFT LEG AND LOW RIGHT RING;Ll;0;L;;;;;N;;;;;
+AB59;LATIN SMALL LETTER X WITH LONG LEFT LEG WITH SERIF;Ll;0;L;;;;;N;;;;;
+AB5A;LATIN SMALL LETTER Y WITH SHORT RIGHT LEG;Ll;0;L;;;;;N;;;;;
+AB5B;MODIFIER BREVE WITH INVERTED BREVE;Sk;0;L;;;;;N;;;;;
+AB5C;MODIFIER LETTER SMALL HENG;Lm;0;L;<super> A727;;;;N;;;;;
+AB5D;MODIFIER LETTER SMALL L WITH INVERTED LAZY S;Lm;0;L;<super> AB37;;;;N;;;;;
+AB5E;MODIFIER LETTER SMALL L WITH MIDDLE TILDE;Lm;0;L;<super> 026B;;;;N;;;;;
+AB5F;MODIFIER LETTER SMALL U WITH LEFT HOOK;Lm;0;L;<super> AB52;;;;N;;;;;
+AB60;LATIN SMALL LETTER SAKHA YAT;Ll;0;L;;;;;N;;;;;
+AB61;LATIN SMALL LETTER IOTIFIED E;Ll;0;L;;;;;N;;;;;
+AB62;LATIN SMALL LETTER OPEN OE;Ll;0;L;;;;;N;;;;;
+AB63;LATIN SMALL LETTER UO;Ll;0;L;;;;;N;;;;;
+AB64;LATIN SMALL LETTER INVERTED ALPHA;Ll;0;L;;;;;N;;;;;
+AB65;GREEK LETTER SMALL CAPITAL OMEGA;Ll;0;L;;;;;N;;;;;
+AB70;CHEROKEE SMALL LETTER A;Ll;0;L;;;;;N;;;13A0;;13A0
+AB71;CHEROKEE SMALL LETTER E;Ll;0;L;;;;;N;;;13A1;;13A1
+AB72;CHEROKEE SMALL LETTER I;Ll;0;L;;;;;N;;;13A2;;13A2
+AB73;CHEROKEE SMALL LETTER O;Ll;0;L;;;;;N;;;13A3;;13A3
+AB74;CHEROKEE SMALL LETTER U;Ll;0;L;;;;;N;;;13A4;;13A4
+AB75;CHEROKEE SMALL LETTER V;Ll;0;L;;;;;N;;;13A5;;13A5
+AB76;CHEROKEE SMALL LETTER GA;Ll;0;L;;;;;N;;;13A6;;13A6
+AB77;CHEROKEE SMALL LETTER KA;Ll;0;L;;;;;N;;;13A7;;13A7
+AB78;CHEROKEE SMALL LETTER GE;Ll;0;L;;;;;N;;;13A8;;13A8
+AB79;CHEROKEE SMALL LETTER GI;Ll;0;L;;;;;N;;;13A9;;13A9
+AB7A;CHEROKEE SMALL LETTER GO;Ll;0;L;;;;;N;;;13AA;;13AA
+AB7B;CHEROKEE SMALL LETTER GU;Ll;0;L;;;;;N;;;13AB;;13AB
+AB7C;CHEROKEE SMALL LETTER GV;Ll;0;L;;;;;N;;;13AC;;13AC
+AB7D;CHEROKEE SMALL LETTER HA;Ll;0;L;;;;;N;;;13AD;;13AD
+AB7E;CHEROKEE SMALL LETTER HE;Ll;0;L;;;;;N;;;13AE;;13AE
+AB7F;CHEROKEE SMALL LETTER HI;Ll;0;L;;;;;N;;;13AF;;13AF
+AB80;CHEROKEE SMALL LETTER HO;Ll;0;L;;;;;N;;;13B0;;13B0
+AB81;CHEROKEE SMALL LETTER HU;Ll;0;L;;;;;N;;;13B1;;13B1
+AB82;CHEROKEE SMALL LETTER HV;Ll;0;L;;;;;N;;;13B2;;13B2
+AB83;CHEROKEE SMALL LETTER LA;Ll;0;L;;;;;N;;;13B3;;13B3
+AB84;CHEROKEE SMALL LETTER LE;Ll;0;L;;;;;N;;;13B4;;13B4
+AB85;CHEROKEE SMALL LETTER LI;Ll;0;L;;;;;N;;;13B5;;13B5
+AB86;CHEROKEE SMALL LETTER LO;Ll;0;L;;;;;N;;;13B6;;13B6
+AB87;CHEROKEE SMALL LETTER LU;Ll;0;L;;;;;N;;;13B7;;13B7
+AB88;CHEROKEE SMALL LETTER LV;Ll;0;L;;;;;N;;;13B8;;13B8
+AB89;CHEROKEE SMALL LETTER MA;Ll;0;L;;;;;N;;;13B9;;13B9
+AB8A;CHEROKEE SMALL LETTER ME;Ll;0;L;;;;;N;;;13BA;;13BA
+AB8B;CHEROKEE SMALL LETTER MI;Ll;0;L;;;;;N;;;13BB;;13BB
+AB8C;CHEROKEE SMALL LETTER MO;Ll;0;L;;;;;N;;;13BC;;13BC
+AB8D;CHEROKEE SMALL LETTER MU;Ll;0;L;;;;;N;;;13BD;;13BD
+AB8E;CHEROKEE SMALL LETTER NA;Ll;0;L;;;;;N;;;13BE;;13BE
+AB8F;CHEROKEE SMALL LETTER HNA;Ll;0;L;;;;;N;;;13BF;;13BF
+AB90;CHEROKEE SMALL LETTER NAH;Ll;0;L;;;;;N;;;13C0;;13C0
+AB91;CHEROKEE SMALL LETTER NE;Ll;0;L;;;;;N;;;13C1;;13C1
+AB92;CHEROKEE SMALL LETTER NI;Ll;0;L;;;;;N;;;13C2;;13C2
+AB93;CHEROKEE SMALL LETTER NO;Ll;0;L;;;;;N;;;13C3;;13C3
+AB94;CHEROKEE SMALL LETTER NU;Ll;0;L;;;;;N;;;13C4;;13C4
+AB95;CHEROKEE SMALL LETTER NV;Ll;0;L;;;;;N;;;13C5;;13C5
+AB96;CHEROKEE SMALL LETTER QUA;Ll;0;L;;;;;N;;;13C6;;13C6
+AB97;CHEROKEE SMALL LETTER QUE;Ll;0;L;;;;;N;;;13C7;;13C7
+AB98;CHEROKEE SMALL LETTER QUI;Ll;0;L;;;;;N;;;13C8;;13C8
+AB99;CHEROKEE SMALL LETTER QUO;Ll;0;L;;;;;N;;;13C9;;13C9
+AB9A;CHEROKEE SMALL LETTER QUU;Ll;0;L;;;;;N;;;13CA;;13CA
+AB9B;CHEROKEE SMALL LETTER QUV;Ll;0;L;;;;;N;;;13CB;;13CB
+AB9C;CHEROKEE SMALL LETTER SA;Ll;0;L;;;;;N;;;13CC;;13CC
+AB9D;CHEROKEE SMALL LETTER S;Ll;0;L;;;;;N;;;13CD;;13CD
+AB9E;CHEROKEE SMALL LETTER SE;Ll;0;L;;;;;N;;;13CE;;13CE
+AB9F;CHEROKEE SMALL LETTER SI;Ll;0;L;;;;;N;;;13CF;;13CF
+ABA0;CHEROKEE SMALL LETTER SO;Ll;0;L;;;;;N;;;13D0;;13D0
+ABA1;CHEROKEE SMALL LETTER SU;Ll;0;L;;;;;N;;;13D1;;13D1
+ABA2;CHEROKEE SMALL LETTER SV;Ll;0;L;;;;;N;;;13D2;;13D2
+ABA3;CHEROKEE SMALL LETTER DA;Ll;0;L;;;;;N;;;13D3;;13D3
+ABA4;CHEROKEE SMALL LETTER TA;Ll;0;L;;;;;N;;;13D4;;13D4
+ABA5;CHEROKEE SMALL LETTER DE;Ll;0;L;;;;;N;;;13D5;;13D5
+ABA6;CHEROKEE SMALL LETTER TE;Ll;0;L;;;;;N;;;13D6;;13D6
+ABA7;CHEROKEE SMALL LETTER DI;Ll;0;L;;;;;N;;;13D7;;13D7
+ABA8;CHEROKEE SMALL LETTER TI;Ll;0;L;;;;;N;;;13D8;;13D8
+ABA9;CHEROKEE SMALL LETTER DO;Ll;0;L;;;;;N;;;13D9;;13D9
+ABAA;CHEROKEE SMALL LETTER DU;Ll;0;L;;;;;N;;;13DA;;13DA
+ABAB;CHEROKEE SMALL LETTER DV;Ll;0;L;;;;;N;;;13DB;;13DB
+ABAC;CHEROKEE SMALL LETTER DLA;Ll;0;L;;;;;N;;;13DC;;13DC
+ABAD;CHEROKEE SMALL LETTER TLA;Ll;0;L;;;;;N;;;13DD;;13DD
+ABAE;CHEROKEE SMALL LETTER TLE;Ll;0;L;;;;;N;;;13DE;;13DE
+ABAF;CHEROKEE SMALL LETTER TLI;Ll;0;L;;;;;N;;;13DF;;13DF
+ABB0;CHEROKEE SMALL LETTER TLO;Ll;0;L;;;;;N;;;13E0;;13E0
+ABB1;CHEROKEE SMALL LETTER TLU;Ll;0;L;;;;;N;;;13E1;;13E1
+ABB2;CHEROKEE SMALL LETTER TLV;Ll;0;L;;;;;N;;;13E2;;13E2
+ABB3;CHEROKEE SMALL LETTER TSA;Ll;0;L;;;;;N;;;13E3;;13E3
+ABB4;CHEROKEE SMALL LETTER TSE;Ll;0;L;;;;;N;;;13E4;;13E4
+ABB5;CHEROKEE SMALL LETTER TSI;Ll;0;L;;;;;N;;;13E5;;13E5
+ABB6;CHEROKEE SMALL LETTER TSO;Ll;0;L;;;;;N;;;13E6;;13E6
+ABB7;CHEROKEE SMALL LETTER TSU;Ll;0;L;;;;;N;;;13E7;;13E7
+ABB8;CHEROKEE SMALL LETTER TSV;Ll;0;L;;;;;N;;;13E8;;13E8
+ABB9;CHEROKEE SMALL LETTER WA;Ll;0;L;;;;;N;;;13E9;;13E9
+ABBA;CHEROKEE SMALL LETTER WE;Ll;0;L;;;;;N;;;13EA;;13EA
+ABBB;CHEROKEE SMALL LETTER WI;Ll;0;L;;;;;N;;;13EB;;13EB
+ABBC;CHEROKEE SMALL LETTER WO;Ll;0;L;;;;;N;;;13EC;;13EC
+ABBD;CHEROKEE SMALL LETTER WU;Ll;0;L;;;;;N;;;13ED;;13ED
+ABBE;CHEROKEE SMALL LETTER WV;Ll;0;L;;;;;N;;;13EE;;13EE
+ABBF;CHEROKEE SMALL LETTER YA;Ll;0;L;;;;;N;;;13EF;;13EF
+ABC0;MEETEI MAYEK LETTER KOK;Lo;0;L;;;;;N;;;;;
+ABC1;MEETEI MAYEK LETTER SAM;Lo;0;L;;;;;N;;;;;
+ABC2;MEETEI MAYEK LETTER LAI;Lo;0;L;;;;;N;;;;;
+ABC3;MEETEI MAYEK LETTER MIT;Lo;0;L;;;;;N;;;;;
+ABC4;MEETEI MAYEK LETTER PA;Lo;0;L;;;;;N;;;;;
+ABC5;MEETEI MAYEK LETTER NA;Lo;0;L;;;;;N;;;;;
+ABC6;MEETEI MAYEK LETTER CHIL;Lo;0;L;;;;;N;;;;;
+ABC7;MEETEI MAYEK LETTER TIL;Lo;0;L;;;;;N;;;;;
+ABC8;MEETEI MAYEK LETTER KHOU;Lo;0;L;;;;;N;;;;;
+ABC9;MEETEI MAYEK LETTER NGOU;Lo;0;L;;;;;N;;;;;
+ABCA;MEETEI MAYEK LETTER THOU;Lo;0;L;;;;;N;;;;;
+ABCB;MEETEI MAYEK LETTER WAI;Lo;0;L;;;;;N;;;;;
+ABCC;MEETEI MAYEK LETTER YANG;Lo;0;L;;;;;N;;;;;
+ABCD;MEETEI MAYEK LETTER HUK;Lo;0;L;;;;;N;;;;;
+ABCE;MEETEI MAYEK LETTER UN;Lo;0;L;;;;;N;;;;;
+ABCF;MEETEI MAYEK LETTER I;Lo;0;L;;;;;N;;;;;
+ABD0;MEETEI MAYEK LETTER PHAM;Lo;0;L;;;;;N;;;;;
+ABD1;MEETEI MAYEK LETTER ATIYA;Lo;0;L;;;;;N;;;;;
+ABD2;MEETEI MAYEK LETTER GOK;Lo;0;L;;;;;N;;;;;
+ABD3;MEETEI MAYEK LETTER JHAM;Lo;0;L;;;;;N;;;;;
+ABD4;MEETEI MAYEK LETTER RAI;Lo;0;L;;;;;N;;;;;
+ABD5;MEETEI MAYEK LETTER BA;Lo;0;L;;;;;N;;;;;
+ABD6;MEETEI MAYEK LETTER JIL;Lo;0;L;;;;;N;;;;;
+ABD7;MEETEI MAYEK LETTER DIL;Lo;0;L;;;;;N;;;;;
+ABD8;MEETEI MAYEK LETTER GHOU;Lo;0;L;;;;;N;;;;;
+ABD9;MEETEI MAYEK LETTER DHOU;Lo;0;L;;;;;N;;;;;
+ABDA;MEETEI MAYEK LETTER BHAM;Lo;0;L;;;;;N;;;;;
+ABDB;MEETEI MAYEK LETTER KOK LONSUM;Lo;0;L;;;;;N;;;;;
+ABDC;MEETEI MAYEK LETTER LAI LONSUM;Lo;0;L;;;;;N;;;;;
+ABDD;MEETEI MAYEK LETTER MIT LONSUM;Lo;0;L;;;;;N;;;;;
+ABDE;MEETEI MAYEK LETTER PA LONSUM;Lo;0;L;;;;;N;;;;;
+ABDF;MEETEI MAYEK LETTER NA LONSUM;Lo;0;L;;;;;N;;;;;
+ABE0;MEETEI MAYEK LETTER TIL LONSUM;Lo;0;L;;;;;N;;;;;
+ABE1;MEETEI MAYEK LETTER NGOU LONSUM;Lo;0;L;;;;;N;;;;;
+ABE2;MEETEI MAYEK LETTER I LONSUM;Lo;0;L;;;;;N;;;;;
+ABE3;MEETEI MAYEK VOWEL SIGN ONAP;Mc;0;L;;;;;N;;;;;
+ABE4;MEETEI MAYEK VOWEL SIGN INAP;Mc;0;L;;;;;N;;;;;
+ABE5;MEETEI MAYEK VOWEL SIGN ANAP;Mn;0;NSM;;;;;N;;;;;
+ABE6;MEETEI MAYEK VOWEL SIGN YENAP;Mc;0;L;;;;;N;;;;;
+ABE7;MEETEI MAYEK VOWEL SIGN SOUNAP;Mc;0;L;;;;;N;;;;;
+ABE8;MEETEI MAYEK VOWEL SIGN UNAP;Mn;0;NSM;;;;;N;;;;;
+ABE9;MEETEI MAYEK VOWEL SIGN CHEINAP;Mc;0;L;;;;;N;;;;;
+ABEA;MEETEI MAYEK VOWEL SIGN NUNG;Mc;0;L;;;;;N;;;;;
+ABEB;MEETEI MAYEK CHEIKHEI;Po;0;L;;;;;N;;;;;
+ABEC;MEETEI MAYEK LUM IYEK;Mc;0;L;;;;;N;;;;;
+ABED;MEETEI MAYEK APUN IYEK;Mn;9;NSM;;;;;N;;;;;
+ABF0;MEETEI MAYEK DIGIT ZERO;Nd;0;L;;0;0;0;N;;;;;
+ABF1;MEETEI MAYEK DIGIT ONE;Nd;0;L;;1;1;1;N;;;;;
+ABF2;MEETEI MAYEK DIGIT TWO;Nd;0;L;;2;2;2;N;;;;;
+ABF3;MEETEI MAYEK DIGIT THREE;Nd;0;L;;3;3;3;N;;;;;
+ABF4;MEETEI MAYEK DIGIT FOUR;Nd;0;L;;4;4;4;N;;;;;
+ABF5;MEETEI MAYEK DIGIT FIVE;Nd;0;L;;5;5;5;N;;;;;
+ABF6;MEETEI MAYEK DIGIT SIX;Nd;0;L;;6;6;6;N;;;;;
+ABF7;MEETEI MAYEK DIGIT SEVEN;Nd;0;L;;7;7;7;N;;;;;
+ABF8;MEETEI MAYEK DIGIT EIGHT;Nd;0;L;;8;8;8;N;;;;;
+ABF9;MEETEI MAYEK DIGIT NINE;Nd;0;L;;9;9;9;N;;;;;
+AC00;<Hangul Syllable, First>;Lo;0;L;;;;;N;;;;;
+D7A3;<Hangul Syllable, Last>;Lo;0;L;;;;;N;;;;;
+D7B0;HANGUL JUNGSEONG O-YEO;Lo;0;L;;;;;N;;;;;
+D7B1;HANGUL JUNGSEONG O-O-I;Lo;0;L;;;;;N;;;;;
+D7B2;HANGUL JUNGSEONG YO-A;Lo;0;L;;;;;N;;;;;
+D7B3;HANGUL JUNGSEONG YO-AE;Lo;0;L;;;;;N;;;;;
+D7B4;HANGUL JUNGSEONG YO-EO;Lo;0;L;;;;;N;;;;;
+D7B5;HANGUL JUNGSEONG U-YEO;Lo;0;L;;;;;N;;;;;
+D7B6;HANGUL JUNGSEONG U-I-I;Lo;0;L;;;;;N;;;;;
+D7B7;HANGUL JUNGSEONG YU-AE;Lo;0;L;;;;;N;;;;;
+D7B8;HANGUL JUNGSEONG YU-O;Lo;0;L;;;;;N;;;;;
+D7B9;HANGUL JUNGSEONG EU-A;Lo;0;L;;;;;N;;;;;
+D7BA;HANGUL JUNGSEONG EU-EO;Lo;0;L;;;;;N;;;;;
+D7BB;HANGUL JUNGSEONG EU-E;Lo;0;L;;;;;N;;;;;
+D7BC;HANGUL JUNGSEONG EU-O;Lo;0;L;;;;;N;;;;;
+D7BD;HANGUL JUNGSEONG I-YA-O;Lo;0;L;;;;;N;;;;;
+D7BE;HANGUL JUNGSEONG I-YAE;Lo;0;L;;;;;N;;;;;
+D7BF;HANGUL JUNGSEONG I-YEO;Lo;0;L;;;;;N;;;;;
+D7C0;HANGUL JUNGSEONG I-YE;Lo;0;L;;;;;N;;;;;
+D7C1;HANGUL JUNGSEONG I-O-I;Lo;0;L;;;;;N;;;;;
+D7C2;HANGUL JUNGSEONG I-YO;Lo;0;L;;;;;N;;;;;
+D7C3;HANGUL JUNGSEONG I-YU;Lo;0;L;;;;;N;;;;;
+D7C4;HANGUL JUNGSEONG I-I;Lo;0;L;;;;;N;;;;;
+D7C5;HANGUL JUNGSEONG ARAEA-A;Lo;0;L;;;;;N;;;;;
+D7C6;HANGUL JUNGSEONG ARAEA-E;Lo;0;L;;;;;N;;;;;
+D7CB;HANGUL JONGSEONG NIEUN-RIEUL;Lo;0;L;;;;;N;;;;;
+D7CC;HANGUL JONGSEONG NIEUN-CHIEUCH;Lo;0;L;;;;;N;;;;;
+D7CD;HANGUL JONGSEONG SSANGTIKEUT;Lo;0;L;;;;;N;;;;;
+D7CE;HANGUL JONGSEONG SSANGTIKEUT-PIEUP;Lo;0;L;;;;;N;;;;;
+D7CF;HANGUL JONGSEONG TIKEUT-PIEUP;Lo;0;L;;;;;N;;;;;
+D7D0;HANGUL JONGSEONG TIKEUT-SIOS;Lo;0;L;;;;;N;;;;;
+D7D1;HANGUL JONGSEONG TIKEUT-SIOS-KIYEOK;Lo;0;L;;;;;N;;;;;
+D7D2;HANGUL JONGSEONG TIKEUT-CIEUC;Lo;0;L;;;;;N;;;;;
+D7D3;HANGUL JONGSEONG TIKEUT-CHIEUCH;Lo;0;L;;;;;N;;;;;
+D7D4;HANGUL JONGSEONG TIKEUT-THIEUTH;Lo;0;L;;;;;N;;;;;
+D7D5;HANGUL JONGSEONG RIEUL-SSANGKIYEOK;Lo;0;L;;;;;N;;;;;
+D7D6;HANGUL JONGSEONG RIEUL-KIYEOK-HIEUH;Lo;0;L;;;;;N;;;;;
+D7D7;HANGUL JONGSEONG SSANGRIEUL-KHIEUKH;Lo;0;L;;;;;N;;;;;
+D7D8;HANGUL JONGSEONG RIEUL-MIEUM-HIEUH;Lo;0;L;;;;;N;;;;;
+D7D9;HANGUL JONGSEONG RIEUL-PIEUP-TIKEUT;Lo;0;L;;;;;N;;;;;
+D7DA;HANGUL JONGSEONG RIEUL-PIEUP-PHIEUPH;Lo;0;L;;;;;N;;;;;
+D7DB;HANGUL JONGSEONG RIEUL-YESIEUNG;Lo;0;L;;;;;N;;;;;
+D7DC;HANGUL JONGSEONG RIEUL-YEORINHIEUH-HIEUH;Lo;0;L;;;;;N;;;;;
+D7DD;HANGUL JONGSEONG KAPYEOUNRIEUL;Lo;0;L;;;;;N;;;;;
+D7DE;HANGUL JONGSEONG MIEUM-NIEUN;Lo;0;L;;;;;N;;;;;
+D7DF;HANGUL JONGSEONG MIEUM-SSANGNIEUN;Lo;0;L;;;;;N;;;;;
+D7E0;HANGUL JONGSEONG SSANGMIEUM;Lo;0;L;;;;;N;;;;;
+D7E1;HANGUL JONGSEONG MIEUM-PIEUP-SIOS;Lo;0;L;;;;;N;;;;;
+D7E2;HANGUL JONGSEONG MIEUM-CIEUC;Lo;0;L;;;;;N;;;;;
+D7E3;HANGUL JONGSEONG PIEUP-TIKEUT;Lo;0;L;;;;;N;;;;;
+D7E4;HANGUL JONGSEONG PIEUP-RIEUL-PHIEUPH;Lo;0;L;;;;;N;;;;;
+D7E5;HANGUL JONGSEONG PIEUP-MIEUM;Lo;0;L;;;;;N;;;;;
+D7E6;HANGUL JONGSEONG SSANGPIEUP;Lo;0;L;;;;;N;;;;;
+D7E7;HANGUL JONGSEONG PIEUP-SIOS-TIKEUT;Lo;0;L;;;;;N;;;;;
+D7E8;HANGUL JONGSEONG PIEUP-CIEUC;Lo;0;L;;;;;N;;;;;
+D7E9;HANGUL JONGSEONG PIEUP-CHIEUCH;Lo;0;L;;;;;N;;;;;
+D7EA;HANGUL JONGSEONG SIOS-MIEUM;Lo;0;L;;;;;N;;;;;
+D7EB;HANGUL JONGSEONG SIOS-KAPYEOUNPIEUP;Lo;0;L;;;;;N;;;;;
+D7EC;HANGUL JONGSEONG SSANGSIOS-KIYEOK;Lo;0;L;;;;;N;;;;;
+D7ED;HANGUL JONGSEONG SSANGSIOS-TIKEUT;Lo;0;L;;;;;N;;;;;
+D7EE;HANGUL JONGSEONG SIOS-PANSIOS;Lo;0;L;;;;;N;;;;;
+D7EF;HANGUL JONGSEONG SIOS-CIEUC;Lo;0;L;;;;;N;;;;;
+D7F0;HANGUL JONGSEONG SIOS-CHIEUCH;Lo;0;L;;;;;N;;;;;
+D7F1;HANGUL JONGSEONG SIOS-THIEUTH;Lo;0;L;;;;;N;;;;;
+D7F2;HANGUL JONGSEONG SIOS-HIEUH;Lo;0;L;;;;;N;;;;;
+D7F3;HANGUL JONGSEONG PANSIOS-PIEUP;Lo;0;L;;;;;N;;;;;
+D7F4;HANGUL JONGSEONG PANSIOS-KAPYEOUNPIEUP;Lo;0;L;;;;;N;;;;;
+D7F5;HANGUL JONGSEONG YESIEUNG-MIEUM;Lo;0;L;;;;;N;;;;;
+D7F6;HANGUL JONGSEONG YESIEUNG-HIEUH;Lo;0;L;;;;;N;;;;;
+D7F7;HANGUL JONGSEONG CIEUC-PIEUP;Lo;0;L;;;;;N;;;;;
+D7F8;HANGUL JONGSEONG CIEUC-SSANGPIEUP;Lo;0;L;;;;;N;;;;;
+D7F9;HANGUL JONGSEONG SSANGCIEUC;Lo;0;L;;;;;N;;;;;
+D7FA;HANGUL JONGSEONG PHIEUPH-SIOS;Lo;0;L;;;;;N;;;;;
+D7FB;HANGUL JONGSEONG PHIEUPH-THIEUTH;Lo;0;L;;;;;N;;;;;
+D800;<Non Private Use High Surrogate, First>;Cs;0;L;;;;;N;;;;;
+DB7F;<Non Private Use High Surrogate, Last>;Cs;0;L;;;;;N;;;;;
+DB80;<Private Use High Surrogate, First>;Cs;0;L;;;;;N;;;;;
+DBFF;<Private Use High Surrogate, Last>;Cs;0;L;;;;;N;;;;;
+DC00;<Low Surrogate, First>;Cs;0;L;;;;;N;;;;;
+DFFF;<Low Surrogate, Last>;Cs;0;L;;;;;N;;;;;
+E000;<Private Use, First>;Co;0;L;;;;;N;;;;;
+F8FF;<Private Use, Last>;Co;0;L;;;;;N;;;;;
+F900;CJK COMPATIBILITY IDEOGRAPH-F900;Lo;0;L;8C48;;;;N;;;;;
+F901;CJK COMPATIBILITY IDEOGRAPH-F901;Lo;0;L;66F4;;;;N;;;;;
+F902;CJK COMPATIBILITY IDEOGRAPH-F902;Lo;0;L;8ECA;;;;N;;;;;
+F903;CJK COMPATIBILITY IDEOGRAPH-F903;Lo;0;L;8CC8;;;;N;;;;;
+F904;CJK COMPATIBILITY IDEOGRAPH-F904;Lo;0;L;6ED1;;;;N;;;;;
+F905;CJK COMPATIBILITY IDEOGRAPH-F905;Lo;0;L;4E32;;;;N;;;;;
+F906;CJK COMPATIBILITY IDEOGRAPH-F906;Lo;0;L;53E5;;;;N;;;;;
+F907;CJK COMPATIBILITY IDEOGRAPH-F907;Lo;0;L;9F9C;;;;N;;;;;
+F908;CJK COMPATIBILITY IDEOGRAPH-F908;Lo;0;L;9F9C;;;;N;;;;;
+F909;CJK COMPATIBILITY IDEOGRAPH-F909;Lo;0;L;5951;;;;N;;;;;
+F90A;CJK COMPATIBILITY IDEOGRAPH-F90A;Lo;0;L;91D1;;;;N;;;;;
+F90B;CJK COMPATIBILITY IDEOGRAPH-F90B;Lo;0;L;5587;;;;N;;;;;
+F90C;CJK COMPATIBILITY IDEOGRAPH-F90C;Lo;0;L;5948;;;;N;;;;;
+F90D;CJK COMPATIBILITY IDEOGRAPH-F90D;Lo;0;L;61F6;;;;N;;;;;
+F90E;CJK COMPATIBILITY IDEOGRAPH-F90E;Lo;0;L;7669;;;;N;;;;;
+F90F;CJK COMPATIBILITY IDEOGRAPH-F90F;Lo;0;L;7F85;;;;N;;;;;
+F910;CJK COMPATIBILITY IDEOGRAPH-F910;Lo;0;L;863F;;;;N;;;;;
+F911;CJK COMPATIBILITY IDEOGRAPH-F911;Lo;0;L;87BA;;;;N;;;;;
+F912;CJK COMPATIBILITY IDEOGRAPH-F912;Lo;0;L;88F8;;;;N;;;;;
+F913;CJK COMPATIBILITY IDEOGRAPH-F913;Lo;0;L;908F;;;;N;;;;;
+F914;CJK COMPATIBILITY IDEOGRAPH-F914;Lo;0;L;6A02;;;;N;;;;;
+F915;CJK COMPATIBILITY IDEOGRAPH-F915;Lo;0;L;6D1B;;;;N;;;;;
+F916;CJK COMPATIBILITY IDEOGRAPH-F916;Lo;0;L;70D9;;;;N;;;;;
+F917;CJK COMPATIBILITY IDEOGRAPH-F917;Lo;0;L;73DE;;;;N;;;;;
+F918;CJK COMPATIBILITY IDEOGRAPH-F918;Lo;0;L;843D;;;;N;;;;;
+F919;CJK COMPATIBILITY IDEOGRAPH-F919;Lo;0;L;916A;;;;N;;;;;
+F91A;CJK COMPATIBILITY IDEOGRAPH-F91A;Lo;0;L;99F1;;;;N;;;;;
+F91B;CJK COMPATIBILITY IDEOGRAPH-F91B;Lo;0;L;4E82;;;;N;;;;;
+F91C;CJK COMPATIBILITY IDEOGRAPH-F91C;Lo;0;L;5375;;;;N;;;;;
+F91D;CJK COMPATIBILITY IDEOGRAPH-F91D;Lo;0;L;6B04;;;;N;;;;;
+F91E;CJK COMPATIBILITY IDEOGRAPH-F91E;Lo;0;L;721B;;;;N;;;;;
+F91F;CJK COMPATIBILITY IDEOGRAPH-F91F;Lo;0;L;862D;;;;N;;;;;
+F920;CJK COMPATIBILITY IDEOGRAPH-F920;Lo;0;L;9E1E;;;;N;;;;;
+F921;CJK COMPATIBILITY IDEOGRAPH-F921;Lo;0;L;5D50;;;;N;;;;;
+F922;CJK COMPATIBILITY IDEOGRAPH-F922;Lo;0;L;6FEB;;;;N;;;;;
+F923;CJK COMPATIBILITY IDEOGRAPH-F923;Lo;0;L;85CD;;;;N;;;;;
+F924;CJK COMPATIBILITY IDEOGRAPH-F924;Lo;0;L;8964;;;;N;;;;;
+F925;CJK COMPATIBILITY IDEOGRAPH-F925;Lo;0;L;62C9;;;;N;;;;;
+F926;CJK COMPATIBILITY IDEOGRAPH-F926;Lo;0;L;81D8;;;;N;;;;;
+F927;CJK COMPATIBILITY IDEOGRAPH-F927;Lo;0;L;881F;;;;N;;;;;
+F928;CJK COMPATIBILITY IDEOGRAPH-F928;Lo;0;L;5ECA;;;;N;;;;;
+F929;CJK COMPATIBILITY IDEOGRAPH-F929;Lo;0;L;6717;;;;N;;;;;
+F92A;CJK COMPATIBILITY IDEOGRAPH-F92A;Lo;0;L;6D6A;;;;N;;;;;
+F92B;CJK COMPATIBILITY IDEOGRAPH-F92B;Lo;0;L;72FC;;;;N;;;;;
+F92C;CJK COMPATIBILITY IDEOGRAPH-F92C;Lo;0;L;90CE;;;;N;;;;;
+F92D;CJK COMPATIBILITY IDEOGRAPH-F92D;Lo;0;L;4F86;;;;N;;;;;
+F92E;CJK COMPATIBILITY IDEOGRAPH-F92E;Lo;0;L;51B7;;;;N;;;;;
+F92F;CJK COMPATIBILITY IDEOGRAPH-F92F;Lo;0;L;52DE;;;;N;;;;;
+F930;CJK COMPATIBILITY IDEOGRAPH-F930;Lo;0;L;64C4;;;;N;;;;;
+F931;CJK COMPATIBILITY IDEOGRAPH-F931;Lo;0;L;6AD3;;;;N;;;;;
+F932;CJK COMPATIBILITY IDEOGRAPH-F932;Lo;0;L;7210;;;;N;;;;;
+F933;CJK COMPATIBILITY IDEOGRAPH-F933;Lo;0;L;76E7;;;;N;;;;;
+F934;CJK COMPATIBILITY IDEOGRAPH-F934;Lo;0;L;8001;;;;N;;;;;
+F935;CJK COMPATIBILITY IDEOGRAPH-F935;Lo;0;L;8606;;;;N;;;;;
+F936;CJK COMPATIBILITY IDEOGRAPH-F936;Lo;0;L;865C;;;;N;;;;;
+F937;CJK COMPATIBILITY IDEOGRAPH-F937;Lo;0;L;8DEF;;;;N;;;;;
+F938;CJK COMPATIBILITY IDEOGRAPH-F938;Lo;0;L;9732;;;;N;;;;;
+F939;CJK COMPATIBILITY IDEOGRAPH-F939;Lo;0;L;9B6F;;;;N;;;;;
+F93A;CJK COMPATIBILITY IDEOGRAPH-F93A;Lo;0;L;9DFA;;;;N;;;;;
+F93B;CJK COMPATIBILITY IDEOGRAPH-F93B;Lo;0;L;788C;;;;N;;;;;
+F93C;CJK COMPATIBILITY IDEOGRAPH-F93C;Lo;0;L;797F;;;;N;;;;;
+F93D;CJK COMPATIBILITY IDEOGRAPH-F93D;Lo;0;L;7DA0;;;;N;;;;;
+F93E;CJK COMPATIBILITY IDEOGRAPH-F93E;Lo;0;L;83C9;;;;N;;;;;
+F93F;CJK COMPATIBILITY IDEOGRAPH-F93F;Lo;0;L;9304;;;;N;;;;;
+F940;CJK COMPATIBILITY IDEOGRAPH-F940;Lo;0;L;9E7F;;;;N;;;;;
+F941;CJK COMPATIBILITY IDEOGRAPH-F941;Lo;0;L;8AD6;;;;N;;;;;
+F942;CJK COMPATIBILITY IDEOGRAPH-F942;Lo;0;L;58DF;;;;N;;;;;
+F943;CJK COMPATIBILITY IDEOGRAPH-F943;Lo;0;L;5F04;;;;N;;;;;
+F944;CJK COMPATIBILITY IDEOGRAPH-F944;Lo;0;L;7C60;;;;N;;;;;
+F945;CJK COMPATIBILITY IDEOGRAPH-F945;Lo;0;L;807E;;;;N;;;;;
+F946;CJK COMPATIBILITY IDEOGRAPH-F946;Lo;0;L;7262;;;;N;;;;;
+F947;CJK COMPATIBILITY IDEOGRAPH-F947;Lo;0;L;78CA;;;;N;;;;;
+F948;CJK COMPATIBILITY IDEOGRAPH-F948;Lo;0;L;8CC2;;;;N;;;;;
+F949;CJK COMPATIBILITY IDEOGRAPH-F949;Lo;0;L;96F7;;;;N;;;;;
+F94A;CJK COMPATIBILITY IDEOGRAPH-F94A;Lo;0;L;58D8;;;;N;;;;;
+F94B;CJK COMPATIBILITY IDEOGRAPH-F94B;Lo;0;L;5C62;;;;N;;;;;
+F94C;CJK COMPATIBILITY IDEOGRAPH-F94C;Lo;0;L;6A13;;;;N;;;;;
+F94D;CJK COMPATIBILITY IDEOGRAPH-F94D;Lo;0;L;6DDA;;;;N;;;;;
+F94E;CJK COMPATIBILITY IDEOGRAPH-F94E;Lo;0;L;6F0F;;;;N;;;;;
+F94F;CJK COMPATIBILITY IDEOGRAPH-F94F;Lo;0;L;7D2F;;;;N;;;;;
+F950;CJK COMPATIBILITY IDEOGRAPH-F950;Lo;0;L;7E37;;;;N;;;;;
+F951;CJK COMPATIBILITY IDEOGRAPH-F951;Lo;0;L;964B;;;;N;;;;;
+F952;CJK COMPATIBILITY IDEOGRAPH-F952;Lo;0;L;52D2;;;;N;;;;;
+F953;CJK COMPATIBILITY IDEOGRAPH-F953;Lo;0;L;808B;;;;N;;;;;
+F954;CJK COMPATIBILITY IDEOGRAPH-F954;Lo;0;L;51DC;;;;N;;;;;
+F955;CJK COMPATIBILITY IDEOGRAPH-F955;Lo;0;L;51CC;;;;N;;;;;
+F956;CJK COMPATIBILITY IDEOGRAPH-F956;Lo;0;L;7A1C;;;;N;;;;;
+F957;CJK COMPATIBILITY IDEOGRAPH-F957;Lo;0;L;7DBE;;;;N;;;;;
+F958;CJK COMPATIBILITY IDEOGRAPH-F958;Lo;0;L;83F1;;;;N;;;;;
+F959;CJK COMPATIBILITY IDEOGRAPH-F959;Lo;0;L;9675;;;;N;;;;;
+F95A;CJK COMPATIBILITY IDEOGRAPH-F95A;Lo;0;L;8B80;;;;N;;;;;
+F95B;CJK COMPATIBILITY IDEOGRAPH-F95B;Lo;0;L;62CF;;;;N;;;;;
+F95C;CJK COMPATIBILITY IDEOGRAPH-F95C;Lo;0;L;6A02;;;;N;;;;;
+F95D;CJK COMPATIBILITY IDEOGRAPH-F95D;Lo;0;L;8AFE;;;;N;;;;;
+F95E;CJK COMPATIBILITY IDEOGRAPH-F95E;Lo;0;L;4E39;;;;N;;;;;
+F95F;CJK COMPATIBILITY IDEOGRAPH-F95F;Lo;0;L;5BE7;;;;N;;;;;
+F960;CJK COMPATIBILITY IDEOGRAPH-F960;Lo;0;L;6012;;;;N;;;;;
+F961;CJK COMPATIBILITY IDEOGRAPH-F961;Lo;0;L;7387;;;;N;;;;;
+F962;CJK COMPATIBILITY IDEOGRAPH-F962;Lo;0;L;7570;;;;N;;;;;
+F963;CJK COMPATIBILITY IDEOGRAPH-F963;Lo;0;L;5317;;;;N;;;;;
+F964;CJK COMPATIBILITY IDEOGRAPH-F964;Lo;0;L;78FB;;;;N;;;;;
+F965;CJK COMPATIBILITY IDEOGRAPH-F965;Lo;0;L;4FBF;;;;N;;;;;
+F966;CJK COMPATIBILITY IDEOGRAPH-F966;Lo;0;L;5FA9;;;;N;;;;;
+F967;CJK COMPATIBILITY IDEOGRAPH-F967;Lo;0;L;4E0D;;;;N;;;;;
+F968;CJK COMPATIBILITY IDEOGRAPH-F968;Lo;0;L;6CCC;;;;N;;;;;
+F969;CJK COMPATIBILITY IDEOGRAPH-F969;Lo;0;L;6578;;;;N;;;;;
+F96A;CJK COMPATIBILITY IDEOGRAPH-F96A;Lo;0;L;7D22;;;;N;;;;;
+F96B;CJK COMPATIBILITY IDEOGRAPH-F96B;Lo;0;L;53C3;;;3;N;;;;;
+F96C;CJK COMPATIBILITY IDEOGRAPH-F96C;Lo;0;L;585E;;;;N;;;;;
+F96D;CJK COMPATIBILITY IDEOGRAPH-F96D;Lo;0;L;7701;;;;N;;;;;
+F96E;CJK COMPATIBILITY IDEOGRAPH-F96E;Lo;0;L;8449;;;;N;;;;;
+F96F;CJK COMPATIBILITY IDEOGRAPH-F96F;Lo;0;L;8AAA;;;;N;;;;;
+F970;CJK COMPATIBILITY IDEOGRAPH-F970;Lo;0;L;6BBA;;;;N;;;;;
+F971;CJK COMPATIBILITY IDEOGRAPH-F971;Lo;0;L;8FB0;;;;N;;;;;
+F972;CJK COMPATIBILITY IDEOGRAPH-F972;Lo;0;L;6C88;;;;N;;;;;
+F973;CJK COMPATIBILITY IDEOGRAPH-F973;Lo;0;L;62FE;;;10;N;;;;;
+F974;CJK COMPATIBILITY IDEOGRAPH-F974;Lo;0;L;82E5;;;;N;;;;;
+F975;CJK COMPATIBILITY IDEOGRAPH-F975;Lo;0;L;63A0;;;;N;;;;;
+F976;CJK COMPATIBILITY IDEOGRAPH-F976;Lo;0;L;7565;;;;N;;;;;
+F977;CJK COMPATIBILITY IDEOGRAPH-F977;Lo;0;L;4EAE;;;;N;;;;;
+F978;CJK COMPATIBILITY IDEOGRAPH-F978;Lo;0;L;5169;;;2;N;;;;;
+F979;CJK COMPATIBILITY IDEOGRAPH-F979;Lo;0;L;51C9;;;;N;;;;;
+F97A;CJK COMPATIBILITY IDEOGRAPH-F97A;Lo;0;L;6881;;;;N;;;;;
+F97B;CJK COMPATIBILITY IDEOGRAPH-F97B;Lo;0;L;7CE7;;;;N;;;;;
+F97C;CJK COMPATIBILITY IDEOGRAPH-F97C;Lo;0;L;826F;;;;N;;;;;
+F97D;CJK COMPATIBILITY IDEOGRAPH-F97D;Lo;0;L;8AD2;;;;N;;;;;
+F97E;CJK COMPATIBILITY IDEOGRAPH-F97E;Lo;0;L;91CF;;;;N;;;;;
+F97F;CJK COMPATIBILITY IDEOGRAPH-F97F;Lo;0;L;52F5;;;;N;;;;;
+F980;CJK COMPATIBILITY IDEOGRAPH-F980;Lo;0;L;5442;;;;N;;;;;
+F981;CJK COMPATIBILITY IDEOGRAPH-F981;Lo;0;L;5973;;;;N;;;;;
+F982;CJK COMPATIBILITY IDEOGRAPH-F982;Lo;0;L;5EEC;;;;N;;;;;
+F983;CJK COMPATIBILITY IDEOGRAPH-F983;Lo;0;L;65C5;;;;N;;;;;
+F984;CJK COMPATIBILITY IDEOGRAPH-F984;Lo;0;L;6FFE;;;;N;;;;;
+F985;CJK COMPATIBILITY IDEOGRAPH-F985;Lo;0;L;792A;;;;N;;;;;
+F986;CJK COMPATIBILITY IDEOGRAPH-F986;Lo;0;L;95AD;;;;N;;;;;
+F987;CJK COMPATIBILITY IDEOGRAPH-F987;Lo;0;L;9A6A;;;;N;;;;;
+F988;CJK COMPATIBILITY IDEOGRAPH-F988;Lo;0;L;9E97;;;;N;;;;;
+F989;CJK COMPATIBILITY IDEOGRAPH-F989;Lo;0;L;9ECE;;;;N;;;;;
+F98A;CJK COMPATIBILITY IDEOGRAPH-F98A;Lo;0;L;529B;;;;N;;;;;
+F98B;CJK COMPATIBILITY IDEOGRAPH-F98B;Lo;0;L;66C6;;;;N;;;;;
+F98C;CJK COMPATIBILITY IDEOGRAPH-F98C;Lo;0;L;6B77;;;;N;;;;;
+F98D;CJK COMPATIBILITY IDEOGRAPH-F98D;Lo;0;L;8F62;;;;N;;;;;
+F98E;CJK COMPATIBILITY IDEOGRAPH-F98E;Lo;0;L;5E74;;;;N;;;;;
+F98F;CJK COMPATIBILITY IDEOGRAPH-F98F;Lo;0;L;6190;;;;N;;;;;
+F990;CJK COMPATIBILITY IDEOGRAPH-F990;Lo;0;L;6200;;;;N;;;;;
+F991;CJK COMPATIBILITY IDEOGRAPH-F991;Lo;0;L;649A;;;;N;;;;;
+F992;CJK COMPATIBILITY IDEOGRAPH-F992;Lo;0;L;6F23;;;;N;;;;;
+F993;CJK COMPATIBILITY IDEOGRAPH-F993;Lo;0;L;7149;;;;N;;;;;
+F994;CJK COMPATIBILITY IDEOGRAPH-F994;Lo;0;L;7489;;;;N;;;;;
+F995;CJK COMPATIBILITY IDEOGRAPH-F995;Lo;0;L;79CA;;;;N;;;;;
+F996;CJK COMPATIBILITY IDEOGRAPH-F996;Lo;0;L;7DF4;;;;N;;;;;
+F997;CJK COMPATIBILITY IDEOGRAPH-F997;Lo;0;L;806F;;;;N;;;;;
+F998;CJK COMPATIBILITY IDEOGRAPH-F998;Lo;0;L;8F26;;;;N;;;;;
+F999;CJK COMPATIBILITY IDEOGRAPH-F999;Lo;0;L;84EE;;;;N;;;;;
+F99A;CJK COMPATIBILITY IDEOGRAPH-F99A;Lo;0;L;9023;;;;N;;;;;
+F99B;CJK COMPATIBILITY IDEOGRAPH-F99B;Lo;0;L;934A;;;;N;;;;;
+F99C;CJK COMPATIBILITY IDEOGRAPH-F99C;Lo;0;L;5217;;;;N;;;;;
+F99D;CJK COMPATIBILITY IDEOGRAPH-F99D;Lo;0;L;52A3;;;;N;;;;;
+F99E;CJK COMPATIBILITY IDEOGRAPH-F99E;Lo;0;L;54BD;;;;N;;;;;
+F99F;CJK COMPATIBILITY IDEOGRAPH-F99F;Lo;0;L;70C8;;;;N;;;;;
+F9A0;CJK COMPATIBILITY IDEOGRAPH-F9A0;Lo;0;L;88C2;;;;N;;;;;
+F9A1;CJK COMPATIBILITY IDEOGRAPH-F9A1;Lo;0;L;8AAA;;;;N;;;;;
+F9A2;CJK COMPATIBILITY IDEOGRAPH-F9A2;Lo;0;L;5EC9;;;;N;;;;;
+F9A3;CJK COMPATIBILITY IDEOGRAPH-F9A3;Lo;0;L;5FF5;;;;N;;;;;
+F9A4;CJK COMPATIBILITY IDEOGRAPH-F9A4;Lo;0;L;637B;;;;N;;;;;
+F9A5;CJK COMPATIBILITY IDEOGRAPH-F9A5;Lo;0;L;6BAE;;;;N;;;;;
+F9A6;CJK COMPATIBILITY IDEOGRAPH-F9A6;Lo;0;L;7C3E;;;;N;;;;;
+F9A7;CJK COMPATIBILITY IDEOGRAPH-F9A7;Lo;0;L;7375;;;;N;;;;;
+F9A8;CJK COMPATIBILITY IDEOGRAPH-F9A8;Lo;0;L;4EE4;;;;N;;;;;
+F9A9;CJK COMPATIBILITY IDEOGRAPH-F9A9;Lo;0;L;56F9;;;;N;;;;;
+F9AA;CJK COMPATIBILITY IDEOGRAPH-F9AA;Lo;0;L;5BE7;;;;N;;;;;
+F9AB;CJK COMPATIBILITY IDEOGRAPH-F9AB;Lo;0;L;5DBA;;;;N;;;;;
+F9AC;CJK COMPATIBILITY IDEOGRAPH-F9AC;Lo;0;L;601C;;;;N;;;;;
+F9AD;CJK COMPATIBILITY IDEOGRAPH-F9AD;Lo;0;L;73B2;;;;N;;;;;
+F9AE;CJK COMPATIBILITY IDEOGRAPH-F9AE;Lo;0;L;7469;;;;N;;;;;
+F9AF;CJK COMPATIBILITY IDEOGRAPH-F9AF;Lo;0;L;7F9A;;;;N;;;;;
+F9B0;CJK COMPATIBILITY IDEOGRAPH-F9B0;Lo;0;L;8046;;;;N;;;;;
+F9B1;CJK COMPATIBILITY IDEOGRAPH-F9B1;Lo;0;L;9234;;;;N;;;;;
+F9B2;CJK COMPATIBILITY IDEOGRAPH-F9B2;Lo;0;L;96F6;;;0;N;;;;;
+F9B3;CJK COMPATIBILITY IDEOGRAPH-F9B3;Lo;0;L;9748;;;;N;;;;;
+F9B4;CJK COMPATIBILITY IDEOGRAPH-F9B4;Lo;0;L;9818;;;;N;;;;;
+F9B5;CJK COMPATIBILITY IDEOGRAPH-F9B5;Lo;0;L;4F8B;;;;N;;;;;
+F9B6;CJK COMPATIBILITY IDEOGRAPH-F9B6;Lo;0;L;79AE;;;;N;;;;;
+F9B7;CJK COMPATIBILITY IDEOGRAPH-F9B7;Lo;0;L;91B4;;;;N;;;;;
+F9B8;CJK COMPATIBILITY IDEOGRAPH-F9B8;Lo;0;L;96B8;;;;N;;;;;
+F9B9;CJK COMPATIBILITY IDEOGRAPH-F9B9;Lo;0;L;60E1;;;;N;;;;;
+F9BA;CJK COMPATIBILITY IDEOGRAPH-F9BA;Lo;0;L;4E86;;;;N;;;;;
+F9BB;CJK COMPATIBILITY IDEOGRAPH-F9BB;Lo;0;L;50DA;;;;N;;;;;
+F9BC;CJK COMPATIBILITY IDEOGRAPH-F9BC;Lo;0;L;5BEE;;;;N;;;;;
+F9BD;CJK COMPATIBILITY IDEOGRAPH-F9BD;Lo;0;L;5C3F;;;;N;;;;;
+F9BE;CJK COMPATIBILITY IDEOGRAPH-F9BE;Lo;0;L;6599;;;;N;;;;;
+F9BF;CJK COMPATIBILITY IDEOGRAPH-F9BF;Lo;0;L;6A02;;;;N;;;;;
+F9C0;CJK COMPATIBILITY IDEOGRAPH-F9C0;Lo;0;L;71CE;;;;N;;;;;
+F9C1;CJK COMPATIBILITY IDEOGRAPH-F9C1;Lo;0;L;7642;;;;N;;;;;
+F9C2;CJK COMPATIBILITY IDEOGRAPH-F9C2;Lo;0;L;84FC;;;;N;;;;;
+F9C3;CJK COMPATIBILITY IDEOGRAPH-F9C3;Lo;0;L;907C;;;;N;;;;;
+F9C4;CJK COMPATIBILITY IDEOGRAPH-F9C4;Lo;0;L;9F8D;;;;N;;;;;
+F9C5;CJK COMPATIBILITY IDEOGRAPH-F9C5;Lo;0;L;6688;;;;N;;;;;
+F9C6;CJK COMPATIBILITY IDEOGRAPH-F9C6;Lo;0;L;962E;;;;N;;;;;
+F9C7;CJK COMPATIBILITY IDEOGRAPH-F9C7;Lo;0;L;5289;;;;N;;;;;
+F9C8;CJK COMPATIBILITY IDEOGRAPH-F9C8;Lo;0;L;677B;;;;N;;;;;
+F9C9;CJK COMPATIBILITY IDEOGRAPH-F9C9;Lo;0;L;67F3;;;;N;;;;;
+F9CA;CJK COMPATIBILITY IDEOGRAPH-F9CA;Lo;0;L;6D41;;;;N;;;;;
+F9CB;CJK COMPATIBILITY IDEOGRAPH-F9CB;Lo;0;L;6E9C;;;;N;;;;;
+F9CC;CJK COMPATIBILITY IDEOGRAPH-F9CC;Lo;0;L;7409;;;;N;;;;;
+F9CD;CJK COMPATIBILITY IDEOGRAPH-F9CD;Lo;0;L;7559;;;;N;;;;;
+F9CE;CJK COMPATIBILITY IDEOGRAPH-F9CE;Lo;0;L;786B;;;;N;;;;;
+F9CF;CJK COMPATIBILITY IDEOGRAPH-F9CF;Lo;0;L;7D10;;;;N;;;;;
+F9D0;CJK COMPATIBILITY IDEOGRAPH-F9D0;Lo;0;L;985E;;;;N;;;;;
+F9D1;CJK COMPATIBILITY IDEOGRAPH-F9D1;Lo;0;L;516D;;;6;N;;;;;
+F9D2;CJK COMPATIBILITY IDEOGRAPH-F9D2;Lo;0;L;622E;;;;N;;;;;
+F9D3;CJK COMPATIBILITY IDEOGRAPH-F9D3;Lo;0;L;9678;;;6;N;;;;;
+F9D4;CJK COMPATIBILITY IDEOGRAPH-F9D4;Lo;0;L;502B;;;;N;;;;;
+F9D5;CJK COMPATIBILITY IDEOGRAPH-F9D5;Lo;0;L;5D19;;;;N;;;;;
+F9D6;CJK COMPATIBILITY IDEOGRAPH-F9D6;Lo;0;L;6DEA;;;;N;;;;;
+F9D7;CJK COMPATIBILITY IDEOGRAPH-F9D7;Lo;0;L;8F2A;;;;N;;;;;
+F9D8;CJK COMPATIBILITY IDEOGRAPH-F9D8;Lo;0;L;5F8B;;;;N;;;;;
+F9D9;CJK COMPATIBILITY IDEOGRAPH-F9D9;Lo;0;L;6144;;;;N;;;;;
+F9DA;CJK COMPATIBILITY IDEOGRAPH-F9DA;Lo;0;L;6817;;;;N;;;;;
+F9DB;CJK COMPATIBILITY IDEOGRAPH-F9DB;Lo;0;L;7387;;;;N;;;;;
+F9DC;CJK COMPATIBILITY IDEOGRAPH-F9DC;Lo;0;L;9686;;;;N;;;;;
+F9DD;CJK COMPATIBILITY IDEOGRAPH-F9DD;Lo;0;L;5229;;;;N;;;;;
+F9DE;CJK COMPATIBILITY IDEOGRAPH-F9DE;Lo;0;L;540F;;;;N;;;;;
+F9DF;CJK COMPATIBILITY IDEOGRAPH-F9DF;Lo;0;L;5C65;;;;N;;;;;
+F9E0;CJK COMPATIBILITY IDEOGRAPH-F9E0;Lo;0;L;6613;;;;N;;;;;
+F9E1;CJK COMPATIBILITY IDEOGRAPH-F9E1;Lo;0;L;674E;;;;N;;;;;
+F9E2;CJK COMPATIBILITY IDEOGRAPH-F9E2;Lo;0;L;68A8;;;;N;;;;;
+F9E3;CJK COMPATIBILITY IDEOGRAPH-F9E3;Lo;0;L;6CE5;;;;N;;;;;
+F9E4;CJK COMPATIBILITY IDEOGRAPH-F9E4;Lo;0;L;7406;;;;N;;;;;
+F9E5;CJK COMPATIBILITY IDEOGRAPH-F9E5;Lo;0;L;75E2;;;;N;;;;;
+F9E6;CJK COMPATIBILITY IDEOGRAPH-F9E6;Lo;0;L;7F79;;;;N;;;;;
+F9E7;CJK COMPATIBILITY IDEOGRAPH-F9E7;Lo;0;L;88CF;;;;N;;;;;
+F9E8;CJK COMPATIBILITY IDEOGRAPH-F9E8;Lo;0;L;88E1;;;;N;;;;;
+F9E9;CJK COMPATIBILITY IDEOGRAPH-F9E9;Lo;0;L;91CC;;;;N;;;;;
+F9EA;CJK COMPATIBILITY IDEOGRAPH-F9EA;Lo;0;L;96E2;;;;N;;;;;
+F9EB;CJK COMPATIBILITY IDEOGRAPH-F9EB;Lo;0;L;533F;;;;N;;;;;
+F9EC;CJK COMPATIBILITY IDEOGRAPH-F9EC;Lo;0;L;6EBA;;;;N;;;;;
+F9ED;CJK COMPATIBILITY IDEOGRAPH-F9ED;Lo;0;L;541D;;;;N;;;;;
+F9EE;CJK COMPATIBILITY IDEOGRAPH-F9EE;Lo;0;L;71D0;;;;N;;;;;
+F9EF;CJK COMPATIBILITY IDEOGRAPH-F9EF;Lo;0;L;7498;;;;N;;;;;
+F9F0;CJK COMPATIBILITY IDEOGRAPH-F9F0;Lo;0;L;85FA;;;;N;;;;;
+F9F1;CJK COMPATIBILITY IDEOGRAPH-F9F1;Lo;0;L;96A3;;;;N;;;;;
+F9F2;CJK COMPATIBILITY IDEOGRAPH-F9F2;Lo;0;L;9C57;;;;N;;;;;
+F9F3;CJK COMPATIBILITY IDEOGRAPH-F9F3;Lo;0;L;9E9F;;;;N;;;;;
+F9F4;CJK COMPATIBILITY IDEOGRAPH-F9F4;Lo;0;L;6797;;;;N;;;;;
+F9F5;CJK COMPATIBILITY IDEOGRAPH-F9F5;Lo;0;L;6DCB;;;;N;;;;;
+F9F6;CJK COMPATIBILITY IDEOGRAPH-F9F6;Lo;0;L;81E8;;;;N;;;;;
+F9F7;CJK COMPATIBILITY IDEOGRAPH-F9F7;Lo;0;L;7ACB;;;;N;;;;;
+F9F8;CJK COMPATIBILITY IDEOGRAPH-F9F8;Lo;0;L;7B20;;;;N;;;;;
+F9F9;CJK COMPATIBILITY IDEOGRAPH-F9F9;Lo;0;L;7C92;;;;N;;;;;
+F9FA;CJK COMPATIBILITY IDEOGRAPH-F9FA;Lo;0;L;72C0;;;;N;;;;;
+F9FB;CJK COMPATIBILITY IDEOGRAPH-F9FB;Lo;0;L;7099;;;;N;;;;;
+F9FC;CJK COMPATIBILITY IDEOGRAPH-F9FC;Lo;0;L;8B58;;;;N;;;;;
+F9FD;CJK COMPATIBILITY IDEOGRAPH-F9FD;Lo;0;L;4EC0;;;10;N;;;;;
+F9FE;CJK COMPATIBILITY IDEOGRAPH-F9FE;Lo;0;L;8336;;;;N;;;;;
+F9FF;CJK COMPATIBILITY IDEOGRAPH-F9FF;Lo;0;L;523A;;;;N;;;;;
+FA00;CJK COMPATIBILITY IDEOGRAPH-FA00;Lo;0;L;5207;;;;N;;;;;
+FA01;CJK COMPATIBILITY IDEOGRAPH-FA01;Lo;0;L;5EA6;;;;N;;;;;
+FA02;CJK COMPATIBILITY IDEOGRAPH-FA02;Lo;0;L;62D3;;;;N;;;;;
+FA03;CJK COMPATIBILITY IDEOGRAPH-FA03;Lo;0;L;7CD6;;;;N;;;;;
+FA04;CJK COMPATIBILITY IDEOGRAPH-FA04;Lo;0;L;5B85;;;;N;;;;;
+FA05;CJK COMPATIBILITY IDEOGRAPH-FA05;Lo;0;L;6D1E;;;;N;;;;;
+FA06;CJK COMPATIBILITY IDEOGRAPH-FA06;Lo;0;L;66B4;;;;N;;;;;
+FA07;CJK COMPATIBILITY IDEOGRAPH-FA07;Lo;0;L;8F3B;;;;N;;;;;
+FA08;CJK COMPATIBILITY IDEOGRAPH-FA08;Lo;0;L;884C;;;;N;;;;;
+FA09;CJK COMPATIBILITY IDEOGRAPH-FA09;Lo;0;L;964D;;;;N;;;;;
+FA0A;CJK COMPATIBILITY IDEOGRAPH-FA0A;Lo;0;L;898B;;;;N;;;;;
+FA0B;CJK COMPATIBILITY IDEOGRAPH-FA0B;Lo;0;L;5ED3;;;;N;;;;;
+FA0C;CJK COMPATIBILITY IDEOGRAPH-FA0C;Lo;0;L;5140;;;;N;;;;;
+FA0D;CJK COMPATIBILITY IDEOGRAPH-FA0D;Lo;0;L;55C0;;;;N;;;;;
+FA0E;CJK COMPATIBILITY IDEOGRAPH-FA0E;Lo;0;L;;;;;N;;;;;
+FA0F;CJK COMPATIBILITY IDEOGRAPH-FA0F;Lo;0;L;;;;;N;;;;;
+FA10;CJK COMPATIBILITY IDEOGRAPH-FA10;Lo;0;L;585A;;;;N;;;;;
+FA11;CJK COMPATIBILITY IDEOGRAPH-FA11;Lo;0;L;;;;;N;;;;;
+FA12;CJK COMPATIBILITY IDEOGRAPH-FA12;Lo;0;L;6674;;;;N;;;;;
+FA13;CJK COMPATIBILITY IDEOGRAPH-FA13;Lo;0;L;;;;;N;;;;;
+FA14;CJK COMPATIBILITY IDEOGRAPH-FA14;Lo;0;L;;;;;N;;;;;
+FA15;CJK COMPATIBILITY IDEOGRAPH-FA15;Lo;0;L;51DE;;;;N;;;;;
+FA16;CJK COMPATIBILITY IDEOGRAPH-FA16;Lo;0;L;732A;;;;N;;;;;
+FA17;CJK COMPATIBILITY IDEOGRAPH-FA17;Lo;0;L;76CA;;;;N;;;;;
+FA18;CJK COMPATIBILITY IDEOGRAPH-FA18;Lo;0;L;793C;;;;N;;;;;
+FA19;CJK COMPATIBILITY IDEOGRAPH-FA19;Lo;0;L;795E;;;;N;;;;;
+FA1A;CJK COMPATIBILITY IDEOGRAPH-FA1A;Lo;0;L;7965;;;;N;;;;;
+FA1B;CJK COMPATIBILITY IDEOGRAPH-FA1B;Lo;0;L;798F;;;;N;;;;;
+FA1C;CJK COMPATIBILITY IDEOGRAPH-FA1C;Lo;0;L;9756;;;;N;;;;;
+FA1D;CJK COMPATIBILITY IDEOGRAPH-FA1D;Lo;0;L;7CBE;;;;N;;;;;
+FA1E;CJK COMPATIBILITY IDEOGRAPH-FA1E;Lo;0;L;7FBD;;;;N;;;;;
+FA1F;CJK COMPATIBILITY IDEOGRAPH-FA1F;Lo;0;L;;;;;N;;;;;
+FA20;CJK COMPATIBILITY IDEOGRAPH-FA20;Lo;0;L;8612;;;;N;;;;;
+FA21;CJK COMPATIBILITY IDEOGRAPH-FA21;Lo;0;L;;;;;N;;;;;
+FA22;CJK COMPATIBILITY IDEOGRAPH-FA22;Lo;0;L;8AF8;;;;N;;;;;
+FA23;CJK COMPATIBILITY IDEOGRAPH-FA23;Lo;0;L;;;;;N;;;;;
+FA24;CJK COMPATIBILITY IDEOGRAPH-FA24;Lo;0;L;;;;;N;;;;;
+FA25;CJK COMPATIBILITY IDEOGRAPH-FA25;Lo;0;L;9038;;;;N;;;;;
+FA26;CJK COMPATIBILITY IDEOGRAPH-FA26;Lo;0;L;90FD;;;;N;;;;;
+FA27;CJK COMPATIBILITY IDEOGRAPH-FA27;Lo;0;L;;;;;N;;;;;
+FA28;CJK COMPATIBILITY IDEOGRAPH-FA28;Lo;0;L;;;;;N;;;;;
+FA29;CJK COMPATIBILITY IDEOGRAPH-FA29;Lo;0;L;;;;;N;;;;;
+FA2A;CJK COMPATIBILITY IDEOGRAPH-FA2A;Lo;0;L;98EF;;;;N;;;;;
+FA2B;CJK COMPATIBILITY IDEOGRAPH-FA2B;Lo;0;L;98FC;;;;N;;;;;
+FA2C;CJK COMPATIBILITY IDEOGRAPH-FA2C;Lo;0;L;9928;;;;N;;;;;
+FA2D;CJK COMPATIBILITY IDEOGRAPH-FA2D;Lo;0;L;9DB4;;;;N;;;;;
+FA2E;CJK COMPATIBILITY IDEOGRAPH-FA2E;Lo;0;L;90DE;;;;N;;;;;
+FA2F;CJK COMPATIBILITY IDEOGRAPH-FA2F;Lo;0;L;96B7;;;;N;;;;;
+FA30;CJK COMPATIBILITY IDEOGRAPH-FA30;Lo;0;L;4FAE;;;;N;;;;;
+FA31;CJK COMPATIBILITY IDEOGRAPH-FA31;Lo;0;L;50E7;;;;N;;;;;
+FA32;CJK COMPATIBILITY IDEOGRAPH-FA32;Lo;0;L;514D;;;;N;;;;;
+FA33;CJK COMPATIBILITY IDEOGRAPH-FA33;Lo;0;L;52C9;;;;N;;;;;
+FA34;CJK COMPATIBILITY IDEOGRAPH-FA34;Lo;0;L;52E4;;;;N;;;;;
+FA35;CJK COMPATIBILITY IDEOGRAPH-FA35;Lo;0;L;5351;;;;N;;;;;
+FA36;CJK COMPATIBILITY IDEOGRAPH-FA36;Lo;0;L;559D;;;;N;;;;;
+FA37;CJK COMPATIBILITY IDEOGRAPH-FA37;Lo;0;L;5606;;;;N;;;;;
+FA38;CJK COMPATIBILITY IDEOGRAPH-FA38;Lo;0;L;5668;;;;N;;;;;
+FA39;CJK COMPATIBILITY IDEOGRAPH-FA39;Lo;0;L;5840;;;;N;;;;;
+FA3A;CJK COMPATIBILITY IDEOGRAPH-FA3A;Lo;0;L;58A8;;;;N;;;;;
+FA3B;CJK COMPATIBILITY IDEOGRAPH-FA3B;Lo;0;L;5C64;;;;N;;;;;
+FA3C;CJK COMPATIBILITY IDEOGRAPH-FA3C;Lo;0;L;5C6E;;;;N;;;;;
+FA3D;CJK COMPATIBILITY IDEOGRAPH-FA3D;Lo;0;L;6094;;;;N;;;;;
+FA3E;CJK COMPATIBILITY IDEOGRAPH-FA3E;Lo;0;L;6168;;;;N;;;;;
+FA3F;CJK COMPATIBILITY IDEOGRAPH-FA3F;Lo;0;L;618E;;;;N;;;;;
+FA40;CJK COMPATIBILITY IDEOGRAPH-FA40;Lo;0;L;61F2;;;;N;;;;;
+FA41;CJK COMPATIBILITY IDEOGRAPH-FA41;Lo;0;L;654F;;;;N;;;;;
+FA42;CJK COMPATIBILITY IDEOGRAPH-FA42;Lo;0;L;65E2;;;;N;;;;;
+FA43;CJK COMPATIBILITY IDEOGRAPH-FA43;Lo;0;L;6691;;;;N;;;;;
+FA44;CJK COMPATIBILITY IDEOGRAPH-FA44;Lo;0;L;6885;;;;N;;;;;
+FA45;CJK COMPATIBILITY IDEOGRAPH-FA45;Lo;0;L;6D77;;;;N;;;;;
+FA46;CJK COMPATIBILITY IDEOGRAPH-FA46;Lo;0;L;6E1A;;;;N;;;;;
+FA47;CJK COMPATIBILITY IDEOGRAPH-FA47;Lo;0;L;6F22;;;;N;;;;;
+FA48;CJK COMPATIBILITY IDEOGRAPH-FA48;Lo;0;L;716E;;;;N;;;;;
+FA49;CJK COMPATIBILITY IDEOGRAPH-FA49;Lo;0;L;722B;;;;N;;;;;
+FA4A;CJK COMPATIBILITY IDEOGRAPH-FA4A;Lo;0;L;7422;;;;N;;;;;
+FA4B;CJK COMPATIBILITY IDEOGRAPH-FA4B;Lo;0;L;7891;;;;N;;;;;
+FA4C;CJK COMPATIBILITY IDEOGRAPH-FA4C;Lo;0;L;793E;;;;N;;;;;
+FA4D;CJK COMPATIBILITY IDEOGRAPH-FA4D;Lo;0;L;7949;;;;N;;;;;
+FA4E;CJK COMPATIBILITY IDEOGRAPH-FA4E;Lo;0;L;7948;;;;N;;;;;
+FA4F;CJK COMPATIBILITY IDEOGRAPH-FA4F;Lo;0;L;7950;;;;N;;;;;
+FA50;CJK COMPATIBILITY IDEOGRAPH-FA50;Lo;0;L;7956;;;;N;;;;;
+FA51;CJK COMPATIBILITY IDEOGRAPH-FA51;Lo;0;L;795D;;;;N;;;;;
+FA52;CJK COMPATIBILITY IDEOGRAPH-FA52;Lo;0;L;798D;;;;N;;;;;
+FA53;CJK COMPATIBILITY IDEOGRAPH-FA53;Lo;0;L;798E;;;;N;;;;;
+FA54;CJK COMPATIBILITY IDEOGRAPH-FA54;Lo;0;L;7A40;;;;N;;;;;
+FA55;CJK COMPATIBILITY IDEOGRAPH-FA55;Lo;0;L;7A81;;;;N;;;;;
+FA56;CJK COMPATIBILITY IDEOGRAPH-FA56;Lo;0;L;7BC0;;;;N;;;;;
+FA57;CJK COMPATIBILITY IDEOGRAPH-FA57;Lo;0;L;7DF4;;;;N;;;;;
+FA58;CJK COMPATIBILITY IDEOGRAPH-FA58;Lo;0;L;7E09;;;;N;;;;;
+FA59;CJK COMPATIBILITY IDEOGRAPH-FA59;Lo;0;L;7E41;;;;N;;;;;
+FA5A;CJK COMPATIBILITY IDEOGRAPH-FA5A;Lo;0;L;7F72;;;;N;;;;;
+FA5B;CJK COMPATIBILITY IDEOGRAPH-FA5B;Lo;0;L;8005;;;;N;;;;;
+FA5C;CJK COMPATIBILITY IDEOGRAPH-FA5C;Lo;0;L;81ED;;;;N;;;;;
+FA5D;CJK COMPATIBILITY IDEOGRAPH-FA5D;Lo;0;L;8279;;;;N;;;;;
+FA5E;CJK COMPATIBILITY IDEOGRAPH-FA5E;Lo;0;L;8279;;;;N;;;;;
+FA5F;CJK COMPATIBILITY IDEOGRAPH-FA5F;Lo;0;L;8457;;;;N;;;;;
+FA60;CJK COMPATIBILITY IDEOGRAPH-FA60;Lo;0;L;8910;;;;N;;;;;
+FA61;CJK COMPATIBILITY IDEOGRAPH-FA61;Lo;0;L;8996;;;;N;;;;;
+FA62;CJK COMPATIBILITY IDEOGRAPH-FA62;Lo;0;L;8B01;;;;N;;;;;
+FA63;CJK COMPATIBILITY IDEOGRAPH-FA63;Lo;0;L;8B39;;;;N;;;;;
+FA64;CJK COMPATIBILITY IDEOGRAPH-FA64;Lo;0;L;8CD3;;;;N;;;;;
+FA65;CJK COMPATIBILITY IDEOGRAPH-FA65;Lo;0;L;8D08;;;;N;;;;;
+FA66;CJK COMPATIBILITY IDEOGRAPH-FA66;Lo;0;L;8FB6;;;;N;;;;;
+FA67;CJK COMPATIBILITY IDEOGRAPH-FA67;Lo;0;L;9038;;;;N;;;;;
+FA68;CJK COMPATIBILITY IDEOGRAPH-FA68;Lo;0;L;96E3;;;;N;;;;;
+FA69;CJK COMPATIBILITY IDEOGRAPH-FA69;Lo;0;L;97FF;;;;N;;;;;
+FA6A;CJK COMPATIBILITY IDEOGRAPH-FA6A;Lo;0;L;983B;;;;N;;;;;
+FA6B;CJK COMPATIBILITY IDEOGRAPH-FA6B;Lo;0;L;6075;;;;N;;;;;
+FA6C;CJK COMPATIBILITY IDEOGRAPH-FA6C;Lo;0;L;242EE;;;;N;;;;;
+FA6D;CJK COMPATIBILITY IDEOGRAPH-FA6D;Lo;0;L;8218;;;;N;;;;;
+FA70;CJK COMPATIBILITY IDEOGRAPH-FA70;Lo;0;L;4E26;;;;N;;;;;
+FA71;CJK COMPATIBILITY IDEOGRAPH-FA71;Lo;0;L;51B5;;;;N;;;;;
+FA72;CJK COMPATIBILITY IDEOGRAPH-FA72;Lo;0;L;5168;;;;N;;;;;
+FA73;CJK COMPATIBILITY IDEOGRAPH-FA73;Lo;0;L;4F80;;;;N;;;;;
+FA74;CJK COMPATIBILITY IDEOGRAPH-FA74;Lo;0;L;5145;;;;N;;;;;
+FA75;CJK COMPATIBILITY IDEOGRAPH-FA75;Lo;0;L;5180;;;;N;;;;;
+FA76;CJK COMPATIBILITY IDEOGRAPH-FA76;Lo;0;L;52C7;;;;N;;;;;
+FA77;CJK COMPATIBILITY IDEOGRAPH-FA77;Lo;0;L;52FA;;;;N;;;;;
+FA78;CJK COMPATIBILITY IDEOGRAPH-FA78;Lo;0;L;559D;;;;N;;;;;
+FA79;CJK COMPATIBILITY IDEOGRAPH-FA79;Lo;0;L;5555;;;;N;;;;;
+FA7A;CJK COMPATIBILITY IDEOGRAPH-FA7A;Lo;0;L;5599;;;;N;;;;;
+FA7B;CJK COMPATIBILITY IDEOGRAPH-FA7B;Lo;0;L;55E2;;;;N;;;;;
+FA7C;CJK COMPATIBILITY IDEOGRAPH-FA7C;Lo;0;L;585A;;;;N;;;;;
+FA7D;CJK COMPATIBILITY IDEOGRAPH-FA7D;Lo;0;L;58B3;;;;N;;;;;
+FA7E;CJK COMPATIBILITY IDEOGRAPH-FA7E;Lo;0;L;5944;;;;N;;;;;
+FA7F;CJK COMPATIBILITY IDEOGRAPH-FA7F;Lo;0;L;5954;;;;N;;;;;
+FA80;CJK COMPATIBILITY IDEOGRAPH-FA80;Lo;0;L;5A62;;;;N;;;;;
+FA81;CJK COMPATIBILITY IDEOGRAPH-FA81;Lo;0;L;5B28;;;;N;;;;;
+FA82;CJK COMPATIBILITY IDEOGRAPH-FA82;Lo;0;L;5ED2;;;;N;;;;;
+FA83;CJK COMPATIBILITY IDEOGRAPH-FA83;Lo;0;L;5ED9;;;;N;;;;;
+FA84;CJK COMPATIBILITY IDEOGRAPH-FA84;Lo;0;L;5F69;;;;N;;;;;
+FA85;CJK COMPATIBILITY IDEOGRAPH-FA85;Lo;0;L;5FAD;;;;N;;;;;
+FA86;CJK COMPATIBILITY IDEOGRAPH-FA86;Lo;0;L;60D8;;;;N;;;;;
+FA87;CJK COMPATIBILITY IDEOGRAPH-FA87;Lo;0;L;614E;;;;N;;;;;
+FA88;CJK COMPATIBILITY IDEOGRAPH-FA88;Lo;0;L;6108;;;;N;;;;;
+FA89;CJK COMPATIBILITY IDEOGRAPH-FA89;Lo;0;L;618E;;;;N;;;;;
+FA8A;CJK COMPATIBILITY IDEOGRAPH-FA8A;Lo;0;L;6160;;;;N;;;;;
+FA8B;CJK COMPATIBILITY IDEOGRAPH-FA8B;Lo;0;L;61F2;;;;N;;;;;
+FA8C;CJK COMPATIBILITY IDEOGRAPH-FA8C;Lo;0;L;6234;;;;N;;;;;
+FA8D;CJK COMPATIBILITY IDEOGRAPH-FA8D;Lo;0;L;63C4;;;;N;;;;;
+FA8E;CJK COMPATIBILITY IDEOGRAPH-FA8E;Lo;0;L;641C;;;;N;;;;;
+FA8F;CJK COMPATIBILITY IDEOGRAPH-FA8F;Lo;0;L;6452;;;;N;;;;;
+FA90;CJK COMPATIBILITY IDEOGRAPH-FA90;Lo;0;L;6556;;;;N;;;;;
+FA91;CJK COMPATIBILITY IDEOGRAPH-FA91;Lo;0;L;6674;;;;N;;;;;
+FA92;CJK COMPATIBILITY IDEOGRAPH-FA92;Lo;0;L;6717;;;;N;;;;;
+FA93;CJK COMPATIBILITY IDEOGRAPH-FA93;Lo;0;L;671B;;;;N;;;;;
+FA94;CJK COMPATIBILITY IDEOGRAPH-FA94;Lo;0;L;6756;;;;N;;;;;
+FA95;CJK COMPATIBILITY IDEOGRAPH-FA95;Lo;0;L;6B79;;;;N;;;;;
+FA96;CJK COMPATIBILITY IDEOGRAPH-FA96;Lo;0;L;6BBA;;;;N;;;;;
+FA97;CJK COMPATIBILITY IDEOGRAPH-FA97;Lo;0;L;6D41;;;;N;;;;;
+FA98;CJK COMPATIBILITY IDEOGRAPH-FA98;Lo;0;L;6EDB;;;;N;;;;;
+FA99;CJK COMPATIBILITY IDEOGRAPH-FA99;Lo;0;L;6ECB;;;;N;;;;;
+FA9A;CJK COMPATIBILITY IDEOGRAPH-FA9A;Lo;0;L;6F22;;;;N;;;;;
+FA9B;CJK COMPATIBILITY IDEOGRAPH-FA9B;Lo;0;L;701E;;;;N;;;;;
+FA9C;CJK COMPATIBILITY IDEOGRAPH-FA9C;Lo;0;L;716E;;;;N;;;;;
+FA9D;CJK COMPATIBILITY IDEOGRAPH-FA9D;Lo;0;L;77A7;;;;N;;;;;
+FA9E;CJK COMPATIBILITY IDEOGRAPH-FA9E;Lo;0;L;7235;;;;N;;;;;
+FA9F;CJK COMPATIBILITY IDEOGRAPH-FA9F;Lo;0;L;72AF;;;;N;;;;;
+FAA0;CJK COMPATIBILITY IDEOGRAPH-FAA0;Lo;0;L;732A;;;;N;;;;;
+FAA1;CJK COMPATIBILITY IDEOGRAPH-FAA1;Lo;0;L;7471;;;;N;;;;;
+FAA2;CJK COMPATIBILITY IDEOGRAPH-FAA2;Lo;0;L;7506;;;;N;;;;;
+FAA3;CJK COMPATIBILITY IDEOGRAPH-FAA3;Lo;0;L;753B;;;;N;;;;;
+FAA4;CJK COMPATIBILITY IDEOGRAPH-FAA4;Lo;0;L;761D;;;;N;;;;;
+FAA5;CJK COMPATIBILITY IDEOGRAPH-FAA5;Lo;0;L;761F;;;;N;;;;;
+FAA6;CJK COMPATIBILITY IDEOGRAPH-FAA6;Lo;0;L;76CA;;;;N;;;;;
+FAA7;CJK COMPATIBILITY IDEOGRAPH-FAA7;Lo;0;L;76DB;;;;N;;;;;
+FAA8;CJK COMPATIBILITY IDEOGRAPH-FAA8;Lo;0;L;76F4;;;;N;;;;;
+FAA9;CJK COMPATIBILITY IDEOGRAPH-FAA9;Lo;0;L;774A;;;;N;;;;;
+FAAA;CJK COMPATIBILITY IDEOGRAPH-FAAA;Lo;0;L;7740;;;;N;;;;;
+FAAB;CJK COMPATIBILITY IDEOGRAPH-FAAB;Lo;0;L;78CC;;;;N;;;;;
+FAAC;CJK COMPATIBILITY IDEOGRAPH-FAAC;Lo;0;L;7AB1;;;;N;;;;;
+FAAD;CJK COMPATIBILITY IDEOGRAPH-FAAD;Lo;0;L;7BC0;;;;N;;;;;
+FAAE;CJK COMPATIBILITY IDEOGRAPH-FAAE;Lo;0;L;7C7B;;;;N;;;;;
+FAAF;CJK COMPATIBILITY IDEOGRAPH-FAAF;Lo;0;L;7D5B;;;;N;;;;;
+FAB0;CJK COMPATIBILITY IDEOGRAPH-FAB0;Lo;0;L;7DF4;;;;N;;;;;
+FAB1;CJK COMPATIBILITY IDEOGRAPH-FAB1;Lo;0;L;7F3E;;;;N;;;;;
+FAB2;CJK COMPATIBILITY IDEOGRAPH-FAB2;Lo;0;L;8005;;;;N;;;;;
+FAB3;CJK COMPATIBILITY IDEOGRAPH-FAB3;Lo;0;L;8352;;;;N;;;;;
+FAB4;CJK COMPATIBILITY IDEOGRAPH-FAB4;Lo;0;L;83EF;;;;N;;;;;
+FAB5;CJK COMPATIBILITY IDEOGRAPH-FAB5;Lo;0;L;8779;;;;N;;;;;
+FAB6;CJK COMPATIBILITY IDEOGRAPH-FAB6;Lo;0;L;8941;;;;N;;;;;
+FAB7;CJK COMPATIBILITY IDEOGRAPH-FAB7;Lo;0;L;8986;;;;N;;;;;
+FAB8;CJK COMPATIBILITY IDEOGRAPH-FAB8;Lo;0;L;8996;;;;N;;;;;
+FAB9;CJK COMPATIBILITY IDEOGRAPH-FAB9;Lo;0;L;8ABF;;;;N;;;;;
+FABA;CJK COMPATIBILITY IDEOGRAPH-FABA;Lo;0;L;8AF8;;;;N;;;;;
+FABB;CJK COMPATIBILITY IDEOGRAPH-FABB;Lo;0;L;8ACB;;;;N;;;;;
+FABC;CJK COMPATIBILITY IDEOGRAPH-FABC;Lo;0;L;8B01;;;;N;;;;;
+FABD;CJK COMPATIBILITY IDEOGRAPH-FABD;Lo;0;L;8AFE;;;;N;;;;;
+FABE;CJK COMPATIBILITY IDEOGRAPH-FABE;Lo;0;L;8AED;;;;N;;;;;
+FABF;CJK COMPATIBILITY IDEOGRAPH-FABF;Lo;0;L;8B39;;;;N;;;;;
+FAC0;CJK COMPATIBILITY IDEOGRAPH-FAC0;Lo;0;L;8B8A;;;;N;;;;;
+FAC1;CJK COMPATIBILITY IDEOGRAPH-FAC1;Lo;0;L;8D08;;;;N;;;;;
+FAC2;CJK COMPATIBILITY IDEOGRAPH-FAC2;Lo;0;L;8F38;;;;N;;;;;
+FAC3;CJK COMPATIBILITY IDEOGRAPH-FAC3;Lo;0;L;9072;;;;N;;;;;
+FAC4;CJK COMPATIBILITY IDEOGRAPH-FAC4;Lo;0;L;9199;;;;N;;;;;
+FAC5;CJK COMPATIBILITY IDEOGRAPH-FAC5;Lo;0;L;9276;;;;N;;;;;
+FAC6;CJK COMPATIBILITY IDEOGRAPH-FAC6;Lo;0;L;967C;;;;N;;;;;
+FAC7;CJK COMPATIBILITY IDEOGRAPH-FAC7;Lo;0;L;96E3;;;;N;;;;;
+FAC8;CJK COMPATIBILITY IDEOGRAPH-FAC8;Lo;0;L;9756;;;;N;;;;;
+FAC9;CJK COMPATIBILITY IDEOGRAPH-FAC9;Lo;0;L;97DB;;;;N;;;;;
+FACA;CJK COMPATIBILITY IDEOGRAPH-FACA;Lo;0;L;97FF;;;;N;;;;;
+FACB;CJK COMPATIBILITY IDEOGRAPH-FACB;Lo;0;L;980B;;;;N;;;;;
+FACC;CJK COMPATIBILITY IDEOGRAPH-FACC;Lo;0;L;983B;;;;N;;;;;
+FACD;CJK COMPATIBILITY IDEOGRAPH-FACD;Lo;0;L;9B12;;;;N;;;;;
+FACE;CJK COMPATIBILITY IDEOGRAPH-FACE;Lo;0;L;9F9C;;;;N;;;;;
+FACF;CJK COMPATIBILITY IDEOGRAPH-FACF;Lo;0;L;2284A;;;;N;;;;;
+FAD0;CJK COMPATIBILITY IDEOGRAPH-FAD0;Lo;0;L;22844;;;;N;;;;;
+FAD1;CJK COMPATIBILITY IDEOGRAPH-FAD1;Lo;0;L;233D5;;;;N;;;;;
+FAD2;CJK COMPATIBILITY IDEOGRAPH-FAD2;Lo;0;L;3B9D;;;;N;;;;;
+FAD3;CJK COMPATIBILITY IDEOGRAPH-FAD3;Lo;0;L;4018;;;;N;;;;;
+FAD4;CJK COMPATIBILITY IDEOGRAPH-FAD4;Lo;0;L;4039;;;;N;;;;;
+FAD5;CJK COMPATIBILITY IDEOGRAPH-FAD5;Lo;0;L;25249;;;;N;;;;;
+FAD6;CJK COMPATIBILITY IDEOGRAPH-FAD6;Lo;0;L;25CD0;;;;N;;;;;
+FAD7;CJK COMPATIBILITY IDEOGRAPH-FAD7;Lo;0;L;27ED3;;;;N;;;;;
+FAD8;CJK COMPATIBILITY IDEOGRAPH-FAD8;Lo;0;L;9F43;;;;N;;;;;
+FAD9;CJK COMPATIBILITY IDEOGRAPH-FAD9;Lo;0;L;9F8E;;;;N;;;;;
+FB00;LATIN SMALL LIGATURE FF;Ll;0;L;<compat> 0066 0066;;;;N;;;;;
+FB01;LATIN SMALL LIGATURE FI;Ll;0;L;<compat> 0066 0069;;;;N;;;;;
+FB02;LATIN SMALL LIGATURE FL;Ll;0;L;<compat> 0066 006C;;;;N;;;;;
+FB03;LATIN SMALL LIGATURE FFI;Ll;0;L;<compat> 0066 0066 0069;;;;N;;;;;
+FB04;LATIN SMALL LIGATURE FFL;Ll;0;L;<compat> 0066 0066 006C;;;;N;;;;;
+FB05;LATIN SMALL LIGATURE LONG S T;Ll;0;L;<compat> 017F 0074;;;;N;;;;;
+FB06;LATIN SMALL LIGATURE ST;Ll;0;L;<compat> 0073 0074;;;;N;;;;;
+FB13;ARMENIAN SMALL LIGATURE MEN NOW;Ll;0;L;<compat> 0574 0576;;;;N;;;;;
+FB14;ARMENIAN SMALL LIGATURE MEN ECH;Ll;0;L;<compat> 0574 0565;;;;N;;;;;
+FB15;ARMENIAN SMALL LIGATURE MEN INI;Ll;0;L;<compat> 0574 056B;;;;N;;;;;
+FB16;ARMENIAN SMALL LIGATURE VEW NOW;Ll;0;L;<compat> 057E 0576;;;;N;;;;;
+FB17;ARMENIAN SMALL LIGATURE MEN XEH;Ll;0;L;<compat> 0574 056D;;;;N;;;;;
+FB1D;HEBREW LETTER YOD WITH HIRIQ;Lo;0;R;05D9 05B4;;;;N;;;;;
+FB1E;HEBREW POINT JUDEO-SPANISH VARIKA;Mn;26;NSM;;;;;N;HEBREW POINT VARIKA;;;;
+FB1F;HEBREW LIGATURE YIDDISH YOD YOD PATAH;Lo;0;R;05F2 05B7;;;;N;;;;;
+FB20;HEBREW LETTER ALTERNATIVE AYIN;Lo;0;R;<font> 05E2;;;;N;;;;;
+FB21;HEBREW LETTER WIDE ALEF;Lo;0;R;<font> 05D0;;;;N;;;;;
+FB22;HEBREW LETTER WIDE DALET;Lo;0;R;<font> 05D3;;;;N;;;;;
+FB23;HEBREW LETTER WIDE HE;Lo;0;R;<font> 05D4;;;;N;;;;;
+FB24;HEBREW LETTER WIDE KAF;Lo;0;R;<font> 05DB;;;;N;;;;;
+FB25;HEBREW LETTER WIDE LAMED;Lo;0;R;<font> 05DC;;;;N;;;;;
+FB26;HEBREW LETTER WIDE FINAL MEM;Lo;0;R;<font> 05DD;;;;N;;;;;
+FB27;HEBREW LETTER WIDE RESH;Lo;0;R;<font> 05E8;;;;N;;;;;
+FB28;HEBREW LETTER WIDE TAV;Lo;0;R;<font> 05EA;;;;N;;;;;
+FB29;HEBREW LETTER ALTERNATIVE PLUS SIGN;Sm;0;ES;<font> 002B;;;;N;;;;;
+FB2A;HEBREW LETTER SHIN WITH SHIN DOT;Lo;0;R;05E9 05C1;;;;N;;;;;
+FB2B;HEBREW LETTER SHIN WITH SIN DOT;Lo;0;R;05E9 05C2;;;;N;;;;;
+FB2C;HEBREW LETTER SHIN WITH DAGESH AND SHIN DOT;Lo;0;R;FB49 05C1;;;;N;;;;;
+FB2D;HEBREW LETTER SHIN WITH DAGESH AND SIN DOT;Lo;0;R;FB49 05C2;;;;N;;;;;
+FB2E;HEBREW LETTER ALEF WITH PATAH;Lo;0;R;05D0 05B7;;;;N;;;;;
+FB2F;HEBREW LETTER ALEF WITH QAMATS;Lo;0;R;05D0 05B8;;;;N;;;;;
+FB30;HEBREW LETTER ALEF WITH MAPIQ;Lo;0;R;05D0 05BC;;;;N;;;;;
+FB31;HEBREW LETTER BET WITH DAGESH;Lo;0;R;05D1 05BC;;;;N;;;;;
+FB32;HEBREW LETTER GIMEL WITH DAGESH;Lo;0;R;05D2 05BC;;;;N;;;;;
+FB33;HEBREW LETTER DALET WITH DAGESH;Lo;0;R;05D3 05BC;;;;N;;;;;
+FB34;HEBREW LETTER HE WITH MAPIQ;Lo;0;R;05D4 05BC;;;;N;;;;;
+FB35;HEBREW LETTER VAV WITH DAGESH;Lo;0;R;05D5 05BC;;;;N;;;;;
+FB36;HEBREW LETTER ZAYIN WITH DAGESH;Lo;0;R;05D6 05BC;;;;N;;;;;
+FB38;HEBREW LETTER TET WITH DAGESH;Lo;0;R;05D8 05BC;;;;N;;;;;
+FB39;HEBREW LETTER YOD WITH DAGESH;Lo;0;R;05D9 05BC;;;;N;;;;;
+FB3A;HEBREW LETTER FINAL KAF WITH DAGESH;Lo;0;R;05DA 05BC;;;;N;;;;;
+FB3B;HEBREW LETTER KAF WITH DAGESH;Lo;0;R;05DB 05BC;;;;N;;;;;
+FB3C;HEBREW LETTER LAMED WITH DAGESH;Lo;0;R;05DC 05BC;;;;N;;;;;
+FB3E;HEBREW LETTER MEM WITH DAGESH;Lo;0;R;05DE 05BC;;;;N;;;;;
+FB40;HEBREW LETTER NUN WITH DAGESH;Lo;0;R;05E0 05BC;;;;N;;;;;
+FB41;HEBREW LETTER SAMEKH WITH DAGESH;Lo;0;R;05E1 05BC;;;;N;;;;;
+FB43;HEBREW LETTER FINAL PE WITH DAGESH;Lo;0;R;05E3 05BC;;;;N;;;;;
+FB44;HEBREW LETTER PE WITH DAGESH;Lo;0;R;05E4 05BC;;;;N;;;;;
+FB46;HEBREW LETTER TSADI WITH DAGESH;Lo;0;R;05E6 05BC;;;;N;;;;;
+FB47;HEBREW LETTER QOF WITH DAGESH;Lo;0;R;05E7 05BC;;;;N;;;;;
+FB48;HEBREW LETTER RESH WITH DAGESH;Lo;0;R;05E8 05BC;;;;N;;;;;
+FB49;HEBREW LETTER SHIN WITH DAGESH;Lo;0;R;05E9 05BC;;;;N;;;;;
+FB4A;HEBREW LETTER TAV WITH DAGESH;Lo;0;R;05EA 05BC;;;;N;;;;;
+FB4B;HEBREW LETTER VAV WITH HOLAM;Lo;0;R;05D5 05B9;;;;N;;;;;
+FB4C;HEBREW LETTER BET WITH RAFE;Lo;0;R;05D1 05BF;;;;N;;;;;
+FB4D;HEBREW LETTER KAF WITH RAFE;Lo;0;R;05DB 05BF;;;;N;;;;;
+FB4E;HEBREW LETTER PE WITH RAFE;Lo;0;R;05E4 05BF;;;;N;;;;;
+FB4F;HEBREW LIGATURE ALEF LAMED;Lo;0;R;<compat> 05D0 05DC;;;;N;;;;;
+FB50;ARABIC LETTER ALEF WASLA ISOLATED FORM;Lo;0;AL;<isolated> 0671;;;;N;;;;;
+FB51;ARABIC LETTER ALEF WASLA FINAL FORM;Lo;0;AL;<final> 0671;;;;N;;;;;
+FB52;ARABIC LETTER BEEH ISOLATED FORM;Lo;0;AL;<isolated> 067B;;;;N;;;;;
+FB53;ARABIC LETTER BEEH FINAL FORM;Lo;0;AL;<final> 067B;;;;N;;;;;
+FB54;ARABIC LETTER BEEH INITIAL FORM;Lo;0;AL;<initial> 067B;;;;N;;;;;
+FB55;ARABIC LETTER BEEH MEDIAL FORM;Lo;0;AL;<medial> 067B;;;;N;;;;;
+FB56;ARABIC LETTER PEH ISOLATED FORM;Lo;0;AL;<isolated> 067E;;;;N;;;;;
+FB57;ARABIC LETTER PEH FINAL FORM;Lo;0;AL;<final> 067E;;;;N;;;;;
+FB58;ARABIC LETTER PEH INITIAL FORM;Lo;0;AL;<initial> 067E;;;;N;;;;;
+FB59;ARABIC LETTER PEH MEDIAL FORM;Lo;0;AL;<medial> 067E;;;;N;;;;;
+FB5A;ARABIC LETTER BEHEH ISOLATED FORM;Lo;0;AL;<isolated> 0680;;;;N;;;;;
+FB5B;ARABIC LETTER BEHEH FINAL FORM;Lo;0;AL;<final> 0680;;;;N;;;;;
+FB5C;ARABIC LETTER BEHEH INITIAL FORM;Lo;0;AL;<initial> 0680;;;;N;;;;;
+FB5D;ARABIC LETTER BEHEH MEDIAL FORM;Lo;0;AL;<medial> 0680;;;;N;;;;;
+FB5E;ARABIC LETTER TTEHEH ISOLATED FORM;Lo;0;AL;<isolated> 067A;;;;N;;;;;
+FB5F;ARABIC LETTER TTEHEH FINAL FORM;Lo;0;AL;<final> 067A;;;;N;;;;;
+FB60;ARABIC LETTER TTEHEH INITIAL FORM;Lo;0;AL;<initial> 067A;;;;N;;;;;
+FB61;ARABIC LETTER TTEHEH MEDIAL FORM;Lo;0;AL;<medial> 067A;;;;N;;;;;
+FB62;ARABIC LETTER TEHEH ISOLATED FORM;Lo;0;AL;<isolated> 067F;;;;N;;;;;
+FB63;ARABIC LETTER TEHEH FINAL FORM;Lo;0;AL;<final> 067F;;;;N;;;;;
+FB64;ARABIC LETTER TEHEH INITIAL FORM;Lo;0;AL;<initial> 067F;;;;N;;;;;
+FB65;ARABIC LETTER TEHEH MEDIAL FORM;Lo;0;AL;<medial> 067F;;;;N;;;;;
+FB66;ARABIC LETTER TTEH ISOLATED FORM;Lo;0;AL;<isolated> 0679;;;;N;;;;;
+FB67;ARABIC LETTER TTEH FINAL FORM;Lo;0;AL;<final> 0679;;;;N;;;;;
+FB68;ARABIC LETTER TTEH INITIAL FORM;Lo;0;AL;<initial> 0679;;;;N;;;;;
+FB69;ARABIC LETTER TTEH MEDIAL FORM;Lo;0;AL;<medial> 0679;;;;N;;;;;
+FB6A;ARABIC LETTER VEH ISOLATED FORM;Lo;0;AL;<isolated> 06A4;;;;N;;;;;
+FB6B;ARABIC LETTER VEH FINAL FORM;Lo;0;AL;<final> 06A4;;;;N;;;;;
+FB6C;ARABIC LETTER VEH INITIAL FORM;Lo;0;AL;<initial> 06A4;;;;N;;;;;
+FB6D;ARABIC LETTER VEH MEDIAL FORM;Lo;0;AL;<medial> 06A4;;;;N;;;;;
+FB6E;ARABIC LETTER PEHEH ISOLATED FORM;Lo;0;AL;<isolated> 06A6;;;;N;;;;;
+FB6F;ARABIC LETTER PEHEH FINAL FORM;Lo;0;AL;<final> 06A6;;;;N;;;;;
+FB70;ARABIC LETTER PEHEH INITIAL FORM;Lo;0;AL;<initial> 06A6;;;;N;;;;;
+FB71;ARABIC LETTER PEHEH MEDIAL FORM;Lo;0;AL;<medial> 06A6;;;;N;;;;;
+FB72;ARABIC LETTER DYEH ISOLATED FORM;Lo;0;AL;<isolated> 0684;;;;N;;;;;
+FB73;ARABIC LETTER DYEH FINAL FORM;Lo;0;AL;<final> 0684;;;;N;;;;;
+FB74;ARABIC LETTER DYEH INITIAL FORM;Lo;0;AL;<initial> 0684;;;;N;;;;;
+FB75;ARABIC LETTER DYEH MEDIAL FORM;Lo;0;AL;<medial> 0684;;;;N;;;;;
+FB76;ARABIC LETTER NYEH ISOLATED FORM;Lo;0;AL;<isolated> 0683;;;;N;;;;;
+FB77;ARABIC LETTER NYEH FINAL FORM;Lo;0;AL;<final> 0683;;;;N;;;;;
+FB78;ARABIC LETTER NYEH INITIAL FORM;Lo;0;AL;<initial> 0683;;;;N;;;;;
+FB79;ARABIC LETTER NYEH MEDIAL FORM;Lo;0;AL;<medial> 0683;;;;N;;;;;
+FB7A;ARABIC LETTER TCHEH ISOLATED FORM;Lo;0;AL;<isolated> 0686;;;;N;;;;;
+FB7B;ARABIC LETTER TCHEH FINAL FORM;Lo;0;AL;<final> 0686;;;;N;;;;;
+FB7C;ARABIC LETTER TCHEH INITIAL FORM;Lo;0;AL;<initial> 0686;;;;N;;;;;
+FB7D;ARABIC LETTER TCHEH MEDIAL FORM;Lo;0;AL;<medial> 0686;;;;N;;;;;
+FB7E;ARABIC LETTER TCHEHEH ISOLATED FORM;Lo;0;AL;<isolated> 0687;;;;N;;;;;
+FB7F;ARABIC LETTER TCHEHEH FINAL FORM;Lo;0;AL;<final> 0687;;;;N;;;;;
+FB80;ARABIC LETTER TCHEHEH INITIAL FORM;Lo;0;AL;<initial> 0687;;;;N;;;;;
+FB81;ARABIC LETTER TCHEHEH MEDIAL FORM;Lo;0;AL;<medial> 0687;;;;N;;;;;
+FB82;ARABIC LETTER DDAHAL ISOLATED FORM;Lo;0;AL;<isolated> 068D;;;;N;;;;;
+FB83;ARABIC LETTER DDAHAL FINAL FORM;Lo;0;AL;<final> 068D;;;;N;;;;;
+FB84;ARABIC LETTER DAHAL ISOLATED FORM;Lo;0;AL;<isolated> 068C;;;;N;;;;;
+FB85;ARABIC LETTER DAHAL FINAL FORM;Lo;0;AL;<final> 068C;;;;N;;;;;
+FB86;ARABIC LETTER DUL ISOLATED FORM;Lo;0;AL;<isolated> 068E;;;;N;;;;;
+FB87;ARABIC LETTER DUL FINAL FORM;Lo;0;AL;<final> 068E;;;;N;;;;;
+FB88;ARABIC LETTER DDAL ISOLATED FORM;Lo;0;AL;<isolated> 0688;;;;N;;;;;
+FB89;ARABIC LETTER DDAL FINAL FORM;Lo;0;AL;<final> 0688;;;;N;;;;;
+FB8A;ARABIC LETTER JEH ISOLATED FORM;Lo;0;AL;<isolated> 0698;;;;N;;;;;
+FB8B;ARABIC LETTER JEH FINAL FORM;Lo;0;AL;<final> 0698;;;;N;;;;;
+FB8C;ARABIC LETTER RREH ISOLATED FORM;Lo;0;AL;<isolated> 0691;;;;N;;;;;
+FB8D;ARABIC LETTER RREH FINAL FORM;Lo;0;AL;<final> 0691;;;;N;;;;;
+FB8E;ARABIC LETTER KEHEH ISOLATED FORM;Lo;0;AL;<isolated> 06A9;;;;N;;;;;
+FB8F;ARABIC LETTER KEHEH FINAL FORM;Lo;0;AL;<final> 06A9;;;;N;;;;;
+FB90;ARABIC LETTER KEHEH INITIAL FORM;Lo;0;AL;<initial> 06A9;;;;N;;;;;
+FB91;ARABIC LETTER KEHEH MEDIAL FORM;Lo;0;AL;<medial> 06A9;;;;N;;;;;
+FB92;ARABIC LETTER GAF ISOLATED FORM;Lo;0;AL;<isolated> 06AF;;;;N;;;;;
+FB93;ARABIC LETTER GAF FINAL FORM;Lo;0;AL;<final> 06AF;;;;N;;;;;
+FB94;ARABIC LETTER GAF INITIAL FORM;Lo;0;AL;<initial> 06AF;;;;N;;;;;
+FB95;ARABIC LETTER GAF MEDIAL FORM;Lo;0;AL;<medial> 06AF;;;;N;;;;;
+FB96;ARABIC LETTER GUEH ISOLATED FORM;Lo;0;AL;<isolated> 06B3;;;;N;;;;;
+FB97;ARABIC LETTER GUEH FINAL FORM;Lo;0;AL;<final> 06B3;;;;N;;;;;
+FB98;ARABIC LETTER GUEH INITIAL FORM;Lo;0;AL;<initial> 06B3;;;;N;;;;;
+FB99;ARABIC LETTER GUEH MEDIAL FORM;Lo;0;AL;<medial> 06B3;;;;N;;;;;
+FB9A;ARABIC LETTER NGOEH ISOLATED FORM;Lo;0;AL;<isolated> 06B1;;;;N;;;;;
+FB9B;ARABIC LETTER NGOEH FINAL FORM;Lo;0;AL;<final> 06B1;;;;N;;;;;
+FB9C;ARABIC LETTER NGOEH INITIAL FORM;Lo;0;AL;<initial> 06B1;;;;N;;;;;
+FB9D;ARABIC LETTER NGOEH MEDIAL FORM;Lo;0;AL;<medial> 06B1;;;;N;;;;;
+FB9E;ARABIC LETTER NOON GHUNNA ISOLATED FORM;Lo;0;AL;<isolated> 06BA;;;;N;;;;;
+FB9F;ARABIC LETTER NOON GHUNNA FINAL FORM;Lo;0;AL;<final> 06BA;;;;N;;;;;
+FBA0;ARABIC LETTER RNOON ISOLATED FORM;Lo;0;AL;<isolated> 06BB;;;;N;;;;;
+FBA1;ARABIC LETTER RNOON FINAL FORM;Lo;0;AL;<final> 06BB;;;;N;;;;;
+FBA2;ARABIC LETTER RNOON INITIAL FORM;Lo;0;AL;<initial> 06BB;;;;N;;;;;
+FBA3;ARABIC LETTER RNOON MEDIAL FORM;Lo;0;AL;<medial> 06BB;;;;N;;;;;
+FBA4;ARABIC LETTER HEH WITH YEH ABOVE ISOLATED FORM;Lo;0;AL;<isolated> 06C0;;;;N;;;;;
+FBA5;ARABIC LETTER HEH WITH YEH ABOVE FINAL FORM;Lo;0;AL;<final> 06C0;;;;N;;;;;
+FBA6;ARABIC LETTER HEH GOAL ISOLATED FORM;Lo;0;AL;<isolated> 06C1;;;;N;;;;;
+FBA7;ARABIC LETTER HEH GOAL FINAL FORM;Lo;0;AL;<final> 06C1;;;;N;;;;;
+FBA8;ARABIC LETTER HEH GOAL INITIAL FORM;Lo;0;AL;<initial> 06C1;;;;N;;;;;
+FBA9;ARABIC LETTER HEH GOAL MEDIAL FORM;Lo;0;AL;<medial> 06C1;;;;N;;;;;
+FBAA;ARABIC LETTER HEH DOACHASHMEE ISOLATED FORM;Lo;0;AL;<isolated> 06BE;;;;N;;;;;
+FBAB;ARABIC LETTER HEH DOACHASHMEE FINAL FORM;Lo;0;AL;<final> 06BE;;;;N;;;;;
+FBAC;ARABIC LETTER HEH DOACHASHMEE INITIAL FORM;Lo;0;AL;<initial> 06BE;;;;N;;;;;
+FBAD;ARABIC LETTER HEH DOACHASHMEE MEDIAL FORM;Lo;0;AL;<medial> 06BE;;;;N;;;;;
+FBAE;ARABIC LETTER YEH BARREE ISOLATED FORM;Lo;0;AL;<isolated> 06D2;;;;N;;;;;
+FBAF;ARABIC LETTER YEH BARREE FINAL FORM;Lo;0;AL;<final> 06D2;;;;N;;;;;
+FBB0;ARABIC LETTER YEH BARREE WITH HAMZA ABOVE ISOLATED FORM;Lo;0;AL;<isolated> 06D3;;;;N;;;;;
+FBB1;ARABIC LETTER YEH BARREE WITH HAMZA ABOVE FINAL FORM;Lo;0;AL;<final> 06D3;;;;N;;;;;
+FBB2;ARABIC SYMBOL DOT ABOVE;Sk;0;AL;;;;;N;;;;;
+FBB3;ARABIC SYMBOL DOT BELOW;Sk;0;AL;;;;;N;;;;;
+FBB4;ARABIC SYMBOL TWO DOTS ABOVE;Sk;0;AL;;;;;N;;;;;
+FBB5;ARABIC SYMBOL TWO DOTS BELOW;Sk;0;AL;;;;;N;;;;;
+FBB6;ARABIC SYMBOL THREE DOTS ABOVE;Sk;0;AL;;;;;N;;;;;
+FBB7;ARABIC SYMBOL THREE DOTS BELOW;Sk;0;AL;;;;;N;;;;;
+FBB8;ARABIC SYMBOL THREE DOTS POINTING DOWNWARDS ABOVE;Sk;0;AL;;;;;N;;;;;
+FBB9;ARABIC SYMBOL THREE DOTS POINTING DOWNWARDS BELOW;Sk;0;AL;;;;;N;;;;;
+FBBA;ARABIC SYMBOL FOUR DOTS ABOVE;Sk;0;AL;;;;;N;;;;;
+FBBB;ARABIC SYMBOL FOUR DOTS BELOW;Sk;0;AL;;;;;N;;;;;
+FBBC;ARABIC SYMBOL DOUBLE VERTICAL BAR BELOW;Sk;0;AL;;;;;N;;;;;
+FBBD;ARABIC SYMBOL TWO DOTS VERTICALLY ABOVE;Sk;0;AL;;;;;N;;;;;
+FBBE;ARABIC SYMBOL TWO DOTS VERTICALLY BELOW;Sk;0;AL;;;;;N;;;;;
+FBBF;ARABIC SYMBOL RING;Sk;0;AL;;;;;N;;;;;
+FBC0;ARABIC SYMBOL SMALL TAH ABOVE;Sk;0;AL;;;;;N;;;;;
+FBC1;ARABIC SYMBOL SMALL TAH BELOW;Sk;0;AL;;;;;N;;;;;
+FBD3;ARABIC LETTER NG ISOLATED FORM;Lo;0;AL;<isolated> 06AD;;;;N;;;;;
+FBD4;ARABIC LETTER NG FINAL FORM;Lo;0;AL;<final> 06AD;;;;N;;;;;
+FBD5;ARABIC LETTER NG INITIAL FORM;Lo;0;AL;<initial> 06AD;;;;N;;;;;
+FBD6;ARABIC LETTER NG MEDIAL FORM;Lo;0;AL;<medial> 06AD;;;;N;;;;;
+FBD7;ARABIC LETTER U ISOLATED FORM;Lo;0;AL;<isolated> 06C7;;;;N;;;;;
+FBD8;ARABIC LETTER U FINAL FORM;Lo;0;AL;<final> 06C7;;;;N;;;;;
+FBD9;ARABIC LETTER OE ISOLATED FORM;Lo;0;AL;<isolated> 06C6;;;;N;;;;;
+FBDA;ARABIC LETTER OE FINAL FORM;Lo;0;AL;<final> 06C6;;;;N;;;;;
+FBDB;ARABIC LETTER YU ISOLATED FORM;Lo;0;AL;<isolated> 06C8;;;;N;;;;;
+FBDC;ARABIC LETTER YU FINAL FORM;Lo;0;AL;<final> 06C8;;;;N;;;;;
+FBDD;ARABIC LETTER U WITH HAMZA ABOVE ISOLATED FORM;Lo;0;AL;<isolated> 0677;;;;N;;;;;
+FBDE;ARABIC LETTER VE ISOLATED FORM;Lo;0;AL;<isolated> 06CB;;;;N;;;;;
+FBDF;ARABIC LETTER VE FINAL FORM;Lo;0;AL;<final> 06CB;;;;N;;;;;
+FBE0;ARABIC LETTER KIRGHIZ OE ISOLATED FORM;Lo;0;AL;<isolated> 06C5;;;;N;;;;;
+FBE1;ARABIC LETTER KIRGHIZ OE FINAL FORM;Lo;0;AL;<final> 06C5;;;;N;;;;;
+FBE2;ARABIC LETTER KIRGHIZ YU ISOLATED FORM;Lo;0;AL;<isolated> 06C9;;;;N;;;;;
+FBE3;ARABIC LETTER KIRGHIZ YU FINAL FORM;Lo;0;AL;<final> 06C9;;;;N;;;;;
+FBE4;ARABIC LETTER E ISOLATED FORM;Lo;0;AL;<isolated> 06D0;;;;N;;;;;
+FBE5;ARABIC LETTER E FINAL FORM;Lo;0;AL;<final> 06D0;;;;N;;;;;
+FBE6;ARABIC LETTER E INITIAL FORM;Lo;0;AL;<initial> 06D0;;;;N;;;;;
+FBE7;ARABIC LETTER E MEDIAL FORM;Lo;0;AL;<medial> 06D0;;;;N;;;;;
+FBE8;ARABIC LETTER UIGHUR KAZAKH KIRGHIZ ALEF MAKSURA INITIAL FORM;Lo;0;AL;<initial> 0649;;;;N;;;;;
+FBE9;ARABIC LETTER UIGHUR KAZAKH KIRGHIZ ALEF MAKSURA MEDIAL FORM;Lo;0;AL;<medial> 0649;;;;N;;;;;
+FBEA;ARABIC LIGATURE YEH WITH HAMZA ABOVE WITH ALEF ISOLATED FORM;Lo;0;AL;<isolated> 0626 0627;;;;N;;;;;
+FBEB;ARABIC LIGATURE YEH WITH HAMZA ABOVE WITH ALEF FINAL FORM;Lo;0;AL;<final> 0626 0627;;;;N;;;;;
+FBEC;ARABIC LIGATURE YEH WITH HAMZA ABOVE WITH AE ISOLATED FORM;Lo;0;AL;<isolated> 0626 06D5;;;;N;;;;;
+FBED;ARABIC LIGATURE YEH WITH HAMZA ABOVE WITH AE FINAL FORM;Lo;0;AL;<final> 0626 06D5;;;;N;;;;;
+FBEE;ARABIC LIGATURE YEH WITH HAMZA ABOVE WITH WAW ISOLATED FORM;Lo;0;AL;<isolated> 0626 0648;;;;N;;;;;
+FBEF;ARABIC LIGATURE YEH WITH HAMZA ABOVE WITH WAW FINAL FORM;Lo;0;AL;<final> 0626 0648;;;;N;;;;;
+FBF0;ARABIC LIGATURE YEH WITH HAMZA ABOVE WITH U ISOLATED FORM;Lo;0;AL;<isolated> 0626 06C7;;;;N;;;;;
+FBF1;ARABIC LIGATURE YEH WITH HAMZA ABOVE WITH U FINAL FORM;Lo;0;AL;<final> 0626 06C7;;;;N;;;;;
+FBF2;ARABIC LIGATURE YEH WITH HAMZA ABOVE WITH OE ISOLATED FORM;Lo;0;AL;<isolated> 0626 06C6;;;;N;;;;;
+FBF3;ARABIC LIGATURE YEH WITH HAMZA ABOVE WITH OE FINAL FORM;Lo;0;AL;<final> 0626 06C6;;;;N;;;;;
+FBF4;ARABIC LIGATURE YEH WITH HAMZA ABOVE WITH YU ISOLATED FORM;Lo;0;AL;<isolated> 0626 06C8;;;;N;;;;;
+FBF5;ARABIC LIGATURE YEH WITH HAMZA ABOVE WITH YU FINAL FORM;Lo;0;AL;<final> 0626 06C8;;;;N;;;;;
+FBF6;ARABIC LIGATURE YEH WITH HAMZA ABOVE WITH E ISOLATED FORM;Lo;0;AL;<isolated> 0626 06D0;;;;N;;;;;
+FBF7;ARABIC LIGATURE YEH WITH HAMZA ABOVE WITH E FINAL FORM;Lo;0;AL;<final> 0626 06D0;;;;N;;;;;
+FBF8;ARABIC LIGATURE YEH WITH HAMZA ABOVE WITH E INITIAL FORM;Lo;0;AL;<initial> 0626 06D0;;;;N;;;;;
+FBF9;ARABIC LIGATURE UIGHUR KIRGHIZ YEH WITH HAMZA ABOVE WITH ALEF MAKSURA ISOLATED FORM;Lo;0;AL;<isolated> 0626 0649;;;;N;;;;;
+FBFA;ARABIC LIGATURE UIGHUR KIRGHIZ YEH WITH HAMZA ABOVE WITH ALEF MAKSURA FINAL FORM;Lo;0;AL;<final> 0626 0649;;;;N;;;;;
+FBFB;ARABIC LIGATURE UIGHUR KIRGHIZ YEH WITH HAMZA ABOVE WITH ALEF MAKSURA INITIAL FORM;Lo;0;AL;<initial> 0626 0649;;;;N;;;;;
+FBFC;ARABIC LETTER FARSI YEH ISOLATED FORM;Lo;0;AL;<isolated> 06CC;;;;N;;;;;
+FBFD;ARABIC LETTER FARSI YEH FINAL FORM;Lo;0;AL;<final> 06CC;;;;N;;;;;
+FBFE;ARABIC LETTER FARSI YEH INITIAL FORM;Lo;0;AL;<initial> 06CC;;;;N;;;;;
+FBFF;ARABIC LETTER FARSI YEH MEDIAL FORM;Lo;0;AL;<medial> 06CC;;;;N;;;;;
+FC00;ARABIC LIGATURE YEH WITH HAMZA ABOVE WITH JEEM ISOLATED FORM;Lo;0;AL;<isolated> 0626 062C;;;;N;;;;;
+FC01;ARABIC LIGATURE YEH WITH HAMZA ABOVE WITH HAH ISOLATED FORM;Lo;0;AL;<isolated> 0626 062D;;;;N;;;;;
+FC02;ARABIC LIGATURE YEH WITH HAMZA ABOVE WITH MEEM ISOLATED FORM;Lo;0;AL;<isolated> 0626 0645;;;;N;;;;;
+FC03;ARABIC LIGATURE YEH WITH HAMZA ABOVE WITH ALEF MAKSURA ISOLATED FORM;Lo;0;AL;<isolated> 0626 0649;;;;N;;;;;
+FC04;ARABIC LIGATURE YEH WITH HAMZA ABOVE WITH YEH ISOLATED FORM;Lo;0;AL;<isolated> 0626 064A;;;;N;;;;;
+FC05;ARABIC LIGATURE BEH WITH JEEM ISOLATED FORM;Lo;0;AL;<isolated> 0628 062C;;;;N;;;;;
+FC06;ARABIC LIGATURE BEH WITH HAH ISOLATED FORM;Lo;0;AL;<isolated> 0628 062D;;;;N;;;;;
+FC07;ARABIC LIGATURE BEH WITH KHAH ISOLATED FORM;Lo;0;AL;<isolated> 0628 062E;;;;N;;;;;
+FC08;ARABIC LIGATURE BEH WITH MEEM ISOLATED FORM;Lo;0;AL;<isolated> 0628 0645;;;;N;;;;;
+FC09;ARABIC LIGATURE BEH WITH ALEF MAKSURA ISOLATED FORM;Lo;0;AL;<isolated> 0628 0649;;;;N;;;;;
+FC0A;ARABIC LIGATURE BEH WITH YEH ISOLATED FORM;Lo;0;AL;<isolated> 0628 064A;;;;N;;;;;
+FC0B;ARABIC LIGATURE TEH WITH JEEM ISOLATED FORM;Lo;0;AL;<isolated> 062A 062C;;;;N;;;;;
+FC0C;ARABIC LIGATURE TEH WITH HAH ISOLATED FORM;Lo;0;AL;<isolated> 062A 062D;;;;N;;;;;
+FC0D;ARABIC LIGATURE TEH WITH KHAH ISOLATED FORM;Lo;0;AL;<isolated> 062A 062E;;;;N;;;;;
+FC0E;ARABIC LIGATURE TEH WITH MEEM ISOLATED FORM;Lo;0;AL;<isolated> 062A 0645;;;;N;;;;;
+FC0F;ARABIC LIGATURE TEH WITH ALEF MAKSURA ISOLATED FORM;Lo;0;AL;<isolated> 062A 0649;;;;N;;;;;
+FC10;ARABIC LIGATURE TEH WITH YEH ISOLATED FORM;Lo;0;AL;<isolated> 062A 064A;;;;N;;;;;
+FC11;ARABIC LIGATURE THEH WITH JEEM ISOLATED FORM;Lo;0;AL;<isolated> 062B 062C;;;;N;;;;;
+FC12;ARABIC LIGATURE THEH WITH MEEM ISOLATED FORM;Lo;0;AL;<isolated> 062B 0645;;;;N;;;;;
+FC13;ARABIC LIGATURE THEH WITH ALEF MAKSURA ISOLATED FORM;Lo;0;AL;<isolated> 062B 0649;;;;N;;;;;
+FC14;ARABIC LIGATURE THEH WITH YEH ISOLATED FORM;Lo;0;AL;<isolated> 062B 064A;;;;N;;;;;
+FC15;ARABIC LIGATURE JEEM WITH HAH ISOLATED FORM;Lo;0;AL;<isolated> 062C 062D;;;;N;;;;;
+FC16;ARABIC LIGATURE JEEM WITH MEEM ISOLATED FORM;Lo;0;AL;<isolated> 062C 0645;;;;N;;;;;
+FC17;ARABIC LIGATURE HAH WITH JEEM ISOLATED FORM;Lo;0;AL;<isolated> 062D 062C;;;;N;;;;;
+FC18;ARABIC LIGATURE HAH WITH MEEM ISOLATED FORM;Lo;0;AL;<isolated> 062D 0645;;;;N;;;;;
+FC19;ARABIC LIGATURE KHAH WITH JEEM ISOLATED FORM;Lo;0;AL;<isolated> 062E 062C;;;;N;;;;;
+FC1A;ARABIC LIGATURE KHAH WITH HAH ISOLATED FORM;Lo;0;AL;<isolated> 062E 062D;;;;N;;;;;
+FC1B;ARABIC LIGATURE KHAH WITH MEEM ISOLATED FORM;Lo;0;AL;<isolated> 062E 0645;;;;N;;;;;
+FC1C;ARABIC LIGATURE SEEN WITH JEEM ISOLATED FORM;Lo;0;AL;<isolated> 0633 062C;;;;N;;;;;
+FC1D;ARABIC LIGATURE SEEN WITH HAH ISOLATED FORM;Lo;0;AL;<isolated> 0633 062D;;;;N;;;;;
+FC1E;ARABIC LIGATURE SEEN WITH KHAH ISOLATED FORM;Lo;0;AL;<isolated> 0633 062E;;;;N;;;;;
+FC1F;ARABIC LIGATURE SEEN WITH MEEM ISOLATED FORM;Lo;0;AL;<isolated> 0633 0645;;;;N;;;;;
+FC20;ARABIC LIGATURE SAD WITH HAH ISOLATED FORM;Lo;0;AL;<isolated> 0635 062D;;;;N;;;;;
+FC21;ARABIC LIGATURE SAD WITH MEEM ISOLATED FORM;Lo;0;AL;<isolated> 0635 0645;;;;N;;;;;
+FC22;ARABIC LIGATURE DAD WITH JEEM ISOLATED FORM;Lo;0;AL;<isolated> 0636 062C;;;;N;;;;;
+FC23;ARABIC LIGATURE DAD WITH HAH ISOLATED FORM;Lo;0;AL;<isolated> 0636 062D;;;;N;;;;;
+FC24;ARABIC LIGATURE DAD WITH KHAH ISOLATED FORM;Lo;0;AL;<isolated> 0636 062E;;;;N;;;;;
+FC25;ARABIC LIGATURE DAD WITH MEEM ISOLATED FORM;Lo;0;AL;<isolated> 0636 0645;;;;N;;;;;
+FC26;ARABIC LIGATURE TAH WITH HAH ISOLATED FORM;Lo;0;AL;<isolated> 0637 062D;;;;N;;;;;
+FC27;ARABIC LIGATURE TAH WITH MEEM ISOLATED FORM;Lo;0;AL;<isolated> 0637 0645;;;;N;;;;;
+FC28;ARABIC LIGATURE ZAH WITH MEEM ISOLATED FORM;Lo;0;AL;<isolated> 0638 0645;;;;N;;;;;
+FC29;ARABIC LIGATURE AIN WITH JEEM ISOLATED FORM;Lo;0;AL;<isolated> 0639 062C;;;;N;;;;;
+FC2A;ARABIC LIGATURE AIN WITH MEEM ISOLATED FORM;Lo;0;AL;<isolated> 0639 0645;;;;N;;;;;
+FC2B;ARABIC LIGATURE GHAIN WITH JEEM ISOLATED FORM;Lo;0;AL;<isolated> 063A 062C;;;;N;;;;;
+FC2C;ARABIC LIGATURE GHAIN WITH MEEM ISOLATED FORM;Lo;0;AL;<isolated> 063A 0645;;;;N;;;;;
+FC2D;ARABIC LIGATURE FEH WITH JEEM ISOLATED FORM;Lo;0;AL;<isolated> 0641 062C;;;;N;;;;;
+FC2E;ARABIC LIGATURE FEH WITH HAH ISOLATED FORM;Lo;0;AL;<isolated> 0641 062D;;;;N;;;;;
+FC2F;ARABIC LIGATURE FEH WITH KHAH ISOLATED FORM;Lo;0;AL;<isolated> 0641 062E;;;;N;;;;;
+FC30;ARABIC LIGATURE FEH WITH MEEM ISOLATED FORM;Lo;0;AL;<isolated> 0641 0645;;;;N;;;;;
+FC31;ARABIC LIGATURE FEH WITH ALEF MAKSURA ISOLATED FORM;Lo;0;AL;<isolated> 0641 0649;;;;N;;;;;
+FC32;ARABIC LIGATURE FEH WITH YEH ISOLATED FORM;Lo;0;AL;<isolated> 0641 064A;;;;N;;;;;
+FC33;ARABIC LIGATURE QAF WITH HAH ISOLATED FORM;Lo;0;AL;<isolated> 0642 062D;;;;N;;;;;
+FC34;ARABIC LIGATURE QAF WITH MEEM ISOLATED FORM;Lo;0;AL;<isolated> 0642 0645;;;;N;;;;;
+FC35;ARABIC LIGATURE QAF WITH ALEF MAKSURA ISOLATED FORM;Lo;0;AL;<isolated> 0642 0649;;;;N;;;;;
+FC36;ARABIC LIGATURE QAF WITH YEH ISOLATED FORM;Lo;0;AL;<isolated> 0642 064A;;;;N;;;;;
+FC37;ARABIC LIGATURE KAF WITH ALEF ISOLATED FORM;Lo;0;AL;<isolated> 0643 0627;;;;N;;;;;
+FC38;ARABIC LIGATURE KAF WITH JEEM ISOLATED FORM;Lo;0;AL;<isolated> 0643 062C;;;;N;;;;;
+FC39;ARABIC LIGATURE KAF WITH HAH ISOLATED FORM;Lo;0;AL;<isolated> 0643 062D;;;;N;;;;;
+FC3A;ARABIC LIGATURE KAF WITH KHAH ISOLATED FORM;Lo;0;AL;<isolated> 0643 062E;;;;N;;;;;
+FC3B;ARABIC LIGATURE KAF WITH LAM ISOLATED FORM;Lo;0;AL;<isolated> 0643 0644;;;;N;;;;;
+FC3C;ARABIC LIGATURE KAF WITH MEEM ISOLATED FORM;Lo;0;AL;<isolated> 0643 0645;;;;N;;;;;
+FC3D;ARABIC LIGATURE KAF WITH ALEF MAKSURA ISOLATED FORM;Lo;0;AL;<isolated> 0643 0649;;;;N;;;;;
+FC3E;ARABIC LIGATURE KAF WITH YEH ISOLATED FORM;Lo;0;AL;<isolated> 0643 064A;;;;N;;;;;
+FC3F;ARABIC LIGATURE LAM WITH JEEM ISOLATED FORM;Lo;0;AL;<isolated> 0644 062C;;;;N;;;;;
+FC40;ARABIC LIGATURE LAM WITH HAH ISOLATED FORM;Lo;0;AL;<isolated> 0644 062D;;;;N;;;;;
+FC41;ARABIC LIGATURE LAM WITH KHAH ISOLATED FORM;Lo;0;AL;<isolated> 0644 062E;;;;N;;;;;
+FC42;ARABIC LIGATURE LAM WITH MEEM ISOLATED FORM;Lo;0;AL;<isolated> 0644 0645;;;;N;;;;;
+FC43;ARABIC LIGATURE LAM WITH ALEF MAKSURA ISOLATED FORM;Lo;0;AL;<isolated> 0644 0649;;;;N;;;;;
+FC44;ARABIC LIGATURE LAM WITH YEH ISOLATED FORM;Lo;0;AL;<isolated> 0644 064A;;;;N;;;;;
+FC45;ARABIC LIGATURE MEEM WITH JEEM ISOLATED FORM;Lo;0;AL;<isolated> 0645 062C;;;;N;;;;;
+FC46;ARABIC LIGATURE MEEM WITH HAH ISOLATED FORM;Lo;0;AL;<isolated> 0645 062D;;;;N;;;;;
+FC47;ARABIC LIGATURE MEEM WITH KHAH ISOLATED FORM;Lo;0;AL;<isolated> 0645 062E;;;;N;;;;;
+FC48;ARABIC LIGATURE MEEM WITH MEEM ISOLATED FORM;Lo;0;AL;<isolated> 0645 0645;;;;N;;;;;
+FC49;ARABIC LIGATURE MEEM WITH ALEF MAKSURA ISOLATED FORM;Lo;0;AL;<isolated> 0645 0649;;;;N;;;;;
+FC4A;ARABIC LIGATURE MEEM WITH YEH ISOLATED FORM;Lo;0;AL;<isolated> 0645 064A;;;;N;;;;;
+FC4B;ARABIC LIGATURE NOON WITH JEEM ISOLATED FORM;Lo;0;AL;<isolated> 0646 062C;;;;N;;;;;
+FC4C;ARABIC LIGATURE NOON WITH HAH ISOLATED FORM;Lo;0;AL;<isolated> 0646 062D;;;;N;;;;;
+FC4D;ARABIC LIGATURE NOON WITH KHAH ISOLATED FORM;Lo;0;AL;<isolated> 0646 062E;;;;N;;;;;
+FC4E;ARABIC LIGATURE NOON WITH MEEM ISOLATED FORM;Lo;0;AL;<isolated> 0646 0645;;;;N;;;;;
+FC4F;ARABIC LIGATURE NOON WITH ALEF MAKSURA ISOLATED FORM;Lo;0;AL;<isolated> 0646 0649;;;;N;;;;;
+FC50;ARABIC LIGATURE NOON WITH YEH ISOLATED FORM;Lo;0;AL;<isolated> 0646 064A;;;;N;;;;;
+FC51;ARABIC LIGATURE HEH WITH JEEM ISOLATED FORM;Lo;0;AL;<isolated> 0647 062C;;;;N;;;;;
+FC52;ARABIC LIGATURE HEH WITH MEEM ISOLATED FORM;Lo;0;AL;<isolated> 0647 0645;;;;N;;;;;
+FC53;ARABIC LIGATURE HEH WITH ALEF MAKSURA ISOLATED FORM;Lo;0;AL;<isolated> 0647 0649;;;;N;;;;;
+FC54;ARABIC LIGATURE HEH WITH YEH ISOLATED FORM;Lo;0;AL;<isolated> 0647 064A;;;;N;;;;;
+FC55;ARABIC LIGATURE YEH WITH JEEM ISOLATED FORM;Lo;0;AL;<isolated> 064A 062C;;;;N;;;;;
+FC56;ARABIC LIGATURE YEH WITH HAH ISOLATED FORM;Lo;0;AL;<isolated> 064A 062D;;;;N;;;;;
+FC57;ARABIC LIGATURE YEH WITH KHAH ISOLATED FORM;Lo;0;AL;<isolated> 064A 062E;;;;N;;;;;
+FC58;ARABIC LIGATURE YEH WITH MEEM ISOLATED FORM;Lo;0;AL;<isolated> 064A 0645;;;;N;;;;;
+FC59;ARABIC LIGATURE YEH WITH ALEF MAKSURA ISOLATED FORM;Lo;0;AL;<isolated> 064A 0649;;;;N;;;;;
+FC5A;ARABIC LIGATURE YEH WITH YEH ISOLATED FORM;Lo;0;AL;<isolated> 064A 064A;;;;N;;;;;
+FC5B;ARABIC LIGATURE THAL WITH SUPERSCRIPT ALEF ISOLATED FORM;Lo;0;AL;<isolated> 0630 0670;;;;N;;;;;
+FC5C;ARABIC LIGATURE REH WITH SUPERSCRIPT ALEF ISOLATED FORM;Lo;0;AL;<isolated> 0631 0670;;;;N;;;;;
+FC5D;ARABIC LIGATURE ALEF MAKSURA WITH SUPERSCRIPT ALEF ISOLATED FORM;Lo;0;AL;<isolated> 0649 0670;;;;N;;;;;
+FC5E;ARABIC LIGATURE SHADDA WITH DAMMATAN ISOLATED FORM;Lo;0;AL;<isolated> 0020 064C 0651;;;;N;;;;;
+FC5F;ARABIC LIGATURE SHADDA WITH KASRATAN ISOLATED FORM;Lo;0;AL;<isolated> 0020 064D 0651;;;;N;;;;;
+FC60;ARABIC LIGATURE SHADDA WITH FATHA ISOLATED FORM;Lo;0;AL;<isolated> 0020 064E 0651;;;;N;;;;;
+FC61;ARABIC LIGATURE SHADDA WITH DAMMA ISOLATED FORM;Lo;0;AL;<isolated> 0020 064F 0651;;;;N;;;;;
+FC62;ARABIC LIGATURE SHADDA WITH KASRA ISOLATED FORM;Lo;0;AL;<isolated> 0020 0650 0651;;;;N;;;;;
+FC63;ARABIC LIGATURE SHADDA WITH SUPERSCRIPT ALEF ISOLATED FORM;Lo;0;AL;<isolated> 0020 0651 0670;;;;N;;;;;
+FC64;ARABIC LIGATURE YEH WITH HAMZA ABOVE WITH REH FINAL FORM;Lo;0;AL;<final> 0626 0631;;;;N;;;;;
+FC65;ARABIC LIGATURE YEH WITH HAMZA ABOVE WITH ZAIN FINAL FORM;Lo;0;AL;<final> 0626 0632;;;;N;;;;;
+FC66;ARABIC LIGATURE YEH WITH HAMZA ABOVE WITH MEEM FINAL FORM;Lo;0;AL;<final> 0626 0645;;;;N;;;;;
+FC67;ARABIC LIGATURE YEH WITH HAMZA ABOVE WITH NOON FINAL FORM;Lo;0;AL;<final> 0626 0646;;;;N;;;;;
+FC68;ARABIC LIGATURE YEH WITH HAMZA ABOVE WITH ALEF MAKSURA FINAL FORM;Lo;0;AL;<final> 0626 0649;;;;N;;;;;
+FC69;ARABIC LIGATURE YEH WITH HAMZA ABOVE WITH YEH FINAL FORM;Lo;0;AL;<final> 0626 064A;;;;N;;;;;
+FC6A;ARABIC LIGATURE BEH WITH REH FINAL FORM;Lo;0;AL;<final> 0628 0631;;;;N;;;;;
+FC6B;ARABIC LIGATURE BEH WITH ZAIN FINAL FORM;Lo;0;AL;<final> 0628 0632;;;;N;;;;;
+FC6C;ARABIC LIGATURE BEH WITH MEEM FINAL FORM;Lo;0;AL;<final> 0628 0645;;;;N;;;;;
+FC6D;ARABIC LIGATURE BEH WITH NOON FINAL FORM;Lo;0;AL;<final> 0628 0646;;;;N;;;;;
+FC6E;ARABIC LIGATURE BEH WITH ALEF MAKSURA FINAL FORM;Lo;0;AL;<final> 0628 0649;;;;N;;;;;
+FC6F;ARABIC LIGATURE BEH WITH YEH FINAL FORM;Lo;0;AL;<final> 0628 064A;;;;N;;;;;
+FC70;ARABIC LIGATURE TEH WITH REH FINAL FORM;Lo;0;AL;<final> 062A 0631;;;;N;;;;;
+FC71;ARABIC LIGATURE TEH WITH ZAIN FINAL FORM;Lo;0;AL;<final> 062A 0632;;;;N;;;;;
+FC72;ARABIC LIGATURE TEH WITH MEEM FINAL FORM;Lo;0;AL;<final> 062A 0645;;;;N;;;;;
+FC73;ARABIC LIGATURE TEH WITH NOON FINAL FORM;Lo;0;AL;<final> 062A 0646;;;;N;;;;;
+FC74;ARABIC LIGATURE TEH WITH ALEF MAKSURA FINAL FORM;Lo;0;AL;<final> 062A 0649;;;;N;;;;;
+FC75;ARABIC LIGATURE TEH WITH YEH FINAL FORM;Lo;0;AL;<final> 062A 064A;;;;N;;;;;
+FC76;ARABIC LIGATURE THEH WITH REH FINAL FORM;Lo;0;AL;<final> 062B 0631;;;;N;;;;;
+FC77;ARABIC LIGATURE THEH WITH ZAIN FINAL FORM;Lo;0;AL;<final> 062B 0632;;;;N;;;;;
+FC78;ARABIC LIGATURE THEH WITH MEEM FINAL FORM;Lo;0;AL;<final> 062B 0645;;;;N;;;;;
+FC79;ARABIC LIGATURE THEH WITH NOON FINAL FORM;Lo;0;AL;<final> 062B 0646;;;;N;;;;;
+FC7A;ARABIC LIGATURE THEH WITH ALEF MAKSURA FINAL FORM;Lo;0;AL;<final> 062B 0649;;;;N;;;;;
+FC7B;ARABIC LIGATURE THEH WITH YEH FINAL FORM;Lo;0;AL;<final> 062B 064A;;;;N;;;;;
+FC7C;ARABIC LIGATURE FEH WITH ALEF MAKSURA FINAL FORM;Lo;0;AL;<final> 0641 0649;;;;N;;;;;
+FC7D;ARABIC LIGATURE FEH WITH YEH FINAL FORM;Lo;0;AL;<final> 0641 064A;;;;N;;;;;
+FC7E;ARABIC LIGATURE QAF WITH ALEF MAKSURA FINAL FORM;Lo;0;AL;<final> 0642 0649;;;;N;;;;;
+FC7F;ARABIC LIGATURE QAF WITH YEH FINAL FORM;Lo;0;AL;<final> 0642 064A;;;;N;;;;;
+FC80;ARABIC LIGATURE KAF WITH ALEF FINAL FORM;Lo;0;AL;<final> 0643 0627;;;;N;;;;;
+FC81;ARABIC LIGATURE KAF WITH LAM FINAL FORM;Lo;0;AL;<final> 0643 0644;;;;N;;;;;
+FC82;ARABIC LIGATURE KAF WITH MEEM FINAL FORM;Lo;0;AL;<final> 0643 0645;;;;N;;;;;
+FC83;ARABIC LIGATURE KAF WITH ALEF MAKSURA FINAL FORM;Lo;0;AL;<final> 0643 0649;;;;N;;;;;
+FC84;ARABIC LIGATURE KAF WITH YEH FINAL FORM;Lo;0;AL;<final> 0643 064A;;;;N;;;;;
+FC85;ARABIC LIGATURE LAM WITH MEEM FINAL FORM;Lo;0;AL;<final> 0644 0645;;;;N;;;;;
+FC86;ARABIC LIGATURE LAM WITH ALEF MAKSURA FINAL FORM;Lo;0;AL;<final> 0644 0649;;;;N;;;;;
+FC87;ARABIC LIGATURE LAM WITH YEH FINAL FORM;Lo;0;AL;<final> 0644 064A;;;;N;;;;;
+FC88;ARABIC LIGATURE MEEM WITH ALEF FINAL FORM;Lo;0;AL;<final> 0645 0627;;;;N;;;;;
+FC89;ARABIC LIGATURE MEEM WITH MEEM FINAL FORM;Lo;0;AL;<final> 0645 0645;;;;N;;;;;
+FC8A;ARABIC LIGATURE NOON WITH REH FINAL FORM;Lo;0;AL;<final> 0646 0631;;;;N;;;;;
+FC8B;ARABIC LIGATURE NOON WITH ZAIN FINAL FORM;Lo;0;AL;<final> 0646 0632;;;;N;;;;;
+FC8C;ARABIC LIGATURE NOON WITH MEEM FINAL FORM;Lo;0;AL;<final> 0646 0645;;;;N;;;;;
+FC8D;ARABIC LIGATURE NOON WITH NOON FINAL FORM;Lo;0;AL;<final> 0646 0646;;;;N;;;;;
+FC8E;ARABIC LIGATURE NOON WITH ALEF MAKSURA FINAL FORM;Lo;0;AL;<final> 0646 0649;;;;N;;;;;
+FC8F;ARABIC LIGATURE NOON WITH YEH FINAL FORM;Lo;0;AL;<final> 0646 064A;;;;N;;;;;
+FC90;ARABIC LIGATURE ALEF MAKSURA WITH SUPERSCRIPT ALEF FINAL FORM;Lo;0;AL;<final> 0649 0670;;;;N;;;;;
+FC91;ARABIC LIGATURE YEH WITH REH FINAL FORM;Lo;0;AL;<final> 064A 0631;;;;N;;;;;
+FC92;ARABIC LIGATURE YEH WITH ZAIN FINAL FORM;Lo;0;AL;<final> 064A 0632;;;;N;;;;;
+FC93;ARABIC LIGATURE YEH WITH MEEM FINAL FORM;Lo;0;AL;<final> 064A 0645;;;;N;;;;;
+FC94;ARABIC LIGATURE YEH WITH NOON FINAL FORM;Lo;0;AL;<final> 064A 0646;;;;N;;;;;
+FC95;ARABIC LIGATURE YEH WITH ALEF MAKSURA FINAL FORM;Lo;0;AL;<final> 064A 0649;;;;N;;;;;
+FC96;ARABIC LIGATURE YEH WITH YEH FINAL FORM;Lo;0;AL;<final> 064A 064A;;;;N;;;;;
+FC97;ARABIC LIGATURE YEH WITH HAMZA ABOVE WITH JEEM INITIAL FORM;Lo;0;AL;<initial> 0626 062C;;;;N;;;;;
+FC98;ARABIC LIGATURE YEH WITH HAMZA ABOVE WITH HAH INITIAL FORM;Lo;0;AL;<initial> 0626 062D;;;;N;;;;;
+FC99;ARABIC LIGATURE YEH WITH HAMZA ABOVE WITH KHAH INITIAL FORM;Lo;0;AL;<initial> 0626 062E;;;;N;;;;;
+FC9A;ARABIC LIGATURE YEH WITH HAMZA ABOVE WITH MEEM INITIAL FORM;Lo;0;AL;<initial> 0626 0645;;;;N;;;;;
+FC9B;ARABIC LIGATURE YEH WITH HAMZA ABOVE WITH HEH INITIAL FORM;Lo;0;AL;<initial> 0626 0647;;;;N;;;;;
+FC9C;ARABIC LIGATURE BEH WITH JEEM INITIAL FORM;Lo;0;AL;<initial> 0628 062C;;;;N;;;;;
+FC9D;ARABIC LIGATURE BEH WITH HAH INITIAL FORM;Lo;0;AL;<initial> 0628 062D;;;;N;;;;;
+FC9E;ARABIC LIGATURE BEH WITH KHAH INITIAL FORM;Lo;0;AL;<initial> 0628 062E;;;;N;;;;;
+FC9F;ARABIC LIGATURE BEH WITH MEEM INITIAL FORM;Lo;0;AL;<initial> 0628 0645;;;;N;;;;;
+FCA0;ARABIC LIGATURE BEH WITH HEH INITIAL FORM;Lo;0;AL;<initial> 0628 0647;;;;N;;;;;
+FCA1;ARABIC LIGATURE TEH WITH JEEM INITIAL FORM;Lo;0;AL;<initial> 062A 062C;;;;N;;;;;
+FCA2;ARABIC LIGATURE TEH WITH HAH INITIAL FORM;Lo;0;AL;<initial> 062A 062D;;;;N;;;;;
+FCA3;ARABIC LIGATURE TEH WITH KHAH INITIAL FORM;Lo;0;AL;<initial> 062A 062E;;;;N;;;;;
+FCA4;ARABIC LIGATURE TEH WITH MEEM INITIAL FORM;Lo;0;AL;<initial> 062A 0645;;;;N;;;;;
+FCA5;ARABIC LIGATURE TEH WITH HEH INITIAL FORM;Lo;0;AL;<initial> 062A 0647;;;;N;;;;;
+FCA6;ARABIC LIGATURE THEH WITH MEEM INITIAL FORM;Lo;0;AL;<initial> 062B 0645;;;;N;;;;;
+FCA7;ARABIC LIGATURE JEEM WITH HAH INITIAL FORM;Lo;0;AL;<initial> 062C 062D;;;;N;;;;;
+FCA8;ARABIC LIGATURE JEEM WITH MEEM INITIAL FORM;Lo;0;AL;<initial> 062C 0645;;;;N;;;;;
+FCA9;ARABIC LIGATURE HAH WITH JEEM INITIAL FORM;Lo;0;AL;<initial> 062D 062C;;;;N;;;;;
+FCAA;ARABIC LIGATURE HAH WITH MEEM INITIAL FORM;Lo;0;AL;<initial> 062D 0645;;;;N;;;;;
+FCAB;ARABIC LIGATURE KHAH WITH JEEM INITIAL FORM;Lo;0;AL;<initial> 062E 062C;;;;N;;;;;
+FCAC;ARABIC LIGATURE KHAH WITH MEEM INITIAL FORM;Lo;0;AL;<initial> 062E 0645;;;;N;;;;;
+FCAD;ARABIC LIGATURE SEEN WITH JEEM INITIAL FORM;Lo;0;AL;<initial> 0633 062C;;;;N;;;;;
+FCAE;ARABIC LIGATURE SEEN WITH HAH INITIAL FORM;Lo;0;AL;<initial> 0633 062D;;;;N;;;;;
+FCAF;ARABIC LIGATURE SEEN WITH KHAH INITIAL FORM;Lo;0;AL;<initial> 0633 062E;;;;N;;;;;
+FCB0;ARABIC LIGATURE SEEN WITH MEEM INITIAL FORM;Lo;0;AL;<initial> 0633 0645;;;;N;;;;;
+FCB1;ARABIC LIGATURE SAD WITH HAH INITIAL FORM;Lo;0;AL;<initial> 0635 062D;;;;N;;;;;
+FCB2;ARABIC LIGATURE SAD WITH KHAH INITIAL FORM;Lo;0;AL;<initial> 0635 062E;;;;N;;;;;
+FCB3;ARABIC LIGATURE SAD WITH MEEM INITIAL FORM;Lo;0;AL;<initial> 0635 0645;;;;N;;;;;
+FCB4;ARABIC LIGATURE DAD WITH JEEM INITIAL FORM;Lo;0;AL;<initial> 0636 062C;;;;N;;;;;
+FCB5;ARABIC LIGATURE DAD WITH HAH INITIAL FORM;Lo;0;AL;<initial> 0636 062D;;;;N;;;;;
+FCB6;ARABIC LIGATURE DAD WITH KHAH INITIAL FORM;Lo;0;AL;<initial> 0636 062E;;;;N;;;;;
+FCB7;ARABIC LIGATURE DAD WITH MEEM INITIAL FORM;Lo;0;AL;<initial> 0636 0645;;;;N;;;;;
+FCB8;ARABIC LIGATURE TAH WITH HAH INITIAL FORM;Lo;0;AL;<initial> 0637 062D;;;;N;;;;;
+FCB9;ARABIC LIGATURE ZAH WITH MEEM INITIAL FORM;Lo;0;AL;<initial> 0638 0645;;;;N;;;;;
+FCBA;ARABIC LIGATURE AIN WITH JEEM INITIAL FORM;Lo;0;AL;<initial> 0639 062C;;;;N;;;;;
+FCBB;ARABIC LIGATURE AIN WITH MEEM INITIAL FORM;Lo;0;AL;<initial> 0639 0645;;;;N;;;;;
+FCBC;ARABIC LIGATURE GHAIN WITH JEEM INITIAL FORM;Lo;0;AL;<initial> 063A 062C;;;;N;;;;;
+FCBD;ARABIC LIGATURE GHAIN WITH MEEM INITIAL FORM;Lo;0;AL;<initial> 063A 0645;;;;N;;;;;
+FCBE;ARABIC LIGATURE FEH WITH JEEM INITIAL FORM;Lo;0;AL;<initial> 0641 062C;;;;N;;;;;
+FCBF;ARABIC LIGATURE FEH WITH HAH INITIAL FORM;Lo;0;AL;<initial> 0641 062D;;;;N;;;;;
+FCC0;ARABIC LIGATURE FEH WITH KHAH INITIAL FORM;Lo;0;AL;<initial> 0641 062E;;;;N;;;;;
+FCC1;ARABIC LIGATURE FEH WITH MEEM INITIAL FORM;Lo;0;AL;<initial> 0641 0645;;;;N;;;;;
+FCC2;ARABIC LIGATURE QAF WITH HAH INITIAL FORM;Lo;0;AL;<initial> 0642 062D;;;;N;;;;;
+FCC3;ARABIC LIGATURE QAF WITH MEEM INITIAL FORM;Lo;0;AL;<initial> 0642 0645;;;;N;;;;;
+FCC4;ARABIC LIGATURE KAF WITH JEEM INITIAL FORM;Lo;0;AL;<initial> 0643 062C;;;;N;;;;;
+FCC5;ARABIC LIGATURE KAF WITH HAH INITIAL FORM;Lo;0;AL;<initial> 0643 062D;;;;N;;;;;
+FCC6;ARABIC LIGATURE KAF WITH KHAH INITIAL FORM;Lo;0;AL;<initial> 0643 062E;;;;N;;;;;
+FCC7;ARABIC LIGATURE KAF WITH LAM INITIAL FORM;Lo;0;AL;<initial> 0643 0644;;;;N;;;;;
+FCC8;ARABIC LIGATURE KAF WITH MEEM INITIAL FORM;Lo;0;AL;<initial> 0643 0645;;;;N;;;;;
+FCC9;ARABIC LIGATURE LAM WITH JEEM INITIAL FORM;Lo;0;AL;<initial> 0644 062C;;;;N;;;;;
+FCCA;ARABIC LIGATURE LAM WITH HAH INITIAL FORM;Lo;0;AL;<initial> 0644 062D;;;;N;;;;;
+FCCB;ARABIC LIGATURE LAM WITH KHAH INITIAL FORM;Lo;0;AL;<initial> 0644 062E;;;;N;;;;;
+FCCC;ARABIC LIGATURE LAM WITH MEEM INITIAL FORM;Lo;0;AL;<initial> 0644 0645;;;;N;;;;;
+FCCD;ARABIC LIGATURE LAM WITH HEH INITIAL FORM;Lo;0;AL;<initial> 0644 0647;;;;N;;;;;
+FCCE;ARABIC LIGATURE MEEM WITH JEEM INITIAL FORM;Lo;0;AL;<initial> 0645 062C;;;;N;;;;;
+FCCF;ARABIC LIGATURE MEEM WITH HAH INITIAL FORM;Lo;0;AL;<initial> 0645 062D;;;;N;;;;;
+FCD0;ARABIC LIGATURE MEEM WITH KHAH INITIAL FORM;Lo;0;AL;<initial> 0645 062E;;;;N;;;;;
+FCD1;ARABIC LIGATURE MEEM WITH MEEM INITIAL FORM;Lo;0;AL;<initial> 0645 0645;;;;N;;;;;
+FCD2;ARABIC LIGATURE NOON WITH JEEM INITIAL FORM;Lo;0;AL;<initial> 0646 062C;;;;N;;;;;
+FCD3;ARABIC LIGATURE NOON WITH HAH INITIAL FORM;Lo;0;AL;<initial> 0646 062D;;;;N;;;;;
+FCD4;ARABIC LIGATURE NOON WITH KHAH INITIAL FORM;Lo;0;AL;<initial> 0646 062E;;;;N;;;;;
+FCD5;ARABIC LIGATURE NOON WITH MEEM INITIAL FORM;Lo;0;AL;<initial> 0646 0645;;;;N;;;;;
+FCD6;ARABIC LIGATURE NOON WITH HEH INITIAL FORM;Lo;0;AL;<initial> 0646 0647;;;;N;;;;;
+FCD7;ARABIC LIGATURE HEH WITH JEEM INITIAL FORM;Lo;0;AL;<initial> 0647 062C;;;;N;;;;;
+FCD8;ARABIC LIGATURE HEH WITH MEEM INITIAL FORM;Lo;0;AL;<initial> 0647 0645;;;;N;;;;;
+FCD9;ARABIC LIGATURE HEH WITH SUPERSCRIPT ALEF INITIAL FORM;Lo;0;AL;<initial> 0647 0670;;;;N;;;;;
+FCDA;ARABIC LIGATURE YEH WITH JEEM INITIAL FORM;Lo;0;AL;<initial> 064A 062C;;;;N;;;;;
+FCDB;ARABIC LIGATURE YEH WITH HAH INITIAL FORM;Lo;0;AL;<initial> 064A 062D;;;;N;;;;;
+FCDC;ARABIC LIGATURE YEH WITH KHAH INITIAL FORM;Lo;0;AL;<initial> 064A 062E;;;;N;;;;;
+FCDD;ARABIC LIGATURE YEH WITH MEEM INITIAL FORM;Lo;0;AL;<initial> 064A 0645;;;;N;;;;;
+FCDE;ARABIC LIGATURE YEH WITH HEH INITIAL FORM;Lo;0;AL;<initial> 064A 0647;;;;N;;;;;
+FCDF;ARABIC LIGATURE YEH WITH HAMZA ABOVE WITH MEEM MEDIAL FORM;Lo;0;AL;<medial> 0626 0645;;;;N;;;;;
+FCE0;ARABIC LIGATURE YEH WITH HAMZA ABOVE WITH HEH MEDIAL FORM;Lo;0;AL;<medial> 0626 0647;;;;N;;;;;
+FCE1;ARABIC LIGATURE BEH WITH MEEM MEDIAL FORM;Lo;0;AL;<medial> 0628 0645;;;;N;;;;;
+FCE2;ARABIC LIGATURE BEH WITH HEH MEDIAL FORM;Lo;0;AL;<medial> 0628 0647;;;;N;;;;;
+FCE3;ARABIC LIGATURE TEH WITH MEEM MEDIAL FORM;Lo;0;AL;<medial> 062A 0645;;;;N;;;;;
+FCE4;ARABIC LIGATURE TEH WITH HEH MEDIAL FORM;Lo;0;AL;<medial> 062A 0647;;;;N;;;;;
+FCE5;ARABIC LIGATURE THEH WITH MEEM MEDIAL FORM;Lo;0;AL;<medial> 062B 0645;;;;N;;;;;
+FCE6;ARABIC LIGATURE THEH WITH HEH MEDIAL FORM;Lo;0;AL;<medial> 062B 0647;;;;N;;;;;
+FCE7;ARABIC LIGATURE SEEN WITH MEEM MEDIAL FORM;Lo;0;AL;<medial> 0633 0645;;;;N;;;;;
+FCE8;ARABIC LIGATURE SEEN WITH HEH MEDIAL FORM;Lo;0;AL;<medial> 0633 0647;;;;N;;;;;
+FCE9;ARABIC LIGATURE SHEEN WITH MEEM MEDIAL FORM;Lo;0;AL;<medial> 0634 0645;;;;N;;;;;
+FCEA;ARABIC LIGATURE SHEEN WITH HEH MEDIAL FORM;Lo;0;AL;<medial> 0634 0647;;;;N;;;;;
+FCEB;ARABIC LIGATURE KAF WITH LAM MEDIAL FORM;Lo;0;AL;<medial> 0643 0644;;;;N;;;;;
+FCEC;ARABIC LIGATURE KAF WITH MEEM MEDIAL FORM;Lo;0;AL;<medial> 0643 0645;;;;N;;;;;
+FCED;ARABIC LIGATURE LAM WITH MEEM MEDIAL FORM;Lo;0;AL;<medial> 0644 0645;;;;N;;;;;
+FCEE;ARABIC LIGATURE NOON WITH MEEM MEDIAL FORM;Lo;0;AL;<medial> 0646 0645;;;;N;;;;;
+FCEF;ARABIC LIGATURE NOON WITH HEH MEDIAL FORM;Lo;0;AL;<medial> 0646 0647;;;;N;;;;;
+FCF0;ARABIC LIGATURE YEH WITH MEEM MEDIAL FORM;Lo;0;AL;<medial> 064A 0645;;;;N;;;;;
+FCF1;ARABIC LIGATURE YEH WITH HEH MEDIAL FORM;Lo;0;AL;<medial> 064A 0647;;;;N;;;;;
+FCF2;ARABIC LIGATURE SHADDA WITH FATHA MEDIAL FORM;Lo;0;AL;<medial> 0640 064E 0651;;;;N;;;;;
+FCF3;ARABIC LIGATURE SHADDA WITH DAMMA MEDIAL FORM;Lo;0;AL;<medial> 0640 064F 0651;;;;N;;;;;
+FCF4;ARABIC LIGATURE SHADDA WITH KASRA MEDIAL FORM;Lo;0;AL;<medial> 0640 0650 0651;;;;N;;;;;
+FCF5;ARABIC LIGATURE TAH WITH ALEF MAKSURA ISOLATED FORM;Lo;0;AL;<isolated> 0637 0649;;;;N;;;;;
+FCF6;ARABIC LIGATURE TAH WITH YEH ISOLATED FORM;Lo;0;AL;<isolated> 0637 064A;;;;N;;;;;
+FCF7;ARABIC LIGATURE AIN WITH ALEF MAKSURA ISOLATED FORM;Lo;0;AL;<isolated> 0639 0649;;;;N;;;;;
+FCF8;ARABIC LIGATURE AIN WITH YEH ISOLATED FORM;Lo;0;AL;<isolated> 0639 064A;;;;N;;;;;
+FCF9;ARABIC LIGATURE GHAIN WITH ALEF MAKSURA ISOLATED FORM;Lo;0;AL;<isolated> 063A 0649;;;;N;;;;;
+FCFA;ARABIC LIGATURE GHAIN WITH YEH ISOLATED FORM;Lo;0;AL;<isolated> 063A 064A;;;;N;;;;;
+FCFB;ARABIC LIGATURE SEEN WITH ALEF MAKSURA ISOLATED FORM;Lo;0;AL;<isolated> 0633 0649;;;;N;;;;;
+FCFC;ARABIC LIGATURE SEEN WITH YEH ISOLATED FORM;Lo;0;AL;<isolated> 0633 064A;;;;N;;;;;
+FCFD;ARABIC LIGATURE SHEEN WITH ALEF MAKSURA ISOLATED FORM;Lo;0;AL;<isolated> 0634 0649;;;;N;;;;;
+FCFE;ARABIC LIGATURE SHEEN WITH YEH ISOLATED FORM;Lo;0;AL;<isolated> 0634 064A;;;;N;;;;;
+FCFF;ARABIC LIGATURE HAH WITH ALEF MAKSURA ISOLATED FORM;Lo;0;AL;<isolated> 062D 0649;;;;N;;;;;
+FD00;ARABIC LIGATURE HAH WITH YEH ISOLATED FORM;Lo;0;AL;<isolated> 062D 064A;;;;N;;;;;
+FD01;ARABIC LIGATURE JEEM WITH ALEF MAKSURA ISOLATED FORM;Lo;0;AL;<isolated> 062C 0649;;;;N;;;;;
+FD02;ARABIC LIGATURE JEEM WITH YEH ISOLATED FORM;Lo;0;AL;<isolated> 062C 064A;;;;N;;;;;
+FD03;ARABIC LIGATURE KHAH WITH ALEF MAKSURA ISOLATED FORM;Lo;0;AL;<isolated> 062E 0649;;;;N;;;;;
+FD04;ARABIC LIGATURE KHAH WITH YEH ISOLATED FORM;Lo;0;AL;<isolated> 062E 064A;;;;N;;;;;
+FD05;ARABIC LIGATURE SAD WITH ALEF MAKSURA ISOLATED FORM;Lo;0;AL;<isolated> 0635 0649;;;;N;;;;;
+FD06;ARABIC LIGATURE SAD WITH YEH ISOLATED FORM;Lo;0;AL;<isolated> 0635 064A;;;;N;;;;;
+FD07;ARABIC LIGATURE DAD WITH ALEF MAKSURA ISOLATED FORM;Lo;0;AL;<isolated> 0636 0649;;;;N;;;;;
+FD08;ARABIC LIGATURE DAD WITH YEH ISOLATED FORM;Lo;0;AL;<isolated> 0636 064A;;;;N;;;;;
+FD09;ARABIC LIGATURE SHEEN WITH JEEM ISOLATED FORM;Lo;0;AL;<isolated> 0634 062C;;;;N;;;;;
+FD0A;ARABIC LIGATURE SHEEN WITH HAH ISOLATED FORM;Lo;0;AL;<isolated> 0634 062D;;;;N;;;;;
+FD0B;ARABIC LIGATURE SHEEN WITH KHAH ISOLATED FORM;Lo;0;AL;<isolated> 0634 062E;;;;N;;;;;
+FD0C;ARABIC LIGATURE SHEEN WITH MEEM ISOLATED FORM;Lo;0;AL;<isolated> 0634 0645;;;;N;;;;;
+FD0D;ARABIC LIGATURE SHEEN WITH REH ISOLATED FORM;Lo;0;AL;<isolated> 0634 0631;;;;N;;;;;
+FD0E;ARABIC LIGATURE SEEN WITH REH ISOLATED FORM;Lo;0;AL;<isolated> 0633 0631;;;;N;;;;;
+FD0F;ARABIC LIGATURE SAD WITH REH ISOLATED FORM;Lo;0;AL;<isolated> 0635 0631;;;;N;;;;;
+FD10;ARABIC LIGATURE DAD WITH REH ISOLATED FORM;Lo;0;AL;<isolated> 0636 0631;;;;N;;;;;
+FD11;ARABIC LIGATURE TAH WITH ALEF MAKSURA FINAL FORM;Lo;0;AL;<final> 0637 0649;;;;N;;;;;
+FD12;ARABIC LIGATURE TAH WITH YEH FINAL FORM;Lo;0;AL;<final> 0637 064A;;;;N;;;;;
+FD13;ARABIC LIGATURE AIN WITH ALEF MAKSURA FINAL FORM;Lo;0;AL;<final> 0639 0649;;;;N;;;;;
+FD14;ARABIC LIGATURE AIN WITH YEH FINAL FORM;Lo;0;AL;<final> 0639 064A;;;;N;;;;;
+FD15;ARABIC LIGATURE GHAIN WITH ALEF MAKSURA FINAL FORM;Lo;0;AL;<final> 063A 0649;;;;N;;;;;
+FD16;ARABIC LIGATURE GHAIN WITH YEH FINAL FORM;Lo;0;AL;<final> 063A 064A;;;;N;;;;;
+FD17;ARABIC LIGATURE SEEN WITH ALEF MAKSURA FINAL FORM;Lo;0;AL;<final> 0633 0649;;;;N;;;;;
+FD18;ARABIC LIGATURE SEEN WITH YEH FINAL FORM;Lo;0;AL;<final> 0633 064A;;;;N;;;;;
+FD19;ARABIC LIGATURE SHEEN WITH ALEF MAKSURA FINAL FORM;Lo;0;AL;<final> 0634 0649;;;;N;;;;;
+FD1A;ARABIC LIGATURE SHEEN WITH YEH FINAL FORM;Lo;0;AL;<final> 0634 064A;;;;N;;;;;
+FD1B;ARABIC LIGATURE HAH WITH ALEF MAKSURA FINAL FORM;Lo;0;AL;<final> 062D 0649;;;;N;;;;;
+FD1C;ARABIC LIGATURE HAH WITH YEH FINAL FORM;Lo;0;AL;<final> 062D 064A;;;;N;;;;;
+FD1D;ARABIC LIGATURE JEEM WITH ALEF MAKSURA FINAL FORM;Lo;0;AL;<final> 062C 0649;;;;N;;;;;
+FD1E;ARABIC LIGATURE JEEM WITH YEH FINAL FORM;Lo;0;AL;<final> 062C 064A;;;;N;;;;;
+FD1F;ARABIC LIGATURE KHAH WITH ALEF MAKSURA FINAL FORM;Lo;0;AL;<final> 062E 0649;;;;N;;;;;
+FD20;ARABIC LIGATURE KHAH WITH YEH FINAL FORM;Lo;0;AL;<final> 062E 064A;;;;N;;;;;
+FD21;ARABIC LIGATURE SAD WITH ALEF MAKSURA FINAL FORM;Lo;0;AL;<final> 0635 0649;;;;N;;;;;
+FD22;ARABIC LIGATURE SAD WITH YEH FINAL FORM;Lo;0;AL;<final> 0635 064A;;;;N;;;;;
+FD23;ARABIC LIGATURE DAD WITH ALEF MAKSURA FINAL FORM;Lo;0;AL;<final> 0636 0649;;;;N;;;;;
+FD24;ARABIC LIGATURE DAD WITH YEH FINAL FORM;Lo;0;AL;<final> 0636 064A;;;;N;;;;;
+FD25;ARABIC LIGATURE SHEEN WITH JEEM FINAL FORM;Lo;0;AL;<final> 0634 062C;;;;N;;;;;
+FD26;ARABIC LIGATURE SHEEN WITH HAH FINAL FORM;Lo;0;AL;<final> 0634 062D;;;;N;;;;;
+FD27;ARABIC LIGATURE SHEEN WITH KHAH FINAL FORM;Lo;0;AL;<final> 0634 062E;;;;N;;;;;
+FD28;ARABIC LIGATURE SHEEN WITH MEEM FINAL FORM;Lo;0;AL;<final> 0634 0645;;;;N;;;;;
+FD29;ARABIC LIGATURE SHEEN WITH REH FINAL FORM;Lo;0;AL;<final> 0634 0631;;;;N;;;;;
+FD2A;ARABIC LIGATURE SEEN WITH REH FINAL FORM;Lo;0;AL;<final> 0633 0631;;;;N;;;;;
+FD2B;ARABIC LIGATURE SAD WITH REH FINAL FORM;Lo;0;AL;<final> 0635 0631;;;;N;;;;;
+FD2C;ARABIC LIGATURE DAD WITH REH FINAL FORM;Lo;0;AL;<final> 0636 0631;;;;N;;;;;
+FD2D;ARABIC LIGATURE SHEEN WITH JEEM INITIAL FORM;Lo;0;AL;<initial> 0634 062C;;;;N;;;;;
+FD2E;ARABIC LIGATURE SHEEN WITH HAH INITIAL FORM;Lo;0;AL;<initial> 0634 062D;;;;N;;;;;
+FD2F;ARABIC LIGATURE SHEEN WITH KHAH INITIAL FORM;Lo;0;AL;<initial> 0634 062E;;;;N;;;;;
+FD30;ARABIC LIGATURE SHEEN WITH MEEM INITIAL FORM;Lo;0;AL;<initial> 0634 0645;;;;N;;;;;
+FD31;ARABIC LIGATURE SEEN WITH HEH INITIAL FORM;Lo;0;AL;<initial> 0633 0647;;;;N;;;;;
+FD32;ARABIC LIGATURE SHEEN WITH HEH INITIAL FORM;Lo;0;AL;<initial> 0634 0647;;;;N;;;;;
+FD33;ARABIC LIGATURE TAH WITH MEEM INITIAL FORM;Lo;0;AL;<initial> 0637 0645;;;;N;;;;;
+FD34;ARABIC LIGATURE SEEN WITH JEEM MEDIAL FORM;Lo;0;AL;<medial> 0633 062C;;;;N;;;;;
+FD35;ARABIC LIGATURE SEEN WITH HAH MEDIAL FORM;Lo;0;AL;<medial> 0633 062D;;;;N;;;;;
+FD36;ARABIC LIGATURE SEEN WITH KHAH MEDIAL FORM;Lo;0;AL;<medial> 0633 062E;;;;N;;;;;
+FD37;ARABIC LIGATURE SHEEN WITH JEEM MEDIAL FORM;Lo;0;AL;<medial> 0634 062C;;;;N;;;;;
+FD38;ARABIC LIGATURE SHEEN WITH HAH MEDIAL FORM;Lo;0;AL;<medial> 0634 062D;;;;N;;;;;
+FD39;ARABIC LIGATURE SHEEN WITH KHAH MEDIAL FORM;Lo;0;AL;<medial> 0634 062E;;;;N;;;;;
+FD3A;ARABIC LIGATURE TAH WITH MEEM MEDIAL FORM;Lo;0;AL;<medial> 0637 0645;;;;N;;;;;
+FD3B;ARABIC LIGATURE ZAH WITH MEEM MEDIAL FORM;Lo;0;AL;<medial> 0638 0645;;;;N;;;;;
+FD3C;ARABIC LIGATURE ALEF WITH FATHATAN FINAL FORM;Lo;0;AL;<final> 0627 064B;;;;N;;;;;
+FD3D;ARABIC LIGATURE ALEF WITH FATHATAN ISOLATED FORM;Lo;0;AL;<isolated> 0627 064B;;;;N;;;;;
+FD3E;ORNATE LEFT PARENTHESIS;Pe;0;ON;;;;;N;;;;;
+FD3F;ORNATE RIGHT PARENTHESIS;Ps;0;ON;;;;;N;;;;;
+FD50;ARABIC LIGATURE TEH WITH JEEM WITH MEEM INITIAL FORM;Lo;0;AL;<initial> 062A 062C 0645;;;;N;;;;;
+FD51;ARABIC LIGATURE TEH WITH HAH WITH JEEM FINAL FORM;Lo;0;AL;<final> 062A 062D 062C;;;;N;;;;;
+FD52;ARABIC LIGATURE TEH WITH HAH WITH JEEM INITIAL FORM;Lo;0;AL;<initial> 062A 062D 062C;;;;N;;;;;
+FD53;ARABIC LIGATURE TEH WITH HAH WITH MEEM INITIAL FORM;Lo;0;AL;<initial> 062A 062D 0645;;;;N;;;;;
+FD54;ARABIC LIGATURE TEH WITH KHAH WITH MEEM INITIAL FORM;Lo;0;AL;<initial> 062A 062E 0645;;;;N;;;;;
+FD55;ARABIC LIGATURE TEH WITH MEEM WITH JEEM INITIAL FORM;Lo;0;AL;<initial> 062A 0645 062C;;;;N;;;;;
+FD56;ARABIC LIGATURE TEH WITH MEEM WITH HAH INITIAL FORM;Lo;0;AL;<initial> 062A 0645 062D;;;;N;;;;;
+FD57;ARABIC LIGATURE TEH WITH MEEM WITH KHAH INITIAL FORM;Lo;0;AL;<initial> 062A 0645 062E;;;;N;;;;;
+FD58;ARABIC LIGATURE JEEM WITH MEEM WITH HAH FINAL FORM;Lo;0;AL;<final> 062C 0645 062D;;;;N;;;;;
+FD59;ARABIC LIGATURE JEEM WITH MEEM WITH HAH INITIAL FORM;Lo;0;AL;<initial> 062C 0645 062D;;;;N;;;;;
+FD5A;ARABIC LIGATURE HAH WITH MEEM WITH YEH FINAL FORM;Lo;0;AL;<final> 062D 0645 064A;;;;N;;;;;
+FD5B;ARABIC LIGATURE HAH WITH MEEM WITH ALEF MAKSURA FINAL FORM;Lo;0;AL;<final> 062D 0645 0649;;;;N;;;;;
+FD5C;ARABIC LIGATURE SEEN WITH HAH WITH JEEM INITIAL FORM;Lo;0;AL;<initial> 0633 062D 062C;;;;N;;;;;
+FD5D;ARABIC LIGATURE SEEN WITH JEEM WITH HAH INITIAL FORM;Lo;0;AL;<initial> 0633 062C 062D;;;;N;;;;;
+FD5E;ARABIC LIGATURE SEEN WITH JEEM WITH ALEF MAKSURA FINAL FORM;Lo;0;AL;<final> 0633 062C 0649;;;;N;;;;;
+FD5F;ARABIC LIGATURE SEEN WITH MEEM WITH HAH FINAL FORM;Lo;0;AL;<final> 0633 0645 062D;;;;N;;;;;
+FD60;ARABIC LIGATURE SEEN WITH MEEM WITH HAH INITIAL FORM;Lo;0;AL;<initial> 0633 0645 062D;;;;N;;;;;
+FD61;ARABIC LIGATURE SEEN WITH MEEM WITH JEEM INITIAL FORM;Lo;0;AL;<initial> 0633 0645 062C;;;;N;;;;;
+FD62;ARABIC LIGATURE SEEN WITH MEEM WITH MEEM FINAL FORM;Lo;0;AL;<final> 0633 0645 0645;;;;N;;;;;
+FD63;ARABIC LIGATURE SEEN WITH MEEM WITH MEEM INITIAL FORM;Lo;0;AL;<initial> 0633 0645 0645;;;;N;;;;;
+FD64;ARABIC LIGATURE SAD WITH HAH WITH HAH FINAL FORM;Lo;0;AL;<final> 0635 062D 062D;;;;N;;;;;
+FD65;ARABIC LIGATURE SAD WITH HAH WITH HAH INITIAL FORM;Lo;0;AL;<initial> 0635 062D 062D;;;;N;;;;;
+FD66;ARABIC LIGATURE SAD WITH MEEM WITH MEEM FINAL FORM;Lo;0;AL;<final> 0635 0645 0645;;;;N;;;;;
+FD67;ARABIC LIGATURE SHEEN WITH HAH WITH MEEM FINAL FORM;Lo;0;AL;<final> 0634 062D 0645;;;;N;;;;;
+FD68;ARABIC LIGATURE SHEEN WITH HAH WITH MEEM INITIAL FORM;Lo;0;AL;<initial> 0634 062D 0645;;;;N;;;;;
+FD69;ARABIC LIGATURE SHEEN WITH JEEM WITH YEH FINAL FORM;Lo;0;AL;<final> 0634 062C 064A;;;;N;;;;;
+FD6A;ARABIC LIGATURE SHEEN WITH MEEM WITH KHAH FINAL FORM;Lo;0;AL;<final> 0634 0645 062E;;;;N;;;;;
+FD6B;ARABIC LIGATURE SHEEN WITH MEEM WITH KHAH INITIAL FORM;Lo;0;AL;<initial> 0634 0645 062E;;;;N;;;;;
+FD6C;ARABIC LIGATURE SHEEN WITH MEEM WITH MEEM FINAL FORM;Lo;0;AL;<final> 0634 0645 0645;;;;N;;;;;
+FD6D;ARABIC LIGATURE SHEEN WITH MEEM WITH MEEM INITIAL FORM;Lo;0;AL;<initial> 0634 0645 0645;;;;N;;;;;
+FD6E;ARABIC LIGATURE DAD WITH HAH WITH ALEF MAKSURA FINAL FORM;Lo;0;AL;<final> 0636 062D 0649;;;;N;;;;;
+FD6F;ARABIC LIGATURE DAD WITH KHAH WITH MEEM FINAL FORM;Lo;0;AL;<final> 0636 062E 0645;;;;N;;;;;
+FD70;ARABIC LIGATURE DAD WITH KHAH WITH MEEM INITIAL FORM;Lo;0;AL;<initial> 0636 062E 0645;;;;N;;;;;
+FD71;ARABIC LIGATURE TAH WITH MEEM WITH HAH FINAL FORM;Lo;0;AL;<final> 0637 0645 062D;;;;N;;;;;
+FD72;ARABIC LIGATURE TAH WITH MEEM WITH HAH INITIAL FORM;Lo;0;AL;<initial> 0637 0645 062D;;;;N;;;;;
+FD73;ARABIC LIGATURE TAH WITH MEEM WITH MEEM INITIAL FORM;Lo;0;AL;<initial> 0637 0645 0645;;;;N;;;;;
+FD74;ARABIC LIGATURE TAH WITH MEEM WITH YEH FINAL FORM;Lo;0;AL;<final> 0637 0645 064A;;;;N;;;;;
+FD75;ARABIC LIGATURE AIN WITH JEEM WITH MEEM FINAL FORM;Lo;0;AL;<final> 0639 062C 0645;;;;N;;;;;
+FD76;ARABIC LIGATURE AIN WITH MEEM WITH MEEM FINAL FORM;Lo;0;AL;<final> 0639 0645 0645;;;;N;;;;;
+FD77;ARABIC LIGATURE AIN WITH MEEM WITH MEEM INITIAL FORM;Lo;0;AL;<initial> 0639 0645 0645;;;;N;;;;;
+FD78;ARABIC LIGATURE AIN WITH MEEM WITH ALEF MAKSURA FINAL FORM;Lo;0;AL;<final> 0639 0645 0649;;;;N;;;;;
+FD79;ARABIC LIGATURE GHAIN WITH MEEM WITH MEEM FINAL FORM;Lo;0;AL;<final> 063A 0645 0645;;;;N;;;;;
+FD7A;ARABIC LIGATURE GHAIN WITH MEEM WITH YEH FINAL FORM;Lo;0;AL;<final> 063A 0645 064A;;;;N;;;;;
+FD7B;ARABIC LIGATURE GHAIN WITH MEEM WITH ALEF MAKSURA FINAL FORM;Lo;0;AL;<final> 063A 0645 0649;;;;N;;;;;
+FD7C;ARABIC LIGATURE FEH WITH KHAH WITH MEEM FINAL FORM;Lo;0;AL;<final> 0641 062E 0645;;;;N;;;;;
+FD7D;ARABIC LIGATURE FEH WITH KHAH WITH MEEM INITIAL FORM;Lo;0;AL;<initial> 0641 062E 0645;;;;N;;;;;
+FD7E;ARABIC LIGATURE QAF WITH MEEM WITH HAH FINAL FORM;Lo;0;AL;<final> 0642 0645 062D;;;;N;;;;;
+FD7F;ARABIC LIGATURE QAF WITH MEEM WITH MEEM FINAL FORM;Lo;0;AL;<final> 0642 0645 0645;;;;N;;;;;
+FD80;ARABIC LIGATURE LAM WITH HAH WITH MEEM FINAL FORM;Lo;0;AL;<final> 0644 062D 0645;;;;N;;;;;
+FD81;ARABIC LIGATURE LAM WITH HAH WITH YEH FINAL FORM;Lo;0;AL;<final> 0644 062D 064A;;;;N;;;;;
+FD82;ARABIC LIGATURE LAM WITH HAH WITH ALEF MAKSURA FINAL FORM;Lo;0;AL;<final> 0644 062D 0649;;;;N;;;;;
+FD83;ARABIC LIGATURE LAM WITH JEEM WITH JEEM INITIAL FORM;Lo;0;AL;<initial> 0644 062C 062C;;;;N;;;;;
+FD84;ARABIC LIGATURE LAM WITH JEEM WITH JEEM FINAL FORM;Lo;0;AL;<final> 0644 062C 062C;;;;N;;;;;
+FD85;ARABIC LIGATURE LAM WITH KHAH WITH MEEM FINAL FORM;Lo;0;AL;<final> 0644 062E 0645;;;;N;;;;;
+FD86;ARABIC LIGATURE LAM WITH KHAH WITH MEEM INITIAL FORM;Lo;0;AL;<initial> 0644 062E 0645;;;;N;;;;;
+FD87;ARABIC LIGATURE LAM WITH MEEM WITH HAH FINAL FORM;Lo;0;AL;<final> 0644 0645 062D;;;;N;;;;;
+FD88;ARABIC LIGATURE LAM WITH MEEM WITH HAH INITIAL FORM;Lo;0;AL;<initial> 0644 0645 062D;;;;N;;;;;
+FD89;ARABIC LIGATURE MEEM WITH HAH WITH JEEM INITIAL FORM;Lo;0;AL;<initial> 0645 062D 062C;;;;N;;;;;
+FD8A;ARABIC LIGATURE MEEM WITH HAH WITH MEEM INITIAL FORM;Lo;0;AL;<initial> 0645 062D 0645;;;;N;;;;;
+FD8B;ARABIC LIGATURE MEEM WITH HAH WITH YEH FINAL FORM;Lo;0;AL;<final> 0645 062D 064A;;;;N;;;;;
+FD8C;ARABIC LIGATURE MEEM WITH JEEM WITH HAH INITIAL FORM;Lo;0;AL;<initial> 0645 062C 062D;;;;N;;;;;
+FD8D;ARABIC LIGATURE MEEM WITH JEEM WITH MEEM INITIAL FORM;Lo;0;AL;<initial> 0645 062C 0645;;;;N;;;;;
+FD8E;ARABIC LIGATURE MEEM WITH KHAH WITH JEEM INITIAL FORM;Lo;0;AL;<initial> 0645 062E 062C;;;;N;;;;;
+FD8F;ARABIC LIGATURE MEEM WITH KHAH WITH MEEM INITIAL FORM;Lo;0;AL;<initial> 0645 062E 0645;;;;N;;;;;
+FD92;ARABIC LIGATURE MEEM WITH JEEM WITH KHAH INITIAL FORM;Lo;0;AL;<initial> 0645 062C 062E;;;;N;;;;;
+FD93;ARABIC LIGATURE HEH WITH MEEM WITH JEEM INITIAL FORM;Lo;0;AL;<initial> 0647 0645 062C;;;;N;;;;;
+FD94;ARABIC LIGATURE HEH WITH MEEM WITH MEEM INITIAL FORM;Lo;0;AL;<initial> 0647 0645 0645;;;;N;;;;;
+FD95;ARABIC LIGATURE NOON WITH HAH WITH MEEM INITIAL FORM;Lo;0;AL;<initial> 0646 062D 0645;;;;N;;;;;
+FD96;ARABIC LIGATURE NOON WITH HAH WITH ALEF MAKSURA FINAL FORM;Lo;0;AL;<final> 0646 062D 0649;;;;N;;;;;
+FD97;ARABIC LIGATURE NOON WITH JEEM WITH MEEM FINAL FORM;Lo;0;AL;<final> 0646 062C 0645;;;;N;;;;;
+FD98;ARABIC LIGATURE NOON WITH JEEM WITH MEEM INITIAL FORM;Lo;0;AL;<initial> 0646 062C 0645;;;;N;;;;;
+FD99;ARABIC LIGATURE NOON WITH JEEM WITH ALEF MAKSURA FINAL FORM;Lo;0;AL;<final> 0646 062C 0649;;;;N;;;;;
+FD9A;ARABIC LIGATURE NOON WITH MEEM WITH YEH FINAL FORM;Lo;0;AL;<final> 0646 0645 064A;;;;N;;;;;
+FD9B;ARABIC LIGATURE NOON WITH MEEM WITH ALEF MAKSURA FINAL FORM;Lo;0;AL;<final> 0646 0645 0649;;;;N;;;;;
+FD9C;ARABIC LIGATURE YEH WITH MEEM WITH MEEM FINAL FORM;Lo;0;AL;<final> 064A 0645 0645;;;;N;;;;;
+FD9D;ARABIC LIGATURE YEH WITH MEEM WITH MEEM INITIAL FORM;Lo;0;AL;<initial> 064A 0645 0645;;;;N;;;;;
+FD9E;ARABIC LIGATURE BEH WITH KHAH WITH YEH FINAL FORM;Lo;0;AL;<final> 0628 062E 064A;;;;N;;;;;
+FD9F;ARABIC LIGATURE TEH WITH JEEM WITH YEH FINAL FORM;Lo;0;AL;<final> 062A 062C 064A;;;;N;;;;;
+FDA0;ARABIC LIGATURE TEH WITH JEEM WITH ALEF MAKSURA FINAL FORM;Lo;0;AL;<final> 062A 062C 0649;;;;N;;;;;
+FDA1;ARABIC LIGATURE TEH WITH KHAH WITH YEH FINAL FORM;Lo;0;AL;<final> 062A 062E 064A;;;;N;;;;;
+FDA2;ARABIC LIGATURE TEH WITH KHAH WITH ALEF MAKSURA FINAL FORM;Lo;0;AL;<final> 062A 062E 0649;;;;N;;;;;
+FDA3;ARABIC LIGATURE TEH WITH MEEM WITH YEH FINAL FORM;Lo;0;AL;<final> 062A 0645 064A;;;;N;;;;;
+FDA4;ARABIC LIGATURE TEH WITH MEEM WITH ALEF MAKSURA FINAL FORM;Lo;0;AL;<final> 062A 0645 0649;;;;N;;;;;
+FDA5;ARABIC LIGATURE JEEM WITH MEEM WITH YEH FINAL FORM;Lo;0;AL;<final> 062C 0645 064A;;;;N;;;;;
+FDA6;ARABIC LIGATURE JEEM WITH HAH WITH ALEF MAKSURA FINAL FORM;Lo;0;AL;<final> 062C 062D 0649;;;;N;;;;;
+FDA7;ARABIC LIGATURE JEEM WITH MEEM WITH ALEF MAKSURA FINAL FORM;Lo;0;AL;<final> 062C 0645 0649;;;;N;;;;;
+FDA8;ARABIC LIGATURE SEEN WITH KHAH WITH ALEF MAKSURA FINAL FORM;Lo;0;AL;<final> 0633 062E 0649;;;;N;;;;;
+FDA9;ARABIC LIGATURE SAD WITH HAH WITH YEH FINAL FORM;Lo;0;AL;<final> 0635 062D 064A;;;;N;;;;;
+FDAA;ARABIC LIGATURE SHEEN WITH HAH WITH YEH FINAL FORM;Lo;0;AL;<final> 0634 062D 064A;;;;N;;;;;
+FDAB;ARABIC LIGATURE DAD WITH HAH WITH YEH FINAL FORM;Lo;0;AL;<final> 0636 062D 064A;;;;N;;;;;
+FDAC;ARABIC LIGATURE LAM WITH JEEM WITH YEH FINAL FORM;Lo;0;AL;<final> 0644 062C 064A;;;;N;;;;;
+FDAD;ARABIC LIGATURE LAM WITH MEEM WITH YEH FINAL FORM;Lo;0;AL;<final> 0644 0645 064A;;;;N;;;;;
+FDAE;ARABIC LIGATURE YEH WITH HAH WITH YEH FINAL FORM;Lo;0;AL;<final> 064A 062D 064A;;;;N;;;;;
+FDAF;ARABIC LIGATURE YEH WITH JEEM WITH YEH FINAL FORM;Lo;0;AL;<final> 064A 062C 064A;;;;N;;;;;
+FDB0;ARABIC LIGATURE YEH WITH MEEM WITH YEH FINAL FORM;Lo;0;AL;<final> 064A 0645 064A;;;;N;;;;;
+FDB1;ARABIC LIGATURE MEEM WITH MEEM WITH YEH FINAL FORM;Lo;0;AL;<final> 0645 0645 064A;;;;N;;;;;
+FDB2;ARABIC LIGATURE QAF WITH MEEM WITH YEH FINAL FORM;Lo;0;AL;<final> 0642 0645 064A;;;;N;;;;;
+FDB3;ARABIC LIGATURE NOON WITH HAH WITH YEH FINAL FORM;Lo;0;AL;<final> 0646 062D 064A;;;;N;;;;;
+FDB4;ARABIC LIGATURE QAF WITH MEEM WITH HAH INITIAL FORM;Lo;0;AL;<initial> 0642 0645 062D;;;;N;;;;;
+FDB5;ARABIC LIGATURE LAM WITH HAH WITH MEEM INITIAL FORM;Lo;0;AL;<initial> 0644 062D 0645;;;;N;;;;;
+FDB6;ARABIC LIGATURE AIN WITH MEEM WITH YEH FINAL FORM;Lo;0;AL;<final> 0639 0645 064A;;;;N;;;;;
+FDB7;ARABIC LIGATURE KAF WITH MEEM WITH YEH FINAL FORM;Lo;0;AL;<final> 0643 0645 064A;;;;N;;;;;
+FDB8;ARABIC LIGATURE NOON WITH JEEM WITH HAH INITIAL FORM;Lo;0;AL;<initial> 0646 062C 062D;;;;N;;;;;
+FDB9;ARABIC LIGATURE MEEM WITH KHAH WITH YEH FINAL FORM;Lo;0;AL;<final> 0645 062E 064A;;;;N;;;;;
+FDBA;ARABIC LIGATURE LAM WITH JEEM WITH MEEM INITIAL FORM;Lo;0;AL;<initial> 0644 062C 0645;;;;N;;;;;
+FDBB;ARABIC LIGATURE KAF WITH MEEM WITH MEEM FINAL FORM;Lo;0;AL;<final> 0643 0645 0645;;;;N;;;;;
+FDBC;ARABIC LIGATURE LAM WITH JEEM WITH MEEM FINAL FORM;Lo;0;AL;<final> 0644 062C 0645;;;;N;;;;;
+FDBD;ARABIC LIGATURE NOON WITH JEEM WITH HAH FINAL FORM;Lo;0;AL;<final> 0646 062C 062D;;;;N;;;;;
+FDBE;ARABIC LIGATURE JEEM WITH HAH WITH YEH FINAL FORM;Lo;0;AL;<final> 062C 062D 064A;;;;N;;;;;
+FDBF;ARABIC LIGATURE HAH WITH JEEM WITH YEH FINAL FORM;Lo;0;AL;<final> 062D 062C 064A;;;;N;;;;;
+FDC0;ARABIC LIGATURE MEEM WITH JEEM WITH YEH FINAL FORM;Lo;0;AL;<final> 0645 062C 064A;;;;N;;;;;
+FDC1;ARABIC LIGATURE FEH WITH MEEM WITH YEH FINAL FORM;Lo;0;AL;<final> 0641 0645 064A;;;;N;;;;;
+FDC2;ARABIC LIGATURE BEH WITH HAH WITH YEH FINAL FORM;Lo;0;AL;<final> 0628 062D 064A;;;;N;;;;;
+FDC3;ARABIC LIGATURE KAF WITH MEEM WITH MEEM INITIAL FORM;Lo;0;AL;<initial> 0643 0645 0645;;;;N;;;;;
+FDC4;ARABIC LIGATURE AIN WITH JEEM WITH MEEM INITIAL FORM;Lo;0;AL;<initial> 0639 062C 0645;;;;N;;;;;
+FDC5;ARABIC LIGATURE SAD WITH MEEM WITH MEEM INITIAL FORM;Lo;0;AL;<initial> 0635 0645 0645;;;;N;;;;;
+FDC6;ARABIC LIGATURE SEEN WITH KHAH WITH YEH FINAL FORM;Lo;0;AL;<final> 0633 062E 064A;;;;N;;;;;
+FDC7;ARABIC LIGATURE NOON WITH JEEM WITH YEH FINAL FORM;Lo;0;AL;<final> 0646 062C 064A;;;;N;;;;;
+FDF0;ARABIC LIGATURE SALLA USED AS KORANIC STOP SIGN ISOLATED FORM;Lo;0;AL;<isolated> 0635 0644 06D2;;;;N;;;;;
+FDF1;ARABIC LIGATURE QALA USED AS KORANIC STOP SIGN ISOLATED FORM;Lo;0;AL;<isolated> 0642 0644 06D2;;;;N;;;;;
+FDF2;ARABIC LIGATURE ALLAH ISOLATED FORM;Lo;0;AL;<isolated> 0627 0644 0644 0647;;;;N;;;;;
+FDF3;ARABIC LIGATURE AKBAR ISOLATED FORM;Lo;0;AL;<isolated> 0627 0643 0628 0631;;;;N;;;;;
+FDF4;ARABIC LIGATURE MOHAMMAD ISOLATED FORM;Lo;0;AL;<isolated> 0645 062D 0645 062F;;;;N;;;;;
+FDF5;ARABIC LIGATURE SALAM ISOLATED FORM;Lo;0;AL;<isolated> 0635 0644 0639 0645;;;;N;;;;;
+FDF6;ARABIC LIGATURE RASOUL ISOLATED FORM;Lo;0;AL;<isolated> 0631 0633 0648 0644;;;;N;;;;;
+FDF7;ARABIC LIGATURE ALAYHE ISOLATED FORM;Lo;0;AL;<isolated> 0639 0644 064A 0647;;;;N;;;;;
+FDF8;ARABIC LIGATURE WASALLAM ISOLATED FORM;Lo;0;AL;<isolated> 0648 0633 0644 0645;;;;N;;;;;
+FDF9;ARABIC LIGATURE SALLA ISOLATED FORM;Lo;0;AL;<isolated> 0635 0644 0649;;;;N;;;;;
+FDFA;ARABIC LIGATURE SALLALLAHOU ALAYHE WASALLAM;Lo;0;AL;<isolated> 0635 0644 0649 0020 0627 0644 0644 0647 0020 0639 0644 064A 0647 0020 0648 0633 0644 0645;;;;N;ARABIC LETTER SALLALLAHOU ALAYHE WASALLAM;;;;
+FDFB;ARABIC LIGATURE JALLAJALALOUHOU;Lo;0;AL;<isolated> 062C 0644 0020 062C 0644 0627 0644 0647;;;;N;ARABIC LETTER JALLAJALALOUHOU;;;;
+FDFC;RIAL SIGN;Sc;0;AL;<isolated> 0631 06CC 0627 0644;;;;N;;;;;
+FDFD;ARABIC LIGATURE BISMILLAH AR-RAHMAN AR-RAHEEM;So;0;ON;;;;;N;;;;;
+FE00;VARIATION SELECTOR-1;Mn;0;NSM;;;;;N;;;;;
+FE01;VARIATION SELECTOR-2;Mn;0;NSM;;;;;N;;;;;
+FE02;VARIATION SELECTOR-3;Mn;0;NSM;;;;;N;;;;;
+FE03;VARIATION SELECTOR-4;Mn;0;NSM;;;;;N;;;;;
+FE04;VARIATION SELECTOR-5;Mn;0;NSM;;;;;N;;;;;
+FE05;VARIATION SELECTOR-6;Mn;0;NSM;;;;;N;;;;;
+FE06;VARIATION SELECTOR-7;Mn;0;NSM;;;;;N;;;;;
+FE07;VARIATION SELECTOR-8;Mn;0;NSM;;;;;N;;;;;
+FE08;VARIATION SELECTOR-9;Mn;0;NSM;;;;;N;;;;;
+FE09;VARIATION SELECTOR-10;Mn;0;NSM;;;;;N;;;;;
+FE0A;VARIATION SELECTOR-11;Mn;0;NSM;;;;;N;;;;;
+FE0B;VARIATION SELECTOR-12;Mn;0;NSM;;;;;N;;;;;
+FE0C;VARIATION SELECTOR-13;Mn;0;NSM;;;;;N;;;;;
+FE0D;VARIATION SELECTOR-14;Mn;0;NSM;;;;;N;;;;;
+FE0E;VARIATION SELECTOR-15;Mn;0;NSM;;;;;N;;;;;
+FE0F;VARIATION SELECTOR-16;Mn;0;NSM;;;;;N;;;;;
+FE10;PRESENTATION FORM FOR VERTICAL COMMA;Po;0;ON;<vertical> 002C;;;;N;;;;;
+FE11;PRESENTATION FORM FOR VERTICAL IDEOGRAPHIC COMMA;Po;0;ON;<vertical> 3001;;;;N;;;;;
+FE12;PRESENTATION FORM FOR VERTICAL IDEOGRAPHIC FULL STOP;Po;0;ON;<vertical> 3002;;;;N;;;;;
+FE13;PRESENTATION FORM FOR VERTICAL COLON;Po;0;ON;<vertical> 003A;;;;N;;;;;
+FE14;PRESENTATION FORM FOR VERTICAL SEMICOLON;Po;0;ON;<vertical> 003B;;;;N;;;;;
+FE15;PRESENTATION FORM FOR VERTICAL EXCLAMATION MARK;Po;0;ON;<vertical> 0021;;;;N;;;;;
+FE16;PRESENTATION FORM FOR VERTICAL QUESTION MARK;Po;0;ON;<vertical> 003F;;;;N;;;;;
+FE17;PRESENTATION FORM FOR VERTICAL LEFT WHITE LENTICULAR BRACKET;Ps;0;ON;<vertical> 3016;;;;N;;;;;
+FE18;PRESENTATION FORM FOR VERTICAL RIGHT WHITE LENTICULAR BRAKCET;Pe;0;ON;<vertical> 3017;;;;N;;;;;
+FE19;PRESENTATION FORM FOR VERTICAL HORIZONTAL ELLIPSIS;Po;0;ON;<vertical> 2026;;;;N;;;;;
+FE20;COMBINING LIGATURE LEFT HALF;Mn;230;NSM;;;;;N;;;;;
+FE21;COMBINING LIGATURE RIGHT HALF;Mn;230;NSM;;;;;N;;;;;
+FE22;COMBINING DOUBLE TILDE LEFT HALF;Mn;230;NSM;;;;;N;;;;;
+FE23;COMBINING DOUBLE TILDE RIGHT HALF;Mn;230;NSM;;;;;N;;;;;
+FE24;COMBINING MACRON LEFT HALF;Mn;230;NSM;;;;;N;;;;;
+FE25;COMBINING MACRON RIGHT HALF;Mn;230;NSM;;;;;N;;;;;
+FE26;COMBINING CONJOINING MACRON;Mn;230;NSM;;;;;N;;;;;
+FE27;COMBINING LIGATURE LEFT HALF BELOW;Mn;220;NSM;;;;;N;;;;;
+FE28;COMBINING LIGATURE RIGHT HALF BELOW;Mn;220;NSM;;;;;N;;;;;
+FE29;COMBINING TILDE LEFT HALF BELOW;Mn;220;NSM;;;;;N;;;;;
+FE2A;COMBINING TILDE RIGHT HALF BELOW;Mn;220;NSM;;;;;N;;;;;
+FE2B;COMBINING MACRON LEFT HALF BELOW;Mn;220;NSM;;;;;N;;;;;
+FE2C;COMBINING MACRON RIGHT HALF BELOW;Mn;220;NSM;;;;;N;;;;;
+FE2D;COMBINING CONJOINING MACRON BELOW;Mn;220;NSM;;;;;N;;;;;
+FE2E;COMBINING CYRILLIC TITLO LEFT HALF;Mn;230;NSM;;;;;N;;;;;
+FE2F;COMBINING CYRILLIC TITLO RIGHT HALF;Mn;230;NSM;;;;;N;;;;;
+FE30;PRESENTATION FORM FOR VERTICAL TWO DOT LEADER;Po;0;ON;<vertical> 2025;;;;N;GLYPH FOR VERTICAL TWO DOT LEADER;;;;
+FE31;PRESENTATION FORM FOR VERTICAL EM DASH;Pd;0;ON;<vertical> 2014;;;;N;GLYPH FOR VERTICAL EM DASH;;;;
+FE32;PRESENTATION FORM FOR VERTICAL EN DASH;Pd;0;ON;<vertical> 2013;;;;N;GLYPH FOR VERTICAL EN DASH;;;;
+FE33;PRESENTATION FORM FOR VERTICAL LOW LINE;Pc;0;ON;<vertical> 005F;;;;N;GLYPH FOR VERTICAL SPACING UNDERSCORE;;;;
+FE34;PRESENTATION FORM FOR VERTICAL WAVY LOW LINE;Pc;0;ON;<vertical> 005F;;;;N;GLYPH FOR VERTICAL SPACING WAVY UNDERSCORE;;;;
+FE35;PRESENTATION FORM FOR VERTICAL LEFT PARENTHESIS;Ps;0;ON;<vertical> 0028;;;;N;GLYPH FOR VERTICAL OPENING PARENTHESIS;;;;
+FE36;PRESENTATION FORM FOR VERTICAL RIGHT PARENTHESIS;Pe;0;ON;<vertical> 0029;;;;N;GLYPH FOR VERTICAL CLOSING PARENTHESIS;;;;
+FE37;PRESENTATION FORM FOR VERTICAL LEFT CURLY BRACKET;Ps;0;ON;<vertical> 007B;;;;N;GLYPH FOR VERTICAL OPENING CURLY BRACKET;;;;
+FE38;PRESENTATION FORM FOR VERTICAL RIGHT CURLY BRACKET;Pe;0;ON;<vertical> 007D;;;;N;GLYPH FOR VERTICAL CLOSING CURLY BRACKET;;;;
+FE39;PRESENTATION FORM FOR VERTICAL LEFT TORTOISE SHELL BRACKET;Ps;0;ON;<vertical> 3014;;;;N;GLYPH FOR VERTICAL OPENING TORTOISE SHELL BRACKET;;;;
+FE3A;PRESENTATION FORM FOR VERTICAL RIGHT TORTOISE SHELL BRACKET;Pe;0;ON;<vertical> 3015;;;;N;GLYPH FOR VERTICAL CLOSING TORTOISE SHELL BRACKET;;;;
+FE3B;PRESENTATION FORM FOR VERTICAL LEFT BLACK LENTICULAR BRACKET;Ps;0;ON;<vertical> 3010;;;;N;GLYPH FOR VERTICAL OPENING BLACK LENTICULAR BRACKET;;;;
+FE3C;PRESENTATION FORM FOR VERTICAL RIGHT BLACK LENTICULAR BRACKET;Pe;0;ON;<vertical> 3011;;;;N;GLYPH FOR VERTICAL CLOSING BLACK LENTICULAR BRACKET;;;;
+FE3D;PRESENTATION FORM FOR VERTICAL LEFT DOUBLE ANGLE BRACKET;Ps;0;ON;<vertical> 300A;;;;N;GLYPH FOR VERTICAL OPENING DOUBLE ANGLE BRACKET;;;;
+FE3E;PRESENTATION FORM FOR VERTICAL RIGHT DOUBLE ANGLE BRACKET;Pe;0;ON;<vertical> 300B;;;;N;GLYPH FOR VERTICAL CLOSING DOUBLE ANGLE BRACKET;;;;
+FE3F;PRESENTATION FORM FOR VERTICAL LEFT ANGLE BRACKET;Ps;0;ON;<vertical> 3008;;;;N;GLYPH FOR VERTICAL OPENING ANGLE BRACKET;;;;
+FE40;PRESENTATION FORM FOR VERTICAL RIGHT ANGLE BRACKET;Pe;0;ON;<vertical> 3009;;;;N;GLYPH FOR VERTICAL CLOSING ANGLE BRACKET;;;;
+FE41;PRESENTATION FORM FOR VERTICAL LEFT CORNER BRACKET;Ps;0;ON;<vertical> 300C;;;;N;GLYPH FOR VERTICAL OPENING CORNER BRACKET;;;;
+FE42;PRESENTATION FORM FOR VERTICAL RIGHT CORNER BRACKET;Pe;0;ON;<vertical> 300D;;;;N;GLYPH FOR VERTICAL CLOSING CORNER BRACKET;;;;
+FE43;PRESENTATION FORM FOR VERTICAL LEFT WHITE CORNER BRACKET;Ps;0;ON;<vertical> 300E;;;;N;GLYPH FOR VERTICAL OPENING WHITE CORNER BRACKET;;;;
+FE44;PRESENTATION FORM FOR VERTICAL RIGHT WHITE CORNER BRACKET;Pe;0;ON;<vertical> 300F;;;;N;GLYPH FOR VERTICAL CLOSING WHITE CORNER BRACKET;;;;
+FE45;SESAME DOT;Po;0;ON;;;;;N;;;;;
+FE46;WHITE SESAME DOT;Po;0;ON;;;;;N;;;;;
+FE47;PRESENTATION FORM FOR VERTICAL LEFT SQUARE BRACKET;Ps;0;ON;<vertical> 005B;;;;N;;;;;
+FE48;PRESENTATION FORM FOR VERTICAL RIGHT SQUARE BRACKET;Pe;0;ON;<vertical> 005D;;;;N;;;;;
+FE49;DASHED OVERLINE;Po;0;ON;<compat> 203E;;;;N;SPACING DASHED OVERSCORE;;;;
+FE4A;CENTRELINE OVERLINE;Po;0;ON;<compat> 203E;;;;N;SPACING CENTERLINE OVERSCORE;;;;
+FE4B;WAVY OVERLINE;Po;0;ON;<compat> 203E;;;;N;SPACING WAVY OVERSCORE;;;;
+FE4C;DOUBLE WAVY OVERLINE;Po;0;ON;<compat> 203E;;;;N;SPACING DOUBLE WAVY OVERSCORE;;;;
+FE4D;DASHED LOW LINE;Pc;0;ON;<compat> 005F;;;;N;SPACING DASHED UNDERSCORE;;;;
+FE4E;CENTRELINE LOW LINE;Pc;0;ON;<compat> 005F;;;;N;SPACING CENTERLINE UNDERSCORE;;;;
+FE4F;WAVY LOW LINE;Pc;0;ON;<compat> 005F;;;;N;SPACING WAVY UNDERSCORE;;;;
+FE50;SMALL COMMA;Po;0;CS;<small> 002C;;;;N;;;;;
+FE51;SMALL IDEOGRAPHIC COMMA;Po;0;ON;<small> 3001;;;;N;;;;;
+FE52;SMALL FULL STOP;Po;0;CS;<small> 002E;;;;N;SMALL PERIOD;;;;
+FE54;SMALL SEMICOLON;Po;0;ON;<small> 003B;;;;N;;;;;
+FE55;SMALL COLON;Po;0;CS;<small> 003A;;;;N;;;;;
+FE56;SMALL QUESTION MARK;Po;0;ON;<small> 003F;;;;N;;;;;
+FE57;SMALL EXCLAMATION MARK;Po;0;ON;<small> 0021;;;;N;;;;;
+FE58;SMALL EM DASH;Pd;0;ON;<small> 2014;;;;N;;;;;
+FE59;SMALL LEFT PARENTHESIS;Ps;0;ON;<small> 0028;;;;Y;SMALL OPENING PARENTHESIS;;;;
+FE5A;SMALL RIGHT PARENTHESIS;Pe;0;ON;<small> 0029;;;;Y;SMALL CLOSING PARENTHESIS;;;;
+FE5B;SMALL LEFT CURLY BRACKET;Ps;0;ON;<small> 007B;;;;Y;SMALL OPENING CURLY BRACKET;;;;
+FE5C;SMALL RIGHT CURLY BRACKET;Pe;0;ON;<small> 007D;;;;Y;SMALL CLOSING CURLY BRACKET;;;;
+FE5D;SMALL LEFT TORTOISE SHELL BRACKET;Ps;0;ON;<small> 3014;;;;Y;SMALL OPENING TORTOISE SHELL BRACKET;;;;
+FE5E;SMALL RIGHT TORTOISE SHELL BRACKET;Pe;0;ON;<small> 3015;;;;Y;SMALL CLOSING TORTOISE SHELL BRACKET;;;;
+FE5F;SMALL NUMBER SIGN;Po;0;ET;<small> 0023;;;;N;;;;;
+FE60;SMALL AMPERSAND;Po;0;ON;<small> 0026;;;;N;;;;;
+FE61;SMALL ASTERISK;Po;0;ON;<small> 002A;;;;N;;;;;
+FE62;SMALL PLUS SIGN;Sm;0;ES;<small> 002B;;;;N;;;;;
+FE63;SMALL HYPHEN-MINUS;Pd;0;ES;<small> 002D;;;;N;;;;;
+FE64;SMALL LESS-THAN SIGN;Sm;0;ON;<small> 003C;;;;Y;;;;;
+FE65;SMALL GREATER-THAN SIGN;Sm;0;ON;<small> 003E;;;;Y;;;;;
+FE66;SMALL EQUALS SIGN;Sm;0;ON;<small> 003D;;;;N;;;;;
+FE68;SMALL REVERSE SOLIDUS;Po;0;ON;<small> 005C;;;;N;SMALL BACKSLASH;;;;
+FE69;SMALL DOLLAR SIGN;Sc;0;ET;<small> 0024;;;;N;;;;;
+FE6A;SMALL PERCENT SIGN;Po;0;ET;<small> 0025;;;;N;;;;;
+FE6B;SMALL COMMERCIAL AT;Po;0;ON;<small> 0040;;;;N;;;;;
+FE70;ARABIC FATHATAN ISOLATED FORM;Lo;0;AL;<isolated> 0020 064B;;;;N;ARABIC SPACING FATHATAN;;;;
+FE71;ARABIC TATWEEL WITH FATHATAN ABOVE;Lo;0;AL;<medial> 0640 064B;;;;N;ARABIC FATHATAN ON TATWEEL;;;;
+FE72;ARABIC DAMMATAN ISOLATED FORM;Lo;0;AL;<isolated> 0020 064C;;;;N;ARABIC SPACING DAMMATAN;;;;
+FE73;ARABIC TAIL FRAGMENT;Lo;0;AL;;;;;N;;;;;
+FE74;ARABIC KASRATAN ISOLATED FORM;Lo;0;AL;<isolated> 0020 064D;;;;N;ARABIC SPACING KASRATAN;;;;
+FE76;ARABIC FATHA ISOLATED FORM;Lo;0;AL;<isolated> 0020 064E;;;;N;ARABIC SPACING FATHAH;;;;
+FE77;ARABIC FATHA MEDIAL FORM;Lo;0;AL;<medial> 0640 064E;;;;N;ARABIC FATHAH ON TATWEEL;;;;
+FE78;ARABIC DAMMA ISOLATED FORM;Lo;0;AL;<isolated> 0020 064F;;;;N;ARABIC SPACING DAMMAH;;;;
+FE79;ARABIC DAMMA MEDIAL FORM;Lo;0;AL;<medial> 0640 064F;;;;N;ARABIC DAMMAH ON TATWEEL;;;;
+FE7A;ARABIC KASRA ISOLATED FORM;Lo;0;AL;<isolated> 0020 0650;;;;N;ARABIC SPACING KASRAH;;;;
+FE7B;ARABIC KASRA MEDIAL FORM;Lo;0;AL;<medial> 0640 0650;;;;N;ARABIC KASRAH ON TATWEEL;;;;
+FE7C;ARABIC SHADDA ISOLATED FORM;Lo;0;AL;<isolated> 0020 0651;;;;N;ARABIC SPACING SHADDAH;;;;
+FE7D;ARABIC SHADDA MEDIAL FORM;Lo;0;AL;<medial> 0640 0651;;;;N;ARABIC SHADDAH ON TATWEEL;;;;
+FE7E;ARABIC SUKUN ISOLATED FORM;Lo;0;AL;<isolated> 0020 0652;;;;N;ARABIC SPACING SUKUN;;;;
+FE7F;ARABIC SUKUN MEDIAL FORM;Lo;0;AL;<medial> 0640 0652;;;;N;ARABIC SUKUN ON TATWEEL;;;;
+FE80;ARABIC LETTER HAMZA ISOLATED FORM;Lo;0;AL;<isolated> 0621;;;;N;GLYPH FOR ISOLATE ARABIC HAMZAH;;;;
+FE81;ARABIC LETTER ALEF WITH MADDA ABOVE ISOLATED FORM;Lo;0;AL;<isolated> 0622;;;;N;GLYPH FOR ISOLATE ARABIC MADDAH ON ALEF;;;;
+FE82;ARABIC LETTER ALEF WITH MADDA ABOVE FINAL FORM;Lo;0;AL;<final> 0622;;;;N;GLYPH FOR FINAL ARABIC MADDAH ON ALEF;;;;
+FE83;ARABIC LETTER ALEF WITH HAMZA ABOVE ISOLATED FORM;Lo;0;AL;<isolated> 0623;;;;N;GLYPH FOR ISOLATE ARABIC HAMZAH ON ALEF;;;;
+FE84;ARABIC LETTER ALEF WITH HAMZA ABOVE FINAL FORM;Lo;0;AL;<final> 0623;;;;N;GLYPH FOR FINAL ARABIC HAMZAH ON ALEF;;;;
+FE85;ARABIC LETTER WAW WITH HAMZA ABOVE ISOLATED FORM;Lo;0;AL;<isolated> 0624;;;;N;GLYPH FOR ISOLATE ARABIC HAMZAH ON WAW;;;;
+FE86;ARABIC LETTER WAW WITH HAMZA ABOVE FINAL FORM;Lo;0;AL;<final> 0624;;;;N;GLYPH FOR FINAL ARABIC HAMZAH ON WAW;;;;
+FE87;ARABIC LETTER ALEF WITH HAMZA BELOW ISOLATED FORM;Lo;0;AL;<isolated> 0625;;;;N;GLYPH FOR ISOLATE ARABIC HAMZAH UNDER ALEF;;;;
+FE88;ARABIC LETTER ALEF WITH HAMZA BELOW FINAL FORM;Lo;0;AL;<final> 0625;;;;N;GLYPH FOR FINAL ARABIC HAMZAH UNDER ALEF;;;;
+FE89;ARABIC LETTER YEH WITH HAMZA ABOVE ISOLATED FORM;Lo;0;AL;<isolated> 0626;;;;N;GLYPH FOR ISOLATE ARABIC HAMZAH ON YA;;;;
+FE8A;ARABIC LETTER YEH WITH HAMZA ABOVE FINAL FORM;Lo;0;AL;<final> 0626;;;;N;GLYPH FOR FINAL ARABIC HAMZAH ON YA;;;;
+FE8B;ARABIC LETTER YEH WITH HAMZA ABOVE INITIAL FORM;Lo;0;AL;<initial> 0626;;;;N;GLYPH FOR INITIAL ARABIC HAMZAH ON YA;;;;
+FE8C;ARABIC LETTER YEH WITH HAMZA ABOVE MEDIAL FORM;Lo;0;AL;<medial> 0626;;;;N;GLYPH FOR MEDIAL ARABIC HAMZAH ON YA;;;;
+FE8D;ARABIC LETTER ALEF ISOLATED FORM;Lo;0;AL;<isolated> 0627;;;;N;GLYPH FOR ISOLATE ARABIC ALEF;;;;
+FE8E;ARABIC LETTER ALEF FINAL FORM;Lo;0;AL;<final> 0627;;;;N;GLYPH FOR FINAL ARABIC ALEF;;;;
+FE8F;ARABIC LETTER BEH ISOLATED FORM;Lo;0;AL;<isolated> 0628;;;;N;GLYPH FOR ISOLATE ARABIC BAA;;;;
+FE90;ARABIC LETTER BEH FINAL FORM;Lo;0;AL;<final> 0628;;;;N;GLYPH FOR FINAL ARABIC BAA;;;;
+FE91;ARABIC LETTER BEH INITIAL FORM;Lo;0;AL;<initial> 0628;;;;N;GLYPH FOR INITIAL ARABIC BAA;;;;
+FE92;ARABIC LETTER BEH MEDIAL FORM;Lo;0;AL;<medial> 0628;;;;N;GLYPH FOR MEDIAL ARABIC BAA;;;;
+FE93;ARABIC LETTER TEH MARBUTA ISOLATED FORM;Lo;0;AL;<isolated> 0629;;;;N;GLYPH FOR ISOLATE ARABIC TAA MARBUTAH;;;;
+FE94;ARABIC LETTER TEH MARBUTA FINAL FORM;Lo;0;AL;<final> 0629;;;;N;GLYPH FOR FINAL ARABIC TAA MARBUTAH;;;;
+FE95;ARABIC LETTER TEH ISOLATED FORM;Lo;0;AL;<isolated> 062A;;;;N;GLYPH FOR ISOLATE ARABIC TAA;;;;
+FE96;ARABIC LETTER TEH FINAL FORM;Lo;0;AL;<final> 062A;;;;N;GLYPH FOR FINAL ARABIC TAA;;;;
+FE97;ARABIC LETTER TEH INITIAL FORM;Lo;0;AL;<initial> 062A;;;;N;GLYPH FOR INITIAL ARABIC TAA;;;;
+FE98;ARABIC LETTER TEH MEDIAL FORM;Lo;0;AL;<medial> 062A;;;;N;GLYPH FOR MEDIAL ARABIC TAA;;;;
+FE99;ARABIC LETTER THEH ISOLATED FORM;Lo;0;AL;<isolated> 062B;;;;N;GLYPH FOR ISOLATE ARABIC THAA;;;;
+FE9A;ARABIC LETTER THEH FINAL FORM;Lo;0;AL;<final> 062B;;;;N;GLYPH FOR FINAL ARABIC THAA;;;;
+FE9B;ARABIC LETTER THEH INITIAL FORM;Lo;0;AL;<initial> 062B;;;;N;GLYPH FOR INITIAL ARABIC THAA;;;;
+FE9C;ARABIC LETTER THEH MEDIAL FORM;Lo;0;AL;<medial> 062B;;;;N;GLYPH FOR MEDIAL ARABIC THAA;;;;
+FE9D;ARABIC LETTER JEEM ISOLATED FORM;Lo;0;AL;<isolated> 062C;;;;N;GLYPH FOR ISOLATE ARABIC JEEM;;;;
+FE9E;ARABIC LETTER JEEM FINAL FORM;Lo;0;AL;<final> 062C;;;;N;GLYPH FOR FINAL ARABIC JEEM;;;;
+FE9F;ARABIC LETTER JEEM INITIAL FORM;Lo;0;AL;<initial> 062C;;;;N;GLYPH FOR INITIAL ARABIC JEEM;;;;
+FEA0;ARABIC LETTER JEEM MEDIAL FORM;Lo;0;AL;<medial> 062C;;;;N;GLYPH FOR MEDIAL ARABIC JEEM;;;;
+FEA1;ARABIC LETTER HAH ISOLATED FORM;Lo;0;AL;<isolated> 062D;;;;N;GLYPH FOR ISOLATE ARABIC HAA;;;;
+FEA2;ARABIC LETTER HAH FINAL FORM;Lo;0;AL;<final> 062D;;;;N;GLYPH FOR FINAL ARABIC HAA;;;;
+FEA3;ARABIC LETTER HAH INITIAL FORM;Lo;0;AL;<initial> 062D;;;;N;GLYPH FOR INITIAL ARABIC HAA;;;;
+FEA4;ARABIC LETTER HAH MEDIAL FORM;Lo;0;AL;<medial> 062D;;;;N;GLYPH FOR MEDIAL ARABIC HAA;;;;
+FEA5;ARABIC LETTER KHAH ISOLATED FORM;Lo;0;AL;<isolated> 062E;;;;N;GLYPH FOR ISOLATE ARABIC KHAA;;;;
+FEA6;ARABIC LETTER KHAH FINAL FORM;Lo;0;AL;<final> 062E;;;;N;GLYPH FOR FINAL ARABIC KHAA;;;;
+FEA7;ARABIC LETTER KHAH INITIAL FORM;Lo;0;AL;<initial> 062E;;;;N;GLYPH FOR INITIAL ARABIC KHAA;;;;
+FEA8;ARABIC LETTER KHAH MEDIAL FORM;Lo;0;AL;<medial> 062E;;;;N;GLYPH FOR MEDIAL ARABIC KHAA;;;;
+FEA9;ARABIC LETTER DAL ISOLATED FORM;Lo;0;AL;<isolated> 062F;;;;N;GLYPH FOR ISOLATE ARABIC DAL;;;;
+FEAA;ARABIC LETTER DAL FINAL FORM;Lo;0;AL;<final> 062F;;;;N;GLYPH FOR FINAL ARABIC DAL;;;;
+FEAB;ARABIC LETTER THAL ISOLATED FORM;Lo;0;AL;<isolated> 0630;;;;N;GLYPH FOR ISOLATE ARABIC THAL;;;;
+FEAC;ARABIC LETTER THAL FINAL FORM;Lo;0;AL;<final> 0630;;;;N;GLYPH FOR FINAL ARABIC THAL;;;;
+FEAD;ARABIC LETTER REH ISOLATED FORM;Lo;0;AL;<isolated> 0631;;;;N;GLYPH FOR ISOLATE ARABIC RA;;;;
+FEAE;ARABIC LETTER REH FINAL FORM;Lo;0;AL;<final> 0631;;;;N;GLYPH FOR FINAL ARABIC RA;;;;
+FEAF;ARABIC LETTER ZAIN ISOLATED FORM;Lo;0;AL;<isolated> 0632;;;;N;GLYPH FOR ISOLATE ARABIC ZAIN;;;;
+FEB0;ARABIC LETTER ZAIN FINAL FORM;Lo;0;AL;<final> 0632;;;;N;GLYPH FOR FINAL ARABIC ZAIN;;;;
+FEB1;ARABIC LETTER SEEN ISOLATED FORM;Lo;0;AL;<isolated> 0633;;;;N;GLYPH FOR ISOLATE ARABIC SEEN;;;;
+FEB2;ARABIC LETTER SEEN FINAL FORM;Lo;0;AL;<final> 0633;;;;N;GLYPH FOR FINAL ARABIC SEEN;;;;
+FEB3;ARABIC LETTER SEEN INITIAL FORM;Lo;0;AL;<initial> 0633;;;;N;GLYPH FOR INITIAL ARABIC SEEN;;;;
+FEB4;ARABIC LETTER SEEN MEDIAL FORM;Lo;0;AL;<medial> 0633;;;;N;GLYPH FOR MEDIAL ARABIC SEEN;;;;
+FEB5;ARABIC LETTER SHEEN ISOLATED FORM;Lo;0;AL;<isolated> 0634;;;;N;GLYPH FOR ISOLATE ARABIC SHEEN;;;;
+FEB6;ARABIC LETTER SHEEN FINAL FORM;Lo;0;AL;<final> 0634;;;;N;GLYPH FOR FINAL ARABIC SHEEN;;;;
+FEB7;ARABIC LETTER SHEEN INITIAL FORM;Lo;0;AL;<initial> 0634;;;;N;GLYPH FOR INITIAL ARABIC SHEEN;;;;
+FEB8;ARABIC LETTER SHEEN MEDIAL FORM;Lo;0;AL;<medial> 0634;;;;N;GLYPH FOR MEDIAL ARABIC SHEEN;;;;
+FEB9;ARABIC LETTER SAD ISOLATED FORM;Lo;0;AL;<isolated> 0635;;;;N;GLYPH FOR ISOLATE ARABIC SAD;;;;
+FEBA;ARABIC LETTER SAD FINAL FORM;Lo;0;AL;<final> 0635;;;;N;GLYPH FOR FINAL ARABIC SAD;;;;
+FEBB;ARABIC LETTER SAD INITIAL FORM;Lo;0;AL;<initial> 0635;;;;N;GLYPH FOR INITIAL ARABIC SAD;;;;
+FEBC;ARABIC LETTER SAD MEDIAL FORM;Lo;0;AL;<medial> 0635;;;;N;GLYPH FOR MEDIAL ARABIC SAD;;;;
+FEBD;ARABIC LETTER DAD ISOLATED FORM;Lo;0;AL;<isolated> 0636;;;;N;GLYPH FOR ISOLATE ARABIC DAD;;;;
+FEBE;ARABIC LETTER DAD FINAL FORM;Lo;0;AL;<final> 0636;;;;N;GLYPH FOR FINAL ARABIC DAD;;;;
+FEBF;ARABIC LETTER DAD INITIAL FORM;Lo;0;AL;<initial> 0636;;;;N;GLYPH FOR INITIAL ARABIC DAD;;;;
+FEC0;ARABIC LETTER DAD MEDIAL FORM;Lo;0;AL;<medial> 0636;;;;N;GLYPH FOR MEDIAL ARABIC DAD;;;;
+FEC1;ARABIC LETTER TAH ISOLATED FORM;Lo;0;AL;<isolated> 0637;;;;N;GLYPH FOR ISOLATE ARABIC TAH;;;;
+FEC2;ARABIC LETTER TAH FINAL FORM;Lo;0;AL;<final> 0637;;;;N;GLYPH FOR FINAL ARABIC TAH;;;;
+FEC3;ARABIC LETTER TAH INITIAL FORM;Lo;0;AL;<initial> 0637;;;;N;GLYPH FOR INITIAL ARABIC TAH;;;;
+FEC4;ARABIC LETTER TAH MEDIAL FORM;Lo;0;AL;<medial> 0637;;;;N;GLYPH FOR MEDIAL ARABIC TAH;;;;
+FEC5;ARABIC LETTER ZAH ISOLATED FORM;Lo;0;AL;<isolated> 0638;;;;N;GLYPH FOR ISOLATE ARABIC DHAH;;;;
+FEC6;ARABIC LETTER ZAH FINAL FORM;Lo;0;AL;<final> 0638;;;;N;GLYPH FOR FINAL ARABIC DHAH;;;;
+FEC7;ARABIC LETTER ZAH INITIAL FORM;Lo;0;AL;<initial> 0638;;;;N;GLYPH FOR INITIAL ARABIC DHAH;;;;
+FEC8;ARABIC LETTER ZAH MEDIAL FORM;Lo;0;AL;<medial> 0638;;;;N;GLYPH FOR MEDIAL ARABIC DHAH;;;;
+FEC9;ARABIC LETTER AIN ISOLATED FORM;Lo;0;AL;<isolated> 0639;;;;N;GLYPH FOR ISOLATE ARABIC AIN;;;;
+FECA;ARABIC LETTER AIN FINAL FORM;Lo;0;AL;<final> 0639;;;;N;GLYPH FOR FINAL ARABIC AIN;;;;
+FECB;ARABIC LETTER AIN INITIAL FORM;Lo;0;AL;<initial> 0639;;;;N;GLYPH FOR INITIAL ARABIC AIN;;;;
+FECC;ARABIC LETTER AIN MEDIAL FORM;Lo;0;AL;<medial> 0639;;;;N;GLYPH FOR MEDIAL ARABIC AIN;;;;
+FECD;ARABIC LETTER GHAIN ISOLATED FORM;Lo;0;AL;<isolated> 063A;;;;N;GLYPH FOR ISOLATE ARABIC GHAIN;;;;
+FECE;ARABIC LETTER GHAIN FINAL FORM;Lo;0;AL;<final> 063A;;;;N;GLYPH FOR FINAL ARABIC GHAIN;;;;
+FECF;ARABIC LETTER GHAIN INITIAL FORM;Lo;0;AL;<initial> 063A;;;;N;GLYPH FOR INITIAL ARABIC GHAIN;;;;
+FED0;ARABIC LETTER GHAIN MEDIAL FORM;Lo;0;AL;<medial> 063A;;;;N;GLYPH FOR MEDIAL ARABIC GHAIN;;;;
+FED1;ARABIC LETTER FEH ISOLATED FORM;Lo;0;AL;<isolated> 0641;;;;N;GLYPH FOR ISOLATE ARABIC FA;;;;
+FED2;ARABIC LETTER FEH FINAL FORM;Lo;0;AL;<final> 0641;;;;N;GLYPH FOR FINAL ARABIC FA;;;;
+FED3;ARABIC LETTER FEH INITIAL FORM;Lo;0;AL;<initial> 0641;;;;N;GLYPH FOR INITIAL ARABIC FA;;;;
+FED4;ARABIC LETTER FEH MEDIAL FORM;Lo;0;AL;<medial> 0641;;;;N;GLYPH FOR MEDIAL ARABIC FA;;;;
+FED5;ARABIC LETTER QAF ISOLATED FORM;Lo;0;AL;<isolated> 0642;;;;N;GLYPH FOR ISOLATE ARABIC QAF;;;;
+FED6;ARABIC LETTER QAF FINAL FORM;Lo;0;AL;<final> 0642;;;;N;GLYPH FOR FINAL ARABIC QAF;;;;
+FED7;ARABIC LETTER QAF INITIAL FORM;Lo;0;AL;<initial> 0642;;;;N;GLYPH FOR INITIAL ARABIC QAF;;;;
+FED8;ARABIC LETTER QAF MEDIAL FORM;Lo;0;AL;<medial> 0642;;;;N;GLYPH FOR MEDIAL ARABIC QAF;;;;
+FED9;ARABIC LETTER KAF ISOLATED FORM;Lo;0;AL;<isolated> 0643;;;;N;GLYPH FOR ISOLATE ARABIC CAF;;;;
+FEDA;ARABIC LETTER KAF FINAL FORM;Lo;0;AL;<final> 0643;;;;N;GLYPH FOR FINAL ARABIC CAF;;;;
+FEDB;ARABIC LETTER KAF INITIAL FORM;Lo;0;AL;<initial> 0643;;;;N;GLYPH FOR INITIAL ARABIC CAF;;;;
+FEDC;ARABIC LETTER KAF MEDIAL FORM;Lo;0;AL;<medial> 0643;;;;N;GLYPH FOR MEDIAL ARABIC CAF;;;;
+FEDD;ARABIC LETTER LAM ISOLATED FORM;Lo;0;AL;<isolated> 0644;;;;N;GLYPH FOR ISOLATE ARABIC LAM;;;;
+FEDE;ARABIC LETTER LAM FINAL FORM;Lo;0;AL;<final> 0644;;;;N;GLYPH FOR FINAL ARABIC LAM;;;;
+FEDF;ARABIC LETTER LAM INITIAL FORM;Lo;0;AL;<initial> 0644;;;;N;GLYPH FOR INITIAL ARABIC LAM;;;;
+FEE0;ARABIC LETTER LAM MEDIAL FORM;Lo;0;AL;<medial> 0644;;;;N;GLYPH FOR MEDIAL ARABIC LAM;;;;
+FEE1;ARABIC LETTER MEEM ISOLATED FORM;Lo;0;AL;<isolated> 0645;;;;N;GLYPH FOR ISOLATE ARABIC MEEM;;;;
+FEE2;ARABIC LETTER MEEM FINAL FORM;Lo;0;AL;<final> 0645;;;;N;GLYPH FOR FINAL ARABIC MEEM;;;;
+FEE3;ARABIC LETTER MEEM INITIAL FORM;Lo;0;AL;<initial> 0645;;;;N;GLYPH FOR INITIAL ARABIC MEEM;;;;
+FEE4;ARABIC LETTER MEEM MEDIAL FORM;Lo;0;AL;<medial> 0645;;;;N;GLYPH FOR MEDIAL ARABIC MEEM;;;;
+FEE5;ARABIC LETTER NOON ISOLATED FORM;Lo;0;AL;<isolated> 0646;;;;N;GLYPH FOR ISOLATE ARABIC NOON;;;;
+FEE6;ARABIC LETTER NOON FINAL FORM;Lo;0;AL;<final> 0646;;;;N;GLYPH FOR FINAL ARABIC NOON;;;;
+FEE7;ARABIC LETTER NOON INITIAL FORM;Lo;0;AL;<initial> 0646;;;;N;GLYPH FOR INITIAL ARABIC NOON;;;;
+FEE8;ARABIC LETTER NOON MEDIAL FORM;Lo;0;AL;<medial> 0646;;;;N;GLYPH FOR MEDIAL ARABIC NOON;;;;
+FEE9;ARABIC LETTER HEH ISOLATED FORM;Lo;0;AL;<isolated> 0647;;;;N;GLYPH FOR ISOLATE ARABIC HA;;;;
+FEEA;ARABIC LETTER HEH FINAL FORM;Lo;0;AL;<final> 0647;;;;N;GLYPH FOR FINAL ARABIC HA;;;;
+FEEB;ARABIC LETTER HEH INITIAL FORM;Lo;0;AL;<initial> 0647;;;;N;GLYPH FOR INITIAL ARABIC HA;;;;
+FEEC;ARABIC LETTER HEH MEDIAL FORM;Lo;0;AL;<medial> 0647;;;;N;GLYPH FOR MEDIAL ARABIC HA;;;;
+FEED;ARABIC LETTER WAW ISOLATED FORM;Lo;0;AL;<isolated> 0648;;;;N;GLYPH FOR ISOLATE ARABIC WAW;;;;
+FEEE;ARABIC LETTER WAW FINAL FORM;Lo;0;AL;<final> 0648;;;;N;GLYPH FOR FINAL ARABIC WAW;;;;
+FEEF;ARABIC LETTER ALEF MAKSURA ISOLATED FORM;Lo;0;AL;<isolated> 0649;;;;N;GLYPH FOR ISOLATE ARABIC ALEF MAQSURAH;;;;
+FEF0;ARABIC LETTER ALEF MAKSURA FINAL FORM;Lo;0;AL;<final> 0649;;;;N;GLYPH FOR FINAL ARABIC ALEF MAQSURAH;;;;
+FEF1;ARABIC LETTER YEH ISOLATED FORM;Lo;0;AL;<isolated> 064A;;;;N;GLYPH FOR ISOLATE ARABIC YA;;;;
+FEF2;ARABIC LETTER YEH FINAL FORM;Lo;0;AL;<final> 064A;;;;N;GLYPH FOR FINAL ARABIC YA;;;;
+FEF3;ARABIC LETTER YEH INITIAL FORM;Lo;0;AL;<initial> 064A;;;;N;GLYPH FOR INITIAL ARABIC YA;;;;
+FEF4;ARABIC LETTER YEH MEDIAL FORM;Lo;0;AL;<medial> 064A;;;;N;GLYPH FOR MEDIAL ARABIC YA;;;;
+FEF5;ARABIC LIGATURE LAM WITH ALEF WITH MADDA ABOVE ISOLATED FORM;Lo;0;AL;<isolated> 0644 0622;;;;N;GLYPH FOR ISOLATE ARABIC MADDAH ON LIGATURE LAM ALEF;;;;
+FEF6;ARABIC LIGATURE LAM WITH ALEF WITH MADDA ABOVE FINAL FORM;Lo;0;AL;<final> 0644 0622;;;;N;GLYPH FOR FINAL ARABIC MADDAH ON LIGATURE LAM ALEF;;;;
+FEF7;ARABIC LIGATURE LAM WITH ALEF WITH HAMZA ABOVE ISOLATED FORM;Lo;0;AL;<isolated> 0644 0623;;;;N;GLYPH FOR ISOLATE ARABIC HAMZAH ON LIGATURE LAM ALEF;;;;
+FEF8;ARABIC LIGATURE LAM WITH ALEF WITH HAMZA ABOVE FINAL FORM;Lo;0;AL;<final> 0644 0623;;;;N;GLYPH FOR FINAL ARABIC HAMZAH ON LIGATURE LAM ALEF;;;;
+FEF9;ARABIC LIGATURE LAM WITH ALEF WITH HAMZA BELOW ISOLATED FORM;Lo;0;AL;<isolated> 0644 0625;;;;N;GLYPH FOR ISOLATE ARABIC HAMZAH UNDER LIGATURE LAM ALEF;;;;
+FEFA;ARABIC LIGATURE LAM WITH ALEF WITH HAMZA BELOW FINAL FORM;Lo;0;AL;<final> 0644 0625;;;;N;GLYPH FOR FINAL ARABIC HAMZAH UNDER LIGATURE LAM ALEF;;;;
+FEFB;ARABIC LIGATURE LAM WITH ALEF ISOLATED FORM;Lo;0;AL;<isolated> 0644 0627;;;;N;GLYPH FOR ISOLATE ARABIC LIGATURE LAM ALEF;;;;
+FEFC;ARABIC LIGATURE LAM WITH ALEF FINAL FORM;Lo;0;AL;<final> 0644 0627;;;;N;GLYPH FOR FINAL ARABIC LIGATURE LAM ALEF;;;;
+FEFF;ZERO WIDTH NO-BREAK SPACE;Cf;0;BN;;;;;N;BYTE ORDER MARK;;;;
+FF01;FULLWIDTH EXCLAMATION MARK;Po;0;ON;<wide> 0021;;;;N;;;;;
+FF02;FULLWIDTH QUOTATION MARK;Po;0;ON;<wide> 0022;;;;N;;;;;
+FF03;FULLWIDTH NUMBER SIGN;Po;0;ET;<wide> 0023;;;;N;;;;;
+FF04;FULLWIDTH DOLLAR SIGN;Sc;0;ET;<wide> 0024;;;;N;;;;;
+FF05;FULLWIDTH PERCENT SIGN;Po;0;ET;<wide> 0025;;;;N;;;;;
+FF06;FULLWIDTH AMPERSAND;Po;0;ON;<wide> 0026;;;;N;;;;;
+FF07;FULLWIDTH APOSTROPHE;Po;0;ON;<wide> 0027;;;;N;;;;;
+FF08;FULLWIDTH LEFT PARENTHESIS;Ps;0;ON;<wide> 0028;;;;Y;FULLWIDTH OPENING PARENTHESIS;;;;
+FF09;FULLWIDTH RIGHT PARENTHESIS;Pe;0;ON;<wide> 0029;;;;Y;FULLWIDTH CLOSING PARENTHESIS;;;;
+FF0A;FULLWIDTH ASTERISK;Po;0;ON;<wide> 002A;;;;N;;;;;
+FF0B;FULLWIDTH PLUS SIGN;Sm;0;ES;<wide> 002B;;;;N;;;;;
+FF0C;FULLWIDTH COMMA;Po;0;CS;<wide> 002C;;;;N;;;;;
+FF0D;FULLWIDTH HYPHEN-MINUS;Pd;0;ES;<wide> 002D;;;;N;;;;;
+FF0E;FULLWIDTH FULL STOP;Po;0;CS;<wide> 002E;;;;N;FULLWIDTH PERIOD;;;;
+FF0F;FULLWIDTH SOLIDUS;Po;0;CS;<wide> 002F;;;;N;FULLWIDTH SLASH;;;;
+FF10;FULLWIDTH DIGIT ZERO;Nd;0;EN;<wide> 0030;0;0;0;N;;;;;
+FF11;FULLWIDTH DIGIT ONE;Nd;0;EN;<wide> 0031;1;1;1;N;;;;;
+FF12;FULLWIDTH DIGIT TWO;Nd;0;EN;<wide> 0032;2;2;2;N;;;;;
+FF13;FULLWIDTH DIGIT THREE;Nd;0;EN;<wide> 0033;3;3;3;N;;;;;
+FF14;FULLWIDTH DIGIT FOUR;Nd;0;EN;<wide> 0034;4;4;4;N;;;;;
+FF15;FULLWIDTH DIGIT FIVE;Nd;0;EN;<wide> 0035;5;5;5;N;;;;;
+FF16;FULLWIDTH DIGIT SIX;Nd;0;EN;<wide> 0036;6;6;6;N;;;;;
+FF17;FULLWIDTH DIGIT SEVEN;Nd;0;EN;<wide> 0037;7;7;7;N;;;;;
+FF18;FULLWIDTH DIGIT EIGHT;Nd;0;EN;<wide> 0038;8;8;8;N;;;;;
+FF19;FULLWIDTH DIGIT NINE;Nd;0;EN;<wide> 0039;9;9;9;N;;;;;
+FF1A;FULLWIDTH COLON;Po;0;CS;<wide> 003A;;;;N;;;;;
+FF1B;FULLWIDTH SEMICOLON;Po;0;ON;<wide> 003B;;;;N;;;;;
+FF1C;FULLWIDTH LESS-THAN SIGN;Sm;0;ON;<wide> 003C;;;;Y;;;;;
+FF1D;FULLWIDTH EQUALS SIGN;Sm;0;ON;<wide> 003D;;;;N;;;;;
+FF1E;FULLWIDTH GREATER-THAN SIGN;Sm;0;ON;<wide> 003E;;;;Y;;;;;
+FF1F;FULLWIDTH QUESTION MARK;Po;0;ON;<wide> 003F;;;;N;;;;;
+FF20;FULLWIDTH COMMERCIAL AT;Po;0;ON;<wide> 0040;;;;N;;;;;
+FF21;FULLWIDTH LATIN CAPITAL LETTER A;Lu;0;L;<wide> 0041;;;;N;;;;FF41;
+FF22;FULLWIDTH LATIN CAPITAL LETTER B;Lu;0;L;<wide> 0042;;;;N;;;;FF42;
+FF23;FULLWIDTH LATIN CAPITAL LETTER C;Lu;0;L;<wide> 0043;;;;N;;;;FF43;
+FF24;FULLWIDTH LATIN CAPITAL LETTER D;Lu;0;L;<wide> 0044;;;;N;;;;FF44;
+FF25;FULLWIDTH LATIN CAPITAL LETTER E;Lu;0;L;<wide> 0045;;;;N;;;;FF45;
+FF26;FULLWIDTH LATIN CAPITAL LETTER F;Lu;0;L;<wide> 0046;;;;N;;;;FF46;
+FF27;FULLWIDTH LATIN CAPITAL LETTER G;Lu;0;L;<wide> 0047;;;;N;;;;FF47;
+FF28;FULLWIDTH LATIN CAPITAL LETTER H;Lu;0;L;<wide> 0048;;;;N;;;;FF48;
+FF29;FULLWIDTH LATIN CAPITAL LETTER I;Lu;0;L;<wide> 0049;;;;N;;;;FF49;
+FF2A;FULLWIDTH LATIN CAPITAL LETTER J;Lu;0;L;<wide> 004A;;;;N;;;;FF4A;
+FF2B;FULLWIDTH LATIN CAPITAL LETTER K;Lu;0;L;<wide> 004B;;;;N;;;;FF4B;
+FF2C;FULLWIDTH LATIN CAPITAL LETTER L;Lu;0;L;<wide> 004C;;;;N;;;;FF4C;
+FF2D;FULLWIDTH LATIN CAPITAL LETTER M;Lu;0;L;<wide> 004D;;;;N;;;;FF4D;
+FF2E;FULLWIDTH LATIN CAPITAL LETTER N;Lu;0;L;<wide> 004E;;;;N;;;;FF4E;
+FF2F;FULLWIDTH LATIN CAPITAL LETTER O;Lu;0;L;<wide> 004F;;;;N;;;;FF4F;
+FF30;FULLWIDTH LATIN CAPITAL LETTER P;Lu;0;L;<wide> 0050;;;;N;;;;FF50;
+FF31;FULLWIDTH LATIN CAPITAL LETTER Q;Lu;0;L;<wide> 0051;;;;N;;;;FF51;
+FF32;FULLWIDTH LATIN CAPITAL LETTER R;Lu;0;L;<wide> 0052;;;;N;;;;FF52;
+FF33;FULLWIDTH LATIN CAPITAL LETTER S;Lu;0;L;<wide> 0053;;;;N;;;;FF53;
+FF34;FULLWIDTH LATIN CAPITAL LETTER T;Lu;0;L;<wide> 0054;;;;N;;;;FF54;
+FF35;FULLWIDTH LATIN CAPITAL LETTER U;Lu;0;L;<wide> 0055;;;;N;;;;FF55;
+FF36;FULLWIDTH LATIN CAPITAL LETTER V;Lu;0;L;<wide> 0056;;;;N;;;;FF56;
+FF37;FULLWIDTH LATIN CAPITAL LETTER W;Lu;0;L;<wide> 0057;;;;N;;;;FF57;
+FF38;FULLWIDTH LATIN CAPITAL LETTER X;Lu;0;L;<wide> 0058;;;;N;;;;FF58;
+FF39;FULLWIDTH LATIN CAPITAL LETTER Y;Lu;0;L;<wide> 0059;;;;N;;;;FF59;
+FF3A;FULLWIDTH LATIN CAPITAL LETTER Z;Lu;0;L;<wide> 005A;;;;N;;;;FF5A;
+FF3B;FULLWIDTH LEFT SQUARE BRACKET;Ps;0;ON;<wide> 005B;;;;Y;FULLWIDTH OPENING SQUARE BRACKET;;;;
+FF3C;FULLWIDTH REVERSE SOLIDUS;Po;0;ON;<wide> 005C;;;;N;FULLWIDTH BACKSLASH;;;;
+FF3D;FULLWIDTH RIGHT SQUARE BRACKET;Pe;0;ON;<wide> 005D;;;;Y;FULLWIDTH CLOSING SQUARE BRACKET;;;;
+FF3E;FULLWIDTH CIRCUMFLEX ACCENT;Sk;0;ON;<wide> 005E;;;;N;FULLWIDTH SPACING CIRCUMFLEX;;;;
+FF3F;FULLWIDTH LOW LINE;Pc;0;ON;<wide> 005F;;;;N;FULLWIDTH SPACING UNDERSCORE;;;;
+FF40;FULLWIDTH GRAVE ACCENT;Sk;0;ON;<wide> 0060;;;;N;FULLWIDTH SPACING GRAVE;;;;
+FF41;FULLWIDTH LATIN SMALL LETTER A;Ll;0;L;<wide> 0061;;;;N;;;FF21;;FF21
+FF42;FULLWIDTH LATIN SMALL LETTER B;Ll;0;L;<wide> 0062;;;;N;;;FF22;;FF22
+FF43;FULLWIDTH LATIN SMALL LETTER C;Ll;0;L;<wide> 0063;;;;N;;;FF23;;FF23
+FF44;FULLWIDTH LATIN SMALL LETTER D;Ll;0;L;<wide> 0064;;;;N;;;FF24;;FF24
+FF45;FULLWIDTH LATIN SMALL LETTER E;Ll;0;L;<wide> 0065;;;;N;;;FF25;;FF25
+FF46;FULLWIDTH LATIN SMALL LETTER F;Ll;0;L;<wide> 0066;;;;N;;;FF26;;FF26
+FF47;FULLWIDTH LATIN SMALL LETTER G;Ll;0;L;<wide> 0067;;;;N;;;FF27;;FF27
+FF48;FULLWIDTH LATIN SMALL LETTER H;Ll;0;L;<wide> 0068;;;;N;;;FF28;;FF28
+FF49;FULLWIDTH LATIN SMALL LETTER I;Ll;0;L;<wide> 0069;;;;N;;;FF29;;FF29
+FF4A;FULLWIDTH LATIN SMALL LETTER J;Ll;0;L;<wide> 006A;;;;N;;;FF2A;;FF2A
+FF4B;FULLWIDTH LATIN SMALL LETTER K;Ll;0;L;<wide> 006B;;;;N;;;FF2B;;FF2B
+FF4C;FULLWIDTH LATIN SMALL LETTER L;Ll;0;L;<wide> 006C;;;;N;;;FF2C;;FF2C
+FF4D;FULLWIDTH LATIN SMALL LETTER M;Ll;0;L;<wide> 006D;;;;N;;;FF2D;;FF2D
+FF4E;FULLWIDTH LATIN SMALL LETTER N;Ll;0;L;<wide> 006E;;;;N;;;FF2E;;FF2E
+FF4F;FULLWIDTH LATIN SMALL LETTER O;Ll;0;L;<wide> 006F;;;;N;;;FF2F;;FF2F
+FF50;FULLWIDTH LATIN SMALL LETTER P;Ll;0;L;<wide> 0070;;;;N;;;FF30;;FF30
+FF51;FULLWIDTH LATIN SMALL LETTER Q;Ll;0;L;<wide> 0071;;;;N;;;FF31;;FF31
+FF52;FULLWIDTH LATIN SMALL LETTER R;Ll;0;L;<wide> 0072;;;;N;;;FF32;;FF32
+FF53;FULLWIDTH LATIN SMALL LETTER S;Ll;0;L;<wide> 0073;;;;N;;;FF33;;FF33
+FF54;FULLWIDTH LATIN SMALL LETTER T;Ll;0;L;<wide> 0074;;;;N;;;FF34;;FF34
+FF55;FULLWIDTH LATIN SMALL LETTER U;Ll;0;L;<wide> 0075;;;;N;;;FF35;;FF35
+FF56;FULLWIDTH LATIN SMALL LETTER V;Ll;0;L;<wide> 0076;;;;N;;;FF36;;FF36
+FF57;FULLWIDTH LATIN SMALL LETTER W;Ll;0;L;<wide> 0077;;;;N;;;FF37;;FF37
+FF58;FULLWIDTH LATIN SMALL LETTER X;Ll;0;L;<wide> 0078;;;;N;;;FF38;;FF38
+FF59;FULLWIDTH LATIN SMALL LETTER Y;Ll;0;L;<wide> 0079;;;;N;;;FF39;;FF39
+FF5A;FULLWIDTH LATIN SMALL LETTER Z;Ll;0;L;<wide> 007A;;;;N;;;FF3A;;FF3A
+FF5B;FULLWIDTH LEFT CURLY BRACKET;Ps;0;ON;<wide> 007B;;;;Y;FULLWIDTH OPENING CURLY BRACKET;;;;
+FF5C;FULLWIDTH VERTICAL LINE;Sm;0;ON;<wide> 007C;;;;N;FULLWIDTH VERTICAL BAR;;;;
+FF5D;FULLWIDTH RIGHT CURLY BRACKET;Pe;0;ON;<wide> 007D;;;;Y;FULLWIDTH CLOSING CURLY BRACKET;;;;
+FF5E;FULLWIDTH TILDE;Sm;0;ON;<wide> 007E;;;;N;FULLWIDTH SPACING TILDE;;;;
+FF5F;FULLWIDTH LEFT WHITE PARENTHESIS;Ps;0;ON;<wide> 2985;;;;Y;;;;;
+FF60;FULLWIDTH RIGHT WHITE PARENTHESIS;Pe;0;ON;<wide> 2986;;;;Y;;;;;
+FF61;HALFWIDTH IDEOGRAPHIC FULL STOP;Po;0;ON;<narrow> 3002;;;;N;HALFWIDTH IDEOGRAPHIC PERIOD;;;;
+FF62;HALFWIDTH LEFT CORNER BRACKET;Ps;0;ON;<narrow> 300C;;;;Y;HALFWIDTH OPENING CORNER BRACKET;;;;
+FF63;HALFWIDTH RIGHT CORNER BRACKET;Pe;0;ON;<narrow> 300D;;;;Y;HALFWIDTH CLOSING CORNER BRACKET;;;;
+FF64;HALFWIDTH IDEOGRAPHIC COMMA;Po;0;ON;<narrow> 3001;;;;N;;;;;
+FF65;HALFWIDTH KATAKANA MIDDLE DOT;Po;0;ON;<narrow> 30FB;;;;N;;;;;
+FF66;HALFWIDTH KATAKANA LETTER WO;Lo;0;L;<narrow> 30F2;;;;N;;;;;
+FF67;HALFWIDTH KATAKANA LETTER SMALL A;Lo;0;L;<narrow> 30A1;;;;N;;;;;
+FF68;HALFWIDTH KATAKANA LETTER SMALL I;Lo;0;L;<narrow> 30A3;;;;N;;;;;
+FF69;HALFWIDTH KATAKANA LETTER SMALL U;Lo;0;L;<narrow> 30A5;;;;N;;;;;
+FF6A;HALFWIDTH KATAKANA LETTER SMALL E;Lo;0;L;<narrow> 30A7;;;;N;;;;;
+FF6B;HALFWIDTH KATAKANA LETTER SMALL O;Lo;0;L;<narrow> 30A9;;;;N;;;;;
+FF6C;HALFWIDTH KATAKANA LETTER SMALL YA;Lo;0;L;<narrow> 30E3;;;;N;;;;;
+FF6D;HALFWIDTH KATAKANA LETTER SMALL YU;Lo;0;L;<narrow> 30E5;;;;N;;;;;
+FF6E;HALFWIDTH KATAKANA LETTER SMALL YO;Lo;0;L;<narrow> 30E7;;;;N;;;;;
+FF6F;HALFWIDTH KATAKANA LETTER SMALL TU;Lo;0;L;<narrow> 30C3;;;;N;;;;;
+FF70;HALFWIDTH KATAKANA-HIRAGANA PROLONGED SOUND MARK;Lm;0;L;<narrow> 30FC;;;;N;;;;;
+FF71;HALFWIDTH KATAKANA LETTER A;Lo;0;L;<narrow> 30A2;;;;N;;;;;
+FF72;HALFWIDTH KATAKANA LETTER I;Lo;0;L;<narrow> 30A4;;;;N;;;;;
+FF73;HALFWIDTH KATAKANA LETTER U;Lo;0;L;<narrow> 30A6;;;;N;;;;;
+FF74;HALFWIDTH KATAKANA LETTER E;Lo;0;L;<narrow> 30A8;;;;N;;;;;
+FF75;HALFWIDTH KATAKANA LETTER O;Lo;0;L;<narrow> 30AA;;;;N;;;;;
+FF76;HALFWIDTH KATAKANA LETTER KA;Lo;0;L;<narrow> 30AB;;;;N;;;;;
+FF77;HALFWIDTH KATAKANA LETTER KI;Lo;0;L;<narrow> 30AD;;;;N;;;;;
+FF78;HALFWIDTH KATAKANA LETTER KU;Lo;0;L;<narrow> 30AF;;;;N;;;;;
+FF79;HALFWIDTH KATAKANA LETTER KE;Lo;0;L;<narrow> 30B1;;;;N;;;;;
+FF7A;HALFWIDTH KATAKANA LETTER KO;Lo;0;L;<narrow> 30B3;;;;N;;;;;
+FF7B;HALFWIDTH KATAKANA LETTER SA;Lo;0;L;<narrow> 30B5;;;;N;;;;;
+FF7C;HALFWIDTH KATAKANA LETTER SI;Lo;0;L;<narrow> 30B7;;;;N;;;;;
+FF7D;HALFWIDTH KATAKANA LETTER SU;Lo;0;L;<narrow> 30B9;;;;N;;;;;
+FF7E;HALFWIDTH KATAKANA LETTER SE;Lo;0;L;<narrow> 30BB;;;;N;;;;;
+FF7F;HALFWIDTH KATAKANA LETTER SO;Lo;0;L;<narrow> 30BD;;;;N;;;;;
+FF80;HALFWIDTH KATAKANA LETTER TA;Lo;0;L;<narrow> 30BF;;;;N;;;;;
+FF81;HALFWIDTH KATAKANA LETTER TI;Lo;0;L;<narrow> 30C1;;;;N;;;;;
+FF82;HALFWIDTH KATAKANA LETTER TU;Lo;0;L;<narrow> 30C4;;;;N;;;;;
+FF83;HALFWIDTH KATAKANA LETTER TE;Lo;0;L;<narrow> 30C6;;;;N;;;;;
+FF84;HALFWIDTH KATAKANA LETTER TO;Lo;0;L;<narrow> 30C8;;;;N;;;;;
+FF85;HALFWIDTH KATAKANA LETTER NA;Lo;0;L;<narrow> 30CA;;;;N;;;;;
+FF86;HALFWIDTH KATAKANA LETTER NI;Lo;0;L;<narrow> 30CB;;;;N;;;;;
+FF87;HALFWIDTH KATAKANA LETTER NU;Lo;0;L;<narrow> 30CC;;;;N;;;;;
+FF88;HALFWIDTH KATAKANA LETTER NE;Lo;0;L;<narrow> 30CD;;;;N;;;;;
+FF89;HALFWIDTH KATAKANA LETTER NO;Lo;0;L;<narrow> 30CE;;;;N;;;;;
+FF8A;HALFWIDTH KATAKANA LETTER HA;Lo;0;L;<narrow> 30CF;;;;N;;;;;
+FF8B;HALFWIDTH KATAKANA LETTER HI;Lo;0;L;<narrow> 30D2;;;;N;;;;;
+FF8C;HALFWIDTH KATAKANA LETTER HU;Lo;0;L;<narrow> 30D5;;;;N;;;;;
+FF8D;HALFWIDTH KATAKANA LETTER HE;Lo;0;L;<narrow> 30D8;;;;N;;;;;
+FF8E;HALFWIDTH KATAKANA LETTER HO;Lo;0;L;<narrow> 30DB;;;;N;;;;;
+FF8F;HALFWIDTH KATAKANA LETTER MA;Lo;0;L;<narrow> 30DE;;;;N;;;;;
+FF90;HALFWIDTH KATAKANA LETTER MI;Lo;0;L;<narrow> 30DF;;;;N;;;;;
+FF91;HALFWIDTH KATAKANA LETTER MU;Lo;0;L;<narrow> 30E0;;;;N;;;;;
+FF92;HALFWIDTH KATAKANA LETTER ME;Lo;0;L;<narrow> 30E1;;;;N;;;;;
+FF93;HALFWIDTH KATAKANA LETTER MO;Lo;0;L;<narrow> 30E2;;;;N;;;;;
+FF94;HALFWIDTH KATAKANA LETTER YA;Lo;0;L;<narrow> 30E4;;;;N;;;;;
+FF95;HALFWIDTH KATAKANA LETTER YU;Lo;0;L;<narrow> 30E6;;;;N;;;;;
+FF96;HALFWIDTH KATAKANA LETTER YO;Lo;0;L;<narrow> 30E8;;;;N;;;;;
+FF97;HALFWIDTH KATAKANA LETTER RA;Lo;0;L;<narrow> 30E9;;;;N;;;;;
+FF98;HALFWIDTH KATAKANA LETTER RI;Lo;0;L;<narrow> 30EA;;;;N;;;;;
+FF99;HALFWIDTH KATAKANA LETTER RU;Lo;0;L;<narrow> 30EB;;;;N;;;;;
+FF9A;HALFWIDTH KATAKANA LETTER RE;Lo;0;L;<narrow> 30EC;;;;N;;;;;
+FF9B;HALFWIDTH KATAKANA LETTER RO;Lo;0;L;<narrow> 30ED;;;;N;;;;;
+FF9C;HALFWIDTH KATAKANA LETTER WA;Lo;0;L;<narrow> 30EF;;;;N;;;;;
+FF9D;HALFWIDTH KATAKANA LETTER N;Lo;0;L;<narrow> 30F3;;;;N;;;;;
+FF9E;HALFWIDTH KATAKANA VOICED SOUND MARK;Lm;0;L;<narrow> 3099;;;;N;;;;;
+FF9F;HALFWIDTH KATAKANA SEMI-VOICED SOUND MARK;Lm;0;L;<narrow> 309A;;;;N;;;;;
+FFA0;HALFWIDTH HANGUL FILLER;Lo;0;L;<narrow> 3164;;;;N;HALFWIDTH HANGUL CAE OM;;;;
+FFA1;HALFWIDTH HANGUL LETTER KIYEOK;Lo;0;L;<narrow> 3131;;;;N;HALFWIDTH HANGUL LETTER GIYEOG;;;;
+FFA2;HALFWIDTH HANGUL LETTER SSANGKIYEOK;Lo;0;L;<narrow> 3132;;;;N;HALFWIDTH HANGUL LETTER SSANG GIYEOG;;;;
+FFA3;HALFWIDTH HANGUL LETTER KIYEOK-SIOS;Lo;0;L;<narrow> 3133;;;;N;HALFWIDTH HANGUL LETTER GIYEOG SIOS;;;;
+FFA4;HALFWIDTH HANGUL LETTER NIEUN;Lo;0;L;<narrow> 3134;;;;N;;;;;
+FFA5;HALFWIDTH HANGUL LETTER NIEUN-CIEUC;Lo;0;L;<narrow> 3135;;;;N;HALFWIDTH HANGUL LETTER NIEUN JIEUJ;;;;
+FFA6;HALFWIDTH HANGUL LETTER NIEUN-HIEUH;Lo;0;L;<narrow> 3136;;;;N;HALFWIDTH HANGUL LETTER NIEUN HIEUH;;;;
+FFA7;HALFWIDTH HANGUL LETTER TIKEUT;Lo;0;L;<narrow> 3137;;;;N;HALFWIDTH HANGUL LETTER DIGEUD;;;;
+FFA8;HALFWIDTH HANGUL LETTER SSANGTIKEUT;Lo;0;L;<narrow> 3138;;;;N;HALFWIDTH HANGUL LETTER SSANG DIGEUD;;;;
+FFA9;HALFWIDTH HANGUL LETTER RIEUL;Lo;0;L;<narrow> 3139;;;;N;HALFWIDTH HANGUL LETTER LIEUL;;;;
+FFAA;HALFWIDTH HANGUL LETTER RIEUL-KIYEOK;Lo;0;L;<narrow> 313A;;;;N;HALFWIDTH HANGUL LETTER LIEUL GIYEOG;;;;
+FFAB;HALFWIDTH HANGUL LETTER RIEUL-MIEUM;Lo;0;L;<narrow> 313B;;;;N;HALFWIDTH HANGUL LETTER LIEUL MIEUM;;;;
+FFAC;HALFWIDTH HANGUL LETTER RIEUL-PIEUP;Lo;0;L;<narrow> 313C;;;;N;HALFWIDTH HANGUL LETTER LIEUL BIEUB;;;;
+FFAD;HALFWIDTH HANGUL LETTER RIEUL-SIOS;Lo;0;L;<narrow> 313D;;;;N;HALFWIDTH HANGUL LETTER LIEUL SIOS;;;;
+FFAE;HALFWIDTH HANGUL LETTER RIEUL-THIEUTH;Lo;0;L;<narrow> 313E;;;;N;HALFWIDTH HANGUL LETTER LIEUL TIEUT;;;;
+FFAF;HALFWIDTH HANGUL LETTER RIEUL-PHIEUPH;Lo;0;L;<narrow> 313F;;;;N;HALFWIDTH HANGUL LETTER LIEUL PIEUP;;;;
+FFB0;HALFWIDTH HANGUL LETTER RIEUL-HIEUH;Lo;0;L;<narrow> 3140;;;;N;HALFWIDTH HANGUL LETTER LIEUL HIEUH;;;;
+FFB1;HALFWIDTH HANGUL LETTER MIEUM;Lo;0;L;<narrow> 3141;;;;N;;;;;
+FFB2;HALFWIDTH HANGUL LETTER PIEUP;Lo;0;L;<narrow> 3142;;;;N;HALFWIDTH HANGUL LETTER BIEUB;;;;
+FFB3;HALFWIDTH HANGUL LETTER SSANGPIEUP;Lo;0;L;<narrow> 3143;;;;N;HALFWIDTH HANGUL LETTER SSANG BIEUB;;;;
+FFB4;HALFWIDTH HANGUL LETTER PIEUP-SIOS;Lo;0;L;<narrow> 3144;;;;N;HALFWIDTH HANGUL LETTER BIEUB SIOS;;;;
+FFB5;HALFWIDTH HANGUL LETTER SIOS;Lo;0;L;<narrow> 3145;;;;N;;;;;
+FFB6;HALFWIDTH HANGUL LETTER SSANGSIOS;Lo;0;L;<narrow> 3146;;;;N;HALFWIDTH HANGUL LETTER SSANG SIOS;;;;
+FFB7;HALFWIDTH HANGUL LETTER IEUNG;Lo;0;L;<narrow> 3147;;;;N;;;;;
+FFB8;HALFWIDTH HANGUL LETTER CIEUC;Lo;0;L;<narrow> 3148;;;;N;HALFWIDTH HANGUL LETTER JIEUJ;;;;
+FFB9;HALFWIDTH HANGUL LETTER SSANGCIEUC;Lo;0;L;<narrow> 3149;;;;N;HALFWIDTH HANGUL LETTER SSANG JIEUJ;;;;
+FFBA;HALFWIDTH HANGUL LETTER CHIEUCH;Lo;0;L;<narrow> 314A;;;;N;HALFWIDTH HANGUL LETTER CIEUC;;;;
+FFBB;HALFWIDTH HANGUL LETTER KHIEUKH;Lo;0;L;<narrow> 314B;;;;N;HALFWIDTH HANGUL LETTER KIYEOK;;;;
+FFBC;HALFWIDTH HANGUL LETTER THIEUTH;Lo;0;L;<narrow> 314C;;;;N;HALFWIDTH HANGUL LETTER TIEUT;;;;
+FFBD;HALFWIDTH HANGUL LETTER PHIEUPH;Lo;0;L;<narrow> 314D;;;;N;HALFWIDTH HANGUL LETTER PIEUP;;;;
+FFBE;HALFWIDTH HANGUL LETTER HIEUH;Lo;0;L;<narrow> 314E;;;;N;;;;;
+FFC2;HALFWIDTH HANGUL LETTER A;Lo;0;L;<narrow> 314F;;;;N;;;;;
+FFC3;HALFWIDTH HANGUL LETTER AE;Lo;0;L;<narrow> 3150;;;;N;;;;;
+FFC4;HALFWIDTH HANGUL LETTER YA;Lo;0;L;<narrow> 3151;;;;N;;;;;
+FFC5;HALFWIDTH HANGUL LETTER YAE;Lo;0;L;<narrow> 3152;;;;N;;;;;
+FFC6;HALFWIDTH HANGUL LETTER EO;Lo;0;L;<narrow> 3153;;;;N;;;;;
+FFC7;HALFWIDTH HANGUL LETTER E;Lo;0;L;<narrow> 3154;;;;N;;;;;
+FFCA;HALFWIDTH HANGUL LETTER YEO;Lo;0;L;<narrow> 3155;;;;N;;;;;
+FFCB;HALFWIDTH HANGUL LETTER YE;Lo;0;L;<narrow> 3156;;;;N;;;;;
+FFCC;HALFWIDTH HANGUL LETTER O;Lo;0;L;<narrow> 3157;;;;N;;;;;
+FFCD;HALFWIDTH HANGUL LETTER WA;Lo;0;L;<narrow> 3158;;;;N;;;;;
+FFCE;HALFWIDTH HANGUL LETTER WAE;Lo;0;L;<narrow> 3159;;;;N;;;;;
+FFCF;HALFWIDTH HANGUL LETTER OE;Lo;0;L;<narrow> 315A;;;;N;;;;;
+FFD2;HALFWIDTH HANGUL LETTER YO;Lo;0;L;<narrow> 315B;;;;N;;;;;
+FFD3;HALFWIDTH HANGUL LETTER U;Lo;0;L;<narrow> 315C;;;;N;;;;;
+FFD4;HALFWIDTH HANGUL LETTER WEO;Lo;0;L;<narrow> 315D;;;;N;;;;;
+FFD5;HALFWIDTH HANGUL LETTER WE;Lo;0;L;<narrow> 315E;;;;N;;;;;
+FFD6;HALFWIDTH HANGUL LETTER WI;Lo;0;L;<narrow> 315F;;;;N;;;;;
+FFD7;HALFWIDTH HANGUL LETTER YU;Lo;0;L;<narrow> 3160;;;;N;;;;;
+FFDA;HALFWIDTH HANGUL LETTER EU;Lo;0;L;<narrow> 3161;;;;N;;;;;
+FFDB;HALFWIDTH HANGUL LETTER YI;Lo;0;L;<narrow> 3162;;;;N;;;;;
+FFDC;HALFWIDTH HANGUL LETTER I;Lo;0;L;<narrow> 3163;;;;N;;;;;
+FFE0;FULLWIDTH CENT SIGN;Sc;0;ET;<wide> 00A2;;;;N;;;;;
+FFE1;FULLWIDTH POUND SIGN;Sc;0;ET;<wide> 00A3;;;;N;;;;;
+FFE2;FULLWIDTH NOT SIGN;Sm;0;ON;<wide> 00AC;;;;N;;;;;
+FFE3;FULLWIDTH MACRON;Sk;0;ON;<wide> 00AF;;;;N;FULLWIDTH SPACING MACRON;;;;
+FFE4;FULLWIDTH BROKEN BAR;So;0;ON;<wide> 00A6;;;;N;FULLWIDTH BROKEN VERTICAL BAR;;;;
+FFE5;FULLWIDTH YEN SIGN;Sc;0;ET;<wide> 00A5;;;;N;;;;;
+FFE6;FULLWIDTH WON SIGN;Sc;0;ET;<wide> 20A9;;;;N;;;;;
+FFE8;HALFWIDTH FORMS LIGHT VERTICAL;So;0;ON;<narrow> 2502;;;;N;;;;;
+FFE9;HALFWIDTH LEFTWARDS ARROW;Sm;0;ON;<narrow> 2190;;;;N;;;;;
+FFEA;HALFWIDTH UPWARDS ARROW;Sm;0;ON;<narrow> 2191;;;;N;;;;;
+FFEB;HALFWIDTH RIGHTWARDS ARROW;Sm;0;ON;<narrow> 2192;;;;N;;;;;
+FFEC;HALFWIDTH DOWNWARDS ARROW;Sm;0;ON;<narrow> 2193;;;;N;;;;;
+FFED;HALFWIDTH BLACK SQUARE;So;0;ON;<narrow> 25A0;;;;N;;;;;
+FFEE;HALFWIDTH WHITE CIRCLE;So;0;ON;<narrow> 25CB;;;;N;;;;;
+FFF9;INTERLINEAR ANNOTATION ANCHOR;Cf;0;ON;;;;;N;;;;;
+FFFA;INTERLINEAR ANNOTATION SEPARATOR;Cf;0;ON;;;;;N;;;;;
+FFFB;INTERLINEAR ANNOTATION TERMINATOR;Cf;0;ON;;;;;N;;;;;
+FFFC;OBJECT REPLACEMENT CHARACTER;So;0;ON;;;;;N;;;;;
+FFFD;REPLACEMENT CHARACTER;So;0;ON;;;;;N;;;;;
+10000;LINEAR B SYLLABLE B008 A;Lo;0;L;;;;;N;;;;;
+10001;LINEAR B SYLLABLE B038 E;Lo;0;L;;;;;N;;;;;
+10002;LINEAR B SYLLABLE B028 I;Lo;0;L;;;;;N;;;;;
+10003;LINEAR B SYLLABLE B061 O;Lo;0;L;;;;;N;;;;;
+10004;LINEAR B SYLLABLE B010 U;Lo;0;L;;;;;N;;;;;
+10005;LINEAR B SYLLABLE B001 DA;Lo;0;L;;;;;N;;;;;
+10006;LINEAR B SYLLABLE B045 DE;Lo;0;L;;;;;N;;;;;
+10007;LINEAR B SYLLABLE B007 DI;Lo;0;L;;;;;N;;;;;
+10008;LINEAR B SYLLABLE B014 DO;Lo;0;L;;;;;N;;;;;
+10009;LINEAR B SYLLABLE B051 DU;Lo;0;L;;;;;N;;;;;
+1000A;LINEAR B SYLLABLE B057 JA;Lo;0;L;;;;;N;;;;;
+1000B;LINEAR B SYLLABLE B046 JE;Lo;0;L;;;;;N;;;;;
+1000D;LINEAR B SYLLABLE B036 JO;Lo;0;L;;;;;N;;;;;
+1000E;LINEAR B SYLLABLE B065 JU;Lo;0;L;;;;;N;;;;;
+1000F;LINEAR B SYLLABLE B077 KA;Lo;0;L;;;;;N;;;;;
+10010;LINEAR B SYLLABLE B044 KE;Lo;0;L;;;;;N;;;;;
+10011;LINEAR B SYLLABLE B067 KI;Lo;0;L;;;;;N;;;;;
+10012;LINEAR B SYLLABLE B070 KO;Lo;0;L;;;;;N;;;;;
+10013;LINEAR B SYLLABLE B081 KU;Lo;0;L;;;;;N;;;;;
+10014;LINEAR B SYLLABLE B080 MA;Lo;0;L;;;;;N;;;;;
+10015;LINEAR B SYLLABLE B013 ME;Lo;0;L;;;;;N;;;;;
+10016;LINEAR B SYLLABLE B073 MI;Lo;0;L;;;;;N;;;;;
+10017;LINEAR B SYLLABLE B015 MO;Lo;0;L;;;;;N;;;;;
+10018;LINEAR B SYLLABLE B023 MU;Lo;0;L;;;;;N;;;;;
+10019;LINEAR B SYLLABLE B006 NA;Lo;0;L;;;;;N;;;;;
+1001A;LINEAR B SYLLABLE B024 NE;Lo;0;L;;;;;N;;;;;
+1001B;LINEAR B SYLLABLE B030 NI;Lo;0;L;;;;;N;;;;;
+1001C;LINEAR B SYLLABLE B052 NO;Lo;0;L;;;;;N;;;;;
+1001D;LINEAR B SYLLABLE B055 NU;Lo;0;L;;;;;N;;;;;
+1001E;LINEAR B SYLLABLE B003 PA;Lo;0;L;;;;;N;;;;;
+1001F;LINEAR B SYLLABLE B072 PE;Lo;0;L;;;;;N;;;;;
+10020;LINEAR B SYLLABLE B039 PI;Lo;0;L;;;;;N;;;;;
+10021;LINEAR B SYLLABLE B011 PO;Lo;0;L;;;;;N;;;;;
+10022;LINEAR B SYLLABLE B050 PU;Lo;0;L;;;;;N;;;;;
+10023;LINEAR B SYLLABLE B016 QA;Lo;0;L;;;;;N;;;;;
+10024;LINEAR B SYLLABLE B078 QE;Lo;0;L;;;;;N;;;;;
+10025;LINEAR B SYLLABLE B021 QI;Lo;0;L;;;;;N;;;;;
+10026;LINEAR B SYLLABLE B032 QO;Lo;0;L;;;;;N;;;;;
+10028;LINEAR B SYLLABLE B060 RA;Lo;0;L;;;;;N;;;;;
+10029;LINEAR B SYLLABLE B027 RE;Lo;0;L;;;;;N;;;;;
+1002A;LINEAR B SYLLABLE B053 RI;Lo;0;L;;;;;N;;;;;
+1002B;LINEAR B SYLLABLE B002 RO;Lo;0;L;;;;;N;;;;;
+1002C;LINEAR B SYLLABLE B026 RU;Lo;0;L;;;;;N;;;;;
+1002D;LINEAR B SYLLABLE B031 SA;Lo;0;L;;;;;N;;;;;
+1002E;LINEAR B SYLLABLE B009 SE;Lo;0;L;;;;;N;;;;;
+1002F;LINEAR B SYLLABLE B041 SI;Lo;0;L;;;;;N;;;;;
+10030;LINEAR B SYLLABLE B012 SO;Lo;0;L;;;;;N;;;;;
+10031;LINEAR B SYLLABLE B058 SU;Lo;0;L;;;;;N;;;;;
+10032;LINEAR B SYLLABLE B059 TA;Lo;0;L;;;;;N;;;;;
+10033;LINEAR B SYLLABLE B004 TE;Lo;0;L;;;;;N;;;;;
+10034;LINEAR B SYLLABLE B037 TI;Lo;0;L;;;;;N;;;;;
+10035;LINEAR B SYLLABLE B005 TO;Lo;0;L;;;;;N;;;;;
+10036;LINEAR B SYLLABLE B069 TU;Lo;0;L;;;;;N;;;;;
+10037;LINEAR B SYLLABLE B054 WA;Lo;0;L;;;;;N;;;;;
+10038;LINEAR B SYLLABLE B075 WE;Lo;0;L;;;;;N;;;;;
+10039;LINEAR B SYLLABLE B040 WI;Lo;0;L;;;;;N;;;;;
+1003A;LINEAR B SYLLABLE B042 WO;Lo;0;L;;;;;N;;;;;
+1003C;LINEAR B SYLLABLE B017 ZA;Lo;0;L;;;;;N;;;;;
+1003D;LINEAR B SYLLABLE B074 ZE;Lo;0;L;;;;;N;;;;;
+1003F;LINEAR B SYLLABLE B020 ZO;Lo;0;L;;;;;N;;;;;
+10040;LINEAR B SYLLABLE B025 A2;Lo;0;L;;;;;N;;;;;
+10041;LINEAR B SYLLABLE B043 A3;Lo;0;L;;;;;N;;;;;
+10042;LINEAR B SYLLABLE B085 AU;Lo;0;L;;;;;N;;;;;
+10043;LINEAR B SYLLABLE B071 DWE;Lo;0;L;;;;;N;;;;;
+10044;LINEAR B SYLLABLE B090 DWO;Lo;0;L;;;;;N;;;;;
+10045;LINEAR B SYLLABLE B048 NWA;Lo;0;L;;;;;N;;;;;
+10046;LINEAR B SYLLABLE B029 PU2;Lo;0;L;;;;;N;;;;;
+10047;LINEAR B SYLLABLE B062 PTE;Lo;0;L;;;;;N;;;;;
+10048;LINEAR B SYLLABLE B076 RA2;Lo;0;L;;;;;N;;;;;
+10049;LINEAR B SYLLABLE B033 RA3;Lo;0;L;;;;;N;;;;;
+1004A;LINEAR B SYLLABLE B068 RO2;Lo;0;L;;;;;N;;;;;
+1004B;LINEAR B SYLLABLE B066 TA2;Lo;0;L;;;;;N;;;;;
+1004C;LINEAR B SYLLABLE B087 TWE;Lo;0;L;;;;;N;;;;;
+1004D;LINEAR B SYLLABLE B091 TWO;Lo;0;L;;;;;N;;;;;
+10050;LINEAR B SYMBOL B018;Lo;0;L;;;;;N;;;;;
+10051;LINEAR B SYMBOL B019;Lo;0;L;;;;;N;;;;;
+10052;LINEAR B SYMBOL B022;Lo;0;L;;;;;N;;;;;
+10053;LINEAR B SYMBOL B034;Lo;0;L;;;;;N;;;;;
+10054;LINEAR B SYMBOL B047;Lo;0;L;;;;;N;;;;;
+10055;LINEAR B SYMBOL B049;Lo;0;L;;;;;N;;;;;
+10056;LINEAR B SYMBOL B056;Lo;0;L;;;;;N;;;;;
+10057;LINEAR B SYMBOL B063;Lo;0;L;;;;;N;;;;;
+10058;LINEAR B SYMBOL B064;Lo;0;L;;;;;N;;;;;
+10059;LINEAR B SYMBOL B079;Lo;0;L;;;;;N;;;;;
+1005A;LINEAR B SYMBOL B082;Lo;0;L;;;;;N;;;;;
+1005B;LINEAR B SYMBOL B083;Lo;0;L;;;;;N;;;;;
+1005C;LINEAR B SYMBOL B086;Lo;0;L;;;;;N;;;;;
+1005D;LINEAR B SYMBOL B089;Lo;0;L;;;;;N;;;;;
+10080;LINEAR B IDEOGRAM B100 MAN;Lo;0;L;;;;;N;;;;;
+10081;LINEAR B IDEOGRAM B102 WOMAN;Lo;0;L;;;;;N;;;;;
+10082;LINEAR B IDEOGRAM B104 DEER;Lo;0;L;;;;;N;;;;;
+10083;LINEAR B IDEOGRAM B105 EQUID;Lo;0;L;;;;;N;;;;;
+10084;LINEAR B IDEOGRAM B105F MARE;Lo;0;L;;;;;N;;;;;
+10085;LINEAR B IDEOGRAM B105M STALLION;Lo;0;L;;;;;N;;;;;
+10086;LINEAR B IDEOGRAM B106F EWE;Lo;0;L;;;;;N;;;;;
+10087;LINEAR B IDEOGRAM B106M RAM;Lo;0;L;;;;;N;;;;;
+10088;LINEAR B IDEOGRAM B107F SHE-GOAT;Lo;0;L;;;;;N;;;;;
+10089;LINEAR B IDEOGRAM B107M HE-GOAT;Lo;0;L;;;;;N;;;;;
+1008A;LINEAR B IDEOGRAM B108F SOW;Lo;0;L;;;;;N;;;;;
+1008B;LINEAR B IDEOGRAM B108M BOAR;Lo;0;L;;;;;N;;;;;
+1008C;LINEAR B IDEOGRAM B109F COW;Lo;0;L;;;;;N;;;;;
+1008D;LINEAR B IDEOGRAM B109M BULL;Lo;0;L;;;;;N;;;;;
+1008E;LINEAR B IDEOGRAM B120 WHEAT;Lo;0;L;;;;;N;;;;;
+1008F;LINEAR B IDEOGRAM B121 BARLEY;Lo;0;L;;;;;N;;;;;
+10090;LINEAR B IDEOGRAM B122 OLIVE;Lo;0;L;;;;;N;;;;;
+10091;LINEAR B IDEOGRAM B123 SPICE;Lo;0;L;;;;;N;;;;;
+10092;LINEAR B IDEOGRAM B125 CYPERUS;Lo;0;L;;;;;N;;;;;
+10093;LINEAR B MONOGRAM B127 KAPO;Lo;0;L;;;;;N;;;;;
+10094;LINEAR B MONOGRAM B128 KANAKO;Lo;0;L;;;;;N;;;;;
+10095;LINEAR B IDEOGRAM B130 OIL;Lo;0;L;;;;;N;;;;;
+10096;LINEAR B IDEOGRAM B131 WINE;Lo;0;L;;;;;N;;;;;
+10097;LINEAR B IDEOGRAM B132;Lo;0;L;;;;;N;;;;;
+10098;LINEAR B MONOGRAM B133 AREPA;Lo;0;L;;;;;N;;;;;
+10099;LINEAR B MONOGRAM B135 MERI;Lo;0;L;;;;;N;;;;;
+1009A;LINEAR B IDEOGRAM B140 BRONZE;Lo;0;L;;;;;N;;;;;
+1009B;LINEAR B IDEOGRAM B141 GOLD;Lo;0;L;;;;;N;;;;;
+1009C;LINEAR B IDEOGRAM B142;Lo;0;L;;;;;N;;;;;
+1009D;LINEAR B IDEOGRAM B145 WOOL;Lo;0;L;;;;;N;;;;;
+1009E;LINEAR B IDEOGRAM B146;Lo;0;L;;;;;N;;;;;
+1009F;LINEAR B IDEOGRAM B150;Lo;0;L;;;;;N;;;;;
+100A0;LINEAR B IDEOGRAM B151 HORN;Lo;0;L;;;;;N;;;;;
+100A1;LINEAR B IDEOGRAM B152;Lo;0;L;;;;;N;;;;;
+100A2;LINEAR B IDEOGRAM B153;Lo;0;L;;;;;N;;;;;
+100A3;LINEAR B IDEOGRAM B154;Lo;0;L;;;;;N;;;;;
+100A4;LINEAR B MONOGRAM B156 TURO2;Lo;0;L;;;;;N;;;;;
+100A5;LINEAR B IDEOGRAM B157;Lo;0;L;;;;;N;;;;;
+100A6;LINEAR B IDEOGRAM B158;Lo;0;L;;;;;N;;;;;
+100A7;LINEAR B IDEOGRAM B159 CLOTH;Lo;0;L;;;;;N;;;;;
+100A8;LINEAR B IDEOGRAM B160;Lo;0;L;;;;;N;;;;;
+100A9;LINEAR B IDEOGRAM B161;Lo;0;L;;;;;N;;;;;
+100AA;LINEAR B IDEOGRAM B162 GARMENT;Lo;0;L;;;;;N;;;;;
+100AB;LINEAR B IDEOGRAM B163 ARMOUR;Lo;0;L;;;;;N;;;;;
+100AC;LINEAR B IDEOGRAM B164;Lo;0;L;;;;;N;;;;;
+100AD;LINEAR B IDEOGRAM B165;Lo;0;L;;;;;N;;;;;
+100AE;LINEAR B IDEOGRAM B166;Lo;0;L;;;;;N;;;;;
+100AF;LINEAR B IDEOGRAM B167;Lo;0;L;;;;;N;;;;;
+100B0;LINEAR B IDEOGRAM B168;Lo;0;L;;;;;N;;;;;
+100B1;LINEAR B IDEOGRAM B169;Lo;0;L;;;;;N;;;;;
+100B2;LINEAR B IDEOGRAM B170;Lo;0;L;;;;;N;;;;;
+100B3;LINEAR B IDEOGRAM B171;Lo;0;L;;;;;N;;;;;
+100B4;LINEAR B IDEOGRAM B172;Lo;0;L;;;;;N;;;;;
+100B5;LINEAR B IDEOGRAM B173 MONTH;Lo;0;L;;;;;N;;;;;
+100B6;LINEAR B IDEOGRAM B174;Lo;0;L;;;;;N;;;;;
+100B7;LINEAR B IDEOGRAM B176 TREE;Lo;0;L;;;;;N;;;;;
+100B8;LINEAR B IDEOGRAM B177;Lo;0;L;;;;;N;;;;;
+100B9;LINEAR B IDEOGRAM B178;Lo;0;L;;;;;N;;;;;
+100BA;LINEAR B IDEOGRAM B179;Lo;0;L;;;;;N;;;;;
+100BB;LINEAR B IDEOGRAM B180;Lo;0;L;;;;;N;;;;;
+100BC;LINEAR B IDEOGRAM B181;Lo;0;L;;;;;N;;;;;
+100BD;LINEAR B IDEOGRAM B182;Lo;0;L;;;;;N;;;;;
+100BE;LINEAR B IDEOGRAM B183;Lo;0;L;;;;;N;;;;;
+100BF;LINEAR B IDEOGRAM B184;Lo;0;L;;;;;N;;;;;
+100C0;LINEAR B IDEOGRAM B185;Lo;0;L;;;;;N;;;;;
+100C1;LINEAR B IDEOGRAM B189;Lo;0;L;;;;;N;;;;;
+100C2;LINEAR B IDEOGRAM B190;Lo;0;L;;;;;N;;;;;
+100C3;LINEAR B IDEOGRAM B191 HELMET;Lo;0;L;;;;;N;;;;;
+100C4;LINEAR B IDEOGRAM B220 FOOTSTOOL;Lo;0;L;;;;;N;;;;;
+100C5;LINEAR B IDEOGRAM B225 BATHTUB;Lo;0;L;;;;;N;;;;;
+100C6;LINEAR B IDEOGRAM B230 SPEAR;Lo;0;L;;;;;N;;;;;
+100C7;LINEAR B IDEOGRAM B231 ARROW;Lo;0;L;;;;;N;;;;;
+100C8;LINEAR B IDEOGRAM B232;Lo;0;L;;;;;N;;;;;
+100C9;LINEAR B IDEOGRAM B233 SWORD;Lo;0;L;;;;;N;;;;;
+100CA;LINEAR B IDEOGRAM B234;Lo;0;L;;;;;N;;;;;
+100CB;LINEAR B IDEOGRAM B236;Lo;0;L;;;;;N;;;;;
+100CC;LINEAR B IDEOGRAM B240 WHEELED CHARIOT;Lo;0;L;;;;;N;;;;;
+100CD;LINEAR B IDEOGRAM B241 CHARIOT;Lo;0;L;;;;;N;;;;;
+100CE;LINEAR B IDEOGRAM B242 CHARIOT FRAME;Lo;0;L;;;;;N;;;;;
+100CF;LINEAR B IDEOGRAM B243 WHEEL;Lo;0;L;;;;;N;;;;;
+100D0;LINEAR B IDEOGRAM B245;Lo;0;L;;;;;N;;;;;
+100D1;LINEAR B IDEOGRAM B246;Lo;0;L;;;;;N;;;;;
+100D2;LINEAR B MONOGRAM B247 DIPTE;Lo;0;L;;;;;N;;;;;
+100D3;LINEAR B IDEOGRAM B248;Lo;0;L;;;;;N;;;;;
+100D4;LINEAR B IDEOGRAM B249;Lo;0;L;;;;;N;;;;;
+100D5;LINEAR B IDEOGRAM B251;Lo;0;L;;;;;N;;;;;
+100D6;LINEAR B IDEOGRAM B252;Lo;0;L;;;;;N;;;;;
+100D7;LINEAR B IDEOGRAM B253;Lo;0;L;;;;;N;;;;;
+100D8;LINEAR B IDEOGRAM B254 DART;Lo;0;L;;;;;N;;;;;
+100D9;LINEAR B IDEOGRAM B255;Lo;0;L;;;;;N;;;;;
+100DA;LINEAR B IDEOGRAM B256;Lo;0;L;;;;;N;;;;;
+100DB;LINEAR B IDEOGRAM B257;Lo;0;L;;;;;N;;;;;
+100DC;LINEAR B IDEOGRAM B258;Lo;0;L;;;;;N;;;;;
+100DD;LINEAR B IDEOGRAM B259;Lo;0;L;;;;;N;;;;;
+100DE;LINEAR B IDEOGRAM VESSEL B155;Lo;0;L;;;;;N;;;;;
+100DF;LINEAR B IDEOGRAM VESSEL B200;Lo;0;L;;;;;N;;;;;
+100E0;LINEAR B IDEOGRAM VESSEL B201;Lo;0;L;;;;;N;;;;;
+100E1;LINEAR B IDEOGRAM VESSEL B202;Lo;0;L;;;;;N;;;;;
+100E2;LINEAR B IDEOGRAM VESSEL B203;Lo;0;L;;;;;N;;;;;
+100E3;LINEAR B IDEOGRAM VESSEL B204;Lo;0;L;;;;;N;;;;;
+100E4;LINEAR B IDEOGRAM VESSEL B205;Lo;0;L;;;;;N;;;;;
+100E5;LINEAR B IDEOGRAM VESSEL B206;Lo;0;L;;;;;N;;;;;
+100E6;LINEAR B IDEOGRAM VESSEL B207;Lo;0;L;;;;;N;;;;;
+100E7;LINEAR B IDEOGRAM VESSEL B208;Lo;0;L;;;;;N;;;;;
+100E8;LINEAR B IDEOGRAM VESSEL B209;Lo;0;L;;;;;N;;;;;
+100E9;LINEAR B IDEOGRAM VESSEL B210;Lo;0;L;;;;;N;;;;;
+100EA;LINEAR B IDEOGRAM VESSEL B211;Lo;0;L;;;;;N;;;;;
+100EB;LINEAR B IDEOGRAM VESSEL B212;Lo;0;L;;;;;N;;;;;
+100EC;LINEAR B IDEOGRAM VESSEL B213;Lo;0;L;;;;;N;;;;;
+100ED;LINEAR B IDEOGRAM VESSEL B214;Lo;0;L;;;;;N;;;;;
+100EE;LINEAR B IDEOGRAM VESSEL B215;Lo;0;L;;;;;N;;;;;
+100EF;LINEAR B IDEOGRAM VESSEL B216;Lo;0;L;;;;;N;;;;;
+100F0;LINEAR B IDEOGRAM VESSEL B217;Lo;0;L;;;;;N;;;;;
+100F1;LINEAR B IDEOGRAM VESSEL B218;Lo;0;L;;;;;N;;;;;
+100F2;LINEAR B IDEOGRAM VESSEL B219;Lo;0;L;;;;;N;;;;;
+100F3;LINEAR B IDEOGRAM VESSEL B221;Lo;0;L;;;;;N;;;;;
+100F4;LINEAR B IDEOGRAM VESSEL B222;Lo;0;L;;;;;N;;;;;
+100F5;LINEAR B IDEOGRAM VESSEL B226;Lo;0;L;;;;;N;;;;;
+100F6;LINEAR B IDEOGRAM VESSEL B227;Lo;0;L;;;;;N;;;;;
+100F7;LINEAR B IDEOGRAM VESSEL B228;Lo;0;L;;;;;N;;;;;
+100F8;LINEAR B IDEOGRAM VESSEL B229;Lo;0;L;;;;;N;;;;;
+100F9;LINEAR B IDEOGRAM VESSEL B250;Lo;0;L;;;;;N;;;;;
+100FA;LINEAR B IDEOGRAM VESSEL B305;Lo;0;L;;;;;N;;;;;
+10100;AEGEAN WORD SEPARATOR LINE;Po;0;L;;;;;N;;;;;
+10101;AEGEAN WORD SEPARATOR DOT;Po;0;ON;;;;;N;;;;;
+10102;AEGEAN CHECK MARK;Po;0;L;;;;;N;;;;;
+10107;AEGEAN NUMBER ONE;No;0;L;;;;1;N;;;;;
+10108;AEGEAN NUMBER TWO;No;0;L;;;;2;N;;;;;
+10109;AEGEAN NUMBER THREE;No;0;L;;;;3;N;;;;;
+1010A;AEGEAN NUMBER FOUR;No;0;L;;;;4;N;;;;;
+1010B;AEGEAN NUMBER FIVE;No;0;L;;;;5;N;;;;;
+1010C;AEGEAN NUMBER SIX;No;0;L;;;;6;N;;;;;
+1010D;AEGEAN NUMBER SEVEN;No;0;L;;;;7;N;;;;;
+1010E;AEGEAN NUMBER EIGHT;No;0;L;;;;8;N;;;;;
+1010F;AEGEAN NUMBER NINE;No;0;L;;;;9;N;;;;;
+10110;AEGEAN NUMBER TEN;No;0;L;;;;10;N;;;;;
+10111;AEGEAN NUMBER TWENTY;No;0;L;;;;20;N;;;;;
+10112;AEGEAN NUMBER THIRTY;No;0;L;;;;30;N;;;;;
+10113;AEGEAN NUMBER FORTY;No;0;L;;;;40;N;;;;;
+10114;AEGEAN NUMBER FIFTY;No;0;L;;;;50;N;;;;;
+10115;AEGEAN NUMBER SIXTY;No;0;L;;;;60;N;;;;;
+10116;AEGEAN NUMBER SEVENTY;No;0;L;;;;70;N;;;;;
+10117;AEGEAN NUMBER EIGHTY;No;0;L;;;;80;N;;;;;
+10118;AEGEAN NUMBER NINETY;No;0;L;;;;90;N;;;;;
+10119;AEGEAN NUMBER ONE HUNDRED;No;0;L;;;;100;N;;;;;
+1011A;AEGEAN NUMBER TWO HUNDRED;No;0;L;;;;200;N;;;;;
+1011B;AEGEAN NUMBER THREE HUNDRED;No;0;L;;;;300;N;;;;;
+1011C;AEGEAN NUMBER FOUR HUNDRED;No;0;L;;;;400;N;;;;;
+1011D;AEGEAN NUMBER FIVE HUNDRED;No;0;L;;;;500;N;;;;;
+1011E;AEGEAN NUMBER SIX HUNDRED;No;0;L;;;;600;N;;;;;
+1011F;AEGEAN NUMBER SEVEN HUNDRED;No;0;L;;;;700;N;;;;;
+10120;AEGEAN NUMBER EIGHT HUNDRED;No;0;L;;;;800;N;;;;;
+10121;AEGEAN NUMBER NINE HUNDRED;No;0;L;;;;900;N;;;;;
+10122;AEGEAN NUMBER ONE THOUSAND;No;0;L;;;;1000;N;;;;;
+10123;AEGEAN NUMBER TWO THOUSAND;No;0;L;;;;2000;N;;;;;
+10124;AEGEAN NUMBER THREE THOUSAND;No;0;L;;;;3000;N;;;;;
+10125;AEGEAN NUMBER FOUR THOUSAND;No;0;L;;;;4000;N;;;;;
+10126;AEGEAN NUMBER FIVE THOUSAND;No;0;L;;;;5000;N;;;;;
+10127;AEGEAN NUMBER SIX THOUSAND;No;0;L;;;;6000;N;;;;;
+10128;AEGEAN NUMBER SEVEN THOUSAND;No;0;L;;;;7000;N;;;;;
+10129;AEGEAN NUMBER EIGHT THOUSAND;No;0;L;;;;8000;N;;;;;
+1012A;AEGEAN NUMBER NINE THOUSAND;No;0;L;;;;9000;N;;;;;
+1012B;AEGEAN NUMBER TEN THOUSAND;No;0;L;;;;10000;N;;;;;
+1012C;AEGEAN NUMBER TWENTY THOUSAND;No;0;L;;;;20000;N;;;;;
+1012D;AEGEAN NUMBER THIRTY THOUSAND;No;0;L;;;;30000;N;;;;;
+1012E;AEGEAN NUMBER FORTY THOUSAND;No;0;L;;;;40000;N;;;;;
+1012F;AEGEAN NUMBER FIFTY THOUSAND;No;0;L;;;;50000;N;;;;;
+10130;AEGEAN NUMBER SIXTY THOUSAND;No;0;L;;;;60000;N;;;;;
+10131;AEGEAN NUMBER SEVENTY THOUSAND;No;0;L;;;;70000;N;;;;;
+10132;AEGEAN NUMBER EIGHTY THOUSAND;No;0;L;;;;80000;N;;;;;
+10133;AEGEAN NUMBER NINETY THOUSAND;No;0;L;;;;90000;N;;;;;
+10137;AEGEAN WEIGHT BASE UNIT;So;0;L;;;;;N;;;;;
+10138;AEGEAN WEIGHT FIRST SUBUNIT;So;0;L;;;;;N;;;;;
+10139;AEGEAN WEIGHT SECOND SUBUNIT;So;0;L;;;;;N;;;;;
+1013A;AEGEAN WEIGHT THIRD SUBUNIT;So;0;L;;;;;N;;;;;
+1013B;AEGEAN WEIGHT FOURTH SUBUNIT;So;0;L;;;;;N;;;;;
+1013C;AEGEAN DRY MEASURE FIRST SUBUNIT;So;0;L;;;;;N;;;;;
+1013D;AEGEAN LIQUID MEASURE FIRST SUBUNIT;So;0;L;;;;;N;;;;;
+1013E;AEGEAN MEASURE SECOND SUBUNIT;So;0;L;;;;;N;;;;;
+1013F;AEGEAN MEASURE THIRD SUBUNIT;So;0;L;;;;;N;;;;;
+10140;GREEK ACROPHONIC ATTIC ONE QUARTER;Nl;0;ON;;;;1/4;N;;;;;
+10141;GREEK ACROPHONIC ATTIC ONE HALF;Nl;0;ON;;;;1/2;N;;;;;
+10142;GREEK ACROPHONIC ATTIC ONE DRACHMA;Nl;0;ON;;;;1;N;;;;;
+10143;GREEK ACROPHONIC ATTIC FIVE;Nl;0;ON;;;;5;N;;;;;
+10144;GREEK ACROPHONIC ATTIC FIFTY;Nl;0;ON;;;;50;N;;;;;
+10145;GREEK ACROPHONIC ATTIC FIVE HUNDRED;Nl;0;ON;;;;500;N;;;;;
+10146;GREEK ACROPHONIC ATTIC FIVE THOUSAND;Nl;0;ON;;;;5000;N;;;;;
+10147;GREEK ACROPHONIC ATTIC FIFTY THOUSAND;Nl;0;ON;;;;50000;N;;;;;
+10148;GREEK ACROPHONIC ATTIC FIVE TALENTS;Nl;0;ON;;;;5;N;;;;;
+10149;GREEK ACROPHONIC ATTIC TEN TALENTS;Nl;0;ON;;;;10;N;;;;;
+1014A;GREEK ACROPHONIC ATTIC FIFTY TALENTS;Nl;0;ON;;;;50;N;;;;;
+1014B;GREEK ACROPHONIC ATTIC ONE HUNDRED TALENTS;Nl;0;ON;;;;100;N;;;;;
+1014C;GREEK ACROPHONIC ATTIC FIVE HUNDRED TALENTS;Nl;0;ON;;;;500;N;;;;;
+1014D;GREEK ACROPHONIC ATTIC ONE THOUSAND TALENTS;Nl;0;ON;;;;1000;N;;;;;
+1014E;GREEK ACROPHONIC ATTIC FIVE THOUSAND TALENTS;Nl;0;ON;;;;5000;N;;;;;
+1014F;GREEK ACROPHONIC ATTIC FIVE STATERS;Nl;0;ON;;;;5;N;;;;;
+10150;GREEK ACROPHONIC ATTIC TEN STATERS;Nl;0;ON;;;;10;N;;;;;
+10151;GREEK ACROPHONIC ATTIC FIFTY STATERS;Nl;0;ON;;;;50;N;;;;;
+10152;GREEK ACROPHONIC ATTIC ONE HUNDRED STATERS;Nl;0;ON;;;;100;N;;;;;
+10153;GREEK ACROPHONIC ATTIC FIVE HUNDRED STATERS;Nl;0;ON;;;;500;N;;;;;
+10154;GREEK ACROPHONIC ATTIC ONE THOUSAND STATERS;Nl;0;ON;;;;1000;N;;;;;
+10155;GREEK ACROPHONIC ATTIC TEN THOUSAND STATERS;Nl;0;ON;;;;10000;N;;;;;
+10156;GREEK ACROPHONIC ATTIC FIFTY THOUSAND STATERS;Nl;0;ON;;;;50000;N;;;;;
+10157;GREEK ACROPHONIC ATTIC TEN MNAS;Nl;0;ON;;;;10;N;;;;;
+10158;GREEK ACROPHONIC HERAEUM ONE PLETHRON;Nl;0;ON;;;;1;N;;;;;
+10159;GREEK ACROPHONIC THESPIAN ONE;Nl;0;ON;;;;1;N;;;;;
+1015A;GREEK ACROPHONIC HERMIONIAN ONE;Nl;0;ON;;;;1;N;;;;;
+1015B;GREEK ACROPHONIC EPIDAUREAN TWO;Nl;0;ON;;;;2;N;;;;;
+1015C;GREEK ACROPHONIC THESPIAN TWO;Nl;0;ON;;;;2;N;;;;;
+1015D;GREEK ACROPHONIC CYRENAIC TWO DRACHMAS;Nl;0;ON;;;;2;N;;;;;
+1015E;GREEK ACROPHONIC EPIDAUREAN TWO DRACHMAS;Nl;0;ON;;;;2;N;;;;;
+1015F;GREEK ACROPHONIC TROEZENIAN FIVE;Nl;0;ON;;;;5;N;;;;;
+10160;GREEK ACROPHONIC TROEZENIAN TEN;Nl;0;ON;;;;10;N;;;;;
+10161;GREEK ACROPHONIC TROEZENIAN TEN ALTERNATE FORM;Nl;0;ON;;;;10;N;;;;;
+10162;GREEK ACROPHONIC HERMIONIAN TEN;Nl;0;ON;;;;10;N;;;;;
+10163;GREEK ACROPHONIC MESSENIAN TEN;Nl;0;ON;;;;10;N;;;;;
+10164;GREEK ACROPHONIC THESPIAN TEN;Nl;0;ON;;;;10;N;;;;;
+10165;GREEK ACROPHONIC THESPIAN THIRTY;Nl;0;ON;;;;30;N;;;;;
+10166;GREEK ACROPHONIC TROEZENIAN FIFTY;Nl;0;ON;;;;50;N;;;;;
+10167;GREEK ACROPHONIC TROEZENIAN FIFTY ALTERNATE FORM;Nl;0;ON;;;;50;N;;;;;
+10168;GREEK ACROPHONIC HERMIONIAN FIFTY;Nl;0;ON;;;;50;N;;;;;
+10169;GREEK ACROPHONIC THESPIAN FIFTY;Nl;0;ON;;;;50;N;;;;;
+1016A;GREEK ACROPHONIC THESPIAN ONE HUNDRED;Nl;0;ON;;;;100;N;;;;;
+1016B;GREEK ACROPHONIC THESPIAN THREE HUNDRED;Nl;0;ON;;;;300;N;;;;;
+1016C;GREEK ACROPHONIC EPIDAUREAN FIVE HUNDRED;Nl;0;ON;;;;500;N;;;;;
+1016D;GREEK ACROPHONIC TROEZENIAN FIVE HUNDRED;Nl;0;ON;;;;500;N;;;;;
+1016E;GREEK ACROPHONIC THESPIAN FIVE HUNDRED;Nl;0;ON;;;;500;N;;;;;
+1016F;GREEK ACROPHONIC CARYSTIAN FIVE HUNDRED;Nl;0;ON;;;;500;N;;;;;
+10170;GREEK ACROPHONIC NAXIAN FIVE HUNDRED;Nl;0;ON;;;;500;N;;;;;
+10171;GREEK ACROPHONIC THESPIAN ONE THOUSAND;Nl;0;ON;;;;1000;N;;;;;
+10172;GREEK ACROPHONIC THESPIAN FIVE THOUSAND;Nl;0;ON;;;;5000;N;;;;;
+10173;GREEK ACROPHONIC DELPHIC FIVE MNAS;Nl;0;ON;;;;5;N;;;;;
+10174;GREEK ACROPHONIC STRATIAN FIFTY MNAS;Nl;0;ON;;;;50;N;;;;;
+10175;GREEK ONE HALF SIGN;No;0;ON;;;;1/2;N;;;;;
+10176;GREEK ONE HALF SIGN ALTERNATE FORM;No;0;ON;;;;1/2;N;;;;;
+10177;GREEK TWO THIRDS SIGN;No;0;ON;;;;2/3;N;;;;;
+10178;GREEK THREE QUARTERS SIGN;No;0;ON;;;;3/4;N;;;;;
+10179;GREEK YEAR SIGN;So;0;ON;;;;;N;;;;;
+1017A;GREEK TALENT SIGN;So;0;ON;;;;;N;;;;;
+1017B;GREEK DRACHMA SIGN;So;0;ON;;;;;N;;;;;
+1017C;GREEK OBOL SIGN;So;0;ON;;;;;N;;;;;
+1017D;GREEK TWO OBOLS SIGN;So;0;ON;;;;;N;;;;;
+1017E;GREEK THREE OBOLS SIGN;So;0;ON;;;;;N;;;;;
+1017F;GREEK FOUR OBOLS SIGN;So;0;ON;;;;;N;;;;;
+10180;GREEK FIVE OBOLS SIGN;So;0;ON;;;;;N;;;;;
+10181;GREEK METRETES SIGN;So;0;ON;;;;;N;;;;;
+10182;GREEK KYATHOS BASE SIGN;So;0;ON;;;;;N;;;;;
+10183;GREEK LITRA SIGN;So;0;ON;;;;;N;;;;;
+10184;GREEK OUNKIA SIGN;So;0;ON;;;;;N;;;;;
+10185;GREEK XESTES SIGN;So;0;ON;;;;;N;;;;;
+10186;GREEK ARTABE SIGN;So;0;ON;;;;;N;;;;;
+10187;GREEK AROURA SIGN;So;0;ON;;;;;N;;;;;
+10188;GREEK GRAMMA SIGN;So;0;ON;;;;;N;;;;;
+10189;GREEK TRYBLION BASE SIGN;So;0;ON;;;;;N;;;;;
+1018A;GREEK ZERO SIGN;No;0;ON;;;;0;N;;;;;
+1018B;GREEK ONE QUARTER SIGN;No;0;ON;;;;1/4;N;;;;;
+1018C;GREEK SINUSOID SIGN;So;0;ON;;;;;N;;;;;
+1018D;GREEK INDICTION SIGN;So;0;L;;;;;N;;;;;
+1018E;NOMISMA SIGN;So;0;L;;;;;N;;;;;
+10190;ROMAN SEXTANS SIGN;So;0;ON;;;;;N;;;;;
+10191;ROMAN UNCIA SIGN;So;0;ON;;;;;N;;;;;
+10192;ROMAN SEMUNCIA SIGN;So;0;ON;;;;;N;;;;;
+10193;ROMAN SEXTULA SIGN;So;0;ON;;;;;N;;;;;
+10194;ROMAN DIMIDIA SEXTULA SIGN;So;0;ON;;;;;N;;;;;
+10195;ROMAN SILIQUA SIGN;So;0;ON;;;;;N;;;;;
+10196;ROMAN DENARIUS SIGN;So;0;ON;;;;;N;;;;;
+10197;ROMAN QUINARIUS SIGN;So;0;ON;;;;;N;;;;;
+10198;ROMAN SESTERTIUS SIGN;So;0;ON;;;;;N;;;;;
+10199;ROMAN DUPONDIUS SIGN;So;0;ON;;;;;N;;;;;
+1019A;ROMAN AS SIGN;So;0;ON;;;;;N;;;;;
+1019B;ROMAN CENTURIAL SIGN;So;0;ON;;;;;N;;;;;
+101A0;GREEK SYMBOL TAU RHO;So;0;ON;;;;;N;;;;;
+101D0;PHAISTOS DISC SIGN PEDESTRIAN;So;0;L;;;;;N;;;;;
+101D1;PHAISTOS DISC SIGN PLUMED HEAD;So;0;L;;;;;N;;;;;
+101D2;PHAISTOS DISC SIGN TATTOOED HEAD;So;0;L;;;;;N;;;;;
+101D3;PHAISTOS DISC SIGN CAPTIVE;So;0;L;;;;;N;;;;;
+101D4;PHAISTOS DISC SIGN CHILD;So;0;L;;;;;N;;;;;
+101D5;PHAISTOS DISC SIGN WOMAN;So;0;L;;;;;N;;;;;
+101D6;PHAISTOS DISC SIGN HELMET;So;0;L;;;;;N;;;;;
+101D7;PHAISTOS DISC SIGN GAUNTLET;So;0;L;;;;;N;;;;;
+101D8;PHAISTOS DISC SIGN TIARA;So;0;L;;;;;N;;;;;
+101D9;PHAISTOS DISC SIGN ARROW;So;0;L;;;;;N;;;;;
+101DA;PHAISTOS DISC SIGN BOW;So;0;L;;;;;N;;;;;
+101DB;PHAISTOS DISC SIGN SHIELD;So;0;L;;;;;N;;;;;
+101DC;PHAISTOS DISC SIGN CLUB;So;0;L;;;;;N;;;;;
+101DD;PHAISTOS DISC SIGN MANACLES;So;0;L;;;;;N;;;;;
+101DE;PHAISTOS DISC SIGN MATTOCK;So;0;L;;;;;N;;;;;
+101DF;PHAISTOS DISC SIGN SAW;So;0;L;;;;;N;;;;;
+101E0;PHAISTOS DISC SIGN LID;So;0;L;;;;;N;;;;;
+101E1;PHAISTOS DISC SIGN BOOMERANG;So;0;L;;;;;N;;;;;
+101E2;PHAISTOS DISC SIGN CARPENTRY PLANE;So;0;L;;;;;N;;;;;
+101E3;PHAISTOS DISC SIGN DOLIUM;So;0;L;;;;;N;;;;;
+101E4;PHAISTOS DISC SIGN COMB;So;0;L;;;;;N;;;;;
+101E5;PHAISTOS DISC SIGN SLING;So;0;L;;;;;N;;;;;
+101E6;PHAISTOS DISC SIGN COLUMN;So;0;L;;;;;N;;;;;
+101E7;PHAISTOS DISC SIGN BEEHIVE;So;0;L;;;;;N;;;;;
+101E8;PHAISTOS DISC SIGN SHIP;So;0;L;;;;;N;;;;;
+101E9;PHAISTOS DISC SIGN HORN;So;0;L;;;;;N;;;;;
+101EA;PHAISTOS DISC SIGN HIDE;So;0;L;;;;;N;;;;;
+101EB;PHAISTOS DISC SIGN BULLS LEG;So;0;L;;;;;N;;;;;
+101EC;PHAISTOS DISC SIGN CAT;So;0;L;;;;;N;;;;;
+101ED;PHAISTOS DISC SIGN RAM;So;0;L;;;;;N;;;;;
+101EE;PHAISTOS DISC SIGN EAGLE;So;0;L;;;;;N;;;;;
+101EF;PHAISTOS DISC SIGN DOVE;So;0;L;;;;;N;;;;;
+101F0;PHAISTOS DISC SIGN TUNNY;So;0;L;;;;;N;;;;;
+101F1;PHAISTOS DISC SIGN BEE;So;0;L;;;;;N;;;;;
+101F2;PHAISTOS DISC SIGN PLANE TREE;So;0;L;;;;;N;;;;;
+101F3;PHAISTOS DISC SIGN VINE;So;0;L;;;;;N;;;;;
+101F4;PHAISTOS DISC SIGN PAPYRUS;So;0;L;;;;;N;;;;;
+101F5;PHAISTOS DISC SIGN ROSETTE;So;0;L;;;;;N;;;;;
+101F6;PHAISTOS DISC SIGN LILY;So;0;L;;;;;N;;;;;
+101F7;PHAISTOS DISC SIGN OX BACK;So;0;L;;;;;N;;;;;
+101F8;PHAISTOS DISC SIGN FLUTE;So;0;L;;;;;N;;;;;
+101F9;PHAISTOS DISC SIGN GRATER;So;0;L;;;;;N;;;;;
+101FA;PHAISTOS DISC SIGN STRAINER;So;0;L;;;;;N;;;;;
+101FB;PHAISTOS DISC SIGN SMALL AXE;So;0;L;;;;;N;;;;;
+101FC;PHAISTOS DISC SIGN WAVY BAND;So;0;L;;;;;N;;;;;
+101FD;PHAISTOS DISC SIGN COMBINING OBLIQUE STROKE;Mn;220;NSM;;;;;N;;;;;
+10280;LYCIAN LETTER A;Lo;0;L;;;;;N;;;;;
+10281;LYCIAN LETTER E;Lo;0;L;;;;;N;;;;;
+10282;LYCIAN LETTER B;Lo;0;L;;;;;N;;;;;
+10283;LYCIAN LETTER BH;Lo;0;L;;;;;N;;;;;
+10284;LYCIAN LETTER G;Lo;0;L;;;;;N;;;;;
+10285;LYCIAN LETTER D;Lo;0;L;;;;;N;;;;;
+10286;LYCIAN LETTER I;Lo;0;L;;;;;N;;;;;
+10287;LYCIAN LETTER W;Lo;0;L;;;;;N;;;;;
+10288;LYCIAN LETTER Z;Lo;0;L;;;;;N;;;;;
+10289;LYCIAN LETTER TH;Lo;0;L;;;;;N;;;;;
+1028A;LYCIAN LETTER J;Lo;0;L;;;;;N;;;;;
+1028B;LYCIAN LETTER K;Lo;0;L;;;;;N;;;;;
+1028C;LYCIAN LETTER Q;Lo;0;L;;;;;N;;;;;
+1028D;LYCIAN LETTER L;Lo;0;L;;;;;N;;;;;
+1028E;LYCIAN LETTER M;Lo;0;L;;;;;N;;;;;
+1028F;LYCIAN LETTER N;Lo;0;L;;;;;N;;;;;
+10290;LYCIAN LETTER MM;Lo;0;L;;;;;N;;;;;
+10291;LYCIAN LETTER NN;Lo;0;L;;;;;N;;;;;
+10292;LYCIAN LETTER U;Lo;0;L;;;;;N;;;;;
+10293;LYCIAN LETTER P;Lo;0;L;;;;;N;;;;;
+10294;LYCIAN LETTER KK;Lo;0;L;;;;;N;;;;;
+10295;LYCIAN LETTER R;Lo;0;L;;;;;N;;;;;
+10296;LYCIAN LETTER S;Lo;0;L;;;;;N;;;;;
+10297;LYCIAN LETTER T;Lo;0;L;;;;;N;;;;;
+10298;LYCIAN LETTER TT;Lo;0;L;;;;;N;;;;;
+10299;LYCIAN LETTER AN;Lo;0;L;;;;;N;;;;;
+1029A;LYCIAN LETTER EN;Lo;0;L;;;;;N;;;;;
+1029B;LYCIAN LETTER H;Lo;0;L;;;;;N;;;;;
+1029C;LYCIAN LETTER X;Lo;0;L;;;;;N;;;;;
+102A0;CARIAN LETTER A;Lo;0;L;;;;;N;;;;;
+102A1;CARIAN LETTER P2;Lo;0;L;;;;;N;;;;;
+102A2;CARIAN LETTER D;Lo;0;L;;;;;N;;;;;
+102A3;CARIAN LETTER L;Lo;0;L;;;;;N;;;;;
+102A4;CARIAN LETTER UUU;Lo;0;L;;;;;N;;;;;
+102A5;CARIAN LETTER R;Lo;0;L;;;;;N;;;;;
+102A6;CARIAN LETTER LD;Lo;0;L;;;;;N;;;;;
+102A7;CARIAN LETTER A2;Lo;0;L;;;;;N;;;;;
+102A8;CARIAN LETTER Q;Lo;0;L;;;;;N;;;;;
+102A9;CARIAN LETTER B;Lo;0;L;;;;;N;;;;;
+102AA;CARIAN LETTER M;Lo;0;L;;;;;N;;;;;
+102AB;CARIAN LETTER O;Lo;0;L;;;;;N;;;;;
+102AC;CARIAN LETTER D2;Lo;0;L;;;;;N;;;;;
+102AD;CARIAN LETTER T;Lo;0;L;;;;;N;;;;;
+102AE;CARIAN LETTER SH;Lo;0;L;;;;;N;;;;;
+102AF;CARIAN LETTER SH2;Lo;0;L;;;;;N;;;;;
+102B0;CARIAN LETTER S;Lo;0;L;;;;;N;;;;;
+102B1;CARIAN LETTER C-18;Lo;0;L;;;;;N;;;;;
+102B2;CARIAN LETTER U;Lo;0;L;;;;;N;;;;;
+102B3;CARIAN LETTER NN;Lo;0;L;;;;;N;;;;;
+102B4;CARIAN LETTER X;Lo;0;L;;;;;N;;;;;
+102B5;CARIAN LETTER N;Lo;0;L;;;;;N;;;;;
+102B6;CARIAN LETTER TT2;Lo;0;L;;;;;N;;;;;
+102B7;CARIAN LETTER P;Lo;0;L;;;;;N;;;;;
+102B8;CARIAN LETTER SS;Lo;0;L;;;;;N;;;;;
+102B9;CARIAN LETTER I;Lo;0;L;;;;;N;;;;;
+102BA;CARIAN LETTER E;Lo;0;L;;;;;N;;;;;
+102BB;CARIAN LETTER UUUU;Lo;0;L;;;;;N;;;;;
+102BC;CARIAN LETTER K;Lo;0;L;;;;;N;;;;;
+102BD;CARIAN LETTER K2;Lo;0;L;;;;;N;;;;;
+102BE;CARIAN LETTER ND;Lo;0;L;;;;;N;;;;;
+102BF;CARIAN LETTER UU;Lo;0;L;;;;;N;;;;;
+102C0;CARIAN LETTER G;Lo;0;L;;;;;N;;;;;
+102C1;CARIAN LETTER G2;Lo;0;L;;;;;N;;;;;
+102C2;CARIAN LETTER ST;Lo;0;L;;;;;N;;;;;
+102C3;CARIAN LETTER ST2;Lo;0;L;;;;;N;;;;;
+102C4;CARIAN LETTER NG;Lo;0;L;;;;;N;;;;;
+102C5;CARIAN LETTER II;Lo;0;L;;;;;N;;;;;
+102C6;CARIAN LETTER C-39;Lo;0;L;;;;;N;;;;;
+102C7;CARIAN LETTER TT;Lo;0;L;;;;;N;;;;;
+102C8;CARIAN LETTER UUU2;Lo;0;L;;;;;N;;;;;
+102C9;CARIAN LETTER RR;Lo;0;L;;;;;N;;;;;
+102CA;CARIAN LETTER MB;Lo;0;L;;;;;N;;;;;
+102CB;CARIAN LETTER MB2;Lo;0;L;;;;;N;;;;;
+102CC;CARIAN LETTER MB3;Lo;0;L;;;;;N;;;;;
+102CD;CARIAN LETTER MB4;Lo;0;L;;;;;N;;;;;
+102CE;CARIAN LETTER LD2;Lo;0;L;;;;;N;;;;;
+102CF;CARIAN LETTER E2;Lo;0;L;;;;;N;;;;;
+102D0;CARIAN LETTER UUU3;Lo;0;L;;;;;N;;;;;
+102E0;COPTIC EPACT THOUSANDS MARK;Mn;220;NSM;;;;;N;;;;;
+102E1;COPTIC EPACT DIGIT ONE;No;0;EN;;;;1;N;;;;;
+102E2;COPTIC EPACT DIGIT TWO;No;0;EN;;;;2;N;;;;;
+102E3;COPTIC EPACT DIGIT THREE;No;0;EN;;;;3;N;;;;;
+102E4;COPTIC EPACT DIGIT FOUR;No;0;EN;;;;4;N;;;;;
+102E5;COPTIC EPACT DIGIT FIVE;No;0;EN;;;;5;N;;;;;
+102E6;COPTIC EPACT DIGIT SIX;No;0;EN;;;;6;N;;;;;
+102E7;COPTIC EPACT DIGIT SEVEN;No;0;EN;;;;7;N;;;;;
+102E8;COPTIC EPACT DIGIT EIGHT;No;0;EN;;;;8;N;;;;;
+102E9;COPTIC EPACT DIGIT NINE;No;0;EN;;;;9;N;;;;;
+102EA;COPTIC EPACT NUMBER TEN;No;0;EN;;;;10;N;;;;;
+102EB;COPTIC EPACT NUMBER TWENTY;No;0;EN;;;;20;N;;;;;
+102EC;COPTIC EPACT NUMBER THIRTY;No;0;EN;;;;30;N;;;;;
+102ED;COPTIC EPACT NUMBER FORTY;No;0;EN;;;;40;N;;;;;
+102EE;COPTIC EPACT NUMBER FIFTY;No;0;EN;;;;50;N;;;;;
+102EF;COPTIC EPACT NUMBER SIXTY;No;0;EN;;;;60;N;;;;;
+102F0;COPTIC EPACT NUMBER SEVENTY;No;0;EN;;;;70;N;;;;;
+102F1;COPTIC EPACT NUMBER EIGHTY;No;0;EN;;;;80;N;;;;;
+102F2;COPTIC EPACT NUMBER NINETY;No;0;EN;;;;90;N;;;;;
+102F3;COPTIC EPACT NUMBER ONE HUNDRED;No;0;EN;;;;100;N;;;;;
+102F4;COPTIC EPACT NUMBER TWO HUNDRED;No;0;EN;;;;200;N;;;;;
+102F5;COPTIC EPACT NUMBER THREE HUNDRED;No;0;EN;;;;300;N;;;;;
+102F6;COPTIC EPACT NUMBER FOUR HUNDRED;No;0;EN;;;;400;N;;;;;
+102F7;COPTIC EPACT NUMBER FIVE HUNDRED;No;0;EN;;;;500;N;;;;;
+102F8;COPTIC EPACT NUMBER SIX HUNDRED;No;0;EN;;;;600;N;;;;;
+102F9;COPTIC EPACT NUMBER SEVEN HUNDRED;No;0;EN;;;;700;N;;;;;
+102FA;COPTIC EPACT NUMBER EIGHT HUNDRED;No;0;EN;;;;800;N;;;;;
+102FB;COPTIC EPACT NUMBER NINE HUNDRED;No;0;EN;;;;900;N;;;;;
+10300;OLD ITALIC LETTER A;Lo;0;L;;;;;N;;;;;
+10301;OLD ITALIC LETTER BE;Lo;0;L;;;;;N;;;;;
+10302;OLD ITALIC LETTER KE;Lo;0;L;;;;;N;;;;;
+10303;OLD ITALIC LETTER DE;Lo;0;L;;;;;N;;;;;
+10304;OLD ITALIC LETTER E;Lo;0;L;;;;;N;;;;;
+10305;OLD ITALIC LETTER VE;Lo;0;L;;;;;N;;;;;
+10306;OLD ITALIC LETTER ZE;Lo;0;L;;;;;N;;;;;
+10307;OLD ITALIC LETTER HE;Lo;0;L;;;;;N;;;;;
+10308;OLD ITALIC LETTER THE;Lo;0;L;;;;;N;;;;;
+10309;OLD ITALIC LETTER I;Lo;0;L;;;;;N;;;;;
+1030A;OLD ITALIC LETTER KA;Lo;0;L;;;;;N;;;;;
+1030B;OLD ITALIC LETTER EL;Lo;0;L;;;;;N;;;;;
+1030C;OLD ITALIC LETTER EM;Lo;0;L;;;;;N;;;;;
+1030D;OLD ITALIC LETTER EN;Lo;0;L;;;;;N;;;;;
+1030E;OLD ITALIC LETTER ESH;Lo;0;L;;;;;N;;;;;
+1030F;OLD ITALIC LETTER O;Lo;0;L;;;;;N;;;;;
+10310;OLD ITALIC LETTER PE;Lo;0;L;;;;;N;;;;;
+10311;OLD ITALIC LETTER SHE;Lo;0;L;;;;;N;;;;;
+10312;OLD ITALIC LETTER KU;Lo;0;L;;;;;N;;;;;
+10313;OLD ITALIC LETTER ER;Lo;0;L;;;;;N;;;;;
+10314;OLD ITALIC LETTER ES;Lo;0;L;;;;;N;;;;;
+10315;OLD ITALIC LETTER TE;Lo;0;L;;;;;N;;;;;
+10316;OLD ITALIC LETTER U;Lo;0;L;;;;;N;;;;;
+10317;OLD ITALIC LETTER EKS;Lo;0;L;;;;;N;;;;;
+10318;OLD ITALIC LETTER PHE;Lo;0;L;;;;;N;;;;;
+10319;OLD ITALIC LETTER KHE;Lo;0;L;;;;;N;;;;;
+1031A;OLD ITALIC LETTER EF;Lo;0;L;;;;;N;;;;;
+1031B;OLD ITALIC LETTER ERS;Lo;0;L;;;;;N;;;;;
+1031C;OLD ITALIC LETTER CHE;Lo;0;L;;;;;N;;;;;
+1031D;OLD ITALIC LETTER II;Lo;0;L;;;;;N;;;;;
+1031E;OLD ITALIC LETTER UU;Lo;0;L;;;;;N;;;;;
+1031F;OLD ITALIC LETTER ESS;Lo;0;L;;;;;N;;;;;
+10320;OLD ITALIC NUMERAL ONE;No;0;L;;;;1;N;;;;;
+10321;OLD ITALIC NUMERAL FIVE;No;0;L;;;;5;N;;;;;
+10322;OLD ITALIC NUMERAL TEN;No;0;L;;;;10;N;;;;;
+10323;OLD ITALIC NUMERAL FIFTY;No;0;L;;;;50;N;;;;;
+10330;GOTHIC LETTER AHSA;Lo;0;L;;;;;N;;;;;
+10331;GOTHIC LETTER BAIRKAN;Lo;0;L;;;;;N;;;;;
+10332;GOTHIC LETTER GIBA;Lo;0;L;;;;;N;;;;;
+10333;GOTHIC LETTER DAGS;Lo;0;L;;;;;N;;;;;
+10334;GOTHIC LETTER AIHVUS;Lo;0;L;;;;;N;;;;;
+10335;GOTHIC LETTER QAIRTHRA;Lo;0;L;;;;;N;;;;;
+10336;GOTHIC LETTER IUJA;Lo;0;L;;;;;N;;;;;
+10337;GOTHIC LETTER HAGL;Lo;0;L;;;;;N;;;;;
+10338;GOTHIC LETTER THIUTH;Lo;0;L;;;;;N;;;;;
+10339;GOTHIC LETTER EIS;Lo;0;L;;;;;N;;;;;
+1033A;GOTHIC LETTER KUSMA;Lo;0;L;;;;;N;;;;;
+1033B;GOTHIC LETTER LAGUS;Lo;0;L;;;;;N;;;;;
+1033C;GOTHIC LETTER MANNA;Lo;0;L;;;;;N;;;;;
+1033D;GOTHIC LETTER NAUTHS;Lo;0;L;;;;;N;;;;;
+1033E;GOTHIC LETTER JER;Lo;0;L;;;;;N;;;;;
+1033F;GOTHIC LETTER URUS;Lo;0;L;;;;;N;;;;;
+10340;GOTHIC LETTER PAIRTHRA;Lo;0;L;;;;;N;;;;;
+10341;GOTHIC LETTER NINETY;Nl;0;L;;;;90;N;;;;;
+10342;GOTHIC LETTER RAIDA;Lo;0;L;;;;;N;;;;;
+10343;GOTHIC LETTER SAUIL;Lo;0;L;;;;;N;;;;;
+10344;GOTHIC LETTER TEIWS;Lo;0;L;;;;;N;;;;;
+10345;GOTHIC LETTER WINJA;Lo;0;L;;;;;N;;;;;
+10346;GOTHIC LETTER FAIHU;Lo;0;L;;;;;N;;;;;
+10347;GOTHIC LETTER IGGWS;Lo;0;L;;;;;N;;;;;
+10348;GOTHIC LETTER HWAIR;Lo;0;L;;;;;N;;;;;
+10349;GOTHIC LETTER OTHAL;Lo;0;L;;;;;N;;;;;
+1034A;GOTHIC LETTER NINE HUNDRED;Nl;0;L;;;;900;N;;;;;
+10350;OLD PERMIC LETTER AN;Lo;0;L;;;;;N;;;;;
+10351;OLD PERMIC LETTER BUR;Lo;0;L;;;;;N;;;;;
+10352;OLD PERMIC LETTER GAI;Lo;0;L;;;;;N;;;;;
+10353;OLD PERMIC LETTER DOI;Lo;0;L;;;;;N;;;;;
+10354;OLD PERMIC LETTER E;Lo;0;L;;;;;N;;;;;
+10355;OLD PERMIC LETTER ZHOI;Lo;0;L;;;;;N;;;;;
+10356;OLD PERMIC LETTER DZHOI;Lo;0;L;;;;;N;;;;;
+10357;OLD PERMIC LETTER ZATA;Lo;0;L;;;;;N;;;;;
+10358;OLD PERMIC LETTER DZITA;Lo;0;L;;;;;N;;;;;
+10359;OLD PERMIC LETTER I;Lo;0;L;;;;;N;;;;;
+1035A;OLD PERMIC LETTER KOKE;Lo;0;L;;;;;N;;;;;
+1035B;OLD PERMIC LETTER LEI;Lo;0;L;;;;;N;;;;;
+1035C;OLD PERMIC LETTER MENOE;Lo;0;L;;;;;N;;;;;
+1035D;OLD PERMIC LETTER NENOE;Lo;0;L;;;;;N;;;;;
+1035E;OLD PERMIC LETTER VOOI;Lo;0;L;;;;;N;;;;;
+1035F;OLD PERMIC LETTER PEEI;Lo;0;L;;;;;N;;;;;
+10360;OLD PERMIC LETTER REI;Lo;0;L;;;;;N;;;;;
+10361;OLD PERMIC LETTER SII;Lo;0;L;;;;;N;;;;;
+10362;OLD PERMIC LETTER TAI;Lo;0;L;;;;;N;;;;;
+10363;OLD PERMIC LETTER U;Lo;0;L;;;;;N;;;;;
+10364;OLD PERMIC LETTER CHERY;Lo;0;L;;;;;N;;;;;
+10365;OLD PERMIC LETTER SHOOI;Lo;0;L;;;;;N;;;;;
+10366;OLD PERMIC LETTER SHCHOOI;Lo;0;L;;;;;N;;;;;
+10367;OLD PERMIC LETTER YRY;Lo;0;L;;;;;N;;;;;
+10368;OLD PERMIC LETTER YERU;Lo;0;L;;;;;N;;;;;
+10369;OLD PERMIC LETTER O;Lo;0;L;;;;;N;;;;;
+1036A;OLD PERMIC LETTER OO;Lo;0;L;;;;;N;;;;;
+1036B;OLD PERMIC LETTER EF;Lo;0;L;;;;;N;;;;;
+1036C;OLD PERMIC LETTER HA;Lo;0;L;;;;;N;;;;;
+1036D;OLD PERMIC LETTER TSIU;Lo;0;L;;;;;N;;;;;
+1036E;OLD PERMIC LETTER VER;Lo;0;L;;;;;N;;;;;
+1036F;OLD PERMIC LETTER YER;Lo;0;L;;;;;N;;;;;
+10370;OLD PERMIC LETTER YERI;Lo;0;L;;;;;N;;;;;
+10371;OLD PERMIC LETTER YAT;Lo;0;L;;;;;N;;;;;
+10372;OLD PERMIC LETTER IE;Lo;0;L;;;;;N;;;;;
+10373;OLD PERMIC LETTER YU;Lo;0;L;;;;;N;;;;;
+10374;OLD PERMIC LETTER YA;Lo;0;L;;;;;N;;;;;
+10375;OLD PERMIC LETTER IA;Lo;0;L;;;;;N;;;;;
+10376;COMBINING OLD PERMIC LETTER AN;Mn;230;NSM;;;;;N;;;;;
+10377;COMBINING OLD PERMIC LETTER DOI;Mn;230;NSM;;;;;N;;;;;
+10378;COMBINING OLD PERMIC LETTER ZATA;Mn;230;NSM;;;;;N;;;;;
+10379;COMBINING OLD PERMIC LETTER NENOE;Mn;230;NSM;;;;;N;;;;;
+1037A;COMBINING OLD PERMIC LETTER SII;Mn;230;NSM;;;;;N;;;;;
+10380;UGARITIC LETTER ALPA;Lo;0;L;;;;;N;;;;;
+10381;UGARITIC LETTER BETA;Lo;0;L;;;;;N;;;;;
+10382;UGARITIC LETTER GAMLA;Lo;0;L;;;;;N;;;;;
+10383;UGARITIC LETTER KHA;Lo;0;L;;;;;N;;;;;
+10384;UGARITIC LETTER DELTA;Lo;0;L;;;;;N;;;;;
+10385;UGARITIC LETTER HO;Lo;0;L;;;;;N;;;;;
+10386;UGARITIC LETTER WO;Lo;0;L;;;;;N;;;;;
+10387;UGARITIC LETTER ZETA;Lo;0;L;;;;;N;;;;;
+10388;UGARITIC LETTER HOTA;Lo;0;L;;;;;N;;;;;
+10389;UGARITIC LETTER TET;Lo;0;L;;;;;N;;;;;
+1038A;UGARITIC LETTER YOD;Lo;0;L;;;;;N;;;;;
+1038B;UGARITIC LETTER KAF;Lo;0;L;;;;;N;;;;;
+1038C;UGARITIC LETTER SHIN;Lo;0;L;;;;;N;;;;;
+1038D;UGARITIC LETTER LAMDA;Lo;0;L;;;;;N;;;;;
+1038E;UGARITIC LETTER MEM;Lo;0;L;;;;;N;;;;;
+1038F;UGARITIC LETTER DHAL;Lo;0;L;;;;;N;;;;;
+10390;UGARITIC LETTER NUN;Lo;0;L;;;;;N;;;;;
+10391;UGARITIC LETTER ZU;Lo;0;L;;;;;N;;;;;
+10392;UGARITIC LETTER SAMKA;Lo;0;L;;;;;N;;;;;
+10393;UGARITIC LETTER AIN;Lo;0;L;;;;;N;;;;;
+10394;UGARITIC LETTER PU;Lo;0;L;;;;;N;;;;;
+10395;UGARITIC LETTER SADE;Lo;0;L;;;;;N;;;;;
+10396;UGARITIC LETTER QOPA;Lo;0;L;;;;;N;;;;;
+10397;UGARITIC LETTER RASHA;Lo;0;L;;;;;N;;;;;
+10398;UGARITIC LETTER THANNA;Lo;0;L;;;;;N;;;;;
+10399;UGARITIC LETTER GHAIN;Lo;0;L;;;;;N;;;;;
+1039A;UGARITIC LETTER TO;Lo;0;L;;;;;N;;;;;
+1039B;UGARITIC LETTER I;Lo;0;L;;;;;N;;;;;
+1039C;UGARITIC LETTER U;Lo;0;L;;;;;N;;;;;
+1039D;UGARITIC LETTER SSU;Lo;0;L;;;;;N;;;;;
+1039F;UGARITIC WORD DIVIDER;Po;0;L;;;;;N;;;;;
+103A0;OLD PERSIAN SIGN A;Lo;0;L;;;;;N;;;;;
+103A1;OLD PERSIAN SIGN I;Lo;0;L;;;;;N;;;;;
+103A2;OLD PERSIAN SIGN U;Lo;0;L;;;;;N;;;;;
+103A3;OLD PERSIAN SIGN KA;Lo;0;L;;;;;N;;;;;
+103A4;OLD PERSIAN SIGN KU;Lo;0;L;;;;;N;;;;;
+103A5;OLD PERSIAN SIGN GA;Lo;0;L;;;;;N;;;;;
+103A6;OLD PERSIAN SIGN GU;Lo;0;L;;;;;N;;;;;
+103A7;OLD PERSIAN SIGN XA;Lo;0;L;;;;;N;;;;;
+103A8;OLD PERSIAN SIGN CA;Lo;0;L;;;;;N;;;;;
+103A9;OLD PERSIAN SIGN JA;Lo;0;L;;;;;N;;;;;
+103AA;OLD PERSIAN SIGN JI;Lo;0;L;;;;;N;;;;;
+103AB;OLD PERSIAN SIGN TA;Lo;0;L;;;;;N;;;;;
+103AC;OLD PERSIAN SIGN TU;Lo;0;L;;;;;N;;;;;
+103AD;OLD PERSIAN SIGN DA;Lo;0;L;;;;;N;;;;;
+103AE;OLD PERSIAN SIGN DI;Lo;0;L;;;;;N;;;;;
+103AF;OLD PERSIAN SIGN DU;Lo;0;L;;;;;N;;;;;
+103B0;OLD PERSIAN SIGN THA;Lo;0;L;;;;;N;;;;;
+103B1;OLD PERSIAN SIGN PA;Lo;0;L;;;;;N;;;;;
+103B2;OLD PERSIAN SIGN BA;Lo;0;L;;;;;N;;;;;
+103B3;OLD PERSIAN SIGN FA;Lo;0;L;;;;;N;;;;;
+103B4;OLD PERSIAN SIGN NA;Lo;0;L;;;;;N;;;;;
+103B5;OLD PERSIAN SIGN NU;Lo;0;L;;;;;N;;;;;
+103B6;OLD PERSIAN SIGN MA;Lo;0;L;;;;;N;;;;;
+103B7;OLD PERSIAN SIGN MI;Lo;0;L;;;;;N;;;;;
+103B8;OLD PERSIAN SIGN MU;Lo;0;L;;;;;N;;;;;
+103B9;OLD PERSIAN SIGN YA;Lo;0;L;;;;;N;;;;;
+103BA;OLD PERSIAN SIGN VA;Lo;0;L;;;;;N;;;;;
+103BB;OLD PERSIAN SIGN VI;Lo;0;L;;;;;N;;;;;
+103BC;OLD PERSIAN SIGN RA;Lo;0;L;;;;;N;;;;;
+103BD;OLD PERSIAN SIGN RU;Lo;0;L;;;;;N;;;;;
+103BE;OLD PERSIAN SIGN LA;Lo;0;L;;;;;N;;;;;
+103BF;OLD PERSIAN SIGN SA;Lo;0;L;;;;;N;;;;;
+103C0;OLD PERSIAN SIGN ZA;Lo;0;L;;;;;N;;;;;
+103C1;OLD PERSIAN SIGN SHA;Lo;0;L;;;;;N;;;;;
+103C2;OLD PERSIAN SIGN SSA;Lo;0;L;;;;;N;;;;;
+103C3;OLD PERSIAN SIGN HA;Lo;0;L;;;;;N;;;;;
+103C8;OLD PERSIAN SIGN AURAMAZDAA;Lo;0;L;;;;;N;;;;;
+103C9;OLD PERSIAN SIGN AURAMAZDAA-2;Lo;0;L;;;;;N;;;;;
+103CA;OLD PERSIAN SIGN AURAMAZDAAHA;Lo;0;L;;;;;N;;;;;
+103CB;OLD PERSIAN SIGN XSHAAYATHIYA;Lo;0;L;;;;;N;;;;;
+103CC;OLD PERSIAN SIGN DAHYAAUSH;Lo;0;L;;;;;N;;;;;
+103CD;OLD PERSIAN SIGN DAHYAAUSH-2;Lo;0;L;;;;;N;;;;;
+103CE;OLD PERSIAN SIGN BAGA;Lo;0;L;;;;;N;;;;;
+103CF;OLD PERSIAN SIGN BUUMISH;Lo;0;L;;;;;N;;;;;
+103D0;OLD PERSIAN WORD DIVIDER;Po;0;L;;;;;N;;;;;
+103D1;OLD PERSIAN NUMBER ONE;Nl;0;L;;;;1;N;;;;;
+103D2;OLD PERSIAN NUMBER TWO;Nl;0;L;;;;2;N;;;;;
+103D3;OLD PERSIAN NUMBER TEN;Nl;0;L;;;;10;N;;;;;
+103D4;OLD PERSIAN NUMBER TWENTY;Nl;0;L;;;;20;N;;;;;
+103D5;OLD PERSIAN NUMBER HUNDRED;Nl;0;L;;;;100;N;;;;;
+10400;DESERET CAPITAL LETTER LONG I;Lu;0;L;;;;;N;;;;10428;
+10401;DESERET CAPITAL LETTER LONG E;Lu;0;L;;;;;N;;;;10429;
+10402;DESERET CAPITAL LETTER LONG A;Lu;0;L;;;;;N;;;;1042A;
+10403;DESERET CAPITAL LETTER LONG AH;Lu;0;L;;;;;N;;;;1042B;
+10404;DESERET CAPITAL LETTER LONG O;Lu;0;L;;;;;N;;;;1042C;
+10405;DESERET CAPITAL LETTER LONG OO;Lu;0;L;;;;;N;;;;1042D;
+10406;DESERET CAPITAL LETTER SHORT I;Lu;0;L;;;;;N;;;;1042E;
+10407;DESERET CAPITAL LETTER SHORT E;Lu;0;L;;;;;N;;;;1042F;
+10408;DESERET CAPITAL LETTER SHORT A;Lu;0;L;;;;;N;;;;10430;
+10409;DESERET CAPITAL LETTER SHORT AH;Lu;0;L;;;;;N;;;;10431;
+1040A;DESERET CAPITAL LETTER SHORT O;Lu;0;L;;;;;N;;;;10432;
+1040B;DESERET CAPITAL LETTER SHORT OO;Lu;0;L;;;;;N;;;;10433;
+1040C;DESERET CAPITAL LETTER AY;Lu;0;L;;;;;N;;;;10434;
+1040D;DESERET CAPITAL LETTER OW;Lu;0;L;;;;;N;;;;10435;
+1040E;DESERET CAPITAL LETTER WU;Lu;0;L;;;;;N;;;;10436;
+1040F;DESERET CAPITAL LETTER YEE;Lu;0;L;;;;;N;;;;10437;
+10410;DESERET CAPITAL LETTER H;Lu;0;L;;;;;N;;;;10438;
+10411;DESERET CAPITAL LETTER PEE;Lu;0;L;;;;;N;;;;10439;
+10412;DESERET CAPITAL LETTER BEE;Lu;0;L;;;;;N;;;;1043A;
+10413;DESERET CAPITAL LETTER TEE;Lu;0;L;;;;;N;;;;1043B;
+10414;DESERET CAPITAL LETTER DEE;Lu;0;L;;;;;N;;;;1043C;
+10415;DESERET CAPITAL LETTER CHEE;Lu;0;L;;;;;N;;;;1043D;
+10416;DESERET CAPITAL LETTER JEE;Lu;0;L;;;;;N;;;;1043E;
+10417;DESERET CAPITAL LETTER KAY;Lu;0;L;;;;;N;;;;1043F;
+10418;DESERET CAPITAL LETTER GAY;Lu;0;L;;;;;N;;;;10440;
+10419;DESERET CAPITAL LETTER EF;Lu;0;L;;;;;N;;;;10441;
+1041A;DESERET CAPITAL LETTER VEE;Lu;0;L;;;;;N;;;;10442;
+1041B;DESERET CAPITAL LETTER ETH;Lu;0;L;;;;;N;;;;10443;
+1041C;DESERET CAPITAL LETTER THEE;Lu;0;L;;;;;N;;;;10444;
+1041D;DESERET CAPITAL LETTER ES;Lu;0;L;;;;;N;;;;10445;
+1041E;DESERET CAPITAL LETTER ZEE;Lu;0;L;;;;;N;;;;10446;
+1041F;DESERET CAPITAL LETTER ESH;Lu;0;L;;;;;N;;;;10447;
+10420;DESERET CAPITAL LETTER ZHEE;Lu;0;L;;;;;N;;;;10448;
+10421;DESERET CAPITAL LETTER ER;Lu;0;L;;;;;N;;;;10449;
+10422;DESERET CAPITAL LETTER EL;Lu;0;L;;;;;N;;;;1044A;
+10423;DESERET CAPITAL LETTER EM;Lu;0;L;;;;;N;;;;1044B;
+10424;DESERET CAPITAL LETTER EN;Lu;0;L;;;;;N;;;;1044C;
+10425;DESERET CAPITAL LETTER ENG;Lu;0;L;;;;;N;;;;1044D;
+10426;DESERET CAPITAL LETTER OI;Lu;0;L;;;;;N;;;;1044E;
+10427;DESERET CAPITAL LETTER EW;Lu;0;L;;;;;N;;;;1044F;
+10428;DESERET SMALL LETTER LONG I;Ll;0;L;;;;;N;;;10400;;10400
+10429;DESERET SMALL LETTER LONG E;Ll;0;L;;;;;N;;;10401;;10401
+1042A;DESERET SMALL LETTER LONG A;Ll;0;L;;;;;N;;;10402;;10402
+1042B;DESERET SMALL LETTER LONG AH;Ll;0;L;;;;;N;;;10403;;10403
+1042C;DESERET SMALL LETTER LONG O;Ll;0;L;;;;;N;;;10404;;10404
+1042D;DESERET SMALL LETTER LONG OO;Ll;0;L;;;;;N;;;10405;;10405
+1042E;DESERET SMALL LETTER SHORT I;Ll;0;L;;;;;N;;;10406;;10406
+1042F;DESERET SMALL LETTER SHORT E;Ll;0;L;;;;;N;;;10407;;10407
+10430;DESERET SMALL LETTER SHORT A;Ll;0;L;;;;;N;;;10408;;10408
+10431;DESERET SMALL LETTER SHORT AH;Ll;0;L;;;;;N;;;10409;;10409
+10432;DESERET SMALL LETTER SHORT O;Ll;0;L;;;;;N;;;1040A;;1040A
+10433;DESERET SMALL LETTER SHORT OO;Ll;0;L;;;;;N;;;1040B;;1040B
+10434;DESERET SMALL LETTER AY;Ll;0;L;;;;;N;;;1040C;;1040C
+10435;DESERET SMALL LETTER OW;Ll;0;L;;;;;N;;;1040D;;1040D
+10436;DESERET SMALL LETTER WU;Ll;0;L;;;;;N;;;1040E;;1040E
+10437;DESERET SMALL LETTER YEE;Ll;0;L;;;;;N;;;1040F;;1040F
+10438;DESERET SMALL LETTER H;Ll;0;L;;;;;N;;;10410;;10410
+10439;DESERET SMALL LETTER PEE;Ll;0;L;;;;;N;;;10411;;10411
+1043A;DESERET SMALL LETTER BEE;Ll;0;L;;;;;N;;;10412;;10412
+1043B;DESERET SMALL LETTER TEE;Ll;0;L;;;;;N;;;10413;;10413
+1043C;DESERET SMALL LETTER DEE;Ll;0;L;;;;;N;;;10414;;10414
+1043D;DESERET SMALL LETTER CHEE;Ll;0;L;;;;;N;;;10415;;10415
+1043E;DESERET SMALL LETTER JEE;Ll;0;L;;;;;N;;;10416;;10416
+1043F;DESERET SMALL LETTER KAY;Ll;0;L;;;;;N;;;10417;;10417
+10440;DESERET SMALL LETTER GAY;Ll;0;L;;;;;N;;;10418;;10418
+10441;DESERET SMALL LETTER EF;Ll;0;L;;;;;N;;;10419;;10419
+10442;DESERET SMALL LETTER VEE;Ll;0;L;;;;;N;;;1041A;;1041A
+10443;DESERET SMALL LETTER ETH;Ll;0;L;;;;;N;;;1041B;;1041B
+10444;DESERET SMALL LETTER THEE;Ll;0;L;;;;;N;;;1041C;;1041C
+10445;DESERET SMALL LETTER ES;Ll;0;L;;;;;N;;;1041D;;1041D
+10446;DESERET SMALL LETTER ZEE;Ll;0;L;;;;;N;;;1041E;;1041E
+10447;DESERET SMALL LETTER ESH;Ll;0;L;;;;;N;;;1041F;;1041F
+10448;DESERET SMALL LETTER ZHEE;Ll;0;L;;;;;N;;;10420;;10420
+10449;DESERET SMALL LETTER ER;Ll;0;L;;;;;N;;;10421;;10421
+1044A;DESERET SMALL LETTER EL;Ll;0;L;;;;;N;;;10422;;10422
+1044B;DESERET SMALL LETTER EM;Ll;0;L;;;;;N;;;10423;;10423
+1044C;DESERET SMALL LETTER EN;Ll;0;L;;;;;N;;;10424;;10424
+1044D;DESERET SMALL LETTER ENG;Ll;0;L;;;;;N;;;10425;;10425
+1044E;DESERET SMALL LETTER OI;Ll;0;L;;;;;N;;;10426;;10426
+1044F;DESERET SMALL LETTER EW;Ll;0;L;;;;;N;;;10427;;10427
+10450;SHAVIAN LETTER PEEP;Lo;0;L;;;;;N;;;;;
+10451;SHAVIAN LETTER TOT;Lo;0;L;;;;;N;;;;;
+10452;SHAVIAN LETTER KICK;Lo;0;L;;;;;N;;;;;
+10453;SHAVIAN LETTER FEE;Lo;0;L;;;;;N;;;;;
+10454;SHAVIAN LETTER THIGH;Lo;0;L;;;;;N;;;;;
+10455;SHAVIAN LETTER SO;Lo;0;L;;;;;N;;;;;
+10456;SHAVIAN LETTER SURE;Lo;0;L;;;;;N;;;;;
+10457;SHAVIAN LETTER CHURCH;Lo;0;L;;;;;N;;;;;
+10458;SHAVIAN LETTER YEA;Lo;0;L;;;;;N;;;;;
+10459;SHAVIAN LETTER HUNG;Lo;0;L;;;;;N;;;;;
+1045A;SHAVIAN LETTER BIB;Lo;0;L;;;;;N;;;;;
+1045B;SHAVIAN LETTER DEAD;Lo;0;L;;;;;N;;;;;
+1045C;SHAVIAN LETTER GAG;Lo;0;L;;;;;N;;;;;
+1045D;SHAVIAN LETTER VOW;Lo;0;L;;;;;N;;;;;
+1045E;SHAVIAN LETTER THEY;Lo;0;L;;;;;N;;;;;
+1045F;SHAVIAN LETTER ZOO;Lo;0;L;;;;;N;;;;;
+10460;SHAVIAN LETTER MEASURE;Lo;0;L;;;;;N;;;;;
+10461;SHAVIAN LETTER JUDGE;Lo;0;L;;;;;N;;;;;
+10462;SHAVIAN LETTER WOE;Lo;0;L;;;;;N;;;;;
+10463;SHAVIAN LETTER HA-HA;Lo;0;L;;;;;N;;;;;
+10464;SHAVIAN LETTER LOLL;Lo;0;L;;;;;N;;;;;
+10465;SHAVIAN LETTER MIME;Lo;0;L;;;;;N;;;;;
+10466;SHAVIAN LETTER IF;Lo;0;L;;;;;N;;;;;
+10467;SHAVIAN LETTER EGG;Lo;0;L;;;;;N;;;;;
+10468;SHAVIAN LETTER ASH;Lo;0;L;;;;;N;;;;;
+10469;SHAVIAN LETTER ADO;Lo;0;L;;;;;N;;;;;
+1046A;SHAVIAN LETTER ON;Lo;0;L;;;;;N;;;;;
+1046B;SHAVIAN LETTER WOOL;Lo;0;L;;;;;N;;;;;
+1046C;SHAVIAN LETTER OUT;Lo;0;L;;;;;N;;;;;
+1046D;SHAVIAN LETTER AH;Lo;0;L;;;;;N;;;;;
+1046E;SHAVIAN LETTER ROAR;Lo;0;L;;;;;N;;;;;
+1046F;SHAVIAN LETTER NUN;Lo;0;L;;;;;N;;;;;
+10470;SHAVIAN LETTER EAT;Lo;0;L;;;;;N;;;;;
+10471;SHAVIAN LETTER AGE;Lo;0;L;;;;;N;;;;;
+10472;SHAVIAN LETTER ICE;Lo;0;L;;;;;N;;;;;
+10473;SHAVIAN LETTER UP;Lo;0;L;;;;;N;;;;;
+10474;SHAVIAN LETTER OAK;Lo;0;L;;;;;N;;;;;
+10475;SHAVIAN LETTER OOZE;Lo;0;L;;;;;N;;;;;
+10476;SHAVIAN LETTER OIL;Lo;0;L;;;;;N;;;;;
+10477;SHAVIAN LETTER AWE;Lo;0;L;;;;;N;;;;;
+10478;SHAVIAN LETTER ARE;Lo;0;L;;;;;N;;;;;
+10479;SHAVIAN LETTER OR;Lo;0;L;;;;;N;;;;;
+1047A;SHAVIAN LETTER AIR;Lo;0;L;;;;;N;;;;;
+1047B;SHAVIAN LETTER ERR;Lo;0;L;;;;;N;;;;;
+1047C;SHAVIAN LETTER ARRAY;Lo;0;L;;;;;N;;;;;
+1047D;SHAVIAN LETTER EAR;Lo;0;L;;;;;N;;;;;
+1047E;SHAVIAN LETTER IAN;Lo;0;L;;;;;N;;;;;
+1047F;SHAVIAN LETTER YEW;Lo;0;L;;;;;N;;;;;
+10480;OSMANYA LETTER ALEF;Lo;0;L;;;;;N;;;;;
+10481;OSMANYA LETTER BA;Lo;0;L;;;;;N;;;;;
+10482;OSMANYA LETTER TA;Lo;0;L;;;;;N;;;;;
+10483;OSMANYA LETTER JA;Lo;0;L;;;;;N;;;;;
+10484;OSMANYA LETTER XA;Lo;0;L;;;;;N;;;;;
+10485;OSMANYA LETTER KHA;Lo;0;L;;;;;N;;;;;
+10486;OSMANYA LETTER DEEL;Lo;0;L;;;;;N;;;;;
+10487;OSMANYA LETTER RA;Lo;0;L;;;;;N;;;;;
+10488;OSMANYA LETTER SA;Lo;0;L;;;;;N;;;;;
+10489;OSMANYA LETTER SHIIN;Lo;0;L;;;;;N;;;;;
+1048A;OSMANYA LETTER DHA;Lo;0;L;;;;;N;;;;;
+1048B;OSMANYA LETTER CAYN;Lo;0;L;;;;;N;;;;;
+1048C;OSMANYA LETTER GA;Lo;0;L;;;;;N;;;;;
+1048D;OSMANYA LETTER FA;Lo;0;L;;;;;N;;;;;
+1048E;OSMANYA LETTER QAAF;Lo;0;L;;;;;N;;;;;
+1048F;OSMANYA LETTER KAAF;Lo;0;L;;;;;N;;;;;
+10490;OSMANYA LETTER LAAN;Lo;0;L;;;;;N;;;;;
+10491;OSMANYA LETTER MIIN;Lo;0;L;;;;;N;;;;;
+10492;OSMANYA LETTER NUUN;Lo;0;L;;;;;N;;;;;
+10493;OSMANYA LETTER WAW;Lo;0;L;;;;;N;;;;;
+10494;OSMANYA LETTER HA;Lo;0;L;;;;;N;;;;;
+10495;OSMANYA LETTER YA;Lo;0;L;;;;;N;;;;;
+10496;OSMANYA LETTER A;Lo;0;L;;;;;N;;;;;
+10497;OSMANYA LETTER E;Lo;0;L;;;;;N;;;;;
+10498;OSMANYA LETTER I;Lo;0;L;;;;;N;;;;;
+10499;OSMANYA LETTER O;Lo;0;L;;;;;N;;;;;
+1049A;OSMANYA LETTER U;Lo;0;L;;;;;N;;;;;
+1049B;OSMANYA LETTER AA;Lo;0;L;;;;;N;;;;;
+1049C;OSMANYA LETTER EE;Lo;0;L;;;;;N;;;;;
+1049D;OSMANYA LETTER OO;Lo;0;L;;;;;N;;;;;
+104A0;OSMANYA DIGIT ZERO;Nd;0;L;;0;0;0;N;;;;;
+104A1;OSMANYA DIGIT ONE;Nd;0;L;;1;1;1;N;;;;;
+104A2;OSMANYA DIGIT TWO;Nd;0;L;;2;2;2;N;;;;;
+104A3;OSMANYA DIGIT THREE;Nd;0;L;;3;3;3;N;;;;;
+104A4;OSMANYA DIGIT FOUR;Nd;0;L;;4;4;4;N;;;;;
+104A5;OSMANYA DIGIT FIVE;Nd;0;L;;5;5;5;N;;;;;
+104A6;OSMANYA DIGIT SIX;Nd;0;L;;6;6;6;N;;;;;
+104A7;OSMANYA DIGIT SEVEN;Nd;0;L;;7;7;7;N;;;;;
+104A8;OSMANYA DIGIT EIGHT;Nd;0;L;;8;8;8;N;;;;;
+104A9;OSMANYA DIGIT NINE;Nd;0;L;;9;9;9;N;;;;;
+104B0;OSAGE CAPITAL LETTER A;Lu;0;L;;;;;N;;;;104D8;
+104B1;OSAGE CAPITAL LETTER AI;Lu;0;L;;;;;N;;;;104D9;
+104B2;OSAGE CAPITAL LETTER AIN;Lu;0;L;;;;;N;;;;104DA;
+104B3;OSAGE CAPITAL LETTER AH;Lu;0;L;;;;;N;;;;104DB;
+104B4;OSAGE CAPITAL LETTER BRA;Lu;0;L;;;;;N;;;;104DC;
+104B5;OSAGE CAPITAL LETTER CHA;Lu;0;L;;;;;N;;;;104DD;
+104B6;OSAGE CAPITAL LETTER EHCHA;Lu;0;L;;;;;N;;;;104DE;
+104B7;OSAGE CAPITAL LETTER E;Lu;0;L;;;;;N;;;;104DF;
+104B8;OSAGE CAPITAL LETTER EIN;Lu;0;L;;;;;N;;;;104E0;
+104B9;OSAGE CAPITAL LETTER HA;Lu;0;L;;;;;N;;;;104E1;
+104BA;OSAGE CAPITAL LETTER HYA;Lu;0;L;;;;;N;;;;104E2;
+104BB;OSAGE CAPITAL LETTER I;Lu;0;L;;;;;N;;;;104E3;
+104BC;OSAGE CAPITAL LETTER KA;Lu;0;L;;;;;N;;;;104E4;
+104BD;OSAGE CAPITAL LETTER EHKA;Lu;0;L;;;;;N;;;;104E5;
+104BE;OSAGE CAPITAL LETTER KYA;Lu;0;L;;;;;N;;;;104E6;
+104BF;OSAGE CAPITAL LETTER LA;Lu;0;L;;;;;N;;;;104E7;
+104C0;OSAGE CAPITAL LETTER MA;Lu;0;L;;;;;N;;;;104E8;
+104C1;OSAGE CAPITAL LETTER NA;Lu;0;L;;;;;N;;;;104E9;
+104C2;OSAGE CAPITAL LETTER O;Lu;0;L;;;;;N;;;;104EA;
+104C3;OSAGE CAPITAL LETTER OIN;Lu;0;L;;;;;N;;;;104EB;
+104C4;OSAGE CAPITAL LETTER PA;Lu;0;L;;;;;N;;;;104EC;
+104C5;OSAGE CAPITAL LETTER EHPA;Lu;0;L;;;;;N;;;;104ED;
+104C6;OSAGE CAPITAL LETTER SA;Lu;0;L;;;;;N;;;;104EE;
+104C7;OSAGE CAPITAL LETTER SHA;Lu;0;L;;;;;N;;;;104EF;
+104C8;OSAGE CAPITAL LETTER TA;Lu;0;L;;;;;N;;;;104F0;
+104C9;OSAGE CAPITAL LETTER EHTA;Lu;0;L;;;;;N;;;;104F1;
+104CA;OSAGE CAPITAL LETTER TSA;Lu;0;L;;;;;N;;;;104F2;
+104CB;OSAGE CAPITAL LETTER EHTSA;Lu;0;L;;;;;N;;;;104F3;
+104CC;OSAGE CAPITAL LETTER TSHA;Lu;0;L;;;;;N;;;;104F4;
+104CD;OSAGE CAPITAL LETTER DHA;Lu;0;L;;;;;N;;;;104F5;
+104CE;OSAGE CAPITAL LETTER U;Lu;0;L;;;;;N;;;;104F6;
+104CF;OSAGE CAPITAL LETTER WA;Lu;0;L;;;;;N;;;;104F7;
+104D0;OSAGE CAPITAL LETTER KHA;Lu;0;L;;;;;N;;;;104F8;
+104D1;OSAGE CAPITAL LETTER GHA;Lu;0;L;;;;;N;;;;104F9;
+104D2;OSAGE CAPITAL LETTER ZA;Lu;0;L;;;;;N;;;;104FA;
+104D3;OSAGE CAPITAL LETTER ZHA;Lu;0;L;;;;;N;;;;104FB;
+104D8;OSAGE SMALL LETTER A;Ll;0;L;;;;;N;;;104B0;;104B0
+104D9;OSAGE SMALL LETTER AI;Ll;0;L;;;;;N;;;104B1;;104B1
+104DA;OSAGE SMALL LETTER AIN;Ll;0;L;;;;;N;;;104B2;;104B2
+104DB;OSAGE SMALL LETTER AH;Ll;0;L;;;;;N;;;104B3;;104B3
+104DC;OSAGE SMALL LETTER BRA;Ll;0;L;;;;;N;;;104B4;;104B4
+104DD;OSAGE SMALL LETTER CHA;Ll;0;L;;;;;N;;;104B5;;104B5
+104DE;OSAGE SMALL LETTER EHCHA;Ll;0;L;;;;;N;;;104B6;;104B6
+104DF;OSAGE SMALL LETTER E;Ll;0;L;;;;;N;;;104B7;;104B7
+104E0;OSAGE SMALL LETTER EIN;Ll;0;L;;;;;N;;;104B8;;104B8
+104E1;OSAGE SMALL LETTER HA;Ll;0;L;;;;;N;;;104B9;;104B9
+104E2;OSAGE SMALL LETTER HYA;Ll;0;L;;;;;N;;;104BA;;104BA
+104E3;OSAGE SMALL LETTER I;Ll;0;L;;;;;N;;;104BB;;104BB
+104E4;OSAGE SMALL LETTER KA;Ll;0;L;;;;;N;;;104BC;;104BC
+104E5;OSAGE SMALL LETTER EHKA;Ll;0;L;;;;;N;;;104BD;;104BD
+104E6;OSAGE SMALL LETTER KYA;Ll;0;L;;;;;N;;;104BE;;104BE
+104E7;OSAGE SMALL LETTER LA;Ll;0;L;;;;;N;;;104BF;;104BF
+104E8;OSAGE SMALL LETTER MA;Ll;0;L;;;;;N;;;104C0;;104C0
+104E9;OSAGE SMALL LETTER NA;Ll;0;L;;;;;N;;;104C1;;104C1
+104EA;OSAGE SMALL LETTER O;Ll;0;L;;;;;N;;;104C2;;104C2
+104EB;OSAGE SMALL LETTER OIN;Ll;0;L;;;;;N;;;104C3;;104C3
+104EC;OSAGE SMALL LETTER PA;Ll;0;L;;;;;N;;;104C4;;104C4
+104ED;OSAGE SMALL LETTER EHPA;Ll;0;L;;;;;N;;;104C5;;104C5
+104EE;OSAGE SMALL LETTER SA;Ll;0;L;;;;;N;;;104C6;;104C6
+104EF;OSAGE SMALL LETTER SHA;Ll;0;L;;;;;N;;;104C7;;104C7
+104F0;OSAGE SMALL LETTER TA;Ll;0;L;;;;;N;;;104C8;;104C8
+104F1;OSAGE SMALL LETTER EHTA;Ll;0;L;;;;;N;;;104C9;;104C9
+104F2;OSAGE SMALL LETTER TSA;Ll;0;L;;;;;N;;;104CA;;104CA
+104F3;OSAGE SMALL LETTER EHTSA;Ll;0;L;;;;;N;;;104CB;;104CB
+104F4;OSAGE SMALL LETTER TSHA;Ll;0;L;;;;;N;;;104CC;;104CC
+104F5;OSAGE SMALL LETTER DHA;Ll;0;L;;;;;N;;;104CD;;104CD
+104F6;OSAGE SMALL LETTER U;Ll;0;L;;;;;N;;;104CE;;104CE
+104F7;OSAGE SMALL LETTER WA;Ll;0;L;;;;;N;;;104CF;;104CF
+104F8;OSAGE SMALL LETTER KHA;Ll;0;L;;;;;N;;;104D0;;104D0
+104F9;OSAGE SMALL LETTER GHA;Ll;0;L;;;;;N;;;104D1;;104D1
+104FA;OSAGE SMALL LETTER ZA;Ll;0;L;;;;;N;;;104D2;;104D2
+104FB;OSAGE SMALL LETTER ZHA;Ll;0;L;;;;;N;;;104D3;;104D3
+10500;ELBASAN LETTER A;Lo;0;L;;;;;N;;;;;
+10501;ELBASAN LETTER BE;Lo;0;L;;;;;N;;;;;
+10502;ELBASAN LETTER CE;Lo;0;L;;;;;N;;;;;
+10503;ELBASAN LETTER CHE;Lo;0;L;;;;;N;;;;;
+10504;ELBASAN LETTER DE;Lo;0;L;;;;;N;;;;;
+10505;ELBASAN LETTER NDE;Lo;0;L;;;;;N;;;;;
+10506;ELBASAN LETTER DHE;Lo;0;L;;;;;N;;;;;
+10507;ELBASAN LETTER EI;Lo;0;L;;;;;N;;;;;
+10508;ELBASAN LETTER E;Lo;0;L;;;;;N;;;;;
+10509;ELBASAN LETTER FE;Lo;0;L;;;;;N;;;;;
+1050A;ELBASAN LETTER GE;Lo;0;L;;;;;N;;;;;
+1050B;ELBASAN LETTER GJE;Lo;0;L;;;;;N;;;;;
+1050C;ELBASAN LETTER HE;Lo;0;L;;;;;N;;;;;
+1050D;ELBASAN LETTER I;Lo;0;L;;;;;N;;;;;
+1050E;ELBASAN LETTER JE;Lo;0;L;;;;;N;;;;;
+1050F;ELBASAN LETTER KE;Lo;0;L;;;;;N;;;;;
+10510;ELBASAN LETTER LE;Lo;0;L;;;;;N;;;;;
+10511;ELBASAN LETTER LLE;Lo;0;L;;;;;N;;;;;
+10512;ELBASAN LETTER ME;Lo;0;L;;;;;N;;;;;
+10513;ELBASAN LETTER NE;Lo;0;L;;;;;N;;;;;
+10514;ELBASAN LETTER NA;Lo;0;L;;;;;N;;;;;
+10515;ELBASAN LETTER NJE;Lo;0;L;;;;;N;;;;;
+10516;ELBASAN LETTER O;Lo;0;L;;;;;N;;;;;
+10517;ELBASAN LETTER PE;Lo;0;L;;;;;N;;;;;
+10518;ELBASAN LETTER QE;Lo;0;L;;;;;N;;;;;
+10519;ELBASAN LETTER RE;Lo;0;L;;;;;N;;;;;
+1051A;ELBASAN LETTER RRE;Lo;0;L;;;;;N;;;;;
+1051B;ELBASAN LETTER SE;Lo;0;L;;;;;N;;;;;
+1051C;ELBASAN LETTER SHE;Lo;0;L;;;;;N;;;;;
+1051D;ELBASAN LETTER TE;Lo;0;L;;;;;N;;;;;
+1051E;ELBASAN LETTER THE;Lo;0;L;;;;;N;;;;;
+1051F;ELBASAN LETTER U;Lo;0;L;;;;;N;;;;;
+10520;ELBASAN LETTER VE;Lo;0;L;;;;;N;;;;;
+10521;ELBASAN LETTER XE;Lo;0;L;;;;;N;;;;;
+10522;ELBASAN LETTER Y;Lo;0;L;;;;;N;;;;;
+10523;ELBASAN LETTER ZE;Lo;0;L;;;;;N;;;;;
+10524;ELBASAN LETTER ZHE;Lo;0;L;;;;;N;;;;;
+10525;ELBASAN LETTER GHE;Lo;0;L;;;;;N;;;;;
+10526;ELBASAN LETTER GHAMMA;Lo;0;L;;;;;N;;;;;
+10527;ELBASAN LETTER KHE;Lo;0;L;;;;;N;;;;;
+10530;CAUCASIAN ALBANIAN LETTER ALT;Lo;0;L;;;;;N;;;;;
+10531;CAUCASIAN ALBANIAN LETTER BET;Lo;0;L;;;;;N;;;;;
+10532;CAUCASIAN ALBANIAN LETTER GIM;Lo;0;L;;;;;N;;;;;
+10533;CAUCASIAN ALBANIAN LETTER DAT;Lo;0;L;;;;;N;;;;;
+10534;CAUCASIAN ALBANIAN LETTER EB;Lo;0;L;;;;;N;;;;;
+10535;CAUCASIAN ALBANIAN LETTER ZARL;Lo;0;L;;;;;N;;;;;
+10536;CAUCASIAN ALBANIAN LETTER EYN;Lo;0;L;;;;;N;;;;;
+10537;CAUCASIAN ALBANIAN LETTER ZHIL;Lo;0;L;;;;;N;;;;;
+10538;CAUCASIAN ALBANIAN LETTER TAS;Lo;0;L;;;;;N;;;;;
+10539;CAUCASIAN ALBANIAN LETTER CHA;Lo;0;L;;;;;N;;;;;
+1053A;CAUCASIAN ALBANIAN LETTER YOWD;Lo;0;L;;;;;N;;;;;
+1053B;CAUCASIAN ALBANIAN LETTER ZHA;Lo;0;L;;;;;N;;;;;
+1053C;CAUCASIAN ALBANIAN LETTER IRB;Lo;0;L;;;;;N;;;;;
+1053D;CAUCASIAN ALBANIAN LETTER SHA;Lo;0;L;;;;;N;;;;;
+1053E;CAUCASIAN ALBANIAN LETTER LAN;Lo;0;L;;;;;N;;;;;
+1053F;CAUCASIAN ALBANIAN LETTER INYA;Lo;0;L;;;;;N;;;;;
+10540;CAUCASIAN ALBANIAN LETTER XEYN;Lo;0;L;;;;;N;;;;;
+10541;CAUCASIAN ALBANIAN LETTER DYAN;Lo;0;L;;;;;N;;;;;
+10542;CAUCASIAN ALBANIAN LETTER CAR;Lo;0;L;;;;;N;;;;;
+10543;CAUCASIAN ALBANIAN LETTER JHOX;Lo;0;L;;;;;N;;;;;
+10544;CAUCASIAN ALBANIAN LETTER KAR;Lo;0;L;;;;;N;;;;;
+10545;CAUCASIAN ALBANIAN LETTER LYIT;Lo;0;L;;;;;N;;;;;
+10546;CAUCASIAN ALBANIAN LETTER HEYT;Lo;0;L;;;;;N;;;;;
+10547;CAUCASIAN ALBANIAN LETTER QAY;Lo;0;L;;;;;N;;;;;
+10548;CAUCASIAN ALBANIAN LETTER AOR;Lo;0;L;;;;;N;;;;;
+10549;CAUCASIAN ALBANIAN LETTER CHOY;Lo;0;L;;;;;N;;;;;
+1054A;CAUCASIAN ALBANIAN LETTER CHI;Lo;0;L;;;;;N;;;;;
+1054B;CAUCASIAN ALBANIAN LETTER CYAY;Lo;0;L;;;;;N;;;;;
+1054C;CAUCASIAN ALBANIAN LETTER MAQ;Lo;0;L;;;;;N;;;;;
+1054D;CAUCASIAN ALBANIAN LETTER QAR;Lo;0;L;;;;;N;;;;;
+1054E;CAUCASIAN ALBANIAN LETTER NOWC;Lo;0;L;;;;;N;;;;;
+1054F;CAUCASIAN ALBANIAN LETTER DZYAY;Lo;0;L;;;;;N;;;;;
+10550;CAUCASIAN ALBANIAN LETTER SHAK;Lo;0;L;;;;;N;;;;;
+10551;CAUCASIAN ALBANIAN LETTER JAYN;Lo;0;L;;;;;N;;;;;
+10552;CAUCASIAN ALBANIAN LETTER ON;Lo;0;L;;;;;N;;;;;
+10553;CAUCASIAN ALBANIAN LETTER TYAY;Lo;0;L;;;;;N;;;;;
+10554;CAUCASIAN ALBANIAN LETTER FAM;Lo;0;L;;;;;N;;;;;
+10555;CAUCASIAN ALBANIAN LETTER DZAY;Lo;0;L;;;;;N;;;;;
+10556;CAUCASIAN ALBANIAN LETTER CHAT;Lo;0;L;;;;;N;;;;;
+10557;CAUCASIAN ALBANIAN LETTER PEN;Lo;0;L;;;;;N;;;;;
+10558;CAUCASIAN ALBANIAN LETTER GHEYS;Lo;0;L;;;;;N;;;;;
+10559;CAUCASIAN ALBANIAN LETTER RAT;Lo;0;L;;;;;N;;;;;
+1055A;CAUCASIAN ALBANIAN LETTER SEYK;Lo;0;L;;;;;N;;;;;
+1055B;CAUCASIAN ALBANIAN LETTER VEYZ;Lo;0;L;;;;;N;;;;;
+1055C;CAUCASIAN ALBANIAN LETTER TIWR;Lo;0;L;;;;;N;;;;;
+1055D;CAUCASIAN ALBANIAN LETTER SHOY;Lo;0;L;;;;;N;;;;;
+1055E;CAUCASIAN ALBANIAN LETTER IWN;Lo;0;L;;;;;N;;;;;
+1055F;CAUCASIAN ALBANIAN LETTER CYAW;Lo;0;L;;;;;N;;;;;
+10560;CAUCASIAN ALBANIAN LETTER CAYN;Lo;0;L;;;;;N;;;;;
+10561;CAUCASIAN ALBANIAN LETTER YAYD;Lo;0;L;;;;;N;;;;;
+10562;CAUCASIAN ALBANIAN LETTER PIWR;Lo;0;L;;;;;N;;;;;
+10563;CAUCASIAN ALBANIAN LETTER KIW;Lo;0;L;;;;;N;;;;;
+1056F;CAUCASIAN ALBANIAN CITATION MARK;Po;0;L;;;;;N;;;;;
+10600;LINEAR A SIGN AB001;Lo;0;L;;;;;N;;;;;
+10601;LINEAR A SIGN AB002;Lo;0;L;;;;;N;;;;;
+10602;LINEAR A SIGN AB003;Lo;0;L;;;;;N;;;;;
+10603;LINEAR A SIGN AB004;Lo;0;L;;;;;N;;;;;
+10604;LINEAR A SIGN AB005;Lo;0;L;;;;;N;;;;;
+10605;LINEAR A SIGN AB006;Lo;0;L;;;;;N;;;;;
+10606;LINEAR A SIGN AB007;Lo;0;L;;;;;N;;;;;
+10607;LINEAR A SIGN AB008;Lo;0;L;;;;;N;;;;;
+10608;LINEAR A SIGN AB009;Lo;0;L;;;;;N;;;;;
+10609;LINEAR A SIGN AB010;Lo;0;L;;;;;N;;;;;
+1060A;LINEAR A SIGN AB011;Lo;0;L;;;;;N;;;;;
+1060B;LINEAR A SIGN AB013;Lo;0;L;;;;;N;;;;;
+1060C;LINEAR A SIGN AB016;Lo;0;L;;;;;N;;;;;
+1060D;LINEAR A SIGN AB017;Lo;0;L;;;;;N;;;;;
+1060E;LINEAR A SIGN AB020;Lo;0;L;;;;;N;;;;;
+1060F;LINEAR A SIGN AB021;Lo;0;L;;;;;N;;;;;
+10610;LINEAR A SIGN AB021F;Lo;0;L;;;;;N;;;;;
+10611;LINEAR A SIGN AB021M;Lo;0;L;;;;;N;;;;;
+10612;LINEAR A SIGN AB022;Lo;0;L;;;;;N;;;;;
+10613;LINEAR A SIGN AB022F;Lo;0;L;;;;;N;;;;;
+10614;LINEAR A SIGN AB022M;Lo;0;L;;;;;N;;;;;
+10615;LINEAR A SIGN AB023;Lo;0;L;;;;;N;;;;;
+10616;LINEAR A SIGN AB023M;Lo;0;L;;;;;N;;;;;
+10617;LINEAR A SIGN AB024;Lo;0;L;;;;;N;;;;;
+10618;LINEAR A SIGN AB026;Lo;0;L;;;;;N;;;;;
+10619;LINEAR A SIGN AB027;Lo;0;L;;;;;N;;;;;
+1061A;LINEAR A SIGN AB028;Lo;0;L;;;;;N;;;;;
+1061B;LINEAR A SIGN A028B;Lo;0;L;;;;;N;;;;;
+1061C;LINEAR A SIGN AB029;Lo;0;L;;;;;N;;;;;
+1061D;LINEAR A SIGN AB030;Lo;0;L;;;;;N;;;;;
+1061E;LINEAR A SIGN AB031;Lo;0;L;;;;;N;;;;;
+1061F;LINEAR A SIGN AB034;Lo;0;L;;;;;N;;;;;
+10620;LINEAR A SIGN AB037;Lo;0;L;;;;;N;;;;;
+10621;LINEAR A SIGN AB038;Lo;0;L;;;;;N;;;;;
+10622;LINEAR A SIGN AB039;Lo;0;L;;;;;N;;;;;
+10623;LINEAR A SIGN AB040;Lo;0;L;;;;;N;;;;;
+10624;LINEAR A SIGN AB041;Lo;0;L;;;;;N;;;;;
+10625;LINEAR A SIGN AB044;Lo;0;L;;;;;N;;;;;
+10626;LINEAR A SIGN AB045;Lo;0;L;;;;;N;;;;;
+10627;LINEAR A SIGN AB046;Lo;0;L;;;;;N;;;;;
+10628;LINEAR A SIGN AB047;Lo;0;L;;;;;N;;;;;
+10629;LINEAR A SIGN AB048;Lo;0;L;;;;;N;;;;;
+1062A;LINEAR A SIGN AB049;Lo;0;L;;;;;N;;;;;
+1062B;LINEAR A SIGN AB050;Lo;0;L;;;;;N;;;;;
+1062C;LINEAR A SIGN AB051;Lo;0;L;;;;;N;;;;;
+1062D;LINEAR A SIGN AB053;Lo;0;L;;;;;N;;;;;
+1062E;LINEAR A SIGN AB054;Lo;0;L;;;;;N;;;;;
+1062F;LINEAR A SIGN AB055;Lo;0;L;;;;;N;;;;;
+10630;LINEAR A SIGN AB056;Lo;0;L;;;;;N;;;;;
+10631;LINEAR A SIGN AB057;Lo;0;L;;;;;N;;;;;
+10632;LINEAR A SIGN AB058;Lo;0;L;;;;;N;;;;;
+10633;LINEAR A SIGN AB059;Lo;0;L;;;;;N;;;;;
+10634;LINEAR A SIGN AB060;Lo;0;L;;;;;N;;;;;
+10635;LINEAR A SIGN AB061;Lo;0;L;;;;;N;;;;;
+10636;LINEAR A SIGN AB065;Lo;0;L;;;;;N;;;;;
+10637;LINEAR A SIGN AB066;Lo;0;L;;;;;N;;;;;
+10638;LINEAR A SIGN AB067;Lo;0;L;;;;;N;;;;;
+10639;LINEAR A SIGN AB069;Lo;0;L;;;;;N;;;;;
+1063A;LINEAR A SIGN AB070;Lo;0;L;;;;;N;;;;;
+1063B;LINEAR A SIGN AB073;Lo;0;L;;;;;N;;;;;
+1063C;LINEAR A SIGN AB074;Lo;0;L;;;;;N;;;;;
+1063D;LINEAR A SIGN AB076;Lo;0;L;;;;;N;;;;;
+1063E;LINEAR A SIGN AB077;Lo;0;L;;;;;N;;;;;
+1063F;LINEAR A SIGN AB078;Lo;0;L;;;;;N;;;;;
+10640;LINEAR A SIGN AB079;Lo;0;L;;;;;N;;;;;
+10641;LINEAR A SIGN AB080;Lo;0;L;;;;;N;;;;;
+10642;LINEAR A SIGN AB081;Lo;0;L;;;;;N;;;;;
+10643;LINEAR A SIGN AB082;Lo;0;L;;;;;N;;;;;
+10644;LINEAR A SIGN AB085;Lo;0;L;;;;;N;;;;;
+10645;LINEAR A SIGN AB086;Lo;0;L;;;;;N;;;;;
+10646;LINEAR A SIGN AB087;Lo;0;L;;;;;N;;;;;
+10647;LINEAR A SIGN A100-102;Lo;0;L;;;;;N;;;;;
+10648;LINEAR A SIGN AB118;Lo;0;L;;;;;N;;;;;
+10649;LINEAR A SIGN AB120;Lo;0;L;;;;;N;;;;;
+1064A;LINEAR A SIGN A120B;Lo;0;L;;;;;N;;;;;
+1064B;LINEAR A SIGN AB122;Lo;0;L;;;;;N;;;;;
+1064C;LINEAR A SIGN AB123;Lo;0;L;;;;;N;;;;;
+1064D;LINEAR A SIGN AB131A;Lo;0;L;;;;;N;;;;;
+1064E;LINEAR A SIGN AB131B;Lo;0;L;;;;;N;;;;;
+1064F;LINEAR A SIGN A131C;Lo;0;L;;;;;N;;;;;
+10650;LINEAR A SIGN AB164;Lo;0;L;;;;;N;;;;;
+10651;LINEAR A SIGN AB171;Lo;0;L;;;;;N;;;;;
+10652;LINEAR A SIGN AB180;Lo;0;L;;;;;N;;;;;
+10653;LINEAR A SIGN AB188;Lo;0;L;;;;;N;;;;;
+10654;LINEAR A SIGN AB191;Lo;0;L;;;;;N;;;;;
+10655;LINEAR A SIGN A301;Lo;0;L;;;;;N;;;;;
+10656;LINEAR A SIGN A302;Lo;0;L;;;;;N;;;;;
+10657;LINEAR A SIGN A303;Lo;0;L;;;;;N;;;;;
+10658;LINEAR A SIGN A304;Lo;0;L;;;;;N;;;;;
+10659;LINEAR A SIGN A305;Lo;0;L;;;;;N;;;;;
+1065A;LINEAR A SIGN A306;Lo;0;L;;;;;N;;;;;
+1065B;LINEAR A SIGN A307;Lo;0;L;;;;;N;;;;;
+1065C;LINEAR A SIGN A308;Lo;0;L;;;;;N;;;;;
+1065D;LINEAR A SIGN A309A;Lo;0;L;;;;;N;;;;;
+1065E;LINEAR A SIGN A309B;Lo;0;L;;;;;N;;;;;
+1065F;LINEAR A SIGN A309C;Lo;0;L;;;;;N;;;;;
+10660;LINEAR A SIGN A310;Lo;0;L;;;;;N;;;;;
+10661;LINEAR A SIGN A311;Lo;0;L;;;;;N;;;;;
+10662;LINEAR A SIGN A312;Lo;0;L;;;;;N;;;;;
+10663;LINEAR A SIGN A313A;Lo;0;L;;;;;N;;;;;
+10664;LINEAR A SIGN A313B;Lo;0;L;;;;;N;;;;;
+10665;LINEAR A SIGN A313C;Lo;0;L;;;;;N;;;;;
+10666;LINEAR A SIGN A314;Lo;0;L;;;;;N;;;;;
+10667;LINEAR A SIGN A315;Lo;0;L;;;;;N;;;;;
+10668;LINEAR A SIGN A316;Lo;0;L;;;;;N;;;;;
+10669;LINEAR A SIGN A317;Lo;0;L;;;;;N;;;;;
+1066A;LINEAR A SIGN A318;Lo;0;L;;;;;N;;;;;
+1066B;LINEAR A SIGN A319;Lo;0;L;;;;;N;;;;;
+1066C;LINEAR A SIGN A320;Lo;0;L;;;;;N;;;;;
+1066D;LINEAR A SIGN A321;Lo;0;L;;;;;N;;;;;
+1066E;LINEAR A SIGN A322;Lo;0;L;;;;;N;;;;;
+1066F;LINEAR A SIGN A323;Lo;0;L;;;;;N;;;;;
+10670;LINEAR A SIGN A324;Lo;0;L;;;;;N;;;;;
+10671;LINEAR A SIGN A325;Lo;0;L;;;;;N;;;;;
+10672;LINEAR A SIGN A326;Lo;0;L;;;;;N;;;;;
+10673;LINEAR A SIGN A327;Lo;0;L;;;;;N;;;;;
+10674;LINEAR A SIGN A328;Lo;0;L;;;;;N;;;;;
+10675;LINEAR A SIGN A329;Lo;0;L;;;;;N;;;;;
+10676;LINEAR A SIGN A330;Lo;0;L;;;;;N;;;;;
+10677;LINEAR A SIGN A331;Lo;0;L;;;;;N;;;;;
+10678;LINEAR A SIGN A332;Lo;0;L;;;;;N;;;;;
+10679;LINEAR A SIGN A333;Lo;0;L;;;;;N;;;;;
+1067A;LINEAR A SIGN A334;Lo;0;L;;;;;N;;;;;
+1067B;LINEAR A SIGN A335;Lo;0;L;;;;;N;;;;;
+1067C;LINEAR A SIGN A336;Lo;0;L;;;;;N;;;;;
+1067D;LINEAR A SIGN A337;Lo;0;L;;;;;N;;;;;
+1067E;LINEAR A SIGN A338;Lo;0;L;;;;;N;;;;;
+1067F;LINEAR A SIGN A339;Lo;0;L;;;;;N;;;;;
+10680;LINEAR A SIGN A340;Lo;0;L;;;;;N;;;;;
+10681;LINEAR A SIGN A341;Lo;0;L;;;;;N;;;;;
+10682;LINEAR A SIGN A342;Lo;0;L;;;;;N;;;;;
+10683;LINEAR A SIGN A343;Lo;0;L;;;;;N;;;;;
+10684;LINEAR A SIGN A344;Lo;0;L;;;;;N;;;;;
+10685;LINEAR A SIGN A345;Lo;0;L;;;;;N;;;;;
+10686;LINEAR A SIGN A346;Lo;0;L;;;;;N;;;;;
+10687;LINEAR A SIGN A347;Lo;0;L;;;;;N;;;;;
+10688;LINEAR A SIGN A348;Lo;0;L;;;;;N;;;;;
+10689;LINEAR A SIGN A349;Lo;0;L;;;;;N;;;;;
+1068A;LINEAR A SIGN A350;Lo;0;L;;;;;N;;;;;
+1068B;LINEAR A SIGN A351;Lo;0;L;;;;;N;;;;;
+1068C;LINEAR A SIGN A352;Lo;0;L;;;;;N;;;;;
+1068D;LINEAR A SIGN A353;Lo;0;L;;;;;N;;;;;
+1068E;LINEAR A SIGN A354;Lo;0;L;;;;;N;;;;;
+1068F;LINEAR A SIGN A355;Lo;0;L;;;;;N;;;;;
+10690;LINEAR A SIGN A356;Lo;0;L;;;;;N;;;;;
+10691;LINEAR A SIGN A357;Lo;0;L;;;;;N;;;;;
+10692;LINEAR A SIGN A358;Lo;0;L;;;;;N;;;;;
+10693;LINEAR A SIGN A359;Lo;0;L;;;;;N;;;;;
+10694;LINEAR A SIGN A360;Lo;0;L;;;;;N;;;;;
+10695;LINEAR A SIGN A361;Lo;0;L;;;;;N;;;;;
+10696;LINEAR A SIGN A362;Lo;0;L;;;;;N;;;;;
+10697;LINEAR A SIGN A363;Lo;0;L;;;;;N;;;;;
+10698;LINEAR A SIGN A364;Lo;0;L;;;;;N;;;;;
+10699;LINEAR A SIGN A365;Lo;0;L;;;;;N;;;;;
+1069A;LINEAR A SIGN A366;Lo;0;L;;;;;N;;;;;
+1069B;LINEAR A SIGN A367;Lo;0;L;;;;;N;;;;;
+1069C;LINEAR A SIGN A368;Lo;0;L;;;;;N;;;;;
+1069D;LINEAR A SIGN A369;Lo;0;L;;;;;N;;;;;
+1069E;LINEAR A SIGN A370;Lo;0;L;;;;;N;;;;;
+1069F;LINEAR A SIGN A371;Lo;0;L;;;;;N;;;;;
+106A0;LINEAR A SIGN A400-VAS;Lo;0;L;;;;;N;;;;;
+106A1;LINEAR A SIGN A401-VAS;Lo;0;L;;;;;N;;;;;
+106A2;LINEAR A SIGN A402-VAS;Lo;0;L;;;;;N;;;;;
+106A3;LINEAR A SIGN A403-VAS;Lo;0;L;;;;;N;;;;;
+106A4;LINEAR A SIGN A404-VAS;Lo;0;L;;;;;N;;;;;
+106A5;LINEAR A SIGN A405-VAS;Lo;0;L;;;;;N;;;;;
+106A6;LINEAR A SIGN A406-VAS;Lo;0;L;;;;;N;;;;;
+106A7;LINEAR A SIGN A407-VAS;Lo;0;L;;;;;N;;;;;
+106A8;LINEAR A SIGN A408-VAS;Lo;0;L;;;;;N;;;;;
+106A9;LINEAR A SIGN A409-VAS;Lo;0;L;;;;;N;;;;;
+106AA;LINEAR A SIGN A410-VAS;Lo;0;L;;;;;N;;;;;
+106AB;LINEAR A SIGN A411-VAS;Lo;0;L;;;;;N;;;;;
+106AC;LINEAR A SIGN A412-VAS;Lo;0;L;;;;;N;;;;;
+106AD;LINEAR A SIGN A413-VAS;Lo;0;L;;;;;N;;;;;
+106AE;LINEAR A SIGN A414-VAS;Lo;0;L;;;;;N;;;;;
+106AF;LINEAR A SIGN A415-VAS;Lo;0;L;;;;;N;;;;;
+106B0;LINEAR A SIGN A416-VAS;Lo;0;L;;;;;N;;;;;
+106B1;LINEAR A SIGN A417-VAS;Lo;0;L;;;;;N;;;;;
+106B2;LINEAR A SIGN A418-VAS;Lo;0;L;;;;;N;;;;;
+106B3;LINEAR A SIGN A501;Lo;0;L;;;;;N;;;;;
+106B4;LINEAR A SIGN A502;Lo;0;L;;;;;N;;;;;
+106B5;LINEAR A SIGN A503;Lo;0;L;;;;;N;;;;;
+106B6;LINEAR A SIGN A504;Lo;0;L;;;;;N;;;;;
+106B7;LINEAR A SIGN A505;Lo;0;L;;;;;N;;;;;
+106B8;LINEAR A SIGN A506;Lo;0;L;;;;;N;;;;;
+106B9;LINEAR A SIGN A508;Lo;0;L;;;;;N;;;;;
+106BA;LINEAR A SIGN A509;Lo;0;L;;;;;N;;;;;
+106BB;LINEAR A SIGN A510;Lo;0;L;;;;;N;;;;;
+106BC;LINEAR A SIGN A511;Lo;0;L;;;;;N;;;;;
+106BD;LINEAR A SIGN A512;Lo;0;L;;;;;N;;;;;
+106BE;LINEAR A SIGN A513;Lo;0;L;;;;;N;;;;;
+106BF;LINEAR A SIGN A515;Lo;0;L;;;;;N;;;;;
+106C0;LINEAR A SIGN A516;Lo;0;L;;;;;N;;;;;
+106C1;LINEAR A SIGN A520;Lo;0;L;;;;;N;;;;;
+106C2;LINEAR A SIGN A521;Lo;0;L;;;;;N;;;;;
+106C3;LINEAR A SIGN A523;Lo;0;L;;;;;N;;;;;
+106C4;LINEAR A SIGN A524;Lo;0;L;;;;;N;;;;;
+106C5;LINEAR A SIGN A525;Lo;0;L;;;;;N;;;;;
+106C6;LINEAR A SIGN A526;Lo;0;L;;;;;N;;;;;
+106C7;LINEAR A SIGN A527;Lo;0;L;;;;;N;;;;;
+106C8;LINEAR A SIGN A528;Lo;0;L;;;;;N;;;;;
+106C9;LINEAR A SIGN A529;Lo;0;L;;;;;N;;;;;
+106CA;LINEAR A SIGN A530;Lo;0;L;;;;;N;;;;;
+106CB;LINEAR A SIGN A531;Lo;0;L;;;;;N;;;;;
+106CC;LINEAR A SIGN A532;Lo;0;L;;;;;N;;;;;
+106CD;LINEAR A SIGN A534;Lo;0;L;;;;;N;;;;;
+106CE;LINEAR A SIGN A535;Lo;0;L;;;;;N;;;;;
+106CF;LINEAR A SIGN A536;Lo;0;L;;;;;N;;;;;
+106D0;LINEAR A SIGN A537;Lo;0;L;;;;;N;;;;;
+106D1;LINEAR A SIGN A538;Lo;0;L;;;;;N;;;;;
+106D2;LINEAR A SIGN A539;Lo;0;L;;;;;N;;;;;
+106D3;LINEAR A SIGN A540;Lo;0;L;;;;;N;;;;;
+106D4;LINEAR A SIGN A541;Lo;0;L;;;;;N;;;;;
+106D5;LINEAR A SIGN A542;Lo;0;L;;;;;N;;;;;
+106D6;LINEAR A SIGN A545;Lo;0;L;;;;;N;;;;;
+106D7;LINEAR A SIGN A547;Lo;0;L;;;;;N;;;;;
+106D8;LINEAR A SIGN A548;Lo;0;L;;;;;N;;;;;
+106D9;LINEAR A SIGN A549;Lo;0;L;;;;;N;;;;;
+106DA;LINEAR A SIGN A550;Lo;0;L;;;;;N;;;;;
+106DB;LINEAR A SIGN A551;Lo;0;L;;;;;N;;;;;
+106DC;LINEAR A SIGN A552;Lo;0;L;;;;;N;;;;;
+106DD;LINEAR A SIGN A553;Lo;0;L;;;;;N;;;;;
+106DE;LINEAR A SIGN A554;Lo;0;L;;;;;N;;;;;
+106DF;LINEAR A SIGN A555;Lo;0;L;;;;;N;;;;;
+106E0;LINEAR A SIGN A556;Lo;0;L;;;;;N;;;;;
+106E1;LINEAR A SIGN A557;Lo;0;L;;;;;N;;;;;
+106E2;LINEAR A SIGN A559;Lo;0;L;;;;;N;;;;;
+106E3;LINEAR A SIGN A563;Lo;0;L;;;;;N;;;;;
+106E4;LINEAR A SIGN A564;Lo;0;L;;;;;N;;;;;
+106E5;LINEAR A SIGN A565;Lo;0;L;;;;;N;;;;;
+106E6;LINEAR A SIGN A566;Lo;0;L;;;;;N;;;;;
+106E7;LINEAR A SIGN A568;Lo;0;L;;;;;N;;;;;
+106E8;LINEAR A SIGN A569;Lo;0;L;;;;;N;;;;;
+106E9;LINEAR A SIGN A570;Lo;0;L;;;;;N;;;;;
+106EA;LINEAR A SIGN A571;Lo;0;L;;;;;N;;;;;
+106EB;LINEAR A SIGN A572;Lo;0;L;;;;;N;;;;;
+106EC;LINEAR A SIGN A573;Lo;0;L;;;;;N;;;;;
+106ED;LINEAR A SIGN A574;Lo;0;L;;;;;N;;;;;
+106EE;LINEAR A SIGN A575;Lo;0;L;;;;;N;;;;;
+106EF;LINEAR A SIGN A576;Lo;0;L;;;;;N;;;;;
+106F0;LINEAR A SIGN A577;Lo;0;L;;;;;N;;;;;
+106F1;LINEAR A SIGN A578;Lo;0;L;;;;;N;;;;;
+106F2;LINEAR A SIGN A579;Lo;0;L;;;;;N;;;;;
+106F3;LINEAR A SIGN A580;Lo;0;L;;;;;N;;;;;
+106F4;LINEAR A SIGN A581;Lo;0;L;;;;;N;;;;;
+106F5;LINEAR A SIGN A582;Lo;0;L;;;;;N;;;;;
+106F6;LINEAR A SIGN A583;Lo;0;L;;;;;N;;;;;
+106F7;LINEAR A SIGN A584;Lo;0;L;;;;;N;;;;;
+106F8;LINEAR A SIGN A585;Lo;0;L;;;;;N;;;;;
+106F9;LINEAR A SIGN A586;Lo;0;L;;;;;N;;;;;
+106FA;LINEAR A SIGN A587;Lo;0;L;;;;;N;;;;;
+106FB;LINEAR A SIGN A588;Lo;0;L;;;;;N;;;;;
+106FC;LINEAR A SIGN A589;Lo;0;L;;;;;N;;;;;
+106FD;LINEAR A SIGN A591;Lo;0;L;;;;;N;;;;;
+106FE;LINEAR A SIGN A592;Lo;0;L;;;;;N;;;;;
+106FF;LINEAR A SIGN A594;Lo;0;L;;;;;N;;;;;
+10700;LINEAR A SIGN A595;Lo;0;L;;;;;N;;;;;
+10701;LINEAR A SIGN A596;Lo;0;L;;;;;N;;;;;
+10702;LINEAR A SIGN A598;Lo;0;L;;;;;N;;;;;
+10703;LINEAR A SIGN A600;Lo;0;L;;;;;N;;;;;
+10704;LINEAR A SIGN A601;Lo;0;L;;;;;N;;;;;
+10705;LINEAR A SIGN A602;Lo;0;L;;;;;N;;;;;
+10706;LINEAR A SIGN A603;Lo;0;L;;;;;N;;;;;
+10707;LINEAR A SIGN A604;Lo;0;L;;;;;N;;;;;
+10708;LINEAR A SIGN A606;Lo;0;L;;;;;N;;;;;
+10709;LINEAR A SIGN A608;Lo;0;L;;;;;N;;;;;
+1070A;LINEAR A SIGN A609;Lo;0;L;;;;;N;;;;;
+1070B;LINEAR A SIGN A610;Lo;0;L;;;;;N;;;;;
+1070C;LINEAR A SIGN A611;Lo;0;L;;;;;N;;;;;
+1070D;LINEAR A SIGN A612;Lo;0;L;;;;;N;;;;;
+1070E;LINEAR A SIGN A613;Lo;0;L;;;;;N;;;;;
+1070F;LINEAR A SIGN A614;Lo;0;L;;;;;N;;;;;
+10710;LINEAR A SIGN A615;Lo;0;L;;;;;N;;;;;
+10711;LINEAR A SIGN A616;Lo;0;L;;;;;N;;;;;
+10712;LINEAR A SIGN A617;Lo;0;L;;;;;N;;;;;
+10713;LINEAR A SIGN A618;Lo;0;L;;;;;N;;;;;
+10714;LINEAR A SIGN A619;Lo;0;L;;;;;N;;;;;
+10715;LINEAR A SIGN A620;Lo;0;L;;;;;N;;;;;
+10716;LINEAR A SIGN A621;Lo;0;L;;;;;N;;;;;
+10717;LINEAR A SIGN A622;Lo;0;L;;;;;N;;;;;
+10718;LINEAR A SIGN A623;Lo;0;L;;;;;N;;;;;
+10719;LINEAR A SIGN A624;Lo;0;L;;;;;N;;;;;
+1071A;LINEAR A SIGN A626;Lo;0;L;;;;;N;;;;;
+1071B;LINEAR A SIGN A627;Lo;0;L;;;;;N;;;;;
+1071C;LINEAR A SIGN A628;Lo;0;L;;;;;N;;;;;
+1071D;LINEAR A SIGN A629;Lo;0;L;;;;;N;;;;;
+1071E;LINEAR A SIGN A634;Lo;0;L;;;;;N;;;;;
+1071F;LINEAR A SIGN A637;Lo;0;L;;;;;N;;;;;
+10720;LINEAR A SIGN A638;Lo;0;L;;;;;N;;;;;
+10721;LINEAR A SIGN A640;Lo;0;L;;;;;N;;;;;
+10722;LINEAR A SIGN A642;Lo;0;L;;;;;N;;;;;
+10723;LINEAR A SIGN A643;Lo;0;L;;;;;N;;;;;
+10724;LINEAR A SIGN A644;Lo;0;L;;;;;N;;;;;
+10725;LINEAR A SIGN A645;Lo;0;L;;;;;N;;;;;
+10726;LINEAR A SIGN A646;Lo;0;L;;;;;N;;;;;
+10727;LINEAR A SIGN A648;Lo;0;L;;;;;N;;;;;
+10728;LINEAR A SIGN A649;Lo;0;L;;;;;N;;;;;
+10729;LINEAR A SIGN A651;Lo;0;L;;;;;N;;;;;
+1072A;LINEAR A SIGN A652;Lo;0;L;;;;;N;;;;;
+1072B;LINEAR A SIGN A653;Lo;0;L;;;;;N;;;;;
+1072C;LINEAR A SIGN A654;Lo;0;L;;;;;N;;;;;
+1072D;LINEAR A SIGN A655;Lo;0;L;;;;;N;;;;;
+1072E;LINEAR A SIGN A656;Lo;0;L;;;;;N;;;;;
+1072F;LINEAR A SIGN A657;Lo;0;L;;;;;N;;;;;
+10730;LINEAR A SIGN A658;Lo;0;L;;;;;N;;;;;
+10731;LINEAR A SIGN A659;Lo;0;L;;;;;N;;;;;
+10732;LINEAR A SIGN A660;Lo;0;L;;;;;N;;;;;
+10733;LINEAR A SIGN A661;Lo;0;L;;;;;N;;;;;
+10734;LINEAR A SIGN A662;Lo;0;L;;;;;N;;;;;
+10735;LINEAR A SIGN A663;Lo;0;L;;;;;N;;;;;
+10736;LINEAR A SIGN A664;Lo;0;L;;;;;N;;;;;
+10740;LINEAR A SIGN A701 A;Lo;0;L;;;;;N;;;;;
+10741;LINEAR A SIGN A702 B;Lo;0;L;;;;;N;;;;;
+10742;LINEAR A SIGN A703 D;Lo;0;L;;;;;N;;;;;
+10743;LINEAR A SIGN A704 E;Lo;0;L;;;;;N;;;;;
+10744;LINEAR A SIGN A705 F;Lo;0;L;;;;;N;;;;;
+10745;LINEAR A SIGN A706 H;Lo;0;L;;;;;N;;;;;
+10746;LINEAR A SIGN A707 J;Lo;0;L;;;;;N;;;;;
+10747;LINEAR A SIGN A708 K;Lo;0;L;;;;;N;;;;;
+10748;LINEAR A SIGN A709 L;Lo;0;L;;;;;N;;;;;
+10749;LINEAR A SIGN A709-2 L2;Lo;0;L;;;;;N;;;;;
+1074A;LINEAR A SIGN A709-3 L3;Lo;0;L;;;;;N;;;;;
+1074B;LINEAR A SIGN A709-4 L4;Lo;0;L;;;;;N;;;;;
+1074C;LINEAR A SIGN A709-6 L6;Lo;0;L;;;;;N;;;;;
+1074D;LINEAR A SIGN A710 W;Lo;0;L;;;;;N;;;;;
+1074E;LINEAR A SIGN A711 X;Lo;0;L;;;;;N;;;;;
+1074F;LINEAR A SIGN A712 Y;Lo;0;L;;;;;N;;;;;
+10750;LINEAR A SIGN A713 OMEGA;Lo;0;L;;;;;N;;;;;
+10751;LINEAR A SIGN A714 ABB;Lo;0;L;;;;;N;;;;;
+10752;LINEAR A SIGN A715 BB;Lo;0;L;;;;;N;;;;;
+10753;LINEAR A SIGN A717 DD;Lo;0;L;;;;;N;;;;;
+10754;LINEAR A SIGN A726 EYYY;Lo;0;L;;;;;N;;;;;
+10755;LINEAR A SIGN A732 JE;Lo;0;L;;;;;N;;;;;
+10760;LINEAR A SIGN A800;Lo;0;L;;;;;N;;;;;
+10761;LINEAR A SIGN A801;Lo;0;L;;;;;N;;;;;
+10762;LINEAR A SIGN A802;Lo;0;L;;;;;N;;;;;
+10763;LINEAR A SIGN A803;Lo;0;L;;;;;N;;;;;
+10764;LINEAR A SIGN A804;Lo;0;L;;;;;N;;;;;
+10765;LINEAR A SIGN A805;Lo;0;L;;;;;N;;;;;
+10766;LINEAR A SIGN A806;Lo;0;L;;;;;N;;;;;
+10767;LINEAR A SIGN A807;Lo;0;L;;;;;N;;;;;
+10800;CYPRIOT SYLLABLE A;Lo;0;R;;;;;N;;;;;
+10801;CYPRIOT SYLLABLE E;Lo;0;R;;;;;N;;;;;
+10802;CYPRIOT SYLLABLE I;Lo;0;R;;;;;N;;;;;
+10803;CYPRIOT SYLLABLE O;Lo;0;R;;;;;N;;;;;
+10804;CYPRIOT SYLLABLE U;Lo;0;R;;;;;N;;;;;
+10805;CYPRIOT SYLLABLE JA;Lo;0;R;;;;;N;;;;;
+10808;CYPRIOT SYLLABLE JO;Lo;0;R;;;;;N;;;;;
+1080A;CYPRIOT SYLLABLE KA;Lo;0;R;;;;;N;;;;;
+1080B;CYPRIOT SYLLABLE KE;Lo;0;R;;;;;N;;;;;
+1080C;CYPRIOT SYLLABLE KI;Lo;0;R;;;;;N;;;;;
+1080D;CYPRIOT SYLLABLE KO;Lo;0;R;;;;;N;;;;;
+1080E;CYPRIOT SYLLABLE KU;Lo;0;R;;;;;N;;;;;
+1080F;CYPRIOT SYLLABLE LA;Lo;0;R;;;;;N;;;;;
+10810;CYPRIOT SYLLABLE LE;Lo;0;R;;;;;N;;;;;
+10811;CYPRIOT SYLLABLE LI;Lo;0;R;;;;;N;;;;;
+10812;CYPRIOT SYLLABLE LO;Lo;0;R;;;;;N;;;;;
+10813;CYPRIOT SYLLABLE LU;Lo;0;R;;;;;N;;;;;
+10814;CYPRIOT SYLLABLE MA;Lo;0;R;;;;;N;;;;;
+10815;CYPRIOT SYLLABLE ME;Lo;0;R;;;;;N;;;;;
+10816;CYPRIOT SYLLABLE MI;Lo;0;R;;;;;N;;;;;
+10817;CYPRIOT SYLLABLE MO;Lo;0;R;;;;;N;;;;;
+10818;CYPRIOT SYLLABLE MU;Lo;0;R;;;;;N;;;;;
+10819;CYPRIOT SYLLABLE NA;Lo;0;R;;;;;N;;;;;
+1081A;CYPRIOT SYLLABLE NE;Lo;0;R;;;;;N;;;;;
+1081B;CYPRIOT SYLLABLE NI;Lo;0;R;;;;;N;;;;;
+1081C;CYPRIOT SYLLABLE NO;Lo;0;R;;;;;N;;;;;
+1081D;CYPRIOT SYLLABLE NU;Lo;0;R;;;;;N;;;;;
+1081E;CYPRIOT SYLLABLE PA;Lo;0;R;;;;;N;;;;;
+1081F;CYPRIOT SYLLABLE PE;Lo;0;R;;;;;N;;;;;
+10820;CYPRIOT SYLLABLE PI;Lo;0;R;;;;;N;;;;;
+10821;CYPRIOT SYLLABLE PO;Lo;0;R;;;;;N;;;;;
+10822;CYPRIOT SYLLABLE PU;Lo;0;R;;;;;N;;;;;
+10823;CYPRIOT SYLLABLE RA;Lo;0;R;;;;;N;;;;;
+10824;CYPRIOT SYLLABLE RE;Lo;0;R;;;;;N;;;;;
+10825;CYPRIOT SYLLABLE RI;Lo;0;R;;;;;N;;;;;
+10826;CYPRIOT SYLLABLE RO;Lo;0;R;;;;;N;;;;;
+10827;CYPRIOT SYLLABLE RU;Lo;0;R;;;;;N;;;;;
+10828;CYPRIOT SYLLABLE SA;Lo;0;R;;;;;N;;;;;
+10829;CYPRIOT SYLLABLE SE;Lo;0;R;;;;;N;;;;;
+1082A;CYPRIOT SYLLABLE SI;Lo;0;R;;;;;N;;;;;
+1082B;CYPRIOT SYLLABLE SO;Lo;0;R;;;;;N;;;;;
+1082C;CYPRIOT SYLLABLE SU;Lo;0;R;;;;;N;;;;;
+1082D;CYPRIOT SYLLABLE TA;Lo;0;R;;;;;N;;;;;
+1082E;CYPRIOT SYLLABLE TE;Lo;0;R;;;;;N;;;;;
+1082F;CYPRIOT SYLLABLE TI;Lo;0;R;;;;;N;;;;;
+10830;CYPRIOT SYLLABLE TO;Lo;0;R;;;;;N;;;;;
+10831;CYPRIOT SYLLABLE TU;Lo;0;R;;;;;N;;;;;
+10832;CYPRIOT SYLLABLE WA;Lo;0;R;;;;;N;;;;;
+10833;CYPRIOT SYLLABLE WE;Lo;0;R;;;;;N;;;;;
+10834;CYPRIOT SYLLABLE WI;Lo;0;R;;;;;N;;;;;
+10835;CYPRIOT SYLLABLE WO;Lo;0;R;;;;;N;;;;;
+10837;CYPRIOT SYLLABLE XA;Lo;0;R;;;;;N;;;;;
+10838;CYPRIOT SYLLABLE XE;Lo;0;R;;;;;N;;;;;
+1083C;CYPRIOT SYLLABLE ZA;Lo;0;R;;;;;N;;;;;
+1083F;CYPRIOT SYLLABLE ZO;Lo;0;R;;;;;N;;;;;
+10840;IMPERIAL ARAMAIC LETTER ALEPH;Lo;0;R;;;;;N;;;;;
+10841;IMPERIAL ARAMAIC LETTER BETH;Lo;0;R;;;;;N;;;;;
+10842;IMPERIAL ARAMAIC LETTER GIMEL;Lo;0;R;;;;;N;;;;;
+10843;IMPERIAL ARAMAIC LETTER DALETH;Lo;0;R;;;;;N;;;;;
+10844;IMPERIAL ARAMAIC LETTER HE;Lo;0;R;;;;;N;;;;;
+10845;IMPERIAL ARAMAIC LETTER WAW;Lo;0;R;;;;;N;;;;;
+10846;IMPERIAL ARAMAIC LETTER ZAYIN;Lo;0;R;;;;;N;;;;;
+10847;IMPERIAL ARAMAIC LETTER HETH;Lo;0;R;;;;;N;;;;;
+10848;IMPERIAL ARAMAIC LETTER TETH;Lo;0;R;;;;;N;;;;;
+10849;IMPERIAL ARAMAIC LETTER YODH;Lo;0;R;;;;;N;;;;;
+1084A;IMPERIAL ARAMAIC LETTER KAPH;Lo;0;R;;;;;N;;;;;
+1084B;IMPERIAL ARAMAIC LETTER LAMEDH;Lo;0;R;;;;;N;;;;;
+1084C;IMPERIAL ARAMAIC LETTER MEM;Lo;0;R;;;;;N;;;;;
+1084D;IMPERIAL ARAMAIC LETTER NUN;Lo;0;R;;;;;N;;;;;
+1084E;IMPERIAL ARAMAIC LETTER SAMEKH;Lo;0;R;;;;;N;;;;;
+1084F;IMPERIAL ARAMAIC LETTER AYIN;Lo;0;R;;;;;N;;;;;
+10850;IMPERIAL ARAMAIC LETTER PE;Lo;0;R;;;;;N;;;;;
+10851;IMPERIAL ARAMAIC LETTER SADHE;Lo;0;R;;;;;N;;;;;
+10852;IMPERIAL ARAMAIC LETTER QOPH;Lo;0;R;;;;;N;;;;;
+10853;IMPERIAL ARAMAIC LETTER RESH;Lo;0;R;;;;;N;;;;;
+10854;IMPERIAL ARAMAIC LETTER SHIN;Lo;0;R;;;;;N;;;;;
+10855;IMPERIAL ARAMAIC LETTER TAW;Lo;0;R;;;;;N;;;;;
+10857;IMPERIAL ARAMAIC SECTION SIGN;Po;0;R;;;;;N;;;;;
+10858;IMPERIAL ARAMAIC NUMBER ONE;No;0;R;;;;1;N;;;;;
+10859;IMPERIAL ARAMAIC NUMBER TWO;No;0;R;;;;2;N;;;;;
+1085A;IMPERIAL ARAMAIC NUMBER THREE;No;0;R;;;;3;N;;;;;
+1085B;IMPERIAL ARAMAIC NUMBER TEN;No;0;R;;;;10;N;;;;;
+1085C;IMPERIAL ARAMAIC NUMBER TWENTY;No;0;R;;;;20;N;;;;;
+1085D;IMPERIAL ARAMAIC NUMBER ONE HUNDRED;No;0;R;;;;100;N;;;;;
+1085E;IMPERIAL ARAMAIC NUMBER ONE THOUSAND;No;0;R;;;;1000;N;;;;;
+1085F;IMPERIAL ARAMAIC NUMBER TEN THOUSAND;No;0;R;;;;10000;N;;;;;
+10860;PALMYRENE LETTER ALEPH;Lo;0;R;;;;;N;;;;;
+10861;PALMYRENE LETTER BETH;Lo;0;R;;;;;N;;;;;
+10862;PALMYRENE LETTER GIMEL;Lo;0;R;;;;;N;;;;;
+10863;PALMYRENE LETTER DALETH;Lo;0;R;;;;;N;;;;;
+10864;PALMYRENE LETTER HE;Lo;0;R;;;;;N;;;;;
+10865;PALMYRENE LETTER WAW;Lo;0;R;;;;;N;;;;;
+10866;PALMYRENE LETTER ZAYIN;Lo;0;R;;;;;N;;;;;
+10867;PALMYRENE LETTER HETH;Lo;0;R;;;;;N;;;;;
+10868;PALMYRENE LETTER TETH;Lo;0;R;;;;;N;;;;;
+10869;PALMYRENE LETTER YODH;Lo;0;R;;;;;N;;;;;
+1086A;PALMYRENE LETTER KAPH;Lo;0;R;;;;;N;;;;;
+1086B;PALMYRENE LETTER LAMEDH;Lo;0;R;;;;;N;;;;;
+1086C;PALMYRENE LETTER MEM;Lo;0;R;;;;;N;;;;;
+1086D;PALMYRENE LETTER FINAL NUN;Lo;0;R;;;;;N;;;;;
+1086E;PALMYRENE LETTER NUN;Lo;0;R;;;;;N;;;;;
+1086F;PALMYRENE LETTER SAMEKH;Lo;0;R;;;;;N;;;;;
+10870;PALMYRENE LETTER AYIN;Lo;0;R;;;;;N;;;;;
+10871;PALMYRENE LETTER PE;Lo;0;R;;;;;N;;;;;
+10872;PALMYRENE LETTER SADHE;Lo;0;R;;;;;N;;;;;
+10873;PALMYRENE LETTER QOPH;Lo;0;R;;;;;N;;;;;
+10874;PALMYRENE LETTER RESH;Lo;0;R;;;;;N;;;;;
+10875;PALMYRENE LETTER SHIN;Lo;0;R;;;;;N;;;;;
+10876;PALMYRENE LETTER TAW;Lo;0;R;;;;;N;;;;;
+10877;PALMYRENE LEFT-POINTING FLEURON;So;0;R;;;;;N;;;;;
+10878;PALMYRENE RIGHT-POINTING FLEURON;So;0;R;;;;;N;;;;;
+10879;PALMYRENE NUMBER ONE;No;0;R;;;;1;N;;;;;
+1087A;PALMYRENE NUMBER TWO;No;0;R;;;;2;N;;;;;
+1087B;PALMYRENE NUMBER THREE;No;0;R;;;;3;N;;;;;
+1087C;PALMYRENE NUMBER FOUR;No;0;R;;;;4;N;;;;;
+1087D;PALMYRENE NUMBER FIVE;No;0;R;;;;5;N;;;;;
+1087E;PALMYRENE NUMBER TEN;No;0;R;;;;10;N;;;;;
+1087F;PALMYRENE NUMBER TWENTY;No;0;R;;;;20;N;;;;;
+10880;NABATAEAN LETTER FINAL ALEPH;Lo;0;R;;;;;N;;;;;
+10881;NABATAEAN LETTER ALEPH;Lo;0;R;;;;;N;;;;;
+10882;NABATAEAN LETTER FINAL BETH;Lo;0;R;;;;;N;;;;;
+10883;NABATAEAN LETTER BETH;Lo;0;R;;;;;N;;;;;
+10884;NABATAEAN LETTER GIMEL;Lo;0;R;;;;;N;;;;;
+10885;NABATAEAN LETTER DALETH;Lo;0;R;;;;;N;;;;;
+10886;NABATAEAN LETTER FINAL HE;Lo;0;R;;;;;N;;;;;
+10887;NABATAEAN LETTER HE;Lo;0;R;;;;;N;;;;;
+10888;NABATAEAN LETTER WAW;Lo;0;R;;;;;N;;;;;
+10889;NABATAEAN LETTER ZAYIN;Lo;0;R;;;;;N;;;;;
+1088A;NABATAEAN LETTER HETH;Lo;0;R;;;;;N;;;;;
+1088B;NABATAEAN LETTER TETH;Lo;0;R;;;;;N;;;;;
+1088C;NABATAEAN LETTER FINAL YODH;Lo;0;R;;;;;N;;;;;
+1088D;NABATAEAN LETTER YODH;Lo;0;R;;;;;N;;;;;
+1088E;NABATAEAN LETTER FINAL KAPH;Lo;0;R;;;;;N;;;;;
+1088F;NABATAEAN LETTER KAPH;Lo;0;R;;;;;N;;;;;
+10890;NABATAEAN LETTER FINAL LAMEDH;Lo;0;R;;;;;N;;;;;
+10891;NABATAEAN LETTER LAMEDH;Lo;0;R;;;;;N;;;;;
+10892;NABATAEAN LETTER FINAL MEM;Lo;0;R;;;;;N;;;;;
+10893;NABATAEAN LETTER MEM;Lo;0;R;;;;;N;;;;;
+10894;NABATAEAN LETTER FINAL NUN;Lo;0;R;;;;;N;;;;;
+10895;NABATAEAN LETTER NUN;Lo;0;R;;;;;N;;;;;
+10896;NABATAEAN LETTER SAMEKH;Lo;0;R;;;;;N;;;;;
+10897;NABATAEAN LETTER AYIN;Lo;0;R;;;;;N;;;;;
+10898;NABATAEAN LETTER PE;Lo;0;R;;;;;N;;;;;
+10899;NABATAEAN LETTER SADHE;Lo;0;R;;;;;N;;;;;
+1089A;NABATAEAN LETTER QOPH;Lo;0;R;;;;;N;;;;;
+1089B;NABATAEAN LETTER RESH;Lo;0;R;;;;;N;;;;;
+1089C;NABATAEAN LETTER FINAL SHIN;Lo;0;R;;;;;N;;;;;
+1089D;NABATAEAN LETTER SHIN;Lo;0;R;;;;;N;;;;;
+1089E;NABATAEAN LETTER TAW;Lo;0;R;;;;;N;;;;;
+108A7;NABATAEAN NUMBER ONE;No;0;R;;;;1;N;;;;;
+108A8;NABATAEAN NUMBER TWO;No;0;R;;;;2;N;;;;;
+108A9;NABATAEAN NUMBER THREE;No;0;R;;;;3;N;;;;;
+108AA;NABATAEAN NUMBER FOUR;No;0;R;;;;4;N;;;;;
+108AB;NABATAEAN CRUCIFORM NUMBER FOUR;No;0;R;;;;4;N;;;;;
+108AC;NABATAEAN NUMBER FIVE;No;0;R;;;;5;N;;;;;
+108AD;NABATAEAN NUMBER TEN;No;0;R;;;;10;N;;;;;
+108AE;NABATAEAN NUMBER TWENTY;No;0;R;;;;20;N;;;;;
+108AF;NABATAEAN NUMBER ONE HUNDRED;No;0;R;;;;100;N;;;;;
+108E0;HATRAN LETTER ALEPH;Lo;0;R;;;;;N;;;;;
+108E1;HATRAN LETTER BETH;Lo;0;R;;;;;N;;;;;
+108E2;HATRAN LETTER GIMEL;Lo;0;R;;;;;N;;;;;
+108E3;HATRAN LETTER DALETH-RESH;Lo;0;R;;;;;N;;;;;
+108E4;HATRAN LETTER HE;Lo;0;R;;;;;N;;;;;
+108E5;HATRAN LETTER WAW;Lo;0;R;;;;;N;;;;;
+108E6;HATRAN LETTER ZAYN;Lo;0;R;;;;;N;;;;;
+108E7;HATRAN LETTER HETH;Lo;0;R;;;;;N;;;;;
+108E8;HATRAN LETTER TETH;Lo;0;R;;;;;N;;;;;
+108E9;HATRAN LETTER YODH;Lo;0;R;;;;;N;;;;;
+108EA;HATRAN LETTER KAPH;Lo;0;R;;;;;N;;;;;
+108EB;HATRAN LETTER LAMEDH;Lo;0;R;;;;;N;;;;;
+108EC;HATRAN LETTER MEM;Lo;0;R;;;;;N;;;;;
+108ED;HATRAN LETTER NUN;Lo;0;R;;;;;N;;;;;
+108EE;HATRAN LETTER SAMEKH;Lo;0;R;;;;;N;;;;;
+108EF;HATRAN LETTER AYN;Lo;0;R;;;;;N;;;;;
+108F0;HATRAN LETTER PE;Lo;0;R;;;;;N;;;;;
+108F1;HATRAN LETTER SADHE;Lo;0;R;;;;;N;;;;;
+108F2;HATRAN LETTER QOPH;Lo;0;R;;;;;N;;;;;
+108F4;HATRAN LETTER SHIN;Lo;0;R;;;;;N;;;;;
+108F5;HATRAN LETTER TAW;Lo;0;R;;;;;N;;;;;
+108FB;HATRAN NUMBER ONE;No;0;R;;;;1;N;;;;;
+108FC;HATRAN NUMBER FIVE;No;0;R;;;;5;N;;;;;
+108FD;HATRAN NUMBER TEN;No;0;R;;;;10;N;;;;;
+108FE;HATRAN NUMBER TWENTY;No;0;R;;;;20;N;;;;;
+108FF;HATRAN NUMBER ONE HUNDRED;No;0;R;;;;100;N;;;;;
+10900;PHOENICIAN LETTER ALF;Lo;0;R;;;;;N;;;;;
+10901;PHOENICIAN LETTER BET;Lo;0;R;;;;;N;;;;;
+10902;PHOENICIAN LETTER GAML;Lo;0;R;;;;;N;;;;;
+10903;PHOENICIAN LETTER DELT;Lo;0;R;;;;;N;;;;;
+10904;PHOENICIAN LETTER HE;Lo;0;R;;;;;N;;;;;
+10905;PHOENICIAN LETTER WAU;Lo;0;R;;;;;N;;;;;
+10906;PHOENICIAN LETTER ZAI;Lo;0;R;;;;;N;;;;;
+10907;PHOENICIAN LETTER HET;Lo;0;R;;;;;N;;;;;
+10908;PHOENICIAN LETTER TET;Lo;0;R;;;;;N;;;;;
+10909;PHOENICIAN LETTER YOD;Lo;0;R;;;;;N;;;;;
+1090A;PHOENICIAN LETTER KAF;Lo;0;R;;;;;N;;;;;
+1090B;PHOENICIAN LETTER LAMD;Lo;0;R;;;;;N;;;;;
+1090C;PHOENICIAN LETTER MEM;Lo;0;R;;;;;N;;;;;
+1090D;PHOENICIAN LETTER NUN;Lo;0;R;;;;;N;;;;;
+1090E;PHOENICIAN LETTER SEMK;Lo;0;R;;;;;N;;;;;
+1090F;PHOENICIAN LETTER AIN;Lo;0;R;;;;;N;;;;;
+10910;PHOENICIAN LETTER PE;Lo;0;R;;;;;N;;;;;
+10911;PHOENICIAN LETTER SADE;Lo;0;R;;;;;N;;;;;
+10912;PHOENICIAN LETTER QOF;Lo;0;R;;;;;N;;;;;
+10913;PHOENICIAN LETTER ROSH;Lo;0;R;;;;;N;;;;;
+10914;PHOENICIAN LETTER SHIN;Lo;0;R;;;;;N;;;;;
+10915;PHOENICIAN LETTER TAU;Lo;0;R;;;;;N;;;;;
+10916;PHOENICIAN NUMBER ONE;No;0;R;;;;1;N;;;;;
+10917;PHOENICIAN NUMBER TEN;No;0;R;;;;10;N;;;;;
+10918;PHOENICIAN NUMBER TWENTY;No;0;R;;;;20;N;;;;;
+10919;PHOENICIAN NUMBER ONE HUNDRED;No;0;R;;;;100;N;;;;;
+1091A;PHOENICIAN NUMBER TWO;No;0;R;;;;2;N;;;;;
+1091B;PHOENICIAN NUMBER THREE;No;0;R;;;;3;N;;;;;
+1091F;PHOENICIAN WORD SEPARATOR;Po;0;ON;;;;;N;;;;;
+10920;LYDIAN LETTER A;Lo;0;R;;;;;N;;;;;
+10921;LYDIAN LETTER B;Lo;0;R;;;;;N;;;;;
+10922;LYDIAN LETTER G;Lo;0;R;;;;;N;;;;;
+10923;LYDIAN LETTER D;Lo;0;R;;;;;N;;;;;
+10924;LYDIAN LETTER E;Lo;0;R;;;;;N;;;;;
+10925;LYDIAN LETTER V;Lo;0;R;;;;;N;;;;;
+10926;LYDIAN LETTER I;Lo;0;R;;;;;N;;;;;
+10927;LYDIAN LETTER Y;Lo;0;R;;;;;N;;;;;
+10928;LYDIAN LETTER K;Lo;0;R;;;;;N;;;;;
+10929;LYDIAN LETTER L;Lo;0;R;;;;;N;;;;;
+1092A;LYDIAN LETTER M;Lo;0;R;;;;;N;;;;;
+1092B;LYDIAN LETTER N;Lo;0;R;;;;;N;;;;;
+1092C;LYDIAN LETTER O;Lo;0;R;;;;;N;;;;;
+1092D;LYDIAN LETTER R;Lo;0;R;;;;;N;;;;;
+1092E;LYDIAN LETTER SS;Lo;0;R;;;;;N;;;;;
+1092F;LYDIAN LETTER T;Lo;0;R;;;;;N;;;;;
+10930;LYDIAN LETTER U;Lo;0;R;;;;;N;;;;;
+10931;LYDIAN LETTER F;Lo;0;R;;;;;N;;;;;
+10932;LYDIAN LETTER Q;Lo;0;R;;;;;N;;;;;
+10933;LYDIAN LETTER S;Lo;0;R;;;;;N;;;;;
+10934;LYDIAN LETTER TT;Lo;0;R;;;;;N;;;;;
+10935;LYDIAN LETTER AN;Lo;0;R;;;;;N;;;;;
+10936;LYDIAN LETTER EN;Lo;0;R;;;;;N;;;;;
+10937;LYDIAN LETTER LY;Lo;0;R;;;;;N;;;;;
+10938;LYDIAN LETTER NN;Lo;0;R;;;;;N;;;;;
+10939;LYDIAN LETTER C;Lo;0;R;;;;;N;;;;;
+1093F;LYDIAN TRIANGULAR MARK;Po;0;R;;;;;N;;;;;
+10980;MEROITIC HIEROGLYPHIC LETTER A;Lo;0;R;;;;;N;;;;;
+10981;MEROITIC HIEROGLYPHIC LETTER E;Lo;0;R;;;;;N;;;;;
+10982;MEROITIC HIEROGLYPHIC LETTER I;Lo;0;R;;;;;N;;;;;
+10983;MEROITIC HIEROGLYPHIC LETTER O;Lo;0;R;;;;;N;;;;;
+10984;MEROITIC HIEROGLYPHIC LETTER YA;Lo;0;R;;;;;N;;;;;
+10985;MEROITIC HIEROGLYPHIC LETTER WA;Lo;0;R;;;;;N;;;;;
+10986;MEROITIC HIEROGLYPHIC LETTER BA;Lo;0;R;;;;;N;;;;;
+10987;MEROITIC HIEROGLYPHIC LETTER BA-2;Lo;0;R;;;;;N;;;;;
+10988;MEROITIC HIEROGLYPHIC LETTER PA;Lo;0;R;;;;;N;;;;;
+10989;MEROITIC HIEROGLYPHIC LETTER MA;Lo;0;R;;;;;N;;;;;
+1098A;MEROITIC HIEROGLYPHIC LETTER NA;Lo;0;R;;;;;N;;;;;
+1098B;MEROITIC HIEROGLYPHIC LETTER NA-2;Lo;0;R;;;;;N;;;;;
+1098C;MEROITIC HIEROGLYPHIC LETTER NE;Lo;0;R;;;;;N;;;;;
+1098D;MEROITIC HIEROGLYPHIC LETTER NE-2;Lo;0;R;;;;;N;;;;;
+1098E;MEROITIC HIEROGLYPHIC LETTER RA;Lo;0;R;;;;;N;;;;;
+1098F;MEROITIC HIEROGLYPHIC LETTER RA-2;Lo;0;R;;;;;N;;;;;
+10990;MEROITIC HIEROGLYPHIC LETTER LA;Lo;0;R;;;;;N;;;;;
+10991;MEROITIC HIEROGLYPHIC LETTER KHA;Lo;0;R;;;;;N;;;;;
+10992;MEROITIC HIEROGLYPHIC LETTER HHA;Lo;0;R;;;;;N;;;;;
+10993;MEROITIC HIEROGLYPHIC LETTER SA;Lo;0;R;;;;;N;;;;;
+10994;MEROITIC HIEROGLYPHIC LETTER SA-2;Lo;0;R;;;;;N;;;;;
+10995;MEROITIC HIEROGLYPHIC LETTER SE;Lo;0;R;;;;;N;;;;;
+10996;MEROITIC HIEROGLYPHIC LETTER KA;Lo;0;R;;;;;N;;;;;
+10997;MEROITIC HIEROGLYPHIC LETTER QA;Lo;0;R;;;;;N;;;;;
+10998;MEROITIC HIEROGLYPHIC LETTER TA;Lo;0;R;;;;;N;;;;;
+10999;MEROITIC HIEROGLYPHIC LETTER TA-2;Lo;0;R;;;;;N;;;;;
+1099A;MEROITIC HIEROGLYPHIC LETTER TE;Lo;0;R;;;;;N;;;;;
+1099B;MEROITIC HIEROGLYPHIC LETTER TE-2;Lo;0;R;;;;;N;;;;;
+1099C;MEROITIC HIEROGLYPHIC LETTER TO;Lo;0;R;;;;;N;;;;;
+1099D;MEROITIC HIEROGLYPHIC LETTER DA;Lo;0;R;;;;;N;;;;;
+1099E;MEROITIC HIEROGLYPHIC SYMBOL VIDJ;Lo;0;R;;;;;N;;;;;
+1099F;MEROITIC HIEROGLYPHIC SYMBOL VIDJ-2;Lo;0;R;;;;;N;;;;;
+109A0;MEROITIC CURSIVE LETTER A;Lo;0;R;;;;;N;;;;;
+109A1;MEROITIC CURSIVE LETTER E;Lo;0;R;;;;;N;;;;;
+109A2;MEROITIC CURSIVE LETTER I;Lo;0;R;;;;;N;;;;;
+109A3;MEROITIC CURSIVE LETTER O;Lo;0;R;;;;;N;;;;;
+109A4;MEROITIC CURSIVE LETTER YA;Lo;0;R;;;;;N;;;;;
+109A5;MEROITIC CURSIVE LETTER WA;Lo;0;R;;;;;N;;;;;
+109A6;MEROITIC CURSIVE LETTER BA;Lo;0;R;;;;;N;;;;;
+109A7;MEROITIC CURSIVE LETTER PA;Lo;0;R;;;;;N;;;;;
+109A8;MEROITIC CURSIVE LETTER MA;Lo;0;R;;;;;N;;;;;
+109A9;MEROITIC CURSIVE LETTER NA;Lo;0;R;;;;;N;;;;;
+109AA;MEROITIC CURSIVE LETTER NE;Lo;0;R;;;;;N;;;;;
+109AB;MEROITIC CURSIVE LETTER RA;Lo;0;R;;;;;N;;;;;
+109AC;MEROITIC CURSIVE LETTER LA;Lo;0;R;;;;;N;;;;;
+109AD;MEROITIC CURSIVE LETTER KHA;Lo;0;R;;;;;N;;;;;
+109AE;MEROITIC CURSIVE LETTER HHA;Lo;0;R;;;;;N;;;;;
+109AF;MEROITIC CURSIVE LETTER SA;Lo;0;R;;;;;N;;;;;
+109B0;MEROITIC CURSIVE LETTER ARCHAIC SA;Lo;0;R;;;;;N;;;;;
+109B1;MEROITIC CURSIVE LETTER SE;Lo;0;R;;;;;N;;;;;
+109B2;MEROITIC CURSIVE LETTER KA;Lo;0;R;;;;;N;;;;;
+109B3;MEROITIC CURSIVE LETTER QA;Lo;0;R;;;;;N;;;;;
+109B4;MEROITIC CURSIVE LETTER TA;Lo;0;R;;;;;N;;;;;
+109B5;MEROITIC CURSIVE LETTER TE;Lo;0;R;;;;;N;;;;;
+109B6;MEROITIC CURSIVE LETTER TO;Lo;0;R;;;;;N;;;;;
+109B7;MEROITIC CURSIVE LETTER DA;Lo;0;R;;;;;N;;;;;
+109BC;MEROITIC CURSIVE FRACTION ELEVEN TWELFTHS;No;0;R;;;;11/12;N;;;;;
+109BD;MEROITIC CURSIVE FRACTION ONE HALF;No;0;R;;;;1/2;N;;;;;
+109BE;MEROITIC CURSIVE LOGOGRAM RMT;Lo;0;R;;;;;N;;;;;
+109BF;MEROITIC CURSIVE LOGOGRAM IMN;Lo;0;R;;;;;N;;;;;
+109C0;MEROITIC CURSIVE NUMBER ONE;No;0;R;;;;1;N;;;;;
+109C1;MEROITIC CURSIVE NUMBER TWO;No;0;R;;;;2;N;;;;;
+109C2;MEROITIC CURSIVE NUMBER THREE;No;0;R;;;;3;N;;;;;
+109C3;MEROITIC CURSIVE NUMBER FOUR;No;0;R;;;;4;N;;;;;
+109C4;MEROITIC CURSIVE NUMBER FIVE;No;0;R;;;;5;N;;;;;
+109C5;MEROITIC CURSIVE NUMBER SIX;No;0;R;;;;6;N;;;;;
+109C6;MEROITIC CURSIVE NUMBER SEVEN;No;0;R;;;;7;N;;;;;
+109C7;MEROITIC CURSIVE NUMBER EIGHT;No;0;R;;;;8;N;;;;;
+109C8;MEROITIC CURSIVE NUMBER NINE;No;0;R;;;;9;N;;;;;
+109C9;MEROITIC CURSIVE NUMBER TEN;No;0;R;;;;10;N;;;;;
+109CA;MEROITIC CURSIVE NUMBER TWENTY;No;0;R;;;;20;N;;;;;
+109CB;MEROITIC CURSIVE NUMBER THIRTY;No;0;R;;;;30;N;;;;;
+109CC;MEROITIC CURSIVE NUMBER FORTY;No;0;R;;;;40;N;;;;;
+109CD;MEROITIC CURSIVE NUMBER FIFTY;No;0;R;;;;50;N;;;;;
+109CE;MEROITIC CURSIVE NUMBER SIXTY;No;0;R;;;;60;N;;;;;
+109CF;MEROITIC CURSIVE NUMBER SEVENTY;No;0;R;;;;70;N;;;;;
+109D2;MEROITIC CURSIVE NUMBER ONE HUNDRED;No;0;R;;;;100;N;;;;;
+109D3;MEROITIC CURSIVE NUMBER TWO HUNDRED;No;0;R;;;;200;N;;;;;
+109D4;MEROITIC CURSIVE NUMBER THREE HUNDRED;No;0;R;;;;300;N;;;;;
+109D5;MEROITIC CURSIVE NUMBER FOUR HUNDRED;No;0;R;;;;400;N;;;;;
+109D6;MEROITIC CURSIVE NUMBER FIVE HUNDRED;No;0;R;;;;500;N;;;;;
+109D7;MEROITIC CURSIVE NUMBER SIX HUNDRED;No;0;R;;;;600;N;;;;;
+109D8;MEROITIC CURSIVE NUMBER SEVEN HUNDRED;No;0;R;;;;700;N;;;;;
+109D9;MEROITIC CURSIVE NUMBER EIGHT HUNDRED;No;0;R;;;;800;N;;;;;
+109DA;MEROITIC CURSIVE NUMBER NINE HUNDRED;No;0;R;;;;900;N;;;;;
+109DB;MEROITIC CURSIVE NUMBER ONE THOUSAND;No;0;R;;;;1000;N;;;;;
+109DC;MEROITIC CURSIVE NUMBER TWO THOUSAND;No;0;R;;;;2000;N;;;;;
+109DD;MEROITIC CURSIVE NUMBER THREE THOUSAND;No;0;R;;;;3000;N;;;;;
+109DE;MEROITIC CURSIVE NUMBER FOUR THOUSAND;No;0;R;;;;4000;N;;;;;
+109DF;MEROITIC CURSIVE NUMBER FIVE THOUSAND;No;0;R;;;;5000;N;;;;;
+109E0;MEROITIC CURSIVE NUMBER SIX THOUSAND;No;0;R;;;;6000;N;;;;;
+109E1;MEROITIC CURSIVE NUMBER SEVEN THOUSAND;No;0;R;;;;7000;N;;;;;
+109E2;MEROITIC CURSIVE NUMBER EIGHT THOUSAND;No;0;R;;;;8000;N;;;;;
+109E3;MEROITIC CURSIVE NUMBER NINE THOUSAND;No;0;R;;;;9000;N;;;;;
+109E4;MEROITIC CURSIVE NUMBER TEN THOUSAND;No;0;R;;;;10000;N;;;;;
+109E5;MEROITIC CURSIVE NUMBER TWENTY THOUSAND;No;0;R;;;;20000;N;;;;;
+109E6;MEROITIC CURSIVE NUMBER THIRTY THOUSAND;No;0;R;;;;30000;N;;;;;
+109E7;MEROITIC CURSIVE NUMBER FORTY THOUSAND;No;0;R;;;;40000;N;;;;;
+109E8;MEROITIC CURSIVE NUMBER FIFTY THOUSAND;No;0;R;;;;50000;N;;;;;
+109E9;MEROITIC CURSIVE NUMBER SIXTY THOUSAND;No;0;R;;;;60000;N;;;;;
+109EA;MEROITIC CURSIVE NUMBER SEVENTY THOUSAND;No;0;R;;;;70000;N;;;;;
+109EB;MEROITIC CURSIVE NUMBER EIGHTY THOUSAND;No;0;R;;;;80000;N;;;;;
+109EC;MEROITIC CURSIVE NUMBER NINETY THOUSAND;No;0;R;;;;90000;N;;;;;
+109ED;MEROITIC CURSIVE NUMBER ONE HUNDRED THOUSAND;No;0;R;;;;100000;N;;;;;
+109EE;MEROITIC CURSIVE NUMBER TWO HUNDRED THOUSAND;No;0;R;;;;200000;N;;;;;
+109EF;MEROITIC CURSIVE NUMBER THREE HUNDRED THOUSAND;No;0;R;;;;300000;N;;;;;
+109F0;MEROITIC CURSIVE NUMBER FOUR HUNDRED THOUSAND;No;0;R;;;;400000;N;;;;;
+109F1;MEROITIC CURSIVE NUMBER FIVE HUNDRED THOUSAND;No;0;R;;;;500000;N;;;;;
+109F2;MEROITIC CURSIVE NUMBER SIX HUNDRED THOUSAND;No;0;R;;;;600000;N;;;;;
+109F3;MEROITIC CURSIVE NUMBER SEVEN HUNDRED THOUSAND;No;0;R;;;;700000;N;;;;;
+109F4;MEROITIC CURSIVE NUMBER EIGHT HUNDRED THOUSAND;No;0;R;;;;800000;N;;;;;
+109F5;MEROITIC CURSIVE NUMBER NINE HUNDRED THOUSAND;No;0;R;;;;900000;N;;;;;
+109F6;MEROITIC CURSIVE FRACTION ONE TWELFTH;No;0;R;;;;1/12;N;;;;;
+109F7;MEROITIC CURSIVE FRACTION TWO TWELFTHS;No;0;R;;;;2/12;N;;;;;
+109F8;MEROITIC CURSIVE FRACTION THREE TWELFTHS;No;0;R;;;;3/12;N;;;;;
+109F9;MEROITIC CURSIVE FRACTION FOUR TWELFTHS;No;0;R;;;;4/12;N;;;;;
+109FA;MEROITIC CURSIVE FRACTION FIVE TWELFTHS;No;0;R;;;;5/12;N;;;;;
+109FB;MEROITIC CURSIVE FRACTION SIX TWELFTHS;No;0;R;;;;6/12;N;;;;;
+109FC;MEROITIC CURSIVE FRACTION SEVEN TWELFTHS;No;0;R;;;;7/12;N;;;;;
+109FD;MEROITIC CURSIVE FRACTION EIGHT TWELFTHS;No;0;R;;;;8/12;N;;;;;
+109FE;MEROITIC CURSIVE FRACTION NINE TWELFTHS;No;0;R;;;;9/12;N;;;;;
+109FF;MEROITIC CURSIVE FRACTION TEN TWELFTHS;No;0;R;;;;10/12;N;;;;;
+10A00;KHAROSHTHI LETTER A;Lo;0;R;;;;;N;;;;;
+10A01;KHAROSHTHI VOWEL SIGN I;Mn;0;NSM;;;;;N;;;;;
+10A02;KHAROSHTHI VOWEL SIGN U;Mn;0;NSM;;;;;N;;;;;
+10A03;KHAROSHTHI VOWEL SIGN VOCALIC R;Mn;0;NSM;;;;;N;;;;;
+10A05;KHAROSHTHI VOWEL SIGN E;Mn;0;NSM;;;;;N;;;;;
+10A06;KHAROSHTHI VOWEL SIGN O;Mn;0;NSM;;;;;N;;;;;
+10A0C;KHAROSHTHI VOWEL LENGTH MARK;Mn;0;NSM;;;;;N;;;;;
+10A0D;KHAROSHTHI SIGN DOUBLE RING BELOW;Mn;220;NSM;;;;;N;;;;;
+10A0E;KHAROSHTHI SIGN ANUSVARA;Mn;0;NSM;;;;;N;;;;;
+10A0F;KHAROSHTHI SIGN VISARGA;Mn;230;NSM;;;;;N;;;;;
+10A10;KHAROSHTHI LETTER KA;Lo;0;R;;;;;N;;;;;
+10A11;KHAROSHTHI LETTER KHA;Lo;0;R;;;;;N;;;;;
+10A12;KHAROSHTHI LETTER GA;Lo;0;R;;;;;N;;;;;
+10A13;KHAROSHTHI LETTER GHA;Lo;0;R;;;;;N;;;;;
+10A15;KHAROSHTHI LETTER CA;Lo;0;R;;;;;N;;;;;
+10A16;KHAROSHTHI LETTER CHA;Lo;0;R;;;;;N;;;;;
+10A17;KHAROSHTHI LETTER JA;Lo;0;R;;;;;N;;;;;
+10A19;KHAROSHTHI LETTER NYA;Lo;0;R;;;;;N;;;;;
+10A1A;KHAROSHTHI LETTER TTA;Lo;0;R;;;;;N;;;;;
+10A1B;KHAROSHTHI LETTER TTHA;Lo;0;R;;;;;N;;;;;
+10A1C;KHAROSHTHI LETTER DDA;Lo;0;R;;;;;N;;;;;
+10A1D;KHAROSHTHI LETTER DDHA;Lo;0;R;;;;;N;;;;;
+10A1E;KHAROSHTHI LETTER NNA;Lo;0;R;;;;;N;;;;;
+10A1F;KHAROSHTHI LETTER TA;Lo;0;R;;;;;N;;;;;
+10A20;KHAROSHTHI LETTER THA;Lo;0;R;;;;;N;;;;;
+10A21;KHAROSHTHI LETTER DA;Lo;0;R;;;;;N;;;;;
+10A22;KHAROSHTHI LETTER DHA;Lo;0;R;;;;;N;;;;;
+10A23;KHAROSHTHI LETTER NA;Lo;0;R;;;;;N;;;;;
+10A24;KHAROSHTHI LETTER PA;Lo;0;R;;;;;N;;;;;
+10A25;KHAROSHTHI LETTER PHA;Lo;0;R;;;;;N;;;;;
+10A26;KHAROSHTHI LETTER BA;Lo;0;R;;;;;N;;;;;
+10A27;KHAROSHTHI LETTER BHA;Lo;0;R;;;;;N;;;;;
+10A28;KHAROSHTHI LETTER MA;Lo;0;R;;;;;N;;;;;
+10A29;KHAROSHTHI LETTER YA;Lo;0;R;;;;;N;;;;;
+10A2A;KHAROSHTHI LETTER RA;Lo;0;R;;;;;N;;;;;
+10A2B;KHAROSHTHI LETTER LA;Lo;0;R;;;;;N;;;;;
+10A2C;KHAROSHTHI LETTER VA;Lo;0;R;;;;;N;;;;;
+10A2D;KHAROSHTHI LETTER SHA;Lo;0;R;;;;;N;;;;;
+10A2E;KHAROSHTHI LETTER SSA;Lo;0;R;;;;;N;;;;;
+10A2F;KHAROSHTHI LETTER SA;Lo;0;R;;;;;N;;;;;
+10A30;KHAROSHTHI LETTER ZA;Lo;0;R;;;;;N;;;;;
+10A31;KHAROSHTHI LETTER HA;Lo;0;R;;;;;N;;;;;
+10A32;KHAROSHTHI LETTER KKA;Lo;0;R;;;;;N;;;;;
+10A33;KHAROSHTHI LETTER TTTHA;Lo;0;R;;;;;N;;;;;
+10A38;KHAROSHTHI SIGN BAR ABOVE;Mn;230;NSM;;;;;N;;;;;
+10A39;KHAROSHTHI SIGN CAUDA;Mn;1;NSM;;;;;N;;;;;
+10A3A;KHAROSHTHI SIGN DOT BELOW;Mn;220;NSM;;;;;N;;;;;
+10A3F;KHAROSHTHI VIRAMA;Mn;9;NSM;;;;;N;;;;;
+10A40;KHAROSHTHI DIGIT ONE;No;0;R;;;1;1;N;;;;;
+10A41;KHAROSHTHI DIGIT TWO;No;0;R;;;2;2;N;;;;;
+10A42;KHAROSHTHI DIGIT THREE;No;0;R;;;3;3;N;;;;;
+10A43;KHAROSHTHI DIGIT FOUR;No;0;R;;;4;4;N;;;;;
+10A44;KHAROSHTHI NUMBER TEN;No;0;R;;;;10;N;;;;;
+10A45;KHAROSHTHI NUMBER TWENTY;No;0;R;;;;20;N;;;;;
+10A46;KHAROSHTHI NUMBER ONE HUNDRED;No;0;R;;;;100;N;;;;;
+10A47;KHAROSHTHI NUMBER ONE THOUSAND;No;0;R;;;;1000;N;;;;;
+10A50;KHAROSHTHI PUNCTUATION DOT;Po;0;R;;;;;N;;;;;
+10A51;KHAROSHTHI PUNCTUATION SMALL CIRCLE;Po;0;R;;;;;N;;;;;
+10A52;KHAROSHTHI PUNCTUATION CIRCLE;Po;0;R;;;;;N;;;;;
+10A53;KHAROSHTHI PUNCTUATION CRESCENT BAR;Po;0;R;;;;;N;;;;;
+10A54;KHAROSHTHI PUNCTUATION MANGALAM;Po;0;R;;;;;N;;;;;
+10A55;KHAROSHTHI PUNCTUATION LOTUS;Po;0;R;;;;;N;;;;;
+10A56;KHAROSHTHI PUNCTUATION DANDA;Po;0;R;;;;;N;;;;;
+10A57;KHAROSHTHI PUNCTUATION DOUBLE DANDA;Po;0;R;;;;;N;;;;;
+10A58;KHAROSHTHI PUNCTUATION LINES;Po;0;R;;;;;N;;;;;
+10A60;OLD SOUTH ARABIAN LETTER HE;Lo;0;R;;;;;N;;;;;
+10A61;OLD SOUTH ARABIAN LETTER LAMEDH;Lo;0;R;;;;;N;;;;;
+10A62;OLD SOUTH ARABIAN LETTER HETH;Lo;0;R;;;;;N;;;;;
+10A63;OLD SOUTH ARABIAN LETTER MEM;Lo;0;R;;;;;N;;;;;
+10A64;OLD SOUTH ARABIAN LETTER QOPH;Lo;0;R;;;;;N;;;;;
+10A65;OLD SOUTH ARABIAN LETTER WAW;Lo;0;R;;;;;N;;;;;
+10A66;OLD SOUTH ARABIAN LETTER SHIN;Lo;0;R;;;;;N;;;;;
+10A67;OLD SOUTH ARABIAN LETTER RESH;Lo;0;R;;;;;N;;;;;
+10A68;OLD SOUTH ARABIAN LETTER BETH;Lo;0;R;;;;;N;;;;;
+10A69;OLD SOUTH ARABIAN LETTER TAW;Lo;0;R;;;;;N;;;;;
+10A6A;OLD SOUTH ARABIAN LETTER SAT;Lo;0;R;;;;;N;;;;;
+10A6B;OLD SOUTH ARABIAN LETTER KAPH;Lo;0;R;;;;;N;;;;;
+10A6C;OLD SOUTH ARABIAN LETTER NUN;Lo;0;R;;;;;N;;;;;
+10A6D;OLD SOUTH ARABIAN LETTER KHETH;Lo;0;R;;;;;N;;;;;
+10A6E;OLD SOUTH ARABIAN LETTER SADHE;Lo;0;R;;;;;N;;;;;
+10A6F;OLD SOUTH ARABIAN LETTER SAMEKH;Lo;0;R;;;;;N;;;;;
+10A70;OLD SOUTH ARABIAN LETTER FE;Lo;0;R;;;;;N;;;;;
+10A71;OLD SOUTH ARABIAN LETTER ALEF;Lo;0;R;;;;;N;;;;;
+10A72;OLD SOUTH ARABIAN LETTER AYN;Lo;0;R;;;;;N;;;;;
+10A73;OLD SOUTH ARABIAN LETTER DHADHE;Lo;0;R;;;;;N;;;;;
+10A74;OLD SOUTH ARABIAN LETTER GIMEL;Lo;0;R;;;;;N;;;;;
+10A75;OLD SOUTH ARABIAN LETTER DALETH;Lo;0;R;;;;;N;;;;;
+10A76;OLD SOUTH ARABIAN LETTER GHAYN;Lo;0;R;;;;;N;;;;;
+10A77;OLD SOUTH ARABIAN LETTER TETH;Lo;0;R;;;;;N;;;;;
+10A78;OLD SOUTH ARABIAN LETTER ZAYN;Lo;0;R;;;;;N;;;;;
+10A79;OLD SOUTH ARABIAN LETTER DHALETH;Lo;0;R;;;;;N;;;;;
+10A7A;OLD SOUTH ARABIAN LETTER YODH;Lo;0;R;;;;;N;;;;;
+10A7B;OLD SOUTH ARABIAN LETTER THAW;Lo;0;R;;;;;N;;;;;
+10A7C;OLD SOUTH ARABIAN LETTER THETH;Lo;0;R;;;;;N;;;;;
+10A7D;OLD SOUTH ARABIAN NUMBER ONE;No;0;R;;;;1;N;;;;;
+10A7E;OLD SOUTH ARABIAN NUMBER FIFTY;No;0;R;;;;50;N;;;;;
+10A7F;OLD SOUTH ARABIAN NUMERIC INDICATOR;Po;0;R;;;;;N;;;;;
+10A80;OLD NORTH ARABIAN LETTER HEH;Lo;0;R;;;;;N;;;;;
+10A81;OLD NORTH ARABIAN LETTER LAM;Lo;0;R;;;;;N;;;;;
+10A82;OLD NORTH ARABIAN LETTER HAH;Lo;0;R;;;;;N;;;;;
+10A83;OLD NORTH ARABIAN LETTER MEEM;Lo;0;R;;;;;N;;;;;
+10A84;OLD NORTH ARABIAN LETTER QAF;Lo;0;R;;;;;N;;;;;
+10A85;OLD NORTH ARABIAN LETTER WAW;Lo;0;R;;;;;N;;;;;
+10A86;OLD NORTH ARABIAN LETTER ES-2;Lo;0;R;;;;;N;;;;;
+10A87;OLD NORTH ARABIAN LETTER REH;Lo;0;R;;;;;N;;;;;
+10A88;OLD NORTH ARABIAN LETTER BEH;Lo;0;R;;;;;N;;;;;
+10A89;OLD NORTH ARABIAN LETTER TEH;Lo;0;R;;;;;N;;;;;
+10A8A;OLD NORTH ARABIAN LETTER ES-1;Lo;0;R;;;;;N;;;;;
+10A8B;OLD NORTH ARABIAN LETTER KAF;Lo;0;R;;;;;N;;;;;
+10A8C;OLD NORTH ARABIAN LETTER NOON;Lo;0;R;;;;;N;;;;;
+10A8D;OLD NORTH ARABIAN LETTER KHAH;Lo;0;R;;;;;N;;;;;
+10A8E;OLD NORTH ARABIAN LETTER SAD;Lo;0;R;;;;;N;;;;;
+10A8F;OLD NORTH ARABIAN LETTER ES-3;Lo;0;R;;;;;N;;;;;
+10A90;OLD NORTH ARABIAN LETTER FEH;Lo;0;R;;;;;N;;;;;
+10A91;OLD NORTH ARABIAN LETTER ALEF;Lo;0;R;;;;;N;;;;;
+10A92;OLD NORTH ARABIAN LETTER AIN;Lo;0;R;;;;;N;;;;;
+10A93;OLD NORTH ARABIAN LETTER DAD;Lo;0;R;;;;;N;;;;;
+10A94;OLD NORTH ARABIAN LETTER GEEM;Lo;0;R;;;;;N;;;;;
+10A95;OLD NORTH ARABIAN LETTER DAL;Lo;0;R;;;;;N;;;;;
+10A96;OLD NORTH ARABIAN LETTER GHAIN;Lo;0;R;;;;;N;;;;;
+10A97;OLD NORTH ARABIAN LETTER TAH;Lo;0;R;;;;;N;;;;;
+10A98;OLD NORTH ARABIAN LETTER ZAIN;Lo;0;R;;;;;N;;;;;
+10A99;OLD NORTH ARABIAN LETTER THAL;Lo;0;R;;;;;N;;;;;
+10A9A;OLD NORTH ARABIAN LETTER YEH;Lo;0;R;;;;;N;;;;;
+10A9B;OLD NORTH ARABIAN LETTER THEH;Lo;0;R;;;;;N;;;;;
+10A9C;OLD NORTH ARABIAN LETTER ZAH;Lo;0;R;;;;;N;;;;;
+10A9D;OLD NORTH ARABIAN NUMBER ONE;No;0;R;;;;1;N;;;;;
+10A9E;OLD NORTH ARABIAN NUMBER TEN;No;0;R;;;;10;N;;;;;
+10A9F;OLD NORTH ARABIAN NUMBER TWENTY;No;0;R;;;;20;N;;;;;
+10AC0;MANICHAEAN LETTER ALEPH;Lo;0;R;;;;;N;;;;;
+10AC1;MANICHAEAN LETTER BETH;Lo;0;R;;;;;N;;;;;
+10AC2;MANICHAEAN LETTER BHETH;Lo;0;R;;;;;N;;;;;
+10AC3;MANICHAEAN LETTER GIMEL;Lo;0;R;;;;;N;;;;;
+10AC4;MANICHAEAN LETTER GHIMEL;Lo;0;R;;;;;N;;;;;
+10AC5;MANICHAEAN LETTER DALETH;Lo;0;R;;;;;N;;;;;
+10AC6;MANICHAEAN LETTER HE;Lo;0;R;;;;;N;;;;;
+10AC7;MANICHAEAN LETTER WAW;Lo;0;R;;;;;N;;;;;
+10AC8;MANICHAEAN SIGN UD;So;0;R;;;;;N;;;;;
+10AC9;MANICHAEAN LETTER ZAYIN;Lo;0;R;;;;;N;;;;;
+10ACA;MANICHAEAN LETTER ZHAYIN;Lo;0;R;;;;;N;;;;;
+10ACB;MANICHAEAN LETTER JAYIN;Lo;0;R;;;;;N;;;;;
+10ACC;MANICHAEAN LETTER JHAYIN;Lo;0;R;;;;;N;;;;;
+10ACD;MANICHAEAN LETTER HETH;Lo;0;R;;;;;N;;;;;
+10ACE;MANICHAEAN LETTER TETH;Lo;0;R;;;;;N;;;;;
+10ACF;MANICHAEAN LETTER YODH;Lo;0;R;;;;;N;;;;;
+10AD0;MANICHAEAN LETTER KAPH;Lo;0;R;;;;;N;;;;;
+10AD1;MANICHAEAN LETTER XAPH;Lo;0;R;;;;;N;;;;;
+10AD2;MANICHAEAN LETTER KHAPH;Lo;0;R;;;;;N;;;;;
+10AD3;MANICHAEAN LETTER LAMEDH;Lo;0;R;;;;;N;;;;;
+10AD4;MANICHAEAN LETTER DHAMEDH;Lo;0;R;;;;;N;;;;;
+10AD5;MANICHAEAN LETTER THAMEDH;Lo;0;R;;;;;N;;;;;
+10AD6;MANICHAEAN LETTER MEM;Lo;0;R;;;;;N;;;;;
+10AD7;MANICHAEAN LETTER NUN;Lo;0;R;;;;;N;;;;;
+10AD8;MANICHAEAN LETTER SAMEKH;Lo;0;R;;;;;N;;;;;
+10AD9;MANICHAEAN LETTER AYIN;Lo;0;R;;;;;N;;;;;
+10ADA;MANICHAEAN LETTER AAYIN;Lo;0;R;;;;;N;;;;;
+10ADB;MANICHAEAN LETTER PE;Lo;0;R;;;;;N;;;;;
+10ADC;MANICHAEAN LETTER FE;Lo;0;R;;;;;N;;;;;
+10ADD;MANICHAEAN LETTER SADHE;Lo;0;R;;;;;N;;;;;
+10ADE;MANICHAEAN LETTER QOPH;Lo;0;R;;;;;N;;;;;
+10ADF;MANICHAEAN LETTER XOPH;Lo;0;R;;;;;N;;;;;
+10AE0;MANICHAEAN LETTER QHOPH;Lo;0;R;;;;;N;;;;;
+10AE1;MANICHAEAN LETTER RESH;Lo;0;R;;;;;N;;;;;
+10AE2;MANICHAEAN LETTER SHIN;Lo;0;R;;;;;N;;;;;
+10AE3;MANICHAEAN LETTER SSHIN;Lo;0;R;;;;;N;;;;;
+10AE4;MANICHAEAN LETTER TAW;Lo;0;R;;;;;N;;;;;
+10AE5;MANICHAEAN ABBREVIATION MARK ABOVE;Mn;230;NSM;;;;;N;;;;;
+10AE6;MANICHAEAN ABBREVIATION MARK BELOW;Mn;220;NSM;;;;;N;;;;;
+10AEB;MANICHAEAN NUMBER ONE;No;0;R;;;;1;N;;;;;
+10AEC;MANICHAEAN NUMBER FIVE;No;0;R;;;;5;N;;;;;
+10AED;MANICHAEAN NUMBER TEN;No;0;R;;;;10;N;;;;;
+10AEE;MANICHAEAN NUMBER TWENTY;No;0;R;;;;20;N;;;;;
+10AEF;MANICHAEAN NUMBER ONE HUNDRED;No;0;R;;;;100;N;;;;;
+10AF0;MANICHAEAN PUNCTUATION STAR;Po;0;R;;;;;N;;;;;
+10AF1;MANICHAEAN PUNCTUATION FLEURON;Po;0;R;;;;;N;;;;;
+10AF2;MANICHAEAN PUNCTUATION DOUBLE DOT WITHIN DOT;Po;0;R;;;;;N;;;;;
+10AF3;MANICHAEAN PUNCTUATION DOT WITHIN DOT;Po;0;R;;;;;N;;;;;
+10AF4;MANICHAEAN PUNCTUATION DOT;Po;0;R;;;;;N;;;;;
+10AF5;MANICHAEAN PUNCTUATION TWO DOTS;Po;0;R;;;;;N;;;;;
+10AF6;MANICHAEAN PUNCTUATION LINE FILLER;Po;0;R;;;;;N;;;;;
+10B00;AVESTAN LETTER A;Lo;0;R;;;;;N;;;;;
+10B01;AVESTAN LETTER AA;Lo;0;R;;;;;N;;;;;
+10B02;AVESTAN LETTER AO;Lo;0;R;;;;;N;;;;;
+10B03;AVESTAN LETTER AAO;Lo;0;R;;;;;N;;;;;
+10B04;AVESTAN LETTER AN;Lo;0;R;;;;;N;;;;;
+10B05;AVESTAN LETTER AAN;Lo;0;R;;;;;N;;;;;
+10B06;AVESTAN LETTER AE;Lo;0;R;;;;;N;;;;;
+10B07;AVESTAN LETTER AEE;Lo;0;R;;;;;N;;;;;
+10B08;AVESTAN LETTER E;Lo;0;R;;;;;N;;;;;
+10B09;AVESTAN LETTER EE;Lo;0;R;;;;;N;;;;;
+10B0A;AVESTAN LETTER O;Lo;0;R;;;;;N;;;;;
+10B0B;AVESTAN LETTER OO;Lo;0;R;;;;;N;;;;;
+10B0C;AVESTAN LETTER I;Lo;0;R;;;;;N;;;;;
+10B0D;AVESTAN LETTER II;Lo;0;R;;;;;N;;;;;
+10B0E;AVESTAN LETTER U;Lo;0;R;;;;;N;;;;;
+10B0F;AVESTAN LETTER UU;Lo;0;R;;;;;N;;;;;
+10B10;AVESTAN LETTER KE;Lo;0;R;;;;;N;;;;;
+10B11;AVESTAN LETTER XE;Lo;0;R;;;;;N;;;;;
+10B12;AVESTAN LETTER XYE;Lo;0;R;;;;;N;;;;;
+10B13;AVESTAN LETTER XVE;Lo;0;R;;;;;N;;;;;
+10B14;AVESTAN LETTER GE;Lo;0;R;;;;;N;;;;;
+10B15;AVESTAN LETTER GGE;Lo;0;R;;;;;N;;;;;
+10B16;AVESTAN LETTER GHE;Lo;0;R;;;;;N;;;;;
+10B17;AVESTAN LETTER CE;Lo;0;R;;;;;N;;;;;
+10B18;AVESTAN LETTER JE;Lo;0;R;;;;;N;;;;;
+10B19;AVESTAN LETTER TE;Lo;0;R;;;;;N;;;;;
+10B1A;AVESTAN LETTER THE;Lo;0;R;;;;;N;;;;;
+10B1B;AVESTAN LETTER DE;Lo;0;R;;;;;N;;;;;
+10B1C;AVESTAN LETTER DHE;Lo;0;R;;;;;N;;;;;
+10B1D;AVESTAN LETTER TTE;Lo;0;R;;;;;N;;;;;
+10B1E;AVESTAN LETTER PE;Lo;0;R;;;;;N;;;;;
+10B1F;AVESTAN LETTER FE;Lo;0;R;;;;;N;;;;;
+10B20;AVESTAN LETTER BE;Lo;0;R;;;;;N;;;;;
+10B21;AVESTAN LETTER BHE;Lo;0;R;;;;;N;;;;;
+10B22;AVESTAN LETTER NGE;Lo;0;R;;;;;N;;;;;
+10B23;AVESTAN LETTER NGYE;Lo;0;R;;;;;N;;;;;
+10B24;AVESTAN LETTER NGVE;Lo;0;R;;;;;N;;;;;
+10B25;AVESTAN LETTER NE;Lo;0;R;;;;;N;;;;;
+10B26;AVESTAN LETTER NYE;Lo;0;R;;;;;N;;;;;
+10B27;AVESTAN LETTER NNE;Lo;0;R;;;;;N;;;;;
+10B28;AVESTAN LETTER ME;Lo;0;R;;;;;N;;;;;
+10B29;AVESTAN LETTER HME;Lo;0;R;;;;;N;;;;;
+10B2A;AVESTAN LETTER YYE;Lo;0;R;;;;;N;;;;;
+10B2B;AVESTAN LETTER YE;Lo;0;R;;;;;N;;;;;
+10B2C;AVESTAN LETTER VE;Lo;0;R;;;;;N;;;;;
+10B2D;AVESTAN LETTER RE;Lo;0;R;;;;;N;;;;;
+10B2E;AVESTAN LETTER LE;Lo;0;R;;;;;N;;;;;
+10B2F;AVESTAN LETTER SE;Lo;0;R;;;;;N;;;;;
+10B30;AVESTAN LETTER ZE;Lo;0;R;;;;;N;;;;;
+10B31;AVESTAN LETTER SHE;Lo;0;R;;;;;N;;;;;
+10B32;AVESTAN LETTER ZHE;Lo;0;R;;;;;N;;;;;
+10B33;AVESTAN LETTER SHYE;Lo;0;R;;;;;N;;;;;
+10B34;AVESTAN LETTER SSHE;Lo;0;R;;;;;N;;;;;
+10B35;AVESTAN LETTER HE;Lo;0;R;;;;;N;;;;;
+10B39;AVESTAN ABBREVIATION MARK;Po;0;ON;;;;;N;;;;;
+10B3A;TINY TWO DOTS OVER ONE DOT PUNCTUATION;Po;0;ON;;;;;N;;;;;
+10B3B;SMALL TWO DOTS OVER ONE DOT PUNCTUATION;Po;0;ON;;;;;N;;;;;
+10B3C;LARGE TWO DOTS OVER ONE DOT PUNCTUATION;Po;0;ON;;;;;N;;;;;
+10B3D;LARGE ONE DOT OVER TWO DOTS PUNCTUATION;Po;0;ON;;;;;N;;;;;
+10B3E;LARGE TWO RINGS OVER ONE RING PUNCTUATION;Po;0;ON;;;;;N;;;;;
+10B3F;LARGE ONE RING OVER TWO RINGS PUNCTUATION;Po;0;ON;;;;;N;;;;;
+10B40;INSCRIPTIONAL PARTHIAN LETTER ALEPH;Lo;0;R;;;;;N;;;;;
+10B41;INSCRIPTIONAL PARTHIAN LETTER BETH;Lo;0;R;;;;;N;;;;;
+10B42;INSCRIPTIONAL PARTHIAN LETTER GIMEL;Lo;0;R;;;;;N;;;;;
+10B43;INSCRIPTIONAL PARTHIAN LETTER DALETH;Lo;0;R;;;;;N;;;;;
+10B44;INSCRIPTIONAL PARTHIAN LETTER HE;Lo;0;R;;;;;N;;;;;
+10B45;INSCRIPTIONAL PARTHIAN LETTER WAW;Lo;0;R;;;;;N;;;;;
+10B46;INSCRIPTIONAL PARTHIAN LETTER ZAYIN;Lo;0;R;;;;;N;;;;;
+10B47;INSCRIPTIONAL PARTHIAN LETTER HETH;Lo;0;R;;;;;N;;;;;
+10B48;INSCRIPTIONAL PARTHIAN LETTER TETH;Lo;0;R;;;;;N;;;;;
+10B49;INSCRIPTIONAL PARTHIAN LETTER YODH;Lo;0;R;;;;;N;;;;;
+10B4A;INSCRIPTIONAL PARTHIAN LETTER KAPH;Lo;0;R;;;;;N;;;;;
+10B4B;INSCRIPTIONAL PARTHIAN LETTER LAMEDH;Lo;0;R;;;;;N;;;;;
+10B4C;INSCRIPTIONAL PARTHIAN LETTER MEM;Lo;0;R;;;;;N;;;;;
+10B4D;INSCRIPTIONAL PARTHIAN LETTER NUN;Lo;0;R;;;;;N;;;;;
+10B4E;INSCRIPTIONAL PARTHIAN LETTER SAMEKH;Lo;0;R;;;;;N;;;;;
+10B4F;INSCRIPTIONAL PARTHIAN LETTER AYIN;Lo;0;R;;;;;N;;;;;
+10B50;INSCRIPTIONAL PARTHIAN LETTER PE;Lo;0;R;;;;;N;;;;;
+10B51;INSCRIPTIONAL PARTHIAN LETTER SADHE;Lo;0;R;;;;;N;;;;;
+10B52;INSCRIPTIONAL PARTHIAN LETTER QOPH;Lo;0;R;;;;;N;;;;;
+10B53;INSCRIPTIONAL PARTHIAN LETTER RESH;Lo;0;R;;;;;N;;;;;
+10B54;INSCRIPTIONAL PARTHIAN LETTER SHIN;Lo;0;R;;;;;N;;;;;
+10B55;INSCRIPTIONAL PARTHIAN LETTER TAW;Lo;0;R;;;;;N;;;;;
+10B58;INSCRIPTIONAL PARTHIAN NUMBER ONE;No;0;R;;;;1;N;;;;;
+10B59;INSCRIPTIONAL PARTHIAN NUMBER TWO;No;0;R;;;;2;N;;;;;
+10B5A;INSCRIPTIONAL PARTHIAN NUMBER THREE;No;0;R;;;;3;N;;;;;
+10B5B;INSCRIPTIONAL PARTHIAN NUMBER FOUR;No;0;R;;;;4;N;;;;;
+10B5C;INSCRIPTIONAL PARTHIAN NUMBER TEN;No;0;R;;;;10;N;;;;;
+10B5D;INSCRIPTIONAL PARTHIAN NUMBER TWENTY;No;0;R;;;;20;N;;;;;
+10B5E;INSCRIPTIONAL PARTHIAN NUMBER ONE HUNDRED;No;0;R;;;;100;N;;;;;
+10B5F;INSCRIPTIONAL PARTHIAN NUMBER ONE THOUSAND;No;0;R;;;;1000;N;;;;;
+10B60;INSCRIPTIONAL PAHLAVI LETTER ALEPH;Lo;0;R;;;;;N;;;;;
+10B61;INSCRIPTIONAL PAHLAVI LETTER BETH;Lo;0;R;;;;;N;;;;;
+10B62;INSCRIPTIONAL PAHLAVI LETTER GIMEL;Lo;0;R;;;;;N;;;;;
+10B63;INSCRIPTIONAL PAHLAVI LETTER DALETH;Lo;0;R;;;;;N;;;;;
+10B64;INSCRIPTIONAL PAHLAVI LETTER HE;Lo;0;R;;;;;N;;;;;
+10B65;INSCRIPTIONAL PAHLAVI LETTER WAW-AYIN-RESH;Lo;0;R;;;;;N;;;;;
+10B66;INSCRIPTIONAL PAHLAVI LETTER ZAYIN;Lo;0;R;;;;;N;;;;;
+10B67;INSCRIPTIONAL PAHLAVI LETTER HETH;Lo;0;R;;;;;N;;;;;
+10B68;INSCRIPTIONAL PAHLAVI LETTER TETH;Lo;0;R;;;;;N;;;;;
+10B69;INSCRIPTIONAL PAHLAVI LETTER YODH;Lo;0;R;;;;;N;;;;;
+10B6A;INSCRIPTIONAL PAHLAVI LETTER KAPH;Lo;0;R;;;;;N;;;;;
+10B6B;INSCRIPTIONAL PAHLAVI LETTER LAMEDH;Lo;0;R;;;;;N;;;;;
+10B6C;INSCRIPTIONAL PAHLAVI LETTER MEM-QOPH;Lo;0;R;;;;;N;;;;;
+10B6D;INSCRIPTIONAL PAHLAVI LETTER NUN;Lo;0;R;;;;;N;;;;;
+10B6E;INSCRIPTIONAL PAHLAVI LETTER SAMEKH;Lo;0;R;;;;;N;;;;;
+10B6F;INSCRIPTIONAL PAHLAVI LETTER PE;Lo;0;R;;;;;N;;;;;
+10B70;INSCRIPTIONAL PAHLAVI LETTER SADHE;Lo;0;R;;;;;N;;;;;
+10B71;INSCRIPTIONAL PAHLAVI LETTER SHIN;Lo;0;R;;;;;N;;;;;
+10B72;INSCRIPTIONAL PAHLAVI LETTER TAW;Lo;0;R;;;;;N;;;;;
+10B78;INSCRIPTIONAL PAHLAVI NUMBER ONE;No;0;R;;;;1;N;;;;;
+10B79;INSCRIPTIONAL PAHLAVI NUMBER TWO;No;0;R;;;;2;N;;;;;
+10B7A;INSCRIPTIONAL PAHLAVI NUMBER THREE;No;0;R;;;;3;N;;;;;
+10B7B;INSCRIPTIONAL PAHLAVI NUMBER FOUR;No;0;R;;;;4;N;;;;;
+10B7C;INSCRIPTIONAL PAHLAVI NUMBER TEN;No;0;R;;;;10;N;;;;;
+10B7D;INSCRIPTIONAL PAHLAVI NUMBER TWENTY;No;0;R;;;;20;N;;;;;
+10B7E;INSCRIPTIONAL PAHLAVI NUMBER ONE HUNDRED;No;0;R;;;;100;N;;;;;
+10B7F;INSCRIPTIONAL PAHLAVI NUMBER ONE THOUSAND;No;0;R;;;;1000;N;;;;;
+10B80;PSALTER PAHLAVI LETTER ALEPH;Lo;0;R;;;;;N;;;;;
+10B81;PSALTER PAHLAVI LETTER BETH;Lo;0;R;;;;;N;;;;;
+10B82;PSALTER PAHLAVI LETTER GIMEL;Lo;0;R;;;;;N;;;;;
+10B83;PSALTER PAHLAVI LETTER DALETH;Lo;0;R;;;;;N;;;;;
+10B84;PSALTER PAHLAVI LETTER HE;Lo;0;R;;;;;N;;;;;
+10B85;PSALTER PAHLAVI LETTER WAW-AYIN-RESH;Lo;0;R;;;;;N;;;;;
+10B86;PSALTER PAHLAVI LETTER ZAYIN;Lo;0;R;;;;;N;;;;;
+10B87;PSALTER PAHLAVI LETTER HETH;Lo;0;R;;;;;N;;;;;
+10B88;PSALTER PAHLAVI LETTER YODH;Lo;0;R;;;;;N;;;;;
+10B89;PSALTER PAHLAVI LETTER KAPH;Lo;0;R;;;;;N;;;;;
+10B8A;PSALTER PAHLAVI LETTER LAMEDH;Lo;0;R;;;;;N;;;;;
+10B8B;PSALTER PAHLAVI LETTER MEM-QOPH;Lo;0;R;;;;;N;;;;;
+10B8C;PSALTER PAHLAVI LETTER NUN;Lo;0;R;;;;;N;;;;;
+10B8D;PSALTER PAHLAVI LETTER SAMEKH;Lo;0;R;;;;;N;;;;;
+10B8E;PSALTER PAHLAVI LETTER PE;Lo;0;R;;;;;N;;;;;
+10B8F;PSALTER PAHLAVI LETTER SADHE;Lo;0;R;;;;;N;;;;;
+10B90;PSALTER PAHLAVI LETTER SHIN;Lo;0;R;;;;;N;;;;;
+10B91;PSALTER PAHLAVI LETTER TAW;Lo;0;R;;;;;N;;;;;
+10B99;PSALTER PAHLAVI SECTION MARK;Po;0;R;;;;;N;;;;;
+10B9A;PSALTER PAHLAVI TURNED SECTION MARK;Po;0;R;;;;;N;;;;;
+10B9B;PSALTER PAHLAVI FOUR DOTS WITH CROSS;Po;0;R;;;;;N;;;;;
+10B9C;PSALTER PAHLAVI FOUR DOTS WITH DOT;Po;0;R;;;;;N;;;;;
+10BA9;PSALTER PAHLAVI NUMBER ONE;No;0;R;;;;1;N;;;;;
+10BAA;PSALTER PAHLAVI NUMBER TWO;No;0;R;;;;2;N;;;;;
+10BAB;PSALTER PAHLAVI NUMBER THREE;No;0;R;;;;3;N;;;;;
+10BAC;PSALTER PAHLAVI NUMBER FOUR;No;0;R;;;;4;N;;;;;
+10BAD;PSALTER PAHLAVI NUMBER TEN;No;0;R;;;;10;N;;;;;
+10BAE;PSALTER PAHLAVI NUMBER TWENTY;No;0;R;;;;20;N;;;;;
+10BAF;PSALTER PAHLAVI NUMBER ONE HUNDRED;No;0;R;;;;100;N;;;;;
+10C00;OLD TURKIC LETTER ORKHON A;Lo;0;R;;;;;N;;;;;
+10C01;OLD TURKIC LETTER YENISEI A;Lo;0;R;;;;;N;;;;;
+10C02;OLD TURKIC LETTER YENISEI AE;Lo;0;R;;;;;N;;;;;
+10C03;OLD TURKIC LETTER ORKHON I;Lo;0;R;;;;;N;;;;;
+10C04;OLD TURKIC LETTER YENISEI I;Lo;0;R;;;;;N;;;;;
+10C05;OLD TURKIC LETTER YENISEI E;Lo;0;R;;;;;N;;;;;
+10C06;OLD TURKIC LETTER ORKHON O;Lo;0;R;;;;;N;;;;;
+10C07;OLD TURKIC LETTER ORKHON OE;Lo;0;R;;;;;N;;;;;
+10C08;OLD TURKIC LETTER YENISEI OE;Lo;0;R;;;;;N;;;;;
+10C09;OLD TURKIC LETTER ORKHON AB;Lo;0;R;;;;;N;;;;;
+10C0A;OLD TURKIC LETTER YENISEI AB;Lo;0;R;;;;;N;;;;;
+10C0B;OLD TURKIC LETTER ORKHON AEB;Lo;0;R;;;;;N;;;;;
+10C0C;OLD TURKIC LETTER YENISEI AEB;Lo;0;R;;;;;N;;;;;
+10C0D;OLD TURKIC LETTER ORKHON AG;Lo;0;R;;;;;N;;;;;
+10C0E;OLD TURKIC LETTER YENISEI AG;Lo;0;R;;;;;N;;;;;
+10C0F;OLD TURKIC LETTER ORKHON AEG;Lo;0;R;;;;;N;;;;;
+10C10;OLD TURKIC LETTER YENISEI AEG;Lo;0;R;;;;;N;;;;;
+10C11;OLD TURKIC LETTER ORKHON AD;Lo;0;R;;;;;N;;;;;
+10C12;OLD TURKIC LETTER YENISEI AD;Lo;0;R;;;;;N;;;;;
+10C13;OLD TURKIC LETTER ORKHON AED;Lo;0;R;;;;;N;;;;;
+10C14;OLD TURKIC LETTER ORKHON EZ;Lo;0;R;;;;;N;;;;;
+10C15;OLD TURKIC LETTER YENISEI EZ;Lo;0;R;;;;;N;;;;;
+10C16;OLD TURKIC LETTER ORKHON AY;Lo;0;R;;;;;N;;;;;
+10C17;OLD TURKIC LETTER YENISEI AY;Lo;0;R;;;;;N;;;;;
+10C18;OLD TURKIC LETTER ORKHON AEY;Lo;0;R;;;;;N;;;;;
+10C19;OLD TURKIC LETTER YENISEI AEY;Lo;0;R;;;;;N;;;;;
+10C1A;OLD TURKIC LETTER ORKHON AEK;Lo;0;R;;;;;N;;;;;
+10C1B;OLD TURKIC LETTER YENISEI AEK;Lo;0;R;;;;;N;;;;;
+10C1C;OLD TURKIC LETTER ORKHON OEK;Lo;0;R;;;;;N;;;;;
+10C1D;OLD TURKIC LETTER YENISEI OEK;Lo;0;R;;;;;N;;;;;
+10C1E;OLD TURKIC LETTER ORKHON AL;Lo;0;R;;;;;N;;;;;
+10C1F;OLD TURKIC LETTER YENISEI AL;Lo;0;R;;;;;N;;;;;
+10C20;OLD TURKIC LETTER ORKHON AEL;Lo;0;R;;;;;N;;;;;
+10C21;OLD TURKIC LETTER ORKHON ELT;Lo;0;R;;;;;N;;;;;
+10C22;OLD TURKIC LETTER ORKHON EM;Lo;0;R;;;;;N;;;;;
+10C23;OLD TURKIC LETTER ORKHON AN;Lo;0;R;;;;;N;;;;;
+10C24;OLD TURKIC LETTER ORKHON AEN;Lo;0;R;;;;;N;;;;;
+10C25;OLD TURKIC LETTER YENISEI AEN;Lo;0;R;;;;;N;;;;;
+10C26;OLD TURKIC LETTER ORKHON ENT;Lo;0;R;;;;;N;;;;;
+10C27;OLD TURKIC LETTER YENISEI ENT;Lo;0;R;;;;;N;;;;;
+10C28;OLD TURKIC LETTER ORKHON ENC;Lo;0;R;;;;;N;;;;;
+10C29;OLD TURKIC LETTER YENISEI ENC;Lo;0;R;;;;;N;;;;;
+10C2A;OLD TURKIC LETTER ORKHON ENY;Lo;0;R;;;;;N;;;;;
+10C2B;OLD TURKIC LETTER YENISEI ENY;Lo;0;R;;;;;N;;;;;
+10C2C;OLD TURKIC LETTER YENISEI ANG;Lo;0;R;;;;;N;;;;;
+10C2D;OLD TURKIC LETTER ORKHON ENG;Lo;0;R;;;;;N;;;;;
+10C2E;OLD TURKIC LETTER YENISEI AENG;Lo;0;R;;;;;N;;;;;
+10C2F;OLD TURKIC LETTER ORKHON EP;Lo;0;R;;;;;N;;;;;
+10C30;OLD TURKIC LETTER ORKHON OP;Lo;0;R;;;;;N;;;;;
+10C31;OLD TURKIC LETTER ORKHON IC;Lo;0;R;;;;;N;;;;;
+10C32;OLD TURKIC LETTER ORKHON EC;Lo;0;R;;;;;N;;;;;
+10C33;OLD TURKIC LETTER YENISEI EC;Lo;0;R;;;;;N;;;;;
+10C34;OLD TURKIC LETTER ORKHON AQ;Lo;0;R;;;;;N;;;;;
+10C35;OLD TURKIC LETTER YENISEI AQ;Lo;0;R;;;;;N;;;;;
+10C36;OLD TURKIC LETTER ORKHON IQ;Lo;0;R;;;;;N;;;;;
+10C37;OLD TURKIC LETTER YENISEI IQ;Lo;0;R;;;;;N;;;;;
+10C38;OLD TURKIC LETTER ORKHON OQ;Lo;0;R;;;;;N;;;;;
+10C39;OLD TURKIC LETTER YENISEI OQ;Lo;0;R;;;;;N;;;;;
+10C3A;OLD TURKIC LETTER ORKHON AR;Lo;0;R;;;;;N;;;;;
+10C3B;OLD TURKIC LETTER YENISEI AR;Lo;0;R;;;;;N;;;;;
+10C3C;OLD TURKIC LETTER ORKHON AER;Lo;0;R;;;;;N;;;;;
+10C3D;OLD TURKIC LETTER ORKHON AS;Lo;0;R;;;;;N;;;;;
+10C3E;OLD TURKIC LETTER ORKHON AES;Lo;0;R;;;;;N;;;;;
+10C3F;OLD TURKIC LETTER ORKHON ASH;Lo;0;R;;;;;N;;;;;
+10C40;OLD TURKIC LETTER YENISEI ASH;Lo;0;R;;;;;N;;;;;
+10C41;OLD TURKIC LETTER ORKHON ESH;Lo;0;R;;;;;N;;;;;
+10C42;OLD TURKIC LETTER YENISEI ESH;Lo;0;R;;;;;N;;;;;
+10C43;OLD TURKIC LETTER ORKHON AT;Lo;0;R;;;;;N;;;;;
+10C44;OLD TURKIC LETTER YENISEI AT;Lo;0;R;;;;;N;;;;;
+10C45;OLD TURKIC LETTER ORKHON AET;Lo;0;R;;;;;N;;;;;
+10C46;OLD TURKIC LETTER YENISEI AET;Lo;0;R;;;;;N;;;;;
+10C47;OLD TURKIC LETTER ORKHON OT;Lo;0;R;;;;;N;;;;;
+10C48;OLD TURKIC LETTER ORKHON BASH;Lo;0;R;;;;;N;;;;;
+10C80;OLD HUNGARIAN CAPITAL LETTER A;Lu;0;R;;;;;N;;;;10CC0;
+10C81;OLD HUNGARIAN CAPITAL LETTER AA;Lu;0;R;;;;;N;;;;10CC1;
+10C82;OLD HUNGARIAN CAPITAL LETTER EB;Lu;0;R;;;;;N;;;;10CC2;
+10C83;OLD HUNGARIAN CAPITAL LETTER AMB;Lu;0;R;;;;;N;;;;10CC3;
+10C84;OLD HUNGARIAN CAPITAL LETTER EC;Lu;0;R;;;;;N;;;;10CC4;
+10C85;OLD HUNGARIAN CAPITAL LETTER ENC;Lu;0;R;;;;;N;;;;10CC5;
+10C86;OLD HUNGARIAN CAPITAL LETTER ECS;Lu;0;R;;;;;N;;;;10CC6;
+10C87;OLD HUNGARIAN CAPITAL LETTER ED;Lu;0;R;;;;;N;;;;10CC7;
+10C88;OLD HUNGARIAN CAPITAL LETTER AND;Lu;0;R;;;;;N;;;;10CC8;
+10C89;OLD HUNGARIAN CAPITAL LETTER E;Lu;0;R;;;;;N;;;;10CC9;
+10C8A;OLD HUNGARIAN CAPITAL LETTER CLOSE E;Lu;0;R;;;;;N;;;;10CCA;
+10C8B;OLD HUNGARIAN CAPITAL LETTER EE;Lu;0;R;;;;;N;;;;10CCB;
+10C8C;OLD HUNGARIAN CAPITAL LETTER EF;Lu;0;R;;;;;N;;;;10CCC;
+10C8D;OLD HUNGARIAN CAPITAL LETTER EG;Lu;0;R;;;;;N;;;;10CCD;
+10C8E;OLD HUNGARIAN CAPITAL LETTER EGY;Lu;0;R;;;;;N;;;;10CCE;
+10C8F;OLD HUNGARIAN CAPITAL LETTER EH;Lu;0;R;;;;;N;;;;10CCF;
+10C90;OLD HUNGARIAN CAPITAL LETTER I;Lu;0;R;;;;;N;;;;10CD0;
+10C91;OLD HUNGARIAN CAPITAL LETTER II;Lu;0;R;;;;;N;;;;10CD1;
+10C92;OLD HUNGARIAN CAPITAL LETTER EJ;Lu;0;R;;;;;N;;;;10CD2;
+10C93;OLD HUNGARIAN CAPITAL LETTER EK;Lu;0;R;;;;;N;;;;10CD3;
+10C94;OLD HUNGARIAN CAPITAL LETTER AK;Lu;0;R;;;;;N;;;;10CD4;
+10C95;OLD HUNGARIAN CAPITAL LETTER UNK;Lu;0;R;;;;;N;;;;10CD5;
+10C96;OLD HUNGARIAN CAPITAL LETTER EL;Lu;0;R;;;;;N;;;;10CD6;
+10C97;OLD HUNGARIAN CAPITAL LETTER ELY;Lu;0;R;;;;;N;;;;10CD7;
+10C98;OLD HUNGARIAN CAPITAL LETTER EM;Lu;0;R;;;;;N;;;;10CD8;
+10C99;OLD HUNGARIAN CAPITAL LETTER EN;Lu;0;R;;;;;N;;;;10CD9;
+10C9A;OLD HUNGARIAN CAPITAL LETTER ENY;Lu;0;R;;;;;N;;;;10CDA;
+10C9B;OLD HUNGARIAN CAPITAL LETTER O;Lu;0;R;;;;;N;;;;10CDB;
+10C9C;OLD HUNGARIAN CAPITAL LETTER OO;Lu;0;R;;;;;N;;;;10CDC;
+10C9D;OLD HUNGARIAN CAPITAL LETTER NIKOLSBURG OE;Lu;0;R;;;;;N;;;;10CDD;
+10C9E;OLD HUNGARIAN CAPITAL LETTER RUDIMENTA OE;Lu;0;R;;;;;N;;;;10CDE;
+10C9F;OLD HUNGARIAN CAPITAL LETTER OEE;Lu;0;R;;;;;N;;;;10CDF;
+10CA0;OLD HUNGARIAN CAPITAL LETTER EP;Lu;0;R;;;;;N;;;;10CE0;
+10CA1;OLD HUNGARIAN CAPITAL LETTER EMP;Lu;0;R;;;;;N;;;;10CE1;
+10CA2;OLD HUNGARIAN CAPITAL LETTER ER;Lu;0;R;;;;;N;;;;10CE2;
+10CA3;OLD HUNGARIAN CAPITAL LETTER SHORT ER;Lu;0;R;;;;;N;;;;10CE3;
+10CA4;OLD HUNGARIAN CAPITAL LETTER ES;Lu;0;R;;;;;N;;;;10CE4;
+10CA5;OLD HUNGARIAN CAPITAL LETTER ESZ;Lu;0;R;;;;;N;;;;10CE5;
+10CA6;OLD HUNGARIAN CAPITAL LETTER ET;Lu;0;R;;;;;N;;;;10CE6;
+10CA7;OLD HUNGARIAN CAPITAL LETTER ENT;Lu;0;R;;;;;N;;;;10CE7;
+10CA8;OLD HUNGARIAN CAPITAL LETTER ETY;Lu;0;R;;;;;N;;;;10CE8;
+10CA9;OLD HUNGARIAN CAPITAL LETTER ECH;Lu;0;R;;;;;N;;;;10CE9;
+10CAA;OLD HUNGARIAN CAPITAL LETTER U;Lu;0;R;;;;;N;;;;10CEA;
+10CAB;OLD HUNGARIAN CAPITAL LETTER UU;Lu;0;R;;;;;N;;;;10CEB;
+10CAC;OLD HUNGARIAN CAPITAL LETTER NIKOLSBURG UE;Lu;0;R;;;;;N;;;;10CEC;
+10CAD;OLD HUNGARIAN CAPITAL LETTER RUDIMENTA UE;Lu;0;R;;;;;N;;;;10CED;
+10CAE;OLD HUNGARIAN CAPITAL LETTER EV;Lu;0;R;;;;;N;;;;10CEE;
+10CAF;OLD HUNGARIAN CAPITAL LETTER EZ;Lu;0;R;;;;;N;;;;10CEF;
+10CB0;OLD HUNGARIAN CAPITAL LETTER EZS;Lu;0;R;;;;;N;;;;10CF0;
+10CB1;OLD HUNGARIAN CAPITAL LETTER ENT-SHAPED SIGN;Lu;0;R;;;;;N;;;;10CF1;
+10CB2;OLD HUNGARIAN CAPITAL LETTER US;Lu;0;R;;;;;N;;;;10CF2;
+10CC0;OLD HUNGARIAN SMALL LETTER A;Ll;0;R;;;;;N;;;10C80;;10C80
+10CC1;OLD HUNGARIAN SMALL LETTER AA;Ll;0;R;;;;;N;;;10C81;;10C81
+10CC2;OLD HUNGARIAN SMALL LETTER EB;Ll;0;R;;;;;N;;;10C82;;10C82
+10CC3;OLD HUNGARIAN SMALL LETTER AMB;Ll;0;R;;;;;N;;;10C83;;10C83
+10CC4;OLD HUNGARIAN SMALL LETTER EC;Ll;0;R;;;;;N;;;10C84;;10C84
+10CC5;OLD HUNGARIAN SMALL LETTER ENC;Ll;0;R;;;;;N;;;10C85;;10C85
+10CC6;OLD HUNGARIAN SMALL LETTER ECS;Ll;0;R;;;;;N;;;10C86;;10C86
+10CC7;OLD HUNGARIAN SMALL LETTER ED;Ll;0;R;;;;;N;;;10C87;;10C87
+10CC8;OLD HUNGARIAN SMALL LETTER AND;Ll;0;R;;;;;N;;;10C88;;10C88
+10CC9;OLD HUNGARIAN SMALL LETTER E;Ll;0;R;;;;;N;;;10C89;;10C89
+10CCA;OLD HUNGARIAN SMALL LETTER CLOSE E;Ll;0;R;;;;;N;;;10C8A;;10C8A
+10CCB;OLD HUNGARIAN SMALL LETTER EE;Ll;0;R;;;;;N;;;10C8B;;10C8B
+10CCC;OLD HUNGARIAN SMALL LETTER EF;Ll;0;R;;;;;N;;;10C8C;;10C8C
+10CCD;OLD HUNGARIAN SMALL LETTER EG;Ll;0;R;;;;;N;;;10C8D;;10C8D
+10CCE;OLD HUNGARIAN SMALL LETTER EGY;Ll;0;R;;;;;N;;;10C8E;;10C8E
+10CCF;OLD HUNGARIAN SMALL LETTER EH;Ll;0;R;;;;;N;;;10C8F;;10C8F
+10CD0;OLD HUNGARIAN SMALL LETTER I;Ll;0;R;;;;;N;;;10C90;;10C90
+10CD1;OLD HUNGARIAN SMALL LETTER II;Ll;0;R;;;;;N;;;10C91;;10C91
+10CD2;OLD HUNGARIAN SMALL LETTER EJ;Ll;0;R;;;;;N;;;10C92;;10C92
+10CD3;OLD HUNGARIAN SMALL LETTER EK;Ll;0;R;;;;;N;;;10C93;;10C93
+10CD4;OLD HUNGARIAN SMALL LETTER AK;Ll;0;R;;;;;N;;;10C94;;10C94
+10CD5;OLD HUNGARIAN SMALL LETTER UNK;Ll;0;R;;;;;N;;;10C95;;10C95
+10CD6;OLD HUNGARIAN SMALL LETTER EL;Ll;0;R;;;;;N;;;10C96;;10C96
+10CD7;OLD HUNGARIAN SMALL LETTER ELY;Ll;0;R;;;;;N;;;10C97;;10C97
+10CD8;OLD HUNGARIAN SMALL LETTER EM;Ll;0;R;;;;;N;;;10C98;;10C98
+10CD9;OLD HUNGARIAN SMALL LETTER EN;Ll;0;R;;;;;N;;;10C99;;10C99
+10CDA;OLD HUNGARIAN SMALL LETTER ENY;Ll;0;R;;;;;N;;;10C9A;;10C9A
+10CDB;OLD HUNGARIAN SMALL LETTER O;Ll;0;R;;;;;N;;;10C9B;;10C9B
+10CDC;OLD HUNGARIAN SMALL LETTER OO;Ll;0;R;;;;;N;;;10C9C;;10C9C
+10CDD;OLD HUNGARIAN SMALL LETTER NIKOLSBURG OE;Ll;0;R;;;;;N;;;10C9D;;10C9D
+10CDE;OLD HUNGARIAN SMALL LETTER RUDIMENTA OE;Ll;0;R;;;;;N;;;10C9E;;10C9E
+10CDF;OLD HUNGARIAN SMALL LETTER OEE;Ll;0;R;;;;;N;;;10C9F;;10C9F
+10CE0;OLD HUNGARIAN SMALL LETTER EP;Ll;0;R;;;;;N;;;10CA0;;10CA0
+10CE1;OLD HUNGARIAN SMALL LETTER EMP;Ll;0;R;;;;;N;;;10CA1;;10CA1
+10CE2;OLD HUNGARIAN SMALL LETTER ER;Ll;0;R;;;;;N;;;10CA2;;10CA2
+10CE3;OLD HUNGARIAN SMALL LETTER SHORT ER;Ll;0;R;;;;;N;;;10CA3;;10CA3
+10CE4;OLD HUNGARIAN SMALL LETTER ES;Ll;0;R;;;;;N;;;10CA4;;10CA4
+10CE5;OLD HUNGARIAN SMALL LETTER ESZ;Ll;0;R;;;;;N;;;10CA5;;10CA5
+10CE6;OLD HUNGARIAN SMALL LETTER ET;Ll;0;R;;;;;N;;;10CA6;;10CA6
+10CE7;OLD HUNGARIAN SMALL LETTER ENT;Ll;0;R;;;;;N;;;10CA7;;10CA7
+10CE8;OLD HUNGARIAN SMALL LETTER ETY;Ll;0;R;;;;;N;;;10CA8;;10CA8
+10CE9;OLD HUNGARIAN SMALL LETTER ECH;Ll;0;R;;;;;N;;;10CA9;;10CA9
+10CEA;OLD HUNGARIAN SMALL LETTER U;Ll;0;R;;;;;N;;;10CAA;;10CAA
+10CEB;OLD HUNGARIAN SMALL LETTER UU;Ll;0;R;;;;;N;;;10CAB;;10CAB
+10CEC;OLD HUNGARIAN SMALL LETTER NIKOLSBURG UE;Ll;0;R;;;;;N;;;10CAC;;10CAC
+10CED;OLD HUNGARIAN SMALL LETTER RUDIMENTA UE;Ll;0;R;;;;;N;;;10CAD;;10CAD
+10CEE;OLD HUNGARIAN SMALL LETTER EV;Ll;0;R;;;;;N;;;10CAE;;10CAE
+10CEF;OLD HUNGARIAN SMALL LETTER EZ;Ll;0;R;;;;;N;;;10CAF;;10CAF
+10CF0;OLD HUNGARIAN SMALL LETTER EZS;Ll;0;R;;;;;N;;;10CB0;;10CB0
+10CF1;OLD HUNGARIAN SMALL LETTER ENT-SHAPED SIGN;Ll;0;R;;;;;N;;;10CB1;;10CB1
+10CF2;OLD HUNGARIAN SMALL LETTER US;Ll;0;R;;;;;N;;;10CB2;;10CB2
+10CFA;OLD HUNGARIAN NUMBER ONE;No;0;R;;;;1;N;;;;;
+10CFB;OLD HUNGARIAN NUMBER FIVE;No;0;R;;;;5;N;;;;;
+10CFC;OLD HUNGARIAN NUMBER TEN;No;0;R;;;;10;N;;;;;
+10CFD;OLD HUNGARIAN NUMBER FIFTY;No;0;R;;;;50;N;;;;;
+10CFE;OLD HUNGARIAN NUMBER ONE HUNDRED;No;0;R;;;;100;N;;;;;
+10CFF;OLD HUNGARIAN NUMBER ONE THOUSAND;No;0;R;;;;1000;N;;;;;
+10E60;RUMI DIGIT ONE;No;0;AN;;;1;1;N;;;;;
+10E61;RUMI DIGIT TWO;No;0;AN;;;2;2;N;;;;;
+10E62;RUMI DIGIT THREE;No;0;AN;;;3;3;N;;;;;
+10E63;RUMI DIGIT FOUR;No;0;AN;;;4;4;N;;;;;
+10E64;RUMI DIGIT FIVE;No;0;AN;;;5;5;N;;;;;
+10E65;RUMI DIGIT SIX;No;0;AN;;;6;6;N;;;;;
+10E66;RUMI DIGIT SEVEN;No;0;AN;;;7;7;N;;;;;
+10E67;RUMI DIGIT EIGHT;No;0;AN;;;8;8;N;;;;;
+10E68;RUMI DIGIT NINE;No;0;AN;;;9;9;N;;;;;
+10E69;RUMI NUMBER TEN;No;0;AN;;;;10;N;;;;;
+10E6A;RUMI NUMBER TWENTY;No;0;AN;;;;20;N;;;;;
+10E6B;RUMI NUMBER THIRTY;No;0;AN;;;;30;N;;;;;
+10E6C;RUMI NUMBER FORTY;No;0;AN;;;;40;N;;;;;
+10E6D;RUMI NUMBER FIFTY;No;0;AN;;;;50;N;;;;;
+10E6E;RUMI NUMBER SIXTY;No;0;AN;;;;60;N;;;;;
+10E6F;RUMI NUMBER SEVENTY;No;0;AN;;;;70;N;;;;;
+10E70;RUMI NUMBER EIGHTY;No;0;AN;;;;80;N;;;;;
+10E71;RUMI NUMBER NINETY;No;0;AN;;;;90;N;;;;;
+10E72;RUMI NUMBER ONE HUNDRED;No;0;AN;;;;100;N;;;;;
+10E73;RUMI NUMBER TWO HUNDRED;No;0;AN;;;;200;N;;;;;
+10E74;RUMI NUMBER THREE HUNDRED;No;0;AN;;;;300;N;;;;;
+10E75;RUMI NUMBER FOUR HUNDRED;No;0;AN;;;;400;N;;;;;
+10E76;RUMI NUMBER FIVE HUNDRED;No;0;AN;;;;500;N;;;;;
+10E77;RUMI NUMBER SIX HUNDRED;No;0;AN;;;;600;N;;;;;
+10E78;RUMI NUMBER SEVEN HUNDRED;No;0;AN;;;;700;N;;;;;
+10E79;RUMI NUMBER EIGHT HUNDRED;No;0;AN;;;;800;N;;;;;
+10E7A;RUMI NUMBER NINE HUNDRED;No;0;AN;;;;900;N;;;;;
+10E7B;RUMI FRACTION ONE HALF;No;0;AN;;;;1/2;N;;;;;
+10E7C;RUMI FRACTION ONE QUARTER;No;0;AN;;;;1/4;N;;;;;
+10E7D;RUMI FRACTION ONE THIRD;No;0;AN;;;;1/3;N;;;;;
+10E7E;RUMI FRACTION TWO THIRDS;No;0;AN;;;;2/3;N;;;;;
+11000;BRAHMI SIGN CANDRABINDU;Mc;0;L;;;;;N;;;;;
+11001;BRAHMI SIGN ANUSVARA;Mn;0;NSM;;;;;N;;;;;
+11002;BRAHMI SIGN VISARGA;Mc;0;L;;;;;N;;;;;
+11003;BRAHMI SIGN JIHVAMULIYA;Lo;0;L;;;;;N;;;;;
+11004;BRAHMI SIGN UPADHMANIYA;Lo;0;L;;;;;N;;;;;
+11005;BRAHMI LETTER A;Lo;0;L;;;;;N;;;;;
+11006;BRAHMI LETTER AA;Lo;0;L;;;;;N;;;;;
+11007;BRAHMI LETTER I;Lo;0;L;;;;;N;;;;;
+11008;BRAHMI LETTER II;Lo;0;L;;;;;N;;;;;
+11009;BRAHMI LETTER U;Lo;0;L;;;;;N;;;;;
+1100A;BRAHMI LETTER UU;Lo;0;L;;;;;N;;;;;
+1100B;BRAHMI LETTER VOCALIC R;Lo;0;L;;;;;N;;;;;
+1100C;BRAHMI LETTER VOCALIC RR;Lo;0;L;;;;;N;;;;;
+1100D;BRAHMI LETTER VOCALIC L;Lo;0;L;;;;;N;;;;;
+1100E;BRAHMI LETTER VOCALIC LL;Lo;0;L;;;;;N;;;;;
+1100F;BRAHMI LETTER E;Lo;0;L;;;;;N;;;;;
+11010;BRAHMI LETTER AI;Lo;0;L;;;;;N;;;;;
+11011;BRAHMI LETTER O;Lo;0;L;;;;;N;;;;;
+11012;BRAHMI LETTER AU;Lo;0;L;;;;;N;;;;;
+11013;BRAHMI LETTER KA;Lo;0;L;;;;;N;;;;;
+11014;BRAHMI LETTER KHA;Lo;0;L;;;;;N;;;;;
+11015;BRAHMI LETTER GA;Lo;0;L;;;;;N;;;;;
+11016;BRAHMI LETTER GHA;Lo;0;L;;;;;N;;;;;
+11017;BRAHMI LETTER NGA;Lo;0;L;;;;;N;;;;;
+11018;BRAHMI LETTER CA;Lo;0;L;;;;;N;;;;;
+11019;BRAHMI LETTER CHA;Lo;0;L;;;;;N;;;;;
+1101A;BRAHMI LETTER JA;Lo;0;L;;;;;N;;;;;
+1101B;BRAHMI LETTER JHA;Lo;0;L;;;;;N;;;;;
+1101C;BRAHMI LETTER NYA;Lo;0;L;;;;;N;;;;;
+1101D;BRAHMI LETTER TTA;Lo;0;L;;;;;N;;;;;
+1101E;BRAHMI LETTER TTHA;Lo;0;L;;;;;N;;;;;
+1101F;BRAHMI LETTER DDA;Lo;0;L;;;;;N;;;;;
+11020;BRAHMI LETTER DDHA;Lo;0;L;;;;;N;;;;;
+11021;BRAHMI LETTER NNA;Lo;0;L;;;;;N;;;;;
+11022;BRAHMI LETTER TA;Lo;0;L;;;;;N;;;;;
+11023;BRAHMI LETTER THA;Lo;0;L;;;;;N;;;;;
+11024;BRAHMI LETTER DA;Lo;0;L;;;;;N;;;;;
+11025;BRAHMI LETTER DHA;Lo;0;L;;;;;N;;;;;
+11026;BRAHMI LETTER NA;Lo;0;L;;;;;N;;;;;
+11027;BRAHMI LETTER PA;Lo;0;L;;;;;N;;;;;
+11028;BRAHMI LETTER PHA;Lo;0;L;;;;;N;;;;;
+11029;BRAHMI LETTER BA;Lo;0;L;;;;;N;;;;;
+1102A;BRAHMI LETTER BHA;Lo;0;L;;;;;N;;;;;
+1102B;BRAHMI LETTER MA;Lo;0;L;;;;;N;;;;;
+1102C;BRAHMI LETTER YA;Lo;0;L;;;;;N;;;;;
+1102D;BRAHMI LETTER RA;Lo;0;L;;;;;N;;;;;
+1102E;BRAHMI LETTER LA;Lo;0;L;;;;;N;;;;;
+1102F;BRAHMI LETTER VA;Lo;0;L;;;;;N;;;;;
+11030;BRAHMI LETTER SHA;Lo;0;L;;;;;N;;;;;
+11031;BRAHMI LETTER SSA;Lo;0;L;;;;;N;;;;;
+11032;BRAHMI LETTER SA;Lo;0;L;;;;;N;;;;;
+11033;BRAHMI LETTER HA;Lo;0;L;;;;;N;;;;;
+11034;BRAHMI LETTER LLA;Lo;0;L;;;;;N;;;;;
+11035;BRAHMI LETTER OLD TAMIL LLLA;Lo;0;L;;;;;N;;;;;
+11036;BRAHMI LETTER OLD TAMIL RRA;Lo;0;L;;;;;N;;;;;
+11037;BRAHMI LETTER OLD TAMIL NNNA;Lo;0;L;;;;;N;;;;;
+11038;BRAHMI VOWEL SIGN AA;Mn;0;NSM;;;;;N;;;;;
+11039;BRAHMI VOWEL SIGN BHATTIPROLU AA;Mn;0;NSM;;;;;N;;;;;
+1103A;BRAHMI VOWEL SIGN I;Mn;0;NSM;;;;;N;;;;;
+1103B;BRAHMI VOWEL SIGN II;Mn;0;NSM;;;;;N;;;;;
+1103C;BRAHMI VOWEL SIGN U;Mn;0;NSM;;;;;N;;;;;
+1103D;BRAHMI VOWEL SIGN UU;Mn;0;NSM;;;;;N;;;;;
+1103E;BRAHMI VOWEL SIGN VOCALIC R;Mn;0;NSM;;;;;N;;;;;
+1103F;BRAHMI VOWEL SIGN VOCALIC RR;Mn;0;NSM;;;;;N;;;;;
+11040;BRAHMI VOWEL SIGN VOCALIC L;Mn;0;NSM;;;;;N;;;;;
+11041;BRAHMI VOWEL SIGN VOCALIC LL;Mn;0;NSM;;;;;N;;;;;
+11042;BRAHMI VOWEL SIGN E;Mn;0;NSM;;;;;N;;;;;
+11043;BRAHMI VOWEL SIGN AI;Mn;0;NSM;;;;;N;;;;;
+11044;BRAHMI VOWEL SIGN O;Mn;0;NSM;;;;;N;;;;;
+11045;BRAHMI VOWEL SIGN AU;Mn;0;NSM;;;;;N;;;;;
+11046;BRAHMI VIRAMA;Mn;9;NSM;;;;;N;;;;;
+11047;BRAHMI DANDA;Po;0;L;;;;;N;;;;;
+11048;BRAHMI DOUBLE DANDA;Po;0;L;;;;;N;;;;;
+11049;BRAHMI PUNCTUATION DOT;Po;0;L;;;;;N;;;;;
+1104A;BRAHMI PUNCTUATION DOUBLE DOT;Po;0;L;;;;;N;;;;;
+1104B;BRAHMI PUNCTUATION LINE;Po;0;L;;;;;N;;;;;
+1104C;BRAHMI PUNCTUATION CRESCENT BAR;Po;0;L;;;;;N;;;;;
+1104D;BRAHMI PUNCTUATION LOTUS;Po;0;L;;;;;N;;;;;
+11052;BRAHMI NUMBER ONE;No;0;ON;;;1;1;N;;;;;
+11053;BRAHMI NUMBER TWO;No;0;ON;;;2;2;N;;;;;
+11054;BRAHMI NUMBER THREE;No;0;ON;;;3;3;N;;;;;
+11055;BRAHMI NUMBER FOUR;No;0;ON;;;4;4;N;;;;;
+11056;BRAHMI NUMBER FIVE;No;0;ON;;;5;5;N;;;;;
+11057;BRAHMI NUMBER SIX;No;0;ON;;;6;6;N;;;;;
+11058;BRAHMI NUMBER SEVEN;No;0;ON;;;7;7;N;;;;;
+11059;BRAHMI NUMBER EIGHT;No;0;ON;;;8;8;N;;;;;
+1105A;BRAHMI NUMBER NINE;No;0;ON;;;9;9;N;;;;;
+1105B;BRAHMI NUMBER TEN;No;0;ON;;;;10;N;;;;;
+1105C;BRAHMI NUMBER TWENTY;No;0;ON;;;;20;N;;;;;
+1105D;BRAHMI NUMBER THIRTY;No;0;ON;;;;30;N;;;;;
+1105E;BRAHMI NUMBER FORTY;No;0;ON;;;;40;N;;;;;
+1105F;BRAHMI NUMBER FIFTY;No;0;ON;;;;50;N;;;;;
+11060;BRAHMI NUMBER SIXTY;No;0;ON;;;;60;N;;;;;
+11061;BRAHMI NUMBER SEVENTY;No;0;ON;;;;70;N;;;;;
+11062;BRAHMI NUMBER EIGHTY;No;0;ON;;;;80;N;;;;;
+11063;BRAHMI NUMBER NINETY;No;0;ON;;;;90;N;;;;;
+11064;BRAHMI NUMBER ONE HUNDRED;No;0;ON;;;;100;N;;;;;
+11065;BRAHMI NUMBER ONE THOUSAND;No;0;ON;;;;1000;N;;;;;
+11066;BRAHMI DIGIT ZERO;Nd;0;L;;0;0;0;N;;;;;
+11067;BRAHMI DIGIT ONE;Nd;0;L;;1;1;1;N;;;;;
+11068;BRAHMI DIGIT TWO;Nd;0;L;;2;2;2;N;;;;;
+11069;BRAHMI DIGIT THREE;Nd;0;L;;3;3;3;N;;;;;
+1106A;BRAHMI DIGIT FOUR;Nd;0;L;;4;4;4;N;;;;;
+1106B;BRAHMI DIGIT FIVE;Nd;0;L;;5;5;5;N;;;;;
+1106C;BRAHMI DIGIT SIX;Nd;0;L;;6;6;6;N;;;;;
+1106D;BRAHMI DIGIT SEVEN;Nd;0;L;;7;7;7;N;;;;;
+1106E;BRAHMI DIGIT EIGHT;Nd;0;L;;8;8;8;N;;;;;
+1106F;BRAHMI DIGIT NINE;Nd;0;L;;9;9;9;N;;;;;
+1107F;BRAHMI NUMBER JOINER;Mn;9;NSM;;;;;N;;;;;
+11080;KAITHI SIGN CANDRABINDU;Mn;0;NSM;;;;;N;;;;;
+11081;KAITHI SIGN ANUSVARA;Mn;0;NSM;;;;;N;;;;;
+11082;KAITHI SIGN VISARGA;Mc;0;L;;;;;N;;;;;
+11083;KAITHI LETTER A;Lo;0;L;;;;;N;;;;;
+11084;KAITHI LETTER AA;Lo;0;L;;;;;N;;;;;
+11085;KAITHI LETTER I;Lo;0;L;;;;;N;;;;;
+11086;KAITHI LETTER II;Lo;0;L;;;;;N;;;;;
+11087;KAITHI LETTER U;Lo;0;L;;;;;N;;;;;
+11088;KAITHI LETTER UU;Lo;0;L;;;;;N;;;;;
+11089;KAITHI LETTER E;Lo;0;L;;;;;N;;;;;
+1108A;KAITHI LETTER AI;Lo;0;L;;;;;N;;;;;
+1108B;KAITHI LETTER O;Lo;0;L;;;;;N;;;;;
+1108C;KAITHI LETTER AU;Lo;0;L;;;;;N;;;;;
+1108D;KAITHI LETTER KA;Lo;0;L;;;;;N;;;;;
+1108E;KAITHI LETTER KHA;Lo;0;L;;;;;N;;;;;
+1108F;KAITHI LETTER GA;Lo;0;L;;;;;N;;;;;
+11090;KAITHI LETTER GHA;Lo;0;L;;;;;N;;;;;
+11091;KAITHI LETTER NGA;Lo;0;L;;;;;N;;;;;
+11092;KAITHI LETTER CA;Lo;0;L;;;;;N;;;;;
+11093;KAITHI LETTER CHA;Lo;0;L;;;;;N;;;;;
+11094;KAITHI LETTER JA;Lo;0;L;;;;;N;;;;;
+11095;KAITHI LETTER JHA;Lo;0;L;;;;;N;;;;;
+11096;KAITHI LETTER NYA;Lo;0;L;;;;;N;;;;;
+11097;KAITHI LETTER TTA;Lo;0;L;;;;;N;;;;;
+11098;KAITHI LETTER TTHA;Lo;0;L;;;;;N;;;;;
+11099;KAITHI LETTER DDA;Lo;0;L;;;;;N;;;;;
+1109A;KAITHI LETTER DDDHA;Lo;0;L;11099 110BA;;;;N;;;;;
+1109B;KAITHI LETTER DDHA;Lo;0;L;;;;;N;;;;;
+1109C;KAITHI LETTER RHA;Lo;0;L;1109B 110BA;;;;N;;;;;
+1109D;KAITHI LETTER NNA;Lo;0;L;;;;;N;;;;;
+1109E;KAITHI LETTER TA;Lo;0;L;;;;;N;;;;;
+1109F;KAITHI LETTER THA;Lo;0;L;;;;;N;;;;;
+110A0;KAITHI LETTER DA;Lo;0;L;;;;;N;;;;;
+110A1;KAITHI LETTER DHA;Lo;0;L;;;;;N;;;;;
+110A2;KAITHI LETTER NA;Lo;0;L;;;;;N;;;;;
+110A3;KAITHI LETTER PA;Lo;0;L;;;;;N;;;;;
+110A4;KAITHI LETTER PHA;Lo;0;L;;;;;N;;;;;
+110A5;KAITHI LETTER BA;Lo;0;L;;;;;N;;;;;
+110A6;KAITHI LETTER BHA;Lo;0;L;;;;;N;;;;;
+110A7;KAITHI LETTER MA;Lo;0;L;;;;;N;;;;;
+110A8;KAITHI LETTER YA;Lo;0;L;;;;;N;;;;;
+110A9;KAITHI LETTER RA;Lo;0;L;;;;;N;;;;;
+110AA;KAITHI LETTER LA;Lo;0;L;;;;;N;;;;;
+110AB;KAITHI LETTER VA;Lo;0;L;110A5 110BA;;;;N;;;;;
+110AC;KAITHI LETTER SHA;Lo;0;L;;;;;N;;;;;
+110AD;KAITHI LETTER SSA;Lo;0;L;;;;;N;;;;;
+110AE;KAITHI LETTER SA;Lo;0;L;;;;;N;;;;;
+110AF;KAITHI LETTER HA;Lo;0;L;;;;;N;;;;;
+110B0;KAITHI VOWEL SIGN AA;Mc;0;L;;;;;N;;;;;
+110B1;KAITHI VOWEL SIGN I;Mc;0;L;;;;;N;;;;;
+110B2;KAITHI VOWEL SIGN II;Mc;0;L;;;;;N;;;;;
+110B3;KAITHI VOWEL SIGN U;Mn;0;NSM;;;;;N;;;;;
+110B4;KAITHI VOWEL SIGN UU;Mn;0;NSM;;;;;N;;;;;
+110B5;KAITHI VOWEL SIGN E;Mn;0;NSM;;;;;N;;;;;
+110B6;KAITHI VOWEL SIGN AI;Mn;0;NSM;;;;;N;;;;;
+110B7;KAITHI VOWEL SIGN O;Mc;0;L;;;;;N;;;;;
+110B8;KAITHI VOWEL SIGN AU;Mc;0;L;;;;;N;;;;;
+110B9;KAITHI SIGN VIRAMA;Mn;9;NSM;;;;;N;;;;;
+110BA;KAITHI SIGN NUKTA;Mn;7;NSM;;;;;N;;;;;
+110BB;KAITHI ABBREVIATION SIGN;Po;0;L;;;;;N;;;;;
+110BC;KAITHI ENUMERATION SIGN;Po;0;L;;;;;N;;;;;
+110BD;KAITHI NUMBER SIGN;Cf;0;L;;;;;N;;;;;
+110BE;KAITHI SECTION MARK;Po;0;L;;;;;N;;;;;
+110BF;KAITHI DOUBLE SECTION MARK;Po;0;L;;;;;N;;;;;
+110C0;KAITHI DANDA;Po;0;L;;;;;N;;;;;
+110C1;KAITHI DOUBLE DANDA;Po;0;L;;;;;N;;;;;
+110D0;SORA SOMPENG LETTER SAH;Lo;0;L;;;;;N;;;;;
+110D1;SORA SOMPENG LETTER TAH;Lo;0;L;;;;;N;;;;;
+110D2;SORA SOMPENG LETTER BAH;Lo;0;L;;;;;N;;;;;
+110D3;SORA SOMPENG LETTER CAH;Lo;0;L;;;;;N;;;;;
+110D4;SORA SOMPENG LETTER DAH;Lo;0;L;;;;;N;;;;;
+110D5;SORA SOMPENG LETTER GAH;Lo;0;L;;;;;N;;;;;
+110D6;SORA SOMPENG LETTER MAH;Lo;0;L;;;;;N;;;;;
+110D7;SORA SOMPENG LETTER NGAH;Lo;0;L;;;;;N;;;;;
+110D8;SORA SOMPENG LETTER LAH;Lo;0;L;;;;;N;;;;;
+110D9;SORA SOMPENG LETTER NAH;Lo;0;L;;;;;N;;;;;
+110DA;SORA SOMPENG LETTER VAH;Lo;0;L;;;;;N;;;;;
+110DB;SORA SOMPENG LETTER PAH;Lo;0;L;;;;;N;;;;;
+110DC;SORA SOMPENG LETTER YAH;Lo;0;L;;;;;N;;;;;
+110DD;SORA SOMPENG LETTER RAH;Lo;0;L;;;;;N;;;;;
+110DE;SORA SOMPENG LETTER HAH;Lo;0;L;;;;;N;;;;;
+110DF;SORA SOMPENG LETTER KAH;Lo;0;L;;;;;N;;;;;
+110E0;SORA SOMPENG LETTER JAH;Lo;0;L;;;;;N;;;;;
+110E1;SORA SOMPENG LETTER NYAH;Lo;0;L;;;;;N;;;;;
+110E2;SORA SOMPENG LETTER AH;Lo;0;L;;;;;N;;;;;
+110E3;SORA SOMPENG LETTER EEH;Lo;0;L;;;;;N;;;;;
+110E4;SORA SOMPENG LETTER IH;Lo;0;L;;;;;N;;;;;
+110E5;SORA SOMPENG LETTER UH;Lo;0;L;;;;;N;;;;;
+110E6;SORA SOMPENG LETTER OH;Lo;0;L;;;;;N;;;;;
+110E7;SORA SOMPENG LETTER EH;Lo;0;L;;;;;N;;;;;
+110E8;SORA SOMPENG LETTER MAE;Lo;0;L;;;;;N;;;;;
+110F0;SORA SOMPENG DIGIT ZERO;Nd;0;L;;0;0;0;N;;;;;
+110F1;SORA SOMPENG DIGIT ONE;Nd;0;L;;1;1;1;N;;;;;
+110F2;SORA SOMPENG DIGIT TWO;Nd;0;L;;2;2;2;N;;;;;
+110F3;SORA SOMPENG DIGIT THREE;Nd;0;L;;3;3;3;N;;;;;
+110F4;SORA SOMPENG DIGIT FOUR;Nd;0;L;;4;4;4;N;;;;;
+110F5;SORA SOMPENG DIGIT FIVE;Nd;0;L;;5;5;5;N;;;;;
+110F6;SORA SOMPENG DIGIT SIX;Nd;0;L;;6;6;6;N;;;;;
+110F7;SORA SOMPENG DIGIT SEVEN;Nd;0;L;;7;7;7;N;;;;;
+110F8;SORA SOMPENG DIGIT EIGHT;Nd;0;L;;8;8;8;N;;;;;
+110F9;SORA SOMPENG DIGIT NINE;Nd;0;L;;9;9;9;N;;;;;
+11100;CHAKMA SIGN CANDRABINDU;Mn;230;NSM;;;;;N;;;;;
+11101;CHAKMA SIGN ANUSVARA;Mn;230;NSM;;;;;N;;;;;
+11102;CHAKMA SIGN VISARGA;Mn;230;NSM;;;;;N;;;;;
+11103;CHAKMA LETTER AA;Lo;0;L;;;;;N;;;;;
+11104;CHAKMA LETTER I;Lo;0;L;;;;;N;;;;;
+11105;CHAKMA LETTER U;Lo;0;L;;;;;N;;;;;
+11106;CHAKMA LETTER E;Lo;0;L;;;;;N;;;;;
+11107;CHAKMA LETTER KAA;Lo;0;L;;;;;N;;;;;
+11108;CHAKMA LETTER KHAA;Lo;0;L;;;;;N;;;;;
+11109;CHAKMA LETTER GAA;Lo;0;L;;;;;N;;;;;
+1110A;CHAKMA LETTER GHAA;Lo;0;L;;;;;N;;;;;
+1110B;CHAKMA LETTER NGAA;Lo;0;L;;;;;N;;;;;
+1110C;CHAKMA LETTER CAA;Lo;0;L;;;;;N;;;;;
+1110D;CHAKMA LETTER CHAA;Lo;0;L;;;;;N;;;;;
+1110E;CHAKMA LETTER JAA;Lo;0;L;;;;;N;;;;;
+1110F;CHAKMA LETTER JHAA;Lo;0;L;;;;;N;;;;;
+11110;CHAKMA LETTER NYAA;Lo;0;L;;;;;N;;;;;
+11111;CHAKMA LETTER TTAA;Lo;0;L;;;;;N;;;;;
+11112;CHAKMA LETTER TTHAA;Lo;0;L;;;;;N;;;;;
+11113;CHAKMA LETTER DDAA;Lo;0;L;;;;;N;;;;;
+11114;CHAKMA LETTER DDHAA;Lo;0;L;;;;;N;;;;;
+11115;CHAKMA LETTER NNAA;Lo;0;L;;;;;N;;;;;
+11116;CHAKMA LETTER TAA;Lo;0;L;;;;;N;;;;;
+11117;CHAKMA LETTER THAA;Lo;0;L;;;;;N;;;;;
+11118;CHAKMA LETTER DAA;Lo;0;L;;;;;N;;;;;
+11119;CHAKMA LETTER DHAA;Lo;0;L;;;;;N;;;;;
+1111A;CHAKMA LETTER NAA;Lo;0;L;;;;;N;;;;;
+1111B;CHAKMA LETTER PAA;Lo;0;L;;;;;N;;;;;
+1111C;CHAKMA LETTER PHAA;Lo;0;L;;;;;N;;;;;
+1111D;CHAKMA LETTER BAA;Lo;0;L;;;;;N;;;;;
+1111E;CHAKMA LETTER BHAA;Lo;0;L;;;;;N;;;;;
+1111F;CHAKMA LETTER MAA;Lo;0;L;;;;;N;;;;;
+11120;CHAKMA LETTER YYAA;Lo;0;L;;;;;N;;;;;
+11121;CHAKMA LETTER YAA;Lo;0;L;;;;;N;;;;;
+11122;CHAKMA LETTER RAA;Lo;0;L;;;;;N;;;;;
+11123;CHAKMA LETTER LAA;Lo;0;L;;;;;N;;;;;
+11124;CHAKMA LETTER WAA;Lo;0;L;;;;;N;;;;;
+11125;CHAKMA LETTER SAA;Lo;0;L;;;;;N;;;;;
+11126;CHAKMA LETTER HAA;Lo;0;L;;;;;N;;;;;
+11127;CHAKMA VOWEL SIGN A;Mn;0;NSM;;;;;N;;;;;
+11128;CHAKMA VOWEL SIGN I;Mn;0;NSM;;;;;N;;;;;
+11129;CHAKMA VOWEL SIGN II;Mn;0;NSM;;;;;N;;;;;
+1112A;CHAKMA VOWEL SIGN U;Mn;0;NSM;;;;;N;;;;;
+1112B;CHAKMA VOWEL SIGN UU;Mn;0;NSM;;;;;N;;;;;
+1112C;CHAKMA VOWEL SIGN E;Mc;0;L;;;;;N;;;;;
+1112D;CHAKMA VOWEL SIGN AI;Mn;0;NSM;;;;;N;;;;;
+1112E;CHAKMA VOWEL SIGN O;Mn;0;NSM;11131 11127;;;;N;;;;;
+1112F;CHAKMA VOWEL SIGN AU;Mn;0;NSM;11132 11127;;;;N;;;;;
+11130;CHAKMA VOWEL SIGN OI;Mn;0;NSM;;;;;N;;;;;
+11131;CHAKMA O MARK;Mn;0;NSM;;;;;N;;;;;
+11132;CHAKMA AU MARK;Mn;0;NSM;;;;;N;;;;;
+11133;CHAKMA VIRAMA;Mn;9;NSM;;;;;N;;;;;
+11134;CHAKMA MAAYYAA;Mn;9;NSM;;;;;N;;;;;
+11136;CHAKMA DIGIT ZERO;Nd;0;L;;0;0;0;N;;;;;
+11137;CHAKMA DIGIT ONE;Nd;0;L;;1;1;1;N;;;;;
+11138;CHAKMA DIGIT TWO;Nd;0;L;;2;2;2;N;;;;;
+11139;CHAKMA DIGIT THREE;Nd;0;L;;3;3;3;N;;;;;
+1113A;CHAKMA DIGIT FOUR;Nd;0;L;;4;4;4;N;;;;;
+1113B;CHAKMA DIGIT FIVE;Nd;0;L;;5;5;5;N;;;;;
+1113C;CHAKMA DIGIT SIX;Nd;0;L;;6;6;6;N;;;;;
+1113D;CHAKMA DIGIT SEVEN;Nd;0;L;;7;7;7;N;;;;;
+1113E;CHAKMA DIGIT EIGHT;Nd;0;L;;8;8;8;N;;;;;
+1113F;CHAKMA DIGIT NINE;Nd;0;L;;9;9;9;N;;;;;
+11140;CHAKMA SECTION MARK;Po;0;L;;;;;N;;;;;
+11141;CHAKMA DANDA;Po;0;L;;;;;N;;;;;
+11142;CHAKMA DOUBLE DANDA;Po;0;L;;;;;N;;;;;
+11143;CHAKMA QUESTION MARK;Po;0;L;;;;;N;;;;;
+11150;MAHAJANI LETTER A;Lo;0;L;;;;;N;;;;;
+11151;MAHAJANI LETTER I;Lo;0;L;;;;;N;;;;;
+11152;MAHAJANI LETTER U;Lo;0;L;;;;;N;;;;;
+11153;MAHAJANI LETTER E;Lo;0;L;;;;;N;;;;;
+11154;MAHAJANI LETTER O;Lo;0;L;;;;;N;;;;;
+11155;MAHAJANI LETTER KA;Lo;0;L;;;;;N;;;;;
+11156;MAHAJANI LETTER KHA;Lo;0;L;;;;;N;;;;;
+11157;MAHAJANI LETTER GA;Lo;0;L;;;;;N;;;;;
+11158;MAHAJANI LETTER GHA;Lo;0;L;;;;;N;;;;;
+11159;MAHAJANI LETTER CA;Lo;0;L;;;;;N;;;;;
+1115A;MAHAJANI LETTER CHA;Lo;0;L;;;;;N;;;;;
+1115B;MAHAJANI LETTER JA;Lo;0;L;;;;;N;;;;;
+1115C;MAHAJANI LETTER JHA;Lo;0;L;;;;;N;;;;;
+1115D;MAHAJANI LETTER NYA;Lo;0;L;;;;;N;;;;;
+1115E;MAHAJANI LETTER TTA;Lo;0;L;;;;;N;;;;;
+1115F;MAHAJANI LETTER TTHA;Lo;0;L;;;;;N;;;;;
+11160;MAHAJANI LETTER DDA;Lo;0;L;;;;;N;;;;;
+11161;MAHAJANI LETTER DDHA;Lo;0;L;;;;;N;;;;;
+11162;MAHAJANI LETTER NNA;Lo;0;L;;;;;N;;;;;
+11163;MAHAJANI LETTER TA;Lo;0;L;;;;;N;;;;;
+11164;MAHAJANI LETTER THA;Lo;0;L;;;;;N;;;;;
+11165;MAHAJANI LETTER DA;Lo;0;L;;;;;N;;;;;
+11166;MAHAJANI LETTER DHA;Lo;0;L;;;;;N;;;;;
+11167;MAHAJANI LETTER NA;Lo;0;L;;;;;N;;;;;
+11168;MAHAJANI LETTER PA;Lo;0;L;;;;;N;;;;;
+11169;MAHAJANI LETTER PHA;Lo;0;L;;;;;N;;;;;
+1116A;MAHAJANI LETTER BA;Lo;0;L;;;;;N;;;;;
+1116B;MAHAJANI LETTER BHA;Lo;0;L;;;;;N;;;;;
+1116C;MAHAJANI LETTER MA;Lo;0;L;;;;;N;;;;;
+1116D;MAHAJANI LETTER RA;Lo;0;L;;;;;N;;;;;
+1116E;MAHAJANI LETTER LA;Lo;0;L;;;;;N;;;;;
+1116F;MAHAJANI LETTER VA;Lo;0;L;;;;;N;;;;;
+11170;MAHAJANI LETTER SA;Lo;0;L;;;;;N;;;;;
+11171;MAHAJANI LETTER HA;Lo;0;L;;;;;N;;;;;
+11172;MAHAJANI LETTER RRA;Lo;0;L;;;;;N;;;;;
+11173;MAHAJANI SIGN NUKTA;Mn;7;NSM;;;;;N;;;;;
+11174;MAHAJANI ABBREVIATION SIGN;Po;0;L;;;;;N;;;;;
+11175;MAHAJANI SECTION MARK;Po;0;L;;;;;N;;;;;
+11176;MAHAJANI LIGATURE SHRI;Lo;0;L;;;;;N;;;;;
+11180;SHARADA SIGN CANDRABINDU;Mn;0;NSM;;;;;N;;;;;
+11181;SHARADA SIGN ANUSVARA;Mn;0;NSM;;;;;N;;;;;
+11182;SHARADA SIGN VISARGA;Mc;0;L;;;;;N;;;;;
+11183;SHARADA LETTER A;Lo;0;L;;;;;N;;;;;
+11184;SHARADA LETTER AA;Lo;0;L;;;;;N;;;;;
+11185;SHARADA LETTER I;Lo;0;L;;;;;N;;;;;
+11186;SHARADA LETTER II;Lo;0;L;;;;;N;;;;;
+11187;SHARADA LETTER U;Lo;0;L;;;;;N;;;;;
+11188;SHARADA LETTER UU;Lo;0;L;;;;;N;;;;;
+11189;SHARADA LETTER VOCALIC R;Lo;0;L;;;;;N;;;;;
+1118A;SHARADA LETTER VOCALIC RR;Lo;0;L;;;;;N;;;;;
+1118B;SHARADA LETTER VOCALIC L;Lo;0;L;;;;;N;;;;;
+1118C;SHARADA LETTER VOCALIC LL;Lo;0;L;;;;;N;;;;;
+1118D;SHARADA LETTER E;Lo;0;L;;;;;N;;;;;
+1118E;SHARADA LETTER AI;Lo;0;L;;;;;N;;;;;
+1118F;SHARADA LETTER O;Lo;0;L;;;;;N;;;;;
+11190;SHARADA LETTER AU;Lo;0;L;;;;;N;;;;;
+11191;SHARADA LETTER KA;Lo;0;L;;;;;N;;;;;
+11192;SHARADA LETTER KHA;Lo;0;L;;;;;N;;;;;
+11193;SHARADA LETTER GA;Lo;0;L;;;;;N;;;;;
+11194;SHARADA LETTER GHA;Lo;0;L;;;;;N;;;;;
+11195;SHARADA LETTER NGA;Lo;0;L;;;;;N;;;;;
+11196;SHARADA LETTER CA;Lo;0;L;;;;;N;;;;;
+11197;SHARADA LETTER CHA;Lo;0;L;;;;;N;;;;;
+11198;SHARADA LETTER JA;Lo;0;L;;;;;N;;;;;
+11199;SHARADA LETTER JHA;Lo;0;L;;;;;N;;;;;
+1119A;SHARADA LETTER NYA;Lo;0;L;;;;;N;;;;;
+1119B;SHARADA LETTER TTA;Lo;0;L;;;;;N;;;;;
+1119C;SHARADA LETTER TTHA;Lo;0;L;;;;;N;;;;;
+1119D;SHARADA LETTER DDA;Lo;0;L;;;;;N;;;;;
+1119E;SHARADA LETTER DDHA;Lo;0;L;;;;;N;;;;;
+1119F;SHARADA LETTER NNA;Lo;0;L;;;;;N;;;;;
+111A0;SHARADA LETTER TA;Lo;0;L;;;;;N;;;;;
+111A1;SHARADA LETTER THA;Lo;0;L;;;;;N;;;;;
+111A2;SHARADA LETTER DA;Lo;0;L;;;;;N;;;;;
+111A3;SHARADA LETTER DHA;Lo;0;L;;;;;N;;;;;
+111A4;SHARADA LETTER NA;Lo;0;L;;;;;N;;;;;
+111A5;SHARADA LETTER PA;Lo;0;L;;;;;N;;;;;
+111A6;SHARADA LETTER PHA;Lo;0;L;;;;;N;;;;;
+111A7;SHARADA LETTER BA;Lo;0;L;;;;;N;;;;;
+111A8;SHARADA LETTER BHA;Lo;0;L;;;;;N;;;;;
+111A9;SHARADA LETTER MA;Lo;0;L;;;;;N;;;;;
+111AA;SHARADA LETTER YA;Lo;0;L;;;;;N;;;;;
+111AB;SHARADA LETTER RA;Lo;0;L;;;;;N;;;;;
+111AC;SHARADA LETTER LA;Lo;0;L;;;;;N;;;;;
+111AD;SHARADA LETTER LLA;Lo;0;L;;;;;N;;;;;
+111AE;SHARADA LETTER VA;Lo;0;L;;;;;N;;;;;
+111AF;SHARADA LETTER SHA;Lo;0;L;;;;;N;;;;;
+111B0;SHARADA LETTER SSA;Lo;0;L;;;;;N;;;;;
+111B1;SHARADA LETTER SA;Lo;0;L;;;;;N;;;;;
+111B2;SHARADA LETTER HA;Lo;0;L;;;;;N;;;;;
+111B3;SHARADA VOWEL SIGN AA;Mc;0;L;;;;;N;;;;;
+111B4;SHARADA VOWEL SIGN I;Mc;0;L;;;;;N;;;;;
+111B5;SHARADA VOWEL SIGN II;Mc;0;L;;;;;N;;;;;
+111B6;SHARADA VOWEL SIGN U;Mn;0;NSM;;;;;N;;;;;
+111B7;SHARADA VOWEL SIGN UU;Mn;0;NSM;;;;;N;;;;;
+111B8;SHARADA VOWEL SIGN VOCALIC R;Mn;0;NSM;;;;;N;;;;;
+111B9;SHARADA VOWEL SIGN VOCALIC RR;Mn;0;NSM;;;;;N;;;;;
+111BA;SHARADA VOWEL SIGN VOCALIC L;Mn;0;NSM;;;;;N;;;;;
+111BB;SHARADA VOWEL SIGN VOCALIC LL;Mn;0;NSM;;;;;N;;;;;
+111BC;SHARADA VOWEL SIGN E;Mn;0;NSM;;;;;N;;;;;
+111BD;SHARADA VOWEL SIGN AI;Mn;0;NSM;;;;;N;;;;;
+111BE;SHARADA VOWEL SIGN O;Mn;0;NSM;;;;;N;;;;;
+111BF;SHARADA VOWEL SIGN AU;Mc;0;L;;;;;N;;;;;
+111C0;SHARADA SIGN VIRAMA;Mc;9;L;;;;;N;;;;;
+111C1;SHARADA SIGN AVAGRAHA;Lo;0;L;;;;;N;;;;;
+111C2;SHARADA SIGN JIHVAMULIYA;Lo;0;L;;;;;N;;;;;
+111C3;SHARADA SIGN UPADHMANIYA;Lo;0;L;;;;;N;;;;;
+111C4;SHARADA OM;Lo;0;L;;;;;N;;;;;
+111C5;SHARADA DANDA;Po;0;L;;;;;N;;;;;
+111C6;SHARADA DOUBLE DANDA;Po;0;L;;;;;N;;;;;
+111C7;SHARADA ABBREVIATION SIGN;Po;0;L;;;;;N;;;;;
+111C8;SHARADA SEPARATOR;Po;0;L;;;;;N;;;;;
+111C9;SHARADA SANDHI MARK;Po;0;L;;;;;N;;;;;
+111CA;SHARADA SIGN NUKTA;Mn;7;NSM;;;;;N;;;;;
+111CB;SHARADA VOWEL MODIFIER MARK;Mn;0;NSM;;;;;N;;;;;
+111CC;SHARADA EXTRA SHORT VOWEL MARK;Mn;0;NSM;;;;;N;;;;;
+111CD;SHARADA SUTRA MARK;Po;0;L;;;;;N;;;;;
+111D0;SHARADA DIGIT ZERO;Nd;0;L;;0;0;0;N;;;;;
+111D1;SHARADA DIGIT ONE;Nd;0;L;;1;1;1;N;;;;;
+111D2;SHARADA DIGIT TWO;Nd;0;L;;2;2;2;N;;;;;
+111D3;SHARADA DIGIT THREE;Nd;0;L;;3;3;3;N;;;;;
+111D4;SHARADA DIGIT FOUR;Nd;0;L;;4;4;4;N;;;;;
+111D5;SHARADA DIGIT FIVE;Nd;0;L;;5;5;5;N;;;;;
+111D6;SHARADA DIGIT SIX;Nd;0;L;;6;6;6;N;;;;;
+111D7;SHARADA DIGIT SEVEN;Nd;0;L;;7;7;7;N;;;;;
+111D8;SHARADA DIGIT EIGHT;Nd;0;L;;8;8;8;N;;;;;
+111D9;SHARADA DIGIT NINE;Nd;0;L;;9;9;9;N;;;;;
+111DA;SHARADA EKAM;Lo;0;L;;;;;N;;;;;
+111DB;SHARADA SIGN SIDDHAM;Po;0;L;;;;;N;;;;;
+111DC;SHARADA HEADSTROKE;Lo;0;L;;;;;N;;;;;
+111DD;SHARADA CONTINUATION SIGN;Po;0;L;;;;;N;;;;;
+111DE;SHARADA SECTION MARK-1;Po;0;L;;;;;N;;;;;
+111DF;SHARADA SECTION MARK-2;Po;0;L;;;;;N;;;;;
+111E1;SINHALA ARCHAIC DIGIT ONE;No;0;L;;;;1;N;;;;;
+111E2;SINHALA ARCHAIC DIGIT TWO;No;0;L;;;;2;N;;;;;
+111E3;SINHALA ARCHAIC DIGIT THREE;No;0;L;;;;3;N;;;;;
+111E4;SINHALA ARCHAIC DIGIT FOUR;No;0;L;;;;4;N;;;;;
+111E5;SINHALA ARCHAIC DIGIT FIVE;No;0;L;;;;5;N;;;;;
+111E6;SINHALA ARCHAIC DIGIT SIX;No;0;L;;;;6;N;;;;;
+111E7;SINHALA ARCHAIC DIGIT SEVEN;No;0;L;;;;7;N;;;;;
+111E8;SINHALA ARCHAIC DIGIT EIGHT;No;0;L;;;;8;N;;;;;
+111E9;SINHALA ARCHAIC DIGIT NINE;No;0;L;;;;9;N;;;;;
+111EA;SINHALA ARCHAIC NUMBER TEN;No;0;L;;;;10;N;;;;;
+111EB;SINHALA ARCHAIC NUMBER TWENTY;No;0;L;;;;20;N;;;;;
+111EC;SINHALA ARCHAIC NUMBER THIRTY;No;0;L;;;;30;N;;;;;
+111ED;SINHALA ARCHAIC NUMBER FORTY;No;0;L;;;;40;N;;;;;
+111EE;SINHALA ARCHAIC NUMBER FIFTY;No;0;L;;;;50;N;;;;;
+111EF;SINHALA ARCHAIC NUMBER SIXTY;No;0;L;;;;60;N;;;;;
+111F0;SINHALA ARCHAIC NUMBER SEVENTY;No;0;L;;;;70;N;;;;;
+111F1;SINHALA ARCHAIC NUMBER EIGHTY;No;0;L;;;;80;N;;;;;
+111F2;SINHALA ARCHAIC NUMBER NINETY;No;0;L;;;;90;N;;;;;
+111F3;SINHALA ARCHAIC NUMBER ONE HUNDRED;No;0;L;;;;100;N;;;;;
+111F4;SINHALA ARCHAIC NUMBER ONE THOUSAND;No;0;L;;;;1000;N;;;;;
+11200;KHOJKI LETTER A;Lo;0;L;;;;;N;;;;;
+11201;KHOJKI LETTER AA;Lo;0;L;;;;;N;;;;;
+11202;KHOJKI LETTER I;Lo;0;L;;;;;N;;;;;
+11203;KHOJKI LETTER U;Lo;0;L;;;;;N;;;;;
+11204;KHOJKI LETTER E;Lo;0;L;;;;;N;;;;;
+11205;KHOJKI LETTER AI;Lo;0;L;;;;;N;;;;;
+11206;KHOJKI LETTER O;Lo;0;L;;;;;N;;;;;
+11207;KHOJKI LETTER AU;Lo;0;L;;;;;N;;;;;
+11208;KHOJKI LETTER KA;Lo;0;L;;;;;N;;;;;
+11209;KHOJKI LETTER KHA;Lo;0;L;;;;;N;;;;;
+1120A;KHOJKI LETTER GA;Lo;0;L;;;;;N;;;;;
+1120B;KHOJKI LETTER GGA;Lo;0;L;;;;;N;;;;;
+1120C;KHOJKI LETTER GHA;Lo;0;L;;;;;N;;;;;
+1120D;KHOJKI LETTER NGA;Lo;0;L;;;;;N;;;;;
+1120E;KHOJKI LETTER CA;Lo;0;L;;;;;N;;;;;
+1120F;KHOJKI LETTER CHA;Lo;0;L;;;;;N;;;;;
+11210;KHOJKI LETTER JA;Lo;0;L;;;;;N;;;;;
+11211;KHOJKI LETTER JJA;Lo;0;L;;;;;N;;;;;
+11213;KHOJKI LETTER NYA;Lo;0;L;;;;;N;;;;;
+11214;KHOJKI LETTER TTA;Lo;0;L;;;;;N;;;;;
+11215;KHOJKI LETTER TTHA;Lo;0;L;;;;;N;;;;;
+11216;KHOJKI LETTER DDA;Lo;0;L;;;;;N;;;;;
+11217;KHOJKI LETTER DDHA;Lo;0;L;;;;;N;;;;;
+11218;KHOJKI LETTER NNA;Lo;0;L;;;;;N;;;;;
+11219;KHOJKI LETTER TA;Lo;0;L;;;;;N;;;;;
+1121A;KHOJKI LETTER THA;Lo;0;L;;;;;N;;;;;
+1121B;KHOJKI LETTER DA;Lo;0;L;;;;;N;;;;;
+1121C;KHOJKI LETTER DDDA;Lo;0;L;;;;;N;;;;;
+1121D;KHOJKI LETTER DHA;Lo;0;L;;;;;N;;;;;
+1121E;KHOJKI LETTER NA;Lo;0;L;;;;;N;;;;;
+1121F;KHOJKI LETTER PA;Lo;0;L;;;;;N;;;;;
+11220;KHOJKI LETTER PHA;Lo;0;L;;;;;N;;;;;
+11221;KHOJKI LETTER BA;Lo;0;L;;;;;N;;;;;
+11222;KHOJKI LETTER BBA;Lo;0;L;;;;;N;;;;;
+11223;KHOJKI LETTER BHA;Lo;0;L;;;;;N;;;;;
+11224;KHOJKI LETTER MA;Lo;0;L;;;;;N;;;;;
+11225;KHOJKI LETTER YA;Lo;0;L;;;;;N;;;;;
+11226;KHOJKI LETTER RA;Lo;0;L;;;;;N;;;;;
+11227;KHOJKI LETTER LA;Lo;0;L;;;;;N;;;;;
+11228;KHOJKI LETTER VA;Lo;0;L;;;;;N;;;;;
+11229;KHOJKI LETTER SA;Lo;0;L;;;;;N;;;;;
+1122A;KHOJKI LETTER HA;Lo;0;L;;;;;N;;;;;
+1122B;KHOJKI LETTER LLA;Lo;0;L;;;;;N;;;;;
+1122C;KHOJKI VOWEL SIGN AA;Mc;0;L;;;;;N;;;;;
+1122D;KHOJKI VOWEL SIGN I;Mc;0;L;;;;;N;;;;;
+1122E;KHOJKI VOWEL SIGN II;Mc;0;L;;;;;N;;;;;
+1122F;KHOJKI VOWEL SIGN U;Mn;0;NSM;;;;;N;;;;;
+11230;KHOJKI VOWEL SIGN E;Mn;0;NSM;;;;;N;;;;;
+11231;KHOJKI VOWEL SIGN AI;Mn;0;NSM;;;;;N;;;;;
+11232;KHOJKI VOWEL SIGN O;Mc;0;L;;;;;N;;;;;
+11233;KHOJKI VOWEL SIGN AU;Mc;0;L;;;;;N;;;;;
+11234;KHOJKI SIGN ANUSVARA;Mn;0;NSM;;;;;N;;;;;
+11235;KHOJKI SIGN VIRAMA;Mc;9;L;;;;;N;;;;;
+11236;KHOJKI SIGN NUKTA;Mn;7;NSM;;;;;N;;;;;
+11237;KHOJKI SIGN SHADDA;Mn;0;NSM;;;;;N;;;;;
+11238;KHOJKI DANDA;Po;0;L;;;;;N;;;;;
+11239;KHOJKI DOUBLE DANDA;Po;0;L;;;;;N;;;;;
+1123A;KHOJKI WORD SEPARATOR;Po;0;L;;;;;N;;;;;
+1123B;KHOJKI SECTION MARK;Po;0;L;;;;;N;;;;;
+1123C;KHOJKI DOUBLE SECTION MARK;Po;0;L;;;;;N;;;;;
+1123D;KHOJKI ABBREVIATION SIGN;Po;0;L;;;;;N;;;;;
+1123E;KHOJKI SIGN SUKUN;Mn;0;NSM;;;;;N;;;;;
+11280;MULTANI LETTER A;Lo;0;L;;;;;N;;;;;
+11281;MULTANI LETTER I;Lo;0;L;;;;;N;;;;;
+11282;MULTANI LETTER U;Lo;0;L;;;;;N;;;;;
+11283;MULTANI LETTER E;Lo;0;L;;;;;N;;;;;
+11284;MULTANI LETTER KA;Lo;0;L;;;;;N;;;;;
+11285;MULTANI LETTER KHA;Lo;0;L;;;;;N;;;;;
+11286;MULTANI LETTER GA;Lo;0;L;;;;;N;;;;;
+11288;MULTANI LETTER GHA;Lo;0;L;;;;;N;;;;;
+1128A;MULTANI LETTER CA;Lo;0;L;;;;;N;;;;;
+1128B;MULTANI LETTER CHA;Lo;0;L;;;;;N;;;;;
+1128C;MULTANI LETTER JA;Lo;0;L;;;;;N;;;;;
+1128D;MULTANI LETTER JJA;Lo;0;L;;;;;N;;;;;
+1128F;MULTANI LETTER NYA;Lo;0;L;;;;;N;;;;;
+11290;MULTANI LETTER TTA;Lo;0;L;;;;;N;;;;;
+11291;MULTANI LETTER TTHA;Lo;0;L;;;;;N;;;;;
+11292;MULTANI LETTER DDA;Lo;0;L;;;;;N;;;;;
+11293;MULTANI LETTER DDDA;Lo;0;L;;;;;N;;;;;
+11294;MULTANI LETTER DDHA;Lo;0;L;;;;;N;;;;;
+11295;MULTANI LETTER NNA;Lo;0;L;;;;;N;;;;;
+11296;MULTANI LETTER TA;Lo;0;L;;;;;N;;;;;
+11297;MULTANI LETTER THA;Lo;0;L;;;;;N;;;;;
+11298;MULTANI LETTER DA;Lo;0;L;;;;;N;;;;;
+11299;MULTANI LETTER DHA;Lo;0;L;;;;;N;;;;;
+1129A;MULTANI LETTER NA;Lo;0;L;;;;;N;;;;;
+1129B;MULTANI LETTER PA;Lo;0;L;;;;;N;;;;;
+1129C;MULTANI LETTER PHA;Lo;0;L;;;;;N;;;;;
+1129D;MULTANI LETTER BA;Lo;0;L;;;;;N;;;;;
+1129F;MULTANI LETTER BHA;Lo;0;L;;;;;N;;;;;
+112A0;MULTANI LETTER MA;Lo;0;L;;;;;N;;;;;
+112A1;MULTANI LETTER YA;Lo;0;L;;;;;N;;;;;
+112A2;MULTANI LETTER RA;Lo;0;L;;;;;N;;;;;
+112A3;MULTANI LETTER LA;Lo;0;L;;;;;N;;;;;
+112A4;MULTANI LETTER VA;Lo;0;L;;;;;N;;;;;
+112A5;MULTANI LETTER SA;Lo;0;L;;;;;N;;;;;
+112A6;MULTANI LETTER HA;Lo;0;L;;;;;N;;;;;
+112A7;MULTANI LETTER RRA;Lo;0;L;;;;;N;;;;;
+112A8;MULTANI LETTER RHA;Lo;0;L;;;;;N;;;;;
+112A9;MULTANI SECTION MARK;Po;0;L;;;;;N;;;;;
+112B0;KHUDAWADI LETTER A;Lo;0;L;;;;;N;;;;;
+112B1;KHUDAWADI LETTER AA;Lo;0;L;;;;;N;;;;;
+112B2;KHUDAWADI LETTER I;Lo;0;L;;;;;N;;;;;
+112B3;KHUDAWADI LETTER II;Lo;0;L;;;;;N;;;;;
+112B4;KHUDAWADI LETTER U;Lo;0;L;;;;;N;;;;;
+112B5;KHUDAWADI LETTER UU;Lo;0;L;;;;;N;;;;;
+112B6;KHUDAWADI LETTER E;Lo;0;L;;;;;N;;;;;
+112B7;KHUDAWADI LETTER AI;Lo;0;L;;;;;N;;;;;
+112B8;KHUDAWADI LETTER O;Lo;0;L;;;;;N;;;;;
+112B9;KHUDAWADI LETTER AU;Lo;0;L;;;;;N;;;;;
+112BA;KHUDAWADI LETTER KA;Lo;0;L;;;;;N;;;;;
+112BB;KHUDAWADI LETTER KHA;Lo;0;L;;;;;N;;;;;
+112BC;KHUDAWADI LETTER GA;Lo;0;L;;;;;N;;;;;
+112BD;KHUDAWADI LETTER GGA;Lo;0;L;;;;;N;;;;;
+112BE;KHUDAWADI LETTER GHA;Lo;0;L;;;;;N;;;;;
+112BF;KHUDAWADI LETTER NGA;Lo;0;L;;;;;N;;;;;
+112C0;KHUDAWADI LETTER CA;Lo;0;L;;;;;N;;;;;
+112C1;KHUDAWADI LETTER CHA;Lo;0;L;;;;;N;;;;;
+112C2;KHUDAWADI LETTER JA;Lo;0;L;;;;;N;;;;;
+112C3;KHUDAWADI LETTER JJA;Lo;0;L;;;;;N;;;;;
+112C4;KHUDAWADI LETTER JHA;Lo;0;L;;;;;N;;;;;
+112C5;KHUDAWADI LETTER NYA;Lo;0;L;;;;;N;;;;;
+112C6;KHUDAWADI LETTER TTA;Lo;0;L;;;;;N;;;;;
+112C7;KHUDAWADI LETTER TTHA;Lo;0;L;;;;;N;;;;;
+112C8;KHUDAWADI LETTER DDA;Lo;0;L;;;;;N;;;;;
+112C9;KHUDAWADI LETTER DDDA;Lo;0;L;;;;;N;;;;;
+112CA;KHUDAWADI LETTER RRA;Lo;0;L;;;;;N;;;;;
+112CB;KHUDAWADI LETTER DDHA;Lo;0;L;;;;;N;;;;;
+112CC;KHUDAWADI LETTER NNA;Lo;0;L;;;;;N;;;;;
+112CD;KHUDAWADI LETTER TA;Lo;0;L;;;;;N;;;;;
+112CE;KHUDAWADI LETTER THA;Lo;0;L;;;;;N;;;;;
+112CF;KHUDAWADI LETTER DA;Lo;0;L;;;;;N;;;;;
+112D0;KHUDAWADI LETTER DHA;Lo;0;L;;;;;N;;;;;
+112D1;KHUDAWADI LETTER NA;Lo;0;L;;;;;N;;;;;
+112D2;KHUDAWADI LETTER PA;Lo;0;L;;;;;N;;;;;
+112D3;KHUDAWADI LETTER PHA;Lo;0;L;;;;;N;;;;;
+112D4;KHUDAWADI LETTER BA;Lo;0;L;;;;;N;;;;;
+112D5;KHUDAWADI LETTER BBA;Lo;0;L;;;;;N;;;;;
+112D6;KHUDAWADI LETTER BHA;Lo;0;L;;;;;N;;;;;
+112D7;KHUDAWADI LETTER MA;Lo;0;L;;;;;N;;;;;
+112D8;KHUDAWADI LETTER YA;Lo;0;L;;;;;N;;;;;
+112D9;KHUDAWADI LETTER RA;Lo;0;L;;;;;N;;;;;
+112DA;KHUDAWADI LETTER LA;Lo;0;L;;;;;N;;;;;
+112DB;KHUDAWADI LETTER VA;Lo;0;L;;;;;N;;;;;
+112DC;KHUDAWADI LETTER SHA;Lo;0;L;;;;;N;;;;;
+112DD;KHUDAWADI LETTER SA;Lo;0;L;;;;;N;;;;;
+112DE;KHUDAWADI LETTER HA;Lo;0;L;;;;;N;;;;;
+112DF;KHUDAWADI SIGN ANUSVARA;Mn;0;NSM;;;;;N;;;;;
+112E0;KHUDAWADI VOWEL SIGN AA;Mc;0;L;;;;;N;;;;;
+112E1;KHUDAWADI VOWEL SIGN I;Mc;0;L;;;;;N;;;;;
+112E2;KHUDAWADI VOWEL SIGN II;Mc;0;L;;;;;N;;;;;
+112E3;KHUDAWADI VOWEL SIGN U;Mn;0;NSM;;;;;N;;;;;
+112E4;KHUDAWADI VOWEL SIGN UU;Mn;0;NSM;;;;;N;;;;;
+112E5;KHUDAWADI VOWEL SIGN E;Mn;0;NSM;;;;;N;;;;;
+112E6;KHUDAWADI VOWEL SIGN AI;Mn;0;NSM;;;;;N;;;;;
+112E7;KHUDAWADI VOWEL SIGN O;Mn;0;NSM;;;;;N;;;;;
+112E8;KHUDAWADI VOWEL SIGN AU;Mn;0;NSM;;;;;N;;;;;
+112E9;KHUDAWADI SIGN NUKTA;Mn;7;NSM;;;;;N;;;;;
+112EA;KHUDAWADI SIGN VIRAMA;Mn;9;NSM;;;;;N;;;;;
+112F0;KHUDAWADI DIGIT ZERO;Nd;0;L;;0;0;0;N;;;;;
+112F1;KHUDAWADI DIGIT ONE;Nd;0;L;;1;1;1;N;;;;;
+112F2;KHUDAWADI DIGIT TWO;Nd;0;L;;2;2;2;N;;;;;
+112F3;KHUDAWADI DIGIT THREE;Nd;0;L;;3;3;3;N;;;;;
+112F4;KHUDAWADI DIGIT FOUR;Nd;0;L;;4;4;4;N;;;;;
+112F5;KHUDAWADI DIGIT FIVE;Nd;0;L;;5;5;5;N;;;;;
+112F6;KHUDAWADI DIGIT SIX;Nd;0;L;;6;6;6;N;;;;;
+112F7;KHUDAWADI DIGIT SEVEN;Nd;0;L;;7;7;7;N;;;;;
+112F8;KHUDAWADI DIGIT EIGHT;Nd;0;L;;8;8;8;N;;;;;
+112F9;KHUDAWADI DIGIT NINE;Nd;0;L;;9;9;9;N;;;;;
+11300;GRANTHA SIGN COMBINING ANUSVARA ABOVE;Mn;0;NSM;;;;;N;;;;;
+11301;GRANTHA SIGN CANDRABINDU;Mn;0;NSM;;;;;N;;;;;
+11302;GRANTHA SIGN ANUSVARA;Mc;0;L;;;;;N;;;;;
+11303;GRANTHA SIGN VISARGA;Mc;0;L;;;;;N;;;;;
+11305;GRANTHA LETTER A;Lo;0;L;;;;;N;;;;;
+11306;GRANTHA LETTER AA;Lo;0;L;;;;;N;;;;;
+11307;GRANTHA LETTER I;Lo;0;L;;;;;N;;;;;
+11308;GRANTHA LETTER II;Lo;0;L;;;;;N;;;;;
+11309;GRANTHA LETTER U;Lo;0;L;;;;;N;;;;;
+1130A;GRANTHA LETTER UU;Lo;0;L;;;;;N;;;;;
+1130B;GRANTHA LETTER VOCALIC R;Lo;0;L;;;;;N;;;;;
+1130C;GRANTHA LETTER VOCALIC L;Lo;0;L;;;;;N;;;;;
+1130F;GRANTHA LETTER EE;Lo;0;L;;;;;N;;;;;
+11310;GRANTHA LETTER AI;Lo;0;L;;;;;N;;;;;
+11313;GRANTHA LETTER OO;Lo;0;L;;;;;N;;;;;
+11314;GRANTHA LETTER AU;Lo;0;L;;;;;N;;;;;
+11315;GRANTHA LETTER KA;Lo;0;L;;;;;N;;;;;
+11316;GRANTHA LETTER KHA;Lo;0;L;;;;;N;;;;;
+11317;GRANTHA LETTER GA;Lo;0;L;;;;;N;;;;;
+11318;GRANTHA LETTER GHA;Lo;0;L;;;;;N;;;;;
+11319;GRANTHA LETTER NGA;Lo;0;L;;;;;N;;;;;
+1131A;GRANTHA LETTER CA;Lo;0;L;;;;;N;;;;;
+1131B;GRANTHA LETTER CHA;Lo;0;L;;;;;N;;;;;
+1131C;GRANTHA LETTER JA;Lo;0;L;;;;;N;;;;;
+1131D;GRANTHA LETTER JHA;Lo;0;L;;;;;N;;;;;
+1131E;GRANTHA LETTER NYA;Lo;0;L;;;;;N;;;;;
+1131F;GRANTHA LETTER TTA;Lo;0;L;;;;;N;;;;;
+11320;GRANTHA LETTER TTHA;Lo;0;L;;;;;N;;;;;
+11321;GRANTHA LETTER DDA;Lo;0;L;;;;;N;;;;;
+11322;GRANTHA LETTER DDHA;Lo;0;L;;;;;N;;;;;
+11323;GRANTHA LETTER NNA;Lo;0;L;;;;;N;;;;;
+11324;GRANTHA LETTER TA;Lo;0;L;;;;;N;;;;;
+11325;GRANTHA LETTER THA;Lo;0;L;;;;;N;;;;;
+11326;GRANTHA LETTER DA;Lo;0;L;;;;;N;;;;;
+11327;GRANTHA LETTER DHA;Lo;0;L;;;;;N;;;;;
+11328;GRANTHA LETTER NA;Lo;0;L;;;;;N;;;;;
+1132A;GRANTHA LETTER PA;Lo;0;L;;;;;N;;;;;
+1132B;GRANTHA LETTER PHA;Lo;0;L;;;;;N;;;;;
+1132C;GRANTHA LETTER BA;Lo;0;L;;;;;N;;;;;
+1132D;GRANTHA LETTER BHA;Lo;0;L;;;;;N;;;;;
+1132E;GRANTHA LETTER MA;Lo;0;L;;;;;N;;;;;
+1132F;GRANTHA LETTER YA;Lo;0;L;;;;;N;;;;;
+11330;GRANTHA LETTER RA;Lo;0;L;;;;;N;;;;;
+11332;GRANTHA LETTER LA;Lo;0;L;;;;;N;;;;;
+11333;GRANTHA LETTER LLA;Lo;0;L;;;;;N;;;;;
+11335;GRANTHA LETTER VA;Lo;0;L;;;;;N;;;;;
+11336;GRANTHA LETTER SHA;Lo;0;L;;;;;N;;;;;
+11337;GRANTHA LETTER SSA;Lo;0;L;;;;;N;;;;;
+11338;GRANTHA LETTER SA;Lo;0;L;;;;;N;;;;;
+11339;GRANTHA LETTER HA;Lo;0;L;;;;;N;;;;;
+1133C;GRANTHA SIGN NUKTA;Mn;7;NSM;;;;;N;;;;;
+1133D;GRANTHA SIGN AVAGRAHA;Lo;0;L;;;;;N;;;;;
+1133E;GRANTHA VOWEL SIGN AA;Mc;0;L;;;;;N;;;;;
+1133F;GRANTHA VOWEL SIGN I;Mc;0;L;;;;;N;;;;;
+11340;GRANTHA VOWEL SIGN II;Mn;0;NSM;;;;;N;;;;;
+11341;GRANTHA VOWEL SIGN U;Mc;0;L;;;;;N;;;;;
+11342;GRANTHA VOWEL SIGN UU;Mc;0;L;;;;;N;;;;;
+11343;GRANTHA VOWEL SIGN VOCALIC R;Mc;0;L;;;;;N;;;;;
+11344;GRANTHA VOWEL SIGN VOCALIC RR;Mc;0;L;;;;;N;;;;;
+11347;GRANTHA VOWEL SIGN EE;Mc;0;L;;;;;N;;;;;
+11348;GRANTHA VOWEL SIGN AI;Mc;0;L;;;;;N;;;;;
+1134B;GRANTHA VOWEL SIGN OO;Mc;0;L;11347 1133E;;;;N;;;;;
+1134C;GRANTHA VOWEL SIGN AU;Mc;0;L;11347 11357;;;;N;;;;;
+1134D;GRANTHA SIGN VIRAMA;Mc;9;L;;;;;N;;;;;
+11350;GRANTHA OM;Lo;0;L;;;;;N;;;;;
+11357;GRANTHA AU LENGTH MARK;Mc;0;L;;;;;N;;;;;
+1135D;GRANTHA SIGN PLUTA;Lo;0;L;;;;;N;;;;;
+1135E;GRANTHA LETTER VEDIC ANUSVARA;Lo;0;L;;;;;N;;;;;
+1135F;GRANTHA LETTER VEDIC DOUBLE ANUSVARA;Lo;0;L;;;;;N;;;;;
+11360;GRANTHA LETTER VOCALIC RR;Lo;0;L;;;;;N;;;;;
+11361;GRANTHA LETTER VOCALIC LL;Lo;0;L;;;;;N;;;;;
+11362;GRANTHA VOWEL SIGN VOCALIC L;Mc;0;L;;;;;N;;;;;
+11363;GRANTHA VOWEL SIGN VOCALIC LL;Mc;0;L;;;;;N;;;;;
+11366;COMBINING GRANTHA DIGIT ZERO;Mn;230;NSM;;;;;N;;;;;
+11367;COMBINING GRANTHA DIGIT ONE;Mn;230;NSM;;;;;N;;;;;
+11368;COMBINING GRANTHA DIGIT TWO;Mn;230;NSM;;;;;N;;;;;
+11369;COMBINING GRANTHA DIGIT THREE;Mn;230;NSM;;;;;N;;;;;
+1136A;COMBINING GRANTHA DIGIT FOUR;Mn;230;NSM;;;;;N;;;;;
+1136B;COMBINING GRANTHA DIGIT FIVE;Mn;230;NSM;;;;;N;;;;;
+1136C;COMBINING GRANTHA DIGIT SIX;Mn;230;NSM;;;;;N;;;;;
+11370;COMBINING GRANTHA LETTER A;Mn;230;NSM;;;;;N;;;;;
+11371;COMBINING GRANTHA LETTER KA;Mn;230;NSM;;;;;N;;;;;
+11372;COMBINING GRANTHA LETTER NA;Mn;230;NSM;;;;;N;;;;;
+11373;COMBINING GRANTHA LETTER VI;Mn;230;NSM;;;;;N;;;;;
+11374;COMBINING GRANTHA LETTER PA;Mn;230;NSM;;;;;N;;;;;
+11400;NEWA LETTER A;Lo;0;L;;;;;N;;;;;
+11401;NEWA LETTER AA;Lo;0;L;;;;;N;;;;;
+11402;NEWA LETTER I;Lo;0;L;;;;;N;;;;;
+11403;NEWA LETTER II;Lo;0;L;;;;;N;;;;;
+11404;NEWA LETTER U;Lo;0;L;;;;;N;;;;;
+11405;NEWA LETTER UU;Lo;0;L;;;;;N;;;;;
+11406;NEWA LETTER VOCALIC R;Lo;0;L;;;;;N;;;;;
+11407;NEWA LETTER VOCALIC RR;Lo;0;L;;;;;N;;;;;
+11408;NEWA LETTER VOCALIC L;Lo;0;L;;;;;N;;;;;
+11409;NEWA LETTER VOCALIC LL;Lo;0;L;;;;;N;;;;;
+1140A;NEWA LETTER E;Lo;0;L;;;;;N;;;;;
+1140B;NEWA LETTER AI;Lo;0;L;;;;;N;;;;;
+1140C;NEWA LETTER O;Lo;0;L;;;;;N;;;;;
+1140D;NEWA LETTER AU;Lo;0;L;;;;;N;;;;;
+1140E;NEWA LETTER KA;Lo;0;L;;;;;N;;;;;
+1140F;NEWA LETTER KHA;Lo;0;L;;;;;N;;;;;
+11410;NEWA LETTER GA;Lo;0;L;;;;;N;;;;;
+11411;NEWA LETTER GHA;Lo;0;L;;;;;N;;;;;
+11412;NEWA LETTER NGA;Lo;0;L;;;;;N;;;;;
+11413;NEWA LETTER NGHA;Lo;0;L;;;;;N;;;;;
+11414;NEWA LETTER CA;Lo;0;L;;;;;N;;;;;
+11415;NEWA LETTER CHA;Lo;0;L;;;;;N;;;;;
+11416;NEWA LETTER JA;Lo;0;L;;;;;N;;;;;
+11417;NEWA LETTER JHA;Lo;0;L;;;;;N;;;;;
+11418;NEWA LETTER NYA;Lo;0;L;;;;;N;;;;;
+11419;NEWA LETTER NYHA;Lo;0;L;;;;;N;;;;;
+1141A;NEWA LETTER TTA;Lo;0;L;;;;;N;;;;;
+1141B;NEWA LETTER TTHA;Lo;0;L;;;;;N;;;;;
+1141C;NEWA LETTER DDA;Lo;0;L;;;;;N;;;;;
+1141D;NEWA LETTER DDHA;Lo;0;L;;;;;N;;;;;
+1141E;NEWA LETTER NNA;Lo;0;L;;;;;N;;;;;
+1141F;NEWA LETTER TA;Lo;0;L;;;;;N;;;;;
+11420;NEWA LETTER THA;Lo;0;L;;;;;N;;;;;
+11421;NEWA LETTER DA;Lo;0;L;;;;;N;;;;;
+11422;NEWA LETTER DHA;Lo;0;L;;;;;N;;;;;
+11423;NEWA LETTER NA;Lo;0;L;;;;;N;;;;;
+11424;NEWA LETTER NHA;Lo;0;L;;;;;N;;;;;
+11425;NEWA LETTER PA;Lo;0;L;;;;;N;;;;;
+11426;NEWA LETTER PHA;Lo;0;L;;;;;N;;;;;
+11427;NEWA LETTER BA;Lo;0;L;;;;;N;;;;;
+11428;NEWA LETTER BHA;Lo;0;L;;;;;N;;;;;
+11429;NEWA LETTER MA;Lo;0;L;;;;;N;;;;;
+1142A;NEWA LETTER MHA;Lo;0;L;;;;;N;;;;;
+1142B;NEWA LETTER YA;Lo;0;L;;;;;N;;;;;
+1142C;NEWA LETTER RA;Lo;0;L;;;;;N;;;;;
+1142D;NEWA LETTER RHA;Lo;0;L;;;;;N;;;;;
+1142E;NEWA LETTER LA;Lo;0;L;;;;;N;;;;;
+1142F;NEWA LETTER LHA;Lo;0;L;;;;;N;;;;;
+11430;NEWA LETTER WA;Lo;0;L;;;;;N;;;;;
+11431;NEWA LETTER SHA;Lo;0;L;;;;;N;;;;;
+11432;NEWA LETTER SSA;Lo;0;L;;;;;N;;;;;
+11433;NEWA LETTER SA;Lo;0;L;;;;;N;;;;;
+11434;NEWA LETTER HA;Lo;0;L;;;;;N;;;;;
+11435;NEWA VOWEL SIGN AA;Mc;0;L;;;;;N;;;;;
+11436;NEWA VOWEL SIGN I;Mc;0;L;;;;;N;;;;;
+11437;NEWA VOWEL SIGN II;Mc;0;L;;;;;N;;;;;
+11438;NEWA VOWEL SIGN U;Mn;0;NSM;;;;;N;;;;;
+11439;NEWA VOWEL SIGN UU;Mn;0;NSM;;;;;N;;;;;
+1143A;NEWA VOWEL SIGN VOCALIC R;Mn;0;NSM;;;;;N;;;;;
+1143B;NEWA VOWEL SIGN VOCALIC RR;Mn;0;NSM;;;;;N;;;;;
+1143C;NEWA VOWEL SIGN VOCALIC L;Mn;0;NSM;;;;;N;;;;;
+1143D;NEWA VOWEL SIGN VOCALIC LL;Mn;0;NSM;;;;;N;;;;;
+1143E;NEWA VOWEL SIGN E;Mn;0;NSM;;;;;N;;;;;
+1143F;NEWA VOWEL SIGN AI;Mn;0;NSM;;;;;N;;;;;
+11440;NEWA VOWEL SIGN O;Mc;0;L;;;;;N;;;;;
+11441;NEWA VOWEL SIGN AU;Mc;0;L;;;;;N;;;;;
+11442;NEWA SIGN VIRAMA;Mn;9;NSM;;;;;N;;;;;
+11443;NEWA SIGN CANDRABINDU;Mn;0;NSM;;;;;N;;;;;
+11444;NEWA SIGN ANUSVARA;Mn;0;NSM;;;;;N;;;;;
+11445;NEWA SIGN VISARGA;Mc;0;L;;;;;N;;;;;
+11446;NEWA SIGN NUKTA;Mn;7;NSM;;;;;N;;;;;
+11447;NEWA SIGN AVAGRAHA;Lo;0;L;;;;;N;;;;;
+11448;NEWA SIGN FINAL ANUSVARA;Lo;0;L;;;;;N;;;;;
+11449;NEWA OM;Lo;0;L;;;;;N;;;;;
+1144A;NEWA SIDDHI;Lo;0;L;;;;;N;;;;;
+1144B;NEWA DANDA;Po;0;L;;;;;N;;;;;
+1144C;NEWA DOUBLE DANDA;Po;0;L;;;;;N;;;;;
+1144D;NEWA COMMA;Po;0;L;;;;;N;;;;;
+1144E;NEWA GAP FILLER;Po;0;L;;;;;N;;;;;
+1144F;NEWA ABBREVIATION SIGN;Po;0;L;;;;;N;;;;;
+11450;NEWA DIGIT ZERO;Nd;0;L;;0;0;0;N;;;;;
+11451;NEWA DIGIT ONE;Nd;0;L;;1;1;1;N;;;;;
+11452;NEWA DIGIT TWO;Nd;0;L;;2;2;2;N;;;;;
+11453;NEWA DIGIT THREE;Nd;0;L;;3;3;3;N;;;;;
+11454;NEWA DIGIT FOUR;Nd;0;L;;4;4;4;N;;;;;
+11455;NEWA DIGIT FIVE;Nd;0;L;;5;5;5;N;;;;;
+11456;NEWA DIGIT SIX;Nd;0;L;;6;6;6;N;;;;;
+11457;NEWA DIGIT SEVEN;Nd;0;L;;7;7;7;N;;;;;
+11458;NEWA DIGIT EIGHT;Nd;0;L;;8;8;8;N;;;;;
+11459;NEWA DIGIT NINE;Nd;0;L;;9;9;9;N;;;;;
+1145B;NEWA PLACEHOLDER MARK;Po;0;L;;;;;N;;;;;
+1145D;NEWA INSERTION SIGN;Po;0;L;;;;;N;;;;;
+11480;TIRHUTA ANJI;Lo;0;L;;;;;N;;;;;
+11481;TIRHUTA LETTER A;Lo;0;L;;;;;N;;;;;
+11482;TIRHUTA LETTER AA;Lo;0;L;;;;;N;;;;;
+11483;TIRHUTA LETTER I;Lo;0;L;;;;;N;;;;;
+11484;TIRHUTA LETTER II;Lo;0;L;;;;;N;;;;;
+11485;TIRHUTA LETTER U;Lo;0;L;;;;;N;;;;;
+11486;TIRHUTA LETTER UU;Lo;0;L;;;;;N;;;;;
+11487;TIRHUTA LETTER VOCALIC R;Lo;0;L;;;;;N;;;;;
+11488;TIRHUTA LETTER VOCALIC RR;Lo;0;L;;;;;N;;;;;
+11489;TIRHUTA LETTER VOCALIC L;Lo;0;L;;;;;N;;;;;
+1148A;TIRHUTA LETTER VOCALIC LL;Lo;0;L;;;;;N;;;;;
+1148B;TIRHUTA LETTER E;Lo;0;L;;;;;N;;;;;
+1148C;TIRHUTA LETTER AI;Lo;0;L;;;;;N;;;;;
+1148D;TIRHUTA LETTER O;Lo;0;L;;;;;N;;;;;
+1148E;TIRHUTA LETTER AU;Lo;0;L;;;;;N;;;;;
+1148F;TIRHUTA LETTER KA;Lo;0;L;;;;;N;;;;;
+11490;TIRHUTA LETTER KHA;Lo;0;L;;;;;N;;;;;
+11491;TIRHUTA LETTER GA;Lo;0;L;;;;;N;;;;;
+11492;TIRHUTA LETTER GHA;Lo;0;L;;;;;N;;;;;
+11493;TIRHUTA LETTER NGA;Lo;0;L;;;;;N;;;;;
+11494;TIRHUTA LETTER CA;Lo;0;L;;;;;N;;;;;
+11495;TIRHUTA LETTER CHA;Lo;0;L;;;;;N;;;;;
+11496;TIRHUTA LETTER JA;Lo;0;L;;;;;N;;;;;
+11497;TIRHUTA LETTER JHA;Lo;0;L;;;;;N;;;;;
+11498;TIRHUTA LETTER NYA;Lo;0;L;;;;;N;;;;;
+11499;TIRHUTA LETTER TTA;Lo;0;L;;;;;N;;;;;
+1149A;TIRHUTA LETTER TTHA;Lo;0;L;;;;;N;;;;;
+1149B;TIRHUTA LETTER DDA;Lo;0;L;;;;;N;;;;;
+1149C;TIRHUTA LETTER DDHA;Lo;0;L;;;;;N;;;;;
+1149D;TIRHUTA LETTER NNA;Lo;0;L;;;;;N;;;;;
+1149E;TIRHUTA LETTER TA;Lo;0;L;;;;;N;;;;;
+1149F;TIRHUTA LETTER THA;Lo;0;L;;;;;N;;;;;
+114A0;TIRHUTA LETTER DA;Lo;0;L;;;;;N;;;;;
+114A1;TIRHUTA LETTER DHA;Lo;0;L;;;;;N;;;;;
+114A2;TIRHUTA LETTER NA;Lo;0;L;;;;;N;;;;;
+114A3;TIRHUTA LETTER PA;Lo;0;L;;;;;N;;;;;
+114A4;TIRHUTA LETTER PHA;Lo;0;L;;;;;N;;;;;
+114A5;TIRHUTA LETTER BA;Lo;0;L;;;;;N;;;;;
+114A6;TIRHUTA LETTER BHA;Lo;0;L;;;;;N;;;;;
+114A7;TIRHUTA LETTER MA;Lo;0;L;;;;;N;;;;;
+114A8;TIRHUTA LETTER YA;Lo;0;L;;;;;N;;;;;
+114A9;TIRHUTA LETTER RA;Lo;0;L;;;;;N;;;;;
+114AA;TIRHUTA LETTER LA;Lo;0;L;;;;;N;;;;;
+114AB;TIRHUTA LETTER VA;Lo;0;L;;;;;N;;;;;
+114AC;TIRHUTA LETTER SHA;Lo;0;L;;;;;N;;;;;
+114AD;TIRHUTA LETTER SSA;Lo;0;L;;;;;N;;;;;
+114AE;TIRHUTA LETTER SA;Lo;0;L;;;;;N;;;;;
+114AF;TIRHUTA LETTER HA;Lo;0;L;;;;;N;;;;;
+114B0;TIRHUTA VOWEL SIGN AA;Mc;0;L;;;;;N;;;;;
+114B1;TIRHUTA VOWEL SIGN I;Mc;0;L;;;;;N;;;;;
+114B2;TIRHUTA VOWEL SIGN II;Mc;0;L;;;;;N;;;;;
+114B3;TIRHUTA VOWEL SIGN U;Mn;0;NSM;;;;;N;;;;;
+114B4;TIRHUTA VOWEL SIGN UU;Mn;0;NSM;;;;;N;;;;;
+114B5;TIRHUTA VOWEL SIGN VOCALIC R;Mn;0;NSM;;;;;N;;;;;
+114B6;TIRHUTA VOWEL SIGN VOCALIC RR;Mn;0;NSM;;;;;N;;;;;
+114B7;TIRHUTA VOWEL SIGN VOCALIC L;Mn;0;NSM;;;;;N;;;;;
+114B8;TIRHUTA VOWEL SIGN VOCALIC LL;Mn;0;NSM;;;;;N;;;;;
+114B9;TIRHUTA VOWEL SIGN E;Mc;0;L;;;;;N;;;;;
+114BA;TIRHUTA VOWEL SIGN SHORT E;Mn;0;NSM;;;;;N;;;;;
+114BB;TIRHUTA VOWEL SIGN AI;Mc;0;L;114B9 114BA;;;;N;;;;;
+114BC;TIRHUTA VOWEL SIGN O;Mc;0;L;114B9 114B0;;;;N;;;;;
+114BD;TIRHUTA VOWEL SIGN SHORT O;Mc;0;L;;;;;N;;;;;
+114BE;TIRHUTA VOWEL SIGN AU;Mc;0;L;114B9 114BD;;;;N;;;;;
+114BF;TIRHUTA SIGN CANDRABINDU;Mn;0;NSM;;;;;N;;;;;
+114C0;TIRHUTA SIGN ANUSVARA;Mn;0;NSM;;;;;N;;;;;
+114C1;TIRHUTA SIGN VISARGA;Mc;0;L;;;;;N;;;;;
+114C2;TIRHUTA SIGN VIRAMA;Mn;9;NSM;;;;;N;;;;;
+114C3;TIRHUTA SIGN NUKTA;Mn;7;NSM;;;;;N;;;;;
+114C4;TIRHUTA SIGN AVAGRAHA;Lo;0;L;;;;;N;;;;;
+114C5;TIRHUTA GVANG;Lo;0;L;;;;;N;;;;;
+114C6;TIRHUTA ABBREVIATION SIGN;Po;0;L;;;;;N;;;;;
+114C7;TIRHUTA OM;Lo;0;L;;;;;N;;;;;
+114D0;TIRHUTA DIGIT ZERO;Nd;0;L;;0;0;0;N;;;;;
+114D1;TIRHUTA DIGIT ONE;Nd;0;L;;1;1;1;N;;;;;
+114D2;TIRHUTA DIGIT TWO;Nd;0;L;;2;2;2;N;;;;;
+114D3;TIRHUTA DIGIT THREE;Nd;0;L;;3;3;3;N;;;;;
+114D4;TIRHUTA DIGIT FOUR;Nd;0;L;;4;4;4;N;;;;;
+114D5;TIRHUTA DIGIT FIVE;Nd;0;L;;5;5;5;N;;;;;
+114D6;TIRHUTA DIGIT SIX;Nd;0;L;;6;6;6;N;;;;;
+114D7;TIRHUTA DIGIT SEVEN;Nd;0;L;;7;7;7;N;;;;;
+114D8;TIRHUTA DIGIT EIGHT;Nd;0;L;;8;8;8;N;;;;;
+114D9;TIRHUTA DIGIT NINE;Nd;0;L;;9;9;9;N;;;;;
+11580;SIDDHAM LETTER A;Lo;0;L;;;;;N;;;;;
+11581;SIDDHAM LETTER AA;Lo;0;L;;;;;N;;;;;
+11582;SIDDHAM LETTER I;Lo;0;L;;;;;N;;;;;
+11583;SIDDHAM LETTER II;Lo;0;L;;;;;N;;;;;
+11584;SIDDHAM LETTER U;Lo;0;L;;;;;N;;;;;
+11585;SIDDHAM LETTER UU;Lo;0;L;;;;;N;;;;;
+11586;SIDDHAM LETTER VOCALIC R;Lo;0;L;;;;;N;;;;;
+11587;SIDDHAM LETTER VOCALIC RR;Lo;0;L;;;;;N;;;;;
+11588;SIDDHAM LETTER VOCALIC L;Lo;0;L;;;;;N;;;;;
+11589;SIDDHAM LETTER VOCALIC LL;Lo;0;L;;;;;N;;;;;
+1158A;SIDDHAM LETTER E;Lo;0;L;;;;;N;;;;;
+1158B;SIDDHAM LETTER AI;Lo;0;L;;;;;N;;;;;
+1158C;SIDDHAM LETTER O;Lo;0;L;;;;;N;;;;;
+1158D;SIDDHAM LETTER AU;Lo;0;L;;;;;N;;;;;
+1158E;SIDDHAM LETTER KA;Lo;0;L;;;;;N;;;;;
+1158F;SIDDHAM LETTER KHA;Lo;0;L;;;;;N;;;;;
+11590;SIDDHAM LETTER GA;Lo;0;L;;;;;N;;;;;
+11591;SIDDHAM LETTER GHA;Lo;0;L;;;;;N;;;;;
+11592;SIDDHAM LETTER NGA;Lo;0;L;;;;;N;;;;;
+11593;SIDDHAM LETTER CA;Lo;0;L;;;;;N;;;;;
+11594;SIDDHAM LETTER CHA;Lo;0;L;;;;;N;;;;;
+11595;SIDDHAM LETTER JA;Lo;0;L;;;;;N;;;;;
+11596;SIDDHAM LETTER JHA;Lo;0;L;;;;;N;;;;;
+11597;SIDDHAM LETTER NYA;Lo;0;L;;;;;N;;;;;
+11598;SIDDHAM LETTER TTA;Lo;0;L;;;;;N;;;;;
+11599;SIDDHAM LETTER TTHA;Lo;0;L;;;;;N;;;;;
+1159A;SIDDHAM LETTER DDA;Lo;0;L;;;;;N;;;;;
+1159B;SIDDHAM LETTER DDHA;Lo;0;L;;;;;N;;;;;
+1159C;SIDDHAM LETTER NNA;Lo;0;L;;;;;N;;;;;
+1159D;SIDDHAM LETTER TA;Lo;0;L;;;;;N;;;;;
+1159E;SIDDHAM LETTER THA;Lo;0;L;;;;;N;;;;;
+1159F;SIDDHAM LETTER DA;Lo;0;L;;;;;N;;;;;
+115A0;SIDDHAM LETTER DHA;Lo;0;L;;;;;N;;;;;
+115A1;SIDDHAM LETTER NA;Lo;0;L;;;;;N;;;;;
+115A2;SIDDHAM LETTER PA;Lo;0;L;;;;;N;;;;;
+115A3;SIDDHAM LETTER PHA;Lo;0;L;;;;;N;;;;;
+115A4;SIDDHAM LETTER BA;Lo;0;L;;;;;N;;;;;
+115A5;SIDDHAM LETTER BHA;Lo;0;L;;;;;N;;;;;
+115A6;SIDDHAM LETTER MA;Lo;0;L;;;;;N;;;;;
+115A7;SIDDHAM LETTER YA;Lo;0;L;;;;;N;;;;;
+115A8;SIDDHAM LETTER RA;Lo;0;L;;;;;N;;;;;
+115A9;SIDDHAM LETTER LA;Lo;0;L;;;;;N;;;;;
+115AA;SIDDHAM LETTER VA;Lo;0;L;;;;;N;;;;;
+115AB;SIDDHAM LETTER SHA;Lo;0;L;;;;;N;;;;;
+115AC;SIDDHAM LETTER SSA;Lo;0;L;;;;;N;;;;;
+115AD;SIDDHAM LETTER SA;Lo;0;L;;;;;N;;;;;
+115AE;SIDDHAM LETTER HA;Lo;0;L;;;;;N;;;;;
+115AF;SIDDHAM VOWEL SIGN AA;Mc;0;L;;;;;N;;;;;
+115B0;SIDDHAM VOWEL SIGN I;Mc;0;L;;;;;N;;;;;
+115B1;SIDDHAM VOWEL SIGN II;Mc;0;L;;;;;N;;;;;
+115B2;SIDDHAM VOWEL SIGN U;Mn;0;NSM;;;;;N;;;;;
+115B3;SIDDHAM VOWEL SIGN UU;Mn;0;NSM;;;;;N;;;;;
+115B4;SIDDHAM VOWEL SIGN VOCALIC R;Mn;0;NSM;;;;;N;;;;;
+115B5;SIDDHAM VOWEL SIGN VOCALIC RR;Mn;0;NSM;;;;;N;;;;;
+115B8;SIDDHAM VOWEL SIGN E;Mc;0;L;;;;;N;;;;;
+115B9;SIDDHAM VOWEL SIGN AI;Mc;0;L;;;;;N;;;;;
+115BA;SIDDHAM VOWEL SIGN O;Mc;0;L;115B8 115AF;;;;N;;;;;
+115BB;SIDDHAM VOWEL SIGN AU;Mc;0;L;115B9 115AF;;;;N;;;;;
+115BC;SIDDHAM SIGN CANDRABINDU;Mn;0;NSM;;;;;N;;;;;
+115BD;SIDDHAM SIGN ANUSVARA;Mn;0;NSM;;;;;N;;;;;
+115BE;SIDDHAM SIGN VISARGA;Mc;0;L;;;;;N;;;;;
+115BF;SIDDHAM SIGN VIRAMA;Mn;9;NSM;;;;;N;;;;;
+115C0;SIDDHAM SIGN NUKTA;Mn;7;NSM;;;;;N;;;;;
+115C1;SIDDHAM SIGN SIDDHAM;Po;0;L;;;;;N;;;;;
+115C2;SIDDHAM DANDA;Po;0;L;;;;;N;;;;;
+115C3;SIDDHAM DOUBLE DANDA;Po;0;L;;;;;N;;;;;
+115C4;SIDDHAM SEPARATOR DOT;Po;0;L;;;;;N;;;;;
+115C5;SIDDHAM SEPARATOR BAR;Po;0;L;;;;;N;;;;;
+115C6;SIDDHAM REPETITION MARK-1;Po;0;L;;;;;N;;;;;
+115C7;SIDDHAM REPETITION MARK-2;Po;0;L;;;;;N;;;;;
+115C8;SIDDHAM REPETITION MARK-3;Po;0;L;;;;;N;;;;;
+115C9;SIDDHAM END OF TEXT MARK;Po;0;L;;;;;N;;;;;
+115CA;SIDDHAM SECTION MARK WITH TRIDENT AND U-SHAPED ORNAMENTS;Po;0;L;;;;;N;;;;;
+115CB;SIDDHAM SECTION MARK WITH TRIDENT AND DOTTED CRESCENTS;Po;0;L;;;;;N;;;;;
+115CC;SIDDHAM SECTION MARK WITH RAYS AND DOTTED CRESCENTS;Po;0;L;;;;;N;;;;;
+115CD;SIDDHAM SECTION MARK WITH RAYS AND DOTTED DOUBLE CRESCENTS;Po;0;L;;;;;N;;;;;
+115CE;SIDDHAM SECTION MARK WITH RAYS AND DOTTED TRIPLE CRESCENTS;Po;0;L;;;;;N;;;;;
+115CF;SIDDHAM SECTION MARK DOUBLE RING;Po;0;L;;;;;N;;;;;
+115D0;SIDDHAM SECTION MARK DOUBLE RING WITH RAYS;Po;0;L;;;;;N;;;;;
+115D1;SIDDHAM SECTION MARK WITH DOUBLE CRESCENTS;Po;0;L;;;;;N;;;;;
+115D2;SIDDHAM SECTION MARK WITH TRIPLE CRESCENTS;Po;0;L;;;;;N;;;;;
+115D3;SIDDHAM SECTION MARK WITH QUADRUPLE CRESCENTS;Po;0;L;;;;;N;;;;;
+115D4;SIDDHAM SECTION MARK WITH SEPTUPLE CRESCENTS;Po;0;L;;;;;N;;;;;
+115D5;SIDDHAM SECTION MARK WITH CIRCLES AND RAYS;Po;0;L;;;;;N;;;;;
+115D6;SIDDHAM SECTION MARK WITH CIRCLES AND TWO ENCLOSURES;Po;0;L;;;;;N;;;;;
+115D7;SIDDHAM SECTION MARK WITH CIRCLES AND FOUR ENCLOSURES;Po;0;L;;;;;N;;;;;
+115D8;SIDDHAM LETTER THREE-CIRCLE ALTERNATE I;Lo;0;L;;;;;N;;;;;
+115D9;SIDDHAM LETTER TWO-CIRCLE ALTERNATE I;Lo;0;L;;;;;N;;;;;
+115DA;SIDDHAM LETTER TWO-CIRCLE ALTERNATE II;Lo;0;L;;;;;N;;;;;
+115DB;SIDDHAM LETTER ALTERNATE U;Lo;0;L;;;;;N;;;;;
+115DC;SIDDHAM VOWEL SIGN ALTERNATE U;Mn;0;NSM;;;;;N;;;;;
+115DD;SIDDHAM VOWEL SIGN ALTERNATE UU;Mn;0;NSM;;;;;N;;;;;
+11600;MODI LETTER A;Lo;0;L;;;;;N;;;;;
+11601;MODI LETTER AA;Lo;0;L;;;;;N;;;;;
+11602;MODI LETTER I;Lo;0;L;;;;;N;;;;;
+11603;MODI LETTER II;Lo;0;L;;;;;N;;;;;
+11604;MODI LETTER U;Lo;0;L;;;;;N;;;;;
+11605;MODI LETTER UU;Lo;0;L;;;;;N;;;;;
+11606;MODI LETTER VOCALIC R;Lo;0;L;;;;;N;;;;;
+11607;MODI LETTER VOCALIC RR;Lo;0;L;;;;;N;;;;;
+11608;MODI LETTER VOCALIC L;Lo;0;L;;;;;N;;;;;
+11609;MODI LETTER VOCALIC LL;Lo;0;L;;;;;N;;;;;
+1160A;MODI LETTER E;Lo;0;L;;;;;N;;;;;
+1160B;MODI LETTER AI;Lo;0;L;;;;;N;;;;;
+1160C;MODI LETTER O;Lo;0;L;;;;;N;;;;;
+1160D;MODI LETTER AU;Lo;0;L;;;;;N;;;;;
+1160E;MODI LETTER KA;Lo;0;L;;;;;N;;;;;
+1160F;MODI LETTER KHA;Lo;0;L;;;;;N;;;;;
+11610;MODI LETTER GA;Lo;0;L;;;;;N;;;;;
+11611;MODI LETTER GHA;Lo;0;L;;;;;N;;;;;
+11612;MODI LETTER NGA;Lo;0;L;;;;;N;;;;;
+11613;MODI LETTER CA;Lo;0;L;;;;;N;;;;;
+11614;MODI LETTER CHA;Lo;0;L;;;;;N;;;;;
+11615;MODI LETTER JA;Lo;0;L;;;;;N;;;;;
+11616;MODI LETTER JHA;Lo;0;L;;;;;N;;;;;
+11617;MODI LETTER NYA;Lo;0;L;;;;;N;;;;;
+11618;MODI LETTER TTA;Lo;0;L;;;;;N;;;;;
+11619;MODI LETTER TTHA;Lo;0;L;;;;;N;;;;;
+1161A;MODI LETTER DDA;Lo;0;L;;;;;N;;;;;
+1161B;MODI LETTER DDHA;Lo;0;L;;;;;N;;;;;
+1161C;MODI LETTER NNA;Lo;0;L;;;;;N;;;;;
+1161D;MODI LETTER TA;Lo;0;L;;;;;N;;;;;
+1161E;MODI LETTER THA;Lo;0;L;;;;;N;;;;;
+1161F;MODI LETTER DA;Lo;0;L;;;;;N;;;;;
+11620;MODI LETTER DHA;Lo;0;L;;;;;N;;;;;
+11621;MODI LETTER NA;Lo;0;L;;;;;N;;;;;
+11622;MODI LETTER PA;Lo;0;L;;;;;N;;;;;
+11623;MODI LETTER PHA;Lo;0;L;;;;;N;;;;;
+11624;MODI LETTER BA;Lo;0;L;;;;;N;;;;;
+11625;MODI LETTER BHA;Lo;0;L;;;;;N;;;;;
+11626;MODI LETTER MA;Lo;0;L;;;;;N;;;;;
+11627;MODI LETTER YA;Lo;0;L;;;;;N;;;;;
+11628;MODI LETTER RA;Lo;0;L;;;;;N;;;;;
+11629;MODI LETTER LA;Lo;0;L;;;;;N;;;;;
+1162A;MODI LETTER VA;Lo;0;L;;;;;N;;;;;
+1162B;MODI LETTER SHA;Lo;0;L;;;;;N;;;;;
+1162C;MODI LETTER SSA;Lo;0;L;;;;;N;;;;;
+1162D;MODI LETTER SA;Lo;0;L;;;;;N;;;;;
+1162E;MODI LETTER HA;Lo;0;L;;;;;N;;;;;
+1162F;MODI LETTER LLA;Lo;0;L;;;;;N;;;;;
+11630;MODI VOWEL SIGN AA;Mc;0;L;;;;;N;;;;;
+11631;MODI VOWEL SIGN I;Mc;0;L;;;;;N;;;;;
+11632;MODI VOWEL SIGN II;Mc;0;L;;;;;N;;;;;
+11633;MODI VOWEL SIGN U;Mn;0;NSM;;;;;N;;;;;
+11634;MODI VOWEL SIGN UU;Mn;0;NSM;;;;;N;;;;;
+11635;MODI VOWEL SIGN VOCALIC R;Mn;0;NSM;;;;;N;;;;;
+11636;MODI VOWEL SIGN VOCALIC RR;Mn;0;NSM;;;;;N;;;;;
+11637;MODI VOWEL SIGN VOCALIC L;Mn;0;NSM;;;;;N;;;;;
+11638;MODI VOWEL SIGN VOCALIC LL;Mn;0;NSM;;;;;N;;;;;
+11639;MODI VOWEL SIGN E;Mn;0;NSM;;;;;N;;;;;
+1163A;MODI VOWEL SIGN AI;Mn;0;NSM;;;;;N;;;;;
+1163B;MODI VOWEL SIGN O;Mc;0;L;;;;;N;;;;;
+1163C;MODI VOWEL SIGN AU;Mc;0;L;;;;;N;;;;;
+1163D;MODI SIGN ANUSVARA;Mn;0;NSM;;;;;N;;;;;
+1163E;MODI SIGN VISARGA;Mc;0;L;;;;;N;;;;;
+1163F;MODI SIGN VIRAMA;Mn;9;NSM;;;;;N;;;;;
+11640;MODI SIGN ARDHACANDRA;Mn;0;NSM;;;;;N;;;;;
+11641;MODI DANDA;Po;0;L;;;;;N;;;;;
+11642;MODI DOUBLE DANDA;Po;0;L;;;;;N;;;;;
+11643;MODI ABBREVIATION SIGN;Po;0;L;;;;;N;;;;;
+11644;MODI SIGN HUVA;Lo;0;L;;;;;N;;;;;
+11650;MODI DIGIT ZERO;Nd;0;L;;0;0;0;N;;;;;
+11651;MODI DIGIT ONE;Nd;0;L;;1;1;1;N;;;;;
+11652;MODI DIGIT TWO;Nd;0;L;;2;2;2;N;;;;;
+11653;MODI DIGIT THREE;Nd;0;L;;3;3;3;N;;;;;
+11654;MODI DIGIT FOUR;Nd;0;L;;4;4;4;N;;;;;
+11655;MODI DIGIT FIVE;Nd;0;L;;5;5;5;N;;;;;
+11656;MODI DIGIT SIX;Nd;0;L;;6;6;6;N;;;;;
+11657;MODI DIGIT SEVEN;Nd;0;L;;7;7;7;N;;;;;
+11658;MODI DIGIT EIGHT;Nd;0;L;;8;8;8;N;;;;;
+11659;MODI DIGIT NINE;Nd;0;L;;9;9;9;N;;;;;
+11660;MONGOLIAN BIRGA WITH ORNAMENT;Po;0;ON;;;;;N;;;;;
+11661;MONGOLIAN ROTATED BIRGA;Po;0;ON;;;;;N;;;;;
+11662;MONGOLIAN DOUBLE BIRGA WITH ORNAMENT;Po;0;ON;;;;;N;;;;;
+11663;MONGOLIAN TRIPLE BIRGA WITH ORNAMENT;Po;0;ON;;;;;N;;;;;
+11664;MONGOLIAN BIRGA WITH DOUBLE ORNAMENT;Po;0;ON;;;;;N;;;;;
+11665;MONGOLIAN ROTATED BIRGA WITH ORNAMENT;Po;0;ON;;;;;N;;;;;
+11666;MONGOLIAN ROTATED BIRGA WITH DOUBLE ORNAMENT;Po;0;ON;;;;;N;;;;;
+11667;MONGOLIAN INVERTED BIRGA;Po;0;ON;;;;;N;;;;;
+11668;MONGOLIAN INVERTED BIRGA WITH DOUBLE ORNAMENT;Po;0;ON;;;;;N;;;;;
+11669;MONGOLIAN SWIRL BIRGA;Po;0;ON;;;;;N;;;;;
+1166A;MONGOLIAN SWIRL BIRGA WITH ORNAMENT;Po;0;ON;;;;;N;;;;;
+1166B;MONGOLIAN SWIRL BIRGA WITH DOUBLE ORNAMENT;Po;0;ON;;;;;N;;;;;
+1166C;MONGOLIAN TURNED SWIRL BIRGA WITH DOUBLE ORNAMENT;Po;0;ON;;;;;N;;;;;
+11680;TAKRI LETTER A;Lo;0;L;;;;;N;;;;;
+11681;TAKRI LETTER AA;Lo;0;L;;;;;N;;;;;
+11682;TAKRI LETTER I;Lo;0;L;;;;;N;;;;;
+11683;TAKRI LETTER II;Lo;0;L;;;;;N;;;;;
+11684;TAKRI LETTER U;Lo;0;L;;;;;N;;;;;
+11685;TAKRI LETTER UU;Lo;0;L;;;;;N;;;;;
+11686;TAKRI LETTER E;Lo;0;L;;;;;N;;;;;
+11687;TAKRI LETTER AI;Lo;0;L;;;;;N;;;;;
+11688;TAKRI LETTER O;Lo;0;L;;;;;N;;;;;
+11689;TAKRI LETTER AU;Lo;0;L;;;;;N;;;;;
+1168A;TAKRI LETTER KA;Lo;0;L;;;;;N;;;;;
+1168B;TAKRI LETTER KHA;Lo;0;L;;;;;N;;;;;
+1168C;TAKRI LETTER GA;Lo;0;L;;;;;N;;;;;
+1168D;TAKRI LETTER GHA;Lo;0;L;;;;;N;;;;;
+1168E;TAKRI LETTER NGA;Lo;0;L;;;;;N;;;;;
+1168F;TAKRI LETTER CA;Lo;0;L;;;;;N;;;;;
+11690;TAKRI LETTER CHA;Lo;0;L;;;;;N;;;;;
+11691;TAKRI LETTER JA;Lo;0;L;;;;;N;;;;;
+11692;TAKRI LETTER JHA;Lo;0;L;;;;;N;;;;;
+11693;TAKRI LETTER NYA;Lo;0;L;;;;;N;;;;;
+11694;TAKRI LETTER TTA;Lo;0;L;;;;;N;;;;;
+11695;TAKRI LETTER TTHA;Lo;0;L;;;;;N;;;;;
+11696;TAKRI LETTER DDA;Lo;0;L;;;;;N;;;;;
+11697;TAKRI LETTER DDHA;Lo;0;L;;;;;N;;;;;
+11698;TAKRI LETTER NNA;Lo;0;L;;;;;N;;;;;
+11699;TAKRI LETTER TA;Lo;0;L;;;;;N;;;;;
+1169A;TAKRI LETTER THA;Lo;0;L;;;;;N;;;;;
+1169B;TAKRI LETTER DA;Lo;0;L;;;;;N;;;;;
+1169C;TAKRI LETTER DHA;Lo;0;L;;;;;N;;;;;
+1169D;TAKRI LETTER NA;Lo;0;L;;;;;N;;;;;
+1169E;TAKRI LETTER PA;Lo;0;L;;;;;N;;;;;
+1169F;TAKRI LETTER PHA;Lo;0;L;;;;;N;;;;;
+116A0;TAKRI LETTER BA;Lo;0;L;;;;;N;;;;;
+116A1;TAKRI LETTER BHA;Lo;0;L;;;;;N;;;;;
+116A2;TAKRI LETTER MA;Lo;0;L;;;;;N;;;;;
+116A3;TAKRI LETTER YA;Lo;0;L;;;;;N;;;;;
+116A4;TAKRI LETTER RA;Lo;0;L;;;;;N;;;;;
+116A5;TAKRI LETTER LA;Lo;0;L;;;;;N;;;;;
+116A6;TAKRI LETTER VA;Lo;0;L;;;;;N;;;;;
+116A7;TAKRI LETTER SHA;Lo;0;L;;;;;N;;;;;
+116A8;TAKRI LETTER SA;Lo;0;L;;;;;N;;;;;
+116A9;TAKRI LETTER HA;Lo;0;L;;;;;N;;;;;
+116AA;TAKRI LETTER RRA;Lo;0;L;;;;;N;;;;;
+116AB;TAKRI SIGN ANUSVARA;Mn;0;NSM;;;;;N;;;;;
+116AC;TAKRI SIGN VISARGA;Mc;0;L;;;;;N;;;;;
+116AD;TAKRI VOWEL SIGN AA;Mn;0;NSM;;;;;N;;;;;
+116AE;TAKRI VOWEL SIGN I;Mc;0;L;;;;;N;;;;;
+116AF;TAKRI VOWEL SIGN II;Mc;0;L;;;;;N;;;;;
+116B0;TAKRI VOWEL SIGN U;Mn;0;NSM;;;;;N;;;;;
+116B1;TAKRI VOWEL SIGN UU;Mn;0;NSM;;;;;N;;;;;
+116B2;TAKRI VOWEL SIGN E;Mn;0;NSM;;;;;N;;;;;
+116B3;TAKRI VOWEL SIGN AI;Mn;0;NSM;;;;;N;;;;;
+116B4;TAKRI VOWEL SIGN O;Mn;0;NSM;;;;;N;;;;;
+116B5;TAKRI VOWEL SIGN AU;Mn;0;NSM;;;;;N;;;;;
+116B6;TAKRI SIGN VIRAMA;Mc;9;L;;;;;N;;;;;
+116B7;TAKRI SIGN NUKTA;Mn;7;NSM;;;;;N;;;;;
+116C0;TAKRI DIGIT ZERO;Nd;0;L;;0;0;0;N;;;;;
+116C1;TAKRI DIGIT ONE;Nd;0;L;;1;1;1;N;;;;;
+116C2;TAKRI DIGIT TWO;Nd;0;L;;2;2;2;N;;;;;
+116C3;TAKRI DIGIT THREE;Nd;0;L;;3;3;3;N;;;;;
+116C4;TAKRI DIGIT FOUR;Nd;0;L;;4;4;4;N;;;;;
+116C5;TAKRI DIGIT FIVE;Nd;0;L;;5;5;5;N;;;;;
+116C6;TAKRI DIGIT SIX;Nd;0;L;;6;6;6;N;;;;;
+116C7;TAKRI DIGIT SEVEN;Nd;0;L;;7;7;7;N;;;;;
+116C8;TAKRI DIGIT EIGHT;Nd;0;L;;8;8;8;N;;;;;
+116C9;TAKRI DIGIT NINE;Nd;0;L;;9;9;9;N;;;;;
+11700;AHOM LETTER KA;Lo;0;L;;;;;N;;;;;
+11701;AHOM LETTER KHA;Lo;0;L;;;;;N;;;;;
+11702;AHOM LETTER NGA;Lo;0;L;;;;;N;;;;;
+11703;AHOM LETTER NA;Lo;0;L;;;;;N;;;;;
+11704;AHOM LETTER TA;Lo;0;L;;;;;N;;;;;
+11705;AHOM LETTER ALTERNATE TA;Lo;0;L;;;;;N;;;;;
+11706;AHOM LETTER PA;Lo;0;L;;;;;N;;;;;
+11707;AHOM LETTER PHA;Lo;0;L;;;;;N;;;;;
+11708;AHOM LETTER BA;Lo;0;L;;;;;N;;;;;
+11709;AHOM LETTER MA;Lo;0;L;;;;;N;;;;;
+1170A;AHOM LETTER JA;Lo;0;L;;;;;N;;;;;
+1170B;AHOM LETTER CHA;Lo;0;L;;;;;N;;;;;
+1170C;AHOM LETTER THA;Lo;0;L;;;;;N;;;;;
+1170D;AHOM LETTER RA;Lo;0;L;;;;;N;;;;;
+1170E;AHOM LETTER LA;Lo;0;L;;;;;N;;;;;
+1170F;AHOM LETTER SA;Lo;0;L;;;;;N;;;;;
+11710;AHOM LETTER NYA;Lo;0;L;;;;;N;;;;;
+11711;AHOM LETTER HA;Lo;0;L;;;;;N;;;;;
+11712;AHOM LETTER A;Lo;0;L;;;;;N;;;;;
+11713;AHOM LETTER DA;Lo;0;L;;;;;N;;;;;
+11714;AHOM LETTER DHA;Lo;0;L;;;;;N;;;;;
+11715;AHOM LETTER GA;Lo;0;L;;;;;N;;;;;
+11716;AHOM LETTER ALTERNATE GA;Lo;0;L;;;;;N;;;;;
+11717;AHOM LETTER GHA;Lo;0;L;;;;;N;;;;;
+11718;AHOM LETTER BHA;Lo;0;L;;;;;N;;;;;
+11719;AHOM LETTER JHA;Lo;0;L;;;;;N;;;;;
+1171D;AHOM CONSONANT SIGN MEDIAL LA;Mn;0;NSM;;;;;N;;;;;
+1171E;AHOM CONSONANT SIGN MEDIAL RA;Mn;0;NSM;;;;;N;;;;;
+1171F;AHOM CONSONANT SIGN MEDIAL LIGATING RA;Mn;0;NSM;;;;;N;;;;;
+11720;AHOM VOWEL SIGN A;Mc;0;L;;;;;N;;;;;
+11721;AHOM VOWEL SIGN AA;Mc;0;L;;;;;N;;;;;
+11722;AHOM VOWEL SIGN I;Mn;0;NSM;;;;;N;;;;;
+11723;AHOM VOWEL SIGN II;Mn;0;NSM;;;;;N;;;;;
+11724;AHOM VOWEL SIGN U;Mn;0;NSM;;;;;N;;;;;
+11725;AHOM VOWEL SIGN UU;Mn;0;NSM;;;;;N;;;;;
+11726;AHOM VOWEL SIGN E;Mc;0;L;;;;;N;;;;;
+11727;AHOM VOWEL SIGN AW;Mn;0;NSM;;;;;N;;;;;
+11728;AHOM VOWEL SIGN O;Mn;0;NSM;;;;;N;;;;;
+11729;AHOM VOWEL SIGN AI;Mn;0;NSM;;;;;N;;;;;
+1172A;AHOM VOWEL SIGN AM;Mn;0;NSM;;;;;N;;;;;
+1172B;AHOM SIGN KILLER;Mn;9;NSM;;;;;N;;;;;
+11730;AHOM DIGIT ZERO;Nd;0;L;;0;0;0;N;;;;;
+11731;AHOM DIGIT ONE;Nd;0;L;;1;1;1;N;;;;;
+11732;AHOM DIGIT TWO;Nd;0;L;;2;2;2;N;;;;;
+11733;AHOM DIGIT THREE;Nd;0;L;;3;3;3;N;;;;;
+11734;AHOM DIGIT FOUR;Nd;0;L;;4;4;4;N;;;;;
+11735;AHOM DIGIT FIVE;Nd;0;L;;5;5;5;N;;;;;
+11736;AHOM DIGIT SIX;Nd;0;L;;6;6;6;N;;;;;
+11737;AHOM DIGIT SEVEN;Nd;0;L;;7;7;7;N;;;;;
+11738;AHOM DIGIT EIGHT;Nd;0;L;;8;8;8;N;;;;;
+11739;AHOM DIGIT NINE;Nd;0;L;;9;9;9;N;;;;;
+1173A;AHOM NUMBER TEN;No;0;L;;;;10;N;;;;;
+1173B;AHOM NUMBER TWENTY;No;0;L;;;;20;N;;;;;
+1173C;AHOM SIGN SMALL SECTION;Po;0;L;;;;;N;;;;;
+1173D;AHOM SIGN SECTION;Po;0;L;;;;;N;;;;;
+1173E;AHOM SIGN RULAI;Po;0;L;;;;;N;;;;;
+1173F;AHOM SYMBOL VI;So;0;L;;;;;N;;;;;
+118A0;WARANG CITI CAPITAL LETTER NGAA;Lu;0;L;;;;;N;;;;118C0;
+118A1;WARANG CITI CAPITAL LETTER A;Lu;0;L;;;;;N;;;;118C1;
+118A2;WARANG CITI CAPITAL LETTER WI;Lu;0;L;;;;;N;;;;118C2;
+118A3;WARANG CITI CAPITAL LETTER YU;Lu;0;L;;;;;N;;;;118C3;
+118A4;WARANG CITI CAPITAL LETTER YA;Lu;0;L;;;;;N;;;;118C4;
+118A5;WARANG CITI CAPITAL LETTER YO;Lu;0;L;;;;;N;;;;118C5;
+118A6;WARANG CITI CAPITAL LETTER II;Lu;0;L;;;;;N;;;;118C6;
+118A7;WARANG CITI CAPITAL LETTER UU;Lu;0;L;;;;;N;;;;118C7;
+118A8;WARANG CITI CAPITAL LETTER E;Lu;0;L;;;;;N;;;;118C8;
+118A9;WARANG CITI CAPITAL LETTER O;Lu;0;L;;;;;N;;;;118C9;
+118AA;WARANG CITI CAPITAL LETTER ANG;Lu;0;L;;;;;N;;;;118CA;
+118AB;WARANG CITI CAPITAL LETTER GA;Lu;0;L;;;;;N;;;;118CB;
+118AC;WARANG CITI CAPITAL LETTER KO;Lu;0;L;;;;;N;;;;118CC;
+118AD;WARANG CITI CAPITAL LETTER ENY;Lu;0;L;;;;;N;;;;118CD;
+118AE;WARANG CITI CAPITAL LETTER YUJ;Lu;0;L;;;;;N;;;;118CE;
+118AF;WARANG CITI CAPITAL LETTER UC;Lu;0;L;;;;;N;;;;118CF;
+118B0;WARANG CITI CAPITAL LETTER ENN;Lu;0;L;;;;;N;;;;118D0;
+118B1;WARANG CITI CAPITAL LETTER ODD;Lu;0;L;;;;;N;;;;118D1;
+118B2;WARANG CITI CAPITAL LETTER TTE;Lu;0;L;;;;;N;;;;118D2;
+118B3;WARANG CITI CAPITAL LETTER NUNG;Lu;0;L;;;;;N;;;;118D3;
+118B4;WARANG CITI CAPITAL LETTER DA;Lu;0;L;;;;;N;;;;118D4;
+118B5;WARANG CITI CAPITAL LETTER AT;Lu;0;L;;;;;N;;;;118D5;
+118B6;WARANG CITI CAPITAL LETTER AM;Lu;0;L;;;;;N;;;;118D6;
+118B7;WARANG CITI CAPITAL LETTER BU;Lu;0;L;;;;;N;;;;118D7;
+118B8;WARANG CITI CAPITAL LETTER PU;Lu;0;L;;;;;N;;;;118D8;
+118B9;WARANG CITI CAPITAL LETTER HIYO;Lu;0;L;;;;;N;;;;118D9;
+118BA;WARANG CITI CAPITAL LETTER HOLO;Lu;0;L;;;;;N;;;;118DA;
+118BB;WARANG CITI CAPITAL LETTER HORR;Lu;0;L;;;;;N;;;;118DB;
+118BC;WARANG CITI CAPITAL LETTER HAR;Lu;0;L;;;;;N;;;;118DC;
+118BD;WARANG CITI CAPITAL LETTER SSUU;Lu;0;L;;;;;N;;;;118DD;
+118BE;WARANG CITI CAPITAL LETTER SII;Lu;0;L;;;;;N;;;;118DE;
+118BF;WARANG CITI CAPITAL LETTER VIYO;Lu;0;L;;;;;N;;;;118DF;
+118C0;WARANG CITI SMALL LETTER NGAA;Ll;0;L;;;;;N;;;118A0;;118A0
+118C1;WARANG CITI SMALL LETTER A;Ll;0;L;;;;;N;;;118A1;;118A1
+118C2;WARANG CITI SMALL LETTER WI;Ll;0;L;;;;;N;;;118A2;;118A2
+118C3;WARANG CITI SMALL LETTER YU;Ll;0;L;;;;;N;;;118A3;;118A3
+118C4;WARANG CITI SMALL LETTER YA;Ll;0;L;;;;;N;;;118A4;;118A4
+118C5;WARANG CITI SMALL LETTER YO;Ll;0;L;;;;;N;;;118A5;;118A5
+118C6;WARANG CITI SMALL LETTER II;Ll;0;L;;;;;N;;;118A6;;118A6
+118C7;WARANG CITI SMALL LETTER UU;Ll;0;L;;;;;N;;;118A7;;118A7
+118C8;WARANG CITI SMALL LETTER E;Ll;0;L;;;;;N;;;118A8;;118A8
+118C9;WARANG CITI SMALL LETTER O;Ll;0;L;;;;;N;;;118A9;;118A9
+118CA;WARANG CITI SMALL LETTER ANG;Ll;0;L;;;;;N;;;118AA;;118AA
+118CB;WARANG CITI SMALL LETTER GA;Ll;0;L;;;;;N;;;118AB;;118AB
+118CC;WARANG CITI SMALL LETTER KO;Ll;0;L;;;;;N;;;118AC;;118AC
+118CD;WARANG CITI SMALL LETTER ENY;Ll;0;L;;;;;N;;;118AD;;118AD
+118CE;WARANG CITI SMALL LETTER YUJ;Ll;0;L;;;;;N;;;118AE;;118AE
+118CF;WARANG CITI SMALL LETTER UC;Ll;0;L;;;;;N;;;118AF;;118AF
+118D0;WARANG CITI SMALL LETTER ENN;Ll;0;L;;;;;N;;;118B0;;118B0
+118D1;WARANG CITI SMALL LETTER ODD;Ll;0;L;;;;;N;;;118B1;;118B1
+118D2;WARANG CITI SMALL LETTER TTE;Ll;0;L;;;;;N;;;118B2;;118B2
+118D3;WARANG CITI SMALL LETTER NUNG;Ll;0;L;;;;;N;;;118B3;;118B3
+118D4;WARANG CITI SMALL LETTER DA;Ll;0;L;;;;;N;;;118B4;;118B4
+118D5;WARANG CITI SMALL LETTER AT;Ll;0;L;;;;;N;;;118B5;;118B5
+118D6;WARANG CITI SMALL LETTER AM;Ll;0;L;;;;;N;;;118B6;;118B6
+118D7;WARANG CITI SMALL LETTER BU;Ll;0;L;;;;;N;;;118B7;;118B7
+118D8;WARANG CITI SMALL LETTER PU;Ll;0;L;;;;;N;;;118B8;;118B8
+118D9;WARANG CITI SMALL LETTER HIYO;Ll;0;L;;;;;N;;;118B9;;118B9
+118DA;WARANG CITI SMALL LETTER HOLO;Ll;0;L;;;;;N;;;118BA;;118BA
+118DB;WARANG CITI SMALL LETTER HORR;Ll;0;L;;;;;N;;;118BB;;118BB
+118DC;WARANG CITI SMALL LETTER HAR;Ll;0;L;;;;;N;;;118BC;;118BC
+118DD;WARANG CITI SMALL LETTER SSUU;Ll;0;L;;;;;N;;;118BD;;118BD
+118DE;WARANG CITI SMALL LETTER SII;Ll;0;L;;;;;N;;;118BE;;118BE
+118DF;WARANG CITI SMALL LETTER VIYO;Ll;0;L;;;;;N;;;118BF;;118BF
+118E0;WARANG CITI DIGIT ZERO;Nd;0;L;;0;0;0;N;;;;;
+118E1;WARANG CITI DIGIT ONE;Nd;0;L;;1;1;1;N;;;;;
+118E2;WARANG CITI DIGIT TWO;Nd;0;L;;2;2;2;N;;;;;
+118E3;WARANG CITI DIGIT THREE;Nd;0;L;;3;3;3;N;;;;;
+118E4;WARANG CITI DIGIT FOUR;Nd;0;L;;4;4;4;N;;;;;
+118E5;WARANG CITI DIGIT FIVE;Nd;0;L;;5;5;5;N;;;;;
+118E6;WARANG CITI DIGIT SIX;Nd;0;L;;6;6;6;N;;;;;
+118E7;WARANG CITI DIGIT SEVEN;Nd;0;L;;7;7;7;N;;;;;
+118E8;WARANG CITI DIGIT EIGHT;Nd;0;L;;8;8;8;N;;;;;
+118E9;WARANG CITI DIGIT NINE;Nd;0;L;;9;9;9;N;;;;;
+118EA;WARANG CITI NUMBER TEN;No;0;L;;;;10;N;;;;;
+118EB;WARANG CITI NUMBER TWENTY;No;0;L;;;;20;N;;;;;
+118EC;WARANG CITI NUMBER THIRTY;No;0;L;;;;30;N;;;;;
+118ED;WARANG CITI NUMBER FORTY;No;0;L;;;;40;N;;;;;
+118EE;WARANG CITI NUMBER FIFTY;No;0;L;;;;50;N;;;;;
+118EF;WARANG CITI NUMBER SIXTY;No;0;L;;;;60;N;;;;;
+118F0;WARANG CITI NUMBER SEVENTY;No;0;L;;;;70;N;;;;;
+118F1;WARANG CITI NUMBER EIGHTY;No;0;L;;;;80;N;;;;;
+118F2;WARANG CITI NUMBER NINETY;No;0;L;;;;90;N;;;;;
+118FF;WARANG CITI OM;Lo;0;L;;;;;N;;;;;
+11AC0;PAU CIN HAU LETTER PA;Lo;0;L;;;;;N;;;;;
+11AC1;PAU CIN HAU LETTER KA;Lo;0;L;;;;;N;;;;;
+11AC2;PAU CIN HAU LETTER LA;Lo;0;L;;;;;N;;;;;
+11AC3;PAU CIN HAU LETTER MA;Lo;0;L;;;;;N;;;;;
+11AC4;PAU CIN HAU LETTER DA;Lo;0;L;;;;;N;;;;;
+11AC5;PAU CIN HAU LETTER ZA;Lo;0;L;;;;;N;;;;;
+11AC6;PAU CIN HAU LETTER VA;Lo;0;L;;;;;N;;;;;
+11AC7;PAU CIN HAU LETTER NGA;Lo;0;L;;;;;N;;;;;
+11AC8;PAU CIN HAU LETTER HA;Lo;0;L;;;;;N;;;;;
+11AC9;PAU CIN HAU LETTER GA;Lo;0;L;;;;;N;;;;;
+11ACA;PAU CIN HAU LETTER KHA;Lo;0;L;;;;;N;;;;;
+11ACB;PAU CIN HAU LETTER SA;Lo;0;L;;;;;N;;;;;
+11ACC;PAU CIN HAU LETTER BA;Lo;0;L;;;;;N;;;;;
+11ACD;PAU CIN HAU LETTER CA;Lo;0;L;;;;;N;;;;;
+11ACE;PAU CIN HAU LETTER TA;Lo;0;L;;;;;N;;;;;
+11ACF;PAU CIN HAU LETTER THA;Lo;0;L;;;;;N;;;;;
+11AD0;PAU CIN HAU LETTER NA;Lo;0;L;;;;;N;;;;;
+11AD1;PAU CIN HAU LETTER PHA;Lo;0;L;;;;;N;;;;;
+11AD2;PAU CIN HAU LETTER RA;Lo;0;L;;;;;N;;;;;
+11AD3;PAU CIN HAU LETTER FA;Lo;0;L;;;;;N;;;;;
+11AD4;PAU CIN HAU LETTER CHA;Lo;0;L;;;;;N;;;;;
+11AD5;PAU CIN HAU LETTER A;Lo;0;L;;;;;N;;;;;
+11AD6;PAU CIN HAU LETTER E;Lo;0;L;;;;;N;;;;;
+11AD7;PAU CIN HAU LETTER I;Lo;0;L;;;;;N;;;;;
+11AD8;PAU CIN HAU LETTER O;Lo;0;L;;;;;N;;;;;
+11AD9;PAU CIN HAU LETTER U;Lo;0;L;;;;;N;;;;;
+11ADA;PAU CIN HAU LETTER UA;Lo;0;L;;;;;N;;;;;
+11ADB;PAU CIN HAU LETTER IA;Lo;0;L;;;;;N;;;;;
+11ADC;PAU CIN HAU LETTER FINAL P;Lo;0;L;;;;;N;;;;;
+11ADD;PAU CIN HAU LETTER FINAL K;Lo;0;L;;;;;N;;;;;
+11ADE;PAU CIN HAU LETTER FINAL T;Lo;0;L;;;;;N;;;;;
+11ADF;PAU CIN HAU LETTER FINAL M;Lo;0;L;;;;;N;;;;;
+11AE0;PAU CIN HAU LETTER FINAL N;Lo;0;L;;;;;N;;;;;
+11AE1;PAU CIN HAU LETTER FINAL L;Lo;0;L;;;;;N;;;;;
+11AE2;PAU CIN HAU LETTER FINAL W;Lo;0;L;;;;;N;;;;;
+11AE3;PAU CIN HAU LETTER FINAL NG;Lo;0;L;;;;;N;;;;;
+11AE4;PAU CIN HAU LETTER FINAL Y;Lo;0;L;;;;;N;;;;;
+11AE5;PAU CIN HAU RISING TONE LONG;Lo;0;L;;;;;N;;;;;
+11AE6;PAU CIN HAU RISING TONE;Lo;0;L;;;;;N;;;;;
+11AE7;PAU CIN HAU SANDHI GLOTTAL STOP;Lo;0;L;;;;;N;;;;;
+11AE8;PAU CIN HAU RISING TONE LONG FINAL;Lo;0;L;;;;;N;;;;;
+11AE9;PAU CIN HAU RISING TONE FINAL;Lo;0;L;;;;;N;;;;;
+11AEA;PAU CIN HAU SANDHI GLOTTAL STOP FINAL;Lo;0;L;;;;;N;;;;;
+11AEB;PAU CIN HAU SANDHI TONE LONG;Lo;0;L;;;;;N;;;;;
+11AEC;PAU CIN HAU SANDHI TONE;Lo;0;L;;;;;N;;;;;
+11AED;PAU CIN HAU SANDHI TONE LONG FINAL;Lo;0;L;;;;;N;;;;;
+11AEE;PAU CIN HAU SANDHI TONE FINAL;Lo;0;L;;;;;N;;;;;
+11AEF;PAU CIN HAU MID-LEVEL TONE;Lo;0;L;;;;;N;;;;;
+11AF0;PAU CIN HAU GLOTTAL STOP VARIANT;Lo;0;L;;;;;N;;;;;
+11AF1;PAU CIN HAU MID-LEVEL TONE LONG FINAL;Lo;0;L;;;;;N;;;;;
+11AF2;PAU CIN HAU MID-LEVEL TONE FINAL;Lo;0;L;;;;;N;;;;;
+11AF3;PAU CIN HAU LOW-FALLING TONE LONG;Lo;0;L;;;;;N;;;;;
+11AF4;PAU CIN HAU LOW-FALLING TONE;Lo;0;L;;;;;N;;;;;
+11AF5;PAU CIN HAU GLOTTAL STOP;Lo;0;L;;;;;N;;;;;
+11AF6;PAU CIN HAU LOW-FALLING TONE LONG FINAL;Lo;0;L;;;;;N;;;;;
+11AF7;PAU CIN HAU LOW-FALLING TONE FINAL;Lo;0;L;;;;;N;;;;;
+11AF8;PAU CIN HAU GLOTTAL STOP FINAL;Lo;0;L;;;;;N;;;;;
+11C00;BHAIKSUKI LETTER A;Lo;0;L;;;;;N;;;;;
+11C01;BHAIKSUKI LETTER AA;Lo;0;L;;;;;N;;;;;
+11C02;BHAIKSUKI LETTER I;Lo;0;L;;;;;N;;;;;
+11C03;BHAIKSUKI LETTER II;Lo;0;L;;;;;N;;;;;
+11C04;BHAIKSUKI LETTER U;Lo;0;L;;;;;N;;;;;
+11C05;BHAIKSUKI LETTER UU;Lo;0;L;;;;;N;;;;;
+11C06;BHAIKSUKI LETTER VOCALIC R;Lo;0;L;;;;;N;;;;;
+11C07;BHAIKSUKI LETTER VOCALIC RR;Lo;0;L;;;;;N;;;;;
+11C08;BHAIKSUKI LETTER VOCALIC L;Lo;0;L;;;;;N;;;;;
+11C0A;BHAIKSUKI LETTER E;Lo;0;L;;;;;N;;;;;
+11C0B;BHAIKSUKI LETTER AI;Lo;0;L;;;;;N;;;;;
+11C0C;BHAIKSUKI LETTER O;Lo;0;L;;;;;N;;;;;
+11C0D;BHAIKSUKI LETTER AU;Lo;0;L;;;;;N;;;;;
+11C0E;BHAIKSUKI LETTER KA;Lo;0;L;;;;;N;;;;;
+11C0F;BHAIKSUKI LETTER KHA;Lo;0;L;;;;;N;;;;;
+11C10;BHAIKSUKI LETTER GA;Lo;0;L;;;;;N;;;;;
+11C11;BHAIKSUKI LETTER GHA;Lo;0;L;;;;;N;;;;;
+11C12;BHAIKSUKI LETTER NGA;Lo;0;L;;;;;N;;;;;
+11C13;BHAIKSUKI LETTER CA;Lo;0;L;;;;;N;;;;;
+11C14;BHAIKSUKI LETTER CHA;Lo;0;L;;;;;N;;;;;
+11C15;BHAIKSUKI LETTER JA;Lo;0;L;;;;;N;;;;;
+11C16;BHAIKSUKI LETTER JHA;Lo;0;L;;;;;N;;;;;
+11C17;BHAIKSUKI LETTER NYA;Lo;0;L;;;;;N;;;;;
+11C18;BHAIKSUKI LETTER TTA;Lo;0;L;;;;;N;;;;;
+11C19;BHAIKSUKI LETTER TTHA;Lo;0;L;;;;;N;;;;;
+11C1A;BHAIKSUKI LETTER DDA;Lo;0;L;;;;;N;;;;;
+11C1B;BHAIKSUKI LETTER DDHA;Lo;0;L;;;;;N;;;;;
+11C1C;BHAIKSUKI LETTER NNA;Lo;0;L;;;;;N;;;;;
+11C1D;BHAIKSUKI LETTER TA;Lo;0;L;;;;;N;;;;;
+11C1E;BHAIKSUKI LETTER THA;Lo;0;L;;;;;N;;;;;
+11C1F;BHAIKSUKI LETTER DA;Lo;0;L;;;;;N;;;;;
+11C20;BHAIKSUKI LETTER DHA;Lo;0;L;;;;;N;;;;;
+11C21;BHAIKSUKI LETTER NA;Lo;0;L;;;;;N;;;;;
+11C22;BHAIKSUKI LETTER PA;Lo;0;L;;;;;N;;;;;
+11C23;BHAIKSUKI LETTER PHA;Lo;0;L;;;;;N;;;;;
+11C24;BHAIKSUKI LETTER BA;Lo;0;L;;;;;N;;;;;
+11C25;BHAIKSUKI LETTER BHA;Lo;0;L;;;;;N;;;;;
+11C26;BHAIKSUKI LETTER MA;Lo;0;L;;;;;N;;;;;
+11C27;BHAIKSUKI LETTER YA;Lo;0;L;;;;;N;;;;;
+11C28;BHAIKSUKI LETTER RA;Lo;0;L;;;;;N;;;;;
+11C29;BHAIKSUKI LETTER LA;Lo;0;L;;;;;N;;;;;
+11C2A;BHAIKSUKI LETTER VA;Lo;0;L;;;;;N;;;;;
+11C2B;BHAIKSUKI LETTER SHA;Lo;0;L;;;;;N;;;;;
+11C2C;BHAIKSUKI LETTER SSA;Lo;0;L;;;;;N;;;;;
+11C2D;BHAIKSUKI LETTER SA;Lo;0;L;;;;;N;;;;;
+11C2E;BHAIKSUKI LETTER HA;Lo;0;L;;;;;N;;;;;
+11C2F;BHAIKSUKI VOWEL SIGN AA;Mc;0;L;;;;;N;;;;;
+11C30;BHAIKSUKI VOWEL SIGN I;Mn;0;NSM;;;;;N;;;;;
+11C31;BHAIKSUKI VOWEL SIGN II;Mn;0;NSM;;;;;N;;;;;
+11C32;BHAIKSUKI VOWEL SIGN U;Mn;0;NSM;;;;;N;;;;;
+11C33;BHAIKSUKI VOWEL SIGN UU;Mn;0;NSM;;;;;N;;;;;
+11C34;BHAIKSUKI VOWEL SIGN VOCALIC R;Mn;0;NSM;;;;;N;;;;;
+11C35;BHAIKSUKI VOWEL SIGN VOCALIC RR;Mn;0;NSM;;;;;N;;;;;
+11C36;BHAIKSUKI VOWEL SIGN VOCALIC L;Mn;0;NSM;;;;;N;;;;;
+11C38;BHAIKSUKI VOWEL SIGN E;Mn;0;NSM;;;;;N;;;;;
+11C39;BHAIKSUKI VOWEL SIGN AI;Mn;0;NSM;;;;;N;;;;;
+11C3A;BHAIKSUKI VOWEL SIGN O;Mn;0;NSM;;;;;N;;;;;
+11C3B;BHAIKSUKI VOWEL SIGN AU;Mn;0;NSM;;;;;N;;;;;
+11C3C;BHAIKSUKI SIGN CANDRABINDU;Mn;0;NSM;;;;;N;;;;;
+11C3D;BHAIKSUKI SIGN ANUSVARA;Mn;0;NSM;;;;;N;;;;;
+11C3E;BHAIKSUKI SIGN VISARGA;Mc;0;L;;;;;N;;;;;
+11C3F;BHAIKSUKI SIGN VIRAMA;Mn;9;L;;;;;N;;;;;
+11C40;BHAIKSUKI SIGN AVAGRAHA;Lo;0;L;;;;;N;;;;;
+11C41;BHAIKSUKI DANDA;Po;0;L;;;;;N;;;;;
+11C42;BHAIKSUKI DOUBLE DANDA;Po;0;L;;;;;N;;;;;
+11C43;BHAIKSUKI WORD SEPARATOR;Po;0;L;;;;;N;;;;;
+11C44;BHAIKSUKI GAP FILLER-1;Po;0;L;;;;;N;;;;;
+11C45;BHAIKSUKI GAP FILLER-2;Po;0;L;;;;;N;;;;;
+11C50;BHAIKSUKI DIGIT ZERO;Nd;0;L;;0;0;0;N;;;;;
+11C51;BHAIKSUKI DIGIT ONE;Nd;0;L;;1;1;1;N;;;;;
+11C52;BHAIKSUKI DIGIT TWO;Nd;0;L;;2;2;2;N;;;;;
+11C53;BHAIKSUKI DIGIT THREE;Nd;0;L;;3;3;3;N;;;;;
+11C54;BHAIKSUKI DIGIT FOUR;Nd;0;L;;4;4;4;N;;;;;
+11C55;BHAIKSUKI DIGIT FIVE;Nd;0;L;;5;5;5;N;;;;;
+11C56;BHAIKSUKI DIGIT SIX;Nd;0;L;;6;6;6;N;;;;;
+11C57;BHAIKSUKI DIGIT SEVEN;Nd;0;L;;7;7;7;N;;;;;
+11C58;BHAIKSUKI DIGIT EIGHT;Nd;0;L;;8;8;8;N;;;;;
+11C59;BHAIKSUKI DIGIT NINE;Nd;0;L;;9;9;9;N;;;;;
+11C5A;BHAIKSUKI NUMBER ONE;No;0;L;;;;1;N;;;;;
+11C5B;BHAIKSUKI NUMBER TWO;No;0;L;;;;2;N;;;;;
+11C5C;BHAIKSUKI NUMBER THREE;No;0;L;;;;3;N;;;;;
+11C5D;BHAIKSUKI NUMBER FOUR;No;0;L;;;;4;N;;;;;
+11C5E;BHAIKSUKI NUMBER FIVE;No;0;L;;;;5;N;;;;;
+11C5F;BHAIKSUKI NUMBER SIX;No;0;L;;;;6;N;;;;;
+11C60;BHAIKSUKI NUMBER SEVEN;No;0;L;;;;7;N;;;;;
+11C61;BHAIKSUKI NUMBER EIGHT;No;0;L;;;;8;N;;;;;
+11C62;BHAIKSUKI NUMBER NINE;No;0;L;;;;9;N;;;;;
+11C63;BHAIKSUKI NUMBER TEN;No;0;L;;;;10;N;;;;;
+11C64;BHAIKSUKI NUMBER TWENTY;No;0;L;;;;20;N;;;;;
+11C65;BHAIKSUKI NUMBER THIRTY;No;0;L;;;;30;N;;;;;
+11C66;BHAIKSUKI NUMBER FORTY;No;0;L;;;;40;N;;;;;
+11C67;BHAIKSUKI NUMBER FIFTY;No;0;L;;;;50;N;;;;;
+11C68;BHAIKSUKI NUMBER SIXTY;No;0;L;;;;60;N;;;;;
+11C69;BHAIKSUKI NUMBER SEVENTY;No;0;L;;;;70;N;;;;;
+11C6A;BHAIKSUKI NUMBER EIGHTY;No;0;L;;;;80;N;;;;;
+11C6B;BHAIKSUKI NUMBER NINETY;No;0;L;;;;90;N;;;;;
+11C6C;BHAIKSUKI HUNDREDS UNIT MARK;No;0;L;;;;100;N;;;;;
+11C70;MARCHEN HEAD MARK;Po;0;L;;;;;N;;;;;
+11C71;MARCHEN MARK SHAD;Po;0;L;;;;;N;;;;;
+11C72;MARCHEN LETTER KA;Lo;0;L;;;;;N;;;;;
+11C73;MARCHEN LETTER KHA;Lo;0;L;;;;;N;;;;;
+11C74;MARCHEN LETTER GA;Lo;0;L;;;;;N;;;;;
+11C75;MARCHEN LETTER NGA;Lo;0;L;;;;;N;;;;;
+11C76;MARCHEN LETTER CA;Lo;0;L;;;;;N;;;;;
+11C77;MARCHEN LETTER CHA;Lo;0;L;;;;;N;;;;;
+11C78;MARCHEN LETTER JA;Lo;0;L;;;;;N;;;;;
+11C79;MARCHEN LETTER NYA;Lo;0;L;;;;;N;;;;;
+11C7A;MARCHEN LETTER TA;Lo;0;L;;;;;N;;;;;
+11C7B;MARCHEN LETTER THA;Lo;0;L;;;;;N;;;;;
+11C7C;MARCHEN LETTER DA;Lo;0;L;;;;;N;;;;;
+11C7D;MARCHEN LETTER NA;Lo;0;L;;;;;N;;;;;
+11C7E;MARCHEN LETTER PA;Lo;0;L;;;;;N;;;;;
+11C7F;MARCHEN LETTER PHA;Lo;0;L;;;;;N;;;;;
+11C80;MARCHEN LETTER BA;Lo;0;L;;;;;N;;;;;
+11C81;MARCHEN LETTER MA;Lo;0;L;;;;;N;;;;;
+11C82;MARCHEN LETTER TSA;Lo;0;L;;;;;N;;;;;
+11C83;MARCHEN LETTER TSHA;Lo;0;L;;;;;N;;;;;
+11C84;MARCHEN LETTER DZA;Lo;0;L;;;;;N;;;;;
+11C85;MARCHEN LETTER WA;Lo;0;L;;;;;N;;;;;
+11C86;MARCHEN LETTER ZHA;Lo;0;L;;;;;N;;;;;
+11C87;MARCHEN LETTER ZA;Lo;0;L;;;;;N;;;;;
+11C88;MARCHEN LETTER -A;Lo;0;L;;;;;N;;;;;
+11C89;MARCHEN LETTER YA;Lo;0;L;;;;;N;;;;;
+11C8A;MARCHEN LETTER RA;Lo;0;L;;;;;N;;;;;
+11C8B;MARCHEN LETTER LA;Lo;0;L;;;;;N;;;;;
+11C8C;MARCHEN LETTER SHA;Lo;0;L;;;;;N;;;;;
+11C8D;MARCHEN LETTER SA;Lo;0;L;;;;;N;;;;;
+11C8E;MARCHEN LETTER HA;Lo;0;L;;;;;N;;;;;
+11C8F;MARCHEN LETTER A;Lo;0;L;;;;;N;;;;;
+11C92;MARCHEN SUBJOINED LETTER KA;Mn;0;NSM;;;;;N;;;;;
+11C93;MARCHEN SUBJOINED LETTER KHA;Mn;0;NSM;;;;;N;;;;;
+11C94;MARCHEN SUBJOINED LETTER GA;Mn;0;NSM;;;;;N;;;;;
+11C95;MARCHEN SUBJOINED LETTER NGA;Mn;0;NSM;;;;;N;;;;;
+11C96;MARCHEN SUBJOINED LETTER CA;Mn;0;NSM;;;;;N;;;;;
+11C97;MARCHEN SUBJOINED LETTER CHA;Mn;0;NSM;;;;;N;;;;;
+11C98;MARCHEN SUBJOINED LETTER JA;Mn;0;NSM;;;;;N;;;;;
+11C99;MARCHEN SUBJOINED LETTER NYA;Mn;0;NSM;;;;;N;;;;;
+11C9A;MARCHEN SUBJOINED LETTER TA;Mn;0;NSM;;;;;N;;;;;
+11C9B;MARCHEN SUBJOINED LETTER THA;Mn;0;NSM;;;;;N;;;;;
+11C9C;MARCHEN SUBJOINED LETTER DA;Mn;0;NSM;;;;;N;;;;;
+11C9D;MARCHEN SUBJOINED LETTER NA;Mn;0;NSM;;;;;N;;;;;
+11C9E;MARCHEN SUBJOINED LETTER PA;Mn;0;NSM;;;;;N;;;;;
+11C9F;MARCHEN SUBJOINED LETTER PHA;Mn;0;NSM;;;;;N;;;;;
+11CA0;MARCHEN SUBJOINED LETTER BA;Mn;0;NSM;;;;;N;;;;;
+11CA1;MARCHEN SUBJOINED LETTER MA;Mn;0;NSM;;;;;N;;;;;
+11CA2;MARCHEN SUBJOINED LETTER TSA;Mn;0;NSM;;;;;N;;;;;
+11CA3;MARCHEN SUBJOINED LETTER TSHA;Mn;0;NSM;;;;;N;;;;;
+11CA4;MARCHEN SUBJOINED LETTER DZA;Mn;0;NSM;;;;;N;;;;;
+11CA5;MARCHEN SUBJOINED LETTER WA;Mn;0;NSM;;;;;N;;;;;
+11CA6;MARCHEN SUBJOINED LETTER ZHA;Mn;0;NSM;;;;;N;;;;;
+11CA7;MARCHEN SUBJOINED LETTER ZA;Mn;0;NSM;;;;;N;;;;;
+11CA9;MARCHEN SUBJOINED LETTER YA;Mc;0;L;;;;;N;;;;;
+11CAA;MARCHEN SUBJOINED LETTER RA;Mn;0;NSM;;;;;N;;;;;
+11CAB;MARCHEN SUBJOINED LETTER LA;Mn;0;NSM;;;;;N;;;;;
+11CAC;MARCHEN SUBJOINED LETTER SHA;Mn;0;NSM;;;;;N;;;;;
+11CAD;MARCHEN SUBJOINED LETTER SA;Mn;0;NSM;;;;;N;;;;;
+11CAE;MARCHEN SUBJOINED LETTER HA;Mn;0;NSM;;;;;N;;;;;
+11CAF;MARCHEN SUBJOINED LETTER A;Mn;0;NSM;;;;;N;;;;;
+11CB0;MARCHEN VOWEL SIGN AA;Mn;0;NSM;;;;;N;;;;;
+11CB1;MARCHEN VOWEL SIGN I;Mc;0;L;;;;;N;;;;;
+11CB2;MARCHEN VOWEL SIGN U;Mn;0;NSM;;;;;N;;;;;
+11CB3;MARCHEN VOWEL SIGN E;Mn;0;NSM;;;;;N;;;;;
+11CB4;MARCHEN VOWEL SIGN O;Mc;0;L;;;;;N;;;;;
+11CB5;MARCHEN SIGN ANUSVARA;Mn;0;NSM;;;;;N;;;;;
+11CB6;MARCHEN SIGN CANDRABINDU;Mn;0;NSM;;;;;N;;;;;
+12000;CUNEIFORM SIGN A;Lo;0;L;;;;;N;;;;;
+12001;CUNEIFORM SIGN A TIMES A;Lo;0;L;;;;;N;;;;;
+12002;CUNEIFORM SIGN A TIMES BAD;Lo;0;L;;;;;N;;;;;
+12003;CUNEIFORM SIGN A TIMES GAN2 TENU;Lo;0;L;;;;;N;;;;;
+12004;CUNEIFORM SIGN A TIMES HA;Lo;0;L;;;;;N;;;;;
+12005;CUNEIFORM SIGN A TIMES IGI;Lo;0;L;;;;;N;;;;;
+12006;CUNEIFORM SIGN A TIMES LAGAR GUNU;Lo;0;L;;;;;N;;;;;
+12007;CUNEIFORM SIGN A TIMES MUSH;Lo;0;L;;;;;N;;;;;
+12008;CUNEIFORM SIGN A TIMES SAG;Lo;0;L;;;;;N;;;;;
+12009;CUNEIFORM SIGN A2;Lo;0;L;;;;;N;;;;;
+1200A;CUNEIFORM SIGN AB;Lo;0;L;;;;;N;;;;;
+1200B;CUNEIFORM SIGN AB TIMES ASH2;Lo;0;L;;;;;N;;;;;
+1200C;CUNEIFORM SIGN AB TIMES DUN3 GUNU;Lo;0;L;;;;;N;;;;;
+1200D;CUNEIFORM SIGN AB TIMES GAL;Lo;0;L;;;;;N;;;;;
+1200E;CUNEIFORM SIGN AB TIMES GAN2 TENU;Lo;0;L;;;;;N;;;;;
+1200F;CUNEIFORM SIGN AB TIMES HA;Lo;0;L;;;;;N;;;;;
+12010;CUNEIFORM SIGN AB TIMES IGI GUNU;Lo;0;L;;;;;N;;;;;
+12011;CUNEIFORM SIGN AB TIMES IMIN;Lo;0;L;;;;;N;;;;;
+12012;CUNEIFORM SIGN AB TIMES LAGAB;Lo;0;L;;;;;N;;;;;
+12013;CUNEIFORM SIGN AB TIMES SHESH;Lo;0;L;;;;;N;;;;;
+12014;CUNEIFORM SIGN AB TIMES U PLUS U PLUS U;Lo;0;L;;;;;N;;;;;
+12015;CUNEIFORM SIGN AB GUNU;Lo;0;L;;;;;N;;;;;
+12016;CUNEIFORM SIGN AB2;Lo;0;L;;;;;N;;;;;
+12017;CUNEIFORM SIGN AB2 TIMES BALAG;Lo;0;L;;;;;N;;;;;
+12018;CUNEIFORM SIGN AB2 TIMES GAN2 TENU;Lo;0;L;;;;;N;;;;;
+12019;CUNEIFORM SIGN AB2 TIMES ME PLUS EN;Lo;0;L;;;;;N;;;;;
+1201A;CUNEIFORM SIGN AB2 TIMES SHA3;Lo;0;L;;;;;N;;;;;
+1201B;CUNEIFORM SIGN AB2 TIMES TAK4;Lo;0;L;;;;;N;;;;;
+1201C;CUNEIFORM SIGN AD;Lo;0;L;;;;;N;;;;;
+1201D;CUNEIFORM SIGN AK;Lo;0;L;;;;;N;;;;;
+1201E;CUNEIFORM SIGN AK TIMES ERIN2;Lo;0;L;;;;;N;;;;;
+1201F;CUNEIFORM SIGN AK TIMES SHITA PLUS GISH;Lo;0;L;;;;;N;;;;;
+12020;CUNEIFORM SIGN AL;Lo;0;L;;;;;N;;;;;
+12021;CUNEIFORM SIGN AL TIMES AL;Lo;0;L;;;;;N;;;;;
+12022;CUNEIFORM SIGN AL TIMES DIM2;Lo;0;L;;;;;N;;;;;
+12023;CUNEIFORM SIGN AL TIMES GISH;Lo;0;L;;;;;N;;;;;
+12024;CUNEIFORM SIGN AL TIMES HA;Lo;0;L;;;;;N;;;;;
+12025;CUNEIFORM SIGN AL TIMES KAD3;Lo;0;L;;;;;N;;;;;
+12026;CUNEIFORM SIGN AL TIMES KI;Lo;0;L;;;;;N;;;;;
+12027;CUNEIFORM SIGN AL TIMES SHE;Lo;0;L;;;;;N;;;;;
+12028;CUNEIFORM SIGN AL TIMES USH;Lo;0;L;;;;;N;;;;;
+12029;CUNEIFORM SIGN ALAN;Lo;0;L;;;;;N;;;;;
+1202A;CUNEIFORM SIGN ALEPH;Lo;0;L;;;;;N;;;;;
+1202B;CUNEIFORM SIGN AMAR;Lo;0;L;;;;;N;;;;;
+1202C;CUNEIFORM SIGN AMAR TIMES SHE;Lo;0;L;;;;;N;;;;;
+1202D;CUNEIFORM SIGN AN;Lo;0;L;;;;;N;;;;;
+1202E;CUNEIFORM SIGN AN OVER AN;Lo;0;L;;;;;N;;;;;
+1202F;CUNEIFORM SIGN AN THREE TIMES;Lo;0;L;;;;;N;;;;;
+12030;CUNEIFORM SIGN AN PLUS NAGA OPPOSING AN PLUS NAGA;Lo;0;L;;;;;N;;;;;
+12031;CUNEIFORM SIGN AN PLUS NAGA SQUARED;Lo;0;L;;;;;N;;;;;
+12032;CUNEIFORM SIGN ANSHE;Lo;0;L;;;;;N;;;;;
+12033;CUNEIFORM SIGN APIN;Lo;0;L;;;;;N;;;;;
+12034;CUNEIFORM SIGN ARAD;Lo;0;L;;;;;N;;;;;
+12035;CUNEIFORM SIGN ARAD TIMES KUR;Lo;0;L;;;;;N;;;;;
+12036;CUNEIFORM SIGN ARKAB;Lo;0;L;;;;;N;;;;;
+12037;CUNEIFORM SIGN ASAL2;Lo;0;L;;;;;N;;;;;
+12038;CUNEIFORM SIGN ASH;Lo;0;L;;;;;N;;;;;
+12039;CUNEIFORM SIGN ASH ZIDA TENU;Lo;0;L;;;;;N;;;;;
+1203A;CUNEIFORM SIGN ASH KABA TENU;Lo;0;L;;;;;N;;;;;
+1203B;CUNEIFORM SIGN ASH OVER ASH TUG2 OVER TUG2 TUG2 OVER TUG2 PAP;Lo;0;L;;;;;N;;;;;
+1203C;CUNEIFORM SIGN ASH OVER ASH OVER ASH;Lo;0;L;;;;;N;;;;;
+1203D;CUNEIFORM SIGN ASH OVER ASH OVER ASH CROSSING ASH OVER ASH OVER ASH;Lo;0;L;;;;;N;;;;;
+1203E;CUNEIFORM SIGN ASH2;Lo;0;L;;;;;N;;;;;
+1203F;CUNEIFORM SIGN ASHGAB;Lo;0;L;;;;;N;;;;;
+12040;CUNEIFORM SIGN BA;Lo;0;L;;;;;N;;;;;
+12041;CUNEIFORM SIGN BAD;Lo;0;L;;;;;N;;;;;
+12042;CUNEIFORM SIGN BAG3;Lo;0;L;;;;;N;;;;;
+12043;CUNEIFORM SIGN BAHAR2;Lo;0;L;;;;;N;;;;;
+12044;CUNEIFORM SIGN BAL;Lo;0;L;;;;;N;;;;;
+12045;CUNEIFORM SIGN BAL OVER BAL;Lo;0;L;;;;;N;;;;;
+12046;CUNEIFORM SIGN BALAG;Lo;0;L;;;;;N;;;;;
+12047;CUNEIFORM SIGN BAR;Lo;0;L;;;;;N;;;;;
+12048;CUNEIFORM SIGN BARA2;Lo;0;L;;;;;N;;;;;
+12049;CUNEIFORM SIGN BI;Lo;0;L;;;;;N;;;;;
+1204A;CUNEIFORM SIGN BI TIMES A;Lo;0;L;;;;;N;;;;;
+1204B;CUNEIFORM SIGN BI TIMES GAR;Lo;0;L;;;;;N;;;;;
+1204C;CUNEIFORM SIGN BI TIMES IGI GUNU;Lo;0;L;;;;;N;;;;;
+1204D;CUNEIFORM SIGN BU;Lo;0;L;;;;;N;;;;;
+1204E;CUNEIFORM SIGN BU OVER BU AB;Lo;0;L;;;;;N;;;;;
+1204F;CUNEIFORM SIGN BU OVER BU UN;Lo;0;L;;;;;N;;;;;
+12050;CUNEIFORM SIGN BU CROSSING BU;Lo;0;L;;;;;N;;;;;
+12051;CUNEIFORM SIGN BULUG;Lo;0;L;;;;;N;;;;;
+12052;CUNEIFORM SIGN BULUG OVER BULUG;Lo;0;L;;;;;N;;;;;
+12053;CUNEIFORM SIGN BUR;Lo;0;L;;;;;N;;;;;
+12054;CUNEIFORM SIGN BUR2;Lo;0;L;;;;;N;;;;;
+12055;CUNEIFORM SIGN DA;Lo;0;L;;;;;N;;;;;
+12056;CUNEIFORM SIGN DAG;Lo;0;L;;;;;N;;;;;
+12057;CUNEIFORM SIGN DAG KISIM5 TIMES A PLUS MASH;Lo;0;L;;;;;N;;;;;
+12058;CUNEIFORM SIGN DAG KISIM5 TIMES AMAR;Lo;0;L;;;;;N;;;;;
+12059;CUNEIFORM SIGN DAG KISIM5 TIMES BALAG;Lo;0;L;;;;;N;;;;;
+1205A;CUNEIFORM SIGN DAG KISIM5 TIMES BI;Lo;0;L;;;;;N;;;;;
+1205B;CUNEIFORM SIGN DAG KISIM5 TIMES GA;Lo;0;L;;;;;N;;;;;
+1205C;CUNEIFORM SIGN DAG KISIM5 TIMES GA PLUS MASH;Lo;0;L;;;;;N;;;;;
+1205D;CUNEIFORM SIGN DAG KISIM5 TIMES GI;Lo;0;L;;;;;N;;;;;
+1205E;CUNEIFORM SIGN DAG KISIM5 TIMES GIR2;Lo;0;L;;;;;N;;;;;
+1205F;CUNEIFORM SIGN DAG KISIM5 TIMES GUD;Lo;0;L;;;;;N;;;;;
+12060;CUNEIFORM SIGN DAG KISIM5 TIMES HA;Lo;0;L;;;;;N;;;;;
+12061;CUNEIFORM SIGN DAG KISIM5 TIMES IR;Lo;0;L;;;;;N;;;;;
+12062;CUNEIFORM SIGN DAG KISIM5 TIMES IR PLUS LU;Lo;0;L;;;;;N;;;;;
+12063;CUNEIFORM SIGN DAG KISIM5 TIMES KAK;Lo;0;L;;;;;N;;;;;
+12064;CUNEIFORM SIGN DAG KISIM5 TIMES LA;Lo;0;L;;;;;N;;;;;
+12065;CUNEIFORM SIGN DAG KISIM5 TIMES LU;Lo;0;L;;;;;N;;;;;
+12066;CUNEIFORM SIGN DAG KISIM5 TIMES LU PLUS MASH2;Lo;0;L;;;;;N;;;;;
+12067;CUNEIFORM SIGN DAG KISIM5 TIMES LUM;Lo;0;L;;;;;N;;;;;
+12068;CUNEIFORM SIGN DAG KISIM5 TIMES NE;Lo;0;L;;;;;N;;;;;
+12069;CUNEIFORM SIGN DAG KISIM5 TIMES PAP PLUS PAP;Lo;0;L;;;;;N;;;;;
+1206A;CUNEIFORM SIGN DAG KISIM5 TIMES SI;Lo;0;L;;;;;N;;;;;
+1206B;CUNEIFORM SIGN DAG KISIM5 TIMES TAK4;Lo;0;L;;;;;N;;;;;
+1206C;CUNEIFORM SIGN DAG KISIM5 TIMES U2 PLUS GIR2;Lo;0;L;;;;;N;;;;;
+1206D;CUNEIFORM SIGN DAG KISIM5 TIMES USH;Lo;0;L;;;;;N;;;;;
+1206E;CUNEIFORM SIGN DAM;Lo;0;L;;;;;N;;;;;
+1206F;CUNEIFORM SIGN DAR;Lo;0;L;;;;;N;;;;;
+12070;CUNEIFORM SIGN DARA3;Lo;0;L;;;;;N;;;;;
+12071;CUNEIFORM SIGN DARA4;Lo;0;L;;;;;N;;;;;
+12072;CUNEIFORM SIGN DI;Lo;0;L;;;;;N;;;;;
+12073;CUNEIFORM SIGN DIB;Lo;0;L;;;;;N;;;;;
+12074;CUNEIFORM SIGN DIM;Lo;0;L;;;;;N;;;;;
+12075;CUNEIFORM SIGN DIM TIMES SHE;Lo;0;L;;;;;N;;;;;
+12076;CUNEIFORM SIGN DIM2;Lo;0;L;;;;;N;;;;;
+12077;CUNEIFORM SIGN DIN;Lo;0;L;;;;;N;;;;;
+12078;CUNEIFORM SIGN DIN KASKAL U GUNU DISH;Lo;0;L;;;;;N;;;;;
+12079;CUNEIFORM SIGN DISH;Lo;0;L;;;;;N;;;;;
+1207A;CUNEIFORM SIGN DU;Lo;0;L;;;;;N;;;;;
+1207B;CUNEIFORM SIGN DU OVER DU;Lo;0;L;;;;;N;;;;;
+1207C;CUNEIFORM SIGN DU GUNU;Lo;0;L;;;;;N;;;;;
+1207D;CUNEIFORM SIGN DU SHESHIG;Lo;0;L;;;;;N;;;;;
+1207E;CUNEIFORM SIGN DUB;Lo;0;L;;;;;N;;;;;
+1207F;CUNEIFORM SIGN DUB TIMES ESH2;Lo;0;L;;;;;N;;;;;
+12080;CUNEIFORM SIGN DUB2;Lo;0;L;;;;;N;;;;;
+12081;CUNEIFORM SIGN DUG;Lo;0;L;;;;;N;;;;;
+12082;CUNEIFORM SIGN DUGUD;Lo;0;L;;;;;N;;;;;
+12083;CUNEIFORM SIGN DUH;Lo;0;L;;;;;N;;;;;
+12084;CUNEIFORM SIGN DUN;Lo;0;L;;;;;N;;;;;
+12085;CUNEIFORM SIGN DUN3;Lo;0;L;;;;;N;;;;;
+12086;CUNEIFORM SIGN DUN3 GUNU;Lo;0;L;;;;;N;;;;;
+12087;CUNEIFORM SIGN DUN3 GUNU GUNU;Lo;0;L;;;;;N;;;;;
+12088;CUNEIFORM SIGN DUN4;Lo;0;L;;;;;N;;;;;
+12089;CUNEIFORM SIGN DUR2;Lo;0;L;;;;;N;;;;;
+1208A;CUNEIFORM SIGN E;Lo;0;L;;;;;N;;;;;
+1208B;CUNEIFORM SIGN E TIMES PAP;Lo;0;L;;;;;N;;;;;
+1208C;CUNEIFORM SIGN E OVER E NUN OVER NUN;Lo;0;L;;;;;N;;;;;
+1208D;CUNEIFORM SIGN E2;Lo;0;L;;;;;N;;;;;
+1208E;CUNEIFORM SIGN E2 TIMES A PLUS HA PLUS DA;Lo;0;L;;;;;N;;;;;
+1208F;CUNEIFORM SIGN E2 TIMES GAR;Lo;0;L;;;;;N;;;;;
+12090;CUNEIFORM SIGN E2 TIMES MI;Lo;0;L;;;;;N;;;;;
+12091;CUNEIFORM SIGN E2 TIMES SAL;Lo;0;L;;;;;N;;;;;
+12092;CUNEIFORM SIGN E2 TIMES SHE;Lo;0;L;;;;;N;;;;;
+12093;CUNEIFORM SIGN E2 TIMES U;Lo;0;L;;;;;N;;;;;
+12094;CUNEIFORM SIGN EDIN;Lo;0;L;;;;;N;;;;;
+12095;CUNEIFORM SIGN EGIR;Lo;0;L;;;;;N;;;;;
+12096;CUNEIFORM SIGN EL;Lo;0;L;;;;;N;;;;;
+12097;CUNEIFORM SIGN EN;Lo;0;L;;;;;N;;;;;
+12098;CUNEIFORM SIGN EN TIMES GAN2;Lo;0;L;;;;;N;;;;;
+12099;CUNEIFORM SIGN EN TIMES GAN2 TENU;Lo;0;L;;;;;N;;;;;
+1209A;CUNEIFORM SIGN EN TIMES ME;Lo;0;L;;;;;N;;;;;
+1209B;CUNEIFORM SIGN EN CROSSING EN;Lo;0;L;;;;;N;;;;;
+1209C;CUNEIFORM SIGN EN OPPOSING EN;Lo;0;L;;;;;N;;;;;
+1209D;CUNEIFORM SIGN EN SQUARED;Lo;0;L;;;;;N;;;;;
+1209E;CUNEIFORM SIGN EREN;Lo;0;L;;;;;N;;;;;
+1209F;CUNEIFORM SIGN ERIN2;Lo;0;L;;;;;N;;;;;
+120A0;CUNEIFORM SIGN ESH2;Lo;0;L;;;;;N;;;;;
+120A1;CUNEIFORM SIGN EZEN;Lo;0;L;;;;;N;;;;;
+120A2;CUNEIFORM SIGN EZEN TIMES A;Lo;0;L;;;;;N;;;;;
+120A3;CUNEIFORM SIGN EZEN TIMES A PLUS LAL;Lo;0;L;;;;;N;;;;;
+120A4;CUNEIFORM SIGN EZEN TIMES A PLUS LAL TIMES LAL;Lo;0;L;;;;;N;;;;;
+120A5;CUNEIFORM SIGN EZEN TIMES AN;Lo;0;L;;;;;N;;;;;
+120A6;CUNEIFORM SIGN EZEN TIMES BAD;Lo;0;L;;;;;N;;;;;
+120A7;CUNEIFORM SIGN EZEN TIMES DUN3 GUNU;Lo;0;L;;;;;N;;;;;
+120A8;CUNEIFORM SIGN EZEN TIMES DUN3 GUNU GUNU;Lo;0;L;;;;;N;;;;;
+120A9;CUNEIFORM SIGN EZEN TIMES HA;Lo;0;L;;;;;N;;;;;
+120AA;CUNEIFORM SIGN EZEN TIMES HA GUNU;Lo;0;L;;;;;N;;;;;
+120AB;CUNEIFORM SIGN EZEN TIMES IGI GUNU;Lo;0;L;;;;;N;;;;;
+120AC;CUNEIFORM SIGN EZEN TIMES KASKAL;Lo;0;L;;;;;N;;;;;
+120AD;CUNEIFORM SIGN EZEN TIMES KASKAL SQUARED;Lo;0;L;;;;;N;;;;;
+120AE;CUNEIFORM SIGN EZEN TIMES KU3;Lo;0;L;;;;;N;;;;;
+120AF;CUNEIFORM SIGN EZEN TIMES LA;Lo;0;L;;;;;N;;;;;
+120B0;CUNEIFORM SIGN EZEN TIMES LAL TIMES LAL;Lo;0;L;;;;;N;;;;;
+120B1;CUNEIFORM SIGN EZEN TIMES LI;Lo;0;L;;;;;N;;;;;
+120B2;CUNEIFORM SIGN EZEN TIMES LU;Lo;0;L;;;;;N;;;;;
+120B3;CUNEIFORM SIGN EZEN TIMES U2;Lo;0;L;;;;;N;;;;;
+120B4;CUNEIFORM SIGN EZEN TIMES UD;Lo;0;L;;;;;N;;;;;
+120B5;CUNEIFORM SIGN GA;Lo;0;L;;;;;N;;;;;
+120B6;CUNEIFORM SIGN GA GUNU;Lo;0;L;;;;;N;;;;;
+120B7;CUNEIFORM SIGN GA2;Lo;0;L;;;;;N;;;;;
+120B8;CUNEIFORM SIGN GA2 TIMES A PLUS DA PLUS HA;Lo;0;L;;;;;N;;;;;
+120B9;CUNEIFORM SIGN GA2 TIMES A PLUS HA;Lo;0;L;;;;;N;;;;;
+120BA;CUNEIFORM SIGN GA2 TIMES A PLUS IGI;Lo;0;L;;;;;N;;;;;
+120BB;CUNEIFORM SIGN GA2 TIMES AB2 TENU PLUS TAB;Lo;0;L;;;;;N;;;;;
+120BC;CUNEIFORM SIGN GA2 TIMES AN;Lo;0;L;;;;;N;;;;;
+120BD;CUNEIFORM SIGN GA2 TIMES ASH;Lo;0;L;;;;;N;;;;;
+120BE;CUNEIFORM SIGN GA2 TIMES ASH2 PLUS GAL;Lo;0;L;;;;;N;;;;;
+120BF;CUNEIFORM SIGN GA2 TIMES BAD;Lo;0;L;;;;;N;;;;;
+120C0;CUNEIFORM SIGN GA2 TIMES BAR PLUS RA;Lo;0;L;;;;;N;;;;;
+120C1;CUNEIFORM SIGN GA2 TIMES BUR;Lo;0;L;;;;;N;;;;;
+120C2;CUNEIFORM SIGN GA2 TIMES BUR PLUS RA;Lo;0;L;;;;;N;;;;;
+120C3;CUNEIFORM SIGN GA2 TIMES DA;Lo;0;L;;;;;N;;;;;
+120C4;CUNEIFORM SIGN GA2 TIMES DI;Lo;0;L;;;;;N;;;;;
+120C5;CUNEIFORM SIGN GA2 TIMES DIM TIMES SHE;Lo;0;L;;;;;N;;;;;
+120C6;CUNEIFORM SIGN GA2 TIMES DUB;Lo;0;L;;;;;N;;;;;
+120C7;CUNEIFORM SIGN GA2 TIMES EL;Lo;0;L;;;;;N;;;;;
+120C8;CUNEIFORM SIGN GA2 TIMES EL PLUS LA;Lo;0;L;;;;;N;;;;;
+120C9;CUNEIFORM SIGN GA2 TIMES EN;Lo;0;L;;;;;N;;;;;
+120CA;CUNEIFORM SIGN GA2 TIMES EN TIMES GAN2 TENU;Lo;0;L;;;;;N;;;;;
+120CB;CUNEIFORM SIGN GA2 TIMES GAN2 TENU;Lo;0;L;;;;;N;;;;;
+120CC;CUNEIFORM SIGN GA2 TIMES GAR;Lo;0;L;;;;;N;;;;;
+120CD;CUNEIFORM SIGN GA2 TIMES GI;Lo;0;L;;;;;N;;;;;
+120CE;CUNEIFORM SIGN GA2 TIMES GI4;Lo;0;L;;;;;N;;;;;
+120CF;CUNEIFORM SIGN GA2 TIMES GI4 PLUS A;Lo;0;L;;;;;N;;;;;
+120D0;CUNEIFORM SIGN GA2 TIMES GIR2 PLUS SU;Lo;0;L;;;;;N;;;;;
+120D1;CUNEIFORM SIGN GA2 TIMES HA PLUS LU PLUS ESH2;Lo;0;L;;;;;N;;;;;
+120D2;CUNEIFORM SIGN GA2 TIMES HAL;Lo;0;L;;;;;N;;;;;
+120D3;CUNEIFORM SIGN GA2 TIMES HAL PLUS LA;Lo;0;L;;;;;N;;;;;
+120D4;CUNEIFORM SIGN GA2 TIMES HI PLUS LI;Lo;0;L;;;;;N;;;;;
+120D5;CUNEIFORM SIGN GA2 TIMES HUB2;Lo;0;L;;;;;N;;;;;
+120D6;CUNEIFORM SIGN GA2 TIMES IGI GUNU;Lo;0;L;;;;;N;;;;;
+120D7;CUNEIFORM SIGN GA2 TIMES ISH PLUS HU PLUS ASH;Lo;0;L;;;;;N;;;;;
+120D8;CUNEIFORM SIGN GA2 TIMES KAK;Lo;0;L;;;;;N;;;;;
+120D9;CUNEIFORM SIGN GA2 TIMES KASKAL;Lo;0;L;;;;;N;;;;;
+120DA;CUNEIFORM SIGN GA2 TIMES KID;Lo;0;L;;;;;N;;;;;
+120DB;CUNEIFORM SIGN GA2 TIMES KID PLUS LAL;Lo;0;L;;;;;N;;;;;
+120DC;CUNEIFORM SIGN GA2 TIMES KU3 PLUS AN;Lo;0;L;;;;;N;;;;;
+120DD;CUNEIFORM SIGN GA2 TIMES LA;Lo;0;L;;;;;N;;;;;
+120DE;CUNEIFORM SIGN GA2 TIMES ME PLUS EN;Lo;0;L;;;;;N;;;;;
+120DF;CUNEIFORM SIGN GA2 TIMES MI;Lo;0;L;;;;;N;;;;;
+120E0;CUNEIFORM SIGN GA2 TIMES NUN;Lo;0;L;;;;;N;;;;;
+120E1;CUNEIFORM SIGN GA2 TIMES NUN OVER NUN;Lo;0;L;;;;;N;;;;;
+120E2;CUNEIFORM SIGN GA2 TIMES PA;Lo;0;L;;;;;N;;;;;
+120E3;CUNEIFORM SIGN GA2 TIMES SAL;Lo;0;L;;;;;N;;;;;
+120E4;CUNEIFORM SIGN GA2 TIMES SAR;Lo;0;L;;;;;N;;;;;
+120E5;CUNEIFORM SIGN GA2 TIMES SHE;Lo;0;L;;;;;N;;;;;
+120E6;CUNEIFORM SIGN GA2 TIMES SHE PLUS TUR;Lo;0;L;;;;;N;;;;;
+120E7;CUNEIFORM SIGN GA2 TIMES SHID;Lo;0;L;;;;;N;;;;;
+120E8;CUNEIFORM SIGN GA2 TIMES SUM;Lo;0;L;;;;;N;;;;;
+120E9;CUNEIFORM SIGN GA2 TIMES TAK4;Lo;0;L;;;;;N;;;;;
+120EA;CUNEIFORM SIGN GA2 TIMES U;Lo;0;L;;;;;N;;;;;
+120EB;CUNEIFORM SIGN GA2 TIMES UD;Lo;0;L;;;;;N;;;;;
+120EC;CUNEIFORM SIGN GA2 TIMES UD PLUS DU;Lo;0;L;;;;;N;;;;;
+120ED;CUNEIFORM SIGN GA2 OVER GA2;Lo;0;L;;;;;N;;;;;
+120EE;CUNEIFORM SIGN GABA;Lo;0;L;;;;;N;;;;;
+120EF;CUNEIFORM SIGN GABA CROSSING GABA;Lo;0;L;;;;;N;;;;;
+120F0;CUNEIFORM SIGN GAD;Lo;0;L;;;;;N;;;;;
+120F1;CUNEIFORM SIGN GAD OVER GAD GAR OVER GAR;Lo;0;L;;;;;N;;;;;
+120F2;CUNEIFORM SIGN GAL;Lo;0;L;;;;;N;;;;;
+120F3;CUNEIFORM SIGN GAL GAD OVER GAD GAR OVER GAR;Lo;0;L;;;;;N;;;;;
+120F4;CUNEIFORM SIGN GALAM;Lo;0;L;;;;;N;;;;;
+120F5;CUNEIFORM SIGN GAM;Lo;0;L;;;;;N;;;;;
+120F6;CUNEIFORM SIGN GAN;Lo;0;L;;;;;N;;;;;
+120F7;CUNEIFORM SIGN GAN2;Lo;0;L;;;;;N;;;;;
+120F8;CUNEIFORM SIGN GAN2 TENU;Lo;0;L;;;;;N;;;;;
+120F9;CUNEIFORM SIGN GAN2 OVER GAN2;Lo;0;L;;;;;N;;;;;
+120FA;CUNEIFORM SIGN GAN2 CROSSING GAN2;Lo;0;L;;;;;N;;;;;
+120FB;CUNEIFORM SIGN GAR;Lo;0;L;;;;;N;;;;;
+120FC;CUNEIFORM SIGN GAR3;Lo;0;L;;;;;N;;;;;
+120FD;CUNEIFORM SIGN GASHAN;Lo;0;L;;;;;N;;;;;
+120FE;CUNEIFORM SIGN GESHTIN;Lo;0;L;;;;;N;;;;;
+120FF;CUNEIFORM SIGN GESHTIN TIMES KUR;Lo;0;L;;;;;N;;;;;
+12100;CUNEIFORM SIGN GI;Lo;0;L;;;;;N;;;;;
+12101;CUNEIFORM SIGN GI TIMES E;Lo;0;L;;;;;N;;;;;
+12102;CUNEIFORM SIGN GI TIMES U;Lo;0;L;;;;;N;;;;;
+12103;CUNEIFORM SIGN GI CROSSING GI;Lo;0;L;;;;;N;;;;;
+12104;CUNEIFORM SIGN GI4;Lo;0;L;;;;;N;;;;;
+12105;CUNEIFORM SIGN GI4 OVER GI4;Lo;0;L;;;;;N;;;;;
+12106;CUNEIFORM SIGN GI4 CROSSING GI4;Lo;0;L;;;;;N;;;;;
+12107;CUNEIFORM SIGN GIDIM;Lo;0;L;;;;;N;;;;;
+12108;CUNEIFORM SIGN GIR2;Lo;0;L;;;;;N;;;;;
+12109;CUNEIFORM SIGN GIR2 GUNU;Lo;0;L;;;;;N;;;;;
+1210A;CUNEIFORM SIGN GIR3;Lo;0;L;;;;;N;;;;;
+1210B;CUNEIFORM SIGN GIR3 TIMES A PLUS IGI;Lo;0;L;;;;;N;;;;;
+1210C;CUNEIFORM SIGN GIR3 TIMES GAN2 TENU;Lo;0;L;;;;;N;;;;;
+1210D;CUNEIFORM SIGN GIR3 TIMES IGI;Lo;0;L;;;;;N;;;;;
+1210E;CUNEIFORM SIGN GIR3 TIMES LU PLUS IGI;Lo;0;L;;;;;N;;;;;
+1210F;CUNEIFORM SIGN GIR3 TIMES PA;Lo;0;L;;;;;N;;;;;
+12110;CUNEIFORM SIGN GISAL;Lo;0;L;;;;;N;;;;;
+12111;CUNEIFORM SIGN GISH;Lo;0;L;;;;;N;;;;;
+12112;CUNEIFORM SIGN GISH CROSSING GISH;Lo;0;L;;;;;N;;;;;
+12113;CUNEIFORM SIGN GISH TIMES BAD;Lo;0;L;;;;;N;;;;;
+12114;CUNEIFORM SIGN GISH TIMES TAK4;Lo;0;L;;;;;N;;;;;
+12115;CUNEIFORM SIGN GISH TENU;Lo;0;L;;;;;N;;;;;
+12116;CUNEIFORM SIGN GU;Lo;0;L;;;;;N;;;;;
+12117;CUNEIFORM SIGN GU CROSSING GU;Lo;0;L;;;;;N;;;;;
+12118;CUNEIFORM SIGN GU2;Lo;0;L;;;;;N;;;;;
+12119;CUNEIFORM SIGN GU2 TIMES KAK;Lo;0;L;;;;;N;;;;;
+1211A;CUNEIFORM SIGN GU2 TIMES KAK TIMES IGI GUNU;Lo;0;L;;;;;N;;;;;
+1211B;CUNEIFORM SIGN GU2 TIMES NUN;Lo;0;L;;;;;N;;;;;
+1211C;CUNEIFORM SIGN GU2 TIMES SAL PLUS TUG2;Lo;0;L;;;;;N;;;;;
+1211D;CUNEIFORM SIGN GU2 GUNU;Lo;0;L;;;;;N;;;;;
+1211E;CUNEIFORM SIGN GUD;Lo;0;L;;;;;N;;;;;
+1211F;CUNEIFORM SIGN GUD TIMES A PLUS KUR;Lo;0;L;;;;;N;;;;;
+12120;CUNEIFORM SIGN GUD TIMES KUR;Lo;0;L;;;;;N;;;;;
+12121;CUNEIFORM SIGN GUD OVER GUD LUGAL;Lo;0;L;;;;;N;;;;;
+12122;CUNEIFORM SIGN GUL;Lo;0;L;;;;;N;;;;;
+12123;CUNEIFORM SIGN GUM;Lo;0;L;;;;;N;;;;;
+12124;CUNEIFORM SIGN GUM TIMES SHE;Lo;0;L;;;;;N;;;;;
+12125;CUNEIFORM SIGN GUR;Lo;0;L;;;;;N;;;;;
+12126;CUNEIFORM SIGN GUR7;Lo;0;L;;;;;N;;;;;
+12127;CUNEIFORM SIGN GURUN;Lo;0;L;;;;;N;;;;;
+12128;CUNEIFORM SIGN GURUSH;Lo;0;L;;;;;N;;;;;
+12129;CUNEIFORM SIGN HA;Lo;0;L;;;;;N;;;;;
+1212A;CUNEIFORM SIGN HA TENU;Lo;0;L;;;;;N;;;;;
+1212B;CUNEIFORM SIGN HA GUNU;Lo;0;L;;;;;N;;;;;
+1212C;CUNEIFORM SIGN HAL;Lo;0;L;;;;;N;;;;;
+1212D;CUNEIFORM SIGN HI;Lo;0;L;;;;;N;;;;;
+1212E;CUNEIFORM SIGN HI TIMES ASH;Lo;0;L;;;;;N;;;;;
+1212F;CUNEIFORM SIGN HI TIMES ASH2;Lo;0;L;;;;;N;;;;;
+12130;CUNEIFORM SIGN HI TIMES BAD;Lo;0;L;;;;;N;;;;;
+12131;CUNEIFORM SIGN HI TIMES DISH;Lo;0;L;;;;;N;;;;;
+12132;CUNEIFORM SIGN HI TIMES GAD;Lo;0;L;;;;;N;;;;;
+12133;CUNEIFORM SIGN HI TIMES KIN;Lo;0;L;;;;;N;;;;;
+12134;CUNEIFORM SIGN HI TIMES NUN;Lo;0;L;;;;;N;;;;;
+12135;CUNEIFORM SIGN HI TIMES SHE;Lo;0;L;;;;;N;;;;;
+12136;CUNEIFORM SIGN HI TIMES U;Lo;0;L;;;;;N;;;;;
+12137;CUNEIFORM SIGN HU;Lo;0;L;;;;;N;;;;;
+12138;CUNEIFORM SIGN HUB2;Lo;0;L;;;;;N;;;;;
+12139;CUNEIFORM SIGN HUB2 TIMES AN;Lo;0;L;;;;;N;;;;;
+1213A;CUNEIFORM SIGN HUB2 TIMES HAL;Lo;0;L;;;;;N;;;;;
+1213B;CUNEIFORM SIGN HUB2 TIMES KASKAL;Lo;0;L;;;;;N;;;;;
+1213C;CUNEIFORM SIGN HUB2 TIMES LISH;Lo;0;L;;;;;N;;;;;
+1213D;CUNEIFORM SIGN HUB2 TIMES UD;Lo;0;L;;;;;N;;;;;
+1213E;CUNEIFORM SIGN HUL2;Lo;0;L;;;;;N;;;;;
+1213F;CUNEIFORM SIGN I;Lo;0;L;;;;;N;;;;;
+12140;CUNEIFORM SIGN I A;Lo;0;L;;;;;N;;;;;
+12141;CUNEIFORM SIGN IB;Lo;0;L;;;;;N;;;;;
+12142;CUNEIFORM SIGN IDIM;Lo;0;L;;;;;N;;;;;
+12143;CUNEIFORM SIGN IDIM OVER IDIM BUR;Lo;0;L;;;;;N;;;;;
+12144;CUNEIFORM SIGN IDIM OVER IDIM SQUARED;Lo;0;L;;;;;N;;;;;
+12145;CUNEIFORM SIGN IG;Lo;0;L;;;;;N;;;;;
+12146;CUNEIFORM SIGN IGI;Lo;0;L;;;;;N;;;;;
+12147;CUNEIFORM SIGN IGI DIB;Lo;0;L;;;;;N;;;;;
+12148;CUNEIFORM SIGN IGI RI;Lo;0;L;;;;;N;;;;;
+12149;CUNEIFORM SIGN IGI OVER IGI SHIR OVER SHIR UD OVER UD;Lo;0;L;;;;;N;;;;;
+1214A;CUNEIFORM SIGN IGI GUNU;Lo;0;L;;;;;N;;;;;
+1214B;CUNEIFORM SIGN IL;Lo;0;L;;;;;N;;;;;
+1214C;CUNEIFORM SIGN IL TIMES GAN2 TENU;Lo;0;L;;;;;N;;;;;
+1214D;CUNEIFORM SIGN IL2;Lo;0;L;;;;;N;;;;;
+1214E;CUNEIFORM SIGN IM;Lo;0;L;;;;;N;;;;;
+1214F;CUNEIFORM SIGN IM TIMES TAK4;Lo;0;L;;;;;N;;;;;
+12150;CUNEIFORM SIGN IM CROSSING IM;Lo;0;L;;;;;N;;;;;
+12151;CUNEIFORM SIGN IM OPPOSING IM;Lo;0;L;;;;;N;;;;;
+12152;CUNEIFORM SIGN IM SQUARED;Lo;0;L;;;;;N;;;;;
+12153;CUNEIFORM SIGN IMIN;Lo;0;L;;;;;N;;;;;
+12154;CUNEIFORM SIGN IN;Lo;0;L;;;;;N;;;;;
+12155;CUNEIFORM SIGN IR;Lo;0;L;;;;;N;;;;;
+12156;CUNEIFORM SIGN ISH;Lo;0;L;;;;;N;;;;;
+12157;CUNEIFORM SIGN KA;Lo;0;L;;;;;N;;;;;
+12158;CUNEIFORM SIGN KA TIMES A;Lo;0;L;;;;;N;;;;;
+12159;CUNEIFORM SIGN KA TIMES AD;Lo;0;L;;;;;N;;;;;
+1215A;CUNEIFORM SIGN KA TIMES AD PLUS KU3;Lo;0;L;;;;;N;;;;;
+1215B;CUNEIFORM SIGN KA TIMES ASH2;Lo;0;L;;;;;N;;;;;
+1215C;CUNEIFORM SIGN KA TIMES BAD;Lo;0;L;;;;;N;;;;;
+1215D;CUNEIFORM SIGN KA TIMES BALAG;Lo;0;L;;;;;N;;;;;
+1215E;CUNEIFORM SIGN KA TIMES BAR;Lo;0;L;;;;;N;;;;;
+1215F;CUNEIFORM SIGN KA TIMES BI;Lo;0;L;;;;;N;;;;;
+12160;CUNEIFORM SIGN KA TIMES ERIN2;Lo;0;L;;;;;N;;;;;
+12161;CUNEIFORM SIGN KA TIMES ESH2;Lo;0;L;;;;;N;;;;;
+12162;CUNEIFORM SIGN KA TIMES GA;Lo;0;L;;;;;N;;;;;
+12163;CUNEIFORM SIGN KA TIMES GAL;Lo;0;L;;;;;N;;;;;
+12164;CUNEIFORM SIGN KA TIMES GAN2 TENU;Lo;0;L;;;;;N;;;;;
+12165;CUNEIFORM SIGN KA TIMES GAR;Lo;0;L;;;;;N;;;;;
+12166;CUNEIFORM SIGN KA TIMES GAR PLUS SHA3 PLUS A;Lo;0;L;;;;;N;;;;;
+12167;CUNEIFORM SIGN KA TIMES GI;Lo;0;L;;;;;N;;;;;
+12168;CUNEIFORM SIGN KA TIMES GIR2;Lo;0;L;;;;;N;;;;;
+12169;CUNEIFORM SIGN KA TIMES GISH PLUS SAR;Lo;0;L;;;;;N;;;;;
+1216A;CUNEIFORM SIGN KA TIMES GISH CROSSING GISH;Lo;0;L;;;;;N;;;;;
+1216B;CUNEIFORM SIGN KA TIMES GU;Lo;0;L;;;;;N;;;;;
+1216C;CUNEIFORM SIGN KA TIMES GUR7;Lo;0;L;;;;;N;;;;;
+1216D;CUNEIFORM SIGN KA TIMES IGI;Lo;0;L;;;;;N;;;;;
+1216E;CUNEIFORM SIGN KA TIMES IM;Lo;0;L;;;;;N;;;;;
+1216F;CUNEIFORM SIGN KA TIMES KAK;Lo;0;L;;;;;N;;;;;
+12170;CUNEIFORM SIGN KA TIMES KI;Lo;0;L;;;;;N;;;;;
+12171;CUNEIFORM SIGN KA TIMES KID;Lo;0;L;;;;;N;;;;;
+12172;CUNEIFORM SIGN KA TIMES LI;Lo;0;L;;;;;N;;;;;
+12173;CUNEIFORM SIGN KA TIMES LU;Lo;0;L;;;;;N;;;;;
+12174;CUNEIFORM SIGN KA TIMES ME;Lo;0;L;;;;;N;;;;;
+12175;CUNEIFORM SIGN KA TIMES ME PLUS DU;Lo;0;L;;;;;N;;;;;
+12176;CUNEIFORM SIGN KA TIMES ME PLUS GI;Lo;0;L;;;;;N;;;;;
+12177;CUNEIFORM SIGN KA TIMES ME PLUS TE;Lo;0;L;;;;;N;;;;;
+12178;CUNEIFORM SIGN KA TIMES MI;Lo;0;L;;;;;N;;;;;
+12179;CUNEIFORM SIGN KA TIMES MI PLUS NUNUZ;Lo;0;L;;;;;N;;;;;
+1217A;CUNEIFORM SIGN KA TIMES NE;Lo;0;L;;;;;N;;;;;
+1217B;CUNEIFORM SIGN KA TIMES NUN;Lo;0;L;;;;;N;;;;;
+1217C;CUNEIFORM SIGN KA TIMES PI;Lo;0;L;;;;;N;;;;;
+1217D;CUNEIFORM SIGN KA TIMES RU;Lo;0;L;;;;;N;;;;;
+1217E;CUNEIFORM SIGN KA TIMES SA;Lo;0;L;;;;;N;;;;;
+1217F;CUNEIFORM SIGN KA TIMES SAR;Lo;0;L;;;;;N;;;;;
+12180;CUNEIFORM SIGN KA TIMES SHA;Lo;0;L;;;;;N;;;;;
+12181;CUNEIFORM SIGN KA TIMES SHE;Lo;0;L;;;;;N;;;;;
+12182;CUNEIFORM SIGN KA TIMES SHID;Lo;0;L;;;;;N;;;;;
+12183;CUNEIFORM SIGN KA TIMES SHU;Lo;0;L;;;;;N;;;;;
+12184;CUNEIFORM SIGN KA TIMES SIG;Lo;0;L;;;;;N;;;;;
+12185;CUNEIFORM SIGN KA TIMES SUHUR;Lo;0;L;;;;;N;;;;;
+12186;CUNEIFORM SIGN KA TIMES TAR;Lo;0;L;;;;;N;;;;;
+12187;CUNEIFORM SIGN KA TIMES U;Lo;0;L;;;;;N;;;;;
+12188;CUNEIFORM SIGN KA TIMES U2;Lo;0;L;;;;;N;;;;;
+12189;CUNEIFORM SIGN KA TIMES UD;Lo;0;L;;;;;N;;;;;
+1218A;CUNEIFORM SIGN KA TIMES UMUM TIMES PA;Lo;0;L;;;;;N;;;;;
+1218B;CUNEIFORM SIGN KA TIMES USH;Lo;0;L;;;;;N;;;;;
+1218C;CUNEIFORM SIGN KA TIMES ZI;Lo;0;L;;;;;N;;;;;
+1218D;CUNEIFORM SIGN KA2;Lo;0;L;;;;;N;;;;;
+1218E;CUNEIFORM SIGN KA2 CROSSING KA2;Lo;0;L;;;;;N;;;;;
+1218F;CUNEIFORM SIGN KAB;Lo;0;L;;;;;N;;;;;
+12190;CUNEIFORM SIGN KAD2;Lo;0;L;;;;;N;;;;;
+12191;CUNEIFORM SIGN KAD3;Lo;0;L;;;;;N;;;;;
+12192;CUNEIFORM SIGN KAD4;Lo;0;L;;;;;N;;;;;
+12193;CUNEIFORM SIGN KAD5;Lo;0;L;;;;;N;;;;;
+12194;CUNEIFORM SIGN KAD5 OVER KAD5;Lo;0;L;;;;;N;;;;;
+12195;CUNEIFORM SIGN KAK;Lo;0;L;;;;;N;;;;;
+12196;CUNEIFORM SIGN KAK TIMES IGI GUNU;Lo;0;L;;;;;N;;;;;
+12197;CUNEIFORM SIGN KAL;Lo;0;L;;;;;N;;;;;
+12198;CUNEIFORM SIGN KAL TIMES BAD;Lo;0;L;;;;;N;;;;;
+12199;CUNEIFORM SIGN KAL CROSSING KAL;Lo;0;L;;;;;N;;;;;
+1219A;CUNEIFORM SIGN KAM2;Lo;0;L;;;;;N;;;;;
+1219B;CUNEIFORM SIGN KAM4;Lo;0;L;;;;;N;;;;;
+1219C;CUNEIFORM SIGN KASKAL;Lo;0;L;;;;;N;;;;;
+1219D;CUNEIFORM SIGN KASKAL LAGAB TIMES U OVER LAGAB TIMES U;Lo;0;L;;;;;N;;;;;
+1219E;CUNEIFORM SIGN KASKAL OVER KASKAL LAGAB TIMES U OVER LAGAB TIMES U;Lo;0;L;;;;;N;;;;;
+1219F;CUNEIFORM SIGN KESH2;Lo;0;L;;;;;N;;;;;
+121A0;CUNEIFORM SIGN KI;Lo;0;L;;;;;N;;;;;
+121A1;CUNEIFORM SIGN KI TIMES BAD;Lo;0;L;;;;;N;;;;;
+121A2;CUNEIFORM SIGN KI TIMES U;Lo;0;L;;;;;N;;;;;
+121A3;CUNEIFORM SIGN KI TIMES UD;Lo;0;L;;;;;N;;;;;
+121A4;CUNEIFORM SIGN KID;Lo;0;L;;;;;N;;;;;
+121A5;CUNEIFORM SIGN KIN;Lo;0;L;;;;;N;;;;;
+121A6;CUNEIFORM SIGN KISAL;Lo;0;L;;;;;N;;;;;
+121A7;CUNEIFORM SIGN KISH;Lo;0;L;;;;;N;;;;;
+121A8;CUNEIFORM SIGN KISIM5;Lo;0;L;;;;;N;;;;;
+121A9;CUNEIFORM SIGN KISIM5 OVER KISIM5;Lo;0;L;;;;;N;;;;;
+121AA;CUNEIFORM SIGN KU;Lo;0;L;;;;;N;;;;;
+121AB;CUNEIFORM SIGN KU OVER HI TIMES ASH2 KU OVER HI TIMES ASH2;Lo;0;L;;;;;N;;;;;
+121AC;CUNEIFORM SIGN KU3;Lo;0;L;;;;;N;;;;;
+121AD;CUNEIFORM SIGN KU4;Lo;0;L;;;;;N;;;;;
+121AE;CUNEIFORM SIGN KU4 VARIANT FORM;Lo;0;L;;;;;N;;;;;
+121AF;CUNEIFORM SIGN KU7;Lo;0;L;;;;;N;;;;;
+121B0;CUNEIFORM SIGN KUL;Lo;0;L;;;;;N;;;;;
+121B1;CUNEIFORM SIGN KUL GUNU;Lo;0;L;;;;;N;;;;;
+121B2;CUNEIFORM SIGN KUN;Lo;0;L;;;;;N;;;;;
+121B3;CUNEIFORM SIGN KUR;Lo;0;L;;;;;N;;;;;
+121B4;CUNEIFORM SIGN KUR OPPOSING KUR;Lo;0;L;;;;;N;;;;;
+121B5;CUNEIFORM SIGN KUSHU2;Lo;0;L;;;;;N;;;;;
+121B6;CUNEIFORM SIGN KWU318;Lo;0;L;;;;;N;;;;;
+121B7;CUNEIFORM SIGN LA;Lo;0;L;;;;;N;;;;;
+121B8;CUNEIFORM SIGN LAGAB;Lo;0;L;;;;;N;;;;;
+121B9;CUNEIFORM SIGN LAGAB TIMES A;Lo;0;L;;;;;N;;;;;
+121BA;CUNEIFORM SIGN LAGAB TIMES A PLUS DA PLUS HA;Lo;0;L;;;;;N;;;;;
+121BB;CUNEIFORM SIGN LAGAB TIMES A PLUS GAR;Lo;0;L;;;;;N;;;;;
+121BC;CUNEIFORM SIGN LAGAB TIMES A PLUS LAL;Lo;0;L;;;;;N;;;;;
+121BD;CUNEIFORM SIGN LAGAB TIMES AL;Lo;0;L;;;;;N;;;;;
+121BE;CUNEIFORM SIGN LAGAB TIMES AN;Lo;0;L;;;;;N;;;;;
+121BF;CUNEIFORM SIGN LAGAB TIMES ASH ZIDA TENU;Lo;0;L;;;;;N;;;;;
+121C0;CUNEIFORM SIGN LAGAB TIMES BAD;Lo;0;L;;;;;N;;;;;
+121C1;CUNEIFORM SIGN LAGAB TIMES BI;Lo;0;L;;;;;N;;;;;
+121C2;CUNEIFORM SIGN LAGAB TIMES DAR;Lo;0;L;;;;;N;;;;;
+121C3;CUNEIFORM SIGN LAGAB TIMES EN;Lo;0;L;;;;;N;;;;;
+121C4;CUNEIFORM SIGN LAGAB TIMES GA;Lo;0;L;;;;;N;;;;;
+121C5;CUNEIFORM SIGN LAGAB TIMES GAR;Lo;0;L;;;;;N;;;;;
+121C6;CUNEIFORM SIGN LAGAB TIMES GUD;Lo;0;L;;;;;N;;;;;
+121C7;CUNEIFORM SIGN LAGAB TIMES GUD PLUS GUD;Lo;0;L;;;;;N;;;;;
+121C8;CUNEIFORM SIGN LAGAB TIMES HA;Lo;0;L;;;;;N;;;;;
+121C9;CUNEIFORM SIGN LAGAB TIMES HAL;Lo;0;L;;;;;N;;;;;
+121CA;CUNEIFORM SIGN LAGAB TIMES HI TIMES NUN;Lo;0;L;;;;;N;;;;;
+121CB;CUNEIFORM SIGN LAGAB TIMES IGI GUNU;Lo;0;L;;;;;N;;;;;
+121CC;CUNEIFORM SIGN LAGAB TIMES IM;Lo;0;L;;;;;N;;;;;
+121CD;CUNEIFORM SIGN LAGAB TIMES IM PLUS HA;Lo;0;L;;;;;N;;;;;
+121CE;CUNEIFORM SIGN LAGAB TIMES IM PLUS LU;Lo;0;L;;;;;N;;;;;
+121CF;CUNEIFORM SIGN LAGAB TIMES KI;Lo;0;L;;;;;N;;;;;
+121D0;CUNEIFORM SIGN LAGAB TIMES KIN;Lo;0;L;;;;;N;;;;;
+121D1;CUNEIFORM SIGN LAGAB TIMES KU3;Lo;0;L;;;;;N;;;;;
+121D2;CUNEIFORM SIGN LAGAB TIMES KUL;Lo;0;L;;;;;N;;;;;
+121D3;CUNEIFORM SIGN LAGAB TIMES KUL PLUS HI PLUS A;Lo;0;L;;;;;N;;;;;
+121D4;CUNEIFORM SIGN LAGAB TIMES LAGAB;Lo;0;L;;;;;N;;;;;
+121D5;CUNEIFORM SIGN LAGAB TIMES LISH;Lo;0;L;;;;;N;;;;;
+121D6;CUNEIFORM SIGN LAGAB TIMES LU;Lo;0;L;;;;;N;;;;;
+121D7;CUNEIFORM SIGN LAGAB TIMES LUL;Lo;0;L;;;;;N;;;;;
+121D8;CUNEIFORM SIGN LAGAB TIMES ME;Lo;0;L;;;;;N;;;;;
+121D9;CUNEIFORM SIGN LAGAB TIMES ME PLUS EN;Lo;0;L;;;;;N;;;;;
+121DA;CUNEIFORM SIGN LAGAB TIMES MUSH;Lo;0;L;;;;;N;;;;;
+121DB;CUNEIFORM SIGN LAGAB TIMES NE;Lo;0;L;;;;;N;;;;;
+121DC;CUNEIFORM SIGN LAGAB TIMES SHE PLUS SUM;Lo;0;L;;;;;N;;;;;
+121DD;CUNEIFORM SIGN LAGAB TIMES SHITA PLUS GISH PLUS ERIN2;Lo;0;L;;;;;N;;;;;
+121DE;CUNEIFORM SIGN LAGAB TIMES SHITA PLUS GISH TENU;Lo;0;L;;;;;N;;;;;
+121DF;CUNEIFORM SIGN LAGAB TIMES SHU2;Lo;0;L;;;;;N;;;;;
+121E0;CUNEIFORM SIGN LAGAB TIMES SHU2 PLUS SHU2;Lo;0;L;;;;;N;;;;;
+121E1;CUNEIFORM SIGN LAGAB TIMES SUM;Lo;0;L;;;;;N;;;;;
+121E2;CUNEIFORM SIGN LAGAB TIMES TAG;Lo;0;L;;;;;N;;;;;
+121E3;CUNEIFORM SIGN LAGAB TIMES TAK4;Lo;0;L;;;;;N;;;;;
+121E4;CUNEIFORM SIGN LAGAB TIMES TE PLUS A PLUS SU PLUS NA;Lo;0;L;;;;;N;;;;;
+121E5;CUNEIFORM SIGN LAGAB TIMES U;Lo;0;L;;;;;N;;;;;
+121E6;CUNEIFORM SIGN LAGAB TIMES U PLUS A;Lo;0;L;;;;;N;;;;;
+121E7;CUNEIFORM SIGN LAGAB TIMES U PLUS U PLUS U;Lo;0;L;;;;;N;;;;;
+121E8;CUNEIFORM SIGN LAGAB TIMES U2 PLUS ASH;Lo;0;L;;;;;N;;;;;
+121E9;CUNEIFORM SIGN LAGAB TIMES UD;Lo;0;L;;;;;N;;;;;
+121EA;CUNEIFORM SIGN LAGAB TIMES USH;Lo;0;L;;;;;N;;;;;
+121EB;CUNEIFORM SIGN LAGAB SQUARED;Lo;0;L;;;;;N;;;;;
+121EC;CUNEIFORM SIGN LAGAR;Lo;0;L;;;;;N;;;;;
+121ED;CUNEIFORM SIGN LAGAR TIMES SHE;Lo;0;L;;;;;N;;;;;
+121EE;CUNEIFORM SIGN LAGAR TIMES SHE PLUS SUM;Lo;0;L;;;;;N;;;;;
+121EF;CUNEIFORM SIGN LAGAR GUNU;Lo;0;L;;;;;N;;;;;
+121F0;CUNEIFORM SIGN LAGAR GUNU OVER LAGAR GUNU SHE;Lo;0;L;;;;;N;;;;;
+121F1;CUNEIFORM SIGN LAHSHU;Lo;0;L;;;;;N;;;;;
+121F2;CUNEIFORM SIGN LAL;Lo;0;L;;;;;N;;;;;
+121F3;CUNEIFORM SIGN LAL TIMES LAL;Lo;0;L;;;;;N;;;;;
+121F4;CUNEIFORM SIGN LAM;Lo;0;L;;;;;N;;;;;
+121F5;CUNEIFORM SIGN LAM TIMES KUR;Lo;0;L;;;;;N;;;;;
+121F6;CUNEIFORM SIGN LAM TIMES KUR PLUS RU;Lo;0;L;;;;;N;;;;;
+121F7;CUNEIFORM SIGN LI;Lo;0;L;;;;;N;;;;;
+121F8;CUNEIFORM SIGN LIL;Lo;0;L;;;;;N;;;;;
+121F9;CUNEIFORM SIGN LIMMU2;Lo;0;L;;;;;N;;;;;
+121FA;CUNEIFORM SIGN LISH;Lo;0;L;;;;;N;;;;;
+121FB;CUNEIFORM SIGN LU;Lo;0;L;;;;;N;;;;;
+121FC;CUNEIFORM SIGN LU TIMES BAD;Lo;0;L;;;;;N;;;;;
+121FD;CUNEIFORM SIGN LU2;Lo;0;L;;;;;N;;;;;
+121FE;CUNEIFORM SIGN LU2 TIMES AL;Lo;0;L;;;;;N;;;;;
+121FF;CUNEIFORM SIGN LU2 TIMES BAD;Lo;0;L;;;;;N;;;;;
+12200;CUNEIFORM SIGN LU2 TIMES ESH2;Lo;0;L;;;;;N;;;;;
+12201;CUNEIFORM SIGN LU2 TIMES ESH2 TENU;Lo;0;L;;;;;N;;;;;
+12202;CUNEIFORM SIGN LU2 TIMES GAN2 TENU;Lo;0;L;;;;;N;;;;;
+12203;CUNEIFORM SIGN LU2 TIMES HI TIMES BAD;Lo;0;L;;;;;N;;;;;
+12204;CUNEIFORM SIGN LU2 TIMES IM;Lo;0;L;;;;;N;;;;;
+12205;CUNEIFORM SIGN LU2 TIMES KAD2;Lo;0;L;;;;;N;;;;;
+12206;CUNEIFORM SIGN LU2 TIMES KAD3;Lo;0;L;;;;;N;;;;;
+12207;CUNEIFORM SIGN LU2 TIMES KAD3 PLUS ASH;Lo;0;L;;;;;N;;;;;
+12208;CUNEIFORM SIGN LU2 TIMES KI;Lo;0;L;;;;;N;;;;;
+12209;CUNEIFORM SIGN LU2 TIMES LA PLUS ASH;Lo;0;L;;;;;N;;;;;
+1220A;CUNEIFORM SIGN LU2 TIMES LAGAB;Lo;0;L;;;;;N;;;;;
+1220B;CUNEIFORM SIGN LU2 TIMES ME PLUS EN;Lo;0;L;;;;;N;;;;;
+1220C;CUNEIFORM SIGN LU2 TIMES NE;Lo;0;L;;;;;N;;;;;
+1220D;CUNEIFORM SIGN LU2 TIMES NU;Lo;0;L;;;;;N;;;;;
+1220E;CUNEIFORM SIGN LU2 TIMES SI PLUS ASH;Lo;0;L;;;;;N;;;;;
+1220F;CUNEIFORM SIGN LU2 TIMES SIK2 PLUS BU;Lo;0;L;;;;;N;;;;;
+12210;CUNEIFORM SIGN LU2 TIMES TUG2;Lo;0;L;;;;;N;;;;;
+12211;CUNEIFORM SIGN LU2 TENU;Lo;0;L;;;;;N;;;;;
+12212;CUNEIFORM SIGN LU2 CROSSING LU2;Lo;0;L;;;;;N;;;;;
+12213;CUNEIFORM SIGN LU2 OPPOSING LU2;Lo;0;L;;;;;N;;;;;
+12214;CUNEIFORM SIGN LU2 SQUARED;Lo;0;L;;;;;N;;;;;
+12215;CUNEIFORM SIGN LU2 SHESHIG;Lo;0;L;;;;;N;;;;;
+12216;CUNEIFORM SIGN LU3;Lo;0;L;;;;;N;;;;;
+12217;CUNEIFORM SIGN LUGAL;Lo;0;L;;;;;N;;;;;
+12218;CUNEIFORM SIGN LUGAL OVER LUGAL;Lo;0;L;;;;;N;;;;;
+12219;CUNEIFORM SIGN LUGAL OPPOSING LUGAL;Lo;0;L;;;;;N;;;;;
+1221A;CUNEIFORM SIGN LUGAL SHESHIG;Lo;0;L;;;;;N;;;;;
+1221B;CUNEIFORM SIGN LUH;Lo;0;L;;;;;N;;;;;
+1221C;CUNEIFORM SIGN LUL;Lo;0;L;;;;;N;;;;;
+1221D;CUNEIFORM SIGN LUM;Lo;0;L;;;;;N;;;;;
+1221E;CUNEIFORM SIGN LUM OVER LUM;Lo;0;L;;;;;N;;;;;
+1221F;CUNEIFORM SIGN LUM OVER LUM GAR OVER GAR;Lo;0;L;;;;;N;;;;;
+12220;CUNEIFORM SIGN MA;Lo;0;L;;;;;N;;;;;
+12221;CUNEIFORM SIGN MA TIMES TAK4;Lo;0;L;;;;;N;;;;;
+12222;CUNEIFORM SIGN MA GUNU;Lo;0;L;;;;;N;;;;;
+12223;CUNEIFORM SIGN MA2;Lo;0;L;;;;;N;;;;;
+12224;CUNEIFORM SIGN MAH;Lo;0;L;;;;;N;;;;;
+12225;CUNEIFORM SIGN MAR;Lo;0;L;;;;;N;;;;;
+12226;CUNEIFORM SIGN MASH;Lo;0;L;;;;;N;;;;;
+12227;CUNEIFORM SIGN MASH2;Lo;0;L;;;;;N;;;;;
+12228;CUNEIFORM SIGN ME;Lo;0;L;;;;;N;;;;;
+12229;CUNEIFORM SIGN MES;Lo;0;L;;;;;N;;;;;
+1222A;CUNEIFORM SIGN MI;Lo;0;L;;;;;N;;;;;
+1222B;CUNEIFORM SIGN MIN;Lo;0;L;;;;;N;;;;;
+1222C;CUNEIFORM SIGN MU;Lo;0;L;;;;;N;;;;;
+1222D;CUNEIFORM SIGN MU OVER MU;Lo;0;L;;;;;N;;;;;
+1222E;CUNEIFORM SIGN MUG;Lo;0;L;;;;;N;;;;;
+1222F;CUNEIFORM SIGN MUG GUNU;Lo;0;L;;;;;N;;;;;
+12230;CUNEIFORM SIGN MUNSUB;Lo;0;L;;;;;N;;;;;
+12231;CUNEIFORM SIGN MURGU2;Lo;0;L;;;;;N;;;;;
+12232;CUNEIFORM SIGN MUSH;Lo;0;L;;;;;N;;;;;
+12233;CUNEIFORM SIGN MUSH TIMES A;Lo;0;L;;;;;N;;;;;
+12234;CUNEIFORM SIGN MUSH TIMES KUR;Lo;0;L;;;;;N;;;;;
+12235;CUNEIFORM SIGN MUSH TIMES ZA;Lo;0;L;;;;;N;;;;;
+12236;CUNEIFORM SIGN MUSH OVER MUSH;Lo;0;L;;;;;N;;;;;
+12237;CUNEIFORM SIGN MUSH OVER MUSH TIMES A PLUS NA;Lo;0;L;;;;;N;;;;;
+12238;CUNEIFORM SIGN MUSH CROSSING MUSH;Lo;0;L;;;;;N;;;;;
+12239;CUNEIFORM SIGN MUSH3;Lo;0;L;;;;;N;;;;;
+1223A;CUNEIFORM SIGN MUSH3 TIMES A;Lo;0;L;;;;;N;;;;;
+1223B;CUNEIFORM SIGN MUSH3 TIMES A PLUS DI;Lo;0;L;;;;;N;;;;;
+1223C;CUNEIFORM SIGN MUSH3 TIMES DI;Lo;0;L;;;;;N;;;;;
+1223D;CUNEIFORM SIGN MUSH3 GUNU;Lo;0;L;;;;;N;;;;;
+1223E;CUNEIFORM SIGN NA;Lo;0;L;;;;;N;;;;;
+1223F;CUNEIFORM SIGN NA2;Lo;0;L;;;;;N;;;;;
+12240;CUNEIFORM SIGN NAGA;Lo;0;L;;;;;N;;;;;
+12241;CUNEIFORM SIGN NAGA INVERTED;Lo;0;L;;;;;N;;;;;
+12242;CUNEIFORM SIGN NAGA TIMES SHU TENU;Lo;0;L;;;;;N;;;;;
+12243;CUNEIFORM SIGN NAGA OPPOSING NAGA;Lo;0;L;;;;;N;;;;;
+12244;CUNEIFORM SIGN NAGAR;Lo;0;L;;;;;N;;;;;
+12245;CUNEIFORM SIGN NAM NUTILLU;Lo;0;L;;;;;N;;;;;
+12246;CUNEIFORM SIGN NAM;Lo;0;L;;;;;N;;;;;
+12247;CUNEIFORM SIGN NAM2;Lo;0;L;;;;;N;;;;;
+12248;CUNEIFORM SIGN NE;Lo;0;L;;;;;N;;;;;
+12249;CUNEIFORM SIGN NE TIMES A;Lo;0;L;;;;;N;;;;;
+1224A;CUNEIFORM SIGN NE TIMES UD;Lo;0;L;;;;;N;;;;;
+1224B;CUNEIFORM SIGN NE SHESHIG;Lo;0;L;;;;;N;;;;;
+1224C;CUNEIFORM SIGN NI;Lo;0;L;;;;;N;;;;;
+1224D;CUNEIFORM SIGN NI TIMES E;Lo;0;L;;;;;N;;;;;
+1224E;CUNEIFORM SIGN NI2;Lo;0;L;;;;;N;;;;;
+1224F;CUNEIFORM SIGN NIM;Lo;0;L;;;;;N;;;;;
+12250;CUNEIFORM SIGN NIM TIMES GAN2 TENU;Lo;0;L;;;;;N;;;;;
+12251;CUNEIFORM SIGN NIM TIMES GAR PLUS GAN2 TENU;Lo;0;L;;;;;N;;;;;
+12252;CUNEIFORM SIGN NINDA2;Lo;0;L;;;;;N;;;;;
+12253;CUNEIFORM SIGN NINDA2 TIMES AN;Lo;0;L;;;;;N;;;;;
+12254;CUNEIFORM SIGN NINDA2 TIMES ASH;Lo;0;L;;;;;N;;;;;
+12255;CUNEIFORM SIGN NINDA2 TIMES ASH PLUS ASH;Lo;0;L;;;;;N;;;;;
+12256;CUNEIFORM SIGN NINDA2 TIMES GUD;Lo;0;L;;;;;N;;;;;
+12257;CUNEIFORM SIGN NINDA2 TIMES ME PLUS GAN2 TENU;Lo;0;L;;;;;N;;;;;
+12258;CUNEIFORM SIGN NINDA2 TIMES NE;Lo;0;L;;;;;N;;;;;
+12259;CUNEIFORM SIGN NINDA2 TIMES NUN;Lo;0;L;;;;;N;;;;;
+1225A;CUNEIFORM SIGN NINDA2 TIMES SHE;Lo;0;L;;;;;N;;;;;
+1225B;CUNEIFORM SIGN NINDA2 TIMES SHE PLUS A AN;Lo;0;L;;;;;N;;;;;
+1225C;CUNEIFORM SIGN NINDA2 TIMES SHE PLUS ASH;Lo;0;L;;;;;N;;;;;
+1225D;CUNEIFORM SIGN NINDA2 TIMES SHE PLUS ASH PLUS ASH;Lo;0;L;;;;;N;;;;;
+1225E;CUNEIFORM SIGN NINDA2 TIMES U2 PLUS ASH;Lo;0;L;;;;;N;;;;;
+1225F;CUNEIFORM SIGN NINDA2 TIMES USH;Lo;0;L;;;;;N;;;;;
+12260;CUNEIFORM SIGN NISAG;Lo;0;L;;;;;N;;;;;
+12261;CUNEIFORM SIGN NU;Lo;0;L;;;;;N;;;;;
+12262;CUNEIFORM SIGN NU11;Lo;0;L;;;;;N;;;;;
+12263;CUNEIFORM SIGN NUN;Lo;0;L;;;;;N;;;;;
+12264;CUNEIFORM SIGN NUN LAGAR TIMES GAR;Lo;0;L;;;;;N;;;;;
+12265;CUNEIFORM SIGN NUN LAGAR TIMES MASH;Lo;0;L;;;;;N;;;;;
+12266;CUNEIFORM SIGN NUN LAGAR TIMES SAL;Lo;0;L;;;;;N;;;;;
+12267;CUNEIFORM SIGN NUN LAGAR TIMES SAL OVER NUN LAGAR TIMES SAL;Lo;0;L;;;;;N;;;;;
+12268;CUNEIFORM SIGN NUN LAGAR TIMES USH;Lo;0;L;;;;;N;;;;;
+12269;CUNEIFORM SIGN NUN TENU;Lo;0;L;;;;;N;;;;;
+1226A;CUNEIFORM SIGN NUN OVER NUN;Lo;0;L;;;;;N;;;;;
+1226B;CUNEIFORM SIGN NUN CROSSING NUN;Lo;0;L;;;;;N;;;;;
+1226C;CUNEIFORM SIGN NUN CROSSING NUN LAGAR OVER LAGAR;Lo;0;L;;;;;N;;;;;
+1226D;CUNEIFORM SIGN NUNUZ;Lo;0;L;;;;;N;;;;;
+1226E;CUNEIFORM SIGN NUNUZ AB2 TIMES ASHGAB;Lo;0;L;;;;;N;;;;;
+1226F;CUNEIFORM SIGN NUNUZ AB2 TIMES BI;Lo;0;L;;;;;N;;;;;
+12270;CUNEIFORM SIGN NUNUZ AB2 TIMES DUG;Lo;0;L;;;;;N;;;;;
+12271;CUNEIFORM SIGN NUNUZ AB2 TIMES GUD;Lo;0;L;;;;;N;;;;;
+12272;CUNEIFORM SIGN NUNUZ AB2 TIMES IGI GUNU;Lo;0;L;;;;;N;;;;;
+12273;CUNEIFORM SIGN NUNUZ AB2 TIMES KAD3;Lo;0;L;;;;;N;;;;;
+12274;CUNEIFORM SIGN NUNUZ AB2 TIMES LA;Lo;0;L;;;;;N;;;;;
+12275;CUNEIFORM SIGN NUNUZ AB2 TIMES NE;Lo;0;L;;;;;N;;;;;
+12276;CUNEIFORM SIGN NUNUZ AB2 TIMES SILA3;Lo;0;L;;;;;N;;;;;
+12277;CUNEIFORM SIGN NUNUZ AB2 TIMES U2;Lo;0;L;;;;;N;;;;;
+12278;CUNEIFORM SIGN NUNUZ KISIM5 TIMES BI;Lo;0;L;;;;;N;;;;;
+12279;CUNEIFORM SIGN NUNUZ KISIM5 TIMES BI U;Lo;0;L;;;;;N;;;;;
+1227A;CUNEIFORM SIGN PA;Lo;0;L;;;;;N;;;;;
+1227B;CUNEIFORM SIGN PAD;Lo;0;L;;;;;N;;;;;
+1227C;CUNEIFORM SIGN PAN;Lo;0;L;;;;;N;;;;;
+1227D;CUNEIFORM SIGN PAP;Lo;0;L;;;;;N;;;;;
+1227E;CUNEIFORM SIGN PESH2;Lo;0;L;;;;;N;;;;;
+1227F;CUNEIFORM SIGN PI;Lo;0;L;;;;;N;;;;;
+12280;CUNEIFORM SIGN PI TIMES A;Lo;0;L;;;;;N;;;;;
+12281;CUNEIFORM SIGN PI TIMES AB;Lo;0;L;;;;;N;;;;;
+12282;CUNEIFORM SIGN PI TIMES BI;Lo;0;L;;;;;N;;;;;
+12283;CUNEIFORM SIGN PI TIMES BU;Lo;0;L;;;;;N;;;;;
+12284;CUNEIFORM SIGN PI TIMES E;Lo;0;L;;;;;N;;;;;
+12285;CUNEIFORM SIGN PI TIMES I;Lo;0;L;;;;;N;;;;;
+12286;CUNEIFORM SIGN PI TIMES IB;Lo;0;L;;;;;N;;;;;
+12287;CUNEIFORM SIGN PI TIMES U;Lo;0;L;;;;;N;;;;;
+12288;CUNEIFORM SIGN PI TIMES U2;Lo;0;L;;;;;N;;;;;
+12289;CUNEIFORM SIGN PI CROSSING PI;Lo;0;L;;;;;N;;;;;
+1228A;CUNEIFORM SIGN PIRIG;Lo;0;L;;;;;N;;;;;
+1228B;CUNEIFORM SIGN PIRIG TIMES KAL;Lo;0;L;;;;;N;;;;;
+1228C;CUNEIFORM SIGN PIRIG TIMES UD;Lo;0;L;;;;;N;;;;;
+1228D;CUNEIFORM SIGN PIRIG TIMES ZA;Lo;0;L;;;;;N;;;;;
+1228E;CUNEIFORM SIGN PIRIG OPPOSING PIRIG;Lo;0;L;;;;;N;;;;;
+1228F;CUNEIFORM SIGN RA;Lo;0;L;;;;;N;;;;;
+12290;CUNEIFORM SIGN RAB;Lo;0;L;;;;;N;;;;;
+12291;CUNEIFORM SIGN RI;Lo;0;L;;;;;N;;;;;
+12292;CUNEIFORM SIGN RU;Lo;0;L;;;;;N;;;;;
+12293;CUNEIFORM SIGN SA;Lo;0;L;;;;;N;;;;;
+12294;CUNEIFORM SIGN SAG NUTILLU;Lo;0;L;;;;;N;;;;;
+12295;CUNEIFORM SIGN SAG;Lo;0;L;;;;;N;;;;;
+12296;CUNEIFORM SIGN SAG TIMES A;Lo;0;L;;;;;N;;;;;
+12297;CUNEIFORM SIGN SAG TIMES DU;Lo;0;L;;;;;N;;;;;
+12298;CUNEIFORM SIGN SAG TIMES DUB;Lo;0;L;;;;;N;;;;;
+12299;CUNEIFORM SIGN SAG TIMES HA;Lo;0;L;;;;;N;;;;;
+1229A;CUNEIFORM SIGN SAG TIMES KAK;Lo;0;L;;;;;N;;;;;
+1229B;CUNEIFORM SIGN SAG TIMES KUR;Lo;0;L;;;;;N;;;;;
+1229C;CUNEIFORM SIGN SAG TIMES LUM;Lo;0;L;;;;;N;;;;;
+1229D;CUNEIFORM SIGN SAG TIMES MI;Lo;0;L;;;;;N;;;;;
+1229E;CUNEIFORM SIGN SAG TIMES NUN;Lo;0;L;;;;;N;;;;;
+1229F;CUNEIFORM SIGN SAG TIMES SAL;Lo;0;L;;;;;N;;;;;
+122A0;CUNEIFORM SIGN SAG TIMES SHID;Lo;0;L;;;;;N;;;;;
+122A1;CUNEIFORM SIGN SAG TIMES TAB;Lo;0;L;;;;;N;;;;;
+122A2;CUNEIFORM SIGN SAG TIMES U2;Lo;0;L;;;;;N;;;;;
+122A3;CUNEIFORM SIGN SAG TIMES UB;Lo;0;L;;;;;N;;;;;
+122A4;CUNEIFORM SIGN SAG TIMES UM;Lo;0;L;;;;;N;;;;;
+122A5;CUNEIFORM SIGN SAG TIMES UR;Lo;0;L;;;;;N;;;;;
+122A6;CUNEIFORM SIGN SAG TIMES USH;Lo;0;L;;;;;N;;;;;
+122A7;CUNEIFORM SIGN SAG OVER SAG;Lo;0;L;;;;;N;;;;;
+122A8;CUNEIFORM SIGN SAG GUNU;Lo;0;L;;;;;N;;;;;
+122A9;CUNEIFORM SIGN SAL;Lo;0;L;;;;;N;;;;;
+122AA;CUNEIFORM SIGN SAL LAGAB TIMES ASH2;Lo;0;L;;;;;N;;;;;
+122AB;CUNEIFORM SIGN SANGA2;Lo;0;L;;;;;N;;;;;
+122AC;CUNEIFORM SIGN SAR;Lo;0;L;;;;;N;;;;;
+122AD;CUNEIFORM SIGN SHA;Lo;0;L;;;;;N;;;;;
+122AE;CUNEIFORM SIGN SHA3;Lo;0;L;;;;;N;;;;;
+122AF;CUNEIFORM SIGN SHA3 TIMES A;Lo;0;L;;;;;N;;;;;
+122B0;CUNEIFORM SIGN SHA3 TIMES BAD;Lo;0;L;;;;;N;;;;;
+122B1;CUNEIFORM SIGN SHA3 TIMES GISH;Lo;0;L;;;;;N;;;;;
+122B2;CUNEIFORM SIGN SHA3 TIMES NE;Lo;0;L;;;;;N;;;;;
+122B3;CUNEIFORM SIGN SHA3 TIMES SHU2;Lo;0;L;;;;;N;;;;;
+122B4;CUNEIFORM SIGN SHA3 TIMES TUR;Lo;0;L;;;;;N;;;;;
+122B5;CUNEIFORM SIGN SHA3 TIMES U;Lo;0;L;;;;;N;;;;;
+122B6;CUNEIFORM SIGN SHA3 TIMES U PLUS A;Lo;0;L;;;;;N;;;;;
+122B7;CUNEIFORM SIGN SHA6;Lo;0;L;;;;;N;;;;;
+122B8;CUNEIFORM SIGN SHAB6;Lo;0;L;;;;;N;;;;;
+122B9;CUNEIFORM SIGN SHAR2;Lo;0;L;;;;;N;;;;;
+122BA;CUNEIFORM SIGN SHE;Lo;0;L;;;;;N;;;;;
+122BB;CUNEIFORM SIGN SHE HU;Lo;0;L;;;;;N;;;;;
+122BC;CUNEIFORM SIGN SHE OVER SHE GAD OVER GAD GAR OVER GAR;Lo;0;L;;;;;N;;;;;
+122BD;CUNEIFORM SIGN SHE OVER SHE TAB OVER TAB GAR OVER GAR;Lo;0;L;;;;;N;;;;;
+122BE;CUNEIFORM SIGN SHEG9;Lo;0;L;;;;;N;;;;;
+122BF;CUNEIFORM SIGN SHEN;Lo;0;L;;;;;N;;;;;
+122C0;CUNEIFORM SIGN SHESH;Lo;0;L;;;;;N;;;;;
+122C1;CUNEIFORM SIGN SHESH2;Lo;0;L;;;;;N;;;;;
+122C2;CUNEIFORM SIGN SHESHLAM;Lo;0;L;;;;;N;;;;;
+122C3;CUNEIFORM SIGN SHID;Lo;0;L;;;;;N;;;;;
+122C4;CUNEIFORM SIGN SHID TIMES A;Lo;0;L;;;;;N;;;;;
+122C5;CUNEIFORM SIGN SHID TIMES IM;Lo;0;L;;;;;N;;;;;
+122C6;CUNEIFORM SIGN SHIM;Lo;0;L;;;;;N;;;;;
+122C7;CUNEIFORM SIGN SHIM TIMES A;Lo;0;L;;;;;N;;;;;
+122C8;CUNEIFORM SIGN SHIM TIMES BAL;Lo;0;L;;;;;N;;;;;
+122C9;CUNEIFORM SIGN SHIM TIMES BULUG;Lo;0;L;;;;;N;;;;;
+122CA;CUNEIFORM SIGN SHIM TIMES DIN;Lo;0;L;;;;;N;;;;;
+122CB;CUNEIFORM SIGN SHIM TIMES GAR;Lo;0;L;;;;;N;;;;;
+122CC;CUNEIFORM SIGN SHIM TIMES IGI;Lo;0;L;;;;;N;;;;;
+122CD;CUNEIFORM SIGN SHIM TIMES IGI GUNU;Lo;0;L;;;;;N;;;;;
+122CE;CUNEIFORM SIGN SHIM TIMES KUSHU2;Lo;0;L;;;;;N;;;;;
+122CF;CUNEIFORM SIGN SHIM TIMES LUL;Lo;0;L;;;;;N;;;;;
+122D0;CUNEIFORM SIGN SHIM TIMES MUG;Lo;0;L;;;;;N;;;;;
+122D1;CUNEIFORM SIGN SHIM TIMES SAL;Lo;0;L;;;;;N;;;;;
+122D2;CUNEIFORM SIGN SHINIG;Lo;0;L;;;;;N;;;;;
+122D3;CUNEIFORM SIGN SHIR;Lo;0;L;;;;;N;;;;;
+122D4;CUNEIFORM SIGN SHIR TENU;Lo;0;L;;;;;N;;;;;
+122D5;CUNEIFORM SIGN SHIR OVER SHIR BUR OVER BUR;Lo;0;L;;;;;N;;;;;
+122D6;CUNEIFORM SIGN SHITA;Lo;0;L;;;;;N;;;;;
+122D7;CUNEIFORM SIGN SHU;Lo;0;L;;;;;N;;;;;
+122D8;CUNEIFORM SIGN SHU OVER INVERTED SHU;Lo;0;L;;;;;N;;;;;
+122D9;CUNEIFORM SIGN SHU2;Lo;0;L;;;;;N;;;;;
+122DA;CUNEIFORM SIGN SHUBUR;Lo;0;L;;;;;N;;;;;
+122DB;CUNEIFORM SIGN SI;Lo;0;L;;;;;N;;;;;
+122DC;CUNEIFORM SIGN SI GUNU;Lo;0;L;;;;;N;;;;;
+122DD;CUNEIFORM SIGN SIG;Lo;0;L;;;;;N;;;;;
+122DE;CUNEIFORM SIGN SIG4;Lo;0;L;;;;;N;;;;;
+122DF;CUNEIFORM SIGN SIG4 OVER SIG4 SHU2;Lo;0;L;;;;;N;;;;;
+122E0;CUNEIFORM SIGN SIK2;Lo;0;L;;;;;N;;;;;
+122E1;CUNEIFORM SIGN SILA3;Lo;0;L;;;;;N;;;;;
+122E2;CUNEIFORM SIGN SU;Lo;0;L;;;;;N;;;;;
+122E3;CUNEIFORM SIGN SU OVER SU;Lo;0;L;;;;;N;;;;;
+122E4;CUNEIFORM SIGN SUD;Lo;0;L;;;;;N;;;;;
+122E5;CUNEIFORM SIGN SUD2;Lo;0;L;;;;;N;;;;;
+122E6;CUNEIFORM SIGN SUHUR;Lo;0;L;;;;;N;;;;;
+122E7;CUNEIFORM SIGN SUM;Lo;0;L;;;;;N;;;;;
+122E8;CUNEIFORM SIGN SUMASH;Lo;0;L;;;;;N;;;;;
+122E9;CUNEIFORM SIGN SUR;Lo;0;L;;;;;N;;;;;
+122EA;CUNEIFORM SIGN SUR9;Lo;0;L;;;;;N;;;;;
+122EB;CUNEIFORM SIGN TA;Lo;0;L;;;;;N;;;;;
+122EC;CUNEIFORM SIGN TA ASTERISK;Lo;0;L;;;;;N;;;;;
+122ED;CUNEIFORM SIGN TA TIMES HI;Lo;0;L;;;;;N;;;;;
+122EE;CUNEIFORM SIGN TA TIMES MI;Lo;0;L;;;;;N;;;;;
+122EF;CUNEIFORM SIGN TA GUNU;Lo;0;L;;;;;N;;;;;
+122F0;CUNEIFORM SIGN TAB;Lo;0;L;;;;;N;;;;;
+122F1;CUNEIFORM SIGN TAB OVER TAB NI OVER NI DISH OVER DISH;Lo;0;L;;;;;N;;;;;
+122F2;CUNEIFORM SIGN TAB SQUARED;Lo;0;L;;;;;N;;;;;
+122F3;CUNEIFORM SIGN TAG;Lo;0;L;;;;;N;;;;;
+122F4;CUNEIFORM SIGN TAG TIMES BI;Lo;0;L;;;;;N;;;;;
+122F5;CUNEIFORM SIGN TAG TIMES GUD;Lo;0;L;;;;;N;;;;;
+122F6;CUNEIFORM SIGN TAG TIMES SHE;Lo;0;L;;;;;N;;;;;
+122F7;CUNEIFORM SIGN TAG TIMES SHU;Lo;0;L;;;;;N;;;;;
+122F8;CUNEIFORM SIGN TAG TIMES TUG2;Lo;0;L;;;;;N;;;;;
+122F9;CUNEIFORM SIGN TAG TIMES UD;Lo;0;L;;;;;N;;;;;
+122FA;CUNEIFORM SIGN TAK4;Lo;0;L;;;;;N;;;;;
+122FB;CUNEIFORM SIGN TAR;Lo;0;L;;;;;N;;;;;
+122FC;CUNEIFORM SIGN TE;Lo;0;L;;;;;N;;;;;
+122FD;CUNEIFORM SIGN TE GUNU;Lo;0;L;;;;;N;;;;;
+122FE;CUNEIFORM SIGN TI;Lo;0;L;;;;;N;;;;;
+122FF;CUNEIFORM SIGN TI TENU;Lo;0;L;;;;;N;;;;;
+12300;CUNEIFORM SIGN TIL;Lo;0;L;;;;;N;;;;;
+12301;CUNEIFORM SIGN TIR;Lo;0;L;;;;;N;;;;;
+12302;CUNEIFORM SIGN TIR TIMES TAK4;Lo;0;L;;;;;N;;;;;
+12303;CUNEIFORM SIGN TIR OVER TIR;Lo;0;L;;;;;N;;;;;
+12304;CUNEIFORM SIGN TIR OVER TIR GAD OVER GAD GAR OVER GAR;Lo;0;L;;;;;N;;;;;
+12305;CUNEIFORM SIGN TU;Lo;0;L;;;;;N;;;;;
+12306;CUNEIFORM SIGN TUG2;Lo;0;L;;;;;N;;;;;
+12307;CUNEIFORM SIGN TUK;Lo;0;L;;;;;N;;;;;
+12308;CUNEIFORM SIGN TUM;Lo;0;L;;;;;N;;;;;
+12309;CUNEIFORM SIGN TUR;Lo;0;L;;;;;N;;;;;
+1230A;CUNEIFORM SIGN TUR OVER TUR ZA OVER ZA;Lo;0;L;;;;;N;;;;;
+1230B;CUNEIFORM SIGN U;Lo;0;L;;;;;N;;;;;
+1230C;CUNEIFORM SIGN U GUD;Lo;0;L;;;;;N;;;;;
+1230D;CUNEIFORM SIGN U U U;Lo;0;L;;;;;N;;;;;
+1230E;CUNEIFORM SIGN U OVER U PA OVER PA GAR OVER GAR;Lo;0;L;;;;;N;;;;;
+1230F;CUNEIFORM SIGN U OVER U SUR OVER SUR;Lo;0;L;;;;;N;;;;;
+12310;CUNEIFORM SIGN U OVER U U REVERSED OVER U REVERSED;Lo;0;L;;;;;N;;;;;
+12311;CUNEIFORM SIGN U2;Lo;0;L;;;;;N;;;;;
+12312;CUNEIFORM SIGN UB;Lo;0;L;;;;;N;;;;;
+12313;CUNEIFORM SIGN UD;Lo;0;L;;;;;N;;;;;
+12314;CUNEIFORM SIGN UD KUSHU2;Lo;0;L;;;;;N;;;;;
+12315;CUNEIFORM SIGN UD TIMES BAD;Lo;0;L;;;;;N;;;;;
+12316;CUNEIFORM SIGN UD TIMES MI;Lo;0;L;;;;;N;;;;;
+12317;CUNEIFORM SIGN UD TIMES U PLUS U PLUS U;Lo;0;L;;;;;N;;;;;
+12318;CUNEIFORM SIGN UD TIMES U PLUS U PLUS U GUNU;Lo;0;L;;;;;N;;;;;
+12319;CUNEIFORM SIGN UD GUNU;Lo;0;L;;;;;N;;;;;
+1231A;CUNEIFORM SIGN UD SHESHIG;Lo;0;L;;;;;N;;;;;
+1231B;CUNEIFORM SIGN UD SHESHIG TIMES BAD;Lo;0;L;;;;;N;;;;;
+1231C;CUNEIFORM SIGN UDUG;Lo;0;L;;;;;N;;;;;
+1231D;CUNEIFORM SIGN UM;Lo;0;L;;;;;N;;;;;
+1231E;CUNEIFORM SIGN UM TIMES LAGAB;Lo;0;L;;;;;N;;;;;
+1231F;CUNEIFORM SIGN UM TIMES ME PLUS DA;Lo;0;L;;;;;N;;;;;
+12320;CUNEIFORM SIGN UM TIMES SHA3;Lo;0;L;;;;;N;;;;;
+12321;CUNEIFORM SIGN UM TIMES U;Lo;0;L;;;;;N;;;;;
+12322;CUNEIFORM SIGN UMBIN;Lo;0;L;;;;;N;;;;;
+12323;CUNEIFORM SIGN UMUM;Lo;0;L;;;;;N;;;;;
+12324;CUNEIFORM SIGN UMUM TIMES KASKAL;Lo;0;L;;;;;N;;;;;
+12325;CUNEIFORM SIGN UMUM TIMES PA;Lo;0;L;;;;;N;;;;;
+12326;CUNEIFORM SIGN UN;Lo;0;L;;;;;N;;;;;
+12327;CUNEIFORM SIGN UN GUNU;Lo;0;L;;;;;N;;;;;
+12328;CUNEIFORM SIGN UR;Lo;0;L;;;;;N;;;;;
+12329;CUNEIFORM SIGN UR CROSSING UR;Lo;0;L;;;;;N;;;;;
+1232A;CUNEIFORM SIGN UR SHESHIG;Lo;0;L;;;;;N;;;;;
+1232B;CUNEIFORM SIGN UR2;Lo;0;L;;;;;N;;;;;
+1232C;CUNEIFORM SIGN UR2 TIMES A PLUS HA;Lo;0;L;;;;;N;;;;;
+1232D;CUNEIFORM SIGN UR2 TIMES A PLUS NA;Lo;0;L;;;;;N;;;;;
+1232E;CUNEIFORM SIGN UR2 TIMES AL;Lo;0;L;;;;;N;;;;;
+1232F;CUNEIFORM SIGN UR2 TIMES HA;Lo;0;L;;;;;N;;;;;
+12330;CUNEIFORM SIGN UR2 TIMES NUN;Lo;0;L;;;;;N;;;;;
+12331;CUNEIFORM SIGN UR2 TIMES U2;Lo;0;L;;;;;N;;;;;
+12332;CUNEIFORM SIGN UR2 TIMES U2 PLUS ASH;Lo;0;L;;;;;N;;;;;
+12333;CUNEIFORM SIGN UR2 TIMES U2 PLUS BI;Lo;0;L;;;;;N;;;;;
+12334;CUNEIFORM SIGN UR4;Lo;0;L;;;;;N;;;;;
+12335;CUNEIFORM SIGN URI;Lo;0;L;;;;;N;;;;;
+12336;CUNEIFORM SIGN URI3;Lo;0;L;;;;;N;;;;;
+12337;CUNEIFORM SIGN URU;Lo;0;L;;;;;N;;;;;
+12338;CUNEIFORM SIGN URU TIMES A;Lo;0;L;;;;;N;;;;;
+12339;CUNEIFORM SIGN URU TIMES ASHGAB;Lo;0;L;;;;;N;;;;;
+1233A;CUNEIFORM SIGN URU TIMES BAR;Lo;0;L;;;;;N;;;;;
+1233B;CUNEIFORM SIGN URU TIMES DUN;Lo;0;L;;;;;N;;;;;
+1233C;CUNEIFORM SIGN URU TIMES GA;Lo;0;L;;;;;N;;;;;
+1233D;CUNEIFORM SIGN URU TIMES GAL;Lo;0;L;;;;;N;;;;;
+1233E;CUNEIFORM SIGN URU TIMES GAN2 TENU;Lo;0;L;;;;;N;;;;;
+1233F;CUNEIFORM SIGN URU TIMES GAR;Lo;0;L;;;;;N;;;;;
+12340;CUNEIFORM SIGN URU TIMES GU;Lo;0;L;;;;;N;;;;;
+12341;CUNEIFORM SIGN URU TIMES HA;Lo;0;L;;;;;N;;;;;
+12342;CUNEIFORM SIGN URU TIMES IGI;Lo;0;L;;;;;N;;;;;
+12343;CUNEIFORM SIGN URU TIMES IM;Lo;0;L;;;;;N;;;;;
+12344;CUNEIFORM SIGN URU TIMES ISH;Lo;0;L;;;;;N;;;;;
+12345;CUNEIFORM SIGN URU TIMES KI;Lo;0;L;;;;;N;;;;;
+12346;CUNEIFORM SIGN URU TIMES LUM;Lo;0;L;;;;;N;;;;;
+12347;CUNEIFORM SIGN URU TIMES MIN;Lo;0;L;;;;;N;;;;;
+12348;CUNEIFORM SIGN URU TIMES PA;Lo;0;L;;;;;N;;;;;
+12349;CUNEIFORM SIGN URU TIMES SHE;Lo;0;L;;;;;N;;;;;
+1234A;CUNEIFORM SIGN URU TIMES SIG4;Lo;0;L;;;;;N;;;;;
+1234B;CUNEIFORM SIGN URU TIMES TU;Lo;0;L;;;;;N;;;;;
+1234C;CUNEIFORM SIGN URU TIMES U PLUS GUD;Lo;0;L;;;;;N;;;;;
+1234D;CUNEIFORM SIGN URU TIMES UD;Lo;0;L;;;;;N;;;;;
+1234E;CUNEIFORM SIGN URU TIMES URUDA;Lo;0;L;;;;;N;;;;;
+1234F;CUNEIFORM SIGN URUDA;Lo;0;L;;;;;N;;;;;
+12350;CUNEIFORM SIGN URUDA TIMES U;Lo;0;L;;;;;N;;;;;
+12351;CUNEIFORM SIGN USH;Lo;0;L;;;;;N;;;;;
+12352;CUNEIFORM SIGN USH TIMES A;Lo;0;L;;;;;N;;;;;
+12353;CUNEIFORM SIGN USH TIMES KU;Lo;0;L;;;;;N;;;;;
+12354;CUNEIFORM SIGN USH TIMES KUR;Lo;0;L;;;;;N;;;;;
+12355;CUNEIFORM SIGN USH TIMES TAK4;Lo;0;L;;;;;N;;;;;
+12356;CUNEIFORM SIGN USHX;Lo;0;L;;;;;N;;;;;
+12357;CUNEIFORM SIGN USH2;Lo;0;L;;;;;N;;;;;
+12358;CUNEIFORM SIGN USHUMX;Lo;0;L;;;;;N;;;;;
+12359;CUNEIFORM SIGN UTUKI;Lo;0;L;;;;;N;;;;;
+1235A;CUNEIFORM SIGN UZ3;Lo;0;L;;;;;N;;;;;
+1235B;CUNEIFORM SIGN UZ3 TIMES KASKAL;Lo;0;L;;;;;N;;;;;
+1235C;CUNEIFORM SIGN UZU;Lo;0;L;;;;;N;;;;;
+1235D;CUNEIFORM SIGN ZA;Lo;0;L;;;;;N;;;;;
+1235E;CUNEIFORM SIGN ZA TENU;Lo;0;L;;;;;N;;;;;
+1235F;CUNEIFORM SIGN ZA SQUARED TIMES KUR;Lo;0;L;;;;;N;;;;;
+12360;CUNEIFORM SIGN ZAG;Lo;0;L;;;;;N;;;;;
+12361;CUNEIFORM SIGN ZAMX;Lo;0;L;;;;;N;;;;;
+12362;CUNEIFORM SIGN ZE2;Lo;0;L;;;;;N;;;;;
+12363;CUNEIFORM SIGN ZI;Lo;0;L;;;;;N;;;;;
+12364;CUNEIFORM SIGN ZI OVER ZI;Lo;0;L;;;;;N;;;;;
+12365;CUNEIFORM SIGN ZI3;Lo;0;L;;;;;N;;;;;
+12366;CUNEIFORM SIGN ZIB;Lo;0;L;;;;;N;;;;;
+12367;CUNEIFORM SIGN ZIB KABA TENU;Lo;0;L;;;;;N;;;;;
+12368;CUNEIFORM SIGN ZIG;Lo;0;L;;;;;N;;;;;
+12369;CUNEIFORM SIGN ZIZ2;Lo;0;L;;;;;N;;;;;
+1236A;CUNEIFORM SIGN ZU;Lo;0;L;;;;;N;;;;;
+1236B;CUNEIFORM SIGN ZU5;Lo;0;L;;;;;N;;;;;
+1236C;CUNEIFORM SIGN ZU5 TIMES A;Lo;0;L;;;;;N;;;;;
+1236D;CUNEIFORM SIGN ZUBUR;Lo;0;L;;;;;N;;;;;
+1236E;CUNEIFORM SIGN ZUM;Lo;0;L;;;;;N;;;;;
+1236F;CUNEIFORM SIGN KAP ELAMITE;Lo;0;L;;;;;N;;;;;
+12370;CUNEIFORM SIGN AB TIMES NUN;Lo;0;L;;;;;N;;;;;
+12371;CUNEIFORM SIGN AB2 TIMES A;Lo;0;L;;;;;N;;;;;
+12372;CUNEIFORM SIGN AMAR TIMES KUG;Lo;0;L;;;;;N;;;;;
+12373;CUNEIFORM SIGN DAG KISIM5 TIMES U2 PLUS MASH;Lo;0;L;;;;;N;;;;;
+12374;CUNEIFORM SIGN DAG3;Lo;0;L;;;;;N;;;;;
+12375;CUNEIFORM SIGN DISH PLUS SHU;Lo;0;L;;;;;N;;;;;
+12376;CUNEIFORM SIGN DUB TIMES SHE;Lo;0;L;;;;;N;;;;;
+12377;CUNEIFORM SIGN EZEN TIMES GUD;Lo;0;L;;;;;N;;;;;
+12378;CUNEIFORM SIGN EZEN TIMES SHE;Lo;0;L;;;;;N;;;;;
+12379;CUNEIFORM SIGN GA2 TIMES AN PLUS KAK PLUS A;Lo;0;L;;;;;N;;;;;
+1237A;CUNEIFORM SIGN GA2 TIMES ASH2;Lo;0;L;;;;;N;;;;;
+1237B;CUNEIFORM SIGN GE22;Lo;0;L;;;;;N;;;;;
+1237C;CUNEIFORM SIGN GIG;Lo;0;L;;;;;N;;;;;
+1237D;CUNEIFORM SIGN HUSH;Lo;0;L;;;;;N;;;;;
+1237E;CUNEIFORM SIGN KA TIMES ANSHE;Lo;0;L;;;;;N;;;;;
+1237F;CUNEIFORM SIGN KA TIMES ASH3;Lo;0;L;;;;;N;;;;;
+12380;CUNEIFORM SIGN KA TIMES GISH;Lo;0;L;;;;;N;;;;;
+12381;CUNEIFORM SIGN KA TIMES GUD;Lo;0;L;;;;;N;;;;;
+12382;CUNEIFORM SIGN KA TIMES HI TIMES ASH2;Lo;0;L;;;;;N;;;;;
+12383;CUNEIFORM SIGN KA TIMES LUM;Lo;0;L;;;;;N;;;;;
+12384;CUNEIFORM SIGN KA TIMES PA;Lo;0;L;;;;;N;;;;;
+12385;CUNEIFORM SIGN KA TIMES SHUL;Lo;0;L;;;;;N;;;;;
+12386;CUNEIFORM SIGN KA TIMES TU;Lo;0;L;;;;;N;;;;;
+12387;CUNEIFORM SIGN KA TIMES UR2;Lo;0;L;;;;;N;;;;;
+12388;CUNEIFORM SIGN LAGAB TIMES GI;Lo;0;L;;;;;N;;;;;
+12389;CUNEIFORM SIGN LU2 SHESHIG TIMES BAD;Lo;0;L;;;;;N;;;;;
+1238A;CUNEIFORM SIGN LU2 TIMES ESH2 PLUS LAL;Lo;0;L;;;;;N;;;;;
+1238B;CUNEIFORM SIGN LU2 TIMES SHU;Lo;0;L;;;;;N;;;;;
+1238C;CUNEIFORM SIGN MESH;Lo;0;L;;;;;N;;;;;
+1238D;CUNEIFORM SIGN MUSH3 TIMES ZA;Lo;0;L;;;;;N;;;;;
+1238E;CUNEIFORM SIGN NA4;Lo;0;L;;;;;N;;;;;
+1238F;CUNEIFORM SIGN NIN;Lo;0;L;;;;;N;;;;;
+12390;CUNEIFORM SIGN NIN9;Lo;0;L;;;;;N;;;;;
+12391;CUNEIFORM SIGN NINDA2 TIMES BAL;Lo;0;L;;;;;N;;;;;
+12392;CUNEIFORM SIGN NINDA2 TIMES GI;Lo;0;L;;;;;N;;;;;
+12393;CUNEIFORM SIGN NU11 ROTATED NINETY DEGREES;Lo;0;L;;;;;N;;;;;
+12394;CUNEIFORM SIGN PESH2 ASTERISK;Lo;0;L;;;;;N;;;;;
+12395;CUNEIFORM SIGN PIR2;Lo;0;L;;;;;N;;;;;
+12396;CUNEIFORM SIGN SAG TIMES IGI GUNU;Lo;0;L;;;;;N;;;;;
+12397;CUNEIFORM SIGN TI2;Lo;0;L;;;;;N;;;;;
+12398;CUNEIFORM SIGN UM TIMES ME;Lo;0;L;;;;;N;;;;;
+12399;CUNEIFORM SIGN U U;Lo;0;L;;;;;N;;;;;
+12400;CUNEIFORM NUMERIC SIGN TWO ASH;Nl;0;L;;;;2;N;;;;;
+12401;CUNEIFORM NUMERIC SIGN THREE ASH;Nl;0;L;;;;3;N;;;;;
+12402;CUNEIFORM NUMERIC SIGN FOUR ASH;Nl;0;L;;;;4;N;;;;;
+12403;CUNEIFORM NUMERIC SIGN FIVE ASH;Nl;0;L;;;;5;N;;;;;
+12404;CUNEIFORM NUMERIC SIGN SIX ASH;Nl;0;L;;;;6;N;;;;;
+12405;CUNEIFORM NUMERIC SIGN SEVEN ASH;Nl;0;L;;;;7;N;;;;;
+12406;CUNEIFORM NUMERIC SIGN EIGHT ASH;Nl;0;L;;;;8;N;;;;;
+12407;CUNEIFORM NUMERIC SIGN NINE ASH;Nl;0;L;;;;9;N;;;;;
+12408;CUNEIFORM NUMERIC SIGN THREE DISH;Nl;0;L;;;;3;N;;;;;
+12409;CUNEIFORM NUMERIC SIGN FOUR DISH;Nl;0;L;;;;4;N;;;;;
+1240A;CUNEIFORM NUMERIC SIGN FIVE DISH;Nl;0;L;;;;5;N;;;;;
+1240B;CUNEIFORM NUMERIC SIGN SIX DISH;Nl;0;L;;;;6;N;;;;;
+1240C;CUNEIFORM NUMERIC SIGN SEVEN DISH;Nl;0;L;;;;7;N;;;;;
+1240D;CUNEIFORM NUMERIC SIGN EIGHT DISH;Nl;0;L;;;;8;N;;;;;
+1240E;CUNEIFORM NUMERIC SIGN NINE DISH;Nl;0;L;;;;9;N;;;;;
+1240F;CUNEIFORM NUMERIC SIGN FOUR U;Nl;0;L;;;;4;N;;;;;
+12410;CUNEIFORM NUMERIC SIGN FIVE U;Nl;0;L;;;;5;N;;;;;
+12411;CUNEIFORM NUMERIC SIGN SIX U;Nl;0;L;;;;6;N;;;;;
+12412;CUNEIFORM NUMERIC SIGN SEVEN U;Nl;0;L;;;;7;N;;;;;
+12413;CUNEIFORM NUMERIC SIGN EIGHT U;Nl;0;L;;;;8;N;;;;;
+12414;CUNEIFORM NUMERIC SIGN NINE U;Nl;0;L;;;;9;N;;;;;
+12415;CUNEIFORM NUMERIC SIGN ONE GESH2;Nl;0;L;;;;1;N;;;;;
+12416;CUNEIFORM NUMERIC SIGN TWO GESH2;Nl;0;L;;;;2;N;;;;;
+12417;CUNEIFORM NUMERIC SIGN THREE GESH2;Nl;0;L;;;;3;N;;;;;
+12418;CUNEIFORM NUMERIC SIGN FOUR GESH2;Nl;0;L;;;;4;N;;;;;
+12419;CUNEIFORM NUMERIC SIGN FIVE GESH2;Nl;0;L;;;;5;N;;;;;
+1241A;CUNEIFORM NUMERIC SIGN SIX GESH2;Nl;0;L;;;;6;N;;;;;
+1241B;CUNEIFORM NUMERIC SIGN SEVEN GESH2;Nl;0;L;;;;7;N;;;;;
+1241C;CUNEIFORM NUMERIC SIGN EIGHT GESH2;Nl;0;L;;;;8;N;;;;;
+1241D;CUNEIFORM NUMERIC SIGN NINE GESH2;Nl;0;L;;;;9;N;;;;;
+1241E;CUNEIFORM NUMERIC SIGN ONE GESHU;Nl;0;L;;;;1;N;;;;;
+1241F;CUNEIFORM NUMERIC SIGN TWO GESHU;Nl;0;L;;;;2;N;;;;;
+12420;CUNEIFORM NUMERIC SIGN THREE GESHU;Nl;0;L;;;;3;N;;;;;
+12421;CUNEIFORM NUMERIC SIGN FOUR GESHU;Nl;0;L;;;;4;N;;;;;
+12422;CUNEIFORM NUMERIC SIGN FIVE GESHU;Nl;0;L;;;;5;N;;;;;
+12423;CUNEIFORM NUMERIC SIGN TWO SHAR2;Nl;0;L;;;;2;N;;;;;
+12424;CUNEIFORM NUMERIC SIGN THREE SHAR2;Nl;0;L;;;;3;N;;;;;
+12425;CUNEIFORM NUMERIC SIGN THREE SHAR2 VARIANT FORM;Nl;0;L;;;;3;N;;;;;
+12426;CUNEIFORM NUMERIC SIGN FOUR SHAR2;Nl;0;L;;;;4;N;;;;;
+12427;CUNEIFORM NUMERIC SIGN FIVE SHAR2;Nl;0;L;;;;5;N;;;;;
+12428;CUNEIFORM NUMERIC SIGN SIX SHAR2;Nl;0;L;;;;6;N;;;;;
+12429;CUNEIFORM NUMERIC SIGN SEVEN SHAR2;Nl;0;L;;;;7;N;;;;;
+1242A;CUNEIFORM NUMERIC SIGN EIGHT SHAR2;Nl;0;L;;;;8;N;;;;;
+1242B;CUNEIFORM NUMERIC SIGN NINE SHAR2;Nl;0;L;;;;9;N;;;;;
+1242C;CUNEIFORM NUMERIC SIGN ONE SHARU;Nl;0;L;;;;1;N;;;;;
+1242D;CUNEIFORM NUMERIC SIGN TWO SHARU;Nl;0;L;;;;2;N;;;;;
+1242E;CUNEIFORM NUMERIC SIGN THREE SHARU;Nl;0;L;;;;3;N;;;;;
+1242F;CUNEIFORM NUMERIC SIGN THREE SHARU VARIANT FORM;Nl;0;L;;;;3;N;;;;;
+12430;CUNEIFORM NUMERIC SIGN FOUR SHARU;Nl;0;L;;;;4;N;;;;;
+12431;CUNEIFORM NUMERIC SIGN FIVE SHARU;Nl;0;L;;;;5;N;;;;;
+12432;CUNEIFORM NUMERIC SIGN SHAR2 TIMES GAL PLUS DISH;Nl;0;L;;;;216000;N;;;;;
+12433;CUNEIFORM NUMERIC SIGN SHAR2 TIMES GAL PLUS MIN;Nl;0;L;;;;432000;N;;;;;
+12434;CUNEIFORM NUMERIC SIGN ONE BURU;Nl;0;L;;;;1;N;;;;;
+12435;CUNEIFORM NUMERIC SIGN TWO BURU;Nl;0;L;;;;2;N;;;;;
+12436;CUNEIFORM NUMERIC SIGN THREE BURU;Nl;0;L;;;;3;N;;;;;
+12437;CUNEIFORM NUMERIC SIGN THREE BURU VARIANT FORM;Nl;0;L;;;;3;N;;;;;
+12438;CUNEIFORM NUMERIC SIGN FOUR BURU;Nl;0;L;;;;4;N;;;;;
+12439;CUNEIFORM NUMERIC SIGN FIVE BURU;Nl;0;L;;;;5;N;;;;;
+1243A;CUNEIFORM NUMERIC SIGN THREE VARIANT FORM ESH16;Nl;0;L;;;;3;N;;;;;
+1243B;CUNEIFORM NUMERIC SIGN THREE VARIANT FORM ESH21;Nl;0;L;;;;3;N;;;;;
+1243C;CUNEIFORM NUMERIC SIGN FOUR VARIANT FORM LIMMU;Nl;0;L;;;;4;N;;;;;
+1243D;CUNEIFORM NUMERIC SIGN FOUR VARIANT FORM LIMMU4;Nl;0;L;;;;4;N;;;;;
+1243E;CUNEIFORM NUMERIC SIGN FOUR VARIANT FORM LIMMU A;Nl;0;L;;;;4;N;;;;;
+1243F;CUNEIFORM NUMERIC SIGN FOUR VARIANT FORM LIMMU B;Nl;0;L;;;;4;N;;;;;
+12440;CUNEIFORM NUMERIC SIGN SIX VARIANT FORM ASH9;Nl;0;L;;;;6;N;;;;;
+12441;CUNEIFORM NUMERIC SIGN SEVEN VARIANT FORM IMIN3;Nl;0;L;;;;7;N;;;;;
+12442;CUNEIFORM NUMERIC SIGN SEVEN VARIANT FORM IMIN A;Nl;0;L;;;;7;N;;;;;
+12443;CUNEIFORM NUMERIC SIGN SEVEN VARIANT FORM IMIN B;Nl;0;L;;;;7;N;;;;;
+12444;CUNEIFORM NUMERIC SIGN EIGHT VARIANT FORM USSU;Nl;0;L;;;;8;N;;;;;
+12445;CUNEIFORM NUMERIC SIGN EIGHT VARIANT FORM USSU3;Nl;0;L;;;;8;N;;;;;
+12446;CUNEIFORM NUMERIC SIGN NINE VARIANT FORM ILIMMU;Nl;0;L;;;;9;N;;;;;
+12447;CUNEIFORM NUMERIC SIGN NINE VARIANT FORM ILIMMU3;Nl;0;L;;;;9;N;;;;;
+12448;CUNEIFORM NUMERIC SIGN NINE VARIANT FORM ILIMMU4;Nl;0;L;;;;9;N;;;;;
+12449;CUNEIFORM NUMERIC SIGN NINE VARIANT FORM ILIMMU A;Nl;0;L;;;;9;N;;;;;
+1244A;CUNEIFORM NUMERIC SIGN TWO ASH TENU;Nl;0;L;;;;2;N;;;;;
+1244B;CUNEIFORM NUMERIC SIGN THREE ASH TENU;Nl;0;L;;;;3;N;;;;;
+1244C;CUNEIFORM NUMERIC SIGN FOUR ASH TENU;Nl;0;L;;;;4;N;;;;;
+1244D;CUNEIFORM NUMERIC SIGN FIVE ASH TENU;Nl;0;L;;;;5;N;;;;;
+1244E;CUNEIFORM NUMERIC SIGN SIX ASH TENU;Nl;0;L;;;;6;N;;;;;
+1244F;CUNEIFORM NUMERIC SIGN ONE BAN2;Nl;0;L;;;;1;N;;;;;
+12450;CUNEIFORM NUMERIC SIGN TWO BAN2;Nl;0;L;;;;2;N;;;;;
+12451;CUNEIFORM NUMERIC SIGN THREE BAN2;Nl;0;L;;;;3;N;;;;;
+12452;CUNEIFORM NUMERIC SIGN FOUR BAN2;Nl;0;L;;;;4;N;;;;;
+12453;CUNEIFORM NUMERIC SIGN FOUR BAN2 VARIANT FORM;Nl;0;L;;;;4;N;;;;;
+12454;CUNEIFORM NUMERIC SIGN FIVE BAN2;Nl;0;L;;;;5;N;;;;;
+12455;CUNEIFORM NUMERIC SIGN FIVE BAN2 VARIANT FORM;Nl;0;L;;;;5;N;;;;;
+12456;CUNEIFORM NUMERIC SIGN NIGIDAMIN;Nl;0;L;;;;2;N;;;;;
+12457;CUNEIFORM NUMERIC SIGN NIGIDAESH;Nl;0;L;;;;3;N;;;;;
+12458;CUNEIFORM NUMERIC SIGN ONE ESHE3;Nl;0;L;;;;1;N;;;;;
+12459;CUNEIFORM NUMERIC SIGN TWO ESHE3;Nl;0;L;;;;2;N;;;;;
+1245A;CUNEIFORM NUMERIC SIGN ONE THIRD DISH;Nl;0;L;;;;1/3;N;;;;;
+1245B;CUNEIFORM NUMERIC SIGN TWO THIRDS DISH;Nl;0;L;;;;2/3;N;;;;;
+1245C;CUNEIFORM NUMERIC SIGN FIVE SIXTHS DISH;Nl;0;L;;;;5/6;N;;;;;
+1245D;CUNEIFORM NUMERIC SIGN ONE THIRD VARIANT FORM A;Nl;0;L;;;;1/3;N;;;;;
+1245E;CUNEIFORM NUMERIC SIGN TWO THIRDS VARIANT FORM A;Nl;0;L;;;;2/3;N;;;;;
+1245F;CUNEIFORM NUMERIC SIGN ONE EIGHTH ASH;Nl;0;L;;;;1/8;N;;;;;
+12460;CUNEIFORM NUMERIC SIGN ONE QUARTER ASH;Nl;0;L;;;;1/4;N;;;;;
+12461;CUNEIFORM NUMERIC SIGN OLD ASSYRIAN ONE SIXTH;Nl;0;L;;;;1/6;N;;;;;
+12462;CUNEIFORM NUMERIC SIGN OLD ASSYRIAN ONE QUARTER;Nl;0;L;;;;1/4;N;;;;;
+12463;CUNEIFORM NUMERIC SIGN ONE QUARTER GUR;Nl;0;L;;;;1/4;N;;;;;
+12464;CUNEIFORM NUMERIC SIGN ONE HALF GUR;Nl;0;L;;;;1/2;N;;;;;
+12465;CUNEIFORM NUMERIC SIGN ELAMITE ONE THIRD;Nl;0;L;;;;1/3;N;;;;;
+12466;CUNEIFORM NUMERIC SIGN ELAMITE TWO THIRDS;Nl;0;L;;;;2/3;N;;;;;
+12467;CUNEIFORM NUMERIC SIGN ELAMITE FORTY;Nl;0;L;;;;40;N;;;;;
+12468;CUNEIFORM NUMERIC SIGN ELAMITE FIFTY;Nl;0;L;;;;50;N;;;;;
+12469;CUNEIFORM NUMERIC SIGN FOUR U VARIANT FORM;Nl;0;L;;;;4;N;;;;;
+1246A;CUNEIFORM NUMERIC SIGN FIVE U VARIANT FORM;Nl;0;L;;;;5;N;;;;;
+1246B;CUNEIFORM NUMERIC SIGN SIX U VARIANT FORM;Nl;0;L;;;;6;N;;;;;
+1246C;CUNEIFORM NUMERIC SIGN SEVEN U VARIANT FORM;Nl;0;L;;;;7;N;;;;;
+1246D;CUNEIFORM NUMERIC SIGN EIGHT U VARIANT FORM;Nl;0;L;;;;8;N;;;;;
+1246E;CUNEIFORM NUMERIC SIGN NINE U VARIANT FORM;Nl;0;L;;;;9;N;;;;;
+12470;CUNEIFORM PUNCTUATION SIGN OLD ASSYRIAN WORD DIVIDER;Po;0;L;;;;;N;;;;;
+12471;CUNEIFORM PUNCTUATION SIGN VERTICAL COLON;Po;0;L;;;;;N;;;;;
+12472;CUNEIFORM PUNCTUATION SIGN DIAGONAL COLON;Po;0;L;;;;;N;;;;;
+12473;CUNEIFORM PUNCTUATION SIGN DIAGONAL TRICOLON;Po;0;L;;;;;N;;;;;
+12474;CUNEIFORM PUNCTUATION SIGN DIAGONAL QUADCOLON;Po;0;L;;;;;N;;;;;
+12480;CUNEIFORM SIGN AB TIMES NUN TENU;Lo;0;L;;;;;N;;;;;
+12481;CUNEIFORM SIGN AB TIMES SHU2;Lo;0;L;;;;;N;;;;;
+12482;CUNEIFORM SIGN AD TIMES ESH2;Lo;0;L;;;;;N;;;;;
+12483;CUNEIFORM SIGN BAD TIMES DISH TENU;Lo;0;L;;;;;N;;;;;
+12484;CUNEIFORM SIGN BAHAR2 TIMES AB2;Lo;0;L;;;;;N;;;;;
+12485;CUNEIFORM SIGN BAHAR2 TIMES NI;Lo;0;L;;;;;N;;;;;
+12486;CUNEIFORM SIGN BAHAR2 TIMES ZA;Lo;0;L;;;;;N;;;;;
+12487;CUNEIFORM SIGN BU OVER BU TIMES NA2;Lo;0;L;;;;;N;;;;;
+12488;CUNEIFORM SIGN DA TIMES TAK4;Lo;0;L;;;;;N;;;;;
+12489;CUNEIFORM SIGN DAG TIMES KUR;Lo;0;L;;;;;N;;;;;
+1248A;CUNEIFORM SIGN DIM TIMES IGI;Lo;0;L;;;;;N;;;;;
+1248B;CUNEIFORM SIGN DIM TIMES U U U;Lo;0;L;;;;;N;;;;;
+1248C;CUNEIFORM SIGN DIM2 TIMES UD;Lo;0;L;;;;;N;;;;;
+1248D;CUNEIFORM SIGN DUG TIMES ANSHE;Lo;0;L;;;;;N;;;;;
+1248E;CUNEIFORM SIGN DUG TIMES ASH;Lo;0;L;;;;;N;;;;;
+1248F;CUNEIFORM SIGN DUG TIMES ASH AT LEFT;Lo;0;L;;;;;N;;;;;
+12490;CUNEIFORM SIGN DUG TIMES DIN;Lo;0;L;;;;;N;;;;;
+12491;CUNEIFORM SIGN DUG TIMES DUN;Lo;0;L;;;;;N;;;;;
+12492;CUNEIFORM SIGN DUG TIMES ERIN2;Lo;0;L;;;;;N;;;;;
+12493;CUNEIFORM SIGN DUG TIMES GA;Lo;0;L;;;;;N;;;;;
+12494;CUNEIFORM SIGN DUG TIMES GI;Lo;0;L;;;;;N;;;;;
+12495;CUNEIFORM SIGN DUG TIMES GIR2 GUNU;Lo;0;L;;;;;N;;;;;
+12496;CUNEIFORM SIGN DUG TIMES GISH;Lo;0;L;;;;;N;;;;;
+12497;CUNEIFORM SIGN DUG TIMES HA;Lo;0;L;;;;;N;;;;;
+12498;CUNEIFORM SIGN DUG TIMES HI;Lo;0;L;;;;;N;;;;;
+12499;CUNEIFORM SIGN DUG TIMES IGI GUNU;Lo;0;L;;;;;N;;;;;
+1249A;CUNEIFORM SIGN DUG TIMES KASKAL;Lo;0;L;;;;;N;;;;;
+1249B;CUNEIFORM SIGN DUG TIMES KUR;Lo;0;L;;;;;N;;;;;
+1249C;CUNEIFORM SIGN DUG TIMES KUSHU2;Lo;0;L;;;;;N;;;;;
+1249D;CUNEIFORM SIGN DUG TIMES KUSHU2 PLUS KASKAL;Lo;0;L;;;;;N;;;;;
+1249E;CUNEIFORM SIGN DUG TIMES LAK-020;Lo;0;L;;;;;N;;;;;
+1249F;CUNEIFORM SIGN DUG TIMES LAM;Lo;0;L;;;;;N;;;;;
+124A0;CUNEIFORM SIGN DUG TIMES LAM TIMES KUR;Lo;0;L;;;;;N;;;;;
+124A1;CUNEIFORM SIGN DUG TIMES LUH PLUS GISH;Lo;0;L;;;;;N;;;;;
+124A2;CUNEIFORM SIGN DUG TIMES MASH;Lo;0;L;;;;;N;;;;;
+124A3;CUNEIFORM SIGN DUG TIMES MES;Lo;0;L;;;;;N;;;;;
+124A4;CUNEIFORM SIGN DUG TIMES MI;Lo;0;L;;;;;N;;;;;
+124A5;CUNEIFORM SIGN DUG TIMES NI;Lo;0;L;;;;;N;;;;;
+124A6;CUNEIFORM SIGN DUG TIMES PI;Lo;0;L;;;;;N;;;;;
+124A7;CUNEIFORM SIGN DUG TIMES SHE;Lo;0;L;;;;;N;;;;;
+124A8;CUNEIFORM SIGN DUG TIMES SI GUNU;Lo;0;L;;;;;N;;;;;
+124A9;CUNEIFORM SIGN E2 TIMES KUR;Lo;0;L;;;;;N;;;;;
+124AA;CUNEIFORM SIGN E2 TIMES PAP;Lo;0;L;;;;;N;;;;;
+124AB;CUNEIFORM SIGN ERIN2 X;Lo;0;L;;;;;N;;;;;
+124AC;CUNEIFORM SIGN ESH2 CROSSING ESH2;Lo;0;L;;;;;N;;;;;
+124AD;CUNEIFORM SIGN EZEN SHESHIG TIMES ASH;Lo;0;L;;;;;N;;;;;
+124AE;CUNEIFORM SIGN EZEN SHESHIG TIMES HI;Lo;0;L;;;;;N;;;;;
+124AF;CUNEIFORM SIGN EZEN SHESHIG TIMES IGI GUNU;Lo;0;L;;;;;N;;;;;
+124B0;CUNEIFORM SIGN EZEN SHESHIG TIMES LA;Lo;0;L;;;;;N;;;;;
+124B1;CUNEIFORM SIGN EZEN SHESHIG TIMES LAL;Lo;0;L;;;;;N;;;;;
+124B2;CUNEIFORM SIGN EZEN SHESHIG TIMES ME;Lo;0;L;;;;;N;;;;;
+124B3;CUNEIFORM SIGN EZEN SHESHIG TIMES MES;Lo;0;L;;;;;N;;;;;
+124B4;CUNEIFORM SIGN EZEN SHESHIG TIMES SU;Lo;0;L;;;;;N;;;;;
+124B5;CUNEIFORM SIGN EZEN TIMES SU;Lo;0;L;;;;;N;;;;;
+124B6;CUNEIFORM SIGN GA2 TIMES BAHAR2;Lo;0;L;;;;;N;;;;;
+124B7;CUNEIFORM SIGN GA2 TIMES DIM GUNU;Lo;0;L;;;;;N;;;;;
+124B8;CUNEIFORM SIGN GA2 TIMES DUG TIMES IGI GUNU;Lo;0;L;;;;;N;;;;;
+124B9;CUNEIFORM SIGN GA2 TIMES DUG TIMES KASKAL;Lo;0;L;;;;;N;;;;;
+124BA;CUNEIFORM SIGN GA2 TIMES EREN;Lo;0;L;;;;;N;;;;;
+124BB;CUNEIFORM SIGN GA2 TIMES GA;Lo;0;L;;;;;N;;;;;
+124BC;CUNEIFORM SIGN GA2 TIMES GAR PLUS DI;Lo;0;L;;;;;N;;;;;
+124BD;CUNEIFORM SIGN GA2 TIMES GAR PLUS NE;Lo;0;L;;;;;N;;;;;
+124BE;CUNEIFORM SIGN GA2 TIMES HA PLUS A;Lo;0;L;;;;;N;;;;;
+124BF;CUNEIFORM SIGN GA2 TIMES KUSHU2 PLUS KASKAL;Lo;0;L;;;;;N;;;;;
+124C0;CUNEIFORM SIGN GA2 TIMES LAM;Lo;0;L;;;;;N;;;;;
+124C1;CUNEIFORM SIGN GA2 TIMES LAM TIMES KUR;Lo;0;L;;;;;N;;;;;
+124C2;CUNEIFORM SIGN GA2 TIMES LUH;Lo;0;L;;;;;N;;;;;
+124C3;CUNEIFORM SIGN GA2 TIMES MUSH;Lo;0;L;;;;;N;;;;;
+124C4;CUNEIFORM SIGN GA2 TIMES NE;Lo;0;L;;;;;N;;;;;
+124C5;CUNEIFORM SIGN GA2 TIMES NE PLUS E2;Lo;0;L;;;;;N;;;;;
+124C6;CUNEIFORM SIGN GA2 TIMES NE PLUS GI;Lo;0;L;;;;;N;;;;;
+124C7;CUNEIFORM SIGN GA2 TIMES SHIM;Lo;0;L;;;;;N;;;;;
+124C8;CUNEIFORM SIGN GA2 TIMES ZIZ2;Lo;0;L;;;;;N;;;;;
+124C9;CUNEIFORM SIGN GABA ROTATED NINETY DEGREES;Lo;0;L;;;;;N;;;;;
+124CA;CUNEIFORM SIGN GESHTIN TIMES U;Lo;0;L;;;;;N;;;;;
+124CB;CUNEIFORM SIGN GISH TIMES GISH CROSSING GISH;Lo;0;L;;;;;N;;;;;
+124CC;CUNEIFORM SIGN GU2 TIMES IGI GUNU;Lo;0;L;;;;;N;;;;;
+124CD;CUNEIFORM SIGN GUD PLUS GISH TIMES TAK4;Lo;0;L;;;;;N;;;;;
+124CE;CUNEIFORM SIGN HA TENU GUNU;Lo;0;L;;;;;N;;;;;
+124CF;CUNEIFORM SIGN HI TIMES ASH OVER HI TIMES ASH;Lo;0;L;;;;;N;;;;;
+124D0;CUNEIFORM SIGN KA TIMES BU;Lo;0;L;;;;;N;;;;;
+124D1;CUNEIFORM SIGN KA TIMES KA;Lo;0;L;;;;;N;;;;;
+124D2;CUNEIFORM SIGN KA TIMES U U U;Lo;0;L;;;;;N;;;;;
+124D3;CUNEIFORM SIGN KA TIMES UR;Lo;0;L;;;;;N;;;;;
+124D4;CUNEIFORM SIGN LAGAB TIMES ZU OVER ZU;Lo;0;L;;;;;N;;;;;
+124D5;CUNEIFORM SIGN LAK-003;Lo;0;L;;;;;N;;;;;
+124D6;CUNEIFORM SIGN LAK-021;Lo;0;L;;;;;N;;;;;
+124D7;CUNEIFORM SIGN LAK-025;Lo;0;L;;;;;N;;;;;
+124D8;CUNEIFORM SIGN LAK-030;Lo;0;L;;;;;N;;;;;
+124D9;CUNEIFORM SIGN LAK-050;Lo;0;L;;;;;N;;;;;
+124DA;CUNEIFORM SIGN LAK-051;Lo;0;L;;;;;N;;;;;
+124DB;CUNEIFORM SIGN LAK-062;Lo;0;L;;;;;N;;;;;
+124DC;CUNEIFORM SIGN LAK-079 OVER LAK-079 GUNU;Lo;0;L;;;;;N;;;;;
+124DD;CUNEIFORM SIGN LAK-080;Lo;0;L;;;;;N;;;;;
+124DE;CUNEIFORM SIGN LAK-081 OVER LAK-081;Lo;0;L;;;;;N;;;;;
+124DF;CUNEIFORM SIGN LAK-092;Lo;0;L;;;;;N;;;;;
+124E0;CUNEIFORM SIGN LAK-130;Lo;0;L;;;;;N;;;;;
+124E1;CUNEIFORM SIGN LAK-142;Lo;0;L;;;;;N;;;;;
+124E2;CUNEIFORM SIGN LAK-210;Lo;0;L;;;;;N;;;;;
+124E3;CUNEIFORM SIGN LAK-219;Lo;0;L;;;;;N;;;;;
+124E4;CUNEIFORM SIGN LAK-220;Lo;0;L;;;;;N;;;;;
+124E5;CUNEIFORM SIGN LAK-225;Lo;0;L;;;;;N;;;;;
+124E6;CUNEIFORM SIGN LAK-228;Lo;0;L;;;;;N;;;;;
+124E7;CUNEIFORM SIGN LAK-238;Lo;0;L;;;;;N;;;;;
+124E8;CUNEIFORM SIGN LAK-265;Lo;0;L;;;;;N;;;;;
+124E9;CUNEIFORM SIGN LAK-266;Lo;0;L;;;;;N;;;;;
+124EA;CUNEIFORM SIGN LAK-343;Lo;0;L;;;;;N;;;;;
+124EB;CUNEIFORM SIGN LAK-347;Lo;0;L;;;;;N;;;;;
+124EC;CUNEIFORM SIGN LAK-348;Lo;0;L;;;;;N;;;;;
+124ED;CUNEIFORM SIGN LAK-383;Lo;0;L;;;;;N;;;;;
+124EE;CUNEIFORM SIGN LAK-384;Lo;0;L;;;;;N;;;;;
+124EF;CUNEIFORM SIGN LAK-390;Lo;0;L;;;;;N;;;;;
+124F0;CUNEIFORM SIGN LAK-441;Lo;0;L;;;;;N;;;;;
+124F1;CUNEIFORM SIGN LAK-449;Lo;0;L;;;;;N;;;;;
+124F2;CUNEIFORM SIGN LAK-449 TIMES GU;Lo;0;L;;;;;N;;;;;
+124F3;CUNEIFORM SIGN LAK-449 TIMES IGI;Lo;0;L;;;;;N;;;;;
+124F4;CUNEIFORM SIGN LAK-449 TIMES PAP PLUS LU3;Lo;0;L;;;;;N;;;;;
+124F5;CUNEIFORM SIGN LAK-449 TIMES PAP PLUS PAP PLUS LU3;Lo;0;L;;;;;N;;;;;
+124F6;CUNEIFORM SIGN LAK-449 TIMES U2 PLUS BA;Lo;0;L;;;;;N;;;;;
+124F7;CUNEIFORM SIGN LAK-450;Lo;0;L;;;;;N;;;;;
+124F8;CUNEIFORM SIGN LAK-457;Lo;0;L;;;;;N;;;;;
+124F9;CUNEIFORM SIGN LAK-470;Lo;0;L;;;;;N;;;;;
+124FA;CUNEIFORM SIGN LAK-483;Lo;0;L;;;;;N;;;;;
+124FB;CUNEIFORM SIGN LAK-490;Lo;0;L;;;;;N;;;;;
+124FC;CUNEIFORM SIGN LAK-492;Lo;0;L;;;;;N;;;;;
+124FD;CUNEIFORM SIGN LAK-493;Lo;0;L;;;;;N;;;;;
+124FE;CUNEIFORM SIGN LAK-495;Lo;0;L;;;;;N;;;;;
+124FF;CUNEIFORM SIGN LAK-550;Lo;0;L;;;;;N;;;;;
+12500;CUNEIFORM SIGN LAK-608;Lo;0;L;;;;;N;;;;;
+12501;CUNEIFORM SIGN LAK-617;Lo;0;L;;;;;N;;;;;
+12502;CUNEIFORM SIGN LAK-617 TIMES ASH;Lo;0;L;;;;;N;;;;;
+12503;CUNEIFORM SIGN LAK-617 TIMES BAD;Lo;0;L;;;;;N;;;;;
+12504;CUNEIFORM SIGN LAK-617 TIMES DUN3 GUNU GUNU;Lo;0;L;;;;;N;;;;;
+12505;CUNEIFORM SIGN LAK-617 TIMES KU3;Lo;0;L;;;;;N;;;;;
+12506;CUNEIFORM SIGN LAK-617 TIMES LA;Lo;0;L;;;;;N;;;;;
+12507;CUNEIFORM SIGN LAK-617 TIMES TAR;Lo;0;L;;;;;N;;;;;
+12508;CUNEIFORM SIGN LAK-617 TIMES TE;Lo;0;L;;;;;N;;;;;
+12509;CUNEIFORM SIGN LAK-617 TIMES U2;Lo;0;L;;;;;N;;;;;
+1250A;CUNEIFORM SIGN LAK-617 TIMES UD;Lo;0;L;;;;;N;;;;;
+1250B;CUNEIFORM SIGN LAK-617 TIMES URUDA;Lo;0;L;;;;;N;;;;;
+1250C;CUNEIFORM SIGN LAK-636;Lo;0;L;;;;;N;;;;;
+1250D;CUNEIFORM SIGN LAK-648;Lo;0;L;;;;;N;;;;;
+1250E;CUNEIFORM SIGN LAK-648 TIMES DUB;Lo;0;L;;;;;N;;;;;
+1250F;CUNEIFORM SIGN LAK-648 TIMES GA;Lo;0;L;;;;;N;;;;;
+12510;CUNEIFORM SIGN LAK-648 TIMES IGI;Lo;0;L;;;;;N;;;;;
+12511;CUNEIFORM SIGN LAK-648 TIMES IGI GUNU;Lo;0;L;;;;;N;;;;;
+12512;CUNEIFORM SIGN LAK-648 TIMES NI;Lo;0;L;;;;;N;;;;;
+12513;CUNEIFORM SIGN LAK-648 TIMES PAP PLUS PAP PLUS LU3;Lo;0;L;;;;;N;;;;;
+12514;CUNEIFORM SIGN LAK-648 TIMES SHESH PLUS KI;Lo;0;L;;;;;N;;;;;
+12515;CUNEIFORM SIGN LAK-648 TIMES UD;Lo;0;L;;;;;N;;;;;
+12516;CUNEIFORM SIGN LAK-648 TIMES URUDA;Lo;0;L;;;;;N;;;;;
+12517;CUNEIFORM SIGN LAK-724;Lo;0;L;;;;;N;;;;;
+12518;CUNEIFORM SIGN LAK-749;Lo;0;L;;;;;N;;;;;
+12519;CUNEIFORM SIGN LU2 GUNU TIMES ASH;Lo;0;L;;;;;N;;;;;
+1251A;CUNEIFORM SIGN LU2 TIMES DISH;Lo;0;L;;;;;N;;;;;
+1251B;CUNEIFORM SIGN LU2 TIMES HAL;Lo;0;L;;;;;N;;;;;
+1251C;CUNEIFORM SIGN LU2 TIMES PAP;Lo;0;L;;;;;N;;;;;
+1251D;CUNEIFORM SIGN LU2 TIMES PAP PLUS PAP PLUS LU3;Lo;0;L;;;;;N;;;;;
+1251E;CUNEIFORM SIGN LU2 TIMES TAK4;Lo;0;L;;;;;N;;;;;
+1251F;CUNEIFORM SIGN MI PLUS ZA7;Lo;0;L;;;;;N;;;;;
+12520;CUNEIFORM SIGN MUSH OVER MUSH TIMES GA;Lo;0;L;;;;;N;;;;;
+12521;CUNEIFORM SIGN MUSH OVER MUSH TIMES KAK;Lo;0;L;;;;;N;;;;;
+12522;CUNEIFORM SIGN NINDA2 TIMES DIM GUNU;Lo;0;L;;;;;N;;;;;
+12523;CUNEIFORM SIGN NINDA2 TIMES GISH;Lo;0;L;;;;;N;;;;;
+12524;CUNEIFORM SIGN NINDA2 TIMES GUL;Lo;0;L;;;;;N;;;;;
+12525;CUNEIFORM SIGN NINDA2 TIMES HI;Lo;0;L;;;;;N;;;;;
+12526;CUNEIFORM SIGN NINDA2 TIMES KESH2;Lo;0;L;;;;;N;;;;;
+12527;CUNEIFORM SIGN NINDA2 TIMES LAK-050;Lo;0;L;;;;;N;;;;;
+12528;CUNEIFORM SIGN NINDA2 TIMES MASH;Lo;0;L;;;;;N;;;;;
+12529;CUNEIFORM SIGN NINDA2 TIMES PAP PLUS PAP;Lo;0;L;;;;;N;;;;;
+1252A;CUNEIFORM SIGN NINDA2 TIMES U;Lo;0;L;;;;;N;;;;;
+1252B;CUNEIFORM SIGN NINDA2 TIMES U PLUS U;Lo;0;L;;;;;N;;;;;
+1252C;CUNEIFORM SIGN NINDA2 TIMES URUDA;Lo;0;L;;;;;N;;;;;
+1252D;CUNEIFORM SIGN SAG GUNU TIMES HA;Lo;0;L;;;;;N;;;;;
+1252E;CUNEIFORM SIGN SAG TIMES EN;Lo;0;L;;;;;N;;;;;
+1252F;CUNEIFORM SIGN SAG TIMES SHE AT LEFT;Lo;0;L;;;;;N;;;;;
+12530;CUNEIFORM SIGN SAG TIMES TAK4;Lo;0;L;;;;;N;;;;;
+12531;CUNEIFORM SIGN SHA6 TENU;Lo;0;L;;;;;N;;;;;
+12532;CUNEIFORM SIGN SHE OVER SHE;Lo;0;L;;;;;N;;;;;
+12533;CUNEIFORM SIGN SHE PLUS HUB2;Lo;0;L;;;;;N;;;;;
+12534;CUNEIFORM SIGN SHE PLUS NAM2;Lo;0;L;;;;;N;;;;;
+12535;CUNEIFORM SIGN SHE PLUS SAR;Lo;0;L;;;;;N;;;;;
+12536;CUNEIFORM SIGN SHU2 PLUS DUG TIMES NI;Lo;0;L;;;;;N;;;;;
+12537;CUNEIFORM SIGN SHU2 PLUS E2 TIMES AN;Lo;0;L;;;;;N;;;;;
+12538;CUNEIFORM SIGN SI TIMES TAK4;Lo;0;L;;;;;N;;;;;
+12539;CUNEIFORM SIGN TAK4 PLUS SAG;Lo;0;L;;;;;N;;;;;
+1253A;CUNEIFORM SIGN TUM TIMES GAN2 TENU;Lo;0;L;;;;;N;;;;;
+1253B;CUNEIFORM SIGN TUM TIMES THREE DISH;Lo;0;L;;;;;N;;;;;
+1253C;CUNEIFORM SIGN UR2 INVERTED;Lo;0;L;;;;;N;;;;;
+1253D;CUNEIFORM SIGN UR2 TIMES UD;Lo;0;L;;;;;N;;;;;
+1253E;CUNEIFORM SIGN URU TIMES DARA3;Lo;0;L;;;;;N;;;;;
+1253F;CUNEIFORM SIGN URU TIMES LAK-668;Lo;0;L;;;;;N;;;;;
+12540;CUNEIFORM SIGN URU TIMES LU3;Lo;0;L;;;;;N;;;;;
+12541;CUNEIFORM SIGN ZA7;Lo;0;L;;;;;N;;;;;
+12542;CUNEIFORM SIGN ZU OVER ZU PLUS SAR;Lo;0;L;;;;;N;;;;;
+12543;CUNEIFORM SIGN ZU5 TIMES THREE DISH TENU;Lo;0;L;;;;;N;;;;;
+13000;EGYPTIAN HIEROGLYPH A001;Lo;0;L;;;;;N;;;;;
+13001;EGYPTIAN HIEROGLYPH A002;Lo;0;L;;;;;N;;;;;
+13002;EGYPTIAN HIEROGLYPH A003;Lo;0;L;;;;;N;;;;;
+13003;EGYPTIAN HIEROGLYPH A004;Lo;0;L;;;;;N;;;;;
+13004;EGYPTIAN HIEROGLYPH A005;Lo;0;L;;;;;N;;;;;
+13005;EGYPTIAN HIEROGLYPH A005A;Lo;0;L;;;;;N;;;;;
+13006;EGYPTIAN HIEROGLYPH A006;Lo;0;L;;;;;N;;;;;
+13007;EGYPTIAN HIEROGLYPH A006A;Lo;0;L;;;;;N;;;;;
+13008;EGYPTIAN HIEROGLYPH A006B;Lo;0;L;;;;;N;;;;;
+13009;EGYPTIAN HIEROGLYPH A007;Lo;0;L;;;;;N;;;;;
+1300A;EGYPTIAN HIEROGLYPH A008;Lo;0;L;;;;;N;;;;;
+1300B;EGYPTIAN HIEROGLYPH A009;Lo;0;L;;;;;N;;;;;
+1300C;EGYPTIAN HIEROGLYPH A010;Lo;0;L;;;;;N;;;;;
+1300D;EGYPTIAN HIEROGLYPH A011;Lo;0;L;;;;;N;;;;;
+1300E;EGYPTIAN HIEROGLYPH A012;Lo;0;L;;;;;N;;;;;
+1300F;EGYPTIAN HIEROGLYPH A013;Lo;0;L;;;;;N;;;;;
+13010;EGYPTIAN HIEROGLYPH A014;Lo;0;L;;;;;N;;;;;
+13011;EGYPTIAN HIEROGLYPH A014A;Lo;0;L;;;;;N;;;;;
+13012;EGYPTIAN HIEROGLYPH A015;Lo;0;L;;;;;N;;;;;
+13013;EGYPTIAN HIEROGLYPH A016;Lo;0;L;;;;;N;;;;;
+13014;EGYPTIAN HIEROGLYPH A017;Lo;0;L;;;;;N;;;;;
+13015;EGYPTIAN HIEROGLYPH A017A;Lo;0;L;;;;;N;;;;;
+13016;EGYPTIAN HIEROGLYPH A018;Lo;0;L;;;;;N;;;;;
+13017;EGYPTIAN HIEROGLYPH A019;Lo;0;L;;;;;N;;;;;
+13018;EGYPTIAN HIEROGLYPH A020;Lo;0;L;;;;;N;;;;;
+13019;EGYPTIAN HIEROGLYPH A021;Lo;0;L;;;;;N;;;;;
+1301A;EGYPTIAN HIEROGLYPH A022;Lo;0;L;;;;;N;;;;;
+1301B;EGYPTIAN HIEROGLYPH A023;Lo;0;L;;;;;N;;;;;
+1301C;EGYPTIAN HIEROGLYPH A024;Lo;0;L;;;;;N;;;;;
+1301D;EGYPTIAN HIEROGLYPH A025;Lo;0;L;;;;;N;;;;;
+1301E;EGYPTIAN HIEROGLYPH A026;Lo;0;L;;;;;N;;;;;
+1301F;EGYPTIAN HIEROGLYPH A027;Lo;0;L;;;;;N;;;;;
+13020;EGYPTIAN HIEROGLYPH A028;Lo;0;L;;;;;N;;;;;
+13021;EGYPTIAN HIEROGLYPH A029;Lo;0;L;;;;;N;;;;;
+13022;EGYPTIAN HIEROGLYPH A030;Lo;0;L;;;;;N;;;;;
+13023;EGYPTIAN HIEROGLYPH A031;Lo;0;L;;;;;N;;;;;
+13024;EGYPTIAN HIEROGLYPH A032;Lo;0;L;;;;;N;;;;;
+13025;EGYPTIAN HIEROGLYPH A032A;Lo;0;L;;;;;N;;;;;
+13026;EGYPTIAN HIEROGLYPH A033;Lo;0;L;;;;;N;;;;;
+13027;EGYPTIAN HIEROGLYPH A034;Lo;0;L;;;;;N;;;;;
+13028;EGYPTIAN HIEROGLYPH A035;Lo;0;L;;;;;N;;;;;
+13029;EGYPTIAN HIEROGLYPH A036;Lo;0;L;;;;;N;;;;;
+1302A;EGYPTIAN HIEROGLYPH A037;Lo;0;L;;;;;N;;;;;
+1302B;EGYPTIAN HIEROGLYPH A038;Lo;0;L;;;;;N;;;;;
+1302C;EGYPTIAN HIEROGLYPH A039;Lo;0;L;;;;;N;;;;;
+1302D;EGYPTIAN HIEROGLYPH A040;Lo;0;L;;;;;N;;;;;
+1302E;EGYPTIAN HIEROGLYPH A040A;Lo;0;L;;;;;N;;;;;
+1302F;EGYPTIAN HIEROGLYPH A041;Lo;0;L;;;;;N;;;;;
+13030;EGYPTIAN HIEROGLYPH A042;Lo;0;L;;;;;N;;;;;
+13031;EGYPTIAN HIEROGLYPH A042A;Lo;0;L;;;;;N;;;;;
+13032;EGYPTIAN HIEROGLYPH A043;Lo;0;L;;;;;N;;;;;
+13033;EGYPTIAN HIEROGLYPH A043A;Lo;0;L;;;;;N;;;;;
+13034;EGYPTIAN HIEROGLYPH A044;Lo;0;L;;;;;N;;;;;
+13035;EGYPTIAN HIEROGLYPH A045;Lo;0;L;;;;;N;;;;;
+13036;EGYPTIAN HIEROGLYPH A045A;Lo;0;L;;;;;N;;;;;
+13037;EGYPTIAN HIEROGLYPH A046;Lo;0;L;;;;;N;;;;;
+13038;EGYPTIAN HIEROGLYPH A047;Lo;0;L;;;;;N;;;;;
+13039;EGYPTIAN HIEROGLYPH A048;Lo;0;L;;;;;N;;;;;
+1303A;EGYPTIAN HIEROGLYPH A049;Lo;0;L;;;;;N;;;;;
+1303B;EGYPTIAN HIEROGLYPH A050;Lo;0;L;;;;;N;;;;;
+1303C;EGYPTIAN HIEROGLYPH A051;Lo;0;L;;;;;N;;;;;
+1303D;EGYPTIAN HIEROGLYPH A052;Lo;0;L;;;;;N;;;;;
+1303E;EGYPTIAN HIEROGLYPH A053;Lo;0;L;;;;;N;;;;;
+1303F;EGYPTIAN HIEROGLYPH A054;Lo;0;L;;;;;N;;;;;
+13040;EGYPTIAN HIEROGLYPH A055;Lo;0;L;;;;;N;;;;;
+13041;EGYPTIAN HIEROGLYPH A056;Lo;0;L;;;;;N;;;;;
+13042;EGYPTIAN HIEROGLYPH A057;Lo;0;L;;;;;N;;;;;
+13043;EGYPTIAN HIEROGLYPH A058;Lo;0;L;;;;;N;;;;;
+13044;EGYPTIAN HIEROGLYPH A059;Lo;0;L;;;;;N;;;;;
+13045;EGYPTIAN HIEROGLYPH A060;Lo;0;L;;;;;N;;;;;
+13046;EGYPTIAN HIEROGLYPH A061;Lo;0;L;;;;;N;;;;;
+13047;EGYPTIAN HIEROGLYPH A062;Lo;0;L;;;;;N;;;;;
+13048;EGYPTIAN HIEROGLYPH A063;Lo;0;L;;;;;N;;;;;
+13049;EGYPTIAN HIEROGLYPH A064;Lo;0;L;;;;;N;;;;;
+1304A;EGYPTIAN HIEROGLYPH A065;Lo;0;L;;;;;N;;;;;
+1304B;EGYPTIAN HIEROGLYPH A066;Lo;0;L;;;;;N;;;;;
+1304C;EGYPTIAN HIEROGLYPH A067;Lo;0;L;;;;;N;;;;;
+1304D;EGYPTIAN HIEROGLYPH A068;Lo;0;L;;;;;N;;;;;
+1304E;EGYPTIAN HIEROGLYPH A069;Lo;0;L;;;;;N;;;;;
+1304F;EGYPTIAN HIEROGLYPH A070;Lo;0;L;;;;;N;;;;;
+13050;EGYPTIAN HIEROGLYPH B001;Lo;0;L;;;;;N;;;;;
+13051;EGYPTIAN HIEROGLYPH B002;Lo;0;L;;;;;N;;;;;
+13052;EGYPTIAN HIEROGLYPH B003;Lo;0;L;;;;;N;;;;;
+13053;EGYPTIAN HIEROGLYPH B004;Lo;0;L;;;;;N;;;;;
+13054;EGYPTIAN HIEROGLYPH B005;Lo;0;L;;;;;N;;;;;
+13055;EGYPTIAN HIEROGLYPH B005A;Lo;0;L;;;;;N;;;;;
+13056;EGYPTIAN HIEROGLYPH B006;Lo;0;L;;;;;N;;;;;
+13057;EGYPTIAN HIEROGLYPH B007;Lo;0;L;;;;;N;;;;;
+13058;EGYPTIAN HIEROGLYPH B008;Lo;0;L;;;;;N;;;;;
+13059;EGYPTIAN HIEROGLYPH B009;Lo;0;L;;;;;N;;;;;
+1305A;EGYPTIAN HIEROGLYPH C001;Lo;0;L;;;;;N;;;;;
+1305B;EGYPTIAN HIEROGLYPH C002;Lo;0;L;;;;;N;;;;;
+1305C;EGYPTIAN HIEROGLYPH C002A;Lo;0;L;;;;;N;;;;;
+1305D;EGYPTIAN HIEROGLYPH C002B;Lo;0;L;;;;;N;;;;;
+1305E;EGYPTIAN HIEROGLYPH C002C;Lo;0;L;;;;;N;;;;;
+1305F;EGYPTIAN HIEROGLYPH C003;Lo;0;L;;;;;N;;;;;
+13060;EGYPTIAN HIEROGLYPH C004;Lo;0;L;;;;;N;;;;;
+13061;EGYPTIAN HIEROGLYPH C005;Lo;0;L;;;;;N;;;;;
+13062;EGYPTIAN HIEROGLYPH C006;Lo;0;L;;;;;N;;;;;
+13063;EGYPTIAN HIEROGLYPH C007;Lo;0;L;;;;;N;;;;;
+13064;EGYPTIAN HIEROGLYPH C008;Lo;0;L;;;;;N;;;;;
+13065;EGYPTIAN HIEROGLYPH C009;Lo;0;L;;;;;N;;;;;
+13066;EGYPTIAN HIEROGLYPH C010;Lo;0;L;;;;;N;;;;;
+13067;EGYPTIAN HIEROGLYPH C010A;Lo;0;L;;;;;N;;;;;
+13068;EGYPTIAN HIEROGLYPH C011;Lo;0;L;;;;;N;;;;;
+13069;EGYPTIAN HIEROGLYPH C012;Lo;0;L;;;;;N;;;;;
+1306A;EGYPTIAN HIEROGLYPH C013;Lo;0;L;;;;;N;;;;;
+1306B;EGYPTIAN HIEROGLYPH C014;Lo;0;L;;;;;N;;;;;
+1306C;EGYPTIAN HIEROGLYPH C015;Lo;0;L;;;;;N;;;;;
+1306D;EGYPTIAN HIEROGLYPH C016;Lo;0;L;;;;;N;;;;;
+1306E;EGYPTIAN HIEROGLYPH C017;Lo;0;L;;;;;N;;;;;
+1306F;EGYPTIAN HIEROGLYPH C018;Lo;0;L;;;;;N;;;;;
+13070;EGYPTIAN HIEROGLYPH C019;Lo;0;L;;;;;N;;;;;
+13071;EGYPTIAN HIEROGLYPH C020;Lo;0;L;;;;;N;;;;;
+13072;EGYPTIAN HIEROGLYPH C021;Lo;0;L;;;;;N;;;;;
+13073;EGYPTIAN HIEROGLYPH C022;Lo;0;L;;;;;N;;;;;
+13074;EGYPTIAN HIEROGLYPH C023;Lo;0;L;;;;;N;;;;;
+13075;EGYPTIAN HIEROGLYPH C024;Lo;0;L;;;;;N;;;;;
+13076;EGYPTIAN HIEROGLYPH D001;Lo;0;L;;;;;N;;;;;
+13077;EGYPTIAN HIEROGLYPH D002;Lo;0;L;;;;;N;;;;;
+13078;EGYPTIAN HIEROGLYPH D003;Lo;0;L;;;;;N;;;;;
+13079;EGYPTIAN HIEROGLYPH D004;Lo;0;L;;;;;N;;;;;
+1307A;EGYPTIAN HIEROGLYPH D005;Lo;0;L;;;;;N;;;;;
+1307B;EGYPTIAN HIEROGLYPH D006;Lo;0;L;;;;;N;;;;;
+1307C;EGYPTIAN HIEROGLYPH D007;Lo;0;L;;;;;N;;;;;
+1307D;EGYPTIAN HIEROGLYPH D008;Lo;0;L;;;;;N;;;;;
+1307E;EGYPTIAN HIEROGLYPH D008A;Lo;0;L;;;;;N;;;;;
+1307F;EGYPTIAN HIEROGLYPH D009;Lo;0;L;;;;;N;;;;;
+13080;EGYPTIAN HIEROGLYPH D010;Lo;0;L;;;;;N;;;;;
+13081;EGYPTIAN HIEROGLYPH D011;Lo;0;L;;;;;N;;;;;
+13082;EGYPTIAN HIEROGLYPH D012;Lo;0;L;;;;;N;;;;;
+13083;EGYPTIAN HIEROGLYPH D013;Lo;0;L;;;;;N;;;;;
+13084;EGYPTIAN HIEROGLYPH D014;Lo;0;L;;;;;N;;;;;
+13085;EGYPTIAN HIEROGLYPH D015;Lo;0;L;;;;;N;;;;;
+13086;EGYPTIAN HIEROGLYPH D016;Lo;0;L;;;;;N;;;;;
+13087;EGYPTIAN HIEROGLYPH D017;Lo;0;L;;;;;N;;;;;
+13088;EGYPTIAN HIEROGLYPH D018;Lo;0;L;;;;;N;;;;;
+13089;EGYPTIAN HIEROGLYPH D019;Lo;0;L;;;;;N;;;;;
+1308A;EGYPTIAN HIEROGLYPH D020;Lo;0;L;;;;;N;;;;;
+1308B;EGYPTIAN HIEROGLYPH D021;Lo;0;L;;;;;N;;;;;
+1308C;EGYPTIAN HIEROGLYPH D022;Lo;0;L;;;;;N;;;;;
+1308D;EGYPTIAN HIEROGLYPH D023;Lo;0;L;;;;;N;;;;;
+1308E;EGYPTIAN HIEROGLYPH D024;Lo;0;L;;;;;N;;;;;
+1308F;EGYPTIAN HIEROGLYPH D025;Lo;0;L;;;;;N;;;;;
+13090;EGYPTIAN HIEROGLYPH D026;Lo;0;L;;;;;N;;;;;
+13091;EGYPTIAN HIEROGLYPH D027;Lo;0;L;;;;;N;;;;;
+13092;EGYPTIAN HIEROGLYPH D027A;Lo;0;L;;;;;N;;;;;
+13093;EGYPTIAN HIEROGLYPH D028;Lo;0;L;;;;;N;;;;;
+13094;EGYPTIAN HIEROGLYPH D029;Lo;0;L;;;;;N;;;;;
+13095;EGYPTIAN HIEROGLYPH D030;Lo;0;L;;;;;N;;;;;
+13096;EGYPTIAN HIEROGLYPH D031;Lo;0;L;;;;;N;;;;;
+13097;EGYPTIAN HIEROGLYPH D031A;Lo;0;L;;;;;N;;;;;
+13098;EGYPTIAN HIEROGLYPH D032;Lo;0;L;;;;;N;;;;;
+13099;EGYPTIAN HIEROGLYPH D033;Lo;0;L;;;;;N;;;;;
+1309A;EGYPTIAN HIEROGLYPH D034;Lo;0;L;;;;;N;;;;;
+1309B;EGYPTIAN HIEROGLYPH D034A;Lo;0;L;;;;;N;;;;;
+1309C;EGYPTIAN HIEROGLYPH D035;Lo;0;L;;;;;N;;;;;
+1309D;EGYPTIAN HIEROGLYPH D036;Lo;0;L;;;;;N;;;;;
+1309E;EGYPTIAN HIEROGLYPH D037;Lo;0;L;;;;;N;;;;;
+1309F;EGYPTIAN HIEROGLYPH D038;Lo;0;L;;;;;N;;;;;
+130A0;EGYPTIAN HIEROGLYPH D039;Lo;0;L;;;;;N;;;;;
+130A1;EGYPTIAN HIEROGLYPH D040;Lo;0;L;;;;;N;;;;;
+130A2;EGYPTIAN HIEROGLYPH D041;Lo;0;L;;;;;N;;;;;
+130A3;EGYPTIAN HIEROGLYPH D042;Lo;0;L;;;;;N;;;;;
+130A4;EGYPTIAN HIEROGLYPH D043;Lo;0;L;;;;;N;;;;;
+130A5;EGYPTIAN HIEROGLYPH D044;Lo;0;L;;;;;N;;;;;
+130A6;EGYPTIAN HIEROGLYPH D045;Lo;0;L;;;;;N;;;;;
+130A7;EGYPTIAN HIEROGLYPH D046;Lo;0;L;;;;;N;;;;;
+130A8;EGYPTIAN HIEROGLYPH D046A;Lo;0;L;;;;;N;;;;;
+130A9;EGYPTIAN HIEROGLYPH D047;Lo;0;L;;;;;N;;;;;
+130AA;EGYPTIAN HIEROGLYPH D048;Lo;0;L;;;;;N;;;;;
+130AB;EGYPTIAN HIEROGLYPH D048A;Lo;0;L;;;;;N;;;;;
+130AC;EGYPTIAN HIEROGLYPH D049;Lo;0;L;;;;;N;;;;;
+130AD;EGYPTIAN HIEROGLYPH D050;Lo;0;L;;;;;N;;;;;
+130AE;EGYPTIAN HIEROGLYPH D050A;Lo;0;L;;;;;N;;;;;
+130AF;EGYPTIAN HIEROGLYPH D050B;Lo;0;L;;;;;N;;;;;
+130B0;EGYPTIAN HIEROGLYPH D050C;Lo;0;L;;;;;N;;;;;
+130B1;EGYPTIAN HIEROGLYPH D050D;Lo;0;L;;;;;N;;;;;
+130B2;EGYPTIAN HIEROGLYPH D050E;Lo;0;L;;;;;N;;;;;
+130B3;EGYPTIAN HIEROGLYPH D050F;Lo;0;L;;;;;N;;;;;
+130B4;EGYPTIAN HIEROGLYPH D050G;Lo;0;L;;;;;N;;;;;
+130B5;EGYPTIAN HIEROGLYPH D050H;Lo;0;L;;;;;N;;;;;
+130B6;EGYPTIAN HIEROGLYPH D050I;Lo;0;L;;;;;N;;;;;
+130B7;EGYPTIAN HIEROGLYPH D051;Lo;0;L;;;;;N;;;;;
+130B8;EGYPTIAN HIEROGLYPH D052;Lo;0;L;;;;;N;;;;;
+130B9;EGYPTIAN HIEROGLYPH D052A;Lo;0;L;;;;;N;;;;;
+130BA;EGYPTIAN HIEROGLYPH D053;Lo;0;L;;;;;N;;;;;
+130BB;EGYPTIAN HIEROGLYPH D054;Lo;0;L;;;;;N;;;;;
+130BC;EGYPTIAN HIEROGLYPH D054A;Lo;0;L;;;;;N;;;;;
+130BD;EGYPTIAN HIEROGLYPH D055;Lo;0;L;;;;;N;;;;;
+130BE;EGYPTIAN HIEROGLYPH D056;Lo;0;L;;;;;N;;;;;
+130BF;EGYPTIAN HIEROGLYPH D057;Lo;0;L;;;;;N;;;;;
+130C0;EGYPTIAN HIEROGLYPH D058;Lo;0;L;;;;;N;;;;;
+130C1;EGYPTIAN HIEROGLYPH D059;Lo;0;L;;;;;N;;;;;
+130C2;EGYPTIAN HIEROGLYPH D060;Lo;0;L;;;;;N;;;;;
+130C3;EGYPTIAN HIEROGLYPH D061;Lo;0;L;;;;;N;;;;;
+130C4;EGYPTIAN HIEROGLYPH D062;Lo;0;L;;;;;N;;;;;
+130C5;EGYPTIAN HIEROGLYPH D063;Lo;0;L;;;;;N;;;;;
+130C6;EGYPTIAN HIEROGLYPH D064;Lo;0;L;;;;;N;;;;;
+130C7;EGYPTIAN HIEROGLYPH D065;Lo;0;L;;;;;N;;;;;
+130C8;EGYPTIAN HIEROGLYPH D066;Lo;0;L;;;;;N;;;;;
+130C9;EGYPTIAN HIEROGLYPH D067;Lo;0;L;;;;;N;;;;;
+130CA;EGYPTIAN HIEROGLYPH D067A;Lo;0;L;;;;;N;;;;;
+130CB;EGYPTIAN HIEROGLYPH D067B;Lo;0;L;;;;;N;;;;;
+130CC;EGYPTIAN HIEROGLYPH D067C;Lo;0;L;;;;;N;;;;;
+130CD;EGYPTIAN HIEROGLYPH D067D;Lo;0;L;;;;;N;;;;;
+130CE;EGYPTIAN HIEROGLYPH D067E;Lo;0;L;;;;;N;;;;;
+130CF;EGYPTIAN HIEROGLYPH D067F;Lo;0;L;;;;;N;;;;;
+130D0;EGYPTIAN HIEROGLYPH D067G;Lo;0;L;;;;;N;;;;;
+130D1;EGYPTIAN HIEROGLYPH D067H;Lo;0;L;;;;;N;;;;;
+130D2;EGYPTIAN HIEROGLYPH E001;Lo;0;L;;;;;N;;;;;
+130D3;EGYPTIAN HIEROGLYPH E002;Lo;0;L;;;;;N;;;;;
+130D4;EGYPTIAN HIEROGLYPH E003;Lo;0;L;;;;;N;;;;;
+130D5;EGYPTIAN HIEROGLYPH E004;Lo;0;L;;;;;N;;;;;
+130D6;EGYPTIAN HIEROGLYPH E005;Lo;0;L;;;;;N;;;;;
+130D7;EGYPTIAN HIEROGLYPH E006;Lo;0;L;;;;;N;;;;;
+130D8;EGYPTIAN HIEROGLYPH E007;Lo;0;L;;;;;N;;;;;
+130D9;EGYPTIAN HIEROGLYPH E008;Lo;0;L;;;;;N;;;;;
+130DA;EGYPTIAN HIEROGLYPH E008A;Lo;0;L;;;;;N;;;;;
+130DB;EGYPTIAN HIEROGLYPH E009;Lo;0;L;;;;;N;;;;;
+130DC;EGYPTIAN HIEROGLYPH E009A;Lo;0;L;;;;;N;;;;;
+130DD;EGYPTIAN HIEROGLYPH E010;Lo;0;L;;;;;N;;;;;
+130DE;EGYPTIAN HIEROGLYPH E011;Lo;0;L;;;;;N;;;;;
+130DF;EGYPTIAN HIEROGLYPH E012;Lo;0;L;;;;;N;;;;;
+130E0;EGYPTIAN HIEROGLYPH E013;Lo;0;L;;;;;N;;;;;
+130E1;EGYPTIAN HIEROGLYPH E014;Lo;0;L;;;;;N;;;;;
+130E2;EGYPTIAN HIEROGLYPH E015;Lo;0;L;;;;;N;;;;;
+130E3;EGYPTIAN HIEROGLYPH E016;Lo;0;L;;;;;N;;;;;
+130E4;EGYPTIAN HIEROGLYPH E016A;Lo;0;L;;;;;N;;;;;
+130E5;EGYPTIAN HIEROGLYPH E017;Lo;0;L;;;;;N;;;;;
+130E6;EGYPTIAN HIEROGLYPH E017A;Lo;0;L;;;;;N;;;;;
+130E7;EGYPTIAN HIEROGLYPH E018;Lo;0;L;;;;;N;;;;;
+130E8;EGYPTIAN HIEROGLYPH E019;Lo;0;L;;;;;N;;;;;
+130E9;EGYPTIAN HIEROGLYPH E020;Lo;0;L;;;;;N;;;;;
+130EA;EGYPTIAN HIEROGLYPH E020A;Lo;0;L;;;;;N;;;;;
+130EB;EGYPTIAN HIEROGLYPH E021;Lo;0;L;;;;;N;;;;;
+130EC;EGYPTIAN HIEROGLYPH E022;Lo;0;L;;;;;N;;;;;
+130ED;EGYPTIAN HIEROGLYPH E023;Lo;0;L;;;;;N;;;;;
+130EE;EGYPTIAN HIEROGLYPH E024;Lo;0;L;;;;;N;;;;;
+130EF;EGYPTIAN HIEROGLYPH E025;Lo;0;L;;;;;N;;;;;
+130F0;EGYPTIAN HIEROGLYPH E026;Lo;0;L;;;;;N;;;;;
+130F1;EGYPTIAN HIEROGLYPH E027;Lo;0;L;;;;;N;;;;;
+130F2;EGYPTIAN HIEROGLYPH E028;Lo;0;L;;;;;N;;;;;
+130F3;EGYPTIAN HIEROGLYPH E028A;Lo;0;L;;;;;N;;;;;
+130F4;EGYPTIAN HIEROGLYPH E029;Lo;0;L;;;;;N;;;;;
+130F5;EGYPTIAN HIEROGLYPH E030;Lo;0;L;;;;;N;;;;;
+130F6;EGYPTIAN HIEROGLYPH E031;Lo;0;L;;;;;N;;;;;
+130F7;EGYPTIAN HIEROGLYPH E032;Lo;0;L;;;;;N;;;;;
+130F8;EGYPTIAN HIEROGLYPH E033;Lo;0;L;;;;;N;;;;;
+130F9;EGYPTIAN HIEROGLYPH E034;Lo;0;L;;;;;N;;;;;
+130FA;EGYPTIAN HIEROGLYPH E034A;Lo;0;L;;;;;N;;;;;
+130FB;EGYPTIAN HIEROGLYPH E036;Lo;0;L;;;;;N;;;;;
+130FC;EGYPTIAN HIEROGLYPH E037;Lo;0;L;;;;;N;;;;;
+130FD;EGYPTIAN HIEROGLYPH E038;Lo;0;L;;;;;N;;;;;
+130FE;EGYPTIAN HIEROGLYPH F001;Lo;0;L;;;;;N;;;;;
+130FF;EGYPTIAN HIEROGLYPH F001A;Lo;0;L;;;;;N;;;;;
+13100;EGYPTIAN HIEROGLYPH F002;Lo;0;L;;;;;N;;;;;
+13101;EGYPTIAN HIEROGLYPH F003;Lo;0;L;;;;;N;;;;;
+13102;EGYPTIAN HIEROGLYPH F004;Lo;0;L;;;;;N;;;;;
+13103;EGYPTIAN HIEROGLYPH F005;Lo;0;L;;;;;N;;;;;
+13104;EGYPTIAN HIEROGLYPH F006;Lo;0;L;;;;;N;;;;;
+13105;EGYPTIAN HIEROGLYPH F007;Lo;0;L;;;;;N;;;;;
+13106;EGYPTIAN HIEROGLYPH F008;Lo;0;L;;;;;N;;;;;
+13107;EGYPTIAN HIEROGLYPH F009;Lo;0;L;;;;;N;;;;;
+13108;EGYPTIAN HIEROGLYPH F010;Lo;0;L;;;;;N;;;;;
+13109;EGYPTIAN HIEROGLYPH F011;Lo;0;L;;;;;N;;;;;
+1310A;EGYPTIAN HIEROGLYPH F012;Lo;0;L;;;;;N;;;;;
+1310B;EGYPTIAN HIEROGLYPH F013;Lo;0;L;;;;;N;;;;;
+1310C;EGYPTIAN HIEROGLYPH F013A;Lo;0;L;;;;;N;;;;;
+1310D;EGYPTIAN HIEROGLYPH F014;Lo;0;L;;;;;N;;;;;
+1310E;EGYPTIAN HIEROGLYPH F015;Lo;0;L;;;;;N;;;;;
+1310F;EGYPTIAN HIEROGLYPH F016;Lo;0;L;;;;;N;;;;;
+13110;EGYPTIAN HIEROGLYPH F017;Lo;0;L;;;;;N;;;;;
+13111;EGYPTIAN HIEROGLYPH F018;Lo;0;L;;;;;N;;;;;
+13112;EGYPTIAN HIEROGLYPH F019;Lo;0;L;;;;;N;;;;;
+13113;EGYPTIAN HIEROGLYPH F020;Lo;0;L;;;;;N;;;;;
+13114;EGYPTIAN HIEROGLYPH F021;Lo;0;L;;;;;N;;;;;
+13115;EGYPTIAN HIEROGLYPH F021A;Lo;0;L;;;;;N;;;;;
+13116;EGYPTIAN HIEROGLYPH F022;Lo;0;L;;;;;N;;;;;
+13117;EGYPTIAN HIEROGLYPH F023;Lo;0;L;;;;;N;;;;;
+13118;EGYPTIAN HIEROGLYPH F024;Lo;0;L;;;;;N;;;;;
+13119;EGYPTIAN HIEROGLYPH F025;Lo;0;L;;;;;N;;;;;
+1311A;EGYPTIAN HIEROGLYPH F026;Lo;0;L;;;;;N;;;;;
+1311B;EGYPTIAN HIEROGLYPH F027;Lo;0;L;;;;;N;;;;;
+1311C;EGYPTIAN HIEROGLYPH F028;Lo;0;L;;;;;N;;;;;
+1311D;EGYPTIAN HIEROGLYPH F029;Lo;0;L;;;;;N;;;;;
+1311E;EGYPTIAN HIEROGLYPH F030;Lo;0;L;;;;;N;;;;;
+1311F;EGYPTIAN HIEROGLYPH F031;Lo;0;L;;;;;N;;;;;
+13120;EGYPTIAN HIEROGLYPH F031A;Lo;0;L;;;;;N;;;;;
+13121;EGYPTIAN HIEROGLYPH F032;Lo;0;L;;;;;N;;;;;
+13122;EGYPTIAN HIEROGLYPH F033;Lo;0;L;;;;;N;;;;;
+13123;EGYPTIAN HIEROGLYPH F034;Lo;0;L;;;;;N;;;;;
+13124;EGYPTIAN HIEROGLYPH F035;Lo;0;L;;;;;N;;;;;
+13125;EGYPTIAN HIEROGLYPH F036;Lo;0;L;;;;;N;;;;;
+13126;EGYPTIAN HIEROGLYPH F037;Lo;0;L;;;;;N;;;;;
+13127;EGYPTIAN HIEROGLYPH F037A;Lo;0;L;;;;;N;;;;;
+13128;EGYPTIAN HIEROGLYPH F038;Lo;0;L;;;;;N;;;;;
+13129;EGYPTIAN HIEROGLYPH F038A;Lo;0;L;;;;;N;;;;;
+1312A;EGYPTIAN HIEROGLYPH F039;Lo;0;L;;;;;N;;;;;
+1312B;EGYPTIAN HIEROGLYPH F040;Lo;0;L;;;;;N;;;;;
+1312C;EGYPTIAN HIEROGLYPH F041;Lo;0;L;;;;;N;;;;;
+1312D;EGYPTIAN HIEROGLYPH F042;Lo;0;L;;;;;N;;;;;
+1312E;EGYPTIAN HIEROGLYPH F043;Lo;0;L;;;;;N;;;;;
+1312F;EGYPTIAN HIEROGLYPH F044;Lo;0;L;;;;;N;;;;;
+13130;EGYPTIAN HIEROGLYPH F045;Lo;0;L;;;;;N;;;;;
+13131;EGYPTIAN HIEROGLYPH F045A;Lo;0;L;;;;;N;;;;;
+13132;EGYPTIAN HIEROGLYPH F046;Lo;0;L;;;;;N;;;;;
+13133;EGYPTIAN HIEROGLYPH F046A;Lo;0;L;;;;;N;;;;;
+13134;EGYPTIAN HIEROGLYPH F047;Lo;0;L;;;;;N;;;;;
+13135;EGYPTIAN HIEROGLYPH F047A;Lo;0;L;;;;;N;;;;;
+13136;EGYPTIAN HIEROGLYPH F048;Lo;0;L;;;;;N;;;;;
+13137;EGYPTIAN HIEROGLYPH F049;Lo;0;L;;;;;N;;;;;
+13138;EGYPTIAN HIEROGLYPH F050;Lo;0;L;;;;;N;;;;;
+13139;EGYPTIAN HIEROGLYPH F051;Lo;0;L;;;;;N;;;;;
+1313A;EGYPTIAN HIEROGLYPH F051A;Lo;0;L;;;;;N;;;;;
+1313B;EGYPTIAN HIEROGLYPH F051B;Lo;0;L;;;;;N;;;;;
+1313C;EGYPTIAN HIEROGLYPH F051C;Lo;0;L;;;;;N;;;;;
+1313D;EGYPTIAN HIEROGLYPH F052;Lo;0;L;;;;;N;;;;;
+1313E;EGYPTIAN HIEROGLYPH F053;Lo;0;L;;;;;N;;;;;
+1313F;EGYPTIAN HIEROGLYPH G001;Lo;0;L;;;;;N;;;;;
+13140;EGYPTIAN HIEROGLYPH G002;Lo;0;L;;;;;N;;;;;
+13141;EGYPTIAN HIEROGLYPH G003;Lo;0;L;;;;;N;;;;;
+13142;EGYPTIAN HIEROGLYPH G004;Lo;0;L;;;;;N;;;;;
+13143;EGYPTIAN HIEROGLYPH G005;Lo;0;L;;;;;N;;;;;
+13144;EGYPTIAN HIEROGLYPH G006;Lo;0;L;;;;;N;;;;;
+13145;EGYPTIAN HIEROGLYPH G006A;Lo;0;L;;;;;N;;;;;
+13146;EGYPTIAN HIEROGLYPH G007;Lo;0;L;;;;;N;;;;;
+13147;EGYPTIAN HIEROGLYPH G007A;Lo;0;L;;;;;N;;;;;
+13148;EGYPTIAN HIEROGLYPH G007B;Lo;0;L;;;;;N;;;;;
+13149;EGYPTIAN HIEROGLYPH G008;Lo;0;L;;;;;N;;;;;
+1314A;EGYPTIAN HIEROGLYPH G009;Lo;0;L;;;;;N;;;;;
+1314B;EGYPTIAN HIEROGLYPH G010;Lo;0;L;;;;;N;;;;;
+1314C;EGYPTIAN HIEROGLYPH G011;Lo;0;L;;;;;N;;;;;
+1314D;EGYPTIAN HIEROGLYPH G011A;Lo;0;L;;;;;N;;;;;
+1314E;EGYPTIAN HIEROGLYPH G012;Lo;0;L;;;;;N;;;;;
+1314F;EGYPTIAN HIEROGLYPH G013;Lo;0;L;;;;;N;;;;;
+13150;EGYPTIAN HIEROGLYPH G014;Lo;0;L;;;;;N;;;;;
+13151;EGYPTIAN HIEROGLYPH G015;Lo;0;L;;;;;N;;;;;
+13152;EGYPTIAN HIEROGLYPH G016;Lo;0;L;;;;;N;;;;;
+13153;EGYPTIAN HIEROGLYPH G017;Lo;0;L;;;;;N;;;;;
+13154;EGYPTIAN HIEROGLYPH G018;Lo;0;L;;;;;N;;;;;
+13155;EGYPTIAN HIEROGLYPH G019;Lo;0;L;;;;;N;;;;;
+13156;EGYPTIAN HIEROGLYPH G020;Lo;0;L;;;;;N;;;;;
+13157;EGYPTIAN HIEROGLYPH G020A;Lo;0;L;;;;;N;;;;;
+13158;EGYPTIAN HIEROGLYPH G021;Lo;0;L;;;;;N;;;;;
+13159;EGYPTIAN HIEROGLYPH G022;Lo;0;L;;;;;N;;;;;
+1315A;EGYPTIAN HIEROGLYPH G023;Lo;0;L;;;;;N;;;;;
+1315B;EGYPTIAN HIEROGLYPH G024;Lo;0;L;;;;;N;;;;;
+1315C;EGYPTIAN HIEROGLYPH G025;Lo;0;L;;;;;N;;;;;
+1315D;EGYPTIAN HIEROGLYPH G026;Lo;0;L;;;;;N;;;;;
+1315E;EGYPTIAN HIEROGLYPH G026A;Lo;0;L;;;;;N;;;;;
+1315F;EGYPTIAN HIEROGLYPH G027;Lo;0;L;;;;;N;;;;;
+13160;EGYPTIAN HIEROGLYPH G028;Lo;0;L;;;;;N;;;;;
+13161;EGYPTIAN HIEROGLYPH G029;Lo;0;L;;;;;N;;;;;
+13162;EGYPTIAN HIEROGLYPH G030;Lo;0;L;;;;;N;;;;;
+13163;EGYPTIAN HIEROGLYPH G031;Lo;0;L;;;;;N;;;;;
+13164;EGYPTIAN HIEROGLYPH G032;Lo;0;L;;;;;N;;;;;
+13165;EGYPTIAN HIEROGLYPH G033;Lo;0;L;;;;;N;;;;;
+13166;EGYPTIAN HIEROGLYPH G034;Lo;0;L;;;;;N;;;;;
+13167;EGYPTIAN HIEROGLYPH G035;Lo;0;L;;;;;N;;;;;
+13168;EGYPTIAN HIEROGLYPH G036;Lo;0;L;;;;;N;;;;;
+13169;EGYPTIAN HIEROGLYPH G036A;Lo;0;L;;;;;N;;;;;
+1316A;EGYPTIAN HIEROGLYPH G037;Lo;0;L;;;;;N;;;;;
+1316B;EGYPTIAN HIEROGLYPH G037A;Lo;0;L;;;;;N;;;;;
+1316C;EGYPTIAN HIEROGLYPH G038;Lo;0;L;;;;;N;;;;;
+1316D;EGYPTIAN HIEROGLYPH G039;Lo;0;L;;;;;N;;;;;
+1316E;EGYPTIAN HIEROGLYPH G040;Lo;0;L;;;;;N;;;;;
+1316F;EGYPTIAN HIEROGLYPH G041;Lo;0;L;;;;;N;;;;;
+13170;EGYPTIAN HIEROGLYPH G042;Lo;0;L;;;;;N;;;;;
+13171;EGYPTIAN HIEROGLYPH G043;Lo;0;L;;;;;N;;;;;
+13172;EGYPTIAN HIEROGLYPH G043A;Lo;0;L;;;;;N;;;;;
+13173;EGYPTIAN HIEROGLYPH G044;Lo;0;L;;;;;N;;;;;
+13174;EGYPTIAN HIEROGLYPH G045;Lo;0;L;;;;;N;;;;;
+13175;EGYPTIAN HIEROGLYPH G045A;Lo;0;L;;;;;N;;;;;
+13176;EGYPTIAN HIEROGLYPH G046;Lo;0;L;;;;;N;;;;;
+13177;EGYPTIAN HIEROGLYPH G047;Lo;0;L;;;;;N;;;;;
+13178;EGYPTIAN HIEROGLYPH G048;Lo;0;L;;;;;N;;;;;
+13179;EGYPTIAN HIEROGLYPH G049;Lo;0;L;;;;;N;;;;;
+1317A;EGYPTIAN HIEROGLYPH G050;Lo;0;L;;;;;N;;;;;
+1317B;EGYPTIAN HIEROGLYPH G051;Lo;0;L;;;;;N;;;;;
+1317C;EGYPTIAN HIEROGLYPH G052;Lo;0;L;;;;;N;;;;;
+1317D;EGYPTIAN HIEROGLYPH G053;Lo;0;L;;;;;N;;;;;
+1317E;EGYPTIAN HIEROGLYPH G054;Lo;0;L;;;;;N;;;;;
+1317F;EGYPTIAN HIEROGLYPH H001;Lo;0;L;;;;;N;;;;;
+13180;EGYPTIAN HIEROGLYPH H002;Lo;0;L;;;;;N;;;;;
+13181;EGYPTIAN HIEROGLYPH H003;Lo;0;L;;;;;N;;;;;
+13182;EGYPTIAN HIEROGLYPH H004;Lo;0;L;;;;;N;;;;;
+13183;EGYPTIAN HIEROGLYPH H005;Lo;0;L;;;;;N;;;;;
+13184;EGYPTIAN HIEROGLYPH H006;Lo;0;L;;;;;N;;;;;
+13185;EGYPTIAN HIEROGLYPH H006A;Lo;0;L;;;;;N;;;;;
+13186;EGYPTIAN HIEROGLYPH H007;Lo;0;L;;;;;N;;;;;
+13187;EGYPTIAN HIEROGLYPH H008;Lo;0;L;;;;;N;;;;;
+13188;EGYPTIAN HIEROGLYPH I001;Lo;0;L;;;;;N;;;;;
+13189;EGYPTIAN HIEROGLYPH I002;Lo;0;L;;;;;N;;;;;
+1318A;EGYPTIAN HIEROGLYPH I003;Lo;0;L;;;;;N;;;;;
+1318B;EGYPTIAN HIEROGLYPH I004;Lo;0;L;;;;;N;;;;;
+1318C;EGYPTIAN HIEROGLYPH I005;Lo;0;L;;;;;N;;;;;
+1318D;EGYPTIAN HIEROGLYPH I005A;Lo;0;L;;;;;N;;;;;
+1318E;EGYPTIAN HIEROGLYPH I006;Lo;0;L;;;;;N;;;;;
+1318F;EGYPTIAN HIEROGLYPH I007;Lo;0;L;;;;;N;;;;;
+13190;EGYPTIAN HIEROGLYPH I008;Lo;0;L;;;;;N;;;;;
+13191;EGYPTIAN HIEROGLYPH I009;Lo;0;L;;;;;N;;;;;
+13192;EGYPTIAN HIEROGLYPH I009A;Lo;0;L;;;;;N;;;;;
+13193;EGYPTIAN HIEROGLYPH I010;Lo;0;L;;;;;N;;;;;
+13194;EGYPTIAN HIEROGLYPH I010A;Lo;0;L;;;;;N;;;;;
+13195;EGYPTIAN HIEROGLYPH I011;Lo;0;L;;;;;N;;;;;
+13196;EGYPTIAN HIEROGLYPH I011A;Lo;0;L;;;;;N;;;;;
+13197;EGYPTIAN HIEROGLYPH I012;Lo;0;L;;;;;N;;;;;
+13198;EGYPTIAN HIEROGLYPH I013;Lo;0;L;;;;;N;;;;;
+13199;EGYPTIAN HIEROGLYPH I014;Lo;0;L;;;;;N;;;;;
+1319A;EGYPTIAN HIEROGLYPH I015;Lo;0;L;;;;;N;;;;;
+1319B;EGYPTIAN HIEROGLYPH K001;Lo;0;L;;;;;N;;;;;
+1319C;EGYPTIAN HIEROGLYPH K002;Lo;0;L;;;;;N;;;;;
+1319D;EGYPTIAN HIEROGLYPH K003;Lo;0;L;;;;;N;;;;;
+1319E;EGYPTIAN HIEROGLYPH K004;Lo;0;L;;;;;N;;;;;
+1319F;EGYPTIAN HIEROGLYPH K005;Lo;0;L;;;;;N;;;;;
+131A0;EGYPTIAN HIEROGLYPH K006;Lo;0;L;;;;;N;;;;;
+131A1;EGYPTIAN HIEROGLYPH K007;Lo;0;L;;;;;N;;;;;
+131A2;EGYPTIAN HIEROGLYPH K008;Lo;0;L;;;;;N;;;;;
+131A3;EGYPTIAN HIEROGLYPH L001;Lo;0;L;;;;;N;;;;;
+131A4;EGYPTIAN HIEROGLYPH L002;Lo;0;L;;;;;N;;;;;
+131A5;EGYPTIAN HIEROGLYPH L002A;Lo;0;L;;;;;N;;;;;
+131A6;EGYPTIAN HIEROGLYPH L003;Lo;0;L;;;;;N;;;;;
+131A7;EGYPTIAN HIEROGLYPH L004;Lo;0;L;;;;;N;;;;;
+131A8;EGYPTIAN HIEROGLYPH L005;Lo;0;L;;;;;N;;;;;
+131A9;EGYPTIAN HIEROGLYPH L006;Lo;0;L;;;;;N;;;;;
+131AA;EGYPTIAN HIEROGLYPH L006A;Lo;0;L;;;;;N;;;;;
+131AB;EGYPTIAN HIEROGLYPH L007;Lo;0;L;;;;;N;;;;;
+131AC;EGYPTIAN HIEROGLYPH L008;Lo;0;L;;;;;N;;;;;
+131AD;EGYPTIAN HIEROGLYPH M001;Lo;0;L;;;;;N;;;;;
+131AE;EGYPTIAN HIEROGLYPH M001A;Lo;0;L;;;;;N;;;;;
+131AF;EGYPTIAN HIEROGLYPH M001B;Lo;0;L;;;;;N;;;;;
+131B0;EGYPTIAN HIEROGLYPH M002;Lo;0;L;;;;;N;;;;;
+131B1;EGYPTIAN HIEROGLYPH M003;Lo;0;L;;;;;N;;;;;
+131B2;EGYPTIAN HIEROGLYPH M003A;Lo;0;L;;;;;N;;;;;
+131B3;EGYPTIAN HIEROGLYPH M004;Lo;0;L;;;;;N;;;;;
+131B4;EGYPTIAN HIEROGLYPH M005;Lo;0;L;;;;;N;;;;;
+131B5;EGYPTIAN HIEROGLYPH M006;Lo;0;L;;;;;N;;;;;
+131B6;EGYPTIAN HIEROGLYPH M007;Lo;0;L;;;;;N;;;;;
+131B7;EGYPTIAN HIEROGLYPH M008;Lo;0;L;;;;;N;;;;;
+131B8;EGYPTIAN HIEROGLYPH M009;Lo;0;L;;;;;N;;;;;
+131B9;EGYPTIAN HIEROGLYPH M010;Lo;0;L;;;;;N;;;;;
+131BA;EGYPTIAN HIEROGLYPH M010A;Lo;0;L;;;;;N;;;;;
+131BB;EGYPTIAN HIEROGLYPH M011;Lo;0;L;;;;;N;;;;;
+131BC;EGYPTIAN HIEROGLYPH M012;Lo;0;L;;;;;N;;;;;
+131BD;EGYPTIAN HIEROGLYPH M012A;Lo;0;L;;;;;N;;;;;
+131BE;EGYPTIAN HIEROGLYPH M012B;Lo;0;L;;;;;N;;;;;
+131BF;EGYPTIAN HIEROGLYPH M012C;Lo;0;L;;;;;N;;;;;
+131C0;EGYPTIAN HIEROGLYPH M012D;Lo;0;L;;;;;N;;;;;
+131C1;EGYPTIAN HIEROGLYPH M012E;Lo;0;L;;;;;N;;;;;
+131C2;EGYPTIAN HIEROGLYPH M012F;Lo;0;L;;;;;N;;;;;
+131C3;EGYPTIAN HIEROGLYPH M012G;Lo;0;L;;;;;N;;;;;
+131C4;EGYPTIAN HIEROGLYPH M012H;Lo;0;L;;;;;N;;;;;
+131C5;EGYPTIAN HIEROGLYPH M013;Lo;0;L;;;;;N;;;;;
+131C6;EGYPTIAN HIEROGLYPH M014;Lo;0;L;;;;;N;;;;;
+131C7;EGYPTIAN HIEROGLYPH M015;Lo;0;L;;;;;N;;;;;
+131C8;EGYPTIAN HIEROGLYPH M015A;Lo;0;L;;;;;N;;;;;
+131C9;EGYPTIAN HIEROGLYPH M016;Lo;0;L;;;;;N;;;;;
+131CA;EGYPTIAN HIEROGLYPH M016A;Lo;0;L;;;;;N;;;;;
+131CB;EGYPTIAN HIEROGLYPH M017;Lo;0;L;;;;;N;;;;;
+131CC;EGYPTIAN HIEROGLYPH M017A;Lo;0;L;;;;;N;;;;;
+131CD;EGYPTIAN HIEROGLYPH M018;Lo;0;L;;;;;N;;;;;
+131CE;EGYPTIAN HIEROGLYPH M019;Lo;0;L;;;;;N;;;;;
+131CF;EGYPTIAN HIEROGLYPH M020;Lo;0;L;;;;;N;;;;;
+131D0;EGYPTIAN HIEROGLYPH M021;Lo;0;L;;;;;N;;;;;
+131D1;EGYPTIAN HIEROGLYPH M022;Lo;0;L;;;;;N;;;;;
+131D2;EGYPTIAN HIEROGLYPH M022A;Lo;0;L;;;;;N;;;;;
+131D3;EGYPTIAN HIEROGLYPH M023;Lo;0;L;;;;;N;;;;;
+131D4;EGYPTIAN HIEROGLYPH M024;Lo;0;L;;;;;N;;;;;
+131D5;EGYPTIAN HIEROGLYPH M024A;Lo;0;L;;;;;N;;;;;
+131D6;EGYPTIAN HIEROGLYPH M025;Lo;0;L;;;;;N;;;;;
+131D7;EGYPTIAN HIEROGLYPH M026;Lo;0;L;;;;;N;;;;;
+131D8;EGYPTIAN HIEROGLYPH M027;Lo;0;L;;;;;N;;;;;
+131D9;EGYPTIAN HIEROGLYPH M028;Lo;0;L;;;;;N;;;;;
+131DA;EGYPTIAN HIEROGLYPH M028A;Lo;0;L;;;;;N;;;;;
+131DB;EGYPTIAN HIEROGLYPH M029;Lo;0;L;;;;;N;;;;;
+131DC;EGYPTIAN HIEROGLYPH M030;Lo;0;L;;;;;N;;;;;
+131DD;EGYPTIAN HIEROGLYPH M031;Lo;0;L;;;;;N;;;;;
+131DE;EGYPTIAN HIEROGLYPH M031A;Lo;0;L;;;;;N;;;;;
+131DF;EGYPTIAN HIEROGLYPH M032;Lo;0;L;;;;;N;;;;;
+131E0;EGYPTIAN HIEROGLYPH M033;Lo;0;L;;;;;N;;;;;
+131E1;EGYPTIAN HIEROGLYPH M033A;Lo;0;L;;;;;N;;;;;
+131E2;EGYPTIAN HIEROGLYPH M033B;Lo;0;L;;;;;N;;;;;
+131E3;EGYPTIAN HIEROGLYPH M034;Lo;0;L;;;;;N;;;;;
+131E4;EGYPTIAN HIEROGLYPH M035;Lo;0;L;;;;;N;;;;;
+131E5;EGYPTIAN HIEROGLYPH M036;Lo;0;L;;;;;N;;;;;
+131E6;EGYPTIAN HIEROGLYPH M037;Lo;0;L;;;;;N;;;;;
+131E7;EGYPTIAN HIEROGLYPH M038;Lo;0;L;;;;;N;;;;;
+131E8;EGYPTIAN HIEROGLYPH M039;Lo;0;L;;;;;N;;;;;
+131E9;EGYPTIAN HIEROGLYPH M040;Lo;0;L;;;;;N;;;;;
+131EA;EGYPTIAN HIEROGLYPH M040A;Lo;0;L;;;;;N;;;;;
+131EB;EGYPTIAN HIEROGLYPH M041;Lo;0;L;;;;;N;;;;;
+131EC;EGYPTIAN HIEROGLYPH M042;Lo;0;L;;;;;N;;;;;
+131ED;EGYPTIAN HIEROGLYPH M043;Lo;0;L;;;;;N;;;;;
+131EE;EGYPTIAN HIEROGLYPH M044;Lo;0;L;;;;;N;;;;;
+131EF;EGYPTIAN HIEROGLYPH N001;Lo;0;L;;;;;N;;;;;
+131F0;EGYPTIAN HIEROGLYPH N002;Lo;0;L;;;;;N;;;;;
+131F1;EGYPTIAN HIEROGLYPH N003;Lo;0;L;;;;;N;;;;;
+131F2;EGYPTIAN HIEROGLYPH N004;Lo;0;L;;;;;N;;;;;
+131F3;EGYPTIAN HIEROGLYPH N005;Lo;0;L;;;;;N;;;;;
+131F4;EGYPTIAN HIEROGLYPH N006;Lo;0;L;;;;;N;;;;;
+131F5;EGYPTIAN HIEROGLYPH N007;Lo;0;L;;;;;N;;;;;
+131F6;EGYPTIAN HIEROGLYPH N008;Lo;0;L;;;;;N;;;;;
+131F7;EGYPTIAN HIEROGLYPH N009;Lo;0;L;;;;;N;;;;;
+131F8;EGYPTIAN HIEROGLYPH N010;Lo;0;L;;;;;N;;;;;
+131F9;EGYPTIAN HIEROGLYPH N011;Lo;0;L;;;;;N;;;;;
+131FA;EGYPTIAN HIEROGLYPH N012;Lo;0;L;;;;;N;;;;;
+131FB;EGYPTIAN HIEROGLYPH N013;Lo;0;L;;;;;N;;;;;
+131FC;EGYPTIAN HIEROGLYPH N014;Lo;0;L;;;;;N;;;;;
+131FD;EGYPTIAN HIEROGLYPH N015;Lo;0;L;;;;;N;;;;;
+131FE;EGYPTIAN HIEROGLYPH N016;Lo;0;L;;;;;N;;;;;
+131FF;EGYPTIAN HIEROGLYPH N017;Lo;0;L;;;;;N;;;;;
+13200;EGYPTIAN HIEROGLYPH N018;Lo;0;L;;;;;N;;;;;
+13201;EGYPTIAN HIEROGLYPH N018A;Lo;0;L;;;;;N;;;;;
+13202;EGYPTIAN HIEROGLYPH N018B;Lo;0;L;;;;;N;;;;;
+13203;EGYPTIAN HIEROGLYPH N019;Lo;0;L;;;;;N;;;;;
+13204;EGYPTIAN HIEROGLYPH N020;Lo;0;L;;;;;N;;;;;
+13205;EGYPTIAN HIEROGLYPH N021;Lo;0;L;;;;;N;;;;;
+13206;EGYPTIAN HIEROGLYPH N022;Lo;0;L;;;;;N;;;;;
+13207;EGYPTIAN HIEROGLYPH N023;Lo;0;L;;;;;N;;;;;
+13208;EGYPTIAN HIEROGLYPH N024;Lo;0;L;;;;;N;;;;;
+13209;EGYPTIAN HIEROGLYPH N025;Lo;0;L;;;;;N;;;;;
+1320A;EGYPTIAN HIEROGLYPH N025A;Lo;0;L;;;;;N;;;;;
+1320B;EGYPTIAN HIEROGLYPH N026;Lo;0;L;;;;;N;;;;;
+1320C;EGYPTIAN HIEROGLYPH N027;Lo;0;L;;;;;N;;;;;
+1320D;EGYPTIAN HIEROGLYPH N028;Lo;0;L;;;;;N;;;;;
+1320E;EGYPTIAN HIEROGLYPH N029;Lo;0;L;;;;;N;;;;;
+1320F;EGYPTIAN HIEROGLYPH N030;Lo;0;L;;;;;N;;;;;
+13210;EGYPTIAN HIEROGLYPH N031;Lo;0;L;;;;;N;;;;;
+13211;EGYPTIAN HIEROGLYPH N032;Lo;0;L;;;;;N;;;;;
+13212;EGYPTIAN HIEROGLYPH N033;Lo;0;L;;;;;N;;;;;
+13213;EGYPTIAN HIEROGLYPH N033A;Lo;0;L;;;;;N;;;;;
+13214;EGYPTIAN HIEROGLYPH N034;Lo;0;L;;;;;N;;;;;
+13215;EGYPTIAN HIEROGLYPH N034A;Lo;0;L;;;;;N;;;;;
+13216;EGYPTIAN HIEROGLYPH N035;Lo;0;L;;;;;N;;;;;
+13217;EGYPTIAN HIEROGLYPH N035A;Lo;0;L;;;;;N;;;;;
+13218;EGYPTIAN HIEROGLYPH N036;Lo;0;L;;;;;N;;;;;
+13219;EGYPTIAN HIEROGLYPH N037;Lo;0;L;;;;;N;;;;;
+1321A;EGYPTIAN HIEROGLYPH N037A;Lo;0;L;;;;;N;;;;;
+1321B;EGYPTIAN HIEROGLYPH N038;Lo;0;L;;;;;N;;;;;
+1321C;EGYPTIAN HIEROGLYPH N039;Lo;0;L;;;;;N;;;;;
+1321D;EGYPTIAN HIEROGLYPH N040;Lo;0;L;;;;;N;;;;;
+1321E;EGYPTIAN HIEROGLYPH N041;Lo;0;L;;;;;N;;;;;
+1321F;EGYPTIAN HIEROGLYPH N042;Lo;0;L;;;;;N;;;;;
+13220;EGYPTIAN HIEROGLYPH NL001;Lo;0;L;;;;;N;;;;;
+13221;EGYPTIAN HIEROGLYPH NL002;Lo;0;L;;;;;N;;;;;
+13222;EGYPTIAN HIEROGLYPH NL003;Lo;0;L;;;;;N;;;;;
+13223;EGYPTIAN HIEROGLYPH NL004;Lo;0;L;;;;;N;;;;;
+13224;EGYPTIAN HIEROGLYPH NL005;Lo;0;L;;;;;N;;;;;
+13225;EGYPTIAN HIEROGLYPH NL005A;Lo;0;L;;;;;N;;;;;
+13226;EGYPTIAN HIEROGLYPH NL006;Lo;0;L;;;;;N;;;;;
+13227;EGYPTIAN HIEROGLYPH NL007;Lo;0;L;;;;;N;;;;;
+13228;EGYPTIAN HIEROGLYPH NL008;Lo;0;L;;;;;N;;;;;
+13229;EGYPTIAN HIEROGLYPH NL009;Lo;0;L;;;;;N;;;;;
+1322A;EGYPTIAN HIEROGLYPH NL010;Lo;0;L;;;;;N;;;;;
+1322B;EGYPTIAN HIEROGLYPH NL011;Lo;0;L;;;;;N;;;;;
+1322C;EGYPTIAN HIEROGLYPH NL012;Lo;0;L;;;;;N;;;;;
+1322D;EGYPTIAN HIEROGLYPH NL013;Lo;0;L;;;;;N;;;;;
+1322E;EGYPTIAN HIEROGLYPH NL014;Lo;0;L;;;;;N;;;;;
+1322F;EGYPTIAN HIEROGLYPH NL015;Lo;0;L;;;;;N;;;;;
+13230;EGYPTIAN HIEROGLYPH NL016;Lo;0;L;;;;;N;;;;;
+13231;EGYPTIAN HIEROGLYPH NL017;Lo;0;L;;;;;N;;;;;
+13232;EGYPTIAN HIEROGLYPH NL017A;Lo;0;L;;;;;N;;;;;
+13233;EGYPTIAN HIEROGLYPH NL018;Lo;0;L;;;;;N;;;;;
+13234;EGYPTIAN HIEROGLYPH NL019;Lo;0;L;;;;;N;;;;;
+13235;EGYPTIAN HIEROGLYPH NL020;Lo;0;L;;;;;N;;;;;
+13236;EGYPTIAN HIEROGLYPH NU001;Lo;0;L;;;;;N;;;;;
+13237;EGYPTIAN HIEROGLYPH NU002;Lo;0;L;;;;;N;;;;;
+13238;EGYPTIAN HIEROGLYPH NU003;Lo;0;L;;;;;N;;;;;
+13239;EGYPTIAN HIEROGLYPH NU004;Lo;0;L;;;;;N;;;;;
+1323A;EGYPTIAN HIEROGLYPH NU005;Lo;0;L;;;;;N;;;;;
+1323B;EGYPTIAN HIEROGLYPH NU006;Lo;0;L;;;;;N;;;;;
+1323C;EGYPTIAN HIEROGLYPH NU007;Lo;0;L;;;;;N;;;;;
+1323D;EGYPTIAN HIEROGLYPH NU008;Lo;0;L;;;;;N;;;;;
+1323E;EGYPTIAN HIEROGLYPH NU009;Lo;0;L;;;;;N;;;;;
+1323F;EGYPTIAN HIEROGLYPH NU010;Lo;0;L;;;;;N;;;;;
+13240;EGYPTIAN HIEROGLYPH NU010A;Lo;0;L;;;;;N;;;;;
+13241;EGYPTIAN HIEROGLYPH NU011;Lo;0;L;;;;;N;;;;;
+13242;EGYPTIAN HIEROGLYPH NU011A;Lo;0;L;;;;;N;;;;;
+13243;EGYPTIAN HIEROGLYPH NU012;Lo;0;L;;;;;N;;;;;
+13244;EGYPTIAN HIEROGLYPH NU013;Lo;0;L;;;;;N;;;;;
+13245;EGYPTIAN HIEROGLYPH NU014;Lo;0;L;;;;;N;;;;;
+13246;EGYPTIAN HIEROGLYPH NU015;Lo;0;L;;;;;N;;;;;
+13247;EGYPTIAN HIEROGLYPH NU016;Lo;0;L;;;;;N;;;;;
+13248;EGYPTIAN HIEROGLYPH NU017;Lo;0;L;;;;;N;;;;;
+13249;EGYPTIAN HIEROGLYPH NU018;Lo;0;L;;;;;N;;;;;
+1324A;EGYPTIAN HIEROGLYPH NU018A;Lo;0;L;;;;;N;;;;;
+1324B;EGYPTIAN HIEROGLYPH NU019;Lo;0;L;;;;;N;;;;;
+1324C;EGYPTIAN HIEROGLYPH NU020;Lo;0;L;;;;;N;;;;;
+1324D;EGYPTIAN HIEROGLYPH NU021;Lo;0;L;;;;;N;;;;;
+1324E;EGYPTIAN HIEROGLYPH NU022;Lo;0;L;;;;;N;;;;;
+1324F;EGYPTIAN HIEROGLYPH NU022A;Lo;0;L;;;;;N;;;;;
+13250;EGYPTIAN HIEROGLYPH O001;Lo;0;L;;;;;N;;;;;
+13251;EGYPTIAN HIEROGLYPH O001A;Lo;0;L;;;;;N;;;;;
+13252;EGYPTIAN HIEROGLYPH O002;Lo;0;L;;;;;N;;;;;
+13253;EGYPTIAN HIEROGLYPH O003;Lo;0;L;;;;;N;;;;;
+13254;EGYPTIAN HIEROGLYPH O004;Lo;0;L;;;;;N;;;;;
+13255;EGYPTIAN HIEROGLYPH O005;Lo;0;L;;;;;N;;;;;
+13256;EGYPTIAN HIEROGLYPH O005A;Lo;0;L;;;;;N;;;;;
+13257;EGYPTIAN HIEROGLYPH O006;Lo;0;L;;;;;N;;;;;
+13258;EGYPTIAN HIEROGLYPH O006A;Lo;0;L;;;;;N;;;;;
+13259;EGYPTIAN HIEROGLYPH O006B;Lo;0;L;;;;;N;;;;;
+1325A;EGYPTIAN HIEROGLYPH O006C;Lo;0;L;;;;;N;;;;;
+1325B;EGYPTIAN HIEROGLYPH O006D;Lo;0;L;;;;;N;;;;;
+1325C;EGYPTIAN HIEROGLYPH O006E;Lo;0;L;;;;;N;;;;;
+1325D;EGYPTIAN HIEROGLYPH O006F;Lo;0;L;;;;;N;;;;;
+1325E;EGYPTIAN HIEROGLYPH O007;Lo;0;L;;;;;N;;;;;
+1325F;EGYPTIAN HIEROGLYPH O008;Lo;0;L;;;;;N;;;;;
+13260;EGYPTIAN HIEROGLYPH O009;Lo;0;L;;;;;N;;;;;
+13261;EGYPTIAN HIEROGLYPH O010;Lo;0;L;;;;;N;;;;;
+13262;EGYPTIAN HIEROGLYPH O010A;Lo;0;L;;;;;N;;;;;
+13263;EGYPTIAN HIEROGLYPH O010B;Lo;0;L;;;;;N;;;;;
+13264;EGYPTIAN HIEROGLYPH O010C;Lo;0;L;;;;;N;;;;;
+13265;EGYPTIAN HIEROGLYPH O011;Lo;0;L;;;;;N;;;;;
+13266;EGYPTIAN HIEROGLYPH O012;Lo;0;L;;;;;N;;;;;
+13267;EGYPTIAN HIEROGLYPH O013;Lo;0;L;;;;;N;;;;;
+13268;EGYPTIAN HIEROGLYPH O014;Lo;0;L;;;;;N;;;;;
+13269;EGYPTIAN HIEROGLYPH O015;Lo;0;L;;;;;N;;;;;
+1326A;EGYPTIAN HIEROGLYPH O016;Lo;0;L;;;;;N;;;;;
+1326B;EGYPTIAN HIEROGLYPH O017;Lo;0;L;;;;;N;;;;;
+1326C;EGYPTIAN HIEROGLYPH O018;Lo;0;L;;;;;N;;;;;
+1326D;EGYPTIAN HIEROGLYPH O019;Lo;0;L;;;;;N;;;;;
+1326E;EGYPTIAN HIEROGLYPH O019A;Lo;0;L;;;;;N;;;;;
+1326F;EGYPTIAN HIEROGLYPH O020;Lo;0;L;;;;;N;;;;;
+13270;EGYPTIAN HIEROGLYPH O020A;Lo;0;L;;;;;N;;;;;
+13271;EGYPTIAN HIEROGLYPH O021;Lo;0;L;;;;;N;;;;;
+13272;EGYPTIAN HIEROGLYPH O022;Lo;0;L;;;;;N;;;;;
+13273;EGYPTIAN HIEROGLYPH O023;Lo;0;L;;;;;N;;;;;
+13274;EGYPTIAN HIEROGLYPH O024;Lo;0;L;;;;;N;;;;;
+13275;EGYPTIAN HIEROGLYPH O024A;Lo;0;L;;;;;N;;;;;
+13276;EGYPTIAN HIEROGLYPH O025;Lo;0;L;;;;;N;;;;;
+13277;EGYPTIAN HIEROGLYPH O025A;Lo;0;L;;;;;N;;;;;
+13278;EGYPTIAN HIEROGLYPH O026;Lo;0;L;;;;;N;;;;;
+13279;EGYPTIAN HIEROGLYPH O027;Lo;0;L;;;;;N;;;;;
+1327A;EGYPTIAN HIEROGLYPH O028;Lo;0;L;;;;;N;;;;;
+1327B;EGYPTIAN HIEROGLYPH O029;Lo;0;L;;;;;N;;;;;
+1327C;EGYPTIAN HIEROGLYPH O029A;Lo;0;L;;;;;N;;;;;
+1327D;EGYPTIAN HIEROGLYPH O030;Lo;0;L;;;;;N;;;;;
+1327E;EGYPTIAN HIEROGLYPH O030A;Lo;0;L;;;;;N;;;;;
+1327F;EGYPTIAN HIEROGLYPH O031;Lo;0;L;;;;;N;;;;;
+13280;EGYPTIAN HIEROGLYPH O032;Lo;0;L;;;;;N;;;;;
+13281;EGYPTIAN HIEROGLYPH O033;Lo;0;L;;;;;N;;;;;
+13282;EGYPTIAN HIEROGLYPH O033A;Lo;0;L;;;;;N;;;;;
+13283;EGYPTIAN HIEROGLYPH O034;Lo;0;L;;;;;N;;;;;
+13284;EGYPTIAN HIEROGLYPH O035;Lo;0;L;;;;;N;;;;;
+13285;EGYPTIAN HIEROGLYPH O036;Lo;0;L;;;;;N;;;;;
+13286;EGYPTIAN HIEROGLYPH O036A;Lo;0;L;;;;;N;;;;;
+13287;EGYPTIAN HIEROGLYPH O036B;Lo;0;L;;;;;N;;;;;
+13288;EGYPTIAN HIEROGLYPH O036C;Lo;0;L;;;;;N;;;;;
+13289;EGYPTIAN HIEROGLYPH O036D;Lo;0;L;;;;;N;;;;;
+1328A;EGYPTIAN HIEROGLYPH O037;Lo;0;L;;;;;N;;;;;
+1328B;EGYPTIAN HIEROGLYPH O038;Lo;0;L;;;;;N;;;;;
+1328C;EGYPTIAN HIEROGLYPH O039;Lo;0;L;;;;;N;;;;;
+1328D;EGYPTIAN HIEROGLYPH O040;Lo;0;L;;;;;N;;;;;
+1328E;EGYPTIAN HIEROGLYPH O041;Lo;0;L;;;;;N;;;;;
+1328F;EGYPTIAN HIEROGLYPH O042;Lo;0;L;;;;;N;;;;;
+13290;EGYPTIAN HIEROGLYPH O043;Lo;0;L;;;;;N;;;;;
+13291;EGYPTIAN HIEROGLYPH O044;Lo;0;L;;;;;N;;;;;
+13292;EGYPTIAN HIEROGLYPH O045;Lo;0;L;;;;;N;;;;;
+13293;EGYPTIAN HIEROGLYPH O046;Lo;0;L;;;;;N;;;;;
+13294;EGYPTIAN HIEROGLYPH O047;Lo;0;L;;;;;N;;;;;
+13295;EGYPTIAN HIEROGLYPH O048;Lo;0;L;;;;;N;;;;;
+13296;EGYPTIAN HIEROGLYPH O049;Lo;0;L;;;;;N;;;;;
+13297;EGYPTIAN HIEROGLYPH O050;Lo;0;L;;;;;N;;;;;
+13298;EGYPTIAN HIEROGLYPH O050A;Lo;0;L;;;;;N;;;;;
+13299;EGYPTIAN HIEROGLYPH O050B;Lo;0;L;;;;;N;;;;;
+1329A;EGYPTIAN HIEROGLYPH O051;Lo;0;L;;;;;N;;;;;
+1329B;EGYPTIAN HIEROGLYPH P001;Lo;0;L;;;;;N;;;;;
+1329C;EGYPTIAN HIEROGLYPH P001A;Lo;0;L;;;;;N;;;;;
+1329D;EGYPTIAN HIEROGLYPH P002;Lo;0;L;;;;;N;;;;;
+1329E;EGYPTIAN HIEROGLYPH P003;Lo;0;L;;;;;N;;;;;
+1329F;EGYPTIAN HIEROGLYPH P003A;Lo;0;L;;;;;N;;;;;
+132A0;EGYPTIAN HIEROGLYPH P004;Lo;0;L;;;;;N;;;;;
+132A1;EGYPTIAN HIEROGLYPH P005;Lo;0;L;;;;;N;;;;;
+132A2;EGYPTIAN HIEROGLYPH P006;Lo;0;L;;;;;N;;;;;
+132A3;EGYPTIAN HIEROGLYPH P007;Lo;0;L;;;;;N;;;;;
+132A4;EGYPTIAN HIEROGLYPH P008;Lo;0;L;;;;;N;;;;;
+132A5;EGYPTIAN HIEROGLYPH P009;Lo;0;L;;;;;N;;;;;
+132A6;EGYPTIAN HIEROGLYPH P010;Lo;0;L;;;;;N;;;;;
+132A7;EGYPTIAN HIEROGLYPH P011;Lo;0;L;;;;;N;;;;;
+132A8;EGYPTIAN HIEROGLYPH Q001;Lo;0;L;;;;;N;;;;;
+132A9;EGYPTIAN HIEROGLYPH Q002;Lo;0;L;;;;;N;;;;;
+132AA;EGYPTIAN HIEROGLYPH Q003;Lo;0;L;;;;;N;;;;;
+132AB;EGYPTIAN HIEROGLYPH Q004;Lo;0;L;;;;;N;;;;;
+132AC;EGYPTIAN HIEROGLYPH Q005;Lo;0;L;;;;;N;;;;;
+132AD;EGYPTIAN HIEROGLYPH Q006;Lo;0;L;;;;;N;;;;;
+132AE;EGYPTIAN HIEROGLYPH Q007;Lo;0;L;;;;;N;;;;;
+132AF;EGYPTIAN HIEROGLYPH R001;Lo;0;L;;;;;N;;;;;
+132B0;EGYPTIAN HIEROGLYPH R002;Lo;0;L;;;;;N;;;;;
+132B1;EGYPTIAN HIEROGLYPH R002A;Lo;0;L;;;;;N;;;;;
+132B2;EGYPTIAN HIEROGLYPH R003;Lo;0;L;;;;;N;;;;;
+132B3;EGYPTIAN HIEROGLYPH R003A;Lo;0;L;;;;;N;;;;;
+132B4;EGYPTIAN HIEROGLYPH R003B;Lo;0;L;;;;;N;;;;;
+132B5;EGYPTIAN HIEROGLYPH R004;Lo;0;L;;;;;N;;;;;
+132B6;EGYPTIAN HIEROGLYPH R005;Lo;0;L;;;;;N;;;;;
+132B7;EGYPTIAN HIEROGLYPH R006;Lo;0;L;;;;;N;;;;;
+132B8;EGYPTIAN HIEROGLYPH R007;Lo;0;L;;;;;N;;;;;
+132B9;EGYPTIAN HIEROGLYPH R008;Lo;0;L;;;;;N;;;;;
+132BA;EGYPTIAN HIEROGLYPH R009;Lo;0;L;;;;;N;;;;;
+132BB;EGYPTIAN HIEROGLYPH R010;Lo;0;L;;;;;N;;;;;
+132BC;EGYPTIAN HIEROGLYPH R010A;Lo;0;L;;;;;N;;;;;
+132BD;EGYPTIAN HIEROGLYPH R011;Lo;0;L;;;;;N;;;;;
+132BE;EGYPTIAN HIEROGLYPH R012;Lo;0;L;;;;;N;;;;;
+132BF;EGYPTIAN HIEROGLYPH R013;Lo;0;L;;;;;N;;;;;
+132C0;EGYPTIAN HIEROGLYPH R014;Lo;0;L;;;;;N;;;;;
+132C1;EGYPTIAN HIEROGLYPH R015;Lo;0;L;;;;;N;;;;;
+132C2;EGYPTIAN HIEROGLYPH R016;Lo;0;L;;;;;N;;;;;
+132C3;EGYPTIAN HIEROGLYPH R016A;Lo;0;L;;;;;N;;;;;
+132C4;EGYPTIAN HIEROGLYPH R017;Lo;0;L;;;;;N;;;;;
+132C5;EGYPTIAN HIEROGLYPH R018;Lo;0;L;;;;;N;;;;;
+132C6;EGYPTIAN HIEROGLYPH R019;Lo;0;L;;;;;N;;;;;
+132C7;EGYPTIAN HIEROGLYPH R020;Lo;0;L;;;;;N;;;;;
+132C8;EGYPTIAN HIEROGLYPH R021;Lo;0;L;;;;;N;;;;;
+132C9;EGYPTIAN HIEROGLYPH R022;Lo;0;L;;;;;N;;;;;
+132CA;EGYPTIAN HIEROGLYPH R023;Lo;0;L;;;;;N;;;;;
+132CB;EGYPTIAN HIEROGLYPH R024;Lo;0;L;;;;;N;;;;;
+132CC;EGYPTIAN HIEROGLYPH R025;Lo;0;L;;;;;N;;;;;
+132CD;EGYPTIAN HIEROGLYPH R026;Lo;0;L;;;;;N;;;;;
+132CE;EGYPTIAN HIEROGLYPH R027;Lo;0;L;;;;;N;;;;;
+132CF;EGYPTIAN HIEROGLYPH R028;Lo;0;L;;;;;N;;;;;
+132D0;EGYPTIAN HIEROGLYPH R029;Lo;0;L;;;;;N;;;;;
+132D1;EGYPTIAN HIEROGLYPH S001;Lo;0;L;;;;;N;;;;;
+132D2;EGYPTIAN HIEROGLYPH S002;Lo;0;L;;;;;N;;;;;
+132D3;EGYPTIAN HIEROGLYPH S002A;Lo;0;L;;;;;N;;;;;
+132D4;EGYPTIAN HIEROGLYPH S003;Lo;0;L;;;;;N;;;;;
+132D5;EGYPTIAN HIEROGLYPH S004;Lo;0;L;;;;;N;;;;;
+132D6;EGYPTIAN HIEROGLYPH S005;Lo;0;L;;;;;N;;;;;
+132D7;EGYPTIAN HIEROGLYPH S006;Lo;0;L;;;;;N;;;;;
+132D8;EGYPTIAN HIEROGLYPH S006A;Lo;0;L;;;;;N;;;;;
+132D9;EGYPTIAN HIEROGLYPH S007;Lo;0;L;;;;;N;;;;;
+132DA;EGYPTIAN HIEROGLYPH S008;Lo;0;L;;;;;N;;;;;
+132DB;EGYPTIAN HIEROGLYPH S009;Lo;0;L;;;;;N;;;;;
+132DC;EGYPTIAN HIEROGLYPH S010;Lo;0;L;;;;;N;;;;;
+132DD;EGYPTIAN HIEROGLYPH S011;Lo;0;L;;;;;N;;;;;
+132DE;EGYPTIAN HIEROGLYPH S012;Lo;0;L;;;;;N;;;;;
+132DF;EGYPTIAN HIEROGLYPH S013;Lo;0;L;;;;;N;;;;;
+132E0;EGYPTIAN HIEROGLYPH S014;Lo;0;L;;;;;N;;;;;
+132E1;EGYPTIAN HIEROGLYPH S014A;Lo;0;L;;;;;N;;;;;
+132E2;EGYPTIAN HIEROGLYPH S014B;Lo;0;L;;;;;N;;;;;
+132E3;EGYPTIAN HIEROGLYPH S015;Lo;0;L;;;;;N;;;;;
+132E4;EGYPTIAN HIEROGLYPH S016;Lo;0;L;;;;;N;;;;;
+132E5;EGYPTIAN HIEROGLYPH S017;Lo;0;L;;;;;N;;;;;
+132E6;EGYPTIAN HIEROGLYPH S017A;Lo;0;L;;;;;N;;;;;
+132E7;EGYPTIAN HIEROGLYPH S018;Lo;0;L;;;;;N;;;;;
+132E8;EGYPTIAN HIEROGLYPH S019;Lo;0;L;;;;;N;;;;;
+132E9;EGYPTIAN HIEROGLYPH S020;Lo;0;L;;;;;N;;;;;
+132EA;EGYPTIAN HIEROGLYPH S021;Lo;0;L;;;;;N;;;;;
+132EB;EGYPTIAN HIEROGLYPH S022;Lo;0;L;;;;;N;;;;;
+132EC;EGYPTIAN HIEROGLYPH S023;Lo;0;L;;;;;N;;;;;
+132ED;EGYPTIAN HIEROGLYPH S024;Lo;0;L;;;;;N;;;;;
+132EE;EGYPTIAN HIEROGLYPH S025;Lo;0;L;;;;;N;;;;;
+132EF;EGYPTIAN HIEROGLYPH S026;Lo;0;L;;;;;N;;;;;
+132F0;EGYPTIAN HIEROGLYPH S026A;Lo;0;L;;;;;N;;;;;
+132F1;EGYPTIAN HIEROGLYPH S026B;Lo;0;L;;;;;N;;;;;
+132F2;EGYPTIAN HIEROGLYPH S027;Lo;0;L;;;;;N;;;;;
+132F3;EGYPTIAN HIEROGLYPH S028;Lo;0;L;;;;;N;;;;;
+132F4;EGYPTIAN HIEROGLYPH S029;Lo;0;L;;;;;N;;;;;
+132F5;EGYPTIAN HIEROGLYPH S030;Lo;0;L;;;;;N;;;;;
+132F6;EGYPTIAN HIEROGLYPH S031;Lo;0;L;;;;;N;;;;;
+132F7;EGYPTIAN HIEROGLYPH S032;Lo;0;L;;;;;N;;;;;
+132F8;EGYPTIAN HIEROGLYPH S033;Lo;0;L;;;;;N;;;;;
+132F9;EGYPTIAN HIEROGLYPH S034;Lo;0;L;;;;;N;;;;;
+132FA;EGYPTIAN HIEROGLYPH S035;Lo;0;L;;;;;N;;;;;
+132FB;EGYPTIAN HIEROGLYPH S035A;Lo;0;L;;;;;N;;;;;
+132FC;EGYPTIAN HIEROGLYPH S036;Lo;0;L;;;;;N;;;;;
+132FD;EGYPTIAN HIEROGLYPH S037;Lo;0;L;;;;;N;;;;;
+132FE;EGYPTIAN HIEROGLYPH S038;Lo;0;L;;;;;N;;;;;
+132FF;EGYPTIAN HIEROGLYPH S039;Lo;0;L;;;;;N;;;;;
+13300;EGYPTIAN HIEROGLYPH S040;Lo;0;L;;;;;N;;;;;
+13301;EGYPTIAN HIEROGLYPH S041;Lo;0;L;;;;;N;;;;;
+13302;EGYPTIAN HIEROGLYPH S042;Lo;0;L;;;;;N;;;;;
+13303;EGYPTIAN HIEROGLYPH S043;Lo;0;L;;;;;N;;;;;
+13304;EGYPTIAN HIEROGLYPH S044;Lo;0;L;;;;;N;;;;;
+13305;EGYPTIAN HIEROGLYPH S045;Lo;0;L;;;;;N;;;;;
+13306;EGYPTIAN HIEROGLYPH S046;Lo;0;L;;;;;N;;;;;
+13307;EGYPTIAN HIEROGLYPH T001;Lo;0;L;;;;;N;;;;;
+13308;EGYPTIAN HIEROGLYPH T002;Lo;0;L;;;;;N;;;;;
+13309;EGYPTIAN HIEROGLYPH T003;Lo;0;L;;;;;N;;;;;
+1330A;EGYPTIAN HIEROGLYPH T003A;Lo;0;L;;;;;N;;;;;
+1330B;EGYPTIAN HIEROGLYPH T004;Lo;0;L;;;;;N;;;;;
+1330C;EGYPTIAN HIEROGLYPH T005;Lo;0;L;;;;;N;;;;;
+1330D;EGYPTIAN HIEROGLYPH T006;Lo;0;L;;;;;N;;;;;
+1330E;EGYPTIAN HIEROGLYPH T007;Lo;0;L;;;;;N;;;;;
+1330F;EGYPTIAN HIEROGLYPH T007A;Lo;0;L;;;;;N;;;;;
+13310;EGYPTIAN HIEROGLYPH T008;Lo;0;L;;;;;N;;;;;
+13311;EGYPTIAN HIEROGLYPH T008A;Lo;0;L;;;;;N;;;;;
+13312;EGYPTIAN HIEROGLYPH T009;Lo;0;L;;;;;N;;;;;
+13313;EGYPTIAN HIEROGLYPH T009A;Lo;0;L;;;;;N;;;;;
+13314;EGYPTIAN HIEROGLYPH T010;Lo;0;L;;;;;N;;;;;
+13315;EGYPTIAN HIEROGLYPH T011;Lo;0;L;;;;;N;;;;;
+13316;EGYPTIAN HIEROGLYPH T011A;Lo;0;L;;;;;N;;;;;
+13317;EGYPTIAN HIEROGLYPH T012;Lo;0;L;;;;;N;;;;;
+13318;EGYPTIAN HIEROGLYPH T013;Lo;0;L;;;;;N;;;;;
+13319;EGYPTIAN HIEROGLYPH T014;Lo;0;L;;;;;N;;;;;
+1331A;EGYPTIAN HIEROGLYPH T015;Lo;0;L;;;;;N;;;;;
+1331B;EGYPTIAN HIEROGLYPH T016;Lo;0;L;;;;;N;;;;;
+1331C;EGYPTIAN HIEROGLYPH T016A;Lo;0;L;;;;;N;;;;;
+1331D;EGYPTIAN HIEROGLYPH T017;Lo;0;L;;;;;N;;;;;
+1331E;EGYPTIAN HIEROGLYPH T018;Lo;0;L;;;;;N;;;;;
+1331F;EGYPTIAN HIEROGLYPH T019;Lo;0;L;;;;;N;;;;;
+13320;EGYPTIAN HIEROGLYPH T020;Lo;0;L;;;;;N;;;;;
+13321;EGYPTIAN HIEROGLYPH T021;Lo;0;L;;;;;N;;;;;
+13322;EGYPTIAN HIEROGLYPH T022;Lo;0;L;;;;;N;;;;;
+13323;EGYPTIAN HIEROGLYPH T023;Lo;0;L;;;;;N;;;;;
+13324;EGYPTIAN HIEROGLYPH T024;Lo;0;L;;;;;N;;;;;
+13325;EGYPTIAN HIEROGLYPH T025;Lo;0;L;;;;;N;;;;;
+13326;EGYPTIAN HIEROGLYPH T026;Lo;0;L;;;;;N;;;;;
+13327;EGYPTIAN HIEROGLYPH T027;Lo;0;L;;;;;N;;;;;
+13328;EGYPTIAN HIEROGLYPH T028;Lo;0;L;;;;;N;;;;;
+13329;EGYPTIAN HIEROGLYPH T029;Lo;0;L;;;;;N;;;;;
+1332A;EGYPTIAN HIEROGLYPH T030;Lo;0;L;;;;;N;;;;;
+1332B;EGYPTIAN HIEROGLYPH T031;Lo;0;L;;;;;N;;;;;
+1332C;EGYPTIAN HIEROGLYPH T032;Lo;0;L;;;;;N;;;;;
+1332D;EGYPTIAN HIEROGLYPH T032A;Lo;0;L;;;;;N;;;;;
+1332E;EGYPTIAN HIEROGLYPH T033;Lo;0;L;;;;;N;;;;;
+1332F;EGYPTIAN HIEROGLYPH T033A;Lo;0;L;;;;;N;;;;;
+13330;EGYPTIAN HIEROGLYPH T034;Lo;0;L;;;;;N;;;;;
+13331;EGYPTIAN HIEROGLYPH T035;Lo;0;L;;;;;N;;;;;
+13332;EGYPTIAN HIEROGLYPH T036;Lo;0;L;;;;;N;;;;;
+13333;EGYPTIAN HIEROGLYPH U001;Lo;0;L;;;;;N;;;;;
+13334;EGYPTIAN HIEROGLYPH U002;Lo;0;L;;;;;N;;;;;
+13335;EGYPTIAN HIEROGLYPH U003;Lo;0;L;;;;;N;;;;;
+13336;EGYPTIAN HIEROGLYPH U004;Lo;0;L;;;;;N;;;;;
+13337;EGYPTIAN HIEROGLYPH U005;Lo;0;L;;;;;N;;;;;
+13338;EGYPTIAN HIEROGLYPH U006;Lo;0;L;;;;;N;;;;;
+13339;EGYPTIAN HIEROGLYPH U006A;Lo;0;L;;;;;N;;;;;
+1333A;EGYPTIAN HIEROGLYPH U006B;Lo;0;L;;;;;N;;;;;
+1333B;EGYPTIAN HIEROGLYPH U007;Lo;0;L;;;;;N;;;;;
+1333C;EGYPTIAN HIEROGLYPH U008;Lo;0;L;;;;;N;;;;;
+1333D;EGYPTIAN HIEROGLYPH U009;Lo;0;L;;;;;N;;;;;
+1333E;EGYPTIAN HIEROGLYPH U010;Lo;0;L;;;;;N;;;;;
+1333F;EGYPTIAN HIEROGLYPH U011;Lo;0;L;;;;;N;;;;;
+13340;EGYPTIAN HIEROGLYPH U012;Lo;0;L;;;;;N;;;;;
+13341;EGYPTIAN HIEROGLYPH U013;Lo;0;L;;;;;N;;;;;
+13342;EGYPTIAN HIEROGLYPH U014;Lo;0;L;;;;;N;;;;;
+13343;EGYPTIAN HIEROGLYPH U015;Lo;0;L;;;;;N;;;;;
+13344;EGYPTIAN HIEROGLYPH U016;Lo;0;L;;;;;N;;;;;
+13345;EGYPTIAN HIEROGLYPH U017;Lo;0;L;;;;;N;;;;;
+13346;EGYPTIAN HIEROGLYPH U018;Lo;0;L;;;;;N;;;;;
+13347;EGYPTIAN HIEROGLYPH U019;Lo;0;L;;;;;N;;;;;
+13348;EGYPTIAN HIEROGLYPH U020;Lo;0;L;;;;;N;;;;;
+13349;EGYPTIAN HIEROGLYPH U021;Lo;0;L;;;;;N;;;;;
+1334A;EGYPTIAN HIEROGLYPH U022;Lo;0;L;;;;;N;;;;;
+1334B;EGYPTIAN HIEROGLYPH U023;Lo;0;L;;;;;N;;;;;
+1334C;EGYPTIAN HIEROGLYPH U023A;Lo;0;L;;;;;N;;;;;
+1334D;EGYPTIAN HIEROGLYPH U024;Lo;0;L;;;;;N;;;;;
+1334E;EGYPTIAN HIEROGLYPH U025;Lo;0;L;;;;;N;;;;;
+1334F;EGYPTIAN HIEROGLYPH U026;Lo;0;L;;;;;N;;;;;
+13350;EGYPTIAN HIEROGLYPH U027;Lo;0;L;;;;;N;;;;;
+13351;EGYPTIAN HIEROGLYPH U028;Lo;0;L;;;;;N;;;;;
+13352;EGYPTIAN HIEROGLYPH U029;Lo;0;L;;;;;N;;;;;
+13353;EGYPTIAN HIEROGLYPH U029A;Lo;0;L;;;;;N;;;;;
+13354;EGYPTIAN HIEROGLYPH U030;Lo;0;L;;;;;N;;;;;
+13355;EGYPTIAN HIEROGLYPH U031;Lo;0;L;;;;;N;;;;;
+13356;EGYPTIAN HIEROGLYPH U032;Lo;0;L;;;;;N;;;;;
+13357;EGYPTIAN HIEROGLYPH U032A;Lo;0;L;;;;;N;;;;;
+13358;EGYPTIAN HIEROGLYPH U033;Lo;0;L;;;;;N;;;;;
+13359;EGYPTIAN HIEROGLYPH U034;Lo;0;L;;;;;N;;;;;
+1335A;EGYPTIAN HIEROGLYPH U035;Lo;0;L;;;;;N;;;;;
+1335B;EGYPTIAN HIEROGLYPH U036;Lo;0;L;;;;;N;;;;;
+1335C;EGYPTIAN HIEROGLYPH U037;Lo;0;L;;;;;N;;;;;
+1335D;EGYPTIAN HIEROGLYPH U038;Lo;0;L;;;;;N;;;;;
+1335E;EGYPTIAN HIEROGLYPH U039;Lo;0;L;;;;;N;;;;;
+1335F;EGYPTIAN HIEROGLYPH U040;Lo;0;L;;;;;N;;;;;
+13360;EGYPTIAN HIEROGLYPH U041;Lo;0;L;;;;;N;;;;;
+13361;EGYPTIAN HIEROGLYPH U042;Lo;0;L;;;;;N;;;;;
+13362;EGYPTIAN HIEROGLYPH V001;Lo;0;L;;;;;N;;;;;
+13363;EGYPTIAN HIEROGLYPH V001A;Lo;0;L;;;;;N;;;;;
+13364;EGYPTIAN HIEROGLYPH V001B;Lo;0;L;;;;;N;;;;;
+13365;EGYPTIAN HIEROGLYPH V001C;Lo;0;L;;;;;N;;;;;
+13366;EGYPTIAN HIEROGLYPH V001D;Lo;0;L;;;;;N;;;;;
+13367;EGYPTIAN HIEROGLYPH V001E;Lo;0;L;;;;;N;;;;;
+13368;EGYPTIAN HIEROGLYPH V001F;Lo;0;L;;;;;N;;;;;
+13369;EGYPTIAN HIEROGLYPH V001G;Lo;0;L;;;;;N;;;;;
+1336A;EGYPTIAN HIEROGLYPH V001H;Lo;0;L;;;;;N;;;;;
+1336B;EGYPTIAN HIEROGLYPH V001I;Lo;0;L;;;;;N;;;;;
+1336C;EGYPTIAN HIEROGLYPH V002;Lo;0;L;;;;;N;;;;;
+1336D;EGYPTIAN HIEROGLYPH V002A;Lo;0;L;;;;;N;;;;;
+1336E;EGYPTIAN HIEROGLYPH V003;Lo;0;L;;;;;N;;;;;
+1336F;EGYPTIAN HIEROGLYPH V004;Lo;0;L;;;;;N;;;;;
+13370;EGYPTIAN HIEROGLYPH V005;Lo;0;L;;;;;N;;;;;
+13371;EGYPTIAN HIEROGLYPH V006;Lo;0;L;;;;;N;;;;;
+13372;EGYPTIAN HIEROGLYPH V007;Lo;0;L;;;;;N;;;;;
+13373;EGYPTIAN HIEROGLYPH V007A;Lo;0;L;;;;;N;;;;;
+13374;EGYPTIAN HIEROGLYPH V007B;Lo;0;L;;;;;N;;;;;
+13375;EGYPTIAN HIEROGLYPH V008;Lo;0;L;;;;;N;;;;;
+13376;EGYPTIAN HIEROGLYPH V009;Lo;0;L;;;;;N;;;;;
+13377;EGYPTIAN HIEROGLYPH V010;Lo;0;L;;;;;N;;;;;
+13378;EGYPTIAN HIEROGLYPH V011;Lo;0;L;;;;;N;;;;;
+13379;EGYPTIAN HIEROGLYPH V011A;Lo;0;L;;;;;N;;;;;
+1337A;EGYPTIAN HIEROGLYPH V011B;Lo;0;L;;;;;N;;;;;
+1337B;EGYPTIAN HIEROGLYPH V011C;Lo;0;L;;;;;N;;;;;
+1337C;EGYPTIAN HIEROGLYPH V012;Lo;0;L;;;;;N;;;;;
+1337D;EGYPTIAN HIEROGLYPH V012A;Lo;0;L;;;;;N;;;;;
+1337E;EGYPTIAN HIEROGLYPH V012B;Lo;0;L;;;;;N;;;;;
+1337F;EGYPTIAN HIEROGLYPH V013;Lo;0;L;;;;;N;;;;;
+13380;EGYPTIAN HIEROGLYPH V014;Lo;0;L;;;;;N;;;;;
+13381;EGYPTIAN HIEROGLYPH V015;Lo;0;L;;;;;N;;;;;
+13382;EGYPTIAN HIEROGLYPH V016;Lo;0;L;;;;;N;;;;;
+13383;EGYPTIAN HIEROGLYPH V017;Lo;0;L;;;;;N;;;;;
+13384;EGYPTIAN HIEROGLYPH V018;Lo;0;L;;;;;N;;;;;
+13385;EGYPTIAN HIEROGLYPH V019;Lo;0;L;;;;;N;;;;;
+13386;EGYPTIAN HIEROGLYPH V020;Lo;0;L;;;;;N;;;;;
+13387;EGYPTIAN HIEROGLYPH V020A;Lo;0;L;;;;;N;;;;;
+13388;EGYPTIAN HIEROGLYPH V020B;Lo;0;L;;;;;N;;;;;
+13389;EGYPTIAN HIEROGLYPH V020C;Lo;0;L;;;;;N;;;;;
+1338A;EGYPTIAN HIEROGLYPH V020D;Lo;0;L;;;;;N;;;;;
+1338B;EGYPTIAN HIEROGLYPH V020E;Lo;0;L;;;;;N;;;;;
+1338C;EGYPTIAN HIEROGLYPH V020F;Lo;0;L;;;;;N;;;;;
+1338D;EGYPTIAN HIEROGLYPH V020G;Lo;0;L;;;;;N;;;;;
+1338E;EGYPTIAN HIEROGLYPH V020H;Lo;0;L;;;;;N;;;;;
+1338F;EGYPTIAN HIEROGLYPH V020I;Lo;0;L;;;;;N;;;;;
+13390;EGYPTIAN HIEROGLYPH V020J;Lo;0;L;;;;;N;;;;;
+13391;EGYPTIAN HIEROGLYPH V020K;Lo;0;L;;;;;N;;;;;
+13392;EGYPTIAN HIEROGLYPH V020L;Lo;0;L;;;;;N;;;;;
+13393;EGYPTIAN HIEROGLYPH V021;Lo;0;L;;;;;N;;;;;
+13394;EGYPTIAN HIEROGLYPH V022;Lo;0;L;;;;;N;;;;;
+13395;EGYPTIAN HIEROGLYPH V023;Lo;0;L;;;;;N;;;;;
+13396;EGYPTIAN HIEROGLYPH V023A;Lo;0;L;;;;;N;;;;;
+13397;EGYPTIAN HIEROGLYPH V024;Lo;0;L;;;;;N;;;;;
+13398;EGYPTIAN HIEROGLYPH V025;Lo;0;L;;;;;N;;;;;
+13399;EGYPTIAN HIEROGLYPH V026;Lo;0;L;;;;;N;;;;;
+1339A;EGYPTIAN HIEROGLYPH V027;Lo;0;L;;;;;N;;;;;
+1339B;EGYPTIAN HIEROGLYPH V028;Lo;0;L;;;;;N;;;;;
+1339C;EGYPTIAN HIEROGLYPH V028A;Lo;0;L;;;;;N;;;;;
+1339D;EGYPTIAN HIEROGLYPH V029;Lo;0;L;;;;;N;;;;;
+1339E;EGYPTIAN HIEROGLYPH V029A;Lo;0;L;;;;;N;;;;;
+1339F;EGYPTIAN HIEROGLYPH V030;Lo;0;L;;;;;N;;;;;
+133A0;EGYPTIAN HIEROGLYPH V030A;Lo;0;L;;;;;N;;;;;
+133A1;EGYPTIAN HIEROGLYPH V031;Lo;0;L;;;;;N;;;;;
+133A2;EGYPTIAN HIEROGLYPH V031A;Lo;0;L;;;;;N;;;;;
+133A3;EGYPTIAN HIEROGLYPH V032;Lo;0;L;;;;;N;;;;;
+133A4;EGYPTIAN HIEROGLYPH V033;Lo;0;L;;;;;N;;;;;
+133A5;EGYPTIAN HIEROGLYPH V033A;Lo;0;L;;;;;N;;;;;
+133A6;EGYPTIAN HIEROGLYPH V034;Lo;0;L;;;;;N;;;;;
+133A7;EGYPTIAN HIEROGLYPH V035;Lo;0;L;;;;;N;;;;;
+133A8;EGYPTIAN HIEROGLYPH V036;Lo;0;L;;;;;N;;;;;
+133A9;EGYPTIAN HIEROGLYPH V037;Lo;0;L;;;;;N;;;;;
+133AA;EGYPTIAN HIEROGLYPH V037A;Lo;0;L;;;;;N;;;;;
+133AB;EGYPTIAN HIEROGLYPH V038;Lo;0;L;;;;;N;;;;;
+133AC;EGYPTIAN HIEROGLYPH V039;Lo;0;L;;;;;N;;;;;
+133AD;EGYPTIAN HIEROGLYPH V040;Lo;0;L;;;;;N;;;;;
+133AE;EGYPTIAN HIEROGLYPH V040A;Lo;0;L;;;;;N;;;;;
+133AF;EGYPTIAN HIEROGLYPH W001;Lo;0;L;;;;;N;;;;;
+133B0;EGYPTIAN HIEROGLYPH W002;Lo;0;L;;;;;N;;;;;
+133B1;EGYPTIAN HIEROGLYPH W003;Lo;0;L;;;;;N;;;;;
+133B2;EGYPTIAN HIEROGLYPH W003A;Lo;0;L;;;;;N;;;;;
+133B3;EGYPTIAN HIEROGLYPH W004;Lo;0;L;;;;;N;;;;;
+133B4;EGYPTIAN HIEROGLYPH W005;Lo;0;L;;;;;N;;;;;
+133B5;EGYPTIAN HIEROGLYPH W006;Lo;0;L;;;;;N;;;;;
+133B6;EGYPTIAN HIEROGLYPH W007;Lo;0;L;;;;;N;;;;;
+133B7;EGYPTIAN HIEROGLYPH W008;Lo;0;L;;;;;N;;;;;
+133B8;EGYPTIAN HIEROGLYPH W009;Lo;0;L;;;;;N;;;;;
+133B9;EGYPTIAN HIEROGLYPH W009A;Lo;0;L;;;;;N;;;;;
+133BA;EGYPTIAN HIEROGLYPH W010;Lo;0;L;;;;;N;;;;;
+133BB;EGYPTIAN HIEROGLYPH W010A;Lo;0;L;;;;;N;;;;;
+133BC;EGYPTIAN HIEROGLYPH W011;Lo;0;L;;;;;N;;;;;
+133BD;EGYPTIAN HIEROGLYPH W012;Lo;0;L;;;;;N;;;;;
+133BE;EGYPTIAN HIEROGLYPH W013;Lo;0;L;;;;;N;;;;;
+133BF;EGYPTIAN HIEROGLYPH W014;Lo;0;L;;;;;N;;;;;
+133C0;EGYPTIAN HIEROGLYPH W014A;Lo;0;L;;;;;N;;;;;
+133C1;EGYPTIAN HIEROGLYPH W015;Lo;0;L;;;;;N;;;;;
+133C2;EGYPTIAN HIEROGLYPH W016;Lo;0;L;;;;;N;;;;;
+133C3;EGYPTIAN HIEROGLYPH W017;Lo;0;L;;;;;N;;;;;
+133C4;EGYPTIAN HIEROGLYPH W017A;Lo;0;L;;;;;N;;;;;
+133C5;EGYPTIAN HIEROGLYPH W018;Lo;0;L;;;;;N;;;;;
+133C6;EGYPTIAN HIEROGLYPH W018A;Lo;0;L;;;;;N;;;;;
+133C7;EGYPTIAN HIEROGLYPH W019;Lo;0;L;;;;;N;;;;;
+133C8;EGYPTIAN HIEROGLYPH W020;Lo;0;L;;;;;N;;;;;
+133C9;EGYPTIAN HIEROGLYPH W021;Lo;0;L;;;;;N;;;;;
+133CA;EGYPTIAN HIEROGLYPH W022;Lo;0;L;;;;;N;;;;;
+133CB;EGYPTIAN HIEROGLYPH W023;Lo;0;L;;;;;N;;;;;
+133CC;EGYPTIAN HIEROGLYPH W024;Lo;0;L;;;;;N;;;;;
+133CD;EGYPTIAN HIEROGLYPH W024A;Lo;0;L;;;;;N;;;;;
+133CE;EGYPTIAN HIEROGLYPH W025;Lo;0;L;;;;;N;;;;;
+133CF;EGYPTIAN HIEROGLYPH X001;Lo;0;L;;;;;N;;;;;
+133D0;EGYPTIAN HIEROGLYPH X002;Lo;0;L;;;;;N;;;;;
+133D1;EGYPTIAN HIEROGLYPH X003;Lo;0;L;;;;;N;;;;;
+133D2;EGYPTIAN HIEROGLYPH X004;Lo;0;L;;;;;N;;;;;
+133D3;EGYPTIAN HIEROGLYPH X004A;Lo;0;L;;;;;N;;;;;
+133D4;EGYPTIAN HIEROGLYPH X004B;Lo;0;L;;;;;N;;;;;
+133D5;EGYPTIAN HIEROGLYPH X005;Lo;0;L;;;;;N;;;;;
+133D6;EGYPTIAN HIEROGLYPH X006;Lo;0;L;;;;;N;;;;;
+133D7;EGYPTIAN HIEROGLYPH X006A;Lo;0;L;;;;;N;;;;;
+133D8;EGYPTIAN HIEROGLYPH X007;Lo;0;L;;;;;N;;;;;
+133D9;EGYPTIAN HIEROGLYPH X008;Lo;0;L;;;;;N;;;;;
+133DA;EGYPTIAN HIEROGLYPH X008A;Lo;0;L;;;;;N;;;;;
+133DB;EGYPTIAN HIEROGLYPH Y001;Lo;0;L;;;;;N;;;;;
+133DC;EGYPTIAN HIEROGLYPH Y001A;Lo;0;L;;;;;N;;;;;
+133DD;EGYPTIAN HIEROGLYPH Y002;Lo;0;L;;;;;N;;;;;
+133DE;EGYPTIAN HIEROGLYPH Y003;Lo;0;L;;;;;N;;;;;
+133DF;EGYPTIAN HIEROGLYPH Y004;Lo;0;L;;;;;N;;;;;
+133E0;EGYPTIAN HIEROGLYPH Y005;Lo;0;L;;;;;N;;;;;
+133E1;EGYPTIAN HIEROGLYPH Y006;Lo;0;L;;;;;N;;;;;
+133E2;EGYPTIAN HIEROGLYPH Y007;Lo;0;L;;;;;N;;;;;
+133E3;EGYPTIAN HIEROGLYPH Y008;Lo;0;L;;;;;N;;;;;
+133E4;EGYPTIAN HIEROGLYPH Z001;Lo;0;L;;;;;N;;;;;
+133E5;EGYPTIAN HIEROGLYPH Z002;Lo;0;L;;;;;N;;;;;
+133E6;EGYPTIAN HIEROGLYPH Z002A;Lo;0;L;;;;;N;;;;;
+133E7;EGYPTIAN HIEROGLYPH Z002B;Lo;0;L;;;;;N;;;;;
+133E8;EGYPTIAN HIEROGLYPH Z002C;Lo;0;L;;;;;N;;;;;
+133E9;EGYPTIAN HIEROGLYPH Z002D;Lo;0;L;;;;;N;;;;;
+133EA;EGYPTIAN HIEROGLYPH Z003;Lo;0;L;;;;;N;;;;;
+133EB;EGYPTIAN HIEROGLYPH Z003A;Lo;0;L;;;;;N;;;;;
+133EC;EGYPTIAN HIEROGLYPH Z003B;Lo;0;L;;;;;N;;;;;
+133ED;EGYPTIAN HIEROGLYPH Z004;Lo;0;L;;;;;N;;;;;
+133EE;EGYPTIAN HIEROGLYPH Z004A;Lo;0;L;;;;;N;;;;;
+133EF;EGYPTIAN HIEROGLYPH Z005;Lo;0;L;;;;;N;;;;;
+133F0;EGYPTIAN HIEROGLYPH Z005A;Lo;0;L;;;;;N;;;;;
+133F1;EGYPTIAN HIEROGLYPH Z006;Lo;0;L;;;;;N;;;;;
+133F2;EGYPTIAN HIEROGLYPH Z007;Lo;0;L;;;;;N;;;;;
+133F3;EGYPTIAN HIEROGLYPH Z008;Lo;0;L;;;;;N;;;;;
+133F4;EGYPTIAN HIEROGLYPH Z009;Lo;0;L;;;;;N;;;;;
+133F5;EGYPTIAN HIEROGLYPH Z010;Lo;0;L;;;;;N;;;;;
+133F6;EGYPTIAN HIEROGLYPH Z011;Lo;0;L;;;;;N;;;;;
+133F7;EGYPTIAN HIEROGLYPH Z012;Lo;0;L;;;;;N;;;;;
+133F8;EGYPTIAN HIEROGLYPH Z013;Lo;0;L;;;;;N;;;;;
+133F9;EGYPTIAN HIEROGLYPH Z014;Lo;0;L;;;;;N;;;;;
+133FA;EGYPTIAN HIEROGLYPH Z015;Lo;0;L;;;;;N;;;;;
+133FB;EGYPTIAN HIEROGLYPH Z015A;Lo;0;L;;;;;N;;;;;
+133FC;EGYPTIAN HIEROGLYPH Z015B;Lo;0;L;;;;;N;;;;;
+133FD;EGYPTIAN HIEROGLYPH Z015C;Lo;0;L;;;;;N;;;;;
+133FE;EGYPTIAN HIEROGLYPH Z015D;Lo;0;L;;;;;N;;;;;
+133FF;EGYPTIAN HIEROGLYPH Z015E;Lo;0;L;;;;;N;;;;;
+13400;EGYPTIAN HIEROGLYPH Z015F;Lo;0;L;;;;;N;;;;;
+13401;EGYPTIAN HIEROGLYPH Z015G;Lo;0;L;;;;;N;;;;;
+13402;EGYPTIAN HIEROGLYPH Z015H;Lo;0;L;;;;;N;;;;;
+13403;EGYPTIAN HIEROGLYPH Z015I;Lo;0;L;;;;;N;;;;;
+13404;EGYPTIAN HIEROGLYPH Z016;Lo;0;L;;;;;N;;;;;
+13405;EGYPTIAN HIEROGLYPH Z016A;Lo;0;L;;;;;N;;;;;
+13406;EGYPTIAN HIEROGLYPH Z016B;Lo;0;L;;;;;N;;;;;
+13407;EGYPTIAN HIEROGLYPH Z016C;Lo;0;L;;;;;N;;;;;
+13408;EGYPTIAN HIEROGLYPH Z016D;Lo;0;L;;;;;N;;;;;
+13409;EGYPTIAN HIEROGLYPH Z016E;Lo;0;L;;;;;N;;;;;
+1340A;EGYPTIAN HIEROGLYPH Z016F;Lo;0;L;;;;;N;;;;;
+1340B;EGYPTIAN HIEROGLYPH Z016G;Lo;0;L;;;;;N;;;;;
+1340C;EGYPTIAN HIEROGLYPH Z016H;Lo;0;L;;;;;N;;;;;
+1340D;EGYPTIAN HIEROGLYPH AA001;Lo;0;L;;;;;N;;;;;
+1340E;EGYPTIAN HIEROGLYPH AA002;Lo;0;L;;;;;N;;;;;
+1340F;EGYPTIAN HIEROGLYPH AA003;Lo;0;L;;;;;N;;;;;
+13410;EGYPTIAN HIEROGLYPH AA004;Lo;0;L;;;;;N;;;;;
+13411;EGYPTIAN HIEROGLYPH AA005;Lo;0;L;;;;;N;;;;;
+13412;EGYPTIAN HIEROGLYPH AA006;Lo;0;L;;;;;N;;;;;
+13413;EGYPTIAN HIEROGLYPH AA007;Lo;0;L;;;;;N;;;;;
+13414;EGYPTIAN HIEROGLYPH AA007A;Lo;0;L;;;;;N;;;;;
+13415;EGYPTIAN HIEROGLYPH AA007B;Lo;0;L;;;;;N;;;;;
+13416;EGYPTIAN HIEROGLYPH AA008;Lo;0;L;;;;;N;;;;;
+13417;EGYPTIAN HIEROGLYPH AA009;Lo;0;L;;;;;N;;;;;
+13418;EGYPTIAN HIEROGLYPH AA010;Lo;0;L;;;;;N;;;;;
+13419;EGYPTIAN HIEROGLYPH AA011;Lo;0;L;;;;;N;;;;;
+1341A;EGYPTIAN HIEROGLYPH AA012;Lo;0;L;;;;;N;;;;;
+1341B;EGYPTIAN HIEROGLYPH AA013;Lo;0;L;;;;;N;;;;;
+1341C;EGYPTIAN HIEROGLYPH AA014;Lo;0;L;;;;;N;;;;;
+1341D;EGYPTIAN HIEROGLYPH AA015;Lo;0;L;;;;;N;;;;;
+1341E;EGYPTIAN HIEROGLYPH AA016;Lo;0;L;;;;;N;;;;;
+1341F;EGYPTIAN HIEROGLYPH AA017;Lo;0;L;;;;;N;;;;;
+13420;EGYPTIAN HIEROGLYPH AA018;Lo;0;L;;;;;N;;;;;
+13421;EGYPTIAN HIEROGLYPH AA019;Lo;0;L;;;;;N;;;;;
+13422;EGYPTIAN HIEROGLYPH AA020;Lo;0;L;;;;;N;;;;;
+13423;EGYPTIAN HIEROGLYPH AA021;Lo;0;L;;;;;N;;;;;
+13424;EGYPTIAN HIEROGLYPH AA022;Lo;0;L;;;;;N;;;;;
+13425;EGYPTIAN HIEROGLYPH AA023;Lo;0;L;;;;;N;;;;;
+13426;EGYPTIAN HIEROGLYPH AA024;Lo;0;L;;;;;N;;;;;
+13427;EGYPTIAN HIEROGLYPH AA025;Lo;0;L;;;;;N;;;;;
+13428;EGYPTIAN HIEROGLYPH AA026;Lo;0;L;;;;;N;;;;;
+13429;EGYPTIAN HIEROGLYPH AA027;Lo;0;L;;;;;N;;;;;
+1342A;EGYPTIAN HIEROGLYPH AA028;Lo;0;L;;;;;N;;;;;
+1342B;EGYPTIAN HIEROGLYPH AA029;Lo;0;L;;;;;N;;;;;
+1342C;EGYPTIAN HIEROGLYPH AA030;Lo;0;L;;;;;N;;;;;
+1342D;EGYPTIAN HIEROGLYPH AA031;Lo;0;L;;;;;N;;;;;
+1342E;EGYPTIAN HIEROGLYPH AA032;Lo;0;L;;;;;N;;;;;
+14400;ANATOLIAN HIEROGLYPH A001;Lo;0;L;;;;;N;;;;;
+14401;ANATOLIAN HIEROGLYPH A002;Lo;0;L;;;;;N;;;;;
+14402;ANATOLIAN HIEROGLYPH A003;Lo;0;L;;;;;N;;;;;
+14403;ANATOLIAN HIEROGLYPH A004;Lo;0;L;;;;;N;;;;;
+14404;ANATOLIAN HIEROGLYPH A005;Lo;0;L;;;;;N;;;;;
+14405;ANATOLIAN HIEROGLYPH A006;Lo;0;L;;;;;N;;;;;
+14406;ANATOLIAN HIEROGLYPH A007;Lo;0;L;;;;;N;;;;;
+14407;ANATOLIAN HIEROGLYPH A008;Lo;0;L;;;;;N;;;;;
+14408;ANATOLIAN HIEROGLYPH A009;Lo;0;L;;;;;N;;;;;
+14409;ANATOLIAN HIEROGLYPH A010;Lo;0;L;;;;;N;;;;;
+1440A;ANATOLIAN HIEROGLYPH A010A;Lo;0;L;;;;;N;;;;;
+1440B;ANATOLIAN HIEROGLYPH A011;Lo;0;L;;;;;N;;;;;
+1440C;ANATOLIAN HIEROGLYPH A012;Lo;0;L;;;;;N;;;;;
+1440D;ANATOLIAN HIEROGLYPH A013;Lo;0;L;;;;;N;;;;;
+1440E;ANATOLIAN HIEROGLYPH A014;Lo;0;L;;;;;N;;;;;
+1440F;ANATOLIAN HIEROGLYPH A015;Lo;0;L;;;;;N;;;;;
+14410;ANATOLIAN HIEROGLYPH A016;Lo;0;L;;;;;N;;;;;
+14411;ANATOLIAN HIEROGLYPH A017;Lo;0;L;;;;;N;;;;;
+14412;ANATOLIAN HIEROGLYPH A018;Lo;0;L;;;;;N;;;;;
+14413;ANATOLIAN HIEROGLYPH A019;Lo;0;L;;;;;N;;;;;
+14414;ANATOLIAN HIEROGLYPH A020;Lo;0;L;;;;;N;;;;;
+14415;ANATOLIAN HIEROGLYPH A021;Lo;0;L;;;;;N;;;;;
+14416;ANATOLIAN HIEROGLYPH A022;Lo;0;L;;;;;N;;;;;
+14417;ANATOLIAN HIEROGLYPH A023;Lo;0;L;;;;;N;;;;;
+14418;ANATOLIAN HIEROGLYPH A024;Lo;0;L;;;;;N;;;;;
+14419;ANATOLIAN HIEROGLYPH A025;Lo;0;L;;;;;N;;;;;
+1441A;ANATOLIAN HIEROGLYPH A026;Lo;0;L;;;;;N;;;;;
+1441B;ANATOLIAN HIEROGLYPH A026A;Lo;0;L;;;;;N;;;;;
+1441C;ANATOLIAN HIEROGLYPH A027;Lo;0;L;;;;;N;;;;;
+1441D;ANATOLIAN HIEROGLYPH A028;Lo;0;L;;;;;N;;;;;
+1441E;ANATOLIAN HIEROGLYPH A029;Lo;0;L;;;;;N;;;;;
+1441F;ANATOLIAN HIEROGLYPH A030;Lo;0;L;;;;;N;;;;;
+14420;ANATOLIAN HIEROGLYPH A031;Lo;0;L;;;;;N;;;;;
+14421;ANATOLIAN HIEROGLYPH A032;Lo;0;L;;;;;N;;;;;
+14422;ANATOLIAN HIEROGLYPH A033;Lo;0;L;;;;;N;;;;;
+14423;ANATOLIAN HIEROGLYPH A034;Lo;0;L;;;;;N;;;;;
+14424;ANATOLIAN HIEROGLYPH A035;Lo;0;L;;;;;N;;;;;
+14425;ANATOLIAN HIEROGLYPH A036;Lo;0;L;;;;;N;;;;;
+14426;ANATOLIAN HIEROGLYPH A037;Lo;0;L;;;;;N;;;;;
+14427;ANATOLIAN HIEROGLYPH A038;Lo;0;L;;;;;N;;;;;
+14428;ANATOLIAN HIEROGLYPH A039;Lo;0;L;;;;;N;;;;;
+14429;ANATOLIAN HIEROGLYPH A039A;Lo;0;L;;;;;N;;;;;
+1442A;ANATOLIAN HIEROGLYPH A040;Lo;0;L;;;;;N;;;;;
+1442B;ANATOLIAN HIEROGLYPH A041;Lo;0;L;;;;;N;;;;;
+1442C;ANATOLIAN HIEROGLYPH A041A;Lo;0;L;;;;;N;;;;;
+1442D;ANATOLIAN HIEROGLYPH A042;Lo;0;L;;;;;N;;;;;
+1442E;ANATOLIAN HIEROGLYPH A043;Lo;0;L;;;;;N;;;;;
+1442F;ANATOLIAN HIEROGLYPH A044;Lo;0;L;;;;;N;;;;;
+14430;ANATOLIAN HIEROGLYPH A045;Lo;0;L;;;;;N;;;;;
+14431;ANATOLIAN HIEROGLYPH A045A;Lo;0;L;;;;;N;;;;;
+14432;ANATOLIAN HIEROGLYPH A046;Lo;0;L;;;;;N;;;;;
+14433;ANATOLIAN HIEROGLYPH A046A;Lo;0;L;;;;;N;;;;;
+14434;ANATOLIAN HIEROGLYPH A046B;Lo;0;L;;;;;N;;;;;
+14435;ANATOLIAN HIEROGLYPH A047;Lo;0;L;;;;;N;;;;;
+14436;ANATOLIAN HIEROGLYPH A048;Lo;0;L;;;;;N;;;;;
+14437;ANATOLIAN HIEROGLYPH A049;Lo;0;L;;;;;N;;;;;
+14438;ANATOLIAN HIEROGLYPH A050;Lo;0;L;;;;;N;;;;;
+14439;ANATOLIAN HIEROGLYPH A051;Lo;0;L;;;;;N;;;;;
+1443A;ANATOLIAN HIEROGLYPH A052;Lo;0;L;;;;;N;;;;;
+1443B;ANATOLIAN HIEROGLYPH A053;Lo;0;L;;;;;N;;;;;
+1443C;ANATOLIAN HIEROGLYPH A054;Lo;0;L;;;;;N;;;;;
+1443D;ANATOLIAN HIEROGLYPH A055;Lo;0;L;;;;;N;;;;;
+1443E;ANATOLIAN HIEROGLYPH A056;Lo;0;L;;;;;N;;;;;
+1443F;ANATOLIAN HIEROGLYPH A057;Lo;0;L;;;;;N;;;;;
+14440;ANATOLIAN HIEROGLYPH A058;Lo;0;L;;;;;N;;;;;
+14441;ANATOLIAN HIEROGLYPH A059;Lo;0;L;;;;;N;;;;;
+14442;ANATOLIAN HIEROGLYPH A060;Lo;0;L;;;;;N;;;;;
+14443;ANATOLIAN HIEROGLYPH A061;Lo;0;L;;;;;N;;;;;
+14444;ANATOLIAN HIEROGLYPH A062;Lo;0;L;;;;;N;;;;;
+14445;ANATOLIAN HIEROGLYPH A063;Lo;0;L;;;;;N;;;;;
+14446;ANATOLIAN HIEROGLYPH A064;Lo;0;L;;;;;N;;;;;
+14447;ANATOLIAN HIEROGLYPH A065;Lo;0;L;;;;;N;;;;;
+14448;ANATOLIAN HIEROGLYPH A066;Lo;0;L;;;;;N;;;;;
+14449;ANATOLIAN HIEROGLYPH A066A;Lo;0;L;;;;;N;;;;;
+1444A;ANATOLIAN HIEROGLYPH A066B;Lo;0;L;;;;;N;;;;;
+1444B;ANATOLIAN HIEROGLYPH A066C;Lo;0;L;;;;;N;;;;;
+1444C;ANATOLIAN HIEROGLYPH A067;Lo;0;L;;;;;N;;;;;
+1444D;ANATOLIAN HIEROGLYPH A068;Lo;0;L;;;;;N;;;;;
+1444E;ANATOLIAN HIEROGLYPH A069;Lo;0;L;;;;;N;;;;;
+1444F;ANATOLIAN HIEROGLYPH A070;Lo;0;L;;;;;N;;;;;
+14450;ANATOLIAN HIEROGLYPH A071;Lo;0;L;;;;;N;;;;;
+14451;ANATOLIAN HIEROGLYPH A072;Lo;0;L;;;;;N;;;;;
+14452;ANATOLIAN HIEROGLYPH A073;Lo;0;L;;;;;N;;;;;
+14453;ANATOLIAN HIEROGLYPH A074;Lo;0;L;;;;;N;;;;;
+14454;ANATOLIAN HIEROGLYPH A075;Lo;0;L;;;;;N;;;;;
+14455;ANATOLIAN HIEROGLYPH A076;Lo;0;L;;;;;N;;;;;
+14456;ANATOLIAN HIEROGLYPH A077;Lo;0;L;;;;;N;;;;;
+14457;ANATOLIAN HIEROGLYPH A078;Lo;0;L;;;;;N;;;;;
+14458;ANATOLIAN HIEROGLYPH A079;Lo;0;L;;;;;N;;;;;
+14459;ANATOLIAN HIEROGLYPH A080;Lo;0;L;;;;;N;;;;;
+1445A;ANATOLIAN HIEROGLYPH A081;Lo;0;L;;;;;N;;;;;
+1445B;ANATOLIAN HIEROGLYPH A082;Lo;0;L;;;;;N;;;;;
+1445C;ANATOLIAN HIEROGLYPH A083;Lo;0;L;;;;;N;;;;;
+1445D;ANATOLIAN HIEROGLYPH A084;Lo;0;L;;;;;N;;;;;
+1445E;ANATOLIAN HIEROGLYPH A085;Lo;0;L;;;;;N;;;;;
+1445F;ANATOLIAN HIEROGLYPH A086;Lo;0;L;;;;;N;;;;;
+14460;ANATOLIAN HIEROGLYPH A087;Lo;0;L;;;;;N;;;;;
+14461;ANATOLIAN HIEROGLYPH A088;Lo;0;L;;;;;N;;;;;
+14462;ANATOLIAN HIEROGLYPH A089;Lo;0;L;;;;;N;;;;;
+14463;ANATOLIAN HIEROGLYPH A090;Lo;0;L;;;;;N;;;;;
+14464;ANATOLIAN HIEROGLYPH A091;Lo;0;L;;;;;N;;;;;
+14465;ANATOLIAN HIEROGLYPH A092;Lo;0;L;;;;;N;;;;;
+14466;ANATOLIAN HIEROGLYPH A093;Lo;0;L;;;;;N;;;;;
+14467;ANATOLIAN HIEROGLYPH A094;Lo;0;L;;;;;N;;;;;
+14468;ANATOLIAN HIEROGLYPH A095;Lo;0;L;;;;;N;;;;;
+14469;ANATOLIAN HIEROGLYPH A096;Lo;0;L;;;;;N;;;;;
+1446A;ANATOLIAN HIEROGLYPH A097;Lo;0;L;;;;;N;;;;;
+1446B;ANATOLIAN HIEROGLYPH A097A;Lo;0;L;;;;;N;;;;;
+1446C;ANATOLIAN HIEROGLYPH A098;Lo;0;L;;;;;N;;;;;
+1446D;ANATOLIAN HIEROGLYPH A098A;Lo;0;L;;;;;N;;;;;
+1446E;ANATOLIAN HIEROGLYPH A099;Lo;0;L;;;;;N;;;;;
+1446F;ANATOLIAN HIEROGLYPH A100;Lo;0;L;;;;;N;;;;;
+14470;ANATOLIAN HIEROGLYPH A100A;Lo;0;L;;;;;N;;;;;
+14471;ANATOLIAN HIEROGLYPH A101;Lo;0;L;;;;;N;;;;;
+14472;ANATOLIAN HIEROGLYPH A101A;Lo;0;L;;;;;N;;;;;
+14473;ANATOLIAN HIEROGLYPH A102;Lo;0;L;;;;;N;;;;;
+14474;ANATOLIAN HIEROGLYPH A102A;Lo;0;L;;;;;N;;;;;
+14475;ANATOLIAN HIEROGLYPH A103;Lo;0;L;;;;;N;;;;;
+14476;ANATOLIAN HIEROGLYPH A104;Lo;0;L;;;;;N;;;;;
+14477;ANATOLIAN HIEROGLYPH A104A;Lo;0;L;;;;;N;;;;;
+14478;ANATOLIAN HIEROGLYPH A104B;Lo;0;L;;;;;N;;;;;
+14479;ANATOLIAN HIEROGLYPH A104C;Lo;0;L;;;;;N;;;;;
+1447A;ANATOLIAN HIEROGLYPH A105;Lo;0;L;;;;;N;;;;;
+1447B;ANATOLIAN HIEROGLYPH A105A;Lo;0;L;;;;;N;;;;;
+1447C;ANATOLIAN HIEROGLYPH A105B;Lo;0;L;;;;;N;;;;;
+1447D;ANATOLIAN HIEROGLYPH A106;Lo;0;L;;;;;N;;;;;
+1447E;ANATOLIAN HIEROGLYPH A107;Lo;0;L;;;;;N;;;;;
+1447F;ANATOLIAN HIEROGLYPH A107A;Lo;0;L;;;;;N;;;;;
+14480;ANATOLIAN HIEROGLYPH A107B;Lo;0;L;;;;;N;;;;;
+14481;ANATOLIAN HIEROGLYPH A107C;Lo;0;L;;;;;N;;;;;
+14482;ANATOLIAN HIEROGLYPH A108;Lo;0;L;;;;;N;;;;;
+14483;ANATOLIAN HIEROGLYPH A109;Lo;0;L;;;;;N;;;;;
+14484;ANATOLIAN HIEROGLYPH A110;Lo;0;L;;;;;N;;;;;
+14485;ANATOLIAN HIEROGLYPH A110A;Lo;0;L;;;;;N;;;;;
+14486;ANATOLIAN HIEROGLYPH A110B;Lo;0;L;;;;;N;;;;;
+14487;ANATOLIAN HIEROGLYPH A111;Lo;0;L;;;;;N;;;;;
+14488;ANATOLIAN HIEROGLYPH A112;Lo;0;L;;;;;N;;;;;
+14489;ANATOLIAN HIEROGLYPH A113;Lo;0;L;;;;;N;;;;;
+1448A;ANATOLIAN HIEROGLYPH A114;Lo;0;L;;;;;N;;;;;
+1448B;ANATOLIAN HIEROGLYPH A115;Lo;0;L;;;;;N;;;;;
+1448C;ANATOLIAN HIEROGLYPH A115A;Lo;0;L;;;;;N;;;;;
+1448D;ANATOLIAN HIEROGLYPH A116;Lo;0;L;;;;;N;;;;;
+1448E;ANATOLIAN HIEROGLYPH A117;Lo;0;L;;;;;N;;;;;
+1448F;ANATOLIAN HIEROGLYPH A118;Lo;0;L;;;;;N;;;;;
+14490;ANATOLIAN HIEROGLYPH A119;Lo;0;L;;;;;N;;;;;
+14491;ANATOLIAN HIEROGLYPH A120;Lo;0;L;;;;;N;;;;;
+14492;ANATOLIAN HIEROGLYPH A121;Lo;0;L;;;;;N;;;;;
+14493;ANATOLIAN HIEROGLYPH A122;Lo;0;L;;;;;N;;;;;
+14494;ANATOLIAN HIEROGLYPH A123;Lo;0;L;;;;;N;;;;;
+14495;ANATOLIAN HIEROGLYPH A124;Lo;0;L;;;;;N;;;;;
+14496;ANATOLIAN HIEROGLYPH A125;Lo;0;L;;;;;N;;;;;
+14497;ANATOLIAN HIEROGLYPH A125A;Lo;0;L;;;;;N;;;;;
+14498;ANATOLIAN HIEROGLYPH A126;Lo;0;L;;;;;N;;;;;
+14499;ANATOLIAN HIEROGLYPH A127;Lo;0;L;;;;;N;;;;;
+1449A;ANATOLIAN HIEROGLYPH A128;Lo;0;L;;;;;N;;;;;
+1449B;ANATOLIAN HIEROGLYPH A129;Lo;0;L;;;;;N;;;;;
+1449C;ANATOLIAN HIEROGLYPH A130;Lo;0;L;;;;;N;;;;;
+1449D;ANATOLIAN HIEROGLYPH A131;Lo;0;L;;;;;N;;;;;
+1449E;ANATOLIAN HIEROGLYPH A132;Lo;0;L;;;;;N;;;;;
+1449F;ANATOLIAN HIEROGLYPH A133;Lo;0;L;;;;;N;;;;;
+144A0;ANATOLIAN HIEROGLYPH A134;Lo;0;L;;;;;N;;;;;
+144A1;ANATOLIAN HIEROGLYPH A135;Lo;0;L;;;;;N;;;;;
+144A2;ANATOLIAN HIEROGLYPH A135A;Lo;0;L;;;;;N;;;;;
+144A3;ANATOLIAN HIEROGLYPH A136;Lo;0;L;;;;;N;;;;;
+144A4;ANATOLIAN HIEROGLYPH A137;Lo;0;L;;;;;N;;;;;
+144A5;ANATOLIAN HIEROGLYPH A138;Lo;0;L;;;;;N;;;;;
+144A6;ANATOLIAN HIEROGLYPH A139;Lo;0;L;;;;;N;;;;;
+144A7;ANATOLIAN HIEROGLYPH A140;Lo;0;L;;;;;N;;;;;
+144A8;ANATOLIAN HIEROGLYPH A141;Lo;0;L;;;;;N;;;;;
+144A9;ANATOLIAN HIEROGLYPH A142;Lo;0;L;;;;;N;;;;;
+144AA;ANATOLIAN HIEROGLYPH A143;Lo;0;L;;;;;N;;;;;
+144AB;ANATOLIAN HIEROGLYPH A144;Lo;0;L;;;;;N;;;;;
+144AC;ANATOLIAN HIEROGLYPH A145;Lo;0;L;;;;;N;;;;;
+144AD;ANATOLIAN HIEROGLYPH A146;Lo;0;L;;;;;N;;;;;
+144AE;ANATOLIAN HIEROGLYPH A147;Lo;0;L;;;;;N;;;;;
+144AF;ANATOLIAN HIEROGLYPH A148;Lo;0;L;;;;;N;;;;;
+144B0;ANATOLIAN HIEROGLYPH A149;Lo;0;L;;;;;N;;;;;
+144B1;ANATOLIAN HIEROGLYPH A150;Lo;0;L;;;;;N;;;;;
+144B2;ANATOLIAN HIEROGLYPH A151;Lo;0;L;;;;;N;;;;;
+144B3;ANATOLIAN HIEROGLYPH A152;Lo;0;L;;;;;N;;;;;
+144B4;ANATOLIAN HIEROGLYPH A153;Lo;0;L;;;;;N;;;;;
+144B5;ANATOLIAN HIEROGLYPH A154;Lo;0;L;;;;;N;;;;;
+144B6;ANATOLIAN HIEROGLYPH A155;Lo;0;L;;;;;N;;;;;
+144B7;ANATOLIAN HIEROGLYPH A156;Lo;0;L;;;;;N;;;;;
+144B8;ANATOLIAN HIEROGLYPH A157;Lo;0;L;;;;;N;;;;;
+144B9;ANATOLIAN HIEROGLYPH A158;Lo;0;L;;;;;N;;;;;
+144BA;ANATOLIAN HIEROGLYPH A159;Lo;0;L;;;;;N;;;;;
+144BB;ANATOLIAN HIEROGLYPH A160;Lo;0;L;;;;;N;;;;;
+144BC;ANATOLIAN HIEROGLYPH A161;Lo;0;L;;;;;N;;;;;
+144BD;ANATOLIAN HIEROGLYPH A162;Lo;0;L;;;;;N;;;;;
+144BE;ANATOLIAN HIEROGLYPH A163;Lo;0;L;;;;;N;;;;;
+144BF;ANATOLIAN HIEROGLYPH A164;Lo;0;L;;;;;N;;;;;
+144C0;ANATOLIAN HIEROGLYPH A165;Lo;0;L;;;;;N;;;;;
+144C1;ANATOLIAN HIEROGLYPH A166;Lo;0;L;;;;;N;;;;;
+144C2;ANATOLIAN HIEROGLYPH A167;Lo;0;L;;;;;N;;;;;
+144C3;ANATOLIAN HIEROGLYPH A168;Lo;0;L;;;;;N;;;;;
+144C4;ANATOLIAN HIEROGLYPH A169;Lo;0;L;;;;;N;;;;;
+144C5;ANATOLIAN HIEROGLYPH A170;Lo;0;L;;;;;N;;;;;
+144C6;ANATOLIAN HIEROGLYPH A171;Lo;0;L;;;;;N;;;;;
+144C7;ANATOLIAN HIEROGLYPH A172;Lo;0;L;;;;;N;;;;;
+144C8;ANATOLIAN HIEROGLYPH A173;Lo;0;L;;;;;N;;;;;
+144C9;ANATOLIAN HIEROGLYPH A174;Lo;0;L;;;;;N;;;;;
+144CA;ANATOLIAN HIEROGLYPH A175;Lo;0;L;;;;;N;;;;;
+144CB;ANATOLIAN HIEROGLYPH A176;Lo;0;L;;;;;N;;;;;
+144CC;ANATOLIAN HIEROGLYPH A177;Lo;0;L;;;;;N;;;;;
+144CD;ANATOLIAN HIEROGLYPH A178;Lo;0;L;;;;;N;;;;;
+144CE;ANATOLIAN HIEROGLYPH A179;Lo;0;L;;;;;N;;;;;
+144CF;ANATOLIAN HIEROGLYPH A180;Lo;0;L;;;;;N;;;;;
+144D0;ANATOLIAN HIEROGLYPH A181;Lo;0;L;;;;;N;;;;;
+144D1;ANATOLIAN HIEROGLYPH A182;Lo;0;L;;;;;N;;;;;
+144D2;ANATOLIAN HIEROGLYPH A183;Lo;0;L;;;;;N;;;;;
+144D3;ANATOLIAN HIEROGLYPH A184;Lo;0;L;;;;;N;;;;;
+144D4;ANATOLIAN HIEROGLYPH A185;Lo;0;L;;;;;N;;;;;
+144D5;ANATOLIAN HIEROGLYPH A186;Lo;0;L;;;;;N;;;;;
+144D6;ANATOLIAN HIEROGLYPH A187;Lo;0;L;;;;;N;;;;;
+144D7;ANATOLIAN HIEROGLYPH A188;Lo;0;L;;;;;N;;;;;
+144D8;ANATOLIAN HIEROGLYPH A189;Lo;0;L;;;;;N;;;;;
+144D9;ANATOLIAN HIEROGLYPH A190;Lo;0;L;;;;;N;;;;;
+144DA;ANATOLIAN HIEROGLYPH A191;Lo;0;L;;;;;N;;;;;
+144DB;ANATOLIAN HIEROGLYPH A192;Lo;0;L;;;;;N;;;;;
+144DC;ANATOLIAN HIEROGLYPH A193;Lo;0;L;;;;;N;;;;;
+144DD;ANATOLIAN HIEROGLYPH A194;Lo;0;L;;;;;N;;;;;
+144DE;ANATOLIAN HIEROGLYPH A195;Lo;0;L;;;;;N;;;;;
+144DF;ANATOLIAN HIEROGLYPH A196;Lo;0;L;;;;;N;;;;;
+144E0;ANATOLIAN HIEROGLYPH A197;Lo;0;L;;;;;N;;;;;
+144E1;ANATOLIAN HIEROGLYPH A198;Lo;0;L;;;;;N;;;;;
+144E2;ANATOLIAN HIEROGLYPH A199;Lo;0;L;;;;;N;;;;;
+144E3;ANATOLIAN HIEROGLYPH A200;Lo;0;L;;;;;N;;;;;
+144E4;ANATOLIAN HIEROGLYPH A201;Lo;0;L;;;;;N;;;;;
+144E5;ANATOLIAN HIEROGLYPH A202;Lo;0;L;;;;;N;;;;;
+144E6;ANATOLIAN HIEROGLYPH A202A;Lo;0;L;;;;;N;;;;;
+144E7;ANATOLIAN HIEROGLYPH A202B;Lo;0;L;;;;;N;;;;;
+144E8;ANATOLIAN HIEROGLYPH A203;Lo;0;L;;;;;N;;;;;
+144E9;ANATOLIAN HIEROGLYPH A204;Lo;0;L;;;;;N;;;;;
+144EA;ANATOLIAN HIEROGLYPH A205;Lo;0;L;;;;;N;;;;;
+144EB;ANATOLIAN HIEROGLYPH A206;Lo;0;L;;;;;N;;;;;
+144EC;ANATOLIAN HIEROGLYPH A207;Lo;0;L;;;;;N;;;;;
+144ED;ANATOLIAN HIEROGLYPH A207A;Lo;0;L;;;;;N;;;;;
+144EE;ANATOLIAN HIEROGLYPH A208;Lo;0;L;;;;;N;;;;;
+144EF;ANATOLIAN HIEROGLYPH A209;Lo;0;L;;;;;N;;;;;
+144F0;ANATOLIAN HIEROGLYPH A209A;Lo;0;L;;;;;N;;;;;
+144F1;ANATOLIAN HIEROGLYPH A210;Lo;0;L;;;;;N;;;;;
+144F2;ANATOLIAN HIEROGLYPH A211;Lo;0;L;;;;;N;;;;;
+144F3;ANATOLIAN HIEROGLYPH A212;Lo;0;L;;;;;N;;;;;
+144F4;ANATOLIAN HIEROGLYPH A213;Lo;0;L;;;;;N;;;;;
+144F5;ANATOLIAN HIEROGLYPH A214;Lo;0;L;;;;;N;;;;;
+144F6;ANATOLIAN HIEROGLYPH A215;Lo;0;L;;;;;N;;;;;
+144F7;ANATOLIAN HIEROGLYPH A215A;Lo;0;L;;;;;N;;;;;
+144F8;ANATOLIAN HIEROGLYPH A216;Lo;0;L;;;;;N;;;;;
+144F9;ANATOLIAN HIEROGLYPH A216A;Lo;0;L;;;;;N;;;;;
+144FA;ANATOLIAN HIEROGLYPH A217;Lo;0;L;;;;;N;;;;;
+144FB;ANATOLIAN HIEROGLYPH A218;Lo;0;L;;;;;N;;;;;
+144FC;ANATOLIAN HIEROGLYPH A219;Lo;0;L;;;;;N;;;;;
+144FD;ANATOLIAN HIEROGLYPH A220;Lo;0;L;;;;;N;;;;;
+144FE;ANATOLIAN HIEROGLYPH A221;Lo;0;L;;;;;N;;;;;
+144FF;ANATOLIAN HIEROGLYPH A222;Lo;0;L;;;;;N;;;;;
+14500;ANATOLIAN HIEROGLYPH A223;Lo;0;L;;;;;N;;;;;
+14501;ANATOLIAN HIEROGLYPH A224;Lo;0;L;;;;;N;;;;;
+14502;ANATOLIAN HIEROGLYPH A225;Lo;0;L;;;;;N;;;;;
+14503;ANATOLIAN HIEROGLYPH A226;Lo;0;L;;;;;N;;;;;
+14504;ANATOLIAN HIEROGLYPH A227;Lo;0;L;;;;;N;;;;;
+14505;ANATOLIAN HIEROGLYPH A227A;Lo;0;L;;;;;N;;;;;
+14506;ANATOLIAN HIEROGLYPH A228;Lo;0;L;;;;;N;;;;;
+14507;ANATOLIAN HIEROGLYPH A229;Lo;0;L;;;;;N;;;;;
+14508;ANATOLIAN HIEROGLYPH A230;Lo;0;L;;;;;N;;;;;
+14509;ANATOLIAN HIEROGLYPH A231;Lo;0;L;;;;;N;;;;;
+1450A;ANATOLIAN HIEROGLYPH A232;Lo;0;L;;;;;N;;;;;
+1450B;ANATOLIAN HIEROGLYPH A233;Lo;0;L;;;;;N;;;;;
+1450C;ANATOLIAN HIEROGLYPH A234;Lo;0;L;;;;;N;;;;;
+1450D;ANATOLIAN HIEROGLYPH A235;Lo;0;L;;;;;N;;;;;
+1450E;ANATOLIAN HIEROGLYPH A236;Lo;0;L;;;;;N;;;;;
+1450F;ANATOLIAN HIEROGLYPH A237;Lo;0;L;;;;;N;;;;;
+14510;ANATOLIAN HIEROGLYPH A238;Lo;0;L;;;;;N;;;;;
+14511;ANATOLIAN HIEROGLYPH A239;Lo;0;L;;;;;N;;;;;
+14512;ANATOLIAN HIEROGLYPH A240;Lo;0;L;;;;;N;;;;;
+14513;ANATOLIAN HIEROGLYPH A241;Lo;0;L;;;;;N;;;;;
+14514;ANATOLIAN HIEROGLYPH A242;Lo;0;L;;;;;N;;;;;
+14515;ANATOLIAN HIEROGLYPH A243;Lo;0;L;;;;;N;;;;;
+14516;ANATOLIAN HIEROGLYPH A244;Lo;0;L;;;;;N;;;;;
+14517;ANATOLIAN HIEROGLYPH A245;Lo;0;L;;;;;N;;;;;
+14518;ANATOLIAN HIEROGLYPH A246;Lo;0;L;;;;;N;;;;;
+14519;ANATOLIAN HIEROGLYPH A247;Lo;0;L;;;;;N;;;;;
+1451A;ANATOLIAN HIEROGLYPH A248;Lo;0;L;;;;;N;;;;;
+1451B;ANATOLIAN HIEROGLYPH A249;Lo;0;L;;;;;N;;;;;
+1451C;ANATOLIAN HIEROGLYPH A250;Lo;0;L;;;;;N;;;;;
+1451D;ANATOLIAN HIEROGLYPH A251;Lo;0;L;;;;;N;;;;;
+1451E;ANATOLIAN HIEROGLYPH A252;Lo;0;L;;;;;N;;;;;
+1451F;ANATOLIAN HIEROGLYPH A253;Lo;0;L;;;;;N;;;;;
+14520;ANATOLIAN HIEROGLYPH A254;Lo;0;L;;;;;N;;;;;
+14521;ANATOLIAN HIEROGLYPH A255;Lo;0;L;;;;;N;;;;;
+14522;ANATOLIAN HIEROGLYPH A256;Lo;0;L;;;;;N;;;;;
+14523;ANATOLIAN HIEROGLYPH A257;Lo;0;L;;;;;N;;;;;
+14524;ANATOLIAN HIEROGLYPH A258;Lo;0;L;;;;;N;;;;;
+14525;ANATOLIAN HIEROGLYPH A259;Lo;0;L;;;;;N;;;;;
+14526;ANATOLIAN HIEROGLYPH A260;Lo;0;L;;;;;N;;;;;
+14527;ANATOLIAN HIEROGLYPH A261;Lo;0;L;;;;;N;;;;;
+14528;ANATOLIAN HIEROGLYPH A262;Lo;0;L;;;;;N;;;;;
+14529;ANATOLIAN HIEROGLYPH A263;Lo;0;L;;;;;N;;;;;
+1452A;ANATOLIAN HIEROGLYPH A264;Lo;0;L;;;;;N;;;;;
+1452B;ANATOLIAN HIEROGLYPH A265;Lo;0;L;;;;;N;;;;;
+1452C;ANATOLIAN HIEROGLYPH A266;Lo;0;L;;;;;N;;;;;
+1452D;ANATOLIAN HIEROGLYPH A267;Lo;0;L;;;;;N;;;;;
+1452E;ANATOLIAN HIEROGLYPH A267A;Lo;0;L;;;;;N;;;;;
+1452F;ANATOLIAN HIEROGLYPH A268;Lo;0;L;;;;;N;;;;;
+14530;ANATOLIAN HIEROGLYPH A269;Lo;0;L;;;;;N;;;;;
+14531;ANATOLIAN HIEROGLYPH A270;Lo;0;L;;;;;N;;;;;
+14532;ANATOLIAN HIEROGLYPH A271;Lo;0;L;;;;;N;;;;;
+14533;ANATOLIAN HIEROGLYPH A272;Lo;0;L;;;;;N;;;;;
+14534;ANATOLIAN HIEROGLYPH A273;Lo;0;L;;;;;N;;;;;
+14535;ANATOLIAN HIEROGLYPH A274;Lo;0;L;;;;;N;;;;;
+14536;ANATOLIAN HIEROGLYPH A275;Lo;0;L;;;;;N;;;;;
+14537;ANATOLIAN HIEROGLYPH A276;Lo;0;L;;;;;N;;;;;
+14538;ANATOLIAN HIEROGLYPH A277;Lo;0;L;;;;;N;;;;;
+14539;ANATOLIAN HIEROGLYPH A278;Lo;0;L;;;;;N;;;;;
+1453A;ANATOLIAN HIEROGLYPH A279;Lo;0;L;;;;;N;;;;;
+1453B;ANATOLIAN HIEROGLYPH A280;Lo;0;L;;;;;N;;;;;
+1453C;ANATOLIAN HIEROGLYPH A281;Lo;0;L;;;;;N;;;;;
+1453D;ANATOLIAN HIEROGLYPH A282;Lo;0;L;;;;;N;;;;;
+1453E;ANATOLIAN HIEROGLYPH A283;Lo;0;L;;;;;N;;;;;
+1453F;ANATOLIAN HIEROGLYPH A284;Lo;0;L;;;;;N;;;;;
+14540;ANATOLIAN HIEROGLYPH A285;Lo;0;L;;;;;N;;;;;
+14541;ANATOLIAN HIEROGLYPH A286;Lo;0;L;;;;;N;;;;;
+14542;ANATOLIAN HIEROGLYPH A287;Lo;0;L;;;;;N;;;;;
+14543;ANATOLIAN HIEROGLYPH A288;Lo;0;L;;;;;N;;;;;
+14544;ANATOLIAN HIEROGLYPH A289;Lo;0;L;;;;;N;;;;;
+14545;ANATOLIAN HIEROGLYPH A289A;Lo;0;L;;;;;N;;;;;
+14546;ANATOLIAN HIEROGLYPH A290;Lo;0;L;;;;;N;;;;;
+14547;ANATOLIAN HIEROGLYPH A291;Lo;0;L;;;;;N;;;;;
+14548;ANATOLIAN HIEROGLYPH A292;Lo;0;L;;;;;N;;;;;
+14549;ANATOLIAN HIEROGLYPH A293;Lo;0;L;;;;;N;;;;;
+1454A;ANATOLIAN HIEROGLYPH A294;Lo;0;L;;;;;N;;;;;
+1454B;ANATOLIAN HIEROGLYPH A294A;Lo;0;L;;;;;N;;;;;
+1454C;ANATOLIAN HIEROGLYPH A295;Lo;0;L;;;;;N;;;;;
+1454D;ANATOLIAN HIEROGLYPH A296;Lo;0;L;;;;;N;;;;;
+1454E;ANATOLIAN HIEROGLYPH A297;Lo;0;L;;;;;N;;;;;
+1454F;ANATOLIAN HIEROGLYPH A298;Lo;0;L;;;;;N;;;;;
+14550;ANATOLIAN HIEROGLYPH A299;Lo;0;L;;;;;N;;;;;
+14551;ANATOLIAN HIEROGLYPH A299A;Lo;0;L;;;;;N;;;;;
+14552;ANATOLIAN HIEROGLYPH A300;Lo;0;L;;;;;N;;;;;
+14553;ANATOLIAN HIEROGLYPH A301;Lo;0;L;;;;;N;;;;;
+14554;ANATOLIAN HIEROGLYPH A302;Lo;0;L;;;;;N;;;;;
+14555;ANATOLIAN HIEROGLYPH A303;Lo;0;L;;;;;N;;;;;
+14556;ANATOLIAN HIEROGLYPH A304;Lo;0;L;;;;;N;;;;;
+14557;ANATOLIAN HIEROGLYPH A305;Lo;0;L;;;;;N;;;;;
+14558;ANATOLIAN HIEROGLYPH A306;Lo;0;L;;;;;N;;;;;
+14559;ANATOLIAN HIEROGLYPH A307;Lo;0;L;;;;;N;;;;;
+1455A;ANATOLIAN HIEROGLYPH A308;Lo;0;L;;;;;N;;;;;
+1455B;ANATOLIAN HIEROGLYPH A309;Lo;0;L;;;;;N;;;;;
+1455C;ANATOLIAN HIEROGLYPH A309A;Lo;0;L;;;;;N;;;;;
+1455D;ANATOLIAN HIEROGLYPH A310;Lo;0;L;;;;;N;;;;;
+1455E;ANATOLIAN HIEROGLYPH A311;Lo;0;L;;;;;N;;;;;
+1455F;ANATOLIAN HIEROGLYPH A312;Lo;0;L;;;;;N;;;;;
+14560;ANATOLIAN HIEROGLYPH A313;Lo;0;L;;;;;N;;;;;
+14561;ANATOLIAN HIEROGLYPH A314;Lo;0;L;;;;;N;;;;;
+14562;ANATOLIAN HIEROGLYPH A315;Lo;0;L;;;;;N;;;;;
+14563;ANATOLIAN HIEROGLYPH A316;Lo;0;L;;;;;N;;;;;
+14564;ANATOLIAN HIEROGLYPH A317;Lo;0;L;;;;;N;;;;;
+14565;ANATOLIAN HIEROGLYPH A318;Lo;0;L;;;;;N;;;;;
+14566;ANATOLIAN HIEROGLYPH A319;Lo;0;L;;;;;N;;;;;
+14567;ANATOLIAN HIEROGLYPH A320;Lo;0;L;;;;;N;;;;;
+14568;ANATOLIAN HIEROGLYPH A321;Lo;0;L;;;;;N;;;;;
+14569;ANATOLIAN HIEROGLYPH A322;Lo;0;L;;;;;N;;;;;
+1456A;ANATOLIAN HIEROGLYPH A323;Lo;0;L;;;;;N;;;;;
+1456B;ANATOLIAN HIEROGLYPH A324;Lo;0;L;;;;;N;;;;;
+1456C;ANATOLIAN HIEROGLYPH A325;Lo;0;L;;;;;N;;;;;
+1456D;ANATOLIAN HIEROGLYPH A326;Lo;0;L;;;;;N;;;;;
+1456E;ANATOLIAN HIEROGLYPH A327;Lo;0;L;;;;;N;;;;;
+1456F;ANATOLIAN HIEROGLYPH A328;Lo;0;L;;;;;N;;;;;
+14570;ANATOLIAN HIEROGLYPH A329;Lo;0;L;;;;;N;;;;;
+14571;ANATOLIAN HIEROGLYPH A329A;Lo;0;L;;;;;N;;;;;
+14572;ANATOLIAN HIEROGLYPH A330;Lo;0;L;;;;;N;;;;;
+14573;ANATOLIAN HIEROGLYPH A331;Lo;0;L;;;;;N;;;;;
+14574;ANATOLIAN HIEROGLYPH A332A;Lo;0;L;;;;;N;;;;;
+14575;ANATOLIAN HIEROGLYPH A332B;Lo;0;L;;;;;N;;;;;
+14576;ANATOLIAN HIEROGLYPH A332C;Lo;0;L;;;;;N;;;;;
+14577;ANATOLIAN HIEROGLYPH A333;Lo;0;L;;;;;N;;;;;
+14578;ANATOLIAN HIEROGLYPH A334;Lo;0;L;;;;;N;;;;;
+14579;ANATOLIAN HIEROGLYPH A335;Lo;0;L;;;;;N;;;;;
+1457A;ANATOLIAN HIEROGLYPH A336;Lo;0;L;;;;;N;;;;;
+1457B;ANATOLIAN HIEROGLYPH A336A;Lo;0;L;;;;;N;;;;;
+1457C;ANATOLIAN HIEROGLYPH A336B;Lo;0;L;;;;;N;;;;;
+1457D;ANATOLIAN HIEROGLYPH A336C;Lo;0;L;;;;;N;;;;;
+1457E;ANATOLIAN HIEROGLYPH A337;Lo;0;L;;;;;N;;;;;
+1457F;ANATOLIAN HIEROGLYPH A338;Lo;0;L;;;;;N;;;;;
+14580;ANATOLIAN HIEROGLYPH A339;Lo;0;L;;;;;N;;;;;
+14581;ANATOLIAN HIEROGLYPH A340;Lo;0;L;;;;;N;;;;;
+14582;ANATOLIAN HIEROGLYPH A341;Lo;0;L;;;;;N;;;;;
+14583;ANATOLIAN HIEROGLYPH A342;Lo;0;L;;;;;N;;;;;
+14584;ANATOLIAN HIEROGLYPH A343;Lo;0;L;;;;;N;;;;;
+14585;ANATOLIAN HIEROGLYPH A344;Lo;0;L;;;;;N;;;;;
+14586;ANATOLIAN HIEROGLYPH A345;Lo;0;L;;;;;N;;;;;
+14587;ANATOLIAN HIEROGLYPH A346;Lo;0;L;;;;;N;;;;;
+14588;ANATOLIAN HIEROGLYPH A347;Lo;0;L;;;;;N;;;;;
+14589;ANATOLIAN HIEROGLYPH A348;Lo;0;L;;;;;N;;;;;
+1458A;ANATOLIAN HIEROGLYPH A349;Lo;0;L;;;;;N;;;;;
+1458B;ANATOLIAN HIEROGLYPH A350;Lo;0;L;;;;;N;;;;;
+1458C;ANATOLIAN HIEROGLYPH A351;Lo;0;L;;;;;N;;;;;
+1458D;ANATOLIAN HIEROGLYPH A352;Lo;0;L;;;;;N;;;;;
+1458E;ANATOLIAN HIEROGLYPH A353;Lo;0;L;;;;;N;;;;;
+1458F;ANATOLIAN HIEROGLYPH A354;Lo;0;L;;;;;N;;;;;
+14590;ANATOLIAN HIEROGLYPH A355;Lo;0;L;;;;;N;;;;;
+14591;ANATOLIAN HIEROGLYPH A356;Lo;0;L;;;;;N;;;;;
+14592;ANATOLIAN HIEROGLYPH A357;Lo;0;L;;;;;N;;;;;
+14593;ANATOLIAN HIEROGLYPH A358;Lo;0;L;;;;;N;;;;;
+14594;ANATOLIAN HIEROGLYPH A359;Lo;0;L;;;;;N;;;;;
+14595;ANATOLIAN HIEROGLYPH A359A;Lo;0;L;;;;;N;;;;;
+14596;ANATOLIAN HIEROGLYPH A360;Lo;0;L;;;;;N;;;;;
+14597;ANATOLIAN HIEROGLYPH A361;Lo;0;L;;;;;N;;;;;
+14598;ANATOLIAN HIEROGLYPH A362;Lo;0;L;;;;;N;;;;;
+14599;ANATOLIAN HIEROGLYPH A363;Lo;0;L;;;;;N;;;;;
+1459A;ANATOLIAN HIEROGLYPH A364;Lo;0;L;;;;;N;;;;;
+1459B;ANATOLIAN HIEROGLYPH A364A;Lo;0;L;;;;;N;;;;;
+1459C;ANATOLIAN HIEROGLYPH A365;Lo;0;L;;;;;N;;;;;
+1459D;ANATOLIAN HIEROGLYPH A366;Lo;0;L;;;;;N;;;;;
+1459E;ANATOLIAN HIEROGLYPH A367;Lo;0;L;;;;;N;;;;;
+1459F;ANATOLIAN HIEROGLYPH A368;Lo;0;L;;;;;N;;;;;
+145A0;ANATOLIAN HIEROGLYPH A368A;Lo;0;L;;;;;N;;;;;
+145A1;ANATOLIAN HIEROGLYPH A369;Lo;0;L;;;;;N;;;;;
+145A2;ANATOLIAN HIEROGLYPH A370;Lo;0;L;;;;;N;;;;;
+145A3;ANATOLIAN HIEROGLYPH A371;Lo;0;L;;;;;N;;;;;
+145A4;ANATOLIAN HIEROGLYPH A371A;Lo;0;L;;;;;N;;;;;
+145A5;ANATOLIAN HIEROGLYPH A372;Lo;0;L;;;;;N;;;;;
+145A6;ANATOLIAN HIEROGLYPH A373;Lo;0;L;;;;;N;;;;;
+145A7;ANATOLIAN HIEROGLYPH A374;Lo;0;L;;;;;N;;;;;
+145A8;ANATOLIAN HIEROGLYPH A375;Lo;0;L;;;;;N;;;;;
+145A9;ANATOLIAN HIEROGLYPH A376;Lo;0;L;;;;;N;;;;;
+145AA;ANATOLIAN HIEROGLYPH A377;Lo;0;L;;;;;N;;;;;
+145AB;ANATOLIAN HIEROGLYPH A378;Lo;0;L;;;;;N;;;;;
+145AC;ANATOLIAN HIEROGLYPH A379;Lo;0;L;;;;;N;;;;;
+145AD;ANATOLIAN HIEROGLYPH A380;Lo;0;L;;;;;N;;;;;
+145AE;ANATOLIAN HIEROGLYPH A381;Lo;0;L;;;;;N;;;;;
+145AF;ANATOLIAN HIEROGLYPH A381A;Lo;0;L;;;;;N;;;;;
+145B0;ANATOLIAN HIEROGLYPH A382;Lo;0;L;;;;;N;;;;;
+145B1;ANATOLIAN HIEROGLYPH A383 RA OR RI;Lo;0;L;;;;;N;;;;;
+145B2;ANATOLIAN HIEROGLYPH A383A;Lo;0;L;;;;;N;;;;;
+145B3;ANATOLIAN HIEROGLYPH A384;Lo;0;L;;;;;N;;;;;
+145B4;ANATOLIAN HIEROGLYPH A385;Lo;0;L;;;;;N;;;;;
+145B5;ANATOLIAN HIEROGLYPH A386;Lo;0;L;;;;;N;;;;;
+145B6;ANATOLIAN HIEROGLYPH A386A;Lo;0;L;;;;;N;;;;;
+145B7;ANATOLIAN HIEROGLYPH A387;Lo;0;L;;;;;N;;;;;
+145B8;ANATOLIAN HIEROGLYPH A388;Lo;0;L;;;;;N;;;;;
+145B9;ANATOLIAN HIEROGLYPH A389;Lo;0;L;;;;;N;;;;;
+145BA;ANATOLIAN HIEROGLYPH A390;Lo;0;L;;;;;N;;;;;
+145BB;ANATOLIAN HIEROGLYPH A391;Lo;0;L;;;;;N;;;;;
+145BC;ANATOLIAN HIEROGLYPH A392;Lo;0;L;;;;;N;;;;;
+145BD;ANATOLIAN HIEROGLYPH A393 EIGHT;Lo;0;L;;;;;N;;;;;
+145BE;ANATOLIAN HIEROGLYPH A394;Lo;0;L;;;;;N;;;;;
+145BF;ANATOLIAN HIEROGLYPH A395;Lo;0;L;;;;;N;;;;;
+145C0;ANATOLIAN HIEROGLYPH A396;Lo;0;L;;;;;N;;;;;
+145C1;ANATOLIAN HIEROGLYPH A397;Lo;0;L;;;;;N;;;;;
+145C2;ANATOLIAN HIEROGLYPH A398;Lo;0;L;;;;;N;;;;;
+145C3;ANATOLIAN HIEROGLYPH A399;Lo;0;L;;;;;N;;;;;
+145C4;ANATOLIAN HIEROGLYPH A400;Lo;0;L;;;;;N;;;;;
+145C5;ANATOLIAN HIEROGLYPH A401;Lo;0;L;;;;;N;;;;;
+145C6;ANATOLIAN HIEROGLYPH A402;Lo;0;L;;;;;N;;;;;
+145C7;ANATOLIAN HIEROGLYPH A403;Lo;0;L;;;;;N;;;;;
+145C8;ANATOLIAN HIEROGLYPH A404;Lo;0;L;;;;;N;;;;;
+145C9;ANATOLIAN HIEROGLYPH A405;Lo;0;L;;;;;N;;;;;
+145CA;ANATOLIAN HIEROGLYPH A406;Lo;0;L;;;;;N;;;;;
+145CB;ANATOLIAN HIEROGLYPH A407;Lo;0;L;;;;;N;;;;;
+145CC;ANATOLIAN HIEROGLYPH A408;Lo;0;L;;;;;N;;;;;
+145CD;ANATOLIAN HIEROGLYPH A409;Lo;0;L;;;;;N;;;;;
+145CE;ANATOLIAN HIEROGLYPH A410 BEGIN LOGOGRAM MARK;Lo;0;L;;;;;N;;;;;
+145CF;ANATOLIAN HIEROGLYPH A410A END LOGOGRAM MARK;Lo;0;L;;;;;N;;;;;
+145D0;ANATOLIAN HIEROGLYPH A411;Lo;0;L;;;;;N;;;;;
+145D1;ANATOLIAN HIEROGLYPH A412;Lo;0;L;;;;;N;;;;;
+145D2;ANATOLIAN HIEROGLYPH A413;Lo;0;L;;;;;N;;;;;
+145D3;ANATOLIAN HIEROGLYPH A414;Lo;0;L;;;;;N;;;;;
+145D4;ANATOLIAN HIEROGLYPH A415;Lo;0;L;;;;;N;;;;;
+145D5;ANATOLIAN HIEROGLYPH A416;Lo;0;L;;;;;N;;;;;
+145D6;ANATOLIAN HIEROGLYPH A417;Lo;0;L;;;;;N;;;;;
+145D7;ANATOLIAN HIEROGLYPH A418;Lo;0;L;;;;;N;;;;;
+145D8;ANATOLIAN HIEROGLYPH A419;Lo;0;L;;;;;N;;;;;
+145D9;ANATOLIAN HIEROGLYPH A420;Lo;0;L;;;;;N;;;;;
+145DA;ANATOLIAN HIEROGLYPH A421;Lo;0;L;;;;;N;;;;;
+145DB;ANATOLIAN HIEROGLYPH A422;Lo;0;L;;;;;N;;;;;
+145DC;ANATOLIAN HIEROGLYPH A423;Lo;0;L;;;;;N;;;;;
+145DD;ANATOLIAN HIEROGLYPH A424;Lo;0;L;;;;;N;;;;;
+145DE;ANATOLIAN HIEROGLYPH A425;Lo;0;L;;;;;N;;;;;
+145DF;ANATOLIAN HIEROGLYPH A426;Lo;0;L;;;;;N;;;;;
+145E0;ANATOLIAN HIEROGLYPH A427;Lo;0;L;;;;;N;;;;;
+145E1;ANATOLIAN HIEROGLYPH A428;Lo;0;L;;;;;N;;;;;
+145E2;ANATOLIAN HIEROGLYPH A429;Lo;0;L;;;;;N;;;;;
+145E3;ANATOLIAN HIEROGLYPH A430;Lo;0;L;;;;;N;;;;;
+145E4;ANATOLIAN HIEROGLYPH A431;Lo;0;L;;;;;N;;;;;
+145E5;ANATOLIAN HIEROGLYPH A432;Lo;0;L;;;;;N;;;;;
+145E6;ANATOLIAN HIEROGLYPH A433;Lo;0;L;;;;;N;;;;;
+145E7;ANATOLIAN HIEROGLYPH A434;Lo;0;L;;;;;N;;;;;
+145E8;ANATOLIAN HIEROGLYPH A435;Lo;0;L;;;;;N;;;;;
+145E9;ANATOLIAN HIEROGLYPH A436;Lo;0;L;;;;;N;;;;;
+145EA;ANATOLIAN HIEROGLYPH A437;Lo;0;L;;;;;N;;;;;
+145EB;ANATOLIAN HIEROGLYPH A438;Lo;0;L;;;;;N;;;;;
+145EC;ANATOLIAN HIEROGLYPH A439;Lo;0;L;;;;;N;;;;;
+145ED;ANATOLIAN HIEROGLYPH A440;Lo;0;L;;;;;N;;;;;
+145EE;ANATOLIAN HIEROGLYPH A441;Lo;0;L;;;;;N;;;;;
+145EF;ANATOLIAN HIEROGLYPH A442;Lo;0;L;;;;;N;;;;;
+145F0;ANATOLIAN HIEROGLYPH A443;Lo;0;L;;;;;N;;;;;
+145F1;ANATOLIAN HIEROGLYPH A444;Lo;0;L;;;;;N;;;;;
+145F2;ANATOLIAN HIEROGLYPH A445;Lo;0;L;;;;;N;;;;;
+145F3;ANATOLIAN HIEROGLYPH A446;Lo;0;L;;;;;N;;;;;
+145F4;ANATOLIAN HIEROGLYPH A447;Lo;0;L;;;;;N;;;;;
+145F5;ANATOLIAN HIEROGLYPH A448;Lo;0;L;;;;;N;;;;;
+145F6;ANATOLIAN HIEROGLYPH A449;Lo;0;L;;;;;N;;;;;
+145F7;ANATOLIAN HIEROGLYPH A450;Lo;0;L;;;;;N;;;;;
+145F8;ANATOLIAN HIEROGLYPH A450A;Lo;0;L;;;;;N;;;;;
+145F9;ANATOLIAN HIEROGLYPH A451;Lo;0;L;;;;;N;;;;;
+145FA;ANATOLIAN HIEROGLYPH A452;Lo;0;L;;;;;N;;;;;
+145FB;ANATOLIAN HIEROGLYPH A453;Lo;0;L;;;;;N;;;;;
+145FC;ANATOLIAN HIEROGLYPH A454;Lo;0;L;;;;;N;;;;;
+145FD;ANATOLIAN HIEROGLYPH A455;Lo;0;L;;;;;N;;;;;
+145FE;ANATOLIAN HIEROGLYPH A456;Lo;0;L;;;;;N;;;;;
+145FF;ANATOLIAN HIEROGLYPH A457;Lo;0;L;;;;;N;;;;;
+14600;ANATOLIAN HIEROGLYPH A457A;Lo;0;L;;;;;N;;;;;
+14601;ANATOLIAN HIEROGLYPH A458;Lo;0;L;;;;;N;;;;;
+14602;ANATOLIAN HIEROGLYPH A459;Lo;0;L;;;;;N;;;;;
+14603;ANATOLIAN HIEROGLYPH A460;Lo;0;L;;;;;N;;;;;
+14604;ANATOLIAN HIEROGLYPH A461;Lo;0;L;;;;;N;;;;;
+14605;ANATOLIAN HIEROGLYPH A462;Lo;0;L;;;;;N;;;;;
+14606;ANATOLIAN HIEROGLYPH A463;Lo;0;L;;;;;N;;;;;
+14607;ANATOLIAN HIEROGLYPH A464;Lo;0;L;;;;;N;;;;;
+14608;ANATOLIAN HIEROGLYPH A465;Lo;0;L;;;;;N;;;;;
+14609;ANATOLIAN HIEROGLYPH A466;Lo;0;L;;;;;N;;;;;
+1460A;ANATOLIAN HIEROGLYPH A467;Lo;0;L;;;;;N;;;;;
+1460B;ANATOLIAN HIEROGLYPH A468;Lo;0;L;;;;;N;;;;;
+1460C;ANATOLIAN HIEROGLYPH A469;Lo;0;L;;;;;N;;;;;
+1460D;ANATOLIAN HIEROGLYPH A470;Lo;0;L;;;;;N;;;;;
+1460E;ANATOLIAN HIEROGLYPH A471;Lo;0;L;;;;;N;;;;;
+1460F;ANATOLIAN HIEROGLYPH A472;Lo;0;L;;;;;N;;;;;
+14610;ANATOLIAN HIEROGLYPH A473;Lo;0;L;;;;;N;;;;;
+14611;ANATOLIAN HIEROGLYPH A474;Lo;0;L;;;;;N;;;;;
+14612;ANATOLIAN HIEROGLYPH A475;Lo;0;L;;;;;N;;;;;
+14613;ANATOLIAN HIEROGLYPH A476;Lo;0;L;;;;;N;;;;;
+14614;ANATOLIAN HIEROGLYPH A477;Lo;0;L;;;;;N;;;;;
+14615;ANATOLIAN HIEROGLYPH A478;Lo;0;L;;;;;N;;;;;
+14616;ANATOLIAN HIEROGLYPH A479;Lo;0;L;;;;;N;;;;;
+14617;ANATOLIAN HIEROGLYPH A480;Lo;0;L;;;;;N;;;;;
+14618;ANATOLIAN HIEROGLYPH A481;Lo;0;L;;;;;N;;;;;
+14619;ANATOLIAN HIEROGLYPH A482;Lo;0;L;;;;;N;;;;;
+1461A;ANATOLIAN HIEROGLYPH A483;Lo;0;L;;;;;N;;;;;
+1461B;ANATOLIAN HIEROGLYPH A484;Lo;0;L;;;;;N;;;;;
+1461C;ANATOLIAN HIEROGLYPH A485;Lo;0;L;;;;;N;;;;;
+1461D;ANATOLIAN HIEROGLYPH A486;Lo;0;L;;;;;N;;;;;
+1461E;ANATOLIAN HIEROGLYPH A487;Lo;0;L;;;;;N;;;;;
+1461F;ANATOLIAN HIEROGLYPH A488;Lo;0;L;;;;;N;;;;;
+14620;ANATOLIAN HIEROGLYPH A489;Lo;0;L;;;;;N;;;;;
+14621;ANATOLIAN HIEROGLYPH A490;Lo;0;L;;;;;N;;;;;
+14622;ANATOLIAN HIEROGLYPH A491;Lo;0;L;;;;;N;;;;;
+14623;ANATOLIAN HIEROGLYPH A492;Lo;0;L;;;;;N;;;;;
+14624;ANATOLIAN HIEROGLYPH A493;Lo;0;L;;;;;N;;;;;
+14625;ANATOLIAN HIEROGLYPH A494;Lo;0;L;;;;;N;;;;;
+14626;ANATOLIAN HIEROGLYPH A495;Lo;0;L;;;;;N;;;;;
+14627;ANATOLIAN HIEROGLYPH A496;Lo;0;L;;;;;N;;;;;
+14628;ANATOLIAN HIEROGLYPH A497;Lo;0;L;;;;;N;;;;;
+14629;ANATOLIAN HIEROGLYPH A501;Lo;0;L;;;;;N;;;;;
+1462A;ANATOLIAN HIEROGLYPH A502;Lo;0;L;;;;;N;;;;;
+1462B;ANATOLIAN HIEROGLYPH A503;Lo;0;L;;;;;N;;;;;
+1462C;ANATOLIAN HIEROGLYPH A504;Lo;0;L;;;;;N;;;;;
+1462D;ANATOLIAN HIEROGLYPH A505;Lo;0;L;;;;;N;;;;;
+1462E;ANATOLIAN HIEROGLYPH A506;Lo;0;L;;;;;N;;;;;
+1462F;ANATOLIAN HIEROGLYPH A507;Lo;0;L;;;;;N;;;;;
+14630;ANATOLIAN HIEROGLYPH A508;Lo;0;L;;;;;N;;;;;
+14631;ANATOLIAN HIEROGLYPH A509;Lo;0;L;;;;;N;;;;;
+14632;ANATOLIAN HIEROGLYPH A510;Lo;0;L;;;;;N;;;;;
+14633;ANATOLIAN HIEROGLYPH A511;Lo;0;L;;;;;N;;;;;
+14634;ANATOLIAN HIEROGLYPH A512;Lo;0;L;;;;;N;;;;;
+14635;ANATOLIAN HIEROGLYPH A513;Lo;0;L;;;;;N;;;;;
+14636;ANATOLIAN HIEROGLYPH A514;Lo;0;L;;;;;N;;;;;
+14637;ANATOLIAN HIEROGLYPH A515;Lo;0;L;;;;;N;;;;;
+14638;ANATOLIAN HIEROGLYPH A516;Lo;0;L;;;;;N;;;;;
+14639;ANATOLIAN HIEROGLYPH A517;Lo;0;L;;;;;N;;;;;
+1463A;ANATOLIAN HIEROGLYPH A518;Lo;0;L;;;;;N;;;;;
+1463B;ANATOLIAN HIEROGLYPH A519;Lo;0;L;;;;;N;;;;;
+1463C;ANATOLIAN HIEROGLYPH A520;Lo;0;L;;;;;N;;;;;
+1463D;ANATOLIAN HIEROGLYPH A521;Lo;0;L;;;;;N;;;;;
+1463E;ANATOLIAN HIEROGLYPH A522;Lo;0;L;;;;;N;;;;;
+1463F;ANATOLIAN HIEROGLYPH A523;Lo;0;L;;;;;N;;;;;
+14640;ANATOLIAN HIEROGLYPH A524;Lo;0;L;;;;;N;;;;;
+14641;ANATOLIAN HIEROGLYPH A525;Lo;0;L;;;;;N;;;;;
+14642;ANATOLIAN HIEROGLYPH A526;Lo;0;L;;;;;N;;;;;
+14643;ANATOLIAN HIEROGLYPH A527;Lo;0;L;;;;;N;;;;;
+14644;ANATOLIAN HIEROGLYPH A528;Lo;0;L;;;;;N;;;;;
+14645;ANATOLIAN HIEROGLYPH A529;Lo;0;L;;;;;N;;;;;
+14646;ANATOLIAN HIEROGLYPH A530;Lo;0;L;;;;;N;;;;;
+16800;BAMUM LETTER PHASE-A NGKUE MFON;Lo;0;L;;;;;N;;;;;
+16801;BAMUM LETTER PHASE-A GBIEE FON;Lo;0;L;;;;;N;;;;;
+16802;BAMUM LETTER PHASE-A PON MFON PIPAEMGBIEE;Lo;0;L;;;;;N;;;;;
+16803;BAMUM LETTER PHASE-A PON MFON PIPAEMBA;Lo;0;L;;;;;N;;;;;
+16804;BAMUM LETTER PHASE-A NAA MFON;Lo;0;L;;;;;N;;;;;
+16805;BAMUM LETTER PHASE-A SHUENSHUET;Lo;0;L;;;;;N;;;;;
+16806;BAMUM LETTER PHASE-A TITA MFON;Lo;0;L;;;;;N;;;;;
+16807;BAMUM LETTER PHASE-A NZA MFON;Lo;0;L;;;;;N;;;;;
+16808;BAMUM LETTER PHASE-A SHINDA PA NJI;Lo;0;L;;;;;N;;;;;
+16809;BAMUM LETTER PHASE-A PON PA NJI PIPAEMGBIEE;Lo;0;L;;;;;N;;;;;
+1680A;BAMUM LETTER PHASE-A PON PA NJI PIPAEMBA;Lo;0;L;;;;;N;;;;;
+1680B;BAMUM LETTER PHASE-A MAEMBGBIEE;Lo;0;L;;;;;N;;;;;
+1680C;BAMUM LETTER PHASE-A TU MAEMBA;Lo;0;L;;;;;N;;;;;
+1680D;BAMUM LETTER PHASE-A NGANGU;Lo;0;L;;;;;N;;;;;
+1680E;BAMUM LETTER PHASE-A MAEMVEUX;Lo;0;L;;;;;N;;;;;
+1680F;BAMUM LETTER PHASE-A MANSUAE;Lo;0;L;;;;;N;;;;;
+16810;BAMUM LETTER PHASE-A MVEUAENGAM;Lo;0;L;;;;;N;;;;;
+16811;BAMUM LETTER PHASE-A SEUNYAM;Lo;0;L;;;;;N;;;;;
+16812;BAMUM LETTER PHASE-A NTOQPEN;Lo;0;L;;;;;N;;;;;
+16813;BAMUM LETTER PHASE-A KEUKEUTNDA;Lo;0;L;;;;;N;;;;;
+16814;BAMUM LETTER PHASE-A NKINDI;Lo;0;L;;;;;N;;;;;
+16815;BAMUM LETTER PHASE-A SUU;Lo;0;L;;;;;N;;;;;
+16816;BAMUM LETTER PHASE-A NGKUENZEUM;Lo;0;L;;;;;N;;;;;
+16817;BAMUM LETTER PHASE-A LAPAQ;Lo;0;L;;;;;N;;;;;
+16818;BAMUM LETTER PHASE-A LET KUT;Lo;0;L;;;;;N;;;;;
+16819;BAMUM LETTER PHASE-A NTAP MFAA;Lo;0;L;;;;;N;;;;;
+1681A;BAMUM LETTER PHASE-A MAEKEUP;Lo;0;L;;;;;N;;;;;
+1681B;BAMUM LETTER PHASE-A PASHAE;Lo;0;L;;;;;N;;;;;
+1681C;BAMUM LETTER PHASE-A GHEUAERAE;Lo;0;L;;;;;N;;;;;
+1681D;BAMUM LETTER PHASE-A PAMSHAE;Lo;0;L;;;;;N;;;;;
+1681E;BAMUM LETTER PHASE-A MON NGGEUAET;Lo;0;L;;;;;N;;;;;
+1681F;BAMUM LETTER PHASE-A NZUN MEUT;Lo;0;L;;;;;N;;;;;
+16820;BAMUM LETTER PHASE-A U YUQ NAE;Lo;0;L;;;;;N;;;;;
+16821;BAMUM LETTER PHASE-A GHEUAEGHEUAE;Lo;0;L;;;;;N;;;;;
+16822;BAMUM LETTER PHASE-A NTAP NTAA;Lo;0;L;;;;;N;;;;;
+16823;BAMUM LETTER PHASE-A SISA;Lo;0;L;;;;;N;;;;;
+16824;BAMUM LETTER PHASE-A MGBASA;Lo;0;L;;;;;N;;;;;
+16825;BAMUM LETTER PHASE-A MEUNJOMNDEUQ;Lo;0;L;;;;;N;;;;;
+16826;BAMUM LETTER PHASE-A MOOMPUQ;Lo;0;L;;;;;N;;;;;
+16827;BAMUM LETTER PHASE-A KAFA;Lo;0;L;;;;;N;;;;;
+16828;BAMUM LETTER PHASE-A PA LEERAEWA;Lo;0;L;;;;;N;;;;;
+16829;BAMUM LETTER PHASE-A NDA LEERAEWA;Lo;0;L;;;;;N;;;;;
+1682A;BAMUM LETTER PHASE-A PET;Lo;0;L;;;;;N;;;;;
+1682B;BAMUM LETTER PHASE-A MAEMKPEN;Lo;0;L;;;;;N;;;;;
+1682C;BAMUM LETTER PHASE-A NIKA;Lo;0;L;;;;;N;;;;;
+1682D;BAMUM LETTER PHASE-A PUP;Lo;0;L;;;;;N;;;;;
+1682E;BAMUM LETTER PHASE-A TUAEP;Lo;0;L;;;;;N;;;;;
+1682F;BAMUM LETTER PHASE-A LUAEP;Lo;0;L;;;;;N;;;;;
+16830;BAMUM LETTER PHASE-A SONJAM;Lo;0;L;;;;;N;;;;;
+16831;BAMUM LETTER PHASE-A TEUTEUWEN;Lo;0;L;;;;;N;;;;;
+16832;BAMUM LETTER PHASE-A MAENYI;Lo;0;L;;;;;N;;;;;
+16833;BAMUM LETTER PHASE-A KET;Lo;0;L;;;;;N;;;;;
+16834;BAMUM LETTER PHASE-A NDAANGGEUAET;Lo;0;L;;;;;N;;;;;
+16835;BAMUM LETTER PHASE-A KUOQ;Lo;0;L;;;;;N;;;;;
+16836;BAMUM LETTER PHASE-A MOOMEUT;Lo;0;L;;;;;N;;;;;
+16837;BAMUM LETTER PHASE-A SHUM;Lo;0;L;;;;;N;;;;;
+16838;BAMUM LETTER PHASE-A LOMMAE;Lo;0;L;;;;;N;;;;;
+16839;BAMUM LETTER PHASE-A FIRI;Lo;0;L;;;;;N;;;;;
+1683A;BAMUM LETTER PHASE-A ROM;Lo;0;L;;;;;N;;;;;
+1683B;BAMUM LETTER PHASE-A KPOQ;Lo;0;L;;;;;N;;;;;
+1683C;BAMUM LETTER PHASE-A SOQ;Lo;0;L;;;;;N;;;;;
+1683D;BAMUM LETTER PHASE-A MAP PIEET;Lo;0;L;;;;;N;;;;;
+1683E;BAMUM LETTER PHASE-A SHIRAE;Lo;0;L;;;;;N;;;;;
+1683F;BAMUM LETTER PHASE-A NTAP;Lo;0;L;;;;;N;;;;;
+16840;BAMUM LETTER PHASE-A SHOQ NSHUT YUM;Lo;0;L;;;;;N;;;;;
+16841;BAMUM LETTER PHASE-A NYIT MONGKEUAEQ;Lo;0;L;;;;;N;;;;;
+16842;BAMUM LETTER PHASE-A PAARAE;Lo;0;L;;;;;N;;;;;
+16843;BAMUM LETTER PHASE-A NKAARAE;Lo;0;L;;;;;N;;;;;
+16844;BAMUM LETTER PHASE-A UNKNOWN;Lo;0;L;;;;;N;;;;;
+16845;BAMUM LETTER PHASE-A NGGEN;Lo;0;L;;;;;N;;;;;
+16846;BAMUM LETTER PHASE-A MAESI;Lo;0;L;;;;;N;;;;;
+16847;BAMUM LETTER PHASE-A NJAM;Lo;0;L;;;;;N;;;;;
+16848;BAMUM LETTER PHASE-A MBANYI;Lo;0;L;;;;;N;;;;;
+16849;BAMUM LETTER PHASE-A NYET;Lo;0;L;;;;;N;;;;;
+1684A;BAMUM LETTER PHASE-A TEUAEN;Lo;0;L;;;;;N;;;;;
+1684B;BAMUM LETTER PHASE-A SOT;Lo;0;L;;;;;N;;;;;
+1684C;BAMUM LETTER PHASE-A PAAM;Lo;0;L;;;;;N;;;;;
+1684D;BAMUM LETTER PHASE-A NSHIEE;Lo;0;L;;;;;N;;;;;
+1684E;BAMUM LETTER PHASE-A MAEM;Lo;0;L;;;;;N;;;;;
+1684F;BAMUM LETTER PHASE-A NYI;Lo;0;L;;;;;N;;;;;
+16850;BAMUM LETTER PHASE-A KAQ;Lo;0;L;;;;;N;;;;;
+16851;BAMUM LETTER PHASE-A NSHA;Lo;0;L;;;;;N;;;;;
+16852;BAMUM LETTER PHASE-A VEE;Lo;0;L;;;;;N;;;;;
+16853;BAMUM LETTER PHASE-A LU;Lo;0;L;;;;;N;;;;;
+16854;BAMUM LETTER PHASE-A NEN;Lo;0;L;;;;;N;;;;;
+16855;BAMUM LETTER PHASE-A NAQ;Lo;0;L;;;;;N;;;;;
+16856;BAMUM LETTER PHASE-A MBAQ;Lo;0;L;;;;;N;;;;;
+16857;BAMUM LETTER PHASE-B NSHUET;Lo;0;L;;;;;N;;;;;
+16858;BAMUM LETTER PHASE-B TU MAEMGBIEE;Lo;0;L;;;;;N;;;;;
+16859;BAMUM LETTER PHASE-B SIEE;Lo;0;L;;;;;N;;;;;
+1685A;BAMUM LETTER PHASE-B SET TU;Lo;0;L;;;;;N;;;;;
+1685B;BAMUM LETTER PHASE-B LOM NTEUM;Lo;0;L;;;;;N;;;;;
+1685C;BAMUM LETTER PHASE-B MBA MAELEE;Lo;0;L;;;;;N;;;;;
+1685D;BAMUM LETTER PHASE-B KIEEM;Lo;0;L;;;;;N;;;;;
+1685E;BAMUM LETTER PHASE-B YEURAE;Lo;0;L;;;;;N;;;;;
+1685F;BAMUM LETTER PHASE-B MBAARAE;Lo;0;L;;;;;N;;;;;
+16860;BAMUM LETTER PHASE-B KAM;Lo;0;L;;;;;N;;;;;
+16861;BAMUM LETTER PHASE-B PEESHI;Lo;0;L;;;;;N;;;;;
+16862;BAMUM LETTER PHASE-B YAFU LEERAEWA;Lo;0;L;;;;;N;;;;;
+16863;BAMUM LETTER PHASE-B LAM NSHUT NYAM;Lo;0;L;;;;;N;;;;;
+16864;BAMUM LETTER PHASE-B NTIEE SHEUOQ;Lo;0;L;;;;;N;;;;;
+16865;BAMUM LETTER PHASE-B NDU NJAA;Lo;0;L;;;;;N;;;;;
+16866;BAMUM LETTER PHASE-B GHEUGHEUAEM;Lo;0;L;;;;;N;;;;;
+16867;BAMUM LETTER PHASE-B PIT;Lo;0;L;;;;;N;;;;;
+16868;BAMUM LETTER PHASE-B TU NSIEE;Lo;0;L;;;;;N;;;;;
+16869;BAMUM LETTER PHASE-B SHET NJAQ;Lo;0;L;;;;;N;;;;;
+1686A;BAMUM LETTER PHASE-B SHEUAEQTU;Lo;0;L;;;;;N;;;;;
+1686B;BAMUM LETTER PHASE-B MFON TEUAEQ;Lo;0;L;;;;;N;;;;;
+1686C;BAMUM LETTER PHASE-B MBIT MBAAKET;Lo;0;L;;;;;N;;;;;
+1686D;BAMUM LETTER PHASE-B NYI NTEUM;Lo;0;L;;;;;N;;;;;
+1686E;BAMUM LETTER PHASE-B KEUPUQ;Lo;0;L;;;;;N;;;;;
+1686F;BAMUM LETTER PHASE-B GHEUGHEN;Lo;0;L;;;;;N;;;;;
+16870;BAMUM LETTER PHASE-B KEUYEUX;Lo;0;L;;;;;N;;;;;
+16871;BAMUM LETTER PHASE-B LAANAE;Lo;0;L;;;;;N;;;;;
+16872;BAMUM LETTER PHASE-B PARUM;Lo;0;L;;;;;N;;;;;
+16873;BAMUM LETTER PHASE-B VEUM;Lo;0;L;;;;;N;;;;;
+16874;BAMUM LETTER PHASE-B NGKINDI MVOP;Lo;0;L;;;;;N;;;;;
+16875;BAMUM LETTER PHASE-B NGGEU MBU;Lo;0;L;;;;;N;;;;;
+16876;BAMUM LETTER PHASE-B WUAET;Lo;0;L;;;;;N;;;;;
+16877;BAMUM LETTER PHASE-B SAKEUAE;Lo;0;L;;;;;N;;;;;
+16878;BAMUM LETTER PHASE-B TAAM;Lo;0;L;;;;;N;;;;;
+16879;BAMUM LETTER PHASE-B MEUQ;Lo;0;L;;;;;N;;;;;
+1687A;BAMUM LETTER PHASE-B NGGUOQ;Lo;0;L;;;;;N;;;;;
+1687B;BAMUM LETTER PHASE-B NGGUOQ LARGE;Lo;0;L;;;;;N;;;;;
+1687C;BAMUM LETTER PHASE-B MFIYAQ;Lo;0;L;;;;;N;;;;;
+1687D;BAMUM LETTER PHASE-B SUE;Lo;0;L;;;;;N;;;;;
+1687E;BAMUM LETTER PHASE-B MBEURI;Lo;0;L;;;;;N;;;;;
+1687F;BAMUM LETTER PHASE-B MONTIEEN;Lo;0;L;;;;;N;;;;;
+16880;BAMUM LETTER PHASE-B NYAEMAE;Lo;0;L;;;;;N;;;;;
+16881;BAMUM LETTER PHASE-B PUNGAAM;Lo;0;L;;;;;N;;;;;
+16882;BAMUM LETTER PHASE-B MEUT NGGEET;Lo;0;L;;;;;N;;;;;
+16883;BAMUM LETTER PHASE-B FEUX;Lo;0;L;;;;;N;;;;;
+16884;BAMUM LETTER PHASE-B MBUOQ;Lo;0;L;;;;;N;;;;;
+16885;BAMUM LETTER PHASE-B FEE;Lo;0;L;;;;;N;;;;;
+16886;BAMUM LETTER PHASE-B KEUAEM;Lo;0;L;;;;;N;;;;;
+16887;BAMUM LETTER PHASE-B MA NJEUAENA;Lo;0;L;;;;;N;;;;;
+16888;BAMUM LETTER PHASE-B MA NJUQA;Lo;0;L;;;;;N;;;;;
+16889;BAMUM LETTER PHASE-B LET;Lo;0;L;;;;;N;;;;;
+1688A;BAMUM LETTER PHASE-B NGGAAM;Lo;0;L;;;;;N;;;;;
+1688B;BAMUM LETTER PHASE-B NSEN;Lo;0;L;;;;;N;;;;;
+1688C;BAMUM LETTER PHASE-B MA;Lo;0;L;;;;;N;;;;;
+1688D;BAMUM LETTER PHASE-B KIQ;Lo;0;L;;;;;N;;;;;
+1688E;BAMUM LETTER PHASE-B NGOM;Lo;0;L;;;;;N;;;;;
+1688F;BAMUM LETTER PHASE-C NGKUE MAEMBA;Lo;0;L;;;;;N;;;;;
+16890;BAMUM LETTER PHASE-C NZA;Lo;0;L;;;;;N;;;;;
+16891;BAMUM LETTER PHASE-C YUM;Lo;0;L;;;;;N;;;;;
+16892;BAMUM LETTER PHASE-C WANGKUOQ;Lo;0;L;;;;;N;;;;;
+16893;BAMUM LETTER PHASE-C NGGEN;Lo;0;L;;;;;N;;;;;
+16894;BAMUM LETTER PHASE-C NDEUAEREE;Lo;0;L;;;;;N;;;;;
+16895;BAMUM LETTER PHASE-C NGKAQ;Lo;0;L;;;;;N;;;;;
+16896;BAMUM LETTER PHASE-C GHARAE;Lo;0;L;;;;;N;;;;;
+16897;BAMUM LETTER PHASE-C MBEEKEET;Lo;0;L;;;;;N;;;;;
+16898;BAMUM LETTER PHASE-C GBAYI;Lo;0;L;;;;;N;;;;;
+16899;BAMUM LETTER PHASE-C NYIR MKPARAQ MEUN;Lo;0;L;;;;;N;;;;;
+1689A;BAMUM LETTER PHASE-C NTU MBIT;Lo;0;L;;;;;N;;;;;
+1689B;BAMUM LETTER PHASE-C MBEUM;Lo;0;L;;;;;N;;;;;
+1689C;BAMUM LETTER PHASE-C PIRIEEN;Lo;0;L;;;;;N;;;;;
+1689D;BAMUM LETTER PHASE-C NDOMBU;Lo;0;L;;;;;N;;;;;
+1689E;BAMUM LETTER PHASE-C MBAA CABBAGE-TREE;Lo;0;L;;;;;N;;;;;
+1689F;BAMUM LETTER PHASE-C KEUSHEUAEP;Lo;0;L;;;;;N;;;;;
+168A0;BAMUM LETTER PHASE-C GHAP;Lo;0;L;;;;;N;;;;;
+168A1;BAMUM LETTER PHASE-C KEUKAQ;Lo;0;L;;;;;N;;;;;
+168A2;BAMUM LETTER PHASE-C YU MUOMAE;Lo;0;L;;;;;N;;;;;
+168A3;BAMUM LETTER PHASE-C NZEUM;Lo;0;L;;;;;N;;;;;
+168A4;BAMUM LETTER PHASE-C MBUE;Lo;0;L;;;;;N;;;;;
+168A5;BAMUM LETTER PHASE-C NSEUAEN;Lo;0;L;;;;;N;;;;;
+168A6;BAMUM LETTER PHASE-C MBIT;Lo;0;L;;;;;N;;;;;
+168A7;BAMUM LETTER PHASE-C YEUQ;Lo;0;L;;;;;N;;;;;
+168A8;BAMUM LETTER PHASE-C KPARAQ;Lo;0;L;;;;;N;;;;;
+168A9;BAMUM LETTER PHASE-C KAA;Lo;0;L;;;;;N;;;;;
+168AA;BAMUM LETTER PHASE-C SEUX;Lo;0;L;;;;;N;;;;;
+168AB;BAMUM LETTER PHASE-C NDIDA;Lo;0;L;;;;;N;;;;;
+168AC;BAMUM LETTER PHASE-C TAASHAE;Lo;0;L;;;;;N;;;;;
+168AD;BAMUM LETTER PHASE-C NJUEQ;Lo;0;L;;;;;N;;;;;
+168AE;BAMUM LETTER PHASE-C TITA YUE;Lo;0;L;;;;;N;;;;;
+168AF;BAMUM LETTER PHASE-C SUAET;Lo;0;L;;;;;N;;;;;
+168B0;BAMUM LETTER PHASE-C NGGUAEN NYAM;Lo;0;L;;;;;N;;;;;
+168B1;BAMUM LETTER PHASE-C VEUX;Lo;0;L;;;;;N;;;;;
+168B2;BAMUM LETTER PHASE-C NANSANAQ;Lo;0;L;;;;;N;;;;;
+168B3;BAMUM LETTER PHASE-C MA KEUAERI;Lo;0;L;;;;;N;;;;;
+168B4;BAMUM LETTER PHASE-C NTAA;Lo;0;L;;;;;N;;;;;
+168B5;BAMUM LETTER PHASE-C NGGUON;Lo;0;L;;;;;N;;;;;
+168B6;BAMUM LETTER PHASE-C LAP;Lo;0;L;;;;;N;;;;;
+168B7;BAMUM LETTER PHASE-C MBIRIEEN;Lo;0;L;;;;;N;;;;;
+168B8;BAMUM LETTER PHASE-C MGBASAQ;Lo;0;L;;;;;N;;;;;
+168B9;BAMUM LETTER PHASE-C NTEUNGBA;Lo;0;L;;;;;N;;;;;
+168BA;BAMUM LETTER PHASE-C TEUTEUX;Lo;0;L;;;;;N;;;;;
+168BB;BAMUM LETTER PHASE-C NGGUM;Lo;0;L;;;;;N;;;;;
+168BC;BAMUM LETTER PHASE-C FUE;Lo;0;L;;;;;N;;;;;
+168BD;BAMUM LETTER PHASE-C NDEUT;Lo;0;L;;;;;N;;;;;
+168BE;BAMUM LETTER PHASE-C NSA;Lo;0;L;;;;;N;;;;;
+168BF;BAMUM LETTER PHASE-C NSHAQ;Lo;0;L;;;;;N;;;;;
+168C0;BAMUM LETTER PHASE-C BUNG;Lo;0;L;;;;;N;;;;;
+168C1;BAMUM LETTER PHASE-C VEUAEPEN;Lo;0;L;;;;;N;;;;;
+168C2;BAMUM LETTER PHASE-C MBERAE;Lo;0;L;;;;;N;;;;;
+168C3;BAMUM LETTER PHASE-C RU;Lo;0;L;;;;;N;;;;;
+168C4;BAMUM LETTER PHASE-C NJAEM;Lo;0;L;;;;;N;;;;;
+168C5;BAMUM LETTER PHASE-C LAM;Lo;0;L;;;;;N;;;;;
+168C6;BAMUM LETTER PHASE-C TITUAEP;Lo;0;L;;;;;N;;;;;
+168C7;BAMUM LETTER PHASE-C NSUOT NGOM;Lo;0;L;;;;;N;;;;;
+168C8;BAMUM LETTER PHASE-C NJEEEE;Lo;0;L;;;;;N;;;;;
+168C9;BAMUM LETTER PHASE-C KET;Lo;0;L;;;;;N;;;;;
+168CA;BAMUM LETTER PHASE-C NGGU;Lo;0;L;;;;;N;;;;;
+168CB;BAMUM LETTER PHASE-C MAESI;Lo;0;L;;;;;N;;;;;
+168CC;BAMUM LETTER PHASE-C MBUAEM;Lo;0;L;;;;;N;;;;;
+168CD;BAMUM LETTER PHASE-C LU;Lo;0;L;;;;;N;;;;;
+168CE;BAMUM LETTER PHASE-C KUT;Lo;0;L;;;;;N;;;;;
+168CF;BAMUM LETTER PHASE-C NJAM;Lo;0;L;;;;;N;;;;;
+168D0;BAMUM LETTER PHASE-C NGOM;Lo;0;L;;;;;N;;;;;
+168D1;BAMUM LETTER PHASE-C WUP;Lo;0;L;;;;;N;;;;;
+168D2;BAMUM LETTER PHASE-C NGGUEET;Lo;0;L;;;;;N;;;;;
+168D3;BAMUM LETTER PHASE-C NSOM;Lo;0;L;;;;;N;;;;;
+168D4;BAMUM LETTER PHASE-C NTEN;Lo;0;L;;;;;N;;;;;
+168D5;BAMUM LETTER PHASE-C KUOP NKAARAE;Lo;0;L;;;;;N;;;;;
+168D6;BAMUM LETTER PHASE-C NSUN;Lo;0;L;;;;;N;;;;;
+168D7;BAMUM LETTER PHASE-C NDAM;Lo;0;L;;;;;N;;;;;
+168D8;BAMUM LETTER PHASE-C MA NSIEE;Lo;0;L;;;;;N;;;;;
+168D9;BAMUM LETTER PHASE-C YAA;Lo;0;L;;;;;N;;;;;
+168DA;BAMUM LETTER PHASE-C NDAP;Lo;0;L;;;;;N;;;;;
+168DB;BAMUM LETTER PHASE-C SHUEQ;Lo;0;L;;;;;N;;;;;
+168DC;BAMUM LETTER PHASE-C SETFON;Lo;0;L;;;;;N;;;;;
+168DD;BAMUM LETTER PHASE-C MBI;Lo;0;L;;;;;N;;;;;
+168DE;BAMUM LETTER PHASE-C MAEMBA;Lo;0;L;;;;;N;;;;;
+168DF;BAMUM LETTER PHASE-C MBANYI;Lo;0;L;;;;;N;;;;;
+168E0;BAMUM LETTER PHASE-C KEUSEUX;Lo;0;L;;;;;N;;;;;
+168E1;BAMUM LETTER PHASE-C MBEUX;Lo;0;L;;;;;N;;;;;
+168E2;BAMUM LETTER PHASE-C KEUM;Lo;0;L;;;;;N;;;;;
+168E3;BAMUM LETTER PHASE-C MBAA PICKET;Lo;0;L;;;;;N;;;;;
+168E4;BAMUM LETTER PHASE-C YUWOQ;Lo;0;L;;;;;N;;;;;
+168E5;BAMUM LETTER PHASE-C NJEUX;Lo;0;L;;;;;N;;;;;
+168E6;BAMUM LETTER PHASE-C MIEE;Lo;0;L;;;;;N;;;;;
+168E7;BAMUM LETTER PHASE-C MUAE;Lo;0;L;;;;;N;;;;;
+168E8;BAMUM LETTER PHASE-C SHIQ;Lo;0;L;;;;;N;;;;;
+168E9;BAMUM LETTER PHASE-C KEN LAW;Lo;0;L;;;;;N;;;;;
+168EA;BAMUM LETTER PHASE-C KEN FATIGUE;Lo;0;L;;;;;N;;;;;
+168EB;BAMUM LETTER PHASE-C NGAQ;Lo;0;L;;;;;N;;;;;
+168EC;BAMUM LETTER PHASE-C NAQ;Lo;0;L;;;;;N;;;;;
+168ED;BAMUM LETTER PHASE-C LIQ;Lo;0;L;;;;;N;;;;;
+168EE;BAMUM LETTER PHASE-C PIN;Lo;0;L;;;;;N;;;;;
+168EF;BAMUM LETTER PHASE-C PEN;Lo;0;L;;;;;N;;;;;
+168F0;BAMUM LETTER PHASE-C TET;Lo;0;L;;;;;N;;;;;
+168F1;BAMUM LETTER PHASE-D MBUO;Lo;0;L;;;;;N;;;;;
+168F2;BAMUM LETTER PHASE-D WAP;Lo;0;L;;;;;N;;;;;
+168F3;BAMUM LETTER PHASE-D NJI;Lo;0;L;;;;;N;;;;;
+168F4;BAMUM LETTER PHASE-D MFON;Lo;0;L;;;;;N;;;;;
+168F5;BAMUM LETTER PHASE-D NJIEE;Lo;0;L;;;;;N;;;;;
+168F6;BAMUM LETTER PHASE-D LIEE;Lo;0;L;;;;;N;;;;;
+168F7;BAMUM LETTER PHASE-D NJEUT;Lo;0;L;;;;;N;;;;;
+168F8;BAMUM LETTER PHASE-D NSHEE;Lo;0;L;;;;;N;;;;;
+168F9;BAMUM LETTER PHASE-D NGGAAMAE;Lo;0;L;;;;;N;;;;;
+168FA;BAMUM LETTER PHASE-D NYAM;Lo;0;L;;;;;N;;;;;
+168FB;BAMUM LETTER PHASE-D WUAEN;Lo;0;L;;;;;N;;;;;
+168FC;BAMUM LETTER PHASE-D NGKUN;Lo;0;L;;;;;N;;;;;
+168FD;BAMUM LETTER PHASE-D SHEE;Lo;0;L;;;;;N;;;;;
+168FE;BAMUM LETTER PHASE-D NGKAP;Lo;0;L;;;;;N;;;;;
+168FF;BAMUM LETTER PHASE-D KEUAETMEUN;Lo;0;L;;;;;N;;;;;
+16900;BAMUM LETTER PHASE-D TEUT;Lo;0;L;;;;;N;;;;;
+16901;BAMUM LETTER PHASE-D SHEUAE;Lo;0;L;;;;;N;;;;;
+16902;BAMUM LETTER PHASE-D NJAP;Lo;0;L;;;;;N;;;;;
+16903;BAMUM LETTER PHASE-D SUE;Lo;0;L;;;;;N;;;;;
+16904;BAMUM LETTER PHASE-D KET;Lo;0;L;;;;;N;;;;;
+16905;BAMUM LETTER PHASE-D YAEMMAE;Lo;0;L;;;;;N;;;;;
+16906;BAMUM LETTER PHASE-D KUOM;Lo;0;L;;;;;N;;;;;
+16907;BAMUM LETTER PHASE-D SAP;Lo;0;L;;;;;N;;;;;
+16908;BAMUM LETTER PHASE-D MFEUT;Lo;0;L;;;;;N;;;;;
+16909;BAMUM LETTER PHASE-D NDEUX;Lo;0;L;;;;;N;;;;;
+1690A;BAMUM LETTER PHASE-D MALEERI;Lo;0;L;;;;;N;;;;;
+1690B;BAMUM LETTER PHASE-D MEUT;Lo;0;L;;;;;N;;;;;
+1690C;BAMUM LETTER PHASE-D SEUAEQ;Lo;0;L;;;;;N;;;;;
+1690D;BAMUM LETTER PHASE-D YEN;Lo;0;L;;;;;N;;;;;
+1690E;BAMUM LETTER PHASE-D NJEUAEM;Lo;0;L;;;;;N;;;;;
+1690F;BAMUM LETTER PHASE-D KEUOT MBUAE;Lo;0;L;;;;;N;;;;;
+16910;BAMUM LETTER PHASE-D NGKEURI;Lo;0;L;;;;;N;;;;;
+16911;BAMUM LETTER PHASE-D TU;Lo;0;L;;;;;N;;;;;
+16912;BAMUM LETTER PHASE-D GHAA;Lo;0;L;;;;;N;;;;;
+16913;BAMUM LETTER PHASE-D NGKYEE;Lo;0;L;;;;;N;;;;;
+16914;BAMUM LETTER PHASE-D FEUFEUAET;Lo;0;L;;;;;N;;;;;
+16915;BAMUM LETTER PHASE-D NDEE;Lo;0;L;;;;;N;;;;;
+16916;BAMUM LETTER PHASE-D MGBOFUM;Lo;0;L;;;;;N;;;;;
+16917;BAMUM LETTER PHASE-D LEUAEP;Lo;0;L;;;;;N;;;;;
+16918;BAMUM LETTER PHASE-D NDON;Lo;0;L;;;;;N;;;;;
+16919;BAMUM LETTER PHASE-D MONI;Lo;0;L;;;;;N;;;;;
+1691A;BAMUM LETTER PHASE-D MGBEUN;Lo;0;L;;;;;N;;;;;
+1691B;BAMUM LETTER PHASE-D PUUT;Lo;0;L;;;;;N;;;;;
+1691C;BAMUM LETTER PHASE-D MGBIEE;Lo;0;L;;;;;N;;;;;
+1691D;BAMUM LETTER PHASE-D MFO;Lo;0;L;;;;;N;;;;;
+1691E;BAMUM LETTER PHASE-D LUM;Lo;0;L;;;;;N;;;;;
+1691F;BAMUM LETTER PHASE-D NSIEEP;Lo;0;L;;;;;N;;;;;
+16920;BAMUM LETTER PHASE-D MBAA;Lo;0;L;;;;;N;;;;;
+16921;BAMUM LETTER PHASE-D KWAET;Lo;0;L;;;;;N;;;;;
+16922;BAMUM LETTER PHASE-D NYET;Lo;0;L;;;;;N;;;;;
+16923;BAMUM LETTER PHASE-D TEUAEN;Lo;0;L;;;;;N;;;;;
+16924;BAMUM LETTER PHASE-D SOT;Lo;0;L;;;;;N;;;;;
+16925;BAMUM LETTER PHASE-D YUWOQ;Lo;0;L;;;;;N;;;;;
+16926;BAMUM LETTER PHASE-D KEUM;Lo;0;L;;;;;N;;;;;
+16927;BAMUM LETTER PHASE-D RAEM;Lo;0;L;;;;;N;;;;;
+16928;BAMUM LETTER PHASE-D TEEEE;Lo;0;L;;;;;N;;;;;
+16929;BAMUM LETTER PHASE-D NGKEUAEQ;Lo;0;L;;;;;N;;;;;
+1692A;BAMUM LETTER PHASE-D MFEUAE;Lo;0;L;;;;;N;;;;;
+1692B;BAMUM LETTER PHASE-D NSIEET;Lo;0;L;;;;;N;;;;;
+1692C;BAMUM LETTER PHASE-D KEUP;Lo;0;L;;;;;N;;;;;
+1692D;BAMUM LETTER PHASE-D PIP;Lo;0;L;;;;;N;;;;;
+1692E;BAMUM LETTER PHASE-D PEUTAE;Lo;0;L;;;;;N;;;;;
+1692F;BAMUM LETTER PHASE-D NYUE;Lo;0;L;;;;;N;;;;;
+16930;BAMUM LETTER PHASE-D LET;Lo;0;L;;;;;N;;;;;
+16931;BAMUM LETTER PHASE-D NGGAAM;Lo;0;L;;;;;N;;;;;
+16932;BAMUM LETTER PHASE-D MFIEE;Lo;0;L;;;;;N;;;;;
+16933;BAMUM LETTER PHASE-D NGGWAEN;Lo;0;L;;;;;N;;;;;
+16934;BAMUM LETTER PHASE-D YUOM;Lo;0;L;;;;;N;;;;;
+16935;BAMUM LETTER PHASE-D PAP;Lo;0;L;;;;;N;;;;;
+16936;BAMUM LETTER PHASE-D YUOP;Lo;0;L;;;;;N;;;;;
+16937;BAMUM LETTER PHASE-D NDAM;Lo;0;L;;;;;N;;;;;
+16938;BAMUM LETTER PHASE-D NTEUM;Lo;0;L;;;;;N;;;;;
+16939;BAMUM LETTER PHASE-D SUAE;Lo;0;L;;;;;N;;;;;
+1693A;BAMUM LETTER PHASE-D KUN;Lo;0;L;;;;;N;;;;;
+1693B;BAMUM LETTER PHASE-D NGGEUX;Lo;0;L;;;;;N;;;;;
+1693C;BAMUM LETTER PHASE-D NGKIEE;Lo;0;L;;;;;N;;;;;
+1693D;BAMUM LETTER PHASE-D TUOT;Lo;0;L;;;;;N;;;;;
+1693E;BAMUM LETTER PHASE-D MEUN;Lo;0;L;;;;;N;;;;;
+1693F;BAMUM LETTER PHASE-D KUQ;Lo;0;L;;;;;N;;;;;
+16940;BAMUM LETTER PHASE-D NSUM;Lo;0;L;;;;;N;;;;;
+16941;BAMUM LETTER PHASE-D TEUN;Lo;0;L;;;;;N;;;;;
+16942;BAMUM LETTER PHASE-D MAENJET;Lo;0;L;;;;;N;;;;;
+16943;BAMUM LETTER PHASE-D NGGAP;Lo;0;L;;;;;N;;;;;
+16944;BAMUM LETTER PHASE-D LEUM;Lo;0;L;;;;;N;;;;;
+16945;BAMUM LETTER PHASE-D NGGUOM;Lo;0;L;;;;;N;;;;;
+16946;BAMUM LETTER PHASE-D NSHUT;Lo;0;L;;;;;N;;;;;
+16947;BAMUM LETTER PHASE-D NJUEQ;Lo;0;L;;;;;N;;;;;
+16948;BAMUM LETTER PHASE-D GHEUAE;Lo;0;L;;;;;N;;;;;
+16949;BAMUM LETTER PHASE-D KU;Lo;0;L;;;;;N;;;;;
+1694A;BAMUM LETTER PHASE-D REN OLD;Lo;0;L;;;;;N;;;;;
+1694B;BAMUM LETTER PHASE-D TAE;Lo;0;L;;;;;N;;;;;
+1694C;BAMUM LETTER PHASE-D TOQ;Lo;0;L;;;;;N;;;;;
+1694D;BAMUM LETTER PHASE-D NYI;Lo;0;L;;;;;N;;;;;
+1694E;BAMUM LETTER PHASE-D RII;Lo;0;L;;;;;N;;;;;
+1694F;BAMUM LETTER PHASE-D LEEEE;Lo;0;L;;;;;N;;;;;
+16950;BAMUM LETTER PHASE-D MEEEE;Lo;0;L;;;;;N;;;;;
+16951;BAMUM LETTER PHASE-D M;Lo;0;L;;;;;N;;;;;
+16952;BAMUM LETTER PHASE-D SUU;Lo;0;L;;;;;N;;;;;
+16953;BAMUM LETTER PHASE-D MU;Lo;0;L;;;;;N;;;;;
+16954;BAMUM LETTER PHASE-D SHII;Lo;0;L;;;;;N;;;;;
+16955;BAMUM LETTER PHASE-D SHEUX;Lo;0;L;;;;;N;;;;;
+16956;BAMUM LETTER PHASE-D KYEE;Lo;0;L;;;;;N;;;;;
+16957;BAMUM LETTER PHASE-D NU;Lo;0;L;;;;;N;;;;;
+16958;BAMUM LETTER PHASE-D SHU;Lo;0;L;;;;;N;;;;;
+16959;BAMUM LETTER PHASE-D NTEE;Lo;0;L;;;;;N;;;;;
+1695A;BAMUM LETTER PHASE-D PEE;Lo;0;L;;;;;N;;;;;
+1695B;BAMUM LETTER PHASE-D NI;Lo;0;L;;;;;N;;;;;
+1695C;BAMUM LETTER PHASE-D SHOQ;Lo;0;L;;;;;N;;;;;
+1695D;BAMUM LETTER PHASE-D PUQ;Lo;0;L;;;;;N;;;;;
+1695E;BAMUM LETTER PHASE-D MVOP;Lo;0;L;;;;;N;;;;;
+1695F;BAMUM LETTER PHASE-D LOQ;Lo;0;L;;;;;N;;;;;
+16960;BAMUM LETTER PHASE-D REN MUCH;Lo;0;L;;;;;N;;;;;
+16961;BAMUM LETTER PHASE-D TI;Lo;0;L;;;;;N;;;;;
+16962;BAMUM LETTER PHASE-D NTUU;Lo;0;L;;;;;N;;;;;
+16963;BAMUM LETTER PHASE-D MBAA SEVEN;Lo;0;L;;;;;N;;;;;
+16964;BAMUM LETTER PHASE-D SAQ;Lo;0;L;;;;;N;;;;;
+16965;BAMUM LETTER PHASE-D FAA;Lo;0;L;;;;;N;;;;;
+16966;BAMUM LETTER PHASE-E NDAP;Lo;0;L;;;;;N;;;;;
+16967;BAMUM LETTER PHASE-E TOON;Lo;0;L;;;;;N;;;;;
+16968;BAMUM LETTER PHASE-E MBEUM;Lo;0;L;;;;;N;;;;;
+16969;BAMUM LETTER PHASE-E LAP;Lo;0;L;;;;;N;;;;;
+1696A;BAMUM LETTER PHASE-E VOM;Lo;0;L;;;;;N;;;;;
+1696B;BAMUM LETTER PHASE-E LOON;Lo;0;L;;;;;N;;;;;
+1696C;BAMUM LETTER PHASE-E PAA;Lo;0;L;;;;;N;;;;;
+1696D;BAMUM LETTER PHASE-E SOM;Lo;0;L;;;;;N;;;;;
+1696E;BAMUM LETTER PHASE-E RAQ;Lo;0;L;;;;;N;;;;;
+1696F;BAMUM LETTER PHASE-E NSHUOP;Lo;0;L;;;;;N;;;;;
+16970;BAMUM LETTER PHASE-E NDUN;Lo;0;L;;;;;N;;;;;
+16971;BAMUM LETTER PHASE-E PUAE;Lo;0;L;;;;;N;;;;;
+16972;BAMUM LETTER PHASE-E TAM;Lo;0;L;;;;;N;;;;;
+16973;BAMUM LETTER PHASE-E NGKA;Lo;0;L;;;;;N;;;;;
+16974;BAMUM LETTER PHASE-E KPEUX;Lo;0;L;;;;;N;;;;;
+16975;BAMUM LETTER PHASE-E WUO;Lo;0;L;;;;;N;;;;;
+16976;BAMUM LETTER PHASE-E SEE;Lo;0;L;;;;;N;;;;;
+16977;BAMUM LETTER PHASE-E NGGEUAET;Lo;0;L;;;;;N;;;;;
+16978;BAMUM LETTER PHASE-E PAAM;Lo;0;L;;;;;N;;;;;
+16979;BAMUM LETTER PHASE-E TOO;Lo;0;L;;;;;N;;;;;
+1697A;BAMUM LETTER PHASE-E KUOP;Lo;0;L;;;;;N;;;;;
+1697B;BAMUM LETTER PHASE-E LOM;Lo;0;L;;;;;N;;;;;
+1697C;BAMUM LETTER PHASE-E NSHIEE;Lo;0;L;;;;;N;;;;;
+1697D;BAMUM LETTER PHASE-E NGOP;Lo;0;L;;;;;N;;;;;
+1697E;BAMUM LETTER PHASE-E MAEM;Lo;0;L;;;;;N;;;;;
+1697F;BAMUM LETTER PHASE-E NGKEUX;Lo;0;L;;;;;N;;;;;
+16980;BAMUM LETTER PHASE-E NGOQ;Lo;0;L;;;;;N;;;;;
+16981;BAMUM LETTER PHASE-E NSHUE;Lo;0;L;;;;;N;;;;;
+16982;BAMUM LETTER PHASE-E RIMGBA;Lo;0;L;;;;;N;;;;;
+16983;BAMUM LETTER PHASE-E NJEUX;Lo;0;L;;;;;N;;;;;
+16984;BAMUM LETTER PHASE-E PEEM;Lo;0;L;;;;;N;;;;;
+16985;BAMUM LETTER PHASE-E SAA;Lo;0;L;;;;;N;;;;;
+16986;BAMUM LETTER PHASE-E NGGURAE;Lo;0;L;;;;;N;;;;;
+16987;BAMUM LETTER PHASE-E MGBA;Lo;0;L;;;;;N;;;;;
+16988;BAMUM LETTER PHASE-E GHEUX;Lo;0;L;;;;;N;;;;;
+16989;BAMUM LETTER PHASE-E NGKEUAEM;Lo;0;L;;;;;N;;;;;
+1698A;BAMUM LETTER PHASE-E NJAEMLI;Lo;0;L;;;;;N;;;;;
+1698B;BAMUM LETTER PHASE-E MAP;Lo;0;L;;;;;N;;;;;
+1698C;BAMUM LETTER PHASE-E LOOT;Lo;0;L;;;;;N;;;;;
+1698D;BAMUM LETTER PHASE-E NGGEEEE;Lo;0;L;;;;;N;;;;;
+1698E;BAMUM LETTER PHASE-E NDIQ;Lo;0;L;;;;;N;;;;;
+1698F;BAMUM LETTER PHASE-E TAEN NTEUM;Lo;0;L;;;;;N;;;;;
+16990;BAMUM LETTER PHASE-E SET;Lo;0;L;;;;;N;;;;;
+16991;BAMUM LETTER PHASE-E PUM;Lo;0;L;;;;;N;;;;;
+16992;BAMUM LETTER PHASE-E NDAA SOFTNESS;Lo;0;L;;;;;N;;;;;
+16993;BAMUM LETTER PHASE-E NGGUAESHAE NYAM;Lo;0;L;;;;;N;;;;;
+16994;BAMUM LETTER PHASE-E YIEE;Lo;0;L;;;;;N;;;;;
+16995;BAMUM LETTER PHASE-E GHEUN;Lo;0;L;;;;;N;;;;;
+16996;BAMUM LETTER PHASE-E TUAE;Lo;0;L;;;;;N;;;;;
+16997;BAMUM LETTER PHASE-E YEUAE;Lo;0;L;;;;;N;;;;;
+16998;BAMUM LETTER PHASE-E PO;Lo;0;L;;;;;N;;;;;
+16999;BAMUM LETTER PHASE-E TUMAE;Lo;0;L;;;;;N;;;;;
+1699A;BAMUM LETTER PHASE-E KEUAE;Lo;0;L;;;;;N;;;;;
+1699B;BAMUM LETTER PHASE-E SUAEN;Lo;0;L;;;;;N;;;;;
+1699C;BAMUM LETTER PHASE-E TEUAEQ;Lo;0;L;;;;;N;;;;;
+1699D;BAMUM LETTER PHASE-E VEUAE;Lo;0;L;;;;;N;;;;;
+1699E;BAMUM LETTER PHASE-E WEUX;Lo;0;L;;;;;N;;;;;
+1699F;BAMUM LETTER PHASE-E LAAM;Lo;0;L;;;;;N;;;;;
+169A0;BAMUM LETTER PHASE-E PU;Lo;0;L;;;;;N;;;;;
+169A1;BAMUM LETTER PHASE-E TAAQ;Lo;0;L;;;;;N;;;;;
+169A2;BAMUM LETTER PHASE-E GHAAMAE;Lo;0;L;;;;;N;;;;;
+169A3;BAMUM LETTER PHASE-E NGEUREUT;Lo;0;L;;;;;N;;;;;
+169A4;BAMUM LETTER PHASE-E SHEUAEQ;Lo;0;L;;;;;N;;;;;
+169A5;BAMUM LETTER PHASE-E MGBEN;Lo;0;L;;;;;N;;;;;
+169A6;BAMUM LETTER PHASE-E MBEE;Lo;0;L;;;;;N;;;;;
+169A7;BAMUM LETTER PHASE-E NZAQ;Lo;0;L;;;;;N;;;;;
+169A8;BAMUM LETTER PHASE-E NKOM;Lo;0;L;;;;;N;;;;;
+169A9;BAMUM LETTER PHASE-E GBET;Lo;0;L;;;;;N;;;;;
+169AA;BAMUM LETTER PHASE-E TUM;Lo;0;L;;;;;N;;;;;
+169AB;BAMUM LETTER PHASE-E KUET;Lo;0;L;;;;;N;;;;;
+169AC;BAMUM LETTER PHASE-E YAP;Lo;0;L;;;;;N;;;;;
+169AD;BAMUM LETTER PHASE-E NYI CLEAVER;Lo;0;L;;;;;N;;;;;
+169AE;BAMUM LETTER PHASE-E YIT;Lo;0;L;;;;;N;;;;;
+169AF;BAMUM LETTER PHASE-E MFEUQ;Lo;0;L;;;;;N;;;;;
+169B0;BAMUM LETTER PHASE-E NDIAQ;Lo;0;L;;;;;N;;;;;
+169B1;BAMUM LETTER PHASE-E PIEEQ;Lo;0;L;;;;;N;;;;;
+169B2;BAMUM LETTER PHASE-E YUEQ;Lo;0;L;;;;;N;;;;;
+169B3;BAMUM LETTER PHASE-E LEUAEM;Lo;0;L;;;;;N;;;;;
+169B4;BAMUM LETTER PHASE-E FUE;Lo;0;L;;;;;N;;;;;
+169B5;BAMUM LETTER PHASE-E GBEUX;Lo;0;L;;;;;N;;;;;
+169B6;BAMUM LETTER PHASE-E NGKUP;Lo;0;L;;;;;N;;;;;
+169B7;BAMUM LETTER PHASE-E KET;Lo;0;L;;;;;N;;;;;
+169B8;BAMUM LETTER PHASE-E MAE;Lo;0;L;;;;;N;;;;;
+169B9;BAMUM LETTER PHASE-E NGKAAMI;Lo;0;L;;;;;N;;;;;
+169BA;BAMUM LETTER PHASE-E GHET;Lo;0;L;;;;;N;;;;;
+169BB;BAMUM LETTER PHASE-E FA;Lo;0;L;;;;;N;;;;;
+169BC;BAMUM LETTER PHASE-E NTUM;Lo;0;L;;;;;N;;;;;
+169BD;BAMUM LETTER PHASE-E PEUT;Lo;0;L;;;;;N;;;;;
+169BE;BAMUM LETTER PHASE-E YEUM;Lo;0;L;;;;;N;;;;;
+169BF;BAMUM LETTER PHASE-E NGGEUAE;Lo;0;L;;;;;N;;;;;
+169C0;BAMUM LETTER PHASE-E NYI BETWEEN;Lo;0;L;;;;;N;;;;;
+169C1;BAMUM LETTER PHASE-E NZUQ;Lo;0;L;;;;;N;;;;;
+169C2;BAMUM LETTER PHASE-E POON;Lo;0;L;;;;;N;;;;;
+169C3;BAMUM LETTER PHASE-E MIEE;Lo;0;L;;;;;N;;;;;
+169C4;BAMUM LETTER PHASE-E FUET;Lo;0;L;;;;;N;;;;;
+169C5;BAMUM LETTER PHASE-E NAE;Lo;0;L;;;;;N;;;;;
+169C6;BAMUM LETTER PHASE-E MUAE;Lo;0;L;;;;;N;;;;;
+169C7;BAMUM LETTER PHASE-E GHEUAE;Lo;0;L;;;;;N;;;;;
+169C8;BAMUM LETTER PHASE-E FU I;Lo;0;L;;;;;N;;;;;
+169C9;BAMUM LETTER PHASE-E MVI;Lo;0;L;;;;;N;;;;;
+169CA;BAMUM LETTER PHASE-E PUAQ;Lo;0;L;;;;;N;;;;;
+169CB;BAMUM LETTER PHASE-E NGKUM;Lo;0;L;;;;;N;;;;;
+169CC;BAMUM LETTER PHASE-E KUT;Lo;0;L;;;;;N;;;;;
+169CD;BAMUM LETTER PHASE-E PIET;Lo;0;L;;;;;N;;;;;
+169CE;BAMUM LETTER PHASE-E NTAP;Lo;0;L;;;;;N;;;;;
+169CF;BAMUM LETTER PHASE-E YEUAET;Lo;0;L;;;;;N;;;;;
+169D0;BAMUM LETTER PHASE-E NGGUP;Lo;0;L;;;;;N;;;;;
+169D1;BAMUM LETTER PHASE-E PA PEOPLE;Lo;0;L;;;;;N;;;;;
+169D2;BAMUM LETTER PHASE-E FU CALL;Lo;0;L;;;;;N;;;;;
+169D3;BAMUM LETTER PHASE-E FOM;Lo;0;L;;;;;N;;;;;
+169D4;BAMUM LETTER PHASE-E NJEE;Lo;0;L;;;;;N;;;;;
+169D5;BAMUM LETTER PHASE-E A;Lo;0;L;;;;;N;;;;;
+169D6;BAMUM LETTER PHASE-E TOQ;Lo;0;L;;;;;N;;;;;
+169D7;BAMUM LETTER PHASE-E O;Lo;0;L;;;;;N;;;;;
+169D8;BAMUM LETTER PHASE-E I;Lo;0;L;;;;;N;;;;;
+169D9;BAMUM LETTER PHASE-E LAQ;Lo;0;L;;;;;N;;;;;
+169DA;BAMUM LETTER PHASE-E PA PLURAL;Lo;0;L;;;;;N;;;;;
+169DB;BAMUM LETTER PHASE-E TAA;Lo;0;L;;;;;N;;;;;
+169DC;BAMUM LETTER PHASE-E TAQ;Lo;0;L;;;;;N;;;;;
+169DD;BAMUM LETTER PHASE-E NDAA MY HOUSE;Lo;0;L;;;;;N;;;;;
+169DE;BAMUM LETTER PHASE-E SHIQ;Lo;0;L;;;;;N;;;;;
+169DF;BAMUM LETTER PHASE-E YEUX;Lo;0;L;;;;;N;;;;;
+169E0;BAMUM LETTER PHASE-E NGUAE;Lo;0;L;;;;;N;;;;;
+169E1;BAMUM LETTER PHASE-E YUAEN;Lo;0;L;;;;;N;;;;;
+169E2;BAMUM LETTER PHASE-E YOQ SWIMMING;Lo;0;L;;;;;N;;;;;
+169E3;BAMUM LETTER PHASE-E YOQ COVER;Lo;0;L;;;;;N;;;;;
+169E4;BAMUM LETTER PHASE-E YUQ;Lo;0;L;;;;;N;;;;;
+169E5;BAMUM LETTER PHASE-E YUN;Lo;0;L;;;;;N;;;;;
+169E6;BAMUM LETTER PHASE-E KEUX;Lo;0;L;;;;;N;;;;;
+169E7;BAMUM LETTER PHASE-E PEUX;Lo;0;L;;;;;N;;;;;
+169E8;BAMUM LETTER PHASE-E NJEE EPOCH;Lo;0;L;;;;;N;;;;;
+169E9;BAMUM LETTER PHASE-E PUE;Lo;0;L;;;;;N;;;;;
+169EA;BAMUM LETTER PHASE-E WUE;Lo;0;L;;;;;N;;;;;
+169EB;BAMUM LETTER PHASE-E FEE;Lo;0;L;;;;;N;;;;;
+169EC;BAMUM LETTER PHASE-E VEE;Lo;0;L;;;;;N;;;;;
+169ED;BAMUM LETTER PHASE-E LU;Lo;0;L;;;;;N;;;;;
+169EE;BAMUM LETTER PHASE-E MI;Lo;0;L;;;;;N;;;;;
+169EF;BAMUM LETTER PHASE-E REUX;Lo;0;L;;;;;N;;;;;
+169F0;BAMUM LETTER PHASE-E RAE;Lo;0;L;;;;;N;;;;;
+169F1;BAMUM LETTER PHASE-E NGUAET;Lo;0;L;;;;;N;;;;;
+169F2;BAMUM LETTER PHASE-E NGA;Lo;0;L;;;;;N;;;;;
+169F3;BAMUM LETTER PHASE-E SHO;Lo;0;L;;;;;N;;;;;
+169F4;BAMUM LETTER PHASE-E SHOQ;Lo;0;L;;;;;N;;;;;
+169F5;BAMUM LETTER PHASE-E FU REMEDY;Lo;0;L;;;;;N;;;;;
+169F6;BAMUM LETTER PHASE-E NA;Lo;0;L;;;;;N;;;;;
+169F7;BAMUM LETTER PHASE-E PI;Lo;0;L;;;;;N;;;;;
+169F8;BAMUM LETTER PHASE-E LOQ;Lo;0;L;;;;;N;;;;;
+169F9;BAMUM LETTER PHASE-E KO;Lo;0;L;;;;;N;;;;;
+169FA;BAMUM LETTER PHASE-E MEN;Lo;0;L;;;;;N;;;;;
+169FB;BAMUM LETTER PHASE-E MA;Lo;0;L;;;;;N;;;;;
+169FC;BAMUM LETTER PHASE-E MAQ;Lo;0;L;;;;;N;;;;;
+169FD;BAMUM LETTER PHASE-E TEU;Lo;0;L;;;;;N;;;;;
+169FE;BAMUM LETTER PHASE-E KI;Lo;0;L;;;;;N;;;;;
+169FF;BAMUM LETTER PHASE-E MON;Lo;0;L;;;;;N;;;;;
+16A00;BAMUM LETTER PHASE-E TEN;Lo;0;L;;;;;N;;;;;
+16A01;BAMUM LETTER PHASE-E FAQ;Lo;0;L;;;;;N;;;;;
+16A02;BAMUM LETTER PHASE-E GHOM;Lo;0;L;;;;;N;;;;;
+16A03;BAMUM LETTER PHASE-F KA;Lo;0;L;;;;;N;;;;;
+16A04;BAMUM LETTER PHASE-F U;Lo;0;L;;;;;N;;;;;
+16A05;BAMUM LETTER PHASE-F KU;Lo;0;L;;;;;N;;;;;
+16A06;BAMUM LETTER PHASE-F EE;Lo;0;L;;;;;N;;;;;
+16A07;BAMUM LETTER PHASE-F REE;Lo;0;L;;;;;N;;;;;
+16A08;BAMUM LETTER PHASE-F TAE;Lo;0;L;;;;;N;;;;;
+16A09;BAMUM LETTER PHASE-F NYI;Lo;0;L;;;;;N;;;;;
+16A0A;BAMUM LETTER PHASE-F LA;Lo;0;L;;;;;N;;;;;
+16A0B;BAMUM LETTER PHASE-F RII;Lo;0;L;;;;;N;;;;;
+16A0C;BAMUM LETTER PHASE-F RIEE;Lo;0;L;;;;;N;;;;;
+16A0D;BAMUM LETTER PHASE-F MEEEE;Lo;0;L;;;;;N;;;;;
+16A0E;BAMUM LETTER PHASE-F TAA;Lo;0;L;;;;;N;;;;;
+16A0F;BAMUM LETTER PHASE-F NDAA;Lo;0;L;;;;;N;;;;;
+16A10;BAMUM LETTER PHASE-F NJAEM;Lo;0;L;;;;;N;;;;;
+16A11;BAMUM LETTER PHASE-F M;Lo;0;L;;;;;N;;;;;
+16A12;BAMUM LETTER PHASE-F SUU;Lo;0;L;;;;;N;;;;;
+16A13;BAMUM LETTER PHASE-F SHII;Lo;0;L;;;;;N;;;;;
+16A14;BAMUM LETTER PHASE-F SI;Lo;0;L;;;;;N;;;;;
+16A15;BAMUM LETTER PHASE-F SEUX;Lo;0;L;;;;;N;;;;;
+16A16;BAMUM LETTER PHASE-F KYEE;Lo;0;L;;;;;N;;;;;
+16A17;BAMUM LETTER PHASE-F KET;Lo;0;L;;;;;N;;;;;
+16A18;BAMUM LETTER PHASE-F NUAE;Lo;0;L;;;;;N;;;;;
+16A19;BAMUM LETTER PHASE-F NU;Lo;0;L;;;;;N;;;;;
+16A1A;BAMUM LETTER PHASE-F NJUAE;Lo;0;L;;;;;N;;;;;
+16A1B;BAMUM LETTER PHASE-F YOQ;Lo;0;L;;;;;N;;;;;
+16A1C;BAMUM LETTER PHASE-F SHU;Lo;0;L;;;;;N;;;;;
+16A1D;BAMUM LETTER PHASE-F YA;Lo;0;L;;;;;N;;;;;
+16A1E;BAMUM LETTER PHASE-F NSHA;Lo;0;L;;;;;N;;;;;
+16A1F;BAMUM LETTER PHASE-F PEUX;Lo;0;L;;;;;N;;;;;
+16A20;BAMUM LETTER PHASE-F NTEE;Lo;0;L;;;;;N;;;;;
+16A21;BAMUM LETTER PHASE-F WUE;Lo;0;L;;;;;N;;;;;
+16A22;BAMUM LETTER PHASE-F PEE;Lo;0;L;;;;;N;;;;;
+16A23;BAMUM LETTER PHASE-F RU;Lo;0;L;;;;;N;;;;;
+16A24;BAMUM LETTER PHASE-F NI;Lo;0;L;;;;;N;;;;;
+16A25;BAMUM LETTER PHASE-F REUX;Lo;0;L;;;;;N;;;;;
+16A26;BAMUM LETTER PHASE-F KEN;Lo;0;L;;;;;N;;;;;
+16A27;BAMUM LETTER PHASE-F NGKWAEN;Lo;0;L;;;;;N;;;;;
+16A28;BAMUM LETTER PHASE-F NGGA;Lo;0;L;;;;;N;;;;;
+16A29;BAMUM LETTER PHASE-F SHO;Lo;0;L;;;;;N;;;;;
+16A2A;BAMUM LETTER PHASE-F PUAE;Lo;0;L;;;;;N;;;;;
+16A2B;BAMUM LETTER PHASE-F FOM;Lo;0;L;;;;;N;;;;;
+16A2C;BAMUM LETTER PHASE-F WA;Lo;0;L;;;;;N;;;;;
+16A2D;BAMUM LETTER PHASE-F LI;Lo;0;L;;;;;N;;;;;
+16A2E;BAMUM LETTER PHASE-F LOQ;Lo;0;L;;;;;N;;;;;
+16A2F;BAMUM LETTER PHASE-F KO;Lo;0;L;;;;;N;;;;;
+16A30;BAMUM LETTER PHASE-F MBEN;Lo;0;L;;;;;N;;;;;
+16A31;BAMUM LETTER PHASE-F REN;Lo;0;L;;;;;N;;;;;
+16A32;BAMUM LETTER PHASE-F MA;Lo;0;L;;;;;N;;;;;
+16A33;BAMUM LETTER PHASE-F MO;Lo;0;L;;;;;N;;;;;
+16A34;BAMUM LETTER PHASE-F MBAA;Lo;0;L;;;;;N;;;;;
+16A35;BAMUM LETTER PHASE-F TET;Lo;0;L;;;;;N;;;;;
+16A36;BAMUM LETTER PHASE-F KPA;Lo;0;L;;;;;N;;;;;
+16A37;BAMUM LETTER PHASE-F SAMBA;Lo;0;L;;;;;N;;;;;
+16A38;BAMUM LETTER PHASE-F VUEQ;Lo;0;L;;;;;N;;;;;
+16A40;MRO LETTER TA;Lo;0;L;;;;;N;;;;;
+16A41;MRO LETTER NGI;Lo;0;L;;;;;N;;;;;
+16A42;MRO LETTER YO;Lo;0;L;;;;;N;;;;;
+16A43;MRO LETTER MIM;Lo;0;L;;;;;N;;;;;
+16A44;MRO LETTER BA;Lo;0;L;;;;;N;;;;;
+16A45;MRO LETTER DA;Lo;0;L;;;;;N;;;;;
+16A46;MRO LETTER A;Lo;0;L;;;;;N;;;;;
+16A47;MRO LETTER PHI;Lo;0;L;;;;;N;;;;;
+16A48;MRO LETTER KHAI;Lo;0;L;;;;;N;;;;;
+16A49;MRO LETTER HAO;Lo;0;L;;;;;N;;;;;
+16A4A;MRO LETTER DAI;Lo;0;L;;;;;N;;;;;
+16A4B;MRO LETTER CHU;Lo;0;L;;;;;N;;;;;
+16A4C;MRO LETTER KEAAE;Lo;0;L;;;;;N;;;;;
+16A4D;MRO LETTER OL;Lo;0;L;;;;;N;;;;;
+16A4E;MRO LETTER MAEM;Lo;0;L;;;;;N;;;;;
+16A4F;MRO LETTER NIN;Lo;0;L;;;;;N;;;;;
+16A50;MRO LETTER PA;Lo;0;L;;;;;N;;;;;
+16A51;MRO LETTER OO;Lo;0;L;;;;;N;;;;;
+16A52;MRO LETTER O;Lo;0;L;;;;;N;;;;;
+16A53;MRO LETTER RO;Lo;0;L;;;;;N;;;;;
+16A54;MRO LETTER SHI;Lo;0;L;;;;;N;;;;;
+16A55;MRO LETTER THEA;Lo;0;L;;;;;N;;;;;
+16A56;MRO LETTER EA;Lo;0;L;;;;;N;;;;;
+16A57;MRO LETTER WA;Lo;0;L;;;;;N;;;;;
+16A58;MRO LETTER E;Lo;0;L;;;;;N;;;;;
+16A59;MRO LETTER KO;Lo;0;L;;;;;N;;;;;
+16A5A;MRO LETTER LAN;Lo;0;L;;;;;N;;;;;
+16A5B;MRO LETTER LA;Lo;0;L;;;;;N;;;;;
+16A5C;MRO LETTER HAI;Lo;0;L;;;;;N;;;;;
+16A5D;MRO LETTER RI;Lo;0;L;;;;;N;;;;;
+16A5E;MRO LETTER TEK;Lo;0;L;;;;;N;;;;;
+16A60;MRO DIGIT ZERO;Nd;0;L;;0;0;0;N;;;;;
+16A61;MRO DIGIT ONE;Nd;0;L;;1;1;1;N;;;;;
+16A62;MRO DIGIT TWO;Nd;0;L;;2;2;2;N;;;;;
+16A63;MRO DIGIT THREE;Nd;0;L;;3;3;3;N;;;;;
+16A64;MRO DIGIT FOUR;Nd;0;L;;4;4;4;N;;;;;
+16A65;MRO DIGIT FIVE;Nd;0;L;;5;5;5;N;;;;;
+16A66;MRO DIGIT SIX;Nd;0;L;;6;6;6;N;;;;;
+16A67;MRO DIGIT SEVEN;Nd;0;L;;7;7;7;N;;;;;
+16A68;MRO DIGIT EIGHT;Nd;0;L;;8;8;8;N;;;;;
+16A69;MRO DIGIT NINE;Nd;0;L;;9;9;9;N;;;;;
+16A6E;MRO DANDA;Po;0;L;;;;;N;;;;;
+16A6F;MRO DOUBLE DANDA;Po;0;L;;;;;N;;;;;
+16AD0;BASSA VAH LETTER ENNI;Lo;0;L;;;;;N;;;;;
+16AD1;BASSA VAH LETTER KA;Lo;0;L;;;;;N;;;;;
+16AD2;BASSA VAH LETTER SE;Lo;0;L;;;;;N;;;;;
+16AD3;BASSA VAH LETTER FA;Lo;0;L;;;;;N;;;;;
+16AD4;BASSA VAH LETTER MBE;Lo;0;L;;;;;N;;;;;
+16AD5;BASSA VAH LETTER YIE;Lo;0;L;;;;;N;;;;;
+16AD6;BASSA VAH LETTER GAH;Lo;0;L;;;;;N;;;;;
+16AD7;BASSA VAH LETTER DHII;Lo;0;L;;;;;N;;;;;
+16AD8;BASSA VAH LETTER KPAH;Lo;0;L;;;;;N;;;;;
+16AD9;BASSA VAH LETTER JO;Lo;0;L;;;;;N;;;;;
+16ADA;BASSA VAH LETTER HWAH;Lo;0;L;;;;;N;;;;;
+16ADB;BASSA VAH LETTER WA;Lo;0;L;;;;;N;;;;;
+16ADC;BASSA VAH LETTER ZO;Lo;0;L;;;;;N;;;;;
+16ADD;BASSA VAH LETTER GBU;Lo;0;L;;;;;N;;;;;
+16ADE;BASSA VAH LETTER DO;Lo;0;L;;;;;N;;;;;
+16ADF;BASSA VAH LETTER CE;Lo;0;L;;;;;N;;;;;
+16AE0;BASSA VAH LETTER UWU;Lo;0;L;;;;;N;;;;;
+16AE1;BASSA VAH LETTER TO;Lo;0;L;;;;;N;;;;;
+16AE2;BASSA VAH LETTER BA;Lo;0;L;;;;;N;;;;;
+16AE3;BASSA VAH LETTER VU;Lo;0;L;;;;;N;;;;;
+16AE4;BASSA VAH LETTER YEIN;Lo;0;L;;;;;N;;;;;
+16AE5;BASSA VAH LETTER PA;Lo;0;L;;;;;N;;;;;
+16AE6;BASSA VAH LETTER WADDA;Lo;0;L;;;;;N;;;;;
+16AE7;BASSA VAH LETTER A;Lo;0;L;;;;;N;;;;;
+16AE8;BASSA VAH LETTER O;Lo;0;L;;;;;N;;;;;
+16AE9;BASSA VAH LETTER OO;Lo;0;L;;;;;N;;;;;
+16AEA;BASSA VAH LETTER U;Lo;0;L;;;;;N;;;;;
+16AEB;BASSA VAH LETTER EE;Lo;0;L;;;;;N;;;;;
+16AEC;BASSA VAH LETTER E;Lo;0;L;;;;;N;;;;;
+16AED;BASSA VAH LETTER I;Lo;0;L;;;;;N;;;;;
+16AF0;BASSA VAH COMBINING HIGH TONE;Mn;1;NSM;;;;;N;;;;;
+16AF1;BASSA VAH COMBINING LOW TONE;Mn;1;NSM;;;;;N;;;;;
+16AF2;BASSA VAH COMBINING MID TONE;Mn;1;NSM;;;;;N;;;;;
+16AF3;BASSA VAH COMBINING LOW-MID TONE;Mn;1;NSM;;;;;N;;;;;
+16AF4;BASSA VAH COMBINING HIGH-LOW TONE;Mn;1;NSM;;;;;N;;;;;
+16AF5;BASSA VAH FULL STOP;Po;0;L;;;;;N;;;;;
+16B00;PAHAWH HMONG VOWEL KEEB;Lo;0;L;;;;;N;;;;;
+16B01;PAHAWH HMONG VOWEL KEEV;Lo;0;L;;;;;N;;;;;
+16B02;PAHAWH HMONG VOWEL KIB;Lo;0;L;;;;;N;;;;;
+16B03;PAHAWH HMONG VOWEL KIV;Lo;0;L;;;;;N;;;;;
+16B04;PAHAWH HMONG VOWEL KAUB;Lo;0;L;;;;;N;;;;;
+16B05;PAHAWH HMONG VOWEL KAUV;Lo;0;L;;;;;N;;;;;
+16B06;PAHAWH HMONG VOWEL KUB;Lo;0;L;;;;;N;;;;;
+16B07;PAHAWH HMONG VOWEL KUV;Lo;0;L;;;;;N;;;;;
+16B08;PAHAWH HMONG VOWEL KEB;Lo;0;L;;;;;N;;;;;
+16B09;PAHAWH HMONG VOWEL KEV;Lo;0;L;;;;;N;;;;;
+16B0A;PAHAWH HMONG VOWEL KAIB;Lo;0;L;;;;;N;;;;;
+16B0B;PAHAWH HMONG VOWEL KAIV;Lo;0;L;;;;;N;;;;;
+16B0C;PAHAWH HMONG VOWEL KOOB;Lo;0;L;;;;;N;;;;;
+16B0D;PAHAWH HMONG VOWEL KOOV;Lo;0;L;;;;;N;;;;;
+16B0E;PAHAWH HMONG VOWEL KAWB;Lo;0;L;;;;;N;;;;;
+16B0F;PAHAWH HMONG VOWEL KAWV;Lo;0;L;;;;;N;;;;;
+16B10;PAHAWH HMONG VOWEL KUAB;Lo;0;L;;;;;N;;;;;
+16B11;PAHAWH HMONG VOWEL KUAV;Lo;0;L;;;;;N;;;;;
+16B12;PAHAWH HMONG VOWEL KOB;Lo;0;L;;;;;N;;;;;
+16B13;PAHAWH HMONG VOWEL KOV;Lo;0;L;;;;;N;;;;;
+16B14;PAHAWH HMONG VOWEL KIAB;Lo;0;L;;;;;N;;;;;
+16B15;PAHAWH HMONG VOWEL KIAV;Lo;0;L;;;;;N;;;;;
+16B16;PAHAWH HMONG VOWEL KAB;Lo;0;L;;;;;N;;;;;
+16B17;PAHAWH HMONG VOWEL KAV;Lo;0;L;;;;;N;;;;;
+16B18;PAHAWH HMONG VOWEL KWB;Lo;0;L;;;;;N;;;;;
+16B19;PAHAWH HMONG VOWEL KWV;Lo;0;L;;;;;N;;;;;
+16B1A;PAHAWH HMONG VOWEL KAAB;Lo;0;L;;;;;N;;;;;
+16B1B;PAHAWH HMONG VOWEL KAAV;Lo;0;L;;;;;N;;;;;
+16B1C;PAHAWH HMONG CONSONANT VAU;Lo;0;L;;;;;N;;;;;
+16B1D;PAHAWH HMONG CONSONANT NTSAU;Lo;0;L;;;;;N;;;;;
+16B1E;PAHAWH HMONG CONSONANT LAU;Lo;0;L;;;;;N;;;;;
+16B1F;PAHAWH HMONG CONSONANT HAU;Lo;0;L;;;;;N;;;;;
+16B20;PAHAWH HMONG CONSONANT NLAU;Lo;0;L;;;;;N;;;;;
+16B21;PAHAWH HMONG CONSONANT RAU;Lo;0;L;;;;;N;;;;;
+16B22;PAHAWH HMONG CONSONANT NKAU;Lo;0;L;;;;;N;;;;;
+16B23;PAHAWH HMONG CONSONANT QHAU;Lo;0;L;;;;;N;;;;;
+16B24;PAHAWH HMONG CONSONANT YAU;Lo;0;L;;;;;N;;;;;
+16B25;PAHAWH HMONG CONSONANT HLAU;Lo;0;L;;;;;N;;;;;
+16B26;PAHAWH HMONG CONSONANT MAU;Lo;0;L;;;;;N;;;;;
+16B27;PAHAWH HMONG CONSONANT CHAU;Lo;0;L;;;;;N;;;;;
+16B28;PAHAWH HMONG CONSONANT NCHAU;Lo;0;L;;;;;N;;;;;
+16B29;PAHAWH HMONG CONSONANT HNAU;Lo;0;L;;;;;N;;;;;
+16B2A;PAHAWH HMONG CONSONANT PLHAU;Lo;0;L;;;;;N;;;;;
+16B2B;PAHAWH HMONG CONSONANT NTHAU;Lo;0;L;;;;;N;;;;;
+16B2C;PAHAWH HMONG CONSONANT NAU;Lo;0;L;;;;;N;;;;;
+16B2D;PAHAWH HMONG CONSONANT AU;Lo;0;L;;;;;N;;;;;
+16B2E;PAHAWH HMONG CONSONANT XAU;Lo;0;L;;;;;N;;;;;
+16B2F;PAHAWH HMONG CONSONANT CAU;Lo;0;L;;;;;N;;;;;
+16B30;PAHAWH HMONG MARK CIM TUB;Mn;230;NSM;;;;;N;;;;;
+16B31;PAHAWH HMONG MARK CIM SO;Mn;230;NSM;;;;;N;;;;;
+16B32;PAHAWH HMONG MARK CIM KES;Mn;230;NSM;;;;;N;;;;;
+16B33;PAHAWH HMONG MARK CIM KHAV;Mn;230;NSM;;;;;N;;;;;
+16B34;PAHAWH HMONG MARK CIM SUAM;Mn;230;NSM;;;;;N;;;;;
+16B35;PAHAWH HMONG MARK CIM HOM;Mn;230;NSM;;;;;N;;;;;
+16B36;PAHAWH HMONG MARK CIM TAUM;Mn;230;NSM;;;;;N;;;;;
+16B37;PAHAWH HMONG SIGN VOS THOM;Po;0;L;;;;;N;;;;;
+16B38;PAHAWH HMONG SIGN VOS TSHAB CEEB;Po;0;L;;;;;N;;;;;
+16B39;PAHAWH HMONG SIGN CIM CHEEM;Po;0;L;;;;;N;;;;;
+16B3A;PAHAWH HMONG SIGN VOS THIAB;Po;0;L;;;;;N;;;;;
+16B3B;PAHAWH HMONG SIGN VOS FEEM;Po;0;L;;;;;N;;;;;
+16B3C;PAHAWH HMONG SIGN XYEEM NTXIV;So;0;L;;;;;N;;;;;
+16B3D;PAHAWH HMONG SIGN XYEEM RHO;So;0;L;;;;;N;;;;;
+16B3E;PAHAWH HMONG SIGN XYEEM TOV;So;0;L;;;;;N;;;;;
+16B3F;PAHAWH HMONG SIGN XYEEM FAIB;So;0;L;;;;;N;;;;;
+16B40;PAHAWH HMONG SIGN VOS SEEV;Lm;0;L;;;;;N;;;;;
+16B41;PAHAWH HMONG SIGN MEEJ SUAB;Lm;0;L;;;;;N;;;;;
+16B42;PAHAWH HMONG SIGN VOS NRUA;Lm;0;L;;;;;N;;;;;
+16B43;PAHAWH HMONG SIGN IB YAM;Lm;0;L;;;;;N;;;;;
+16B44;PAHAWH HMONG SIGN XAUS;Po;0;L;;;;;N;;;;;
+16B45;PAHAWH HMONG SIGN CIM TSOV ROG;So;0;L;;;;;N;;;;;
+16B50;PAHAWH HMONG DIGIT ZERO;Nd;0;L;;0;0;0;N;;;;;
+16B51;PAHAWH HMONG DIGIT ONE;Nd;0;L;;1;1;1;N;;;;;
+16B52;PAHAWH HMONG DIGIT TWO;Nd;0;L;;2;2;2;N;;;;;
+16B53;PAHAWH HMONG DIGIT THREE;Nd;0;L;;3;3;3;N;;;;;
+16B54;PAHAWH HMONG DIGIT FOUR;Nd;0;L;;4;4;4;N;;;;;
+16B55;PAHAWH HMONG DIGIT FIVE;Nd;0;L;;5;5;5;N;;;;;
+16B56;PAHAWH HMONG DIGIT SIX;Nd;0;L;;6;6;6;N;;;;;
+16B57;PAHAWH HMONG DIGIT SEVEN;Nd;0;L;;7;7;7;N;;;;;
+16B58;PAHAWH HMONG DIGIT EIGHT;Nd;0;L;;8;8;8;N;;;;;
+16B59;PAHAWH HMONG DIGIT NINE;Nd;0;L;;9;9;9;N;;;;;
+16B5B;PAHAWH HMONG NUMBER TENS;No;0;L;;;;10;N;;;;;
+16B5C;PAHAWH HMONG NUMBER HUNDREDS;No;0;L;;;;100;N;;;;;
+16B5D;PAHAWH HMONG NUMBER TEN THOUSANDS;No;0;L;;;;10000;N;;;;;
+16B5E;PAHAWH HMONG NUMBER MILLIONS;No;0;L;;;;1000000;N;;;;;
+16B5F;PAHAWH HMONG NUMBER HUNDRED MILLIONS;No;0;L;;;;100000000;N;;;;;
+16B60;PAHAWH HMONG NUMBER TEN BILLIONS;No;0;L;;;;10000000000;N;;;;;
+16B61;PAHAWH HMONG NUMBER TRILLIONS;No;0;L;;;;1000000000000;N;;;;;
+16B63;PAHAWH HMONG SIGN VOS LUB;Lo;0;L;;;;;N;;;;;
+16B64;PAHAWH HMONG SIGN XYOO;Lo;0;L;;;;;N;;;;;
+16B65;PAHAWH HMONG SIGN HLI;Lo;0;L;;;;;N;;;;;
+16B66;PAHAWH HMONG SIGN THIRD-STAGE HLI;Lo;0;L;;;;;N;;;;;
+16B67;PAHAWH HMONG SIGN ZWJ THAJ;Lo;0;L;;;;;N;;;;;
+16B68;PAHAWH HMONG SIGN HNUB;Lo;0;L;;;;;N;;;;;
+16B69;PAHAWH HMONG SIGN NQIG;Lo;0;L;;;;;N;;;;;
+16B6A;PAHAWH HMONG SIGN XIAB;Lo;0;L;;;;;N;;;;;
+16B6B;PAHAWH HMONG SIGN NTUJ;Lo;0;L;;;;;N;;;;;
+16B6C;PAHAWH HMONG SIGN AV;Lo;0;L;;;;;N;;;;;
+16B6D;PAHAWH HMONG SIGN TXHEEJ CEEV;Lo;0;L;;;;;N;;;;;
+16B6E;PAHAWH HMONG SIGN MEEJ TSEEB;Lo;0;L;;;;;N;;;;;
+16B6F;PAHAWH HMONG SIGN TAU;Lo;0;L;;;;;N;;;;;
+16B70;PAHAWH HMONG SIGN LOS;Lo;0;L;;;;;N;;;;;
+16B71;PAHAWH HMONG SIGN MUS;Lo;0;L;;;;;N;;;;;
+16B72;PAHAWH HMONG SIGN CIM HAIS LUS NTOG NTOG;Lo;0;L;;;;;N;;;;;
+16B73;PAHAWH HMONG SIGN CIM CUAM TSHOOJ;Lo;0;L;;;;;N;;;;;
+16B74;PAHAWH HMONG SIGN CIM TXWV;Lo;0;L;;;;;N;;;;;
+16B75;PAHAWH HMONG SIGN CIM TXWV CHWV;Lo;0;L;;;;;N;;;;;
+16B76;PAHAWH HMONG SIGN CIM PUB DAWB;Lo;0;L;;;;;N;;;;;
+16B77;PAHAWH HMONG SIGN CIM NRES TOS;Lo;0;L;;;;;N;;;;;
+16B7D;PAHAWH HMONG CLAN SIGN TSHEEJ;Lo;0;L;;;;;N;;;;;
+16B7E;PAHAWH HMONG CLAN SIGN YEEG;Lo;0;L;;;;;N;;;;;
+16B7F;PAHAWH HMONG CLAN SIGN LIS;Lo;0;L;;;;;N;;;;;
+16B80;PAHAWH HMONG CLAN SIGN LAUJ;Lo;0;L;;;;;N;;;;;
+16B81;PAHAWH HMONG CLAN SIGN XYOOJ;Lo;0;L;;;;;N;;;;;
+16B82;PAHAWH HMONG CLAN SIGN KOO;Lo;0;L;;;;;N;;;;;
+16B83;PAHAWH HMONG CLAN SIGN HAWJ;Lo;0;L;;;;;N;;;;;
+16B84;PAHAWH HMONG CLAN SIGN MUAS;Lo;0;L;;;;;N;;;;;
+16B85;PAHAWH HMONG CLAN SIGN THOJ;Lo;0;L;;;;;N;;;;;
+16B86;PAHAWH HMONG CLAN SIGN TSAB;Lo;0;L;;;;;N;;;;;
+16B87;PAHAWH HMONG CLAN SIGN PHAB;Lo;0;L;;;;;N;;;;;
+16B88;PAHAWH HMONG CLAN SIGN KHAB;Lo;0;L;;;;;N;;;;;
+16B89;PAHAWH HMONG CLAN SIGN HAM;Lo;0;L;;;;;N;;;;;
+16B8A;PAHAWH HMONG CLAN SIGN VAJ;Lo;0;L;;;;;N;;;;;
+16B8B;PAHAWH HMONG CLAN SIGN FAJ;Lo;0;L;;;;;N;;;;;
+16B8C;PAHAWH HMONG CLAN SIGN YAJ;Lo;0;L;;;;;N;;;;;
+16B8D;PAHAWH HMONG CLAN SIGN TSWB;Lo;0;L;;;;;N;;;;;
+16B8E;PAHAWH HMONG CLAN SIGN KWM;Lo;0;L;;;;;N;;;;;
+16B8F;PAHAWH HMONG CLAN SIGN VWJ;Lo;0;L;;;;;N;;;;;
+16F00;MIAO LETTER PA;Lo;0;L;;;;;N;;;;;
+16F01;MIAO LETTER BA;Lo;0;L;;;;;N;;;;;
+16F02;MIAO LETTER YI PA;Lo;0;L;;;;;N;;;;;
+16F03;MIAO LETTER PLA;Lo;0;L;;;;;N;;;;;
+16F04;MIAO LETTER MA;Lo;0;L;;;;;N;;;;;
+16F05;MIAO LETTER MHA;Lo;0;L;;;;;N;;;;;
+16F06;MIAO LETTER ARCHAIC MA;Lo;0;L;;;;;N;;;;;
+16F07;MIAO LETTER FA;Lo;0;L;;;;;N;;;;;
+16F08;MIAO LETTER VA;Lo;0;L;;;;;N;;;;;
+16F09;MIAO LETTER VFA;Lo;0;L;;;;;N;;;;;
+16F0A;MIAO LETTER TA;Lo;0;L;;;;;N;;;;;
+16F0B;MIAO LETTER DA;Lo;0;L;;;;;N;;;;;
+16F0C;MIAO LETTER YI TTA;Lo;0;L;;;;;N;;;;;
+16F0D;MIAO LETTER YI TA;Lo;0;L;;;;;N;;;;;
+16F0E;MIAO LETTER TTA;Lo;0;L;;;;;N;;;;;
+16F0F;MIAO LETTER DDA;Lo;0;L;;;;;N;;;;;
+16F10;MIAO LETTER NA;Lo;0;L;;;;;N;;;;;
+16F11;MIAO LETTER NHA;Lo;0;L;;;;;N;;;;;
+16F12;MIAO LETTER YI NNA;Lo;0;L;;;;;N;;;;;
+16F13;MIAO LETTER ARCHAIC NA;Lo;0;L;;;;;N;;;;;
+16F14;MIAO LETTER NNA;Lo;0;L;;;;;N;;;;;
+16F15;MIAO LETTER NNHA;Lo;0;L;;;;;N;;;;;
+16F16;MIAO LETTER LA;Lo;0;L;;;;;N;;;;;
+16F17;MIAO LETTER LYA;Lo;0;L;;;;;N;;;;;
+16F18;MIAO LETTER LHA;Lo;0;L;;;;;N;;;;;
+16F19;MIAO LETTER LHYA;Lo;0;L;;;;;N;;;;;
+16F1A;MIAO LETTER TLHA;Lo;0;L;;;;;N;;;;;
+16F1B;MIAO LETTER DLHA;Lo;0;L;;;;;N;;;;;
+16F1C;MIAO LETTER TLHYA;Lo;0;L;;;;;N;;;;;
+16F1D;MIAO LETTER DLHYA;Lo;0;L;;;;;N;;;;;
+16F1E;MIAO LETTER KA;Lo;0;L;;;;;N;;;;;
+16F1F;MIAO LETTER GA;Lo;0;L;;;;;N;;;;;
+16F20;MIAO LETTER YI KA;Lo;0;L;;;;;N;;;;;
+16F21;MIAO LETTER QA;Lo;0;L;;;;;N;;;;;
+16F22;MIAO LETTER QGA;Lo;0;L;;;;;N;;;;;
+16F23;MIAO LETTER NGA;Lo;0;L;;;;;N;;;;;
+16F24;MIAO LETTER NGHA;Lo;0;L;;;;;N;;;;;
+16F25;MIAO LETTER ARCHAIC NGA;Lo;0;L;;;;;N;;;;;
+16F26;MIAO LETTER HA;Lo;0;L;;;;;N;;;;;
+16F27;MIAO LETTER XA;Lo;0;L;;;;;N;;;;;
+16F28;MIAO LETTER GHA;Lo;0;L;;;;;N;;;;;
+16F29;MIAO LETTER GHHA;Lo;0;L;;;;;N;;;;;
+16F2A;MIAO LETTER TSSA;Lo;0;L;;;;;N;;;;;
+16F2B;MIAO LETTER DZZA;Lo;0;L;;;;;N;;;;;
+16F2C;MIAO LETTER NYA;Lo;0;L;;;;;N;;;;;
+16F2D;MIAO LETTER NYHA;Lo;0;L;;;;;N;;;;;
+16F2E;MIAO LETTER TSHA;Lo;0;L;;;;;N;;;;;
+16F2F;MIAO LETTER DZHA;Lo;0;L;;;;;N;;;;;
+16F30;MIAO LETTER YI TSHA;Lo;0;L;;;;;N;;;;;
+16F31;MIAO LETTER YI DZHA;Lo;0;L;;;;;N;;;;;
+16F32;MIAO LETTER REFORMED TSHA;Lo;0;L;;;;;N;;;;;
+16F33;MIAO LETTER SHA;Lo;0;L;;;;;N;;;;;
+16F34;MIAO LETTER SSA;Lo;0;L;;;;;N;;;;;
+16F35;MIAO LETTER ZHA;Lo;0;L;;;;;N;;;;;
+16F36;MIAO LETTER ZSHA;Lo;0;L;;;;;N;;;;;
+16F37;MIAO LETTER TSA;Lo;0;L;;;;;N;;;;;
+16F38;MIAO LETTER DZA;Lo;0;L;;;;;N;;;;;
+16F39;MIAO LETTER YI TSA;Lo;0;L;;;;;N;;;;;
+16F3A;MIAO LETTER SA;Lo;0;L;;;;;N;;;;;
+16F3B;MIAO LETTER ZA;Lo;0;L;;;;;N;;;;;
+16F3C;MIAO LETTER ZSA;Lo;0;L;;;;;N;;;;;
+16F3D;MIAO LETTER ZZA;Lo;0;L;;;;;N;;;;;
+16F3E;MIAO LETTER ZZSA;Lo;0;L;;;;;N;;;;;
+16F3F;MIAO LETTER ARCHAIC ZZA;Lo;0;L;;;;;N;;;;;
+16F40;MIAO LETTER ZZYA;Lo;0;L;;;;;N;;;;;
+16F41;MIAO LETTER ZZSYA;Lo;0;L;;;;;N;;;;;
+16F42;MIAO LETTER WA;Lo;0;L;;;;;N;;;;;
+16F43;MIAO LETTER AH;Lo;0;L;;;;;N;;;;;
+16F44;MIAO LETTER HHA;Lo;0;L;;;;;N;;;;;
+16F50;MIAO LETTER NASALIZATION;Lo;0;L;;;;;N;;;;;
+16F51;MIAO SIGN ASPIRATION;Mc;0;L;;;;;N;;;;;
+16F52;MIAO SIGN REFORMED VOICING;Mc;0;L;;;;;N;;;;;
+16F53;MIAO SIGN REFORMED ASPIRATION;Mc;0;L;;;;;N;;;;;
+16F54;MIAO VOWEL SIGN A;Mc;0;L;;;;;N;;;;;
+16F55;MIAO VOWEL SIGN AA;Mc;0;L;;;;;N;;;;;
+16F56;MIAO VOWEL SIGN AHH;Mc;0;L;;;;;N;;;;;
+16F57;MIAO VOWEL SIGN AN;Mc;0;L;;;;;N;;;;;
+16F58;MIAO VOWEL SIGN ANG;Mc;0;L;;;;;N;;;;;
+16F59;MIAO VOWEL SIGN O;Mc;0;L;;;;;N;;;;;
+16F5A;MIAO VOWEL SIGN OO;Mc;0;L;;;;;N;;;;;
+16F5B;MIAO VOWEL SIGN WO;Mc;0;L;;;;;N;;;;;
+16F5C;MIAO VOWEL SIGN W;Mc;0;L;;;;;N;;;;;
+16F5D;MIAO VOWEL SIGN E;Mc;0;L;;;;;N;;;;;
+16F5E;MIAO VOWEL SIGN EN;Mc;0;L;;;;;N;;;;;
+16F5F;MIAO VOWEL SIGN ENG;Mc;0;L;;;;;N;;;;;
+16F60;MIAO VOWEL SIGN OEY;Mc;0;L;;;;;N;;;;;
+16F61;MIAO VOWEL SIGN I;Mc;0;L;;;;;N;;;;;
+16F62;MIAO VOWEL SIGN IA;Mc;0;L;;;;;N;;;;;
+16F63;MIAO VOWEL SIGN IAN;Mc;0;L;;;;;N;;;;;
+16F64;MIAO VOWEL SIGN IANG;Mc;0;L;;;;;N;;;;;
+16F65;MIAO VOWEL SIGN IO;Mc;0;L;;;;;N;;;;;
+16F66;MIAO VOWEL SIGN IE;Mc;0;L;;;;;N;;;;;
+16F67;MIAO VOWEL SIGN II;Mc;0;L;;;;;N;;;;;
+16F68;MIAO VOWEL SIGN IU;Mc;0;L;;;;;N;;;;;
+16F69;MIAO VOWEL SIGN ING;Mc;0;L;;;;;N;;;;;
+16F6A;MIAO VOWEL SIGN U;Mc;0;L;;;;;N;;;;;
+16F6B;MIAO VOWEL SIGN UA;Mc;0;L;;;;;N;;;;;
+16F6C;MIAO VOWEL SIGN UAN;Mc;0;L;;;;;N;;;;;
+16F6D;MIAO VOWEL SIGN UANG;Mc;0;L;;;;;N;;;;;
+16F6E;MIAO VOWEL SIGN UU;Mc;0;L;;;;;N;;;;;
+16F6F;MIAO VOWEL SIGN UEI;Mc;0;L;;;;;N;;;;;
+16F70;MIAO VOWEL SIGN UNG;Mc;0;L;;;;;N;;;;;
+16F71;MIAO VOWEL SIGN Y;Mc;0;L;;;;;N;;;;;
+16F72;MIAO VOWEL SIGN YI;Mc;0;L;;;;;N;;;;;
+16F73;MIAO VOWEL SIGN AE;Mc;0;L;;;;;N;;;;;
+16F74;MIAO VOWEL SIGN AEE;Mc;0;L;;;;;N;;;;;
+16F75;MIAO VOWEL SIGN ERR;Mc;0;L;;;;;N;;;;;
+16F76;MIAO VOWEL SIGN ROUNDED ERR;Mc;0;L;;;;;N;;;;;
+16F77;MIAO VOWEL SIGN ER;Mc;0;L;;;;;N;;;;;
+16F78;MIAO VOWEL SIGN ROUNDED ER;Mc;0;L;;;;;N;;;;;
+16F79;MIAO VOWEL SIGN AI;Mc;0;L;;;;;N;;;;;
+16F7A;MIAO VOWEL SIGN EI;Mc;0;L;;;;;N;;;;;
+16F7B;MIAO VOWEL SIGN AU;Mc;0;L;;;;;N;;;;;
+16F7C;MIAO VOWEL SIGN OU;Mc;0;L;;;;;N;;;;;
+16F7D;MIAO VOWEL SIGN N;Mc;0;L;;;;;N;;;;;
+16F7E;MIAO VOWEL SIGN NG;Mc;0;L;;;;;N;;;;;
+16F8F;MIAO TONE RIGHT;Mn;0;NSM;;;;;N;;;;;
+16F90;MIAO TONE TOP RIGHT;Mn;0;NSM;;;;;N;;;;;
+16F91;MIAO TONE ABOVE;Mn;0;NSM;;;;;N;;;;;
+16F92;MIAO TONE BELOW;Mn;0;NSM;;;;;N;;;;;
+16F93;MIAO LETTER TONE-2;Lm;0;L;;;;;N;;;;;
+16F94;MIAO LETTER TONE-3;Lm;0;L;;;;;N;;;;;
+16F95;MIAO LETTER TONE-4;Lm;0;L;;;;;N;;;;;
+16F96;MIAO LETTER TONE-5;Lm;0;L;;;;;N;;;;;
+16F97;MIAO LETTER TONE-6;Lm;0;L;;;;;N;;;;;
+16F98;MIAO LETTER TONE-7;Lm;0;L;;;;;N;;;;;
+16F99;MIAO LETTER TONE-8;Lm;0;L;;;;;N;;;;;
+16F9A;MIAO LETTER REFORMED TONE-1;Lm;0;L;;;;;N;;;;;
+16F9B;MIAO LETTER REFORMED TONE-2;Lm;0;L;;;;;N;;;;;
+16F9C;MIAO LETTER REFORMED TONE-4;Lm;0;L;;;;;N;;;;;
+16F9D;MIAO LETTER REFORMED TONE-5;Lm;0;L;;;;;N;;;;;
+16F9E;MIAO LETTER REFORMED TONE-6;Lm;0;L;;;;;N;;;;;
+16F9F;MIAO LETTER REFORMED TONE-8;Lm;0;L;;;;;N;;;;;
+16FE0;TANGUT ITERATION MARK;Lm;0;L;;;;;N;;;;;
+17000;<Tangut Ideograph, First>;Lo;0;L;;;;;N;;;;;
+187EC;<Tangut Ideograph, Last>;Lo;0;L;;;;;N;;;;;
+18800;TANGUT COMPONENT-001;Lo;0;L;;;;;N;;;;;
+18801;TANGUT COMPONENT-002;Lo;0;L;;;;;N;;;;;
+18802;TANGUT COMPONENT-003;Lo;0;L;;;;;N;;;;;
+18803;TANGUT COMPONENT-004;Lo;0;L;;;;;N;;;;;
+18804;TANGUT COMPONENT-005;Lo;0;L;;;;;N;;;;;
+18805;TANGUT COMPONENT-006;Lo;0;L;;;;;N;;;;;
+18806;TANGUT COMPONENT-007;Lo;0;L;;;;;N;;;;;
+18807;TANGUT COMPONENT-008;Lo;0;L;;;;;N;;;;;
+18808;TANGUT COMPONENT-009;Lo;0;L;;;;;N;;;;;
+18809;TANGUT COMPONENT-010;Lo;0;L;;;;;N;;;;;
+1880A;TANGUT COMPONENT-011;Lo;0;L;;;;;N;;;;;
+1880B;TANGUT COMPONENT-012;Lo;0;L;;;;;N;;;;;
+1880C;TANGUT COMPONENT-013;Lo;0;L;;;;;N;;;;;
+1880D;TANGUT COMPONENT-014;Lo;0;L;;;;;N;;;;;
+1880E;TANGUT COMPONENT-015;Lo;0;L;;;;;N;;;;;
+1880F;TANGUT COMPONENT-016;Lo;0;L;;;;;N;;;;;
+18810;TANGUT COMPONENT-017;Lo;0;L;;;;;N;;;;;
+18811;TANGUT COMPONENT-018;Lo;0;L;;;;;N;;;;;
+18812;TANGUT COMPONENT-019;Lo;0;L;;;;;N;;;;;
+18813;TANGUT COMPONENT-020;Lo;0;L;;;;;N;;;;;
+18814;TANGUT COMPONENT-021;Lo;0;L;;;;;N;;;;;
+18815;TANGUT COMPONENT-022;Lo;0;L;;;;;N;;;;;
+18816;TANGUT COMPONENT-023;Lo;0;L;;;;;N;;;;;
+18817;TANGUT COMPONENT-024;Lo;0;L;;;;;N;;;;;
+18818;TANGUT COMPONENT-025;Lo;0;L;;;;;N;;;;;
+18819;TANGUT COMPONENT-026;Lo;0;L;;;;;N;;;;;
+1881A;TANGUT COMPONENT-027;Lo;0;L;;;;;N;;;;;
+1881B;TANGUT COMPONENT-028;Lo;0;L;;;;;N;;;;;
+1881C;TANGUT COMPONENT-029;Lo;0;L;;;;;N;;;;;
+1881D;TANGUT COMPONENT-030;Lo;0;L;;;;;N;;;;;
+1881E;TANGUT COMPONENT-031;Lo;0;L;;;;;N;;;;;
+1881F;TANGUT COMPONENT-032;Lo;0;L;;;;;N;;;;;
+18820;TANGUT COMPONENT-033;Lo;0;L;;;;;N;;;;;
+18821;TANGUT COMPONENT-034;Lo;0;L;;;;;N;;;;;
+18822;TANGUT COMPONENT-035;Lo;0;L;;;;;N;;;;;
+18823;TANGUT COMPONENT-036;Lo;0;L;;;;;N;;;;;
+18824;TANGUT COMPONENT-037;Lo;0;L;;;;;N;;;;;
+18825;TANGUT COMPONENT-038;Lo;0;L;;;;;N;;;;;
+18826;TANGUT COMPONENT-039;Lo;0;L;;;;;N;;;;;
+18827;TANGUT COMPONENT-040;Lo;0;L;;;;;N;;;;;
+18828;TANGUT COMPONENT-041;Lo;0;L;;;;;N;;;;;
+18829;TANGUT COMPONENT-042;Lo;0;L;;;;;N;;;;;
+1882A;TANGUT COMPONENT-043;Lo;0;L;;;;;N;;;;;
+1882B;TANGUT COMPONENT-044;Lo;0;L;;;;;N;;;;;
+1882C;TANGUT COMPONENT-045;Lo;0;L;;;;;N;;;;;
+1882D;TANGUT COMPONENT-046;Lo;0;L;;;;;N;;;;;
+1882E;TANGUT COMPONENT-047;Lo;0;L;;;;;N;;;;;
+1882F;TANGUT COMPONENT-048;Lo;0;L;;;;;N;;;;;
+18830;TANGUT COMPONENT-049;Lo;0;L;;;;;N;;;;;
+18831;TANGUT COMPONENT-050;Lo;0;L;;;;;N;;;;;
+18832;TANGUT COMPONENT-051;Lo;0;L;;;;;N;;;;;
+18833;TANGUT COMPONENT-052;Lo;0;L;;;;;N;;;;;
+18834;TANGUT COMPONENT-053;Lo;0;L;;;;;N;;;;;
+18835;TANGUT COMPONENT-054;Lo;0;L;;;;;N;;;;;
+18836;TANGUT COMPONENT-055;Lo;0;L;;;;;N;;;;;
+18837;TANGUT COMPONENT-056;Lo;0;L;;;;;N;;;;;
+18838;TANGUT COMPONENT-057;Lo;0;L;;;;;N;;;;;
+18839;TANGUT COMPONENT-058;Lo;0;L;;;;;N;;;;;
+1883A;TANGUT COMPONENT-059;Lo;0;L;;;;;N;;;;;
+1883B;TANGUT COMPONENT-060;Lo;0;L;;;;;N;;;;;
+1883C;TANGUT COMPONENT-061;Lo;0;L;;;;;N;;;;;
+1883D;TANGUT COMPONENT-062;Lo;0;L;;;;;N;;;;;
+1883E;TANGUT COMPONENT-063;Lo;0;L;;;;;N;;;;;
+1883F;TANGUT COMPONENT-064;Lo;0;L;;;;;N;;;;;
+18840;TANGUT COMPONENT-065;Lo;0;L;;;;;N;;;;;
+18841;TANGUT COMPONENT-066;Lo;0;L;;;;;N;;;;;
+18842;TANGUT COMPONENT-067;Lo;0;L;;;;;N;;;;;
+18843;TANGUT COMPONENT-068;Lo;0;L;;;;;N;;;;;
+18844;TANGUT COMPONENT-069;Lo;0;L;;;;;N;;;;;
+18845;TANGUT COMPONENT-070;Lo;0;L;;;;;N;;;;;
+18846;TANGUT COMPONENT-071;Lo;0;L;;;;;N;;;;;
+18847;TANGUT COMPONENT-072;Lo;0;L;;;;;N;;;;;
+18848;TANGUT COMPONENT-073;Lo;0;L;;;;;N;;;;;
+18849;TANGUT COMPONENT-074;Lo;0;L;;;;;N;;;;;
+1884A;TANGUT COMPONENT-075;Lo;0;L;;;;;N;;;;;
+1884B;TANGUT COMPONENT-076;Lo;0;L;;;;;N;;;;;
+1884C;TANGUT COMPONENT-077;Lo;0;L;;;;;N;;;;;
+1884D;TANGUT COMPONENT-078;Lo;0;L;;;;;N;;;;;
+1884E;TANGUT COMPONENT-079;Lo;0;L;;;;;N;;;;;
+1884F;TANGUT COMPONENT-080;Lo;0;L;;;;;N;;;;;
+18850;TANGUT COMPONENT-081;Lo;0;L;;;;;N;;;;;
+18851;TANGUT COMPONENT-082;Lo;0;L;;;;;N;;;;;
+18852;TANGUT COMPONENT-083;Lo;0;L;;;;;N;;;;;
+18853;TANGUT COMPONENT-084;Lo;0;L;;;;;N;;;;;
+18854;TANGUT COMPONENT-085;Lo;0;L;;;;;N;;;;;
+18855;TANGUT COMPONENT-086;Lo;0;L;;;;;N;;;;;
+18856;TANGUT COMPONENT-087;Lo;0;L;;;;;N;;;;;
+18857;TANGUT COMPONENT-088;Lo;0;L;;;;;N;;;;;
+18858;TANGUT COMPONENT-089;Lo;0;L;;;;;N;;;;;
+18859;TANGUT COMPONENT-090;Lo;0;L;;;;;N;;;;;
+1885A;TANGUT COMPONENT-091;Lo;0;L;;;;;N;;;;;
+1885B;TANGUT COMPONENT-092;Lo;0;L;;;;;N;;;;;
+1885C;TANGUT COMPONENT-093;Lo;0;L;;;;;N;;;;;
+1885D;TANGUT COMPONENT-094;Lo;0;L;;;;;N;;;;;
+1885E;TANGUT COMPONENT-095;Lo;0;L;;;;;N;;;;;
+1885F;TANGUT COMPONENT-096;Lo;0;L;;;;;N;;;;;
+18860;TANGUT COMPONENT-097;Lo;0;L;;;;;N;;;;;
+18861;TANGUT COMPONENT-098;Lo;0;L;;;;;N;;;;;
+18862;TANGUT COMPONENT-099;Lo;0;L;;;;;N;;;;;
+18863;TANGUT COMPONENT-100;Lo;0;L;;;;;N;;;;;
+18864;TANGUT COMPONENT-101;Lo;0;L;;;;;N;;;;;
+18865;TANGUT COMPONENT-102;Lo;0;L;;;;;N;;;;;
+18866;TANGUT COMPONENT-103;Lo;0;L;;;;;N;;;;;
+18867;TANGUT COMPONENT-104;Lo;0;L;;;;;N;;;;;
+18868;TANGUT COMPONENT-105;Lo;0;L;;;;;N;;;;;
+18869;TANGUT COMPONENT-106;Lo;0;L;;;;;N;;;;;
+1886A;TANGUT COMPONENT-107;Lo;0;L;;;;;N;;;;;
+1886B;TANGUT COMPONENT-108;Lo;0;L;;;;;N;;;;;
+1886C;TANGUT COMPONENT-109;Lo;0;L;;;;;N;;;;;
+1886D;TANGUT COMPONENT-110;Lo;0;L;;;;;N;;;;;
+1886E;TANGUT COMPONENT-111;Lo;0;L;;;;;N;;;;;
+1886F;TANGUT COMPONENT-112;Lo;0;L;;;;;N;;;;;
+18870;TANGUT COMPONENT-113;Lo;0;L;;;;;N;;;;;
+18871;TANGUT COMPONENT-114;Lo;0;L;;;;;N;;;;;
+18872;TANGUT COMPONENT-115;Lo;0;L;;;;;N;;;;;
+18873;TANGUT COMPONENT-116;Lo;0;L;;;;;N;;;;;
+18874;TANGUT COMPONENT-117;Lo;0;L;;;;;N;;;;;
+18875;TANGUT COMPONENT-118;Lo;0;L;;;;;N;;;;;
+18876;TANGUT COMPONENT-119;Lo;0;L;;;;;N;;;;;
+18877;TANGUT COMPONENT-120;Lo;0;L;;;;;N;;;;;
+18878;TANGUT COMPONENT-121;Lo;0;L;;;;;N;;;;;
+18879;TANGUT COMPONENT-122;Lo;0;L;;;;;N;;;;;
+1887A;TANGUT COMPONENT-123;Lo;0;L;;;;;N;;;;;
+1887B;TANGUT COMPONENT-124;Lo;0;L;;;;;N;;;;;
+1887C;TANGUT COMPONENT-125;Lo;0;L;;;;;N;;;;;
+1887D;TANGUT COMPONENT-126;Lo;0;L;;;;;N;;;;;
+1887E;TANGUT COMPONENT-127;Lo;0;L;;;;;N;;;;;
+1887F;TANGUT COMPONENT-128;Lo;0;L;;;;;N;;;;;
+18880;TANGUT COMPONENT-129;Lo;0;L;;;;;N;;;;;
+18881;TANGUT COMPONENT-130;Lo;0;L;;;;;N;;;;;
+18882;TANGUT COMPONENT-131;Lo;0;L;;;;;N;;;;;
+18883;TANGUT COMPONENT-132;Lo;0;L;;;;;N;;;;;
+18884;TANGUT COMPONENT-133;Lo;0;L;;;;;N;;;;;
+18885;TANGUT COMPONENT-134;Lo;0;L;;;;;N;;;;;
+18886;TANGUT COMPONENT-135;Lo;0;L;;;;;N;;;;;
+18887;TANGUT COMPONENT-136;Lo;0;L;;;;;N;;;;;
+18888;TANGUT COMPONENT-137;Lo;0;L;;;;;N;;;;;
+18889;TANGUT COMPONENT-138;Lo;0;L;;;;;N;;;;;
+1888A;TANGUT COMPONENT-139;Lo;0;L;;;;;N;;;;;
+1888B;TANGUT COMPONENT-140;Lo;0;L;;;;;N;;;;;
+1888C;TANGUT COMPONENT-141;Lo;0;L;;;;;N;;;;;
+1888D;TANGUT COMPONENT-142;Lo;0;L;;;;;N;;;;;
+1888E;TANGUT COMPONENT-143;Lo;0;L;;;;;N;;;;;
+1888F;TANGUT COMPONENT-144;Lo;0;L;;;;;N;;;;;
+18890;TANGUT COMPONENT-145;Lo;0;L;;;;;N;;;;;
+18891;TANGUT COMPONENT-146;Lo;0;L;;;;;N;;;;;
+18892;TANGUT COMPONENT-147;Lo;0;L;;;;;N;;;;;
+18893;TANGUT COMPONENT-148;Lo;0;L;;;;;N;;;;;
+18894;TANGUT COMPONENT-149;Lo;0;L;;;;;N;;;;;
+18895;TANGUT COMPONENT-150;Lo;0;L;;;;;N;;;;;
+18896;TANGUT COMPONENT-151;Lo;0;L;;;;;N;;;;;
+18897;TANGUT COMPONENT-152;Lo;0;L;;;;;N;;;;;
+18898;TANGUT COMPONENT-153;Lo;0;L;;;;;N;;;;;
+18899;TANGUT COMPONENT-154;Lo;0;L;;;;;N;;;;;
+1889A;TANGUT COMPONENT-155;Lo;0;L;;;;;N;;;;;
+1889B;TANGUT COMPONENT-156;Lo;0;L;;;;;N;;;;;
+1889C;TANGUT COMPONENT-157;Lo;0;L;;;;;N;;;;;
+1889D;TANGUT COMPONENT-158;Lo;0;L;;;;;N;;;;;
+1889E;TANGUT COMPONENT-159;Lo;0;L;;;;;N;;;;;
+1889F;TANGUT COMPONENT-160;Lo;0;L;;;;;N;;;;;
+188A0;TANGUT COMPONENT-161;Lo;0;L;;;;;N;;;;;
+188A1;TANGUT COMPONENT-162;Lo;0;L;;;;;N;;;;;
+188A2;TANGUT COMPONENT-163;Lo;0;L;;;;;N;;;;;
+188A3;TANGUT COMPONENT-164;Lo;0;L;;;;;N;;;;;
+188A4;TANGUT COMPONENT-165;Lo;0;L;;;;;N;;;;;
+188A5;TANGUT COMPONENT-166;Lo;0;L;;;;;N;;;;;
+188A6;TANGUT COMPONENT-167;Lo;0;L;;;;;N;;;;;
+188A7;TANGUT COMPONENT-168;Lo;0;L;;;;;N;;;;;
+188A8;TANGUT COMPONENT-169;Lo;0;L;;;;;N;;;;;
+188A9;TANGUT COMPONENT-170;Lo;0;L;;;;;N;;;;;
+188AA;TANGUT COMPONENT-171;Lo;0;L;;;;;N;;;;;
+188AB;TANGUT COMPONENT-172;Lo;0;L;;;;;N;;;;;
+188AC;TANGUT COMPONENT-173;Lo;0;L;;;;;N;;;;;
+188AD;TANGUT COMPONENT-174;Lo;0;L;;;;;N;;;;;
+188AE;TANGUT COMPONENT-175;Lo;0;L;;;;;N;;;;;
+188AF;TANGUT COMPONENT-176;Lo;0;L;;;;;N;;;;;
+188B0;TANGUT COMPONENT-177;Lo;0;L;;;;;N;;;;;
+188B1;TANGUT COMPONENT-178;Lo;0;L;;;;;N;;;;;
+188B2;TANGUT COMPONENT-179;Lo;0;L;;;;;N;;;;;
+188B3;TANGUT COMPONENT-180;Lo;0;L;;;;;N;;;;;
+188B4;TANGUT COMPONENT-181;Lo;0;L;;;;;N;;;;;
+188B5;TANGUT COMPONENT-182;Lo;0;L;;;;;N;;;;;
+188B6;TANGUT COMPONENT-183;Lo;0;L;;;;;N;;;;;
+188B7;TANGUT COMPONENT-184;Lo;0;L;;;;;N;;;;;
+188B8;TANGUT COMPONENT-185;Lo;0;L;;;;;N;;;;;
+188B9;TANGUT COMPONENT-186;Lo;0;L;;;;;N;;;;;
+188BA;TANGUT COMPONENT-187;Lo;0;L;;;;;N;;;;;
+188BB;TANGUT COMPONENT-188;Lo;0;L;;;;;N;;;;;
+188BC;TANGUT COMPONENT-189;Lo;0;L;;;;;N;;;;;
+188BD;TANGUT COMPONENT-190;Lo;0;L;;;;;N;;;;;
+188BE;TANGUT COMPONENT-191;Lo;0;L;;;;;N;;;;;
+188BF;TANGUT COMPONENT-192;Lo;0;L;;;;;N;;;;;
+188C0;TANGUT COMPONENT-193;Lo;0;L;;;;;N;;;;;
+188C1;TANGUT COMPONENT-194;Lo;0;L;;;;;N;;;;;
+188C2;TANGUT COMPONENT-195;Lo;0;L;;;;;N;;;;;
+188C3;TANGUT COMPONENT-196;Lo;0;L;;;;;N;;;;;
+188C4;TANGUT COMPONENT-197;Lo;0;L;;;;;N;;;;;
+188C5;TANGUT COMPONENT-198;Lo;0;L;;;;;N;;;;;
+188C6;TANGUT COMPONENT-199;Lo;0;L;;;;;N;;;;;
+188C7;TANGUT COMPONENT-200;Lo;0;L;;;;;N;;;;;
+188C8;TANGUT COMPONENT-201;Lo;0;L;;;;;N;;;;;
+188C9;TANGUT COMPONENT-202;Lo;0;L;;;;;N;;;;;
+188CA;TANGUT COMPONENT-203;Lo;0;L;;;;;N;;;;;
+188CB;TANGUT COMPONENT-204;Lo;0;L;;;;;N;;;;;
+188CC;TANGUT COMPONENT-205;Lo;0;L;;;;;N;;;;;
+188CD;TANGUT COMPONENT-206;Lo;0;L;;;;;N;;;;;
+188CE;TANGUT COMPONENT-207;Lo;0;L;;;;;N;;;;;
+188CF;TANGUT COMPONENT-208;Lo;0;L;;;;;N;;;;;
+188D0;TANGUT COMPONENT-209;Lo;0;L;;;;;N;;;;;
+188D1;TANGUT COMPONENT-210;Lo;0;L;;;;;N;;;;;
+188D2;TANGUT COMPONENT-211;Lo;0;L;;;;;N;;;;;
+188D3;TANGUT COMPONENT-212;Lo;0;L;;;;;N;;;;;
+188D4;TANGUT COMPONENT-213;Lo;0;L;;;;;N;;;;;
+188D5;TANGUT COMPONENT-214;Lo;0;L;;;;;N;;;;;
+188D6;TANGUT COMPONENT-215;Lo;0;L;;;;;N;;;;;
+188D7;TANGUT COMPONENT-216;Lo;0;L;;;;;N;;;;;
+188D8;TANGUT COMPONENT-217;Lo;0;L;;;;;N;;;;;
+188D9;TANGUT COMPONENT-218;Lo;0;L;;;;;N;;;;;
+188DA;TANGUT COMPONENT-219;Lo;0;L;;;;;N;;;;;
+188DB;TANGUT COMPONENT-220;Lo;0;L;;;;;N;;;;;
+188DC;TANGUT COMPONENT-221;Lo;0;L;;;;;N;;;;;
+188DD;TANGUT COMPONENT-222;Lo;0;L;;;;;N;;;;;
+188DE;TANGUT COMPONENT-223;Lo;0;L;;;;;N;;;;;
+188DF;TANGUT COMPONENT-224;Lo;0;L;;;;;N;;;;;
+188E0;TANGUT COMPONENT-225;Lo;0;L;;;;;N;;;;;
+188E1;TANGUT COMPONENT-226;Lo;0;L;;;;;N;;;;;
+188E2;TANGUT COMPONENT-227;Lo;0;L;;;;;N;;;;;
+188E3;TANGUT COMPONENT-228;Lo;0;L;;;;;N;;;;;
+188E4;TANGUT COMPONENT-229;Lo;0;L;;;;;N;;;;;
+188E5;TANGUT COMPONENT-230;Lo;0;L;;;;;N;;;;;
+188E6;TANGUT COMPONENT-231;Lo;0;L;;;;;N;;;;;
+188E7;TANGUT COMPONENT-232;Lo;0;L;;;;;N;;;;;
+188E8;TANGUT COMPONENT-233;Lo;0;L;;;;;N;;;;;
+188E9;TANGUT COMPONENT-234;Lo;0;L;;;;;N;;;;;
+188EA;TANGUT COMPONENT-235;Lo;0;L;;;;;N;;;;;
+188EB;TANGUT COMPONENT-236;Lo;0;L;;;;;N;;;;;
+188EC;TANGUT COMPONENT-237;Lo;0;L;;;;;N;;;;;
+188ED;TANGUT COMPONENT-238;Lo;0;L;;;;;N;;;;;
+188EE;TANGUT COMPONENT-239;Lo;0;L;;;;;N;;;;;
+188EF;TANGUT COMPONENT-240;Lo;0;L;;;;;N;;;;;
+188F0;TANGUT COMPONENT-241;Lo;0;L;;;;;N;;;;;
+188F1;TANGUT COMPONENT-242;Lo;0;L;;;;;N;;;;;
+188F2;TANGUT COMPONENT-243;Lo;0;L;;;;;N;;;;;
+188F3;TANGUT COMPONENT-244;Lo;0;L;;;;;N;;;;;
+188F4;TANGUT COMPONENT-245;Lo;0;L;;;;;N;;;;;
+188F5;TANGUT COMPONENT-246;Lo;0;L;;;;;N;;;;;
+188F6;TANGUT COMPONENT-247;Lo;0;L;;;;;N;;;;;
+188F7;TANGUT COMPONENT-248;Lo;0;L;;;;;N;;;;;
+188F8;TANGUT COMPONENT-249;Lo;0;L;;;;;N;;;;;
+188F9;TANGUT COMPONENT-250;Lo;0;L;;;;;N;;;;;
+188FA;TANGUT COMPONENT-251;Lo;0;L;;;;;N;;;;;
+188FB;TANGUT COMPONENT-252;Lo;0;L;;;;;N;;;;;
+188FC;TANGUT COMPONENT-253;Lo;0;L;;;;;N;;;;;
+188FD;TANGUT COMPONENT-254;Lo;0;L;;;;;N;;;;;
+188FE;TANGUT COMPONENT-255;Lo;0;L;;;;;N;;;;;
+188FF;TANGUT COMPONENT-256;Lo;0;L;;;;;N;;;;;
+18900;TANGUT COMPONENT-257;Lo;0;L;;;;;N;;;;;
+18901;TANGUT COMPONENT-258;Lo;0;L;;;;;N;;;;;
+18902;TANGUT COMPONENT-259;Lo;0;L;;;;;N;;;;;
+18903;TANGUT COMPONENT-260;Lo;0;L;;;;;N;;;;;
+18904;TANGUT COMPONENT-261;Lo;0;L;;;;;N;;;;;
+18905;TANGUT COMPONENT-262;Lo;0;L;;;;;N;;;;;
+18906;TANGUT COMPONENT-263;Lo;0;L;;;;;N;;;;;
+18907;TANGUT COMPONENT-264;Lo;0;L;;;;;N;;;;;
+18908;TANGUT COMPONENT-265;Lo;0;L;;;;;N;;;;;
+18909;TANGUT COMPONENT-266;Lo;0;L;;;;;N;;;;;
+1890A;TANGUT COMPONENT-267;Lo;0;L;;;;;N;;;;;
+1890B;TANGUT COMPONENT-268;Lo;0;L;;;;;N;;;;;
+1890C;TANGUT COMPONENT-269;Lo;0;L;;;;;N;;;;;
+1890D;TANGUT COMPONENT-270;Lo;0;L;;;;;N;;;;;
+1890E;TANGUT COMPONENT-271;Lo;0;L;;;;;N;;;;;
+1890F;TANGUT COMPONENT-272;Lo;0;L;;;;;N;;;;;
+18910;TANGUT COMPONENT-273;Lo;0;L;;;;;N;;;;;
+18911;TANGUT COMPONENT-274;Lo;0;L;;;;;N;;;;;
+18912;TANGUT COMPONENT-275;Lo;0;L;;;;;N;;;;;
+18913;TANGUT COMPONENT-276;Lo;0;L;;;;;N;;;;;
+18914;TANGUT COMPONENT-277;Lo;0;L;;;;;N;;;;;
+18915;TANGUT COMPONENT-278;Lo;0;L;;;;;N;;;;;
+18916;TANGUT COMPONENT-279;Lo;0;L;;;;;N;;;;;
+18917;TANGUT COMPONENT-280;Lo;0;L;;;;;N;;;;;
+18918;TANGUT COMPONENT-281;Lo;0;L;;;;;N;;;;;
+18919;TANGUT COMPONENT-282;Lo;0;L;;;;;N;;;;;
+1891A;TANGUT COMPONENT-283;Lo;0;L;;;;;N;;;;;
+1891B;TANGUT COMPONENT-284;Lo;0;L;;;;;N;;;;;
+1891C;TANGUT COMPONENT-285;Lo;0;L;;;;;N;;;;;
+1891D;TANGUT COMPONENT-286;Lo;0;L;;;;;N;;;;;
+1891E;TANGUT COMPONENT-287;Lo;0;L;;;;;N;;;;;
+1891F;TANGUT COMPONENT-288;Lo;0;L;;;;;N;;;;;
+18920;TANGUT COMPONENT-289;Lo;0;L;;;;;N;;;;;
+18921;TANGUT COMPONENT-290;Lo;0;L;;;;;N;;;;;
+18922;TANGUT COMPONENT-291;Lo;0;L;;;;;N;;;;;
+18923;TANGUT COMPONENT-292;Lo;0;L;;;;;N;;;;;
+18924;TANGUT COMPONENT-293;Lo;0;L;;;;;N;;;;;
+18925;TANGUT COMPONENT-294;Lo;0;L;;;;;N;;;;;
+18926;TANGUT COMPONENT-295;Lo;0;L;;;;;N;;;;;
+18927;TANGUT COMPONENT-296;Lo;0;L;;;;;N;;;;;
+18928;TANGUT COMPONENT-297;Lo;0;L;;;;;N;;;;;
+18929;TANGUT COMPONENT-298;Lo;0;L;;;;;N;;;;;
+1892A;TANGUT COMPONENT-299;Lo;0;L;;;;;N;;;;;
+1892B;TANGUT COMPONENT-300;Lo;0;L;;;;;N;;;;;
+1892C;TANGUT COMPONENT-301;Lo;0;L;;;;;N;;;;;
+1892D;TANGUT COMPONENT-302;Lo;0;L;;;;;N;;;;;
+1892E;TANGUT COMPONENT-303;Lo;0;L;;;;;N;;;;;
+1892F;TANGUT COMPONENT-304;Lo;0;L;;;;;N;;;;;
+18930;TANGUT COMPONENT-305;Lo;0;L;;;;;N;;;;;
+18931;TANGUT COMPONENT-306;Lo;0;L;;;;;N;;;;;
+18932;TANGUT COMPONENT-307;Lo;0;L;;;;;N;;;;;
+18933;TANGUT COMPONENT-308;Lo;0;L;;;;;N;;;;;
+18934;TANGUT COMPONENT-309;Lo;0;L;;;;;N;;;;;
+18935;TANGUT COMPONENT-310;Lo;0;L;;;;;N;;;;;
+18936;TANGUT COMPONENT-311;Lo;0;L;;;;;N;;;;;
+18937;TANGUT COMPONENT-312;Lo;0;L;;;;;N;;;;;
+18938;TANGUT COMPONENT-313;Lo;0;L;;;;;N;;;;;
+18939;TANGUT COMPONENT-314;Lo;0;L;;;;;N;;;;;
+1893A;TANGUT COMPONENT-315;Lo;0;L;;;;;N;;;;;
+1893B;TANGUT COMPONENT-316;Lo;0;L;;;;;N;;;;;
+1893C;TANGUT COMPONENT-317;Lo;0;L;;;;;N;;;;;
+1893D;TANGUT COMPONENT-318;Lo;0;L;;;;;N;;;;;
+1893E;TANGUT COMPONENT-319;Lo;0;L;;;;;N;;;;;
+1893F;TANGUT COMPONENT-320;Lo;0;L;;;;;N;;;;;
+18940;TANGUT COMPONENT-321;Lo;0;L;;;;;N;;;;;
+18941;TANGUT COMPONENT-322;Lo;0;L;;;;;N;;;;;
+18942;TANGUT COMPONENT-323;Lo;0;L;;;;;N;;;;;
+18943;TANGUT COMPONENT-324;Lo;0;L;;;;;N;;;;;
+18944;TANGUT COMPONENT-325;Lo;0;L;;;;;N;;;;;
+18945;TANGUT COMPONENT-326;Lo;0;L;;;;;N;;;;;
+18946;TANGUT COMPONENT-327;Lo;0;L;;;;;N;;;;;
+18947;TANGUT COMPONENT-328;Lo;0;L;;;;;N;;;;;
+18948;TANGUT COMPONENT-329;Lo;0;L;;;;;N;;;;;
+18949;TANGUT COMPONENT-330;Lo;0;L;;;;;N;;;;;
+1894A;TANGUT COMPONENT-331;Lo;0;L;;;;;N;;;;;
+1894B;TANGUT COMPONENT-332;Lo;0;L;;;;;N;;;;;
+1894C;TANGUT COMPONENT-333;Lo;0;L;;;;;N;;;;;
+1894D;TANGUT COMPONENT-334;Lo;0;L;;;;;N;;;;;
+1894E;TANGUT COMPONENT-335;Lo;0;L;;;;;N;;;;;
+1894F;TANGUT COMPONENT-336;Lo;0;L;;;;;N;;;;;
+18950;TANGUT COMPONENT-337;Lo;0;L;;;;;N;;;;;
+18951;TANGUT COMPONENT-338;Lo;0;L;;;;;N;;;;;
+18952;TANGUT COMPONENT-339;Lo;0;L;;;;;N;;;;;
+18953;TANGUT COMPONENT-340;Lo;0;L;;;;;N;;;;;
+18954;TANGUT COMPONENT-341;Lo;0;L;;;;;N;;;;;
+18955;TANGUT COMPONENT-342;Lo;0;L;;;;;N;;;;;
+18956;TANGUT COMPONENT-343;Lo;0;L;;;;;N;;;;;
+18957;TANGUT COMPONENT-344;Lo;0;L;;;;;N;;;;;
+18958;TANGUT COMPONENT-345;Lo;0;L;;;;;N;;;;;
+18959;TANGUT COMPONENT-346;Lo;0;L;;;;;N;;;;;
+1895A;TANGUT COMPONENT-347;Lo;0;L;;;;;N;;;;;
+1895B;TANGUT COMPONENT-348;Lo;0;L;;;;;N;;;;;
+1895C;TANGUT COMPONENT-349;Lo;0;L;;;;;N;;;;;
+1895D;TANGUT COMPONENT-350;Lo;0;L;;;;;N;;;;;
+1895E;TANGUT COMPONENT-351;Lo;0;L;;;;;N;;;;;
+1895F;TANGUT COMPONENT-352;Lo;0;L;;;;;N;;;;;
+18960;TANGUT COMPONENT-353;Lo;0;L;;;;;N;;;;;
+18961;TANGUT COMPONENT-354;Lo;0;L;;;;;N;;;;;
+18962;TANGUT COMPONENT-355;Lo;0;L;;;;;N;;;;;
+18963;TANGUT COMPONENT-356;Lo;0;L;;;;;N;;;;;
+18964;TANGUT COMPONENT-357;Lo;0;L;;;;;N;;;;;
+18965;TANGUT COMPONENT-358;Lo;0;L;;;;;N;;;;;
+18966;TANGUT COMPONENT-359;Lo;0;L;;;;;N;;;;;
+18967;TANGUT COMPONENT-360;Lo;0;L;;;;;N;;;;;
+18968;TANGUT COMPONENT-361;Lo;0;L;;;;;N;;;;;
+18969;TANGUT COMPONENT-362;Lo;0;L;;;;;N;;;;;
+1896A;TANGUT COMPONENT-363;Lo;0;L;;;;;N;;;;;
+1896B;TANGUT COMPONENT-364;Lo;0;L;;;;;N;;;;;
+1896C;TANGUT COMPONENT-365;Lo;0;L;;;;;N;;;;;
+1896D;TANGUT COMPONENT-366;Lo;0;L;;;;;N;;;;;
+1896E;TANGUT COMPONENT-367;Lo;0;L;;;;;N;;;;;
+1896F;TANGUT COMPONENT-368;Lo;0;L;;;;;N;;;;;
+18970;TANGUT COMPONENT-369;Lo;0;L;;;;;N;;;;;
+18971;TANGUT COMPONENT-370;Lo;0;L;;;;;N;;;;;
+18972;TANGUT COMPONENT-371;Lo;0;L;;;;;N;;;;;
+18973;TANGUT COMPONENT-372;Lo;0;L;;;;;N;;;;;
+18974;TANGUT COMPONENT-373;Lo;0;L;;;;;N;;;;;
+18975;TANGUT COMPONENT-374;Lo;0;L;;;;;N;;;;;
+18976;TANGUT COMPONENT-375;Lo;0;L;;;;;N;;;;;
+18977;TANGUT COMPONENT-376;Lo;0;L;;;;;N;;;;;
+18978;TANGUT COMPONENT-377;Lo;0;L;;;;;N;;;;;
+18979;TANGUT COMPONENT-378;Lo;0;L;;;;;N;;;;;
+1897A;TANGUT COMPONENT-379;Lo;0;L;;;;;N;;;;;
+1897B;TANGUT COMPONENT-380;Lo;0;L;;;;;N;;;;;
+1897C;TANGUT COMPONENT-381;Lo;0;L;;;;;N;;;;;
+1897D;TANGUT COMPONENT-382;Lo;0;L;;;;;N;;;;;
+1897E;TANGUT COMPONENT-383;Lo;0;L;;;;;N;;;;;
+1897F;TANGUT COMPONENT-384;Lo;0;L;;;;;N;;;;;
+18980;TANGUT COMPONENT-385;Lo;0;L;;;;;N;;;;;
+18981;TANGUT COMPONENT-386;Lo;0;L;;;;;N;;;;;
+18982;TANGUT COMPONENT-387;Lo;0;L;;;;;N;;;;;
+18983;TANGUT COMPONENT-388;Lo;0;L;;;;;N;;;;;
+18984;TANGUT COMPONENT-389;Lo;0;L;;;;;N;;;;;
+18985;TANGUT COMPONENT-390;Lo;0;L;;;;;N;;;;;
+18986;TANGUT COMPONENT-391;Lo;0;L;;;;;N;;;;;
+18987;TANGUT COMPONENT-392;Lo;0;L;;;;;N;;;;;
+18988;TANGUT COMPONENT-393;Lo;0;L;;;;;N;;;;;
+18989;TANGUT COMPONENT-394;Lo;0;L;;;;;N;;;;;
+1898A;TANGUT COMPONENT-395;Lo;0;L;;;;;N;;;;;
+1898B;TANGUT COMPONENT-396;Lo;0;L;;;;;N;;;;;
+1898C;TANGUT COMPONENT-397;Lo;0;L;;;;;N;;;;;
+1898D;TANGUT COMPONENT-398;Lo;0;L;;;;;N;;;;;
+1898E;TANGUT COMPONENT-399;Lo;0;L;;;;;N;;;;;
+1898F;TANGUT COMPONENT-400;Lo;0;L;;;;;N;;;;;
+18990;TANGUT COMPONENT-401;Lo;0;L;;;;;N;;;;;
+18991;TANGUT COMPONENT-402;Lo;0;L;;;;;N;;;;;
+18992;TANGUT COMPONENT-403;Lo;0;L;;;;;N;;;;;
+18993;TANGUT COMPONENT-404;Lo;0;L;;;;;N;;;;;
+18994;TANGUT COMPONENT-405;Lo;0;L;;;;;N;;;;;
+18995;TANGUT COMPONENT-406;Lo;0;L;;;;;N;;;;;
+18996;TANGUT COMPONENT-407;Lo;0;L;;;;;N;;;;;
+18997;TANGUT COMPONENT-408;Lo;0;L;;;;;N;;;;;
+18998;TANGUT COMPONENT-409;Lo;0;L;;;;;N;;;;;
+18999;TANGUT COMPONENT-410;Lo;0;L;;;;;N;;;;;
+1899A;TANGUT COMPONENT-411;Lo;0;L;;;;;N;;;;;
+1899B;TANGUT COMPONENT-412;Lo;0;L;;;;;N;;;;;
+1899C;TANGUT COMPONENT-413;Lo;0;L;;;;;N;;;;;
+1899D;TANGUT COMPONENT-414;Lo;0;L;;;;;N;;;;;
+1899E;TANGUT COMPONENT-415;Lo;0;L;;;;;N;;;;;
+1899F;TANGUT COMPONENT-416;Lo;0;L;;;;;N;;;;;
+189A0;TANGUT COMPONENT-417;Lo;0;L;;;;;N;;;;;
+189A1;TANGUT COMPONENT-418;Lo;0;L;;;;;N;;;;;
+189A2;TANGUT COMPONENT-419;Lo;0;L;;;;;N;;;;;
+189A3;TANGUT COMPONENT-420;Lo;0;L;;;;;N;;;;;
+189A4;TANGUT COMPONENT-421;Lo;0;L;;;;;N;;;;;
+189A5;TANGUT COMPONENT-422;Lo;0;L;;;;;N;;;;;
+189A6;TANGUT COMPONENT-423;Lo;0;L;;;;;N;;;;;
+189A7;TANGUT COMPONENT-424;Lo;0;L;;;;;N;;;;;
+189A8;TANGUT COMPONENT-425;Lo;0;L;;;;;N;;;;;
+189A9;TANGUT COMPONENT-426;Lo;0;L;;;;;N;;;;;
+189AA;TANGUT COMPONENT-427;Lo;0;L;;;;;N;;;;;
+189AB;TANGUT COMPONENT-428;Lo;0;L;;;;;N;;;;;
+189AC;TANGUT COMPONENT-429;Lo;0;L;;;;;N;;;;;
+189AD;TANGUT COMPONENT-430;Lo;0;L;;;;;N;;;;;
+189AE;TANGUT COMPONENT-431;Lo;0;L;;;;;N;;;;;
+189AF;TANGUT COMPONENT-432;Lo;0;L;;;;;N;;;;;
+189B0;TANGUT COMPONENT-433;Lo;0;L;;;;;N;;;;;
+189B1;TANGUT COMPONENT-434;Lo;0;L;;;;;N;;;;;
+189B2;TANGUT COMPONENT-435;Lo;0;L;;;;;N;;;;;
+189B3;TANGUT COMPONENT-436;Lo;0;L;;;;;N;;;;;
+189B4;TANGUT COMPONENT-437;Lo;0;L;;;;;N;;;;;
+189B5;TANGUT COMPONENT-438;Lo;0;L;;;;;N;;;;;
+189B6;TANGUT COMPONENT-439;Lo;0;L;;;;;N;;;;;
+189B7;TANGUT COMPONENT-440;Lo;0;L;;;;;N;;;;;
+189B8;TANGUT COMPONENT-441;Lo;0;L;;;;;N;;;;;
+189B9;TANGUT COMPONENT-442;Lo;0;L;;;;;N;;;;;
+189BA;TANGUT COMPONENT-443;Lo;0;L;;;;;N;;;;;
+189BB;TANGUT COMPONENT-444;Lo;0;L;;;;;N;;;;;
+189BC;TANGUT COMPONENT-445;Lo;0;L;;;;;N;;;;;
+189BD;TANGUT COMPONENT-446;Lo;0;L;;;;;N;;;;;
+189BE;TANGUT COMPONENT-447;Lo;0;L;;;;;N;;;;;
+189BF;TANGUT COMPONENT-448;Lo;0;L;;;;;N;;;;;
+189C0;TANGUT COMPONENT-449;Lo;0;L;;;;;N;;;;;
+189C1;TANGUT COMPONENT-450;Lo;0;L;;;;;N;;;;;
+189C2;TANGUT COMPONENT-451;Lo;0;L;;;;;N;;;;;
+189C3;TANGUT COMPONENT-452;Lo;0;L;;;;;N;;;;;
+189C4;TANGUT COMPONENT-453;Lo;0;L;;;;;N;;;;;
+189C5;TANGUT COMPONENT-454;Lo;0;L;;;;;N;;;;;
+189C6;TANGUT COMPONENT-455;Lo;0;L;;;;;N;;;;;
+189C7;TANGUT COMPONENT-456;Lo;0;L;;;;;N;;;;;
+189C8;TANGUT COMPONENT-457;Lo;0;L;;;;;N;;;;;
+189C9;TANGUT COMPONENT-458;Lo;0;L;;;;;N;;;;;
+189CA;TANGUT COMPONENT-459;Lo;0;L;;;;;N;;;;;
+189CB;TANGUT COMPONENT-460;Lo;0;L;;;;;N;;;;;
+189CC;TANGUT COMPONENT-461;Lo;0;L;;;;;N;;;;;
+189CD;TANGUT COMPONENT-462;Lo;0;L;;;;;N;;;;;
+189CE;TANGUT COMPONENT-463;Lo;0;L;;;;;N;;;;;
+189CF;TANGUT COMPONENT-464;Lo;0;L;;;;;N;;;;;
+189D0;TANGUT COMPONENT-465;Lo;0;L;;;;;N;;;;;
+189D1;TANGUT COMPONENT-466;Lo;0;L;;;;;N;;;;;
+189D2;TANGUT COMPONENT-467;Lo;0;L;;;;;N;;;;;
+189D3;TANGUT COMPONENT-468;Lo;0;L;;;;;N;;;;;
+189D4;TANGUT COMPONENT-469;Lo;0;L;;;;;N;;;;;
+189D5;TANGUT COMPONENT-470;Lo;0;L;;;;;N;;;;;
+189D6;TANGUT COMPONENT-471;Lo;0;L;;;;;N;;;;;
+189D7;TANGUT COMPONENT-472;Lo;0;L;;;;;N;;;;;
+189D8;TANGUT COMPONENT-473;Lo;0;L;;;;;N;;;;;
+189D9;TANGUT COMPONENT-474;Lo;0;L;;;;;N;;;;;
+189DA;TANGUT COMPONENT-475;Lo;0;L;;;;;N;;;;;
+189DB;TANGUT COMPONENT-476;Lo;0;L;;;;;N;;;;;
+189DC;TANGUT COMPONENT-477;Lo;0;L;;;;;N;;;;;
+189DD;TANGUT COMPONENT-478;Lo;0;L;;;;;N;;;;;
+189DE;TANGUT COMPONENT-479;Lo;0;L;;;;;N;;;;;
+189DF;TANGUT COMPONENT-480;Lo;0;L;;;;;N;;;;;
+189E0;TANGUT COMPONENT-481;Lo;0;L;;;;;N;;;;;
+189E1;TANGUT COMPONENT-482;Lo;0;L;;;;;N;;;;;
+189E2;TANGUT COMPONENT-483;Lo;0;L;;;;;N;;;;;
+189E3;TANGUT COMPONENT-484;Lo;0;L;;;;;N;;;;;
+189E4;TANGUT COMPONENT-485;Lo;0;L;;;;;N;;;;;
+189E5;TANGUT COMPONENT-486;Lo;0;L;;;;;N;;;;;
+189E6;TANGUT COMPONENT-487;Lo;0;L;;;;;N;;;;;
+189E7;TANGUT COMPONENT-488;Lo;0;L;;;;;N;;;;;
+189E8;TANGUT COMPONENT-489;Lo;0;L;;;;;N;;;;;
+189E9;TANGUT COMPONENT-490;Lo;0;L;;;;;N;;;;;
+189EA;TANGUT COMPONENT-491;Lo;0;L;;;;;N;;;;;
+189EB;TANGUT COMPONENT-492;Lo;0;L;;;;;N;;;;;
+189EC;TANGUT COMPONENT-493;Lo;0;L;;;;;N;;;;;
+189ED;TANGUT COMPONENT-494;Lo;0;L;;;;;N;;;;;
+189EE;TANGUT COMPONENT-495;Lo;0;L;;;;;N;;;;;
+189EF;TANGUT COMPONENT-496;Lo;0;L;;;;;N;;;;;
+189F0;TANGUT COMPONENT-497;Lo;0;L;;;;;N;;;;;
+189F1;TANGUT COMPONENT-498;Lo;0;L;;;;;N;;;;;
+189F2;TANGUT COMPONENT-499;Lo;0;L;;;;;N;;;;;
+189F3;TANGUT COMPONENT-500;Lo;0;L;;;;;N;;;;;
+189F4;TANGUT COMPONENT-501;Lo;0;L;;;;;N;;;;;
+189F5;TANGUT COMPONENT-502;Lo;0;L;;;;;N;;;;;
+189F6;TANGUT COMPONENT-503;Lo;0;L;;;;;N;;;;;
+189F7;TANGUT COMPONENT-504;Lo;0;L;;;;;N;;;;;
+189F8;TANGUT COMPONENT-505;Lo;0;L;;;;;N;;;;;
+189F9;TANGUT COMPONENT-506;Lo;0;L;;;;;N;;;;;
+189FA;TANGUT COMPONENT-507;Lo;0;L;;;;;N;;;;;
+189FB;TANGUT COMPONENT-508;Lo;0;L;;;;;N;;;;;
+189FC;TANGUT COMPONENT-509;Lo;0;L;;;;;N;;;;;
+189FD;TANGUT COMPONENT-510;Lo;0;L;;;;;N;;;;;
+189FE;TANGUT COMPONENT-511;Lo;0;L;;;;;N;;;;;
+189FF;TANGUT COMPONENT-512;Lo;0;L;;;;;N;;;;;
+18A00;TANGUT COMPONENT-513;Lo;0;L;;;;;N;;;;;
+18A01;TANGUT COMPONENT-514;Lo;0;L;;;;;N;;;;;
+18A02;TANGUT COMPONENT-515;Lo;0;L;;;;;N;;;;;
+18A03;TANGUT COMPONENT-516;Lo;0;L;;;;;N;;;;;
+18A04;TANGUT COMPONENT-517;Lo;0;L;;;;;N;;;;;
+18A05;TANGUT COMPONENT-518;Lo;0;L;;;;;N;;;;;
+18A06;TANGUT COMPONENT-519;Lo;0;L;;;;;N;;;;;
+18A07;TANGUT COMPONENT-520;Lo;0;L;;;;;N;;;;;
+18A08;TANGUT COMPONENT-521;Lo;0;L;;;;;N;;;;;
+18A09;TANGUT COMPONENT-522;Lo;0;L;;;;;N;;;;;
+18A0A;TANGUT COMPONENT-523;Lo;0;L;;;;;N;;;;;
+18A0B;TANGUT COMPONENT-524;Lo;0;L;;;;;N;;;;;
+18A0C;TANGUT COMPONENT-525;Lo;0;L;;;;;N;;;;;
+18A0D;TANGUT COMPONENT-526;Lo;0;L;;;;;N;;;;;
+18A0E;TANGUT COMPONENT-527;Lo;0;L;;;;;N;;;;;
+18A0F;TANGUT COMPONENT-528;Lo;0;L;;;;;N;;;;;
+18A10;TANGUT COMPONENT-529;Lo;0;L;;;;;N;;;;;
+18A11;TANGUT COMPONENT-530;Lo;0;L;;;;;N;;;;;
+18A12;TANGUT COMPONENT-531;Lo;0;L;;;;;N;;;;;
+18A13;TANGUT COMPONENT-532;Lo;0;L;;;;;N;;;;;
+18A14;TANGUT COMPONENT-533;Lo;0;L;;;;;N;;;;;
+18A15;TANGUT COMPONENT-534;Lo;0;L;;;;;N;;;;;
+18A16;TANGUT COMPONENT-535;Lo;0;L;;;;;N;;;;;
+18A17;TANGUT COMPONENT-536;Lo;0;L;;;;;N;;;;;
+18A18;TANGUT COMPONENT-537;Lo;0;L;;;;;N;;;;;
+18A19;TANGUT COMPONENT-538;Lo;0;L;;;;;N;;;;;
+18A1A;TANGUT COMPONENT-539;Lo;0;L;;;;;N;;;;;
+18A1B;TANGUT COMPONENT-540;Lo;0;L;;;;;N;;;;;
+18A1C;TANGUT COMPONENT-541;Lo;0;L;;;;;N;;;;;
+18A1D;TANGUT COMPONENT-542;Lo;0;L;;;;;N;;;;;
+18A1E;TANGUT COMPONENT-543;Lo;0;L;;;;;N;;;;;
+18A1F;TANGUT COMPONENT-544;Lo;0;L;;;;;N;;;;;
+18A20;TANGUT COMPONENT-545;Lo;0;L;;;;;N;;;;;
+18A21;TANGUT COMPONENT-546;Lo;0;L;;;;;N;;;;;
+18A22;TANGUT COMPONENT-547;Lo;0;L;;;;;N;;;;;
+18A23;TANGUT COMPONENT-548;Lo;0;L;;;;;N;;;;;
+18A24;TANGUT COMPONENT-549;Lo;0;L;;;;;N;;;;;
+18A25;TANGUT COMPONENT-550;Lo;0;L;;;;;N;;;;;
+18A26;TANGUT COMPONENT-551;Lo;0;L;;;;;N;;;;;
+18A27;TANGUT COMPONENT-552;Lo;0;L;;;;;N;;;;;
+18A28;TANGUT COMPONENT-553;Lo;0;L;;;;;N;;;;;
+18A29;TANGUT COMPONENT-554;Lo;0;L;;;;;N;;;;;
+18A2A;TANGUT COMPONENT-555;Lo;0;L;;;;;N;;;;;
+18A2B;TANGUT COMPONENT-556;Lo;0;L;;;;;N;;;;;
+18A2C;TANGUT COMPONENT-557;Lo;0;L;;;;;N;;;;;
+18A2D;TANGUT COMPONENT-558;Lo;0;L;;;;;N;;;;;
+18A2E;TANGUT COMPONENT-559;Lo;0;L;;;;;N;;;;;
+18A2F;TANGUT COMPONENT-560;Lo;0;L;;;;;N;;;;;
+18A30;TANGUT COMPONENT-561;Lo;0;L;;;;;N;;;;;
+18A31;TANGUT COMPONENT-562;Lo;0;L;;;;;N;;;;;
+18A32;TANGUT COMPONENT-563;Lo;0;L;;;;;N;;;;;
+18A33;TANGUT COMPONENT-564;Lo;0;L;;;;;N;;;;;
+18A34;TANGUT COMPONENT-565;Lo;0;L;;;;;N;;;;;
+18A35;TANGUT COMPONENT-566;Lo;0;L;;;;;N;;;;;
+18A36;TANGUT COMPONENT-567;Lo;0;L;;;;;N;;;;;
+18A37;TANGUT COMPONENT-568;Lo;0;L;;;;;N;;;;;
+18A38;TANGUT COMPONENT-569;Lo;0;L;;;;;N;;;;;
+18A39;TANGUT COMPONENT-570;Lo;0;L;;;;;N;;;;;
+18A3A;TANGUT COMPONENT-571;Lo;0;L;;;;;N;;;;;
+18A3B;TANGUT COMPONENT-572;Lo;0;L;;;;;N;;;;;
+18A3C;TANGUT COMPONENT-573;Lo;0;L;;;;;N;;;;;
+18A3D;TANGUT COMPONENT-574;Lo;0;L;;;;;N;;;;;
+18A3E;TANGUT COMPONENT-575;Lo;0;L;;;;;N;;;;;
+18A3F;TANGUT COMPONENT-576;Lo;0;L;;;;;N;;;;;
+18A40;TANGUT COMPONENT-577;Lo;0;L;;;;;N;;;;;
+18A41;TANGUT COMPONENT-578;Lo;0;L;;;;;N;;;;;
+18A42;TANGUT COMPONENT-579;Lo;0;L;;;;;N;;;;;
+18A43;TANGUT COMPONENT-580;Lo;0;L;;;;;N;;;;;
+18A44;TANGUT COMPONENT-581;Lo;0;L;;;;;N;;;;;
+18A45;TANGUT COMPONENT-582;Lo;0;L;;;;;N;;;;;
+18A46;TANGUT COMPONENT-583;Lo;0;L;;;;;N;;;;;
+18A47;TANGUT COMPONENT-584;Lo;0;L;;;;;N;;;;;
+18A48;TANGUT COMPONENT-585;Lo;0;L;;;;;N;;;;;
+18A49;TANGUT COMPONENT-586;Lo;0;L;;;;;N;;;;;
+18A4A;TANGUT COMPONENT-587;Lo;0;L;;;;;N;;;;;
+18A4B;TANGUT COMPONENT-588;Lo;0;L;;;;;N;;;;;
+18A4C;TANGUT COMPONENT-589;Lo;0;L;;;;;N;;;;;
+18A4D;TANGUT COMPONENT-590;Lo;0;L;;;;;N;;;;;
+18A4E;TANGUT COMPONENT-591;Lo;0;L;;;;;N;;;;;
+18A4F;TANGUT COMPONENT-592;Lo;0;L;;;;;N;;;;;
+18A50;TANGUT COMPONENT-593;Lo;0;L;;;;;N;;;;;
+18A51;TANGUT COMPONENT-594;Lo;0;L;;;;;N;;;;;
+18A52;TANGUT COMPONENT-595;Lo;0;L;;;;;N;;;;;
+18A53;TANGUT COMPONENT-596;Lo;0;L;;;;;N;;;;;
+18A54;TANGUT COMPONENT-597;Lo;0;L;;;;;N;;;;;
+18A55;TANGUT COMPONENT-598;Lo;0;L;;;;;N;;;;;
+18A56;TANGUT COMPONENT-599;Lo;0;L;;;;;N;;;;;
+18A57;TANGUT COMPONENT-600;Lo;0;L;;;;;N;;;;;
+18A58;TANGUT COMPONENT-601;Lo;0;L;;;;;N;;;;;
+18A59;TANGUT COMPONENT-602;Lo;0;L;;;;;N;;;;;
+18A5A;TANGUT COMPONENT-603;Lo;0;L;;;;;N;;;;;
+18A5B;TANGUT COMPONENT-604;Lo;0;L;;;;;N;;;;;
+18A5C;TANGUT COMPONENT-605;Lo;0;L;;;;;N;;;;;
+18A5D;TANGUT COMPONENT-606;Lo;0;L;;;;;N;;;;;
+18A5E;TANGUT COMPONENT-607;Lo;0;L;;;;;N;;;;;
+18A5F;TANGUT COMPONENT-608;Lo;0;L;;;;;N;;;;;
+18A60;TANGUT COMPONENT-609;Lo;0;L;;;;;N;;;;;
+18A61;TANGUT COMPONENT-610;Lo;0;L;;;;;N;;;;;
+18A62;TANGUT COMPONENT-611;Lo;0;L;;;;;N;;;;;
+18A63;TANGUT COMPONENT-612;Lo;0;L;;;;;N;;;;;
+18A64;TANGUT COMPONENT-613;Lo;0;L;;;;;N;;;;;
+18A65;TANGUT COMPONENT-614;Lo;0;L;;;;;N;;;;;
+18A66;TANGUT COMPONENT-615;Lo;0;L;;;;;N;;;;;
+18A67;TANGUT COMPONENT-616;Lo;0;L;;;;;N;;;;;
+18A68;TANGUT COMPONENT-617;Lo;0;L;;;;;N;;;;;
+18A69;TANGUT COMPONENT-618;Lo;0;L;;;;;N;;;;;
+18A6A;TANGUT COMPONENT-619;Lo;0;L;;;;;N;;;;;
+18A6B;TANGUT COMPONENT-620;Lo;0;L;;;;;N;;;;;
+18A6C;TANGUT COMPONENT-621;Lo;0;L;;;;;N;;;;;
+18A6D;TANGUT COMPONENT-622;Lo;0;L;;;;;N;;;;;
+18A6E;TANGUT COMPONENT-623;Lo;0;L;;;;;N;;;;;
+18A6F;TANGUT COMPONENT-624;Lo;0;L;;;;;N;;;;;
+18A70;TANGUT COMPONENT-625;Lo;0;L;;;;;N;;;;;
+18A71;TANGUT COMPONENT-626;Lo;0;L;;;;;N;;;;;
+18A72;TANGUT COMPONENT-627;Lo;0;L;;;;;N;;;;;
+18A73;TANGUT COMPONENT-628;Lo;0;L;;;;;N;;;;;
+18A74;TANGUT COMPONENT-629;Lo;0;L;;;;;N;;;;;
+18A75;TANGUT COMPONENT-630;Lo;0;L;;;;;N;;;;;
+18A76;TANGUT COMPONENT-631;Lo;0;L;;;;;N;;;;;
+18A77;TANGUT COMPONENT-632;Lo;0;L;;;;;N;;;;;
+18A78;TANGUT COMPONENT-633;Lo;0;L;;;;;N;;;;;
+18A79;TANGUT COMPONENT-634;Lo;0;L;;;;;N;;;;;
+18A7A;TANGUT COMPONENT-635;Lo;0;L;;;;;N;;;;;
+18A7B;TANGUT COMPONENT-636;Lo;0;L;;;;;N;;;;;
+18A7C;TANGUT COMPONENT-637;Lo;0;L;;;;;N;;;;;
+18A7D;TANGUT COMPONENT-638;Lo;0;L;;;;;N;;;;;
+18A7E;TANGUT COMPONENT-639;Lo;0;L;;;;;N;;;;;
+18A7F;TANGUT COMPONENT-640;Lo;0;L;;;;;N;;;;;
+18A80;TANGUT COMPONENT-641;Lo;0;L;;;;;N;;;;;
+18A81;TANGUT COMPONENT-642;Lo;0;L;;;;;N;;;;;
+18A82;TANGUT COMPONENT-643;Lo;0;L;;;;;N;;;;;
+18A83;TANGUT COMPONENT-644;Lo;0;L;;;;;N;;;;;
+18A84;TANGUT COMPONENT-645;Lo;0;L;;;;;N;;;;;
+18A85;TANGUT COMPONENT-646;Lo;0;L;;;;;N;;;;;
+18A86;TANGUT COMPONENT-647;Lo;0;L;;;;;N;;;;;
+18A87;TANGUT COMPONENT-648;Lo;0;L;;;;;N;;;;;
+18A88;TANGUT COMPONENT-649;Lo;0;L;;;;;N;;;;;
+18A89;TANGUT COMPONENT-650;Lo;0;L;;;;;N;;;;;
+18A8A;TANGUT COMPONENT-651;Lo;0;L;;;;;N;;;;;
+18A8B;TANGUT COMPONENT-652;Lo;0;L;;;;;N;;;;;
+18A8C;TANGUT COMPONENT-653;Lo;0;L;;;;;N;;;;;
+18A8D;TANGUT COMPONENT-654;Lo;0;L;;;;;N;;;;;
+18A8E;TANGUT COMPONENT-655;Lo;0;L;;;;;N;;;;;
+18A8F;TANGUT COMPONENT-656;Lo;0;L;;;;;N;;;;;
+18A90;TANGUT COMPONENT-657;Lo;0;L;;;;;N;;;;;
+18A91;TANGUT COMPONENT-658;Lo;0;L;;;;;N;;;;;
+18A92;TANGUT COMPONENT-659;Lo;0;L;;;;;N;;;;;
+18A93;TANGUT COMPONENT-660;Lo;0;L;;;;;N;;;;;
+18A94;TANGUT COMPONENT-661;Lo;0;L;;;;;N;;;;;
+18A95;TANGUT COMPONENT-662;Lo;0;L;;;;;N;;;;;
+18A96;TANGUT COMPONENT-663;Lo;0;L;;;;;N;;;;;
+18A97;TANGUT COMPONENT-664;Lo;0;L;;;;;N;;;;;
+18A98;TANGUT COMPONENT-665;Lo;0;L;;;;;N;;;;;
+18A99;TANGUT COMPONENT-666;Lo;0;L;;;;;N;;;;;
+18A9A;TANGUT COMPONENT-667;Lo;0;L;;;;;N;;;;;
+18A9B;TANGUT COMPONENT-668;Lo;0;L;;;;;N;;;;;
+18A9C;TANGUT COMPONENT-669;Lo;0;L;;;;;N;;;;;
+18A9D;TANGUT COMPONENT-670;Lo;0;L;;;;;N;;;;;
+18A9E;TANGUT COMPONENT-671;Lo;0;L;;;;;N;;;;;
+18A9F;TANGUT COMPONENT-672;Lo;0;L;;;;;N;;;;;
+18AA0;TANGUT COMPONENT-673;Lo;0;L;;;;;N;;;;;
+18AA1;TANGUT COMPONENT-674;Lo;0;L;;;;;N;;;;;
+18AA2;TANGUT COMPONENT-675;Lo;0;L;;;;;N;;;;;
+18AA3;TANGUT COMPONENT-676;Lo;0;L;;;;;N;;;;;
+18AA4;TANGUT COMPONENT-677;Lo;0;L;;;;;N;;;;;
+18AA5;TANGUT COMPONENT-678;Lo;0;L;;;;;N;;;;;
+18AA6;TANGUT COMPONENT-679;Lo;0;L;;;;;N;;;;;
+18AA7;TANGUT COMPONENT-680;Lo;0;L;;;;;N;;;;;
+18AA8;TANGUT COMPONENT-681;Lo;0;L;;;;;N;;;;;
+18AA9;TANGUT COMPONENT-682;Lo;0;L;;;;;N;;;;;
+18AAA;TANGUT COMPONENT-683;Lo;0;L;;;;;N;;;;;
+18AAB;TANGUT COMPONENT-684;Lo;0;L;;;;;N;;;;;
+18AAC;TANGUT COMPONENT-685;Lo;0;L;;;;;N;;;;;
+18AAD;TANGUT COMPONENT-686;Lo;0;L;;;;;N;;;;;
+18AAE;TANGUT COMPONENT-687;Lo;0;L;;;;;N;;;;;
+18AAF;TANGUT COMPONENT-688;Lo;0;L;;;;;N;;;;;
+18AB0;TANGUT COMPONENT-689;Lo;0;L;;;;;N;;;;;
+18AB1;TANGUT COMPONENT-690;Lo;0;L;;;;;N;;;;;
+18AB2;TANGUT COMPONENT-691;Lo;0;L;;;;;N;;;;;
+18AB3;TANGUT COMPONENT-692;Lo;0;L;;;;;N;;;;;
+18AB4;TANGUT COMPONENT-693;Lo;0;L;;;;;N;;;;;
+18AB5;TANGUT COMPONENT-694;Lo;0;L;;;;;N;;;;;
+18AB6;TANGUT COMPONENT-695;Lo;0;L;;;;;N;;;;;
+18AB7;TANGUT COMPONENT-696;Lo;0;L;;;;;N;;;;;
+18AB8;TANGUT COMPONENT-697;Lo;0;L;;;;;N;;;;;
+18AB9;TANGUT COMPONENT-698;Lo;0;L;;;;;N;;;;;
+18ABA;TANGUT COMPONENT-699;Lo;0;L;;;;;N;;;;;
+18ABB;TANGUT COMPONENT-700;Lo;0;L;;;;;N;;;;;
+18ABC;TANGUT COMPONENT-701;Lo;0;L;;;;;N;;;;;
+18ABD;TANGUT COMPONENT-702;Lo;0;L;;;;;N;;;;;
+18ABE;TANGUT COMPONENT-703;Lo;0;L;;;;;N;;;;;
+18ABF;TANGUT COMPONENT-704;Lo;0;L;;;;;N;;;;;
+18AC0;TANGUT COMPONENT-705;Lo;0;L;;;;;N;;;;;
+18AC1;TANGUT COMPONENT-706;Lo;0;L;;;;;N;;;;;
+18AC2;TANGUT COMPONENT-707;Lo;0;L;;;;;N;;;;;
+18AC3;TANGUT COMPONENT-708;Lo;0;L;;;;;N;;;;;
+18AC4;TANGUT COMPONENT-709;Lo;0;L;;;;;N;;;;;
+18AC5;TANGUT COMPONENT-710;Lo;0;L;;;;;N;;;;;
+18AC6;TANGUT COMPONENT-711;Lo;0;L;;;;;N;;;;;
+18AC7;TANGUT COMPONENT-712;Lo;0;L;;;;;N;;;;;
+18AC8;TANGUT COMPONENT-713;Lo;0;L;;;;;N;;;;;
+18AC9;TANGUT COMPONENT-714;Lo;0;L;;;;;N;;;;;
+18ACA;TANGUT COMPONENT-715;Lo;0;L;;;;;N;;;;;
+18ACB;TANGUT COMPONENT-716;Lo;0;L;;;;;N;;;;;
+18ACC;TANGUT COMPONENT-717;Lo;0;L;;;;;N;;;;;
+18ACD;TANGUT COMPONENT-718;Lo;0;L;;;;;N;;;;;
+18ACE;TANGUT COMPONENT-719;Lo;0;L;;;;;N;;;;;
+18ACF;TANGUT COMPONENT-720;Lo;0;L;;;;;N;;;;;
+18AD0;TANGUT COMPONENT-721;Lo;0;L;;;;;N;;;;;
+18AD1;TANGUT COMPONENT-722;Lo;0;L;;;;;N;;;;;
+18AD2;TANGUT COMPONENT-723;Lo;0;L;;;;;N;;;;;
+18AD3;TANGUT COMPONENT-724;Lo;0;L;;;;;N;;;;;
+18AD4;TANGUT COMPONENT-725;Lo;0;L;;;;;N;;;;;
+18AD5;TANGUT COMPONENT-726;Lo;0;L;;;;;N;;;;;
+18AD6;TANGUT COMPONENT-727;Lo;0;L;;;;;N;;;;;
+18AD7;TANGUT COMPONENT-728;Lo;0;L;;;;;N;;;;;
+18AD8;TANGUT COMPONENT-729;Lo;0;L;;;;;N;;;;;
+18AD9;TANGUT COMPONENT-730;Lo;0;L;;;;;N;;;;;
+18ADA;TANGUT COMPONENT-731;Lo;0;L;;;;;N;;;;;
+18ADB;TANGUT COMPONENT-732;Lo;0;L;;;;;N;;;;;
+18ADC;TANGUT COMPONENT-733;Lo;0;L;;;;;N;;;;;
+18ADD;TANGUT COMPONENT-734;Lo;0;L;;;;;N;;;;;
+18ADE;TANGUT COMPONENT-735;Lo;0;L;;;;;N;;;;;
+18ADF;TANGUT COMPONENT-736;Lo;0;L;;;;;N;;;;;
+18AE0;TANGUT COMPONENT-737;Lo;0;L;;;;;N;;;;;
+18AE1;TANGUT COMPONENT-738;Lo;0;L;;;;;N;;;;;
+18AE2;TANGUT COMPONENT-739;Lo;0;L;;;;;N;;;;;
+18AE3;TANGUT COMPONENT-740;Lo;0;L;;;;;N;;;;;
+18AE4;TANGUT COMPONENT-741;Lo;0;L;;;;;N;;;;;
+18AE5;TANGUT COMPONENT-742;Lo;0;L;;;;;N;;;;;
+18AE6;TANGUT COMPONENT-743;Lo;0;L;;;;;N;;;;;
+18AE7;TANGUT COMPONENT-744;Lo;0;L;;;;;N;;;;;
+18AE8;TANGUT COMPONENT-745;Lo;0;L;;;;;N;;;;;
+18AE9;TANGUT COMPONENT-746;Lo;0;L;;;;;N;;;;;
+18AEA;TANGUT COMPONENT-747;Lo;0;L;;;;;N;;;;;
+18AEB;TANGUT COMPONENT-748;Lo;0;L;;;;;N;;;;;
+18AEC;TANGUT COMPONENT-749;Lo;0;L;;;;;N;;;;;
+18AED;TANGUT COMPONENT-750;Lo;0;L;;;;;N;;;;;
+18AEE;TANGUT COMPONENT-751;Lo;0;L;;;;;N;;;;;
+18AEF;TANGUT COMPONENT-752;Lo;0;L;;;;;N;;;;;
+18AF0;TANGUT COMPONENT-753;Lo;0;L;;;;;N;;;;;
+18AF1;TANGUT COMPONENT-754;Lo;0;L;;;;;N;;;;;
+18AF2;TANGUT COMPONENT-755;Lo;0;L;;;;;N;;;;;
+1B000;KATAKANA LETTER ARCHAIC E;Lo;0;L;;;;;N;;;;;
+1B001;HIRAGANA LETTER ARCHAIC YE;Lo;0;L;;;;;N;;;;;
+1BC00;DUPLOYAN LETTER H;Lo;0;L;;;;;N;;;;;
+1BC01;DUPLOYAN LETTER X;Lo;0;L;;;;;N;;;;;
+1BC02;DUPLOYAN LETTER P;Lo;0;L;;;;;N;;;;;
+1BC03;DUPLOYAN LETTER T;Lo;0;L;;;;;N;;;;;
+1BC04;DUPLOYAN LETTER F;Lo;0;L;;;;;N;;;;;
+1BC05;DUPLOYAN LETTER K;Lo;0;L;;;;;N;;;;;
+1BC06;DUPLOYAN LETTER L;Lo;0;L;;;;;N;;;;;
+1BC07;DUPLOYAN LETTER B;Lo;0;L;;;;;N;;;;;
+1BC08;DUPLOYAN LETTER D;Lo;0;L;;;;;N;;;;;
+1BC09;DUPLOYAN LETTER V;Lo;0;L;;;;;N;;;;;
+1BC0A;DUPLOYAN LETTER G;Lo;0;L;;;;;N;;;;;
+1BC0B;DUPLOYAN LETTER R;Lo;0;L;;;;;N;;;;;
+1BC0C;DUPLOYAN LETTER P N;Lo;0;L;;;;;N;;;;;
+1BC0D;DUPLOYAN LETTER D S;Lo;0;L;;;;;N;;;;;
+1BC0E;DUPLOYAN LETTER F N;Lo;0;L;;;;;N;;;;;
+1BC0F;DUPLOYAN LETTER K M;Lo;0;L;;;;;N;;;;;
+1BC10;DUPLOYAN LETTER R S;Lo;0;L;;;;;N;;;;;
+1BC11;DUPLOYAN LETTER TH;Lo;0;L;;;;;N;;;;;
+1BC12;DUPLOYAN LETTER SLOAN DH;Lo;0;L;;;;;N;;;;;
+1BC13;DUPLOYAN LETTER DH;Lo;0;L;;;;;N;;;;;
+1BC14;DUPLOYAN LETTER KK;Lo;0;L;;;;;N;;;;;
+1BC15;DUPLOYAN LETTER SLOAN J;Lo;0;L;;;;;N;;;;;
+1BC16;DUPLOYAN LETTER HL;Lo;0;L;;;;;N;;;;;
+1BC17;DUPLOYAN LETTER LH;Lo;0;L;;;;;N;;;;;
+1BC18;DUPLOYAN LETTER RH;Lo;0;L;;;;;N;;;;;
+1BC19;DUPLOYAN LETTER M;Lo;0;L;;;;;N;;;;;
+1BC1A;DUPLOYAN LETTER N;Lo;0;L;;;;;N;;;;;
+1BC1B;DUPLOYAN LETTER J;Lo;0;L;;;;;N;;;;;
+1BC1C;DUPLOYAN LETTER S;Lo;0;L;;;;;N;;;;;
+1BC1D;DUPLOYAN LETTER M N;Lo;0;L;;;;;N;;;;;
+1BC1E;DUPLOYAN LETTER N M;Lo;0;L;;;;;N;;;;;
+1BC1F;DUPLOYAN LETTER J M;Lo;0;L;;;;;N;;;;;
+1BC20;DUPLOYAN LETTER S J;Lo;0;L;;;;;N;;;;;
+1BC21;DUPLOYAN LETTER M WITH DOT;Lo;0;L;;;;;N;;;;;
+1BC22;DUPLOYAN LETTER N WITH DOT;Lo;0;L;;;;;N;;;;;
+1BC23;DUPLOYAN LETTER J WITH DOT;Lo;0;L;;;;;N;;;;;
+1BC24;DUPLOYAN LETTER J WITH DOTS INSIDE AND ABOVE;Lo;0;L;;;;;N;;;;;
+1BC25;DUPLOYAN LETTER S WITH DOT;Lo;0;L;;;;;N;;;;;
+1BC26;DUPLOYAN LETTER S WITH DOT BELOW;Lo;0;L;;;;;N;;;;;
+1BC27;DUPLOYAN LETTER M S;Lo;0;L;;;;;N;;;;;
+1BC28;DUPLOYAN LETTER N S;Lo;0;L;;;;;N;;;;;
+1BC29;DUPLOYAN LETTER J S;Lo;0;L;;;;;N;;;;;
+1BC2A;DUPLOYAN LETTER S S;Lo;0;L;;;;;N;;;;;
+1BC2B;DUPLOYAN LETTER M N S;Lo;0;L;;;;;N;;;;;
+1BC2C;DUPLOYAN LETTER N M S;Lo;0;L;;;;;N;;;;;
+1BC2D;DUPLOYAN LETTER J M S;Lo;0;L;;;;;N;;;;;
+1BC2E;DUPLOYAN LETTER S J S;Lo;0;L;;;;;N;;;;;
+1BC2F;DUPLOYAN LETTER J S WITH DOT;Lo;0;L;;;;;N;;;;;
+1BC30;DUPLOYAN LETTER J N;Lo;0;L;;;;;N;;;;;
+1BC31;DUPLOYAN LETTER J N S;Lo;0;L;;;;;N;;;;;
+1BC32;DUPLOYAN LETTER S T;Lo;0;L;;;;;N;;;;;
+1BC33;DUPLOYAN LETTER S T R;Lo;0;L;;;;;N;;;;;
+1BC34;DUPLOYAN LETTER S P;Lo;0;L;;;;;N;;;;;
+1BC35;DUPLOYAN LETTER S P R;Lo;0;L;;;;;N;;;;;
+1BC36;DUPLOYAN LETTER T S;Lo;0;L;;;;;N;;;;;
+1BC37;DUPLOYAN LETTER T R S;Lo;0;L;;;;;N;;;;;
+1BC38;DUPLOYAN LETTER W;Lo;0;L;;;;;N;;;;;
+1BC39;DUPLOYAN LETTER WH;Lo;0;L;;;;;N;;;;;
+1BC3A;DUPLOYAN LETTER W R;Lo;0;L;;;;;N;;;;;
+1BC3B;DUPLOYAN LETTER S N;Lo;0;L;;;;;N;;;;;
+1BC3C;DUPLOYAN LETTER S M;Lo;0;L;;;;;N;;;;;
+1BC3D;DUPLOYAN LETTER K R S;Lo;0;L;;;;;N;;;;;
+1BC3E;DUPLOYAN LETTER G R S;Lo;0;L;;;;;N;;;;;
+1BC3F;DUPLOYAN LETTER S K;Lo;0;L;;;;;N;;;;;
+1BC40;DUPLOYAN LETTER S K R;Lo;0;L;;;;;N;;;;;
+1BC41;DUPLOYAN LETTER A;Lo;0;L;;;;;N;;;;;
+1BC42;DUPLOYAN LETTER SLOAN OW;Lo;0;L;;;;;N;;;;;
+1BC43;DUPLOYAN LETTER OA;Lo;0;L;;;;;N;;;;;
+1BC44;DUPLOYAN LETTER O;Lo;0;L;;;;;N;;;;;
+1BC45;DUPLOYAN LETTER AOU;Lo;0;L;;;;;N;;;;;
+1BC46;DUPLOYAN LETTER I;Lo;0;L;;;;;N;;;;;
+1BC47;DUPLOYAN LETTER E;Lo;0;L;;;;;N;;;;;
+1BC48;DUPLOYAN LETTER IE;Lo;0;L;;;;;N;;;;;
+1BC49;DUPLOYAN LETTER SHORT I;Lo;0;L;;;;;N;;;;;
+1BC4A;DUPLOYAN LETTER UI;Lo;0;L;;;;;N;;;;;
+1BC4B;DUPLOYAN LETTER EE;Lo;0;L;;;;;N;;;;;
+1BC4C;DUPLOYAN LETTER SLOAN EH;Lo;0;L;;;;;N;;;;;
+1BC4D;DUPLOYAN LETTER ROMANIAN I;Lo;0;L;;;;;N;;;;;
+1BC4E;DUPLOYAN LETTER SLOAN EE;Lo;0;L;;;;;N;;;;;
+1BC4F;DUPLOYAN LETTER LONG I;Lo;0;L;;;;;N;;;;;
+1BC50;DUPLOYAN LETTER YE;Lo;0;L;;;;;N;;;;;
+1BC51;DUPLOYAN LETTER U;Lo;0;L;;;;;N;;;;;
+1BC52;DUPLOYAN LETTER EU;Lo;0;L;;;;;N;;;;;
+1BC53;DUPLOYAN LETTER XW;Lo;0;L;;;;;N;;;;;
+1BC54;DUPLOYAN LETTER U N;Lo;0;L;;;;;N;;;;;
+1BC55;DUPLOYAN LETTER LONG U;Lo;0;L;;;;;N;;;;;
+1BC56;DUPLOYAN LETTER ROMANIAN U;Lo;0;L;;;;;N;;;;;
+1BC57;DUPLOYAN LETTER UH;Lo;0;L;;;;;N;;;;;
+1BC58;DUPLOYAN LETTER SLOAN U;Lo;0;L;;;;;N;;;;;
+1BC59;DUPLOYAN LETTER OOH;Lo;0;L;;;;;N;;;;;
+1BC5A;DUPLOYAN LETTER OW;Lo;0;L;;;;;N;;;;;
+1BC5B;DUPLOYAN LETTER OU;Lo;0;L;;;;;N;;;;;
+1BC5C;DUPLOYAN LETTER WA;Lo;0;L;;;;;N;;;;;
+1BC5D;DUPLOYAN LETTER WO;Lo;0;L;;;;;N;;;;;
+1BC5E;DUPLOYAN LETTER WI;Lo;0;L;;;;;N;;;;;
+1BC5F;DUPLOYAN LETTER WEI;Lo;0;L;;;;;N;;;;;
+1BC60;DUPLOYAN LETTER WOW;Lo;0;L;;;;;N;;;;;
+1BC61;DUPLOYAN LETTER NASAL U;Lo;0;L;;;;;N;;;;;
+1BC62;DUPLOYAN LETTER NASAL O;Lo;0;L;;;;;N;;;;;
+1BC63;DUPLOYAN LETTER NASAL I;Lo;0;L;;;;;N;;;;;
+1BC64;DUPLOYAN LETTER NASAL A;Lo;0;L;;;;;N;;;;;
+1BC65;DUPLOYAN LETTER PERNIN AN;Lo;0;L;;;;;N;;;;;
+1BC66;DUPLOYAN LETTER PERNIN AM;Lo;0;L;;;;;N;;;;;
+1BC67;DUPLOYAN LETTER SLOAN EN;Lo;0;L;;;;;N;;;;;
+1BC68;DUPLOYAN LETTER SLOAN AN;Lo;0;L;;;;;N;;;;;
+1BC69;DUPLOYAN LETTER SLOAN ON;Lo;0;L;;;;;N;;;;;
+1BC6A;DUPLOYAN LETTER VOCALIC M;Lo;0;L;;;;;N;;;;;
+1BC70;DUPLOYAN AFFIX LEFT HORIZONTAL SECANT;Lo;0;L;;;;;N;;;;;
+1BC71;DUPLOYAN AFFIX MID HORIZONTAL SECANT;Lo;0;L;;;;;N;;;;;
+1BC72;DUPLOYAN AFFIX RIGHT HORIZONTAL SECANT;Lo;0;L;;;;;N;;;;;
+1BC73;DUPLOYAN AFFIX LOW VERTICAL SECANT;Lo;0;L;;;;;N;;;;;
+1BC74;DUPLOYAN AFFIX MID VERTICAL SECANT;Lo;0;L;;;;;N;;;;;
+1BC75;DUPLOYAN AFFIX HIGH VERTICAL SECANT;Lo;0;L;;;;;N;;;;;
+1BC76;DUPLOYAN AFFIX ATTACHED SECANT;Lo;0;L;;;;;N;;;;;
+1BC77;DUPLOYAN AFFIX ATTACHED LEFT-TO-RIGHT SECANT;Lo;0;L;;;;;N;;;;;
+1BC78;DUPLOYAN AFFIX ATTACHED TANGENT;Lo;0;L;;;;;N;;;;;
+1BC79;DUPLOYAN AFFIX ATTACHED TAIL;Lo;0;L;;;;;N;;;;;
+1BC7A;DUPLOYAN AFFIX ATTACHED E HOOK;Lo;0;L;;;;;N;;;;;
+1BC7B;DUPLOYAN AFFIX ATTACHED I HOOK;Lo;0;L;;;;;N;;;;;
+1BC7C;DUPLOYAN AFFIX ATTACHED TANGENT HOOK;Lo;0;L;;;;;N;;;;;
+1BC80;DUPLOYAN AFFIX HIGH ACUTE;Lo;0;L;;;;;N;;;;;
+1BC81;DUPLOYAN AFFIX HIGH TIGHT ACUTE;Lo;0;L;;;;;N;;;;;
+1BC82;DUPLOYAN AFFIX HIGH GRAVE;Lo;0;L;;;;;N;;;;;
+1BC83;DUPLOYAN AFFIX HIGH LONG GRAVE;Lo;0;L;;;;;N;;;;;
+1BC84;DUPLOYAN AFFIX HIGH DOT;Lo;0;L;;;;;N;;;;;
+1BC85;DUPLOYAN AFFIX HIGH CIRCLE;Lo;0;L;;;;;N;;;;;
+1BC86;DUPLOYAN AFFIX HIGH LINE;Lo;0;L;;;;;N;;;;;
+1BC87;DUPLOYAN AFFIX HIGH WAVE;Lo;0;L;;;;;N;;;;;
+1BC88;DUPLOYAN AFFIX HIGH VERTICAL;Lo;0;L;;;;;N;;;;;
+1BC90;DUPLOYAN AFFIX LOW ACUTE;Lo;0;L;;;;;N;;;;;
+1BC91;DUPLOYAN AFFIX LOW TIGHT ACUTE;Lo;0;L;;;;;N;;;;;
+1BC92;DUPLOYAN AFFIX LOW GRAVE;Lo;0;L;;;;;N;;;;;
+1BC93;DUPLOYAN AFFIX LOW LONG GRAVE;Lo;0;L;;;;;N;;;;;
+1BC94;DUPLOYAN AFFIX LOW DOT;Lo;0;L;;;;;N;;;;;
+1BC95;DUPLOYAN AFFIX LOW CIRCLE;Lo;0;L;;;;;N;;;;;
+1BC96;DUPLOYAN AFFIX LOW LINE;Lo;0;L;;;;;N;;;;;
+1BC97;DUPLOYAN AFFIX LOW WAVE;Lo;0;L;;;;;N;;;;;
+1BC98;DUPLOYAN AFFIX LOW VERTICAL;Lo;0;L;;;;;N;;;;;
+1BC99;DUPLOYAN AFFIX LOW ARROW;Lo;0;L;;;;;N;;;;;
+1BC9C;DUPLOYAN SIGN O WITH CROSS;So;0;L;;;;;N;;;;;
+1BC9D;DUPLOYAN THICK LETTER SELECTOR;Mn;0;NSM;;;;;N;;;;;
+1BC9E;DUPLOYAN DOUBLE MARK;Mn;1;NSM;;;;;N;;;;;
+1BC9F;DUPLOYAN PUNCTUATION CHINOOK FULL STOP;Po;0;L;;;;;N;;;;;
+1BCA0;SHORTHAND FORMAT LETTER OVERLAP;Cf;0;BN;;;;;N;;;;;
+1BCA1;SHORTHAND FORMAT CONTINUING OVERLAP;Cf;0;BN;;;;;N;;;;;
+1BCA2;SHORTHAND FORMAT DOWN STEP;Cf;0;BN;;;;;N;;;;;
+1BCA3;SHORTHAND FORMAT UP STEP;Cf;0;BN;;;;;N;;;;;
+1D000;BYZANTINE MUSICAL SYMBOL PSILI;So;0;L;;;;;N;;;;;
+1D001;BYZANTINE MUSICAL SYMBOL DASEIA;So;0;L;;;;;N;;;;;
+1D002;BYZANTINE MUSICAL SYMBOL PERISPOMENI;So;0;L;;;;;N;;;;;
+1D003;BYZANTINE MUSICAL SYMBOL OXEIA EKFONITIKON;So;0;L;;;;;N;;;;;
+1D004;BYZANTINE MUSICAL SYMBOL OXEIA DIPLI;So;0;L;;;;;N;;;;;
+1D005;BYZANTINE MUSICAL SYMBOL VAREIA EKFONITIKON;So;0;L;;;;;N;;;;;
+1D006;BYZANTINE MUSICAL SYMBOL VAREIA DIPLI;So;0;L;;;;;N;;;;;
+1D007;BYZANTINE MUSICAL SYMBOL KATHISTI;So;0;L;;;;;N;;;;;
+1D008;BYZANTINE MUSICAL SYMBOL SYRMATIKI;So;0;L;;;;;N;;;;;
+1D009;BYZANTINE MUSICAL SYMBOL PARAKLITIKI;So;0;L;;;;;N;;;;;
+1D00A;BYZANTINE MUSICAL SYMBOL YPOKRISIS;So;0;L;;;;;N;;;;;
+1D00B;BYZANTINE MUSICAL SYMBOL YPOKRISIS DIPLI;So;0;L;;;;;N;;;;;
+1D00C;BYZANTINE MUSICAL SYMBOL KREMASTI;So;0;L;;;;;N;;;;;
+1D00D;BYZANTINE MUSICAL SYMBOL APESO EKFONITIKON;So;0;L;;;;;N;;;;;
+1D00E;BYZANTINE MUSICAL SYMBOL EXO EKFONITIKON;So;0;L;;;;;N;;;;;
+1D00F;BYZANTINE MUSICAL SYMBOL TELEIA;So;0;L;;;;;N;;;;;
+1D010;BYZANTINE MUSICAL SYMBOL KENTIMATA;So;0;L;;;;;N;;;;;
+1D011;BYZANTINE MUSICAL SYMBOL APOSTROFOS;So;0;L;;;;;N;;;;;
+1D012;BYZANTINE MUSICAL SYMBOL APOSTROFOS DIPLI;So;0;L;;;;;N;;;;;
+1D013;BYZANTINE MUSICAL SYMBOL SYNEVMA;So;0;L;;;;;N;;;;;
+1D014;BYZANTINE MUSICAL SYMBOL THITA;So;0;L;;;;;N;;;;;
+1D015;BYZANTINE MUSICAL SYMBOL OLIGON ARCHAION;So;0;L;;;;;N;;;;;
+1D016;BYZANTINE MUSICAL SYMBOL GORGON ARCHAION;So;0;L;;;;;N;;;;;
+1D017;BYZANTINE MUSICAL SYMBOL PSILON;So;0;L;;;;;N;;;;;
+1D018;BYZANTINE MUSICAL SYMBOL CHAMILON;So;0;L;;;;;N;;;;;
+1D019;BYZANTINE MUSICAL SYMBOL VATHY;So;0;L;;;;;N;;;;;
+1D01A;BYZANTINE MUSICAL SYMBOL ISON ARCHAION;So;0;L;;;;;N;;;;;
+1D01B;BYZANTINE MUSICAL SYMBOL KENTIMA ARCHAION;So;0;L;;;;;N;;;;;
+1D01C;BYZANTINE MUSICAL SYMBOL KENTIMATA ARCHAION;So;0;L;;;;;N;;;;;
+1D01D;BYZANTINE MUSICAL SYMBOL SAXIMATA;So;0;L;;;;;N;;;;;
+1D01E;BYZANTINE MUSICAL SYMBOL PARICHON;So;0;L;;;;;N;;;;;
+1D01F;BYZANTINE MUSICAL SYMBOL STAVROS APODEXIA;So;0;L;;;;;N;;;;;
+1D020;BYZANTINE MUSICAL SYMBOL OXEIAI ARCHAION;So;0;L;;;;;N;;;;;
+1D021;BYZANTINE MUSICAL SYMBOL VAREIAI ARCHAION;So;0;L;;;;;N;;;;;
+1D022;BYZANTINE MUSICAL SYMBOL APODERMA ARCHAION;So;0;L;;;;;N;;;;;
+1D023;BYZANTINE MUSICAL SYMBOL APOTHEMA;So;0;L;;;;;N;;;;;
+1D024;BYZANTINE MUSICAL SYMBOL KLASMA;So;0;L;;;;;N;;;;;
+1D025;BYZANTINE MUSICAL SYMBOL REVMA;So;0;L;;;;;N;;;;;
+1D026;BYZANTINE MUSICAL SYMBOL PIASMA ARCHAION;So;0;L;;;;;N;;;;;
+1D027;BYZANTINE MUSICAL SYMBOL TINAGMA;So;0;L;;;;;N;;;;;
+1D028;BYZANTINE MUSICAL SYMBOL ANATRICHISMA;So;0;L;;;;;N;;;;;
+1D029;BYZANTINE MUSICAL SYMBOL SEISMA;So;0;L;;;;;N;;;;;
+1D02A;BYZANTINE MUSICAL SYMBOL SYNAGMA ARCHAION;So;0;L;;;;;N;;;;;
+1D02B;BYZANTINE MUSICAL SYMBOL SYNAGMA META STAVROU;So;0;L;;;;;N;;;;;
+1D02C;BYZANTINE MUSICAL SYMBOL OYRANISMA ARCHAION;So;0;L;;;;;N;;;;;
+1D02D;BYZANTINE MUSICAL SYMBOL THEMA;So;0;L;;;;;N;;;;;
+1D02E;BYZANTINE MUSICAL SYMBOL LEMOI;So;0;L;;;;;N;;;;;
+1D02F;BYZANTINE MUSICAL SYMBOL DYO;So;0;L;;;;;N;;;;;
+1D030;BYZANTINE MUSICAL SYMBOL TRIA;So;0;L;;;;;N;;;;;
+1D031;BYZANTINE MUSICAL SYMBOL TESSERA;So;0;L;;;;;N;;;;;
+1D032;BYZANTINE MUSICAL SYMBOL KRATIMATA;So;0;L;;;;;N;;;;;
+1D033;BYZANTINE MUSICAL SYMBOL APESO EXO NEO;So;0;L;;;;;N;;;;;
+1D034;BYZANTINE MUSICAL SYMBOL FTHORA ARCHAION;So;0;L;;;;;N;;;;;
+1D035;BYZANTINE MUSICAL SYMBOL IMIFTHORA;So;0;L;;;;;N;;;;;
+1D036;BYZANTINE MUSICAL SYMBOL TROMIKON ARCHAION;So;0;L;;;;;N;;;;;
+1D037;BYZANTINE MUSICAL SYMBOL KATAVA TROMIKON;So;0;L;;;;;N;;;;;
+1D038;BYZANTINE MUSICAL SYMBOL PELASTON;So;0;L;;;;;N;;;;;
+1D039;BYZANTINE MUSICAL SYMBOL PSIFISTON;So;0;L;;;;;N;;;;;
+1D03A;BYZANTINE MUSICAL SYMBOL KONTEVMA;So;0;L;;;;;N;;;;;
+1D03B;BYZANTINE MUSICAL SYMBOL CHOREVMA ARCHAION;So;0;L;;;;;N;;;;;
+1D03C;BYZANTINE MUSICAL SYMBOL RAPISMA;So;0;L;;;;;N;;;;;
+1D03D;BYZANTINE MUSICAL SYMBOL PARAKALESMA ARCHAION;So;0;L;;;;;N;;;;;
+1D03E;BYZANTINE MUSICAL SYMBOL PARAKLITIKI ARCHAION;So;0;L;;;;;N;;;;;
+1D03F;BYZANTINE MUSICAL SYMBOL ICHADIN;So;0;L;;;;;N;;;;;
+1D040;BYZANTINE MUSICAL SYMBOL NANA;So;0;L;;;;;N;;;;;
+1D041;BYZANTINE MUSICAL SYMBOL PETASMA;So;0;L;;;;;N;;;;;
+1D042;BYZANTINE MUSICAL SYMBOL KONTEVMA ALLO;So;0;L;;;;;N;;;;;
+1D043;BYZANTINE MUSICAL SYMBOL TROMIKON ALLO;So;0;L;;;;;N;;;;;
+1D044;BYZANTINE MUSICAL SYMBOL STRAGGISMATA;So;0;L;;;;;N;;;;;
+1D045;BYZANTINE MUSICAL SYMBOL GRONTHISMATA;So;0;L;;;;;N;;;;;
+1D046;BYZANTINE MUSICAL SYMBOL ISON NEO;So;0;L;;;;;N;;;;;
+1D047;BYZANTINE MUSICAL SYMBOL OLIGON NEO;So;0;L;;;;;N;;;;;
+1D048;BYZANTINE MUSICAL SYMBOL OXEIA NEO;So;0;L;;;;;N;;;;;
+1D049;BYZANTINE MUSICAL SYMBOL PETASTI;So;0;L;;;;;N;;;;;
+1D04A;BYZANTINE MUSICAL SYMBOL KOUFISMA;So;0;L;;;;;N;;;;;
+1D04B;BYZANTINE MUSICAL SYMBOL PETASTOKOUFISMA;So;0;L;;;;;N;;;;;
+1D04C;BYZANTINE MUSICAL SYMBOL KRATIMOKOUFISMA;So;0;L;;;;;N;;;;;
+1D04D;BYZANTINE MUSICAL SYMBOL PELASTON NEO;So;0;L;;;;;N;;;;;
+1D04E;BYZANTINE MUSICAL SYMBOL KENTIMATA NEO ANO;So;0;L;;;;;N;;;;;
+1D04F;BYZANTINE MUSICAL SYMBOL KENTIMA NEO ANO;So;0;L;;;;;N;;;;;
+1D050;BYZANTINE MUSICAL SYMBOL YPSILI;So;0;L;;;;;N;;;;;
+1D051;BYZANTINE MUSICAL SYMBOL APOSTROFOS NEO;So;0;L;;;;;N;;;;;
+1D052;BYZANTINE MUSICAL SYMBOL APOSTROFOI SYNDESMOS NEO;So;0;L;;;;;N;;;;;
+1D053;BYZANTINE MUSICAL SYMBOL YPORROI;So;0;L;;;;;N;;;;;
+1D054;BYZANTINE MUSICAL SYMBOL KRATIMOYPORROON;So;0;L;;;;;N;;;;;
+1D055;BYZANTINE MUSICAL SYMBOL ELAFRON;So;0;L;;;;;N;;;;;
+1D056;BYZANTINE MUSICAL SYMBOL CHAMILI;So;0;L;;;;;N;;;;;
+1D057;BYZANTINE MUSICAL SYMBOL MIKRON ISON;So;0;L;;;;;N;;;;;
+1D058;BYZANTINE MUSICAL SYMBOL VAREIA NEO;So;0;L;;;;;N;;;;;
+1D059;BYZANTINE MUSICAL SYMBOL PIASMA NEO;So;0;L;;;;;N;;;;;
+1D05A;BYZANTINE MUSICAL SYMBOL PSIFISTON NEO;So;0;L;;;;;N;;;;;
+1D05B;BYZANTINE MUSICAL SYMBOL OMALON;So;0;L;;;;;N;;;;;
+1D05C;BYZANTINE MUSICAL SYMBOL ANTIKENOMA;So;0;L;;;;;N;;;;;
+1D05D;BYZANTINE MUSICAL SYMBOL LYGISMA;So;0;L;;;;;N;;;;;
+1D05E;BYZANTINE MUSICAL SYMBOL PARAKLITIKI NEO;So;0;L;;;;;N;;;;;
+1D05F;BYZANTINE MUSICAL SYMBOL PARAKALESMA NEO;So;0;L;;;;;N;;;;;
+1D060;BYZANTINE MUSICAL SYMBOL ETERON PARAKALESMA;So;0;L;;;;;N;;;;;
+1D061;BYZANTINE MUSICAL SYMBOL KYLISMA;So;0;L;;;;;N;;;;;
+1D062;BYZANTINE MUSICAL SYMBOL ANTIKENOKYLISMA;So;0;L;;;;;N;;;;;
+1D063;BYZANTINE MUSICAL SYMBOL TROMIKON NEO;So;0;L;;;;;N;;;;;
+1D064;BYZANTINE MUSICAL SYMBOL EKSTREPTON;So;0;L;;;;;N;;;;;
+1D065;BYZANTINE MUSICAL SYMBOL SYNAGMA NEO;So;0;L;;;;;N;;;;;
+1D066;BYZANTINE MUSICAL SYMBOL SYRMA;So;0;L;;;;;N;;;;;
+1D067;BYZANTINE MUSICAL SYMBOL CHOREVMA NEO;So;0;L;;;;;N;;;;;
+1D068;BYZANTINE MUSICAL SYMBOL EPEGERMA;So;0;L;;;;;N;;;;;
+1D069;BYZANTINE MUSICAL SYMBOL SEISMA NEO;So;0;L;;;;;N;;;;;
+1D06A;BYZANTINE MUSICAL SYMBOL XIRON KLASMA;So;0;L;;;;;N;;;;;
+1D06B;BYZANTINE MUSICAL SYMBOL TROMIKOPSIFISTON;So;0;L;;;;;N;;;;;
+1D06C;BYZANTINE MUSICAL SYMBOL PSIFISTOLYGISMA;So;0;L;;;;;N;;;;;
+1D06D;BYZANTINE MUSICAL SYMBOL TROMIKOLYGISMA;So;0;L;;;;;N;;;;;
+1D06E;BYZANTINE MUSICAL SYMBOL TROMIKOPARAKALESMA;So;0;L;;;;;N;;;;;
+1D06F;BYZANTINE MUSICAL SYMBOL PSIFISTOPARAKALESMA;So;0;L;;;;;N;;;;;
+1D070;BYZANTINE MUSICAL SYMBOL TROMIKOSYNAGMA;So;0;L;;;;;N;;;;;
+1D071;BYZANTINE MUSICAL SYMBOL PSIFISTOSYNAGMA;So;0;L;;;;;N;;;;;
+1D072;BYZANTINE MUSICAL SYMBOL GORGOSYNTHETON;So;0;L;;;;;N;;;;;
+1D073;BYZANTINE MUSICAL SYMBOL ARGOSYNTHETON;So;0;L;;;;;N;;;;;
+1D074;BYZANTINE MUSICAL SYMBOL ETERON ARGOSYNTHETON;So;0;L;;;;;N;;;;;
+1D075;BYZANTINE MUSICAL SYMBOL OYRANISMA NEO;So;0;L;;;;;N;;;;;
+1D076;BYZANTINE MUSICAL SYMBOL THEMATISMOS ESO;So;0;L;;;;;N;;;;;
+1D077;BYZANTINE MUSICAL SYMBOL THEMATISMOS EXO;So;0;L;;;;;N;;;;;
+1D078;BYZANTINE MUSICAL SYMBOL THEMA APLOUN;So;0;L;;;;;N;;;;;
+1D079;BYZANTINE MUSICAL SYMBOL THES KAI APOTHES;So;0;L;;;;;N;;;;;
+1D07A;BYZANTINE MUSICAL SYMBOL KATAVASMA;So;0;L;;;;;N;;;;;
+1D07B;BYZANTINE MUSICAL SYMBOL ENDOFONON;So;0;L;;;;;N;;;;;
+1D07C;BYZANTINE MUSICAL SYMBOL YFEN KATO;So;0;L;;;;;N;;;;;
+1D07D;BYZANTINE MUSICAL SYMBOL YFEN ANO;So;0;L;;;;;N;;;;;
+1D07E;BYZANTINE MUSICAL SYMBOL STAVROS;So;0;L;;;;;N;;;;;
+1D07F;BYZANTINE MUSICAL SYMBOL KLASMA ANO;So;0;L;;;;;N;;;;;
+1D080;BYZANTINE MUSICAL SYMBOL DIPLI ARCHAION;So;0;L;;;;;N;;;;;
+1D081;BYZANTINE MUSICAL SYMBOL KRATIMA ARCHAION;So;0;L;;;;;N;;;;;
+1D082;BYZANTINE MUSICAL SYMBOL KRATIMA ALLO;So;0;L;;;;;N;;;;;
+1D083;BYZANTINE MUSICAL SYMBOL KRATIMA NEO;So;0;L;;;;;N;;;;;
+1D084;BYZANTINE MUSICAL SYMBOL APODERMA NEO;So;0;L;;;;;N;;;;;
+1D085;BYZANTINE MUSICAL SYMBOL APLI;So;0;L;;;;;N;;;;;
+1D086;BYZANTINE MUSICAL SYMBOL DIPLI;So;0;L;;;;;N;;;;;
+1D087;BYZANTINE MUSICAL SYMBOL TRIPLI;So;0;L;;;;;N;;;;;
+1D088;BYZANTINE MUSICAL SYMBOL TETRAPLI;So;0;L;;;;;N;;;;;
+1D089;BYZANTINE MUSICAL SYMBOL KORONIS;So;0;L;;;;;N;;;;;
+1D08A;BYZANTINE MUSICAL SYMBOL LEIMMA ENOS CHRONOU;So;0;L;;;;;N;;;;;
+1D08B;BYZANTINE MUSICAL SYMBOL LEIMMA DYO CHRONON;So;0;L;;;;;N;;;;;
+1D08C;BYZANTINE MUSICAL SYMBOL LEIMMA TRION CHRONON;So;0;L;;;;;N;;;;;
+1D08D;BYZANTINE MUSICAL SYMBOL LEIMMA TESSARON CHRONON;So;0;L;;;;;N;;;;;
+1D08E;BYZANTINE MUSICAL SYMBOL LEIMMA IMISEOS CHRONOU;So;0;L;;;;;N;;;;;
+1D08F;BYZANTINE MUSICAL SYMBOL GORGON NEO ANO;So;0;L;;;;;N;;;;;
+1D090;BYZANTINE MUSICAL SYMBOL GORGON PARESTIGMENON ARISTERA;So;0;L;;;;;N;;;;;
+1D091;BYZANTINE MUSICAL SYMBOL GORGON PARESTIGMENON DEXIA;So;0;L;;;;;N;;;;;
+1D092;BYZANTINE MUSICAL SYMBOL DIGORGON;So;0;L;;;;;N;;;;;
+1D093;BYZANTINE MUSICAL SYMBOL DIGORGON PARESTIGMENON ARISTERA KATO;So;0;L;;;;;N;;;;;
+1D094;BYZANTINE MUSICAL SYMBOL DIGORGON PARESTIGMENON ARISTERA ANO;So;0;L;;;;;N;;;;;
+1D095;BYZANTINE MUSICAL SYMBOL DIGORGON PARESTIGMENON DEXIA;So;0;L;;;;;N;;;;;
+1D096;BYZANTINE MUSICAL SYMBOL TRIGORGON;So;0;L;;;;;N;;;;;
+1D097;BYZANTINE MUSICAL SYMBOL ARGON;So;0;L;;;;;N;;;;;
+1D098;BYZANTINE MUSICAL SYMBOL IMIDIARGON;So;0;L;;;;;N;;;;;
+1D099;BYZANTINE MUSICAL SYMBOL DIARGON;So;0;L;;;;;N;;;;;
+1D09A;BYZANTINE MUSICAL SYMBOL AGOGI POLI ARGI;So;0;L;;;;;N;;;;;
+1D09B;BYZANTINE MUSICAL SYMBOL AGOGI ARGOTERI;So;0;L;;;;;N;;;;;
+1D09C;BYZANTINE MUSICAL SYMBOL AGOGI ARGI;So;0;L;;;;;N;;;;;
+1D09D;BYZANTINE MUSICAL SYMBOL AGOGI METRIA;So;0;L;;;;;N;;;;;
+1D09E;BYZANTINE MUSICAL SYMBOL AGOGI MESI;So;0;L;;;;;N;;;;;
+1D09F;BYZANTINE MUSICAL SYMBOL AGOGI GORGI;So;0;L;;;;;N;;;;;
+1D0A0;BYZANTINE MUSICAL SYMBOL AGOGI GORGOTERI;So;0;L;;;;;N;;;;;
+1D0A1;BYZANTINE MUSICAL SYMBOL AGOGI POLI GORGI;So;0;L;;;;;N;;;;;
+1D0A2;BYZANTINE MUSICAL SYMBOL MARTYRIA PROTOS ICHOS;So;0;L;;;;;N;;;;;
+1D0A3;BYZANTINE MUSICAL SYMBOL MARTYRIA ALLI PROTOS ICHOS;So;0;L;;;;;N;;;;;
+1D0A4;BYZANTINE MUSICAL SYMBOL MARTYRIA DEYTEROS ICHOS;So;0;L;;;;;N;;;;;
+1D0A5;BYZANTINE MUSICAL SYMBOL MARTYRIA ALLI DEYTEROS ICHOS;So;0;L;;;;;N;;;;;
+1D0A6;BYZANTINE MUSICAL SYMBOL MARTYRIA TRITOS ICHOS;So;0;L;;;;;N;;;;;
+1D0A7;BYZANTINE MUSICAL SYMBOL MARTYRIA TRIFONIAS;So;0;L;;;;;N;;;;;
+1D0A8;BYZANTINE MUSICAL SYMBOL MARTYRIA TETARTOS ICHOS;So;0;L;;;;;N;;;;;
+1D0A9;BYZANTINE MUSICAL SYMBOL MARTYRIA TETARTOS LEGETOS ICHOS;So;0;L;;;;;N;;;;;
+1D0AA;BYZANTINE MUSICAL SYMBOL MARTYRIA LEGETOS ICHOS;So;0;L;;;;;N;;;;;
+1D0AB;BYZANTINE MUSICAL SYMBOL MARTYRIA PLAGIOS ICHOS;So;0;L;;;;;N;;;;;
+1D0AC;BYZANTINE MUSICAL SYMBOL ISAKIA TELOUS ICHIMATOS;So;0;L;;;;;N;;;;;
+1D0AD;BYZANTINE MUSICAL SYMBOL APOSTROFOI TELOUS ICHIMATOS;So;0;L;;;;;N;;;;;
+1D0AE;BYZANTINE MUSICAL SYMBOL FANEROSIS TETRAFONIAS;So;0;L;;;;;N;;;;;
+1D0AF;BYZANTINE MUSICAL SYMBOL FANEROSIS MONOFONIAS;So;0;L;;;;;N;;;;;
+1D0B0;BYZANTINE MUSICAL SYMBOL FANEROSIS DIFONIAS;So;0;L;;;;;N;;;;;
+1D0B1;BYZANTINE MUSICAL SYMBOL MARTYRIA VARYS ICHOS;So;0;L;;;;;N;;;;;
+1D0B2;BYZANTINE MUSICAL SYMBOL MARTYRIA PROTOVARYS ICHOS;So;0;L;;;;;N;;;;;
+1D0B3;BYZANTINE MUSICAL SYMBOL MARTYRIA PLAGIOS TETARTOS ICHOS;So;0;L;;;;;N;;;;;
+1D0B4;BYZANTINE MUSICAL SYMBOL GORTHMIKON N APLOUN;So;0;L;;;;;N;;;;;
+1D0B5;BYZANTINE MUSICAL SYMBOL GORTHMIKON N DIPLOUN;So;0;L;;;;;N;;;;;
+1D0B6;BYZANTINE MUSICAL SYMBOL ENARXIS KAI FTHORA VOU;So;0;L;;;;;N;;;;;
+1D0B7;BYZANTINE MUSICAL SYMBOL IMIFONON;So;0;L;;;;;N;;;;;
+1D0B8;BYZANTINE MUSICAL SYMBOL IMIFTHORON;So;0;L;;;;;N;;;;;
+1D0B9;BYZANTINE MUSICAL SYMBOL FTHORA ARCHAION DEYTEROU ICHOU;So;0;L;;;;;N;;;;;
+1D0BA;BYZANTINE MUSICAL SYMBOL FTHORA DIATONIKI PA;So;0;L;;;;;N;;;;;
+1D0BB;BYZANTINE MUSICAL SYMBOL FTHORA DIATONIKI NANA;So;0;L;;;;;N;;;;;
+1D0BC;BYZANTINE MUSICAL SYMBOL FTHORA NAOS ICHOS;So;0;L;;;;;N;;;;;
+1D0BD;BYZANTINE MUSICAL SYMBOL FTHORA DIATONIKI DI;So;0;L;;;;;N;;;;;
+1D0BE;BYZANTINE MUSICAL SYMBOL FTHORA SKLIRON DIATONON DI;So;0;L;;;;;N;;;;;
+1D0BF;BYZANTINE MUSICAL SYMBOL FTHORA DIATONIKI KE;So;0;L;;;;;N;;;;;
+1D0C0;BYZANTINE MUSICAL SYMBOL FTHORA DIATONIKI ZO;So;0;L;;;;;N;;;;;
+1D0C1;BYZANTINE MUSICAL SYMBOL FTHORA DIATONIKI NI KATO;So;0;L;;;;;N;;;;;
+1D0C2;BYZANTINE MUSICAL SYMBOL FTHORA DIATONIKI NI ANO;So;0;L;;;;;N;;;;;
+1D0C3;BYZANTINE MUSICAL SYMBOL FTHORA MALAKON CHROMA DIFONIAS;So;0;L;;;;;N;;;;;
+1D0C4;BYZANTINE MUSICAL SYMBOL FTHORA MALAKON CHROMA MONOFONIAS;So;0;L;;;;;N;;;;;
+1D0C5;BYZANTINE MUSICAL SYMBOL FHTORA SKLIRON CHROMA VASIS;So;0;L;;;;;N;;;;;
+1D0C6;BYZANTINE MUSICAL SYMBOL FTHORA SKLIRON CHROMA SYNAFI;So;0;L;;;;;N;;;;;
+1D0C7;BYZANTINE MUSICAL SYMBOL FTHORA NENANO;So;0;L;;;;;N;;;;;
+1D0C8;BYZANTINE MUSICAL SYMBOL CHROA ZYGOS;So;0;L;;;;;N;;;;;
+1D0C9;BYZANTINE MUSICAL SYMBOL CHROA KLITON;So;0;L;;;;;N;;;;;
+1D0CA;BYZANTINE MUSICAL SYMBOL CHROA SPATHI;So;0;L;;;;;N;;;;;
+1D0CB;BYZANTINE MUSICAL SYMBOL FTHORA I YFESIS TETARTIMORION;So;0;L;;;;;N;;;;;
+1D0CC;BYZANTINE MUSICAL SYMBOL FTHORA ENARMONIOS ANTIFONIA;So;0;L;;;;;N;;;;;
+1D0CD;BYZANTINE MUSICAL SYMBOL YFESIS TRITIMORION;So;0;L;;;;;N;;;;;
+1D0CE;BYZANTINE MUSICAL SYMBOL DIESIS TRITIMORION;So;0;L;;;;;N;;;;;
+1D0CF;BYZANTINE MUSICAL SYMBOL DIESIS TETARTIMORION;So;0;L;;;;;N;;;;;
+1D0D0;BYZANTINE MUSICAL SYMBOL DIESIS APLI DYO DODEKATA;So;0;L;;;;;N;;;;;
+1D0D1;BYZANTINE MUSICAL SYMBOL DIESIS MONOGRAMMOS TESSERA DODEKATA;So;0;L;;;;;N;;;;;
+1D0D2;BYZANTINE MUSICAL SYMBOL DIESIS DIGRAMMOS EX DODEKATA;So;0;L;;;;;N;;;;;
+1D0D3;BYZANTINE MUSICAL SYMBOL DIESIS TRIGRAMMOS OKTO DODEKATA;So;0;L;;;;;N;;;;;
+1D0D4;BYZANTINE MUSICAL SYMBOL YFESIS APLI DYO DODEKATA;So;0;L;;;;;N;;;;;
+1D0D5;BYZANTINE MUSICAL SYMBOL YFESIS MONOGRAMMOS TESSERA DODEKATA;So;0;L;;;;;N;;;;;
+1D0D6;BYZANTINE MUSICAL SYMBOL YFESIS DIGRAMMOS EX DODEKATA;So;0;L;;;;;N;;;;;
+1D0D7;BYZANTINE MUSICAL SYMBOL YFESIS TRIGRAMMOS OKTO DODEKATA;So;0;L;;;;;N;;;;;
+1D0D8;BYZANTINE MUSICAL SYMBOL GENIKI DIESIS;So;0;L;;;;;N;;;;;
+1D0D9;BYZANTINE MUSICAL SYMBOL GENIKI YFESIS;So;0;L;;;;;N;;;;;
+1D0DA;BYZANTINE MUSICAL SYMBOL DIASTOLI APLI MIKRI;So;0;L;;;;;N;;;;;
+1D0DB;BYZANTINE MUSICAL SYMBOL DIASTOLI APLI MEGALI;So;0;L;;;;;N;;;;;
+1D0DC;BYZANTINE MUSICAL SYMBOL DIASTOLI DIPLI;So;0;L;;;;;N;;;;;
+1D0DD;BYZANTINE MUSICAL SYMBOL DIASTOLI THESEOS;So;0;L;;;;;N;;;;;
+1D0DE;BYZANTINE MUSICAL SYMBOL SIMANSIS THESEOS;So;0;L;;;;;N;;;;;
+1D0DF;BYZANTINE MUSICAL SYMBOL SIMANSIS THESEOS DISIMOU;So;0;L;;;;;N;;;;;
+1D0E0;BYZANTINE MUSICAL SYMBOL SIMANSIS THESEOS TRISIMOU;So;0;L;;;;;N;;;;;
+1D0E1;BYZANTINE MUSICAL SYMBOL SIMANSIS THESEOS TETRASIMOU;So;0;L;;;;;N;;;;;
+1D0E2;BYZANTINE MUSICAL SYMBOL SIMANSIS ARSEOS;So;0;L;;;;;N;;;;;
+1D0E3;BYZANTINE MUSICAL SYMBOL SIMANSIS ARSEOS DISIMOU;So;0;L;;;;;N;;;;;
+1D0E4;BYZANTINE MUSICAL SYMBOL SIMANSIS ARSEOS TRISIMOU;So;0;L;;;;;N;;;;;
+1D0E5;BYZANTINE MUSICAL SYMBOL SIMANSIS ARSEOS TETRASIMOU;So;0;L;;;;;N;;;;;
+1D0E6;BYZANTINE MUSICAL SYMBOL DIGRAMMA GG;So;0;L;;;;;N;;;;;
+1D0E7;BYZANTINE MUSICAL SYMBOL DIFTOGGOS OU;So;0;L;;;;;N;;;;;
+1D0E8;BYZANTINE MUSICAL SYMBOL STIGMA;So;0;L;;;;;N;;;;;
+1D0E9;BYZANTINE MUSICAL SYMBOL ARKTIKO PA;So;0;L;;;;;N;;;;;
+1D0EA;BYZANTINE MUSICAL SYMBOL ARKTIKO VOU;So;0;L;;;;;N;;;;;
+1D0EB;BYZANTINE MUSICAL SYMBOL ARKTIKO GA;So;0;L;;;;;N;;;;;
+1D0EC;BYZANTINE MUSICAL SYMBOL ARKTIKO DI;So;0;L;;;;;N;;;;;
+1D0ED;BYZANTINE MUSICAL SYMBOL ARKTIKO KE;So;0;L;;;;;N;;;;;
+1D0EE;BYZANTINE MUSICAL SYMBOL ARKTIKO ZO;So;0;L;;;;;N;;;;;
+1D0EF;BYZANTINE MUSICAL SYMBOL ARKTIKO NI;So;0;L;;;;;N;;;;;
+1D0F0;BYZANTINE MUSICAL SYMBOL KENTIMATA NEO MESO;So;0;L;;;;;N;;;;;
+1D0F1;BYZANTINE MUSICAL SYMBOL KENTIMA NEO MESO;So;0;L;;;;;N;;;;;
+1D0F2;BYZANTINE MUSICAL SYMBOL KENTIMATA NEO KATO;So;0;L;;;;;N;;;;;
+1D0F3;BYZANTINE MUSICAL SYMBOL KENTIMA NEO KATO;So;0;L;;;;;N;;;;;
+1D0F4;BYZANTINE MUSICAL SYMBOL KLASMA KATO;So;0;L;;;;;N;;;;;
+1D0F5;BYZANTINE MUSICAL SYMBOL GORGON NEO KATO;So;0;L;;;;;N;;;;;
+1D100;MUSICAL SYMBOL SINGLE BARLINE;So;0;L;;;;;N;;;;;
+1D101;MUSICAL SYMBOL DOUBLE BARLINE;So;0;L;;;;;N;;;;;
+1D102;MUSICAL SYMBOL FINAL BARLINE;So;0;L;;;;;N;;;;;
+1D103;MUSICAL SYMBOL REVERSE FINAL BARLINE;So;0;L;;;;;N;;;;;
+1D104;MUSICAL SYMBOL DASHED BARLINE;So;0;L;;;;;N;;;;;
+1D105;MUSICAL SYMBOL SHORT BARLINE;So;0;L;;;;;N;;;;;
+1D106;MUSICAL SYMBOL LEFT REPEAT SIGN;So;0;L;;;;;N;;;;;
+1D107;MUSICAL SYMBOL RIGHT REPEAT SIGN;So;0;L;;;;;N;;;;;
+1D108;MUSICAL SYMBOL REPEAT DOTS;So;0;L;;;;;N;;;;;
+1D109;MUSICAL SYMBOL DAL SEGNO;So;0;L;;;;;N;;;;;
+1D10A;MUSICAL SYMBOL DA CAPO;So;0;L;;;;;N;;;;;
+1D10B;MUSICAL SYMBOL SEGNO;So;0;L;;;;;N;;;;;
+1D10C;MUSICAL SYMBOL CODA;So;0;L;;;;;N;;;;;
+1D10D;MUSICAL SYMBOL REPEATED FIGURE-1;So;0;L;;;;;N;;;;;
+1D10E;MUSICAL SYMBOL REPEATED FIGURE-2;So;0;L;;;;;N;;;;;
+1D10F;MUSICAL SYMBOL REPEATED FIGURE-3;So;0;L;;;;;N;;;;;
+1D110;MUSICAL SYMBOL FERMATA;So;0;L;;;;;N;;;;;
+1D111;MUSICAL SYMBOL FERMATA BELOW;So;0;L;;;;;N;;;;;
+1D112;MUSICAL SYMBOL BREATH MARK;So;0;L;;;;;N;;;;;
+1D113;MUSICAL SYMBOL CAESURA;So;0;L;;;;;N;;;;;
+1D114;MUSICAL SYMBOL BRACE;So;0;L;;;;;N;;;;;
+1D115;MUSICAL SYMBOL BRACKET;So;0;L;;;;;N;;;;;
+1D116;MUSICAL SYMBOL ONE-LINE STAFF;So;0;L;;;;;N;;;;;
+1D117;MUSICAL SYMBOL TWO-LINE STAFF;So;0;L;;;;;N;;;;;
+1D118;MUSICAL SYMBOL THREE-LINE STAFF;So;0;L;;;;;N;;;;;
+1D119;MUSICAL SYMBOL FOUR-LINE STAFF;So;0;L;;;;;N;;;;;
+1D11A;MUSICAL SYMBOL FIVE-LINE STAFF;So;0;L;;;;;N;;;;;
+1D11B;MUSICAL SYMBOL SIX-LINE STAFF;So;0;L;;;;;N;;;;;
+1D11C;MUSICAL SYMBOL SIX-STRING FRETBOARD;So;0;L;;;;;N;;;;;
+1D11D;MUSICAL SYMBOL FOUR-STRING FRETBOARD;So;0;L;;;;;N;;;;;
+1D11E;MUSICAL SYMBOL G CLEF;So;0;L;;;;;N;;;;;
+1D11F;MUSICAL SYMBOL G CLEF OTTAVA ALTA;So;0;L;;;;;N;;;;;
+1D120;MUSICAL SYMBOL G CLEF OTTAVA BASSA;So;0;L;;;;;N;;;;;
+1D121;MUSICAL SYMBOL C CLEF;So;0;L;;;;;N;;;;;
+1D122;MUSICAL SYMBOL F CLEF;So;0;L;;;;;N;;;;;
+1D123;MUSICAL SYMBOL F CLEF OTTAVA ALTA;So;0;L;;;;;N;;;;;
+1D124;MUSICAL SYMBOL F CLEF OTTAVA BASSA;So;0;L;;;;;N;;;;;
+1D125;MUSICAL SYMBOL DRUM CLEF-1;So;0;L;;;;;N;;;;;
+1D126;MUSICAL SYMBOL DRUM CLEF-2;So;0;L;;;;;N;;;;;
+1D129;MUSICAL SYMBOL MULTIPLE MEASURE REST;So;0;L;;;;;N;;;;;
+1D12A;MUSICAL SYMBOL DOUBLE SHARP;So;0;L;;;;;N;;;;;
+1D12B;MUSICAL SYMBOL DOUBLE FLAT;So;0;L;;;;;N;;;;;
+1D12C;MUSICAL SYMBOL FLAT UP;So;0;L;;;;;N;;;;;
+1D12D;MUSICAL SYMBOL FLAT DOWN;So;0;L;;;;;N;;;;;
+1D12E;MUSICAL SYMBOL NATURAL UP;So;0;L;;;;;N;;;;;
+1D12F;MUSICAL SYMBOL NATURAL DOWN;So;0;L;;;;;N;;;;;
+1D130;MUSICAL SYMBOL SHARP UP;So;0;L;;;;;N;;;;;
+1D131;MUSICAL SYMBOL SHARP DOWN;So;0;L;;;;;N;;;;;
+1D132;MUSICAL SYMBOL QUARTER TONE SHARP;So;0;L;;;;;N;;;;;
+1D133;MUSICAL SYMBOL QUARTER TONE FLAT;So;0;L;;;;;N;;;;;
+1D134;MUSICAL SYMBOL COMMON TIME;So;0;L;;;;;N;;;;;
+1D135;MUSICAL SYMBOL CUT TIME;So;0;L;;;;;N;;;;;
+1D136;MUSICAL SYMBOL OTTAVA ALTA;So;0;L;;;;;N;;;;;
+1D137;MUSICAL SYMBOL OTTAVA BASSA;So;0;L;;;;;N;;;;;
+1D138;MUSICAL SYMBOL QUINDICESIMA ALTA;So;0;L;;;;;N;;;;;
+1D139;MUSICAL SYMBOL QUINDICESIMA BASSA;So;0;L;;;;;N;;;;;
+1D13A;MUSICAL SYMBOL MULTI REST;So;0;L;;;;;N;;;;;
+1D13B;MUSICAL SYMBOL WHOLE REST;So;0;L;;;;;N;;;;;
+1D13C;MUSICAL SYMBOL HALF REST;So;0;L;;;;;N;;;;;
+1D13D;MUSICAL SYMBOL QUARTER REST;So;0;L;;;;;N;;;;;
+1D13E;MUSICAL SYMBOL EIGHTH REST;So;0;L;;;;;N;;;;;
+1D13F;MUSICAL SYMBOL SIXTEENTH REST;So;0;L;;;;;N;;;;;
+1D140;MUSICAL SYMBOL THIRTY-SECOND REST;So;0;L;;;;;N;;;;;
+1D141;MUSICAL SYMBOL SIXTY-FOURTH REST;So;0;L;;;;;N;;;;;
+1D142;MUSICAL SYMBOL ONE HUNDRED TWENTY-EIGHTH REST;So;0;L;;;;;N;;;;;
+1D143;MUSICAL SYMBOL X NOTEHEAD;So;0;L;;;;;N;;;;;
+1D144;MUSICAL SYMBOL PLUS NOTEHEAD;So;0;L;;;;;N;;;;;
+1D145;MUSICAL SYMBOL CIRCLE X NOTEHEAD;So;0;L;;;;;N;;;;;
+1D146;MUSICAL SYMBOL SQUARE NOTEHEAD WHITE;So;0;L;;;;;N;;;;;
+1D147;MUSICAL SYMBOL SQUARE NOTEHEAD BLACK;So;0;L;;;;;N;;;;;
+1D148;MUSICAL SYMBOL TRIANGLE NOTEHEAD UP WHITE;So;0;L;;;;;N;;;;;
+1D149;MUSICAL SYMBOL TRIANGLE NOTEHEAD UP BLACK;So;0;L;;;;;N;;;;;
+1D14A;MUSICAL SYMBOL TRIANGLE NOTEHEAD LEFT WHITE;So;0;L;;;;;N;;;;;
+1D14B;MUSICAL SYMBOL TRIANGLE NOTEHEAD LEFT BLACK;So;0;L;;;;;N;;;;;
+1D14C;MUSICAL SYMBOL TRIANGLE NOTEHEAD RIGHT WHITE;So;0;L;;;;;N;;;;;
+1D14D;MUSICAL SYMBOL TRIANGLE NOTEHEAD RIGHT BLACK;So;0;L;;;;;N;;;;;
+1D14E;MUSICAL SYMBOL TRIANGLE NOTEHEAD DOWN WHITE;So;0;L;;;;;N;;;;;
+1D14F;MUSICAL SYMBOL TRIANGLE NOTEHEAD DOWN BLACK;So;0;L;;;;;N;;;;;
+1D150;MUSICAL SYMBOL TRIANGLE NOTEHEAD UP RIGHT WHITE;So;0;L;;;;;N;;;;;
+1D151;MUSICAL SYMBOL TRIANGLE NOTEHEAD UP RIGHT BLACK;So;0;L;;;;;N;;;;;
+1D152;MUSICAL SYMBOL MOON NOTEHEAD WHITE;So;0;L;;;;;N;;;;;
+1D153;MUSICAL SYMBOL MOON NOTEHEAD BLACK;So;0;L;;;;;N;;;;;
+1D154;MUSICAL SYMBOL TRIANGLE-ROUND NOTEHEAD DOWN WHITE;So;0;L;;;;;N;;;;;
+1D155;MUSICAL SYMBOL TRIANGLE-ROUND NOTEHEAD DOWN BLACK;So;0;L;;;;;N;;;;;
+1D156;MUSICAL SYMBOL PARENTHESIS NOTEHEAD;So;0;L;;;;;N;;;;;
+1D157;MUSICAL SYMBOL VOID NOTEHEAD;So;0;L;;;;;N;;;;;
+1D158;MUSICAL SYMBOL NOTEHEAD BLACK;So;0;L;;;;;N;;;;;
+1D159;MUSICAL SYMBOL NULL NOTEHEAD;So;0;L;;;;;N;;;;;
+1D15A;MUSICAL SYMBOL CLUSTER NOTEHEAD WHITE;So;0;L;;;;;N;;;;;
+1D15B;MUSICAL SYMBOL CLUSTER NOTEHEAD BLACK;So;0;L;;;;;N;;;;;
+1D15C;MUSICAL SYMBOL BREVE;So;0;L;;;;;N;;;;;
+1D15D;MUSICAL SYMBOL WHOLE NOTE;So;0;L;;;;;N;;;;;
+1D15E;MUSICAL SYMBOL HALF NOTE;So;0;L;1D157 1D165;;;;N;;;;;
+1D15F;MUSICAL SYMBOL QUARTER NOTE;So;0;L;1D158 1D165;;;;N;;;;;
+1D160;MUSICAL SYMBOL EIGHTH NOTE;So;0;L;1D15F 1D16E;;;;N;;;;;
+1D161;MUSICAL SYMBOL SIXTEENTH NOTE;So;0;L;1D15F 1D16F;;;;N;;;;;
+1D162;MUSICAL SYMBOL THIRTY-SECOND NOTE;So;0;L;1D15F 1D170;;;;N;;;;;
+1D163;MUSICAL SYMBOL SIXTY-FOURTH NOTE;So;0;L;1D15F 1D171;;;;N;;;;;
+1D164;MUSICAL SYMBOL ONE HUNDRED TWENTY-EIGHTH NOTE;So;0;L;1D15F 1D172;;;;N;;;;;
+1D165;MUSICAL SYMBOL COMBINING STEM;Mc;216;L;;;;;N;;;;;
+1D166;MUSICAL SYMBOL COMBINING SPRECHGESANG STEM;Mc;216;L;;;;;N;;;;;
+1D167;MUSICAL SYMBOL COMBINING TREMOLO-1;Mn;1;NSM;;;;;N;;;;;
+1D168;MUSICAL SYMBOL COMBINING TREMOLO-2;Mn;1;NSM;;;;;N;;;;;
+1D169;MUSICAL SYMBOL COMBINING TREMOLO-3;Mn;1;NSM;;;;;N;;;;;
+1D16A;MUSICAL SYMBOL FINGERED TREMOLO-1;So;0;L;;;;;N;;;;;
+1D16B;MUSICAL SYMBOL FINGERED TREMOLO-2;So;0;L;;;;;N;;;;;
+1D16C;MUSICAL SYMBOL FINGERED TREMOLO-3;So;0;L;;;;;N;;;;;
+1D16D;MUSICAL SYMBOL COMBINING AUGMENTATION DOT;Mc;226;L;;;;;N;;;;;
+1D16E;MUSICAL SYMBOL COMBINING FLAG-1;Mc;216;L;;;;;N;;;;;
+1D16F;MUSICAL SYMBOL COMBINING FLAG-2;Mc;216;L;;;;;N;;;;;
+1D170;MUSICAL SYMBOL COMBINING FLAG-3;Mc;216;L;;;;;N;;;;;
+1D171;MUSICAL SYMBOL COMBINING FLAG-4;Mc;216;L;;;;;N;;;;;
+1D172;MUSICAL SYMBOL COMBINING FLAG-5;Mc;216;L;;;;;N;;;;;
+1D173;MUSICAL SYMBOL BEGIN BEAM;Cf;0;BN;;;;;N;;;;;
+1D174;MUSICAL SYMBOL END BEAM;Cf;0;BN;;;;;N;;;;;
+1D175;MUSICAL SYMBOL BEGIN TIE;Cf;0;BN;;;;;N;;;;;
+1D176;MUSICAL SYMBOL END TIE;Cf;0;BN;;;;;N;;;;;
+1D177;MUSICAL SYMBOL BEGIN SLUR;Cf;0;BN;;;;;N;;;;;
+1D178;MUSICAL SYMBOL END SLUR;Cf;0;BN;;;;;N;;;;;
+1D179;MUSICAL SYMBOL BEGIN PHRASE;Cf;0;BN;;;;;N;;;;;
+1D17A;MUSICAL SYMBOL END PHRASE;Cf;0;BN;;;;;N;;;;;
+1D17B;MUSICAL SYMBOL COMBINING ACCENT;Mn;220;NSM;;;;;N;;;;;
+1D17C;MUSICAL SYMBOL COMBINING STACCATO;Mn;220;NSM;;;;;N;;;;;
+1D17D;MUSICAL SYMBOL COMBINING TENUTO;Mn;220;NSM;;;;;N;;;;;
+1D17E;MUSICAL SYMBOL COMBINING STACCATISSIMO;Mn;220;NSM;;;;;N;;;;;
+1D17F;MUSICAL SYMBOL COMBINING MARCATO;Mn;220;NSM;;;;;N;;;;;
+1D180;MUSICAL SYMBOL COMBINING MARCATO-STACCATO;Mn;220;NSM;;;;;N;;;;;
+1D181;MUSICAL SYMBOL COMBINING ACCENT-STACCATO;Mn;220;NSM;;;;;N;;;;;
+1D182;MUSICAL SYMBOL COMBINING LOURE;Mn;220;NSM;;;;;N;;;;;
+1D183;MUSICAL SYMBOL ARPEGGIATO UP;So;0;L;;;;;N;;;;;
+1D184;MUSICAL SYMBOL ARPEGGIATO DOWN;So;0;L;;;;;N;;;;;
+1D185;MUSICAL SYMBOL COMBINING DOIT;Mn;230;NSM;;;;;N;;;;;
+1D186;MUSICAL SYMBOL COMBINING RIP;Mn;230;NSM;;;;;N;;;;;
+1D187;MUSICAL SYMBOL COMBINING FLIP;Mn;230;NSM;;;;;N;;;;;
+1D188;MUSICAL SYMBOL COMBINING SMEAR;Mn;230;NSM;;;;;N;;;;;
+1D189;MUSICAL SYMBOL COMBINING BEND;Mn;230;NSM;;;;;N;;;;;
+1D18A;MUSICAL SYMBOL COMBINING DOUBLE TONGUE;Mn;220;NSM;;;;;N;;;;;
+1D18B;MUSICAL SYMBOL COMBINING TRIPLE TONGUE;Mn;220;NSM;;;;;N;;;;;
+1D18C;MUSICAL SYMBOL RINFORZANDO;So;0;L;;;;;N;;;;;
+1D18D;MUSICAL SYMBOL SUBITO;So;0;L;;;;;N;;;;;
+1D18E;MUSICAL SYMBOL Z;So;0;L;;;;;N;;;;;
+1D18F;MUSICAL SYMBOL PIANO;So;0;L;;;;;N;;;;;
+1D190;MUSICAL SYMBOL MEZZO;So;0;L;;;;;N;;;;;
+1D191;MUSICAL SYMBOL FORTE;So;0;L;;;;;N;;;;;
+1D192;MUSICAL SYMBOL CRESCENDO;So;0;L;;;;;N;;;;;
+1D193;MUSICAL SYMBOL DECRESCENDO;So;0;L;;;;;N;;;;;
+1D194;MUSICAL SYMBOL GRACE NOTE SLASH;So;0;L;;;;;N;;;;;
+1D195;MUSICAL SYMBOL GRACE NOTE NO SLASH;So;0;L;;;;;N;;;;;
+1D196;MUSICAL SYMBOL TR;So;0;L;;;;;N;;;;;
+1D197;MUSICAL SYMBOL TURN;So;0;L;;;;;N;;;;;
+1D198;MUSICAL SYMBOL INVERTED TURN;So;0;L;;;;;N;;;;;
+1D199;MUSICAL SYMBOL TURN SLASH;So;0;L;;;;;N;;;;;
+1D19A;MUSICAL SYMBOL TURN UP;So;0;L;;;;;N;;;;;
+1D19B;MUSICAL SYMBOL ORNAMENT STROKE-1;So;0;L;;;;;N;;;;;
+1D19C;MUSICAL SYMBOL ORNAMENT STROKE-2;So;0;L;;;;;N;;;;;
+1D19D;MUSICAL SYMBOL ORNAMENT STROKE-3;So;0;L;;;;;N;;;;;
+1D19E;MUSICAL SYMBOL ORNAMENT STROKE-4;So;0;L;;;;;N;;;;;
+1D19F;MUSICAL SYMBOL ORNAMENT STROKE-5;So;0;L;;;;;N;;;;;
+1D1A0;MUSICAL SYMBOL ORNAMENT STROKE-6;So;0;L;;;;;N;;;;;
+1D1A1;MUSICAL SYMBOL ORNAMENT STROKE-7;So;0;L;;;;;N;;;;;
+1D1A2;MUSICAL SYMBOL ORNAMENT STROKE-8;So;0;L;;;;;N;;;;;
+1D1A3;MUSICAL SYMBOL ORNAMENT STROKE-9;So;0;L;;;;;N;;;;;
+1D1A4;MUSICAL SYMBOL ORNAMENT STROKE-10;So;0;L;;;;;N;;;;;
+1D1A5;MUSICAL SYMBOL ORNAMENT STROKE-11;So;0;L;;;;;N;;;;;
+1D1A6;MUSICAL SYMBOL HAUPTSTIMME;So;0;L;;;;;N;;;;;
+1D1A7;MUSICAL SYMBOL NEBENSTIMME;So;0;L;;;;;N;;;;;
+1D1A8;MUSICAL SYMBOL END OF STIMME;So;0;L;;;;;N;;;;;
+1D1A9;MUSICAL SYMBOL DEGREE SLASH;So;0;L;;;;;N;;;;;
+1D1AA;MUSICAL SYMBOL COMBINING DOWN BOW;Mn;230;NSM;;;;;N;;;;;
+1D1AB;MUSICAL SYMBOL COMBINING UP BOW;Mn;230;NSM;;;;;N;;;;;
+1D1AC;MUSICAL SYMBOL COMBINING HARMONIC;Mn;230;NSM;;;;;N;;;;;
+1D1AD;MUSICAL SYMBOL COMBINING SNAP PIZZICATO;Mn;230;NSM;;;;;N;;;;;
+1D1AE;MUSICAL SYMBOL PEDAL MARK;So;0;L;;;;;N;;;;;
+1D1AF;MUSICAL SYMBOL PEDAL UP MARK;So;0;L;;;;;N;;;;;
+1D1B0;MUSICAL SYMBOL HALF PEDAL MARK;So;0;L;;;;;N;;;;;
+1D1B1;MUSICAL SYMBOL GLISSANDO UP;So;0;L;;;;;N;;;;;
+1D1B2;MUSICAL SYMBOL GLISSANDO DOWN;So;0;L;;;;;N;;;;;
+1D1B3;MUSICAL SYMBOL WITH FINGERNAILS;So;0;L;;;;;N;;;;;
+1D1B4;MUSICAL SYMBOL DAMP;So;0;L;;;;;N;;;;;
+1D1B5;MUSICAL SYMBOL DAMP ALL;So;0;L;;;;;N;;;;;
+1D1B6;MUSICAL SYMBOL MAXIMA;So;0;L;;;;;N;;;;;
+1D1B7;MUSICAL SYMBOL LONGA;So;0;L;;;;;N;;;;;
+1D1B8;MUSICAL SYMBOL BREVIS;So;0;L;;;;;N;;;;;
+1D1B9;MUSICAL SYMBOL SEMIBREVIS WHITE;So;0;L;;;;;N;;;;;
+1D1BA;MUSICAL SYMBOL SEMIBREVIS BLACK;So;0;L;;;;;N;;;;;
+1D1BB;MUSICAL SYMBOL MINIMA;So;0;L;1D1B9 1D165;;;;N;;;;;
+1D1BC;MUSICAL SYMBOL MINIMA BLACK;So;0;L;1D1BA 1D165;;;;N;;;;;
+1D1BD;MUSICAL SYMBOL SEMIMINIMA WHITE;So;0;L;1D1BB 1D16E;;;;N;;;;;
+1D1BE;MUSICAL SYMBOL SEMIMINIMA BLACK;So;0;L;1D1BC 1D16E;;;;N;;;;;
+1D1BF;MUSICAL SYMBOL FUSA WHITE;So;0;L;1D1BB 1D16F;;;;N;;;;;
+1D1C0;MUSICAL SYMBOL FUSA BLACK;So;0;L;1D1BC 1D16F;;;;N;;;;;
+1D1C1;MUSICAL SYMBOL LONGA PERFECTA REST;So;0;L;;;;;N;;;;;
+1D1C2;MUSICAL SYMBOL LONGA IMPERFECTA REST;So;0;L;;;;;N;;;;;
+1D1C3;MUSICAL SYMBOL BREVIS REST;So;0;L;;;;;N;;;;;
+1D1C4;MUSICAL SYMBOL SEMIBREVIS REST;So;0;L;;;;;N;;;;;
+1D1C5;MUSICAL SYMBOL MINIMA REST;So;0;L;;;;;N;;;;;
+1D1C6;MUSICAL SYMBOL SEMIMINIMA REST;So;0;L;;;;;N;;;;;
+1D1C7;MUSICAL SYMBOL TEMPUS PERFECTUM CUM PROLATIONE PERFECTA;So;0;L;;;;;N;;;;;
+1D1C8;MUSICAL SYMBOL TEMPUS PERFECTUM CUM PROLATIONE IMPERFECTA;So;0;L;;;;;N;;;;;
+1D1C9;MUSICAL SYMBOL TEMPUS PERFECTUM CUM PROLATIONE PERFECTA DIMINUTION-1;So;0;L;;;;;N;;;;;
+1D1CA;MUSICAL SYMBOL TEMPUS IMPERFECTUM CUM PROLATIONE PERFECTA;So;0;L;;;;;N;;;;;
+1D1CB;MUSICAL SYMBOL TEMPUS IMPERFECTUM CUM PROLATIONE IMPERFECTA;So;0;L;;;;;N;;;;;
+1D1CC;MUSICAL SYMBOL TEMPUS IMPERFECTUM CUM PROLATIONE IMPERFECTA DIMINUTION-1;So;0;L;;;;;N;;;;;
+1D1CD;MUSICAL SYMBOL TEMPUS IMPERFECTUM CUM PROLATIONE IMPERFECTA DIMINUTION-2;So;0;L;;;;;N;;;;;
+1D1CE;MUSICAL SYMBOL TEMPUS IMPERFECTUM CUM PROLATIONE IMPERFECTA DIMINUTION-3;So;0;L;;;;;N;;;;;
+1D1CF;MUSICAL SYMBOL CROIX;So;0;L;;;;;N;;;;;
+1D1D0;MUSICAL SYMBOL GREGORIAN C CLEF;So;0;L;;;;;N;;;;;
+1D1D1;MUSICAL SYMBOL GREGORIAN F CLEF;So;0;L;;;;;N;;;;;
+1D1D2;MUSICAL SYMBOL SQUARE B;So;0;L;;;;;N;;;;;
+1D1D3;MUSICAL SYMBOL VIRGA;So;0;L;;;;;N;;;;;
+1D1D4;MUSICAL SYMBOL PODATUS;So;0;L;;;;;N;;;;;
+1D1D5;MUSICAL SYMBOL CLIVIS;So;0;L;;;;;N;;;;;
+1D1D6;MUSICAL SYMBOL SCANDICUS;So;0;L;;;;;N;;;;;
+1D1D7;MUSICAL SYMBOL CLIMACUS;So;0;L;;;;;N;;;;;
+1D1D8;MUSICAL SYMBOL TORCULUS;So;0;L;;;;;N;;;;;
+1D1D9;MUSICAL SYMBOL PORRECTUS;So;0;L;;;;;N;;;;;
+1D1DA;MUSICAL SYMBOL PORRECTUS FLEXUS;So;0;L;;;;;N;;;;;
+1D1DB;MUSICAL SYMBOL SCANDICUS FLEXUS;So;0;L;;;;;N;;;;;
+1D1DC;MUSICAL SYMBOL TORCULUS RESUPINUS;So;0;L;;;;;N;;;;;
+1D1DD;MUSICAL SYMBOL PES SUBPUNCTIS;So;0;L;;;;;N;;;;;
+1D1DE;MUSICAL SYMBOL KIEVAN C CLEF;So;0;L;;;;;N;;;;;
+1D1DF;MUSICAL SYMBOL KIEVAN END OF PIECE;So;0;L;;;;;N;;;;;
+1D1E0;MUSICAL SYMBOL KIEVAN FINAL NOTE;So;0;L;;;;;N;;;;;
+1D1E1;MUSICAL SYMBOL KIEVAN RECITATIVE MARK;So;0;L;;;;;N;;;;;
+1D1E2;MUSICAL SYMBOL KIEVAN WHOLE NOTE;So;0;L;;;;;N;;;;;
+1D1E3;MUSICAL SYMBOL KIEVAN HALF NOTE;So;0;L;;;;;N;;;;;
+1D1E4;MUSICAL SYMBOL KIEVAN QUARTER NOTE STEM DOWN;So;0;L;;;;;N;;;;;
+1D1E5;MUSICAL SYMBOL KIEVAN QUARTER NOTE STEM UP;So;0;L;;;;;N;;;;;
+1D1E6;MUSICAL SYMBOL KIEVAN EIGHTH NOTE STEM DOWN;So;0;L;;;;;N;;;;;
+1D1E7;MUSICAL SYMBOL KIEVAN EIGHTH NOTE STEM UP;So;0;L;;;;;N;;;;;
+1D1E8;MUSICAL SYMBOL KIEVAN FLAT SIGN;So;0;L;;;;;N;;;;;
+1D200;GREEK VOCAL NOTATION SYMBOL-1;So;0;ON;;;;;N;;;;;
+1D201;GREEK VOCAL NOTATION SYMBOL-2;So;0;ON;;;;;N;;;;;
+1D202;GREEK VOCAL NOTATION SYMBOL-3;So;0;ON;;;;;N;;;;;
+1D203;GREEK VOCAL NOTATION SYMBOL-4;So;0;ON;;;;;N;;;;;
+1D204;GREEK VOCAL NOTATION SYMBOL-5;So;0;ON;;;;;N;;;;;
+1D205;GREEK VOCAL NOTATION SYMBOL-6;So;0;ON;;;;;N;;;;;
+1D206;GREEK VOCAL NOTATION SYMBOL-7;So;0;ON;;;;;N;;;;;
+1D207;GREEK VOCAL NOTATION SYMBOL-8;So;0;ON;;;;;N;;;;;
+1D208;GREEK VOCAL NOTATION SYMBOL-9;So;0;ON;;;;;N;;;;;
+1D209;GREEK VOCAL NOTATION SYMBOL-10;So;0;ON;;;;;N;;;;;
+1D20A;GREEK VOCAL NOTATION SYMBOL-11;So;0;ON;;;;;N;;;;;
+1D20B;GREEK VOCAL NOTATION SYMBOL-12;So;0;ON;;;;;N;;;;;
+1D20C;GREEK VOCAL NOTATION SYMBOL-13;So;0;ON;;;;;N;;;;;
+1D20D;GREEK VOCAL NOTATION SYMBOL-14;So;0;ON;;;;;N;;;;;
+1D20E;GREEK VOCAL NOTATION SYMBOL-15;So;0;ON;;;;;N;;;;;
+1D20F;GREEK VOCAL NOTATION SYMBOL-16;So;0;ON;;;;;N;;;;;
+1D210;GREEK VOCAL NOTATION SYMBOL-17;So;0;ON;;;;;N;;;;;
+1D211;GREEK VOCAL NOTATION SYMBOL-18;So;0;ON;;;;;N;;;;;
+1D212;GREEK VOCAL NOTATION SYMBOL-19;So;0;ON;;;;;N;;;;;
+1D213;GREEK VOCAL NOTATION SYMBOL-20;So;0;ON;;;;;N;;;;;
+1D214;GREEK VOCAL NOTATION SYMBOL-21;So;0;ON;;;;;N;;;;;
+1D215;GREEK VOCAL NOTATION SYMBOL-22;So;0;ON;;;;;N;;;;;
+1D216;GREEK VOCAL NOTATION SYMBOL-23;So;0;ON;;;;;N;;;;;
+1D217;GREEK VOCAL NOTATION SYMBOL-24;So;0;ON;;;;;N;;;;;
+1D218;GREEK VOCAL NOTATION SYMBOL-50;So;0;ON;;;;;N;;;;;
+1D219;GREEK VOCAL NOTATION SYMBOL-51;So;0;ON;;;;;N;;;;;
+1D21A;GREEK VOCAL NOTATION SYMBOL-52;So;0;ON;;;;;N;;;;;
+1D21B;GREEK VOCAL NOTATION SYMBOL-53;So;0;ON;;;;;N;;;;;
+1D21C;GREEK VOCAL NOTATION SYMBOL-54;So;0;ON;;;;;N;;;;;
+1D21D;GREEK INSTRUMENTAL NOTATION SYMBOL-1;So;0;ON;;;;;N;;;;;
+1D21E;GREEK INSTRUMENTAL NOTATION SYMBOL-2;So;0;ON;;;;;N;;;;;
+1D21F;GREEK INSTRUMENTAL NOTATION SYMBOL-4;So;0;ON;;;;;N;;;;;
+1D220;GREEK INSTRUMENTAL NOTATION SYMBOL-5;So;0;ON;;;;;N;;;;;
+1D221;GREEK INSTRUMENTAL NOTATION SYMBOL-7;So;0;ON;;;;;N;;;;;
+1D222;GREEK INSTRUMENTAL NOTATION SYMBOL-8;So;0;ON;;;;;N;;;;;
+1D223;GREEK INSTRUMENTAL NOTATION SYMBOL-11;So;0;ON;;;;;N;;;;;
+1D224;GREEK INSTRUMENTAL NOTATION SYMBOL-12;So;0;ON;;;;;N;;;;;
+1D225;GREEK INSTRUMENTAL NOTATION SYMBOL-13;So;0;ON;;;;;N;;;;;
+1D226;GREEK INSTRUMENTAL NOTATION SYMBOL-14;So;0;ON;;;;;N;;;;;
+1D227;GREEK INSTRUMENTAL NOTATION SYMBOL-17;So;0;ON;;;;;N;;;;;
+1D228;GREEK INSTRUMENTAL NOTATION SYMBOL-18;So;0;ON;;;;;N;;;;;
+1D229;GREEK INSTRUMENTAL NOTATION SYMBOL-19;So;0;ON;;;;;N;;;;;
+1D22A;GREEK INSTRUMENTAL NOTATION SYMBOL-23;So;0;ON;;;;;N;;;;;
+1D22B;GREEK INSTRUMENTAL NOTATION SYMBOL-24;So;0;ON;;;;;N;;;;;
+1D22C;GREEK INSTRUMENTAL NOTATION SYMBOL-25;So;0;ON;;;;;N;;;;;
+1D22D;GREEK INSTRUMENTAL NOTATION SYMBOL-26;So;0;ON;;;;;N;;;;;
+1D22E;GREEK INSTRUMENTAL NOTATION SYMBOL-27;So;0;ON;;;;;N;;;;;
+1D22F;GREEK INSTRUMENTAL NOTATION SYMBOL-29;So;0;ON;;;;;N;;;;;
+1D230;GREEK INSTRUMENTAL NOTATION SYMBOL-30;So;0;ON;;;;;N;;;;;
+1D231;GREEK INSTRUMENTAL NOTATION SYMBOL-32;So;0;ON;;;;;N;;;;;
+1D232;GREEK INSTRUMENTAL NOTATION SYMBOL-36;So;0;ON;;;;;N;;;;;
+1D233;GREEK INSTRUMENTAL NOTATION SYMBOL-37;So;0;ON;;;;;N;;;;;
+1D234;GREEK INSTRUMENTAL NOTATION SYMBOL-38;So;0;ON;;;;;N;;;;;
+1D235;GREEK INSTRUMENTAL NOTATION SYMBOL-39;So;0;ON;;;;;N;;;;;
+1D236;GREEK INSTRUMENTAL NOTATION SYMBOL-40;So;0;ON;;;;;N;;;;;
+1D237;GREEK INSTRUMENTAL NOTATION SYMBOL-42;So;0;ON;;;;;N;;;;;
+1D238;GREEK INSTRUMENTAL NOTATION SYMBOL-43;So;0;ON;;;;;N;;;;;
+1D239;GREEK INSTRUMENTAL NOTATION SYMBOL-45;So;0;ON;;;;;N;;;;;
+1D23A;GREEK INSTRUMENTAL NOTATION SYMBOL-47;So;0;ON;;;;;N;;;;;
+1D23B;GREEK INSTRUMENTAL NOTATION SYMBOL-48;So;0;ON;;;;;N;;;;;
+1D23C;GREEK INSTRUMENTAL NOTATION SYMBOL-49;So;0;ON;;;;;N;;;;;
+1D23D;GREEK INSTRUMENTAL NOTATION SYMBOL-50;So;0;ON;;;;;N;;;;;
+1D23E;GREEK INSTRUMENTAL NOTATION SYMBOL-51;So;0;ON;;;;;N;;;;;
+1D23F;GREEK INSTRUMENTAL NOTATION SYMBOL-52;So;0;ON;;;;;N;;;;;
+1D240;GREEK INSTRUMENTAL NOTATION SYMBOL-53;So;0;ON;;;;;N;;;;;
+1D241;GREEK INSTRUMENTAL NOTATION SYMBOL-54;So;0;ON;;;;;N;;;;;
+1D242;COMBINING GREEK MUSICAL TRISEME;Mn;230;NSM;;;;;N;;;;;
+1D243;COMBINING GREEK MUSICAL TETRASEME;Mn;230;NSM;;;;;N;;;;;
+1D244;COMBINING GREEK MUSICAL PENTASEME;Mn;230;NSM;;;;;N;;;;;
+1D245;GREEK MUSICAL LEIMMA;So;0;ON;;;;;N;;;;;
+1D300;MONOGRAM FOR EARTH;So;0;ON;;;;;N;;;;;
+1D301;DIGRAM FOR HEAVENLY EARTH;So;0;ON;;;;;N;;;;;
+1D302;DIGRAM FOR HUMAN EARTH;So;0;ON;;;;;N;;;;;
+1D303;DIGRAM FOR EARTHLY HEAVEN;So;0;ON;;;;;N;;;;;
+1D304;DIGRAM FOR EARTHLY HUMAN;So;0;ON;;;;;N;;;;;
+1D305;DIGRAM FOR EARTH;So;0;ON;;;;;N;;;;;
+1D306;TETRAGRAM FOR CENTRE;So;0;ON;;;;;N;;;;;
+1D307;TETRAGRAM FOR FULL CIRCLE;So;0;ON;;;;;N;;;;;
+1D308;TETRAGRAM FOR MIRED;So;0;ON;;;;;N;;;;;
+1D309;TETRAGRAM FOR BARRIER;So;0;ON;;;;;N;;;;;
+1D30A;TETRAGRAM FOR KEEPING SMALL;So;0;ON;;;;;N;;;;;
+1D30B;TETRAGRAM FOR CONTRARIETY;So;0;ON;;;;;N;;;;;
+1D30C;TETRAGRAM FOR ASCENT;So;0;ON;;;;;N;;;;;
+1D30D;TETRAGRAM FOR OPPOSITION;So;0;ON;;;;;N;;;;;
+1D30E;TETRAGRAM FOR BRANCHING OUT;So;0;ON;;;;;N;;;;;
+1D30F;TETRAGRAM FOR DEFECTIVENESS OR DISTORTION;So;0;ON;;;;;N;;;;;
+1D310;TETRAGRAM FOR DIVERGENCE;So;0;ON;;;;;N;;;;;
+1D311;TETRAGRAM FOR YOUTHFULNESS;So;0;ON;;;;;N;;;;;
+1D312;TETRAGRAM FOR INCREASE;So;0;ON;;;;;N;;;;;
+1D313;TETRAGRAM FOR PENETRATION;So;0;ON;;;;;N;;;;;
+1D314;TETRAGRAM FOR REACH;So;0;ON;;;;;N;;;;;
+1D315;TETRAGRAM FOR CONTACT;So;0;ON;;;;;N;;;;;
+1D316;TETRAGRAM FOR HOLDING BACK;So;0;ON;;;;;N;;;;;
+1D317;TETRAGRAM FOR WAITING;So;0;ON;;;;;N;;;;;
+1D318;TETRAGRAM FOR FOLLOWING;So;0;ON;;;;;N;;;;;
+1D319;TETRAGRAM FOR ADVANCE;So;0;ON;;;;;N;;;;;
+1D31A;TETRAGRAM FOR RELEASE;So;0;ON;;;;;N;;;;;
+1D31B;TETRAGRAM FOR RESISTANCE;So;0;ON;;;;;N;;;;;
+1D31C;TETRAGRAM FOR EASE;So;0;ON;;;;;N;;;;;
+1D31D;TETRAGRAM FOR JOY;So;0;ON;;;;;N;;;;;
+1D31E;TETRAGRAM FOR CONTENTION;So;0;ON;;;;;N;;;;;
+1D31F;TETRAGRAM FOR ENDEAVOUR;So;0;ON;;;;;N;;;;;
+1D320;TETRAGRAM FOR DUTIES;So;0;ON;;;;;N;;;;;
+1D321;TETRAGRAM FOR CHANGE;So;0;ON;;;;;N;;;;;
+1D322;TETRAGRAM FOR DECISIVENESS;So;0;ON;;;;;N;;;;;
+1D323;TETRAGRAM FOR BOLD RESOLUTION;So;0;ON;;;;;N;;;;;
+1D324;TETRAGRAM FOR PACKING;So;0;ON;;;;;N;;;;;
+1D325;TETRAGRAM FOR LEGION;So;0;ON;;;;;N;;;;;
+1D326;TETRAGRAM FOR CLOSENESS;So;0;ON;;;;;N;;;;;
+1D327;TETRAGRAM FOR KINSHIP;So;0;ON;;;;;N;;;;;
+1D328;TETRAGRAM FOR GATHERING;So;0;ON;;;;;N;;;;;
+1D329;TETRAGRAM FOR STRENGTH;So;0;ON;;;;;N;;;;;
+1D32A;TETRAGRAM FOR PURITY;So;0;ON;;;;;N;;;;;
+1D32B;TETRAGRAM FOR FULLNESS;So;0;ON;;;;;N;;;;;
+1D32C;TETRAGRAM FOR RESIDENCE;So;0;ON;;;;;N;;;;;
+1D32D;TETRAGRAM FOR LAW OR MODEL;So;0;ON;;;;;N;;;;;
+1D32E;TETRAGRAM FOR RESPONSE;So;0;ON;;;;;N;;;;;
+1D32F;TETRAGRAM FOR GOING TO MEET;So;0;ON;;;;;N;;;;;
+1D330;TETRAGRAM FOR ENCOUNTERS;So;0;ON;;;;;N;;;;;
+1D331;TETRAGRAM FOR STOVE;So;0;ON;;;;;N;;;;;
+1D332;TETRAGRAM FOR GREATNESS;So;0;ON;;;;;N;;;;;
+1D333;TETRAGRAM FOR ENLARGEMENT;So;0;ON;;;;;N;;;;;
+1D334;TETRAGRAM FOR PATTERN;So;0;ON;;;;;N;;;;;
+1D335;TETRAGRAM FOR RITUAL;So;0;ON;;;;;N;;;;;
+1D336;TETRAGRAM FOR FLIGHT;So;0;ON;;;;;N;;;;;
+1D337;TETRAGRAM FOR VASTNESS OR WASTING;So;0;ON;;;;;N;;;;;
+1D338;TETRAGRAM FOR CONSTANCY;So;0;ON;;;;;N;;;;;
+1D339;TETRAGRAM FOR MEASURE;So;0;ON;;;;;N;;;;;
+1D33A;TETRAGRAM FOR ETERNITY;So;0;ON;;;;;N;;;;;
+1D33B;TETRAGRAM FOR UNITY;So;0;ON;;;;;N;;;;;
+1D33C;TETRAGRAM FOR DIMINISHMENT;So;0;ON;;;;;N;;;;;
+1D33D;TETRAGRAM FOR CLOSED MOUTH;So;0;ON;;;;;N;;;;;
+1D33E;TETRAGRAM FOR GUARDEDNESS;So;0;ON;;;;;N;;;;;
+1D33F;TETRAGRAM FOR GATHERING IN;So;0;ON;;;;;N;;;;;
+1D340;TETRAGRAM FOR MASSING;So;0;ON;;;;;N;;;;;
+1D341;TETRAGRAM FOR ACCUMULATION;So;0;ON;;;;;N;;;;;
+1D342;TETRAGRAM FOR EMBELLISHMENT;So;0;ON;;;;;N;;;;;
+1D343;TETRAGRAM FOR DOUBT;So;0;ON;;;;;N;;;;;
+1D344;TETRAGRAM FOR WATCH;So;0;ON;;;;;N;;;;;
+1D345;TETRAGRAM FOR SINKING;So;0;ON;;;;;N;;;;;
+1D346;TETRAGRAM FOR INNER;So;0;ON;;;;;N;;;;;
+1D347;TETRAGRAM FOR DEPARTURE;So;0;ON;;;;;N;;;;;
+1D348;TETRAGRAM FOR DARKENING;So;0;ON;;;;;N;;;;;
+1D349;TETRAGRAM FOR DIMMING;So;0;ON;;;;;N;;;;;
+1D34A;TETRAGRAM FOR EXHAUSTION;So;0;ON;;;;;N;;;;;
+1D34B;TETRAGRAM FOR SEVERANCE;So;0;ON;;;;;N;;;;;
+1D34C;TETRAGRAM FOR STOPPAGE;So;0;ON;;;;;N;;;;;
+1D34D;TETRAGRAM FOR HARDNESS;So;0;ON;;;;;N;;;;;
+1D34E;TETRAGRAM FOR COMPLETION;So;0;ON;;;;;N;;;;;
+1D34F;TETRAGRAM FOR CLOSURE;So;0;ON;;;;;N;;;;;
+1D350;TETRAGRAM FOR FAILURE;So;0;ON;;;;;N;;;;;
+1D351;TETRAGRAM FOR AGGRAVATION;So;0;ON;;;;;N;;;;;
+1D352;TETRAGRAM FOR COMPLIANCE;So;0;ON;;;;;N;;;;;
+1D353;TETRAGRAM FOR ON THE VERGE;So;0;ON;;;;;N;;;;;
+1D354;TETRAGRAM FOR DIFFICULTIES;So;0;ON;;;;;N;;;;;
+1D355;TETRAGRAM FOR LABOURING;So;0;ON;;;;;N;;;;;
+1D356;TETRAGRAM FOR FOSTERING;So;0;ON;;;;;N;;;;;
+1D360;COUNTING ROD UNIT DIGIT ONE;No;0;L;;;;1;N;;;;;
+1D361;COUNTING ROD UNIT DIGIT TWO;No;0;L;;;;2;N;;;;;
+1D362;COUNTING ROD UNIT DIGIT THREE;No;0;L;;;;3;N;;;;;
+1D363;COUNTING ROD UNIT DIGIT FOUR;No;0;L;;;;4;N;;;;;
+1D364;COUNTING ROD UNIT DIGIT FIVE;No;0;L;;;;5;N;;;;;
+1D365;COUNTING ROD UNIT DIGIT SIX;No;0;L;;;;6;N;;;;;
+1D366;COUNTING ROD UNIT DIGIT SEVEN;No;0;L;;;;7;N;;;;;
+1D367;COUNTING ROD UNIT DIGIT EIGHT;No;0;L;;;;8;N;;;;;
+1D368;COUNTING ROD UNIT DIGIT NINE;No;0;L;;;;9;N;;;;;
+1D369;COUNTING ROD TENS DIGIT ONE;No;0;L;;;;10;N;;;;;
+1D36A;COUNTING ROD TENS DIGIT TWO;No;0;L;;;;20;N;;;;;
+1D36B;COUNTING ROD TENS DIGIT THREE;No;0;L;;;;30;N;;;;;
+1D36C;COUNTING ROD TENS DIGIT FOUR;No;0;L;;;;40;N;;;;;
+1D36D;COUNTING ROD TENS DIGIT FIVE;No;0;L;;;;50;N;;;;;
+1D36E;COUNTING ROD TENS DIGIT SIX;No;0;L;;;;60;N;;;;;
+1D36F;COUNTING ROD TENS DIGIT SEVEN;No;0;L;;;;70;N;;;;;
+1D370;COUNTING ROD TENS DIGIT EIGHT;No;0;L;;;;80;N;;;;;
+1D371;COUNTING ROD TENS DIGIT NINE;No;0;L;;;;90;N;;;;;
+1D400;MATHEMATICAL BOLD CAPITAL A;Lu;0;L;<font> 0041;;;;N;;;;;
+1D401;MATHEMATICAL BOLD CAPITAL B;Lu;0;L;<font> 0042;;;;N;;;;;
+1D402;MATHEMATICAL BOLD CAPITAL C;Lu;0;L;<font> 0043;;;;N;;;;;
+1D403;MATHEMATICAL BOLD CAPITAL D;Lu;0;L;<font> 0044;;;;N;;;;;
+1D404;MATHEMATICAL BOLD CAPITAL E;Lu;0;L;<font> 0045;;;;N;;;;;
+1D405;MATHEMATICAL BOLD CAPITAL F;Lu;0;L;<font> 0046;;;;N;;;;;
+1D406;MATHEMATICAL BOLD CAPITAL G;Lu;0;L;<font> 0047;;;;N;;;;;
+1D407;MATHEMATICAL BOLD CAPITAL H;Lu;0;L;<font> 0048;;;;N;;;;;
+1D408;MATHEMATICAL BOLD CAPITAL I;Lu;0;L;<font> 0049;;;;N;;;;;
+1D409;MATHEMATICAL BOLD CAPITAL J;Lu;0;L;<font> 004A;;;;N;;;;;
+1D40A;MATHEMATICAL BOLD CAPITAL K;Lu;0;L;<font> 004B;;;;N;;;;;
+1D40B;MATHEMATICAL BOLD CAPITAL L;Lu;0;L;<font> 004C;;;;N;;;;;
+1D40C;MATHEMATICAL BOLD CAPITAL M;Lu;0;L;<font> 004D;;;;N;;;;;
+1D40D;MATHEMATICAL BOLD CAPITAL N;Lu;0;L;<font> 004E;;;;N;;;;;
+1D40E;MATHEMATICAL BOLD CAPITAL O;Lu;0;L;<font> 004F;;;;N;;;;;
+1D40F;MATHEMATICAL BOLD CAPITAL P;Lu;0;L;<font> 0050;;;;N;;;;;
+1D410;MATHEMATICAL BOLD CAPITAL Q;Lu;0;L;<font> 0051;;;;N;;;;;
+1D411;MATHEMATICAL BOLD CAPITAL R;Lu;0;L;<font> 0052;;;;N;;;;;
+1D412;MATHEMATICAL BOLD CAPITAL S;Lu;0;L;<font> 0053;;;;N;;;;;
+1D413;MATHEMATICAL BOLD CAPITAL T;Lu;0;L;<font> 0054;;;;N;;;;;
+1D414;MATHEMATICAL BOLD CAPITAL U;Lu;0;L;<font> 0055;;;;N;;;;;
+1D415;MATHEMATICAL BOLD CAPITAL V;Lu;0;L;<font> 0056;;;;N;;;;;
+1D416;MATHEMATICAL BOLD CAPITAL W;Lu;0;L;<font> 0057;;;;N;;;;;
+1D417;MATHEMATICAL BOLD CAPITAL X;Lu;0;L;<font> 0058;;;;N;;;;;
+1D418;MATHEMATICAL BOLD CAPITAL Y;Lu;0;L;<font> 0059;;;;N;;;;;
+1D419;MATHEMATICAL BOLD CAPITAL Z;Lu;0;L;<font> 005A;;;;N;;;;;
+1D41A;MATHEMATICAL BOLD SMALL A;Ll;0;L;<font> 0061;;;;N;;;;;
+1D41B;MATHEMATICAL BOLD SMALL B;Ll;0;L;<font> 0062;;;;N;;;;;
+1D41C;MATHEMATICAL BOLD SMALL C;Ll;0;L;<font> 0063;;;;N;;;;;
+1D41D;MATHEMATICAL BOLD SMALL D;Ll;0;L;<font> 0064;;;;N;;;;;
+1D41E;MATHEMATICAL BOLD SMALL E;Ll;0;L;<font> 0065;;;;N;;;;;
+1D41F;MATHEMATICAL BOLD SMALL F;Ll;0;L;<font> 0066;;;;N;;;;;
+1D420;MATHEMATICAL BOLD SMALL G;Ll;0;L;<font> 0067;;;;N;;;;;
+1D421;MATHEMATICAL BOLD SMALL H;Ll;0;L;<font> 0068;;;;N;;;;;
+1D422;MATHEMATICAL BOLD SMALL I;Ll;0;L;<font> 0069;;;;N;;;;;
+1D423;MATHEMATICAL BOLD SMALL J;Ll;0;L;<font> 006A;;;;N;;;;;
+1D424;MATHEMATICAL BOLD SMALL K;Ll;0;L;<font> 006B;;;;N;;;;;
+1D425;MATHEMATICAL BOLD SMALL L;Ll;0;L;<font> 006C;;;;N;;;;;
+1D426;MATHEMATICAL BOLD SMALL M;Ll;0;L;<font> 006D;;;;N;;;;;
+1D427;MATHEMATICAL BOLD SMALL N;Ll;0;L;<font> 006E;;;;N;;;;;
+1D428;MATHEMATICAL BOLD SMALL O;Ll;0;L;<font> 006F;;;;N;;;;;
+1D429;MATHEMATICAL BOLD SMALL P;Ll;0;L;<font> 0070;;;;N;;;;;
+1D42A;MATHEMATICAL BOLD SMALL Q;Ll;0;L;<font> 0071;;;;N;;;;;
+1D42B;MATHEMATICAL BOLD SMALL R;Ll;0;L;<font> 0072;;;;N;;;;;
+1D42C;MATHEMATICAL BOLD SMALL S;Ll;0;L;<font> 0073;;;;N;;;;;
+1D42D;MATHEMATICAL BOLD SMALL T;Ll;0;L;<font> 0074;;;;N;;;;;
+1D42E;MATHEMATICAL BOLD SMALL U;Ll;0;L;<font> 0075;;;;N;;;;;
+1D42F;MATHEMATICAL BOLD SMALL V;Ll;0;L;<font> 0076;;;;N;;;;;
+1D430;MATHEMATICAL BOLD SMALL W;Ll;0;L;<font> 0077;;;;N;;;;;
+1D431;MATHEMATICAL BOLD SMALL X;Ll;0;L;<font> 0078;;;;N;;;;;
+1D432;MATHEMATICAL BOLD SMALL Y;Ll;0;L;<font> 0079;;;;N;;;;;
+1D433;MATHEMATICAL BOLD SMALL Z;Ll;0;L;<font> 007A;;;;N;;;;;
+1D434;MATHEMATICAL ITALIC CAPITAL A;Lu;0;L;<font> 0041;;;;N;;;;;
+1D435;MATHEMATICAL ITALIC CAPITAL B;Lu;0;L;<font> 0042;;;;N;;;;;
+1D436;MATHEMATICAL ITALIC CAPITAL C;Lu;0;L;<font> 0043;;;;N;;;;;
+1D437;MATHEMATICAL ITALIC CAPITAL D;Lu;0;L;<font> 0044;;;;N;;;;;
+1D438;MATHEMATICAL ITALIC CAPITAL E;Lu;0;L;<font> 0045;;;;N;;;;;
+1D439;MATHEMATICAL ITALIC CAPITAL F;Lu;0;L;<font> 0046;;;;N;;;;;
+1D43A;MATHEMATICAL ITALIC CAPITAL G;Lu;0;L;<font> 0047;;;;N;;;;;
+1D43B;MATHEMATICAL ITALIC CAPITAL H;Lu;0;L;<font> 0048;;;;N;;;;;
+1D43C;MATHEMATICAL ITALIC CAPITAL I;Lu;0;L;<font> 0049;;;;N;;;;;
+1D43D;MATHEMATICAL ITALIC CAPITAL J;Lu;0;L;<font> 004A;;;;N;;;;;
+1D43E;MATHEMATICAL ITALIC CAPITAL K;Lu;0;L;<font> 004B;;;;N;;;;;
+1D43F;MATHEMATICAL ITALIC CAPITAL L;Lu;0;L;<font> 004C;;;;N;;;;;
+1D440;MATHEMATICAL ITALIC CAPITAL M;Lu;0;L;<font> 004D;;;;N;;;;;
+1D441;MATHEMATICAL ITALIC CAPITAL N;Lu;0;L;<font> 004E;;;;N;;;;;
+1D442;MATHEMATICAL ITALIC CAPITAL O;Lu;0;L;<font> 004F;;;;N;;;;;
+1D443;MATHEMATICAL ITALIC CAPITAL P;Lu;0;L;<font> 0050;;;;N;;;;;
+1D444;MATHEMATICAL ITALIC CAPITAL Q;Lu;0;L;<font> 0051;;;;N;;;;;
+1D445;MATHEMATICAL ITALIC CAPITAL R;Lu;0;L;<font> 0052;;;;N;;;;;
+1D446;MATHEMATICAL ITALIC CAPITAL S;Lu;0;L;<font> 0053;;;;N;;;;;
+1D447;MATHEMATICAL ITALIC CAPITAL T;Lu;0;L;<font> 0054;;;;N;;;;;
+1D448;MATHEMATICAL ITALIC CAPITAL U;Lu;0;L;<font> 0055;;;;N;;;;;
+1D449;MATHEMATICAL ITALIC CAPITAL V;Lu;0;L;<font> 0056;;;;N;;;;;
+1D44A;MATHEMATICAL ITALIC CAPITAL W;Lu;0;L;<font> 0057;;;;N;;;;;
+1D44B;MATHEMATICAL ITALIC CAPITAL X;Lu;0;L;<font> 0058;;;;N;;;;;
+1D44C;MATHEMATICAL ITALIC CAPITAL Y;Lu;0;L;<font> 0059;;;;N;;;;;
+1D44D;MATHEMATICAL ITALIC CAPITAL Z;Lu;0;L;<font> 005A;;;;N;;;;;
+1D44E;MATHEMATICAL ITALIC SMALL A;Ll;0;L;<font> 0061;;;;N;;;;;
+1D44F;MATHEMATICAL ITALIC SMALL B;Ll;0;L;<font> 0062;;;;N;;;;;
+1D450;MATHEMATICAL ITALIC SMALL C;Ll;0;L;<font> 0063;;;;N;;;;;
+1D451;MATHEMATICAL ITALIC SMALL D;Ll;0;L;<font> 0064;;;;N;;;;;
+1D452;MATHEMATICAL ITALIC SMALL E;Ll;0;L;<font> 0065;;;;N;;;;;
+1D453;MATHEMATICAL ITALIC SMALL F;Ll;0;L;<font> 0066;;;;N;;;;;
+1D454;MATHEMATICAL ITALIC SMALL G;Ll;0;L;<font> 0067;;;;N;;;;;
+1D456;MATHEMATICAL ITALIC SMALL I;Ll;0;L;<font> 0069;;;;N;;;;;
+1D457;MATHEMATICAL ITALIC SMALL J;Ll;0;L;<font> 006A;;;;N;;;;;
+1D458;MATHEMATICAL ITALIC SMALL K;Ll;0;L;<font> 006B;;;;N;;;;;
+1D459;MATHEMATICAL ITALIC SMALL L;Ll;0;L;<font> 006C;;;;N;;;;;
+1D45A;MATHEMATICAL ITALIC SMALL M;Ll;0;L;<font> 006D;;;;N;;;;;
+1D45B;MATHEMATICAL ITALIC SMALL N;Ll;0;L;<font> 006E;;;;N;;;;;
+1D45C;MATHEMATICAL ITALIC SMALL O;Ll;0;L;<font> 006F;;;;N;;;;;
+1D45D;MATHEMATICAL ITALIC SMALL P;Ll;0;L;<font> 0070;;;;N;;;;;
+1D45E;MATHEMATICAL ITALIC SMALL Q;Ll;0;L;<font> 0071;;;;N;;;;;
+1D45F;MATHEMATICAL ITALIC SMALL R;Ll;0;L;<font> 0072;;;;N;;;;;
+1D460;MATHEMATICAL ITALIC SMALL S;Ll;0;L;<font> 0073;;;;N;;;;;
+1D461;MATHEMATICAL ITALIC SMALL T;Ll;0;L;<font> 0074;;;;N;;;;;
+1D462;MATHEMATICAL ITALIC SMALL U;Ll;0;L;<font> 0075;;;;N;;;;;
+1D463;MATHEMATICAL ITALIC SMALL V;Ll;0;L;<font> 0076;;;;N;;;;;
+1D464;MATHEMATICAL ITALIC SMALL W;Ll;0;L;<font> 0077;;;;N;;;;;
+1D465;MATHEMATICAL ITALIC SMALL X;Ll;0;L;<font> 0078;;;;N;;;;;
+1D466;MATHEMATICAL ITALIC SMALL Y;Ll;0;L;<font> 0079;;;;N;;;;;
+1D467;MATHEMATICAL ITALIC SMALL Z;Ll;0;L;<font> 007A;;;;N;;;;;
+1D468;MATHEMATICAL BOLD ITALIC CAPITAL A;Lu;0;L;<font> 0041;;;;N;;;;;
+1D469;MATHEMATICAL BOLD ITALIC CAPITAL B;Lu;0;L;<font> 0042;;;;N;;;;;
+1D46A;MATHEMATICAL BOLD ITALIC CAPITAL C;Lu;0;L;<font> 0043;;;;N;;;;;
+1D46B;MATHEMATICAL BOLD ITALIC CAPITAL D;Lu;0;L;<font> 0044;;;;N;;;;;
+1D46C;MATHEMATICAL BOLD ITALIC CAPITAL E;Lu;0;L;<font> 0045;;;;N;;;;;
+1D46D;MATHEMATICAL BOLD ITALIC CAPITAL F;Lu;0;L;<font> 0046;;;;N;;;;;
+1D46E;MATHEMATICAL BOLD ITALIC CAPITAL G;Lu;0;L;<font> 0047;;;;N;;;;;
+1D46F;MATHEMATICAL BOLD ITALIC CAPITAL H;Lu;0;L;<font> 0048;;;;N;;;;;
+1D470;MATHEMATICAL BOLD ITALIC CAPITAL I;Lu;0;L;<font> 0049;;;;N;;;;;
+1D471;MATHEMATICAL BOLD ITALIC CAPITAL J;Lu;0;L;<font> 004A;;;;N;;;;;
+1D472;MATHEMATICAL BOLD ITALIC CAPITAL K;Lu;0;L;<font> 004B;;;;N;;;;;
+1D473;MATHEMATICAL BOLD ITALIC CAPITAL L;Lu;0;L;<font> 004C;;;;N;;;;;
+1D474;MATHEMATICAL BOLD ITALIC CAPITAL M;Lu;0;L;<font> 004D;;;;N;;;;;
+1D475;MATHEMATICAL BOLD ITALIC CAPITAL N;Lu;0;L;<font> 004E;;;;N;;;;;
+1D476;MATHEMATICAL BOLD ITALIC CAPITAL O;Lu;0;L;<font> 004F;;;;N;;;;;
+1D477;MATHEMATICAL BOLD ITALIC CAPITAL P;Lu;0;L;<font> 0050;;;;N;;;;;
+1D478;MATHEMATICAL BOLD ITALIC CAPITAL Q;Lu;0;L;<font> 0051;;;;N;;;;;
+1D479;MATHEMATICAL BOLD ITALIC CAPITAL R;Lu;0;L;<font> 0052;;;;N;;;;;
+1D47A;MATHEMATICAL BOLD ITALIC CAPITAL S;Lu;0;L;<font> 0053;;;;N;;;;;
+1D47B;MATHEMATICAL BOLD ITALIC CAPITAL T;Lu;0;L;<font> 0054;;;;N;;;;;
+1D47C;MATHEMATICAL BOLD ITALIC CAPITAL U;Lu;0;L;<font> 0055;;;;N;;;;;
+1D47D;MATHEMATICAL BOLD ITALIC CAPITAL V;Lu;0;L;<font> 0056;;;;N;;;;;
+1D47E;MATHEMATICAL BOLD ITALIC CAPITAL W;Lu;0;L;<font> 0057;;;;N;;;;;
+1D47F;MATHEMATICAL BOLD ITALIC CAPITAL X;Lu;0;L;<font> 0058;;;;N;;;;;
+1D480;MATHEMATICAL BOLD ITALIC CAPITAL Y;Lu;0;L;<font> 0059;;;;N;;;;;
+1D481;MATHEMATICAL BOLD ITALIC CAPITAL Z;Lu;0;L;<font> 005A;;;;N;;;;;
+1D482;MATHEMATICAL BOLD ITALIC SMALL A;Ll;0;L;<font> 0061;;;;N;;;;;
+1D483;MATHEMATICAL BOLD ITALIC SMALL B;Ll;0;L;<font> 0062;;;;N;;;;;
+1D484;MATHEMATICAL BOLD ITALIC SMALL C;Ll;0;L;<font> 0063;;;;N;;;;;
+1D485;MATHEMATICAL BOLD ITALIC SMALL D;Ll;0;L;<font> 0064;;;;N;;;;;
+1D486;MATHEMATICAL BOLD ITALIC SMALL E;Ll;0;L;<font> 0065;;;;N;;;;;
+1D487;MATHEMATICAL BOLD ITALIC SMALL F;Ll;0;L;<font> 0066;;;;N;;;;;
+1D488;MATHEMATICAL BOLD ITALIC SMALL G;Ll;0;L;<font> 0067;;;;N;;;;;
+1D489;MATHEMATICAL BOLD ITALIC SMALL H;Ll;0;L;<font> 0068;;;;N;;;;;
+1D48A;MATHEMATICAL BOLD ITALIC SMALL I;Ll;0;L;<font> 0069;;;;N;;;;;
+1D48B;MATHEMATICAL BOLD ITALIC SMALL J;Ll;0;L;<font> 006A;;;;N;;;;;
+1D48C;MATHEMATICAL BOLD ITALIC SMALL K;Ll;0;L;<font> 006B;;;;N;;;;;
+1D48D;MATHEMATICAL BOLD ITALIC SMALL L;Ll;0;L;<font> 006C;;;;N;;;;;
+1D48E;MATHEMATICAL BOLD ITALIC SMALL M;Ll;0;L;<font> 006D;;;;N;;;;;
+1D48F;MATHEMATICAL BOLD ITALIC SMALL N;Ll;0;L;<font> 006E;;;;N;;;;;
+1D490;MATHEMATICAL BOLD ITALIC SMALL O;Ll;0;L;<font> 006F;;;;N;;;;;
+1D491;MATHEMATICAL BOLD ITALIC SMALL P;Ll;0;L;<font> 0070;;;;N;;;;;
+1D492;MATHEMATICAL BOLD ITALIC SMALL Q;Ll;0;L;<font> 0071;;;;N;;;;;
+1D493;MATHEMATICAL BOLD ITALIC SMALL R;Ll;0;L;<font> 0072;;;;N;;;;;
+1D494;MATHEMATICAL BOLD ITALIC SMALL S;Ll;0;L;<font> 0073;;;;N;;;;;
+1D495;MATHEMATICAL BOLD ITALIC SMALL T;Ll;0;L;<font> 0074;;;;N;;;;;
+1D496;MATHEMATICAL BOLD ITALIC SMALL U;Ll;0;L;<font> 0075;;;;N;;;;;
+1D497;MATHEMATICAL BOLD ITALIC SMALL V;Ll;0;L;<font> 0076;;;;N;;;;;
+1D498;MATHEMATICAL BOLD ITALIC SMALL W;Ll;0;L;<font> 0077;;;;N;;;;;
+1D499;MATHEMATICAL BOLD ITALIC SMALL X;Ll;0;L;<font> 0078;;;;N;;;;;
+1D49A;MATHEMATICAL BOLD ITALIC SMALL Y;Ll;0;L;<font> 0079;;;;N;;;;;
+1D49B;MATHEMATICAL BOLD ITALIC SMALL Z;Ll;0;L;<font> 007A;;;;N;;;;;
+1D49C;MATHEMATICAL SCRIPT CAPITAL A;Lu;0;L;<font> 0041;;;;N;;;;;
+1D49E;MATHEMATICAL SCRIPT CAPITAL C;Lu;0;L;<font> 0043;;;;N;;;;;
+1D49F;MATHEMATICAL SCRIPT CAPITAL D;Lu;0;L;<font> 0044;;;;N;;;;;
+1D4A2;MATHEMATICAL SCRIPT CAPITAL G;Lu;0;L;<font> 0047;;;;N;;;;;
+1D4A5;MATHEMATICAL SCRIPT CAPITAL J;Lu;0;L;<font> 004A;;;;N;;;;;
+1D4A6;MATHEMATICAL SCRIPT CAPITAL K;Lu;0;L;<font> 004B;;;;N;;;;;
+1D4A9;MATHEMATICAL SCRIPT CAPITAL N;Lu;0;L;<font> 004E;;;;N;;;;;
+1D4AA;MATHEMATICAL SCRIPT CAPITAL O;Lu;0;L;<font> 004F;;;;N;;;;;
+1D4AB;MATHEMATICAL SCRIPT CAPITAL P;Lu;0;L;<font> 0050;;;;N;;;;;
+1D4AC;MATHEMATICAL SCRIPT CAPITAL Q;Lu;0;L;<font> 0051;;;;N;;;;;
+1D4AE;MATHEMATICAL SCRIPT CAPITAL S;Lu;0;L;<font> 0053;;;;N;;;;;
+1D4AF;MATHEMATICAL SCRIPT CAPITAL T;Lu;0;L;<font> 0054;;;;N;;;;;
+1D4B0;MATHEMATICAL SCRIPT CAPITAL U;Lu;0;L;<font> 0055;;;;N;;;;;
+1D4B1;MATHEMATICAL SCRIPT CAPITAL V;Lu;0;L;<font> 0056;;;;N;;;;;
+1D4B2;MATHEMATICAL SCRIPT CAPITAL W;Lu;0;L;<font> 0057;;;;N;;;;;
+1D4B3;MATHEMATICAL SCRIPT CAPITAL X;Lu;0;L;<font> 0058;;;;N;;;;;
+1D4B4;MATHEMATICAL SCRIPT CAPITAL Y;Lu;0;L;<font> 0059;;;;N;;;;;
+1D4B5;MATHEMATICAL SCRIPT CAPITAL Z;Lu;0;L;<font> 005A;;;;N;;;;;
+1D4B6;MATHEMATICAL SCRIPT SMALL A;Ll;0;L;<font> 0061;;;;N;;;;;
+1D4B7;MATHEMATICAL SCRIPT SMALL B;Ll;0;L;<font> 0062;;;;N;;;;;
+1D4B8;MATHEMATICAL SCRIPT SMALL C;Ll;0;L;<font> 0063;;;;N;;;;;
+1D4B9;MATHEMATICAL SCRIPT SMALL D;Ll;0;L;<font> 0064;;;;N;;;;;
+1D4BB;MATHEMATICAL SCRIPT SMALL F;Ll;0;L;<font> 0066;;;;N;;;;;
+1D4BD;MATHEMATICAL SCRIPT SMALL H;Ll;0;L;<font> 0068;;;;N;;;;;
+1D4BE;MATHEMATICAL SCRIPT SMALL I;Ll;0;L;<font> 0069;;;;N;;;;;
+1D4BF;MATHEMATICAL SCRIPT SMALL J;Ll;0;L;<font> 006A;;;;N;;;;;
+1D4C0;MATHEMATICAL SCRIPT SMALL K;Ll;0;L;<font> 006B;;;;N;;;;;
+1D4C1;MATHEMATICAL SCRIPT SMALL L;Ll;0;L;<font> 006C;;;;N;;;;;
+1D4C2;MATHEMATICAL SCRIPT SMALL M;Ll;0;L;<font> 006D;;;;N;;;;;
+1D4C3;MATHEMATICAL SCRIPT SMALL N;Ll;0;L;<font> 006E;;;;N;;;;;
+1D4C5;MATHEMATICAL SCRIPT SMALL P;Ll;0;L;<font> 0070;;;;N;;;;;
+1D4C6;MATHEMATICAL SCRIPT SMALL Q;Ll;0;L;<font> 0071;;;;N;;;;;
+1D4C7;MATHEMATICAL SCRIPT SMALL R;Ll;0;L;<font> 0072;;;;N;;;;;
+1D4C8;MATHEMATICAL SCRIPT SMALL S;Ll;0;L;<font> 0073;;;;N;;;;;
+1D4C9;MATHEMATICAL SCRIPT SMALL T;Ll;0;L;<font> 0074;;;;N;;;;;
+1D4CA;MATHEMATICAL SCRIPT SMALL U;Ll;0;L;<font> 0075;;;;N;;;;;
+1D4CB;MATHEMATICAL SCRIPT SMALL V;Ll;0;L;<font> 0076;;;;N;;;;;
+1D4CC;MATHEMATICAL SCRIPT SMALL W;Ll;0;L;<font> 0077;;;;N;;;;;
+1D4CD;MATHEMATICAL SCRIPT SMALL X;Ll;0;L;<font> 0078;;;;N;;;;;
+1D4CE;MATHEMATICAL SCRIPT SMALL Y;Ll;0;L;<font> 0079;;;;N;;;;;
+1D4CF;MATHEMATICAL SCRIPT SMALL Z;Ll;0;L;<font> 007A;;;;N;;;;;
+1D4D0;MATHEMATICAL BOLD SCRIPT CAPITAL A;Lu;0;L;<font> 0041;;;;N;;;;;
+1D4D1;MATHEMATICAL BOLD SCRIPT CAPITAL B;Lu;0;L;<font> 0042;;;;N;;;;;
+1D4D2;MATHEMATICAL BOLD SCRIPT CAPITAL C;Lu;0;L;<font> 0043;;;;N;;;;;
+1D4D3;MATHEMATICAL BOLD SCRIPT CAPITAL D;Lu;0;L;<font> 0044;;;;N;;;;;
+1D4D4;MATHEMATICAL BOLD SCRIPT CAPITAL E;Lu;0;L;<font> 0045;;;;N;;;;;
+1D4D5;MATHEMATICAL BOLD SCRIPT CAPITAL F;Lu;0;L;<font> 0046;;;;N;;;;;
+1D4D6;MATHEMATICAL BOLD SCRIPT CAPITAL G;Lu;0;L;<font> 0047;;;;N;;;;;
+1D4D7;MATHEMATICAL BOLD SCRIPT CAPITAL H;Lu;0;L;<font> 0048;;;;N;;;;;
+1D4D8;MATHEMATICAL BOLD SCRIPT CAPITAL I;Lu;0;L;<font> 0049;;;;N;;;;;
+1D4D9;MATHEMATICAL BOLD SCRIPT CAPITAL J;Lu;0;L;<font> 004A;;;;N;;;;;
+1D4DA;MATHEMATICAL BOLD SCRIPT CAPITAL K;Lu;0;L;<font> 004B;;;;N;;;;;
+1D4DB;MATHEMATICAL BOLD SCRIPT CAPITAL L;Lu;0;L;<font> 004C;;;;N;;;;;
+1D4DC;MATHEMATICAL BOLD SCRIPT CAPITAL M;Lu;0;L;<font> 004D;;;;N;;;;;
+1D4DD;MATHEMATICAL BOLD SCRIPT CAPITAL N;Lu;0;L;<font> 004E;;;;N;;;;;
+1D4DE;MATHEMATICAL BOLD SCRIPT CAPITAL O;Lu;0;L;<font> 004F;;;;N;;;;;
+1D4DF;MATHEMATICAL BOLD SCRIPT CAPITAL P;Lu;0;L;<font> 0050;;;;N;;;;;
+1D4E0;MATHEMATICAL BOLD SCRIPT CAPITAL Q;Lu;0;L;<font> 0051;;;;N;;;;;
+1D4E1;MATHEMATICAL BOLD SCRIPT CAPITAL R;Lu;0;L;<font> 0052;;;;N;;;;;
+1D4E2;MATHEMATICAL BOLD SCRIPT CAPITAL S;Lu;0;L;<font> 0053;;;;N;;;;;
+1D4E3;MATHEMATICAL BOLD SCRIPT CAPITAL T;Lu;0;L;<font> 0054;;;;N;;;;;
+1D4E4;MATHEMATICAL BOLD SCRIPT CAPITAL U;Lu;0;L;<font> 0055;;;;N;;;;;
+1D4E5;MATHEMATICAL BOLD SCRIPT CAPITAL V;Lu;0;L;<font> 0056;;;;N;;;;;
+1D4E6;MATHEMATICAL BOLD SCRIPT CAPITAL W;Lu;0;L;<font> 0057;;;;N;;;;;
+1D4E7;MATHEMATICAL BOLD SCRIPT CAPITAL X;Lu;0;L;<font> 0058;;;;N;;;;;
+1D4E8;MATHEMATICAL BOLD SCRIPT CAPITAL Y;Lu;0;L;<font> 0059;;;;N;;;;;
+1D4E9;MATHEMATICAL BOLD SCRIPT CAPITAL Z;Lu;0;L;<font> 005A;;;;N;;;;;
+1D4EA;MATHEMATICAL BOLD SCRIPT SMALL A;Ll;0;L;<font> 0061;;;;N;;;;;
+1D4EB;MATHEMATICAL BOLD SCRIPT SMALL B;Ll;0;L;<font> 0062;;;;N;;;;;
+1D4EC;MATHEMATICAL BOLD SCRIPT SMALL C;Ll;0;L;<font> 0063;;;;N;;;;;
+1D4ED;MATHEMATICAL BOLD SCRIPT SMALL D;Ll;0;L;<font> 0064;;;;N;;;;;
+1D4EE;MATHEMATICAL BOLD SCRIPT SMALL E;Ll;0;L;<font> 0065;;;;N;;;;;
+1D4EF;MATHEMATICAL BOLD SCRIPT SMALL F;Ll;0;L;<font> 0066;;;;N;;;;;
+1D4F0;MATHEMATICAL BOLD SCRIPT SMALL G;Ll;0;L;<font> 0067;;;;N;;;;;
+1D4F1;MATHEMATICAL BOLD SCRIPT SMALL H;Ll;0;L;<font> 0068;;;;N;;;;;
+1D4F2;MATHEMATICAL BOLD SCRIPT SMALL I;Ll;0;L;<font> 0069;;;;N;;;;;
+1D4F3;MATHEMATICAL BOLD SCRIPT SMALL J;Ll;0;L;<font> 006A;;;;N;;;;;
+1D4F4;MATHEMATICAL BOLD SCRIPT SMALL K;Ll;0;L;<font> 006B;;;;N;;;;;
+1D4F5;MATHEMATICAL BOLD SCRIPT SMALL L;Ll;0;L;<font> 006C;;;;N;;;;;
+1D4F6;MATHEMATICAL BOLD SCRIPT SMALL M;Ll;0;L;<font> 006D;;;;N;;;;;
+1D4F7;MATHEMATICAL BOLD SCRIPT SMALL N;Ll;0;L;<font> 006E;;;;N;;;;;
+1D4F8;MATHEMATICAL BOLD SCRIPT SMALL O;Ll;0;L;<font> 006F;;;;N;;;;;
+1D4F9;MATHEMATICAL BOLD SCRIPT SMALL P;Ll;0;L;<font> 0070;;;;N;;;;;
+1D4FA;MATHEMATICAL BOLD SCRIPT SMALL Q;Ll;0;L;<font> 0071;;;;N;;;;;
+1D4FB;MATHEMATICAL BOLD SCRIPT SMALL R;Ll;0;L;<font> 0072;;;;N;;;;;
+1D4FC;MATHEMATICAL BOLD SCRIPT SMALL S;Ll;0;L;<font> 0073;;;;N;;;;;
+1D4FD;MATHEMATICAL BOLD SCRIPT SMALL T;Ll;0;L;<font> 0074;;;;N;;;;;
+1D4FE;MATHEMATICAL BOLD SCRIPT SMALL U;Ll;0;L;<font> 0075;;;;N;;;;;
+1D4FF;MATHEMATICAL BOLD SCRIPT SMALL V;Ll;0;L;<font> 0076;;;;N;;;;;
+1D500;MATHEMATICAL BOLD SCRIPT SMALL W;Ll;0;L;<font> 0077;;;;N;;;;;
+1D501;MATHEMATICAL BOLD SCRIPT SMALL X;Ll;0;L;<font> 0078;;;;N;;;;;
+1D502;MATHEMATICAL BOLD SCRIPT SMALL Y;Ll;0;L;<font> 0079;;;;N;;;;;
+1D503;MATHEMATICAL BOLD SCRIPT SMALL Z;Ll;0;L;<font> 007A;;;;N;;;;;
+1D504;MATHEMATICAL FRAKTUR CAPITAL A;Lu;0;L;<font> 0041;;;;N;;;;;
+1D505;MATHEMATICAL FRAKTUR CAPITAL B;Lu;0;L;<font> 0042;;;;N;;;;;
+1D507;MATHEMATICAL FRAKTUR CAPITAL D;Lu;0;L;<font> 0044;;;;N;;;;;
+1D508;MATHEMATICAL FRAKTUR CAPITAL E;Lu;0;L;<font> 0045;;;;N;;;;;
+1D509;MATHEMATICAL FRAKTUR CAPITAL F;Lu;0;L;<font> 0046;;;;N;;;;;
+1D50A;MATHEMATICAL FRAKTUR CAPITAL G;Lu;0;L;<font> 0047;;;;N;;;;;
+1D50D;MATHEMATICAL FRAKTUR CAPITAL J;Lu;0;L;<font> 004A;;;;N;;;;;
+1D50E;MATHEMATICAL FRAKTUR CAPITAL K;Lu;0;L;<font> 004B;;;;N;;;;;
+1D50F;MATHEMATICAL FRAKTUR CAPITAL L;Lu;0;L;<font> 004C;;;;N;;;;;
+1D510;MATHEMATICAL FRAKTUR CAPITAL M;Lu;0;L;<font> 004D;;;;N;;;;;
+1D511;MATHEMATICAL FRAKTUR CAPITAL N;Lu;0;L;<font> 004E;;;;N;;;;;
+1D512;MATHEMATICAL FRAKTUR CAPITAL O;Lu;0;L;<font> 004F;;;;N;;;;;
+1D513;MATHEMATICAL FRAKTUR CAPITAL P;Lu;0;L;<font> 0050;;;;N;;;;;
+1D514;MATHEMATICAL FRAKTUR CAPITAL Q;Lu;0;L;<font> 0051;;;;N;;;;;
+1D516;MATHEMATICAL FRAKTUR CAPITAL S;Lu;0;L;<font> 0053;;;;N;;;;;
+1D517;MATHEMATICAL FRAKTUR CAPITAL T;Lu;0;L;<font> 0054;;;;N;;;;;
+1D518;MATHEMATICAL FRAKTUR CAPITAL U;Lu;0;L;<font> 0055;;;;N;;;;;
+1D519;MATHEMATICAL FRAKTUR CAPITAL V;Lu;0;L;<font> 0056;;;;N;;;;;
+1D51A;MATHEMATICAL FRAKTUR CAPITAL W;Lu;0;L;<font> 0057;;;;N;;;;;
+1D51B;MATHEMATICAL FRAKTUR CAPITAL X;Lu;0;L;<font> 0058;;;;N;;;;;
+1D51C;MATHEMATICAL FRAKTUR CAPITAL Y;Lu;0;L;<font> 0059;;;;N;;;;;
+1D51E;MATHEMATICAL FRAKTUR SMALL A;Ll;0;L;<font> 0061;;;;N;;;;;
+1D51F;MATHEMATICAL FRAKTUR SMALL B;Ll;0;L;<font> 0062;;;;N;;;;;
+1D520;MATHEMATICAL FRAKTUR SMALL C;Ll;0;L;<font> 0063;;;;N;;;;;
+1D521;MATHEMATICAL FRAKTUR SMALL D;Ll;0;L;<font> 0064;;;;N;;;;;
+1D522;MATHEMATICAL FRAKTUR SMALL E;Ll;0;L;<font> 0065;;;;N;;;;;
+1D523;MATHEMATICAL FRAKTUR SMALL F;Ll;0;L;<font> 0066;;;;N;;;;;
+1D524;MATHEMATICAL FRAKTUR SMALL G;Ll;0;L;<font> 0067;;;;N;;;;;
+1D525;MATHEMATICAL FRAKTUR SMALL H;Ll;0;L;<font> 0068;;;;N;;;;;
+1D526;MATHEMATICAL FRAKTUR SMALL I;Ll;0;L;<font> 0069;;;;N;;;;;
+1D527;MATHEMATICAL FRAKTUR SMALL J;Ll;0;L;<font> 006A;;;;N;;;;;
+1D528;MATHEMATICAL FRAKTUR SMALL K;Ll;0;L;<font> 006B;;;;N;;;;;
+1D529;MATHEMATICAL FRAKTUR SMALL L;Ll;0;L;<font> 006C;;;;N;;;;;
+1D52A;MATHEMATICAL FRAKTUR SMALL M;Ll;0;L;<font> 006D;;;;N;;;;;
+1D52B;MATHEMATICAL FRAKTUR SMALL N;Ll;0;L;<font> 006E;;;;N;;;;;
+1D52C;MATHEMATICAL FRAKTUR SMALL O;Ll;0;L;<font> 006F;;;;N;;;;;
+1D52D;MATHEMATICAL FRAKTUR SMALL P;Ll;0;L;<font> 0070;;;;N;;;;;
+1D52E;MATHEMATICAL FRAKTUR SMALL Q;Ll;0;L;<font> 0071;;;;N;;;;;
+1D52F;MATHEMATICAL FRAKTUR SMALL R;Ll;0;L;<font> 0072;;;;N;;;;;
+1D530;MATHEMATICAL FRAKTUR SMALL S;Ll;0;L;<font> 0073;;;;N;;;;;
+1D531;MATHEMATICAL FRAKTUR SMALL T;Ll;0;L;<font> 0074;;;;N;;;;;
+1D532;MATHEMATICAL FRAKTUR SMALL U;Ll;0;L;<font> 0075;;;;N;;;;;
+1D533;MATHEMATICAL FRAKTUR SMALL V;Ll;0;L;<font> 0076;;;;N;;;;;
+1D534;MATHEMATICAL FRAKTUR SMALL W;Ll;0;L;<font> 0077;;;;N;;;;;
+1D535;MATHEMATICAL FRAKTUR SMALL X;Ll;0;L;<font> 0078;;;;N;;;;;
+1D536;MATHEMATICAL FRAKTUR SMALL Y;Ll;0;L;<font> 0079;;;;N;;;;;
+1D537;MATHEMATICAL FRAKTUR SMALL Z;Ll;0;L;<font> 007A;;;;N;;;;;
+1D538;MATHEMATICAL DOUBLE-STRUCK CAPITAL A;Lu;0;L;<font> 0041;;;;N;;;;;
+1D539;MATHEMATICAL DOUBLE-STRUCK CAPITAL B;Lu;0;L;<font> 0042;;;;N;;;;;
+1D53B;MATHEMATICAL DOUBLE-STRUCK CAPITAL D;Lu;0;L;<font> 0044;;;;N;;;;;
+1D53C;MATHEMATICAL DOUBLE-STRUCK CAPITAL E;Lu;0;L;<font> 0045;;;;N;;;;;
+1D53D;MATHEMATICAL DOUBLE-STRUCK CAPITAL F;Lu;0;L;<font> 0046;;;;N;;;;;
+1D53E;MATHEMATICAL DOUBLE-STRUCK CAPITAL G;Lu;0;L;<font> 0047;;;;N;;;;;
+1D540;MATHEMATICAL DOUBLE-STRUCK CAPITAL I;Lu;0;L;<font> 0049;;;;N;;;;;
+1D541;MATHEMATICAL DOUBLE-STRUCK CAPITAL J;Lu;0;L;<font> 004A;;;;N;;;;;
+1D542;MATHEMATICAL DOUBLE-STRUCK CAPITAL K;Lu;0;L;<font> 004B;;;;N;;;;;
+1D543;MATHEMATICAL DOUBLE-STRUCK CAPITAL L;Lu;0;L;<font> 004C;;;;N;;;;;
+1D544;MATHEMATICAL DOUBLE-STRUCK CAPITAL M;Lu;0;L;<font> 004D;;;;N;;;;;
+1D546;MATHEMATICAL DOUBLE-STRUCK CAPITAL O;Lu;0;L;<font> 004F;;;;N;;;;;
+1D54A;MATHEMATICAL DOUBLE-STRUCK CAPITAL S;Lu;0;L;<font> 0053;;;;N;;;;;
+1D54B;MATHEMATICAL DOUBLE-STRUCK CAPITAL T;Lu;0;L;<font> 0054;;;;N;;;;;
+1D54C;MATHEMATICAL DOUBLE-STRUCK CAPITAL U;Lu;0;L;<font> 0055;;;;N;;;;;
+1D54D;MATHEMATICAL DOUBLE-STRUCK CAPITAL V;Lu;0;L;<font> 0056;;;;N;;;;;
+1D54E;MATHEMATICAL DOUBLE-STRUCK CAPITAL W;Lu;0;L;<font> 0057;;;;N;;;;;
+1D54F;MATHEMATICAL DOUBLE-STRUCK CAPITAL X;Lu;0;L;<font> 0058;;;;N;;;;;
+1D550;MATHEMATICAL DOUBLE-STRUCK CAPITAL Y;Lu;0;L;<font> 0059;;;;N;;;;;
+1D552;MATHEMATICAL DOUBLE-STRUCK SMALL A;Ll;0;L;<font> 0061;;;;N;;;;;
+1D553;MATHEMATICAL DOUBLE-STRUCK SMALL B;Ll;0;L;<font> 0062;;;;N;;;;;
+1D554;MATHEMATICAL DOUBLE-STRUCK SMALL C;Ll;0;L;<font> 0063;;;;N;;;;;
+1D555;MATHEMATICAL DOUBLE-STRUCK SMALL D;Ll;0;L;<font> 0064;;;;N;;;;;
+1D556;MATHEMATICAL DOUBLE-STRUCK SMALL E;Ll;0;L;<font> 0065;;;;N;;;;;
+1D557;MATHEMATICAL DOUBLE-STRUCK SMALL F;Ll;0;L;<font> 0066;;;;N;;;;;
+1D558;MATHEMATICAL DOUBLE-STRUCK SMALL G;Ll;0;L;<font> 0067;;;;N;;;;;
+1D559;MATHEMATICAL DOUBLE-STRUCK SMALL H;Ll;0;L;<font> 0068;;;;N;;;;;
+1D55A;MATHEMATICAL DOUBLE-STRUCK SMALL I;Ll;0;L;<font> 0069;;;;N;;;;;
+1D55B;MATHEMATICAL DOUBLE-STRUCK SMALL J;Ll;0;L;<font> 006A;;;;N;;;;;
+1D55C;MATHEMATICAL DOUBLE-STRUCK SMALL K;Ll;0;L;<font> 006B;;;;N;;;;;
+1D55D;MATHEMATICAL DOUBLE-STRUCK SMALL L;Ll;0;L;<font> 006C;;;;N;;;;;
+1D55E;MATHEMATICAL DOUBLE-STRUCK SMALL M;Ll;0;L;<font> 006D;;;;N;;;;;
+1D55F;MATHEMATICAL DOUBLE-STRUCK SMALL N;Ll;0;L;<font> 006E;;;;N;;;;;
+1D560;MATHEMATICAL DOUBLE-STRUCK SMALL O;Ll;0;L;<font> 006F;;;;N;;;;;
+1D561;MATHEMATICAL DOUBLE-STRUCK SMALL P;Ll;0;L;<font> 0070;;;;N;;;;;
+1D562;MATHEMATICAL DOUBLE-STRUCK SMALL Q;Ll;0;L;<font> 0071;;;;N;;;;;
+1D563;MATHEMATICAL DOUBLE-STRUCK SMALL R;Ll;0;L;<font> 0072;;;;N;;;;;
+1D564;MATHEMATICAL DOUBLE-STRUCK SMALL S;Ll;0;L;<font> 0073;;;;N;;;;;
+1D565;MATHEMATICAL DOUBLE-STRUCK SMALL T;Ll;0;L;<font> 0074;;;;N;;;;;
+1D566;MATHEMATICAL DOUBLE-STRUCK SMALL U;Ll;0;L;<font> 0075;;;;N;;;;;
+1D567;MATHEMATICAL DOUBLE-STRUCK SMALL V;Ll;0;L;<font> 0076;;;;N;;;;;
+1D568;MATHEMATICAL DOUBLE-STRUCK SMALL W;Ll;0;L;<font> 0077;;;;N;;;;;
+1D569;MATHEMATICAL DOUBLE-STRUCK SMALL X;Ll;0;L;<font> 0078;;;;N;;;;;
+1D56A;MATHEMATICAL DOUBLE-STRUCK SMALL Y;Ll;0;L;<font> 0079;;;;N;;;;;
+1D56B;MATHEMATICAL DOUBLE-STRUCK SMALL Z;Ll;0;L;<font> 007A;;;;N;;;;;
+1D56C;MATHEMATICAL BOLD FRAKTUR CAPITAL A;Lu;0;L;<font> 0041;;;;N;;;;;
+1D56D;MATHEMATICAL BOLD FRAKTUR CAPITAL B;Lu;0;L;<font> 0042;;;;N;;;;;
+1D56E;MATHEMATICAL BOLD FRAKTUR CAPITAL C;Lu;0;L;<font> 0043;;;;N;;;;;
+1D56F;MATHEMATICAL BOLD FRAKTUR CAPITAL D;Lu;0;L;<font> 0044;;;;N;;;;;
+1D570;MATHEMATICAL BOLD FRAKTUR CAPITAL E;Lu;0;L;<font> 0045;;;;N;;;;;
+1D571;MATHEMATICAL BOLD FRAKTUR CAPITAL F;Lu;0;L;<font> 0046;;;;N;;;;;
+1D572;MATHEMATICAL BOLD FRAKTUR CAPITAL G;Lu;0;L;<font> 0047;;;;N;;;;;
+1D573;MATHEMATICAL BOLD FRAKTUR CAPITAL H;Lu;0;L;<font> 0048;;;;N;;;;;
+1D574;MATHEMATICAL BOLD FRAKTUR CAPITAL I;Lu;0;L;<font> 0049;;;;N;;;;;
+1D575;MATHEMATICAL BOLD FRAKTUR CAPITAL J;Lu;0;L;<font> 004A;;;;N;;;;;
+1D576;MATHEMATICAL BOLD FRAKTUR CAPITAL K;Lu;0;L;<font> 004B;;;;N;;;;;
+1D577;MATHEMATICAL BOLD FRAKTUR CAPITAL L;Lu;0;L;<font> 004C;;;;N;;;;;
+1D578;MATHEMATICAL BOLD FRAKTUR CAPITAL M;Lu;0;L;<font> 004D;;;;N;;;;;
+1D579;MATHEMATICAL BOLD FRAKTUR CAPITAL N;Lu;0;L;<font> 004E;;;;N;;;;;
+1D57A;MATHEMATICAL BOLD FRAKTUR CAPITAL O;Lu;0;L;<font> 004F;;;;N;;;;;
+1D57B;MATHEMATICAL BOLD FRAKTUR CAPITAL P;Lu;0;L;<font> 0050;;;;N;;;;;
+1D57C;MATHEMATICAL BOLD FRAKTUR CAPITAL Q;Lu;0;L;<font> 0051;;;;N;;;;;
+1D57D;MATHEMATICAL BOLD FRAKTUR CAPITAL R;Lu;0;L;<font> 0052;;;;N;;;;;
+1D57E;MATHEMATICAL BOLD FRAKTUR CAPITAL S;Lu;0;L;<font> 0053;;;;N;;;;;
+1D57F;MATHEMATICAL BOLD FRAKTUR CAPITAL T;Lu;0;L;<font> 0054;;;;N;;;;;
+1D580;MATHEMATICAL BOLD FRAKTUR CAPITAL U;Lu;0;L;<font> 0055;;;;N;;;;;
+1D581;MATHEMATICAL BOLD FRAKTUR CAPITAL V;Lu;0;L;<font> 0056;;;;N;;;;;
+1D582;MATHEMATICAL BOLD FRAKTUR CAPITAL W;Lu;0;L;<font> 0057;;;;N;;;;;
+1D583;MATHEMATICAL BOLD FRAKTUR CAPITAL X;Lu;0;L;<font> 0058;;;;N;;;;;
+1D584;MATHEMATICAL BOLD FRAKTUR CAPITAL Y;Lu;0;L;<font> 0059;;;;N;;;;;
+1D585;MATHEMATICAL BOLD FRAKTUR CAPITAL Z;Lu;0;L;<font> 005A;;;;N;;;;;
+1D586;MATHEMATICAL BOLD FRAKTUR SMALL A;Ll;0;L;<font> 0061;;;;N;;;;;
+1D587;MATHEMATICAL BOLD FRAKTUR SMALL B;Ll;0;L;<font> 0062;;;;N;;;;;
+1D588;MATHEMATICAL BOLD FRAKTUR SMALL C;Ll;0;L;<font> 0063;;;;N;;;;;
+1D589;MATHEMATICAL BOLD FRAKTUR SMALL D;Ll;0;L;<font> 0064;;;;N;;;;;
+1D58A;MATHEMATICAL BOLD FRAKTUR SMALL E;Ll;0;L;<font> 0065;;;;N;;;;;
+1D58B;MATHEMATICAL BOLD FRAKTUR SMALL F;Ll;0;L;<font> 0066;;;;N;;;;;
+1D58C;MATHEMATICAL BOLD FRAKTUR SMALL G;Ll;0;L;<font> 0067;;;;N;;;;;
+1D58D;MATHEMATICAL BOLD FRAKTUR SMALL H;Ll;0;L;<font> 0068;;;;N;;;;;
+1D58E;MATHEMATICAL BOLD FRAKTUR SMALL I;Ll;0;L;<font> 0069;;;;N;;;;;
+1D58F;MATHEMATICAL BOLD FRAKTUR SMALL J;Ll;0;L;<font> 006A;;;;N;;;;;
+1D590;MATHEMATICAL BOLD FRAKTUR SMALL K;Ll;0;L;<font> 006B;;;;N;;;;;
+1D591;MATHEMATICAL BOLD FRAKTUR SMALL L;Ll;0;L;<font> 006C;;;;N;;;;;
+1D592;MATHEMATICAL BOLD FRAKTUR SMALL M;Ll;0;L;<font> 006D;;;;N;;;;;
+1D593;MATHEMATICAL BOLD FRAKTUR SMALL N;Ll;0;L;<font> 006E;;;;N;;;;;
+1D594;MATHEMATICAL BOLD FRAKTUR SMALL O;Ll;0;L;<font> 006F;;;;N;;;;;
+1D595;MATHEMATICAL BOLD FRAKTUR SMALL P;Ll;0;L;<font> 0070;;;;N;;;;;
+1D596;MATHEMATICAL BOLD FRAKTUR SMALL Q;Ll;0;L;<font> 0071;;;;N;;;;;
+1D597;MATHEMATICAL BOLD FRAKTUR SMALL R;Ll;0;L;<font> 0072;;;;N;;;;;
+1D598;MATHEMATICAL BOLD FRAKTUR SMALL S;Ll;0;L;<font> 0073;;;;N;;;;;
+1D599;MATHEMATICAL BOLD FRAKTUR SMALL T;Ll;0;L;<font> 0074;;;;N;;;;;
+1D59A;MATHEMATICAL BOLD FRAKTUR SMALL U;Ll;0;L;<font> 0075;;;;N;;;;;
+1D59B;MATHEMATICAL BOLD FRAKTUR SMALL V;Ll;0;L;<font> 0076;;;;N;;;;;
+1D59C;MATHEMATICAL BOLD FRAKTUR SMALL W;Ll;0;L;<font> 0077;;;;N;;;;;
+1D59D;MATHEMATICAL BOLD FRAKTUR SMALL X;Ll;0;L;<font> 0078;;;;N;;;;;
+1D59E;MATHEMATICAL BOLD FRAKTUR SMALL Y;Ll;0;L;<font> 0079;;;;N;;;;;
+1D59F;MATHEMATICAL BOLD FRAKTUR SMALL Z;Ll;0;L;<font> 007A;;;;N;;;;;
+1D5A0;MATHEMATICAL SANS-SERIF CAPITAL A;Lu;0;L;<font> 0041;;;;N;;;;;
+1D5A1;MATHEMATICAL SANS-SERIF CAPITAL B;Lu;0;L;<font> 0042;;;;N;;;;;
+1D5A2;MATHEMATICAL SANS-SERIF CAPITAL C;Lu;0;L;<font> 0043;;;;N;;;;;
+1D5A3;MATHEMATICAL SANS-SERIF CAPITAL D;Lu;0;L;<font> 0044;;;;N;;;;;
+1D5A4;MATHEMATICAL SANS-SERIF CAPITAL E;Lu;0;L;<font> 0045;;;;N;;;;;
+1D5A5;MATHEMATICAL SANS-SERIF CAPITAL F;Lu;0;L;<font> 0046;;;;N;;;;;
+1D5A6;MATHEMATICAL SANS-SERIF CAPITAL G;Lu;0;L;<font> 0047;;;;N;;;;;
+1D5A7;MATHEMATICAL SANS-SERIF CAPITAL H;Lu;0;L;<font> 0048;;;;N;;;;;
+1D5A8;MATHEMATICAL SANS-SERIF CAPITAL I;Lu;0;L;<font> 0049;;;;N;;;;;
+1D5A9;MATHEMATICAL SANS-SERIF CAPITAL J;Lu;0;L;<font> 004A;;;;N;;;;;
+1D5AA;MATHEMATICAL SANS-SERIF CAPITAL K;Lu;0;L;<font> 004B;;;;N;;;;;
+1D5AB;MATHEMATICAL SANS-SERIF CAPITAL L;Lu;0;L;<font> 004C;;;;N;;;;;
+1D5AC;MATHEMATICAL SANS-SERIF CAPITAL M;Lu;0;L;<font> 004D;;;;N;;;;;
+1D5AD;MATHEMATICAL SANS-SERIF CAPITAL N;Lu;0;L;<font> 004E;;;;N;;;;;
+1D5AE;MATHEMATICAL SANS-SERIF CAPITAL O;Lu;0;L;<font> 004F;;;;N;;;;;
+1D5AF;MATHEMATICAL SANS-SERIF CAPITAL P;Lu;0;L;<font> 0050;;;;N;;;;;
+1D5B0;MATHEMATICAL SANS-SERIF CAPITAL Q;Lu;0;L;<font> 0051;;;;N;;;;;
+1D5B1;MATHEMATICAL SANS-SERIF CAPITAL R;Lu;0;L;<font> 0052;;;;N;;;;;
+1D5B2;MATHEMATICAL SANS-SERIF CAPITAL S;Lu;0;L;<font> 0053;;;;N;;;;;
+1D5B3;MATHEMATICAL SANS-SERIF CAPITAL T;Lu;0;L;<font> 0054;;;;N;;;;;
+1D5B4;MATHEMATICAL SANS-SERIF CAPITAL U;Lu;0;L;<font> 0055;;;;N;;;;;
+1D5B5;MATHEMATICAL SANS-SERIF CAPITAL V;Lu;0;L;<font> 0056;;;;N;;;;;
+1D5B6;MATHEMATICAL SANS-SERIF CAPITAL W;Lu;0;L;<font> 0057;;;;N;;;;;
+1D5B7;MATHEMATICAL SANS-SERIF CAPITAL X;Lu;0;L;<font> 0058;;;;N;;;;;
+1D5B8;MATHEMATICAL SANS-SERIF CAPITAL Y;Lu;0;L;<font> 0059;;;;N;;;;;
+1D5B9;MATHEMATICAL SANS-SERIF CAPITAL Z;Lu;0;L;<font> 005A;;;;N;;;;;
+1D5BA;MATHEMATICAL SANS-SERIF SMALL A;Ll;0;L;<font> 0061;;;;N;;;;;
+1D5BB;MATHEMATICAL SANS-SERIF SMALL B;Ll;0;L;<font> 0062;;;;N;;;;;
+1D5BC;MATHEMATICAL SANS-SERIF SMALL C;Ll;0;L;<font> 0063;;;;N;;;;;
+1D5BD;MATHEMATICAL SANS-SERIF SMALL D;Ll;0;L;<font> 0064;;;;N;;;;;
+1D5BE;MATHEMATICAL SANS-SERIF SMALL E;Ll;0;L;<font> 0065;;;;N;;;;;
+1D5BF;MATHEMATICAL SANS-SERIF SMALL F;Ll;0;L;<font> 0066;;;;N;;;;;
+1D5C0;MATHEMATICAL SANS-SERIF SMALL G;Ll;0;L;<font> 0067;;;;N;;;;;
+1D5C1;MATHEMATICAL SANS-SERIF SMALL H;Ll;0;L;<font> 0068;;;;N;;;;;
+1D5C2;MATHEMATICAL SANS-SERIF SMALL I;Ll;0;L;<font> 0069;;;;N;;;;;
+1D5C3;MATHEMATICAL SANS-SERIF SMALL J;Ll;0;L;<font> 006A;;;;N;;;;;
+1D5C4;MATHEMATICAL SANS-SERIF SMALL K;Ll;0;L;<font> 006B;;;;N;;;;;
+1D5C5;MATHEMATICAL SANS-SERIF SMALL L;Ll;0;L;<font> 006C;;;;N;;;;;
+1D5C6;MATHEMATICAL SANS-SERIF SMALL M;Ll;0;L;<font> 006D;;;;N;;;;;
+1D5C7;MATHEMATICAL SANS-SERIF SMALL N;Ll;0;L;<font> 006E;;;;N;;;;;
+1D5C8;MATHEMATICAL SANS-SERIF SMALL O;Ll;0;L;<font> 006F;;;;N;;;;;
+1D5C9;MATHEMATICAL SANS-SERIF SMALL P;Ll;0;L;<font> 0070;;;;N;;;;;
+1D5CA;MATHEMATICAL SANS-SERIF SMALL Q;Ll;0;L;<font> 0071;;;;N;;;;;
+1D5CB;MATHEMATICAL SANS-SERIF SMALL R;Ll;0;L;<font> 0072;;;;N;;;;;
+1D5CC;MATHEMATICAL SANS-SERIF SMALL S;Ll;0;L;<font> 0073;;;;N;;;;;
+1D5CD;MATHEMATICAL SANS-SERIF SMALL T;Ll;0;L;<font> 0074;;;;N;;;;;
+1D5CE;MATHEMATICAL SANS-SERIF SMALL U;Ll;0;L;<font> 0075;;;;N;;;;;
+1D5CF;MATHEMATICAL SANS-SERIF SMALL V;Ll;0;L;<font> 0076;;;;N;;;;;
+1D5D0;MATHEMATICAL SANS-SERIF SMALL W;Ll;0;L;<font> 0077;;;;N;;;;;
+1D5D1;MATHEMATICAL SANS-SERIF SMALL X;Ll;0;L;<font> 0078;;;;N;;;;;
+1D5D2;MATHEMATICAL SANS-SERIF SMALL Y;Ll;0;L;<font> 0079;;;;N;;;;;
+1D5D3;MATHEMATICAL SANS-SERIF SMALL Z;Ll;0;L;<font> 007A;;;;N;;;;;
+1D5D4;MATHEMATICAL SANS-SERIF BOLD CAPITAL A;Lu;0;L;<font> 0041;;;;N;;;;;
+1D5D5;MATHEMATICAL SANS-SERIF BOLD CAPITAL B;Lu;0;L;<font> 0042;;;;N;;;;;
+1D5D6;MATHEMATICAL SANS-SERIF BOLD CAPITAL C;Lu;0;L;<font> 0043;;;;N;;;;;
+1D5D7;MATHEMATICAL SANS-SERIF BOLD CAPITAL D;Lu;0;L;<font> 0044;;;;N;;;;;
+1D5D8;MATHEMATICAL SANS-SERIF BOLD CAPITAL E;Lu;0;L;<font> 0045;;;;N;;;;;
+1D5D9;MATHEMATICAL SANS-SERIF BOLD CAPITAL F;Lu;0;L;<font> 0046;;;;N;;;;;
+1D5DA;MATHEMATICAL SANS-SERIF BOLD CAPITAL G;Lu;0;L;<font> 0047;;;;N;;;;;
+1D5DB;MATHEMATICAL SANS-SERIF BOLD CAPITAL H;Lu;0;L;<font> 0048;;;;N;;;;;
+1D5DC;MATHEMATICAL SANS-SERIF BOLD CAPITAL I;Lu;0;L;<font> 0049;;;;N;;;;;
+1D5DD;MATHEMATICAL SANS-SERIF BOLD CAPITAL J;Lu;0;L;<font> 004A;;;;N;;;;;
+1D5DE;MATHEMATICAL SANS-SERIF BOLD CAPITAL K;Lu;0;L;<font> 004B;;;;N;;;;;
+1D5DF;MATHEMATICAL SANS-SERIF BOLD CAPITAL L;Lu;0;L;<font> 004C;;;;N;;;;;
+1D5E0;MATHEMATICAL SANS-SERIF BOLD CAPITAL M;Lu;0;L;<font> 004D;;;;N;;;;;
+1D5E1;MATHEMATICAL SANS-SERIF BOLD CAPITAL N;Lu;0;L;<font> 004E;;;;N;;;;;
+1D5E2;MATHEMATICAL SANS-SERIF BOLD CAPITAL O;Lu;0;L;<font> 004F;;;;N;;;;;
+1D5E3;MATHEMATICAL SANS-SERIF BOLD CAPITAL P;Lu;0;L;<font> 0050;;;;N;;;;;
+1D5E4;MATHEMATICAL SANS-SERIF BOLD CAPITAL Q;Lu;0;L;<font> 0051;;;;N;;;;;
+1D5E5;MATHEMATICAL SANS-SERIF BOLD CAPITAL R;Lu;0;L;<font> 0052;;;;N;;;;;
+1D5E6;MATHEMATICAL SANS-SERIF BOLD CAPITAL S;Lu;0;L;<font> 0053;;;;N;;;;;
+1D5E7;MATHEMATICAL SANS-SERIF BOLD CAPITAL T;Lu;0;L;<font> 0054;;;;N;;;;;
+1D5E8;MATHEMATICAL SANS-SERIF BOLD CAPITAL U;Lu;0;L;<font> 0055;;;;N;;;;;
+1D5E9;MATHEMATICAL SANS-SERIF BOLD CAPITAL V;Lu;0;L;<font> 0056;;;;N;;;;;
+1D5EA;MATHEMATICAL SANS-SERIF BOLD CAPITAL W;Lu;0;L;<font> 0057;;;;N;;;;;
+1D5EB;MATHEMATICAL SANS-SERIF BOLD CAPITAL X;Lu;0;L;<font> 0058;;;;N;;;;;
+1D5EC;MATHEMATICAL SANS-SERIF BOLD CAPITAL Y;Lu;0;L;<font> 0059;;;;N;;;;;
+1D5ED;MATHEMATICAL SANS-SERIF BOLD CAPITAL Z;Lu;0;L;<font> 005A;;;;N;;;;;
+1D5EE;MATHEMATICAL SANS-SERIF BOLD SMALL A;Ll;0;L;<font> 0061;;;;N;;;;;
+1D5EF;MATHEMATICAL SANS-SERIF BOLD SMALL B;Ll;0;L;<font> 0062;;;;N;;;;;
+1D5F0;MATHEMATICAL SANS-SERIF BOLD SMALL C;Ll;0;L;<font> 0063;;;;N;;;;;
+1D5F1;MATHEMATICAL SANS-SERIF BOLD SMALL D;Ll;0;L;<font> 0064;;;;N;;;;;
+1D5F2;MATHEMATICAL SANS-SERIF BOLD SMALL E;Ll;0;L;<font> 0065;;;;N;;;;;
+1D5F3;MATHEMATICAL SANS-SERIF BOLD SMALL F;Ll;0;L;<font> 0066;;;;N;;;;;
+1D5F4;MATHEMATICAL SANS-SERIF BOLD SMALL G;Ll;0;L;<font> 0067;;;;N;;;;;
+1D5F5;MATHEMATICAL SANS-SERIF BOLD SMALL H;Ll;0;L;<font> 0068;;;;N;;;;;
+1D5F6;MATHEMATICAL SANS-SERIF BOLD SMALL I;Ll;0;L;<font> 0069;;;;N;;;;;
+1D5F7;MATHEMATICAL SANS-SERIF BOLD SMALL J;Ll;0;L;<font> 006A;;;;N;;;;;
+1D5F8;MATHEMATICAL SANS-SERIF BOLD SMALL K;Ll;0;L;<font> 006B;;;;N;;;;;
+1D5F9;MATHEMATICAL SANS-SERIF BOLD SMALL L;Ll;0;L;<font> 006C;;;;N;;;;;
+1D5FA;MATHEMATICAL SANS-SERIF BOLD SMALL M;Ll;0;L;<font> 006D;;;;N;;;;;
+1D5FB;MATHEMATICAL SANS-SERIF BOLD SMALL N;Ll;0;L;<font> 006E;;;;N;;;;;
+1D5FC;MATHEMATICAL SANS-SERIF BOLD SMALL O;Ll;0;L;<font> 006F;;;;N;;;;;
+1D5FD;MATHEMATICAL SANS-SERIF BOLD SMALL P;Ll;0;L;<font> 0070;;;;N;;;;;
+1D5FE;MATHEMATICAL SANS-SERIF BOLD SMALL Q;Ll;0;L;<font> 0071;;;;N;;;;;
+1D5FF;MATHEMATICAL SANS-SERIF BOLD SMALL R;Ll;0;L;<font> 0072;;;;N;;;;;
+1D600;MATHEMATICAL SANS-SERIF BOLD SMALL S;Ll;0;L;<font> 0073;;;;N;;;;;
+1D601;MATHEMATICAL SANS-SERIF BOLD SMALL T;Ll;0;L;<font> 0074;;;;N;;;;;
+1D602;MATHEMATICAL SANS-SERIF BOLD SMALL U;Ll;0;L;<font> 0075;;;;N;;;;;
+1D603;MATHEMATICAL SANS-SERIF BOLD SMALL V;Ll;0;L;<font> 0076;;;;N;;;;;
+1D604;MATHEMATICAL SANS-SERIF BOLD SMALL W;Ll;0;L;<font> 0077;;;;N;;;;;
+1D605;MATHEMATICAL SANS-SERIF BOLD SMALL X;Ll;0;L;<font> 0078;;;;N;;;;;
+1D606;MATHEMATICAL SANS-SERIF BOLD SMALL Y;Ll;0;L;<font> 0079;;;;N;;;;;
+1D607;MATHEMATICAL SANS-SERIF BOLD SMALL Z;Ll;0;L;<font> 007A;;;;N;;;;;
+1D608;MATHEMATICAL SANS-SERIF ITALIC CAPITAL A;Lu;0;L;<font> 0041;;;;N;;;;;
+1D609;MATHEMATICAL SANS-SERIF ITALIC CAPITAL B;Lu;0;L;<font> 0042;;;;N;;;;;
+1D60A;MATHEMATICAL SANS-SERIF ITALIC CAPITAL C;Lu;0;L;<font> 0043;;;;N;;;;;
+1D60B;MATHEMATICAL SANS-SERIF ITALIC CAPITAL D;Lu;0;L;<font> 0044;;;;N;;;;;
+1D60C;MATHEMATICAL SANS-SERIF ITALIC CAPITAL E;Lu;0;L;<font> 0045;;;;N;;;;;
+1D60D;MATHEMATICAL SANS-SERIF ITALIC CAPITAL F;Lu;0;L;<font> 0046;;;;N;;;;;
+1D60E;MATHEMATICAL SANS-SERIF ITALIC CAPITAL G;Lu;0;L;<font> 0047;;;;N;;;;;
+1D60F;MATHEMATICAL SANS-SERIF ITALIC CAPITAL H;Lu;0;L;<font> 0048;;;;N;;;;;
+1D610;MATHEMATICAL SANS-SERIF ITALIC CAPITAL I;Lu;0;L;<font> 0049;;;;N;;;;;
+1D611;MATHEMATICAL SANS-SERIF ITALIC CAPITAL J;Lu;0;L;<font> 004A;;;;N;;;;;
+1D612;MATHEMATICAL SANS-SERIF ITALIC CAPITAL K;Lu;0;L;<font> 004B;;;;N;;;;;
+1D613;MATHEMATICAL SANS-SERIF ITALIC CAPITAL L;Lu;0;L;<font> 004C;;;;N;;;;;
+1D614;MATHEMATICAL SANS-SERIF ITALIC CAPITAL M;Lu;0;L;<font> 004D;;;;N;;;;;
+1D615;MATHEMATICAL SANS-SERIF ITALIC CAPITAL N;Lu;0;L;<font> 004E;;;;N;;;;;
+1D616;MATHEMATICAL SANS-SERIF ITALIC CAPITAL O;Lu;0;L;<font> 004F;;;;N;;;;;
+1D617;MATHEMATICAL SANS-SERIF ITALIC CAPITAL P;Lu;0;L;<font> 0050;;;;N;;;;;
+1D618;MATHEMATICAL SANS-SERIF ITALIC CAPITAL Q;Lu;0;L;<font> 0051;;;;N;;;;;
+1D619;MATHEMATICAL SANS-SERIF ITALIC CAPITAL R;Lu;0;L;<font> 0052;;;;N;;;;;
+1D61A;MATHEMATICAL SANS-SERIF ITALIC CAPITAL S;Lu;0;L;<font> 0053;;;;N;;;;;
+1D61B;MATHEMATICAL SANS-SERIF ITALIC CAPITAL T;Lu;0;L;<font> 0054;;;;N;;;;;
+1D61C;MATHEMATICAL SANS-SERIF ITALIC CAPITAL U;Lu;0;L;<font> 0055;;;;N;;;;;
+1D61D;MATHEMATICAL SANS-SERIF ITALIC CAPITAL V;Lu;0;L;<font> 0056;;;;N;;;;;
+1D61E;MATHEMATICAL SANS-SERIF ITALIC CAPITAL W;Lu;0;L;<font> 0057;;;;N;;;;;
+1D61F;MATHEMATICAL SANS-SERIF ITALIC CAPITAL X;Lu;0;L;<font> 0058;;;;N;;;;;
+1D620;MATHEMATICAL SANS-SERIF ITALIC CAPITAL Y;Lu;0;L;<font> 0059;;;;N;;;;;
+1D621;MATHEMATICAL SANS-SERIF ITALIC CAPITAL Z;Lu;0;L;<font> 005A;;;;N;;;;;
+1D622;MATHEMATICAL SANS-SERIF ITALIC SMALL A;Ll;0;L;<font> 0061;;;;N;;;;;
+1D623;MATHEMATICAL SANS-SERIF ITALIC SMALL B;Ll;0;L;<font> 0062;;;;N;;;;;
+1D624;MATHEMATICAL SANS-SERIF ITALIC SMALL C;Ll;0;L;<font> 0063;;;;N;;;;;
+1D625;MATHEMATICAL SANS-SERIF ITALIC SMALL D;Ll;0;L;<font> 0064;;;;N;;;;;
+1D626;MATHEMATICAL SANS-SERIF ITALIC SMALL E;Ll;0;L;<font> 0065;;;;N;;;;;
+1D627;MATHEMATICAL SANS-SERIF ITALIC SMALL F;Ll;0;L;<font> 0066;;;;N;;;;;
+1D628;MATHEMATICAL SANS-SERIF ITALIC SMALL G;Ll;0;L;<font> 0067;;;;N;;;;;
+1D629;MATHEMATICAL SANS-SERIF ITALIC SMALL H;Ll;0;L;<font> 0068;;;;N;;;;;
+1D62A;MATHEMATICAL SANS-SERIF ITALIC SMALL I;Ll;0;L;<font> 0069;;;;N;;;;;
+1D62B;MATHEMATICAL SANS-SERIF ITALIC SMALL J;Ll;0;L;<font> 006A;;;;N;;;;;
+1D62C;MATHEMATICAL SANS-SERIF ITALIC SMALL K;Ll;0;L;<font> 006B;;;;N;;;;;
+1D62D;MATHEMATICAL SANS-SERIF ITALIC SMALL L;Ll;0;L;<font> 006C;;;;N;;;;;
+1D62E;MATHEMATICAL SANS-SERIF ITALIC SMALL M;Ll;0;L;<font> 006D;;;;N;;;;;
+1D62F;MATHEMATICAL SANS-SERIF ITALIC SMALL N;Ll;0;L;<font> 006E;;;;N;;;;;
+1D630;MATHEMATICAL SANS-SERIF ITALIC SMALL O;Ll;0;L;<font> 006F;;;;N;;;;;
+1D631;MATHEMATICAL SANS-SERIF ITALIC SMALL P;Ll;0;L;<font> 0070;;;;N;;;;;
+1D632;MATHEMATICAL SANS-SERIF ITALIC SMALL Q;Ll;0;L;<font> 0071;;;;N;;;;;
+1D633;MATHEMATICAL SANS-SERIF ITALIC SMALL R;Ll;0;L;<font> 0072;;;;N;;;;;
+1D634;MATHEMATICAL SANS-SERIF ITALIC SMALL S;Ll;0;L;<font> 0073;;;;N;;;;;
+1D635;MATHEMATICAL SANS-SERIF ITALIC SMALL T;Ll;0;L;<font> 0074;;;;N;;;;;
+1D636;MATHEMATICAL SANS-SERIF ITALIC SMALL U;Ll;0;L;<font> 0075;;;;N;;;;;
+1D637;MATHEMATICAL SANS-SERIF ITALIC SMALL V;Ll;0;L;<font> 0076;;;;N;;;;;
+1D638;MATHEMATICAL SANS-SERIF ITALIC SMALL W;Ll;0;L;<font> 0077;;;;N;;;;;
+1D639;MATHEMATICAL SANS-SERIF ITALIC SMALL X;Ll;0;L;<font> 0078;;;;N;;;;;
+1D63A;MATHEMATICAL SANS-SERIF ITALIC SMALL Y;Ll;0;L;<font> 0079;;;;N;;;;;
+1D63B;MATHEMATICAL SANS-SERIF ITALIC SMALL Z;Ll;0;L;<font> 007A;;;;N;;;;;
+1D63C;MATHEMATICAL SANS-SERIF BOLD ITALIC CAPITAL A;Lu;0;L;<font> 0041;;;;N;;;;;
+1D63D;MATHEMATICAL SANS-SERIF BOLD ITALIC CAPITAL B;Lu;0;L;<font> 0042;;;;N;;;;;
+1D63E;MATHEMATICAL SANS-SERIF BOLD ITALIC CAPITAL C;Lu;0;L;<font> 0043;;;;N;;;;;
+1D63F;MATHEMATICAL SANS-SERIF BOLD ITALIC CAPITAL D;Lu;0;L;<font> 0044;;;;N;;;;;
+1D640;MATHEMATICAL SANS-SERIF BOLD ITALIC CAPITAL E;Lu;0;L;<font> 0045;;;;N;;;;;
+1D641;MATHEMATICAL SANS-SERIF BOLD ITALIC CAPITAL F;Lu;0;L;<font> 0046;;;;N;;;;;
+1D642;MATHEMATICAL SANS-SERIF BOLD ITALIC CAPITAL G;Lu;0;L;<font> 0047;;;;N;;;;;
+1D643;MATHEMATICAL SANS-SERIF BOLD ITALIC CAPITAL H;Lu;0;L;<font> 0048;;;;N;;;;;
+1D644;MATHEMATICAL SANS-SERIF BOLD ITALIC CAPITAL I;Lu;0;L;<font> 0049;;;;N;;;;;
+1D645;MATHEMATICAL SANS-SERIF BOLD ITALIC CAPITAL J;Lu;0;L;<font> 004A;;;;N;;;;;
+1D646;MATHEMATICAL SANS-SERIF BOLD ITALIC CAPITAL K;Lu;0;L;<font> 004B;;;;N;;;;;
+1D647;MATHEMATICAL SANS-SERIF BOLD ITALIC CAPITAL L;Lu;0;L;<font> 004C;;;;N;;;;;
+1D648;MATHEMATICAL SANS-SERIF BOLD ITALIC CAPITAL M;Lu;0;L;<font> 004D;;;;N;;;;;
+1D649;MATHEMATICAL SANS-SERIF BOLD ITALIC CAPITAL N;Lu;0;L;<font> 004E;;;;N;;;;;
+1D64A;MATHEMATICAL SANS-SERIF BOLD ITALIC CAPITAL O;Lu;0;L;<font> 004F;;;;N;;;;;
+1D64B;MATHEMATICAL SANS-SERIF BOLD ITALIC CAPITAL P;Lu;0;L;<font> 0050;;;;N;;;;;
+1D64C;MATHEMATICAL SANS-SERIF BOLD ITALIC CAPITAL Q;Lu;0;L;<font> 0051;;;;N;;;;;
+1D64D;MATHEMATICAL SANS-SERIF BOLD ITALIC CAPITAL R;Lu;0;L;<font> 0052;;;;N;;;;;
+1D64E;MATHEMATICAL SANS-SERIF BOLD ITALIC CAPITAL S;Lu;0;L;<font> 0053;;;;N;;;;;
+1D64F;MATHEMATICAL SANS-SERIF BOLD ITALIC CAPITAL T;Lu;0;L;<font> 0054;;;;N;;;;;
+1D650;MATHEMATICAL SANS-SERIF BOLD ITALIC CAPITAL U;Lu;0;L;<font> 0055;;;;N;;;;;
+1D651;MATHEMATICAL SANS-SERIF BOLD ITALIC CAPITAL V;Lu;0;L;<font> 0056;;;;N;;;;;
+1D652;MATHEMATICAL SANS-SERIF BOLD ITALIC CAPITAL W;Lu;0;L;<font> 0057;;;;N;;;;;
+1D653;MATHEMATICAL SANS-SERIF BOLD ITALIC CAPITAL X;Lu;0;L;<font> 0058;;;;N;;;;;
+1D654;MATHEMATICAL SANS-SERIF BOLD ITALIC CAPITAL Y;Lu;0;L;<font> 0059;;;;N;;;;;
+1D655;MATHEMATICAL SANS-SERIF BOLD ITALIC CAPITAL Z;Lu;0;L;<font> 005A;;;;N;;;;;
+1D656;MATHEMATICAL SANS-SERIF BOLD ITALIC SMALL A;Ll;0;L;<font> 0061;;;;N;;;;;
+1D657;MATHEMATICAL SANS-SERIF BOLD ITALIC SMALL B;Ll;0;L;<font> 0062;;;;N;;;;;
+1D658;MATHEMATICAL SANS-SERIF BOLD ITALIC SMALL C;Ll;0;L;<font> 0063;;;;N;;;;;
+1D659;MATHEMATICAL SANS-SERIF BOLD ITALIC SMALL D;Ll;0;L;<font> 0064;;;;N;;;;;
+1D65A;MATHEMATICAL SANS-SERIF BOLD ITALIC SMALL E;Ll;0;L;<font> 0065;;;;N;;;;;
+1D65B;MATHEMATICAL SANS-SERIF BOLD ITALIC SMALL F;Ll;0;L;<font> 0066;;;;N;;;;;
+1D65C;MATHEMATICAL SANS-SERIF BOLD ITALIC SMALL G;Ll;0;L;<font> 0067;;;;N;;;;;
+1D65D;MATHEMATICAL SANS-SERIF BOLD ITALIC SMALL H;Ll;0;L;<font> 0068;;;;N;;;;;
+1D65E;MATHEMATICAL SANS-SERIF BOLD ITALIC SMALL I;Ll;0;L;<font> 0069;;;;N;;;;;
+1D65F;MATHEMATICAL SANS-SERIF BOLD ITALIC SMALL J;Ll;0;L;<font> 006A;;;;N;;;;;
+1D660;MATHEMATICAL SANS-SERIF BOLD ITALIC SMALL K;Ll;0;L;<font> 006B;;;;N;;;;;
+1D661;MATHEMATICAL SANS-SERIF BOLD ITALIC SMALL L;Ll;0;L;<font> 006C;;;;N;;;;;
+1D662;MATHEMATICAL SANS-SERIF BOLD ITALIC SMALL M;Ll;0;L;<font> 006D;;;;N;;;;;
+1D663;MATHEMATICAL SANS-SERIF BOLD ITALIC SMALL N;Ll;0;L;<font> 006E;;;;N;;;;;
+1D664;MATHEMATICAL SANS-SERIF BOLD ITALIC SMALL O;Ll;0;L;<font> 006F;;;;N;;;;;
+1D665;MATHEMATICAL SANS-SERIF BOLD ITALIC SMALL P;Ll;0;L;<font> 0070;;;;N;;;;;
+1D666;MATHEMATICAL SANS-SERIF BOLD ITALIC SMALL Q;Ll;0;L;<font> 0071;;;;N;;;;;
+1D667;MATHEMATICAL SANS-SERIF BOLD ITALIC SMALL R;Ll;0;L;<font> 0072;;;;N;;;;;
+1D668;MATHEMATICAL SANS-SERIF BOLD ITALIC SMALL S;Ll;0;L;<font> 0073;;;;N;;;;;
+1D669;MATHEMATICAL SANS-SERIF BOLD ITALIC SMALL T;Ll;0;L;<font> 0074;;;;N;;;;;
+1D66A;MATHEMATICAL SANS-SERIF BOLD ITALIC SMALL U;Ll;0;L;<font> 0075;;;;N;;;;;
+1D66B;MATHEMATICAL SANS-SERIF BOLD ITALIC SMALL V;Ll;0;L;<font> 0076;;;;N;;;;;
+1D66C;MATHEMATICAL SANS-SERIF BOLD ITALIC SMALL W;Ll;0;L;<font> 0077;;;;N;;;;;
+1D66D;MATHEMATICAL SANS-SERIF BOLD ITALIC SMALL X;Ll;0;L;<font> 0078;;;;N;;;;;
+1D66E;MATHEMATICAL SANS-SERIF BOLD ITALIC SMALL Y;Ll;0;L;<font> 0079;;;;N;;;;;
+1D66F;MATHEMATICAL SANS-SERIF BOLD ITALIC SMALL Z;Ll;0;L;<font> 007A;;;;N;;;;;
+1D670;MATHEMATICAL MONOSPACE CAPITAL A;Lu;0;L;<font> 0041;;;;N;;;;;
+1D671;MATHEMATICAL MONOSPACE CAPITAL B;Lu;0;L;<font> 0042;;;;N;;;;;
+1D672;MATHEMATICAL MONOSPACE CAPITAL C;Lu;0;L;<font> 0043;;;;N;;;;;
+1D673;MATHEMATICAL MONOSPACE CAPITAL D;Lu;0;L;<font> 0044;;;;N;;;;;
+1D674;MATHEMATICAL MONOSPACE CAPITAL E;Lu;0;L;<font> 0045;;;;N;;;;;
+1D675;MATHEMATICAL MONOSPACE CAPITAL F;Lu;0;L;<font> 0046;;;;N;;;;;
+1D676;MATHEMATICAL MONOSPACE CAPITAL G;Lu;0;L;<font> 0047;;;;N;;;;;
+1D677;MATHEMATICAL MONOSPACE CAPITAL H;Lu;0;L;<font> 0048;;;;N;;;;;
+1D678;MATHEMATICAL MONOSPACE CAPITAL I;Lu;0;L;<font> 0049;;;;N;;;;;
+1D679;MATHEMATICAL MONOSPACE CAPITAL J;Lu;0;L;<font> 004A;;;;N;;;;;
+1D67A;MATHEMATICAL MONOSPACE CAPITAL K;Lu;0;L;<font> 004B;;;;N;;;;;
+1D67B;MATHEMATICAL MONOSPACE CAPITAL L;Lu;0;L;<font> 004C;;;;N;;;;;
+1D67C;MATHEMATICAL MONOSPACE CAPITAL M;Lu;0;L;<font> 004D;;;;N;;;;;
+1D67D;MATHEMATICAL MONOSPACE CAPITAL N;Lu;0;L;<font> 004E;;;;N;;;;;
+1D67E;MATHEMATICAL MONOSPACE CAPITAL O;Lu;0;L;<font> 004F;;;;N;;;;;
+1D67F;MATHEMATICAL MONOSPACE CAPITAL P;Lu;0;L;<font> 0050;;;;N;;;;;
+1D680;MATHEMATICAL MONOSPACE CAPITAL Q;Lu;0;L;<font> 0051;;;;N;;;;;
+1D681;MATHEMATICAL MONOSPACE CAPITAL R;Lu;0;L;<font> 0052;;;;N;;;;;
+1D682;MATHEMATICAL MONOSPACE CAPITAL S;Lu;0;L;<font> 0053;;;;N;;;;;
+1D683;MATHEMATICAL MONOSPACE CAPITAL T;Lu;0;L;<font> 0054;;;;N;;;;;
+1D684;MATHEMATICAL MONOSPACE CAPITAL U;Lu;0;L;<font> 0055;;;;N;;;;;
+1D685;MATHEMATICAL MONOSPACE CAPITAL V;Lu;0;L;<font> 0056;;;;N;;;;;
+1D686;MATHEMATICAL MONOSPACE CAPITAL W;Lu;0;L;<font> 0057;;;;N;;;;;
+1D687;MATHEMATICAL MONOSPACE CAPITAL X;Lu;0;L;<font> 0058;;;;N;;;;;
+1D688;MATHEMATICAL MONOSPACE CAPITAL Y;Lu;0;L;<font> 0059;;;;N;;;;;
+1D689;MATHEMATICAL MONOSPACE CAPITAL Z;Lu;0;L;<font> 005A;;;;N;;;;;
+1D68A;MATHEMATICAL MONOSPACE SMALL A;Ll;0;L;<font> 0061;;;;N;;;;;
+1D68B;MATHEMATICAL MONOSPACE SMALL B;Ll;0;L;<font> 0062;;;;N;;;;;
+1D68C;MATHEMATICAL MONOSPACE SMALL C;Ll;0;L;<font> 0063;;;;N;;;;;
+1D68D;MATHEMATICAL MONOSPACE SMALL D;Ll;0;L;<font> 0064;;;;N;;;;;
+1D68E;MATHEMATICAL MONOSPACE SMALL E;Ll;0;L;<font> 0065;;;;N;;;;;
+1D68F;MATHEMATICAL MONOSPACE SMALL F;Ll;0;L;<font> 0066;;;;N;;;;;
+1D690;MATHEMATICAL MONOSPACE SMALL G;Ll;0;L;<font> 0067;;;;N;;;;;
+1D691;MATHEMATICAL MONOSPACE SMALL H;Ll;0;L;<font> 0068;;;;N;;;;;
+1D692;MATHEMATICAL MONOSPACE SMALL I;Ll;0;L;<font> 0069;;;;N;;;;;
+1D693;MATHEMATICAL MONOSPACE SMALL J;Ll;0;L;<font> 006A;;;;N;;;;;
+1D694;MATHEMATICAL MONOSPACE SMALL K;Ll;0;L;<font> 006B;;;;N;;;;;
+1D695;MATHEMATICAL MONOSPACE SMALL L;Ll;0;L;<font> 006C;;;;N;;;;;
+1D696;MATHEMATICAL MONOSPACE SMALL M;Ll;0;L;<font> 006D;;;;N;;;;;
+1D697;MATHEMATICAL MONOSPACE SMALL N;Ll;0;L;<font> 006E;;;;N;;;;;
+1D698;MATHEMATICAL MONOSPACE SMALL O;Ll;0;L;<font> 006F;;;;N;;;;;
+1D699;MATHEMATICAL MONOSPACE SMALL P;Ll;0;L;<font> 0070;;;;N;;;;;
+1D69A;MATHEMATICAL MONOSPACE SMALL Q;Ll;0;L;<font> 0071;;;;N;;;;;
+1D69B;MATHEMATICAL MONOSPACE SMALL R;Ll;0;L;<font> 0072;;;;N;;;;;
+1D69C;MATHEMATICAL MONOSPACE SMALL S;Ll;0;L;<font> 0073;;;;N;;;;;
+1D69D;MATHEMATICAL MONOSPACE SMALL T;Ll;0;L;<font> 0074;;;;N;;;;;
+1D69E;MATHEMATICAL MONOSPACE SMALL U;Ll;0;L;<font> 0075;;;;N;;;;;
+1D69F;MATHEMATICAL MONOSPACE SMALL V;Ll;0;L;<font> 0076;;;;N;;;;;
+1D6A0;MATHEMATICAL MONOSPACE SMALL W;Ll;0;L;<font> 0077;;;;N;;;;;
+1D6A1;MATHEMATICAL MONOSPACE SMALL X;Ll;0;L;<font> 0078;;;;N;;;;;
+1D6A2;MATHEMATICAL MONOSPACE SMALL Y;Ll;0;L;<font> 0079;;;;N;;;;;
+1D6A3;MATHEMATICAL MONOSPACE SMALL Z;Ll;0;L;<font> 007A;;;;N;;;;;
+1D6A4;MATHEMATICAL ITALIC SMALL DOTLESS I;Ll;0;L;<font> 0131;;;;N;;;;;
+1D6A5;MATHEMATICAL ITALIC SMALL DOTLESS J;Ll;0;L;<font> 0237;;;;N;;;;;
+1D6A8;MATHEMATICAL BOLD CAPITAL ALPHA;Lu;0;L;<font> 0391;;;;N;;;;;
+1D6A9;MATHEMATICAL BOLD CAPITAL BETA;Lu;0;L;<font> 0392;;;;N;;;;;
+1D6AA;MATHEMATICAL BOLD CAPITAL GAMMA;Lu;0;L;<font> 0393;;;;N;;;;;
+1D6AB;MATHEMATICAL BOLD CAPITAL DELTA;Lu;0;L;<font> 0394;;;;N;;;;;
+1D6AC;MATHEMATICAL BOLD CAPITAL EPSILON;Lu;0;L;<font> 0395;;;;N;;;;;
+1D6AD;MATHEMATICAL BOLD CAPITAL ZETA;Lu;0;L;<font> 0396;;;;N;;;;;
+1D6AE;MATHEMATICAL BOLD CAPITAL ETA;Lu;0;L;<font> 0397;;;;N;;;;;
+1D6AF;MATHEMATICAL BOLD CAPITAL THETA;Lu;0;L;<font> 0398;;;;N;;;;;
+1D6B0;MATHEMATICAL BOLD CAPITAL IOTA;Lu;0;L;<font> 0399;;;;N;;;;;
+1D6B1;MATHEMATICAL BOLD CAPITAL KAPPA;Lu;0;L;<font> 039A;;;;N;;;;;
+1D6B2;MATHEMATICAL BOLD CAPITAL LAMDA;Lu;0;L;<font> 039B;;;;N;;;;;
+1D6B3;MATHEMATICAL BOLD CAPITAL MU;Lu;0;L;<font> 039C;;;;N;;;;;
+1D6B4;MATHEMATICAL BOLD CAPITAL NU;Lu;0;L;<font> 039D;;;;N;;;;;
+1D6B5;MATHEMATICAL BOLD CAPITAL XI;Lu;0;L;<font> 039E;;;;N;;;;;
+1D6B6;MATHEMATICAL BOLD CAPITAL OMICRON;Lu;0;L;<font> 039F;;;;N;;;;;
+1D6B7;MATHEMATICAL BOLD CAPITAL PI;Lu;0;L;<font> 03A0;;;;N;;;;;
+1D6B8;MATHEMATICAL BOLD CAPITAL RHO;Lu;0;L;<font> 03A1;;;;N;;;;;
+1D6B9;MATHEMATICAL BOLD CAPITAL THETA SYMBOL;Lu;0;L;<font> 03F4;;;;N;;;;;
+1D6BA;MATHEMATICAL BOLD CAPITAL SIGMA;Lu;0;L;<font> 03A3;;;;N;;;;;
+1D6BB;MATHEMATICAL BOLD CAPITAL TAU;Lu;0;L;<font> 03A4;;;;N;;;;;
+1D6BC;MATHEMATICAL BOLD CAPITAL UPSILON;Lu;0;L;<font> 03A5;;;;N;;;;;
+1D6BD;MATHEMATICAL BOLD CAPITAL PHI;Lu;0;L;<font> 03A6;;;;N;;;;;
+1D6BE;MATHEMATICAL BOLD CAPITAL CHI;Lu;0;L;<font> 03A7;;;;N;;;;;
+1D6BF;MATHEMATICAL BOLD CAPITAL PSI;Lu;0;L;<font> 03A8;;;;N;;;;;
+1D6C0;MATHEMATICAL BOLD CAPITAL OMEGA;Lu;0;L;<font> 03A9;;;;N;;;;;
+1D6C1;MATHEMATICAL BOLD NABLA;Sm;0;L;<font> 2207;;;;N;;;;;
+1D6C2;MATHEMATICAL BOLD SMALL ALPHA;Ll;0;L;<font> 03B1;;;;N;;;;;
+1D6C3;MATHEMATICAL BOLD SMALL BETA;Ll;0;L;<font> 03B2;;;;N;;;;;
+1D6C4;MATHEMATICAL BOLD SMALL GAMMA;Ll;0;L;<font> 03B3;;;;N;;;;;
+1D6C5;MATHEMATICAL BOLD SMALL DELTA;Ll;0;L;<font> 03B4;;;;N;;;;;
+1D6C6;MATHEMATICAL BOLD SMALL EPSILON;Ll;0;L;<font> 03B5;;;;N;;;;;
+1D6C7;MATHEMATICAL BOLD SMALL ZETA;Ll;0;L;<font> 03B6;;;;N;;;;;
+1D6C8;MATHEMATICAL BOLD SMALL ETA;Ll;0;L;<font> 03B7;;;;N;;;;;
+1D6C9;MATHEMATICAL BOLD SMALL THETA;Ll;0;L;<font> 03B8;;;;N;;;;;
+1D6CA;MATHEMATICAL BOLD SMALL IOTA;Ll;0;L;<font> 03B9;;;;N;;;;;
+1D6CB;MATHEMATICAL BOLD SMALL KAPPA;Ll;0;L;<font> 03BA;;;;N;;;;;
+1D6CC;MATHEMATICAL BOLD SMALL LAMDA;Ll;0;L;<font> 03BB;;;;N;;;;;
+1D6CD;MATHEMATICAL BOLD SMALL MU;Ll;0;L;<font> 03BC;;;;N;;;;;
+1D6CE;MATHEMATICAL BOLD SMALL NU;Ll;0;L;<font> 03BD;;;;N;;;;;
+1D6CF;MATHEMATICAL BOLD SMALL XI;Ll;0;L;<font> 03BE;;;;N;;;;;
+1D6D0;MATHEMATICAL BOLD SMALL OMICRON;Ll;0;L;<font> 03BF;;;;N;;;;;
+1D6D1;MATHEMATICAL BOLD SMALL PI;Ll;0;L;<font> 03C0;;;;N;;;;;
+1D6D2;MATHEMATICAL BOLD SMALL RHO;Ll;0;L;<font> 03C1;;;;N;;;;;
+1D6D3;MATHEMATICAL BOLD SMALL FINAL SIGMA;Ll;0;L;<font> 03C2;;;;N;;;;;
+1D6D4;MATHEMATICAL BOLD SMALL SIGMA;Ll;0;L;<font> 03C3;;;;N;;;;;
+1D6D5;MATHEMATICAL BOLD SMALL TAU;Ll;0;L;<font> 03C4;;;;N;;;;;
+1D6D6;MATHEMATICAL BOLD SMALL UPSILON;Ll;0;L;<font> 03C5;;;;N;;;;;
+1D6D7;MATHEMATICAL BOLD SMALL PHI;Ll;0;L;<font> 03C6;;;;N;;;;;
+1D6D8;MATHEMATICAL BOLD SMALL CHI;Ll;0;L;<font> 03C7;;;;N;;;;;
+1D6D9;MATHEMATICAL BOLD SMALL PSI;Ll;0;L;<font> 03C8;;;;N;;;;;
+1D6DA;MATHEMATICAL BOLD SMALL OMEGA;Ll;0;L;<font> 03C9;;;;N;;;;;
+1D6DB;MATHEMATICAL BOLD PARTIAL DIFFERENTIAL;Sm;0;ON;<font> 2202;;;;Y;;;;;
+1D6DC;MATHEMATICAL BOLD EPSILON SYMBOL;Ll;0;L;<font> 03F5;;;;N;;;;;
+1D6DD;MATHEMATICAL BOLD THETA SYMBOL;Ll;0;L;<font> 03D1;;;;N;;;;;
+1D6DE;MATHEMATICAL BOLD KAPPA SYMBOL;Ll;0;L;<font> 03F0;;;;N;;;;;
+1D6DF;MATHEMATICAL BOLD PHI SYMBOL;Ll;0;L;<font> 03D5;;;;N;;;;;
+1D6E0;MATHEMATICAL BOLD RHO SYMBOL;Ll;0;L;<font> 03F1;;;;N;;;;;
+1D6E1;MATHEMATICAL BOLD PI SYMBOL;Ll;0;L;<font> 03D6;;;;N;;;;;
+1D6E2;MATHEMATICAL ITALIC CAPITAL ALPHA;Lu;0;L;<font> 0391;;;;N;;;;;
+1D6E3;MATHEMATICAL ITALIC CAPITAL BETA;Lu;0;L;<font> 0392;;;;N;;;;;
+1D6E4;MATHEMATICAL ITALIC CAPITAL GAMMA;Lu;0;L;<font> 0393;;;;N;;;;;
+1D6E5;MATHEMATICAL ITALIC CAPITAL DELTA;Lu;0;L;<font> 0394;;;;N;;;;;
+1D6E6;MATHEMATICAL ITALIC CAPITAL EPSILON;Lu;0;L;<font> 0395;;;;N;;;;;
+1D6E7;MATHEMATICAL ITALIC CAPITAL ZETA;Lu;0;L;<font> 0396;;;;N;;;;;
+1D6E8;MATHEMATICAL ITALIC CAPITAL ETA;Lu;0;L;<font> 0397;;;;N;;;;;
+1D6E9;MATHEMATICAL ITALIC CAPITAL THETA;Lu;0;L;<font> 0398;;;;N;;;;;
+1D6EA;MATHEMATICAL ITALIC CAPITAL IOTA;Lu;0;L;<font> 0399;;;;N;;;;;
+1D6EB;MATHEMATICAL ITALIC CAPITAL KAPPA;Lu;0;L;<font> 039A;;;;N;;;;;
+1D6EC;MATHEMATICAL ITALIC CAPITAL LAMDA;Lu;0;L;<font> 039B;;;;N;;;;;
+1D6ED;MATHEMATICAL ITALIC CAPITAL MU;Lu;0;L;<font> 039C;;;;N;;;;;
+1D6EE;MATHEMATICAL ITALIC CAPITAL NU;Lu;0;L;<font> 039D;;;;N;;;;;
+1D6EF;MATHEMATICAL ITALIC CAPITAL XI;Lu;0;L;<font> 039E;;;;N;;;;;
+1D6F0;MATHEMATICAL ITALIC CAPITAL OMICRON;Lu;0;L;<font> 039F;;;;N;;;;;
+1D6F1;MATHEMATICAL ITALIC CAPITAL PI;Lu;0;L;<font> 03A0;;;;N;;;;;
+1D6F2;MATHEMATICAL ITALIC CAPITAL RHO;Lu;0;L;<font> 03A1;;;;N;;;;;
+1D6F3;MATHEMATICAL ITALIC CAPITAL THETA SYMBOL;Lu;0;L;<font> 03F4;;;;N;;;;;
+1D6F4;MATHEMATICAL ITALIC CAPITAL SIGMA;Lu;0;L;<font> 03A3;;;;N;;;;;
+1D6F5;MATHEMATICAL ITALIC CAPITAL TAU;Lu;0;L;<font> 03A4;;;;N;;;;;
+1D6F6;MATHEMATICAL ITALIC CAPITAL UPSILON;Lu;0;L;<font> 03A5;;;;N;;;;;
+1D6F7;MATHEMATICAL ITALIC CAPITAL PHI;Lu;0;L;<font> 03A6;;;;N;;;;;
+1D6F8;MATHEMATICAL ITALIC CAPITAL CHI;Lu;0;L;<font> 03A7;;;;N;;;;;
+1D6F9;MATHEMATICAL ITALIC CAPITAL PSI;Lu;0;L;<font> 03A8;;;;N;;;;;
+1D6FA;MATHEMATICAL ITALIC CAPITAL OMEGA;Lu;0;L;<font> 03A9;;;;N;;;;;
+1D6FB;MATHEMATICAL ITALIC NABLA;Sm;0;L;<font> 2207;;;;N;;;;;
+1D6FC;MATHEMATICAL ITALIC SMALL ALPHA;Ll;0;L;<font> 03B1;;;;N;;;;;
+1D6FD;MATHEMATICAL ITALIC SMALL BETA;Ll;0;L;<font> 03B2;;;;N;;;;;
+1D6FE;MATHEMATICAL ITALIC SMALL GAMMA;Ll;0;L;<font> 03B3;;;;N;;;;;
+1D6FF;MATHEMATICAL ITALIC SMALL DELTA;Ll;0;L;<font> 03B4;;;;N;;;;;
+1D700;MATHEMATICAL ITALIC SMALL EPSILON;Ll;0;L;<font> 03B5;;;;N;;;;;
+1D701;MATHEMATICAL ITALIC SMALL ZETA;Ll;0;L;<font> 03B6;;;;N;;;;;
+1D702;MATHEMATICAL ITALIC SMALL ETA;Ll;0;L;<font> 03B7;;;;N;;;;;
+1D703;MATHEMATICAL ITALIC SMALL THETA;Ll;0;L;<font> 03B8;;;;N;;;;;
+1D704;MATHEMATICAL ITALIC SMALL IOTA;Ll;0;L;<font> 03B9;;;;N;;;;;
+1D705;MATHEMATICAL ITALIC SMALL KAPPA;Ll;0;L;<font> 03BA;;;;N;;;;;
+1D706;MATHEMATICAL ITALIC SMALL LAMDA;Ll;0;L;<font> 03BB;;;;N;;;;;
+1D707;MATHEMATICAL ITALIC SMALL MU;Ll;0;L;<font> 03BC;;;;N;;;;;
+1D708;MATHEMATICAL ITALIC SMALL NU;Ll;0;L;<font> 03BD;;;;N;;;;;
+1D709;MATHEMATICAL ITALIC SMALL XI;Ll;0;L;<font> 03BE;;;;N;;;;;
+1D70A;MATHEMATICAL ITALIC SMALL OMICRON;Ll;0;L;<font> 03BF;;;;N;;;;;
+1D70B;MATHEMATICAL ITALIC SMALL PI;Ll;0;L;<font> 03C0;;;;N;;;;;
+1D70C;MATHEMATICAL ITALIC SMALL RHO;Ll;0;L;<font> 03C1;;;;N;;;;;
+1D70D;MATHEMATICAL ITALIC SMALL FINAL SIGMA;Ll;0;L;<font> 03C2;;;;N;;;;;
+1D70E;MATHEMATICAL ITALIC SMALL SIGMA;Ll;0;L;<font> 03C3;;;;N;;;;;
+1D70F;MATHEMATICAL ITALIC SMALL TAU;Ll;0;L;<font> 03C4;;;;N;;;;;
+1D710;MATHEMATICAL ITALIC SMALL UPSILON;Ll;0;L;<font> 03C5;;;;N;;;;;
+1D711;MATHEMATICAL ITALIC SMALL PHI;Ll;0;L;<font> 03C6;;;;N;;;;;
+1D712;MATHEMATICAL ITALIC SMALL CHI;Ll;0;L;<font> 03C7;;;;N;;;;;
+1D713;MATHEMATICAL ITALIC SMALL PSI;Ll;0;L;<font> 03C8;;;;N;;;;;
+1D714;MATHEMATICAL ITALIC SMALL OMEGA;Ll;0;L;<font> 03C9;;;;N;;;;;
+1D715;MATHEMATICAL ITALIC PARTIAL DIFFERENTIAL;Sm;0;ON;<font> 2202;;;;Y;;;;;
+1D716;MATHEMATICAL ITALIC EPSILON SYMBOL;Ll;0;L;<font> 03F5;;;;N;;;;;
+1D717;MATHEMATICAL ITALIC THETA SYMBOL;Ll;0;L;<font> 03D1;;;;N;;;;;
+1D718;MATHEMATICAL ITALIC KAPPA SYMBOL;Ll;0;L;<font> 03F0;;;;N;;;;;
+1D719;MATHEMATICAL ITALIC PHI SYMBOL;Ll;0;L;<font> 03D5;;;;N;;;;;
+1D71A;MATHEMATICAL ITALIC RHO SYMBOL;Ll;0;L;<font> 03F1;;;;N;;;;;
+1D71B;MATHEMATICAL ITALIC PI SYMBOL;Ll;0;L;<font> 03D6;;;;N;;;;;
+1D71C;MATHEMATICAL BOLD ITALIC CAPITAL ALPHA;Lu;0;L;<font> 0391;;;;N;;;;;
+1D71D;MATHEMATICAL BOLD ITALIC CAPITAL BETA;Lu;0;L;<font> 0392;;;;N;;;;;
+1D71E;MATHEMATICAL BOLD ITALIC CAPITAL GAMMA;Lu;0;L;<font> 0393;;;;N;;;;;
+1D71F;MATHEMATICAL BOLD ITALIC CAPITAL DELTA;Lu;0;L;<font> 0394;;;;N;;;;;
+1D720;MATHEMATICAL BOLD ITALIC CAPITAL EPSILON;Lu;0;L;<font> 0395;;;;N;;;;;
+1D721;MATHEMATICAL BOLD ITALIC CAPITAL ZETA;Lu;0;L;<font> 0396;;;;N;;;;;
+1D722;MATHEMATICAL BOLD ITALIC CAPITAL ETA;Lu;0;L;<font> 0397;;;;N;;;;;
+1D723;MATHEMATICAL BOLD ITALIC CAPITAL THETA;Lu;0;L;<font> 0398;;;;N;;;;;
+1D724;MATHEMATICAL BOLD ITALIC CAPITAL IOTA;Lu;0;L;<font> 0399;;;;N;;;;;
+1D725;MATHEMATICAL BOLD ITALIC CAPITAL KAPPA;Lu;0;L;<font> 039A;;;;N;;;;;
+1D726;MATHEMATICAL BOLD ITALIC CAPITAL LAMDA;Lu;0;L;<font> 039B;;;;N;;;;;
+1D727;MATHEMATICAL BOLD ITALIC CAPITAL MU;Lu;0;L;<font> 039C;;;;N;;;;;
+1D728;MATHEMATICAL BOLD ITALIC CAPITAL NU;Lu;0;L;<font> 039D;;;;N;;;;;
+1D729;MATHEMATICAL BOLD ITALIC CAPITAL XI;Lu;0;L;<font> 039E;;;;N;;;;;
+1D72A;MATHEMATICAL BOLD ITALIC CAPITAL OMICRON;Lu;0;L;<font> 039F;;;;N;;;;;
+1D72B;MATHEMATICAL BOLD ITALIC CAPITAL PI;Lu;0;L;<font> 03A0;;;;N;;;;;
+1D72C;MATHEMATICAL BOLD ITALIC CAPITAL RHO;Lu;0;L;<font> 03A1;;;;N;;;;;
+1D72D;MATHEMATICAL BOLD ITALIC CAPITAL THETA SYMBOL;Lu;0;L;<font> 03F4;;;;N;;;;;
+1D72E;MATHEMATICAL BOLD ITALIC CAPITAL SIGMA;Lu;0;L;<font> 03A3;;;;N;;;;;
+1D72F;MATHEMATICAL BOLD ITALIC CAPITAL TAU;Lu;0;L;<font> 03A4;;;;N;;;;;
+1D730;MATHEMATICAL BOLD ITALIC CAPITAL UPSILON;Lu;0;L;<font> 03A5;;;;N;;;;;
+1D731;MATHEMATICAL BOLD ITALIC CAPITAL PHI;Lu;0;L;<font> 03A6;;;;N;;;;;
+1D732;MATHEMATICAL BOLD ITALIC CAPITAL CHI;Lu;0;L;<font> 03A7;;;;N;;;;;
+1D733;MATHEMATICAL BOLD ITALIC CAPITAL PSI;Lu;0;L;<font> 03A8;;;;N;;;;;
+1D734;MATHEMATICAL BOLD ITALIC CAPITAL OMEGA;Lu;0;L;<font> 03A9;;;;N;;;;;
+1D735;MATHEMATICAL BOLD ITALIC NABLA;Sm;0;L;<font> 2207;;;;N;;;;;
+1D736;MATHEMATICAL BOLD ITALIC SMALL ALPHA;Ll;0;L;<font> 03B1;;;;N;;;;;
+1D737;MATHEMATICAL BOLD ITALIC SMALL BETA;Ll;0;L;<font> 03B2;;;;N;;;;;
+1D738;MATHEMATICAL BOLD ITALIC SMALL GAMMA;Ll;0;L;<font> 03B3;;;;N;;;;;
+1D739;MATHEMATICAL BOLD ITALIC SMALL DELTA;Ll;0;L;<font> 03B4;;;;N;;;;;
+1D73A;MATHEMATICAL BOLD ITALIC SMALL EPSILON;Ll;0;L;<font> 03B5;;;;N;;;;;
+1D73B;MATHEMATICAL BOLD ITALIC SMALL ZETA;Ll;0;L;<font> 03B6;;;;N;;;;;
+1D73C;MATHEMATICAL BOLD ITALIC SMALL ETA;Ll;0;L;<font> 03B7;;;;N;;;;;
+1D73D;MATHEMATICAL BOLD ITALIC SMALL THETA;Ll;0;L;<font> 03B8;;;;N;;;;;
+1D73E;MATHEMATICAL BOLD ITALIC SMALL IOTA;Ll;0;L;<font> 03B9;;;;N;;;;;
+1D73F;MATHEMATICAL BOLD ITALIC SMALL KAPPA;Ll;0;L;<font> 03BA;;;;N;;;;;
+1D740;MATHEMATICAL BOLD ITALIC SMALL LAMDA;Ll;0;L;<font> 03BB;;;;N;;;;;
+1D741;MATHEMATICAL BOLD ITALIC SMALL MU;Ll;0;L;<font> 03BC;;;;N;;;;;
+1D742;MATHEMATICAL BOLD ITALIC SMALL NU;Ll;0;L;<font> 03BD;;;;N;;;;;
+1D743;MATHEMATICAL BOLD ITALIC SMALL XI;Ll;0;L;<font> 03BE;;;;N;;;;;
+1D744;MATHEMATICAL BOLD ITALIC SMALL OMICRON;Ll;0;L;<font> 03BF;;;;N;;;;;
+1D745;MATHEMATICAL BOLD ITALIC SMALL PI;Ll;0;L;<font> 03C0;;;;N;;;;;
+1D746;MATHEMATICAL BOLD ITALIC SMALL RHO;Ll;0;L;<font> 03C1;;;;N;;;;;
+1D747;MATHEMATICAL BOLD ITALIC SMALL FINAL SIGMA;Ll;0;L;<font> 03C2;;;;N;;;;;
+1D748;MATHEMATICAL BOLD ITALIC SMALL SIGMA;Ll;0;L;<font> 03C3;;;;N;;;;;
+1D749;MATHEMATICAL BOLD ITALIC SMALL TAU;Ll;0;L;<font> 03C4;;;;N;;;;;
+1D74A;MATHEMATICAL BOLD ITALIC SMALL UPSILON;Ll;0;L;<font> 03C5;;;;N;;;;;
+1D74B;MATHEMATICAL BOLD ITALIC SMALL PHI;Ll;0;L;<font> 03C6;;;;N;;;;;
+1D74C;MATHEMATICAL BOLD ITALIC SMALL CHI;Ll;0;L;<font> 03C7;;;;N;;;;;
+1D74D;MATHEMATICAL BOLD ITALIC SMALL PSI;Ll;0;L;<font> 03C8;;;;N;;;;;
+1D74E;MATHEMATICAL BOLD ITALIC SMALL OMEGA;Ll;0;L;<font> 03C9;;;;N;;;;;
+1D74F;MATHEMATICAL BOLD ITALIC PARTIAL DIFFERENTIAL;Sm;0;ON;<font> 2202;;;;Y;;;;;
+1D750;MATHEMATICAL BOLD ITALIC EPSILON SYMBOL;Ll;0;L;<font> 03F5;;;;N;;;;;
+1D751;MATHEMATICAL BOLD ITALIC THETA SYMBOL;Ll;0;L;<font> 03D1;;;;N;;;;;
+1D752;MATHEMATICAL BOLD ITALIC KAPPA SYMBOL;Ll;0;L;<font> 03F0;;;;N;;;;;
+1D753;MATHEMATICAL BOLD ITALIC PHI SYMBOL;Ll;0;L;<font> 03D5;;;;N;;;;;
+1D754;MATHEMATICAL BOLD ITALIC RHO SYMBOL;Ll;0;L;<font> 03F1;;;;N;;;;;
+1D755;MATHEMATICAL BOLD ITALIC PI SYMBOL;Ll;0;L;<font> 03D6;;;;N;;;;;
+1D756;MATHEMATICAL SANS-SERIF BOLD CAPITAL ALPHA;Lu;0;L;<font> 0391;;;;N;;;;;
+1D757;MATHEMATICAL SANS-SERIF BOLD CAPITAL BETA;Lu;0;L;<font> 0392;;;;N;;;;;
+1D758;MATHEMATICAL SANS-SERIF BOLD CAPITAL GAMMA;Lu;0;L;<font> 0393;;;;N;;;;;
+1D759;MATHEMATICAL SANS-SERIF BOLD CAPITAL DELTA;Lu;0;L;<font> 0394;;;;N;;;;;
+1D75A;MATHEMATICAL SANS-SERIF BOLD CAPITAL EPSILON;Lu;0;L;<font> 0395;;;;N;;;;;
+1D75B;MATHEMATICAL SANS-SERIF BOLD CAPITAL ZETA;Lu;0;L;<font> 0396;;;;N;;;;;
+1D75C;MATHEMATICAL SANS-SERIF BOLD CAPITAL ETA;Lu;0;L;<font> 0397;;;;N;;;;;
+1D75D;MATHEMATICAL SANS-SERIF BOLD CAPITAL THETA;Lu;0;L;<font> 0398;;;;N;;;;;
+1D75E;MATHEMATICAL SANS-SERIF BOLD CAPITAL IOTA;Lu;0;L;<font> 0399;;;;N;;;;;
+1D75F;MATHEMATICAL SANS-SERIF BOLD CAPITAL KAPPA;Lu;0;L;<font> 039A;;;;N;;;;;
+1D760;MATHEMATICAL SANS-SERIF BOLD CAPITAL LAMDA;Lu;0;L;<font> 039B;;;;N;;;;;
+1D761;MATHEMATICAL SANS-SERIF BOLD CAPITAL MU;Lu;0;L;<font> 039C;;;;N;;;;;
+1D762;MATHEMATICAL SANS-SERIF BOLD CAPITAL NU;Lu;0;L;<font> 039D;;;;N;;;;;
+1D763;MATHEMATICAL SANS-SERIF BOLD CAPITAL XI;Lu;0;L;<font> 039E;;;;N;;;;;
+1D764;MATHEMATICAL SANS-SERIF BOLD CAPITAL OMICRON;Lu;0;L;<font> 039F;;;;N;;;;;
+1D765;MATHEMATICAL SANS-SERIF BOLD CAPITAL PI;Lu;0;L;<font> 03A0;;;;N;;;;;
+1D766;MATHEMATICAL SANS-SERIF BOLD CAPITAL RHO;Lu;0;L;<font> 03A1;;;;N;;;;;
+1D767;MATHEMATICAL SANS-SERIF BOLD CAPITAL THETA SYMBOL;Lu;0;L;<font> 03F4;;;;N;;;;;
+1D768;MATHEMATICAL SANS-SERIF BOLD CAPITAL SIGMA;Lu;0;L;<font> 03A3;;;;N;;;;;
+1D769;MATHEMATICAL SANS-SERIF BOLD CAPITAL TAU;Lu;0;L;<font> 03A4;;;;N;;;;;
+1D76A;MATHEMATICAL SANS-SERIF BOLD CAPITAL UPSILON;Lu;0;L;<font> 03A5;;;;N;;;;;
+1D76B;MATHEMATICAL SANS-SERIF BOLD CAPITAL PHI;Lu;0;L;<font> 03A6;;;;N;;;;;
+1D76C;MATHEMATICAL SANS-SERIF BOLD CAPITAL CHI;Lu;0;L;<font> 03A7;;;;N;;;;;
+1D76D;MATHEMATICAL SANS-SERIF BOLD CAPITAL PSI;Lu;0;L;<font> 03A8;;;;N;;;;;
+1D76E;MATHEMATICAL SANS-SERIF BOLD CAPITAL OMEGA;Lu;0;L;<font> 03A9;;;;N;;;;;
+1D76F;MATHEMATICAL SANS-SERIF BOLD NABLA;Sm;0;L;<font> 2207;;;;N;;;;;
+1D770;MATHEMATICAL SANS-SERIF BOLD SMALL ALPHA;Ll;0;L;<font> 03B1;;;;N;;;;;
+1D771;MATHEMATICAL SANS-SERIF BOLD SMALL BETA;Ll;0;L;<font> 03B2;;;;N;;;;;
+1D772;MATHEMATICAL SANS-SERIF BOLD SMALL GAMMA;Ll;0;L;<font> 03B3;;;;N;;;;;
+1D773;MATHEMATICAL SANS-SERIF BOLD SMALL DELTA;Ll;0;L;<font> 03B4;;;;N;;;;;
+1D774;MATHEMATICAL SANS-SERIF BOLD SMALL EPSILON;Ll;0;L;<font> 03B5;;;;N;;;;;
+1D775;MATHEMATICAL SANS-SERIF BOLD SMALL ZETA;Ll;0;L;<font> 03B6;;;;N;;;;;
+1D776;MATHEMATICAL SANS-SERIF BOLD SMALL ETA;Ll;0;L;<font> 03B7;;;;N;;;;;
+1D777;MATHEMATICAL SANS-SERIF BOLD SMALL THETA;Ll;0;L;<font> 03B8;;;;N;;;;;
+1D778;MATHEMATICAL SANS-SERIF BOLD SMALL IOTA;Ll;0;L;<font> 03B9;;;;N;;;;;
+1D779;MATHEMATICAL SANS-SERIF BOLD SMALL KAPPA;Ll;0;L;<font> 03BA;;;;N;;;;;
+1D77A;MATHEMATICAL SANS-SERIF BOLD SMALL LAMDA;Ll;0;L;<font> 03BB;;;;N;;;;;
+1D77B;MATHEMATICAL SANS-SERIF BOLD SMALL MU;Ll;0;L;<font> 03BC;;;;N;;;;;
+1D77C;MATHEMATICAL SANS-SERIF BOLD SMALL NU;Ll;0;L;<font> 03BD;;;;N;;;;;
+1D77D;MATHEMATICAL SANS-SERIF BOLD SMALL XI;Ll;0;L;<font> 03BE;;;;N;;;;;
+1D77E;MATHEMATICAL SANS-SERIF BOLD SMALL OMICRON;Ll;0;L;<font> 03BF;;;;N;;;;;
+1D77F;MATHEMATICAL SANS-SERIF BOLD SMALL PI;Ll;0;L;<font> 03C0;;;;N;;;;;
+1D780;MATHEMATICAL SANS-SERIF BOLD SMALL RHO;Ll;0;L;<font> 03C1;;;;N;;;;;
+1D781;MATHEMATICAL SANS-SERIF BOLD SMALL FINAL SIGMA;Ll;0;L;<font> 03C2;;;;N;;;;;
+1D782;MATHEMATICAL SANS-SERIF BOLD SMALL SIGMA;Ll;0;L;<font> 03C3;;;;N;;;;;
+1D783;MATHEMATICAL SANS-SERIF BOLD SMALL TAU;Ll;0;L;<font> 03C4;;;;N;;;;;
+1D784;MATHEMATICAL SANS-SERIF BOLD SMALL UPSILON;Ll;0;L;<font> 03C5;;;;N;;;;;
+1D785;MATHEMATICAL SANS-SERIF BOLD SMALL PHI;Ll;0;L;<font> 03C6;;;;N;;;;;
+1D786;MATHEMATICAL SANS-SERIF BOLD SMALL CHI;Ll;0;L;<font> 03C7;;;;N;;;;;
+1D787;MATHEMATICAL SANS-SERIF BOLD SMALL PSI;Ll;0;L;<font> 03C8;;;;N;;;;;
+1D788;MATHEMATICAL SANS-SERIF BOLD SMALL OMEGA;Ll;0;L;<font> 03C9;;;;N;;;;;
+1D789;MATHEMATICAL SANS-SERIF BOLD PARTIAL DIFFERENTIAL;Sm;0;ON;<font> 2202;;;;Y;;;;;
+1D78A;MATHEMATICAL SANS-SERIF BOLD EPSILON SYMBOL;Ll;0;L;<font> 03F5;;;;N;;;;;
+1D78B;MATHEMATICAL SANS-SERIF BOLD THETA SYMBOL;Ll;0;L;<font> 03D1;;;;N;;;;;
+1D78C;MATHEMATICAL SANS-SERIF BOLD KAPPA SYMBOL;Ll;0;L;<font> 03F0;;;;N;;;;;
+1D78D;MATHEMATICAL SANS-SERIF BOLD PHI SYMBOL;Ll;0;L;<font> 03D5;;;;N;;;;;
+1D78E;MATHEMATICAL SANS-SERIF BOLD RHO SYMBOL;Ll;0;L;<font> 03F1;;;;N;;;;;
+1D78F;MATHEMATICAL SANS-SERIF BOLD PI SYMBOL;Ll;0;L;<font> 03D6;;;;N;;;;;
+1D790;MATHEMATICAL SANS-SERIF BOLD ITALIC CAPITAL ALPHA;Lu;0;L;<font> 0391;;;;N;;;;;
+1D791;MATHEMATICAL SANS-SERIF BOLD ITALIC CAPITAL BETA;Lu;0;L;<font> 0392;;;;N;;;;;
+1D792;MATHEMATICAL SANS-SERIF BOLD ITALIC CAPITAL GAMMA;Lu;0;L;<font> 0393;;;;N;;;;;
+1D793;MATHEMATICAL SANS-SERIF BOLD ITALIC CAPITAL DELTA;Lu;0;L;<font> 0394;;;;N;;;;;
+1D794;MATHEMATICAL SANS-SERIF BOLD ITALIC CAPITAL EPSILON;Lu;0;L;<font> 0395;;;;N;;;;;
+1D795;MATHEMATICAL SANS-SERIF BOLD ITALIC CAPITAL ZETA;Lu;0;L;<font> 0396;;;;N;;;;;
+1D796;MATHEMATICAL SANS-SERIF BOLD ITALIC CAPITAL ETA;Lu;0;L;<font> 0397;;;;N;;;;;
+1D797;MATHEMATICAL SANS-SERIF BOLD ITALIC CAPITAL THETA;Lu;0;L;<font> 0398;;;;N;;;;;
+1D798;MATHEMATICAL SANS-SERIF BOLD ITALIC CAPITAL IOTA;Lu;0;L;<font> 0399;;;;N;;;;;
+1D799;MATHEMATICAL SANS-SERIF BOLD ITALIC CAPITAL KAPPA;Lu;0;L;<font> 039A;;;;N;;;;;
+1D79A;MATHEMATICAL SANS-SERIF BOLD ITALIC CAPITAL LAMDA;Lu;0;L;<font> 039B;;;;N;;;;;
+1D79B;MATHEMATICAL SANS-SERIF BOLD ITALIC CAPITAL MU;Lu;0;L;<font> 039C;;;;N;;;;;
+1D79C;MATHEMATICAL SANS-SERIF BOLD ITALIC CAPITAL NU;Lu;0;L;<font> 039D;;;;N;;;;;
+1D79D;MATHEMATICAL SANS-SERIF BOLD ITALIC CAPITAL XI;Lu;0;L;<font> 039E;;;;N;;;;;
+1D79E;MATHEMATICAL SANS-SERIF BOLD ITALIC CAPITAL OMICRON;Lu;0;L;<font> 039F;;;;N;;;;;
+1D79F;MATHEMATICAL SANS-SERIF BOLD ITALIC CAPITAL PI;Lu;0;L;<font> 03A0;;;;N;;;;;
+1D7A0;MATHEMATICAL SANS-SERIF BOLD ITALIC CAPITAL RHO;Lu;0;L;<font> 03A1;;;;N;;;;;
+1D7A1;MATHEMATICAL SANS-SERIF BOLD ITALIC CAPITAL THETA SYMBOL;Lu;0;L;<font> 03F4;;;;N;;;;;
+1D7A2;MATHEMATICAL SANS-SERIF BOLD ITALIC CAPITAL SIGMA;Lu;0;L;<font> 03A3;;;;N;;;;;
+1D7A3;MATHEMATICAL SANS-SERIF BOLD ITALIC CAPITAL TAU;Lu;0;L;<font> 03A4;;;;N;;;;;
+1D7A4;MATHEMATICAL SANS-SERIF BOLD ITALIC CAPITAL UPSILON;Lu;0;L;<font> 03A5;;;;N;;;;;
+1D7A5;MATHEMATICAL SANS-SERIF BOLD ITALIC CAPITAL PHI;Lu;0;L;<font> 03A6;;;;N;;;;;
+1D7A6;MATHEMATICAL SANS-SERIF BOLD ITALIC CAPITAL CHI;Lu;0;L;<font> 03A7;;;;N;;;;;
+1D7A7;MATHEMATICAL SANS-SERIF BOLD ITALIC CAPITAL PSI;Lu;0;L;<font> 03A8;;;;N;;;;;
+1D7A8;MATHEMATICAL SANS-SERIF BOLD ITALIC CAPITAL OMEGA;Lu;0;L;<font> 03A9;;;;N;;;;;
+1D7A9;MATHEMATICAL SANS-SERIF BOLD ITALIC NABLA;Sm;0;L;<font> 2207;;;;N;;;;;
+1D7AA;MATHEMATICAL SANS-SERIF BOLD ITALIC SMALL ALPHA;Ll;0;L;<font> 03B1;;;;N;;;;;
+1D7AB;MATHEMATICAL SANS-SERIF BOLD ITALIC SMALL BETA;Ll;0;L;<font> 03B2;;;;N;;;;;
+1D7AC;MATHEMATICAL SANS-SERIF BOLD ITALIC SMALL GAMMA;Ll;0;L;<font> 03B3;;;;N;;;;;
+1D7AD;MATHEMATICAL SANS-SERIF BOLD ITALIC SMALL DELTA;Ll;0;L;<font> 03B4;;;;N;;;;;
+1D7AE;MATHEMATICAL SANS-SERIF BOLD ITALIC SMALL EPSILON;Ll;0;L;<font> 03B5;;;;N;;;;;
+1D7AF;MATHEMATICAL SANS-SERIF BOLD ITALIC SMALL ZETA;Ll;0;L;<font> 03B6;;;;N;;;;;
+1D7B0;MATHEMATICAL SANS-SERIF BOLD ITALIC SMALL ETA;Ll;0;L;<font> 03B7;;;;N;;;;;
+1D7B1;MATHEMATICAL SANS-SERIF BOLD ITALIC SMALL THETA;Ll;0;L;<font> 03B8;;;;N;;;;;
+1D7B2;MATHEMATICAL SANS-SERIF BOLD ITALIC SMALL IOTA;Ll;0;L;<font> 03B9;;;;N;;;;;
+1D7B3;MATHEMATICAL SANS-SERIF BOLD ITALIC SMALL KAPPA;Ll;0;L;<font> 03BA;;;;N;;;;;
+1D7B4;MATHEMATICAL SANS-SERIF BOLD ITALIC SMALL LAMDA;Ll;0;L;<font> 03BB;;;;N;;;;;
+1D7B5;MATHEMATICAL SANS-SERIF BOLD ITALIC SMALL MU;Ll;0;L;<font> 03BC;;;;N;;;;;
+1D7B6;MATHEMATICAL SANS-SERIF BOLD ITALIC SMALL NU;Ll;0;L;<font> 03BD;;;;N;;;;;
+1D7B7;MATHEMATICAL SANS-SERIF BOLD ITALIC SMALL XI;Ll;0;L;<font> 03BE;;;;N;;;;;
+1D7B8;MATHEMATICAL SANS-SERIF BOLD ITALIC SMALL OMICRON;Ll;0;L;<font> 03BF;;;;N;;;;;
+1D7B9;MATHEMATICAL SANS-SERIF BOLD ITALIC SMALL PI;Ll;0;L;<font> 03C0;;;;N;;;;;
+1D7BA;MATHEMATICAL SANS-SERIF BOLD ITALIC SMALL RHO;Ll;0;L;<font> 03C1;;;;N;;;;;
+1D7BB;MATHEMATICAL SANS-SERIF BOLD ITALIC SMALL FINAL SIGMA;Ll;0;L;<font> 03C2;;;;N;;;;;
+1D7BC;MATHEMATICAL SANS-SERIF BOLD ITALIC SMALL SIGMA;Ll;0;L;<font> 03C3;;;;N;;;;;
+1D7BD;MATHEMATICAL SANS-SERIF BOLD ITALIC SMALL TAU;Ll;0;L;<font> 03C4;;;;N;;;;;
+1D7BE;MATHEMATICAL SANS-SERIF BOLD ITALIC SMALL UPSILON;Ll;0;L;<font> 03C5;;;;N;;;;;
+1D7BF;MATHEMATICAL SANS-SERIF BOLD ITALIC SMALL PHI;Ll;0;L;<font> 03C6;;;;N;;;;;
+1D7C0;MATHEMATICAL SANS-SERIF BOLD ITALIC SMALL CHI;Ll;0;L;<font> 03C7;;;;N;;;;;
+1D7C1;MATHEMATICAL SANS-SERIF BOLD ITALIC SMALL PSI;Ll;0;L;<font> 03C8;;;;N;;;;;
+1D7C2;MATHEMATICAL SANS-SERIF BOLD ITALIC SMALL OMEGA;Ll;0;L;<font> 03C9;;;;N;;;;;
+1D7C3;MATHEMATICAL SANS-SERIF BOLD ITALIC PARTIAL DIFFERENTIAL;Sm;0;ON;<font> 2202;;;;Y;;;;;
+1D7C4;MATHEMATICAL SANS-SERIF BOLD ITALIC EPSILON SYMBOL;Ll;0;L;<font> 03F5;;;;N;;;;;
+1D7C5;MATHEMATICAL SANS-SERIF BOLD ITALIC THETA SYMBOL;Ll;0;L;<font> 03D1;;;;N;;;;;
+1D7C6;MATHEMATICAL SANS-SERIF BOLD ITALIC KAPPA SYMBOL;Ll;0;L;<font> 03F0;;;;N;;;;;
+1D7C7;MATHEMATICAL SANS-SERIF BOLD ITALIC PHI SYMBOL;Ll;0;L;<font> 03D5;;;;N;;;;;
+1D7C8;MATHEMATICAL SANS-SERIF BOLD ITALIC RHO SYMBOL;Ll;0;L;<font> 03F1;;;;N;;;;;
+1D7C9;MATHEMATICAL SANS-SERIF BOLD ITALIC PI SYMBOL;Ll;0;L;<font> 03D6;;;;N;;;;;
+1D7CA;MATHEMATICAL BOLD CAPITAL DIGAMMA;Lu;0;L;<font> 03DC;;;;N;;;;;
+1D7CB;MATHEMATICAL BOLD SMALL DIGAMMA;Ll;0;L;<font> 03DD;;;;N;;;;;
+1D7CE;MATHEMATICAL BOLD DIGIT ZERO;Nd;0;EN;<font> 0030;0;0;0;N;;;;;
+1D7CF;MATHEMATICAL BOLD DIGIT ONE;Nd;0;EN;<font> 0031;1;1;1;N;;;;;
+1D7D0;MATHEMATICAL BOLD DIGIT TWO;Nd;0;EN;<font> 0032;2;2;2;N;;;;;
+1D7D1;MATHEMATICAL BOLD DIGIT THREE;Nd;0;EN;<font> 0033;3;3;3;N;;;;;
+1D7D2;MATHEMATICAL BOLD DIGIT FOUR;Nd;0;EN;<font> 0034;4;4;4;N;;;;;
+1D7D3;MATHEMATICAL BOLD DIGIT FIVE;Nd;0;EN;<font> 0035;5;5;5;N;;;;;
+1D7D4;MATHEMATICAL BOLD DIGIT SIX;Nd;0;EN;<font> 0036;6;6;6;N;;;;;
+1D7D5;MATHEMATICAL BOLD DIGIT SEVEN;Nd;0;EN;<font> 0037;7;7;7;N;;;;;
+1D7D6;MATHEMATICAL BOLD DIGIT EIGHT;Nd;0;EN;<font> 0038;8;8;8;N;;;;;
+1D7D7;MATHEMATICAL BOLD DIGIT NINE;Nd;0;EN;<font> 0039;9;9;9;N;;;;;
+1D7D8;MATHEMATICAL DOUBLE-STRUCK DIGIT ZERO;Nd;0;EN;<font> 0030;0;0;0;N;;;;;
+1D7D9;MATHEMATICAL DOUBLE-STRUCK DIGIT ONE;Nd;0;EN;<font> 0031;1;1;1;N;;;;;
+1D7DA;MATHEMATICAL DOUBLE-STRUCK DIGIT TWO;Nd;0;EN;<font> 0032;2;2;2;N;;;;;
+1D7DB;MATHEMATICAL DOUBLE-STRUCK DIGIT THREE;Nd;0;EN;<font> 0033;3;3;3;N;;;;;
+1D7DC;MATHEMATICAL DOUBLE-STRUCK DIGIT FOUR;Nd;0;EN;<font> 0034;4;4;4;N;;;;;
+1D7DD;MATHEMATICAL DOUBLE-STRUCK DIGIT FIVE;Nd;0;EN;<font> 0035;5;5;5;N;;;;;
+1D7DE;MATHEMATICAL DOUBLE-STRUCK DIGIT SIX;Nd;0;EN;<font> 0036;6;6;6;N;;;;;
+1D7DF;MATHEMATICAL DOUBLE-STRUCK DIGIT SEVEN;Nd;0;EN;<font> 0037;7;7;7;N;;;;;
+1D7E0;MATHEMATICAL DOUBLE-STRUCK DIGIT EIGHT;Nd;0;EN;<font> 0038;8;8;8;N;;;;;
+1D7E1;MATHEMATICAL DOUBLE-STRUCK DIGIT NINE;Nd;0;EN;<font> 0039;9;9;9;N;;;;;
+1D7E2;MATHEMATICAL SANS-SERIF DIGIT ZERO;Nd;0;EN;<font> 0030;0;0;0;N;;;;;
+1D7E3;MATHEMATICAL SANS-SERIF DIGIT ONE;Nd;0;EN;<font> 0031;1;1;1;N;;;;;
+1D7E4;MATHEMATICAL SANS-SERIF DIGIT TWO;Nd;0;EN;<font> 0032;2;2;2;N;;;;;
+1D7E5;MATHEMATICAL SANS-SERIF DIGIT THREE;Nd;0;EN;<font> 0033;3;3;3;N;;;;;
+1D7E6;MATHEMATICAL SANS-SERIF DIGIT FOUR;Nd;0;EN;<font> 0034;4;4;4;N;;;;;
+1D7E7;MATHEMATICAL SANS-SERIF DIGIT FIVE;Nd;0;EN;<font> 0035;5;5;5;N;;;;;
+1D7E8;MATHEMATICAL SANS-SERIF DIGIT SIX;Nd;0;EN;<font> 0036;6;6;6;N;;;;;
+1D7E9;MATHEMATICAL SANS-SERIF DIGIT SEVEN;Nd;0;EN;<font> 0037;7;7;7;N;;;;;
+1D7EA;MATHEMATICAL SANS-SERIF DIGIT EIGHT;Nd;0;EN;<font> 0038;8;8;8;N;;;;;
+1D7EB;MATHEMATICAL SANS-SERIF DIGIT NINE;Nd;0;EN;<font> 0039;9;9;9;N;;;;;
+1D7EC;MATHEMATICAL SANS-SERIF BOLD DIGIT ZERO;Nd;0;EN;<font> 0030;0;0;0;N;;;;;
+1D7ED;MATHEMATICAL SANS-SERIF BOLD DIGIT ONE;Nd;0;EN;<font> 0031;1;1;1;N;;;;;
+1D7EE;MATHEMATICAL SANS-SERIF BOLD DIGIT TWO;Nd;0;EN;<font> 0032;2;2;2;N;;;;;
+1D7EF;MATHEMATICAL SANS-SERIF BOLD DIGIT THREE;Nd;0;EN;<font> 0033;3;3;3;N;;;;;
+1D7F0;MATHEMATICAL SANS-SERIF BOLD DIGIT FOUR;Nd;0;EN;<font> 0034;4;4;4;N;;;;;
+1D7F1;MATHEMATICAL SANS-SERIF BOLD DIGIT FIVE;Nd;0;EN;<font> 0035;5;5;5;N;;;;;
+1D7F2;MATHEMATICAL SANS-SERIF BOLD DIGIT SIX;Nd;0;EN;<font> 0036;6;6;6;N;;;;;
+1D7F3;MATHEMATICAL SANS-SERIF BOLD DIGIT SEVEN;Nd;0;EN;<font> 0037;7;7;7;N;;;;;
+1D7F4;MATHEMATICAL SANS-SERIF BOLD DIGIT EIGHT;Nd;0;EN;<font> 0038;8;8;8;N;;;;;
+1D7F5;MATHEMATICAL SANS-SERIF BOLD DIGIT NINE;Nd;0;EN;<font> 0039;9;9;9;N;;;;;
+1D7F6;MATHEMATICAL MONOSPACE DIGIT ZERO;Nd;0;EN;<font> 0030;0;0;0;N;;;;;
+1D7F7;MATHEMATICAL MONOSPACE DIGIT ONE;Nd;0;EN;<font> 0031;1;1;1;N;;;;;
+1D7F8;MATHEMATICAL MONOSPACE DIGIT TWO;Nd;0;EN;<font> 0032;2;2;2;N;;;;;
+1D7F9;MATHEMATICAL MONOSPACE DIGIT THREE;Nd;0;EN;<font> 0033;3;3;3;N;;;;;
+1D7FA;MATHEMATICAL MONOSPACE DIGIT FOUR;Nd;0;EN;<font> 0034;4;4;4;N;;;;;
+1D7FB;MATHEMATICAL MONOSPACE DIGIT FIVE;Nd;0;EN;<font> 0035;5;5;5;N;;;;;
+1D7FC;MATHEMATICAL MONOSPACE DIGIT SIX;Nd;0;EN;<font> 0036;6;6;6;N;;;;;
+1D7FD;MATHEMATICAL MONOSPACE DIGIT SEVEN;Nd;0;EN;<font> 0037;7;7;7;N;;;;;
+1D7FE;MATHEMATICAL MONOSPACE DIGIT EIGHT;Nd;0;EN;<font> 0038;8;8;8;N;;;;;
+1D7FF;MATHEMATICAL MONOSPACE DIGIT NINE;Nd;0;EN;<font> 0039;9;9;9;N;;;;;
+1D800;SIGNWRITING HAND-FIST INDEX;So;0;L;;;;;N;;;;;
+1D801;SIGNWRITING HAND-CIRCLE INDEX;So;0;L;;;;;N;;;;;
+1D802;SIGNWRITING HAND-CUP INDEX;So;0;L;;;;;N;;;;;
+1D803;SIGNWRITING HAND-OVAL INDEX;So;0;L;;;;;N;;;;;
+1D804;SIGNWRITING HAND-HINGE INDEX;So;0;L;;;;;N;;;;;
+1D805;SIGNWRITING HAND-ANGLE INDEX;So;0;L;;;;;N;;;;;
+1D806;SIGNWRITING HAND-FIST INDEX BENT;So;0;L;;;;;N;;;;;
+1D807;SIGNWRITING HAND-CIRCLE INDEX BENT;So;0;L;;;;;N;;;;;
+1D808;SIGNWRITING HAND-FIST THUMB UNDER INDEX BENT;So;0;L;;;;;N;;;;;
+1D809;SIGNWRITING HAND-FIST INDEX RAISED KNUCKLE;So;0;L;;;;;N;;;;;
+1D80A;SIGNWRITING HAND-FIST INDEX CUPPED;So;0;L;;;;;N;;;;;
+1D80B;SIGNWRITING HAND-FIST INDEX HINGED;So;0;L;;;;;N;;;;;
+1D80C;SIGNWRITING HAND-FIST INDEX HINGED LOW;So;0;L;;;;;N;;;;;
+1D80D;SIGNWRITING HAND-CIRCLE INDEX HINGE;So;0;L;;;;;N;;;;;
+1D80E;SIGNWRITING HAND-FIST INDEX MIDDLE;So;0;L;;;;;N;;;;;
+1D80F;SIGNWRITING HAND-CIRCLE INDEX MIDDLE;So;0;L;;;;;N;;;;;
+1D810;SIGNWRITING HAND-FIST INDEX MIDDLE BENT;So;0;L;;;;;N;;;;;
+1D811;SIGNWRITING HAND-FIST INDEX MIDDLE RAISED KNUCKLES;So;0;L;;;;;N;;;;;
+1D812;SIGNWRITING HAND-FIST INDEX MIDDLE HINGED;So;0;L;;;;;N;;;;;
+1D813;SIGNWRITING HAND-FIST INDEX UP MIDDLE HINGED;So;0;L;;;;;N;;;;;
+1D814;SIGNWRITING HAND-FIST INDEX HINGED MIDDLE UP;So;0;L;;;;;N;;;;;
+1D815;SIGNWRITING HAND-FIST INDEX MIDDLE CONJOINED;So;0;L;;;;;N;;;;;
+1D816;SIGNWRITING HAND-FIST INDEX MIDDLE CONJOINED INDEX BENT;So;0;L;;;;;N;;;;;
+1D817;SIGNWRITING HAND-FIST INDEX MIDDLE CONJOINED MIDDLE BENT;So;0;L;;;;;N;;;;;
+1D818;SIGNWRITING HAND-FIST INDEX MIDDLE CONJOINED CUPPED;So;0;L;;;;;N;;;;;
+1D819;SIGNWRITING HAND-FIST INDEX MIDDLE CONJOINED HINGED;So;0;L;;;;;N;;;;;
+1D81A;SIGNWRITING HAND-FIST INDEX MIDDLE CROSSED;So;0;L;;;;;N;;;;;
+1D81B;SIGNWRITING HAND-CIRCLE INDEX MIDDLE CROSSED;So;0;L;;;;;N;;;;;
+1D81C;SIGNWRITING HAND-FIST MIDDLE BENT OVER INDEX;So;0;L;;;;;N;;;;;
+1D81D;SIGNWRITING HAND-FIST INDEX BENT OVER MIDDLE;So;0;L;;;;;N;;;;;
+1D81E;SIGNWRITING HAND-FIST INDEX MIDDLE THUMB;So;0;L;;;;;N;;;;;
+1D81F;SIGNWRITING HAND-CIRCLE INDEX MIDDLE THUMB;So;0;L;;;;;N;;;;;
+1D820;SIGNWRITING HAND-FIST INDEX MIDDLE STRAIGHT THUMB BENT;So;0;L;;;;;N;;;;;
+1D821;SIGNWRITING HAND-FIST INDEX MIDDLE BENT THUMB STRAIGHT;So;0;L;;;;;N;;;;;
+1D822;SIGNWRITING HAND-FIST INDEX MIDDLE THUMB BENT;So;0;L;;;;;N;;;;;
+1D823;SIGNWRITING HAND-FIST INDEX MIDDLE HINGED SPREAD THUMB SIDE;So;0;L;;;;;N;;;;;
+1D824;SIGNWRITING HAND-FIST INDEX UP MIDDLE HINGED THUMB SIDE;So;0;L;;;;;N;;;;;
+1D825;SIGNWRITING HAND-FIST INDEX UP MIDDLE HINGED THUMB CONJOINED;So;0;L;;;;;N;;;;;
+1D826;SIGNWRITING HAND-FIST INDEX HINGED MIDDLE UP THUMB SIDE;So;0;L;;;;;N;;;;;
+1D827;SIGNWRITING HAND-FIST INDEX MIDDLE UP SPREAD THUMB FORWARD;So;0;L;;;;;N;;;;;
+1D828;SIGNWRITING HAND-FIST INDEX MIDDLE THUMB CUPPED;So;0;L;;;;;N;;;;;
+1D829;SIGNWRITING HAND-FIST INDEX MIDDLE THUMB CIRCLED;So;0;L;;;;;N;;;;;
+1D82A;SIGNWRITING HAND-FIST INDEX MIDDLE THUMB HOOKED;So;0;L;;;;;N;;;;;
+1D82B;SIGNWRITING HAND-FIST INDEX MIDDLE THUMB HINGED;So;0;L;;;;;N;;;;;
+1D82C;SIGNWRITING HAND-FIST THUMB BETWEEN INDEX MIDDLE STRAIGHT;So;0;L;;;;;N;;;;;
+1D82D;SIGNWRITING HAND-FIST INDEX MIDDLE CONJOINED THUMB SIDE;So;0;L;;;;;N;;;;;
+1D82E;SIGNWRITING HAND-FIST INDEX MIDDLE CONJOINED THUMB SIDE CONJOINED;So;0;L;;;;;N;;;;;
+1D82F;SIGNWRITING HAND-FIST INDEX MIDDLE CONJOINED THUMB SIDE BENT;So;0;L;;;;;N;;;;;
+1D830;SIGNWRITING HAND-FIST MIDDLE THUMB HOOKED INDEX UP;So;0;L;;;;;N;;;;;
+1D831;SIGNWRITING HAND-FIST INDEX THUMB HOOKED MIDDLE UP;So;0;L;;;;;N;;;;;
+1D832;SIGNWRITING HAND-FIST INDEX MIDDLE CONJOINED HINGED THUMB SIDE;So;0;L;;;;;N;;;;;
+1D833;SIGNWRITING HAND-FIST INDEX MIDDLE CROSSED THUMB SIDE;So;0;L;;;;;N;;;;;
+1D834;SIGNWRITING HAND-FIST INDEX MIDDLE CONJOINED THUMB FORWARD;So;0;L;;;;;N;;;;;
+1D835;SIGNWRITING HAND-FIST INDEX MIDDLE CONJOINED CUPPED THUMB FORWARD;So;0;L;;;;;N;;;;;
+1D836;SIGNWRITING HAND-FIST MIDDLE THUMB CUPPED INDEX UP;So;0;L;;;;;N;;;;;
+1D837;SIGNWRITING HAND-FIST INDEX THUMB CUPPED MIDDLE UP;So;0;L;;;;;N;;;;;
+1D838;SIGNWRITING HAND-FIST MIDDLE THUMB CIRCLED INDEX UP;So;0;L;;;;;N;;;;;
+1D839;SIGNWRITING HAND-FIST MIDDLE THUMB CIRCLED INDEX HINGED;So;0;L;;;;;N;;;;;
+1D83A;SIGNWRITING HAND-FIST INDEX THUMB ANGLED OUT MIDDLE UP;So;0;L;;;;;N;;;;;
+1D83B;SIGNWRITING HAND-FIST INDEX THUMB ANGLED IN MIDDLE UP;So;0;L;;;;;N;;;;;
+1D83C;SIGNWRITING HAND-FIST INDEX THUMB CIRCLED MIDDLE UP;So;0;L;;;;;N;;;;;
+1D83D;SIGNWRITING HAND-FIST INDEX MIDDLE THUMB CONJOINED HINGED;So;0;L;;;;;N;;;;;
+1D83E;SIGNWRITING HAND-FIST INDEX MIDDLE THUMB ANGLED OUT;So;0;L;;;;;N;;;;;
+1D83F;SIGNWRITING HAND-FIST INDEX MIDDLE THUMB ANGLED;So;0;L;;;;;N;;;;;
+1D840;SIGNWRITING HAND-FIST MIDDLE THUMB ANGLED OUT INDEX UP;So;0;L;;;;;N;;;;;
+1D841;SIGNWRITING HAND-FIST MIDDLE THUMB ANGLED OUT INDEX CROSSED;So;0;L;;;;;N;;;;;
+1D842;SIGNWRITING HAND-FIST MIDDLE THUMB ANGLED INDEX UP;So;0;L;;;;;N;;;;;
+1D843;SIGNWRITING HAND-FIST INDEX THUMB HOOKED MIDDLE HINGED;So;0;L;;;;;N;;;;;
+1D844;SIGNWRITING HAND-FLAT FOUR FINGERS;So;0;L;;;;;N;;;;;
+1D845;SIGNWRITING HAND-FLAT FOUR FINGERS BENT;So;0;L;;;;;N;;;;;
+1D846;SIGNWRITING HAND-FLAT FOUR FINGERS HINGED;So;0;L;;;;;N;;;;;
+1D847;SIGNWRITING HAND-FLAT FOUR FINGERS CONJOINED;So;0;L;;;;;N;;;;;
+1D848;SIGNWRITING HAND-FLAT FOUR FINGERS CONJOINED SPLIT;So;0;L;;;;;N;;;;;
+1D849;SIGNWRITING HAND-CLAW FOUR FINGERS CONJOINED;So;0;L;;;;;N;;;;;
+1D84A;SIGNWRITING HAND-FIST FOUR FINGERS CONJOINED BENT;So;0;L;;;;;N;;;;;
+1D84B;SIGNWRITING HAND-HINGE FOUR FINGERS CONJOINED;So;0;L;;;;;N;;;;;
+1D84C;SIGNWRITING HAND-FLAT FIVE FINGERS SPREAD;So;0;L;;;;;N;;;;;
+1D84D;SIGNWRITING HAND-FLAT HEEL FIVE FINGERS SPREAD;So;0;L;;;;;N;;;;;
+1D84E;SIGNWRITING HAND-FLAT FIVE FINGERS SPREAD FOUR BENT;So;0;L;;;;;N;;;;;
+1D84F;SIGNWRITING HAND-FLAT HEEL FIVE FINGERS SPREAD FOUR BENT;So;0;L;;;;;N;;;;;
+1D850;SIGNWRITING HAND-FLAT FIVE FINGERS SPREAD BENT;So;0;L;;;;;N;;;;;
+1D851;SIGNWRITING HAND-FLAT HEEL FIVE FINGERS SPREAD BENT;So;0;L;;;;;N;;;;;
+1D852;SIGNWRITING HAND-FLAT FIVE FINGERS SPREAD THUMB FORWARD;So;0;L;;;;;N;;;;;
+1D853;SIGNWRITING HAND-CUP FIVE FINGERS SPREAD;So;0;L;;;;;N;;;;;
+1D854;SIGNWRITING HAND-CUP FIVE FINGERS SPREAD OPEN;So;0;L;;;;;N;;;;;
+1D855;SIGNWRITING HAND-HINGE FIVE FINGERS SPREAD OPEN;So;0;L;;;;;N;;;;;
+1D856;SIGNWRITING HAND-OVAL FIVE FINGERS SPREAD;So;0;L;;;;;N;;;;;
+1D857;SIGNWRITING HAND-FLAT FIVE FINGERS SPREAD HINGED;So;0;L;;;;;N;;;;;
+1D858;SIGNWRITING HAND-FLAT FIVE FINGERS SPREAD HINGED THUMB SIDE;So;0;L;;;;;N;;;;;
+1D859;SIGNWRITING HAND-FLAT FIVE FINGERS SPREAD HINGED NO THUMB;So;0;L;;;;;N;;;;;
+1D85A;SIGNWRITING HAND-FLAT;So;0;L;;;;;N;;;;;
+1D85B;SIGNWRITING HAND-FLAT BETWEEN PALM FACINGS;So;0;L;;;;;N;;;;;
+1D85C;SIGNWRITING HAND-FLAT HEEL;So;0;L;;;;;N;;;;;
+1D85D;SIGNWRITING HAND-FLAT THUMB SIDE;So;0;L;;;;;N;;;;;
+1D85E;SIGNWRITING HAND-FLAT HEEL THUMB SIDE;So;0;L;;;;;N;;;;;
+1D85F;SIGNWRITING HAND-FLAT THUMB BENT;So;0;L;;;;;N;;;;;
+1D860;SIGNWRITING HAND-FLAT THUMB FORWARD;So;0;L;;;;;N;;;;;
+1D861;SIGNWRITING HAND-FLAT SPLIT INDEX THUMB SIDE;So;0;L;;;;;N;;;;;
+1D862;SIGNWRITING HAND-FLAT SPLIT CENTRE;So;0;L;;;;;N;;;;;
+1D863;SIGNWRITING HAND-FLAT SPLIT CENTRE THUMB SIDE;So;0;L;;;;;N;;;;;
+1D864;SIGNWRITING HAND-FLAT SPLIT CENTRE THUMB SIDE BENT;So;0;L;;;;;N;;;;;
+1D865;SIGNWRITING HAND-FLAT SPLIT LITTLE;So;0;L;;;;;N;;;;;
+1D866;SIGNWRITING HAND-CLAW;So;0;L;;;;;N;;;;;
+1D867;SIGNWRITING HAND-CLAW THUMB SIDE;So;0;L;;;;;N;;;;;
+1D868;SIGNWRITING HAND-CLAW NO THUMB;So;0;L;;;;;N;;;;;
+1D869;SIGNWRITING HAND-CLAW THUMB FORWARD;So;0;L;;;;;N;;;;;
+1D86A;SIGNWRITING HAND-HOOK CURLICUE;So;0;L;;;;;N;;;;;
+1D86B;SIGNWRITING HAND-HOOK;So;0;L;;;;;N;;;;;
+1D86C;SIGNWRITING HAND-CUP OPEN;So;0;L;;;;;N;;;;;
+1D86D;SIGNWRITING HAND-CUP;So;0;L;;;;;N;;;;;
+1D86E;SIGNWRITING HAND-CUP OPEN THUMB SIDE;So;0;L;;;;;N;;;;;
+1D86F;SIGNWRITING HAND-CUP THUMB SIDE;So;0;L;;;;;N;;;;;
+1D870;SIGNWRITING HAND-CUP OPEN NO THUMB;So;0;L;;;;;N;;;;;
+1D871;SIGNWRITING HAND-CUP NO THUMB;So;0;L;;;;;N;;;;;
+1D872;SIGNWRITING HAND-CUP OPEN THUMB FORWARD;So;0;L;;;;;N;;;;;
+1D873;SIGNWRITING HAND-CUP THUMB FORWARD;So;0;L;;;;;N;;;;;
+1D874;SIGNWRITING HAND-CURLICUE OPEN;So;0;L;;;;;N;;;;;
+1D875;SIGNWRITING HAND-CURLICUE;So;0;L;;;;;N;;;;;
+1D876;SIGNWRITING HAND-CIRCLE;So;0;L;;;;;N;;;;;
+1D877;SIGNWRITING HAND-OVAL;So;0;L;;;;;N;;;;;
+1D878;SIGNWRITING HAND-OVAL THUMB SIDE;So;0;L;;;;;N;;;;;
+1D879;SIGNWRITING HAND-OVAL NO THUMB;So;0;L;;;;;N;;;;;
+1D87A;SIGNWRITING HAND-OVAL THUMB FORWARD;So;0;L;;;;;N;;;;;
+1D87B;SIGNWRITING HAND-HINGE OPEN;So;0;L;;;;;N;;;;;
+1D87C;SIGNWRITING HAND-HINGE OPEN THUMB FORWARD;So;0;L;;;;;N;;;;;
+1D87D;SIGNWRITING HAND-HINGE;So;0;L;;;;;N;;;;;
+1D87E;SIGNWRITING HAND-HINGE SMALL;So;0;L;;;;;N;;;;;
+1D87F;SIGNWRITING HAND-HINGE OPEN THUMB SIDE;So;0;L;;;;;N;;;;;
+1D880;SIGNWRITING HAND-HINGE THUMB SIDE;So;0;L;;;;;N;;;;;
+1D881;SIGNWRITING HAND-HINGE OPEN NO THUMB;So;0;L;;;;;N;;;;;
+1D882;SIGNWRITING HAND-HINGE NO THUMB;So;0;L;;;;;N;;;;;
+1D883;SIGNWRITING HAND-HINGE THUMB SIDE TOUCHING INDEX;So;0;L;;;;;N;;;;;
+1D884;SIGNWRITING HAND-HINGE THUMB BETWEEN MIDDLE RING;So;0;L;;;;;N;;;;;
+1D885;SIGNWRITING HAND-ANGLE;So;0;L;;;;;N;;;;;
+1D886;SIGNWRITING HAND-FIST INDEX MIDDLE RING;So;0;L;;;;;N;;;;;
+1D887;SIGNWRITING HAND-CIRCLE INDEX MIDDLE RING;So;0;L;;;;;N;;;;;
+1D888;SIGNWRITING HAND-HINGE INDEX MIDDLE RING;So;0;L;;;;;N;;;;;
+1D889;SIGNWRITING HAND-ANGLE INDEX MIDDLE RING;So;0;L;;;;;N;;;;;
+1D88A;SIGNWRITING HAND-HINGE LITTLE;So;0;L;;;;;N;;;;;
+1D88B;SIGNWRITING HAND-FIST INDEX MIDDLE RING BENT;So;0;L;;;;;N;;;;;
+1D88C;SIGNWRITING HAND-FIST INDEX MIDDLE RING CONJOINED;So;0;L;;;;;N;;;;;
+1D88D;SIGNWRITING HAND-HINGE INDEX MIDDLE RING CONJOINED;So;0;L;;;;;N;;;;;
+1D88E;SIGNWRITING HAND-FIST LITTLE DOWN;So;0;L;;;;;N;;;;;
+1D88F;SIGNWRITING HAND-FIST LITTLE DOWN RIPPLE STRAIGHT;So;0;L;;;;;N;;;;;
+1D890;SIGNWRITING HAND-FIST LITTLE DOWN RIPPLE CURVED;So;0;L;;;;;N;;;;;
+1D891;SIGNWRITING HAND-FIST LITTLE DOWN OTHERS CIRCLED;So;0;L;;;;;N;;;;;
+1D892;SIGNWRITING HAND-FIST LITTLE UP;So;0;L;;;;;N;;;;;
+1D893;SIGNWRITING HAND-FIST THUMB UNDER LITTLE UP;So;0;L;;;;;N;;;;;
+1D894;SIGNWRITING HAND-CIRCLE LITTLE UP;So;0;L;;;;;N;;;;;
+1D895;SIGNWRITING HAND-OVAL LITTLE UP;So;0;L;;;;;N;;;;;
+1D896;SIGNWRITING HAND-ANGLE LITTLE UP;So;0;L;;;;;N;;;;;
+1D897;SIGNWRITING HAND-FIST LITTLE RAISED KNUCKLE;So;0;L;;;;;N;;;;;
+1D898;SIGNWRITING HAND-FIST LITTLE BENT;So;0;L;;;;;N;;;;;
+1D899;SIGNWRITING HAND-FIST LITTLE TOUCHES THUMB;So;0;L;;;;;N;;;;;
+1D89A;SIGNWRITING HAND-FIST LITTLE THUMB;So;0;L;;;;;N;;;;;
+1D89B;SIGNWRITING HAND-HINGE LITTLE THUMB;So;0;L;;;;;N;;;;;
+1D89C;SIGNWRITING HAND-FIST LITTLE INDEX THUMB;So;0;L;;;;;N;;;;;
+1D89D;SIGNWRITING HAND-HINGE LITTLE INDEX THUMB;So;0;L;;;;;N;;;;;
+1D89E;SIGNWRITING HAND-ANGLE LITTLE INDEX THUMB INDEX THUMB OUT;So;0;L;;;;;N;;;;;
+1D89F;SIGNWRITING HAND-ANGLE LITTLE INDEX THUMB INDEX THUMB;So;0;L;;;;;N;;;;;
+1D8A0;SIGNWRITING HAND-FIST LITTLE INDEX;So;0;L;;;;;N;;;;;
+1D8A1;SIGNWRITING HAND-CIRCLE LITTLE INDEX;So;0;L;;;;;N;;;;;
+1D8A2;SIGNWRITING HAND-HINGE LITTLE INDEX;So;0;L;;;;;N;;;;;
+1D8A3;SIGNWRITING HAND-ANGLE LITTLE INDEX;So;0;L;;;;;N;;;;;
+1D8A4;SIGNWRITING HAND-FIST INDEX MIDDLE LITTLE;So;0;L;;;;;N;;;;;
+1D8A5;SIGNWRITING HAND-CIRCLE INDEX MIDDLE LITTLE;So;0;L;;;;;N;;;;;
+1D8A6;SIGNWRITING HAND-HINGE INDEX MIDDLE LITTLE;So;0;L;;;;;N;;;;;
+1D8A7;SIGNWRITING HAND-HINGE RING;So;0;L;;;;;N;;;;;
+1D8A8;SIGNWRITING HAND-ANGLE INDEX MIDDLE LITTLE;So;0;L;;;;;N;;;;;
+1D8A9;SIGNWRITING HAND-FIST INDEX MIDDLE CROSS LITTLE;So;0;L;;;;;N;;;;;
+1D8AA;SIGNWRITING HAND-CIRCLE INDEX MIDDLE CROSS LITTLE;So;0;L;;;;;N;;;;;
+1D8AB;SIGNWRITING HAND-FIST RING DOWN;So;0;L;;;;;N;;;;;
+1D8AC;SIGNWRITING HAND-HINGE RING DOWN INDEX THUMB HOOK MIDDLE;So;0;L;;;;;N;;;;;
+1D8AD;SIGNWRITING HAND-ANGLE RING DOWN MIDDLE THUMB INDEX CROSS;So;0;L;;;;;N;;;;;
+1D8AE;SIGNWRITING HAND-FIST RING UP;So;0;L;;;;;N;;;;;
+1D8AF;SIGNWRITING HAND-FIST RING RAISED KNUCKLE;So;0;L;;;;;N;;;;;
+1D8B0;SIGNWRITING HAND-FIST RING LITTLE;So;0;L;;;;;N;;;;;
+1D8B1;SIGNWRITING HAND-CIRCLE RING LITTLE;So;0;L;;;;;N;;;;;
+1D8B2;SIGNWRITING HAND-OVAL RING LITTLE;So;0;L;;;;;N;;;;;
+1D8B3;SIGNWRITING HAND-ANGLE RING LITTLE;So;0;L;;;;;N;;;;;
+1D8B4;SIGNWRITING HAND-FIST RING MIDDLE;So;0;L;;;;;N;;;;;
+1D8B5;SIGNWRITING HAND-FIST RING MIDDLE CONJOINED;So;0;L;;;;;N;;;;;
+1D8B6;SIGNWRITING HAND-FIST RING MIDDLE RAISED KNUCKLES;So;0;L;;;;;N;;;;;
+1D8B7;SIGNWRITING HAND-FIST RING INDEX;So;0;L;;;;;N;;;;;
+1D8B8;SIGNWRITING HAND-FIST RING THUMB;So;0;L;;;;;N;;;;;
+1D8B9;SIGNWRITING HAND-HOOK RING THUMB;So;0;L;;;;;N;;;;;
+1D8BA;SIGNWRITING HAND-FIST INDEX RING LITTLE;So;0;L;;;;;N;;;;;
+1D8BB;SIGNWRITING HAND-CIRCLE INDEX RING LITTLE;So;0;L;;;;;N;;;;;
+1D8BC;SIGNWRITING HAND-CURLICUE INDEX RING LITTLE ON;So;0;L;;;;;N;;;;;
+1D8BD;SIGNWRITING HAND-HOOK INDEX RING LITTLE OUT;So;0;L;;;;;N;;;;;
+1D8BE;SIGNWRITING HAND-HOOK INDEX RING LITTLE IN;So;0;L;;;;;N;;;;;
+1D8BF;SIGNWRITING HAND-HOOK INDEX RING LITTLE UNDER;So;0;L;;;;;N;;;;;
+1D8C0;SIGNWRITING HAND-CUP INDEX RING LITTLE;So;0;L;;;;;N;;;;;
+1D8C1;SIGNWRITING HAND-HINGE INDEX RING LITTLE;So;0;L;;;;;N;;;;;
+1D8C2;SIGNWRITING HAND-ANGLE INDEX RING LITTLE OUT;So;0;L;;;;;N;;;;;
+1D8C3;SIGNWRITING HAND-ANGLE INDEX RING LITTLE;So;0;L;;;;;N;;;;;
+1D8C4;SIGNWRITING HAND-FIST MIDDLE DOWN;So;0;L;;;;;N;;;;;
+1D8C5;SIGNWRITING HAND-HINGE MIDDLE;So;0;L;;;;;N;;;;;
+1D8C6;SIGNWRITING HAND-FIST MIDDLE UP;So;0;L;;;;;N;;;;;
+1D8C7;SIGNWRITING HAND-CIRCLE MIDDLE UP;So;0;L;;;;;N;;;;;
+1D8C8;SIGNWRITING HAND-FIST MIDDLE RAISED KNUCKLE;So;0;L;;;;;N;;;;;
+1D8C9;SIGNWRITING HAND-FIST MIDDLE UP THUMB SIDE;So;0;L;;;;;N;;;;;
+1D8CA;SIGNWRITING HAND-HOOK MIDDLE THUMB;So;0;L;;;;;N;;;;;
+1D8CB;SIGNWRITING HAND-FIST MIDDLE THUMB LITTLE;So;0;L;;;;;N;;;;;
+1D8CC;SIGNWRITING HAND-FIST MIDDLE LITTLE;So;0;L;;;;;N;;;;;
+1D8CD;SIGNWRITING HAND-FIST MIDDLE RING LITTLE;So;0;L;;;;;N;;;;;
+1D8CE;SIGNWRITING HAND-CIRCLE MIDDLE RING LITTLE;So;0;L;;;;;N;;;;;
+1D8CF;SIGNWRITING HAND-CURLICUE MIDDLE RING LITTLE ON;So;0;L;;;;;N;;;;;
+1D8D0;SIGNWRITING HAND-CUP MIDDLE RING LITTLE;So;0;L;;;;;N;;;;;
+1D8D1;SIGNWRITING HAND-HINGE MIDDLE RING LITTLE;So;0;L;;;;;N;;;;;
+1D8D2;SIGNWRITING HAND-ANGLE MIDDLE RING LITTLE OUT;So;0;L;;;;;N;;;;;
+1D8D3;SIGNWRITING HAND-ANGLE MIDDLE RING LITTLE IN;So;0;L;;;;;N;;;;;
+1D8D4;SIGNWRITING HAND-ANGLE MIDDLE RING LITTLE;So;0;L;;;;;N;;;;;
+1D8D5;SIGNWRITING HAND-CIRCLE MIDDLE RING LITTLE BENT;So;0;L;;;;;N;;;;;
+1D8D6;SIGNWRITING HAND-CLAW MIDDLE RING LITTLE CONJOINED;So;0;L;;;;;N;;;;;
+1D8D7;SIGNWRITING HAND-CLAW MIDDLE RING LITTLE CONJOINED SIDE;So;0;L;;;;;N;;;;;
+1D8D8;SIGNWRITING HAND-HOOK MIDDLE RING LITTLE CONJOINED OUT;So;0;L;;;;;N;;;;;
+1D8D9;SIGNWRITING HAND-HOOK MIDDLE RING LITTLE CONJOINED IN;So;0;L;;;;;N;;;;;
+1D8DA;SIGNWRITING HAND-HOOK MIDDLE RING LITTLE CONJOINED;So;0;L;;;;;N;;;;;
+1D8DB;SIGNWRITING HAND-HINGE INDEX HINGED;So;0;L;;;;;N;;;;;
+1D8DC;SIGNWRITING HAND-FIST INDEX THUMB SIDE;So;0;L;;;;;N;;;;;
+1D8DD;SIGNWRITING HAND-HINGE INDEX THUMB SIDE;So;0;L;;;;;N;;;;;
+1D8DE;SIGNWRITING HAND-FIST INDEX THUMB SIDE THUMB DIAGONAL;So;0;L;;;;;N;;;;;
+1D8DF;SIGNWRITING HAND-FIST INDEX THUMB SIDE THUMB CONJOINED;So;0;L;;;;;N;;;;;
+1D8E0;SIGNWRITING HAND-FIST INDEX THUMB SIDE THUMB BENT;So;0;L;;;;;N;;;;;
+1D8E1;SIGNWRITING HAND-FIST INDEX THUMB SIDE INDEX BENT;So;0;L;;;;;N;;;;;
+1D8E2;SIGNWRITING HAND-FIST INDEX THUMB SIDE BOTH BENT;So;0;L;;;;;N;;;;;
+1D8E3;SIGNWRITING HAND-FIST INDEX THUMB SIDE INDEX HINGE;So;0;L;;;;;N;;;;;
+1D8E4;SIGNWRITING HAND-FIST INDEX THUMB FORWARD INDEX STRAIGHT;So;0;L;;;;;N;;;;;
+1D8E5;SIGNWRITING HAND-FIST INDEX THUMB FORWARD INDEX BENT;So;0;L;;;;;N;;;;;
+1D8E6;SIGNWRITING HAND-FIST INDEX THUMB HOOK;So;0;L;;;;;N;;;;;
+1D8E7;SIGNWRITING HAND-FIST INDEX THUMB CURLICUE;So;0;L;;;;;N;;;;;
+1D8E8;SIGNWRITING HAND-FIST INDEX THUMB CURVE THUMB INSIDE;So;0;L;;;;;N;;;;;
+1D8E9;SIGNWRITING HAND-CLAW INDEX THUMB CURVE THUMB INSIDE;So;0;L;;;;;N;;;;;
+1D8EA;SIGNWRITING HAND-FIST INDEX THUMB CURVE THUMB UNDER;So;0;L;;;;;N;;;;;
+1D8EB;SIGNWRITING HAND-FIST INDEX THUMB CIRCLE;So;0;L;;;;;N;;;;;
+1D8EC;SIGNWRITING HAND-CUP INDEX THUMB;So;0;L;;;;;N;;;;;
+1D8ED;SIGNWRITING HAND-CUP INDEX THUMB OPEN;So;0;L;;;;;N;;;;;
+1D8EE;SIGNWRITING HAND-HINGE INDEX THUMB OPEN;So;0;L;;;;;N;;;;;
+1D8EF;SIGNWRITING HAND-HINGE INDEX THUMB LARGE;So;0;L;;;;;N;;;;;
+1D8F0;SIGNWRITING HAND-HINGE INDEX THUMB;So;0;L;;;;;N;;;;;
+1D8F1;SIGNWRITING HAND-HINGE INDEX THUMB SMALL;So;0;L;;;;;N;;;;;
+1D8F2;SIGNWRITING HAND-ANGLE INDEX THUMB OUT;So;0;L;;;;;N;;;;;
+1D8F3;SIGNWRITING HAND-ANGLE INDEX THUMB IN;So;0;L;;;;;N;;;;;
+1D8F4;SIGNWRITING HAND-ANGLE INDEX THUMB;So;0;L;;;;;N;;;;;
+1D8F5;SIGNWRITING HAND-FIST THUMB;So;0;L;;;;;N;;;;;
+1D8F6;SIGNWRITING HAND-FIST THUMB HEEL;So;0;L;;;;;N;;;;;
+1D8F7;SIGNWRITING HAND-FIST THUMB SIDE DIAGONAL;So;0;L;;;;;N;;;;;
+1D8F8;SIGNWRITING HAND-FIST THUMB SIDE CONJOINED;So;0;L;;;;;N;;;;;
+1D8F9;SIGNWRITING HAND-FIST THUMB SIDE BENT;So;0;L;;;;;N;;;;;
+1D8FA;SIGNWRITING HAND-FIST THUMB FORWARD;So;0;L;;;;;N;;;;;
+1D8FB;SIGNWRITING HAND-FIST THUMB BETWEEN INDEX MIDDLE;So;0;L;;;;;N;;;;;
+1D8FC;SIGNWRITING HAND-FIST THUMB BETWEEN MIDDLE RING;So;0;L;;;;;N;;;;;
+1D8FD;SIGNWRITING HAND-FIST THUMB BETWEEN RING LITTLE;So;0;L;;;;;N;;;;;
+1D8FE;SIGNWRITING HAND-FIST THUMB UNDER TWO FINGERS;So;0;L;;;;;N;;;;;
+1D8FF;SIGNWRITING HAND-FIST THUMB OVER TWO FINGERS;So;0;L;;;;;N;;;;;
+1D900;SIGNWRITING HAND-FIST THUMB UNDER THREE FINGERS;So;0;L;;;;;N;;;;;
+1D901;SIGNWRITING HAND-FIST THUMB UNDER FOUR FINGERS;So;0;L;;;;;N;;;;;
+1D902;SIGNWRITING HAND-FIST THUMB OVER FOUR RAISED KNUCKLES;So;0;L;;;;;N;;;;;
+1D903;SIGNWRITING HAND-FIST;So;0;L;;;;;N;;;;;
+1D904;SIGNWRITING HAND-FIST HEEL;So;0;L;;;;;N;;;;;
+1D905;SIGNWRITING TOUCH SINGLE;So;0;L;;;;;N;;;;;
+1D906;SIGNWRITING TOUCH MULTIPLE;So;0;L;;;;;N;;;;;
+1D907;SIGNWRITING TOUCH BETWEEN;So;0;L;;;;;N;;;;;
+1D908;SIGNWRITING GRASP SINGLE;So;0;L;;;;;N;;;;;
+1D909;SIGNWRITING GRASP MULTIPLE;So;0;L;;;;;N;;;;;
+1D90A;SIGNWRITING GRASP BETWEEN;So;0;L;;;;;N;;;;;
+1D90B;SIGNWRITING STRIKE SINGLE;So;0;L;;;;;N;;;;;
+1D90C;SIGNWRITING STRIKE MULTIPLE;So;0;L;;;;;N;;;;;
+1D90D;SIGNWRITING STRIKE BETWEEN;So;0;L;;;;;N;;;;;
+1D90E;SIGNWRITING BRUSH SINGLE;So;0;L;;;;;N;;;;;
+1D90F;SIGNWRITING BRUSH MULTIPLE;So;0;L;;;;;N;;;;;
+1D910;SIGNWRITING BRUSH BETWEEN;So;0;L;;;;;N;;;;;
+1D911;SIGNWRITING RUB SINGLE;So;0;L;;;;;N;;;;;
+1D912;SIGNWRITING RUB MULTIPLE;So;0;L;;;;;N;;;;;
+1D913;SIGNWRITING RUB BETWEEN;So;0;L;;;;;N;;;;;
+1D914;SIGNWRITING SURFACE SYMBOLS;So;0;L;;;;;N;;;;;
+1D915;SIGNWRITING SURFACE BETWEEN;So;0;L;;;;;N;;;;;
+1D916;SIGNWRITING SQUEEZE LARGE SINGLE;So;0;L;;;;;N;;;;;
+1D917;SIGNWRITING SQUEEZE SMALL SINGLE;So;0;L;;;;;N;;;;;
+1D918;SIGNWRITING SQUEEZE LARGE MULTIPLE;So;0;L;;;;;N;;;;;
+1D919;SIGNWRITING SQUEEZE SMALL MULTIPLE;So;0;L;;;;;N;;;;;
+1D91A;SIGNWRITING SQUEEZE SEQUENTIAL;So;0;L;;;;;N;;;;;
+1D91B;SIGNWRITING FLICK LARGE SINGLE;So;0;L;;;;;N;;;;;
+1D91C;SIGNWRITING FLICK SMALL SINGLE;So;0;L;;;;;N;;;;;
+1D91D;SIGNWRITING FLICK LARGE MULTIPLE;So;0;L;;;;;N;;;;;
+1D91E;SIGNWRITING FLICK SMALL MULTIPLE;So;0;L;;;;;N;;;;;
+1D91F;SIGNWRITING FLICK SEQUENTIAL;So;0;L;;;;;N;;;;;
+1D920;SIGNWRITING SQUEEZE FLICK ALTERNATING;So;0;L;;;;;N;;;;;
+1D921;SIGNWRITING MOVEMENT-HINGE UP DOWN LARGE;So;0;L;;;;;N;;;;;
+1D922;SIGNWRITING MOVEMENT-HINGE UP DOWN SMALL;So;0;L;;;;;N;;;;;
+1D923;SIGNWRITING MOVEMENT-HINGE UP SEQUENTIAL;So;0;L;;;;;N;;;;;
+1D924;SIGNWRITING MOVEMENT-HINGE DOWN SEQUENTIAL;So;0;L;;;;;N;;;;;
+1D925;SIGNWRITING MOVEMENT-HINGE UP DOWN ALTERNATING LARGE;So;0;L;;;;;N;;;;;
+1D926;SIGNWRITING MOVEMENT-HINGE UP DOWN ALTERNATING SMALL;So;0;L;;;;;N;;;;;
+1D927;SIGNWRITING MOVEMENT-HINGE SIDE TO SIDE SCISSORS;So;0;L;;;;;N;;;;;
+1D928;SIGNWRITING MOVEMENT-WALLPLANE FINGER CONTACT;So;0;L;;;;;N;;;;;
+1D929;SIGNWRITING MOVEMENT-FLOORPLANE FINGER CONTACT;So;0;L;;;;;N;;;;;
+1D92A;SIGNWRITING MOVEMENT-WALLPLANE SINGLE STRAIGHT SMALL;So;0;L;;;;;N;;;;;
+1D92B;SIGNWRITING MOVEMENT-WALLPLANE SINGLE STRAIGHT MEDIUM;So;0;L;;;;;N;;;;;
+1D92C;SIGNWRITING MOVEMENT-WALLPLANE SINGLE STRAIGHT LARGE;So;0;L;;;;;N;;;;;
+1D92D;SIGNWRITING MOVEMENT-WALLPLANE SINGLE STRAIGHT LARGEST;So;0;L;;;;;N;;;;;
+1D92E;SIGNWRITING MOVEMENT-WALLPLANE SINGLE WRIST FLEX;So;0;L;;;;;N;;;;;
+1D92F;SIGNWRITING MOVEMENT-WALLPLANE DOUBLE STRAIGHT;So;0;L;;;;;N;;;;;
+1D930;SIGNWRITING MOVEMENT-WALLPLANE DOUBLE WRIST FLEX;So;0;L;;;;;N;;;;;
+1D931;SIGNWRITING MOVEMENT-WALLPLANE DOUBLE ALTERNATING;So;0;L;;;;;N;;;;;
+1D932;SIGNWRITING MOVEMENT-WALLPLANE DOUBLE ALTERNATING WRIST FLEX;So;0;L;;;;;N;;;;;
+1D933;SIGNWRITING MOVEMENT-WALLPLANE CROSS;So;0;L;;;;;N;;;;;
+1D934;SIGNWRITING MOVEMENT-WALLPLANE TRIPLE STRAIGHT MOVEMENT;So;0;L;;;;;N;;;;;
+1D935;SIGNWRITING MOVEMENT-WALLPLANE TRIPLE WRIST FLEX;So;0;L;;;;;N;;;;;
+1D936;SIGNWRITING MOVEMENT-WALLPLANE TRIPLE ALTERNATING;So;0;L;;;;;N;;;;;
+1D937;SIGNWRITING MOVEMENT-WALLPLANE TRIPLE ALTERNATING WRIST FLEX;So;0;L;;;;;N;;;;;
+1D938;SIGNWRITING MOVEMENT-WALLPLANE BEND SMALL;So;0;L;;;;;N;;;;;
+1D939;SIGNWRITING MOVEMENT-WALLPLANE BEND MEDIUM;So;0;L;;;;;N;;;;;
+1D93A;SIGNWRITING MOVEMENT-WALLPLANE BEND LARGE;So;0;L;;;;;N;;;;;
+1D93B;SIGNWRITING MOVEMENT-WALLPLANE CORNER SMALL;So;0;L;;;;;N;;;;;
+1D93C;SIGNWRITING MOVEMENT-WALLPLANE CORNER MEDIUM;So;0;L;;;;;N;;;;;
+1D93D;SIGNWRITING MOVEMENT-WALLPLANE CORNER LARGE;So;0;L;;;;;N;;;;;
+1D93E;SIGNWRITING MOVEMENT-WALLPLANE CORNER ROTATION;So;0;L;;;;;N;;;;;
+1D93F;SIGNWRITING MOVEMENT-WALLPLANE CHECK SMALL;So;0;L;;;;;N;;;;;
+1D940;SIGNWRITING MOVEMENT-WALLPLANE CHECK MEDIUM;So;0;L;;;;;N;;;;;
+1D941;SIGNWRITING MOVEMENT-WALLPLANE CHECK LARGE;So;0;L;;;;;N;;;;;
+1D942;SIGNWRITING MOVEMENT-WALLPLANE BOX SMALL;So;0;L;;;;;N;;;;;
+1D943;SIGNWRITING MOVEMENT-WALLPLANE BOX MEDIUM;So;0;L;;;;;N;;;;;
+1D944;SIGNWRITING MOVEMENT-WALLPLANE BOX LARGE;So;0;L;;;;;N;;;;;
+1D945;SIGNWRITING MOVEMENT-WALLPLANE ZIGZAG SMALL;So;0;L;;;;;N;;;;;
+1D946;SIGNWRITING MOVEMENT-WALLPLANE ZIGZAG MEDIUM;So;0;L;;;;;N;;;;;
+1D947;SIGNWRITING MOVEMENT-WALLPLANE ZIGZAG LARGE;So;0;L;;;;;N;;;;;
+1D948;SIGNWRITING MOVEMENT-WALLPLANE PEAKS SMALL;So;0;L;;;;;N;;;;;
+1D949;SIGNWRITING MOVEMENT-WALLPLANE PEAKS MEDIUM;So;0;L;;;;;N;;;;;
+1D94A;SIGNWRITING MOVEMENT-WALLPLANE PEAKS LARGE;So;0;L;;;;;N;;;;;
+1D94B;SIGNWRITING TRAVEL-WALLPLANE ROTATION-WALLPLANE SINGLE;So;0;L;;;;;N;;;;;
+1D94C;SIGNWRITING TRAVEL-WALLPLANE ROTATION-WALLPLANE DOUBLE;So;0;L;;;;;N;;;;;
+1D94D;SIGNWRITING TRAVEL-WALLPLANE ROTATION-WALLPLANE ALTERNATING;So;0;L;;;;;N;;;;;
+1D94E;SIGNWRITING TRAVEL-WALLPLANE ROTATION-FLOORPLANE SINGLE;So;0;L;;;;;N;;;;;
+1D94F;SIGNWRITING TRAVEL-WALLPLANE ROTATION-FLOORPLANE DOUBLE;So;0;L;;;;;N;;;;;
+1D950;SIGNWRITING TRAVEL-WALLPLANE ROTATION-FLOORPLANE ALTERNATING;So;0;L;;;;;N;;;;;
+1D951;SIGNWRITING TRAVEL-WALLPLANE SHAKING;So;0;L;;;;;N;;;;;
+1D952;SIGNWRITING TRAVEL-WALLPLANE ARM SPIRAL SINGLE;So;0;L;;;;;N;;;;;
+1D953;SIGNWRITING TRAVEL-WALLPLANE ARM SPIRAL DOUBLE;So;0;L;;;;;N;;;;;
+1D954;SIGNWRITING TRAVEL-WALLPLANE ARM SPIRAL TRIPLE;So;0;L;;;;;N;;;;;
+1D955;SIGNWRITING MOVEMENT-DIAGONAL AWAY SMALL;So;0;L;;;;;N;;;;;
+1D956;SIGNWRITING MOVEMENT-DIAGONAL AWAY MEDIUM;So;0;L;;;;;N;;;;;
+1D957;SIGNWRITING MOVEMENT-DIAGONAL AWAY LARGE;So;0;L;;;;;N;;;;;
+1D958;SIGNWRITING MOVEMENT-DIAGONAL AWAY LARGEST;So;0;L;;;;;N;;;;;
+1D959;SIGNWRITING MOVEMENT-DIAGONAL TOWARDS SMALL;So;0;L;;;;;N;;;;;
+1D95A;SIGNWRITING MOVEMENT-DIAGONAL TOWARDS MEDIUM;So;0;L;;;;;N;;;;;
+1D95B;SIGNWRITING MOVEMENT-DIAGONAL TOWARDS LARGE;So;0;L;;;;;N;;;;;
+1D95C;SIGNWRITING MOVEMENT-DIAGONAL TOWARDS LARGEST;So;0;L;;;;;N;;;;;
+1D95D;SIGNWRITING MOVEMENT-DIAGONAL BETWEEN AWAY SMALL;So;0;L;;;;;N;;;;;
+1D95E;SIGNWRITING MOVEMENT-DIAGONAL BETWEEN AWAY MEDIUM;So;0;L;;;;;N;;;;;
+1D95F;SIGNWRITING MOVEMENT-DIAGONAL BETWEEN AWAY LARGE;So;0;L;;;;;N;;;;;
+1D960;SIGNWRITING MOVEMENT-DIAGONAL BETWEEN AWAY LARGEST;So;0;L;;;;;N;;;;;
+1D961;SIGNWRITING MOVEMENT-DIAGONAL BETWEEN TOWARDS SMALL;So;0;L;;;;;N;;;;;
+1D962;SIGNWRITING MOVEMENT-DIAGONAL BETWEEN TOWARDS MEDIUM;So;0;L;;;;;N;;;;;
+1D963;SIGNWRITING MOVEMENT-DIAGONAL BETWEEN TOWARDS LARGE;So;0;L;;;;;N;;;;;
+1D964;SIGNWRITING MOVEMENT-DIAGONAL BETWEEN TOWARDS LARGEST;So;0;L;;;;;N;;;;;
+1D965;SIGNWRITING MOVEMENT-FLOORPLANE SINGLE STRAIGHT SMALL;So;0;L;;;;;N;;;;;
+1D966;SIGNWRITING MOVEMENT-FLOORPLANE SINGLE STRAIGHT MEDIUM;So;0;L;;;;;N;;;;;
+1D967;SIGNWRITING MOVEMENT-FLOORPLANE SINGLE STRAIGHT LARGE;So;0;L;;;;;N;;;;;
+1D968;SIGNWRITING MOVEMENT-FLOORPLANE SINGLE STRAIGHT LARGEST;So;0;L;;;;;N;;;;;
+1D969;SIGNWRITING MOVEMENT-FLOORPLANE SINGLE WRIST FLEX;So;0;L;;;;;N;;;;;
+1D96A;SIGNWRITING MOVEMENT-FLOORPLANE DOUBLE STRAIGHT;So;0;L;;;;;N;;;;;
+1D96B;SIGNWRITING MOVEMENT-FLOORPLANE DOUBLE WRIST FLEX;So;0;L;;;;;N;;;;;
+1D96C;SIGNWRITING MOVEMENT-FLOORPLANE DOUBLE ALTERNATING;So;0;L;;;;;N;;;;;
+1D96D;SIGNWRITING MOVEMENT-FLOORPLANE DOUBLE ALTERNATING WRIST FLEX;So;0;L;;;;;N;;;;;
+1D96E;SIGNWRITING MOVEMENT-FLOORPLANE CROSS;So;0;L;;;;;N;;;;;
+1D96F;SIGNWRITING MOVEMENT-FLOORPLANE TRIPLE STRAIGHT MOVEMENT;So;0;L;;;;;N;;;;;
+1D970;SIGNWRITING MOVEMENT-FLOORPLANE TRIPLE WRIST FLEX;So;0;L;;;;;N;;;;;
+1D971;SIGNWRITING MOVEMENT-FLOORPLANE TRIPLE ALTERNATING MOVEMENT;So;0;L;;;;;N;;;;;
+1D972;SIGNWRITING MOVEMENT-FLOORPLANE TRIPLE ALTERNATING WRIST FLEX;So;0;L;;;;;N;;;;;
+1D973;SIGNWRITING MOVEMENT-FLOORPLANE BEND;So;0;L;;;;;N;;;;;
+1D974;SIGNWRITING MOVEMENT-FLOORPLANE CORNER SMALL;So;0;L;;;;;N;;;;;
+1D975;SIGNWRITING MOVEMENT-FLOORPLANE CORNER MEDIUM;So;0;L;;;;;N;;;;;
+1D976;SIGNWRITING MOVEMENT-FLOORPLANE CORNER LARGE;So;0;L;;;;;N;;;;;
+1D977;SIGNWRITING MOVEMENT-FLOORPLANE CHECK;So;0;L;;;;;N;;;;;
+1D978;SIGNWRITING MOVEMENT-FLOORPLANE BOX SMALL;So;0;L;;;;;N;;;;;
+1D979;SIGNWRITING MOVEMENT-FLOORPLANE BOX MEDIUM;So;0;L;;;;;N;;;;;
+1D97A;SIGNWRITING MOVEMENT-FLOORPLANE BOX LARGE;So;0;L;;;;;N;;;;;
+1D97B;SIGNWRITING MOVEMENT-FLOORPLANE ZIGZAG SMALL;So;0;L;;;;;N;;;;;
+1D97C;SIGNWRITING MOVEMENT-FLOORPLANE ZIGZAG MEDIUM;So;0;L;;;;;N;;;;;
+1D97D;SIGNWRITING MOVEMENT-FLOORPLANE ZIGZAG LARGE;So;0;L;;;;;N;;;;;
+1D97E;SIGNWRITING MOVEMENT-FLOORPLANE PEAKS SMALL;So;0;L;;;;;N;;;;;
+1D97F;SIGNWRITING MOVEMENT-FLOORPLANE PEAKS MEDIUM;So;0;L;;;;;N;;;;;
+1D980;SIGNWRITING MOVEMENT-FLOORPLANE PEAKS LARGE;So;0;L;;;;;N;;;;;
+1D981;SIGNWRITING TRAVEL-FLOORPLANE ROTATION-FLOORPLANE SINGLE;So;0;L;;;;;N;;;;;
+1D982;SIGNWRITING TRAVEL-FLOORPLANE ROTATION-FLOORPLANE DOUBLE;So;0;L;;;;;N;;;;;
+1D983;SIGNWRITING TRAVEL-FLOORPLANE ROTATION-FLOORPLANE ALTERNATING;So;0;L;;;;;N;;;;;
+1D984;SIGNWRITING TRAVEL-FLOORPLANE ROTATION-WALLPLANE SINGLE;So;0;L;;;;;N;;;;;
+1D985;SIGNWRITING TRAVEL-FLOORPLANE ROTATION-WALLPLANE DOUBLE;So;0;L;;;;;N;;;;;
+1D986;SIGNWRITING TRAVEL-FLOORPLANE ROTATION-WALLPLANE ALTERNATING;So;0;L;;;;;N;;;;;
+1D987;SIGNWRITING TRAVEL-FLOORPLANE SHAKING;So;0;L;;;;;N;;;;;
+1D988;SIGNWRITING MOVEMENT-WALLPLANE CURVE QUARTER SMALL;So;0;L;;;;;N;;;;;
+1D989;SIGNWRITING MOVEMENT-WALLPLANE CURVE QUARTER MEDIUM;So;0;L;;;;;N;;;;;
+1D98A;SIGNWRITING MOVEMENT-WALLPLANE CURVE QUARTER LARGE;So;0;L;;;;;N;;;;;
+1D98B;SIGNWRITING MOVEMENT-WALLPLANE CURVE QUARTER LARGEST;So;0;L;;;;;N;;;;;
+1D98C;SIGNWRITING MOVEMENT-WALLPLANE CURVE HALF-CIRCLE SMALL;So;0;L;;;;;N;;;;;
+1D98D;SIGNWRITING MOVEMENT-WALLPLANE CURVE HALF-CIRCLE MEDIUM;So;0;L;;;;;N;;;;;
+1D98E;SIGNWRITING MOVEMENT-WALLPLANE CURVE HALF-CIRCLE LARGE;So;0;L;;;;;N;;;;;
+1D98F;SIGNWRITING MOVEMENT-WALLPLANE CURVE HALF-CIRCLE LARGEST;So;0;L;;;;;N;;;;;
+1D990;SIGNWRITING MOVEMENT-WALLPLANE CURVE THREE-QUARTER CIRCLE SMALL;So;0;L;;;;;N;;;;;
+1D991;SIGNWRITING MOVEMENT-WALLPLANE CURVE THREE-QUARTER CIRCLE MEDIUM;So;0;L;;;;;N;;;;;
+1D992;SIGNWRITING MOVEMENT-WALLPLANE HUMP SMALL;So;0;L;;;;;N;;;;;
+1D993;SIGNWRITING MOVEMENT-WALLPLANE HUMP MEDIUM;So;0;L;;;;;N;;;;;
+1D994;SIGNWRITING MOVEMENT-WALLPLANE HUMP LARGE;So;0;L;;;;;N;;;;;
+1D995;SIGNWRITING MOVEMENT-WALLPLANE LOOP SMALL;So;0;L;;;;;N;;;;;
+1D996;SIGNWRITING MOVEMENT-WALLPLANE LOOP MEDIUM;So;0;L;;;;;N;;;;;
+1D997;SIGNWRITING MOVEMENT-WALLPLANE LOOP LARGE;So;0;L;;;;;N;;;;;
+1D998;SIGNWRITING MOVEMENT-WALLPLANE LOOP SMALL DOUBLE;So;0;L;;;;;N;;;;;
+1D999;SIGNWRITING MOVEMENT-WALLPLANE WAVE CURVE DOUBLE SMALL;So;0;L;;;;;N;;;;;
+1D99A;SIGNWRITING MOVEMENT-WALLPLANE WAVE CURVE DOUBLE MEDIUM;So;0;L;;;;;N;;;;;
+1D99B;SIGNWRITING MOVEMENT-WALLPLANE WAVE CURVE DOUBLE LARGE;So;0;L;;;;;N;;;;;
+1D99C;SIGNWRITING MOVEMENT-WALLPLANE WAVE CURVE TRIPLE SMALL;So;0;L;;;;;N;;;;;
+1D99D;SIGNWRITING MOVEMENT-WALLPLANE WAVE CURVE TRIPLE MEDIUM;So;0;L;;;;;N;;;;;
+1D99E;SIGNWRITING MOVEMENT-WALLPLANE WAVE CURVE TRIPLE LARGE;So;0;L;;;;;N;;;;;
+1D99F;SIGNWRITING MOVEMENT-WALLPLANE CURVE THEN STRAIGHT;So;0;L;;;;;N;;;;;
+1D9A0;SIGNWRITING MOVEMENT-WALLPLANE CURVED CROSS SMALL;So;0;L;;;;;N;;;;;
+1D9A1;SIGNWRITING MOVEMENT-WALLPLANE CURVED CROSS MEDIUM;So;0;L;;;;;N;;;;;
+1D9A2;SIGNWRITING ROTATION-WALLPLANE SINGLE;So;0;L;;;;;N;;;;;
+1D9A3;SIGNWRITING ROTATION-WALLPLANE DOUBLE;So;0;L;;;;;N;;;;;
+1D9A4;SIGNWRITING ROTATION-WALLPLANE ALTERNATE;So;0;L;;;;;N;;;;;
+1D9A5;SIGNWRITING MOVEMENT-WALLPLANE SHAKING;So;0;L;;;;;N;;;;;
+1D9A6;SIGNWRITING MOVEMENT-WALLPLANE CURVE HITTING FRONT WALL;So;0;L;;;;;N;;;;;
+1D9A7;SIGNWRITING MOVEMENT-WALLPLANE HUMP HITTING FRONT WALL;So;0;L;;;;;N;;;;;
+1D9A8;SIGNWRITING MOVEMENT-WALLPLANE LOOP HITTING FRONT WALL;So;0;L;;;;;N;;;;;
+1D9A9;SIGNWRITING MOVEMENT-WALLPLANE WAVE HITTING FRONT WALL;So;0;L;;;;;N;;;;;
+1D9AA;SIGNWRITING ROTATION-WALLPLANE SINGLE HITTING FRONT WALL;So;0;L;;;;;N;;;;;
+1D9AB;SIGNWRITING ROTATION-WALLPLANE DOUBLE HITTING FRONT WALL;So;0;L;;;;;N;;;;;
+1D9AC;SIGNWRITING ROTATION-WALLPLANE ALTERNATING HITTING FRONT WALL;So;0;L;;;;;N;;;;;
+1D9AD;SIGNWRITING MOVEMENT-WALLPLANE CURVE HITTING CHEST;So;0;L;;;;;N;;;;;
+1D9AE;SIGNWRITING MOVEMENT-WALLPLANE HUMP HITTING CHEST;So;0;L;;;;;N;;;;;
+1D9AF;SIGNWRITING MOVEMENT-WALLPLANE LOOP HITTING CHEST;So;0;L;;;;;N;;;;;
+1D9B0;SIGNWRITING MOVEMENT-WALLPLANE WAVE HITTING CHEST;So;0;L;;;;;N;;;;;
+1D9B1;SIGNWRITING ROTATION-WALLPLANE SINGLE HITTING CHEST;So;0;L;;;;;N;;;;;
+1D9B2;SIGNWRITING ROTATION-WALLPLANE DOUBLE HITTING CHEST;So;0;L;;;;;N;;;;;
+1D9B3;SIGNWRITING ROTATION-WALLPLANE ALTERNATING HITTING CHEST;So;0;L;;;;;N;;;;;
+1D9B4;SIGNWRITING MOVEMENT-WALLPLANE WAVE DIAGONAL PATH SMALL;So;0;L;;;;;N;;;;;
+1D9B5;SIGNWRITING MOVEMENT-WALLPLANE WAVE DIAGONAL PATH MEDIUM;So;0;L;;;;;N;;;;;
+1D9B6;SIGNWRITING MOVEMENT-WALLPLANE WAVE DIAGONAL PATH LARGE;So;0;L;;;;;N;;;;;
+1D9B7;SIGNWRITING MOVEMENT-FLOORPLANE CURVE HITTING CEILING SMALL;So;0;L;;;;;N;;;;;
+1D9B8;SIGNWRITING MOVEMENT-FLOORPLANE CURVE HITTING CEILING LARGE;So;0;L;;;;;N;;;;;
+1D9B9;SIGNWRITING MOVEMENT-FLOORPLANE HUMP HITTING CEILING SMALL DOUBLE;So;0;L;;;;;N;;;;;
+1D9BA;SIGNWRITING MOVEMENT-FLOORPLANE HUMP HITTING CEILING LARGE DOUBLE;So;0;L;;;;;N;;;;;
+1D9BB;SIGNWRITING MOVEMENT-FLOORPLANE HUMP HITTING CEILING SMALL TRIPLE;So;0;L;;;;;N;;;;;
+1D9BC;SIGNWRITING MOVEMENT-FLOORPLANE HUMP HITTING CEILING LARGE TRIPLE;So;0;L;;;;;N;;;;;
+1D9BD;SIGNWRITING MOVEMENT-FLOORPLANE LOOP HITTING CEILING SMALL SINGLE;So;0;L;;;;;N;;;;;
+1D9BE;SIGNWRITING MOVEMENT-FLOORPLANE LOOP HITTING CEILING LARGE SINGLE;So;0;L;;;;;N;;;;;
+1D9BF;SIGNWRITING MOVEMENT-FLOORPLANE LOOP HITTING CEILING SMALL DOUBLE;So;0;L;;;;;N;;;;;
+1D9C0;SIGNWRITING MOVEMENT-FLOORPLANE LOOP HITTING CEILING LARGE DOUBLE;So;0;L;;;;;N;;;;;
+1D9C1;SIGNWRITING MOVEMENT-FLOORPLANE WAVE HITTING CEILING SMALL;So;0;L;;;;;N;;;;;
+1D9C2;SIGNWRITING MOVEMENT-FLOORPLANE WAVE HITTING CEILING LARGE;So;0;L;;;;;N;;;;;
+1D9C3;SIGNWRITING ROTATION-FLOORPLANE SINGLE HITTING CEILING;So;0;L;;;;;N;;;;;
+1D9C4;SIGNWRITING ROTATION-FLOORPLANE DOUBLE HITTING CEILING;So;0;L;;;;;N;;;;;
+1D9C5;SIGNWRITING ROTATION-FLOORPLANE ALTERNATING HITTING CEILING;So;0;L;;;;;N;;;;;
+1D9C6;SIGNWRITING MOVEMENT-FLOORPLANE CURVE HITTING FLOOR SMALL;So;0;L;;;;;N;;;;;
+1D9C7;SIGNWRITING MOVEMENT-FLOORPLANE CURVE HITTING FLOOR LARGE;So;0;L;;;;;N;;;;;
+1D9C8;SIGNWRITING MOVEMENT-FLOORPLANE HUMP HITTING FLOOR SMALL DOUBLE;So;0;L;;;;;N;;;;;
+1D9C9;SIGNWRITING MOVEMENT-FLOORPLANE HUMP HITTING FLOOR LARGE DOUBLE;So;0;L;;;;;N;;;;;
+1D9CA;SIGNWRITING MOVEMENT-FLOORPLANE HUMP HITTING FLOOR TRIPLE SMALL TRIPLE;So;0;L;;;;;N;;;;;
+1D9CB;SIGNWRITING MOVEMENT-FLOORPLANE HUMP HITTING FLOOR TRIPLE LARGE TRIPLE;So;0;L;;;;;N;;;;;
+1D9CC;SIGNWRITING MOVEMENT-FLOORPLANE LOOP HITTING FLOOR SMALL SINGLE;So;0;L;;;;;N;;;;;
+1D9CD;SIGNWRITING MOVEMENT-FLOORPLANE LOOP HITTING FLOOR LARGE SINGLE;So;0;L;;;;;N;;;;;
+1D9CE;SIGNWRITING MOVEMENT-FLOORPLANE LOOP HITTING FLOOR SMALL DOUBLE;So;0;L;;;;;N;;;;;
+1D9CF;SIGNWRITING MOVEMENT-FLOORPLANE LOOP HITTING FLOOR LARGE DOUBLE;So;0;L;;;;;N;;;;;
+1D9D0;SIGNWRITING MOVEMENT-FLOORPLANE WAVE HITTING FLOOR SMALL;So;0;L;;;;;N;;;;;
+1D9D1;SIGNWRITING MOVEMENT-FLOORPLANE WAVE HITTING FLOOR LARGE;So;0;L;;;;;N;;;;;
+1D9D2;SIGNWRITING ROTATION-FLOORPLANE SINGLE HITTING FLOOR;So;0;L;;;;;N;;;;;
+1D9D3;SIGNWRITING ROTATION-FLOORPLANE DOUBLE HITTING FLOOR;So;0;L;;;;;N;;;;;
+1D9D4;SIGNWRITING ROTATION-FLOORPLANE ALTERNATING HITTING FLOOR;So;0;L;;;;;N;;;;;
+1D9D5;SIGNWRITING MOVEMENT-FLOORPLANE CURVE SMALL;So;0;L;;;;;N;;;;;
+1D9D6;SIGNWRITING MOVEMENT-FLOORPLANE CURVE MEDIUM;So;0;L;;;;;N;;;;;
+1D9D7;SIGNWRITING MOVEMENT-FLOORPLANE CURVE LARGE;So;0;L;;;;;N;;;;;
+1D9D8;SIGNWRITING MOVEMENT-FLOORPLANE CURVE LARGEST;So;0;L;;;;;N;;;;;
+1D9D9;SIGNWRITING MOVEMENT-FLOORPLANE CURVE COMBINED;So;0;L;;;;;N;;;;;
+1D9DA;SIGNWRITING MOVEMENT-FLOORPLANE HUMP SMALL;So;0;L;;;;;N;;;;;
+1D9DB;SIGNWRITING MOVEMENT-FLOORPLANE LOOP SMALL;So;0;L;;;;;N;;;;;
+1D9DC;SIGNWRITING MOVEMENT-FLOORPLANE WAVE SNAKE;So;0;L;;;;;N;;;;;
+1D9DD;SIGNWRITING MOVEMENT-FLOORPLANE WAVE SMALL;So;0;L;;;;;N;;;;;
+1D9DE;SIGNWRITING MOVEMENT-FLOORPLANE WAVE LARGE;So;0;L;;;;;N;;;;;
+1D9DF;SIGNWRITING ROTATION-FLOORPLANE SINGLE;So;0;L;;;;;N;;;;;
+1D9E0;SIGNWRITING ROTATION-FLOORPLANE DOUBLE;So;0;L;;;;;N;;;;;
+1D9E1;SIGNWRITING ROTATION-FLOORPLANE ALTERNATING;So;0;L;;;;;N;;;;;
+1D9E2;SIGNWRITING MOVEMENT-FLOORPLANE SHAKING PARALLEL;So;0;L;;;;;N;;;;;
+1D9E3;SIGNWRITING MOVEMENT-WALLPLANE ARM CIRCLE SMALL SINGLE;So;0;L;;;;;N;;;;;
+1D9E4;SIGNWRITING MOVEMENT-WALLPLANE ARM CIRCLE MEDIUM SINGLE;So;0;L;;;;;N;;;;;
+1D9E5;SIGNWRITING MOVEMENT-WALLPLANE ARM CIRCLE SMALL DOUBLE;So;0;L;;;;;N;;;;;
+1D9E6;SIGNWRITING MOVEMENT-WALLPLANE ARM CIRCLE MEDIUM DOUBLE;So;0;L;;;;;N;;;;;
+1D9E7;SIGNWRITING MOVEMENT-FLOORPLANE ARM CIRCLE HITTING WALL SMALL SINGLE;So;0;L;;;;;N;;;;;
+1D9E8;SIGNWRITING MOVEMENT-FLOORPLANE ARM CIRCLE HITTING WALL MEDIUM SINGLE;So;0;L;;;;;N;;;;;
+1D9E9;SIGNWRITING MOVEMENT-FLOORPLANE ARM CIRCLE HITTING WALL LARGE SINGLE;So;0;L;;;;;N;;;;;
+1D9EA;SIGNWRITING MOVEMENT-FLOORPLANE ARM CIRCLE HITTING WALL SMALL DOUBLE;So;0;L;;;;;N;;;;;
+1D9EB;SIGNWRITING MOVEMENT-FLOORPLANE ARM CIRCLE HITTING WALL MEDIUM DOUBLE;So;0;L;;;;;N;;;;;
+1D9EC;SIGNWRITING MOVEMENT-FLOORPLANE ARM CIRCLE HITTING WALL LARGE DOUBLE;So;0;L;;;;;N;;;;;
+1D9ED;SIGNWRITING MOVEMENT-WALLPLANE WRIST CIRCLE FRONT SINGLE;So;0;L;;;;;N;;;;;
+1D9EE;SIGNWRITING MOVEMENT-WALLPLANE WRIST CIRCLE FRONT DOUBLE;So;0;L;;;;;N;;;;;
+1D9EF;SIGNWRITING MOVEMENT-FLOORPLANE WRIST CIRCLE HITTING WALL SINGLE;So;0;L;;;;;N;;;;;
+1D9F0;SIGNWRITING MOVEMENT-FLOORPLANE WRIST CIRCLE HITTING WALL DOUBLE;So;0;L;;;;;N;;;;;
+1D9F1;SIGNWRITING MOVEMENT-WALLPLANE FINGER CIRCLES SINGLE;So;0;L;;;;;N;;;;;
+1D9F2;SIGNWRITING MOVEMENT-WALLPLANE FINGER CIRCLES DOUBLE;So;0;L;;;;;N;;;;;
+1D9F3;SIGNWRITING MOVEMENT-FLOORPLANE FINGER CIRCLES HITTING WALL SINGLE;So;0;L;;;;;N;;;;;
+1D9F4;SIGNWRITING MOVEMENT-FLOORPLANE FINGER CIRCLES HITTING WALL DOUBLE;So;0;L;;;;;N;;;;;
+1D9F5;SIGNWRITING DYNAMIC ARROWHEAD SMALL;So;0;L;;;;;N;;;;;
+1D9F6;SIGNWRITING DYNAMIC ARROWHEAD LARGE;So;0;L;;;;;N;;;;;
+1D9F7;SIGNWRITING DYNAMIC FAST;So;0;L;;;;;N;;;;;
+1D9F8;SIGNWRITING DYNAMIC SLOW;So;0;L;;;;;N;;;;;
+1D9F9;SIGNWRITING DYNAMIC TENSE;So;0;L;;;;;N;;;;;
+1D9FA;SIGNWRITING DYNAMIC RELAXED;So;0;L;;;;;N;;;;;
+1D9FB;SIGNWRITING DYNAMIC SIMULTANEOUS;So;0;L;;;;;N;;;;;
+1D9FC;SIGNWRITING DYNAMIC SIMULTANEOUS ALTERNATING;So;0;L;;;;;N;;;;;
+1D9FD;SIGNWRITING DYNAMIC EVERY OTHER TIME;So;0;L;;;;;N;;;;;
+1D9FE;SIGNWRITING DYNAMIC GRADUAL;So;0;L;;;;;N;;;;;
+1D9FF;SIGNWRITING HEAD;So;0;L;;;;;N;;;;;
+1DA00;SIGNWRITING HEAD RIM;Mn;0;NSM;;;;;N;;;;;
+1DA01;SIGNWRITING HEAD MOVEMENT-WALLPLANE STRAIGHT;Mn;0;NSM;;;;;N;;;;;
+1DA02;SIGNWRITING HEAD MOVEMENT-WALLPLANE TILT;Mn;0;NSM;;;;;N;;;;;
+1DA03;SIGNWRITING HEAD MOVEMENT-FLOORPLANE STRAIGHT;Mn;0;NSM;;;;;N;;;;;
+1DA04;SIGNWRITING HEAD MOVEMENT-WALLPLANE CURVE;Mn;0;NSM;;;;;N;;;;;
+1DA05;SIGNWRITING HEAD MOVEMENT-FLOORPLANE CURVE;Mn;0;NSM;;;;;N;;;;;
+1DA06;SIGNWRITING HEAD MOVEMENT CIRCLE;Mn;0;NSM;;;;;N;;;;;
+1DA07;SIGNWRITING FACE DIRECTION POSITION NOSE FORWARD TILTING;Mn;0;NSM;;;;;N;;;;;
+1DA08;SIGNWRITING FACE DIRECTION POSITION NOSE UP OR DOWN;Mn;0;NSM;;;;;N;;;;;
+1DA09;SIGNWRITING FACE DIRECTION POSITION NOSE UP OR DOWN TILTING;Mn;0;NSM;;;;;N;;;;;
+1DA0A;SIGNWRITING EYEBROWS STRAIGHT UP;Mn;0;NSM;;;;;N;;;;;
+1DA0B;SIGNWRITING EYEBROWS STRAIGHT NEUTRAL;Mn;0;NSM;;;;;N;;;;;
+1DA0C;SIGNWRITING EYEBROWS STRAIGHT DOWN;Mn;0;NSM;;;;;N;;;;;
+1DA0D;SIGNWRITING DREAMY EYEBROWS NEUTRAL DOWN;Mn;0;NSM;;;;;N;;;;;
+1DA0E;SIGNWRITING DREAMY EYEBROWS DOWN NEUTRAL;Mn;0;NSM;;;;;N;;;;;
+1DA0F;SIGNWRITING DREAMY EYEBROWS UP NEUTRAL;Mn;0;NSM;;;;;N;;;;;
+1DA10;SIGNWRITING DREAMY EYEBROWS NEUTRAL UP;Mn;0;NSM;;;;;N;;;;;
+1DA11;SIGNWRITING FOREHEAD NEUTRAL;Mn;0;NSM;;;;;N;;;;;
+1DA12;SIGNWRITING FOREHEAD CONTACT;Mn;0;NSM;;;;;N;;;;;
+1DA13;SIGNWRITING FOREHEAD WRINKLED;Mn;0;NSM;;;;;N;;;;;
+1DA14;SIGNWRITING EYES OPEN;Mn;0;NSM;;;;;N;;;;;
+1DA15;SIGNWRITING EYES SQUEEZED;Mn;0;NSM;;;;;N;;;;;
+1DA16;SIGNWRITING EYES CLOSED;Mn;0;NSM;;;;;N;;;;;
+1DA17;SIGNWRITING EYE BLINK SINGLE;Mn;0;NSM;;;;;N;;;;;
+1DA18;SIGNWRITING EYE BLINK MULTIPLE;Mn;0;NSM;;;;;N;;;;;
+1DA19;SIGNWRITING EYES HALF OPEN;Mn;0;NSM;;;;;N;;;;;
+1DA1A;SIGNWRITING EYES WIDE OPEN;Mn;0;NSM;;;;;N;;;;;
+1DA1B;SIGNWRITING EYES HALF CLOSED;Mn;0;NSM;;;;;N;;;;;
+1DA1C;SIGNWRITING EYES WIDENING MOVEMENT;Mn;0;NSM;;;;;N;;;;;
+1DA1D;SIGNWRITING EYE WINK;Mn;0;NSM;;;;;N;;;;;
+1DA1E;SIGNWRITING EYELASHES UP;Mn;0;NSM;;;;;N;;;;;
+1DA1F;SIGNWRITING EYELASHES DOWN;Mn;0;NSM;;;;;N;;;;;
+1DA20;SIGNWRITING EYELASHES FLUTTERING;Mn;0;NSM;;;;;N;;;;;
+1DA21;SIGNWRITING EYEGAZE-WALLPLANE STRAIGHT;Mn;0;NSM;;;;;N;;;;;
+1DA22;SIGNWRITING EYEGAZE-WALLPLANE STRAIGHT DOUBLE;Mn;0;NSM;;;;;N;;;;;
+1DA23;SIGNWRITING EYEGAZE-WALLPLANE STRAIGHT ALTERNATING;Mn;0;NSM;;;;;N;;;;;
+1DA24;SIGNWRITING EYEGAZE-FLOORPLANE STRAIGHT;Mn;0;NSM;;;;;N;;;;;
+1DA25;SIGNWRITING EYEGAZE-FLOORPLANE STRAIGHT DOUBLE;Mn;0;NSM;;;;;N;;;;;
+1DA26;SIGNWRITING EYEGAZE-FLOORPLANE STRAIGHT ALTERNATING;Mn;0;NSM;;;;;N;;;;;
+1DA27;SIGNWRITING EYEGAZE-WALLPLANE CURVED;Mn;0;NSM;;;;;N;;;;;
+1DA28;SIGNWRITING EYEGAZE-FLOORPLANE CURVED;Mn;0;NSM;;;;;N;;;;;
+1DA29;SIGNWRITING EYEGAZE-WALLPLANE CIRCLING;Mn;0;NSM;;;;;N;;;;;
+1DA2A;SIGNWRITING CHEEKS PUFFED;Mn;0;NSM;;;;;N;;;;;
+1DA2B;SIGNWRITING CHEEKS NEUTRAL;Mn;0;NSM;;;;;N;;;;;
+1DA2C;SIGNWRITING CHEEKS SUCKED;Mn;0;NSM;;;;;N;;;;;
+1DA2D;SIGNWRITING TENSE CHEEKS HIGH;Mn;0;NSM;;;;;N;;;;;
+1DA2E;SIGNWRITING TENSE CHEEKS MIDDLE;Mn;0;NSM;;;;;N;;;;;
+1DA2F;SIGNWRITING TENSE CHEEKS LOW;Mn;0;NSM;;;;;N;;;;;
+1DA30;SIGNWRITING EARS;Mn;0;NSM;;;;;N;;;;;
+1DA31;SIGNWRITING NOSE NEUTRAL;Mn;0;NSM;;;;;N;;;;;
+1DA32;SIGNWRITING NOSE CONTACT;Mn;0;NSM;;;;;N;;;;;
+1DA33;SIGNWRITING NOSE WRINKLES;Mn;0;NSM;;;;;N;;;;;
+1DA34;SIGNWRITING NOSE WIGGLES;Mn;0;NSM;;;;;N;;;;;
+1DA35;SIGNWRITING AIR BLOWING OUT;Mn;0;NSM;;;;;N;;;;;
+1DA36;SIGNWRITING AIR SUCKING IN;Mn;0;NSM;;;;;N;;;;;
+1DA37;SIGNWRITING AIR BLOW SMALL ROTATIONS;So;0;L;;;;;N;;;;;
+1DA38;SIGNWRITING AIR SUCK SMALL ROTATIONS;So;0;L;;;;;N;;;;;
+1DA39;SIGNWRITING BREATH INHALE;So;0;L;;;;;N;;;;;
+1DA3A;SIGNWRITING BREATH EXHALE;So;0;L;;;;;N;;;;;
+1DA3B;SIGNWRITING MOUTH CLOSED NEUTRAL;Mn;0;NSM;;;;;N;;;;;
+1DA3C;SIGNWRITING MOUTH CLOSED FORWARD;Mn;0;NSM;;;;;N;;;;;
+1DA3D;SIGNWRITING MOUTH CLOSED CONTACT;Mn;0;NSM;;;;;N;;;;;
+1DA3E;SIGNWRITING MOUTH SMILE;Mn;0;NSM;;;;;N;;;;;
+1DA3F;SIGNWRITING MOUTH SMILE WRINKLED;Mn;0;NSM;;;;;N;;;;;
+1DA40;SIGNWRITING MOUTH SMILE OPEN;Mn;0;NSM;;;;;N;;;;;
+1DA41;SIGNWRITING MOUTH FROWN;Mn;0;NSM;;;;;N;;;;;
+1DA42;SIGNWRITING MOUTH FROWN WRINKLED;Mn;0;NSM;;;;;N;;;;;
+1DA43;SIGNWRITING MOUTH FROWN OPEN;Mn;0;NSM;;;;;N;;;;;
+1DA44;SIGNWRITING MOUTH OPEN CIRCLE;Mn;0;NSM;;;;;N;;;;;
+1DA45;SIGNWRITING MOUTH OPEN FORWARD;Mn;0;NSM;;;;;N;;;;;
+1DA46;SIGNWRITING MOUTH OPEN WRINKLED;Mn;0;NSM;;;;;N;;;;;
+1DA47;SIGNWRITING MOUTH OPEN OVAL;Mn;0;NSM;;;;;N;;;;;
+1DA48;SIGNWRITING MOUTH OPEN OVAL WRINKLED;Mn;0;NSM;;;;;N;;;;;
+1DA49;SIGNWRITING MOUTH OPEN OVAL YAWN;Mn;0;NSM;;;;;N;;;;;
+1DA4A;SIGNWRITING MOUTH OPEN RECTANGLE;Mn;0;NSM;;;;;N;;;;;
+1DA4B;SIGNWRITING MOUTH OPEN RECTANGLE WRINKLED;Mn;0;NSM;;;;;N;;;;;
+1DA4C;SIGNWRITING MOUTH OPEN RECTANGLE YAWN;Mn;0;NSM;;;;;N;;;;;
+1DA4D;SIGNWRITING MOUTH KISS;Mn;0;NSM;;;;;N;;;;;
+1DA4E;SIGNWRITING MOUTH KISS FORWARD;Mn;0;NSM;;;;;N;;;;;
+1DA4F;SIGNWRITING MOUTH KISS WRINKLED;Mn;0;NSM;;;;;N;;;;;
+1DA50;SIGNWRITING MOUTH TENSE;Mn;0;NSM;;;;;N;;;;;
+1DA51;SIGNWRITING MOUTH TENSE FORWARD;Mn;0;NSM;;;;;N;;;;;
+1DA52;SIGNWRITING MOUTH TENSE SUCKED;Mn;0;NSM;;;;;N;;;;;
+1DA53;SIGNWRITING LIPS PRESSED TOGETHER;Mn;0;NSM;;;;;N;;;;;
+1DA54;SIGNWRITING LIP LOWER OVER UPPER;Mn;0;NSM;;;;;N;;;;;
+1DA55;SIGNWRITING LIP UPPER OVER LOWER;Mn;0;NSM;;;;;N;;;;;
+1DA56;SIGNWRITING MOUTH CORNERS;Mn;0;NSM;;;;;N;;;;;
+1DA57;SIGNWRITING MOUTH WRINKLES SINGLE;Mn;0;NSM;;;;;N;;;;;
+1DA58;SIGNWRITING MOUTH WRINKLES DOUBLE;Mn;0;NSM;;;;;N;;;;;
+1DA59;SIGNWRITING TONGUE STICKING OUT FAR;Mn;0;NSM;;;;;N;;;;;
+1DA5A;SIGNWRITING TONGUE LICKING LIPS;Mn;0;NSM;;;;;N;;;;;
+1DA5B;SIGNWRITING TONGUE TIP BETWEEN LIPS;Mn;0;NSM;;;;;N;;;;;
+1DA5C;SIGNWRITING TONGUE TIP TOUCHING INSIDE MOUTH;Mn;0;NSM;;;;;N;;;;;
+1DA5D;SIGNWRITING TONGUE INSIDE MOUTH RELAXED;Mn;0;NSM;;;;;N;;;;;
+1DA5E;SIGNWRITING TONGUE MOVES AGAINST CHEEK;Mn;0;NSM;;;;;N;;;;;
+1DA5F;SIGNWRITING TONGUE CENTRE STICKING OUT;Mn;0;NSM;;;;;N;;;;;
+1DA60;SIGNWRITING TONGUE CENTRE INSIDE MOUTH;Mn;0;NSM;;;;;N;;;;;
+1DA61;SIGNWRITING TEETH;Mn;0;NSM;;;;;N;;;;;
+1DA62;SIGNWRITING TEETH MOVEMENT;Mn;0;NSM;;;;;N;;;;;
+1DA63;SIGNWRITING TEETH ON TONGUE;Mn;0;NSM;;;;;N;;;;;
+1DA64;SIGNWRITING TEETH ON TONGUE MOVEMENT;Mn;0;NSM;;;;;N;;;;;
+1DA65;SIGNWRITING TEETH ON LIPS;Mn;0;NSM;;;;;N;;;;;
+1DA66;SIGNWRITING TEETH ON LIPS MOVEMENT;Mn;0;NSM;;;;;N;;;;;
+1DA67;SIGNWRITING TEETH BITE LIPS;Mn;0;NSM;;;;;N;;;;;
+1DA68;SIGNWRITING MOVEMENT-WALLPLANE JAW;Mn;0;NSM;;;;;N;;;;;
+1DA69;SIGNWRITING MOVEMENT-FLOORPLANE JAW;Mn;0;NSM;;;;;N;;;;;
+1DA6A;SIGNWRITING NECK;Mn;0;NSM;;;;;N;;;;;
+1DA6B;SIGNWRITING HAIR;Mn;0;NSM;;;;;N;;;;;
+1DA6C;SIGNWRITING EXCITEMENT;Mn;0;NSM;;;;;N;;;;;
+1DA6D;SIGNWRITING SHOULDER HIP SPINE;So;0;L;;;;;N;;;;;
+1DA6E;SIGNWRITING SHOULDER HIP POSITIONS;So;0;L;;;;;N;;;;;
+1DA6F;SIGNWRITING WALLPLANE SHOULDER HIP MOVE;So;0;L;;;;;N;;;;;
+1DA70;SIGNWRITING FLOORPLANE SHOULDER HIP MOVE;So;0;L;;;;;N;;;;;
+1DA71;SIGNWRITING SHOULDER TILTING FROM WAIST;So;0;L;;;;;N;;;;;
+1DA72;SIGNWRITING TORSO-WALLPLANE STRAIGHT STRETCH;So;0;L;;;;;N;;;;;
+1DA73;SIGNWRITING TORSO-WALLPLANE CURVED BEND;So;0;L;;;;;N;;;;;
+1DA74;SIGNWRITING TORSO-FLOORPLANE TWISTING;So;0;L;;;;;N;;;;;
+1DA75;SIGNWRITING UPPER BODY TILTING FROM HIP JOINTS;Mn;0;NSM;;;;;N;;;;;
+1DA76;SIGNWRITING LIMB COMBINATION;So;0;L;;;;;N;;;;;
+1DA77;SIGNWRITING LIMB LENGTH-1;So;0;L;;;;;N;;;;;
+1DA78;SIGNWRITING LIMB LENGTH-2;So;0;L;;;;;N;;;;;
+1DA79;SIGNWRITING LIMB LENGTH-3;So;0;L;;;;;N;;;;;
+1DA7A;SIGNWRITING LIMB LENGTH-4;So;0;L;;;;;N;;;;;
+1DA7B;SIGNWRITING LIMB LENGTH-5;So;0;L;;;;;N;;;;;
+1DA7C;SIGNWRITING LIMB LENGTH-6;So;0;L;;;;;N;;;;;
+1DA7D;SIGNWRITING LIMB LENGTH-7;So;0;L;;;;;N;;;;;
+1DA7E;SIGNWRITING FINGER;So;0;L;;;;;N;;;;;
+1DA7F;SIGNWRITING LOCATION-WALLPLANE SPACE;So;0;L;;;;;N;;;;;
+1DA80;SIGNWRITING LOCATION-FLOORPLANE SPACE;So;0;L;;;;;N;;;;;
+1DA81;SIGNWRITING LOCATION HEIGHT;So;0;L;;;;;N;;;;;
+1DA82;SIGNWRITING LOCATION WIDTH;So;0;L;;;;;N;;;;;
+1DA83;SIGNWRITING LOCATION DEPTH;So;0;L;;;;;N;;;;;
+1DA84;SIGNWRITING LOCATION HEAD NECK;Mn;0;NSM;;;;;N;;;;;
+1DA85;SIGNWRITING LOCATION TORSO;So;0;L;;;;;N;;;;;
+1DA86;SIGNWRITING LOCATION LIMBS DIGITS;So;0;L;;;;;N;;;;;
+1DA87;SIGNWRITING COMMA;Po;0;L;;;;;N;;;;;
+1DA88;SIGNWRITING FULL STOP;Po;0;L;;;;;N;;;;;
+1DA89;SIGNWRITING SEMICOLON;Po;0;L;;;;;N;;;;;
+1DA8A;SIGNWRITING COLON;Po;0;L;;;;;N;;;;;
+1DA8B;SIGNWRITING PARENTHESIS;Po;0;L;;;;;N;;;;;
+1DA9B;SIGNWRITING FILL MODIFIER-2;Mn;0;NSM;;;;;N;;;;;
+1DA9C;SIGNWRITING FILL MODIFIER-3;Mn;0;NSM;;;;;N;;;;;
+1DA9D;SIGNWRITING FILL MODIFIER-4;Mn;0;NSM;;;;;N;;;;;
+1DA9E;SIGNWRITING FILL MODIFIER-5;Mn;0;NSM;;;;;N;;;;;
+1DA9F;SIGNWRITING FILL MODIFIER-6;Mn;0;NSM;;;;;N;;;;;
+1DAA1;SIGNWRITING ROTATION MODIFIER-2;Mn;0;NSM;;;;;N;;;;;
+1DAA2;SIGNWRITING ROTATION MODIFIER-3;Mn;0;NSM;;;;;N;;;;;
+1DAA3;SIGNWRITING ROTATION MODIFIER-4;Mn;0;NSM;;;;;N;;;;;
+1DAA4;SIGNWRITING ROTATION MODIFIER-5;Mn;0;NSM;;;;;N;;;;;
+1DAA5;SIGNWRITING ROTATION MODIFIER-6;Mn;0;NSM;;;;;N;;;;;
+1DAA6;SIGNWRITING ROTATION MODIFIER-7;Mn;0;NSM;;;;;N;;;;;
+1DAA7;SIGNWRITING ROTATION MODIFIER-8;Mn;0;NSM;;;;;N;;;;;
+1DAA8;SIGNWRITING ROTATION MODIFIER-9;Mn;0;NSM;;;;;N;;;;;
+1DAA9;SIGNWRITING ROTATION MODIFIER-10;Mn;0;NSM;;;;;N;;;;;
+1DAAA;SIGNWRITING ROTATION MODIFIER-11;Mn;0;NSM;;;;;N;;;;;
+1DAAB;SIGNWRITING ROTATION MODIFIER-12;Mn;0;NSM;;;;;N;;;;;
+1DAAC;SIGNWRITING ROTATION MODIFIER-13;Mn;0;NSM;;;;;N;;;;;
+1DAAD;SIGNWRITING ROTATION MODIFIER-14;Mn;0;NSM;;;;;N;;;;;
+1DAAE;SIGNWRITING ROTATION MODIFIER-15;Mn;0;NSM;;;;;N;;;;;
+1DAAF;SIGNWRITING ROTATION MODIFIER-16;Mn;0;NSM;;;;;N;;;;;
+1E000;COMBINING GLAGOLITIC LETTER AZU;Mn;230;NSM;;;;;N;;;;;
+1E001;COMBINING GLAGOLITIC LETTER BUKY;Mn;230;NSM;;;;;N;;;;;
+1E002;COMBINING GLAGOLITIC LETTER VEDE;Mn;230;NSM;;;;;N;;;;;
+1E003;COMBINING GLAGOLITIC LETTER GLAGOLI;Mn;230;NSM;;;;;N;;;;;
+1E004;COMBINING GLAGOLITIC LETTER DOBRO;Mn;230;NSM;;;;;N;;;;;
+1E005;COMBINING GLAGOLITIC LETTER YESTU;Mn;230;NSM;;;;;N;;;;;
+1E006;COMBINING GLAGOLITIC LETTER ZHIVETE;Mn;230;NSM;;;;;N;;;;;
+1E008;COMBINING GLAGOLITIC LETTER ZEMLJA;Mn;230;NSM;;;;;N;;;;;
+1E009;COMBINING GLAGOLITIC LETTER IZHE;Mn;230;NSM;;;;;N;;;;;
+1E00A;COMBINING GLAGOLITIC LETTER INITIAL IZHE;Mn;230;NSM;;;;;N;;;;;
+1E00B;COMBINING GLAGOLITIC LETTER I;Mn;230;NSM;;;;;N;;;;;
+1E00C;COMBINING GLAGOLITIC LETTER DJERVI;Mn;230;NSM;;;;;N;;;;;
+1E00D;COMBINING GLAGOLITIC LETTER KAKO;Mn;230;NSM;;;;;N;;;;;
+1E00E;COMBINING GLAGOLITIC LETTER LJUDIJE;Mn;230;NSM;;;;;N;;;;;
+1E00F;COMBINING GLAGOLITIC LETTER MYSLITE;Mn;230;NSM;;;;;N;;;;;
+1E010;COMBINING GLAGOLITIC LETTER NASHI;Mn;230;NSM;;;;;N;;;;;
+1E011;COMBINING GLAGOLITIC LETTER ONU;Mn;230;NSM;;;;;N;;;;;
+1E012;COMBINING GLAGOLITIC LETTER POKOJI;Mn;230;NSM;;;;;N;;;;;
+1E013;COMBINING GLAGOLITIC LETTER RITSI;Mn;230;NSM;;;;;N;;;;;
+1E014;COMBINING GLAGOLITIC LETTER SLOVO;Mn;230;NSM;;;;;N;;;;;
+1E015;COMBINING GLAGOLITIC LETTER TVRIDO;Mn;230;NSM;;;;;N;;;;;
+1E016;COMBINING GLAGOLITIC LETTER UKU;Mn;230;NSM;;;;;N;;;;;
+1E017;COMBINING GLAGOLITIC LETTER FRITU;Mn;230;NSM;;;;;N;;;;;
+1E018;COMBINING GLAGOLITIC LETTER HERU;Mn;230;NSM;;;;;N;;;;;
+1E01B;COMBINING GLAGOLITIC LETTER SHTA;Mn;230;NSM;;;;;N;;;;;
+1E01C;COMBINING GLAGOLITIC LETTER TSI;Mn;230;NSM;;;;;N;;;;;
+1E01D;COMBINING GLAGOLITIC LETTER CHRIVI;Mn;230;NSM;;;;;N;;;;;
+1E01E;COMBINING GLAGOLITIC LETTER SHA;Mn;230;NSM;;;;;N;;;;;
+1E01F;COMBINING GLAGOLITIC LETTER YERU;Mn;230;NSM;;;;;N;;;;;
+1E020;COMBINING GLAGOLITIC LETTER YERI;Mn;230;NSM;;;;;N;;;;;
+1E021;COMBINING GLAGOLITIC LETTER YATI;Mn;230;NSM;;;;;N;;;;;
+1E023;COMBINING GLAGOLITIC LETTER YU;Mn;230;NSM;;;;;N;;;;;
+1E024;COMBINING GLAGOLITIC LETTER SMALL YUS;Mn;230;NSM;;;;;N;;;;;
+1E026;COMBINING GLAGOLITIC LETTER YO;Mn;230;NSM;;;;;N;;;;;
+1E027;COMBINING GLAGOLITIC LETTER IOTATED SMALL YUS;Mn;230;NSM;;;;;N;;;;;
+1E028;COMBINING GLAGOLITIC LETTER BIG YUS;Mn;230;NSM;;;;;N;;;;;
+1E029;COMBINING GLAGOLITIC LETTER IOTATED BIG YUS;Mn;230;NSM;;;;;N;;;;;
+1E02A;COMBINING GLAGOLITIC LETTER FITA;Mn;230;NSM;;;;;N;;;;;
+1E800;MENDE KIKAKUI SYLLABLE M001 KI;Lo;0;R;;;;;N;;;;;
+1E801;MENDE KIKAKUI SYLLABLE M002 KA;Lo;0;R;;;;;N;;;;;
+1E802;MENDE KIKAKUI SYLLABLE M003 KU;Lo;0;R;;;;;N;;;;;
+1E803;MENDE KIKAKUI SYLLABLE M065 KEE;Lo;0;R;;;;;N;;;;;
+1E804;MENDE KIKAKUI SYLLABLE M095 KE;Lo;0;R;;;;;N;;;;;
+1E805;MENDE KIKAKUI SYLLABLE M076 KOO;Lo;0;R;;;;;N;;;;;
+1E806;MENDE KIKAKUI SYLLABLE M048 KO;Lo;0;R;;;;;N;;;;;
+1E807;MENDE KIKAKUI SYLLABLE M179 KUA;Lo;0;R;;;;;N;;;;;
+1E808;MENDE KIKAKUI SYLLABLE M004 WI;Lo;0;R;;;;;N;;;;;
+1E809;MENDE KIKAKUI SYLLABLE M005 WA;Lo;0;R;;;;;N;;;;;
+1E80A;MENDE KIKAKUI SYLLABLE M006 WU;Lo;0;R;;;;;N;;;;;
+1E80B;MENDE KIKAKUI SYLLABLE M126 WEE;Lo;0;R;;;;;N;;;;;
+1E80C;MENDE KIKAKUI SYLLABLE M118 WE;Lo;0;R;;;;;N;;;;;
+1E80D;MENDE KIKAKUI SYLLABLE M114 WOO;Lo;0;R;;;;;N;;;;;
+1E80E;MENDE KIKAKUI SYLLABLE M045 WO;Lo;0;R;;;;;N;;;;;
+1E80F;MENDE KIKAKUI SYLLABLE M194 WUI;Lo;0;R;;;;;N;;;;;
+1E810;MENDE KIKAKUI SYLLABLE M143 WEI;Lo;0;R;;;;;N;;;;;
+1E811;MENDE KIKAKUI SYLLABLE M061 WVI;Lo;0;R;;;;;N;;;;;
+1E812;MENDE KIKAKUI SYLLABLE M049 WVA;Lo;0;R;;;;;N;;;;;
+1E813;MENDE KIKAKUI SYLLABLE M139 WVE;Lo;0;R;;;;;N;;;;;
+1E814;MENDE KIKAKUI SYLLABLE M007 MIN;Lo;0;R;;;;;N;;;;;
+1E815;MENDE KIKAKUI SYLLABLE M008 MAN;Lo;0;R;;;;;N;;;;;
+1E816;MENDE KIKAKUI SYLLABLE M009 MUN;Lo;0;R;;;;;N;;;;;
+1E817;MENDE KIKAKUI SYLLABLE M059 MEN;Lo;0;R;;;;;N;;;;;
+1E818;MENDE KIKAKUI SYLLABLE M094 MON;Lo;0;R;;;;;N;;;;;
+1E819;MENDE KIKAKUI SYLLABLE M154 MUAN;Lo;0;R;;;;;N;;;;;
+1E81A;MENDE KIKAKUI SYLLABLE M189 MUEN;Lo;0;R;;;;;N;;;;;
+1E81B;MENDE KIKAKUI SYLLABLE M010 BI;Lo;0;R;;;;;N;;;;;
+1E81C;MENDE KIKAKUI SYLLABLE M011 BA;Lo;0;R;;;;;N;;;;;
+1E81D;MENDE KIKAKUI SYLLABLE M012 BU;Lo;0;R;;;;;N;;;;;
+1E81E;MENDE KIKAKUI SYLLABLE M150 BEE;Lo;0;R;;;;;N;;;;;
+1E81F;MENDE KIKAKUI SYLLABLE M097 BE;Lo;0;R;;;;;N;;;;;
+1E820;MENDE KIKAKUI SYLLABLE M103 BOO;Lo;0;R;;;;;N;;;;;
+1E821;MENDE KIKAKUI SYLLABLE M138 BO;Lo;0;R;;;;;N;;;;;
+1E822;MENDE KIKAKUI SYLLABLE M013 I;Lo;0;R;;;;;N;;;;;
+1E823;MENDE KIKAKUI SYLLABLE M014 A;Lo;0;R;;;;;N;;;;;
+1E824;MENDE KIKAKUI SYLLABLE M015 U;Lo;0;R;;;;;N;;;;;
+1E825;MENDE KIKAKUI SYLLABLE M163 EE;Lo;0;R;;;;;N;;;;;
+1E826;MENDE KIKAKUI SYLLABLE M100 E;Lo;0;R;;;;;N;;;;;
+1E827;MENDE KIKAKUI SYLLABLE M165 OO;Lo;0;R;;;;;N;;;;;
+1E828;MENDE KIKAKUI SYLLABLE M147 O;Lo;0;R;;;;;N;;;;;
+1E829;MENDE KIKAKUI SYLLABLE M137 EI;Lo;0;R;;;;;N;;;;;
+1E82A;MENDE KIKAKUI SYLLABLE M131 IN;Lo;0;R;;;;;N;;;;;
+1E82B;MENDE KIKAKUI SYLLABLE M135 IN;Lo;0;R;;;;;N;;;;;
+1E82C;MENDE KIKAKUI SYLLABLE M195 AN;Lo;0;R;;;;;N;;;;;
+1E82D;MENDE KIKAKUI SYLLABLE M178 EN;Lo;0;R;;;;;N;;;;;
+1E82E;MENDE KIKAKUI SYLLABLE M019 SI;Lo;0;R;;;;;N;;;;;
+1E82F;MENDE KIKAKUI SYLLABLE M020 SA;Lo;0;R;;;;;N;;;;;
+1E830;MENDE KIKAKUI SYLLABLE M021 SU;Lo;0;R;;;;;N;;;;;
+1E831;MENDE KIKAKUI SYLLABLE M162 SEE;Lo;0;R;;;;;N;;;;;
+1E832;MENDE KIKAKUI SYLLABLE M116 SE;Lo;0;R;;;;;N;;;;;
+1E833;MENDE KIKAKUI SYLLABLE M136 SOO;Lo;0;R;;;;;N;;;;;
+1E834;MENDE KIKAKUI SYLLABLE M079 SO;Lo;0;R;;;;;N;;;;;
+1E835;MENDE KIKAKUI SYLLABLE M196 SIA;Lo;0;R;;;;;N;;;;;
+1E836;MENDE KIKAKUI SYLLABLE M025 LI;Lo;0;R;;;;;N;;;;;
+1E837;MENDE KIKAKUI SYLLABLE M026 LA;Lo;0;R;;;;;N;;;;;
+1E838;MENDE KIKAKUI SYLLABLE M027 LU;Lo;0;R;;;;;N;;;;;
+1E839;MENDE KIKAKUI SYLLABLE M084 LEE;Lo;0;R;;;;;N;;;;;
+1E83A;MENDE KIKAKUI SYLLABLE M073 LE;Lo;0;R;;;;;N;;;;;
+1E83B;MENDE KIKAKUI SYLLABLE M054 LOO;Lo;0;R;;;;;N;;;;;
+1E83C;MENDE KIKAKUI SYLLABLE M153 LO;Lo;0;R;;;;;N;;;;;
+1E83D;MENDE KIKAKUI SYLLABLE M110 LONG LE;Lo;0;R;;;;;N;;;;;
+1E83E;MENDE KIKAKUI SYLLABLE M016 DI;Lo;0;R;;;;;N;;;;;
+1E83F;MENDE KIKAKUI SYLLABLE M017 DA;Lo;0;R;;;;;N;;;;;
+1E840;MENDE KIKAKUI SYLLABLE M018 DU;Lo;0;R;;;;;N;;;;;
+1E841;MENDE KIKAKUI SYLLABLE M089 DEE;Lo;0;R;;;;;N;;;;;
+1E842;MENDE KIKAKUI SYLLABLE M180 DOO;Lo;0;R;;;;;N;;;;;
+1E843;MENDE KIKAKUI SYLLABLE M181 DO;Lo;0;R;;;;;N;;;;;
+1E844;MENDE KIKAKUI SYLLABLE M022 TI;Lo;0;R;;;;;N;;;;;
+1E845;MENDE KIKAKUI SYLLABLE M023 TA;Lo;0;R;;;;;N;;;;;
+1E846;MENDE KIKAKUI SYLLABLE M024 TU;Lo;0;R;;;;;N;;;;;
+1E847;MENDE KIKAKUI SYLLABLE M091 TEE;Lo;0;R;;;;;N;;;;;
+1E848;MENDE KIKAKUI SYLLABLE M055 TE;Lo;0;R;;;;;N;;;;;
+1E849;MENDE KIKAKUI SYLLABLE M104 TOO;Lo;0;R;;;;;N;;;;;
+1E84A;MENDE KIKAKUI SYLLABLE M069 TO;Lo;0;R;;;;;N;;;;;
+1E84B;MENDE KIKAKUI SYLLABLE M028 JI;Lo;0;R;;;;;N;;;;;
+1E84C;MENDE KIKAKUI SYLLABLE M029 JA;Lo;0;R;;;;;N;;;;;
+1E84D;MENDE KIKAKUI SYLLABLE M030 JU;Lo;0;R;;;;;N;;;;;
+1E84E;MENDE KIKAKUI SYLLABLE M157 JEE;Lo;0;R;;;;;N;;;;;
+1E84F;MENDE KIKAKUI SYLLABLE M113 JE;Lo;0;R;;;;;N;;;;;
+1E850;MENDE KIKAKUI SYLLABLE M160 JOO;Lo;0;R;;;;;N;;;;;
+1E851;MENDE KIKAKUI SYLLABLE M063 JO;Lo;0;R;;;;;N;;;;;
+1E852;MENDE KIKAKUI SYLLABLE M175 LONG JO;Lo;0;R;;;;;N;;;;;
+1E853;MENDE KIKAKUI SYLLABLE M031 YI;Lo;0;R;;;;;N;;;;;
+1E854;MENDE KIKAKUI SYLLABLE M032 YA;Lo;0;R;;;;;N;;;;;
+1E855;MENDE KIKAKUI SYLLABLE M033 YU;Lo;0;R;;;;;N;;;;;
+1E856;MENDE KIKAKUI SYLLABLE M109 YEE;Lo;0;R;;;;;N;;;;;
+1E857;MENDE KIKAKUI SYLLABLE M080 YE;Lo;0;R;;;;;N;;;;;
+1E858;MENDE KIKAKUI SYLLABLE M141 YOO;Lo;0;R;;;;;N;;;;;
+1E859;MENDE KIKAKUI SYLLABLE M121 YO;Lo;0;R;;;;;N;;;;;
+1E85A;MENDE KIKAKUI SYLLABLE M034 FI;Lo;0;R;;;;;N;;;;;
+1E85B;MENDE KIKAKUI SYLLABLE M035 FA;Lo;0;R;;;;;N;;;;;
+1E85C;MENDE KIKAKUI SYLLABLE M036 FU;Lo;0;R;;;;;N;;;;;
+1E85D;MENDE KIKAKUI SYLLABLE M078 FEE;Lo;0;R;;;;;N;;;;;
+1E85E;MENDE KIKAKUI SYLLABLE M075 FE;Lo;0;R;;;;;N;;;;;
+1E85F;MENDE KIKAKUI SYLLABLE M133 FOO;Lo;0;R;;;;;N;;;;;
+1E860;MENDE KIKAKUI SYLLABLE M088 FO;Lo;0;R;;;;;N;;;;;
+1E861;MENDE KIKAKUI SYLLABLE M197 FUA;Lo;0;R;;;;;N;;;;;
+1E862;MENDE KIKAKUI SYLLABLE M101 FAN;Lo;0;R;;;;;N;;;;;
+1E863;MENDE KIKAKUI SYLLABLE M037 NIN;Lo;0;R;;;;;N;;;;;
+1E864;MENDE KIKAKUI SYLLABLE M038 NAN;Lo;0;R;;;;;N;;;;;
+1E865;MENDE KIKAKUI SYLLABLE M039 NUN;Lo;0;R;;;;;N;;;;;
+1E866;MENDE KIKAKUI SYLLABLE M117 NEN;Lo;0;R;;;;;N;;;;;
+1E867;MENDE KIKAKUI SYLLABLE M169 NON;Lo;0;R;;;;;N;;;;;
+1E868;MENDE KIKAKUI SYLLABLE M176 HI;Lo;0;R;;;;;N;;;;;
+1E869;MENDE KIKAKUI SYLLABLE M041 HA;Lo;0;R;;;;;N;;;;;
+1E86A;MENDE KIKAKUI SYLLABLE M186 HU;Lo;0;R;;;;;N;;;;;
+1E86B;MENDE KIKAKUI SYLLABLE M040 HEE;Lo;0;R;;;;;N;;;;;
+1E86C;MENDE KIKAKUI SYLLABLE M096 HE;Lo;0;R;;;;;N;;;;;
+1E86D;MENDE KIKAKUI SYLLABLE M042 HOO;Lo;0;R;;;;;N;;;;;
+1E86E;MENDE KIKAKUI SYLLABLE M140 HO;Lo;0;R;;;;;N;;;;;
+1E86F;MENDE KIKAKUI SYLLABLE M083 HEEI;Lo;0;R;;;;;N;;;;;
+1E870;MENDE KIKAKUI SYLLABLE M128 HOOU;Lo;0;R;;;;;N;;;;;
+1E871;MENDE KIKAKUI SYLLABLE M053 HIN;Lo;0;R;;;;;N;;;;;
+1E872;MENDE KIKAKUI SYLLABLE M130 HAN;Lo;0;R;;;;;N;;;;;
+1E873;MENDE KIKAKUI SYLLABLE M087 HUN;Lo;0;R;;;;;N;;;;;
+1E874;MENDE KIKAKUI SYLLABLE M052 HEN;Lo;0;R;;;;;N;;;;;
+1E875;MENDE KIKAKUI SYLLABLE M193 HON;Lo;0;R;;;;;N;;;;;
+1E876;MENDE KIKAKUI SYLLABLE M046 HUAN;Lo;0;R;;;;;N;;;;;
+1E877;MENDE KIKAKUI SYLLABLE M090 NGGI;Lo;0;R;;;;;N;;;;;
+1E878;MENDE KIKAKUI SYLLABLE M043 NGGA;Lo;0;R;;;;;N;;;;;
+1E879;MENDE KIKAKUI SYLLABLE M082 NGGU;Lo;0;R;;;;;N;;;;;
+1E87A;MENDE KIKAKUI SYLLABLE M115 NGGEE;Lo;0;R;;;;;N;;;;;
+1E87B;MENDE KIKAKUI SYLLABLE M146 NGGE;Lo;0;R;;;;;N;;;;;
+1E87C;MENDE KIKAKUI SYLLABLE M156 NGGOO;Lo;0;R;;;;;N;;;;;
+1E87D;MENDE KIKAKUI SYLLABLE M120 NGGO;Lo;0;R;;;;;N;;;;;
+1E87E;MENDE KIKAKUI SYLLABLE M159 NGGAA;Lo;0;R;;;;;N;;;;;
+1E87F;MENDE KIKAKUI SYLLABLE M127 NGGUA;Lo;0;R;;;;;N;;;;;
+1E880;MENDE KIKAKUI SYLLABLE M086 LONG NGGE;Lo;0;R;;;;;N;;;;;
+1E881;MENDE KIKAKUI SYLLABLE M106 LONG NGGOO;Lo;0;R;;;;;N;;;;;
+1E882;MENDE KIKAKUI SYLLABLE M183 LONG NGGO;Lo;0;R;;;;;N;;;;;
+1E883;MENDE KIKAKUI SYLLABLE M155 GI;Lo;0;R;;;;;N;;;;;
+1E884;MENDE KIKAKUI SYLLABLE M111 GA;Lo;0;R;;;;;N;;;;;
+1E885;MENDE KIKAKUI SYLLABLE M168 GU;Lo;0;R;;;;;N;;;;;
+1E886;MENDE KIKAKUI SYLLABLE M190 GEE;Lo;0;R;;;;;N;;;;;
+1E887;MENDE KIKAKUI SYLLABLE M166 GUEI;Lo;0;R;;;;;N;;;;;
+1E888;MENDE KIKAKUI SYLLABLE M167 GUAN;Lo;0;R;;;;;N;;;;;
+1E889;MENDE KIKAKUI SYLLABLE M184 NGEN;Lo;0;R;;;;;N;;;;;
+1E88A;MENDE KIKAKUI SYLLABLE M057 NGON;Lo;0;R;;;;;N;;;;;
+1E88B;MENDE KIKAKUI SYLLABLE M177 NGUAN;Lo;0;R;;;;;N;;;;;
+1E88C;MENDE KIKAKUI SYLLABLE M068 PI;Lo;0;R;;;;;N;;;;;
+1E88D;MENDE KIKAKUI SYLLABLE M099 PA;Lo;0;R;;;;;N;;;;;
+1E88E;MENDE KIKAKUI SYLLABLE M050 PU;Lo;0;R;;;;;N;;;;;
+1E88F;MENDE KIKAKUI SYLLABLE M081 PEE;Lo;0;R;;;;;N;;;;;
+1E890;MENDE KIKAKUI SYLLABLE M051 PE;Lo;0;R;;;;;N;;;;;
+1E891;MENDE KIKAKUI SYLLABLE M102 POO;Lo;0;R;;;;;N;;;;;
+1E892;MENDE KIKAKUI SYLLABLE M066 PO;Lo;0;R;;;;;N;;;;;
+1E893;MENDE KIKAKUI SYLLABLE M145 MBI;Lo;0;R;;;;;N;;;;;
+1E894;MENDE KIKAKUI SYLLABLE M062 MBA;Lo;0;R;;;;;N;;;;;
+1E895;MENDE KIKAKUI SYLLABLE M122 MBU;Lo;0;R;;;;;N;;;;;
+1E896;MENDE KIKAKUI SYLLABLE M047 MBEE;Lo;0;R;;;;;N;;;;;
+1E897;MENDE KIKAKUI SYLLABLE M188 MBEE;Lo;0;R;;;;;N;;;;;
+1E898;MENDE KIKAKUI SYLLABLE M072 MBE;Lo;0;R;;;;;N;;;;;
+1E899;MENDE KIKAKUI SYLLABLE M172 MBOO;Lo;0;R;;;;;N;;;;;
+1E89A;MENDE KIKAKUI SYLLABLE M174 MBO;Lo;0;R;;;;;N;;;;;
+1E89B;MENDE KIKAKUI SYLLABLE M187 MBUU;Lo;0;R;;;;;N;;;;;
+1E89C;MENDE KIKAKUI SYLLABLE M161 LONG MBE;Lo;0;R;;;;;N;;;;;
+1E89D;MENDE KIKAKUI SYLLABLE M105 LONG MBOO;Lo;0;R;;;;;N;;;;;
+1E89E;MENDE KIKAKUI SYLLABLE M142 LONG MBO;Lo;0;R;;;;;N;;;;;
+1E89F;MENDE KIKAKUI SYLLABLE M132 KPI;Lo;0;R;;;;;N;;;;;
+1E8A0;MENDE KIKAKUI SYLLABLE M092 KPA;Lo;0;R;;;;;N;;;;;
+1E8A1;MENDE KIKAKUI SYLLABLE M074 KPU;Lo;0;R;;;;;N;;;;;
+1E8A2;MENDE KIKAKUI SYLLABLE M044 KPEE;Lo;0;R;;;;;N;;;;;
+1E8A3;MENDE KIKAKUI SYLLABLE M108 KPE;Lo;0;R;;;;;N;;;;;
+1E8A4;MENDE KIKAKUI SYLLABLE M112 KPOO;Lo;0;R;;;;;N;;;;;
+1E8A5;MENDE KIKAKUI SYLLABLE M158 KPO;Lo;0;R;;;;;N;;;;;
+1E8A6;MENDE KIKAKUI SYLLABLE M124 GBI;Lo;0;R;;;;;N;;;;;
+1E8A7;MENDE KIKAKUI SYLLABLE M056 GBA;Lo;0;R;;;;;N;;;;;
+1E8A8;MENDE KIKAKUI SYLLABLE M148 GBU;Lo;0;R;;;;;N;;;;;
+1E8A9;MENDE KIKAKUI SYLLABLE M093 GBEE;Lo;0;R;;;;;N;;;;;
+1E8AA;MENDE KIKAKUI SYLLABLE M107 GBE;Lo;0;R;;;;;N;;;;;
+1E8AB;MENDE KIKAKUI SYLLABLE M071 GBOO;Lo;0;R;;;;;N;;;;;
+1E8AC;MENDE KIKAKUI SYLLABLE M070 GBO;Lo;0;R;;;;;N;;;;;
+1E8AD;MENDE KIKAKUI SYLLABLE M171 RA;Lo;0;R;;;;;N;;;;;
+1E8AE;MENDE KIKAKUI SYLLABLE M123 NDI;Lo;0;R;;;;;N;;;;;
+1E8AF;MENDE KIKAKUI SYLLABLE M129 NDA;Lo;0;R;;;;;N;;;;;
+1E8B0;MENDE KIKAKUI SYLLABLE M125 NDU;Lo;0;R;;;;;N;;;;;
+1E8B1;MENDE KIKAKUI SYLLABLE M191 NDEE;Lo;0;R;;;;;N;;;;;
+1E8B2;MENDE KIKAKUI SYLLABLE M119 NDE;Lo;0;R;;;;;N;;;;;
+1E8B3;MENDE KIKAKUI SYLLABLE M067 NDOO;Lo;0;R;;;;;N;;;;;
+1E8B4;MENDE KIKAKUI SYLLABLE M064 NDO;Lo;0;R;;;;;N;;;;;
+1E8B5;MENDE KIKAKUI SYLLABLE M152 NJA;Lo;0;R;;;;;N;;;;;
+1E8B6;MENDE KIKAKUI SYLLABLE M192 NJU;Lo;0;R;;;;;N;;;;;
+1E8B7;MENDE KIKAKUI SYLLABLE M149 NJEE;Lo;0;R;;;;;N;;;;;
+1E8B8;MENDE KIKAKUI SYLLABLE M134 NJOO;Lo;0;R;;;;;N;;;;;
+1E8B9;MENDE KIKAKUI SYLLABLE M182 VI;Lo;0;R;;;;;N;;;;;
+1E8BA;MENDE KIKAKUI SYLLABLE M185 VA;Lo;0;R;;;;;N;;;;;
+1E8BB;MENDE KIKAKUI SYLLABLE M151 VU;Lo;0;R;;;;;N;;;;;
+1E8BC;MENDE KIKAKUI SYLLABLE M173 VEE;Lo;0;R;;;;;N;;;;;
+1E8BD;MENDE KIKAKUI SYLLABLE M085 VE;Lo;0;R;;;;;N;;;;;
+1E8BE;MENDE KIKAKUI SYLLABLE M144 VOO;Lo;0;R;;;;;N;;;;;
+1E8BF;MENDE KIKAKUI SYLLABLE M077 VO;Lo;0;R;;;;;N;;;;;
+1E8C0;MENDE KIKAKUI SYLLABLE M164 NYIN;Lo;0;R;;;;;N;;;;;
+1E8C1;MENDE KIKAKUI SYLLABLE M058 NYAN;Lo;0;R;;;;;N;;;;;
+1E8C2;MENDE KIKAKUI SYLLABLE M170 NYUN;Lo;0;R;;;;;N;;;;;
+1E8C3;MENDE KIKAKUI SYLLABLE M098 NYEN;Lo;0;R;;;;;N;;;;;
+1E8C4;MENDE KIKAKUI SYLLABLE M060 NYON;Lo;0;R;;;;;N;;;;;
+1E8C7;MENDE KIKAKUI DIGIT ONE;No;0;R;;;;1;N;;;;;
+1E8C8;MENDE KIKAKUI DIGIT TWO;No;0;R;;;;2;N;;;;;
+1E8C9;MENDE KIKAKUI DIGIT THREE;No;0;R;;;;3;N;;;;;
+1E8CA;MENDE KIKAKUI DIGIT FOUR;No;0;R;;;;4;N;;;;;
+1E8CB;MENDE KIKAKUI DIGIT FIVE;No;0;R;;;;5;N;;;;;
+1E8CC;MENDE KIKAKUI DIGIT SIX;No;0;R;;;;6;N;;;;;
+1E8CD;MENDE KIKAKUI DIGIT SEVEN;No;0;R;;;;7;N;;;;;
+1E8CE;MENDE KIKAKUI DIGIT EIGHT;No;0;R;;;;8;N;;;;;
+1E8CF;MENDE KIKAKUI DIGIT NINE;No;0;R;;;;9;N;;;;;
+1E8D0;MENDE KIKAKUI COMBINING NUMBER TEENS;Mn;220;NSM;;;;;N;;;;;
+1E8D1;MENDE KIKAKUI COMBINING NUMBER TENS;Mn;220;NSM;;;;;N;;;;;
+1E8D2;MENDE KIKAKUI COMBINING NUMBER HUNDREDS;Mn;220;NSM;;;;;N;;;;;
+1E8D3;MENDE KIKAKUI COMBINING NUMBER THOUSANDS;Mn;220;NSM;;;;;N;;;;;
+1E8D4;MENDE KIKAKUI COMBINING NUMBER TEN THOUSANDS;Mn;220;NSM;;;;;N;;;;;
+1E8D5;MENDE KIKAKUI COMBINING NUMBER HUNDRED THOUSANDS;Mn;220;NSM;;;;;N;;;;;
+1E8D6;MENDE KIKAKUI COMBINING NUMBER MILLIONS;Mn;220;NSM;;;;;N;;;;;
+1E900;ADLAM CAPITAL LETTER ALIF;Lu;0;R;;;;;N;;;;1E922;
+1E901;ADLAM CAPITAL LETTER DAALI;Lu;0;R;;;;;N;;;;1E923;
+1E902;ADLAM CAPITAL LETTER LAAM;Lu;0;R;;;;;N;;;;1E924;
+1E903;ADLAM CAPITAL LETTER MIIM;Lu;0;R;;;;;N;;;;1E925;
+1E904;ADLAM CAPITAL LETTER BA;Lu;0;R;;;;;N;;;;1E926;
+1E905;ADLAM CAPITAL LETTER SINNYIIYHE;Lu;0;R;;;;;N;;;;1E927;
+1E906;ADLAM CAPITAL LETTER PE;Lu;0;R;;;;;N;;;;1E928;
+1E907;ADLAM CAPITAL LETTER BHE;Lu;0;R;;;;;N;;;;1E929;
+1E908;ADLAM CAPITAL LETTER RA;Lu;0;R;;;;;N;;;;1E92A;
+1E909;ADLAM CAPITAL LETTER E;Lu;0;R;;;;;N;;;;1E92B;
+1E90A;ADLAM CAPITAL LETTER FA;Lu;0;R;;;;;N;;;;1E92C;
+1E90B;ADLAM CAPITAL LETTER I;Lu;0;R;;;;;N;;;;1E92D;
+1E90C;ADLAM CAPITAL LETTER O;Lu;0;R;;;;;N;;;;1E92E;
+1E90D;ADLAM CAPITAL LETTER DHA;Lu;0;R;;;;;N;;;;1E92F;
+1E90E;ADLAM CAPITAL LETTER YHE;Lu;0;R;;;;;N;;;;1E930;
+1E90F;ADLAM CAPITAL LETTER WAW;Lu;0;R;;;;;N;;;;1E931;
+1E910;ADLAM CAPITAL LETTER NUN;Lu;0;R;;;;;N;;;;1E932;
+1E911;ADLAM CAPITAL LETTER KAF;Lu;0;R;;;;;N;;;;1E933;
+1E912;ADLAM CAPITAL LETTER YA;Lu;0;R;;;;;N;;;;1E934;
+1E913;ADLAM CAPITAL LETTER U;Lu;0;R;;;;;N;;;;1E935;
+1E914;ADLAM CAPITAL LETTER JIIM;Lu;0;R;;;;;N;;;;1E936;
+1E915;ADLAM CAPITAL LETTER CHI;Lu;0;R;;;;;N;;;;1E937;
+1E916;ADLAM CAPITAL LETTER HA;Lu;0;R;;;;;N;;;;1E938;
+1E917;ADLAM CAPITAL LETTER QAAF;Lu;0;R;;;;;N;;;;1E939;
+1E918;ADLAM CAPITAL LETTER GA;Lu;0;R;;;;;N;;;;1E93A;
+1E919;ADLAM CAPITAL LETTER NYA;Lu;0;R;;;;;N;;;;1E93B;
+1E91A;ADLAM CAPITAL LETTER TU;Lu;0;R;;;;;N;;;;1E93C;
+1E91B;ADLAM CAPITAL LETTER NHA;Lu;0;R;;;;;N;;;;1E93D;
+1E91C;ADLAM CAPITAL LETTER VA;Lu;0;R;;;;;N;;;;1E93E;
+1E91D;ADLAM CAPITAL LETTER KHA;Lu;0;R;;;;;N;;;;1E93F;
+1E91E;ADLAM CAPITAL LETTER GBE;Lu;0;R;;;;;N;;;;1E940;
+1E91F;ADLAM CAPITAL LETTER ZAL;Lu;0;R;;;;;N;;;;1E941;
+1E920;ADLAM CAPITAL LETTER KPO;Lu;0;R;;;;;N;;;;1E942;
+1E921;ADLAM CAPITAL LETTER SHA;Lu;0;R;;;;;N;;;;1E943;
+1E922;ADLAM SMALL LETTER ALIF;Ll;0;R;;;;;N;;;1E900;;1E900
+1E923;ADLAM SMALL LETTER DAALI;Ll;0;R;;;;;N;;;1E901;;1E901
+1E924;ADLAM SMALL LETTER LAAM;Ll;0;R;;;;;N;;;1E902;;1E902
+1E925;ADLAM SMALL LETTER MIIM;Ll;0;R;;;;;N;;;1E903;;1E903
+1E926;ADLAM SMALL LETTER BA;Ll;0;R;;;;;N;;;1E904;;1E904
+1E927;ADLAM SMALL LETTER SINNYIIYHE;Ll;0;R;;;;;N;;;1E905;;1E905
+1E928;ADLAM SMALL LETTER PE;Ll;0;R;;;;;N;;;1E906;;1E906
+1E929;ADLAM SMALL LETTER BHE;Ll;0;R;;;;;N;;;1E907;;1E907
+1E92A;ADLAM SMALL LETTER RA;Ll;0;R;;;;;N;;;1E908;;1E908
+1E92B;ADLAM SMALL LETTER E;Ll;0;R;;;;;N;;;1E909;;1E909
+1E92C;ADLAM SMALL LETTER FA;Ll;0;R;;;;;N;;;1E90A;;1E90A
+1E92D;ADLAM SMALL LETTER I;Ll;0;R;;;;;N;;;1E90B;;1E90B
+1E92E;ADLAM SMALL LETTER O;Ll;0;R;;;;;N;;;1E90C;;1E90C
+1E92F;ADLAM SMALL LETTER DHA;Ll;0;R;;;;;N;;;1E90D;;1E90D
+1E930;ADLAM SMALL LETTER YHE;Ll;0;R;;;;;N;;;1E90E;;1E90E
+1E931;ADLAM SMALL LETTER WAW;Ll;0;R;;;;;N;;;1E90F;;1E90F
+1E932;ADLAM SMALL LETTER NUN;Ll;0;R;;;;;N;;;1E910;;1E910
+1E933;ADLAM SMALL LETTER KAF;Ll;0;R;;;;;N;;;1E911;;1E911
+1E934;ADLAM SMALL LETTER YA;Ll;0;R;;;;;N;;;1E912;;1E912
+1E935;ADLAM SMALL LETTER U;Ll;0;R;;;;;N;;;1E913;;1E913
+1E936;ADLAM SMALL LETTER JIIM;Ll;0;R;;;;;N;;;1E914;;1E914
+1E937;ADLAM SMALL LETTER CHI;Ll;0;R;;;;;N;;;1E915;;1E915
+1E938;ADLAM SMALL LETTER HA;Ll;0;R;;;;;N;;;1E916;;1E916
+1E939;ADLAM SMALL LETTER QAAF;Ll;0;R;;;;;N;;;1E917;;1E917
+1E93A;ADLAM SMALL LETTER GA;Ll;0;R;;;;;N;;;1E918;;1E918
+1E93B;ADLAM SMALL LETTER NYA;Ll;0;R;;;;;N;;;1E919;;1E919
+1E93C;ADLAM SMALL LETTER TU;Ll;0;R;;;;;N;;;1E91A;;1E91A
+1E93D;ADLAM SMALL LETTER NHA;Ll;0;R;;;;;N;;;1E91B;;1E91B
+1E93E;ADLAM SMALL LETTER VA;Ll;0;R;;;;;N;;;1E91C;;1E91C
+1E93F;ADLAM SMALL LETTER KHA;Ll;0;R;;;;;N;;;1E91D;;1E91D
+1E940;ADLAM SMALL LETTER GBE;Ll;0;R;;;;;N;;;1E91E;;1E91E
+1E941;ADLAM SMALL LETTER ZAL;Ll;0;R;;;;;N;;;1E91F;;1E91F
+1E942;ADLAM SMALL LETTER KPO;Ll;0;R;;;;;N;;;1E920;;1E920
+1E943;ADLAM SMALL LETTER SHA;Ll;0;R;;;;;N;;;1E921;;1E921
+1E944;ADLAM ALIF LENGTHENER;Mn;230;NSM;;;;;N;;;;;
+1E945;ADLAM VOWEL LENGTHENER;Mn;230;NSM;;;;;N;;;;;
+1E946;ADLAM GEMINATION MARK;Mn;230;NSM;;;;;N;;;;;
+1E947;ADLAM HAMZA;Mn;230;NSM;;;;;N;;;;;
+1E948;ADLAM CONSONANT MODIFIER;Mn;230;NSM;;;;;N;;;;;
+1E949;ADLAM GEMINATE CONSONANT MODIFIER;Mn;230;NSM;;;;;N;;;;;
+1E94A;ADLAM NUKTA;Mn;7;NSM;;;;;N;;;;;
+1E950;ADLAM DIGIT ZERO;Nd;0;R;;0;0;0;N;;;;;
+1E951;ADLAM DIGIT ONE;Nd;0;R;;1;1;1;N;;;;;
+1E952;ADLAM DIGIT TWO;Nd;0;R;;2;2;2;N;;;;;
+1E953;ADLAM DIGIT THREE;Nd;0;R;;3;3;3;N;;;;;
+1E954;ADLAM DIGIT FOUR;Nd;0;R;;4;4;4;N;;;;;
+1E955;ADLAM DIGIT FIVE;Nd;0;R;;5;5;5;N;;;;;
+1E956;ADLAM DIGIT SIX;Nd;0;R;;6;6;6;N;;;;;
+1E957;ADLAM DIGIT SEVEN;Nd;0;R;;7;7;7;N;;;;;
+1E958;ADLAM DIGIT EIGHT;Nd;0;R;;8;8;8;N;;;;;
+1E959;ADLAM DIGIT NINE;Nd;0;R;;9;9;9;N;;;;;
+1E95E;ADLAM INITIAL EXCLAMATION MARK;Po;0;R;;;;;N;;;;;
+1E95F;ADLAM INITIAL QUESTION MARK;Po;0;R;;;;;N;;;;;
+1EE00;ARABIC MATHEMATICAL ALEF;Lo;0;AL;<font> 0627;;;;N;;;;;
+1EE01;ARABIC MATHEMATICAL BEH;Lo;0;AL;<font> 0628;;;;N;;;;;
+1EE02;ARABIC MATHEMATICAL JEEM;Lo;0;AL;<font> 062C;;;;N;;;;;
+1EE03;ARABIC MATHEMATICAL DAL;Lo;0;AL;<font> 062F;;;;N;;;;;
+1EE05;ARABIC MATHEMATICAL WAW;Lo;0;AL;<font> 0648;;;;N;;;;;
+1EE06;ARABIC MATHEMATICAL ZAIN;Lo;0;AL;<font> 0632;;;;N;;;;;
+1EE07;ARABIC MATHEMATICAL HAH;Lo;0;AL;<font> 062D;;;;N;;;;;
+1EE08;ARABIC MATHEMATICAL TAH;Lo;0;AL;<font> 0637;;;;N;;;;;
+1EE09;ARABIC MATHEMATICAL YEH;Lo;0;AL;<font> 064A;;;;N;;;;;
+1EE0A;ARABIC MATHEMATICAL KAF;Lo;0;AL;<font> 0643;;;;N;;;;;
+1EE0B;ARABIC MATHEMATICAL LAM;Lo;0;AL;<font> 0644;;;;N;;;;;
+1EE0C;ARABIC MATHEMATICAL MEEM;Lo;0;AL;<font> 0645;;;;N;;;;;
+1EE0D;ARABIC MATHEMATICAL NOON;Lo;0;AL;<font> 0646;;;;N;;;;;
+1EE0E;ARABIC MATHEMATICAL SEEN;Lo;0;AL;<font> 0633;;;;N;;;;;
+1EE0F;ARABIC MATHEMATICAL AIN;Lo;0;AL;<font> 0639;;;;N;;;;;
+1EE10;ARABIC MATHEMATICAL FEH;Lo;0;AL;<font> 0641;;;;N;;;;;
+1EE11;ARABIC MATHEMATICAL SAD;Lo;0;AL;<font> 0635;;;;N;;;;;
+1EE12;ARABIC MATHEMATICAL QAF;Lo;0;AL;<font> 0642;;;;N;;;;;
+1EE13;ARABIC MATHEMATICAL REH;Lo;0;AL;<font> 0631;;;;N;;;;;
+1EE14;ARABIC MATHEMATICAL SHEEN;Lo;0;AL;<font> 0634;;;;N;;;;;
+1EE15;ARABIC MATHEMATICAL TEH;Lo;0;AL;<font> 062A;;;;N;;;;;
+1EE16;ARABIC MATHEMATICAL THEH;Lo;0;AL;<font> 062B;;;;N;;;;;
+1EE17;ARABIC MATHEMATICAL KHAH;Lo;0;AL;<font> 062E;;;;N;;;;;
+1EE18;ARABIC MATHEMATICAL THAL;Lo;0;AL;<font> 0630;;;;N;;;;;
+1EE19;ARABIC MATHEMATICAL DAD;Lo;0;AL;<font> 0636;;;;N;;;;;
+1EE1A;ARABIC MATHEMATICAL ZAH;Lo;0;AL;<font> 0638;;;;N;;;;;
+1EE1B;ARABIC MATHEMATICAL GHAIN;Lo;0;AL;<font> 063A;;;;N;;;;;
+1EE1C;ARABIC MATHEMATICAL DOTLESS BEH;Lo;0;AL;<font> 066E;;;;N;;;;;
+1EE1D;ARABIC MATHEMATICAL DOTLESS NOON;Lo;0;AL;<font> 06BA;;;;N;;;;;
+1EE1E;ARABIC MATHEMATICAL DOTLESS FEH;Lo;0;AL;<font> 06A1;;;;N;;;;;
+1EE1F;ARABIC MATHEMATICAL DOTLESS QAF;Lo;0;AL;<font> 066F;;;;N;;;;;
+1EE21;ARABIC MATHEMATICAL INITIAL BEH;Lo;0;AL;<font> 0628;;;;N;;;;;
+1EE22;ARABIC MATHEMATICAL INITIAL JEEM;Lo;0;AL;<font> 062C;;;;N;;;;;
+1EE24;ARABIC MATHEMATICAL INITIAL HEH;Lo;0;AL;<font> 0647;;;;N;;;;;
+1EE27;ARABIC MATHEMATICAL INITIAL HAH;Lo;0;AL;<font> 062D;;;;N;;;;;
+1EE29;ARABIC MATHEMATICAL INITIAL YEH;Lo;0;AL;<font> 064A;;;;N;;;;;
+1EE2A;ARABIC MATHEMATICAL INITIAL KAF;Lo;0;AL;<font> 0643;;;;N;;;;;
+1EE2B;ARABIC MATHEMATICAL INITIAL LAM;Lo;0;AL;<font> 0644;;;;N;;;;;
+1EE2C;ARABIC MATHEMATICAL INITIAL MEEM;Lo;0;AL;<font> 0645;;;;N;;;;;
+1EE2D;ARABIC MATHEMATICAL INITIAL NOON;Lo;0;AL;<font> 0646;;;;N;;;;;
+1EE2E;ARABIC MATHEMATICAL INITIAL SEEN;Lo;0;AL;<font> 0633;;;;N;;;;;
+1EE2F;ARABIC MATHEMATICAL INITIAL AIN;Lo;0;AL;<font> 0639;;;;N;;;;;
+1EE30;ARABIC MATHEMATICAL INITIAL FEH;Lo;0;AL;<font> 0641;;;;N;;;;;
+1EE31;ARABIC MATHEMATICAL INITIAL SAD;Lo;0;AL;<font> 0635;;;;N;;;;;
+1EE32;ARABIC MATHEMATICAL INITIAL QAF;Lo;0;AL;<font> 0642;;;;N;;;;;
+1EE34;ARABIC MATHEMATICAL INITIAL SHEEN;Lo;0;AL;<font> 0634;;;;N;;;;;
+1EE35;ARABIC MATHEMATICAL INITIAL TEH;Lo;0;AL;<font> 062A;;;;N;;;;;
+1EE36;ARABIC MATHEMATICAL INITIAL THEH;Lo;0;AL;<font> 062B;;;;N;;;;;
+1EE37;ARABIC MATHEMATICAL INITIAL KHAH;Lo;0;AL;<font> 062E;;;;N;;;;;
+1EE39;ARABIC MATHEMATICAL INITIAL DAD;Lo;0;AL;<font> 0636;;;;N;;;;;
+1EE3B;ARABIC MATHEMATICAL INITIAL GHAIN;Lo;0;AL;<font> 063A;;;;N;;;;;
+1EE42;ARABIC MATHEMATICAL TAILED JEEM;Lo;0;AL;<font> 062C;;;;N;;;;;
+1EE47;ARABIC MATHEMATICAL TAILED HAH;Lo;0;AL;<font> 062D;;;;N;;;;;
+1EE49;ARABIC MATHEMATICAL TAILED YEH;Lo;0;AL;<font> 064A;;;;N;;;;;
+1EE4B;ARABIC MATHEMATICAL TAILED LAM;Lo;0;AL;<font> 0644;;;;N;;;;;
+1EE4D;ARABIC MATHEMATICAL TAILED NOON;Lo;0;AL;<font> 0646;;;;N;;;;;
+1EE4E;ARABIC MATHEMATICAL TAILED SEEN;Lo;0;AL;<font> 0633;;;;N;;;;;
+1EE4F;ARABIC MATHEMATICAL TAILED AIN;Lo;0;AL;<font> 0639;;;;N;;;;;
+1EE51;ARABIC MATHEMATICAL TAILED SAD;Lo;0;AL;<font> 0635;;;;N;;;;;
+1EE52;ARABIC MATHEMATICAL TAILED QAF;Lo;0;AL;<font> 0642;;;;N;;;;;
+1EE54;ARABIC MATHEMATICAL TAILED SHEEN;Lo;0;AL;<font> 0634;;;;N;;;;;
+1EE57;ARABIC MATHEMATICAL TAILED KHAH;Lo;0;AL;<font> 062E;;;;N;;;;;
+1EE59;ARABIC MATHEMATICAL TAILED DAD;Lo;0;AL;<font> 0636;;;;N;;;;;
+1EE5B;ARABIC MATHEMATICAL TAILED GHAIN;Lo;0;AL;<font> 063A;;;;N;;;;;
+1EE5D;ARABIC MATHEMATICAL TAILED DOTLESS NOON;Lo;0;AL;<font> 06BA;;;;N;;;;;
+1EE5F;ARABIC MATHEMATICAL TAILED DOTLESS QAF;Lo;0;AL;<font> 066F;;;;N;;;;;
+1EE61;ARABIC MATHEMATICAL STRETCHED BEH;Lo;0;AL;<font> 0628;;;;N;;;;;
+1EE62;ARABIC MATHEMATICAL STRETCHED JEEM;Lo;0;AL;<font> 062C;;;;N;;;;;
+1EE64;ARABIC MATHEMATICAL STRETCHED HEH;Lo;0;AL;<font> 0647;;;;N;;;;;
+1EE67;ARABIC MATHEMATICAL STRETCHED HAH;Lo;0;AL;<font> 062D;;;;N;;;;;
+1EE68;ARABIC MATHEMATICAL STRETCHED TAH;Lo;0;AL;<font> 0637;;;;N;;;;;
+1EE69;ARABIC MATHEMATICAL STRETCHED YEH;Lo;0;AL;<font> 064A;;;;N;;;;;
+1EE6A;ARABIC MATHEMATICAL STRETCHED KAF;Lo;0;AL;<font> 0643;;;;N;;;;;
+1EE6C;ARABIC MATHEMATICAL STRETCHED MEEM;Lo;0;AL;<font> 0645;;;;N;;;;;
+1EE6D;ARABIC MATHEMATICAL STRETCHED NOON;Lo;0;AL;<font> 0646;;;;N;;;;;
+1EE6E;ARABIC MATHEMATICAL STRETCHED SEEN;Lo;0;AL;<font> 0633;;;;N;;;;;
+1EE6F;ARABIC MATHEMATICAL STRETCHED AIN;Lo;0;AL;<font> 0639;;;;N;;;;;
+1EE70;ARABIC MATHEMATICAL STRETCHED FEH;Lo;0;AL;<font> 0641;;;;N;;;;;
+1EE71;ARABIC MATHEMATICAL STRETCHED SAD;Lo;0;AL;<font> 0635;;;;N;;;;;
+1EE72;ARABIC MATHEMATICAL STRETCHED QAF;Lo;0;AL;<font> 0642;;;;N;;;;;
+1EE74;ARABIC MATHEMATICAL STRETCHED SHEEN;Lo;0;AL;<font> 0634;;;;N;;;;;
+1EE75;ARABIC MATHEMATICAL STRETCHED TEH;Lo;0;AL;<font> 062A;;;;N;;;;;
+1EE76;ARABIC MATHEMATICAL STRETCHED THEH;Lo;0;AL;<font> 062B;;;;N;;;;;
+1EE77;ARABIC MATHEMATICAL STRETCHED KHAH;Lo;0;AL;<font> 062E;;;;N;;;;;
+1EE79;ARABIC MATHEMATICAL STRETCHED DAD;Lo;0;AL;<font> 0636;;;;N;;;;;
+1EE7A;ARABIC MATHEMATICAL STRETCHED ZAH;Lo;0;AL;<font> 0638;;;;N;;;;;
+1EE7B;ARABIC MATHEMATICAL STRETCHED GHAIN;Lo;0;AL;<font> 063A;;;;N;;;;;
+1EE7C;ARABIC MATHEMATICAL STRETCHED DOTLESS BEH;Lo;0;AL;<font> 066E;;;;N;;;;;
+1EE7E;ARABIC MATHEMATICAL STRETCHED DOTLESS FEH;Lo;0;AL;<font> 06A1;;;;N;;;;;
+1EE80;ARABIC MATHEMATICAL LOOPED ALEF;Lo;0;AL;<font> 0627;;;;N;;;;;
+1EE81;ARABIC MATHEMATICAL LOOPED BEH;Lo;0;AL;<font> 0628;;;;N;;;;;
+1EE82;ARABIC MATHEMATICAL LOOPED JEEM;Lo;0;AL;<font> 062C;;;;N;;;;;
+1EE83;ARABIC MATHEMATICAL LOOPED DAL;Lo;0;AL;<font> 062F;;;;N;;;;;
+1EE84;ARABIC MATHEMATICAL LOOPED HEH;Lo;0;AL;<font> 0647;;;;N;;;;;
+1EE85;ARABIC MATHEMATICAL LOOPED WAW;Lo;0;AL;<font> 0648;;;;N;;;;;
+1EE86;ARABIC MATHEMATICAL LOOPED ZAIN;Lo;0;AL;<font> 0632;;;;N;;;;;
+1EE87;ARABIC MATHEMATICAL LOOPED HAH;Lo;0;AL;<font> 062D;;;;N;;;;;
+1EE88;ARABIC MATHEMATICAL LOOPED TAH;Lo;0;AL;<font> 0637;;;;N;;;;;
+1EE89;ARABIC MATHEMATICAL LOOPED YEH;Lo;0;AL;<font> 064A;;;;N;;;;;
+1EE8B;ARABIC MATHEMATICAL LOOPED LAM;Lo;0;AL;<font> 0644;;;;N;;;;;
+1EE8C;ARABIC MATHEMATICAL LOOPED MEEM;Lo;0;AL;<font> 0645;;;;N;;;;;
+1EE8D;ARABIC MATHEMATICAL LOOPED NOON;Lo;0;AL;<font> 0646;;;;N;;;;;
+1EE8E;ARABIC MATHEMATICAL LOOPED SEEN;Lo;0;AL;<font> 0633;;;;N;;;;;
+1EE8F;ARABIC MATHEMATICAL LOOPED AIN;Lo;0;AL;<font> 0639;;;;N;;;;;
+1EE90;ARABIC MATHEMATICAL LOOPED FEH;Lo;0;AL;<font> 0641;;;;N;;;;;
+1EE91;ARABIC MATHEMATICAL LOOPED SAD;Lo;0;AL;<font> 0635;;;;N;;;;;
+1EE92;ARABIC MATHEMATICAL LOOPED QAF;Lo;0;AL;<font> 0642;;;;N;;;;;
+1EE93;ARABIC MATHEMATICAL LOOPED REH;Lo;0;AL;<font> 0631;;;;N;;;;;
+1EE94;ARABIC MATHEMATICAL LOOPED SHEEN;Lo;0;AL;<font> 0634;;;;N;;;;;
+1EE95;ARABIC MATHEMATICAL LOOPED TEH;Lo;0;AL;<font> 062A;;;;N;;;;;
+1EE96;ARABIC MATHEMATICAL LOOPED THEH;Lo;0;AL;<font> 062B;;;;N;;;;;
+1EE97;ARABIC MATHEMATICAL LOOPED KHAH;Lo;0;AL;<font> 062E;;;;N;;;;;
+1EE98;ARABIC MATHEMATICAL LOOPED THAL;Lo;0;AL;<font> 0630;;;;N;;;;;
+1EE99;ARABIC MATHEMATICAL LOOPED DAD;Lo;0;AL;<font> 0636;;;;N;;;;;
+1EE9A;ARABIC MATHEMATICAL LOOPED ZAH;Lo;0;AL;<font> 0638;;;;N;;;;;
+1EE9B;ARABIC MATHEMATICAL LOOPED GHAIN;Lo;0;AL;<font> 063A;;;;N;;;;;
+1EEA1;ARABIC MATHEMATICAL DOUBLE-STRUCK BEH;Lo;0;AL;<font> 0628;;;;N;;;;;
+1EEA2;ARABIC MATHEMATICAL DOUBLE-STRUCK JEEM;Lo;0;AL;<font> 062C;;;;N;;;;;
+1EEA3;ARABIC MATHEMATICAL DOUBLE-STRUCK DAL;Lo;0;AL;<font> 062F;;;;N;;;;;
+1EEA5;ARABIC MATHEMATICAL DOUBLE-STRUCK WAW;Lo;0;AL;<font> 0648;;;;N;;;;;
+1EEA6;ARABIC MATHEMATICAL DOUBLE-STRUCK ZAIN;Lo;0;AL;<font> 0632;;;;N;;;;;
+1EEA7;ARABIC MATHEMATICAL DOUBLE-STRUCK HAH;Lo;0;AL;<font> 062D;;;;N;;;;;
+1EEA8;ARABIC MATHEMATICAL DOUBLE-STRUCK TAH;Lo;0;AL;<font> 0637;;;;N;;;;;
+1EEA9;ARABIC MATHEMATICAL DOUBLE-STRUCK YEH;Lo;0;AL;<font> 064A;;;;N;;;;;
+1EEAB;ARABIC MATHEMATICAL DOUBLE-STRUCK LAM;Lo;0;AL;<font> 0644;;;;N;;;;;
+1EEAC;ARABIC MATHEMATICAL DOUBLE-STRUCK MEEM;Lo;0;AL;<font> 0645;;;;N;;;;;
+1EEAD;ARABIC MATHEMATICAL DOUBLE-STRUCK NOON;Lo;0;AL;<font> 0646;;;;N;;;;;
+1EEAE;ARABIC MATHEMATICAL DOUBLE-STRUCK SEEN;Lo;0;AL;<font> 0633;;;;N;;;;;
+1EEAF;ARABIC MATHEMATICAL DOUBLE-STRUCK AIN;Lo;0;AL;<font> 0639;;;;N;;;;;
+1EEB0;ARABIC MATHEMATICAL DOUBLE-STRUCK FEH;Lo;0;AL;<font> 0641;;;;N;;;;;
+1EEB1;ARABIC MATHEMATICAL DOUBLE-STRUCK SAD;Lo;0;AL;<font> 0635;;;;N;;;;;
+1EEB2;ARABIC MATHEMATICAL DOUBLE-STRUCK QAF;Lo;0;AL;<font> 0642;;;;N;;;;;
+1EEB3;ARABIC MATHEMATICAL DOUBLE-STRUCK REH;Lo;0;AL;<font> 0631;;;;N;;;;;
+1EEB4;ARABIC MATHEMATICAL DOUBLE-STRUCK SHEEN;Lo;0;AL;<font> 0634;;;;N;;;;;
+1EEB5;ARABIC MATHEMATICAL DOUBLE-STRUCK TEH;Lo;0;AL;<font> 062A;;;;N;;;;;
+1EEB6;ARABIC MATHEMATICAL DOUBLE-STRUCK THEH;Lo;0;AL;<font> 062B;;;;N;;;;;
+1EEB7;ARABIC MATHEMATICAL DOUBLE-STRUCK KHAH;Lo;0;AL;<font> 062E;;;;N;;;;;
+1EEB8;ARABIC MATHEMATICAL DOUBLE-STRUCK THAL;Lo;0;AL;<font> 0630;;;;N;;;;;
+1EEB9;ARABIC MATHEMATICAL DOUBLE-STRUCK DAD;Lo;0;AL;<font> 0636;;;;N;;;;;
+1EEBA;ARABIC MATHEMATICAL DOUBLE-STRUCK ZAH;Lo;0;AL;<font> 0638;;;;N;;;;;
+1EEBB;ARABIC MATHEMATICAL DOUBLE-STRUCK GHAIN;Lo;0;AL;<font> 063A;;;;N;;;;;
+1EEF0;ARABIC MATHEMATICAL OPERATOR MEEM WITH HAH WITH TATWEEL;Sm;0;ON;;;;;N;;;;;
+1EEF1;ARABIC MATHEMATICAL OPERATOR HAH WITH DAL;Sm;0;ON;;;;;N;;;;;
+1F000;MAHJONG TILE EAST WIND;So;0;ON;;;;;N;;;;;
+1F001;MAHJONG TILE SOUTH WIND;So;0;ON;;;;;N;;;;;
+1F002;MAHJONG TILE WEST WIND;So;0;ON;;;;;N;;;;;
+1F003;MAHJONG TILE NORTH WIND;So;0;ON;;;;;N;;;;;
+1F004;MAHJONG TILE RED DRAGON;So;0;ON;;;;;N;;;;;
+1F005;MAHJONG TILE GREEN DRAGON;So;0;ON;;;;;N;;;;;
+1F006;MAHJONG TILE WHITE DRAGON;So;0;ON;;;;;N;;;;;
+1F007;MAHJONG TILE ONE OF CHARACTERS;So;0;ON;;;;;N;;;;;
+1F008;MAHJONG TILE TWO OF CHARACTERS;So;0;ON;;;;;N;;;;;
+1F009;MAHJONG TILE THREE OF CHARACTERS;So;0;ON;;;;;N;;;;;
+1F00A;MAHJONG TILE FOUR OF CHARACTERS;So;0;ON;;;;;N;;;;;
+1F00B;MAHJONG TILE FIVE OF CHARACTERS;So;0;ON;;;;;N;;;;;
+1F00C;MAHJONG TILE SIX OF CHARACTERS;So;0;ON;;;;;N;;;;;
+1F00D;MAHJONG TILE SEVEN OF CHARACTERS;So;0;ON;;;;;N;;;;;
+1F00E;MAHJONG TILE EIGHT OF CHARACTERS;So;0;ON;;;;;N;;;;;
+1F00F;MAHJONG TILE NINE OF CHARACTERS;So;0;ON;;;;;N;;;;;
+1F010;MAHJONG TILE ONE OF BAMBOOS;So;0;ON;;;;;N;;;;;
+1F011;MAHJONG TILE TWO OF BAMBOOS;So;0;ON;;;;;N;;;;;
+1F012;MAHJONG TILE THREE OF BAMBOOS;So;0;ON;;;;;N;;;;;
+1F013;MAHJONG TILE FOUR OF BAMBOOS;So;0;ON;;;;;N;;;;;
+1F014;MAHJONG TILE FIVE OF BAMBOOS;So;0;ON;;;;;N;;;;;
+1F015;MAHJONG TILE SIX OF BAMBOOS;So;0;ON;;;;;N;;;;;
+1F016;MAHJONG TILE SEVEN OF BAMBOOS;So;0;ON;;;;;N;;;;;
+1F017;MAHJONG TILE EIGHT OF BAMBOOS;So;0;ON;;;;;N;;;;;
+1F018;MAHJONG TILE NINE OF BAMBOOS;So;0;ON;;;;;N;;;;;
+1F019;MAHJONG TILE ONE OF CIRCLES;So;0;ON;;;;;N;;;;;
+1F01A;MAHJONG TILE TWO OF CIRCLES;So;0;ON;;;;;N;;;;;
+1F01B;MAHJONG TILE THREE OF CIRCLES;So;0;ON;;;;;N;;;;;
+1F01C;MAHJONG TILE FOUR OF CIRCLES;So;0;ON;;;;;N;;;;;
+1F01D;MAHJONG TILE FIVE OF CIRCLES;So;0;ON;;;;;N;;;;;
+1F01E;MAHJONG TILE SIX OF CIRCLES;So;0;ON;;;;;N;;;;;
+1F01F;MAHJONG TILE SEVEN OF CIRCLES;So;0;ON;;;;;N;;;;;
+1F020;MAHJONG TILE EIGHT OF CIRCLES;So;0;ON;;;;;N;;;;;
+1F021;MAHJONG TILE NINE OF CIRCLES;So;0;ON;;;;;N;;;;;
+1F022;MAHJONG TILE PLUM;So;0;ON;;;;;N;;;;;
+1F023;MAHJONG TILE ORCHID;So;0;ON;;;;;N;;;;;
+1F024;MAHJONG TILE BAMBOO;So;0;ON;;;;;N;;;;;
+1F025;MAHJONG TILE CHRYSANTHEMUM;So;0;ON;;;;;N;;;;;
+1F026;MAHJONG TILE SPRING;So;0;ON;;;;;N;;;;;
+1F027;MAHJONG TILE SUMMER;So;0;ON;;;;;N;;;;;
+1F028;MAHJONG TILE AUTUMN;So;0;ON;;;;;N;;;;;
+1F029;MAHJONG TILE WINTER;So;0;ON;;;;;N;;;;;
+1F02A;MAHJONG TILE JOKER;So;0;ON;;;;;N;;;;;
+1F02B;MAHJONG TILE BACK;So;0;ON;;;;;N;;;;;
+1F030;DOMINO TILE HORIZONTAL BACK;So;0;ON;;;;;N;;;;;
+1F031;DOMINO TILE HORIZONTAL-00-00;So;0;ON;;;;;N;;;;;
+1F032;DOMINO TILE HORIZONTAL-00-01;So;0;ON;;;;;N;;;;;
+1F033;DOMINO TILE HORIZONTAL-00-02;So;0;ON;;;;;N;;;;;
+1F034;DOMINO TILE HORIZONTAL-00-03;So;0;ON;;;;;N;;;;;
+1F035;DOMINO TILE HORIZONTAL-00-04;So;0;ON;;;;;N;;;;;
+1F036;DOMINO TILE HORIZONTAL-00-05;So;0;ON;;;;;N;;;;;
+1F037;DOMINO TILE HORIZONTAL-00-06;So;0;ON;;;;;N;;;;;
+1F038;DOMINO TILE HORIZONTAL-01-00;So;0;ON;;;;;N;;;;;
+1F039;DOMINO TILE HORIZONTAL-01-01;So;0;ON;;;;;N;;;;;
+1F03A;DOMINO TILE HORIZONTAL-01-02;So;0;ON;;;;;N;;;;;
+1F03B;DOMINO TILE HORIZONTAL-01-03;So;0;ON;;;;;N;;;;;
+1F03C;DOMINO TILE HORIZONTAL-01-04;So;0;ON;;;;;N;;;;;
+1F03D;DOMINO TILE HORIZONTAL-01-05;So;0;ON;;;;;N;;;;;
+1F03E;DOMINO TILE HORIZONTAL-01-06;So;0;ON;;;;;N;;;;;
+1F03F;DOMINO TILE HORIZONTAL-02-00;So;0;ON;;;;;N;;;;;
+1F040;DOMINO TILE HORIZONTAL-02-01;So;0;ON;;;;;N;;;;;
+1F041;DOMINO TILE HORIZONTAL-02-02;So;0;ON;;;;;N;;;;;
+1F042;DOMINO TILE HORIZONTAL-02-03;So;0;ON;;;;;N;;;;;
+1F043;DOMINO TILE HORIZONTAL-02-04;So;0;ON;;;;;N;;;;;
+1F044;DOMINO TILE HORIZONTAL-02-05;So;0;ON;;;;;N;;;;;
+1F045;DOMINO TILE HORIZONTAL-02-06;So;0;ON;;;;;N;;;;;
+1F046;DOMINO TILE HORIZONTAL-03-00;So;0;ON;;;;;N;;;;;
+1F047;DOMINO TILE HORIZONTAL-03-01;So;0;ON;;;;;N;;;;;
+1F048;DOMINO TILE HORIZONTAL-03-02;So;0;ON;;;;;N;;;;;
+1F049;DOMINO TILE HORIZONTAL-03-03;So;0;ON;;;;;N;;;;;
+1F04A;DOMINO TILE HORIZONTAL-03-04;So;0;ON;;;;;N;;;;;
+1F04B;DOMINO TILE HORIZONTAL-03-05;So;0;ON;;;;;N;;;;;
+1F04C;DOMINO TILE HORIZONTAL-03-06;So;0;ON;;;;;N;;;;;
+1F04D;DOMINO TILE HORIZONTAL-04-00;So;0;ON;;;;;N;;;;;
+1F04E;DOMINO TILE HORIZONTAL-04-01;So;0;ON;;;;;N;;;;;
+1F04F;DOMINO TILE HORIZONTAL-04-02;So;0;ON;;;;;N;;;;;
+1F050;DOMINO TILE HORIZONTAL-04-03;So;0;ON;;;;;N;;;;;
+1F051;DOMINO TILE HORIZONTAL-04-04;So;0;ON;;;;;N;;;;;
+1F052;DOMINO TILE HORIZONTAL-04-05;So;0;ON;;;;;N;;;;;
+1F053;DOMINO TILE HORIZONTAL-04-06;So;0;ON;;;;;N;;;;;
+1F054;DOMINO TILE HORIZONTAL-05-00;So;0;ON;;;;;N;;;;;
+1F055;DOMINO TILE HORIZONTAL-05-01;So;0;ON;;;;;N;;;;;
+1F056;DOMINO TILE HORIZONTAL-05-02;So;0;ON;;;;;N;;;;;
+1F057;DOMINO TILE HORIZONTAL-05-03;So;0;ON;;;;;N;;;;;
+1F058;DOMINO TILE HORIZONTAL-05-04;So;0;ON;;;;;N;;;;;
+1F059;DOMINO TILE HORIZONTAL-05-05;So;0;ON;;;;;N;;;;;
+1F05A;DOMINO TILE HORIZONTAL-05-06;So;0;ON;;;;;N;;;;;
+1F05B;DOMINO TILE HORIZONTAL-06-00;So;0;ON;;;;;N;;;;;
+1F05C;DOMINO TILE HORIZONTAL-06-01;So;0;ON;;;;;N;;;;;
+1F05D;DOMINO TILE HORIZONTAL-06-02;So;0;ON;;;;;N;;;;;
+1F05E;DOMINO TILE HORIZONTAL-06-03;So;0;ON;;;;;N;;;;;
+1F05F;DOMINO TILE HORIZONTAL-06-04;So;0;ON;;;;;N;;;;;
+1F060;DOMINO TILE HORIZONTAL-06-05;So;0;ON;;;;;N;;;;;
+1F061;DOMINO TILE HORIZONTAL-06-06;So;0;ON;;;;;N;;;;;
+1F062;DOMINO TILE VERTICAL BACK;So;0;ON;;;;;N;;;;;
+1F063;DOMINO TILE VERTICAL-00-00;So;0;ON;;;;;N;;;;;
+1F064;DOMINO TILE VERTICAL-00-01;So;0;ON;;;;;N;;;;;
+1F065;DOMINO TILE VERTICAL-00-02;So;0;ON;;;;;N;;;;;
+1F066;DOMINO TILE VERTICAL-00-03;So;0;ON;;;;;N;;;;;
+1F067;DOMINO TILE VERTICAL-00-04;So;0;ON;;;;;N;;;;;
+1F068;DOMINO TILE VERTICAL-00-05;So;0;ON;;;;;N;;;;;
+1F069;DOMINO TILE VERTICAL-00-06;So;0;ON;;;;;N;;;;;
+1F06A;DOMINO TILE VERTICAL-01-00;So;0;ON;;;;;N;;;;;
+1F06B;DOMINO TILE VERTICAL-01-01;So;0;ON;;;;;N;;;;;
+1F06C;DOMINO TILE VERTICAL-01-02;So;0;ON;;;;;N;;;;;
+1F06D;DOMINO TILE VERTICAL-01-03;So;0;ON;;;;;N;;;;;
+1F06E;DOMINO TILE VERTICAL-01-04;So;0;ON;;;;;N;;;;;
+1F06F;DOMINO TILE VERTICAL-01-05;So;0;ON;;;;;N;;;;;
+1F070;DOMINO TILE VERTICAL-01-06;So;0;ON;;;;;N;;;;;
+1F071;DOMINO TILE VERTICAL-02-00;So;0;ON;;;;;N;;;;;
+1F072;DOMINO TILE VERTICAL-02-01;So;0;ON;;;;;N;;;;;
+1F073;DOMINO TILE VERTICAL-02-02;So;0;ON;;;;;N;;;;;
+1F074;DOMINO TILE VERTICAL-02-03;So;0;ON;;;;;N;;;;;
+1F075;DOMINO TILE VERTICAL-02-04;So;0;ON;;;;;N;;;;;
+1F076;DOMINO TILE VERTICAL-02-05;So;0;ON;;;;;N;;;;;
+1F077;DOMINO TILE VERTICAL-02-06;So;0;ON;;;;;N;;;;;
+1F078;DOMINO TILE VERTICAL-03-00;So;0;ON;;;;;N;;;;;
+1F079;DOMINO TILE VERTICAL-03-01;So;0;ON;;;;;N;;;;;
+1F07A;DOMINO TILE VERTICAL-03-02;So;0;ON;;;;;N;;;;;
+1F07B;DOMINO TILE VERTICAL-03-03;So;0;ON;;;;;N;;;;;
+1F07C;DOMINO TILE VERTICAL-03-04;So;0;ON;;;;;N;;;;;
+1F07D;DOMINO TILE VERTICAL-03-05;So;0;ON;;;;;N;;;;;
+1F07E;DOMINO TILE VERTICAL-03-06;So;0;ON;;;;;N;;;;;
+1F07F;DOMINO TILE VERTICAL-04-00;So;0;ON;;;;;N;;;;;
+1F080;DOMINO TILE VERTICAL-04-01;So;0;ON;;;;;N;;;;;
+1F081;DOMINO TILE VERTICAL-04-02;So;0;ON;;;;;N;;;;;
+1F082;DOMINO TILE VERTICAL-04-03;So;0;ON;;;;;N;;;;;
+1F083;DOMINO TILE VERTICAL-04-04;So;0;ON;;;;;N;;;;;
+1F084;DOMINO TILE VERTICAL-04-05;So;0;ON;;;;;N;;;;;
+1F085;DOMINO TILE VERTICAL-04-06;So;0;ON;;;;;N;;;;;
+1F086;DOMINO TILE VERTICAL-05-00;So;0;ON;;;;;N;;;;;
+1F087;DOMINO TILE VERTICAL-05-01;So;0;ON;;;;;N;;;;;
+1F088;DOMINO TILE VERTICAL-05-02;So;0;ON;;;;;N;;;;;
+1F089;DOMINO TILE VERTICAL-05-03;So;0;ON;;;;;N;;;;;
+1F08A;DOMINO TILE VERTICAL-05-04;So;0;ON;;;;;N;;;;;
+1F08B;DOMINO TILE VERTICAL-05-05;So;0;ON;;;;;N;;;;;
+1F08C;DOMINO TILE VERTICAL-05-06;So;0;ON;;;;;N;;;;;
+1F08D;DOMINO TILE VERTICAL-06-00;So;0;ON;;;;;N;;;;;
+1F08E;DOMINO TILE VERTICAL-06-01;So;0;ON;;;;;N;;;;;
+1F08F;DOMINO TILE VERTICAL-06-02;So;0;ON;;;;;N;;;;;
+1F090;DOMINO TILE VERTICAL-06-03;So;0;ON;;;;;N;;;;;
+1F091;DOMINO TILE VERTICAL-06-04;So;0;ON;;;;;N;;;;;
+1F092;DOMINO TILE VERTICAL-06-05;So;0;ON;;;;;N;;;;;
+1F093;DOMINO TILE VERTICAL-06-06;So;0;ON;;;;;N;;;;;
+1F0A0;PLAYING CARD BACK;So;0;ON;;;;;N;;;;;
+1F0A1;PLAYING CARD ACE OF SPADES;So;0;ON;;;;;N;;;;;
+1F0A2;PLAYING CARD TWO OF SPADES;So;0;ON;;;;;N;;;;;
+1F0A3;PLAYING CARD THREE OF SPADES;So;0;ON;;;;;N;;;;;
+1F0A4;PLAYING CARD FOUR OF SPADES;So;0;ON;;;;;N;;;;;
+1F0A5;PLAYING CARD FIVE OF SPADES;So;0;ON;;;;;N;;;;;
+1F0A6;PLAYING CARD SIX OF SPADES;So;0;ON;;;;;N;;;;;
+1F0A7;PLAYING CARD SEVEN OF SPADES;So;0;ON;;;;;N;;;;;
+1F0A8;PLAYING CARD EIGHT OF SPADES;So;0;ON;;;;;N;;;;;
+1F0A9;PLAYING CARD NINE OF SPADES;So;0;ON;;;;;N;;;;;
+1F0AA;PLAYING CARD TEN OF SPADES;So;0;ON;;;;;N;;;;;
+1F0AB;PLAYING CARD JACK OF SPADES;So;0;ON;;;;;N;;;;;
+1F0AC;PLAYING CARD KNIGHT OF SPADES;So;0;ON;;;;;N;;;;;
+1F0AD;PLAYING CARD QUEEN OF SPADES;So;0;ON;;;;;N;;;;;
+1F0AE;PLAYING CARD KING OF SPADES;So;0;ON;;;;;N;;;;;
+1F0B1;PLAYING CARD ACE OF HEARTS;So;0;ON;;;;;N;;;;;
+1F0B2;PLAYING CARD TWO OF HEARTS;So;0;ON;;;;;N;;;;;
+1F0B3;PLAYING CARD THREE OF HEARTS;So;0;ON;;;;;N;;;;;
+1F0B4;PLAYING CARD FOUR OF HEARTS;So;0;ON;;;;;N;;;;;
+1F0B5;PLAYING CARD FIVE OF HEARTS;So;0;ON;;;;;N;;;;;
+1F0B6;PLAYING CARD SIX OF HEARTS;So;0;ON;;;;;N;;;;;
+1F0B7;PLAYING CARD SEVEN OF HEARTS;So;0;ON;;;;;N;;;;;
+1F0B8;PLAYING CARD EIGHT OF HEARTS;So;0;ON;;;;;N;;;;;
+1F0B9;PLAYING CARD NINE OF HEARTS;So;0;ON;;;;;N;;;;;
+1F0BA;PLAYING CARD TEN OF HEARTS;So;0;ON;;;;;N;;;;;
+1F0BB;PLAYING CARD JACK OF HEARTS;So;0;ON;;;;;N;;;;;
+1F0BC;PLAYING CARD KNIGHT OF HEARTS;So;0;ON;;;;;N;;;;;
+1F0BD;PLAYING CARD QUEEN OF HEARTS;So;0;ON;;;;;N;;;;;
+1F0BE;PLAYING CARD KING OF HEARTS;So;0;ON;;;;;N;;;;;
+1F0BF;PLAYING CARD RED JOKER;So;0;ON;;;;;N;;;;;
+1F0C1;PLAYING CARD ACE OF DIAMONDS;So;0;ON;;;;;N;;;;;
+1F0C2;PLAYING CARD TWO OF DIAMONDS;So;0;ON;;;;;N;;;;;
+1F0C3;PLAYING CARD THREE OF DIAMONDS;So;0;ON;;;;;N;;;;;
+1F0C4;PLAYING CARD FOUR OF DIAMONDS;So;0;ON;;;;;N;;;;;
+1F0C5;PLAYING CARD FIVE OF DIAMONDS;So;0;ON;;;;;N;;;;;
+1F0C6;PLAYING CARD SIX OF DIAMONDS;So;0;ON;;;;;N;;;;;
+1F0C7;PLAYING CARD SEVEN OF DIAMONDS;So;0;ON;;;;;N;;;;;
+1F0C8;PLAYING CARD EIGHT OF DIAMONDS;So;0;ON;;;;;N;;;;;
+1F0C9;PLAYING CARD NINE OF DIAMONDS;So;0;ON;;;;;N;;;;;
+1F0CA;PLAYING CARD TEN OF DIAMONDS;So;0;ON;;;;;N;;;;;
+1F0CB;PLAYING CARD JACK OF DIAMONDS;So;0;ON;;;;;N;;;;;
+1F0CC;PLAYING CARD KNIGHT OF DIAMONDS;So;0;ON;;;;;N;;;;;
+1F0CD;PLAYING CARD QUEEN OF DIAMONDS;So;0;ON;;;;;N;;;;;
+1F0CE;PLAYING CARD KING OF DIAMONDS;So;0;ON;;;;;N;;;;;
+1F0CF;PLAYING CARD BLACK JOKER;So;0;ON;;;;;N;;;;;
+1F0D1;PLAYING CARD ACE OF CLUBS;So;0;ON;;;;;N;;;;;
+1F0D2;PLAYING CARD TWO OF CLUBS;So;0;ON;;;;;N;;;;;
+1F0D3;PLAYING CARD THREE OF CLUBS;So;0;ON;;;;;N;;;;;
+1F0D4;PLAYING CARD FOUR OF CLUBS;So;0;ON;;;;;N;;;;;
+1F0D5;PLAYING CARD FIVE OF CLUBS;So;0;ON;;;;;N;;;;;
+1F0D6;PLAYING CARD SIX OF CLUBS;So;0;ON;;;;;N;;;;;
+1F0D7;PLAYING CARD SEVEN OF CLUBS;So;0;ON;;;;;N;;;;;
+1F0D8;PLAYING CARD EIGHT OF CLUBS;So;0;ON;;;;;N;;;;;
+1F0D9;PLAYING CARD NINE OF CLUBS;So;0;ON;;;;;N;;;;;
+1F0DA;PLAYING CARD TEN OF CLUBS;So;0;ON;;;;;N;;;;;
+1F0DB;PLAYING CARD JACK OF CLUBS;So;0;ON;;;;;N;;;;;
+1F0DC;PLAYING CARD KNIGHT OF CLUBS;So;0;ON;;;;;N;;;;;
+1F0DD;PLAYING CARD QUEEN OF CLUBS;So;0;ON;;;;;N;;;;;
+1F0DE;PLAYING CARD KING OF CLUBS;So;0;ON;;;;;N;;;;;
+1F0DF;PLAYING CARD WHITE JOKER;So;0;ON;;;;;N;;;;;
+1F0E0;PLAYING CARD FOOL;So;0;ON;;;;;N;;;;;
+1F0E1;PLAYING CARD TRUMP-1;So;0;ON;;;;;N;;;;;
+1F0E2;PLAYING CARD TRUMP-2;So;0;ON;;;;;N;;;;;
+1F0E3;PLAYING CARD TRUMP-3;So;0;ON;;;;;N;;;;;
+1F0E4;PLAYING CARD TRUMP-4;So;0;ON;;;;;N;;;;;
+1F0E5;PLAYING CARD TRUMP-5;So;0;ON;;;;;N;;;;;
+1F0E6;PLAYING CARD TRUMP-6;So;0;ON;;;;;N;;;;;
+1F0E7;PLAYING CARD TRUMP-7;So;0;ON;;;;;N;;;;;
+1F0E8;PLAYING CARD TRUMP-8;So;0;ON;;;;;N;;;;;
+1F0E9;PLAYING CARD TRUMP-9;So;0;ON;;;;;N;;;;;
+1F0EA;PLAYING CARD TRUMP-10;So;0;ON;;;;;N;;;;;
+1F0EB;PLAYING CARD TRUMP-11;So;0;ON;;;;;N;;;;;
+1F0EC;PLAYING CARD TRUMP-12;So;0;ON;;;;;N;;;;;
+1F0ED;PLAYING CARD TRUMP-13;So;0;ON;;;;;N;;;;;
+1F0EE;PLAYING CARD TRUMP-14;So;0;ON;;;;;N;;;;;
+1F0EF;PLAYING CARD TRUMP-15;So;0;ON;;;;;N;;;;;
+1F0F0;PLAYING CARD TRUMP-16;So;0;ON;;;;;N;;;;;
+1F0F1;PLAYING CARD TRUMP-17;So;0;ON;;;;;N;;;;;
+1F0F2;PLAYING CARD TRUMP-18;So;0;ON;;;;;N;;;;;
+1F0F3;PLAYING CARD TRUMP-19;So;0;ON;;;;;N;;;;;
+1F0F4;PLAYING CARD TRUMP-20;So;0;ON;;;;;N;;;;;
+1F0F5;PLAYING CARD TRUMP-21;So;0;ON;;;;;N;;;;;
+1F100;DIGIT ZERO FULL STOP;No;0;EN;<compat> 0030 002E;;0;0;N;;;;;
+1F101;DIGIT ZERO COMMA;No;0;EN;<compat> 0030 002C;;0;0;N;;;;;
+1F102;DIGIT ONE COMMA;No;0;EN;<compat> 0031 002C;;1;1;N;;;;;
+1F103;DIGIT TWO COMMA;No;0;EN;<compat> 0032 002C;;2;2;N;;;;;
+1F104;DIGIT THREE COMMA;No;0;EN;<compat> 0033 002C;;3;3;N;;;;;
+1F105;DIGIT FOUR COMMA;No;0;EN;<compat> 0034 002C;;4;4;N;;;;;
+1F106;DIGIT FIVE COMMA;No;0;EN;<compat> 0035 002C;;5;5;N;;;;;
+1F107;DIGIT SIX COMMA;No;0;EN;<compat> 0036 002C;;6;6;N;;;;;
+1F108;DIGIT SEVEN COMMA;No;0;EN;<compat> 0037 002C;;7;7;N;;;;;
+1F109;DIGIT EIGHT COMMA;No;0;EN;<compat> 0038 002C;;8;8;N;;;;;
+1F10A;DIGIT NINE COMMA;No;0;EN;<compat> 0039 002C;;9;9;N;;;;;
+1F10B;DINGBAT CIRCLED SANS-SERIF DIGIT ZERO;No;0;ON;;;;0;N;;;;;
+1F10C;DINGBAT NEGATIVE CIRCLED SANS-SERIF DIGIT ZERO;No;0;ON;;;;0;N;;;;;
+1F110;PARENTHESIZED LATIN CAPITAL LETTER A;So;0;L;<compat> 0028 0041 0029;;;;N;;;;;
+1F111;PARENTHESIZED LATIN CAPITAL LETTER B;So;0;L;<compat> 0028 0042 0029;;;;N;;;;;
+1F112;PARENTHESIZED LATIN CAPITAL LETTER C;So;0;L;<compat> 0028 0043 0029;;;;N;;;;;
+1F113;PARENTHESIZED LATIN CAPITAL LETTER D;So;0;L;<compat> 0028 0044 0029;;;;N;;;;;
+1F114;PARENTHESIZED LATIN CAPITAL LETTER E;So;0;L;<compat> 0028 0045 0029;;;;N;;;;;
+1F115;PARENTHESIZED LATIN CAPITAL LETTER F;So;0;L;<compat> 0028 0046 0029;;;;N;;;;;
+1F116;PARENTHESIZED LATIN CAPITAL LETTER G;So;0;L;<compat> 0028 0047 0029;;;;N;;;;;
+1F117;PARENTHESIZED LATIN CAPITAL LETTER H;So;0;L;<compat> 0028 0048 0029;;;;N;;;;;
+1F118;PARENTHESIZED LATIN CAPITAL LETTER I;So;0;L;<compat> 0028 0049 0029;;;;N;;;;;
+1F119;PARENTHESIZED LATIN CAPITAL LETTER J;So;0;L;<compat> 0028 004A 0029;;;;N;;;;;
+1F11A;PARENTHESIZED LATIN CAPITAL LETTER K;So;0;L;<compat> 0028 004B 0029;;;;N;;;;;
+1F11B;PARENTHESIZED LATIN CAPITAL LETTER L;So;0;L;<compat> 0028 004C 0029;;;;N;;;;;
+1F11C;PARENTHESIZED LATIN CAPITAL LETTER M;So;0;L;<compat> 0028 004D 0029;;;;N;;;;;
+1F11D;PARENTHESIZED LATIN CAPITAL LETTER N;So;0;L;<compat> 0028 004E 0029;;;;N;;;;;
+1F11E;PARENTHESIZED LATIN CAPITAL LETTER O;So;0;L;<compat> 0028 004F 0029;;;;N;;;;;
+1F11F;PARENTHESIZED LATIN CAPITAL LETTER P;So;0;L;<compat> 0028 0050 0029;;;;N;;;;;
+1F120;PARENTHESIZED LATIN CAPITAL LETTER Q;So;0;L;<compat> 0028 0051 0029;;;;N;;;;;
+1F121;PARENTHESIZED LATIN CAPITAL LETTER R;So;0;L;<compat> 0028 0052 0029;;;;N;;;;;
+1F122;PARENTHESIZED LATIN CAPITAL LETTER S;So;0;L;<compat> 0028 0053 0029;;;;N;;;;;
+1F123;PARENTHESIZED LATIN CAPITAL LETTER T;So;0;L;<compat> 0028 0054 0029;;;;N;;;;;
+1F124;PARENTHESIZED LATIN CAPITAL LETTER U;So;0;L;<compat> 0028 0055 0029;;;;N;;;;;
+1F125;PARENTHESIZED LATIN CAPITAL LETTER V;So;0;L;<compat> 0028 0056 0029;;;;N;;;;;
+1F126;PARENTHESIZED LATIN CAPITAL LETTER W;So;0;L;<compat> 0028 0057 0029;;;;N;;;;;
+1F127;PARENTHESIZED LATIN CAPITAL LETTER X;So;0;L;<compat> 0028 0058 0029;;;;N;;;;;
+1F128;PARENTHESIZED LATIN CAPITAL LETTER Y;So;0;L;<compat> 0028 0059 0029;;;;N;;;;;
+1F129;PARENTHESIZED LATIN CAPITAL LETTER Z;So;0;L;<compat> 0028 005A 0029;;;;N;;;;;
+1F12A;TORTOISE SHELL BRACKETED LATIN CAPITAL LETTER S;So;0;L;<compat> 3014 0053 3015;;;;N;;;;;
+1F12B;CIRCLED ITALIC LATIN CAPITAL LETTER C;So;0;L;<circle> 0043;;;;N;;;;;
+1F12C;CIRCLED ITALIC LATIN CAPITAL LETTER R;So;0;L;<circle> 0052;;;;N;;;;;
+1F12D;CIRCLED CD;So;0;L;<circle> 0043 0044;;;;N;;;;;
+1F12E;CIRCLED WZ;So;0;L;<circle> 0057 005A;;;;N;;;;;
+1F130;SQUARED LATIN CAPITAL LETTER A;So;0;L;<square> 0041;;;;N;;;;;
+1F131;SQUARED LATIN CAPITAL LETTER B;So;0;L;<square> 0042;;;;N;;;;;
+1F132;SQUARED LATIN CAPITAL LETTER C;So;0;L;<square> 0043;;;;N;;;;;
+1F133;SQUARED LATIN CAPITAL LETTER D;So;0;L;<square> 0044;;;;N;;;;;
+1F134;SQUARED LATIN CAPITAL LETTER E;So;0;L;<square> 0045;;;;N;;;;;
+1F135;SQUARED LATIN CAPITAL LETTER F;So;0;L;<square> 0046;;;;N;;;;;
+1F136;SQUARED LATIN CAPITAL LETTER G;So;0;L;<square> 0047;;;;N;;;;;
+1F137;SQUARED LATIN CAPITAL LETTER H;So;0;L;<square> 0048;;;;N;;;;;
+1F138;SQUARED LATIN CAPITAL LETTER I;So;0;L;<square> 0049;;;;N;;;;;
+1F139;SQUARED LATIN CAPITAL LETTER J;So;0;L;<square> 004A;;;;N;;;;;
+1F13A;SQUARED LATIN CAPITAL LETTER K;So;0;L;<square> 004B;;;;N;;;;;
+1F13B;SQUARED LATIN CAPITAL LETTER L;So;0;L;<square> 004C;;;;N;;;;;
+1F13C;SQUARED LATIN CAPITAL LETTER M;So;0;L;<square> 004D;;;;N;;;;;
+1F13D;SQUARED LATIN CAPITAL LETTER N;So;0;L;<square> 004E;;;;N;;;;;
+1F13E;SQUARED LATIN CAPITAL LETTER O;So;0;L;<square> 004F;;;;N;;;;;
+1F13F;SQUARED LATIN CAPITAL LETTER P;So;0;L;<square> 0050;;;;N;;;;;
+1F140;SQUARED LATIN CAPITAL LETTER Q;So;0;L;<square> 0051;;;;N;;;;;
+1F141;SQUARED LATIN CAPITAL LETTER R;So;0;L;<square> 0052;;;;N;;;;;
+1F142;SQUARED LATIN CAPITAL LETTER S;So;0;L;<square> 0053;;;;N;;;;;
+1F143;SQUARED LATIN CAPITAL LETTER T;So;0;L;<square> 0054;;;;N;;;;;
+1F144;SQUARED LATIN CAPITAL LETTER U;So;0;L;<square> 0055;;;;N;;;;;
+1F145;SQUARED LATIN CAPITAL LETTER V;So;0;L;<square> 0056;;;;N;;;;;
+1F146;SQUARED LATIN CAPITAL LETTER W;So;0;L;<square> 0057;;;;N;;;;;
+1F147;SQUARED LATIN CAPITAL LETTER X;So;0;L;<square> 0058;;;;N;;;;;
+1F148;SQUARED LATIN CAPITAL LETTER Y;So;0;L;<square> 0059;;;;N;;;;;
+1F149;SQUARED LATIN CAPITAL LETTER Z;So;0;L;<square> 005A;;;;N;;;;;
+1F14A;SQUARED HV;So;0;L;<square> 0048 0056;;;;N;;;;;
+1F14B;SQUARED MV;So;0;L;<square> 004D 0056;;;;N;;;;;
+1F14C;SQUARED SD;So;0;L;<square> 0053 0044;;;;N;;;;;
+1F14D;SQUARED SS;So;0;L;<square> 0053 0053;;;;N;;;;;
+1F14E;SQUARED PPV;So;0;L;<square> 0050 0050 0056;;;;N;;;;;
+1F14F;SQUARED WC;So;0;L;<square> 0057 0043;;;;N;;;;;
+1F150;NEGATIVE CIRCLED LATIN CAPITAL LETTER A;So;0;L;;;;;N;;;;;
+1F151;NEGATIVE CIRCLED LATIN CAPITAL LETTER B;So;0;L;;;;;N;;;;;
+1F152;NEGATIVE CIRCLED LATIN CAPITAL LETTER C;So;0;L;;;;;N;;;;;
+1F153;NEGATIVE CIRCLED LATIN CAPITAL LETTER D;So;0;L;;;;;N;;;;;
+1F154;NEGATIVE CIRCLED LATIN CAPITAL LETTER E;So;0;L;;;;;N;;;;;
+1F155;NEGATIVE CIRCLED LATIN CAPITAL LETTER F;So;0;L;;;;;N;;;;;
+1F156;NEGATIVE CIRCLED LATIN CAPITAL LETTER G;So;0;L;;;;;N;;;;;
+1F157;NEGATIVE CIRCLED LATIN CAPITAL LETTER H;So;0;L;;;;;N;;;;;
+1F158;NEGATIVE CIRCLED LATIN CAPITAL LETTER I;So;0;L;;;;;N;;;;;
+1F159;NEGATIVE CIRCLED LATIN CAPITAL LETTER J;So;0;L;;;;;N;;;;;
+1F15A;NEGATIVE CIRCLED LATIN CAPITAL LETTER K;So;0;L;;;;;N;;;;;
+1F15B;NEGATIVE CIRCLED LATIN CAPITAL LETTER L;So;0;L;;;;;N;;;;;
+1F15C;NEGATIVE CIRCLED LATIN CAPITAL LETTER M;So;0;L;;;;;N;;;;;
+1F15D;NEGATIVE CIRCLED LATIN CAPITAL LETTER N;So;0;L;;;;;N;;;;;
+1F15E;NEGATIVE CIRCLED LATIN CAPITAL LETTER O;So;0;L;;;;;N;;;;;
+1F15F;NEGATIVE CIRCLED LATIN CAPITAL LETTER P;So;0;L;;;;;N;;;;;
+1F160;NEGATIVE CIRCLED LATIN CAPITAL LETTER Q;So;0;L;;;;;N;;;;;
+1F161;NEGATIVE CIRCLED LATIN CAPITAL LETTER R;So;0;L;;;;;N;;;;;
+1F162;NEGATIVE CIRCLED LATIN CAPITAL LETTER S;So;0;L;;;;;N;;;;;
+1F163;NEGATIVE CIRCLED LATIN CAPITAL LETTER T;So;0;L;;;;;N;;;;;
+1F164;NEGATIVE CIRCLED LATIN CAPITAL LETTER U;So;0;L;;;;;N;;;;;
+1F165;NEGATIVE CIRCLED LATIN CAPITAL LETTER V;So;0;L;;;;;N;;;;;
+1F166;NEGATIVE CIRCLED LATIN CAPITAL LETTER W;So;0;L;;;;;N;;;;;
+1F167;NEGATIVE CIRCLED LATIN CAPITAL LETTER X;So;0;L;;;;;N;;;;;
+1F168;NEGATIVE CIRCLED LATIN CAPITAL LETTER Y;So;0;L;;;;;N;;;;;
+1F169;NEGATIVE CIRCLED LATIN CAPITAL LETTER Z;So;0;L;;;;;N;;;;;
+1F16A;RAISED MC SIGN;So;0;ON;<super> 004D 0043;;;;N;;;;;
+1F16B;RAISED MD SIGN;So;0;ON;<super> 004D 0044;;;;N;;;;;
+1F170;NEGATIVE SQUARED LATIN CAPITAL LETTER A;So;0;L;;;;;N;;;;;
+1F171;NEGATIVE SQUARED LATIN CAPITAL LETTER B;So;0;L;;;;;N;;;;;
+1F172;NEGATIVE SQUARED LATIN CAPITAL LETTER C;So;0;L;;;;;N;;;;;
+1F173;NEGATIVE SQUARED LATIN CAPITAL LETTER D;So;0;L;;;;;N;;;;;
+1F174;NEGATIVE SQUARED LATIN CAPITAL LETTER E;So;0;L;;;;;N;;;;;
+1F175;NEGATIVE SQUARED LATIN CAPITAL LETTER F;So;0;L;;;;;N;;;;;
+1F176;NEGATIVE SQUARED LATIN CAPITAL LETTER G;So;0;L;;;;;N;;;;;
+1F177;NEGATIVE SQUARED LATIN CAPITAL LETTER H;So;0;L;;;;;N;;;;;
+1F178;NEGATIVE SQUARED LATIN CAPITAL LETTER I;So;0;L;;;;;N;;;;;
+1F179;NEGATIVE SQUARED LATIN CAPITAL LETTER J;So;0;L;;;;;N;;;;;
+1F17A;NEGATIVE SQUARED LATIN CAPITAL LETTER K;So;0;L;;;;;N;;;;;
+1F17B;NEGATIVE SQUARED LATIN CAPITAL LETTER L;So;0;L;;;;;N;;;;;
+1F17C;NEGATIVE SQUARED LATIN CAPITAL LETTER M;So;0;L;;;;;N;;;;;
+1F17D;NEGATIVE SQUARED LATIN CAPITAL LETTER N;So;0;L;;;;;N;;;;;
+1F17E;NEGATIVE SQUARED LATIN CAPITAL LETTER O;So;0;L;;;;;N;;;;;
+1F17F;NEGATIVE SQUARED LATIN CAPITAL LETTER P;So;0;L;;;;;N;;;;;
+1F180;NEGATIVE SQUARED LATIN CAPITAL LETTER Q;So;0;L;;;;;N;;;;;
+1F181;NEGATIVE SQUARED LATIN CAPITAL LETTER R;So;0;L;;;;;N;;;;;
+1F182;NEGATIVE SQUARED LATIN CAPITAL LETTER S;So;0;L;;;;;N;;;;;
+1F183;NEGATIVE SQUARED LATIN CAPITAL LETTER T;So;0;L;;;;;N;;;;;
+1F184;NEGATIVE SQUARED LATIN CAPITAL LETTER U;So;0;L;;;;;N;;;;;
+1F185;NEGATIVE SQUARED LATIN CAPITAL LETTER V;So;0;L;;;;;N;;;;;
+1F186;NEGATIVE SQUARED LATIN CAPITAL LETTER W;So;0;L;;;;;N;;;;;
+1F187;NEGATIVE SQUARED LATIN CAPITAL LETTER X;So;0;L;;;;;N;;;;;
+1F188;NEGATIVE SQUARED LATIN CAPITAL LETTER Y;So;0;L;;;;;N;;;;;
+1F189;NEGATIVE SQUARED LATIN CAPITAL LETTER Z;So;0;L;;;;;N;;;;;
+1F18A;CROSSED NEGATIVE SQUARED LATIN CAPITAL LETTER P;So;0;L;;;;;N;;;;;
+1F18B;NEGATIVE SQUARED IC;So;0;L;;;;;N;;;;;
+1F18C;NEGATIVE SQUARED PA;So;0;L;;;;;N;;;;;
+1F18D;NEGATIVE SQUARED SA;So;0;L;;;;;N;;;;;
+1F18E;NEGATIVE SQUARED AB;So;0;L;;;;;N;;;;;
+1F18F;NEGATIVE SQUARED WC;So;0;L;;;;;N;;;;;
+1F190;SQUARE DJ;So;0;L;<square> 0044 004A;;;;N;;;;;
+1F191;SQUARED CL;So;0;L;;;;;N;;;;;
+1F192;SQUARED COOL;So;0;L;;;;;N;;;;;
+1F193;SQUARED FREE;So;0;L;;;;;N;;;;;
+1F194;SQUARED ID;So;0;L;;;;;N;;;;;
+1F195;SQUARED NEW;So;0;L;;;;;N;;;;;
+1F196;SQUARED NG;So;0;L;;;;;N;;;;;
+1F197;SQUARED OK;So;0;L;;;;;N;;;;;
+1F198;SQUARED SOS;So;0;L;;;;;N;;;;;
+1F199;SQUARED UP WITH EXCLAMATION MARK;So;0;L;;;;;N;;;;;
+1F19A;SQUARED VS;So;0;L;;;;;N;;;;;
+1F19B;SQUARED THREE D;So;0;L;;;;;N;;;;;
+1F19C;SQUARED SECOND SCREEN;So;0;L;;;;;N;;;;;
+1F19D;SQUARED TWO K;So;0;L;;;;;N;;;;;
+1F19E;SQUARED FOUR K;So;0;L;;;;;N;;;;;
+1F19F;SQUARED EIGHT K;So;0;L;;;;;N;;;;;
+1F1A0;SQUARED FIVE POINT ONE;So;0;L;;;;;N;;;;;
+1F1A1;SQUARED SEVEN POINT ONE;So;0;L;;;;;N;;;;;
+1F1A2;SQUARED TWENTY-TWO POINT TWO;So;0;L;;;;;N;;;;;
+1F1A3;SQUARED SIXTY P;So;0;L;;;;;N;;;;;
+1F1A4;SQUARED ONE HUNDRED TWENTY P;So;0;L;;;;;N;;;;;
+1F1A5;SQUARED LATIN SMALL LETTER D;So;0;L;;;;;N;;;;;
+1F1A6;SQUARED HC;So;0;L;;;;;N;;;;;
+1F1A7;SQUARED HDR;So;0;L;;;;;N;;;;;
+1F1A8;SQUARED HI-RES;So;0;L;;;;;N;;;;;
+1F1A9;SQUARED LOSSLESS;So;0;L;;;;;N;;;;;
+1F1AA;SQUARED SHV;So;0;L;;;;;N;;;;;
+1F1AB;SQUARED UHD;So;0;L;;;;;N;;;;;
+1F1AC;SQUARED VOD;So;0;L;;;;;N;;;;;
+1F1E6;REGIONAL INDICATOR SYMBOL LETTER A;So;0;L;;;;;N;;;;;
+1F1E7;REGIONAL INDICATOR SYMBOL LETTER B;So;0;L;;;;;N;;;;;
+1F1E8;REGIONAL INDICATOR SYMBOL LETTER C;So;0;L;;;;;N;;;;;
+1F1E9;REGIONAL INDICATOR SYMBOL LETTER D;So;0;L;;;;;N;;;;;
+1F1EA;REGIONAL INDICATOR SYMBOL LETTER E;So;0;L;;;;;N;;;;;
+1F1EB;REGIONAL INDICATOR SYMBOL LETTER F;So;0;L;;;;;N;;;;;
+1F1EC;REGIONAL INDICATOR SYMBOL LETTER G;So;0;L;;;;;N;;;;;
+1F1ED;REGIONAL INDICATOR SYMBOL LETTER H;So;0;L;;;;;N;;;;;
+1F1EE;REGIONAL INDICATOR SYMBOL LETTER I;So;0;L;;;;;N;;;;;
+1F1EF;REGIONAL INDICATOR SYMBOL LETTER J;So;0;L;;;;;N;;;;;
+1F1F0;REGIONAL INDICATOR SYMBOL LETTER K;So;0;L;;;;;N;;;;;
+1F1F1;REGIONAL INDICATOR SYMBOL LETTER L;So;0;L;;;;;N;;;;;
+1F1F2;REGIONAL INDICATOR SYMBOL LETTER M;So;0;L;;;;;N;;;;;
+1F1F3;REGIONAL INDICATOR SYMBOL LETTER N;So;0;L;;;;;N;;;;;
+1F1F4;REGIONAL INDICATOR SYMBOL LETTER O;So;0;L;;;;;N;;;;;
+1F1F5;REGIONAL INDICATOR SYMBOL LETTER P;So;0;L;;;;;N;;;;;
+1F1F6;REGIONAL INDICATOR SYMBOL LETTER Q;So;0;L;;;;;N;;;;;
+1F1F7;REGIONAL INDICATOR SYMBOL LETTER R;So;0;L;;;;;N;;;;;
+1F1F8;REGIONAL INDICATOR SYMBOL LETTER S;So;0;L;;;;;N;;;;;
+1F1F9;REGIONAL INDICATOR SYMBOL LETTER T;So;0;L;;;;;N;;;;;
+1F1FA;REGIONAL INDICATOR SYMBOL LETTER U;So;0;L;;;;;N;;;;;
+1F1FB;REGIONAL INDICATOR SYMBOL LETTER V;So;0;L;;;;;N;;;;;
+1F1FC;REGIONAL INDICATOR SYMBOL LETTER W;So;0;L;;;;;N;;;;;
+1F1FD;REGIONAL INDICATOR SYMBOL LETTER X;So;0;L;;;;;N;;;;;
+1F1FE;REGIONAL INDICATOR SYMBOL LETTER Y;So;0;L;;;;;N;;;;;
+1F1FF;REGIONAL INDICATOR SYMBOL LETTER Z;So;0;L;;;;;N;;;;;
+1F200;SQUARE HIRAGANA HOKA;So;0;L;<square> 307B 304B;;;;N;;;;;
+1F201;SQUARED KATAKANA KOKO;So;0;L;<square> 30B3 30B3;;;;N;;;;;
+1F202;SQUARED KATAKANA SA;So;0;L;<square> 30B5;;;;N;;;;;
+1F210;SQUARED CJK UNIFIED IDEOGRAPH-624B;So;0;L;<square> 624B;;;;N;;;;;
+1F211;SQUARED CJK UNIFIED IDEOGRAPH-5B57;So;0;L;<square> 5B57;;;;N;;;;;
+1F212;SQUARED CJK UNIFIED IDEOGRAPH-53CC;So;0;L;<square> 53CC;;;;N;;;;;
+1F213;SQUARED KATAKANA DE;So;0;L;<square> 30C7;;;;N;;;;;
+1F214;SQUARED CJK UNIFIED IDEOGRAPH-4E8C;So;0;L;<square> 4E8C;;;;N;;;;;
+1F215;SQUARED CJK UNIFIED IDEOGRAPH-591A;So;0;L;<square> 591A;;;;N;;;;;
+1F216;SQUARED CJK UNIFIED IDEOGRAPH-89E3;So;0;L;<square> 89E3;;;;N;;;;;
+1F217;SQUARED CJK UNIFIED IDEOGRAPH-5929;So;0;L;<square> 5929;;;;N;;;;;
+1F218;SQUARED CJK UNIFIED IDEOGRAPH-4EA4;So;0;L;<square> 4EA4;;;;N;;;;;
+1F219;SQUARED CJK UNIFIED IDEOGRAPH-6620;So;0;L;<square> 6620;;;;N;;;;;
+1F21A;SQUARED CJK UNIFIED IDEOGRAPH-7121;So;0;L;<square> 7121;;;;N;;;;;
+1F21B;SQUARED CJK UNIFIED IDEOGRAPH-6599;So;0;L;<square> 6599;;;;N;;;;;
+1F21C;SQUARED CJK UNIFIED IDEOGRAPH-524D;So;0;L;<square> 524D;;;;N;;;;;
+1F21D;SQUARED CJK UNIFIED IDEOGRAPH-5F8C;So;0;L;<square> 5F8C;;;;N;;;;;
+1F21E;SQUARED CJK UNIFIED IDEOGRAPH-518D;So;0;L;<square> 518D;;;;N;;;;;
+1F21F;SQUARED CJK UNIFIED IDEOGRAPH-65B0;So;0;L;<square> 65B0;;;;N;;;;;
+1F220;SQUARED CJK UNIFIED IDEOGRAPH-521D;So;0;L;<square> 521D;;;;N;;;;;
+1F221;SQUARED CJK UNIFIED IDEOGRAPH-7D42;So;0;L;<square> 7D42;;;;N;;;;;
+1F222;SQUARED CJK UNIFIED IDEOGRAPH-751F;So;0;L;<square> 751F;;;;N;;;;;
+1F223;SQUARED CJK UNIFIED IDEOGRAPH-8CA9;So;0;L;<square> 8CA9;;;;N;;;;;
+1F224;SQUARED CJK UNIFIED IDEOGRAPH-58F0;So;0;L;<square> 58F0;;;;N;;;;;
+1F225;SQUARED CJK UNIFIED IDEOGRAPH-5439;So;0;L;<square> 5439;;;;N;;;;;
+1F226;SQUARED CJK UNIFIED IDEOGRAPH-6F14;So;0;L;<square> 6F14;;;;N;;;;;
+1F227;SQUARED CJK UNIFIED IDEOGRAPH-6295;So;0;L;<square> 6295;;;;N;;;;;
+1F228;SQUARED CJK UNIFIED IDEOGRAPH-6355;So;0;L;<square> 6355;;;;N;;;;;
+1F229;SQUARED CJK UNIFIED IDEOGRAPH-4E00;So;0;L;<square> 4E00;;;;N;;;;;
+1F22A;SQUARED CJK UNIFIED IDEOGRAPH-4E09;So;0;L;<square> 4E09;;;;N;;;;;
+1F22B;SQUARED CJK UNIFIED IDEOGRAPH-904A;So;0;L;<square> 904A;;;;N;;;;;
+1F22C;SQUARED CJK UNIFIED IDEOGRAPH-5DE6;So;0;L;<square> 5DE6;;;;N;;;;;
+1F22D;SQUARED CJK UNIFIED IDEOGRAPH-4E2D;So;0;L;<square> 4E2D;;;;N;;;;;
+1F22E;SQUARED CJK UNIFIED IDEOGRAPH-53F3;So;0;L;<square> 53F3;;;;N;;;;;
+1F22F;SQUARED CJK UNIFIED IDEOGRAPH-6307;So;0;L;<square> 6307;;;;N;;;;;
+1F230;SQUARED CJK UNIFIED IDEOGRAPH-8D70;So;0;L;<square> 8D70;;;;N;;;;;
+1F231;SQUARED CJK UNIFIED IDEOGRAPH-6253;So;0;L;<square> 6253;;;;N;;;;;
+1F232;SQUARED CJK UNIFIED IDEOGRAPH-7981;So;0;L;<square> 7981;;;;N;;;;;
+1F233;SQUARED CJK UNIFIED IDEOGRAPH-7A7A;So;0;L;<square> 7A7A;;;;N;;;;;
+1F234;SQUARED CJK UNIFIED IDEOGRAPH-5408;So;0;L;<square> 5408;;;;N;;;;;
+1F235;SQUARED CJK UNIFIED IDEOGRAPH-6E80;So;0;L;<square> 6E80;;;;N;;;;;
+1F236;SQUARED CJK UNIFIED IDEOGRAPH-6709;So;0;L;<square> 6709;;;;N;;;;;
+1F237;SQUARED CJK UNIFIED IDEOGRAPH-6708;So;0;L;<square> 6708;;;;N;;;;;
+1F238;SQUARED CJK UNIFIED IDEOGRAPH-7533;So;0;L;<square> 7533;;;;N;;;;;
+1F239;SQUARED CJK UNIFIED IDEOGRAPH-5272;So;0;L;<square> 5272;;;;N;;;;;
+1F23A;SQUARED CJK UNIFIED IDEOGRAPH-55B6;So;0;L;<square> 55B6;;;;N;;;;;
+1F23B;SQUARED CJK UNIFIED IDEOGRAPH-914D;So;0;L;<square> 914D;;;;N;;;;;
+1F240;TORTOISE SHELL BRACKETED CJK UNIFIED IDEOGRAPH-672C;So;0;L;<compat> 3014 672C 3015;;;;N;;;;;
+1F241;TORTOISE SHELL BRACKETED CJK UNIFIED IDEOGRAPH-4E09;So;0;L;<compat> 3014 4E09 3015;;;;N;;;;;
+1F242;TORTOISE SHELL BRACKETED CJK UNIFIED IDEOGRAPH-4E8C;So;0;L;<compat> 3014 4E8C 3015;;;;N;;;;;
+1F243;TORTOISE SHELL BRACKETED CJK UNIFIED IDEOGRAPH-5B89;So;0;L;<compat> 3014 5B89 3015;;;;N;;;;;
+1F244;TORTOISE SHELL BRACKETED CJK UNIFIED IDEOGRAPH-70B9;So;0;L;<compat> 3014 70B9 3015;;;;N;;;;;
+1F245;TORTOISE SHELL BRACKETED CJK UNIFIED IDEOGRAPH-6253;So;0;L;<compat> 3014 6253 3015;;;;N;;;;;
+1F246;TORTOISE SHELL BRACKETED CJK UNIFIED IDEOGRAPH-76D7;So;0;L;<compat> 3014 76D7 3015;;;;N;;;;;
+1F247;TORTOISE SHELL BRACKETED CJK UNIFIED IDEOGRAPH-52DD;So;0;L;<compat> 3014 52DD 3015;;;;N;;;;;
+1F248;TORTOISE SHELL BRACKETED CJK UNIFIED IDEOGRAPH-6557;So;0;L;<compat> 3014 6557 3015;;;;N;;;;;
+1F250;CIRCLED IDEOGRAPH ADVANTAGE;So;0;L;<circle> 5F97;;;;N;;;;;
+1F251;CIRCLED IDEOGRAPH ACCEPT;So;0;L;<circle> 53EF;;;;N;;;;;
+1F300;CYCLONE;So;0;ON;;;;;N;;;;;
+1F301;FOGGY;So;0;ON;;;;;N;;;;;
+1F302;CLOSED UMBRELLA;So;0;ON;;;;;N;;;;;
+1F303;NIGHT WITH STARS;So;0;ON;;;;;N;;;;;
+1F304;SUNRISE OVER MOUNTAINS;So;0;ON;;;;;N;;;;;
+1F305;SUNRISE;So;0;ON;;;;;N;;;;;
+1F306;CITYSCAPE AT DUSK;So;0;ON;;;;;N;;;;;
+1F307;SUNSET OVER BUILDINGS;So;0;ON;;;;;N;;;;;
+1F308;RAINBOW;So;0;ON;;;;;N;;;;;
+1F309;BRIDGE AT NIGHT;So;0;ON;;;;;N;;;;;
+1F30A;WATER WAVE;So;0;ON;;;;;N;;;;;
+1F30B;VOLCANO;So;0;ON;;;;;N;;;;;
+1F30C;MILKY WAY;So;0;ON;;;;;N;;;;;
+1F30D;EARTH GLOBE EUROPE-AFRICA;So;0;ON;;;;;N;;;;;
+1F30E;EARTH GLOBE AMERICAS;So;0;ON;;;;;N;;;;;
+1F30F;EARTH GLOBE ASIA-AUSTRALIA;So;0;ON;;;;;N;;;;;
+1F310;GLOBE WITH MERIDIANS;So;0;ON;;;;;N;;;;;
+1F311;NEW MOON SYMBOL;So;0;ON;;;;;N;;;;;
+1F312;WAXING CRESCENT MOON SYMBOL;So;0;ON;;;;;N;;;;;
+1F313;FIRST QUARTER MOON SYMBOL;So;0;ON;;;;;N;;;;;
+1F314;WAXING GIBBOUS MOON SYMBOL;So;0;ON;;;;;N;;;;;
+1F315;FULL MOON SYMBOL;So;0;ON;;;;;N;;;;;
+1F316;WANING GIBBOUS MOON SYMBOL;So;0;ON;;;;;N;;;;;
+1F317;LAST QUARTER MOON SYMBOL;So;0;ON;;;;;N;;;;;
+1F318;WANING CRESCENT MOON SYMBOL;So;0;ON;;;;;N;;;;;
+1F319;CRESCENT MOON;So;0;ON;;;;;N;;;;;
+1F31A;NEW MOON WITH FACE;So;0;ON;;;;;N;;;;;
+1F31B;FIRST QUARTER MOON WITH FACE;So;0;ON;;;;;N;;;;;
+1F31C;LAST QUARTER MOON WITH FACE;So;0;ON;;;;;N;;;;;
+1F31D;FULL MOON WITH FACE;So;0;ON;;;;;N;;;;;
+1F31E;SUN WITH FACE;So;0;ON;;;;;N;;;;;
+1F31F;GLOWING STAR;So;0;ON;;;;;N;;;;;
+1F320;SHOOTING STAR;So;0;ON;;;;;N;;;;;
+1F321;THERMOMETER;So;0;ON;;;;;N;;;;;
+1F322;BLACK DROPLET;So;0;ON;;;;;N;;;;;
+1F323;WHITE SUN;So;0;ON;;;;;N;;;;;
+1F324;WHITE SUN WITH SMALL CLOUD;So;0;ON;;;;;N;;;;;
+1F325;WHITE SUN BEHIND CLOUD;So;0;ON;;;;;N;;;;;
+1F326;WHITE SUN BEHIND CLOUD WITH RAIN;So;0;ON;;;;;N;;;;;
+1F327;CLOUD WITH RAIN;So;0;ON;;;;;N;;;;;
+1F328;CLOUD WITH SNOW;So;0;ON;;;;;N;;;;;
+1F329;CLOUD WITH LIGHTNING;So;0;ON;;;;;N;;;;;
+1F32A;CLOUD WITH TORNADO;So;0;ON;;;;;N;;;;;
+1F32B;FOG;So;0;ON;;;;;N;;;;;
+1F32C;WIND BLOWING FACE;So;0;ON;;;;;N;;;;;
+1F32D;HOT DOG;So;0;ON;;;;;N;;;;;
+1F32E;TACO;So;0;ON;;;;;N;;;;;
+1F32F;BURRITO;So;0;ON;;;;;N;;;;;
+1F330;CHESTNUT;So;0;ON;;;;;N;;;;;
+1F331;SEEDLING;So;0;ON;;;;;N;;;;;
+1F332;EVERGREEN TREE;So;0;ON;;;;;N;;;;;
+1F333;DECIDUOUS TREE;So;0;ON;;;;;N;;;;;
+1F334;PALM TREE;So;0;ON;;;;;N;;;;;
+1F335;CACTUS;So;0;ON;;;;;N;;;;;
+1F336;HOT PEPPER;So;0;ON;;;;;N;;;;;
+1F337;TULIP;So;0;ON;;;;;N;;;;;
+1F338;CHERRY BLOSSOM;So;0;ON;;;;;N;;;;;
+1F339;ROSE;So;0;ON;;;;;N;;;;;
+1F33A;HIBISCUS;So;0;ON;;;;;N;;;;;
+1F33B;SUNFLOWER;So;0;ON;;;;;N;;;;;
+1F33C;BLOSSOM;So;0;ON;;;;;N;;;;;
+1F33D;EAR OF MAIZE;So;0;ON;;;;;N;;;;;
+1F33E;EAR OF RICE;So;0;ON;;;;;N;;;;;
+1F33F;HERB;So;0;ON;;;;;N;;;;;
+1F340;FOUR LEAF CLOVER;So;0;ON;;;;;N;;;;;
+1F341;MAPLE LEAF;So;0;ON;;;;;N;;;;;
+1F342;FALLEN LEAF;So;0;ON;;;;;N;;;;;
+1F343;LEAF FLUTTERING IN WIND;So;0;ON;;;;;N;;;;;
+1F344;MUSHROOM;So;0;ON;;;;;N;;;;;
+1F345;TOMATO;So;0;ON;;;;;N;;;;;
+1F346;AUBERGINE;So;0;ON;;;;;N;;;;;
+1F347;GRAPES;So;0;ON;;;;;N;;;;;
+1F348;MELON;So;0;ON;;;;;N;;;;;
+1F349;WATERMELON;So;0;ON;;;;;N;;;;;
+1F34A;TANGERINE;So;0;ON;;;;;N;;;;;
+1F34B;LEMON;So;0;ON;;;;;N;;;;;
+1F34C;BANANA;So;0;ON;;;;;N;;;;;
+1F34D;PINEAPPLE;So;0;ON;;;;;N;;;;;
+1F34E;RED APPLE;So;0;ON;;;;;N;;;;;
+1F34F;GREEN APPLE;So;0;ON;;;;;N;;;;;
+1F350;PEAR;So;0;ON;;;;;N;;;;;
+1F351;PEACH;So;0;ON;;;;;N;;;;;
+1F352;CHERRIES;So;0;ON;;;;;N;;;;;
+1F353;STRAWBERRY;So;0;ON;;;;;N;;;;;
+1F354;HAMBURGER;So;0;ON;;;;;N;;;;;
+1F355;SLICE OF PIZZA;So;0;ON;;;;;N;;;;;
+1F356;MEAT ON BONE;So;0;ON;;;;;N;;;;;
+1F357;POULTRY LEG;So;0;ON;;;;;N;;;;;
+1F358;RICE CRACKER;So;0;ON;;;;;N;;;;;
+1F359;RICE BALL;So;0;ON;;;;;N;;;;;
+1F35A;COOKED RICE;So;0;ON;;;;;N;;;;;
+1F35B;CURRY AND RICE;So;0;ON;;;;;N;;;;;
+1F35C;STEAMING BOWL;So;0;ON;;;;;N;;;;;
+1F35D;SPAGHETTI;So;0;ON;;;;;N;;;;;
+1F35E;BREAD;So;0;ON;;;;;N;;;;;
+1F35F;FRENCH FRIES;So;0;ON;;;;;N;;;;;
+1F360;ROASTED SWEET POTATO;So;0;ON;;;;;N;;;;;
+1F361;DANGO;So;0;ON;;;;;N;;;;;
+1F362;ODEN;So;0;ON;;;;;N;;;;;
+1F363;SUSHI;So;0;ON;;;;;N;;;;;
+1F364;FRIED SHRIMP;So;0;ON;;;;;N;;;;;
+1F365;FISH CAKE WITH SWIRL DESIGN;So;0;ON;;;;;N;;;;;
+1F366;SOFT ICE CREAM;So;0;ON;;;;;N;;;;;
+1F367;SHAVED ICE;So;0;ON;;;;;N;;;;;
+1F368;ICE CREAM;So;0;ON;;;;;N;;;;;
+1F369;DOUGHNUT;So;0;ON;;;;;N;;;;;
+1F36A;COOKIE;So;0;ON;;;;;N;;;;;
+1F36B;CHOCOLATE BAR;So;0;ON;;;;;N;;;;;
+1F36C;CANDY;So;0;ON;;;;;N;;;;;
+1F36D;LOLLIPOP;So;0;ON;;;;;N;;;;;
+1F36E;CUSTARD;So;0;ON;;;;;N;;;;;
+1F36F;HONEY POT;So;0;ON;;;;;N;;;;;
+1F370;SHORTCAKE;So;0;ON;;;;;N;;;;;
+1F371;BENTO BOX;So;0;ON;;;;;N;;;;;
+1F372;POT OF FOOD;So;0;ON;;;;;N;;;;;
+1F373;COOKING;So;0;ON;;;;;N;;;;;
+1F374;FORK AND KNIFE;So;0;ON;;;;;N;;;;;
+1F375;TEACUP WITHOUT HANDLE;So;0;ON;;;;;N;;;;;
+1F376;SAKE BOTTLE AND CUP;So;0;ON;;;;;N;;;;;
+1F377;WINE GLASS;So;0;ON;;;;;N;;;;;
+1F378;COCKTAIL GLASS;So;0;ON;;;;;N;;;;;
+1F379;TROPICAL DRINK;So;0;ON;;;;;N;;;;;
+1F37A;BEER MUG;So;0;ON;;;;;N;;;;;
+1F37B;CLINKING BEER MUGS;So;0;ON;;;;;N;;;;;
+1F37C;BABY BOTTLE;So;0;ON;;;;;N;;;;;
+1F37D;FORK AND KNIFE WITH PLATE;So;0;ON;;;;;N;;;;;
+1F37E;BOTTLE WITH POPPING CORK;So;0;ON;;;;;N;;;;;
+1F37F;POPCORN;So;0;ON;;;;;N;;;;;
+1F380;RIBBON;So;0;ON;;;;;N;;;;;
+1F381;WRAPPED PRESENT;So;0;ON;;;;;N;;;;;
+1F382;BIRTHDAY CAKE;So;0;ON;;;;;N;;;;;
+1F383;JACK-O-LANTERN;So;0;ON;;;;;N;;;;;
+1F384;CHRISTMAS TREE;So;0;ON;;;;;N;;;;;
+1F385;FATHER CHRISTMAS;So;0;ON;;;;;N;;;;;
+1F386;FIREWORKS;So;0;ON;;;;;N;;;;;
+1F387;FIREWORK SPARKLER;So;0;ON;;;;;N;;;;;
+1F388;BALLOON;So;0;ON;;;;;N;;;;;
+1F389;PARTY POPPER;So;0;ON;;;;;N;;;;;
+1F38A;CONFETTI BALL;So;0;ON;;;;;N;;;;;
+1F38B;TANABATA TREE;So;0;ON;;;;;N;;;;;
+1F38C;CROSSED FLAGS;So;0;ON;;;;;N;;;;;
+1F38D;PINE DECORATION;So;0;ON;;;;;N;;;;;
+1F38E;JAPANESE DOLLS;So;0;ON;;;;;N;;;;;
+1F38F;CARP STREAMER;So;0;ON;;;;;N;;;;;
+1F390;WIND CHIME;So;0;ON;;;;;N;;;;;
+1F391;MOON VIEWING CEREMONY;So;0;ON;;;;;N;;;;;
+1F392;SCHOOL SATCHEL;So;0;ON;;;;;N;;;;;
+1F393;GRADUATION CAP;So;0;ON;;;;;N;;;;;
+1F394;HEART WITH TIP ON THE LEFT;So;0;ON;;;;;N;;;;;
+1F395;BOUQUET OF FLOWERS;So;0;ON;;;;;N;;;;;
+1F396;MILITARY MEDAL;So;0;ON;;;;;N;;;;;
+1F397;REMINDER RIBBON;So;0;ON;;;;;N;;;;;
+1F398;MUSICAL KEYBOARD WITH JACKS;So;0;ON;;;;;N;;;;;
+1F399;STUDIO MICROPHONE;So;0;ON;;;;;N;;;;;
+1F39A;LEVEL SLIDER;So;0;ON;;;;;N;;;;;
+1F39B;CONTROL KNOBS;So;0;ON;;;;;N;;;;;
+1F39C;BEAMED ASCENDING MUSICAL NOTES;So;0;ON;;;;;N;;;;;
+1F39D;BEAMED DESCENDING MUSICAL NOTES;So;0;ON;;;;;N;;;;;
+1F39E;FILM FRAMES;So;0;ON;;;;;N;;;;;
+1F39F;ADMISSION TICKETS;So;0;ON;;;;;N;;;;;
+1F3A0;CAROUSEL HORSE;So;0;ON;;;;;N;;;;;
+1F3A1;FERRIS WHEEL;So;0;ON;;;;;N;;;;;
+1F3A2;ROLLER COASTER;So;0;ON;;;;;N;;;;;
+1F3A3;FISHING POLE AND FISH;So;0;ON;;;;;N;;;;;
+1F3A4;MICROPHONE;So;0;ON;;;;;N;;;;;
+1F3A5;MOVIE CAMERA;So;0;ON;;;;;N;;;;;
+1F3A6;CINEMA;So;0;ON;;;;;N;;;;;
+1F3A7;HEADPHONE;So;0;ON;;;;;N;;;;;
+1F3A8;ARTIST PALETTE;So;0;ON;;;;;N;;;;;
+1F3A9;TOP HAT;So;0;ON;;;;;N;;;;;
+1F3AA;CIRCUS TENT;So;0;ON;;;;;N;;;;;
+1F3AB;TICKET;So;0;ON;;;;;N;;;;;
+1F3AC;CLAPPER BOARD;So;0;ON;;;;;N;;;;;
+1F3AD;PERFORMING ARTS;So;0;ON;;;;;N;;;;;
+1F3AE;VIDEO GAME;So;0;ON;;;;;N;;;;;
+1F3AF;DIRECT HIT;So;0;ON;;;;;N;;;;;
+1F3B0;SLOT MACHINE;So;0;ON;;;;;N;;;;;
+1F3B1;BILLIARDS;So;0;ON;;;;;N;;;;;
+1F3B2;GAME DIE;So;0;ON;;;;;N;;;;;
+1F3B3;BOWLING;So;0;ON;;;;;N;;;;;
+1F3B4;FLOWER PLAYING CARDS;So;0;ON;;;;;N;;;;;
+1F3B5;MUSICAL NOTE;So;0;ON;;;;;N;;;;;
+1F3B6;MULTIPLE MUSICAL NOTES;So;0;ON;;;;;N;;;;;
+1F3B7;SAXOPHONE;So;0;ON;;;;;N;;;;;
+1F3B8;GUITAR;So;0;ON;;;;;N;;;;;
+1F3B9;MUSICAL KEYBOARD;So;0;ON;;;;;N;;;;;
+1F3BA;TRUMPET;So;0;ON;;;;;N;;;;;
+1F3BB;VIOLIN;So;0;ON;;;;;N;;;;;
+1F3BC;MUSICAL SCORE;So;0;ON;;;;;N;;;;;
+1F3BD;RUNNING SHIRT WITH SASH;So;0;ON;;;;;N;;;;;
+1F3BE;TENNIS RACQUET AND BALL;So;0;ON;;;;;N;;;;;
+1F3BF;SKI AND SKI BOOT;So;0;ON;;;;;N;;;;;
+1F3C0;BASKETBALL AND HOOP;So;0;ON;;;;;N;;;;;
+1F3C1;CHEQUERED FLAG;So;0;ON;;;;;N;;;;;
+1F3C2;SNOWBOARDER;So;0;ON;;;;;N;;;;;
+1F3C3;RUNNER;So;0;ON;;;;;N;;;;;
+1F3C4;SURFER;So;0;ON;;;;;N;;;;;
+1F3C5;SPORTS MEDAL;So;0;ON;;;;;N;;;;;
+1F3C6;TROPHY;So;0;ON;;;;;N;;;;;
+1F3C7;HORSE RACING;So;0;ON;;;;;N;;;;;
+1F3C8;AMERICAN FOOTBALL;So;0;ON;;;;;N;;;;;
+1F3C9;RUGBY FOOTBALL;So;0;ON;;;;;N;;;;;
+1F3CA;SWIMMER;So;0;ON;;;;;N;;;;;
+1F3CB;WEIGHT LIFTER;So;0;ON;;;;;N;;;;;
+1F3CC;GOLFER;So;0;ON;;;;;N;;;;;
+1F3CD;RACING MOTORCYCLE;So;0;ON;;;;;N;;;;;
+1F3CE;RACING CAR;So;0;ON;;;;;N;;;;;
+1F3CF;CRICKET BAT AND BALL;So;0;ON;;;;;N;;;;;
+1F3D0;VOLLEYBALL;So;0;ON;;;;;N;;;;;
+1F3D1;FIELD HOCKEY STICK AND BALL;So;0;ON;;;;;N;;;;;
+1F3D2;ICE HOCKEY STICK AND PUCK;So;0;ON;;;;;N;;;;;
+1F3D3;TABLE TENNIS PADDLE AND BALL;So;0;ON;;;;;N;;;;;
+1F3D4;SNOW CAPPED MOUNTAIN;So;0;ON;;;;;N;;;;;
+1F3D5;CAMPING;So;0;ON;;;;;N;;;;;
+1F3D6;BEACH WITH UMBRELLA;So;0;ON;;;;;N;;;;;
+1F3D7;BUILDING CONSTRUCTION;So;0;ON;;;;;N;;;;;
+1F3D8;HOUSE BUILDINGS;So;0;ON;;;;;N;;;;;
+1F3D9;CITYSCAPE;So;0;ON;;;;;N;;;;;
+1F3DA;DERELICT HOUSE BUILDING;So;0;ON;;;;;N;;;;;
+1F3DB;CLASSICAL BUILDING;So;0;ON;;;;;N;;;;;
+1F3DC;DESERT;So;0;ON;;;;;N;;;;;
+1F3DD;DESERT ISLAND;So;0;ON;;;;;N;;;;;
+1F3DE;NATIONAL PARK;So;0;ON;;;;;N;;;;;
+1F3DF;STADIUM;So;0;ON;;;;;N;;;;;
+1F3E0;HOUSE BUILDING;So;0;ON;;;;;N;;;;;
+1F3E1;HOUSE WITH GARDEN;So;0;ON;;;;;N;;;;;
+1F3E2;OFFICE BUILDING;So;0;ON;;;;;N;;;;;
+1F3E3;JAPANESE POST OFFICE;So;0;ON;;;;;N;;;;;
+1F3E4;EUROPEAN POST OFFICE;So;0;ON;;;;;N;;;;;
+1F3E5;HOSPITAL;So;0;ON;;;;;N;;;;;
+1F3E6;BANK;So;0;ON;;;;;N;;;;;
+1F3E7;AUTOMATED TELLER MACHINE;So;0;ON;;;;;N;;;;;
+1F3E8;HOTEL;So;0;ON;;;;;N;;;;;
+1F3E9;LOVE HOTEL;So;0;ON;;;;;N;;;;;
+1F3EA;CONVENIENCE STORE;So;0;ON;;;;;N;;;;;
+1F3EB;SCHOOL;So;0;ON;;;;;N;;;;;
+1F3EC;DEPARTMENT STORE;So;0;ON;;;;;N;;;;;
+1F3ED;FACTORY;So;0;ON;;;;;N;;;;;
+1F3EE;IZAKAYA LANTERN;So;0;ON;;;;;N;;;;;
+1F3EF;JAPANESE CASTLE;So;0;ON;;;;;N;;;;;
+1F3F0;EUROPEAN CASTLE;So;0;ON;;;;;N;;;;;
+1F3F1;WHITE PENNANT;So;0;ON;;;;;N;;;;;
+1F3F2;BLACK PENNANT;So;0;ON;;;;;N;;;;;
+1F3F3;WAVING WHITE FLAG;So;0;ON;;;;;N;;;;;
+1F3F4;WAVING BLACK FLAG;So;0;ON;;;;;N;;;;;
+1F3F5;ROSETTE;So;0;ON;;;;;N;;;;;
+1F3F6;BLACK ROSETTE;So;0;ON;;;;;N;;;;;
+1F3F7;LABEL;So;0;ON;;;;;N;;;;;
+1F3F8;BADMINTON RACQUET AND SHUTTLECOCK;So;0;ON;;;;;N;;;;;
+1F3F9;BOW AND ARROW;So;0;ON;;;;;N;;;;;
+1F3FA;AMPHORA;So;0;ON;;;;;N;;;;;
+1F3FB;EMOJI MODIFIER FITZPATRICK TYPE-1-2;Sk;0;ON;;;;;N;;;;;
+1F3FC;EMOJI MODIFIER FITZPATRICK TYPE-3;Sk;0;ON;;;;;N;;;;;
+1F3FD;EMOJI MODIFIER FITZPATRICK TYPE-4;Sk;0;ON;;;;;N;;;;;
+1F3FE;EMOJI MODIFIER FITZPATRICK TYPE-5;Sk;0;ON;;;;;N;;;;;
+1F3FF;EMOJI MODIFIER FITZPATRICK TYPE-6;Sk;0;ON;;;;;N;;;;;
+1F400;RAT;So;0;ON;;;;;N;;;;;
+1F401;MOUSE;So;0;ON;;;;;N;;;;;
+1F402;OX;So;0;ON;;;;;N;;;;;
+1F403;WATER BUFFALO;So;0;ON;;;;;N;;;;;
+1F404;COW;So;0;ON;;;;;N;;;;;
+1F405;TIGER;So;0;ON;;;;;N;;;;;
+1F406;LEOPARD;So;0;ON;;;;;N;;;;;
+1F407;RABBIT;So;0;ON;;;;;N;;;;;
+1F408;CAT;So;0;ON;;;;;N;;;;;
+1F409;DRAGON;So;0;ON;;;;;N;;;;;
+1F40A;CROCODILE;So;0;ON;;;;;N;;;;;
+1F40B;WHALE;So;0;ON;;;;;N;;;;;
+1F40C;SNAIL;So;0;ON;;;;;N;;;;;
+1F40D;SNAKE;So;0;ON;;;;;N;;;;;
+1F40E;HORSE;So;0;ON;;;;;N;;;;;
+1F40F;RAM;So;0;ON;;;;;N;;;;;
+1F410;GOAT;So;0;ON;;;;;N;;;;;
+1F411;SHEEP;So;0;ON;;;;;N;;;;;
+1F412;MONKEY;So;0;ON;;;;;N;;;;;
+1F413;ROOSTER;So;0;ON;;;;;N;;;;;
+1F414;CHICKEN;So;0;ON;;;;;N;;;;;
+1F415;DOG;So;0;ON;;;;;N;;;;;
+1F416;PIG;So;0;ON;;;;;N;;;;;
+1F417;BOAR;So;0;ON;;;;;N;;;;;
+1F418;ELEPHANT;So;0;ON;;;;;N;;;;;
+1F419;OCTOPUS;So;0;ON;;;;;N;;;;;
+1F41A;SPIRAL SHELL;So;0;ON;;;;;N;;;;;
+1F41B;BUG;So;0;ON;;;;;N;;;;;
+1F41C;ANT;So;0;ON;;;;;N;;;;;
+1F41D;HONEYBEE;So;0;ON;;;;;N;;;;;
+1F41E;LADY BEETLE;So;0;ON;;;;;N;;;;;
+1F41F;FISH;So;0;ON;;;;;N;;;;;
+1F420;TROPICAL FISH;So;0;ON;;;;;N;;;;;
+1F421;BLOWFISH;So;0;ON;;;;;N;;;;;
+1F422;TURTLE;So;0;ON;;;;;N;;;;;
+1F423;HATCHING CHICK;So;0;ON;;;;;N;;;;;
+1F424;BABY CHICK;So;0;ON;;;;;N;;;;;
+1F425;FRONT-FACING BABY CHICK;So;0;ON;;;;;N;;;;;
+1F426;BIRD;So;0;ON;;;;;N;;;;;
+1F427;PENGUIN;So;0;ON;;;;;N;;;;;
+1F428;KOALA;So;0;ON;;;;;N;;;;;
+1F429;POODLE;So;0;ON;;;;;N;;;;;
+1F42A;DROMEDARY CAMEL;So;0;ON;;;;;N;;;;;
+1F42B;BACTRIAN CAMEL;So;0;ON;;;;;N;;;;;
+1F42C;DOLPHIN;So;0;ON;;;;;N;;;;;
+1F42D;MOUSE FACE;So;0;ON;;;;;N;;;;;
+1F42E;COW FACE;So;0;ON;;;;;N;;;;;
+1F42F;TIGER FACE;So;0;ON;;;;;N;;;;;
+1F430;RABBIT FACE;So;0;ON;;;;;N;;;;;
+1F431;CAT FACE;So;0;ON;;;;;N;;;;;
+1F432;DRAGON FACE;So;0;ON;;;;;N;;;;;
+1F433;SPOUTING WHALE;So;0;ON;;;;;N;;;;;
+1F434;HORSE FACE;So;0;ON;;;;;N;;;;;
+1F435;MONKEY FACE;So;0;ON;;;;;N;;;;;
+1F436;DOG FACE;So;0;ON;;;;;N;;;;;
+1F437;PIG FACE;So;0;ON;;;;;N;;;;;
+1F438;FROG FACE;So;0;ON;;;;;N;;;;;
+1F439;HAMSTER FACE;So;0;ON;;;;;N;;;;;
+1F43A;WOLF FACE;So;0;ON;;;;;N;;;;;
+1F43B;BEAR FACE;So;0;ON;;;;;N;;;;;
+1F43C;PANDA FACE;So;0;ON;;;;;N;;;;;
+1F43D;PIG NOSE;So;0;ON;;;;;N;;;;;
+1F43E;PAW PRINTS;So;0;ON;;;;;N;;;;;
+1F43F;CHIPMUNK;So;0;ON;;;;;N;;;;;
+1F440;EYES;So;0;ON;;;;;N;;;;;
+1F441;EYE;So;0;ON;;;;;N;;;;;
+1F442;EAR;So;0;ON;;;;;N;;;;;
+1F443;NOSE;So;0;ON;;;;;N;;;;;
+1F444;MOUTH;So;0;ON;;;;;N;;;;;
+1F445;TONGUE;So;0;ON;;;;;N;;;;;
+1F446;WHITE UP POINTING BACKHAND INDEX;So;0;ON;;;;;N;;;;;
+1F447;WHITE DOWN POINTING BACKHAND INDEX;So;0;ON;;;;;N;;;;;
+1F448;WHITE LEFT POINTING BACKHAND INDEX;So;0;ON;;;;;N;;;;;
+1F449;WHITE RIGHT POINTING BACKHAND INDEX;So;0;ON;;;;;N;;;;;
+1F44A;FISTED HAND SIGN;So;0;ON;;;;;N;;;;;
+1F44B;WAVING HAND SIGN;So;0;ON;;;;;N;;;;;
+1F44C;OK HAND SIGN;So;0;ON;;;;;N;;;;;
+1F44D;THUMBS UP SIGN;So;0;ON;;;;;N;;;;;
+1F44E;THUMBS DOWN SIGN;So;0;ON;;;;;N;;;;;
+1F44F;CLAPPING HANDS SIGN;So;0;ON;;;;;N;;;;;
+1F450;OPEN HANDS SIGN;So;0;ON;;;;;N;;;;;
+1F451;CROWN;So;0;ON;;;;;N;;;;;
+1F452;WOMANS HAT;So;0;ON;;;;;N;;;;;
+1F453;EYEGLASSES;So;0;ON;;;;;N;;;;;
+1F454;NECKTIE;So;0;ON;;;;;N;;;;;
+1F455;T-SHIRT;So;0;ON;;;;;N;;;;;
+1F456;JEANS;So;0;ON;;;;;N;;;;;
+1F457;DRESS;So;0;ON;;;;;N;;;;;
+1F458;KIMONO;So;0;ON;;;;;N;;;;;
+1F459;BIKINI;So;0;ON;;;;;N;;;;;
+1F45A;WOMANS CLOTHES;So;0;ON;;;;;N;;;;;
+1F45B;PURSE;So;0;ON;;;;;N;;;;;
+1F45C;HANDBAG;So;0;ON;;;;;N;;;;;
+1F45D;POUCH;So;0;ON;;;;;N;;;;;
+1F45E;MANS SHOE;So;0;ON;;;;;N;;;;;
+1F45F;ATHLETIC SHOE;So;0;ON;;;;;N;;;;;
+1F460;HIGH-HEELED SHOE;So;0;ON;;;;;N;;;;;
+1F461;WOMANS SANDAL;So;0;ON;;;;;N;;;;;
+1F462;WOMANS BOOTS;So;0;ON;;;;;N;;;;;
+1F463;FOOTPRINTS;So;0;ON;;;;;N;;;;;
+1F464;BUST IN SILHOUETTE;So;0;ON;;;;;N;;;;;
+1F465;BUSTS IN SILHOUETTE;So;0;ON;;;;;N;;;;;
+1F466;BOY;So;0;ON;;;;;N;;;;;
+1F467;GIRL;So;0;ON;;;;;N;;;;;
+1F468;MAN;So;0;ON;;;;;N;;;;;
+1F469;WOMAN;So;0;ON;;;;;N;;;;;
+1F46A;FAMILY;So;0;ON;;;;;N;;;;;
+1F46B;MAN AND WOMAN HOLDING HANDS;So;0;ON;;;;;N;;;;;
+1F46C;TWO MEN HOLDING HANDS;So;0;ON;;;;;N;;;;;
+1F46D;TWO WOMEN HOLDING HANDS;So;0;ON;;;;;N;;;;;
+1F46E;POLICE OFFICER;So;0;ON;;;;;N;;;;;
+1F46F;WOMAN WITH BUNNY EARS;So;0;ON;;;;;N;;;;;
+1F470;BRIDE WITH VEIL;So;0;ON;;;;;N;;;;;
+1F471;PERSON WITH BLOND HAIR;So;0;ON;;;;;N;;;;;
+1F472;MAN WITH GUA PI MAO;So;0;ON;;;;;N;;;;;
+1F473;MAN WITH TURBAN;So;0;ON;;;;;N;;;;;
+1F474;OLDER MAN;So;0;ON;;;;;N;;;;;
+1F475;OLDER WOMAN;So;0;ON;;;;;N;;;;;
+1F476;BABY;So;0;ON;;;;;N;;;;;
+1F477;CONSTRUCTION WORKER;So;0;ON;;;;;N;;;;;
+1F478;PRINCESS;So;0;ON;;;;;N;;;;;
+1F479;JAPANESE OGRE;So;0;ON;;;;;N;;;;;
+1F47A;JAPANESE GOBLIN;So;0;ON;;;;;N;;;;;
+1F47B;GHOST;So;0;ON;;;;;N;;;;;
+1F47C;BABY ANGEL;So;0;ON;;;;;N;;;;;
+1F47D;EXTRATERRESTRIAL ALIEN;So;0;ON;;;;;N;;;;;
+1F47E;ALIEN MONSTER;So;0;ON;;;;;N;;;;;
+1F47F;IMP;So;0;ON;;;;;N;;;;;
+1F480;SKULL;So;0;ON;;;;;N;;;;;
+1F481;INFORMATION DESK PERSON;So;0;ON;;;;;N;;;;;
+1F482;GUARDSMAN;So;0;ON;;;;;N;;;;;
+1F483;DANCER;So;0;ON;;;;;N;;;;;
+1F484;LIPSTICK;So;0;ON;;;;;N;;;;;
+1F485;NAIL POLISH;So;0;ON;;;;;N;;;;;
+1F486;FACE MASSAGE;So;0;ON;;;;;N;;;;;
+1F487;HAIRCUT;So;0;ON;;;;;N;;;;;
+1F488;BARBER POLE;So;0;ON;;;;;N;;;;;
+1F489;SYRINGE;So;0;ON;;;;;N;;;;;
+1F48A;PILL;So;0;ON;;;;;N;;;;;
+1F48B;KISS MARK;So;0;ON;;;;;N;;;;;
+1F48C;LOVE LETTER;So;0;ON;;;;;N;;;;;
+1F48D;RING;So;0;ON;;;;;N;;;;;
+1F48E;GEM STONE;So;0;ON;;;;;N;;;;;
+1F48F;KISS;So;0;ON;;;;;N;;;;;
+1F490;BOUQUET;So;0;ON;;;;;N;;;;;
+1F491;COUPLE WITH HEART;So;0;ON;;;;;N;;;;;
+1F492;WEDDING;So;0;ON;;;;;N;;;;;
+1F493;BEATING HEART;So;0;ON;;;;;N;;;;;
+1F494;BROKEN HEART;So;0;ON;;;;;N;;;;;
+1F495;TWO HEARTS;So;0;ON;;;;;N;;;;;
+1F496;SPARKLING HEART;So;0;ON;;;;;N;;;;;
+1F497;GROWING HEART;So;0;ON;;;;;N;;;;;
+1F498;HEART WITH ARROW;So;0;ON;;;;;N;;;;;
+1F499;BLUE HEART;So;0;ON;;;;;N;;;;;
+1F49A;GREEN HEART;So;0;ON;;;;;N;;;;;
+1F49B;YELLOW HEART;So;0;ON;;;;;N;;;;;
+1F49C;PURPLE HEART;So;0;ON;;;;;N;;;;;
+1F49D;HEART WITH RIBBON;So;0;ON;;;;;N;;;;;
+1F49E;REVOLVING HEARTS;So;0;ON;;;;;N;;;;;
+1F49F;HEART DECORATION;So;0;ON;;;;;N;;;;;
+1F4A0;DIAMOND SHAPE WITH A DOT INSIDE;So;0;ON;;;;;N;;;;;
+1F4A1;ELECTRIC LIGHT BULB;So;0;ON;;;;;N;;;;;
+1F4A2;ANGER SYMBOL;So;0;ON;;;;;N;;;;;
+1F4A3;BOMB;So;0;ON;;;;;N;;;;;
+1F4A4;SLEEPING SYMBOL;So;0;ON;;;;;N;;;;;
+1F4A5;COLLISION SYMBOL;So;0;ON;;;;;N;;;;;
+1F4A6;SPLASHING SWEAT SYMBOL;So;0;ON;;;;;N;;;;;
+1F4A7;DROPLET;So;0;ON;;;;;N;;;;;
+1F4A8;DASH SYMBOL;So;0;ON;;;;;N;;;;;
+1F4A9;PILE OF POO;So;0;ON;;;;;N;;;;;
+1F4AA;FLEXED BICEPS;So;0;ON;;;;;N;;;;;
+1F4AB;DIZZY SYMBOL;So;0;ON;;;;;N;;;;;
+1F4AC;SPEECH BALLOON;So;0;ON;;;;;N;;;;;
+1F4AD;THOUGHT BALLOON;So;0;ON;;;;;N;;;;;
+1F4AE;WHITE FLOWER;So;0;ON;;;;;N;;;;;
+1F4AF;HUNDRED POINTS SYMBOL;So;0;ON;;;;;N;;;;;
+1F4B0;MONEY BAG;So;0;ON;;;;;N;;;;;
+1F4B1;CURRENCY EXCHANGE;So;0;ON;;;;;N;;;;;
+1F4B2;HEAVY DOLLAR SIGN;So;0;ON;;;;;N;;;;;
+1F4B3;CREDIT CARD;So;0;ON;;;;;N;;;;;
+1F4B4;BANKNOTE WITH YEN SIGN;So;0;ON;;;;;N;;;;;
+1F4B5;BANKNOTE WITH DOLLAR SIGN;So;0;ON;;;;;N;;;;;
+1F4B6;BANKNOTE WITH EURO SIGN;So;0;ON;;;;;N;;;;;
+1F4B7;BANKNOTE WITH POUND SIGN;So;0;ON;;;;;N;;;;;
+1F4B8;MONEY WITH WINGS;So;0;ON;;;;;N;;;;;
+1F4B9;CHART WITH UPWARDS TREND AND YEN SIGN;So;0;ON;;;;;N;;;;;
+1F4BA;SEAT;So;0;ON;;;;;N;;;;;
+1F4BB;PERSONAL COMPUTER;So;0;ON;;;;;N;;;;;
+1F4BC;BRIEFCASE;So;0;ON;;;;;N;;;;;
+1F4BD;MINIDISC;So;0;ON;;;;;N;;;;;
+1F4BE;FLOPPY DISK;So;0;ON;;;;;N;;;;;
+1F4BF;OPTICAL DISC;So;0;ON;;;;;N;;;;;
+1F4C0;DVD;So;0;ON;;;;;N;;;;;
+1F4C1;FILE FOLDER;So;0;ON;;;;;N;;;;;
+1F4C2;OPEN FILE FOLDER;So;0;ON;;;;;N;;;;;
+1F4C3;PAGE WITH CURL;So;0;ON;;;;;N;;;;;
+1F4C4;PAGE FACING UP;So;0;ON;;;;;N;;;;;
+1F4C5;CALENDAR;So;0;ON;;;;;N;;;;;
+1F4C6;TEAR-OFF CALENDAR;So;0;ON;;;;;N;;;;;
+1F4C7;CARD INDEX;So;0;ON;;;;;N;;;;;
+1F4C8;CHART WITH UPWARDS TREND;So;0;ON;;;;;N;;;;;
+1F4C9;CHART WITH DOWNWARDS TREND;So;0;ON;;;;;N;;;;;
+1F4CA;BAR CHART;So;0;ON;;;;;N;;;;;
+1F4CB;CLIPBOARD;So;0;ON;;;;;N;;;;;
+1F4CC;PUSHPIN;So;0;ON;;;;;N;;;;;
+1F4CD;ROUND PUSHPIN;So;0;ON;;;;;N;;;;;
+1F4CE;PAPERCLIP;So;0;ON;;;;;N;;;;;
+1F4CF;STRAIGHT RULER;So;0;ON;;;;;N;;;;;
+1F4D0;TRIANGULAR RULER;So;0;ON;;;;;N;;;;;
+1F4D1;BOOKMARK TABS;So;0;ON;;;;;N;;;;;
+1F4D2;LEDGER;So;0;ON;;;;;N;;;;;
+1F4D3;NOTEBOOK;So;0;ON;;;;;N;;;;;
+1F4D4;NOTEBOOK WITH DECORATIVE COVER;So;0;ON;;;;;N;;;;;
+1F4D5;CLOSED BOOK;So;0;ON;;;;;N;;;;;
+1F4D6;OPEN BOOK;So;0;ON;;;;;N;;;;;
+1F4D7;GREEN BOOK;So;0;ON;;;;;N;;;;;
+1F4D8;BLUE BOOK;So;0;ON;;;;;N;;;;;
+1F4D9;ORANGE BOOK;So;0;ON;;;;;N;;;;;
+1F4DA;BOOKS;So;0;ON;;;;;N;;;;;
+1F4DB;NAME BADGE;So;0;ON;;;;;N;;;;;
+1F4DC;SCROLL;So;0;ON;;;;;N;;;;;
+1F4DD;MEMO;So;0;ON;;;;;N;;;;;
+1F4DE;TELEPHONE RECEIVER;So;0;ON;;;;;N;;;;;
+1F4DF;PAGER;So;0;ON;;;;;N;;;;;
+1F4E0;FAX MACHINE;So;0;ON;;;;;N;;;;;
+1F4E1;SATELLITE ANTENNA;So;0;ON;;;;;N;;;;;
+1F4E2;PUBLIC ADDRESS LOUDSPEAKER;So;0;ON;;;;;N;;;;;
+1F4E3;CHEERING MEGAPHONE;So;0;ON;;;;;N;;;;;
+1F4E4;OUTBOX TRAY;So;0;ON;;;;;N;;;;;
+1F4E5;INBOX TRAY;So;0;ON;;;;;N;;;;;
+1F4E6;PACKAGE;So;0;ON;;;;;N;;;;;
+1F4E7;E-MAIL SYMBOL;So;0;ON;;;;;N;;;;;
+1F4E8;INCOMING ENVELOPE;So;0;ON;;;;;N;;;;;
+1F4E9;ENVELOPE WITH DOWNWARDS ARROW ABOVE;So;0;ON;;;;;N;;;;;
+1F4EA;CLOSED MAILBOX WITH LOWERED FLAG;So;0;ON;;;;;N;;;;;
+1F4EB;CLOSED MAILBOX WITH RAISED FLAG;So;0;ON;;;;;N;;;;;
+1F4EC;OPEN MAILBOX WITH RAISED FLAG;So;0;ON;;;;;N;;;;;
+1F4ED;OPEN MAILBOX WITH LOWERED FLAG;So;0;ON;;;;;N;;;;;
+1F4EE;POSTBOX;So;0;ON;;;;;N;;;;;
+1F4EF;POSTAL HORN;So;0;ON;;;;;N;;;;;
+1F4F0;NEWSPAPER;So;0;ON;;;;;N;;;;;
+1F4F1;MOBILE PHONE;So;0;ON;;;;;N;;;;;
+1F4F2;MOBILE PHONE WITH RIGHTWARDS ARROW AT LEFT;So;0;ON;;;;;N;;;;;
+1F4F3;VIBRATION MODE;So;0;ON;;;;;N;;;;;
+1F4F4;MOBILE PHONE OFF;So;0;ON;;;;;N;;;;;
+1F4F5;NO MOBILE PHONES;So;0;ON;;;;;N;;;;;
+1F4F6;ANTENNA WITH BARS;So;0;ON;;;;;N;;;;;
+1F4F7;CAMERA;So;0;ON;;;;;N;;;;;
+1F4F8;CAMERA WITH FLASH;So;0;ON;;;;;N;;;;;
+1F4F9;VIDEO CAMERA;So;0;ON;;;;;N;;;;;
+1F4FA;TELEVISION;So;0;ON;;;;;N;;;;;
+1F4FB;RADIO;So;0;ON;;;;;N;;;;;
+1F4FC;VIDEOCASSETTE;So;0;ON;;;;;N;;;;;
+1F4FD;FILM PROJECTOR;So;0;ON;;;;;N;;;;;
+1F4FE;PORTABLE STEREO;So;0;ON;;;;;N;;;;;
+1F4FF;PRAYER BEADS;So;0;ON;;;;;N;;;;;
+1F500;TWISTED RIGHTWARDS ARROWS;So;0;ON;;;;;N;;;;;
+1F501;CLOCKWISE RIGHTWARDS AND LEFTWARDS OPEN CIRCLE ARROWS;So;0;ON;;;;;N;;;;;
+1F502;CLOCKWISE RIGHTWARDS AND LEFTWARDS OPEN CIRCLE ARROWS WITH CIRCLED ONE OVERLAY;So;0;ON;;;;;N;;;;;
+1F503;CLOCKWISE DOWNWARDS AND UPWARDS OPEN CIRCLE ARROWS;So;0;ON;;;;;N;;;;;
+1F504;ANTICLOCKWISE DOWNWARDS AND UPWARDS OPEN CIRCLE ARROWS;So;0;ON;;;;;N;;;;;
+1F505;LOW BRIGHTNESS SYMBOL;So;0;ON;;;;;N;;;;;
+1F506;HIGH BRIGHTNESS SYMBOL;So;0;ON;;;;;N;;;;;
+1F507;SPEAKER WITH CANCELLATION STROKE;So;0;ON;;;;;N;;;;;
+1F508;SPEAKER;So;0;ON;;;;;N;;;;;
+1F509;SPEAKER WITH ONE SOUND WAVE;So;0;ON;;;;;N;;;;;
+1F50A;SPEAKER WITH THREE SOUND WAVES;So;0;ON;;;;;N;;;;;
+1F50B;BATTERY;So;0;ON;;;;;N;;;;;
+1F50C;ELECTRIC PLUG;So;0;ON;;;;;N;;;;;
+1F50D;LEFT-POINTING MAGNIFYING GLASS;So;0;ON;;;;;N;;;;;
+1F50E;RIGHT-POINTING MAGNIFYING GLASS;So;0;ON;;;;;N;;;;;
+1F50F;LOCK WITH INK PEN;So;0;ON;;;;;N;;;;;
+1F510;CLOSED LOCK WITH KEY;So;0;ON;;;;;N;;;;;
+1F511;KEY;So;0;ON;;;;;N;;;;;
+1F512;LOCK;So;0;ON;;;;;N;;;;;
+1F513;OPEN LOCK;So;0;ON;;;;;N;;;;;
+1F514;BELL;So;0;ON;;;;;N;;;;;
+1F515;BELL WITH CANCELLATION STROKE;So;0;ON;;;;;N;;;;;
+1F516;BOOKMARK;So;0;ON;;;;;N;;;;;
+1F517;LINK SYMBOL;So;0;ON;;;;;N;;;;;
+1F518;RADIO BUTTON;So;0;ON;;;;;N;;;;;
+1F519;BACK WITH LEFTWARDS ARROW ABOVE;So;0;ON;;;;;N;;;;;
+1F51A;END WITH LEFTWARDS ARROW ABOVE;So;0;ON;;;;;N;;;;;
+1F51B;ON WITH EXCLAMATION MARK WITH LEFT RIGHT ARROW ABOVE;So;0;ON;;;;;N;;;;;
+1F51C;SOON WITH RIGHTWARDS ARROW ABOVE;So;0;ON;;;;;N;;;;;
+1F51D;TOP WITH UPWARDS ARROW ABOVE;So;0;ON;;;;;N;;;;;
+1F51E;NO ONE UNDER EIGHTEEN SYMBOL;So;0;ON;;;;;N;;;;;
+1F51F;KEYCAP TEN;So;0;ON;;;;;N;;;;;
+1F520;INPUT SYMBOL FOR LATIN CAPITAL LETTERS;So;0;ON;;;;;N;;;;;
+1F521;INPUT SYMBOL FOR LATIN SMALL LETTERS;So;0;ON;;;;;N;;;;;
+1F522;INPUT SYMBOL FOR NUMBERS;So;0;ON;;;;;N;;;;;
+1F523;INPUT SYMBOL FOR SYMBOLS;So;0;ON;;;;;N;;;;;
+1F524;INPUT SYMBOL FOR LATIN LETTERS;So;0;ON;;;;;N;;;;;
+1F525;FIRE;So;0;ON;;;;;N;;;;;
+1F526;ELECTRIC TORCH;So;0;ON;;;;;N;;;;;
+1F527;WRENCH;So;0;ON;;;;;N;;;;;
+1F528;HAMMER;So;0;ON;;;;;N;;;;;
+1F529;NUT AND BOLT;So;0;ON;;;;;N;;;;;
+1F52A;HOCHO;So;0;ON;;;;;N;;;;;
+1F52B;PISTOL;So;0;ON;;;;;N;;;;;
+1F52C;MICROSCOPE;So;0;ON;;;;;N;;;;;
+1F52D;TELESCOPE;So;0;ON;;;;;N;;;;;
+1F52E;CRYSTAL BALL;So;0;ON;;;;;N;;;;;
+1F52F;SIX POINTED STAR WITH MIDDLE DOT;So;0;ON;;;;;N;;;;;
+1F530;JAPANESE SYMBOL FOR BEGINNER;So;0;ON;;;;;N;;;;;
+1F531;TRIDENT EMBLEM;So;0;ON;;;;;N;;;;;
+1F532;BLACK SQUARE BUTTON;So;0;ON;;;;;N;;;;;
+1F533;WHITE SQUARE BUTTON;So;0;ON;;;;;N;;;;;
+1F534;LARGE RED CIRCLE;So;0;ON;;;;;N;;;;;
+1F535;LARGE BLUE CIRCLE;So;0;ON;;;;;N;;;;;
+1F536;LARGE ORANGE DIAMOND;So;0;ON;;;;;N;;;;;
+1F537;LARGE BLUE DIAMOND;So;0;ON;;;;;N;;;;;
+1F538;SMALL ORANGE DIAMOND;So;0;ON;;;;;N;;;;;
+1F539;SMALL BLUE DIAMOND;So;0;ON;;;;;N;;;;;
+1F53A;UP-POINTING RED TRIANGLE;So;0;ON;;;;;N;;;;;
+1F53B;DOWN-POINTING RED TRIANGLE;So;0;ON;;;;;N;;;;;
+1F53C;UP-POINTING SMALL RED TRIANGLE;So;0;ON;;;;;N;;;;;
+1F53D;DOWN-POINTING SMALL RED TRIANGLE;So;0;ON;;;;;N;;;;;
+1F53E;LOWER RIGHT SHADOWED WHITE CIRCLE;So;0;ON;;;;;N;;;;;
+1F53F;UPPER RIGHT SHADOWED WHITE CIRCLE;So;0;ON;;;;;N;;;;;
+1F540;CIRCLED CROSS POMMEE;So;0;ON;;;;;N;;;;;
+1F541;CROSS POMMEE WITH HALF-CIRCLE BELOW;So;0;ON;;;;;N;;;;;
+1F542;CROSS POMMEE;So;0;ON;;;;;N;;;;;
+1F543;NOTCHED LEFT SEMICIRCLE WITH THREE DOTS;So;0;ON;;;;;N;;;;;
+1F544;NOTCHED RIGHT SEMICIRCLE WITH THREE DOTS;So;0;ON;;;;;N;;;;;
+1F545;SYMBOL FOR MARKS CHAPTER;So;0;ON;;;;;N;;;;;
+1F546;WHITE LATIN CROSS;So;0;ON;;;;;N;;;;;
+1F547;HEAVY LATIN CROSS;So;0;ON;;;;;N;;;;;
+1F548;CELTIC CROSS;So;0;ON;;;;;N;;;;;
+1F549;OM SYMBOL;So;0;ON;;;;;N;;;;;
+1F54A;DOVE OF PEACE;So;0;ON;;;;;N;;;;;
+1F54B;KAABA;So;0;ON;;;;;N;;;;;
+1F54C;MOSQUE;So;0;ON;;;;;N;;;;;
+1F54D;SYNAGOGUE;So;0;ON;;;;;N;;;;;
+1F54E;MENORAH WITH NINE BRANCHES;So;0;ON;;;;;N;;;;;
+1F54F;BOWL OF HYGIEIA;So;0;ON;;;;;N;;;;;
+1F550;CLOCK FACE ONE OCLOCK;So;0;ON;;;;;N;;;;;
+1F551;CLOCK FACE TWO OCLOCK;So;0;ON;;;;;N;;;;;
+1F552;CLOCK FACE THREE OCLOCK;So;0;ON;;;;;N;;;;;
+1F553;CLOCK FACE FOUR OCLOCK;So;0;ON;;;;;N;;;;;
+1F554;CLOCK FACE FIVE OCLOCK;So;0;ON;;;;;N;;;;;
+1F555;CLOCK FACE SIX OCLOCK;So;0;ON;;;;;N;;;;;
+1F556;CLOCK FACE SEVEN OCLOCK;So;0;ON;;;;;N;;;;;
+1F557;CLOCK FACE EIGHT OCLOCK;So;0;ON;;;;;N;;;;;
+1F558;CLOCK FACE NINE OCLOCK;So;0;ON;;;;;N;;;;;
+1F559;CLOCK FACE TEN OCLOCK;So;0;ON;;;;;N;;;;;
+1F55A;CLOCK FACE ELEVEN OCLOCK;So;0;ON;;;;;N;;;;;
+1F55B;CLOCK FACE TWELVE OCLOCK;So;0;ON;;;;;N;;;;;
+1F55C;CLOCK FACE ONE-THIRTY;So;0;ON;;;;;N;;;;;
+1F55D;CLOCK FACE TWO-THIRTY;So;0;ON;;;;;N;;;;;
+1F55E;CLOCK FACE THREE-THIRTY;So;0;ON;;;;;N;;;;;
+1F55F;CLOCK FACE FOUR-THIRTY;So;0;ON;;;;;N;;;;;
+1F560;CLOCK FACE FIVE-THIRTY;So;0;ON;;;;;N;;;;;
+1F561;CLOCK FACE SIX-THIRTY;So;0;ON;;;;;N;;;;;
+1F562;CLOCK FACE SEVEN-THIRTY;So;0;ON;;;;;N;;;;;
+1F563;CLOCK FACE EIGHT-THIRTY;So;0;ON;;;;;N;;;;;
+1F564;CLOCK FACE NINE-THIRTY;So;0;ON;;;;;N;;;;;
+1F565;CLOCK FACE TEN-THIRTY;So;0;ON;;;;;N;;;;;
+1F566;CLOCK FACE ELEVEN-THIRTY;So;0;ON;;;;;N;;;;;
+1F567;CLOCK FACE TWELVE-THIRTY;So;0;ON;;;;;N;;;;;
+1F568;RIGHT SPEAKER;So;0;ON;;;;;N;;;;;
+1F569;RIGHT SPEAKER WITH ONE SOUND WAVE;So;0;ON;;;;;N;;;;;
+1F56A;RIGHT SPEAKER WITH THREE SOUND WAVES;So;0;ON;;;;;N;;;;;
+1F56B;BULLHORN;So;0;ON;;;;;N;;;;;
+1F56C;BULLHORN WITH SOUND WAVES;So;0;ON;;;;;N;;;;;
+1F56D;RINGING BELL;So;0;ON;;;;;N;;;;;
+1F56E;BOOK;So;0;ON;;;;;N;;;;;
+1F56F;CANDLE;So;0;ON;;;;;N;;;;;
+1F570;MANTELPIECE CLOCK;So;0;ON;;;;;N;;;;;
+1F571;BLACK SKULL AND CROSSBONES;So;0;ON;;;;;N;;;;;
+1F572;NO PIRACY;So;0;ON;;;;;N;;;;;
+1F573;HOLE;So;0;ON;;;;;N;;;;;
+1F574;MAN IN BUSINESS SUIT LEVITATING;So;0;ON;;;;;N;;;;;
+1F575;SLEUTH OR SPY;So;0;ON;;;;;N;;;;;
+1F576;DARK SUNGLASSES;So;0;ON;;;;;N;;;;;
+1F577;SPIDER;So;0;ON;;;;;N;;;;;
+1F578;SPIDER WEB;So;0;ON;;;;;N;;;;;
+1F579;JOYSTICK;So;0;ON;;;;;N;;;;;
+1F57A;MAN DANCING;So;0;ON;;;;;N;;;;;
+1F57B;LEFT HAND TELEPHONE RECEIVER;So;0;ON;;;;;N;;;;;
+1F57C;TELEPHONE RECEIVER WITH PAGE;So;0;ON;;;;;N;;;;;
+1F57D;RIGHT HAND TELEPHONE RECEIVER;So;0;ON;;;;;N;;;;;
+1F57E;WHITE TOUCHTONE TELEPHONE;So;0;ON;;;;;N;;;;;
+1F57F;BLACK TOUCHTONE TELEPHONE;So;0;ON;;;;;N;;;;;
+1F580;TELEPHONE ON TOP OF MODEM;So;0;ON;;;;;N;;;;;
+1F581;CLAMSHELL MOBILE PHONE;So;0;ON;;;;;N;;;;;
+1F582;BACK OF ENVELOPE;So;0;ON;;;;;N;;;;;
+1F583;STAMPED ENVELOPE;So;0;ON;;;;;N;;;;;
+1F584;ENVELOPE WITH LIGHTNING;So;0;ON;;;;;N;;;;;
+1F585;FLYING ENVELOPE;So;0;ON;;;;;N;;;;;
+1F586;PEN OVER STAMPED ENVELOPE;So;0;ON;;;;;N;;;;;
+1F587;LINKED PAPERCLIPS;So;0;ON;;;;;N;;;;;
+1F588;BLACK PUSHPIN;So;0;ON;;;;;N;;;;;
+1F589;LOWER LEFT PENCIL;So;0;ON;;;;;N;;;;;
+1F58A;LOWER LEFT BALLPOINT PEN;So;0;ON;;;;;N;;;;;
+1F58B;LOWER LEFT FOUNTAIN PEN;So;0;ON;;;;;N;;;;;
+1F58C;LOWER LEFT PAINTBRUSH;So;0;ON;;;;;N;;;;;
+1F58D;LOWER LEFT CRAYON;So;0;ON;;;;;N;;;;;
+1F58E;LEFT WRITING HAND;So;0;ON;;;;;N;;;;;
+1F58F;TURNED OK HAND SIGN;So;0;ON;;;;;N;;;;;
+1F590;RAISED HAND WITH FINGERS SPLAYED;So;0;ON;;;;;N;;;;;
+1F591;REVERSED RAISED HAND WITH FINGERS SPLAYED;So;0;ON;;;;;N;;;;;
+1F592;REVERSED THUMBS UP SIGN;So;0;ON;;;;;N;;;;;
+1F593;REVERSED THUMBS DOWN SIGN;So;0;ON;;;;;N;;;;;
+1F594;REVERSED VICTORY HAND;So;0;ON;;;;;N;;;;;
+1F595;REVERSED HAND WITH MIDDLE FINGER EXTENDED;So;0;ON;;;;;N;;;;;
+1F596;RAISED HAND WITH PART BETWEEN MIDDLE AND RING FINGERS;So;0;ON;;;;;N;;;;;
+1F597;WHITE DOWN POINTING LEFT HAND INDEX;So;0;ON;;;;;N;;;;;
+1F598;SIDEWAYS WHITE LEFT POINTING INDEX;So;0;ON;;;;;N;;;;;
+1F599;SIDEWAYS WHITE RIGHT POINTING INDEX;So;0;ON;;;;;N;;;;;
+1F59A;SIDEWAYS BLACK LEFT POINTING INDEX;So;0;ON;;;;;N;;;;;
+1F59B;SIDEWAYS BLACK RIGHT POINTING INDEX;So;0;ON;;;;;N;;;;;
+1F59C;BLACK LEFT POINTING BACKHAND INDEX;So;0;ON;;;;;N;;;;;
+1F59D;BLACK RIGHT POINTING BACKHAND INDEX;So;0;ON;;;;;N;;;;;
+1F59E;SIDEWAYS WHITE UP POINTING INDEX;So;0;ON;;;;;N;;;;;
+1F59F;SIDEWAYS WHITE DOWN POINTING INDEX;So;0;ON;;;;;N;;;;;
+1F5A0;SIDEWAYS BLACK UP POINTING INDEX;So;0;ON;;;;;N;;;;;
+1F5A1;SIDEWAYS BLACK DOWN POINTING INDEX;So;0;ON;;;;;N;;;;;
+1F5A2;BLACK UP POINTING BACKHAND INDEX;So;0;ON;;;;;N;;;;;
+1F5A3;BLACK DOWN POINTING BACKHAND INDEX;So;0;ON;;;;;N;;;;;
+1F5A4;BLACK HEART;So;0;ON;;;;;N;;;;;
+1F5A5;DESKTOP COMPUTER;So;0;ON;;;;;N;;;;;
+1F5A6;KEYBOARD AND MOUSE;So;0;ON;;;;;N;;;;;
+1F5A7;THREE NETWORKED COMPUTERS;So;0;ON;;;;;N;;;;;
+1F5A8;PRINTER;So;0;ON;;;;;N;;;;;
+1F5A9;POCKET CALCULATOR;So;0;ON;;;;;N;;;;;
+1F5AA;BLACK HARD SHELL FLOPPY DISK;So;0;ON;;;;;N;;;;;
+1F5AB;WHITE HARD SHELL FLOPPY DISK;So;0;ON;;;;;N;;;;;
+1F5AC;SOFT SHELL FLOPPY DISK;So;0;ON;;;;;N;;;;;
+1F5AD;TAPE CARTRIDGE;So;0;ON;;;;;N;;;;;
+1F5AE;WIRED KEYBOARD;So;0;ON;;;;;N;;;;;
+1F5AF;ONE BUTTON MOUSE;So;0;ON;;;;;N;;;;;
+1F5B0;TWO BUTTON MOUSE;So;0;ON;;;;;N;;;;;
+1F5B1;THREE BUTTON MOUSE;So;0;ON;;;;;N;;;;;
+1F5B2;TRACKBALL;So;0;ON;;;;;N;;;;;
+1F5B3;OLD PERSONAL COMPUTER;So;0;ON;;;;;N;;;;;
+1F5B4;HARD DISK;So;0;ON;;;;;N;;;;;
+1F5B5;SCREEN;So;0;ON;;;;;N;;;;;
+1F5B6;PRINTER ICON;So;0;ON;;;;;N;;;;;
+1F5B7;FAX ICON;So;0;ON;;;;;N;;;;;
+1F5B8;OPTICAL DISC ICON;So;0;ON;;;;;N;;;;;
+1F5B9;DOCUMENT WITH TEXT;So;0;ON;;;;;N;;;;;
+1F5BA;DOCUMENT WITH TEXT AND PICTURE;So;0;ON;;;;;N;;;;;
+1F5BB;DOCUMENT WITH PICTURE;So;0;ON;;;;;N;;;;;
+1F5BC;FRAME WITH PICTURE;So;0;ON;;;;;N;;;;;
+1F5BD;FRAME WITH TILES;So;0;ON;;;;;N;;;;;
+1F5BE;FRAME WITH AN X;So;0;ON;;;;;N;;;;;
+1F5BF;BLACK FOLDER;So;0;ON;;;;;N;;;;;
+1F5C0;FOLDER;So;0;ON;;;;;N;;;;;
+1F5C1;OPEN FOLDER;So;0;ON;;;;;N;;;;;
+1F5C2;CARD INDEX DIVIDERS;So;0;ON;;;;;N;;;;;
+1F5C3;CARD FILE BOX;So;0;ON;;;;;N;;;;;
+1F5C4;FILE CABINET;So;0;ON;;;;;N;;;;;
+1F5C5;EMPTY NOTE;So;0;ON;;;;;N;;;;;
+1F5C6;EMPTY NOTE PAGE;So;0;ON;;;;;N;;;;;
+1F5C7;EMPTY NOTE PAD;So;0;ON;;;;;N;;;;;
+1F5C8;NOTE;So;0;ON;;;;;N;;;;;
+1F5C9;NOTE PAGE;So;0;ON;;;;;N;;;;;
+1F5CA;NOTE PAD;So;0;ON;;;;;N;;;;;
+1F5CB;EMPTY DOCUMENT;So;0;ON;;;;;N;;;;;
+1F5CC;EMPTY PAGE;So;0;ON;;;;;N;;;;;
+1F5CD;EMPTY PAGES;So;0;ON;;;;;N;;;;;
+1F5CE;DOCUMENT;So;0;ON;;;;;N;;;;;
+1F5CF;PAGE;So;0;ON;;;;;N;;;;;
+1F5D0;PAGES;So;0;ON;;;;;N;;;;;
+1F5D1;WASTEBASKET;So;0;ON;;;;;N;;;;;
+1F5D2;SPIRAL NOTE PAD;So;0;ON;;;;;N;;;;;
+1F5D3;SPIRAL CALENDAR PAD;So;0;ON;;;;;N;;;;;
+1F5D4;DESKTOP WINDOW;So;0;ON;;;;;N;;;;;
+1F5D5;MINIMIZE;So;0;ON;;;;;N;;;;;
+1F5D6;MAXIMIZE;So;0;ON;;;;;N;;;;;
+1F5D7;OVERLAP;So;0;ON;;;;;N;;;;;
+1F5D8;CLOCKWISE RIGHT AND LEFT SEMICIRCLE ARROWS;So;0;ON;;;;;N;;;;;
+1F5D9;CANCELLATION X;So;0;ON;;;;;N;;;;;
+1F5DA;INCREASE FONT SIZE SYMBOL;So;0;ON;;;;;N;;;;;
+1F5DB;DECREASE FONT SIZE SYMBOL;So;0;ON;;;;;N;;;;;
+1F5DC;COMPRESSION;So;0;ON;;;;;N;;;;;
+1F5DD;OLD KEY;So;0;ON;;;;;N;;;;;
+1F5DE;ROLLED-UP NEWSPAPER;So;0;ON;;;;;N;;;;;
+1F5DF;PAGE WITH CIRCLED TEXT;So;0;ON;;;;;N;;;;;
+1F5E0;STOCK CHART;So;0;ON;;;;;N;;;;;
+1F5E1;DAGGER KNIFE;So;0;ON;;;;;N;;;;;
+1F5E2;LIPS;So;0;ON;;;;;N;;;;;
+1F5E3;SPEAKING HEAD IN SILHOUETTE;So;0;ON;;;;;N;;;;;
+1F5E4;THREE RAYS ABOVE;So;0;ON;;;;;N;;;;;
+1F5E5;THREE RAYS BELOW;So;0;ON;;;;;N;;;;;
+1F5E6;THREE RAYS LEFT;So;0;ON;;;;;N;;;;;
+1F5E7;THREE RAYS RIGHT;So;0;ON;;;;;N;;;;;
+1F5E8;LEFT SPEECH BUBBLE;So;0;ON;;;;;N;;;;;
+1F5E9;RIGHT SPEECH BUBBLE;So;0;ON;;;;;N;;;;;
+1F5EA;TWO SPEECH BUBBLES;So;0;ON;;;;;N;;;;;
+1F5EB;THREE SPEECH BUBBLES;So;0;ON;;;;;N;;;;;
+1F5EC;LEFT THOUGHT BUBBLE;So;0;ON;;;;;N;;;;;
+1F5ED;RIGHT THOUGHT BUBBLE;So;0;ON;;;;;N;;;;;
+1F5EE;LEFT ANGER BUBBLE;So;0;ON;;;;;N;;;;;
+1F5EF;RIGHT ANGER BUBBLE;So;0;ON;;;;;N;;;;;
+1F5F0;MOOD BUBBLE;So;0;ON;;;;;N;;;;;
+1F5F1;LIGHTNING MOOD BUBBLE;So;0;ON;;;;;N;;;;;
+1F5F2;LIGHTNING MOOD;So;0;ON;;;;;N;;;;;
+1F5F3;BALLOT BOX WITH BALLOT;So;0;ON;;;;;N;;;;;
+1F5F4;BALLOT SCRIPT X;So;0;ON;;;;;N;;;;;
+1F5F5;BALLOT BOX WITH SCRIPT X;So;0;ON;;;;;N;;;;;
+1F5F6;BALLOT BOLD SCRIPT X;So;0;ON;;;;;N;;;;;
+1F5F7;BALLOT BOX WITH BOLD SCRIPT X;So;0;ON;;;;;N;;;;;
+1F5F8;LIGHT CHECK MARK;So;0;ON;;;;;N;;;;;
+1F5F9;BALLOT BOX WITH BOLD CHECK;So;0;ON;;;;;N;;;;;
+1F5FA;WORLD MAP;So;0;ON;;;;;N;;;;;
+1F5FB;MOUNT FUJI;So;0;ON;;;;;N;;;;;
+1F5FC;TOKYO TOWER;So;0;ON;;;;;N;;;;;
+1F5FD;STATUE OF LIBERTY;So;0;ON;;;;;N;;;;;
+1F5FE;SILHOUETTE OF JAPAN;So;0;ON;;;;;N;;;;;
+1F5FF;MOYAI;So;0;ON;;;;;N;;;;;
+1F600;GRINNING FACE;So;0;ON;;;;;N;;;;;
+1F601;GRINNING FACE WITH SMILING EYES;So;0;ON;;;;;N;;;;;
+1F602;FACE WITH TEARS OF JOY;So;0;ON;;;;;N;;;;;
+1F603;SMILING FACE WITH OPEN MOUTH;So;0;ON;;;;;N;;;;;
+1F604;SMILING FACE WITH OPEN MOUTH AND SMILING EYES;So;0;ON;;;;;N;;;;;
+1F605;SMILING FACE WITH OPEN MOUTH AND COLD SWEAT;So;0;ON;;;;;N;;;;;
+1F606;SMILING FACE WITH OPEN MOUTH AND TIGHTLY-CLOSED EYES;So;0;ON;;;;;N;;;;;
+1F607;SMILING FACE WITH HALO;So;0;ON;;;;;N;;;;;
+1F608;SMILING FACE WITH HORNS;So;0;ON;;;;;N;;;;;
+1F609;WINKING FACE;So;0;ON;;;;;N;;;;;
+1F60A;SMILING FACE WITH SMILING EYES;So;0;ON;;;;;N;;;;;
+1F60B;FACE SAVOURING DELICIOUS FOOD;So;0;ON;;;;;N;;;;;
+1F60C;RELIEVED FACE;So;0;ON;;;;;N;;;;;
+1F60D;SMILING FACE WITH HEART-SHAPED EYES;So;0;ON;;;;;N;;;;;
+1F60E;SMILING FACE WITH SUNGLASSES;So;0;ON;;;;;N;;;;;
+1F60F;SMIRKING FACE;So;0;ON;;;;;N;;;;;
+1F610;NEUTRAL FACE;So;0;ON;;;;;N;;;;;
+1F611;EXPRESSIONLESS FACE;So;0;ON;;;;;N;;;;;
+1F612;UNAMUSED FACE;So;0;ON;;;;;N;;;;;
+1F613;FACE WITH COLD SWEAT;So;0;ON;;;;;N;;;;;
+1F614;PENSIVE FACE;So;0;ON;;;;;N;;;;;
+1F615;CONFUSED FACE;So;0;ON;;;;;N;;;;;
+1F616;CONFOUNDED FACE;So;0;ON;;;;;N;;;;;
+1F617;KISSING FACE;So;0;ON;;;;;N;;;;;
+1F618;FACE THROWING A KISS;So;0;ON;;;;;N;;;;;
+1F619;KISSING FACE WITH SMILING EYES;So;0;ON;;;;;N;;;;;
+1F61A;KISSING FACE WITH CLOSED EYES;So;0;ON;;;;;N;;;;;
+1F61B;FACE WITH STUCK-OUT TONGUE;So;0;ON;;;;;N;;;;;
+1F61C;FACE WITH STUCK-OUT TONGUE AND WINKING EYE;So;0;ON;;;;;N;;;;;
+1F61D;FACE WITH STUCK-OUT TONGUE AND TIGHTLY-CLOSED EYES;So;0;ON;;;;;N;;;;;
+1F61E;DISAPPOINTED FACE;So;0;ON;;;;;N;;;;;
+1F61F;WORRIED FACE;So;0;ON;;;;;N;;;;;
+1F620;ANGRY FACE;So;0;ON;;;;;N;;;;;
+1F621;POUTING FACE;So;0;ON;;;;;N;;;;;
+1F622;CRYING FACE;So;0;ON;;;;;N;;;;;
+1F623;PERSEVERING FACE;So;0;ON;;;;;N;;;;;
+1F624;FACE WITH LOOK OF TRIUMPH;So;0;ON;;;;;N;;;;;
+1F625;DISAPPOINTED BUT RELIEVED FACE;So;0;ON;;;;;N;;;;;
+1F626;FROWNING FACE WITH OPEN MOUTH;So;0;ON;;;;;N;;;;;
+1F627;ANGUISHED FACE;So;0;ON;;;;;N;;;;;
+1F628;FEARFUL FACE;So;0;ON;;;;;N;;;;;
+1F629;WEARY FACE;So;0;ON;;;;;N;;;;;
+1F62A;SLEEPY FACE;So;0;ON;;;;;N;;;;;
+1F62B;TIRED FACE;So;0;ON;;;;;N;;;;;
+1F62C;GRIMACING FACE;So;0;ON;;;;;N;;;;;
+1F62D;LOUDLY CRYING FACE;So;0;ON;;;;;N;;;;;
+1F62E;FACE WITH OPEN MOUTH;So;0;ON;;;;;N;;;;;
+1F62F;HUSHED FACE;So;0;ON;;;;;N;;;;;
+1F630;FACE WITH OPEN MOUTH AND COLD SWEAT;So;0;ON;;;;;N;;;;;
+1F631;FACE SCREAMING IN FEAR;So;0;ON;;;;;N;;;;;
+1F632;ASTONISHED FACE;So;0;ON;;;;;N;;;;;
+1F633;FLUSHED FACE;So;0;ON;;;;;N;;;;;
+1F634;SLEEPING FACE;So;0;ON;;;;;N;;;;;
+1F635;DIZZY FACE;So;0;ON;;;;;N;;;;;
+1F636;FACE WITHOUT MOUTH;So;0;ON;;;;;N;;;;;
+1F637;FACE WITH MEDICAL MASK;So;0;ON;;;;;N;;;;;
+1F638;GRINNING CAT FACE WITH SMILING EYES;So;0;ON;;;;;N;;;;;
+1F639;CAT FACE WITH TEARS OF JOY;So;0;ON;;;;;N;;;;;
+1F63A;SMILING CAT FACE WITH OPEN MOUTH;So;0;ON;;;;;N;;;;;
+1F63B;SMILING CAT FACE WITH HEART-SHAPED EYES;So;0;ON;;;;;N;;;;;
+1F63C;CAT FACE WITH WRY SMILE;So;0;ON;;;;;N;;;;;
+1F63D;KISSING CAT FACE WITH CLOSED EYES;So;0;ON;;;;;N;;;;;
+1F63E;POUTING CAT FACE;So;0;ON;;;;;N;;;;;
+1F63F;CRYING CAT FACE;So;0;ON;;;;;N;;;;;
+1F640;WEARY CAT FACE;So;0;ON;;;;;N;;;;;
+1F641;SLIGHTLY FROWNING FACE;So;0;ON;;;;;N;;;;;
+1F642;SLIGHTLY SMILING FACE;So;0;ON;;;;;N;;;;;
+1F643;UPSIDE-DOWN FACE;So;0;ON;;;;;N;;;;;
+1F644;FACE WITH ROLLING EYES;So;0;ON;;;;;N;;;;;
+1F645;FACE WITH NO GOOD GESTURE;So;0;ON;;;;;N;;;;;
+1F646;FACE WITH OK GESTURE;So;0;ON;;;;;N;;;;;
+1F647;PERSON BOWING DEEPLY;So;0;ON;;;;;N;;;;;
+1F648;SEE-NO-EVIL MONKEY;So;0;ON;;;;;N;;;;;
+1F649;HEAR-NO-EVIL MONKEY;So;0;ON;;;;;N;;;;;
+1F64A;SPEAK-NO-EVIL MONKEY;So;0;ON;;;;;N;;;;;
+1F64B;HAPPY PERSON RAISING ONE HAND;So;0;ON;;;;;N;;;;;
+1F64C;PERSON RAISING BOTH HANDS IN CELEBRATION;So;0;ON;;;;;N;;;;;
+1F64D;PERSON FROWNING;So;0;ON;;;;;N;;;;;
+1F64E;PERSON WITH POUTING FACE;So;0;ON;;;;;N;;;;;
+1F64F;PERSON WITH FOLDED HANDS;So;0;ON;;;;;N;;;;;
+1F650;NORTH WEST POINTING LEAF;So;0;ON;;;;;N;;;;;
+1F651;SOUTH WEST POINTING LEAF;So;0;ON;;;;;N;;;;;
+1F652;NORTH EAST POINTING LEAF;So;0;ON;;;;;N;;;;;
+1F653;SOUTH EAST POINTING LEAF;So;0;ON;;;;;N;;;;;
+1F654;TURNED NORTH WEST POINTING LEAF;So;0;ON;;;;;N;;;;;
+1F655;TURNED SOUTH WEST POINTING LEAF;So;0;ON;;;;;N;;;;;
+1F656;TURNED NORTH EAST POINTING LEAF;So;0;ON;;;;;N;;;;;
+1F657;TURNED SOUTH EAST POINTING LEAF;So;0;ON;;;;;N;;;;;
+1F658;NORTH WEST POINTING VINE LEAF;So;0;ON;;;;;N;;;;;
+1F659;SOUTH WEST POINTING VINE LEAF;So;0;ON;;;;;N;;;;;
+1F65A;NORTH EAST POINTING VINE LEAF;So;0;ON;;;;;N;;;;;
+1F65B;SOUTH EAST POINTING VINE LEAF;So;0;ON;;;;;N;;;;;
+1F65C;HEAVY NORTH WEST POINTING VINE LEAF;So;0;ON;;;;;N;;;;;
+1F65D;HEAVY SOUTH WEST POINTING VINE LEAF;So;0;ON;;;;;N;;;;;
+1F65E;HEAVY NORTH EAST POINTING VINE LEAF;So;0;ON;;;;;N;;;;;
+1F65F;HEAVY SOUTH EAST POINTING VINE LEAF;So;0;ON;;;;;N;;;;;
+1F660;NORTH WEST POINTING BUD;So;0;ON;;;;;N;;;;;
+1F661;SOUTH WEST POINTING BUD;So;0;ON;;;;;N;;;;;
+1F662;NORTH EAST POINTING BUD;So;0;ON;;;;;N;;;;;
+1F663;SOUTH EAST POINTING BUD;So;0;ON;;;;;N;;;;;
+1F664;HEAVY NORTH WEST POINTING BUD;So;0;ON;;;;;N;;;;;
+1F665;HEAVY SOUTH WEST POINTING BUD;So;0;ON;;;;;N;;;;;
+1F666;HEAVY NORTH EAST POINTING BUD;So;0;ON;;;;;N;;;;;
+1F667;HEAVY SOUTH EAST POINTING BUD;So;0;ON;;;;;N;;;;;
+1F668;HOLLOW QUILT SQUARE ORNAMENT;So;0;ON;;;;;N;;;;;
+1F669;HOLLOW QUILT SQUARE ORNAMENT IN BLACK SQUARE;So;0;ON;;;;;N;;;;;
+1F66A;SOLID QUILT SQUARE ORNAMENT;So;0;ON;;;;;N;;;;;
+1F66B;SOLID QUILT SQUARE ORNAMENT IN BLACK SQUARE;So;0;ON;;;;;N;;;;;
+1F66C;LEFTWARDS ROCKET;So;0;ON;;;;;N;;;;;
+1F66D;UPWARDS ROCKET;So;0;ON;;;;;N;;;;;
+1F66E;RIGHTWARDS ROCKET;So;0;ON;;;;;N;;;;;
+1F66F;DOWNWARDS ROCKET;So;0;ON;;;;;N;;;;;
+1F670;SCRIPT LIGATURE ET ORNAMENT;So;0;ON;;;;;N;;;;;
+1F671;HEAVY SCRIPT LIGATURE ET ORNAMENT;So;0;ON;;;;;N;;;;;
+1F672;LIGATURE OPEN ET ORNAMENT;So;0;ON;;;;;N;;;;;
+1F673;HEAVY LIGATURE OPEN ET ORNAMENT;So;0;ON;;;;;N;;;;;
+1F674;HEAVY AMPERSAND ORNAMENT;So;0;ON;;;;;N;;;;;
+1F675;SWASH AMPERSAND ORNAMENT;So;0;ON;;;;;N;;;;;
+1F676;SANS-SERIF HEAVY DOUBLE TURNED COMMA QUOTATION MARK ORNAMENT;So;0;ON;;;;;N;;;;;
+1F677;SANS-SERIF HEAVY DOUBLE COMMA QUOTATION MARK ORNAMENT;So;0;ON;;;;;N;;;;;
+1F678;SANS-SERIF HEAVY LOW DOUBLE COMMA QUOTATION MARK ORNAMENT;So;0;ON;;;;;N;;;;;
+1F679;HEAVY INTERROBANG ORNAMENT;So;0;ON;;;;;N;;;;;
+1F67A;SANS-SERIF INTERROBANG ORNAMENT;So;0;ON;;;;;N;;;;;
+1F67B;HEAVY SANS-SERIF INTERROBANG ORNAMENT;So;0;ON;;;;;N;;;;;
+1F67C;VERY HEAVY SOLIDUS;So;0;ON;;;;;N;;;;;
+1F67D;VERY HEAVY REVERSE SOLIDUS;So;0;ON;;;;;N;;;;;
+1F67E;CHECKER BOARD;So;0;ON;;;;;N;;;;;
+1F67F;REVERSE CHECKER BOARD;So;0;ON;;;;;N;;;;;
+1F680;ROCKET;So;0;ON;;;;;N;;;;;
+1F681;HELICOPTER;So;0;ON;;;;;N;;;;;
+1F682;STEAM LOCOMOTIVE;So;0;ON;;;;;N;;;;;
+1F683;RAILWAY CAR;So;0;ON;;;;;N;;;;;
+1F684;HIGH-SPEED TRAIN;So;0;ON;;;;;N;;;;;
+1F685;HIGH-SPEED TRAIN WITH BULLET NOSE;So;0;ON;;;;;N;;;;;
+1F686;TRAIN;So;0;ON;;;;;N;;;;;
+1F687;METRO;So;0;ON;;;;;N;;;;;
+1F688;LIGHT RAIL;So;0;ON;;;;;N;;;;;
+1F689;STATION;So;0;ON;;;;;N;;;;;
+1F68A;TRAM;So;0;ON;;;;;N;;;;;
+1F68B;TRAM CAR;So;0;ON;;;;;N;;;;;
+1F68C;BUS;So;0;ON;;;;;N;;;;;
+1F68D;ONCOMING BUS;So;0;ON;;;;;N;;;;;
+1F68E;TROLLEYBUS;So;0;ON;;;;;N;;;;;
+1F68F;BUS STOP;So;0;ON;;;;;N;;;;;
+1F690;MINIBUS;So;0;ON;;;;;N;;;;;
+1F691;AMBULANCE;So;0;ON;;;;;N;;;;;
+1F692;FIRE ENGINE;So;0;ON;;;;;N;;;;;
+1F693;POLICE CAR;So;0;ON;;;;;N;;;;;
+1F694;ONCOMING POLICE CAR;So;0;ON;;;;;N;;;;;
+1F695;TAXI;So;0;ON;;;;;N;;;;;
+1F696;ONCOMING TAXI;So;0;ON;;;;;N;;;;;
+1F697;AUTOMOBILE;So;0;ON;;;;;N;;;;;
+1F698;ONCOMING AUTOMOBILE;So;0;ON;;;;;N;;;;;
+1F699;RECREATIONAL VEHICLE;So;0;ON;;;;;N;;;;;
+1F69A;DELIVERY TRUCK;So;0;ON;;;;;N;;;;;
+1F69B;ARTICULATED LORRY;So;0;ON;;;;;N;;;;;
+1F69C;TRACTOR;So;0;ON;;;;;N;;;;;
+1F69D;MONORAIL;So;0;ON;;;;;N;;;;;
+1F69E;MOUNTAIN RAILWAY;So;0;ON;;;;;N;;;;;
+1F69F;SUSPENSION RAILWAY;So;0;ON;;;;;N;;;;;
+1F6A0;MOUNTAIN CABLEWAY;So;0;ON;;;;;N;;;;;
+1F6A1;AERIAL TRAMWAY;So;0;ON;;;;;N;;;;;
+1F6A2;SHIP;So;0;ON;;;;;N;;;;;
+1F6A3;ROWBOAT;So;0;ON;;;;;N;;;;;
+1F6A4;SPEEDBOAT;So;0;ON;;;;;N;;;;;
+1F6A5;HORIZONTAL TRAFFIC LIGHT;So;0;ON;;;;;N;;;;;
+1F6A6;VERTICAL TRAFFIC LIGHT;So;0;ON;;;;;N;;;;;
+1F6A7;CONSTRUCTION SIGN;So;0;ON;;;;;N;;;;;
+1F6A8;POLICE CARS REVOLVING LIGHT;So;0;ON;;;;;N;;;;;
+1F6A9;TRIANGULAR FLAG ON POST;So;0;ON;;;;;N;;;;;
+1F6AA;DOOR;So;0;ON;;;;;N;;;;;
+1F6AB;NO ENTRY SIGN;So;0;ON;;;;;N;;;;;
+1F6AC;SMOKING SYMBOL;So;0;ON;;;;;N;;;;;
+1F6AD;NO SMOKING SYMBOL;So;0;ON;;;;;N;;;;;
+1F6AE;PUT LITTER IN ITS PLACE SYMBOL;So;0;ON;;;;;N;;;;;
+1F6AF;DO NOT LITTER SYMBOL;So;0;ON;;;;;N;;;;;
+1F6B0;POTABLE WATER SYMBOL;So;0;ON;;;;;N;;;;;
+1F6B1;NON-POTABLE WATER SYMBOL;So;0;ON;;;;;N;;;;;
+1F6B2;BICYCLE;So;0;ON;;;;;N;;;;;
+1F6B3;NO BICYCLES;So;0;ON;;;;;N;;;;;
+1F6B4;BICYCLIST;So;0;ON;;;;;N;;;;;
+1F6B5;MOUNTAIN BICYCLIST;So;0;ON;;;;;N;;;;;
+1F6B6;PEDESTRIAN;So;0;ON;;;;;N;;;;;
+1F6B7;NO PEDESTRIANS;So;0;ON;;;;;N;;;;;
+1F6B8;CHILDREN CROSSING;So;0;ON;;;;;N;;;;;
+1F6B9;MENS SYMBOL;So;0;ON;;;;;N;;;;;
+1F6BA;WOMENS SYMBOL;So;0;ON;;;;;N;;;;;
+1F6BB;RESTROOM;So;0;ON;;;;;N;;;;;
+1F6BC;BABY SYMBOL;So;0;ON;;;;;N;;;;;
+1F6BD;TOILET;So;0;ON;;;;;N;;;;;
+1F6BE;WATER CLOSET;So;0;ON;;;;;N;;;;;
+1F6BF;SHOWER;So;0;ON;;;;;N;;;;;
+1F6C0;BATH;So;0;ON;;;;;N;;;;;
+1F6C1;BATHTUB;So;0;ON;;;;;N;;;;;
+1F6C2;PASSPORT CONTROL;So;0;ON;;;;;N;;;;;
+1F6C3;CUSTOMS;So;0;ON;;;;;N;;;;;
+1F6C4;BAGGAGE CLAIM;So;0;ON;;;;;N;;;;;
+1F6C5;LEFT LUGGAGE;So;0;ON;;;;;N;;;;;
+1F6C6;TRIANGLE WITH ROUNDED CORNERS;So;0;ON;;;;;N;;;;;
+1F6C7;PROHIBITED SIGN;So;0;ON;;;;;N;;;;;
+1F6C8;CIRCLED INFORMATION SOURCE;So;0;ON;;;;;N;;;;;
+1F6C9;BOYS SYMBOL;So;0;ON;;;;;N;;;;;
+1F6CA;GIRLS SYMBOL;So;0;ON;;;;;N;;;;;
+1F6CB;COUCH AND LAMP;So;0;ON;;;;;N;;;;;
+1F6CC;SLEEPING ACCOMMODATION;So;0;ON;;;;;N;;;;;
+1F6CD;SHOPPING BAGS;So;0;ON;;;;;N;;;;;
+1F6CE;BELLHOP BELL;So;0;ON;;;;;N;;;;;
+1F6CF;BED;So;0;ON;;;;;N;;;;;
+1F6D0;PLACE OF WORSHIP;So;0;ON;;;;;N;;;;;
+1F6D1;OCTAGONAL SIGN;So;0;ON;;;;;N;;;;;
+1F6D2;SHOPPING TROLLEY;So;0;ON;;;;;N;;;;;
+1F6E0;HAMMER AND WRENCH;So;0;ON;;;;;N;;;;;
+1F6E1;SHIELD;So;0;ON;;;;;N;;;;;
+1F6E2;OIL DRUM;So;0;ON;;;;;N;;;;;
+1F6E3;MOTORWAY;So;0;ON;;;;;N;;;;;
+1F6E4;RAILWAY TRACK;So;0;ON;;;;;N;;;;;
+1F6E5;MOTOR BOAT;So;0;ON;;;;;N;;;;;
+1F6E6;UP-POINTING MILITARY AIRPLANE;So;0;ON;;;;;N;;;;;
+1F6E7;UP-POINTING AIRPLANE;So;0;ON;;;;;N;;;;;
+1F6E8;UP-POINTING SMALL AIRPLANE;So;0;ON;;;;;N;;;;;
+1F6E9;SMALL AIRPLANE;So;0;ON;;;;;N;;;;;
+1F6EA;NORTHEAST-POINTING AIRPLANE;So;0;ON;;;;;N;;;;;
+1F6EB;AIRPLANE DEPARTURE;So;0;ON;;;;;N;;;;;
+1F6EC;AIRPLANE ARRIVING;So;0;ON;;;;;N;;;;;
+1F6F0;SATELLITE;So;0;ON;;;;;N;;;;;
+1F6F1;ONCOMING FIRE ENGINE;So;0;ON;;;;;N;;;;;
+1F6F2;DIESEL LOCOMOTIVE;So;0;ON;;;;;N;;;;;
+1F6F3;PASSENGER SHIP;So;0;ON;;;;;N;;;;;
+1F6F4;SCOOTER;So;0;ON;;;;;N;;;;;
+1F6F5;MOTOR SCOOTER;So;0;ON;;;;;N;;;;;
+1F6F6;CANOE;So;0;ON;;;;;N;;;;;
+1F700;ALCHEMICAL SYMBOL FOR QUINTESSENCE;So;0;ON;;;;;N;;;;;
+1F701;ALCHEMICAL SYMBOL FOR AIR;So;0;ON;;;;;N;;;;;
+1F702;ALCHEMICAL SYMBOL FOR FIRE;So;0;ON;;;;;N;;;;;
+1F703;ALCHEMICAL SYMBOL FOR EARTH;So;0;ON;;;;;N;;;;;
+1F704;ALCHEMICAL SYMBOL FOR WATER;So;0;ON;;;;;N;;;;;
+1F705;ALCHEMICAL SYMBOL FOR AQUAFORTIS;So;0;ON;;;;;N;;;;;
+1F706;ALCHEMICAL SYMBOL FOR AQUA REGIA;So;0;ON;;;;;N;;;;;
+1F707;ALCHEMICAL SYMBOL FOR AQUA REGIA-2;So;0;ON;;;;;N;;;;;
+1F708;ALCHEMICAL SYMBOL FOR AQUA VITAE;So;0;ON;;;;;N;;;;;
+1F709;ALCHEMICAL SYMBOL FOR AQUA VITAE-2;So;0;ON;;;;;N;;;;;
+1F70A;ALCHEMICAL SYMBOL FOR VINEGAR;So;0;ON;;;;;N;;;;;
+1F70B;ALCHEMICAL SYMBOL FOR VINEGAR-2;So;0;ON;;;;;N;;;;;
+1F70C;ALCHEMICAL SYMBOL FOR VINEGAR-3;So;0;ON;;;;;N;;;;;
+1F70D;ALCHEMICAL SYMBOL FOR SULFUR;So;0;ON;;;;;N;;;;;
+1F70E;ALCHEMICAL SYMBOL FOR PHILOSOPHERS SULFUR;So;0;ON;;;;;N;;;;;
+1F70F;ALCHEMICAL SYMBOL FOR BLACK SULFUR;So;0;ON;;;;;N;;;;;
+1F710;ALCHEMICAL SYMBOL FOR MERCURY SUBLIMATE;So;0;ON;;;;;N;;;;;
+1F711;ALCHEMICAL SYMBOL FOR MERCURY SUBLIMATE-2;So;0;ON;;;;;N;;;;;
+1F712;ALCHEMICAL SYMBOL FOR MERCURY SUBLIMATE-3;So;0;ON;;;;;N;;;;;
+1F713;ALCHEMICAL SYMBOL FOR CINNABAR;So;0;ON;;;;;N;;;;;
+1F714;ALCHEMICAL SYMBOL FOR SALT;So;0;ON;;;;;N;;;;;
+1F715;ALCHEMICAL SYMBOL FOR NITRE;So;0;ON;;;;;N;;;;;
+1F716;ALCHEMICAL SYMBOL FOR VITRIOL;So;0;ON;;;;;N;;;;;
+1F717;ALCHEMICAL SYMBOL FOR VITRIOL-2;So;0;ON;;;;;N;;;;;
+1F718;ALCHEMICAL SYMBOL FOR ROCK SALT;So;0;ON;;;;;N;;;;;
+1F719;ALCHEMICAL SYMBOL FOR ROCK SALT-2;So;0;ON;;;;;N;;;;;
+1F71A;ALCHEMICAL SYMBOL FOR GOLD;So;0;ON;;;;;N;;;;;
+1F71B;ALCHEMICAL SYMBOL FOR SILVER;So;0;ON;;;;;N;;;;;
+1F71C;ALCHEMICAL SYMBOL FOR IRON ORE;So;0;ON;;;;;N;;;;;
+1F71D;ALCHEMICAL SYMBOL FOR IRON ORE-2;So;0;ON;;;;;N;;;;;
+1F71E;ALCHEMICAL SYMBOL FOR CROCUS OF IRON;So;0;ON;;;;;N;;;;;
+1F71F;ALCHEMICAL SYMBOL FOR REGULUS OF IRON;So;0;ON;;;;;N;;;;;
+1F720;ALCHEMICAL SYMBOL FOR COPPER ORE;So;0;ON;;;;;N;;;;;
+1F721;ALCHEMICAL SYMBOL FOR IRON-COPPER ORE;So;0;ON;;;;;N;;;;;
+1F722;ALCHEMICAL SYMBOL FOR SUBLIMATE OF COPPER;So;0;ON;;;;;N;;;;;
+1F723;ALCHEMICAL SYMBOL FOR CROCUS OF COPPER;So;0;ON;;;;;N;;;;;
+1F724;ALCHEMICAL SYMBOL FOR CROCUS OF COPPER-2;So;0;ON;;;;;N;;;;;
+1F725;ALCHEMICAL SYMBOL FOR COPPER ANTIMONIATE;So;0;ON;;;;;N;;;;;
+1F726;ALCHEMICAL SYMBOL FOR SALT OF COPPER ANTIMONIATE;So;0;ON;;;;;N;;;;;
+1F727;ALCHEMICAL SYMBOL FOR SUBLIMATE OF SALT OF COPPER;So;0;ON;;;;;N;;;;;
+1F728;ALCHEMICAL SYMBOL FOR VERDIGRIS;So;0;ON;;;;;N;;;;;
+1F729;ALCHEMICAL SYMBOL FOR TIN ORE;So;0;ON;;;;;N;;;;;
+1F72A;ALCHEMICAL SYMBOL FOR LEAD ORE;So;0;ON;;;;;N;;;;;
+1F72B;ALCHEMICAL SYMBOL FOR ANTIMONY ORE;So;0;ON;;;;;N;;;;;
+1F72C;ALCHEMICAL SYMBOL FOR SUBLIMATE OF ANTIMONY;So;0;ON;;;;;N;;;;;
+1F72D;ALCHEMICAL SYMBOL FOR SALT OF ANTIMONY;So;0;ON;;;;;N;;;;;
+1F72E;ALCHEMICAL SYMBOL FOR SUBLIMATE OF SALT OF ANTIMONY;So;0;ON;;;;;N;;;;;
+1F72F;ALCHEMICAL SYMBOL FOR VINEGAR OF ANTIMONY;So;0;ON;;;;;N;;;;;
+1F730;ALCHEMICAL SYMBOL FOR REGULUS OF ANTIMONY;So;0;ON;;;;;N;;;;;
+1F731;ALCHEMICAL SYMBOL FOR REGULUS OF ANTIMONY-2;So;0;ON;;;;;N;;;;;
+1F732;ALCHEMICAL SYMBOL FOR REGULUS;So;0;ON;;;;;N;;;;;
+1F733;ALCHEMICAL SYMBOL FOR REGULUS-2;So;0;ON;;;;;N;;;;;
+1F734;ALCHEMICAL SYMBOL FOR REGULUS-3;So;0;ON;;;;;N;;;;;
+1F735;ALCHEMICAL SYMBOL FOR REGULUS-4;So;0;ON;;;;;N;;;;;
+1F736;ALCHEMICAL SYMBOL FOR ALKALI;So;0;ON;;;;;N;;;;;
+1F737;ALCHEMICAL SYMBOL FOR ALKALI-2;So;0;ON;;;;;N;;;;;
+1F738;ALCHEMICAL SYMBOL FOR MARCASITE;So;0;ON;;;;;N;;;;;
+1F739;ALCHEMICAL SYMBOL FOR SAL-AMMONIAC;So;0;ON;;;;;N;;;;;
+1F73A;ALCHEMICAL SYMBOL FOR ARSENIC;So;0;ON;;;;;N;;;;;
+1F73B;ALCHEMICAL SYMBOL FOR REALGAR;So;0;ON;;;;;N;;;;;
+1F73C;ALCHEMICAL SYMBOL FOR REALGAR-2;So;0;ON;;;;;N;;;;;
+1F73D;ALCHEMICAL SYMBOL FOR AURIPIGMENT;So;0;ON;;;;;N;;;;;
+1F73E;ALCHEMICAL SYMBOL FOR BISMUTH ORE;So;0;ON;;;;;N;;;;;
+1F73F;ALCHEMICAL SYMBOL FOR TARTAR;So;0;ON;;;;;N;;;;;
+1F740;ALCHEMICAL SYMBOL FOR TARTAR-2;So;0;ON;;;;;N;;;;;
+1F741;ALCHEMICAL SYMBOL FOR QUICK LIME;So;0;ON;;;;;N;;;;;
+1F742;ALCHEMICAL SYMBOL FOR BORAX;So;0;ON;;;;;N;;;;;
+1F743;ALCHEMICAL SYMBOL FOR BORAX-2;So;0;ON;;;;;N;;;;;
+1F744;ALCHEMICAL SYMBOL FOR BORAX-3;So;0;ON;;;;;N;;;;;
+1F745;ALCHEMICAL SYMBOL FOR ALUM;So;0;ON;;;;;N;;;;;
+1F746;ALCHEMICAL SYMBOL FOR OIL;So;0;ON;;;;;N;;;;;
+1F747;ALCHEMICAL SYMBOL FOR SPIRIT;So;0;ON;;;;;N;;;;;
+1F748;ALCHEMICAL SYMBOL FOR TINCTURE;So;0;ON;;;;;N;;;;;
+1F749;ALCHEMICAL SYMBOL FOR GUM;So;0;ON;;;;;N;;;;;
+1F74A;ALCHEMICAL SYMBOL FOR WAX;So;0;ON;;;;;N;;;;;
+1F74B;ALCHEMICAL SYMBOL FOR POWDER;So;0;ON;;;;;N;;;;;
+1F74C;ALCHEMICAL SYMBOL FOR CALX;So;0;ON;;;;;N;;;;;
+1F74D;ALCHEMICAL SYMBOL FOR TUTTY;So;0;ON;;;;;N;;;;;
+1F74E;ALCHEMICAL SYMBOL FOR CAPUT MORTUUM;So;0;ON;;;;;N;;;;;
+1F74F;ALCHEMICAL SYMBOL FOR SCEPTER OF JOVE;So;0;ON;;;;;N;;;;;
+1F750;ALCHEMICAL SYMBOL FOR CADUCEUS;So;0;ON;;;;;N;;;;;
+1F751;ALCHEMICAL SYMBOL FOR TRIDENT;So;0;ON;;;;;N;;;;;
+1F752;ALCHEMICAL SYMBOL FOR STARRED TRIDENT;So;0;ON;;;;;N;;;;;
+1F753;ALCHEMICAL SYMBOL FOR LODESTONE;So;0;ON;;;;;N;;;;;
+1F754;ALCHEMICAL SYMBOL FOR SOAP;So;0;ON;;;;;N;;;;;
+1F755;ALCHEMICAL SYMBOL FOR URINE;So;0;ON;;;;;N;;;;;
+1F756;ALCHEMICAL SYMBOL FOR HORSE DUNG;So;0;ON;;;;;N;;;;;
+1F757;ALCHEMICAL SYMBOL FOR ASHES;So;0;ON;;;;;N;;;;;
+1F758;ALCHEMICAL SYMBOL FOR POT ASHES;So;0;ON;;;;;N;;;;;
+1F759;ALCHEMICAL SYMBOL FOR BRICK;So;0;ON;;;;;N;;;;;
+1F75A;ALCHEMICAL SYMBOL FOR POWDERED BRICK;So;0;ON;;;;;N;;;;;
+1F75B;ALCHEMICAL SYMBOL FOR AMALGAM;So;0;ON;;;;;N;;;;;
+1F75C;ALCHEMICAL SYMBOL FOR STRATUM SUPER STRATUM;So;0;ON;;;;;N;;;;;
+1F75D;ALCHEMICAL SYMBOL FOR STRATUM SUPER STRATUM-2;So;0;ON;;;;;N;;;;;
+1F75E;ALCHEMICAL SYMBOL FOR SUBLIMATION;So;0;ON;;;;;N;;;;;
+1F75F;ALCHEMICAL SYMBOL FOR PRECIPITATE;So;0;ON;;;;;N;;;;;
+1F760;ALCHEMICAL SYMBOL FOR DISTILL;So;0;ON;;;;;N;;;;;
+1F761;ALCHEMICAL SYMBOL FOR DISSOLVE;So;0;ON;;;;;N;;;;;
+1F762;ALCHEMICAL SYMBOL FOR DISSOLVE-2;So;0;ON;;;;;N;;;;;
+1F763;ALCHEMICAL SYMBOL FOR PURIFY;So;0;ON;;;;;N;;;;;
+1F764;ALCHEMICAL SYMBOL FOR PUTREFACTION;So;0;ON;;;;;N;;;;;
+1F765;ALCHEMICAL SYMBOL FOR CRUCIBLE;So;0;ON;;;;;N;;;;;
+1F766;ALCHEMICAL SYMBOL FOR CRUCIBLE-2;So;0;ON;;;;;N;;;;;
+1F767;ALCHEMICAL SYMBOL FOR CRUCIBLE-3;So;0;ON;;;;;N;;;;;
+1F768;ALCHEMICAL SYMBOL FOR CRUCIBLE-4;So;0;ON;;;;;N;;;;;
+1F769;ALCHEMICAL SYMBOL FOR CRUCIBLE-5;So;0;ON;;;;;N;;;;;
+1F76A;ALCHEMICAL SYMBOL FOR ALEMBIC;So;0;ON;;;;;N;;;;;
+1F76B;ALCHEMICAL SYMBOL FOR BATH OF MARY;So;0;ON;;;;;N;;;;;
+1F76C;ALCHEMICAL SYMBOL FOR BATH OF VAPOURS;So;0;ON;;;;;N;;;;;
+1F76D;ALCHEMICAL SYMBOL FOR RETORT;So;0;ON;;;;;N;;;;;
+1F76E;ALCHEMICAL SYMBOL FOR HOUR;So;0;ON;;;;;N;;;;;
+1F76F;ALCHEMICAL SYMBOL FOR NIGHT;So;0;ON;;;;;N;;;;;
+1F770;ALCHEMICAL SYMBOL FOR DAY-NIGHT;So;0;ON;;;;;N;;;;;
+1F771;ALCHEMICAL SYMBOL FOR MONTH;So;0;ON;;;;;N;;;;;
+1F772;ALCHEMICAL SYMBOL FOR HALF DRAM;So;0;ON;;;;;N;;;;;
+1F773;ALCHEMICAL SYMBOL FOR HALF OUNCE;So;0;ON;;;;;N;;;;;
+1F780;BLACK LEFT-POINTING ISOSCELES RIGHT TRIANGLE;So;0;ON;;;;;N;;;;;
+1F781;BLACK UP-POINTING ISOSCELES RIGHT TRIANGLE;So;0;ON;;;;;N;;;;;
+1F782;BLACK RIGHT-POINTING ISOSCELES RIGHT TRIANGLE;So;0;ON;;;;;N;;;;;
+1F783;BLACK DOWN-POINTING ISOSCELES RIGHT TRIANGLE;So;0;ON;;;;;N;;;;;
+1F784;BLACK SLIGHTLY SMALL CIRCLE;So;0;ON;;;;;N;;;;;
+1F785;MEDIUM BOLD WHITE CIRCLE;So;0;ON;;;;;N;;;;;
+1F786;BOLD WHITE CIRCLE;So;0;ON;;;;;N;;;;;
+1F787;HEAVY WHITE CIRCLE;So;0;ON;;;;;N;;;;;
+1F788;VERY HEAVY WHITE CIRCLE;So;0;ON;;;;;N;;;;;
+1F789;EXTREMELY HEAVY WHITE CIRCLE;So;0;ON;;;;;N;;;;;
+1F78A;WHITE CIRCLE CONTAINING BLACK SMALL CIRCLE;So;0;ON;;;;;N;;;;;
+1F78B;ROUND TARGET;So;0;ON;;;;;N;;;;;
+1F78C;BLACK TINY SQUARE;So;0;ON;;;;;N;;;;;
+1F78D;BLACK SLIGHTLY SMALL SQUARE;So;0;ON;;;;;N;;;;;
+1F78E;LIGHT WHITE SQUARE;So;0;ON;;;;;N;;;;;
+1F78F;MEDIUM WHITE SQUARE;So;0;ON;;;;;N;;;;;
+1F790;BOLD WHITE SQUARE;So;0;ON;;;;;N;;;;;
+1F791;HEAVY WHITE SQUARE;So;0;ON;;;;;N;;;;;
+1F792;VERY HEAVY WHITE SQUARE;So;0;ON;;;;;N;;;;;
+1F793;EXTREMELY HEAVY WHITE SQUARE;So;0;ON;;;;;N;;;;;
+1F794;WHITE SQUARE CONTAINING BLACK VERY SMALL SQUARE;So;0;ON;;;;;N;;;;;
+1F795;WHITE SQUARE CONTAINING BLACK MEDIUM SQUARE;So;0;ON;;;;;N;;;;;
+1F796;SQUARE TARGET;So;0;ON;;;;;N;;;;;
+1F797;BLACK TINY DIAMOND;So;0;ON;;;;;N;;;;;
+1F798;BLACK VERY SMALL DIAMOND;So;0;ON;;;;;N;;;;;
+1F799;BLACK MEDIUM SMALL DIAMOND;So;0;ON;;;;;N;;;;;
+1F79A;WHITE DIAMOND CONTAINING BLACK VERY SMALL DIAMOND;So;0;ON;;;;;N;;;;;
+1F79B;WHITE DIAMOND CONTAINING BLACK MEDIUM DIAMOND;So;0;ON;;;;;N;;;;;
+1F79C;DIAMOND TARGET;So;0;ON;;;;;N;;;;;
+1F79D;BLACK TINY LOZENGE;So;0;ON;;;;;N;;;;;
+1F79E;BLACK VERY SMALL LOZENGE;So;0;ON;;;;;N;;;;;
+1F79F;BLACK MEDIUM SMALL LOZENGE;So;0;ON;;;;;N;;;;;
+1F7A0;WHITE LOZENGE CONTAINING BLACK SMALL LOZENGE;So;0;ON;;;;;N;;;;;
+1F7A1;THIN GREEK CROSS;So;0;ON;;;;;N;;;;;
+1F7A2;LIGHT GREEK CROSS;So;0;ON;;;;;N;;;;;
+1F7A3;MEDIUM GREEK CROSS;So;0;ON;;;;;N;;;;;
+1F7A4;BOLD GREEK CROSS;So;0;ON;;;;;N;;;;;
+1F7A5;VERY BOLD GREEK CROSS;So;0;ON;;;;;N;;;;;
+1F7A6;VERY HEAVY GREEK CROSS;So;0;ON;;;;;N;;;;;
+1F7A7;EXTREMELY HEAVY GREEK CROSS;So;0;ON;;;;;N;;;;;
+1F7A8;THIN SALTIRE;So;0;ON;;;;;N;;;;;
+1F7A9;LIGHT SALTIRE;So;0;ON;;;;;N;;;;;
+1F7AA;MEDIUM SALTIRE;So;0;ON;;;;;N;;;;;
+1F7AB;BOLD SALTIRE;So;0;ON;;;;;N;;;;;
+1F7AC;HEAVY SALTIRE;So;0;ON;;;;;N;;;;;
+1F7AD;VERY HEAVY SALTIRE;So;0;ON;;;;;N;;;;;
+1F7AE;EXTREMELY HEAVY SALTIRE;So;0;ON;;;;;N;;;;;
+1F7AF;LIGHT FIVE SPOKED ASTERISK;So;0;ON;;;;;N;;;;;
+1F7B0;MEDIUM FIVE SPOKED ASTERISK;So;0;ON;;;;;N;;;;;
+1F7B1;BOLD FIVE SPOKED ASTERISK;So;0;ON;;;;;N;;;;;
+1F7B2;HEAVY FIVE SPOKED ASTERISK;So;0;ON;;;;;N;;;;;
+1F7B3;VERY HEAVY FIVE SPOKED ASTERISK;So;0;ON;;;;;N;;;;;
+1F7B4;EXTREMELY HEAVY FIVE SPOKED ASTERISK;So;0;ON;;;;;N;;;;;
+1F7B5;LIGHT SIX SPOKED ASTERISK;So;0;ON;;;;;N;;;;;
+1F7B6;MEDIUM SIX SPOKED ASTERISK;So;0;ON;;;;;N;;;;;
+1F7B7;BOLD SIX SPOKED ASTERISK;So;0;ON;;;;;N;;;;;
+1F7B8;HEAVY SIX SPOKED ASTERISK;So;0;ON;;;;;N;;;;;
+1F7B9;VERY HEAVY SIX SPOKED ASTERISK;So;0;ON;;;;;N;;;;;
+1F7BA;EXTREMELY HEAVY SIX SPOKED ASTERISK;So;0;ON;;;;;N;;;;;
+1F7BB;LIGHT EIGHT SPOKED ASTERISK;So;0;ON;;;;;N;;;;;
+1F7BC;MEDIUM EIGHT SPOKED ASTERISK;So;0;ON;;;;;N;;;;;
+1F7BD;BOLD EIGHT SPOKED ASTERISK;So;0;ON;;;;;N;;;;;
+1F7BE;HEAVY EIGHT SPOKED ASTERISK;So;0;ON;;;;;N;;;;;
+1F7BF;VERY HEAVY EIGHT SPOKED ASTERISK;So;0;ON;;;;;N;;;;;
+1F7C0;LIGHT THREE POINTED BLACK STAR;So;0;ON;;;;;N;;;;;
+1F7C1;MEDIUM THREE POINTED BLACK STAR;So;0;ON;;;;;N;;;;;
+1F7C2;THREE POINTED BLACK STAR;So;0;ON;;;;;N;;;;;
+1F7C3;MEDIUM THREE POINTED PINWHEEL STAR;So;0;ON;;;;;N;;;;;
+1F7C4;LIGHT FOUR POINTED BLACK STAR;So;0;ON;;;;;N;;;;;
+1F7C5;MEDIUM FOUR POINTED BLACK STAR;So;0;ON;;;;;N;;;;;
+1F7C6;FOUR POINTED BLACK STAR;So;0;ON;;;;;N;;;;;
+1F7C7;MEDIUM FOUR POINTED PINWHEEL STAR;So;0;ON;;;;;N;;;;;
+1F7C8;REVERSE LIGHT FOUR POINTED PINWHEEL STAR;So;0;ON;;;;;N;;;;;
+1F7C9;LIGHT FIVE POINTED BLACK STAR;So;0;ON;;;;;N;;;;;
+1F7CA;HEAVY FIVE POINTED BLACK STAR;So;0;ON;;;;;N;;;;;
+1F7CB;MEDIUM SIX POINTED BLACK STAR;So;0;ON;;;;;N;;;;;
+1F7CC;HEAVY SIX POINTED BLACK STAR;So;0;ON;;;;;N;;;;;
+1F7CD;SIX POINTED PINWHEEL STAR;So;0;ON;;;;;N;;;;;
+1F7CE;MEDIUM EIGHT POINTED BLACK STAR;So;0;ON;;;;;N;;;;;
+1F7CF;HEAVY EIGHT POINTED BLACK STAR;So;0;ON;;;;;N;;;;;
+1F7D0;VERY HEAVY EIGHT POINTED BLACK STAR;So;0;ON;;;;;N;;;;;
+1F7D1;HEAVY EIGHT POINTED PINWHEEL STAR;So;0;ON;;;;;N;;;;;
+1F7D2;LIGHT TWELVE POINTED BLACK STAR;So;0;ON;;;;;N;;;;;
+1F7D3;HEAVY TWELVE POINTED BLACK STAR;So;0;ON;;;;;N;;;;;
+1F7D4;HEAVY TWELVE POINTED PINWHEEL STAR;So;0;ON;;;;;N;;;;;
+1F800;LEFTWARDS ARROW WITH SMALL TRIANGLE ARROWHEAD;So;0;ON;;;;;N;;;;;
+1F801;UPWARDS ARROW WITH SMALL TRIANGLE ARROWHEAD;So;0;ON;;;;;N;;;;;
+1F802;RIGHTWARDS ARROW WITH SMALL TRIANGLE ARROWHEAD;So;0;ON;;;;;N;;;;;
+1F803;DOWNWARDS ARROW WITH SMALL TRIANGLE ARROWHEAD;So;0;ON;;;;;N;;;;;
+1F804;LEFTWARDS ARROW WITH MEDIUM TRIANGLE ARROWHEAD;So;0;ON;;;;;N;;;;;
+1F805;UPWARDS ARROW WITH MEDIUM TRIANGLE ARROWHEAD;So;0;ON;;;;;N;;;;;
+1F806;RIGHTWARDS ARROW WITH MEDIUM TRIANGLE ARROWHEAD;So;0;ON;;;;;N;;;;;
+1F807;DOWNWARDS ARROW WITH MEDIUM TRIANGLE ARROWHEAD;So;0;ON;;;;;N;;;;;
+1F808;LEFTWARDS ARROW WITH LARGE TRIANGLE ARROWHEAD;So;0;ON;;;;;N;;;;;
+1F809;UPWARDS ARROW WITH LARGE TRIANGLE ARROWHEAD;So;0;ON;;;;;N;;;;;
+1F80A;RIGHTWARDS ARROW WITH LARGE TRIANGLE ARROWHEAD;So;0;ON;;;;;N;;;;;
+1F80B;DOWNWARDS ARROW WITH LARGE TRIANGLE ARROWHEAD;So;0;ON;;;;;N;;;;;
+1F810;LEFTWARDS ARROW WITH SMALL EQUILATERAL ARROWHEAD;So;0;ON;;;;;N;;;;;
+1F811;UPWARDS ARROW WITH SMALL EQUILATERAL ARROWHEAD;So;0;ON;;;;;N;;;;;
+1F812;RIGHTWARDS ARROW WITH SMALL EQUILATERAL ARROWHEAD;So;0;ON;;;;;N;;;;;
+1F813;DOWNWARDS ARROW WITH SMALL EQUILATERAL ARROWHEAD;So;0;ON;;;;;N;;;;;
+1F814;LEFTWARDS ARROW WITH EQUILATERAL ARROWHEAD;So;0;ON;;;;;N;;;;;
+1F815;UPWARDS ARROW WITH EQUILATERAL ARROWHEAD;So;0;ON;;;;;N;;;;;
+1F816;RIGHTWARDS ARROW WITH EQUILATERAL ARROWHEAD;So;0;ON;;;;;N;;;;;
+1F817;DOWNWARDS ARROW WITH EQUILATERAL ARROWHEAD;So;0;ON;;;;;N;;;;;
+1F818;HEAVY LEFTWARDS ARROW WITH EQUILATERAL ARROWHEAD;So;0;ON;;;;;N;;;;;
+1F819;HEAVY UPWARDS ARROW WITH EQUILATERAL ARROWHEAD;So;0;ON;;;;;N;;;;;
+1F81A;HEAVY RIGHTWARDS ARROW WITH EQUILATERAL ARROWHEAD;So;0;ON;;;;;N;;;;;
+1F81B;HEAVY DOWNWARDS ARROW WITH EQUILATERAL ARROWHEAD;So;0;ON;;;;;N;;;;;
+1F81C;HEAVY LEFTWARDS ARROW WITH LARGE EQUILATERAL ARROWHEAD;So;0;ON;;;;;N;;;;;
+1F81D;HEAVY UPWARDS ARROW WITH LARGE EQUILATERAL ARROWHEAD;So;0;ON;;;;;N;;;;;
+1F81E;HEAVY RIGHTWARDS ARROW WITH LARGE EQUILATERAL ARROWHEAD;So;0;ON;;;;;N;;;;;
+1F81F;HEAVY DOWNWARDS ARROW WITH LARGE EQUILATERAL ARROWHEAD;So;0;ON;;;;;N;;;;;
+1F820;LEFTWARDS TRIANGLE-HEADED ARROW WITH NARROW SHAFT;So;0;ON;;;;;N;;;;;
+1F821;UPWARDS TRIANGLE-HEADED ARROW WITH NARROW SHAFT;So;0;ON;;;;;N;;;;;
+1F822;RIGHTWARDS TRIANGLE-HEADED ARROW WITH NARROW SHAFT;So;0;ON;;;;;N;;;;;
+1F823;DOWNWARDS TRIANGLE-HEADED ARROW WITH NARROW SHAFT;So;0;ON;;;;;N;;;;;
+1F824;LEFTWARDS TRIANGLE-HEADED ARROW WITH MEDIUM SHAFT;So;0;ON;;;;;N;;;;;
+1F825;UPWARDS TRIANGLE-HEADED ARROW WITH MEDIUM SHAFT;So;0;ON;;;;;N;;;;;
+1F826;RIGHTWARDS TRIANGLE-HEADED ARROW WITH MEDIUM SHAFT;So;0;ON;;;;;N;;;;;
+1F827;DOWNWARDS TRIANGLE-HEADED ARROW WITH MEDIUM SHAFT;So;0;ON;;;;;N;;;;;
+1F828;LEFTWARDS TRIANGLE-HEADED ARROW WITH BOLD SHAFT;So;0;ON;;;;;N;;;;;
+1F829;UPWARDS TRIANGLE-HEADED ARROW WITH BOLD SHAFT;So;0;ON;;;;;N;;;;;
+1F82A;RIGHTWARDS TRIANGLE-HEADED ARROW WITH BOLD SHAFT;So;0;ON;;;;;N;;;;;
+1F82B;DOWNWARDS TRIANGLE-HEADED ARROW WITH BOLD SHAFT;So;0;ON;;;;;N;;;;;
+1F82C;LEFTWARDS TRIANGLE-HEADED ARROW WITH HEAVY SHAFT;So;0;ON;;;;;N;;;;;
+1F82D;UPWARDS TRIANGLE-HEADED ARROW WITH HEAVY SHAFT;So;0;ON;;;;;N;;;;;
+1F82E;RIGHTWARDS TRIANGLE-HEADED ARROW WITH HEAVY SHAFT;So;0;ON;;;;;N;;;;;
+1F82F;DOWNWARDS TRIANGLE-HEADED ARROW WITH HEAVY SHAFT;So;0;ON;;;;;N;;;;;
+1F830;LEFTWARDS TRIANGLE-HEADED ARROW WITH VERY HEAVY SHAFT;So;0;ON;;;;;N;;;;;
+1F831;UPWARDS TRIANGLE-HEADED ARROW WITH VERY HEAVY SHAFT;So;0;ON;;;;;N;;;;;
+1F832;RIGHTWARDS TRIANGLE-HEADED ARROW WITH VERY HEAVY SHAFT;So;0;ON;;;;;N;;;;;
+1F833;DOWNWARDS TRIANGLE-HEADED ARROW WITH VERY HEAVY SHAFT;So;0;ON;;;;;N;;;;;
+1F834;LEFTWARDS FINGER-POST ARROW;So;0;ON;;;;;N;;;;;
+1F835;UPWARDS FINGER-POST ARROW;So;0;ON;;;;;N;;;;;
+1F836;RIGHTWARDS FINGER-POST ARROW;So;0;ON;;;;;N;;;;;
+1F837;DOWNWARDS FINGER-POST ARROW;So;0;ON;;;;;N;;;;;
+1F838;LEFTWARDS SQUARED ARROW;So;0;ON;;;;;N;;;;;
+1F839;UPWARDS SQUARED ARROW;So;0;ON;;;;;N;;;;;
+1F83A;RIGHTWARDS SQUARED ARROW;So;0;ON;;;;;N;;;;;
+1F83B;DOWNWARDS SQUARED ARROW;So;0;ON;;;;;N;;;;;
+1F83C;LEFTWARDS COMPRESSED ARROW;So;0;ON;;;;;N;;;;;
+1F83D;UPWARDS COMPRESSED ARROW;So;0;ON;;;;;N;;;;;
+1F83E;RIGHTWARDS COMPRESSED ARROW;So;0;ON;;;;;N;;;;;
+1F83F;DOWNWARDS COMPRESSED ARROW;So;0;ON;;;;;N;;;;;
+1F840;LEFTWARDS HEAVY COMPRESSED ARROW;So;0;ON;;;;;N;;;;;
+1F841;UPWARDS HEAVY COMPRESSED ARROW;So;0;ON;;;;;N;;;;;
+1F842;RIGHTWARDS HEAVY COMPRESSED ARROW;So;0;ON;;;;;N;;;;;
+1F843;DOWNWARDS HEAVY COMPRESSED ARROW;So;0;ON;;;;;N;;;;;
+1F844;LEFTWARDS HEAVY ARROW;So;0;ON;;;;;N;;;;;
+1F845;UPWARDS HEAVY ARROW;So;0;ON;;;;;N;;;;;
+1F846;RIGHTWARDS HEAVY ARROW;So;0;ON;;;;;N;;;;;
+1F847;DOWNWARDS HEAVY ARROW;So;0;ON;;;;;N;;;;;
+1F850;LEFTWARDS SANS-SERIF ARROW;So;0;ON;;;;;N;;;;;
+1F851;UPWARDS SANS-SERIF ARROW;So;0;ON;;;;;N;;;;;
+1F852;RIGHTWARDS SANS-SERIF ARROW;So;0;ON;;;;;N;;;;;
+1F853;DOWNWARDS SANS-SERIF ARROW;So;0;ON;;;;;N;;;;;
+1F854;NORTH WEST SANS-SERIF ARROW;So;0;ON;;;;;N;;;;;
+1F855;NORTH EAST SANS-SERIF ARROW;So;0;ON;;;;;N;;;;;
+1F856;SOUTH EAST SANS-SERIF ARROW;So;0;ON;;;;;N;;;;;
+1F857;SOUTH WEST SANS-SERIF ARROW;So;0;ON;;;;;N;;;;;
+1F858;LEFT RIGHT SANS-SERIF ARROW;So;0;ON;;;;;N;;;;;
+1F859;UP DOWN SANS-SERIF ARROW;So;0;ON;;;;;N;;;;;
+1F860;WIDE-HEADED LEFTWARDS LIGHT BARB ARROW;So;0;ON;;;;;N;;;;;
+1F861;WIDE-HEADED UPWARDS LIGHT BARB ARROW;So;0;ON;;;;;N;;;;;
+1F862;WIDE-HEADED RIGHTWARDS LIGHT BARB ARROW;So;0;ON;;;;;N;;;;;
+1F863;WIDE-HEADED DOWNWARDS LIGHT BARB ARROW;So;0;ON;;;;;N;;;;;
+1F864;WIDE-HEADED NORTH WEST LIGHT BARB ARROW;So;0;ON;;;;;N;;;;;
+1F865;WIDE-HEADED NORTH EAST LIGHT BARB ARROW;So;0;ON;;;;;N;;;;;
+1F866;WIDE-HEADED SOUTH EAST LIGHT BARB ARROW;So;0;ON;;;;;N;;;;;
+1F867;WIDE-HEADED SOUTH WEST LIGHT BARB ARROW;So;0;ON;;;;;N;;;;;
+1F868;WIDE-HEADED LEFTWARDS BARB ARROW;So;0;ON;;;;;N;;;;;
+1F869;WIDE-HEADED UPWARDS BARB ARROW;So;0;ON;;;;;N;;;;;
+1F86A;WIDE-HEADED RIGHTWARDS BARB ARROW;So;0;ON;;;;;N;;;;;
+1F86B;WIDE-HEADED DOWNWARDS BARB ARROW;So;0;ON;;;;;N;;;;;
+1F86C;WIDE-HEADED NORTH WEST BARB ARROW;So;0;ON;;;;;N;;;;;
+1F86D;WIDE-HEADED NORTH EAST BARB ARROW;So;0;ON;;;;;N;;;;;
+1F86E;WIDE-HEADED SOUTH EAST BARB ARROW;So;0;ON;;;;;N;;;;;
+1F86F;WIDE-HEADED SOUTH WEST BARB ARROW;So;0;ON;;;;;N;;;;;
+1F870;WIDE-HEADED LEFTWARDS MEDIUM BARB ARROW;So;0;ON;;;;;N;;;;;
+1F871;WIDE-HEADED UPWARDS MEDIUM BARB ARROW;So;0;ON;;;;;N;;;;;
+1F872;WIDE-HEADED RIGHTWARDS MEDIUM BARB ARROW;So;0;ON;;;;;N;;;;;
+1F873;WIDE-HEADED DOWNWARDS MEDIUM BARB ARROW;So;0;ON;;;;;N;;;;;
+1F874;WIDE-HEADED NORTH WEST MEDIUM BARB ARROW;So;0;ON;;;;;N;;;;;
+1F875;WIDE-HEADED NORTH EAST MEDIUM BARB ARROW;So;0;ON;;;;;N;;;;;
+1F876;WIDE-HEADED SOUTH EAST MEDIUM BARB ARROW;So;0;ON;;;;;N;;;;;
+1F877;WIDE-HEADED SOUTH WEST MEDIUM BARB ARROW;So;0;ON;;;;;N;;;;;
+1F878;WIDE-HEADED LEFTWARDS HEAVY BARB ARROW;So;0;ON;;;;;N;;;;;
+1F879;WIDE-HEADED UPWARDS HEAVY BARB ARROW;So;0;ON;;;;;N;;;;;
+1F87A;WIDE-HEADED RIGHTWARDS HEAVY BARB ARROW;So;0;ON;;;;;N;;;;;
+1F87B;WIDE-HEADED DOWNWARDS HEAVY BARB ARROW;So;0;ON;;;;;N;;;;;
+1F87C;WIDE-HEADED NORTH WEST HEAVY BARB ARROW;So;0;ON;;;;;N;;;;;
+1F87D;WIDE-HEADED NORTH EAST HEAVY BARB ARROW;So;0;ON;;;;;N;;;;;
+1F87E;WIDE-HEADED SOUTH EAST HEAVY BARB ARROW;So;0;ON;;;;;N;;;;;
+1F87F;WIDE-HEADED SOUTH WEST HEAVY BARB ARROW;So;0;ON;;;;;N;;;;;
+1F880;WIDE-HEADED LEFTWARDS VERY HEAVY BARB ARROW;So;0;ON;;;;;N;;;;;
+1F881;WIDE-HEADED UPWARDS VERY HEAVY BARB ARROW;So;0;ON;;;;;N;;;;;
+1F882;WIDE-HEADED RIGHTWARDS VERY HEAVY BARB ARROW;So;0;ON;;;;;N;;;;;
+1F883;WIDE-HEADED DOWNWARDS VERY HEAVY BARB ARROW;So;0;ON;;;;;N;;;;;
+1F884;WIDE-HEADED NORTH WEST VERY HEAVY BARB ARROW;So;0;ON;;;;;N;;;;;
+1F885;WIDE-HEADED NORTH EAST VERY HEAVY BARB ARROW;So;0;ON;;;;;N;;;;;
+1F886;WIDE-HEADED SOUTH EAST VERY HEAVY BARB ARROW;So;0;ON;;;;;N;;;;;
+1F887;WIDE-HEADED SOUTH WEST VERY HEAVY BARB ARROW;So;0;ON;;;;;N;;;;;
+1F890;LEFTWARDS TRIANGLE ARROWHEAD;So;0;ON;;;;;N;;;;;
+1F891;UPWARDS TRIANGLE ARROWHEAD;So;0;ON;;;;;N;;;;;
+1F892;RIGHTWARDS TRIANGLE ARROWHEAD;So;0;ON;;;;;N;;;;;
+1F893;DOWNWARDS TRIANGLE ARROWHEAD;So;0;ON;;;;;N;;;;;
+1F894;LEFTWARDS WHITE ARROW WITHIN TRIANGLE ARROWHEAD;So;0;ON;;;;;N;;;;;
+1F895;UPWARDS WHITE ARROW WITHIN TRIANGLE ARROWHEAD;So;0;ON;;;;;N;;;;;
+1F896;RIGHTWARDS WHITE ARROW WITHIN TRIANGLE ARROWHEAD;So;0;ON;;;;;N;;;;;
+1F897;DOWNWARDS WHITE ARROW WITHIN TRIANGLE ARROWHEAD;So;0;ON;;;;;N;;;;;
+1F898;LEFTWARDS ARROW WITH NOTCHED TAIL;So;0;ON;;;;;N;;;;;
+1F899;UPWARDS ARROW WITH NOTCHED TAIL;So;0;ON;;;;;N;;;;;
+1F89A;RIGHTWARDS ARROW WITH NOTCHED TAIL;So;0;ON;;;;;N;;;;;
+1F89B;DOWNWARDS ARROW WITH NOTCHED TAIL;So;0;ON;;;;;N;;;;;
+1F89C;HEAVY ARROW SHAFT WIDTH ONE;So;0;ON;;;;;N;;;;;
+1F89D;HEAVY ARROW SHAFT WIDTH TWO THIRDS;So;0;ON;;;;;N;;;;;
+1F89E;HEAVY ARROW SHAFT WIDTH ONE HALF;So;0;ON;;;;;N;;;;;
+1F89F;HEAVY ARROW SHAFT WIDTH ONE THIRD;So;0;ON;;;;;N;;;;;
+1F8A0;LEFTWARDS BOTTOM-SHADED WHITE ARROW;So;0;ON;;;;;N;;;;;
+1F8A1;RIGHTWARDS BOTTOM SHADED WHITE ARROW;So;0;ON;;;;;N;;;;;
+1F8A2;LEFTWARDS TOP SHADED WHITE ARROW;So;0;ON;;;;;N;;;;;
+1F8A3;RIGHTWARDS TOP SHADED WHITE ARROW;So;0;ON;;;;;N;;;;;
+1F8A4;LEFTWARDS LEFT-SHADED WHITE ARROW;So;0;ON;;;;;N;;;;;
+1F8A5;RIGHTWARDS RIGHT-SHADED WHITE ARROW;So;0;ON;;;;;N;;;;;
+1F8A6;LEFTWARDS RIGHT-SHADED WHITE ARROW;So;0;ON;;;;;N;;;;;
+1F8A7;RIGHTWARDS LEFT-SHADED WHITE ARROW;So;0;ON;;;;;N;;;;;
+1F8A8;LEFTWARDS BACK-TILTED SHADOWED WHITE ARROW;So;0;ON;;;;;N;;;;;
+1F8A9;RIGHTWARDS BACK-TILTED SHADOWED WHITE ARROW;So;0;ON;;;;;N;;;;;
+1F8AA;LEFTWARDS FRONT-TILTED SHADOWED WHITE ARROW;So;0;ON;;;;;N;;;;;
+1F8AB;RIGHTWARDS FRONT-TILTED SHADOWED WHITE ARROW;So;0;ON;;;;;N;;;;;
+1F8AC;WHITE ARROW SHAFT WIDTH ONE;So;0;ON;;;;;N;;;;;
+1F8AD;WHITE ARROW SHAFT WIDTH TWO THIRDS;So;0;ON;;;;;N;;;;;
+1F910;ZIPPER-MOUTH FACE;So;0;ON;;;;;N;;;;;
+1F911;MONEY-MOUTH FACE;So;0;ON;;;;;N;;;;;
+1F912;FACE WITH THERMOMETER;So;0;ON;;;;;N;;;;;
+1F913;NERD FACE;So;0;ON;;;;;N;;;;;
+1F914;THINKING FACE;So;0;ON;;;;;N;;;;;
+1F915;FACE WITH HEAD-BANDAGE;So;0;ON;;;;;N;;;;;
+1F916;ROBOT FACE;So;0;ON;;;;;N;;;;;
+1F917;HUGGING FACE;So;0;ON;;;;;N;;;;;
+1F918;SIGN OF THE HORNS;So;0;ON;;;;;N;;;;;
+1F919;CALL ME HAND;So;0;ON;;;;;N;;;;;
+1F91A;RAISED BACK OF HAND;So;0;ON;;;;;N;;;;;
+1F91B;LEFT-FACING FIST;So;0;ON;;;;;N;;;;;
+1F91C;RIGHT-FACING FIST;So;0;ON;;;;;N;;;;;
+1F91D;HANDSHAKE;So;0;ON;;;;;N;;;;;
+1F91E;HAND WITH INDEX AND MIDDLE FINGERS CROSSED;So;0;ON;;;;;N;;;;;
+1F920;FACE WITH COWBOY HAT;So;0;ON;;;;;N;;;;;
+1F921;CLOWN FACE;So;0;ON;;;;;N;;;;;
+1F922;NAUSEATED FACE;So;0;ON;;;;;N;;;;;
+1F923;ROLLING ON THE FLOOR LAUGHING;So;0;ON;;;;;N;;;;;
+1F924;DROOLING FACE;So;0;ON;;;;;N;;;;;
+1F925;LYING FACE;So;0;ON;;;;;N;;;;;
+1F926;FACE PALM;So;0;ON;;;;;N;;;;;
+1F927;SNEEZING FACE;So;0;ON;;;;;N;;;;;
+1F930;PREGNANT WOMAN;So;0;ON;;;;;N;;;;;
+1F933;SELFIE;So;0;ON;;;;;N;;;;;
+1F934;PRINCE;So;0;ON;;;;;N;;;;;
+1F935;MAN IN TUXEDO;So;0;ON;;;;;N;;;;;
+1F936;MOTHER CHRISTMAS;So;0;ON;;;;;N;;;;;
+1F937;SHRUG;So;0;ON;;;;;N;;;;;
+1F938;PERSON DOING CARTWHEEL;So;0;ON;;;;;N;;;;;
+1F939;JUGGLING;So;0;ON;;;;;N;;;;;
+1F93A;FENCER;So;0;ON;;;;;N;;;;;
+1F93B;MODERN PENTATHLON;So;0;ON;;;;;N;;;;;
+1F93C;WRESTLERS;So;0;ON;;;;;N;;;;;
+1F93D;WATER POLO;So;0;ON;;;;;N;;;;;
+1F93E;HANDBALL;So;0;ON;;;;;N;;;;;
+1F940;WILTED FLOWER;So;0;ON;;;;;N;;;;;
+1F941;DRUM WITH DRUMSTICKS;So;0;ON;;;;;N;;;;;
+1F942;CLINKING GLASSES;So;0;ON;;;;;N;;;;;
+1F943;TUMBLER GLASS;So;0;ON;;;;;N;;;;;
+1F944;SPOON;So;0;ON;;;;;N;;;;;
+1F945;GOAL NET;So;0;ON;;;;;N;;;;;
+1F946;RIFLE;So;0;ON;;;;;N;;;;;
+1F947;FIRST PLACE MEDAL;So;0;ON;;;;;N;;;;;
+1F948;SECOND PLACE MEDAL;So;0;ON;;;;;N;;;;;
+1F949;THIRD PLACE MEDAL;So;0;ON;;;;;N;;;;;
+1F94A;BOXING GLOVE;So;0;ON;;;;;N;;;;;
+1F94B;MARTIAL ARTS UNIFORM;So;0;ON;;;;;N;;;;;
+1F950;CROISSANT;So;0;ON;;;;;N;;;;;
+1F951;AVOCADO;So;0;ON;;;;;N;;;;;
+1F952;CUCUMBER;So;0;ON;;;;;N;;;;;
+1F953;BACON;So;0;ON;;;;;N;;;;;
+1F954;POTATO;So;0;ON;;;;;N;;;;;
+1F955;CARROT;So;0;ON;;;;;N;;;;;
+1F956;BAGUETTE BREAD;So;0;ON;;;;;N;;;;;
+1F957;GREEN SALAD;So;0;ON;;;;;N;;;;;
+1F958;SHALLOW PAN OF FOOD;So;0;ON;;;;;N;;;;;
+1F959;STUFFED FLATBREAD;So;0;ON;;;;;N;;;;;
+1F95A;EGG;So;0;ON;;;;;N;;;;;
+1F95B;GLASS OF MILK;So;0;ON;;;;;N;;;;;
+1F95C;PEANUTS;So;0;ON;;;;;N;;;;;
+1F95D;KIWIFRUIT;So;0;ON;;;;;N;;;;;
+1F95E;PANCAKES;So;0;ON;;;;;N;;;;;
+1F980;CRAB;So;0;ON;;;;;N;;;;;
+1F981;LION FACE;So;0;ON;;;;;N;;;;;
+1F982;SCORPION;So;0;ON;;;;;N;;;;;
+1F983;TURKEY;So;0;ON;;;;;N;;;;;
+1F984;UNICORN FACE;So;0;ON;;;;;N;;;;;
+1F985;EAGLE;So;0;ON;;;;;N;;;;;
+1F986;DUCK;So;0;ON;;;;;N;;;;;
+1F987;BAT;So;0;ON;;;;;N;;;;;
+1F988;SHARK;So;0;ON;;;;;N;;;;;
+1F989;OWL;So;0;ON;;;;;N;;;;;
+1F98A;FOX FACE;So;0;ON;;;;;N;;;;;
+1F98B;BUTTERFLY;So;0;ON;;;;;N;;;;;
+1F98C;DEER;So;0;ON;;;;;N;;;;;
+1F98D;GORILLA;So;0;ON;;;;;N;;;;;
+1F98E;LIZARD;So;0;ON;;;;;N;;;;;
+1F98F;RHINOCEROS;So;0;ON;;;;;N;;;;;
+1F990;SHRIMP;So;0;ON;;;;;N;;;;;
+1F991;SQUID;So;0;ON;;;;;N;;;;;
+1F9C0;CHEESE WEDGE;So;0;ON;;;;;N;;;;;
+20000;<CJK Ideograph Extension B, First>;Lo;0;L;;;;;N;;;;;
+2A6D6;<CJK Ideograph Extension B, Last>;Lo;0;L;;;;;N;;;;;
+2A700;<CJK Ideograph Extension C, First>;Lo;0;L;;;;;N;;;;;
+2B734;<CJK Ideograph Extension C, Last>;Lo;0;L;;;;;N;;;;;
+2B740;<CJK Ideograph Extension D, First>;Lo;0;L;;;;;N;;;;;
+2B81D;<CJK Ideograph Extension D, Last>;Lo;0;L;;;;;N;;;;;
+2B820;<CJK Ideograph Extension E, First>;Lo;0;L;;;;;N;;;;;
+2CEA1;<CJK Ideograph Extension E, Last>;Lo;0;L;;;;;N;;;;;
+2F800;CJK COMPATIBILITY IDEOGRAPH-2F800;Lo;0;L;4E3D;;;;N;;;;;
+2F801;CJK COMPATIBILITY IDEOGRAPH-2F801;Lo;0;L;4E38;;;;N;;;;;
+2F802;CJK COMPATIBILITY IDEOGRAPH-2F802;Lo;0;L;4E41;;;;N;;;;;
+2F803;CJK COMPATIBILITY IDEOGRAPH-2F803;Lo;0;L;20122;;;;N;;;;;
+2F804;CJK COMPATIBILITY IDEOGRAPH-2F804;Lo;0;L;4F60;;;;N;;;;;
+2F805;CJK COMPATIBILITY IDEOGRAPH-2F805;Lo;0;L;4FAE;;;;N;;;;;
+2F806;CJK COMPATIBILITY IDEOGRAPH-2F806;Lo;0;L;4FBB;;;;N;;;;;
+2F807;CJK COMPATIBILITY IDEOGRAPH-2F807;Lo;0;L;5002;;;;N;;;;;
+2F808;CJK COMPATIBILITY IDEOGRAPH-2F808;Lo;0;L;507A;;;;N;;;;;
+2F809;CJK COMPATIBILITY IDEOGRAPH-2F809;Lo;0;L;5099;;;;N;;;;;
+2F80A;CJK COMPATIBILITY IDEOGRAPH-2F80A;Lo;0;L;50E7;;;;N;;;;;
+2F80B;CJK COMPATIBILITY IDEOGRAPH-2F80B;Lo;0;L;50CF;;;;N;;;;;
+2F80C;CJK COMPATIBILITY IDEOGRAPH-2F80C;Lo;0;L;349E;;;;N;;;;;
+2F80D;CJK COMPATIBILITY IDEOGRAPH-2F80D;Lo;0;L;2063A;;;;N;;;;;
+2F80E;CJK COMPATIBILITY IDEOGRAPH-2F80E;Lo;0;L;514D;;;;N;;;;;
+2F80F;CJK COMPATIBILITY IDEOGRAPH-2F80F;Lo;0;L;5154;;;;N;;;;;
+2F810;CJK COMPATIBILITY IDEOGRAPH-2F810;Lo;0;L;5164;;;;N;;;;;
+2F811;CJK COMPATIBILITY IDEOGRAPH-2F811;Lo;0;L;5177;;;;N;;;;;
+2F812;CJK COMPATIBILITY IDEOGRAPH-2F812;Lo;0;L;2051C;;;;N;;;;;
+2F813;CJK COMPATIBILITY IDEOGRAPH-2F813;Lo;0;L;34B9;;;;N;;;;;
+2F814;CJK COMPATIBILITY IDEOGRAPH-2F814;Lo;0;L;5167;;;;N;;;;;
+2F815;CJK COMPATIBILITY IDEOGRAPH-2F815;Lo;0;L;518D;;;;N;;;;;
+2F816;CJK COMPATIBILITY IDEOGRAPH-2F816;Lo;0;L;2054B;;;;N;;;;;
+2F817;CJK COMPATIBILITY IDEOGRAPH-2F817;Lo;0;L;5197;;;;N;;;;;
+2F818;CJK COMPATIBILITY IDEOGRAPH-2F818;Lo;0;L;51A4;;;;N;;;;;
+2F819;CJK COMPATIBILITY IDEOGRAPH-2F819;Lo;0;L;4ECC;;;;N;;;;;
+2F81A;CJK COMPATIBILITY IDEOGRAPH-2F81A;Lo;0;L;51AC;;;;N;;;;;
+2F81B;CJK COMPATIBILITY IDEOGRAPH-2F81B;Lo;0;L;51B5;;;;N;;;;;
+2F81C;CJK COMPATIBILITY IDEOGRAPH-2F81C;Lo;0;L;291DF;;;;N;;;;;
+2F81D;CJK COMPATIBILITY IDEOGRAPH-2F81D;Lo;0;L;51F5;;;;N;;;;;
+2F81E;CJK COMPATIBILITY IDEOGRAPH-2F81E;Lo;0;L;5203;;;;N;;;;;
+2F81F;CJK COMPATIBILITY IDEOGRAPH-2F81F;Lo;0;L;34DF;;;;N;;;;;
+2F820;CJK COMPATIBILITY IDEOGRAPH-2F820;Lo;0;L;523B;;;;N;;;;;
+2F821;CJK COMPATIBILITY IDEOGRAPH-2F821;Lo;0;L;5246;;;;N;;;;;
+2F822;CJK COMPATIBILITY IDEOGRAPH-2F822;Lo;0;L;5272;;;;N;;;;;
+2F823;CJK COMPATIBILITY IDEOGRAPH-2F823;Lo;0;L;5277;;;;N;;;;;
+2F824;CJK COMPATIBILITY IDEOGRAPH-2F824;Lo;0;L;3515;;;;N;;;;;
+2F825;CJK COMPATIBILITY IDEOGRAPH-2F825;Lo;0;L;52C7;;;;N;;;;;
+2F826;CJK COMPATIBILITY IDEOGRAPH-2F826;Lo;0;L;52C9;;;;N;;;;;
+2F827;CJK COMPATIBILITY IDEOGRAPH-2F827;Lo;0;L;52E4;;;;N;;;;;
+2F828;CJK COMPATIBILITY IDEOGRAPH-2F828;Lo;0;L;52FA;;;;N;;;;;
+2F829;CJK COMPATIBILITY IDEOGRAPH-2F829;Lo;0;L;5305;;;;N;;;;;
+2F82A;CJK COMPATIBILITY IDEOGRAPH-2F82A;Lo;0;L;5306;;;;N;;;;;
+2F82B;CJK COMPATIBILITY IDEOGRAPH-2F82B;Lo;0;L;5317;;;;N;;;;;
+2F82C;CJK COMPATIBILITY IDEOGRAPH-2F82C;Lo;0;L;5349;;;;N;;;;;
+2F82D;CJK COMPATIBILITY IDEOGRAPH-2F82D;Lo;0;L;5351;;;;N;;;;;
+2F82E;CJK COMPATIBILITY IDEOGRAPH-2F82E;Lo;0;L;535A;;;;N;;;;;
+2F82F;CJK COMPATIBILITY IDEOGRAPH-2F82F;Lo;0;L;5373;;;;N;;;;;
+2F830;CJK COMPATIBILITY IDEOGRAPH-2F830;Lo;0;L;537D;;;;N;;;;;
+2F831;CJK COMPATIBILITY IDEOGRAPH-2F831;Lo;0;L;537F;;;;N;;;;;
+2F832;CJK COMPATIBILITY IDEOGRAPH-2F832;Lo;0;L;537F;;;;N;;;;;
+2F833;CJK COMPATIBILITY IDEOGRAPH-2F833;Lo;0;L;537F;;;;N;;;;;
+2F834;CJK COMPATIBILITY IDEOGRAPH-2F834;Lo;0;L;20A2C;;;;N;;;;;
+2F835;CJK COMPATIBILITY IDEOGRAPH-2F835;Lo;0;L;7070;;;;N;;;;;
+2F836;CJK COMPATIBILITY IDEOGRAPH-2F836;Lo;0;L;53CA;;;;N;;;;;
+2F837;CJK COMPATIBILITY IDEOGRAPH-2F837;Lo;0;L;53DF;;;;N;;;;;
+2F838;CJK COMPATIBILITY IDEOGRAPH-2F838;Lo;0;L;20B63;;;;N;;;;;
+2F839;CJK COMPATIBILITY IDEOGRAPH-2F839;Lo;0;L;53EB;;;;N;;;;;
+2F83A;CJK COMPATIBILITY IDEOGRAPH-2F83A;Lo;0;L;53F1;;;;N;;;;;
+2F83B;CJK COMPATIBILITY IDEOGRAPH-2F83B;Lo;0;L;5406;;;;N;;;;;
+2F83C;CJK COMPATIBILITY IDEOGRAPH-2F83C;Lo;0;L;549E;;;;N;;;;;
+2F83D;CJK COMPATIBILITY IDEOGRAPH-2F83D;Lo;0;L;5438;;;;N;;;;;
+2F83E;CJK COMPATIBILITY IDEOGRAPH-2F83E;Lo;0;L;5448;;;;N;;;;;
+2F83F;CJK COMPATIBILITY IDEOGRAPH-2F83F;Lo;0;L;5468;;;;N;;;;;
+2F840;CJK COMPATIBILITY IDEOGRAPH-2F840;Lo;0;L;54A2;;;;N;;;;;
+2F841;CJK COMPATIBILITY IDEOGRAPH-2F841;Lo;0;L;54F6;;;;N;;;;;
+2F842;CJK COMPATIBILITY IDEOGRAPH-2F842;Lo;0;L;5510;;;;N;;;;;
+2F843;CJK COMPATIBILITY IDEOGRAPH-2F843;Lo;0;L;5553;;;;N;;;;;
+2F844;CJK COMPATIBILITY IDEOGRAPH-2F844;Lo;0;L;5563;;;;N;;;;;
+2F845;CJK COMPATIBILITY IDEOGRAPH-2F845;Lo;0;L;5584;;;;N;;;;;
+2F846;CJK COMPATIBILITY IDEOGRAPH-2F846;Lo;0;L;5584;;;;N;;;;;
+2F847;CJK COMPATIBILITY IDEOGRAPH-2F847;Lo;0;L;5599;;;;N;;;;;
+2F848;CJK COMPATIBILITY IDEOGRAPH-2F848;Lo;0;L;55AB;;;;N;;;;;
+2F849;CJK COMPATIBILITY IDEOGRAPH-2F849;Lo;0;L;55B3;;;;N;;;;;
+2F84A;CJK COMPATIBILITY IDEOGRAPH-2F84A;Lo;0;L;55C2;;;;N;;;;;
+2F84B;CJK COMPATIBILITY IDEOGRAPH-2F84B;Lo;0;L;5716;;;;N;;;;;
+2F84C;CJK COMPATIBILITY IDEOGRAPH-2F84C;Lo;0;L;5606;;;;N;;;;;
+2F84D;CJK COMPATIBILITY IDEOGRAPH-2F84D;Lo;0;L;5717;;;;N;;;;;
+2F84E;CJK COMPATIBILITY IDEOGRAPH-2F84E;Lo;0;L;5651;;;;N;;;;;
+2F84F;CJK COMPATIBILITY IDEOGRAPH-2F84F;Lo;0;L;5674;;;;N;;;;;
+2F850;CJK COMPATIBILITY IDEOGRAPH-2F850;Lo;0;L;5207;;;;N;;;;;
+2F851;CJK COMPATIBILITY IDEOGRAPH-2F851;Lo;0;L;58EE;;;;N;;;;;
+2F852;CJK COMPATIBILITY IDEOGRAPH-2F852;Lo;0;L;57CE;;;;N;;;;;
+2F853;CJK COMPATIBILITY IDEOGRAPH-2F853;Lo;0;L;57F4;;;;N;;;;;
+2F854;CJK COMPATIBILITY IDEOGRAPH-2F854;Lo;0;L;580D;;;;N;;;;;
+2F855;CJK COMPATIBILITY IDEOGRAPH-2F855;Lo;0;L;578B;;;;N;;;;;
+2F856;CJK COMPATIBILITY IDEOGRAPH-2F856;Lo;0;L;5832;;;;N;;;;;
+2F857;CJK COMPATIBILITY IDEOGRAPH-2F857;Lo;0;L;5831;;;;N;;;;;
+2F858;CJK COMPATIBILITY IDEOGRAPH-2F858;Lo;0;L;58AC;;;;N;;;;;
+2F859;CJK COMPATIBILITY IDEOGRAPH-2F859;Lo;0;L;214E4;;;;N;;;;;
+2F85A;CJK COMPATIBILITY IDEOGRAPH-2F85A;Lo;0;L;58F2;;;;N;;;;;
+2F85B;CJK COMPATIBILITY IDEOGRAPH-2F85B;Lo;0;L;58F7;;;;N;;;;;
+2F85C;CJK COMPATIBILITY IDEOGRAPH-2F85C;Lo;0;L;5906;;;;N;;;;;
+2F85D;CJK COMPATIBILITY IDEOGRAPH-2F85D;Lo;0;L;591A;;;;N;;;;;
+2F85E;CJK COMPATIBILITY IDEOGRAPH-2F85E;Lo;0;L;5922;;;;N;;;;;
+2F85F;CJK COMPATIBILITY IDEOGRAPH-2F85F;Lo;0;L;5962;;;;N;;;;;
+2F860;CJK COMPATIBILITY IDEOGRAPH-2F860;Lo;0;L;216A8;;;;N;;;;;
+2F861;CJK COMPATIBILITY IDEOGRAPH-2F861;Lo;0;L;216EA;;;;N;;;;;
+2F862;CJK COMPATIBILITY IDEOGRAPH-2F862;Lo;0;L;59EC;;;;N;;;;;
+2F863;CJK COMPATIBILITY IDEOGRAPH-2F863;Lo;0;L;5A1B;;;;N;;;;;
+2F864;CJK COMPATIBILITY IDEOGRAPH-2F864;Lo;0;L;5A27;;;;N;;;;;
+2F865;CJK COMPATIBILITY IDEOGRAPH-2F865;Lo;0;L;59D8;;;;N;;;;;
+2F866;CJK COMPATIBILITY IDEOGRAPH-2F866;Lo;0;L;5A66;;;;N;;;;;
+2F867;CJK COMPATIBILITY IDEOGRAPH-2F867;Lo;0;L;36EE;;;;N;;;;;
+2F868;CJK COMPATIBILITY IDEOGRAPH-2F868;Lo;0;L;36FC;;;;N;;;;;
+2F869;CJK COMPATIBILITY IDEOGRAPH-2F869;Lo;0;L;5B08;;;;N;;;;;
+2F86A;CJK COMPATIBILITY IDEOGRAPH-2F86A;Lo;0;L;5B3E;;;;N;;;;;
+2F86B;CJK COMPATIBILITY IDEOGRAPH-2F86B;Lo;0;L;5B3E;;;;N;;;;;
+2F86C;CJK COMPATIBILITY IDEOGRAPH-2F86C;Lo;0;L;219C8;;;;N;;;;;
+2F86D;CJK COMPATIBILITY IDEOGRAPH-2F86D;Lo;0;L;5BC3;;;;N;;;;;
+2F86E;CJK COMPATIBILITY IDEOGRAPH-2F86E;Lo;0;L;5BD8;;;;N;;;;;
+2F86F;CJK COMPATIBILITY IDEOGRAPH-2F86F;Lo;0;L;5BE7;;;;N;;;;;
+2F870;CJK COMPATIBILITY IDEOGRAPH-2F870;Lo;0;L;5BF3;;;;N;;;;;
+2F871;CJK COMPATIBILITY IDEOGRAPH-2F871;Lo;0;L;21B18;;;;N;;;;;
+2F872;CJK COMPATIBILITY IDEOGRAPH-2F872;Lo;0;L;5BFF;;;;N;;;;;
+2F873;CJK COMPATIBILITY IDEOGRAPH-2F873;Lo;0;L;5C06;;;;N;;;;;
+2F874;CJK COMPATIBILITY IDEOGRAPH-2F874;Lo;0;L;5F53;;;;N;;;;;
+2F875;CJK COMPATIBILITY IDEOGRAPH-2F875;Lo;0;L;5C22;;;;N;;;;;
+2F876;CJK COMPATIBILITY IDEOGRAPH-2F876;Lo;0;L;3781;;;;N;;;;;
+2F877;CJK COMPATIBILITY IDEOGRAPH-2F877;Lo;0;L;5C60;;;;N;;;;;
+2F878;CJK COMPATIBILITY IDEOGRAPH-2F878;Lo;0;L;5C6E;;;;N;;;;;
+2F879;CJK COMPATIBILITY IDEOGRAPH-2F879;Lo;0;L;5CC0;;;;N;;;;;
+2F87A;CJK COMPATIBILITY IDEOGRAPH-2F87A;Lo;0;L;5C8D;;;;N;;;;;
+2F87B;CJK COMPATIBILITY IDEOGRAPH-2F87B;Lo;0;L;21DE4;;;;N;;;;;
+2F87C;CJK COMPATIBILITY IDEOGRAPH-2F87C;Lo;0;L;5D43;;;;N;;;;;
+2F87D;CJK COMPATIBILITY IDEOGRAPH-2F87D;Lo;0;L;21DE6;;;;N;;;;;
+2F87E;CJK COMPATIBILITY IDEOGRAPH-2F87E;Lo;0;L;5D6E;;;;N;;;;;
+2F87F;CJK COMPATIBILITY IDEOGRAPH-2F87F;Lo;0;L;5D6B;;;;N;;;;;
+2F880;CJK COMPATIBILITY IDEOGRAPH-2F880;Lo;0;L;5D7C;;;;N;;;;;
+2F881;CJK COMPATIBILITY IDEOGRAPH-2F881;Lo;0;L;5DE1;;;;N;;;;;
+2F882;CJK COMPATIBILITY IDEOGRAPH-2F882;Lo;0;L;5DE2;;;;N;;;;;
+2F883;CJK COMPATIBILITY IDEOGRAPH-2F883;Lo;0;L;382F;;;;N;;;;;
+2F884;CJK COMPATIBILITY IDEOGRAPH-2F884;Lo;0;L;5DFD;;;;N;;;;;
+2F885;CJK COMPATIBILITY IDEOGRAPH-2F885;Lo;0;L;5E28;;;;N;;;;;
+2F886;CJK COMPATIBILITY IDEOGRAPH-2F886;Lo;0;L;5E3D;;;;N;;;;;
+2F887;CJK COMPATIBILITY IDEOGRAPH-2F887;Lo;0;L;5E69;;;;N;;;;;
+2F888;CJK COMPATIBILITY IDEOGRAPH-2F888;Lo;0;L;3862;;;;N;;;;;
+2F889;CJK COMPATIBILITY IDEOGRAPH-2F889;Lo;0;L;22183;;;;N;;;;;
+2F88A;CJK COMPATIBILITY IDEOGRAPH-2F88A;Lo;0;L;387C;;;;N;;;;;
+2F88B;CJK COMPATIBILITY IDEOGRAPH-2F88B;Lo;0;L;5EB0;;;;N;;;;;
+2F88C;CJK COMPATIBILITY IDEOGRAPH-2F88C;Lo;0;L;5EB3;;;;N;;;;;
+2F88D;CJK COMPATIBILITY IDEOGRAPH-2F88D;Lo;0;L;5EB6;;;;N;;;;;
+2F88E;CJK COMPATIBILITY IDEOGRAPH-2F88E;Lo;0;L;5ECA;;;;N;;;;;
+2F88F;CJK COMPATIBILITY IDEOGRAPH-2F88F;Lo;0;L;2A392;;;;N;;;;;
+2F890;CJK COMPATIBILITY IDEOGRAPH-2F890;Lo;0;L;5EFE;;;9;N;;;;;
+2F891;CJK COMPATIBILITY IDEOGRAPH-2F891;Lo;0;L;22331;;;;N;;;;;
+2F892;CJK COMPATIBILITY IDEOGRAPH-2F892;Lo;0;L;22331;;;;N;;;;;
+2F893;CJK COMPATIBILITY IDEOGRAPH-2F893;Lo;0;L;8201;;;;N;;;;;
+2F894;CJK COMPATIBILITY IDEOGRAPH-2F894;Lo;0;L;5F22;;;;N;;;;;
+2F895;CJK COMPATIBILITY IDEOGRAPH-2F895;Lo;0;L;5F22;;;;N;;;;;
+2F896;CJK COMPATIBILITY IDEOGRAPH-2F896;Lo;0;L;38C7;;;;N;;;;;
+2F897;CJK COMPATIBILITY IDEOGRAPH-2F897;Lo;0;L;232B8;;;;N;;;;;
+2F898;CJK COMPATIBILITY IDEOGRAPH-2F898;Lo;0;L;261DA;;;;N;;;;;
+2F899;CJK COMPATIBILITY IDEOGRAPH-2F899;Lo;0;L;5F62;;;;N;;;;;
+2F89A;CJK COMPATIBILITY IDEOGRAPH-2F89A;Lo;0;L;5F6B;;;;N;;;;;
+2F89B;CJK COMPATIBILITY IDEOGRAPH-2F89B;Lo;0;L;38E3;;;;N;;;;;
+2F89C;CJK COMPATIBILITY IDEOGRAPH-2F89C;Lo;0;L;5F9A;;;;N;;;;;
+2F89D;CJK COMPATIBILITY IDEOGRAPH-2F89D;Lo;0;L;5FCD;;;;N;;;;;
+2F89E;CJK COMPATIBILITY IDEOGRAPH-2F89E;Lo;0;L;5FD7;;;;N;;;;;
+2F89F;CJK COMPATIBILITY IDEOGRAPH-2F89F;Lo;0;L;5FF9;;;;N;;;;;
+2F8A0;CJK COMPATIBILITY IDEOGRAPH-2F8A0;Lo;0;L;6081;;;;N;;;;;
+2F8A1;CJK COMPATIBILITY IDEOGRAPH-2F8A1;Lo;0;L;393A;;;;N;;;;;
+2F8A2;CJK COMPATIBILITY IDEOGRAPH-2F8A2;Lo;0;L;391C;;;;N;;;;;
+2F8A3;CJK COMPATIBILITY IDEOGRAPH-2F8A3;Lo;0;L;6094;;;;N;;;;;
+2F8A4;CJK COMPATIBILITY IDEOGRAPH-2F8A4;Lo;0;L;226D4;;;;N;;;;;
+2F8A5;CJK COMPATIBILITY IDEOGRAPH-2F8A5;Lo;0;L;60C7;;;;N;;;;;
+2F8A6;CJK COMPATIBILITY IDEOGRAPH-2F8A6;Lo;0;L;6148;;;;N;;;;;
+2F8A7;CJK COMPATIBILITY IDEOGRAPH-2F8A7;Lo;0;L;614C;;;;N;;;;;
+2F8A8;CJK COMPATIBILITY IDEOGRAPH-2F8A8;Lo;0;L;614E;;;;N;;;;;
+2F8A9;CJK COMPATIBILITY IDEOGRAPH-2F8A9;Lo;0;L;614C;;;;N;;;;;
+2F8AA;CJK COMPATIBILITY IDEOGRAPH-2F8AA;Lo;0;L;617A;;;;N;;;;;
+2F8AB;CJK COMPATIBILITY IDEOGRAPH-2F8AB;Lo;0;L;618E;;;;N;;;;;
+2F8AC;CJK COMPATIBILITY IDEOGRAPH-2F8AC;Lo;0;L;61B2;;;;N;;;;;
+2F8AD;CJK COMPATIBILITY IDEOGRAPH-2F8AD;Lo;0;L;61A4;;;;N;;;;;
+2F8AE;CJK COMPATIBILITY IDEOGRAPH-2F8AE;Lo;0;L;61AF;;;;N;;;;;
+2F8AF;CJK COMPATIBILITY IDEOGRAPH-2F8AF;Lo;0;L;61DE;;;;N;;;;;
+2F8B0;CJK COMPATIBILITY IDEOGRAPH-2F8B0;Lo;0;L;61F2;;;;N;;;;;
+2F8B1;CJK COMPATIBILITY IDEOGRAPH-2F8B1;Lo;0;L;61F6;;;;N;;;;;
+2F8B2;CJK COMPATIBILITY IDEOGRAPH-2F8B2;Lo;0;L;6210;;;;N;;;;;
+2F8B3;CJK COMPATIBILITY IDEOGRAPH-2F8B3;Lo;0;L;621B;;;;N;;;;;
+2F8B4;CJK COMPATIBILITY IDEOGRAPH-2F8B4;Lo;0;L;625D;;;;N;;;;;
+2F8B5;CJK COMPATIBILITY IDEOGRAPH-2F8B5;Lo;0;L;62B1;;;;N;;;;;
+2F8B6;CJK COMPATIBILITY IDEOGRAPH-2F8B6;Lo;0;L;62D4;;;;N;;;;;
+2F8B7;CJK COMPATIBILITY IDEOGRAPH-2F8B7;Lo;0;L;6350;;;;N;;;;;
+2F8B8;CJK COMPATIBILITY IDEOGRAPH-2F8B8;Lo;0;L;22B0C;;;;N;;;;;
+2F8B9;CJK COMPATIBILITY IDEOGRAPH-2F8B9;Lo;0;L;633D;;;;N;;;;;
+2F8BA;CJK COMPATIBILITY IDEOGRAPH-2F8BA;Lo;0;L;62FC;;;;N;;;;;
+2F8BB;CJK COMPATIBILITY IDEOGRAPH-2F8BB;Lo;0;L;6368;;;;N;;;;;
+2F8BC;CJK COMPATIBILITY IDEOGRAPH-2F8BC;Lo;0;L;6383;;;;N;;;;;
+2F8BD;CJK COMPATIBILITY IDEOGRAPH-2F8BD;Lo;0;L;63E4;;;;N;;;;;
+2F8BE;CJK COMPATIBILITY IDEOGRAPH-2F8BE;Lo;0;L;22BF1;;;;N;;;;;
+2F8BF;CJK COMPATIBILITY IDEOGRAPH-2F8BF;Lo;0;L;6422;;;;N;;;;;
+2F8C0;CJK COMPATIBILITY IDEOGRAPH-2F8C0;Lo;0;L;63C5;;;;N;;;;;
+2F8C1;CJK COMPATIBILITY IDEOGRAPH-2F8C1;Lo;0;L;63A9;;;;N;;;;;
+2F8C2;CJK COMPATIBILITY IDEOGRAPH-2F8C2;Lo;0;L;3A2E;;;;N;;;;;
+2F8C3;CJK COMPATIBILITY IDEOGRAPH-2F8C3;Lo;0;L;6469;;;;N;;;;;
+2F8C4;CJK COMPATIBILITY IDEOGRAPH-2F8C4;Lo;0;L;647E;;;;N;;;;;
+2F8C5;CJK COMPATIBILITY IDEOGRAPH-2F8C5;Lo;0;L;649D;;;;N;;;;;
+2F8C6;CJK COMPATIBILITY IDEOGRAPH-2F8C6;Lo;0;L;6477;;;;N;;;;;
+2F8C7;CJK COMPATIBILITY IDEOGRAPH-2F8C7;Lo;0;L;3A6C;;;;N;;;;;
+2F8C8;CJK COMPATIBILITY IDEOGRAPH-2F8C8;Lo;0;L;654F;;;;N;;;;;
+2F8C9;CJK COMPATIBILITY IDEOGRAPH-2F8C9;Lo;0;L;656C;;;;N;;;;;
+2F8CA;CJK COMPATIBILITY IDEOGRAPH-2F8CA;Lo;0;L;2300A;;;;N;;;;;
+2F8CB;CJK COMPATIBILITY IDEOGRAPH-2F8CB;Lo;0;L;65E3;;;;N;;;;;
+2F8CC;CJK COMPATIBILITY IDEOGRAPH-2F8CC;Lo;0;L;66F8;;;;N;;;;;
+2F8CD;CJK COMPATIBILITY IDEOGRAPH-2F8CD;Lo;0;L;6649;;;;N;;;;;
+2F8CE;CJK COMPATIBILITY IDEOGRAPH-2F8CE;Lo;0;L;3B19;;;;N;;;;;
+2F8CF;CJK COMPATIBILITY IDEOGRAPH-2F8CF;Lo;0;L;6691;;;;N;;;;;
+2F8D0;CJK COMPATIBILITY IDEOGRAPH-2F8D0;Lo;0;L;3B08;;;;N;;;;;
+2F8D1;CJK COMPATIBILITY IDEOGRAPH-2F8D1;Lo;0;L;3AE4;;;;N;;;;;
+2F8D2;CJK COMPATIBILITY IDEOGRAPH-2F8D2;Lo;0;L;5192;;;;N;;;;;
+2F8D3;CJK COMPATIBILITY IDEOGRAPH-2F8D3;Lo;0;L;5195;;;;N;;;;;
+2F8D4;CJK COMPATIBILITY IDEOGRAPH-2F8D4;Lo;0;L;6700;;;;N;;;;;
+2F8D5;CJK COMPATIBILITY IDEOGRAPH-2F8D5;Lo;0;L;669C;;;;N;;;;;
+2F8D6;CJK COMPATIBILITY IDEOGRAPH-2F8D6;Lo;0;L;80AD;;;;N;;;;;
+2F8D7;CJK COMPATIBILITY IDEOGRAPH-2F8D7;Lo;0;L;43D9;;;;N;;;;;
+2F8D8;CJK COMPATIBILITY IDEOGRAPH-2F8D8;Lo;0;L;6717;;;;N;;;;;
+2F8D9;CJK COMPATIBILITY IDEOGRAPH-2F8D9;Lo;0;L;671B;;;;N;;;;;
+2F8DA;CJK COMPATIBILITY IDEOGRAPH-2F8DA;Lo;0;L;6721;;;;N;;;;;
+2F8DB;CJK COMPATIBILITY IDEOGRAPH-2F8DB;Lo;0;L;675E;;;;N;;;;;
+2F8DC;CJK COMPATIBILITY IDEOGRAPH-2F8DC;Lo;0;L;6753;;;;N;;;;;
+2F8DD;CJK COMPATIBILITY IDEOGRAPH-2F8DD;Lo;0;L;233C3;;;;N;;;;;
+2F8DE;CJK COMPATIBILITY IDEOGRAPH-2F8DE;Lo;0;L;3B49;;;;N;;;;;
+2F8DF;CJK COMPATIBILITY IDEOGRAPH-2F8DF;Lo;0;L;67FA;;;;N;;;;;
+2F8E0;CJK COMPATIBILITY IDEOGRAPH-2F8E0;Lo;0;L;6785;;;;N;;;;;
+2F8E1;CJK COMPATIBILITY IDEOGRAPH-2F8E1;Lo;0;L;6852;;;;N;;;;;
+2F8E2;CJK COMPATIBILITY IDEOGRAPH-2F8E2;Lo;0;L;6885;;;;N;;;;;
+2F8E3;CJK COMPATIBILITY IDEOGRAPH-2F8E3;Lo;0;L;2346D;;;;N;;;;;
+2F8E4;CJK COMPATIBILITY IDEOGRAPH-2F8E4;Lo;0;L;688E;;;;N;;;;;
+2F8E5;CJK COMPATIBILITY IDEOGRAPH-2F8E5;Lo;0;L;681F;;;;N;;;;;
+2F8E6;CJK COMPATIBILITY IDEOGRAPH-2F8E6;Lo;0;L;6914;;;;N;;;;;
+2F8E7;CJK COMPATIBILITY IDEOGRAPH-2F8E7;Lo;0;L;3B9D;;;;N;;;;;
+2F8E8;CJK COMPATIBILITY IDEOGRAPH-2F8E8;Lo;0;L;6942;;;;N;;;;;
+2F8E9;CJK COMPATIBILITY IDEOGRAPH-2F8E9;Lo;0;L;69A3;;;;N;;;;;
+2F8EA;CJK COMPATIBILITY IDEOGRAPH-2F8EA;Lo;0;L;69EA;;;;N;;;;;
+2F8EB;CJK COMPATIBILITY IDEOGRAPH-2F8EB;Lo;0;L;6AA8;;;;N;;;;;
+2F8EC;CJK COMPATIBILITY IDEOGRAPH-2F8EC;Lo;0;L;236A3;;;;N;;;;;
+2F8ED;CJK COMPATIBILITY IDEOGRAPH-2F8ED;Lo;0;L;6ADB;;;;N;;;;;
+2F8EE;CJK COMPATIBILITY IDEOGRAPH-2F8EE;Lo;0;L;3C18;;;;N;;;;;
+2F8EF;CJK COMPATIBILITY IDEOGRAPH-2F8EF;Lo;0;L;6B21;;;;N;;;;;
+2F8F0;CJK COMPATIBILITY IDEOGRAPH-2F8F0;Lo;0;L;238A7;;;;N;;;;;
+2F8F1;CJK COMPATIBILITY IDEOGRAPH-2F8F1;Lo;0;L;6B54;;;;N;;;;;
+2F8F2;CJK COMPATIBILITY IDEOGRAPH-2F8F2;Lo;0;L;3C4E;;;;N;;;;;
+2F8F3;CJK COMPATIBILITY IDEOGRAPH-2F8F3;Lo;0;L;6B72;;;;N;;;;;
+2F8F4;CJK COMPATIBILITY IDEOGRAPH-2F8F4;Lo;0;L;6B9F;;;;N;;;;;
+2F8F5;CJK COMPATIBILITY IDEOGRAPH-2F8F5;Lo;0;L;6BBA;;;;N;;;;;
+2F8F6;CJK COMPATIBILITY IDEOGRAPH-2F8F6;Lo;0;L;6BBB;;;;N;;;;;
+2F8F7;CJK COMPATIBILITY IDEOGRAPH-2F8F7;Lo;0;L;23A8D;;;;N;;;;;
+2F8F8;CJK COMPATIBILITY IDEOGRAPH-2F8F8;Lo;0;L;21D0B;;;;N;;;;;
+2F8F9;CJK COMPATIBILITY IDEOGRAPH-2F8F9;Lo;0;L;23AFA;;;;N;;;;;
+2F8FA;CJK COMPATIBILITY IDEOGRAPH-2F8FA;Lo;0;L;6C4E;;;;N;;;;;
+2F8FB;CJK COMPATIBILITY IDEOGRAPH-2F8FB;Lo;0;L;23CBC;;;;N;;;;;
+2F8FC;CJK COMPATIBILITY IDEOGRAPH-2F8FC;Lo;0;L;6CBF;;;;N;;;;;
+2F8FD;CJK COMPATIBILITY IDEOGRAPH-2F8FD;Lo;0;L;6CCD;;;;N;;;;;
+2F8FE;CJK COMPATIBILITY IDEOGRAPH-2F8FE;Lo;0;L;6C67;;;;N;;;;;
+2F8FF;CJK COMPATIBILITY IDEOGRAPH-2F8FF;Lo;0;L;6D16;;;;N;;;;;
+2F900;CJK COMPATIBILITY IDEOGRAPH-2F900;Lo;0;L;6D3E;;;;N;;;;;
+2F901;CJK COMPATIBILITY IDEOGRAPH-2F901;Lo;0;L;6D77;;;;N;;;;;
+2F902;CJK COMPATIBILITY IDEOGRAPH-2F902;Lo;0;L;6D41;;;;N;;;;;
+2F903;CJK COMPATIBILITY IDEOGRAPH-2F903;Lo;0;L;6D69;;;;N;;;;;
+2F904;CJK COMPATIBILITY IDEOGRAPH-2F904;Lo;0;L;6D78;;;;N;;;;;
+2F905;CJK COMPATIBILITY IDEOGRAPH-2F905;Lo;0;L;6D85;;;;N;;;;;
+2F906;CJK COMPATIBILITY IDEOGRAPH-2F906;Lo;0;L;23D1E;;;;N;;;;;
+2F907;CJK COMPATIBILITY IDEOGRAPH-2F907;Lo;0;L;6D34;;;;N;;;;;
+2F908;CJK COMPATIBILITY IDEOGRAPH-2F908;Lo;0;L;6E2F;;;;N;;;;;
+2F909;CJK COMPATIBILITY IDEOGRAPH-2F909;Lo;0;L;6E6E;;;;N;;;;;
+2F90A;CJK COMPATIBILITY IDEOGRAPH-2F90A;Lo;0;L;3D33;;;;N;;;;;
+2F90B;CJK COMPATIBILITY IDEOGRAPH-2F90B;Lo;0;L;6ECB;;;;N;;;;;
+2F90C;CJK COMPATIBILITY IDEOGRAPH-2F90C;Lo;0;L;6EC7;;;;N;;;;;
+2F90D;CJK COMPATIBILITY IDEOGRAPH-2F90D;Lo;0;L;23ED1;;;;N;;;;;
+2F90E;CJK COMPATIBILITY IDEOGRAPH-2F90E;Lo;0;L;6DF9;;;;N;;;;;
+2F90F;CJK COMPATIBILITY IDEOGRAPH-2F90F;Lo;0;L;6F6E;;;;N;;;;;
+2F910;CJK COMPATIBILITY IDEOGRAPH-2F910;Lo;0;L;23F5E;;;;N;;;;;
+2F911;CJK COMPATIBILITY IDEOGRAPH-2F911;Lo;0;L;23F8E;;;;N;;;;;
+2F912;CJK COMPATIBILITY IDEOGRAPH-2F912;Lo;0;L;6FC6;;;;N;;;;;
+2F913;CJK COMPATIBILITY IDEOGRAPH-2F913;Lo;0;L;7039;;;;N;;;;;
+2F914;CJK COMPATIBILITY IDEOGRAPH-2F914;Lo;0;L;701E;;;;N;;;;;
+2F915;CJK COMPATIBILITY IDEOGRAPH-2F915;Lo;0;L;701B;;;;N;;;;;
+2F916;CJK COMPATIBILITY IDEOGRAPH-2F916;Lo;0;L;3D96;;;;N;;;;;
+2F917;CJK COMPATIBILITY IDEOGRAPH-2F917;Lo;0;L;704A;;;;N;;;;;
+2F918;CJK COMPATIBILITY IDEOGRAPH-2F918;Lo;0;L;707D;;;;N;;;;;
+2F919;CJK COMPATIBILITY IDEOGRAPH-2F919;Lo;0;L;7077;;;;N;;;;;
+2F91A;CJK COMPATIBILITY IDEOGRAPH-2F91A;Lo;0;L;70AD;;;;N;;;;;
+2F91B;CJK COMPATIBILITY IDEOGRAPH-2F91B;Lo;0;L;20525;;;;N;;;;;
+2F91C;CJK COMPATIBILITY IDEOGRAPH-2F91C;Lo;0;L;7145;;;;N;;;;;
+2F91D;CJK COMPATIBILITY IDEOGRAPH-2F91D;Lo;0;L;24263;;;;N;;;;;
+2F91E;CJK COMPATIBILITY IDEOGRAPH-2F91E;Lo;0;L;719C;;;;N;;;;;
+2F91F;CJK COMPATIBILITY IDEOGRAPH-2F91F;Lo;0;L;243AB;;;;N;;;;;
+2F920;CJK COMPATIBILITY IDEOGRAPH-2F920;Lo;0;L;7228;;;;N;;;;;
+2F921;CJK COMPATIBILITY IDEOGRAPH-2F921;Lo;0;L;7235;;;;N;;;;;
+2F922;CJK COMPATIBILITY IDEOGRAPH-2F922;Lo;0;L;7250;;;;N;;;;;
+2F923;CJK COMPATIBILITY IDEOGRAPH-2F923;Lo;0;L;24608;;;;N;;;;;
+2F924;CJK COMPATIBILITY IDEOGRAPH-2F924;Lo;0;L;7280;;;;N;;;;;
+2F925;CJK COMPATIBILITY IDEOGRAPH-2F925;Lo;0;L;7295;;;;N;;;;;
+2F926;CJK COMPATIBILITY IDEOGRAPH-2F926;Lo;0;L;24735;;;;N;;;;;
+2F927;CJK COMPATIBILITY IDEOGRAPH-2F927;Lo;0;L;24814;;;;N;;;;;
+2F928;CJK COMPATIBILITY IDEOGRAPH-2F928;Lo;0;L;737A;;;;N;;;;;
+2F929;CJK COMPATIBILITY IDEOGRAPH-2F929;Lo;0;L;738B;;;;N;;;;;
+2F92A;CJK COMPATIBILITY IDEOGRAPH-2F92A;Lo;0;L;3EAC;;;;N;;;;;
+2F92B;CJK COMPATIBILITY IDEOGRAPH-2F92B;Lo;0;L;73A5;;;;N;;;;;
+2F92C;CJK COMPATIBILITY IDEOGRAPH-2F92C;Lo;0;L;3EB8;;;;N;;;;;
+2F92D;CJK COMPATIBILITY IDEOGRAPH-2F92D;Lo;0;L;3EB8;;;;N;;;;;
+2F92E;CJK COMPATIBILITY IDEOGRAPH-2F92E;Lo;0;L;7447;;;;N;;;;;
+2F92F;CJK COMPATIBILITY IDEOGRAPH-2F92F;Lo;0;L;745C;;;;N;;;;;
+2F930;CJK COMPATIBILITY IDEOGRAPH-2F930;Lo;0;L;7471;;;;N;;;;;
+2F931;CJK COMPATIBILITY IDEOGRAPH-2F931;Lo;0;L;7485;;;;N;;;;;
+2F932;CJK COMPATIBILITY IDEOGRAPH-2F932;Lo;0;L;74CA;;;;N;;;;;
+2F933;CJK COMPATIBILITY IDEOGRAPH-2F933;Lo;0;L;3F1B;;;;N;;;;;
+2F934;CJK COMPATIBILITY IDEOGRAPH-2F934;Lo;0;L;7524;;;;N;;;;;
+2F935;CJK COMPATIBILITY IDEOGRAPH-2F935;Lo;0;L;24C36;;;;N;;;;;
+2F936;CJK COMPATIBILITY IDEOGRAPH-2F936;Lo;0;L;753E;;;;N;;;;;
+2F937;CJK COMPATIBILITY IDEOGRAPH-2F937;Lo;0;L;24C92;;;;N;;;;;
+2F938;CJK COMPATIBILITY IDEOGRAPH-2F938;Lo;0;L;7570;;;;N;;;;;
+2F939;CJK COMPATIBILITY IDEOGRAPH-2F939;Lo;0;L;2219F;;;;N;;;;;
+2F93A;CJK COMPATIBILITY IDEOGRAPH-2F93A;Lo;0;L;7610;;;;N;;;;;
+2F93B;CJK COMPATIBILITY IDEOGRAPH-2F93B;Lo;0;L;24FA1;;;;N;;;;;
+2F93C;CJK COMPATIBILITY IDEOGRAPH-2F93C;Lo;0;L;24FB8;;;;N;;;;;
+2F93D;CJK COMPATIBILITY IDEOGRAPH-2F93D;Lo;0;L;25044;;;;N;;;;;
+2F93E;CJK COMPATIBILITY IDEOGRAPH-2F93E;Lo;0;L;3FFC;;;;N;;;;;
+2F93F;CJK COMPATIBILITY IDEOGRAPH-2F93F;Lo;0;L;4008;;;;N;;;;;
+2F940;CJK COMPATIBILITY IDEOGRAPH-2F940;Lo;0;L;76F4;;;;N;;;;;
+2F941;CJK COMPATIBILITY IDEOGRAPH-2F941;Lo;0;L;250F3;;;;N;;;;;
+2F942;CJK COMPATIBILITY IDEOGRAPH-2F942;Lo;0;L;250F2;;;;N;;;;;
+2F943;CJK COMPATIBILITY IDEOGRAPH-2F943;Lo;0;L;25119;;;;N;;;;;
+2F944;CJK COMPATIBILITY IDEOGRAPH-2F944;Lo;0;L;25133;;;;N;;;;;
+2F945;CJK COMPATIBILITY IDEOGRAPH-2F945;Lo;0;L;771E;;;;N;;;;;
+2F946;CJK COMPATIBILITY IDEOGRAPH-2F946;Lo;0;L;771F;;;;N;;;;;
+2F947;CJK COMPATIBILITY IDEOGRAPH-2F947;Lo;0;L;771F;;;;N;;;;;
+2F948;CJK COMPATIBILITY IDEOGRAPH-2F948;Lo;0;L;774A;;;;N;;;;;
+2F949;CJK COMPATIBILITY IDEOGRAPH-2F949;Lo;0;L;4039;;;;N;;;;;
+2F94A;CJK COMPATIBILITY IDEOGRAPH-2F94A;Lo;0;L;778B;;;;N;;;;;
+2F94B;CJK COMPATIBILITY IDEOGRAPH-2F94B;Lo;0;L;4046;;;;N;;;;;
+2F94C;CJK COMPATIBILITY IDEOGRAPH-2F94C;Lo;0;L;4096;;;;N;;;;;
+2F94D;CJK COMPATIBILITY IDEOGRAPH-2F94D;Lo;0;L;2541D;;;;N;;;;;
+2F94E;CJK COMPATIBILITY IDEOGRAPH-2F94E;Lo;0;L;784E;;;;N;;;;;
+2F94F;CJK COMPATIBILITY IDEOGRAPH-2F94F;Lo;0;L;788C;;;;N;;;;;
+2F950;CJK COMPATIBILITY IDEOGRAPH-2F950;Lo;0;L;78CC;;;;N;;;;;
+2F951;CJK COMPATIBILITY IDEOGRAPH-2F951;Lo;0;L;40E3;;;;N;;;;;
+2F952;CJK COMPATIBILITY IDEOGRAPH-2F952;Lo;0;L;25626;;;;N;;;;;
+2F953;CJK COMPATIBILITY IDEOGRAPH-2F953;Lo;0;L;7956;;;;N;;;;;
+2F954;CJK COMPATIBILITY IDEOGRAPH-2F954;Lo;0;L;2569A;;;;N;;;;;
+2F955;CJK COMPATIBILITY IDEOGRAPH-2F955;Lo;0;L;256C5;;;;N;;;;;
+2F956;CJK COMPATIBILITY IDEOGRAPH-2F956;Lo;0;L;798F;;;;N;;;;;
+2F957;CJK COMPATIBILITY IDEOGRAPH-2F957;Lo;0;L;79EB;;;;N;;;;;
+2F958;CJK COMPATIBILITY IDEOGRAPH-2F958;Lo;0;L;412F;;;;N;;;;;
+2F959;CJK COMPATIBILITY IDEOGRAPH-2F959;Lo;0;L;7A40;;;;N;;;;;
+2F95A;CJK COMPATIBILITY IDEOGRAPH-2F95A;Lo;0;L;7A4A;;;;N;;;;;
+2F95B;CJK COMPATIBILITY IDEOGRAPH-2F95B;Lo;0;L;7A4F;;;;N;;;;;
+2F95C;CJK COMPATIBILITY IDEOGRAPH-2F95C;Lo;0;L;2597C;;;;N;;;;;
+2F95D;CJK COMPATIBILITY IDEOGRAPH-2F95D;Lo;0;L;25AA7;;;;N;;;;;
+2F95E;CJK COMPATIBILITY IDEOGRAPH-2F95E;Lo;0;L;25AA7;;;;N;;;;;
+2F95F;CJK COMPATIBILITY IDEOGRAPH-2F95F;Lo;0;L;7AEE;;;;N;;;;;
+2F960;CJK COMPATIBILITY IDEOGRAPH-2F960;Lo;0;L;4202;;;;N;;;;;
+2F961;CJK COMPATIBILITY IDEOGRAPH-2F961;Lo;0;L;25BAB;;;;N;;;;;
+2F962;CJK COMPATIBILITY IDEOGRAPH-2F962;Lo;0;L;7BC6;;;;N;;;;;
+2F963;CJK COMPATIBILITY IDEOGRAPH-2F963;Lo;0;L;7BC9;;;;N;;;;;
+2F964;CJK COMPATIBILITY IDEOGRAPH-2F964;Lo;0;L;4227;;;;N;;;;;
+2F965;CJK COMPATIBILITY IDEOGRAPH-2F965;Lo;0;L;25C80;;;;N;;;;;
+2F966;CJK COMPATIBILITY IDEOGRAPH-2F966;Lo;0;L;7CD2;;;;N;;;;;
+2F967;CJK COMPATIBILITY IDEOGRAPH-2F967;Lo;0;L;42A0;;;;N;;;;;
+2F968;CJK COMPATIBILITY IDEOGRAPH-2F968;Lo;0;L;7CE8;;;;N;;;;;
+2F969;CJK COMPATIBILITY IDEOGRAPH-2F969;Lo;0;L;7CE3;;;;N;;;;;
+2F96A;CJK COMPATIBILITY IDEOGRAPH-2F96A;Lo;0;L;7D00;;;;N;;;;;
+2F96B;CJK COMPATIBILITY IDEOGRAPH-2F96B;Lo;0;L;25F86;;;;N;;;;;
+2F96C;CJK COMPATIBILITY IDEOGRAPH-2F96C;Lo;0;L;7D63;;;;N;;;;;
+2F96D;CJK COMPATIBILITY IDEOGRAPH-2F96D;Lo;0;L;4301;;;;N;;;;;
+2F96E;CJK COMPATIBILITY IDEOGRAPH-2F96E;Lo;0;L;7DC7;;;;N;;;;;
+2F96F;CJK COMPATIBILITY IDEOGRAPH-2F96F;Lo;0;L;7E02;;;;N;;;;;
+2F970;CJK COMPATIBILITY IDEOGRAPH-2F970;Lo;0;L;7E45;;;;N;;;;;
+2F971;CJK COMPATIBILITY IDEOGRAPH-2F971;Lo;0;L;4334;;;;N;;;;;
+2F972;CJK COMPATIBILITY IDEOGRAPH-2F972;Lo;0;L;26228;;;;N;;;;;
+2F973;CJK COMPATIBILITY IDEOGRAPH-2F973;Lo;0;L;26247;;;;N;;;;;
+2F974;CJK COMPATIBILITY IDEOGRAPH-2F974;Lo;0;L;4359;;;;N;;;;;
+2F975;CJK COMPATIBILITY IDEOGRAPH-2F975;Lo;0;L;262D9;;;;N;;;;;
+2F976;CJK COMPATIBILITY IDEOGRAPH-2F976;Lo;0;L;7F7A;;;;N;;;;;
+2F977;CJK COMPATIBILITY IDEOGRAPH-2F977;Lo;0;L;2633E;;;;N;;;;;
+2F978;CJK COMPATIBILITY IDEOGRAPH-2F978;Lo;0;L;7F95;;;;N;;;;;
+2F979;CJK COMPATIBILITY IDEOGRAPH-2F979;Lo;0;L;7FFA;;;;N;;;;;
+2F97A;CJK COMPATIBILITY IDEOGRAPH-2F97A;Lo;0;L;8005;;;;N;;;;;
+2F97B;CJK COMPATIBILITY IDEOGRAPH-2F97B;Lo;0;L;264DA;;;;N;;;;;
+2F97C;CJK COMPATIBILITY IDEOGRAPH-2F97C;Lo;0;L;26523;;;;N;;;;;
+2F97D;CJK COMPATIBILITY IDEOGRAPH-2F97D;Lo;0;L;8060;;;;N;;;;;
+2F97E;CJK COMPATIBILITY IDEOGRAPH-2F97E;Lo;0;L;265A8;;;;N;;;;;
+2F97F;CJK COMPATIBILITY IDEOGRAPH-2F97F;Lo;0;L;8070;;;;N;;;;;
+2F980;CJK COMPATIBILITY IDEOGRAPH-2F980;Lo;0;L;2335F;;;;N;;;;;
+2F981;CJK COMPATIBILITY IDEOGRAPH-2F981;Lo;0;L;43D5;;;;N;;;;;
+2F982;CJK COMPATIBILITY IDEOGRAPH-2F982;Lo;0;L;80B2;;;;N;;;;;
+2F983;CJK COMPATIBILITY IDEOGRAPH-2F983;Lo;0;L;8103;;;;N;;;;;
+2F984;CJK COMPATIBILITY IDEOGRAPH-2F984;Lo;0;L;440B;;;;N;;;;;
+2F985;CJK COMPATIBILITY IDEOGRAPH-2F985;Lo;0;L;813E;;;;N;;;;;
+2F986;CJK COMPATIBILITY IDEOGRAPH-2F986;Lo;0;L;5AB5;;;;N;;;;;
+2F987;CJK COMPATIBILITY IDEOGRAPH-2F987;Lo;0;L;267A7;;;;N;;;;;
+2F988;CJK COMPATIBILITY IDEOGRAPH-2F988;Lo;0;L;267B5;;;;N;;;;;
+2F989;CJK COMPATIBILITY IDEOGRAPH-2F989;Lo;0;L;23393;;;;N;;;;;
+2F98A;CJK COMPATIBILITY IDEOGRAPH-2F98A;Lo;0;L;2339C;;;;N;;;;;
+2F98B;CJK COMPATIBILITY IDEOGRAPH-2F98B;Lo;0;L;8201;;;;N;;;;;
+2F98C;CJK COMPATIBILITY IDEOGRAPH-2F98C;Lo;0;L;8204;;;;N;;;;;
+2F98D;CJK COMPATIBILITY IDEOGRAPH-2F98D;Lo;0;L;8F9E;;;;N;;;;;
+2F98E;CJK COMPATIBILITY IDEOGRAPH-2F98E;Lo;0;L;446B;;;;N;;;;;
+2F98F;CJK COMPATIBILITY IDEOGRAPH-2F98F;Lo;0;L;8291;;;;N;;;;;
+2F990;CJK COMPATIBILITY IDEOGRAPH-2F990;Lo;0;L;828B;;;;N;;;;;
+2F991;CJK COMPATIBILITY IDEOGRAPH-2F991;Lo;0;L;829D;;;;N;;;;;
+2F992;CJK COMPATIBILITY IDEOGRAPH-2F992;Lo;0;L;52B3;;;;N;;;;;
+2F993;CJK COMPATIBILITY IDEOGRAPH-2F993;Lo;0;L;82B1;;;;N;;;;;
+2F994;CJK COMPATIBILITY IDEOGRAPH-2F994;Lo;0;L;82B3;;;;N;;;;;
+2F995;CJK COMPATIBILITY IDEOGRAPH-2F995;Lo;0;L;82BD;;;;N;;;;;
+2F996;CJK COMPATIBILITY IDEOGRAPH-2F996;Lo;0;L;82E6;;;;N;;;;;
+2F997;CJK COMPATIBILITY IDEOGRAPH-2F997;Lo;0;L;26B3C;;;;N;;;;;
+2F998;CJK COMPATIBILITY IDEOGRAPH-2F998;Lo;0;L;82E5;;;;N;;;;;
+2F999;CJK COMPATIBILITY IDEOGRAPH-2F999;Lo;0;L;831D;;;;N;;;;;
+2F99A;CJK COMPATIBILITY IDEOGRAPH-2F99A;Lo;0;L;8363;;;;N;;;;;
+2F99B;CJK COMPATIBILITY IDEOGRAPH-2F99B;Lo;0;L;83AD;;;;N;;;;;
+2F99C;CJK COMPATIBILITY IDEOGRAPH-2F99C;Lo;0;L;8323;;;;N;;;;;
+2F99D;CJK COMPATIBILITY IDEOGRAPH-2F99D;Lo;0;L;83BD;;;;N;;;;;
+2F99E;CJK COMPATIBILITY IDEOGRAPH-2F99E;Lo;0;L;83E7;;;;N;;;;;
+2F99F;CJK COMPATIBILITY IDEOGRAPH-2F99F;Lo;0;L;8457;;;;N;;;;;
+2F9A0;CJK COMPATIBILITY IDEOGRAPH-2F9A0;Lo;0;L;8353;;;;N;;;;;
+2F9A1;CJK COMPATIBILITY IDEOGRAPH-2F9A1;Lo;0;L;83CA;;;;N;;;;;
+2F9A2;CJK COMPATIBILITY IDEOGRAPH-2F9A2;Lo;0;L;83CC;;;;N;;;;;
+2F9A3;CJK COMPATIBILITY IDEOGRAPH-2F9A3;Lo;0;L;83DC;;;;N;;;;;
+2F9A4;CJK COMPATIBILITY IDEOGRAPH-2F9A4;Lo;0;L;26C36;;;;N;;;;;
+2F9A5;CJK COMPATIBILITY IDEOGRAPH-2F9A5;Lo;0;L;26D6B;;;;N;;;;;
+2F9A6;CJK COMPATIBILITY IDEOGRAPH-2F9A6;Lo;0;L;26CD5;;;;N;;;;;
+2F9A7;CJK COMPATIBILITY IDEOGRAPH-2F9A7;Lo;0;L;452B;;;;N;;;;;
+2F9A8;CJK COMPATIBILITY IDEOGRAPH-2F9A8;Lo;0;L;84F1;;;;N;;;;;
+2F9A9;CJK COMPATIBILITY IDEOGRAPH-2F9A9;Lo;0;L;84F3;;;;N;;;;;
+2F9AA;CJK COMPATIBILITY IDEOGRAPH-2F9AA;Lo;0;L;8516;;;;N;;;;;
+2F9AB;CJK COMPATIBILITY IDEOGRAPH-2F9AB;Lo;0;L;273CA;;;;N;;;;;
+2F9AC;CJK COMPATIBILITY IDEOGRAPH-2F9AC;Lo;0;L;8564;;;;N;;;;;
+2F9AD;CJK COMPATIBILITY IDEOGRAPH-2F9AD;Lo;0;L;26F2C;;;;N;;;;;
+2F9AE;CJK COMPATIBILITY IDEOGRAPH-2F9AE;Lo;0;L;455D;;;;N;;;;;
+2F9AF;CJK COMPATIBILITY IDEOGRAPH-2F9AF;Lo;0;L;4561;;;;N;;;;;
+2F9B0;CJK COMPATIBILITY IDEOGRAPH-2F9B0;Lo;0;L;26FB1;;;;N;;;;;
+2F9B1;CJK COMPATIBILITY IDEOGRAPH-2F9B1;Lo;0;L;270D2;;;;N;;;;;
+2F9B2;CJK COMPATIBILITY IDEOGRAPH-2F9B2;Lo;0;L;456B;;;;N;;;;;
+2F9B3;CJK COMPATIBILITY IDEOGRAPH-2F9B3;Lo;0;L;8650;;;;N;;;;;
+2F9B4;CJK COMPATIBILITY IDEOGRAPH-2F9B4;Lo;0;L;865C;;;;N;;;;;
+2F9B5;CJK COMPATIBILITY IDEOGRAPH-2F9B5;Lo;0;L;8667;;;;N;;;;;
+2F9B6;CJK COMPATIBILITY IDEOGRAPH-2F9B6;Lo;0;L;8669;;;;N;;;;;
+2F9B7;CJK COMPATIBILITY IDEOGRAPH-2F9B7;Lo;0;L;86A9;;;;N;;;;;
+2F9B8;CJK COMPATIBILITY IDEOGRAPH-2F9B8;Lo;0;L;8688;;;;N;;;;;
+2F9B9;CJK COMPATIBILITY IDEOGRAPH-2F9B9;Lo;0;L;870E;;;;N;;;;;
+2F9BA;CJK COMPATIBILITY IDEOGRAPH-2F9BA;Lo;0;L;86E2;;;;N;;;;;
+2F9BB;CJK COMPATIBILITY IDEOGRAPH-2F9BB;Lo;0;L;8779;;;;N;;;;;
+2F9BC;CJK COMPATIBILITY IDEOGRAPH-2F9BC;Lo;0;L;8728;;;;N;;;;;
+2F9BD;CJK COMPATIBILITY IDEOGRAPH-2F9BD;Lo;0;L;876B;;;;N;;;;;
+2F9BE;CJK COMPATIBILITY IDEOGRAPH-2F9BE;Lo;0;L;8786;;;;N;;;;;
+2F9BF;CJK COMPATIBILITY IDEOGRAPH-2F9BF;Lo;0;L;45D7;;;;N;;;;;
+2F9C0;CJK COMPATIBILITY IDEOGRAPH-2F9C0;Lo;0;L;87E1;;;;N;;;;;
+2F9C1;CJK COMPATIBILITY IDEOGRAPH-2F9C1;Lo;0;L;8801;;;;N;;;;;
+2F9C2;CJK COMPATIBILITY IDEOGRAPH-2F9C2;Lo;0;L;45F9;;;;N;;;;;
+2F9C3;CJK COMPATIBILITY IDEOGRAPH-2F9C3;Lo;0;L;8860;;;;N;;;;;
+2F9C4;CJK COMPATIBILITY IDEOGRAPH-2F9C4;Lo;0;L;8863;;;;N;;;;;
+2F9C5;CJK COMPATIBILITY IDEOGRAPH-2F9C5;Lo;0;L;27667;;;;N;;;;;
+2F9C6;CJK COMPATIBILITY IDEOGRAPH-2F9C6;Lo;0;L;88D7;;;;N;;;;;
+2F9C7;CJK COMPATIBILITY IDEOGRAPH-2F9C7;Lo;0;L;88DE;;;;N;;;;;
+2F9C8;CJK COMPATIBILITY IDEOGRAPH-2F9C8;Lo;0;L;4635;;;;N;;;;;
+2F9C9;CJK COMPATIBILITY IDEOGRAPH-2F9C9;Lo;0;L;88FA;;;;N;;;;;
+2F9CA;CJK COMPATIBILITY IDEOGRAPH-2F9CA;Lo;0;L;34BB;;;;N;;;;;
+2F9CB;CJK COMPATIBILITY IDEOGRAPH-2F9CB;Lo;0;L;278AE;;;;N;;;;;
+2F9CC;CJK COMPATIBILITY IDEOGRAPH-2F9CC;Lo;0;L;27966;;;;N;;;;;
+2F9CD;CJK COMPATIBILITY IDEOGRAPH-2F9CD;Lo;0;L;46BE;;;;N;;;;;
+2F9CE;CJK COMPATIBILITY IDEOGRAPH-2F9CE;Lo;0;L;46C7;;;;N;;;;;
+2F9CF;CJK COMPATIBILITY IDEOGRAPH-2F9CF;Lo;0;L;8AA0;;;;N;;;;;
+2F9D0;CJK COMPATIBILITY IDEOGRAPH-2F9D0;Lo;0;L;8AED;;;;N;;;;;
+2F9D1;CJK COMPATIBILITY IDEOGRAPH-2F9D1;Lo;0;L;8B8A;;;;N;;;;;
+2F9D2;CJK COMPATIBILITY IDEOGRAPH-2F9D2;Lo;0;L;8C55;;;;N;;;;;
+2F9D3;CJK COMPATIBILITY IDEOGRAPH-2F9D3;Lo;0;L;27CA8;;;;N;;;;;
+2F9D4;CJK COMPATIBILITY IDEOGRAPH-2F9D4;Lo;0;L;8CAB;;;;N;;;;;
+2F9D5;CJK COMPATIBILITY IDEOGRAPH-2F9D5;Lo;0;L;8CC1;;;;N;;;;;
+2F9D6;CJK COMPATIBILITY IDEOGRAPH-2F9D6;Lo;0;L;8D1B;;;;N;;;;;
+2F9D7;CJK COMPATIBILITY IDEOGRAPH-2F9D7;Lo;0;L;8D77;;;;N;;;;;
+2F9D8;CJK COMPATIBILITY IDEOGRAPH-2F9D8;Lo;0;L;27F2F;;;;N;;;;;
+2F9D9;CJK COMPATIBILITY IDEOGRAPH-2F9D9;Lo;0;L;20804;;;;N;;;;;
+2F9DA;CJK COMPATIBILITY IDEOGRAPH-2F9DA;Lo;0;L;8DCB;;;;N;;;;;
+2F9DB;CJK COMPATIBILITY IDEOGRAPH-2F9DB;Lo;0;L;8DBC;;;;N;;;;;
+2F9DC;CJK COMPATIBILITY IDEOGRAPH-2F9DC;Lo;0;L;8DF0;;;;N;;;;;
+2F9DD;CJK COMPATIBILITY IDEOGRAPH-2F9DD;Lo;0;L;208DE;;;;N;;;;;
+2F9DE;CJK COMPATIBILITY IDEOGRAPH-2F9DE;Lo;0;L;8ED4;;;;N;;;;;
+2F9DF;CJK COMPATIBILITY IDEOGRAPH-2F9DF;Lo;0;L;8F38;;;;N;;;;;
+2F9E0;CJK COMPATIBILITY IDEOGRAPH-2F9E0;Lo;0;L;285D2;;;;N;;;;;
+2F9E1;CJK COMPATIBILITY IDEOGRAPH-2F9E1;Lo;0;L;285ED;;;;N;;;;;
+2F9E2;CJK COMPATIBILITY IDEOGRAPH-2F9E2;Lo;0;L;9094;;;;N;;;;;
+2F9E3;CJK COMPATIBILITY IDEOGRAPH-2F9E3;Lo;0;L;90F1;;;;N;;;;;
+2F9E4;CJK COMPATIBILITY IDEOGRAPH-2F9E4;Lo;0;L;9111;;;;N;;;;;
+2F9E5;CJK COMPATIBILITY IDEOGRAPH-2F9E5;Lo;0;L;2872E;;;;N;;;;;
+2F9E6;CJK COMPATIBILITY IDEOGRAPH-2F9E6;Lo;0;L;911B;;;;N;;;;;
+2F9E7;CJK COMPATIBILITY IDEOGRAPH-2F9E7;Lo;0;L;9238;;;;N;;;;;
+2F9E8;CJK COMPATIBILITY IDEOGRAPH-2F9E8;Lo;0;L;92D7;;;;N;;;;;
+2F9E9;CJK COMPATIBILITY IDEOGRAPH-2F9E9;Lo;0;L;92D8;;;;N;;;;;
+2F9EA;CJK COMPATIBILITY IDEOGRAPH-2F9EA;Lo;0;L;927C;;;;N;;;;;
+2F9EB;CJK COMPATIBILITY IDEOGRAPH-2F9EB;Lo;0;L;93F9;;;;N;;;;;
+2F9EC;CJK COMPATIBILITY IDEOGRAPH-2F9EC;Lo;0;L;9415;;;;N;;;;;
+2F9ED;CJK COMPATIBILITY IDEOGRAPH-2F9ED;Lo;0;L;28BFA;;;;N;;;;;
+2F9EE;CJK COMPATIBILITY IDEOGRAPH-2F9EE;Lo;0;L;958B;;;;N;;;;;
+2F9EF;CJK COMPATIBILITY IDEOGRAPH-2F9EF;Lo;0;L;4995;;;;N;;;;;
+2F9F0;CJK COMPATIBILITY IDEOGRAPH-2F9F0;Lo;0;L;95B7;;;;N;;;;;
+2F9F1;CJK COMPATIBILITY IDEOGRAPH-2F9F1;Lo;0;L;28D77;;;;N;;;;;
+2F9F2;CJK COMPATIBILITY IDEOGRAPH-2F9F2;Lo;0;L;49E6;;;;N;;;;;
+2F9F3;CJK COMPATIBILITY IDEOGRAPH-2F9F3;Lo;0;L;96C3;;;;N;;;;;
+2F9F4;CJK COMPATIBILITY IDEOGRAPH-2F9F4;Lo;0;L;5DB2;;;;N;;;;;
+2F9F5;CJK COMPATIBILITY IDEOGRAPH-2F9F5;Lo;0;L;9723;;;;N;;;;;
+2F9F6;CJK COMPATIBILITY IDEOGRAPH-2F9F6;Lo;0;L;29145;;;;N;;;;;
+2F9F7;CJK COMPATIBILITY IDEOGRAPH-2F9F7;Lo;0;L;2921A;;;;N;;;;;
+2F9F8;CJK COMPATIBILITY IDEOGRAPH-2F9F8;Lo;0;L;4A6E;;;;N;;;;;
+2F9F9;CJK COMPATIBILITY IDEOGRAPH-2F9F9;Lo;0;L;4A76;;;;N;;;;;
+2F9FA;CJK COMPATIBILITY IDEOGRAPH-2F9FA;Lo;0;L;97E0;;;;N;;;;;
+2F9FB;CJK COMPATIBILITY IDEOGRAPH-2F9FB;Lo;0;L;2940A;;;;N;;;;;
+2F9FC;CJK COMPATIBILITY IDEOGRAPH-2F9FC;Lo;0;L;4AB2;;;;N;;;;;
+2F9FD;CJK COMPATIBILITY IDEOGRAPH-2F9FD;Lo;0;L;29496;;;;N;;;;;
+2F9FE;CJK COMPATIBILITY IDEOGRAPH-2F9FE;Lo;0;L;980B;;;;N;;;;;
+2F9FF;CJK COMPATIBILITY IDEOGRAPH-2F9FF;Lo;0;L;980B;;;;N;;;;;
+2FA00;CJK COMPATIBILITY IDEOGRAPH-2FA00;Lo;0;L;9829;;;;N;;;;;
+2FA01;CJK COMPATIBILITY IDEOGRAPH-2FA01;Lo;0;L;295B6;;;;N;;;;;
+2FA02;CJK COMPATIBILITY IDEOGRAPH-2FA02;Lo;0;L;98E2;;;;N;;;;;
+2FA03;CJK COMPATIBILITY IDEOGRAPH-2FA03;Lo;0;L;4B33;;;;N;;;;;
+2FA04;CJK COMPATIBILITY IDEOGRAPH-2FA04;Lo;0;L;9929;;;;N;;;;;
+2FA05;CJK COMPATIBILITY IDEOGRAPH-2FA05;Lo;0;L;99A7;;;;N;;;;;
+2FA06;CJK COMPATIBILITY IDEOGRAPH-2FA06;Lo;0;L;99C2;;;;N;;;;;
+2FA07;CJK COMPATIBILITY IDEOGRAPH-2FA07;Lo;0;L;99FE;;;;N;;;;;
+2FA08;CJK COMPATIBILITY IDEOGRAPH-2FA08;Lo;0;L;4BCE;;;;N;;;;;
+2FA09;CJK COMPATIBILITY IDEOGRAPH-2FA09;Lo;0;L;29B30;;;;N;;;;;
+2FA0A;CJK COMPATIBILITY IDEOGRAPH-2FA0A;Lo;0;L;9B12;;;;N;;;;;
+2FA0B;CJK COMPATIBILITY IDEOGRAPH-2FA0B;Lo;0;L;9C40;;;;N;;;;;
+2FA0C;CJK COMPATIBILITY IDEOGRAPH-2FA0C;Lo;0;L;9CFD;;;;N;;;;;
+2FA0D;CJK COMPATIBILITY IDEOGRAPH-2FA0D;Lo;0;L;4CCE;;;;N;;;;;
+2FA0E;CJK COMPATIBILITY IDEOGRAPH-2FA0E;Lo;0;L;4CED;;;;N;;;;;
+2FA0F;CJK COMPATIBILITY IDEOGRAPH-2FA0F;Lo;0;L;9D67;;;;N;;;;;
+2FA10;CJK COMPATIBILITY IDEOGRAPH-2FA10;Lo;0;L;2A0CE;;;;N;;;;;
+2FA11;CJK COMPATIBILITY IDEOGRAPH-2FA11;Lo;0;L;4CF8;;;;N;;;;;
+2FA12;CJK COMPATIBILITY IDEOGRAPH-2FA12;Lo;0;L;2A105;;;;N;;;;;
+2FA13;CJK COMPATIBILITY IDEOGRAPH-2FA13;Lo;0;L;2A20E;;;;N;;;;;
+2FA14;CJK COMPATIBILITY IDEOGRAPH-2FA14;Lo;0;L;2A291;;;;N;;;;;
+2FA15;CJK COMPATIBILITY IDEOGRAPH-2FA15;Lo;0;L;9EBB;;;;N;;;;;
+2FA16;CJK COMPATIBILITY IDEOGRAPH-2FA16;Lo;0;L;4D56;;;;N;;;;;
+2FA17;CJK COMPATIBILITY IDEOGRAPH-2FA17;Lo;0;L;9EF9;;;;N;;;;;
+2FA18;CJK COMPATIBILITY IDEOGRAPH-2FA18;Lo;0;L;9EFE;;;;N;;;;;
+2FA19;CJK COMPATIBILITY IDEOGRAPH-2FA19;Lo;0;L;9F05;;;;N;;;;;
+2FA1A;CJK COMPATIBILITY IDEOGRAPH-2FA1A;Lo;0;L;9F0F;;;;N;;;;;
+2FA1B;CJK COMPATIBILITY IDEOGRAPH-2FA1B;Lo;0;L;9F16;;;;N;;;;;
+2FA1C;CJK COMPATIBILITY IDEOGRAPH-2FA1C;Lo;0;L;9F3B;;;;N;;;;;
+2FA1D;CJK COMPATIBILITY IDEOGRAPH-2FA1D;Lo;0;L;2A600;;;;N;;;;;
+E0001;LANGUAGE TAG;Cf;0;BN;;;;;N;;;;;
+E0020;TAG SPACE;Cf;0;BN;;;;;N;;;;;
+E0021;TAG EXCLAMATION MARK;Cf;0;BN;;;;;N;;;;;
+E0022;TAG QUOTATION MARK;Cf;0;BN;;;;;N;;;;;
+E0023;TAG NUMBER SIGN;Cf;0;BN;;;;;N;;;;;
+E0024;TAG DOLLAR SIGN;Cf;0;BN;;;;;N;;;;;
+E0025;TAG PERCENT SIGN;Cf;0;BN;;;;;N;;;;;
+E0026;TAG AMPERSAND;Cf;0;BN;;;;;N;;;;;
+E0027;TAG APOSTROPHE;Cf;0;BN;;;;;N;;;;;
+E0028;TAG LEFT PARENTHESIS;Cf;0;BN;;;;;N;;;;;
+E0029;TAG RIGHT PARENTHESIS;Cf;0;BN;;;;;N;;;;;
+E002A;TAG ASTERISK;Cf;0;BN;;;;;N;;;;;
+E002B;TAG PLUS SIGN;Cf;0;BN;;;;;N;;;;;
+E002C;TAG COMMA;Cf;0;BN;;;;;N;;;;;
+E002D;TAG HYPHEN-MINUS;Cf;0;BN;;;;;N;;;;;
+E002E;TAG FULL STOP;Cf;0;BN;;;;;N;;;;;
+E002F;TAG SOLIDUS;Cf;0;BN;;;;;N;;;;;
+E0030;TAG DIGIT ZERO;Cf;0;BN;;;;;N;;;;;
+E0031;TAG DIGIT ONE;Cf;0;BN;;;;;N;;;;;
+E0032;TAG DIGIT TWO;Cf;0;BN;;;;;N;;;;;
+E0033;TAG DIGIT THREE;Cf;0;BN;;;;;N;;;;;
+E0034;TAG DIGIT FOUR;Cf;0;BN;;;;;N;;;;;
+E0035;TAG DIGIT FIVE;Cf;0;BN;;;;;N;;;;;
+E0036;TAG DIGIT SIX;Cf;0;BN;;;;;N;;;;;
+E0037;TAG DIGIT SEVEN;Cf;0;BN;;;;;N;;;;;
+E0038;TAG DIGIT EIGHT;Cf;0;BN;;;;;N;;;;;
+E0039;TAG DIGIT NINE;Cf;0;BN;;;;;N;;;;;
+E003A;TAG COLON;Cf;0;BN;;;;;N;;;;;
+E003B;TAG SEMICOLON;Cf;0;BN;;;;;N;;;;;
+E003C;TAG LESS-THAN SIGN;Cf;0;BN;;;;;N;;;;;
+E003D;TAG EQUALS SIGN;Cf;0;BN;;;;;N;;;;;
+E003E;TAG GREATER-THAN SIGN;Cf;0;BN;;;;;N;;;;;
+E003F;TAG QUESTION MARK;Cf;0;BN;;;;;N;;;;;
+E0040;TAG COMMERCIAL AT;Cf;0;BN;;;;;N;;;;;
+E0041;TAG LATIN CAPITAL LETTER A;Cf;0;BN;;;;;N;;;;;
+E0042;TAG LATIN CAPITAL LETTER B;Cf;0;BN;;;;;N;;;;;
+E0043;TAG LATIN CAPITAL LETTER C;Cf;0;BN;;;;;N;;;;;
+E0044;TAG LATIN CAPITAL LETTER D;Cf;0;BN;;;;;N;;;;;
+E0045;TAG LATIN CAPITAL LETTER E;Cf;0;BN;;;;;N;;;;;
+E0046;TAG LATIN CAPITAL LETTER F;Cf;0;BN;;;;;N;;;;;
+E0047;TAG LATIN CAPITAL LETTER G;Cf;0;BN;;;;;N;;;;;
+E0048;TAG LATIN CAPITAL LETTER H;Cf;0;BN;;;;;N;;;;;
+E0049;TAG LATIN CAPITAL LETTER I;Cf;0;BN;;;;;N;;;;;
+E004A;TAG LATIN CAPITAL LETTER J;Cf;0;BN;;;;;N;;;;;
+E004B;TAG LATIN CAPITAL LETTER K;Cf;0;BN;;;;;N;;;;;
+E004C;TAG LATIN CAPITAL LETTER L;Cf;0;BN;;;;;N;;;;;
+E004D;TAG LATIN CAPITAL LETTER M;Cf;0;BN;;;;;N;;;;;
+E004E;TAG LATIN CAPITAL LETTER N;Cf;0;BN;;;;;N;;;;;
+E004F;TAG LATIN CAPITAL LETTER O;Cf;0;BN;;;;;N;;;;;
+E0050;TAG LATIN CAPITAL LETTER P;Cf;0;BN;;;;;N;;;;;
+E0051;TAG LATIN CAPITAL LETTER Q;Cf;0;BN;;;;;N;;;;;
+E0052;TAG LATIN CAPITAL LETTER R;Cf;0;BN;;;;;N;;;;;
+E0053;TAG LATIN CAPITAL LETTER S;Cf;0;BN;;;;;N;;;;;
+E0054;TAG LATIN CAPITAL LETTER T;Cf;0;BN;;;;;N;;;;;
+E0055;TAG LATIN CAPITAL LETTER U;Cf;0;BN;;;;;N;;;;;
+E0056;TAG LATIN CAPITAL LETTER V;Cf;0;BN;;;;;N;;;;;
+E0057;TAG LATIN CAPITAL LETTER W;Cf;0;BN;;;;;N;;;;;
+E0058;TAG LATIN CAPITAL LETTER X;Cf;0;BN;;;;;N;;;;;
+E0059;TAG LATIN CAPITAL LETTER Y;Cf;0;BN;;;;;N;;;;;
+E005A;TAG LATIN CAPITAL LETTER Z;Cf;0;BN;;;;;N;;;;;
+E005B;TAG LEFT SQUARE BRACKET;Cf;0;BN;;;;;N;;;;;
+E005C;TAG REVERSE SOLIDUS;Cf;0;BN;;;;;N;;;;;
+E005D;TAG RIGHT SQUARE BRACKET;Cf;0;BN;;;;;N;;;;;
+E005E;TAG CIRCUMFLEX ACCENT;Cf;0;BN;;;;;N;;;;;
+E005F;TAG LOW LINE;Cf;0;BN;;;;;N;;;;;
+E0060;TAG GRAVE ACCENT;Cf;0;BN;;;;;N;;;;;
+E0061;TAG LATIN SMALL LETTER A;Cf;0;BN;;;;;N;;;;;
+E0062;TAG LATIN SMALL LETTER B;Cf;0;BN;;;;;N;;;;;
+E0063;TAG LATIN SMALL LETTER C;Cf;0;BN;;;;;N;;;;;
+E0064;TAG LATIN SMALL LETTER D;Cf;0;BN;;;;;N;;;;;
+E0065;TAG LATIN SMALL LETTER E;Cf;0;BN;;;;;N;;;;;
+E0066;TAG LATIN SMALL LETTER F;Cf;0;BN;;;;;N;;;;;
+E0067;TAG LATIN SMALL LETTER G;Cf;0;BN;;;;;N;;;;;
+E0068;TAG LATIN SMALL LETTER H;Cf;0;BN;;;;;N;;;;;
+E0069;TAG LATIN SMALL LETTER I;Cf;0;BN;;;;;N;;;;;
+E006A;TAG LATIN SMALL LETTER J;Cf;0;BN;;;;;N;;;;;
+E006B;TAG LATIN SMALL LETTER K;Cf;0;BN;;;;;N;;;;;
+E006C;TAG LATIN SMALL LETTER L;Cf;0;BN;;;;;N;;;;;
+E006D;TAG LATIN SMALL LETTER M;Cf;0;BN;;;;;N;;;;;
+E006E;TAG LATIN SMALL LETTER N;Cf;0;BN;;;;;N;;;;;
+E006F;TAG LATIN SMALL LETTER O;Cf;0;BN;;;;;N;;;;;
+E0070;TAG LATIN SMALL LETTER P;Cf;0;BN;;;;;N;;;;;
+E0071;TAG LATIN SMALL LETTER Q;Cf;0;BN;;;;;N;;;;;
+E0072;TAG LATIN SMALL LETTER R;Cf;0;BN;;;;;N;;;;;
+E0073;TAG LATIN SMALL LETTER S;Cf;0;BN;;;;;N;;;;;
+E0074;TAG LATIN SMALL LETTER T;Cf;0;BN;;;;;N;;;;;
+E0075;TAG LATIN SMALL LETTER U;Cf;0;BN;;;;;N;;;;;
+E0076;TAG LATIN SMALL LETTER V;Cf;0;BN;;;;;N;;;;;
+E0077;TAG LATIN SMALL LETTER W;Cf;0;BN;;;;;N;;;;;
+E0078;TAG LATIN SMALL LETTER X;Cf;0;BN;;;;;N;;;;;
+E0079;TAG LATIN SMALL LETTER Y;Cf;0;BN;;;;;N;;;;;
+E007A;TAG LATIN SMALL LETTER Z;Cf;0;BN;;;;;N;;;;;
+E007B;TAG LEFT CURLY BRACKET;Cf;0;BN;;;;;N;;;;;
+E007C;TAG VERTICAL LINE;Cf;0;BN;;;;;N;;;;;
+E007D;TAG RIGHT CURLY BRACKET;Cf;0;BN;;;;;N;;;;;
+E007E;TAG TILDE;Cf;0;BN;;;;;N;;;;;
+E007F;CANCEL TAG;Cf;0;BN;;;;;N;;;;;
+E0100;VARIATION SELECTOR-17;Mn;0;NSM;;;;;N;;;;;
+E0101;VARIATION SELECTOR-18;Mn;0;NSM;;;;;N;;;;;
+E0102;VARIATION SELECTOR-19;Mn;0;NSM;;;;;N;;;;;
+E0103;VARIATION SELECTOR-20;Mn;0;NSM;;;;;N;;;;;
+E0104;VARIATION SELECTOR-21;Mn;0;NSM;;;;;N;;;;;
+E0105;VARIATION SELECTOR-22;Mn;0;NSM;;;;;N;;;;;
+E0106;VARIATION SELECTOR-23;Mn;0;NSM;;;;;N;;;;;
+E0107;VARIATION SELECTOR-24;Mn;0;NSM;;;;;N;;;;;
+E0108;VARIATION SELECTOR-25;Mn;0;NSM;;;;;N;;;;;
+E0109;VARIATION SELECTOR-26;Mn;0;NSM;;;;;N;;;;;
+E010A;VARIATION SELECTOR-27;Mn;0;NSM;;;;;N;;;;;
+E010B;VARIATION SELECTOR-28;Mn;0;NSM;;;;;N;;;;;
+E010C;VARIATION SELECTOR-29;Mn;0;NSM;;;;;N;;;;;
+E010D;VARIATION SELECTOR-30;Mn;0;NSM;;;;;N;;;;;
+E010E;VARIATION SELECTOR-31;Mn;0;NSM;;;;;N;;;;;
+E010F;VARIATION SELECTOR-32;Mn;0;NSM;;;;;N;;;;;
+E0110;VARIATION SELECTOR-33;Mn;0;NSM;;;;;N;;;;;
+E0111;VARIATION SELECTOR-34;Mn;0;NSM;;;;;N;;;;;
+E0112;VARIATION SELECTOR-35;Mn;0;NSM;;;;;N;;;;;
+E0113;VARIATION SELECTOR-36;Mn;0;NSM;;;;;N;;;;;
+E0114;VARIATION SELECTOR-37;Mn;0;NSM;;;;;N;;;;;
+E0115;VARIATION SELECTOR-38;Mn;0;NSM;;;;;N;;;;;
+E0116;VARIATION SELECTOR-39;Mn;0;NSM;;;;;N;;;;;
+E0117;VARIATION SELECTOR-40;Mn;0;NSM;;;;;N;;;;;
+E0118;VARIATION SELECTOR-41;Mn;0;NSM;;;;;N;;;;;
+E0119;VARIATION SELECTOR-42;Mn;0;NSM;;;;;N;;;;;
+E011A;VARIATION SELECTOR-43;Mn;0;NSM;;;;;N;;;;;
+E011B;VARIATION SELECTOR-44;Mn;0;NSM;;;;;N;;;;;
+E011C;VARIATION SELECTOR-45;Mn;0;NSM;;;;;N;;;;;
+E011D;VARIATION SELECTOR-46;Mn;0;NSM;;;;;N;;;;;
+E011E;VARIATION SELECTOR-47;Mn;0;NSM;;;;;N;;;;;
+E011F;VARIATION SELECTOR-48;Mn;0;NSM;;;;;N;;;;;
+E0120;VARIATION SELECTOR-49;Mn;0;NSM;;;;;N;;;;;
+E0121;VARIATION SELECTOR-50;Mn;0;NSM;;;;;N;;;;;
+E0122;VARIATION SELECTOR-51;Mn;0;NSM;;;;;N;;;;;
+E0123;VARIATION SELECTOR-52;Mn;0;NSM;;;;;N;;;;;
+E0124;VARIATION SELECTOR-53;Mn;0;NSM;;;;;N;;;;;
+E0125;VARIATION SELECTOR-54;Mn;0;NSM;;;;;N;;;;;
+E0126;VARIATION SELECTOR-55;Mn;0;NSM;;;;;N;;;;;
+E0127;VARIATION SELECTOR-56;Mn;0;NSM;;;;;N;;;;;
+E0128;VARIATION SELECTOR-57;Mn;0;NSM;;;;;N;;;;;
+E0129;VARIATION SELECTOR-58;Mn;0;NSM;;;;;N;;;;;
+E012A;VARIATION SELECTOR-59;Mn;0;NSM;;;;;N;;;;;
+E012B;VARIATION SELECTOR-60;Mn;0;NSM;;;;;N;;;;;
+E012C;VARIATION SELECTOR-61;Mn;0;NSM;;;;;N;;;;;
+E012D;VARIATION SELECTOR-62;Mn;0;NSM;;;;;N;;;;;
+E012E;VARIATION SELECTOR-63;Mn;0;NSM;;;;;N;;;;;
+E012F;VARIATION SELECTOR-64;Mn;0;NSM;;;;;N;;;;;
+E0130;VARIATION SELECTOR-65;Mn;0;NSM;;;;;N;;;;;
+E0131;VARIATION SELECTOR-66;Mn;0;NSM;;;;;N;;;;;
+E0132;VARIATION SELECTOR-67;Mn;0;NSM;;;;;N;;;;;
+E0133;VARIATION SELECTOR-68;Mn;0;NSM;;;;;N;;;;;
+E0134;VARIATION SELECTOR-69;Mn;0;NSM;;;;;N;;;;;
+E0135;VARIATION SELECTOR-70;Mn;0;NSM;;;;;N;;;;;
+E0136;VARIATION SELECTOR-71;Mn;0;NSM;;;;;N;;;;;
+E0137;VARIATION SELECTOR-72;Mn;0;NSM;;;;;N;;;;;
+E0138;VARIATION SELECTOR-73;Mn;0;NSM;;;;;N;;;;;
+E0139;VARIATION SELECTOR-74;Mn;0;NSM;;;;;N;;;;;
+E013A;VARIATION SELECTOR-75;Mn;0;NSM;;;;;N;;;;;
+E013B;VARIATION SELECTOR-76;Mn;0;NSM;;;;;N;;;;;
+E013C;VARIATION SELECTOR-77;Mn;0;NSM;;;;;N;;;;;
+E013D;VARIATION SELECTOR-78;Mn;0;NSM;;;;;N;;;;;
+E013E;VARIATION SELECTOR-79;Mn;0;NSM;;;;;N;;;;;
+E013F;VARIATION SELECTOR-80;Mn;0;NSM;;;;;N;;;;;
+E0140;VARIATION SELECTOR-81;Mn;0;NSM;;;;;N;;;;;
+E0141;VARIATION SELECTOR-82;Mn;0;NSM;;;;;N;;;;;
+E0142;VARIATION SELECTOR-83;Mn;0;NSM;;;;;N;;;;;
+E0143;VARIATION SELECTOR-84;Mn;0;NSM;;;;;N;;;;;
+E0144;VARIATION SELECTOR-85;Mn;0;NSM;;;;;N;;;;;
+E0145;VARIATION SELECTOR-86;Mn;0;NSM;;;;;N;;;;;
+E0146;VARIATION SELECTOR-87;Mn;0;NSM;;;;;N;;;;;
+E0147;VARIATION SELECTOR-88;Mn;0;NSM;;;;;N;;;;;
+E0148;VARIATION SELECTOR-89;Mn;0;NSM;;;;;N;;;;;
+E0149;VARIATION SELECTOR-90;Mn;0;NSM;;;;;N;;;;;
+E014A;VARIATION SELECTOR-91;Mn;0;NSM;;;;;N;;;;;
+E014B;VARIATION SELECTOR-92;Mn;0;NSM;;;;;N;;;;;
+E014C;VARIATION SELECTOR-93;Mn;0;NSM;;;;;N;;;;;
+E014D;VARIATION SELECTOR-94;Mn;0;NSM;;;;;N;;;;;
+E014E;VARIATION SELECTOR-95;Mn;0;NSM;;;;;N;;;;;
+E014F;VARIATION SELECTOR-96;Mn;0;NSM;;;;;N;;;;;
+E0150;VARIATION SELECTOR-97;Mn;0;NSM;;;;;N;;;;;
+E0151;VARIATION SELECTOR-98;Mn;0;NSM;;;;;N;;;;;
+E0152;VARIATION SELECTOR-99;Mn;0;NSM;;;;;N;;;;;
+E0153;VARIATION SELECTOR-100;Mn;0;NSM;;;;;N;;;;;
+E0154;VARIATION SELECTOR-101;Mn;0;NSM;;;;;N;;;;;
+E0155;VARIATION SELECTOR-102;Mn;0;NSM;;;;;N;;;;;
+E0156;VARIATION SELECTOR-103;Mn;0;NSM;;;;;N;;;;;
+E0157;VARIATION SELECTOR-104;Mn;0;NSM;;;;;N;;;;;
+E0158;VARIATION SELECTOR-105;Mn;0;NSM;;;;;N;;;;;
+E0159;VARIATION SELECTOR-106;Mn;0;NSM;;;;;N;;;;;
+E015A;VARIATION SELECTOR-107;Mn;0;NSM;;;;;N;;;;;
+E015B;VARIATION SELECTOR-108;Mn;0;NSM;;;;;N;;;;;
+E015C;VARIATION SELECTOR-109;Mn;0;NSM;;;;;N;;;;;
+E015D;VARIATION SELECTOR-110;Mn;0;NSM;;;;;N;;;;;
+E015E;VARIATION SELECTOR-111;Mn;0;NSM;;;;;N;;;;;
+E015F;VARIATION SELECTOR-112;Mn;0;NSM;;;;;N;;;;;
+E0160;VARIATION SELECTOR-113;Mn;0;NSM;;;;;N;;;;;
+E0161;VARIATION SELECTOR-114;Mn;0;NSM;;;;;N;;;;;
+E0162;VARIATION SELECTOR-115;Mn;0;NSM;;;;;N;;;;;
+E0163;VARIATION SELECTOR-116;Mn;0;NSM;;;;;N;;;;;
+E0164;VARIATION SELECTOR-117;Mn;0;NSM;;;;;N;;;;;
+E0165;VARIATION SELECTOR-118;Mn;0;NSM;;;;;N;;;;;
+E0166;VARIATION SELECTOR-119;Mn;0;NSM;;;;;N;;;;;
+E0167;VARIATION SELECTOR-120;Mn;0;NSM;;;;;N;;;;;
+E0168;VARIATION SELECTOR-121;Mn;0;NSM;;;;;N;;;;;
+E0169;VARIATION SELECTOR-122;Mn;0;NSM;;;;;N;;;;;
+E016A;VARIATION SELECTOR-123;Mn;0;NSM;;;;;N;;;;;
+E016B;VARIATION SELECTOR-124;Mn;0;NSM;;;;;N;;;;;
+E016C;VARIATION SELECTOR-125;Mn;0;NSM;;;;;N;;;;;
+E016D;VARIATION SELECTOR-126;Mn;0;NSM;;;;;N;;;;;
+E016E;VARIATION SELECTOR-127;Mn;0;NSM;;;;;N;;;;;
+E016F;VARIATION SELECTOR-128;Mn;0;NSM;;;;;N;;;;;
+E0170;VARIATION SELECTOR-129;Mn;0;NSM;;;;;N;;;;;
+E0171;VARIATION SELECTOR-130;Mn;0;NSM;;;;;N;;;;;
+E0172;VARIATION SELECTOR-131;Mn;0;NSM;;;;;N;;;;;
+E0173;VARIATION SELECTOR-132;Mn;0;NSM;;;;;N;;;;;
+E0174;VARIATION SELECTOR-133;Mn;0;NSM;;;;;N;;;;;
+E0175;VARIATION SELECTOR-134;Mn;0;NSM;;;;;N;;;;;
+E0176;VARIATION SELECTOR-135;Mn;0;NSM;;;;;N;;;;;
+E0177;VARIATION SELECTOR-136;Mn;0;NSM;;;;;N;;;;;
+E0178;VARIATION SELECTOR-137;Mn;0;NSM;;;;;N;;;;;
+E0179;VARIATION SELECTOR-138;Mn;0;NSM;;;;;N;;;;;
+E017A;VARIATION SELECTOR-139;Mn;0;NSM;;;;;N;;;;;
+E017B;VARIATION SELECTOR-140;Mn;0;NSM;;;;;N;;;;;
+E017C;VARIATION SELECTOR-141;Mn;0;NSM;;;;;N;;;;;
+E017D;VARIATION SELECTOR-142;Mn;0;NSM;;;;;N;;;;;
+E017E;VARIATION SELECTOR-143;Mn;0;NSM;;;;;N;;;;;
+E017F;VARIATION SELECTOR-144;Mn;0;NSM;;;;;N;;;;;
+E0180;VARIATION SELECTOR-145;Mn;0;NSM;;;;;N;;;;;
+E0181;VARIATION SELECTOR-146;Mn;0;NSM;;;;;N;;;;;
+E0182;VARIATION SELECTOR-147;Mn;0;NSM;;;;;N;;;;;
+E0183;VARIATION SELECTOR-148;Mn;0;NSM;;;;;N;;;;;
+E0184;VARIATION SELECTOR-149;Mn;0;NSM;;;;;N;;;;;
+E0185;VARIATION SELECTOR-150;Mn;0;NSM;;;;;N;;;;;
+E0186;VARIATION SELECTOR-151;Mn;0;NSM;;;;;N;;;;;
+E0187;VARIATION SELECTOR-152;Mn;0;NSM;;;;;N;;;;;
+E0188;VARIATION SELECTOR-153;Mn;0;NSM;;;;;N;;;;;
+E0189;VARIATION SELECTOR-154;Mn;0;NSM;;;;;N;;;;;
+E018A;VARIATION SELECTOR-155;Mn;0;NSM;;;;;N;;;;;
+E018B;VARIATION SELECTOR-156;Mn;0;NSM;;;;;N;;;;;
+E018C;VARIATION SELECTOR-157;Mn;0;NSM;;;;;N;;;;;
+E018D;VARIATION SELECTOR-158;Mn;0;NSM;;;;;N;;;;;
+E018E;VARIATION SELECTOR-159;Mn;0;NSM;;;;;N;;;;;
+E018F;VARIATION SELECTOR-160;Mn;0;NSM;;;;;N;;;;;
+E0190;VARIATION SELECTOR-161;Mn;0;NSM;;;;;N;;;;;
+E0191;VARIATION SELECTOR-162;Mn;0;NSM;;;;;N;;;;;
+E0192;VARIATION SELECTOR-163;Mn;0;NSM;;;;;N;;;;;
+E0193;VARIATION SELECTOR-164;Mn;0;NSM;;;;;N;;;;;
+E0194;VARIATION SELECTOR-165;Mn;0;NSM;;;;;N;;;;;
+E0195;VARIATION SELECTOR-166;Mn;0;NSM;;;;;N;;;;;
+E0196;VARIATION SELECTOR-167;Mn;0;NSM;;;;;N;;;;;
+E0197;VARIATION SELECTOR-168;Mn;0;NSM;;;;;N;;;;;
+E0198;VARIATION SELECTOR-169;Mn;0;NSM;;;;;N;;;;;
+E0199;VARIATION SELECTOR-170;Mn;0;NSM;;;;;N;;;;;
+E019A;VARIATION SELECTOR-171;Mn;0;NSM;;;;;N;;;;;
+E019B;VARIATION SELECTOR-172;Mn;0;NSM;;;;;N;;;;;
+E019C;VARIATION SELECTOR-173;Mn;0;NSM;;;;;N;;;;;
+E019D;VARIATION SELECTOR-174;Mn;0;NSM;;;;;N;;;;;
+E019E;VARIATION SELECTOR-175;Mn;0;NSM;;;;;N;;;;;
+E019F;VARIATION SELECTOR-176;Mn;0;NSM;;;;;N;;;;;
+E01A0;VARIATION SELECTOR-177;Mn;0;NSM;;;;;N;;;;;
+E01A1;VARIATION SELECTOR-178;Mn;0;NSM;;;;;N;;;;;
+E01A2;VARIATION SELECTOR-179;Mn;0;NSM;;;;;N;;;;;
+E01A3;VARIATION SELECTOR-180;Mn;0;NSM;;;;;N;;;;;
+E01A4;VARIATION SELECTOR-181;Mn;0;NSM;;;;;N;;;;;
+E01A5;VARIATION SELECTOR-182;Mn;0;NSM;;;;;N;;;;;
+E01A6;VARIATION SELECTOR-183;Mn;0;NSM;;;;;N;;;;;
+E01A7;VARIATION SELECTOR-184;Mn;0;NSM;;;;;N;;;;;
+E01A8;VARIATION SELECTOR-185;Mn;0;NSM;;;;;N;;;;;
+E01A9;VARIATION SELECTOR-186;Mn;0;NSM;;;;;N;;;;;
+E01AA;VARIATION SELECTOR-187;Mn;0;NSM;;;;;N;;;;;
+E01AB;VARIATION SELECTOR-188;Mn;0;NSM;;;;;N;;;;;
+E01AC;VARIATION SELECTOR-189;Mn;0;NSM;;;;;N;;;;;
+E01AD;VARIATION SELECTOR-190;Mn;0;NSM;;;;;N;;;;;
+E01AE;VARIATION SELECTOR-191;Mn;0;NSM;;;;;N;;;;;
+E01AF;VARIATION SELECTOR-192;Mn;0;NSM;;;;;N;;;;;
+E01B0;VARIATION SELECTOR-193;Mn;0;NSM;;;;;N;;;;;
+E01B1;VARIATION SELECTOR-194;Mn;0;NSM;;;;;N;;;;;
+E01B2;VARIATION SELECTOR-195;Mn;0;NSM;;;;;N;;;;;
+E01B3;VARIATION SELECTOR-196;Mn;0;NSM;;;;;N;;;;;
+E01B4;VARIATION SELECTOR-197;Mn;0;NSM;;;;;N;;;;;
+E01B5;VARIATION SELECTOR-198;Mn;0;NSM;;;;;N;;;;;
+E01B6;VARIATION SELECTOR-199;Mn;0;NSM;;;;;N;;;;;
+E01B7;VARIATION SELECTOR-200;Mn;0;NSM;;;;;N;;;;;
+E01B8;VARIATION SELECTOR-201;Mn;0;NSM;;;;;N;;;;;
+E01B9;VARIATION SELECTOR-202;Mn;0;NSM;;;;;N;;;;;
+E01BA;VARIATION SELECTOR-203;Mn;0;NSM;;;;;N;;;;;
+E01BB;VARIATION SELECTOR-204;Mn;0;NSM;;;;;N;;;;;
+E01BC;VARIATION SELECTOR-205;Mn;0;NSM;;;;;N;;;;;
+E01BD;VARIATION SELECTOR-206;Mn;0;NSM;;;;;N;;;;;
+E01BE;VARIATION SELECTOR-207;Mn;0;NSM;;;;;N;;;;;
+E01BF;VARIATION SELECTOR-208;Mn;0;NSM;;;;;N;;;;;
+E01C0;VARIATION SELECTOR-209;Mn;0;NSM;;;;;N;;;;;
+E01C1;VARIATION SELECTOR-210;Mn;0;NSM;;;;;N;;;;;
+E01C2;VARIATION SELECTOR-211;Mn;0;NSM;;;;;N;;;;;
+E01C3;VARIATION SELECTOR-212;Mn;0;NSM;;;;;N;;;;;
+E01C4;VARIATION SELECTOR-213;Mn;0;NSM;;;;;N;;;;;
+E01C5;VARIATION SELECTOR-214;Mn;0;NSM;;;;;N;;;;;
+E01C6;VARIATION SELECTOR-215;Mn;0;NSM;;;;;N;;;;;
+E01C7;VARIATION SELECTOR-216;Mn;0;NSM;;;;;N;;;;;
+E01C8;VARIATION SELECTOR-217;Mn;0;NSM;;;;;N;;;;;
+E01C9;VARIATION SELECTOR-218;Mn;0;NSM;;;;;N;;;;;
+E01CA;VARIATION SELECTOR-219;Mn;0;NSM;;;;;N;;;;;
+E01CB;VARIATION SELECTOR-220;Mn;0;NSM;;;;;N;;;;;
+E01CC;VARIATION SELECTOR-221;Mn;0;NSM;;;;;N;;;;;
+E01CD;VARIATION SELECTOR-222;Mn;0;NSM;;;;;N;;;;;
+E01CE;VARIATION SELECTOR-223;Mn;0;NSM;;;;;N;;;;;
+E01CF;VARIATION SELECTOR-224;Mn;0;NSM;;;;;N;;;;;
+E01D0;VARIATION SELECTOR-225;Mn;0;NSM;;;;;N;;;;;
+E01D1;VARIATION SELECTOR-226;Mn;0;NSM;;;;;N;;;;;
+E01D2;VARIATION SELECTOR-227;Mn;0;NSM;;;;;N;;;;;
+E01D3;VARIATION SELECTOR-228;Mn;0;NSM;;;;;N;;;;;
+E01D4;VARIATION SELECTOR-229;Mn;0;NSM;;;;;N;;;;;
+E01D5;VARIATION SELECTOR-230;Mn;0;NSM;;;;;N;;;;;
+E01D6;VARIATION SELECTOR-231;Mn;0;NSM;;;;;N;;;;;
+E01D7;VARIATION SELECTOR-232;Mn;0;NSM;;;;;N;;;;;
+E01D8;VARIATION SELECTOR-233;Mn;0;NSM;;;;;N;;;;;
+E01D9;VARIATION SELECTOR-234;Mn;0;NSM;;;;;N;;;;;
+E01DA;VARIATION SELECTOR-235;Mn;0;NSM;;;;;N;;;;;
+E01DB;VARIATION SELECTOR-236;Mn;0;NSM;;;;;N;;;;;
+E01DC;VARIATION SELECTOR-237;Mn;0;NSM;;;;;N;;;;;
+E01DD;VARIATION SELECTOR-238;Mn;0;NSM;;;;;N;;;;;
+E01DE;VARIATION SELECTOR-239;Mn;0;NSM;;;;;N;;;;;
+E01DF;VARIATION SELECTOR-240;Mn;0;NSM;;;;;N;;;;;
+E01E0;VARIATION SELECTOR-241;Mn;0;NSM;;;;;N;;;;;
+E01E1;VARIATION SELECTOR-242;Mn;0;NSM;;;;;N;;;;;
+E01E2;VARIATION SELECTOR-243;Mn;0;NSM;;;;;N;;;;;
+E01E3;VARIATION SELECTOR-244;Mn;0;NSM;;;;;N;;;;;
+E01E4;VARIATION SELECTOR-245;Mn;0;NSM;;;;;N;;;;;
+E01E5;VARIATION SELECTOR-246;Mn;0;NSM;;;;;N;;;;;
+E01E6;VARIATION SELECTOR-247;Mn;0;NSM;;;;;N;;;;;
+E01E7;VARIATION SELECTOR-248;Mn;0;NSM;;;;;N;;;;;
+E01E8;VARIATION SELECTOR-249;Mn;0;NSM;;;;;N;;;;;
+E01E9;VARIATION SELECTOR-250;Mn;0;NSM;;;;;N;;;;;
+E01EA;VARIATION SELECTOR-251;Mn;0;NSM;;;;;N;;;;;
+E01EB;VARIATION SELECTOR-252;Mn;0;NSM;;;;;N;;;;;
+E01EC;VARIATION SELECTOR-253;Mn;0;NSM;;;;;N;;;;;
+E01ED;VARIATION SELECTOR-254;Mn;0;NSM;;;;;N;;;;;
+E01EE;VARIATION SELECTOR-255;Mn;0;NSM;;;;;N;;;;;
+E01EF;VARIATION SELECTOR-256;Mn;0;NSM;;;;;N;;;;;
+F0000;<Plane 15 Private Use, First>;Co;0;L;;;;;N;;;;;
+FFFFD;<Plane 15 Private Use, Last>;Co;0;L;;;;;N;;;;;
+100000;<Plane 16 Private Use, First>;Co;0;L;;;;;N;;;;;
+10FFFD;<Plane 16 Private Use, Last>;Co;0;L;;;;;N;;;;;
diff --git a/src/lib/aqueue.c b/src/lib/aqueue.c
new file mode 100644
index 0000000..c770bb7
--- /dev/null
+++ b/src/lib/aqueue.c
@@ -0,0 +1,124 @@
+/* Copyright (c) 2003-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "aqueue.h"
+
+struct aqueue *aqueue_init(struct array *array)
+{
+ struct aqueue *aqueue;
+
+ aqueue = i_new(struct aqueue, 1);
+ aqueue->arr = array;
+ aqueue->area_size = buffer_get_writable_size(aqueue->arr->buffer) /
+ aqueue->arr->element_size;
+ i_assert(aqueue->area_size > 0);
+ return aqueue;
+}
+
+void aqueue_deinit(struct aqueue **_aqueue)
+{
+ struct aqueue *aqueue = *_aqueue;
+
+ *_aqueue = NULL;
+ i_free(aqueue);
+}
+
+static void aqueue_grow(struct aqueue *aqueue)
+{
+ unsigned int orig_area_size, count;
+
+ i_assert(aqueue->full && aqueue->head == aqueue->tail);
+
+ orig_area_size = aqueue->area_size;
+ (void)array_append_space_i(aqueue->arr);
+ aqueue->area_size = buffer_get_writable_size(aqueue->arr->buffer) /
+ aqueue->arr->element_size;
+ i_assert(orig_area_size < aqueue->area_size);
+
+ count = I_MIN(aqueue->area_size - orig_area_size, aqueue->head);
+ array_copy(aqueue->arr, orig_area_size, aqueue->arr, 0, count);
+ if (count < aqueue->area_size - orig_area_size)
+ aqueue->head = orig_area_size + count;
+ else {
+ array_copy(aqueue->arr, 0, aqueue->arr, count,
+ aqueue->head - count);
+ aqueue->head -= count;
+ }
+
+ i_assert(aqueue->head != aqueue->tail);
+ aqueue->full = FALSE;
+}
+
+void aqueue_append(struct aqueue *aqueue, const void *data)
+{
+ if (aqueue->full) {
+ aqueue_grow(aqueue);
+ i_assert(!aqueue->full);
+ }
+
+ array_idx_set_i(aqueue->arr, aqueue->head, data);
+ aqueue->head = (aqueue->head + 1) % aqueue->area_size;
+ aqueue->full = aqueue->head == aqueue->tail;
+}
+
+void aqueue_delete(struct aqueue *aqueue, unsigned int n)
+{
+ unsigned int idx, count = aqueue_count(aqueue);
+
+ i_assert(n < count);
+
+ aqueue->full = FALSE;
+ if (n == 0) {
+ /* optimized deletion from tail */
+ aqueue->tail = (aqueue->tail + 1) % aqueue->area_size;
+ return;
+ }
+ if (n == count-1) {
+ /* optimized deletion from head */
+ aqueue->head = (aqueue->head + aqueue->area_size - 1) %
+ aqueue->area_size;
+ return;
+ }
+
+ idx = aqueue_idx(aqueue, n);
+ if ((n < count/2 || idx > aqueue->head) && idx > aqueue->tail) {
+ /* move tail forward.
+ ..tail##idx##head.. or ##head..tail##idx## */
+ array_copy(aqueue->arr, aqueue->tail + 1,
+ aqueue->arr, aqueue->tail,
+ idx - aqueue->tail);
+ aqueue->tail++;
+ i_assert(aqueue->tail < aqueue->area_size);
+ } else {
+ /* move head backward.
+ ..tail##idx##head.. or ##idx##head..tail## */
+ i_assert(idx < aqueue->head);
+ array_copy(aqueue->arr, idx,
+ aqueue->arr, idx + 1,
+ aqueue->head - idx);
+ aqueue->head = (aqueue->head + aqueue->area_size - 1) %
+ aqueue->area_size;
+ }
+ i_assert(aqueue->head < aqueue->area_size &&
+ aqueue->head != aqueue->tail);
+}
+
+void aqueue_delete_tail(struct aqueue *aqueue)
+{
+ aqueue_delete(aqueue, 0);
+}
+
+void aqueue_clear(struct aqueue *aqueue)
+{
+ aqueue->head = aqueue->tail = 0;
+ aqueue->full = FALSE;
+}
+
+unsigned int aqueue_count(const struct aqueue *aqueue)
+{
+ unsigned int area_size = aqueue->area_size;
+
+ return aqueue->full ? area_size :
+ (area_size - aqueue->tail + aqueue->head) % area_size;
+}
diff --git a/src/lib/aqueue.h b/src/lib/aqueue.h
new file mode 100644
index 0000000..fffe310
--- /dev/null
+++ b/src/lib/aqueue.h
@@ -0,0 +1,41 @@
+#ifndef AQUEUE_H
+#define AQUEUE_H
+
+/* Dynamically growing queue. Use the array directly to access the data,
+ for example:
+
+ count = queue_count(queue);
+ for (i = 0; i < count; i++) {
+ data = array[queue_idx(i)];
+ }
+*/
+
+struct aqueue {
+ struct array *arr;
+ unsigned int head, tail, area_size;
+ bool full;
+};
+
+struct aqueue *aqueue_init(struct array *array);
+void aqueue_deinit(struct aqueue **aqueue);
+
+/* Append item to head */
+void aqueue_append(struct aqueue *aqueue, const void *data);
+/* Delete last item from tail */
+void aqueue_delete_tail(struct aqueue *aqueue);
+/* Remove item from n'th position */
+void aqueue_delete(struct aqueue *aqueue, unsigned int n);
+/* Clear the entire aqueue */
+void aqueue_clear(struct aqueue *aqueue);
+
+/* Returns the number of items in aqueue. */
+unsigned int aqueue_count(const struct aqueue *aqueue) ATTR_PURE;
+
+/* Returns array index of n'th element in aqueue. */
+static inline unsigned int ATTR_PURE
+aqueue_idx(const struct aqueue *aqueue, unsigned int n)
+{
+ return (aqueue->tail + n) % aqueue->area_size;
+}
+
+#endif
diff --git a/src/lib/array-decl.h b/src/lib/array-decl.h
new file mode 100644
index 0000000..6201df9
--- /dev/null
+++ b/src/lib/array-decl.h
@@ -0,0 +1,26 @@
+#ifndef ARRAY_DECL_H
+#define ARRAY_DECL_H
+
+#define ARRAY(array_type) union { struct array arr; array_type const *const *v; array_type **v_modifiable; }
+#define ARRAY_INIT { { NULL, 0 } }
+
+#define ARRAY_DEFINE_TYPE(name, array_type) \
+ union array ## __ ## name { struct array arr; array_type const *const *v; array_type **v_modifiable; }
+#define ARRAY_TYPE(name) \
+ union array ## __ ## name
+
+struct array {
+ buffer_t *buffer;
+ size_t element_size;
+};
+
+ARRAY_DEFINE_TYPE(string, char *);
+ARRAY_DEFINE_TYPE(const_string, const char *);
+ARRAY_DEFINE_TYPE(uint8_t, uint8_t);
+ARRAY_DEFINE_TYPE(uint16_t, uint16_t);
+ARRAY_DEFINE_TYPE(uint32_t, uint32_t);
+ARRAY_DEFINE_TYPE(uint64_t, uint64_t);
+ARRAY_DEFINE_TYPE(uint, unsigned int);
+ARRAY_DEFINE_TYPE(void_array, void *);
+
+#endif
diff --git a/src/lib/array.c b/src/lib/array.c
new file mode 100644
index 0000000..44b91a5
--- /dev/null
+++ b/src/lib/array.c
@@ -0,0 +1,166 @@
+/* Copyright (c) 2003-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+
+
+void *
+array_idx_modifiable_i(const struct array *array, unsigned int idx)
+{
+ i_assert(idx < array->buffer->used / array->element_size);
+ return PTR_OFFSET(array->buffer->data, idx * array->element_size);
+}
+
+void *array_idx_get_space_i(struct array *array, unsigned int idx)
+{
+ return buffer_get_space_unsafe(array->buffer, idx * array->element_size,
+ array->element_size);
+}
+
+void array_idx_set_i(struct array *array, unsigned int idx, const void *data)
+{
+ buffer_write(array->buffer, idx * array->element_size,
+ data, array->element_size);
+}
+
+void array_idx_clear_i(struct array *array, unsigned int idx)
+{
+ buffer_write_zero(array->buffer, idx * array->element_size,
+ array->element_size);
+}
+
+void *array_insert_space_i(struct array *array, unsigned int idx)
+{
+ void *data;
+ size_t pos;
+
+ pos = idx * array->element_size;
+ buffer_copy(array->buffer, pos + array->element_size,
+ array->buffer, pos, SIZE_MAX);
+
+ data = buffer_get_space_unsafe(array->buffer, pos, array->element_size);
+ memset(data, 0, array->element_size);
+ return data;
+}
+
+bool array_cmp_i(const struct array *array1, const struct array *array2)
+{
+ if (!array_is_created_i(array1) || array1->buffer->used == 0)
+ return !array_is_created_i(array2) || array2->buffer->used == 0;
+
+ if (!array_is_created_i(array2))
+ return FALSE;
+
+ return buffer_cmp(array1->buffer, array2->buffer);
+}
+
+bool array_equal_fn_i(const struct array *array1, const struct array *array2,
+ int (*cmp)(const void *, const void*))
+{
+ unsigned int count1, count2, i;
+ size_t size;
+
+ if (!array_is_created_i(array1) || array1->buffer->used == 0)
+ return !array_is_created_i(array2) || array2->buffer->used == 0;
+
+ if (!array_is_created_i(array2))
+ return FALSE;
+
+ count1 = array_count_i(array1); count2 = array_count_i(array2);
+ if (count1 != count2)
+ return FALSE;
+
+ size = array1->element_size;
+ i_assert(size == array2->element_size);
+
+ for (i = 0; i < count1; i++) {
+ if (cmp(CONST_PTR_OFFSET(array1->buffer->data, i * size),
+ CONST_PTR_OFFSET(array2->buffer->data, i * size)) != 0)
+ return FALSE;
+ }
+ return TRUE;
+}
+
+bool array_equal_fn_ctx_i(const struct array *array1, const struct array *array2,
+ int (*cmp)(const void *, const void *, const void *),
+ const void *context)
+{
+ unsigned int count1, count2, i;
+ size_t size;
+
+ if (!array_is_created_i(array1) || array1->buffer->used == 0)
+ return !array_is_created_i(array2) || array2->buffer->used == 0;
+
+ if (!array_is_created_i(array2))
+ return FALSE;
+
+ count1 = array_count_i(array1); count2 = array_count_i(array2);
+ if (count1 != count2)
+ return FALSE;
+
+ size = array1->element_size;
+ i_assert(size == array2->element_size);
+
+ for (i = 0; i < count1; i++) {
+ if (cmp(CONST_PTR_OFFSET(array1->buffer->data, i * size),
+ CONST_PTR_OFFSET(array2->buffer->data, i * size), context) != 0)
+ return FALSE;
+ }
+ return TRUE;
+}
+
+void array_reverse_i(struct array *array)
+{
+ const size_t element_size = array->element_size;
+ unsigned int i, count = array_count_i(array);
+ size_t size;
+ void *data, *tmp;
+
+ data = buffer_get_modifiable_data(array->buffer, &size);
+ tmp = t_buffer_get(array->element_size);
+ for (i = 0; i+1 < count; i++, count--) {
+ memcpy(tmp, PTR_OFFSET(data, i * element_size), element_size);
+ memcpy(PTR_OFFSET(data, i * element_size),
+ PTR_OFFSET(data, (count-1) * element_size),
+ element_size);
+ memcpy(PTR_OFFSET(data, (count-1) * element_size), tmp,
+ element_size);
+ }
+}
+
+void array_sort_i(struct array *array, int (*cmp)(const void *, const void *))
+{
+ unsigned int count;
+
+ count = array_count_i(array);
+ if (count == 0)
+ return;
+ qsort(buffer_get_modifiable_data(array->buffer, NULL),
+ count, array->element_size, cmp);
+}
+
+void *array_bsearch_i(struct array *array, const void *key,
+ int (*cmp)(const void *, const void *))
+{
+ unsigned int count;
+
+ count = array_count_i(array);
+ return bsearch(key, array->buffer->data,
+ count, array->element_size, cmp);
+}
+
+const void *array_lsearch_i(const struct array *array, const void *key,
+ int (*cmp)(const void *, const void *))
+{
+ const void * const data = array->buffer->data;
+ const size_t s = array->element_size;
+ unsigned int idx;
+
+ for (idx = 0; idx < array_count_i(array); idx++) {
+ if (cmp(key, CONST_PTR_OFFSET(data, idx * s)) == 0) {
+ return PTR_OFFSET(data, idx * s);
+ }
+ }
+
+ return NULL;
+}
diff --git a/src/lib/array.h b/src/lib/array.h
new file mode 100644
index 0000000..9349e8a
--- /dev/null
+++ b/src/lib/array.h
@@ -0,0 +1,420 @@
+#ifndef ARRAY_H
+#define ARRAY_H
+
+/* Array is a buffer accessible using fixed size elements. As long as the
+ compiler provides a typeof() operator, the array provides type safety. If
+ a wrong type is tried to be added to the array, or if the array's contents
+ are tried to be used using a wrong type, the compiler will give a warning.
+
+ Example usage:
+
+ struct foo {
+ ARRAY(struct bar) bars;
+ ...
+ };
+
+ i_array_init(&foo->bars, 10);
+
+ struct bar *bar = array_idx(&foo->bars, 5);
+ struct baz *baz = array_idx(&foo->bars, 5); // compiler warning
+
+ If you want to pass an array as a parameter to a function, you'll need to
+ create a type for the array using ARRAY_DEFINE_TYPE() and use the type in
+ the parameter using ARRAY_TYPE(). Any arrays that you want to be passing
+ around, such as structure members as in the above example, must also be
+ defined using ARRAY_TYPE() too, rather than ARRAY().
+
+ Example:
+
+ ARRAY_DEFINE_TYPE(foo, struct foo);
+ void do_foo(ARRAY_TYPE(foo) *foos) {
+ struct foo *foo = array_idx(foos, 0);
+ }
+ struct foo_manager {
+ ARRAY_TYPE(foo) foos; // pedantically, ARRAY(struct foo) is a different type
+ };
+ // ...
+ do_foo(&my_foo_manager->foos); // No compiler warning about mismatched types
+
+*/
+#include "array-decl.h"
+#include "buffer.h"
+
+#define p_array_init(array, pool, init_count) \
+ array_create(array, pool, sizeof(**(array)->v), init_count)
+#define i_array_init(array, init_count) \
+ p_array_init(array, default_pool, init_count)
+#define t_array_init(array, init_count) \
+ p_array_init(array, pool_datastack_create(), init_count)
+
+#ifdef HAVE_TYPEOF
+# define ARRAY_TYPE_CAST_CONST(array) \
+ (typeof(*(array)->v))
+# define ARRAY_TYPE_CAST_MODIFIABLE(array) \
+ (typeof(*(array)->v_modifiable))
+# define ARRAY_TYPE_CHECK(array, data) \
+ COMPILE_ERROR_IF_TYPES_NOT_COMPATIBLE( \
+ **(array)->v_modifiable, *(data))
+# define ARRAY_TYPES_CHECK(array1, array2) \
+ COMPILE_ERROR_IF_TYPES_NOT_COMPATIBLE( \
+ **(array1)->v_modifiable, **(array2)->v_modifiable)
+
+#else
+# define ARRAY_TYPE_CAST_CONST(array)
+# define ARRAY_TYPE_CAST_MODIFIABLE(array)
+# define ARRAY_TYPE_CHECK(array, data) 0
+# define ARRAY_TYPES_CHECK(array1, array2) 0
+#endif
+
+/* Usage:
+ ARRAY(struct foo) foo_arr;
+ struct foo *foo;
+
+ array_foreach(&foo_arr, foo) {
+ ..
+ }
+
+ Note that deleting an element while iterating will cause the iteration to
+ skip over the next element. So deleting a single element and breaking out
+ of the loop is fine, but continuing the loop is likely a bug. Use
+ array_foreach_reverse() instead when deleting multiple elements.
+*/
+#define array_foreach(array, elem) \
+ for (const void *elem ## __foreach_end = \
+ (const char *)(elem = *(array)->v) + (array)->arr.buffer->used; \
+ elem != elem ## __foreach_end; (elem)++)
+#define array_foreach_modifiable(array, elem) \
+ for (const void *elem ## _end = \
+ (const char *)(elem = ARRAY_TYPE_CAST_MODIFIABLE(array) \
+ buffer_get_modifiable_data((array)->arr.buffer, NULL)) + \
+ (array)->arr.buffer->used; \
+ elem != elem ## _end; (elem)++)
+
+/* Iterate the array in reverse order. */
+#define array_foreach_reverse(array, elem) \
+ for (elem = CONST_PTR_OFFSET(*(array)->v, (array)->arr.buffer->used); \
+ (const char *)(elem--) > (const char *)*(array)->v; )
+#define array_foreach_reverse_modifiable(array, elem) \
+ for (elem = ARRAY_TYPE_CAST_MODIFIABLE(array) \
+ ((char *)buffer_get_modifiable_data((array)->arr.buffer, NULL) + \
+ (array)->arr.buffer->used); \
+ (const char *)(elem--) > (const char *)*(array)->v; )
+
+/* Usage:
+ ARRAY(struct foo *) foo_ptrs_arr;
+ struct foo *foo;
+
+ array_foreach_elem(&foo_ptrs_arr, foo) {
+ ..
+ } */
+#define array_foreach_elem(array, elem) \
+ for (const void *_foreach_end = \
+ CONST_PTR_OFFSET(*(array)->v, (array)->arr.buffer->used), \
+ *_foreach_ptr = CONST_PTR_OFFSET(*(array)->v, ARRAY_TYPE_CHECK(array, &elem) + \
+ COMPILE_ERROR_IF_TRUE(sizeof(elem) > sizeof(void *))) \
+ ; \
+ (_foreach_ptr != _foreach_end && \
+ (memcpy(&elem, _foreach_ptr, sizeof(elem)), TRUE)) \
+ ; \
+ _foreach_ptr = CONST_PTR_OFFSET(_foreach_ptr, sizeof(elem)))
+
+
+#define array_ptr_to_idx(array, elem) \
+ ((elem) - (array)->v[0])
+/* Return index of iterated element inside array_foreach() or
+ array_foreach_modifiable() loop. Note that this doesn't work inside
+ array_foreach_elem() loop. */
+#define array_foreach_idx(array, elem) \
+ array_ptr_to_idx(array, elem)
+
+static inline void
+array_create_from_buffer_i(struct array *array, buffer_t *buffer,
+ size_t element_size)
+{
+ array->buffer = buffer;
+ array->element_size = element_size;
+}
+#define array_create_from_buffer(array, buffer, element_size) \
+ array_create_from_buffer_i(&(array)->arr, buffer, element_size)
+
+static inline void
+array_create_i(struct array *array, pool_t pool,
+ size_t element_size, unsigned int init_count)
+{
+ buffer_t *buffer;
+
+ buffer = buffer_create_dynamic_max(pool, init_count * element_size,
+ SIZE_MAX / element_size < UINT_MAX ? SIZE_MAX :
+ UINT_MAX * element_size);
+ array_create_from_buffer_i(array, buffer, element_size);
+}
+#define array_create(array, pool, element_size, init_count) \
+ array_create_i(&(array)->arr, pool, element_size, init_count)
+
+static inline void
+array_free_i(struct array *array)
+{
+ buffer_free(&array->buffer);
+}
+#define array_free(array) \
+ array_free_i(&(array)->arr)
+
+static inline void * ATTR_WARN_UNUSED_RESULT
+array_free_without_data_i(struct array *array)
+{
+ return buffer_free_without_data(&array->buffer);
+}
+#define array_free_without_data(array) \
+ ARRAY_TYPE_CAST_MODIFIABLE(array)array_free_without_data_i(&(array)->arr)
+
+static inline bool
+array_is_created_i(const struct array *array)
+{
+ return array->buffer != NULL;
+}
+#define array_is_created(array) \
+ array_is_created_i(&(array)->arr)
+
+static inline pool_t ATTR_PURE
+array_get_pool_i(struct array *array)
+{
+ return buffer_get_pool(array->buffer);
+}
+#define array_get_pool(array) \
+ array_get_pool_i(&(array)->arr)
+
+static inline void
+array_clear_i(struct array *array)
+{
+ buffer_set_used_size(array->buffer, 0);
+}
+#define array_clear(array) \
+ array_clear_i(&(array)->arr)
+
+static inline unsigned int ATTR_PURE
+array_count_i(const struct array *array)
+{
+ return array->buffer->used / array->element_size;
+}
+#define array_count(array) \
+ array_count_i(&(array)->arr)
+/* No need for the real count if all we're doing is comparing against 0 */
+#define array_is_empty(array) \
+ ((array)->arr.buffer->used == 0)
+#define array_not_empty(array) \
+ ((array)->arr.buffer->used > 0)
+
+static inline void
+array_append_i(struct array *array, const void *data, unsigned int count)
+{
+ buffer_append(array->buffer, data, count * array->element_size);
+}
+
+#define array_append(array, data, count) \
+ TYPE_CHECKS(void, ARRAY_TYPE_CHECK(array, data), \
+ array_append_i(&(array)->arr, data, count))
+
+static inline void
+array_append_array_i(struct array *dest_array, const struct array *src_array)
+{
+ i_assert(dest_array->element_size == src_array->element_size);
+ buffer_append_buf(dest_array->buffer, src_array->buffer, 0, SIZE_MAX);
+}
+#define array_append_array(dest_array, src_array) \
+ TYPE_CHECKS(void, ARRAY_TYPES_CHECK(dest_array, src_array), \
+ array_append_array_i(&(dest_array)->arr, &(src_array)->arr))
+
+static inline void
+array_insert_i(struct array *array, unsigned int idx,
+ const void *data, unsigned int count)
+{
+ buffer_insert(array->buffer, idx * array->element_size,
+ data, count * array->element_size);
+}
+
+#define array_insert(array, idx, data, count) \
+ TYPE_CHECKS(void, ARRAY_TYPE_CHECK(array, data), \
+ array_insert_i(&(array)->arr, idx, data, count))
+
+static inline void
+array_delete_i(struct array *array, unsigned int idx, unsigned int count)
+{
+ buffer_delete(array->buffer, idx * array->element_size,
+ count * array->element_size);
+}
+#define array_delete(array, idx, count) \
+ array_delete_i(&(array)->arr, idx, count)
+
+static inline const void *
+array_get_i(const struct array *array, unsigned int *count_r)
+{
+ *count_r = array_count_i(array);
+ return array->buffer->data;
+}
+#define array_get(array, count) \
+ ARRAY_TYPE_CAST_CONST(array)array_get_i(&(array)->arr, count)
+
+/* Re: i_assert() vs. pure: https://gcc.gnu.org/bugzilla/show_bug.cgi?id=51971#c1 */
+static inline const void * ATTR_PURE
+array_idx_i(const struct array *array, unsigned int idx)
+{
+ i_assert(idx < array->buffer->used / array->element_size);
+ return CONST_PTR_OFFSET(array->buffer->data, idx * array->element_size);
+}
+
+#define array_front(array) array_idx(array, 0)
+#define array_front_modifiable(array) array_idx_modifiable(array, 0)
+#define array_back(array) array_idx(array, array_count(array)-1)
+#define array_back_modifiable(array) array_idx_modifiable(array, array_count(array)-1)
+#define array_pop_back(array) array_delete(array, array_count(array)-1, 1);
+#define array_push_back(array, item) array_append(array, (item), 1)
+#define array_pop_front(array) array_delete(array, 0, 1)
+#define array_push_front(array, item) array_insert(array, 0, (item), 1)
+
+#define array_idx(array, idx) \
+ ARRAY_TYPE_CAST_CONST(array)array_idx_i(&(array)->arr, idx)
+/* Using *array_idx() will fail if the compiler doesn't support typeof().
+ The same can be done with array_idx_elem() for arrays that have pointers. */
+#ifdef HAVE_TYPEOF
+# define array_idx_elem(array, idx) \
+ (TRUE ? *array_idx(array, idx) : \
+ COMPILE_ERROR_IF_TRUE(sizeof(**(array)->v) != sizeof(void *)))
+#else
+# define array_idx_elem(array, idx) \
+ (*(void **)array_idx_i(&(array)->arr, idx))
+#endif
+
+static inline void *
+array_get_modifiable_i(struct array *array, unsigned int *count_r)
+{
+ *count_r = array_count_i(array);
+ return buffer_get_modifiable_data(array->buffer, NULL);
+}
+#define array_get_modifiable(array, count) \
+ ARRAY_TYPE_CAST_MODIFIABLE(array) \
+ array_get_modifiable_i(&(array)->arr, count)
+
+void *
+array_idx_modifiable_i(const struct array *array, unsigned int idx) ATTR_PURE;
+#define array_idx_modifiable(array, idx) \
+ ARRAY_TYPE_CAST_MODIFIABLE(array) \
+ array_idx_modifiable_i(&(array)->arr, idx)
+
+void *array_idx_get_space_i(struct array *array, unsigned int idx);
+#define array_idx_get_space(array, idx) \
+ ARRAY_TYPE_CAST_MODIFIABLE(array) \
+ array_idx_get_space_i(&(array)->arr, idx)
+
+void array_idx_set_i(struct array *array, unsigned int idx, const void *data);
+#define array_idx_set(array, idx, data) \
+ TYPE_CHECKS(void, ARRAY_TYPE_CHECK(array, data), \
+ array_idx_set_i(&(array)->arr, idx, data))
+
+void array_idx_clear_i(struct array *array, unsigned int idx);
+#define array_idx_clear(array, idx) \
+ array_idx_clear_i(&(array)->arr, idx)
+
+static inline void *
+array_append_space_i(struct array *array)
+{
+ void *data;
+
+ data = buffer_append_space_unsafe(array->buffer, array->element_size);
+ memset(data, 0, array->element_size);
+ return data;
+}
+#define array_append_space(array) \
+ ARRAY_TYPE_CAST_MODIFIABLE(array)array_append_space_i(&(array)->arr)
+#define array_append_zero(array) \
+ (void)array_append_space_i(&(array)->arr)
+
+void *array_insert_space_i(struct array *array, unsigned int idx);
+#define array_insert_space(array, idx) \
+ ARRAY_TYPE_CAST_MODIFIABLE(array) \
+ array_insert_space_i(&(array)->arr, idx)
+
+static inline void
+array_copy(struct array *dest, unsigned int dest_idx,
+ const struct array *src, unsigned int src_idx, unsigned int count)
+{
+ i_assert(dest->element_size == src->element_size);
+
+ buffer_copy(dest->buffer, dest_idx * dest->element_size,
+ src->buffer, src_idx * src->element_size,
+ count * dest->element_size);
+}
+
+bool array_cmp_i(const struct array *array1,
+ const struct array *array2) ATTR_PURE;
+#define array_cmp(array1, array2) \
+ array_cmp_i(&(array1)->arr, &(array2)->arr)
+
+/* Test equality via a comparator */
+bool array_equal_fn_i(const struct array *array1,
+ const struct array *array2,
+ int (*cmp)(const void*, const void *)) ATTR_PURE;
+#define array_equal_fn(array1, array2, cmp) \
+ TYPE_CHECKS(bool, \
+ ARRAY_TYPES_CHECK(array1, array2) || \
+ CALLBACK_TYPECHECK(cmp, int (*)(typeof(*(array1)->v), \
+ typeof(*(array2)->v))), \
+ array_equal_fn_i(&(array1)->arr, &(array2)->arr, \
+ (int (*)(const void *, const void *))cmp))
+bool array_equal_fn_ctx_i(const struct array *array1,
+ const struct array *array2,
+ int (*cmp)(const void*, const void *, const void *),
+ const void *context) ATTR_PURE;
+/* Same, but with a context pointer.
+ context can't be void* as ``const typeof(context)'' won't compile,
+ so ``const typeof(*context)*'' is required instead, and that requires a
+ complete type. */
+#define array_equal_fn_ctx(array1, array2, cmp, ctx) \
+ TYPE_CHECKS(bool, \
+ ARRAY_TYPES_CHECK(array1, array2) || \
+ CALLBACK_TYPECHECK(cmp, int (*)(typeof(*(array1)->v), \
+ typeof(*(array2)->v), \
+ const typeof(*ctx)*)), \
+ array_equal_fn_ctx_i(&(array1)->arr, &(array2)->arr, \
+ (int (*)(const void *, const void *, const void *))cmp, ctx))
+
+void array_reverse_i(struct array *array);
+#define array_reverse(array) \
+ array_reverse_i(&(array)->arr)
+
+void array_sort_i(struct array *array, int (*cmp)(const void *, const void *));
+#define array_sort(array, cmp) \
+ TYPE_CHECKS(void, \
+ CALLBACK_TYPECHECK(cmp, int (*)(typeof(*(array)->v), \
+ typeof(*(array)->v))), \
+ array_sort_i(&(array)->arr, (int (*)(const void *, const void *))cmp))
+
+void *array_bsearch_i(struct array *array, const void *key,
+ int (*cmp)(const void *, const void *));
+#define array_bsearch(array, key, cmp) \
+ TYPE_CHECKS(void *, \
+ CALLBACK_TYPECHECK(cmp, int (*)(typeof(const typeof(*key) *), \
+ typeof(*(array)->v))), \
+ ARRAY_TYPE_CAST_MODIFIABLE(array)array_bsearch_i(&(array)->arr, \
+ (const void *)key, (int (*)(const void *, const void *))cmp))
+
+/* Returns pointer to first element for which cmp(key,elem)==0, or NULL */
+const void *array_lsearch_i(const struct array *array, const void *key,
+ int (*cmp)(const void *, const void *));
+static inline void *array_lsearch_modifiable_i(struct array *array, const void *key,
+ int (*cmp)(const void *, const void *))
+{
+ return (void *)array_lsearch_i(array, key, cmp);
+}
+#define ARRAY_LSEARCH_CALL(modifiable, array, key, cmp) \
+ TYPE_CHECKS(void *, \
+ CALLBACK_TYPECHECK(cmp, int (*)(typeof(const typeof(*key) *), \
+ typeof(*(array)->v))), \
+ array_lsearch##modifiable##i( \
+ &(array)->arr, (const void *)key, \
+ (int (*)(const void *, const void *))cmp))
+#define array_lsearch(array, key, cmp) \
+ ARRAY_TYPE_CAST_CONST(array)ARRAY_LSEARCH_CALL(_, array, key, cmp)
+#define array_lsearch_modifiable(array, key, cmp) \
+ ARRAY_TYPE_CAST_MODIFIABLE(array)ARRAY_LSEARCH_CALL(_modifiable_, array, key, cmp)
+
+#endif
diff --git a/src/lib/askpass.c b/src/lib/askpass.c
new file mode 100644
index 0000000..bb9c128
--- /dev/null
+++ b/src/lib/askpass.c
@@ -0,0 +1,72 @@
+/* Copyright (c) 2006-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "buffer.h"
+#include "str.h"
+#include "askpass.h"
+
+#include <stdio.h>
+#include <termios.h>
+#include <fcntl.h>
+#include <unistd.h>
+
+static void askpass_str(const char *prompt, buffer_t *pass)
+{
+ struct termios old_tio, tio;
+ bool tty, restore_tio = FALSE;
+ char ch;
+ int fd;
+
+ tty = isatty(STDIN_FILENO) != 0;
+ if (tty) {
+ fputs(prompt, stderr);
+ fflush(stderr);
+
+ fd = open("/dev/tty", O_RDONLY);
+ if (fd < 0)
+ i_fatal("open(/dev/tty) failed: %m");
+
+ /* turn off echo */
+ if (tcgetattr(fd, &old_tio) == 0) {
+ restore_tio = TRUE;
+ tio = old_tio;
+ tio.c_lflag &= ENUM_NEGATE(ECHO | ECHONL);
+ (void)tcsetattr(fd, TCSAFLUSH, &tio);
+ }
+ } else {
+ /* read it from stdin without showing a prompt */
+ fd = STDIN_FILENO;
+ }
+
+ /* read the password */
+ while (read(fd, &ch, 1) > 0) {
+ if (ch == '\n' || ch == '\r')
+ break;
+ buffer_append_c(pass, ch);
+ }
+
+ if (tty) {
+ if (restore_tio)
+ (void)tcsetattr(fd, TCSAFLUSH, &old_tio);
+
+ fputs("\n", stderr); fflush(stderr);
+ i_close_fd(&fd);
+ }
+}
+
+void askpass(const char *prompt, char *buf, size_t buf_size)
+{
+ buffer_t str;
+
+ buffer_create_from_data(&str, buf, buf_size);
+ askpass_str(prompt, &str);
+ buffer_append_c(&str, '\0');
+}
+
+const char *t_askpass(const char *prompt)
+{
+ string_t *str = t_str_new(32);
+
+ askpass_str(prompt, str);
+ return str_c(str);
+}
diff --git a/src/lib/askpass.h b/src/lib/askpass.h
new file mode 100644
index 0000000..59ceb75
--- /dev/null
+++ b/src/lib/askpass.h
@@ -0,0 +1,7 @@
+#ifndef ASKPASS_H
+#define ASKPASS_H
+
+void askpass(const char *prompt, char *buf, size_t buf_size);
+const char *t_askpass(const char *prompt);
+
+#endif
diff --git a/src/lib/backtrace-string.c b/src/lib/backtrace-string.c
new file mode 100644
index 0000000..2ac90ec
--- /dev/null
+++ b/src/lib/backtrace-string.c
@@ -0,0 +1,156 @@
+/* Copyright (c) 2006-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "str.h"
+#include "backtrace-string.h"
+
+#define MAX_STACK_SIZE 30
+#define BACKTRACE_SKIP_PREFIX "backtrace_"
+
+#if defined(HAVE_LIBUNWIND)
+
+#include <libunwind.h>
+
+static int backtrace_append_unwind(string_t *str)
+{
+ size_t str_orig_size = str_len(str);
+ char proc_name[256];
+ int ret;
+ unsigned int fp = 0;
+ unw_cursor_t c;
+ unw_context_t ctx;
+ unw_proc_info_t pip;
+ bool success = FALSE;
+
+ if ((ret = unw_getcontext(&ctx)) != 0) {
+ str_printfa(str, "unw_getcontext() failed: %d", ret);
+ return -1;
+ }
+ if ((ret = unw_init_local(&c, &ctx)) != 0) {
+ str_printfa(str, "unw_init_local() failed: %d", ret);
+ return -1;
+ }
+
+ do {
+ str_printfa(str, "#%d ", fp);
+ if ((ret = unw_get_proc_info(&c, &pip)) != 0) {
+ str_printfa(str, "[unw_get_proc_info_failed(): %d]", ret);
+ } else if (pip.start_ip == 0 || pip.end_ip == 0) {
+ str_append(str, "[no start/end information]");
+ } else if ((ret = unw_get_proc_name(&c, proc_name, sizeof(proc_name), 0)) != 0 &&
+ ret != UNW_ENOMEM) {
+ str_printfa(str, "[unw_get_proc_name() failed: %d]", ret);
+ } else if (!success && str_begins(proc_name, BACKTRACE_SKIP_PREFIX)) {
+ str_truncate(str, str_orig_size);
+ continue;
+ } else {
+ str_append_max(str, proc_name, sizeof(proc_name));
+ str_printfa(str, "[0x%08zx]", pip.start_ip);
+ success = TRUE;
+ }
+ str_append(str, " -> ");
+ fp++;
+ } while ((ret = unw_step(&c)) > 0);
+
+ /* remove ' -> ' */
+ if (str->used > 4)
+ str_truncate(str, str->used - 4);
+ return ret == 0 && success ? 0 : -1;
+}
+#endif
+
+#if defined(HAVE_BACKTRACE_SYMBOLS) && defined(HAVE_EXECINFO_H)
+/* Linux */
+#include <execinfo.h>
+
+static int backtrace_append_libc(string_t *str)
+{
+ size_t str_orig_size = str_len(str);
+ void *stack[MAX_STACK_SIZE];
+ char **strings;
+ int ret, i;
+
+ ret = backtrace(stack, N_ELEMENTS(stack));
+ if (ret <= 0)
+ return -1;
+
+ strings = backtrace_symbols(stack, ret);
+ for (i = 0; i < ret; i++) {
+ if (str_len(str) > str_orig_size)
+ str_append(str, " -> ");
+
+ if (strings == NULL) {
+ /* out of memory case */
+ str_printfa(str, "0x%p", stack[i]);
+ } else if (str_len(str) != str_orig_size ||
+ !str_begins(strings[i], BACKTRACE_SKIP_PREFIX))
+ str_append(str, strings[i]);
+ }
+ free(strings);
+ return 0;
+}
+#elif defined(HAVE_WALKCONTEXT) && defined(HAVE_UCONTEXT_H)
+/* Solaris */
+#include <ucontext.h>
+
+struct walk_context {
+ string_t *str;
+ unsigned int pos;
+};
+
+static int walk_callback(uintptr_t ptr, int signo ATTR_UNUSED,
+ void *context)
+{
+ struct walk_context *ctx = context;
+
+ if (ctx->pos > 0)
+ str_append(ctx->str, " -> ");
+ str_printfa(ctx->str, "0x%p", (void *)ptr);
+ ctx->pos++;
+ return 0;
+}
+
+static int backtrace_append_libc(string_t *str)
+{
+ ucontext_t uc;
+ struct walk_context ctx;
+
+ if (getcontext(&uc) < 0)
+ return -1;
+
+ ctx.str = str;
+ ctx.pos = 0;
+ walkcontext(&uc, walk_callback, &ctx);
+ return 0;
+}
+#else
+static int backtrace_append_libc(string_t *str ATTR_UNUSED)
+{
+ return -1;
+}
+#endif
+
+int backtrace_append(string_t *str)
+{
+#if defined(HAVE_LIBUNWIND)
+ size_t orig_len = str_len(str);
+ if (backtrace_append_unwind(str) == 0)
+ return 0;
+ /* failed to get useful backtrace. libc's own method is likely
+ better. */
+ str_truncate(str, orig_len);
+#endif
+ return backtrace_append_libc(str);
+}
+
+int backtrace_get(const char **backtrace_r)
+{
+ string_t *str;
+
+ str = t_str_new(512);
+ if (backtrace_append(str) < 0)
+ return -1;
+
+ *backtrace_r = str_c(str);
+ return 0;
+}
diff --git a/src/lib/backtrace-string.h b/src/lib/backtrace-string.h
new file mode 100644
index 0000000..ef448d8
--- /dev/null
+++ b/src/lib/backtrace-string.h
@@ -0,0 +1,8 @@
+#ifndef BACKTRACE_STRING_H
+#define BACKTRACE_STRING_H
+
+/* Returns 0 if ok, -1 if failure. */
+int backtrace_append(string_t *str);
+int backtrace_get(const char **backtrace_r);
+
+#endif
diff --git a/src/lib/base32.c b/src/lib/base32.c
new file mode 100644
index 0000000..89ae978
--- /dev/null
+++ b/src/lib/base32.c
@@ -0,0 +1,324 @@
+/* Copyright (c) 2007-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "base32.h"
+#include "buffer.h"
+
+static const char b32enc[] =
+ "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
+
+static const char b32hexenc[] =
+ "0123456789ABCDEFGHIJKLMNOPQRSTUV";
+
+static const unsigned char b32dec[256] = {
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, /* 0-7 */
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, /* 8-15 */
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, /* 16-23 */
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, /* 24-31 */
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, /* 32-39 */
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, /* 40-47 */
+ 0xff, 0xff, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f, /* 48-55 */
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, /* 56-63 */
+ 0xff, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, /* 64-71 */
+ 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, /* 72-79 */
+ 0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, /* 80-87 */
+ 0x17, 0x18, 0x19, 0xff, 0xff, 0xff, 0xff, 0xff, /* 88-95 */
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, /* 96-103 */
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, /* 104-111 */
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, /* 112-119 */
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, /* 120-127 */
+
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, /* 128-255 */
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+};
+
+static const unsigned char b32hexdec[256] = {
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, /* 0-7 */
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, /* 8-15 */
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, /* 16-23 */
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, /* 24-31 */
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, /* 32-39 */
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, /* 40-47 */
+ 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, /* 48-55 */
+ 0x08, 0x09, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, /* 56-63 */
+ 0xff, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10, /* 64-71 */
+ 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, /* 72-79 */
+ 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f, 0xff, /* 80-87 */
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, /* 88-95 */
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, /* 96-103 */
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, /* 104-111 */
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, /* 112-119 */
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, /* 120-127 */
+
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, /* 128-255 */
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+};
+
+static void
+base32_encode_with_alphabet(const char *alph,
+ bool pad, const void *src, size_t src_size, buffer_t *dest)
+{
+ const unsigned char *src_c = src;
+ unsigned char tmp[8], endb;
+ size_t src_pos;
+
+ /* [5 3][2 5 1][4 4][1 5 2][3 5]
+ (5)(3 2)(5)(1 4)(4 1)(5)(2 3)(5)
+ */
+
+ /* encode main part */
+ for (src_pos = 0; src_pos + 4 < src_size; src_pos += 5) {
+ tmp[0] = alph[src_c[src_pos] >> 3];
+ tmp[1] = alph[((src_c[src_pos] & 0x07) << 2) |
+ ((src_c[src_pos+1] >> 6) & 0x03)];
+ tmp[2] = alph[((src_c[src_pos+1] >> 1) & 0x1f)];
+ tmp[3] = alph[((src_c[src_pos+1] & 0x01) << 4) |
+ (src_c[src_pos+2] >> 4)];
+ tmp[4] = alph[((src_c[src_pos+2] & 0x0f) << 1) |
+ (src_c[src_pos+3] >> 7)];
+ tmp[5] = alph[((src_c[src_pos+3] >> 2) & 0x1f)];
+ tmp[6] = alph[((src_c[src_pos+3] & 0x03) << 3) |
+ (src_c[src_pos+4] >> 5)];
+ tmp[7] = alph[src_c[src_pos+4] & 0x1f];
+ buffer_append(dest, tmp, 8);
+ }
+
+ /* encode last < 5 bytes if any */
+ if (src_pos < src_size) {
+ tmp[0] = alph[src_c[src_pos] >> 3];
+ switch (src_size - src_pos) {
+ case 1:
+ tmp[1] = alph[((src_c[src_pos] & 0x07) << 2)];
+ endb = 2;
+ break;
+ case 2:
+ tmp[1] = alph[((src_c[src_pos] & 0x07) << 2) |
+ ((src_c[src_pos+1] >> 6) & 0x03)];
+ tmp[2] = alph[((src_c[src_pos+1] >> 1) & 0x1f)];
+ tmp[3] = alph[((src_c[src_pos+1] & 0x01) << 4)];
+ endb = 4;
+ break;
+ case 3:
+ tmp[1] = alph[((src_c[src_pos] & 0x07) << 2) |
+ ((src_c[src_pos+1] >> 6) & 0x03)];
+ tmp[2] = alph[((src_c[src_pos+1] >> 1) & 0x1f)];
+ tmp[3] = alph[((src_c[src_pos+1] & 0x01) << 4) |
+ (src_c[src_pos+2] >> 4)];
+ tmp[4] = alph[((src_c[src_pos+2] & 0x0f) << 1)];
+ endb = 5;
+ break;
+ case 4:
+ tmp[1] = alph[((src_c[src_pos] & 0x07) << 2) |
+ ((src_c[src_pos+1] >> 6) & 0x03)];
+ tmp[2] = alph[((src_c[src_pos+1] >> 1) & 0x1f)];
+ tmp[3] = alph[((src_c[src_pos+1] & 0x01) << 4) |
+ (src_c[src_pos+2] >> 4)];
+ tmp[4] = alph[((src_c[src_pos+2] & 0x0f) << 1) |
+ (src_c[src_pos+3] >> 7)];
+ tmp[5] = alph[((src_c[src_pos+3] >> 2) & 0x1f)];
+ tmp[6] = alph[((src_c[src_pos+3] & 0x03) << 3)];
+ endb = 7;
+ break;
+ default:
+ i_unreached();
+ }
+
+ /* add padding if required */
+ if (pad) {
+ memset(&tmp[endb], '=', sizeof(tmp)-endb);
+ buffer_append(dest, tmp, 8);
+ } else {
+ buffer_append(dest, tmp, endb);
+ }
+ }
+}
+
+void base32_encode(bool pad, const void *src, size_t src_size,
+ buffer_t *dest)
+{
+ base32_encode_with_alphabet(b32enc, pad, src, src_size, dest);
+}
+
+void base32hex_encode(bool pad, const void *src, size_t src_size,
+ buffer_t *dest)
+{
+ base32_encode_with_alphabet(b32hexenc, pad, src, src_size, dest);
+}
+
+#define IS_EMPTY(c) \
+ ((c) == '\n' || (c) == '\r' || (c) == ' ' || (c) == '\t')
+
+static int
+base32_decode_with_alphabet(const unsigned char *alph,
+ const void *src, size_t src_size, size_t *src_pos_r,
+ buffer_t *dest)
+{
+ const unsigned char *src_c = src;
+ size_t block_pos, src_pos;
+ unsigned char output[5], ipos, opos;
+ int ret = 1;
+
+ /* (5)(3 2)(5)(1 4)(4 1)(5)(2 3)(5)
+ [5 3][2 5 1][4 4][1 5 2][3 5]
+ */
+ ipos = opos = 0;
+ block_pos = 0;
+ for (src_pos = 0; src_pos < src_size; src_pos++) {
+ unsigned char input = alph[src_c[src_pos]];
+
+ if (input == 0xff) {
+ if (unlikely(!IS_EMPTY(src_c[src_pos])))
+ break;
+ continue;
+ }
+
+ ipos++;
+ switch (ipos) {
+ case 1:
+ output[0] = input << 3;
+ opos = 0;
+ break;
+ case 2:
+ output[0] |= input >> 2;
+ output[1] = (input & 0x03) << 6;
+ opos = 1;
+ break;
+ case 3:
+ output[1] |= input << 1;
+ opos = 1;
+ break;
+ case 4:
+ output[1] |= input >> 4;
+ output[2] = (input & 0x0f) << 4;
+ opos = 2;
+ break;
+ case 5:
+ output[2] |= input >> 1;
+ output[3] = (input & 0x01) << 7;
+ opos = 3;
+ break;
+ case 6:
+ output[3] |= input << 2;
+ opos = 3;
+ break;
+ case 7:
+ output[3] |= input >> 3;
+ output[4] = ((input & 0x07) << 5);
+ opos = 4;
+ break;
+ case 8:
+ output[4] |= input;
+ buffer_append(dest, output, 5);
+ ipos = 0;
+ opos = 0;
+ block_pos = src_pos;
+ break;
+ default:
+ i_unreached();
+ }
+ }
+
+ if (ipos > 0) {
+ for (; src_pos < src_size; src_pos++) {
+ if (src_c[src_pos] != '=') {
+ if (unlikely(!IS_EMPTY(src_c[src_pos]))) {
+ ret = -1;
+ break;
+ }
+ continue;
+ }
+ if (++ipos >= 8) {
+ buffer_append(dest, output, opos);
+ ipos = 0;
+ ret = 0;
+ src_pos++;
+ break;
+ }
+ }
+ }
+
+ if (src_pos_r != NULL) {
+ if (ipos == 0) {
+ for (; src_pos < src_size; src_pos++) {
+ if (!IS_EMPTY(src_c[src_pos]))
+ break;
+ }
+
+ *src_pos_r = src_pos;
+ } else {
+ *src_pos_r = block_pos;
+ }
+ }
+ return ret;
+}
+
+int base32_decode(const void *src, size_t src_size,
+ size_t *src_pos_r, buffer_t *dest)
+{
+ return base32_decode_with_alphabet
+ (b32dec, src, src_size, src_pos_r, dest);
+}
+int base32hex_decode(const void *src, size_t src_size,
+ size_t *src_pos_r, buffer_t *dest)
+{
+ return base32_decode_with_alphabet
+ (b32hexdec, src, src_size, src_pos_r, dest);
+}
+
+buffer_t *t_base32_decode_str(const char *str)
+{
+ buffer_t *buf;
+ size_t len = strlen(str);
+
+ buf = t_buffer_create(MAX_BASE32_DECODED_SIZE(len));
+ (void)base32_decode(str, len, NULL, buf);
+ return buf;
+}
+
+buffer_t *t_base32hex_decode_str(const char *str)
+{
+ buffer_t *buf;
+ size_t len = strlen(str);
+
+ buf = t_buffer_create(MAX_BASE32_DECODED_SIZE(len));
+ (void)base32hex_decode(str, len, NULL, buf);
+ return buf;
+}
+
+bool base32_is_valid_char(char c)
+{
+ return b32dec[(uint8_t)c] != 0xff;
+}
+
+bool base32hex_is_valid_char(char c)
+{
+ return b32hexdec[(uint8_t)c] != 0xff;
+}
diff --git a/src/lib/base32.h b/src/lib/base32.h
new file mode 100644
index 0000000..6a7b52a
--- /dev/null
+++ b/src/lib/base32.h
@@ -0,0 +1,47 @@
+#ifndef BASE32_H
+#define BASE32_H
+
+/* Translates binary data into base32 (RFC 4648, Section 6). The src must not
+ point to dest buffer. The pad argument determines whether output is padded
+ with '='.
+ */
+void base32_encode(bool pad, const void *src, size_t src_size,
+ buffer_t *dest);
+
+/* Translates binary data into base32hex (RFC 4648, Section 7). The src must
+ not point to dest buffer. The pad argument determines whether output is
+ padded with '='.
+ */
+void base32hex_encode(bool pad, const void *src, size_t src_size,
+ buffer_t *dest);
+
+/* Translates base32/base32hex data into binary and appends it to dest buffer.
+ dest may point to same buffer as src. Returns 1 if all ok, 0 if end of
+ base32 data found, -1 if data is invalid.
+
+ Any whitespace characters are ignored.
+
+ This function may be called multiple times for parsing the same stream.
+ If src_pos is non-NULL, it's updated to first non-translated character in
+ src. */
+int base32_decode(const void *src, size_t src_size,
+ size_t *src_pos_r, buffer_t *dest) ATTR_NULL(4);
+int base32hex_decode(const void *src, size_t src_size,
+ size_t *src_pos_r, buffer_t *dest) ATTR_NULL(4);
+
+/* Decode given string to a buffer allocated from data stack. */
+buffer_t *t_base32_decode_str(const char *str);
+buffer_t *t_base32hex_decode_str(const char *str);
+
+/* Returns TRUE if c is a valid base32 encoding character (excluding '=') */
+bool base32_is_valid_char(char c);
+bool base32hex_is_valid_char(char c);
+
+/* max. buffer size required for base32_encode()/base32hex_encode() */
+#define MAX_BASE32_ENCODED_SIZE(size) \
+ ((size) / 5 * 8 + 8)
+/* max. buffer size required for base32_decode()/base32hex_decode() */
+#define MAX_BASE32_DECODED_SIZE(size) \
+ ((size) / 8 * 5 + 5)
+
+#endif
diff --git a/src/lib/base64.c b/src/lib/base64.c
new file mode 100644
index 0000000..5e4f96a
--- /dev/null
+++ b/src/lib/base64.c
@@ -0,0 +1,968 @@
+/* Copyright (c) 2007-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "base64.h"
+#include "buffer.h"
+
+/*
+ * Low-level Base64 encoder
+ */
+
+uoff_t base64_get_full_encoded_size(struct base64_encoder *enc, uoff_t src_size)
+{
+ bool crlf = HAS_ALL_BITS(enc->flags, BASE64_ENCODE_FLAG_CRLF);
+ bool no_padding = HAS_ALL_BITS(enc->flags,
+ BASE64_ENCODE_FLAG_NO_PADDING);
+ uoff_t out_size;
+ uoff_t newlines;
+
+ if (src_size == 0)
+ return 0;
+
+ /* calculate size of encoded data */
+ out_size = MAX_BASE64_ENCODED_SIZE(src_size);
+ if (no_padding) {
+ switch (src_size % 3) {
+ case 0:
+ break;
+ case 1:
+ i_assert(out_size > 2);
+ out_size -= 2;
+ break;
+ case 2:
+ i_assert(out_size > 1);
+ out_size -= 1;
+ break;
+ }
+ }
+
+ if (out_size > enc->max_line_len) {
+ /* newline between each full line */
+ i_assert(enc->max_line_len > 0);
+ newlines = (out_size / enc->max_line_len) - 1;
+ /* an extra newline to separate the partial last line from the
+ previous full line */
+ if ((out_size % enc->max_line_len) != 0)
+ newlines++;
+
+ /* update size with added newlines */
+ out_size += newlines * (crlf ? 2 : 1);
+ }
+
+ return out_size;
+}
+
+static size_t
+base64_encode_get_out_size(struct base64_encoder *enc, size_t src_size)
+{
+ size_t res_size = enc->w_buf_len;
+
+ i_assert(enc->w_buf_len <= sizeof(enc->w_buf));
+
+ if (src_size == 0)
+ return res_size;
+
+ /* Handle sub-position */
+ switch (enc->sub_pos) {
+ case 0:
+ break;
+ case 1:
+ res_size++;
+ src_size--;
+ if (src_size == 0)
+ return res_size;
+ /* fall through */
+ case 2:
+ res_size += 2;
+ src_size--;
+ break;
+ default:
+ i_unreached();
+ }
+
+ /* We're now at a 3-byte boundary */
+ if (src_size == 0)
+ return res_size;
+
+ /* Calculate size we can append to the output from remaining input */
+ res_size += ((src_size) / 3) * 4;
+ switch (src_size % 3) {
+ case 0:
+ break;
+ case 1:
+ res_size += 1;
+ break;
+ case 2:
+ res_size += 2;
+ break;
+ }
+ return res_size;
+}
+
+size_t base64_encode_get_size(struct base64_encoder *enc, size_t src_size)
+{
+ bool crlf = HAS_ALL_BITS(enc->flags, BASE64_ENCODE_FLAG_CRLF);
+ size_t out_size = base64_encode_get_out_size(enc, src_size);
+
+ if (src_size == 0) {
+ /* last block */
+ switch (enc->sub_pos) {
+ case 0:
+ break;
+ case 1:
+ out_size += 3;
+ break;
+ case 2:
+ out_size += 2;
+ break;
+ default:
+ i_unreached();
+ }
+ }
+
+ if (enc->max_line_len < SIZE_MAX) {
+ size_t line_part, lines;
+
+ /* Calculate how many line endings must be added */
+ i_assert(enc->max_line_len > 0);
+ lines = out_size / enc->max_line_len;
+ line_part = out_size % enc->max_line_len;
+ if (enc->cur_line_len > (enc->max_line_len - line_part))
+ lines++;
+
+ out_size += lines * (crlf ? 2 : 1);
+ }
+
+ if (enc->pending_lf)
+ out_size++;
+
+ return out_size;
+}
+
+size_t base64_encode_get_full_space(struct base64_encoder *enc,
+ size_t dst_space)
+{
+ bool crlf = HAS_ALL_BITS(enc->flags, BASE64_ENCODE_FLAG_CRLF);
+ bool no_padding = HAS_ALL_BITS(enc->flags,
+ BASE64_ENCODE_FLAG_NO_PADDING);
+ size_t src_space = 0;
+
+ i_assert(enc->w_buf_len <= sizeof(enc->w_buf));
+
+ if (enc->max_line_len < SIZE_MAX) {
+ size_t max_line_space, lines, nl_space;
+
+ /* Calculate how many line endings must be added if all space
+ were used. */
+ i_assert(enc->max_line_len < SIZE_MAX-2);
+ max_line_space = enc->max_line_len + (crlf ? 2 : 1);
+ lines = dst_space / max_line_space;
+
+ /* Calculate how much space is used by newline characters and
+ subtract this from the available space. */
+ nl_space = lines * (crlf ? 2 : 1);
+ if (dst_space <= nl_space)
+ return 0;
+ dst_space -= nl_space;
+ }
+
+ if (dst_space <= enc->w_buf_len)
+ return 0;
+ dst_space -= enc->w_buf_len;
+
+ if (enc->pending_lf)
+ dst_space--;
+ if (dst_space == 0)
+ return 0;
+
+ /* Handle sub-position */
+ switch (enc->sub_pos) {
+ case 0:
+ break;
+ case 1:
+ dst_space--;
+ src_space++;
+ /* fall through */
+ case 2:
+ if (dst_space < 2)
+ return src_space;
+ dst_space -= 2;
+ src_space++;
+ break;
+ default:
+ i_unreached();
+ }
+
+ if (dst_space == 0)
+ return src_space;
+
+ src_space += dst_space / 4 * 3;
+ if (no_padding) {
+ switch (dst_space % 4) {
+ case 0:
+ case 1:
+ break;
+ case 2:
+ src_space += 1;
+ break;
+ case 3:
+ src_space += 2;
+ break;
+ }
+ }
+
+ return src_space;
+}
+
+static void
+base64_encode_more_data(struct base64_encoder *enc,
+ const unsigned char *src_c, size_t src_size,
+ size_t *src_pos_r, size_t dst_avail, buffer_t *dest)
+{
+ const struct base64_scheme *b64 = enc->b64;
+ const char *b64enc = b64->encmap;
+ size_t res_size;
+ unsigned char *start, *ptr, *end;
+ size_t src_pos;
+
+ i_assert(!enc->pending_lf);
+
+ /* determine how much we can write in destination buffer */
+ if (dst_avail == 0) {
+ *src_pos_r = 0;
+ return;
+ }
+
+ /* pre-allocate space in the destination buffer */
+ res_size = base64_encode_get_out_size(enc, src_size);
+ if (res_size > dst_avail)
+ res_size = dst_avail;
+
+ start = buffer_append_space_unsafe(dest, res_size);
+ end = start + res_size;
+ ptr = start;
+
+ /* write bytes not written in previous call */
+ i_assert(enc->w_buf_len <= sizeof(enc->w_buf));
+ if (enc->w_buf_len > res_size) {
+ memcpy(ptr, enc->w_buf, res_size);
+ ptr += res_size;
+ enc->w_buf_len -= res_size;
+ memmove(enc->w_buf, enc->w_buf + res_size, enc->w_buf_len);
+ } else if (enc->w_buf_len > 0) {
+ memcpy(ptr, enc->w_buf, enc->w_buf_len);
+ ptr += enc->w_buf_len;
+ enc->w_buf_len = 0;
+ }
+ if (ptr == end) {
+ *src_pos_r = 0;
+ return;
+ }
+ i_assert(enc->w_buf_len == 0);
+ i_assert(src_size != 0);
+
+ /* Handle sub-position */
+ src_pos = 0;
+ switch (enc->sub_pos) {
+ case 0:
+ break;
+ case 1:
+ i_assert(ptr < end);
+ ptr[0] = b64enc[enc->buf | (src_c[src_pos] >> 4)];
+ ptr++;
+ enc->buf = (src_c[src_pos] & 0x0f) << 2;
+ src_pos++;
+ if (src_pos == src_size || ptr == end) {
+ enc->sub_pos = 2;
+ *src_pos_r = src_pos;
+ return;
+ }
+ /* fall through */
+ case 2:
+ ptr[0] = b64enc[enc->buf | ((src_c[src_pos] & 0xc0) >> 6)];
+ enc->w_buf[0] = b64enc[src_c[src_pos] & 0x3f];
+ ptr++;
+ src_pos++;
+ if (ptr < end) {
+ ptr[0] = enc->w_buf[0];
+ ptr++;
+ enc->w_buf_len = 0;
+ } else {
+ enc->sub_pos = 0;
+ enc->w_buf_len = 1;
+ *src_pos_r = src_pos;
+ return;
+ }
+ break;
+ default:
+ i_unreached();
+ }
+ enc->sub_pos = 0;
+
+ /* We're now at a 3-byte boundary */
+ if (src_pos == src_size) {
+ i_assert(ptr == end);
+ *src_pos_r = src_pos;
+ return;
+ }
+
+ /* Convert the bulk */
+ for (; src_size - src_pos > 2 && &ptr[3] < end;
+ src_pos += 3, ptr += 4) {
+ ptr[0] = b64enc[src_c[src_pos] >> 2];
+ ptr[1] = b64enc[((src_c[src_pos] & 0x03) << 4) |
+ (src_c[src_pos+1] >> 4)];
+ ptr[2] = b64enc[((src_c[src_pos+1] & 0x0f) << 2) |
+ ((src_c[src_pos+2] & 0xc0) >> 6)];
+ ptr[3] = b64enc[src_c[src_pos+2] & 0x3f];
+ }
+
+ /* Convert the bytes beyond the last 3-byte boundary and update state
+ for next call */
+ switch (src_size - src_pos) {
+ case 0:
+ enc->sub_pos = 0;
+ enc->buf = 0;
+ break;
+ case 1:
+ enc->sub_pos = 1;
+ enc->w_buf[0] = b64enc[src_c[src_pos] >> 2];
+ enc->w_buf_len = 1;
+ enc->buf = (src_c[src_pos] & 0x03) << 4;
+ src_pos++;
+ break;
+ case 2:
+ enc->sub_pos = 2;
+ enc->w_buf[0] = b64enc[src_c[src_pos] >> 2];
+ enc->w_buf[1] = b64enc[((src_c[src_pos] & 0x03) << 4) |
+ (src_c[src_pos+1] >> 4)];
+ enc->w_buf_len = 2;
+ enc->buf = (src_c[src_pos+1] & 0x0f) << 2;
+ src_pos += 2;
+ break;
+ default:
+ /* hit the end of the destination buffer */
+ enc->sub_pos = 0;
+ enc->w_buf[0] = b64enc[src_c[src_pos] >> 2];
+ enc->w_buf[1] = b64enc[((src_c[src_pos] & 0x03) << 4) |
+ (src_c[src_pos+1] >> 4)];
+ enc->w_buf[2] = b64enc[((src_c[src_pos+1] & 0x0f) << 2) |
+ ((src_c[src_pos+2] & 0xc0) >> 6)];
+ enc->w_buf[3] = b64enc[src_c[src_pos+2] & 0x3f];
+ enc->w_buf_len = 4;
+ enc->buf = 0;
+ src_pos += 3;
+ }
+
+ /* fill the remaining allocated space */
+ i_assert(ptr <= end);
+ res_size = end - ptr;
+ i_assert(enc->w_buf_len <= sizeof(enc->w_buf));
+ if (enc->w_buf_len > res_size) {
+ memcpy(ptr, enc->w_buf, res_size);
+ ptr += res_size;
+ enc->w_buf_len -= res_size;
+ memmove(enc->w_buf, enc->w_buf + res_size, enc->w_buf_len);
+ } else if (enc->w_buf_len > 0) {
+ memcpy(ptr, enc->w_buf, enc->w_buf_len);
+ ptr += enc->w_buf_len;
+ enc->w_buf_len = 0;
+ }
+
+ i_assert(ptr == end);
+ *src_pos_r = src_pos;
+}
+
+bool base64_encode_more(struct base64_encoder *enc,
+ const void *src, size_t src_size, size_t *src_pos_r,
+ buffer_t *dest)
+{
+ bool crlf = HAS_ALL_BITS(enc->flags, BASE64_ENCODE_FLAG_CRLF);
+ const unsigned char *src_c, *src_p;
+ size_t src_pos, src_left;
+
+ i_assert(!enc->finishing);
+ i_assert(!enc->finished);
+
+ src_p = src_c = src;
+ src_left = src_size;
+ while (src_left > 0) {
+ size_t dst_avail, dst_pos, line_avail, written;
+
+ /* determine how much we can write in destination buffer */
+ dst_avail = buffer_get_avail_size(dest);
+ if (dst_avail == 0)
+ break;
+
+ /* Emit pending newline immediately */
+ if (enc->pending_lf) {
+ i_assert(crlf);
+ buffer_append_c(dest, '\n');
+ enc->pending_lf = FALSE;
+ dst_avail--;
+ if (dst_avail == 0)
+ break;
+ }
+
+ i_assert(enc->max_line_len > 0);
+ i_assert(enc->cur_line_len <= enc->max_line_len);
+ line_avail = I_MIN(enc->max_line_len - enc->cur_line_len,
+ dst_avail);
+
+ if (line_avail > 0) {
+ dst_pos = dest->used;
+ base64_encode_more_data(enc, src_p, src_left, &src_pos,
+ line_avail, dest);
+ i_assert(src_pos <= src_left);
+ src_p += src_pos;
+ src_left -= src_pos;
+ i_assert(dest->used >= dst_pos);
+ written = dest->used - dst_pos;
+
+ i_assert(written <= line_avail);
+ i_assert(written <= enc->max_line_len);
+ i_assert(enc->cur_line_len <=
+ (enc->max_line_len - written));
+ enc->cur_line_len += written;
+ dst_avail -= written;
+ }
+
+ if (dst_avail == 0)
+ break;
+
+ if (src_left > 0 && enc->cur_line_len == enc->max_line_len) {
+ if (crlf) {
+ if (dst_avail >= 2) {
+ /* emit the full CRLF sequence */
+ buffer_append(dest, "\r\n", 2);
+ } else {
+ /* emit CR */
+ buffer_append_c(dest, '\r');
+ /* remember the LF */
+ enc->pending_lf = TRUE;
+ }
+ } else {
+ buffer_append_c(dest, '\n');
+ }
+ enc->cur_line_len = 0;
+ }
+ }
+
+ i_assert(src_p >= src_c);
+ src_pos = (src_p - src_c);
+ if (src_pos_r != NULL)
+ *src_pos_r = src_pos;
+ return (src_pos == src_size);
+}
+
+bool base64_encode_finish(struct base64_encoder *enc, buffer_t *dest)
+{
+ const struct base64_scheme *b64 = enc->b64;
+ const char *b64enc = b64->encmap;
+ bool crlf = HAS_ALL_BITS(enc->flags, BASE64_ENCODE_FLAG_CRLF);
+ bool padding = HAS_NO_BITS(enc->flags, BASE64_ENCODE_FLAG_NO_PADDING);
+ unsigned char *ptr, *end;
+ size_t dst_avail, line_avail, write_full, write;
+ unsigned int w_buf_pos = 0;
+
+ i_assert(!enc->finished);
+ enc->finishing = TRUE;
+
+ dst_avail = 0;
+ if (dest != NULL)
+ dst_avail = buffer_get_avail_size(dest);
+
+ if (enc->w_buf_len > 0 || enc->pending_lf) {
+ if (dst_avail == 0)
+ return FALSE;
+ i_assert(enc->w_buf_len <= sizeof(enc->w_buf));
+ }
+
+ i_assert(enc->max_line_len > 0);
+ i_assert(enc->cur_line_len <= enc->max_line_len);
+ line_avail = enc->max_line_len - enc->cur_line_len;
+
+ switch (enc->sub_pos) {
+ case 0:
+ break;
+ case 1:
+ i_assert(enc->w_buf_len <= (sizeof(enc->w_buf) - 3));
+ enc->w_buf[enc->w_buf_len] = b64enc[enc->buf];
+ enc->w_buf_len++;
+ if (padding) {
+ enc->w_buf[enc->w_buf_len + 0] = '=';
+ enc->w_buf[enc->w_buf_len + 1] = '=';
+ enc->w_buf_len += 2;
+ }
+ break;
+ case 2:
+ i_assert(enc->w_buf_len <= (sizeof(enc->w_buf) - 2));
+ enc->w_buf[enc->w_buf_len] = b64enc[enc->buf];
+ enc->w_buf_len++;
+ if (padding) {
+ enc->w_buf[enc->w_buf_len + 0] = '=';
+ enc->w_buf_len++;
+ }
+ break;
+ default:
+ i_unreached();
+ }
+ enc->sub_pos = 0;
+
+ write_full = write = enc->w_buf_len;
+ if (enc->pending_lf)
+ write_full++;
+ if (enc->max_line_len < SIZE_MAX && line_avail < write) {
+ unsigned int lines;
+
+ lines = I_MAX((write - line_avail) / enc->max_line_len, 1);
+ write_full += lines * (crlf ? 2 : 1);
+ } else {
+ line_avail = write;
+ }
+
+ if (write_full == 0) {
+ enc->finished = TRUE;
+ return TRUE;
+ }
+
+ i_assert(dest != NULL);
+ if (write_full > dst_avail)
+ write_full = dst_avail;
+
+ ptr = buffer_append_space_unsafe(dest, write_full);
+ end = ptr + write_full;
+ if (enc->pending_lf) {
+ ptr[0] = '\n';
+ dst_avail--;
+ ptr++;
+ enc->pending_lf = FALSE;
+ }
+ if (line_avail > dst_avail)
+ line_avail = dst_avail;
+ if (line_avail > 0) {
+ memcpy(ptr, enc->w_buf, line_avail);
+ ptr += line_avail;
+ w_buf_pos += line_avail;
+ }
+ while (ptr < end && w_buf_pos < enc->w_buf_len) {
+ enc->cur_line_len = 0;
+ if (crlf) {
+ ptr[0] = '\r';
+ ptr++;
+ if (ptr == end) {
+ enc->pending_lf = TRUE;
+ break;
+ }
+ }
+ ptr[0] = '\n';
+ ptr++;
+ if (ptr == end)
+ break;
+
+ write = I_MIN(enc->w_buf_len - w_buf_pos, enc->max_line_len);
+ write = I_MIN(write, (size_t)(end - ptr));
+ memcpy(ptr, &enc->w_buf[w_buf_pos], write);
+ ptr += write;
+ w_buf_pos += write;
+ enc->cur_line_len += write;
+ i_assert(ptr <= end);
+ }
+ i_assert(ptr == end);
+ if (w_buf_pos < enc->w_buf_len) {
+ enc->w_buf_len -= w_buf_pos;
+ memmove(enc->w_buf, enc->w_buf + w_buf_pos, enc->w_buf_len);
+ return FALSE;
+ }
+ if (enc->pending_lf)
+ return FALSE;
+ enc->finished = TRUE;
+ return TRUE;
+}
+
+/*
+ * Low-level Base64 decoder
+ */
+
+#define IS_EMPTY(c) \
+ ((c) == '\n' || (c) == '\r' || (c) == ' ' || (c) == '\t')
+
+static inline void
+base64_skip_whitespace(struct base64_decoder *dec, const unsigned char *src_c,
+ size_t src_size, size_t *src_pos)
+{
+ if (HAS_ALL_BITS(dec->flags, BASE64_DECODE_FLAG_NO_WHITESPACE))
+ return;
+
+ /* skip any whitespace in the padding */
+ while ((*src_pos) < src_size && IS_EMPTY(src_c[(*src_pos)]))
+ (*src_pos)++;
+}
+
+int base64_decode_more(struct base64_decoder *dec,
+ const void *src, size_t src_size, size_t *src_pos_r,
+ buffer_t *dest)
+{
+ const struct base64_scheme *b64 = dec->b64;
+ const unsigned char *src_c = src;
+ bool expect_boundary = HAS_ALL_BITS(
+ dec->flags, BASE64_DECODE_FLAG_EXPECT_BOUNDARY);
+ bool no_whitespace = HAS_ALL_BITS(
+ dec->flags, BASE64_DECODE_FLAG_NO_WHITESPACE);
+ bool no_padding = HAS_ALL_BITS(
+ dec->flags, BASE64_DECODE_FLAG_NO_PADDING);
+ size_t src_pos, dst_avail;
+ int ret = 1;
+
+ i_assert(!dec->finished);
+ i_assert(!dec->failed);
+
+ if (dec->seen_boundary) {
+ /* already seen the boundary/end of base64 data */
+ if (src_pos_r != NULL)
+ *src_pos_r = 0;
+ dec->failed = TRUE;
+ return -1;
+ }
+
+ src_pos = 0;
+ if (dec->seen_end) {
+ /* skip any whitespace at the end */
+ base64_skip_whitespace(dec, src_c, src_size, &src_pos);
+ if (src_pos_r != NULL)
+ *src_pos_r = src_pos;
+ if (src_pos < src_size) {
+ if (!expect_boundary) {
+ dec->failed = TRUE;
+ return -1;
+ }
+ dec->seen_boundary = TRUE;
+ return 0;
+ }
+ if (no_whitespace) {
+ dec->seen_boundary = TRUE;
+ return 0;
+ }
+ /* more whitespace may follow */
+ return 1;
+ }
+
+ if (src_size == 0) {
+ if (src_pos_r != NULL)
+ *src_pos_r = 0;
+ return 1;
+ }
+
+ dst_avail = buffer_get_avail_size(dest);
+ if (dst_avail == 0) {
+ i_assert(src_pos_r != NULL);
+ *src_pos_r = 0;
+ return 1;
+ }
+
+ for (; !dec->seen_padding && src_pos < src_size; src_pos++) {
+ unsigned char in = src_c[src_pos];
+ unsigned char dm = b64->decmap[in];
+
+ if (dm == 0xff) {
+ if (no_whitespace) {
+ ret = -1;
+ break;
+ }
+ if (unlikely(!IS_EMPTY(in))) {
+ ret = -1;
+ break;
+ }
+ continue;
+ }
+
+ if (dst_avail == 0) {
+ i_assert(src_pos_r != NULL);
+ *src_pos_r = src_pos;
+ return 1;
+ }
+
+ switch (dec->sub_pos) {
+ case 0:
+ dec->buf = dm;
+ dec->sub_pos++;
+ break;
+ case 1:
+ dec->buf = (dec->buf << 2) | (dm >> 4);
+ buffer_append_c(dest, dec->buf);
+ dst_avail--;
+ dec->buf = dm;
+ dec->sub_pos++;
+ break;
+ case 2:
+ dec->buf = ((dec->buf << 4) & 0xff) | (dm >> 2);
+ buffer_append_c(dest, dec->buf);
+ dst_avail--;
+ dec->buf = dm;
+ dec->sub_pos++;
+ break;
+ case 3:
+ dec->buf = ((dec->buf << 6) & 0xc0) | dm;
+ buffer_append_c(dest, dec->buf);
+ dst_avail--;
+ dec->buf = 0;
+ dec->sub_pos = 0;
+ break;
+ default:
+ i_unreached();
+ }
+ }
+
+ if (dec->seen_padding) {
+ /* skip any whitespace in or after the padding */
+ base64_skip_whitespace(dec, src_c, src_size, &src_pos);
+ if (src_pos == src_size) {
+ if (src_pos_r != NULL)
+ *src_pos_r = src_pos;
+ return 1;
+ }
+ }
+
+ if (dec->seen_padding || ret < 0) {
+ /* try to parse the end (padding) of the base64 input */
+ i_assert(src_pos < src_size);
+
+ if (no_padding) {
+ /* no padding allowed */
+ i_assert(!dec->seen_padding);
+ ret = -1;
+ } else {
+ switch (dec->sub_pos) {
+ case 0:
+ case 1:
+ /* no padding expected */
+ ret = -1;
+ break;
+ case 2:
+ if (unlikely(src_c[src_pos] != '=')) {
+ /* invalid character */
+ ret = -1;
+ break;
+ }
+ dec->seen_padding = TRUE;
+ dec->sub_pos++;
+ src_pos++;
+ if (src_pos == src_size) {
+ ret = 1;
+ break;
+ }
+ /* skip any whitespace in the padding */
+ base64_skip_whitespace(dec, src_c, src_size,
+ &src_pos);
+ if (src_pos == src_size) {
+ ret = 1;
+ break;
+ }
+ /* fall through */
+ case 3:
+ if (unlikely(src_c[src_pos] != '=')) {
+ /* invalid character */
+ ret = -1;
+ break;
+ }
+ dec->seen_padding = TRUE;
+ dec->seen_end = TRUE;
+ dec->sub_pos = 0;
+ src_pos++;
+ /* skip any trailing whitespace */
+ base64_skip_whitespace(dec, src_c, src_size,
+ &src_pos);
+ if (src_pos < src_size) {
+ ret = -1;
+ break;
+ }
+ if (no_whitespace) {
+ dec->seen_boundary = TRUE;
+ ret = 0;
+ } else {
+ /* more whitespace may follow */
+ ret = 1;
+ }
+ break;
+ }
+ }
+ }
+
+ if (ret < 0) {
+ if (!expect_boundary) {
+ dec->failed = TRUE;
+ } else {
+ dec->seen_boundary = TRUE;
+ ret = 0;
+ }
+ }
+ if (src_pos_r != NULL)
+ *src_pos_r = src_pos;
+ return ret;
+}
+
+int base64_decode_finish(struct base64_decoder *dec)
+{
+ i_assert(!dec->finished);
+ dec->finished = TRUE;
+
+ if (dec->failed)
+ return -1;
+
+ if (HAS_ALL_BITS(dec->flags,
+ BASE64_DECODE_FLAG_NO_PADDING)) {
+ i_assert(!dec->seen_padding);
+ return 0;
+ }
+ if (HAS_ALL_BITS(dec->flags,
+ BASE64_DECODE_FLAG_IGNORE_PADDING))
+ return 0;
+ return (dec->sub_pos == 0 ? 0 : -1);
+}
+
+/*
+ * Generic Base64 API
+ */
+
+buffer_t *t_base64_scheme_encode(const struct base64_scheme *b64,
+ enum base64_encode_flags flags,
+ size_t max_line_len,
+ const void *src, size_t src_size)
+{
+ buffer_t *buf;
+
+ buf = t_buffer_create(MAX_BASE64_ENCODED_SIZE(src_size));
+ base64_scheme_encode(b64, flags, max_line_len, src, src_size, buf);
+ return buf;
+}
+
+
+int base64_scheme_decode(const struct base64_scheme *b64,
+ enum base64_decode_flags flags,
+ const void *src, size_t src_size, buffer_t *dest)
+{
+ struct base64_decoder dec;
+ int ret;
+
+ base64_decode_init(&dec, b64, flags);
+ ret = base64_decode_more(&dec, src, src_size, NULL, dest);
+ if (ret >= 0)
+ ret = base64_decode_finish(&dec);
+
+ return ret;
+}
+
+buffer_t *t_base64_scheme_decode(const struct base64_scheme *b64,
+ enum base64_decode_flags flags,
+ const void *src, size_t src_size)
+{
+ buffer_t *buf;
+
+ buf = t_buffer_create(MAX_BASE64_DECODED_SIZE(src_size));
+ (void)base64_scheme_decode(b64, flags, src, src_size, buf);
+ return buf;
+}
+
+/*
+ * "base64" encoding scheme (RFC 4648, Section 4)
+ */
+
+struct base64_scheme base64_scheme = {
+ .encmap = {
+ 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H',
+ 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P',
+ 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X',
+ 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f',
+ 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n',
+ 'o', 'p', 'q', 'r', 's', 't', 'u', 'v',
+ 'w', 'x', 'y', 'z', '0', '1', '2', '3',
+ '4', '5', '6', '7', '8', '9', '+', '/',
+ },
+ .decmap = {
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, /* 0-7 */
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, /* 8-15 */
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, /* 16-23 */
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, /* 24-31 */
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, /* 32-39 */
+ 0xff, 0xff, 0xff, 0x3e, 0xff, 0xff, 0xff, 0x3f, /* 40-47 */
+ 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3a, 0x3b, /* 48-55 */
+ 0x3c, 0x3d, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, /* 56-63 */
+ 0xff, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, /* 64-71 */
+ 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, /* 72-79 */
+ 0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, /* 80-87 */
+ 0x17, 0x18, 0x19, 0xff, 0xff, 0xff, 0xff, 0xff, /* 88-95 */
+ 0xff, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f, 0x20, /* 96-103 */
+ 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28, /* 104-111 */
+ 0x29, 0x2a, 0x2b, 0x2c, 0x2d, 0x2e, 0x2f, 0x30, /* 112-119 */
+ 0x31, 0x32, 0x33, 0xff, 0xff, 0xff, 0xff, 0xff, /* 120-127 */
+
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, /* 128-255 */
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ },
+};
+
+/*
+ * "base64url" encoding scheme (RFC 4648, Section 5)
+ */
+
+struct base64_scheme base64url_scheme = {
+ .encmap = {
+ 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H',
+ 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P',
+ 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X',
+ 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f',
+ 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n',
+ 'o', 'p', 'q', 'r', 's', 't', 'u', 'v',
+ 'w', 'x', 'y', 'z', '0', '1', '2', '3',
+ '4', '5', '6', '7', '8', '9', '-', '_',
+ },
+ .decmap = {
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, /* 0-7 */
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, /* 8-15 */
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, /* 16-23 */
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, /* 24-31 */
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, /* 32-39 */
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0x3e, 0xff, 0xff, /* 40-47 */
+ 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3a, 0x3b, /* 48-55 */
+ 0x3c, 0x3d, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, /* 56-63 */
+ 0xff, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, /* 64-71 */
+ 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, /* 72-79 */
+ 0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, /* 80-87 */
+ 0x17, 0x18, 0x19, 0xff, 0xff, 0xff, 0xff, 0x3f, /* 88-95 */
+ 0xff, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f, 0x20, /* 96-103 */
+ 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28, /* 104-111 */
+ 0x29, 0x2a, 0x2b, 0x2c, 0x2d, 0x2e, 0x2f, 0x30, /* 112-119 */
+ 0x31, 0x32, 0x33, 0xff, 0xff, 0xff, 0xff, 0xff, /* 120-127 */
+
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, /* 128-255 */
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ },
+};
diff --git a/src/lib/base64.h b/src/lib/base64.h
new file mode 100644
index 0000000..ec6ac17
--- /dev/null
+++ b/src/lib/base64.h
@@ -0,0 +1,403 @@
+#ifndef BASE64_H
+#define BASE64_H
+
+/*
+ * Common Base64
+ */
+
+/* max. buffer size required for base64_encode() */
+#define MAX_BASE64_ENCODED_SIZE(size) \
+ ((((size) + 2) / 3) * 4)
+/* max. buffer size required for base64_decode() */
+#define MAX_BASE64_DECODED_SIZE(size) \
+ (((size) + 3) / 4 * 3)
+
+struct base64_scheme {
+ const char encmap[64];
+ const unsigned char decmap[256];
+};
+
+/*
+ * Low-level Base64 encoder
+ */
+
+enum base64_encode_flags {
+ /* Use CRLF instead of the default LF as line ending. */
+ BASE64_ENCODE_FLAG_CRLF = BIT(0),
+ /* Encode no padding at the end of the data. */
+ BASE64_ENCODE_FLAG_NO_PADDING = BIT(1),
+};
+
+struct base64_encoder {
+ const struct base64_scheme *b64;
+ enum base64_encode_flags flags;
+ size_t max_line_len;
+
+ /* state */
+ unsigned int sub_pos;
+ unsigned char buf;
+ size_t cur_line_len;
+
+ unsigned char w_buf[10];
+ unsigned int w_buf_len;
+
+ bool pending_lf:1;
+ bool finishing:1;
+ bool finished:1;
+};
+
+/* Returns TRUE when base64_encode_finish() was called on this encoder. */
+static inline bool
+base64_encode_is_finished(struct base64_encoder *enc)
+{
+ return enc->finished;
+}
+
+/* Initialize the Base64 encoder. The b64 parameter is the definition of the
+ particular Base64 encoding scheme that is used.
+ */
+static inline void
+base64_encode_init(struct base64_encoder *enc,
+ const struct base64_scheme *b64,
+ enum base64_encode_flags flags,
+ size_t max_line_len)
+{
+ i_zero(enc);
+ enc->b64 = b64;
+ enc->flags = flags;
+ enc->max_line_len = (max_line_len == 0 ? SIZE_MAX : max_line_len);
+}
+
+/* Reset the Base64 encoder to its initial state. */
+static inline void
+base64_encode_reset(struct base64_encoder *enc)
+{
+ const struct base64_scheme *b64 = enc->b64;
+ enum base64_encode_flags flags = enc->flags;
+ size_t max_line_len = enc->max_line_len;
+
+ base64_encode_init(enc, b64, flags, max_line_len);
+}
+
+/* Translate the size of the full encoder input to the size of the encoder
+ output.
+ */
+uoff_t base64_get_full_encoded_size(struct base64_encoder *enc,
+ uoff_t src_size);
+/* Translate the size of the next input to the size of the output once encoded.
+ This yields the amount of data appended to the dest buffer by
+ base64_encode_more() with the indicated src_size. */
+size_t base64_encode_get_size(struct base64_encoder *enc, size_t src_size);
+
+/* Translate the space in the destination buffer to the number of bytes that can
+ be encoded at most to complete the full base64 encoding, including padding
+ and newlines if configured. */
+size_t base64_encode_get_full_space(struct base64_encoder *enc,
+ size_t dst_space);
+
+/* Translates binary data into some form of Base64. The src must not point to
+ dest buffer. Returns TRUE when all the provided data is encoded. Returns
+ FALSE when the space in the provided buffer is insufficient. The return value
+ may be ignored. If src_pos_r is non-NULL, it's updated to first
+ non-translated character in src.
+ */
+bool ATTR_NOWARN_UNUSED_RESULT
+base64_encode_more(struct base64_encoder *enc, const void *src, size_t src_size,
+ size_t *src_pos_r, buffer_t *dest) ATTR_NULL(4);
+
+/* Finishes Base64 encoding. Returns TRUE when all the provided data is encoded.
+ Returns FALSE when the space in the provided buffer is insufficient. The
+ return value may be ignored.
+ */
+bool ATTR_NOWARN_UNUSED_RESULT
+base64_encode_finish(struct base64_encoder *enc, buffer_t *dest) ATTR_NULL(2);
+
+/*
+ * Low-level Base64 decoder
+ */
+
+enum base64_decode_flags {
+ /* Decode input until a boundary is reached. This boundary is a
+ non-Base64 input sequence that would normally trigger a decode error;
+ e.g., Base64 data followed by a ':'. With this flag, it is possible
+ to decode such a Base64 prefix. The base64_decode_finish() function
+ will still check that the Base64 data ends properly (padding). */
+ BASE64_DECODE_FLAG_EXPECT_BOUNDARY = BIT(0),
+ /* Prohibit whitespace in the input. */
+ BASE64_DECODE_FLAG_NO_WHITESPACE = BIT(1),
+ /* Require absence of padding at the end of the input. */
+ BASE64_DECODE_FLAG_NO_PADDING = BIT(2),
+ /* Ignore padding at the end of the input. This flag is ignored when
+ BASE64_DECODE_FLAG_NO_PADDING is also set. If both of these flags are
+ absent, padding is required (the default). */
+ BASE64_DECODE_FLAG_IGNORE_PADDING = BIT(3),
+};
+
+struct base64_decoder {
+ const struct base64_scheme *b64;
+ enum base64_decode_flags flags;
+
+ /* state */
+ unsigned int sub_pos;
+ unsigned char buf;
+
+ bool seen_padding:1;
+ bool seen_end:1;
+ bool seen_boundary:1;
+ bool finished:1;
+ bool failed:1;
+};
+
+/* Returns TRUE when base64_decode_finish() was called on this decoder. */
+static inline bool
+base64_decode_is_finished(struct base64_decoder *dec)
+{
+ return dec->finished;
+}
+
+/* Initialize the Base64 decoder. The b64 parameter is the definition of the
+ particular Base64 encoding scheme that is expected.
+ */
+static inline void
+base64_decode_init(struct base64_decoder *dec,
+ const struct base64_scheme *b64,
+ enum base64_decode_flags flags)
+{
+ i_zero(dec);
+ dec->b64 = b64;
+ dec->flags = flags;
+}
+
+/* Reset the Base64 decoder to its initial state. */
+static inline void
+base64_decode_reset(struct base64_decoder *dec)
+{
+ const struct base64_scheme *b64 = dec->b64;
+ enum base64_decode_flags flags = dec->flags;
+
+ base64_decode_init(dec, b64, flags);
+}
+
+/* Translates some form of Base64 data into binary and appends it to dest
+ buffer. dest may point to same buffer as src. Returns 1 if all ok, 0 if end
+ of base64 data found, -1 if data is invalid.
+
+ By default, any CR, LF characters are ignored, as well as any whitespace.
+ This can be overridden using the BASE64_DECODE_FLAG_NO_WHITESPACE flag.
+
+ If src_pos is non-NULL, it's updated to first non-translated character in
+ src.
+ */
+int base64_decode_more(struct base64_decoder *dec,
+ const void *src, size_t src_size, size_t *src_pos_r,
+ buffer_t *dest) ATTR_NULL(4);
+/* Finishes Base64 decoding. This function checks whether the encoded data ends
+ in the proper padding. Returns 0 if all ok, and -1 if data is invalid.
+ */
+int base64_decode_finish(struct base64_decoder *dec);
+
+/*
+ * Generic Base64 API
+ */
+
+/* Translates binary data into some variant of Base64. The src must not point to
+ dest buffer.
+
+ The b64 parameter is the definition of the particular Base 64 encoding scheme
+ that is used. See below for specific functions.
+ */
+static inline void
+base64_scheme_encode(const struct base64_scheme *b64,
+ enum base64_encode_flags flags, size_t max_line_len,
+ const void *src, size_t src_size, buffer_t *dest)
+{
+ struct base64_encoder enc;
+
+ base64_encode_init(&enc, b64, flags, max_line_len);
+ base64_encode_more(&enc, src, src_size, NULL, dest);
+ base64_encode_finish(&enc, dest);
+}
+
+buffer_t *t_base64_scheme_encode(const struct base64_scheme *b64,
+ enum base64_encode_flags flags,
+ size_t max_line_len,
+ const void *src, size_t src_size);
+
+static inline buffer_t *
+t_base64_scheme_encode_str(const struct base64_scheme *b64,
+ enum base64_encode_flags flags, size_t max_line_len,
+ const char *src)
+{
+ return t_base64_scheme_encode(b64, flags, max_line_len,
+ src, strlen(src));
+}
+
+/* Translates some variant of Base64 data into binary and appends it to dest
+ buffer. dest may point to same buffer as src. Returns 1 if all ok, 0 if end
+ of Base64 data found, -1 if data is invalid.
+
+ The b64 parameter is the definition of the particular Base 64 encoding scheme
+ that is expected. See below for specific functions.
+
+ Any CR, LF characters are ignored, as well as whitespace at beginning or
+ end of line.
+ */
+int base64_scheme_decode(const struct base64_scheme *b64,
+ enum base64_decode_flags flags,
+ const void *src, size_t src_size, buffer_t *dest);
+
+/* Decode given data to a buffer allocated from data stack.
+
+ The b64 parameter is the definition of the particular Base 64 encoding scheme
+ that is expected. See below for specific functions.
+ */
+buffer_t *t_base64_scheme_decode(const struct base64_scheme *b64,
+ enum base64_decode_flags flags,
+ const void *src, size_t src_size);
+/* Decode given string to a buffer allocated from data stack.
+
+ The b64 parameter is the definition of the particular Base 64 encoding scheme
+ that is expected. See below for specific functions.
+ */
+static inline buffer_t *
+t_base64_scheme_decode_str(const struct base64_scheme *b64,
+ enum base64_decode_flags flags, const char *str)
+{
+ return t_base64_scheme_decode(b64, flags, str, strlen(str));
+}
+
+/* Returns TRUE if c is a valid encoding character (excluding '=') for the
+ provided base64 mapping table */
+static inline bool
+base64_scheme_is_valid_char(const struct base64_scheme *b64, char c)
+{
+ return b64->decmap[(uint8_t)c] != 0xff;
+}
+
+/*
+ * "base64" encoding scheme (RFC 4648, Section 4)
+ */
+
+extern struct base64_scheme base64_scheme;
+
+/* Translates binary data into base64. See base64_scheme_encode(). */
+static inline void
+base64_encode(const void *src, size_t src_size, buffer_t *dest)
+{
+ base64_scheme_encode(&base64_scheme, 0, 0, src, src_size, dest);
+}
+
+static inline buffer_t *
+t_base64_encode(enum base64_encode_flags flags, size_t max_line_len,
+ const void *src, size_t src_size)
+{
+ return t_base64_scheme_encode(&base64_scheme, flags, max_line_len,
+ src, src_size);
+}
+
+static inline buffer_t *
+t_base64_encode_str(enum base64_encode_flags flags, size_t max_line_len,
+ const char *src)
+{
+ return t_base64_scheme_encode(&base64_scheme, flags, max_line_len,
+ src, strlen(src));
+}
+
+/* Translates base64 data into binary and appends it to dest buffer. See
+ base64_scheme_decode().
+
+ The src_pos_r parameter is deprecated and MUST be NULL.
+ */
+static inline int
+base64_decode(const void *src, size_t src_size, size_t *src_pos_r ATTR_UNUSED,
+ buffer_t *dest) ATTR_NULL(3)
+{
+ // NOTE: src_pos_r is deprecated here; to be removed in v2.4 */
+ i_assert(src_pos_r == NULL);
+
+ return base64_scheme_decode(&base64_scheme, 0, src, src_size, dest);
+}
+
+/* Decode given data to a buffer allocated from data stack. */
+static inline buffer_t *
+t_base64_decode(enum base64_decode_flags flags,
+ const void *src, size_t src_size)
+{
+ return t_base64_scheme_decode(&base64_scheme, flags, src, src_size);
+}
+
+/* Decode given string to a buffer allocated from data stack. */
+static inline buffer_t *t_base64_decode_str(const char *str)
+{
+ return t_base64_scheme_decode_str(&base64_scheme, 0, str);
+}
+
+/* Returns TRUE if c is a valid base64 encoding character (excluding '=') */
+static inline bool base64_is_valid_char(char c)
+{
+ return base64_scheme_is_valid_char(&base64_scheme, c);
+}
+
+/*
+ * "base64url" encoding scheme (RFC 4648, Section 5)
+ */
+
+extern struct base64_scheme base64url_scheme;
+
+/* Translates binary data into base64url. See base64_scheme_encode(). */
+static inline void
+base64url_encode(enum base64_encode_flags flags, size_t max_line_len,
+ const void *src, size_t src_size, buffer_t *dest)
+{
+ base64_scheme_encode(&base64url_scheme, flags, max_line_len,
+ src, src_size, dest);
+}
+
+static inline buffer_t *
+t_base64url_encode(enum base64_encode_flags flags, size_t max_line_len,
+ const void *src, size_t src_size)
+{
+ return t_base64_scheme_encode(&base64url_scheme, flags, max_line_len,
+ src, src_size);
+}
+
+static inline buffer_t *
+t_base64url_encode_str(enum base64_encode_flags flags, size_t max_line_len,
+ const char *src)
+{
+ return t_base64_scheme_encode(&base64url_scheme, flags, max_line_len,
+ src, strlen(src));
+}
+
+/* Translates base64url data into binary and appends it to dest buffer. See
+ base64_scheme_decode(). */
+static inline int
+base64url_decode(enum base64_decode_flags flags,
+ const void *src, size_t src_size, buffer_t *dest)
+{
+ return base64_scheme_decode(&base64url_scheme, flags,
+ src, src_size, dest);
+}
+
+/* Decode given data to a buffer allocated from data stack. */
+static inline buffer_t *
+t_base64url_decode(enum base64_decode_flags flags,
+ const void *src, size_t src_size)
+{
+ return t_base64_scheme_decode(&base64url_scheme, flags, src, src_size);
+}
+
+/* Decode given string to a buffer allocated from data stack. */
+static inline buffer_t *
+t_base64url_decode_str(enum base64_decode_flags flags, const char *str)
+{
+ return t_base64_scheme_decode_str(&base64url_scheme, flags, str);
+}
+
+/* Returns TRUE if c is a valid base64url encoding character (excluding '=') */
+static inline bool base64url_is_valid_char(char c)
+{
+ return base64_scheme_is_valid_char(&base64url_scheme, c);
+}
+
+#endif
diff --git a/src/lib/bits.c b/src/lib/bits.c
new file mode 100644
index 0000000..6366cf2
--- /dev/null
+++ b/src/lib/bits.c
@@ -0,0 +1,36 @@
+/* Copyright (c) 2001-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+
+/*
+ * We could use bits_required64() unconditionally, but that's unnecessary
+ * and way more heavy weight on 32-bit systems.
+ */
+#ifdef _LP64
+#define BITS_REQUIRED(x) bits_required64(x)
+#else
+#define BITS_REQUIRED(x) bits_required32(x)
+#endif
+
+size_t nearest_power(size_t num)
+{
+ i_assert(num <= ((size_t)1 << (CHAR_BIT*sizeof(size_t) - 1)));
+
+ if (num == 0)
+ return 1;
+
+ return 1UL << BITS_REQUIRED(num - 1);
+}
+
+#if __GNUC__ > 3 || (__GNUC__ == 3 && __GNUC_MINOR__ >= 4)
+/* Lucky you, it's all inline intrinsics */
+#else
+unsigned int bits_required8(uint8_t num)
+{
+ int ret = 0;
+ if (num > 0xf) { ret += 4; num >>= 4; }
+ if (num > 0x3) { ret += 2; num >>= 2; }
+ num &= ~(num>>1); /* 3->2, else unchanged */
+ return ret + num;
+}
+#endif
diff --git a/src/lib/bits.h b/src/lib/bits.h
new file mode 100644
index 0000000..2e84755
--- /dev/null
+++ b/src/lib/bits.h
@@ -0,0 +1,184 @@
+#ifndef BITS_H
+#define BITS_H
+
+#define UINT64_SUM_OVERFLOWS(a, b) \
+ (a > (uint64_t)-1 - b)
+
+#define BIT(n) (1u << (n))
+
+/* These expressions make it easy to ensure that bit test expressions
+ are boolean in order to satisfy the in-house -Wstrict-bool. */
+/* ((val & bits) == 0) is very common */
+#define HAS_NO_BITS(val,bits) (((val) & (bits)) == 0)
+/* ((val & bits) != 0) is even more common */
+/* Note - illogical behaviour if bits==0, fixing that requires potential
+ multiple evaluation, but it's a corner case that should never occur. */
+#define HAS_ANY_BITS(val,bits) (((val) & (bits)) != 0)
+/* ((val & bits) == bits) is uncommon */
+#define HAS_ALL_BITS(val,bits) ((~(val) & (bits)) == 0)
+
+/* negation implemented without using the subtraction operator
+ ~(x - 1) = 1 + ~x these are equivalent by -(-x) == ~(~(x)) == x */
+#define UNSIGNED_MINUS(x) (1 + ~(x))
+
+/* Returns x, such that x is the smallest power of 2 >= num. */
+size_t nearest_power(size_t num) ATTR_CONST;
+
+#if __GNUC__ > 3 || (__GNUC__ == 3 && __GNUC_MINOR__ >= 4)
+
+/* Returns TRUE if 2^x=num, i.e. if num has only a single bit set to 1. */
+static inline bool ATTR_CONST
+bits_is_power_of_two(uint64_t num)
+{
+ return __builtin_popcountll(num) == 1;
+}
+
+static inline unsigned int ATTR_CONST
+bits_required32(uint32_t num)
+{
+ return num == 0 ? 0 : 32 - __builtin_clz(num);
+}
+static inline unsigned int ATTR_CONST
+bits_required8(uint8_t num) { return bits_required32(num); }
+
+static inline unsigned int ATTR_CONST
+bits_required16(uint16_t num) { return bits_required32(num); }
+
+static inline unsigned int ATTR_CONST
+bits_required64(uint64_t num)
+{
+ return num == 0 ? 0 : 64 - __builtin_clzll(num);
+}
+
+#else
+
+/* Returns TRUE if 2^x=num, i.e. if num has only a single bit set to 1. */
+static inline bool ATTR_CONST
+bits_is_power_of_two(uint64_t num)
+{
+ return num != 0 && (num & (num + ~0ULL)) == 0;
+}
+
+unsigned int bits_required8(uint8_t num) ATTR_CONST;
+
+static inline
+unsigned int bits_required16(uint16_t num)
+{
+ return (num <= 0xff) ? bits_required8(num)
+ : 8 + bits_required8(num >> 8);
+}
+static inline
+unsigned int bits_required32(uint32_t num)
+{
+ return (num <= 0xffff) ? bits_required16(num)
+ : 16 + bits_required16(num >> 16);
+}
+static inline
+unsigned int bits_required64(uint64_t num)
+{
+ return (num <= 0xffffffff) ? bits_required32(num)
+ : 32 + bits_required32(num >> 32);
+}
+
+#endif
+
+static inline uint64_t ATTR_NO_SANITIZE_INTEGER
+ ATTR_NO_SANITIZE_IMPLICIT_CONVERSION
+bits_rotl64(uint64_t num, unsigned int count)
+{
+ const unsigned int mask = CHAR_BIT*sizeof(num) - 1;
+ count &= mask;
+ return (num << count) | (num >> (UNSIGNED_MINUS(count) & mask));
+}
+
+static inline uint32_t ATTR_NO_SANITIZE_INTEGER
+ ATTR_NO_SANITIZE_IMPLICIT_CONVERSION
+bits_rotl32(uint32_t num, unsigned int count)
+{
+ const unsigned int mask = CHAR_BIT*sizeof(num) - 1;
+ count &= mask;
+ return (num << count) | (num >> (UNSIGNED_MINUS(count) & mask));
+}
+
+static inline uint64_t ATTR_NO_SANITIZE_INTEGER
+ ATTR_NO_SANITIZE_IMPLICIT_CONVERSION
+bits_rotr64(uint64_t num, unsigned int count)
+{
+ const unsigned int mask = CHAR_BIT*sizeof(num) - 1;
+ count &= mask;
+ return (num >> count) | (num << (UNSIGNED_MINUS(count) & mask));
+}
+
+static inline uint32_t ATTR_NO_SANITIZE_INTEGER
+ ATTR_NO_SANITIZE_IMPLICIT_CONVERSION
+bits_rotr32(uint32_t num, unsigned int count)
+{
+ const unsigned int mask = CHAR_BIT*sizeof(num) - 1;
+ count &= mask;
+ return (num >> count) | (num << (UNSIGNED_MINUS(count) & mask));
+}
+
+/* These functions look too big to be inline, but in almost all expected
+ uses, 'fracbits' will be a compile-time constant, and most of the
+ expressions will simplify greatly.
+*/
+
+/* Perform a piecewise-linear approximation to a log2, with fracbits "fractional" bits.
+ Best explained with examples:
+ With 2 fractional bits splitting each power of 2 into 4 bands:
+ 00, 01, 10, 11 -> 00, 01, 10, 11 (small corner cases)
+ 100, 101, 110, 111 -> 100, 101, 110, 111 ([4-8) split into 4 bands)
+ 1000, 1001, 1010, 1011 -> 1000, 1000, 1001, 1001 ([8-15) split ...
+ 1100, 1101, 1110, 1111 -> 1010, 1010, 1011, 1011 ... into 4 bands)
+ [16..31) -> 11bb
+ [32..63) -> 100bb
+ [64..127) -> 101bb
+ [128..255) -> 110bb
+ e.g. 236 = 11101100 -> ((8-2)<<2 == 11000) + (111.....>> 5 == 111) - 100 == 11011
+ */
+static inline unsigned int ATTR_CONST
+bits_fraclog(unsigned int val, unsigned int fracbits)
+{
+ unsigned bits = bits_required32(val);
+ if (bits <= fracbits + 1)
+ return val;
+
+ unsigned int bandnum = bits - fracbits;
+ unsigned int bandstart = bandnum << fracbits;
+ unsigned int fracoffsbad = val >> (bandnum - 1); /* has leading 1 still */
+ unsigned int bucket = bandstart + fracoffsbad - BIT(fracbits);
+ return bucket;
+}
+static inline unsigned int ATTR_CONST
+bits_fraclog_bucket_start(unsigned int bucket, unsigned int fracbits)
+{
+ unsigned int bandnum = bucket >> fracbits;
+ if (bandnum <= 1)
+ return bucket;
+ if (fracbits == 0)
+ return BIT(bucket - 1);
+ unsigned int fracoffs = bucket & (BIT(fracbits)-1);
+ unsigned int fracoffs1 = BIT(fracbits) + fracoffs;
+ unsigned int bandstart = fracoffs1 << (bandnum - 1);
+ return bandstart;
+}
+static inline unsigned int ATTR_CONST ATTR_NO_SANITIZE_INTEGER
+ ATTR_NO_SANITIZE_IMPLICIT_CONVERSION
+bits_fraclog_bucket_end(unsigned int bucket, unsigned int fracbits)
+{
+ unsigned int bandnum = bucket >> fracbits;
+ if (bandnum <= 1)
+ return bucket;
+ if (fracbits == 0)
+ return BIT(bucket - 1) * 2 - 1;
+ unsigned int fracoffs = bucket & (BIT(fracbits)-1);
+ unsigned int nextfracoffs1 = 1 + BIT(fracbits) + fracoffs;
+ unsigned int nextbandstart = nextfracoffs1 << (bandnum - 1);
+ return nextbandstart - 1;
+}
+/* UNSAFE: multiple use of parameter (but expecting a constant in reality).
+ But a macro as it's most likely to be used to declare an array size.
+*/
+#define BITS_FRACLOG_BUCKETS(bits) ((33u - (bits)) << (bits))
+
+#endif
diff --git a/src/lib/bsearch-insert-pos.c b/src/lib/bsearch-insert-pos.c
new file mode 100644
index 0000000..dc92f47
--- /dev/null
+++ b/src/lib/bsearch-insert-pos.c
@@ -0,0 +1,48 @@
+/* Copyright (c) 2005-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "bsearch-insert-pos.h"
+
+#undef bsearch_insert_pos
+bool bsearch_insert_pos(const void *key, const void *base, unsigned int nmemb,
+ size_t size, int (*cmp)(const void *, const void *),
+ unsigned int *idx_r)
+{
+ const void *p;
+ unsigned int idx, left_idx, right_idx;
+ int ret;
+
+ i_assert(nmemb < INT_MAX);
+
+ idx = 0; left_idx = 0; right_idx = nmemb;
+ while (left_idx < right_idx) {
+ idx = (left_idx + right_idx) / 2;
+
+ p = CONST_PTR_OFFSET(base, idx * size);
+ ret = cmp(key, p);
+ if (ret > 0)
+ left_idx = idx+1;
+ else if (ret < 0)
+ right_idx = idx;
+ else {
+ *idx_r = idx;
+ return TRUE;
+ }
+ }
+
+ if (left_idx > idx)
+ idx++;
+
+ *idx_r = idx;
+ return FALSE;
+}
+
+bool array_bsearch_insert_pos_i(const struct array *array, const void *key,
+ int (*cmp)(const void *, const void *),
+ unsigned int *idx_r)
+{
+ return bsearch_insert_pos(key, array->buffer->data,
+ array_count_i(array),
+ array->element_size, cmp, idx_r);
+}
diff --git a/src/lib/bsearch-insert-pos.h b/src/lib/bsearch-insert-pos.h
new file mode 100644
index 0000000..320182b
--- /dev/null
+++ b/src/lib/bsearch-insert-pos.h
@@ -0,0 +1,52 @@
+#ifndef BSEARCH_INSERT_POS_H
+#define BSEARCH_INSERT_POS_H
+
+/* Binary search template - getdata must be the name of a pure function
+ or a function-like macro that takes the two obvious parameters. */
+#define BINARY_NUMERIC_SEARCH(getdata, data, count, value, idx_r) \
+ unsigned int idx, left_idx, right_idx; \
+ \
+ i_assert((count) < INT_MAX); \
+ left_idx = 0; right_idx = (count); \
+ while (left_idx < right_idx) { \
+ idx = (left_idx + right_idx) / 2; \
+ \
+ if (getdata(data, idx) < (value)) \
+ left_idx = idx+1; \
+ else if (getdata(data, idx) > (value))\
+ right_idx = idx; \
+ else { \
+ *(idx_r) = idx; \
+ return TRUE; \
+ } \
+ } \
+ return FALSE
+
+#define BINARY_SEARCH_ARRAY_GET(array, index) ((array)[(index)])
+#define BINARY_NUMBER_SEARCH(data, count, value, idx_r) \
+ BINARY_NUMERIC_SEARCH(BINARY_SEARCH_ARRAY_GET, data, count, value, idx_r);
+
+/* If key is found, returns TRUE and sets idx_r to the position where the key
+ was found. If key isn't found, returns FALSE and sets idx_r to the position
+ where the key should be inserted. */
+bool ATTR_NOWARN_UNUSED_RESULT
+bsearch_insert_pos(const void *key, const void *base, unsigned int nmemb,
+ size_t size, int (*cmp)(const void *, const void *),
+ unsigned int *idx_r);
+#define bsearch_insert_pos(key, base, nmemb, size, cmp, idx_r) \
+ bsearch_insert_pos(key, base, nmemb, size - \
+ CALLBACK_TYPECHECK(cmp, int (*)(typeof(const typeof(*key) *), \
+ typeof(const typeof(*base) *))), \
+ (int (*)(const void *, const void *))cmp, idx_r)
+
+bool ATTR_NOWARN_UNUSED_RESULT
+array_bsearch_insert_pos_i(const struct array *array, const void *key,
+ int (*cmp)(const void *, const void *),
+ unsigned int *idx_r);
+#define array_bsearch_insert_pos(array, key, cmp, idx_r) \
+ array_bsearch_insert_pos_i(&(array)->arr - \
+ CALLBACK_TYPECHECK(cmp, int (*)(typeof(const typeof(*key) *), \
+ typeof(*(array)->v))), \
+ (const void *)key, (int (*)(const void *, const void *))cmp, idx_r)
+
+#endif
diff --git a/src/lib/buffer-istream.c b/src/lib/buffer-istream.c
new file mode 100644
index 0000000..3898fd0
--- /dev/null
+++ b/src/lib/buffer-istream.c
@@ -0,0 +1,49 @@
+/* Copyright (c) 2020 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "buffer.h"
+#include "eacces-error.h"
+#include "istream.h"
+
+enum buffer_append_result
+buffer_append_full_istream(buffer_t *buf, struct istream *is, size_t max_read_size,
+ const char **error_r)
+{
+ const unsigned char *data;
+ size_t size;
+ ssize_t ret;
+
+ while ((ret = i_stream_read_more(is, &data, &size)) > 0) {
+ if (max_read_size == 0)
+ return BUFFER_APPEND_READ_MAX_SIZE;
+ size = I_MIN(max_read_size, size);
+ buffer_append(buf, data, size);
+ i_stream_skip(is, size);
+ max_read_size -= size;
+ }
+
+ if (ret == 0)
+ return BUFFER_APPEND_READ_MORE;
+
+ i_assert(is->eof);
+
+ if (is->stream_errno != 0) {
+ *error_r = i_stream_get_error(is);
+ return BUFFER_APPEND_READ_ERROR;
+ }
+ return BUFFER_APPEND_OK;
+}
+
+enum buffer_append_result
+buffer_append_full_file(buffer_t *buf, const char *file, size_t max_read_size,
+ const char **error_r)
+{
+ struct istream *is = i_stream_create_file(file, IO_BLOCK_SIZE);
+ enum buffer_append_result res =
+ buffer_append_full_istream(buf, is, max_read_size, error_r);
+ if (is->stream_errno == EACCES)
+ *error_r = eacces_error_get("open", file);
+ i_stream_unref(&is);
+ i_assert(res != BUFFER_APPEND_READ_MORE);
+ return res;
+}
diff --git a/src/lib/buffer.c b/src/lib/buffer.c
new file mode 100644
index 0000000..64255b5
--- /dev/null
+++ b/src/lib/buffer.c
@@ -0,0 +1,495 @@
+/* Copyright (c) 2002-2018 Dovecot authors, see the included COPYING file */
+
+/* @UNSAFE: whole file */
+
+#include "lib.h"
+#include "buffer.h"
+
+struct real_buffer {
+ union {
+ struct buffer buf;
+ struct {
+ /* public: */
+ const void *r_buffer;
+ size_t used;
+ /* private: */
+ unsigned char *w_buffer;
+ size_t dirty, alloc, writable_size, max_size;
+
+ pool_t pool;
+
+ bool alloced:1;
+ bool dynamic:1;
+ };
+ };
+};
+typedef int buffer_check_sizes[COMPILE_ERROR_IF_TRUE(sizeof(struct real_buffer) > sizeof(buffer_t)) ?1:1];
+
+static void buffer_alloc(struct real_buffer *buf, size_t size)
+{
+ i_assert(buf->w_buffer == NULL || buf->alloced);
+
+ if (size == buf->alloc)
+ return;
+
+ i_assert(size > buf->alloc);
+
+ if (buf->w_buffer == NULL)
+ buf->w_buffer = p_malloc(buf->pool, size);
+ else
+ buf->w_buffer = p_realloc(buf->pool, buf->w_buffer, buf->alloc, size);
+ buf->alloc = size;
+ buf->writable_size = size-1; /* -1 for str_c() NUL */
+
+ buf->r_buffer = buf->w_buffer;
+ buf->alloced = TRUE;
+}
+
+static inline void
+buffer_check_limits(struct real_buffer *buf, size_t pos, size_t data_size)
+{
+ size_t new_size;
+
+ if (unlikely(buf->max_size - pos < data_size))
+ i_panic("Buffer write out of range (%zu + %zu)", pos, data_size);
+
+ new_size = pos + data_size;
+
+ if (new_size > buf->used && buf->used < buf->dirty) {
+ /* clear used..dirty area */
+ size_t max = I_MIN(I_MIN(buf->alloc, buf->dirty), new_size);
+
+ memset(buf->w_buffer + buf->used, 0, max - buf->used);
+ }
+
+ /* Use buf->writable_size instead of buf->alloc to always keep +1 byte
+ available in case str_c() is called for this buffer. This is mainly
+ for cases where the buffer is allocated from data stack, and str_c()
+ is called in a separate stack frame. */
+ if (new_size > buf->writable_size) {
+ if (unlikely(!buf->dynamic)) {
+ i_panic("Buffer full (%zu > %zu, pool %s)",
+ pos + data_size, buf->alloc,
+ buf->pool == NULL ? "<none>" :
+ pool_get_name(buf->pool));
+ }
+
+ size_t new_alloc_size =
+ pool_get_exp_grown_size(buf->pool, buf->alloc,
+ new_size + 1);
+ if (new_alloc_size > buf->max_size) {
+ /* limit to max_size, but do include +1 for
+ str_c() NUL */
+ new_alloc_size = buf->max_size + 1;
+ }
+ buffer_alloc(buf, new_alloc_size);
+ }
+#if 0
+ else if (new_size > buf->used && buf->alloced &&
+ !buf->pool->alloconly_pool && !buf->pool->datastack_pool) {
+ void *new_buf;
+
+ /* buffer's size increased: move the buffer's memory elsewhere.
+ this should help catch bugs where old pointers are tried to
+ be used to access the buffer's memory */
+ new_buf = p_malloc(buf->pool, buf->alloc);
+ memcpy(new_buf, buf->w_buffer, buf->alloc);
+ p_free(buf->pool, buf->w_buffer);
+
+ buf->w_buffer = new_buf;
+ buf->r_buffer = new_buf;
+ }
+#endif
+
+ if (new_size > buf->used)
+ buf->used = new_size;
+ i_assert(buf->used <= buf->alloc);
+ i_assert(buf->w_buffer != NULL);
+}
+
+static inline void
+buffer_check_append_limits(struct real_buffer *buf, size_t data_size)
+{
+ /* Fast path: See if data to be appended fits into allocated buffer.
+ If it does, we don't even need to memset() the dirty buffer since
+ it's going to be filled with the newly appended data. */
+ if (buf->writable_size - buf->used < data_size)
+ buffer_check_limits(buf, buf->used, data_size);
+ else
+ buf->used += data_size;
+}
+
+#undef buffer_create_from_data
+void buffer_create_from_data(buffer_t *buffer, void *data, size_t size)
+{
+ struct real_buffer *buf;
+
+ i_assert(sizeof(*buffer) >= sizeof(struct real_buffer));
+
+ buf = container_of(buffer, struct real_buffer, buf);
+ i_zero(buf);
+ buf->alloc = buf->writable_size = buf->max_size = size;
+ buf->r_buffer = buf->w_buffer = data;
+ /* clear the whole memory area. unnecessary usually, but if the
+ buffer is used by e.g. str_c() it tries to access uninitialized
+ memory */
+ memset(data, 0, size);
+}
+
+#undef buffer_create_from_const_data
+void buffer_create_from_const_data(buffer_t *buffer,
+ const void *data, size_t size)
+{
+ struct real_buffer *buf;
+
+ i_assert(sizeof(*buffer) >= sizeof(struct real_buffer));
+
+ buf = container_of(buffer, struct real_buffer, buf);
+ i_zero(buf);
+
+ buf->used = buf->alloc = buf->writable_size = buf->max_size = size;
+ buf->r_buffer = data;
+ i_assert(buf->w_buffer == NULL);
+}
+
+buffer_t *buffer_create_dynamic(pool_t pool, size_t init_size)
+{
+ return buffer_create_dynamic_max(pool, init_size, SIZE_MAX);
+}
+
+buffer_t *buffer_create_dynamic_max(pool_t pool, size_t init_size,
+ size_t max_size)
+{
+ struct real_buffer *buf;
+
+#ifdef DEBUG
+ /* we increment this by 1 later on, so if it's SIZE_MAX
+ it turns into 0 and hides a potential bug.
+
+ Too scary to use in production for now, though. This
+ can change in future. */
+ i_assert(init_size < SIZE_MAX);
+#endif
+
+ buf = p_new(pool, struct real_buffer, 1);
+ buf->pool = pool;
+ buf->dynamic = TRUE;
+ buf->max_size = max_size;
+ /* buffer_alloc() reserves +1 for str_c() NIL, so add +1 here to
+ init_size so we can actually write that much to the buffer without
+ realloc */
+ buffer_alloc(buf, init_size+1);
+ return &buf->buf;
+}
+
+void buffer_free(buffer_t **_buf)
+{
+ if (*_buf == NULL)
+ return;
+ struct real_buffer *buf = container_of(*_buf, struct real_buffer, buf);
+
+ *_buf = NULL;
+ if (buf->alloced)
+ p_free(buf->pool, buf->w_buffer);
+ if (buf->pool != NULL)
+ p_free(buf->pool, buf);
+}
+
+void *buffer_free_without_data(buffer_t **_buf)
+{
+ struct real_buffer *buf = container_of(*_buf, struct real_buffer, buf);
+ void *data;
+
+ *_buf = NULL;
+
+ data = buf->w_buffer;
+ p_free(buf->pool, buf);
+ return data;
+}
+
+pool_t buffer_get_pool(const buffer_t *_buf)
+{
+ const struct real_buffer *buf =
+ container_of(_buf, const struct real_buffer, buf);
+
+ return buf->pool;
+}
+
+void buffer_write(buffer_t *_buf, size_t pos,
+ const void *data, size_t data_size)
+{
+ struct real_buffer *buf = container_of(_buf, struct real_buffer, buf);
+
+ buffer_check_limits(buf, pos, data_size);
+ if (data_size > 0)
+ memcpy(buf->w_buffer + pos, data, data_size);
+}
+
+void buffer_append(buffer_t *_buf, const void *data, size_t data_size)
+{
+ struct real_buffer *buf = container_of(_buf, struct real_buffer, buf);
+
+ if (data_size > 0) {
+ size_t pos = buf->used;
+ buffer_check_append_limits(buf, data_size);
+ memcpy(buf->w_buffer + pos, data, data_size);
+ }
+}
+
+void buffer_append_c(buffer_t *_buf, unsigned char chr)
+{
+ struct real_buffer *buf = container_of(_buf, struct real_buffer, buf);
+ size_t pos = buf->used;
+
+ buffer_check_append_limits(buf, 1);
+ buf->w_buffer[pos] = chr;
+}
+
+void buffer_insert(buffer_t *_buf, size_t pos,
+ const void *data, size_t data_size)
+{
+ struct real_buffer *buf = container_of(_buf, struct real_buffer, buf);
+
+ if (pos >= buf->used)
+ buffer_write(_buf, pos, data, data_size);
+ else {
+ buffer_copy(_buf, pos + data_size, _buf, pos, SIZE_MAX);
+ memcpy(buf->w_buffer + pos, data, data_size);
+ }
+}
+
+void buffer_delete(buffer_t *_buf, size_t pos, size_t size)
+{
+ struct real_buffer *buf = container_of(_buf, struct real_buffer, buf);
+ size_t end_size;
+
+ if (pos >= buf->used)
+ return;
+ end_size = buf->used - pos;
+
+ if (size < end_size) {
+ /* delete from between */
+ end_size -= size;
+ memmove(buf->w_buffer + pos,
+ buf->w_buffer + pos + size, end_size);
+ } else {
+ /* delete the rest of the buffer */
+ end_size = 0;
+ }
+
+ buffer_set_used_size(_buf, pos + end_size);
+}
+
+void buffer_replace(buffer_t *_buf, size_t pos, size_t size,
+ const void *data, size_t data_size)
+{
+ struct real_buffer *buf = container_of(_buf, struct real_buffer, buf);
+ size_t end_size;
+
+ if (pos >= buf->used) {
+ buffer_write(_buf, pos, data, data_size);
+ return;
+ }
+ end_size = buf->used - pos;
+
+ if (size < end_size) {
+ end_size -= size;
+ if (data_size == 0) {
+ /* delete from between */
+ memmove(buf->w_buffer + pos,
+ buf->w_buffer + pos + size, end_size);
+ } else {
+ /* insert */
+ buffer_copy(_buf, pos + data_size, _buf, pos + size,
+ SIZE_MAX);
+ memcpy(buf->w_buffer + pos, data, data_size);
+ }
+ } else {
+ /* overwrite the end */
+ end_size = 0;
+ buffer_write(_buf, pos, data, data_size);
+ }
+
+ buffer_set_used_size(_buf, pos + data_size + end_size);
+}
+
+
+void buffer_write_zero(buffer_t *_buf, size_t pos, size_t data_size)
+{
+ struct real_buffer *buf = container_of(_buf, struct real_buffer, buf);
+
+ buffer_check_limits(buf, pos, data_size);
+ memset(buf->w_buffer + pos, 0, data_size);
+}
+
+void buffer_append_zero(buffer_t *_buf, size_t data_size)
+{
+ struct real_buffer *buf = container_of(_buf, struct real_buffer, buf);
+
+ /* NOTE: When appending it's enough to check that the limits are
+ valid, because the data is already guaranteed to be zero-filled. */
+ buffer_check_limits(buf, buf->used, data_size);
+}
+
+void buffer_insert_zero(buffer_t *_buf, size_t pos, size_t data_size)
+{
+ struct real_buffer *buf = container_of(_buf, struct real_buffer, buf);
+
+ if (pos >= buf->used)
+ buffer_write_zero(_buf, pos, data_size);
+ else {
+ buffer_copy(_buf, pos + data_size, _buf, pos, SIZE_MAX);
+ memset(buf->w_buffer + pos, 0, data_size);
+ }
+}
+
+void buffer_copy(buffer_t *_dest, size_t dest_pos,
+ const buffer_t *_src, size_t src_pos, size_t copy_size)
+{
+ struct real_buffer *dest = container_of(_dest, struct real_buffer, buf);
+ const struct real_buffer *src =
+ container_of(_src, const struct real_buffer, buf);
+ size_t max_size;
+
+ i_assert(src_pos <= src->used);
+
+ max_size = src->used - src_pos;
+ if (copy_size > max_size)
+ copy_size = max_size;
+
+ buffer_check_limits(dest, dest_pos, copy_size);
+ i_assert(src->r_buffer != NULL);
+
+ if (src == dest) {
+ memmove(dest->w_buffer + dest_pos,
+ CONST_PTR_OFFSET(src->r_buffer, src_pos), copy_size);
+ } else {
+ memcpy(dest->w_buffer + dest_pos,
+ CONST_PTR_OFFSET(src->r_buffer, src_pos), copy_size);
+ }
+}
+
+void buffer_append_buf(buffer_t *dest, const buffer_t *src,
+ size_t src_pos, size_t copy_size)
+{
+ buffer_copy(dest, dest->used, src, src_pos, copy_size);
+}
+
+void *buffer_get_space_unsafe(buffer_t *_buf, size_t pos, size_t size)
+{
+ struct real_buffer *buf = container_of(_buf, struct real_buffer, buf);
+
+ buffer_check_limits(buf, pos, size);
+ return buf->w_buffer + pos;
+}
+
+void *buffer_append_space_unsafe(buffer_t *buf, size_t size)
+{
+ /* NOTE: can't use buffer_check_append_limits() here because it doesn't
+ guarantee that the buffer is zero-filled. */
+ return buffer_get_space_unsafe(buf, buf->used, size);
+}
+
+void *buffer_get_modifiable_data(const buffer_t *_buf, size_t *used_size_r)
+{
+ const struct real_buffer *buf =
+ container_of(_buf, const struct real_buffer, buf);
+
+ if (used_size_r != NULL)
+ *used_size_r = buf->used;
+ i_assert(buf->used == 0 || buf->w_buffer != NULL);
+ return buf->w_buffer;
+}
+
+void buffer_set_used_size(buffer_t *_buf, size_t used_size)
+{
+ struct real_buffer *buf = container_of(_buf, struct real_buffer, buf);
+
+ i_assert(used_size <= buf->alloc);
+
+ if (buf->used > buf->dirty)
+ buf->dirty = buf->used;
+
+ buf->used = used_size;
+}
+
+size_t buffer_get_size(const buffer_t *_buf)
+{
+ const struct real_buffer *buf =
+ container_of(_buf, const struct real_buffer, buf);
+
+ return buf->alloc;
+}
+
+size_t buffer_get_writable_size(const buffer_t *_buf)
+{
+ const struct real_buffer *buf =
+ container_of(_buf, const struct real_buffer, buf);
+
+ /* Use buf->writable_size instead of buf->alloc to reserve +1 for
+ str_c() NUL in buffer_check_limits(). Otherwise the caller might
+ increase the buffer's alloc size unnecessarily when it just wants
+ to access the entire buffer. */
+ return buf->writable_size;
+}
+
+size_t buffer_get_avail_size(const buffer_t *_buf)
+{
+ const struct real_buffer *buf =
+ container_of(_buf, const struct real_buffer, buf);
+
+ i_assert(buf->alloc >= buf->used);
+ return ((buf->dynamic ? SIZE_MAX : buf->alloc) - buf->used);
+}
+
+bool buffer_cmp(const buffer_t *buf1, const buffer_t *buf2)
+{
+ if (buf1->used != buf2->used)
+ return FALSE;
+ if (buf1->used == 0)
+ return TRUE;
+
+ return memcmp(buf1->data, buf2->data, buf1->used) == 0;
+}
+
+void buffer_verify_pool(buffer_t *_buf)
+{
+ const struct real_buffer *buf =
+ container_of(_buf, struct real_buffer, buf);
+ void *ret;
+
+ if (buf->pool != NULL && buf->pool->datastack_pool && buf->alloc > 0) {
+ /* this doesn't really do anything except verify the
+ stack frame */
+ ret = p_realloc(buf->pool, buf->w_buffer,
+ buf->alloc, buf->alloc);
+ i_assert(ret == buf->w_buffer);
+ }
+}
+
+void ATTR_NO_SANITIZE_IMPLICIT_CONVERSION
+ ATTR_NO_SANITIZE_INTEGER
+buffer_truncate_rshift_bits(buffer_t *buf, size_t bits)
+{
+ /* no-op if it's shorten than bits in any case.. */
+ if (buf->used * 8 < bits) return;
+
+ if (bits > 0) {
+ /* truncate it to closest byte boundary */
+ size_t bytes = ((bits + 7) & ~(size_t)7) / 8;
+ /* remaining bits */
+ bits = bits % 8;
+ buffer_set_used_size(buf, I_MIN(bytes, buf->used));
+ unsigned char *ptr = buffer_get_modifiable_data(buf, &bytes);
+ /* right shift over byte array */
+ if (bits > 0) {
+ for(size_t i=bytes-1;i>0;i--)
+ ptr[i] = (ptr[i]>>(8-bits)) +
+ ((ptr[i-1]&(0xff>>(bits)))<<bits);
+ ptr[0] = ptr[0]>>(8-bits);
+ }
+ } else {
+ buffer_set_used_size(buf, 0);
+ }
+}
+
diff --git a/src/lib/buffer.h b/src/lib/buffer.h
new file mode 100644
index 0000000..902ec88
--- /dev/null
+++ b/src/lib/buffer.h
@@ -0,0 +1,199 @@
+#ifndef BUFFER_H
+#define BUFFER_H
+
+struct buffer {
+ union {
+ struct {
+ const void *data;
+ const size_t used;
+ };
+ void *priv[9];
+ };
+};
+
+/* WARNING: Be careful with functions that return pointers to data.
+ With dynamic buffers they are valid only as long as buffer is not
+ realloc()ed. You shouldn't rely on it being valid if you have modified
+ buffer in any way. */
+
+/* Create a modifiable buffer from given data. Writes past this size will
+ i_panic(). */
+void buffer_create_from_data(buffer_t *buffer, void *data, size_t size);
+/* Create a non-modifiable buffer from given data. */
+void buffer_create_from_const_data(buffer_t *buffer,
+ const void *data, size_t size);
+#define buffer_create_from_data(b,d,s) \
+ TYPE_CHECKS(void, \
+ COMPILE_ERROR_IF_TRUE(__builtin_object_size((d),1) < ((s)>0?(s):1)), \
+ buffer_create_from_data((b), (d), (s)))
+#define buffer_create_from_const_data(b,d,s) \
+ TYPE_CHECKS(void, \
+ COMPILE_ERROR_IF_TRUE(__builtin_object_size((d),1) < ((s)>0?(s):1)), \
+ buffer_create_from_const_data((b), (d), (s)))
+
+/* Creates a dynamically growing buffer. Whenever write would exceed the
+ current size it's grown. */
+buffer_t *buffer_create_dynamic(pool_t pool, size_t init_size);
+/* Create a dynamically growing buffer with a maximum size. Writes past the
+ maximum size will i_panic(). Internally allow it to grow max_size+1 so
+ str_c() NUL can be used. */
+buffer_t *buffer_create_dynamic_max(pool_t pool, size_t init_size,
+ size_t max_size);
+
+#define t_buffer_create(init_size) \
+ buffer_create_dynamic(pool_datastack_create(), (init_size))
+
+/* Free the memory used by buffer. Not needed if the memory is free'd
+ directly from the memory pool. */
+void buffer_free(buffer_t **buf);
+/* Free the memory used by buffer structure, but return the buffer data
+ unfree'd. */
+void *buffer_free_without_data(buffer_t **buf);
+
+/* Returns the pool buffer was created with. */
+pool_t buffer_get_pool(const buffer_t *buf) ATTR_PURE;
+
+/* Write data to buffer at specified position. If pos is beyond the buffer's
+ current size, it is zero-filled up to that point (even if data_size==0). */
+void buffer_write(buffer_t *buf, size_t pos,
+ const void *data, size_t data_size);
+/* Append data to buffer. */
+void buffer_append(buffer_t *buf, const void *data, size_t data_size);
+/* Append character to buffer. */
+void buffer_append_c(buffer_t *buf, unsigned char chr);
+
+/* Insert the provided data into the buffer at position pos. If pos points past
+ the current buffer size, the gap is zero-filled. */
+void buffer_insert(buffer_t *buf, size_t pos,
+ const void *data, size_t data_size);
+/* Delete data with the indicated size from the buffer at position pos. The
+ deleted block may cross the current buffer size boundary, which is ignored.
+ */
+void buffer_delete(buffer_t *buf, size_t pos, size_t size);
+/* Replace the data in the buffer with the indicated size at position pos with
+ the provided data. This is a more optimized version of
+ buffer_delete(buf, pos, size); buffer_insert(buf, pos, data, data_size); */
+void buffer_replace(buffer_t *buf, size_t pos, size_t size,
+ const void *data, size_t data_size);
+
+/* Fill buffer with zero bytes. */
+void buffer_write_zero(buffer_t *buf, size_t pos, size_t data_size);
+void buffer_append_zero(buffer_t *buf, size_t data_size);
+void buffer_insert_zero(buffer_t *buf, size_t pos, size_t data_size);
+
+/* Copy data from buffer to another. The buffers may be same in which case
+ it's internal copying, possibly with overlapping positions (ie. memmove()
+ like functionality). copy_size may be set to SIZE_MAX to copy the rest of
+ the used data in buffer. */
+void buffer_copy(buffer_t *dest, size_t dest_pos,
+ const buffer_t *src, size_t src_pos, size_t copy_size);
+/* Append data to buffer from another. copy_size may be set to SIZE_MAX to
+ copy the rest of the used data in buffer. */
+void buffer_append_buf(buffer_t *dest, const buffer_t *src,
+ size_t src_pos, size_t copy_size);
+
+/* Returns pointer to specified position in buffer. WARNING: The returned
+ address may become invalid if you add more data to buffer. */
+void *buffer_get_space_unsafe(buffer_t *buf, size_t pos, size_t size);
+/* Increase the buffer usage by given size, and return a pointer to beginning
+ of it. */
+void *buffer_append_space_unsafe(buffer_t *buf, size_t size);
+
+/* Like buffer_get_data(), but don't return it as const. Returns NULL if the
+ buffer is non-modifiable. WARNING: The returned address may become invalid
+ if you add more data to buffer. */
+void *buffer_get_modifiable_data(const buffer_t *buf, size_t *used_size_r)
+ ATTR_NULL(2);
+
+/* Set the "used size" of buffer, ie. 0 would set the buffer empty.
+ Must not be used to grow buffer. The data after the buffer's new size will
+ be effectively lost, because e.g. buffer_get_space_unsafe() will zero out
+ the contents. */
+void buffer_set_used_size(buffer_t *buf, size_t used_size);
+
+/* Returns the current buffer size. */
+size_t buffer_get_size(const buffer_t *buf) ATTR_PURE;
+/* Returns how many bytes we can write to buffer without increasing its size.
+ With dynamic buffers this is buffer_get_size()-1, because the extra 1 byte
+ is reserved for str_c()'s NUL. */
+size_t buffer_get_writable_size(const buffer_t *buf) ATTR_PURE;
+/* Returns the maximum number of bytes we can append to the buffer. If the
+ buffer is dynamic, this is always near SIZE_MAX. */
+size_t buffer_get_avail_size(const buffer_t *buf) ATTR_PURE;
+
+/* Returns TRUE if buffer contents are identical. */
+bool buffer_cmp(const buffer_t *buf1, const buffer_t *buf2);
+
+/* Returns pointer to beginning of buffer data. Current used size of buffer is
+ stored in used_size if it's non-NULL. */
+static inline const void * ATTR_NULL(2)
+buffer_get_data(const buffer_t *buf, size_t *used_size_r)
+{
+ if (used_size_r != NULL)
+ *used_size_r = buf->used;
+ return buf->data;
+}
+
+/* Returns the current used buffer size. */
+static inline size_t ATTR_PURE
+buffer_get_used_size(const buffer_t *buf)
+{
+ return buf->used;
+}
+
+/* Crash if buffer was allocated from data stack and stack frame has changed.
+ This can be used as an assert-like check to verify that it's valid to
+ increase the buffer size here, instead of crashing only randomly when the
+ buffer needs to be increased. */
+void buffer_verify_pool(buffer_t *buf);
+
+/* This will truncate your byte buffer to contain at most
+ given number of bits.
+
+ 1 bits: 01 00000001
+ 2 bits: 03 00000011
+ 3 bits: 07 00000111
+ 4 bits: 0f 00001111
+ 5 bits: 1f 00011111
+ 6 bits: 3f 00111111
+ 7 bits: 7f 01111111
+ 8 bits: ff 11111111
+ 9 bits: 01ff 0000000111111111
+10 bits: 03ff 0000001111111111
+11 bits: 07ff 0000011111111111
+12 bits: 0fff 0000111111111111
+13 bits: 1fff 0001111111111111
+14 bits: 3fff 0011111111111111
+15 bits: 7fff 0111111111111111
+16 bits: ffff 1111111111111111
+
+ and so forth
+
+*/
+void buffer_truncate_rshift_bits(buffer_t *buf, size_t bits);
+
+enum buffer_append_result {
+ /* Stream reached EOF successfully */
+ BUFFER_APPEND_OK = 0,
+ /* Error was encountered */
+ BUFFER_APPEND_READ_ERROR = -1,
+ /* Stream is non-blocking, call again later */
+ BUFFER_APPEND_READ_MORE = -2,
+ /* Stream was consumed up to max_read_size */
+ BUFFER_APPEND_READ_MAX_SIZE = -3,
+};
+
+/* Attempt to fully read a stream. Since this can be a network stream, it
+ can return BUFFER_APPEND_READ_MORE, which means you need to call this
+ function again. It is caller's responsibility to keep track of
+ max_read_size in case more reading is needed. */
+enum buffer_append_result
+buffer_append_full_istream(buffer_t *buf, struct istream *is, size_t max_read_size,
+ const char **error_r);
+
+/* Attempt to fully read a file. BUFFER_APPEND_READ_MORE is never returned. */
+enum buffer_append_result
+buffer_append_full_file(buffer_t *buf, const char *file, size_t max_read_size,
+ const char **error_r);
+
+#endif
diff --git a/src/lib/byteorder.h b/src/lib/byteorder.h
new file mode 100644
index 0000000..e7912bd
--- /dev/null
+++ b/src/lib/byteorder.h
@@ -0,0 +1,268 @@
+/*
+ * Copyright (c) 2016-2017 Josef 'Jeff' Sipek <jeffpc@josefsipek.net>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+#ifndef BYTEORDER_H
+#define BYTEORDER_H
+
+/*
+ * These prototypes exist to catch bugs in the code generating macros below.
+ */
+/* return byte swapped input */
+static inline uint64_t i_bswap_64(uint64_t in);
+static inline uint32_t i_bswap_32(uint32_t in);
+static inline uint16_t i_bswap_16(uint16_t in);
+static inline uint8_t i_bswap_8(uint8_t in);
+
+/* load an unaligned cpu native endian number from memory */
+static inline uint64_t cpu64_to_cpu_unaligned(const void *in);
+static inline uint32_t cpu32_to_cpu_unaligned(const void *in);
+static inline uint16_t cpu16_to_cpu_unaligned(const void *in);
+static inline uint8_t cpu8_to_cpu_unaligned(const void *in);
+
+/* load an unaligned big endian number from memory */
+static inline uint64_t be64_to_cpu_unaligned(const void *in);
+static inline uint32_t be32_to_cpu_unaligned(const void *in);
+static inline uint16_t be16_to_cpu_unaligned(const void *in);
+static inline uint8_t be8_to_cpu_unaligned(const void *in);
+
+/* load an unaligned little endian number from memory */
+static inline uint64_t le64_to_cpu_unaligned(const void *in);
+static inline uint32_t le32_to_cpu_unaligned(const void *in);
+static inline uint16_t le16_to_cpu_unaligned(const void *in);
+static inline uint8_t le8_to_cpu_unaligned(const void *in);
+
+/* store into memory a cpu native endian number as a big endian number */
+static inline void cpu64_to_be_unaligned(uint64_t in, void *out);
+static inline void cpu32_to_be_unaligned(uint32_t in, void *out);
+static inline void cpu16_to_be_unaligned(uint16_t in, void *out);
+static inline void cpu8_to_be_unaligned(uint8_t in, void *out);
+
+/* store into memory a cpu native endian number as a little endian number */
+static inline void cpu64_to_le_unaligned(uint64_t in, void *out);
+static inline void cpu32_to_le_unaligned(uint32_t in, void *out);
+static inline void cpu16_to_le_unaligned(uint16_t in, void *out);
+static inline void cpu8_to_le_unaligned(uint8_t in, void *out);
+
+/* convert a big endian input into cpu native endian */
+static inline uint64_t be64_to_cpu(uint64_t in);
+static inline uint32_t be32_to_cpu(uint32_t in);
+static inline uint16_t be16_to_cpu(uint16_t in);
+static inline uint8_t be8_to_cpu(uint8_t in);
+
+/* convert a cpu native endian input into big endian */
+static inline uint64_t cpu64_to_be(uint64_t in);
+static inline uint32_t cpu32_to_be(uint32_t in);
+static inline uint16_t cpu16_to_be(uint16_t in);
+static inline uint8_t cpu8_to_be(uint8_t in);
+
+/* convert a little endian input into cpu native endian */
+static inline uint64_t le64_to_cpu(uint64_t in);
+static inline uint32_t le32_to_cpu(uint32_t in);
+static inline uint16_t le16_to_cpu(uint16_t in);
+static inline uint8_t le8_to_cpu(uint8_t in);
+
+/* convert a cpu native endian input into little endian */
+static inline uint64_t cpu64_to_le(uint64_t in);
+static inline uint32_t cpu32_to_le(uint32_t in);
+static inline uint16_t cpu16_to_le(uint16_t in);
+static inline uint8_t cpu8_to_le(uint8_t in);
+
+/*
+ * byte swapping
+ */
+static inline uint64_t i_bswap_64(uint64_t in)
+{
+ return ((in & 0xff00000000000000ULL) >> 56) |
+ ((in & 0x00ff000000000000ULL) >> 40) |
+ ((in & 0x0000ff0000000000ULL) >> 24) |
+ ((in & 0x000000ff00000000ULL) >> 8) |
+ ((in & 0x00000000ff000000ULL) << 8) |
+ ((in & 0x0000000000ff0000ULL) << 24) |
+ ((in & 0x000000000000ff00ULL) << 40) |
+ ((in & 0x00000000000000ffULL) << 56);
+}
+
+static inline uint32_t i_bswap_32(uint32_t in)
+{
+ return ((in & 0xff000000) >> 24) |
+ ((in & 0x00ff0000) >> 8) |
+ ((in & 0x0000ff00) << 8) |
+ ((in & 0x000000ff) << 24);
+}
+
+static inline uint16_t i_bswap_16(uint16_t in)
+{
+ return ((in & 0xff00) >> 8) |
+ ((in & 0x00ff) << 8);
+}
+
+static inline uint8_t i_bswap_8(uint8_t in)
+{
+ return (in & 0xff);
+}
+
+/*
+ * unaligned big-endian integer
+ */
+static inline uint64_t be64_to_cpu_unaligned(const void *in)
+{
+ const uint8_t *p = (const uint8_t *) in;
+
+ return (((uint64_t) p[0] << 56) |
+ ((uint64_t) p[1] << 48) |
+ ((uint64_t) p[2] << 40) |
+ ((uint64_t) p[3] << 32) |
+ ((uint64_t) p[4] << 24) |
+ ((uint64_t) p[5] << 16) |
+ ((uint64_t) p[6] << 8) |
+ ((uint64_t) p[7]));
+}
+
+static inline void cpu64_to_be_unaligned(uint64_t in, void *out)
+{
+ uint8_t *p = (uint8_t *) out;
+
+ p[0] = (in >> 56) & 0xff;
+ p[1] = (in >> 48) & 0xff;
+ p[2] = (in >> 40) & 0xff;
+ p[3] = (in >> 32) & 0xff;
+ p[4] = (in >> 24) & 0xff;
+ p[5] = (in >> 16) & 0xff;
+ p[6] = (in >> 8) & 0xff;
+ p[7] = in & 0xff;
+}
+
+static inline uint32_t be32_to_cpu_unaligned(const void *in)
+{
+ const uint8_t *p = (const uint8_t *) in;
+
+ return (((uint32_t) p[0] << 24) |
+ ((uint32_t) p[1] << 16) |
+ ((uint32_t) p[2] << 8) |
+ ((uint32_t) p[3]));
+}
+
+static inline void cpu32_to_be_unaligned(uint32_t in, void *out)
+{
+ uint8_t *p = (uint8_t *) out;
+
+ p[0] = (in >> 24) & 0xff;
+ p[1] = (in >> 16) & 0xff;
+ p[2] = (in >> 8) & 0xff;
+ p[3] = in & 0xff;
+}
+
+static inline uint16_t be16_to_cpu_unaligned(const void *in)
+{
+ const uint8_t *p = (const uint8_t *) in;
+
+ return (((uint16_t) p[0] << 8) |
+ ((uint16_t) p[1]));
+}
+
+static inline void cpu16_to_be_unaligned(uint16_t in, void *out)
+{
+ uint8_t *p = (uint8_t *) out;
+
+ p[0] = (in >> 8) & 0xff;
+ p[1] = in & 0xff;
+}
+
+static inline uint8_t be8_to_cpu_unaligned(const void *in)
+{
+ return *((const uint8_t *) in);
+}
+
+static inline void cpu8_to_be_unaligned(uint8_t in, void *out)
+{
+ uint8_t *p = (uint8_t *) out;
+
+ *p = in;
+}
+
+/*
+ * unaligned little-endian & cpu-endian integers
+ */
+#define __GEN(size, bswap) \
+static inline uint##size##_t le##size##_to_cpu_unaligned(const void *in)\
+{ \
+ uint##size##_t x = be##size##_to_cpu_unaligned(in); \
+ /* we read a LE int as BE, so we always have to byte swap */ \
+ return i_bswap_##size(x); \
+} \
+static inline void cpu##size##_to_le_unaligned(uint##size##_t in, \
+ void *out) \
+{ \
+ /* we'll be writing in BE, so we always have to byte swap */ \
+ cpu##size##_to_be_unaligned(i_bswap_##size(in), out); \
+} \
+static inline uint##size##_t cpu##size##_to_cpu_unaligned(const void *in)\
+{ \
+ uint##size##_t x = be##size##_to_cpu_unaligned(in); \
+ return bswap; \
+}
+
+#ifdef WORDS_BIGENDIAN
+#define GEN(size) __GEN(size, x)
+#else
+#define GEN(size) __GEN(size, i_bswap_##size(x))
+#endif
+
+GEN(64)
+GEN(32)
+GEN(16)
+GEN(8)
+
+#undef __GEN
+#undef GEN
+
+/*
+ * byte ordering
+ */
+#define ___GEN(from, size, to, bswap) \
+static inline uint##size##_t from##size##_to_##to(uint##size##_t x) \
+{ \
+ return bswap; \
+}
+
+#ifdef WORDS_BIGENDIAN
+#define __GEN(from, size, to, be, le) ___GEN(from, size, to, be)
+#else
+#define __GEN(from, size, to, be, le) ___GEN(from, size, to, le)
+#endif
+
+#define GEN(size) \
+ __GEN(be, size, cpu, x, i_bswap_##size(x)) \
+ __GEN(cpu, size, be, x, i_bswap_##size(x)) \
+ __GEN(le, size, cpu, i_bswap_##size(x), x) \
+ __GEN(cpu, size, le, i_bswap_##size(x), x)
+
+GEN(64)
+GEN(32)
+GEN(16)
+GEN(8)
+
+#undef ___GEN
+#undef __GEN
+#undef GEN
+
+#endif
diff --git a/src/lib/child-wait.c b/src/lib/child-wait.c
new file mode 100644
index 0000000..024d81d
--- /dev/null
+++ b/src/lib/child-wait.c
@@ -0,0 +1,139 @@
+/* Copyright (c) 2007-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "lib-signals.h"
+#include "hash.h"
+#include "child-wait.h"
+
+#include <sys/wait.h>
+
+struct child_wait {
+ unsigned int pid_count;
+
+ child_wait_callback_t *callback;
+ void *context;
+};
+
+static int child_wait_refcount = 0;
+
+/* pid_t => wait */
+static HASH_TABLE(void *, struct child_wait *) child_pids;
+
+static void
+sigchld_handler(const siginfo_t *si ATTR_UNUSED, void *context ATTR_UNUSED);
+
+#undef child_wait_new_with_pid
+struct child_wait *
+child_wait_new_with_pid(pid_t pid, child_wait_callback_t *callback,
+ void *context)
+{
+ struct child_wait *wait;
+
+ wait = i_new(struct child_wait, 1);
+ wait->callback = callback;
+ wait->context = context;
+
+ if (pid != (pid_t)-1)
+ child_wait_add_pid(wait, pid);
+ return wait;
+}
+
+void child_wait_free(struct child_wait **_wait)
+{
+ struct child_wait *wait = *_wait;
+ struct hash_iterate_context *iter;
+ void *key;
+ struct child_wait *value;
+
+ *_wait = NULL;
+
+ if (wait->pid_count > 0) {
+ /* this should be rare, so iterating hash is fast enough */
+ iter = hash_table_iterate_init(child_pids);
+ while (hash_table_iterate(iter, child_pids, &key, &value)) {
+ if (value == wait) {
+ hash_table_remove(child_pids, key);
+ if (--wait->pid_count == 0)
+ break;
+ }
+ }
+ hash_table_iterate_deinit(&iter);
+ }
+
+ i_free(wait);
+}
+
+void child_wait_add_pid(struct child_wait *wait, pid_t pid)
+{
+ wait->pid_count++;
+ hash_table_insert(child_pids, POINTER_CAST(pid), wait);
+
+ lib_signals_set_expected(SIGCHLD, TRUE, sigchld_handler, NULL);
+}
+
+void child_wait_remove_pid(struct child_wait *wait, pid_t pid)
+{
+ wait->pid_count--;
+ hash_table_remove(child_pids, POINTER_CAST(pid));
+
+ if (hash_table_count(child_pids) == 0)
+ lib_signals_set_expected(SIGCHLD, FALSE, sigchld_handler, NULL);
+}
+
+static void
+sigchld_handler(const siginfo_t *si ATTR_UNUSED, void *context ATTR_UNUSED)
+{
+ struct child_wait_status status;
+
+ while ((status.pid = waitpid(-1, &status.status, WNOHANG)) > 0) {
+ status.wait = hash_table_lookup(child_pids,
+ POINTER_CAST(status.pid));
+ if (status.wait != NULL) {
+ child_wait_remove_pid(status.wait, status.pid);
+ status.wait->callback(&status, status.wait->context);
+ }
+ }
+
+ if (status.pid == -1 && errno != EINTR && errno != ECHILD)
+ i_error("waitpid() failed: %m");
+}
+
+void child_wait_switch_ioloop(void)
+{
+ lib_signals_switch_ioloop(SIGCHLD, sigchld_handler, NULL);
+}
+
+void child_wait_init(void)
+{
+ if (child_wait_refcount++ > 0) {
+ child_wait_switch_ioloop();
+ return;
+ }
+
+ hash_table_create_direct(&child_pids, default_pool, 0);
+
+ lib_signals_set_handler(SIGCHLD, LIBSIG_FLAGS_SAFE,
+ sigchld_handler, NULL);
+}
+
+void child_wait_deinit(void)
+{
+ struct hash_iterate_context *iter;
+ void *key;
+ struct child_wait *value;
+
+ i_assert(child_wait_refcount > 0);
+ if (--child_wait_refcount > 0) {
+ child_wait_switch_ioloop();
+ return;
+ }
+
+ lib_signals_unset_handler(SIGCHLD, sigchld_handler, NULL);
+
+ iter = hash_table_iterate_init(child_pids);
+ while (hash_table_iterate(iter, child_pids, &key, &value))
+ i_free(value);
+ hash_table_iterate_deinit(&iter);
+
+ hash_table_destroy(&child_pids);
+}
diff --git a/src/lib/child-wait.h b/src/lib/child-wait.h
new file mode 100644
index 0000000..c495638
--- /dev/null
+++ b/src/lib/child-wait.h
@@ -0,0 +1,34 @@
+#ifndef CHILD_WAIT_H
+#define CHILD_WAIT_H
+
+struct child_wait_status {
+ struct child_wait *wait;
+
+ pid_t pid;
+ int status;
+};
+
+typedef void child_wait_callback_t(const struct child_wait_status *status,
+ void *context);
+
+struct child_wait *
+child_wait_new_with_pid(pid_t pid, child_wait_callback_t *callback,
+ void *context) ATTR_NULL(3);
+#define child_wait_new_with_pid(pid, callback, context) \
+ child_wait_new_with_pid(pid - \
+ CALLBACK_TYPECHECK(callback, void (*)( \
+ const struct child_wait_status *status, typeof(context))), \
+ (child_wait_callback_t *)callback, context)
+#define child_wait_new(callback, context) \
+ child_wait_new_with_pid((pid_t)-1, callback, context)
+void child_wait_free(struct child_wait **wait);
+
+void child_wait_add_pid(struct child_wait *wait, pid_t pid);
+void child_wait_remove_pid(struct child_wait *wait, pid_t pid);
+
+void child_wait_switch_ioloop(void);
+
+void child_wait_init(void);
+void child_wait_deinit(void);
+
+#endif
diff --git a/src/lib/compat.c b/src/lib/compat.c
new file mode 100644
index 0000000..524ef37
--- /dev/null
+++ b/src/lib/compat.c
@@ -0,0 +1,268 @@
+/* Copyright (c) 2002-2018 Dovecot authors, see the included COPYING file */
+
+#include "config.h"
+#undef HAVE_CONFIG_H
+
+/* Linux needs the _XOPEN_SOURCE define, but others don't. It needs to be
+ defined before unistd.h, so we need the above config.h include hack.. */
+#ifdef PREAD_WRAPPERS
+# define _XOPEN_SOURCE 500 /* Linux */
+#endif
+
+#define IN_COMPAT_C
+#include "lib.h"
+
+#include <stdio.h>
+#include <ctype.h>
+#include <unistd.h>
+#include <syslog.h>
+#include <time.h>
+#include <sys/time.h>
+
+#ifndef INADDR_NONE
+# define INADDR_NONE INADDR_BROADCAST
+#endif
+
+#if !defined (HAVE_STRCASECMP) && !defined (HAVE_STRICMP)
+int i_my_strcasecmp(const char *s1, const char *s2)
+{
+ while (*s1 != '\0' && i_toupper(*s1) == i_toupper(*s2)) {
+ s1++; s2++;
+ }
+
+ return i_toupper(*s1) - i_toupper(*s2);
+}
+
+int i_my_strncasecmp(const char *s1, const char *s2, size_t max_chars)
+{
+ while (max_chars > 1 && *s1 != '\0' &&
+ i_toupper(*s1) == i_toupper(*s2)) {
+ s1++; s2++; max_chars--;
+ }
+
+ return i_toupper(*s1) - i_toupper(*s2);
+}
+#endif
+
+#ifndef HAVE_INET_ATON
+int i_my_inet_aton(const char *cp, struct in_addr *inp)
+{
+ in_addr_t addr;
+
+ addr = inet_addr(cp);
+ if (addr == INADDR_NONE)
+ return 0;
+
+ inp->s_addr = addr;
+ return 1;
+}
+#endif
+
+#ifndef HAVE_VSYSLOG
+void i_my_vsyslog(int priority, const char *format, va_list args)
+{
+ T_BEGIN {
+ syslog(priority, "%s", t_strdup_vprintf(format, args));
+ } T_END;
+}
+#endif
+
+#ifndef HAVE_GETPAGESIZE
+int i_my_getpagesize(void)
+{
+#ifdef _SC_PAGESIZE
+ return sysconf(_SC_PAGESIZE);
+#else
+# ifdef __GNUC__
+# warning Guessing page size to be 4096
+# endif
+ return 4096;
+#endif
+}
+#endif
+
+#ifndef HAVE_WRITEV
+ssize_t i_my_writev(int fd, const struct iovec *iov, int iov_len)
+{
+ size_t written;
+ ssize_t ret;
+ int i;
+
+ written = 0;
+ for (i = 0; i < iov_len; i++, iov++) {
+ ret = write(fd, iov->iov_base, iov->iov_len);
+ if (ret < 0)
+ return -1;
+
+ written += ret;
+ if ((size_t)ret != iov->iov_len)
+ break;
+ }
+
+ if (written > SSIZE_T_MAX) {
+ errno = ERANGE;
+ return -1;
+ }
+
+ return (ssize_t)written;
+}
+#endif
+
+#if !defined(HAVE_PREAD) || defined(PREAD_BROKEN)
+ssize_t i_my_pread(int fd, void *buf, size_t count, off_t offset)
+{
+ ssize_t ret;
+ off_t old_offset;
+
+ old_offset = lseek(fd, 0, SEEK_CUR);
+ if (old_offset == -1)
+ return -1;
+
+ if (lseek(fd, offset, SEEK_SET) < 0)
+ return -1;
+
+ ret = read(fd, buf, count);
+ if (ret < 0)
+ return -1;
+
+ if (lseek(fd, old_offset, SEEK_SET) < 0)
+ return -1;
+ return ret;
+}
+
+ssize_t i_my_pwrite(int fd, const void *buf, size_t count, off_t offset)
+{
+ ssize_t ret;
+ off_t old_offset;
+
+ old_offset = lseek(fd, 0, SEEK_CUR);
+ if (old_offset == -1)
+ return -1;
+
+ if (lseek(fd, offset, SEEK_SET) < 0)
+ return -1;
+
+ ret = write(fd, buf, count);
+ if (ret < 0)
+ return -1;
+
+ if (lseek(fd, old_offset, SEEK_SET) < 0)
+ return -1;
+ return ret;
+}
+#elif defined(PREAD_WRAPPERS)
+
+ssize_t i_my_pread(int fd, void *buf, size_t count, off_t offset)
+{
+ ssize_t ret;
+
+ ret = pread(fd, buf, count, offset);
+ return ret;
+}
+
+ssize_t i_my_pwrite(int fd, const void *buf, size_t count, off_t offset)
+{
+ return pwrite(fd, buf, count, offset);
+}
+#endif
+
+#ifndef HAVE_SETEUID
+int i_my_seteuid(uid_t euid)
+{
+#ifdef HAVE_SETREUID
+ /* HP-UX at least doesn't have seteuid() but has setreuid() */
+ return setreuid(-1, euid);
+#else
+# error Missing seteuid functionality
+#endif
+}
+#endif
+
+#ifndef HAVE_SETEGID
+int i_my_setegid(gid_t egid)
+{
+#ifdef HAVE_SETRESGID
+ /* HP-UX at least doesn't have setegid() but has setresgid() */
+ return setresgid(-1, egid, -1);
+#else
+# error Missing setegid functionality
+#endif
+}
+#endif
+
+#ifndef HAVE_LIBGEN_H
+char *i_my_basename(char *path)
+{
+ char *p;
+
+ /* note that this isn't POSIX-compliant basename() replacement.
+ too much trouble without any gain. */
+ p = strrchr(path, '/');
+ return p == NULL ? path : p + 1;
+}
+#endif
+
+#ifdef HAVE_OLD_VSNPRINTF
+#undef vsnprintf
+int i_my_vsnprintf(char *str, size_t size, const char *format, va_list ap)
+{
+ size_t tmp_size;
+ char *tmp;
+ int ret;
+
+ /* On overflow HP-UX returns -1, IRIX and Tru64 return size-1. */
+ ret = vsnprintf(str, size, format, ap);
+ if (ret >= 0 && (size_t)ret+1 != size)
+ return ret;
+
+ /* see if data stack has enough available space for it */
+ tmp_size = t_get_bytes_available();
+ if (tmp_size > size) {
+ tmp = t_buffer_get(tmp_size);
+ ret = vsnprintf(tmp, tmp_size, format, ap);
+ if (ret >= 0 && (size_t)ret+1 != tmp_size) {
+ if (size > 0) {
+ memcpy(str, tmp, size-1);
+ str[size-1] = '\0';
+ }
+ return ret;
+ }
+ } else {
+ tmp_size = size;
+ }
+
+ /* try to allocate enough memory to get it to fit. */
+ do {
+ tmp_size = nearest_power(tmp_size+1);
+ tmp = i_malloc(tmp_size);
+ ret = vsnprintf(tmp, tmp_size, format, ap);
+ if (ret >= 0 && (size_t)ret+1 != tmp_size) {
+ if (size > 0) {
+ memcpy(str, tmp, size-1);
+ str[size-1] = '\0';
+ }
+ i_free(tmp);
+ return ret;
+ }
+ i_free(tmp);
+ } while (tmp_size < 1024*1024);
+
+ i_panic("my_vsnprintf(): Output string too big");
+}
+#endif
+
+#ifndef HAVE_CLOCK_GETTIME
+int i_my_clock_gettime(int clk_id, struct timespec *tp)
+{
+ struct timeval tv;
+
+ i_assert(clk_id == CLOCK_REALTIME);
+
+ /* fallback to using microseconds */
+ if (gettimeofday(&tv, NULL) < 0)
+ return -1;
+ tp->tv_sec = tv.tv_sec;
+ tp->tv_nsec = tv.tv_usec * 1000;
+ return 0;
+}
+#endif
diff --git a/src/lib/compat.h b/src/lib/compat.h
new file mode 100644
index 0000000..ce3127d
--- /dev/null
+++ b/src/lib/compat.h
@@ -0,0 +1,323 @@
+#ifndef COMPAT_H
+#define COMPAT_H
+
+/* _ILP32 and _LP64 are common but not universal, make sure that exactly one
+ of them is defined. */
+#if !defined(_ILP32) && \
+ (SIZEOF_INT == 4) && (SIZEOF_LONG == 4) && (SIZEOF_VOID_P == 4)
+# define _ILP32
+#endif
+#if !defined(_LP64) && \
+ (SIZEOF_INT == 4) && (SIZEOF_LONG == 8) && (SIZEOF_VOID_P == 8)
+# define _LP64
+#endif
+#if defined(_ILP32) && defined(_LP64)
+# error "Cannot have both _ILP32 and _LP64 defined"
+#elif !defined(_ILP32) && !defined(_LP64)
+# error "Must have one of _ILP32 and _LP64 defined"
+#endif
+
+/* well, this is obviously wrong since it assumes it's 64bit, but older
+ GCCs don't define it and we really want it. */
+#ifndef LLONG_MAX
+# define LLONG_MAX 9223372036854775807LL
+#endif
+
+#if ((__GNUC__ > 3 || (__GNUC__ == 3 && __GNUC_MINOR__ > 3)) && \
+ defined(HAVE_TYPEOF)) && !defined(__cplusplus)
+# define HAVE_TYPE_CHECKS
+#endif
+
+/* We really want NULL to be a pointer, since we have various type-checks
+ that may result in compiler warnings/errors if it's not. Do this only when
+ type checking is used - it's not otherwise needed and causes compiling
+ problems with e.g. Sun C compiler. */
+#ifdef HAVE_TYPE_CHECKS
+# undef NULL
+# define NULL ((void *)0)
+#endif
+
+#ifndef __has_extension
+ #define __has_extension(x) 0 // Compatibility with non-clang compilers.
+#endif
+
+#if (defined(__GNUC__) && __GNUC__ > 4 || (__GNUC__ == 4 && __GNUC_MINOR__ >= 5)) || \
+ (defined(__clang__) && (__has_extension(attribute_deprecated_with_message)))
+# define HAVE_ATTR_DEPRECATED
+int rand(void) __attribute__((deprecated("Do not use rand, use i_rand")));
+int rand_r(unsigned int*) __attribute__((deprecated("Do not use rand_r, use i_rand")));
+#endif
+
+#ifndef __cplusplus
+#ifdef HAVE__BOOL
+typedef _Bool bool;
+#else
+typedef int bool;
+#endif
+#endif
+
+#if defined (HAVE_UOFF_T)
+/* native support */
+#elif defined (UOFF_T_INT)
+typedef unsigned int uoff_t;
+#elif defined (UOFF_T_LONG)
+typedef unsigned long uoff_t;
+#elif defined (UOFF_T_LONG_LONG)
+typedef unsigned long long uoff_t;
+#else
+# error uoff_t size not set
+#endif
+
+#ifndef HAVE_UINTMAX_T
+# if SIZEOF_LONG_LONG > 0
+typedef unsigned long long uintmax_t;
+# else
+typedef unsigned long uintmax_t;
+# endif
+#endif
+
+#ifndef HAVE_UINT_FAST32_T
+# if SIZEOF_INT >= 4
+typedef unsigned int uint_fast32_t;
+# else
+typedef unsigned long uint_fast32_t;
+# endif
+#endif
+
+#ifndef HAVE_SOCKLEN_T
+typedef int socklen_t;
+#endif
+
+/* WORDS_BIGENDIAN needs to be undefined if not enabled */
+#if defined(WORDS_BIGENDIAN) && WORDS_BIGENDIAN == 0
+# undef WORDS_BIGENDIAN
+#endif
+
+#ifdef HAVE_SYS_SYSMACROS_H
+# include <sys/sysmacros.h>
+# ifdef HAVE_SYS_MKDEV_H
+# include <sys/mkdev.h> /* UnixWare */
+# endif
+# define CMP_DEV_T(a, b) (major(a) == major(b) && minor(a) == minor(b))
+#elif !defined (DEV_T_STRUCT)
+# define CMP_DEV_T(a, b) ((a) == (b))
+#else
+# error I do not know how to compare dev_t
+#endif
+
+#ifdef HAVE_STAT_XTIM
+# define HAVE_ST_NSECS
+# define ST_ATIME_NSEC(st) ((unsigned long)(st).st_atim.tv_nsec)
+# define ST_MTIME_NSEC(st) ((unsigned long)(st).st_mtim.tv_nsec)
+# define ST_CTIME_NSEC(st) ((unsigned long)(st).st_ctim.tv_nsec)
+#elif defined (HAVE_STAT_XTIMESPEC)
+# define HAVE_ST_NSECS
+# define ST_ATIME_NSEC(st) ((unsigned long)(st).st_atimespec.tv_nsec)
+# define ST_MTIME_NSEC(st) ((unsigned long)(st).st_mtimespec.tv_nsec)
+# define ST_CTIME_NSEC(st) ((unsigned long)(st).st_ctimespec.tv_nsec)
+#else
+# define ST_ATIME_NSEC(st) 0UL
+# define ST_MTIME_NSEC(st) 0UL
+# define ST_CTIME_NSEC(st) 0UL
+#endif
+
+#ifdef HAVE_ST_NSECS
+/* TRUE if a nanosecond timestamp from struct stat matches another nanosecond.
+ If nanoseconds aren't supported in struct stat, returns always TRUE (useful
+ with NFS if some hosts support nanoseconds and others don't). */
+# define ST_NTIMES_EQUAL(ns1, ns2) ((ns1) == (ns2))
+#else
+# define ST_NTIMES_EQUAL(ns1, ns2) TRUE
+#endif
+
+#define CMP_ST_MTIME(st1, st2) \
+ ((st1)->st_mtime == (st2)->st_mtime && \
+ ST_NTIMES_EQUAL(ST_MTIME_NSEC(*(st1)), ST_MTIME_NSEC(*(st2))))
+#define CMP_ST_CTIME(st1, st2) \
+ ((st1)->st_ctime == (st2)->st_ctime && \
+ ST_NTIMES_EQUAL(ST_CTIME_NSEC(*(st1)), ST_CTIME_NSEC(*(st2))))
+
+/* strcasecmp(), strncasecmp() */
+#ifndef HAVE_STRCASECMP
+# ifdef HAVE_STRICMP
+# define strcasecmp stricmp
+# define strncasecmp strnicmp
+# else
+# define strcasecmp i_my_strcasecmp
+# define strncasecmp i_my_strncasecmp
+int i_my_strcasecmp(const char *s1, const char *s2);
+int i_my_strncasecmp(const char *s1, const char *s2, size_t max_chars);
+# endif
+#endif
+
+#ifndef HAVE_INET_ATON
+# include <sys/socket.h>
+# include <netinet/in.h>
+# include <arpa/inet.h>
+# define inet_aton i_my_inet_aton
+int i_my_inet_aton(const char *cp, struct in_addr *inp);
+#endif
+
+#ifndef HAVE_VSYSLOG
+# define vsyslog i_my_vsyslog
+void i_my_vsyslog(int priority, const char *format, va_list args);
+#endif
+
+#ifndef HAVE_GETPAGESIZE
+# define getpagesize i_my_getpagesize
+int i_my_getpagesize(void);
+#endif
+
+#ifndef HAVE_FDATASYNC
+# define fdatasync fsync
+#endif
+
+struct const_iovec {
+ const void *iov_base;
+ size_t iov_len;
+};
+
+#ifndef HAVE_STRUCT_IOVEC
+struct iovec {
+ void *iov_base;
+ size_t iov_len;
+};
+#endif
+
+/* IOV_MAX should be in limits.h nowadays. Linux still (2005) requires
+ defining _XOPEN_SOURCE to get that value. UIO_MAXIOV works with it though,
+ so use it instead. 16 is the lowest acceptable value for all OSes. */
+#ifndef IOV_MAX
+# include <sys/uio.h>
+# ifdef UIO_MAXIOV
+# define IOV_MAX UIO_MAXIOV
+# else
+# define IOV_MAX 16
+# endif
+#endif
+
+#ifndef HAVE_WRITEV
+# define writev i_my_writev
+struct iovec;
+ssize_t i_my_writev(int fd, const struct iovec *iov, int iov_len);
+#endif
+
+#if !defined(HAVE_PREAD) || defined(PREAD_WRAPPERS) || defined(PREAD_BROKEN)
+# ifndef IN_COMPAT_C
+# define pread i_my_pread
+# define pwrite i_my_pwrite
+# endif
+ssize_t i_my_pread(int fd, void *buf, size_t count, off_t offset);
+ssize_t i_my_pwrite(int fd, const void *buf, size_t count, off_t offset);
+#endif
+
+#ifndef HAVE_SETEUID
+# define seteuid i_my_seteuid
+int i_my_seteuid(uid_t euid);
+#endif
+
+#ifndef HAVE_SETEGID
+# define setegid i_my_setegid
+int i_my_setegid(gid_t egid);
+#endif
+
+#ifndef HAVE_LIBGEN_H
+# define basename i_my_basename
+char *i_my_basename(char *path);
+#endif
+
+#ifdef HAVE_OLD_VSNPRINTF
+# include <stdio.h>
+# define vsnprintf i_my_vsnprintf
+int i_my_vsnprintf(char *str, size_t size, const char *format, va_list ap);
+#endif
+
+#ifndef HAVE_CLOCK_GETTIME
+# include <time.h>
+# undef CLOCK_REALTIME
+# define CLOCK_REALTIME 1
+# define clock_gettime i_my_clock_gettime
+int i_my_clock_gettime(int clk_id, struct timespec *tp);
+#endif
+
+/* ctype.h isn't safe with signed chars,
+ use our own instead if really needed */
+#define i_toupper(x) ((char) toupper((int) (unsigned char) (x)))
+#define i_tolower(x) ((char) tolower((int) (unsigned char) (x)))
+#define i_isalnum(x) (isalnum((int) (unsigned char) (x)) != 0)
+#define i_isalpha(x) (isalpha((int) (unsigned char) (x)) != 0)
+#define i_isascii(x) (isascii((int) (unsigned char) (x)) != 0)
+#define i_isblank(x) (isblank((int) (unsigned char) (x)) != 0)
+#define i_iscntrl(x) (iscntrl((int) (unsigned char) (x)) != 0)
+#define i_isdigit(x) (isdigit((int) (unsigned char) (x)) != 0)
+#define i_isgraph(x) (isgraph((int) (unsigned char) (x)) != 0)
+#define i_islower(x) (islower((int) (unsigned char) (x)) != 0)
+#define i_isprint(x) (isprint((int) (unsigned char) (x)) != 0)
+#define i_ispunct(x) (ispunct((int) (unsigned char) (x)) != 0)
+#define i_isspace(x) (isspace((int) (unsigned char) (x)) != 0)
+#define i_isupper(x) (isupper((int) (unsigned char) (x)) != 0)
+#define i_isxdigit(x) (isxdigit((int) (unsigned char) (x)) != 0)
+
+#ifndef EOVERFLOW
+# define EOVERFLOW ERANGE
+#endif
+
+#ifdef EDQUOT
+# define ENOSPACE(errno) ((errno) == ENOSPC || (errno) == EDQUOT)
+# define ENOQUOTA(errno) ((errno) == EDQUOT)
+#else
+/* probably all modern OSes have EDQUOT, but just in case one doesn't assume
+ that ENOSPC is the same as "over quota". */
+# define ENOSPACE(errno) ((errno) == ENOSPC)
+# define ENOQUOTA(errno) ((errno) == ENOSPC)
+#endif
+
+/* EPERM is returned sometimes if device doesn't support such modification */
+#ifdef EROFS
+# define ENOACCESS(errno) \
+ ((errno) == EACCES || (errno) == EROFS || (errno) == EPERM)
+#else
+# define ENOACCESS(errno) ((errno) == EACCES || (errno) == EPERM)
+#endif
+
+#define ENOTFOUND(errno) \
+ ((errno) == ENOENT || (errno) == ENOTDIR || \
+ (errno) == ELOOP || (errno) == ENAMETOOLONG)
+
+#define ECANTLINK(errno) \
+ ((errno) == EXDEV || (errno) == EMLINK || (errno) == EPERM)
+
+/* Returns TRUE if unlink() failed because it attempted to delete a directory */
+#define UNLINK_EISDIR(errno) \
+ ((errno) == EPERM || /* POSIX */ \
+ (errno) == EISDIR) /* Linux */
+
+/* EBUSY is given by some NFS implementations */
+#define EDESTDIREXISTS(errno) \
+ ((errno) == EEXIST || (errno) == ENOTEMPTY || (errno) == EBUSY)
+
+/* fstat() returns ENOENT instead of ESTALE with some Linux versions */
+#define ESTALE_FSTAT(errno) \
+ ((errno) == ESTALE || (errno) == ENOENT)
+
+#if !defined(_POSIX_SYNCHRONIZED_IO) && \
+ defined(__ENVIRONMENT_MAC_OS_X_VERSION_MIN_REQUIRED__) && \
+ (__ENVIRONMENT_MAC_OS_X_VERSION_MIN_REQUIRED__ >= 1060)
+/* OS X Snow Leopard has fdatasync(), but no prototype for it. */
+int fdatasync(int);
+#endif
+
+/* Try to keep IO operations at least this size */
+#ifndef IO_BLOCK_SIZE
+# define IO_BLOCK_SIZE 8192
+#endif
+/* Default size for data blocks transferred over the network */
+#ifndef NET_BLOCK_SIZE
+# define NET_BLOCK_SIZE (128*1024)
+#endif
+
+#if !defined(PIPE_BUF) && defined(_POSIX_PIPE_BUF)
+# define PIPE_BUF (8 * _POSIX_PIPE_BUF) /* for HURD */
+#endif
+
+#endif
diff --git a/src/lib/connection.c b/src/lib/connection.c
new file mode 100644
index 0000000..dae5c92
--- /dev/null
+++ b/src/lib/connection.c
@@ -0,0 +1,952 @@
+/* Copyright (c) 2013-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "str.h"
+#include "ioloop.h"
+#include "istream.h"
+#include "istream-unix.h"
+#include "ostream.h"
+#include "ostream-unix.h"
+#include "iostream.h"
+#include "net.h"
+#include "strescape.h"
+#include "llist.h"
+#include "time-util.h"
+#include "connection.h"
+
+#include <unistd.h>
+#include <libgen.h>
+
+static void connection_handshake_ready(struct connection *conn)
+{
+ conn->handshake_received = TRUE;
+ if (conn->v.handshake_ready != NULL)
+ conn->v.handshake_ready(conn);
+}
+
+static void connection_closed(struct connection *conn,
+ enum connection_disconnect_reason reason)
+{
+ conn->disconnect_reason = reason;
+ conn->v.destroy(conn);
+}
+
+static void connection_idle_timeout(struct connection *conn)
+{
+ connection_closed(conn, CONNECTION_DISCONNECT_IDLE_TIMEOUT);
+}
+
+static void connection_connect_timeout(struct connection *conn)
+{
+ connection_closed(conn, CONNECTION_DISCONNECT_CONNECT_TIMEOUT);
+}
+
+void connection_input_default(struct connection *conn)
+{
+ const char *line;
+ struct istream *input;
+ struct ostream *output;
+ int ret = 0;
+
+ if (!conn->handshake_received &&
+ conn->v.handshake != NULL) {
+ if ((ret = conn->v.handshake(conn)) < 0) {
+ connection_closed(
+ conn, CONNECTION_DISCONNECT_HANDSHAKE_FAILED);
+ return;
+ } else if (ret == 0) {
+ return;
+ } else {
+ connection_handshake_ready(conn);
+ }
+ }
+
+ switch (connection_input_read(conn)) {
+ case -1:
+ return;
+ case 0: /* allow calling this function for buffered input */
+ case 1:
+ break;
+ default:
+ i_unreached();
+ }
+
+ input = conn->input;
+ output = conn->output;
+ i_stream_ref(input);
+ if (output != NULL) {
+ o_stream_ref(output);
+ o_stream_cork(output);
+ }
+ while (!input->closed && (line = i_stream_next_line(input)) != NULL) {
+ T_BEGIN {
+ if (!conn->handshake_received &&
+ conn->v.handshake_line != NULL) {
+ ret = conn->v.handshake_line(conn, line);
+ if (ret > 0)
+ connection_handshake_ready(conn);
+ else if (ret == 0)
+ /* continue reading */
+ ret = 1;
+ else
+ conn->disconnect_reason =
+ CONNECTION_DISCONNECT_HANDSHAKE_FAILED;
+ } else {
+ ret = conn->v.input_line(conn, line);
+ }
+ } T_END;
+ if (ret <= 0)
+ break;
+ }
+ if (output != NULL) {
+ o_stream_uncork(output);
+ o_stream_unref(&output);
+ }
+ if (ret < 0 && !input->closed) {
+ enum connection_disconnect_reason reason =
+ conn->disconnect_reason;
+ if (reason == CONNECTION_DISCONNECT_NOT)
+ reason = CONNECTION_DISCONNECT_DEINIT;
+ connection_closed(conn, reason);
+ }
+ i_stream_unref(&input);
+}
+
+int connection_verify_version(struct connection *conn,
+ const char *service_name,
+ unsigned int major_version,
+ unsigned int minor_version)
+{
+ i_assert(!conn->version_received);
+
+ if (strcmp(service_name, conn->list->set.service_name_in) != 0) {
+ e_error(conn->event, "Connected to wrong socket type. "
+ "We want '%s', but received '%s'",
+ conn->list->set.service_name_in, service_name);
+ return -1;
+ }
+
+ if (major_version != conn->list->set.major_version) {
+ e_error(conn->event, "Socket supports major version %u, "
+ "but we support only %u (mixed old and new binaries?)",
+ major_version, conn->list->set.major_version);
+ return -1;
+ }
+
+ conn->minor_version = minor_version;
+ conn->version_received = TRUE;
+ return 0;
+}
+
+int connection_handshake_args_default(struct connection *conn,
+ const char *const *args)
+{
+ unsigned int major_version, minor_version;
+
+ if (conn->version_received)
+ return 1;
+
+ /* VERSION <tab> service_name <tab> major version <tab> minor version */
+ if (str_array_length(args) != 4 ||
+ strcmp(args[0], "VERSION") != 0 ||
+ str_to_uint(args[2], &major_version) < 0 ||
+ str_to_uint(args[3], &minor_version) < 0) {
+ e_error(conn->event, "didn't reply with a valid VERSION line: %s",
+ t_strarray_join(args, "\t"));
+ return -1;
+ }
+
+ if (connection_verify_version(conn, args[1],
+ major_version, minor_version) < 0)
+ return -1;
+ return 1;
+}
+
+int connection_input_line_default(struct connection *conn, const char *line)
+{
+ const char *const *args;
+
+ args = t_strsplit_tabescaped(line);
+ if (args[0] == NULL && !conn->list->set.allow_empty_args_input) {
+ e_error(conn->event, "Unexpectedly received empty line");
+ return -1;
+ }
+
+ if (!conn->handshake_received &&
+ (conn->v.handshake_args != connection_handshake_args_default ||
+ conn->list->set.major_version != 0)) {
+ int ret;
+ if ((ret = conn->v.handshake_args(conn, args)) == 0)
+ ret = 1; /* continue reading */
+ else if (ret > 0)
+ connection_handshake_ready(conn);
+ else {
+ conn->disconnect_reason =
+ CONNECTION_DISCONNECT_HANDSHAKE_FAILED;
+ }
+ return ret;
+ } else if (!conn->handshake_received) {
+ /* we don't do handshakes */
+ connection_handshake_ready(conn);
+ }
+
+ /* version must be handled though, by something */
+ i_assert(conn->version_received);
+
+ return conn->v.input_args(conn, args);
+}
+
+void connection_input_halt(struct connection *conn)
+{
+ io_remove(&conn->io);
+ timeout_remove(&conn->to);
+}
+
+static void
+connection_input_resume_full(struct connection *conn, bool set_io_pending)
+{
+ i_assert(!conn->disconnected);
+
+ if (conn->io != NULL) {
+ /* do nothing */
+ } else if (conn->input != NULL) {
+ conn->io = io_add_istream_to(conn->ioloop, conn->input,
+ *conn->v.input, conn);
+ if (set_io_pending)
+ io_set_pending(conn->io);
+ } else if (conn->fd_in != -1) {
+ conn->io = io_add_to(conn->ioloop, conn->fd_in, IO_READ,
+ *conn->v.input, conn);
+ if (set_io_pending)
+ io_set_pending(conn->io);
+ }
+ if (conn->input_idle_timeout_secs != 0 && conn->to == NULL) {
+ conn->to = timeout_add_to(conn->ioloop,
+ conn->input_idle_timeout_secs*1000,
+ *conn->v.idle_timeout, conn);
+ }
+}
+
+void connection_input_resume(struct connection *conn)
+{
+ connection_input_resume_full(conn, TRUE);
+}
+
+static void connection_update_property_label(struct connection *conn)
+{
+ const char *label;
+
+ if (conn->remote_ip.family == 0) {
+ if (conn->remote_uid == (uid_t)-1)
+ label = NULL;
+ else if (conn->remote_pid != (pid_t)-1) {
+ label = t_strdup_printf("pid=%ld,uid=%ld",
+ (long)conn->remote_pid,
+ (long)conn->remote_uid);
+ } else {
+ label = t_strdup_printf("uid=%ld",
+ (long)conn->remote_uid);
+ }
+ } else if (conn->remote_ip.family == AF_INET6) {
+ label = t_strdup_printf("[%s]:%u",
+ net_ip2addr(&conn->remote_ip),
+ conn->remote_port);
+ } else {
+ label = t_strdup_printf("%s:%u",
+ net_ip2addr(&conn->remote_ip),
+ conn->remote_port);
+ }
+
+ i_assert(label != NULL || conn->property_label == NULL);
+ if (conn->property_label != NULL &&
+ strcmp(conn->property_label, label) != 0) {
+ e_debug(conn->event, "Updated peer address to %s", label);
+ }
+
+ i_free(conn->property_label);
+ conn->property_label = i_strdup(label);
+}
+
+static void connection_update_label(struct connection *conn)
+{
+ bool unix_socket = conn->unix_socket ||
+ (conn->remote_ip.family == 0 && conn->remote_uid != (uid_t)-1);
+ string_t *label;
+
+ label = t_str_new(64);
+ if (conn->base_name != NULL)
+ str_append(label, conn->base_name);
+ if (conn->property_label != NULL) {
+ if (str_len(label) == 0)
+ str_append(label, conn->property_label);
+ else {
+ str_append(label, " (");
+ str_append(label, conn->property_label);
+ str_append(label, ")");
+ }
+ }
+ if (str_len(label) == 0) {
+ if (conn->fd_in >= 0 &&
+ (conn->fd_in == conn->fd_out || conn->fd_out < 0))
+ str_printfa(label, "fd=%d", conn->fd_in);
+ else if (conn->fd_in < 0 && conn->fd_out >= 0)
+ str_printfa(label, "fd=%d", conn->fd_out);
+ else if (conn->fd_in >= 0 && conn->fd_out >= 0) {
+ str_printfa(label, "fd_in=%d,fd_out=%d",
+ conn->fd_in, conn->fd_out);
+ }
+ }
+ if (unix_socket && str_len(label) > 0)
+ str_insert(label, 0, "unix:");
+ if (conn->list->set.log_connection_id) {
+ if (str_len(label) > 0)
+ str_append_c(label, ' ');
+ str_printfa(label, "[%u]", conn->id);
+ }
+
+ i_free(conn->label);
+ conn->label = i_strdup(str_c(label));
+}
+
+static const char *
+connection_create_stream_name(struct connection *conn, int fd)
+{
+ string_t *name;
+
+ name = t_str_new(64);
+ str_append(name, "(conn");
+ if (conn->unix_socket ||
+ (conn->remote_ip.family == 0 && conn->remote_uid != (uid_t)-1))
+ str_append(name, ":unix");
+ if (conn->base_name != NULL) {
+ str_append_c(name, ':');
+ str_append(name, conn->base_name);
+ } else if (conn->property_label != NULL) {
+ str_append_c(name, ':');
+ str_append(name, conn->property_label);
+ } else if (fd >= 0) {
+ str_printfa(name, ":fd=%d", fd);
+ }
+ if (conn->list->set.log_connection_id) {
+ if (str_len(name) == 5)
+ str_append_c(name, ':');
+ else
+ str_append_c(name, ',');
+ str_printfa(name, "id=%u", conn->id);
+ }
+ str_append_c(name, ')');
+
+ return str_c(name);
+}
+
+static void connection_update_stream_names(struct connection *conn)
+{
+ if (conn->input != NULL) {
+ i_stream_set_name(
+ conn->input,
+ connection_create_stream_name(conn, conn->fd_in));
+ }
+ if (conn->output != NULL) {
+ o_stream_set_name(
+ conn->output,
+ connection_create_stream_name(conn, conn->fd_out));
+ }
+}
+
+void connection_update_event(struct connection *conn)
+{
+ string_t *prefix;
+
+ prefix = t_str_new(64);
+ str_append(prefix, "conn");
+ if (strlen(conn->label) > 0) {
+ str_append_c(prefix, ' ');
+ str_append(prefix, conn->label);
+ }
+ str_append(prefix, ": ");
+ event_set_append_log_prefix(conn->event, str_c(prefix));
+
+ if (conn->local_ip.family > 0) {
+ event_add_str(conn->event, conn->list->set.client ?
+ "source_ip" : "local_ip",
+ net_ip2addr(&conn->local_ip));
+ }
+
+ if (conn->remote_ip.family > 0) {
+ event_add_str(conn->event, conn->list->set.client ?
+ "dest_ip" : "remote_ip",
+ net_ip2addr(&conn->remote_ip));
+ }
+ if (conn->remote_port > 0) {
+ event_add_int(conn->event, conn->list->set.client ?
+ "dest_port" : "remote_port",
+ conn->remote_port);
+ }
+
+ if (conn->remote_pid != (pid_t)-1)
+ event_add_int(conn->event, "remote_pid", conn->remote_pid);
+ if (conn->remote_uid != (uid_t)-1)
+ event_add_int(conn->event, "remote_uid", conn->remote_uid);
+}
+
+void connection_update_properties(struct connection *conn)
+{
+ int fd = (conn->fd_in < 0 ? conn->fd_out : conn->fd_in);
+ struct net_unix_cred cred;
+
+ if (conn->remote_ip.family != 0) {
+ /* remote IP was already set */
+ } else if (conn->unix_peer_checked) {
+ /* already checked */
+ } else if (fd < 0) {
+ /* not connected yet - wait */
+ } else {
+ if (net_getpeername(fd, &conn->remote_ip,
+ &conn->remote_port) == 0) {
+ /* either TCP or UNIX socket connection */
+ errno = 0;
+ }
+
+ if (conn->remote_ip.family != 0) {
+ /* TCP connection */
+ i_assert(conn->remote_port != 0);
+ } else if (errno == ENOTSOCK) {
+ /* getpeername() already found out this can't be a UNIX
+ socket connection */
+ } else if (net_getunixcred(fd, &cred) == 0) {
+ conn->remote_pid = cred.pid;
+ conn->remote_uid = cred.uid;
+ }
+ conn->unix_peer_checked = TRUE;
+ }
+
+ connection_update_property_label(conn);
+ connection_update_label(conn);
+ connection_update_stream_names(conn);
+ connection_update_event(conn);
+
+ conn->name = (conn->base_name != NULL ?
+ conn->base_name : conn->property_label);
+}
+
+static void connection_init_streams(struct connection *conn)
+{
+ const struct connection_settings *set = &conn->list->set;
+
+ i_assert(conn->io == NULL);
+ i_assert(conn->input == NULL);
+ i_assert(conn->output == NULL);
+ i_assert(conn->to == NULL);
+
+ conn->handshake_received = FALSE;
+ conn->version_received = set->major_version == 0;
+
+ if (set->input_max_size != 0) {
+ if (conn->unix_socket)
+ conn->input = i_stream_create_unix(conn->fd_in,
+ set->input_max_size);
+ else
+ conn->input = i_stream_create_fd(conn->fd_in,
+ set->input_max_size);
+ i_stream_switch_ioloop_to(conn->input, conn->ioloop);
+ }
+ if (set->output_max_size != 0) {
+ if (conn->unix_socket)
+ conn->output = o_stream_create_unix(conn->fd_out,
+ set->output_max_size);
+ else
+ conn->output = o_stream_create_fd(conn->fd_out,
+ set->output_max_size);
+ o_stream_set_no_error_handling(conn->output, TRUE);
+ o_stream_set_finish_via_child(conn->output, FALSE);
+ o_stream_switch_ioloop_to(conn->output, conn->ioloop);
+ }
+ connection_update_stream_names(conn);
+
+ conn->disconnected = FALSE;
+ i_assert(conn->to == NULL);
+ connection_input_resume_full(conn, FALSE);
+ i_assert(conn->to != NULL || conn->input_idle_timeout_secs == 0);
+ if (set->major_version != 0 && !set->dont_send_version) {
+ e_debug(conn->event, "Sending version handshake");
+ o_stream_nsend_str(conn->output, t_strdup_printf(
+ "VERSION\t%s\t%u\t%u\n", set->service_name_out,
+ set->major_version, set->minor_version));
+ }
+}
+
+void connection_streams_changed(struct connection *conn)
+{
+ const struct connection_settings *set = &conn->list->set;
+
+ if (set->input_max_size != 0 && conn->io != NULL) {
+ connection_input_halt(conn);
+ connection_input_resume(conn);
+ }
+}
+
+static void connection_client_connected(struct connection *conn, bool success)
+{
+ i_assert(conn->list->set.client);
+
+ connection_update_properties(conn);
+
+ conn->connect_finished = ioloop_timeval;
+
+ struct event_passthrough *e = event_create_passthrough(conn->event)->
+ set_name("server_connection_connected");
+ if (success) {
+ e_debug(e->event(), "Client connected (fd=%d)",
+ conn->fd_in);
+ } else {
+ e_debug(e->event(), "Client connection failed (fd=%d)",
+ conn->fd_in);
+ }
+
+ if (success)
+ connection_init_streams(conn);
+ if (conn->v.client_connected != NULL)
+ conn->v.client_connected(conn, success);
+ if (!success) {
+ connection_closed(conn, CONNECTION_DISCONNECT_CONN_CLOSED);
+ }
+}
+
+static void
+connection_init_full(struct connection_list *list, struct connection *conn,
+ const char *name, int fd_in, int fd_out)
+{
+ if (conn->id == 0) {
+ if (list->id_counter == 0)
+ list->id_counter++;
+ conn->id = list->id_counter++;
+ }
+
+ conn->ioloop = current_ioloop;
+ conn->fd_in = fd_in;
+ conn->fd_out = fd_out;
+ conn->disconnected = TRUE;
+ conn->remote_uid = (uid_t)-1;
+ conn->remote_pid = (pid_t)-1;
+
+ i_free(conn->base_name);
+ conn->base_name = i_strdup(name);
+
+ if (list->set.input_idle_timeout_secs != 0 &&
+ conn->input_idle_timeout_secs == 0) {
+ conn->input_idle_timeout_secs =
+ list->set.input_idle_timeout_secs;
+ }
+
+ if (conn->event == NULL)
+ conn->event = event_create(conn->event_parent);
+ if (list->set.debug)
+ event_set_forced_debug(conn->event, TRUE);
+
+ if (conn->list != NULL) {
+ i_assert(conn->list == list);
+ } else {
+ conn->list = list;
+ DLLIST_PREPEND(&list->connections, conn);
+ list->connections_count++;
+ }
+
+ connection_update_properties(conn);
+ connection_set_default_handlers(conn);
+}
+
+void connection_init(struct connection_list *list, struct connection *conn,
+ const char *name)
+{
+ connection_init_full(list, conn, name, -1, -1);
+}
+
+void connection_init_server(struct connection_list *list,
+ struct connection *conn, const char *name,
+ int fd_in, int fd_out)
+{
+ i_assert(!list->set.client);
+
+ connection_init_full(list, conn, name, fd_in, fd_out);
+
+ struct event_passthrough *e = event_create_passthrough(conn->event)->
+ set_name("client_connection_connected");
+ /* fd_out differs from fd_in only for stdin/stdout. Keep the logging
+ output nice and clean by logging only the fd_in. If it's 0, it'll
+ also be obvious that fd_out=1. */
+ e_debug(e->event(), "Server accepted connection (fd=%d)", fd_in);
+
+ connection_init_streams(conn);
+}
+
+void connection_init_server_ip(struct connection_list *list,
+ struct connection *conn, const char *name,
+ int fd_in, int fd_out,
+ const struct ip_addr *remote_ip,
+ in_port_t remote_port)
+{
+ if (remote_ip != NULL && remote_ip->family != 0)
+ conn->remote_ip = *remote_ip;
+ if (remote_port != 0)
+ conn->remote_port = remote_port;
+
+ connection_init_server(list, conn, name, fd_in, fd_out);
+}
+
+void connection_init_client_fd(struct connection_list *list,
+ struct connection *conn, const char *name,
+ int fd_in, int fd_out)
+{
+ i_assert(list->set.client);
+
+ connection_init_full(list, conn, name, fd_in, fd_out);
+
+ struct event_passthrough *e = event_create_passthrough(conn->event)->
+ set_name("server_connection_connected");
+ /* fd_out differs from fd_in only for stdin/stdout. Keep the logging
+ output nice and clean by logging only the fd_in. If it's 0, it'll
+ also be obvious that fd_out=1. */
+ e_debug(e->event(), "Client connected (fd=%d)", fd_in);
+
+ connection_client_connected(conn, TRUE);
+}
+
+void connection_init_client_ip_from(struct connection_list *list,
+ struct connection *conn,
+ const char *hostname,
+ const struct ip_addr *ip, in_port_t port,
+ const struct ip_addr *my_ip)
+{
+ const char *name = NULL;
+
+ if (hostname != NULL)
+ name = t_strdup_printf("%s:%u", hostname, port);
+
+ i_assert(list->set.client);
+
+ conn->remote_ip = *ip;
+ conn->remote_port = port;
+
+ if (my_ip != NULL)
+ conn->local_ip = *my_ip;
+ else
+ i_zero(&conn->local_ip);
+
+ connection_init(list, conn, name);
+ if (hostname != NULL)
+ event_add_str(conn->event, "dest_host", hostname);
+ connection_update_event(conn);
+}
+
+void connection_init_client_ip(struct connection_list *list,
+ struct connection *conn, const char *hostname,
+ const struct ip_addr *ip, in_port_t port)
+{
+ connection_init_client_ip_from(list, conn, hostname, ip, port, NULL);
+}
+
+void connection_init_client_unix(struct connection_list *list,
+ struct connection *conn, const char *path)
+{
+ i_assert(list->set.client);
+
+ conn->unix_socket = TRUE;
+
+ connection_init(list, conn, path);
+ event_add_str(conn->event, "socket_path", path);
+}
+
+void connection_init_from_streams(struct connection_list *list,
+ struct connection *conn, const char *name,
+ struct istream *input, struct ostream *output)
+{
+ connection_init_full(list, conn, name,
+ i_stream_get_fd(input), o_stream_get_fd(output));
+
+ i_assert(conn->fd_in >= 0);
+ i_assert(conn->fd_out >= 0);
+ i_assert(conn->io == NULL);
+ i_assert(conn->input == NULL);
+ i_assert(conn->output == NULL);
+ i_assert(conn->to == NULL);
+
+ conn->input = input;
+ i_stream_ref(conn->input);
+
+ conn->output = output;
+ o_stream_ref(conn->output);
+ o_stream_set_no_error_handling(conn->output, TRUE);
+
+ connection_update_stream_names(conn);
+
+ conn->disconnected = FALSE;
+ connection_input_resume_full(conn, FALSE);
+
+ if (conn->v.client_connected != NULL)
+ conn->v.client_connected(conn, TRUE);
+}
+
+static void connection_socket_connected(struct connection *conn)
+{
+ io_remove(&conn->io);
+ timeout_remove(&conn->to);
+
+ errno = net_geterror(conn->fd_in);
+ connection_client_connected(conn, errno == 0);
+}
+
+int connection_client_connect(struct connection *conn)
+{
+ const struct connection_settings *set = &conn->list->set;
+ int fd;
+
+ i_assert(conn->list->set.client);
+ i_assert(conn->fd_in == -1);
+
+ e_debug(conn->event, "Connecting");
+
+ if (conn->remote_port != 0) {
+ fd = net_connect_ip(&conn->remote_ip, conn->remote_port,
+ (conn->local_ip.family != 0 ?
+ &conn->local_ip : NULL));
+ } else if (conn->list->set.unix_client_connect_msecs == 0) {
+ fd = net_connect_unix(conn->base_name);
+ } else {
+ fd = net_connect_unix_with_retries(
+ conn->base_name,
+ conn->list->set.unix_client_connect_msecs);
+ }
+ if (fd == -1)
+ return -1;
+ conn->fd_in = conn->fd_out = fd;
+ conn->connect_started = ioloop_timeval;
+ conn->disconnected = FALSE;
+
+ if (conn->remote_port != 0 ||
+ conn->list->set.delayed_unix_client_connected_callback) {
+ connection_update_properties(conn);
+ conn->io = io_add_to(conn->ioloop, conn->fd_out, IO_WRITE,
+ connection_socket_connected, conn);
+ e_debug(conn->event,
+ "Waiting for connect (fd=%d) to finish for max %u msecs",
+ fd, set->client_connect_timeout_msecs);
+ if (set->client_connect_timeout_msecs != 0) {
+ conn->to = timeout_add_to(conn->ioloop,
+ set->client_connect_timeout_msecs,
+ *conn->v.connect_timeout, conn);
+ }
+ } else {
+ connection_client_connected(conn, TRUE);
+ }
+ return 0;
+}
+
+static void connection_update_counters(struct connection *conn)
+{
+ if (conn->input != NULL)
+ event_add_int(conn->event, "bytes_in", conn->input->v_offset);
+ if (conn->output != NULL)
+ event_add_int(conn->event, "bytes_out", conn->output->offset);
+}
+
+void connection_disconnect(struct connection *conn)
+{
+ if (conn->disconnected)
+ return;
+ connection_update_counters(conn);
+ /* client connects to a Server, and Server gets connection from Client
+ */
+ const char *ename = conn->list->set.client ?
+ "server_connection_disconnected" :
+ "client_connection_disconnected";
+
+ struct event_passthrough *e = event_create_passthrough(conn->event)->
+ set_name(ename)->
+ add_str("reason", connection_disconnect_reason(conn));
+ e_debug(e->event(), "Disconnected: %s (fd=%d)",
+ connection_disconnect_reason(conn), conn->fd_in);
+
+ conn->last_input = 0;
+ i_zero(&conn->last_input_tv);
+ timeout_remove(&conn->to);
+ io_remove(&conn->io);
+ i_stream_close(conn->input);
+ i_stream_destroy(&conn->input);
+ o_stream_close(conn->output);
+ o_stream_destroy(&conn->output);
+ fd_close_maybe_stdio(&conn->fd_in, &conn->fd_out);
+ conn->disconnected = TRUE;
+}
+
+void connection_deinit(struct connection *conn)
+{
+ i_assert(conn->list->connections_count > 0);
+
+ conn->list->connections_count--;
+ DLLIST_REMOVE(&conn->list->connections, conn);
+
+ connection_disconnect(conn);
+ i_free(conn->base_name);
+ i_free(conn->label);
+ i_free(conn->property_label);
+ event_unref(&conn->event);
+ conn->list = NULL;
+}
+
+int connection_input_read(struct connection *conn)
+{
+ conn->last_input = ioloop_time;
+ conn->last_input_tv = ioloop_timeval;
+ if (conn->to != NULL)
+ timeout_reset(conn->to);
+
+ switch (i_stream_read(conn->input)) {
+ case -2:
+ /* buffer full */
+ switch (conn->list->set.input_full_behavior) {
+ case CONNECTION_BEHAVIOR_DESTROY:
+ connection_closed(conn,
+ CONNECTION_DISCONNECT_BUFFER_FULL);
+ return -1;
+ case CONNECTION_BEHAVIOR_ALLOW:
+ return -2;
+ }
+ i_unreached();
+ case -1:
+ /* disconnected */
+ connection_closed(conn, CONNECTION_DISCONNECT_CONN_CLOSED);
+ return -1;
+ case 0:
+ /* nothing new read */
+ return 0;
+ default:
+ /* something was read */
+ return 1;
+ }
+}
+
+const char *connection_disconnect_reason(struct connection *conn)
+{
+ switch (conn->disconnect_reason) {
+ case CONNECTION_DISCONNECT_DEINIT:
+ return "Deinitializing";
+ case CONNECTION_DISCONNECT_CONNECT_TIMEOUT: {
+ unsigned int msecs =
+ conn->list->set.client_connect_timeout_msecs;
+ return t_strdup_printf("connect() timed out in %u.%03u secs",
+ msecs/1000, msecs%1000);
+ }
+ case CONNECTION_DISCONNECT_IDLE_TIMEOUT:
+ return "Idle timeout";
+ case CONNECTION_DISCONNECT_CONN_CLOSED:
+ if (conn->input == NULL)
+ return t_strdup_printf("connect() failed: %m");
+ /* fall through */
+ case CONNECTION_DISCONNECT_NOT:
+ case CONNECTION_DISCONNECT_BUFFER_FULL:
+ return io_stream_get_disconnect_reason(conn->input, conn->output);
+ case CONNECTION_DISCONNECT_HANDSHAKE_FAILED:
+ return "Handshake failed";
+ }
+ i_unreached();
+}
+
+const char *connection_input_timeout_reason(struct connection *conn)
+{
+ if (conn->last_input_tv.tv_sec != 0) {
+ int diff = timeval_diff_msecs(&ioloop_timeval,
+ &conn->last_input_tv);
+ return t_strdup_printf("No input for %u.%03u secs",
+ diff/1000, diff%1000);
+ } else if (conn->connect_finished.tv_sec != 0) {
+ int diff = timeval_diff_msecs(&ioloop_timeval,
+ &conn->connect_finished);
+ return t_strdup_printf(
+ "No input since connected %u.%03u secs ago",
+ diff/1000, diff%1000);
+ } else {
+ int diff = timeval_diff_msecs(&ioloop_timeval,
+ &conn->connect_started);
+ return t_strdup_printf("connect() timed out after %u.%03u secs",
+ diff/1000, diff%1000);
+ }
+}
+
+void connection_set_handlers(struct connection *conn,
+ const struct connection_vfuncs *vfuncs)
+{
+ connection_input_halt(conn);
+ i_assert(vfuncs->destroy != NULL);
+ conn->v = *vfuncs;
+ if (conn->v.input == NULL)
+ conn->v.input = connection_input_default;
+ if (conn->v.input_line == NULL)
+ conn->v.input_line = connection_input_line_default;
+ if (conn->v.handshake_args == NULL)
+ conn->v.handshake_args = connection_handshake_args_default;
+ if (conn->v.idle_timeout == NULL)
+ conn->v.idle_timeout = connection_idle_timeout;
+ if (conn->v.connect_timeout == NULL)
+ conn->v.connect_timeout = connection_connect_timeout;
+ if (!conn->disconnected)
+ connection_input_resume_full(conn, FALSE);
+}
+
+void connection_set_default_handlers(struct connection *conn)
+{
+ connection_set_handlers(conn, &conn->list->v);
+}
+
+void connection_switch_ioloop_to(struct connection *conn,
+ struct ioloop *ioloop)
+{
+ conn->ioloop = ioloop;
+ if (conn->io != NULL)
+ conn->io = io_loop_move_io_to(ioloop, &conn->io);
+ if (conn->to != NULL)
+ conn->to = io_loop_move_timeout_to(ioloop, &conn->to);
+ if (conn->input != NULL)
+ i_stream_switch_ioloop_to(conn->input, ioloop);
+ if (conn->output != NULL)
+ o_stream_switch_ioloop_to(conn->output, ioloop);
+}
+
+void connection_switch_ioloop(struct connection *conn)
+{
+ connection_switch_ioloop_to(conn, current_ioloop);
+}
+
+struct connection_list *
+connection_list_init(const struct connection_settings *set,
+ const struct connection_vfuncs *vfuncs)
+{
+ struct connection_list *list;
+
+ i_assert(vfuncs->input != NULL ||
+ set->input_full_behavior != CONNECTION_BEHAVIOR_ALLOW);
+ i_assert(set->major_version == 0 ||
+ (set->service_name_in != NULL &&
+ set->service_name_out != NULL &&
+ set->output_max_size != 0));
+
+ list = i_new(struct connection_list, 1);
+ list->set = *set;
+ list->v = *vfuncs;
+
+ return list;
+}
+
+void connection_list_deinit(struct connection_list **_list)
+{
+ struct connection_list *list = *_list;
+ struct connection *conn;
+
+ *_list = NULL;
+
+ while (list->connections != NULL) {
+ conn = list->connections;
+ connection_closed(conn, CONNECTION_DISCONNECT_DEINIT);
+ i_assert(conn != list->connections);
+ }
+ i_free(list);
+}
diff --git a/src/lib/connection.h b/src/lib/connection.h
new file mode 100644
index 0000000..2dafacc
--- /dev/null
+++ b/src/lib/connection.h
@@ -0,0 +1,259 @@
+#ifndef CONNECTION_H
+#define CONNECTION_H
+
+#include "net.h"
+
+struct ioloop;
+struct connection;
+
+enum connection_behavior {
+ CONNECTION_BEHAVIOR_DESTROY = 0,
+ CONNECTION_BEHAVIOR_ALLOW
+};
+
+enum connection_disconnect_reason {
+ /* not disconnected yet */
+ CONNECTION_DISCONNECT_NOT = 0,
+ /* normal requested disconnection */
+ CONNECTION_DISCONNECT_DEINIT,
+ /* input buffer full */
+ CONNECTION_DISCONNECT_BUFFER_FULL,
+ /* connection got disconnected */
+ CONNECTION_DISCONNECT_CONN_CLOSED,
+ /* connect() timed out */
+ CONNECTION_DISCONNECT_CONNECT_TIMEOUT,
+ /* remote didn't send input */
+ CONNECTION_DISCONNECT_IDLE_TIMEOUT,
+ /* handshake failed */
+ CONNECTION_DISCONNECT_HANDSHAKE_FAILED,
+};
+
+struct connection_vfuncs {
+ void (*destroy)(struct connection *conn);
+ /* For UNIX socket clients this gets called immediately (unless
+ delayed_unix_client_connected_callback=TRUE) with success=TRUE,
+ for IP connections it gets called later:
+
+ If connect() fails, sets success=FALSE and errno. Streams aren't
+ initialized in that situation either. destroy() is called after
+ the callback. */
+ void (*client_connected)(struct connection *conn, bool success);
+
+ /* implement one of the input*() methods.
+ They return 1 = ok, continue. 0 = ok, but stop processing more
+ lines, -1 = error, disconnect the client. */
+ void (*input)(struct connection *conn);
+ int (*input_line)(struct connection *conn, const char *line);
+ int (*input_args)(struct connection *conn, const char *const *args);
+
+ /* handshake functions. Defaults to version checking.
+ must return 1 when handshake is completed, otherwise return 0.
+ return -1 to indicate error and disconnect client.
+
+ if you implement this, remember to call connection_verify_version
+ yourself, otherwise you end up with assert crash.
+
+ these will not be called if you implement `input` virtual function.
+ */
+ int (*handshake)(struct connection *conn);
+ int (*handshake_line)(struct connection *conn, const char *line);
+ int (*handshake_args)(struct connection *conn, const char *const *args);
+
+ /* Called when the connection handshake is ready. */
+ void (*handshake_ready)(struct connection *conn);
+
+ /* Called when input_idle_timeout_secs is reached, defaults to disconnect */
+ void (*idle_timeout)(struct connection *conn);
+ /* Called when client_connect_timeout_msecs is reached, defaults to disconnect */
+ void (*connect_timeout)(struct connection *conn);
+};
+
+struct connection_settings {
+ const char *service_name_in;
+ const char *service_name_out;
+ unsigned int major_version, minor_version;
+
+ unsigned int client_connect_timeout_msecs;
+ unsigned int input_idle_timeout_secs;
+
+ /* These need to be non-zero for corresponding stream to
+ be created. */
+ size_t input_max_size;
+ size_t output_max_size;
+ enum connection_behavior input_full_behavior;
+
+ /* Set to TRUE if this is a client */
+ bool client;
+
+ /* Set to TRUE if version should not be sent */
+ bool dont_send_version;
+ /* By default when only input_args() is used, or when
+ connection_input_line_default() is used, empty lines aren't allowed
+ since it would result in additional args[0] == NULL check. Setting
+ this to TRUE passes it through instead of logging an error. */
+ bool allow_empty_args_input;
+ /* Don't call client_connected() immediately on
+ connection_client_connect() with UNIX sockets. This is mainly
+ to make the functionality identical with inet sockets, which may
+ simplify the calling code. */
+ bool delayed_unix_client_connected_callback;
+ /* Put the connection id in the log prefix */
+ bool log_connection_id;
+ /* If connect() to UNIX socket fails with EAGAIN, retry for this many
+ milliseconds before giving up (0 = try once) */
+ unsigned int unix_client_connect_msecs;
+ /* Turn on debug logging */
+ bool debug;
+};
+
+struct connection {
+ struct connection *prev, *next;
+ struct connection_list *list;
+
+ /* The name for the connection provided by the application. This is
+ usually a host name or a unix socket path. This may be NULL if the
+ application provides none. */
+ char *base_name;
+ /* The name of the connection determined by the connection API. It is
+ equal to base_name when that is available and otherwise it is
+ composed from the connection properties; e.g., "ip:port". */
+ const char *name;
+
+ char *label;
+ char *property_label;
+ unsigned int id;
+
+ int fd_in, fd_out;
+ struct ioloop *ioloop;
+ struct io *io;
+ struct istream *input;
+ struct ostream *output;
+
+ unsigned int input_idle_timeout_secs;
+ struct timeout *to;
+ time_t last_input;
+ struct timeval last_input_tv;
+ struct timeval connect_started;
+ struct timeval connect_finished;
+
+ /* set to parent event before calling init */
+ struct event *event_parent;
+ struct event *event;
+
+ /* connection properties */
+ struct ip_addr local_ip, remote_ip;
+ in_port_t remote_port;
+ pid_t remote_pid;
+ uid_t remote_uid;
+
+ /* received minor version */
+ unsigned int minor_version;
+
+ /* handlers */
+ struct connection_vfuncs v;
+
+ enum connection_disconnect_reason disconnect_reason;
+
+ bool version_received:1;
+ bool handshake_received:1;
+ bool unix_socket:1;
+ bool unix_peer_checked:1;
+ bool disconnected:1;
+};
+
+struct connection_list {
+ struct connection *connections;
+ unsigned int connections_count;
+
+ unsigned int id_counter;
+
+ struct connection_settings set;
+ struct connection_vfuncs v;
+};
+
+void connection_init(struct connection_list *list, struct connection *conn,
+ const char *name) ATTR_NULL(3);
+void connection_init_server(struct connection_list *list,
+ struct connection *conn, const char *name,
+ int fd_in, int fd_out) ATTR_NULL(3);
+void connection_init_server_ip(struct connection_list *list,
+ struct connection *conn, const char *name,
+ int fd_in, int fd_out,
+ const struct ip_addr *remote_ip,
+ in_port_t remote_port) ATTR_NULL(3, 6);
+void connection_init_client_ip(struct connection_list *list,
+ struct connection *conn, const char *hostname,
+ const struct ip_addr *ip, in_port_t port)
+ ATTR_NULL(3);
+void connection_init_client_ip_from(struct connection_list *list,
+ struct connection *conn,
+ const char *hostname,
+ const struct ip_addr *ip, in_port_t port,
+ const struct ip_addr *my_ip) ATTR_NULL(3,6);
+void connection_init_client_unix(struct connection_list *list,
+ struct connection *conn, const char *path);
+void connection_init_client_fd(struct connection_list *list,
+ struct connection *conn, const char *name,
+ int fd_int, int fd_out) ATTR_NULL(3);
+void connection_init_from_streams(struct connection_list *list,
+ struct connection *conn, const char *name,
+ struct istream *input,
+ struct ostream *output) ATTR_NULL(3);
+
+int connection_client_connect(struct connection *conn);
+
+/* Disconnects a connection */
+void connection_disconnect(struct connection *conn);
+
+/* Deinitializes a connection, calls disconnect */
+void connection_deinit(struct connection *conn);
+
+void connection_input_halt(struct connection *conn);
+/* Resume connection handling. If a new IO was added, it's marked as having
+ pending input. */
+void connection_input_resume(struct connection *conn);
+
+/* Update event fields and log prefix based on connection properties. */
+void connection_update_event(struct connection *conn);
+
+/* Update connection properties and labels */
+void connection_update_properties(struct connection *conn);
+
+/* This needs to be called if the input/output streams are changed */
+void connection_streams_changed(struct connection *conn);
+
+/* Returns -1 = disconnected, 0 = nothing new, 1 = something new.
+ If input_full_behavior is ALLOW, may return also -2 = buffer full. */
+int connection_input_read(struct connection *conn);
+/* Verify that VERSION input matches what we expect. */
+int connection_verify_version(struct connection *conn,
+ const char *service_name,
+ unsigned int major_version,
+ unsigned int minor_version);
+
+int connection_handshake_args_default(struct connection *conn,
+ const char *const *args);
+
+/* Returns human-readable reason for why connection was disconnected. */
+const char *connection_disconnect_reason(struct connection *conn);
+/* Returns human-readable reason for why connection timed out,
+ e.g. "No input for 10.023 secs". */
+const char *connection_input_timeout_reason(struct connection *conn);
+
+void connection_switch_ioloop_to(struct connection *conn,
+ struct ioloop *ioloop);
+void connection_switch_ioloop(struct connection *conn);
+
+struct connection_list *
+connection_list_init(const struct connection_settings *set,
+ const struct connection_vfuncs *vfuncs);
+void connection_list_deinit(struct connection_list **list);
+
+void connection_input_default(struct connection *conn);
+int connection_input_line_default(struct connection *conn, const char *line);
+
+/* Change handlers, calls connection_input_halt and connection_input_resume */
+void connection_set_handlers(struct connection *conn, const struct connection_vfuncs *vfuncs);
+void connection_set_default_handlers(struct connection *conn);
+
+#endif
diff --git a/src/lib/cpu-limit.c b/src/lib/cpu-limit.c
new file mode 100644
index 0000000..754717a
--- /dev/null
+++ b/src/lib/cpu-limit.c
@@ -0,0 +1,200 @@
+/* Copyright (c) 2020 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "lib-signals.h"
+#include "time-util.h"
+#include "cpu-limit.h"
+
+#include <sys/time.h>
+#include <sys/resource.h>
+
+struct cpu_limit {
+ struct cpu_limit *parent;
+
+ enum cpu_limit_type type;
+ unsigned int cpu_limit_secs;
+ struct rusage initial_usage;
+
+ bool limit_reached;
+};
+
+static struct cpu_limit *cpu_limit;
+static struct rlimit orig_limit, last_set_rlimit;
+static volatile sig_atomic_t xcpu_signal_counter;
+static sig_atomic_t checked_signal_counter;
+static unsigned int rlim_cur_adjust_secs;
+
+static void
+cpu_limit_handler(const siginfo_t *si ATTR_UNUSED, void *context ATTR_UNUSED)
+{
+ xcpu_signal_counter++;
+}
+
+static unsigned int
+cpu_limit_get_usage_msecs_with(struct cpu_limit *climit,
+ enum cpu_limit_type type,
+ const struct rusage *rusage)
+{
+ struct timeval cpu_usage = { 0, 0 };
+ int usage_diff;
+
+ if ((type & CPU_LIMIT_TYPE_USER) != 0)
+ timeval_add(&cpu_usage, &rusage->ru_utime);
+ if ((type & CPU_LIMIT_TYPE_SYSTEM) != 0)
+ timeval_add(&cpu_usage, &rusage->ru_stime);
+
+ struct timeval initial_total = { 0, 0 };
+ if ((type & CPU_LIMIT_TYPE_USER) != 0)
+ timeval_add(&initial_total, &climit->initial_usage.ru_utime);
+ if ((type & CPU_LIMIT_TYPE_SYSTEM) != 0)
+ timeval_add(&initial_total, &climit->initial_usage.ru_stime);
+ usage_diff = timeval_diff_msecs(&cpu_usage, &initial_total);
+ i_assert(usage_diff >= 0);
+
+ return (unsigned int)usage_diff;
+}
+
+unsigned int
+cpu_limit_get_usage_msecs(struct cpu_limit *climit, enum cpu_limit_type type)
+{
+ struct rusage rusage;
+
+ /* Query cpu usage so far */
+ if (getrusage(RUSAGE_SELF, &rusage) < 0)
+ i_fatal("getrusage() failed: %m");
+
+ return cpu_limit_get_usage_msecs_with(climit, type, &rusage);
+}
+
+static bool
+cpu_limit_update_recursive(struct cpu_limit *climit,
+ const struct rusage *rusage,
+ unsigned int *max_wait_secs)
+{
+ if (climit == NULL)
+ return FALSE;
+ if (cpu_limit_update_recursive(climit->parent, rusage, max_wait_secs)) {
+ /* parent's limit reached */
+ climit->limit_reached = TRUE;
+ return TRUE;
+ }
+ unsigned int secs_used =
+ cpu_limit_get_usage_msecs_with(climit, climit->type, rusage)/1000;
+ if (secs_used >= climit->cpu_limit_secs) {
+ climit->limit_reached = TRUE;
+ return TRUE;
+ }
+ unsigned int secs_left = climit->cpu_limit_secs - secs_used;
+ if (*max_wait_secs > secs_left)
+ *max_wait_secs = secs_left;
+ return FALSE;
+}
+
+static void cpu_limit_update_rlimit(void)
+{
+ struct rusage rusage;
+ struct rlimit rlimit;
+ unsigned int max_wait_secs = UINT_MAX;
+
+ if (getrusage(RUSAGE_SELF, &rusage) < 0)
+ i_fatal("getrusage() failed: %m");
+
+ (void)cpu_limit_update_recursive(cpu_limit, &rusage, &max_wait_secs);
+ if (max_wait_secs == UINT_MAX) {
+ /* All the limits have reached now. Restore the original
+ limits. */
+ rlimit = orig_limit;
+ } else {
+ struct timeval tv_limit = rusage.ru_utime;
+ timeval_add(&tv_limit, &rusage.ru_stime);
+
+ i_zero(&rlimit);
+ /* Add +1 second to round up. */
+ rlimit.rlim_cur = tv_limit.tv_sec +
+ max_wait_secs + 1 + rlim_cur_adjust_secs;
+ rlimit.rlim_max = orig_limit.rlim_max;
+ }
+ if (last_set_rlimit.rlim_cur != rlimit.rlim_cur) {
+ last_set_rlimit = rlimit;
+ if (setrlimit(RLIMIT_CPU, &rlimit) < 0)
+ i_fatal("setrlimit() failed: %m");
+ }
+}
+
+struct cpu_limit *
+cpu_limit_init(unsigned int cpu_limit_secs, enum cpu_limit_type type)
+{
+ struct cpu_limit *climit;
+ struct rusage rusage;
+
+ i_assert(cpu_limit_secs > 0);
+ i_assert(type != 0);
+
+ climit = i_new(struct cpu_limit, 1);
+ climit->parent = cpu_limit;
+ climit->type = type;
+ climit->cpu_limit_secs = cpu_limit_secs;
+
+ /* Query current limit */
+ if (climit->parent == NULL) {
+ if (getrlimit(RLIMIT_CPU, &orig_limit) < 0)
+ i_fatal("getrlimit() failed: %m");
+ }
+
+ /* Query cpu usage so far */
+ if (getrusage(RUSAGE_SELF, &rusage) < 0)
+ i_fatal("getrusage() failed: %m");
+ climit->initial_usage = rusage;
+
+ if (climit->parent == NULL) {
+ lib_signals_set_handler(SIGXCPU, LIBSIG_FLAG_RESTART,
+ cpu_limit_handler, NULL);
+ }
+
+ cpu_limit = climit;
+ cpu_limit_update_rlimit();
+ return climit;
+}
+
+void cpu_limit_deinit(struct cpu_limit **_climit)
+{
+ struct cpu_limit *climit = *_climit;
+
+ *_climit = NULL;
+ if (climit == NULL)
+ return;
+
+ i_assert(climit == cpu_limit);
+
+ cpu_limit = climit->parent;
+ cpu_limit_update_rlimit();
+ if (climit->parent == NULL)
+ lib_signals_unset_handler(SIGXCPU, cpu_limit_handler, NULL);
+ i_free(climit);
+}
+
+bool cpu_limit_exceeded(struct cpu_limit *climit)
+{
+ static struct timeval tv_last = { 0, 0 };
+ struct timeval tv_now;
+
+ if (checked_signal_counter != xcpu_signal_counter) {
+ i_gettimeofday(&tv_now);
+ if (tv_last.tv_sec != 0 &&
+ timeval_diff_msecs(&tv_now, &tv_last) < 1000) {
+ /* Additional sanity check: We're getting here more
+ rapidly than once per second. This isn't expected
+ to happen, but at least in theory it could happen
+ because rlim_cur isn't clearly calculated from just
+ the user+system CPU usage. So in case rlim_cur is
+ too low and keeps firing XCPU signal, try to
+ increase rlim_cur by 1 second. Eventually it should
+ become large enough. */
+ rlim_cur_adjust_secs++;
+ }
+
+ checked_signal_counter = xcpu_signal_counter;
+ cpu_limit_update_rlimit();
+ }
+ return climit->limit_reached;
+}
diff --git a/src/lib/cpu-limit.h b/src/lib/cpu-limit.h
new file mode 100644
index 0000000..b6e45ae
--- /dev/null
+++ b/src/lib/cpu-limit.h
@@ -0,0 +1,67 @@
+#ifndef CPU_LIMIT
+#define CPU_LIMIT
+
+struct cpu_limit;
+
+enum cpu_limit_type {
+ CPU_LIMIT_TYPE_USER = BIT(0),
+ CPU_LIMIT_TYPE_SYSTEM = BIT(1),
+};
+#define CPU_LIMIT_TYPE_ALL (CPU_LIMIT_TYPE_USER | CPU_LIMIT_TYPE_SYSTEM)
+
+/* Start tracking CPU usage. This internally uses setrlimit(RLIMIT_CPU) to
+ trigger SIGXCPU to avoid constantly calling getrlimit() to check if the CPU
+ usage has reached a limit. Once all limits created by this API are released,
+ the original CPU resource limits are restored (if any).
+
+ CPU time limits can be nested, i.e. they are never independent. The outer
+ limits contain the bounded maximum limit for the inner limits. For example
+ the code execution flow might be:
+ - Set 30s CPU limit (outer limit)
+ - Use up 5s of CPU
+ - Set 40s CPU limit (inner limit)
+ - Infinite loop
+ The inner loop's limit won't even be reached here. After the inner loops
+ runs for 25 seconds, the outer loop's 30s limit is reached. This causes
+ both the inner and the other limit's cpu_limit_exceeded() to return TRUE.
+ It's expected that the inner execution stops and returns back to the outer
+ execution, which notices that the outer execution has also reached the limit.
+
+ Another example where the inner limit is reached:
+ - Set 30s CPU limit (outer limit)
+ - Use up 5s of CPU
+ - Set 10s CPU limit (inner limit)
+ - Infinite loop
+ Here the inner 10s limit is reached, and the inner execution stops. The
+ outer execution could still run for another 15 seconds.
+
+ Example usage:
+
+ bool limit_reached = FALSE;
+ limit = cpu_limit_init(5, CPU_LIMIT_TYPE_ALL);
+ while (long_operation_iterate_once()) {
+ if (cpu_limit_exceeded(limit)) {
+ limit_reached = TRUE; // operation took >=5 secs
+ break;
+ }
+ }
+ cpu_limit_deinit(&limit);
+*/
+struct cpu_limit *
+cpu_limit_init(unsigned int cpu_limit_secs, enum cpu_limit_type type);
+void cpu_limit_deinit(struct cpu_limit **_climit);
+
+/* Returns TRUE if the CPU limit has been exceeded for this limit or any of its
+ parents. */
+bool cpu_limit_exceeded(struct cpu_limit *climit);
+
+unsigned int cpu_limit_get_usage_msecs(struct cpu_limit *climit,
+ enum cpu_limit_type type);
+
+static inline unsigned int
+cpu_limit_get_usage_secs(struct cpu_limit *climit, enum cpu_limit_type type)
+{
+ return cpu_limit_get_usage_msecs(climit, type) / 1000;
+}
+
+#endif
diff --git a/src/lib/crc32.c b/src/lib/crc32.c
new file mode 100644
index 0000000..576c689
--- /dev/null
+++ b/src/lib/crc32.c
@@ -0,0 +1,91 @@
+/* Copyright (c) 2006-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "crc32.h"
+
+static uint32_t crc32tab[256] = {
+ 0x00000000,
+ 0x77073096, 0xEE0E612C, 0x990951BA, 0x076DC419, 0x706AF48F,
+ 0xE963A535, 0x9E6495A3, 0x0EDB8832, 0x79DCB8A4, 0xE0D5E91E,
+ 0x97D2D988, 0x09B64C2B, 0x7EB17CBD, 0xE7B82D07, 0x90BF1D91,
+ 0x1DB71064, 0x6AB020F2, 0xF3B97148, 0x84BE41DE, 0x1ADAD47D,
+ 0x6DDDE4EB, 0xF4D4B551, 0x83D385C7, 0x136C9856, 0x646BA8C0,
+ 0xFD62F97A, 0x8A65C9EC, 0x14015C4F, 0x63066CD9, 0xFA0F3D63,
+ 0x8D080DF5, 0x3B6E20C8, 0x4C69105E, 0xD56041E4, 0xA2677172,
+ 0x3C03E4D1, 0x4B04D447, 0xD20D85FD, 0xA50AB56B, 0x35B5A8FA,
+ 0x42B2986C, 0xDBBBC9D6, 0xACBCF940, 0x32D86CE3, 0x45DF5C75,
+ 0xDCD60DCF, 0xABD13D59, 0x26D930AC, 0x51DE003A, 0xC8D75180,
+ 0xBFD06116, 0x21B4F4B5, 0x56B3C423, 0xCFBA9599, 0xB8BDA50F,
+ 0x2802B89E, 0x5F058808, 0xC60CD9B2, 0xB10BE924, 0x2F6F7C87,
+ 0x58684C11, 0xC1611DAB, 0xB6662D3D, 0x76DC4190, 0x01DB7106,
+ 0x98D220BC, 0xEFD5102A, 0x71B18589, 0x06B6B51F, 0x9FBFE4A5,
+ 0xE8B8D433, 0x7807C9A2, 0x0F00F934, 0x9609A88E, 0xE10E9818,
+ 0x7F6A0DBB, 0x086D3D2D, 0x91646C97, 0xE6635C01, 0x6B6B51F4,
+ 0x1C6C6162, 0x856530D8, 0xF262004E, 0x6C0695ED, 0x1B01A57B,
+ 0x8208F4C1, 0xF50FC457, 0x65B0D9C6, 0x12B7E950, 0x8BBEB8EA,
+ 0xFCB9887C, 0x62DD1DDF, 0x15DA2D49, 0x8CD37CF3, 0xFBD44C65,
+ 0x4DB26158, 0x3AB551CE, 0xA3BC0074, 0xD4BB30E2, 0x4ADFA541,
+ 0x3DD895D7, 0xA4D1C46D, 0xD3D6F4FB, 0x4369E96A, 0x346ED9FC,
+ 0xAD678846, 0xDA60B8D0, 0x44042D73, 0x33031DE5, 0xAA0A4C5F,
+ 0xDD0D7CC9, 0x5005713C, 0x270241AA, 0xBE0B1010, 0xC90C2086,
+ 0x5768B525, 0x206F85B3, 0xB966D409, 0xCE61E49F, 0x5EDEF90E,
+ 0x29D9C998, 0xB0D09822, 0xC7D7A8B4, 0x59B33D17, 0x2EB40D81,
+ 0xB7BD5C3B, 0xC0BA6CAD, 0xEDB88320, 0x9ABFB3B6, 0x03B6E20C,
+ 0x74B1D29A, 0xEAD54739, 0x9DD277AF, 0x04DB2615, 0x73DC1683,
+ 0xE3630B12, 0x94643B84, 0x0D6D6A3E, 0x7A6A5AA8, 0xE40ECF0B,
+ 0x9309FF9D, 0x0A00AE27, 0x7D079EB1, 0xF00F9344, 0x8708A3D2,
+ 0x1E01F268, 0x6906C2FE, 0xF762575D, 0x806567CB, 0x196C3671,
+ 0x6E6B06E7, 0xFED41B76, 0x89D32BE0, 0x10DA7A5A, 0x67DD4ACC,
+ 0xF9B9DF6F, 0x8EBEEFF9, 0x17B7BE43, 0x60B08ED5, 0xD6D6A3E8,
+ 0xA1D1937E, 0x38D8C2C4, 0x4FDFF252, 0xD1BB67F1, 0xA6BC5767,
+ 0x3FB506DD, 0x48B2364B, 0xD80D2BDA, 0xAF0A1B4C, 0x36034AF6,
+ 0x41047A60, 0xDF60EFC3, 0xA867DF55, 0x316E8EEF, 0x4669BE79,
+ 0xCB61B38C, 0xBC66831A, 0x256FD2A0, 0x5268E236, 0xCC0C7795,
+ 0xBB0B4703, 0x220216B9, 0x5505262F, 0xC5BA3BBE, 0xB2BD0B28,
+ 0x2BB45A92, 0x5CB36A04, 0xC2D7FFA7, 0xB5D0CF31, 0x2CD99E8B,
+ 0x5BDEAE1D, 0x9B64C2B0, 0xEC63F226, 0x756AA39C, 0x026D930A,
+ 0x9C0906A9, 0xEB0E363F, 0x72076785, 0x05005713, 0x95BF4A82,
+ 0xE2B87A14, 0x7BB12BAE, 0x0CB61B38, 0x92D28E9B, 0xE5D5BE0D,
+ 0x7CDCEFB7, 0x0BDBDF21, 0x86D3D2D4, 0xF1D4E242, 0x68DDB3F8,
+ 0x1FDA836E, 0x81BE16CD, 0xF6B9265B, 0x6FB077E1, 0x18B74777,
+ 0x88085AE6, 0xFF0F6A70, 0x66063BCA, 0x11010B5C, 0x8F659EFF,
+ 0xF862AE69, 0x616BFFD3, 0x166CCF45, 0xA00AE278, 0xD70DD2EE,
+ 0x4E048354, 0x3903B3C2, 0xA7672661, 0xD06016F7, 0x4969474D,
+ 0x3E6E77DB, 0xAED16A4A, 0xD9D65ADC, 0x40DF0B66, 0x37D83BF0,
+ 0xA9BCAE53, 0xDEBB9EC5, 0x47B2CF7F, 0x30B5FFE9, 0xBDBDF21C,
+ 0xCABAC28A, 0x53B39330, 0x24B4A3A6, 0xBAD03605, 0xCDD70693,
+ 0x54DE5729, 0x23D967BF, 0xB3667A2E, 0xC4614AB8, 0x5D681B02,
+ 0x2A6F2B94, 0xB40BBE37, 0xC30C8EA1, 0x5A05DF1B, 0x2D02EF8D
+};
+
+uint32_t crc32_data(const void *data, size_t size)
+{
+ return crc32_data_more(0, data, size);
+}
+
+uint32_t crc32_data_more(uint32_t crc, const void *data, size_t size)
+{
+ const uint8_t *p = data, *end = p + size;
+
+ crc ^= 0xffffffff;
+ for (; p != end; p++)
+ crc = (crc >> 8) ^ crc32tab[((crc ^ *p) & 0xff)];
+ crc ^= 0xffffffff;
+ return crc;
+}
+
+uint32_t crc32_str(const char *str)
+{
+ return crc32_str_more(0, str);
+}
+
+uint32_t crc32_str_more(uint32_t crc, const char *str)
+{
+ const uint8_t *p = (const uint8_t *)str;
+
+ crc ^= 0xffffffff;
+ for (; *p != '\0'; p++)
+ crc = (crc >> 8) ^ crc32tab[((crc ^ *p) & 0xff)];
+ crc ^= 0xffffffff;
+ return crc;
+}
diff --git a/src/lib/crc32.h b/src/lib/crc32.h
new file mode 100644
index 0000000..7892212
--- /dev/null
+++ b/src/lib/crc32.h
@@ -0,0 +1,10 @@
+#ifndef CRC32_H
+#define CRC32_H
+
+uint32_t crc32_data(const void *data, size_t size) ATTR_PURE;
+uint32_t crc32_str(const char *str) ATTR_PURE;
+
+uint32_t crc32_data_more(uint32_t crc, const void *data, size_t size) ATTR_PURE;
+uint32_t crc32_str_more(uint32_t crc, const char *str) ATTR_PURE;
+
+#endif
diff --git a/src/lib/data-stack.c b/src/lib/data-stack.c
new file mode 100644
index 0000000..acbedc9
--- /dev/null
+++ b/src/lib/data-stack.c
@@ -0,0 +1,777 @@
+/* Copyright (c) 2002-2018 Dovecot authors, see the included COPYING file */
+
+/* @UNSAFE: whole file */
+
+#include "lib.h"
+#include "backtrace-string.h"
+#include "str.h"
+#include "data-stack.h"
+
+
+/* Initial stack size - this should be kept in a size that doesn't exceed
+ in a normal use to avoid extra malloc()ing. */
+#ifdef DEBUG
+# define INITIAL_STACK_SIZE (1024*10)
+#else
+# define INITIAL_STACK_SIZE (1024*32)
+#endif
+
+#ifdef DEBUG
+# define CLEAR_CHR 0xD5 /* D5 is mnemonic for "Data 5tack" */
+# define SENTRY_COUNT (4*8)
+# define BLOCK_CANARY ((void *)0xBADBADD5BADBADD5) /* contains 'D5' */
+# define ALLOC_SIZE(size) (MEM_ALIGN(sizeof(size_t)) + MEM_ALIGN(size + SENTRY_COUNT))
+#else
+# define CLEAR_CHR 0
+# define BLOCK_CANARY NULL
+# define block_canary_check(block) do { ; } while(0)
+# define ALLOC_SIZE(size) MEM_ALIGN(size)
+#endif
+
+struct stack_block {
+ struct stack_block *prev, *next;
+
+ size_t size, left;
+#ifdef DEBUG
+ /* The lowest value that "left" has been in this block since it was
+ last popped. This is used to keep track which parts of the block
+ needs to be cleared if DEBUG is used. */
+ size_t left_lowwater;
+#endif
+ /* NULL or a poison value, just in case something accesses
+ the memory in front of an allocated area */
+ void *canary;
+ unsigned char data[FLEXIBLE_ARRAY_MEMBER];
+};
+
+#define SIZEOF_MEMBLOCK MEM_ALIGN(sizeof(struct stack_block))
+
+#define STACK_BLOCK_DATA(block) \
+ (block->data + (SIZEOF_MEMBLOCK - sizeof(struct stack_block)))
+
+struct stack_frame {
+ struct stack_frame *prev;
+
+ struct stack_block *block;
+ /* Each frame initializes this to current_block->left, i.e. how much
+ free space is left in the block. So the frame's start position in
+ the block is (block.size - block_space_left) */
+ size_t block_space_left;
+ size_t last_alloc_size;
+ const char *marker;
+#ifdef DEBUG
+ /* Fairly arbitrary profiling data */
+ unsigned long long alloc_bytes;
+ unsigned int alloc_count;
+#endif
+};
+
+#ifdef STATIC_CHECKER
+struct data_stack_frame {
+ unsigned int id;
+};
+#endif
+
+unsigned int data_stack_frame_id = 0;
+
+static bool data_stack_initialized = FALSE;
+static data_stack_frame_t root_frame_id;
+
+static struct stack_frame *current_frame;
+
+/* The latest block currently used for allocation. current_block->next is
+ always NULL. */
+static struct stack_block *current_block;
+/* The largest block that data stack has allocated so far, which was already
+ freed. This can prevent rapid malloc()+free()ing when data stack is grown
+ and shrunk constantly. */
+static struct stack_block *unused_block = NULL;
+
+static struct event *event_datastack = NULL;
+static bool event_datastack_deinitialized = FALSE;
+
+static struct stack_block *last_buffer_block;
+static size_t last_buffer_size;
+static bool outofmem = FALSE;
+
+static union {
+ struct stack_block block;
+ unsigned char data[512];
+} outofmem_area;
+
+static struct stack_block *mem_block_alloc(size_t min_size);
+
+static inline
+unsigned char *data_stack_after_last_alloc(struct stack_block *block)
+{
+ return STACK_BLOCK_DATA(block) + (block->size - block->left);
+}
+
+static void data_stack_last_buffer_reset(bool preserve_data ATTR_UNUSED)
+{
+ if (last_buffer_block != NULL) {
+#ifdef DEBUG
+ unsigned char *last_alloc_end, *p, *pend;
+
+ /* We assume that this function gets called before
+ current_block changes. */
+ i_assert(last_buffer_block == current_block);
+
+ last_alloc_end = data_stack_after_last_alloc(current_block);
+ p = last_alloc_end + MEM_ALIGN(sizeof(size_t)) + last_buffer_size;
+ pend = last_alloc_end + ALLOC_SIZE(last_buffer_size);
+#endif
+ /* reset t_buffer_get() mark - not really needed but makes it
+ easier to notice if t_malloc()/t_push()/t_pop() is called
+ between t_buffer_get() and t_buffer_alloc().
+ do this before we get to i_panic() to avoid recursive
+ panics. */
+ last_buffer_block = NULL;
+
+#ifdef DEBUG
+ /* NOTE: If the below panic triggers, it may also be due to an
+ internal bug in data-stack (since this is rather complex). While
+ debugging whether that is the case, it's a good idea to change the
+ i_panic() to abort(). Otherwise the i_panic() changes the
+ data-stack's internal state and complicates debugging. */
+ while (p < pend)
+ if (*p++ != CLEAR_CHR)
+ i_panic("t_buffer_get(): buffer overflow");
+
+ if (!preserve_data) {
+ p = last_alloc_end;
+ memset(p, CLEAR_CHR, SENTRY_COUNT);
+ }
+#endif
+ }
+}
+
+data_stack_frame_t t_push(const char *marker)
+{
+ struct stack_frame *frame;
+
+ i_assert(marker != NULL);
+
+ if (unlikely(!data_stack_initialized)) {
+ /* kludgy, but allow this before initialization */
+ data_stack_init();
+ return t_push(marker);
+ }
+
+ /* allocate new block */
+ frame = t_buffer_get(sizeof(*frame));
+ frame->prev = current_frame;
+ current_frame = frame;
+
+ /* mark our current position */
+ current_frame->block = current_block;
+ current_frame->block_space_left = current_block->left;
+ current_frame->last_alloc_size = 0;
+ current_frame->marker = marker;
+#ifdef DEBUG
+ current_frame->alloc_bytes = 0;
+ current_frame->alloc_count = 0;
+#endif
+
+ t_buffer_alloc(sizeof(*frame));
+
+#ifndef STATIC_CHECKER
+ return data_stack_frame_id++;
+#else
+ struct data_stack_frame *ds_frame = i_new(struct data_stack_frame, 1);
+ ds_frame->id = data_stack_frame_id++;
+ return ds_frame;
+#endif
+}
+
+data_stack_frame_t t_push_named(const char *format, ...)
+{
+ data_stack_frame_t ret = t_push(format);
+#ifdef DEBUG
+ va_list args;
+ va_start(args, format);
+ current_frame->marker = p_strdup_vprintf(unsafe_data_stack_pool, format, args);
+ va_end(args);
+#else
+ (void)format; /* unused in non-DEBUG builds */
+#endif
+
+ return ret;
+}
+
+#ifdef DEBUG
+static void block_canary_check(struct stack_block *block)
+{
+ if (block->canary != BLOCK_CANARY) {
+ /* Make sure i_panic() won't try to allocate from the
+ same block by falling back onto our emergency block. */
+ current_block = &outofmem_area.block;
+ i_panic("Corrupted data stack canary");
+ }
+}
+#endif
+
+static void free_blocks(struct stack_block *block)
+{
+ struct stack_block *next;
+
+ /* free all the blocks, except if any of them is bigger than
+ unused_block, replace it */
+ while (block != NULL) {
+ block_canary_check(block);
+ next = block->next;
+
+#ifdef DEBUG
+ memset(STACK_BLOCK_DATA(block), CLEAR_CHR, block->size);
+#endif
+
+ if (block == &outofmem_area.block)
+ ;
+ else if (unused_block == NULL ||
+ block->size > unused_block->size) {
+ free(unused_block);
+ unused_block = block;
+ } else {
+ free(block);
+ }
+
+ block = next;
+ }
+}
+
+#ifdef DEBUG
+static void t_pop_verify(void)
+{
+ struct stack_block *block;
+ unsigned char *p;
+ size_t pos, max_pos, used_size;
+
+ block = current_frame->block;
+ pos = block->size - current_frame->block_space_left;
+ while (block != NULL) {
+ block_canary_check(block);
+ used_size = block->size - block->left;
+ p = STACK_BLOCK_DATA(block);
+ while (pos < used_size) {
+ size_t requested_size = *(size_t *)(p + pos);
+ if (used_size - pos < requested_size)
+ i_panic("data stack[%s]: saved alloc size broken",
+ current_frame->marker);
+ max_pos = pos + ALLOC_SIZE(requested_size);
+ pos += MEM_ALIGN(sizeof(size_t)) + requested_size;
+
+ for (; pos < max_pos; pos++) {
+ if (p[pos] != CLEAR_CHR)
+ i_panic("data stack[%s]: buffer overflow",
+ current_frame->marker);
+ }
+ }
+
+ /* if we had used t_buffer_get(), the rest of the buffer
+ may not contain CLEAR_CHRs. but we've already checked all
+ the allocations, so there's no need to check them anyway. */
+ block = block->next;
+ pos = 0;
+ }
+}
+#endif
+
+void t_pop_last_unsafe(void)
+{
+ size_t block_space_left;
+
+ if (unlikely(current_frame == NULL))
+ i_panic("t_pop() called with empty stack");
+
+ data_stack_last_buffer_reset(FALSE);
+#ifdef DEBUG
+ t_pop_verify();
+#endif
+
+ /* Usually the block doesn't change. If it doesn't, the next pointer
+ must also be NULL. */
+ if (current_block != current_frame->block) {
+ current_block = current_frame->block;
+ if (current_block->next != NULL) {
+ /* free unused blocks */
+ free_blocks(current_block->next);
+ current_block->next = NULL;
+ }
+ }
+ block_canary_check(current_block);
+
+ /* current_frame points inside the stack frame that will be freed.
+ make sure it's not accessed after it's already freed/cleaned. */
+ block_space_left = current_frame->block_space_left;
+ current_frame = current_frame->prev;
+
+#ifdef DEBUG
+ size_t start_pos, end_pos;
+
+ start_pos = current_block->size - block_space_left;
+ end_pos = current_block->size - current_block->left_lowwater;
+ i_assert(end_pos >= start_pos);
+ memset(STACK_BLOCK_DATA(current_block) + start_pos, CLEAR_CHR,
+ end_pos - start_pos);
+ current_block->left_lowwater = block_space_left;
+#endif
+
+ current_block->left = block_space_left;
+
+ data_stack_frame_id--;
+}
+
+bool t_pop(data_stack_frame_t *id)
+{
+ t_pop_last_unsafe();
+#ifndef STATIC_CHECKER
+ if (unlikely(data_stack_frame_id != *id))
+ return FALSE;
+ *id = 0;
+#else
+ unsigned int frame_id = (*id)->id;
+ i_free_and_null(*id);
+
+ if (unlikely(data_stack_frame_id != frame_id))
+ return FALSE;
+#endif
+ return TRUE;
+}
+
+bool t_pop_pass_str(data_stack_frame_t *id, const char **str)
+{
+ if (str == NULL || !data_stack_frame_contains(id, *str))
+ return t_pop(id);
+
+ /* FIXME: The string could be memmove()d to the beginning of the
+ data stack frame and the previous frame's size extended past it.
+ This would avoid the malloc. It's a bit complicated though. */
+ char *tmp_str = i_strdup(*str);
+ bool ret = t_pop(id);
+ *str = t_strdup(tmp_str);
+ i_free(tmp_str);
+ return ret;
+}
+
+static void mem_block_reset(struct stack_block *block)
+{
+ block->prev = NULL;
+ block->next = NULL;
+ block->left = block->size;
+#ifdef DEBUG
+ block->left_lowwater = block->size;
+#endif
+}
+
+static struct stack_block *mem_block_alloc(size_t min_size)
+{
+ struct stack_block *block;
+ size_t prev_size, alloc_size;
+
+ prev_size = current_block == NULL ? 0 : current_block->size;
+ /* Use INITIAL_STACK_SIZE without growing it to nearest power. */
+ alloc_size = prev_size == 0 ? min_size :
+ nearest_power(MALLOC_ADD(prev_size, min_size));
+
+ /* nearest_power() returns 2^n values, so alloc_size can't be
+ anywhere close to SIZE_MAX */
+ block = malloc(SIZEOF_MEMBLOCK + alloc_size);
+ if (unlikely(block == NULL)) {
+ if (outofmem) {
+ if (min_size > outofmem_area.block.left)
+ abort();
+ return &outofmem_area.block;
+ }
+ outofmem = TRUE;
+ i_panic("data stack: Out of memory when allocating %zu bytes",
+ alloc_size + SIZEOF_MEMBLOCK);
+ }
+ block->size = alloc_size;
+ block->canary = BLOCK_CANARY;
+ mem_block_reset(block);
+#ifdef DEBUG
+ memset(STACK_BLOCK_DATA(block), CLEAR_CHR, alloc_size);
+#endif
+ return block;
+}
+
+static void data_stack_send_grow_event(size_t last_alloc_size)
+{
+ if (event_datastack_deinitialized) {
+ /* already in the deinitialization code -
+ don't send more events */
+ return;
+ }
+ if (event_datastack == NULL)
+ event_datastack = event_create(NULL);
+ event_set_name(event_datastack, "data_stack_grow");
+ event_add_int(event_datastack, "alloc_size", data_stack_get_alloc_size());
+ event_add_int(event_datastack, "used_size", data_stack_get_used_size());
+ event_add_int(event_datastack, "last_alloc_size", last_alloc_size);
+ event_add_int(event_datastack, "last_block_size", current_block->size);
+#ifdef DEBUG
+ event_add_int(event_datastack, "frame_alloc_bytes",
+ current_frame->alloc_bytes);
+ event_add_int(event_datastack, "frame_alloc_count",
+ current_frame->alloc_count);
+#endif
+ event_add_str(event_datastack, "frame_marker", current_frame->marker);
+
+ /* It's possible that the data stack gets grown and shrunk rapidly.
+ Try to avoid doing expensive work if the event isn't even used for
+ anything. Note that at this point all the event fields must be
+ set already that might potentially be used by the filters. */
+ if (!event_want_debug(event_datastack))
+ return;
+
+ /* Getting backtrace is potentially inefficient, so do it after
+ checking if the event is wanted. Note that this prevents using the
+ backtrace field in event field comparisons. */
+ const char *backtrace;
+ if (backtrace_get(&backtrace) == 0)
+ event_add_str(event_datastack, "backtrace", backtrace);
+
+ string_t *str = t_str_new(128);
+ str_printfa(str, "total_used=%zu, total_alloc=%zu, last_alloc_size=%zu",
+ data_stack_get_used_size(),
+ data_stack_get_alloc_size(),
+ last_alloc_size);
+#ifdef DEBUG
+ str_printfa(str, ", frame_bytes=%llu, frame_alloc_count=%u",
+ current_frame->alloc_bytes, current_frame->alloc_count);
+#endif
+ e_debug(event_datastack, "Growing data stack by %zu for '%s' (%s)",
+ current_block->size, current_frame->marker,
+ str_c(str));
+}
+
+static void *t_malloc_real(size_t size, bool permanent)
+{
+ void *ret;
+ size_t alloc_size;
+ bool warn = FALSE;
+#ifdef DEBUG
+ int old_errno = errno;
+#endif
+
+ if (unlikely(size == 0 || size > SSIZE_T_MAX))
+ i_panic("Trying to allocate %zu bytes", size);
+
+ if (unlikely(!data_stack_initialized)) {
+ /* kludgy, but allow this before initialization */
+ data_stack_init();
+ }
+ block_canary_check(current_block);
+
+ /* allocate only aligned amount of memory so alignment comes
+ always properly */
+ alloc_size = ALLOC_SIZE(size);
+#ifdef DEBUG
+ if(permanent) {
+ current_frame->alloc_bytes += alloc_size;
+ current_frame->alloc_count++;
+ }
+#endif
+ data_stack_last_buffer_reset(TRUE);
+
+ if (permanent) {
+ /* used for t_try_realloc() */
+ current_frame->last_alloc_size = alloc_size;
+ }
+
+ if (current_block->left < alloc_size) {
+ struct stack_block *block;
+
+ /* current block is full, see if we can use the unused_block */
+ if (unused_block != NULL && unused_block->size >= alloc_size) {
+ block = unused_block;
+ unused_block = NULL;
+ mem_block_reset(block);
+ } else {
+ /* current block is full, allocate a new one */
+ block = mem_block_alloc(alloc_size);
+ warn = TRUE;
+ }
+
+ /* The newly allocated block will replace the current_block,
+ i.e. current_block always points to the last element in
+ the linked list. */
+ block->prev = current_block;
+ current_block->next = block;
+ current_block = block;
+ }
+
+ /* enough space in current block, use it */
+ ret = data_stack_after_last_alloc(current_block);
+
+#ifdef DEBUG
+ if (current_block->left - alloc_size < current_block->left_lowwater)
+ current_block->left_lowwater = current_block->left - alloc_size;
+#endif
+ if (permanent)
+ current_block->left -= alloc_size;
+
+ if (warn) T_BEGIN {
+ /* sending event can cause errno changes. */
+#ifdef DEBUG
+ i_assert(errno == old_errno);
+#else
+ int old_errno = errno;
+#endif
+ /* warn after allocation, so if e_debug() wants to
+ allocate more memory we don't go to infinite loop */
+ data_stack_send_grow_event(alloc_size);
+ /* reset errno back to what it was */
+ errno = old_errno;
+ } T_END;
+#ifdef DEBUG
+ memcpy(ret, &size, sizeof(size));
+ ret = PTR_OFFSET(ret, MEM_ALIGN(sizeof(size)));
+ /* make sure the sentry contains CLEAR_CHRs. it might not if we
+ had used t_buffer_get(). */
+ memset(PTR_OFFSET(ret, size), CLEAR_CHR,
+ MEM_ALIGN(size + SENTRY_COUNT) - size);
+
+ /* we rely on errno not changing. it shouldn't. */
+ i_assert(errno == old_errno);
+#endif
+ return ret;
+}
+
+void *t_malloc_no0(size_t size)
+{
+ return t_malloc_real(size, TRUE);
+}
+
+void *t_malloc0(size_t size)
+{
+ void *mem;
+
+ mem = t_malloc_real(size, TRUE);
+ memset(mem, 0, size);
+ return mem;
+}
+
+bool ATTR_NO_SANITIZE_INTEGER
+t_try_realloc(void *mem, size_t size)
+{
+ size_t debug_adjust = 0, last_alloc_size;
+ unsigned char *after_last_alloc;
+
+ if (unlikely(size == 0 || size > SSIZE_T_MAX))
+ i_panic("Trying to allocate %zu bytes", size);
+ block_canary_check(current_block);
+ data_stack_last_buffer_reset(TRUE);
+
+ last_alloc_size = current_frame->last_alloc_size;
+
+ /* see if we're trying to grow the memory we allocated last */
+ after_last_alloc = data_stack_after_last_alloc(current_block);
+#ifdef DEBUG
+ debug_adjust = MEM_ALIGN(sizeof(size_t));
+#endif
+ if (after_last_alloc - last_alloc_size + debug_adjust == mem) {
+ /* yeah, see if we have space to grow */
+ size_t new_alloc_size, alloc_growth;
+
+ new_alloc_size = ALLOC_SIZE(size);
+ alloc_growth = (new_alloc_size - last_alloc_size);
+#ifdef DEBUG
+ size_t old_raw_size; /* sorry, non-C99 users - add braces if you need them */
+ old_raw_size = *(size_t *)PTR_OFFSET(mem, -(ptrdiff_t)MEM_ALIGN(sizeof(size_t)));
+ i_assert(ALLOC_SIZE(old_raw_size) == last_alloc_size);
+ /* Only check one byte for over-run, that catches most
+ offenders who are likely to use t_try_realloc() */
+ i_assert(((unsigned char*)mem)[old_raw_size] == CLEAR_CHR);
+#endif
+
+ if (current_block->left >= alloc_growth) {
+ /* just shrink the available size */
+ current_block->left -= alloc_growth;
+ current_frame->last_alloc_size = new_alloc_size;
+#ifdef DEBUG
+ if (current_block->left < current_block->left_lowwater)
+ current_block->left_lowwater = current_block->left;
+ /* All reallocs are permanent by definition
+ However, they don't count as a new allocation */
+ current_frame->alloc_bytes += alloc_growth;
+ *(size_t *)PTR_OFFSET(mem, -(ptrdiff_t)MEM_ALIGN(sizeof(size_t))) = size;
+ memset(PTR_OFFSET(mem, size), CLEAR_CHR,
+ new_alloc_size - size - MEM_ALIGN(sizeof(size_t)));
+#endif
+ return TRUE;
+ }
+ }
+
+ return FALSE;
+}
+
+size_t t_get_bytes_available(void)
+{
+ block_canary_check(current_block);
+#ifndef DEBUG
+ const unsigned int min_extra = 0;
+#else
+ const unsigned int min_extra = SENTRY_COUNT + MEM_ALIGN(sizeof(size_t));
+ if (current_block->left < min_extra)
+ return 0;
+#endif
+ size_t size = current_block->left - min_extra;
+ i_assert(ALLOC_SIZE(size) == current_block->left);
+ return size;
+}
+
+void *t_buffer_get(size_t size)
+{
+ void *ret;
+
+ ret = t_malloc_real(size, FALSE);
+
+ last_buffer_size = size;
+ last_buffer_block = current_block;
+ return ret;
+}
+
+void *t_buffer_reget(void *buffer, size_t size)
+{
+ size_t old_size;
+ void *new_buffer;
+
+ old_size = last_buffer_size;
+ if (size <= old_size)
+ return buffer;
+
+ new_buffer = t_buffer_get(size);
+ if (new_buffer != buffer)
+ memcpy(new_buffer, buffer, old_size);
+
+ return new_buffer;
+}
+
+void t_buffer_alloc(size_t size)
+{
+ i_assert(last_buffer_block != NULL);
+ i_assert(last_buffer_size >= size);
+ i_assert(current_block->left >= size);
+
+ /* we've already reserved the space, now we just mark it used */
+ (void)t_malloc_real(size, TRUE);
+}
+
+void t_buffer_alloc_last_full(void)
+{
+ if (last_buffer_block != NULL)
+ (void)t_malloc_real(last_buffer_size, TRUE);
+}
+
+bool data_stack_frame_contains(data_stack_frame_t *id, const void *_ptr)
+{
+ const unsigned char *block_data, *ptr = _ptr;
+ const struct stack_block *block;
+ unsigned int wanted_frame_id;
+ size_t block_start_pos, block_used;
+
+ /* first handle the fast path - NULL can never be within the frame */
+ if (ptr == NULL)
+ return FALSE;
+
+#ifndef STATIC_CHECKER
+ wanted_frame_id = *id;
+#else
+ wanted_frame_id = (*id)->id;
+#endif
+ /* Too much effort to support more than the latest frame.
+ It's the only thing that is currently needed anyway. */
+ i_assert(wanted_frame_id+1 == data_stack_frame_id);
+ block = current_frame->block;
+ i_assert(block != NULL);
+
+ /* See if it's in the frame's first block. Only the data after
+ block_start_pos belong to this frame. */
+ block_data = STACK_BLOCK_DATA(block);
+ block_start_pos = block->size - current_frame->block_space_left;
+ block_used = block->size - block->left;
+ if (ptr >= block_data + block_start_pos &&
+ ptr <= block_data + block_used)
+ return TRUE;
+
+ /* See if it's in the other blocks. All the data in them belong to
+ this frame. */
+ for (block = block->next; block != NULL; block = block->next) {
+ block_data = STACK_BLOCK_DATA(block);
+ block_used = block->size - block->left;
+ if (ptr >= block_data && ptr < block_data + block_used)
+ return TRUE;
+ }
+ return FALSE;
+}
+
+size_t data_stack_get_alloc_size(void)
+{
+ struct stack_block *block;
+ size_t size = 0;
+
+ i_assert(current_block->next == NULL);
+
+ for (block = current_block; block != NULL; block = block->prev)
+ size += block->size;
+ return size;
+}
+
+size_t data_stack_get_used_size(void)
+{
+ struct stack_block *block;
+ size_t size = 0;
+
+ i_assert(current_block->next == NULL);
+
+ for (block = current_block; block != NULL; block = block->prev)
+ size += block->size - block->left;
+ return size;
+}
+
+void data_stack_free_unused(void)
+{
+ free(unused_block);
+ unused_block = NULL;
+}
+
+void data_stack_init(void)
+{
+ if (data_stack_initialized) {
+ /* already initialized (we did auto-initialization in
+ t_malloc/t_push) */
+ return;
+ }
+ data_stack_initialized = TRUE;
+ data_stack_frame_id = 1;
+
+ outofmem_area.block.size = outofmem_area.block.left =
+ sizeof(outofmem_area) - sizeof(outofmem_area.block);
+ outofmem_area.block.canary = BLOCK_CANARY;
+
+ current_block = mem_block_alloc(INITIAL_STACK_SIZE);
+ current_frame = NULL;
+
+ last_buffer_block = NULL;
+ last_buffer_size = 0;
+
+ root_frame_id = t_push("data_stack_init");
+}
+
+void data_stack_deinit_event(void)
+{
+ event_unref(&event_datastack);
+ event_datastack_deinitialized = TRUE;
+}
+
+void data_stack_deinit(void)
+{
+ if (!t_pop(&root_frame_id) ||
+ current_frame != NULL)
+ i_panic("Missing t_pop() call");
+
+ free(current_block);
+ current_block = NULL;
+ data_stack_free_unused();
+}
diff --git a/src/lib/data-stack.h b/src/lib/data-stack.h
new file mode 100644
index 0000000..ba2b566
--- /dev/null
+++ b/src/lib/data-stack.h
@@ -0,0 +1,165 @@
+#ifndef DATA_STACK_H
+#define DATA_STACK_H
+
+/* Data stack makes it very easy to implement functions returning dynamic data
+ without having to worry much about memory management like freeing the
+ result or having large enough buffers for the result.
+
+ t_ prefix was chosen to describe functions allocating memory from data
+ stack. "t" meaning temporary.
+
+ Advantages over control stack and alloca():
+ - Functions can return a value allocated from data stack
+ - We can portably specify how much data we want to allocate at runtime
+
+ Advantages over malloc():
+ - FAST, most of the time allocating memory means only updating a couple of
+ pointers and integers. Freeing the memory all at once also is a fast
+ operation.
+ - No need to free() each allocation resulting in prettier code
+ - No memory leaks
+ - No memory fragmentation
+
+ Disadvantages:
+ - Allocating memory inside loops can accidentally allocate a lot of memory
+ if the loops are long and you forgot to place t_push() and t_pop() there.
+ - t_malloc()ed data could be accidentally stored into permanent location
+ and accessed after it's already been freed. const'ing the return values
+ helps for most uses though (see the t_malloc() description).
+ - Debugging invalid memory usage may be difficult using existing tools,
+ although compiling with DEBUG enabled helps finding simple buffer
+ overflows.
+*/
+
+#ifndef STATIC_CHECKER
+typedef unsigned int data_stack_frame_t;
+#else
+typedef struct data_stack_frame *data_stack_frame_t;
+#endif
+
+extern unsigned int data_stack_frame_id;
+
+/* All t_..() allocations between t_push*() and t_pop() are freed after t_pop()
+ is called. Returns the current stack frame number, which can be used
+ to detect missing t_pop() calls:
+
+ x = t_push(marker); .. if (!t_pop(x)) abort();
+
+ In DEBUG mode, t_push_named() makes a temporary allocation for the name,
+ but is safe to call in a loop as it performs the allocation within its own
+ frame. However, you should always prefer to use T_BEGIN { ... } T_END below.
+*/
+data_stack_frame_t t_push(const char *marker) ATTR_HOT;
+data_stack_frame_t t_push_named(const char *format, ...) ATTR_HOT ATTR_FORMAT(1, 2);
+/* Returns TRUE on success, FALSE if t_pop() call was leaked. The caller
+ should panic. */
+bool t_pop(data_stack_frame_t *id) ATTR_HOT;
+/* Same as t_pop(), but move str out of the stack frame if it is inside.
+ This can be used to easily move e.g. error strings outside stack frames. */
+bool t_pop_pass_str(data_stack_frame_t *id, const char **str);
+
+/* Pop the last data stack frame. This shouldn't be called outside test code. */
+void t_pop_last_unsafe(void);
+
+/* Usage: T_BEGIN { code } T_END */
+#define T_STRING(x) #x
+#define T_XSTRING(x) T_STRING(x) /* expand and then stringify */
+#define T_BEGIN \
+ STMT_START { \
+ data_stack_frame_t _data_stack_cur_id = t_push(__FILE__ ":" T_XSTRING(__LINE__));
+#define T_END \
+ STMT_START { \
+ if (unlikely(!t_pop(&_data_stack_cur_id))) \
+ i_panic("Leaked t_pop() call"); \
+ } STMT_END; \
+ } STMT_END
+
+/* Usage:
+ const char *error;
+ T_BEGIN {
+ ...
+ if (ret < 0)
+ error = t_strdup_printf("foo() failed: %m");
+ } T_END_PASS_STR_IF(ret < 0, &error);
+ // error is still valid
+*/
+#define T_END_PASS_STR_IF(pass_condition, str) \
+ STMT_START { \
+ if (unlikely(!t_pop_pass_str(&_data_stack_cur_id, (pass_condition) ? (str) : NULL))) \
+ i_panic("Leaked t_pop() call"); \
+ } STMT_END; \
+ } STMT_END
+/*
+ Usage:
+ const char *result;
+ T_BEGIN {
+ ...
+ result = t_strdup_printf(...);
+ } T_END_PASS_STR(&result);
+ // result is still valid
+*/
+#define T_END_PASS_STR(str) \
+ T_END_PASS_STR_IF(TRUE, str)
+
+/* WARNING: Be careful when using these functions, it's too easy to
+ accidentally save the returned value somewhere permanently.
+
+ You probably should never use these functions directly, rather
+ create functions that return 'const xxx*' types and use t_malloc()
+ internally in them. This is a lot safer, since usually compiler
+ warns if you try to place them in xxx*. See strfuncs.c for examples.
+
+ t_malloc() calls never fail. If there's not enough memory left,
+ i_panic() will be called. */
+void *t_malloc_no0(size_t size) ATTR_MALLOC ATTR_RETURNS_NONNULL;
+void *t_malloc0(size_t size) ATTR_MALLOC ATTR_RETURNS_NONNULL;
+
+/* Try growing allocated memory. Returns TRUE if successful. Works only
+ for last allocated memory in current stack frame. */
+bool t_try_realloc(void *mem, size_t size);
+
+/* Returns the number of bytes available in data stack without allocating
+ more memory. */
+size_t t_get_bytes_available(void) ATTR_PURE;
+
+#define t_new(type, count) \
+ ((type *) t_malloc0(MALLOC_MULTIPLY((unsigned int)sizeof(type), (count))) + \
+ COMPILE_ERROR_IF_TRUE(sizeof(type) > UINT_MAX))
+
+/* Returns pointer to a temporary buffer you can use. The buffer will be
+ invalid as soon as next t_malloc() is called!
+
+ If you wish to grow the buffer, you must give the full wanted size
+ in the size parameter. If return value doesn't point to the same value
+ as last time, you need to memcpy() data from the old buffer to the
+ new one (or do some other trickery). See t_buffer_reget(). */
+void *t_buffer_get(size_t size) ATTR_RETURNS_NONNULL;
+
+/* Grow the buffer, memcpy()ing the memory to new location if needed. */
+void *t_buffer_reget(void *buffer, size_t size) ATTR_RETURNS_NONNULL;
+
+/* Make the last t_buffer_get()ed buffer permanent. Note that size MUST be
+ less or equal than the size you gave with last t_buffer_get() or the
+ result will be undefined. */
+void t_buffer_alloc(size_t size);
+/* Allocate the last t_buffer_get()ed data entirely. */
+void t_buffer_alloc_last_full(void);
+
+/* Returns TRUE if ptr is allocated within the given data stack frame.
+ Currently this assert-crashes if the data stack frame isn't the latest. */
+bool data_stack_frame_contains(data_stack_frame_t *id, const void *ptr);
+
+/* Returns the number of bytes malloc()ated for data stack. */
+size_t data_stack_get_alloc_size(void);
+/* Returns the number of bytes currently used in data stack. */
+size_t data_stack_get_used_size(void);
+
+/* Free all the memory that is currently unused (i.e. reserved for growing
+ data stack quickly). */
+void data_stack_free_unused(void);
+
+void data_stack_init(void);
+void data_stack_deinit_event(void);
+void data_stack_deinit(void);
+
+#endif
diff --git a/src/lib/eacces-error.c b/src/lib/eacces-error.c
new file mode 100644
index 0000000..954ea78
--- /dev/null
+++ b/src/lib/eacces-error.c
@@ -0,0 +1,310 @@
+/* Copyright (c) 2007-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "str.h"
+#include "path-util.h"
+#include "ipwd.h"
+#include "restrict-access.h"
+#include "eacces-error.h"
+
+#include <sys/stat.h>
+#include <unistd.h>
+
+static bool is_in_group(gid_t gid)
+{
+ const gid_t *gids;
+ unsigned int i, count;
+
+ if (getegid() == gid)
+ return TRUE;
+
+ gids = restrict_get_groups_list(&count);
+ for (i = 0; i < count; i++) {
+ if (gids[i] == gid)
+ return TRUE;
+ }
+ return FALSE;
+}
+
+static void write_eacces_error(string_t *errmsg, const char *path, int mode)
+{
+ char c;
+
+ switch (mode) {
+ case R_OK:
+ c = 'r';
+ break;
+ case W_OK:
+ c = 'w';
+ break;
+ case X_OK:
+ c = 'x';
+ break;
+ default:
+ i_unreached();
+ }
+ str_printfa(errmsg, " missing +%c perm: %s", c, path);
+}
+
+static int
+test_manual_access(const char *path, int access_mode, bool write_eacces,
+ string_t *errmsg)
+{
+ const struct group *group;
+ bool user_not_in_group = FALSE;
+ struct stat st;
+ int mode;
+
+ if (stat(path, &st) < 0) {
+ str_printfa(errmsg, " stat(%s) failed: %m", path);
+ return -1;
+ }
+
+ switch (access_mode) {
+ case R_OK:
+ mode = 04;
+ break;
+ case W_OK:
+ mode = 02;
+ break;
+ case X_OK:
+ mode = 01;
+ break;
+ default:
+ i_unreached();
+ }
+
+ if (st.st_uid == geteuid())
+ st.st_mode = (st.st_mode & 0700) >> 6;
+ else if (is_in_group(st.st_gid))
+ st.st_mode = (st.st_mode & 0070) >> 3;
+ else {
+ if ((((st.st_mode & 0070) >> 3) & mode) != 0)
+ user_not_in_group = TRUE;
+ st.st_mode = (st.st_mode & 0007);
+ }
+
+ if ((st.st_mode & mode) != 0)
+ return 0;
+
+ if (write_eacces)
+ write_eacces_error(errmsg, path, access_mode);
+ if (user_not_in_group) {
+ /* group would have had enough permissions,
+ but we don't belong to the group */
+ str_printfa(errmsg, ", we're not in group %s",
+ dec2str(st.st_gid));
+ group = getgrgid(st.st_gid);
+ if (group != NULL)
+ str_printfa(errmsg, "(%s)", group->gr_name);
+ }
+ errno = EACCES;
+ return -1;
+}
+
+static int test_access(const char *path, int access_mode, string_t *errmsg)
+{
+ struct stat st;
+
+ if (getuid() == geteuid()) {
+ if (access(path, access_mode) == 0)
+ return 0;
+ if (errno == EACCES) {
+ write_eacces_error(errmsg, path, access_mode);
+ if (test_manual_access(path, access_mode,
+ FALSE, errmsg) == 0) {
+ str_append(errmsg, ", UNIX perms appear ok "
+ "(ACL/MAC wrong?)");
+ }
+ errno = EACCES;
+ } else {
+ str_printfa(errmsg, ", access(%s, %d) failed: %m",
+ path, access_mode);
+ }
+ return -1;
+ }
+
+ /* access() uses real uid, not effective uid.
+ we'll have to do these checks manually. */
+ switch (access_mode) {
+ case X_OK:
+ if (stat(t_strconcat(path, "/test", NULL), &st) == 0)
+ return 0;
+ if (errno == ENOENT || errno == ENOTDIR)
+ return 0;
+ if (errno == EACCES)
+ write_eacces_error(errmsg, path, access_mode);
+ else
+ str_printfa(errmsg, ", stat(%s/test) failed: %m", path);
+ return -1;
+ case R_OK:
+ case W_OK:
+ break;
+ default:
+ i_unreached();
+ }
+
+ return test_manual_access(path, access_mode, TRUE, errmsg);
+}
+
+static const char *
+eacces_error_get_full(const char *func, const char *path, bool creating)
+{
+ const char *prev_path, *dir = NULL, *p;
+ const char *pw_name = NULL, *gr_name = NULL;
+ struct passwd pw;
+ struct group group;
+ string_t *errmsg;
+ struct stat st;
+ int orig_errno, ret, missing_mode = 0;
+
+ orig_errno = errno;
+ errmsg = t_str_new(256);
+ str_printfa(errmsg, "%s(%s)", func, path);
+ if (*path != '/') {
+ const char *error;
+ if (t_get_working_dir(&dir, &error) < 0) {
+ i_error("eacces_error_get_full: %s", error);
+ str_printfa(errmsg, " in an unknown directory");
+ } else {
+ str_printfa(errmsg, " in directory %s", dir);
+ path = t_strconcat(dir, "/", path, NULL);
+ }
+ }
+ str_printfa(errmsg, " failed: Permission denied (euid=%s",
+ dec2str(geteuid()));
+
+ switch (i_getpwuid(geteuid(), &pw)) {
+ case -1:
+ str_append(errmsg, "(<getpwuid() error>)");
+ break;
+ case 0:
+ str_append(errmsg, "(<unknown>)");
+ break;
+ default:
+ pw_name = t_strdup(pw.pw_name);
+ str_printfa(errmsg, "(%s)", pw_name);
+ break;
+ }
+
+ str_printfa(errmsg, " egid=%s", dec2str(getegid()));
+ switch (i_getgrgid(getegid(), &group)) {
+ case -1:
+ str_append(errmsg, "(<getgrgid() error>)");
+ break;
+ case 0:
+ str_append(errmsg, "(<unknown>)");
+ break;
+ default:
+ gr_name = t_strdup(group.gr_name);
+ str_printfa(errmsg, "(%s)", gr_name);
+ break;
+ }
+
+ prev_path = path; ret = -1;
+ while (strcmp(prev_path, "/") != 0) {
+ if ((p = strrchr(prev_path, '/')) == NULL)
+ break;
+
+ dir = t_strdup_until(prev_path, p);
+ ret = stat(dir, &st);
+ if (ret == 0)
+ break;
+ if (errno == EACCES && strcmp(dir, "/") != 0) {
+ /* see if we have access to parent directory */
+ } else if (errno == ENOENT && creating &&
+ strcmp(dir, "/") != 0) {
+ /* probably mkdir_parents() failed here, find the first
+ parent directory we couldn't create */
+ } else {
+ /* some other error, can't handle it */
+ str_printfa(errmsg, " stat(%s) failed: %m", dir);
+ break;
+ }
+ prev_path = dir;
+ }
+
+ if (ret == 0) {
+ /* dir is the first parent directory we can stat() */
+ if (test_access(dir, X_OK, errmsg) < 0) {
+ if (errno == EACCES)
+ missing_mode = 1;
+ } else if (creating && test_access(dir, W_OK, errmsg) < 0) {
+ if (errno == EACCES)
+ missing_mode = 2;
+ } else if (prev_path == path &&
+ test_access(path, R_OK, errmsg) < 0) {
+ } else if (!creating && test_access(path, W_OK, errmsg) < 0) {
+ /* this produces a wrong error if the operation didn't
+ actually need write permissions, but we don't know
+ it here.. */
+ if (errno == EACCES)
+ missing_mode = 4;
+ } else {
+ str_append(errmsg, " UNIX perms appear ok "
+ "(ACL/MAC wrong?)");
+ }
+ }
+ if (ret < 0)
+ ;
+ else if (st.st_uid != geteuid()) {
+ if (pw_name != NULL && i_getpwuid(st.st_uid, &pw) > 0 &&
+ strcmp(pw.pw_name, pw_name) == 0) {
+ str_printfa(errmsg, ", conflicting dir uid=%s(%s)",
+ dec2str(st.st_uid), pw_name);
+ } else {
+ str_printfa(errmsg, ", dir owned by %s:%s mode=0%o",
+ dec2str(st.st_uid), dec2str(st.st_gid),
+ (unsigned int)(st.st_mode & 0777));
+ }
+ } else if (missing_mode != 0 &&
+ (((st.st_mode & 0700) >> 6) & missing_mode) == 0) {
+ str_append(errmsg, ", dir owner missing perms");
+ }
+ if (ret == 0 && gr_name != NULL && st.st_gid != getegid()) {
+ if (i_getgrgid(st.st_gid, &group) > 0 &&
+ strcmp(group.gr_name, gr_name) == 0) {
+ str_printfa(errmsg, ", conflicting dir gid=%s(%s)",
+ dec2str(st.st_gid), gr_name);
+ }
+ }
+ str_append_c(errmsg, ')');
+ errno = orig_errno;
+ return str_c(errmsg);
+}
+
+const char *eacces_error_get(const char *func, const char *path)
+{
+ return eacces_error_get_full(func, path, FALSE);
+}
+
+const char *eacces_error_get_creating(const char *func, const char *path)
+{
+ return eacces_error_get_full(func, path, TRUE);
+}
+
+const char *eperm_error_get_chgrp(const char *func, const char *path,
+ gid_t gid, const char *gid_origin)
+{
+ string_t *errmsg;
+ const struct group *group;
+ int orig_errno = errno;
+
+ errmsg = t_str_new(256);
+
+ str_printfa(errmsg, "%s(%s, group=%s", func, path, dec2str(gid));
+ group = getgrgid(gid);
+ if (group != NULL)
+ str_printfa(errmsg, "(%s)", group->gr_name);
+
+ str_printfa(errmsg, ") failed: Operation not permitted (egid=%s",
+ dec2str(getegid()));
+ group = getgrgid(getegid());
+ if (group != NULL)
+ str_printfa(errmsg, "(%s)", group->gr_name);
+ if (gid_origin != NULL)
+ str_printfa(errmsg, ", group based on %s", gid_origin);
+ str_append(errmsg, " - see http://wiki2.dovecot.org/Errors/ChgrpNoPerm)");
+ errno = orig_errno;
+ return str_c(errmsg);
+}
diff --git a/src/lib/eacces-error.h b/src/lib/eacces-error.h
new file mode 100644
index 0000000..a15b406
--- /dev/null
+++ b/src/lib/eacces-error.h
@@ -0,0 +1,14 @@
+#ifndef EACCES_ERROR_H
+#define EACCES_ERROR_H
+
+/* Return a user-friendly error message for EACCES failures. */
+const char *eacces_error_get(const char *func, const char *path);
+const char *eacces_error_get_creating(const char *func, const char *path);
+/* Return a user-friendly error message for fchown() or chown() EPERM
+ failures when only the group is being changed. gid_origin specifies why
+ exactly this group is being used. */
+const char *eperm_error_get_chgrp(const char *func, const char *path,
+ gid_t gid, const char *gid_origin)
+ ATTR_NULL(4);
+
+#endif
diff --git a/src/lib/env-util.c b/src/lib/env-util.c
new file mode 100644
index 0000000..490e4f6
--- /dev/null
+++ b/src/lib/env-util.c
@@ -0,0 +1,140 @@
+/* Copyright (c) 2002-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "env-util.h"
+
+#ifdef __APPLE__
+# include <crt_externs.h>
+#endif
+
+struct env_backup {
+ pool_t pool;
+ const char **strings;
+};
+
+void env_put(const char *name, const char *value)
+{
+ i_assert(strchr(name, '=') == NULL);
+
+ if (setenv(name, value, 1) != 0)
+ i_fatal("setenv(%s, %s) failed: %m", name, value);
+}
+
+void env_put_array(const char *const *envs)
+{
+ for (unsigned int i = 0; envs[i] != NULL; i++) {
+ const char *value = strchr(envs[i], '=');
+ i_assert(value != NULL);
+ T_BEGIN {
+ const char *name = t_strdup_until(envs[i], value++);
+ env_put(name, value);
+ } T_END;
+ }
+}
+
+void env_remove(const char *name)
+{
+ if (unsetenv(name) < 0)
+ i_fatal("unsetenv(%s) failed: %m", name);
+}
+
+void env_clean(void)
+{
+#ifdef HAVE_CLEARENV
+ if (clearenv() < 0)
+ i_fatal("clearenv() failed");
+#else
+ char ***environ_p = env_get_environ_p();
+
+ /* Try to clear the environment.
+
+ a) environ = NULL crashes on OS X.
+ b) *environ = NULL doesn't work on FreeBSD 7.0.
+ c) environ = emptyenv doesn't work on Haiku OS
+ d) environ = calloc() should work everywhere
+ */
+ *environ_p = calloc(1, sizeof(**environ_p));
+#endif
+}
+
+static void env_clean_except_real(const char *const preserve_envs[])
+{
+ ARRAY_TYPE(const_string) copy;
+ const char *value, *const *envp;
+ unsigned int i, count;
+
+ t_array_init(&copy, 16);
+ for (i = 0; preserve_envs[i] != NULL; i++) {
+ const char *key = preserve_envs[i];
+
+ value = getenv(key);
+ if (value != NULL) {
+ key = t_strdup(key);
+ value = t_strdup(value);
+ array_push_back(&copy, &key);
+ array_push_back(&copy, &value);
+ }
+ }
+
+ /* Note that if the original environment was set with env_put(), the
+ environment strings will be invalid after env_clean(). That's why
+ we t_strdup() them above. */
+ env_clean();
+
+ envp = array_get(&copy, &count);
+ for (i = 0; i < count; i += 2)
+ env_put(envp[i], envp[i+1]);
+}
+
+void env_clean_except(const char *const preserve_envs[])
+{
+ T_BEGIN {
+ env_clean_except_real(preserve_envs);
+ } T_END;
+}
+
+struct env_backup *env_backup_save(void)
+{
+ char **environ = *env_get_environ_p();
+ struct env_backup *env;
+ unsigned int i, count;
+ pool_t pool;
+
+ i_assert(environ != NULL);
+
+ for (count = 0; environ[count] != NULL; count++) ;
+
+ pool = pool_alloconly_create("saved environment", 4096);
+ env = p_new(pool, struct env_backup, 1);
+ env->pool = pool;
+ env->strings = p_new(pool, const char *, count + 1);
+ for (i = 0; i < count; i++)
+ env->strings[i] = p_strdup(pool, environ[i]);
+ return env;
+}
+
+void env_backup_restore(struct env_backup *env)
+{
+ env_clean();
+ env_put_array(env->strings);
+}
+
+void env_backup_free(struct env_backup **_env)
+{
+ struct env_backup *env = *_env;
+
+ *_env = NULL;
+ pool_unref(&env->pool);
+}
+
+char ***env_get_environ_p(void)
+{
+#ifdef __APPLE__
+ return _NSGetEnviron();
+#else
+ extern char **environ;
+
+ return &environ;
+#endif
+}
diff --git a/src/lib/env-util.h b/src/lib/env-util.h
new file mode 100644
index 0000000..e5b87de
--- /dev/null
+++ b/src/lib/env-util.h
@@ -0,0 +1,30 @@
+#ifndef ENV_UTIL_H
+#define ENV_UTIL_H
+
+/* Add a new environment variable or replace an existing one.
+ Wrapper to setenv(). Note that setenv() often doesn't free memory used by
+ replaced environment, so don't keep repeatedly changing values in
+ environment. */
+void env_put(const char *name, const char *value);
+/* env_put() NULL-terminated array of name=value strings */
+void env_put_array(const char *const *envs);
+/* Remove a single environment. */
+void env_remove(const char *name);
+/* Clear all environment variables. */
+void env_clean(void);
+/* Clear all environment variables except what's listed in preserve_envs[] */
+void env_clean_except(const char *const preserve_envs[]);
+
+/* Save a copy of the current environment. */
+struct env_backup *env_backup_save(void);
+/* Clear the current environment and restore the backup. */
+void env_backup_restore(struct env_backup *env);
+/* Free the memory used by environment backup. */
+void env_backup_free(struct env_backup **env);
+
+/* Returns the value of "&environ". This is more portable than using it
+ directly. */
+char ***env_get_environ_p(void);
+
+
+#endif
diff --git a/src/lib/event-filter-lexer.c b/src/lib/event-filter-lexer.c
new file mode 100644
index 0000000..32b8cd8
--- /dev/null
+++ b/src/lib/event-filter-lexer.c
@@ -0,0 +1,2305 @@
+#line 2 "event-filter-lexer.c"
+
+#line 4 "event-filter-lexer.c"
+
+#define YY_INT_ALIGNED short int
+
+/* A lexical scanner generated by flex */
+
+#define FLEX_SCANNER
+#define YY_FLEX_MAJOR_VERSION 2
+#define YY_FLEX_MINOR_VERSION 6
+#define YY_FLEX_SUBMINOR_VERSION 4
+#if YY_FLEX_SUBMINOR_VERSION > 0
+#define FLEX_BETA
+#endif
+
+#ifdef yy_create_buffer
+#define event_filter_parser__create_buffer_ALREADY_DEFINED
+#else
+#define yy_create_buffer event_filter_parser__create_buffer
+#endif
+
+#ifdef yy_delete_buffer
+#define event_filter_parser__delete_buffer_ALREADY_DEFINED
+#else
+#define yy_delete_buffer event_filter_parser__delete_buffer
+#endif
+
+#ifdef yy_scan_buffer
+#define event_filter_parser__scan_buffer_ALREADY_DEFINED
+#else
+#define yy_scan_buffer event_filter_parser__scan_buffer
+#endif
+
+#ifdef yy_scan_string
+#define event_filter_parser__scan_string_ALREADY_DEFINED
+#else
+#define yy_scan_string event_filter_parser__scan_string
+#endif
+
+#ifdef yy_scan_bytes
+#define event_filter_parser__scan_bytes_ALREADY_DEFINED
+#else
+#define yy_scan_bytes event_filter_parser__scan_bytes
+#endif
+
+#ifdef yy_init_buffer
+#define event_filter_parser__init_buffer_ALREADY_DEFINED
+#else
+#define yy_init_buffer event_filter_parser__init_buffer
+#endif
+
+#ifdef yy_flush_buffer
+#define event_filter_parser__flush_buffer_ALREADY_DEFINED
+#else
+#define yy_flush_buffer event_filter_parser__flush_buffer
+#endif
+
+#ifdef yy_load_buffer_state
+#define event_filter_parser__load_buffer_state_ALREADY_DEFINED
+#else
+#define yy_load_buffer_state event_filter_parser__load_buffer_state
+#endif
+
+#ifdef yy_switch_to_buffer
+#define event_filter_parser__switch_to_buffer_ALREADY_DEFINED
+#else
+#define yy_switch_to_buffer event_filter_parser__switch_to_buffer
+#endif
+
+#ifdef yypush_buffer_state
+#define event_filter_parser_push_buffer_state_ALREADY_DEFINED
+#else
+#define yypush_buffer_state event_filter_parser_push_buffer_state
+#endif
+
+#ifdef yypop_buffer_state
+#define event_filter_parser_pop_buffer_state_ALREADY_DEFINED
+#else
+#define yypop_buffer_state event_filter_parser_pop_buffer_state
+#endif
+
+#ifdef yyensure_buffer_stack
+#define event_filter_parser_ensure_buffer_stack_ALREADY_DEFINED
+#else
+#define yyensure_buffer_stack event_filter_parser_ensure_buffer_stack
+#endif
+
+#ifdef yylex
+#define event_filter_parser_lex_ALREADY_DEFINED
+#else
+#define yylex event_filter_parser_lex
+#endif
+
+#ifdef yyrestart
+#define event_filter_parser_restart_ALREADY_DEFINED
+#else
+#define yyrestart event_filter_parser_restart
+#endif
+
+#ifdef yylex_init
+#define event_filter_parser_lex_init_ALREADY_DEFINED
+#else
+#define yylex_init event_filter_parser_lex_init
+#endif
+
+#ifdef yylex_init_extra
+#define event_filter_parser_lex_init_extra_ALREADY_DEFINED
+#else
+#define yylex_init_extra event_filter_parser_lex_init_extra
+#endif
+
+#ifdef yylex_destroy
+#define event_filter_parser_lex_destroy_ALREADY_DEFINED
+#else
+#define yylex_destroy event_filter_parser_lex_destroy
+#endif
+
+#ifdef yyget_debug
+#define event_filter_parser_get_debug_ALREADY_DEFINED
+#else
+#define yyget_debug event_filter_parser_get_debug
+#endif
+
+#ifdef yyset_debug
+#define event_filter_parser_set_debug_ALREADY_DEFINED
+#else
+#define yyset_debug event_filter_parser_set_debug
+#endif
+
+#ifdef yyget_extra
+#define event_filter_parser_get_extra_ALREADY_DEFINED
+#else
+#define yyget_extra event_filter_parser_get_extra
+#endif
+
+#ifdef yyset_extra
+#define event_filter_parser_set_extra_ALREADY_DEFINED
+#else
+#define yyset_extra event_filter_parser_set_extra
+#endif
+
+#ifdef yyget_in
+#define event_filter_parser_get_in_ALREADY_DEFINED
+#else
+#define yyget_in event_filter_parser_get_in
+#endif
+
+#ifdef yyset_in
+#define event_filter_parser_set_in_ALREADY_DEFINED
+#else
+#define yyset_in event_filter_parser_set_in
+#endif
+
+#ifdef yyget_out
+#define event_filter_parser_get_out_ALREADY_DEFINED
+#else
+#define yyget_out event_filter_parser_get_out
+#endif
+
+#ifdef yyset_out
+#define event_filter_parser_set_out_ALREADY_DEFINED
+#else
+#define yyset_out event_filter_parser_set_out
+#endif
+
+#ifdef yyget_leng
+#define event_filter_parser_get_leng_ALREADY_DEFINED
+#else
+#define yyget_leng event_filter_parser_get_leng
+#endif
+
+#ifdef yyget_text
+#define event_filter_parser_get_text_ALREADY_DEFINED
+#else
+#define yyget_text event_filter_parser_get_text
+#endif
+
+#ifdef yyget_lineno
+#define event_filter_parser_get_lineno_ALREADY_DEFINED
+#else
+#define yyget_lineno event_filter_parser_get_lineno
+#endif
+
+#ifdef yyset_lineno
+#define event_filter_parser_set_lineno_ALREADY_DEFINED
+#else
+#define yyset_lineno event_filter_parser_set_lineno
+#endif
+
+#ifdef yyget_column
+#define event_filter_parser_get_column_ALREADY_DEFINED
+#else
+#define yyget_column event_filter_parser_get_column
+#endif
+
+#ifdef yyset_column
+#define event_filter_parser_set_column_ALREADY_DEFINED
+#else
+#define yyset_column event_filter_parser_set_column
+#endif
+
+#ifdef yywrap
+#define event_filter_parser_wrap_ALREADY_DEFINED
+#else
+#define yywrap event_filter_parser_wrap
+#endif
+
+#ifdef yyget_lval
+#define event_filter_parser_get_lval_ALREADY_DEFINED
+#else
+#define yyget_lval event_filter_parser_get_lval
+#endif
+
+#ifdef yyset_lval
+#define event_filter_parser_set_lval_ALREADY_DEFINED
+#else
+#define yyset_lval event_filter_parser_set_lval
+#endif
+
+#ifdef yyalloc
+#define event_filter_parser_alloc_ALREADY_DEFINED
+#else
+#define yyalloc event_filter_parser_alloc
+#endif
+
+#ifdef yyrealloc
+#define event_filter_parser_realloc_ALREADY_DEFINED
+#else
+#define yyrealloc event_filter_parser_realloc
+#endif
+
+#ifdef yyfree
+#define event_filter_parser_free_ALREADY_DEFINED
+#else
+#define yyfree event_filter_parser_free
+#endif
+
+/* First, we deal with platform-specific or compiler-specific issues. */
+
+/* begin standard C headers. */
+#include <stdio.h>
+#include <string.h>
+#include <errno.h>
+#include <stdlib.h>
+
+/* end standard C headers. */
+
+/* flex integer type definitions */
+
+#ifndef FLEXINT_H
+#define FLEXINT_H
+
+/* C99 systems have <inttypes.h>. Non-C99 systems may or may not. */
+
+#if defined (__STDC_VERSION__) && __STDC_VERSION__ >= 199901L
+
+/* C99 says to define __STDC_LIMIT_MACROS before including stdint.h,
+ * if you want the limit (max/min) macros for int types.
+ */
+#ifndef __STDC_LIMIT_MACROS
+#define __STDC_LIMIT_MACROS 1
+#endif
+
+#include <inttypes.h>
+typedef int8_t flex_int8_t;
+typedef uint8_t flex_uint8_t;
+typedef int16_t flex_int16_t;
+typedef uint16_t flex_uint16_t;
+typedef int32_t flex_int32_t;
+typedef uint32_t flex_uint32_t;
+#else
+typedef signed char flex_int8_t;
+typedef short int flex_int16_t;
+typedef int flex_int32_t;
+typedef unsigned char flex_uint8_t;
+typedef unsigned short int flex_uint16_t;
+typedef unsigned int flex_uint32_t;
+
+/* Limits of integral types. */
+#ifndef INT8_MIN
+#define INT8_MIN (-128)
+#endif
+#ifndef INT16_MIN
+#define INT16_MIN (-32767-1)
+#endif
+#ifndef INT32_MIN
+#define INT32_MIN (-2147483647-1)
+#endif
+#ifndef INT8_MAX
+#define INT8_MAX (127)
+#endif
+#ifndef INT16_MAX
+#define INT16_MAX (32767)
+#endif
+#ifndef INT32_MAX
+#define INT32_MAX (2147483647)
+#endif
+#ifndef UINT8_MAX
+#define UINT8_MAX (255U)
+#endif
+#ifndef UINT16_MAX
+#define UINT16_MAX (65535U)
+#endif
+#ifndef UINT32_MAX
+#define UINT32_MAX (4294967295U)
+#endif
+
+#ifndef SIZE_MAX
+#define SIZE_MAX (~(size_t)0)
+#endif
+
+#endif /* ! C99 */
+
+#endif /* ! FLEXINT_H */
+
+/* begin standard C++ headers. */
+
+/* TODO: this is always defined, so inline it */
+#define yyconst const
+
+#if defined(__GNUC__) && __GNUC__ >= 3
+#define yynoreturn __attribute__((__noreturn__))
+#else
+#define yynoreturn
+#endif
+
+/* Returned upon end-of-file. */
+#define YY_NULL 0
+
+/* Promotes a possibly negative, possibly signed char to an
+ * integer in range [0..255] for use as an array index.
+ */
+#define YY_SC_TO_UI(c) ((YY_CHAR) (c))
+
+/* An opaque pointer. */
+#ifndef YY_TYPEDEF_YY_SCANNER_T
+#define YY_TYPEDEF_YY_SCANNER_T
+typedef void* yyscan_t;
+#endif
+
+/* For convenience, these vars (plus the bison vars far below)
+ are macros in the reentrant scanner. */
+#define yyin yyg->yyin_r
+#define yyout yyg->yyout_r
+#define yyextra yyg->yyextra_r
+#define yyleng yyg->yyleng_r
+#define yytext yyg->yytext_r
+#define yylineno (YY_CURRENT_BUFFER_LVALUE->yy_bs_lineno)
+#define yycolumn (YY_CURRENT_BUFFER_LVALUE->yy_bs_column)
+#define yy_flex_debug yyg->yy_flex_debug_r
+
+/* Enter a start condition. This macro really ought to take a parameter,
+ * but we do it the disgusting crufty way forced on us by the ()-less
+ * definition of BEGIN.
+ */
+#define BEGIN yyg->yy_start = 1 + 2 *
+/* Translate the current start state into a value that can be later handed
+ * to BEGIN to return to the state. The YYSTATE alias is for lex
+ * compatibility.
+ */
+#define YY_START ((yyg->yy_start - 1) / 2)
+#define YYSTATE YY_START
+/* Action number for EOF rule of a given start state. */
+#define YY_STATE_EOF(state) (YY_END_OF_BUFFER + state + 1)
+/* Special action meaning "start processing a new file". */
+#define YY_NEW_FILE yyrestart( yyin , yyscanner )
+#define YY_END_OF_BUFFER_CHAR 0
+
+/* Size of default input buffer. */
+#ifndef YY_BUF_SIZE
+#ifdef __ia64__
+/* On IA-64, the buffer size is 16k, not 8k.
+ * Moreover, YY_BUF_SIZE is 2*YY_READ_BUF_SIZE in the general case.
+ * Ditto for the __ia64__ case accordingly.
+ */
+#define YY_BUF_SIZE 32768
+#else
+#define YY_BUF_SIZE 16384
+#endif /* __ia64__ */
+#endif
+
+/* The state buf must be large enough to hold one state per character in the main buffer.
+ */
+#define YY_STATE_BUF_SIZE ((YY_BUF_SIZE + 2) * sizeof(yy_state_type))
+
+#ifndef YY_TYPEDEF_YY_BUFFER_STATE
+#define YY_TYPEDEF_YY_BUFFER_STATE
+typedef struct yy_buffer_state *YY_BUFFER_STATE;
+#endif
+
+#ifndef YY_TYPEDEF_YY_SIZE_T
+#define YY_TYPEDEF_YY_SIZE_T
+typedef size_t yy_size_t;
+#endif
+
+#define EOB_ACT_CONTINUE_SCAN 0
+#define EOB_ACT_END_OF_FILE 1
+#define EOB_ACT_LAST_MATCH 2
+
+ #define YY_LESS_LINENO(n)
+ #define YY_LINENO_REWIND_TO(ptr)
+
+/* Return all but the first "n" matched characters back to the input stream. */
+#define yyless(n) \
+ do \
+ { \
+ /* Undo effects of setting up yytext. */ \
+ int yyless_macro_arg = (n); \
+ YY_LESS_LINENO(yyless_macro_arg);\
+ *yy_cp = yyg->yy_hold_char; \
+ YY_RESTORE_YY_MORE_OFFSET \
+ yyg->yy_c_buf_p = yy_cp = yy_bp + yyless_macro_arg - YY_MORE_ADJ; \
+ YY_DO_BEFORE_ACTION; /* set up yytext again */ \
+ } \
+ while ( 0 )
+#define unput(c) yyunput( c, yyg->yytext_ptr , yyscanner )
+
+#ifndef YY_STRUCT_YY_BUFFER_STATE
+#define YY_STRUCT_YY_BUFFER_STATE
+struct yy_buffer_state
+ {
+ FILE *yy_input_file;
+
+ char *yy_ch_buf; /* input buffer */
+ char *yy_buf_pos; /* current position in input buffer */
+
+ /* Size of input buffer in bytes, not including room for EOB
+ * characters.
+ */
+ int yy_buf_size;
+
+ /* Number of characters read into yy_ch_buf, not including EOB
+ * characters.
+ */
+ int yy_n_chars;
+
+ /* Whether we "own" the buffer - i.e., we know we created it,
+ * and can realloc() it to grow it, and should free() it to
+ * delete it.
+ */
+ int yy_is_our_buffer;
+
+ /* Whether this is an "interactive" input source; if so, and
+ * if we're using stdio for input, then we want to use getc()
+ * instead of fread(), to make sure we stop fetching input after
+ * each newline.
+ */
+ int yy_is_interactive;
+
+ /* Whether we're considered to be at the beginning of a line.
+ * If so, '^' rules will be active on the next match, otherwise
+ * not.
+ */
+ int yy_at_bol;
+
+ int yy_bs_lineno; /**< The line count. */
+ int yy_bs_column; /**< The column count. */
+
+ /* Whether to try to fill the input buffer when we reach the
+ * end of it.
+ */
+ int yy_fill_buffer;
+
+ int yy_buffer_status;
+
+#define YY_BUFFER_NEW 0
+#define YY_BUFFER_NORMAL 1
+ /* When an EOF's been seen but there's still some text to process
+ * then we mark the buffer as YY_EOF_PENDING, to indicate that we
+ * shouldn't try reading from the input source any more. We might
+ * still have a bunch of tokens to match, though, because of
+ * possible backing-up.
+ *
+ * When we actually see the EOF, we change the status to "new"
+ * (via yyrestart()), so that the user can continue scanning by
+ * just pointing yyin at a new input file.
+ */
+#define YY_BUFFER_EOF_PENDING 2
+
+ };
+#endif /* !YY_STRUCT_YY_BUFFER_STATE */
+
+/* We provide macros for accessing buffer states in case in the
+ * future we want to put the buffer states in a more general
+ * "scanner state".
+ *
+ * Returns the top of the stack, or NULL.
+ */
+#define YY_CURRENT_BUFFER ( yyg->yy_buffer_stack \
+ ? yyg->yy_buffer_stack[yyg->yy_buffer_stack_top] \
+ : NULL)
+/* Same as previous macro, but useful when we know that the buffer stack is not
+ * NULL or when we need an lvalue. For internal use only.
+ */
+#define YY_CURRENT_BUFFER_LVALUE yyg->yy_buffer_stack[yyg->yy_buffer_stack_top]
+
+void yyrestart ( FILE *input_file , yyscan_t yyscanner );
+void yy_switch_to_buffer ( YY_BUFFER_STATE new_buffer , yyscan_t yyscanner );
+YY_BUFFER_STATE yy_create_buffer ( FILE *file, int size , yyscan_t yyscanner );
+void yy_delete_buffer ( YY_BUFFER_STATE b , yyscan_t yyscanner );
+void yy_flush_buffer ( YY_BUFFER_STATE b , yyscan_t yyscanner );
+void yypush_buffer_state ( YY_BUFFER_STATE new_buffer , yyscan_t yyscanner );
+void yypop_buffer_state ( yyscan_t yyscanner );
+
+static void yyensure_buffer_stack ( yyscan_t yyscanner );
+static void yy_load_buffer_state ( yyscan_t yyscanner );
+static void yy_init_buffer ( YY_BUFFER_STATE b, FILE *file , yyscan_t yyscanner );
+#define YY_FLUSH_BUFFER yy_flush_buffer( YY_CURRENT_BUFFER , yyscanner)
+
+YY_BUFFER_STATE yy_scan_buffer ( char *base, yy_size_t size , yyscan_t yyscanner );
+YY_BUFFER_STATE yy_scan_string ( const char *yy_str , yyscan_t yyscanner );
+YY_BUFFER_STATE yy_scan_bytes ( const char *bytes, int len , yyscan_t yyscanner );
+
+void *yyalloc ( yy_size_t , yyscan_t yyscanner );
+void *yyrealloc ( void *, yy_size_t , yyscan_t yyscanner );
+void yyfree ( void * , yyscan_t yyscanner );
+
+#define yy_new_buffer yy_create_buffer
+#define yy_set_interactive(is_interactive) \
+ { \
+ if ( ! YY_CURRENT_BUFFER ){ \
+ yyensure_buffer_stack (yyscanner); \
+ YY_CURRENT_BUFFER_LVALUE = \
+ yy_create_buffer( yyin, YY_BUF_SIZE , yyscanner); \
+ } \
+ YY_CURRENT_BUFFER_LVALUE->yy_is_interactive = is_interactive; \
+ }
+#define yy_set_bol(at_bol) \
+ { \
+ if ( ! YY_CURRENT_BUFFER ){\
+ yyensure_buffer_stack (yyscanner); \
+ YY_CURRENT_BUFFER_LVALUE = \
+ yy_create_buffer( yyin, YY_BUF_SIZE , yyscanner); \
+ } \
+ YY_CURRENT_BUFFER_LVALUE->yy_at_bol = at_bol; \
+ }
+#define YY_AT_BOL() (YY_CURRENT_BUFFER_LVALUE->yy_at_bol)
+
+/* Begin user sect3 */
+
+#define event_filter_parser_wrap(yyscanner) (/*CONSTCOND*/1)
+#define YY_SKIP_YYWRAP
+typedef flex_uint8_t YY_CHAR;
+
+typedef int yy_state_type;
+
+#define yytext_ptr yytext_r
+
+static yy_state_type yy_get_previous_state ( yyscan_t yyscanner );
+static yy_state_type yy_try_NUL_trans ( yy_state_type current_state , yyscan_t yyscanner);
+static int yy_get_next_buffer ( yyscan_t yyscanner );
+static void yynoreturn yy_fatal_error ( const char* msg , yyscan_t yyscanner );
+
+/* Done after the current pattern has been matched and before the
+ * corresponding action - sets up yytext.
+ */
+#define YY_DO_BEFORE_ACTION \
+ yyg->yytext_ptr = yy_bp; \
+ yyleng = (int) (yy_cp - yy_bp); \
+ yyg->yy_hold_char = *yy_cp; \
+ *yy_cp = '\0'; \
+ yyg->yy_c_buf_p = yy_cp;
+#define YY_NUM_RULES 14
+#define YY_END_OF_BUFFER 15
+/* This struct is not used in this scanner,
+ but its presence is necessary. */
+struct yy_trans_info
+ {
+ flex_int32_t yy_verify;
+ flex_int32_t yy_nxt;
+ };
+static const flex_int16_t yy_accept[29] =
+ { 0,
+ 0, 0, 0, 0, 15, 13, 12, 12, 1, 10,
+ 11, 11, 11, 11, 3, 2, 14, 11, 11, 11,
+ 8, 3, 6, 5, 4, 7, 9, 0
+ } ;
+
+static const YY_CHAR yy_ec[256] =
+ { 0,
+ 1, 1, 1, 1, 1, 1, 1, 1, 2, 3,
+ 1, 1, 2, 1, 1, 1, 1, 1, 1, 1,
+ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+ 1, 2, 1, 4, 1, 1, 1, 1, 1, 5,
+ 5, 6, 1, 1, 6, 6, 1, 6, 6, 6,
+ 6, 6, 6, 6, 6, 6, 6, 6, 1, 5,
+ 5, 5, 6, 1, 7, 6, 6, 8, 6, 6,
+ 6, 6, 6, 6, 6, 6, 6, 9, 10, 6,
+ 6, 11, 6, 12, 6, 6, 6, 6, 6, 6,
+ 1, 13, 1, 1, 6, 1, 7, 6, 6, 8,
+
+ 6, 6, 6, 6, 6, 6, 6, 6, 6, 9,
+ 10, 6, 6, 11, 6, 12, 6, 6, 6, 6,
+ 6, 6, 1, 1, 1, 1, 1, 1, 1, 1,
+ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+
+ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+ 1, 1, 1, 1, 1
+ } ;
+
+static const YY_CHAR yy_meta[14] =
+ { 0,
+ 1, 1, 2, 3, 1, 4, 4, 4, 4, 4,
+ 4, 4, 3
+ } ;
+
+static const flex_int16_t yy_base[33] =
+ { 0,
+ 0, 0, 10, 20, 21, 56, 56, 56, 56, 56,
+ 0, 11, 9, 7, 0, 56, 30, 0, 9, 4,
+ 0, 0, 56, 56, 56, 0, 0, 56, 43, 11,
+ 47, 51
+ } ;
+
+static const flex_int16_t yy_def[33] =
+ { 0,
+ 28, 1, 29, 29, 28, 28, 28, 28, 28, 28,
+ 30, 30, 30, 30, 31, 28, 32, 30, 30, 30,
+ 30, 31, 28, 28, 28, 30, 30, 0, 28, 28,
+ 28, 28
+ } ;
+
+static const flex_int16_t yy_nxt[70] =
+ { 0,
+ 6, 7, 8, 9, 10, 11, 12, 11, 13, 14,
+ 11, 11, 6, 16, 18, 27, 26, 21, 20, 19,
+ 28, 28, 17, 16, 28, 28, 28, 28, 28, 28,
+ 28, 28, 17, 24, 28, 28, 28, 28, 28, 28,
+ 28, 28, 25, 15, 15, 15, 15, 22, 22, 28,
+ 22, 23, 28, 23, 23, 5, 28, 28, 28, 28,
+ 28, 28, 28, 28, 28, 28, 28, 28, 28
+ } ;
+
+static const flex_int16_t yy_chk[70] =
+ { 0,
+ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+ 1, 1, 1, 3, 30, 20, 19, 14, 13, 12,
+ 5, 0, 3, 4, 0, 0, 0, 0, 0, 0,
+ 0, 0, 4, 17, 0, 0, 0, 0, 0, 0,
+ 0, 0, 17, 29, 29, 29, 29, 31, 31, 0,
+ 31, 32, 0, 32, 32, 28, 28, 28, 28, 28,
+ 28, 28, 28, 28, 28, 28, 28, 28, 28
+ } ;
+
+/* The intent behind this definition is that it'll catch
+ * any uses of REJECT which flex missed.
+ */
+#define REJECT reject_used_but_not_detected
+#define yymore() yymore_used_but_not_detected
+#define YY_MORE_ADJ 0
+#define YY_RESTORE_YY_MORE_OFFSET
+#line 1 "event-filter-lexer.l"
+/* Copyright (c) 2020 Dovecot authors, see the included COPYING file */
+#define YY_NO_INPUT 1
+#line 13 "event-filter-lexer.l"
+#include "lib.h"
+#include "str.h"
+#include "event-filter-private.h"
+#include "event-filter-parser.h"
+
+#define YY_FATAL_ERROR(msg) { i_fatal("event filter parsing: %s", (msg)); }
+
+/* mimic renaming done by bison's api.prefix %define */
+#define YYSTYPE EVENT_FILTER_PARSER_STYPE
+
+#define YY_INPUT(buf, result, max_size) \
+ result = event_filter_parser_input_proc(buf, max_size, yyscanner)
+static size_t event_filter_parser_input_proc(char *buf, size_t size, yyscan_t scanner);
+
+#ifdef __clang__
+#pragma clang diagnostic push
+/* ignore "unknown warning" warning if we're using unpatched clang */
+#pragma clang diagnostic ignored "-Wunknown-warning-option"
+/* ignore strict bool warnings in generated code */
+#pragma clang diagnostic ignored "-Wstrict-bool"
+/* ignore sign comparison errors (buggy flex) */
+#pragma clang diagnostic ignored "-Wsign-compare"
+/* ignore unused functions */
+#pragma clang diagnostic ignored "-Wunused-function"
+/* ignore unused parameters */
+#pragma clang diagnostic ignored "-Wunused-parameter"
+#else
+/* and same for gcc */
+#pragma GCC diagnostic push
+#pragma GCC diagnostic ignored "-Wsign-compare"
+#pragma GCC diagnostic ignored "-Wunused-function"
+#pragma GCC diagnostic ignored "-Wunused-parameter"
+#endif
+
+#line 702 "event-filter-lexer.c"
+
+#line 704 "event-filter-lexer.c"
+
+#define INITIAL 0
+#define string 1
+
+#ifndef YY_NO_UNISTD_H
+/* Special case for "unistd.h", since it is non-ANSI. We include it way
+ * down here because we want the user's section 1 to have been scanned first.
+ * The user has a chance to override it with an option.
+ */
+#include <unistd.h>
+#endif
+
+#ifndef YY_EXTRA_TYPE
+#define YY_EXTRA_TYPE void *
+#endif
+
+/* Holds the entire state of the reentrant scanner. */
+struct yyguts_t
+ {
+
+ /* User-defined. Not touched by flex. */
+ YY_EXTRA_TYPE yyextra_r;
+
+ /* The rest are the same as the globals declared in the non-reentrant scanner. */
+ FILE *yyin_r, *yyout_r;
+ size_t yy_buffer_stack_top; /**< index of top of stack. */
+ size_t yy_buffer_stack_max; /**< capacity of stack. */
+ YY_BUFFER_STATE * yy_buffer_stack; /**< Stack as an array. */
+ char yy_hold_char;
+ int yy_n_chars;
+ int yyleng_r;
+ char *yy_c_buf_p;
+ int yy_init;
+ int yy_start;
+ int yy_did_buffer_switch_on_eof;
+ int yy_start_stack_ptr;
+ int yy_start_stack_depth;
+ int *yy_start_stack;
+ yy_state_type yy_last_accepting_state;
+ char* yy_last_accepting_cpos;
+
+ int yylineno_r;
+ int yy_flex_debug_r;
+
+ char *yytext_r;
+ int yy_more_flag;
+ int yy_more_len;
+
+ YYSTYPE * yylval_r;
+
+ }; /* end struct yyguts_t */
+
+static int yy_init_globals ( yyscan_t yyscanner );
+
+ /* This must go here because YYSTYPE and YYLTYPE are included
+ * from bison output in section 1.*/
+ # define yylval yyg->yylval_r
+
+int yylex_init (yyscan_t* scanner);
+
+int yylex_init_extra ( YY_EXTRA_TYPE user_defined, yyscan_t* scanner);
+
+/* Accessor methods to globals.
+ These are made visible to non-reentrant scanners for convenience. */
+
+int yylex_destroy ( yyscan_t yyscanner );
+
+int yyget_debug ( yyscan_t yyscanner );
+
+void yyset_debug ( int debug_flag , yyscan_t yyscanner );
+
+YY_EXTRA_TYPE yyget_extra ( yyscan_t yyscanner );
+
+void yyset_extra ( YY_EXTRA_TYPE user_defined , yyscan_t yyscanner );
+
+FILE *yyget_in ( yyscan_t yyscanner );
+
+void yyset_in ( FILE * _in_str , yyscan_t yyscanner );
+
+FILE *yyget_out ( yyscan_t yyscanner );
+
+void yyset_out ( FILE * _out_str , yyscan_t yyscanner );
+
+ int yyget_leng ( yyscan_t yyscanner );
+
+char *yyget_text ( yyscan_t yyscanner );
+
+int yyget_lineno ( yyscan_t yyscanner );
+
+void yyset_lineno ( int _line_number , yyscan_t yyscanner );
+
+int yyget_column ( yyscan_t yyscanner );
+
+void yyset_column ( int _column_no , yyscan_t yyscanner );
+
+YYSTYPE * yyget_lval ( yyscan_t yyscanner );
+
+void yyset_lval ( YYSTYPE * yylval_param , yyscan_t yyscanner );
+
+/* Macros after this point can all be overridden by user definitions in
+ * section 1.
+ */
+
+#ifndef YY_SKIP_YYWRAP
+#ifdef __cplusplus
+extern "C" int yywrap ( yyscan_t yyscanner );
+#else
+extern int yywrap ( yyscan_t yyscanner );
+#endif
+#endif
+
+#ifndef YY_NO_UNPUT
+
+#endif
+
+#ifndef yytext_ptr
+static void yy_flex_strncpy ( char *, const char *, int , yyscan_t yyscanner);
+#endif
+
+#ifdef YY_NEED_STRLEN
+static int yy_flex_strlen ( const char * , yyscan_t yyscanner);
+#endif
+
+#ifndef YY_NO_INPUT
+#ifdef __cplusplus
+static int yyinput ( yyscan_t yyscanner );
+#else
+static int input ( yyscan_t yyscanner );
+#endif
+
+#endif
+
+/* Amount of stuff to slurp up with each read. */
+#ifndef YY_READ_BUF_SIZE
+#ifdef __ia64__
+/* On IA-64, the buffer size is 16k, not 8k */
+#define YY_READ_BUF_SIZE 16384
+#else
+#define YY_READ_BUF_SIZE 8192
+#endif /* __ia64__ */
+#endif
+
+/* Copy whatever the last rule matched to the standard output. */
+#ifndef ECHO
+/* This used to be an fputs(), but since the string might contain NUL's,
+ * we now use fwrite().
+ */
+#define ECHO do { if (fwrite( yytext, (size_t) yyleng, 1, yyout )) {} } while (0)
+#endif
+
+/* Gets input and stuffs it into "buf". number of characters read, or YY_NULL,
+ * is returned in "result".
+ */
+#ifndef YY_INPUT
+#define YY_INPUT(buf,result,max_size) \
+ if ( YY_CURRENT_BUFFER_LVALUE->yy_is_interactive ) \
+ { \
+ int c = '*'; \
+ int n; \
+ for ( n = 0; n < max_size && \
+ (c = getc( yyin )) != EOF && c != '\n'; ++n ) \
+ buf[n] = (char) c; \
+ if ( c == '\n' ) \
+ buf[n++] = (char) c; \
+ if ( c == EOF && ferror( yyin ) ) \
+ YY_FATAL_ERROR( "input in flex scanner failed" ); \
+ result = n; \
+ } \
+ else \
+ { \
+ errno=0; \
+ while ( (result = (int) fread(buf, 1, (yy_size_t) max_size, yyin)) == 0 && ferror(yyin)) \
+ { \
+ if( errno != EINTR) \
+ { \
+ YY_FATAL_ERROR( "input in flex scanner failed" ); \
+ break; \
+ } \
+ errno=0; \
+ clearerr(yyin); \
+ } \
+ }\
+\
+
+#endif
+
+/* No semi-colon after return; correct usage is to write "yyterminate();" -
+ * we don't want an extra ';' after the "return" because that will cause
+ * some compilers to complain about unreachable statements.
+ */
+#ifndef yyterminate
+#define yyterminate() return YY_NULL
+#endif
+
+/* Number of entries by which start-condition stack grows. */
+#ifndef YY_START_STACK_INCR
+#define YY_START_STACK_INCR 25
+#endif
+
+/* Report a fatal error. */
+#ifndef YY_FATAL_ERROR
+#define YY_FATAL_ERROR(msg) yy_fatal_error( msg , yyscanner)
+#endif
+
+/* end tables serialization structures and prototypes */
+
+/* Default declaration of generated scanner - a define so the user can
+ * easily add parameters.
+ */
+#ifndef YY_DECL
+#define YY_DECL_IS_OURS 1
+
+extern int yylex \
+ (YYSTYPE * yylval_param , yyscan_t yyscanner);
+
+#define YY_DECL int yylex \
+ (YYSTYPE * yylval_param , yyscan_t yyscanner)
+#endif /* !YY_DECL */
+
+/* Code executed at the beginning of each rule, after yytext and yyleng
+ * have been set up.
+ */
+#ifndef YY_USER_ACTION
+#define YY_USER_ACTION
+#endif
+
+/* Code executed at the end of each rule. */
+#ifndef YY_BREAK
+#define YY_BREAK /*LINTED*/break;
+#endif
+
+#define YY_RULE_SETUP \
+ YY_USER_ACTION
+
+/** The main scanner function which does all the work.
+ */
+YY_DECL
+{
+ yy_state_type yy_current_state;
+ char *yy_cp, *yy_bp;
+ int yy_act;
+ struct yyguts_t * yyg = (struct yyguts_t*)yyscanner;
+
+ yylval = yylval_param;
+
+ if ( !yyg->yy_init )
+ {
+ yyg->yy_init = 1;
+
+#ifdef YY_USER_INIT
+ YY_USER_INIT;
+#endif
+
+ if ( ! yyg->yy_start )
+ yyg->yy_start = 1; /* first start state */
+
+ if ( ! yyin )
+ yyin = stdin;
+
+ if ( ! yyout )
+ yyout = stdout;
+
+ if ( ! YY_CURRENT_BUFFER ) {
+ yyensure_buffer_stack (yyscanner);
+ YY_CURRENT_BUFFER_LVALUE =
+ yy_create_buffer( yyin, YY_BUF_SIZE , yyscanner);
+ }
+
+ yy_load_buffer_state( yyscanner );
+ }
+
+ {
+#line 51 "event-filter-lexer.l"
+
+#line 53 "event-filter-lexer.l"
+ string_t *str_buf = NULL;
+
+#line 982 "event-filter-lexer.c"
+
+ while ( /*CONSTCOND*/1 ) /* loops until end-of-file is reached */
+ {
+ yy_cp = yyg->yy_c_buf_p;
+
+ /* Support of yytext. */
+ *yy_cp = yyg->yy_hold_char;
+
+ /* yy_bp points to the position in yy_ch_buf of the start of
+ * the current run.
+ */
+ yy_bp = yy_cp;
+
+ yy_current_state = yyg->yy_start;
+yy_match:
+ do
+ {
+ YY_CHAR yy_c = yy_ec[YY_SC_TO_UI(*yy_cp)] ;
+ if ( yy_accept[yy_current_state] )
+ {
+ yyg->yy_last_accepting_state = yy_current_state;
+ yyg->yy_last_accepting_cpos = yy_cp;
+ }
+ while ( yy_chk[yy_base[yy_current_state] + yy_c] != yy_current_state )
+ {
+ yy_current_state = (int) yy_def[yy_current_state];
+ if ( yy_current_state >= 29 )
+ yy_c = yy_meta[yy_c];
+ }
+ yy_current_state = yy_nxt[yy_base[yy_current_state] + yy_c];
+ ++yy_cp;
+ }
+ while ( yy_current_state != 28 );
+ yy_cp = yyg->yy_last_accepting_cpos;
+ yy_current_state = yyg->yy_last_accepting_state;
+
+yy_find_action:
+ yy_act = yy_accept[yy_current_state];
+
+ YY_DO_BEFORE_ACTION;
+
+do_action: /* This label is used only to access EOF actions. */
+
+ switch ( yy_act )
+ { /* beginning of action switch */
+ case 0: /* must back up */
+ /* undo the effects of YY_DO_BEFORE_ACTION */
+ *yy_cp = yyg->yy_hold_char;
+ yy_cp = yyg->yy_last_accepting_cpos;
+ yy_current_state = yyg->yy_last_accepting_state;
+ goto yy_find_action;
+
+case 1:
+YY_RULE_SETUP
+#line 55 "event-filter-lexer.l"
+{
+ BEGIN(string);
+
+ str_buf = t_str_new(128);
+ }
+ YY_BREAK
+case 2:
+YY_RULE_SETUP
+#line 60 "event-filter-lexer.l"
+{
+ yylval->str = str_c(str_buf);
+ BEGIN(INITIAL);
+ return STRING;
+ }
+ YY_BREAK
+/* Note: these have to match the event_filter_append_escaped() behavior */
+case 3:
+/* rule 3 can match eol */
+YY_RULE_SETUP
+#line 66 "event-filter-lexer.l"
+{ str_append(str_buf, yytext); }
+ YY_BREAK
+case 4:
+YY_RULE_SETUP
+#line 67 "event-filter-lexer.l"
+{ str_append_c(str_buf, '\\'); }
+ YY_BREAK
+case 5:
+YY_RULE_SETUP
+#line 68 "event-filter-lexer.l"
+{ str_append_c(str_buf, '"'); }
+ YY_BREAK
+case 6:
+YY_RULE_SETUP
+#line 69 "event-filter-lexer.l"
+{ str_append(str_buf, yytext); }
+ YY_BREAK
+case 7:
+YY_RULE_SETUP
+#line 71 "event-filter-lexer.l"
+{ return AND; }
+ YY_BREAK
+case 8:
+YY_RULE_SETUP
+#line 72 "event-filter-lexer.l"
+{ return OR; }
+ YY_BREAK
+case 9:
+YY_RULE_SETUP
+#line 73 "event-filter-lexer.l"
+{ return NOT; }
+ YY_BREAK
+case 10:
+YY_RULE_SETUP
+#line 74 "event-filter-lexer.l"
+{ return *yytext; }
+ YY_BREAK
+case 11:
+YY_RULE_SETUP
+#line 75 "event-filter-lexer.l"
+{ yylval->str = t_strdup(yytext); return TOKEN; }
+ YY_BREAK
+case 12:
+/* rule 12 can match eol */
+YY_RULE_SETUP
+#line 76 "event-filter-lexer.l"
+{ /* ignore */ }
+ YY_BREAK
+case 13:
+YY_RULE_SETUP
+#line 77 "event-filter-lexer.l"
+{
+ /*
+ * We simply return the char to the
+ * and let the grammar error out
+ * with a syntax error.
+ *
+ * Note: The cast is significant
+ * since utf-8 bytes >=128 will
+ * otherwise result in sign
+ * extension and a negative int
+ * getting returned on some
+ * platforms (e.g., x86) which in
+ * turn confuses the parser. E.g.,
+ * if:
+ * *yytext = '\x80'
+ * we get:
+ * *yytext -> -128
+ * (int) *yytext -> -128
+ * which is wrong. With the
+ * unsigned char cast, we get:
+ * (u.c.) *yytext -> 128
+ * (int)(u.c.) *yytext -> 128
+ * which is correct.
+ */
+ return (unsigned char) *yytext;
+ }
+ YY_BREAK
+case 14:
+YY_RULE_SETUP
+#line 103 "event-filter-lexer.l"
+ECHO;
+ YY_BREAK
+#line 1141 "event-filter-lexer.c"
+case YY_STATE_EOF(INITIAL):
+case YY_STATE_EOF(string):
+ yyterminate();
+
+ case YY_END_OF_BUFFER:
+ {
+ /* Amount of text matched not including the EOB char. */
+ int yy_amount_of_matched_text = (int) (yy_cp - yyg->yytext_ptr) - 1;
+
+ /* Undo the effects of YY_DO_BEFORE_ACTION. */
+ *yy_cp = yyg->yy_hold_char;
+ YY_RESTORE_YY_MORE_OFFSET
+
+ if ( YY_CURRENT_BUFFER_LVALUE->yy_buffer_status == YY_BUFFER_NEW )
+ {
+ /* We're scanning a new file or input source. It's
+ * possible that this happened because the user
+ * just pointed yyin at a new source and called
+ * yylex(). If so, then we have to assure
+ * consistency between YY_CURRENT_BUFFER and our
+ * globals. Here is the right place to do so, because
+ * this is the first action (other than possibly a
+ * back-up) that will match for the new input source.
+ */
+ yyg->yy_n_chars = YY_CURRENT_BUFFER_LVALUE->yy_n_chars;
+ YY_CURRENT_BUFFER_LVALUE->yy_input_file = yyin;
+ YY_CURRENT_BUFFER_LVALUE->yy_buffer_status = YY_BUFFER_NORMAL;
+ }
+
+ /* Note that here we test for yy_c_buf_p "<=" to the position
+ * of the first EOB in the buffer, since yy_c_buf_p will
+ * already have been incremented past the NUL character
+ * (since all states make transitions on EOB to the
+ * end-of-buffer state). Contrast this with the test
+ * in input().
+ */
+ if ( yyg->yy_c_buf_p <= &YY_CURRENT_BUFFER_LVALUE->yy_ch_buf[yyg->yy_n_chars] )
+ { /* This was really a NUL. */
+ yy_state_type yy_next_state;
+
+ yyg->yy_c_buf_p = yyg->yytext_ptr + yy_amount_of_matched_text;
+
+ yy_current_state = yy_get_previous_state( yyscanner );
+
+ /* Okay, we're now positioned to make the NUL
+ * transition. We couldn't have
+ * yy_get_previous_state() go ahead and do it
+ * for us because it doesn't know how to deal
+ * with the possibility of jamming (and we don't
+ * want to build jamming into it because then it
+ * will run more slowly).
+ */
+
+ yy_next_state = yy_try_NUL_trans( yy_current_state , yyscanner);
+
+ yy_bp = yyg->yytext_ptr + YY_MORE_ADJ;
+
+ if ( yy_next_state )
+ {
+ /* Consume the NUL. */
+ yy_cp = ++yyg->yy_c_buf_p;
+ yy_current_state = yy_next_state;
+ goto yy_match;
+ }
+
+ else
+ {
+ yy_cp = yyg->yy_last_accepting_cpos;
+ yy_current_state = yyg->yy_last_accepting_state;
+ goto yy_find_action;
+ }
+ }
+
+ else switch ( yy_get_next_buffer( yyscanner ) )
+ {
+ case EOB_ACT_END_OF_FILE:
+ {
+ yyg->yy_did_buffer_switch_on_eof = 0;
+
+ if ( yywrap( yyscanner ) )
+ {
+ /* Note: because we've taken care in
+ * yy_get_next_buffer() to have set up
+ * yytext, we can now set up
+ * yy_c_buf_p so that if some total
+ * hoser (like flex itself) wants to
+ * call the scanner after we return the
+ * YY_NULL, it'll still work - another
+ * YY_NULL will get returned.
+ */
+ yyg->yy_c_buf_p = yyg->yytext_ptr + YY_MORE_ADJ;
+
+ yy_act = YY_STATE_EOF(YY_START);
+ goto do_action;
+ }
+
+ else
+ {
+ if ( ! yyg->yy_did_buffer_switch_on_eof )
+ YY_NEW_FILE;
+ }
+ break;
+ }
+
+ case EOB_ACT_CONTINUE_SCAN:
+ yyg->yy_c_buf_p =
+ yyg->yytext_ptr + yy_amount_of_matched_text;
+
+ yy_current_state = yy_get_previous_state( yyscanner );
+
+ yy_cp = yyg->yy_c_buf_p;
+ yy_bp = yyg->yytext_ptr + YY_MORE_ADJ;
+ goto yy_match;
+
+ case EOB_ACT_LAST_MATCH:
+ yyg->yy_c_buf_p =
+ &YY_CURRENT_BUFFER_LVALUE->yy_ch_buf[yyg->yy_n_chars];
+
+ yy_current_state = yy_get_previous_state( yyscanner );
+
+ yy_cp = yyg->yy_c_buf_p;
+ yy_bp = yyg->yytext_ptr + YY_MORE_ADJ;
+ goto yy_find_action;
+ }
+ break;
+ }
+
+ default:
+ YY_FATAL_ERROR(
+ "fatal flex scanner internal error--no action found" );
+ } /* end of action switch */
+ } /* end of scanning one token */
+ } /* end of user's declarations */
+} /* end of yylex */
+
+/* yy_get_next_buffer - try to read in a new buffer
+ *
+ * Returns a code representing an action:
+ * EOB_ACT_LAST_MATCH -
+ * EOB_ACT_CONTINUE_SCAN - continue scanning from current position
+ * EOB_ACT_END_OF_FILE - end of file
+ */
+static int yy_get_next_buffer (yyscan_t yyscanner)
+{
+ struct yyguts_t * yyg = (struct yyguts_t*)yyscanner;
+ char *dest = YY_CURRENT_BUFFER_LVALUE->yy_ch_buf;
+ char *source = yyg->yytext_ptr;
+ int number_to_move, i;
+ int ret_val;
+
+ if ( yyg->yy_c_buf_p > &YY_CURRENT_BUFFER_LVALUE->yy_ch_buf[yyg->yy_n_chars + 1] )
+ YY_FATAL_ERROR(
+ "fatal flex scanner internal error--end of buffer missed" );
+
+ if ( YY_CURRENT_BUFFER_LVALUE->yy_fill_buffer == 0 )
+ { /* Don't try to fill the buffer, so this is an EOF. */
+ if ( yyg->yy_c_buf_p - yyg->yytext_ptr - YY_MORE_ADJ == 1 )
+ {
+ /* We matched a single character, the EOB, so
+ * treat this as a final EOF.
+ */
+ return EOB_ACT_END_OF_FILE;
+ }
+
+ else
+ {
+ /* We matched some text prior to the EOB, first
+ * process it.
+ */
+ return EOB_ACT_LAST_MATCH;
+ }
+ }
+
+ /* Try to read more data. */
+
+ /* First move last chars to start of buffer. */
+ number_to_move = (int) (yyg->yy_c_buf_p - yyg->yytext_ptr - 1);
+
+ for ( i = 0; i < number_to_move; ++i )
+ *(dest++) = *(source++);
+
+ if ( YY_CURRENT_BUFFER_LVALUE->yy_buffer_status == YY_BUFFER_EOF_PENDING )
+ /* don't do the read, it's not guaranteed to return an EOF,
+ * just force an EOF
+ */
+ YY_CURRENT_BUFFER_LVALUE->yy_n_chars = yyg->yy_n_chars = 0;
+
+ else
+ {
+ int num_to_read =
+ YY_CURRENT_BUFFER_LVALUE->yy_buf_size - number_to_move - 1;
+
+ while ( num_to_read <= 0 )
+ { /* Not enough room in the buffer - grow it. */
+
+ /* just a shorter name for the current buffer */
+ YY_BUFFER_STATE b = YY_CURRENT_BUFFER_LVALUE;
+
+ int yy_c_buf_p_offset =
+ (int) (yyg->yy_c_buf_p - b->yy_ch_buf);
+
+ if ( b->yy_is_our_buffer )
+ {
+ int new_size = b->yy_buf_size * 2;
+
+ if ( new_size <= 0 )
+ b->yy_buf_size += b->yy_buf_size / 8;
+ else
+ b->yy_buf_size *= 2;
+
+ b->yy_ch_buf = (char *)
+ /* Include room in for 2 EOB chars. */
+ yyrealloc( (void *) b->yy_ch_buf,
+ (yy_size_t) (b->yy_buf_size + 2) , yyscanner );
+ }
+ else
+ /* Can't grow it, we don't own it. */
+ b->yy_ch_buf = NULL;
+
+ if ( ! b->yy_ch_buf )
+ YY_FATAL_ERROR(
+ "fatal error - scanner input buffer overflow" );
+
+ yyg->yy_c_buf_p = &b->yy_ch_buf[yy_c_buf_p_offset];
+
+ num_to_read = YY_CURRENT_BUFFER_LVALUE->yy_buf_size -
+ number_to_move - 1;
+
+ }
+
+ if ( num_to_read > YY_READ_BUF_SIZE )
+ num_to_read = YY_READ_BUF_SIZE;
+
+ /* Read in more data. */
+ YY_INPUT( (&YY_CURRENT_BUFFER_LVALUE->yy_ch_buf[number_to_move]),
+ yyg->yy_n_chars, num_to_read );
+
+ YY_CURRENT_BUFFER_LVALUE->yy_n_chars = yyg->yy_n_chars;
+ }
+
+ if ( yyg->yy_n_chars == 0 )
+ {
+ if ( number_to_move == YY_MORE_ADJ )
+ {
+ ret_val = EOB_ACT_END_OF_FILE;
+ yyrestart( yyin , yyscanner);
+ }
+
+ else
+ {
+ ret_val = EOB_ACT_LAST_MATCH;
+ YY_CURRENT_BUFFER_LVALUE->yy_buffer_status =
+ YY_BUFFER_EOF_PENDING;
+ }
+ }
+
+ else
+ ret_val = EOB_ACT_CONTINUE_SCAN;
+
+ if ((yyg->yy_n_chars + number_to_move) > YY_CURRENT_BUFFER_LVALUE->yy_buf_size) {
+ /* Extend the array by 50%, plus the number we really need. */
+ int new_size = yyg->yy_n_chars + number_to_move + (yyg->yy_n_chars >> 1);
+ YY_CURRENT_BUFFER_LVALUE->yy_ch_buf = (char *) yyrealloc(
+ (void *) YY_CURRENT_BUFFER_LVALUE->yy_ch_buf, (yy_size_t) new_size , yyscanner );
+ if ( ! YY_CURRENT_BUFFER_LVALUE->yy_ch_buf )
+ YY_FATAL_ERROR( "out of dynamic memory in yy_get_next_buffer()" );
+ /* "- 2" to take care of EOB's */
+ YY_CURRENT_BUFFER_LVALUE->yy_buf_size = (int) (new_size - 2);
+ }
+
+ yyg->yy_n_chars += number_to_move;
+ YY_CURRENT_BUFFER_LVALUE->yy_ch_buf[yyg->yy_n_chars] = YY_END_OF_BUFFER_CHAR;
+ YY_CURRENT_BUFFER_LVALUE->yy_ch_buf[yyg->yy_n_chars + 1] = YY_END_OF_BUFFER_CHAR;
+
+ yyg->yytext_ptr = &YY_CURRENT_BUFFER_LVALUE->yy_ch_buf[0];
+
+ return ret_val;
+}
+
+/* yy_get_previous_state - get the state just before the EOB char was reached */
+
+ static yy_state_type yy_get_previous_state (yyscan_t yyscanner)
+{
+ yy_state_type yy_current_state;
+ char *yy_cp;
+ struct yyguts_t * yyg = (struct yyguts_t*)yyscanner;
+
+ yy_current_state = yyg->yy_start;
+
+ for ( yy_cp = yyg->yytext_ptr + YY_MORE_ADJ; yy_cp < yyg->yy_c_buf_p; ++yy_cp )
+ {
+ YY_CHAR yy_c = (*yy_cp ? yy_ec[YY_SC_TO_UI(*yy_cp)] : 1);
+ if ( yy_accept[yy_current_state] )
+ {
+ yyg->yy_last_accepting_state = yy_current_state;
+ yyg->yy_last_accepting_cpos = yy_cp;
+ }
+ while ( yy_chk[yy_base[yy_current_state] + yy_c] != yy_current_state )
+ {
+ yy_current_state = (int) yy_def[yy_current_state];
+ if ( yy_current_state >= 29 )
+ yy_c = yy_meta[yy_c];
+ }
+ yy_current_state = yy_nxt[yy_base[yy_current_state] + yy_c];
+ }
+
+ return yy_current_state;
+}
+
+/* yy_try_NUL_trans - try to make a transition on the NUL character
+ *
+ * synopsis
+ * next_state = yy_try_NUL_trans( current_state );
+ */
+ static yy_state_type yy_try_NUL_trans (yy_state_type yy_current_state , yyscan_t yyscanner)
+{
+ int yy_is_jam;
+ struct yyguts_t * yyg = (struct yyguts_t*)yyscanner; /* This var may be unused depending upon options. */
+ char *yy_cp = yyg->yy_c_buf_p;
+
+ YY_CHAR yy_c = 1;
+ if ( yy_accept[yy_current_state] )
+ {
+ yyg->yy_last_accepting_state = yy_current_state;
+ yyg->yy_last_accepting_cpos = yy_cp;
+ }
+ while ( yy_chk[yy_base[yy_current_state] + yy_c] != yy_current_state )
+ {
+ yy_current_state = (int) yy_def[yy_current_state];
+ if ( yy_current_state >= 29 )
+ yy_c = yy_meta[yy_c];
+ }
+ yy_current_state = yy_nxt[yy_base[yy_current_state] + yy_c];
+ yy_is_jam = (yy_current_state == 28);
+
+ (void)yyg;
+ return yy_is_jam ? 0 : yy_current_state;
+}
+
+#ifndef YY_NO_UNPUT
+
+#endif
+
+#ifndef YY_NO_INPUT
+#ifdef __cplusplus
+ static int yyinput (yyscan_t yyscanner)
+#else
+ static int input (yyscan_t yyscanner)
+#endif
+
+{
+ int c;
+ struct yyguts_t * yyg = (struct yyguts_t*)yyscanner;
+
+ *yyg->yy_c_buf_p = yyg->yy_hold_char;
+
+ if ( *yyg->yy_c_buf_p == YY_END_OF_BUFFER_CHAR )
+ {
+ /* yy_c_buf_p now points to the character we want to return.
+ * If this occurs *before* the EOB characters, then it's a
+ * valid NUL; if not, then we've hit the end of the buffer.
+ */
+ if ( yyg->yy_c_buf_p < &YY_CURRENT_BUFFER_LVALUE->yy_ch_buf[yyg->yy_n_chars] )
+ /* This was really a NUL. */
+ *yyg->yy_c_buf_p = '\0';
+
+ else
+ { /* need more input */
+ int offset = (int) (yyg->yy_c_buf_p - yyg->yytext_ptr);
+ ++yyg->yy_c_buf_p;
+
+ switch ( yy_get_next_buffer( yyscanner ) )
+ {
+ case EOB_ACT_LAST_MATCH:
+ /* This happens because yy_g_n_b()
+ * sees that we've accumulated a
+ * token and flags that we need to
+ * try matching the token before
+ * proceeding. But for input(),
+ * there's no matching to consider.
+ * So convert the EOB_ACT_LAST_MATCH
+ * to EOB_ACT_END_OF_FILE.
+ */
+
+ /* Reset buffer status. */
+ yyrestart( yyin , yyscanner);
+
+ /*FALLTHROUGH*/
+
+ case EOB_ACT_END_OF_FILE:
+ {
+ if ( yywrap( yyscanner ) )
+ return 0;
+
+ if ( ! yyg->yy_did_buffer_switch_on_eof )
+ YY_NEW_FILE;
+#ifdef __cplusplus
+ return yyinput(yyscanner);
+#else
+ return input(yyscanner);
+#endif
+ }
+
+ case EOB_ACT_CONTINUE_SCAN:
+ yyg->yy_c_buf_p = yyg->yytext_ptr + offset;
+ break;
+ }
+ }
+ }
+
+ c = *(unsigned char *) yyg->yy_c_buf_p; /* cast for 8-bit char's */
+ *yyg->yy_c_buf_p = '\0'; /* preserve yytext */
+ yyg->yy_hold_char = *++yyg->yy_c_buf_p;
+
+ return c;
+}
+#endif /* ifndef YY_NO_INPUT */
+
+/** Immediately switch to a different input stream.
+ * @param input_file A readable stream.
+ * @param yyscanner The scanner object.
+ * @note This function does not reset the start condition to @c INITIAL .
+ */
+ void yyrestart (FILE * input_file , yyscan_t yyscanner)
+{
+ struct yyguts_t * yyg = (struct yyguts_t*)yyscanner;
+
+ if ( ! YY_CURRENT_BUFFER ){
+ yyensure_buffer_stack (yyscanner);
+ YY_CURRENT_BUFFER_LVALUE =
+ yy_create_buffer( yyin, YY_BUF_SIZE , yyscanner);
+ }
+
+ yy_init_buffer( YY_CURRENT_BUFFER, input_file , yyscanner);
+ yy_load_buffer_state( yyscanner );
+}
+
+/** Switch to a different input buffer.
+ * @param new_buffer The new input buffer.
+ * @param yyscanner The scanner object.
+ */
+ void yy_switch_to_buffer (YY_BUFFER_STATE new_buffer , yyscan_t yyscanner)
+{
+ struct yyguts_t * yyg = (struct yyguts_t*)yyscanner;
+
+ /* TODO. We should be able to replace this entire function body
+ * with
+ * yypop_buffer_state();
+ * yypush_buffer_state(new_buffer);
+ */
+ yyensure_buffer_stack (yyscanner);
+ if ( YY_CURRENT_BUFFER == new_buffer )
+ return;
+
+ if ( YY_CURRENT_BUFFER )
+ {
+ /* Flush out information for old buffer. */
+ *yyg->yy_c_buf_p = yyg->yy_hold_char;
+ YY_CURRENT_BUFFER_LVALUE->yy_buf_pos = yyg->yy_c_buf_p;
+ YY_CURRENT_BUFFER_LVALUE->yy_n_chars = yyg->yy_n_chars;
+ }
+
+ YY_CURRENT_BUFFER_LVALUE = new_buffer;
+ yy_load_buffer_state( yyscanner );
+
+ /* We don't actually know whether we did this switch during
+ * EOF (yywrap()) processing, but the only time this flag
+ * is looked at is after yywrap() is called, so it's safe
+ * to go ahead and always set it.
+ */
+ yyg->yy_did_buffer_switch_on_eof = 1;
+}
+
+static void yy_load_buffer_state (yyscan_t yyscanner)
+{
+ struct yyguts_t * yyg = (struct yyguts_t*)yyscanner;
+ yyg->yy_n_chars = YY_CURRENT_BUFFER_LVALUE->yy_n_chars;
+ yyg->yytext_ptr = yyg->yy_c_buf_p = YY_CURRENT_BUFFER_LVALUE->yy_buf_pos;
+ yyin = YY_CURRENT_BUFFER_LVALUE->yy_input_file;
+ yyg->yy_hold_char = *yyg->yy_c_buf_p;
+}
+
+/** Allocate and initialize an input buffer state.
+ * @param file A readable stream.
+ * @param size The character buffer size in bytes. When in doubt, use @c YY_BUF_SIZE.
+ * @param yyscanner The scanner object.
+ * @return the allocated buffer state.
+ */
+ YY_BUFFER_STATE yy_create_buffer (FILE * file, int size , yyscan_t yyscanner)
+{
+ YY_BUFFER_STATE b;
+
+ b = (YY_BUFFER_STATE) yyalloc( sizeof( struct yy_buffer_state ) , yyscanner );
+ if ( ! b )
+ YY_FATAL_ERROR( "out of dynamic memory in yy_create_buffer()" );
+
+ b->yy_buf_size = size;
+
+ /* yy_ch_buf has to be 2 characters longer than the size given because
+ * we need to put in 2 end-of-buffer characters.
+ */
+ b->yy_ch_buf = (char *) yyalloc( (yy_size_t) (b->yy_buf_size + 2) , yyscanner );
+ if ( ! b->yy_ch_buf )
+ YY_FATAL_ERROR( "out of dynamic memory in yy_create_buffer()" );
+
+ b->yy_is_our_buffer = 1;
+
+ yy_init_buffer( b, file , yyscanner);
+
+ return b;
+}
+
+/** Destroy the buffer.
+ * @param b a buffer created with yy_create_buffer()
+ * @param yyscanner The scanner object.
+ */
+ void yy_delete_buffer (YY_BUFFER_STATE b , yyscan_t yyscanner)
+{
+ struct yyguts_t * yyg = (struct yyguts_t*)yyscanner;
+
+ if ( ! b )
+ return;
+
+ if ( b == YY_CURRENT_BUFFER ) /* Not sure if we should pop here. */
+ YY_CURRENT_BUFFER_LVALUE = (YY_BUFFER_STATE) 0;
+
+ if ( b->yy_is_our_buffer )
+ yyfree( (void *) b->yy_ch_buf , yyscanner );
+
+ yyfree( (void *) b , yyscanner );
+}
+
+/* Initializes or reinitializes a buffer.
+ * This function is sometimes called more than once on the same buffer,
+ * such as during a yyrestart() or at EOF.
+ */
+ static void yy_init_buffer (YY_BUFFER_STATE b, FILE * file , yyscan_t yyscanner)
+
+{
+ int oerrno = errno;
+ struct yyguts_t * yyg = (struct yyguts_t*)yyscanner;
+
+ yy_flush_buffer( b , yyscanner);
+
+ b->yy_input_file = file;
+ b->yy_fill_buffer = 1;
+
+ /* If b is the current buffer, then yy_init_buffer was _probably_
+ * called from yyrestart() or through yy_get_next_buffer.
+ * In that case, we don't want to reset the lineno or column.
+ */
+ if (b != YY_CURRENT_BUFFER){
+ b->yy_bs_lineno = 1;
+ b->yy_bs_column = 0;
+ }
+
+ b->yy_is_interactive = 0;
+
+ errno = oerrno;
+}
+
+/** Discard all buffered characters. On the next scan, YY_INPUT will be called.
+ * @param b the buffer state to be flushed, usually @c YY_CURRENT_BUFFER.
+ * @param yyscanner The scanner object.
+ */
+ void yy_flush_buffer (YY_BUFFER_STATE b , yyscan_t yyscanner)
+{
+ struct yyguts_t * yyg = (struct yyguts_t*)yyscanner;
+ if ( ! b )
+ return;
+
+ b->yy_n_chars = 0;
+
+ /* We always need two end-of-buffer characters. The first causes
+ * a transition to the end-of-buffer state. The second causes
+ * a jam in that state.
+ */
+ b->yy_ch_buf[0] = YY_END_OF_BUFFER_CHAR;
+ b->yy_ch_buf[1] = YY_END_OF_BUFFER_CHAR;
+
+ b->yy_buf_pos = &b->yy_ch_buf[0];
+
+ b->yy_at_bol = 1;
+ b->yy_buffer_status = YY_BUFFER_NEW;
+
+ if ( b == YY_CURRENT_BUFFER )
+ yy_load_buffer_state( yyscanner );
+}
+
+/** Pushes the new state onto the stack. The new state becomes
+ * the current state. This function will allocate the stack
+ * if necessary.
+ * @param new_buffer The new state.
+ * @param yyscanner The scanner object.
+ */
+void yypush_buffer_state (YY_BUFFER_STATE new_buffer , yyscan_t yyscanner)
+{
+ struct yyguts_t * yyg = (struct yyguts_t*)yyscanner;
+ if (new_buffer == NULL)
+ return;
+
+ yyensure_buffer_stack(yyscanner);
+
+ /* This block is copied from yy_switch_to_buffer. */
+ if ( YY_CURRENT_BUFFER )
+ {
+ /* Flush out information for old buffer. */
+ *yyg->yy_c_buf_p = yyg->yy_hold_char;
+ YY_CURRENT_BUFFER_LVALUE->yy_buf_pos = yyg->yy_c_buf_p;
+ YY_CURRENT_BUFFER_LVALUE->yy_n_chars = yyg->yy_n_chars;
+ }
+
+ /* Only push if top exists. Otherwise, replace top. */
+ if (YY_CURRENT_BUFFER)
+ yyg->yy_buffer_stack_top++;
+ YY_CURRENT_BUFFER_LVALUE = new_buffer;
+
+ /* copied from yy_switch_to_buffer. */
+ yy_load_buffer_state( yyscanner );
+ yyg->yy_did_buffer_switch_on_eof = 1;
+}
+
+/** Removes and deletes the top of the stack, if present.
+ * The next element becomes the new top.
+ * @param yyscanner The scanner object.
+ */
+void yypop_buffer_state (yyscan_t yyscanner)
+{
+ struct yyguts_t * yyg = (struct yyguts_t*)yyscanner;
+ if (!YY_CURRENT_BUFFER)
+ return;
+
+ yy_delete_buffer(YY_CURRENT_BUFFER , yyscanner);
+ YY_CURRENT_BUFFER_LVALUE = NULL;
+ if (yyg->yy_buffer_stack_top > 0)
+ --yyg->yy_buffer_stack_top;
+
+ if (YY_CURRENT_BUFFER) {
+ yy_load_buffer_state( yyscanner );
+ yyg->yy_did_buffer_switch_on_eof = 1;
+ }
+}
+
+/* Allocates the stack if it does not exist.
+ * Guarantees space for at least one push.
+ */
+static void yyensure_buffer_stack (yyscan_t yyscanner)
+{
+ yy_size_t num_to_alloc;
+ struct yyguts_t * yyg = (struct yyguts_t*)yyscanner;
+
+ if (!yyg->yy_buffer_stack) {
+
+ /* First allocation is just for 2 elements, since we don't know if this
+ * scanner will even need a stack. We use 2 instead of 1 to avoid an
+ * immediate realloc on the next call.
+ */
+ num_to_alloc = 1; /* After all that talk, this was set to 1 anyways... */
+ yyg->yy_buffer_stack = (struct yy_buffer_state**)yyalloc
+ (num_to_alloc * sizeof(struct yy_buffer_state*)
+ , yyscanner);
+ if ( ! yyg->yy_buffer_stack )
+ YY_FATAL_ERROR( "out of dynamic memory in yyensure_buffer_stack()" );
+
+ memset(yyg->yy_buffer_stack, 0, num_to_alloc * sizeof(struct yy_buffer_state*));
+
+ yyg->yy_buffer_stack_max = num_to_alloc;
+ yyg->yy_buffer_stack_top = 0;
+ return;
+ }
+
+ if (yyg->yy_buffer_stack_top >= (yyg->yy_buffer_stack_max) - 1){
+
+ /* Increase the buffer to prepare for a possible push. */
+ yy_size_t grow_size = 8 /* arbitrary grow size */;
+
+ num_to_alloc = yyg->yy_buffer_stack_max + grow_size;
+ yyg->yy_buffer_stack = (struct yy_buffer_state**)yyrealloc
+ (yyg->yy_buffer_stack,
+ num_to_alloc * sizeof(struct yy_buffer_state*)
+ , yyscanner);
+ if ( ! yyg->yy_buffer_stack )
+ YY_FATAL_ERROR( "out of dynamic memory in yyensure_buffer_stack()" );
+
+ /* zero only the new slots.*/
+ memset(yyg->yy_buffer_stack + yyg->yy_buffer_stack_max, 0, grow_size * sizeof(struct yy_buffer_state*));
+ yyg->yy_buffer_stack_max = num_to_alloc;
+ }
+}
+
+/** Setup the input buffer state to scan directly from a user-specified character buffer.
+ * @param base the character buffer
+ * @param size the size in bytes of the character buffer
+ * @param yyscanner The scanner object.
+ * @return the newly allocated buffer state object.
+ */
+YY_BUFFER_STATE yy_scan_buffer (char * base, yy_size_t size , yyscan_t yyscanner)
+{
+ YY_BUFFER_STATE b;
+
+ if ( size < 2 ||
+ base[size-2] != YY_END_OF_BUFFER_CHAR ||
+ base[size-1] != YY_END_OF_BUFFER_CHAR )
+ /* They forgot to leave room for the EOB's. */
+ return NULL;
+
+ b = (YY_BUFFER_STATE) yyalloc( sizeof( struct yy_buffer_state ) , yyscanner );
+ if ( ! b )
+ YY_FATAL_ERROR( "out of dynamic memory in yy_scan_buffer()" );
+
+ b->yy_buf_size = (int) (size - 2); /* "- 2" to take care of EOB's */
+ b->yy_buf_pos = b->yy_ch_buf = base;
+ b->yy_is_our_buffer = 0;
+ b->yy_input_file = NULL;
+ b->yy_n_chars = b->yy_buf_size;
+ b->yy_is_interactive = 0;
+ b->yy_at_bol = 1;
+ b->yy_fill_buffer = 0;
+ b->yy_buffer_status = YY_BUFFER_NEW;
+
+ yy_switch_to_buffer( b , yyscanner );
+
+ return b;
+}
+
+/** Setup the input buffer state to scan a string. The next call to yylex() will
+ * scan from a @e copy of @a str.
+ * @param yystr a NUL-terminated string to scan
+ * @param yyscanner The scanner object.
+ * @return the newly allocated buffer state object.
+ * @note If you want to scan bytes that may contain NUL values, then use
+ * yy_scan_bytes() instead.
+ */
+YY_BUFFER_STATE yy_scan_string (const char * yystr , yyscan_t yyscanner)
+{
+
+ return yy_scan_bytes( yystr, (int) strlen(yystr) , yyscanner);
+}
+
+/** Setup the input buffer state to scan the given bytes. The next call to yylex() will
+ * scan from a @e copy of @a bytes.
+ * @param yybytes the byte buffer to scan
+ * @param _yybytes_len the number of bytes in the buffer pointed to by @a bytes.
+ * @param yyscanner The scanner object.
+ * @return the newly allocated buffer state object.
+ */
+YY_BUFFER_STATE yy_scan_bytes (const char * yybytes, int _yybytes_len , yyscan_t yyscanner)
+{
+ YY_BUFFER_STATE b;
+ char *buf;
+ yy_size_t n;
+ int i;
+
+ /* Get memory for full buffer, including space for trailing EOB's. */
+ n = (yy_size_t) (_yybytes_len + 2);
+ buf = (char *) yyalloc( n , yyscanner );
+ if ( ! buf )
+ YY_FATAL_ERROR( "out of dynamic memory in yy_scan_bytes()" );
+
+ for ( i = 0; i < _yybytes_len; ++i )
+ buf[i] = yybytes[i];
+
+ buf[_yybytes_len] = buf[_yybytes_len+1] = YY_END_OF_BUFFER_CHAR;
+
+ b = yy_scan_buffer( buf, n , yyscanner);
+ if ( ! b )
+ YY_FATAL_ERROR( "bad buffer in yy_scan_bytes()" );
+
+ /* It's okay to grow etc. this buffer, and we should throw it
+ * away when we're done.
+ */
+ b->yy_is_our_buffer = 1;
+
+ return b;
+}
+
+#ifndef YY_EXIT_FAILURE
+#define YY_EXIT_FAILURE 2
+#endif
+
+static void yynoreturn yy_fatal_error (const char* msg , yyscan_t yyscanner)
+{
+ struct yyguts_t * yyg = (struct yyguts_t*)yyscanner;
+ (void)yyg;
+ fprintf( stderr, "%s\n", msg );
+ exit( YY_EXIT_FAILURE );
+}
+
+/* Redefine yyless() so it works in section 3 code. */
+
+#undef yyless
+#define yyless(n) \
+ do \
+ { \
+ /* Undo effects of setting up yytext. */ \
+ int yyless_macro_arg = (n); \
+ YY_LESS_LINENO(yyless_macro_arg);\
+ yytext[yyleng] = yyg->yy_hold_char; \
+ yyg->yy_c_buf_p = yytext + yyless_macro_arg; \
+ yyg->yy_hold_char = *yyg->yy_c_buf_p; \
+ *yyg->yy_c_buf_p = '\0'; \
+ yyleng = yyless_macro_arg; \
+ } \
+ while ( 0 )
+
+/* Accessor methods (get/set functions) to struct members. */
+
+/** Get the user-defined data for this scanner.
+ * @param yyscanner The scanner object.
+ */
+YY_EXTRA_TYPE yyget_extra (yyscan_t yyscanner)
+{
+ struct yyguts_t * yyg = (struct yyguts_t*)yyscanner;
+ return yyextra;
+}
+
+/** Get the current line number.
+ * @param yyscanner The scanner object.
+ */
+int yyget_lineno (yyscan_t yyscanner)
+{
+ struct yyguts_t * yyg = (struct yyguts_t*)yyscanner;
+
+ if (! YY_CURRENT_BUFFER)
+ return 0;
+
+ return yylineno;
+}
+
+/** Get the current column number.
+ * @param yyscanner The scanner object.
+ */
+int yyget_column (yyscan_t yyscanner)
+{
+ struct yyguts_t * yyg = (struct yyguts_t*)yyscanner;
+
+ if (! YY_CURRENT_BUFFER)
+ return 0;
+
+ return yycolumn;
+}
+
+/** Get the input stream.
+ * @param yyscanner The scanner object.
+ */
+FILE *yyget_in (yyscan_t yyscanner)
+{
+ struct yyguts_t * yyg = (struct yyguts_t*)yyscanner;
+ return yyin;
+}
+
+/** Get the output stream.
+ * @param yyscanner The scanner object.
+ */
+FILE *yyget_out (yyscan_t yyscanner)
+{
+ struct yyguts_t * yyg = (struct yyguts_t*)yyscanner;
+ return yyout;
+}
+
+/** Get the length of the current token.
+ * @param yyscanner The scanner object.
+ */
+int yyget_leng (yyscan_t yyscanner)
+{
+ struct yyguts_t * yyg = (struct yyguts_t*)yyscanner;
+ return yyleng;
+}
+
+/** Get the current token.
+ * @param yyscanner The scanner object.
+ */
+
+char *yyget_text (yyscan_t yyscanner)
+{
+ struct yyguts_t * yyg = (struct yyguts_t*)yyscanner;
+ return yytext;
+}
+
+/** Set the user-defined data. This data is never touched by the scanner.
+ * @param user_defined The data to be associated with this scanner.
+ * @param yyscanner The scanner object.
+ */
+void yyset_extra (YY_EXTRA_TYPE user_defined , yyscan_t yyscanner)
+{
+ struct yyguts_t * yyg = (struct yyguts_t*)yyscanner;
+ yyextra = user_defined ;
+}
+
+/** Set the current line number.
+ * @param _line_number line number
+ * @param yyscanner The scanner object.
+ */
+void yyset_lineno (int _line_number , yyscan_t yyscanner)
+{
+ struct yyguts_t * yyg = (struct yyguts_t*)yyscanner;
+
+ /* lineno is only valid if an input buffer exists. */
+ if (! YY_CURRENT_BUFFER )
+ YY_FATAL_ERROR( "yyset_lineno called with no buffer" );
+
+ yylineno = _line_number;
+}
+
+/** Set the current column.
+ * @param _column_no column number
+ * @param yyscanner The scanner object.
+ */
+void yyset_column (int _column_no , yyscan_t yyscanner)
+{
+ struct yyguts_t * yyg = (struct yyguts_t*)yyscanner;
+
+ /* column is only valid if an input buffer exists. */
+ if (! YY_CURRENT_BUFFER )
+ YY_FATAL_ERROR( "yyset_column called with no buffer" );
+
+ yycolumn = _column_no;
+}
+
+/** Set the input stream. This does not discard the current
+ * input buffer.
+ * @param _in_str A readable stream.
+ * @param yyscanner The scanner object.
+ * @see yy_switch_to_buffer
+ */
+void yyset_in (FILE * _in_str , yyscan_t yyscanner)
+{
+ struct yyguts_t * yyg = (struct yyguts_t*)yyscanner;
+ yyin = _in_str ;
+}
+
+void yyset_out (FILE * _out_str , yyscan_t yyscanner)
+{
+ struct yyguts_t * yyg = (struct yyguts_t*)yyscanner;
+ yyout = _out_str ;
+}
+
+int yyget_debug (yyscan_t yyscanner)
+{
+ struct yyguts_t * yyg = (struct yyguts_t*)yyscanner;
+ return yy_flex_debug;
+}
+
+void yyset_debug (int _bdebug , yyscan_t yyscanner)
+{
+ struct yyguts_t * yyg = (struct yyguts_t*)yyscanner;
+ yy_flex_debug = _bdebug ;
+}
+
+/* Accessor methods for yylval and yylloc */
+
+YYSTYPE * yyget_lval (yyscan_t yyscanner)
+{
+ struct yyguts_t * yyg = (struct yyguts_t*)yyscanner;
+ return yylval;
+}
+
+void yyset_lval (YYSTYPE * yylval_param , yyscan_t yyscanner)
+{
+ struct yyguts_t * yyg = (struct yyguts_t*)yyscanner;
+ yylval = yylval_param;
+}
+
+/* User-visible API */
+
+/* yylex_init is special because it creates the scanner itself, so it is
+ * the ONLY reentrant function that doesn't take the scanner as the last argument.
+ * That's why we explicitly handle the declaration, instead of using our macros.
+ */
+int yylex_init(yyscan_t* ptr_yy_globals)
+{
+ if (ptr_yy_globals == NULL){
+ errno = EINVAL;
+ return 1;
+ }
+
+ *ptr_yy_globals = (yyscan_t) yyalloc ( sizeof( struct yyguts_t ), NULL );
+
+ if (*ptr_yy_globals == NULL){
+ errno = ENOMEM;
+ return 1;
+ }
+
+ /* By setting to 0xAA, we expose bugs in yy_init_globals. Leave at 0x00 for releases. */
+ memset(*ptr_yy_globals,0x00,sizeof(struct yyguts_t));
+
+ return yy_init_globals ( *ptr_yy_globals );
+}
+
+/* yylex_init_extra has the same functionality as yylex_init, but follows the
+ * convention of taking the scanner as the last argument. Note however, that
+ * this is a *pointer* to a scanner, as it will be allocated by this call (and
+ * is the reason, too, why this function also must handle its own declaration).
+ * The user defined value in the first argument will be available to yyalloc in
+ * the yyextra field.
+ */
+int yylex_init_extra( YY_EXTRA_TYPE yy_user_defined, yyscan_t* ptr_yy_globals )
+{
+ struct yyguts_t dummy_yyguts;
+
+ yyset_extra (yy_user_defined, &dummy_yyguts);
+
+ if (ptr_yy_globals == NULL){
+ errno = EINVAL;
+ return 1;
+ }
+
+ *ptr_yy_globals = (yyscan_t) yyalloc ( sizeof( struct yyguts_t ), &dummy_yyguts );
+
+ if (*ptr_yy_globals == NULL){
+ errno = ENOMEM;
+ return 1;
+ }
+
+ /* By setting to 0xAA, we expose bugs in
+ yy_init_globals. Leave at 0x00 for releases. */
+ memset(*ptr_yy_globals,0x00,sizeof(struct yyguts_t));
+
+ yyset_extra (yy_user_defined, *ptr_yy_globals);
+
+ return yy_init_globals ( *ptr_yy_globals );
+}
+
+static int yy_init_globals (yyscan_t yyscanner)
+{
+ struct yyguts_t * yyg = (struct yyguts_t*)yyscanner;
+ /* Initialization is the same as for the non-reentrant scanner.
+ * This function is called from yylex_destroy(), so don't allocate here.
+ */
+
+ yyg->yy_buffer_stack = NULL;
+ yyg->yy_buffer_stack_top = 0;
+ yyg->yy_buffer_stack_max = 0;
+ yyg->yy_c_buf_p = NULL;
+ yyg->yy_init = 0;
+ yyg->yy_start = 0;
+
+ yyg->yy_start_stack_ptr = 0;
+ yyg->yy_start_stack_depth = 0;
+ yyg->yy_start_stack = NULL;
+
+/* Defined in main.c */
+#ifdef YY_STDINIT
+ yyin = stdin;
+ yyout = stdout;
+#else
+ yyin = NULL;
+ yyout = NULL;
+#endif
+
+ /* For future reference: Set errno on error, since we are called by
+ * yylex_init()
+ */
+ return 0;
+}
+
+/* yylex_destroy is for both reentrant and non-reentrant scanners. */
+int yylex_destroy (yyscan_t yyscanner)
+{
+ struct yyguts_t * yyg = (struct yyguts_t*)yyscanner;
+
+ /* Pop the buffer stack, destroying each element. */
+ while(YY_CURRENT_BUFFER){
+ yy_delete_buffer( YY_CURRENT_BUFFER , yyscanner );
+ YY_CURRENT_BUFFER_LVALUE = NULL;
+ yypop_buffer_state(yyscanner);
+ }
+
+ /* Destroy the stack itself. */
+ yyfree(yyg->yy_buffer_stack , yyscanner);
+ yyg->yy_buffer_stack = NULL;
+
+ /* Destroy the start condition stack. */
+ yyfree( yyg->yy_start_stack , yyscanner );
+ yyg->yy_start_stack = NULL;
+
+ /* Reset the globals. This is important in a non-reentrant scanner so the next time
+ * yylex() is called, initialization will occur. */
+ yy_init_globals( yyscanner);
+
+ /* Destroy the main struct (reentrant only). */
+ yyfree ( yyscanner , yyscanner );
+ yyscanner = NULL;
+ return 0;
+}
+
+/*
+ * Internal utility routines.
+ */
+
+#ifndef yytext_ptr
+static void yy_flex_strncpy (char* s1, const char * s2, int n , yyscan_t yyscanner)
+{
+ struct yyguts_t * yyg = (struct yyguts_t*)yyscanner;
+ (void)yyg;
+
+ int i;
+ for ( i = 0; i < n; ++i )
+ s1[i] = s2[i];
+}
+#endif
+
+#ifdef YY_NEED_STRLEN
+static int yy_flex_strlen (const char * s , yyscan_t yyscanner)
+{
+ int n;
+ for ( n = 0; s[n]; ++n )
+ ;
+
+ return n;
+}
+#endif
+
+#define YYTABLES_NAME "yytables"
+
+#line 103 "event-filter-lexer.l"
+
+#ifdef __clang__
+#pragma clang diagnostic pop
+#endif
+
+void *yyalloc(size_t bytes, void* yyscanner ATTR_UNUSED)
+{
+ void *ptr = calloc(1, bytes);
+ if (ptr == NULL)
+ i_fatal_status(FATAL_OUTOFMEM, "calloc(1, %zu): Out of memory",
+ bytes);
+ return ptr;
+}
+
+void *yyrealloc (void *ptr, size_t bytes, void *yyscanner ATTR_UNUSED)
+{
+ void *nptr = realloc(ptr, bytes);
+ if (nptr == NULL)
+ i_fatal_status(FATAL_OUTOFMEM, "realloc(ptr, %zu): Out of memory",
+ bytes);
+ return nptr;
+}
+
+void yyfree(void *ptr, void *yyscanner ATTR_UNUSED)
+{
+ if (ptr == NULL)
+ return;
+ free(ptr);
+}
+
+static size_t event_filter_parser_input_proc(char *buf, size_t size, yyscan_t scanner)
+{
+ struct event_filter_parser_state *state;
+ size_t num_bytes;
+
+ state = event_filter_parser_get_extra(scanner);
+
+ if (state->len == state->pos)
+ return 0;
+
+ i_assert(state->len > state->pos);
+
+ num_bytes = I_MIN(state->len - state->pos, size);
+ memcpy(buf, state->input + state->pos, num_bytes);
+ state->pos += num_bytes;
+
+ return num_bytes;
+}
+
diff --git a/src/lib/event-filter-lexer.l b/src/lib/event-filter-lexer.l
new file mode 100644
index 0000000..08af117
--- /dev/null
+++ b/src/lib/event-filter-lexer.l
@@ -0,0 +1,149 @@
+/* Copyright (c) 2020 Dovecot authors, see the included COPYING file */
+
+%option nounput
+%option noinput
+%option noyywrap
+%option noyyalloc noyyrealloc noyyfree
+%option reentrant
+%option bison-bridge
+%option never-interactive
+%option prefix="event_filter_parser_"
+
+%{
+#include "lib.h"
+#include "str.h"
+#include "event-filter-private.h"
+#include "event-filter-parser.h"
+
+#define YY_FATAL_ERROR(msg) { i_fatal("event filter parsing: %s", (msg)); }
+
+/* mimic renaming done by bison's api.prefix %define */
+#define YYSTYPE EVENT_FILTER_PARSER_STYPE
+
+#define YY_INPUT(buf, result, max_size) \
+ result = event_filter_parser_input_proc(buf, max_size, yyscanner)
+static size_t event_filter_parser_input_proc(char *buf, size_t size, yyscan_t scanner);
+
+#ifdef __clang__
+#pragma clang diagnostic push
+/* ignore "unknown warning" warning if we're using unpatched clang */
+#pragma clang diagnostic ignored "-Wunknown-warning-option"
+/* ignore strict bool warnings in generated code */
+#pragma clang diagnostic ignored "-Wstrict-bool"
+/* ignore sign comparison errors (buggy flex) */
+#pragma clang diagnostic ignored "-Wsign-compare"
+/* ignore unused functions */
+#pragma clang diagnostic ignored "-Wunused-function"
+/* ignore unused parameters */
+#pragma clang diagnostic ignored "-Wunused-parameter"
+#else
+/* and same for gcc */
+#pragma GCC diagnostic push
+#pragma GCC diagnostic ignored "-Wsign-compare"
+#pragma GCC diagnostic ignored "-Wunused-function"
+#pragma GCC diagnostic ignored "-Wunused-parameter"
+#endif
+
+%}
+
+%x string
+
+%%
+ string_t *str_buf = NULL;
+
+\" {
+ BEGIN(string);
+
+ str_buf = t_str_new(128);
+ }
+<string>\" {
+ yylval->str = str_c(str_buf);
+ BEGIN(INITIAL);
+ return STRING;
+ }
+ /* Note: these have to match the event_filter_append_escaped() behavior */
+<string>[^\\"]+ { str_append(str_buf, yytext); }
+<string>\\\\ { str_append_c(str_buf, '\\'); }
+<string>\\\" { str_append_c(str_buf, '"'); }
+<string>\\. { str_append(str_buf, yytext); }
+
+[Aa][Nn][Dd] { return AND; }
+[Oo][Rr] { return OR; }
+[Nn][Oo][Tt] { return NOT; }
+[<>=()] { return *yytext; }
+[A-Za-z0-9:.*?_-]+ { yylval->str = t_strdup(yytext); return TOKEN; }
+[ \t\n\r] { /* ignore */ }
+. {
+ /*
+ * We simply return the char to the
+ * and let the grammar error out
+ * with a syntax error.
+ *
+ * Note: The cast is significant
+ * since utf-8 bytes >=128 will
+ * otherwise result in sign
+ * extension and a negative int
+ * getting returned on some
+ * platforms (e.g., x86) which in
+ * turn confuses the parser. E.g.,
+ * if:
+ * *yytext = '\x80'
+ * we get:
+ * *yytext -> -128
+ * (int) *yytext -> -128
+ * which is wrong. With the
+ * unsigned char cast, we get:
+ * (u.c.) *yytext -> 128
+ * (int)(u.c.) *yytext -> 128
+ * which is correct.
+ */
+ return (unsigned char) *yytext;
+ }
+%%
+#ifdef __clang__
+#pragma clang diagnostic pop
+#endif
+
+void *yyalloc(size_t bytes, void* yyscanner ATTR_UNUSED)
+{
+ void *ptr = calloc(1, bytes);
+ if (ptr == NULL)
+ i_fatal_status(FATAL_OUTOFMEM, "calloc(1, %zu): Out of memory",
+ bytes);
+ return ptr;
+}
+
+void *yyrealloc (void *ptr, size_t bytes, void *yyscanner ATTR_UNUSED)
+{
+ void *nptr = realloc(ptr, bytes);
+ if (nptr == NULL)
+ i_fatal_status(FATAL_OUTOFMEM, "realloc(ptr, %zu): Out of memory",
+ bytes);
+ return nptr;
+}
+
+void yyfree(void *ptr, void *yyscanner ATTR_UNUSED)
+{
+ if (ptr == NULL)
+ return;
+ free(ptr);
+}
+
+static size_t event_filter_parser_input_proc(char *buf, size_t size, yyscan_t scanner)
+{
+ struct event_filter_parser_state *state;
+ size_t num_bytes;
+
+ state = event_filter_parser_get_extra(scanner);
+
+ if (state->len == state->pos)
+ return 0;
+
+ i_assert(state->len > state->pos);
+
+ num_bytes = I_MIN(state->len - state->pos, size);
+ memcpy(buf, state->input + state->pos, num_bytes);
+ state->pos += num_bytes;
+
+ return num_bytes;
+}
diff --git a/src/lib/event-filter-parser.c b/src/lib/event-filter-parser.c
new file mode 100644
index 0000000..db0258a
--- /dev/null
+++ b/src/lib/event-filter-parser.c
@@ -0,0 +1,1730 @@
+/* A Bison parser, made by GNU Bison 3.3.2. */
+
+/* Bison implementation for Yacc-like parsers in C
+
+ Copyright (C) 1984, 1989-1990, 2000-2015, 2018-2019 Free Software Foundation,
+ Inc.
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>. */
+
+/* As a special exception, you may create a larger work that contains
+ part or all of the Bison parser skeleton and distribute that work
+ under terms of your choice, so long as that work isn't itself a
+ parser generator using the skeleton or a modified version thereof
+ as a parser skeleton. Alternatively, if you modify or redistribute
+ the parser skeleton itself, you may (at your option) remove this
+ special exception, which will cause the skeleton and the resulting
+ Bison output files to be licensed under the GNU General Public
+ License without this special exception.
+
+ This special exception was added by the Free Software Foundation in
+ version 2.2 of Bison. */
+
+/* C LALR(1) parser skeleton written by Richard Stallman, by
+ simplifying the original so-called "semantic" parser. */
+
+/* All symbols defined below should begin with yy or YY, to avoid
+ infringing on user name space. This should be done even for local
+ variables, as they might otherwise be expanded by user macros.
+ There are some unavoidable exceptions within include files to
+ define necessary library symbols; they are noted "INFRINGES ON
+ USER NAME SPACE" below. */
+
+/* Undocumented macros, especially those whose name start with YY_,
+ are private implementation details. Do not rely on them. */
+
+/* Identify Bison output. */
+#define YYBISON 1
+
+/* Bison version. */
+#define YYBISON_VERSION "3.3.2"
+
+/* Skeleton name. */
+#define YYSKELETON_NAME "yacc.c"
+
+/* Pure parsers. */
+#define YYPURE 1
+
+/* Push parsers. */
+#define YYPUSH 0
+
+/* Pull parsers. */
+#define YYPULL 1
+
+/* Substitute the type names. */
+#define YYSTYPE EVENT_FILTER_PARSER_STYPE
+/* Substitute the variable and function names. */
+#define yyparse event_filter_parser_parse
+#define yylex event_filter_parser_lex
+#define yyerror event_filter_parser_error
+#define yydebug event_filter_parser_debug
+#define yynerrs event_filter_parser_nerrs
+
+
+/* First part of user prologue. */
+#line 11 "event-filter-parser.y" /* yacc.c:337 */
+
+#include "lib.h"
+#include "wildcard-match.h"
+#include "lib-event-private.h"
+#include "event-filter-private.h"
+
+#define scanner state->scanner
+
+#define YYERROR_VERBOSE
+
+extern int event_filter_parser_lex(void *, void *);
+
+void event_filter_parser_error(void *scan, const char *e)
+{
+ struct event_filter_parser_state *state = scan;
+
+ state->error = t_strdup_printf("event filter: %s", e);
+}
+
+static struct event_filter_node *key_value(struct event_filter_parser_state *state,
+ const char *a, const char *b,
+ enum event_filter_node_op op)
+{
+ struct event_filter_node *node;
+ enum event_filter_node_type type;
+
+ if (strcmp(a, "event") == 0)
+ type = EVENT_FILTER_NODE_TYPE_EVENT_NAME_WILDCARD;
+ else if (strcmp(a, "category") == 0)
+ type = EVENT_FILTER_NODE_TYPE_EVENT_CATEGORY;
+ else if (strcmp(a, "source_location") == 0)
+ type = EVENT_FILTER_NODE_TYPE_EVENT_SOURCE_LOCATION;
+ else
+ type = EVENT_FILTER_NODE_TYPE_EVENT_FIELD_WILDCARD;
+
+ /* only fields support comparators other than EQ */
+ if ((type != EVENT_FILTER_NODE_TYPE_EVENT_FIELD_WILDCARD) &&
+ (op != EVENT_FILTER_OP_CMP_EQ)) {
+ state->error = "Only fields support inequality comparisons";
+ return NULL;
+ }
+
+ node = p_new(state->pool, struct event_filter_node, 1);
+ node->type = type;
+ node->op = op;
+
+ switch (type) {
+ case EVENT_FILTER_NODE_TYPE_LOGIC:
+ i_unreached();
+ case EVENT_FILTER_NODE_TYPE_EVENT_NAME_WILDCARD:
+ node->str = p_strdup(state->pool, b);
+ if (wildcard_is_literal(node->str))
+ node->type = EVENT_FILTER_NODE_TYPE_EVENT_NAME_EXACT;
+ break;
+ case EVENT_FILTER_NODE_TYPE_EVENT_SOURCE_LOCATION: {
+ const char *colon = strrchr(b, ':');
+ const char *file;
+ uintmax_t line;
+
+ /* split "filename:line-number", but also handle "filename" */
+ if (colon != NULL) {
+ if (str_to_uintmax(colon + 1, &line) < 0) {
+ file = p_strdup(state->pool, b);
+ line = 0;
+ } else {
+ file = p_strdup_until(state->pool, b, colon);
+ }
+ } else {
+ file = p_strdup(state->pool, b);
+ line = 0;
+ }
+
+ node->str = file;
+ node->intmax = line;
+ break;
+ }
+ case EVENT_FILTER_NODE_TYPE_EVENT_CATEGORY:
+ if (!event_filter_category_to_log_type(b, &node->category.log_type)) {
+ node->category.name = p_strdup(state->pool, b);
+ node->category.ptr = event_category_find_registered(b);
+ }
+ break;
+ case EVENT_FILTER_NODE_TYPE_EVENT_FIELD_WILDCARD:
+ node->field.key = p_strdup(state->pool, a);
+ node->field.value.str = p_strdup(state->pool, b);
+
+ /* Filter currently supports only comparing strings
+ and numbers. */
+ if (str_to_intmax(b, &node->field.value.intmax) < 0) {
+ /* not a number - no problem
+ Either we have a string, or a number with wildcards */
+ node->field.value.intmax = INT_MIN;
+ }
+
+ if (wildcard_is_literal(node->field.value.str))
+ node->type = EVENT_FILTER_NODE_TYPE_EVENT_FIELD_EXACT;
+ break;
+ case EVENT_FILTER_NODE_TYPE_EVENT_NAME_EXACT:
+ case EVENT_FILTER_NODE_TYPE_EVENT_FIELD_EXACT:
+ i_unreached();
+ }
+
+ return node;
+}
+
+static struct event_filter_node *logic(struct event_filter_parser_state *state,
+ struct event_filter_node *a,
+ struct event_filter_node *b,
+ enum event_filter_node_op op)
+{
+ struct event_filter_node *node;
+
+ node = p_new(state->pool, struct event_filter_node, 1);
+ node->type = EVENT_FILTER_NODE_TYPE_LOGIC;
+ node->op = op;
+ node->children[0] = a;
+ node->children[1] = b;
+
+ return node;
+}
+
+#ifdef __clang__
+/* ignore "unknown warning" warning if we're using unpatched clang */
+#pragma clang diagnostic ignored "-Wunknown-warning-option"
+/* ignore strict bool warnings in generated code */
+#pragma clang diagnostic ignored "-Wstrict-bool"
+#endif
+
+#line 206 "event-filter-parser.c" /* yacc.c:337 */
+# ifndef YY_NULLPTR
+# if defined __cplusplus
+# if 201103L <= __cplusplus
+# define YY_NULLPTR nullptr
+# else
+# define YY_NULLPTR 0
+# endif
+# else
+# define YY_NULLPTR ((void*)0)
+# endif
+# endif
+
+/* Enabling verbose error messages. */
+#ifdef YYERROR_VERBOSE
+# undef YYERROR_VERBOSE
+# define YYERROR_VERBOSE 1
+#else
+# define YYERROR_VERBOSE 1
+#endif
+
+/* In a future release of Bison, this section will be replaced
+ by #include "event-filter-parser.h". */
+#ifndef YY_EVENT_FILTER_PARSER_EVENT_FILTER_PARSER_H_INCLUDED
+# define YY_EVENT_FILTER_PARSER_EVENT_FILTER_PARSER_H_INCLUDED
+/* Debug traces. */
+#ifndef EVENT_FILTER_PARSER_DEBUG
+# if defined YYDEBUG
+#if YYDEBUG
+# define EVENT_FILTER_PARSER_DEBUG 1
+# else
+# define EVENT_FILTER_PARSER_DEBUG 0
+# endif
+# else /* ! defined YYDEBUG */
+# define EVENT_FILTER_PARSER_DEBUG 0
+# endif /* ! defined YYDEBUG */
+#endif /* ! defined EVENT_FILTER_PARSER_DEBUG */
+#if EVENT_FILTER_PARSER_DEBUG
+extern int event_filter_parser_debug;
+#endif
+
+/* Token type. */
+#ifndef EVENT_FILTER_PARSER_TOKENTYPE
+# define EVENT_FILTER_PARSER_TOKENTYPE
+ enum event_filter_parser_tokentype
+ {
+ TOKEN = 258,
+ STRING = 259,
+ AND = 260,
+ OR = 261,
+ NOT = 262
+ };
+#endif
+
+/* Value type. */
+#if ! defined EVENT_FILTER_PARSER_STYPE && ! defined EVENT_FILTER_PARSER_STYPE_IS_DECLARED
+
+union EVENT_FILTER_PARSER_STYPE
+{
+#line 140 "event-filter-parser.y" /* yacc.c:352 */
+
+ const char *str;
+ enum event_filter_node_op op;
+ struct event_filter_node *node;
+
+#line 271 "event-filter-parser.c" /* yacc.c:352 */
+};
+
+typedef union EVENT_FILTER_PARSER_STYPE EVENT_FILTER_PARSER_STYPE;
+# define EVENT_FILTER_PARSER_STYPE_IS_TRIVIAL 1
+# define EVENT_FILTER_PARSER_STYPE_IS_DECLARED 1
+#endif
+
+
+
+int event_filter_parser_parse (struct event_filter_parser_state *state);
+
+#endif /* !YY_EVENT_FILTER_PARSER_EVENT_FILTER_PARSER_H_INCLUDED */
+
+
+
+#ifdef short
+# undef short
+#endif
+
+#ifdef YYTYPE_UINT8
+typedef YYTYPE_UINT8 yytype_uint8;
+#else
+typedef unsigned char yytype_uint8;
+#endif
+
+#ifdef YYTYPE_INT8
+typedef YYTYPE_INT8 yytype_int8;
+#else
+typedef signed char yytype_int8;
+#endif
+
+#ifdef YYTYPE_UINT16
+typedef YYTYPE_UINT16 yytype_uint16;
+#else
+typedef unsigned short yytype_uint16;
+#endif
+
+#ifdef YYTYPE_INT16
+typedef YYTYPE_INT16 yytype_int16;
+#else
+typedef short yytype_int16;
+#endif
+
+#ifndef YYSIZE_T
+# ifdef __SIZE_TYPE__
+# define YYSIZE_T __SIZE_TYPE__
+# elif defined size_t
+# define YYSIZE_T size_t
+# elif ! defined YYSIZE_T
+# include <stddef.h> /* INFRINGES ON USER NAME SPACE */
+# define YYSIZE_T size_t
+# else
+# define YYSIZE_T unsigned
+# endif
+#endif
+
+#define YYSIZE_MAXIMUM ((YYSIZE_T) -1)
+
+#ifndef YY_
+# if defined YYENABLE_NLS && YYENABLE_NLS
+# if ENABLE_NLS
+# include <libintl.h> /* INFRINGES ON USER NAME SPACE */
+# define YY_(Msgid) dgettext ("bison-runtime", Msgid)
+# endif
+# endif
+# ifndef YY_
+# define YY_(Msgid) Msgid
+# endif
+#endif
+
+#ifndef YY_ATTRIBUTE
+# if (defined __GNUC__ \
+ && (2 < __GNUC__ || (__GNUC__ == 2 && 96 <= __GNUC_MINOR__))) \
+ || defined __SUNPRO_C && 0x5110 <= __SUNPRO_C
+# define YY_ATTRIBUTE(Spec) __attribute__(Spec)
+# else
+# define YY_ATTRIBUTE(Spec) /* empty */
+# endif
+#endif
+
+#ifndef YY_ATTRIBUTE_PURE
+# define YY_ATTRIBUTE_PURE YY_ATTRIBUTE ((__pure__))
+#endif
+
+#ifndef YY_ATTRIBUTE_UNUSED
+# define YY_ATTRIBUTE_UNUSED YY_ATTRIBUTE ((__unused__))
+#endif
+
+/* Suppress unused-variable warnings by "using" E. */
+#if ! defined lint || defined __GNUC__
+# define YYUSE(E) ((void) (E))
+#else
+# define YYUSE(E) /* empty */
+#endif
+
+#if defined __GNUC__ && ! defined __ICC && 407 <= __GNUC__ * 100 + __GNUC_MINOR__
+/* Suppress an incorrect diagnostic about yylval being uninitialized. */
+# define YY_IGNORE_MAYBE_UNINITIALIZED_BEGIN \
+ _Pragma ("GCC diagnostic push") \
+ _Pragma ("GCC diagnostic ignored \"-Wuninitialized\"")\
+ _Pragma ("GCC diagnostic ignored \"-Wmaybe-uninitialized\"")
+# define YY_IGNORE_MAYBE_UNINITIALIZED_END \
+ _Pragma ("GCC diagnostic pop")
+#else
+# define YY_INITIAL_VALUE(Value) Value
+#endif
+#ifndef YY_IGNORE_MAYBE_UNINITIALIZED_BEGIN
+# define YY_IGNORE_MAYBE_UNINITIALIZED_BEGIN
+# define YY_IGNORE_MAYBE_UNINITIALIZED_END
+#endif
+#ifndef YY_INITIAL_VALUE
+# define YY_INITIAL_VALUE(Value) /* Nothing. */
+#endif
+
+
+#if ! defined yyoverflow || YYERROR_VERBOSE
+
+/* The parser invokes alloca or malloc; define the necessary symbols. */
+
+# ifdef YYSTACK_USE_ALLOCA
+# if YYSTACK_USE_ALLOCA
+# ifdef __GNUC__
+# define YYSTACK_ALLOC __builtin_alloca
+# elif defined __BUILTIN_VA_ARG_INCR
+# include <alloca.h> /* INFRINGES ON USER NAME SPACE */
+# elif defined _AIX
+# define YYSTACK_ALLOC __alloca
+# elif defined _MSC_VER
+# include <malloc.h> /* INFRINGES ON USER NAME SPACE */
+# define alloca _alloca
+# else
+# define YYSTACK_ALLOC alloca
+# if ! defined _ALLOCA_H && ! defined EXIT_SUCCESS
+# include <stdlib.h> /* INFRINGES ON USER NAME SPACE */
+ /* Use EXIT_SUCCESS as a witness for stdlib.h. */
+# ifndef EXIT_SUCCESS
+# define EXIT_SUCCESS 0
+# endif
+# endif
+# endif
+# endif
+# endif
+
+# ifdef YYSTACK_ALLOC
+ /* Pacify GCC's 'empty if-body' warning. */
+# define YYSTACK_FREE(Ptr) do { /* empty */; } while (0)
+# ifndef YYSTACK_ALLOC_MAXIMUM
+ /* The OS might guarantee only one guard page at the bottom of the stack,
+ and a page size can be as small as 4096 bytes. So we cannot safely
+ invoke alloca (N) if N exceeds 4096. Use a slightly smaller number
+ to allow for a few compiler-allocated temporary stack slots. */
+# define YYSTACK_ALLOC_MAXIMUM 4032 /* reasonable circa 2006 */
+# endif
+# else
+# define YYSTACK_ALLOC YYMALLOC
+# define YYSTACK_FREE YYFREE
+# ifndef YYSTACK_ALLOC_MAXIMUM
+# define YYSTACK_ALLOC_MAXIMUM YYSIZE_MAXIMUM
+# endif
+# if (defined __cplusplus && ! defined EXIT_SUCCESS \
+ && ! ((defined YYMALLOC || defined malloc) \
+ && (defined YYFREE || defined free)))
+# include <stdlib.h> /* INFRINGES ON USER NAME SPACE */
+# ifndef EXIT_SUCCESS
+# define EXIT_SUCCESS 0
+# endif
+# endif
+# ifndef YYMALLOC
+# define YYMALLOC malloc
+# if ! defined malloc && ! defined EXIT_SUCCESS
+void *malloc (YYSIZE_T); /* INFRINGES ON USER NAME SPACE */
+# endif
+# endif
+# ifndef YYFREE
+# define YYFREE free
+# if ! defined free && ! defined EXIT_SUCCESS
+void free (void *); /* INFRINGES ON USER NAME SPACE */
+# endif
+# endif
+# endif
+#endif /* ! defined yyoverflow || YYERROR_VERBOSE */
+
+
+#if (! defined yyoverflow \
+ && (! defined __cplusplus \
+ || (defined EVENT_FILTER_PARSER_STYPE_IS_TRIVIAL && EVENT_FILTER_PARSER_STYPE_IS_TRIVIAL)))
+
+/* A type that is properly aligned for any stack member. */
+union yyalloc
+{
+ yytype_int16 yyss_alloc;
+ YYSTYPE yyvs_alloc;
+};
+
+/* The size of the maximum gap between one aligned stack and the next. */
+# define YYSTACK_GAP_MAXIMUM (sizeof (union yyalloc) - 1)
+
+/* The size of an array large to enough to hold all stacks, each with
+ N elements. */
+# define YYSTACK_BYTES(N) \
+ ((N) * (sizeof (yytype_int16) + sizeof (YYSTYPE)) \
+ + YYSTACK_GAP_MAXIMUM)
+
+# define YYCOPY_NEEDED 1
+
+/* Relocate STACK from its old location to the new one. The
+ local variables YYSIZE and YYSTACKSIZE give the old and new number of
+ elements in the stack, and YYPTR gives the new location of the
+ stack. Advance YYPTR to a properly aligned location for the next
+ stack. */
+# define YYSTACK_RELOCATE(Stack_alloc, Stack) \
+ do \
+ { \
+ YYSIZE_T yynewbytes; \
+ YYCOPY (&yyptr->Stack_alloc, Stack, yysize); \
+ Stack = &yyptr->Stack_alloc; \
+ yynewbytes = yystacksize * sizeof (*Stack) + YYSTACK_GAP_MAXIMUM; \
+ yyptr += yynewbytes / sizeof (*yyptr); \
+ } \
+ while (0)
+
+#endif
+
+#if defined YYCOPY_NEEDED && YYCOPY_NEEDED
+/* Copy COUNT objects from SRC to DST. The source and destination do
+ not overlap. */
+# ifndef YYCOPY
+# if defined __GNUC__ && 1 < __GNUC__
+# define YYCOPY(Dst, Src, Count) \
+ __builtin_memcpy (Dst, Src, (Count) * sizeof (*(Src)))
+# else
+# define YYCOPY(Dst, Src, Count) \
+ do \
+ { \
+ YYSIZE_T yyi; \
+ for (yyi = 0; yyi < (Count); yyi++) \
+ (Dst)[yyi] = (Src)[yyi]; \
+ } \
+ while (0)
+# endif
+# endif
+#endif /* !YYCOPY_NEEDED */
+
+/* YYFINAL -- State number of the termination state. */
+#define YYFINAL 11
+/* YYLAST -- Last index in YYTABLE. */
+#define YYLAST 24
+
+/* YYNTOKENS -- Number of terminals. */
+#define YYNTOKENS 13
+/* YYNNTS -- Number of nonterminals. */
+#define YYNNTS 7
+/* YYNRULES -- Number of rules. */
+#define YYNRULES 21
+/* YYNSTATES -- Number of states. */
+#define YYNSTATES 29
+
+#define YYUNDEFTOK 2
+#define YYMAXUTOK 262
+
+/* YYTRANSLATE(TOKEN-NUM) -- Symbol number corresponding to TOKEN-NUM
+ as returned by yylex, with out-of-bounds checking. */
+#define YYTRANSLATE(YYX) \
+ ((unsigned) (YYX) <= YYMAXUTOK ? yytranslate[YYX] : YYUNDEFTOK)
+
+/* YYTRANSLATE[TOKEN-NUM] -- Symbol number corresponding to TOKEN-NUM
+ as returned by yylex. */
+static const yytype_uint8 yytranslate[] =
+{
+ 0, 2, 2, 2, 2, 2, 2, 2, 2, 2,
+ 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
+ 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
+ 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
+ 8, 9, 2, 2, 2, 2, 2, 2, 2, 2,
+ 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
+ 12, 10, 11, 2, 2, 2, 2, 2, 2, 2,
+ 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
+ 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
+ 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
+ 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
+ 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
+ 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
+ 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
+ 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
+ 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
+ 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
+ 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
+ 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
+ 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
+ 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
+ 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
+ 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
+ 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
+ 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
+ 2, 2, 2, 2, 2, 2, 1, 2, 3, 4,
+ 5, 6, 7
+};
+
+#if EVENT_FILTER_PARSER_DEBUG
+ /* YYRLINE[YYN] -- Source line where rule number YYN was defined. */
+static const yytype_uint8 yyrline[] =
+{
+ 0, 157, 157, 158, 161, 162, 163, 164, 165, 168,
+ 177, 178, 181, 182, 183, 184, 185, 188, 189, 190,
+ 191, 192
+};
+#endif
+
+#if EVENT_FILTER_PARSER_DEBUG || YYERROR_VERBOSE || 1
+/* YYTNAME[SYMBOL-NUM] -- String name of the symbol SYMBOL-NUM.
+ First, the terminals, then, starting at YYNTOKENS, nonterminals. */
+static const char *const yytname[] =
+{
+ "$end", "error", "$undefined", "TOKEN", "STRING", "AND", "OR", "NOT",
+ "'('", "')'", "'='", "'>'", "'<'", "$accept", "filter", "expr",
+ "key_value", "key", "value", "op", YY_NULLPTR
+};
+#endif
+
+# ifdef YYPRINT
+/* YYTOKNUM[NUM] -- (External) token number corresponding to the
+ (internal) symbol number NUM (which must be that of a token). */
+static const yytype_uint16 yytoknum[] =
+{
+ 0, 256, 257, 258, 259, 260, 261, 262, 40, 41,
+ 61, 62, 60
+};
+# endif
+
+#define YYPACT_NINF -6
+
+#define yypact_value_is_default(Yystate) \
+ (!!((Yystate) == (-6)))
+
+#define YYTABLE_NINF -1
+
+#define yytable_value_is_error(Yytable_value) \
+ 0
+
+ /* YYPACT[STATE-NUM] -- Index in YYTABLE of the portion describing
+ STATE-NUM. */
+static const yytype_int8 yypact[] =
+{
+ -1, -6, -6, -1, -1, 4, 13, -6, 12, -6,
+ 11, -6, -1, -1, -6, -5, -2, 8, -6, -6,
+ -6, -6, -6, -6, -6, -6, -6, -6, -6
+};
+
+ /* YYDEFACT[STATE-NUM] -- Default reduction number in state STATE-NUM.
+ Performed when YYTABLE does not specify something else to do. Zero
+ means the default is an error. */
+static const yytype_uint8 yydefact[] =
+{
+ 3, 10, 11, 0, 0, 0, 2, 8, 0, 6,
+ 0, 1, 0, 0, 17, 18, 19, 0, 7, 4,
+ 5, 20, 21, 12, 13, 14, 15, 16, 9
+};
+
+ /* YYPGOTO[NTERM-NUM]. */
+static const yytype_int8 yypgoto[] =
+{
+ -6, -6, -3, -6, -6, -6, -6
+};
+
+ /* YYDEFGOTO[NTERM-NUM]. */
+static const yytype_int8 yydefgoto[] =
+{
+ -1, 5, 6, 7, 8, 28, 17
+};
+
+ /* YYTABLE[YYPACT[STATE-NUM]] -- What to do in state STATE-NUM. If
+ positive, shift that token. If negative, reduce the rule whose
+ number is the opposite. If YYTABLE_NINF, syntax error. */
+static const yytype_uint8 yytable[] =
+{
+ 9, 10, 1, 2, 11, 21, 3, 4, 22, 19,
+ 20, 23, 24, 25, 26, 27, 12, 13, 12, 13,
+ 18, 0, 14, 15, 16
+};
+
+static const yytype_int8 yycheck[] =
+{
+ 3, 4, 3, 4, 0, 10, 7, 8, 10, 12,
+ 13, 3, 4, 5, 6, 7, 5, 6, 5, 6,
+ 9, -1, 10, 11, 12
+};
+
+ /* YYSTOS[STATE-NUM] -- The (internal number of the) accessing
+ symbol of state STATE-NUM. */
+static const yytype_uint8 yystos[] =
+{
+ 0, 3, 4, 7, 8, 14, 15, 16, 17, 15,
+ 15, 0, 5, 6, 10, 11, 12, 19, 9, 15,
+ 15, 10, 10, 3, 4, 5, 6, 7, 18
+};
+
+ /* YYR1[YYN] -- Symbol number of symbol that rule YYN derives. */
+static const yytype_uint8 yyr1[] =
+{
+ 0, 13, 14, 14, 15, 15, 15, 15, 15, 16,
+ 17, 17, 18, 18, 18, 18, 18, 19, 19, 19,
+ 19, 19
+};
+
+ /* YYR2[YYN] -- Number of symbols on the right hand side of rule YYN. */
+static const yytype_uint8 yyr2[] =
+{
+ 0, 2, 1, 0, 3, 3, 2, 3, 1, 3,
+ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+ 2, 2
+};
+
+
+#define yyerrok (yyerrstatus = 0)
+#define yyclearin (yychar = YYEMPTY)
+#define YYEMPTY (-2)
+#define YYEOF 0
+
+#define YYACCEPT goto yyacceptlab
+#define YYABORT goto yyabortlab
+#define YYERROR goto yyerrorlab
+
+
+#define YYRECOVERING() (!!yyerrstatus)
+
+#define YYBACKUP(Token, Value) \
+ do \
+ if (yychar == YYEMPTY) \
+ { \
+ yychar = (Token); \
+ yylval = (Value); \
+ YYPOPSTACK (yylen); \
+ yystate = *yyssp; \
+ goto yybackup; \
+ } \
+ else \
+ { \
+ yyerror (state, YY_("syntax error: cannot back up")); \
+ YYERROR; \
+ } \
+ while (0)
+
+/* Error token number */
+#define YYTERROR 1
+#define YYERRCODE 256
+
+
+
+/* Enable debugging if requested. */
+#if EVENT_FILTER_PARSER_DEBUG
+
+# ifndef YYFPRINTF
+# include <stdio.h> /* INFRINGES ON USER NAME SPACE */
+# define YYFPRINTF fprintf
+# endif
+
+# define YYDPRINTF(Args) \
+do { \
+ if (yydebug) \
+ YYFPRINTF Args; \
+} while (0)
+
+/* This macro is provided for backward compatibility. */
+#ifndef YY_LOCATION_PRINT
+# define YY_LOCATION_PRINT(File, Loc) ((void) 0)
+#endif
+
+
+# define YY_SYMBOL_PRINT(Title, Type, Value, Location) \
+do { \
+ if (yydebug) \
+ { \
+ YYFPRINTF (stderr, "%s ", Title); \
+ yy_symbol_print (stderr, \
+ Type, Value, state); \
+ YYFPRINTF (stderr, "\n"); \
+ } \
+} while (0)
+
+
+/*-----------------------------------.
+| Print this symbol's value on YYO. |
+`-----------------------------------*/
+
+static void
+yy_symbol_value_print (FILE *yyo, int yytype, YYSTYPE const * const yyvaluep, struct event_filter_parser_state *state)
+{
+ FILE *yyoutput = yyo;
+ YYUSE (yyoutput);
+ YYUSE (state);
+ if (!yyvaluep)
+ return;
+# ifdef YYPRINT
+ if (yytype < YYNTOKENS)
+ YYPRINT (yyo, yytoknum[yytype], *yyvaluep);
+# endif
+ YYUSE (yytype);
+}
+
+
+/*---------------------------.
+| Print this symbol on YYO. |
+`---------------------------*/
+
+static void
+yy_symbol_print (FILE *yyo, int yytype, YYSTYPE const * const yyvaluep, struct event_filter_parser_state *state)
+{
+ YYFPRINTF (yyo, "%s %s (",
+ yytype < YYNTOKENS ? "token" : "nterm", yytname[yytype]);
+
+ yy_symbol_value_print (yyo, yytype, yyvaluep, state);
+ YYFPRINTF (yyo, ")");
+}
+
+/*------------------------------------------------------------------.
+| yy_stack_print -- Print the state stack from its BOTTOM up to its |
+| TOP (included). |
+`------------------------------------------------------------------*/
+
+static void
+yy_stack_print (yytype_int16 *yybottom, yytype_int16 *yytop)
+{
+ YYFPRINTF (stderr, "Stack now");
+ for (; yybottom <= yytop; yybottom++)
+ {
+ int yybot = *yybottom;
+ YYFPRINTF (stderr, " %d", yybot);
+ }
+ YYFPRINTF (stderr, "\n");
+}
+
+# define YY_STACK_PRINT(Bottom, Top) \
+do { \
+ if (yydebug) \
+ yy_stack_print ((Bottom), (Top)); \
+} while (0)
+
+
+/*------------------------------------------------.
+| Report that the YYRULE is going to be reduced. |
+`------------------------------------------------*/
+
+static void
+yy_reduce_print (yytype_int16 *yyssp, YYSTYPE *yyvsp, int yyrule, struct event_filter_parser_state *state)
+{
+ unsigned long yylno = yyrline[yyrule];
+ int yynrhs = yyr2[yyrule];
+ int yyi;
+ YYFPRINTF (stderr, "Reducing stack by rule %d (line %lu):\n",
+ yyrule - 1, yylno);
+ /* The symbols being reduced. */
+ for (yyi = 0; yyi < yynrhs; yyi++)
+ {
+ YYFPRINTF (stderr, " $%d = ", yyi + 1);
+ yy_symbol_print (stderr,
+ yystos[yyssp[yyi + 1 - yynrhs]],
+ &yyvsp[(yyi + 1) - (yynrhs)]
+ , state);
+ YYFPRINTF (stderr, "\n");
+ }
+}
+
+# define YY_REDUCE_PRINT(Rule) \
+do { \
+ if (yydebug) \
+ yy_reduce_print (yyssp, yyvsp, Rule, state); \
+} while (0)
+
+/* Nonzero means print parse trace. It is left uninitialized so that
+ multiple parsers can coexist. */
+int yydebug;
+#else /* !EVENT_FILTER_PARSER_DEBUG */
+# define YYDPRINTF(Args)
+# define YY_SYMBOL_PRINT(Title, Type, Value, Location)
+# define YY_STACK_PRINT(Bottom, Top)
+# define YY_REDUCE_PRINT(Rule)
+#endif /* !EVENT_FILTER_PARSER_DEBUG */
+
+
+/* YYINITDEPTH -- initial size of the parser's stacks. */
+#ifndef YYINITDEPTH
+# define YYINITDEPTH 200
+#endif
+
+/* YYMAXDEPTH -- maximum size the stacks can grow to (effective only
+ if the built-in stack extension method is used).
+
+ Do not make this value too large; the results are undefined if
+ YYSTACK_ALLOC_MAXIMUM < YYSTACK_BYTES (YYMAXDEPTH)
+ evaluated with infinite-precision integer arithmetic. */
+
+#ifndef YYMAXDEPTH
+# define YYMAXDEPTH 10000
+#endif
+
+
+#if YYERROR_VERBOSE
+
+# ifndef yystrlen
+# if defined __GLIBC__ && defined _STRING_H
+# define yystrlen strlen
+# else
+/* Return the length of YYSTR. */
+static YYSIZE_T
+yystrlen (const char *yystr)
+{
+ YYSIZE_T yylen;
+ for (yylen = 0; yystr[yylen]; yylen++)
+ continue;
+ return yylen;
+}
+# endif
+# endif
+
+# ifndef yystpcpy
+# if defined __GLIBC__ && defined _STRING_H && defined _GNU_SOURCE
+# define yystpcpy stpcpy
+# else
+/* Copy YYSRC to YYDEST, returning the address of the terminating '\0' in
+ YYDEST. */
+static char *
+yystpcpy (char *yydest, const char *yysrc)
+{
+ char *yyd = yydest;
+ const char *yys = yysrc;
+
+ while ((*yyd++ = *yys++) != '\0')
+ continue;
+
+ return yyd - 1;
+}
+# endif
+# endif
+
+# ifndef yytnamerr
+/* Copy to YYRES the contents of YYSTR after stripping away unnecessary
+ quotes and backslashes, so that it's suitable for yyerror. The
+ heuristic is that double-quoting is unnecessary unless the string
+ contains an apostrophe, a comma, or backslash (other than
+ backslash-backslash). YYSTR is taken from yytname. If YYRES is
+ null, do not copy; instead, return the length of what the result
+ would have been. */
+static YYSIZE_T
+yytnamerr (char *yyres, const char *yystr)
+{
+ if (*yystr == '"')
+ {
+ YYSIZE_T yyn = 0;
+ char const *yyp = yystr;
+
+ for (;;)
+ switch (*++yyp)
+ {
+ case '\'':
+ case ',':
+ goto do_not_strip_quotes;
+
+ case '\\':
+ if (*++yyp != '\\')
+ goto do_not_strip_quotes;
+ else
+ goto append;
+
+ append:
+ default:
+ if (yyres)
+ yyres[yyn] = *yyp;
+ yyn++;
+ break;
+
+ case '"':
+ if (yyres)
+ yyres[yyn] = '\0';
+ return yyn;
+ }
+ do_not_strip_quotes: ;
+ }
+
+ if (! yyres)
+ return yystrlen (yystr);
+
+ return (YYSIZE_T) (yystpcpy (yyres, yystr) - yyres);
+}
+# endif
+
+/* Copy into *YYMSG, which is of size *YYMSG_ALLOC, an error message
+ about the unexpected token YYTOKEN for the state stack whose top is
+ YYSSP.
+
+ Return 0 if *YYMSG was successfully written. Return 1 if *YYMSG is
+ not large enough to hold the message. In that case, also set
+ *YYMSG_ALLOC to the required number of bytes. Return 2 if the
+ required number of bytes is too large to store. */
+static int
+yysyntax_error (YYSIZE_T *yymsg_alloc, char **yymsg,
+ yytype_int16 *yyssp, int yytoken)
+{
+ YYSIZE_T yysize0 = yytnamerr (YY_NULLPTR, yytname[yytoken]);
+ YYSIZE_T yysize = yysize0;
+ enum { YYERROR_VERBOSE_ARGS_MAXIMUM = 5 };
+ /* Internationalized format string. */
+ const char *yyformat = YY_NULLPTR;
+ /* Arguments of yyformat. */
+ char const *yyarg[YYERROR_VERBOSE_ARGS_MAXIMUM];
+ /* Number of reported tokens (one for the "unexpected", one per
+ "expected"). */
+ int yycount = 0;
+
+ /* There are many possibilities here to consider:
+ - If this state is a consistent state with a default action, then
+ the only way this function was invoked is if the default action
+ is an error action. In that case, don't check for expected
+ tokens because there are none.
+ - The only way there can be no lookahead present (in yychar) is if
+ this state is a consistent state with a default action. Thus,
+ detecting the absence of a lookahead is sufficient to determine
+ that there is no unexpected or expected token to report. In that
+ case, just report a simple "syntax error".
+ - Don't assume there isn't a lookahead just because this state is a
+ consistent state with a default action. There might have been a
+ previous inconsistent state, consistent state with a non-default
+ action, or user semantic action that manipulated yychar.
+ - Of course, the expected token list depends on states to have
+ correct lookahead information, and it depends on the parser not
+ to perform extra reductions after fetching a lookahead from the
+ scanner and before detecting a syntax error. Thus, state merging
+ (from LALR or IELR) and default reductions corrupt the expected
+ token list. However, the list is correct for canonical LR with
+ one exception: it will still contain any token that will not be
+ accepted due to an error action in a later state.
+ */
+ if (yytoken != YYEMPTY)
+ {
+ int yyn = yypact[*yyssp];
+ yyarg[yycount++] = yytname[yytoken];
+ if (!yypact_value_is_default (yyn))
+ {
+ /* Start YYX at -YYN if negative to avoid negative indexes in
+ YYCHECK. In other words, skip the first -YYN actions for
+ this state because they are default actions. */
+ int yyxbegin = yyn < 0 ? -yyn : 0;
+ /* Stay within bounds of both yycheck and yytname. */
+ int yychecklim = YYLAST - yyn + 1;
+ int yyxend = yychecklim < YYNTOKENS ? yychecklim : YYNTOKENS;
+ int yyx;
+
+ for (yyx = yyxbegin; yyx < yyxend; ++yyx)
+ if (yycheck[yyx + yyn] == yyx && yyx != YYTERROR
+ && !yytable_value_is_error (yytable[yyx + yyn]))
+ {
+ if (yycount == YYERROR_VERBOSE_ARGS_MAXIMUM)
+ {
+ yycount = 1;
+ yysize = yysize0;
+ break;
+ }
+ yyarg[yycount++] = yytname[yyx];
+ {
+ YYSIZE_T yysize1 = yysize + yytnamerr (YY_NULLPTR, yytname[yyx]);
+ if (yysize <= yysize1 && yysize1 <= YYSTACK_ALLOC_MAXIMUM)
+ yysize = yysize1;
+ else
+ return 2;
+ }
+ }
+ }
+ }
+
+ switch (yycount)
+ {
+# define YYCASE_(N, S) \
+ case N: \
+ yyformat = S; \
+ break
+ default: /* Avoid compiler warnings. */
+ YYCASE_(0, YY_("syntax error"));
+ YYCASE_(1, YY_("syntax error, unexpected %s"));
+ YYCASE_(2, YY_("syntax error, unexpected %s, expecting %s"));
+ YYCASE_(3, YY_("syntax error, unexpected %s, expecting %s or %s"));
+ YYCASE_(4, YY_("syntax error, unexpected %s, expecting %s or %s or %s"));
+ YYCASE_(5, YY_("syntax error, unexpected %s, expecting %s or %s or %s or %s"));
+# undef YYCASE_
+ }
+
+ {
+ YYSIZE_T yysize1 = yysize + yystrlen (yyformat);
+ if (yysize <= yysize1 && yysize1 <= YYSTACK_ALLOC_MAXIMUM)
+ yysize = yysize1;
+ else
+ return 2;
+ }
+
+ if (*yymsg_alloc < yysize)
+ {
+ *yymsg_alloc = 2 * yysize;
+ if (! (yysize <= *yymsg_alloc
+ && *yymsg_alloc <= YYSTACK_ALLOC_MAXIMUM))
+ *yymsg_alloc = YYSTACK_ALLOC_MAXIMUM;
+ return 1;
+ }
+
+ /* Avoid sprintf, as that infringes on the user's name space.
+ Don't have undefined behavior even if the translation
+ produced a string with the wrong number of "%s"s. */
+ {
+ char *yyp = *yymsg;
+ int yyi = 0;
+ while ((*yyp = *yyformat) != '\0')
+ if (*yyp == '%' && yyformat[1] == 's' && yyi < yycount)
+ {
+ yyp += yytnamerr (yyp, yyarg[yyi++]);
+ yyformat += 2;
+ }
+ else
+ {
+ yyp++;
+ yyformat++;
+ }
+ }
+ return 0;
+}
+#endif /* YYERROR_VERBOSE */
+
+/*-----------------------------------------------.
+| Release the memory associated to this symbol. |
+`-----------------------------------------------*/
+
+static void
+yydestruct (const char *yymsg, int yytype, YYSTYPE *yyvaluep, struct event_filter_parser_state *state)
+{
+ YYUSE (yyvaluep);
+ YYUSE (state);
+ if (!yymsg)
+ yymsg = "Deleting";
+ YY_SYMBOL_PRINT (yymsg, yytype, yyvaluep, yylocationp);
+
+ YY_IGNORE_MAYBE_UNINITIALIZED_BEGIN
+ YYUSE (yytype);
+ YY_IGNORE_MAYBE_UNINITIALIZED_END
+}
+
+
+
+
+/*----------.
+| yyparse. |
+`----------*/
+
+int
+yyparse (struct event_filter_parser_state *state)
+{
+/* The lookahead symbol. */
+int yychar;
+
+
+/* The semantic value of the lookahead symbol. */
+/* Default value used for initialization, for pacifying older GCCs
+ or non-GCC compilers. */
+YY_INITIAL_VALUE (static YYSTYPE yyval_default;)
+YYSTYPE yylval YY_INITIAL_VALUE (= yyval_default);
+
+ /* Number of syntax errors so far. */
+ int yynerrs;
+
+ int yystate;
+ /* Number of tokens to shift before error messages enabled. */
+ int yyerrstatus;
+
+ /* The stacks and their tools:
+ 'yyss': related to states.
+ 'yyvs': related to semantic values.
+
+ Refer to the stacks through separate pointers, to allow yyoverflow
+ to reallocate them elsewhere. */
+
+ /* The state stack. */
+ yytype_int16 yyssa[YYINITDEPTH];
+ yytype_int16 *yyss;
+ yytype_int16 *yyssp;
+
+ /* The semantic value stack. */
+ YYSTYPE yyvsa[YYINITDEPTH];
+ YYSTYPE *yyvs;
+ YYSTYPE *yyvsp;
+
+ YYSIZE_T yystacksize;
+
+ int yyn;
+ int yyresult;
+ /* Lookahead token as an internal (translated) token number. */
+ int yytoken = 0;
+ /* The variables used to return semantic value and location from the
+ action routines. */
+ YYSTYPE yyval;
+
+#if YYERROR_VERBOSE
+ /* Buffer for error messages, and its allocated size. */
+ char yymsgbuf[128];
+ char *yymsg = yymsgbuf;
+ YYSIZE_T yymsg_alloc = sizeof yymsgbuf;
+#endif
+
+#define YYPOPSTACK(N) (yyvsp -= (N), yyssp -= (N))
+
+ /* The number of symbols on the RHS of the reduced rule.
+ Keep to zero when no symbol should be popped. */
+ int yylen = 0;
+
+ yyssp = yyss = yyssa;
+ yyvsp = yyvs = yyvsa;
+ yystacksize = YYINITDEPTH;
+
+ YYDPRINTF ((stderr, "Starting parse\n"));
+
+ yystate = 0;
+ yyerrstatus = 0;
+ yynerrs = 0;
+ yychar = YYEMPTY; /* Cause a token to be read. */
+ goto yysetstate;
+
+
+/*------------------------------------------------------------.
+| yynewstate -- push a new state, which is found in yystate. |
+`------------------------------------------------------------*/
+yynewstate:
+ /* In all cases, when you get here, the value and location stacks
+ have just been pushed. So pushing a state here evens the stacks. */
+ yyssp++;
+
+
+/*--------------------------------------------------------------------.
+| yynewstate -- set current state (the top of the stack) to yystate. |
+`--------------------------------------------------------------------*/
+yysetstate:
+ *yyssp = (yytype_int16) yystate;
+
+ if (yyss + yystacksize - 1 <= yyssp)
+#if !defined yyoverflow && !defined YYSTACK_RELOCATE
+ goto yyexhaustedlab;
+#else
+ {
+ /* Get the current used size of the three stacks, in elements. */
+ YYSIZE_T yysize = (YYSIZE_T) (yyssp - yyss + 1);
+
+# if defined yyoverflow
+ {
+ /* Give user a chance to reallocate the stack. Use copies of
+ these so that the &'s don't force the real ones into
+ memory. */
+ YYSTYPE *yyvs1 = yyvs;
+ yytype_int16 *yyss1 = yyss;
+
+ /* Each stack pointer address is followed by the size of the
+ data in use in that stack, in bytes. This used to be a
+ conditional around just the two extra args, but that might
+ be undefined if yyoverflow is a macro. */
+ yyoverflow (YY_("memory exhausted"),
+ &yyss1, yysize * sizeof (*yyssp),
+ &yyvs1, yysize * sizeof (*yyvsp),
+ &yystacksize);
+ yyss = yyss1;
+ yyvs = yyvs1;
+ }
+# else /* defined YYSTACK_RELOCATE */
+ /* Extend the stack our own way. */
+ if (YYMAXDEPTH <= yystacksize)
+ goto yyexhaustedlab;
+ yystacksize *= 2;
+ if (YYMAXDEPTH < yystacksize)
+ yystacksize = YYMAXDEPTH;
+
+ {
+ yytype_int16 *yyss1 = yyss;
+ union yyalloc *yyptr =
+ (union yyalloc *) YYSTACK_ALLOC (YYSTACK_BYTES (yystacksize));
+ if (! yyptr)
+ goto yyexhaustedlab;
+ YYSTACK_RELOCATE (yyss_alloc, yyss);
+ YYSTACK_RELOCATE (yyvs_alloc, yyvs);
+# undef YYSTACK_RELOCATE
+ if (yyss1 != yyssa)
+ YYSTACK_FREE (yyss1);
+ }
+# endif
+
+ yyssp = yyss + yysize - 1;
+ yyvsp = yyvs + yysize - 1;
+
+ YYDPRINTF ((stderr, "Stack size increased to %lu\n",
+ (unsigned long) yystacksize));
+
+ if (yyss + yystacksize - 1 <= yyssp)
+ YYABORT;
+ }
+#endif /* !defined yyoverflow && !defined YYSTACK_RELOCATE */
+
+ YYDPRINTF ((stderr, "Entering state %d\n", yystate));
+
+ if (yystate == YYFINAL)
+ YYACCEPT;
+
+ goto yybackup;
+
+
+/*-----------.
+| yybackup. |
+`-----------*/
+yybackup:
+ /* Do appropriate processing given the current state. Read a
+ lookahead token if we need one and don't already have one. */
+
+ /* First try to decide what to do without reference to lookahead token. */
+ yyn = yypact[yystate];
+ if (yypact_value_is_default (yyn))
+ goto yydefault;
+
+ /* Not known => get a lookahead token if don't already have one. */
+
+ /* YYCHAR is either YYEMPTY or YYEOF or a valid lookahead symbol. */
+ if (yychar == YYEMPTY)
+ {
+ YYDPRINTF ((stderr, "Reading a token: "));
+ yychar = yylex (&yylval, scanner);
+ }
+
+ if (yychar <= YYEOF)
+ {
+ yychar = yytoken = YYEOF;
+ YYDPRINTF ((stderr, "Now at end of input.\n"));
+ }
+ else
+ {
+ yytoken = YYTRANSLATE (yychar);
+ YY_SYMBOL_PRINT ("Next token is", yytoken, &yylval, &yylloc);
+ }
+
+ /* If the proper action on seeing token YYTOKEN is to reduce or to
+ detect an error, take that action. */
+ yyn += yytoken;
+ if (yyn < 0 || YYLAST < yyn || yycheck[yyn] != yytoken)
+ goto yydefault;
+ yyn = yytable[yyn];
+ if (yyn <= 0)
+ {
+ if (yytable_value_is_error (yyn))
+ goto yyerrlab;
+ yyn = -yyn;
+ goto yyreduce;
+ }
+
+ /* Count tokens shifted since error; after three, turn off error
+ status. */
+ if (yyerrstatus)
+ yyerrstatus--;
+
+ /* Shift the lookahead token. */
+ YY_SYMBOL_PRINT ("Shifting", yytoken, &yylval, &yylloc);
+
+ /* Discard the shifted token. */
+ yychar = YYEMPTY;
+
+ yystate = yyn;
+ YY_IGNORE_MAYBE_UNINITIALIZED_BEGIN
+ *++yyvsp = yylval;
+ YY_IGNORE_MAYBE_UNINITIALIZED_END
+
+ goto yynewstate;
+
+
+/*-----------------------------------------------------------.
+| yydefault -- do the default action for the current state. |
+`-----------------------------------------------------------*/
+yydefault:
+ yyn = yydefact[yystate];
+ if (yyn == 0)
+ goto yyerrlab;
+ goto yyreduce;
+
+
+/*-----------------------------.
+| yyreduce -- do a reduction. |
+`-----------------------------*/
+yyreduce:
+ /* yyn is the number of a rule to reduce with. */
+ yylen = yyr2[yyn];
+
+ /* If YYLEN is nonzero, implement the default value of the action:
+ '$$ = $1'.
+
+ Otherwise, the following line sets YYVAL to garbage.
+ This behavior is undocumented and Bison
+ users should not rely upon it. Assigning to YYVAL
+ unconditionally makes the parser a bit smaller, and it avoids a
+ GCC warning that YYVAL may be used uninitialized. */
+ yyval = yyvsp[1-yylen];
+
+
+ YY_REDUCE_PRINT (yyn);
+ switch (yyn)
+ {
+ case 2:
+#line 157 "event-filter-parser.y" /* yacc.c:1652 */
+ { state->output = (yyvsp[0].node); }
+#line 1375 "event-filter-parser.c" /* yacc.c:1652 */
+ break;
+
+ case 3:
+#line 158 "event-filter-parser.y" /* yacc.c:1652 */
+ { state->output = NULL; }
+#line 1381 "event-filter-parser.c" /* yacc.c:1652 */
+ break;
+
+ case 4:
+#line 161 "event-filter-parser.y" /* yacc.c:1652 */
+ { (yyval.node) = logic(state, (yyvsp[-2].node), (yyvsp[0].node), EVENT_FILTER_OP_AND); }
+#line 1387 "event-filter-parser.c" /* yacc.c:1652 */
+ break;
+
+ case 5:
+#line 162 "event-filter-parser.y" /* yacc.c:1652 */
+ { (yyval.node) = logic(state, (yyvsp[-2].node), (yyvsp[0].node), EVENT_FILTER_OP_OR); }
+#line 1393 "event-filter-parser.c" /* yacc.c:1652 */
+ break;
+
+ case 6:
+#line 163 "event-filter-parser.y" /* yacc.c:1652 */
+ { (yyval.node) = logic(state, (yyvsp[0].node), NULL, EVENT_FILTER_OP_NOT); }
+#line 1399 "event-filter-parser.c" /* yacc.c:1652 */
+ break;
+
+ case 7:
+#line 164 "event-filter-parser.y" /* yacc.c:1652 */
+ { (yyval.node) = (yyvsp[-1].node); }
+#line 1405 "event-filter-parser.c" /* yacc.c:1652 */
+ break;
+
+ case 8:
+#line 165 "event-filter-parser.y" /* yacc.c:1652 */
+ { (yyval.node) = (yyvsp[0].node); }
+#line 1411 "event-filter-parser.c" /* yacc.c:1652 */
+ break;
+
+ case 9:
+#line 168 "event-filter-parser.y" /* yacc.c:1652 */
+ {
+ (yyval.node) = key_value(state, (yyvsp[-2].str), (yyvsp[0].str), (yyvsp[-1].op));
+ if ((yyval.node) == NULL) {
+ yyerror(state, state->error);
+ YYERROR;
+ }
+ }
+#line 1423 "event-filter-parser.c" /* yacc.c:1652 */
+ break;
+
+ case 10:
+#line 177 "event-filter-parser.y" /* yacc.c:1652 */
+ { (yyval.str) = (yyvsp[0].str); }
+#line 1429 "event-filter-parser.c" /* yacc.c:1652 */
+ break;
+
+ case 11:
+#line 178 "event-filter-parser.y" /* yacc.c:1652 */
+ { (yyval.str) = (yyvsp[0].str); }
+#line 1435 "event-filter-parser.c" /* yacc.c:1652 */
+ break;
+
+ case 12:
+#line 181 "event-filter-parser.y" /* yacc.c:1652 */
+ { (yyval.str) = (yyvsp[0].str); }
+#line 1441 "event-filter-parser.c" /* yacc.c:1652 */
+ break;
+
+ case 13:
+#line 182 "event-filter-parser.y" /* yacc.c:1652 */
+ { (yyval.str) = (yyvsp[0].str); }
+#line 1447 "event-filter-parser.c" /* yacc.c:1652 */
+ break;
+
+ case 14:
+#line 183 "event-filter-parser.y" /* yacc.c:1652 */
+ { (yyval.str) = "and"; }
+#line 1453 "event-filter-parser.c" /* yacc.c:1652 */
+ break;
+
+ case 15:
+#line 184 "event-filter-parser.y" /* yacc.c:1652 */
+ { (yyval.str) = "or"; }
+#line 1459 "event-filter-parser.c" /* yacc.c:1652 */
+ break;
+
+ case 16:
+#line 185 "event-filter-parser.y" /* yacc.c:1652 */
+ { (yyval.str) = "not"; }
+#line 1465 "event-filter-parser.c" /* yacc.c:1652 */
+ break;
+
+ case 17:
+#line 188 "event-filter-parser.y" /* yacc.c:1652 */
+ { (yyval.op) = EVENT_FILTER_OP_CMP_EQ; }
+#line 1471 "event-filter-parser.c" /* yacc.c:1652 */
+ break;
+
+ case 18:
+#line 189 "event-filter-parser.y" /* yacc.c:1652 */
+ { (yyval.op) = EVENT_FILTER_OP_CMP_GT; }
+#line 1477 "event-filter-parser.c" /* yacc.c:1652 */
+ break;
+
+ case 19:
+#line 190 "event-filter-parser.y" /* yacc.c:1652 */
+ { (yyval.op) = EVENT_FILTER_OP_CMP_LT; }
+#line 1483 "event-filter-parser.c" /* yacc.c:1652 */
+ break;
+
+ case 20:
+#line 191 "event-filter-parser.y" /* yacc.c:1652 */
+ { (yyval.op) = EVENT_FILTER_OP_CMP_GE; }
+#line 1489 "event-filter-parser.c" /* yacc.c:1652 */
+ break;
+
+ case 21:
+#line 192 "event-filter-parser.y" /* yacc.c:1652 */
+ { (yyval.op) = EVENT_FILTER_OP_CMP_LE; }
+#line 1495 "event-filter-parser.c" /* yacc.c:1652 */
+ break;
+
+
+#line 1499 "event-filter-parser.c" /* yacc.c:1652 */
+ default: break;
+ }
+ /* User semantic actions sometimes alter yychar, and that requires
+ that yytoken be updated with the new translation. We take the
+ approach of translating immediately before every use of yytoken.
+ One alternative is translating here after every semantic action,
+ but that translation would be missed if the semantic action invokes
+ YYABORT, YYACCEPT, or YYERROR immediately after altering yychar or
+ if it invokes YYBACKUP. In the case of YYABORT or YYACCEPT, an
+ incorrect destructor might then be invoked immediately. In the
+ case of YYERROR or YYBACKUP, subsequent parser actions might lead
+ to an incorrect destructor call or verbose syntax error message
+ before the lookahead is translated. */
+ YY_SYMBOL_PRINT ("-> $$ =", yyr1[yyn], &yyval, &yyloc);
+
+ YYPOPSTACK (yylen);
+ yylen = 0;
+ YY_STACK_PRINT (yyss, yyssp);
+
+ *++yyvsp = yyval;
+
+ /* Now 'shift' the result of the reduction. Determine what state
+ that goes to, based on the state we popped back to and the rule
+ number reduced by. */
+ {
+ const int yylhs = yyr1[yyn] - YYNTOKENS;
+ const int yyi = yypgoto[yylhs] + *yyssp;
+ yystate = (0 <= yyi && yyi <= YYLAST && yycheck[yyi] == *yyssp
+ ? yytable[yyi]
+ : yydefgoto[yylhs]);
+ }
+
+ goto yynewstate;
+
+
+/*--------------------------------------.
+| yyerrlab -- here on detecting error. |
+`--------------------------------------*/
+yyerrlab:
+ /* Make sure we have latest lookahead translation. See comments at
+ user semantic actions for why this is necessary. */
+ yytoken = yychar == YYEMPTY ? YYEMPTY : YYTRANSLATE (yychar);
+
+ /* If not already recovering from an error, report this error. */
+ if (!yyerrstatus)
+ {
+ ++yynerrs;
+#if ! YYERROR_VERBOSE
+ yyerror (state, YY_("syntax error"));
+#else
+# define YYSYNTAX_ERROR yysyntax_error (&yymsg_alloc, &yymsg, \
+ yyssp, yytoken)
+ {
+ char const *yymsgp = YY_("syntax error");
+ int yysyntax_error_status;
+ yysyntax_error_status = YYSYNTAX_ERROR;
+ if (yysyntax_error_status == 0)
+ yymsgp = yymsg;
+ else if (yysyntax_error_status == 1)
+ {
+ if (yymsg != yymsgbuf)
+ YYSTACK_FREE (yymsg);
+ yymsg = (char *) YYSTACK_ALLOC (yymsg_alloc);
+ if (!yymsg)
+ {
+ yymsg = yymsgbuf;
+ yymsg_alloc = sizeof yymsgbuf;
+ yysyntax_error_status = 2;
+ }
+ else
+ {
+ yysyntax_error_status = YYSYNTAX_ERROR;
+ yymsgp = yymsg;
+ }
+ }
+ yyerror (state, yymsgp);
+ if (yysyntax_error_status == 2)
+ goto yyexhaustedlab;
+ }
+# undef YYSYNTAX_ERROR
+#endif
+ }
+
+
+
+ if (yyerrstatus == 3)
+ {
+ /* If just tried and failed to reuse lookahead token after an
+ error, discard it. */
+
+ if (yychar <= YYEOF)
+ {
+ /* Return failure if at end of input. */
+ if (yychar == YYEOF)
+ YYABORT;
+ }
+ else
+ {
+ yydestruct ("Error: discarding",
+ yytoken, &yylval, state);
+ yychar = YYEMPTY;
+ }
+ }
+
+ /* Else will try to reuse lookahead token after shifting the error
+ token. */
+ goto yyerrlab1;
+
+
+/*---------------------------------------------------.
+| yyerrorlab -- error raised explicitly by YYERROR. |
+`---------------------------------------------------*/
+yyerrorlab:
+ /* Pacify compilers when the user code never invokes YYERROR and the
+ label yyerrorlab therefore never appears in user code. */
+ if (0)
+ YYERROR;
+
+ /* Do not reclaim the symbols of the rule whose action triggered
+ this YYERROR. */
+ YYPOPSTACK (yylen);
+ yylen = 0;
+ YY_STACK_PRINT (yyss, yyssp);
+ yystate = *yyssp;
+ goto yyerrlab1;
+
+
+/*-------------------------------------------------------------.
+| yyerrlab1 -- common code for both syntax error and YYERROR. |
+`-------------------------------------------------------------*/
+yyerrlab1:
+ yyerrstatus = 3; /* Each real token shifted decrements this. */
+
+ for (;;)
+ {
+ yyn = yypact[yystate];
+ if (!yypact_value_is_default (yyn))
+ {
+ yyn += YYTERROR;
+ if (0 <= yyn && yyn <= YYLAST && yycheck[yyn] == YYTERROR)
+ {
+ yyn = yytable[yyn];
+ if (0 < yyn)
+ break;
+ }
+ }
+
+ /* Pop the current state because it cannot handle the error token. */
+ if (yyssp == yyss)
+ YYABORT;
+
+
+ yydestruct ("Error: popping",
+ yystos[yystate], yyvsp, state);
+ YYPOPSTACK (1);
+ yystate = *yyssp;
+ YY_STACK_PRINT (yyss, yyssp);
+ }
+
+ YY_IGNORE_MAYBE_UNINITIALIZED_BEGIN
+ *++yyvsp = yylval;
+ YY_IGNORE_MAYBE_UNINITIALIZED_END
+
+
+ /* Shift the error token. */
+ YY_SYMBOL_PRINT ("Shifting", yystos[yyn], yyvsp, yylsp);
+
+ yystate = yyn;
+ goto yynewstate;
+
+
+/*-------------------------------------.
+| yyacceptlab -- YYACCEPT comes here. |
+`-------------------------------------*/
+yyacceptlab:
+ yyresult = 0;
+ goto yyreturn;
+
+
+/*-----------------------------------.
+| yyabortlab -- YYABORT comes here. |
+`-----------------------------------*/
+yyabortlab:
+ yyresult = 1;
+ goto yyreturn;
+
+
+#if !defined yyoverflow || YYERROR_VERBOSE
+/*-------------------------------------------------.
+| yyexhaustedlab -- memory exhaustion comes here. |
+`-------------------------------------------------*/
+yyexhaustedlab:
+ yyerror (state, YY_("memory exhausted"));
+ yyresult = 2;
+ /* Fall through. */
+#endif
+
+
+/*-----------------------------------------------------.
+| yyreturn -- parsing is finished, return the result. |
+`-----------------------------------------------------*/
+yyreturn:
+ if (yychar != YYEMPTY)
+ {
+ /* Make sure we have latest lookahead translation. See comments at
+ user semantic actions for why this is necessary. */
+ yytoken = YYTRANSLATE (yychar);
+ yydestruct ("Cleanup: discarding lookahead",
+ yytoken, &yylval, state);
+ }
+ /* Do not reclaim the symbols of the rule whose action triggered
+ this YYABORT or YYACCEPT. */
+ YYPOPSTACK (yylen);
+ YY_STACK_PRINT (yyss, yyssp);
+ while (yyssp != yyss)
+ {
+ yydestruct ("Cleanup: popping",
+ yystos[*yyssp], yyvsp, state);
+ YYPOPSTACK (1);
+ }
+#ifndef yyoverflow
+ if (yyss != yyssa)
+ YYSTACK_FREE (yyss);
+#endif
+#if YYERROR_VERBOSE
+ if (yymsg != yymsgbuf)
+ YYSTACK_FREE (yymsg);
+#endif
+ return yyresult;
+}
+#line 194 "event-filter-parser.y" /* yacc.c:1918 */
+
diff --git a/src/lib/event-filter-parser.h b/src/lib/event-filter-parser.h
new file mode 100644
index 0000000..6636054
--- /dev/null
+++ b/src/lib/event-filter-parser.h
@@ -0,0 +1,91 @@
+/* A Bison parser, made by GNU Bison 3.3.2. */
+
+/* Bison interface for Yacc-like parsers in C
+
+ Copyright (C) 1984, 1989-1990, 2000-2015, 2018-2019 Free Software Foundation,
+ Inc.
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>. */
+
+/* As a special exception, you may create a larger work that contains
+ part or all of the Bison parser skeleton and distribute that work
+ under terms of your choice, so long as that work isn't itself a
+ parser generator using the skeleton or a modified version thereof
+ as a parser skeleton. Alternatively, if you modify or redistribute
+ the parser skeleton itself, you may (at your option) remove this
+ special exception, which will cause the skeleton and the resulting
+ Bison output files to be licensed under the GNU General Public
+ License without this special exception.
+
+ This special exception was added by the Free Software Foundation in
+ version 2.2 of Bison. */
+
+/* Undocumented macros, especially those whose name start with YY_,
+ are private implementation details. Do not rely on them. */
+
+#ifndef YY_EVENT_FILTER_PARSER_EVENT_FILTER_PARSER_H_INCLUDED
+# define YY_EVENT_FILTER_PARSER_EVENT_FILTER_PARSER_H_INCLUDED
+/* Debug traces. */
+#ifndef EVENT_FILTER_PARSER_DEBUG
+# if defined YYDEBUG
+#if YYDEBUG
+# define EVENT_FILTER_PARSER_DEBUG 1
+# else
+# define EVENT_FILTER_PARSER_DEBUG 0
+# endif
+# else /* ! defined YYDEBUG */
+# define EVENT_FILTER_PARSER_DEBUG 0
+# endif /* ! defined YYDEBUG */
+#endif /* ! defined EVENT_FILTER_PARSER_DEBUG */
+#if EVENT_FILTER_PARSER_DEBUG
+extern int event_filter_parser_debug;
+#endif
+
+/* Token type. */
+#ifndef EVENT_FILTER_PARSER_TOKENTYPE
+# define EVENT_FILTER_PARSER_TOKENTYPE
+ enum event_filter_parser_tokentype
+ {
+ TOKEN = 258,
+ STRING = 259,
+ AND = 260,
+ OR = 261,
+ NOT = 262
+ };
+#endif
+
+/* Value type. */
+#if ! defined EVENT_FILTER_PARSER_STYPE && ! defined EVENT_FILTER_PARSER_STYPE_IS_DECLARED
+
+union EVENT_FILTER_PARSER_STYPE
+{
+#line 140 "event-filter-parser.y" /* yacc.c:1921 */
+
+ const char *str;
+ enum event_filter_node_op op;
+ struct event_filter_node *node;
+
+#line 80 "event-filter-parser.h" /* yacc.c:1921 */
+};
+
+typedef union EVENT_FILTER_PARSER_STYPE EVENT_FILTER_PARSER_STYPE;
+# define EVENT_FILTER_PARSER_STYPE_IS_TRIVIAL 1
+# define EVENT_FILTER_PARSER_STYPE_IS_DECLARED 1
+#endif
+
+
+
+int event_filter_parser_parse (struct event_filter_parser_state *state);
+
+#endif /* !YY_EVENT_FILTER_PARSER_EVENT_FILTER_PARSER_H_INCLUDED */
diff --git a/src/lib/event-filter-parser.y b/src/lib/event-filter-parser.y
new file mode 100644
index 0000000..652b896
--- /dev/null
+++ b/src/lib/event-filter-parser.y
@@ -0,0 +1,194 @@
+/* Copyright (c) 2020 Dovecot authors, see the included COPYING file */
+
+%define api.pure
+%define api.prefix {event_filter_parser_}
+%define parse.error verbose
+%lex-param {void *scanner}
+%parse-param {struct event_filter_parser_state *state}
+
+%defines
+
+%{
+#include "lib.h"
+#include "wildcard-match.h"
+#include "lib-event-private.h"
+#include "event-filter-private.h"
+
+#define scanner state->scanner
+
+#define YYERROR_VERBOSE
+
+extern int event_filter_parser_lex(void *, void *);
+
+void event_filter_parser_error(void *scan, const char *e)
+{
+ struct event_filter_parser_state *state = scan;
+
+ state->error = t_strdup_printf("event filter: %s", e);
+}
+
+static struct event_filter_node *key_value(struct event_filter_parser_state *state,
+ const char *a, const char *b,
+ enum event_filter_node_op op)
+{
+ struct event_filter_node *node;
+ enum event_filter_node_type type;
+
+ if (strcmp(a, "event") == 0)
+ type = EVENT_FILTER_NODE_TYPE_EVENT_NAME_WILDCARD;
+ else if (strcmp(a, "category") == 0)
+ type = EVENT_FILTER_NODE_TYPE_EVENT_CATEGORY;
+ else if (strcmp(a, "source_location") == 0)
+ type = EVENT_FILTER_NODE_TYPE_EVENT_SOURCE_LOCATION;
+ else
+ type = EVENT_FILTER_NODE_TYPE_EVENT_FIELD_WILDCARD;
+
+ /* only fields support comparators other than EQ */
+ if ((type != EVENT_FILTER_NODE_TYPE_EVENT_FIELD_WILDCARD) &&
+ (op != EVENT_FILTER_OP_CMP_EQ)) {
+ state->error = "Only fields support inequality comparisons";
+ return NULL;
+ }
+
+ node = p_new(state->pool, struct event_filter_node, 1);
+ node->type = type;
+ node->op = op;
+
+ switch (type) {
+ case EVENT_FILTER_NODE_TYPE_LOGIC:
+ i_unreached();
+ case EVENT_FILTER_NODE_TYPE_EVENT_NAME_WILDCARD:
+ node->str = p_strdup(state->pool, b);
+ if (wildcard_is_literal(node->str))
+ node->type = EVENT_FILTER_NODE_TYPE_EVENT_NAME_EXACT;
+ break;
+ case EVENT_FILTER_NODE_TYPE_EVENT_SOURCE_LOCATION: {
+ const char *colon = strrchr(b, ':');
+ const char *file;
+ uintmax_t line;
+
+ /* split "filename:line-number", but also handle "filename" */
+ if (colon != NULL) {
+ if (str_to_uintmax(colon + 1, &line) < 0) {
+ file = p_strdup(state->pool, b);
+ line = 0;
+ } else {
+ file = p_strdup_until(state->pool, b, colon);
+ }
+ } else {
+ file = p_strdup(state->pool, b);
+ line = 0;
+ }
+
+ node->str = file;
+ node->intmax = line;
+ break;
+ }
+ case EVENT_FILTER_NODE_TYPE_EVENT_CATEGORY:
+ if (!event_filter_category_to_log_type(b, &node->category.log_type)) {
+ node->category.name = p_strdup(state->pool, b);
+ node->category.ptr = event_category_find_registered(b);
+ }
+ break;
+ case EVENT_FILTER_NODE_TYPE_EVENT_FIELD_WILDCARD:
+ node->field.key = p_strdup(state->pool, a);
+ node->field.value.str = p_strdup(state->pool, b);
+
+ /* Filter currently supports only comparing strings
+ and numbers. */
+ if (str_to_intmax(b, &node->field.value.intmax) < 0) {
+ /* not a number - no problem
+ Either we have a string, or a number with wildcards */
+ node->field.value.intmax = INT_MIN;
+ }
+
+ if (wildcard_is_literal(node->field.value.str))
+ node->type = EVENT_FILTER_NODE_TYPE_EVENT_FIELD_EXACT;
+ break;
+ case EVENT_FILTER_NODE_TYPE_EVENT_NAME_EXACT:
+ case EVENT_FILTER_NODE_TYPE_EVENT_FIELD_EXACT:
+ i_unreached();
+ }
+
+ return node;
+}
+
+static struct event_filter_node *logic(struct event_filter_parser_state *state,
+ struct event_filter_node *a,
+ struct event_filter_node *b,
+ enum event_filter_node_op op)
+{
+ struct event_filter_node *node;
+
+ node = p_new(state->pool, struct event_filter_node, 1);
+ node->type = EVENT_FILTER_NODE_TYPE_LOGIC;
+ node->op = op;
+ node->children[0] = a;
+ node->children[1] = b;
+
+ return node;
+}
+
+#ifdef __clang__
+/* ignore "unknown warning" warning if we're using unpatched clang */
+#pragma clang diagnostic ignored "-Wunknown-warning-option"
+/* ignore strict bool warnings in generated code */
+#pragma clang diagnostic ignored "-Wstrict-bool"
+#endif
+%}
+
+%union {
+ const char *str;
+ enum event_filter_node_op op;
+ struct event_filter_node *node;
+};
+
+%token <str> TOKEN STRING
+%token AND OR NOT
+
+%type <str> key value
+%type <op> op
+%type <node> expr key_value
+
+%left AND OR
+%right NOT
+
+%%
+filter : expr { state->output = $1; }
+ | %empty { state->output = NULL; }
+ ;
+
+expr : expr AND expr { $$ = logic(state, $1, $3, EVENT_FILTER_OP_AND); }
+ | expr OR expr { $$ = logic(state, $1, $3, EVENT_FILTER_OP_OR); }
+ | NOT expr { $$ = logic(state, $2, NULL, EVENT_FILTER_OP_NOT); }
+ | '(' expr ')' { $$ = $2; }
+ | key_value { $$ = $1; }
+ ;
+
+key_value : key op value {
+ $$ = key_value(state, $1, $3, $2);
+ if ($$ == NULL) {
+ yyerror(state, state->error);
+ YYERROR;
+ }
+ }
+ ;
+
+key : TOKEN { $$ = $1; }
+ | STRING { $$ = $1; }
+ ;
+
+value : TOKEN { $$ = $1; }
+ | STRING { $$ = $1; }
+ | AND { $$ = "and"; }
+ | OR { $$ = "or"; }
+ | NOT { $$ = "not"; }
+ ;
+
+op : '=' { $$ = EVENT_FILTER_OP_CMP_EQ; }
+ | '>' { $$ = EVENT_FILTER_OP_CMP_GT; }
+ | '<' { $$ = EVENT_FILTER_OP_CMP_LT; }
+ | '>' '=' { $$ = EVENT_FILTER_OP_CMP_GE; }
+ | '<' '=' { $$ = EVENT_FILTER_OP_CMP_LE; }
+ ;
+%%
diff --git a/src/lib/event-filter-private.h b/src/lib/event-filter-private.h
new file mode 100644
index 0000000..5cd47bc
--- /dev/null
+++ b/src/lib/event-filter-private.h
@@ -0,0 +1,121 @@
+#ifndef EVENT_FILTER_PRIVATE_H
+#define EVENT_FILTER_PRIVATE_H
+
+#include "event-filter.h"
+
+enum event_filter_node_op {
+ /* leaf nodes */
+ EVENT_FILTER_OP_CMP_EQ = 1,
+ EVENT_FILTER_OP_CMP_GT,
+ EVENT_FILTER_OP_CMP_LT,
+ EVENT_FILTER_OP_CMP_GE,
+ EVENT_FILTER_OP_CMP_LE,
+
+ /* internal nodes */
+ EVENT_FILTER_OP_AND,
+ EVENT_FILTER_OP_OR,
+ EVENT_FILTER_OP_NOT,
+};
+
+struct event_filter {
+ struct event_filter *prev, *next;
+
+ pool_t pool;
+ int refcount;
+ ARRAY(struct event_filter_query_internal) queries;
+
+ bool fragment;
+ bool named_queries_only;
+};
+
+enum event_filter_node_type {
+ /* internal nodes */
+ EVENT_FILTER_NODE_TYPE_LOGIC = 1, /* children */
+
+ /* leaf nodes */
+ EVENT_FILTER_NODE_TYPE_EVENT_NAME_EXACT, /* str */
+ EVENT_FILTER_NODE_TYPE_EVENT_NAME_WILDCARD, /* str */
+ EVENT_FILTER_NODE_TYPE_EVENT_SOURCE_LOCATION, /* str + int */
+ EVENT_FILTER_NODE_TYPE_EVENT_CATEGORY, /* cat */
+ EVENT_FILTER_NODE_TYPE_EVENT_FIELD_EXACT, /* field */
+ EVENT_FILTER_NODE_TYPE_EVENT_FIELD_WILDCARD, /* field */
+};
+
+enum event_filter_log_type {
+ EVENT_FILTER_LOG_TYPE_DEBUG = BIT(0),
+ EVENT_FILTER_LOG_TYPE_INFO = BIT(1),
+ EVENT_FILTER_LOG_TYPE_WARNING = BIT(2),
+ EVENT_FILTER_LOG_TYPE_ERROR = BIT(3),
+ EVENT_FILTER_LOG_TYPE_FATAL = BIT(4),
+ EVENT_FILTER_LOG_TYPE_PANIC = BIT(5),
+
+ EVENT_FILTER_LOG_TYPE_ALL = 0xff,
+};
+
+struct event_filter_node {
+ enum event_filter_node_type type;
+ enum event_filter_node_op op;
+
+ /* internal node */
+ struct event_filter_node *children[2];
+
+ /* leaf node */
+ const char *str;
+ uintmax_t intmax;
+ struct {
+ /*
+ * We may be dealing with one of three situations:
+ *
+ * 1) the category is a special "log type" category
+ * 2) the category is a "normal" category which is:
+ * a) registered
+ * b) not registered
+ *
+ * A "log type" category is always stored here as the
+ * log_type enum value with the name and ptr members being
+ * NULL.
+ *
+ * A regular category always has a name. Additionally, if
+ * it is registered, the category pointer is non-NULL.
+ */
+ enum event_filter_log_type log_type;
+ const char *name;
+ struct event_category *ptr;
+ } category;
+ struct event_field field;
+};
+
+bool event_filter_category_to_log_type(const char *name,
+ enum event_filter_log_type *log_type_r);
+
+/* lexer & parser state */
+struct event_filter_parser_state {
+ void *scanner;
+ const char *input;
+ size_t len;
+ size_t pos;
+
+ pool_t pool;
+ struct event_filter_node *output;
+ const char *error;
+ bool has_event_name:1;
+};
+
+int event_filter_parser_lex_init(void **scanner);
+int event_filter_parser_lex_destroy(void *yyscanner);
+int event_filter_parser_parse(struct event_filter_parser_state *state);
+void event_filter_parser_set_extra(void *user, void *yyscanner);
+void event_filter_parser_error(void *scan, const char *e);
+
+/* the following are exposed to allow for unit testing */
+bool
+event_filter_query_match_eval(struct event_filter_node *node,
+ struct event *event, const char *source_filename,
+ unsigned int source_linenum,
+ enum event_filter_log_type log_type);
+const char *
+event_filter_category_from_log_type(enum event_filter_log_type log_type);
+struct event_filter_node *
+event_filter_get_expr_for_testing(struct event_filter *filter, unsigned int *count_r);
+
+#endif
diff --git a/src/lib/event-filter.c b/src/lib/event-filter.c
new file mode 100644
index 0000000..fe146e3
--- /dev/null
+++ b/src/lib/event-filter.c
@@ -0,0 +1,855 @@
+/* Copyright (c) 2017-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "llist.h"
+#include "str.h"
+#include "strescape.h"
+#include "wildcard-match.h"
+#include "lib-event-private.h"
+#include "event-filter.h"
+#include "event-filter-private.h"
+
+/* Note: this has to match the regexp behavior in the event filter lexer file */
+#define event_filter_append_escaped(dst, str) \
+ str_append_escaped((dst), (str), strlen(str))
+
+enum event_filter_code {
+ EVENT_FILTER_CODE_NAME = 'n',
+ EVENT_FILTER_CODE_SOURCE = 's',
+ EVENT_FILTER_CODE_CATEGORY = 'c',
+ EVENT_FILTER_CODE_FIELD = 'f',
+};
+
+/* map <log type> to <event filter log type & name> */
+static const struct log_type_map {
+ enum event_filter_log_type log_type;
+ const char *name;
+} event_filter_log_type_map[] = {
+ [LOG_TYPE_DEBUG] = { EVENT_FILTER_LOG_TYPE_DEBUG, "debug" },
+ [LOG_TYPE_INFO] = { EVENT_FILTER_LOG_TYPE_INFO, "info" },
+ [LOG_TYPE_WARNING] = { EVENT_FILTER_LOG_TYPE_WARNING, "warning" },
+ [LOG_TYPE_ERROR] = { EVENT_FILTER_LOG_TYPE_ERROR, "error" },
+ [LOG_TYPE_FATAL] = { EVENT_FILTER_LOG_TYPE_FATAL, "fatal" },
+ [LOG_TYPE_PANIC] = { EVENT_FILTER_LOG_TYPE_PANIC, "panic" },
+};
+
+struct event_filter_query_internal {
+ struct event_filter_node *expr;
+ void *context;
+};
+
+static struct event_filter *event_filters = NULL;
+
+static struct event_filter *event_filter_create_real(pool_t pool, bool fragment)
+{
+ struct event_filter *filter;
+
+ filter = p_new(pool, struct event_filter, 1);
+ filter->pool = pool;
+ filter->refcount = 1;
+ filter->named_queries_only = TRUE;
+ filter->fragment = fragment;
+ p_array_init(&filter->queries, pool, 4);
+ if (!fragment)
+ DLLIST_PREPEND(&event_filters, filter);
+ return filter;
+}
+
+struct event_filter *event_filter_create(void)
+{
+ return event_filter_create_real(pool_alloconly_create("event filter", 2048), FALSE);
+}
+
+struct event_filter *event_filter_create_fragment(pool_t pool)
+{
+ return event_filter_create_real(pool, TRUE);
+}
+
+void event_filter_ref(struct event_filter *filter)
+{
+ i_assert(filter->refcount > 0);
+ filter->refcount++;
+}
+
+void event_filter_unref(struct event_filter **_filter)
+{
+ struct event_filter *filter = *_filter;
+
+ if (filter == NULL)
+ return;
+ i_assert(filter->refcount > 0);
+
+ *_filter = NULL;
+ if (--filter->refcount > 0)
+ return;
+
+ if (!filter->fragment) {
+ DLLIST_REMOVE(&event_filters, filter);
+
+ /* fragments' pools are freed by the consumer */
+ pool_unref(&filter->pool);
+ }
+}
+
+/*
+ * Look for an existing query with the same context pointer and return it.
+ *
+ * If not found, allocate a new internal query and return it.
+ */
+static struct event_filter_query_internal *
+event_filter_get_or_alloc_internal_query(struct event_filter *filter,
+ void *context)
+{
+ struct event_filter_query_internal *query;
+
+ array_foreach_modifiable(&filter->queries, query) {
+ if (query->context == context)
+ return query;
+ }
+
+ /* no matching context, allocate a new query */
+ query = array_append_space(&filter->queries);
+ query->context = context;
+ query->expr = NULL;
+
+ return query;
+}
+
+static void add_node(pool_t pool, struct event_filter_node **root,
+ struct event_filter_node *new,
+ enum event_filter_node_op op)
+{
+ struct event_filter_node *parent;
+
+ i_assert((op == EVENT_FILTER_OP_AND) || (op == EVENT_FILTER_OP_OR));
+
+ if (*root == NULL) {
+ *root = new;
+ return;
+ }
+
+ parent = p_new(pool, struct event_filter_node, 1);
+ parent->type = EVENT_FILTER_NODE_TYPE_LOGIC;
+ parent->op = op;
+ parent->children[0] = *root;
+ parent->children[1] = new;
+
+ *root = parent;
+}
+
+static bool filter_node_requires_event_name(struct event_filter_node *node)
+{
+ switch (node->op) {
+ case EVENT_FILTER_OP_NOT:
+ return filter_node_requires_event_name(node->children[0]);
+ case EVENT_FILTER_OP_AND:
+ return filter_node_requires_event_name(node->children[0]) ||
+ filter_node_requires_event_name(node->children[1]);
+ case EVENT_FILTER_OP_OR:
+ return filter_node_requires_event_name(node->children[0]) &&
+ filter_node_requires_event_name(node->children[1]);
+ default:
+ return node->type == EVENT_FILTER_NODE_TYPE_EVENT_NAME_WILDCARD ||
+ node->type == EVENT_FILTER_NODE_TYPE_EVENT_NAME_EXACT;
+ }
+}
+
+int event_filter_parse(const char *str, struct event_filter *filter,
+ const char **error_r)
+{
+ struct event_filter_query_internal *int_query;
+ struct event_filter_parser_state state;
+ int ret;
+
+ i_zero(&state);
+ state.input = str;
+ state.len = strlen(str);
+ state.pos = 0;
+ state.pool = filter->pool;
+
+ event_filter_parser_lex_init(&state.scanner);
+ event_filter_parser_set_extra(&state, state.scanner);
+
+ ret = event_filter_parser_parse(&state);
+
+ event_filter_parser_lex_destroy(state.scanner);
+
+ if ((ret == 0) && (state.output != NULL)) {
+ /* success - non-NULL expression */
+ i_assert(state.error == NULL);
+
+ int_query = event_filter_get_or_alloc_internal_query(filter, NULL);
+
+ add_node(filter->pool, &int_query->expr, state.output,
+ EVENT_FILTER_OP_OR);
+
+ filter->named_queries_only = filter->named_queries_only &&
+ filter_node_requires_event_name(state.output);
+ } else if (ret != 0) {
+ /* error */
+ i_assert(state.error != NULL);
+
+ *error_r = state.error;
+ }
+
+ /*
+ * Note that success with a NULL expression output is possible, but
+ * turns into a no-op.
+ */
+
+ return (ret != 0) ? -1 : 0;
+}
+
+bool event_filter_category_to_log_type(const char *name,
+ enum event_filter_log_type *log_type_r)
+{
+ unsigned int i;
+
+ for (i = 0; i < N_ELEMENTS(event_filter_log_type_map); i++) {
+ if (strcmp(name, event_filter_log_type_map[i].name) == 0) {
+ *log_type_r = event_filter_log_type_map[i].log_type;
+ return TRUE;
+ }
+ }
+ return FALSE;
+}
+
+const char *
+event_filter_category_from_log_type(enum event_filter_log_type log_type)
+{
+ unsigned int i;
+
+ for (i = 0; i < N_ELEMENTS(event_filter_log_type_map); i++) {
+ if (event_filter_log_type_map[i].log_type == log_type)
+ return event_filter_log_type_map[i].name;
+ }
+ i_unreached();
+}
+
+static struct event_filter_node *
+clone_expr(pool_t pool, struct event_filter_node *old)
+{
+ struct event_filter_node *new;
+
+ if (old == NULL)
+ return NULL;
+
+ new = p_new(pool, struct event_filter_node, 1);
+ new->type = old->type;
+ new->op = old->op;
+ new->children[0] = clone_expr(pool, old->children[0]);
+ new->children[1] = clone_expr(pool, old->children[1]);
+ new->str = p_strdup(pool, old->str);
+ new->intmax = old->intmax;
+ new->category.log_type = old->category.log_type;
+ new->category.name = p_strdup(pool, old->category.name);
+ new->category.ptr = old->category.ptr;
+ new->field.key = p_strdup(pool, old->field.key);
+ new->field.value_type = old->field.value_type;
+ new->field.value.str = p_strdup(pool, old->field.value.str);
+ new->field.value.intmax = old->field.value.intmax;
+ new->field.value.timeval = old->field.value.timeval;
+
+ return new;
+}
+
+static void
+event_filter_merge_with_context_internal(struct event_filter *dest,
+ const struct event_filter *src,
+ void *new_context, bool with_context)
+{
+ const struct event_filter_query_internal *int_query;
+
+ array_foreach(&src->queries, int_query) T_BEGIN {
+ void *context = with_context ? new_context : int_query->context;
+ struct event_filter_query_internal *new;
+
+ new = event_filter_get_or_alloc_internal_query(dest, context);
+
+ add_node(dest->pool, &new->expr,
+ clone_expr(dest->pool, int_query->expr),
+ EVENT_FILTER_OP_OR);
+ } T_END;
+}
+
+bool event_filter_remove_queries_with_context(struct event_filter *filter,
+ void *context)
+{
+ const struct event_filter_query_internal *int_query;
+ unsigned int idx;
+
+ array_foreach(&filter->queries, int_query) {
+ if (int_query->context == context) {
+ idx = array_foreach_idx(&filter->queries, int_query);
+ array_delete(&filter->queries, idx, 1);
+ return TRUE;
+ }
+ }
+ return FALSE;
+}
+
+void event_filter_merge(struct event_filter *dest,
+ const struct event_filter *src)
+{
+ event_filter_merge_with_context_internal(dest, src, NULL, FALSE);
+}
+
+void event_filter_merge_with_context(struct event_filter *dest,
+ const struct event_filter *src,
+ void *new_context)
+{
+ event_filter_merge_with_context_internal(dest, src, new_context, TRUE);
+}
+
+static const char *
+event_filter_export_query_expr_op(enum event_filter_node_op op)
+{
+ switch (op) {
+ case EVENT_FILTER_OP_AND:
+ case EVENT_FILTER_OP_OR:
+ case EVENT_FILTER_OP_NOT:
+ i_unreached();
+ case EVENT_FILTER_OP_CMP_EQ:
+ return "=";
+ case EVENT_FILTER_OP_CMP_GT:
+ return ">";
+ case EVENT_FILTER_OP_CMP_LT:
+ return "<";
+ case EVENT_FILTER_OP_CMP_GE:
+ return ">=";
+ case EVENT_FILTER_OP_CMP_LE:
+ return "<=";
+ }
+
+ i_unreached();
+}
+
+static void
+event_filter_export_query_expr(const struct event_filter_query_internal *query,
+ struct event_filter_node *node,
+ string_t *dest)
+{
+ switch (node->type) {
+ case EVENT_FILTER_NODE_TYPE_LOGIC:
+ str_append_c(dest, '(');
+ switch (node->op) {
+ case EVENT_FILTER_OP_AND:
+ event_filter_export_query_expr(query, node->children[0], dest);
+ str_append(dest, " AND ");
+ event_filter_export_query_expr(query, node->children[1], dest);
+ break;
+ case EVENT_FILTER_OP_OR:
+ event_filter_export_query_expr(query, node->children[0], dest);
+ str_append(dest, " OR ");
+ event_filter_export_query_expr(query, node->children[1], dest);
+ break;
+ case EVENT_FILTER_OP_NOT:
+ str_append(dest, "NOT ");
+ event_filter_export_query_expr(query, node->children[0], dest);
+ break;
+ case EVENT_FILTER_OP_CMP_EQ:
+ case EVENT_FILTER_OP_CMP_GT:
+ case EVENT_FILTER_OP_CMP_LT:
+ case EVENT_FILTER_OP_CMP_GE:
+ case EVENT_FILTER_OP_CMP_LE:
+ i_unreached();
+ }
+ str_append_c(dest, ')');
+ break;
+ case EVENT_FILTER_NODE_TYPE_EVENT_NAME_EXACT:
+ case EVENT_FILTER_NODE_TYPE_EVENT_NAME_WILDCARD:
+ str_append(dest, "event");
+ str_append(dest, event_filter_export_query_expr_op(node->op));
+ str_append_c(dest, '"');
+ event_filter_append_escaped(dest, node->str);
+ str_append_c(dest, '"');
+ break;
+ case EVENT_FILTER_NODE_TYPE_EVENT_SOURCE_LOCATION:
+ str_append(dest, "source_location");
+ str_append(dest, event_filter_export_query_expr_op(node->op));
+ str_append_c(dest, '"');
+ event_filter_append_escaped(dest, node->str);
+ if (node->intmax != 0)
+ str_printfa(dest, ":%ju", node->intmax);
+ str_append_c(dest, '"');
+ break;
+ case EVENT_FILTER_NODE_TYPE_EVENT_CATEGORY:
+ str_append(dest, "category");
+ str_append(dest, event_filter_export_query_expr_op(node->op));
+ if (node->category.name != NULL) {
+ str_append_c(dest, '"');
+ event_filter_append_escaped(dest, node->category.name);
+ str_append_c(dest, '"');
+ } else
+ str_append(dest, event_filter_category_from_log_type(node->category.log_type));
+ break;
+ case EVENT_FILTER_NODE_TYPE_EVENT_FIELD_EXACT:
+ case EVENT_FILTER_NODE_TYPE_EVENT_FIELD_WILDCARD:
+ str_append_c(dest, '"');
+ event_filter_append_escaped(dest, node->field.key);
+ str_append_c(dest, '"');
+ str_append(dest, event_filter_export_query_expr_op(node->op));
+ str_append_c(dest, '"');
+ event_filter_append_escaped(dest, node->field.value.str);
+ str_append_c(dest, '"');
+ break;
+ }
+}
+
+static void
+event_filter_export_query(const struct event_filter_query_internal *query,
+ string_t *dest)
+{
+ str_append_c(dest, '(');
+ event_filter_export_query_expr(query, query->expr, dest);
+ str_append_c(dest, ')');
+}
+
+void event_filter_export(struct event_filter *filter, string_t *dest)
+{
+ const struct event_filter_query_internal *query;
+ bool first = TRUE;
+
+ array_foreach(&filter->queries, query) {
+ if (!first)
+ str_append(dest, " OR ");
+ first = FALSE;
+ event_filter_export_query(query, dest);
+ }
+}
+
+struct event_filter_node *
+event_filter_get_expr_for_testing(struct event_filter *filter,
+ unsigned int *count_r)
+{
+ const struct event_filter_query_internal *queries;
+
+ queries = array_get(&filter->queries, count_r);
+
+ return (*count_r == 0) ? NULL : queries[0].expr;
+}
+
+static bool
+event_category_match(const struct event_category *category,
+ const struct event_category *wanted_category)
+{
+ for (; category != NULL; category = category->parent) {
+ if (category->internal == wanted_category->internal)
+ return TRUE;
+ }
+ return FALSE;
+}
+
+static bool
+event_has_category_nonrecursive(struct event *event,
+ struct event_category *wanted_category)
+{
+ struct event_category *cat;
+
+ if (array_is_created(&event->categories)) {
+ array_foreach_elem(&event->categories, cat) {
+ if (event_category_match(cat, wanted_category))
+ return TRUE;
+ }
+ }
+ return FALSE;
+}
+
+static bool
+event_has_category(struct event *event, struct event_filter_node *node,
+ enum event_filter_log_type log_type)
+{
+ struct event_category *wanted_category = node->category.ptr;
+
+ /* category is a log type */
+ if (node->category.name == NULL)
+ return (node->category.log_type & log_type) != 0;
+
+ /* category not registered, therefore the event cannot have it */
+ if (wanted_category == NULL)
+ return FALSE;
+
+ while (event != NULL) {
+ if (event_has_category_nonrecursive(event, wanted_category))
+ return TRUE;
+ /* try also the parent events */
+ event = event_get_parent(event);
+ }
+ /* check also the global event and its parents */
+ event = event_get_global();
+ while (event != NULL) {
+ if (event_has_category_nonrecursive(event, wanted_category))
+ return TRUE;
+ event = event_get_parent(event);
+ }
+ return FALSE;
+}
+
+static bool
+event_match_strlist_recursive(struct event *event,
+ const struct event_field *wanted_field,
+ bool use_strcmp, bool *seen)
+{
+ const char *wanted_value = wanted_field->value.str;
+ const struct event_field *field;
+ const char *value;
+ bool match;
+
+ if (event == NULL)
+ return FALSE;
+
+ field = event_find_field_nonrecursive(event, wanted_field->key);
+ if (field != NULL) {
+ i_assert(field->value_type == EVENT_FIELD_VALUE_TYPE_STRLIST);
+ array_foreach_elem(&field->value.strlist, value) {
+ *seen = TRUE;
+ match = use_strcmp ? strcmp(value, wanted_value) == 0 :
+ wildcard_match_icase(value, wanted_value);
+ if (match)
+ return TRUE;
+ }
+ }
+ return event_match_strlist_recursive(event->parent, wanted_field,
+ use_strcmp, seen);
+}
+
+static bool
+event_match_strlist(struct event *event, const struct event_field *wanted_field,
+ bool use_strcmp)
+{
+ bool seen = FALSE;
+
+ if (event_match_strlist_recursive(event, wanted_field,
+ use_strcmp, &seen))
+ return TRUE;
+ if (event_match_strlist_recursive(event_get_global(),
+ wanted_field, use_strcmp, &seen))
+ return TRUE;
+ if (wanted_field->value.str[0] == '\0' && !seen) {
+ /* strlist="" matches nonexistent strlist */
+ return TRUE;
+ }
+ return FALSE;
+
+}
+
+static bool
+event_match_field(struct event *event, const struct event_field *wanted_field,
+ enum event_filter_node_op op, bool use_strcmp)
+{
+ const struct event_field *field;
+
+ /* wanted_field has the value in all available formats */
+ field = event_find_field_recursive(event, wanted_field->key);
+ if (field == NULL) {
+ /* field="" matches nonexistent field */
+ return wanted_field->value.str[0] == '\0';
+ }
+
+ switch (field->value_type) {
+ case EVENT_FIELD_VALUE_TYPE_STR:
+ if (op != EVENT_FILTER_OP_CMP_EQ) {
+ /* we only support string equality comparisons */
+ return FALSE;
+ }
+ if (field->value.str[0] == '\0') {
+ /* field was removed, but it matches field="" filter */
+ return wanted_field->value.str[0] == '\0';
+ }
+ if (use_strcmp)
+ return strcasecmp(field->value.str, wanted_field->value.str) == 0;
+ else
+ return wildcard_match_icase(field->value.str, wanted_field->value.str);
+ case EVENT_FIELD_VALUE_TYPE_INTMAX:
+ if (wanted_field->value.intmax > INT_MIN) {
+ /* compare against an integer */
+ switch (op) {
+ case EVENT_FILTER_OP_CMP_EQ:
+ return field->value.intmax == wanted_field->value.intmax;
+ case EVENT_FILTER_OP_CMP_GT:
+ return field->value.intmax > wanted_field->value.intmax;
+ case EVENT_FILTER_OP_CMP_LT:
+ return field->value.intmax < wanted_field->value.intmax;
+ case EVENT_FILTER_OP_CMP_GE:
+ return field->value.intmax >= wanted_field->value.intmax;
+ case EVENT_FILTER_OP_CMP_LE:
+ return field->value.intmax <= wanted_field->value.intmax;
+ case EVENT_FILTER_OP_AND:
+ case EVENT_FILTER_OP_OR:
+ case EVENT_FILTER_OP_NOT:
+ i_unreached();
+ }
+ i_unreached();
+ } else {
+ /* compare against an "integer" with wildcards */
+ if (op != EVENT_FILTER_OP_CMP_EQ) {
+ /* we only support string equality comparisons */
+ return FALSE;
+ }
+ char tmp[MAX_INT_STRLEN];
+ i_snprintf(tmp, sizeof(tmp), "%jd", field->value.intmax);
+ if (use_strcmp)
+ return strcasecmp(field->value.str, wanted_field->value.str) == 0;
+ else
+ return wildcard_match_icase(tmp, wanted_field->value.str);
+ }
+ case EVENT_FIELD_VALUE_TYPE_TIMEVAL:
+ /* there's no point to support matching exact timestamps */
+ return FALSE;
+ case EVENT_FIELD_VALUE_TYPE_STRLIST:
+ /* check if the value is (or is not) on the list,
+ only string matching makes sense here. */
+ if (op != EVENT_FILTER_OP_CMP_EQ)
+ return FALSE;
+ return event_match_strlist(event, wanted_field, use_strcmp);
+ }
+ i_unreached();
+}
+
+static bool
+event_filter_query_match_cmp(struct event_filter_node *node,
+ struct event *event, const char *source_filename,
+ unsigned int source_linenum,
+ enum event_filter_log_type log_type)
+{
+ i_assert((node->op == EVENT_FILTER_OP_CMP_EQ) ||
+ (node->op == EVENT_FILTER_OP_CMP_GT) ||
+ (node->op == EVENT_FILTER_OP_CMP_LT) ||
+ (node->op == EVENT_FILTER_OP_CMP_GE) ||
+ (node->op == EVENT_FILTER_OP_CMP_LE));
+
+ switch (node->type) {
+ case EVENT_FILTER_NODE_TYPE_LOGIC:
+ i_unreached();
+ case EVENT_FILTER_NODE_TYPE_EVENT_NAME_EXACT:
+ return (event->sending_name != NULL) &&
+ strcmp(event->sending_name, node->str) == 0;
+ case EVENT_FILTER_NODE_TYPE_EVENT_NAME_WILDCARD:
+ return (event->sending_name != NULL) &&
+ wildcard_match(event->sending_name, node->str);
+ case EVENT_FILTER_NODE_TYPE_EVENT_SOURCE_LOCATION:
+ return !((source_linenum != node->intmax &&
+ node->intmax != 0) ||
+ source_filename == NULL ||
+ strcmp(event->source_filename, node->str) != 0);
+ case EVENT_FILTER_NODE_TYPE_EVENT_CATEGORY:
+ return event_has_category(event, node, log_type);
+ case EVENT_FILTER_NODE_TYPE_EVENT_FIELD_EXACT:
+ return event_match_field(event, &node->field, node->op,
+ TRUE);
+ case EVENT_FILTER_NODE_TYPE_EVENT_FIELD_WILDCARD:
+ return event_match_field(event, &node->field, node->op,
+ FALSE);
+ }
+
+ i_unreached();
+}
+
+bool
+event_filter_query_match_eval(struct event_filter_node *node,
+ struct event *event, const char *source_filename,
+ unsigned int source_linenum,
+ enum event_filter_log_type log_type)
+{
+ switch (node->op) {
+ case EVENT_FILTER_OP_CMP_EQ:
+ case EVENT_FILTER_OP_CMP_GT:
+ case EVENT_FILTER_OP_CMP_LT:
+ case EVENT_FILTER_OP_CMP_GE:
+ case EVENT_FILTER_OP_CMP_LE:
+ return event_filter_query_match_cmp(node, event, source_filename,
+ source_linenum, log_type);
+ case EVENT_FILTER_OP_AND:
+ return event_filter_query_match_eval(node->children[0], event,
+ source_filename, source_linenum,
+ log_type) &&
+ event_filter_query_match_eval(node->children[1], event,
+ source_filename, source_linenum,
+ log_type);
+ case EVENT_FILTER_OP_OR:
+ return event_filter_query_match_eval(node->children[0], event,
+ source_filename, source_linenum,
+ log_type) ||
+ event_filter_query_match_eval(node->children[1], event,
+ source_filename, source_linenum,
+ log_type);
+ case EVENT_FILTER_OP_NOT:
+ return !event_filter_query_match_eval(node->children[0], event,
+ source_filename, source_linenum,
+ log_type);
+ }
+
+ i_unreached();
+}
+
+static bool
+event_filter_query_match(const struct event_filter_query_internal *query,
+ struct event *event, const char *source_filename,
+ unsigned int source_linenum,
+ const struct failure_context *ctx)
+{
+ enum event_filter_log_type log_type;
+
+ i_assert(ctx->type < N_ELEMENTS(event_filter_log_type_map));
+ log_type = event_filter_log_type_map[ctx->type].log_type;
+
+ return event_filter_query_match_eval(query->expr, event, source_filename,
+ source_linenum, log_type);
+}
+
+static bool
+event_filter_match_fastpath(struct event_filter *filter, struct event *event)
+{
+ if (filter->named_queries_only && event->sending_name == NULL) {
+ /* No debug logging is enabled. Only named events may be wanted
+ for stats. This event doesn't have a name, so we don't need
+ to check any further. */
+ return FALSE;
+ }
+ return TRUE;
+}
+
+bool event_filter_match(struct event_filter *filter, struct event *event,
+ const struct failure_context *ctx)
+{
+ if (filter == NULL)
+ return FALSE;
+ return event_filter_match_source(filter, event, event->source_filename,
+ event->source_linenum, ctx);
+}
+
+bool event_filter_match_source(struct event_filter *filter, struct event *event,
+ const char *source_filename,
+ unsigned int source_linenum,
+ const struct failure_context *ctx)
+{
+ const struct event_filter_query_internal *query;
+
+ i_assert(!filter->fragment);
+
+ if (!event_filter_match_fastpath(filter, event))
+ return FALSE;
+
+ array_foreach(&filter->queries, query) {
+ if (event_filter_query_match(query, event, source_filename,
+ source_linenum, ctx))
+ return TRUE;
+ }
+ return FALSE;
+}
+
+struct event_filter_match_iter {
+ struct event_filter *filter;
+ struct event *event;
+ const struct failure_context *failure_ctx;
+ unsigned int idx;
+};
+
+struct event_filter_match_iter *
+event_filter_match_iter_init(struct event_filter *filter, struct event *event,
+ const struct failure_context *ctx)
+{
+ struct event_filter_match_iter *iter;
+
+ i_assert(!filter->fragment);
+
+ iter = i_new(struct event_filter_match_iter, 1);
+ iter->filter = filter;
+ iter->event = event;
+ iter->failure_ctx = ctx;
+ if (!event_filter_match_fastpath(filter, event))
+ iter->idx = UINT_MAX;
+ return iter;
+}
+
+void *event_filter_match_iter_next(struct event_filter_match_iter *iter)
+{
+ const struct event_filter_query_internal *queries;
+ unsigned int count;
+
+ queries = array_get(&iter->filter->queries, &count);
+ while (iter->idx < count) {
+ const struct event_filter_query_internal *query =
+ &queries[iter->idx];
+
+ iter->idx++;
+ if (query->context != NULL &&
+ event_filter_query_match(query, iter->event,
+ iter->event->source_filename,
+ iter->event->source_linenum,
+ iter->failure_ctx))
+ return query->context;
+ }
+ return NULL;
+}
+
+void event_filter_match_iter_deinit(struct event_filter_match_iter **_iter)
+{
+ struct event_filter_match_iter *iter = *_iter;
+
+ *_iter = NULL;
+ i_free(iter);
+}
+
+static void
+event_filter_query_update_category(struct event_filter_query_internal *query,
+ struct event_filter_node *node,
+ struct event_category *category,
+ bool add)
+{
+ if (node == NULL)
+ return;
+
+ switch (node->type) {
+ case EVENT_FILTER_NODE_TYPE_LOGIC:
+ event_filter_query_update_category(query, node->children[0], category, add);
+ event_filter_query_update_category(query, node->children[1], category, add);
+ break;
+ case EVENT_FILTER_NODE_TYPE_EVENT_NAME_EXACT:
+ case EVENT_FILTER_NODE_TYPE_EVENT_NAME_WILDCARD:
+ case EVENT_FILTER_NODE_TYPE_EVENT_SOURCE_LOCATION:
+ case EVENT_FILTER_NODE_TYPE_EVENT_FIELD_EXACT:
+ case EVENT_FILTER_NODE_TYPE_EVENT_FIELD_WILDCARD:
+ break;
+ case EVENT_FILTER_NODE_TYPE_EVENT_CATEGORY:
+ if (node->category.name == NULL)
+ break; /* log type */
+
+ if (add) {
+ if (node->category.ptr != NULL)
+ break;
+
+ if (strcmp(node->category.name, category->name) == 0)
+ node->category.ptr = category;
+ } else {
+ if (node->category.ptr == category)
+ node->category.ptr = NULL;
+ }
+ break;
+ }
+}
+
+static void event_filter_category_registered(struct event_category *category)
+{
+ const bool add = category->internal != NULL;
+ struct event_filter_query_internal *query;
+ struct event_filter *filter;
+
+ for (filter = event_filters; filter != NULL; filter = filter->next) {
+ array_foreach_modifiable(&filter->queries, query) {
+ event_filter_query_update_category(query, query->expr,
+ category, add);
+ }
+ }
+}
+
+void event_filter_init(void)
+{
+ event_category_register_callback(event_filter_category_registered);
+}
+
+void event_filter_deinit(void)
+{
+ event_category_unregister_callback(event_filter_category_registered);
+}
diff --git a/src/lib/event-filter.h b/src/lib/event-filter.h
new file mode 100644
index 0000000..0a55e05
--- /dev/null
+++ b/src/lib/event-filter.h
@@ -0,0 +1,64 @@
+#ifndef EVENT_FILTER_H
+#define EVENT_FILTER_H
+
+struct event;
+
+struct event_filter_field {
+ const char *key;
+ const char *value;
+};
+
+struct event_filter *event_filter_create(void);
+struct event_filter *event_filter_create_fragment(pool_t pool);
+void event_filter_ref(struct event_filter *filter);
+void event_filter_unref(struct event_filter **filter);
+
+/* Add queries from source filter to destination filter. */
+void event_filter_merge(struct event_filter *dest,
+ const struct event_filter *src);
+/* Add queries from source filter to destination filter, but with supplied
+ context overriding whatever context source queries had. */
+void event_filter_merge_with_context(struct event_filter *dest,
+ const struct event_filter *src,
+ void *new_context);
+
+/* Remove query with given context from filter.
+ Returns TRUE if query was removed, otherwise FALSE. */
+bool event_filter_remove_queries_with_context(struct event_filter *filter,
+ void *context);
+
+/* Export the filter into a string. The context pointers aren't exported. */
+void event_filter_export(struct event_filter *filter, string_t *dest);
+/* Add queries to the filter from the given string. The string is expected to
+ be generated by event_filter_export(). Returns TRUE on success, FALSE on
+ invalid string. */
+#define event_filter_import(filter, str, error_r) \
+ (event_filter_parse((str), (filter), (error_r)) == 0)
+
+/* Parse a string-ified query, filling the passed in filter */
+int event_filter_parse(const char *str, struct event_filter *filter,
+ const char **error_r);
+
+/* Returns TRUE if the event matches the event filter. */
+bool event_filter_match(struct event_filter *filter, struct event *event,
+ const struct failure_context *ctx);
+/* Same as event_filter_match(), but use the given source filename:linenum
+ instead of taking it from the event. */
+bool event_filter_match_source(struct event_filter *filter, struct event *event,
+ const char *source_filename,
+ unsigned int source_linenum,
+ const struct failure_context *ctx);
+
+/* Iterate through all queries with non-NULL context that match the event. */
+struct event_filter_match_iter *
+event_filter_match_iter_init(struct event_filter *filter, struct event *event,
+ const struct failure_context *ctx);
+/* Return context for the query that matched, or NULL when there are no more
+ matches. Note: This skips over any queries that have NULL context. */
+void *event_filter_match_iter_next(struct event_filter_match_iter *iter);
+void event_filter_match_iter_deinit(struct event_filter_match_iter **iter);
+
+void event_filter_init(void);
+void event_filter_deinit(void);
+
+#endif
diff --git a/src/lib/event-log.c b/src/lib/event-log.c
new file mode 100644
index 0000000..f8f2f79
--- /dev/null
+++ b/src/lib/event-log.c
@@ -0,0 +1,461 @@
+/* Copyright (c) 2017-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "str.h"
+#include "event-filter.h"
+#include "lib-event-private.h"
+
+unsigned int event_filter_replace_counter = 1;
+
+static struct event_filter *global_debug_log_filter = NULL;
+static struct event_filter *global_debug_send_filter = NULL;
+static struct event_filter *global_core_log_filter = NULL;
+
+#undef e_error
+void e_error(struct event *event,
+ const char *source_filename, unsigned int source_linenum,
+ const char *fmt, ...)
+{
+ struct event_log_params params = {
+ .log_type = LOG_TYPE_ERROR,
+ .source_filename = source_filename,
+ .source_linenum = source_linenum,
+ };
+ va_list args;
+
+ va_start(args, fmt);
+ T_BEGIN {
+ event_logv(event, &params, fmt, args);
+ } T_END;
+ va_end(args);
+}
+
+#undef e_warning
+void e_warning(struct event *event,
+ const char *source_filename, unsigned int source_linenum,
+ const char *fmt, ...)
+{
+ struct event_log_params params = {
+ .log_type = LOG_TYPE_WARNING,
+ .source_filename = source_filename,
+ .source_linenum = source_linenum,
+ };
+ va_list args;
+
+ va_start(args, fmt);
+ T_BEGIN {
+ event_logv(event, &params, fmt, args);
+ } T_END;
+ va_end(args);
+}
+
+#undef e_info
+void e_info(struct event *event,
+ const char *source_filename, unsigned int source_linenum,
+ const char *fmt, ...)
+{
+ struct event_log_params params = {
+ .log_type = LOG_TYPE_INFO,
+ .source_filename = source_filename,
+ .source_linenum = source_linenum,
+ };
+ va_list args;
+
+ va_start(args, fmt);
+ T_BEGIN {
+ event_logv(event, &params, fmt, args);
+ } T_END;
+ va_end(args);
+}
+
+#undef e_debug
+void e_debug(struct event *event,
+ const char *source_filename, unsigned int source_linenum,
+ const char *fmt, ...)
+{
+ struct event_log_params params = {
+ .log_type = LOG_TYPE_DEBUG,
+ .source_filename = source_filename,
+ .source_linenum = source_linenum,
+ };
+ va_list args;
+
+ va_start(args, fmt);
+ T_BEGIN {
+ event_logv(event, &params, fmt, args);
+ } T_END;
+ va_end(args);
+}
+
+#undef e_log
+void e_log(struct event *event, enum log_type level,
+ const char *source_filename, unsigned int source_linenum,
+ const char *fmt, ...)
+{
+ struct event_log_params params = {
+ .log_type = level,
+ .source_filename = source_filename,
+ .source_linenum = source_linenum,
+ };
+ va_list args;
+
+ va_start(args, fmt);
+ T_BEGIN {
+ event_logv(event, &params, fmt, args);
+ } T_END;
+ va_end(args);
+}
+
+struct event_get_log_message_context {
+ const struct event_log_params *params;
+
+ string_t *log_prefix;
+ const char *message;
+ unsigned int type_pos;
+
+ bool replace_prefix:1;
+ bool str_out_done:1;
+};
+
+static inline void ATTR_FORMAT(2, 0)
+event_get_log_message_str_out(struct event_get_log_message_context *glmctx,
+ const char *fmt, va_list args)
+{
+ const struct event_log_params *params = glmctx->params;
+ string_t *str_out = params->base_str_out;
+
+ /* The message is appended once in full, rather than incremental during
+ the recursion. */
+
+ if (glmctx->str_out_done || str_out == NULL)
+ return;
+
+ /* append the current log prefix to the string buffer */
+ if (params->base_str_prefix != NULL && !glmctx->replace_prefix)
+ str_append(str_out, params->base_str_prefix);
+ str_append_str(str_out, glmctx->log_prefix);
+
+ if (glmctx->message != NULL) {
+ /* a child event already constructed a message */
+ str_append(str_out, glmctx->message);
+ } else {
+ va_list args_copy;
+
+ /* construct message from format and arguments */
+ VA_COPY(args_copy, args);
+ str_vprintfa(str_out, fmt, args_copy);
+ va_end(args_copy);
+ }
+
+ /* finished with the string buffer */
+ glmctx->str_out_done = TRUE;
+}
+
+static bool ATTR_FORMAT(4, 0)
+event_get_log_message(struct event *event,
+ struct event_get_log_message_context *glmctx,
+ unsigned int prefixes_dropped,
+ const char *fmt, va_list args)
+{
+ const struct event_log_params *params = glmctx->params;
+ const char *prefix = event->log_prefix;
+ bool ret = FALSE;
+
+ /* Reached the base event? */
+ if (event == params->base_event) {
+ /* Append the message to the provided string buffer. */
+ event_get_log_message_str_out(glmctx, fmt, args);
+ /* Insert the base send prefix */
+ if (params->base_send_prefix != NULL) {
+ str_insert(glmctx->log_prefix, 0,
+ params->base_send_prefix);
+ ret = TRUE;
+ }
+ }
+
+ /* Call the message amendment callback for this event if there is one.
+ */
+ if (event->log_message_callback != NULL) {
+ const char *in_message;
+
+ /* construct the log message composed by children and arguments
+ */
+ if (glmctx->message == NULL) {
+ str_vprintfa(glmctx->log_prefix, fmt, args);
+ in_message = str_c(glmctx->log_prefix);
+ } else if (str_len(glmctx->log_prefix) == 0) {
+ in_message = glmctx->message;
+ } else {
+ str_append(glmctx->log_prefix, glmctx->message);
+ in_message = str_c(glmctx->log_prefix);
+ }
+
+ /* reformat the log message */
+ glmctx->message = event->log_message_callback(
+ event->log_message_callback_context,
+ glmctx->params->log_type, in_message);
+
+ /* continue with a cleared prefix buffer (as prefix is now part
+ of *message_r). */
+ str_truncate(glmctx->log_prefix, 0);
+ ret = TRUE;
+ }
+
+ if (event->log_prefix_callback != NULL) {
+ prefix = event->log_prefix_callback(
+ event->log_prefix_callback_context);
+ }
+ if (event->log_prefix_replace) {
+ /* this event replaces all parent log prefixes */
+ glmctx->replace_prefix = TRUE;
+ glmctx->type_pos = (prefix == NULL ? 0 : strlen(prefix));
+ event_get_log_message_str_out(glmctx, fmt, args);
+ }
+ if (prefix != NULL) {
+ if (event->log_prefix_replace || prefixes_dropped == 0) {
+ str_insert(glmctx->log_prefix, 0, prefix);
+ ret = TRUE;
+ } else if (prefixes_dropped > 0) {
+ prefixes_dropped--;
+ }
+ }
+ if (event->parent == NULL) {
+ event_get_log_message_str_out(glmctx, fmt, args);
+ if (params->base_event == NULL &&
+ params->base_send_prefix != NULL &&
+ !glmctx->replace_prefix) {
+ str_insert(glmctx->log_prefix, 0,
+ params->base_send_prefix);
+ ret = TRUE;
+ }
+ } else if (!event->log_prefix_replace &&
+ (!params->no_send || !glmctx->str_out_done)) {
+ prefixes_dropped += event->log_prefixes_dropped;
+ if (event_get_log_message(event->parent, glmctx,
+ prefixes_dropped, fmt, args))
+ ret = TRUE;
+ }
+ return ret;
+}
+
+void event_log(struct event *event, const struct event_log_params *params,
+ const char *fmt, ...)
+{
+ va_list args;
+
+ va_start(args, fmt);
+ event_logv(event, params, fmt, args);
+ va_end(args);
+}
+
+#undef event_want_log_level
+bool event_want_log_level(struct event *event, enum log_type level,
+ const char *source_filename,
+ unsigned int source_linenum)
+{
+ struct failure_context ctx = { .type = LOG_TYPE_DEBUG };
+
+ if (level >= event->min_log_level) {
+ /* Always log when level is at least this high */
+ return TRUE;
+ }
+
+ if (event->debug_level_checked_filter_counter == event_filter_replace_counter) {
+ /* Log filters haven't changed since we last checked this, so
+ we can rely on the last cached value. FIXME: this doesn't
+ work correctly if event changes and the change affects
+ whether the filters would match. */
+ return event->sending_debug_log;
+ }
+ event->debug_level_checked_filter_counter =
+ event_filter_replace_counter;
+
+ if (event->forced_debug) {
+ /* Debugging is forced for this event (and its children) */
+ event->sending_debug_log = TRUE;
+ } else if (global_debug_log_filter != NULL &&
+ event_filter_match_source(global_debug_log_filter, event,
+ source_filename, source_linenum, &ctx)) {
+ /* log_debug filter matched */
+ event->sending_debug_log = TRUE;
+ } else if (global_core_log_filter != NULL &&
+ event_filter_match_source(global_core_log_filter, event,
+ source_filename, source_linenum, &ctx)) {
+ /* log_core_filter matched */
+ event->sending_debug_log = TRUE;
+ } else {
+ event->sending_debug_log = FALSE;
+ }
+ return event->sending_debug_log;
+}
+
+#undef event_want_level
+bool event_want_level(struct event *event, enum log_type level,
+ const char *source_filename,
+ unsigned int source_linenum)
+{
+ if (event_want_log_level(event, level, source_filename, source_linenum))
+ return TRUE;
+
+ /* see if debug send filtering matches */
+ if (global_debug_send_filter != NULL) {
+ struct failure_context ctx = { .type = LOG_TYPE_DEBUG };
+
+ if (event_filter_match_source(global_debug_send_filter, event,
+ source_filename, source_linenum,
+ &ctx))
+ return TRUE;
+ }
+ return FALSE;
+}
+
+static void ATTR_FORMAT(3, 0)
+event_logv_params(struct event *event, const struct event_log_params *params,
+ const char *fmt, va_list args)
+{
+ struct event_get_log_message_context glmctx;
+
+ struct failure_context ctx = {
+ .type = params->log_type,
+ };
+ bool abort_after_event = FALSE;
+
+ i_assert(!params->no_send || params->base_str_out != NULL);
+
+ if (global_core_log_filter != NULL &&
+ event_filter_match_source(global_core_log_filter, event,
+ event->source_filename,
+ event->source_linenum, &ctx))
+ abort_after_event = TRUE;
+
+ i_zero(&glmctx);
+ glmctx.params = params;
+ glmctx.log_prefix = t_str_new(64);
+ if (!event_get_log_message(event, &glmctx, 0, fmt, args)) {
+ /* keep log prefix as it is */
+ if (params->base_str_out != NULL && !glmctx.str_out_done) {
+ va_list args_copy;
+
+ VA_COPY(args_copy, args);
+ str_vprintfa(params->base_str_out, fmt, args_copy);
+ va_end(args_copy);
+ }
+ if (!params->no_send)
+ event_vsend(event, &ctx, fmt, args);
+ } else if (params->no_send) {
+ /* don't send the event */
+ } else if (glmctx.replace_prefix) {
+ /* event overrides the log prefix (even if it's "") */
+ ctx.log_prefix = str_c(glmctx.log_prefix);
+ ctx.log_prefix_type_pos = glmctx.type_pos;
+ if (glmctx.message != NULL)
+ event_send(event, &ctx, "%s", glmctx.message);
+ else
+ event_vsend(event, &ctx, fmt, args);
+ } else {
+ /* append to log prefix, but don't fully replace it */
+ if (glmctx.message != NULL)
+ str_append(glmctx.log_prefix, glmctx.message);
+ else
+ str_vprintfa(glmctx.log_prefix, fmt, args);
+ event_send(event, &ctx, "%s", str_c(glmctx.log_prefix));
+ }
+ if (abort_after_event)
+ abort();
+}
+
+void event_logv(struct event *event, const struct event_log_params *params,
+ const char *fmt, va_list args)
+{
+ const char *orig_source_filename = event->source_filename;
+ unsigned int orig_source_linenum = event->source_linenum;
+ int old_errno = errno;
+
+ if (params->source_filename != NULL) {
+ event_set_source(event, params->source_filename,
+ params->source_linenum, TRUE);
+ }
+
+ (void)event_want_log_level(event, params->log_type,
+ event->source_filename,
+ event->source_linenum);
+
+ event_ref(event);
+ event_logv_params(event, params, fmt, args);
+ event_set_source(event, orig_source_filename,
+ orig_source_linenum, TRUE);
+ event_unref(&event);
+ errno = old_errno;
+}
+
+struct event *event_set_forced_debug(struct event *event, bool force)
+{
+ if (force)
+ event->forced_debug = TRUE;
+ event_recalculate_debug_level(event);
+ return event;
+}
+
+struct event *event_unset_forced_debug(struct event *event)
+{
+ event->forced_debug = FALSE;
+ event_recalculate_debug_level(event);
+ return event;
+}
+
+void event_set_global_debug_log_filter(struct event_filter *filter)
+{
+ event_unset_global_debug_log_filter();
+ global_debug_log_filter = filter;
+ event_filter_ref(global_debug_log_filter);
+ event_filter_replace_counter++;
+}
+
+struct event_filter *event_get_global_debug_log_filter(void)
+{
+ return global_debug_log_filter;
+}
+
+void event_unset_global_debug_log_filter(void)
+{
+ event_filter_unref(&global_debug_log_filter);
+ event_filter_replace_counter++;
+}
+
+void event_set_global_debug_send_filter(struct event_filter *filter)
+{
+ event_unset_global_debug_send_filter();
+ global_debug_send_filter = filter;
+ event_filter_ref(global_debug_send_filter);
+ event_filter_replace_counter++;
+}
+
+struct event_filter *event_get_global_debug_send_filter(void)
+{
+ return global_debug_send_filter;
+}
+
+void event_unset_global_debug_send_filter(void)
+{
+ event_filter_unref(&global_debug_send_filter);
+ event_filter_replace_counter++;
+}
+
+void event_set_global_core_log_filter(struct event_filter *filter)
+{
+ event_unset_global_core_log_filter();
+ global_core_log_filter = filter;
+ event_filter_ref(global_core_log_filter);
+ event_filter_replace_counter++;
+}
+
+struct event_filter *event_get_global_core_log_filter(void)
+{
+ return global_core_log_filter;
+}
+
+void event_unset_global_core_log_filter(void)
+{
+ event_filter_unref(&global_core_log_filter);
+ event_filter_replace_counter++;
+}
diff --git a/src/lib/event-log.h b/src/lib/event-log.h
new file mode 100644
index 0000000..e5b7eb8
--- /dev/null
+++ b/src/lib/event-log.h
@@ -0,0 +1,151 @@
+#ifndef EVENT_LOG_H
+#define EVENT_LOG_H
+
+struct event_filter;
+
+#include "lib-event.h"
+
+struct event_log_params {
+ enum log_type log_type;
+ const char *source_filename;
+ unsigned int source_linenum;
+
+ /* Base event used as a reference for base_* parameters (see below) */
+ struct event *base_event;
+
+ /* Append the event message to base_str_out in addition to emitting the
+ event as normal. The message appended to the string buffer includes
+ prefixes and message callback modifications by parent events up until
+ the base_event. The event is otherwise sent as normal with the full
+ prefixes and all modifications up to the root event (unless
+ no_send=TRUE). This is primarily useful to mimic (part of) event
+ logging in parallel logs that are visible to users. */
+ string_t *base_str_out;
+
+ /* Prefix inserted at the base_event for the sent log message. */
+ const char *base_send_prefix;
+ /* Prefix inserted at the base_event for the log message appended to the
+ string buffer. */
+ const char *base_str_prefix;
+
+ /* Don't actually send the event; only append to the provided string
+ buffer (base_str_out must not be NULL). */
+ bool no_send:1;
+};
+
+/* Increased every time global event filters have changed. */
+extern unsigned int event_filter_replace_counter;
+
+void e_error(struct event *event,
+ const char *source_filename, unsigned int source_linenum,
+ const char *fmt, ...) ATTR_FORMAT(4, 5);
+#define e_error(_event, ...) STMT_START { \
+ struct event *_tmp_event = (_event); \
+ if (event_want_level(_tmp_event, LOG_TYPE_ERROR)) \
+ e_error(_tmp_event, __FILE__, __LINE__, __VA_ARGS__); \
+ else \
+ event_send_abort(_tmp_event); \
+ } STMT_END
+void e_warning(struct event *event,
+ const char *source_filename, unsigned int source_linenum,
+ const char *fmt, ...) ATTR_FORMAT(4, 5);
+#define e_warning(_event, ...) STMT_START { \
+ struct event *_tmp_event = (_event); \
+ if (event_want_level(_tmp_event, LOG_TYPE_WARNING)) \
+ e_warning(_tmp_event, __FILE__, __LINE__, __VA_ARGS__); \
+ else \
+ event_send_abort(_tmp_event); \
+ } STMT_END
+void e_info(struct event *event,
+ const char *source_filename, unsigned int source_linenum,
+ const char *fmt, ...) ATTR_FORMAT(4, 5);
+#define e_info(_event, ...) STMT_START { \
+ struct event *_tmp_event = (_event); \
+ if (event_want_level(_tmp_event, LOG_TYPE_INFO)) \
+ e_info(_tmp_event, __FILE__, __LINE__, __VA_ARGS__); \
+ else \
+ event_send_abort(_tmp_event); \
+ } STMT_END
+void e_debug(struct event *event,
+ const char *source_filename, unsigned int source_linenum,
+ const char *fmt, ...) ATTR_FORMAT(4, 5);
+#define e_debug(_event, ...) STMT_START { \
+ struct event *_tmp_event = (_event); \
+ if (event_want_debug(_tmp_event)) \
+ e_debug(_tmp_event, __FILE__, __LINE__, __VA_ARGS__); \
+ else \
+ event_send_abort(_tmp_event); \
+ } STMT_END
+
+void e_log(struct event *event, enum log_type level,
+ const char *source_filename, unsigned int source_linenum,
+ const char *fmt, ...) ATTR_FORMAT(5, 6);
+#define e_log(_event, level, ...) STMT_START { \
+ struct event *_tmp_event = (_event); \
+ if (event_want_level(_tmp_event, level)) \
+ e_log(_tmp_event, level, __FILE__, __LINE__, __VA_ARGS__); \
+ else \
+ event_send_abort(_tmp_event); \
+ } STMT_END
+
+/* Returns TRUE if event should be logged. Typically event_want_debug_log()
+ could be used in deciding whether to build an expensive debug log message
+ (e.g. requires extra disk IO). Note that if this is used, the actual
+ event being sent won't be matched against event filters because it's never
+ called. The result of the check is cached in the event, so repeated calls
+ are efficient. */
+bool event_want_log_level(struct event *event, enum log_type level,
+ const char *source_filename,
+ unsigned int source_linenum);
+#define event_want_log_level(_event, level) event_want_log_level((_event), (level), __FILE__, __LINE__)
+#define event_want_debug_log(_event) event_want_log_level((_event), LOG_TYPE_DEBUG)
+
+/* Returns TRUE if event should be processed (for logging or sending to stats).
+ The logging is checked with event_want_log_level() with the same caching
+ behavior. */
+bool event_want_level(struct event *event, enum log_type level,
+ const char *source_filename,
+ unsigned int source_linenum);
+#define event_want_level(_event, level) event_want_level((_event), (level), __FILE__, __LINE__)
+#define event_want_debug(_event) event_want_level((_event), LOG_TYPE_DEBUG)
+
+void event_log(struct event *event, const struct event_log_params *params,
+ const char *fmt, ...)
+ ATTR_FORMAT(3, 4);
+void event_logv(struct event *event, const struct event_log_params *params,
+ const char *fmt, va_list args)
+ ATTR_FORMAT(3, 0);
+
+/* If debugging is forced, the global debug log filter is ignored. Changing
+ this applies only to this event and any child event that is created
+ afterwards. It doesn't apply to existing child events (mainly for
+ performance reasons).
+
+ Note that event_set_forced_debug(event, FALSE) is a no-op. To disable
+ forced-debug, use event_unset_forced_debug(event). */
+struct event *event_set_forced_debug(struct event *event, bool force);
+/* Set the forced-debug to FALSE */
+struct event *event_unset_forced_debug(struct event *event);
+/* Set the global filter to logging debug events. */
+void event_set_global_debug_log_filter(struct event_filter *filter);
+/* Return the current global debug log event filter. */
+struct event_filter *event_get_global_debug_log_filter(void);
+/* Unset global debug log filter, if one exists. */
+void event_unset_global_debug_log_filter(void);
+
+/* Set the global filter to sending debug events. The debug events are also
+ sent if they match the global debug log filter. */
+void event_set_global_debug_send_filter(struct event_filter *filter);
+/* Return the current global debug send event filter. */
+struct event_filter *event_get_global_debug_send_filter(void);
+/* Unset global debug send filter, if one exists. */
+void event_unset_global_debug_send_filter(void);
+
+/* Set/replace the global core filter, which abort()s on matching events. */
+void event_set_global_core_log_filter(struct event_filter *filter);
+/* Return the current global core filter. */
+struct event_filter *event_get_global_core_log_filter(void);
+/* Unset the global core filter, if one exists. */
+void event_unset_global_core_log_filter(void);
+
+#endif
diff --git a/src/lib/execv-const.c b/src/lib/execv-const.c
new file mode 100644
index 0000000..d9bda82
--- /dev/null
+++ b/src/lib/execv-const.c
@@ -0,0 +1,33 @@
+/* Copyright (c) 2010-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "execv-const.h"
+
+#include <unistd.h>
+
+static char **argv_drop_const(const char *const argv[])
+{
+ char **ret;
+ unsigned int i, count;
+
+ for (count = 0; argv[count] != NULL; count++) ;
+
+ ret = t_new(char *, count + 1);
+ for (i = 0; i < count; i++)
+ ret[i] = t_strdup_noconst(argv[i]);
+ return ret;
+}
+
+void execv_const(const char *path, const char *const argv[])
+{
+ (void)execv(path, argv_drop_const(argv));
+ i_fatal_status(errno == ENOMEM ? FATAL_OUTOFMEM : FATAL_EXEC,
+ "execv(%s) failed: %m", path);
+}
+
+void execvp_const(const char *file, const char *const argv[])
+{
+ (void)execvp(file, argv_drop_const(argv));
+ i_fatal_status(errno == ENOMEM ? FATAL_OUTOFMEM : FATAL_EXEC,
+ "execvp(%s) failed: %m", file);
+}
diff --git a/src/lib/execv-const.h b/src/lib/execv-const.h
new file mode 100644
index 0000000..f112cec
--- /dev/null
+++ b/src/lib/execv-const.h
@@ -0,0 +1,9 @@
+#ifndef EXECV_CONST_H
+#define EXECV_CONST_H
+
+/* Just like execv() and execvp(), except argv points to const strings.
+ Also if calling execv*() fails, these functions call i_fatal(). */
+void execv_const(const char *path, const char *const argv[]) ATTR_NORETURN;
+void execvp_const(const char *file, const char *const argv[]) ATTR_NORETURN;
+
+#endif
diff --git a/src/lib/failures-private.h b/src/lib/failures-private.h
new file mode 100644
index 0000000..864247c
--- /dev/null
+++ b/src/lib/failures-private.h
@@ -0,0 +1,26 @@
+#ifndef FAILURES_PRIVATE_H
+#define FAILURES_PRIVATE_H
+
+typedef int
+failure_write_to_file_t(enum log_type type, string_t *data, size_t prefix_len);
+typedef string_t *
+failure_format_str_t(const struct failure_context *ctx, size_t *prefix_len_r,
+ const char *format, va_list args);
+typedef void failure_on_handler_failure_t(const struct failure_context *ctx);
+typedef void failure_post_handler_t(const struct failure_context *ctx);
+
+struct failure_handler_vfuncs {
+ failure_write_to_file_t *write;
+ failure_format_str_t *format;
+ failure_on_handler_failure_t *on_handler_failure;
+ failure_post_handler_t *post_handler;
+};
+
+struct failure_handler_config {
+ int fatal_err_reset;
+ struct failure_handler_vfuncs *v;
+};
+
+extern struct failure_handler_config failure_handler;
+
+#endif
diff --git a/src/lib/failures.c b/src/lib/failures.c
new file mode 100644
index 0000000..083e3b8
--- /dev/null
+++ b/src/lib/failures.c
@@ -0,0 +1,998 @@
+/* Copyright (c) 2002-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "ioloop.h"
+#include "str.h"
+#include "hostpid.h"
+#include "net.h"
+#include "process-title.h"
+#include "lib-signals.h"
+#include "backtrace-string.h"
+#include "printf-format-fix.h"
+#include "write-full.h"
+#include "time-util.h"
+#include "failures-private.h"
+
+#include <unistd.h>
+#include <fcntl.h>
+#include <syslog.h>
+#include <time.h>
+#include <poll.h>
+
+#define LOG_TYPE_FLAG_PREFIX_LEN 0x40
+#define LOG_TYPE_FLAG_DISABLE_LOG_PREFIX 0x80
+
+const char *failure_log_type_prefixes[LOG_TYPE_COUNT] = {
+ "Debug: ",
+ "Info: ",
+ "Warning: ",
+ "Error: ",
+ "Fatal: ",
+ "Panic: "
+};
+
+const char *failure_log_type_names[LOG_TYPE_COUNT] = {
+ "debug", "info", "warning", "error", "fatal", "panic"
+};
+
+static int log_fd_write(int fd, const unsigned char *data, size_t len);
+
+static void error_handler_real(const struct failure_context *ctx,
+ const char *format, va_list args);
+
+/* Initialize working defaults */
+static failure_callback_t *fatal_handler ATTR_NORETURN =
+ default_fatal_handler;
+static failure_callback_t *error_handler = default_error_handler;
+static failure_callback_t *info_handler = default_error_handler;
+static failure_callback_t *debug_handler = default_error_handler;
+static void (*failure_exit_callback)(int *) = NULL;
+
+static struct failure_context failure_ctx_debug = { .type = LOG_TYPE_DEBUG };
+static struct failure_context failure_ctx_info = { .type = LOG_TYPE_INFO };
+static struct failure_context failure_ctx_warning = { .type = LOG_TYPE_WARNING };
+static struct failure_context failure_ctx_error = { .type = LOG_TYPE_ERROR };
+
+static int log_fd = STDERR_FILENO, log_info_fd = STDERR_FILENO,
+ log_debug_fd = STDERR_FILENO;
+static char *log_prefix = NULL;
+static char *log_stamp_format = NULL, *log_stamp_format_suffix = NULL;
+static bool failure_ignore_errors = FALSE, log_prefix_sent = FALSE;
+static bool coredump_on_error = FALSE;
+static void log_timestamp_add(const struct failure_context *ctx, string_t *str);
+static void log_prefix_add(const struct failure_context *ctx, string_t *str);
+static int i_failure_send_option_forced(const char *key, const char *value);
+static int internal_send_split(string_t *full_str, size_t prefix_len);
+
+static string_t * ATTR_FORMAT(3, 0) default_format(const struct failure_context *ctx,
+ size_t *prefix_len_r ATTR_UNUSED,
+ const char *format,
+ va_list args)
+{
+ string_t *str = t_str_new(256);
+ log_timestamp_add(ctx, str);
+ log_prefix_add(ctx, str);
+
+ /* make sure there's no %n in there and fix %m */
+ str_vprintfa(str, printf_format_fix(format), args);
+ return str;
+}
+
+static int default_write(enum log_type type, string_t *data, size_t prefix_len ATTR_UNUSED)
+{
+ int fd;
+
+ switch (type) {
+ case LOG_TYPE_DEBUG:
+ fd = log_debug_fd;
+ break;
+ case LOG_TYPE_INFO:
+ fd = log_info_fd;
+ break;
+ default:
+ fd = log_fd;
+ break;
+ }
+ str_append_c(data, '\n');
+ return log_fd_write(fd, str_data(data), str_len(data));
+}
+
+static void default_on_handler_failure(const struct failure_context *ctx)
+{
+ const char *log_type = "info";
+ switch (ctx->type) {
+ case LOG_TYPE_DEBUG:
+ log_type = "debug";
+ /* fall through */
+ case LOG_TYPE_INFO:
+ /* we failed to log to info/debug log, try to log the
+ write error to error log - maybe that'll work. */
+ i_fatal_status(FATAL_LOGWRITE, "write() failed to %s log: %m",
+ log_type);
+ break;
+ default:
+ failure_exit(FATAL_LOGWRITE);
+ break;
+ }
+}
+
+static void default_post_handler(const struct failure_context *ctx)
+{
+ if (ctx->type == LOG_TYPE_ERROR && coredump_on_error)
+ abort();
+}
+
+static string_t * ATTR_FORMAT(3, 0) syslog_format(const struct failure_context *ctx,
+ size_t *prefix_len_r ATTR_UNUSED,
+ const char *format,
+ va_list args)
+{
+ string_t *str = t_str_new(128);
+ if (ctx->type == LOG_TYPE_INFO) {
+ if (ctx->log_prefix != NULL)
+ str_append(str, ctx->log_prefix);
+ else if (log_prefix != NULL)
+ str_append(str, log_prefix);
+ } else {
+ log_prefix_add(ctx, str);
+ }
+ str_vprintfa(str, format, args);
+ return str;
+}
+
+static int syslog_write(enum log_type type, string_t *data, size_t prefix_len ATTR_UNUSED)
+{
+ int level = LOG_ERR;
+
+ switch (type) {
+ case LOG_TYPE_DEBUG:
+ level = LOG_DEBUG;
+ break;
+ case LOG_TYPE_INFO:
+ level = LOG_INFO;
+ break;
+ case LOG_TYPE_WARNING:
+ level = LOG_WARNING;
+ break;
+ case LOG_TYPE_ERROR:
+ level = LOG_ERR;
+ break;
+ case LOG_TYPE_FATAL:
+ case LOG_TYPE_PANIC:
+ level = LOG_CRIT;
+ break;
+ case LOG_TYPE_COUNT:
+ case LOG_TYPE_OPTION:
+ i_unreached();
+ }
+ syslog(level, "%s", str_c(data));
+ return 0;
+}
+
+static void syslog_on_handler_failure(const struct failure_context *ctx ATTR_UNUSED)
+{
+ failure_exit(FATAL_LOGERROR);
+}
+
+static void syslog_post_handler(const struct failure_context *ctx ATTR_UNUSED)
+{
+}
+
+static string_t * ATTR_FORMAT(3, 0) internal_format(const struct failure_context *ctx,
+ size_t *prefix_len_r,
+ const char *format,
+ va_list args)
+{
+ string_t *str;
+ unsigned char log_type = ctx->type + 1;
+
+ if (ctx->log_prefix != NULL) {
+ log_type |= LOG_TYPE_FLAG_DISABLE_LOG_PREFIX;
+ if (ctx->log_prefix_type_pos != 0)
+ log_type |= LOG_TYPE_FLAG_PREFIX_LEN;
+ } else if (!log_prefix_sent && log_prefix != NULL) {
+ if (i_failure_send_option_forced("prefix", log_prefix) < 0) {
+ /* Failed to write log prefix. The log message writing
+ would likely fail as well, but don't even try since
+ the log prefix would be wrong. */
+ return NULL;
+ }
+ log_prefix_sent = TRUE;
+ }
+
+ str = t_str_new(128);
+ str_printfa(str, "\001%c%s ", log_type, my_pid);
+ if ((log_type & LOG_TYPE_FLAG_PREFIX_LEN) != 0)
+ str_printfa(str, "%u ", ctx->log_prefix_type_pos);
+ if (ctx->log_prefix != NULL)
+ str_append(str, ctx->log_prefix);
+ *prefix_len_r = str_len(str);
+
+ str_vprintfa(str, format, args);
+ return str;
+}
+
+static int internal_write(enum log_type type ATTR_UNUSED, string_t *data, size_t prefix_len)
+{
+ if (str_len(data)+1 <= PIPE_BUF) {
+ str_append_c(data, '\n');
+ return log_fd_write(STDERR_FILENO,
+ str_data(data), str_len(data));
+ }
+ return internal_send_split(data, prefix_len);
+}
+
+static void internal_on_handler_failure(const struct failure_context *ctx ATTR_UNUSED)
+{
+ failure_exit(FATAL_LOGERROR);
+}
+
+static void internal_post_handler(const struct failure_context *ctx ATTR_UNUSED)
+{
+}
+
+static struct failure_handler_vfuncs default_handler_vfuncs = {
+ .write = &default_write,
+ .format = &default_format,
+ .on_handler_failure = &default_on_handler_failure,
+ .post_handler = &default_post_handler
+};
+
+static struct failure_handler_vfuncs syslog_handler_vfuncs = {
+ .write = &syslog_write,
+ .format = &syslog_format,
+ .on_handler_failure = &syslog_on_handler_failure,
+ .post_handler = &syslog_post_handler
+};
+
+static struct failure_handler_vfuncs internal_handler_vfuncs = {
+ .write = &internal_write,
+ .format = &internal_format,
+ .on_handler_failure = &internal_on_handler_failure,
+ .post_handler = &internal_post_handler
+};
+
+struct failure_handler_config failure_handler = { .fatal_err_reset = FATAL_LOGWRITE,
+ .v = &default_handler_vfuncs };
+
+static int common_handler(const struct failure_context *ctx,
+ const char *format, va_list args)
+{
+ static int recursed = 0;
+ int ret;
+ size_t prefix_len = 0;
+
+ if (recursed >= 2) {
+ /* we're being called from some signal handler or we ran
+ out of memory */
+ return -1;
+ }
+ recursed++;
+
+ T_BEGIN {
+ string_t *str = failure_handler.v->format(ctx, &prefix_len, format, args);
+ ret = str == NULL ? -1 :
+ failure_handler.v->write(ctx->type, str, prefix_len);
+ } T_END;
+
+ if (ret < 0 && failure_ignore_errors)
+ ret = 0;
+
+ recursed--;
+ return ret;
+}
+
+static void error_handler_real(const struct failure_context *ctx,
+ const char *format, va_list args)
+{
+ if (common_handler(ctx, format, args) < 0)
+ failure_handler.v->on_handler_failure(ctx);
+ failure_handler.v->post_handler(ctx);
+}
+
+static void ATTR_FORMAT(2, 0)
+i_internal_error_handler(const struct failure_context *ctx,
+ const char *format, va_list args);
+
+/* kludgy .. we want to trust log_stamp_format with -Wformat-nonliteral */
+static const char *
+get_log_stamp_format(const char *format_arg, unsigned int timestamp_usecs)
+ ATTR_FORMAT_ARG(1);
+
+static const char *get_log_stamp_format(const char *format_arg ATTR_UNUSED,
+ unsigned int timestamp_usecs)
+{
+ if (log_stamp_format_suffix == NULL)
+ return log_stamp_format;
+ return t_strdup_printf("%s%06u%s", log_stamp_format,
+ timestamp_usecs, log_stamp_format_suffix);
+}
+
+void failure_exit(int status)
+{
+ static bool recursed = FALSE;
+
+ if (failure_exit_callback != NULL && !recursed) {
+ recursed = TRUE;
+ failure_exit_callback(&status);
+ }
+ lib_exit(status);
+}
+
+static void log_timestamp_add(const struct failure_context *ctx, string_t *str)
+{
+ const struct tm *tm = ctx->timestamp;
+ char buf[256];
+ struct timeval now;
+
+ if (log_stamp_format != NULL) {
+ if (tm == NULL) {
+ i_gettimeofday(&now);
+ tm = localtime(&now.tv_sec);
+ } else {
+ now.tv_usec = ctx->timestamp_usecs;
+ }
+
+ if (strftime(buf, sizeof(buf),
+ get_log_stamp_format("unused", now.tv_usec), tm) > 0)
+ str_append(str, buf);
+ }
+}
+
+static void log_prefix_add(const struct failure_context *ctx, string_t *str)
+{
+ if (ctx->log_prefix == NULL) {
+ /* use global log prefix */
+ if (log_prefix != NULL)
+ str_append(str, log_prefix);
+ str_append(str, failure_log_type_prefixes[ctx->type]);
+ } else if (ctx->log_prefix_type_pos == 0) {
+ str_append(str, ctx->log_prefix);
+ str_append(str, failure_log_type_prefixes[ctx->type]);
+ } else {
+ i_assert(ctx->log_prefix_type_pos <= strlen(ctx->log_prefix));
+ str_append_data(str, ctx->log_prefix, ctx->log_prefix_type_pos);
+ str_append(str, failure_log_type_prefixes[ctx->type]);
+ str_append(str, ctx->log_prefix + ctx->log_prefix_type_pos);
+ }
+}
+
+static void fd_wait_writable(int fd)
+{
+ struct pollfd pfd = {
+ .fd = fd,
+ .events = POLLOUT | POLLERR | POLLHUP | POLLNVAL,
+ };
+
+ /* Use poll() instead of ioloop, because we don't want to recurse back
+ to log writing in case something fails. */
+ if (poll(&pfd, 1, -1) < 0 && errno != EINTR) {
+ /* Unexpected error. We're already blocking on log writes,
+ so we can't log it. */
+ abort();
+ }
+}
+
+static int log_fd_write(int fd, const unsigned char *data, size_t len)
+{
+ ssize_t ret;
+ unsigned int prev_signal_term_counter = signal_term_counter;
+ unsigned int terminal_eintr_count = 0;
+ const char *old_title = NULL;
+ bool failed = FALSE, process_title_changed = FALSE;
+
+ while (!failed &&
+ (ret = write(fd, data, len)) != (ssize_t)len) {
+ if (ret > 0) {
+ /* some was written, continue.. */
+ data += ret;
+ len -= ret;
+ continue;
+ }
+ if (ret == 0) {
+ /* out of disk space? */
+ errno = ENOSPC;
+ failed = TRUE;
+ break;
+ }
+ switch (errno) {
+ case EAGAIN: {
+ /* Log fd is nonblocking - wait until we can write more.
+ Indicate in process title that the process is waiting
+ because it's waiting on the log.
+
+ Remember that the log fd is shared across processes,
+ which also means the log fd flags are shared. So if
+ one process changes the O_NONBLOCK flag for a log fd,
+ all the processes see the change. To avoid problems,
+ we'll wait using poll() instead of changing the
+ O_NONBLOCK flag. */
+ if (!process_title_changed) {
+ const char *title;
+
+ process_title_changed = TRUE;
+ old_title = t_strdup(process_title_get());
+ if (old_title == NULL)
+ title = "[blocking on log write]";
+ else
+ title = t_strdup_printf("%s - [blocking on log write]",
+ old_title);
+ process_title_set(title);
+ }
+ fd_wait_writable(fd);
+ break;
+ }
+ case EINTR:
+ if (prev_signal_term_counter == signal_term_counter) {
+ /* non-terminal signal. ignore. */
+ } else if (terminal_eintr_count++ == 0) {
+ /* we'd rather not die in the middle of
+ writing to log. try again once more */
+ } else {
+ /* received two terminal signals.
+ someone wants us dead. */
+ failed = TRUE;
+ break;
+ }
+ break;
+ default:
+ failed = TRUE;
+ break;
+ }
+ prev_signal_term_counter = signal_term_counter;
+ }
+ if (process_title_changed)
+ process_title_set(old_title);
+ return failed ? -1 : 0;
+}
+
+static void ATTR_NORETURN
+default_fatal_finish(enum log_type type, int status)
+{
+ const char *backtrace;
+ static int recursed = 0;
+
+ recursed++;
+ if ((type == LOG_TYPE_PANIC || status == FATAL_OUTOFMEM) &&
+ recursed == 1) {
+ if (backtrace_get(&backtrace) == 0)
+ i_error("Raw backtrace: %s", backtrace);
+ }
+ recursed--;
+
+ if (type == LOG_TYPE_PANIC || getenv("CORE_ERROR") != NULL ||
+ (status == FATAL_OUTOFMEM && getenv("CORE_OUTOFMEM") != NULL))
+ abort();
+ else
+ failure_exit(status);
+}
+
+static void ATTR_NORETURN fatal_handler_real(const struct failure_context *ctx,
+ const char *format, va_list args)
+{
+ int status = ctx->exit_status;
+ if (common_handler(ctx, format, args) < 0 &&
+ status == FATAL_DEFAULT)
+ status = failure_handler.fatal_err_reset;
+ default_fatal_finish(ctx->type, status);
+}
+
+void default_fatal_handler(const struct failure_context *ctx,
+ const char *format, va_list args)
+{
+ failure_handler.v = &default_handler_vfuncs;
+ failure_handler.fatal_err_reset = FATAL_LOGWRITE;
+ fatal_handler_real(ctx, format, args);
+}
+
+void default_error_handler(const struct failure_context *ctx,
+ const char *format, va_list args)
+{
+ failure_handler.v = &default_handler_vfuncs;
+ failure_handler.fatal_err_reset = FATAL_LOGWRITE;
+ error_handler_real(ctx, format, args);
+}
+
+void i_log_type(const struct failure_context *ctx, const char *format, ...)
+{
+ va_list args;
+
+ va_start(args, format);
+ i_log_typev(ctx, format, args);
+ va_end(args);
+}
+
+void i_log_typev(const struct failure_context *ctx, const char *format,
+ va_list args)
+{
+ switch (ctx->type) {
+ case LOG_TYPE_DEBUG:
+ debug_handler(ctx, format, args);
+ break;
+ case LOG_TYPE_INFO:
+ info_handler(ctx, format, args);
+ break;
+ default:
+ error_handler(ctx, format, args);
+ }
+}
+
+void i_panic(const char *format, ...)
+{
+ struct failure_context ctx;
+ va_list args;
+
+ lib_set_clean_exit(TRUE);
+ i_zero(&ctx);
+ ctx.type = LOG_TYPE_PANIC;
+
+ va_start(args, format);
+ fatal_handler(&ctx, format, args);
+ i_unreached();
+ /*va_end(args);*/
+}
+
+void i_fatal(const char *format, ...)
+{
+ struct failure_context ctx;
+ va_list args;
+
+ lib_set_clean_exit(TRUE);
+ i_zero(&ctx);
+ ctx.type = LOG_TYPE_FATAL;
+ ctx.exit_status = FATAL_DEFAULT;
+
+ va_start(args, format);
+ fatal_handler(&ctx, format, args);
+ i_unreached();
+ /*va_end(args);*/
+}
+
+void i_fatal_status(int status, const char *format, ...)
+{
+ struct failure_context ctx;
+ va_list args;
+
+ lib_set_clean_exit(TRUE);
+ i_zero(&ctx);
+ ctx.type = LOG_TYPE_FATAL;
+ ctx.exit_status = status;
+
+ va_start(args, format);
+ fatal_handler(&ctx, format, args);
+ i_unreached();
+ /*va_end(args);*/
+}
+
+void i_error(const char *format, ...)
+{
+ int old_errno = errno;
+ va_list args;
+
+ va_start(args, format);
+ error_handler(&failure_ctx_error, format, args);
+ va_end(args);
+
+ errno = old_errno;
+}
+
+void i_warning(const char *format, ...)
+{
+ int old_errno = errno;
+ va_list args;
+
+ va_start(args, format);
+ error_handler(&failure_ctx_warning, format, args);
+ va_end(args);
+
+ errno = old_errno;
+}
+
+void i_info(const char *format, ...)
+{
+ int old_errno = errno;
+ va_list args;
+
+ va_start(args, format);
+ info_handler(&failure_ctx_info, format, args);
+ va_end(args);
+
+ errno = old_errno;
+}
+
+void i_debug(const char *format, ...)
+{
+ int old_errno = errno;
+ va_list args;
+
+ va_start(args, format);
+ debug_handler(&failure_ctx_debug, format, args);
+ va_end(args);
+
+ errno = old_errno;
+}
+
+void i_set_fatal_handler(failure_callback_t *callback ATTR_NORETURN)
+{
+ fatal_handler = callback;
+}
+
+void i_set_error_handler(failure_callback_t *callback)
+{
+ coredump_on_error = getenv("CORE_ERROR") != NULL;
+ error_handler = callback;
+}
+
+void i_set_info_handler(failure_callback_t *callback)
+{
+ info_handler = callback;
+}
+
+void i_set_debug_handler(failure_callback_t *callback)
+{
+ debug_handler = callback;
+}
+
+void i_get_failure_handlers(failure_callback_t **fatal_callback_r,
+ failure_callback_t **error_callback_r,
+ failure_callback_t **info_callback_r,
+ failure_callback_t **debug_callback_r)
+{
+ *fatal_callback_r = fatal_handler;
+ *error_callback_r = error_handler;
+ *info_callback_r = info_handler;
+ *debug_callback_r = debug_handler;
+}
+
+void i_syslog_fatal_handler(const struct failure_context *ctx,
+ const char *format, va_list args)
+{
+ failure_handler.v = &syslog_handler_vfuncs;
+ failure_handler.fatal_err_reset = FATAL_LOGERROR;
+ fatal_handler_real(ctx, format, args);
+}
+
+void i_syslog_error_handler(const struct failure_context *ctx,
+ const char *format, va_list args)
+{
+ failure_handler.v = &syslog_handler_vfuncs;
+ failure_handler.fatal_err_reset = FATAL_LOGERROR;
+ error_handler_real(ctx, format, args);
+}
+
+void i_set_failure_syslog(const char *ident, int options, int facility)
+{
+ openlog(ident, options, facility);
+
+ i_set_fatal_handler(i_syslog_fatal_handler);
+ i_set_error_handler(i_syslog_error_handler);
+ i_set_info_handler(i_syslog_error_handler);
+ i_set_debug_handler(i_syslog_error_handler);
+}
+
+static void open_log_file(int *fd, const char *path)
+{
+ const char *str;
+
+ if (*fd != STDERR_FILENO) {
+ if (close(*fd) < 0) {
+ str = t_strdup_printf("close(%d) failed: %m\n", *fd);
+ (void)write_full(STDERR_FILENO, str, strlen(str));
+ }
+ }
+
+ if (path == NULL || strcmp(path, "/dev/stderr") == 0)
+ *fd = STDERR_FILENO;
+ else {
+ *fd = open(path, O_CREAT | O_APPEND | O_WRONLY, 0600);
+ if (*fd == -1) {
+ *fd = STDERR_FILENO;
+ str = t_strdup_printf("Can't open log file %s: %m\n",
+ path);
+ (void)write_full(STDERR_FILENO, str, strlen(str));
+ if (fd == &log_fd)
+ failure_exit(FATAL_LOGOPEN);
+ else
+ i_fatal_status(FATAL_LOGOPEN, "%s", str);
+ }
+ fd_close_on_exec(*fd, TRUE);
+ }
+}
+
+void i_set_failure_file(const char *path, const char *prefix)
+{
+ i_set_failure_prefix("%s", prefix);
+
+ if (log_info_fd != STDERR_FILENO && log_info_fd != log_fd) {
+ if (close(log_info_fd) < 0)
+ i_error("close(%d) failed: %m", log_info_fd);
+ }
+
+ if (log_debug_fd != STDERR_FILENO && log_debug_fd != log_info_fd &&
+ log_debug_fd != log_fd) {
+ if (close(log_debug_fd) < 0)
+ i_error("close(%d) failed: %m", log_debug_fd);
+ }
+
+ open_log_file(&log_fd, path);
+ /* if info/debug logs are elsewhere, i_set_info/debug_file()
+ overrides these later. */
+ log_info_fd = log_fd;
+ log_debug_fd = log_fd;
+
+ i_set_fatal_handler(default_fatal_handler);
+ i_set_error_handler(default_error_handler);
+ i_set_info_handler(default_error_handler);
+ i_set_debug_handler(default_error_handler);
+}
+
+static int i_failure_send_option_forced(const char *key, const char *value)
+{
+ const char *str;
+
+ str = t_strdup_printf("\001%c%s %s=%s\n", LOG_TYPE_OPTION+1,
+ my_pid, key, value);
+ return log_fd_write(STDERR_FILENO, (const unsigned char *)str,
+ strlen(str));
+}
+
+static void i_failure_send_option(const char *key, const char *value)
+{
+ if (error_handler == i_internal_error_handler)
+ (void)i_failure_send_option_forced(key, value);
+}
+
+void i_set_failure_prefix(const char *prefix_fmt, ...)
+{
+ va_list args;
+
+ va_start(args, prefix_fmt);
+ i_free(log_prefix);
+ log_prefix = i_strdup_vprintf(prefix_fmt, args);
+ va_end(args);
+
+ log_prefix_sent = FALSE;
+}
+
+void i_unset_failure_prefix(void)
+{
+ i_free(log_prefix);
+ log_prefix = i_strdup("");
+ log_prefix_sent = FALSE;
+}
+
+const char *i_get_failure_prefix(void)
+{
+ return log_prefix != NULL ? log_prefix : "";
+}
+
+static int internal_send_split(string_t *full_str, size_t prefix_len)
+{
+ /* This function splits the log line into PIPE_BUF sized blocks, so
+ the log process doesn't see partial lines. The log prefix is
+ repeated for each sent line. However, if the log prefix is
+ excessively long, we're still going to send the log lines even
+ if they are longer than PIPE_BUF. LINE_MIN_TEXT_LEN controls the
+ minimum number of bytes we're going to send of the actual log line
+ regardless of the log prefix length. (Alternative solution could be
+ to just forcibly split the line to PIPE_BUF length blocks without
+ repeating the log prefix for subsequent lines.) */
+#define LINE_MIN_TEXT_LEN 128
+#if LINE_MIN_TEXT_LEN >= PIPE_BUF
+# error LINE_MIN_TEXT_LEN too large
+#endif
+ string_t *str;
+ size_t max_text_len, pos = prefix_len;
+
+ str = t_str_new(PIPE_BUF);
+ str_append_data(str, str_data(full_str), prefix_len);
+ if (prefix_len < PIPE_BUF) {
+ max_text_len = I_MAX(PIPE_BUF - prefix_len - 1,
+ LINE_MIN_TEXT_LEN);
+ } else {
+ max_text_len = LINE_MIN_TEXT_LEN;
+ }
+
+ while (pos < str_len(full_str)) {
+ str_truncate(str, prefix_len);
+ str_append_max(str, str_c(full_str) + pos, max_text_len);
+ str_append_c(str, '\n');
+ if (log_fd_write(STDERR_FILENO,
+ str_data(str), str_len(str)) < 0)
+ return -1;
+ pos += max_text_len;
+ }
+ return 0;
+}
+
+
+static bool line_parse_prefix(const char *line, enum log_type *log_type_r,
+ bool *replace_prefix_r, bool *have_prefix_len_r)
+{
+ if (*line != 1)
+ return FALSE;
+
+ unsigned char log_type = (line[1] & 0x3f);
+ if (log_type == '\0') {
+ i_warning("Broken log line follows (type=NUL)");
+ return FALSE;
+ }
+ log_type--;
+
+ if (log_type > LOG_TYPE_OPTION) {
+ i_warning("Broken log line follows (type=%d)", log_type);
+ return FALSE;
+ }
+ *log_type_r = log_type;
+ *replace_prefix_r = (line[1] & LOG_TYPE_FLAG_DISABLE_LOG_PREFIX) != 0;
+ *have_prefix_len_r = (line[1] & LOG_TYPE_FLAG_PREFIX_LEN) != 0;
+ return TRUE;
+}
+
+void i_failure_parse_line(const char *line, struct failure_line *failure)
+{
+ bool have_prefix_len = FALSE;
+
+ i_zero(failure);
+ if (!line_parse_prefix(line, &failure->log_type,
+ &failure->disable_log_prefix,
+ &have_prefix_len)) {
+ failure->log_type = LOG_TYPE_ERROR;
+ failure->text = line;
+ return;
+ }
+
+ line += 2;
+ failure->text = line;
+ while (*line >= '0' && *line <= '9') {
+ failure->pid = failure->pid*10 + (*line - '0');
+ line++;
+ }
+ if (*line != ' ') {
+ /* some old protocol? */
+ failure->pid = 0;
+ return;
+ }
+ line++;
+
+ if (have_prefix_len) {
+ if (str_parse_uint(line, &failure->log_prefix_len, &line) < 0 ||
+ line[0] != ' ') {
+ /* unexpected, but ignore */
+ } else {
+ line++;
+ if (failure->log_prefix_len > strlen(line)) {
+ /* invalid */
+ failure->log_prefix_len = 0;
+ }
+ }
+ }
+ failure->text = line;
+}
+
+static void ATTR_NORETURN ATTR_FORMAT(2, 0)
+i_internal_fatal_handler(const struct failure_context *ctx,
+ const char *format, va_list args)
+{
+ failure_handler.v = &internal_handler_vfuncs;
+ failure_handler.fatal_err_reset = FATAL_LOGERROR;
+ fatal_handler_real(ctx, format, args);
+
+
+}
+
+static void
+i_internal_error_handler(const struct failure_context *ctx,
+ const char *format, va_list args)
+{
+ failure_handler.v = &internal_handler_vfuncs;
+ failure_handler.fatal_err_reset = FATAL_LOGERROR;
+ error_handler_real(ctx, format, args);
+}
+
+void i_set_failure_internal(void)
+{
+ fd_set_nonblock(STDERR_FILENO, TRUE);
+ i_set_fatal_handler(i_internal_fatal_handler);
+ i_set_error_handler(i_internal_error_handler);
+ i_set_info_handler(i_internal_error_handler);
+ i_set_debug_handler(i_internal_error_handler);
+}
+
+bool i_failure_handler_is_internal(failure_callback_t *const callback)
+{
+ return callback == i_internal_fatal_handler ||
+ callback == i_internal_error_handler;
+}
+
+void i_set_failure_ignore_errors(bool ignore)
+{
+ failure_ignore_errors = ignore;
+}
+
+void i_set_info_file(const char *path)
+{
+ if (log_info_fd == log_fd)
+ log_info_fd = STDERR_FILENO;
+
+ open_log_file(&log_info_fd, path);
+ info_handler = default_error_handler;
+ /* write debug-level messages to the info_log_path,
+ until i_set_debug_file() was called */
+ log_debug_fd = log_info_fd;
+ i_set_debug_handler(default_error_handler);
+}
+
+void i_set_debug_file(const char *path)
+{
+ if (log_debug_fd == log_fd || log_debug_fd == log_info_fd)
+ log_debug_fd = STDERR_FILENO;
+
+ open_log_file(&log_debug_fd, path);
+ debug_handler = default_error_handler;
+}
+
+void i_set_failure_timestamp_format(const char *fmt)
+{
+ const char *p;
+
+ i_free(log_stamp_format);
+ i_free_and_null(log_stamp_format_suffix);
+
+ p = strstr(fmt, "%{usecs}");
+ if (p == NULL)
+ log_stamp_format = i_strdup(fmt);
+ else {
+ log_stamp_format = i_strdup_until(fmt, p);
+ log_stamp_format_suffix = i_strdup(p + 8);
+ }
+}
+
+void i_set_failure_send_ip(const struct ip_addr *ip)
+{
+ i_failure_send_option("ip", net_ip2addr(ip));
+}
+
+void i_set_failure_send_prefix(const char *prefix)
+{
+ i_failure_send_option("prefix", prefix);
+}
+
+void i_set_failure_exit_callback(void (*callback)(int *status))
+{
+ failure_exit_callback = callback;
+}
+
+void failures_deinit(void)
+{
+ if (log_debug_fd == log_info_fd || log_debug_fd == log_fd)
+ log_debug_fd = STDERR_FILENO;
+
+ if (log_info_fd == log_fd)
+ log_info_fd = STDERR_FILENO;
+
+ if (log_fd != STDERR_FILENO) {
+ i_close_fd(&log_fd);
+ log_fd = STDERR_FILENO;
+ }
+
+ if (log_info_fd != STDERR_FILENO) {
+ i_close_fd(&log_info_fd);
+ log_info_fd = STDERR_FILENO;
+ }
+
+ if (log_debug_fd != STDERR_FILENO) {
+ i_close_fd(&log_debug_fd);
+ log_debug_fd = STDERR_FILENO;
+ }
+
+ i_free_and_null(log_prefix);
+ i_free_and_null(log_stamp_format);
+ i_free_and_null(log_stamp_format_suffix);
+}
+
+#undef i_unreached
+void i_unreached(const char *source_filename, int source_linenum)
+{
+ i_panic("file %s: line %d: unreached", source_filename, source_linenum);
+}
diff --git a/src/lib/failures.h b/src/lib/failures.h
new file mode 100644
index 0000000..afb4c6f
--- /dev/null
+++ b/src/lib/failures.h
@@ -0,0 +1,157 @@
+#ifndef FAILURES_H
+#define FAILURES_H
+
+struct ip_addr;
+
+/* Default exit status codes that we could use. */
+enum fatal_exit_status {
+ FATAL_LOGOPEN = 80, /* Can't open log file */
+ FATAL_LOGWRITE = 81, /* Can't write to log file */
+ FATAL_LOGERROR = 82, /* Internal logging error */
+ FATAL_OUTOFMEM = 83, /* Out of memory */
+ FATAL_EXEC = 84, /* exec() failed */
+
+ FATAL_DEFAULT = 89
+};
+
+enum log_type {
+ LOG_TYPE_DEBUG,
+ LOG_TYPE_INFO,
+ LOG_TYPE_WARNING,
+ LOG_TYPE_ERROR,
+ LOG_TYPE_FATAL,
+ LOG_TYPE_PANIC,
+
+ LOG_TYPE_COUNT,
+ /* special case */
+ LOG_TYPE_OPTION
+};
+
+struct failure_line {
+ pid_t pid;
+ enum log_type log_type;
+ /* If non-zero, the first log_prefix_len bytes in text indicate
+ the log prefix. This implies disable_log_prefix=TRUE. */
+ unsigned int log_prefix_len;
+ /* Disable the global log prefix. */
+ bool disable_log_prefix;
+ const char *text;
+};
+
+struct failure_context {
+ enum log_type type;
+ int exit_status; /* for LOG_TYPE_FATAL */
+ const struct tm *timestamp; /* NULL = use time() + localtime() */
+ unsigned int timestamp_usecs;
+ const char *log_prefix; /* override the default log prefix */
+ /* If non-0, insert the log type text (e.g. "Info: ") at this position
+ in the log_prefix instead of appending it. */
+ unsigned int log_prefix_type_pos;
+};
+
+#define DEFAULT_FAILURE_STAMP_FORMAT "%b %d %H:%M:%S "
+
+typedef void failure_callback_t(const struct failure_context *ctx,
+ const char *format, va_list args);
+
+extern const char *failure_log_type_prefixes[];
+extern const char *failure_log_type_names[];
+
+void i_log_type(const struct failure_context *ctx, const char *format, ...)
+ ATTR_FORMAT(2, 3);
+void i_log_typev(const struct failure_context *ctx, const char *format,
+ va_list args) ATTR_FORMAT(2, 0);
+
+void i_panic(const char *format, ...) ATTR_FORMAT(1, 2) ATTR_NORETURN ATTR_COLD;
+void i_unreached(const char *source_filename, int source_linenum)
+ ATTR_NORETURN ATTR_COLD;
+#define i_unreached() \
+ i_unreached(__FILE__, __LINE__)
+void i_fatal(const char *format, ...) ATTR_FORMAT(1, 2) ATTR_NORETURN ATTR_COLD;
+void i_error(const char *format, ...) ATTR_FORMAT(1, 2) ATTR_COLD;
+void i_warning(const char *format, ...) ATTR_FORMAT(1, 2);
+void i_info(const char *format, ...) ATTR_FORMAT(1, 2);
+void i_debug(const char *format, ...) ATTR_FORMAT(1, 2);
+
+void i_fatal_status(int status, const char *format, ...)
+ ATTR_FORMAT(2, 3) ATTR_NORETURN ATTR_COLD;
+
+/* Change failure handlers. */
+#ifndef __cplusplus
+void i_set_fatal_handler(failure_callback_t *callback ATTR_NORETURN);
+#else
+/* Older g++ doesn't like attributes in parameters */
+void i_set_fatal_handler(failure_callback_t *callback);
+#endif
+void i_set_error_handler(failure_callback_t *callback);
+void i_set_info_handler(failure_callback_t *callback);
+void i_set_debug_handler(failure_callback_t *callback);
+void i_get_failure_handlers(failure_callback_t **fatal_callback_r,
+ failure_callback_t **error_callback_r,
+ failure_callback_t **info_callback_r,
+ failure_callback_t **debug_callback_r);
+
+/* Send failures to file. */
+void default_fatal_handler(const struct failure_context *ctx,
+ const char *format, va_list args)
+ ATTR_NORETURN ATTR_FORMAT(2, 0);
+void default_error_handler(const struct failure_context *ctx,
+ const char *format, va_list args)
+ ATTR_FORMAT(2, 0);
+
+/* Send failures to syslog() */
+void i_syslog_fatal_handler(const struct failure_context *ctx,
+ const char *format, va_list args)
+ ATTR_NORETURN ATTR_FORMAT(2, 0);
+void i_syslog_error_handler(const struct failure_context *ctx,
+ const char *format, va_list args)
+ ATTR_FORMAT(2, 0);
+
+/* Open syslog and set failure/info/debug handlers to use it. */
+void i_set_failure_syslog(const char *ident, int options, int facility);
+
+/* Send failures to specified log file instead of stderr. */
+void i_set_failure_file(const char *path, const char *prefix);
+
+/* Send errors to stderr using internal error protocol. */
+void i_set_failure_internal(void);
+/* Returns TRUE if the given callback handler was set via
+ i_set_failure_internal(). */
+bool i_failure_handler_is_internal(failure_callback_t *const callback);
+/* If writing to log fails, ignore it instead of existing with
+ FATAL_LOGWRITE or FATAL_LOGERROR. */
+void i_set_failure_ignore_errors(bool ignore);
+
+/* Send informational messages to specified log file. i_set_failure_*()
+ functions modify the info file too, so call this function after them. */
+void i_set_info_file(const char *path);
+
+/* Send debug-level message to the given log file. The i_set_info_file()
+ function modifies also the debug log file, so call this function after it. */
+void i_set_debug_file(const char *path);
+
+/* Set the failure prefix. */
+void i_set_failure_prefix(const char *prefix_fmt, ...) ATTR_FORMAT(1, 2);
+/* Set prefix to "". */
+void i_unset_failure_prefix(void);
+/* Returns the current failure prefix (never NULL). */
+const char *i_get_failure_prefix(void);
+/* Prefix failures with a timestamp. fmt is in strftime() format. */
+void i_set_failure_timestamp_format(const char *fmt);
+/* When logging with internal error protocol, update the process's current
+ IP address / log prefix by sending it to log process. This is mainly used to
+ improve the error message if the process crashes. */
+void i_set_failure_send_ip(const struct ip_addr *ip);
+void i_set_failure_send_prefix(const char *prefix);
+
+/* Call the callback before exit()ing. The callback may update the status. */
+void i_set_failure_exit_callback(void (*callback)(int *status));
+/* Call the exit callback and exit() */
+void failure_exit(int status) ATTR_NORETURN ATTR_COLD;
+
+/* Parse a line logged using internal failure handler */
+void i_failure_parse_line(const char *line, struct failure_line *failure);
+
+void failures_deinit(void);
+
+#endif
diff --git a/src/lib/fd-util.c b/src/lib/fd-util.c
new file mode 100644
index 0000000..01315bc
--- /dev/null
+++ b/src/lib/fd-util.c
@@ -0,0 +1,166 @@
+/* Copyright (c) 1999-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "net.h"
+
+#include <unistd.h>
+#include <fcntl.h>
+#include <sys/stat.h>
+#include <sys/un.h>
+
+void fd_close_on_exec(int fd, bool set)
+{
+ int flags;
+
+ flags = fcntl(fd, F_GETFD, 0);
+ if (flags < 0)
+ i_fatal("fcntl(F_GETFD, %d) failed: %m", fd);
+
+ flags = set ? (flags | FD_CLOEXEC) : (flags & ~FD_CLOEXEC);
+ if (fcntl(fd, F_SETFD, flags) < 0)
+ i_fatal("fcntl(F_SETFD, %d) failed: %m", fd);
+}
+
+void fd_debug_verify_leaks(int first_fd, int last_fd)
+{
+ struct ip_addr addr, raddr;
+ in_port_t port, rport;
+ struct stat st;
+ int old_errno;
+ bool leaks = FALSE;
+
+ for (int fd = first_fd; fd <= last_fd; ++fd) {
+ if (fcntl(fd, F_GETFD, 0) == -1 && errno == EBADF)
+ continue;
+
+ old_errno = errno;
+
+ if (net_getsockname(fd, &addr, &port) == 0) {
+ if (addr.family == AF_UNIX) {
+ struct sockaddr_un sa;
+
+ socklen_t socklen = sizeof(sa);
+
+ if (getsockname(fd, (void *)&sa,
+ &socklen) < 0)
+ sa.sun_path[0] = '\0';
+
+ i_error("Leaked UNIX socket fd %d: %s",
+ fd, sa.sun_path);
+ leaks = TRUE;
+ continue;
+ }
+
+ if (net_getpeername(fd, &raddr, &rport) < 0) {
+ i_zero(&raddr);
+ rport = 0;
+ }
+ i_error("Leaked socket fd %d: %s:%u -> %s:%u",
+ fd, net_ip2addr(&addr), port,
+ net_ip2addr(&raddr), rport);
+ leaks = TRUE;
+ continue;
+ }
+
+ if (fstat(fd, &st) == 0) {
+#ifdef __APPLE__
+ /* OSX workaround: gettimeofday() calls shm_open()
+ internally and the fd won't get closed on exec.
+ We'll just skip all ino/dev=0 files and hope they
+ weren't anything else. */
+ if (st.st_ino == 0 && st.st_dev == 0)
+ continue;
+#endif
+#ifdef HAVE_SYS_SYSMACROS_H
+ i_error("Leaked file fd %d: dev %s.%s inode %s",
+ fd, dec2str(major(st.st_dev)),
+ dec2str(minor(st.st_dev)), dec2str(st.st_ino));
+ leaks = TRUE;
+ continue;
+#else
+ i_error("Leaked file fd %d: dev %s inode %s",
+ fd, dec2str(st.st_dev),
+ dec2str(st.st_ino));
+ leaks = TRUE;
+ continue;
+#endif
+ }
+
+ i_error("Leaked unknown fd %d (errno = %s)",
+ fd, strerror(old_errno));
+ leaks = TRUE;
+ continue;
+ }
+ if (leaks)
+ i_fatal("fd leak found");
+}
+
+void fd_set_nonblock(int fd, bool nonblock)
+{
+ int flags;
+
+ i_assert(fd > -1);
+
+ flags = fcntl(fd, F_GETFL, 0);
+ if (flags < 0)
+ i_fatal("fcntl(%d, F_GETFL) failed: %m", fd);
+
+ if (nonblock)
+ flags |= O_NONBLOCK;
+ else
+ flags &= ENUM_NEGATE(O_NONBLOCK);
+
+ if (fcntl(fd, F_SETFL, flags) < 0)
+ i_fatal("fcntl(%d, F_SETFL) failed: %m", fd);
+}
+
+void fd_close_maybe_stdio(int *fd_in, int *fd_out)
+{
+ int *fdp[2] = { fd_in, fd_out };
+
+ if (*fd_in == *fd_out)
+ *fd_in = -1;
+
+ for (unsigned int i = 0; i < N_ELEMENTS(fdp); i++) {
+ if (*fdp[i] == -1)
+ ;
+ else if (*fdp[i] > 1)
+ i_close_fd(fdp[i]);
+ else if (dup2(dev_null_fd, *fdp[i]) == *fdp[i])
+ *fdp[i] = -1;
+ else
+ i_fatal("dup2(/dev/null, %d) failed: %m", *fdp[i]);
+ }
+}
+
+#undef i_close_fd_path
+void i_close_fd_path(int *fd, const char *path, const char *arg,
+ const char *func, const char *file, int line)
+{
+ int saved_errno;
+
+ if (*fd == -1)
+ return;
+
+ if (unlikely(*fd <= 0)) {
+ i_panic("%s: close(%s%s%s) @ %s:%d attempted with fd=%d",
+ func, arg,
+ (path == NULL) ? "" : " = ",
+ (path == NULL) ? "" : path,
+ file, line, *fd);
+ }
+
+ saved_errno = errno;
+ /* Ignore ECONNRESET because we don't really care about it here,
+ as we are closing the socket down in any case. There might be
+ unsent data but nothing we can do about that. */
+ if (unlikely(close(*fd) < 0 && errno != ECONNRESET))
+ i_error("%s: close(%s%s%s) @ %s:%d failed (fd=%d): %m",
+ func, arg,
+ (path == NULL) ? "" : " = ",
+ (path == NULL) ? "" : path,
+ file, line, *fd);
+ errno = saved_errno;
+
+ *fd = -1;
+}
diff --git a/src/lib/fd-util.h b/src/lib/fd-util.h
new file mode 100644
index 0000000..54bdd63
--- /dev/null
+++ b/src/lib/fd-util.h
@@ -0,0 +1,26 @@
+#ifndef FD_UTIL_H
+#define FD_UTIL_H
+
+/* Change close-on-exec flag of fd. */
+void fd_close_on_exec(int fd, bool set);
+
+/* Verify that fds in given range don't exist. */
+void fd_debug_verify_leaks(int first_fd, int last_fd);
+
+/* Set file descriptor to blocking/nonblocking state */
+void fd_set_nonblock(int fd, bool nonblock);
+
+/* Close fd_in and fd_out, unless they're already -1. They can point to the
+ same fd, in which case they're closed only once. If they point to stdin
+ or stdout, they're replaced with /dev/null. */
+void fd_close_maybe_stdio(int *fd_in, int *fd_out);
+
+/* Close the fd and set it to -1. This assert-crashes if fd == 0, and is a
+ no-op if fd == -1. Normally fd == 0 would happen only if an uninitialized
+ fd is attempted to be closed, which is a bug. */
+void i_close_fd_path(int *fd, const char *path, const char *arg,
+ const char *func, const char *file, int line);
+#define i_close_fd_path(fd, path) i_close_fd_path((fd), (path), #fd, __func__, __FILE__, __LINE__)
+#define i_close_fd(fd) i_close_fd_path((fd), NULL)
+
+#endif
diff --git a/src/lib/fdatasync-path.c b/src/lib/fdatasync-path.c
new file mode 100644
index 0000000..769f483
--- /dev/null
+++ b/src/lib/fdatasync-path.c
@@ -0,0 +1,31 @@
+/* Copyright (c) 2008-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "fdatasync-path.h"
+
+#include <fcntl.h>
+#include <unistd.h>
+
+int fdatasync_path(const char *path)
+{
+ int fd, ret = 0;
+
+ /* Directories need to be opened as read-only.
+ fsync() doesn't appear to care about it. */
+ fd = open(path, O_RDONLY);
+ if (fd == -1)
+ return -1;
+ if (fdatasync(fd) < 0) {
+ /* Some OSes/FSes don't allow fsyncing directories. Silently
+ ignore the problem. */
+ if (errno == EBADF) {
+ /* e.g. NetBSD */
+ } else if (errno == EINVAL) {
+ /* e.g. Linux+CIFS */
+ } else {
+ ret = -1;
+ }
+ }
+ i_close_fd(&fd);
+ return ret;
+}
diff --git a/src/lib/fdatasync-path.h b/src/lib/fdatasync-path.h
new file mode 100644
index 0000000..1da6e6e
--- /dev/null
+++ b/src/lib/fdatasync-path.h
@@ -0,0 +1,7 @@
+#ifndef FDATASYNC_PATH_H
+#define FDATASYNC_PATH_H
+
+/* Open and fdatasync() the path. Works for files and directories. */
+int fdatasync_path(const char *path);
+
+#endif
diff --git a/src/lib/fdpass.c b/src/lib/fdpass.c
new file mode 100644
index 0000000..bd54443
--- /dev/null
+++ b/src/lib/fdpass.c
@@ -0,0 +1,212 @@
+/* Copyright (c) 2002-2018 Dovecot authors, see the included COPYING file */
+
+/*
+ fdpass.c - File descriptor passing between processes via UNIX sockets
+
+ This isn't fully portable, but pretty much all UNIXes nowadays should
+ support this. If you're having runtime problems with fd_read(), check the
+ end of fd_read() and play with the if condition. If you're having problems
+ with fd_send(), try defining BUGGY_CMSG_MACROS.
+
+ If this file doesn't compile at all, you should check if this is supported
+ in your system at all. It may require some extra #define to enable it.
+ If not, you're pretty much out of luck. Cygwin didn't last I checked.
+*/
+
+#define _XPG4_2
+
+#if defined(irix) || defined (__irix__) || defined(sgi) || defined (__sgi__)
+# define _XOPEN_SOURCE 4 /* for IRIX */
+#endif
+
+#if !defined(_AIX) && !defined(_XOPEN_SOURCE_EXTENDED)
+# define _XOPEN_SOURCE_EXTENDED /* for Tru64, breaks AIX */
+#endif
+
+#ifdef HAVE_CONFIG_H
+# include "lib.h"
+#else
+# define i_assert(x)
+#endif
+
+#include <string.h>
+#include <limits.h>
+#include <sys/types.h>
+
+#include <sys/socket.h>
+#include <sys/un.h>
+#include <sys/uio.h>
+
+#include "fdpass.h"
+
+#ifndef HAVE_CONFIG_H
+struct const_iovec {
+ const void *iov_base;
+ size_t iov_len;
+};
+#endif
+
+/* RFC 2292 defines CMSG_*() macros, but some operating systems don't have them
+ so we'll define our own if they don't exist.
+
+ CMSG_LEN(data) is used to calculate size of sizeof(struct cmsghdr) +
+ sizeof(data) and padding between them.
+
+ CMSG_SPACE(data) also calculates the padding needed after the data, in case
+ multiple objects are sent.
+
+ cmsghdr contains cmsg_len field and two integers. cmsg_len is sometimes
+ defined as sockaddr_t and sometimes size_t, so it can be either 32bit or
+ 64bit. This padding is added by compiler in sizeof(struct cmsghdr).
+
+ Padding required by CMSG_DATA() can vary. Usually it wants size_t or 32bit.
+ With Solaris it's in _CMSG_DATA_ALIGNMENT (32bit), we assume others want
+ size_t.
+
+ We don't really need CMSG_SPACE() to be exactly correct, because currently
+ we send only one object at a time. But anyway I'm trying to keep that
+ correct in case it's sometimes needed..
+*/
+
+#ifdef BUGGY_CMSG_MACROS
+/* Some OSes have broken CMSG macros in 64bit systems. The macros use 64bit
+ alignment while kernel uses 32bit alignment. */
+# undef CMSG_SPACE
+# undef CMSG_LEN
+# undef CMSG_DATA
+# define CMSG_DATA(cmsg) ((char *)((cmsg) + 1))
+# define _CMSG_DATA_ALIGNMENT 4
+# define _CMSG_HDR_ALIGNMENT 4
+#endif
+
+#ifndef CMSG_SPACE
+# define MY_ALIGN(len, align) \
+ (((len) + align - 1) & ~(align - 1))
+
+/* Alignment between cmsghdr and data */
+# ifndef _CMSG_DATA_ALIGNMENT
+# define _CMSG_DATA_ALIGNMENT sizeof(size_t)
+# endif
+/* Alignment between data and next cmsghdr */
+# ifndef _CMSG_HDR_ALIGNMENT
+# define _CMSG_HDR_ALIGNMENT sizeof(size_t)
+# endif
+
+# define CMSG_SPACE(len) \
+ (MY_ALIGN(sizeof(struct cmsghdr), _CMSG_DATA_ALIGNMENT) + \
+ MY_ALIGN(len, _CMSG_HDR_ALIGNMENT))
+# define CMSG_LEN(len) \
+ (MY_ALIGN(sizeof(struct cmsghdr), _CMSG_DATA_ALIGNMENT) + (len))
+#endif
+
+#ifdef SCM_RIGHTS
+
+ssize_t fd_send(int handle, int send_fd, const void *data, size_t size)
+{
+ struct msghdr msg;
+ struct const_iovec iov;
+ struct cmsghdr *cmsg;
+ char buf[CMSG_SPACE(sizeof(int))];
+
+ /* at least one byte is required to be sent with fd passing */
+ i_assert(size > 0 && size < INT_MAX);
+
+ memset(&msg, 0, sizeof(struct msghdr));
+
+ iov.iov_base = data;
+ iov.iov_len = size;
+
+ msg.msg_iov = (void *)&iov;
+ msg.msg_iovlen = 1;
+
+ if (send_fd != -1) {
+ /* set the control and controllen before CMSG_FIRSTHDR(). */
+ memset(buf, 0, sizeof(buf));
+ msg.msg_control = buf;
+ msg.msg_controllen = sizeof(buf);
+
+ cmsg = CMSG_FIRSTHDR(&msg);
+ cmsg->cmsg_level = SOL_SOCKET;
+ cmsg->cmsg_type = SCM_RIGHTS;
+ cmsg->cmsg_len = CMSG_LEN(sizeof(int));
+ memcpy(CMSG_DATA(cmsg), &send_fd, sizeof(send_fd));
+
+ /* set the real length we want to use. Do it after all is
+ set just in case CMSG macros required the extra padding
+ in the end. */
+ msg.msg_controllen = cmsg->cmsg_len;
+ }
+
+ return sendmsg(handle, &msg, 0);
+}
+
+#ifdef LINUX20
+/* Linux 2.0.x doesn't set any cmsg fields. Note that this might make some
+ attacks possible so don't do it unless you really have to. */
+# define CHECK_CMSG(cmsg) ((cmsg) != NULL)
+#else
+# define CHECK_CMSG(cmsg) \
+ ((cmsg) != NULL && \
+ (size_t)(cmsg)->cmsg_len >= (size_t)CMSG_LEN(sizeof(int)) && \
+ (cmsg)->cmsg_level == SOL_SOCKET && (cmsg)->cmsg_type == SCM_RIGHTS)
+#endif
+
+ssize_t fd_read(int handle, void *data, size_t size, int *fd)
+{
+ struct msghdr msg;
+ struct iovec iov;
+ struct cmsghdr *cmsg;
+ ssize_t ret;
+ char buf[CMSG_SPACE(sizeof(int))];
+
+ i_assert(size > 0 && size < INT_MAX);
+
+ memset(&msg, 0, sizeof (struct msghdr));
+
+ iov.iov_base = data;
+ iov.iov_len = size;
+
+ msg.msg_iov = &iov;
+ msg.msg_iovlen = 1;
+
+ memset(buf, 0, sizeof(buf));
+ msg.msg_control = buf;
+ msg.msg_controllen = sizeof(buf);
+
+ ret = recvmsg(handle, &msg, 0);
+ if (ret <= 0) {
+ *fd = -1;
+ return ret;
+ }
+
+ /* at least one byte transferred - we should have the fd now.
+ do extra checks to make sure it really is an fd that is being
+ transferred to avoid potential DoS conditions. some systems don't
+ set all these values correctly however so CHECK_CMSG() is somewhat
+ system dependent */
+ cmsg = CMSG_FIRSTHDR(&msg);
+ if (!CHECK_CMSG(cmsg))
+ *fd = -1;
+ else
+ memcpy(fd, CMSG_DATA(cmsg), sizeof(*fd));
+ return ret;
+}
+
+#else
+# ifdef __GNUC__
+# warning SCM_RIGHTS not supported, privilege separation not possible
+# endif
+ssize_t fd_send(int handle ATTR_UNUSED, int send_fd ATTR_UNUSED,
+ const void *data ATTR_UNUSED, size_t size ATTR_UNUSED)
+{
+ errno = ENOSYS;
+ return -1;
+}
+
+ssize_t fd_read(int handle ATTR_UNUSED, void *data ATTR_UNUSED,
+ size_t size ATTR_UNUSED, int *fd ATTR_UNUSED)
+{
+ errno = ENOSYS;
+ return -1;
+}
+#endif
diff --git a/src/lib/fdpass.h b/src/lib/fdpass.h
new file mode 100644
index 0000000..c5d42af
--- /dev/null
+++ b/src/lib/fdpass.h
@@ -0,0 +1,15 @@
+#ifndef FDPASS_H
+#define FDPASS_H
+
+/* Send data and send_fd (unless it's -1) via sendmsg(). Returns number of
+ bytes sent, or -1 on error. If at least 1 byte was sent, the send_fd was
+ also sent. */
+ssize_t fd_send(int handle, int send_fd, const void *data, size_t size);
+
+/* Receive data and fd via recvmsg(). Returns number of bytes read, 0 on
+ disconnection, or -1 on error. If at least 1 byte was read, the fd is also
+ returned (if it had been sent). If there was no fd received, it's set to
+ -1. See test-istream-unix.c for different test cases. */
+ssize_t fd_read(int handle, void *data, size_t size, int *fd_r);
+
+#endif
diff --git a/src/lib/file-cache.c b/src/lib/file-cache.c
new file mode 100644
index 0000000..008021e
--- /dev/null
+++ b/src/lib/file-cache.c
@@ -0,0 +1,336 @@
+/* Copyright (c) 2004-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "buffer.h"
+#include "mmap-util.h"
+#include "file-cache.h"
+
+#include <sys/stat.h>
+
+struct file_cache {
+ int fd;
+ char *path;
+ buffer_t *page_bitmask;
+
+ void *mmap_base;
+ size_t mmap_length;
+ size_t read_highwater;
+};
+
+struct file_cache *file_cache_new(int fd)
+{
+ return file_cache_new_path(fd, "");
+}
+
+struct file_cache *file_cache_new_path(int fd, const char *path)
+{
+ struct file_cache *cache;
+
+ cache = i_new(struct file_cache, 1);
+ cache->fd = fd;
+ cache->path = i_strdup(path);
+ cache->page_bitmask = buffer_create_dynamic(default_pool, 128);
+ return cache;
+}
+
+void file_cache_free(struct file_cache **_cache)
+{
+ struct file_cache *cache = *_cache;
+
+ *_cache = NULL;
+
+ if (cache->mmap_base != NULL) {
+ if (munmap_anon(cache->mmap_base, cache->mmap_length) < 0)
+ i_error("munmap_anon(%s) failed: %m", cache->path);
+ }
+ buffer_free(&cache->page_bitmask);
+ i_free(cache->path);
+ i_free(cache);
+}
+
+void file_cache_set_fd(struct file_cache *cache, int fd)
+{
+ cache->fd = fd;
+ file_cache_invalidate(cache, 0, cache->mmap_length);
+}
+
+int file_cache_set_size(struct file_cache *cache, uoff_t size)
+{
+ size_t page_size = mmap_get_page_size();
+ uoff_t diff;
+ void *new_base;
+
+ i_assert(page_size > 0);
+
+ diff = size % page_size;
+ if (diff != 0)
+ size += page_size - diff;
+
+ i_assert((size % page_size) == 0);
+ if (size <= cache->mmap_length)
+ return 0;
+
+ if (size > SIZE_MAX) {
+ i_error("file_cache_set_size(%s, %"PRIuUOFF_T"): size too large",
+ cache->path, size);
+ return -1;
+ }
+
+ /* grow mmaping */
+ if (cache->mmap_base == NULL) {
+ cache->mmap_base = mmap_anon(size);
+ if (cache->mmap_base == MAP_FAILED) {
+ i_error("mmap_anon(%s, %"PRIuUOFF_T") failed: %m",
+ cache->path, size);
+ cache->mmap_base = NULL;
+ cache->mmap_length = 0;
+ return -1;
+ }
+ } else {
+ new_base = mremap_anon(cache->mmap_base, cache->mmap_length,
+ size, MREMAP_MAYMOVE);
+ if (new_base == MAP_FAILED) {
+ i_error("mremap_anon(%s, %"PRIuUOFF_T") failed: %m",
+ cache->path, size);
+ return -1;
+ }
+
+ cache->mmap_base = new_base;
+ }
+ cache->mmap_length = size;
+ return 0;
+}
+
+ssize_t file_cache_read(struct file_cache *cache, uoff_t offset, size_t size)
+{
+ size_t page_size = mmap_get_page_size();
+ size_t poffset, psize, dest_offset, dest_size;
+ unsigned char *bits, *dest;
+ ssize_t ret;
+
+ i_assert(page_size > 0);
+
+ if (size > SSIZE_T_MAX) {
+ /* make sure our calculations won't overflow. most likely
+ we'll be reading less data, but allow it anyway so caller
+ doesn't have to deal with any extra checks. */
+ size = SSIZE_T_MAX;
+ }
+ if (offset >= UOFF_T_MAX - size)
+ size = UOFF_T_MAX - offset;
+
+ if (offset + size > cache->mmap_length &&
+ offset + size - cache->mmap_length > 1024*1024) {
+ /* growing more than a megabyte, make sure that the
+ file is large enough so we don't allocate memory
+ more than needed */
+ struct stat st;
+
+ if (fstat(cache->fd, &st) < 0) {
+ if (errno != ESTALE)
+ i_error("fstat(%s) failed: %m", cache->path);
+ return -1;
+ }
+
+ if (offset + size > (uoff_t)st.st_size) {
+ if (offset >= (uoff_t)st.st_size)
+ return 0;
+ size = (uoff_t)st.st_size - offset;
+ }
+ }
+
+ if (file_cache_set_size(cache, offset + size) < 0)
+ return -1;
+
+ poffset = offset / page_size;
+ psize = (offset + size + page_size-1) / page_size - poffset;
+ i_assert(psize > 0);
+
+ bits = buffer_get_space_unsafe(cache->page_bitmask, 0,
+ (poffset + psize + CHAR_BIT - 1) /
+ CHAR_BIT);
+
+ dest_offset = poffset * page_size;
+ dest = PTR_OFFSET(cache->mmap_base, dest_offset);
+ dest_size = page_size;
+
+ while (psize > 0) {
+ if ((bits[poffset / CHAR_BIT] & (1 << (poffset % CHAR_BIT))) != 0) {
+ /* page is already in cache */
+ dest_offset += page_size;
+ if (dest_offset <= cache->read_highwater) {
+ psize--; poffset++;
+ dest += page_size;
+ continue;
+ }
+
+ /* this is the last partially cached block.
+ use the caching only if we don't want to
+ read past read_highwater */
+ if (offset + size <= cache->read_highwater) {
+ i_assert(psize == 1);
+ break;
+ }
+
+ /* mark the block noncached again and
+ read it */
+ bits[poffset / CHAR_BIT] &=
+ ~(1 << (poffset % CHAR_BIT));
+ dest_offset -= page_size;
+ }
+
+ ret = pread(cache->fd, dest, dest_size, dest_offset);
+ if (ret <= 0) {
+ if (ret < 0)
+ return -1;
+
+ /* EOF. mark the last block as cached even if it
+ isn't completely. read_highwater tells us how far
+ we've actually made. */
+ if (dest_offset == cache->read_highwater) {
+ i_assert(poffset ==
+ cache->read_highwater / page_size);
+ bits[poffset / CHAR_BIT] |=
+ 1 << (poffset % CHAR_BIT);
+ }
+ return dest_offset <= offset ? 0 :
+ dest_offset - offset < size ?
+ dest_offset - offset : size;
+ }
+
+ dest += ret;
+ dest_offset += ret;
+
+ if (cache->read_highwater < dest_offset) {
+ unsigned int high_poffset =
+ cache->read_highwater / page_size;
+
+ /* read_highwater needs to be updated. if we didn't
+ just read that block, we can't trust anymore that
+ we have it cached */
+ bits[high_poffset / CHAR_BIT] &=
+ ~(1 << (high_poffset % CHAR_BIT));
+ cache->read_highwater = dest_offset;
+ }
+
+ if ((size_t)ret != dest_size) {
+ /* partial read - probably EOF but make sure. */
+ dest_size -= ret;
+ continue;
+ }
+
+ bits[poffset / CHAR_BIT] |= 1 << (poffset % CHAR_BIT);
+ dest_size = page_size;
+ psize--; poffset++;
+ }
+
+ return size;
+}
+
+const void *file_cache_get_map(struct file_cache *cache, size_t *size_r)
+{
+ *size_r = cache->read_highwater;
+ return cache->mmap_base;
+}
+
+void file_cache_write(struct file_cache *cache, const void *data, size_t size,
+ uoff_t offset)
+{
+ size_t page_size = mmap_get_page_size();
+ unsigned char *bits;
+ unsigned int first_page, last_page;
+
+ i_assert(page_size > 0);
+ i_assert(UOFF_T_MAX - offset > size);
+
+ if (file_cache_set_size(cache, offset + size) < 0) {
+ /* couldn't grow mapping. just make sure the written memory
+ area is invalidated then. */
+ file_cache_invalidate(cache, offset, size);
+ return;
+ }
+
+ memcpy(PTR_OFFSET(cache->mmap_base, offset), data, size);
+
+ if (cache->read_highwater < offset + size) {
+ unsigned int page = cache->read_highwater / page_size;
+
+ bits = buffer_get_space_unsafe(cache->page_bitmask,
+ page / CHAR_BIT, 1);
+ *bits &= ~(1 << (page % CHAR_BIT));
+ cache->read_highwater = offset + size;
+ }
+
+ /* mark fully written pages cached */
+ if (size >= page_size) {
+ first_page = offset / page_size;
+ last_page = (offset + size) / page_size;
+ if ((offset % page_size) != 0)
+ first_page++;
+
+ bits = buffer_get_space_unsafe(cache->page_bitmask, 0,
+ last_page / CHAR_BIT + 1);
+ for (; first_page < last_page; first_page++) {
+ bits[first_page / CHAR_BIT] |=
+ 1 << (first_page % CHAR_BIT);
+ }
+ }
+}
+
+void file_cache_invalidate(struct file_cache *cache, uoff_t offset, uoff_t size)
+{
+ size_t page_size = mmap_get_page_size();
+ unsigned char *bits, mask;
+ unsigned int i;
+
+ if (offset >= cache->read_highwater || size == 0)
+ return;
+
+ i_assert(page_size > 0);
+
+ if (size > cache->read_highwater - offset) {
+ /* ignore anything after read highwater */
+ size = cache->read_highwater - offset;
+ }
+ if (size >= cache->read_highwater) {
+ /* we're invalidating everything up to read highwater.
+ drop the highwater position. */
+ cache->read_highwater = offset & ~(page_size-1);
+ }
+
+ size = (offset + size + page_size-1) / page_size;
+ offset /= page_size;
+ i_assert(size > offset);
+ size -= offset;
+
+ if (size != 1) {
+ /* tell operating system that we don't need the memory anymore
+ and it may free it. don't bother to do it for single pages,
+ there's a good chance that they get re-read back
+ immediately. */
+ (void)madvise(PTR_OFFSET(cache->mmap_base, offset * page_size),
+ size * page_size, MADV_DONTNEED);
+ }
+
+ bits = buffer_get_space_unsafe(cache->page_bitmask, offset / CHAR_BIT,
+ 1 + (size + CHAR_BIT - 1) / CHAR_BIT);
+
+ /* set the first byte */
+ for (i = offset % CHAR_BIT, mask = 0; i < CHAR_BIT && size > 0; i++) {
+ mask |= 1 << i;
+ size--;
+ }
+ *bits++ &= ~mask;
+
+ /* set the middle bytes */
+ memset(bits, 0, size / CHAR_BIT);
+ bits += size / CHAR_BIT;
+ size %= CHAR_BIT;
+
+ /* set the last byte */
+ if (size > 0) {
+ for (i = 0, mask = 0; i < size; i++)
+ mask |= 1 << i;
+ *bits &= ~mask;
+ }
+}
diff --git a/src/lib/file-cache.h b/src/lib/file-cache.h
new file mode 100644
index 0000000..afffbd4
--- /dev/null
+++ b/src/lib/file-cache.h
@@ -0,0 +1,37 @@
+#ifndef FILE_CACHE_H
+#define FILE_CACHE_H
+
+/* Create a new file cache. It works very much like file-backed mmap()ed
+ memory, but it works more nicely with remote filesystems (no SIGBUS). */
+struct file_cache *file_cache_new(int fd);
+struct file_cache *file_cache_new_path(int fd, const char *path);
+/* Destroy the cache and set cache pointer to NULL. */
+void file_cache_free(struct file_cache **cache);
+
+/* Change cached file descriptor. Invalidates the whole cache. */
+void file_cache_set_fd(struct file_cache *cache, int fd);
+
+/* Change the memory allocated for the cache. This can be used to immediately
+ set the maximum size so there's no need to grow the memory area with
+ possibly slow copying. */
+int file_cache_set_size(struct file_cache *cache, uoff_t size);
+
+/* Read data from file, returns how many bytes was actually read or -1 if
+ error occurred. */
+ssize_t file_cache_read(struct file_cache *cache, uoff_t offset, size_t size);
+
+/* Returns pointer to beginning of cached file. Only parts of the returned
+ memory that are valid are the ones that have been file_cache_read().
+ Note that the pointer may become invalid after calling file_cache_read(). */
+const void *file_cache_get_map(struct file_cache *cache, size_t *size_r);
+
+/* Update cached memory area. Mark fully written pages as cached. */
+void file_cache_write(struct file_cache *cache, const void *data, size_t size,
+ uoff_t offset);
+
+/* Invalidate cached memory area. It will be read again next time it's tried
+ to be accessed. */
+void file_cache_invalidate(struct file_cache *cache,
+ uoff_t offset, uoff_t size);
+
+#endif
diff --git a/src/lib/file-copy.c b/src/lib/file-copy.c
new file mode 100644
index 0000000..c9b8e3e
--- /dev/null
+++ b/src/lib/file-copy.c
@@ -0,0 +1,125 @@
+/* Copyright (c) 2006-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "istream.h"
+#include "ostream.h"
+#include "file-copy.h"
+
+#include <stdio.h>
+#include <unistd.h>
+#include <fcntl.h>
+#include <sys/stat.h>
+
+static int file_copy_to_tmp(const char *srcpath, const char *tmppath,
+ bool try_hardlink)
+{
+ struct istream *input;
+ struct ostream *output;
+ struct stat st;
+ mode_t old_umask;
+ int fd_in, fd_out;
+ int ret = -1;
+
+ if (try_hardlink) {
+ /* see if hardlinking works */
+ if (link(srcpath, tmppath) == 0)
+ return 1;
+ if (errno == EEXIST) {
+ if (i_unlink_if_exists(tmppath) < 0)
+ return -1;
+ if (link(srcpath, tmppath) == 0)
+ return 1;
+ }
+ if (errno == ENOENT)
+ return 0;
+ if (!ECANTLINK(errno)) {
+ i_error("link(%s, %s) failed: %m", srcpath, tmppath);
+ return -1;
+ }
+
+ /* fallback to manual copying */
+ }
+
+ fd_in = open(srcpath, O_RDONLY);
+ if (fd_in == -1) {
+ if (errno == ENOENT)
+ return 0;
+ i_error("open(%s) failed: %m", srcpath);
+ return -1;
+ }
+
+ if (fstat(fd_in, &st) < 0) {
+ i_error("fstat(%s) failed: %m", srcpath);
+ i_close_fd(&fd_in);
+ return -1;
+ }
+
+ old_umask = umask(0);
+ fd_out = open(tmppath, O_WRONLY | O_CREAT | O_TRUNC, st.st_mode);
+ umask(old_umask);
+ if (fd_out == -1) {
+ i_error("open(%s, O_CREAT) failed: %m", tmppath);
+ i_close_fd(&fd_in);
+ return -1;
+ }
+
+ /* try to change the group, don't really care if it fails */
+ if (fchown(fd_out, (uid_t)-1, st.st_gid) < 0 && errno != EPERM)
+ i_error("fchown(%s) failed: %m", tmppath);
+
+ input = i_stream_create_fd(fd_in, IO_BLOCK_SIZE);
+ output = o_stream_create_fd_file(fd_out, 0, FALSE);
+
+ switch (o_stream_send_istream(output, input)) {
+ case OSTREAM_SEND_ISTREAM_RESULT_FINISHED:
+ ret = 0;
+ break;
+ case OSTREAM_SEND_ISTREAM_RESULT_WAIT_INPUT:
+ case OSTREAM_SEND_ISTREAM_RESULT_WAIT_OUTPUT:
+ i_unreached();
+ case OSTREAM_SEND_ISTREAM_RESULT_ERROR_INPUT:
+ i_error("read(%s) failed: %s", srcpath,
+ i_stream_get_error(input));
+ break;
+ case OSTREAM_SEND_ISTREAM_RESULT_ERROR_OUTPUT:
+ i_error("write(%s) failed: %s", tmppath,
+ o_stream_get_error(output));
+ break;
+ }
+
+ i_stream_destroy(&input);
+ o_stream_destroy(&output);
+
+ if (close(fd_in) < 0) {
+ i_error("close(%s) failed: %m", srcpath);
+ ret = -1;
+ }
+ if (close(fd_out) < 0) {
+ i_error("close(%s) failed: %m", tmppath);
+ ret = -1;
+ }
+ return ret < 0 ? -1 : 1;
+}
+
+int file_copy(const char *srcpath, const char *destpath, bool try_hardlink)
+{
+ int ret;
+
+ T_BEGIN {
+ const char *tmppath;
+
+ tmppath = t_strconcat(destpath, ".tmp", NULL);
+
+ ret = file_copy_to_tmp(srcpath, tmppath, try_hardlink);
+ if (ret > 0) {
+ if (rename(tmppath, destpath) < 0) {
+ i_error("rename(%s, %s) failed: %m",
+ tmppath, destpath);
+ ret = -1;
+ }
+ }
+ if (ret < 0)
+ i_unlink(tmppath);
+ } T_END;
+ return ret;
+}
diff --git a/src/lib/file-copy.h b/src/lib/file-copy.h
new file mode 100644
index 0000000..f70b941
--- /dev/null
+++ b/src/lib/file-copy.h
@@ -0,0 +1,12 @@
+#ifndef FILE_COPY_H
+#define FILE_COPY_H
+
+/* Copy file atomically. First try hardlinking, then fallback to creating
+ a temporary file (destpath.tmp) and rename()ing it over srcpath.
+ If the destination file already exists, it may or may not be overwritten,
+ so that shouldn't be relied on.
+
+ Returns -1 = error, 0 = source file not found, 1 = ok */
+int file_copy(const char *srcpath, const char *destpath, bool try_hardlink);
+
+#endif
diff --git a/src/lib/file-create-locked.c b/src/lib/file-create-locked.c
new file mode 100644
index 0000000..47fe7f9
--- /dev/null
+++ b/src/lib/file-create-locked.c
@@ -0,0 +1,193 @@
+/* Copyright (c) 2015-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "str.h"
+#include "safe-mkstemp.h"
+#include "mkdir-parents.h"
+#include "file-lock.h"
+#include "file-create-locked.h"
+
+#include <unistd.h>
+#include <fcntl.h>
+#include <sys/stat.h>
+
+/* Try mkdir() + lock creation multiple times. This allows the lock file
+ creation to work even while the directory is simultaneously being
+ rmdir()ed. */
+#define MAX_MKDIR_COUNT 10
+#define MAX_RETRY_COUNT 1000
+
+static int
+try_lock_existing(int fd, const char *path,
+ const struct file_create_settings *set,
+ struct file_lock **lock_r, const char **error_r)
+{
+ struct file_lock_settings lock_set = set->lock_settings;
+ struct stat st1, st2;
+ int ret;
+
+ lock_set.unlink_on_free = FALSE;
+ lock_set.close_on_free = FALSE;
+
+ if (fstat(fd, &st1) < 0) {
+ *error_r = t_strdup_printf("fstat(%s) failed: %m", path);
+ return -1;
+ }
+ if (file_wait_lock(fd, path, F_WRLCK, &lock_set, set->lock_timeout_secs,
+ lock_r, error_r) <= 0)
+ return -1;
+ if (stat(path, &st2) == 0) {
+ ret = st1.st_ino == st2.st_ino &&
+ CMP_DEV_T(st1.st_dev, st2.st_dev) ? 1 : 0;
+ } else if (errno == ENOENT) {
+ ret = 0;
+ } else {
+ *error_r = t_strdup_printf("stat(%s) failed: %m", path);
+ ret = -1;
+ }
+ if (ret <= 0) {
+ /* the fd is closed next - no need to unlock */
+ file_lock_free(lock_r);
+ } else {
+ file_lock_set_unlink_on_free(
+ *lock_r, set->lock_settings.unlink_on_free);
+ file_lock_set_close_on_free(
+ *lock_r, set->lock_settings.close_on_free);
+ }
+ return ret;
+}
+
+static int
+try_mkdir(const char *path, const struct file_create_settings *set,
+ const char **error_r)
+{
+ uid_t uid = set->mkdir_uid != 0 ? set->mkdir_uid : (uid_t)-1;
+ gid_t gid = set->mkdir_gid != 0 ? set->mkdir_gid : (gid_t)-1;
+ const char *p = strrchr(path, '/');
+ if (p == NULL)
+ return 0;
+
+ const char *dir = t_strdup_until(path, p);
+ int ret;
+ if (uid != (uid_t)-1)
+ ret = mkdir_parents_chown(dir, set->mkdir_mode, uid, gid);
+ else {
+ ret = mkdir_parents_chgrp(dir, set->mkdir_mode,
+ gid, set->gid_origin);
+ }
+ if (ret < 0 && errno != EEXIST) {
+ *error_r = t_strdup_printf("mkdir_parents(%s) failed: %m", dir);
+ return -1;
+ }
+ return 1;
+}
+
+static int
+try_create_new(const char *path, const struct file_create_settings *set,
+ int *fd_r, struct file_lock **lock_r, const char **error_r)
+{
+ string_t *temp_path = t_str_new(128);
+ int fd, orig_errno, ret = 1;
+ int mode = set->mode != 0 ? set->mode : 0600;
+ uid_t uid = set->uid != 0 ? set->uid : (uid_t)-1;
+ uid_t gid = set->gid != 0 ? set->gid : (gid_t)-1;
+
+ str_append(temp_path, path);
+ for (unsigned int i = 0; ret > 0; i++) {
+ if (uid != (uid_t)-1)
+ fd = safe_mkstemp(temp_path, mode, uid, gid);
+ else
+ fd = safe_mkstemp_group(temp_path, mode, gid, set->gid_origin);
+ if (fd != -1 || errno != ENOENT || set->mkdir_mode == 0 ||
+ i >= MAX_MKDIR_COUNT)
+ break;
+
+ int orig_errno = errno;
+ if ((ret = try_mkdir(path, set, error_r)) < 0)
+ return -1;
+ errno = orig_errno;
+ }
+ if (fd == -1) {
+ *error_r = t_strdup_printf("safe_mkstemp(%s) failed: %m", path);
+ return -1;
+ }
+
+ struct file_lock_settings lock_set = set->lock_settings;
+ lock_set.unlink_on_free = FALSE;
+ lock_set.close_on_free = FALSE;
+
+ ret = -1;
+ if (file_try_lock(fd, str_c(temp_path), F_WRLCK, &lock_set,
+ lock_r, error_r) <= 0) {
+ } else if (link(str_c(temp_path), path) < 0) {
+ if (errno == EEXIST) {
+ /* just created by somebody else */
+ ret = 0;
+ } else if (errno == ENOENT) {
+ /* nobody should be deleting the temp file unless the
+ entire directory is deleted. */
+ *error_r = t_strdup_printf(
+ "Temporary file %s was unexpectedly deleted",
+ str_c(temp_path));
+ } else {
+ *error_r = t_strdup_printf("link(%s, %s) failed: %m",
+ str_c(temp_path), path);
+ }
+ file_lock_free(lock_r);
+ } else {
+ file_lock_set_path(*lock_r, path);
+ file_lock_set_unlink_on_free(
+ *lock_r, set->lock_settings.unlink_on_free);
+ file_lock_set_close_on_free(
+ *lock_r, set->lock_settings.close_on_free);
+ i_unlink_if_exists(str_c(temp_path));
+ *fd_r = fd;
+ return 1;
+ }
+ orig_errno = errno;
+ i_close_fd(&fd);
+ i_unlink_if_exists(str_c(temp_path));
+ errno = orig_errno;
+ return ret;
+}
+
+int file_create_locked(const char *path, const struct file_create_settings *set,
+ struct file_lock **lock_r, bool *created_r,
+ const char **error_r)
+{
+ unsigned int i;
+ int fd, ret;
+
+ for (i = 0; i < MAX_RETRY_COUNT; i++) {
+ fd = open(path, O_RDWR);
+ if (fd != -1) {
+ ret = try_lock_existing(fd, path, set, lock_r, error_r);
+ if (ret > 0) {
+ /* successfully locked an existing file */
+ *created_r = FALSE;
+ return fd;
+ }
+ i_close_fd(&fd);
+ if (ret < 0)
+ return -1;
+ } else if (errno != ENOENT) {
+ *error_r = t_strdup_printf("open(%s) failed: %m", path);
+ return -1;
+ } else {
+ /* try to create the file */
+ ret = try_create_new(path, set, &fd, lock_r, error_r);
+ if (ret < 0)
+ return -1;
+ if (ret > 0) {
+ /* successfully created a new locked file */
+ *created_r = TRUE;
+ return fd;
+ }
+ /* the file was just created - try again opening and
+ locking it */
+ }
+ }
+ *error_r = t_strdup_printf("Creating a locked file %s keeps failing", path);
+ errno = EINVAL;
+ return -1;
+}
diff --git a/src/lib/file-create-locked.h b/src/lib/file-create-locked.h
new file mode 100644
index 0000000..b88e3c8
--- /dev/null
+++ b/src/lib/file-create-locked.h
@@ -0,0 +1,47 @@
+#ifndef FILE_CREATE_LOCKED_H
+#define FILE_CREATE_LOCKED_H
+
+#include "file-lock.h"
+
+struct file_create_settings {
+ /* 0 = try locking without waiting */
+ unsigned int lock_timeout_secs;
+
+ struct file_lock_settings lock_settings;
+
+ /* 0 = 0600 */
+ int mode;
+ /* 0 = default */
+ uid_t uid;
+ /* 0 = default */
+ gid_t gid;
+ const char *gid_origin;
+
+ /* If parent directory doesn't exist, mkdir() it with this mode.
+ 0 = don't mkdir(). The parent directories are assumed to be
+ potentially rmdir() simultaneously, so the mkdir()+locking may be
+ attempted multiple times. */
+ int mkdir_mode;
+ /* 0 = default */
+ uid_t mkdir_uid;
+ /* 0 = default */
+ gid_t mkdir_gid;
+ const char *mkdir_gid_origin;
+};
+
+/* Either open an existing file and lock it, or create the file locked.
+ The creation is done by creating a temp file and link()ing it to path.
+ If link() fails, opening is retried again. Returns fd on success,
+ -1 on error. errno is preserved for the last failed syscall, so most
+ importantly ENOENT could mean that the directory doesn't exist and EAGAIN
+ means locking timed out.
+
+ If this function is used to create lock files, file_lock_set_unlink_on_free()
+ should be used for the resulting lock. It attempts to avoid unlinking the
+ file if there are already other processes using the lock. That can help to
+ avoid "Creating a locked file ... keeps failing" errors */
+int file_create_locked(const char *path, const struct file_create_settings *set,
+ struct file_lock **lock_r, bool *created_r,
+ const char **error_r);
+
+#endif
diff --git a/src/lib/file-dotlock.c b/src/lib/file-dotlock.c
new file mode 100644
index 0000000..ad14ef0
--- /dev/null
+++ b/src/lib/file-dotlock.c
@@ -0,0 +1,925 @@
+/* Copyright (c) 2003-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "ioloop.h"
+#include "str.h"
+#include "hex-binary.h"
+#include "hostpid.h"
+#include "file-lock.h"
+#include "eacces-error.h"
+#include "write-full.h"
+#include "safe-mkstemp.h"
+#include "nfs-workarounds.h"
+#include "file-dotlock.h"
+#include "sleep.h"
+
+#include <stdio.h>
+#include <signal.h>
+#include <time.h>
+#include <utime.h>
+#include <sys/stat.h>
+
+#define DEFAULT_LOCK_SUFFIX ".lock"
+
+/* 0.1 .. 0.2msec */
+#define LOCK_RANDOM_USLEEP_TIME (100000 + (unsigned int)i_rand() % 100000)
+/* Maximum 3 second wait between dotlock checks */
+#define LOCK_MAX_WAIT_USECS (1000000 * 3)
+
+/* If the dotlock is newer than this, don't verify that the PID it contains
+ is valid (since it most likely is). */
+#define STALE_PID_CHECK_SECS 2
+
+/* Maximum difference between current time and create file's ctime before
+ logging a warning. Should be less than a second in normal operation. */
+#define MAX_TIME_DIFF 30
+/* NFS may return a cached mtime in stat(). A later non-cached stat() may
+ return a slightly different mtime. Allow the difference to be this much
+ and still consider it to be the same mtime. */
+#define FILE_DOTLOCK_MAX_STAT_MTIME_DIFF 1
+
+struct dotlock {
+ struct dotlock_settings settings;
+
+ dev_t dev;
+ ino_t ino;
+ time_t mtime;
+
+ char *path;
+ char *lock_path;
+ int fd;
+
+ time_t lock_time;
+};
+
+struct file_change_info {
+ dev_t dev;
+ ino_t ino;
+ off_t size;
+ time_t ctime, mtime;
+};
+
+struct lock_info {
+ const struct dotlock_settings *set;
+ const char *path, *lock_path, *temp_path;
+ int fd;
+
+ struct file_change_info lock_info;
+ struct file_change_info file_info;
+
+ time_t last_pid_check;
+ time_t last_change;
+ unsigned int wait_usecs;
+
+ bool have_pid:1;
+ bool pid_read:1;
+ bool use_io_notify:1;
+ bool lock_stated:1;
+};
+
+static struct dotlock *
+file_dotlock_alloc(const struct dotlock_settings *settings, const char *path)
+{
+ struct dotlock *dotlock;
+
+ dotlock = i_new(struct dotlock, 1);
+ dotlock->settings = *settings;
+ if (dotlock->settings.lock_suffix == NULL)
+ dotlock->settings.lock_suffix = DEFAULT_LOCK_SUFFIX;
+ dotlock->path = i_strdup(path);
+ dotlock->fd = -1;
+
+ return dotlock;
+}
+
+static pid_t read_local_pid(const char *lock_path)
+{
+ char buf[512], *host;
+ int fd;
+ ssize_t ret;
+ pid_t pid;
+
+ fd = open(lock_path, O_RDONLY);
+ if (fd == -1)
+ return -1; /* ignore the actual error */
+
+ /* read line */
+ ret = read(fd, buf, sizeof(buf)-1);
+ i_close_fd(&fd);
+ if (ret <= 0)
+ return -1;
+
+ /* fix the string */
+ if (buf[ret-1] == '\n')
+ ret--;
+ buf[ret] = '\0';
+
+ /* it should contain pid:host */
+ host = strchr(buf, ':');
+ if (host == NULL)
+ return -1;
+ *host++ = '\0';
+
+ /* host must be ours */
+ if (strcmp(host, my_hostname) != 0)
+ return -1;
+
+ if (str_to_pid(buf, &pid) < 0)
+ return -1;
+ if (pid <= 0)
+ return -1;
+ return pid;
+}
+
+static bool
+update_change_info(const struct stat *st, struct file_change_info *change,
+ time_t *last_change_r, time_t now, bool check_ctime)
+{
+ /* ctime is checked only if we're not doing NFS attribute cache
+ flushes. it changes them. */
+ if (change->ino != st->st_ino || !CMP_DEV_T(change->dev, st->st_dev) ||
+ (change->ctime != st->st_ctime && check_ctime) ||
+ change->mtime != st->st_mtime || change->size != st->st_size) {
+ time_t change_time = now;
+
+ if (change->ctime == 0) {
+ /* First check, set last_change to file's change time.
+ Use mtime instead if it's higher, but only if it's
+ not higher than current time, because the mtime
+ can also be used for keeping metadata. */
+ change_time = st->st_mtime <= now &&
+ (st->st_mtime > st->st_ctime || !check_ctime) ?
+ st->st_mtime : st->st_ctime;
+ }
+ if (*last_change_r < change_time)
+ *last_change_r = change_time;
+ change->ino = st->st_ino;
+ change->dev = st->st_dev;
+ change->ctime = st->st_ctime;
+ change->mtime = st->st_mtime;
+ change->size = st->st_size;
+ return TRUE;
+ }
+ return FALSE;
+}
+
+static int update_lock_info(time_t now, struct lock_info *lock_info,
+ bool *changed_r)
+{
+ struct stat st;
+
+ /* don't waste time flushing attribute cache the first time we're here.
+ if it's stale we'll get back here soon. */
+ if (lock_info->set->nfs_flush && lock_info->lock_stated) {
+ nfs_flush_file_handle_cache(lock_info->lock_path);
+ nfs_flush_attr_cache_unlocked(lock_info->lock_path);
+ }
+
+ lock_info->lock_stated = TRUE;
+ if (nfs_safe_lstat(lock_info->lock_path, &st) < 0) {
+ if (errno != ENOENT) {
+ i_error("lstat(%s) failed: %m", lock_info->lock_path);
+ return -1;
+ }
+ return 1;
+ }
+
+ *changed_r = update_change_info(&st, &lock_info->lock_info,
+ &lock_info->last_change, now,
+ !lock_info->set->nfs_flush);
+ return 0;
+}
+
+static int dotlock_override(struct lock_info *lock_info)
+{
+ if (i_unlink_if_exists(lock_info->lock_path) < 0)
+ return -1;
+
+ /* make sure we sleep for a while after overriding the lock file.
+ otherwise another process might try to override it at the same time
+ and unlink our newly created dotlock. */
+ if (lock_info->use_io_notify)
+ i_sleep_usecs(LOCK_RANDOM_USLEEP_TIME);
+ return 0;
+}
+
+static int check_lock(time_t now, struct lock_info *lock_info)
+{
+ time_t stale_timeout = lock_info->set->stale_timeout;
+ pid_t pid = -1;
+ bool changed;
+ int ret;
+
+ if ((ret = update_lock_info(now, lock_info, &changed)) != 0)
+ return ret;
+ if (changed || !lock_info->pid_read) {
+ /* either our first check or someone else got the lock file.
+ if the dotlock was created only a couple of seconds ago,
+ don't bother to read its PID. */
+ if (lock_info->lock_info.mtime >= now - STALE_PID_CHECK_SECS)
+ lock_info->pid_read = FALSE;
+ else {
+ pid = read_local_pid(lock_info->lock_path);
+ lock_info->pid_read = TRUE;
+ }
+ lock_info->have_pid = pid != -1;
+ } else if (!lock_info->have_pid) {
+ /* no pid checking */
+ } else {
+ if (lock_info->last_pid_check == now) {
+ /* we just checked the pid */
+ return 0;
+ }
+
+ /* re-read the pid. even if all times and inodes are the same,
+ the PID in the file might have changed if lock files were
+ rapidly being recreated. */
+ pid = read_local_pid(lock_info->lock_path);
+ lock_info->have_pid = pid != -1;
+ }
+
+ if (lock_info->have_pid) {
+ /* we've local PID. Check if it exists. */
+ if (kill(pid, 0) == 0 || errno != ESRCH) {
+ if (pid != getpid()) {
+ /* process exists, don't override */
+ return 0;
+ }
+ /* it's us. either we're locking it again, or it's a
+ stale lock file with same pid than us. either way,
+ recreate it.. */
+ }
+
+ /* doesn't exist - now check again if the dotlock was just
+ deleted or replaced */
+ if ((ret = update_lock_info(now, lock_info, &changed)) != 0)
+ return ret;
+
+ if (!changed) {
+ /* still there, go ahead and override it */
+ return dotlock_override(lock_info);
+ }
+ return 1;
+ }
+
+ if (stale_timeout == 0) {
+ /* no change checking */
+ return 0;
+ }
+
+ if (now > lock_info->last_change + stale_timeout) {
+ struct stat st;
+
+ /* possibly stale lock file. check also the timestamp of the
+ file we're protecting. */
+ if (lock_info->set->nfs_flush) {
+ nfs_flush_file_handle_cache(lock_info->path);
+ nfs_flush_attr_cache_maybe_locked(lock_info->path);
+ }
+ if (nfs_safe_stat(lock_info->path, &st) < 0) {
+ if (errno == ENOENT) {
+ /* file doesn't exist. treat it as if
+ it hasn't changed */
+ } else {
+ i_error("stat(%s) failed: %m", lock_info->path);
+ return -1;
+ }
+ } else {
+ (void)update_change_info(&st, &lock_info->file_info,
+ &lock_info->last_change, now,
+ !lock_info->set->nfs_flush);
+ }
+ }
+
+ if (now > lock_info->last_change + stale_timeout) {
+ /* no changes for a while, assume stale lock */
+ return dotlock_override(lock_info);
+ }
+
+ return 0;
+}
+
+static int file_write_pid(int fd, const char *path, bool nfs_flush)
+{
+ const char *str;
+
+ /* write our pid and host, if possible */
+ str = t_strdup_printf("%s:%s", my_pid, my_hostname);
+ if (write_full(fd, str, strlen(str)) < 0 ||
+ (nfs_flush && fdatasync(fd) < 0)) {
+ /* failed, leave it empty then */
+ if (ftruncate(fd, 0) < 0) {
+ i_error("ftruncate(%s) failed: %m", path);
+ return -1;
+ }
+ }
+ return 0;
+}
+
+static int try_create_lock_hardlink(struct lock_info *lock_info, bool write_pid,
+ string_t *tmp_path, time_t now)
+{
+ const char *temp_prefix = lock_info->set->temp_prefix;
+ const char *p;
+ mode_t old_mask;
+ struct stat st;
+
+ if (lock_info->temp_path == NULL) {
+ /* we'll need our temp file first. */
+ i_assert(lock_info->fd == -1);
+
+ p = strrchr(lock_info->lock_path, '/');
+
+ str_truncate(tmp_path, 0);
+ if (temp_prefix != NULL) {
+ if (*temp_prefix != '/' && p != NULL) {
+ /* add directory */
+ str_append_data(tmp_path, lock_info->lock_path,
+ p - lock_info->lock_path);
+ str_append_c(tmp_path, '/');
+ }
+ str_append(tmp_path, temp_prefix);
+ } else {
+ if (p != NULL) {
+ /* add directory */
+ str_append_data(tmp_path, lock_info->lock_path,
+ p - lock_info->lock_path);
+ str_append_c(tmp_path, '/');
+ }
+ str_printfa(tmp_path, ".temp.%s.%s.",
+ my_hostname, my_pid);
+ }
+
+ old_mask = umask(0666);
+ lock_info->fd = safe_mkstemp(tmp_path, 0666 ^ old_mask,
+ (uid_t)-1, (gid_t)-1);
+ umask(old_mask);
+ if (lock_info->fd == -1)
+ return -1;
+
+ if (write_pid) {
+ if (file_write_pid(lock_info->fd,
+ str_c(tmp_path),
+ lock_info->set->nfs_flush) < 0) {
+ i_close_fd(&lock_info->fd);
+ return -1;
+ }
+ }
+
+ lock_info->temp_path = str_c(tmp_path);
+ } else if (fstat(lock_info->fd, &st) < 0) {
+ i_error("fstat(%s) failed: %m", lock_info->temp_path);
+ return -1;
+ } else if (st.st_ctime < now) {
+ /* we've been waiting for a while.
+ refresh the file's timestamp. */
+ if (utime(lock_info->temp_path, NULL) < 0)
+ i_error("utime(%s) failed: %m", lock_info->temp_path);
+ }
+
+ if (nfs_safe_link(lock_info->temp_path,
+ lock_info->lock_path, TRUE) < 0) {
+ if (errno == EEXIST)
+ return 0;
+
+ if (errno != EACCES) {
+ i_error("link(%s, %s) failed: %m",
+ lock_info->temp_path, lock_info->lock_path);
+ }
+ return -1;
+ }
+
+ if (i_unlink(lock_info->temp_path) < 0) {
+ /* non-fatal, continue */
+ }
+ lock_info->temp_path = NULL;
+ return 1;
+}
+
+static int try_create_lock_excl(struct lock_info *lock_info, bool write_pid)
+{
+ int fd;
+
+ fd = open(lock_info->lock_path, O_RDWR | O_EXCL | O_CREAT, 0666);
+ if (fd == -1) {
+ if (errno == EEXIST)
+ return 0;
+
+ if (errno != ENOENT && errno != EACCES)
+ i_error("open(%s) failed: %m", lock_info->lock_path);
+ return -1;
+ }
+
+ if (write_pid) {
+ if (file_write_pid(fd, lock_info->lock_path,
+ lock_info->set->nfs_flush) < 0) {
+ i_close_fd(&fd);
+ return -1;
+ }
+ }
+
+ lock_info->fd = fd;
+ return 1;
+}
+
+static void dotlock_wait_end(struct ioloop *ioloop)
+{
+ io_loop_stop(ioloop);
+}
+
+static void dotlock_wait(struct lock_info *lock_info)
+{
+ struct ioloop *ioloop;
+ struct io *io;
+ struct timeout *to;
+
+ if (!lock_info->use_io_notify) {
+ i_sleep_usecs(lock_info->wait_usecs);
+ return;
+ }
+
+ ioloop = io_loop_create();
+ switch (io_add_notify(lock_info->lock_path, dotlock_wait_end,
+ ioloop, &io)) {
+ case IO_NOTIFY_ADDED:
+ break;
+ case IO_NOTIFY_NOTFOUND:
+ /* the lock file doesn't exist anymore, don't sleep */
+ io_loop_destroy(&ioloop);
+ return;
+ case IO_NOTIFY_NOSUPPORT:
+ /* listening for files not supported */
+ io_loop_destroy(&ioloop);
+ lock_info->use_io_notify = FALSE;
+ i_sleep_usecs(LOCK_RANDOM_USLEEP_TIME);
+ return;
+ }
+ /* timeout after a random time even when using notify, since it
+ doesn't work reliably with e.g. NFS. */
+ to = timeout_add(lock_info->wait_usecs/1000,
+ dotlock_wait_end, ioloop);
+ io_loop_run(ioloop);
+ io_remove(&io);
+ timeout_remove(&to);
+ io_loop_destroy(&ioloop);
+}
+
+static int
+dotlock_create(struct dotlock *dotlock, enum dotlock_create_flags flags,
+ bool write_pid, const char **lock_path_r)
+{
+ const struct dotlock_settings *set = &dotlock->settings;
+ const char *lock_path;
+ struct lock_info lock_info;
+ struct stat st;
+ unsigned int stale_notify_threshold;
+ unsigned int change_secs, wait_left;
+ time_t now, max_wait_time, last_notify;
+ time_t prev_last_change = 0, prev_wait_update = 0;
+ string_t *tmp_path;
+ int ret;
+ bool do_wait;
+
+ now = time(NULL);
+
+ lock_path = *lock_path_r =
+ t_strconcat(dotlock->path, set->lock_suffix, NULL);
+ stale_notify_threshold = set->stale_timeout / 2;
+ max_wait_time = (flags & DOTLOCK_CREATE_FLAG_NONBLOCK) != 0 ? 0 :
+ now + set->timeout;
+ tmp_path = t_str_new(256);
+
+ i_zero(&lock_info);
+ lock_info.path = dotlock->path;
+ lock_info.set = set;
+ lock_info.lock_path = lock_path;
+ lock_info.fd = -1;
+ lock_info.use_io_notify = set->use_io_notify;
+
+ last_notify = 0; do_wait = FALSE;
+
+ file_lock_wait_start();
+ do {
+ if (do_wait) {
+ if (prev_last_change != lock_info.last_change) {
+ /* dotlock changed since last check,
+ reset the wait time */
+ lock_info.wait_usecs = LOCK_RANDOM_USLEEP_TIME;
+ prev_last_change = lock_info.last_change;
+ prev_wait_update = now;
+ } else if (prev_wait_update != now &&
+ lock_info.wait_usecs < LOCK_MAX_WAIT_USECS) {
+ /* we've been waiting for a while now, increase
+ the wait time to avoid wasting CPU */
+ prev_wait_update = now;
+ lock_info.wait_usecs += lock_info.wait_usecs/2;
+ }
+ dotlock_wait(&lock_info);
+ now = time(NULL);
+ }
+
+ ret = check_lock(now, &lock_info);
+ if (ret < 0)
+ break;
+
+ if (ret == 1) {
+ if ((flags & DOTLOCK_CREATE_FLAG_CHECKONLY) != 0)
+ break;
+
+ ret = set->use_excl_lock ?
+ try_create_lock_excl(&lock_info, write_pid) :
+ try_create_lock_hardlink(&lock_info, write_pid,
+ tmp_path, now);
+ if (ret != 0) {
+ /* if we succeeded, get the current time once
+ more in case disk I/O usage was really high
+ and it took a long time to create the lock */
+ now = time(NULL);
+ break;
+ }
+ }
+
+ if (last_notify != now && set->callback != NULL &&
+ now < max_wait_time) {
+ last_notify = now;
+ change_secs = now - lock_info.last_change;
+ wait_left = max_wait_time - now;
+
+ if (change_secs >= stale_notify_threshold &&
+ change_secs <= wait_left) {
+ unsigned int secs_left =
+ set->stale_timeout < change_secs ?
+ 0 : set->stale_timeout - change_secs;
+ if (!set->callback(secs_left, TRUE,
+ set->context)) {
+ /* we don't want to override */
+ lock_info.last_change = now;
+ }
+ } else if (wait_left > 0) {
+ (void)set->callback(wait_left, FALSE,
+ set->context);
+ }
+ }
+
+ do_wait = TRUE;
+ now = time(NULL);
+ } while (now < max_wait_time);
+ file_lock_wait_end(dotlock->path);
+
+ if (ret > 0) {
+ i_assert(lock_info.fd != -1);
+ if (fstat(lock_info.fd, &st) < 0) {
+ i_error("fstat(%s) failed: %m", lock_path);
+ ret = -1;
+ } else {
+ /* successful dotlock creation */
+ dotlock->dev = st.st_dev;
+ dotlock->ino = st.st_ino;
+
+ dotlock->fd = lock_info.fd;
+ dotlock->lock_time = now;
+ lock_info.fd = -1;
+
+ if (st.st_ctime + MAX_TIME_DIFF < now ||
+ st.st_ctime - MAX_TIME_DIFF > now) {
+ i_warning("Created dotlock file's timestamp is "
+ "different than current time "
+ "(%s vs %s): %s", dec2str(st.st_ctime),
+ dec2str(now), dotlock->path);
+ }
+ }
+ }
+
+ if (lock_info.fd != -1) {
+ int old_errno = errno;
+
+ if (close(lock_info.fd) < 0)
+ i_error("close(%s) failed: %m", lock_path);
+ errno = old_errno;
+ }
+ if (lock_info.temp_path != NULL)
+ i_unlink(lock_info.temp_path);
+
+ if (ret == 0)
+ errno = EAGAIN;
+ return ret;
+}
+
+static void file_dotlock_free(struct dotlock **_dotlock)
+{
+ struct dotlock *dotlock = *_dotlock;
+ int old_errno;
+
+ *_dotlock = NULL;
+
+ if (dotlock->fd != -1) {
+ old_errno = errno;
+ if (close(dotlock->fd) < 0)
+ i_error("close(%s) failed: %m", dotlock->path);
+ dotlock->fd = -1;
+ errno = old_errno;
+ }
+
+ i_free(dotlock->path);
+ i_free(dotlock->lock_path);
+ i_free(dotlock);
+}
+
+static int file_dotlock_create_real(struct dotlock *dotlock,
+ enum dotlock_create_flags flags)
+{
+ const char *lock_path;
+ struct stat st;
+ int fd, ret;
+
+ ret = dotlock_create(dotlock, flags, TRUE, &lock_path);
+ if (ret <= 0 || (flags & DOTLOCK_CREATE_FLAG_CHECKONLY) != 0)
+ return ret;
+
+ fd = dotlock->fd;
+ dotlock->fd = -1;
+
+ if (close(fd) < 0) {
+ i_error("close(%s) failed: %m", lock_path);
+ return -1;
+ }
+
+ /* With NFS the writes may have been flushed only when closing the
+ file. Get the mtime again after that to avoid "dotlock was modified"
+ errors. */
+ if (lstat(lock_path, &st) < 0) {
+ if (errno != ENOENT)
+ i_error("stat(%s) failed: %m", lock_path);
+ else {
+ i_error("dotlock %s was immediately deleted under us",
+ lock_path);
+ }
+ return -1;
+ }
+ /* extra sanity check won't hurt.. */
+ if (st.st_dev != dotlock->dev || st.st_ino != dotlock->ino) {
+ errno = ENOENT;
+ i_error("dotlock %s was immediately recreated under us",
+ lock_path);
+ return -1;
+ }
+ dotlock->mtime = st.st_mtime;
+ return 1;
+}
+
+int file_dotlock_create(const struct dotlock_settings *set, const char *path,
+ enum dotlock_create_flags flags,
+ struct dotlock **dotlock_r)
+{
+ struct dotlock *dotlock;
+ int ret;
+
+ dotlock = file_dotlock_alloc(set, path);
+ T_BEGIN {
+ ret = file_dotlock_create_real(dotlock, flags);
+ } T_END;
+ if (ret <= 0 || (flags & DOTLOCK_CREATE_FLAG_CHECKONLY) != 0)
+ file_dotlock_free(&dotlock);
+
+ *dotlock_r = dotlock;
+ return ret;
+}
+
+static void dotlock_replaced_warning(struct dotlock *dotlock, bool deleted)
+{
+ const char *lock_path;
+ time_t now = time(NULL);
+
+ lock_path = file_dotlock_get_lock_path(dotlock);
+ if (dotlock->mtime == dotlock->lock_time) {
+ i_warning("Our dotlock file %s was %s "
+ "(locked %d secs ago, touched %d secs ago)",
+ lock_path, deleted ? "deleted" : "overridden",
+ (int)(now - dotlock->lock_time),
+ (int)(now - dotlock->mtime));
+ } else {
+ i_warning("Our dotlock file %s was %s "
+ "(kept it %d secs)", lock_path,
+ deleted ? "deleted" : "overridden",
+ (int)(now - dotlock->lock_time));
+ }
+}
+
+static bool file_dotlock_has_mtime_changed(time_t t1, time_t t2)
+{
+ time_t diff;
+
+ if (t1 == t2)
+ return FALSE;
+
+ /* with NFS t1 may have been looked up from local cache.
+ allow it to be a little bit different. */
+ diff = t1 > t2 ? t1-t2 : t2-t1;
+ return diff > FILE_DOTLOCK_MAX_STAT_MTIME_DIFF;
+}
+
+int file_dotlock_delete(struct dotlock **dotlock_p)
+{
+ struct dotlock *dotlock;
+ const char *lock_path;
+ struct stat st;
+ int ret;
+
+ dotlock = *dotlock_p;
+ *dotlock_p = NULL;
+
+ lock_path = file_dotlock_get_lock_path(dotlock);
+ if (nfs_safe_lstat(lock_path, &st) < 0) {
+ if (errno == ENOENT) {
+ dotlock_replaced_warning(dotlock, TRUE);
+ file_dotlock_free(&dotlock);
+ return 0;
+ }
+
+ i_error("lstat(%s) failed: %m", lock_path);
+ file_dotlock_free(&dotlock);
+ return -1;
+ }
+
+ if (dotlock->ino != st.st_ino ||
+ !CMP_DEV_T(dotlock->dev, st.st_dev)) {
+ dotlock_replaced_warning(dotlock, FALSE);
+ errno = EEXIST;
+ file_dotlock_free(&dotlock);
+ return 0;
+ }
+
+ if (file_dotlock_has_mtime_changed(dotlock->mtime, st.st_mtime) &&
+ dotlock->fd == -1) {
+ i_warning("Our dotlock file %s was modified (%s vs %s), "
+ "assuming it wasn't overridden (kept it %d secs)",
+ lock_path,
+ dec2str(dotlock->mtime), dec2str(st.st_mtime),
+ (int)(time(NULL) - dotlock->lock_time));
+ }
+
+ if ((ret = i_unlink_if_exists(lock_path)) == 0)
+ dotlock_replaced_warning(dotlock, TRUE);
+ file_dotlock_free(&dotlock);
+ return ret;
+}
+
+int file_dotlock_open(const struct dotlock_settings *set, const char *path,
+ enum dotlock_create_flags flags,
+ struct dotlock **dotlock_r)
+{
+ struct dotlock *dotlock;
+ int ret;
+
+ dotlock = file_dotlock_alloc(set, path);
+ T_BEGIN {
+ const char *lock_path;
+
+ ret = dotlock_create(dotlock, flags, FALSE, &lock_path);
+ } T_END;
+
+ if (ret <= 0) {
+ file_dotlock_free(&dotlock);
+ *dotlock_r = NULL;
+ return -1;
+ }
+
+ *dotlock_r = dotlock;
+ return dotlock->fd;
+}
+
+static int ATTR_NULL(7)
+file_dotlock_open_mode_full(const struct dotlock_settings *set, const char *path,
+ enum dotlock_create_flags flags,
+ mode_t mode, uid_t uid, gid_t gid,
+ const char *gid_origin, struct dotlock **dotlock_r)
+{
+ struct dotlock *dotlock;
+ mode_t old_mask;
+ int fd;
+
+ old_mask = umask(0666 ^ mode);
+ fd = file_dotlock_open(set, path, flags, &dotlock);
+ umask(old_mask);
+
+ if (fd != -1 && (uid != (uid_t)-1 || gid != (gid_t)-1)) {
+ if (fchown(fd, uid, gid) < 0) {
+ if (errno == EPERM && uid == (uid_t)-1) {
+ i_error("%s", eperm_error_get_chgrp("fchown",
+ file_dotlock_get_lock_path(dotlock),
+ gid, gid_origin));
+ } else {
+ i_error("fchown(%s, %ld, %ld) failed: %m",
+ file_dotlock_get_lock_path(dotlock),
+ (long)uid, (long)gid);
+ }
+ file_dotlock_delete(&dotlock);
+ return -1;
+ }
+ }
+ *dotlock_r = dotlock;
+ return fd;
+}
+
+int file_dotlock_open_mode(const struct dotlock_settings *set, const char *path,
+ enum dotlock_create_flags flags,
+ mode_t mode, uid_t uid, gid_t gid,
+ struct dotlock **dotlock_r)
+{
+ return file_dotlock_open_mode_full(set, path, flags, mode, uid, gid,
+ NULL, dotlock_r);
+}
+
+int file_dotlock_open_group(const struct dotlock_settings *set, const char *path,
+ enum dotlock_create_flags flags,
+ mode_t mode, gid_t gid, const char *gid_origin,
+ struct dotlock **dotlock_r)
+{
+ return file_dotlock_open_mode_full(set, path, flags, mode, (uid_t)-1,
+ gid, gid_origin, dotlock_r);
+}
+
+int file_dotlock_replace(struct dotlock **dotlock_p,
+ enum dotlock_replace_flags flags)
+{
+ struct dotlock *dotlock;
+ const char *lock_path;
+ bool is_locked;
+
+ dotlock = *dotlock_p;
+ *dotlock_p = NULL;
+
+ is_locked = (flags & DOTLOCK_REPLACE_FLAG_VERIFY_OWNER) == 0 ? TRUE :
+ file_dotlock_is_locked(dotlock);
+
+ if ((flags & DOTLOCK_REPLACE_FLAG_DONT_CLOSE_FD) != 0)
+ dotlock->fd = -1;
+
+ if (!is_locked) {
+ dotlock_replaced_warning(dotlock, FALSE);
+ errno = EEXIST;
+ file_dotlock_free(&dotlock);
+ return 0;
+ }
+
+ lock_path = file_dotlock_get_lock_path(dotlock);
+ if (rename(lock_path, dotlock->path) < 0) {
+ i_error("rename(%s, %s) failed: %m", lock_path, dotlock->path);
+ if (errno == ENOENT)
+ dotlock_replaced_warning(dotlock, TRUE);
+ file_dotlock_free(&dotlock);
+ return -1;
+ }
+ file_dotlock_free(&dotlock);
+ return 1;
+}
+
+int file_dotlock_touch(struct dotlock *dotlock)
+{
+ time_t now = time(NULL);
+ struct utimbuf buf;
+ int ret = 0;
+
+ if (dotlock->mtime == now)
+ return 0;
+
+ dotlock->mtime = now;
+ buf.actime = buf.modtime = now;
+
+ T_BEGIN {
+ const char *lock_path = file_dotlock_get_lock_path(dotlock);
+ if (utime(lock_path, &buf) < 0) {
+ i_error("utime(%s) failed: %m", lock_path);
+ ret = -1;
+ }
+ } T_END;
+ return ret;
+}
+
+bool file_dotlock_is_locked(struct dotlock *dotlock)
+{
+ struct stat st, st2;
+ const char *lock_path;
+
+ lock_path = file_dotlock_get_lock_path(dotlock);
+ if (fstat(dotlock->fd, &st) < 0) {
+ i_error("fstat(%s) failed: %m", lock_path);
+ return FALSE;
+ }
+
+ if (nfs_safe_lstat(lock_path, &st2) < 0) {
+ i_error("lstat(%s) failed: %m", lock_path);
+ return FALSE;
+ }
+ return st.st_ino == st2.st_ino && CMP_DEV_T(st.st_dev, st2.st_dev);
+}
+
+const char *file_dotlock_get_lock_path(struct dotlock *dotlock)
+{
+ if (dotlock->lock_path == NULL) {
+ dotlock->lock_path =
+ i_strconcat(dotlock->path,
+ dotlock->settings.lock_suffix, NULL);
+ }
+ return dotlock->lock_path;
+}
diff --git a/src/lib/file-dotlock.h b/src/lib/file-dotlock.h
new file mode 100644
index 0000000..0b958c8
--- /dev/null
+++ b/src/lib/file-dotlock.h
@@ -0,0 +1,94 @@
+#ifndef FILE_DOTLOCK_H
+#define FILE_DOTLOCK_H
+
+#include <unistd.h>
+#include <fcntl.h>
+
+struct dotlock;
+
+struct dotlock_settings {
+ /* Dotlock files are created by first creating a temp file and then
+ link()ing it to the dotlock. temp_prefix specifies the prefix to
+ use for temp files. It may contain a full path. Default is
+ ".temp.hostname.pid.". */
+ const char *temp_prefix;
+ /* Use this suffix for dotlock filenames. Default is ".lock". */
+ const char *lock_suffix;
+
+ /* Abort after this many seconds. */
+ unsigned int timeout;
+ /* Override the lock file when it and the file we're protecting is
+ older than stale_timeout. */
+ unsigned int stale_timeout;
+
+ /* Callback is called once in a while. stale is set to TRUE if stale
+ lock is detected and will be overridden in secs_left. If callback
+ returns FALSE then, the lock will not be overridden. */
+ bool (*callback)(unsigned int secs_left, bool stale, void *context);
+ void *context;
+
+ /* Rely on O_EXCL locking to work instead of using hardlinks.
+ It's faster, but doesn't work with all NFS implementations. */
+ bool use_excl_lock:1;
+ /* Flush NFS attribute cache before stating files. */
+ bool nfs_flush:1;
+ /* Use io_add_notify() to speed up finding out when an existing
+ dotlock is deleted */
+ bool use_io_notify:1;
+};
+
+enum dotlock_create_flags {
+ /* If lock already exists, fail immediately */
+ DOTLOCK_CREATE_FLAG_NONBLOCK = 0x01,
+ /* Don't actually create the lock file, only make sure it doesn't
+ exist. This is racy, so you shouldn't rely on it much. */
+ DOTLOCK_CREATE_FLAG_CHECKONLY = 0x02
+};
+
+enum dotlock_replace_flags {
+ /* Check that lock file hasn't been overridden before renaming. */
+ DOTLOCK_REPLACE_FLAG_VERIFY_OWNER = 0x01,
+ /* Don't close the file descriptor. */
+ DOTLOCK_REPLACE_FLAG_DONT_CLOSE_FD = 0x02
+};
+
+/* Create dotlock. Returns 1 if successful, 0 if timeout or -1 if error.
+ When returning 0, errno is also set to EAGAIN. */
+int file_dotlock_create(const struct dotlock_settings *set, const char *path,
+ enum dotlock_create_flags flags,
+ struct dotlock **dotlock_r);
+
+/* Delete the dotlock file. Returns 1 if successful, 0 if the file had already
+ been deleted or reused by someone else, -1 if I/O error. */
+int ATTR_NOWARN_UNUSED_RESULT
+file_dotlock_delete(struct dotlock **dotlock);
+
+/* Use dotlock as the new content for file. This provides read safety without
+ locks, but it's not very good for large files. Returns fd for lock file.
+ If locking timed out, returns -1 and errno = EAGAIN. */
+int file_dotlock_open(const struct dotlock_settings *set, const char *path,
+ enum dotlock_create_flags flags,
+ struct dotlock **dotlock_r);
+/* Like file_dotlock_open(), but use the given file permissions. */
+int file_dotlock_open_mode(const struct dotlock_settings *set, const char *path,
+ enum dotlock_create_flags flags,
+ mode_t mode, uid_t uid, gid_t gid,
+ struct dotlock **dotlock_r);
+int file_dotlock_open_group(const struct dotlock_settings *set, const char *path,
+ enum dotlock_create_flags flags,
+ mode_t mode, gid_t gid, const char *gid_origin,
+ struct dotlock **dotlock_r);
+/* Replaces the file dotlock protects with the dotlock file itself. */
+int file_dotlock_replace(struct dotlock **dotlock,
+ enum dotlock_replace_flags flags);
+/* Update dotlock's mtime. If you're keeping the dotlock for a long time,
+ it's a good idea to update it once in a while so others won't override it.
+ If the timestamp is less than a second old, it's not updated. */
+int file_dotlock_touch(struct dotlock *dotlock);
+/* Returns TRUE if the lock is still ok, FALSE if it's been overridden. */
+bool file_dotlock_is_locked(struct dotlock *dotlock);
+
+/* Returns the lock file path. */
+const char *file_dotlock_get_lock_path(struct dotlock *dotlock);
+
+#endif
diff --git a/src/lib/file-lock.c b/src/lib/file-lock.c
new file mode 100644
index 0000000..f17beea
--- /dev/null
+++ b/src/lib/file-lock.c
@@ -0,0 +1,530 @@
+/* Copyright (c) 2002-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "istream.h"
+#include "file-lock.h"
+#include "file-dotlock.h"
+#include "time-util.h"
+
+#include <time.h>
+#include <sys/stat.h>
+#ifdef HAVE_FLOCK
+# include <sys/file.h>
+#endif
+
+struct file_lock {
+ struct file_lock_settings set;
+
+ int fd;
+ char *path;
+ struct dotlock *dotlock;
+
+ struct timeval locked_time;
+ int lock_type;
+};
+
+static struct timeval lock_wait_start;
+static uint64_t file_lock_wait_usecs = 0;
+static long long file_lock_slow_warning_usecs = -1;
+
+static void file_lock_log_warning_if_slow(struct file_lock *lock);
+
+bool file_lock_method_parse(const char *name, enum file_lock_method *method_r)
+{
+ if (strcasecmp(name, "fcntl") == 0)
+ *method_r = FILE_LOCK_METHOD_FCNTL;
+ else if (strcasecmp(name, "flock") == 0)
+ *method_r = FILE_LOCK_METHOD_FLOCK;
+ else if (strcasecmp(name, "dotlock") == 0)
+ *method_r = FILE_LOCK_METHOD_DOTLOCK;
+ else
+ return FALSE;
+ return TRUE;
+}
+
+const char *file_lock_method_to_str(enum file_lock_method method)
+{
+ switch (method) {
+ case FILE_LOCK_METHOD_FCNTL:
+ return "fcntl";
+ case FILE_LOCK_METHOD_FLOCK:
+ return "flock";
+ case FILE_LOCK_METHOD_DOTLOCK:
+ return "dotlock";
+ }
+ i_unreached();
+}
+
+int file_try_lock(int fd, const char *path, int lock_type,
+ const struct file_lock_settings *set,
+ struct file_lock **lock_r, const char **error_r)
+{
+ return file_wait_lock(fd, path, lock_type, set, 0, lock_r, error_r);
+}
+
+static const char *
+file_lock_find_fcntl(int lock_fd, int lock_type)
+{
+ struct flock fl;
+
+ i_zero(&fl);
+ fl.l_type = lock_type;
+ fl.l_whence = SEEK_SET;
+ fl.l_start = 0;
+ fl.l_len = 0;
+
+ if (fcntl(lock_fd, F_GETLK, &fl) < 0 ||
+ fl.l_type == F_UNLCK || fl.l_pid == -1 || fl.l_pid == 0)
+ return "";
+ return t_strdup_printf(" (%s lock held by pid %ld)",
+ fl.l_type == F_RDLCK ? "READ" : "WRITE", (long)fl.l_pid);
+}
+
+static const char *
+file_lock_find_proc_locks(int lock_fd ATTR_UNUSED)
+{
+ /* do anything except Linux support this? don't bother trying it for
+ OSes we don't know about. */
+#ifdef __linux__
+ static bool have_proc_locks = TRUE;
+ struct stat st;
+ char node_buf[MAX_INT_STRLEN * 3 + 2];
+ struct istream *input;
+ const char *line, *lock_type = "";
+ pid_t pid = 0;
+ int fd;
+
+ if (!have_proc_locks)
+ return NULL;
+
+ if (fstat(lock_fd, &st) < 0)
+ return "";
+ i_snprintf(node_buf, sizeof(node_buf), "%02x:%02x:%llu",
+ major(st.st_dev), minor(st.st_dev),
+ (unsigned long long)st.st_ino);
+ fd = open("/proc/locks", O_RDONLY);
+ if (fd == -1) {
+ have_proc_locks = FALSE;
+ return "";
+ }
+ input = i_stream_create_fd_autoclose(&fd, 512);
+ while (pid == 0 && (line = i_stream_read_next_line(input)) != NULL) T_BEGIN {
+ const char *const *args = t_strsplit_spaces(line, " ");
+
+ /* number: FLOCK/POSIX ADVISORY READ/WRITE pid
+ major:minor:inode region-start region-end */
+ if (str_array_length(args) < 8) {
+ ; /* don't continue from within a T_BEGIN {...} T_END */
+ } else if (strcmp(args[5], node_buf) == 0) {
+ lock_type = strcmp(args[3], "READ") == 0 ?
+ "READ" : "WRITE";
+ if (str_to_pid(args[4], &pid) < 0)
+ pid = 0;
+ }
+ } T_END;
+ i_stream_destroy(&input);
+ if (pid == 0) {
+ /* not found */
+ return "";
+ }
+ if (pid == getpid())
+ return " (BUG: lock is held by our own process)";
+ return t_strdup_printf(" (%s lock held by pid %ld)", lock_type, (long)pid);
+#else
+ return "";
+#endif
+}
+
+const char *file_lock_find(int lock_fd, enum file_lock_method lock_method,
+ int lock_type)
+{
+ const char *ret;
+
+ if (lock_method == FILE_LOCK_METHOD_FCNTL) {
+ ret = file_lock_find_fcntl(lock_fd, lock_type);
+ if (ret[0] != '\0')
+ return ret;
+ }
+ return file_lock_find_proc_locks(lock_fd);
+}
+
+static bool err_is_lock_timeout(time_t started, unsigned int timeout_secs)
+{
+ /* if EINTR took at least timeout_secs-1 number of seconds,
+ assume it was the alarm. otherwise log EINTR failure.
+ (We most likely don't want to retry EINTR since a signal
+ means somebody wants us to stop blocking). */
+ return errno == EINTR &&
+ (unsigned long)(time(NULL) - started + 1) >= timeout_secs;
+}
+
+static int file_lock_do(int fd, const char *path, int lock_type,
+ const struct file_lock_settings *set,
+ unsigned int timeout_secs, const char **error_r)
+{
+ const char *lock_type_str;
+ time_t started = time(NULL);
+ int ret;
+
+ i_assert(fd != -1);
+
+ if (timeout_secs != 0) {
+ alarm(timeout_secs);
+ file_lock_wait_start();
+ }
+
+ lock_type_str = lock_type == F_UNLCK ? "unlock" :
+ (lock_type == F_RDLCK ? "read-lock" : "write-lock");
+
+ switch (set->lock_method) {
+ case FILE_LOCK_METHOD_FCNTL: {
+#ifndef HAVE_FCNTL
+ *error_r = t_strdup_printf(
+ "Can't lock file %s: fcntl() locks not supported", path);
+ return -1;
+#else
+ struct flock fl;
+
+ fl.l_type = lock_type;
+ fl.l_whence = SEEK_SET;
+ fl.l_start = 0;
+ fl.l_len = 0;
+
+ ret = fcntl(fd, timeout_secs != 0 ? F_SETLKW : F_SETLK, &fl);
+ if (timeout_secs != 0) {
+ alarm(0);
+ file_lock_wait_end(path);
+ }
+
+ if (ret == 0)
+ break;
+
+ if (timeout_secs == 0 &&
+ (errno == EACCES || errno == EAGAIN)) {
+ /* locked by another process */
+ *error_r = t_strdup_printf(
+ "fcntl(%s, %s, F_SETLK) locking failed: %m "
+ "(File is already locked)", path, lock_type_str);
+ return 0;
+ }
+
+ if (err_is_lock_timeout(started, timeout_secs)) {
+ errno = EAGAIN;
+ *error_r = t_strdup_printf(
+ "fcntl(%s, %s, F_SETLKW) locking failed: "
+ "Timed out after %u seconds%s",
+ path, lock_type_str, timeout_secs,
+ file_lock_find(fd, set->lock_method,
+ lock_type));
+ return 0;
+ }
+ *error_r = t_strdup_printf("fcntl(%s, %s, %s) locking failed: %m",
+ path, lock_type_str, timeout_secs == 0 ? "F_SETLK" : "F_SETLKW");
+ if (errno == EDEADLK && !set->allow_deadlock) {
+ i_panic("%s%s", *error_r,
+ file_lock_find(fd, set->lock_method,
+ lock_type));
+ }
+ return -1;
+#endif
+ }
+ case FILE_LOCK_METHOD_FLOCK: {
+#ifndef HAVE_FLOCK
+ *error_r = t_strdup_printf(
+ "Can't lock file %s: flock() not supported", path);
+ return -1;
+#else
+ int operation = timeout_secs != 0 ? 0 : LOCK_NB;
+
+ switch (lock_type) {
+ case F_RDLCK:
+ operation |= LOCK_SH;
+ break;
+ case F_WRLCK:
+ operation |= LOCK_EX;
+ break;
+ case F_UNLCK:
+ operation |= LOCK_UN;
+ break;
+ }
+
+ ret = flock(fd, operation);
+ if (timeout_secs != 0) {
+ alarm(0);
+ file_lock_wait_end(path);
+ }
+
+ if (ret == 0)
+ break;
+
+ if (timeout_secs == 0 && errno == EWOULDBLOCK) {
+ /* locked by another process */
+ *error_r = t_strdup_printf(
+ "flock(%s, %s) failed: %m "
+ "(File is already locked)", path, lock_type_str);
+ return 0;
+ }
+ if (err_is_lock_timeout(started, timeout_secs)) {
+ errno = EAGAIN;
+ *error_r = t_strdup_printf("flock(%s, %s) failed: "
+ "Timed out after %u seconds%s",
+ path, lock_type_str, timeout_secs,
+ file_lock_find(fd, set->lock_method,
+ lock_type));
+ return 0;
+ }
+ *error_r = t_strdup_printf("flock(%s, %s) failed: %m",
+ path, lock_type_str);
+ if (errno == EDEADLK && !set->allow_deadlock) {
+ i_panic("%s%s", *error_r,
+ file_lock_find(fd, set->lock_method,
+ lock_type));
+ }
+ return -1;
+#endif
+ }
+ case FILE_LOCK_METHOD_DOTLOCK:
+ /* we shouldn't get here */
+ i_unreached();
+ }
+
+ return 1;
+}
+
+int file_wait_lock(int fd, const char *path, int lock_type,
+ const struct file_lock_settings *set,
+ unsigned int timeout_secs,
+ struct file_lock **lock_r, const char **error_r)
+{
+ struct file_lock *lock;
+ int ret;
+
+ ret = file_lock_do(fd, path, lock_type, set, timeout_secs, error_r);
+ if (ret <= 0)
+ return ret;
+
+ lock = i_new(struct file_lock, 1);
+ lock->set = *set;
+ lock->fd = fd;
+ lock->path = i_strdup(path);
+ lock->lock_type = lock_type;
+ i_gettimeofday(&lock->locked_time);
+ *lock_r = lock;
+ return 1;
+}
+
+int file_lock_try_update(struct file_lock *lock, int lock_type)
+{
+ const char *error;
+ int ret;
+
+ ret = file_lock_do(lock->fd, lock->path, lock_type, &lock->set, 0,
+ &error);
+ if (ret <= 0)
+ return ret;
+ file_lock_log_warning_if_slow(lock);
+ lock->lock_type = lock_type;
+ return 1;
+}
+
+void file_lock_set_unlink_on_free(struct file_lock *lock, bool set)
+{
+ lock->set.unlink_on_free = set;
+}
+
+void file_lock_set_close_on_free(struct file_lock *lock, bool set)
+{
+ lock->set.close_on_free = set;
+}
+
+struct file_lock *file_lock_from_dotlock(struct dotlock **dotlock)
+{
+ struct file_lock *lock;
+
+ lock = i_new(struct file_lock, 1);
+ lock->set.lock_method = FILE_LOCK_METHOD_DOTLOCK;
+ lock->fd = -1;
+ lock->path = i_strdup(file_dotlock_get_lock_path(*dotlock));
+ lock->lock_type = F_WRLCK;
+ i_gettimeofday(&lock->locked_time);
+ lock->dotlock = *dotlock;
+
+ *dotlock = NULL;
+ return lock;
+}
+
+static void file_unlock_real(struct file_lock *lock)
+{
+ const char *error;
+
+ if (file_lock_do(lock->fd, lock->path, F_UNLCK, &lock->set, 0,
+ &error) == 0) {
+ /* this shouldn't happen */
+ i_error("file_unlock(%s) failed: %m", lock->path);
+ }
+}
+
+void file_unlock(struct file_lock **_lock)
+{
+ struct file_lock *lock = *_lock;
+
+ *_lock = NULL;
+
+ /* unlocking is unnecessary when the file is unlinked. or alternatively
+ the unlink() must be done before unlocking, because otherwise it
+ could be deleting the new lock. */
+ i_assert(!lock->set.unlink_on_free);
+
+ if (lock->dotlock == NULL)
+ file_unlock_real(lock);
+ file_lock_free(&lock);
+}
+
+static void file_try_unlink_locked(struct file_lock *lock)
+{
+ struct file_lock *temp_lock = NULL;
+ struct file_lock_settings temp_set = lock->set;
+ struct stat st1, st2;
+ const char *error;
+ int ret;
+
+ temp_set.close_on_free = FALSE;
+ temp_set.unlink_on_free = FALSE;
+
+ file_unlock_real(lock);
+ ret = file_try_lock(lock->fd, lock->path, F_WRLCK, &temp_set,
+ &temp_lock, &error);
+ if (ret < 0) {
+ i_error("file_lock_free(): Unexpectedly failed to retry locking %s: %s",
+ lock->path, error);
+ } else if (ret == 0) {
+ /* already locked by someone else */
+ } else if (fstat(lock->fd, &st1) < 0) {
+ /* not expected to happen */
+ i_error("file_lock_free(): fstat(%s) failed: %m", lock->path);
+ } else if (stat(lock->path, &st2) < 0) {
+ if (errno != ENOENT)
+ i_error("file_lock_free(): stat(%s) failed: %m", lock->path);
+ } else if (st1.st_ino != st2.st_ino ||
+ !CMP_DEV_T(st1.st_dev, st2.st_dev)) {
+ /* lock file was recreated already - don't delete it */
+ } else {
+ /* nobody was waiting on the lock - unlink it */
+ i_unlink(lock->path);
+ }
+ file_lock_free(&temp_lock);
+}
+
+void file_lock_free(struct file_lock **_lock)
+{
+ struct file_lock *lock = *_lock;
+
+ if (lock == NULL)
+ return;
+
+ *_lock = NULL;
+
+ if (lock->dotlock != NULL)
+ file_dotlock_delete(&lock->dotlock);
+ if (lock->set.unlink_on_free)
+ file_try_unlink_locked(lock);
+ if (lock->set.close_on_free)
+ i_close_fd(&lock->fd);
+
+ file_lock_log_warning_if_slow(lock);
+ i_free(lock->path);
+ i_free(lock);
+}
+
+const char *file_lock_get_path(struct file_lock *lock)
+{
+ return lock->path;
+}
+
+void file_lock_set_path(struct file_lock *lock, const char *path)
+{
+ if (path != lock->path) {
+ i_free(lock->path);
+ lock->path = i_strdup(path);
+ }
+}
+
+void file_lock_wait_start(void)
+{
+ i_assert(lock_wait_start.tv_sec == 0);
+
+ i_gettimeofday(&lock_wait_start);
+}
+
+static void file_lock_wait_init_warning(void)
+{
+ const char *value;
+
+ i_assert(file_lock_slow_warning_usecs == -1);
+
+ value = getenv("FILE_LOCK_SLOW_WARNING_MSECS");
+ if (value == NULL)
+ file_lock_slow_warning_usecs = LLONG_MAX;
+ else if (str_to_llong(value, &file_lock_slow_warning_usecs) == 0 &&
+ file_lock_slow_warning_usecs > 0) {
+ file_lock_slow_warning_usecs *= 1000;
+ } else {
+ i_error("FILE_LOCK_SLOW_WARNING_MSECS: "
+ "Invalid value '%s' - ignoring", value);
+ file_lock_slow_warning_usecs = LLONG_MAX;
+ }
+}
+
+static void file_lock_log_warning_if_slow(struct file_lock *lock)
+{
+ if (file_lock_slow_warning_usecs < 0)
+ file_lock_wait_init_warning();
+ if (file_lock_slow_warning_usecs == LLONG_MAX) {
+ /* slowness checking is disabled */
+ return;
+ }
+ if (lock->lock_type != F_WRLCK) {
+ /* some shared locks can legitimately be kept for a long time.
+ don't warn about them. */
+ return;
+ }
+
+ struct timeval now;
+ i_gettimeofday(&now);
+
+ int diff = timeval_diff_msecs(&now, &lock->locked_time);
+ if (diff > file_lock_slow_warning_usecs/1000) {
+ i_warning("Lock %s kept for %d.%03d secs", lock->path,
+ diff / 1000, diff % 1000);
+ }
+}
+
+void file_lock_wait_end(const char *lock_name)
+{
+ struct timeval now;
+
+ i_assert(lock_wait_start.tv_sec != 0);
+
+ i_gettimeofday(&now);
+ long long diff = timeval_diff_usecs(&now, &lock_wait_start);
+ if (diff < 0) {
+ /* time moved backwards */
+ diff = 0;
+ }
+ if (diff > file_lock_slow_warning_usecs) {
+ if (file_lock_slow_warning_usecs < 0)
+ file_lock_wait_init_warning();
+ if (diff > file_lock_slow_warning_usecs) {
+ int diff_msecs = (diff + 999) / 1000;
+ i_warning("Locking %s took %d.%03d secs", lock_name,
+ diff_msecs / 1000, diff_msecs % 1000);
+ }
+ }
+ file_lock_wait_usecs += diff;
+ lock_wait_start.tv_sec = 0;
+}
+
+uint64_t file_lock_wait_get_total_usecs(void)
+{
+ return file_lock_wait_usecs;
+}
diff --git a/src/lib/file-lock.h b/src/lib/file-lock.h
new file mode 100644
index 0000000..0939adc
--- /dev/null
+++ b/src/lib/file-lock.h
@@ -0,0 +1,87 @@
+#ifndef FILE_LOCK_H
+#define FILE_LOCK_H
+
+#include <unistd.h>
+#include <fcntl.h>
+
+#define DEFAULT_LOCK_TIMEOUT 120
+
+struct file_lock;
+struct dotlock;
+
+enum file_lock_method {
+ FILE_LOCK_METHOD_FCNTL,
+ FILE_LOCK_METHOD_FLOCK,
+ FILE_LOCK_METHOD_DOTLOCK
+};
+
+struct file_lock_settings {
+ enum file_lock_method lock_method;
+
+ /* When the lock is freed, close the fd automatically. This can
+ be useful for files that are only created to exist as lock files. */
+ bool unlink_on_free:1;
+ /* When the lock is freed, unlink() the file automatically, unless other
+ processes are already waiting on the lock. This can be useful for
+ files that are only created to exist as lock files. */
+ bool close_on_free:1;
+ /* Do not panic when the kernel returns EDEADLK while acquiring the
+ lock. */
+ bool allow_deadlock:1;
+};
+
+/* Parse lock method from given string. Returns TRUE if ok,
+ FALSE if name is unknown. */
+bool file_lock_method_parse(const char *name, enum file_lock_method *method_r);
+/* Convert lock method to string. */
+const char *file_lock_method_to_str(enum file_lock_method method);
+
+/* Lock the file. Returns 1 if successful, 0 if file is already locked,
+ or -1 if error. lock_type is F_WRLCK or F_RDLCK. */
+int file_try_lock(int fd, const char *path, int lock_type,
+ const struct file_lock_settings *set,
+ struct file_lock **lock_r, const char **error_r);
+/* Like lock_try_lock(), but return 0 only after having tried to lock for
+ timeout_secs. */
+int file_wait_lock(int fd, const char *path, int lock_type,
+ const struct file_lock_settings *set,
+ unsigned int timeout_secs,
+ struct file_lock **lock_r, const char **error_r);
+/* Change the lock type. WARNING: This isn't an atomic operation!
+ The result is the same as file_unlock() + file_try_lock(). */
+int file_lock_try_update(struct file_lock *lock, int lock_type);
+/* When the lock is freed, unlink() the file automatically, unless other
+ processes are already waiting on the lock. This can be useful for files that
+ are only created to exist as lock files. */
+void file_lock_set_unlink_on_free(struct file_lock *lock, bool set);
+/* When the lock is freed, close the fd automatically. This can
+ be useful for files that are only created to exist as lock files. */
+void file_lock_set_close_on_free(struct file_lock *lock, bool set);
+
+/* Convert dotlock into file_lock, which can be deleted with either
+ file_unlock() or file_lock_free(). */
+struct file_lock *file_lock_from_dotlock(struct dotlock **dotlock);
+
+/* Unlock and free the lock. */
+void file_unlock(struct file_lock **lock);
+/* Free the lock without unlocking it (because you're closing the fd anyway). */
+void file_lock_free(struct file_lock **lock);
+
+/* Returns the path given as parameter to file_*lock*(). */
+const char *file_lock_get_path(struct file_lock *lock);
+/* Update lock file's path (after it gets renamed by the caller). This is
+ useful mainly together with file_lock_set_unlink_on_free(). */
+void file_lock_set_path(struct file_lock *lock, const char *path);
+
+/* Returns human-readable string containing the process that has the file
+ currently locked. Returns "" if unknown, otherwise " (string)". */
+const char *file_lock_find(int lock_fd, enum file_lock_method lock_method,
+ int lock_type);
+
+/* Track the duration of a lock wait. */
+void file_lock_wait_start(void);
+void file_lock_wait_end(const char *lock_name);
+/* Return how many microseconds has been spent on lock waiting. */
+uint64_t file_lock_wait_get_total_usecs(void);
+
+#endif
diff --git a/src/lib/file-set-size.c b/src/lib/file-set-size.c
new file mode 100644
index 0000000..1f5938f
--- /dev/null
+++ b/src/lib/file-set-size.c
@@ -0,0 +1,108 @@
+/* Copyright (c) 2002-2018 Dovecot authors, see the included COPYING file */
+
+#ifdef HAVE_CONFIG_H
+# include "config.h"
+#endif
+
+#ifdef HAVE_POSIX_FALLOCATE
+# define _XOPEN_SOURCE 600 /* Required by glibc, breaks Solaris 9 */
+#endif
+#define _GNU_SOURCE /* for fallocate() */
+#include "lib.h"
+#include "file-set-size.h"
+
+#include <unistd.h>
+#include <fcntl.h>
+#include <sys/stat.h>
+#if defined(HAVE_LINUX_FALLOC_H) && !defined(FALLOC_FL_KEEP_SIZE)
+/* Legacy Linux does not have the FALLOC_FL_* flags under fcntl.h */
+# include <linux/falloc.h>
+#endif
+
+int file_set_size(int fd, off_t size)
+{
+#ifdef HAVE_POSIX_FALLOCATE
+ static bool posix_fallocate_supported = TRUE;
+#endif
+ char block[IO_BLOCK_SIZE];
+ off_t offset;
+ ssize_t ret;
+ struct stat st;
+
+ i_assert(size >= 0);
+
+ if (fstat(fd, &st) < 0) {
+ i_error("fstat() failed: %m");
+ return -1;
+ }
+
+ if (size < st.st_size) {
+ if (ftruncate(fd, size) < 0) {
+ i_error("ftruncate() failed: %m");
+ return -1;
+ }
+ return 0;
+ }
+ if (size == st.st_size)
+ return 0;
+
+#ifdef HAVE_POSIX_FALLOCATE
+ if (posix_fallocate_supported) {
+ int err;
+
+ err = posix_fallocate(fd, st.st_size, size - st.st_size);
+ if (err == 0)
+ return 0;
+
+ if (err != EINVAL /* Solaris */ &&
+ err != EOPNOTSUPP /* AOX */) {
+ if (!ENOSPACE(err))
+ i_error("posix_fallocate() failed: %m");
+ return -1;
+ }
+ /* Not supported by kernel, fallback to writing. */
+ posix_fallocate_supported = FALSE;
+ }
+#endif
+ /* start growing the file */
+ offset = st.st_size;
+ memset(block, 0, I_MIN((ssize_t)sizeof(block), size - offset));
+
+ while (offset < size) {
+ ret = pwrite(fd, block,
+ I_MIN((ssize_t)sizeof(block), size - offset),
+ offset);
+ if (ret < 0) {
+ if (!ENOSPACE(errno))
+ i_error("pwrite() failed: %m");
+ return -1;
+ }
+ offset += ret;
+ }
+ return 0;
+}
+
+int file_preallocate(int fd ATTR_UNUSED, off_t size ATTR_UNUSED)
+{
+#if defined(HAVE_FALLOCATE) && defined(FALLOC_FL_KEEP_SIZE)
+ /* Linux */
+ if (fallocate(fd, FALLOC_FL_KEEP_SIZE, 0, size) < 0)
+ return errno == ENOSYS ? 0 : -1;
+ return 1;
+#elif defined (F_PREALLOCATE)
+ /* OSX */
+ fstore_t fs;
+
+ i_zero(&fs);
+ fs.fst_flags = F_ALLOCATECONTIG;
+ fs.fst_posmode = F_PEOFPOSMODE;
+ fs.fst_offset = 0;
+ fs.fst_length = size;
+ fs.fst_bytesalloc = 0;
+ if (fcntl(fd, F_PREALLOCATE, &fs) < 0)
+ return -1;
+ return fs.fst_bytesalloc > 0 ? 1 : 0;
+#else
+ return 0;
+#endif
+}
diff --git a/src/lib/file-set-size.h b/src/lib/file-set-size.h
new file mode 100644
index 0000000..6752d32
--- /dev/null
+++ b/src/lib/file-set-size.h
@@ -0,0 +1,13 @@
+#ifndef FILE_SET_SIZE_H
+#define FILE_SET_SIZE_H
+
+/* Shrink/grow file. If file is grown, the new data is guaranteed to
+ be zeros. The file offset may be anywhere after this call.
+ Returns -1 if failed, 0 if successful. */
+int file_set_size(int fd, off_t size);
+/* Preallocate file to given size, without actually changing the size
+ reported by stat(). Returns 1 if ok, 0 if not supported by this filesystem,
+ -1 if error. */
+int file_preallocate(int fd, off_t size);
+
+#endif
diff --git a/src/lib/fsync-mode.h b/src/lib/fsync-mode.h
new file mode 100644
index 0000000..5873614
--- /dev/null
+++ b/src/lib/fsync-mode.h
@@ -0,0 +1,14 @@
+#ifndef FSYNC_MODE_H
+#define FSYNC_MODE_H
+
+enum fsync_mode {
+ /* fsync when it's necessary for data safety. */
+ FSYNC_MODE_OPTIMIZED = 0,
+ /* never fsync (in case of a crash can lose data) */
+ FSYNC_MODE_NEVER,
+ /* fsync after all writes. this is necessary with NFS to avoid
+ write failures being delayed until file is close(). */
+ FSYNC_MODE_ALWAYS
+};
+
+#endif
diff --git a/src/lib/guid.c b/src/lib/guid.c
new file mode 100644
index 0000000..7a825fd
--- /dev/null
+++ b/src/lib/guid.c
@@ -0,0 +1,174 @@
+/* Copyright (c) 2011-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "ioloop.h"
+#include "buffer.h"
+#include "str.h"
+#include "sha1.h"
+#include "hash.h"
+#include "hex-binary.h"
+#include "hostpid.h"
+#include "guid.h"
+
+#include <unistd.h>
+#include <time.h>
+
+const char *guid_generate(void)
+{
+ static struct timespec ts = { 0, 0 };
+ static unsigned int pid = 0;
+
+ /* we'll use the current time in nanoseconds as the initial 64bit
+ counter. */
+ if (ts.tv_sec == 0) {
+ if (clock_gettime(CLOCK_REALTIME, &ts) < 0)
+ i_fatal("clock_gettime() failed: %m");
+ pid = getpid();
+ } else if ((uint32_t)ts.tv_nsec < (uint32_t)-1) {
+ ts.tv_nsec++;
+ } else {
+ ts.tv_sec++;
+ ts.tv_nsec = 0;
+ }
+ return t_strdup_printf("%08x%08lx.%x.%s",
+ (unsigned int)ts.tv_nsec,
+ (unsigned long)ts.tv_sec,
+ pid, my_hostname);
+}
+
+void guid_128_host_hash_get(const char *host,
+ unsigned char hash_r[STATIC_ARRAY GUID_128_HOST_HASH_SIZE])
+{
+ unsigned char full_hash[SHA1_RESULTLEN];
+
+ sha1_get_digest(host, strlen(host), full_hash);
+ memcpy(hash_r, full_hash + sizeof(full_hash)-GUID_128_HOST_HASH_SIZE,
+ GUID_128_HOST_HASH_SIZE);
+}
+
+void guid_128_generate(guid_128_t guid_r)
+{
+#if GUID_128_HOST_HASH_SIZE != 4
+# error GUID_128_HOST_HASH_SIZE must be 4
+#endif
+ static struct timespec ts = { 0, 0 };
+ static uint8_t guid_static[8];
+ uint32_t pid;
+
+ /* we'll use the current time in nanoseconds as the initial 64bit
+ counter. */
+ if (ts.tv_sec == 0) {
+ if (clock_gettime(CLOCK_REALTIME, &ts) < 0)
+ i_fatal("clock_gettime() failed: %m");
+ pid = getpid();
+
+ guid_static[0] = (pid & 0x000000ff);
+ guid_static[1] = (pid & 0x0000ff00) >> 8;
+ guid_static[2] = (pid & 0x00ff0000) >> 16;
+ guid_static[3] = (pid & 0xff000000) >> 24;
+ guid_128_host_hash_get(my_hostdomain(), guid_static+4);
+ } else if (ioloop_timeval.tv_sec > ts.tv_sec ||
+ (ioloop_timeval.tv_sec == ts.tv_sec &&
+ ioloop_timeval.tv_usec * 1000 > ts.tv_nsec)) {
+ /* use ioloop's time since we have it. it doesn't provide any
+ more uniqueness, but it allows finding out more reliably
+ when a GUID was created. */
+ ts.tv_sec = ioloop_timeval.tv_sec;
+ ts.tv_nsec = ioloop_timeval.tv_usec*1000;
+ } else if (ts.tv_nsec < 999999999L) {
+ ts.tv_nsec++;
+ } else {
+ ts.tv_sec++;
+ ts.tv_nsec = 0;
+ }
+
+ guid_r[0] = (ts.tv_nsec & 0x000000ff);
+ guid_r[1] = (ts.tv_nsec & 0x0000ff00) >> 8;
+ guid_r[2] = (ts.tv_nsec & 0x00ff0000) >> 16;
+ guid_r[3] = (ts.tv_nsec & 0xff000000) >> 24;
+ guid_r[4] = (ts.tv_sec & 0x000000ff);
+ guid_r[5] = (ts.tv_sec & 0x0000ff00) >> 8;
+ guid_r[6] = (ts.tv_sec & 0x00ff0000) >> 16;
+ guid_r[7] = (ts.tv_sec & 0xff000000) >> 24;
+ memcpy(guid_r + 8, guid_static, 8);
+}
+
+bool guid_128_is_empty(const guid_128_t guid)
+{
+ unsigned int i;
+
+ for (i = 0; i < GUID_128_SIZE; i++) {
+ if (guid[i] != 0)
+ return FALSE;
+ }
+ return TRUE;
+}
+
+bool guid_128_equals(const guid_128_t guid1, const guid_128_t guid2)
+{
+ return memcmp(guid1, guid2, GUID_128_SIZE) == 0;
+}
+
+int guid_128_from_string(const char *str, guid_128_t guid_r)
+{
+ buffer_t buf;
+
+ buffer_create_from_data(&buf, guid_r, GUID_128_SIZE);
+ return strlen(str) == GUID_128_SIZE*2 &&
+ hex_to_binary(str, &buf) == 0 &&
+ buf.used == GUID_128_SIZE ? 0 : -1;
+}
+
+const char *guid_128_to_string(const guid_128_t guid)
+{
+ return binary_to_hex(guid, GUID_128_SIZE);
+}
+
+unsigned int guid_128_hash(const guid_128_t guid)
+{
+ return mem_hash(guid, GUID_128_SIZE);
+}
+
+int guid_128_cmp(const guid_128_t guid1, const guid_128_t guid2)
+{
+ return memcmp(guid1, guid2, GUID_128_SIZE);
+}
+
+const char *guid_128_to_uuid_string(const guid_128_t guid, enum uuid_format format)
+{
+ switch(format) {
+ case FORMAT_COMPACT:
+ return guid_128_to_string(guid);
+ case FORMAT_RECORD:
+ return t_strdup_printf("%02x%02x%02x%02x-%02x%02x-%02x%02x-%02x%02x-%02x%02x%02x%02x%02x%02x",
+ guid[0], guid[1], guid[2], guid[3], guid[4],
+ guid[5], guid[6], guid[7], guid[8], guid[9],
+ guid[10], guid[11], guid[12], guid[13], guid[14],
+ guid[15]);
+ case FORMAT_MICROSOFT:
+ return t_strdup_printf("{%s}", guid_128_to_uuid_string(guid, FORMAT_RECORD));
+ }
+ i_unreached();
+}
+
+int guid_128_from_uuid_string(const char *str, guid_128_t guid_r)
+{
+ size_t i,len,m=0;
+ int ret;
+ T_BEGIN {
+ len = strlen(str);
+ string_t *str2 = t_str_new(len);
+ for(i=0; i < len; i++) {
+ /* Microsoft format */
+ if (i==0 && str[i] == '{') { m=1; continue; }
+ else if (i == len-1 && str[i] == '}') continue;
+ /* 8-4-4-4-12 */
+ if (((i==8+m) || (i==13+m) || (i==18+m) || (i==23+m)) &&
+ str[i] == '-') continue;
+ str_append_c(str2, str[i]);
+ }
+ ret = guid_128_from_string(str_c(str2), guid_r);
+ } T_END;
+
+ return ret;
+}
diff --git a/src/lib/guid.h b/src/lib/guid.h
new file mode 100644
index 0000000..0bf58d8
--- /dev/null
+++ b/src/lib/guid.h
@@ -0,0 +1,52 @@
+#ifndef GUID_H
+#define GUID_H
+
+#define GUID_128_SIZE 16
+typedef uint8_t guid_128_t[GUID_128_SIZE];
+
+#define GUID_128_HOST_HASH_SIZE 4
+
+ARRAY_DEFINE_TYPE(guid_128_t, guid_128_t);
+
+enum uuid_format {
+ FORMAT_RECORD,
+ FORMAT_COMPACT,
+ FORMAT_MICROSOFT,
+};
+/* Generate a GUID (contains host name) */
+const char *guid_generate(void);
+/* Generate 128 bit GUID */
+void guid_128_generate(guid_128_t guid_r);
+/* Returns TRUE if GUID is empty (not set / unknown). */
+bool guid_128_is_empty(const guid_128_t guid) ATTR_PURE;
+static inline void guid_128_empty(guid_128_t guid)
+{
+ memset(guid, 0, GUID_128_SIZE);
+}
+/* Returns TRUE if two GUIDs are equal. */
+bool guid_128_equals(const guid_128_t guid1, const guid_128_t guid2) ATTR_PURE;
+/* Copy GUID */
+static inline void guid_128_copy(guid_128_t dest, const guid_128_t src)
+{
+ memcpy(dest, src, GUID_128_SIZE);
+}
+
+/* Returns GUID as a hex string. */
+const char *guid_128_to_string(const guid_128_t guid);
+/* Parse GUID from a string. Returns 0 if ok, -1 if GUID isn't valid. */
+int guid_128_from_string(const char *str, guid_128_t guid_r);
+
+/* Returns GUID as a UUID hex string. */
+const char *guid_128_to_uuid_string(const guid_128_t guid, enum uuid_format format);
+/* Parse GUID from a UUID string. Returns 0 if ok, -1 if UUID isn't valid. */
+int guid_128_from_uuid_string(const char *str, guid_128_t guid_r);
+
+/* guid_128 hash/cmp functions for hash.h */
+unsigned int guid_128_hash(const guid_128_t guid) ATTR_PURE;
+int guid_128_cmp(const guid_128_t guid1, const guid_128_t guid2) ATTR_PURE;
+
+/* Return the hash of host used by guid_128_generate(). */
+void guid_128_host_hash_get(const char *host,
+ unsigned char hash_r[STATIC_ARRAY GUID_128_HOST_HASH_SIZE]);
+
+#endif
diff --git a/src/lib/hash-decl.h b/src/lib/hash-decl.h
new file mode 100644
index 0000000..60fb4e1
--- /dev/null
+++ b/src/lib/hash-decl.h
@@ -0,0 +1,20 @@
+#ifndef HASH_DECL_H
+#define HASH_DECL_H
+
+#define HASH_TABLE_UNION(key_type, value_type) { \
+ struct hash_table *_table; \
+ key_type _key; \
+ key_type *_keyp; \
+ const key_type _const_key; \
+ value_type _value; \
+ value_type *_valuep; \
+ }
+
+#define HASH_TABLE_DEFINE_TYPE(name, key_type, value_type) \
+ union hash ## __ ## name HASH_TABLE_UNION(key_type, value_type)
+#define HASH_TABLE(key_type, value_type) \
+ union HASH_TABLE_UNION(key_type, value_type)
+#define HASH_TABLE_TYPE(name) \
+ union hash ## __ ## name
+
+#endif
diff --git a/src/lib/hash-format.c b/src/lib/hash-format.c
new file mode 100644
index 0000000..41ed495
--- /dev/null
+++ b/src/lib/hash-format.c
@@ -0,0 +1,245 @@
+/* Copyright (c) 2010-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "base64.h"
+#include "hex-binary.h"
+#include "str.h"
+#include "hash-method.h"
+#include "hash-format.h"
+
+enum hash_encoding {
+ HASH_ENCODING_HEX,
+ HASH_ENCODING_HEX_SHORT,
+ HASH_ENCODING_BASE64
+};
+
+struct hash_format_list {
+ struct hash_format_list *next;
+
+ const struct hash_method *method;
+ void *context;
+ unsigned int bits;
+ enum hash_encoding encoding;
+};
+
+struct hash_format {
+ pool_t pool;
+ const char *str;
+
+ struct hash_format_list *list, **pos;
+ unsigned char *digest;
+};
+
+static int
+hash_format_parse(const char *str, unsigned int *idxp,
+ const struct hash_method **method_r,
+ unsigned int *bits_r, const char **error_r)
+{
+ const char *name, *end, *bitsp;
+ unsigned int bits, i = *idxp;
+
+ /* we should have "hash_name}" or "hash_name:bits}" */
+ end = strchr(str+i, '}');
+ if (end == NULL) {
+ *error_r = "Missing '}'";
+ return -1;
+ }
+ *idxp = end - str;
+ name = t_strdup_until(str+i, end);
+
+ bitsp = strchr(name, ':');
+ if (bitsp != NULL)
+ name = t_strdup_until(name, bitsp++);
+
+ *method_r = hash_method_lookup(name);
+ if (*method_r == NULL) {
+ *error_r = t_strconcat("Unknown hash method: ", name, NULL);
+ return -1;
+ }
+
+ bits = (*method_r)->digest_size * 8;
+ if (bitsp != NULL) {
+ if (str_to_uint(bitsp, &bits) < 0 ||
+ bits == 0 || bits > (*method_r)->digest_size*8) {
+ *error_r = t_strconcat("Invalid :bits number: ",
+ bitsp, NULL);
+ return -1;
+ }
+ if ((bits % 8) != 0) {
+ *error_r = t_strconcat(
+ "Currently :bits must be divisible by 8: ",
+ bitsp, NULL);
+ return -1;
+ }
+ }
+ *bits_r = bits;
+ return 0;
+}
+
+static int
+hash_format_string_analyze(struct hash_format *format, const char *str,
+ const char **error_r)
+{
+ struct hash_format_list *list;
+ unsigned int i;
+
+ for (i = 0; str[i] != '\0'; i++) {
+ if (str[i] != '%')
+ continue;
+ i++;
+
+ list = p_new(format->pool, struct hash_format_list, 1);
+ list->encoding = HASH_ENCODING_HEX;
+ *format->pos = list;
+ format->pos = &list->next;
+
+ if (str[i] == 'B') {
+ list->encoding = HASH_ENCODING_BASE64;
+ i++;
+ } else if (str[i] == 'X') {
+ list->encoding = HASH_ENCODING_HEX_SHORT;
+ i++;
+ }
+ if (str[i++] != '{') {
+ *error_r = "No '{' after '%'";
+ return -1;
+ }
+ if (hash_format_parse(str, &i, &list->method,
+ &list->bits, error_r) < 0)
+ return -1;
+ list->context = p_malloc(format->pool,
+ list->method->context_size);
+ list->method->init(list->context);
+ }
+ return 0;
+}
+
+int hash_format_init(const char *format_string, struct hash_format **format_r,
+ const char **error_r)
+{
+ struct hash_format *format;
+ pool_t pool;
+ int ret;
+
+ pool = pool_alloconly_create("hash format", 1024);
+ format = p_new(pool, struct hash_format, 1);
+ format->pool = pool;
+ format->str = p_strdup(pool, format_string);
+ format->pos = &format->list;
+ T_BEGIN {
+ ret = hash_format_string_analyze(format, format_string,
+ error_r);
+ if (ret < 0)
+ *error_r = p_strdup(format->pool, *error_r);
+ } T_END;
+ if (ret < 0) {
+ *error_r = t_strdup(*error_r);
+ pool_unref(&pool);
+ return -1;
+ }
+ *format_r = format;
+ return 0;
+}
+
+void hash_format_loop(struct hash_format *format,
+ const void *data, size_t size)
+{
+ struct hash_format_list *list;
+
+ for (list = format->list; list != NULL; list = list->next)
+ list->method->loop(list->context, data, size);
+}
+
+void hash_format_reset(struct hash_format *format)
+{
+ struct hash_format_list *list;
+
+ for (list = format->list; list != NULL; list = list->next) {
+ memset(list->context, 0, list->method->context_size);
+ list->method->init(list->context);
+ }
+}
+
+static void
+hash_format_digest(string_t *dest, const struct hash_format_list *list,
+ const unsigned char *digest)
+{
+ unsigned int i, orig_len, size = list->bits / 8;
+
+ i_assert(list->bits % 8 == 0);
+
+ switch (list->encoding) {
+ case HASH_ENCODING_HEX:
+ binary_to_hex_append(dest, digest, size);
+ break;
+ case HASH_ENCODING_HEX_SHORT:
+ orig_len = str_len(dest);
+ binary_to_hex_append(dest, digest, size);
+ /* drop leading zeros, except if it's the only one */
+ for (i = orig_len; i < str_len(dest); i++) {
+ if (str_data(dest)[i] != '0')
+ break;
+ }
+ if (i == str_len(dest)) i--;
+ str_delete(dest, orig_len, i-orig_len);
+ break;
+ case HASH_ENCODING_BASE64:
+ orig_len = str_len(dest);
+ base64_encode(digest, size, dest);
+ /* drop trailing '=' chars */
+ while (str_len(dest) > orig_len &&
+ str_data(dest)[str_len(dest)-1] == '=')
+ str_truncate(dest, str_len(dest)-1);
+ break;
+ }
+}
+
+void hash_format_write(struct hash_format *format, string_t *dest)
+{
+ struct hash_format_list *list;
+ const char *p;
+ unsigned int i, max_digest_size = 0;
+
+ for (list = format->list; list != NULL; list = list->next) {
+ if (max_digest_size < list->method->digest_size)
+ max_digest_size = list->method->digest_size;
+ }
+ if (format->digest == NULL)
+ format->digest = p_malloc(format->pool, max_digest_size);
+
+ list = format->list;
+ for (i = 0; format->str[i] != '\0'; i++) {
+ if (format->str[i] != '%') {
+ str_append_c(dest, format->str[i]);
+ continue;
+ }
+
+ /* we already verified that the string is ok */
+ i_assert(list != NULL);
+ list->method->result(list->context, format->digest);
+ hash_format_digest(dest, list, format->digest);
+ list = list->next;
+
+ p = strchr(format->str+i, '}');
+ i_assert(p != NULL);
+ i = p - format->str;
+ }
+}
+
+void hash_format_deinit(struct hash_format **_format, string_t *dest)
+{
+ struct hash_format *format = *_format;
+
+ *_format = NULL;
+
+ hash_format_write(format, dest);
+ pool_unref(&format->pool);
+}
+
+void hash_format_deinit_free(struct hash_format **_format)
+{
+ struct hash_format *format = *_format;
+
+ *_format = NULL;
+ pool_unref(&format->pool);
+}
diff --git a/src/lib/hash-format.h b/src/lib/hash-format.h
new file mode 100644
index 0000000..177dc6f
--- /dev/null
+++ b/src/lib/hash-format.h
@@ -0,0 +1,23 @@
+#ifndef HASH_FORMAT_H
+#define HASH_FORMAT_H
+
+struct hash_format;
+
+/* Initialize formatting hash. Format can contain text with %{sha1} style
+ variables. Each hash hash can be also truncated by specifying the number
+ of bits to truncate to, such as %{sha1:80}. */
+int hash_format_init(const char *format_string, struct hash_format **format_r,
+ const char **error_r);
+/* Add more data to hash. */
+void hash_format_loop(struct hash_format *format,
+ const void *data, size_t size);
+/* Finish the hash and write it into given string. */
+void hash_format_write(struct hash_format *format, string_t *dest);
+/* Reset hash to initial state. */
+void hash_format_reset(struct hash_format *format);
+/* Write the hash into given string and free used memory. */
+void hash_format_deinit(struct hash_format **format, string_t *dest);
+/* Free used memory without writing to string. */
+void hash_format_deinit_free(struct hash_format **format);
+
+#endif
diff --git a/src/lib/hash-method.c b/src/lib/hash-method.c
new file mode 100644
index 0000000..d7a6bed
--- /dev/null
+++ b/src/lib/hash-method.c
@@ -0,0 +1,98 @@
+/* Copyright (c) 2010-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "md4.h"
+#include "md5.h"
+#include "sha1.h"
+#include "sha2.h"
+#include "sha3.h"
+#include "hash-method.h"
+
+const struct hash_method *hash_method_lookup(const char *name)
+{
+ unsigned int i;
+
+ for (i = 0; hash_methods[i] != NULL; i++) {
+ if (strcmp(hash_methods[i]->name, name) == 0)
+ return hash_methods[i];
+ }
+ return NULL;
+}
+
+static void hash_method_init_size(void *context)
+{
+ uint64_t *ctx = context;
+
+ *ctx = 0;
+}
+
+static void
+hash_method_loop_size(void *context, const void *data ATTR_UNUSED, size_t size)
+{
+ uint64_t *ctx = context;
+
+ *ctx += size;
+}
+
+static void hash_method_result_size(void *context, unsigned char *result_r)
+{
+ uint64_t *ctx = context;
+
+ result_r[0] = (*ctx & 0xff00000000000000ULL) >> 56;
+ result_r[1] = (*ctx & 0x00ff000000000000ULL) >> 48;
+ result_r[2] = (*ctx & 0x0000ff0000000000ULL) >> 40;
+ result_r[3] = (*ctx & 0x000000ff00000000ULL) >> 32;
+ result_r[4] = (*ctx & 0x00000000ff000000ULL) >> 24;
+ result_r[5] = (*ctx & 0x0000000000ff0000ULL) >> 16;
+ result_r[6] = (*ctx & 0x000000000000ff00ULL) >> 8;
+ result_r[7] = (*ctx & 0x00000000000000ffULL);
+}
+
+void hash_method_get_digest(const struct hash_method *meth,
+ const void *data, size_t data_len,
+ unsigned char *result_r)
+{
+ i_assert(meth != NULL);
+ i_assert(data_len == 0 || data != NULL);
+ unsigned char ctx[meth->context_size];
+
+ meth->init(ctx);
+ meth->loop(ctx, data == NULL ? "" : data, data_len);
+ meth->result(ctx, result_r);
+}
+
+buffer_t *t_hash_data(const struct hash_method *meth,
+ const void *data, size_t data_len)
+{
+ i_assert(meth != NULL);
+ buffer_t *result = t_buffer_create(meth->digest_size);
+ unsigned char *resptr = buffer_append_space_unsafe(result,
+ meth->digest_size);
+
+ hash_method_get_digest(meth, data, data_len, resptr);
+ return result;
+}
+
+static const struct hash_method hash_method_size = {
+ .name = "size",
+ .block_size = 1,
+ .context_size = sizeof(uint64_t),
+ .digest_size = sizeof(uint64_t),
+
+ .init = hash_method_init_size,
+ .loop = hash_method_loop_size,
+ .result = hash_method_result_size
+};
+
+const struct hash_method *hash_methods[] = {
+ &hash_method_md4,
+ &hash_method_md5,
+ &hash_method_sha1,
+ &hash_method_sha256,
+ &hash_method_sha384,
+ &hash_method_sha512,
+ &hash_method_sha3_256,
+ &hash_method_sha3_512,
+ &hash_method_size,
+ NULL
+};
diff --git a/src/lib/hash-method.h b/src/lib/hash-method.h
new file mode 100644
index 0000000..c4250de
--- /dev/null
+++ b/src/lib/hash-method.h
@@ -0,0 +1,55 @@
+#ifndef HASH_METHOD_H
+#define HASH_METHOD_H
+
+#include "buffer.h"
+
+struct hash_method {
+ const char *name;
+ /* Block size for the algorithm */
+ unsigned int block_size;
+ /* Number of bytes that must be allocated for context */
+ unsigned int context_size;
+ /* Number of bytes that must be allocated for result()'s digest */
+ unsigned int digest_size;
+
+ void (*init)(void *context);
+ void (*loop)(void *context, const void *data, size_t size);
+ void (*result)(void *context, unsigned char *digest_r);
+};
+
+const struct hash_method *hash_method_lookup(const char *name);
+
+/* NULL-terminated list of all hash methods */
+extern const struct hash_method *hash_methods[];
+
+void hash_method_get_digest(const struct hash_method *meth,
+ const void *data, size_t data_len,
+ unsigned char *result_r);
+
+/** Simple datastack helpers for digesting (hashing)
+
+ * USAGE:
+
+ buffer_t *result = t_hash_str(hash_method_lookup("sha256"), "hello world");
+ const char *hex = binary_to_hex(result->data, result->used);
+
+*/
+
+buffer_t *t_hash_data(const struct hash_method *meth,
+ const void *data, size_t data_len);
+
+static inline
+buffer_t *t_hash_buffer(const struct hash_method *meth,
+ const buffer_t *data)
+{
+ return t_hash_data(meth, data->data, data->used);
+}
+
+static inline
+buffer_t *t_hash_str(const struct hash_method *meth,
+ const char *data)
+{
+ return t_hash_data(meth, data, strlen(data));
+}
+
+#endif
diff --git a/src/lib/hash.c b/src/lib/hash.c
new file mode 100644
index 0000000..5530de1
--- /dev/null
+++ b/src/lib/hash.c
@@ -0,0 +1,593 @@
+/* Copyright (c) 2002-2018 Dovecot authors, see the included COPYING file */
+
+/* @UNSAFE: whole file */
+
+#include "lib.h"
+#include "hash.h"
+#include "primes.h"
+
+#include <ctype.h>
+
+#define HASH_TABLE_MIN_SIZE 67
+
+#undef hash_table_create
+#undef hash_table_create_direct
+#undef hash_table_destroy
+#undef hash_table_clear
+#undef hash_table_lookup
+#undef hash_table_lookup_full
+#undef hash_table_insert
+#undef hash_table_update
+#undef hash_table_try_remove
+#undef hash_table_count
+#undef hash_table_iterate_init
+#undef hash_table_iterate
+#undef hash_table_freeze
+#undef hash_table_thaw
+#undef hash_table_copy
+
+struct hash_node {
+ struct hash_node *next;
+ void *key;
+ void *value;
+};
+
+struct hash_table {
+ pool_t node_pool;
+
+ int frozen;
+ unsigned int initial_size, nodes_count, removed_count;
+
+ unsigned int size;
+ struct hash_node *nodes;
+ struct hash_node *free_nodes;
+
+ hash_callback_t *hash_cb;
+ hash_cmp_callback_t *key_compare_cb;
+};
+
+struct hash_iterate_context {
+ struct hash_table *table;
+ struct hash_node *next;
+ unsigned int pos;
+};
+
+enum hash_table_operation{
+ HASH_TABLE_OP_INSERT,
+ HASH_TABLE_OP_UPDATE,
+ HASH_TABLE_OP_RESIZE
+};
+
+static bool hash_table_resize(struct hash_table *table, bool grow);
+
+void hash_table_create(struct hash_table **table_r, pool_t node_pool,
+ unsigned int initial_size, hash_callback_t *hash_cb,
+ hash_cmp_callback_t *key_compare_cb)
+{
+ struct hash_table *table;
+
+ pool_ref(node_pool);
+ table = i_new(struct hash_table, 1);
+ table->node_pool = node_pool;
+ table->initial_size =
+ I_MAX(primes_closest(initial_size), HASH_TABLE_MIN_SIZE);
+
+ table->hash_cb = hash_cb;
+ table->key_compare_cb = key_compare_cb;
+
+ table->size = table->initial_size;
+ table->nodes = i_new(struct hash_node, table->size);
+ *table_r = table;
+}
+
+static unsigned int direct_hash(const void *p)
+{
+ /* NOTE: may truncate the value, but that doesn't matter. */
+ return POINTER_CAST_TO(p, unsigned int);
+}
+
+static int direct_cmp(const void *p1, const void *p2)
+{
+ return p1 == p2 ? 0 : 1;
+}
+
+void hash_table_create_direct(struct hash_table **table_r, pool_t node_pool,
+ unsigned int initial_size)
+{
+ hash_table_create(table_r, node_pool, initial_size,
+ direct_hash, direct_cmp);
+}
+
+static void free_node(struct hash_table *table, struct hash_node *node)
+{
+ if (!table->node_pool->alloconly_pool)
+ p_free(table->node_pool, node);
+ else {
+ node->next = table->free_nodes;
+ table->free_nodes = node;
+ }
+}
+
+static void destroy_node_list(struct hash_table *table, struct hash_node *node)
+{
+ struct hash_node *next;
+
+ while (node != NULL) {
+ next = node->next;
+ p_free(table->node_pool, node);
+ node = next;
+ }
+}
+
+static void hash_table_destroy_nodes(struct hash_table *table)
+{
+ unsigned int i;
+
+ for (i = 0; i < table->size; i++) {
+ if (table->nodes[i].next != NULL)
+ destroy_node_list(table, table->nodes[i].next);
+ }
+}
+
+void hash_table_destroy(struct hash_table **_table)
+{
+ struct hash_table *table = *_table;
+
+ if (table == NULL)
+ return;
+ *_table = NULL;
+
+ i_assert(table->frozen == 0);
+
+ if (!table->node_pool->alloconly_pool) {
+ hash_table_destroy_nodes(table);
+ destroy_node_list(table, table->free_nodes);
+ }
+
+ pool_unref(&table->node_pool);
+ i_free(table->nodes);
+ i_free(table);
+}
+
+void hash_table_clear(struct hash_table *table, bool free_nodes)
+{
+ i_assert(table->frozen == 0);
+
+ if (!table->node_pool->alloconly_pool)
+ hash_table_destroy_nodes(table);
+
+ if (free_nodes) {
+ if (!table->node_pool->alloconly_pool)
+ destroy_node_list(table, table->free_nodes);
+ table->free_nodes = NULL;
+ }
+
+ memset(table->nodes, 0, sizeof(struct hash_node) * table->size);
+
+ table->nodes_count = 0;
+ table->removed_count = 0;
+}
+
+static struct hash_node *
+hash_table_lookup_node(const struct hash_table *table,
+ const void *key, unsigned int hash)
+{
+ struct hash_node *node;
+
+ node = &table->nodes[hash % table->size];
+
+ do {
+ if (node->key != NULL) {
+ if (table->key_compare_cb(node->key, key) == 0)
+ return node;
+ }
+ node = node->next;
+ } while (node != NULL);
+
+ return NULL;
+}
+
+void *hash_table_lookup(const struct hash_table *table, const void *key)
+{
+ struct hash_node *node;
+
+ node = hash_table_lookup_node(table, key, table->hash_cb(key));
+ return node != NULL ? node->value : NULL;
+}
+
+bool hash_table_lookup_full(const struct hash_table *table,
+ const void *lookup_key,
+ void **orig_key, void **value)
+{
+ struct hash_node *node;
+
+ node = hash_table_lookup_node(table, lookup_key,
+ table->hash_cb(lookup_key));
+ if (node == NULL)
+ return FALSE;
+
+ *orig_key = node->key;
+ *value = node->value;
+ return TRUE;
+}
+
+static void
+hash_table_insert_node(struct hash_table *table, void *key, void *value,
+ enum hash_table_operation opcode)
+{
+ struct hash_node *node, *prev;
+ unsigned int hash;
+ bool check_existing = TRUE;
+
+ i_assert(table->nodes_count < UINT_MAX);
+ i_assert(key != NULL);
+
+ if(opcode == HASH_TABLE_OP_RESIZE)
+ check_existing = FALSE;
+ hash = table->hash_cb(key);
+
+ if (check_existing && table->removed_count > 0) {
+ /* there may be holes, have to check everything */
+ node = hash_table_lookup_node(table, key, hash);
+ if (node != NULL) {
+ i_assert(opcode == HASH_TABLE_OP_UPDATE);
+ node->value = value;
+ return;
+ }
+
+ check_existing = FALSE;
+ }
+
+ /* a) primary node */
+ node = &table->nodes[hash % table->size];
+ if (node->key == NULL) {
+ table->nodes_count++;
+
+ node->key = key;
+ node->value = value;
+ return;
+ }
+ if (check_existing) {
+ if (table->key_compare_cb(node->key, key) == 0) {
+ i_assert(opcode == HASH_TABLE_OP_UPDATE);
+ node->value = value;
+ return;
+ }
+ }
+
+ /* b) collisions list */
+ prev = node; node = node->next;
+ while (node != NULL) {
+ if (node->key == NULL)
+ break;
+
+ if (check_existing) {
+ if (table->key_compare_cb(node->key, key) == 0) {
+ i_assert(opcode == HASH_TABLE_OP_UPDATE);
+ node->value = value;
+ return;
+ }
+ }
+ prev = node;
+ node = node->next;
+ }
+
+ if (node == NULL) {
+ if (table->frozen == 0 && hash_table_resize(table, TRUE)) {
+ /* resized table, try again */
+ hash_table_insert_node(table, key, value, HASH_TABLE_OP_RESIZE);
+ return;
+ }
+
+ if (table->free_nodes == NULL)
+ node = p_new(table->node_pool, struct hash_node, 1);
+ else {
+ node = table->free_nodes;
+ table->free_nodes = node->next;
+ node->next = NULL;
+ }
+ prev->next = node;
+ }
+
+ node->key = key;
+ node->value = value;
+
+ table->nodes_count++;
+}
+
+void hash_table_insert(struct hash_table *table, void *key, void *value)
+{
+ hash_table_insert_node(table, key, value, HASH_TABLE_OP_INSERT);
+}
+
+void hash_table_update(struct hash_table *table, void *key, void *value)
+{
+ hash_table_insert_node(table, key, value, HASH_TABLE_OP_UPDATE);
+}
+
+static void
+hash_table_compress(struct hash_table *table, struct hash_node *root)
+{
+ struct hash_node *node, *next;
+
+ i_assert(table->frozen == 0);
+
+ /* remove deleted nodes from the list */
+ for (node = root; node->next != NULL; ) {
+ next = node->next;
+
+ if (next->key == NULL) {
+ node->next = next->next;
+ free_node(table, next);
+ } else {
+ node = next;
+ }
+ }
+
+ /* update root */
+ if (root->key == NULL && root->next != NULL) {
+ next = root->next;
+ *root = *next;
+ free_node(table, next);
+ }
+}
+
+static void hash_table_compress_removed(struct hash_table *table)
+{
+ unsigned int i;
+
+ for (i = 0; i < table->size; i++)
+ hash_table_compress(table, &table->nodes[i]);
+
+ table->removed_count = 0;
+}
+
+bool hash_table_try_remove(struct hash_table *table, const void *key)
+{
+ struct hash_node *node;
+ unsigned int hash;
+
+ hash = table->hash_cb(key);
+
+ node = hash_table_lookup_node(table, key, hash);
+ if (unlikely(node == NULL))
+ return FALSE;
+
+ node->key = NULL;
+ table->nodes_count--;
+
+ if (table->frozen != 0)
+ table->removed_count++;
+ else if (!hash_table_resize(table, FALSE))
+ hash_table_compress(table, &table->nodes[hash % table->size]);
+ return TRUE;
+}
+
+unsigned int hash_table_count(const struct hash_table *table)
+{
+ return table->nodes_count;
+}
+
+struct hash_iterate_context *hash_table_iterate_init(struct hash_table *table)
+{
+ struct hash_iterate_context *ctx;
+
+ hash_table_freeze(table);
+
+ ctx = i_new(struct hash_iterate_context, 1);
+ ctx->table = table;
+ ctx->next = &table->nodes[0];
+ return ctx;
+}
+
+static struct hash_node *
+hash_table_iterate_next(struct hash_iterate_context *ctx,
+ struct hash_node *node)
+{
+ do {
+ node = node->next;
+ if (node == NULL) {
+ if (++ctx->pos == ctx->table->size) {
+ ctx->pos--;
+ return NULL;
+ }
+ node = &ctx->table->nodes[ctx->pos];
+ }
+ } while (node->key == NULL);
+
+ return node;
+}
+
+bool hash_table_iterate(struct hash_iterate_context *ctx,
+ void **key_r, void **value_r)
+{
+ struct hash_node *node;
+
+ node = ctx->next;
+ if (node != NULL && node->key == NULL)
+ node = hash_table_iterate_next(ctx, node);
+ if (node == NULL) {
+ *key_r = *value_r = NULL;
+ return FALSE;
+ }
+ *key_r = node->key;
+ *value_r = node->value;
+
+ ctx->next = hash_table_iterate_next(ctx, node);
+ return TRUE;
+}
+
+void hash_table_iterate_deinit(struct hash_iterate_context **_ctx)
+{
+ struct hash_iterate_context *ctx = *_ctx;
+
+ if (ctx == NULL)
+ return;
+
+ *_ctx = NULL;
+ hash_table_thaw(ctx->table);
+ i_free(ctx);
+}
+
+void hash_table_freeze(struct hash_table *table)
+{
+ table->frozen++;
+}
+
+void hash_table_thaw(struct hash_table *table)
+{
+ i_assert(table->frozen > 0);
+
+ if (--table->frozen > 0)
+ return;
+
+ if (table->removed_count > 0) {
+ if (!hash_table_resize(table, FALSE))
+ hash_table_compress_removed(table);
+ }
+}
+
+static bool hash_table_resize(struct hash_table *table, bool grow)
+{
+ struct hash_node *old_nodes, *node, *next;
+ unsigned int next_size, old_size, i;
+ float nodes_per_list;
+
+ i_assert(table->frozen == 0);
+
+ nodes_per_list = (float) table->nodes_count / (float) table->size;
+ if (nodes_per_list > 0.3 && nodes_per_list < 2.0)
+ return FALSE;
+
+ next_size = I_MAX(primes_closest(table->nodes_count+1),
+ table->initial_size);
+ if (next_size == table->size)
+ return FALSE;
+
+ if (grow && table->size >= next_size)
+ return FALSE;
+
+ /* recreate primary table */
+ old_size = table->size;
+ old_nodes = table->nodes;
+
+ table->size = I_MAX(next_size, HASH_TABLE_MIN_SIZE);
+ table->nodes = i_new(struct hash_node, table->size);
+
+ table->nodes_count = 0;
+ table->removed_count = 0;
+
+ table->frozen++;
+
+ /* move the data */
+ for (i = 0; i < old_size; i++) {
+ node = &old_nodes[i];
+ if (node->key != NULL) {
+ hash_table_insert_node(table, node->key,
+ node->value, HASH_TABLE_OP_RESIZE);
+ }
+
+ for (node = node->next; node != NULL; node = next) {
+ next = node->next;
+
+ if (node->key != NULL) {
+ hash_table_insert_node(table, node->key,
+ node->value, HASH_TABLE_OP_RESIZE);
+ }
+ free_node(table, node);
+ }
+ }
+
+ table->frozen--;
+
+ i_free(old_nodes);
+ return TRUE;
+}
+
+void hash_table_copy(struct hash_table *dest, struct hash_table *src)
+{
+ struct hash_iterate_context *iter;
+ void *key, *value;
+
+ hash_table_freeze(dest);
+
+ iter = hash_table_iterate_init(src);
+ while (hash_table_iterate(iter, &key, &value))
+ hash_table_insert(dest, key, value);
+ hash_table_iterate_deinit(&iter);
+
+ hash_table_thaw(dest);
+}
+
+/* a char* hash function from ASU -- from glib */
+unsigned int ATTR_NO_SANITIZE_INTEGER
+str_hash(const char *p)
+{
+ const unsigned char *s = (const unsigned char *)p;
+ unsigned int g, h = 0;
+
+ while (*s != '\0') {
+ h = (h << 4) + *s;
+ if ((g = h & 0xf0000000UL) != 0) {
+ h = h ^ (g >> 24);
+ h = h ^ g;
+ }
+ s++;
+ }
+
+ return h;
+}
+
+/* a char* hash function from ASU -- from glib */
+unsigned int ATTR_NO_SANITIZE_INTEGER
+strcase_hash(const char *p)
+{
+ const unsigned char *s = (const unsigned char *)p;
+ unsigned int g, h = 0;
+
+ while (*s != '\0') {
+ h = (h << 4) + i_toupper(*s);
+ if ((g = h & 0xf0000000UL) != 0) {
+ h = h ^ (g >> 24);
+ h = h ^ g;
+ }
+ s++;
+ }
+
+ return h;
+}
+
+unsigned int ATTR_NO_SANITIZE_INTEGER
+mem_hash(const void *p, unsigned int size)
+{
+ const unsigned char *s = p;
+ unsigned int i, g, h = 0;
+
+ for (i = 0; i < size; i++) {
+ h = (h << 4) + *s;
+ if ((g = h & 0xf0000000UL) != 0) {
+ h = h ^ (g >> 24);
+ h = h ^ g;
+ }
+ s++;
+ }
+ return h;
+}
+
+unsigned int ATTR_NO_SANITIZE_INTEGER
+strfastcase_hash(const char *p)
+{
+ const unsigned char *s = (const unsigned char *)p;
+ unsigned int g, h = 0;
+
+ while (*s != '\0') {
+ h = (h << 4) + ((*s) & ~0x20U);
+ if ((g = h & 0xf0000000UL) != 0) {
+ h = h ^ (g >> 24);
+ h = h ^ g;
+ }
+ s++;
+ }
+
+ return h;
+}
diff --git a/src/lib/hash.h b/src/lib/hash.h
new file mode 100644
index 0000000..65b6b8e
--- /dev/null
+++ b/src/lib/hash.h
@@ -0,0 +1,175 @@
+#ifndef HASH_H
+#define HASH_H
+
+struct hash_table;
+
+#ifdef HAVE_TYPEOF
+# define HASH_VALUE_CAST(table) (typeof((table)._value))
+#else
+# define HASH_VALUE_CAST(table)
+#endif
+
+/* Returns hash code. */
+typedef unsigned int hash_callback_t(const void *p);
+/* Returns 0 if the pointers are equal. */
+typedef int hash_cmp_callback_t(const void *p1, const void *p2);
+
+/* Create a new hash table. If initial_size is 0, the default value is used.
+ table_pool is used to allocate/free large hash tables, node_pool is used
+ for smaller allocations and can also be alloconly pool. The pools must not
+ be free'd before hash_table_destroy() is called. */
+void hash_table_create(struct hash_table **table_r, pool_t node_pool,
+ unsigned int initial_size,
+ hash_callback_t *hash_cb,
+ hash_cmp_callback_t *key_compare_cb);
+#define hash_table_create(table, pool, size, hash_cb, key_cmp_cb) \
+ TYPE_CHECKS(void, \
+ COMPILE_ERROR_IF_TRUE( \
+ sizeof((*table)._key) != sizeof(void *) || \
+ sizeof((*table)._value) != sizeof(void *)) || \
+ COMPILE_ERROR_IF_TRUE( \
+ !__builtin_types_compatible_p(typeof(&key_cmp_cb), \
+ int (*)(typeof((*table)._key), typeof((*table)._key))) && \
+ !__builtin_types_compatible_p(typeof(&key_cmp_cb), \
+ int (*)(typeof((*table)._const_key), typeof((*table)._const_key)))) || \
+ COMPILE_ERROR_IF_TRUE( \
+ !__builtin_types_compatible_p(typeof(&hash_cb), \
+ unsigned int (*)(typeof((*table)._key))) && \
+ !__builtin_types_compatible_p(typeof(&hash_cb), \
+ unsigned int (*)(typeof((*table)._const_key)))), \
+ hash_table_create(&(*table)._table, pool, size, \
+ (hash_callback_t *)hash_cb, \
+ (hash_cmp_callback_t *)key_cmp_cb))
+
+/* Create hash table where comparisons are done directly with the pointers. */
+void hash_table_create_direct(struct hash_table **table_r, pool_t node_pool,
+ unsigned int initial_size);
+#define hash_table_create_direct(table, pool, size) \
+ TYPE_CHECKS(void, \
+ COMPILE_ERROR_IF_TRUE( \
+ sizeof((*table)._key) != sizeof(void *) || \
+ sizeof((*table)._value) != sizeof(void *)), \
+ hash_table_create_direct(&(*table)._table, pool, size))
+
+#define hash_table_is_created(table) \
+ ((table)._table != NULL)
+
+void hash_table_destroy(struct hash_table **table);
+#define hash_table_destroy(table) \
+ hash_table_destroy(&(*table)._table)
+/* Remove all nodes from hash table. If free_collisions is TRUE, the
+ memory allocated from node_pool is freed, or discarded with alloconly pools.
+ WARNING: If you p_clear() the node_pool, the free_collisions must be TRUE. */
+void hash_table_clear(struct hash_table *table, bool free_collisions);
+#define hash_table_clear(table, free_collisions) \
+ hash_table_clear((table)._table, free_collisions)
+
+void *hash_table_lookup(const struct hash_table *table, const void *key) ATTR_PURE;
+#define hash_table_lookup(table, key) \
+ TYPE_CHECKS(void *, \
+ COMPILE_ERROR_IF_TYPES2_NOT_COMPATIBLE((table)._key, (table)._const_key, key), \
+ HASH_VALUE_CAST(table)hash_table_lookup((table)._table, (key)))
+
+bool hash_table_lookup_full(const struct hash_table *table,
+ const void *lookup_key,
+ void **orig_key_r, void **value_r);
+#ifndef __cplusplus
+# define hash_table_lookup_full(table, lookup_key, orig_key_r, value_r) \
+ TYPE_CHECKS(bool, \
+ COMPILE_ERROR_IF_TYPES2_NOT_COMPATIBLE((table)._const_key, (table)._key, lookup_key) || \
+ COMPILE_ERROR_IF_TYPES_NOT_COMPATIBLE((table)._keyp, orig_key_r) || \
+ COMPILE_ERROR_IF_TRUE(sizeof(*(orig_key_r)) != sizeof(void *)) || \
+ COMPILE_ERROR_IF_TYPES_NOT_COMPATIBLE((table)._valuep, value_r) || \
+ COMPILE_ERROR_IF_TRUE(sizeof(*(value_r)) != sizeof(void *)), \
+ hash_table_lookup_full((table)._table, \
+ (lookup_key), (void *)(orig_key_r), (void *)(value_r)))
+#else
+/* C++ requires (void **) casting, but that's not possible with strict
+ aliasing, so .. we'll just disable the type checks */
+# define hash_table_lookup_full(table, lookup_key, orig_key_r, value_r) \
+ hash_table_lookup_full((table)._table, lookup_key, orig_key_r, value_r)
+#endif
+
+/* Suppose to insert a new key-value node to the hash table.
+ If the key already exists, assert-crash. */
+void hash_table_insert(struct hash_table *table, void *key, void *value);
+/* If the key doesn't exists, do the exact same as hash_table_insert()
+ If the key already exists, preserve the original key and update only the value.*/
+void hash_table_update(struct hash_table *table, void *key, void *value);
+#define hash_table_insert(table, key, value) \
+ TYPE_CHECKS(void, \
+ COMPILE_ERROR_IF_TYPES_NOT_COMPATIBLE((table)._key, key) || \
+ COMPILE_ERROR_IF_TYPES_NOT_COMPATIBLE((table)._value, value), \
+ hash_table_insert((table)._table, (void *)(key), (void *)(value)))
+#define hash_table_update(table, key, value) \
+ TYPE_CHECKS(void, \
+ COMPILE_ERROR_IF_TYPES_NOT_COMPATIBLE((table)._key, key) || \
+ COMPILE_ERROR_IF_TYPES_NOT_COMPATIBLE((table)._value, value), \
+ hash_table_update((table)._table, (void *)(key), (void *)(value)))
+
+bool hash_table_try_remove(struct hash_table *table, const void *key);
+#define hash_table_try_remove(table, key) \
+ TYPE_CHECKS(bool, \
+ COMPILE_ERROR_IF_TYPES2_NOT_COMPATIBLE((table)._const_key, (table)._key, key), \
+ hash_table_try_remove((table)._table, (const void *)(key)))
+#define hash_table_remove(table, key) \
+ STMT_START { \
+ if (unlikely(!hash_table_try_remove(table, key))) \
+ i_panic("key not found from hash"); \
+ } STMT_END
+unsigned int hash_table_count(const struct hash_table *table) ATTR_PURE;
+#define hash_table_count(table) \
+ hash_table_count((table)._table)
+
+/* Iterates through all nodes in hash table. You may safely call hash_table_*()
+ functions while iterating, but if you add any new nodes, they may or may
+ not be called for in this iteration. */
+struct hash_iterate_context *hash_table_iterate_init(struct hash_table *table);
+#define hash_table_iterate_init(table) \
+ hash_table_iterate_init((table)._table)
+bool hash_table_iterate(struct hash_iterate_context *ctx,
+ void **key_r, void **value_r);
+#ifndef __cplusplus
+# define hash_table_iterate(ctx, table, key_r, value_r) \
+ TYPE_CHECKS(bool, \
+ COMPILE_ERROR_IF_TYPES_NOT_COMPATIBLE((table)._keyp, key_r) || \
+ COMPILE_ERROR_IF_TRUE(sizeof(*(key_r)) != sizeof(void *)) || \
+ COMPILE_ERROR_IF_TRUE(sizeof(*(value_r)) != sizeof(void *)) || \
+ COMPILE_ERROR_IF_TYPES_NOT_COMPATIBLE((table)._valuep, value_r), \
+ hash_table_iterate(ctx, (void *)(key_r), (void *)(value_r)))
+#else
+/* C++ requires (void **) casting, but that's not possible with strict
+ aliasing, so .. we'll just disable the type checks */
+# define hash_table_iterate(ctx, table, key_r, value_r) \
+ hash_table_iterate(ctx, key_r, value_r)
+#endif
+
+void hash_table_iterate_deinit(struct hash_iterate_context **ctx);
+
+/* Hash table isn't resized, and removed nodes aren't removed from
+ the list while hash table is freezed. Supports nesting. */
+void hash_table_freeze(struct hash_table *table);
+void hash_table_thaw(struct hash_table *table);
+#define hash_table_freeze(table) \
+ hash_table_freeze((table)._table)
+#define hash_table_thaw(table) \
+ hash_table_thaw((table)._table)
+
+/* Copy all nodes from one hash table to another */
+void hash_table_copy(struct hash_table *dest, struct hash_table *src);
+#define hash_table_copy(table1, table2) \
+ hash_table_copy((table1)._table, (table2)._table)
+
+/* hash function for strings */
+unsigned int str_hash(const char *p) ATTR_PURE;
+unsigned int strcase_hash(const char *p) ATTR_PURE;
+
+/* fast hash function which uppercases a-z. Does not work well
+ with input that consists from non number/letter input, as
+ it works by dropping 0x20. */
+unsigned int strfastcase_hash(const char *p) ATTR_PURE;
+
+/* a generic hash for a given memory block */
+unsigned int mem_hash(const void *p, unsigned int size) ATTR_PURE;
+
+#endif
diff --git a/src/lib/hash2.c b/src/lib/hash2.c
new file mode 100644
index 0000000..a8a23e0
--- /dev/null
+++ b/src/lib/hash2.c
@@ -0,0 +1,242 @@
+/* Copyright (c) 2002-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "primes.h"
+#include "hash2.h"
+
+#define HASH_TABLE_MIN_SIZE 131
+
+struct hash2_value {
+ struct hash2_value *next;
+ unsigned int key_hash;
+ /* user_data[value_size] */
+};
+ARRAY_DEFINE_TYPE(hash2_value, struct hash2_value *);
+
+struct hash2_table {
+ unsigned int count;
+ unsigned int initial_size;
+ unsigned int hash_table_size;
+ unsigned int value_size;
+
+ pool_t value_pool;
+ struct hash2_value *deleted_values;
+
+ ARRAY_TYPE(hash2_value) hash_table;
+
+ hash2_key_callback_t *key_hash_cb;
+ hash2_cmp_callback_t *key_compare_cb;
+ void *context;
+};
+
+static void hash2_alloc_table(struct hash2_table *hash, unsigned int size)
+{
+ hash->hash_table_size = size;
+
+ i_array_init(&hash->hash_table, hash->hash_table_size);
+ (void)array_idx_get_space(&hash->hash_table, hash->hash_table_size-1);
+}
+
+struct hash2_table *
+hash2_create(unsigned int initial_size, unsigned int value_size,
+ hash2_key_callback_t *key_hash_cb,
+ hash2_cmp_callback_t *key_compare_cb, void *context)
+{
+ struct hash2_table *hash;
+
+ hash = i_new(struct hash2_table, 1);
+ hash->initial_size = I_MAX(initial_size, HASH_TABLE_MIN_SIZE);
+ hash->value_size = value_size;
+ hash->key_hash_cb = key_hash_cb;
+ hash->key_compare_cb = key_compare_cb;
+ hash->context = context;
+
+ hash->value_pool = pool_alloconly_create("hash2 value pool", 16384);
+ hash2_alloc_table(hash, hash->initial_size);
+ return hash;
+}
+
+void hash2_destroy(struct hash2_table **_hash)
+{
+ struct hash2_table *hash = *_hash;
+
+ *_hash = NULL;
+ array_free(&hash->hash_table);
+ pool_unref(&hash->value_pool);
+ i_free(hash);
+}
+
+void hash2_clear(struct hash2_table *hash)
+{
+ array_free(&hash->hash_table);
+ hash2_alloc_table(hash, hash->initial_size);
+ p_clear(hash->value_pool);
+ hash->count = 0;
+ hash->deleted_values = NULL;
+}
+
+static void hash2_resize(struct hash2_table *hash, bool grow)
+{
+ ARRAY_TYPE(hash2_value) old_hash_table;
+ struct hash2_value *old_hash, *value, **valuep, *next;
+ unsigned int next_size, old_count, i, idx;
+ float nodes_per_list;
+
+ nodes_per_list = (float)hash->count / (float)hash->hash_table_size;
+ if (nodes_per_list > 0.3 && nodes_per_list < 2.0)
+ return;
+
+ next_size = I_MAX(primes_closest(hash->count + 1), hash->initial_size);
+ if (hash->hash_table_size >= next_size &&
+ (grow || next_size == hash->hash_table_size))
+ return;
+
+ old_hash_table = hash->hash_table;
+ hash2_alloc_table(hash, next_size);
+
+ old_count = array_count(&old_hash_table);
+ for (i = 0; i < old_count; i++) {
+ old_hash = array_idx_elem(&old_hash_table, i);
+ for (value = old_hash; value != NULL; value = next) {
+ next = value->next;
+
+ idx = value->key_hash % hash->hash_table_size;
+ valuep = array_idx_modifiable(&hash->hash_table, idx);
+ value->next = *valuep;
+ *valuep = value;
+ }
+ }
+ array_free(&old_hash_table);
+}
+
+void *hash2_lookup(const struct hash2_table *hash, const void *key)
+{
+ unsigned int key_hash = hash->key_hash_cb(key);
+ struct hash2_value *value;
+ void *user_value;
+
+ value = array_idx_elem(&hash->hash_table,
+ key_hash % hash->hash_table_size);
+ while (value != NULL) {
+ if (value->key_hash == key_hash) {
+ user_value = value + 1;
+ if (hash->key_compare_cb(key, user_value,
+ hash->context))
+ return user_value;
+ }
+ value = value->next;
+ }
+ return NULL;
+}
+
+void *hash2_iterate(const struct hash2_table *hash,
+ unsigned int key_hash, struct hash2_iter *iter)
+{
+ struct hash2_value *value;
+
+ if (iter->value == NULL) {
+ iter->key_hash = key_hash;
+ value = array_idx_elem(&hash->hash_table,
+ key_hash % hash->hash_table_size);
+ iter->next_value = value;
+ }
+ while (iter->next_value != NULL) {
+ if (iter->next_value->key_hash == key_hash) {
+ iter->value = iter->next_value;
+ iter->next_value = iter->next_value->next;
+ return iter->value + 1;
+ }
+ iter->next_value = iter->next_value->next;
+ }
+ return NULL;
+}
+
+void *hash2_insert(struct hash2_table *hash, const void *key)
+{
+ return hash2_insert_hash(hash, hash->key_hash_cb(key));
+}
+
+void *hash2_insert_hash(struct hash2_table *hash, unsigned int key_hash)
+{
+ struct hash2_value *value, **valuep;
+
+ hash2_resize(hash, TRUE);
+
+ if (hash->deleted_values != NULL) {
+ value = hash->deleted_values;
+ hash->deleted_values = value->next;
+ value->next = NULL;
+ memset(value + 1, 0, hash->value_size);
+ } else {
+ value = p_malloc(hash->value_pool,
+ sizeof(*value) + hash->value_size);
+ }
+ value->key_hash = key_hash;
+
+ valuep = array_idx_modifiable(&hash->hash_table,
+ key_hash % hash->hash_table_size);
+ value->next = *valuep;
+ *valuep = value;
+
+ hash->count++;
+ return value + 1;
+}
+
+static void
+hash2_remove_value_p(struct hash2_table *hash, struct hash2_value **valuep)
+{
+ struct hash2_value *deleted_value;
+
+ deleted_value = *valuep;
+ *valuep = deleted_value->next;
+
+ deleted_value->next = hash->deleted_values;
+ hash->deleted_values = deleted_value;
+
+ hash->count--;
+}
+
+void hash2_remove(struct hash2_table *hash, const void *key)
+{
+ unsigned int key_hash = hash->key_hash_cb(key);
+ struct hash2_value **valuep;
+
+ valuep = array_idx_modifiable(&hash->hash_table,
+ key_hash % hash->hash_table_size);
+ while (*valuep != NULL) {
+ if ((*valuep)->key_hash == key_hash &&
+ hash->key_compare_cb(key, (*valuep) + 1, hash->context)) {
+ hash2_remove_value_p(hash, valuep);
+ hash2_resize(hash, FALSE);
+ return;
+ }
+ valuep = &(*valuep)->next;
+ }
+ i_panic("hash2_remove(): key not found");
+}
+
+void hash2_remove_iter(struct hash2_table *hash, struct hash2_iter *iter)
+{
+ struct hash2_value **valuep, *next;
+
+ valuep = array_idx_modifiable(&hash->hash_table,
+ iter->key_hash % hash->hash_table_size);
+ while (*valuep != NULL) {
+ if (*valuep == iter->value) {
+ next = (*valuep)->next;
+ /* don't allow resizing, otherwise iterating would
+ break completely */
+ hash2_remove_value_p(hash, valuep);
+ iter->next_value = next;
+ return;
+ }
+ valuep = &(*valuep)->next;
+ }
+ i_panic("hash2_remove_value(): key/value not found");
+}
+
+unsigned int hash2_count(const struct hash2_table *hash)
+{
+ return hash->count;
+}
diff --git a/src/lib/hash2.h b/src/lib/hash2.h
new file mode 100644
index 0000000..d7febe6
--- /dev/null
+++ b/src/lib/hash2.h
@@ -0,0 +1,56 @@
+#ifndef HASH2_H
+#define HASH2_H
+
+#include "hash.h"
+
+struct hash2_iter {
+ struct hash2_value *value, *next_value;
+ unsigned int key_hash;
+};
+
+/* Returns hash code for the key. */
+typedef unsigned int hash2_key_callback_t(const void *key);
+/* Returns TRUE if the key matches the value. */
+typedef bool hash2_cmp_callback_t(const void *key, const void *value,
+ void *context);
+
+/* Create a new hash table. If initial_size is 0, the default value is used. */
+struct hash2_table *
+hash2_create(unsigned int initial_size, unsigned int value_size,
+ hash2_key_callback_t *key_hash_cb,
+ hash2_cmp_callback_t *key_compare_cb, void *context) ATTR_NULL(5);
+void hash2_destroy(struct hash2_table **hash);
+/* Remove all nodes from hash table. */
+void hash2_clear(struct hash2_table *hash);
+
+void *hash2_lookup(const struct hash2_table *hash, const void *key) ATTR_PURE;
+/* Iterate through all nodes with the given hash. iter must initially be
+ zero-filled. */
+void *hash2_iterate(const struct hash2_table *hash,
+ unsigned int key_hash, struct hash2_iter *iter);
+/* Insert node to the hash table and returns pointer to the value that can be
+ written to. Assumes it doesn't already exist (or that a duplicate entry
+ is wanted). */
+void *hash2_insert(struct hash2_table *hash, const void *key);
+/* Like hash2_insert(), but insert directly using a hash. */
+void *hash2_insert_hash(struct hash2_table *hash, unsigned int key_hash);
+/* Remove a node. */
+void hash2_remove(struct hash2_table *hash, const void *key);
+/* Remove the last node iterator returned. Iterating continues from the next
+ node. */
+void hash2_remove_iter(struct hash2_table *hash, struct hash2_iter *iter);
+/* Return the number of nodes in hash table. */
+unsigned int hash2_count(const struct hash2_table *hash) ATTR_PURE;
+
+/* can be used with string keys */
+static inline bool hash2_strcmp(const void *a, const void *b, void *ctx ATTR_UNUSED)
+{
+ return strcmp(a, b) == 0;
+}
+
+static inline unsigned int hash2_str_hash(const void *key)
+{
+ return str_hash(key);
+}
+
+#endif
diff --git a/src/lib/hex-binary.c b/src/lib/hex-binary.c
new file mode 100644
index 0000000..a6d7849
--- /dev/null
+++ b/src/lib/hex-binary.c
@@ -0,0 +1,85 @@
+/* Copyright (c) 2002-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "buffer.h"
+#include "hex-binary.h"
+
+static void
+binary_to_hex_case(unsigned char *dest, const unsigned char *data,
+ size_t size, bool ucase)
+{
+ unsigned char *p;
+ char base_char;
+ size_t i;
+ int value;
+
+ /* @UNSAFE */
+ base_char = ucase ? 'A' : 'a';
+
+ p = dest;
+ for (i = 0; i < size; i++) {
+ value = data[i] >> 4;
+ *p++ = value < 10 ? value + '0' : value - 10 + base_char;
+
+ value = data[i] & 0x0f;
+ *p++ = value < 10 ? value + '0' : value - 10 + base_char;
+ }
+}
+
+const char *binary_to_hex(const unsigned char *data, size_t size)
+{
+ unsigned char *dest = t_malloc_no0(MALLOC_MULTIPLY(size, 2) + 1);
+
+ binary_to_hex_case(dest, data, size, FALSE);
+ dest[size*2] = '\0';
+ return (char *)dest;
+}
+
+const char *binary_to_hex_ucase(const unsigned char *data, size_t size)
+{
+ unsigned char *dest = t_malloc_no0(MALLOC_MULTIPLY(size, 2) + 1);
+
+ binary_to_hex_case(dest, data, size, TRUE);
+ dest[size*2] = '\0';
+ return (char *)dest;
+}
+
+void binary_to_hex_append(string_t *dest, const unsigned char *data,
+ size_t size)
+{
+ unsigned char *buf;
+
+ buf = buffer_append_space_unsafe(dest, size * 2);
+ binary_to_hex_case(buf, data, size, FALSE);
+}
+
+int hex_to_binary(const char *data, buffer_t *dest)
+{
+ int value;
+
+ while (*data != '\0') {
+ if (*data >= '0' && *data <= '9')
+ value = (*data - '0') << 4;
+ else if (*data >= 'a' && *data <= 'f')
+ value = (*data - 'a' + 10) << 4;
+ else if (*data >= 'A' && *data <= 'F')
+ value = (*data - 'A' + 10) << 4;
+ else
+ return -1;
+
+ data++;
+ if (*data >= '0' && *data <= '9')
+ value |= *data - '0';
+ else if (*data >= 'a' && *data <= 'f')
+ value |= *data - 'a' + 10;
+ else if (*data >= 'A' && *data <= 'F')
+ value |= *data - 'A' + 10;
+ else
+ return -1;
+
+ buffer_append_c(dest, value);
+ data++;
+ }
+
+ return 0;
+}
diff --git a/src/lib/hex-binary.h b/src/lib/hex-binary.h
new file mode 100644
index 0000000..24c3a39
--- /dev/null
+++ b/src/lib/hex-binary.h
@@ -0,0 +1,15 @@
+#ifndef HEX_BINARY_H
+#define HEX_BINARY_H
+
+/* Convert binary to hex digits allocating return value from data stack */
+const char *binary_to_hex(const unsigned char *data, size_t size);
+const char *binary_to_hex_ucase(const unsigned char *data, size_t size);
+
+void binary_to_hex_append(string_t *dest, const unsigned char *data,
+ size_t size);
+
+/* Convert hex to binary. data and dest may point to same value.
+ Returns 0 if all ok, -1 if data is invalid. */
+int hex_to_binary(const char *data, buffer_t *dest);
+
+#endif
diff --git a/src/lib/hex-dec.c b/src/lib/hex-dec.c
new file mode 100644
index 0000000..1c68001
--- /dev/null
+++ b/src/lib/hex-dec.c
@@ -0,0 +1,38 @@
+/* Copyright (c) 2005-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "hex-dec.h"
+
+void dec2hex(unsigned char *hexstr, uintmax_t dec, unsigned int hexstr_size)
+{
+ unsigned int i;
+
+ for (i = 0; i < hexstr_size; i++) {
+ unsigned int value = dec & 0x0f;
+ if (value < 10)
+ hexstr[hexstr_size-i-1] = value + '0';
+ else
+ hexstr[hexstr_size-i-1] = value - 10 + 'A';
+ dec >>= 4;
+ }
+}
+
+uintmax_t hex2dec(const unsigned char *data, unsigned int len)
+{
+ unsigned int i;
+ uintmax_t value = 0;
+
+ for (i = 0; i < len; i++) {
+ value = value*0x10;
+ if (data[i] >= '0' && data[i] <= '9')
+ value += data[i]-'0';
+ else if (data[i] >= 'A' && data[i] <= 'F')
+ value += data[i]-'A' + 10;
+ else if (data[i] >= 'a' && data[i] <= 'f')
+ value += data[i]-'a' + 10;
+ else
+ return 0;
+ }
+ return value;
+}
+
diff --git a/src/lib/hex-dec.h b/src/lib/hex-dec.h
new file mode 100644
index 0000000..33e87bb
--- /dev/null
+++ b/src/lib/hex-dec.h
@@ -0,0 +1,12 @@
+#ifndef HEX_DEC_H
+#define HEX_DEC_H
+
+#define DEC2HEX(hexstr, str) \
+ dec2hex(hexstr, str, sizeof(hexstr))
+
+/* Decimal -> hex string translation. The result isn't NUL-terminated. */
+void dec2hex(unsigned char *hexstr, uintmax_t dec, unsigned int hexstr_size);
+/* Parses hex string and returns its decimal value, or 0 in case of errors */
+uintmax_t hex2dec(const unsigned char *data, unsigned int len) ATTR_PURE;
+
+#endif
diff --git a/src/lib/hmac-cram-md5.c b/src/lib/hmac-cram-md5.c
new file mode 100644
index 0000000..46d73c4
--- /dev/null
+++ b/src/lib/hmac-cram-md5.c
@@ -0,0 +1,65 @@
+/*
+ * CRAM-MD5 (RFC 2195) compatibility code
+ * Copyright (c) 2003 Joshua Goodall <joshua@roughtrade.net>
+ *
+ * This software is released under the MIT license.
+ */
+
+#include "lib.h"
+#include "md5.h"
+#include "hmac-cram-md5.h"
+
+void hmac_md5_get_cram_context(struct hmac_context *_hmac_ctx,
+ unsigned char context_digest[CRAM_MD5_CONTEXTLEN])
+{
+ struct hmac_context_priv *hmac_ctx = &_hmac_ctx->u.priv;
+ unsigned char *cdp;
+
+ struct md5_context *ctx = (void*)hmac_ctx->ctx;
+ struct md5_context *ctxo = (void*)hmac_ctx->ctxo;
+
+#define CDPUT(p, c) STMT_START { \
+ *(p)++ = (c) & 0xff; \
+ *(p)++ = (c) >> 8 & 0xff; \
+ *(p)++ = (c) >> 16 & 0xff; \
+ *(p)++ = (c) >> 24 & 0xff; \
+} STMT_END
+ cdp = context_digest;
+ CDPUT(cdp, ctxo->a);
+ CDPUT(cdp, ctxo->b);
+ CDPUT(cdp, ctxo->c);
+ CDPUT(cdp, ctxo->d);
+ CDPUT(cdp, ctx->a);
+ CDPUT(cdp, ctx->b);
+ CDPUT(cdp, ctx->c);
+ CDPUT(cdp, ctx->d);
+}
+
+void hmac_md5_set_cram_context(struct hmac_context *_hmac_ctx,
+ const unsigned char context_digest[CRAM_MD5_CONTEXTLEN])
+{
+ struct hmac_context_priv *hmac_ctx = &_hmac_ctx->u.priv;
+ const unsigned char *cdp;
+
+ struct md5_context *ctx = (void*)hmac_ctx->ctx;
+ struct md5_context *ctxo = (void*)hmac_ctx->ctxo;
+
+#define CDGET(p, c) STMT_START { \
+ (c) = (*p++); \
+ (c) += (*p++ << 8); \
+ (c) += (*p++ << 16); \
+ (c) += ((uint32_t)(*p++) << 24); \
+} STMT_END
+ cdp = context_digest;
+ CDGET(cdp, ctxo->a);
+ CDGET(cdp, ctxo->b);
+ CDGET(cdp, ctxo->c);
+ CDGET(cdp, ctxo->d);
+ CDGET(cdp, ctx->a);
+ CDGET(cdp, ctx->b);
+ CDGET(cdp, ctx->c);
+ CDGET(cdp, ctx->d);
+
+ ctxo->lo = ctx->lo = 64;
+ ctxo->hi = ctx->hi = 0;
+}
diff --git a/src/lib/hmac-cram-md5.h b/src/lib/hmac-cram-md5.h
new file mode 100644
index 0000000..5b0a384
--- /dev/null
+++ b/src/lib/hmac-cram-md5.h
@@ -0,0 +1,14 @@
+#ifndef HMAC_CRAM_MD5_H
+#define HMAC_CRAM_MD5_H
+
+#include "hmac.h"
+
+#define CRAM_MD5_CONTEXTLEN 32
+
+void hmac_md5_get_cram_context(struct hmac_context *ctx,
+ unsigned char context_digest[CRAM_MD5_CONTEXTLEN]);
+void hmac_md5_set_cram_context(struct hmac_context *ctx,
+ const unsigned char context_digest[CRAM_MD5_CONTEXTLEN]);
+
+
+#endif
diff --git a/src/lib/hmac.c b/src/lib/hmac.c
new file mode 100644
index 0000000..0138a4a
--- /dev/null
+++ b/src/lib/hmac.c
@@ -0,0 +1,152 @@
+/*
+ * HMAC (RFC-2104) implementation.
+ *
+ * Copyright (c) 2004 Andrey Panin <pazke@donpac.ru>
+ * Copyright (c) 2011-2016 Florian Zeitz <florob@babelmonkeys.de>
+ *
+ * This software is released under the MIT license.
+ */
+
+#include "lib.h"
+#include "hmac.h"
+#include "safe-memset.h"
+#include "buffer.h"
+
+#include "hex-binary.h"
+
+void hmac_init(struct hmac_context *_ctx, const unsigned char *key,
+ size_t key_len, const struct hash_method *meth)
+{
+ struct hmac_context_priv *ctx = &_ctx->u.priv;
+ unsigned int i;
+ unsigned char k_ipad[meth->block_size];
+ unsigned char k_opad[meth->block_size];
+ unsigned char hashedkey[meth->digest_size];
+
+ i_assert(meth->context_size <= HMAC_MAX_CONTEXT_SIZE);
+
+ ctx->hash = meth;
+
+ if (key_len > meth->block_size) {
+ meth->init(ctx->ctx);
+ meth->loop(ctx->ctx, key, key_len);
+ meth->result(ctx->ctx, hashedkey);
+ key = hashedkey;
+ key_len = meth->digest_size;
+ }
+
+ memcpy(k_ipad, key, key_len);
+ memset(k_ipad + key_len, 0, meth->block_size - key_len);
+ memcpy(k_opad, k_ipad, meth->block_size);
+
+ for (i = 0; i < meth->block_size; i++) {
+ k_ipad[i] ^= 0x36;
+ k_opad[i] ^= 0x5c;
+ }
+
+ meth->init(ctx->ctx);
+ meth->loop(ctx->ctx, k_ipad, meth->block_size);
+ meth->init(ctx->ctxo);
+ meth->loop(ctx->ctxo, k_opad, meth->block_size);
+
+ safe_memset(k_ipad, 0, meth->block_size);
+ safe_memset(k_opad, 0, meth->block_size);
+}
+
+void hmac_final(struct hmac_context *_ctx, unsigned char *digest)
+{
+ struct hmac_context_priv *ctx = &_ctx->u.priv;
+
+ ctx->hash->result(ctx->ctx, digest);
+
+ ctx->hash->loop(ctx->ctxo, digest, ctx->hash->digest_size);
+ ctx->hash->result(ctx->ctxo, digest);
+}
+
+buffer_t *t_hmac_data(const struct hash_method *meth,
+ const unsigned char *key, size_t key_len,
+ const void *data, size_t data_len)
+{
+ struct hmac_context ctx;
+ i_assert(meth != NULL);
+ i_assert(key != NULL && key_len > 0);
+ i_assert(data != NULL || data_len == 0);
+
+ buffer_t *res = t_buffer_create(meth->digest_size);
+ hmac_init(&ctx, key, key_len, meth);
+ if (data_len > 0)
+ hmac_update(&ctx, data, data_len);
+ unsigned char *buf = buffer_get_space_unsafe(res, 0, meth->digest_size);
+ hmac_final(&ctx, buf);
+ return res;
+}
+
+buffer_t *t_hmac_buffer(const struct hash_method *meth,
+ const unsigned char *key, size_t key_len,
+ const buffer_t *data)
+{
+ return t_hmac_data(meth, key, key_len, data->data, data->used);
+}
+
+buffer_t *t_hmac_str(const struct hash_method *meth,
+ const unsigned char *key, size_t key_len,
+ const char *data)
+{
+ return t_hmac_data(meth, key, key_len, data, strlen(data));
+}
+
+void hmac_hkdf(const struct hash_method *method,
+ const unsigned char *salt, size_t salt_len,
+ const unsigned char *ikm, size_t ikm_len,
+ const unsigned char *info, size_t info_len,
+ buffer_t *okm_r, size_t okm_len)
+{
+ i_assert(method != NULL);
+ i_assert(okm_len < 255*method->digest_size);
+ struct hmac_context key_mac;
+ struct hmac_context info_mac;
+ size_t remain = okm_len;
+ unsigned char prk[method->digest_size];
+ unsigned char okm[method->digest_size];
+ /* N = ceil(L/HashLen) */
+ unsigned int rounds = (okm_len + method->digest_size - 1)/method->digest_size;
+
+ /* salt and info can be NULL */
+ i_assert(salt != NULL || salt_len == 0);
+ i_assert(info != NULL || info_len == 0);
+
+ i_assert(ikm != NULL && ikm_len > 0);
+ i_assert(okm_r != NULL && okm_len > 0);
+
+ /* but they still need valid pointer, reduces
+ complains from static analysers */
+ if (salt == NULL)
+ salt = &uchar_nul;
+ if (info == NULL)
+ info = &uchar_nul;
+
+ /* extract */
+ hmac_init(&key_mac, salt, salt_len, method);
+ hmac_update(&key_mac, ikm, ikm_len);
+ hmac_final(&key_mac, prk);
+
+ /* expand */
+ for (unsigned int i = 0; remain > 0 && i < rounds; i++) {
+ unsigned char round = (i+1);
+ size_t amt = remain;
+ if (amt > method->digest_size)
+ amt = method->digest_size;
+ hmac_init(&info_mac, prk, method->digest_size, method);
+ if (i > 0)
+ hmac_update(&info_mac, okm, method->digest_size);
+ hmac_update(&info_mac, info, info_len);
+ hmac_update(&info_mac, &round, 1);
+ memset(okm, 0, method->digest_size);
+ hmac_final(&info_mac, okm);
+ buffer_append(okm_r, okm, amt);
+ remain -= amt;
+ }
+
+ safe_memset(prk, 0, sizeof(prk));
+ safe_memset(okm, 0, sizeof(okm));
+}
diff --git a/src/lib/hmac.h b/src/lib/hmac.h
new file mode 100644
index 0000000..b32c039
--- /dev/null
+++ b/src/lib/hmac.h
@@ -0,0 +1,65 @@
+#ifndef HMAC_H
+#define HMAC_H
+
+#include "hash-method.h"
+#include "sha1.h"
+#include "sha2.h"
+
+#define HMAC_MAX_CONTEXT_SIZE sizeof(struct sha512_ctx)
+
+struct hmac_context_priv {
+ char ctx[HMAC_MAX_CONTEXT_SIZE];
+ char ctxo[HMAC_MAX_CONTEXT_SIZE];
+ const struct hash_method *hash;
+};
+
+struct hmac_context {
+ union {
+ struct hmac_context_priv priv;
+ uint64_t padding_requirement;
+ } u;
+};
+
+void hmac_init(struct hmac_context *ctx, const unsigned char *key,
+ size_t key_len, const struct hash_method *meth);
+void hmac_final(struct hmac_context *ctx, unsigned char *digest);
+
+
+static inline void
+hmac_update(struct hmac_context *_ctx, const void *data, size_t size)
+{
+ struct hmac_context_priv *ctx = &_ctx->u.priv;
+
+ ctx->hash->loop(ctx->ctx, data, size);
+}
+
+buffer_t *t_hmac_data(const struct hash_method *meth,
+ const unsigned char *key, size_t key_len,
+ const void *data, size_t data_len);
+buffer_t *t_hmac_buffer(const struct hash_method *meth,
+ const unsigned char *key, size_t key_len,
+ const buffer_t *data);
+buffer_t *t_hmac_str(const struct hash_method *meth,
+ const unsigned char *key, size_t key_len,
+ const char *data);
+
+void hmac_hkdf(const struct hash_method *method,
+ const unsigned char *salt, size_t salt_len,
+ const unsigned char *ikm, size_t ikm_len,
+ const unsigned char *info, size_t info_len,
+ buffer_t *okm_r, size_t okm_len);
+
+static inline buffer_t *
+t_hmac_hkdf(const struct hash_method *method,
+ const unsigned char *salt, size_t salt_len,
+ const unsigned char *ikm, size_t ikm_len,
+ const unsigned char *info, size_t info_len,
+ size_t okm_len)
+{
+ buffer_t *okm_buffer = t_buffer_create(okm_len);
+ hmac_hkdf(method, salt, salt_len, ikm, ikm_len, info, info_len,
+ okm_buffer, okm_len);
+ return okm_buffer;
+}
+
+#endif
diff --git a/src/lib/home-expand.c b/src/lib/home-expand.c
new file mode 100644
index 0000000..282ede9
--- /dev/null
+++ b/src/lib/home-expand.c
@@ -0,0 +1,72 @@
+/* Copyright (c) 2003-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "ipwd.h"
+#include "home-expand.h"
+
+
+int home_try_expand(const char **_path)
+{
+ const char *path = *_path;
+ const char *name, *home, *p;
+ struct passwd pw;
+
+ if (path == NULL || *path != '~')
+ return 0;
+
+ path++;
+ if (*path == '/' || *path == '\0') {
+ home = getenv("HOME");
+ if (*path != '\0') path++;
+ } else {
+ p = strchr(path, '/');
+ if (p == NULL) {
+ name = path;
+ path = "";
+ } else {
+ name = t_strdup_until(path, p);
+ path = p+1;
+ }
+ switch (i_getpwnam(name, &pw)) {
+ case -1:
+ i_error("getpwnam(%s) failed: %m", name);
+ home = NULL;
+ break;
+ case 0:
+ home = NULL;
+ break;
+ default:
+ home = pw.pw_dir;
+ break;
+ }
+ }
+
+ if (home == NULL)
+ return -1;
+
+ if (*path == '\0')
+ *_path = t_strdup(home);
+ else
+ *_path = t_strconcat(home, "/", path, NULL);
+ return 0;
+}
+
+const char *home_expand(const char *path)
+{
+ (void)home_try_expand(&path);
+ return path;
+}
+
+const char *home_expand_tilde(const char *path, const char *home)
+{
+ if (path == NULL || *path != '~')
+ return path;
+
+ if (path[1] == '\0')
+ return home;
+ if (path[1] != '/')
+ return path;
+
+ /* ~/ used */
+ return t_strconcat(home, path + 1, NULL);
+}
diff --git a/src/lib/home-expand.h b/src/lib/home-expand.h
new file mode 100644
index 0000000..5ba2ff7
--- /dev/null
+++ b/src/lib/home-expand.h
@@ -0,0 +1,12 @@
+#ifndef HOME_EXPAND_H
+#define HOME_EXPAND_H
+
+/* expand ~/ or ~user/ in beginning of path. If user is unknown, the original
+ path is returned without modification. */
+const char *home_expand(const char *path);
+/* Returns 0 if ok, -1 if user wasn't found. */
+int home_try_expand(const char **path);
+/* Expand ~/ in the beginning of the path with the give home directory. */
+const char *home_expand_tilde(const char *path, const char *home);
+
+#endif
diff --git a/src/lib/hook-build.c b/src/lib/hook-build.c
new file mode 100644
index 0000000..3b4a409
--- /dev/null
+++ b/src/lib/hook-build.c
@@ -0,0 +1,121 @@
+/* Copyright (c) 2009-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "llist.h"
+#include "hook-build.h"
+
+struct hook_stack {
+ struct hook_stack *prev, *next;
+
+ /* Pointer to vfuncs struct. This assumes that a struct containing
+ function pointers equals to an array of function pointers. Not
+ ANSI-C, but should work in all OSes supported by Dovecot. Much
+ easier anyway than doing this work manually.. */
+ void (**vfuncs)();
+ /* nonzero in the areas where vfuncs has been changed */
+ void (**mask)();
+};
+
+struct hook_build_context {
+ pool_t pool;
+ /* size of the vfuncs struct */
+ size_t size;
+ /* number of function pointers in the struct */
+ unsigned int count;
+
+ struct hook_stack *head, *tail;
+};
+
+static void hook_build_append(struct hook_build_context *ctx, void (**vfuncs)())
+{
+ struct hook_stack *stack;
+
+ stack = p_new(ctx->pool, struct hook_stack, 1);
+ stack->vfuncs = vfuncs;
+ stack->mask = p_malloc(ctx->pool, ctx->size);
+ DLLIST2_APPEND(&ctx->head, &ctx->tail, stack);
+}
+
+struct hook_build_context *hook_build_init(void (**vfuncs)(), size_t size)
+{
+ struct hook_build_context *ctx;
+ pool_t pool;
+
+ i_assert((size % sizeof(void (*)())) == 0);
+
+ pool = pool_alloconly_create("hook build context", 2048);
+ ctx = p_new(pool, struct hook_build_context, 1);
+ ctx->pool = pool;
+ ctx->size = size;
+ ctx->count = size / sizeof(void (*)());
+ hook_build_append(ctx, vfuncs);
+ return ctx;
+}
+
+static void
+hook_update_mask(struct hook_build_context *ctx, struct hook_stack *stack,
+ void (**vlast)())
+{
+ unsigned int i;
+
+ for (i = 0; i < ctx->count; i++) {
+ if (stack->vfuncs[i] != vlast[i]) {
+ i_assert(stack->vfuncs[i] != NULL);
+ stack->mask[i] = stack->vfuncs[i];
+ }
+ }
+}
+
+static void
+hook_copy_stack(struct hook_build_context *ctx, struct hook_stack *stack)
+{
+ unsigned int i;
+
+ i_assert(stack->next != NULL);
+
+ for (i = 0; i < ctx->count; i++) {
+ if (stack->mask[i] == NULL) {
+ stack->vfuncs[i] = stack->next->vfuncs[i];
+ stack->mask[i] = stack->next->mask[i];
+ }
+ }
+}
+
+void hook_build_update(struct hook_build_context *ctx, void *_vlast)
+{
+ void (**vlast)() = _vlast;
+ struct hook_stack *stack;
+
+ if (ctx->tail->vfuncs == vlast) {
+ /* no vfuncs overridden */
+ return;
+ }
+
+ /* ctx->vfuncs_stack->vfuncs points to the root vfuncs,
+ ctx->vfuncs_stack->next->vfuncs points to the first super function
+ that is being called, and so on.
+
+ the previous plugin added its vfuncs to the stack tail.
+ vlast contains the previous plugin's super vfuncs, which is where
+ the next plugin should put its own vfuncs.
+
+ first we'll need to figure out what vfuncs the previous plugin
+ changed and update the mask */
+ hook_update_mask(ctx, ctx->tail, vlast);
+
+ /* now go up in the stack as long as the mask isn't set,
+ and update the vfuncs */
+ for (stack = ctx->tail->prev; stack != NULL; stack = stack->prev)
+ hook_copy_stack(ctx, stack);
+
+ /* add vlast to stack */
+ hook_build_append(ctx, vlast);
+}
+
+void hook_build_deinit(struct hook_build_context **_ctx)
+{
+ struct hook_build_context *ctx = *_ctx;
+ *_ctx = NULL;
+ pool_unref(&ctx->pool);
+}
diff --git a/src/lib/hook-build.h b/src/lib/hook-build.h
new file mode 100644
index 0000000..03c1579
--- /dev/null
+++ b/src/lib/hook-build.h
@@ -0,0 +1,19 @@
+/* Copyright (c) 2017-2018 Dovecot authors, see the included COPYING file */
+#ifndef HOOK_BUILD_H
+#define HOOK_BUILD_H 1
+
+struct hook_build_context;
+struct hook_stack;
+
+/* Initialize new hook building context, vfuncs should point to
+ the functions table that is being manipulated, and size should be
+ the size of this table. */
+struct hook_build_context *hook_build_init(void (**vfuncs)(), size_t size);
+
+/* This is called after a hook may have updated vfuncs */
+void hook_build_update(struct hook_build_context *ctx, void *_vlast);
+
+/* Free memory used by build context */
+void hook_build_deinit(struct hook_build_context **_ctx);
+
+#endif
diff --git a/src/lib/hostpid.c b/src/lib/hostpid.c
new file mode 100644
index 0000000..3e91ade
--- /dev/null
+++ b/src/lib/hostpid.c
@@ -0,0 +1,69 @@
+/* Copyright (c) 2002-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "hostpid.h"
+
+#include <unistd.h>
+#include <netdb.h>
+
+#define HOSTNAME_DISALLOWED_CHARS "/\r\n\t"
+
+const char *my_hostname = NULL;
+const char *my_pid = NULL;
+
+static char *my_hostname_dup = NULL;
+static char *my_domain = NULL;
+
+void hostpid_init(void)
+{
+ static char pid[MAX_INT_STRLEN];
+ char hostname[256];
+ const char *value;
+
+ /* allow calling hostpid_init() multiple times to reset hostname */
+ i_free_and_null(my_hostname_dup);
+ i_free_and_null(my_domain);
+
+ value = getenv(MY_HOSTNAME_ENV);
+ if (value == NULL) {
+ if (gethostname(hostname, sizeof(hostname)-1) < 0)
+ i_fatal("gethostname() failed: %m");
+ hostname[sizeof(hostname)-1] = '\0';
+ value = hostname;
+ }
+
+ if (value[0] == '\0' ||
+ strcspn(value, HOSTNAME_DISALLOWED_CHARS) != strlen(value))
+ i_fatal("Invalid system hostname: '%s'", value);
+ my_hostname_dup = i_strdup(value);
+ my_hostname = my_hostname_dup;
+
+ i_snprintf(pid, sizeof(pid), "%lld", (long long)getpid());
+ my_pid = pid;
+}
+
+void hostpid_deinit(void)
+{
+ i_free(my_hostname_dup);
+ i_free(my_domain);
+}
+
+const char *my_hostdomain(void)
+{
+ struct hostent *hent;
+ const char *name;
+
+ if (my_domain == NULL) {
+ name = getenv(MY_HOSTDOMAIN_ENV);
+ if (name == NULL) {
+ hent = gethostbyname(my_hostname);
+ name = hent != NULL ? hent->h_name : NULL;
+ if (name == NULL) {
+ /* failed, use just the hostname */
+ name = my_hostname;
+ }
+ }
+ my_domain = i_strdup(name);
+ }
+ return my_domain;
+}
diff --git a/src/lib/hostpid.h b/src/lib/hostpid.h
new file mode 100644
index 0000000..0581e78
--- /dev/null
+++ b/src/lib/hostpid.h
@@ -0,0 +1,21 @@
+#ifndef HOSTPID_H
+#define HOSTPID_H
+
+/* These environments override the hostname/hostdomain if they're set.
+ Master process normally sets these to child processes. */
+#define MY_HOSTNAME_ENV "DOVECOT_HOSTNAME"
+#define MY_HOSTDOMAIN_ENV "DOVECOT_HOSTDOMAIN"
+
+extern const char *my_hostname;
+extern const char *my_pid;
+
+/* Initializes my_hostname and my_pid. */
+void hostpid_init(void);
+void hostpid_deinit(void);
+
+/* Returns the current host+domain, or if it fails fallback to returning
+ hostname. */
+const char *my_hostdomain(void);
+
+#endif
+
diff --git a/src/lib/imem.c b/src/lib/imem.c
new file mode 100644
index 0000000..a991a92
--- /dev/null
+++ b/src/lib/imem.c
@@ -0,0 +1,78 @@
+/* Copyright (c) 2002-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+
+pool_t default_pool = &static_system_pool;
+
+void *i_malloc(size_t size)
+{
+ return p_malloc(default_pool, size);
+}
+
+void *i_realloc(void *mem, size_t old_size, size_t new_size)
+{
+ return p_realloc(default_pool, mem, old_size, new_size);
+}
+
+char *i_strdup(const char *str)
+{
+ return p_strdup(default_pool, str);
+}
+
+void *i_memdup(const void *data, size_t size)
+{
+ return p_memdup(default_pool, data, size);
+}
+
+char *i_strdup_empty(const char *str)
+{
+ return p_strdup_empty(default_pool, str);
+}
+
+char *i_strdup_until(const void *str, const void *end)
+{
+ return p_strdup_until(default_pool, str, end);
+}
+
+char *i_strndup(const void *str, size_t max_chars)
+{
+ i_assert(str != NULL);
+ return p_strndup(default_pool, str, max_chars);
+}
+
+char *i_strdup_printf(const char *format, ...)
+{
+ va_list args;
+ char *ret;
+
+ va_start(args, format);
+ ret = p_strdup_vprintf(default_pool, format, args);
+ va_end(args);
+ return ret;
+}
+
+char *i_strdup_vprintf(const char *format, va_list args)
+{
+ return p_strdup_vprintf(default_pool, format, args);
+}
+
+char *i_strconcat(const char *str1, ...)
+{
+ va_list args;
+ char *ret;
+ size_t len;
+
+ i_assert(str1 != NULL);
+
+ va_start(args, str1);
+
+ T_BEGIN {
+ const char *temp = vstrconcat(str1, args, &len);
+ t_buffer_alloc(len);
+ ret = p_malloc(default_pool, len);
+ memcpy(ret, temp, len);
+ } T_END;
+
+ va_end(args);
+ return ret;
+}
diff --git a/src/lib/imem.h b/src/lib/imem.h
new file mode 100644
index 0000000..ed8c2eb
--- /dev/null
+++ b/src/lib/imem.h
@@ -0,0 +1,45 @@
+#ifndef IMEM_H
+#define IMEM_H
+
+/* For easy allocation of memory from default memory pool. */
+
+extern pool_t default_pool;
+
+#define i_new(type, count) p_new(default_pool, type, count)
+#define i_realloc_type(mem, type, old_count, new_count) \
+ p_realloc_type(default_pool, mem, type, old_count, new_count)
+
+void *i_malloc(size_t size) ATTR_MALLOC ATTR_RETURNS_NONNULL;
+void *i_realloc(void *mem, size_t old_size, size_t new_size)
+ ATTR_WARN_UNUSED_RESULT ATTR_RETURNS_NONNULL;
+
+/* i_free() and i_free_and_null() are now guaranteed to both set mem=NULL,
+ so either one of them can be used. */
+#ifndef STATIC_CHECKER
+# define i_free(mem) p_free_and_null(default_pool, mem)
+#else
+# define i_free(mem) \
+ STMT_START { \
+ pool_system_free(default_pool, mem); \
+ (mem) = NULL; \
+ } STMT_END
+#endif
+#define i_free_and_null(mem) i_free(mem)
+
+/* string functions */
+char *i_strdup(const char *str) ATTR_MALLOC;
+void *i_memdup(const void *data, size_t size) ATTR_MALLOC;
+/* like i_strdup(), but if str == "", return NULL */
+char *i_strdup_empty(const char *str) ATTR_MALLOC;
+/* *end isn't included */
+char *i_strdup_until(const void *str, const void *end)
+ ATTR_MALLOC ATTR_RETURNS_NONNULL;
+char *i_strndup(const void *str, size_t max_chars) ATTR_MALLOC;
+char *i_strdup_printf(const char *format, ...)
+ ATTR_FORMAT(1, 2) ATTR_MALLOC ATTR_RETURNS_NONNULL;
+char *i_strdup_vprintf(const char *format, va_list args)
+ ATTR_FORMAT(1, 0) ATTR_MALLOC ATTR_RETURNS_NONNULL;
+
+char *i_strconcat(const char *str1, ...) ATTR_SENTINEL ATTR_MALLOC;
+
+#endif
diff --git a/src/lib/ioloop-epoll.c b/src/lib/ioloop-epoll.c
new file mode 100644
index 0000000..ad41008
--- /dev/null
+++ b/src/lib/ioloop-epoll.c
@@ -0,0 +1,230 @@
+/* Copyright (c) 2004-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "sleep.h"
+#include "ioloop-private.h"
+#include "ioloop-iolist.h"
+
+#ifdef IOLOOP_EPOLL
+
+#include <sys/epoll.h>
+#include <unistd.h>
+
+struct ioloop_handler_context {
+ int epfd;
+
+ unsigned int deleted_count;
+ ARRAY(struct io_list *) fd_index;
+ ARRAY(struct epoll_event) events;
+};
+
+void io_loop_handler_init(struct ioloop *ioloop, unsigned int initial_fd_count)
+{
+ struct ioloop_handler_context *ctx;
+
+ ioloop->handler_context = ctx = i_new(struct ioloop_handler_context, 1);
+
+ i_array_init(&ctx->events, initial_fd_count);
+ i_array_init(&ctx->fd_index, initial_fd_count);
+
+ ctx->epfd = epoll_create(initial_fd_count);
+ if (ctx->epfd < 0) {
+ if (errno != EMFILE)
+ i_fatal("epoll_create(): %m");
+ else {
+ i_fatal("epoll_create(): %m (you may need to increase "
+ "/proc/sys/fs/epoll/max_user_instances)");
+ }
+ }
+ fd_close_on_exec(ctx->epfd, TRUE);
+}
+
+void io_loop_handler_deinit(struct ioloop *ioloop)
+{
+ struct ioloop_handler_context *ctx = ioloop->handler_context;
+ struct io_list **list;
+ unsigned int i, count;
+
+ list = array_get_modifiable(&ctx->fd_index, &count);
+ for (i = 0; i < count; i++)
+ i_free(list[i]);
+
+ if (close(ctx->epfd) < 0)
+ i_error("close(epoll) failed: %m");
+ array_free(&ioloop->handler_context->fd_index);
+ array_free(&ioloop->handler_context->events);
+ i_free(ioloop->handler_context);
+}
+
+#define IO_EPOLL_ERROR (EPOLLERR | EPOLLHUP)
+#define IO_EPOLL_INPUT (EPOLLIN | EPOLLPRI | IO_EPOLL_ERROR)
+#define IO_EPOLL_OUTPUT (EPOLLOUT | IO_EPOLL_ERROR)
+
+static int epoll_event_mask(struct io_list *list)
+{
+ int events = 0, i;
+ struct io_file *io;
+
+ for (i = 0; i < IOLOOP_IOLIST_IOS_PER_FD; i++) {
+ io = list->ios[i];
+
+ if (io == NULL)
+ continue;
+
+ if ((io->io.condition & IO_READ) != 0)
+ events |= IO_EPOLL_INPUT;
+ if ((io->io.condition & IO_WRITE) != 0)
+ events |= IO_EPOLL_OUTPUT;
+ if ((io->io.condition & IO_ERROR) != 0)
+ events |= IO_EPOLL_ERROR;
+ }
+
+ return events;
+}
+
+void io_loop_handle_add(struct io_file *io)
+{
+ struct ioloop_handler_context *ctx = io->io.ioloop->handler_context;
+ struct io_list **list;
+ struct epoll_event event;
+ int op;
+ bool first;
+
+ list = array_idx_get_space(&ctx->fd_index, io->fd);
+ if (*list == NULL)
+ *list = i_new(struct io_list, 1);
+
+ first = ioloop_iolist_add(*list, io);
+
+ i_zero(&event);
+ event.data.ptr = *list;
+ event.events = epoll_event_mask(*list);
+
+ op = first ? EPOLL_CTL_ADD : EPOLL_CTL_MOD;
+
+ if (epoll_ctl(ctx->epfd, op, io->fd, &event) < 0) {
+ if (errno == EPERM && op == EPOLL_CTL_ADD) {
+ i_panic("epoll_ctl(add, %d) failed: %m "
+ "(fd doesn't support epoll%s)", io->fd,
+ io->fd != STDIN_FILENO ? "" :
+ " - instead of '<file', try 'cat file|'");
+ }
+ i_panic("epoll_ctl(%s, %d) failed: %m",
+ op == EPOLL_CTL_ADD ? "add" : "mod", io->fd);
+ }
+
+ if (first) {
+ /* allow epoll_wait() to return the maximum number of events
+ by keeping space allocated for each file descriptor */
+ if (ctx->deleted_count > 0)
+ ctx->deleted_count--;
+ else
+ array_append_zero(&ctx->events);
+ }
+}
+
+void io_loop_handle_remove(struct io_file *io, bool closed)
+{
+ struct ioloop_handler_context *ctx = io->io.ioloop->handler_context;
+ struct io_list **list;
+ struct epoll_event event;
+ int op;
+ bool last;
+
+ list = array_idx_modifiable(&ctx->fd_index, io->fd);
+ last = ioloop_iolist_del(*list, io);
+
+ if (!closed) {
+ i_zero(&event);
+ event.data.ptr = *list;
+ event.events = epoll_event_mask(*list);
+
+ op = last ? EPOLL_CTL_DEL : EPOLL_CTL_MOD;
+
+ if (epoll_ctl(ctx->epfd, op, io->fd, &event) < 0) {
+ const char *errstr = t_strdup_printf(
+ "epoll_ctl(%s, %d) failed: %m",
+ op == EPOLL_CTL_DEL ? "del" : "mod", io->fd);
+ if (errno != ENOSPC && errno != ENOMEM)
+ i_panic("%s", errstr);
+ else
+ i_error("%s", errstr);
+ }
+ }
+ if (last) {
+ /* since we're not freeing memory in any case, just increase
+ deleted counter so next handle_add() can just decrease it
+ instead of appending to the events array */
+ ctx->deleted_count++;
+ }
+ i_free(io);
+}
+
+void io_loop_handler_run_internal(struct ioloop *ioloop)
+{
+ struct ioloop_handler_context *ctx = ioloop->handler_context;
+ struct epoll_event *events;
+ const struct epoll_event *event;
+ struct io_list *list;
+ struct io_file *io;
+ struct timeval tv;
+ unsigned int events_count;
+ int msecs, ret, i, j;
+ bool call;
+
+ i_assert(ctx != NULL);
+
+ /* get the time left for next timeout task */
+ msecs = io_loop_run_get_wait_time(ioloop, &tv);
+
+ events = array_get_modifiable(&ctx->events, &events_count);
+ if (ioloop->io_files != NULL && events_count > ctx->deleted_count) {
+ ret = epoll_wait(ctx->epfd, events, events_count, msecs);
+ if (ret < 0 && errno != EINTR)
+ i_fatal("epoll_wait(): %m");
+ } else {
+ /* no I/Os, but we should have some timeouts.
+ just wait for them. */
+ i_assert(msecs >= 0);
+ i_sleep_intr_msecs(msecs);
+ ret = 0;
+ }
+
+ /* execute timeout handlers */
+ io_loop_handle_timeouts(ioloop);
+
+ if (!ioloop->running)
+ return;
+
+ for (i = 0; i < ret; i++) {
+ /* io_loop_handle_add() may cause events array reallocation,
+ so we have use array_idx() */
+ event = array_idx(&ctx->events, i);
+ list = event->data.ptr;
+
+ for (j = 0; j < IOLOOP_IOLIST_IOS_PER_FD; j++) {
+ io = list->ios[j];
+ if (io == NULL)
+ continue;
+
+ call = FALSE;
+ if ((event->events & (EPOLLHUP | EPOLLERR)) != 0)
+ call = TRUE;
+ else if ((io->io.condition & IO_READ) != 0)
+ call = (event->events & EPOLLIN) != 0;
+ else if ((io->io.condition & IO_WRITE) != 0)
+ call = (event->events & EPOLLOUT) != 0;
+ else if ((io->io.condition & IO_ERROR) != 0)
+ call = (event->events & IO_EPOLL_ERROR) != 0;
+
+ if (call) {
+ io_loop_call_io(&io->io);
+ if (!ioloop->running)
+ return;
+ }
+ }
+ }
+}
+
+#endif /* IOLOOP_EPOLL */
diff --git a/src/lib/ioloop-iolist.c b/src/lib/ioloop-iolist.c
new file mode 100644
index 0000000..28229af
--- /dev/null
+++ b/src/lib/ioloop-iolist.c
@@ -0,0 +1,56 @@
+/*
+ * Copyright (c) 2004 Andrey Panin <pazke@donpac.ru>
+ *
+ * This software is released under the MIT license.
+ */
+
+#include "lib.h"
+#include "ioloop-private.h"
+#include "ioloop-iolist.h"
+
+bool ioloop_iolist_add(struct io_list *list, struct io_file *io)
+{
+ int i, idx;
+
+ if ((io->io.condition & IO_READ) != 0)
+ idx = IOLOOP_IOLIST_INPUT;
+ else if ((io->io.condition & IO_WRITE) != 0)
+ idx = IOLOOP_IOLIST_OUTPUT;
+ else if ((io->io.condition & IO_ERROR) != 0)
+ idx = IOLOOP_IOLIST_ERROR;
+ else {
+ i_unreached();
+ }
+
+ if (list->ios[idx] != NULL) {
+ i_panic("io_add(0x%x) called twice fd=%d, callback=%p -> %p",
+ io->io.condition, io->fd, list->ios[idx]->io.callback,
+ io->io.callback);
+ }
+ i_assert(list->ios[idx] == NULL);
+ list->ios[idx] = io;
+
+ /* check if this was the first one */
+ for (i = 0; i < IOLOOP_IOLIST_IOS_PER_FD; i++) {
+ if (i != idx && list->ios[i] != NULL)
+ return FALSE;
+ }
+
+ return TRUE;
+}
+
+bool ioloop_iolist_del(struct io_list *list, struct io_file *io)
+{
+ bool last = TRUE;
+ int i;
+
+ for (i = 0; i < IOLOOP_IOLIST_IOS_PER_FD; i++) {
+ if (list->ios[i] != NULL) {
+ if (list->ios[i] == io)
+ list->ios[i] = NULL;
+ else
+ last = FALSE;
+ }
+ }
+ return last;
+}
diff --git a/src/lib/ioloop-iolist.h b/src/lib/ioloop-iolist.h
new file mode 100644
index 0000000..24fa871
--- /dev/null
+++ b/src/lib/ioloop-iolist.h
@@ -0,0 +1,19 @@
+#ifndef IOLOOP_IOLIST_H
+#define IOLOOP_IOLIST_H
+
+enum {
+ IOLOOP_IOLIST_INPUT,
+ IOLOOP_IOLIST_OUTPUT,
+ IOLOOP_IOLIST_ERROR,
+
+ IOLOOP_IOLIST_IOS_PER_FD
+};
+
+struct io_list {
+ struct io_file *ios[IOLOOP_IOLIST_IOS_PER_FD];
+};
+
+bool ioloop_iolist_add(struct io_list *list, struct io_file *io);
+bool ioloop_iolist_del(struct io_list *list, struct io_file *io);
+
+#endif
diff --git a/src/lib/ioloop-kqueue.c b/src/lib/ioloop-kqueue.c
new file mode 100644
index 0000000..061b1b1
--- /dev/null
+++ b/src/lib/ioloop-kqueue.c
@@ -0,0 +1,175 @@
+/*
+ * BSD kqueue() based ioloop handler.
+ *
+ * Copyright (c) 2005 Vaclav Haisman <v.haisman@sh.cvut.cz>
+ */
+
+#include "lib.h"
+
+#ifdef IOLOOP_KQUEUE
+
+#include "array.h"
+#include "sleep.h"
+#include "ioloop-private.h"
+
+#include <unistd.h>
+#include <fcntl.h>
+#include <sys/types.h>
+#include <sys/event.h>
+#include <sys/time.h>
+
+/* kevent.udata's type just has to be different in NetBSD than in
+ FreeBSD and OpenBSD.. */
+#ifdef __NetBSD__
+# define MY_EV_SET(a, b, c, d, e, f, g) \
+ EV_SET(a, b, c, d, e, f, (intptr_t)g)
+#else
+# define MY_EV_SET(a, b, c, d, e, f, g) \
+ EV_SET(a, b, c, d, e, f, g)
+#endif
+
+struct ioloop_handler_context {
+ int kq;
+
+ unsigned int deleted_count;
+ ARRAY(struct kevent) events;
+};
+
+void io_loop_handler_init(struct ioloop *ioloop, unsigned int initial_fd_count)
+{
+ struct ioloop_handler_context *ctx;
+
+ ioloop->handler_context = ctx = i_new(struct ioloop_handler_context, 1);
+ ctx->kq = kqueue();
+ if (ctx->kq < 0)
+ i_fatal("kqueue() in io_loop_handler_init() failed: %m");
+ fd_close_on_exec(ctx->kq, TRUE);
+
+ i_array_init(&ctx->events, initial_fd_count);
+}
+
+void io_loop_handler_deinit(struct ioloop *ioloop)
+{
+ if (close(ioloop->handler_context->kq) < 0)
+ i_error("close(kqueue) in io_loop_handler_deinit() failed: %m");
+ array_free(&ioloop->handler_context->events);
+ i_free(ioloop->handler_context);
+}
+
+void io_loop_handle_add(struct io_file *io)
+{
+ struct ioloop_handler_context *ctx = io->io.ioloop->handler_context;
+ struct kevent ev;
+
+ if ((io->io.condition & (IO_READ | IO_ERROR)) != 0) {
+ MY_EV_SET(&ev, io->fd, EVFILT_READ, EV_ADD, 0, 0, io);
+ if (kevent(ctx->kq, &ev, 1, NULL, 0, NULL) < 0)
+ i_panic("kevent(EV_ADD, READ, %d) failed: %m", io->fd);
+ }
+ if ((io->io.condition & IO_WRITE) != 0) {
+ MY_EV_SET(&ev, io->fd, EVFILT_WRITE, EV_ADD, 0, 0, io);
+ if (kevent(ctx->kq, &ev, 1, NULL, 0, NULL) < 0)
+ i_panic("kevent(EV_ADD, WRITE, %d) failed: %m", io->fd);
+ }
+
+ /* allow kevent() to return the maximum number of events
+ by keeping space allocated for each handle */
+ if (ctx->deleted_count > 0)
+ ctx->deleted_count--;
+ else
+ array_append_zero(&ctx->events);
+}
+
+void io_loop_handle_remove(struct io_file *io, bool closed)
+{
+ struct ioloop_handler_context *ctx = io->io.ioloop->handler_context;
+ struct kevent ev;
+
+ i_assert(io->io.condition != 0);
+ if ((io->io.condition & (IO_READ | IO_ERROR)) != 0 && !closed) {
+ MY_EV_SET(&ev, io->fd, EVFILT_READ, EV_DELETE, 0, 0, NULL);
+ if (kevent(ctx->kq, &ev, 1, NULL, 0, NULL) < 0)
+ i_error("kevent(EV_DELETE, %d) failed: %m", io->fd);
+ }
+ if ((io->io.condition & IO_WRITE) != 0 && !closed) {
+ MY_EV_SET(&ev, io->fd, EVFILT_WRITE, EV_DELETE, 0, 0, NULL);
+ if (kevent(ctx->kq, &ev, 1, NULL, 0, NULL) < 0)
+ i_error("kevent(EV_DELETE, %d) failed: %m", io->fd);
+ }
+ io->io.condition = 0;
+
+ /* since we're not freeing memory in any case, just increase
+ deleted counter so next handle_add() can just decrease it
+ instead of appending to the events array */
+ ctx->deleted_count++;
+
+ i_assert(io->refcount > 0);
+ if (--io->refcount == 0)
+ i_free(io);
+}
+
+void io_loop_handler_run_internal(struct ioloop *ioloop)
+{
+ struct ioloop_handler_context *ctx = ioloop->handler_context;
+ struct kevent *events;
+ const struct kevent *event;
+ struct timeval tv;
+ struct timespec ts;
+ struct io_file *io;
+ unsigned int events_count;
+ int ret, i, msecs;
+
+ /* get the time left for next timeout task */
+ msecs = io_loop_run_get_wait_time(ioloop, &tv);
+ ts.tv_sec = tv.tv_sec;
+ ts.tv_nsec = tv.tv_usec * 1000;
+
+ /* wait for events */
+ events = array_get_modifiable(&ctx->events, &events_count);
+
+ if (events_count > 0) {
+ ret = kevent (ctx->kq, NULL, 0, events, events_count, &ts);
+ if (ret < 0 && errno != EINTR) {
+ i_panic("kevent(events=%u, ts=%ld.%u) failed: %m",
+ events_count, (long)ts.tv_sec,
+ (unsigned int)ts.tv_nsec);
+ }
+ } else {
+ i_assert(msecs >= 0);
+ i_sleep_intr_msecs(msecs);
+ ret = 0;
+ }
+
+ /* reference all IOs */
+ for (i = 0; i < ret; i++) {
+ io = (void *)events[i].udata;
+ i_assert(io->refcount > 0);
+ io->refcount++;
+ }
+
+ /* execute timeout handlers */
+ io_loop_handle_timeouts(ioloop);
+
+ if (!ioloop->running)
+ return;
+
+ for (i = 0; i < ret; i++) {
+ /* io_loop_handle_add() may cause events array reallocation,
+ so we have use array_idx() */
+ event = array_idx(&ctx->events, i);
+ io = (void *)event->udata;
+
+ /* callback is NULL if io_remove() was already called */
+ if (io->io.callback != NULL) {
+ io_loop_call_io(&io->io);
+ if (!ioloop->running)
+ break;
+ }
+
+ i_assert(io->refcount > 0);
+ if (--io->refcount == 0)
+ i_free(io);
+ }
+}
+
+#endif
diff --git a/src/lib/ioloop-notify-fd.c b/src/lib/ioloop-notify-fd.c
new file mode 100644
index 0000000..d3ec29b
--- /dev/null
+++ b/src/lib/ioloop-notify-fd.c
@@ -0,0 +1,55 @@
+/* Copyright (c) 2007-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "ioloop-private.h"
+#include "ioloop-notify-fd.h"
+
+#if defined(IOLOOP_NOTIFY_INOTIFY)
+
+struct io *io_notify_fd_add(struct ioloop_notify_fd_context *ctx, int fd,
+ io_callback_t *callback, void *context)
+{
+ struct io_notify *io;
+
+ io = i_new(struct io_notify, 1);
+ io->io.condition = IO_NOTIFY;
+ io->io.callback = callback;
+ io->io.context = context;
+ io->io.ioloop = current_ioloop;
+ io->fd = fd;
+
+ if (ctx->notifies != NULL) {
+ ctx->notifies->prev = io;
+ io->next = ctx->notifies;
+ }
+ ctx->notifies = io;
+ return &io->io;
+}
+
+void io_notify_fd_free(struct ioloop_notify_fd_context *ctx,
+ struct io_notify *io)
+{
+ if (io->prev != NULL)
+ io->prev->next = io->next;
+ else
+ ctx->notifies = io->next;
+
+ if (io->next != NULL)
+ io->next->prev = io->prev;
+ i_free(io);
+}
+
+struct io_notify *
+io_notify_fd_find(struct ioloop_notify_fd_context *ctx, int fd)
+{
+ struct io_notify *io;
+
+ for (io = ctx->notifies; io != NULL; io = io->next) {
+ if (io->fd == fd)
+ return io;
+ }
+
+ return NULL;
+}
+
+#endif
diff --git a/src/lib/ioloop-notify-fd.h b/src/lib/ioloop-notify-fd.h
new file mode 100644
index 0000000..95bbb73
--- /dev/null
+++ b/src/lib/ioloop-notify-fd.h
@@ -0,0 +1,28 @@
+#ifndef IOLOOP_NOTIFY_FD_H
+#define IOLOOP_NOTIFY_FD_H
+
+/* common notify code for fd-based notifications (dnotify, inotify) */
+
+struct io_notify {
+ struct io io;
+
+ /* use a doubly linked list so that io_remove() is quick */
+ struct io_notify *prev, *next;
+
+ int fd;
+};
+
+struct ioloop_notify_fd_context {
+ struct io_notify *notifies;
+};
+
+struct io *
+io_notify_fd_add(struct ioloop_notify_fd_context *ctx, int fd,
+ io_callback_t *callback, void *context) ATTR_NULL(4);
+void io_notify_fd_free(struct ioloop_notify_fd_context *ctx,
+ struct io_notify *io);
+
+struct io_notify *
+io_notify_fd_find(struct ioloop_notify_fd_context *ctx, int fd);
+
+#endif
diff --git a/src/lib/ioloop-notify-inotify.c b/src/lib/ioloop-notify-inotify.c
new file mode 100644
index 0000000..796ac31
--- /dev/null
+++ b/src/lib/ioloop-notify-inotify.c
@@ -0,0 +1,237 @@
+/* Copyright (c) 2005-2018 Dovecot authors, see the included COPYING file */
+
+#define _GNU_SOURCE
+#include "lib.h"
+
+#ifdef IOLOOP_NOTIFY_INOTIFY
+
+#include "ioloop-private.h"
+#include "ioloop-notify-fd.h"
+#include "buffer.h"
+#include "net.h"
+#include "ipwd.h"
+#include "time-util.h"
+
+#include <stdio.h>
+#include <unistd.h>
+#include <fcntl.h>
+#include <sys/ioctl.h>
+#include <sys/inotify.h>
+
+#define INOTIFY_BUFLEN (32*1024)
+
+struct ioloop_notify_handler_context {
+ struct ioloop_notify_fd_context fd_ctx;
+
+ int inotify_fd;
+ struct io *event_io;
+
+ bool disabled;
+};
+
+static struct ioloop_notify_handler_context *io_loop_notify_handler_init(void);
+
+static bool inotify_input_more(struct ioloop *ioloop)
+{
+ struct ioloop_notify_handler_context *ctx =
+ ioloop->notify_handler_context;
+ const struct inotify_event *event;
+ unsigned char event_buf[INOTIFY_BUFLEN];
+ struct io_notify *io;
+ ssize_t ret, pos;
+
+ /* read as many events as there is available and fit into our buffer.
+ only full events are returned by the kernel. */
+ ret = read(ctx->inotify_fd, event_buf, sizeof(event_buf));
+ if (ret <= 0) {
+ if (ret == 0 || errno == EAGAIN) {
+ /* nothing more to read */
+ return FALSE;
+ }
+ i_fatal("read(inotify) failed: %m");
+ }
+
+ i_gettimeofday(&ioloop_timeval);
+ ioloop_time = ioloop_timeval.tv_sec;
+
+ for (pos = 0; pos < ret; ) {
+ if ((size_t)(ret - pos) < sizeof(*event))
+ break;
+
+ event = (struct inotify_event *)(event_buf + pos);
+ i_assert(event->len < (size_t)ret);
+ pos += sizeof(*event) + event->len;
+
+ io = io_notify_fd_find(&ctx->fd_ctx, event->wd);
+ if (io != NULL) {
+ if ((event->mask & IN_IGNORED) != 0) {
+ /* calling inotify_rm_watch() would now give
+ EINVAL */
+ io->fd = -1;
+ }
+ io_loop_call_io(&io->io);
+ }
+ }
+ if (pos != ret)
+ i_error("read(inotify) returned partial event");
+ return (size_t)ret >= sizeof(event_buf)-512;
+}
+
+static void inotify_input(struct ioloop *ioloop)
+{
+ while (inotify_input_more(ioloop)) ;
+}
+
+#undef io_add_notify
+enum io_notify_result
+io_add_notify(const char *path, const char *source_filename,
+ unsigned int source_linenum,
+ io_callback_t *callback, void *context, struct io **io_r)
+{
+ struct ioloop_notify_handler_context *ctx =
+ current_ioloop->notify_handler_context;
+ int wd;
+
+ *io_r = NULL;
+
+ if (ctx == NULL)
+ ctx = io_loop_notify_handler_init();
+ if (ctx->disabled)
+ return IO_NOTIFY_NOSUPPORT;
+
+ wd = inotify_add_watch(ctx->inotify_fd, path,
+ IN_CREATE | IN_DELETE | IN_DELETE_SELF |
+ IN_MOVE | IN_MODIFY);
+ if (wd < 0) {
+ /* ESTALE could happen with NFS. Don't bother giving an error
+ message then. */
+ if (errno == ENOENT || errno == ESTALE)
+ return IO_NOTIFY_NOTFOUND;
+
+ if (errno != ENOSPC)
+ i_error("inotify_add_watch(%s) failed: %m", path);
+ else {
+ i_warning("Inotify watch limit for user exceeded, "
+ "disabling. Increase "
+ "/proc/sys/fs/inotify/max_user_watches");
+ }
+ ctx->disabled = TRUE;
+ return IO_NOTIFY_NOSUPPORT;
+ }
+
+ if (ctx->event_io == NULL) {
+ ctx->event_io = io_add(ctx->inotify_fd, IO_READ,
+ inotify_input, current_ioloop);
+ }
+
+ *io_r = io_notify_fd_add(&ctx->fd_ctx, wd, callback, context);
+ (*io_r)->source_filename = source_filename;
+ (*io_r)->source_linenum = source_linenum;
+ return IO_NOTIFY_ADDED;
+}
+
+void io_loop_notify_remove(struct io *_io)
+{
+ struct ioloop_notify_handler_context *ctx =
+ _io->ioloop->notify_handler_context;
+ struct io_notify *io = (struct io_notify *)_io;
+
+ if (io->fd != -1) {
+ /* ernro=EINVAL happens if the file itself is deleted and
+ kernel has sent IN_IGNORED event which we haven't read. */
+ if (inotify_rm_watch(ctx->inotify_fd, io->fd) < 0 &&
+ errno != EINVAL)
+ i_error("inotify_rm_watch() failed: %m");
+ }
+
+ io_notify_fd_free(&ctx->fd_ctx, io);
+
+ if (ctx->fd_ctx.notifies == NULL && ctx->event_io != NULL)
+ io_remove(&ctx->event_io);
+}
+
+static void ioloop_inotify_user_limit_exceeded(void)
+{
+ struct passwd pw;
+ const char *name;
+ uid_t uid = geteuid();
+
+ if (i_getpwuid(uid, &pw) <= 0)
+ name = t_strdup_printf("UID %s", dec2str(uid));
+ else {
+ name = t_strdup_printf("%s (UID %s)",
+ dec2str(uid), pw.pw_name);
+ }
+ i_warning("Inotify instance limit for user %s exceeded, disabling. "
+ "Increase /proc/sys/fs/inotify/max_user_instances", name);
+}
+
+static struct ioloop_notify_handler_context *io_loop_notify_handler_init(void)
+{
+ struct ioloop *ioloop = current_ioloop;
+ struct ioloop_notify_handler_context *ctx;
+
+ ctx = ioloop->notify_handler_context =
+ i_new(struct ioloop_notify_handler_context, 1);
+
+ ctx->inotify_fd = inotify_init();
+ if (ctx->inotify_fd == -1) {
+ if (errno != EMFILE)
+ i_error("inotify_init() failed: %m");
+ else
+ ioloop_inotify_user_limit_exceeded();
+ ctx->disabled = TRUE;
+ } else {
+ fd_close_on_exec(ctx->inotify_fd, TRUE);
+ fd_set_nonblock(ctx->inotify_fd, TRUE);
+ }
+ return ctx;
+}
+
+void io_loop_notify_handler_deinit(struct ioloop *ioloop)
+{
+ struct ioloop_notify_handler_context *ctx =
+ ioloop->notify_handler_context;
+
+ while (ctx->fd_ctx.notifies != NULL) {
+ struct io_notify *io = ctx->fd_ctx.notifies;
+ struct io *_io = &io->io;
+
+ i_warning("I/O notify leak: %p (%s:%u, fd %d)",
+ (void *)_io->callback,
+ _io->source_filename,
+ _io->source_linenum, io->fd);
+ io_remove(&_io);
+ }
+
+ i_close_fd(&ctx->inotify_fd);
+ i_free(ctx);
+}
+
+int io_loop_extract_notify_fd(struct ioloop *ioloop)
+{
+ struct ioloop_notify_handler_context *ctx =
+ ioloop->notify_handler_context;
+ struct io_notify *io;
+ int fd, new_inotify_fd;
+
+ if (ctx == NULL || ctx->inotify_fd == -1)
+ return -1;
+
+ new_inotify_fd = inotify_init();
+ if (new_inotify_fd == -1) {
+ if (errno != EMFILE)
+ i_error("inotify_init() failed: %m");
+ else
+ ioloop_inotify_user_limit_exceeded();
+ return -1;
+ }
+ for (io = ctx->fd_ctx.notifies; io != NULL; io = io->next)
+ io->fd = -1;
+ io_remove(&ctx->event_io);
+ fd = ctx->inotify_fd;
+ ctx->inotify_fd = new_inotify_fd;
+ return fd;
+}
+
+#endif
diff --git a/src/lib/ioloop-notify-kqueue.c b/src/lib/ioloop-notify-kqueue.c
new file mode 100644
index 0000000..9831f05
--- /dev/null
+++ b/src/lib/ioloop-notify-kqueue.c
@@ -0,0 +1,223 @@
+/*
+ * BSD kqueue() based ioloop notify handler.
+ *
+ * Copyright (c) 2005 Vaclav Haisman <v.haisman@sh.cvut.cz>
+ */
+
+#define _GNU_SOURCE
+#include "lib.h"
+
+#ifdef IOLOOP_NOTIFY_KQUEUE
+
+#include "ioloop-private.h"
+#include "llist.h"
+#include <unistd.h>
+#include <fcntl.h>
+#include <sys/types.h>
+#include <sys/event.h>
+#include <sys/time.h>
+#include <sys/stat.h>
+
+/* kevent.udata's type just has to be different in NetBSD than in
+ FreeBSD and OpenBSD.. */
+#ifdef __NetBSD__
+# define MY_EV_SET(a, b, c, d, e, f, g) \
+ EV_SET(a, b, c, d, e, f, (intptr_t)g)
+#else
+# define MY_EV_SET(a, b, c, d, e, f, g) \
+ EV_SET(a, b, c, d, e, f, g)
+#endif
+
+struct io_notify {
+ struct io io;
+ int refcount;
+ int fd;
+ struct io_notify *prev, *next;
+};
+
+struct ioloop_notify_handler_context {
+ int kq;
+ struct io *event_io;
+ struct io_notify *notifies;
+};
+
+static void
+io_loop_notify_free(struct ioloop_notify_handler_context *ctx,
+ struct io_notify *io)
+{
+ DLLIST_REMOVE(&ctx->notifies, io);
+ i_free(io);
+}
+
+static void event_callback(struct ioloop_notify_handler_context *ctx)
+{
+ struct io_notify *io;
+ struct kevent events[64];
+ struct timespec ts;
+ int i, ret;
+
+ ts.tv_sec = 0;
+ ts.tv_nsec = 0;
+
+ ret = kevent(ctx->kq, NULL, 0, events, N_ELEMENTS(events), &ts);
+ if (ret <= 0) {
+ if (ret == 0 || errno == EINTR)
+ return;
+
+ i_fatal("kevent(notify) failed: %m");
+ }
+
+ i_gettimeofday(&ioloop_timeval);
+ ioloop_time = ioloop_timeval.tv_sec;
+
+ for (i = 0; i < ret; i++) {
+ io = (void *)events[i].udata;
+ i_assert(io->refcount >= 1);
+ io->refcount++;
+ }
+ for (i = 0; i < ret; i++) {
+ io = (void *)events[i].udata;
+ /* there can be multiple events for a single io.
+ call the callback only once if that happens. */
+ if (io->refcount == 2 && io->io.callback != NULL)
+ io_loop_call_io(&io->io);
+
+ if (--io->refcount == 0)
+ io_loop_notify_free(ctx, io);
+ }
+}
+
+static struct ioloop_notify_handler_context *io_loop_notify_handler_init(void)
+{
+ struct ioloop_notify_handler_context *ctx;
+
+ ctx = current_ioloop->notify_handler_context =
+ i_new(struct ioloop_notify_handler_context, 1);
+ ctx->kq = kqueue();
+ if (ctx->kq < 0)
+ i_fatal("kqueue(notify) failed: %m");
+ fd_close_on_exec(ctx->kq, TRUE);
+ return ctx;
+}
+
+void io_loop_notify_handler_deinit(struct ioloop *ioloop)
+{
+ struct ioloop_notify_handler_context *ctx =
+ ioloop->notify_handler_context;
+
+ while (ctx->notifies != NULL) {
+ struct io_notify *io = ctx->notifies;
+ struct io *_io = &io->io;
+
+ i_warning("I/O notify leak: %p (%s:%u, fd %d)",
+ (void *)_io->callback,
+ _io->source_filename,
+ _io->source_linenum, io->fd);
+ io_remove(&_io);
+ }
+
+ io_remove(&ctx->event_io);
+ if (close(ctx->kq) < 0)
+ i_error("close(kqueue notify) failed: %m");
+ i_free(ctx);
+}
+
+#undef io_add_notify
+enum io_notify_result
+io_add_notify(const char *path, const char *source_filename,
+ unsigned int source_linenum,
+ io_callback_t *callback, void *context, struct io **io_r)
+{
+ struct ioloop_notify_handler_context *ctx =
+ current_ioloop->notify_handler_context;
+ struct kevent ev;
+ struct io_notify *io;
+ int fd;
+
+ if (ctx == NULL)
+ ctx = io_loop_notify_handler_init();
+
+ fd = open(path, O_RDONLY);
+ if (fd == -1) {
+ /* ESTALE could happen with NFS. Don't bother giving an error
+ message then. */
+ if (errno != ENOENT && errno != ESTALE)
+ i_error("open(%s) for kq notify failed: %m", path);
+ return IO_NOTIFY_NOTFOUND;
+ }
+ fd_close_on_exec(fd, TRUE);
+
+ io = i_new(struct io_notify, 1);
+ io->io.condition = IO_NOTIFY;
+ io->io.source_filename = source_filename;
+ io->io.source_linenum = source_linenum;
+ io->io.callback = callback;
+ io->io.context = context;
+ io->io.ioloop = current_ioloop;
+ io->refcount = 1;
+ io->fd = fd;
+
+ /* EV_CLEAR flag is needed because the EVFILT_VNODE filter reports
+ event state transitions and not the current state. With this flag,
+ the same event is only returned once. */
+ MY_EV_SET(&ev, fd, EVFILT_VNODE, EV_ADD | EV_CLEAR,
+ NOTE_DELETE | NOTE_RENAME | NOTE_WRITE | NOTE_EXTEND |
+ NOTE_REVOKE, 0, io);
+ if (kevent(ctx->kq, &ev, 1, NULL, 0, NULL) < 0) {
+ i_error("kevent(%d, %s) for notify failed: %m", fd, path);
+ i_close_fd(&fd);
+ i_free(io);
+ return IO_NOTIFY_NOSUPPORT;
+ }
+
+ if (ctx->event_io == NULL) {
+ ctx->event_io = io_add(ctx->kq, IO_READ, event_callback,
+ io->io.ioloop->notify_handler_context);
+ }
+ DLLIST_PREPEND(&ctx->notifies, io);
+ *io_r = &io->io;
+ return IO_NOTIFY_ADDED;
+}
+
+void io_loop_notify_remove(struct io *_io)
+{
+ struct ioloop_notify_handler_context *ctx =
+ _io->ioloop->notify_handler_context;
+ struct io_notify *io = (struct io_notify *)_io;
+ struct kevent ev;
+
+ MY_EV_SET(&ev, io->fd, EVFILT_VNODE, EV_DELETE, 0, 0, NULL);
+ if (kevent(ctx->kq, &ev, 1, NULL, 0, 0) < 0)
+ i_error("kevent(%d) for notify remove failed: %m", io->fd);
+ if (close(io->fd) < 0)
+ i_error("close(%d) for notify remove failed: %m", io->fd);
+ io->fd = -1;
+
+ if (--io->refcount == 0)
+ io_loop_notify_free(ctx, io);
+}
+
+int io_loop_extract_notify_fd(struct ioloop *ioloop)
+{
+ struct ioloop_notify_handler_context *ctx =
+ ioloop->notify_handler_context;
+ struct io_notify *io;
+ int fd, new_kq;
+
+ if (ctx == NULL || ctx->kq == -1)
+ return -1;
+
+ new_kq = kqueue();
+ if (new_kq < 0) {
+ i_error("kqueue(notify) failed: %m");
+ return -1;
+ }
+ for (io = ctx->notifies; io != NULL; io = io->next)
+ io->fd = -1;
+ io_remove(&ctx->event_io);
+ fd = ctx->kq;
+ ctx->kq = new_kq;
+ return fd;
+}
+
+#endif
diff --git a/src/lib/ioloop-notify-none.c b/src/lib/ioloop-notify-none.c
new file mode 100644
index 0000000..f91c26f
--- /dev/null
+++ b/src/lib/ioloop-notify-none.c
@@ -0,0 +1,33 @@
+/* Copyright (c) 2003-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "ioloop-private.h"
+
+#ifdef IOLOOP_NOTIFY_NONE
+
+#undef io_add_notify
+enum io_notify_result
+io_add_notify(const char *path ATTR_UNUSED,
+ const char *source_filename ATTR_UNUSED,
+ unsigned int source_linenum ATTR_UNUSED,
+ io_callback_t *callback ATTR_UNUSED,
+ void *context ATTR_UNUSED, struct io **io_r)
+{
+ *io_r = NULL;
+ return IO_NOTIFY_NOSUPPORT;
+}
+
+void io_loop_notify_remove(struct io *io ATTR_UNUSED)
+{
+}
+
+void io_loop_notify_handler_deinit(struct ioloop *ioloop ATTR_UNUSED)
+{
+}
+
+int io_loop_extract_notify_fd(struct ioloop *ioloop ATTR_UNUSED)
+{
+ return -1;
+}
+
+#endif
diff --git a/src/lib/ioloop-poll.c b/src/lib/ioloop-poll.c
new file mode 100644
index 0000000..02cdce6
--- /dev/null
+++ b/src/lib/ioloop-poll.c
@@ -0,0 +1,221 @@
+/* Copyright (c) 2002-2018 Dovecot authors, see the included COPYING file */
+
+/* @UNSAFE: whole file */
+
+#include "lib.h"
+#include "ioloop-private.h"
+
+#ifdef IOLOOP_POLL
+
+#include <fcntl.h>
+#include <sys/poll.h>
+
+struct ioloop_handler_context {
+ unsigned int fds_count, fds_pos;
+ struct pollfd *fds;
+
+ unsigned int idx_count;
+ int *fd_index;
+};
+
+void io_loop_handler_init(struct ioloop *ioloop, unsigned int initial_fd_count)
+{
+ struct ioloop_handler_context *ctx;
+
+ ioloop->handler_context = ctx = i_new(struct ioloop_handler_context, 1);
+ ctx->fds_count = initial_fd_count;
+ ctx->fds = i_new(struct pollfd, ctx->fds_count);
+
+ ctx->idx_count = initial_fd_count;
+ ctx->fd_index = i_new(int, ctx->idx_count);
+ memset(ctx->fd_index, 0xff, sizeof(int) * ctx->idx_count);
+}
+
+void io_loop_handler_deinit(struct ioloop *ioloop)
+{
+ i_free(ioloop->handler_context->fds);
+ i_free(ioloop->handler_context->fd_index);
+ i_free(ioloop->handler_context);
+}
+
+#define IO_POLL_ERROR (POLLERR | POLLHUP | POLLNVAL)
+#define IO_POLL_INPUT (POLLIN | POLLPRI | IO_POLL_ERROR)
+#define IO_POLL_OUTPUT (POLLOUT | IO_POLL_ERROR)
+
+void io_loop_handle_add(struct io_file *io)
+{
+ struct ioloop_handler_context *ctx = io->io.ioloop->handler_context;
+ enum io_condition condition = io->io.condition;
+ unsigned int old_count;
+ int index, old_events, fd = io->fd;
+
+ if ((unsigned int)fd >= ctx->idx_count) {
+ /* grow the fd -> index array */
+ old_count = ctx->idx_count;
+
+ ctx->idx_count = nearest_power((unsigned int) fd+1);
+
+ ctx->fd_index = i_realloc_type(ctx->fd_index, int,
+ old_count, ctx->idx_count);
+ memset(ctx->fd_index + old_count, 0xff,
+ sizeof(int) * (ctx->idx_count-old_count));
+ }
+
+ if (ctx->fds_pos >= ctx->fds_count) {
+ /* grow the fd array */
+ old_count = ctx->fds_count;
+
+ ctx->fds_count = nearest_power(ctx->fds_count+1);
+
+ ctx->fds = i_realloc_type(ctx->fds, struct pollfd,
+ old_count, ctx->fds_count);
+ }
+
+ if (ctx->fd_index[fd] != -1) {
+ /* update existing pollfd */
+ index = ctx->fd_index[fd];
+ } else {
+ /* add new pollfd */
+ index = ctx->fds_pos++;
+
+ ctx->fd_index[fd] = index;
+ ctx->fds[index].fd = fd;
+ ctx->fds[index].events = 0;
+ ctx->fds[index].revents = 0;
+ }
+
+ old_events = ctx->fds[index].events;
+ if ((condition & IO_READ) != 0)
+ ctx->fds[index].events |= IO_POLL_INPUT;
+ if ((condition & IO_WRITE) != 0)
+ ctx->fds[index].events |= IO_POLL_OUTPUT;
+ if ((condition & IO_ERROR) != 0)
+ ctx->fds[index].events |= IO_POLL_ERROR;
+ i_assert(ctx->fds[index].events != old_events);
+}
+
+void io_loop_handle_remove(struct io_file *io, bool closed ATTR_UNUSED)
+{
+ struct ioloop_handler_context *ctx = io->io.ioloop->handler_context;
+ enum io_condition condition = io->io.condition;
+ int index, fd = io->fd;
+
+ index = ctx->fd_index[fd];
+ i_assert(index >= 0 && (unsigned int) index < ctx->fds_count);
+
+#ifdef DEBUG
+ if (!closed) {
+ /* io_remove() is required to be called before fd is closed.
+ This is required by epoll/kqueue, but since poll is more
+ commonly used while developing, this check here should catch
+ the error early enough not to cause problems for kqueue
+ users. */
+ if (fcntl(io->fd, F_GETFD, 0) < 0) {
+ if (errno == EBADF)
+ i_panic("io_remove(%d) called too late", io->fd);
+ else
+ i_error("fcntl(%d, F_GETFD) failed: %m", io->fd);
+ }
+ }
+#endif
+ i_free(io);
+
+ if ((condition & IO_READ) != 0) {
+ ctx->fds[index].events &= ENUM_NEGATE(POLLIN | POLLPRI);
+ ctx->fds[index].revents &= ENUM_NEGATE(POLLIN | POLLPRI);
+ }
+ if ((condition & IO_WRITE) != 0) {
+ ctx->fds[index].events &= ENUM_NEGATE(POLLOUT);
+ ctx->fds[index].revents &= ENUM_NEGATE(POLLOUT);
+ }
+
+ if ((ctx->fds[index].events & (POLLIN|POLLOUT)) == 0) {
+ /* remove the whole pollfd */
+ ctx->fd_index[ctx->fds[index].fd] = -1;
+ if (--ctx->fds_pos == (unsigned int) index)
+ return; /* removing last one */
+
+ /* move the last pollfd over the removed one */
+ ctx->fds[index] = ctx->fds[ctx->fds_pos];
+ ctx->fd_index[ctx->fds[index].fd] = index;
+ }
+}
+
+void io_loop_handler_run_internal(struct ioloop *ioloop)
+{
+ struct ioloop_handler_context *ctx = ioloop->handler_context;
+ struct pollfd *pollfd;
+ struct timeval tv;
+ struct io_file *io;
+ int msecs, ret;
+ bool call;
+
+ /* get the time left for next timeout task */
+ msecs = io_loop_run_get_wait_time(ioloop, &tv);
+#ifdef _AIX
+ if (msecs > 1000) {
+ /* AIX seems to check IO_POLL_ERRORs only at the beginning of
+ the poll() call, not during it. keep timeouts short enough
+ so that we'll notice them pretty quickly. */
+ msecs = 1000;
+ }
+#endif
+
+ ret = poll(ctx->fds, ctx->fds_pos, msecs);
+ if (ret < 0 && errno != EINTR)
+ i_fatal("poll(): %m");
+
+ /* execute timeout handlers */
+ io_loop_handle_timeouts(ioloop);
+
+ if (ret <= 0 || !ioloop->running) {
+ /* no I/O events */
+ return;
+ }
+
+ io = ioloop->io_files;
+ for (; io != NULL && ret > 0; io = ioloop->next_io_file) {
+ ioloop->next_io_file = io->next;
+
+ if (io->fd == -1) {
+ /* io_add_istream() without fd */
+ continue;
+ }
+ pollfd = &ctx->fds[ctx->fd_index[io->fd]];
+ if (pollfd->revents != 0) {
+ if (pollfd->revents & POLLNVAL) {
+ i_error("invalid I/O fd %d, callback %p",
+ io->fd, (void *) io->io.callback);
+ pollfd->events = 0;
+ pollfd->revents = 0;
+ call = TRUE;
+ } else if ((io->io.condition &
+ (IO_READ|IO_WRITE)) == (IO_READ|IO_WRITE)) {
+ call = TRUE;
+ pollfd->revents = 0;
+ } else if ((io->io.condition & IO_READ) != 0) {
+ call = (pollfd->revents & IO_POLL_INPUT) != 0;
+ pollfd->revents &= ENUM_NEGATE(IO_POLL_INPUT);
+ } else if ((io->io.condition & IO_WRITE) != 0) {
+ call = (pollfd->revents & IO_POLL_OUTPUT) != 0;
+ pollfd->revents &= ENUM_NEGATE(IO_POLL_OUTPUT);
+ } else if ((io->io.condition & IO_ERROR) != 0) {
+ call = (pollfd->revents & IO_POLL_ERROR) != 0;
+ pollfd->revents &= ENUM_NEGATE(IO_POLL_ERROR);
+ } else {
+ call = FALSE;
+ }
+
+ if (pollfd->revents == 0)
+ ret--;
+
+ if (call) {
+ io_loop_call_io(&io->io);
+ if (!ioloop->running)
+ return;
+ }
+ }
+ }
+}
+
+#endif
diff --git a/src/lib/ioloop-private.h b/src/lib/ioloop-private.h
new file mode 100644
index 0000000..799ca4d
--- /dev/null
+++ b/src/lib/ioloop-private.h
@@ -0,0 +1,129 @@
+#ifndef IOLOOP_PRIVATE_H
+#define IOLOOP_PRIVATE_H
+
+#include "priorityq.h"
+#include "ioloop.h"
+#include "array-decl.h"
+
+#ifndef IOLOOP_INITIAL_FD_COUNT
+# define IOLOOP_INITIAL_FD_COUNT 128
+#endif
+
+struct ioloop {
+ struct ioloop *prev;
+
+ struct ioloop_context *cur_ctx;
+
+ struct io_file *io_files;
+ struct io_file *next_io_file;
+ struct priorityq *timeouts;
+ ARRAY(struct timeout *) timeouts_new;
+ struct io_wait_timer *wait_timers;
+
+ struct ioloop_handler_context *handler_context;
+ struct ioloop_notify_handler_context *notify_handler_context;
+ unsigned int max_fd_count;
+
+ io_loop_time_moved_callback_t *time_moved_callback;
+ struct timeval next_max_time;
+ uint64_t ioloop_wait_usecs;
+ struct timeval wait_started;
+
+ unsigned int io_pending_count;
+
+ bool running:1;
+ bool iolooping:1;
+ bool stop_after_run_loop:1;
+};
+
+struct io {
+ enum io_condition condition;
+ const char *source_filename;
+ unsigned int source_linenum;
+ /* trigger I/O callback even if OS doesn't think there is input
+ pending */
+ bool pending;
+ /* This IO event shouldn't be the only thing being waited on, because
+ it would just result in infinite wait. */
+ bool never_wait_alone;
+
+ io_callback_t *callback;
+ void *context;
+
+ struct ioloop *ioloop;
+ struct ioloop_context *ctx;
+};
+
+struct io_file {
+ struct io io;
+
+ /* use a doubly linked list so that io_remove() is quick */
+ struct io_file *prev, *next;
+
+ int refcount;
+ int fd;
+
+ /* only for io_add_istream(), a bit kludgy to be here.. */
+ struct istream *istream;
+};
+
+struct timeout {
+ struct priorityq_item item;
+ const char *source_filename;
+ unsigned int source_linenum;
+
+ unsigned int msecs;
+ struct timeval next_run;
+
+ timeout_callback_t *callback;
+ void *context;
+
+ struct ioloop *ioloop;
+ struct ioloop_context *ctx;
+
+ bool one_shot:1;
+};
+
+struct io_wait_timer {
+ struct io_wait_timer *prev, *next;
+ const char *source_filename;
+ unsigned int source_linenum;
+
+ struct ioloop *ioloop;
+ uint64_t usecs;
+};
+
+struct ioloop_context_callback {
+ io_callback_t *activate;
+ io_callback_t *deactivate;
+ void *context;
+ bool activated;
+};
+
+struct ioloop_context {
+ int refcount;
+ struct ioloop *ioloop;
+ ARRAY(struct ioloop_context_callback) callbacks;
+ ARRAY(struct event *) global_event_stack;
+ struct event *root_global_event;
+};
+
+int io_loop_run_get_wait_time(struct ioloop *ioloop, struct timeval *tv_r);
+void io_loop_handle_timeouts(struct ioloop *ioloop);
+void io_loop_call_io(struct io *io);
+
+void io_loop_handler_run_internal(struct ioloop *ioloop);
+
+/* I/O handler calls */
+void io_loop_handle_add(struct io_file *io);
+void io_loop_handle_remove(struct io_file *io, bool closed);
+
+void io_loop_handler_init(struct ioloop *ioloop, unsigned int initial_fd_count);
+void io_loop_handler_deinit(struct ioloop *ioloop);
+
+void io_loop_notify_remove(struct io *io);
+void io_loop_notify_handler_deinit(struct ioloop *ioloop);
+
+struct event *io_loop_get_active_global_root(void);
+
+#endif
diff --git a/src/lib/ioloop-select.c b/src/lib/ioloop-select.c
new file mode 100644
index 0000000..d5deb16
--- /dev/null
+++ b/src/lib/ioloop-select.c
@@ -0,0 +1,148 @@
+/* Copyright (c) 2002-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "ioloop-private.h"
+
+#ifdef IOLOOP_SELECT
+
+#ifdef HAVE_SYS_SELECT_H
+# include <sys/select.h> /* According to POSIX 1003.1-2001 */
+#endif
+#include <sys/time.h>
+#include <unistd.h>
+
+struct ioloop_handler_context {
+ int highest_fd;
+ fd_set read_fds, write_fds, except_fds;
+ fd_set tmp_read_fds, tmp_write_fds, tmp_except_fds;
+};
+
+static void update_highest_fd(struct ioloop *ioloop)
+{
+ struct ioloop_handler_context *ctx = ioloop->handler_context;
+ struct io_file *io;
+ int max_highest_fd;
+
+ max_highest_fd = ctx->highest_fd-1;
+ ctx->highest_fd = -1;
+
+ for (io = ioloop->io_files; io != NULL; io = io->next) {
+ if (io->fd <= ctx->highest_fd)
+ continue;
+
+ ctx->highest_fd = io->fd;
+
+ if (ctx->highest_fd == max_highest_fd)
+ break;
+ }
+}
+
+void io_loop_handler_init(struct ioloop *ioloop,
+ unsigned int initial_fd_count ATTR_UNUSED)
+{
+ struct ioloop_handler_context *ctx;
+
+ ioloop->handler_context = ctx = i_new(struct ioloop_handler_context, 1);
+ ctx->highest_fd = -1;
+ FD_ZERO(&ctx->read_fds);
+ FD_ZERO(&ctx->write_fds);
+ FD_ZERO(&ctx->except_fds);
+}
+
+void io_loop_handler_deinit(struct ioloop *ioloop)
+{
+ i_free(ioloop->handler_context);
+}
+
+void io_loop_handle_add(struct io_file *io)
+{
+ struct ioloop_handler_context *ctx = io->io.ioloop->handler_context;
+ enum io_condition condition = io->io.condition;
+ int fd = io->fd;
+
+ i_assert(fd >= 0);
+
+ if (fd >= FD_SETSIZE)
+ i_fatal("fd %d too large for select()", fd);
+
+ if ((condition & (IO_READ | IO_ERROR)) != 0)
+ FD_SET(fd, &ctx->read_fds);
+ if ((condition & IO_WRITE) != 0)
+ FD_SET(fd, &ctx->write_fds);
+ FD_SET(fd, &ctx->except_fds);
+
+ if (io->fd > ctx->highest_fd)
+ ctx->highest_fd = io->fd;
+}
+
+void io_loop_handle_remove(struct io_file *io, bool closed ATTR_UNUSED)
+{
+ struct ioloop_handler_context *ctx = io->io.ioloop->handler_context;
+ enum io_condition condition = io->io.condition;
+ int fd = io->fd;
+
+ i_assert(fd >= 0 && fd < FD_SETSIZE);
+
+ if ((condition & (IO_READ | IO_ERROR)) != 0)
+ FD_CLR(fd, &ctx->read_fds);
+ if ((condition & IO_WRITE) != 0)
+ FD_CLR(fd, &ctx->write_fds);
+
+ if (!FD_ISSET(fd, &ctx->read_fds) && !FD_ISSET(fd, &ctx->write_fds)) {
+ FD_CLR(fd, &ctx->except_fds);
+
+ /* check if we removed the highest fd */
+ if (io->fd == ctx->highest_fd)
+ update_highest_fd(io->io.ioloop);
+ }
+ i_free(io);
+}
+
+#define io_check_condition(ctx, fd, cond) \
+ ((FD_ISSET((fd), &(ctx)->tmp_read_fds) && ((cond) & (IO_READ|IO_ERROR)) != 0) || \
+ (FD_ISSET((fd), &(ctx)->tmp_write_fds) && ((cond) & IO_WRITE) != 0) || \
+ (FD_ISSET((fd), &(ctx)->tmp_except_fds)))
+
+void io_loop_handler_run_internal(struct ioloop *ioloop)
+{
+ struct ioloop_handler_context *ctx = ioloop->handler_context;
+ struct timeval tv;
+ struct io_file *io;
+ int ret;
+
+ /* get the time left for next timeout task */
+ io_loop_run_get_wait_time(ioloop, &tv);
+
+ memcpy(&ctx->tmp_read_fds, &ctx->read_fds, sizeof(fd_set));
+ memcpy(&ctx->tmp_write_fds, &ctx->write_fds, sizeof(fd_set));
+ memcpy(&ctx->tmp_except_fds, &ctx->except_fds, sizeof(fd_set));
+
+ ret = select(ctx->highest_fd + 1, &ctx->tmp_read_fds,
+ &ctx->tmp_write_fds, &ctx->tmp_except_fds, &tv);
+ if (ret < 0 && errno != EINTR)
+ i_warning("select() : %m");
+
+ /* execute timeout handlers */
+ io_loop_handle_timeouts(ioloop);
+
+ if (ret <= 0 || !ioloop->running) {
+ /* no I/O events */
+ return;
+ }
+
+ io = ioloop->io_files;
+ for (; io != NULL && ret > 0; io = ioloop->next_io_file) {
+ ioloop->next_io_file = io->next;
+
+ if (io->fd == -1) {
+ /* io_add_istream() without fd */
+ } else if (io_check_condition(ctx, io->fd, io->io.condition)) {
+ ret--;
+ io_loop_call_io(&io->io);
+ if (!ioloop->running)
+ break;
+ }
+ }
+}
+
+#endif
diff --git a/src/lib/ioloop.c b/src/lib/ioloop.c
new file mode 100644
index 0000000..e248808
--- /dev/null
+++ b/src/lib/ioloop.c
@@ -0,0 +1,1389 @@
+/* Copyright (c) 2002-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "backtrace-string.h"
+#include "llist.h"
+#include "time-util.h"
+#include "istream-private.h"
+#include "ioloop-private.h"
+
+#include <unistd.h>
+
+/* Dovecot attempts to detect also when time suddenly jumps forwards.
+ This is done by getting the minimum timeout wait in epoll() (or similar)
+ and then seeing if the current time after epoll() is past the timeout.
+ This can't be very exact, so likely the difference is always at least
+ 1 microsecond. In high load situations it can be somewhat higher.
+ Dovecot generally doesn't have very important short timeouts, so to avoid
+ logging many warnings about this, use a rather high value. */
+#define IOLOOP_TIME_MOVED_FORWARDS_MIN_USECS (100000)
+
+time_t ioloop_time = 0;
+struct timeval ioloop_timeval;
+struct ioloop *current_ioloop = NULL;
+uint64_t ioloop_global_wait_usecs = 0;
+
+static ARRAY(io_switch_callback_t *) io_switch_callbacks = ARRAY_INIT;
+static ARRAY(io_destroy_callback_t *) io_destroy_callbacks = ARRAY_INIT;
+static bool panic_on_leak = FALSE, panic_on_leak_set = FALSE;
+
+static time_t data_stack_last_free_unused = 0;
+
+static void io_loop_initialize_handler(struct ioloop *ioloop)
+{
+ unsigned int initial_fd_count;
+
+ initial_fd_count = ioloop->max_fd_count > 0 &&
+ ioloop->max_fd_count < IOLOOP_INITIAL_FD_COUNT ?
+ ioloop->max_fd_count : IOLOOP_INITIAL_FD_COUNT;
+ io_loop_handler_init(ioloop, initial_fd_count);
+}
+
+static struct io_file *
+io_add_file(struct ioloop *ioloop, int fd, enum io_condition condition,
+ const char *source_filename,
+ unsigned int source_linenum,
+ io_callback_t *callback, void *context)
+{
+ struct io_file *io;
+
+ i_assert(callback != NULL);
+ i_assert((condition & IO_NOTIFY) == 0);
+
+ io = i_new(struct io_file, 1);
+ io->io.condition = condition;
+ io->io.callback = callback;
+ io->io.context = context;
+ io->io.ioloop = ioloop;
+ io->io.source_filename = source_filename;
+ io->io.source_linenum = source_linenum;
+ io->refcount = 1;
+ io->fd = fd;
+
+ if (io->io.ioloop->cur_ctx != NULL) {
+ io->io.ctx = io->io.ioloop->cur_ctx;
+ io_loop_context_ref(io->io.ctx);
+ }
+
+ if (io->io.ioloop->handler_context == NULL)
+ io_loop_initialize_handler(io->io.ioloop);
+ if (fd != -1)
+ io_loop_handle_add(io);
+ else {
+ /* we're adding an istream whose only way to get notified
+ is to call i_stream_set_input_pending() */
+ }
+
+ if (io->io.ioloop->io_files != NULL) {
+ io->io.ioloop->io_files->prev = io;
+ io->next = io->io.ioloop->io_files;
+ }
+ io->io.ioloop->io_files = io;
+ return io;
+}
+
+#undef io_add_to
+struct io *io_add_to(struct ioloop *ioloop, int fd, enum io_condition condition,
+ const char *source_filename, unsigned int source_linenum,
+ io_callback_t *callback, void *context)
+{
+ struct io_file *io;
+
+ i_assert(fd >= 0);
+ io = io_add_file(ioloop, fd, condition,
+ source_filename, source_linenum,
+ callback, context);
+ return &io->io;
+}
+
+#undef io_add
+struct io *io_add(int fd, enum io_condition condition,
+ const char *source_filename,
+ unsigned int source_linenum,
+ io_callback_t *callback, void *context)
+{
+ return io_add_to(current_ioloop, fd, condition,
+ source_filename, source_linenum,
+ callback, context);
+}
+
+#undef io_add_istream_to
+struct io *io_add_istream_to(struct ioloop *ioloop, struct istream *input,
+ const char *source_filename,
+ unsigned int source_linenum,
+ io_callback_t *callback, void *context)
+{
+ struct io_file *io;
+
+ io = io_add_file(ioloop, i_stream_get_fd(input), IO_READ,
+ source_filename, source_linenum, callback, context);
+ io->istream = input;
+ i_stream_ref(io->istream);
+ i_stream_set_io(io->istream, &io->io);
+ return &io->io;
+}
+
+#undef io_add_istream
+struct io *io_add_istream(struct istream *input, const char *source_filename,
+ unsigned int source_linenum,
+ io_callback_t *callback, void *context)
+{
+ return io_add_istream_to(current_ioloop, input,
+ source_filename, source_linenum,
+ callback, context);
+}
+
+static void io_file_unlink(struct io_file *io)
+{
+ if (io->prev != NULL)
+ io->prev->next = io->next;
+ else
+ io->io.ioloop->io_files = io->next;
+
+ if (io->next != NULL)
+ io->next->prev = io->prev;
+
+ /* if we got here from an I/O handler callback, make sure we
+ don't try to handle this one next. */
+ if (io->io.ioloop->next_io_file == io)
+ io->io.ioloop->next_io_file = io->next;
+}
+
+static void io_remove_full(struct io **_io, bool closed)
+{
+ struct io *io = *_io;
+
+ i_assert(io->callback != NULL);
+
+ *_io = NULL;
+
+ /* make sure the callback doesn't get called anymore.
+ kqueue code relies on this. */
+ io->callback = NULL;
+
+ if (io->pending) {
+ i_assert(io->ioloop->io_pending_count > 0);
+ io->ioloop->io_pending_count--;
+ }
+
+ if (io->ctx != NULL)
+ io_loop_context_unref(&io->ctx);
+
+ if ((io->condition & IO_NOTIFY) != 0)
+ io_loop_notify_remove(io);
+ else {
+ struct io_file *io_file = (struct io_file *)io;
+ struct istream *istream = io_file->istream;
+
+ if (istream != NULL) {
+ /* remove io before it's freed */
+ i_stream_unset_io(istream, io);
+ }
+
+ io_file_unlink(io_file);
+ if (io_file->fd != -1)
+ io_loop_handle_remove(io_file, closed);
+ else
+ i_free(io);
+
+ /* remove io from the ioloop before unreferencing the istream,
+ because a destroyed istream may automatically close the
+ fd. */
+ i_stream_unref(&istream);
+ }
+}
+
+void io_remove(struct io **io)
+{
+ if (*io == NULL)
+ return;
+
+ io_remove_full(io, FALSE);
+}
+
+void io_remove_closed(struct io **io)
+{
+ if (*io == NULL)
+ return;
+
+ i_assert(((*io)->condition & IO_NOTIFY) == 0);
+
+ io_remove_full(io, TRUE);
+}
+
+void io_set_pending(struct io *io)
+{
+ i_assert((io->condition & IO_NOTIFY) == 0);
+
+ if (!io->pending) {
+ io->pending = TRUE;
+ io->ioloop->io_pending_count++;
+ }
+}
+
+bool io_is_pending(struct io *io)
+{
+ return io->pending;
+}
+
+void io_set_never_wait_alone(struct io *io, bool set)
+{
+ io->never_wait_alone = set;
+}
+
+static void timeout_update_next(struct timeout *timeout, struct timeval *tv_now)
+{
+ if (tv_now == NULL)
+ i_gettimeofday(&timeout->next_run);
+ else {
+ timeout->next_run.tv_sec = tv_now->tv_sec;
+ timeout->next_run.tv_usec = tv_now->tv_usec;
+ }
+
+ /* we don't want microsecond accuracy or this function will be
+ called all the time - millisecond is more than enough */
+ timeout->next_run.tv_usec -= timeout->next_run.tv_usec % 1000;
+
+ timeout->next_run.tv_sec += timeout->msecs/1000;
+ timeout->next_run.tv_usec += (timeout->msecs%1000)*1000;
+
+ if (timeout->next_run.tv_usec >= 1000000) {
+ timeout->next_run.tv_sec++;
+ timeout->next_run.tv_usec -= 1000000;
+ }
+}
+
+static struct timeout *
+timeout_add_common(struct ioloop *ioloop, const char *source_filename,
+ unsigned int source_linenum,
+ timeout_callback_t *callback, void *context)
+{
+ struct timeout *timeout;
+
+ timeout = i_new(struct timeout, 1);
+ timeout->item.idx = UINT_MAX;
+ timeout->source_filename = source_filename;
+ timeout->source_linenum = source_linenum;
+ timeout->ioloop = ioloop;
+
+ timeout->callback = callback;
+ timeout->context = context;
+
+ if (timeout->ioloop->cur_ctx != NULL) {
+ timeout->ctx = timeout->ioloop->cur_ctx;
+ io_loop_context_ref(timeout->ctx);
+ }
+
+ return timeout;
+}
+
+#undef timeout_add_to
+struct timeout *timeout_add_to(struct ioloop *ioloop, unsigned int msecs,
+ const char *source_filename,
+ unsigned int source_linenum,
+ timeout_callback_t *callback, void *context)
+{
+ struct timeout *timeout;
+
+ timeout = timeout_add_common(ioloop, source_filename, source_linenum,
+ callback, context);
+ timeout->msecs = msecs;
+
+ if (msecs > 0) {
+ /* start this timeout in the next run cycle */
+ array_push_back(&timeout->ioloop->timeouts_new, &timeout);
+ } else {
+ /* Trigger zero timeouts as soon as possible. When ioloop is
+ running, refresh the timestamp to prevent infinite loops
+ in case a timeout callback keeps recreating the 0-timeout. */
+ timeout_update_next(timeout, timeout->ioloop->running ?
+ NULL : &ioloop_timeval);
+ priorityq_add(timeout->ioloop->timeouts, &timeout->item);
+ }
+ return timeout;
+}
+
+#undef timeout_add
+struct timeout *timeout_add(unsigned int msecs, const char *source_filename,
+ unsigned int source_linenum,
+ timeout_callback_t *callback, void *context)
+{
+ return timeout_add_to(current_ioloop, msecs,
+ source_filename, source_linenum,
+ callback, context);
+}
+
+#undef timeout_add_short_to
+struct timeout *
+timeout_add_short_to(struct ioloop *ioloop, unsigned int msecs,
+ const char *source_filename, unsigned int source_linenum,
+ timeout_callback_t *callback, void *context)
+{
+ return timeout_add_to(ioloop, msecs,
+ source_filename, source_linenum,
+ callback, context);
+}
+
+#undef timeout_add_short
+struct timeout *
+timeout_add_short(unsigned int msecs, const char *source_filename,
+ unsigned int source_linenum,
+ timeout_callback_t *callback, void *context)
+{
+ return timeout_add(msecs, source_filename, source_linenum,
+ callback, context);
+}
+
+#undef timeout_add_absolute_to
+struct timeout *
+timeout_add_absolute_to(struct ioloop *ioloop, const struct timeval *time,
+ const char *source_filename,
+ unsigned int source_linenum,
+ timeout_callback_t *callback, void *context)
+{
+ struct timeout *timeout;
+
+ timeout = timeout_add_common(ioloop, source_filename, source_linenum,
+ callback, context);
+ timeout->one_shot = TRUE;
+ timeout->next_run = *time;
+
+ priorityq_add(timeout->ioloop->timeouts, &timeout->item);
+ return timeout;
+}
+
+#undef timeout_add_absolute
+struct timeout *
+timeout_add_absolute(const struct timeval *time,
+ const char *source_filename,
+ unsigned int source_linenum,
+ timeout_callback_t *callback, void *context)
+{
+ return timeout_add_absolute_to(current_ioloop, time,
+ source_filename, source_linenum,
+ callback, context);
+}
+
+static struct timeout *
+timeout_copy(const struct timeout *old_to, struct ioloop *ioloop)
+{
+ struct timeout *new_to;
+
+ new_to = timeout_add_common(ioloop,
+ old_to->source_filename, old_to->source_linenum,
+ old_to->callback, old_to->context);
+ new_to->one_shot = old_to->one_shot;
+ new_to->msecs = old_to->msecs;
+ new_to->next_run = old_to->next_run;
+
+ if (old_to->item.idx != UINT_MAX)
+ priorityq_add(new_to->ioloop->timeouts, &new_to->item);
+ else if (!new_to->one_shot) {
+ i_assert(new_to->msecs > 0);
+ array_push_back(&new_to->ioloop->timeouts_new, &new_to);
+ }
+
+ return new_to;
+}
+
+static void timeout_free(struct timeout *timeout)
+{
+ if (timeout->ctx != NULL)
+ io_loop_context_unref(&timeout->ctx);
+ i_free(timeout);
+}
+
+void timeout_remove(struct timeout **_timeout)
+{
+ struct timeout *timeout = *_timeout;
+ struct ioloop *ioloop;
+
+ if (timeout == NULL)
+ return;
+
+ ioloop = timeout->ioloop;
+
+ *_timeout = NULL;
+ if (timeout->item.idx != UINT_MAX)
+ priorityq_remove(timeout->ioloop->timeouts, &timeout->item);
+ else if (!timeout->one_shot && timeout->msecs > 0) {
+ struct timeout *const *to_idx;
+ array_foreach(&ioloop->timeouts_new, to_idx) {
+ if (*to_idx == timeout) {
+ array_delete(&ioloop->timeouts_new,
+ array_foreach_idx(&ioloop->timeouts_new, to_idx), 1);
+ break;
+ }
+ }
+ }
+ timeout_free(timeout);
+}
+
+static void ATTR_NULL(2)
+timeout_reset_timeval(struct timeout *timeout, struct timeval *tv_now)
+{
+ if (timeout->item.idx == UINT_MAX)
+ return;
+
+ timeout_update_next(timeout, tv_now);
+ /* If we came here from io_loop_handle_timeouts_real(), next_run must
+ be larger than tv_now or it can go to infinite loop. This would
+ mainly happen with 0 ms timeouts. Avoid this by making sure
+ next_run is at least 1 us higher than tv_now.
+
+ Note that some callers (like master process's process_min_avail
+ preforking timeout) really do want the 0 ms timeout to trigger
+ multiple times as rapidly as it can (but in separate ioloop runs).
+ So don't increase it more than by 1 us. */
+ if (tv_now != NULL && timeval_cmp(&timeout->next_run, tv_now) <= 0) {
+ timeout->next_run = *tv_now;
+ timeval_add_usecs(&timeout->next_run, 1);
+ }
+ priorityq_remove(timeout->ioloop->timeouts, &timeout->item);
+ priorityq_add(timeout->ioloop->timeouts, &timeout->item);
+}
+
+void timeout_reset(struct timeout *timeout)
+{
+ i_assert(!timeout->one_shot);
+ timeout_reset_timeval(timeout, NULL);
+}
+
+static int timeout_get_wait_time(struct timeout *timeout, struct timeval *tv_r,
+ struct timeval *tv_now, bool in_timeout_loop)
+{
+ int ret;
+
+ if (tv_now->tv_sec == 0)
+ i_gettimeofday(tv_now);
+ tv_r->tv_sec = tv_now->tv_sec;
+ tv_r->tv_usec = tv_now->tv_usec;
+
+ i_assert(tv_r->tv_sec > 0);
+ i_assert(timeout->next_run.tv_sec > 0);
+
+ tv_r->tv_sec = timeout->next_run.tv_sec - tv_r->tv_sec;
+ tv_r->tv_usec = timeout->next_run.tv_usec - tv_r->tv_usec;
+ if (tv_r->tv_usec < 0) {
+ tv_r->tv_sec--;
+ tv_r->tv_usec += 1000000;
+ }
+
+ if (tv_r->tv_sec < 0) {
+ /* The timeout should have been called already */
+ tv_r->tv_sec = 0;
+ tv_r->tv_usec = 0;
+ return 0;
+ }
+ if (tv_r->tv_sec == 0 && tv_r->tv_usec == 1 && !in_timeout_loop) {
+ /* Possibly 0 ms timeout. Don't wait for a full millisecond
+ for it to trigger. */
+ tv_r->tv_usec = 0;
+ return 0;
+ }
+ if (tv_r->tv_sec > INT_MAX/1000-1)
+ tv_r->tv_sec = INT_MAX/1000-1;
+
+ /* round wait times up to next millisecond */
+ ret = tv_r->tv_sec * 1000 + (tv_r->tv_usec + 999) / 1000;
+ i_assert(ret >= 0 && tv_r->tv_sec >= 0 && tv_r->tv_usec >= 0);
+ return ret;
+}
+
+static int io_loop_get_wait_time(struct ioloop *ioloop, struct timeval *tv_r)
+{
+ struct timeval tv_now;
+ struct priorityq_item *item;
+ struct timeout *timeout;
+ int msecs;
+
+ item = priorityq_peek(ioloop->timeouts);
+ timeout = (struct timeout *)item;
+
+ /* we need to see if there are pending IO waiting,
+ if there is, we set msecs = 0 to ensure they are
+ processed without delay */
+ if (timeout == NULL && ioloop->io_pending_count == 0) {
+ /* no timeouts. use INT_MAX msecs for timeval and
+ return -1 for poll/epoll infinity. */
+ tv_r->tv_sec = INT_MAX / 1000;
+ tv_r->tv_usec = 0;
+ ioloop->next_max_time.tv_sec = (1ULL << (TIME_T_MAX_BITS-1)) - 1;
+ ioloop->next_max_time.tv_usec = 0;
+ return -1;
+ }
+
+ if (ioloop->io_pending_count > 0) {
+ i_gettimeofday(&tv_now);
+ msecs = 0;
+ tv_r->tv_sec = 0;
+ tv_r->tv_usec = 0;
+ } else {
+ tv_now.tv_sec = 0;
+ msecs = timeout_get_wait_time(timeout, tv_r, &tv_now, FALSE);
+ }
+ ioloop->next_max_time = tv_now;
+ timeval_add_msecs(&ioloop->next_max_time, msecs);
+
+ /* update ioloop_timeval - this is meant for io_loop_handle_timeouts()'s
+ ioloop_wait_usecs calculation. normally after this we go to the
+ ioloop and after that we update ioloop_timeval immediately again. */
+ ioloop_timeval = tv_now;
+ ioloop_time = tv_now.tv_sec;
+ i_assert(msecs == 0 || timeout->msecs > 0 || timeout->one_shot);
+ return msecs;
+}
+
+static bool io_loop_have_waitable_io_files(struct ioloop *ioloop)
+{
+ struct io_file *io;
+
+ for (io = ioloop->io_files; io != NULL; io = io->next) {
+ if (io->io.callback != NULL && !io->io.never_wait_alone)
+ return TRUE;
+ }
+ return FALSE;
+}
+
+int io_loop_run_get_wait_time(struct ioloop *ioloop, struct timeval *tv_r)
+{
+ int msecs = io_loop_get_wait_time(ioloop, tv_r);
+ if (msecs < 0 && !io_loop_have_waitable_io_files(ioloop))
+ i_panic("BUG: No IOs or timeouts set. Not waiting for infinity.");
+ return msecs;
+}
+
+static int timeout_cmp(const void *p1, const void *p2)
+{
+ const struct timeout *to1 = p1, *to2 = p2;
+
+ return timeval_cmp(&to1->next_run, &to2->next_run);
+}
+
+static void
+io_loop_default_time_moved(const struct timeval *old_time,
+ const struct timeval *new_time)
+{
+ long long diff = timeval_diff_usecs(old_time, new_time);
+ if (diff > 0) {
+ i_warning("Time moved backwards by %lld.%06lld seconds.",
+ diff / 1000000, diff % 1000000);
+ }
+}
+
+static void io_loop_timeouts_start_new(struct ioloop *ioloop)
+{
+ struct timeout *timeout;
+
+ if (array_count(&ioloop->timeouts_new) == 0)
+ return;
+
+ io_loop_time_refresh();
+
+ array_foreach_elem(&ioloop->timeouts_new, timeout) {
+ i_assert(timeout->next_run.tv_sec == 0 &&
+ timeout->next_run.tv_usec == 0);
+ i_assert(!timeout->one_shot);
+ i_assert(timeout->msecs > 0);
+ timeout_update_next(timeout, &ioloop_timeval);
+ priorityq_add(ioloop->timeouts, &timeout->item);
+ }
+ array_clear(&ioloop->timeouts_new);
+}
+
+static void io_loop_timeouts_update(struct ioloop *ioloop, long long diff_usecs)
+{
+ struct priorityq_item *const *items;
+ unsigned int i, count;
+
+ count = priorityq_count(ioloop->timeouts);
+ items = priorityq_items(ioloop->timeouts);
+ for (i = 0; i < count; i++) {
+ struct timeout *to = (struct timeout *)items[i];
+
+ if (diff_usecs > 0)
+ timeval_add_usecs(&to->next_run, diff_usecs);
+ else
+ timeval_sub_usecs(&to->next_run, -diff_usecs);
+ }
+}
+
+static void io_loops_timeouts_update(long long diff_usecs)
+{
+ struct ioloop *ioloop;
+
+ for (ioloop = current_ioloop; ioloop != NULL; ioloop = ioloop->prev)
+ io_loop_timeouts_update(ioloop, diff_usecs);
+}
+
+static void ioloop_add_wait_time(struct ioloop *ioloop)
+{
+ struct io_wait_timer *timer;
+ long long diff;
+
+ diff = timeval_diff_usecs(&ioloop_timeval, &ioloop->wait_started);
+ if (diff < 0) {
+ /* time moved backwards */
+ diff = 0;
+ }
+
+ ioloop->ioloop_wait_usecs += diff;
+ ioloop_global_wait_usecs += diff;
+
+ for (timer = ioloop->wait_timers; timer != NULL; timer = timer->next)
+ timer->usecs += diff;
+}
+
+static void io_loop_handle_timeouts_real(struct ioloop *ioloop)
+{
+ struct priorityq_item *item;
+ struct timeval tv_old, tv, tv_call;
+ long long diff_usecs;
+ data_stack_frame_t t_id;
+
+ tv_old = ioloop_timeval;
+ i_gettimeofday(&ioloop_timeval);
+
+ diff_usecs = timeval_diff_usecs(&ioloop_timeval, &tv_old);
+ if (unlikely(diff_usecs < 0)) {
+ /* time moved backwards */
+ io_loops_timeouts_update(diff_usecs);
+ ioloop->time_moved_callback(&tv_old, &ioloop_timeval);
+ i_assert(ioloop == current_ioloop);
+ /* the callback may have slept, so check the time again. */
+ i_gettimeofday(&ioloop_timeval);
+ } else {
+ diff_usecs = timeval_diff_usecs(&ioloop->next_max_time,
+ &ioloop_timeval);
+ if (unlikely(-diff_usecs >= IOLOOP_TIME_MOVED_FORWARDS_MIN_USECS)) {
+ io_loops_timeouts_update(-diff_usecs);
+ /* time moved forwards */
+ ioloop->time_moved_callback(&ioloop->next_max_time,
+ &ioloop_timeval);
+ i_assert(ioloop == current_ioloop);
+ }
+ ioloop_add_wait_time(ioloop);
+ }
+
+ ioloop_time = ioloop_timeval.tv_sec;
+ tv_call = ioloop_timeval;
+
+ while (ioloop->running &&
+ (item = priorityq_peek(ioloop->timeouts)) != NULL) {
+ struct timeout *timeout = (struct timeout *)item;
+
+ /* use tv_call to make sure we don't get to infinite loop in
+ case callbacks update ioloop_timeval. */
+ if (timeout_get_wait_time(timeout, &tv, &tv_call, TRUE) > 0)
+ break;
+
+ if (timeout->one_shot) {
+ /* remove timeout from queue */
+ priorityq_remove(timeout->ioloop->timeouts, &timeout->item);
+ } else {
+ /* update timeout's next_run and reposition it in the queue */
+ timeout_reset_timeval(timeout, &tv_call);
+ }
+
+ if (timeout->ctx != NULL)
+ io_loop_context_activate(timeout->ctx);
+ t_id = t_push_named("ioloop timeout handler %p",
+ (void *)timeout->callback);
+ timeout->callback(timeout->context);
+ if (!t_pop(&t_id)) {
+ i_panic("Leaked a t_pop() call in timeout handler %p",
+ (void *)timeout->callback);
+ }
+ if (ioloop->cur_ctx != NULL)
+ io_loop_context_deactivate(ioloop->cur_ctx);
+ i_assert(ioloop == current_ioloop);
+ }
+}
+
+void io_loop_handle_timeouts(struct ioloop *ioloop)
+{
+ T_BEGIN {
+ io_loop_handle_timeouts_real(ioloop);
+ } T_END;
+
+ /* Free the unused memory in data stack once per second. This way if
+ the data stack has grown excessively large temporarily, it won't
+ permanently waste memory. And if the data stack grows back to the
+ same large size, re-allocating it once per second doesn't cause
+ performance problems. */
+ if (data_stack_last_free_unused != ioloop_time) {
+ if (data_stack_last_free_unused != 0)
+ data_stack_free_unused();
+ data_stack_last_free_unused = ioloop_time;
+ }
+}
+
+void io_loop_call_io(struct io *io)
+{
+ struct ioloop *ioloop = io->ioloop;
+ data_stack_frame_t t_id;
+
+ if (io->pending) {
+ i_assert(ioloop->io_pending_count > 0);
+ ioloop->io_pending_count--;
+ io->pending = FALSE;
+ }
+
+ if (io->ctx != NULL)
+ io_loop_context_activate(io->ctx);
+ t_id = t_push_named("ioloop handler %p",
+ (void *)io->callback);
+ io->callback(io->context);
+ if (!t_pop(&t_id)) {
+ i_panic("Leaked a t_pop() call in I/O handler %p",
+ (void *)io->callback);
+ }
+ if (ioloop->cur_ctx != NULL)
+ io_loop_context_deactivate(ioloop->cur_ctx);
+ i_assert(ioloop == current_ioloop);
+}
+
+void io_loop_run(struct ioloop *ioloop)
+{
+ if (ioloop->handler_context == NULL)
+ io_loop_initialize_handler(ioloop);
+
+ if (ioloop->cur_ctx != NULL)
+ io_loop_context_deactivate(ioloop->cur_ctx);
+
+ /* recursive io_loop_run() isn't allowed for the same ioloop.
+ it can break backends. */
+ i_assert(!ioloop->iolooping);
+ ioloop->iolooping = TRUE;
+
+ ioloop->running = TRUE;
+ while (ioloop->running)
+ io_loop_handler_run(ioloop);
+ ioloop->iolooping = FALSE;
+}
+
+static void io_loop_call_pending(struct ioloop *ioloop)
+{
+ struct io_file *io;
+
+ while (ioloop->io_pending_count > 0) {
+ io = ioloop->io_files;
+ do {
+ ioloop->next_io_file = io->next;
+ if (io->io.pending)
+ io_loop_call_io(&io->io);
+ if (ioloop->io_pending_count == 0)
+ break;
+ io = ioloop->next_io_file;
+ } while (io != NULL);
+ }
+}
+
+void io_loop_handler_run(struct ioloop *ioloop)
+{
+ i_assert(ioloop == current_ioloop);
+
+ io_loop_timeouts_start_new(ioloop);
+ ioloop->wait_started = ioloop_timeval;
+ io_loop_handler_run_internal(ioloop);
+ io_loop_call_pending(ioloop);
+ if (ioloop->stop_after_run_loop)
+ io_loop_stop(ioloop);
+
+ i_assert(ioloop == current_ioloop);
+}
+
+void io_loop_stop(struct ioloop *ioloop)
+{
+ ioloop->running = FALSE;
+ ioloop->stop_after_run_loop = FALSE;
+}
+
+void io_loop_stop_delayed(struct ioloop *ioloop)
+{
+ ioloop->stop_after_run_loop = TRUE;
+}
+
+void io_loop_set_running(struct ioloop *ioloop)
+{
+ ioloop->running = TRUE;
+}
+
+void io_loop_set_max_fd_count(struct ioloop *ioloop, unsigned int max_fds)
+{
+ ioloop->max_fd_count = max_fds;
+}
+
+bool io_loop_is_running(struct ioloop *ioloop)
+{
+ return ioloop->running;
+}
+
+void io_loop_time_refresh(void)
+{
+ i_gettimeofday(&ioloop_timeval);
+ ioloop_time = ioloop_timeval.tv_sec;
+}
+
+struct ioloop *io_loop_create(void)
+{
+ struct ioloop *ioloop;
+
+ if (!panic_on_leak_set) {
+ panic_on_leak_set = TRUE;
+ panic_on_leak = getenv("CORE_IO_LEAK") != NULL;
+ }
+
+ /* initialize time */
+ i_gettimeofday(&ioloop_timeval);
+ ioloop_time = ioloop_timeval.tv_sec;
+
+ ioloop = i_new(struct ioloop, 1);
+ ioloop->timeouts = priorityq_init(timeout_cmp, 32);
+ i_array_init(&ioloop->timeouts_new, 8);
+
+ ioloop->time_moved_callback = current_ioloop != NULL ?
+ current_ioloop->time_moved_callback :
+ io_loop_default_time_moved;
+
+ ioloop->prev = current_ioloop;
+ io_loop_set_current(ioloop);
+ return ioloop;
+}
+
+void io_loop_destroy(struct ioloop **_ioloop)
+{
+ struct ioloop *ioloop = *_ioloop;
+ struct timeout *to;
+ struct priorityq_item *item;
+ bool leaks = FALSE;
+
+ *_ioloop = NULL;
+
+ /* ->prev won't work unless loops are destroyed in create order */
+ i_assert(ioloop == current_ioloop);
+ if (array_is_created(&io_destroy_callbacks)) {
+ io_destroy_callback_t *callback;
+ array_foreach_elem(&io_destroy_callbacks, callback) T_BEGIN {
+ callback(current_ioloop);
+ } T_END;
+ }
+
+ io_loop_set_current(current_ioloop->prev);
+
+ if (ioloop->notify_handler_context != NULL)
+ io_loop_notify_handler_deinit(ioloop);
+
+ while (ioloop->io_files != NULL) {
+ struct io_file *io = ioloop->io_files;
+ struct io *_io = &io->io;
+ const char *error = t_strdup_printf(
+ "I/O leak: %p (%s:%u, fd %d)",
+ (void *)io->io.callback,
+ io->io.source_filename,
+ io->io.source_linenum, io->fd);
+
+ if (panic_on_leak)
+ i_panic("%s", error);
+ else
+ i_warning("%s", error);
+ io_remove(&_io);
+ leaks = TRUE;
+ }
+ i_assert(ioloop->io_pending_count == 0);
+
+ array_foreach_elem(&ioloop->timeouts_new, to) {
+ const char *error = t_strdup_printf(
+ "Timeout leak: %p (%s:%u)", (void *)to->callback,
+ to->source_filename,
+ to->source_linenum);
+
+ if (panic_on_leak)
+ i_panic("%s", error);
+ else
+ i_warning("%s", error);
+ timeout_free(to);
+ leaks = TRUE;
+ }
+ array_free(&ioloop->timeouts_new);
+
+ while ((item = priorityq_pop(ioloop->timeouts)) != NULL) {
+ struct timeout *to = (struct timeout *)item;
+ const char *error = t_strdup_printf(
+ "Timeout leak: %p (%s:%u)", (void *)to->callback,
+ to->source_filename,
+ to->source_linenum);
+
+ if (panic_on_leak)
+ i_panic("%s", error);
+ else
+ i_warning("%s", error);
+ timeout_free(to);
+ leaks = TRUE;
+ }
+ priorityq_deinit(&ioloop->timeouts);
+
+ while (ioloop->wait_timers != NULL) {
+ struct io_wait_timer *timer = ioloop->wait_timers;
+ const char *error = t_strdup_printf(
+ "IO wait timer leak: %s:%u",
+ timer->source_filename,
+ timer->source_linenum);
+
+ if (panic_on_leak)
+ i_panic("%s", error);
+ else
+ i_warning("%s", error);
+ io_wait_timer_remove(&timer);
+ leaks = TRUE;
+ }
+
+ if (leaks) {
+ const char *backtrace;
+ if (backtrace_get(&backtrace) == 0)
+ i_warning("Raw backtrace for leaks: %s", backtrace);
+ }
+
+ if (ioloop->handler_context != NULL)
+ io_loop_handler_deinit(ioloop);
+ if (ioloop->cur_ctx != NULL)
+ io_loop_context_unref(&ioloop->cur_ctx);
+ i_free(ioloop);
+}
+
+void io_loop_set_time_moved_callback(struct ioloop *ioloop,
+ io_loop_time_moved_callback_t *callback)
+{
+ ioloop->time_moved_callback = callback;
+}
+
+static void io_switch_callbacks_free(void)
+{
+ array_free(&io_switch_callbacks);
+}
+
+static void io_destroy_callbacks_free(void)
+{
+ array_free(&io_destroy_callbacks);
+}
+
+void io_loop_set_current(struct ioloop *ioloop)
+{
+ io_switch_callback_t *callback;
+ struct ioloop *prev_ioloop = current_ioloop;
+
+ if (ioloop == current_ioloop)
+ return;
+
+ current_ioloop = ioloop;
+ if (array_is_created(&io_switch_callbacks)) {
+ array_foreach_elem(&io_switch_callbacks, callback) T_BEGIN {
+ callback(prev_ioloop);
+ } T_END;
+ }
+}
+
+struct ioloop *io_loop_get_root(void)
+{
+ struct ioloop *ioloop = current_ioloop;
+
+ while (ioloop->prev != NULL)
+ ioloop = ioloop->prev;
+ return ioloop;
+}
+
+void io_loop_add_switch_callback(io_switch_callback_t *callback)
+{
+ if (!array_is_created(&io_switch_callbacks)) {
+ i_array_init(&io_switch_callbacks, 4);
+ lib_atexit_priority(io_switch_callbacks_free, LIB_ATEXIT_PRIORITY_LOW);
+ }
+ array_push_back(&io_switch_callbacks, &callback);
+}
+
+void io_loop_remove_switch_callback(io_switch_callback_t *callback)
+{
+ io_switch_callback_t *const *callbackp;
+ unsigned int idx;
+
+ array_foreach(&io_switch_callbacks, callbackp) {
+ if (*callbackp == callback) {
+ idx = array_foreach_idx(&io_switch_callbacks, callbackp);
+ array_delete(&io_switch_callbacks, idx, 1);
+ return;
+ }
+ }
+ i_unreached();
+}
+
+void io_loop_add_destroy_callback(io_destroy_callback_t *callback)
+{
+ if (!array_is_created(&io_destroy_callbacks)) {
+ i_array_init(&io_destroy_callbacks, 4);
+ lib_atexit_priority(io_destroy_callbacks_free, LIB_ATEXIT_PRIORITY_LOW);
+ }
+ array_push_back(&io_destroy_callbacks, &callback);
+}
+
+void io_loop_remove_destroy_callback(io_destroy_callback_t *callback)
+{
+ io_destroy_callback_t *const *callbackp;
+ unsigned int idx;
+
+ array_foreach(&io_destroy_callbacks, callbackp) {
+ if (*callbackp == callback) {
+ idx = array_foreach_idx(&io_destroy_callbacks, callbackp);
+ array_delete(&io_destroy_callbacks, idx, 1);
+ return;
+ }
+ }
+ i_unreached();
+}
+
+struct ioloop_context *io_loop_context_new(struct ioloop *ioloop)
+{
+ struct ioloop_context *ctx;
+
+ ctx = i_new(struct ioloop_context, 1);
+ ctx->refcount = 1;
+ ctx->ioloop = ioloop;
+ i_array_init(&ctx->callbacks, 4);
+ return ctx;
+}
+
+void io_loop_context_ref(struct ioloop_context *ctx)
+{
+ i_assert(ctx->refcount > 0);
+
+ ctx->refcount++;
+}
+
+void io_loop_context_unref(struct ioloop_context **_ctx)
+{
+ struct ioloop_context *ctx = *_ctx;
+
+ *_ctx = NULL;
+
+ i_assert(ctx->refcount > 0);
+ if (--ctx->refcount > 0)
+ return;
+
+ /* cur_ctx itself keeps a reference */
+ i_assert(ctx->ioloop->cur_ctx != ctx);
+
+ array_free(&ctx->callbacks);
+ array_free(&ctx->global_event_stack);
+ i_free(ctx);
+}
+
+#undef io_loop_context_add_callbacks
+void io_loop_context_add_callbacks(struct ioloop_context *ctx,
+ io_callback_t *activate,
+ io_callback_t *deactivate, void *context)
+{
+ struct ioloop_context_callback cb;
+
+ i_zero(&cb);
+ cb.activate = activate;
+ cb.deactivate = deactivate;
+ cb.context = context;
+
+ array_push_back(&ctx->callbacks, &cb);
+}
+
+#undef io_loop_context_remove_callbacks
+void io_loop_context_remove_callbacks(struct ioloop_context *ctx,
+ io_callback_t *activate,
+ io_callback_t *deactivate, void *context)
+{
+ struct ioloop_context_callback *cb;
+
+ array_foreach_modifiable(&ctx->callbacks, cb) {
+ if (cb->context == context &&
+ cb->activate == activate && cb->deactivate == deactivate) {
+ /* simply mark it as deleted, since we could get
+ here from activate/deactivate loop */
+ cb->activate = NULL;
+ cb->deactivate = NULL;
+ cb->context = NULL;
+ return;
+ }
+ }
+ i_panic("io_loop_context_remove_callbacks() context not found");
+}
+
+static void
+io_loop_context_remove_deleted_callbacks(struct ioloop_context *ctx)
+{
+ const struct ioloop_context_callback *cbs;
+ unsigned int i, count;
+
+ cbs = array_get(&ctx->callbacks, &count);
+ for (i = 0; i < count; ) {
+ if (cbs[i].activate != NULL)
+ i++;
+ else {
+ array_delete(&ctx->callbacks, i, 1);
+ cbs = array_get(&ctx->callbacks, &count);
+ }
+ }
+}
+
+static void io_loop_context_push_global_events(struct ioloop_context *ctx)
+{
+ struct event *const *events;
+ unsigned int i, count;
+
+ ctx->root_global_event = event_get_global();
+
+ if (!array_is_created(&ctx->global_event_stack))
+ return;
+
+ /* push the global events from stack in reverse order */
+ events = array_get(&ctx->global_event_stack, &count);
+ if (count == 0)
+ return;
+
+ /* Remember the oldest global event. We're going to pop until that
+ event when deactivating the context. */
+ for (i = count; i > 0; i--)
+ event_push_global(events[i-1]);
+ array_clear(&ctx->global_event_stack);
+}
+
+static void io_loop_context_pop_global_events(struct ioloop_context *ctx)
+{
+ struct event *event;
+
+ /* ioloop context is always global, so we can't push one ioloop context
+ on top of another one. We'll need to rewind the global event stack
+ until we've reached the event that started this context. We'll push
+ these global events back when the ioloop context is activated
+ again. (We'll assert-crash if the root event is freed before these
+ global events have been popped.) */
+ while ((event = event_get_global()) != ctx->root_global_event) {
+ i_assert(event != NULL);
+ if (!array_is_created(&ctx->global_event_stack))
+ i_array_init(&ctx->global_event_stack, 4);
+ array_push_back(&ctx->global_event_stack, &event);
+ event_pop_global(event);
+ }
+ ctx->root_global_event = NULL;
+}
+
+void io_loop_context_activate(struct ioloop_context *ctx)
+{
+ struct ioloop_context_callback *cb;
+
+ i_assert(ctx->ioloop->cur_ctx == NULL);
+
+ ctx->ioloop->cur_ctx = ctx;
+ io_loop_context_push_global_events(ctx);
+ io_loop_context_ref(ctx);
+ array_foreach_modifiable(&ctx->callbacks, cb) {
+ i_assert(!cb->activated);
+ if (cb->activate != NULL) T_BEGIN {
+ cb->activate(cb->context);
+ } T_END;
+ cb->activated = TRUE;
+ }
+}
+
+void io_loop_context_deactivate(struct ioloop_context *ctx)
+{
+ struct ioloop_context_callback *cb;
+
+ i_assert(ctx->ioloop->cur_ctx == ctx);
+
+ array_foreach_modifiable(&ctx->callbacks, cb) {
+ if (!cb->activated) {
+ /* we just added this callback. don't deactivate it
+ before it gets first activated. */
+ } else {
+ if (cb->deactivate != NULL) T_BEGIN {
+ cb->deactivate(cb->context);
+ } T_END;
+ cb->activated = FALSE;
+ }
+ }
+ ctx->ioloop->cur_ctx = NULL;
+ io_loop_context_pop_global_events(ctx);
+ io_loop_context_remove_deleted_callbacks(ctx);
+ io_loop_context_unref(&ctx);
+}
+
+void io_loop_context_switch(struct ioloop_context *ctx)
+{
+ if (ctx->ioloop->cur_ctx != NULL) {
+ if (ctx->ioloop->cur_ctx == ctx)
+ return;
+ io_loop_context_deactivate(ctx->ioloop->cur_ctx);
+ /* deactivation may remove the cur_ctx */
+ if (ctx->ioloop->cur_ctx != NULL)
+ io_loop_context_unref(&ctx->ioloop->cur_ctx);
+ }
+ io_loop_context_activate(ctx);
+}
+
+struct ioloop_context *io_loop_get_current_context(struct ioloop *ioloop)
+{
+ return ioloop->cur_ctx;
+}
+
+struct io *io_loop_move_io_to(struct ioloop *ioloop, struct io **_io)
+{
+ struct io *old_io = *_io;
+ struct io_file *old_io_file, *new_io_file;
+
+ if (old_io == NULL)
+ return NULL;
+
+ i_assert((old_io->condition & IO_NOTIFY) == 0);
+
+ if (old_io->ioloop == ioloop)
+ return old_io;
+
+ old_io_file = (struct io_file *)old_io;
+ new_io_file = io_add_file(ioloop, old_io_file->fd,
+ old_io->condition, old_io->source_filename,
+ old_io->source_linenum,
+ old_io->callback, old_io->context);
+ if (old_io_file->istream != NULL) {
+ /* reference before io_remove() */
+ new_io_file->istream = old_io_file->istream;
+ i_stream_ref(new_io_file->istream);
+ }
+ if (old_io->pending)
+ io_set_pending(&new_io_file->io);
+ io_remove(_io);
+ if (new_io_file->istream != NULL) {
+ /* update istream io after it was removed with io_remove() */
+ i_stream_set_io(new_io_file->istream, &new_io_file->io);
+ }
+ return &new_io_file->io;
+}
+
+struct io *io_loop_move_io(struct io **_io)
+{
+ return io_loop_move_io_to(current_ioloop, _io);
+}
+
+struct timeout *io_loop_move_timeout_to(struct ioloop *ioloop,
+ struct timeout **_timeout)
+{
+ struct timeout *new_to, *old_to = *_timeout;
+
+ if (old_to == NULL || old_to->ioloop == ioloop)
+ return old_to;
+
+ new_to = timeout_copy(old_to, ioloop);
+ timeout_remove(_timeout);
+ return new_to;
+}
+
+struct timeout *io_loop_move_timeout(struct timeout **_timeout)
+{
+ return io_loop_move_timeout_to(current_ioloop, _timeout);
+}
+
+bool io_loop_have_ios(struct ioloop *ioloop)
+{
+ return ioloop->io_files != NULL;
+}
+
+bool io_loop_have_immediate_timeouts(struct ioloop *ioloop)
+{
+ struct timeval tv;
+
+ return io_loop_get_wait_time(ioloop, &tv) == 0;
+}
+
+bool io_loop_is_empty(struct ioloop *ioloop)
+{
+ return ioloop->io_files == NULL &&
+ priorityq_count(ioloop->timeouts) == 0 &&
+ array_count(&ioloop->timeouts_new) == 0;
+}
+
+uint64_t io_loop_get_wait_usecs(struct ioloop *ioloop)
+{
+ return ioloop->ioloop_wait_usecs;
+}
+
+enum io_condition io_loop_find_fd_conditions(struct ioloop *ioloop, int fd)
+{
+ enum io_condition conditions = 0;
+ struct io_file *io;
+
+ i_assert(fd >= 0);
+
+ for (io = ioloop->io_files; io != NULL; io = io->next) {
+ if (io->fd == fd)
+ conditions |= io->io.condition;
+ }
+ return conditions;
+}
+
+#undef io_wait_timer_add_to
+struct io_wait_timer *
+io_wait_timer_add_to(struct ioloop *ioloop, const char *source_filename,
+ unsigned int source_linenum)
+{
+ struct io_wait_timer *timer;
+
+ timer = i_new(struct io_wait_timer, 1);
+ timer->ioloop = ioloop;
+ timer->source_filename = source_filename;
+ timer->source_linenum = source_linenum;
+ DLLIST_PREPEND(&ioloop->wait_timers, timer);
+ return timer;
+}
+
+#undef io_wait_timer_add
+struct io_wait_timer *
+io_wait_timer_add(const char *source_filename, unsigned int source_linenum)
+{
+ return io_wait_timer_add_to(current_ioloop, source_filename,
+ source_linenum);
+}
+
+struct io_wait_timer *io_wait_timer_move_to(struct io_wait_timer **_timer,
+ struct ioloop *ioloop)
+{
+ struct io_wait_timer *timer = *_timer;
+
+ *_timer = NULL;
+ DLLIST_REMOVE(&timer->ioloop->wait_timers, timer);
+ DLLIST_PREPEND(&ioloop->wait_timers, timer);
+ timer->ioloop = ioloop;
+ return timer;
+}
+
+struct io_wait_timer *io_wait_timer_move(struct io_wait_timer **_timer)
+{
+ return io_wait_timer_move_to(_timer, current_ioloop);
+}
+
+void io_wait_timer_remove(struct io_wait_timer **_timer)
+{
+ struct io_wait_timer *timer = *_timer;
+
+ *_timer = NULL;
+ DLLIST_REMOVE(&timer->ioloop->wait_timers, timer);
+ i_free(timer);
+}
+
+uint64_t io_wait_timer_get_usecs(struct io_wait_timer *timer)
+{
+ return timer->usecs;
+}
+
+struct event *io_loop_get_active_global_root(void)
+{
+ if (current_ioloop == NULL)
+ return NULL;
+ if (current_ioloop->cur_ctx == NULL)
+ return NULL;
+ return current_ioloop->cur_ctx->root_global_event;
+}
diff --git a/src/lib/ioloop.h b/src/lib/ioloop.h
new file mode 100644
index 0000000..32e2892
--- /dev/null
+++ b/src/lib/ioloop.h
@@ -0,0 +1,323 @@
+#ifndef IOLOOP_H
+#define IOLOOP_H
+
+#include <sys/time.h>
+#include <time.h>
+
+struct io;
+struct timeout;
+struct ioloop;
+struct istream;
+
+enum io_condition {
+ IO_READ = 0x01,
+ IO_WRITE = 0x02,
+ /* IO_ERROR can be used to check when writable pipe's reader side
+ closes the pipe. For other uses IO_READ should work just as well. */
+ IO_ERROR = 0x04,
+
+ /* internal */
+ IO_NOTIFY = 0x08
+};
+
+enum io_notify_result {
+ /* Notify added successfully */
+ IO_NOTIFY_ADDED,
+ /* Specified file doesn't exist, can't wait on it */
+ IO_NOTIFY_NOTFOUND,
+ /* Can't add notify for specified file. Main reasons for this:
+ a) No notify support at all, b) Only directory notifies supported */
+ IO_NOTIFY_NOSUPPORT
+};
+
+typedef void io_callback_t(void *context);
+typedef void timeout_callback_t(void *context);
+typedef void io_loop_time_moved_callback_t(const struct timeval *old_time,
+ const struct timeval *new_time);
+typedef void io_switch_callback_t(struct ioloop *prev_ioloop);
+typedef void io_destroy_callback_t(struct ioloop *ioloop);
+
+/* Time when the I/O loop started calling handlers.
+ Can be used instead of time(NULL). */
+extern time_t ioloop_time;
+extern struct timeval ioloop_timeval;
+
+extern struct ioloop *current_ioloop;
+/* Number of microseconds spent on all the ioloops waiting for themselves. */
+extern uint64_t ioloop_global_wait_usecs;
+
+/* You can create different handlers for IO_READ and IO_WRITE. IO_READ and
+ IO_ERROR can't use different handlers (and there's no point anyway).
+
+ Don't try to add multiple handlers for the same type. It's not checked and
+ the behavior will be undefined. */
+struct io *io_add(int fd, enum io_condition condition,
+ const char *source_filename,
+ unsigned int source_linenum,
+ io_callback_t *callback, void *context) ATTR_NULL(5);
+#define io_add(fd, condition, callback, context) \
+ io_add(fd, condition, __FILE__, __LINE__ - \
+ CALLBACK_TYPECHECK(callback, void (*)(typeof(context))), \
+ (io_callback_t *)callback, context)
+struct io *io_add_to(struct ioloop *ioloop, int fd, enum io_condition condition,
+ const char *source_filename,
+ unsigned int source_linenum,
+ io_callback_t *callback, void *context) ATTR_NULL(5);
+#define io_add_to(ioloop, fd, condition, callback, context) \
+ io_add_to(ioloop, fd, condition, __FILE__, __LINE__ - \
+ CALLBACK_TYPECHECK(callback, void (*)(typeof(context))), \
+ (io_callback_t *)callback, context)
+
+enum io_notify_result
+io_add_notify(const char *path, const char *source_filename,
+ unsigned int source_linenum,
+ io_callback_t *callback, void *context,
+ struct io **io_r) ATTR_NULL(3);
+#define io_add_notify(path, callback, context, io_r) \
+ io_add_notify(path, __FILE__, __LINE__ - \
+ CALLBACK_TYPECHECK(callback, void (*)(typeof(context))), \
+ (io_callback_t *)callback, context, io_r)
+
+struct io *io_add_istream(struct istream *input, const char *source_filename,
+ unsigned int source_linenum,
+ io_callback_t *callback, void *context) ATTR_NULL(3);
+#define io_add_istream(input, callback, context) \
+ io_add_istream(input, __FILE__, __LINE__ - \
+ CALLBACK_TYPECHECK(callback, void (*)(typeof(context))), \
+ (io_callback_t *)callback, context)
+struct io *io_add_istream_to(struct ioloop *ioloop, struct istream *input,
+ const char *source_filename,
+ unsigned int source_linenum,
+ io_callback_t *callback, void *context)
+ ATTR_NULL(3);
+#define io_add_istream_to(ioloop, input, callback, context) \
+ io_add_istream_to(ioloop, input, __FILE__, __LINE__ - \
+ CALLBACK_TYPECHECK(callback, void (*)(typeof(context))), \
+ (io_callback_t *)callback, context)
+
+/* Remove I/O handler, and set io pointer to NULL. */
+void io_remove(struct io **io);
+/* Like io_remove(), but assume that the file descriptor is already closed.
+ With some backends this simply frees the memory. */
+void io_remove_closed(struct io **io);
+
+/* Make sure the I/O callback is called by io_loop_run() even if there isn't
+ any input actually pending currently as seen by the OS. This may be useful
+ if some of the input has already read into some internal buffer and the
+ caller wants to handle it the same way as if the fd itself had input. */
+void io_set_pending(struct io *io);
+/* Returns TRUE if io_set_pending() has been called for the IO and its callback
+ hasn't been called yet. */
+bool io_is_pending(struct io *io);
+/* If set, this IO shouldn't be the only thing being waited on, because
+ it would just result in infinite wait. In those situations rather just
+ crash to indicate that there's a bug. */
+void io_set_never_wait_alone(struct io *io, bool set);
+
+/* Timeout handlers */
+struct timeout *
+timeout_add(unsigned int msecs, const char *source_filename,
+ unsigned int source_linenum,
+ timeout_callback_t *callback, void *context) ATTR_NULL(4);
+#define timeout_add(msecs, callback, context) \
+ timeout_add(msecs, __FILE__, __LINE__ - \
+ CALLBACK_TYPECHECK(callback, void (*)(typeof(context))) - \
+ COMPILE_ERROR_IF_TRUE(__builtin_constant_p(msecs) && \
+ ((msecs) > 0 && (msecs) < 1000)), \
+ (io_callback_t *)callback, context)
+struct timeout *
+timeout_add_to(struct ioloop *ioloop, unsigned int msecs,
+ const char *source_filename, unsigned int source_linenum,
+ timeout_callback_t *callback, void *context) ATTR_NULL(4);
+#define timeout_add_to(ioloop, msecs, callback, context) \
+ timeout_add_to(ioloop, msecs, __FILE__, __LINE__ - \
+ CALLBACK_TYPECHECK(callback, void (*)(typeof(context))) - \
+ COMPILE_ERROR_IF_TRUE(__builtin_constant_p(msecs) && \
+ ((msecs) > 0 && (msecs) < 1000)), \
+ (io_callback_t *)callback, context)
+
+struct timeout *
+timeout_add_short(unsigned int msecs, const char *source_filename,
+ unsigned int source_linenum,
+ timeout_callback_t *callback, void *context) ATTR_NULL(4);
+#define timeout_add_short(msecs, callback, context) \
+ timeout_add_short(msecs, __FILE__, __LINE__ - \
+ CALLBACK_TYPECHECK(callback, void (*)(typeof(context))), \
+ (io_callback_t *)callback, context)
+struct timeout *
+timeout_add_short_to(struct ioloop *ioloop, unsigned int msecs,
+ const char *source_filename, unsigned int source_linenum,
+ timeout_callback_t *callback, void *context) ATTR_NULL(4);
+#define timeout_add_short_to(ioloop, msecs, callback, context) \
+ timeout_add_short_to(ioloop, msecs, __FILE__, __LINE__ - \
+ CALLBACK_TYPECHECK(callback, void (*)(typeof(context))), \
+ (io_callback_t *)callback, context)
+
+struct timeout *
+timeout_add_absolute(const struct timeval *time,
+ const char *source_filename,
+ unsigned int source_linenum,
+ timeout_callback_t *callback, void *context) ATTR_NULL(4);
+#define timeout_add_absolute(time, callback, context) \
+ timeout_add_absolute(time, __FILE__, __LINE__ - \
+ CALLBACK_TYPECHECK(callback, void (*)(typeof(context))), \
+ (io_callback_t *)callback, context)
+struct timeout *
+timeout_add_absolute_to(struct ioloop *ioloop,
+ const struct timeval *time,
+ const char *source_filename,
+ unsigned int source_linenum,
+ timeout_callback_t *callback, void *context) ATTR_NULL(4);
+#define timeout_add_absolute_to(ioloop, time, callback, context) \
+ timeout_add_absolute_to(ioloop, time, __FILE__, __LINE__ - \
+ CALLBACK_TYPECHECK(callback, void (*)(typeof(context))), \
+ (io_callback_t *)callback, context)
+
+/* Remove timeout handler, and set timeout pointer to NULL. */
+void timeout_remove(struct timeout **timeout);
+/* Reset timeout so it's next run after now+msecs. */
+void timeout_reset(struct timeout *timeout);
+
+/* Refresh ioloop_time and ioloop_timeval variables. */
+void io_loop_time_refresh(void);
+
+void io_loop_run(struct ioloop *ioloop);
+/* Stop the ioloop immediately. No further IO or timeout callbacks are called.
+ Warning: This is not safe to be called in non-delayed signal handlers. */
+void io_loop_stop(struct ioloop *ioloop);
+/* Stop ioloop after finishing all the pending IOs and timeouts. */
+void io_loop_stop_delayed(struct ioloop *ioloop);
+
+bool io_loop_is_running(struct ioloop *ioloop);
+
+/* call these if you wish to run the iteration only once */
+void io_loop_set_running(struct ioloop *ioloop);
+void io_loop_handler_run(struct ioloop *ioloop);
+
+struct ioloop *io_loop_create(void);
+/* Specify the maximum number of fds we're expecting to use. */
+void io_loop_set_max_fd_count(struct ioloop *ioloop, unsigned int max_fds);
+/* Destroy I/O loop and set ioloop pointer to NULL. */
+void io_loop_destroy(struct ioloop **ioloop);
+
+/* If time moves backwards or jumps forwards call the callback. */
+void io_loop_set_time_moved_callback(struct ioloop *ioloop,
+ io_loop_time_moved_callback_t *callback);
+
+/* Change the current_ioloop. */
+void io_loop_set_current(struct ioloop *ioloop);
+/* Return the root ioloop. */
+struct ioloop *io_loop_get_root(void);
+/* Call the callback whenever ioloop is changed. */
+void io_loop_add_switch_callback(io_switch_callback_t *callback);
+void io_loop_remove_switch_callback(io_switch_callback_t *callback);
+/* Call the callback whenever ioloop is destroyed. */
+void io_loop_add_destroy_callback(io_destroy_callback_t *callback);
+void io_loop_remove_destroy_callback(io_destroy_callback_t *callback);
+
+/* Create a new ioloop context. While the context is activated, it's
+ automatically attached to all the following I/Os and timeouts that are
+ added until the context is deactivated (e.g. returning to back to a running
+ ioloop). Whenever such added I/O or timeout callback is called, this context
+ is automatically activated.
+
+ After the context is created, callbacks should be added to it and the
+ context should be activated with either io_loop_context_activate() or
+ io_loop_context_switch(). */
+struct ioloop_context *io_loop_context_new(struct ioloop *ioloop);
+void io_loop_context_ref(struct ioloop_context *ctx);
+void io_loop_context_unref(struct ioloop_context **ctx);
+/* Call the activate callback when this context is activated (I/O callback is
+ about to be called), and the deactivate callback when the context is
+ deactivated (I/O callback has returned). You can add multiple callbacks.
+
+ The ioloop context is a global state, so only a single context can be active
+ at a time. The callbacks are guaranteed to be called only at their proper
+ states, i.e. activate() callback is called only when switching from
+ no context to the active context, and deactive() is called only when
+ switching from previously activated context into no context. No context is
+ active at a time when the ioloop is destroyed. */
+void io_loop_context_add_callbacks(struct ioloop_context *ctx,
+ io_callback_t *activate,
+ io_callback_t *deactivate, void *context);
+#define io_loop_context_add_callbacks(ctx, activate, deactivate, context) \
+ io_loop_context_add_callbacks(ctx, 1 ? (io_callback_t *)activate : \
+ CALLBACK_TYPECHECK(activate, void (*)(typeof(context))) - \
+ CALLBACK_TYPECHECK(deactivate, void (*)(typeof(context))), \
+ (io_callback_t *)deactivate, context)
+/* Remove callbacks with the given callbacks and context. */
+void io_loop_context_remove_callbacks(struct ioloop_context *ctx,
+ io_callback_t *activate,
+ io_callback_t *deactivate, void *context);
+#define io_loop_context_remove_callbacks(ctx, activate, deactivate, context) \
+ io_loop_context_remove_callbacks(ctx, 1 ? (io_callback_t *)activate : \
+ CALLBACK_TYPECHECK(activate, void (*)(typeof(context))) - \
+ CALLBACK_TYPECHECK(deactivate, void (*)(typeof(context))), \
+ (io_callback_t *)deactivate, context)
+/* Returns the current context set to ioloop. */
+struct ioloop_context *io_loop_get_current_context(struct ioloop *ioloop);
+
+/* Explicitly activate an ioloop context. There must not be any context active
+ at the moment, so this most likely shouldn't be called while ioloop is
+ running. An activated context must be explicitly deactivated with
+ io_loop_context_deactivate() before the ioloop is destroyed, or before
+ any ioloop is run. */
+void io_loop_context_activate(struct ioloop_context *ctx);
+/* Explicitly deactivate an ioloop context. The given context must be currently
+ active or it assert-crashes. This should be called only after a context
+ was explicitly activated with io_loop_context_activate(). */
+void io_loop_context_deactivate(struct ioloop_context *ctx);
+/* If there's an active ioloop context, deactivate it. Then activate the given
+ context. Usually used after creating a new context. */
+void io_loop_context_switch(struct ioloop_context *ctx);
+
+/* Returns fd, which contains all of the ioloop's current notifications.
+ When it becomes readable, there is a new notification. Calling this function
+ stops the existing notifications in the ioloop from working anymore.
+ This function's main idea is that the fd can be passed to another process,
+ which can use it to find out if an interesting notification happens.
+ Returns fd on success, -1 on error. */
+int io_loop_extract_notify_fd(struct ioloop *ioloop);
+
+/* IO wait timers can be used to track how much time the io_wait_timer has
+ spent on waiting in its ioloops. This is similar to
+ io_loop_get_wait_usecs(), but it's easier to use when the wait time needs
+ to be tracked across multiple ioloops. */
+struct io_wait_timer *
+io_wait_timer_add(const char *source_filename, unsigned int source_linenum);
+#define io_wait_timer_add() \
+ io_wait_timer_add(__FILE__, __LINE__)
+struct io_wait_timer *
+io_wait_timer_add_to(struct ioloop *ioloop, const char *source_filename,
+ unsigned int source_linenum);
+#define io_wait_timer_add_to(ioloop) \
+ io_wait_timer_add_to(ioloop, __FILE__, __LINE__)
+
+struct io_wait_timer *io_wait_timer_move(struct io_wait_timer **timer);
+struct io_wait_timer *io_wait_timer_move_to(struct io_wait_timer **timer,
+ struct ioloop *ioloop);
+void io_wait_timer_remove(struct io_wait_timer **timer);
+uint64_t io_wait_timer_get_usecs(struct io_wait_timer *timer);
+
+/* Move the given I/O into the provided/current I/O loop if it's not already
+ there. New I/O is returned, while the old one is freed. */
+struct io *io_loop_move_io_to(struct ioloop *ioloop, struct io **_io);
+struct io *io_loop_move_io(struct io **io);
+/* Like io_loop_move_io(), but for timeouts. */
+struct timeout *io_loop_move_timeout_to(struct ioloop *ioloop,
+ struct timeout **timeout);
+struct timeout *io_loop_move_timeout(struct timeout **timeout);
+/* Returns TRUE if any IOs have been added to the ioloop. */
+bool io_loop_have_ios(struct ioloop *ioloop);
+/* Returns TRUE if there is a pending timeout that is going to be run
+ immediately. */
+bool io_loop_have_immediate_timeouts(struct ioloop *ioloop);
+/* Returns TRUE if there are no IOs or timeouts in the ioloop. */
+bool io_loop_is_empty(struct ioloop *ioloop);
+/* Returns number of microseconds spent on the ioloop waiting itself. */
+uint64_t io_loop_get_wait_usecs(struct ioloop *ioloop);
+/* Return all io conditions added for the given fd. This needs to scan through
+ all the file ios in the ioloop. */
+enum io_condition io_loop_find_fd_conditions(struct ioloop *ioloop, int fd);
+
+#endif
diff --git a/src/lib/iostream-private.h b/src/lib/iostream-private.h
new file mode 100644
index 0000000..9aa72e9
--- /dev/null
+++ b/src/lib/iostream-private.h
@@ -0,0 +1,51 @@
+#ifndef IOSTREAM_PRIVATE_H
+#define IOSTREAM_PRIVATE_H
+
+#include "iostream.h"
+
+/* This file is private to input stream and output stream implementations */
+
+struct iostream_destroy_callback {
+ void (*callback)(void *context);
+ void *context;
+};
+
+struct iostream_private {
+ int refcount;
+ char *name;
+ char *error;
+ struct ioloop *ioloop;
+
+ void (*close)(struct iostream_private *streami, bool close_parent);
+ void (*destroy)(struct iostream_private *stream);
+ void (*set_max_buffer_size)(struct iostream_private *stream,
+ size_t max_size);
+
+ ARRAY(struct iostream_destroy_callback) destroy_callbacks;
+};
+
+void io_stream_init(struct iostream_private *stream);
+void io_stream_ref(struct iostream_private *stream);
+bool io_stream_unref(struct iostream_private *stream);
+void io_stream_free(struct iostream_private *stream);
+void io_stream_close(struct iostream_private *stream, bool close_parent);
+void io_stream_set_max_buffer_size(struct iostream_private *stream,
+ size_t max_size);
+void io_stream_add_destroy_callback(struct iostream_private *stream,
+ void (*callback)(void *), void *context);
+void io_stream_remove_destroy_callback(struct iostream_private *stream,
+ void (*callback)(void *));
+/* Set a specific error for the stream. This shouldn't be used for regular
+ syscall errors where stream's errno is enough, since it's used by default.
+ The stream errno must always be set even if the error string is also set.
+ Setting this error replaces the previously set error. */
+void io_stream_set_error(struct iostream_private *stream,
+ const char *fmt, ...) ATTR_FORMAT(2, 3);
+void io_stream_set_verror(struct iostream_private *stream,
+ const char *fmt, va_list args) ATTR_FORMAT(2, 0);
+
+void io_stream_switch_ioloop_to(struct iostream_private *stream,
+ struct ioloop *ioloop);
+struct ioloop *io_stream_get_ioloop(struct iostream_private *stream);
+
+#endif
diff --git a/src/lib/iostream-proxy.c b/src/lib/iostream-proxy.c
new file mode 100644
index 0000000..2663420
--- /dev/null
+++ b/src/lib/iostream-proxy.c
@@ -0,0 +1,173 @@
+/* Copyright (c) 2002-2018 Dovecot authors, see the included COPYING file
+ */
+#include "lib.h"
+#include "buffer.h"
+#include "str.h"
+#include "iostream-pump.h"
+#include "iostream-proxy.h"
+#include <unistd.h>
+
+#undef iostream_proxy_set_completion_callback
+
+struct iostream_proxy {
+ struct iostream_pump *ltr;
+ struct iostream_pump *rtl;
+
+ unsigned int ref;
+
+ iostream_proxy_callback_t *callback;
+ void *context;
+};
+
+static void
+iostream_proxy_completion(struct iostream_proxy *proxy,
+ enum iostream_proxy_side side,
+ enum iostream_pump_status pump_status)
+{
+ enum iostream_proxy_status status;
+
+ switch (pump_status) {
+ case IOSTREAM_PUMP_STATUS_INPUT_EOF:
+ status = IOSTREAM_PROXY_STATUS_INPUT_EOF;
+ break;
+ case IOSTREAM_PUMP_STATUS_INPUT_ERROR:
+ status = IOSTREAM_PROXY_STATUS_INPUT_ERROR;
+ break;
+ case IOSTREAM_PUMP_STATUS_OUTPUT_ERROR:
+ status = IOSTREAM_PROXY_STATUS_OTHER_SIDE_OUTPUT_ERROR;
+ break;
+ default:
+ i_unreached();
+ }
+ proxy->callback(side, status, proxy->context);
+}
+
+static
+void iostream_proxy_rtl_completion(enum iostream_pump_status status,
+ struct iostream_proxy *proxy)
+{
+ iostream_proxy_completion(proxy, IOSTREAM_PROXY_SIDE_RIGHT, status);
+}
+
+static
+void iostream_proxy_ltr_completion(enum iostream_pump_status status,
+ struct iostream_proxy *proxy)
+{
+ iostream_proxy_completion(proxy, IOSTREAM_PROXY_SIDE_LEFT, status);
+}
+
+struct iostream_proxy *
+iostream_proxy_create(struct istream *left_input, struct ostream *left_output,
+ struct istream *right_input, struct ostream *right_output)
+{
+ i_assert(left_input != NULL &&
+ right_input != NULL &&
+ left_output != NULL &&
+ right_output != NULL);
+
+ /* create proxy */
+ struct iostream_proxy *proxy = i_new(struct iostream_proxy, 1);
+
+ proxy->ltr = iostream_pump_create(left_input, right_output);
+ proxy->rtl = iostream_pump_create(right_input, left_output);
+
+ iostream_pump_set_completion_callback(proxy->ltr, iostream_proxy_ltr_completion, proxy);
+ iostream_pump_set_completion_callback(proxy->rtl, iostream_proxy_rtl_completion, proxy);
+
+ proxy->ref = 1;
+
+ return proxy;
+}
+
+void iostream_proxy_start(struct iostream_proxy *proxy)
+{
+ i_assert(proxy != NULL);
+ i_assert(proxy->callback != NULL);
+
+ iostream_pump_start(proxy->rtl);
+ iostream_pump_start(proxy->ltr);
+}
+
+void iostream_proxy_set_completion_callback(struct iostream_proxy *proxy,
+ iostream_proxy_callback_t *callback,
+ void *context)
+{
+ i_assert(proxy != NULL);
+
+ proxy->callback = callback;
+ proxy->context = context;
+}
+
+struct istream *iostream_proxy_get_istream(struct iostream_proxy *proxy, enum iostream_proxy_side side)
+{
+ i_assert(proxy != NULL);
+
+ switch(side) {
+ case IOSTREAM_PROXY_SIDE_LEFT: return iostream_pump_get_input(proxy->ltr);
+ case IOSTREAM_PROXY_SIDE_RIGHT: return iostream_pump_get_input(proxy->rtl);
+ default: i_unreached();
+ }
+}
+
+struct ostream *iostream_proxy_get_ostream(struct iostream_proxy *proxy, enum iostream_proxy_side side)
+{
+ i_assert(proxy != NULL);
+
+ switch(side) {
+ case IOSTREAM_PROXY_SIDE_LEFT: return iostream_pump_get_output(proxy->ltr);
+ case IOSTREAM_PROXY_SIDE_RIGHT: return iostream_pump_get_output(proxy->rtl);
+ default: i_unreached();
+ }
+}
+
+void iostream_proxy_ref(struct iostream_proxy *proxy)
+{
+ i_assert(proxy != NULL && proxy->ref > 0);
+ proxy->ref++;
+}
+
+void iostream_proxy_unref(struct iostream_proxy **proxy_r)
+{
+ struct iostream_proxy *proxy;
+
+ if (proxy_r == NULL || *proxy_r == NULL)
+ return;
+
+ proxy = *proxy_r;
+ *proxy_r = NULL;
+
+ i_assert(proxy->ref > 0);
+ if (--proxy->ref == 0) {
+ /* pumps will call stop internally
+ if refcount drops to 0 */
+ iostream_pump_unref(&proxy->ltr);
+ iostream_pump_unref(&proxy->rtl);
+ i_free(proxy);
+ }
+}
+
+void iostream_proxy_stop(struct iostream_proxy *proxy)
+{
+ i_assert(proxy != NULL);
+ iostream_pump_stop(proxy->ltr);
+ iostream_pump_stop(proxy->rtl);
+}
+
+bool iostream_proxy_is_waiting_output(struct iostream_proxy *proxy,
+ enum iostream_proxy_side side)
+{
+ switch (side) {
+ case IOSTREAM_PROXY_SIDE_LEFT:
+ return iostream_pump_is_waiting_output(proxy->ltr);
+ case IOSTREAM_PROXY_SIDE_RIGHT:
+ return iostream_pump_is_waiting_output(proxy->rtl);
+ }
+ i_unreached();
+}
+
+void iostream_proxy_switch_ioloop(struct iostream_proxy *proxy)
+{
+ i_assert(proxy != NULL);
+ iostream_pump_switch_ioloop(proxy->ltr);
+ iostream_pump_switch_ioloop(proxy->rtl);
+}
diff --git a/src/lib/iostream-proxy.h b/src/lib/iostream-proxy.h
new file mode 100644
index 0000000..5e8850f
--- /dev/null
+++ b/src/lib/iostream-proxy.h
@@ -0,0 +1,87 @@
+/* Copyright (c) 2002-2018 Dovecot authors, see the included COPYING file
+ */
+#ifndef IOSTREAM_PROXY_H
+#define IOSTREAM_PROXY_H 1
+
+/**
+
+iostream-proxy
+=============
+
+This construct will proxy data between two pairs of
+istream and ostream. Data is proxied from left to right
+and right to left using iostream-pump.
+
+The proxy requires you to provide completion callback. The
+completion callback is called with success parameter to
+indicate whether it ended with error.
+
+The istreams and ostreams are reffed on creation and unreffed
+on unref.
+
+**/
+
+struct istream;
+struct ostream;
+struct iostream_proxy;
+
+enum iostream_proxy_side {
+ /* Input is coming from left side's istream and is proxied to
+ right side's ostream. */
+ IOSTREAM_PROXY_SIDE_LEFT,
+ /* Input is coming from right side's istream and is proxied to
+ left side's ostream. */
+ IOSTREAM_PROXY_SIDE_RIGHT
+};
+
+enum iostream_proxy_status {
+ /* proxy succeeded - EOF received from istream and all output was
+ written successfully to ostream. */
+ IOSTREAM_PROXY_STATUS_INPUT_EOF,
+ /* proxy failed - istream returned an error */
+ IOSTREAM_PROXY_STATUS_INPUT_ERROR,
+ /* proxy failed - other side's ostream returned an error */
+ IOSTREAM_PROXY_STATUS_OTHER_SIDE_OUTPUT_ERROR,
+};
+
+/* The callback maybe be called once or twice. Usually the first call should
+ destroy the proxy, but it's possible for it to just wait for the other
+ direction of the proxy to finish as well and call the callback.
+
+ Note that the sides mean which side is the reader side. If the failure is in
+ ostream, it's the other side's ostream that failed. So for example if
+ side=left, the write failed to the right side's ostream.
+
+ The callback is called when the proxy succeeds or fails due to
+ iostreams. (It's not called if proxy is destroyed.) */
+typedef void iostream_proxy_callback_t(enum iostream_proxy_side side,
+ enum iostream_proxy_status status,
+ void *context);
+
+struct iostream_proxy *
+iostream_proxy_create(struct istream *left_input, struct ostream *left_output,
+ struct istream *right_input, struct ostream *right_output);
+
+struct istream *iostream_proxy_get_istream(struct iostream_proxy *proxy, enum iostream_proxy_side);
+struct ostream *iostream_proxy_get_ostream(struct iostream_proxy *proxy, enum iostream_proxy_side);
+
+void iostream_proxy_start(struct iostream_proxy *proxy);
+void iostream_proxy_stop(struct iostream_proxy *proxy);
+
+/* See iostream_pump_is_waiting_output() */
+bool iostream_proxy_is_waiting_output(struct iostream_proxy *proxy,
+ enum iostream_proxy_side side);
+
+void iostream_proxy_set_completion_callback(struct iostream_proxy *proxy,
+ iostream_proxy_callback_t *callback, void *context);
+#define iostream_proxy_set_completion_callback(proxy, callback, context) \
+ iostream_proxy_set_completion_callback(proxy, (iostream_proxy_callback_t *)callback, \
+ TRUE ? context : \
+ CALLBACK_TYPECHECK(callback, void (*)(enum iostream_proxy_side side, enum iostream_proxy_status, typeof(context))))
+
+void iostream_proxy_ref(struct iostream_proxy *proxy);
+void iostream_proxy_unref(struct iostream_proxy **proxy_r);
+
+void iostream_proxy_switch_ioloop(struct iostream_proxy *proxy);
+
+#endif
diff --git a/src/lib/iostream-pump.c b/src/lib/iostream-pump.c
new file mode 100644
index 0000000..cebae3d
--- /dev/null
+++ b/src/lib/iostream-pump.c
@@ -0,0 +1,251 @@
+/* Copyright (c) 2002-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "buffer.h"
+#include "str.h"
+#include "iostream-pump.h"
+#include "istream.h"
+#include "ostream.h"
+#include <unistd.h>
+
+#undef iostream_pump_set_completion_callback
+
+struct iostream_pump {
+ int refcount;
+
+ struct istream *input;
+ struct ostream *output;
+
+ struct io *io;
+
+ iostream_pump_callback_t *callback;
+ void *context;
+
+ bool waiting_output;
+ bool completed;
+};
+
+static void iostream_pump_copy(struct iostream_pump *pump)
+{
+ enum ostream_send_istream_result res;
+ size_t old_size;
+
+ o_stream_cork(pump->output);
+ old_size = o_stream_get_max_buffer_size(pump->output);
+ o_stream_set_max_buffer_size(pump->output,
+ I_MIN(IO_BLOCK_SIZE,
+ o_stream_get_max_buffer_size(pump->output)));
+ res = o_stream_send_istream(pump->output, pump->input);
+ o_stream_set_max_buffer_size(pump->output, old_size);
+ o_stream_uncork(pump->output);
+
+ switch(res) {
+ case OSTREAM_SEND_ISTREAM_RESULT_ERROR_INPUT:
+ io_remove(&pump->io);
+ pump->callback(IOSTREAM_PUMP_STATUS_INPUT_ERROR,
+ pump->context);
+ return;
+ case OSTREAM_SEND_ISTREAM_RESULT_ERROR_OUTPUT:
+ io_remove(&pump->io);
+ pump->callback(IOSTREAM_PUMP_STATUS_OUTPUT_ERROR,
+ pump->context);
+ return;
+ case OSTREAM_SEND_ISTREAM_RESULT_WAIT_OUTPUT:
+ i_assert(!pump->output->blocking);
+ pump->waiting_output = TRUE;
+ io_remove(&pump->io);
+ return;
+ case OSTREAM_SEND_ISTREAM_RESULT_FINISHED:
+ pump->waiting_output = FALSE;
+ io_remove(&pump->io);
+ /* flush it */
+ switch (o_stream_flush(pump->output)) {
+ case -1:
+ pump->callback(IOSTREAM_PUMP_STATUS_OUTPUT_ERROR,
+ pump->context);
+ break;
+ case 0:
+ pump->waiting_output = TRUE;
+ pump->completed = TRUE;
+ break;
+ default:
+ pump->callback(IOSTREAM_PUMP_STATUS_INPUT_EOF,
+ pump->context);
+ break;
+ }
+ return;
+ case OSTREAM_SEND_ISTREAM_RESULT_WAIT_INPUT:
+ i_assert(!pump->input->blocking);
+ pump->waiting_output = FALSE;
+ return;
+ }
+ i_unreached();
+}
+
+static int iostream_pump_flush(struct iostream_pump *pump)
+{
+ int ret;
+
+ if ((ret = o_stream_flush(pump->output)) <= 0) {
+ if (ret < 0) {
+ pump->callback(IOSTREAM_PUMP_STATUS_OUTPUT_ERROR,
+ pump->context);
+ }
+ return ret;
+ }
+ pump->waiting_output = FALSE;
+ if (pump->completed) {
+ pump->callback(IOSTREAM_PUMP_STATUS_INPUT_EOF, pump->context);
+ return 1;
+ }
+
+ if (pump->input->blocking)
+ iostream_pump_copy(pump);
+ else if (pump->io == NULL) {
+ pump->io = io_add_istream(pump->input,
+ iostream_pump_copy, pump);
+ io_set_pending(pump->io);
+ }
+ return ret;
+}
+
+struct iostream_pump *
+iostream_pump_create(struct istream *input, struct ostream *output)
+{
+ struct iostream_pump *pump;
+
+ i_assert(input != NULL &&
+ output != NULL);
+ i_assert(!input->blocking || !output->blocking);
+
+ /* ref streams */
+ i_stream_ref(input);
+ o_stream_ref(output);
+
+ /* create pump */
+ pump = i_new(struct iostream_pump, 1);
+ pump->refcount = 1;
+ pump->input = input;
+ pump->output = output;
+
+ return pump;
+}
+
+void iostream_pump_start(struct iostream_pump *pump)
+{
+ i_assert(pump != NULL);
+ i_assert(pump->callback != NULL);
+
+ /* add flush handler */
+ if (!pump->output->blocking) {
+ o_stream_set_flush_callback(pump->output,
+ iostream_pump_flush, pump);
+ }
+
+ /* make IO objects */
+ if (pump->input->blocking) {
+ i_assert(!pump->output->blocking);
+ o_stream_set_flush_pending(pump->output, TRUE);
+ } else {
+ pump->io = io_add_istream(pump->input,
+ iostream_pump_copy, pump);
+ io_set_pending(pump->io);
+ }
+}
+
+struct istream *iostream_pump_get_input(struct iostream_pump *pump)
+{
+ i_assert(pump != NULL);
+ return pump->input;
+}
+
+struct ostream *iostream_pump_get_output(struct iostream_pump *pump)
+{
+ i_assert(pump != NULL);
+ return pump->output;
+}
+
+void iostream_pump_set_completion_callback(struct iostream_pump *pump,
+ iostream_pump_callback_t *callback,
+ void *context)
+{
+ i_assert(pump != NULL);
+ pump->callback = callback;
+ pump->context = context;
+}
+
+void iostream_pump_ref(struct iostream_pump *pump)
+{
+ i_assert(pump != NULL);
+ i_assert(pump->refcount > 0);
+ pump->refcount++;
+}
+
+void iostream_pump_unref(struct iostream_pump **_pump)
+{
+ i_assert(_pump != NULL);
+ struct iostream_pump *pump = *_pump;
+
+ if (pump == NULL)
+ return;
+
+ i_assert(pump->refcount > 0);
+
+ *_pump = NULL;
+
+ if (--pump->refcount > 0)
+ return;
+
+ iostream_pump_stop(pump);
+
+ o_stream_unref(&pump->output);
+ i_stream_unref(&pump->input);
+ i_free(pump);
+}
+
+void iostream_pump_destroy(struct iostream_pump **_pump)
+{
+ i_assert(_pump != NULL);
+ struct iostream_pump *pump = *_pump;
+
+ if (pump == NULL)
+ return;
+
+ *_pump = NULL;
+
+ iostream_pump_stop(pump);
+ o_stream_unref(&pump->output);
+ i_stream_unref(&pump->input);
+
+ iostream_pump_unref(&pump);
+}
+
+void iostream_pump_stop(struct iostream_pump *pump)
+{
+ i_assert(pump != NULL);
+
+ if (pump->output != NULL)
+ o_stream_unset_flush_callback(pump->output);
+
+ io_remove(&pump->io);
+}
+
+bool iostream_pump_is_waiting_output(struct iostream_pump *pump)
+{
+ return pump->waiting_output;
+}
+
+void iostream_pump_switch_ioloop_to(struct iostream_pump *pump,
+ struct ioloop *ioloop)
+{
+ i_assert(pump != NULL);
+ if (pump->io != NULL)
+ pump->io = io_loop_move_io_to(ioloop, &pump->io);
+ o_stream_switch_ioloop_to(pump->output, ioloop);
+ i_stream_switch_ioloop_to(pump->input, ioloop);
+}
+
+void iostream_pump_switch_ioloop(struct iostream_pump *pump)
+{
+ iostream_pump_switch_ioloop_to(pump, current_ioloop);
+}
diff --git a/src/lib/iostream-pump.h b/src/lib/iostream-pump.h
new file mode 100644
index 0000000..d7317ae
--- /dev/null
+++ b/src/lib/iostream-pump.h
@@ -0,0 +1,69 @@
+#ifndef IOSTREAM_PUMP_H
+#define IOSTREAM_PUMP_H
+
+/* iostream-pump
+ =============
+
+ This construct pumps data from istream to ostream asynchronously.
+
+ The pump requires you to provide completion callback. The completion
+ callback is called with success parameter to indicate whether it ended
+ with error.
+
+ The istream and ostream are reffed on creation and unreffed
+ on unref.
+ */
+
+struct istream;
+struct ostream;
+struct ioloop;
+struct iostream_pump;
+
+enum iostream_pump_status {
+ /* pump succeeded - EOF received from istream and all output was
+ written successfully to ostream. */
+ IOSTREAM_PUMP_STATUS_INPUT_EOF,
+ /* pump failed - istream returned an error */
+ IOSTREAM_PUMP_STATUS_INPUT_ERROR,
+ /* pump failed - ostream returned an error */
+ IOSTREAM_PUMP_STATUS_OUTPUT_ERROR,
+};
+
+/* The callback is called once when the pump succeeds or fails due to
+ iostreams. (It's not called if pump is destroyed.) */
+typedef void iostream_pump_callback_t(enum iostream_pump_status status,
+ void *context);
+
+struct iostream_pump *
+iostream_pump_create(struct istream *input, struct ostream *output);
+
+struct istream *iostream_pump_get_input(struct iostream_pump *pump);
+struct ostream *iostream_pump_get_output(struct iostream_pump *pump);
+
+void iostream_pump_start(struct iostream_pump *pump);
+void iostream_pump_stop(struct iostream_pump *pump);
+
+void iostream_pump_ref(struct iostream_pump *pump);
+void iostream_pump_unref(struct iostream_pump **_pump);
+void iostream_pump_destroy(struct iostream_pump **_pump);
+
+void iostream_pump_set_completion_callback(struct iostream_pump *pump,
+ iostream_pump_callback_t *callback,
+ void *context);
+#define iostream_pump_set_completion_callback(pump, callback, context) \
+ iostream_pump_set_completion_callback(pump, \
+ (iostream_pump_callback_t *)callback, TRUE ? context : \
+ CALLBACK_TYPECHECK(callback, \
+ void (*)(enum iostream_pump_status, typeof(context))))
+
+/* Returns TRUE if the pump is currently only writing to the ostream. The input
+ listener has been removed either because the ostream buffer is full or
+ because the istream already returned EOF. This function can also be called
+ from the completion callback in error conditions. */
+bool iostream_pump_is_waiting_output(struct iostream_pump *pump);
+
+void iostream_pump_switch_ioloop_to(struct iostream_pump *pump,
+ struct ioloop *ioloop);
+void iostream_pump_switch_ioloop(struct iostream_pump *pump);
+
+#endif
diff --git a/src/lib/iostream-rawlog-private.h b/src/lib/iostream-rawlog-private.h
new file mode 100644
index 0000000..9a409e0
--- /dev/null
+++ b/src/lib/iostream-rawlog-private.h
@@ -0,0 +1,25 @@
+#ifndef IOSTREAM_RAWLOG_PRIVATE_H
+#define IOSTREAM_RAWLOG_PRIVATE_H
+
+#include "iostream-rawlog.h"
+
+#define IOSTREAM_RAWLOG_MAX_PREFIX_LEN 3
+
+struct rawlog_iostream {
+ struct iostream_private *iostream;
+ enum iostream_rawlog_flags flags;
+
+ struct ostream *rawlog_output;
+ buffer_t *buffer;
+
+ bool input;
+ bool line_continued;
+};
+
+void iostream_rawlog_init(struct rawlog_iostream *rstream,
+ enum iostream_rawlog_flags flags, bool input);
+void iostream_rawlog_write(struct rawlog_iostream *rstream,
+ const unsigned char *data, size_t size);
+void iostream_rawlog_close(struct rawlog_iostream *rstream);
+
+#endif
diff --git a/src/lib/iostream-rawlog.c b/src/lib/iostream-rawlog.c
new file mode 100644
index 0000000..5805c9b
--- /dev/null
+++ b/src/lib/iostream-rawlog.c
@@ -0,0 +1,298 @@
+/* Copyright (c) 2011-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "hostpid.h"
+#include "ioloop.h"
+#include "buffer.h"
+#include "str.h"
+#include "net.h"
+#include "write-full.h"
+#include "time-util.h"
+#include "istream.h"
+#include "ostream.h"
+#include "iostream-private.h"
+#include "iostream-rawlog-private.h"
+#include "istream-rawlog.h"
+#include "ostream-rawlog.h"
+#include "iostream-rawlog.h"
+
+#include <unistd.h>
+#include <fcntl.h>
+
+#define RAWLOG_MAX_LINE_LEN 8192
+
+static void
+rawlog_write_timestamp(struct rawlog_iostream *rstream, bool line_ends)
+{
+ unsigned char data[MAX_INT_STRLEN + 1 + 6 + 1 + 3];
+ buffer_t buf;
+
+ if ((rstream->flags & IOSTREAM_RAWLOG_FLAG_TIMESTAMP) == 0)
+ return;
+
+ buffer_create_from_data(&buf, data, sizeof(data));
+ str_printfa(&buf, "%"PRIdTIME_T".%06u ",
+ ioloop_timeval.tv_sec,
+ (unsigned int)ioloop_timeval.tv_usec);
+ if ((rstream->flags & IOSTREAM_RAWLOG_FLAG_BUFFERED) != 0) {
+ str_append_c(&buf, rstream->input ? 'I' : 'O');
+ str_append_c(&buf, line_ends ? ':' : '>');
+ str_append_c(&buf, ' ');
+ }
+ o_stream_nsend(rstream->rawlog_output, buf.data, buf.used);
+}
+
+void iostream_rawlog_init(struct rawlog_iostream *rstream,
+ enum iostream_rawlog_flags flags, bool input)
+{
+ rstream->flags = flags;
+ rstream->input = input;
+ if ((rstream->flags & IOSTREAM_RAWLOG_FLAG_BUFFERED) != 0)
+ rstream->buffer = buffer_create_dynamic(default_pool, 1024);
+}
+
+static void
+iostream_rawlog_write_buffered(struct rawlog_iostream *rstream,
+ const unsigned char *data, size_t size)
+{
+ const unsigned char *p;
+ size_t pos;
+ bool line_ends;
+
+ while (size > 0) {
+ p = memchr(data, '\n', size);
+ if (p != NULL) {
+ line_ends = TRUE;
+ pos = p-data + 1;
+ } else if (rstream->buffer->used + size < RAWLOG_MAX_LINE_LEN) {
+ buffer_append(rstream->buffer, data, size);
+ break;
+ } else {
+ line_ends = FALSE;
+ pos = size;
+ }
+
+ rawlog_write_timestamp(rstream, line_ends);
+ if (rstream->buffer->used > 0) {
+ o_stream_nsend(rstream->rawlog_output,
+ rstream->buffer->data,
+ rstream->buffer->used);
+ buffer_set_used_size(rstream->buffer, 0);
+ }
+ o_stream_nsend(rstream->rawlog_output, data, pos);
+
+ data += pos;
+ size -= pos;
+ }
+}
+
+static void
+iostream_rawlog_write_unbuffered(struct rawlog_iostream *rstream,
+ const unsigned char *data, size_t size)
+{
+ size_t i, start;
+
+ if (!rstream->line_continued)
+ rawlog_write_timestamp(rstream, TRUE);
+
+ for (start = 0, i = 1; i < size; i++) {
+ if (data[i-1] == '\n') {
+ o_stream_nsend(rstream->rawlog_output,
+ data + start, i - start);
+ rawlog_write_timestamp(rstream, TRUE);
+ start = i;
+ }
+ }
+ if (start != size) {
+ o_stream_nsend(rstream->rawlog_output,
+ data + start, size - start);
+ }
+ rstream->line_continued = data[size-1] != '\n';
+}
+
+void iostream_rawlog_write(struct rawlog_iostream *rstream,
+ const unsigned char *data, size_t size)
+{
+ if (size == 0 || rstream->rawlog_output == NULL)
+ return;
+
+ io_loop_time_refresh();
+
+ o_stream_cork(rstream->rawlog_output);
+ if ((rstream->flags & IOSTREAM_RAWLOG_FLAG_BUFFERED) != 0)
+ iostream_rawlog_write_buffered(rstream, data, size);
+ else
+ iostream_rawlog_write_unbuffered(rstream, data, size);
+ o_stream_uncork(rstream->rawlog_output);
+
+ if (o_stream_flush(rstream->rawlog_output) < 0) {
+ i_error("write(%s) failed: %s",
+ o_stream_get_name(rstream->rawlog_output),
+ o_stream_get_error(rstream->rawlog_output));
+ iostream_rawlog_close(rstream);
+ }
+}
+
+void iostream_rawlog_close(struct rawlog_iostream *rstream)
+{
+ o_stream_unref(&rstream->rawlog_output);
+ buffer_free(&rstream->buffer);
+}
+
+static void
+iostream_rawlog_create_fd(int fd, const char *path, struct istream **input,
+ struct ostream **output)
+{
+ struct istream *old_input;
+ struct ostream *old_output;
+
+ old_input = *input;
+ old_output = *output;
+ *input = i_stream_create_rawlog(old_input, path, fd,
+ IOSTREAM_RAWLOG_FLAG_BUFFERED |
+ IOSTREAM_RAWLOG_FLAG_TIMESTAMP);
+ *output = o_stream_create_rawlog(old_output, path, fd,
+ IOSTREAM_RAWLOG_FLAG_AUTOCLOSE |
+ IOSTREAM_RAWLOG_FLAG_BUFFERED |
+ IOSTREAM_RAWLOG_FLAG_TIMESTAMP);
+ i_stream_unref(&old_input);
+ o_stream_unref(&old_output);
+}
+
+static int
+iostream_rawlog_try_create_tcp(const char *path,
+ struct istream **input, struct ostream **output)
+{
+ const char *host;
+ struct ip_addr *ips;
+ unsigned int ips_count;
+ in_port_t port;
+ int ret, fd;
+
+ /* tcp:host:port */
+ if (!str_begins(path, "tcp:"))
+ return 0;
+ path += 4;
+
+ if (strchr(path, '/') != NULL)
+ return 0;
+ if (net_str2hostport(path, 0, &host, &port) < 0 || port == 0)
+ return 0;
+
+ ret = net_gethostbyname(host, &ips, &ips_count);
+ if (ret != 0) {
+ i_error("net_gethostbyname(%s) failed: %s", host,
+ net_gethosterror(ret));
+ return -1;
+ }
+ fd = net_connect_ip_blocking(&ips[0], port, NULL);
+ if (fd == -1) {
+ i_error("connect(%s:%u) failed: %m", net_ip2addr(&ips[0]), port);
+ return -1;
+ }
+ iostream_rawlog_create_fd(fd, path, input, output);
+ return 1;
+}
+
+int iostream_rawlog_create(const char *dir, struct istream **input,
+ struct ostream **output)
+{
+ static unsigned int counter = 0;
+ const char *timestamp, *prefix;
+ struct stat st;
+ int ret;
+
+ if ((ret = iostream_rawlog_try_create_tcp(dir, input, output)) != 0)
+ return ret < 0 ? -1 : 0;
+ if (stat(dir, &st) < 0) {
+ if (errno != ENOENT && errno != EACCES)
+ i_error("rawlog: stat(%s) failed: %m", dir);
+ return -1;
+ }
+
+ timestamp = t_strflocaltime("%Y%m%d-%H%M%S", ioloop_time);
+
+ counter++;
+ prefix = t_strdup_printf("%s/%s.%s.%u", dir, timestamp, my_pid, counter);
+ return iostream_rawlog_create_prefix(prefix, input, output);
+}
+
+int iostream_rawlog_create_prefix(const char *prefix, struct istream **input,
+ struct ostream **output)
+{
+ const char *in_path, *out_path;
+ struct istream *old_input;
+ struct ostream *old_output;
+ int in_fd, out_fd;
+
+ in_path = t_strdup_printf("%s.in", prefix);
+ in_fd = open(in_path, O_CREAT | O_APPEND | O_WRONLY, 0600);
+ if (in_fd == -1) {
+ i_error("creat(%s) failed: %m", in_path);
+ return -1;
+ }
+
+ out_path = t_strdup_printf("%s.out", prefix);
+ out_fd = open(out_path, O_CREAT | O_APPEND | O_WRONLY, 0600);
+ if (out_fd == -1) {
+ i_error("creat(%s) failed: %m", out_path);
+ i_close_fd(&in_fd);
+ i_unlink(in_path);
+ return -1;
+ }
+
+ old_input = *input;
+ old_output = *output;
+
+ *input = i_stream_create_rawlog(old_input, in_path, in_fd,
+ IOSTREAM_RAWLOG_FLAG_AUTOCLOSE |
+ IOSTREAM_RAWLOG_FLAG_TIMESTAMP);
+ *output = o_stream_create_rawlog(old_output, out_path, out_fd,
+ IOSTREAM_RAWLOG_FLAG_AUTOCLOSE |
+ IOSTREAM_RAWLOG_FLAG_TIMESTAMP);
+ i_stream_unref(&old_input);
+ o_stream_unref(&old_output);
+ return 0;
+}
+
+int iostream_rawlog_create_path(const char *path, struct istream **input,
+ struct ostream **output)
+{
+ int ret, fd;
+
+ if ((ret = iostream_rawlog_try_create_tcp(path, input, output)) != 0)
+ return ret < 0 ? -1 : 0;
+ fd = open(path, O_CREAT | O_APPEND | O_WRONLY, 0600);
+ if (fd == -1) {
+ i_error("creat(%s) failed: %m", path);
+ return -1;
+ }
+ iostream_rawlog_create_fd(fd, path, input, output);
+ return 0;
+}
+
+void iostream_rawlog_create_from_stream(struct ostream *rawlog_output,
+ struct istream **input,
+ struct ostream **output)
+{
+ const enum iostream_rawlog_flags rawlog_flags =
+ IOSTREAM_RAWLOG_FLAG_BUFFERED |
+ IOSTREAM_RAWLOG_FLAG_TIMESTAMP;
+ struct istream *old_input;
+ struct ostream *old_output;
+
+ if (input != NULL) {
+ old_input = *input;
+ *input = i_stream_create_rawlog_from_stream(old_input,
+ rawlog_output, rawlog_flags);
+ i_stream_unref(&old_input);
+ }
+ if (output != NULL) {
+ old_output = *output;
+ *output = o_stream_create_rawlog_from_stream(old_output,
+ rawlog_output, rawlog_flags);
+ o_stream_unref(&old_output);
+ }
+ if (input != NULL && output != NULL)
+ o_stream_ref(rawlog_output);
+}
diff --git a/src/lib/iostream-rawlog.h b/src/lib/iostream-rawlog.h
new file mode 100644
index 0000000..e90f9c0
--- /dev/null
+++ b/src/lib/iostream-rawlog.h
@@ -0,0 +1,28 @@
+#ifndef IOSTREAM_RAWLOG_H
+#define IOSTREAM_RAWLOG_H
+
+enum iostream_rawlog_flags {
+ IOSTREAM_RAWLOG_FLAG_AUTOCLOSE = 0x01,
+ IOSTREAM_RAWLOG_FLAG_BUFFERED = 0x02,
+ IOSTREAM_RAWLOG_FLAG_TIMESTAMP = 0x04
+};
+
+/* Create rawlog *.in and *.out files to the given directory. */
+int ATTR_NOWARN_UNUSED_RESULT
+iostream_rawlog_create(const char *dir, struct istream **input,
+ struct ostream **output);
+/* Create rawlog prefix.in and prefix.out files. */
+int ATTR_NOWARN_UNUSED_RESULT
+iostream_rawlog_create_prefix(const char *prefix, struct istream **input,
+ struct ostream **output);
+/* Create rawlog path, writing both input and output to the same file. */
+int ATTR_NOWARN_UNUSED_RESULT
+iostream_rawlog_create_path(const char *path, struct istream **input,
+ struct ostream **output);
+/* Create rawlog that appends to the given rawlog_output.
+ Both input and output are written to the same stream. */
+void iostream_rawlog_create_from_stream(struct ostream *rawlog_output,
+ struct istream **input,
+ struct ostream **output);
+
+#endif
diff --git a/src/lib/iostream-temp.c b/src/lib/iostream-temp.c
new file mode 100644
index 0000000..6e14ff5
--- /dev/null
+++ b/src/lib/iostream-temp.c
@@ -0,0 +1,404 @@
+/* Copyright (c) 2013-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "buffer.h"
+#include "str.h"
+#include "safe-mkstemp.h"
+#include "write-full.h"
+#include "istream-private.h"
+#include "ostream-private.h"
+#include "iostream-temp.h"
+
+#include <unistd.h>
+
+#define IOSTREAM_TEMP_MAX_BUF_SIZE_DEFAULT (1024*128)
+
+struct temp_ostream {
+ struct ostream_private ostream;
+
+ char *temp_path_prefix;
+ enum iostream_temp_flags flags;
+ size_t max_mem_size;
+
+ struct istream *dupstream;
+ uoff_t dupstream_offset, dupstream_start_offset;
+ char *name;
+
+ buffer_t *buf;
+ int fd;
+ bool fd_tried;
+ uoff_t fd_size;
+};
+
+static bool o_stream_temp_dup_cancel(struct temp_ostream *tstream,
+ enum ostream_send_istream_result *res_r);
+
+static void
+o_stream_temp_close(struct iostream_private *stream,
+ bool close_parent ATTR_UNUSED)
+{
+ struct temp_ostream *tstream =
+ container_of(stream, struct temp_ostream, ostream.iostream);
+
+ i_close_fd(&tstream->fd);
+ buffer_free(&tstream->buf);
+ i_free(tstream->temp_path_prefix);
+ i_free(tstream->name);
+}
+
+static int o_stream_temp_move_to_fd(struct temp_ostream *tstream)
+{
+ string_t *path;
+
+ if (tstream->fd_tried)
+ return -1;
+ tstream->fd_tried = TRUE;
+
+ path = t_str_new(128);
+ str_append(path, tstream->temp_path_prefix);
+ tstream->fd = safe_mkstemp_hostpid(path, 0600, (uid_t)-1, (gid_t)-1);
+ if (tstream->fd == -1) {
+ i_error("safe_mkstemp(%s) failed: %m", str_c(path));
+ return -1;
+ }
+ if (i_unlink(str_c(path)) < 0) {
+ i_close_fd(&tstream->fd);
+ return -1;
+ }
+ if (write_full(tstream->fd, tstream->buf->data, tstream->buf->used) < 0) {
+ i_error("write(%s) failed: %m", str_c(path));
+ i_close_fd(&tstream->fd);
+ return -1;
+ }
+ /* make the fd available also to o_stream_get_fd(),
+ e.g. for unit tests */
+ tstream->ostream.fd = tstream->fd;
+ tstream->fd_size = tstream->buf->used;
+ buffer_free(&tstream->buf);
+ return 0;
+}
+
+int o_stream_temp_move_to_memory(struct ostream *output)
+{
+ struct temp_ostream *tstream =
+ container_of(output->real_stream, struct temp_ostream, ostream);
+ unsigned char buf[IO_BLOCK_SIZE];
+ uoff_t offset = 0;
+ ssize_t ret = 0;
+
+ i_assert(tstream->buf == NULL);
+ tstream->buf = buffer_create_dynamic(default_pool, 8192);
+ while (offset < tstream->ostream.ostream.offset &&
+ (ret = pread(tstream->fd, buf, sizeof(buf), offset)) > 0) {
+ if ((size_t)ret > tstream->ostream.ostream.offset - offset)
+ ret = tstream->ostream.ostream.offset - offset;
+ buffer_append(tstream->buf, buf, ret);
+ offset += ret;
+ }
+ if (ret < 0) {
+ /* not really expecting this to happen */
+ i_error("iostream-temp %s: read(%s*) failed: %m",
+ o_stream_get_name(&tstream->ostream.ostream),
+ tstream->temp_path_prefix);
+ tstream->ostream.ostream.stream_errno = EIO;
+ return -1;
+ }
+ i_close_fd(&tstream->fd);
+ tstream->ostream.fd = -1;
+ return 0;
+}
+
+static ssize_t
+o_stream_temp_fd_sendv(struct temp_ostream *tstream,
+ const struct const_iovec *iov, unsigned int iov_count)
+{
+ size_t bytes = 0;
+ unsigned int i;
+
+ for (i = 0; i < iov_count; i++) {
+ if (write_full(tstream->fd, iov[i].iov_base, iov[i].iov_len) < 0) {
+ i_error("iostream-temp %s: write(%s*) failed: %m - moving to memory",
+ o_stream_get_name(&tstream->ostream.ostream),
+ tstream->temp_path_prefix);
+ if (o_stream_temp_move_to_memory(&tstream->ostream.ostream) < 0)
+ return -1;
+ for (; i < iov_count; i++) {
+ buffer_append(tstream->buf, iov[i].iov_base, iov[i].iov_len);
+ bytes += iov[i].iov_len;
+ tstream->ostream.ostream.offset += iov[i].iov_len;
+ }
+ i_assert(tstream->fd_tried);
+ return bytes;
+ }
+ bytes += iov[i].iov_len;
+ tstream->ostream.ostream.offset += iov[i].iov_len;
+ }
+ tstream->fd_size += bytes;
+ return bytes;
+}
+
+static ssize_t
+o_stream_temp_sendv(struct ostream_private *stream,
+ const struct const_iovec *iov, unsigned int iov_count)
+{
+ struct temp_ostream *tstream =
+ container_of(stream, struct temp_ostream, ostream);
+ ssize_t ret = 0;
+ unsigned int i;
+ enum ostream_send_istream_result res;
+
+
+ tstream->flags &= ENUM_NEGATE(IOSTREAM_TEMP_FLAG_TRY_FD_DUP);
+ if (tstream->dupstream != NULL) {
+ if (o_stream_temp_dup_cancel(tstream, &res))
+ return -1;
+ }
+
+ if (tstream->fd != -1)
+ return o_stream_temp_fd_sendv(tstream, iov, iov_count);
+
+ for (i = 0; i < iov_count; i++) {
+ if (tstream->buf->used + iov[i].iov_len > tstream->max_mem_size) {
+ if (o_stream_temp_move_to_fd(tstream) == 0) {
+ i_assert(tstream->fd != -1);
+ return o_stream_temp_fd_sendv(tstream, iov+i,
+ iov_count-i);
+ }
+ /* failed to move to temp fd, just keep it in memory */
+ }
+ buffer_append(tstream->buf, iov[i].iov_base, iov[i].iov_len);
+ ret += iov[i].iov_len;
+ stream->ostream.offset += iov[i].iov_len;
+ }
+ return ret;
+}
+
+static bool o_stream_temp_dup_cancel(struct temp_ostream *tstream,
+ enum ostream_send_istream_result *res_r)
+{
+ struct istream *input;
+ uoff_t size = tstream->dupstream_offset -
+ tstream->dupstream_start_offset;
+ bool ret = TRUE; /* use res_r to return error */
+
+ i_stream_seek(tstream->dupstream, tstream->dupstream_start_offset);
+ tstream->ostream.ostream.offset = 0;
+
+ input = i_stream_create_limit(tstream->dupstream, size);
+ i_stream_unref(&tstream->dupstream);
+
+ *res_r = io_stream_copy(&tstream->ostream.ostream, input);
+ switch (*res_r) {
+ case OSTREAM_SEND_ISTREAM_RESULT_FINISHED:
+ /* everything copied */
+ ret = FALSE;
+ break;
+ case OSTREAM_SEND_ISTREAM_RESULT_WAIT_INPUT:
+ case OSTREAM_SEND_ISTREAM_RESULT_WAIT_OUTPUT:
+ i_unreached();
+ case OSTREAM_SEND_ISTREAM_RESULT_ERROR_INPUT:
+ tstream->ostream.ostream.stream_errno = input->stream_errno;
+ io_stream_set_error(&tstream->ostream.iostream,
+ "iostream-temp: read(%s) failed: %s",
+ i_stream_get_name(input),
+ i_stream_get_error(input));
+ break;
+ case OSTREAM_SEND_ISTREAM_RESULT_ERROR_OUTPUT:
+ break;
+ }
+ i_stream_destroy(&input);
+ return ret;
+}
+
+static bool
+o_stream_temp_dup_istream(struct temp_ostream *outstream,
+ struct istream *instream,
+ enum ostream_send_istream_result *res_r)
+{
+ uoff_t in_size;
+
+ if (!instream->readable_fd || i_stream_get_fd(instream) == -1)
+ return FALSE;
+
+ if (i_stream_get_size(instream, TRUE, &in_size) <= 0) {
+ if (outstream->dupstream != NULL)
+ return o_stream_temp_dup_cancel(outstream, res_r);
+ return FALSE;
+ }
+ i_assert(instream->v_offset <= in_size);
+
+ if (outstream->dupstream == NULL) {
+ outstream->dupstream = instream;
+ outstream->dupstream_start_offset = instream->v_offset;
+ i_stream_ref(outstream->dupstream);
+ } else {
+ if (outstream->dupstream != instream ||
+ outstream->dupstream_offset != instream->v_offset ||
+ outstream->dupstream_offset > in_size)
+ return o_stream_temp_dup_cancel(outstream, res_r);
+ }
+ i_stream_seek(instream, in_size);
+ /* we should be at EOF now. o_stream_send_istream() asserts if
+ eof isn't set. */
+ instream->eof = TRUE;
+ outstream->dupstream_offset = instream->v_offset;
+ outstream->ostream.ostream.offset =
+ outstream->dupstream_offset - outstream->dupstream_start_offset;
+ *res_r = OSTREAM_SEND_ISTREAM_RESULT_FINISHED;
+ return TRUE;
+}
+
+static enum ostream_send_istream_result
+o_stream_temp_send_istream(struct ostream_private *_outstream,
+ struct istream *instream)
+{
+ struct temp_ostream *outstream =
+ container_of(_outstream, struct temp_ostream, ostream);
+ enum ostream_send_istream_result res;
+
+ if ((outstream->flags & IOSTREAM_TEMP_FLAG_TRY_FD_DUP) != 0) {
+ if (o_stream_temp_dup_istream(outstream, instream, &res))
+ return res;
+ outstream->flags &= ENUM_NEGATE(IOSTREAM_TEMP_FLAG_TRY_FD_DUP);
+ }
+ return io_stream_copy(&outstream->ostream.ostream, instream);
+}
+
+static int
+o_stream_temp_write_at(struct ostream_private *stream,
+ const void *data, size_t size, uoff_t offset)
+{
+ struct temp_ostream *tstream =
+ container_of(stream, struct temp_ostream, ostream);
+
+ if (tstream->fd == -1) {
+ i_assert(stream->ostream.offset == tstream->buf->used);
+ buffer_write(tstream->buf, offset, data, size);
+ stream->ostream.offset = tstream->buf->used;
+ } else {
+ if (pwrite_full(tstream->fd, data, size, offset) < 0) {
+ stream->ostream.stream_errno = errno;
+ i_close_fd(&tstream->fd);
+ return -1;
+ }
+ if (tstream->fd_size < offset + size)
+ tstream->fd_size = offset + size;
+ }
+ return 0;
+}
+
+static int o_stream_temp_seek(struct ostream_private *_stream, uoff_t offset)
+{
+ _stream->ostream.offset = offset;
+ return 0;
+}
+
+struct ostream *iostream_temp_create(const char *temp_path_prefix,
+ enum iostream_temp_flags flags)
+{
+ return iostream_temp_create_named(temp_path_prefix, flags, "");
+}
+
+struct ostream *iostream_temp_create_named(const char *temp_path_prefix,
+ enum iostream_temp_flags flags,
+ const char *name)
+{
+ return iostream_temp_create_sized(temp_path_prefix, flags, name,
+ IOSTREAM_TEMP_MAX_BUF_SIZE_DEFAULT);
+}
+
+struct ostream *iostream_temp_create_sized(const char *temp_path_prefix,
+ enum iostream_temp_flags flags,
+ const char *name,
+ size_t max_mem_size)
+{
+ struct temp_ostream *tstream;
+ struct ostream *output;
+
+ tstream = i_new(struct temp_ostream, 1);
+ tstream->ostream.ostream.blocking = TRUE;
+ tstream->ostream.sendv = o_stream_temp_sendv;
+ tstream->ostream.send_istream = o_stream_temp_send_istream;
+ tstream->ostream.write_at = o_stream_temp_write_at;
+ tstream->ostream.seek = o_stream_temp_seek;
+ tstream->ostream.iostream.close = o_stream_temp_close;
+ tstream->temp_path_prefix = i_strdup(temp_path_prefix);
+ tstream->flags = flags;
+ tstream->max_mem_size = max_mem_size;
+ tstream->buf = buffer_create_dynamic(default_pool, 8192);
+ tstream->fd = -1;
+
+ output = o_stream_create(&tstream->ostream, NULL, -1);
+ tstream->name = i_strdup(name);
+ if (name[0] == '\0') {
+ o_stream_set_name(output, t_strdup_printf(
+ "(temp iostream in %s)", temp_path_prefix));
+ } else {
+ o_stream_set_name(output, t_strdup_printf(
+ "(temp iostream in %s for %s)", temp_path_prefix, name));
+ }
+ return output;
+}
+
+static void iostream_temp_buf_destroyed(buffer_t *buf)
+{
+ buffer_free(&buf);
+}
+
+struct istream *iostream_temp_finish(struct ostream **output,
+ size_t max_buffer_size)
+{
+ struct temp_ostream *tstream =
+ container_of((*output)->real_stream, struct temp_ostream,
+ ostream);
+ struct istream *input, *input2;
+ uoff_t abs_offset, size;
+ const char *for_path;
+ int fd;
+
+ if (tstream->name[0] == '\0')
+ for_path = "";
+ else
+ for_path = t_strdup_printf(" for %s", tstream->name);
+
+ if (tstream->dupstream != NULL && !tstream->dupstream->closed) {
+ abs_offset = i_stream_get_absolute_offset(tstream->dupstream) -
+ tstream->dupstream->v_offset +
+ tstream->dupstream_start_offset;
+ size = tstream->dupstream_offset -
+ tstream->dupstream_start_offset;
+ fd = dup(i_stream_get_fd(tstream->dupstream));
+ if (fd == -1)
+ input = i_stream_create_error_str(errno, "dup() failed: %m");
+ else {
+ input2 = i_stream_create_fd_autoclose(&fd, max_buffer_size);
+ i_stream_seek(input2, abs_offset);
+ input = i_stream_create_limit(input2, size);
+ i_stream_unref(&input2);
+ }
+ i_stream_set_name(input, t_strdup_printf(
+ "(Temp file in %s%s, from %s)", tstream->temp_path_prefix,
+ for_path, i_stream_get_name(tstream->dupstream)));
+ i_stream_unref(&tstream->dupstream);
+ } else if (tstream->dupstream != NULL) {
+ /* return the original failed stream. */
+ input = tstream->dupstream;
+ } else if (tstream->fd != -1) {
+ int fd = tstream->fd;
+ input = i_stream_create_fd_autoclose(&tstream->fd, max_buffer_size);
+ i_stream_set_name(input, t_strdup_printf(
+ "(Temp file fd %d in %s%s, %"PRIuUOFF_T" bytes)",
+ fd, tstream->temp_path_prefix, for_path, tstream->fd_size));
+ } else {
+ input = i_stream_create_from_data(tstream->buf->data,
+ tstream->buf->used);
+ i_stream_set_name(input, t_strdup_printf(
+ "(Temp buffer in %s%s, %zu bytes)",
+ tstream->temp_path_prefix, for_path, tstream->buf->used));
+ i_stream_add_destroy_callback(input, iostream_temp_buf_destroyed,
+ tstream->buf);
+ tstream->buf = NULL;
+ }
+ o_stream_destroy(output);
+ return input;
+}
diff --git a/src/lib/iostream-temp.h b/src/lib/iostream-temp.h
new file mode 100644
index 0000000..bf5a45d
--- /dev/null
+++ b/src/lib/iostream-temp.h
@@ -0,0 +1,31 @@
+#ifndef IOSTREAM_TEMP_H
+#define IOSTREAM_TEMP_H
+
+enum iostream_temp_flags {
+ /* if o_stream_send_istream() is called with a readable fd, don't
+ actually copy the input stream, just have iostream_temp_finish()
+ return a new iostream pointing to the fd dup()ed */
+ IOSTREAM_TEMP_FLAG_TRY_FD_DUP = 0x01
+};
+
+/* Start writing to given output stream. The data is initially written to
+ memory, and later to a temporary file that is immediately unlinked. */
+struct ostream *iostream_temp_create(const char *temp_path_prefix,
+ enum iostream_temp_flags flags);
+struct ostream *iostream_temp_create_named(const char *temp_path_prefix,
+ enum iostream_temp_flags flags,
+ const char *name);
+struct ostream *iostream_temp_create_sized(const char *temp_path_prefix,
+ enum iostream_temp_flags flags,
+ const char *name,
+ size_t max_mem_size);
+/* Finished writing to stream. Return input stream for it and free the
+ output stream. (It's also possible to abort iostream-temp by simply
+ destroying the ostream.) */
+struct istream *iostream_temp_finish(struct ostream **output,
+ size_t max_buffer_size);
+
+/* For internal testing: */
+int o_stream_temp_move_to_memory(struct ostream *output);
+
+#endif
diff --git a/src/lib/iostream.c b/src/lib/iostream.c
new file mode 100644
index 0000000..f2ba000
--- /dev/null
+++ b/src/lib/iostream.c
@@ -0,0 +1,154 @@
+/* Copyright (c) 2002-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "istream.h"
+#include "ostream.h"
+#include "iostream-private.h"
+
+static void
+io_stream_default_close(struct iostream_private *stream ATTR_UNUSED,
+ bool close_parent ATTR_UNUSED)
+{
+}
+
+static void
+io_stream_default_destroy(struct iostream_private *stream ATTR_UNUSED)
+{
+}
+
+void io_stream_init(struct iostream_private *stream)
+{
+ if (stream->close == NULL)
+ stream->close = io_stream_default_close;
+ if (stream->destroy == NULL)
+ stream->destroy = io_stream_default_destroy;
+ stream->ioloop = current_ioloop;
+
+ stream->refcount = 1;
+}
+
+void io_stream_ref(struct iostream_private *stream)
+{
+ i_assert(stream->refcount > 0);
+
+ stream->refcount++;
+}
+
+bool io_stream_unref(struct iostream_private *stream)
+{
+ i_assert(stream->refcount > 0);
+ if (--stream->refcount != 0)
+ return TRUE;
+
+ stream->close(stream, FALSE);
+ stream->destroy(stream);
+ return FALSE;
+}
+
+void io_stream_free(struct iostream_private *stream)
+{
+ const struct iostream_destroy_callback *dc;
+
+ if (array_is_created(&stream->destroy_callbacks)) {
+ array_foreach(&stream->destroy_callbacks, dc)
+ dc->callback(dc->context);
+ array_free(&stream->destroy_callbacks);
+ }
+
+ i_free(stream->error);
+ i_free(stream->name);
+ i_free(stream);
+}
+
+void io_stream_close(struct iostream_private *stream, bool close_parent)
+{
+ stream->close(stream, close_parent);
+}
+
+void io_stream_set_max_buffer_size(struct iostream_private *stream,
+ size_t max_size)
+{
+ stream->set_max_buffer_size(stream, max_size);
+}
+
+void io_stream_add_destroy_callback(struct iostream_private *stream,
+ void (*callback)(void *), void *context)
+{
+ struct iostream_destroy_callback *dc;
+
+ if (!array_is_created(&stream->destroy_callbacks))
+ i_array_init(&stream->destroy_callbacks, 2);
+ dc = array_append_space(&stream->destroy_callbacks);
+ dc->callback = callback;
+ dc->context = context;
+}
+
+void io_stream_remove_destroy_callback(struct iostream_private *stream,
+ void (*callback)(void *))
+{
+ const struct iostream_destroy_callback *dcs;
+ unsigned int i, count;
+
+ dcs = array_get(&stream->destroy_callbacks, &count);
+ for (i = 0; i < count; i++) {
+ if (dcs[i].callback == callback) {
+ array_delete(&stream->destroy_callbacks, i, 1);
+ return;
+ }
+ }
+ i_unreached();
+}
+
+void io_stream_set_error(struct iostream_private *stream,
+ const char *fmt, ...)
+{
+ va_list args;
+
+ va_start(args, fmt);
+ io_stream_set_verror(stream, fmt, args);
+ va_end(args);
+}
+
+void io_stream_set_verror(struct iostream_private *stream,
+ const char *fmt, va_list args)
+{
+ /* one of the parameters may be the old stream->error, so don't free
+ it before the new error is created. */
+ char *error = i_strdup_vprintf(fmt, args);
+ i_free(stream->error);
+ stream->error = error;
+}
+
+const char *io_stream_get_disconnect_reason(struct istream *input,
+ struct ostream *output)
+{
+ const char *errstr;
+
+ if (input != NULL && input->stream_errno != 0) {
+ errno = input->stream_errno;
+ errstr = i_stream_get_error(input);
+ } else if (output != NULL && output->stream_errno != 0) {
+ errno = output->stream_errno;
+ errstr = o_stream_get_error(output);
+ } else {
+ errno = 0;
+ errstr = "";
+ }
+
+ if (errno == 0 || errno == EPIPE)
+ return "Connection closed";
+ else
+ return t_strdup_printf("Connection closed: %s", errstr);
+}
+
+void io_stream_switch_ioloop_to(struct iostream_private *stream,
+ struct ioloop *ioloop)
+{
+ stream->ioloop = ioloop;
+}
+
+struct ioloop *io_stream_get_ioloop(struct iostream_private *stream)
+{
+ return (stream->ioloop == NULL ? current_ioloop : stream->ioloop);
+}
diff --git a/src/lib/iostream.h b/src/lib/iostream.h
new file mode 100644
index 0000000..87bc374
--- /dev/null
+++ b/src/lib/iostream.h
@@ -0,0 +1,10 @@
+#ifndef IOSTREAM_H
+#define IOSTREAM_H
+
+/* Returns human-readable reason for why iostream was disconnected.
+ The output is either "Connection closed" for clean disconnections or
+ "Connection closed: <error>" for unclean disconnections. */
+const char *io_stream_get_disconnect_reason(struct istream *input,
+ struct ostream *output);
+
+#endif
diff --git a/src/lib/ipwd.c b/src/lib/ipwd.c
new file mode 100644
index 0000000..8ac81fa
--- /dev/null
+++ b/src/lib/ipwd.c
@@ -0,0 +1,103 @@
+/* Copyright (c) 2011-2018 Dovecot authors, see the included COPYING file */
+
+#define _POSIX_PTHREAD_SEMANTICS /* for Solaris */
+#include "lib.h"
+#include "ipwd.h"
+
+#include <unistd.h>
+
+#define PWBUF_MIN_SIZE 128
+#define GRBUF_MIN_SIZE 128
+
+static void *pwbuf = NULL, *grbuf = NULL;
+static size_t pwbuf_size, grbuf_size;
+
+static void pw_init(void)
+{
+ size_t old_pwbuf_size = pwbuf_size;
+
+ if (pwbuf == NULL || errno == ERANGE) {
+ pwbuf_size = nearest_power(old_pwbuf_size + 1);
+ if (pwbuf_size < PWBUF_MIN_SIZE)
+ pwbuf_size = PWBUF_MIN_SIZE;
+ pwbuf = i_realloc(pwbuf, old_pwbuf_size, pwbuf_size);
+ }
+}
+
+static void gr_init(void)
+{
+ size_t old_grbuf_size = grbuf_size;
+
+ if (grbuf == NULL || errno == ERANGE) {
+ grbuf_size = nearest_power(old_grbuf_size + 1);
+ if (grbuf_size < PWBUF_MIN_SIZE)
+ grbuf_size = PWBUF_MIN_SIZE;
+ grbuf = i_realloc(grbuf, old_grbuf_size, grbuf_size);
+ }
+}
+
+void ipwd_deinit(void)
+{
+ i_free_and_null(pwbuf);
+ i_free_and_null(grbuf);
+}
+
+int i_getpwnam(const char *name, struct passwd *pwd_r)
+{
+ struct passwd *result;
+
+ errno = 0;
+ do {
+ pw_init();
+ errno = getpwnam_r(name, pwd_r, pwbuf, pwbuf_size, &result);
+ } while (errno == ERANGE);
+ if (result != NULL)
+ return 1;
+ if (errno == EINVAL) {
+ /* FreeBSD fails here when name="user@domain" */
+ return 0;
+ }
+ return errno == 0 ? 0 : -1;
+}
+
+int i_getpwuid(uid_t uid, struct passwd *pwd_r)
+{
+ struct passwd *result;
+
+ errno = 0;
+ do {
+ pw_init();
+ errno = getpwuid_r(uid, pwd_r, pwbuf, pwbuf_size, &result);
+ } while (errno == ERANGE);
+ if (result != NULL)
+ return 1;
+ return errno == 0 ? 0 : -1;
+}
+
+int i_getgrnam(const char *name, struct group *grp_r)
+{
+ struct group *result;
+
+ errno = 0;
+ do {
+ gr_init();
+ errno = getgrnam_r(name, grp_r, grbuf, grbuf_size, &result);
+ } while (errno == ERANGE);
+ if (result != NULL)
+ return 1;
+ return errno == 0 ? 0 : -1;
+}
+
+int i_getgrgid(gid_t gid, struct group *grp_r)
+{
+ struct group *result;
+
+ errno = 0;
+ do {
+ gr_init();
+ errno = getgrgid_r(gid, grp_r, grbuf, grbuf_size, &result);
+ } while (errno == ERANGE);
+ if (result != NULL)
+ return 1;
+ return errno == 0 ? 0 : -1;
+}
diff --git a/src/lib/ipwd.h b/src/lib/ipwd.h
new file mode 100644
index 0000000..562ebb2
--- /dev/null
+++ b/src/lib/ipwd.h
@@ -0,0 +1,23 @@
+#ifndef IPWD_H
+#define IPWD_H
+
+#include <pwd.h>
+#include <grp.h>
+
+/* Replacements for standard getpw/gr*(), fixing their ability to report errors
+ properly. As with standard getpw/gr*(), second call overwrites data used
+ by the first one.
+
+ Functions return 1 if user/group is found, 0 if not or
+ -1 if error (with errno set). */
+
+int i_getpwnam(const char *name, struct passwd *pwd_r);
+int i_getpwuid(uid_t uid, struct passwd *pwd_r);
+
+int i_getgrnam(const char *name, struct group *grp_r);
+int i_getgrgid(gid_t gid, struct group *grp_r);
+
+/* Free memory used by above functions. */
+void ipwd_deinit(void);
+
+#endif
diff --git a/src/lib/iso8601-date.c b/src/lib/iso8601-date.c
new file mode 100644
index 0000000..a0c84ac
--- /dev/null
+++ b/src/lib/iso8601-date.c
@@ -0,0 +1,309 @@
+/* Copyright (c) 2013-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "utc-offset.h"
+#include "utc-mktime.h"
+#include "iso8601-date.h"
+
+#include <ctype.h>
+
+/* RFC3339/ISO8601 date-time syntax
+
+ date-fullyear = 4DIGIT
+ date-month = 2DIGIT ; 01-12
+ date-mday = 2DIGIT ; 01-28, 01-29, 01-30, 01-31 based on
+ ; month/year
+ time-hour = 2DIGIT ; 00-23
+ time-minute = 2DIGIT ; 00-59
+ time-second = 2DIGIT ; 00-58, 00-59, 00-60 based on leap second
+ ; rules
+ time-secfrac = "." 1*DIGIT
+ time-numoffset = ("+" / "-") time-hour ":" time-minute
+ time-offset = "Z" / time-numoffset
+
+ partial-time = time-hour ":" time-minute ":" time-second [time-secfrac]
+ full-date = date-fullyear "-" date-month "-" date-mday
+ full-time = partial-time time-offset
+
+ date-time = full-date "T" full-time
+ */
+
+struct iso8601_date_parser {
+ const unsigned char *cur, *end;
+
+ struct tm tm;
+ int timezone_offset;
+};
+
+static inline int
+iso8601_date_parse_number(struct iso8601_date_parser *parser,
+ int digits, int *number_r)
+{
+ int i;
+
+ if (parser->cur >= parser->end || !i_isdigit(parser->cur[0]))
+ return 0;
+
+ *number_r = parser->cur[0] - '0';
+ parser->cur++;
+
+ for (i=0; i < digits-1; i++) {
+ if (parser->cur >= parser->end || !i_isdigit(parser->cur[0]))
+ return -1;
+ *number_r = ((*number_r) * 10) + parser->cur[0] - '0';
+ parser->cur++;
+ }
+ return 1;
+}
+
+static int
+iso8601_date_parse_secfrac(struct iso8601_date_parser *parser)
+{
+ /* time-secfrac = "." 1*DIGIT
+
+ NOTE: Currently not applied anywhere, so fraction is just skipped.
+ */
+
+ /* "." */
+ if (parser->cur >= parser->end || parser->cur[0] != '.')
+ return 0;
+ parser->cur++;
+
+ /* 1DIGIT */
+ if (parser->cur >= parser->end || !i_isdigit(parser->cur[0]))
+ return -1;
+ parser->cur++;
+
+ /* *DIGIT */
+ while (parser->cur < parser->end && i_isdigit(parser->cur[0]))
+ parser->cur++;
+ return 1;
+}
+
+static int is08601_date_parse_time_offset(struct iso8601_date_parser *parser)
+{
+ int tz_sign = 1, tz_hour = 0, tz_min = 0;
+
+ /* time-offset = "Z" / time-numoffset
+ time-numoffset = ("+" / "-") time-hour ":" time-minute
+ time-hour = 2DIGIT ; 00-23
+ time-minute = 2DIGIT ; 00-59
+ */
+
+ if (parser->cur >= parser->end)
+ return 0;
+
+ /* time-offset = "Z" / time-numoffset */
+ switch (parser->cur[0]) {
+ case '-':
+ tz_sign = -1;
+ /* fall through */
+ case '+':
+ parser->cur++;
+
+ /* time-hour = 2DIGIT */
+ if (iso8601_date_parse_number(parser, 2, &tz_hour) <= 0)
+ return -1;
+ if (tz_hour > 23)
+ return -1;
+
+ /* ":" */
+ if (parser->cur >= parser->end || parser->cur[0] != ':')
+ return -1;
+ parser->cur++;
+
+ /* time-minute = 2DIGIT */
+ if (iso8601_date_parse_number(parser, 2, &tz_min) <= 0)
+ return -1;
+ if (tz_min > 59)
+ return -1;
+ break;
+ case 'Z':
+ case 'z':
+ parser->cur++;
+ break;
+ default:
+ return -1;
+ }
+
+ parser->timezone_offset = tz_sign*(tz_hour*60 + tz_min);
+ return 1;
+}
+
+static int is08601_date_parse_full_time(struct iso8601_date_parser *parser)
+{
+ /* full-time = partial-time time-offset
+ partial-time = time-hour ":" time-minute ":" time-second [time-secfrac]
+ time-hour = 2DIGIT ; 00-23
+ time-minute = 2DIGIT ; 00-59
+ time-second = 2DIGIT ; 00-58, 00-59, 00-60 based on leap second
+ ; rules
+ */
+
+ /* time-hour = 2DIGIT */
+ if (iso8601_date_parse_number(parser, 2, &parser->tm.tm_hour) <= 0)
+ return -1;
+
+ /* ":" */
+ if (parser->cur >= parser->end || parser->cur[0] != ':')
+ return -1;
+ parser->cur++;
+
+ /* time-minute = 2DIGIT */
+ if (iso8601_date_parse_number(parser, 2, &parser->tm.tm_min) <= 0)
+ return -1;
+
+ /* ":" */
+ if (parser->cur >= parser->end || parser->cur[0] != ':')
+ return -1;
+ parser->cur++;
+
+ /* time-second = 2DIGIT */
+ if (iso8601_date_parse_number(parser, 2, &parser->tm.tm_sec) <= 0)
+ return -1;
+
+ /* [time-secfrac] */
+ if (iso8601_date_parse_secfrac(parser) < 0)
+ return -1;
+
+ /* time-offset */
+ if (is08601_date_parse_time_offset(parser) <= 0)
+ return -1;
+ return 1;
+}
+
+static int is08601_date_parse_full_date(struct iso8601_date_parser *parser)
+{
+ /* full-date = date-fullyear "-" date-month "-" date-mday
+ date-fullyear = 4DIGIT
+ date-month = 2DIGIT ; 01-12
+ date-mday = 2DIGIT ; 01-28, 01-29, 01-30, 01-31 based on
+ ; month/year
+ */
+
+ /* date-fullyear = 4DIGIT */
+ if (iso8601_date_parse_number(parser, 4, &parser->tm.tm_year) <= 0)
+ return -1;
+ if (parser->tm.tm_year < 1900)
+ return -1;
+ parser->tm.tm_year -= 1900;
+
+ /* "-" */
+ if (parser->cur >= parser->end || parser->cur[0] != '-')
+ return -1;
+ parser->cur++;
+
+ /* date-month = 2DIGIT */
+ if (iso8601_date_parse_number(parser, 2, &parser->tm.tm_mon) <= 0)
+ return -1;
+ parser->tm.tm_mon -= 1;
+
+ /* "-" */
+ if (parser->cur >= parser->end || parser->cur[0] != '-')
+ return -1;
+ parser->cur++;
+
+ /* time-second = 2DIGIT */
+ if (iso8601_date_parse_number(parser, 2, &parser->tm.tm_mday) <= 0)
+ return -1;
+ return 1;
+}
+
+static int iso8601_date_parse_date_time(struct iso8601_date_parser *parser)
+{
+ /* date-time = full-date "T" full-time */
+
+ /* full-date */
+ if (is08601_date_parse_full_date(parser) <= 0)
+ return -1;
+
+ /* "T" */
+ if (parser->cur >= parser->end ||
+ (parser->cur[0] != 'T' && parser->cur[0] != 't'))
+ return -1;
+ parser->cur++;
+
+ /* full-time */
+ if (is08601_date_parse_full_time(parser) <= 0)
+ return -1;
+
+ if (parser->cur != parser->end)
+ return -1;
+ return 1;
+}
+
+static bool
+iso8601_date_do_parse(const unsigned char *data, size_t size, struct tm *tm_r,
+ time_t *timestamp_r, int *timezone_offset_r)
+{
+ struct iso8601_date_parser parser;
+ time_t timestamp;
+
+ i_zero(&parser);
+ parser.cur = data;
+ parser.end = data + size;
+
+ if (iso8601_date_parse_date_time(&parser) <= 0)
+ return FALSE;
+
+ parser.tm.tm_isdst = -1;
+ timestamp = utc_mktime(&parser.tm);
+ if (timestamp == (time_t)-1)
+ return FALSE;
+
+ *timezone_offset_r = parser.timezone_offset;
+ *tm_r = parser.tm;
+ *timestamp_r = timestamp - parser.timezone_offset * 60;
+ return TRUE;
+}
+
+bool iso8601_date_parse(const unsigned char *data, size_t size,
+ time_t *timestamp_r, int *timezone_offset_r)
+{
+ struct tm tm;
+
+ return iso8601_date_do_parse(data, size, &tm,
+ timestamp_r, timezone_offset_r);
+}
+
+bool iso8601_date_parse_tm(const unsigned char *data, size_t size,
+ struct tm *tm_r, int *timezone_offset_r)
+{
+ time_t timestamp;
+
+ return iso8601_date_do_parse(data, size, tm_r,
+ &timestamp, timezone_offset_r);
+}
+
+const char *iso8601_date_create_tm(struct tm *tm, int timezone_offset)
+{
+ const char *time_offset;
+
+ if (timezone_offset == INT_MAX)
+ time_offset = "Z";
+ else {
+ char sign = '+';
+ if (timezone_offset < 0) {
+ timezone_offset = -timezone_offset;
+ sign = '-';
+ }
+ time_offset = t_strdup_printf("%c%02d:%02d", sign,
+ timezone_offset / 60,
+ timezone_offset % 60);
+ }
+
+ return t_strdup_printf("%04d-%02d-%02dT%02d:%02d:%02d%s",
+ tm->tm_year + 1900, tm->tm_mon+1, tm->tm_mday,
+ tm->tm_hour, tm->tm_min, tm->tm_sec, time_offset);
+}
+
+const char *iso8601_date_create(time_t timestamp)
+{
+ struct tm *tm;
+ int timezone_offset;
+
+ tm = localtime(&timestamp);
+ timezone_offset = utc_offset(tm, timestamp);
+
+ return iso8601_date_create_tm(tm, timezone_offset);
+}
diff --git a/src/lib/iso8601-date.h b/src/lib/iso8601-date.h
new file mode 100644
index 0000000..f015e7d
--- /dev/null
+++ b/src/lib/iso8601-date.h
@@ -0,0 +1,21 @@
+#ifndef ISO8601_DATE_H
+#define ISO8601_DATE_H
+
+/* Parses ISO8601 (RFC3339) date-time string. timezone_offset is filled with the
+ timezone's difference to UTC in minutes. Returned time_t timestamp is
+ compensated for time zone. */
+bool iso8601_date_parse(const unsigned char *data, size_t size,
+ time_t *timestamp_r, int *timezone_offset_r);
+/* Equal to iso8601_date_parse, but writes uncompensated timestamp to tm_r. */
+bool iso8601_date_parse_tm(const unsigned char *data, size_t size,
+ struct tm *tm_r, int *timezone_offset_r);
+
+/* Create ISO8601 date-time string from given time struct in specified
+ timezone. A zone offset of zero will not to 'Z', but '+00:00'. If
+ zone_offset == INT_MAX, the time zone will be 'Z'. */
+const char *iso8601_date_create_tm(struct tm *tm, int zone_offset);
+
+/* Create ISO8601 date-time string from given time in local timezone. */
+const char *iso8601_date_create(time_t timestamp);
+
+#endif
diff --git a/src/lib/istream-base64-decoder.c b/src/lib/istream-base64-decoder.c
new file mode 100644
index 0000000..6eaa842
--- /dev/null
+++ b/src/lib/istream-base64-decoder.c
@@ -0,0 +1,171 @@
+/* Copyright (c) 2013-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "buffer.h"
+#include "base64.h"
+#include "hex-binary.h"
+#include "istream-private.h"
+#include "istream-base64.h"
+
+struct base64_decoder_istream {
+ struct istream_private istream;
+
+ struct base64_decoder decoder;
+};
+
+static int i_stream_read_parent(struct istream_private *stream)
+{
+ size_t size;
+ ssize_t ret;
+
+ size = i_stream_get_data_size(stream->parent);
+ if (size >= 4)
+ return 1;
+
+ /* we have less than one base64 block.
+ see if there is more data available. */
+ ret = i_stream_read_memarea(stream->parent);
+ if (ret <= 0) {
+ stream->istream.stream_errno = stream->parent->stream_errno;
+ return ret;
+ }
+ size = i_stream_get_data_size(stream->parent);
+ i_assert(size != 0);
+ return 1;
+}
+
+static int
+i_stream_base64_try_decode_block(struct base64_decoder_istream *bstream)
+{
+ struct istream_private *stream = &bstream->istream;
+ const unsigned char *data;
+ size_t size, avail, pos;
+ buffer_t buf;
+
+ data = i_stream_get_data(stream->parent, &size);
+ if (size == 0)
+ return 0;
+
+ if (!i_stream_try_alloc(stream, (size+3)/4*3, &avail))
+ return -2;
+
+ buffer_create_from_data(&buf, stream->w_buffer + stream->pos, avail);
+ if (base64_decode_more(&bstream->decoder, data, size, &pos, &buf) < 0) {
+ io_stream_set_error(&stream->iostream,
+ "Invalid base64 data: 0x%s",
+ binary_to_hex(data+pos, I_MIN(size-pos, 8)));
+ stream->istream.stream_errno = EINVAL;
+ return -1;
+ }
+
+ stream->pos += buf.used;
+ i_stream_skip(stream->parent, pos);
+ return pos > 0 ? 1 : 0;
+}
+
+static void
+i_stream_base64_finish_decode(struct base64_decoder_istream *bstream)
+{
+ struct istream_private *stream = &bstream->istream;
+
+ i_assert(i_stream_get_data_size(stream->parent) == 0);
+
+ if (base64_decode_finish(&bstream->decoder) < 0) {
+ io_stream_set_error(&stream->iostream,
+ "Base64 data ends prematurely");
+ stream->istream.stream_errno = EPIPE;
+ }
+}
+
+static ssize_t i_stream_base64_decoder_read(struct istream_private *stream)
+{
+ struct base64_decoder_istream *bstream =
+ container_of(stream, struct base64_decoder_istream, istream);
+ size_t pre_count, post_count;
+ int ret;
+
+ if (base64_decode_is_finished(&bstream->decoder)) {
+ stream->istream.eof = TRUE;
+ return -1;
+ }
+
+ do {
+ ret = i_stream_read_parent(stream);
+ if (ret == 0)
+ return 0;
+ if (ret < 0 && ret != -2) {
+ if (stream->istream.stream_errno != 0)
+ return -1;
+ if (i_stream_get_data_size(stream->parent) == 0) {
+ i_stream_base64_finish_decode(bstream);
+ stream->istream.eof = TRUE;
+ return -1;
+ }
+ }
+
+ /* encode as many blocks as fits into destination buffer */
+ pre_count = stream->pos - stream->skip;
+ while ((ret = i_stream_base64_try_decode_block(bstream)) > 0) ;
+ post_count = stream->pos - stream->skip;
+ } while (ret == 0 && pre_count == post_count);
+
+ if (ret < 0 && pre_count == post_count)
+ return ret;
+
+ i_assert(post_count > pre_count);
+ return post_count - pre_count;
+}
+
+static void
+i_stream_base64_decoder_seek(struct istream_private *stream,
+ uoff_t v_offset, bool mark)
+{
+ struct base64_decoder_istream *bstream =
+ container_of(stream, struct base64_decoder_istream, istream);
+
+ if (v_offset < stream->istream.v_offset) {
+ /* seeking backwards - go back to beginning and seek
+ forward from there. */
+ stream->parent_expected_offset = stream->parent_start_offset;
+ stream->skip = stream->pos = 0;
+ stream->istream.v_offset = 0;
+ i_stream_seek(stream->parent, 0);
+
+ base64_decode_reset(&bstream->decoder);
+ }
+ i_stream_default_seek_nonseekable(stream, v_offset, mark);
+}
+
+static struct istream *
+i_stream_create_base64_decoder_common(const struct base64_scheme *b64,
+ struct istream *input)
+{
+ struct base64_decoder_istream *bstream;
+
+ bstream = i_new(struct base64_decoder_istream, 1);
+ bstream->istream.max_buffer_size = input->real_stream->max_buffer_size;
+
+ bstream->istream.read = i_stream_base64_decoder_read;
+ bstream->istream.seek = i_stream_base64_decoder_seek;
+
+ bstream->istream.istream.readable_fd = FALSE;
+ bstream->istream.istream.blocking = input->blocking;
+ bstream->istream.istream.seekable = input->seekable;
+
+ base64_decode_init(&bstream->decoder, b64, 0);
+
+ return i_stream_create(&bstream->istream, input,
+ i_stream_get_fd(input), 0);
+}
+
+struct istream *
+i_stream_create_base64_decoder(struct istream *input)
+{
+ return i_stream_create_base64_decoder_common(&base64_scheme, input);
+}
+
+struct istream *
+i_stream_create_base64url_decoder(struct istream *input)
+{
+ return i_stream_create_base64_decoder_common(&base64url_scheme, input);
+}
diff --git a/src/lib/istream-base64-encoder.c b/src/lib/istream-base64-encoder.c
new file mode 100644
index 0000000..22d2786
--- /dev/null
+++ b/src/lib/istream-base64-encoder.c
@@ -0,0 +1,226 @@
+/* Copyright (c) 2003-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "buffer.h"
+#include "base64.h"
+#include "istream-private.h"
+#include "istream-base64.h"
+
+struct base64_encoder_istream {
+ struct istream_private istream;
+
+ struct base64_encoder encoder;
+};
+
+static int i_stream_read_parent(struct istream_private *stream)
+{
+ size_t size;
+ ssize_t ret;
+
+ size = i_stream_get_data_size(stream->parent);
+ if (size > 0)
+ return 1;
+
+ ret = i_stream_read_memarea(stream->parent);
+ if (ret <= 0) {
+ stream->istream.stream_errno = stream->parent->stream_errno;
+ return ret;
+ }
+ size = i_stream_get_data_size(stream->parent);
+ i_assert(size != 0);
+ return 1;
+}
+
+static int
+i_stream_base64_try_encode(struct base64_encoder_istream *bstream)
+{
+ struct istream_private *stream = &bstream->istream;
+ struct base64_encoder *b64enc = &bstream->encoder;
+ const unsigned char *data;
+ size_t size, pos, out_size, avail;
+ buffer_t buf;
+
+ data = i_stream_get_data(stream->parent, &size);
+ if (size == 0)
+ return 0;
+
+ out_size = base64_encode_get_size(b64enc, size);
+ if (!i_stream_try_alloc(stream, out_size, &avail))
+ return -2;
+
+ buffer_create_from_data(&buf, stream->w_buffer + stream->pos, avail);
+ base64_encode_more(b64enc, data, size, &pos, &buf);
+ i_assert(buf.used > 0);
+
+ stream->pos += buf.used;
+ i_stream_skip(stream->parent, pos);
+ return 1;
+}
+
+static int
+i_stream_base64_finish_encode(struct base64_encoder_istream *bstream)
+{
+ struct istream_private *stream = &bstream->istream;
+ struct base64_encoder *b64enc = &bstream->encoder;
+ size_t out_size, buffer_avail;
+ buffer_t buf;
+
+ out_size = base64_encode_get_size(b64enc, 0);
+ if (out_size == 0) {
+ if (base64_encode_finish(b64enc, NULL))
+ stream->istream.eof = TRUE;
+ return 1;
+ }
+
+ if (!i_stream_try_alloc(stream, out_size, &buffer_avail))
+ return -2;
+
+ buffer_create_from_data(&buf, stream->w_buffer + stream->pos,
+ buffer_avail);
+ if (base64_encode_finish(b64enc, &buf))
+ stream->istream.eof = TRUE;
+ i_assert(buf.used > 0);
+
+ stream->pos += buf.used;
+ return 1;
+}
+
+static ssize_t i_stream_base64_encoder_read(struct istream_private *stream)
+{
+ struct base64_encoder_istream *bstream =
+ container_of(stream, struct base64_encoder_istream, istream);
+ size_t pre_count, post_count;
+ int ret;
+
+ if (base64_encode_is_finished(&bstream->encoder)) {
+ stream->istream.eof = TRUE;
+ return -1;
+ }
+
+ pre_count = post_count = 0;
+ do {
+ ret = i_stream_read_parent(stream);
+ if (ret == 0)
+ return 0;
+ if (ret < 0) {
+ if (stream->istream.stream_errno != 0)
+ return -1;
+ if (i_stream_get_data_size(stream->parent) == 0)
+ break;
+ /* add the final partial block */
+ }
+
+ /* encode as many lines as fits into destination buffer */
+ pre_count = stream->pos - stream->skip;
+ while ((ret = i_stream_base64_try_encode(bstream)) > 0) ;
+ post_count = stream->pos - stream->skip;
+ } while (ret == 0 && pre_count == post_count);
+
+ if (ret == -2) {
+ if (pre_count == post_count)
+ return -2;
+ } else if (ret < 0) {
+ if (i_stream_get_data_size(stream->parent) == 0) {
+ i_assert(post_count == pre_count);
+ pre_count = stream->pos - stream->skip;
+ ret = i_stream_base64_finish_encode(bstream);
+ post_count = stream->pos - stream->skip;
+ if (ret <= 0)
+ return ret;
+ }
+ if (pre_count == post_count) {
+ stream->istream.eof = TRUE;
+ return -1;
+ }
+ }
+
+ i_assert(post_count > pre_count);
+ return post_count - pre_count;
+}
+
+static void
+i_stream_base64_encoder_seek(struct istream_private *stream,
+ uoff_t v_offset, bool mark)
+{
+ struct base64_encoder_istream *bstream =
+ container_of(stream, struct base64_encoder_istream, istream);
+
+ if (v_offset < stream->istream.v_offset) {
+ /* seeking backwards - go back to beginning and seek
+ forward from there. */
+ stream->parent_expected_offset = stream->parent_start_offset;
+ stream->skip = stream->pos = 0;
+ stream->istream.v_offset = 0;
+ i_stream_seek(stream->parent, 0);
+
+ base64_encode_reset(&bstream->encoder);
+ }
+ i_stream_default_seek_nonseekable(stream, v_offset, mark);
+}
+
+static int
+i_stream_base64_encoder_stat(struct istream_private *stream,
+ bool exact ATTR_UNUSED)
+{
+ struct base64_encoder_istream *bstream =
+ container_of(stream, struct base64_encoder_istream, istream);
+ const struct stat *st;
+
+ if (i_stream_stat(stream->parent, exact, &st) < 0) {
+ stream->istream.stream_errno = stream->parent->stream_errno;
+ return -1;
+ }
+
+ stream->statbuf = *st;
+ if (st->st_size == 0)
+ return 0;
+
+ stream->statbuf.st_size =
+ base64_get_full_encoded_size(&bstream->encoder, st->st_size);
+ return 0;
+}
+
+static struct istream *
+i_stream_create_base64_encoder_common(const struct base64_scheme *b64,
+ struct istream *input,
+ unsigned int chars_per_line, bool crlf)
+{
+ struct base64_encoder_istream *bstream;
+ enum base64_encode_flags b64_flags = 0;
+
+ i_assert(chars_per_line % 4 == 0);
+
+ bstream = i_new(struct base64_encoder_istream, 1);
+ bstream->istream.max_buffer_size = input->real_stream->max_buffer_size;
+
+ bstream->istream.read = i_stream_base64_encoder_read;
+ bstream->istream.seek = i_stream_base64_encoder_seek;
+ bstream->istream.stat = i_stream_base64_encoder_stat;
+
+ bstream->istream.istream.readable_fd = FALSE;
+ bstream->istream.istream.blocking = input->blocking;
+ bstream->istream.istream.seekable = input->seekable;
+
+ if (crlf)
+ b64_flags |= BASE64_ENCODE_FLAG_CRLF;
+ base64_encode_init(&bstream->encoder, b64, b64_flags, chars_per_line);
+
+ return i_stream_create(&bstream->istream, input,
+ i_stream_get_fd(input), 0);
+}
+
+struct istream *
+i_stream_create_base64_encoder(struct istream *input,
+ unsigned int chars_per_line, bool crlf)
+{
+ return i_stream_create_base64_encoder_common(&base64_scheme, input,
+ chars_per_line, crlf);
+}
+
+struct istream *
+i_stream_create_base64url_encoder(struct istream *input,
+ unsigned int chars_per_line, bool crlf)
+{
+ return i_stream_create_base64_encoder_common(&base64url_scheme, input,
+ chars_per_line, crlf);
+}
diff --git a/src/lib/istream-base64.h b/src/lib/istream-base64.h
new file mode 100644
index 0000000..4f422f7
--- /dev/null
+++ b/src/lib/istream-base64.h
@@ -0,0 +1,16 @@
+#ifndef ISTREAM_BASE64_H
+#define ISTREAM_BASE64_H
+
+struct istream *
+i_stream_create_base64_encoder(struct istream *input,
+ unsigned int chars_per_line, bool crlf);
+struct istream *
+i_stream_create_base64url_encoder(struct istream *input,
+ unsigned int chars_per_line, bool crlf);
+
+struct istream *
+i_stream_create_base64_decoder(struct istream *input);
+struct istream *
+i_stream_create_base64url_decoder(struct istream *input);
+
+#endif
diff --git a/src/lib/istream-callback.c b/src/lib/istream-callback.c
new file mode 100644
index 0000000..6f07d50
--- /dev/null
+++ b/src/lib/istream-callback.c
@@ -0,0 +1,118 @@
+/* Copyright (c) 2014-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "buffer.h"
+#include "istream-private.h"
+#include "istream-callback.h"
+
+struct callback_istream {
+ struct istream_private istream;
+ istream_callback_read_t *callback;
+ void *context;
+
+ buffer_t *buf;
+ size_t prev_pos;
+};
+
+static void i_stream_callback_destroy(struct iostream_private *stream)
+{
+ struct callback_istream *cstream =
+ container_of(stream, struct callback_istream, istream.iostream);
+
+ buffer_free(&cstream->buf);
+}
+
+static ssize_t i_stream_callback_read(struct istream_private *stream)
+{
+ struct callback_istream *cstream =
+ container_of(stream, struct callback_istream, istream);
+ size_t pos;
+
+ if (cstream->callback == NULL) {
+ /* already returned EOF / error */
+ stream->istream.eof = TRUE;
+ return -1;
+ }
+
+ if (stream->skip > 0) {
+ buffer_delete(cstream->buf, 0, stream->skip);
+ stream->pos -= stream->skip;
+ cstream->prev_pos -= stream->skip;
+ stream->skip = 0;
+ }
+ i_assert(cstream->buf->used >= cstream->prev_pos);
+ pos = cstream->prev_pos;
+ if (cstream->buf->used > pos) {
+ /* data was added outside the callback */
+ } else if (!cstream->callback(cstream->buf, cstream->context)) {
+ /* EOF / error */
+ stream->istream.eof = TRUE;
+ cstream->callback = NULL;
+ if (cstream->buf->used == pos ||
+ stream->istream.stream_errno != 0)
+ return -1;
+ /* EOF was returned with some data still added to the buffer.
+ return the buffer first and EOF only on the next call. */
+ } else if (cstream->buf->used == pos) {
+ /* buffer full */
+ i_assert(cstream->buf->used > 0);
+ return -2;
+ }
+ i_assert(cstream->buf->used > pos);
+ stream->buffer = cstream->buf->data;
+ cstream->prev_pos = stream->pos = cstream->buf->used;
+ return cstream->buf->used - pos;
+}
+
+#undef i_stream_create_callback
+struct istream *
+i_stream_create_callback(istream_callback_read_t *callback, void *context)
+{
+ struct callback_istream *cstream;
+ struct istream *istream;
+
+ i_assert(callback != NULL);
+
+ cstream = i_new(struct callback_istream, 1);
+ cstream->callback = callback;
+ cstream->context = context;
+ cstream->buf = buffer_create_dynamic(default_pool, 1024);
+
+ cstream->istream.iostream.destroy = i_stream_callback_destroy;
+ cstream->istream.read = i_stream_callback_read;
+
+ istream = i_stream_create(&cstream->istream, NULL, -1, 0);
+ istream->blocking = TRUE;
+ return istream;
+}
+
+void i_stream_callback_append(struct istream *input,
+ const void *data, size_t size)
+{
+ struct callback_istream *cstream =
+ container_of(input->real_stream,
+ struct callback_istream, istream);
+
+ buffer_append(cstream->buf, data, size);
+}
+
+void i_stream_callback_append_str(struct istream *input, const char *str)
+{
+ i_stream_callback_append(input, str, strlen(str));
+}
+
+buffer_t *i_stream_callback_get_buffer(struct istream *input)
+{
+ struct callback_istream *cstream =
+ container_of(input->real_stream,
+ struct callback_istream, istream);
+
+ return cstream->buf;
+}
+
+void i_stream_callback_set_error(struct istream *input, int stream_errno,
+ const char *error)
+{
+ input->stream_errno = stream_errno;
+ io_stream_set_error(&input->real_stream->iostream, "%s", error);
+}
diff --git a/src/lib/istream-callback.h b/src/lib/istream-callback.h
new file mode 100644
index 0000000..1d91ced
--- /dev/null
+++ b/src/lib/istream-callback.h
@@ -0,0 +1,39 @@
+#ifndef ISTREAM_CALLBACK_H
+#define ISTREAM_CALLBACK_H
+
+/* istream-callback can be used to implement an istream that returns data
+ by calling the specified callback. The callback needs to do:
+
+ a) Add data to buffer unless the buffer size is already too large
+ (the callback can decide by itself what is too large). Return TRUE
+ regardless of whether any data was added.
+
+ b) Return FALSE when it's finished adding data or when it reaches an error.
+ On error i_stream_callback_set_error() must be called before returning.
+
+ i_stream_add_destroy_callback() can be also added to do any cleanups that
+ the callback may need to do.
+*/
+typedef bool istream_callback_read_t(buffer_t *buf, void *context);
+
+struct istream *
+i_stream_create_callback(istream_callback_read_t *callback, void *context);
+#define i_stream_create_callback(callback, context) \
+ i_stream_create_callback(1 ? (istream_callback_read_t *)callback : \
+ CALLBACK_TYPECHECK(callback, bool (*)(buffer_t *buf, typeof(context))), \
+ context)
+
+/* Append data to the istream externally. Typically this is used to add a
+ header to the stream before the callbacks are called. */
+void i_stream_callback_append(struct istream *input,
+ const void *data, size_t size);
+void i_stream_callback_append_str(struct istream *input, const char *str);
+
+/* Returns the istream-callback's internal buffer. This buffer can be used to
+ append data to the stream. */
+buffer_t *i_stream_callback_get_buffer(struct istream *input);
+
+void i_stream_callback_set_error(struct istream *input, int stream_errno,
+ const char *error);
+
+#endif
diff --git a/src/lib/istream-chain.c b/src/lib/istream-chain.c
new file mode 100644
index 0000000..3b9a87b
--- /dev/null
+++ b/src/lib/istream-chain.c
@@ -0,0 +1,349 @@
+/* Copyright (c) 2003-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "llist.h"
+#include "memarea.h"
+#include "istream-private.h"
+#include "istream-chain.h"
+
+struct chain_istream;
+
+struct istream_chain_link {
+ struct istream_chain_link *prev, *next;
+
+ struct istream *stream;
+ bool eof;
+};
+
+struct istream_chain {
+ struct istream_chain_link *head, *tail;
+
+ struct chain_istream *stream;
+};
+
+struct chain_istream {
+ struct istream_private istream;
+
+ /* how much of the previous link's stream still exists at the
+ beginning of our buffer. skipping through this should point to
+ the beginning of the current link's stream. */
+ size_t prev_stream_left;
+ size_t prev_skip;
+
+ struct istream_chain chain;
+};
+
+static void ATTR_NULL(2)
+i_stream_chain_append_internal(struct istream_chain *chain,
+ struct istream *stream)
+{
+ struct istream_chain_link *link;
+
+ if (stream == NULL && chain->tail != NULL && chain->tail->stream == NULL)
+ return;
+
+ link = i_new(struct istream_chain_link, 1);
+ link->stream = stream;
+ link->eof = stream == NULL;
+
+ if (stream != NULL)
+ i_stream_ref(stream);
+
+ if (chain->head == NULL && stream != NULL) {
+ i_stream_set_max_buffer_size(stream,
+ chain->stream->istream.max_buffer_size);
+ }
+ DLLIST2_APPEND(&chain->head, &chain->tail, link);
+ /* if io_add_istream() has been added to this chain stream, notify
+ the callback that we have more data available. */
+ if (stream != NULL)
+ i_stream_set_input_pending(stream, TRUE);
+}
+
+void i_stream_chain_append(struct istream_chain *chain, struct istream *stream)
+{
+ i_stream_chain_append_internal(chain, stream);
+}
+
+void i_stream_chain_append_eof(struct istream_chain *chain)
+{
+ i_stream_chain_append_internal(chain, NULL);
+}
+
+static void
+i_stream_chain_set_max_buffer_size(struct iostream_private *stream,
+ size_t max_size)
+{
+ struct chain_istream *cstream =
+ container_of(stream, struct chain_istream, istream.iostream);
+ struct istream_chain_link *link = cstream->chain.head;
+
+ cstream->istream.max_buffer_size = max_size;
+ while (link != NULL) {
+ if (link->stream != NULL)
+ i_stream_set_max_buffer_size(link->stream, max_size);
+ link = link->next;
+ }
+}
+
+static void i_stream_chain_destroy(struct iostream_private *stream)
+{
+ struct chain_istream *cstream =
+ container_of(stream, struct chain_istream, istream.iostream);
+ struct istream_chain_link *link = cstream->chain.head;
+
+ while (link != NULL) {
+ struct istream_chain_link *next = link->next;
+
+ i_stream_unref(&link->stream);
+ i_free(link);
+ link = next;
+ }
+ i_stream_free_buffer(&cstream->istream);
+}
+
+static void i_stream_chain_read_next(struct chain_istream *cstream)
+{
+ struct istream_chain_link *link = cstream->chain.head;
+ struct istream *prev_input;
+ const unsigned char *data;
+ size_t data_size, cur_data_pos;
+
+ i_assert(link != NULL && link->stream != NULL);
+ i_assert(link->stream->eof);
+
+ prev_input = link->stream;
+ data = i_stream_get_data(prev_input, &data_size);
+
+ DLLIST2_REMOVE(&cstream->chain.head, &cstream->chain.tail, link);
+ i_free(link);
+
+ /* a) we have more streams, b) we have EOF, c) we need to wait
+ for more streams */
+ link = cstream->chain.head;
+ if (link != NULL && link->stream != NULL)
+ i_stream_seek(link->stream, 0);
+
+ if (cstream->prev_stream_left > 0) {
+ /* we've already buffered some of the prev_input. continue
+ appending the rest to it. if it's already at EOF, there's
+ nothing more to append. */
+ cur_data_pos = cstream->istream.pos -
+ (cstream->istream.skip + cstream->prev_stream_left);
+ i_assert(cur_data_pos <= data_size);
+ data += cur_data_pos;
+ data_size -= cur_data_pos;
+ /* the stream has now become "previous", so its contents in
+ buffer are now part of prev_stream_left. */
+ cstream->prev_stream_left += cur_data_pos;
+ } else {
+ cstream->istream.pos = 0;
+ cstream->istream.skip = 0;
+ cstream->prev_stream_left = 0;
+ }
+
+ if (data_size > 0) {
+ if (cstream->istream.memarea != NULL &&
+ memarea_get_refcount(cstream->istream.memarea) > 1)
+ i_stream_memarea_detach(&cstream->istream);
+ memcpy(i_stream_alloc(&cstream->istream, data_size),
+ data, data_size);
+ cstream->istream.pos += data_size;
+ cstream->prev_stream_left += data_size;
+ }
+
+ i_stream_skip(prev_input, i_stream_get_data_size(prev_input));
+ i_stream_unref(&prev_input);
+}
+
+static bool i_stream_chain_skip(struct chain_istream *cstream)
+{
+ struct istream_private *stream = &cstream->istream;
+ struct istream_chain_link *link = cstream->chain.head;
+ size_t bytes_skipped;
+
+ i_assert(stream->skip >= cstream->prev_skip);
+ bytes_skipped = stream->skip - cstream->prev_skip;
+
+ if (cstream->prev_stream_left == 0) {
+ /* no need to worry about buffers, skip everything */
+ } else if (bytes_skipped < cstream->prev_stream_left) {
+ /* we're still skipping inside buffer */
+ cstream->prev_stream_left -= bytes_skipped;
+ bytes_skipped = 0;
+ } else {
+ /* done with the buffer */
+ bytes_skipped -= cstream->prev_stream_left;
+ cstream->prev_stream_left = 0;
+ }
+ if (bytes_skipped > 0) {
+ i_assert(stream->buffer != NULL);
+ stream->pos -= bytes_skipped;
+ stream->skip -= bytes_skipped;
+ stream->buffer += bytes_skipped;
+ }
+ cstream->prev_skip = stream->skip;
+ if (link == NULL || link->eof) {
+ i_assert(bytes_skipped == 0);
+ return FALSE;
+ }
+ i_stream_skip(link->stream, bytes_skipped);
+ return TRUE;
+}
+
+static ssize_t i_stream_chain_read(struct istream_private *stream)
+{
+ struct chain_istream *cstream =
+ container_of(stream, struct chain_istream, istream);
+ struct istream_chain_link *link = cstream->chain.head;
+ const unsigned char *data;
+ size_t data_size, cur_data_pos, new_pos;
+ size_t new_bytes_count;
+ ssize_t ret;
+
+ if (link != NULL && link->eof) {
+ stream->istream.eof = TRUE;
+ return -1;
+ }
+
+ if (!i_stream_chain_skip(cstream))
+ return 0;
+ i_assert(link != NULL);
+
+ i_assert(stream->pos >= stream->skip + cstream->prev_stream_left);
+ cur_data_pos = stream->pos - (stream->skip + cstream->prev_stream_left);
+
+ data = i_stream_get_data(link->stream, &data_size);
+ if (data_size > cur_data_pos)
+ ret = 0;
+ else {
+ /* need to read more */
+ i_assert(cur_data_pos == data_size);
+ ret = i_stream_read_memarea(link->stream);
+ if (ret == -2 || ret == 0)
+ return ret;
+
+ if (ret == -1) {
+ if (link->stream->stream_errno != 0) {
+ io_stream_set_error(&stream->iostream,
+ "read(%s) failed: %s",
+ i_stream_get_name(link->stream),
+ i_stream_get_error(link->stream));
+ stream->istream.stream_errno =
+ link->stream->stream_errno;
+ return -1;
+ }
+ /* EOF of this stream, go to next stream */
+ i_stream_chain_read_next(cstream);
+ cstream->prev_skip = stream->skip;
+ return i_stream_chain_read(stream);
+ }
+ /* we read something */
+ data = i_stream_get_data(link->stream, &data_size);
+ }
+
+ if (data_size == cur_data_pos) {
+ /* nothing new read - preserve the buffer as it was */
+ i_assert(ret == 0 || ret == -1);
+ return ret;
+ }
+ if (cstream->prev_stream_left == 0) {
+ /* we can point directly to the current stream's buffers */
+ stream->buffer = data;
+ stream->pos -= stream->skip;
+ stream->skip = 0;
+ new_pos = data_size;
+ } else {
+ /* we still have some of the previous stream left. merge the
+ new data with it. */
+ i_assert(data_size > cur_data_pos);
+ new_bytes_count = data_size - cur_data_pos;
+ memcpy(i_stream_alloc(stream, new_bytes_count),
+ data + cur_data_pos, new_bytes_count);
+ stream->buffer = stream->w_buffer;
+ new_pos = stream->pos + new_bytes_count;
+ }
+
+ i_assert(new_pos > stream->pos);
+ ret = (ssize_t)(new_pos - stream->pos);
+ stream->pos = new_pos;
+ cstream->prev_skip = stream->skip;
+ return ret;
+}
+
+static void i_stream_chain_close(struct iostream_private *stream,
+ bool close_parent)
+{
+ struct chain_istream *cstream =
+ container_of(stream, struct chain_istream, istream.iostream);
+
+ /* seek to the correct position in parent stream in case it didn't
+ end with EOF */
+ (void)i_stream_chain_skip(cstream);
+
+ if (close_parent) {
+ struct istream_chain_link *link = cstream->chain.head;
+ while (link != NULL) {
+ i_stream_close(link->stream);
+ link = link->next;
+ }
+ }
+}
+
+static struct istream_snapshot *
+i_stream_chain_snapshot(struct istream_private *stream,
+ struct istream_snapshot *prev_snapshot)
+{
+ if (stream->buffer == stream->w_buffer) {
+ /* Two or more istreams have been combined. Snapshot the
+ w_buffer's contents that contains their data. */
+ i_assert(stream->memarea != NULL);
+ return i_stream_default_snapshot(stream, prev_snapshot);
+ }
+ /* Individual istreams are being read. Snapshot the istream directly. */
+ struct chain_istream *cstream =
+ container_of(stream, struct chain_istream, istream);
+ struct istream_chain_link *link = cstream->chain.head;
+ if (link == NULL || link->stream == NULL)
+ return prev_snapshot;
+
+ struct istream_private *_link_stream = link->stream->real_stream;
+ struct istream_snapshot *snapshot = i_new(struct istream_snapshot, 1);
+ snapshot->prev_snapshot =
+ _link_stream->snapshot(_link_stream, prev_snapshot);
+ if (snapshot->prev_snapshot == prev_snapshot) {
+ /* The link stream didn't implement snapshotting in any way.
+ This could cause trouble if the link stream is freed while
+ it's still referred to in this snapshot. Fix this by
+ referencing the link istream. Normally avoid doing this,
+ since the extra references can cause unexpected problems. */
+ snapshot->istream = link->stream;
+ i_stream_ref(snapshot->istream);
+ }
+ return snapshot;
+}
+
+struct istream *i_stream_create_chain(struct istream_chain **chain_r,
+ size_t max_buffer_size)
+{
+ struct chain_istream *cstream;
+
+ cstream = i_new(struct chain_istream, 1);
+ cstream->chain.stream = cstream;
+ cstream->istream.max_buffer_size = max_buffer_size;
+
+ cstream->istream.iostream.close = i_stream_chain_close;
+ cstream->istream.iostream.destroy = i_stream_chain_destroy;
+ cstream->istream.iostream.set_max_buffer_size =
+ i_stream_chain_set_max_buffer_size;
+
+ cstream->istream.read = i_stream_chain_read;
+ cstream->istream.snapshot = i_stream_chain_snapshot;
+
+ cstream->istream.istream.readable_fd = FALSE;
+ cstream->istream.istream.blocking = FALSE;
+ cstream->istream.istream.seekable = FALSE;
+
+ *chain_r = &cstream->chain;
+ return i_stream_create(&cstream->istream, NULL, -1, 0);
+}
diff --git a/src/lib/istream-chain.h b/src/lib/istream-chain.h
new file mode 100644
index 0000000..e5ba68b
--- /dev/null
+++ b/src/lib/istream-chain.h
@@ -0,0 +1,21 @@
+#ifndef ISTREAM_CHAIN_H
+#define ISTREAM_CHAIN_H
+
+struct istream_chain;
+
+/* Flexibly couple input streams into a single chain stream. Input streams can
+ be added after creation of the chain stream, and the chain stream will not
+ signal EOF until all streams are read to EOF and the last stream added was
+ NULL. Streams that were finished to EOF are unreferenced. The chain stream
+ is obviously not seekable and it has no determinable size. The chain_r
+ argument returns a pointer to the chain object. */
+struct istream *i_stream_create_chain(struct istream_chain **chain_r,
+ size_t max_buffer_size);
+
+/* Append an input stream to the chain. */
+void i_stream_chain_append(struct istream_chain *chain, struct istream *stream);
+/* Mark the end of the chain. Only then reads from the chain stream can
+ return EOF. */
+void i_stream_chain_append_eof(struct istream_chain *chain);
+
+#endif
diff --git a/src/lib/istream-concat.c b/src/lib/istream-concat.c
new file mode 100644
index 0000000..4c58b86
--- /dev/null
+++ b/src/lib/istream-concat.c
@@ -0,0 +1,391 @@
+/* Copyright (c) 2007-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "buffer.h"
+#include "memarea.h"
+#include "istream-private.h"
+#include "istream-concat.h"
+
+struct concat_istream {
+ struct istream_private istream;
+
+ struct istream **input, *cur_input;
+ uoff_t *input_size;
+ unsigned int input_count;
+
+ unsigned int cur_idx, unknown_size_idx;
+ size_t prev_stream_left, prev_stream_skip, prev_skip;
+};
+
+static void i_stream_concat_skip(struct concat_istream *cstream);
+
+static void i_stream_concat_close(struct iostream_private *stream,
+ bool close_parent)
+{
+ struct concat_istream *cstream =
+ container_of(stream, struct concat_istream, istream.iostream);
+ i_assert(cstream->cur_input == cstream->input[cstream->cur_idx]);
+ unsigned int i;
+
+ if (cstream->istream.istream.stream_errno == 0) {
+ /* get the parent streams to the wanted offset */
+ (void)i_stream_concat_skip(cstream);
+ }
+
+ if (close_parent) {
+ for (i = 0; i < cstream->input_count; i++)
+ i_stream_close(cstream->input[i]);
+ }
+}
+
+static void i_stream_concat_destroy(struct iostream_private *stream)
+{
+ struct concat_istream *cstream =
+ container_of(stream, struct concat_istream, istream.iostream);
+ i_assert(cstream->cur_input == cstream->input[cstream->cur_idx]);
+ unsigned int i;
+
+ for (i = 0; i < cstream->input_count; i++)
+ i_stream_unref(&cstream->input[i]);
+ i_free(cstream->input);
+ i_free(cstream->input_size);
+ i_stream_free_buffer(&cstream->istream);
+}
+
+static void
+i_stream_concat_set_max_buffer_size(struct iostream_private *stream,
+ size_t max_size)
+{
+ struct concat_istream *cstream =
+ container_of(stream, struct concat_istream, istream.iostream);
+ i_assert(cstream->cur_input == cstream->input[cstream->cur_idx]);
+ unsigned int i;
+
+ cstream->istream.max_buffer_size = max_size;
+ for (i = 0; i < cstream->input_count; i++)
+ i_stream_set_max_buffer_size(cstream->input[i], max_size);
+}
+
+static void i_stream_concat_read_next(struct concat_istream *cstream)
+{
+ struct istream *prev_input = cstream->cur_input;
+ const unsigned char *data;
+ size_t data_size, size;
+
+ i_assert(cstream->cur_input->eof);
+
+ if (cstream->prev_stream_skip != 0) {
+ i_stream_skip(cstream->input[cstream->cur_idx-1], cstream->prev_stream_skip);
+ cstream->prev_stream_skip = 0;
+ }
+
+ data = i_stream_get_data(cstream->cur_input, &data_size);
+ cstream->cur_idx++;
+ cstream->cur_input = cstream->input[cstream->cur_idx];
+ i_stream_seek(cstream->cur_input, 0);
+
+ if (cstream->prev_stream_left > 0 || cstream->istream.pos == 0) {
+ /* all the pending data is already in w_buffer */
+ cstream->prev_stream_skip = data_size;
+ cstream->prev_stream_left += data_size;
+ i_assert(cstream->prev_stream_left ==
+ cstream->istream.pos - cstream->istream.skip);
+ return;
+ }
+ i_assert(cstream->prev_stream_skip == 0);
+
+ /* we already verified that the data size is less than the
+ maximum buffer size */
+ cstream->istream.skip = 0;
+ cstream->istream.pos = 0;
+ if (data_size > 0) {
+ if (cstream->istream.memarea != NULL &&
+ memarea_get_refcount(cstream->istream.memarea) > 1)
+ i_stream_memarea_detach(&cstream->istream);
+ if (!i_stream_try_alloc(&cstream->istream, data_size, &size))
+ i_unreached();
+ i_assert(size >= data_size);
+ }
+
+ cstream->prev_stream_left = data_size;
+ memcpy(cstream->istream.w_buffer, data, data_size);
+ i_stream_skip(prev_input, data_size);
+ cstream->istream.skip = 0;
+ cstream->istream.pos = data_size;
+}
+
+static void i_stream_concat_skip(struct concat_istream *cstream)
+{
+ struct istream_private *stream = &cstream->istream;
+ size_t bytes_skipped;
+
+ i_assert(stream->skip >= cstream->prev_skip);
+ bytes_skipped = stream->skip - cstream->prev_skip;
+
+ if (cstream->prev_stream_left == 0) {
+ /* no need to worry about buffers, skip everything */
+ } else if (bytes_skipped < cstream->prev_stream_left) {
+ /* we're still skipping inside buffer */
+ cstream->prev_stream_left -= bytes_skipped;
+ bytes_skipped = 0;
+ } else {
+ /* done with the buffer */
+ i_stream_skip(cstream->input[cstream->cur_idx-1], cstream->prev_stream_skip);
+ cstream->prev_stream_skip = 0;
+
+ bytes_skipped -= cstream->prev_stream_left;
+ cstream->prev_stream_left = 0;
+ }
+ if (bytes_skipped > 0) {
+ i_assert(stream->buffer != NULL);
+ stream->pos -= bytes_skipped;
+ stream->skip -= bytes_skipped;
+ stream->buffer += bytes_skipped;
+ }
+ cstream->prev_skip = stream->skip;
+ i_stream_skip(cstream->cur_input, bytes_skipped);
+}
+
+static ssize_t i_stream_concat_read(struct istream_private *stream)
+{
+ struct concat_istream *cstream =
+ container_of(stream, struct concat_istream, istream);
+ i_assert(cstream->cur_input == cstream->input[cstream->cur_idx]);
+ const unsigned char *data;
+ size_t size, data_size, cur_data_pos, new_pos;
+ size_t new_bytes_count;
+ ssize_t ret;
+ bool last_stream;
+
+ i_assert(cstream->cur_input != NULL);
+ i_stream_concat_skip(cstream);
+
+ i_assert(stream->pos >= stream->skip + cstream->prev_stream_left);
+ cur_data_pos = stream->pos - (stream->skip + cstream->prev_stream_left);
+
+ data = i_stream_get_data(cstream->cur_input, &data_size);
+ if (data_size > cur_data_pos)
+ ret = 0;
+ else {
+ /* need to read more - NOTE: Can't use i_stream_read_memarea()
+ here, because our stream->buffer may point to the parent
+ istream. Implementing explicit snapshot function to avoid
+ this isn't easy for seekable concat-istreams, because due to
+ seeking it's not necessarily the cur_input that needs to be
+ snapshotted. */
+ i_assert(cur_data_pos == data_size);
+ ret = i_stream_read(cstream->cur_input);
+ if (ret == -2 || ret == 0)
+ return ret;
+
+ if (ret == -1 && cstream->cur_input->stream_errno != 0) {
+ io_stream_set_error(&cstream->istream.iostream,
+ "read(%s) failed: %s",
+ i_stream_get_name(cstream->cur_input),
+ i_stream_get_error(cstream->cur_input));
+ stream->istream.stream_errno =
+ cstream->cur_input->stream_errno;
+ return -1;
+ }
+
+ /* we either read something or we're at EOF */
+ last_stream = cstream->cur_idx+1 >= cstream->input_count;
+ if (ret == -1 && !last_stream) {
+ if (stream->pos - stream->skip >= i_stream_get_max_buffer_size(&stream->istream))
+ return -2;
+
+ i_stream_concat_read_next(cstream);
+ cstream->prev_skip = stream->skip;
+ return i_stream_concat_read(stream);
+ }
+
+ stream->istream.eof = cstream->cur_input->eof && last_stream;
+ i_assert(ret != -1 || stream->istream.eof);
+ data = i_stream_get_data(cstream->cur_input, &data_size);
+ }
+
+ if (data_size == cur_data_pos) {
+ /* nothing new read - preserve the buffer as it was */
+ i_assert(ret == 0 || ret == -1);
+ return ret;
+ }
+ if (cstream->prev_stream_left == 0) {
+ /* we can point directly to the current stream's buffers */
+ stream->buffer = data;
+ stream->pos -= stream->skip;
+ stream->skip = 0;
+ new_pos = data_size;
+ } else {
+ /* we still have some of the previous stream left. merge the
+ new data with it. */
+ i_assert(data_size > cur_data_pos);
+ new_bytes_count = data_size - cur_data_pos;
+ if (!i_stream_try_alloc(stream, new_bytes_count, &size)) {
+ stream->buffer = stream->w_buffer;
+ return -2;
+ }
+ stream->buffer = stream->w_buffer;
+
+ /* we'll copy all the new input to w_buffer. if we skip over
+ prev_stream_left bytes, the next read will switch to
+ pointing to cur_input's data directly. */
+ if (new_bytes_count > size)
+ new_bytes_count = size;
+ memcpy(stream->w_buffer + stream->pos,
+ data + cur_data_pos, new_bytes_count);
+ new_pos = stream->pos + new_bytes_count;
+ }
+
+ i_assert(new_pos > stream->pos);
+ ret = (ssize_t)(new_pos - stream->pos);
+ stream->pos = new_pos;
+ cstream->prev_skip = stream->skip;
+ return ret;
+}
+
+static int
+find_v_offset(struct concat_istream *cstream, uoff_t *v_offset,
+ unsigned int *idx_r)
+{
+ const struct stat *st;
+ unsigned int i;
+
+ for (i = 0; i < cstream->input_count; i++) {
+ if (*v_offset == 0) {
+ /* seek to beginning of this stream */
+ break;
+ }
+ if (i == cstream->unknown_size_idx) {
+ /* we'll need to figure out this stream's size */
+ if (i_stream_stat(cstream->input[i], TRUE, &st) < 0) {
+ io_stream_set_error(&cstream->istream.iostream,
+ "stat(%s) failed: %s",
+ i_stream_get_name(cstream->input[i]),
+ i_stream_get_error(cstream->input[i]));
+ i_error("istream-concat: stat(%s) failed: %s",
+ i_stream_get_name(cstream->input[i]),
+ i_stream_get_error(cstream->input[i]));
+ cstream->istream.istream.stream_errno =
+ cstream->input[i]->stream_errno;
+ return -1;
+ }
+
+ /* @UNSAFE */
+ cstream->input_size[i] = st->st_size;
+ cstream->unknown_size_idx = i + 1;
+ }
+ if (*v_offset < cstream->input_size[i])
+ break;
+ *v_offset -= cstream->input_size[i];
+ }
+
+ *idx_r = i;
+ return 0;
+}
+
+static void i_stream_concat_seek(struct istream_private *stream,
+ uoff_t v_offset, bool mark ATTR_UNUSED)
+{
+ struct concat_istream *cstream =
+ container_of(stream, struct concat_istream, istream);
+ i_assert(cstream->cur_input == cstream->input[cstream->cur_idx]);
+
+ stream->istream.v_offset = v_offset;
+ stream->skip = stream->pos = 0;
+ cstream->prev_stream_left = 0;
+ cstream->prev_stream_skip = 0;
+ cstream->prev_skip = 0;
+
+ if (find_v_offset(cstream, &v_offset, &cstream->cur_idx) < 0) {
+ /* failed */
+ stream->istream.stream_errno = EINVAL;
+ return;
+ }
+ if (cstream->cur_idx < cstream->input_count)
+ cstream->cur_input = cstream->input[cstream->cur_idx];
+ else {
+ /* we allow seeking to EOF, but not past it. */
+ if (v_offset != 0) {
+ io_stream_set_error(&cstream->istream.iostream,
+ "Seeking past EOF by %"PRIuUOFF_T" bytes", v_offset);
+ cstream->istream.istream.stream_errno = EINVAL;
+ return;
+ }
+ i_assert(cstream->cur_idx > 0);
+ /* Position ourselves at the EOF of the last actual stream. */
+ cstream->cur_idx--;
+ cstream->cur_input = cstream->input[cstream->cur_idx];
+ v_offset = cstream->input_size[cstream->cur_idx];
+ }
+ i_stream_seek(cstream->cur_input, v_offset);
+}
+
+static int
+i_stream_concat_stat(struct istream_private *stream, bool exact ATTR_UNUSED)
+{
+ struct concat_istream *cstream =
+ container_of(stream, struct concat_istream, istream);
+ i_assert(cstream->cur_input == cstream->input[cstream->cur_idx]);
+ uoff_t v_offset = UOFF_T_MAX;
+ unsigned int i, cur_idx;
+
+ /* make sure we have all sizes */
+ if (find_v_offset(cstream, &v_offset, &cur_idx) < 0)
+ return -1;
+
+ stream->statbuf.st_size = 0;
+ for (i = 0; i < cstream->unknown_size_idx; i++)
+ stream->statbuf.st_size += cstream->input_size[i];
+ return 0;
+}
+
+struct istream *i_stream_create_concat(struct istream *input[])
+{
+ struct concat_istream *cstream;
+ unsigned int count;
+ size_t max_buffer_size = 0;
+ bool blocking = TRUE, seekable = TRUE;
+
+ /* if any of the streams isn't blocking or seekable, set ourself also
+ nonblocking/nonseekable */
+ for (count = 0; input[count] != NULL; count++) {
+ size_t cur_max = i_stream_get_max_buffer_size(input[count]);
+
+ i_assert(cur_max != 0);
+ if (cur_max != SIZE_MAX && cur_max > max_buffer_size)
+ max_buffer_size = cur_max;
+ if (!input[count]->blocking)
+ blocking = FALSE;
+ if (!input[count]->seekable)
+ seekable = FALSE;
+ i_stream_ref(input[count]);
+ }
+ i_assert(count != 0);
+ if (max_buffer_size == 0)
+ max_buffer_size = SIZE_MAX;
+ if (max_buffer_size < I_STREAM_MIN_SIZE)
+ max_buffer_size = I_STREAM_MIN_SIZE;
+
+ cstream = i_new(struct concat_istream, 1);
+ cstream->input_count = count;
+ cstream->input = p_memdup(default_pool, input, sizeof(*input) * count);
+ cstream->input_size = i_new(uoff_t, count);
+
+ cstream->cur_input = cstream->input[0];
+ i_stream_seek(cstream->cur_input, 0);
+
+ cstream->istream.iostream.close = i_stream_concat_close;
+ cstream->istream.iostream.destroy = i_stream_concat_destroy;
+ cstream->istream.iostream.set_max_buffer_size =
+ i_stream_concat_set_max_buffer_size;
+
+ cstream->istream.max_buffer_size = max_buffer_size;
+ cstream->istream.read = i_stream_concat_read;
+ cstream->istream.seek = i_stream_concat_seek;
+ cstream->istream.stat = i_stream_concat_stat;
+
+ cstream->istream.istream.readable_fd = FALSE;
+ cstream->istream.istream.blocking = blocking;
+ cstream->istream.istream.seekable = seekable;
+ return i_stream_create(&cstream->istream, NULL, -1, 0);
+}
diff --git a/src/lib/istream-concat.h b/src/lib/istream-concat.h
new file mode 100644
index 0000000..a028534
--- /dev/null
+++ b/src/lib/istream-concat.h
@@ -0,0 +1,7 @@
+#ifndef ISTREAM_CONCAT_H
+#define ISTREAM_CONCAT_H
+
+/* Concatenate input streams into a single stream. */
+struct istream *i_stream_create_concat(struct istream *input[]);
+
+#endif
diff --git a/src/lib/istream-crlf.c b/src/lib/istream-crlf.c
new file mode 100644
index 0000000..2d111b9
--- /dev/null
+++ b/src/lib/istream-crlf.c
@@ -0,0 +1,208 @@
+/* Copyright (c) 2007-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "istream-private.h"
+#include "istream-crlf.h"
+
+struct crlf_istream {
+ struct istream_private istream;
+
+ bool pending_cr:1;
+ bool last_cr:1;
+};
+
+static int i_stream_crlf_read_common(struct crlf_istream *cstream)
+{
+ struct istream_private *stream = &cstream->istream;
+ size_t size, avail;
+ ssize_t ret;
+
+ size = i_stream_get_data_size(stream->parent);
+ if (size == 0) {
+ ret = i_stream_read_memarea(stream->parent);
+ if (ret <= 0) {
+ i_assert(ret != -2); /* 0 sized buffer can't be full */
+ stream->istream.stream_errno =
+ stream->parent->stream_errno;
+ stream->istream.eof = stream->parent->eof;
+ return ret;
+ }
+ size = i_stream_get_data_size(stream->parent);
+ i_assert(size != 0);
+ }
+
+ if (!i_stream_try_alloc(stream, size, &avail))
+ return -2;
+ return 1;
+}
+
+static ssize_t i_stream_crlf_read_crlf(struct istream_private *stream)
+{
+ struct crlf_istream *cstream =
+ container_of(stream, struct crlf_istream, istream);
+ const unsigned char *data, *ptr, *src, *src_end;
+ unsigned char *dest, *dest_end;
+ size_t size, copy_len;
+ ssize_t ret;
+
+ ret = i_stream_crlf_read_common(cstream);
+ if (ret <= 0)
+ return ret;
+
+ /* at least one byte was read */
+ data = i_stream_get_data(stream->parent, &size);
+
+ dest = stream->w_buffer + stream->pos;
+ dest_end = stream->w_buffer + stream->buffer_size;
+ src = data;
+ src_end = data + size;
+
+ /* @UNSAFE: add missing CRs */
+ if (*src == '\n') {
+ if (!cstream->last_cr && dest < dest_end)
+ *dest++ = '\r';
+
+ if (dest < dest_end) {
+ *dest++ = '\n';
+ src++;
+ }
+ }
+
+ while (dest < dest_end) {
+ i_assert(src <= src_end);
+ ptr = memchr(src, '\n', src_end - src);
+ if (ptr == NULL)
+ ptr = src_end;
+
+ /* copy data up to LF */
+ copy_len = ptr - src;
+ if (dest + copy_len > dest_end)
+ copy_len = dest_end - dest;
+
+ if (copy_len > 0) {
+ memcpy(dest, src, copy_len);
+
+ dest += copy_len;
+ src += copy_len;
+ }
+
+ i_assert(dest <= dest_end && src <= src_end);
+ if (dest == dest_end || src == src_end)
+ break;
+
+ /* add the CR if necessary and copy the LF.
+ (src >= data+1, because data[0]=='\n' was
+ handled before this loop) */
+ if (src[-1] != '\r')
+ *dest++ = '\r';
+
+ if (dest == dest_end)
+ break;
+
+ *dest++ = '\n';
+ src++;
+ i_assert(src == ptr + 1);
+ }
+
+ i_assert(dest != stream->w_buffer);
+ cstream->last_cr = dest[-1] == '\r';
+ i_stream_skip(stream->parent, src - data);
+
+ ret = (dest - stream->w_buffer) - stream->pos;
+ i_assert(ret > 0);
+ stream->pos = dest - stream->w_buffer;
+ return ret;
+}
+
+static ssize_t i_stream_crlf_read_lf(struct istream_private *stream)
+{
+ struct crlf_istream *cstream =
+ container_of(stream, struct crlf_istream, istream);
+ const unsigned char *data, *p;
+ size_t i, dest, size, max;
+ ssize_t ret;
+ bool pending_cr;
+
+ ret = i_stream_crlf_read_common(cstream);
+ if (ret <= 0)
+ return ret;
+
+ data = i_stream_get_data(stream->parent, &size);
+
+ /* @UNSAFE */
+ /* \r\n -> \n
+ \r<anything> -> \r<anything>
+ \r\r\n -> \r\n */
+ dest = stream->pos;
+ pending_cr = cstream->pending_cr;
+ for (i = 0; i < size && dest < stream->buffer_size; ) {
+ if (data[i] == '\r') {
+ if (pending_cr) {
+ /* \r\r */
+ stream->w_buffer[dest++] = '\r';
+ } else {
+ pending_cr = TRUE;
+ }
+ i++;
+ } else if (data[i] == '\n') {
+ /* [\r]\n */
+ pending_cr = FALSE;
+ stream->w_buffer[dest++] = '\n';
+ i++;
+ } else if (pending_cr) {
+ /* \r<anything> */
+ pending_cr = FALSE;
+ stream->w_buffer[dest++] = '\r';
+ } else {
+ /* copy everything until the next \r */
+ max = I_MIN(size - i, stream->buffer_size - dest);
+ p = memchr(data + i, '\r', max);
+ if (p != NULL)
+ max = p - (data+i);
+ memcpy(stream->w_buffer + dest, data + i, max);
+ dest += max;
+ i += max;
+ }
+ }
+ i_assert(i <= size);
+ i_assert(dest <= stream->buffer_size);
+
+ cstream->pending_cr = pending_cr;
+ i_stream_skip(stream->parent, i);
+
+ ret = dest - stream->pos;
+ if (ret == 0) {
+ i_assert(cstream->pending_cr && size == 1);
+ return i_stream_crlf_read_lf(stream);
+ }
+ i_assert(ret > 0);
+ stream->pos = dest;
+ return ret;
+}
+
+static struct istream *
+i_stream_create_crlf_full(struct istream *input, bool crlf)
+{
+ struct crlf_istream *cstream;
+
+ cstream = i_new(struct crlf_istream, 1);
+ cstream->istream.max_buffer_size = input->real_stream->max_buffer_size;
+ cstream->istream.read = crlf ? i_stream_crlf_read_crlf :
+ i_stream_crlf_read_lf;
+
+ cstream->istream.istream.readable_fd = FALSE;
+ cstream->istream.istream.blocking = input->blocking;
+ cstream->istream.istream.seekable = FALSE;
+ return i_stream_create(&cstream->istream, input,
+ i_stream_get_fd(input), 0);
+}
+
+struct istream *i_stream_create_crlf(struct istream *input)
+{
+ return i_stream_create_crlf_full(input, TRUE);
+}
+
+struct istream *i_stream_create_lf(struct istream *input)
+{
+ return i_stream_create_crlf_full(input, FALSE);
+}
diff --git a/src/lib/istream-crlf.h b/src/lib/istream-crlf.h
new file mode 100644
index 0000000..1ed44c6
--- /dev/null
+++ b/src/lib/istream-crlf.h
@@ -0,0 +1,9 @@
+#ifndef ISTREAM_CRLF_H
+#define ISTREAM_CRLF_H
+
+/* Read all linefeeds as CRLF */
+struct istream *i_stream_create_crlf(struct istream *input);
+/* Read all linefeeds as LF */
+struct istream *i_stream_create_lf(struct istream *input);
+
+#endif
diff --git a/src/lib/istream-data.c b/src/lib/istream-data.c
new file mode 100644
index 0000000..b97fc21
--- /dev/null
+++ b/src/lib/istream-data.c
@@ -0,0 +1,62 @@
+/* Copyright (c) 2002-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "istream-private.h"
+
+static ssize_t i_stream_data_read(struct istream_private *stream)
+{
+ stream->istream.eof = TRUE;
+ return -1;
+}
+
+static void i_stream_data_seek(struct istream_private *stream, uoff_t v_offset,
+ bool mark ATTR_UNUSED)
+{
+ stream->skip = v_offset;
+ stream->istream.v_offset = v_offset;
+}
+
+struct istream *i_stream_create_from_data(const void *data, size_t size)
+{
+ struct istream_private *stream;
+
+ stream = i_new(struct istream_private, 1);
+ stream->buffer = data;
+ stream->pos = size;
+ stream->max_buffer_size = SIZE_MAX;
+
+ stream->read = i_stream_data_read;
+ stream->seek = i_stream_data_seek;
+
+ stream->istream.readable_fd = FALSE;
+ stream->istream.blocking = TRUE;
+ stream->istream.seekable = TRUE;
+ i_stream_create(stream, NULL, -1, ISTREAM_CREATE_FLAG_NOOP_SNAPSHOT);
+ stream->statbuf.st_size = size;
+ i_stream_set_name(&stream->istream, "(buffer)");
+ return &stream->istream;
+}
+
+static void i_stream_copied_data_free(void *data)
+{
+ i_free(data);
+}
+struct istream *
+i_stream_create_copy_from_data(const void *data, size_t size)
+{
+ struct istream *stream;
+ void *buffer;
+
+ if (size == 0) {
+ buffer = "";
+ } else {
+ buffer = i_malloc(size);
+ memcpy(buffer, data, size);
+ }
+ stream = i_stream_create_from_data(buffer, size);
+ if (size > 0) {
+ i_stream_add_destroy_callback
+ (stream, i_stream_copied_data_free, buffer);
+ }
+ return stream;
+}
diff --git a/src/lib/istream-failure-at.c b/src/lib/istream-failure-at.c
new file mode 100644
index 0000000..7dccdd3
--- /dev/null
+++ b/src/lib/istream-failure-at.c
@@ -0,0 +1,97 @@
+/* Copyright (c) 2015-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "istream-private.h"
+#include "istream-failure-at.h"
+
+struct failure_at_istream {
+ struct istream_private istream;
+ int error_code;
+ char *error_string;
+ uoff_t failure_offset;
+};
+
+static void i_stream_failure_at_destroy(struct iostream_private *stream)
+{
+ struct failure_at_istream *fstream =
+ container_of(stream, struct failure_at_istream,
+ istream.iostream);
+
+ i_free(fstream->error_string);
+}
+
+static ssize_t
+i_stream_failure_at_read(struct istream_private *stream)
+{
+ struct failure_at_istream *fstream =
+ container_of(stream, struct failure_at_istream, istream);
+ uoff_t new_offset;
+ ssize_t ret;
+
+ i_stream_seek(stream->parent, stream->parent_start_offset +
+ stream->istream.v_offset);
+
+ ret = i_stream_read_copy_from_parent(&stream->istream);
+ new_offset = stream->istream.v_offset + (stream->pos - stream->skip);
+ if (ret >= 0 && new_offset >= fstream->failure_offset) {
+ if (stream->istream.v_offset >= fstream->failure_offset) {
+ /* we already passed the wanted failure offset,
+ return error immediately. */
+ stream->pos = stream->skip;
+ stream->istream.stream_errno = errno =
+ fstream->error_code;
+ io_stream_set_error(&stream->iostream, "%s",
+ fstream->error_string);
+ ret = -1;
+ } else {
+ /* return data up to the wanted failure offset and
+ on the next read() call return failure */
+ size_t new_pos = fstream->failure_offset -
+ stream->istream.v_offset + stream->skip;
+ i_assert(new_pos >= stream->skip &&
+ stream->pos >= new_pos);
+ ret -= stream->pos - new_pos;
+ stream->pos = new_pos;
+ }
+ } else if (ret < 0 && stream->istream.stream_errno == 0 &&
+ fstream->failure_offset == UOFF_T_MAX) {
+ /* failure at EOF */
+ stream->istream.stream_errno = errno =
+ fstream->error_code;
+ io_stream_set_error(&stream->iostream, "%s",
+ fstream->error_string);
+ }
+ return ret;
+}
+
+struct istream *
+i_stream_create_failure_at(struct istream *input, uoff_t failure_offset,
+ int stream_errno, const char *error_string)
+{
+ struct failure_at_istream *fstream;
+
+ fstream = i_new(struct failure_at_istream, 1);
+ fstream->istream.max_buffer_size = input->real_stream->max_buffer_size;
+ fstream->istream.stream_size_passthrough = TRUE;
+
+ fstream->istream.read = i_stream_failure_at_read;
+ fstream->istream.iostream.destroy = i_stream_failure_at_destroy;
+
+ fstream->istream.istream.readable_fd = input->readable_fd;
+ fstream->istream.istream.blocking = input->blocking;
+ fstream->istream.istream.seekable = input->seekable;
+
+ fstream->error_code = stream_errno;
+ fstream->error_string = i_strdup(error_string);
+ fstream->failure_offset = failure_offset;
+ return i_stream_create(&fstream->istream, input,
+ i_stream_get_fd(input), 0);
+}
+
+struct istream *
+i_stream_create_failure_at_eof(struct istream *input, int stream_errno,
+ const char *error_string)
+{
+ return i_stream_create_failure_at(input, UOFF_T_MAX, stream_errno,
+ error_string);
+}
diff --git a/src/lib/istream-failure-at.h b/src/lib/istream-failure-at.h
new file mode 100644
index 0000000..2ba05d5
--- /dev/null
+++ b/src/lib/istream-failure-at.h
@@ -0,0 +1,11 @@
+#ifndef ISTREAM_FAILURE_AT_H
+#define ISTREAM_FAILURE_AT_H
+
+struct istream *
+i_stream_create_failure_at(struct istream *input, uoff_t failure_offset,
+ int stream_errno, const char *error_string);
+struct istream *
+i_stream_create_failure_at_eof(struct istream *input, int stream_errno,
+ const char *error_string);
+
+#endif
diff --git a/src/lib/istream-file-private.h b/src/lib/istream-file-private.h
new file mode 100644
index 0000000..c4701ed
--- /dev/null
+++ b/src/lib/istream-file-private.h
@@ -0,0 +1,23 @@
+#ifndef ISTREAM_FILE_PRIVATE_H
+#define ISTREAM_FILE_PRIVATE_H
+
+#include "istream-private.h"
+
+struct file_istream {
+ struct istream_private istream;
+
+ uoff_t skip_left;
+
+ bool file:1;
+ bool autoclose_fd:1;
+ bool seen_eof:1;
+};
+
+struct istream *
+i_stream_create_file_common(struct file_istream *fstream,
+ int fd, const char *path,
+ size_t max_buffer_size, bool autoclose_fd);
+ssize_t i_stream_file_read(struct istream_private *stream);
+void i_stream_file_close(struct iostream_private *stream, bool close_parent);
+
+#endif
diff --git a/src/lib/istream-file.c b/src/lib/istream-file.c
new file mode 100644
index 0000000..8c945bd
--- /dev/null
+++ b/src/lib/istream-file.c
@@ -0,0 +1,282 @@
+/* Copyright (c) 2002-2018 Dovecot authors, see the included COPYING file */
+
+/* @UNSAFE: whole file */
+
+#include "lib.h"
+#include "ioloop.h"
+#include "istream-file-private.h"
+#include "net.h"
+
+#include <time.h>
+#include <unistd.h>
+#include <fcntl.h>
+#include <sys/stat.h>
+
+void i_stream_file_close(struct iostream_private *stream,
+ bool close_parent ATTR_UNUSED)
+{
+ struct istream_private *_stream =
+ container_of(stream, struct istream_private, iostream);
+ struct file_istream *fstream =
+ container_of(_stream, struct file_istream, istream);
+
+ if (fstream->autoclose_fd && _stream->fd != -1) {
+ /* Ignore ECONNRESET because we don't really care about it here,
+ as we are closing the socket down in any case. There might be
+ unsent data but nothing we can do about that. */
+ if (unlikely(close(_stream->fd) < 0 && errno != ECONNRESET)) {
+ i_error("file_istream.close(%s) failed: %m",
+ i_stream_get_name(&_stream->istream));
+ }
+ }
+ _stream->fd = -1;
+}
+
+static int i_stream_file_open(struct istream_private *stream)
+{
+ const char *path = i_stream_get_name(&stream->istream);
+
+ stream->fd = open(path, O_RDONLY);
+ if (stream->fd == -1) {
+ io_stream_set_error(&stream->iostream,
+ "open(%s) failed: %m", path);
+ stream->istream.stream_errno = errno;
+ return -1;
+ }
+ return 0;
+}
+
+ssize_t i_stream_file_read(struct istream_private *stream)
+{
+ struct file_istream *fstream =
+ container_of(stream, struct file_istream, istream);
+ uoff_t offset;
+ size_t size;
+ ssize_t ret;
+
+ if (!i_stream_try_alloc(stream, 1, &size))
+ return -2;
+
+ if (stream->fd == -1) {
+ if (i_stream_file_open(stream) < 0)
+ return -1;
+ i_assert(stream->fd != -1);
+ }
+
+ offset = stream->istream.v_offset + (stream->pos - stream->skip);
+
+ if (fstream->file) {
+ ret = pread(stream->fd, stream->w_buffer + stream->pos,
+ size, offset);
+ } else if (fstream->seen_eof) {
+ /* don't try to read() again. EOF from keyboard (^D)
+ requires this to work right. */
+ ret = 0;
+ } else {
+ ret = read(stream->fd, stream->w_buffer + stream->pos,
+ size);
+ }
+
+ if (ret == 0) {
+ /* EOF */
+ stream->istream.eof = TRUE;
+ fstream->seen_eof = TRUE;
+ return -1;
+ }
+
+ if (unlikely(ret < 0)) {
+ if ((errno == EINTR || errno == EAGAIN) &&
+ !stream->istream.blocking) {
+ ret = 0;
+ } else {
+ i_assert(errno != 0);
+ /* if we get EBADF for a valid fd, it means something's
+ really wrong and we'd better just crash. */
+ i_assert(errno != EBADF);
+ if (fstream->file) {
+ io_stream_set_error(&stream->iostream,
+ "pread(size=%zu offset=%"PRIuUOFF_T") failed: %m",
+ size, offset);
+ } else {
+ io_stream_set_error(&stream->iostream,
+ "read(size=%zu) failed: %m",
+ size);
+ }
+ stream->istream.stream_errno = errno;
+ return -1;
+ }
+ }
+
+ if (ret > 0 && fstream->skip_left > 0) {
+ i_assert(!fstream->file);
+ i_assert(stream->skip == stream->pos);
+
+ if (fstream->skip_left >= (size_t)ret) {
+ fstream->skip_left -= ret;
+ ret = 0;
+ } else {
+ ret -= fstream->skip_left;
+ stream->pos += fstream->skip_left;
+ stream->skip += fstream->skip_left;
+ fstream->skip_left = 0;
+ }
+ }
+
+ stream->pos += ret;
+ i_assert(ret != 0 || !fstream->file);
+ i_assert(ret != -1);
+ return ret;
+}
+
+static void i_stream_file_seek(struct istream_private *stream, uoff_t v_offset,
+ bool mark ATTR_UNUSED)
+{
+ struct file_istream *fstream =
+ container_of(stream, struct file_istream, istream);
+
+ if (!stream->istream.seekable) {
+ if (v_offset < stream->istream.v_offset)
+ i_panic("stream doesn't support seeking backwards");
+ fstream->skip_left += v_offset - stream->istream.v_offset;
+ }
+
+ stream->istream.v_offset = v_offset;
+ stream->skip = stream->pos = 0;
+ fstream->seen_eof = FALSE;
+}
+
+static void i_stream_file_sync(struct istream_private *stream)
+{
+ if (!stream->istream.seekable) {
+ /* can't do anything or data would be lost */
+ return;
+ }
+
+ stream->skip = stream->pos = 0;
+ stream->istream.eof = FALSE;
+}
+
+static int
+i_stream_file_stat(struct istream_private *stream, bool exact ATTR_UNUSED)
+{
+ struct file_istream *fstream =
+ container_of(stream, struct file_istream, istream);
+ const char *name = i_stream_get_name(&stream->istream);
+
+ if (!fstream->file) {
+ /* return defaults */
+ } else if (stream->fd != -1) {
+ if (fstat(stream->fd, &stream->statbuf) < 0) {
+ stream->istream.stream_errno = errno;
+ io_stream_set_error(&stream->iostream,
+ "file_istream.fstat(%s) failed: %m", name);
+ i_error("%s", i_stream_get_error(&stream->istream));
+ return -1;
+ }
+ } else {
+ if (stat(name, &stream->statbuf) < 0) {
+ stream->istream.stream_errno = errno;
+ io_stream_set_error(&stream->iostream,
+ "file_istream.stat(%s) failed: %m", name);
+ i_error("%s", i_stream_get_error(&stream->istream));
+ return -1;
+ }
+ }
+ return 0;
+}
+
+struct istream *
+i_stream_create_file_common(struct file_istream *fstream,
+ int fd, const char *path,
+ size_t max_buffer_size, bool autoclose_fd)
+{
+ struct istream *input;
+ struct stat st;
+ bool is_file;
+ int flags;
+
+ fstream->autoclose_fd = autoclose_fd;
+
+ fstream->istream.iostream.close = i_stream_file_close;
+ fstream->istream.max_buffer_size = max_buffer_size;
+ fstream->istream.read = i_stream_file_read;
+ fstream->istream.seek = i_stream_file_seek;
+ fstream->istream.sync = i_stream_file_sync;
+ fstream->istream.stat = i_stream_file_stat;
+
+ /* if it's a file, set the flags properly */
+ if (fd == -1) {
+ /* only the path is known for now - the fd is opened later */
+ is_file = TRUE;
+ } else if (fstat(fd, &st) < 0)
+ is_file = FALSE;
+ else if (S_ISREG(st.st_mode))
+ is_file = TRUE;
+ else if (!S_ISDIR(st.st_mode))
+ is_file = FALSE;
+ else {
+ /* we're trying to open a directory.
+ we're not designed for it. */
+ io_stream_set_error(&fstream->istream.iostream,
+ "%s is a directory, can't read it as file",
+ path != NULL ? path : t_strdup_printf("<fd %d>", fd));
+ fstream->istream.istream.stream_errno = EISDIR;
+ is_file = FALSE;
+ }
+ if (is_file) {
+ fstream->file = TRUE;
+ fstream->istream.istream.blocking = TRUE;
+ fstream->istream.istream.seekable = TRUE;
+ } else if ((flags = fcntl(fd, F_GETFL, 0)) < 0) {
+ i_assert(fd > -1);
+ /* shouldn't happen */
+ fstream->istream.istream.stream_errno = errno;
+ io_stream_set_error(&fstream->istream.iostream,
+ "fcntl(%d, F_GETFL) failed: %m", fd);
+ } else if ((flags & O_NONBLOCK) == 0) {
+ /* blocking socket/fifo */
+ fstream->istream.istream.blocking = TRUE;
+ }
+ fstream->istream.istream.readable_fd = TRUE;
+
+ input = i_stream_create(&fstream->istream, NULL, fd, 0);
+ i_stream_set_name(input, is_file ? "(file)" : "(fd)");
+ return input;
+}
+
+struct istream *i_stream_create_fd(int fd, size_t max_buffer_size)
+{
+ struct file_istream *fstream;
+
+ i_assert(fd != -1);
+
+ fstream = i_new(struct file_istream, 1);
+ return i_stream_create_file_common(fstream, fd, NULL,
+ max_buffer_size, FALSE);
+}
+
+struct istream *i_stream_create_fd_autoclose(int *fd, size_t max_buffer_size)
+{
+ struct istream *input;
+ struct file_istream *fstream;
+
+ i_assert(*fd != -1);
+
+ fstream = i_new(struct file_istream, 1);
+ input = i_stream_create_file_common(fstream, *fd, NULL,
+ max_buffer_size, TRUE);
+ *fd = -1;
+ return input;
+}
+
+struct istream *i_stream_create_file(const char *path, size_t max_buffer_size)
+{
+ struct file_istream *fstream;
+ struct istream *input;
+
+ fstream = i_new(struct file_istream, 1);
+ input = i_stream_create_file_common(fstream, -1, path,
+ max_buffer_size, TRUE);
+ i_stream_set_name(input, path);
+ return input;
+}
diff --git a/src/lib/istream-hash.c b/src/lib/istream-hash.c
new file mode 100644
index 0000000..09d2ac2
--- /dev/null
+++ b/src/lib/istream-hash.c
@@ -0,0 +1,87 @@
+/* Copyright (c) 2013-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "hash-method.h"
+#include "istream-private.h"
+#include "istream-hash.h"
+
+struct hash_istream {
+ struct istream_private istream;
+
+ const struct hash_method *method;
+ void *hash_context;
+ uoff_t high_offset;
+};
+
+static ssize_t
+i_stream_hash_read(struct istream_private *stream)
+{
+ struct hash_istream *hstream =
+ container_of(stream, struct hash_istream, istream);
+ const unsigned char *data;
+ size_t size;
+ uoff_t skip;
+ ssize_t ret;
+
+ i_stream_seek(stream->parent, stream->parent_start_offset +
+ stream->istream.v_offset);
+
+ ret = i_stream_read_copy_from_parent(&stream->istream);
+ if (ret > 0 && hstream->hash_context != NULL) {
+ data = i_stream_get_data(&stream->istream, &size);
+ i_assert((size_t)ret <= size);
+
+ i_assert(stream->istream.v_offset <= hstream->high_offset);
+ skip = hstream->high_offset - stream->istream.v_offset;
+ if (skip < (size_t)size) {
+ hstream->high_offset += (size-skip);
+ hstream->method->loop(hstream->hash_context,
+ data+skip, size-skip);
+ }
+ } else if (ret < 0) {
+ /* we finished hashing it. don't access it anymore, because
+ the memory pointed by the hash may be freed before the
+ istream itself */
+ hstream->hash_context = NULL;
+ }
+ return ret;
+}
+
+static void
+i_stream_hash_seek(struct istream_private *stream,
+ uoff_t v_offset, bool mark ATTR_UNUSED)
+{
+ struct hash_istream *hstream =
+ container_of(stream, struct hash_istream, istream);
+
+ if (hstream->hash_context != NULL) {
+ io_stream_set_error(&stream->iostream,
+ "Seeking not supported before hashing is finished");
+ stream->istream.stream_errno = ESPIPE;
+ }
+ stream->istream.v_offset = v_offset;
+ stream->skip = stream->pos = 0;
+}
+
+struct istream *
+i_stream_create_hash(struct istream *input, const struct hash_method *method,
+ void *hash_context)
+{
+ struct hash_istream *hstream;
+
+ hstream = i_new(struct hash_istream, 1);
+ hstream->istream.max_buffer_size = input->real_stream->max_buffer_size;
+ hstream->istream.stream_size_passthrough = TRUE;
+
+ hstream->istream.read = i_stream_hash_read;
+ hstream->istream.seek = i_stream_hash_seek;
+
+ hstream->istream.istream.readable_fd = input->readable_fd;
+ hstream->istream.istream.blocking = input->blocking;
+ hstream->istream.istream.seekable = input->seekable;
+
+ hstream->method = method;
+ hstream->hash_context = hash_context;
+ return i_stream_create(&hstream->istream, input,
+ i_stream_get_fd(input), 0);
+}
diff --git a/src/lib/istream-hash.h b/src/lib/istream-hash.h
new file mode 100644
index 0000000..bd22201
--- /dev/null
+++ b/src/lib/istream-hash.h
@@ -0,0 +1,12 @@
+#ifndef ISTREAM_HASH_H
+#define ISTREAM_HASH_H
+
+struct hash_method;
+
+/* hash_context must be allocated and initialized by caller. This istream will
+ simply call method->loop() for all the data going through the istream. */
+struct istream *
+i_stream_create_hash(struct istream *input, const struct hash_method *method,
+ void *hash_context);
+
+#endif
diff --git a/src/lib/istream-jsonstr.c b/src/lib/istream-jsonstr.c
new file mode 100644
index 0000000..727f21c
--- /dev/null
+++ b/src/lib/istream-jsonstr.c
@@ -0,0 +1,217 @@
+/* Copyright (c) 2013-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "buffer.h"
+#include "hex-dec.h"
+#include "unichar.h"
+#include "istream-private.h"
+#include "istream-jsonstr.h"
+
+#define MAX_UTF8_LEN 6
+
+struct jsonstr_istream {
+ struct istream_private istream;
+
+ /* The end '"' was found */
+ bool str_end:1;
+};
+
+static int
+i_stream_jsonstr_read_parent(struct jsonstr_istream *jstream,
+ unsigned int min_bytes)
+{
+ struct istream_private *stream = &jstream->istream;
+ size_t size, avail;
+ ssize_t ret;
+
+ size = i_stream_get_data_size(stream->parent);
+ while (size < min_bytes) {
+ ret = i_stream_read_memarea(stream->parent);
+ if (ret <= 0) {
+ if (ret == -2) {
+ /* tiny parent buffer size - shouldn't happen */
+ return -2;
+ }
+ stream->istream.stream_errno =
+ stream->parent->stream_errno;
+ stream->istream.eof = stream->parent->eof;
+ if (ret == -1 && stream->istream.stream_errno == 0) {
+ io_stream_set_error(&stream->iostream,
+ "EOF before trailing <\"> was seen");
+ stream->istream.stream_errno = EPIPE;
+ }
+ return ret;
+ }
+ size = i_stream_get_data_size(stream->parent);
+ }
+
+ if (!i_stream_try_alloc(stream, size, &avail))
+ return -2;
+ return 1;
+}
+
+static int
+i_stream_json_unescape(const unsigned char *src, size_t len,
+ unsigned char *dest,
+ unsigned int *src_size_r, unsigned int *dest_size_r)
+{
+ switch (*src) {
+ case '"':
+ case '\\':
+ case '/':
+ *dest = *src;
+ break;
+ case 'b':
+ *dest = '\b';
+ break;
+ case 'f':
+ *dest = '\f';
+ break;
+ case 'n':
+ *dest = '\n';
+ break;
+ case 'r':
+ *dest = '\r';
+ break;
+ case 't':
+ *dest = '\t';
+ break;
+ case 'u': {
+ char chbuf[5] = {0};
+ unichar_t chr,chr2 = 0;
+ buffer_t buf;
+ if (len < 5)
+ return 5;
+ buffer_create_from_data(&buf, dest, MAX_UTF8_LEN);
+ memcpy(chbuf, src+1, 4);
+ if (str_to_uint32_hex(chbuf, &chr)<0)
+ return -1;
+ if (UTF16_VALID_LOW_SURROGATE(chr))
+ return -1;
+ /* if we encounter surrogate, we need another \\uxxxx */
+ if (UTF16_VALID_HIGH_SURROGATE(chr)) {
+ if (len < 5+2+4)
+ return 5+2+4;
+ if (src[5] != '\\' && src[6] != 'u')
+ return -1;
+ memcpy(chbuf, src+7, 4);
+ if (str_to_uint32_hex(chbuf, &chr2)<0)
+ return -1;
+ if (!UTF16_VALID_LOW_SURROGATE(chr2))
+ return -1;
+ chr = uni_join_surrogate(chr, chr2);
+ }
+ if (!uni_is_valid_ucs4(chr))
+ return -1;
+ uni_ucs4_to_utf8_c(chr, &buf);
+ *src_size_r = 5 + (chr2>0?6:0);
+ *dest_size_r = buf.used;
+ return 0;
+ }
+ default:
+ return -1;
+ }
+ *src_size_r = 1;
+ *dest_size_r = 1;
+ return 0;
+}
+
+static ssize_t i_stream_jsonstr_read(struct istream_private *stream)
+{
+ struct jsonstr_istream *jstream =
+ container_of(stream, struct jsonstr_istream, istream);
+ const unsigned char *data;
+ unsigned int srcskip, destskip, extra;
+ size_t i, dest, size;
+ ssize_t ret, ret2;
+
+ if (jstream->str_end) {
+ stream->istream.eof = TRUE;
+ return -1;
+ }
+
+ ret = i_stream_jsonstr_read_parent(jstream, 1);
+ if (ret <= 0)
+ return ret;
+
+ /* @UNSAFE */
+ dest = stream->pos;
+ extra = 0;
+
+ data = i_stream_get_data(stream->parent, &size);
+ for (i = 0; i < size && dest < stream->buffer_size; ) {
+ if (data[i] == '"') {
+ jstream->str_end = TRUE;
+ if (dest == stream->pos) {
+ stream->istream.eof = TRUE;
+ return -1;
+ }
+ break;
+ } else if (data[i] == '\\') {
+ if (i+1 == size) {
+ /* not enough input for \x */
+ extra = 1;
+ break;
+ }
+ if (data[i+1] == 'u' && stream->buffer_size - dest < MAX_UTF8_LEN) {
+ /* UTF8 output is max. 6 chars */
+ if (dest == stream->pos)
+ return -2;
+ break;
+ }
+ i++;
+ if ((ret2 = i_stream_json_unescape(data + i, size - i,
+ stream->w_buffer + dest,
+ &srcskip, &destskip)) < 0) {
+ /* invalid string */
+ io_stream_set_error(&stream->iostream,
+ "Invalid JSON string");
+ stream->istream.stream_errno = EINVAL;
+ return -1;
+ } else if (ret2 > 0) {
+ /* we need to get more bytes, do not consume
+ escape slash */
+ i--;
+ extra = ret2;
+ break;
+ }
+ i += srcskip;
+ i_assert(i <= size);
+ dest += destskip;
+ i_assert(dest <= stream->buffer_size);
+ } else {
+ stream->w_buffer[dest++] = data[i];
+ i++;
+ }
+ }
+ i_stream_skip(stream->parent, i);
+
+ ret = dest - stream->pos;
+ if (ret == 0) {
+ /* not enough input */
+ i_assert(i == 0);
+ i_assert(extra > 0);
+ ret = i_stream_jsonstr_read_parent(jstream, extra+1);
+ if (ret <= 0)
+ return ret;
+ return i_stream_jsonstr_read(stream);
+ }
+ i_assert(ret > 0);
+ stream->pos = dest;
+ return ret;
+}
+
+struct istream *i_stream_create_jsonstr(struct istream *input)
+{
+ struct jsonstr_istream *dstream;
+
+ dstream = i_new(struct jsonstr_istream, 1);
+ dstream->istream.max_buffer_size = input->real_stream->max_buffer_size;
+ dstream->istream.read = i_stream_jsonstr_read;
+
+ dstream->istream.istream.readable_fd = FALSE;
+ dstream->istream.istream.blocking = input->blocking;
+ dstream->istream.istream.seekable = FALSE;
+ return i_stream_create(&dstream->istream, input,
+ i_stream_get_fd(input), 0);
+}
diff --git a/src/lib/istream-jsonstr.h b/src/lib/istream-jsonstr.h
new file mode 100644
index 0000000..255eccd
--- /dev/null
+++ b/src/lib/istream-jsonstr.h
@@ -0,0 +1,7 @@
+#ifndef ISTREAM_JSONSTR_H
+#define ISTREAM_JSONSTR_H
+
+/* Parse input until '"' is reached. Unescape JSON \x codes. */
+struct istream *i_stream_create_jsonstr(struct istream *input);
+
+#endif
diff --git a/src/lib/istream-limit.c b/src/lib/istream-limit.c
new file mode 100644
index 0000000..f66cef3
--- /dev/null
+++ b/src/lib/istream-limit.c
@@ -0,0 +1,144 @@
+/* Copyright (c) 2003-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "istream-private.h"
+
+struct limit_istream {
+ struct istream_private istream;
+
+ uoff_t v_size;
+};
+
+static void i_stream_limit_destroy(struct iostream_private *stream)
+{
+ struct limit_istream *lstream =
+ container_of(stream, struct limit_istream, istream.iostream);
+ uoff_t v_offset;
+
+ v_offset = lstream->istream.parent_start_offset +
+ lstream->istream.istream.v_offset;
+ if (lstream->istream.parent->seekable ||
+ v_offset > lstream->istream.parent->v_offset) {
+ /* get to same position in parent stream */
+ i_stream_seek(lstream->istream.parent, v_offset);
+ }
+}
+
+static ssize_t i_stream_limit_read(struct istream_private *stream)
+{
+ struct limit_istream *lstream =
+ container_of(stream, struct limit_istream, istream);
+ uoff_t left;
+ ssize_t ret;
+ size_t pos;
+
+ i_stream_seek(stream->parent, lstream->istream.parent_start_offset +
+ stream->istream.v_offset);
+
+ if (stream->istream.v_offset +
+ (stream->pos - stream->skip) >= lstream->v_size) {
+ stream->istream.eof = TRUE;
+ return -1;
+ }
+
+ stream->pos -= stream->skip;
+ stream->skip = 0;
+
+ stream->buffer = i_stream_get_data(stream->parent, &pos);
+ if (pos > stream->pos)
+ ret = 0;
+ else do {
+ ret = i_stream_read_memarea(stream->parent);
+ stream->istream.stream_errno = stream->parent->stream_errno;
+ stream->istream.eof = stream->parent->eof;
+ stream->buffer = i_stream_get_data(stream->parent, &pos);
+ } while (pos <= stream->pos && ret > 0);
+ if (ret == -2)
+ return -2;
+
+ if (lstream->v_size != UOFF_T_MAX) {
+ left = lstream->v_size - stream->istream.v_offset;
+ if (pos >= left) {
+ pos = left;
+ stream->istream.eof = TRUE;
+ }
+ }
+
+ ret = pos > stream->pos ? (ssize_t)(pos - stream->pos) :
+ (ret == 0 ? 0 : -1);
+ stream->pos = pos;
+ i_assert(ret != -1 || stream->istream.eof ||
+ stream->istream.stream_errno != 0);
+ return ret;
+}
+
+static int
+i_stream_limit_stat(struct istream_private *stream, bool exact)
+{
+ struct limit_istream *lstream =
+ container_of(stream, struct limit_istream, istream);
+ const struct stat *st;
+
+ if (i_stream_stat(stream->parent, exact, &st) < 0) {
+ stream->istream.stream_errno = stream->parent->stream_errno;
+ return -1;
+ }
+
+ stream->statbuf = *st;
+ if (lstream->v_size != UOFF_T_MAX)
+ stream->statbuf.st_size = lstream->v_size;
+ return 0;
+}
+
+static int i_stream_limit_get_size(struct istream_private *stream,
+ bool exact, uoff_t *size_r)
+{
+ struct limit_istream *lstream =
+ container_of(stream, struct limit_istream, istream);
+ const struct stat *st;
+
+ if (lstream->v_size != UOFF_T_MAX) {
+ *size_r = lstream->v_size;
+ return 1;
+ }
+
+ if (i_stream_stat(&stream->istream, exact, &st) < 0)
+ return -1;
+ if (st->st_size == -1)
+ return 0;
+
+ *size_r = st->st_size;
+ return 1;
+}
+
+struct istream *i_stream_create_limit(struct istream *input, uoff_t v_size)
+{
+ struct limit_istream *lstream;
+
+ lstream = i_new(struct limit_istream, 1);
+ lstream->v_size = v_size;
+ lstream->istream.max_buffer_size = input->real_stream->max_buffer_size;
+
+ lstream->istream.iostream.destroy = i_stream_limit_destroy;
+ lstream->istream.read = i_stream_limit_read;
+ lstream->istream.stat = i_stream_limit_stat;
+ lstream->istream.get_size = i_stream_limit_get_size;
+
+ lstream->istream.istream.readable_fd = input->readable_fd;
+ lstream->istream.istream.blocking = input->blocking;
+ lstream->istream.istream.seekable = input->seekable;
+ return i_stream_create(&lstream->istream, input,
+ i_stream_get_fd(input), 0);
+}
+
+struct istream *i_stream_create_range(struct istream *input,
+ uoff_t v_offset, uoff_t v_size)
+{
+ uoff_t orig_offset = input->v_offset;
+ struct istream *ret;
+
+ input->v_offset = v_offset;
+ ret = i_stream_create_limit(input, v_size);
+ input->v_offset = orig_offset;
+ return ret;
+}
diff --git a/src/lib/istream-multiplex.c b/src/lib/istream-multiplex.c
new file mode 100644
index 0000000..c70d0cc
--- /dev/null
+++ b/src/lib/istream-multiplex.c
@@ -0,0 +1,298 @@
+/* Copyright (c) 2017-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "ioloop.h"
+#include "array.h"
+#include "istream-private.h"
+#include "istream-multiplex.h"
+
+/* all multiplex packets are [1 byte cid][4 byte length][data] */
+
+struct multiplex_istream;
+
+struct multiplex_ichannel {
+ struct istream_private istream;
+ struct multiplex_istream *mstream;
+ uint8_t cid;
+ size_t pending_pos;
+ bool closed:1;
+};
+
+struct multiplex_istream {
+ struct istream *parent;
+
+ /* channel 0 is main channel */
+ uint8_t cur_channel;
+ unsigned int remain;
+ size_t bufsize;
+ ARRAY(struct multiplex_ichannel *) channels;
+
+ bool blocking:1;
+};
+
+static ssize_t i_stream_multiplex_ichannel_read(struct istream_private *stream);
+
+static struct multiplex_ichannel *
+get_channel(struct multiplex_istream *mstream, uint8_t cid)
+{
+ struct multiplex_ichannel *channel;
+ i_assert(mstream != NULL);
+ array_foreach_elem(&mstream->channels, channel) {
+ if (channel != NULL && channel->cid == cid)
+ return channel;
+ }
+ return NULL;
+}
+
+static void propagate_error(struct multiplex_istream *mstream, int stream_errno)
+{
+ struct multiplex_ichannel *channel;
+ array_foreach_elem(&mstream->channels, channel)
+ if (channel != NULL)
+ channel->istream.istream.stream_errno = stream_errno;
+}
+
+static void propagate_eof(struct multiplex_istream *mstream)
+{
+ struct multiplex_ichannel *channel;
+ array_foreach_elem(&mstream->channels, channel) {
+ if (channel == NULL)
+ continue;
+
+ channel->istream.istream.eof = TRUE;
+ if (mstream->remain > 0) {
+ channel->istream.istream.stream_errno = EPIPE;
+ io_stream_set_error(&channel->istream.iostream,
+ "Unexpected EOF - %u bytes remaining in packet",
+ mstream->remain);
+ }
+ }
+}
+
+static ssize_t
+i_stream_multiplex_read(struct multiplex_istream *mstream,
+ struct multiplex_ichannel *req_channel)
+{
+ const unsigned char *data;
+ size_t len = 0, used, wanted, avail;
+ ssize_t ret, got = 0;
+
+ if (mstream->parent == NULL) {
+ req_channel->istream.istream.eof = TRUE;
+ return -1;
+ }
+
+ (void)i_stream_get_data(mstream->parent, &len);
+
+ if (len == 0 && mstream->parent->closed) {
+ req_channel->istream.istream.eof = TRUE;
+ return -1;
+ }
+
+ if (((mstream->remain > 0 && len == 0) ||
+ (mstream->remain == 0 && len < 5)) &&
+ (ret = i_stream_read_memarea(mstream->parent)) <= 0) {
+ propagate_error(mstream, mstream->parent->stream_errno);
+ if (mstream->parent->eof)
+ propagate_eof(mstream);
+ return ret;
+ }
+
+ for(;;) {
+ data = i_stream_get_data(mstream->parent, &len);
+ if (len == 0) {
+ if (got == 0 && mstream->blocking) {
+ /* can't return 0 with blocking istreams,
+ so try again from the beginning. */
+ return i_stream_multiplex_read(mstream, req_channel);
+ }
+ break;
+ }
+
+ if (mstream->remain > 0) {
+ struct multiplex_ichannel *channel =
+ get_channel(mstream, mstream->cur_channel);
+ wanted = I_MIN(len, mstream->remain);
+ /* is it open? */
+ if (channel != NULL && !channel->closed) {
+ struct istream_private *stream = &channel->istream;
+ stream->pos += channel->pending_pos;
+ bool alloc_ret = i_stream_try_alloc(stream, wanted, &avail);
+ stream->pos -= channel->pending_pos;
+ if (!alloc_ret) {
+ i_stream_set_input_pending(&stream->istream, TRUE);
+ if (channel->cid != req_channel->cid)
+ return 0;
+ if (got > 0)
+ break;
+ return -2;
+ }
+
+ used = I_MIN(wanted, avail);
+
+ /* dump into buffer */
+ if (channel->cid != req_channel->cid) {
+ i_assert(stream->pos + channel->pending_pos + used <= stream->buffer_size);
+ memcpy(stream->w_buffer + stream->pos + channel->pending_pos,
+ data, used);
+ channel->pending_pos += used;
+ i_stream_set_input_pending(&stream->istream, TRUE);
+ } else {
+ i_assert(stream->pos + used <= stream->buffer_size);
+ memcpy(stream->w_buffer + stream->pos, data, used);
+ stream->pos += used;
+ got += used;
+ }
+ } else {
+ used = wanted;
+ }
+ mstream->remain -= used;
+ i_stream_skip(mstream->parent, used);
+ /* see if there is more to read */
+ continue;
+ }
+ if (mstream->remain == 0) {
+ /* need more data */
+ if (len < 5) {
+ ret = i_stream_multiplex_ichannel_read(&req_channel->istream);
+ if (ret > 0)
+ got += ret;
+ break;
+ }
+ /* channel ID */
+ mstream->cur_channel = data[0];
+ /* data length */
+ mstream->remain = be32_to_cpu_unaligned(data+1);
+ i_stream_skip(mstream->parent, 5);
+ }
+ }
+
+ propagate_error(mstream, mstream->parent->stream_errno);
+ if (mstream->parent->eof)
+ propagate_eof(mstream);
+
+ return got;
+}
+
+static ssize_t i_stream_multiplex_ichannel_read(struct istream_private *stream)
+{
+ struct multiplex_ichannel *channel =
+ container_of(stream, struct multiplex_ichannel, istream);
+ /* if previous multiplex read dumped data for us
+ actually serve it here. */
+ if (channel->pending_pos > 0) {
+ ssize_t ret = channel->pending_pos;
+ stream->pos += channel->pending_pos;
+ channel->pending_pos = 0;
+ return ret;
+ }
+ return i_stream_multiplex_read(channel->mstream, channel);
+}
+
+static void
+i_stream_multiplex_ichannel_switch_ioloop_to(struct istream_private *stream,
+ struct ioloop *ioloop)
+{
+ struct multiplex_ichannel *channel =
+ container_of(stream, struct multiplex_ichannel, istream);
+
+ i_stream_switch_ioloop_to(channel->mstream->parent, ioloop);
+}
+
+static void
+i_stream_multiplex_ichannel_close(struct iostream_private *stream, bool close_parent)
+{
+ struct multiplex_ichannel *arr_channel;
+ struct multiplex_ichannel *channel =
+ container_of(stream, struct multiplex_ichannel,
+ istream.iostream);
+ channel->closed = TRUE;
+ if (close_parent) {
+ array_foreach_elem(&channel->mstream->channels, arr_channel)
+ if (arr_channel != NULL && !arr_channel->closed)
+ return;
+ i_stream_close(channel->mstream->parent);
+ }
+}
+
+static void i_stream_multiplex_try_destroy(struct multiplex_istream *mstream)
+{
+ struct multiplex_ichannel *channel;
+ /* can't do anything until they are all closed */
+ array_foreach_elem(&mstream->channels, channel)
+ if (channel != NULL)
+ return;
+ i_stream_unref(&mstream->parent);
+ array_free(&mstream->channels);
+ i_free(mstream);
+}
+
+static void i_stream_multiplex_ichannel_destroy(struct iostream_private *stream)
+{
+ struct multiplex_ichannel **channelp;
+ struct multiplex_ichannel *channel =
+ container_of(stream, struct multiplex_ichannel,
+ istream.iostream);
+ i_stream_multiplex_ichannel_close(stream, TRUE);
+ i_stream_free_buffer(&channel->istream);
+ array_foreach_modifiable(&channel->mstream->channels, channelp) {
+ if (*channelp == channel) {
+ *channelp = NULL;
+ break;
+ }
+ }
+ i_stream_multiplex_try_destroy(channel->mstream);
+}
+
+static struct istream *
+i_stream_add_channel_real(struct multiplex_istream *mstream, uint8_t cid)
+{
+ struct multiplex_ichannel *channel = i_new(struct multiplex_ichannel, 1);
+ channel->cid = cid;
+ channel->mstream = mstream;
+ channel->istream.read = i_stream_multiplex_ichannel_read;
+ channel->istream.switch_ioloop_to = i_stream_multiplex_ichannel_switch_ioloop_to;
+ channel->istream.iostream.close = i_stream_multiplex_ichannel_close;
+ channel->istream.iostream.destroy = i_stream_multiplex_ichannel_destroy;
+ channel->istream.max_buffer_size = mstream->bufsize;
+ channel->istream.istream.blocking = mstream->blocking;
+ if (cid == 0)
+ channel->istream.fd = i_stream_get_fd(mstream->parent);
+ else
+ channel->istream.fd = -1;
+ array_push_back(&channel->mstream->channels, &channel);
+
+ return i_stream_create(&channel->istream, NULL, channel->istream.fd, 0);
+}
+
+struct istream *i_stream_multiplex_add_channel(struct istream *stream, uint8_t cid)
+{
+ struct multiplex_ichannel *chan =
+ container_of(stream->real_stream,
+ struct multiplex_ichannel, istream);
+ i_assert(get_channel(chan->mstream, cid) == NULL);
+
+ return i_stream_add_channel_real(chan->mstream, cid);
+}
+
+struct istream *i_stream_create_multiplex(struct istream *parent, size_t bufsize)
+{
+ struct multiplex_istream *mstream;
+
+ mstream = i_new(struct multiplex_istream, 1);
+ mstream->parent = parent;
+ mstream->bufsize = bufsize;
+ mstream->blocking = parent->blocking;
+ i_array_init(&mstream->channels, 8);
+ i_stream_ref(parent);
+
+ return i_stream_add_channel_real(mstream, 0);
+}
+
+uint8_t i_stream_multiplex_get_channel_id(struct istream *stream)
+{
+ struct multiplex_ichannel *channel =
+ container_of(stream->real_stream,
+ struct multiplex_ichannel, istream);
+ return channel->cid;
+}
diff --git a/src/lib/istream-multiplex.h b/src/lib/istream-multiplex.h
new file mode 100644
index 0000000..3f289f4
--- /dev/null
+++ b/src/lib/istream-multiplex.h
@@ -0,0 +1,8 @@
+#ifndef ISTREAM_MULTIPLEX
+#define ISTREAM_MULTIPLEX 1
+
+struct istream *i_stream_create_multiplex(struct istream *parent, size_t bufsize);
+struct istream *i_stream_multiplex_add_channel(struct istream *stream, uint8_t cid);
+uint8_t i_stream_multiplex_get_channel_id(struct istream *stream);
+
+#endif
diff --git a/src/lib/istream-private.h b/src/lib/istream-private.h
new file mode 100644
index 0000000..b612b9c
--- /dev/null
+++ b/src/lib/istream-private.h
@@ -0,0 +1,140 @@
+#ifndef ISTREAM_PRIVATE_H
+#define ISTREAM_PRIVATE_H
+
+#include "istream.h"
+#include "iostream-private.h"
+
+#define I_STREAM_MIN_SIZE IO_BLOCK_SIZE
+
+struct io;
+
+struct istream_private {
+/* inheritance: */
+ struct iostream_private iostream;
+
+/* methods: */
+ ssize_t (*read)(struct istream_private *stream);
+ void (*seek)(struct istream_private *stream,
+ uoff_t v_offset, bool mark);
+ void (*sync)(struct istream_private *stream);
+ int (*stat)(struct istream_private *stream, bool exact);
+ int (*get_size)(struct istream_private *stream, bool exact, uoff_t *size_r);
+ void (*switch_ioloop_to)(struct istream_private *stream,
+ struct ioloop *ioloop);
+ struct istream_snapshot *
+ (*snapshot)(struct istream_private *stream,
+ struct istream_snapshot *prev_snapshot);
+
+/* data: */
+ struct istream istream;
+
+ int fd;
+ uoff_t start_offset;
+ struct stat statbuf;
+ /* added by io_add_istream() -> i_stream_set_io() */
+ struct io *io;
+
+ const unsigned char *buffer;
+ unsigned char *w_buffer; /* may be NULL */
+
+ size_t buffer_size, max_buffer_size, init_buffer_size, data_limit;
+ size_t skip, pos;
+ /* If seeking backwards within the buffer, the next read() will
+ return again pos..high_pos */
+ size_t high_pos;
+
+ struct istream *parent; /* for filter streams */
+ uoff_t parent_start_offset;
+ /* Initially UOFF_T_MAX. Otherwise it's the exact known stream size,
+ which can be used by stat() / get_size(). */
+ uoff_t cached_stream_size;
+
+ /* parent stream's expected offset is kept here. i_stream_read()
+ always seeks parent stream to here before calling read(). */
+ uoff_t parent_expected_offset;
+
+ struct memarea *memarea;
+ struct istream_snapshot *prev_snapshot;
+ /* increased every time the stream is changed (e.g. seek, read).
+ this way streams can check if their parent streams have been
+ accessed behind them. */
+ unsigned int access_counter;
+ /* Timestamp when read() last returned >0 */
+ struct timeval last_read_timeval;
+
+ string_t *line_str; /* for i_stream_next_line() if w_buffer == NULL */
+ bool line_crlf:1;
+ bool return_nolf_line:1;
+ bool stream_size_passthrough:1; /* stream is parent's size */
+ bool nonpersistent_buffers:1;
+ bool io_pending:1;
+};
+
+struct istream_snapshot {
+ struct istream_snapshot *prev_snapshot;
+ struct memarea *old_memarea;
+ struct istream *istream;
+ void (*free)(struct istream_snapshot *snapshot);
+};
+
+enum istream_create_flag {
+ /* The stream guarantees that the buffer pointer stays valid when it
+ returns <= 0. */
+ ISTREAM_CREATE_FLAG_NOOP_SNAPSHOT = 0x01,
+};
+
+struct istream * ATTR_NOWARN_UNUSED_RESULT
+i_stream_create(struct istream_private *stream, struct istream *parent, int fd,
+ enum istream_create_flag flags) ATTR_NULL(2);
+/* Initialize parent lazily after i_stream_create() has already been called. */
+void i_stream_init_parent(struct istream_private *_stream,
+ struct istream *parent);
+
+void i_stream_compress(struct istream_private *stream);
+void i_stream_grow_buffer(struct istream_private *stream, size_t bytes);
+bool ATTR_NOWARN_UNUSED_RESULT
+i_stream_try_alloc(struct istream_private *stream,
+ size_t wanted_size, size_t *size_r);
+/* Like i_stream_try_alloc(), but compress only if it's the only way to get
+ more space. This can be useful when stream is marked with
+ i_stream_seek_mark() */
+bool ATTR_NOWARN_UNUSED_RESULT
+i_stream_try_alloc_avoid_compress(struct istream_private *stream,
+ size_t wanted_size, size_t *size_r);
+void *i_stream_alloc(struct istream_private *stream, size_t size);
+/* Detach istream from its current memarea. This unreferences the memarea and
+ resets the w_buffer to empty. This can be used to make sure i_stream_*alloc()
+ won't return a pointer to memory referenced to in a snapshot. */
+void i_stream_memarea_detach(struct istream_private *stream);
+/* Free memory allocated by i_stream_*alloc() */
+void i_stream_free_buffer(struct istream_private *stream);
+ssize_t i_stream_read_copy_from_parent(struct istream *istream);
+void i_stream_default_seek_nonseekable(struct istream_private *stream,
+ uoff_t v_offset, bool mark);
+/* Returns FALSE if seeking must be done by starting from the beginning.
+ The caller is then expected to reset the stream and call this function
+ again, which should work then. If TRUE is returned, the seek was either
+ successfully done or stream_errno is set. */
+bool i_stream_nonseekable_try_seek(struct istream_private *stream,
+ uoff_t v_offset);
+
+/* Default snapshot handling: use memarea if it exists, otherwise snapshot
+ parent stream. */
+struct istream_snapshot *
+i_stream_default_snapshot(struct istream_private *stream,
+ struct istream_snapshot *prev_snapshot);
+void i_stream_snapshot_free(struct istream_snapshot **snapshot);
+
+struct istream *i_stream_get_root_io(struct istream *stream);
+void i_stream_set_io(struct istream *stream, struct io *io);
+void i_stream_unset_io(struct istream *stream, struct io *io);
+
+/* Filter istreams should be calling this instead of i_stream_read() to avoid
+ unnecessarily referencing memareas. After this call any pointers to the
+ parent istream's content must be considered as potentially invalid and have
+ to be updated, even if the return value is <=0. */
+ssize_t i_stream_read_memarea(struct istream *stream);
+int i_stream_read_more_memarea(struct istream *stream,
+ const unsigned char **data_r, size_t *size_r);
+
+#endif
diff --git a/src/lib/istream-rawlog.c b/src/lib/istream-rawlog.c
new file mode 100644
index 0000000..2a44429
--- /dev/null
+++ b/src/lib/istream-rawlog.c
@@ -0,0 +1,119 @@
+/* Copyright (c) 2011-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "ostream.h"
+#include "iostream-rawlog-private.h"
+#include "istream-private.h"
+#include "istream-rawlog.h"
+
+struct rawlog_istream {
+ struct istream_private istream;
+ struct rawlog_iostream riostream;
+};
+
+static void i_stream_rawlog_close(struct iostream_private *stream,
+ bool close_parent)
+{
+ struct rawlog_istream *rstream =
+ container_of(stream, struct rawlog_istream, istream.iostream);
+
+ iostream_rawlog_close(&rstream->riostream);
+ if (close_parent)
+ i_stream_close(rstream->istream.parent);
+}
+
+static void i_stream_rawlog_destroy(struct iostream_private *stream)
+{
+ struct rawlog_istream *rstream =
+ container_of(stream, struct rawlog_istream, istream.iostream);
+ uoff_t v_offset;
+
+ v_offset = rstream->istream.parent_start_offset +
+ rstream->istream.istream.v_offset;
+ if (rstream->istream.parent->seekable ||
+ v_offset > rstream->istream.parent->v_offset) {
+ /* get to same position in parent stream */
+ i_stream_seek(rstream->istream.parent, v_offset);
+ }
+}
+
+static ssize_t i_stream_rawlog_read(struct istream_private *stream)
+{
+ struct rawlog_istream *rstream =
+ container_of(stream, struct rawlog_istream, istream);
+ ssize_t ret;
+ size_t pos;
+
+ i_stream_seek(stream->parent, rstream->istream.parent_start_offset +
+ stream->istream.v_offset);
+
+ stream->pos -= stream->skip;
+ stream->skip = 0;
+
+ stream->buffer = i_stream_get_data(stream->parent, &pos);
+ if (pos > stream->pos)
+ ret = 0;
+ else do {
+ ret = i_stream_read_memarea(stream->parent);
+ stream->istream.stream_errno = stream->parent->stream_errno;
+ stream->istream.eof = stream->parent->eof;
+ stream->buffer = i_stream_get_data(stream->parent, &pos);
+ } while (pos <= stream->pos && ret > 0);
+ if (ret == -2)
+ return -2;
+
+ if (pos <= stream->pos)
+ ret = ret == 0 ? 0 : -1;
+ else {
+ ret = (ssize_t)(pos - stream->pos);
+ iostream_rawlog_write(&rstream->riostream,
+ stream->buffer + stream->pos, ret);
+ }
+ stream->pos = pos;
+ i_assert(ret != -1 || stream->istream.eof ||
+ stream->istream.stream_errno != 0);
+ return ret;
+}
+
+struct istream *
+i_stream_create_rawlog(struct istream *input, const char *rawlog_path,
+ int rawlog_fd, enum iostream_rawlog_flags flags)
+{
+ struct ostream *rawlog_output;
+ bool autoclose_fd = (flags & IOSTREAM_RAWLOG_FLAG_AUTOCLOSE) != 0;
+
+ i_assert(rawlog_path != NULL);
+ i_assert(rawlog_fd != -1);
+
+ rawlog_output = autoclose_fd ?
+ o_stream_create_fd_autoclose(&rawlog_fd, 0) :
+ o_stream_create_fd(rawlog_fd, 0);
+ o_stream_set_name(rawlog_output,
+ t_strdup_printf("rawlog(%s)", rawlog_path));
+ return i_stream_create_rawlog_from_stream(input, rawlog_output, flags);
+}
+
+struct istream *
+i_stream_create_rawlog_from_stream(struct istream *input,
+ struct ostream *rawlog_output,
+ enum iostream_rawlog_flags flags)
+{
+ struct rawlog_istream *rstream;
+
+ rstream = i_new(struct rawlog_istream, 1);
+ rstream->istream.max_buffer_size = input->real_stream->max_buffer_size;
+ rstream->istream.stream_size_passthrough = TRUE;
+
+ rstream->riostream.rawlog_output = rawlog_output;
+ iostream_rawlog_init(&rstream->riostream, flags, TRUE);
+
+ rstream->istream.read = i_stream_rawlog_read;
+ rstream->istream.iostream.close = i_stream_rawlog_close;
+ rstream->istream.iostream.destroy = i_stream_rawlog_destroy;
+
+ rstream->istream.istream.readable_fd = input->readable_fd;
+ rstream->istream.istream.blocking = input->blocking;
+ rstream->istream.istream.seekable = input->seekable;
+ return i_stream_create(&rstream->istream, input,
+ i_stream_get_fd(input), 0);
+}
diff --git a/src/lib/istream-rawlog.h b/src/lib/istream-rawlog.h
new file mode 100644
index 0000000..4126f23
--- /dev/null
+++ b/src/lib/istream-rawlog.h
@@ -0,0 +1,14 @@
+#ifndef ISTREAM_RAWLOG_H
+#define ISTREAM_RAWLOG_H
+
+#include "iostream-rawlog.h"
+
+struct istream *
+i_stream_create_rawlog(struct istream *input, const char *rawlog_path,
+ int rawlog_fd, enum iostream_rawlog_flags flags);
+struct istream *
+i_stream_create_rawlog_from_stream(struct istream *input,
+ struct ostream *rawlog_output,
+ enum iostream_rawlog_flags flags);
+
+#endif
diff --git a/src/lib/istream-seekable.c b/src/lib/istream-seekable.c
new file mode 100644
index 0000000..5bc19ad
--- /dev/null
+++ b/src/lib/istream-seekable.c
@@ -0,0 +1,558 @@
+/* Copyright (c) 2005-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "buffer.h"
+#include "str.h"
+#include "memarea.h"
+#include "read-full.h"
+#include "write-full.h"
+#include "safe-mkstemp.h"
+#include "istream-private.h"
+#include "istream-concat.h"
+#include "istream-seekable.h"
+
+#include <unistd.h>
+
+#define BUF_INITIAL_SIZE (1024*32)
+
+struct seekable_istream {
+ struct istream_private istream;
+
+ char *temp_path;
+ uoff_t write_peak;
+ uoff_t size;
+ size_t buffer_peak;
+
+ int (*fd_callback)(const char **path_r, void *context);
+ void *context;
+
+ struct istream **input, *cur_input;
+ struct istream *fd_input;
+ unsigned int cur_idx;
+ int fd;
+ bool free_context;
+};
+
+static void i_stream_seekable_close(struct iostream_private *stream,
+ bool close_parent ATTR_UNUSED)
+{
+ struct seekable_istream *sstream =
+ container_of(stream, struct seekable_istream, istream.iostream);
+
+ sstream->fd = -1;
+ i_stream_close(sstream->fd_input);
+}
+
+static void unref_streams(struct seekable_istream *sstream)
+{
+ unsigned int i;
+
+ for (i = 0; sstream->input[i] != NULL; i++)
+ i_stream_unref(&sstream->input[i]);
+}
+
+static void i_stream_seekable_destroy(struct iostream_private *stream)
+{
+ struct seekable_istream *sstream =
+ container_of(stream, struct seekable_istream, istream.iostream);
+
+ i_stream_free_buffer(&sstream->istream);
+ i_stream_unref(&sstream->fd_input);
+ unref_streams(sstream);
+
+ if (sstream->free_context)
+ i_free(sstream->context);
+ i_free(sstream->temp_path);
+ i_free(sstream->input);
+}
+
+static void
+i_stream_seekable_set_max_buffer_size(struct iostream_private *stream,
+ size_t max_size)
+{
+ struct seekable_istream *sstream =
+ container_of(stream, struct seekable_istream, istream.iostream);
+ unsigned int i;
+
+ sstream->istream.max_buffer_size = max_size;
+ if (sstream->fd_input != NULL)
+ i_stream_set_max_buffer_size(sstream->fd_input, max_size);
+ for (i = 0; sstream->input[i] != NULL; i++)
+ i_stream_set_max_buffer_size(sstream->input[i], max_size);
+}
+
+static int copy_to_temp_file(struct seekable_istream *sstream)
+{
+ struct istream_private *stream = &sstream->istream;
+ const char *path;
+ const unsigned char *buffer;
+ size_t size;
+ int fd;
+
+ fd = sstream->fd_callback(&path, sstream->context);
+ if (fd == -1)
+ return -1;
+
+ /* copy our currently read buffer to it */
+ i_assert(stream->pos <= sstream->buffer_peak);
+ if (write_full(fd, stream->buffer, sstream->buffer_peak) < 0) {
+ if (!ENOSPACE(errno))
+ i_error("istream-seekable: write_full(%s) failed: %m", path);
+ i_close_fd(&fd);
+ return -1;
+ }
+ sstream->temp_path = i_strdup(path);
+ sstream->write_peak = sstream->buffer_peak;
+
+ sstream->fd = fd;
+ sstream->fd_input = i_stream_create_fd_autoclose(&fd,
+ I_MAX(stream->pos, sstream->istream.max_buffer_size));
+ i_stream_set_name(sstream->fd_input, t_strdup_printf(
+ "(seekable temp-istream for: %s)", i_stream_get_name(&stream->istream)));
+
+ /* read back the data we just had in our buffer */
+ for (;;) {
+ buffer = i_stream_get_data(sstream->fd_input, &size);
+ if (size >= stream->pos)
+ break;
+
+ ssize_t ret;
+ if ((ret = i_stream_read_memarea(sstream->fd_input)) <= 0) {
+ i_assert(ret != 0);
+ i_assert(ret != -2);
+ i_error("istream-seekable: Couldn't read back "
+ "in-memory input %s: %s",
+ i_stream_get_name(&stream->istream),
+ i_stream_get_error(sstream->fd_input));
+ i_stream_destroy(&sstream->fd_input);
+ sstream->fd = -1; /* autoclosed by fd_input */
+ return -1;
+ }
+ }
+ /* Set the max buffer size only after we've already read everything
+ into memory. For example with istream-data it's possible that
+ more data exists in buffer than max_buffer_size. */
+ i_stream_set_max_buffer_size(sstream->fd_input,
+ sstream->istream.max_buffer_size);
+ stream->buffer = buffer;
+ i_stream_free_buffer(&sstream->istream);
+ return 0;
+}
+
+static ssize_t read_more(struct seekable_istream *sstream)
+{
+ size_t size;
+ ssize_t ret;
+
+ if (sstream->cur_input == NULL) {
+ sstream->istream.istream.eof = TRUE;
+ return -1;
+ }
+
+ while ((ret = i_stream_read_memarea(sstream->cur_input)) == -1) {
+ if (sstream->cur_input->stream_errno != 0) {
+ io_stream_set_error(&sstream->istream.iostream,
+ "read(%s) failed: %s",
+ i_stream_get_name(sstream->cur_input),
+ i_stream_get_error(sstream->cur_input));
+ sstream->istream.istream.eof = TRUE;
+ sstream->istream.istream.stream_errno =
+ sstream->cur_input->stream_errno;
+ return -1;
+ }
+
+ /* go to next stream */
+ sstream->cur_input = sstream->input[sstream->cur_idx++];
+ if (sstream->cur_input == NULL) {
+ /* last one, EOF */
+ sstream->size = sstream->istream.istream.v_offset +
+ (sstream->istream.pos - sstream->istream.skip);
+ sstream->istream.istream.eof = TRUE;
+ /* Now that EOF is reached, the stream can't return 0
+ anymore. Callers can now use this stream in places
+ that assert that blocking==TRUE. */
+ sstream->istream.istream.blocking = TRUE;
+ unref_streams(sstream);
+ return -1;
+ }
+
+ /* see if stream has pending data */
+ size = i_stream_get_data_size(sstream->cur_input);
+ if (size != 0)
+ return size;
+ }
+ return ret;
+}
+
+static bool read_from_buffer(struct seekable_istream *sstream, ssize_t *ret_r)
+{
+ struct istream_private *stream = &sstream->istream;
+ const unsigned char *data;
+ size_t size, avail_size;
+
+ if (stream->pos < sstream->buffer_peak) {
+ /* This could be the first read() or we could have already
+ seeked backwards. */
+ i_assert(stream->pos == 0 && stream->skip == 0);
+ stream->skip = stream->istream.v_offset;
+ stream->pos = sstream->buffer_peak;
+ size = stream->pos - stream->skip;
+ if (stream->istream.v_offset == sstream->buffer_peak) {
+ /* this could happen after write to temp file failed */
+ return read_from_buffer(sstream, ret_r);
+ }
+ } else {
+ /* need to read more */
+ i_assert(stream->pos == sstream->buffer_peak);
+ size = sstream->cur_input == NULL ? 0 :
+ i_stream_get_data_size(sstream->cur_input);
+ if (size == 0) {
+ /* read more to buffer */
+ *ret_r = read_more(sstream);
+ if (*ret_r == 0 || *ret_r == -1)
+ return TRUE;
+ }
+
+ /* we should have more now. */
+ data = i_stream_get_data(sstream->cur_input, &size);
+ i_assert(size > 0);
+
+ /* change skip to 0 temporarily so i_stream_try_alloc() won't try to
+ compress the buffer. */
+ size_t old_skip = stream->skip;
+ stream->skip = 0;
+ bool have_space = i_stream_try_alloc(stream, size, &avail_size);
+ stream->skip = old_skip;
+ if (!have_space)
+ return FALSE;
+
+ if (size > avail_size)
+ size = avail_size;
+ memcpy(stream->w_buffer + stream->pos, data, size);
+ stream->pos += size;
+ sstream->buffer_peak += size;
+ i_stream_skip(sstream->cur_input, size);
+ }
+
+ *ret_r = size;
+ i_assert(*ret_r > 0);
+ return TRUE;
+}
+
+static int i_stream_seekable_write_failed(struct seekable_istream *sstream)
+{
+ struct istream_private *stream = &sstream->istream;
+ void *data;
+ size_t old_pos = stream->pos;
+
+ i_assert(sstream->fd != -1);
+ i_assert(stream->skip == 0);
+
+ stream->max_buffer_size = SIZE_MAX;
+ stream->pos = 0;
+ data = i_stream_alloc(stream, sstream->write_peak);
+ stream->pos = old_pos;
+
+ if (pread_full(sstream->fd, data, sstream->write_peak, 0) < 0) {
+ sstream->istream.istream.stream_errno = errno;
+ sstream->istream.istream.eof = TRUE;
+ io_stream_set_error(&sstream->istream.iostream,
+ "istream-seekable: read(%s) failed: %m",
+ sstream->temp_path);
+ return -1;
+ }
+ i_stream_destroy(&sstream->fd_input);
+ sstream->fd = -1; /* autoclosed by fd_input */
+
+ i_free_and_null(sstream->temp_path);
+ return 0;
+}
+
+static ssize_t i_stream_seekable_read(struct istream_private *stream)
+{
+ struct seekable_istream *sstream =
+ container_of(stream, struct seekable_istream, istream);
+ const unsigned char *data;
+ size_t size, pos;
+ ssize_t ret;
+
+ if (sstream->fd == -1) {
+ if (read_from_buffer(sstream, &ret))
+ return ret;
+
+ /* copy everything to temp file and use it as the stream */
+ if (copy_to_temp_file(sstream) < 0) {
+ stream->max_buffer_size = SIZE_MAX;
+ if (!read_from_buffer(sstream, &ret))
+ i_unreached();
+ return ret;
+ }
+ i_assert(sstream->fd != -1);
+ }
+
+ stream->buffer = CONST_PTR_OFFSET(stream->buffer, stream->skip);
+ stream->pos -= stream->skip;
+ stream->skip = 0;
+
+ i_assert(stream->istream.v_offset + stream->pos <= sstream->write_peak);
+ if (stream->istream.v_offset + stream->pos == sstream->write_peak) {
+ /* need to read more */
+ if (sstream->cur_input == NULL ||
+ i_stream_get_data_size(sstream->cur_input) == 0) {
+ ret = read_more(sstream);
+ if (ret == -1 || ret == 0)
+ return ret;
+ }
+
+ /* save to our file */
+ data = i_stream_get_data(sstream->cur_input, &size);
+ ret = write(sstream->fd, data, size);
+ if (ret <= 0) {
+ if (ret < 0 && !ENOSPACE(errno)) {
+ i_error("istream-seekable: write_full(%s) failed: %m",
+ sstream->temp_path);
+ }
+ if (i_stream_seekable_write_failed(sstream) < 0)
+ return -1;
+ if (!read_from_buffer(sstream, &ret))
+ i_unreached();
+ return ret;
+ }
+ i_stream_sync(sstream->fd_input);
+ i_stream_skip(sstream->cur_input, ret);
+ sstream->write_peak += ret;
+ }
+
+ i_stream_seek(sstream->fd_input, stream->istream.v_offset);
+ ret = i_stream_read_memarea(sstream->fd_input);
+ if (ret <= 0) {
+ stream->istream.eof = sstream->fd_input->eof;
+ stream->istream.stream_errno =
+ sstream->fd_input->stream_errno;
+ } else {
+ ret = -2;
+ }
+
+ stream->buffer = i_stream_get_data(sstream->fd_input, &pos);
+ stream->pos -= stream->skip;
+ stream->skip = 0;
+
+ ret = pos > stream->pos ? (ssize_t)(pos - stream->pos) : ret;
+ stream->pos = pos;
+ return ret;
+}
+
+static int
+i_stream_seekable_stat(struct istream_private *stream, bool exact)
+{
+ struct seekable_istream *sstream =
+ container_of(stream, struct seekable_istream, istream);
+ const struct stat *st;
+ uoff_t old_offset, len;
+ ssize_t ret;
+
+ if (sstream->size != UOFF_T_MAX) {
+ /* we've already reached EOF and know the size */
+ stream->statbuf.st_size = sstream->size;
+ return 0;
+ }
+
+ /* we want to know the full size of the file, so read until
+ we're finished */
+ old_offset = stream->istream.v_offset;
+ do {
+ i_stream_skip(&stream->istream,
+ stream->pos - stream->skip);
+ } while ((ret = i_stream_seekable_read(stream)) > 0);
+
+ if (ret == 0) {
+ i_panic("i_stream_stat() used for non-blocking "
+ "seekable stream %s offset %"PRIuUOFF_T,
+ i_stream_get_name(sstream->cur_input),
+ sstream->cur_input->v_offset);
+ }
+ i_stream_skip(&stream->istream, stream->pos - stream->skip);
+ len = stream->pos;
+ i_stream_seek(&stream->istream, old_offset);
+ unref_streams(sstream);
+
+ if (stream->istream.stream_errno != 0)
+ return -1;
+
+ if (sstream->fd_input != NULL) {
+ /* using a file backed buffer, we can use real fstat() */
+ if (i_stream_stat(sstream->fd_input, exact, &st) < 0)
+ return -1;
+ stream->statbuf = *st;
+ } else {
+ /* buffer is completely in memory */
+ i_assert(sstream->fd == -1);
+
+ stream->statbuf.st_size = len;
+ }
+ return 0;
+}
+
+static void i_stream_seekable_seek(struct istream_private *stream,
+ uoff_t v_offset, bool mark)
+{
+ if (v_offset <= stream->istream.v_offset) {
+ /* seeking backwards */
+ stream->istream.v_offset = v_offset;
+ stream->skip = stream->pos = 0;
+ } else {
+ /* we can't skip over data we haven't yet read and written to
+ our buffer/temp file */
+ i_stream_default_seek_nonseekable(stream, v_offset, mark);
+ }
+}
+
+static struct istream_snapshot *
+i_stream_seekable_snapshot(struct istream_private *stream,
+ struct istream_snapshot *prev_snapshot)
+{
+ struct seekable_istream *sstream =
+ container_of(stream, struct seekable_istream, istream);
+
+ if (sstream->fd == -1) {
+ /* still in memory */
+ if (stream->memarea == NULL)
+ return prev_snapshot;
+ return i_stream_default_snapshot(stream, prev_snapshot);
+ } else {
+ /* using the fd_input stream */
+ return sstream->fd_input->real_stream->
+ snapshot(sstream->fd_input->real_stream, prev_snapshot);
+ }
+}
+
+struct istream *
+i_streams_merge(struct istream *input[], size_t max_buffer_size,
+ int (*fd_callback)(const char **path_r, void *context),
+ void *context) ATTR_NULL(4)
+{
+ struct seekable_istream *sstream;
+ const unsigned char *data;
+ unsigned int count;
+ size_t size;
+ bool blocking = TRUE;
+
+ i_assert(max_buffer_size > 0);
+
+ /* if any of the streams isn't blocking, set ourself also nonblocking */
+ for (count = 0; input[count] != NULL; count++) {
+ if (!input[count]->blocking)
+ blocking = FALSE;
+ i_stream_ref(input[count]);
+ }
+ i_assert(count != 0);
+
+ sstream = i_new(struct seekable_istream, 1);
+ sstream->fd_callback = fd_callback;
+ sstream->context = context;
+ sstream->istream.max_buffer_size = max_buffer_size;
+ sstream->fd = -1;
+ sstream->size = UOFF_T_MAX;
+
+ sstream->input = i_new(struct istream *, count + 1);
+ memcpy(sstream->input, input, sizeof(*input) * count);
+ sstream->cur_input = sstream->input[0];
+
+ sstream->istream.iostream.close = i_stream_seekable_close;
+ sstream->istream.iostream.destroy = i_stream_seekable_destroy;
+ sstream->istream.iostream.set_max_buffer_size =
+ i_stream_seekable_set_max_buffer_size;
+
+ sstream->istream.read = i_stream_seekable_read;
+ sstream->istream.stat = i_stream_seekable_stat;
+ sstream->istream.seek = i_stream_seekable_seek;
+ sstream->istream.snapshot = i_stream_seekable_snapshot;
+
+ sstream->istream.istream.readable_fd = FALSE;
+ sstream->istream.istream.blocking = blocking;
+ sstream->istream.istream.seekable = TRUE;
+ (void)i_stream_create(&sstream->istream, NULL, -1, 0);
+
+ /* initialize our buffer from first stream's pending data */
+ data = i_stream_get_data(sstream->cur_input, &size);
+ if (size > 0) {
+ memcpy(i_stream_alloc(&sstream->istream, size), data, size);
+ sstream->buffer_peak = size;
+ i_stream_skip(sstream->cur_input, size);
+ }
+ return &sstream->istream.istream;
+}
+
+static bool inputs_are_seekable(struct istream *input[])
+{
+ unsigned int count;
+
+ for (count = 0; input[count] != NULL; count++) {
+ if (!input[count]->seekable)
+ return FALSE;
+ }
+ return TRUE;
+}
+
+struct istream *
+i_stream_create_seekable(struct istream *input[],
+ size_t max_buffer_size,
+ int (*fd_callback)(const char **path_r, void *context),
+ void *context)
+{
+ i_assert(max_buffer_size > 0);
+
+ /* If all input streams are seekable, use concat istream instead */
+ if (inputs_are_seekable(input))
+ return i_stream_create_concat(input);
+
+ return i_streams_merge(input, max_buffer_size, fd_callback, context);
+}
+
+static int seekable_fd_callback(const char **path_r, void *context)
+{
+ char *temp_path_prefix = context;
+ string_t *path;
+ int fd;
+
+ path = t_str_new(128);
+ str_append(path, temp_path_prefix);
+ fd = safe_mkstemp(path, 0600, (uid_t)-1, (gid_t)-1);
+ if (fd == -1) {
+ i_error("istream-seekable: safe_mkstemp(%s) failed: %m", str_c(path));
+ return -1;
+ }
+
+ /* we just want the fd, unlink it */
+ if (i_unlink(str_c(path)) < 0) {
+ /* shouldn't happen.. */
+ i_close_fd(&fd);
+ return -1;
+ }
+
+ *path_r = str_c(path);
+ return fd;
+}
+
+struct istream *
+i_stream_create_seekable_path(struct istream *input[],
+ size_t max_buffer_size,
+ const char *temp_path_prefix)
+{
+ struct seekable_istream *sstream;
+ struct istream *stream;
+
+ i_assert(temp_path_prefix != NULL);
+ i_assert(max_buffer_size > 0);
+
+ if (inputs_are_seekable(input))
+ return i_stream_create_concat(input);
+
+ stream = i_stream_create_seekable(input, max_buffer_size,
+ seekable_fd_callback,
+ i_strdup(temp_path_prefix));
+ sstream = container_of(stream->real_stream,
+ struct seekable_istream, istream);
+ sstream->free_context = TRUE;
+ return stream;
+}
diff --git a/src/lib/istream-seekable.h b/src/lib/istream-seekable.h
new file mode 100644
index 0000000..4cee396
--- /dev/null
+++ b/src/lib/istream-seekable.h
@@ -0,0 +1,28 @@
+#ifndef ISTREAM_SEEKABLE_H
+#define ISTREAM_SEEKABLE_H
+
+/* Create a seekable stream from given NULL-terminated list of input streams.
+ Try to keep it in memory, but use a temporary file if it's too large.
+
+ When max_buffer_size is reached, fd_callback is called. It should return
+ the fd and path of the created file. Typically the callback would also
+ unlink the file before returning. */
+struct istream *
+i_streams_merge(struct istream *input[], size_t max_buffer_size,
+ int (*fd_callback)(const char **path_r, void *context),
+ void *context) ATTR_NULL(4);
+
+/* Same as i_streams_merge(), but if all of the inputs are seekable already,
+ create a concat stream instead. */
+struct istream *
+i_stream_create_seekable(struct istream *input[],
+ size_t max_buffer_size,
+ int (*fd_callback)(const char **path_r, void *context),
+ void *context) ATTR_NULL(4);
+
+struct istream *
+i_stream_create_seekable_path(struct istream *input[],
+ size_t max_buffer_size,
+ const char *temp_path_prefix);
+
+#endif
diff --git a/src/lib/istream-sized.c b/src/lib/istream-sized.c
new file mode 100644
index 0000000..afe95b7
--- /dev/null
+++ b/src/lib/istream-sized.c
@@ -0,0 +1,232 @@
+/* Copyright (c) 2003-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "istream-private.h"
+#include "istream-sized.h"
+
+struct sized_istream {
+ struct istream_private istream;
+
+ istream_sized_callback_t *error_callback;
+ void *error_context;
+
+ uoff_t size;
+ bool min_size_only;
+};
+
+static void i_stream_sized_destroy(struct iostream_private *stream)
+{
+ struct sized_istream *sstream =
+ container_of(stream, struct sized_istream, istream.iostream);
+ uoff_t v_offset;
+
+ v_offset = sstream->istream.parent_start_offset +
+ sstream->istream.istream.v_offset;
+ if (sstream->istream.parent->seekable ||
+ v_offset > sstream->istream.parent->v_offset) {
+ /* get to same position in parent stream */
+ i_stream_seek(sstream->istream.parent, v_offset);
+ }
+}
+
+static const char *
+i_stream_create_sized_default_error_callback(
+ const struct istream_sized_error_data *data, void *context ATTR_UNUSED)
+{
+ if (data->v_offset + data->new_bytes < data->wanted_size) {
+ return t_strdup_printf("Stream is smaller than expected "
+ "(%"PRIuUOFF_T" < %"PRIuUOFF_T")",
+ data->v_offset + data->new_bytes, data->wanted_size);
+ } else {
+ return t_strdup_printf("Stream is larger than expected "
+ "(%"PRIuUOFF_T" > %"PRIuUOFF_T", eof=%d)",
+ data->v_offset + data->new_bytes, data->wanted_size,
+ data->eof ? 1 : 0);
+ }
+}
+
+static ssize_t
+i_stream_sized_parent_read(struct istream_private *stream, size_t *pos_r)
+{
+ ssize_t ret;
+
+ do {
+ ret = i_stream_read_memarea(stream->parent);
+ stream->istream.stream_errno = stream->parent->stream_errno;
+ stream->istream.eof = stream->parent->eof;
+ stream->buffer = i_stream_get_data(stream->parent, pos_r);
+ } while (*pos_r <= stream->pos && ret > 0);
+ return ret;
+}
+
+static ssize_t i_stream_sized_read(struct istream_private *stream)
+{
+ struct sized_istream *sstream =
+ container_of(stream, struct sized_istream, istream);
+ struct istream_sized_error_data data;
+ const char *error;
+ uoff_t left;
+ ssize_t ret;
+ size_t pos;
+
+ i_stream_seek(stream->parent, sstream->istream.parent_start_offset +
+ stream->istream.v_offset);
+
+ stream->pos -= stream->skip;
+ stream->skip = 0;
+
+ stream->buffer = i_stream_get_data(stream->parent, &pos);
+ if (pos > stream->pos)
+ ret = 0;
+ else {
+ if ((ret = i_stream_sized_parent_read(stream, &pos)) == -2)
+ return -2;
+ }
+
+ left = sstream->size - stream->istream.v_offset;
+ if (pos == left && ret != -1) {
+ /* we have exactly the wanted amount of data left, but we
+ don't know yet if there is more data in parent. */
+ ret = i_stream_sized_parent_read(stream, &pos);
+ }
+
+ i_zero(&data);
+ data.v_offset = stream->istream.v_offset;
+ data.new_bytes = pos;
+ data.wanted_size = sstream->size;
+ data.eof = stream->istream.eof;
+
+ if (pos == left) {
+ /* we may or may not be finished, depending on whether
+ parent is at EOF. */
+ } else if (pos > left) {
+ /* parent has more data available than expected */
+ if (!sstream->min_size_only) {
+ error = sstream->error_callback(&data, sstream->error_context);
+ io_stream_set_error(&stream->iostream, "%s", error);
+ stream->istream.stream_errno = EINVAL;
+ return -1;
+ }
+ pos = left;
+ if (pos <= stream->pos) {
+ stream->istream.eof = TRUE;
+ ret = -1;
+ }
+ } else if (!stream->istream.eof) {
+ /* still more to read */
+ } else if (stream->istream.stream_errno == ENOENT) {
+ /* lost the file */
+ } else {
+ /* EOF before we reached the wanted size */
+ error = sstream->error_callback(&data, sstream->error_context);
+ io_stream_set_error(&stream->iostream, "%s", error);
+ stream->istream.stream_errno = EPIPE;
+ }
+
+ ret = pos > stream->pos ? (ssize_t)(pos - stream->pos) :
+ (ret == 0 ? 0 : -1);
+ stream->pos = pos;
+ i_assert(ret != -1 || stream->istream.eof ||
+ stream->istream.stream_errno != 0);
+ return ret;
+}
+
+static int
+i_stream_sized_stat(struct istream_private *stream, bool exact ATTR_UNUSED)
+{
+ struct sized_istream *sstream =
+ container_of(stream, struct sized_istream, istream);
+ const struct stat *st;
+
+ /* parent stream may be base64-decoder. don't waste time decoding the
+ entire stream, since we already know what the size is supposed
+ to be. */
+ if (i_stream_stat(stream->parent, FALSE, &st) < 0) {
+ stream->istream.stream_errno = stream->parent->stream_errno;
+ return -1;
+ }
+
+ stream->statbuf = *st;
+ stream->statbuf.st_size = sstream->size;
+ return 0;
+}
+
+static struct sized_istream *
+i_stream_create_sized_common(struct istream *input, uoff_t size)
+{
+ struct sized_istream *sstream;
+
+ sstream = i_new(struct sized_istream, 1);
+ sstream->size = size;
+ sstream->istream.max_buffer_size = input->real_stream->max_buffer_size;
+
+ sstream->istream.iostream.destroy = i_stream_sized_destroy;
+ sstream->istream.read = i_stream_sized_read;
+ sstream->istream.stat = i_stream_sized_stat;
+
+ sstream->istream.istream.readable_fd = input->readable_fd;
+ sstream->istream.istream.blocking = input->blocking;
+ sstream->istream.istream.seekable = input->seekable;
+ (void)i_stream_create(&sstream->istream, input,
+ i_stream_get_fd(input), 0);
+ return sstream;
+}
+
+struct istream *i_stream_create_sized(struct istream *input, uoff_t size)
+{
+ struct sized_istream *sstream;
+
+ sstream = i_stream_create_sized_common(input, size);
+ sstream->error_callback = i_stream_create_sized_default_error_callback;
+ sstream->error_context = sstream;
+ return &sstream->istream.istream;
+}
+
+struct istream *i_stream_create_sized_range(struct istream *input,
+ uoff_t offset, uoff_t size)
+{
+ uoff_t orig_offset = input->v_offset;
+ struct istream *ret;
+
+ input->v_offset = offset;
+ ret = i_stream_create_sized(input, size);
+ input->v_offset = orig_offset;
+ return ret;
+}
+
+struct istream *i_stream_create_min_sized(struct istream *input, uoff_t min_size)
+{
+ struct istream *ret;
+
+ ret= i_stream_create_sized(input, min_size);
+ struct sized_istream *ret_sstream =
+ container_of(ret->real_stream, struct sized_istream, istream);
+ ret_sstream->min_size_only = TRUE;
+ return ret;
+}
+
+struct istream *i_stream_create_min_sized_range(struct istream *input,
+ uoff_t offset, uoff_t min_size)
+{
+ struct istream *ret;
+
+ ret = i_stream_create_sized_range(input, offset, min_size);
+ struct sized_istream *ret_sstream =
+ container_of(ret->real_stream, struct sized_istream, istream);
+ ret_sstream->min_size_only = TRUE;
+ return ret;
+}
+
+#undef i_stream_create_sized_with_callback
+struct istream *
+i_stream_create_sized_with_callback(struct istream *input, uoff_t size,
+ istream_sized_callback_t *error_callback,
+ void *context)
+{
+ struct sized_istream *sstream;
+
+ sstream = i_stream_create_sized_common(input, size);
+ sstream->error_callback = error_callback;
+ sstream->error_context = context;
+ return &sstream->istream.istream;
+}
diff --git a/src/lib/istream-sized.h b/src/lib/istream-sized.h
new file mode 100644
index 0000000..7cbb6c6
--- /dev/null
+++ b/src/lib/istream-sized.h
@@ -0,0 +1,41 @@
+#ifndef ISTREAM_SIZED_H
+#define ISTREAM_SIZED_H
+
+struct istream_sized_error_data {
+ /* Stream's current v_offset */
+ uoff_t v_offset;
+ /* How many more bytes are being added within this read() */
+ size_t new_bytes;
+ /* What's the original wanted size. */
+ uoff_t wanted_size;
+ /* TRUE if we're at EOF now */
+ bool eof;
+};
+
+typedef const char *
+istream_sized_callback_t(const struct istream_sized_error_data *data,
+ void *context);
+
+/* Assume that input stream is exactly the given size. If the stream is too
+ small, fail with stream_errno=EPIPE. If stream is too large, fail with
+ stream_errno=EINVAL. */
+struct istream *i_stream_create_sized(struct istream *input, uoff_t size);
+struct istream *i_stream_create_sized_range(struct istream *input,
+ uoff_t offset, uoff_t size);
+/* Like i_stream_create_sized*(), but allow input stream's size to be larger. */
+struct istream *i_stream_create_min_sized(struct istream *input, uoff_t min_size);
+struct istream *i_stream_create_min_sized_range(struct istream *input,
+ uoff_t offset, uoff_t min_size);
+/* Same as i_stream_create_sized(), but set the error message via the
+ callback. */
+struct istream *
+i_stream_create_sized_with_callback(struct istream *input, uoff_t size,
+ istream_sized_callback_t *error_callback,
+ void *context);
+#define i_stream_create_sized_with_callback(input, size, error_callback, context) \
+ i_stream_create_sized_with_callback(input, size - \
+ CALLBACK_TYPECHECK(error_callback, \
+ const char *(*)(const struct istream_sized_error_data *, typeof(context))), \
+ (istream_sized_callback_t *)error_callback, context)
+
+#endif
diff --git a/src/lib/istream-tee.c b/src/lib/istream-tee.c
new file mode 100644
index 0000000..858afd7
--- /dev/null
+++ b/src/lib/istream-tee.c
@@ -0,0 +1,258 @@
+/* Copyright (c) 2006-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "istream-private.h"
+#include "istream-tee.h"
+
+struct tee_istream {
+ struct istream *input;
+ struct tee_child_istream *children;
+
+ uoff_t max_read_offset;
+};
+
+struct tee_child_istream {
+ struct istream_private istream;
+
+ struct tee_istream *tee;
+ struct tee_child_istream *next;
+
+ bool last_read_waiting:1;
+};
+
+static void tee_streams_update_buffer(struct tee_istream *tee)
+{
+ struct tee_child_istream *tstream = tee->children;
+ const unsigned char *data;
+ size_t size, old_used;
+
+ data = i_stream_get_data(tee->input, &size);
+ for (; tstream != NULL; tstream = tstream->next) {
+ if (tstream->istream.istream.closed) {
+ tstream->istream.skip = tstream->istream.pos = 0;
+ continue;
+ }
+ old_used = tstream->istream.pos - tstream->istream.skip;
+
+ tstream->istream.buffer = data;
+ i_assert(tstream->istream.istream.v_offset >= tee->input->v_offset);
+ tstream->istream.skip = tstream->istream.istream.v_offset -
+ tee->input->v_offset;
+ i_assert(tstream->istream.skip + old_used <= size);
+ tstream->istream.pos = tstream->istream.skip + old_used;
+
+ tstream->istream.parent_expected_offset =
+ tee->input->v_offset;
+ tstream->istream.access_counter =
+ tee->input->real_stream->access_counter;
+ }
+}
+
+static void tee_streams_skip(struct tee_istream *tee)
+{
+ struct tee_child_istream *tstream = tee->children;
+ size_t min_skip;
+
+ min_skip = SIZE_MAX;
+ for (; tstream != NULL; tstream = tstream->next) {
+ if (tstream->istream.skip < min_skip &&
+ !tstream->istream.istream.closed)
+ min_skip = tstream->istream.skip;
+ }
+
+ if (min_skip > 0 && min_skip != SIZE_MAX) {
+ i_stream_skip(tee->input, min_skip);
+ tee_streams_update_buffer(tee);
+ }
+}
+
+static void i_stream_tee_close(struct iostream_private *stream,
+ bool close_parent ATTR_UNUSED)
+{
+ struct tee_child_istream *tstream =
+ container_of(stream, struct tee_child_istream,
+ istream.iostream);
+
+ tee_streams_skip(tstream->tee);
+}
+
+static void i_stream_tee_destroy(struct iostream_private *stream)
+{
+ struct tee_child_istream *tstream =
+ container_of(stream, struct tee_child_istream,
+ istream.iostream);
+ struct tee_istream *tee = tstream->tee;
+ struct tee_child_istream **p;
+
+ if (tstream->istream.istream.v_offset > tee->max_read_offset)
+ tee->max_read_offset = tstream->istream.istream.v_offset;
+
+ for (p = &tee->children; *p != NULL; p = &(*p)->next) {
+ if (*p == tstream) {
+ *p = tstream->next;
+ break;
+ }
+ }
+
+ if (tee->children == NULL) {
+ /* last child. the tee is now destroyed */
+ i_assert(tee->input->v_offset <= tee->max_read_offset);
+ i_stream_skip(tee->input,
+ tee->max_read_offset - tee->input->v_offset);
+
+ i_stream_unref(&tee->input);
+ i_free(tee);
+ } else {
+ tee_streams_skip(tstream->tee);
+ }
+ /* i_stream_unref() shouldn't unref the parent */
+ tstream->istream.parent = NULL;
+}
+
+static void
+i_stream_tee_set_max_buffer_size(struct iostream_private *stream,
+ size_t max_size)
+{
+ struct tee_child_istream *tstream =
+ container_of(stream, struct tee_child_istream,
+ istream.iostream);
+
+ tstream->istream.max_buffer_size = max_size;
+ i_stream_set_max_buffer_size(tstream->tee->input, max_size);
+}
+
+static ssize_t i_stream_tee_read(struct istream_private *stream)
+{
+ struct tee_child_istream *tstream =
+ container_of(stream, struct tee_child_istream, istream);
+ struct istream *input = tstream->tee->input;
+ const unsigned char *data;
+ size_t size;
+ uoff_t last_high_offset;
+ ssize_t ret;
+
+ tstream->last_read_waiting = FALSE;
+ if (stream->buffer == NULL) {
+ /* initial read */
+ tee_streams_update_buffer(tstream->tee);
+ }
+ data = i_stream_get_data(input, &size);
+
+ /* last_high_offset contains how far we have read this child tee stream
+ so far. input->v_offset + size contains how much is available in
+ the parent stream without having to read more. */
+ last_high_offset = stream->istream.v_offset +
+ (stream->pos - stream->skip);
+ if (stream->pos == size) {
+ /* we've read everything, need to read more */
+ i_assert(last_high_offset == input->v_offset + size);
+ tee_streams_skip(tstream->tee);
+ ret = i_stream_read(input);
+ if (ret <= 0) {
+ size = i_stream_get_data_size(input);
+ if (ret == -2 && stream->skip != 0) {
+ /* someone else is holding the data,
+ wait for it */
+ tstream->last_read_waiting = TRUE;
+ return 0;
+ }
+ stream->istream.stream_errno = input->stream_errno;
+ stream->istream.eof = input->eof;
+ return ret;
+ }
+ tee_streams_update_buffer(tstream->tee);
+ data = i_stream_get_data(input, &size);
+ } else {
+ /* there's still some data available from parent */
+ i_assert(last_high_offset < input->v_offset + size);
+ tee_streams_update_buffer(tstream->tee);
+ i_assert(stream->pos < size);
+ }
+
+ i_assert(stream->buffer == data);
+ ret = size - stream->pos;
+ i_assert(ret > 0);
+ stream->pos = size;
+
+ i_assert(stream->istream.v_offset + (stream->pos - stream->skip) ==
+ input->v_offset + size);
+ return ret;
+}
+
+static int
+i_stream_tee_stat(struct istream_private *stream, bool exact)
+{
+ struct tee_child_istream *tstream =
+ container_of(stream, struct tee_child_istream, istream);
+ const struct stat *st;
+
+ if (i_stream_stat(tstream->tee->input, exact, &st) < 0)
+ return -1;
+ stream->statbuf = *st;
+ return 0;
+}
+
+static void i_stream_tee_sync(struct istream_private *stream)
+{
+ struct tee_child_istream *tstream =
+ container_of(stream, struct tee_child_istream, istream);
+
+ tee_streams_skip(tstream->tee);
+ if (i_stream_get_data_size(tstream->tee->input) != 0) {
+ i_panic("tee-istream: i_stream_sync() called "
+ "with data still buffered");
+ }
+ i_stream_sync(tstream->tee->input);
+}
+
+struct tee_istream *tee_i_stream_create(struct istream *input)
+{
+ struct tee_istream *tee;
+
+ tee = i_new(struct tee_istream, 1);
+ if (input->v_offset == 0) {
+ i_stream_ref(input);
+ tee->input = input;
+ } else {
+ tee->input = i_stream_create_limit(input, UOFF_T_MAX);
+ }
+ return tee;
+}
+
+struct istream *tee_i_stream_create_child(struct tee_istream *tee)
+{
+ struct tee_child_istream *tstream;
+ struct istream *ret, *input = tee->input;
+
+ tstream = i_new(struct tee_child_istream, 1);
+ tstream->tee = tee;
+
+ tstream->istream.max_buffer_size = input->real_stream->max_buffer_size;
+ tstream->istream.iostream.close = i_stream_tee_close;
+ tstream->istream.iostream.destroy = i_stream_tee_destroy;
+ tstream->istream.iostream.set_max_buffer_size =
+ i_stream_tee_set_max_buffer_size;
+
+ tstream->istream.read = i_stream_tee_read;
+ tstream->istream.stat = i_stream_tee_stat;
+ tstream->istream.sync = i_stream_tee_sync;
+
+ tstream->next = tee->children;
+ tee->children = tstream;
+
+ ret = i_stream_create(&tstream->istream, input, i_stream_get_fd(input),
+ ISTREAM_CREATE_FLAG_NOOP_SNAPSHOT);
+ i_stream_set_name(&tstream->istream.istream, i_stream_get_name(input));
+ /* we keep the reference in tee stream, no need for extra references */
+ i_stream_unref(&input);
+ return ret;
+}
+
+bool tee_i_stream_child_is_waiting(struct istream *input)
+{
+ struct tee_child_istream *tstream =
+ container_of(input->real_stream,
+ struct tee_child_istream, istream);
+
+ return tstream->last_read_waiting;
+}
diff --git a/src/lib/istream-tee.h b/src/lib/istream-tee.h
new file mode 100644
index 0000000..82c0c93
--- /dev/null
+++ b/src/lib/istream-tee.h
@@ -0,0 +1,17 @@
+#ifndef ISTREAM_TEE_H
+#define ISTREAM_TEE_H
+
+/* Tee can be used to create multiple child input streams which can access
+ a single non-blocking input stream in a way that data isn't removed from
+ memory until all child streams have consumed the input.
+
+ If the stream's buffer gets full because some child isn't consuming the
+ data, other streams get returned 0 by i_stream_read(). */
+struct tee_istream *tee_i_stream_create(struct istream *input);
+/* Returns TRUE if last read() operation returned 0, because it was waiting
+ for another tee stream to read more of its data. */
+bool tee_i_stream_child_is_waiting(struct istream *input);
+
+struct istream *tee_i_stream_create_child(struct tee_istream *tee);
+
+#endif
diff --git a/src/lib/istream-timeout.c b/src/lib/istream-timeout.c
new file mode 100644
index 0000000..4fcced1
--- /dev/null
+++ b/src/lib/istream-timeout.c
@@ -0,0 +1,150 @@
+/* Copyright (c) 2014-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "ioloop.h"
+#include "time-util.h"
+#include "istream-private.h"
+#include "istream-timeout.h"
+
+struct timeout_istream {
+ struct istream_private istream;
+
+ struct timeout *to;
+ struct timeval last_read_timestamp;
+ time_t created;
+
+ unsigned int timeout_msecs;
+ bool update_timestamp;
+};
+
+static void i_stream_timeout_close(struct iostream_private *stream,
+ bool close_parent)
+{
+ struct timeout_istream *tstream =
+ container_of(stream, struct timeout_istream, istream.iostream);
+
+ timeout_remove(&tstream->to);
+ if (close_parent)
+ i_stream_close(tstream->istream.parent);
+}
+
+static void i_stream_timeout_switch_ioloop_to(struct istream_private *stream,
+ struct ioloop *ioloop)
+{
+ struct timeout_istream *tstream =
+ container_of(stream, struct timeout_istream, istream);
+
+ if (tstream->to != NULL)
+ tstream->to = io_loop_move_timeout_to(ioloop, &tstream->to);
+}
+
+static void i_stream_timeout(struct timeout_istream *tstream)
+{
+ struct iostream_private *iostream = &tstream->istream.iostream;
+ unsigned int over_msecs;
+ int diff;
+
+ if (tstream->update_timestamp) {
+ /* we came here after a long-running code. timeouts are handled
+ before IOs, so wait for i_stream_read() to be called again
+ before assuming that we've timed out. */
+ return;
+ }
+
+ timeout_remove(&tstream->to);
+
+ diff = timeval_diff_msecs(&ioloop_timeval, &tstream->last_read_timestamp);
+ if (diff < (int)tstream->timeout_msecs) {
+ /* we haven't reached the read timeout yet, update it */
+ if (diff < 0)
+ diff = 0;
+ tstream->to = timeout_add_to(io_stream_get_ioloop(iostream),
+ tstream->timeout_msecs - diff,
+ i_stream_timeout, tstream);
+ return;
+ }
+ over_msecs = diff - tstream->timeout_msecs;
+
+ io_stream_set_error(&tstream->istream.iostream,
+ "Read timeout in %u.%03u s after %"PRIuUOFF_T" bytes%s",
+ diff/1000, diff%1000,
+ tstream->istream.istream.v_offset,
+ over_msecs < 1000 ? "" : t_strdup_printf(
+ " (requested timeout in %u ms)", tstream->timeout_msecs));
+ tstream->istream.istream.stream_errno = ETIMEDOUT;
+
+ i_stream_set_input_pending(tstream->istream.parent, TRUE);
+}
+
+static void i_stream_timeout_set_pending(struct timeout_istream *tstream)
+{
+ /* make sure we get called again on the next ioloop run. this updates
+ the timeout to the timestamp where we actually would have wanted to
+ start waiting for more data (so if there is long-running code
+ outside the ioloop it's not counted) */
+ tstream->update_timestamp = TRUE;
+ tstream->last_read_timestamp = ioloop_timeval;
+ i_stream_set_input_pending(&tstream->istream.istream, TRUE);
+}
+
+static ssize_t
+i_stream_timeout_read(struct istream_private *stream)
+{
+ struct timeout_istream *tstream =
+ container_of(stream, struct timeout_istream, istream);
+ struct iostream_private *iostream = &tstream->istream.iostream;
+ ssize_t ret;
+
+ i_stream_seek(stream->parent, stream->parent_start_offset +
+ stream->istream.v_offset);
+
+ ret = i_stream_read_copy_from_parent(&stream->istream);
+ if (ret < 0) {
+ /* failed */
+ if (errno == ECONNRESET || errno == EPIPE) {
+ int diff = ioloop_time - tstream->created;
+
+ io_stream_set_error(&tstream->istream.iostream,
+ "%s (opened %d secs ago)",
+ i_stream_get_error(stream->parent), diff);
+ }
+ } else if (tstream->to == NULL && tstream->timeout_msecs > 0) {
+ /* first read. add the timeout here instead of in init
+ in case the stream is created long before it's actually
+ read from. */
+ tstream->to = timeout_add_to(io_stream_get_ioloop(iostream),
+ tstream->timeout_msecs,
+ i_stream_timeout, tstream);
+ i_stream_timeout_set_pending(tstream);
+ } else if (ret > 0 && tstream->to != NULL) {
+ /* we read something, reset the timeout */
+ timeout_reset(tstream->to);
+ i_stream_timeout_set_pending(tstream);
+ } else if (tstream->update_timestamp) {
+ tstream->update_timestamp = FALSE;
+ tstream->last_read_timestamp = ioloop_timeval;
+ }
+ return ret;
+}
+
+struct istream *
+i_stream_create_timeout(struct istream *input, unsigned int timeout_msecs)
+{
+ struct timeout_istream *tstream;
+
+ tstream = i_new(struct timeout_istream, 1);
+ tstream->timeout_msecs = timeout_msecs;
+ tstream->istream.max_buffer_size = input->real_stream->max_buffer_size;
+ tstream->istream.stream_size_passthrough = TRUE;
+ tstream->created = ioloop_time;
+
+ tstream->istream.read = i_stream_timeout_read;
+ tstream->istream.switch_ioloop_to = i_stream_timeout_switch_ioloop_to;
+ tstream->istream.iostream.close = i_stream_timeout_close;
+
+ tstream->istream.istream.readable_fd = input->readable_fd;
+ tstream->istream.istream.blocking = input->blocking;
+ tstream->istream.istream.seekable = input->seekable;
+ return i_stream_create(&tstream->istream, input,
+ i_stream_get_fd(input), 0);
+}
diff --git a/src/lib/istream-timeout.h b/src/lib/istream-timeout.h
new file mode 100644
index 0000000..a26318a
--- /dev/null
+++ b/src/lib/istream-timeout.h
@@ -0,0 +1,9 @@
+#ifndef ISTREAM_TIMEOUT_H
+#define ISTREAM_TIMEOUT_H
+
+/* Return ETIMEDOUT error if read() doesn't return anything for timeout_msecs.
+ If timeout_msecs=0, there is no timeout. */
+struct istream *
+i_stream_create_timeout(struct istream *input, unsigned int timeout_msecs);
+
+#endif
diff --git a/src/lib/istream-try.c b/src/lib/istream-try.c
new file mode 100644
index 0000000..3b83a7f
--- /dev/null
+++ b/src/lib/istream-try.c
@@ -0,0 +1,165 @@
+/* Copyright (c) 2013-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "istream-private.h"
+#include "istream-try.h"
+
+struct try_istream {
+ struct istream_private istream;
+
+ size_t min_buffer_full_size;
+ unsigned int try_input_count;
+ struct istream **try_input;
+ unsigned int try_idx;
+
+ struct istream *final_input;
+};
+
+static void i_stream_unref_try_inputs(struct try_istream *tstream)
+{
+ for (unsigned int i = 0; i < tstream->try_input_count; i++) {
+ if (tstream->try_input[i] != NULL)
+ i_stream_unref(&tstream->try_input[i]);
+ }
+ tstream->try_input_count = 0;
+ i_free(tstream->try_input);
+}
+
+static void i_stream_try_close(struct iostream_private *stream,
+ bool close_parent)
+{
+ struct try_istream *tstream =
+ container_of(stream, struct try_istream, istream.iostream);
+
+ if (close_parent) {
+ if (tstream->istream.parent != NULL)
+ i_stream_close(tstream->istream.parent);
+ for (unsigned int i = 0; i < tstream->try_input_count; i++) {
+ if (tstream->try_input[i] != NULL)
+ i_stream_close(tstream->try_input[i]);
+ }
+ }
+ i_stream_unref_try_inputs(tstream);
+}
+
+static bool
+i_stream_try_is_buffer_full(struct try_istream *tstream,
+ struct istream *try_input)
+{
+ /* See if one of the parent istreams have their buffer full.
+ This is mainly intended to check with istream-tee whether its
+ parent is full. That means that the try_input has already seen
+ a full buffer of input, but it hasn't decided to return anything
+ yet. But it also hasn't failed, so we'll assume that the input is
+ correct for it and it simply needs a lot more input before it can
+ return anything (e.g. istream-bzlib).
+
+ Note that it's common for buffer_size to be 0 for all parents. This
+ could be e.g. because the root is istream-concat, which breaks the
+ parent hierarchy since it has multiple parents. So the buffer_size
+ check can be thought of just as an optional extra check that
+ sometimes works and sometimes doesn't.
+
+ Note that we don't check whether skip==pos. An istream could be
+ reading its buffer full without skipping over anything. */
+ while (try_input->real_stream->parent != NULL) {
+ try_input = try_input->real_stream->parent;
+ if (try_input->real_stream->pos >= try_input->real_stream->buffer_size &&
+ try_input->real_stream->pos >= tstream->min_buffer_full_size)
+ return TRUE;
+ }
+ return FALSE;
+}
+
+static int i_stream_try_detect(struct try_istream *tstream)
+{
+ int ret;
+
+ for (; tstream->try_idx < tstream->try_input_count; tstream->try_idx++) {
+ struct istream *try_input =
+ tstream->try_input[tstream->try_idx];
+
+ ret = i_stream_read(try_input);
+ if (ret == 0 && i_stream_try_is_buffer_full(tstream, try_input))
+ ret = 1;
+ if (ret > 0) {
+ i_stream_init_parent(&tstream->istream, try_input);
+ i_stream_unref_try_inputs(tstream);
+ return 1;
+ }
+ if (ret == 0)
+ return 0;
+ if (try_input->stream_errno == 0) {
+ /* empty file */
+ tstream->istream.istream.eof = TRUE;
+ return -1;
+ }
+ if (try_input->stream_errno != EINVAL) {
+ tstream->istream.istream.stream_errno =
+ try_input->stream_errno;
+ io_stream_set_error(&tstream->istream.iostream,
+ "Unexpected error while detecting stream format: %s",
+ i_stream_get_error(try_input));
+ return -1;
+ }
+ }
+
+ /* All streams failed with EINVAL. */
+ io_stream_set_error(&tstream->istream.iostream,
+ "Failed to detect stream format");
+ tstream->istream.istream.stream_errno = EINVAL;
+ return -1;
+}
+
+static ssize_t
+i_stream_try_read(struct istream_private *stream)
+{
+ struct try_istream *tstream =
+ container_of(stream, struct try_istream, istream);
+ int ret;
+
+ if (stream->parent == NULL) {
+ if ((ret = i_stream_try_detect(tstream)) <= 0)
+ return ret;
+ }
+
+ i_stream_seek(stream->parent, stream->parent_start_offset +
+ stream->istream.v_offset);
+ return i_stream_read_copy_from_parent(&stream->istream);
+}
+
+struct istream *istream_try_create(struct istream *const input[],
+ size_t min_buffer_full_size)
+{
+ struct try_istream *tstream;
+ unsigned int count;
+ size_t max_buffer_size = I_STREAM_MIN_SIZE;
+ bool blocking = TRUE, seekable = TRUE;
+
+ for (count = 0; input[count] != NULL; count++) {
+ max_buffer_size = I_MAX(max_buffer_size,
+ i_stream_get_max_buffer_size(input[count]));
+ if (!input[count]->blocking)
+ blocking = FALSE;
+ if (!input[count]->seekable)
+ seekable = FALSE;
+ i_stream_ref(input[count]);
+ }
+ i_assert(count != 0);
+
+ tstream = i_new(struct try_istream, 1);
+ tstream->min_buffer_full_size = min_buffer_full_size;
+ tstream->try_input_count = count;
+ tstream->try_input = p_memdup(default_pool, input,
+ sizeof(*input) * count);
+
+ tstream->istream.iostream.close = i_stream_try_close;
+
+ tstream->istream.max_buffer_size = max_buffer_size;
+ tstream->istream.read = i_stream_try_read;
+
+ tstream->istream.istream.readable_fd = FALSE;
+ tstream->istream.istream.blocking = blocking;
+ tstream->istream.istream.seekable = seekable;
+ return i_stream_create(&tstream->istream, NULL, -1, 0);
+}
diff --git a/src/lib/istream-try.h b/src/lib/istream-try.h
new file mode 100644
index 0000000..6f0f8b7
--- /dev/null
+++ b/src/lib/istream-try.h
@@ -0,0 +1,25 @@
+#ifndef ISTREAM_TRY_H
+#define ISTREAM_TRY_H
+
+/* Read from the first input stream that doesn't fail with EINVAL. If any of
+ the streams fail with non-EINVAL, it's treated as a fatal failure and the
+ error is immediately returned. If a stream returns 0, more data is waited
+ for before continuing to the next stream. This allows the last stream to
+ be a fallback stream that always succeeds.
+
+ Once the stream is detected, all the other streams are unreferenced.
+ The streams should usually be children of the same parent tee-istream.
+
+ Detecting whether istream-tee buffer is full or not is a bit tricky.
+ There's no visible difference between non-blocking istream returning 0 and
+ istream-tee buffer being full. To work around this, we treat used buffer
+ sizes <= min_buffer_full_size as being non-blocking istreams, while
+ buffer sizes > min_buffer_full_size are assumed to be due to istream-tee
+ max buffer size being reached. Practically this means that
+ min_buffer_full_size must be smaller than the smallest of the istreams'
+ maximum buffer sizes, but large enough that all the istreams would have
+ returned EINVAL on invalid input by that position. */
+struct istream *istream_try_create(struct istream *const input[],
+ size_t min_buffer_full_size);
+
+#endif
diff --git a/src/lib/istream-unix.c b/src/lib/istream-unix.c
new file mode 100644
index 0000000..3a8fabd
--- /dev/null
+++ b/src/lib/istream-unix.c
@@ -0,0 +1,113 @@
+/* Copyright (c) 2014-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "fdpass.h"
+#include "istream-file-private.h"
+#include "istream-unix.h"
+
+struct unix_istream {
+ struct file_istream fstream;
+ bool next_read_fd;
+ int read_fd;
+};
+
+static void
+i_stream_unix_close(struct iostream_private *stream, bool close_parent)
+{
+ struct unix_istream *ustream =
+ container_of(stream, struct unix_istream,
+ fstream.istream.iostream);
+
+ i_close_fd(&ustream->read_fd);
+ i_stream_file_close(stream, close_parent);
+}
+
+static ssize_t i_stream_unix_read(struct istream_private *stream)
+{
+ struct unix_istream *ustream =
+ container_of(stream, struct unix_istream, fstream.istream);
+ size_t size;
+ ssize_t ret;
+
+ if (!ustream->next_read_fd)
+ return i_stream_file_read(stream);
+
+ i_assert(ustream->read_fd == -1);
+ i_assert(ustream->fstream.skip_left == 0); /* not supported here.. */
+ if (!i_stream_try_alloc(stream, 1, &size))
+ return -2;
+
+ ret = fd_read(stream->fd, stream->w_buffer + stream->pos, size,
+ &ustream->read_fd);
+ if (ustream->read_fd != -1)
+ ustream->next_read_fd = FALSE;
+
+ if (ret == 0) {
+ /* EOF */
+ stream->istream.eof = TRUE;
+ ustream->fstream.seen_eof = TRUE;
+ return -1;
+ }
+
+ if (unlikely(ret < 0)) {
+ if ((errno == EINTR || errno == EAGAIN) &&
+ !stream->istream.blocking) {
+ return 0;
+ } else {
+ i_assert(errno != 0);
+ /* if we get EBADF for a valid fd, it means something's
+ really wrong and we'd better just crash. */
+ i_assert(errno != EBADF);
+ stream->istream.stream_errno = errno;
+ return -1;
+ }
+ }
+ stream->pos += ret;
+ return ret;
+}
+
+struct istream *i_stream_create_unix(int fd, size_t max_buffer_size)
+{
+ struct unix_istream *ustream;
+ struct istream *input;
+
+ i_assert(fd != -1);
+
+ ustream = i_new(struct unix_istream, 1);
+ ustream->read_fd = -1;
+ input = i_stream_create_file_common(&ustream->fstream, fd, NULL,
+ max_buffer_size, FALSE);
+ input->real_stream->iostream.close = i_stream_unix_close;
+ input->real_stream->read = i_stream_unix_read;
+ return input;
+}
+
+void i_stream_unix_set_read_fd(struct istream *input)
+{
+ struct unix_istream *ustream =
+ container_of(input->real_stream, struct unix_istream,
+ fstream.istream);
+
+ ustream->next_read_fd = TRUE;
+}
+
+void i_stream_unix_unset_read_fd(struct istream *input)
+{
+ struct unix_istream *ustream =
+ container_of(input->real_stream, struct unix_istream,
+ fstream.istream);
+
+ ustream->next_read_fd = FALSE;
+}
+
+int i_stream_unix_get_read_fd(struct istream *input)
+{
+ struct unix_istream *ustream =
+ container_of(input->real_stream, struct unix_istream,
+ fstream.istream);
+ int fd;
+
+ fd = ustream->read_fd;
+ ustream->read_fd = -1;
+ return fd;
+}
diff --git a/src/lib/istream-unix.h b/src/lib/istream-unix.h
new file mode 100644
index 0000000..0b29ccc
--- /dev/null
+++ b/src/lib/istream-unix.h
@@ -0,0 +1,15 @@
+#ifndef ISTREAM_UNIX_H
+#define ISTREAM_UNIX_H
+
+struct istream *i_stream_create_unix(int fd, size_t max_buffer_size);
+/* Start trying to read a file descriptor from the UNIX socket. */
+void i_stream_unix_set_read_fd(struct istream *input);
+/* Stop trying to read a file descriptor from the UNIX socket. */
+void i_stream_unix_unset_read_fd(struct istream *input);
+/* Returns the fd that the last i_stream_read() received, or -1 if no fd
+ was received. This function must be called before
+ i_stream_unix_set_read_fd() is called again after successfully receiving
+ a file descriptor. */
+int i_stream_unix_get_read_fd(struct istream *input);
+
+#endif
diff --git a/src/lib/istream.c b/src/lib/istream.c
new file mode 100644
index 0000000..5fc5112
--- /dev/null
+++ b/src/lib/istream.c
@@ -0,0 +1,1316 @@
+/* Copyright (c) 2002-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "ioloop.h"
+#include "array.h"
+#include "str.h"
+#include "memarea.h"
+#include "istream-private.h"
+
+static bool i_stream_is_buffer_invalid(const struct istream_private *stream);
+
+void i_stream_set_name(struct istream *stream, const char *name)
+{
+ i_free(stream->real_stream->iostream.name);
+ stream->real_stream->iostream.name = i_strdup(name);
+}
+
+const char *i_stream_get_name(struct istream *stream)
+{
+ while (stream->real_stream->iostream.name == NULL) {
+ stream = stream->real_stream->parent;
+ if (stream == NULL)
+ return "";
+ }
+ return stream->real_stream->iostream.name;
+}
+
+static void i_stream_close_full(struct istream *stream, bool close_parents)
+{
+ io_stream_close(&stream->real_stream->iostream, close_parents);
+ stream->closed = TRUE;
+
+ if (stream->stream_errno == 0)
+ stream->stream_errno = EPIPE;
+}
+
+void i_stream_destroy(struct istream **stream)
+{
+ if (*stream == NULL)
+ return;
+
+ i_stream_close_full(*stream, FALSE);
+ i_stream_unref(stream);
+}
+
+void i_stream_ref(struct istream *stream)
+{
+ io_stream_ref(&stream->real_stream->iostream);
+}
+
+void i_stream_unref(struct istream **stream)
+{
+ struct istream_private *_stream;
+
+ if (*stream == NULL)
+ return;
+
+ _stream = (*stream)->real_stream;
+
+ if (_stream->iostream.refcount > 1) {
+ if (!io_stream_unref(&_stream->iostream))
+ i_unreached();
+ } else {
+ /* The snapshot may contain pointers to the parent istreams.
+ Free it before io_stream_unref() frees the parents. */
+ i_stream_snapshot_free(&_stream->prev_snapshot);
+
+ if (io_stream_unref(&_stream->iostream))
+ i_unreached();
+ str_free(&_stream->line_str);
+ i_stream_unref(&_stream->parent);
+ io_stream_free(&_stream->iostream);
+ }
+ *stream = NULL;
+}
+
+#undef i_stream_add_destroy_callback
+void i_stream_add_destroy_callback(struct istream *stream,
+ istream_callback_t *callback, void *context)
+{
+ io_stream_add_destroy_callback(&stream->real_stream->iostream,
+ callback, context);
+}
+
+void i_stream_remove_destroy_callback(struct istream *stream,
+ void (*callback)())
+{
+ io_stream_remove_destroy_callback(&stream->real_stream->iostream,
+ callback);
+}
+
+int i_stream_get_fd(struct istream *stream)
+{
+ struct istream_private *_stream = stream->real_stream;
+
+ return _stream->fd;
+}
+
+void i_stream_copy_fd(struct istream *dest, struct istream *source)
+{
+ int fd = i_stream_get_fd(source);
+
+ i_assert(fd != -1);
+ i_assert(dest->real_stream->fd == -1);
+ dest->real_stream->fd = fd;
+ dest->readable_fd = source->readable_fd;
+}
+
+const char *i_stream_get_error(struct istream *stream)
+{
+ struct istream *s;
+
+ /* we'll only return errors for streams that have stream_errno set or
+ that have reached EOF. we might be returning unintended error
+ otherwise. */
+ if (stream->stream_errno == 0)
+ return stream->eof ? "EOF" : "<no error>";
+
+ for (s = stream; s != NULL; s = s->real_stream->parent) {
+ if (s->stream_errno == 0)
+ break;
+ if (s->real_stream->iostream.error != NULL)
+ return s->real_stream->iostream.error;
+ }
+ return strerror(stream->stream_errno);
+}
+
+const char *i_stream_get_disconnect_reason(struct istream *stream)
+{
+ return io_stream_get_disconnect_reason(stream, NULL);
+}
+
+void i_stream_close(struct istream *stream)
+{
+ if (stream != NULL)
+ i_stream_close_full(stream, TRUE);
+}
+
+void i_stream_set_init_buffer_size(struct istream *stream, size_t size)
+{
+ stream->real_stream->init_buffer_size = size;
+}
+
+void i_stream_set_max_buffer_size(struct istream *stream, size_t max_size)
+{
+ io_stream_set_max_buffer_size(&stream->real_stream->iostream, max_size);
+}
+
+size_t i_stream_get_max_buffer_size(struct istream *stream)
+{
+ size_t max_size = 0;
+
+ do {
+ if (max_size < stream->real_stream->max_buffer_size)
+ max_size = stream->real_stream->max_buffer_size;
+ stream = stream->real_stream->parent;
+ } while (stream != NULL);
+ return max_size;
+}
+
+void i_stream_set_return_partial_line(struct istream *stream, bool set)
+{
+ stream->real_stream->return_nolf_line = set;
+}
+
+void i_stream_set_persistent_buffers(struct istream *stream, bool set)
+{
+ do {
+ stream->real_stream->nonpersistent_buffers = !set;
+ stream = stream->real_stream->parent;
+ } while (stream != NULL);
+}
+
+void i_stream_set_blocking(struct istream *stream, bool blocking)
+{
+ int prev_fd = -1;
+
+ do {
+ stream->blocking = blocking;
+ if (stream->real_stream->fd != -1 &&
+ stream->real_stream->fd != prev_fd) {
+ fd_set_nonblock(stream->real_stream->fd, !blocking);
+ prev_fd = stream->real_stream->fd;
+ }
+ stream = stream->real_stream->parent;
+ } while (stream != NULL);
+}
+
+static void i_stream_update(struct istream_private *stream)
+{
+ if (stream->parent == NULL)
+ stream->access_counter++;
+ else {
+ stream->access_counter =
+ stream->parent->real_stream->access_counter;
+ stream->parent_expected_offset = stream->parent->v_offset;
+ }
+}
+
+static bool snapshot_has_memarea(struct istream_snapshot *snapshot,
+ struct memarea *memarea)
+{
+ if (snapshot->old_memarea == memarea)
+ return TRUE;
+ if (snapshot->prev_snapshot != NULL)
+ return snapshot_has_memarea(snapshot->prev_snapshot, memarea);
+ return FALSE;
+}
+
+struct istream_snapshot *
+i_stream_default_snapshot(struct istream_private *stream,
+ struct istream_snapshot *prev_snapshot)
+{
+ struct istream_snapshot *snapshot;
+
+ if (stream->memarea != NULL) {
+ if (prev_snapshot != NULL) {
+ if (snapshot_has_memarea(prev_snapshot, stream->memarea))
+ return prev_snapshot;
+ }
+ /* This stream has a memarea. Reference it, so we can later on
+ rollback if needed. */
+ snapshot = i_new(struct istream_snapshot, 1);
+ snapshot->old_memarea = stream->memarea;
+ snapshot->prev_snapshot = prev_snapshot;
+ memarea_ref(snapshot->old_memarea);
+ return snapshot;
+ }
+ if (stream->parent == NULL) {
+ if (stream->nonpersistent_buffers) {
+ /* Assume that memarea would be used normally, but
+ now it's NULL because the buffer is empty and
+ empty buffers are freed. */
+ i_assert(stream->skip == stream->pos);
+ return prev_snapshot;
+ }
+ i_panic("%s is missing istream.snapshot() implementation",
+ i_stream_get_name(&stream->istream));
+ }
+ struct istream_private *_parent_stream =
+ stream->parent->real_stream;
+ return _parent_stream->snapshot(_parent_stream, prev_snapshot);
+}
+
+void i_stream_snapshot_free(struct istream_snapshot **_snapshot)
+{
+ struct istream_snapshot *snapshot = *_snapshot;
+
+ if (*_snapshot == NULL)
+ return;
+ *_snapshot = NULL;
+
+ i_stream_snapshot_free(&snapshot->prev_snapshot);
+ if (snapshot->free != NULL)
+ snapshot->free(snapshot);
+ else {
+ if (snapshot->old_memarea != NULL)
+ memarea_unref(&snapshot->old_memarea);
+ i_stream_unref(&snapshot->istream);
+ i_free(snapshot);
+ }
+}
+
+static struct istream_snapshot *
+i_stream_noop_snapshot(struct istream_private *stream ATTR_UNUSED,
+ struct istream_snapshot *prev_snapshot)
+{
+ return prev_snapshot;
+}
+
+ssize_t i_stream_read(struct istream *stream)
+{
+ struct istream_private *_stream = stream->real_stream;
+ ssize_t ret;
+#ifdef DEBUG
+ unsigned char prev_buf[4];
+ const unsigned char *prev_data = _stream->buffer;
+ size_t prev_skip = _stream->skip, prev_pos = _stream->pos;
+ bool invalid = i_stream_is_buffer_invalid(_stream);
+
+ i_assert(prev_skip <= prev_pos);
+ if (invalid)
+ ;
+ else if (prev_pos - prev_skip <= 4)
+ memcpy(prev_buf, prev_data + prev_skip, prev_pos - prev_skip);
+ else {
+ memcpy(prev_buf, prev_data + prev_skip, 2);
+ memcpy(prev_buf+2, prev_data + prev_pos - 2, 2);
+ }
+#endif
+
+ if (_stream->skip != _stream->pos || _stream->prev_snapshot != NULL) {
+ _stream->prev_snapshot =
+ _stream->snapshot(_stream, _stream->prev_snapshot);
+ }
+ ret = i_stream_read_memarea(stream);
+ if (ret > 0)
+ i_stream_snapshot_free(&_stream->prev_snapshot);
+#ifdef DEBUG
+ else if (!invalid) {
+ i_assert((_stream->pos - _stream->skip) == (prev_pos - prev_skip) ||
+ prev_pos == prev_skip);
+ if (prev_pos - prev_skip <= 4)
+ i_assert(memcmp(prev_buf, prev_data + prev_skip, prev_pos - prev_skip) == 0);
+ else {
+ i_assert(memcmp(prev_buf, prev_data + prev_skip, 2) == 0);
+ i_assert(memcmp(prev_buf+2, prev_data + prev_pos - 2, 2) == 0);
+ }
+ }
+#endif
+ return ret;
+}
+
+ssize_t i_stream_read_memarea(struct istream *stream)
+{
+ struct istream_private *_stream = stream->real_stream;
+ size_t old_size;
+ ssize_t ret;
+
+ if (unlikely(stream->closed || stream->stream_errno != 0)) {
+ stream->eof = TRUE;
+ errno = stream->stream_errno;
+ return -1;
+ }
+
+ stream->eof = FALSE;
+
+ if (_stream->parent != NULL)
+ i_stream_seek(_stream->parent, _stream->parent_expected_offset);
+
+ old_size = _stream->pos - _stream->skip;
+ if (_stream->pos < _stream->high_pos) {
+ /* we're here because we seeked back within the read buffer. */
+ ret = _stream->high_pos - _stream->pos;
+ _stream->pos = _stream->high_pos;
+ _stream->high_pos = 0;
+ } else {
+ _stream->high_pos = 0;
+ ret = _stream->read(_stream);
+ }
+ i_assert(old_size <= _stream->pos - _stream->skip);
+ switch (ret) {
+ case -2:
+ i_assert(_stream->skip != _stream->pos);
+ break;
+ case -1:
+ if (stream->stream_errno != 0) {
+ /* error handling should be easier if we now just
+ assume the stream is now at EOF */
+ stream->eof = TRUE;
+ errno = stream->stream_errno;
+ } else {
+ i_assert(stream->eof);
+ i_assert(old_size == _stream->pos - _stream->skip);
+ }
+ break;
+ case 0:
+ i_assert(!stream->blocking);
+ break;
+ default:
+ i_assert(ret > 0);
+ i_assert(_stream->skip < _stream->pos);
+ i_assert((size_t)ret+old_size == _stream->pos - _stream->skip);
+ _stream->last_read_timeval = ioloop_timeval;
+ break;
+ }
+
+ if (stream->stream_errno != 0) {
+ /* error handling should be easier if we now just
+ assume the stream is now at EOF. Note that we could get here
+ even if read() didn't return -1, although that's a little
+ bit sloppy istream implementation. */
+ stream->eof = TRUE;
+ }
+
+ i_stream_update(_stream);
+ /* verify that parents' access_counters are valid. the parent's
+ i_stream_read() should guarantee this. */
+ i_assert(!i_stream_is_buffer_invalid(_stream));
+ return ret;
+}
+
+int i_stream_read_more_memarea(struct istream *stream,
+ const unsigned char **data_r, size_t *size_r)
+{
+ *data_r = i_stream_get_data(stream, size_r);
+ if (*size_r > 0)
+ return 1;
+
+ int ret = i_stream_read_memarea(stream);
+ *data_r = i_stream_get_data(stream, size_r);
+ return ret;
+}
+
+void i_stream_get_last_read_time(struct istream *stream, struct timeval *tv_r)
+{
+ *tv_r = stream->real_stream->last_read_timeval;
+}
+
+ssize_t i_stream_read_copy_from_parent(struct istream *istream)
+{
+ struct istream_private *stream = istream->real_stream;
+ size_t pos;
+ ssize_t ret;
+
+ stream->pos -= stream->skip;
+ stream->skip = 0;
+
+ stream->buffer = i_stream_get_data(stream->parent, &pos);
+ if (pos > stream->pos)
+ ret = 0;
+ else do {
+ ret = i_stream_read_memarea(stream->parent);
+ stream->istream.stream_errno = stream->parent->stream_errno;
+ stream->istream.eof = stream->parent->eof;
+ stream->buffer = i_stream_get_data(stream->parent, &pos);
+ /* check again, in case the parent stream had been seeked
+ backwards and the previous read() didn't get us far
+ enough. */
+ } while (pos <= stream->pos && ret > 0);
+ if (ret == -2) {
+ i_stream_update(stream);
+ return -2;
+ }
+
+ ret = pos > stream->pos ? (ssize_t)(pos - stream->pos) :
+ (ret == 0 ? 0 : -1);
+ stream->pos = pos;
+ i_assert(ret != -1 || stream->istream.eof ||
+ stream->istream.stream_errno != 0);
+ i_stream_update(stream);
+ return ret;
+}
+
+void i_stream_free_buffer(struct istream_private *stream)
+{
+ if (stream->memarea != NULL) {
+ memarea_unref(&stream->memarea);
+ stream->w_buffer = NULL;
+ } else if (stream->w_buffer != NULL) {
+ i_free_and_null(stream->w_buffer);
+ } else {
+ /* don't know how to free it */
+ return;
+ }
+ stream->buffer_size = 0;
+}
+
+void i_stream_skip(struct istream *stream, uoff_t count)
+{
+ struct istream_private *_stream = stream->real_stream;
+ size_t data_size;
+
+ data_size = _stream->pos - _stream->skip;
+ if (count <= data_size) {
+ /* within buffer */
+ stream->v_offset += count;
+ _stream->skip += count;
+ if (_stream->nonpersistent_buffers &&
+ _stream->skip == _stream->pos) {
+ _stream->skip = _stream->pos = 0;
+ i_stream_free_buffer(_stream);
+ }
+ return;
+ }
+
+ /* have to seek forward */
+ count -= data_size;
+ _stream->skip = _stream->pos;
+ stream->v_offset += data_size;
+
+ if (unlikely(stream->closed || stream->stream_errno != 0))
+ return;
+
+ _stream->seek(_stream, stream->v_offset + count, FALSE);
+}
+
+static bool i_stream_can_optimize_seek(struct istream_private *stream)
+{
+ if (stream->parent == NULL)
+ return TRUE;
+
+ /* use the fast route only if the parent stream hasn't been changed */
+ if (stream->access_counter !=
+ stream->parent->real_stream->access_counter)
+ return FALSE;
+
+ return i_stream_can_optimize_seek(stream->parent->real_stream);
+}
+
+void i_stream_seek(struct istream *stream, uoff_t v_offset)
+{
+ struct istream_private *_stream = stream->real_stream;
+
+ if (v_offset >= stream->v_offset &&
+ i_stream_can_optimize_seek(_stream))
+ i_stream_skip(stream, v_offset - stream->v_offset);
+ else {
+ if (unlikely(stream->closed || stream->stream_errno != 0)) {
+ stream->eof = TRUE;
+ return;
+ }
+ stream->eof = FALSE;
+ _stream->seek(_stream, v_offset, FALSE);
+ }
+ i_stream_update(_stream);
+}
+
+void i_stream_seek_mark(struct istream *stream, uoff_t v_offset)
+{
+ struct istream_private *_stream = stream->real_stream;
+
+ if (unlikely(stream->closed || stream->stream_errno != 0))
+ return;
+
+ stream->eof = FALSE;
+ _stream->seek(_stream, v_offset, TRUE);
+ i_stream_update(_stream);
+}
+
+void i_stream_sync(struct istream *stream)
+{
+ struct istream_private *_stream = stream->real_stream;
+
+ if (unlikely(stream->closed || stream->stream_errno != 0))
+ return;
+
+ if (_stream->sync != NULL) {
+ _stream->sync(_stream);
+ i_stream_update(_stream);
+ }
+}
+
+int i_stream_stat(struct istream *stream, bool exact, const struct stat **st_r)
+{
+ struct istream_private *_stream = stream->real_stream;
+
+ if (unlikely(stream->closed || stream->stream_errno != 0))
+ return -1;
+
+ if (_stream->stat(_stream, exact) < 0) {
+ stream->eof = TRUE;
+ return -1;
+ }
+ *st_r = &_stream->statbuf;
+ return 0;
+}
+
+int i_stream_get_size(struct istream *stream, bool exact, uoff_t *size_r)
+{
+ struct istream_private *_stream = stream->real_stream;
+
+ if (unlikely(stream->closed || stream->stream_errno != 0))
+ return -1;
+
+ int ret;
+ if ((ret = _stream->get_size(_stream, exact, size_r)) < 0)
+ stream->eof = TRUE;
+ return ret;
+}
+
+bool i_stream_have_bytes_left(struct istream *stream)
+{
+ return i_stream_get_data_size(stream) > 0 || !stream->eof;
+}
+
+bool i_stream_read_eof(struct istream *stream)
+{
+ if (i_stream_get_data_size(stream) == 0)
+ (void)i_stream_read(stream);
+ return !i_stream_have_bytes_left(stream);
+}
+
+uoff_t i_stream_get_absolute_offset(struct istream *stream)
+{
+ uoff_t abs_offset = stream->v_offset;
+ while (stream != NULL) {
+ abs_offset += stream->real_stream->start_offset;
+ stream = stream->real_stream->parent;
+ }
+ return abs_offset;
+}
+
+static char *i_stream_next_line_finish(struct istream_private *stream, size_t i)
+{
+ char *ret;
+ size_t end;
+
+ if (i > stream->skip && stream->buffer[i-1] == '\r') {
+ end = i - 1;
+ stream->line_crlf = TRUE;
+ } else {
+ end = i;
+ stream->line_crlf = FALSE;
+ }
+
+ if (stream->buffer == stream->w_buffer &&
+ end < stream->buffer_size) {
+ /* modify the buffer directly */
+ stream->w_buffer[end] = '\0';
+ ret = (char *)stream->w_buffer + stream->skip;
+ } else {
+ /* use a temporary string to return it */
+ if (stream->line_str == NULL)
+ stream->line_str = str_new(default_pool, 256);
+ str_truncate(stream->line_str, 0);
+ if (stream->skip < end)
+ str_append_data(stream->line_str, stream->buffer + stream->skip,
+ end - stream->skip);
+ ret = str_c_modifiable(stream->line_str);
+ }
+
+ if (i < stream->pos)
+ i++;
+ stream->istream.v_offset += i - stream->skip;
+ stream->skip = i;
+ return ret;
+}
+
+static char *i_stream_last_line(struct istream_private *_stream)
+{
+ if (_stream->istream.eof && _stream->skip != _stream->pos &&
+ _stream->return_nolf_line) {
+ /* the last line is missing LF and we want to return it. */
+ return i_stream_next_line_finish(_stream, _stream->pos);
+ }
+ return NULL;
+}
+
+char *i_stream_next_line(struct istream *stream)
+{
+ struct istream_private *_stream = stream->real_stream;
+ const unsigned char *pos;
+
+ if (_stream->skip >= _stream->pos)
+ return NULL;
+
+ pos = memchr(_stream->buffer + _stream->skip, '\n',
+ _stream->pos - _stream->skip);
+ if (pos != NULL) {
+ return i_stream_next_line_finish(_stream,
+ pos - _stream->buffer);
+ } else {
+ return i_stream_last_line(_stream);
+ }
+}
+
+char *i_stream_read_next_line(struct istream *stream)
+{
+ char *line;
+
+ for (;;) {
+ line = i_stream_next_line(stream);
+ if (line != NULL)
+ break;
+
+ switch (i_stream_read(stream)) {
+ case -2:
+ io_stream_set_error(&stream->real_stream->iostream,
+ "Line is too long (over %zu"
+ " bytes at offset %"PRIuUOFF_T")",
+ i_stream_get_data_size(stream), stream->v_offset);
+ stream->stream_errno = errno = ENOBUFS;
+ stream->eof = TRUE;
+ return NULL;
+ case -1:
+ return i_stream_last_line(stream->real_stream);
+ case 0:
+ return NULL;
+ }
+ }
+ return line;
+}
+
+bool i_stream_last_line_crlf(struct istream *stream)
+{
+ return stream->real_stream->line_crlf;
+}
+
+static bool i_stream_is_buffer_invalid(const struct istream_private *stream)
+{
+ if (stream->parent == NULL) {
+ /* the buffer can't point to parent, because it doesn't exist */
+ return FALSE;
+ }
+ if (stream->w_buffer != NULL) {
+ /* we can pretty safely assume that the stream is using its
+ own private buffer, so it can never become invalid. */
+ return FALSE;
+ }
+ if (stream->access_counter !=
+ stream->parent->real_stream->access_counter) {
+ /* parent has been modified behind this stream, we can't trust
+ that our buffer is valid */
+ return TRUE;
+ }
+ return i_stream_is_buffer_invalid(stream->parent->real_stream);
+}
+
+const unsigned char *
+i_stream_get_data(struct istream *stream, size_t *size_r)
+{
+ struct istream_private *_stream = stream->real_stream;
+
+ if (_stream->skip >= _stream->pos) {
+ *size_r = 0;
+ return uchar_empty_ptr;
+ }
+
+ if (unlikely(i_stream_is_buffer_invalid(_stream))) {
+ /* This stream may be using parent's buffer directly as
+ _stream->buffer, but the parent stream has already been
+ modified indirectly. This means that the buffer might no
+ longer point to where we assume it points to. So we'll
+ just return the stream as empty until it's read again.
+
+ It's a bit ugly to suddenly drop data from the stream that
+ was already read, but since this happens only with shared
+ parent istreams the caller is hopefully aware enough that
+ something like this might happen. The other solutions would
+ be to a) try to automatically read the data back (but we
+ can't handle errors..) or b) always copy data to stream's
+ own buffer instead of pointing to parent's buffer (but this
+ causes data copying that is nearly always unnecessary). */
+ *size_r = 0;
+ /* if we had already read until EOF, mark the stream again as
+ not being at the end of file. */
+ if (stream->stream_errno == 0) {
+ _stream->skip = _stream->pos = 0;
+ stream->eof = FALSE;
+ }
+ return uchar_empty_ptr;
+ }
+
+ *size_r = _stream->pos - _stream->skip;
+ return _stream->buffer + _stream->skip;
+}
+
+size_t i_stream_get_data_size(struct istream *stream)
+{
+ size_t size;
+
+ (void)i_stream_get_data(stream, &size);
+ return size;
+}
+
+unsigned char *i_stream_get_modifiable_data(struct istream *stream,
+ size_t *size_r)
+{
+ struct istream_private *_stream = stream->real_stream;
+
+ if (_stream->skip >= _stream->pos || _stream->w_buffer == NULL) {
+ *size_r = 0;
+ return NULL;
+ }
+
+ *size_r = _stream->pos - _stream->skip;
+ return _stream->w_buffer + _stream->skip;
+}
+
+int i_stream_read_data(struct istream *stream, const unsigned char **data_r,
+ size_t *size_r, size_t threshold)
+{
+ ssize_t ret = 0;
+ bool read_more = FALSE;
+
+ do {
+ *data_r = i_stream_get_data(stream, size_r);
+ if (*size_r > threshold)
+ return 1;
+
+ /* we need more data */
+ ret = i_stream_read(stream);
+ if (ret > 0)
+ read_more = TRUE;
+ } while (ret > 0);
+
+ *data_r = i_stream_get_data(stream, size_r);
+ if (ret == -2)
+ return -2;
+
+ if (ret == 0) {
+ /* need to read more */
+ i_assert(!stream->blocking);
+ return 0;
+ }
+ if (stream->eof) {
+ if (read_more) {
+ /* we read at least some new data */
+ return 0;
+ }
+ } else {
+ i_assert(stream->stream_errno != 0);
+ }
+ return -1;
+}
+
+int i_stream_read_limited(struct istream *stream, const unsigned char **data_r,
+ size_t *size_r, size_t limit)
+{
+ struct istream_private *_stream = stream->real_stream;
+ int ret;
+
+ *data_r = i_stream_get_data(stream, size_r);
+ if (*size_r >= limit) {
+ *size_r = limit;
+ return 1;
+ }
+
+ _stream->data_limit = limit;
+ ret = i_stream_read_more(stream, data_r, size_r);
+ _stream->data_limit = 0;
+
+ if (*size_r >= limit)
+ *size_r = limit;
+ return ret;
+}
+
+void i_stream_compress(struct istream_private *stream)
+{
+ i_assert(stream->memarea == NULL ||
+ memarea_get_refcount(stream->memarea) == 1);
+
+ if (stream->skip != stream->pos) {
+ memmove(stream->w_buffer, stream->w_buffer + stream->skip,
+ stream->pos - stream->skip);
+ }
+ stream->pos -= stream->skip;
+
+ stream->skip = 0;
+}
+
+static void i_stream_w_buffer_free(void *buf)
+{
+ i_free(buf);
+}
+
+static void
+i_stream_w_buffer_realloc(struct istream_private *stream, size_t old_size)
+{
+ void *new_buffer;
+
+ if (stream->memarea != NULL &&
+ memarea_get_refcount(stream->memarea) == 1) {
+ /* Nobody else is referencing the memarea.
+ We can just reallocate it. */
+ memarea_free_without_callback(&stream->memarea);
+ new_buffer = i_realloc(stream->w_buffer, old_size,
+ stream->buffer_size);
+ } else {
+ new_buffer = i_malloc(stream->buffer_size);
+ if (old_size > 0) {
+ i_assert(stream->w_buffer != NULL);
+ memcpy(new_buffer, stream->w_buffer, old_size);
+ }
+ if (stream->memarea != NULL)
+ memarea_unref(&stream->memarea);
+ }
+
+ stream->w_buffer = new_buffer;
+ stream->buffer = new_buffer;
+
+ stream->memarea = memarea_init(stream->w_buffer, stream->buffer_size,
+ i_stream_w_buffer_free, new_buffer);
+}
+
+void i_stream_grow_buffer(struct istream_private *stream, size_t bytes)
+{
+ size_t old_size, max_size;
+
+ old_size = stream->buffer_size;
+
+ stream->buffer_size = stream->pos + bytes;
+ if (stream->buffer_size <= stream->init_buffer_size)
+ stream->buffer_size = stream->init_buffer_size;
+ else
+ stream->buffer_size = nearest_power(stream->buffer_size);
+
+ max_size = i_stream_get_max_buffer_size(&stream->istream);
+ i_assert(max_size > 0);
+ if (stream->buffer_size > max_size)
+ stream->buffer_size = max_size;
+
+ if (stream->buffer_size <= old_size)
+ stream->buffer_size = old_size;
+ else
+ i_stream_w_buffer_realloc(stream, old_size);
+}
+
+bool i_stream_try_alloc(struct istream_private *stream,
+ size_t wanted_size, size_t *size_r)
+{
+ i_assert(wanted_size > 0);
+ i_assert(stream->buffer_size >= stream->pos);
+
+ if (wanted_size > stream->buffer_size - stream->pos) {
+ if (stream->skip > 0) {
+ /* remove the unused bytes from beginning of buffer */
+ if (stream->memarea != NULL &&
+ memarea_get_refcount(stream->memarea) > 1) {
+ /* The memarea is still referenced. We can't
+ overwrite data until extra references are
+ gone. */
+ i_stream_w_buffer_realloc(stream, stream->buffer_size);
+ }
+ i_stream_compress(stream);
+ } else if (stream->buffer_size < i_stream_get_max_buffer_size(&stream->istream)) {
+ /* buffer is full - grow it */
+ i_stream_grow_buffer(stream, I_STREAM_MIN_SIZE);
+ }
+ }
+
+ if (stream->data_limit == 0 ||
+ (stream->buffer_size - stream->skip) < stream->data_limit)
+ *size_r = stream->buffer_size - stream->pos;
+ else {
+ size_t buffered = (stream->pos - stream->skip);
+
+ if (buffered >= stream->data_limit)
+ *size_r = 0;
+ else
+ *size_r = stream->data_limit - buffered;
+ }
+ i_assert(stream->w_buffer != NULL || *size_r == 0);
+ return *size_r > 0;
+}
+
+bool ATTR_NOWARN_UNUSED_RESULT
+i_stream_try_alloc_avoid_compress(struct istream_private *stream,
+ size_t wanted_size, size_t *size_r)
+{
+ size_t old_skip = stream->skip;
+
+ /* try first with skip=0, so no compression is done */
+ stream->skip = 0;
+ bool ret = i_stream_try_alloc(stream, wanted_size, size_r);
+ stream->skip = old_skip;
+ if (ret || old_skip == 0)
+ return ret;
+ /* it's full. try with compression. */
+ return i_stream_try_alloc(stream, wanted_size, size_r);
+}
+
+void *i_stream_alloc(struct istream_private *stream, size_t size)
+{
+ size_t old_size, avail_size;
+
+ (void)i_stream_try_alloc(stream, size, &avail_size);
+ if (avail_size < size) {
+ old_size = stream->buffer_size;
+ stream->buffer_size = nearest_power(stream->pos + size);
+ i_stream_w_buffer_realloc(stream, old_size);
+
+ (void)i_stream_try_alloc(stream, size, &avail_size);
+ i_assert(avail_size >= size);
+ }
+ return stream->w_buffer + stream->pos;
+}
+
+void i_stream_memarea_detach(struct istream_private *stream)
+{
+ if (stream->memarea != NULL) {
+ /* Don't overwrite data in a snapshot. Allocate a new
+ buffer instead. */
+ memarea_unref(&stream->memarea);
+ stream->buffer_size = 0;
+ stream->buffer = NULL;
+ stream->w_buffer = NULL;
+ }
+}
+
+bool i_stream_add_data(struct istream *_stream, const unsigned char *data,
+ size_t size)
+{
+ struct istream_private *stream = _stream->real_stream;
+ size_t size2;
+
+ (void)i_stream_try_alloc(stream, size, &size2);
+ if (size > size2)
+ return FALSE;
+
+ memcpy(stream->w_buffer + stream->pos, data, size);
+ stream->pos += size;
+ return TRUE;
+}
+
+struct istream *i_stream_get_root_io(struct istream *stream)
+{
+ while (stream->real_stream->parent != NULL) {
+ i_assert(stream->real_stream->io == NULL);
+ stream = stream->real_stream->parent;
+ }
+ return stream;
+}
+
+void i_stream_set_input_pending(struct istream *stream, bool pending)
+{
+ if (!pending)
+ return;
+
+ stream = i_stream_get_root_io(stream);
+ if (stream->real_stream->io != NULL)
+ io_set_pending(stream->real_stream->io);
+ else
+ stream->real_stream->io_pending = TRUE;
+}
+
+void i_stream_switch_ioloop_to(struct istream *stream, struct ioloop *ioloop)
+{
+ io_stream_switch_ioloop_to(&stream->real_stream->iostream, ioloop);
+
+ do {
+ if (stream->real_stream->switch_ioloop_to != NULL) {
+ stream->real_stream->switch_ioloop_to(
+ stream->real_stream, ioloop);
+ }
+ stream = stream->real_stream->parent;
+ } while (stream != NULL);
+}
+
+void i_stream_switch_ioloop(struct istream *stream)
+{
+ i_stream_switch_ioloop_to(stream, current_ioloop);
+}
+
+void i_stream_set_io(struct istream *stream, struct io *io)
+{
+ stream = i_stream_get_root_io(stream);
+
+ i_assert(stream->real_stream->io == NULL);
+ stream->real_stream->io = io;
+ if (stream->real_stream->io_pending) {
+ io_set_pending(io);
+ stream->real_stream->io_pending = FALSE;
+ }
+}
+
+void i_stream_unset_io(struct istream *stream, struct io *io)
+{
+ stream = i_stream_get_root_io(stream);
+
+ i_assert(stream->real_stream->io == io);
+ if (io_is_pending(io))
+ stream->real_stream->io_pending = TRUE;
+ stream->real_stream->io = NULL;
+}
+
+static void
+i_stream_default_set_max_buffer_size(struct iostream_private *stream,
+ size_t max_size)
+{
+ struct istream_private *_stream =
+ container_of(stream, struct istream_private, iostream);
+
+ _stream->max_buffer_size = max_size;
+ if (_stream->parent != NULL)
+ i_stream_set_max_buffer_size(_stream->parent, max_size);
+}
+
+static void i_stream_default_close(struct iostream_private *stream,
+ bool close_parent)
+{
+ struct istream_private *_stream =
+ container_of(stream, struct istream_private, iostream);
+
+ if (close_parent)
+ i_stream_close(_stream->parent);
+}
+
+static void i_stream_default_destroy(struct iostream_private *stream)
+{
+ struct istream_private *_stream =
+ container_of(stream, struct istream_private, iostream);
+
+ i_stream_free_buffer(_stream);
+ i_stream_unref(&_stream->parent);
+}
+
+static void
+i_stream_default_seek_seekable(struct istream_private *stream,
+ uoff_t v_offset, bool mark ATTR_UNUSED)
+{
+ stream->istream.v_offset = v_offset;
+ stream->skip = stream->pos = 0;
+}
+
+void i_stream_default_seek_nonseekable(struct istream_private *stream,
+ uoff_t v_offset, bool mark ATTR_UNUSED)
+{
+ size_t available;
+
+ if (stream->istream.v_offset > v_offset)
+ i_panic("stream %s doesn't support seeking backwards",
+ i_stream_get_name(&stream->istream));
+
+ while (stream->istream.v_offset < v_offset) {
+ (void)i_stream_read(&stream->istream);
+
+ available = stream->pos - stream->skip;
+ if (available == 0) {
+ if (stream->istream.stream_errno != 0) {
+ /* read failed */
+ return;
+ }
+ io_stream_set_error(&stream->iostream,
+ "Can't seek to offset %"PRIuUOFF_T
+ ", because we have data only up to offset %"
+ PRIuUOFF_T" (eof=%d)", v_offset,
+ stream->istream.v_offset, stream->istream.eof ? 1 : 0);
+ stream->istream.stream_errno = ESPIPE;
+ return;
+ }
+ if (available <= v_offset - stream->istream.v_offset)
+ i_stream_skip(&stream->istream, available);
+ else {
+ i_stream_skip(&stream->istream,
+ v_offset - stream->istream.v_offset);
+ }
+ }
+}
+
+bool i_stream_nonseekable_try_seek(struct istream_private *stream,
+ uoff_t v_offset)
+{
+ uoff_t start_offset = stream->istream.v_offset - stream->skip;
+
+ if (v_offset < start_offset) {
+ /* have to seek backwards */
+ i_stream_seek(stream->parent, stream->parent_start_offset);
+ stream->parent_expected_offset = stream->parent_start_offset;
+ stream->skip = stream->pos = 0;
+ stream->istream.v_offset = 0;
+ stream->high_pos = 0;
+ return FALSE;
+ }
+
+ if (v_offset <= start_offset + stream->pos) {
+ /* seeking backwards within what's already cached */
+ stream->skip = v_offset - start_offset;
+ stream->istream.v_offset = v_offset;
+ if (stream->high_pos == 0)
+ stream->high_pos = stream->pos;
+ stream->pos = stream->skip;
+ } else {
+ /* read forward */
+ i_stream_default_seek_nonseekable(stream, v_offset, FALSE);
+ }
+ return TRUE;
+}
+
+static int
+seekable_i_stream_get_size(struct istream_private *stream)
+{
+ if (stream->cached_stream_size == UOFF_T_MAX) {
+ uoff_t old_offset = stream->istream.v_offset;
+ ssize_t ret;
+
+ do {
+ i_stream_skip(&stream->istream,
+ i_stream_get_data_size(&stream->istream));
+ } while ((ret = i_stream_read(&stream->istream)) > 0);
+ i_assert(ret == -1);
+ if (stream->istream.stream_errno != 0)
+ return -1;
+
+ stream->cached_stream_size = stream->istream.v_offset;
+ i_stream_seek(&stream->istream, old_offset);
+ }
+ stream->statbuf.st_size = stream->cached_stream_size;
+ return 0;
+}
+
+static int
+i_stream_default_stat(struct istream_private *stream, bool exact)
+{
+ const struct stat *st;
+
+ if (stream->parent == NULL)
+ return stream->istream.stream_errno == 0 ? 0 : -1;
+
+ if (i_stream_stat(stream->parent, exact, &st) < 0) {
+ stream->istream.stream_errno = stream->parent->stream_errno;
+ return -1;
+ }
+ stream->statbuf = *st;
+ if (exact && !stream->stream_size_passthrough) {
+ /* exact size is not known, even if parent returned something */
+ stream->statbuf.st_size = -1;
+ if (stream->istream.seekable) {
+ if (seekable_i_stream_get_size(stream) < 0)
+ return -1;
+ }
+ } else {
+ /* When exact=FALSE always return the parent stat's size, even
+ if we know the exact value. This is necessary because
+ otherwise e.g. mbox code can see two different values and
+ think that the mbox file keeps changing. */
+ }
+ return 0;
+}
+
+static int
+i_stream_default_get_size(struct istream_private *stream,
+ bool exact, uoff_t *size_r)
+{
+ if (stream->stat(stream, exact) < 0)
+ return -1;
+ if (stream->statbuf.st_size == -1)
+ return 0;
+
+ *size_r = stream->statbuf.st_size;
+ return 1;
+}
+
+void i_stream_init_parent(struct istream_private *_stream,
+ struct istream *parent)
+{
+ _stream->access_counter = parent->real_stream->access_counter;
+ _stream->parent = parent;
+ _stream->parent_start_offset = parent->v_offset;
+ _stream->parent_expected_offset = parent->v_offset;
+ _stream->start_offset = parent->v_offset;
+ /* if parent stream is an istream-error, copy the error */
+ _stream->istream.stream_errno = parent->stream_errno;
+ _stream->istream.eof = parent->eof;
+ i_stream_ref(parent);
+}
+
+struct istream *
+i_stream_create(struct istream_private *_stream, struct istream *parent, int fd,
+ enum istream_create_flag flags)
+{
+ bool noop_snapshot = (flags & ISTREAM_CREATE_FLAG_NOOP_SNAPSHOT) != 0;
+
+ _stream->fd = fd;
+ if (parent != NULL)
+ i_stream_init_parent(_stream, parent);
+ else if (_stream->memarea == NULL && !noop_snapshot) {
+ /* The stream has no parent and no memarea yet. We'll assume
+ that it wants to be using memareas for the reads. */
+ _stream->memarea = memarea_init_empty();
+ }
+ _stream->istream.real_stream = _stream;
+
+ if (_stream->iostream.close == NULL)
+ _stream->iostream.close = i_stream_default_close;
+ if (_stream->iostream.destroy == NULL)
+ _stream->iostream.destroy = i_stream_default_destroy;
+ if (_stream->seek == NULL) {
+ _stream->seek = _stream->istream.seekable ?
+ i_stream_default_seek_seekable :
+ i_stream_default_seek_nonseekable;
+ }
+ if (_stream->stat == NULL)
+ _stream->stat = i_stream_default_stat;
+ if (_stream->get_size == NULL)
+ _stream->get_size = i_stream_default_get_size;
+ if (_stream->snapshot == NULL) {
+ _stream->snapshot = noop_snapshot ?
+ i_stream_noop_snapshot :
+ i_stream_default_snapshot;
+ }
+ if (_stream->iostream.set_max_buffer_size == NULL) {
+ _stream->iostream.set_max_buffer_size =
+ i_stream_default_set_max_buffer_size;
+ }
+ if (_stream->init_buffer_size == 0)
+ _stream->init_buffer_size = I_STREAM_MIN_SIZE;
+
+ i_zero(&_stream->statbuf);
+ _stream->statbuf.st_size = -1;
+ _stream->statbuf.st_atime =
+ _stream->statbuf.st_mtime =
+ _stream->statbuf.st_ctime = ioloop_time;
+ _stream->cached_stream_size = UOFF_T_MAX;
+
+ io_stream_init(&_stream->iostream);
+
+ if (_stream->istream.stream_errno != 0)
+ _stream->istream.eof = TRUE;
+
+ return &_stream->istream;
+}
+
+struct istream *i_stream_create_error(int stream_errno)
+{
+ struct istream_private *stream;
+
+ stream = i_new(struct istream_private, 1);
+ stream->istream.closed = TRUE;
+ stream->istream.readable_fd = FALSE;
+ stream->istream.blocking = TRUE;
+ stream->istream.seekable = TRUE;
+ stream->istream.eof = TRUE;
+ stream->istream.stream_errno = stream_errno;
+ /* Nothing can ever actually be read from this stream, but set a
+ reasonable max_buffer_size anyway since some filter istreams don't
+ behave properly otherwise. */
+ stream->max_buffer_size = IO_BLOCK_SIZE;
+ i_stream_create(stream, NULL, -1, 0);
+ i_stream_set_name(&stream->istream, "(error)");
+ return &stream->istream;
+}
+
+struct istream *
+i_stream_create_error_str(int stream_errno, const char *fmt, ...)
+{
+ struct istream *input;
+ va_list args;
+
+ va_start(args, fmt);
+ input = i_stream_create_error(stream_errno);
+ io_stream_set_verror(&input->real_stream->iostream, fmt, args);
+ va_end(args);
+ return input;
+}
diff --git a/src/lib/istream.h b/src/lib/istream.h
new file mode 100644
index 0000000..671d3ff
--- /dev/null
+++ b/src/lib/istream.h
@@ -0,0 +1,255 @@
+#ifndef ISTREAM_H
+#define ISTREAM_H
+
+/* Note that some systems (Solaris) may use a macro to redefine struct stat */
+#include <sys/stat.h>
+
+struct ioloop;
+
+struct istream {
+ uoff_t v_offset;
+
+ /* Commonly used errors:
+
+ ENOENT - File/object doesn't exist.
+ EPIPE - Stream ended unexpectedly (or i_stream_close() was called).
+ ESPIPE - i_stream_seek() was used on a stream that can't be seeked.
+ ENOBUFS - i_stream_read_next_line() was used for a too long line.
+ EIO - Internal error. Retrying may work, but it may also be
+ because of a misconfiguration.
+ EINVAL - Stream is corrupted.
+
+ If stream_errno != 0, eof==TRUE as well.
+ */
+ int stream_errno;
+
+ bool mmaped:1; /* be careful when copying data */
+ bool blocking:1; /* read() shouldn't return 0 */
+ bool closed:1;
+ bool readable_fd:1; /* fd can be read directly if necessary
+ (for sendfile()) */
+ bool seekable:1; /* we can seek() backwards */
+ /* read() has reached to end of file (but there may still be data
+ available in buffer) or stream_errno != 0 */
+ bool eof:1;
+
+ struct istream_private *real_stream;
+};
+
+typedef void istream_callback_t(void *context);
+
+struct istream *i_stream_create_fd(int fd, size_t max_buffer_size);
+/* The fd is set to -1 immediately to avoid accidentally closing it twice. */
+struct istream *i_stream_create_fd_autoclose(int *fd, size_t max_buffer_size);
+/* Open the given path only when something is actually tried to be read from
+ the stream. */
+struct istream *i_stream_create_file(const char *path, size_t max_buffer_size);
+/* Create an input stream using the provided data block. That data block must
+remain allocated during the full lifetime of the stream. */
+struct istream *i_stream_create_from_data(const void *data, size_t size);
+#define i_stream_create_from_buffer(buf) \
+ i_stream_create_from_data((buf)->data, (buf)->used)
+#define i_stream_create_from_string(str) \
+ i_stream_create_from_data(str_data(str), str_len(str))
+/* Create an input stream using a copy of the provided data block. The
+ provided data block may be freed at any time. The copy is freed when the
+ stream is destroyed. */
+struct istream *
+i_stream_create_copy_from_data(const void *data, size_t size);
+#define i_stream_create_copy_from_buffer(buf) \
+ i_stream_create_copy_from_data((buf)->data, (buf)->used)
+#define i_stream_create_copy_from_string(str) \
+ i_stream_create_copy_from_data(str_data(str), str_len(str))
+struct istream *i_stream_create_limit(struct istream *input, uoff_t v_size);
+struct istream *i_stream_create_range(struct istream *input,
+ uoff_t v_offset, uoff_t v_size);
+struct istream *i_stream_create_error(int stream_errno);
+struct istream *
+i_stream_create_error_str(int stream_errno, const char *fmt, ...)
+ ATTR_FORMAT(2, 3);
+
+/* Set name (e.g. path) for input stream. */
+void i_stream_set_name(struct istream *stream, const char *name);
+/* Get input stream's name. If stream itself doesn't have a name,
+ it looks up further into stream's parents until one of them has a name.
+ Returns "" if stream has no name. */
+const char *i_stream_get_name(struct istream *stream);
+
+/* Close this stream (but not its parents) and unreference it. */
+void i_stream_destroy(struct istream **stream);
+
+/* Reference counting. References start from 1, so calling i_stream_unref()
+ destroys the stream if i_stream_ref() is never used. */
+void i_stream_ref(struct istream *stream);
+/* Unreferences the stream and sets stream pointer to NULL. */
+void i_stream_unref(struct istream **stream);
+/* Call the given callback function when stream is destroyed. */
+void i_stream_add_destroy_callback(struct istream *stream,
+ istream_callback_t *callback, void *context)
+ ATTR_NULL(3);
+#define i_stream_add_destroy_callback(stream, callback, context) \
+ i_stream_add_destroy_callback(stream - \
+ CALLBACK_TYPECHECK(callback, void (*)(typeof(context))), \
+ (istream_callback_t *)callback, context)
+/* Remove the destroy callback. */
+void i_stream_remove_destroy_callback(struct istream *stream,
+ void (*callback)());
+
+/* Return file descriptor for stream, or -1 if none is available. */
+int i_stream_get_fd(struct istream *stream);
+/* Copy the file descriptor from source istream to destination istream.
+ The readable_fd is preserved. Assert-crashes if source doesn't have a
+ file descriptor. */
+void i_stream_copy_fd(struct istream *dest, struct istream *source);
+/* Returns error string for the last error. It also returns "EOF" in case there
+ is no error, but eof is set. Otherwise it returns "<no error>". */
+const char *i_stream_get_error(struct istream *stream);
+/* Returns human-readable reason for why istream was disconnected.
+ The output is either "Connection closed" for clean disconnections or
+ "Connection closed: <error>" for unclean disconnections. This is an
+ alternative to i_stream_get_error(), which is preferred to be used when
+ logging errors about client connections. */
+const char *i_stream_get_disconnect_reason(struct istream *stream);
+
+/* Mark the stream and all of its parent streams closed. Any reads after this
+ will return -1. The data already read can still be used. */
+void i_stream_close(struct istream *stream);
+/* Sync the stream with the underlying backend, ie. if a file has been
+ modified, flush any cached data. */
+void i_stream_sync(struct istream *stream);
+
+/* Change the initial size for stream's input buffer. This basically just
+ grows the read buffer size from the default. This function has no effect
+ unless it's called before reading anything. */
+void i_stream_set_init_buffer_size(struct istream *stream, size_t size);
+/* Change the maximum size for stream's input buffer to grow. Useful only
+ for buffered streams (currently only file). This changes also all the
+ parent streams' max buffer size. */
+void i_stream_set_max_buffer_size(struct istream *stream, size_t max_size);
+/* Returns the current max. buffer size for the stream. This function also
+ goes through all of the parent streams and returns the highest seen max
+ buffer size. This is needed because some streams (e.g. istream-chain) change
+ their max buffer size dynamically. */
+size_t i_stream_get_max_buffer_size(struct istream *stream);
+/* Enable/disable i_stream[_read]_next_line() returning the last line if it
+ doesn't end with LF. */
+void i_stream_set_return_partial_line(struct istream *stream, bool set);
+/* Change whether buffers are allocated persistently (default=TRUE). When not,
+ the memory usage is minimized by freeing the stream's buffers whenever they
+ become empty. */
+void i_stream_set_persistent_buffers(struct istream *stream, bool set);
+/* Set the istream blocking or nonblocking, including its parent streams.
+ If any of the istreams have an fd, its O_NONBLOCK flag is changed. */
+void i_stream_set_blocking(struct istream *stream, bool blocking);
+
+/* Returns number of bytes read if read was ok, 0 if stream is non-blocking and
+ no more data is available, -1 if EOF or error, -2 if the input buffer is
+ full. If <=0 is returned, pointers to existing data returned by the previous
+ i_stream_get_data() will stay valid, although calling it again may return
+ a different pointer. The pointers to old data are invalidated again when
+ return value is >0. */
+ssize_t i_stream_read(struct istream *stream);
+/* Skip forward a number of bytes. Never fails, the next read tells if it
+ was successful. */
+void i_stream_skip(struct istream *stream, uoff_t count);
+/* Seek to specified position from beginning of file. Never fails, the next
+ read tells if it was successful. This works only for files, others will
+ set stream_errno=ESPIPE. */
+void i_stream_seek(struct istream *stream, uoff_t v_offset);
+/* Like i_stream_seek(), but also giving a hint that after reading some data
+ we could be seeking back to this mark or somewhere after it. If input
+ stream's implementation is slow in seeking backwards, it can use this hint
+ to cache some of the data in memory. */
+void i_stream_seek_mark(struct istream *stream, uoff_t v_offset);
+/* Returns 0 if ok, -1 if error. As the underlying stream may not be
+ a file, only some of the fields might be set, others would be zero.
+ st_size is always set, and if it's not known, it's -1.
+
+ If exact=FALSE, the stream may not return exactly correct values, but the
+ returned values can be compared to see if anything had changed (eg. in
+ compressed stream st_size could be compressed size) */
+int i_stream_stat(struct istream *stream, bool exact, const struct stat **st_r);
+/* Similar to i_stream_stat() call. Returns 1 if size was successfully
+ set, 0 if size is unknown, -1 if error. */
+int i_stream_get_size(struct istream *stream, bool exact, uoff_t *size_r);
+/* Returns TRUE if there are any bytes left to be read or in buffer. */
+bool i_stream_have_bytes_left(struct istream *stream);
+/* Returns TRUE if there are no bytes currently buffered and i_stream_read()
+ returns EOF/error. Usually it's enough to check for stream->eof instead of
+ calling this function. Note that if the stream isn't at EOF, this function
+ has now read data into the stream buffer. */
+bool i_stream_read_eof(struct istream *stream);
+/* Returns the absolute offset of the stream. This is the stream's current
+ v_offset + the parent's absolute offset when the stream was created. */
+uoff_t i_stream_get_absolute_offset(struct istream *stream);
+
+/* Gets the next line from stream and returns it, or NULL if more data is
+ needed to make a full line. i_stream_set_return_partial_line() specifies
+ if the last line should be returned if it doesn't end with LF. */
+char *i_stream_next_line(struct istream *stream);
+/* Like i_stream_next_line(), but reads for more data if needed. Returns NULL
+ if more data is needed or error occurred. If the input buffer gets full,
+ stream_errno is set to ENOBUFS. */
+char *i_stream_read_next_line(struct istream *stream);
+/* Returns TRUE if the last line read with i_stream_next_line() ended with
+ CRLF (instead of LF). */
+bool i_stream_last_line_crlf(struct istream *stream);
+
+/* Returns pointer to beginning of read data. */
+const unsigned char *i_stream_get_data(struct istream *stream, size_t *size_r);
+size_t i_stream_get_data_size(struct istream *stream);
+/* Like i_stream_get_data(), but returns non-const data. This only works with
+ buffered streams (currently only file), others return NULL. */
+unsigned char *i_stream_get_modifiable_data(struct istream *stream,
+ size_t *size_r);
+/* Like i_stream_get_data(), but read more when needed. Returns 1 if more
+ than threshold bytes are available, 0 if as much or less, -1 if error or
+ EOF with no bytes read that weren't already in buffer, or -2 if stream's
+ input buffer is full. */
+int i_stream_read_data(struct istream *stream, const unsigned char **data_r,
+ size_t *size_r, size_t threshold);
+/* Like i_stream_get_data(), but read more when needed. Returns 1 if at least
+ the wanted number of bytes are available, 0 if less, -1 if error or
+ EOF with no bytes read that weren't already in buffer, or -2 if stream's
+ input buffer is full. */
+static inline int
+i_stream_read_bytes(struct istream *stream, const unsigned char **data_r,
+ size_t *size_r, size_t wanted)
+{
+ i_assert(wanted > 0);
+ return i_stream_read_data(stream, data_r, size_r, wanted - 1);
+}
+/* Short-hand for just requesting more data (i.e. even one byte) */
+static inline int
+i_stream_read_more(struct istream *stream, const unsigned char **data_r,
+ size_t *size_r)
+{
+ int ret = i_stream_read_bytes(stream, data_r, size_r, 1);
+ i_assert(ret != -2); /* stream must have space for at least 1 byte */
+ return ret;
+}
+/* Like i_stream_read_more(), but tries to avoid buffering more than the
+ indicated limit. Use this function to prevent growing the stream buffer
+ beyond what the application is willing to read immediately. Since this
+ function doesn't fully prevent buffering beyond the limit, the amount of data
+ actually buffered can exceed the limit. However, *size_r will allways be <=
+ limit to avoid confusion. */
+int i_stream_read_limited(struct istream *stream, const unsigned char **data_r,
+ size_t *size_r, size_t limit);
+/* Return the timestamp when istream last successfully read something.
+ The timestamp is 0 if nothing has ever been read. */
+void i_stream_get_last_read_time(struct istream *stream, struct timeval *tv_r);
+
+/* Append external data to input stream. Returns TRUE if successful, FALSE if
+ there is not enough space in the stream. */
+bool i_stream_add_data(struct istream *stream, const unsigned char *data,
+ size_t size);
+
+void i_stream_set_input_pending(struct istream *stream, bool pending);
+
+/* If there are any I/O loop items associated with the stream, move all of
+ them to provided/current ioloop. */
+void i_stream_switch_ioloop_to(struct istream *stream, struct ioloop *ioloop);
+void i_stream_switch_ioloop(struct istream *stream);
+
+#endif
diff --git a/src/lib/json-parser.c b/src/lib/json-parser.c
new file mode 100644
index 0000000..a4fb186
--- /dev/null
+++ b/src/lib/json-parser.c
@@ -0,0 +1,850 @@
+/* Copyright (c) 2013-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "str.h"
+#include "istream.h"
+#include "hex-dec.h"
+#include "unichar.h"
+#include "istream-jsonstr.h"
+#include "json-parser.h"
+
+enum json_state {
+ JSON_STATE_ROOT = 0,
+ JSON_STATE_OBJECT_OPEN,
+ JSON_STATE_OBJECT_KEY,
+ JSON_STATE_OBJECT_COLON,
+ JSON_STATE_OBJECT_VALUE,
+ JSON_STATE_OBJECT_SKIP_STRING,
+ JSON_STATE_OBJECT_NEXT,
+ JSON_STATE_ARRAY_OPEN,
+ JSON_STATE_ARRAY_VALUE,
+ JSON_STATE_ARRAY_SKIP_STRING,
+ JSON_STATE_ARRAY_NEXT,
+ JSON_STATE_ARRAY_NEXT_SKIP,
+ JSON_STATE_VALUE,
+ JSON_STATE_DONE
+};
+
+struct json_parser {
+ pool_t pool;
+ struct istream *input;
+ uoff_t highwater_offset;
+ enum json_parser_flags flags;
+
+ const unsigned char *start, *end, *data;
+ const char *error;
+ string_t *value;
+ struct istream *strinput;
+
+ enum json_state state;
+ ARRAY(enum json_state) nesting;
+ unsigned int nested_skip_count;
+
+ bool skipping;
+ bool seen_eof;
+};
+
+static int json_parser_read_more(struct json_parser *parser)
+{
+ uoff_t cur_highwater = parser->input->v_offset +
+ i_stream_get_data_size(parser->input);
+ size_t size;
+ ssize_t ret;
+
+ i_assert(parser->highwater_offset <= cur_highwater);
+
+ if (parser->error != NULL)
+ return -1;
+
+ if (parser->highwater_offset == cur_highwater) {
+ ret = i_stream_read(parser->input);
+ if (ret == -2) {
+ parser->error = "Token too large";
+ return -1;
+ }
+ if (ret < 0 && !parser->seen_eof &&
+ i_stream_get_data_size(parser->input) > 0 &&
+ parser->input->stream_errno == 0) {
+ /* call it once more to finish any pending number */
+ parser->seen_eof = TRUE;
+ } else if (ret <= 0) {
+ return ret;
+ } else {
+ cur_highwater = parser->input->v_offset +
+ i_stream_get_data_size(parser->input);
+ i_assert(parser->highwater_offset < cur_highwater);
+ parser->highwater_offset = cur_highwater;
+ }
+ }
+
+ parser->start = parser->data = i_stream_get_data(parser->input, &size);
+ parser->end = parser->start + size;
+ i_assert(size > 0);
+ return 1;
+}
+
+static void json_parser_update_input_pos(struct json_parser *parser)
+{
+ size_t size;
+
+ if (parser->data == parser->start)
+ return;
+
+ i_stream_skip(parser->input, parser->data - parser->start);
+ parser->start = parser->data = i_stream_get_data(parser->input, &size);
+ parser->end = parser->start + size;
+ if (size > 0) {
+ /* we skipped over some data and there's still data left.
+ no need to read() the next time. */
+ parser->highwater_offset = 0;
+ } else {
+ parser->highwater_offset = parser->input->v_offset;
+ }
+}
+
+struct json_parser *json_parser_init(struct istream *input)
+{
+ return json_parser_init_flags(input, 0);
+}
+
+struct json_parser *json_parser_init_flags(struct istream *input,
+ enum json_parser_flags flags)
+{
+ struct json_parser *parser;
+ pool_t pool = pool_alloconly_create("json parser",
+ sizeof(struct json_parser)+64);
+
+ parser = p_new(pool, struct json_parser, 1);
+ parser->pool = pool;
+ parser->input = input;
+ parser->flags = flags;
+ parser->value = str_new(default_pool, 128);
+ i_array_init(&parser->nesting, 8);
+ i_stream_ref(input);
+
+ if ((flags & JSON_PARSER_NO_ROOT_OBJECT) != 0)
+ parser->state = JSON_STATE_VALUE;
+ return parser;
+}
+
+int json_parser_deinit(struct json_parser **_parser, const char **error_r)
+{
+ struct json_parser *parser = *_parser;
+
+ *_parser = NULL;
+
+ if (parser->error != NULL) {
+ /* actual parser error */
+ *error_r = t_strdup(parser->error);
+ } else if (parser->input->stream_errno != 0) {
+ *error_r = t_strdup_printf("read(%s) failed: %s",
+ i_stream_get_name(parser->input),
+ i_stream_get_error(parser->input));
+ } else if (parser->data == parser->end &&
+ !i_stream_have_bytes_left(parser->input) &&
+ parser->state != JSON_STATE_DONE) {
+ *error_r = "Missing '}'";
+ } else {
+ *error_r = NULL;
+ }
+
+ i_stream_unref(&parser->input);
+ array_free(&parser->nesting);
+ str_free(&parser->value);
+ pool_unref(&parser->pool);
+ return *error_r != NULL ? -1 : 0;
+}
+
+static bool json_parse_whitespace(struct json_parser *parser)
+{
+ for (; parser->data != parser->end; parser->data++) {
+ switch (*parser->data) {
+ case ' ':
+ case '\t':
+ case '\r':
+ case '\n':
+ break;
+ default:
+ json_parser_update_input_pos(parser);
+ return TRUE;
+ }
+ }
+ json_parser_update_input_pos(parser);
+ return FALSE;
+}
+
+static int json_skip_string(struct json_parser *parser)
+{
+ for (; parser->data != parser->end; parser->data++) {
+ if (*parser->data == '"') {
+ parser->data++;
+ json_parser_update_input_pos(parser);
+ return 1;
+ }
+ if (*parser->data == '\\') {
+ parser->data++;
+ if (parser->data == parser->end)
+ break;
+ switch (*parser->data) {
+ case '"':
+ case '\\':
+ case '/':
+ case 'b':
+ case 'f':
+ case 'n':
+ case 'r':
+ case 't':
+ break;
+ case 'u':
+ if (parser->end - parser->data < 4) {
+ parser->data = parser->end;
+ return -1;
+ }
+ parser->data += 3;
+ break;
+ default:
+ parser->error = "Invalid escape string";
+ return -1;
+ }
+ }
+ }
+ json_parser_update_input_pos(parser);
+ return 0;
+}
+
+static int json_parse_unicode_escape(struct json_parser *parser)
+{
+ char chbuf[5] = {0};
+ unichar_t chr, hi_surg;
+
+ parser->data++;
+ if (parser->end - parser->data < 4) {
+ /* wait for more data */
+ parser->data = parser->end;
+ return 0;
+ }
+ memcpy(chbuf, parser->data, 4);
+ if (str_to_uint32_hex(chbuf, &chr) < 0) {
+ parser->error = "Invalid unicode escape seen";
+ return -1;
+ }
+ if (UTF16_VALID_HIGH_SURROGATE(chr)) {
+ /* possible surrogate pair */
+ hi_surg = chr;
+ chr = 0;
+ parser->data += 4;
+ if (parser->data >= parser->end) {
+ /* wait for more data */
+ parser->data = parser->end;
+ return 0;
+ }
+ if ((parser->end - parser->data) < 2) {
+ if (parser->data[0] == '\\') {
+ /* wait for more data */
+ parser->data = parser->end;
+ return 0;
+ }
+ /* error */
+ }
+ if ((parser->end - parser->data) < 6) {
+ if (parser->data[0] == '\\' &&
+ parser->data[1] == 'u') {
+ /* wait for more data */
+ parser->data = parser->end;
+ return 0;
+ }
+ /* error */
+ } else {
+ memcpy(chbuf, &parser->data[2], 4);
+ if (str_to_uint32_hex(chbuf, &chr) < 0) {
+ parser->error = "Invalid unicode escape seen";
+ return -1;
+ }
+ }
+ if (parser->data[0] != '\\' || parser->data[1] != 'u' ||
+ !UTF16_VALID_LOW_SURROGATE(chr)) {
+ parser->error = p_strdup_printf(parser->pool,
+ "High surrogate 0x%04x seen, "
+ "but not followed by low surrogate", hi_surg);
+ return -1;
+ }
+ chr = uni_join_surrogate(hi_surg, chr);
+ parser->data += 2;
+ }
+
+ if (!uni_is_valid_ucs4(chr)) {
+ parser->error = p_strdup_printf(parser->pool,
+ "Invalid unicode character U+%04x", chr);
+ return -1;
+ }
+ if (chr == 0) {
+ parser->error = "\\u0000 not supported in strings";
+ return -1;
+ }
+ uni_ucs4_to_utf8_c(chr, parser->value);
+ parser->data += 3;
+ return 1;
+}
+
+static int json_parse_string(struct json_parser *parser, bool allow_skip,
+ const char **value_r)
+{
+ int ret;
+
+ if (*parser->data != '"')
+ return -1;
+ parser->data++;
+
+ if (parser->skipping && allow_skip) {
+ *value_r = NULL;
+ return json_skip_string(parser);
+ }
+
+ str_truncate(parser->value, 0);
+ for (; parser->data != parser->end; parser->data++) {
+ if (*parser->data == '"') {
+ parser->data++;
+ *value_r = str_c(parser->value);
+ return 1;
+ }
+ switch (*parser->data) {
+ case '\\':
+ if (++parser->data == parser->end)
+ return 0;
+ switch (*parser->data) {
+ case '"':
+ case '\\':
+ case '/':
+ str_append_c(parser->value, *parser->data);
+ break;
+ case 'b':
+ str_append_c(parser->value, '\b');
+ break;
+ case 'f':
+ str_append_c(parser->value, '\f');
+ break;
+ case 'n':
+ str_append_c(parser->value, '\n');
+ break;
+ case 'r':
+ str_append_c(parser->value, '\r');
+ break;
+ case 't':
+ str_append_c(parser->value, '\t');
+ break;
+ case 'u':
+ if ((ret=json_parse_unicode_escape(parser)) <= 0)
+ return ret;
+ break;
+ default:
+ parser->error = "Invalid escape string";
+ return -1;
+ }
+ break;
+ case '\0':
+ parser->error = "NULs not supported in strings";
+ return -1;
+ default:
+ str_append_c(parser->value, *parser->data);
+ break;
+ }
+ }
+ return 0;
+}
+
+static int
+json_parse_digits(struct json_parser *parser)
+{
+ if (parser->data == parser->end)
+ return 0;
+ if (*parser->data < '0' || *parser->data > '9')
+ return -1;
+
+ while (parser->data != parser->end &&
+ *parser->data >= '0' && *parser->data <= '9')
+ str_append_c(parser->value, *parser->data++);
+ return 1;
+}
+
+static int json_parse_int(struct json_parser *parser)
+{
+ int ret;
+
+ if (*parser->data == '-') {
+ str_append_c(parser->value, *parser->data++);
+ if (parser->data == parser->end)
+ return 0;
+ }
+ if (*parser->data == '0')
+ str_append_c(parser->value, *parser->data++);
+ else {
+ if ((ret = json_parse_digits(parser)) <= 0)
+ return ret;
+ }
+ return 1;
+}
+
+static int json_parse_number(struct json_parser *parser, const char **value_r)
+{
+ int ret;
+
+ str_truncate(parser->value, 0);
+ if ((ret = json_parse_int(parser)) <= 0)
+ return ret;
+ if (parser->data != parser->end && *parser->data == '.') {
+ /* frac */
+ str_append_c(parser->value, *parser->data++);
+ if ((ret = json_parse_digits(parser)) <= 0)
+ return ret;
+ }
+ if (parser->data != parser->end &&
+ (*parser->data == 'e' || *parser->data == 'E')) {
+ /* exp */
+ str_append_c(parser->value, *parser->data++);
+ if (parser->data == parser->end)
+ return 0;
+ if (*parser->data == '+' || *parser->data == '-')
+ str_append_c(parser->value, *parser->data++);
+ if ((ret = json_parse_digits(parser)) <= 0)
+ return ret;
+ }
+ if (parser->data == parser->end && !parser->input->eof)
+ return 0;
+ *value_r = str_c(parser->value);
+ return 1;
+}
+
+static int json_parse_atom(struct json_parser *parser, const char *atom)
+{
+ size_t avail, len = strlen(atom);
+
+ avail = parser->end - parser->data;
+ if (avail < len) {
+ if (memcmp(parser->data, atom, avail) != 0)
+ return -1;
+
+ /* everything matches so far, but we need more data */
+ parser->data += avail;
+ return 0;
+ }
+ if (memcmp(parser->data, atom, len) != 0)
+ return -1;
+ parser->data += len;
+ return 1;
+}
+
+static int json_parse_denest(struct json_parser *parser)
+{
+ const enum json_state *nested_states;
+ unsigned count;
+
+ parser->data++;
+ json_parser_update_input_pos(parser);
+
+ nested_states = array_get(&parser->nesting, &count);
+ i_assert(count > 0);
+ if (count == 1) {
+ /* closing root */
+ parser->state = JSON_STATE_DONE;
+ if ((parser->flags & JSON_PARSER_NO_ROOT_OBJECT) == 0)
+ return 0;
+ /* we want to return the ending "]" or "}" to caller */
+ return 1;
+ }
+
+ /* closing a nested object */
+ parser->state = nested_states[count-2] == JSON_STATE_OBJECT_OPEN ?
+ JSON_STATE_OBJECT_NEXT : JSON_STATE_ARRAY_NEXT;
+ array_delete(&parser->nesting, count-1, 1);
+
+ if (parser->nested_skip_count > 0) {
+ parser->nested_skip_count--;
+ return 0;
+ }
+ return 1;
+}
+
+static int
+json_parse_close_object(struct json_parser *parser, enum json_type *type_r)
+{
+ if (json_parse_denest(parser) == 0)
+ return 0;
+ *type_r = JSON_TYPE_OBJECT_END;
+ return 1;
+}
+
+static int
+json_parse_close_array(struct json_parser *parser, enum json_type *type_r)
+{
+ if (json_parse_denest(parser) == 0)
+ return 0;
+ *type_r = JSON_TYPE_ARRAY_END;
+ return 1;
+}
+
+static void json_parser_object_open(struct json_parser *parser)
+{
+ parser->data++;
+ parser->state = JSON_STATE_OBJECT_OPEN;
+ array_push_back(&parser->nesting, &parser->state);
+ json_parser_update_input_pos(parser);
+}
+
+static int
+json_try_parse_next(struct json_parser *parser, enum json_type *type_r,
+ const char **value_r)
+{
+ bool skipping = parser->skipping;
+ int ret;
+
+ if (!json_parse_whitespace(parser))
+ return -1;
+
+ switch (parser->state) {
+ case JSON_STATE_ROOT:
+ if (*parser->data != '{') {
+ parser->error = "Object doesn't begin with '{'";
+ return -1;
+ }
+ json_parser_object_open(parser);
+ return 0;
+ case JSON_STATE_OBJECT_VALUE:
+ case JSON_STATE_ARRAY_VALUE:
+ case JSON_STATE_VALUE:
+ if (*parser->data == '{') {
+ json_parser_object_open(parser);
+
+ if (parser->skipping) {
+ parser->nested_skip_count++;
+ return 0;
+ }
+ *type_r = JSON_TYPE_OBJECT;
+ return 1;
+ } else if (*parser->data == '[') {
+ parser->data++;
+ parser->state = JSON_STATE_ARRAY_OPEN;
+ array_push_back(&parser->nesting, &parser->state);
+ json_parser_update_input_pos(parser);
+
+ if (parser->skipping) {
+ parser->nested_skip_count++;
+ return 0;
+ }
+ *type_r = JSON_TYPE_ARRAY;
+ return 1;
+ }
+
+ if ((ret = json_parse_string(parser, TRUE, value_r)) >= 0) {
+ *type_r = JSON_TYPE_STRING;
+ } else if ((ret = json_parse_number(parser, value_r)) >= 0) {
+ *type_r = JSON_TYPE_NUMBER;
+ } else if ((ret = json_parse_atom(parser, "true")) >= 0) {
+ *type_r = JSON_TYPE_TRUE;
+ *value_r = "true";
+ } else if ((ret = json_parse_atom(parser, "false")) >= 0) {
+ *type_r = JSON_TYPE_FALSE;
+ *value_r = "false";
+ } else if ((ret = json_parse_atom(parser, "null")) >= 0) {
+ *type_r = JSON_TYPE_NULL;
+ *value_r = NULL;
+ } else {
+ if (parser->error == NULL)
+ parser->error = "Invalid data as value";
+ return -1;
+ }
+ if (ret == 0) {
+ i_assert(parser->data == parser->end);
+ if (parser->skipping && *type_r == JSON_TYPE_STRING) {
+ /* a large string that we want to skip over. */
+ json_parser_update_input_pos(parser);
+ parser->state = parser->state == JSON_STATE_OBJECT_VALUE ?
+ JSON_STATE_OBJECT_SKIP_STRING :
+ JSON_STATE_ARRAY_SKIP_STRING;
+ return 0;
+ }
+ return -1;
+ }
+ switch (parser->state) {
+ case JSON_STATE_OBJECT_VALUE:
+ parser->state = JSON_STATE_OBJECT_NEXT;
+ break;
+ case JSON_STATE_ARRAY_VALUE:
+ parser->state = JSON_STATE_ARRAY_NEXT;
+ break;
+ case JSON_STATE_VALUE:
+ parser->state = JSON_STATE_DONE;
+ break;
+ default:
+ i_unreached();
+ }
+ break;
+ case JSON_STATE_OBJECT_OPEN:
+ if (*parser->data == '}')
+ return json_parse_close_object(parser, type_r);
+ parser->state = JSON_STATE_OBJECT_KEY;
+ /* fall through */
+ case JSON_STATE_OBJECT_KEY:
+ if (json_parse_string(parser, FALSE, value_r) <= 0) {
+ parser->error = "Expected string as object key";
+ return -1;
+ }
+ *type_r = JSON_TYPE_OBJECT_KEY;
+ parser->state = JSON_STATE_OBJECT_COLON;
+ break;
+ case JSON_STATE_OBJECT_COLON:
+ if (*parser->data != ':') {
+ parser->error = "Expected ':' after key";
+ return -1;
+ }
+ parser->data++;
+ parser->state = JSON_STATE_OBJECT_VALUE;
+ json_parser_update_input_pos(parser);
+ return 0;
+ case JSON_STATE_OBJECT_NEXT:
+ if (parser->skipping && parser->nested_skip_count == 0) {
+ /* we skipped over the previous value */
+ parser->skipping = FALSE;
+ }
+ if (*parser->data == '}')
+ return json_parse_close_object(parser, type_r);
+ if (*parser->data != ',') {
+ parser->error = "Expected ',' or '}' after object value";
+ return -1;
+ }
+ parser->state = JSON_STATE_OBJECT_KEY;
+ parser->data++;
+ json_parser_update_input_pos(parser);
+ return 0;
+ case JSON_STATE_ARRAY_OPEN:
+ if (*parser->data == ']')
+ return json_parse_close_array(parser, type_r);
+ parser->state = JSON_STATE_ARRAY_VALUE;
+ return 0;
+ case JSON_STATE_ARRAY_NEXT:
+ if (parser->skipping && parser->nested_skip_count == 0) {
+ /* we skipped over the previous value */
+ parser->skipping = FALSE;
+ }
+ /* fall through */
+ case JSON_STATE_ARRAY_NEXT_SKIP:
+ if (*parser->data == ']')
+ return json_parse_close_array(parser, type_r);
+ if (*parser->data != ',') {
+ parser->error = "Expected ',' or '}' after array value";
+ return -1;
+ }
+ parser->state = JSON_STATE_ARRAY_VALUE;
+ parser->data++;
+ json_parser_update_input_pos(parser);
+ return 0;
+ case JSON_STATE_OBJECT_SKIP_STRING:
+ case JSON_STATE_ARRAY_SKIP_STRING:
+ if (json_skip_string(parser) <= 0)
+ return -1;
+ parser->state = parser->state == JSON_STATE_OBJECT_SKIP_STRING ?
+ JSON_STATE_OBJECT_NEXT : JSON_STATE_ARRAY_NEXT;
+ return 0;
+ case JSON_STATE_DONE:
+ parser->error = "Unexpected data at the end";
+ return -1;
+ }
+ json_parser_update_input_pos(parser);
+ return skipping ? 0 : 1;
+}
+
+int json_parse_next(struct json_parser *parser, enum json_type *type_r,
+ const char **value_r)
+{
+ int ret;
+
+ i_assert(parser->strinput == NULL);
+
+ *value_r = NULL;
+
+ while ((ret = json_parser_read_more(parser)) > 0) {
+ while ((ret = json_try_parse_next(parser, type_r, value_r)) == 0)
+ ;
+ if (ret > 0)
+ break;
+ if (parser->data != parser->end)
+ return -1;
+ /* parsing probably failed because there wasn't enough input.
+ reset the error and try reading more. */
+ parser->error = NULL;
+ parser->highwater_offset = parser->input->v_offset +
+ i_stream_get_data_size(parser->input);
+ }
+ return ret;
+}
+
+void json_parse_skip_next(struct json_parser *parser)
+{
+ i_assert(!parser->skipping);
+ i_assert(parser->strinput == NULL);
+ i_assert(parser->state == JSON_STATE_OBJECT_COLON ||
+ parser->state == JSON_STATE_OBJECT_VALUE ||
+ parser->state == JSON_STATE_ARRAY_VALUE ||
+ parser->state == JSON_STATE_ARRAY_NEXT);
+
+ parser->skipping = TRUE;
+ if (parser->state == JSON_STATE_ARRAY_NEXT)
+ parser->state = JSON_STATE_ARRAY_NEXT_SKIP;
+}
+
+void json_parse_skip(struct json_parser *parser)
+{
+ i_assert(!parser->skipping);
+ i_assert(parser->strinput == NULL);
+ i_assert(parser->state == JSON_STATE_OBJECT_NEXT ||
+ parser->state == JSON_STATE_OBJECT_OPEN ||
+ parser->state == JSON_STATE_ARRAY_NEXT ||
+ parser->state == JSON_STATE_ARRAY_OPEN);
+
+ if (parser->state == JSON_STATE_OBJECT_OPEN ||
+ parser->state == JSON_STATE_ARRAY_OPEN)
+ parser->nested_skip_count++;
+
+ parser->skipping = TRUE;
+ if (parser->state == JSON_STATE_ARRAY_NEXT)
+ parser->state = JSON_STATE_ARRAY_NEXT_SKIP;
+}
+
+static void json_strinput_destroyed(struct json_parser *parser)
+{
+ i_assert(parser->strinput != NULL);
+
+ parser->strinput = NULL;
+}
+
+static int
+json_try_parse_stream_start(struct json_parser *parser,
+ struct istream **input_r)
+{
+ if (!json_parse_whitespace(parser))
+ return -1;
+
+ if (parser->state == JSON_STATE_OBJECT_COLON) {
+ if (*parser->data != ':') {
+ parser->error = "Expected ':' after key";
+ return -1;
+ }
+ parser->data++;
+ parser->state = JSON_STATE_OBJECT_VALUE;
+ if (!json_parse_whitespace(parser))
+ return -1;
+ }
+
+ if (*parser->data != '"')
+ return -1;
+ parser->data++;
+ json_parser_update_input_pos(parser);
+
+ parser->state = parser->state == JSON_STATE_OBJECT_VALUE ?
+ JSON_STATE_OBJECT_SKIP_STRING : JSON_STATE_ARRAY_SKIP_STRING;
+ parser->strinput = i_stream_create_jsonstr(parser->input);
+ i_stream_add_destroy_callback(parser->strinput,
+ json_strinput_destroyed, parser);
+
+ *input_r = parser->strinput;
+ return 0;
+}
+
+int json_parse_next_stream(struct json_parser *parser,
+ struct istream **input_r)
+{
+ int ret;
+
+ i_assert(!parser->skipping);
+ i_assert(parser->strinput == NULL);
+ i_assert(parser->state == JSON_STATE_OBJECT_COLON ||
+ parser->state == JSON_STATE_OBJECT_VALUE ||
+ parser->state == JSON_STATE_ARRAY_VALUE);
+
+ *input_r = NULL;
+
+ while ((ret = json_parser_read_more(parser)) > 0) {
+ if (json_try_parse_stream_start(parser, input_r) == 0)
+ break;
+ if (parser->data != parser->end)
+ return -1;
+ /* parsing probably failed because there wasn't enough input.
+ reset the error and try reading more. */
+ parser->error = NULL;
+ parser->highwater_offset = parser->input->v_offset +
+ i_stream_get_data_size(parser->input);
+ }
+ return ret;
+}
+
+static void json_append_escaped_char(string_t *dest, unsigned char src)
+{
+ switch (src) {
+ case '\b':
+ str_append(dest, "\\b");
+ break;
+ case '\f':
+ str_append(dest, "\\f");
+ break;
+ case '\n':
+ str_append(dest, "\\n");
+ break;
+ case '\r':
+ str_append(dest, "\\r");
+ break;
+ case '\t':
+ str_append(dest, "\\t");
+ break;
+ case '"':
+ str_append(dest, "\\\"");
+ break;
+ case '\\':
+ str_append(dest, "\\\\");
+ break;
+ default:
+ if (src < 0x20 || src >= 0x80)
+ str_printfa(dest, "\\u%04x", src);
+ else
+ str_append_c(dest, src);
+ break;
+ }
+}
+
+void json_append_escaped_ucs4(string_t *dest, unichar_t chr)
+{
+ if (chr < 0x80)
+ json_append_escaped_char(dest, (unsigned char)chr);
+ else if (chr == 0x2028 || chr == 0x2029)
+ str_printfa(dest, "\\u%04x", chr);
+ else
+ uni_ucs4_to_utf8_c(chr, dest);
+}
+
+void ostream_escaped_json_format(string_t *dest, unsigned char src)
+{
+ json_append_escaped_char(dest, src);
+}
+
+void json_append_escaped(string_t *dest, const char *src)
+{
+ json_append_escaped_data(dest, (const unsigned char*)src, strlen(src));
+}
+
+void json_append_escaped_data(string_t *dest, const unsigned char *src, size_t size)
+{
+ size_t i;
+ int bytes = 0;
+ unichar_t chr;
+
+ for (i = 0; i < size;) {
+ bytes = uni_utf8_get_char_n(src+i, size-i, &chr);
+ if (bytes > 0 && uni_is_valid_ucs4(chr)) {
+ json_append_escaped_ucs4(dest, chr);
+ i += bytes;
+ } else {
+ str_append_data(dest, UNICODE_REPLACEMENT_CHAR_UTF8,
+ UTF8_REPLACEMENT_CHAR_LEN);
+ i++;
+ }
+ }
+}
diff --git a/src/lib/json-parser.h b/src/lib/json-parser.h
new file mode 100644
index 0000000..745f4a7
--- /dev/null
+++ b/src/lib/json-parser.h
@@ -0,0 +1,61 @@
+#ifndef JSON_PARSER_H
+#define JSON_PARSER_H
+
+#include "unichar.h"
+
+enum json_type {
+ /* { key: */
+ JSON_TYPE_OBJECT_KEY,
+ /* : { new object */
+ JSON_TYPE_OBJECT,
+ /* } (not returned for the root object) */
+ JSON_TYPE_OBJECT_END,
+
+ JSON_TYPE_ARRAY,
+ JSON_TYPE_ARRAY_END,
+
+ JSON_TYPE_STRING,
+ JSON_TYPE_NUMBER,
+ JSON_TYPE_TRUE,
+ JSON_TYPE_FALSE,
+ JSON_TYPE_NULL
+};
+
+enum json_parser_flags {
+ /* By default we assume that the input is an object and parsing skips
+ the root level "{" and "}". If this flag is set, it's possible to
+ parse any other type of JSON values directly. */
+ JSON_PARSER_NO_ROOT_OBJECT = 0x01
+};
+
+/* Parse JSON tokens from the input stream. */
+struct json_parser *json_parser_init(struct istream *input);
+struct json_parser *json_parser_init_flags(struct istream *input,
+ enum json_parser_flags flags);
+
+int json_parser_deinit(struct json_parser **parser, const char **error_r);
+
+/* Parse the next token. Returns 1 if found, 0 if more input stream is
+ non-blocking and needs more input, -1 if input stream is at EOF. */
+int json_parse_next(struct json_parser *parser, enum json_type *type_r,
+ const char **value_r);
+/* Skip the next object value. If it's an object, its members are also
+ skipped. */
+void json_parse_skip_next(struct json_parser *parser);
+/* Skip the remainder of the value parsed earlier by json_parse_next(). */
+void json_parse_skip(struct json_parser *parser);
+/* Return the following string as input stream. Returns 1 if ok, 0 if
+ input stream is non-blocking and needs more input, -1 if the next token
+ isn't a string (call json_parse_next()). */
+int json_parse_next_stream(struct json_parser *parser,
+ struct istream **input_r);
+
+/* Append UCS4 to already opened JSON string. */
+void json_append_escaped_ucs4(string_t *dest, unichar_t chr);
+/* Append data to already opened JSON string. src should be valid UTF-8 data. */
+void json_append_escaped(string_t *dest, const char *src);
+/* Same as json_append_escaped(), but append non-\0 terminated input. */
+void json_append_escaped_data(string_t *dest, const unsigned char *src, size_t size);
+void ostream_escaped_json_format(string_t *dest, unsigned char src);
+
+#endif
diff --git a/src/lib/json-tree.c b/src/lib/json-tree.c
new file mode 100644
index 0000000..cb721c2
--- /dev/null
+++ b/src/lib/json-tree.c
@@ -0,0 +1,173 @@
+/* Copyright (c) 2015-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "json-tree.h"
+
+struct json_tree {
+ pool_t pool;
+ struct json_tree_node *root, *cur, *cur_child;
+};
+
+struct json_tree *
+json_tree_init_type(enum json_type container)
+{
+ struct json_tree *tree;
+ pool_t pool;
+
+ pool = pool_alloconly_create("json tree", 1024);
+ tree = p_new(pool, struct json_tree, 1);
+ tree->pool = pool;
+ tree->root = tree->cur = p_new(tree->pool, struct json_tree_node, 1);
+ tree->cur->value_type = container == JSON_TYPE_ARRAY ? container : JSON_TYPE_OBJECT;
+ return tree;
+}
+
+void json_tree_deinit(struct json_tree **_tree)
+{
+ struct json_tree *tree = *_tree;
+
+ *_tree = NULL;
+ pool_unref(&tree->pool);
+}
+
+static void
+json_tree_append_child(struct json_tree *tree, enum json_type type,
+ const char *value)
+{
+ struct json_tree_node *node;
+
+ node = p_new(tree->pool, struct json_tree_node, 1);
+ node->parent = tree->cur;
+ node->value_type = type;
+ node->value.str = p_strdup(tree->pool, value);
+
+ if (tree->cur_child == NULL)
+ tree->cur->value.child = node;
+ else
+ tree->cur_child->next = node;
+ tree->cur_child = node;
+}
+
+static void
+json_tree_set_cur(struct json_tree *tree, struct json_tree_node *node)
+{
+ tree->cur = node;
+ tree->cur_child = tree->cur->value.child;
+ if (tree->cur_child != NULL) {
+ while (tree->cur_child->next != NULL)
+ tree->cur_child = tree->cur_child->next;
+ }
+}
+
+static int
+json_tree_append_value(struct json_tree *tree, enum json_type type,
+ const char *value)
+{
+ switch (tree->cur->value_type) {
+ case JSON_TYPE_OBJECT_KEY:
+ /* "key": value - we already added the node and set its key,
+ so now just set the value */
+ tree->cur->value_type = type;
+ tree->cur->value.str = p_strdup(tree->pool, value);
+ json_tree_set_cur(tree, tree->cur->parent);
+ break;
+ case JSON_TYPE_ARRAY:
+ /* element in array - add a new node */
+ json_tree_append_child(tree, type, value);
+ break;
+ default:
+ return -1;
+ }
+ return 0;
+}
+
+int json_tree_append(struct json_tree *tree, enum json_type type,
+ const char *value)
+{
+ switch (type) {
+ case JSON_TYPE_OBJECT_KEY:
+ if (tree->cur->value_type != JSON_TYPE_OBJECT)
+ return -1;
+ json_tree_append_child(tree, type, NULL);
+ json_tree_set_cur(tree, tree->cur_child);
+ tree->cur->key = p_strdup(tree->pool, value);
+ break;
+ case JSON_TYPE_ARRAY:
+ if (json_tree_append_value(tree, type, NULL) < 0)
+ return -1;
+ json_tree_set_cur(tree, tree->cur_child);
+ break;
+ case JSON_TYPE_OBJECT:
+ if (tree->cur->value_type == JSON_TYPE_OBJECT_KEY)
+ tree->cur->value_type = JSON_TYPE_OBJECT;
+ else if (tree->cur->value_type == JSON_TYPE_ARRAY) {
+ json_tree_append_child(tree, type, NULL);
+ json_tree_set_cur(tree, tree->cur_child);
+ } else {
+ return -1;
+ }
+ break;
+ case JSON_TYPE_OBJECT_END:
+ if (tree->cur->parent == NULL ||
+ tree->cur->value_type != JSON_TYPE_OBJECT)
+ return -1;
+ json_tree_set_cur(tree, tree->cur->parent);
+ break;
+ case JSON_TYPE_ARRAY_END:
+ if (tree->cur->parent == NULL ||
+ tree->cur->value_type != JSON_TYPE_ARRAY)
+ return -1;
+ json_tree_set_cur(tree, tree->cur->parent);
+ break;
+ case JSON_TYPE_STRING:
+ case JSON_TYPE_NUMBER:
+ case JSON_TYPE_TRUE:
+ case JSON_TYPE_FALSE:
+ case JSON_TYPE_NULL:
+ if (json_tree_append_value(tree, type, value) < 0)
+ return -1;
+ break;
+ }
+ return 0;
+}
+
+const struct json_tree_node *
+json_tree_root(const struct json_tree *tree)
+{
+ return tree->root;
+}
+
+const struct json_tree_node *
+json_tree_find_key(const struct json_tree_node *node, const char *key)
+{
+ i_assert(node->value_type == JSON_TYPE_OBJECT);
+
+ node = json_tree_get_child(node);
+ for (; node != NULL; node = node->next) {
+ if (node->key != NULL && strcmp(node->key, key) == 0)
+ return node;
+ }
+ return NULL;
+}
+
+const struct json_tree_node *
+json_tree_find_child_with(const struct json_tree_node *node,
+ const char *key, const char *value)
+{
+ const struct json_tree_node *child;
+
+ i_assert(node->value_type == JSON_TYPE_OBJECT ||
+ node->value_type == JSON_TYPE_ARRAY);
+
+ for (node = json_tree_get_child(node); node != NULL; node = node->next) {
+ if (node->value_type != JSON_TYPE_OBJECT)
+ continue;
+
+ child = json_tree_find_key(node, key);
+ if (child != NULL &&
+ json_tree_get_value_str(child) != NULL &&
+ strcmp(json_tree_get_value_str(child), value) == 0)
+ return node;
+ }
+ return NULL;
+}
diff --git a/src/lib/json-tree.h b/src/lib/json-tree.h
new file mode 100644
index 0000000..a995c75
--- /dev/null
+++ b/src/lib/json-tree.h
@@ -0,0 +1,62 @@
+#ifndef JSON_TREE_H
+#define JSON_TREE_H
+
+#include "json-parser.h"
+
+/* Direct access to this structure is not encouraged, use the inline
+ function accessors where possible, so that the implementation
+ details can remain fluid, or, even better, hidden. */
+struct json_tree_node {
+ /* object key, or NULL if we're in a list */
+ const char *key;
+ struct json_tree_node *parent, *next;
+
+ enum json_type value_type;
+ struct {
+ /* for JSON_TYPE_OBJECT and JSON_TYPE_ARRAY */
+ struct json_tree_node *child;
+ /* for other types */
+ const char *str;
+ } value;
+};
+static inline ATTR_PURE const struct json_tree_node *json_tree_get_child(const struct json_tree_node *node)
+{
+ return node->value.child;
+}
+static inline ATTR_PURE const char *json_tree_get_value_str(const struct json_tree_node *node)
+{
+ return node->value.str;
+}
+
+/* You can build a list or an object, nothing else. */
+struct json_tree *json_tree_init_type(enum json_type container);
+static inline struct json_tree *json_tree_init(void)
+{
+ return json_tree_init_type(JSON_TYPE_OBJECT);
+}
+static inline struct json_tree *json_tree_init_array(void)
+{
+ return json_tree_init_type(JSON_TYPE_ARRAY);
+}
+
+void json_tree_deinit(struct json_tree **tree);
+
+/* Append data to a tree. The type/value should normally come from json-parser.
+ Returns 0 on success, -1 if the input was invalid (which should never happen
+ if it's coming from json-parser). */
+int json_tree_append(struct json_tree *tree, enum json_type type,
+ const char *value);
+
+/* Return the root node. */
+const struct json_tree_node *
+json_tree_root(const struct json_tree *tree);
+/* Find a node with the specified key from an OBJECT node */
+const struct json_tree_node *
+json_tree_find_key(const struct json_tree_node *node, const char *key);
+/* Find an object node (from an array), which contains the specified key=value
+ in its values. */
+const struct json_tree_node *
+json_tree_find_child_with(const struct json_tree_node *node,
+ const char *key, const char *value);
+
+#endif
diff --git a/src/lib/lib-event-private.h b/src/lib/lib-event-private.h
new file mode 100644
index 0000000..f6d961a
--- /dev/null
+++ b/src/lib/lib-event-private.h
@@ -0,0 +1,114 @@
+#ifndef LIB_EVENT_PRIVATE_H
+#define LIB_EVENT_PRIVATE_H
+
+#include <sys/resource.h>
+
+struct event_pointer {
+ const char *key;
+ void *value;
+};
+
+struct event {
+ /* linked list of all events, newest first */
+ struct event *prev, *next;
+
+ int refcount;
+ pool_t pool;
+ struct event *parent;
+ uint64_t id;
+
+ /* Avoid sending the event to stats over and over. The 'change_id'
+ increments every time something about this event changes. If
+ 'sent_to_stats_id' matches 'change_id', we skip sending this
+ event out. If it doesn't match, we send it and set
+ 'sent_to_stats_id' to 'change_id'. sent_to_stats_id=0 is reserved
+ for "event hasn't been sent". 'change_id' can never be 0. */
+ uint32_t change_id;
+ uint32_t sent_to_stats_id;
+
+ char *log_prefix;
+ unsigned int log_prefixes_dropped;
+ /* sending_debug_log can be used if this value matches
+ event_filter_replace_counter. */
+ unsigned int debug_level_checked_filter_counter;
+ event_log_prefix_callback_t *log_prefix_callback;
+ void *log_prefix_callback_context;
+ event_log_message_callback_t *log_message_callback;
+ void *log_message_callback_context;
+ ARRAY(struct event_pointer) pointers;
+ /* If the event's log level is at least this high, log it. If it's
+ lower, check for debug log filters etc. */
+ enum log_type min_log_level;
+
+ bool log_prefix_from_system_pool:1;
+ bool log_prefix_replace:1;
+ bool passthrough:1;
+ bool forced_debug:1;
+ bool always_log_source:1;
+ bool sending_debug_log:1;
+
+/* Fields that are exported & imported: */
+ struct timeval tv_created_ioloop;
+ struct timeval tv_created;
+ struct timeval tv_last_sent;
+ struct rusage ru_last;
+
+ const char *source_filename;
+ unsigned int source_linenum;
+
+ /* This is the event's name while it's being sent. It'll be removed
+ after the event is sent. */
+ char *sending_name;
+
+ ARRAY(struct event_category *) categories;
+ ARRAY(struct event_field) fields;
+};
+
+enum event_callback_type {
+ /* Event was just created */
+ EVENT_CALLBACK_TYPE_CREATE,
+ /* Event is being sent */
+ EVENT_CALLBACK_TYPE_SEND,
+ /* Event is being freed */
+ EVENT_CALLBACK_TYPE_FREE,
+};
+
+/* Returns TRUE if the event should continue to the next handler. Unless
+ stopped, the final handler logs the event if it matches the log filter. */
+typedef bool event_callback_t(struct event *event,
+ enum event_callback_type type,
+ struct failure_context *ctx,
+ const char *fmt, va_list args);
+/* Called when category is registered or unregistered. The parent category
+ is always already registered. */
+typedef void event_category_callback_t(struct event_category *category);
+
+void event_send(struct event *event, struct failure_context *ctx,
+ const char *fmt, ...) ATTR_FORMAT(3, 4);
+void event_vsend(struct event *event, struct failure_context *ctx,
+ const char *fmt, va_list args) ATTR_FORMAT(3, 0);
+
+struct event *events_get_head(void);
+
+/* Find event category by name. This only finds registered categories. */
+struct event_category *event_category_find_registered(const char *name);
+/* Return all registered categories. */
+struct event_category *const *
+event_get_registered_categories(unsigned int *count_r);
+
+/* Register callback to be called for event's different states. */
+void event_register_callback(event_callback_t *callback);
+void event_unregister_callback(event_callback_t *callback);
+
+/* Register callback to be called whenever categories are registered or
+ unregistered. */
+void event_category_register_callback(event_category_callback_t *callback);
+void event_category_unregister_callback(event_category_callback_t *callback);
+
+static inline void event_recalculate_debug_level(struct event *event)
+{
+ event->debug_level_checked_filter_counter =
+ event_filter_replace_counter - 1;
+}
+
+#endif
diff --git a/src/lib/lib-event.c b/src/lib/lib-event.c
new file mode 100644
index 0000000..87db436
--- /dev/null
+++ b/src/lib/lib-event.c
@@ -0,0 +1,1764 @@
+/* Copyright (c) 2017-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "lib-event-private.h"
+#include "event-filter.h"
+#include "array.h"
+#include "llist.h"
+#include "time-util.h"
+#include "str.h"
+#include "strescape.h"
+#include "ioloop-private.h"
+
+#include <ctype.h>
+
+enum event_code {
+ EVENT_CODE_ALWAYS_LOG_SOURCE = 'a',
+ EVENT_CODE_CATEGORY = 'c',
+ EVENT_CODE_TV_LAST_SENT = 'l',
+ EVENT_CODE_SENDING_NAME = 'n',
+ EVENT_CODE_SOURCE = 's',
+
+ EVENT_CODE_FIELD_INTMAX = 'I',
+ EVENT_CODE_FIELD_STR = 'S',
+ EVENT_CODE_FIELD_TIMEVAL = 'T',
+ EVENT_CODE_FIELD_STRLIST = 'L',
+};
+
+/* Internal event category state.
+
+ Each (unique) event category maps to one internal category. (I.e., if
+ two places attempt to register the same category, they will share the
+ internal state.)
+
+ This is required in order to support multiple registrations of the same
+ category. Currently, the only situation in which this occurs is the
+ stats process receiving categories from other processes and also using
+ the same categories internally.
+
+ During registration, we look up the internal state based on the new
+ category's name. If found, we use it after sanity checking that the two
+ are identical (i.e., they both have the same name and parent). If not
+ found, we allocate a new internal state and use it.
+
+ We stash a pointer to the internal state in struct event_category (the
+ "internal" member). As a result, all category structs for the same
+ category point to the same internal state. */
+struct event_internal_category {
+ /* More than one category can be represented by the internal state.
+ To give consumers a unique but consistent category pointer, we
+ return a pointer to this 'represetative' category structure.
+ Because we allocated it, we know that it will live exactly as
+ long as we need it to. */
+ struct event_category representative;
+
+ struct event_internal_category *parent;
+ char *name;
+ int refcount;
+};
+
+struct event_reason {
+ struct event *event;
+};
+
+extern struct event_passthrough event_passthrough_vfuncs;
+
+static struct event *events = NULL;
+static struct event *current_global_event = NULL;
+static struct event *event_last_passthrough = NULL;
+static ARRAY(event_callback_t *) event_handlers;
+static ARRAY(event_category_callback_t *) event_category_callbacks;
+static ARRAY(struct event_internal_category *) event_registered_categories_internal;
+static ARRAY(struct event_category *) event_registered_categories_representative;
+static ARRAY(struct event *) global_event_stack;
+static uint64_t event_id_counter = 0;
+
+static void get_self_rusage(struct rusage *ru_r)
+{
+ if (getrusage(RUSAGE_SELF, ru_r) < 0)
+ i_fatal("getrusage() failed: %m");
+}
+
+static struct event *
+event_create_internal(struct event *parent, const char *source_filename,
+ unsigned int source_linenum);
+static struct event_internal_category *
+event_category_find_internal(const char *name);
+
+static struct event *last_passthrough_event(void)
+{
+ i_assert(event_last_passthrough != NULL);
+ return event_last_passthrough;
+}
+
+static void event_copy_parent_defaults(struct event *event,
+ const struct event *parent)
+{
+ event->always_log_source = parent->always_log_source;
+ event->passthrough = parent->passthrough;
+ event->min_log_level = parent->min_log_level;
+ event->forced_debug = parent->forced_debug;
+}
+
+static bool
+event_find_category(const struct event *event,
+ const struct event_category *category);
+
+static void event_set_changed(struct event *event)
+{
+ event->change_id++;
+ /* It's unlikely that change_id will ever wrap, but lets be safe
+ anyway. */
+ if (event->change_id == 0 ||
+ event->change_id == event->sent_to_stats_id)
+ event->change_id++;
+}
+
+static bool
+event_call_callbacks(struct event *event, enum event_callback_type type,
+ struct failure_context *ctx, const char *fmt, va_list args)
+{
+ event_callback_t *callback;
+
+ array_foreach_elem(&event_handlers, callback) {
+ bool ret;
+
+ T_BEGIN {
+ ret = callback(event, type, ctx, fmt, args);
+ } T_END;
+ if (!ret) {
+ /* event sending was stopped */
+ return FALSE;
+ }
+ }
+ return TRUE;
+}
+
+static void
+event_call_callbacks_noargs(struct event *event,
+ enum event_callback_type type, ...)
+{
+ va_list args;
+
+ /* the args are empty and not used for anything, but there doesn't seem
+ to be any nice and standard way of passing an initialized va_list
+ as a parameter without va_start(). */
+ va_start(args, type);
+ (void)event_call_callbacks(event, type, NULL, NULL, args);
+ va_end(args);
+}
+
+void event_copy_categories(struct event *to, struct event *from)
+{
+ unsigned int cat_count;
+ struct event_category *const *categories =
+ event_get_categories(from, &cat_count);
+ for (unsigned int i = 1; i <= cat_count; i++)
+ event_add_category(to, categories[cat_count-i]);
+}
+
+void event_copy_fields(struct event *to, struct event *from)
+{
+ const struct event_field *fld;
+ unsigned int count;
+ const char *const *values;
+
+ if (!array_is_created(&from->fields))
+ return;
+ array_foreach(&from->fields, fld) {
+ switch (fld->value_type) {
+ case EVENT_FIELD_VALUE_TYPE_STR:
+ event_add_str(to, fld->key, fld->value.str);
+ break;
+ case EVENT_FIELD_VALUE_TYPE_INTMAX:
+ event_add_int(to, fld->key, fld->value.intmax);
+ break;
+ case EVENT_FIELD_VALUE_TYPE_TIMEVAL:
+ event_add_timeval(to, fld->key, &fld->value.timeval);
+ break;
+ case EVENT_FIELD_VALUE_TYPE_STRLIST:
+ values = array_get(&fld->value.strlist, &count);
+ for (unsigned int i = 0; i < count; i++)
+ event_strlist_append(to, fld->key, values[i]);
+ break;
+ default:
+ break;
+ }
+ }
+}
+
+bool event_has_all_categories(struct event *event, const struct event *other)
+{
+ struct event_category **cat;
+ if (!array_is_created(&other->categories))
+ return TRUE;
+ if (!array_is_created(&event->categories))
+ return FALSE;
+ array_foreach_modifiable(&other->categories, cat) {
+ if (!event_find_category(event, *cat))
+ return FALSE;
+ }
+ return TRUE;
+}
+
+bool event_has_all_fields(struct event *event, const struct event *other)
+{
+ struct event_field *fld;
+ if (!array_is_created(&other->fields))
+ return TRUE;
+ array_foreach_modifiable(&other->fields, fld) {
+ if (event_find_field_nonrecursive(event, fld->key) == NULL)
+ return FALSE;
+ }
+ return TRUE;
+}
+
+struct event *event_dup(const struct event *source)
+{
+ struct event *ret =
+ event_create_internal(source->parent, source->source_filename,
+ source->source_linenum);
+ string_t *str = t_str_new(256);
+ const char *err;
+ event_export(source, str);
+ if (!event_import(ret, str_c(str), &err))
+ i_panic("event_import(%s) failed: %s", str_c(str), err);
+ ret->tv_created_ioloop = source->tv_created_ioloop;
+ return ret;
+}
+
+/*
+ * Copy the source's categories and fields recursively.
+ *
+ * We recurse to the parent before copying this event's data because we may
+ * be overriding a field.
+ */
+static void event_flatten_recurse(struct event *dst, struct event *src,
+ struct event *limit)
+{
+ if (src->parent != limit)
+ event_flatten_recurse(dst, src->parent, limit);
+
+ event_copy_categories(dst, src);
+ event_copy_fields(dst, src);
+}
+
+struct event *event_flatten(struct event *src)
+{
+ struct event *dst;
+
+ /* If we don't have a parent or a global event,
+ we have nothing to flatten. */
+ if (src->parent == NULL && current_global_event == NULL)
+ return event_ref(src);
+
+ /* We have to flatten the event. */
+
+ dst = event_create_internal(NULL, src->source_filename,
+ src->source_linenum);
+ dst = event_set_name(dst, src->sending_name);
+
+ if (current_global_event != NULL)
+ event_flatten_recurse(dst, current_global_event, NULL);
+ event_flatten_recurse(dst, src, NULL);
+
+ dst->tv_created_ioloop = src->tv_created_ioloop;
+ dst->tv_created = src->tv_created;
+ dst->tv_last_sent = src->tv_last_sent;
+
+ return dst;
+}
+
+static inline void replace_parent_ref(struct event *event, struct event *new)
+{
+ if (event->parent == new)
+ return; /* no-op */
+
+ if (new != NULL)
+ event_ref(new);
+
+ event_unref(&event->parent);
+
+ event->parent = new;
+}
+
+/*
+ * Minimize the event and its ancestry.
+ *
+ * In general, the chain of parents starting from this event can be divided
+ * up into four consecutive ranges:
+ *
+ * 1. the event itself
+ * 2. a range of events that should be flattened into the event itself
+ * 3. a range of trivial (i.e., no categories or fields) events that should
+ * be skipped
+ * 4. the rest of the chain
+ *
+ * Except for the first range, the event itself, the remaining ranges can
+ * have zero events.
+ *
+ * As the names of these ranges imply, we want to flatten certain parts of
+ * the ancestry, skip other parts of the ancestry and leave the remainder
+ * untouched.
+ *
+ * For example, suppose that we have an event (A) with ancestors forming the
+ * following graph:
+ *
+ * A -> B -> C -> D -> E -> F
+ *
+ * Further, suppose that B, C, and F contain some categories or fields but
+ * have not yet been sent to an external process that knows how to reference
+ * previously encountered events, and D contains no fields or categories of
+ * its own (but it inherits some from E and F).
+ *
+ * We can define the 4 ranges:
+ *
+ * A: the event
+ * B-C: flattening
+ * D: skipping
+ * E-end: the rest
+ *
+ * The output would therefore be:
+ *
+ * G -> E -> F
+ *
+ * where G contains the fields and categories of A, B, and C (and trivially
+ * D beacuse D was empty).
+ *
+ * Note that even though F has not yet been sent out, we send it now because
+ * it is part of the "rest" range.
+ *
+ * TODO: We could likely apply this function recursively on the "rest"
+ * range, but further investigation is required to determine whether it is
+ * worth it.
+ */
+struct event *event_minimize(struct event *event)
+{
+ struct event *flatten_bound;
+ struct event *skip_bound;
+ struct event *new_event;
+ struct event *cur;
+
+ if (event->parent == NULL)
+ return event_ref(event);
+
+ /* find the bound for field/category flattening */
+ flatten_bound = NULL;
+ for (cur = event->parent; cur != NULL; cur = cur->parent) {
+ if (cur->sent_to_stats_id == 0 &&
+ timeval_cmp(&cur->tv_created_ioloop,
+ &event->tv_created_ioloop) == 0)
+ continue;
+
+ flatten_bound = cur;
+ break;
+ }
+
+ /* continue to find the bound for empty event skipping */
+ skip_bound = NULL;
+ for (; cur != NULL; cur = cur->parent) {
+ if (cur->sent_to_stats_id == 0 &&
+ (!array_is_created(&cur->fields) ||
+ array_is_empty(&cur->fields)) &&
+ (!array_is_created(&cur->categories) ||
+ array_is_empty(&cur->categories)))
+ continue;
+
+ skip_bound = cur;
+ break;
+ }
+
+ /* fast path - no flattening and no skipping to do */
+ if ((event->parent == flatten_bound) &&
+ (event->parent == skip_bound))
+ return event_ref(event);
+
+ new_event = event_dup(event);
+
+ /* flatten */
+ event_flatten_recurse(new_event, event, flatten_bound);
+ replace_parent_ref(new_event, flatten_bound);
+
+ /* skip */
+ replace_parent_ref(new_event, skip_bound);
+
+ return new_event;
+}
+
+static struct event *
+event_create_internal(struct event *parent, const char *source_filename,
+ unsigned int source_linenum)
+{
+ struct event *event;
+ pool_t pool = pool_alloconly_create(MEMPOOL_GROWING"event", 1024);
+
+ event = p_new(pool, struct event, 1);
+ event->refcount = 1;
+ event->id = ++event_id_counter;
+ event->pool = pool;
+ event->tv_created_ioloop = ioloop_timeval;
+ event->min_log_level = LOG_TYPE_INFO;
+ i_gettimeofday(&event->tv_created);
+ event->source_filename = p_strdup(pool, source_filename);
+ event->source_linenum = source_linenum;
+ event->change_id = 1;
+ if (parent != NULL) {
+ event->parent = parent;
+ event_ref(event->parent);
+ event_copy_parent_defaults(event, parent);
+ }
+ DLLIST_PREPEND(&events, event);
+ return event;
+}
+
+#undef event_create
+struct event *event_create(struct event *parent, const char *source_filename,
+ unsigned int source_linenum)
+{
+ struct event *event;
+
+ event = event_create_internal(parent, source_filename, source_linenum);
+ (void)event_call_callbacks_noargs(event, EVENT_CALLBACK_TYPE_CREATE);
+ return event;
+}
+
+#undef event_create_passthrough
+struct event_passthrough *
+event_create_passthrough(struct event *parent, const char *source_filename,
+ unsigned int source_linenum)
+{
+ if (!parent->passthrough) {
+ if (event_last_passthrough != NULL) {
+ /* API is being used in a wrong or dangerous way */
+ i_panic("Can't create multiple passthrough events - "
+ "finish the earlier with ->event()");
+ }
+ struct event *event =
+ event_create(parent, source_filename, source_linenum);
+ event->passthrough = TRUE;
+ /* This event only intends to extend the parent event.
+ Use the parent's creation timestamp. */
+ event->tv_created_ioloop = parent->tv_created_ioloop;
+ event->tv_created = parent->tv_created;
+ memcpy(&event->ru_last, &parent->ru_last, sizeof(parent->ru_last));
+ event_last_passthrough = event;
+ } else {
+ event_last_passthrough = parent;
+ }
+ return &event_passthrough_vfuncs;
+}
+
+struct event *event_ref(struct event *event)
+{
+ i_assert(event->refcount > 0);
+
+ event->refcount++;
+ return event;
+}
+
+void event_unref(struct event **_event)
+{
+ struct event *event = *_event;
+
+ if (event == NULL)
+ return;
+ *_event = NULL;
+
+ i_assert(event->refcount > 0);
+ if (--event->refcount > 0)
+ return;
+ i_assert(event != current_global_event);
+
+ event_call_callbacks_noargs(event, EVENT_CALLBACK_TYPE_FREE);
+
+ if (event_last_passthrough == event)
+ event_last_passthrough = NULL;
+ if (event->log_prefix_from_system_pool)
+ i_free(event->log_prefix);
+ i_free(event->sending_name);
+ event_unref(&event->parent);
+
+ DLLIST_REMOVE(&events, event);
+ pool_unref(&event->pool);
+}
+
+struct event *events_get_head(void)
+{
+ return events;
+}
+
+struct event *event_push_global(struct event *event)
+{
+ i_assert(event != NULL);
+
+ if (current_global_event != NULL) {
+ if (!array_is_created(&global_event_stack))
+ i_array_init(&global_event_stack, 4);
+ array_push_back(&global_event_stack, &current_global_event);
+ }
+ current_global_event = event;
+ return event;
+}
+
+struct event *event_pop_global(struct event *event)
+{
+ i_assert(event != NULL);
+ i_assert(event == current_global_event);
+ /* If the active context's root event is popped, we'll assert-crash
+ later on when deactivating the context and the root event no longer
+ exists. */
+ i_assert(event != io_loop_get_active_global_root());
+
+ if (!array_is_created(&global_event_stack) ||
+ array_count(&global_event_stack) == 0)
+ current_global_event = NULL;
+ else {
+ unsigned int event_count;
+ struct event *const *events =
+ array_get(&global_event_stack, &event_count);
+
+ i_assert(event_count > 0);
+ current_global_event = events[event_count-1];
+ array_delete(&global_event_stack, event_count-1, 1);
+ }
+ return current_global_event;
+}
+
+struct event *event_get_global(void)
+{
+ return current_global_event;
+}
+
+#undef event_reason_begin
+struct event_reason *
+event_reason_begin(const char *reason_code, const char *source_filename,
+ unsigned int source_linenum)
+{
+ struct event_reason *reason;
+
+ reason = i_new(struct event_reason, 1);
+ reason->event = event_create(event_get_global(),
+ source_filename, source_linenum);
+ event_strlist_append(reason->event, EVENT_REASON_CODE, reason_code);
+ event_push_global(reason->event);
+ return reason;
+}
+
+void event_reason_end(struct event_reason **_reason)
+{
+ struct event_reason *reason = *_reason;
+
+ if (reason == NULL)
+ return;
+ event_pop_global(reason->event);
+ /* This event was created only for global use. It shouldn't be
+ permanently stored anywhere. This assert could help catch bugs. */
+ i_assert(reason->event->refcount == 1);
+ event_unref(&reason->event);
+ i_free(reason);
+}
+
+const char *event_reason_code(const char *module, const char *name)
+{
+ return event_reason_code_prefix(module, "", name);
+}
+
+static bool event_reason_code_module_validate(const char *module)
+{
+ const char *p;
+
+ for (p = module; *p != '\0'; p++) {
+ if (*p == ' ' || *p == '-' || *p == ':')
+ return FALSE;
+ if (i_isupper(*p))
+ return FALSE;
+ }
+ return TRUE;
+}
+
+const char *event_reason_code_prefix(const char *module,
+ const char *name_prefix, const char *name)
+{
+ const char *p;
+
+ i_assert(module[0] != '\0');
+ i_assert(name[0] != '\0');
+
+ if (!event_reason_code_module_validate(module)) {
+ i_panic("event_reason_code_prefix(): "
+ "Invalid module '%s'", module);
+ }
+ if (!event_reason_code_module_validate(name_prefix)) {
+ i_panic("event_reason_code_prefix(): "
+ "Invalid name_prefix '%s'", name_prefix);
+ }
+
+ string_t *str = t_str_new(strlen(module) + 1 +
+ strlen(name_prefix) + strlen(name));
+ str_append(str, module);
+ str_append_c(str, ':');
+ str_append(str, name_prefix);
+
+ for (p = name; *p != '\0'; p++) {
+ switch (*p) {
+ case ' ':
+ case '-':
+ str_append_c(str, '_');
+ break;
+ case ':':
+ i_panic("event_reason_code_prefix(): "
+ "name has ':' (%s, %s%s)",
+ module, name_prefix, name);
+ default:
+ str_append_c(str, i_tolower(*p));
+ break;
+ }
+ }
+ return str_c(str);
+}
+
+static struct event *
+event_set_log_prefix(struct event *event, const char *prefix, bool append)
+{
+ event->log_prefix_callback = NULL;
+ event->log_prefix_callback_context = NULL;
+ if (event->log_prefix == NULL) {
+ /* allocate the first log prefix from the pool */
+ event->log_prefix = p_strdup(event->pool, prefix);
+ } else {
+ /* log prefix is being updated multiple times -
+ switch to system pool so we don't keep leaking memory */
+ if (event->log_prefix_from_system_pool)
+ i_free(event->log_prefix);
+ else
+ event->log_prefix_from_system_pool = TRUE;
+ event->log_prefix = i_strdup(prefix);
+ }
+ event->log_prefix_replace = !append;
+ return event;
+}
+
+struct event *
+event_set_append_log_prefix(struct event *event, const char *prefix)
+{
+ return event_set_log_prefix(event, prefix, TRUE);
+}
+
+struct event *event_replace_log_prefix(struct event *event, const char *prefix)
+{
+ return event_set_log_prefix(event, prefix, FALSE);
+}
+
+struct event *
+event_drop_parent_log_prefixes(struct event *event, unsigned int count)
+{
+ event->log_prefixes_dropped = count;
+ return event;
+}
+
+#undef event_set_log_prefix_callback
+struct event *
+event_set_log_prefix_callback(struct event *event,
+ bool replace,
+ event_log_prefix_callback_t *callback,
+ void *context)
+{
+ if (event->log_prefix_from_system_pool)
+ i_free(event->log_prefix);
+ else
+ event->log_prefix = NULL;
+ event->log_prefix_replace = replace;
+ event->log_prefix_callback = callback;
+ event->log_prefix_callback_context = context;
+ return event;
+}
+
+#undef event_set_log_message_callback
+struct event *
+event_set_log_message_callback(struct event *event,
+ event_log_message_callback_t *callback,
+ void *context)
+{
+ event->log_message_callback = callback;
+ event->log_message_callback_context = context;
+ return event;
+}
+
+struct event *
+event_set_name(struct event *event, const char *name)
+{
+ i_free(event->sending_name);
+ event->sending_name = i_strdup(name);
+ return event;
+}
+
+struct event *
+event_set_source(struct event *event, const char *filename,
+ unsigned int linenum, bool literal_fname)
+{
+ if (strcmp(event->source_filename, filename) != 0) {
+ event->source_filename = literal_fname ? filename :
+ p_strdup(event->pool, filename);
+ }
+ event->source_linenum = linenum;
+ return event;
+}
+
+struct event *event_set_always_log_source(struct event *event)
+{
+ event->always_log_source = TRUE;
+ return event;
+}
+
+struct event *event_set_min_log_level(struct event *event, enum log_type level)
+{
+ event->min_log_level = level;
+ event_recalculate_debug_level(event);
+ return event;
+}
+
+enum log_type event_get_min_log_level(const struct event *event)
+{
+ return event->min_log_level;
+}
+
+struct event *event_set_ptr(struct event *event, const char *key, void *value)
+{
+ struct event_pointer *p;
+
+ if (!array_is_created(&event->pointers))
+ p_array_init(&event->pointers, event->pool, 4);
+ else {
+ /* replace existing pointer if the key already exists */
+ array_foreach_modifiable(&event->pointers, p) {
+ if (strcmp(p->key, key) == 0) {
+ p->value = value;
+ return event;
+ }
+ }
+ }
+ p = array_append_space(&event->pointers);
+ p->key = p_strdup(event->pool, key);
+ p->value = value;
+ return event;
+}
+
+void *event_get_ptr(const struct event *event, const char *key)
+{
+ const struct event_pointer *p;
+
+ if (!array_is_created(&event->pointers))
+ return NULL;
+ array_foreach(&event->pointers, p) {
+ if (strcmp(p->key, key) == 0)
+ return p->value;
+ }
+ return NULL;
+}
+
+struct event_category *event_category_find_registered(const char *name)
+{
+ struct event_category *cat;
+
+ array_foreach_elem(&event_registered_categories_representative, cat) {
+ if (strcmp(cat->name, name) == 0)
+ return cat;
+ }
+ return NULL;
+}
+
+static struct event_internal_category *
+event_category_find_internal(const char *name)
+{
+ struct event_internal_category *internal;
+
+ array_foreach_elem(&event_registered_categories_internal, internal) {
+ if (strcmp(internal->name, name) == 0)
+ return internal;
+ }
+
+ return NULL;
+}
+
+struct event_category *const *
+event_get_registered_categories(unsigned int *count_r)
+{
+ return array_get(&event_registered_categories_representative, count_r);
+}
+
+static void
+event_category_add_to_array(struct event_internal_category *internal)
+{
+ struct event_category *representative = &internal->representative;
+
+ array_push_back(&event_registered_categories_internal, &internal);
+ array_push_back(&event_registered_categories_representative,
+ &representative);
+}
+
+static struct event_category *
+event_category_register(struct event_category *category)
+{
+ struct event_internal_category *internal = category->internal;
+ event_category_callback_t *callback;
+ bool allocated;
+
+ if (internal != NULL)
+ return &internal->representative; /* case 2 - see below */
+
+ /* register parent categories first */
+ if (category->parent != NULL)
+ (void) event_category_register(category->parent);
+
+ /* There are four cases we need to handle:
+
+ 1) a new category is registered
+ 2) same category struct is re-registered - already handled above
+ internal NULL check
+ 3) different category struct is registered, but it is identical
+ to the previously registered one
+ 4) different category struct is registered, and it is different
+ from the previously registered one - a programming error */
+ internal = event_category_find_internal(category->name);
+ if (internal == NULL) {
+ /* case 1: first time we saw this name - allocate new */
+ internal = i_new(struct event_internal_category, 1);
+ if (category->parent != NULL)
+ internal->parent = category->parent->internal;
+ internal->name = i_strdup(category->name);
+ internal->refcount = 1;
+ internal->representative.name = internal->name;
+ internal->representative.parent = category->parent;
+ internal->representative.internal = internal;
+
+ event_category_add_to_array(internal);
+
+ allocated = TRUE;
+ } else {
+ /* case 3 or 4: someone registered this name before - share */
+ if ((category->parent != NULL) &&
+ (internal->parent != category->parent->internal)) {
+ /* case 4 */
+ struct event_internal_category *other =
+ category->parent->internal;
+
+ i_panic("event category parent mismatch detected: "
+ "category %p internal %p (%s), "
+ "internal parent %p (%s), public parent %p (%s)",
+ category, internal, internal->name,
+ internal->parent, internal->parent->name,
+ other, other->name);
+ }
+
+ internal->refcount++;
+
+ allocated = FALSE;
+ }
+
+ category->internal = internal;
+
+ if (!allocated) {
+ /* not the first registration of this category */
+ return &internal->representative;
+ }
+
+ array_foreach_elem(&event_category_callbacks, callback) T_BEGIN {
+ callback(&internal->representative);
+ } T_END;
+
+ return &internal->representative;
+}
+
+static bool
+event_find_category(const struct event *event,
+ const struct event_category *category)
+{
+ struct event_internal_category *internal = category->internal;
+ struct event_category *cat;
+
+ /* make sure we're always looking for a representative */
+ i_assert(category == &internal->representative);
+
+ array_foreach_elem(&event->categories, cat) {
+ if (cat == category)
+ return TRUE;
+ }
+ return FALSE;
+}
+
+struct event *
+event_add_categories(struct event *event,
+ struct event_category *const *categories)
+{
+ struct event_category *representative;
+
+ if (!array_is_created(&event->categories))
+ p_array_init(&event->categories, event->pool, 4);
+
+ for (unsigned int i = 0; categories[i] != NULL; i++) {
+ representative = event_category_register(categories[i]);
+ if (!event_find_category(event, representative))
+ array_push_back(&event->categories, &representative);
+ }
+ event_set_changed(event);
+ event_recalculate_debug_level(event);
+ return event;
+}
+
+struct event *
+event_add_category(struct event *event, struct event_category *category)
+{
+ struct event_category *const categories[] = { category, NULL };
+ return event_add_categories(event, categories);
+}
+
+struct event_field *
+event_find_field_nonrecursive(const struct event *event, const char *key)
+{
+ struct event_field *field;
+
+ if (!array_is_created(&event->fields))
+ return NULL;
+
+ array_foreach_modifiable(&event->fields, field) {
+ if (strcmp(field->key, key) == 0)
+ return field;
+ }
+ return NULL;
+}
+
+const struct event_field *
+event_find_field_recursive(const struct event *event, const char *key)
+{
+ const struct event_field *field;
+
+ do {
+ if ((field = event_find_field_nonrecursive(event, key)) != NULL)
+ return field;
+ event = event->parent;
+ } while (event != NULL);
+
+ /* check also the global event and its parents */
+ event = event_get_global();
+ while (event != NULL) {
+ if ((field = event_find_field_nonrecursive(event, key)) != NULL)
+ return field;
+ event = event->parent;
+ }
+ return NULL;
+}
+
+static void
+event_get_recursive_strlist(const struct event *event, pool_t pool,
+ const char *key, ARRAY_TYPE(const_string) *dest)
+{
+ const struct event_field *field;
+ const char *str;
+
+ if (event == NULL)
+ return;
+
+ field = event_find_field_nonrecursive(event, key);
+ if (field != NULL) {
+ if (field->value_type != EVENT_FIELD_VALUE_TYPE_STRLIST) {
+ /* Value type unexpectedly changed. Stop recursing. */
+ return;
+ }
+ array_foreach_elem(&field->value.strlist, str) {
+ if (array_lsearch(dest, &str, i_strcmp_p) == NULL) {
+ if (pool != NULL)
+ str = p_strdup(pool, str);
+ array_push_back(dest, &str);
+ }
+ }
+ }
+ event_get_recursive_strlist(event->parent, pool, key, dest);
+}
+
+const char *
+event_find_field_recursive_str(const struct event *event, const char *key)
+{
+ const struct event_field *field;
+
+ field = event_find_field_recursive(event, key);
+ if (field == NULL)
+ return NULL;
+
+ switch (field->value_type) {
+ case EVENT_FIELD_VALUE_TYPE_STR:
+ return field->value.str;
+ case EVENT_FIELD_VALUE_TYPE_INTMAX:
+ return dec2str(field->value.intmax);
+ case EVENT_FIELD_VALUE_TYPE_TIMEVAL:
+ return t_strdup_printf("%"PRIdTIME_T".%u",
+ field->value.timeval.tv_sec,
+ (unsigned int)field->value.timeval.tv_usec);
+ case EVENT_FIELD_VALUE_TYPE_STRLIST: {
+ ARRAY_TYPE(const_string) list;
+ t_array_init(&list, 8);
+ /* This is a bit different, because it needs to be merging
+ all of the parent events' and global events' lists
+ together. */
+ event_get_recursive_strlist(event, NULL, key, &list);
+ event_get_recursive_strlist(event_get_global(), NULL,
+ key, &list);
+ return t_array_const_string_join(&list, ",");
+ }
+ }
+ i_unreached();
+}
+
+static struct event_field *
+event_get_field(struct event *event, const char *key, bool clear)
+{
+ struct event_field *field;
+
+ field = event_find_field_nonrecursive(event, key);
+ if (field == NULL) {
+ if (!array_is_created(&event->fields))
+ p_array_init(&event->fields, event->pool, 8);
+ field = array_append_space(&event->fields);
+ field->key = p_strdup(event->pool, key);
+ } else if (clear) {
+ i_zero(&field->value);
+ }
+ event_set_changed(event);
+ return field;
+}
+
+struct event *
+event_add_str(struct event *event, const char *key, const char *value)
+{
+ struct event_field *field;
+
+ if (value == NULL) {
+ /* silently ignoring is perhaps better than assert-crashing? */
+ return event;
+ }
+
+ field = event_get_field(event, key, TRUE);
+ field->value_type = EVENT_FIELD_VALUE_TYPE_STR;
+ field->value.str = p_strdup(event->pool, value);
+ return event;
+}
+
+struct event *
+event_strlist_append(struct event *event, const char *key, const char *value)
+{
+ struct event_field *field = event_get_field(event, key, FALSE);
+
+ if (field->value_type != EVENT_FIELD_VALUE_TYPE_STRLIST)
+ i_zero(&field->value);
+ field->value_type = EVENT_FIELD_VALUE_TYPE_STRLIST;
+
+ if (!array_is_created(&field->value.strlist))
+ p_array_init(&field->value.strlist, event->pool, 1);
+
+ /* lets not add empty values there though */
+ if (value == NULL)
+ return event;
+
+ const char *str = p_strdup(event->pool, value);
+ if (array_lsearch(&field->value.strlist, &str, i_strcmp_p) == NULL)
+ array_push_back(&field->value.strlist, &str);
+ return event;
+}
+
+struct event *
+event_strlist_replace(struct event *event, const char *key,
+ const char *const *values, unsigned int count)
+{
+ struct event_field *field = event_get_field(event, key, TRUE);
+ field->value_type = EVENT_FIELD_VALUE_TYPE_STRLIST;
+
+ for (unsigned int i = 0; i < count; i++)
+ event_strlist_append(event, key, values[i]);
+ return event;
+}
+
+struct event *
+event_strlist_copy_recursive(struct event *dest, const struct event *src,
+ const char *key)
+{
+ event_strlist_append(dest, key, NULL);
+ struct event_field *field = event_get_field(dest, key, FALSE);
+ i_assert(field != NULL);
+ event_get_recursive_strlist(src, dest->pool, key,
+ &field->value.strlist);
+ return dest;
+}
+
+struct event *
+event_add_int(struct event *event, const char *key, intmax_t num)
+{
+ struct event_field *field;
+
+ field = event_get_field(event, key, TRUE);
+ field->value_type = EVENT_FIELD_VALUE_TYPE_INTMAX;
+ field->value.intmax = num;
+ return event;
+}
+
+struct event *
+event_add_int_nonzero(struct event *event, const char *key, intmax_t num)
+{
+ if (num != 0)
+ return event_add_int(event, key, num);
+ return event;
+}
+
+struct event *
+event_inc_int(struct event *event, const char *key, intmax_t num)
+{
+ struct event_field *field;
+
+ field = event_find_field_nonrecursive(event, key);
+ if (field == NULL || field->value_type != EVENT_FIELD_VALUE_TYPE_INTMAX)
+ return event_add_int(event, key, num);
+
+ field->value.intmax += num;
+ event_set_changed(event);
+ return event;
+}
+
+struct event *
+event_add_timeval(struct event *event, const char *key,
+ const struct timeval *tv)
+{
+ struct event_field *field;
+
+ field = event_get_field(event, key, TRUE);
+ field->value_type = EVENT_FIELD_VALUE_TYPE_TIMEVAL;
+ field->value.timeval = *tv;
+ return event;
+}
+
+struct event *
+event_add_fields(struct event *event,
+ const struct event_add_field *fields)
+{
+ for (unsigned int i = 0; fields[i].key != NULL; i++) {
+ if (fields[i].value != NULL)
+ event_add_str(event, fields[i].key, fields[i].value);
+ else if (fields[i].value_timeval.tv_sec != 0) {
+ event_add_timeval(event, fields[i].key,
+ &fields[i].value_timeval);
+ } else {
+ event_add_int(event, fields[i].key,
+ fields[i].value_intmax);
+ }
+ }
+ return event;
+}
+
+void event_field_clear(struct event *event, const char *key)
+{
+ event_add_str(event, key, "");
+}
+
+struct event *event_get_parent(const struct event *event)
+{
+ return event->parent;
+}
+
+void event_get_create_time(const struct event *event, struct timeval *tv_r)
+{
+ *tv_r = event->tv_created;
+}
+
+bool event_get_last_send_time(const struct event *event, struct timeval *tv_r)
+{
+ *tv_r = event->tv_last_sent;
+ return tv_r->tv_sec != 0;
+}
+
+void event_get_last_duration(const struct event *event,
+ uintmax_t *duration_usecs_r)
+{
+ if (event->tv_last_sent.tv_sec == 0) {
+ *duration_usecs_r = 0;
+ return;
+ }
+ long long diff = timeval_diff_usecs(&event->tv_last_sent,
+ &event->tv_created);
+ i_assert(diff >= 0);
+ *duration_usecs_r = diff;
+}
+
+const struct event_field *
+event_get_fields(const struct event *event, unsigned int *count_r)
+{
+ if (!array_is_created(&event->fields)) {
+ *count_r = 0;
+ return NULL;
+ }
+ return array_get(&event->fields, count_r);
+}
+
+struct event_category *const *
+event_get_categories(const struct event *event, unsigned int *count_r)
+{
+ if (!array_is_created(&event->categories)) {
+ *count_r = 0;
+ return NULL;
+ }
+ return array_get(&event->categories, count_r);
+}
+
+void event_send(struct event *event, struct failure_context *ctx,
+ const char *fmt, ...)
+{
+ va_list args;
+
+ va_start(args, fmt);
+ event_vsend(event, ctx, fmt, args);
+ va_end(args);
+}
+
+void event_vsend(struct event *event, struct failure_context *ctx,
+ const char *fmt, va_list args)
+{
+ i_gettimeofday(&event->tv_last_sent);
+
+ /* Skip adding user_cpu_usecs if not enabled. */
+ if (event->ru_last.ru_utime.tv_sec != 0 ||
+ event->ru_last.ru_utime.tv_usec != 0) {
+ struct rusage ru_current;
+ get_self_rusage(&ru_current);
+ long long udiff = timeval_diff_usecs(&ru_current.ru_utime,
+ &event->ru_last.ru_utime);
+ event_add_int(event, "user_cpu_usecs", udiff > 0 ? udiff : 0);
+ }
+ if (event_call_callbacks(event, EVENT_CALLBACK_TYPE_SEND,
+ ctx, fmt, args)) {
+ if (ctx->type != LOG_TYPE_DEBUG ||
+ event->sending_debug_log)
+ i_log_typev(ctx, fmt, args);
+ }
+ event_send_abort(event);
+}
+
+void event_send_abort(struct event *event)
+{
+ /* if the event is sent again, it needs a new name */
+ i_free(event->sending_name);
+ if (event->passthrough)
+ event_unref(&event);
+}
+
+static void
+event_export_field_value(string_t *dest, const struct event_field *field)
+{
+ switch (field->value_type) {
+ case EVENT_FIELD_VALUE_TYPE_STR:
+ str_append_c(dest, EVENT_CODE_FIELD_STR);
+ str_append_tabescaped(dest, field->key);
+ str_append_c(dest, '\t');
+ str_append_tabescaped(dest, field->value.str);
+ break;
+ case EVENT_FIELD_VALUE_TYPE_INTMAX:
+ str_append_c(dest, EVENT_CODE_FIELD_INTMAX);
+ str_append_tabescaped(dest, field->key);
+ str_printfa(dest, "\t%jd", field->value.intmax);
+ break;
+ case EVENT_FIELD_VALUE_TYPE_TIMEVAL:
+ str_append_c(dest, EVENT_CODE_FIELD_TIMEVAL);
+ str_append_tabescaped(dest, field->key);
+ str_printfa(dest, "\t%"PRIdTIME_T"\t%u",
+ field->value.timeval.tv_sec,
+ (unsigned int)field->value.timeval.tv_usec);
+ break;
+ case EVENT_FIELD_VALUE_TYPE_STRLIST: {
+ unsigned int count;
+ const char *const *strlist =
+ array_get(&field->value.strlist, &count);
+ str_append_c(dest, EVENT_CODE_FIELD_STRLIST);
+ str_append_tabescaped(dest, field->key);
+ str_printfa(dest, "\t%u", count);
+ for (unsigned int i = 0; i < count; i++) {
+ str_append_c(dest, '\t');
+ str_append_tabescaped(dest, strlist[i]);
+ }
+ }
+ }
+}
+
+void event_export(const struct event *event, string_t *dest)
+{
+ /* required fields: */
+ str_printfa(dest, "%"PRIdTIME_T"\t%u",
+ event->tv_created.tv_sec,
+ (unsigned int)event->tv_created.tv_usec);
+
+ /* optional fields: */
+ if (event->source_filename != NULL) {
+ str_append_c(dest, '\t');
+ str_append_c(dest, EVENT_CODE_SOURCE);
+ str_append_tabescaped(dest, event->source_filename);
+ str_printfa(dest, "\t%u", event->source_linenum);
+ }
+ if (event->always_log_source) {
+ str_append_c(dest, '\t');
+ str_append_c(dest, EVENT_CODE_ALWAYS_LOG_SOURCE);
+ }
+ if (event->tv_last_sent.tv_sec != 0) {
+ str_printfa(dest, "\t%c%"PRIdTIME_T"\t%u",
+ EVENT_CODE_TV_LAST_SENT,
+ event->tv_last_sent.tv_sec,
+ (unsigned int)event->tv_last_sent.tv_usec);
+ }
+ if (event->sending_name != NULL) {
+ str_append_c(dest, '\t');
+ str_append_c(dest, EVENT_CODE_SENDING_NAME);
+ str_append_tabescaped(dest, event->sending_name);
+ }
+
+ if (array_is_created(&event->categories)) {
+ struct event_category *cat;
+ array_foreach_elem(&event->categories, cat) {
+ str_append_c(dest, '\t');
+ str_append_c(dest, EVENT_CODE_CATEGORY);
+ str_append_tabescaped(dest, cat->name);
+ }
+ }
+
+ if (array_is_created(&event->fields)) {
+ const struct event_field *field;
+ array_foreach(&event->fields, field) {
+ str_append_c(dest, '\t');
+ event_export_field_value(dest, field);
+ }
+ }
+}
+
+bool event_import(struct event *event, const char *str, const char **error_r)
+{
+ return event_import_unescaped(event, t_strsplit_tabescaped(str),
+ error_r);
+}
+
+static bool event_import_tv(const char *arg_secs, const char *arg_usecs,
+ struct timeval *tv_r, const char **error_r)
+{
+ unsigned int usecs;
+
+ if (str_to_time(arg_secs, &tv_r->tv_sec) < 0) {
+ *error_r = "Invalid timeval seconds parameter";
+ return FALSE;
+ }
+
+ if (arg_usecs == NULL) {
+ *error_r = "Timeval missing microseconds parameter";
+ return FALSE;
+ }
+ if (str_to_uint(arg_usecs, &usecs) < 0 || usecs >= 1000000) {
+ *error_r = "Invalid timeval microseconds parameter";
+ return FALSE;
+ }
+ tv_r->tv_usec = usecs;
+ return TRUE;
+}
+
+static bool
+event_import_strlist(struct event *event, struct event_field *field,
+ const char *const **_args, const char **error_r)
+{
+ const char *const *args = *_args;
+ unsigned int count, i;
+
+ field->value_type = EVENT_FIELD_VALUE_TYPE_STRLIST;
+ if (str_to_uint(args[0], &count) < 0) {
+ *error_r = t_strdup_printf("Field '%s' has invalid count: '%s'",
+ field->key, args[0]);
+ return FALSE;
+ }
+ p_array_init(&field->value.strlist, event->pool, count);
+ for (i = 1; i <= count && args[i] != NULL; i++) {
+ const char *str = p_strdup(event->pool, args[i]);
+ array_push_back(&field->value.strlist, &str);
+ }
+ if (i < count) {
+ *error_r = t_strdup_printf("Field '%s' has too few values",
+ field->key);
+ return FALSE;
+ }
+ *_args += count;
+ return TRUE;
+}
+
+static bool
+event_import_field(struct event *event, enum event_code code, const char *arg,
+ const char *const **_args, const char **error_r)
+{
+ const char *const *args = *_args;
+ const char *error;
+
+ if (*arg == '\0') {
+ *error_r = "Field name is missing";
+ return FALSE;
+ }
+ struct event_field *field = event_get_field(event, arg, TRUE);
+ if (args[0] == NULL) {
+ *error_r = "Field value is missing";
+ return FALSE;
+ }
+ switch (code) {
+ case EVENT_CODE_FIELD_INTMAX:
+ field->value_type = EVENT_FIELD_VALUE_TYPE_INTMAX;
+ if (str_to_intmax(*args, &field->value.intmax) < 0) {
+ *error_r = t_strdup_printf(
+ "Invalid field value '%s' number for '%s'",
+ *args, field->key);
+ return FALSE;
+ }
+ break;
+ case EVENT_CODE_FIELD_STR:
+ if (field->value_type == EVENT_FIELD_VALUE_TYPE_STR &&
+ null_strcmp(field->value.str, *args) == 0) {
+ /* already identical value */
+ break;
+ }
+ field->value_type = EVENT_FIELD_VALUE_TYPE_STR;
+ field->value.str = p_strdup(event->pool, *args);
+ break;
+ case EVENT_CODE_FIELD_TIMEVAL:
+ field->value_type = EVENT_FIELD_VALUE_TYPE_TIMEVAL;
+ if (!event_import_tv(args[0], args[1],
+ &field->value.timeval, &error)) {
+ *error_r = t_strdup_printf("Field '%s' value '%s': %s",
+ field->key, args[1], error);
+ return FALSE;
+ }
+ args++;
+ break;
+ case EVENT_CODE_FIELD_STRLIST:
+ if (!event_import_strlist(event, field, &args, error_r))
+ return FALSE;
+ break;
+ default:
+ i_unreached();
+ }
+ *_args = args;
+ return TRUE;
+}
+
+
+static bool
+event_import_arg(struct event *event, const char *const **_args,
+ const char **error_r)
+{
+ const char *const *args = *_args;
+ const char *error, *arg = *args;
+ enum event_code code = arg[0];
+
+ arg++;
+ switch (code) {
+ case EVENT_CODE_ALWAYS_LOG_SOURCE:
+ event->always_log_source = TRUE;
+ break;
+ case EVENT_CODE_CATEGORY: {
+ struct event_category *category =
+ event_category_find_registered(arg);
+ if (category == NULL) {
+ *error_r = t_strdup_printf(
+ "Unregistered category: '%s'", arg);
+ return FALSE;
+ }
+ if (!array_is_created(&event->categories))
+ p_array_init(&event->categories, event->pool, 4);
+ if (!event_find_category(event, category))
+ array_push_back(&event->categories, &category);
+ break;
+ }
+ case EVENT_CODE_TV_LAST_SENT:
+ if (!event_import_tv(arg, args[1], &event->tv_last_sent,
+ &error)) {
+ *error_r = t_strdup_printf(
+ "Invalid tv_last_sent: %s", error);
+ return FALSE;
+ }
+ args++;
+ break;
+ case EVENT_CODE_SENDING_NAME:
+ i_free(event->sending_name);
+ event->sending_name = i_strdup(arg);
+ break;
+ case EVENT_CODE_SOURCE: {
+ unsigned int linenum;
+
+ if (args[1] == NULL) {
+ *error_r = "Source line number missing";
+ return FALSE;
+ }
+ if (str_to_uint(args[1], &linenum) < 0) {
+ *error_r = "Invalid Source line number";
+ return FALSE;
+ }
+ event_set_source(event, arg, linenum, FALSE);
+ args++;
+ break;
+ }
+ case EVENT_CODE_FIELD_INTMAX:
+ case EVENT_CODE_FIELD_STR:
+ case EVENT_CODE_FIELD_STRLIST:
+ case EVENT_CODE_FIELD_TIMEVAL: {
+ args++;
+ if (!event_import_field(event, code, arg, &args, error_r))
+ return FALSE;
+ break;
+ }
+ }
+ *_args = args;
+ return TRUE;
+}
+
+bool event_import_unescaped(struct event *event, const char *const *args,
+ const char **error_r)
+{
+ const char *error;
+
+ /* Event's create callback has already added service:<name> category.
+ This imported event may be coming from another service process
+ though, so clear it out. */
+ if (array_is_created(&event->categories))
+ array_clear(&event->categories);
+
+ /* required fields: */
+ if (args[0] == NULL) {
+ *error_r = "Missing required fields";
+ return FALSE;
+ }
+ if (!event_import_tv(args[0], args[1], &event->tv_created, &error)) {
+ *error_r = t_strdup_printf("Invalid tv_created: %s", error);
+ return FALSE;
+ }
+ args += 2;
+
+ /* optional fields: */
+ while (*args != NULL) {
+ if (!event_import_arg(event, &args, error_r))
+ return FALSE;
+ args++;
+ }
+ return TRUE;
+}
+
+void event_register_callback(event_callback_t *callback)
+{
+ array_push_back(&event_handlers, &callback);
+}
+
+void event_unregister_callback(event_callback_t *callback)
+{
+ event_callback_t *const *callbackp;
+
+ array_foreach(&event_handlers, callbackp) {
+ if (*callbackp == callback) {
+ unsigned int idx =
+ array_foreach_idx(&event_handlers, callbackp);
+ array_delete(&event_handlers, idx, 1);
+ return;
+ }
+ }
+ i_unreached();
+}
+
+void event_category_register_callback(event_category_callback_t *callback)
+{
+ array_push_back(&event_category_callbacks, &callback);
+}
+
+void event_category_unregister_callback(event_category_callback_t *callback)
+{
+ event_category_callback_t *const *callbackp;
+
+ array_foreach(&event_category_callbacks, callbackp) {
+ if (*callbackp == callback) {
+ unsigned int idx =
+ array_foreach_idx(&event_category_callbacks,
+ callbackp);
+ array_delete(&event_category_callbacks, idx, 1);
+ return;
+ }
+ }
+ i_unreached();
+}
+
+static struct event_passthrough *
+event_passthrough_set_append_log_prefix(const char *prefix)
+{
+ event_set_append_log_prefix(last_passthrough_event(), prefix);
+ return &event_passthrough_vfuncs;
+}
+
+static struct event_passthrough *
+event_passthrough_replace_log_prefix(const char *prefix)
+{
+ event_replace_log_prefix(last_passthrough_event(), prefix);
+ return &event_passthrough_vfuncs;
+}
+
+static struct event_passthrough *
+event_passthrough_set_name(const char *name)
+{
+ event_set_name(last_passthrough_event(), name);
+ return &event_passthrough_vfuncs;
+}
+
+static struct event_passthrough *
+event_passthrough_set_source(const char *filename,
+ unsigned int linenum, bool literal_fname)
+{
+ event_set_source(last_passthrough_event(), filename,
+ linenum, literal_fname);
+ return &event_passthrough_vfuncs;
+}
+
+static struct event_passthrough *
+event_passthrough_set_always_log_source(void)
+{
+ event_set_always_log_source(last_passthrough_event());
+ return &event_passthrough_vfuncs;
+}
+
+static struct event_passthrough *
+event_passthrough_add_categories(struct event_category *const *categories)
+{
+ event_add_categories(last_passthrough_event(), categories);
+ return &event_passthrough_vfuncs;
+}
+
+static struct event_passthrough *
+event_passthrough_add_category(struct event_category *category)
+{
+ event_add_category(last_passthrough_event(), category);
+ return &event_passthrough_vfuncs;
+}
+
+static struct event_passthrough *
+event_passthrough_add_fields(const struct event_add_field *fields)
+{
+ event_add_fields(last_passthrough_event(), fields);
+ return &event_passthrough_vfuncs;
+}
+
+static struct event_passthrough *
+event_passthrough_add_str(const char *key, const char *value)
+{
+ event_add_str(last_passthrough_event(), key, value);
+ return &event_passthrough_vfuncs;
+}
+
+static struct event_passthrough *
+event_passthrough_strlist_append(const char *key, const char *value)
+{
+ event_strlist_append(last_passthrough_event(), key, value);
+ return &event_passthrough_vfuncs;
+}
+
+static struct event_passthrough *
+event_passthrough_strlist_replace(const char *key, const char *const *values,
+ unsigned int count)
+{
+ event_strlist_replace(last_passthrough_event(), key, values, count);
+ return &event_passthrough_vfuncs;
+}
+
+static struct event_passthrough *
+event_passthrough_add_int(const char *key, intmax_t num)
+{
+ event_add_int(last_passthrough_event(), key, num);
+ return &event_passthrough_vfuncs;
+}
+
+static struct event_passthrough *
+event_passthrough_add_int_nonzero(const char *key, intmax_t num)
+{
+ event_add_int_nonzero(last_passthrough_event(), key, num);
+ return &event_passthrough_vfuncs;
+}
+
+static struct event_passthrough *
+event_passthrough_add_timeval(const char *key, const struct timeval *tv)
+{
+ event_add_timeval(last_passthrough_event(), key, tv);
+ return &event_passthrough_vfuncs;
+}
+
+static struct event_passthrough *
+event_passthrough_inc_int(const char *key, intmax_t num)
+{
+ event_inc_int(last_passthrough_event(), key, num);
+ return &event_passthrough_vfuncs;
+}
+
+static struct event_passthrough *
+event_passthrough_clear_field(const char *key)
+{
+ event_field_clear(last_passthrough_event(), key);
+ return &event_passthrough_vfuncs;
+}
+
+static struct event *event_passthrough_event(void)
+{
+ struct event *event = last_passthrough_event();
+ event_last_passthrough = NULL;
+ return event;
+}
+
+struct event_passthrough event_passthrough_vfuncs = {
+ .append_log_prefix = event_passthrough_set_append_log_prefix,
+ .replace_log_prefix = event_passthrough_replace_log_prefix,
+ .set_name = event_passthrough_set_name,
+ .set_source = event_passthrough_set_source,
+ .set_always_log_source = event_passthrough_set_always_log_source,
+ .add_categories = event_passthrough_add_categories,
+ .add_category = event_passthrough_add_category,
+ .add_fields = event_passthrough_add_fields,
+ .add_str = event_passthrough_add_str,
+ .add_int = event_passthrough_add_int,
+ .add_int_nonzero = event_passthrough_add_int_nonzero,
+ .add_timeval = event_passthrough_add_timeval,
+ .inc_int = event_passthrough_inc_int,
+ .strlist_append = event_passthrough_strlist_append,
+ .strlist_replace = event_passthrough_strlist_replace,
+ .clear_field = event_passthrough_clear_field,
+ .event = event_passthrough_event,
+};
+
+void event_enable_user_cpu_usecs(struct event *event)
+{
+ get_self_rusage(&event->ru_last);
+}
+
+void lib_event_init(void)
+{
+ i_array_init(&event_handlers, 4);
+ i_array_init(&event_category_callbacks, 4);
+ i_array_init(&event_registered_categories_internal, 16);
+ i_array_init(&event_registered_categories_representative, 16);
+}
+
+void lib_event_deinit(void)
+{
+ struct event_internal_category *internal;
+
+ event_unset_global_debug_log_filter();
+ event_unset_global_debug_send_filter();
+ event_unset_global_core_log_filter();
+ for (struct event *event = events; event != NULL; event = event->next) {
+ i_warning("Event %p leaked (parent=%p): %s:%u",
+ event, event->parent,
+ event->source_filename, event->source_linenum);
+ }
+ /* categories cannot be unregistered, so just free them here */
+ array_foreach_elem(&event_registered_categories_internal, internal) {
+ i_free(internal->name);
+ i_free(internal);
+ }
+ array_free(&event_handlers);
+ array_free(&event_category_callbacks);
+ array_free(&event_registered_categories_internal);
+ array_free(&event_registered_categories_representative);
+ array_free(&global_event_stack);
+}
diff --git a/src/lib/lib-event.h b/src/lib/lib-event.h
new file mode 100644
index 0000000..74aafd7
--- /dev/null
+++ b/src/lib/lib-event.h
@@ -0,0 +1,432 @@
+#ifndef LIB_EVENT_H
+#define LIB_EVENT_H
+/* event.h name is probably a bit too generic, so lets avoid using it. */
+
+#include <sys/time.h>
+
+/* Field name for the reason_code string list. */
+#define EVENT_REASON_CODE "reason_code"
+
+struct event;
+struct event_log_params;
+
+/* Hierarchical category of events. Each event can belong to multiple
+ categories. For example [ lib-storage/maildir, syscall/io ]. The categories
+ are expected to live as long as they're used in events. */
+struct event_category {
+ struct event_category *parent;
+ const char *name;
+
+ /* non-NULL if this category has been registered
+
+ Do NOT dereference outside of event code in src/lib.
+
+ At any point in time it is safe to (1) check the pointer for
+ NULL/non-NULL to determine if this particular category instance
+ has been registered, and (2) compare two categories' internal
+ pointers to determine if they represent the same category. */
+ void *internal;
+};
+
+enum event_field_value_type {
+ EVENT_FIELD_VALUE_TYPE_STR,
+ EVENT_FIELD_VALUE_TYPE_INTMAX,
+ EVENT_FIELD_VALUE_TYPE_TIMEVAL,
+ EVENT_FIELD_VALUE_TYPE_STRLIST,
+};
+
+struct event_field {
+ const char *key;
+ enum event_field_value_type value_type;
+ struct {
+ const char *str;
+ intmax_t intmax;
+ struct timeval timeval;
+ ARRAY_TYPE(const_string) strlist;
+ } value;
+};
+
+struct event_add_field {
+ const char *key;
+ /* The first non-0/NULL value is used. */
+ const char *value;
+ intmax_t value_intmax;
+ struct timeval value_timeval;
+};
+
+struct event_passthrough {
+ /* wrappers to event_set_*() and event_add_*() for passthrough events,
+ so these can be chained like:
+ event_create_passthrough(parent)->name("name")->...->event() */
+ struct event_passthrough *
+ (*append_log_prefix)(const char *prefix);
+ struct event_passthrough *
+ (*replace_log_prefix)(const char *prefix);
+ struct event_passthrough *
+ (*set_name)(const char *name);
+ struct event_passthrough *
+ (*set_source)(const char *filename,
+ unsigned int linenum, bool literal_fname);
+ struct event_passthrough *
+ (*set_always_log_source)(void);
+
+ struct event_passthrough *
+ (*add_categories)(struct event_category *const *categories);
+ struct event_passthrough *
+ (*add_category)(struct event_category *category);
+ struct event_passthrough *
+ (*add_fields)(const struct event_add_field *fields);
+
+ struct event_passthrough *
+ (*add_str)(const char *key, const char *value);
+ struct event_passthrough *
+ (*add_int)(const char *key, intmax_t num);
+ struct event_passthrough *
+ (*add_int_nonzero)(const char *key, intmax_t num);
+ struct event_passthrough *
+ (*add_timeval)(const char *key, const struct timeval *tv);
+
+ struct event_passthrough *
+ (*inc_int)(const char *key, intmax_t num);
+
+ struct event_passthrough *
+ (*strlist_append)(const char *key, const char *value);
+ struct event_passthrough *
+ (*strlist_replace)(const char *key, const char *const *value,
+ unsigned int count);
+
+ struct event_passthrough *
+ (*clear_field)(const char *key);
+
+ struct event *(*event)(void);
+};
+
+typedef const char *
+event_log_prefix_callback_t(void *context);
+typedef const char *
+event_log_message_callback_t(void *context, enum log_type log_type,
+ const char *message);
+
+/* Returns TRUE if the event has all the categories that the "other" event has
+ (and maybe more). */
+bool event_has_all_categories(struct event *event, const struct event *other);
+/* Returns TRUE if the event has all the fields that the "other" event has
+ (and maybe more). Only the fields in the events themselves are checked.
+ Parent events' fields are not checked. */
+bool event_has_all_fields(struct event *event, const struct event *other);
+
+/* Returns the source event duplicated into a new event. Event pointers are
+ dropped. */
+struct event *event_dup(const struct event *source);
+/* Returns a flattened version of the source event.
+ Both categories and fields will be flattened.
+ A new reference to the source event is returned if no flattening was
+ needed. Event pointers are dropped if a new event was created. */
+struct event *event_flatten(struct event *src);
+/* Returns a minimized version of the source event.
+ Remove parents with no fields or categories, attempt to flatten fields
+ and categories to avoid sending one-off parent events. (There is a more
+ detailed description in a comment above the function implementation.)
+ A new reference to the source event is returned if no simplification
+ occured. Event pointers are dropped if a new event was created. */
+struct event *event_minimize(struct event *src);
+/* Copy all categories from source to dest.
+ Only the categories in source event itself are copied.
+ Parent events' categories aren't copied. */
+void event_copy_categories(struct event *to, struct event *from);
+/* Copy all fields from source to dest.
+ Only the fields in source event itself are copied.
+ Parent events' fields aren't copied. */
+void event_copy_fields(struct event *to, struct event *from);
+
+/* Create a new empty event under the parent event, or NULL for root event. */
+struct event *event_create(struct event *parent, const char *source_filename,
+ unsigned int source_linenum);
+#define event_create(parent) \
+ event_create((parent), __FILE__, __LINE__)
+/* This is a temporary "passthrough" event. Its main purpose is to make it
+ easier to create temporary events as part of the event parameter in
+ e_error(), e_warning(), e_info() or e_debug(). These passthrough events are
+ automatically freed when the e_*() call is finished. Because this makes the
+ freeing less obvious, it should be avoided outside e_*()'s event parameter.
+
+ The passthrough events also change the API to be more convenient towards
+ being used in a parameter. Instead of having to use e.g.
+ event_add_str(event_set_name(event_create(parent), "name"), "key", "value")
+ the event_passthrough API can be a bit more readable as:
+ event_create_passthrough(parent)->set_name("name")->
+ add_str("key", "value")->event(). The passthrough event is converted to
+ a normal event at the end with the event() call. Note that this API works
+ by modifying the last created passthrough event, so it's not possible to
+ have multiple passthrough events created in parallel. */
+struct event_passthrough *
+event_create_passthrough(struct event *parent, const char *source_filename,
+ unsigned int source_linenum);
+#define event_create_passthrough(parent) \
+ event_create_passthrough((parent), __FILE__, __LINE__)
+
+/* Reference the event. Returns the event parameter. */
+struct event *event_ref(struct event *event);
+/* Unreference the event. If the reference count drops to 0, the event is
+ freed. The current global event's refcount must not drop to 0. */
+void event_unref(struct event **event);
+
+/* Set the event to be the global event and push it at the top of the global
+ event stack. Returns the event parameter. The event must be explicitly
+ popped before it's freed.
+
+ The global event acts as the root event for all the events while they are
+ being emitted. The global events don't permanently affect the event
+ hierarchy. The global events are typically used to add extra fields to all
+ emitted events while some specific work is running.
+
+ For example the global event can be "IMAP command SELECT", which can be used
+ for filtering events that happen while the SELECT command is being executed.
+ However, for the created struct mailbox the parent event should be the
+ mail_user, not the SELECT command. (If the mailbox used SELECT command as
+ the parent event, then any future event emitted via the mailbox event would
+ show SELECT command as the parent, even after SELECT had already finished.)
+
+ The global event works the same as if all the events' roots were instead
+ pointing to the global event. Global events don't affect log prefixes.
+
+ If ioloop contexts are used, the global events will automatically follow the
+ contexts. Any global events pushed while running in a context are popped
+ out when the context is deactivated, and pushed back when context is
+ activated again.
+
+ The created global events should use event_get_global() as their parent
+ event. Only the last pushed global event is used. */
+struct event *event_push_global(struct event *event);
+/* Pop the current global event and set the global event to the next one at
+ the top of the stack. Assert-crash if the current global event isn't the
+ given event parameter. Returns the next (now activated) global event in the
+ stack, or NULL if the stack is now empty. */
+struct event *event_pop_global(struct event *event);
+/* Returns the current global event. */
+struct event *event_get_global(void);
+
+/* Shortcut to create and push a global event and set its reason_code field. */
+struct event_reason *
+event_reason_begin(const char *reason_code, const char *source_filename,
+ unsigned int source_linenum);
+#define event_reason_begin(reason_code) \
+ event_reason_begin(reason_code, __FILE__, __LINE__)
+/* Finish the reason event. It pops the global event, which means it must be
+ at the top of the stack. */
+void event_reason_end(struct event_reason **reason);
+/* Generate a reason code as <module>:<name>. This function does some
+ sanity checks and conversions to make sure the reason codes are reasonable:
+
+ - Assert-crash if module has space, '-', ':' or uppercase characters.
+ - Assert-crash if module is empty
+ - Convert name to lowercase.
+ - Replace all space and '-' in name with '_'.
+ - Assert-crash if name has ':'
+ - assert-crash if name is empty
+*/
+const char *event_reason_code(const char *module, const char *name);
+/* Same as event_reason_code(), but concatenate name_prefix and name.
+ The name_prefix must not contain spaces, '-', ':' or uppercase characters. */
+const char *event_reason_code_prefix(const char *module,
+ const char *name_prefix, const char *name);
+
+/* Set the appended log prefix string for this event. All the parent events'
+ log prefixes will be concatenated together when logging. The log type
+ text (e.g. "Info: ") will be inserted before appended log prefixes (but
+ after replaced log prefix).
+
+ Clears log_prefix callback.
+ */
+struct event *
+event_set_append_log_prefix(struct event *event, const char *prefix);
+/* Replace the full log prefix string for this event. The parent events' log
+ prefixes won't be used. Also, any parent event's message amendment callback
+ is not used.
+
+ Clears log_prefix callback.
+*/
+struct event *event_replace_log_prefix(struct event *event, const char *prefix);
+
+/* Drop count prefixes from parents when this event is used for logging. This
+ does not affect the parent events. This only counts actual prefixes and not
+ parents. If the count is higher than the actual number of prefixes added by
+ parents, all will be dropped. */
+struct event *
+event_drop_parent_log_prefixes(struct event *event, unsigned int count);
+
+/* Sets event prefix callback, sets log_prefix empty */
+struct event *
+event_set_log_prefix_callback(struct event *event, bool replace,
+ event_log_prefix_callback_t *callback,
+ void *context);
+#define event_set_log_prefix_callback(event, replace, callback, context) \
+ event_set_log_prefix_callback(event, replace, \
+ (event_log_prefix_callback_t*)callback, TRUE ? context : \
+ CALLBACK_TYPECHECK(callback, const char *(*)(typeof(context))))
+
+/* Sets event message amendment callback */
+struct event *
+event_set_log_message_callback(struct event *event,
+ event_log_message_callback_t *callback,
+ void *context);
+#define event_set_log_message_callback(event, callback, context) \
+ event_set_log_message_callback(event, \
+ (event_log_message_callback_t*)callback, TRUE ? context : \
+ CALLBACK_TYPECHECK(callback, \
+ const char *(*)(typeof(context), enum log_type, \
+ const char *)))
+
+/* Set the event's name. The name is specific to a single sending of an event,
+ and it'll be automatically cleared once the event is sent. This should
+ typically be used only in a parameter to e_debug(), etc. */
+struct event *
+event_set_name(struct event *event, const char *name);
+/* Set the source filename:linenum to the event. If literal_fname==TRUE,
+ it's assumed that __FILE__ has been used and the pointer is stored directly,
+ otherwise the filename is strdup()ed. */
+struct event *
+event_set_source(struct event *event, const char *filename,
+ unsigned int linenum, bool literal_fname);
+/* Always include the source path:line in the log replies. This is
+ especially useful when logging about unexpected syscall failures, because
+ it allow quickly finding which of the otherwise identical syscalls in the
+ code generated the error. */
+struct event *event_set_always_log_source(struct event *event);
+/* Set minimum normal log level for the event. By default events with INFO
+ level and higher are logged. This can be used to easily hide even the INFO
+ log lines unless some verbose-setting is enabled.
+
+ Note that this functionality is mostly independent of debug logging.
+ Don't use this to enable debug log - use event_set_forced_debug() instead. */
+struct event *event_set_min_log_level(struct event *event, enum log_type level);
+enum log_type event_get_min_log_level(const struct event *event);
+
+/* Add an internal pointer to an event. It can be looked up only with
+ event_get_ptr(). The keys are in their own namespace and won't conflict
+ with event fields. The pointers are specific to this specific event only -
+ they will be dropped from any duplicated/flattened/minimized events. */
+struct event *event_set_ptr(struct event *event, const char *key, void *value);
+/* Return a pointer set with event_set_ptr(), or NULL if it doesn't exist.
+ The pointer is looked up only from the event itself, not its parents. */
+void *event_get_ptr(const struct event *event, const char *key);
+
+/* Add NULL-terminated list of categories to the event. The categories pointer
+ doesn't need to stay valid afterwards, but the event_category structs
+ themselves must be. Returns the event parameter. */
+struct event *
+event_add_categories(struct event *event,
+ struct event_category *const *categories);
+/* Add a single category to the event. */
+struct event *
+event_add_category(struct event *event, struct event_category *category);
+
+/* Add key=value field to the event. If a key already exists, it's replaced.
+ Child events automatically inherit key=values from their parents at the
+ time the event is sent. So changing a key in parent will change the values
+ in the child events as well, unless the key has been overwritten in the
+ child event. Setting the value to "" is the same as event_field_clear().
+ Returns the event parameter. */
+struct event *
+event_add_str(struct event *event, const char *key, const char *value);
+struct event *
+event_add_int(struct event *event, const char *key, intmax_t num);
+/* Adds int value to event if it is non-zero */
+struct event *
+event_add_int_nonzero(struct event *event, const char *key, intmax_t num);
+/* Increase the key's value. If it's not set or isn't an integer type,
+ initialize the value to num. */
+struct event *
+event_inc_int(struct event *event, const char *key, intmax_t num);
+struct event *
+event_add_timeval(struct event *event, const char *key,
+ const struct timeval *tv);
+/* Append new value to list. If the key is not a list, it will
+ be cleared first. NULL values are ignored. Duplicate values are ignored. */
+struct event *
+event_strlist_append(struct event *event, const char *key, const char *value);
+/* Replace value with this strlist. */
+struct event *
+event_strlist_replace(struct event *event, const char *key,
+ const char *const *value, unsigned int count);
+/* Copy the string list from src and its parents to dest. This can be especially
+ useful to copy the current global events' reason_codes to a more permanent
+ (e.g. async) event that can exist after the global events are popped out. */
+struct event *
+event_strlist_copy_recursive(struct event *dest, const struct event *src,
+ const char *key);
+/* Same as event_add_str/int(), but do it via event_field struct. The fields
+ terminates with key=NULL. Returns the event parameter. */
+struct event *
+event_add_fields(struct event *event, const struct event_add_field *fields);
+/* Mark a field as nonexistent. If a parent event has the field set, this
+ allows removing it from the child event. Using an event filter with e.g.
+ "key=*" won't match this field anymore, although it's still visible in
+ event_find_field*() and event_get_fields(). This is the same as using
+ event_add_str() with value="". */
+void event_field_clear(struct event *event, const char *key);
+
+/* Returns the parent event, or NULL if it doesn't exist. */
+struct event *event_get_parent(const struct event *event);
+/* Get the event's creation time. */
+void event_get_create_time(const struct event *event, struct timeval *tv_r);
+/* Get the time when the event was last sent. Returns TRUE if time was
+ returned, FALSE if event has never been sent. */
+bool event_get_last_send_time(const struct event *event, struct timeval *tv_r);
+/* Get the event duration field in microseconds. This is calculated from
+ the event's last sent time. */
+void event_get_last_duration(const struct event *event,
+ uintmax_t *duration_usecs_r);
+/* Returns field for a given key, or NULL if it doesn't exist. */
+struct event_field *
+event_find_field_nonrecursive(const struct event *event, const char *key);
+/* Returns field for a given key, or NULL if it doesn't exist. If the key
+ isn't found from the event itself, find it from parent events, including
+ from the global event. */
+const struct event_field *
+event_find_field_recursive(const struct event *event, const char *key);
+/* Same as event_find_field(), but return the value converted to a string.
+ If the field isn't stored as a string, the result is allocated from
+ data stack. */
+const char *
+event_find_field_recursive_str(const struct event *event, const char *key);
+/* Returns all key=value fields that the event has.
+ Parent events' fields aren't returned. */
+const struct event_field *
+event_get_fields(const struct event *event, unsigned int *count_r);
+/* Return all categories that the event has.
+ Parent events' categories aren't returned. */
+struct event_category *const *
+event_get_categories(const struct event *event, unsigned int *count_r);
+
+/* Export the event into a tabescaped string, so its fields are separated
+ with TABs and there are no NUL, CR or LF characters. */
+void event_export(const struct event *event, string_t *dest);
+/* Import event. The string is expected to be generated by event_export().
+ All the used categories must already be registered.
+ Returns TRUE on success, FALSE on invalid string. */
+bool event_import(struct event *event, const char *str, const char **error_r);
+/* Same as event_import(), but string is already split into an array
+ of strings via *_strsplit_tabescaped(). */
+bool event_import_unescaped(struct event *event, const char *const *args,
+ const char **error_r);
+
+/* The event wasn't sent after all - free everything related to it.
+ Most importantly this frees any passthrough events. Typically this shouldn't
+ need to be called. */
+void event_send_abort(struct event *event);
+
+/* Enable "user_cpu_usecs" event field to event by getting current resource
+ usage which will be used in consequent event_send() to calculate
+ cpu time. This function can be called multiple times to update the current
+ resource usage.
+
+ The "user_cpu_usecs" field is automatically inherited by passthrough events,
+ but not full events.
+*/
+void event_enable_user_cpu_usecs(struct event *event);
+
+void lib_event_init(void);
+void lib_event_deinit(void);
+
+#endif
diff --git a/src/lib/lib-signals.c b/src/lib/lib-signals.c
new file mode 100644
index 0000000..6ac657a
--- /dev/null
+++ b/src/lib/lib-signals.c
@@ -0,0 +1,702 @@
+/* Copyright (c) 2001-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "ioloop.h"
+#include "write-full.h"
+#include "llist.h"
+#include "lib-signals.h"
+
+#include <stdio.h>
+#include <signal.h>
+#include <unistd.h>
+
+#define MAX_SIGNAL_VALUE 63
+
+#define SIGNAL_IS_TERMINAL(signo) \
+ ((signo) == SIGINT || (signo) == SIGQUIT || (signo) == SIGTERM)
+
+#if !defined(SA_SIGINFO) && !defined(SI_NOINFO)
+/* without SA_SIGINFO we don't know what the real code is. we need SI_NOINFO
+ to make sure lib_signal_code_to_str() returns "". */
+# define SI_NOINFO -1
+#endif
+
+struct signal_ioloop {
+ struct signal_ioloop *prev, *next;
+
+ int refcount;
+ struct ioloop *ioloop;
+ struct io *io;
+};
+
+struct signal_handler {
+ signal_handler_t *handler;
+ void *context;
+
+ enum libsig_flags flags;
+ struct signal_handler *next;
+ struct signal_ioloop *sig_ioloop;
+
+ bool expected:1;
+ bool shadowed:1;
+};
+
+volatile unsigned int signal_term_counter = 0;
+
+/* Remember that these are accessed inside signal handler which may be called
+ even while we're initializing/deinitializing. Try hard to keep everything
+ in consistent state. */
+static struct signal_handler *signal_handlers[MAX_SIGNAL_VALUE+1] = { NULL, };
+static int sig_pipe_fd[2] = { -1, -1 };
+
+static bool signals_initialized = FALSE;
+static unsigned int signals_expected = 0;
+static struct signal_ioloop *signal_ioloops = NULL;
+
+static siginfo_t pending_signals[MAX_SIGNAL_VALUE+1];
+static ARRAY(siginfo_t) pending_shadowed_signals;
+static bool have_pending_signals = FALSE;
+static bool have_missing_ioloops = FALSE;
+static bool ioloop_switched = FALSE;
+
+static void signal_read(void *context);
+
+const char *lib_signal_code_to_str(int signo, int sicode)
+{
+ /* common */
+ switch (sicode) {
+#ifdef SI_NOINFO
+ case SI_NOINFO:
+ return "";
+#endif
+ case SI_USER:
+ return "kill";
+#ifdef SI_KERNEL
+ case SI_KERNEL:
+ return "kernel";
+#endif
+ case SI_TIMER:
+ return "timer";
+ }
+
+ /* If SEGV_MAPERR is supported, the rest of them must be too.
+ FreeBSD 6 at least doesn't support these. */
+#ifdef SEGV_MAPERR
+ switch (signo) {
+ case SIGSEGV:
+ switch (sicode) {
+ case SEGV_MAPERR:
+ return "address not mapped";
+ case SEGV_ACCERR:
+ return "invalid permissions";
+ }
+ break;
+ case SIGBUS:
+ switch (sicode) {
+ case BUS_ADRALN:
+ return "invalid address alignment";
+#ifdef BUS_ADRERR /* for OSX 10.3 */
+ case BUS_ADRERR:
+ return "nonexistent physical address";
+#endif
+#ifdef BUS_OBJERR /* for OSX 10.3 */
+ case BUS_OBJERR:
+ return "object-specific hardware error";
+#endif
+ }
+ }
+#endif
+ return t_strdup_printf("unknown %d", sicode);
+}
+
+#ifdef SA_SIGINFO
+static void sig_handler(int signo, siginfo_t *si, void *context ATTR_UNUSED)
+#else
+static void sig_handler(int signo)
+#endif
+{
+ struct signal_handler *h;
+ int saved_errno;
+ char c = 0;
+
+#if defined(SI_NOINFO) || !defined(SA_SIGINFO)
+#ifndef SA_SIGINFO
+ siginfo_t *si = NULL;
+#endif
+ siginfo_t tmp_si;
+
+ if (si == NULL) {
+ /* Solaris can leave this to NULL */
+ i_zero(&tmp_si);
+ tmp_si.si_signo = signo;
+ tmp_si.si_code = SI_NOINFO;
+ si = &tmp_si;
+ }
+#endif
+
+ if (signo < 0 || signo > MAX_SIGNAL_VALUE)
+ return;
+
+ if (SIGNAL_IS_TERMINAL(signo))
+ signal_term_counter++;
+
+ /* remember that we're inside a signal handler which might have been
+ called at any time. don't do anything that's unsafe. we might also
+ get interrupted by another signal while inside this handler. */
+ saved_errno = errno;
+ for (h = signal_handlers[signo]; h != NULL; h = h->next) {
+ if ((h->flags & LIBSIG_FLAG_DELAYED) == 0)
+ h->handler(si, h->context);
+ else if (pending_signals[signo].si_signo == 0) {
+ pending_signals[signo] = *si;
+ if (!have_pending_signals) {
+ if (write(sig_pipe_fd[1], &c, 1) != 1) {
+ lib_signals_syscall_error(
+ "signal: write(sigpipe) failed: ");
+ }
+ have_pending_signals = TRUE;
+ }
+ }
+ }
+ errno = saved_errno;
+}
+
+#ifdef SA_SIGINFO
+static void sig_ignore(int signo ATTR_UNUSED, siginfo_t *si ATTR_UNUSED,
+ void *context ATTR_UNUSED)
+#else
+static void sig_ignore(int signo ATTR_UNUSED)
+#endif
+{
+ /* if we used SIG_IGN instead of this function,
+ the system call might be restarted */
+}
+
+static struct signal_ioloop *
+lib_signals_ioloop_find(struct ioloop *ioloop)
+{
+ struct signal_ioloop *l;
+
+ for (l = signal_ioloops; l != NULL; l = l->next) {
+ if (l->ioloop == ioloop)
+ break;
+ }
+ return l;
+}
+
+static void lib_signals_init_io(struct signal_ioloop *l)
+{
+ i_assert(sig_pipe_fd[0] != -1);
+
+ l->io = io_add_to(l->ioloop, sig_pipe_fd[0], IO_READ, signal_read, NULL);
+ io_set_never_wait_alone(l->io, signals_expected == 0);
+}
+
+static struct signal_ioloop *
+lib_signals_ioloop_ref(struct ioloop *ioloop)
+{
+ struct signal_ioloop *l;
+
+ l = lib_signals_ioloop_find(ioloop);
+ if (l == NULL) {
+ l = i_new(struct signal_ioloop, 1);
+ l->ioloop = ioloop;
+ lib_signals_init_io(l);
+ DLLIST_PREPEND(&signal_ioloops, l);
+ }
+ l->refcount++;
+ return l;
+}
+
+static void lib_signals_ioloop_unref(struct signal_ioloop **_sig_ioloop)
+{
+ struct signal_ioloop *sig_ioloop = *_sig_ioloop;
+
+ *_sig_ioloop = NULL;
+
+ if (sig_ioloop == NULL)
+ return;
+ i_assert(sig_ioloop->refcount > 0);
+ if (--sig_ioloop->refcount > 0)
+ return;
+ io_remove(&sig_ioloop->io);
+ DLLIST_REMOVE(&signal_ioloops, sig_ioloop);
+ i_free(sig_ioloop);
+}
+
+static void signal_handler_switch_ioloop(struct signal_handler *h)
+{
+ lib_signals_ioloop_unref(&h->sig_ioloop);
+ if (current_ioloop != NULL)
+ h->sig_ioloop = lib_signals_ioloop_ref(current_ioloop);
+ else
+ have_missing_ioloops = TRUE;
+}
+
+static void signal_handler_free(struct signal_handler *h)
+{
+ lib_signals_ioloop_unref(&h->sig_ioloop);
+ i_free(h);
+}
+
+static void signal_handle_shadowed(void)
+{
+ const siginfo_t *sis;
+ unsigned int count, i;
+
+ if (!array_is_created(&pending_shadowed_signals) ||
+ array_count(&pending_shadowed_signals) == 0)
+ return;
+
+ sis = array_get(&pending_shadowed_signals, &count);
+ for (i = 0; i < count; i++) {
+ struct signal_handler *h;
+ bool shadowed = FALSE;
+
+ i_assert(sis[i].si_signo > 0);
+ for (h = signal_handlers[sis[i].si_signo]; h != NULL;
+ h = h->next) {
+ i_assert(h->sig_ioloop != NULL);
+ if ((h->flags & LIBSIG_FLAG_DELAYED) == 0 ||
+ (h->flags & LIBSIG_FLAG_IOLOOP_AUTOMOVE) != 0)
+ continue;
+ if (h->shadowed &&
+ h->sig_ioloop->ioloop != current_ioloop) {
+ shadowed = TRUE;
+ continue;
+ }
+ /* handler can be called now */
+ h->shadowed = FALSE;
+ h->handler(&sis[i], h->context);
+ }
+ if (!shadowed) {
+ /* no handlers are shadowed anymore; delete the signal
+ info */
+ array_delete(&pending_shadowed_signals, i, 1);
+ sis = array_get(&pending_shadowed_signals, &count);
+ }
+ }
+}
+
+static void signal_check_shadowed(void)
+{
+ struct signal_ioloop *sig_ioloop;
+
+ if (!array_is_created(&pending_shadowed_signals) ||
+ array_count(&pending_shadowed_signals) == 0)
+ return;
+
+ sig_ioloop = lib_signals_ioloop_find(current_ioloop);
+ if (sig_ioloop != NULL)
+ io_set_pending(sig_ioloop->io);
+}
+
+static void signal_shadow(int signo, const siginfo_t *si)
+{
+ const siginfo_t *sis;
+ unsigned int count, i;
+
+ /* remember last signal info for handlers that cannot run in
+ current ioloop */
+ if (!array_is_created(&pending_shadowed_signals))
+ i_array_init(&pending_shadowed_signals, 4);
+ sis = array_get(&pending_shadowed_signals, &count);
+ for (i = 0; i < count; i++) {
+ i_assert(sis[i].si_signo != 0);
+ if (sis[i].si_signo == signo)
+ break;
+ }
+ array_idx_set(&pending_shadowed_signals, i, si);
+}
+
+static void ATTR_NULL(1) signal_read(void *context ATTR_UNUSED)
+{
+ siginfo_t signals[MAX_SIGNAL_VALUE+1];
+ sigset_t fullset, oldset;
+ struct signal_handler *h;
+ char buf[64];
+ int signo;
+ ssize_t ret;
+
+ if (ioloop_switched) {
+ ioloop_switched = FALSE;
+ /* handle any delayed signal handlers that emerged from the
+ shadow */
+ signal_handle_shadowed();
+ }
+
+ if (sigfillset(&fullset) < 0)
+ i_fatal("sigfillset() failed: %m");
+ if (sigprocmask(SIG_BLOCK, &fullset, &oldset) < 0)
+ i_fatal("sigprocmask() failed: %m");
+
+ /* typically we should read only a single byte, but if a signal is sent
+ while signal handler is running we might get more. */
+ ret = read(sig_pipe_fd[0], buf, sizeof(buf));
+ if (ret > 0) {
+ memcpy(signals, pending_signals, sizeof(signals));
+ memset(pending_signals, 0, sizeof(pending_signals));
+ have_pending_signals = FALSE;
+ } else if (ret < 0) {
+ if (errno != EAGAIN)
+ i_fatal("read(sigpipe) failed: %m");
+ } else {
+ i_fatal("read(sigpipe) failed: EOF");
+ }
+ if (sigprocmask(SIG_SETMASK, &oldset, NULL) < 0)
+ i_fatal("sigprocmask() failed: %m");
+
+ if (ret < 0)
+ return;
+
+ /* call the delayed handlers after signals are copied and unblocked */
+ for (signo = 0; signo < MAX_SIGNAL_VALUE; signo++) {
+ bool shadowed = FALSE;
+
+ if (signals[signo].si_signo == 0)
+ continue;
+
+ for (h = signal_handlers[signo]; h != NULL; h = h->next) {
+ i_assert(h->sig_ioloop != NULL);
+ if ((h->flags & LIBSIG_FLAG_DELAYED) == 0) {
+ /* handler already called immediately in signal
+ context */
+ continue;
+ }
+ if ((h->flags & LIBSIG_FLAG_IOLOOP_AUTOMOVE) == 0 &&
+ h->sig_ioloop->ioloop != current_ioloop) {
+ /* cannot run handler in current ioloop
+ (shadowed) */
+ h->shadowed = TRUE;
+ shadowed = TRUE;
+ continue;
+ }
+ /* handler can be called now */
+ h->handler(&signals[signo], h->context);
+ }
+
+ if (shadowed) {
+ /* remember last signal info for handlers that cannot
+ run in current ioloop (shadowed) */
+ signal_shadow(signo, &signals[signo]);
+ }
+ }
+}
+
+static void lib_signals_update_expected_signals(bool expected)
+{
+ struct signal_ioloop *sig_ioloop;
+
+ if (expected)
+ signals_expected++;
+ else {
+ i_assert(signals_expected > 0);
+ signals_expected--;
+ }
+
+ sig_ioloop = signal_ioloops;
+ for (; sig_ioloop != NULL; sig_ioloop = sig_ioloop->next) {
+ if (sig_ioloop->io != NULL) {
+ io_set_never_wait_alone(sig_ioloop->io,
+ signals_expected == 0);
+ }
+ }
+}
+
+static void lib_signals_ioloop_switch(void)
+{
+ struct signal_handler *h;
+
+ if (current_ioloop == NULL || sig_pipe_fd[0] <= 0)
+ return;
+
+ /* initialize current_ioloop for signal handlers created before the
+ first ioloop. */
+ for (int signo = 0; signo < MAX_SIGNAL_VALUE; signo++) {
+ for (h = signal_handlers[signo]; h != NULL; h = h->next) {
+ if ((h->flags & LIBSIG_FLAG_IOLOOP_AUTOMOVE) != 0)
+ lib_signals_ioloop_unref(&h->sig_ioloop);
+ if (h->sig_ioloop == NULL)
+ h->sig_ioloop = lib_signals_ioloop_ref(current_ioloop);
+ }
+ }
+ have_missing_ioloops = FALSE;
+}
+
+static void lib_signals_ioloop_switched(struct ioloop *prev_ioloop ATTR_UNUSED)
+{
+ ioloop_switched = TRUE;
+
+ lib_signals_ioloop_switch();
+
+ /* check whether we can now handle any shadowed delayed signals */
+ signal_check_shadowed();
+}
+
+static void lib_signals_ioloop_destroyed(struct ioloop *ioloop)
+{
+ struct signal_ioloop *sig_ioloop;
+
+ sig_ioloop = lib_signals_ioloop_find(ioloop);
+ if (sig_ioloop != NULL) {
+ io_remove(&sig_ioloop->io);
+ sig_ioloop->ioloop = NULL;
+ }
+}
+
+void lib_signals_ioloop_detach(void)
+{
+ struct signal_handler *h;
+
+ for (int signo = 0; signo < MAX_SIGNAL_VALUE; signo++) {
+ for (h = signal_handlers[signo]; h != NULL; h = h->next) {
+ if (h->sig_ioloop != NULL) {
+ lib_signals_ioloop_unref(&h->sig_ioloop);
+ have_missing_ioloops = TRUE;
+ }
+ }
+ }
+}
+
+void lib_signals_ioloop_attach(void)
+{
+ if (have_missing_ioloops)
+ lib_signals_ioloop_switch();
+}
+
+static void lib_signals_set(int signo, enum libsig_flags flags)
+{
+ struct sigaction act;
+
+ if (sigemptyset(&act.sa_mask) < 0)
+ i_fatal("sigemptyset(): %m");
+#ifdef SA_SIGINFO
+ act.sa_flags = SA_SIGINFO;
+ act.sa_sigaction = sig_handler;
+#else
+ act.sa_flags = 0;
+ act.sa_handler = sig_handler;
+#endif
+ if ((flags & LIBSIG_FLAG_RESTART) != 0)
+ act.sa_flags |= SA_RESTART;
+ if (sigaction(signo, &act, NULL) < 0)
+ i_fatal("sigaction(%d): %m", signo);
+}
+
+void lib_signals_set_handler(int signo, enum libsig_flags flags,
+ signal_handler_t *handler, void *context)
+{
+ struct signal_handler *h;
+
+ i_assert(handler != NULL);
+
+ if (signo < 0 || signo > MAX_SIGNAL_VALUE) {
+ i_panic("Trying to set signal %d handler, but max is %d",
+ signo, MAX_SIGNAL_VALUE);
+ }
+
+ if (signal_handlers[signo] == NULL && signals_initialized)
+ lib_signals_set(signo, flags);
+
+ h = i_new(struct signal_handler, 1);
+ h->handler = handler;
+ h->context = context;
+ h->flags = flags;
+
+ /* atomically set to signal_handlers[] list */
+ h->next = signal_handlers[signo];
+ signal_handlers[signo] = h;
+
+ if ((flags & LIBSIG_FLAG_DELAYED) != 0 && sig_pipe_fd[0] == -1) {
+ /* first delayed handler */
+ if (pipe(sig_pipe_fd) < 0)
+ i_fatal("pipe() failed: %m");
+ fd_set_nonblock(sig_pipe_fd[0], TRUE);
+ fd_set_nonblock(sig_pipe_fd[1], TRUE);
+ fd_close_on_exec(sig_pipe_fd[0], TRUE);
+ fd_close_on_exec(sig_pipe_fd[1], TRUE);
+ }
+ signal_handler_switch_ioloop(h);
+}
+
+static void lib_signals_ignore_forced(int signo, bool restart_syscalls)
+{
+ struct sigaction act;
+
+ if (sigemptyset(&act.sa_mask) < 0)
+ i_fatal("sigemptyset(): %m");
+ if (restart_syscalls) {
+ act.sa_flags = SA_RESTART;
+ act.sa_handler = SIG_IGN;
+ } else {
+#ifdef SA_SIGINFO
+ act.sa_flags = SA_SIGINFO;
+ act.sa_sigaction = sig_ignore;
+#else
+ act.sa_flags = 0;
+ act.sa_handler = sig_ignore;
+#endif
+ }
+
+ if (sigaction(signo, &act, NULL) < 0)
+ i_fatal("sigaction(%d): %m", signo);
+}
+
+void lib_signals_ignore(int signo, bool restart_syscalls)
+{
+ if (signo < 0 || signo > MAX_SIGNAL_VALUE) {
+ i_panic("Trying to ignore signal %d, but max is %d",
+ signo, MAX_SIGNAL_VALUE);
+ }
+
+ i_assert(signal_handlers[signo] == NULL);
+
+ lib_signals_ignore_forced(signo, restart_syscalls);
+}
+
+void lib_signals_clear_handlers_and_ignore(int signo)
+{
+ struct signal_handler *h;
+
+ if (signal_handlers[signo] == NULL)
+ return;
+
+ lib_signals_ignore_forced(signo, TRUE);
+
+ h = signal_handlers[signo];
+ signal_handlers[signo] = NULL;
+
+ while (h != NULL) {
+ struct signal_handler *h_next = h->next;
+
+ if (h->expected)
+ signals_expected--;
+ signal_handler_free(h);
+ h = h_next;
+ }
+}
+
+void lib_signals_unset_handler(int signo, signal_handler_t *handler,
+ void *context)
+{
+ struct signal_handler *h, **p;
+
+ for (p = &signal_handlers[signo]; *p != NULL; p = &(*p)->next) {
+ if ((*p)->handler == handler && (*p)->context == context) {
+ if (p == &signal_handlers[signo] &&
+ (*p)->next == NULL) {
+ /* last handler is to be removed */
+ lib_signals_ignore_forced(signo, TRUE);
+ }
+ h = *p;
+ *p = h->next;
+ if (h->expected)
+ lib_signals_update_expected_signals(FALSE);
+ signal_handler_free(h);
+ return;
+ }
+ }
+
+ i_panic("lib_signals_unset_handler(%d, %p, %p): handler not found",
+ signo, (void *)handler, context);
+}
+
+void lib_signals_set_expected(int signo, bool expected,
+ signal_handler_t *handler, void *context)
+{
+ struct signal_handler *h;
+
+ for (h = signal_handlers[signo]; h != NULL; h = h->next) {
+ if (h->handler == handler && h->context == context) {
+ if (h->expected == expected)
+ return;
+ h->expected = expected;
+ lib_signals_update_expected_signals(expected);
+ return;
+ }
+ }
+
+ i_panic("lib_signals_set_expected(%d, %p, %p): handler not found",
+ signo, (void *)handler, context);
+}
+
+void lib_signals_switch_ioloop(int signo,
+ signal_handler_t *handler, void *context)
+{
+ struct signal_handler *h;
+
+ for (h = signal_handlers[signo]; h != NULL; h = h->next) {
+ if (h->handler == handler && h->context == context) {
+ i_assert((h->flags & LIBSIG_FLAG_DELAYED) != 0);
+ i_assert((h->flags & LIBSIG_FLAG_IOLOOP_AUTOMOVE) == 0);
+ signal_handler_switch_ioloop(h);
+ /* check whether we can now handle any shadowed delayed
+ signals */
+ signal_check_shadowed();
+ return;
+ }
+ }
+
+ i_panic("lib_signals_switch_ioloop(%d, %p, %p): handler not found",
+ signo, (void *)handler, context);
+}
+
+void lib_signals_syscall_error(const char *prefix)
+{
+ /* @UNSAFE: We're in a signal handler. It's very limited what is
+ allowed in here. Especially strerror() isn't at least officially
+ allowed. */
+ char errno_buf[MAX_INT_STRLEN], *errno_str;
+ errno_str = dec2str_buf(errno_buf, errno);
+
+ size_t prefix_len = strlen(prefix);
+ size_t errno_str_len = strlen(errno_str);
+ char buf[prefix_len + errno_str_len + 1];
+
+ memcpy(buf, prefix, prefix_len);
+ memcpy(buf + prefix_len, errno_str, errno_str_len);
+ buf[prefix_len + errno_str_len] = '\n';
+ if (write_full(STDERR_FILENO, buf,
+ prefix_len + errno_str_len + 1) < 0) {
+ /* can't really do anything */
+ }
+}
+
+void lib_signals_init(void)
+{
+ int i;
+
+ signals_initialized = TRUE;
+ io_loop_add_switch_callback(lib_signals_ioloop_switched);
+ io_loop_add_destroy_callback(lib_signals_ioloop_destroyed);
+
+ /* add signals that were already registered */
+ for (i = 0; i < MAX_SIGNAL_VALUE; i++) {
+ if (signal_handlers[i] != NULL)
+ lib_signals_set(i, signal_handlers[i]->flags);
+ }
+}
+
+void lib_signals_deinit(void)
+{
+ int i;
+
+ for (i = 0; i < MAX_SIGNAL_VALUE; i++) {
+ if (signal_handlers[i] != NULL)
+ lib_signals_clear_handlers_and_ignore(i);
+ }
+ i_assert(signals_expected == 0);
+
+ if (sig_pipe_fd[0] != -1) {
+ if (close(sig_pipe_fd[0]) < 0)
+ i_error("close(sigpipe) failed: %m");
+ if (close(sig_pipe_fd[1]) < 0)
+ i_error("close(sigpipe) failed: %m");
+ sig_pipe_fd[0] = sig_pipe_fd[1] = -1;
+ }
+
+ if (array_is_created(&pending_shadowed_signals))
+ array_free(&pending_shadowed_signals);
+ i_assert(signal_ioloops == NULL);
+}
diff --git a/src/lib/lib-signals.h b/src/lib/lib-signals.h
new file mode 100644
index 0000000..d491de7
--- /dev/null
+++ b/src/lib/lib-signals.h
@@ -0,0 +1,72 @@
+#ifndef LIB_SIGNALS_H
+#define LIB_SIGNALS_H
+
+#include <signal.h>
+
+enum libsig_flags {
+ /* Signal handler will be called later from IO loop when it's safe to
+ do any kind of work */
+ LIBSIG_FLAG_DELAYED = 0x01,
+ /* Restart syscalls instead of having them fail with EINTR */
+ LIBSIG_FLAG_RESTART = 0x02,
+ /* Automatically shift delayed signal handling for this signal
+ to a newly started ioloop. */
+ LIBSIG_FLAG_IOLOOP_AUTOMOVE = 0x04,
+};
+#define LIBSIG_FLAGS_SAFE (LIBSIG_FLAG_DELAYED | LIBSIG_FLAG_RESTART)
+
+typedef void signal_handler_t(const siginfo_t *si, void *context);
+
+/* Number of times a "termination signal" has been received.
+ These signals are SIGINT, SIGQUIT and SIGTERM. Callers can compare this to
+ their saved previous value to see if a syscall returning EINTR should be
+ treated as someone wanting to end the process or just some internal signal
+ that should be ignored, such as SIGCHLD.
+
+ This is marked as volatile so that compiler won't optimize away its
+ comparisons. It may not work perfectly everywhere, such as when accessing it
+ isn't atomic, so you shouldn't heavily rely on its actual value. */
+extern volatile unsigned int signal_term_counter;
+
+/* Convert si_code to string */
+const char *lib_signal_code_to_str(int signo, int sicode);
+
+/* Detach IOs from all ioloops. This isn't normally necessary, except when
+ forking a process. */
+void lib_signals_ioloop_detach(void);
+void lib_signals_ioloop_attach(void);
+
+/* Set signal handler for specific signal. */
+void lib_signals_set_handler(int signo, enum libsig_flags flags,
+ signal_handler_t *handler, void *context)
+ ATTR_NULL(4);
+/* Ignore given signal. */
+void lib_signals_ignore(int signo, bool restart_syscalls);
+/* Clear all signal handlers for a specific signal and set the signal to be
+ ignored. */
+void lib_signals_clear_handlers_and_ignore(int signo);
+/* Unset specific signal handler for specific signal. */
+void lib_signals_unset_handler(int signo,
+ signal_handler_t *handler, void *context)
+ ATTR_NULL(3);
+
+/* Indicate whether signals are expected for the indicated delayed handler. When
+ signals are expected, the io for delayed handlers will be allowed to wait
+ alone on the ioloop. */
+void lib_signals_set_expected(int signo, bool expected,
+ signal_handler_t *handler, void *context);
+ ATTR_NULL(4);
+
+/* Switch ioloop for a specific signal handler created with
+ LIBSIG_FLAG_NO_IOLOOP_AUTOMOVE. */
+void lib_signals_switch_ioloop(int signo,
+ signal_handler_t *handler, void *context);
+
+/* Log a syscall error inside a (non-delayed) signal handler where i_error() is
+ unsafe. errno number will be appended to the prefix. */
+void lib_signals_syscall_error(const char *prefix);
+
+void lib_signals_init(void);
+void lib_signals_deinit(void);
+
+#endif
diff --git a/src/lib/lib.c b/src/lib/lib.c
new file mode 100644
index 0000000..bd2da91
--- /dev/null
+++ b/src/lib/lib.c
@@ -0,0 +1,206 @@
+/* Copyright (c) 2001-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "dovecot-version.h"
+#include "array.h"
+#include "event-filter.h"
+#include "env-util.h"
+#include "hostpid.h"
+#include "ipwd.h"
+#include "process-title.h"
+#include "restrict-access.h"
+#include "var-expand-private.h"
+#include "randgen.h"
+
+#include <fcntl.h>
+#include <unistd.h>
+#include <sys/time.h>
+
+/* Mainly for including the full version information in core dumps.
+ NOTE: Don't set this const - otherwise it won't end up in core dumps. */
+char dovecot_build_info[] = DOVECOT_BUILD_INFO;
+
+static bool lib_initialized = FALSE;
+int dev_null_fd = -1;
+
+struct atexit_callback {
+ int priority;
+ lib_atexit_callback_t *callback;
+};
+
+static ARRAY(struct atexit_callback) atexit_callbacks = ARRAY_INIT;
+static bool lib_clean_exit;
+
+#undef i_unlink
+int i_unlink(const char *path, const char *source_fname,
+ unsigned int source_linenum)
+{
+ if (unlink(path) < 0) {
+ i_error("unlink(%s) failed: %m (in %s:%u)",
+ path, source_fname, source_linenum);
+ return -1;
+ }
+ return 0;
+}
+
+#undef i_unlink_if_exists
+int i_unlink_if_exists(const char *path, const char *source_fname,
+ unsigned int source_linenum)
+{
+ if (unlink(path) == 0)
+ return 1;
+ else if (errno == ENOENT)
+ return 0;
+ else {
+ i_error("unlink(%s) failed: %m (in %s:%u)",
+ path, source_fname, source_linenum);
+ return -1;
+ }
+}
+
+void i_getopt_reset(void)
+{
+#ifdef __GLIBC__
+ /* a) for subcommands allow -options anywhere in command line
+ b) this is actually required for the reset to work (glibc bug?) */
+ optind = 0;
+#else
+ optind = 1;
+#endif
+}
+
+void lib_atexit(lib_atexit_callback_t *callback)
+{
+ lib_atexit_priority(callback, 0);
+}
+
+void lib_atexit_priority(lib_atexit_callback_t *callback, int priority)
+{
+ struct atexit_callback *cb;
+ const struct atexit_callback *callbacks;
+ unsigned int i, count;
+
+ if (!array_is_created(&atexit_callbacks))
+ i_array_init(&atexit_callbacks, 8);
+ else {
+ /* skip if it's already added */
+ callbacks = array_get(&atexit_callbacks, &count);
+ for (i = count; i > 0; i--) {
+ if (callbacks[i-1].callback == callback) {
+ i_assert(callbacks[i-1].priority == priority);
+ return;
+ }
+ }
+ }
+ cb = array_append_space(&atexit_callbacks);
+ cb->priority = priority;
+ cb->callback = callback;
+}
+
+static int atexit_callback_priority_cmp(const struct atexit_callback *cb1,
+ const struct atexit_callback *cb2)
+{
+ return cb1->priority - cb2->priority;
+}
+
+void lib_atexit_run(void)
+{
+ const struct atexit_callback *cb;
+
+ if (array_is_created(&atexit_callbacks)) {
+ array_sort(&atexit_callbacks, atexit_callback_priority_cmp);
+ array_foreach(&atexit_callbacks, cb)
+ (*cb->callback)();
+ array_free(&atexit_callbacks);
+ }
+}
+
+static void lib_open_non_stdio_dev_null(void)
+{
+ dev_null_fd = open("/dev/null", O_WRONLY);
+ if (dev_null_fd == -1)
+ i_fatal("open(/dev/null) failed: %m");
+ /* Make sure stdin, stdout and stderr fds exist. We especially rely on
+ stderr being available and a lot of code doesn't like fd being 0.
+ We'll open /dev/null as write-only also for stdin, since if any
+ reads are attempted from it we'll want them to fail. */
+ while (dev_null_fd < STDERR_FILENO) {
+ dev_null_fd = dup(dev_null_fd);
+ if (dev_null_fd == -1)
+ i_fatal("dup(/dev/null) failed: %m");
+ }
+ /* close the actual /dev/null fd on exec*(), but keep it in stdio fds */
+ fd_close_on_exec(dev_null_fd, TRUE);
+}
+
+void lib_set_clean_exit(bool set)
+{
+ lib_clean_exit = set;
+}
+
+void lib_exit(int status)
+{
+ lib_set_clean_exit(TRUE);
+ exit(status);
+}
+
+static void lib_atexit_handler(void)
+{
+ /* We're already in exit code path. Avoid using any functions that
+ might cause strange breakage. Especially anything that could call
+ exit() again could cause infinite looping in some OSes. */
+ if (!lib_clean_exit) {
+ const char *error = "Unexpected exit - converting to abort\n";
+ if (write(STDERR_FILENO, error, strlen(error)) < 0) {
+ /* ignore */
+ }
+ abort();
+ }
+}
+
+void lib_init(void)
+{
+ i_assert(!lib_initialized);
+ random_init();
+ data_stack_init();
+ hostpid_init();
+ lib_open_non_stdio_dev_null();
+ lib_event_init();
+ event_filter_init();
+ var_expand_extensions_init();
+
+ /* Default to clean exit. Otherwise there would be too many accidents
+ with e.g. command line parsing errors that try to return instead
+ of using lib_exit(). master_service_init_finish() will change this
+ again to be FALSE. */
+ lib_set_clean_exit(TRUE);
+ atexit(lib_atexit_handler);
+
+ lib_initialized = TRUE;
+}
+
+bool lib_is_initialized(void)
+{
+ return lib_initialized;
+}
+
+void lib_deinit(void)
+{
+ i_assert(lib_initialized);
+ lib_initialized = FALSE;
+ lib_atexit_run();
+ ipwd_deinit();
+ hostpid_deinit();
+ var_expand_extensions_deinit();
+ event_filter_deinit();
+ data_stack_deinit_event();
+ lib_event_deinit();
+ restrict_access_deinit();
+ i_close_fd(&dev_null_fd);
+ data_stack_deinit();
+ failures_deinit();
+ process_title_deinit();
+ random_deinit();
+
+ lib_clean_exit = TRUE;
+}
diff --git a/src/lib/lib.h b/src/lib/lib.h
new file mode 100644
index 0000000..9c3ca34
--- /dev/null
+++ b/src/lib/lib.h
@@ -0,0 +1,115 @@
+#ifndef LIB_H
+#define LIB_H
+
+/* default lib includes */
+#ifdef HAVE_CONFIG_H
+# include "config.h"
+#endif
+
+/* default system includes - keep these at minimum.. */
+#include <stddef.h> /* Solaris defines NULL wrong unless this is used */
+#include <stdlib.h>
+#include <string.h> /* strcmp() etc. */
+#ifdef HAVE_STRINGS_H
+# include <strings.h> /* strcasecmp() etc. */
+#endif
+#include <stdarg.h> /* va_list is used everywhere */
+#include <limits.h> /* INT_MAX, etc. */
+#include <errno.h> /* error checking is good */
+#include <sys/types.h> /* many other includes want this */
+#include <inttypes.h> /* PRI* macros */
+
+#ifdef HAVE_STDINT_H
+# include <stdint.h> /* C99 int types, we mostly need uintmax_t */
+#endif
+
+#include "compat.h"
+#include "macros.h"
+#include "failures.h"
+
+#include "malloc-overflow.h"
+#include "data-stack.h"
+#include "mempool.h"
+#include "imem.h"
+#include "byteorder.h"
+#include "fd-util.h"
+
+typedef struct buffer buffer_t;
+typedef struct buffer string_t;
+
+struct istream;
+struct ostream;
+
+typedef void lib_atexit_callback_t(void);
+
+#include "array-decl.h" /* ARRAY*()s may exist in any header */
+#include "bits.h"
+#include "hash-decl.h" /* HASH_TABLE*()s may exist in any header */
+#include "strfuncs.h"
+#include "strnum.h"
+#include "event-log.h"
+
+#define LIB_ATEXIT_PRIORITY_HIGH -10
+#define LIB_ATEXIT_PRIORITY_DEFAULT 0
+#define LIB_ATEXIT_PRIORITY_LOW 10
+
+/* /dev/null opened as O_WRONLY. Opened at lib_init(), so it can be accessed
+ also inside chroots. */
+extern int dev_null_fd;
+
+/* Call unlink(). If it fails, log an error including the source filename
+ and line number. */
+int i_unlink(const char *path, const char *source_fname,
+ unsigned int source_linenum);
+#define i_unlink(path) i_unlink(path, __FILE__, __LINE__)
+/* Same as i_unlink(), but don't log an error if errno=ENOENT. Returns 1 on
+ unlink() success, 0 if errno=ENOENT, -1 on other errors. */
+int i_unlink_if_exists(const char *path, const char *source_fname,
+ unsigned int source_linenum);
+#define i_unlink_if_exists(path) i_unlink_if_exists(path, __FILE__, __LINE__)
+/* Reset getopt() so it can be used for the next args. */
+void i_getopt_reset(void);
+
+/* Call the given callback at the beginning of lib_deinit(). The main
+ difference to atexit() is that liblib's memory allocation and logging
+ functions are still available. Also if lib_atexit() is called multiple times
+ to the same callback, it's added only once. */
+void lib_atexit(lib_atexit_callback_t *callback);
+/* Specify the order in which the callback is called. Lowest numbered
+ priorities are called first. lib_atexit() is called with priority=0. */
+void lib_atexit_priority(lib_atexit_callback_t *callback, int priority);
+/* Manually run the atexit callbacks. lib_deinit() also does this if not
+ explicitly called. */
+void lib_atexit_run(void);
+/* Unless this or lib_deinit() is called, any unexpected exit() will result
+ in abort(). This can be helpful in catching unexpected exits. */
+void lib_set_clean_exit(bool set);
+/* Same as lib_set_clean_exit(TRUE) followed by exit(status). */
+void lib_exit(int status) ATTR_NORETURN;
+
+void lib_init(void);
+bool lib_is_initialized(void);
+void lib_deinit(void);
+
+uint32_t i_rand(void);
+/* Returns a random integer < upper_bound. */
+uint32_t i_rand_limit(uint32_t upper_bound);
+
+static inline unsigned short i_rand_ushort(void)
+{
+ return i_rand_limit(USHRT_MAX + 1);
+}
+
+static inline unsigned char i_rand_uchar(void)
+{
+ return i_rand_limit(UCHAR_MAX + 1);
+}
+
+/* Returns a random integer >= min_val, and <= max_val. */
+static inline uint32_t i_rand_minmax(uint32_t min_val, uint32_t max_val)
+{
+ i_assert(min_val <= max_val);
+ return min_val + i_rand_limit(max_val - min_val + 1);
+}
+
+#endif
diff --git a/src/lib/llist.h b/src/lib/llist.h
new file mode 100644
index 0000000..8a52e87
--- /dev/null
+++ b/src/lib/llist.h
@@ -0,0 +1,81 @@
+#ifndef LLIST_H
+#define LLIST_H
+
+/* Doubly linked list */
+#define DLLIST_PREPEND_FULL(list, item, prev, next) STMT_START { \
+ (item)->prev = NULL; \
+ (item)->next = *(list); \
+ if (*(list) != NULL) (*(list))->prev = (item); \
+ *(list) = (item); \
+ } STMT_END
+
+#define DLLIST_PREPEND(list, item) \
+ DLLIST_PREPEND_FULL(list, item, prev, next)
+
+#define DLLIST_REMOVE_FULL(list, item, prev, next) STMT_START { \
+ if ((item)->prev != NULL) \
+ (item)->prev->next = (item)->next; \
+ else if ((*list) == item) \
+ *(list) = (item)->next; \
+ if ((item)->next != NULL) { \
+ (item)->next->prev = (item)->prev; \
+ (item)->next = NULL; \
+ } \
+ (item)->prev = NULL; \
+ } STMT_END
+
+#define DLLIST_REMOVE(list, item) \
+ DLLIST_REMOVE_FULL(list, item, prev, next)
+
+/* Doubly linked list with head and tail */
+#define DLLIST2_PREPEND_FULL(head, tail, item, prev, next) STMT_START { \
+ (item)->prev = NULL; \
+ (item)->next = *(head); \
+ if (*(head) != NULL) (*(head))->prev = (item); else (*tail) = (item); \
+ *(head) = (item); \
+ } STMT_END
+
+#define DLLIST2_PREPEND(head, tail, item) \
+ DLLIST2_PREPEND_FULL(head, tail, item, prev, next)
+
+#define DLLIST2_APPEND_FULL(head, tail, item, prev, next) STMT_START { \
+ (item)->prev = *(tail); \
+ (item)->next = NULL; \
+ if (*(tail) != NULL) (*(tail))->next = (item); else (*head) = (item); \
+ *(tail) = (item); \
+ } STMT_END
+
+#define DLLIST2_APPEND(head, tail, item) \
+ DLLIST2_APPEND_FULL(head, tail, item, prev, next)
+
+#define DLLIST2_INSERT_AFTER_FULL(head, tail, after, item, prev, next) \
+ STMT_START { \
+ (item)->prev = (after); \
+ (item)->next = (after)->next; \
+ if ((after)->next != NULL) \
+ (after)->next->prev = (item); \
+ (after)->next = (item); \
+ if (*(tail) == (after)) \
+ *(tail) = (item); \
+ } STMT_END
+
+#define DLLIST2_INSERT_AFTER(head, tail, after, item) \
+ DLLIST2_INSERT_AFTER_FULL(head, tail, after, item, prev, next)
+
+#define DLLIST2_REMOVE_FULL(head, tail, item, prev, next) STMT_START { \
+ if ((item)->prev != NULL) \
+ (item)->prev->next = (item)->next; \
+ else if (*(head) == item) \
+ *(head) = (item)->next; \
+ if ((item)->next != NULL) { \
+ (item)->next->prev = (item)->prev; \
+ (item)->next = NULL; \
+ } else if ((*tail) == item) \
+ *(tail) = (item)->prev; \
+ (item)->prev = NULL; \
+ } STMT_END
+
+#define DLLIST2_REMOVE(head, tail, item) \
+ DLLIST2_REMOVE_FULL(head, tail, item, prev, next)
+
+#endif
diff --git a/src/lib/log-throttle.c b/src/lib/log-throttle.c
new file mode 100644
index 0000000..d4b13c4
--- /dev/null
+++ b/src/lib/log-throttle.c
@@ -0,0 +1,78 @@
+/* Copyright (c) 2016-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "ioloop.h"
+#include "time-util.h"
+#include "log-throttle.h"
+
+struct log_throttle {
+ struct log_throttle_settings set;
+ log_throttle_callback_t *callback;
+ void *context;
+
+ struct timeval last_time;
+ unsigned int last_count;
+
+ struct timeout *to_throttled;
+};
+
+#undef log_throttle_init
+struct log_throttle *
+log_throttle_init(const struct log_throttle_settings *set,
+ log_throttle_callback_t *callback, void *context)
+{
+ struct log_throttle *throttle;
+
+ i_assert(set->throttle_at_max_per_interval > 0);
+ i_assert(set->unthrottle_at_max_per_interval > 0);
+
+ throttle = i_new(struct log_throttle, 1);
+ throttle->set = *set;
+ if (throttle->set.interval_msecs == 0)
+ throttle->set.interval_msecs = 1000;
+ throttle->callback = callback;
+ throttle->context = context;
+ throttle->last_time = ioloop_timeval;
+ return throttle;
+}
+
+void log_throttle_deinit(struct log_throttle **_throttle)
+{
+ struct log_throttle *throttle = *_throttle;
+
+ *_throttle = NULL;
+ timeout_remove(&throttle->to_throttled);
+ i_free(throttle);
+}
+
+static void log_throttle_callback(struct log_throttle *throttle)
+{
+ if (throttle->last_count > 0)
+ throttle->callback(throttle->last_count, throttle->context);
+ if (throttle->last_count < throttle->set.unthrottle_at_max_per_interval)
+ timeout_remove(&throttle->to_throttled);
+ throttle->last_count = 0;
+}
+
+bool log_throttle_accept(struct log_throttle *throttle)
+{
+ if (throttle->to_throttled != NULL) {
+ /* unthrottling and last_count resets are done only by
+ the callback */
+ throttle->last_count++;
+ return FALSE;
+ } else if (timeval_diff_msecs(&ioloop_timeval, &throttle->last_time) >=
+ (int)throttle->set.interval_msecs) {
+ throttle->last_time = ioloop_timeval;
+ throttle->last_count = 1;
+ return TRUE;
+ } else if (++throttle->last_count <= throttle->set.throttle_at_max_per_interval) {
+ return TRUE;
+ } else {
+ throttle->last_count = 1;
+ throttle->to_throttled =
+ timeout_add(throttle->set.interval_msecs,
+ log_throttle_callback, throttle);
+ return FALSE;
+ }
+}
diff --git a/src/lib/log-throttle.h b/src/lib/log-throttle.h
new file mode 100644
index 0000000..be98239
--- /dev/null
+++ b/src/lib/log-throttle.h
@@ -0,0 +1,32 @@
+#ifndef LOG_THROTTLE_H
+#define LOG_THROTTLE_H
+
+struct log_throttle_settings {
+ /* Start throttling after we reach this many log events/interval. */
+ unsigned int throttle_at_max_per_interval;
+ /* Throttling continues until there's only this many or below
+ log events/interval. */
+ unsigned int unthrottle_at_max_per_interval;
+ /* Interval unit in milliseconds. The throttled-callback is also called
+ at this interval. Default (0) is 1000 milliseconds. */
+ unsigned int interval_msecs;
+};
+
+typedef void
+log_throttle_callback_t(unsigned int new_events_count, void *context);
+
+struct log_throttle *
+log_throttle_init(const struct log_throttle_settings *set,
+ log_throttle_callback_t *callback, void *context);
+#define log_throttle_init(set, callback, context) \
+ log_throttle_init(set - \
+ CALLBACK_TYPECHECK(callback, void (*)(unsigned int, typeof(context))), \
+ (log_throttle_callback_t *)callback, context)
+void log_throttle_deinit(struct log_throttle **throttle);
+
+/* Increase event count. Returns TRUE if the event should be logged,
+ FALSE if it's throttled. ioloop_timeval is used to determine the current
+ time. */
+bool log_throttle_accept(struct log_throttle *throttle);
+
+#endif
diff --git a/src/lib/macros.h b/src/lib/macros.h
new file mode 100644
index 0000000..8cd159f
--- /dev/null
+++ b/src/lib/macros.h
@@ -0,0 +1,302 @@
+#ifndef MACROS_H
+#define MACROS_H
+
+/* several useful macros, mostly from glib.h */
+
+#ifndef NULL
+# define NULL ((void *)0)
+#endif
+
+#ifndef FALSE
+# define FALSE (!1)
+#endif
+
+#ifndef TRUE
+# define TRUE (!FALSE)
+#endif
+
+#define N_ELEMENTS(arr) \
+ (sizeof(arr) / sizeof((arr)[0]))
+
+#define MEM_ALIGN(size) \
+ (((size) + MEM_ALIGN_SIZE-1) & ~((size_t) MEM_ALIGN_SIZE-1))
+
+#define PTR_OFFSET(ptr, offset) \
+ ((void *) (((uintptr_t) (ptr)) + ((size_t) (offset))))
+#define CONST_PTR_OFFSET(ptr, offset) \
+ ((const void *) (((uintptr_t) (ptr)) + ((size_t) (offset))))
+
+#define container_of(ptr, type, name) \
+ (type *)((char *)(ptr) - offsetof(type, name) + \
+ COMPILE_ERROR_IF_TYPES_NOT_COMPATIBLE(ptr, &((type *) 0)->name))
+
+/* Don't use simply MIN/MAX, as they're often defined elsewhere in include
+ files that are included after this file generating tons of warnings. */
+#define I_MIN(a, b) (((a) < (b)) ? (a) : (b))
+#define I_MAX(a, b) (((a) > (b)) ? (a) : (b))
+
+/* make it easier to cast from/to pointers. assumes that
+ sizeof(uintptr_t) == sizeof(void *) and they're both the largest datatypes
+ that are allowed to be used. so, long long isn't safe with these. */
+#define POINTER_CAST(i) \
+ ((void *) (((uintptr_t)NULL) + (i)))
+#define POINTER_CAST_TO(p, type) \
+ ((type)(uintptr_t)(p))
+
+/* Define VA_COPY() to do the right thing for copying va_list variables.
+ config.h may have already defined VA_COPY as va_copy or __va_copy. */
+#ifndef VA_COPY
+# if defined (__GNUC__) && defined (__PPC__) && \
+ (defined (_CALL_SYSV) || defined (_WIN32))
+# define VA_COPY(ap1, ap2) (*(ap1) = *(ap2))
+# elif defined (VA_COPY_AS_ARRAY)
+# define VA_COPY(ap1, ap2) memmove ((ap1), (ap2), sizeof (va_list))
+# else /* va_list is a pointer */
+# define VA_COPY(ap1, ap2) ((ap1) = (ap2))
+# endif /* va_list is a pointer */
+#endif
+
+/* Provide convenience macros for handling structure
+ * fields through their offsets.
+ */
+#define STRUCT_MEMBER_P(struct_p, struct_offset) \
+ ((void *) ((char *) (struct_p) + (long) (struct_offset)))
+#define CONST_STRUCT_MEMBER_P(struct_p, struct_offset) \
+ ((const void *) ((const char *) (struct_p) + (long) (struct_offset)))
+
+/* Provide simple macro statement wrappers:
+ STMT_START { statements; } STMT_END;
+ can be used as a single statement, as in
+ if (x) STMT_START { ... } STMT_END; else ... */
+#if !(defined (STMT_START) && defined (STMT_END))
+# define STMT_START do
+# define STMT_END while (0)
+#endif
+
+/* Provide macros to feature the GCC function attribute. */
+#if __GNUC__ > 2 || (__GNUC__ == 2 && __GNUC_MINOR__ > 4)
+# define ATTRS_DEFINED
+# define ATTR_FORMAT(format_idx, arg_idx) \
+ __attribute__((format (printf, format_idx, arg_idx)))
+# define ATTR_FORMAT_ARG(arg_idx) \
+ __attribute__((format_arg (arg_idx)))
+# define ATTR_SCANF(format_idx, arg_idx) \
+ __attribute__((format (scanf, format_idx, arg_idx)))
+# define ATTR_STRFTIME(format_idx) \
+ __attribute__((format (strftime, format_idx, 0)))
+# define ATTR_UNUSED __attribute__((unused))
+# define ATTR_NORETURN __attribute__((noreturn))
+# define ATTR_CONST __attribute__((const))
+# define ATTR_PURE __attribute__((pure))
+#else
+# define ATTR_FORMAT(format_idx, arg_idx)
+# define ATTR_FORMAT_ARG(arg_idx)
+# define ATTR_SCANF(format_idx, arg_idx)
+# define ATTR_STRFTIME(format_idx)
+# define ATTR_UNUSED
+# define ATTR_NORETURN
+# define ATTR_CONST
+# define ATTR_PURE
+#endif
+#ifdef HAVE_ATTR_NULL
+# define ATTR_NULL(...) __attribute__((null(__VA_ARGS__)))
+#else
+# define ATTR_NULL(...)
+#endif
+#ifdef HAVE_ATTR_NOWARN_UNUSED_RESULT
+# define ATTR_NOWARN_UNUSED_RESULT __attribute__((nowarn_unused_result))
+#else
+# define ATTR_NOWARN_UNUSED_RESULT
+#endif
+#if __GNUC__ > 2
+# define ATTR_MALLOC __attribute__((malloc))
+#else
+# define ATTR_MALLOC
+#endif
+#if __GNUC__ > 3
+/* GCC 4.0 and later */
+# define ATTR_WARN_UNUSED_RESULT __attribute__((warn_unused_result))
+# define ATTR_SENTINEL __attribute__((sentinel))
+#else
+# define ATTR_WARN_UNUSED_RESULT
+# define ATTR_SENTINEL
+#endif
+#if __GNUC__ > 4 || (__GNUC__ == 4 && __GNUC_MINOR__ >= 3)
+/* GCC 4.3 and later */
+# define ATTR_HOT __attribute__((hot))
+# define ATTR_COLD __attribute__((cold))
+#else
+# define ATTR_HOT
+# define ATTR_COLD
+#endif
+#if __GNUC__ > 4 || (__GNUC__ == 4 && __GNUC_MINOR__ >= 9)
+/* GCC 4.9 and later */
+# define ATTR_RETURNS_NONNULL __attribute__((returns_nonnull))
+#else
+# define ATTR_RETURNS_NONNULL
+#endif
+#ifdef HAVE_ATTR_DEPRECATED
+# define ATTR_DEPRECATED(str) __attribute__((deprecated(str)))
+#else
+# define ATTR_DEPRECATED(str)
+#endif
+
+/* Macros to provide type safety for callback functions' context parameters.
+ This is used like:
+
+ // safe-api.h file:
+ typedef void safe_callback_t(struct foo *foo);
+
+ void safe_run(safe_callback_t *callback, void *context);
+ #define safe_run((safe_callback_t *)callback, \
+ TRUE ? context : CALLBACK_TYPECHECK(callback, void (*)(typeof(context))))
+
+ // safe-api.c file:
+ #undef safe_run
+ void safe_run(safe_callback_t *callback, void *context)
+ {
+ callback(context);
+ }
+
+ // in caller code:
+ static void callback(struct foo *foo);
+ struct foo *foo = ...;
+ safe_run(callback, foo);
+
+ The first step is to create the callback function in a normal way. Type
+ safety is added to it by creating a macro that overrides the function and
+ checks the callback type safety using CALLBACK_TYPECHECK().
+
+ The CALLBACK_TYPECHECK() macro works by giving a compiling failure if the
+ provided callback function isn't compatible with the specified function
+ type parameter. The function type parameter must use typeof(context) in
+ place of the "void *context" parameter, but otherwise use exactly the same
+ function type as what the callback is. The macro then casts the given
+ callback function into the type with "void *context".
+*/
+#ifdef HAVE_TYPE_CHECKS
+# define CALLBACK_TYPECHECK(callback, type) \
+ (COMPILE_ERROR_IF_TRUE(!__builtin_types_compatible_p( \
+ typeof(&callback), type)) ? 1 : 0)
+#else
+# define CALLBACK_TYPECHECK(callback, type) 0
+#endif
+
+#if (__GNUC__ > 3 || (__GNUC__ == 3 && __GNUC_MINOR__ > 0)) && \
+ !defined(__cplusplus) && !defined(STATIC_CHECKER)
+# define COMPILE_ERROR_IF_TRUE(condition) \
+ (sizeof(char[1 - 2 * ((condition) ? 1 : 0)]) > 0 ? FALSE : FALSE)
+#else
+# define COMPILE_ERROR_IF_TRUE(condition) FALSE
+#endif
+
+#ifdef HAVE_TYPE_CHECKS
+# define COMPILE_ERROR_IF_TYPES_NOT_COMPATIBLE(_a, _b) \
+ COMPILE_ERROR_IF_TRUE( \
+ !__builtin_types_compatible_p(typeof(_a), typeof(_b)))
+#define COMPILE_ERROR_IF_TYPES2_NOT_COMPATIBLE(_a1, _a2, _b) \
+ COMPILE_ERROR_IF_TRUE( \
+ !__builtin_types_compatible_p(typeof(_a1), typeof(_b)) && \
+ !__builtin_types_compatible_p(typeof(_a2), typeof(_b)))
+# define TYPE_CHECKS(return_type, checks, func) \
+ (FALSE ? (return_type)(checks) : (func))
+#else
+# define COMPILE_ERROR_IF_TYPES_NOT_COMPATIBLE(_a, _b) 0
+# define COMPILE_ERROR_IF_TYPES2_NOT_COMPATIBLE(_a1, _a2, _b) 0
+# define TYPE_CHECKS(return_type, checks, func) (func)
+#endif
+
+#if __GNUC__ > 2
+# define unlikely(expr) (__builtin_expect((expr) ? 1 : 0, 0) != 0)
+# define likely(expr) (__builtin_expect((expr) ? 1 : 0, 1) != 0)
+#else
+# define unlikely(expr) expr
+# define likely(expr) expr
+#endif
+
+#if defined(__clang__) && ((__clang_major__ > 4) || (__clang_major__ == 3 && __clang_minor__ >= 9))
+# define ATTR_UNSIGNED_WRAPS __attribute__((no_sanitize("integer")))
+#else
+# define ATTR_UNSIGNED_WRAPS
+#endif
+
+/* Provide macros for error handling. */
+#ifdef DISABLE_ASSERTS
+# define i_assert(expr)
+#else
+# define i_assert(expr) STMT_START{ \
+ if (unlikely(!(expr))) \
+ i_panic("file %s: line %d (%s): assertion failed: (%s)", \
+ __FILE__, \
+ __LINE__, \
+ __func__, \
+ #expr); }STMT_END
+#endif
+
+/* Convenience macro to test the versions of dovecot. */
+#define DOVECOT_PREREQ(maj, min, micro) \
+ ((DOVECOT_VERSION_MAJOR << 24) + \
+ (DOVECOT_VERSION_MINOR << 16) + \
+ DOVECOT_VERSION_MICRO >= ((maj) << 24) + ((min) << 16) + (micro))
+
+#ifdef __cplusplus
+# undef STATIC_ARRAY
+# define STATIC_ARRAY
+#endif
+
+/* Convenience wrappers for initializing a struct with zeros, although it can
+ be used for replacing other memset()s also.
+
+ // NOTE: This is the correct way to zero the whole array
+ char arr[5]; i_zero(&arr);
+ // This will give compiler error (or zero only the first element):
+ char arr[5]; i_zero(arr);
+*/
+#define i_zero(p) \
+ memset(p, 0 + COMPILE_ERROR_IF_TRUE(sizeof(p) > sizeof(void *)), sizeof(*(p)))
+#define i_zero_safe(p) \
+ safe_memset(p, 0 + COMPILE_ERROR_IF_TRUE(sizeof(p) > sizeof(void *)), sizeof(*(p)))
+
+#define ST_CHANGED(st_a, st_b) \
+ ((st_a).st_mtime != (st_b).st_mtime || \
+ ST_MTIME_NSEC(st_a) != ST_MTIME_NSEC(st_b) || \
+ (st_a).st_size != (st_b).st_size || \
+ (st_a).st_ino != (st_b).st_ino)
+
+#ifdef HAVE_UNDEFINED_SANITIZER
+# define ATTR_NO_SANITIZE(x) __attribute__((no_sanitize((x))))
+#else
+# define ATTR_NO_SANITIZE(x)
+#endif
+
+/* gcc and clang do this differently, see
+ https://gcc.gnu.org/onlinedocs/gcc-10.2.0/gcc/Common-Function-Attributes.html */
+#ifdef HAVE_FSANITIZE_UNDEFINED
+# ifdef __clang__
+# define ATTR_NO_SANITIZE_UNDEFINED ATTR_NO_SANITIZE("undefined")
+# else
+# define ATTR_NO_SANITIZE_UNDEFINED __attribute__((no_sanitize_undefined))
+# endif
+#else
+# define ATTR_NO_SANITIZE_UNDEFINED
+#endif
+
+#ifdef HAVE_FSANITIZE_INTEGER
+# define ATTR_NO_SANITIZE_INTEGER ATTR_NO_SANITIZE("integer")
+# define ATTR_NO_SANITIZE_IMPLICIT_CONVERSION ATTR_NO_SANITIZE("implicit-conversion")
+#else
+# define ATTR_NO_SANITIZE_INTEGER
+# define ATTR_NO_SANITIZE_IMPLICIT_CONVERSION
+#endif
+
+/* negate enumeration flags in a way that avoids implicit conversion */
+#ifndef STATIC_CHECKER
+# define ENUM_NEGATE(x) \
+ ((unsigned int)(~(x)) + COMPILE_ERROR_IF_TRUE(sizeof((x)) > sizeof(int) || (x) < 0 || (x) > INT_MAX))
+#else
+/* clang scan-build keeps complaining about x > 2147483647 case, so disable the
+ sizeof check. */
+# define ENUM_NEGATE(x) ((unsigned int)(~(x)))
+#endif
+
+#endif
diff --git a/src/lib/malloc-overflow.h b/src/lib/malloc-overflow.h
new file mode 100644
index 0000000..108cc00
--- /dev/null
+++ b/src/lib/malloc-overflow.h
@@ -0,0 +1,54 @@
+#ifndef MALLOC_OVERFLOW_H
+#define MALLOC_OVERFLOW_H
+
+/* MALLOC_*() can be used to calculate memory allocation sizes. If there's an
+ overflow, it'll cleanly panic instead of causing a potential buffer
+ overflow.
+
+ Note that *_malloc(size+1) doesn't need to use MALLOC_ADD(size, 1). It wraps
+ to size==0 and the *_malloc() calls already panic if size==0. */
+static inline size_t
+malloc_multiply_check(size_t a, size_t b, size_t sizeof_a, size_t sizeof_b,
+ const char *fname, unsigned int linenum)
+{
+ /* the first sizeof-checks are intended to optimize away this entire
+ if-check for types that are small enough to never wrap size_t. */
+ if ((sizeof_a * 2 > sizeof(size_t) || sizeof_b * 2 > sizeof(size_t)) &&
+ b != 0 && (a > SIZE_MAX / b)) {
+ i_panic("file %s: line %d: memory allocation overflow: %zu * %zu",
+ fname, linenum, a, b);
+ }
+ return a * b;
+}
+#ifndef STATIC_CHECKER
+# define MALLOC_MULTIPLY(a, b) \
+ malloc_multiply_check(a, b, sizeof(a), sizeof(b), __FILE__, __LINE__)
+#else
+/* avoid warning every time about sizeof(b) when b contains any arithmetic */
+# define MALLOC_MULTIPLY(a, b) \
+ malloc_multiply_check(a, b, sizeof(a), sizeof(size_t), __FILE__, __LINE__)
+#endif
+
+static inline size_t
+malloc_add_check(size_t a, size_t b, size_t sizeof_a, size_t sizeof_b,
+ const char *fname, unsigned int linenum)
+{
+ /* the first sizeof-checks are intended to optimize away this entire
+ if-check for types that are small enough to never wrap size_t. */
+ if ((sizeof_a >= sizeof(size_t) || sizeof_b >= sizeof(size_t)) &&
+ SIZE_MAX - a < b) {
+ i_panic("file %s: line %d: memory allocation overflow: %zu + %zu",
+ fname, linenum, a, b);
+ }
+ return a + b;
+}
+#ifndef STATIC_CHECKER
+# define MALLOC_ADD(a, b) \
+ malloc_add_check(a, b, sizeof(a), sizeof(b), __FILE__, __LINE__)
+#else
+/* avoid warning every time about sizeof(b) when b contains any arithmetic */
+# define MALLOC_ADD(a, b) \
+ malloc_add_check(a, b, sizeof(a), sizeof(size_t), __FILE__, __LINE__)
+#endif
+
+#endif
diff --git a/src/lib/md4.c b/src/lib/md4.c
new file mode 100644
index 0000000..06e3231
--- /dev/null
+++ b/src/lib/md4.c
@@ -0,0 +1,300 @@
+/*
+ * MD4 (RFC-1320) message digest.
+ * Modified from MD5 code by Andrey Panin <pazke@donpac.ru>
+ *
+ * Written by Solar Designer <solar@openwall.com> in 2001, and placed in
+ * the public domain. There's absolutely no warranty.
+ *
+ * This differs from Colin Plumb's older public domain implementation in
+ * that no 32-bit integer data type is required, there's no compile-time
+ * endianness configuration, and the function prototypes match OpenSSL's.
+ * The primary goals are portability and ease of use.
+ *
+ * This implementation is meant to be fast, but not as fast as possible.
+ * Some known optimizations are not included to reduce source code size
+ * and avoid compile-time configuration.
+ */
+
+#include "lib.h"
+#include "safe-memset.h"
+#include "md4.h"
+
+/*
+ * The basic MD4 functions.
+ */
+#define F(x, y, z) ((z) ^ ((x) & ((y) ^ (z))))
+#define G(x, y, z) (((x) & (y)) | ((x) & (z)) | ((y) & (z)))
+#define H(x, y, z) ((x) ^ (y) ^ (z))
+
+/*
+ * The MD4 transformation for all four rounds.
+ */
+#define STEP(f, a, b, c, d, x, s) \
+ (a) += f((b), (c), (d)) + (x); \
+ (a) = ((a) << (s)) | ((a) >> (32 - (s)))
+
+
+/*
+ * SET reads 4 input bytes in little-endian byte order and stores them
+ * in a properly aligned word in host byte order.
+ *
+ * The check for little-endian architectures which tolerate unaligned
+ * memory accesses is just an optimization. Nothing will break if it
+ * doesn't work.
+ */
+#if defined(__i386__) || defined(__x86_64__) || defined(__vax__)
+/* uint_fast32_t might be 64 bit, and thus may read 4 more bytes
+ * beyond the end of the buffer. So only read precisely 32 bits
+ */
+#define SET(n) \
+ (*(const uint32_t *)&ptr[(n) * 4])
+#define GET(n) \
+ SET(n)
+#else
+#define SET(n) \
+ (ctx->block[(n)] = \
+ (uint_fast32_t)ptr[(n) * 4] | \
+ ((uint_fast32_t)ptr[(n) * 4 + 1] << 8) | \
+ ((uint_fast32_t)ptr[(n) * 4 + 2] << 16) | \
+ ((uint_fast32_t)ptr[(n) * 4 + 3] << 24))
+#define GET(n) \
+ (ctx->block[(n)])
+#endif
+
+/*
+ * This processes one or more 64-byte data blocks, but does NOT update
+ * the bit counters. There're no alignment requirements.
+ */
+static const void * ATTR_NOWARN_UNUSED_RESULT ATTR_UNSIGNED_WRAPS
+ ATTR_NO_SANITIZE_UNDEFINED ATTR_NO_SANITIZE_INTEGER
+ ATTR_NO_SANITIZE_IMPLICIT_CONVERSION
+body(struct md4_context *ctx, const void *data, size_t size)
+{
+ const unsigned char *ptr;
+ uint32_t a, b, c, d;
+ uint32_t saved_a, saved_b, saved_c, saved_d;
+
+ ptr = data;
+
+ a = ctx->a;
+ b = ctx->b;
+ c = ctx->c;
+ d = ctx->d;
+
+ do {
+ saved_a = a;
+ saved_b = b;
+ saved_c = c;
+ saved_d = d;
+
+/* Round 1 */
+ STEP(F, a, b, c, d, SET( 0), 3);
+ STEP(F, d, a, b, c, SET( 1), 7);
+ STEP(F, c, d, a, b, SET( 2), 11);
+ STEP(F, b, c, d, a, SET( 3), 19);
+
+ STEP(F, a, b, c, d, SET( 4), 3);
+ STEP(F, d, a, b, c, SET( 5), 7);
+ STEP(F, c, d, a, b, SET( 6), 11);
+ STEP(F, b, c, d, a, SET( 7), 19);
+
+ STEP(F, a, b, c, d, SET( 8), 3);
+ STEP(F, d, a, b, c, SET( 9), 7);
+ STEP(F, c, d, a, b, SET(10), 11);
+ STEP(F, b, c, d, a, SET(11), 19);
+
+ STEP(F, a, b, c, d, SET(12), 3);
+ STEP(F, d, a, b, c, SET(13), 7);
+ STEP(F, c, d, a, b, SET(14), 11);
+ STEP(F, b, c, d, a, SET(15), 19);
+/* Round 2 */
+ STEP(G, a, b, c, d, GET( 0) + 0x5A827999, 3);
+ STEP(G, d, a, b, c, GET( 4) + 0x5A827999, 5);
+ STEP(G, c, d, a, b, GET( 8) + 0x5A827999, 9);
+ STEP(G, b, c, d, a, GET(12) + 0x5A827999, 13);
+
+ STEP(G, a, b, c, d, GET( 1) + 0x5A827999, 3);
+ STEP(G, d, a, b, c, GET( 5) + 0x5A827999, 5);
+ STEP(G, c, d, a, b, GET( 9) + 0x5A827999, 9);
+ STEP(G, b, c, d, a, GET(13) + 0x5A827999, 13);
+
+ STEP(G, a, b, c, d, GET( 2) + 0x5A827999, 3);
+ STEP(G, d, a, b, c, GET( 6) + 0x5A827999, 5);
+ STEP(G, c, d, a, b, GET(10) + 0x5A827999, 9);
+ STEP(G, b, c, d, a, GET(14) + 0x5A827999, 13);
+
+ STEP(G, a, b, c, d, GET( 3) + 0x5A827999, 3);
+ STEP(G, d, a, b, c, GET( 7) + 0x5A827999, 5);
+ STEP(G, c, d, a, b, GET(11) + 0x5A827999, 9);
+ STEP(G, b, c, d, a, GET(15) + 0x5A827999, 13);
+/* Round 3 */
+ STEP(H, a, b, c, d, GET( 0) + 0x6ED9EBA1, 3);
+ STEP(H, d, a, b, c, GET( 8) + 0x6ED9EBA1, 9);
+ STEP(H, c, d, a, b, GET( 4) + 0x6ED9EBA1, 11);
+ STEP(H, b, c, d, a, GET(12) + 0x6ED9EBA1, 15);
+
+ STEP(H, a, b, c, d, GET( 2) + 0x6ED9EBA1, 3);
+ STEP(H, d, a, b, c, GET(10) + 0x6ED9EBA1, 9);
+ STEP(H, c, d, a, b, GET( 6) + 0x6ED9EBA1, 11);
+ STEP(H, b, c, d, a, GET(14) + 0x6ED9EBA1, 15);
+
+ STEP(H, a, b, c, d, GET( 1) + 0x6ED9EBA1, 3);
+ STEP(H, d, a, b, c, GET( 9) + 0x6ED9EBA1, 9);
+ STEP(H, c, d, a, b, GET( 5) + 0x6ED9EBA1, 11);
+ STEP(H, b, c, d, a, GET(13) + 0x6ED9EBA1, 15);
+
+ STEP(H, a, b, c, d, GET( 3) + 0x6ED9EBA1, 3);
+ STEP(H, d, a, b, c, GET(11) + 0x6ED9EBA1, 9);
+ STEP(H, c, d, a, b, GET( 7) + 0x6ED9EBA1, 11);
+ STEP(H, b, c, d, a, GET(15) + 0x6ED9EBA1, 15);
+
+ a += saved_a;
+ b += saved_b;
+ c += saved_c;
+ d += saved_d;
+
+ ptr += 64;
+ } while ((size -= 64) != 0);
+
+ ctx->a = a;
+ ctx->b = b;
+ ctx->c = c;
+ ctx->d = d;
+
+ return ptr;
+}
+
+void md4_init(struct md4_context *ctx)
+{
+ ctx->a = 0x67452301;
+ ctx->b = 0xefcdab89;
+ ctx->c = 0x98badcfe;
+ ctx->d = 0x10325476;
+
+ ctx->lo = 0;
+ ctx->hi = 0;
+}
+
+void md4_update(struct md4_context *ctx, const void *data, size_t size)
+{
+ /* @UNSAFE */
+ uint_fast32_t saved_lo;
+ unsigned long used, free;
+
+ saved_lo = ctx->lo;
+ if ((ctx->lo = (saved_lo + size) & 0x1fffffff) < saved_lo)
+ ctx->hi++;
+ ctx->hi += size >> 29;
+
+ used = saved_lo & 0x3f;
+
+ if (used != 0) {
+ free = 64 - used;
+
+ if (size < free) {
+ memcpy(&ctx->buffer[used], data, size);
+ return;
+ }
+
+ memcpy(&ctx->buffer[used], data, free);
+ data = (const unsigned char *) data + free;
+ size -= free;
+ body(ctx, ctx->buffer, 64);
+ }
+
+ if (size >= 64) {
+ data = body(ctx, data, size & ~0x3fUL);
+ size &= 0x3f;
+ }
+
+ memcpy(ctx->buffer, data, size);
+}
+
+void ATTR_NO_SANITIZE_UNDEFINED ATTR_NO_SANITIZE_INTEGER
+ ATTR_NO_SANITIZE_IMPLICIT_CONVERSION
+md4_final(struct md4_context *ctx, unsigned char result[STATIC_ARRAY MD4_RESULTLEN])
+{
+ /* @UNSAFE */
+ unsigned long used, free;
+
+ used = ctx->lo & 0x3f;
+
+ ctx->buffer[used++] = 0x80;
+
+ free = 64 - used;
+
+ if (free < 8) {
+ memset(&ctx->buffer[used], 0, free);
+ body(ctx, ctx->buffer, 64);
+ used = 0;
+ free = 64;
+ }
+
+ memset(&ctx->buffer[used], 0, free - 8);
+
+ ctx->lo <<= 3;
+ ctx->buffer[56] = ctx->lo;
+ ctx->buffer[57] = ctx->lo >> 8;
+ ctx->buffer[58] = ctx->lo >> 16;
+ ctx->buffer[59] = ctx->lo >> 24;
+ ctx->buffer[60] = ctx->hi;
+ ctx->buffer[61] = ctx->hi >> 8;
+ ctx->buffer[62] = ctx->hi >> 16;
+ ctx->buffer[63] = ctx->hi >> 24;
+
+ body(ctx, ctx->buffer, 64);
+
+ result[0] = ctx->a;
+ result[1] = ctx->a >> 8;
+ result[2] = ctx->a >> 16;
+ result[3] = ctx->a >> 24;
+ result[4] = ctx->b;
+ result[5] = ctx->b >> 8;
+ result[6] = ctx->b >> 16;
+ result[7] = ctx->b >> 24;
+ result[8] = ctx->c;
+ result[9] = ctx->c >> 8;
+ result[10] = ctx->c >> 16;
+ result[11] = ctx->c >> 24;
+ result[12] = ctx->d;
+ result[13] = ctx->d >> 8;
+ result[14] = ctx->d >> 16;
+ result[15] = ctx->d >> 24;
+
+ i_zero_safe(ctx);
+}
+
+void md4_get_digest(const void *data, size_t size,
+ unsigned char result[STATIC_ARRAY MD4_RESULTLEN])
+{
+ struct md4_context ctx;
+
+ md4_init(&ctx);
+ md4_update(&ctx, data, size);
+ md4_final(&ctx, result);
+}
+
+static void hash_method_init_md4(void *context)
+{
+ md4_init(context);
+}
+static void hash_method_loop_md4(void *context, const void *data, size_t size)
+{
+ md4_update(context, data, size);
+}
+
+static void hash_method_result_md4(void *context, unsigned char *result_r)
+{
+ md4_final(context, result_r);
+}
+
+const struct hash_method hash_method_md4 = {
+ .name = "md4",
+ .block_size = 64, /* block size is 512 bits */
+ .context_size = sizeof(struct md4_context),
+ .digest_size = MD4_RESULTLEN,
+
+ .init = hash_method_init_md4,
+ .loop = hash_method_loop_md4,
+ .result = hash_method_result_md4,
+};
diff --git a/src/lib/md4.h b/src/lib/md4.h
new file mode 100644
index 0000000..1530d0d
--- /dev/null
+++ b/src/lib/md4.h
@@ -0,0 +1,33 @@
+/*
+ * This is an OpenSSL-compatible implementation of the RSA Data Security,
+ * Inc. MD4 Message-Digest Algorithm.
+ *
+ * Written by Solar Designer <solar@openwall.com> in 2001, and placed in
+ * the public domain. See md4.c for more information.
+ */
+
+#ifndef MD4_H
+#define MD4_H
+
+#include "hash-method.h"
+
+#define MD4_RESULTLEN (128/8)
+
+struct md4_context {
+ uint_fast32_t lo, hi;
+ uint_fast32_t a, b, c, d;
+ unsigned char buffer[64];
+ uint_fast32_t block[MD4_RESULTLEN];
+};
+
+void md4_init(struct md4_context *ctx);
+void md4_update(struct md4_context *ctx, const void *data, size_t size);
+void md4_final(struct md4_context *ctx,
+ unsigned char result[STATIC_ARRAY MD4_RESULTLEN]);
+
+void md4_get_digest(const void *data, size_t size,
+ unsigned char result[STATIC_ARRAY MD4_RESULTLEN]);
+
+extern const struct hash_method hash_method_md4;
+
+#endif
diff --git a/src/lib/md5.c b/src/lib/md5.c
new file mode 100644
index 0000000..6b5da6c
--- /dev/null
+++ b/src/lib/md5.c
@@ -0,0 +1,314 @@
+/*
+ * This is an OpenSSL-compatible implementation of the RSA Data Security,
+ * Inc. MD5 Message-Digest Algorithm.
+ *
+ * Written by Solar Designer <solar@openwall.com> in 2001, and placed in
+ * the public domain. There's absolutely no warranty.
+ *
+ * This differs from Colin Plumb's older public domain implementation in
+ * that no 32-bit integer data type is required, there's no compile-time
+ * endianness configuration, and the function prototypes match OpenSSL's.
+ * The primary goals are portability and ease of use.
+ *
+ * This implementation is meant to be fast, but not as fast as possible.
+ * Some known optimizations are not included to reduce source code size
+ * and avoid compile-time configuration.
+ */
+
+#include "lib.h"
+#include "safe-memset.h"
+#include "md5.h"
+
+/*
+ * The basic MD5 functions.
+ *
+ * F is optimized compared to its RFC 1321 definition just like in Colin
+ * Plumb's implementation.
+ */
+#define F(x, y, z) ((z) ^ ((x) & ((y) ^ (z))))
+#define G(x, y, z) ((y) ^ ((z) & ((x) ^ (y))))
+#define H(x, y, z) ((x) ^ (y) ^ (z))
+#define I(x, y, z) ((y) ^ ((x) | ~(z)))
+
+/*
+ * The MD5 transformation for all four rounds.
+ */
+#define STEP(f, a, b, c, d, x, t, s) \
+ (a) += f((b), (c), (d)) + (x) + (t); \
+ (a) = (((a) << (s)) | (((a) & 0xffffffff) >> (32 - (s)))); \
+ (a) += (b);
+
+/*
+ * SET reads 4 input bytes in little-endian byte order and stores them
+ * in a properly aligned word in host byte order.
+ *
+ * The check for little-endian architectures which tolerate unaligned
+ * memory accesses is just an optimization. Nothing will break if it
+ * doesn't work.
+ */
+#if defined(__i386__) || defined(__x86_64__) || defined(__vax__)
+#define SET(n) \
+ (*(const uint32_t *)&ptr[(n) * 4])
+#define GET(n) \
+ SET(n)
+#else
+#define SET(n) \
+ (ctx->block[(n)] = \
+ (uint_fast32_t)ptr[(n) * 4] | \
+ ((uint_fast32_t)ptr[(n) * 4 + 1] << 8) | \
+ ((uint_fast32_t)ptr[(n) * 4 + 2] << 16) | \
+ ((uint_fast32_t)ptr[(n) * 4 + 3] << 24))
+#define GET(n) \
+ (ctx->block[(n)])
+#endif
+
+/*
+ * This processes one or more 64-byte data blocks, but does NOT update
+ * the bit counters. There're no alignment requirements.
+ */
+static const void * ATTR_NOWARN_UNUSED_RESULT ATTR_UNSIGNED_WRAPS
+ ATTR_NO_SANITIZE_UNDEFINED ATTR_NO_SANITIZE_INTEGER
+ ATTR_NO_SANITIZE_IMPLICIT_CONVERSION
+body(struct md5_context *ctx, const void *data, size_t size)
+{
+ const unsigned char *ptr;
+ uint_fast32_t a, b, c, d;
+ uint_fast32_t saved_a, saved_b, saved_c, saved_d;
+
+ ptr = data;
+
+ a = ctx->a;
+ b = ctx->b;
+ c = ctx->c;
+ d = ctx->d;
+
+ do {
+ saved_a = a;
+ saved_b = b;
+ saved_c = c;
+ saved_d = d;
+
+/* Round 1 */
+ STEP(F, a, b, c, d, SET(0), 0xd76aa478, 7)
+ STEP(F, d, a, b, c, SET(1), 0xe8c7b756, 12)
+ STEP(F, c, d, a, b, SET(2), 0x242070db, 17)
+ STEP(F, b, c, d, a, SET(3), 0xc1bdceee, 22)
+ STEP(F, a, b, c, d, SET(4), 0xf57c0faf, 7)
+ STEP(F, d, a, b, c, SET(5), 0x4787c62a, 12)
+ STEP(F, c, d, a, b, SET(6), 0xa8304613, 17)
+ STEP(F, b, c, d, a, SET(7), 0xfd469501, 22)
+ STEP(F, a, b, c, d, SET(8), 0x698098d8, 7)
+ STEP(F, d, a, b, c, SET(9), 0x8b44f7af, 12)
+ STEP(F, c, d, a, b, SET(10), 0xffff5bb1, 17)
+ STEP(F, b, c, d, a, SET(11), 0x895cd7be, 22)
+ STEP(F, a, b, c, d, SET(12), 0x6b901122, 7)
+ STEP(F, d, a, b, c, SET(13), 0xfd987193, 12)
+ STEP(F, c, d, a, b, SET(14), 0xa679438e, 17)
+ STEP(F, b, c, d, a, SET(15), 0x49b40821, 22)
+
+/* Round 2 */
+ STEP(G, a, b, c, d, GET(1), 0xf61e2562, 5)
+ STEP(G, d, a, b, c, GET(6), 0xc040b340, 9)
+ STEP(G, c, d, a, b, GET(11), 0x265e5a51, 14)
+ STEP(G, b, c, d, a, GET(0), 0xe9b6c7aa, 20)
+ STEP(G, a, b, c, d, GET(5), 0xd62f105d, 5)
+ STEP(G, d, a, b, c, GET(10), 0x02441453, 9)
+ STEP(G, c, d, a, b, GET(15), 0xd8a1e681, 14)
+ STEP(G, b, c, d, a, GET(4), 0xe7d3fbc8, 20)
+ STEP(G, a, b, c, d, GET(9), 0x21e1cde6, 5)
+ STEP(G, d, a, b, c, GET(14), 0xc33707d6, 9)
+ STEP(G, c, d, a, b, GET(3), 0xf4d50d87, 14)
+ STEP(G, b, c, d, a, GET(8), 0x455a14ed, 20)
+ STEP(G, a, b, c, d, GET(13), 0xa9e3e905, 5)
+ STEP(G, d, a, b, c, GET(2), 0xfcefa3f8, 9)
+ STEP(G, c, d, a, b, GET(7), 0x676f02d9, 14)
+ STEP(G, b, c, d, a, GET(12), 0x8d2a4c8a, 20)
+
+/* Round 3 */
+ STEP(H, a, b, c, d, GET(5), 0xfffa3942, 4)
+ STEP(H, d, a, b, c, GET(8), 0x8771f681, 11)
+ STEP(H, c, d, a, b, GET(11), 0x6d9d6122, 16)
+ STEP(H, b, c, d, a, GET(14), 0xfde5380c, 23)
+ STEP(H, a, b, c, d, GET(1), 0xa4beea44, 4)
+ STEP(H, d, a, b, c, GET(4), 0x4bdecfa9, 11)
+ STEP(H, c, d, a, b, GET(7), 0xf6bb4b60, 16)
+ STEP(H, b, c, d, a, GET(10), 0xbebfbc70, 23)
+ STEP(H, a, b, c, d, GET(13), 0x289b7ec6, 4)
+ STEP(H, d, a, b, c, GET(0), 0xeaa127fa, 11)
+ STEP(H, c, d, a, b, GET(3), 0xd4ef3085, 16)
+ STEP(H, b, c, d, a, GET(6), 0x04881d05, 23)
+ STEP(H, a, b, c, d, GET(9), 0xd9d4d039, 4)
+ STEP(H, d, a, b, c, GET(12), 0xe6db99e5, 11)
+ STEP(H, c, d, a, b, GET(15), 0x1fa27cf8, 16)
+ STEP(H, b, c, d, a, GET(2), 0xc4ac5665, 23)
+
+/* Round 4 */
+ STEP(I, a, b, c, d, GET(0), 0xf4292244, 6)
+ STEP(I, d, a, b, c, GET(7), 0x432aff97, 10)
+ STEP(I, c, d, a, b, GET(14), 0xab9423a7, 15)
+ STEP(I, b, c, d, a, GET(5), 0xfc93a039, 21)
+ STEP(I, a, b, c, d, GET(12), 0x655b59c3, 6)
+ STEP(I, d, a, b, c, GET(3), 0x8f0ccc92, 10)
+ STEP(I, c, d, a, b, GET(10), 0xffeff47d, 15)
+ STEP(I, b, c, d, a, GET(1), 0x85845dd1, 21)
+ STEP(I, a, b, c, d, GET(8), 0x6fa87e4f, 6)
+ STEP(I, d, a, b, c, GET(15), 0xfe2ce6e0, 10)
+ STEP(I, c, d, a, b, GET(6), 0xa3014314, 15)
+ STEP(I, b, c, d, a, GET(13), 0x4e0811a1, 21)
+ STEP(I, a, b, c, d, GET(4), 0xf7537e82, 6)
+ STEP(I, d, a, b, c, GET(11), 0xbd3af235, 10)
+ STEP(I, c, d, a, b, GET(2), 0x2ad7d2bb, 15)
+ STEP(I, b, c, d, a, GET(9), 0xeb86d391, 21)
+
+ a += saved_a;
+ b += saved_b;
+ c += saved_c;
+ d += saved_d;
+
+ ptr += 64;
+ } while ((size -= 64) != 0);
+
+ ctx->a = a;
+ ctx->b = b;
+ ctx->c = c;
+ ctx->d = d;
+
+ return ptr;
+}
+
+void md5_init(struct md5_context *ctx)
+{
+ ctx->a = 0x67452301;
+ ctx->b = 0xefcdab89;
+ ctx->c = 0x98badcfe;
+ ctx->d = 0x10325476;
+
+ ctx->lo = 0;
+ ctx->hi = 0;
+ memset(ctx->block, 0, sizeof(ctx->block));
+}
+
+void ATTR_UNSIGNED_WRAPS
+md5_update(struct md5_context *ctx, const void *data, size_t size)
+{
+ /* @UNSAFE */
+ uint_fast32_t saved_lo;
+ unsigned long used, free;
+
+ saved_lo = ctx->lo;
+ if ((ctx->lo = (saved_lo + size) & 0x1fffffff) < saved_lo)
+ ctx->hi++;
+ ctx->hi += size >> 29;
+
+ used = saved_lo & 0x3f;
+
+ if (used != 0) {
+ free = 64 - used;
+
+ if (size < free) {
+ memcpy(&ctx->buffer[used], data, size);
+ return;
+ }
+
+ memcpy(&ctx->buffer[used], data, free);
+ data = (const unsigned char *) data + free;
+ size -= free;
+ body(ctx, ctx->buffer, 64);
+ }
+
+ if (size >= 64) {
+ data = body(ctx, data, size & ~0x3fUL);
+ size &= 0x3f;
+ }
+
+ memcpy(ctx->buffer, data, size);
+}
+
+void ATTR_UNSIGNED_WRAPS ATTR_NO_SANITIZE_UNDEFINED
+ ATTR_NO_SANITIZE_INTEGER ATTR_NO_SANITIZE_IMPLICIT_CONVERSION
+md5_final(struct md5_context *ctx, unsigned char result[STATIC_ARRAY MD5_RESULTLEN])
+{
+ /* @UNSAFE */
+ unsigned long used, free;
+
+ used = ctx->lo & 0x3f;
+
+ ctx->buffer[used++] = 0x80;
+
+ free = 64 - used;
+
+ if (free < 8) {
+ memset(&ctx->buffer[used], 0, free);
+ body(ctx, ctx->buffer, 64);
+ used = 0;
+ free = 64;
+ }
+
+ memset(&ctx->buffer[used], 0, free - 8);
+
+ ctx->lo <<= 3;
+ ctx->buffer[56] = ctx->lo;
+ ctx->buffer[57] = ctx->lo >> 8;
+ ctx->buffer[58] = ctx->lo >> 16;
+ ctx->buffer[59] = ctx->lo >> 24;
+ ctx->buffer[60] = ctx->hi;
+ ctx->buffer[61] = ctx->hi >> 8;
+ ctx->buffer[62] = ctx->hi >> 16;
+ ctx->buffer[63] = ctx->hi >> 24;
+
+ body(ctx, ctx->buffer, 64);
+
+ result[0] = ctx->a;
+ result[1] = ctx->a >> 8;
+ result[2] = ctx->a >> 16;
+ result[3] = ctx->a >> 24;
+ result[4] = ctx->b;
+ result[5] = ctx->b >> 8;
+ result[6] = ctx->b >> 16;
+ result[7] = ctx->b >> 24;
+ result[8] = ctx->c;
+ result[9] = ctx->c >> 8;
+ result[10] = ctx->c >> 16;
+ result[11] = ctx->c >> 24;
+ result[12] = ctx->d;
+ result[13] = ctx->d >> 8;
+ result[14] = ctx->d >> 16;
+ result[15] = ctx->d >> 24;
+
+ i_zero_safe(ctx);
+}
+
+void md5_get_digest(const void *data, size_t size,
+ unsigned char result[STATIC_ARRAY MD5_RESULTLEN])
+{
+ struct md5_context ctx;
+
+ md5_init(&ctx);
+ md5_update(&ctx, data, size);
+ md5_final(&ctx, result);
+}
+
+static void hash_method_init_md5(void *context)
+{
+ md5_init(context);
+}
+static void hash_method_loop_md5(void *context, const void *data, size_t size)
+{
+ md5_update(context, data, size);
+}
+
+static void hash_method_result_md5(void *context, unsigned char *result_r)
+{
+ md5_final(context, result_r);
+}
+
+const struct hash_method hash_method_md5 = {
+ .name = "md5",
+ .block_size = 64, /* block size is 512 bits */
+ .context_size = sizeof(struct md5_context),
+ .digest_size = MD5_RESULTLEN,
+
+ .init = hash_method_init_md5,
+ .loop = hash_method_loop_md5,
+ .result = hash_method_result_md5,
+};
diff --git a/src/lib/md5.h b/src/lib/md5.h
new file mode 100644
index 0000000..682a6c5
--- /dev/null
+++ b/src/lib/md5.h
@@ -0,0 +1,33 @@
+/*
+ * This is an OpenSSL-compatible implementation of the RSA Data Security,
+ * Inc. MD5 Message-Digest Algorithm.
+ *
+ * Written by Solar Designer <solar@openwall.com> in 2001, and placed in
+ * the public domain. See md5.c for more information.
+ */
+
+#ifndef MD5_H
+#define MD5_H
+
+#include "hash-method.h"
+
+#define MD5_RESULTLEN (128/8)
+
+struct md5_context {
+ uint_fast32_t lo, hi;
+ uint_fast32_t a, b, c, d;
+ unsigned char buffer[64];
+ uint_fast32_t block[MD5_RESULTLEN];
+};
+
+void md5_init(struct md5_context *ctx);
+void md5_update(struct md5_context *ctx, const void *data, size_t size);
+void md5_final(struct md5_context *ctx,
+ unsigned char result[STATIC_ARRAY MD5_RESULTLEN]);
+
+void md5_get_digest(const void *data, size_t size,
+ unsigned char result[STATIC_ARRAY MD5_RESULTLEN]);
+
+extern const struct hash_method hash_method_md5;
+
+#endif
diff --git a/src/lib/memarea.c b/src/lib/memarea.c
new file mode 100644
index 0000000..747ea8b
--- /dev/null
+++ b/src/lib/memarea.c
@@ -0,0 +1,93 @@
+/* Copyright (c) 2017-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "memarea.h"
+
+struct memarea {
+ const void *data;
+ size_t size;
+
+ memarea_free_callback_t *callback;
+ void *context;
+
+ int refcount;
+};
+
+static struct memarea memarea_empty = {
+ .refcount = 1,
+};
+
+#undef memarea_init
+struct memarea *
+memarea_init(const void *data, size_t size,
+ memarea_free_callback_t *callback, void *context)
+{
+ struct memarea *area;
+
+ i_assert(callback != NULL);
+
+ area = i_new(struct memarea, 1);
+ area->data = data;
+ area->size = size;
+ area->callback = callback;
+ area->context = context;
+ area->refcount = 1;
+ return area;
+}
+
+struct memarea *memarea_init_empty(void)
+{
+ i_assert(memarea_empty.refcount > 0);
+ memarea_empty.refcount++;
+ return &memarea_empty;
+}
+
+void memarea_ref(struct memarea *area)
+{
+ i_assert(area->refcount > 0);
+ area->refcount++;
+}
+
+void memarea_unref(struct memarea **_area)
+{
+ struct memarea *area = *_area;
+
+ *_area = NULL;
+ i_assert(area->refcount > 0);
+
+ if (--area->refcount > 0)
+ return;
+ i_assert(area != &memarea_empty);
+ area->callback(area->context);
+ i_free(area);
+}
+
+void memarea_free_without_callback(struct memarea **_area)
+{
+ struct memarea *area = *_area;
+
+ *_area = NULL;
+ i_assert(memarea_get_refcount(area) == 1);
+ i_free(area);
+}
+
+unsigned int memarea_get_refcount(struct memarea *area)
+{
+ i_assert(area->refcount > 0);
+ return area->refcount;
+}
+
+const void *memarea_get(struct memarea *area, size_t *size_r)
+{
+ *size_r = area->size;
+ return area->data;
+}
+
+size_t memarea_get_size(struct memarea *area)
+{
+ return area->size;
+}
+
+void memarea_free_callback_noop(void *context ATTR_UNUSED)
+{
+}
diff --git a/src/lib/memarea.h b/src/lib/memarea.h
new file mode 100644
index 0000000..9c546df
--- /dev/null
+++ b/src/lib/memarea.h
@@ -0,0 +1,31 @@
+#ifndef MEMAREA_H
+#define MEMAREA_H
+
+typedef void memarea_free_callback_t(void *context);
+
+/* Create reference counted memory area. The callback is called when the
+ refcount drops to 0. */
+struct memarea *
+memarea_init(const void *data, size_t size,
+ memarea_free_callback_t *callback, void *context);
+#define memarea_init(data, size, callback, context) \
+ memarea_init(data, size - \
+ CALLBACK_TYPECHECK(callback, void (*)(typeof(context))), \
+ (memarea_free_callback_t *)callback, context)
+/* Returns an empty memory area. */
+struct memarea *memarea_init_empty(void);
+
+void memarea_ref(struct memarea *area);
+void memarea_unref(struct memarea **area);
+/* Free the memory area without calling the callback.
+ This is allowed only when refcount==1. */
+void memarea_free_without_callback(struct memarea **area);
+
+unsigned int memarea_get_refcount(struct memarea *area);
+const void *memarea_get(struct memarea *area, size_t *size_r);
+size_t memarea_get_size(struct memarea *area);
+
+/* free-callback that does nothing */
+void memarea_free_callback_noop(void *context);
+
+#endif
diff --git a/src/lib/mempool-allocfree.c b/src/lib/mempool-allocfree.c
new file mode 100644
index 0000000..07b74a8
--- /dev/null
+++ b/src/lib/mempool-allocfree.c
@@ -0,0 +1,330 @@
+/* Copyright (c) 2017-2018 Dovecot authors, see the included COPYING file */
+
+/* @UNSAFE: whole file */
+#include "lib.h"
+#include "safe-memset.h"
+#include "mempool.h"
+#include "llist.h"
+
+/*
+ * As the name implies, allocfree pools support both allocating and freeing
+ * memory.
+ *
+ * Implementation
+ * ==============
+ *
+ * Each allocfree pool contains a pool structure (struct allocfree_pool) to
+ * keep track of allocfree-specific pool information and zero or more blocks
+ * (struct pool_block) that keep track of ranges of memory used to back the
+ * allocations. The blocks are kept in a doubly-linked list used to keep
+ * track of all allocations that belong to the pool.
+ *
+ * +-----------+
+ * | allocfree |
+ * | pool |
+ * +-----+-----+
+ * |
+ * | blocks +------------+ next +------------+ next
+ * \------->| pool block |<=====>| pool block |<=====>...<====> NULL
+ * +------------+ prev +------------+ prev
+ * | <data> | | <data> |
+ * . .
+ * . .
+ * . | <data> |
+ * . +------------+
+ * | <data> |
+ * +------------+
+ *
+ * Creation
+ * --------
+ *
+ * When an allocfree pool is created the linked list of allocated blocks is
+ * initialized to be empty.
+ *
+ * Allocation & Freeing
+ * --------------------
+ *
+ * Since each allocation (via p_malloc()) corresponds to one block,
+ * allocations are simply a matter of:
+ *
+ * - allocating enough memory from the system heap (via calloc()) to hold
+ * the block header and the requested number of bytes,
+ * - making a note of the user-requested size in the block header,
+ * - adding the new block to the pool's linked list of blocks, and
+ * - returning a pointer to the payload area of the block to the caller.
+ *
+ * Freeing memory is simpler. The passed in pointer is converted to a
+ * struct pool_block pointer. Then the block is removed from the pool's
+ * linked list and free()d.
+ *
+ * If the pool was created via pool_allocfree_create_clean(), all blocks are
+ * safe_memset() to zero just before being free()d.
+ *
+ * Reallocation
+ * ------------
+ *
+ * Reallocation is done by calling realloc() with a new size that is large
+ * enough to cover the requested number of bytes plus the block header
+ * overhead.
+ *
+ * Clearing
+ * --------
+ *
+ * Clearing the pool is supposed to return the pool to the same state it was
+ * in when it was first created. To that end, the allocfree pool frees all
+ * the blocks allocated since the pool's creation. In other words, clearing
+ * is equivalent to (but faster than) calling p_free() for each allocation
+ * in the pool.
+ *
+ * Finally, if the pool was created via pool_allocfree_create_clean(), all
+ * blocks are safe_memset() to zero before being free()d.
+ *
+ * Destruction
+ * -----------
+ *
+ * Destroying a pool first clears it (see above) and then the pool structure
+ * itself is safe_memset() to zero (if pool_allocfree_create_clean() was
+ * used) and free()d. (The clearing leaves the pool in a minimal state
+ * with no blocks allocated.)
+ */
+
+struct allocfree_pool {
+ struct pool pool;
+ int refcount;
+ size_t total_alloc_count;
+ size_t total_alloc_used;
+
+ struct pool_block *blocks;
+#ifdef DEBUG
+ char *name;
+#endif
+ bool clean_frees;
+};
+
+struct pool_block {
+ struct pool_block *prev,*next;
+
+ size_t size;
+ unsigned char *block;
+};
+
+#define SIZEOF_ALLOCFREE_POOL MEM_ALIGN(sizeof(struct allocfree_pool))
+#define SIZEOF_POOLBLOCK (MEM_ALIGN(sizeof(struct pool_block)))
+
+static const char *pool_allocfree_get_name(pool_t pool);
+static void pool_allocfree_ref(pool_t pool);
+static void pool_allocfree_unref(pool_t *pool);
+static void *pool_allocfree_malloc(pool_t pool, size_t size);
+static void pool_allocfree_free(pool_t pool, void *mem);
+static void *pool_allocfree_realloc(pool_t pool, void *mem,
+ size_t old_size, size_t new_size);
+static void pool_allocfree_clear(pool_t pool);
+static size_t pool_allocfree_get_max_easy_alloc_size(pool_t pool);
+
+static const struct pool_vfuncs static_allocfree_pool_vfuncs = {
+ pool_allocfree_get_name,
+
+ pool_allocfree_ref,
+ pool_allocfree_unref,
+
+ pool_allocfree_malloc,
+ pool_allocfree_free,
+
+ pool_allocfree_realloc,
+
+ pool_allocfree_clear,
+ pool_allocfree_get_max_easy_alloc_size
+};
+
+static const struct pool static_allocfree_pool = {
+ .v = &static_allocfree_pool_vfuncs,
+
+ .alloconly_pool = FALSE,
+ .datastack_pool = FALSE
+};
+
+pool_t pool_allocfree_create(const char *name ATTR_UNUSED)
+{
+ struct allocfree_pool *pool;
+
+ if (SIZEOF_POOLBLOCK > (SSIZE_T_MAX - POOL_MAX_ALLOC_SIZE))
+ i_panic("POOL_MAX_ALLOC_SIZE is too large");
+
+ pool = calloc(1, SIZEOF_ALLOCFREE_POOL);
+ if (pool == NULL)
+ i_fatal_status(FATAL_OUTOFMEM, "calloc(1, %zu): Out of memory",
+ SIZEOF_ALLOCFREE_POOL);
+#ifdef DEBUG
+ pool->name = strdup(name);
+#endif
+ pool->pool = static_allocfree_pool;
+ pool->refcount = 1;
+ return &pool->pool;
+}
+
+pool_t pool_allocfree_create_clean(const char *name)
+{
+ struct allocfree_pool *apool;
+ pool_t pool;
+
+ pool = pool_allocfree_create(name);
+ apool = (struct allocfree_pool *)pool;
+ apool->clean_frees = TRUE;
+ return pool;
+}
+
+static void pool_allocfree_destroy(struct allocfree_pool *apool)
+{
+ pool_allocfree_clear(&apool->pool);
+ if (apool->clean_frees)
+ safe_memset(apool, 0, SIZEOF_ALLOCFREE_POOL);
+#ifdef DEBUG
+ free(apool->name);
+#endif
+ free(apool);
+}
+
+static const char *pool_allocfree_get_name(pool_t pool ATTR_UNUSED)
+{
+#ifdef DEBUG
+ struct allocfree_pool *apool =
+ container_of(pool, struct allocfree_pool, pool);
+ return apool->name;
+#else
+ return "alloc";
+#endif
+}
+
+static void pool_allocfree_ref(pool_t pool)
+{
+ struct allocfree_pool *apool =
+ container_of(pool, struct allocfree_pool, pool);
+ i_assert(apool->refcount > 0);
+
+ apool->refcount++;
+}
+
+static void pool_allocfree_unref(pool_t *_pool)
+{
+ pool_t pool = *_pool;
+ struct allocfree_pool *apool =
+ container_of(pool, struct allocfree_pool, pool);
+ i_assert(apool->refcount > 0);
+
+ /* erase the pointer before freeing anything, as the pointer may
+ exist inside the pool's memory area */
+ *_pool = NULL;
+
+ if (--apool->refcount > 0)
+ return;
+
+ pool_allocfree_destroy(apool);
+}
+
+static void *pool_block_attach(struct allocfree_pool *apool, struct pool_block *block)
+{
+ i_assert(block->size > 0);
+ DLLIST_PREPEND(&apool->blocks, block);
+ block->block = PTR_OFFSET(block,SIZEOF_POOLBLOCK);
+ apool->total_alloc_used += block->size;
+ apool->total_alloc_count++;
+ return block->block;
+}
+
+static struct pool_block *
+pool_block_detach(struct allocfree_pool *apool, unsigned char *mem)
+{
+ /* cannot use PTR_OFFSET because of negative value */
+ i_assert((uintptr_t)mem >= SIZEOF_POOLBLOCK);
+ struct pool_block *block = (struct pool_block *)(mem - SIZEOF_POOLBLOCK);
+
+ /* make sure the block we are dealing with is correct */
+ i_assert(block->block == mem);
+ i_assert((block->prev == NULL || block->prev->next == block) &&
+ (block->next == NULL || block->next->prev == block));
+
+ i_assert(apool->total_alloc_used >= block->size);
+ i_assert(apool->total_alloc_count > 0);
+ DLLIST_REMOVE(&apool->blocks, block);
+ apool->total_alloc_used -= block->size;
+ apool->total_alloc_count--;
+
+ return block;
+}
+
+static void *pool_allocfree_malloc(pool_t pool, size_t size)
+{
+ struct allocfree_pool *apool =
+ container_of(pool, struct allocfree_pool, pool);
+
+ struct pool_block *block = calloc(1, SIZEOF_POOLBLOCK + size);
+ if (block == NULL)
+ i_fatal_status(FATAL_OUTOFMEM, "calloc(1, %zu): Out of memory",
+ SIZEOF_POOLBLOCK + size);
+ block->size = size;
+ return pool_block_attach(apool, block);
+}
+
+static void pool_allocfree_free(pool_t pool, void *mem)
+{
+ struct allocfree_pool *apool =
+ container_of(pool, struct allocfree_pool, pool);
+ struct pool_block *block = pool_block_detach(apool, mem);
+ if (apool->clean_frees)
+ safe_memset(block, 0, SIZEOF_POOLBLOCK+block->size);
+ free(block);
+}
+
+static void *pool_allocfree_realloc(pool_t pool, void *mem,
+ size_t old_size, size_t new_size)
+{
+ struct allocfree_pool *apool =
+ container_of(pool, struct allocfree_pool, pool);
+ unsigned char *new_mem;
+
+ struct pool_block *block = pool_block_detach(apool, mem);
+ if ((new_mem = realloc(block, SIZEOF_POOLBLOCK+new_size)) == NULL)
+ i_fatal_status(FATAL_OUTOFMEM, "realloc(block, %zu)",
+ SIZEOF_POOLBLOCK+new_size);
+
+ /* zero out new memory */
+ if (new_size > old_size)
+ memset(new_mem + SIZEOF_POOLBLOCK + old_size, 0,
+ new_size - old_size);
+ block = (struct pool_block*)new_mem;
+ block->size = new_size;
+ return pool_block_attach(apool, block);
+}
+
+static void pool_allocfree_clear(pool_t pool)
+{
+ struct allocfree_pool *apool =
+ container_of(pool, struct allocfree_pool, pool);
+ struct pool_block *block, *next;
+
+ for (block = apool->blocks; block != NULL; block = next) {
+ next = block->next;
+ pool_allocfree_free(pool, block->block);
+ }
+ i_assert(apool->total_alloc_used == 0 && apool->total_alloc_count == 0);
+}
+
+static size_t pool_allocfree_get_max_easy_alloc_size(pool_t pool ATTR_UNUSED)
+{
+ return 0;
+}
+
+size_t pool_allocfree_get_total_used_size(pool_t pool)
+{
+ struct allocfree_pool *apool =
+ container_of(pool, struct allocfree_pool, pool);
+ return apool->total_alloc_used;
+}
+
+size_t pool_allocfree_get_total_alloc_size(pool_t pool)
+{
+ struct allocfree_pool *apool =
+ container_of(pool, struct allocfree_pool, pool);
+ return apool->total_alloc_used +
+ SIZEOF_POOLBLOCK*apool->total_alloc_count + sizeof(*apool);
+}
diff --git a/src/lib/mempool-alloconly.c b/src/lib/mempool-alloconly.c
new file mode 100644
index 0000000..26f3360
--- /dev/null
+++ b/src/lib/mempool-alloconly.c
@@ -0,0 +1,546 @@
+/* Copyright (c) 2002-2018 Dovecot authors, see the included COPYING file */
+
+/* @UNSAFE: whole file */
+#include "lib.h"
+#include "safe-memset.h"
+#include "mempool.h"
+
+/*
+ * As the name implies, alloconly pools support only allocating memory.
+ * Memory freeing is not supported, except as a special case - the pool's
+ * last allocation can be freed. Additionally, p_realloc() also tries to
+ * grow an existing allocation if and only if it is the last allocation,
+ * otherwise it just allocates a new memory area and copies the data there.
+ *
+ * Alloconly pools are commonly used for an object that builds its state
+ * from many memory allocations, but doesn't change (much of) its state.
+ * It is simpler to free such an object by destroying the entire memory
+ * pool.
+ *
+ * Implementation
+ * ==============
+ *
+ * Each alloconly pool contains a pool structure (struct alloconly_pool) to
+ * keep track of alloconly-specific pool information and one or more blocks
+ * (struct pool_block) that keep track of ranges of memory used to back the
+ * allocations. The blocks are kept in a linked list implementing a stack.
+ * The block size decreases the further down the stack one goes.
+ *
+ * +-----------+
+ * | alloconly |
+ * | pool |
+ * +-----+-----+
+ * |
+ * | block +------------+ next +------------+ next
+ * \------->| pool block |------>| pool block |------>...
+ * +------------+ +------------+
+ * | <data> | | <data> |
+ * . .
+ * . .
+ * . | <data> |
+ * . +------------+
+ * | <data> |
+ * +------------+
+ *
+ * Creation
+ * --------
+ *
+ * When an alloconly pool is created, one block is allocated. This block is
+ * large enough to hold the necessary internal structures (struct
+ * alloconly_pool and struct pool_block) and still have enough space to
+ * satisfy allocations for at least the amount of space requested by the
+ * consumer via the size argument to pool_alloconly_create().
+ *
+ * Allocation
+ * ----------
+ *
+ * Each allocation (via p_malloc()) checks the top-most block to see whether
+ * or not it has enough space to satisfy the allocation. If there is not
+ * enough space, it allocates a new block (via block_alloc()) to serve as
+ * the new top-most block. This newly-allocated block is guaranteed to have
+ * enough space for the allocation. Then, regardless of whether or not a
+ * new block was allocated, the allocation code reserves enough space in the
+ * top-most block for the allocation and returns a pointer to it to the
+ * caller.
+ *
+ * The free space tracking within each block is very simple. In addition to
+ * keeping track of the size of the block, the block header contains a
+ * "pointer" to the beginning of free space. A new allocation simply moves
+ * this pointer by the number of bytes allocated.
+ *
+ * Reallocation
+ * ------------
+ *
+ * If the passed in allocation is the last allocation in a block and there
+ * is enough space after it, the allocation is resized. Otherwise, a new
+ * buffer is allocated (see Allocation above) and the contents are copied
+ * over.
+ *
+ * Freeing
+ * -------
+ *
+ * Freeing of the last allocation moves the "pointer" to free space back by
+ * the size of the last allocation.
+ *
+ * Freeing of any other allocation is a no-op.
+ *
+ * Clearing
+ * --------
+ *
+ * Clearing the pool is supposed to return the pool to the same state it was
+ * in when it was first created. To that end, the alloconly pool frees all
+ * the blocks allocated since the pool's creation. The remaining block
+ * (allocated during creation) is reset to consider all the space for
+ * allocations as available.
+ *
+ * In other words, the per-block free space tracking variables are set to
+ * indicate that the full block is available and that there have been no
+ * allocations.
+ *
+ * Finally, if the pool was created via pool_alloconly_create_clean(), all
+ * blocks are safe_memset()/memset() to zero before being free()d.
+ *
+ * Destruction
+ * -----------
+ *
+ * Destroying a pool first clears it (see above). The clearing leaves the
+ * pool in a minimal state with only one block allocated. This remaining
+ * block may be safe_memset() to zero if the pool was created with
+ * pool_alloconly_create_clean().
+ *
+ * Since the pool structure itself is allocated from the first block, this
+ * final call to free() will release the memory allocated for struct
+ * alloconly_pool and struct pool.
+ */
+
+#ifndef DEBUG
+# define POOL_ALLOCONLY_MAX_EXTRA MEM_ALIGN(1)
+#else
+# define POOL_ALLOCONLY_MAX_EXTRA \
+ (MEM_ALIGN(sizeof(size_t)) + MEM_ALIGN(1) + MEM_ALIGN(SENTRY_COUNT))
+#endif
+
+struct alloconly_pool {
+ struct pool pool;
+ int refcount;
+
+ struct pool_block *block;
+#ifdef DEBUG
+ const char *name;
+ size_t base_size;
+ bool disable_warning;
+#endif
+ bool clean_frees;
+};
+
+struct pool_block {
+ struct pool_block *prev;
+
+ size_t size;
+ size_t left;
+ size_t last_alloc_size;
+
+ /* unsigned char data[]; */
+};
+#define SIZEOF_POOLBLOCK (MEM_ALIGN(sizeof(struct pool_block)))
+
+#define POOL_BLOCK_DATA(block) \
+ ((unsigned char *) (block) + SIZEOF_POOLBLOCK)
+
+#define DEFAULT_BASE_SIZE MEM_ALIGN(sizeof(struct alloconly_pool))
+
+#ifdef DEBUG
+# define CLEAR_CHR 0xde
+# define SENTRY_COUNT 8
+#else
+# define SENTRY_COUNT 0
+# define CLEAR_CHR 0
+#endif
+
+static const char *pool_alloconly_get_name(pool_t pool);
+static void pool_alloconly_ref(pool_t pool);
+static void pool_alloconly_unref(pool_t *pool);
+static void *pool_alloconly_malloc(pool_t pool, size_t size);
+static void pool_alloconly_free(pool_t pool, void *mem);
+static void *pool_alloconly_realloc(pool_t pool, void *mem,
+ size_t old_size, size_t new_size);
+static void pool_alloconly_clear(pool_t pool);
+static size_t pool_alloconly_get_max_easy_alloc_size(pool_t pool);
+
+static void block_alloc(struct alloconly_pool *pool, size_t size);
+
+static const struct pool_vfuncs static_alloconly_pool_vfuncs = {
+ pool_alloconly_get_name,
+
+ pool_alloconly_ref,
+ pool_alloconly_unref,
+
+ pool_alloconly_malloc,
+ pool_alloconly_free,
+
+ pool_alloconly_realloc,
+
+ pool_alloconly_clear,
+ pool_alloconly_get_max_easy_alloc_size
+};
+
+static const struct pool static_alloconly_pool = {
+ .v = &static_alloconly_pool_vfuncs,
+
+ .alloconly_pool = TRUE,
+ .datastack_pool = FALSE
+};
+
+#ifdef DEBUG
+static void check_sentries(struct pool_block *block)
+{
+ const unsigned char *data = POOL_BLOCK_DATA(block);
+ size_t i, max_pos, alloc_size, used_size;
+
+ used_size = block->size - block->left;
+ for (i = 0; i < used_size; ) {
+ alloc_size = *(size_t *)(data + i);
+ if (alloc_size == 0 || used_size - i < alloc_size)
+ i_panic("mempool-alloconly: saved alloc size broken");
+ i += MEM_ALIGN(sizeof(alloc_size));
+ max_pos = i + MEM_ALIGN(alloc_size + SENTRY_COUNT);
+ i += alloc_size;
+
+ for (; i < max_pos; i++) {
+ if (data[i] != CLEAR_CHR)
+ i_panic("mempool-alloconly: buffer overflow");
+ }
+ }
+
+ if (i != used_size)
+ i_panic("mempool-alloconly: used_size wrong");
+
+ /* The unused data must be NULs */
+ for (; i < block->size; i++) {
+ if (data[i] != '\0')
+ i_unreached();
+ }
+ if (block->prev != NULL)
+ check_sentries(block->prev);
+}
+#endif
+
+pool_t pool_alloconly_create(const char *name ATTR_UNUSED, size_t size)
+{
+ struct alloconly_pool apool, *new_apool;
+ size_t min_alloc = SIZEOF_POOLBLOCK +
+ MEM_ALIGN(sizeof(struct alloconly_pool) + SENTRY_COUNT);
+
+ if (POOL_ALLOCONLY_MAX_EXTRA > (SSIZE_T_MAX - POOL_MAX_ALLOC_SIZE))
+ i_panic("POOL_MAX_ALLOC_SIZE is too large");
+
+#ifdef DEBUG
+ min_alloc += MEM_ALIGN(strlen(name) + 1 + SENTRY_COUNT) +
+ sizeof(size_t)*2;
+#endif
+
+ /* create a fake alloconly_pool so we can call block_alloc() */
+ i_zero(&apool);
+ apool.pool = static_alloconly_pool;
+ apool.refcount = 1;
+
+ if (size < min_alloc)
+ size = nearest_power(size + min_alloc);
+ block_alloc(&apool, size);
+
+ /* now allocate the actual alloconly_pool from the created block */
+ new_apool = p_new(&apool.pool, struct alloconly_pool, 1);
+ *new_apool = apool;
+#ifdef DEBUG
+ if (str_begins(name, MEMPOOL_GROWING) ||
+ getenv("DEBUG_SILENT") != NULL) {
+ name += strlen(MEMPOOL_GROWING);
+ new_apool->disable_warning = TRUE;
+ }
+ new_apool->name = p_strdup(&new_apool->pool, name);
+
+ /* set base_size so p_clear() doesn't trash alloconly_pool structure. */
+ new_apool->base_size = new_apool->block->size - new_apool->block->left;
+ new_apool->block->last_alloc_size = 0;
+#endif
+ /* the first pool allocations must be from the first block */
+ i_assert(new_apool->block->prev == NULL);
+
+ return &new_apool->pool;
+}
+
+pool_t pool_alloconly_create_clean(const char *name, size_t size)
+{
+ struct alloconly_pool *apool;
+ pool_t pool;
+
+ pool = pool_alloconly_create(name, size);
+ apool = container_of(pool, struct alloconly_pool, pool);
+ apool->clean_frees = TRUE;
+ return pool;
+}
+
+static void pool_alloconly_free_block(struct alloconly_pool *apool ATTR_UNUSED,
+ struct pool_block *block)
+{
+#ifdef DEBUG
+ safe_memset(block, CLEAR_CHR, SIZEOF_POOLBLOCK + block->size);
+#else
+ if (apool->clean_frees) {
+ safe_memset(block, CLEAR_CHR,
+ SIZEOF_POOLBLOCK + block->size);
+ }
+#endif
+ free(block);
+}
+
+static void
+pool_alloconly_free_blocks_until_last(struct alloconly_pool *apool)
+{
+ struct pool_block *block;
+
+ /* destroy all blocks but the oldest, which contains the
+ struct alloconly_pool allocation. */
+ while (apool->block->prev != NULL) {
+ block = apool->block;
+ apool->block = block->prev;
+
+ pool_alloconly_free_block(apool, block);
+ }
+}
+
+static void pool_alloconly_destroy(struct alloconly_pool *apool)
+{
+ /* destroy all but the last block */
+ pool_alloconly_free_blocks_until_last(apool);
+
+ /* destroy the last block */
+ pool_alloconly_free_block(apool, apool->block);
+}
+
+static const char *pool_alloconly_get_name(pool_t pool ATTR_UNUSED)
+{
+#ifdef DEBUG
+ struct alloconly_pool *apool =
+ container_of(pool, struct alloconly_pool, pool);
+
+ return apool->name;
+#else
+ return "alloconly";
+#endif
+}
+
+static void pool_alloconly_ref(pool_t pool)
+{
+ struct alloconly_pool *apool =
+ container_of(pool, struct alloconly_pool, pool);
+
+ apool->refcount++;
+}
+
+static void pool_alloconly_unref(pool_t *pool)
+{
+ struct alloconly_pool *apool =
+ container_of(*pool, struct alloconly_pool, pool);
+
+ /* erase the pointer before freeing anything, as the pointer may
+ exist inside the pool's memory area */
+ *pool = NULL;
+
+ if (--apool->refcount > 0)
+ return;
+
+ pool_alloconly_destroy(apool);
+}
+
+static void block_alloc(struct alloconly_pool *apool, size_t size)
+{
+ struct pool_block *block;
+
+ i_assert(size > SIZEOF_POOLBLOCK);
+ i_assert(size <= SSIZE_T_MAX);
+
+ if (apool->block != NULL) {
+ /* each block is at least twice the size of the previous one */
+ if (size <= apool->block->size)
+ size += apool->block->size;
+
+ /* avoid crashing in nearest_power() if size is too large */
+ size = I_MIN(size, SSIZE_T_MAX);
+ size = nearest_power(size);
+ /* nearest_power() could have grown size to SSIZE_T_MAX+1 */
+ size = I_MIN(size, SSIZE_T_MAX);
+#ifdef DEBUG
+ if (!apool->disable_warning) {
+ /* i_debug() overwrites unallocated data in data
+ stack, so make sure everything is allocated before
+ calling it. */
+ t_buffer_alloc_last_full();
+ i_debug("Growing pool '%s' with: %zu",
+ apool->name, size);
+ }
+#endif
+ }
+
+ block = calloc(size, 1);
+ if (unlikely(block == NULL)) {
+ i_fatal_status(FATAL_OUTOFMEM, "block_alloc(%zu"
+ "): Out of memory", size);
+ }
+ block->prev = apool->block;
+ apool->block = block;
+
+ block->size = size - SIZEOF_POOLBLOCK;
+ block->left = block->size;
+}
+
+static void *pool_alloconly_malloc(pool_t pool, size_t size)
+{
+ struct alloconly_pool *apool =
+ container_of(pool, struct alloconly_pool, pool);
+ void *mem;
+ size_t alloc_size;
+
+#ifndef DEBUG
+ alloc_size = MEM_ALIGN(size);
+#else
+ alloc_size = MEM_ALIGN(sizeof(size)) + MEM_ALIGN(size + SENTRY_COUNT);
+#endif
+
+ if (apool->block->left < alloc_size) {
+ /* we need a new block */
+ block_alloc(apool, alloc_size + SIZEOF_POOLBLOCK);
+ }
+
+ mem = POOL_BLOCK_DATA(apool->block) +
+ (apool->block->size - apool->block->left);
+
+ apool->block->left -= alloc_size;
+ apool->block->last_alloc_size = alloc_size;
+#ifdef DEBUG
+ memcpy(mem, &size, sizeof(size));
+ mem = PTR_OFFSET(mem, MEM_ALIGN(sizeof(size)));
+ /* write CLEAR_CHRs to sentry */
+ memset(PTR_OFFSET(mem, size), CLEAR_CHR,
+ MEM_ALIGN(size + SENTRY_COUNT) - size);
+#endif
+ return mem;
+}
+
+static void pool_alloconly_free(pool_t pool, void *mem)
+{
+ struct alloconly_pool *apool =
+ container_of(pool, struct alloconly_pool, pool);
+
+ /* we can free only the last allocation */
+ if (POOL_BLOCK_DATA(apool->block) +
+ (apool->block->size - apool->block->left -
+ apool->block->last_alloc_size) == mem) {
+ memset(mem, 0, apool->block->last_alloc_size);
+ apool->block->left += apool->block->last_alloc_size;
+ apool->block->last_alloc_size = 0;
+ }
+}
+
+static bool pool_alloconly_try_grow(struct alloconly_pool *apool, void *mem, size_t size)
+{
+ /* see if we want to grow the memory we allocated last */
+ if (POOL_BLOCK_DATA(apool->block) +
+ (apool->block->size - apool->block->left -
+ apool->block->last_alloc_size) == mem) {
+ /* yeah, see if we can grow */
+ if (apool->block->left >= size-apool->block->last_alloc_size) {
+ /* just shrink the available size */
+ apool->block->left -=
+ size - apool->block->last_alloc_size;
+ apool->block->last_alloc_size = size;
+ return TRUE;
+ }
+ }
+
+ return FALSE;
+}
+
+static void *pool_alloconly_realloc(pool_t pool, void *mem,
+ size_t old_size, size_t new_size)
+{
+ struct alloconly_pool *apool =
+ container_of(pool, struct alloconly_pool, pool);
+ unsigned char *new_mem;
+
+ if (new_size <= old_size)
+ return mem;
+
+ new_size = MEM_ALIGN(new_size);
+
+ /* see if we can directly grow it */
+ if (!pool_alloconly_try_grow(apool, mem, new_size)) {
+ /* slow way - allocate + copy */
+ new_mem = pool_alloconly_malloc(pool, new_size);
+ memcpy(new_mem, mem, old_size);
+ mem = new_mem;
+ }
+
+ return mem;
+}
+
+static void pool_alloconly_clear(pool_t pool)
+{
+ struct alloconly_pool *apool =
+ container_of(pool, struct alloconly_pool, pool);
+ size_t base_size, avail_size;
+
+#ifdef DEBUG
+ check_sentries(apool->block);
+#endif
+
+ pool_alloconly_free_blocks_until_last(apool);
+
+ /* clear the first block */
+#ifdef DEBUG
+ base_size = apool->base_size;
+#else
+ base_size = DEFAULT_BASE_SIZE;
+#endif
+ avail_size = apool->block->size - base_size;
+ memset(PTR_OFFSET(POOL_BLOCK_DATA(apool->block), base_size), 0,
+ avail_size - apool->block->left);
+ apool->block->left = avail_size;
+ apool->block->last_alloc_size = 0;
+}
+
+static size_t pool_alloconly_get_max_easy_alloc_size(pool_t pool)
+{
+ struct alloconly_pool *apool =
+ container_of(pool, struct alloconly_pool, pool);
+
+ return apool->block->left;
+}
+
+size_t pool_alloconly_get_total_used_size(pool_t pool)
+{
+ struct alloconly_pool *apool =
+ container_of(pool, struct alloconly_pool, pool);
+ struct pool_block *block;
+ size_t size = 0;
+
+ i_assert(pool->v == &static_alloconly_pool_vfuncs);
+
+ for (block = apool->block; block != NULL; block = block->prev)
+ size += block->size - block->left;
+ return size;
+}
+
+size_t pool_alloconly_get_total_alloc_size(pool_t pool)
+{
+ struct alloconly_pool *apool =
+ container_of(pool, struct alloconly_pool, pool);
+ struct pool_block *block;
+ size_t size = 0;
+
+ i_assert(pool->v == &static_alloconly_pool_vfuncs);
+
+ for (block = apool->block; block != NULL; block = block->prev)
+ size += block->size + SIZEOF_POOLBLOCK;
+ return size;
+}
diff --git a/src/lib/mempool-datastack.c b/src/lib/mempool-datastack.c
new file mode 100644
index 0000000..b3c1094
--- /dev/null
+++ b/src/lib/mempool-datastack.c
@@ -0,0 +1,190 @@
+/* Copyright (c) 2002-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "mempool.h"
+
+/*
+ * The datastack pool is a thin wrapper around the datastack API. It exists
+ * to allow datastack allocations via the pool API.
+ *
+ * Note: Do not confuse it with the *unsafe* datastack pool.
+ *
+ * Implementation
+ * ==============
+ *
+ * A datastack pool maintains information about the datastack frame that was
+ * in use when the pool was created so it can sanity check all p_new(),
+ * p_malloc(), and p_realloc() calls.
+ *
+ * Creation
+ * --------
+ *
+ * When a datastack pool is created, a new pool structure is allocated from
+ * the datastack (via t_new()). The current datastack frame number is saved
+ * into the pool's private data (struct datastack_pool).
+ *
+ * Allocation & Reallocation
+ * -------------------------
+ *
+ * After verifying that the saved datastack frame id matches the currently
+ * active one, the p_malloc() and p_realloc() calls get directed to
+ * t_malloc0() and t_try_realloc(), respectively. There is no
+ * per-allocation information to track.
+ *
+ * Freeing
+ * -------
+ *
+ * Freeing is a no-op unless the currently active data stack frame id is
+ * different from the one saved during pool creation, in which case the
+ * process panics.
+ *
+ * Clearing
+ * --------
+ *
+ * A no-op.
+ *
+ * Destruction
+ * -----------
+ *
+ * Since the memory backing the pool structure itself is allocated from the
+ * datastack via t_new(), the pool and all allocations it made are freed
+ * when the datastack frame is popped.
+ *
+ * Even though the pool maintains a reference count, no memory is freed when
+ * it reaches zero. Once the reference count reaches zero, the state of the
+ * pool is undefined and none of its memory maybe be used.
+ */
+
+static const char *pool_data_stack_get_name(pool_t pool);
+static void pool_data_stack_ref(pool_t pool);
+static void pool_data_stack_unref(pool_t *pool);
+static void *pool_data_stack_malloc(pool_t pool, size_t size);
+static void pool_data_stack_free(pool_t pool, void *mem);
+static void *pool_data_stack_realloc(pool_t pool, void *mem,
+ size_t old_size, size_t new_size);
+static void pool_data_stack_clear(pool_t pool);
+static size_t pool_data_stack_get_max_easy_alloc_size(pool_t pool);
+
+static struct pool_vfuncs static_data_stack_pool_vfuncs = {
+ pool_data_stack_get_name,
+
+ pool_data_stack_ref,
+ pool_data_stack_unref,
+
+ pool_data_stack_malloc,
+ pool_data_stack_free,
+
+ pool_data_stack_realloc,
+
+ pool_data_stack_clear,
+ pool_data_stack_get_max_easy_alloc_size
+};
+
+static const struct pool static_data_stack_pool = {
+ .v = &static_data_stack_pool_vfuncs,
+
+ .alloconly_pool = TRUE,
+ .datastack_pool = TRUE
+};
+
+struct datastack_pool {
+ struct pool pool;
+ int refcount;
+
+ unsigned int data_stack_frame;
+};
+
+pool_t pool_datastack_create(void)
+{
+ struct datastack_pool *dpool;
+
+ dpool = t_new(struct datastack_pool, 1);
+ dpool->pool = static_data_stack_pool;
+ dpool->refcount = 1;
+ dpool->data_stack_frame = data_stack_frame_id;
+ return &dpool->pool;
+}
+
+static const char *pool_data_stack_get_name(pool_t pool ATTR_UNUSED)
+{
+ return "data stack";
+}
+
+static void pool_data_stack_ref(pool_t pool)
+{
+ struct datastack_pool *dpool =
+ container_of(pool, struct datastack_pool, pool);
+
+ if (unlikely(dpool->data_stack_frame != data_stack_frame_id))
+ i_panic("pool_data_stack_ref(): stack frame changed");
+
+ dpool->refcount++;
+}
+
+static void pool_data_stack_unref(pool_t *pool)
+{
+ struct datastack_pool *dpool =
+ container_of(*pool, struct datastack_pool, pool);
+
+ if (unlikely(dpool->data_stack_frame != data_stack_frame_id))
+ i_panic("pool_data_stack_unref(): stack frame changed");
+
+ dpool->refcount--;
+ i_assert(dpool->refcount >= 0);
+
+ *pool = NULL;
+}
+
+static void *pool_data_stack_malloc(pool_t pool ATTR_UNUSED, size_t size)
+{
+ struct datastack_pool *dpool =
+ container_of(pool, struct datastack_pool, pool);
+
+ if (unlikely(dpool->data_stack_frame != data_stack_frame_id))
+ i_panic("pool_data_stack_malloc(): stack frame changed");
+
+ return t_malloc0(size);
+}
+
+static void pool_data_stack_free(pool_t pool, void *mem ATTR_UNUSED)
+{
+ struct datastack_pool *dpool =
+ container_of(pool, struct datastack_pool, pool);
+
+ if (unlikely(dpool->data_stack_frame != data_stack_frame_id))
+ i_panic("pool_data_stack_free(): stack frame changed");
+}
+
+static void *pool_data_stack_realloc(pool_t pool, void *mem,
+ size_t old_size, size_t new_size)
+{
+ struct datastack_pool *dpool =
+ container_of(pool, struct datastack_pool, pool);
+ void *new_mem;
+
+ /* @UNSAFE */
+ if (unlikely(dpool->data_stack_frame != data_stack_frame_id))
+ i_panic("pool_data_stack_realloc(): stack frame changed");
+
+ if (old_size >= new_size)
+ return mem;
+
+ if (!t_try_realloc(mem, new_size)) {
+ new_mem = t_malloc_no0(new_size);
+ memcpy(new_mem, mem, old_size);
+ mem = new_mem;
+ }
+
+ memset((char *) mem + old_size, 0, new_size - old_size);
+ return mem;
+}
+
+static void pool_data_stack_clear(pool_t pool ATTR_UNUSED)
+{
+}
+
+static size_t
+pool_data_stack_get_max_easy_alloc_size(pool_t pool ATTR_UNUSED)
+{
+ return t_get_bytes_available();
+}
diff --git a/src/lib/mempool-system.c b/src/lib/mempool-system.c
new file mode 100644
index 0000000..7d1addc
--- /dev/null
+++ b/src/lib/mempool-system.c
@@ -0,0 +1,163 @@
+/* Copyright (c) 2002-2018 Dovecot authors, see the included COPYING file */
+
+/* @UNSAFE: whole file */
+
+#include "lib.h"
+#include "safe-memset.h"
+#include "mempool.h"
+
+/*
+ * The system pool is a thin wrapper around calloc() and free(). It exists
+ * to allow direct heap usage via the pool API.
+ *
+ * Implementation
+ * ==============
+ *
+ * Creation
+ * --------
+ *
+ * The system pool is created statically and therefore is available at any
+ * time.
+ *
+ * Allocation, Reallocation & Freeing
+ * ----------------------------------
+ *
+ * The p_malloc(), p_realloc(), and p_free() calls get directed to calloc(),
+ * realloc(), and free(). There is no additional per-allocation information
+ * to track.
+ *
+ * Clearing
+ * --------
+ *
+ * Not supported. Attempting to clear the system pool will result in a
+ * panic.
+ *
+ * Destruction
+ * -----------
+ *
+ * It is not possible to destroy the system pool. Any attempt to unref the
+ * pool is a no-op.
+ */
+
+#ifndef HAVE_MALLOC_USABLE_SIZE
+/* no extra includes needed */
+#elif defined (HAVE_MALLOC_NP_H)
+# include <malloc_np.h> /* FreeBSD */
+#elif defined (HAVE_MALLOC_H)
+# include <malloc.h> /* Linux */
+#endif
+
+#define CLEAR_CHR 0xde
+
+static const char *pool_system_get_name(pool_t pool);
+static void pool_system_ref(pool_t pool);
+static void pool_system_unref(pool_t *pool);
+static void *pool_system_malloc(pool_t pool, size_t size);
+static void *pool_system_realloc(pool_t pool, void *mem,
+ size_t old_size, size_t new_size);
+static void pool_system_clear(pool_t pool);
+static size_t pool_system_get_max_easy_alloc_size(pool_t pool);
+
+static struct pool_vfuncs static_system_pool_vfuncs = {
+ pool_system_get_name,
+
+ pool_system_ref,
+ pool_system_unref,
+
+ pool_system_malloc,
+ pool_system_free,
+
+ pool_system_realloc,
+
+ pool_system_clear,
+ pool_system_get_max_easy_alloc_size
+};
+
+struct pool static_system_pool = {
+ .v = &static_system_pool_vfuncs,
+
+ .alloconly_pool = FALSE,
+ .datastack_pool = FALSE
+};
+
+pool_t system_pool = &static_system_pool;
+
+static const char *pool_system_get_name(pool_t pool ATTR_UNUSED)
+{
+ return "system";
+}
+
+static void pool_system_ref(pool_t pool ATTR_UNUSED)
+{
+}
+
+static void pool_system_unref(pool_t *pool ATTR_UNUSED)
+{
+}
+
+static void *pool_system_malloc(pool_t pool ATTR_UNUSED, size_t size)
+{
+ void *mem;
+#ifdef DEBUG
+ int old_errno = errno;
+#endif
+
+ mem = calloc(size, 1);
+ if (unlikely(mem == NULL)) {
+ i_fatal_status(FATAL_OUTOFMEM, "pool_system_malloc(%zu): "
+ "Out of memory", size);
+ }
+#ifdef DEBUG
+ /* we rely on errno not changing. it shouldn't. */
+ i_assert(errno == old_errno);
+#endif
+ return mem;
+}
+
+void pool_system_free(pool_t pool ATTR_UNUSED, void *mem ATTR_UNUSED)
+{
+#ifdef DEBUG
+ int old_errno = errno;
+#endif
+#if defined(HAVE_MALLOC_USABLE_SIZE) && defined(DEBUG)
+ safe_memset(mem, CLEAR_CHR, malloc_usable_size(mem));
+#endif
+ free(mem);
+#ifdef DEBUG
+ /* we rely on errno not changing. it shouldn't. */
+ i_assert(errno == old_errno);
+#endif
+}
+
+static void *pool_system_realloc(pool_t pool ATTR_UNUSED, void *mem,
+ size_t old_size, size_t new_size)
+{
+#if defined(HAVE_MALLOC_USABLE_SIZE)
+ i_assert(old_size == SIZE_MAX || mem == NULL ||
+ old_size <= malloc_usable_size(mem));
+#endif
+
+ mem = realloc(mem, new_size);
+ if (unlikely(mem == NULL)) {
+ i_fatal_status(FATAL_OUTOFMEM, "pool_system_realloc(%zu): "
+ "Out of memory", new_size);
+ }
+
+ if (old_size < new_size) {
+ /* clear new data */
+ memset((char *) mem + old_size, 0, new_size - old_size);
+ }
+
+ return mem;
+}
+
+static void ATTR_NORETURN
+pool_system_clear(pool_t pool ATTR_UNUSED)
+{
+ i_panic("pool_system_clear() must not be called");
+}
+
+static size_t pool_system_get_max_easy_alloc_size(pool_t pool ATTR_UNUSED)
+{
+ return 0;
+}
diff --git a/src/lib/mempool-unsafe-datastack.c b/src/lib/mempool-unsafe-datastack.c
new file mode 100644
index 0000000..2f2751d
--- /dev/null
+++ b/src/lib/mempool-unsafe-datastack.c
@@ -0,0 +1,135 @@
+/* Copyright (c) 2002-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "mempool.h"
+
+/*
+ * The unsafe datastack pool is a very thin wrapper around the datastack
+ * API. It is a simpler version of the datastack pool that does not do any
+ * sanity checking, it simply forwards the calls to the datastack API. It
+ * exists to allow some internal APIs to make datastack allocations via the
+ * pool API.
+ *
+ * Note to consumers: Consider using the (safe) datastack pool instead of
+ * this one.
+ *
+ * Implementation
+ * ==============
+ *
+ * Creation
+ * --------
+ *
+ * The unsafe datastack pool is created statically and therefore is
+ * available at any time after the datastack allocator is initialized.
+ *
+ * Allocation & Reallocation
+ * -------------------------
+ *
+ * The p_malloc() and p_realloc() calls get directed to t_malloc0() and
+ * t_try_realloc(), respectively. There is no additional per-allocation
+ * information to track.
+ *
+ * Freeing
+ * -------
+ *
+ * A no-op.
+ *
+ * Clearing
+ * --------
+ *
+ * A no-op.
+ *
+ * Destruction
+ * -----------
+ *
+ * It is not possible to destroy the unsafe datastack pool. Any attempt to
+ * unref the pool is a no-op.
+ */
+
+static const char *pool_unsafe_data_stack_get_name(pool_t pool);
+static void pool_unsafe_data_stack_ref(pool_t pool);
+static void pool_unsafe_data_stack_unref(pool_t *pool);
+static void *pool_unsafe_data_stack_malloc(pool_t pool, size_t size);
+static void pool_unsafe_data_stack_free(pool_t pool, void *mem);
+static void *pool_unsafe_data_stack_realloc(pool_t pool, void *mem,
+ size_t old_size, size_t new_size);
+static void pool_unsafe_data_stack_clear(pool_t pool);
+static size_t pool_unsafe_data_stack_get_max_easy_alloc_size(pool_t pool);
+
+static struct pool_vfuncs static_unsafe_data_stack_pool_vfuncs = {
+ pool_unsafe_data_stack_get_name,
+
+ pool_unsafe_data_stack_ref,
+ pool_unsafe_data_stack_unref,
+
+ pool_unsafe_data_stack_malloc,
+ pool_unsafe_data_stack_free,
+
+ pool_unsafe_data_stack_realloc,
+
+ pool_unsafe_data_stack_clear,
+ pool_unsafe_data_stack_get_max_easy_alloc_size
+};
+
+static struct pool static_unsafe_data_stack_pool = {
+ .v = &static_unsafe_data_stack_pool_vfuncs,
+
+ .alloconly_pool = TRUE,
+ .datastack_pool = TRUE
+};
+
+pool_t unsafe_data_stack_pool = &static_unsafe_data_stack_pool;
+
+static const char *pool_unsafe_data_stack_get_name(pool_t pool ATTR_UNUSED)
+{
+ return "unsafe data stack";
+}
+
+static void pool_unsafe_data_stack_ref(pool_t pool ATTR_UNUSED)
+{
+}
+
+static void pool_unsafe_data_stack_unref(pool_t *pool ATTR_UNUSED)
+{
+}
+
+static void *pool_unsafe_data_stack_malloc(pool_t pool ATTR_UNUSED,
+ size_t size)
+{
+ return t_malloc0(size);
+}
+
+static void pool_unsafe_data_stack_free(pool_t pool ATTR_UNUSED,
+ void *mem ATTR_UNUSED)
+{
+}
+
+static void *pool_unsafe_data_stack_realloc(pool_t pool ATTR_UNUSED,
+ void *mem,
+ size_t old_size, size_t new_size)
+{
+ void *new_mem;
+
+ /* @UNSAFE */
+ if (old_size >= new_size)
+ return mem;
+
+ if (!t_try_realloc(mem, new_size)) {
+ new_mem = t_malloc_no0(new_size);
+ memcpy(new_mem, mem, old_size);
+ mem = new_mem;
+ }
+
+ memset((char *) mem + old_size, 0, new_size - old_size);
+ return mem;
+}
+
+static void pool_unsafe_data_stack_clear(pool_t pool ATTR_UNUSED)
+{
+}
+
+static size_t
+pool_unsafe_data_stack_get_max_easy_alloc_size(pool_t pool ATTR_UNUSED)
+{
+ return t_get_bytes_available();
+}
diff --git a/src/lib/mempool.c b/src/lib/mempool.c
new file mode 100644
index 0000000..a657481
--- /dev/null
+++ b/src/lib/mempool.c
@@ -0,0 +1,25 @@
+/* Copyright (c) 2005-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+
+/* The various implementations of pools API assume that they'll never be
+ asked for more than SSIZE_T_MAX bytes. This is a sanity check to make
+ sure nobody accidentally bumped the define beyond what's expected. */
+#if POOL_MAX_ALLOC_SIZE > SSIZE_T_MAX
+#error "POOL_MAX_ALLOC_SIZE is too large"
+#endif
+
+size_t pool_get_exp_grown_size(pool_t pool, size_t old_size, size_t min_size)
+{
+ size_t exp_size, easy_size;
+
+ i_assert(old_size < min_size);
+
+ exp_size = nearest_power(min_size);
+ easy_size = old_size + p_get_max_easy_alloc_size(pool);
+
+ if (easy_size < exp_size && easy_size >= min_size)
+ exp_size = easy_size;
+ i_assert(exp_size >= min_size);
+ return exp_size;
+}
diff --git a/src/lib/mempool.h b/src/lib/mempool.h
new file mode 100644
index 0000000..9c7aca0
--- /dev/null
+++ b/src/lib/mempool.h
@@ -0,0 +1,179 @@
+#ifndef MEMPOOL_H
+#define MEMPOOL_H
+
+#include "macros.h"
+
+/* When DEBUG is enabled, Dovecot warns whenever a memory pool is grown.
+ This is done so that the initial pool size could be set large enough so that
+ it wouldn't grow in normal use. For some memory pools it's too difficult
+ to calculate a good initial size, so this prefix should be used with those
+ pools to disable the warning. */
+#define MEMPOOL_GROWING "GROWING-"
+
+/* The maximum allocation size that's allowed. Anything larger than that
+ will panic. No pool ever should need more than 4kB of overhead per
+ allocation. */
+#define POOL_MAX_ALLOC_SIZE (SSIZE_T_MAX - 4096)
+
+/* Memory allocated and reallocated (the new data in it) in pools is always
+ zeroed, it will cost only a few CPU cycles and may well save some debug
+ time. */
+
+typedef struct pool *pool_t;
+
+struct pool_vfuncs {
+ const char *(*get_name)(pool_t pool);
+
+ void (*ref)(pool_t pool);
+ void (*unref)(pool_t *pool);
+
+ void *(*malloc)(pool_t pool, size_t size) ATTR_RETURNS_NONNULL;
+ void (*free)(pool_t pool, void *mem);
+
+ /* memory in old_size..new_size will be zeroed */
+ void *(*realloc)(pool_t pool, void *mem,
+ size_t old_size, size_t new_size)
+ ATTR_WARN_UNUSED_RESULT ATTR_RETURNS_NONNULL;
+
+ /* Frees all the memory in pool. NOTE: system_pool doesn't support
+ this and crashes if it's used */
+ void (*clear)(pool_t pool);
+
+ /* Returns the maximum amount of bytes that can be allocated with
+ minimal trouble. If there's no such concept, always returns 0. */
+ size_t (*get_max_easy_alloc_size)(pool_t pool);
+};
+
+struct pool {
+ const struct pool_vfuncs *v;
+
+ bool alloconly_pool:1;
+ bool datastack_pool:1;
+};
+
+/* system_pool uses calloc() + realloc() + free() */
+extern pool_t system_pool;
+extern struct pool static_system_pool;
+
+/* memory allocated from data_stack is valid only until next t_pop() call.
+ No checks are performed. */
+extern pool_t unsafe_data_stack_pool;
+
+/* Create a new alloc-only pool. Note that `size' specifies the initial
+ malloc()ed block size, part of it is used internally. */
+pool_t pool_alloconly_create(const char *name, size_t size);
+/* Like alloconly pool, but clear the memory before freeing it. The idea is
+ that you could allocate memory for storing sensitive information from this
+ pool, and be sure that it gets cleared from the memory when it's no longer
+ needed. */
+pool_t pool_alloconly_create_clean(const char *name, size_t size);
+
+/* When allocating memory from returned pool, the data stack frame must be
+ the same as it was when calling this function. pool_unref() also checks
+ that the stack frame is the same. This should make it quite safe to use. */
+pool_t pool_datastack_create(void);
+
+/* Create new alloc pool. This is very similar to system pool, but it
+ will deallocate all memory on deinit. */
+pool_t pool_allocfree_create(const char *name);
+
+/* Like alloc pool, but all memory is cleaned before freeing.
+ See pool_alloconly_create_clean. */
+pool_t pool_allocfree_create_clean(const char *name);
+
+/* Similar to nearest_power(), but try not to exceed buffer's easy
+ allocation size. If you don't have any explicit minimum size, use
+ old_size + 1. */
+size_t pool_get_exp_grown_size(pool_t pool, size_t old_size, size_t min_size);
+
+/* We require sizeof(type) to be <= UINT_MAX. This allows compiler to optimize
+ away the entire MALLOC_MULTIPLY() call on 64bit systems. */
+#define p_new(pool, type, count) \
+ ((type *) p_malloc(pool, MALLOC_MULTIPLY((unsigned int)sizeof(type), (count))) + \
+ COMPILE_ERROR_IF_TRUE(sizeof(type) > UINT_MAX))
+
+#define p_realloc_type(pool, mem, type, old_count, new_count) \
+ ((type *) p_realloc(pool, mem, \
+ MALLOC_MULTIPLY((unsigned int)sizeof(type), (old_count)), \
+ MALLOC_MULTIPLY((unsigned int)sizeof(type), (new_count))) + \
+ COMPILE_ERROR_IF_TRUE(sizeof(type) > UINT_MAX))
+
+static inline void * ATTR_MALLOC ATTR_RETURNS_NONNULL
+p_malloc(pool_t pool, size_t size)
+{
+ if (unlikely(size == 0 || size > POOL_MAX_ALLOC_SIZE))
+ i_panic("Trying to allocate %zu bytes", size);
+
+ return pool->v->malloc(pool, size);
+}
+
+static inline void * ATTR_WARN_UNUSED_RESULT ATTR_RETURNS_NONNULL
+p_realloc(pool_t pool, void *mem, size_t old_size, size_t new_size)
+{
+ if (unlikely(new_size == 0 || new_size > POOL_MAX_ALLOC_SIZE))
+ i_panic("Trying to reallocate %zu -> %zu bytes",
+ old_size, new_size);
+
+ if (mem == NULL)
+ return pool->v->malloc(pool, new_size);
+
+ return pool->v->realloc(pool, mem, old_size, new_size);
+}
+
+/* Free the memory. p_free() and p_free_and_null() are now guaranteed to both
+ set mem=NULL, so either one of them can be used. */
+#define p_free(pool, mem) \
+ STMT_START { \
+ p_free_internal(pool, mem); \
+ (mem) = NULL; \
+ } STMT_END
+#define p_free_and_null(pool, mem) p_free(pool, mem)
+
+static inline void p_free_internal(pool_t pool, void *mem)
+{
+ if (mem != NULL)
+ pool->v->free(pool, mem);
+}
+
+static inline void p_clear(pool_t pool)
+{
+ pool->v->clear(pool);
+}
+
+static inline size_t p_get_max_easy_alloc_size(pool_t pool)
+{
+ return pool->v->get_max_easy_alloc_size(pool);
+}
+
+static inline const char *pool_get_name(pool_t pool)
+{
+ return pool->v->get_name(pool);
+}
+
+static inline void pool_ref(pool_t pool)
+{
+ pool->v->ref(pool);
+}
+
+static inline void pool_unref(pool_t *pool)
+{
+ if (*pool != NULL)
+ (*pool)->v->unref(pool);
+}
+
+/* These functions are only for pools created with pool_alloconly_create(): */
+
+/* Returns how much memory has been allocated from this pool. */
+size_t pool_alloconly_get_total_used_size(pool_t pool);
+/* Returns how much system memory has been allocated for this pool. */
+size_t pool_alloconly_get_total_alloc_size(pool_t pool);
+
+/* Returns how much memory has been allocated from this pool. */
+size_t pool_allocfree_get_total_used_size(pool_t pool);
+/* Returns how much system memory has been allocated for this pool. */
+size_t pool_allocfree_get_total_alloc_size(pool_t pool);
+
+/* private: */
+void pool_system_free(pool_t pool, void *mem);
+
+#endif
diff --git a/src/lib/mkdir-parents.c b/src/lib/mkdir-parents.c
new file mode 100644
index 0000000..93aa87d
--- /dev/null
+++ b/src/lib/mkdir-parents.c
@@ -0,0 +1,177 @@
+/* Copyright (c) 2003-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "str.h"
+#include "eacces-error.h"
+#include "mkdir-parents.h"
+#include "ipwd.h"
+
+#include <sys/stat.h>
+#include <fcntl.h>
+#include <unistd.h>
+
+static int ATTR_NULL(5)
+mkdir_chown_full(const char *path, mode_t mode, uid_t uid,
+ gid_t gid, const char *gid_origin)
+{
+ string_t *str;
+ mode_t old_mask;
+ unsigned int i;
+ int ret, fd = -1, orig_errno;
+
+ for (i = 0;; i++) {
+ old_mask = umask(0);
+ ret = mkdir(path, mode);
+ umask(old_mask);
+ if (ret < 0)
+ break;
+ fd = open(path, O_RDONLY);
+ if (fd != -1)
+ break;
+ if (errno != ENOENT || i == 3) {
+ i_error("open(%s) failed: %m", path);
+ return -1;
+ }
+ /* it was just rmdir()ed by someone else? retry */
+ }
+
+ if (ret < 0) {
+ if (errno == EISDIR || errno == ENOSYS) {
+ /* EISDIR check is for BSD/OS which returns it if path
+ contains '/' at the end and it exists.
+
+ ENOSYS check is for NFS mount points. */
+ errno = EEXIST;
+ }
+ i_assert(fd == -1);
+ return -1;
+ }
+ if (fchown(fd, uid, gid) < 0) {
+ i_close_fd(&fd);
+ orig_errno = errno;
+ if (rmdir(path) < 0 && errno != ENOENT)
+ i_error("rmdir(%s) failed: %m", path);
+ errno = orig_errno;
+
+ if (errno == EPERM && uid == (uid_t)-1) {
+ i_error("%s", eperm_error_get_chgrp("fchown", path, gid,
+ gid_origin));
+ return -1;
+ }
+
+ str = t_str_new(256);
+ str_printfa(str, "fchown(%s, %ld", path,
+ uid == (uid_t)-1 ? -1L : (long)uid);
+ if (uid != (uid_t)-1) {
+ struct passwd pw;
+
+ if (i_getpwuid(uid, &pw) > 0)
+ str_printfa(str, "(%s)", pw.pw_name);
+
+ }
+ str_printfa(str, ", %ld",
+ gid == (gid_t)-1 ? -1L : (long)gid);
+ if (gid != (gid_t)-1) {
+ struct group gr;
+
+ if (i_getgrgid(uid, &gr) > 0)
+ str_printfa(str, "(%s)", gr.gr_name);
+ }
+ errno = orig_errno;
+ i_error("%s) failed: %m", str_c(str));
+ return -1;
+ }
+ if (gid != (gid_t)-1 && (mode & S_ISGID) == 0) {
+ /* make sure the directory doesn't have setgid bit enabled
+ (in case its parent had) */
+ if (fchmod(fd, mode) < 0) {
+ orig_errno = errno;
+ if (rmdir(path) < 0 && errno != ENOENT)
+ i_error("rmdir(%s) failed: %m", path);
+ errno = orig_errno;
+ i_error("fchmod(%s) failed: %m", path);
+ i_close_fd(&fd);
+ return -1;
+ }
+ }
+ i_close_fd(&fd);
+ return 0;
+}
+
+int mkdir_chown(const char *path, mode_t mode, uid_t uid, gid_t gid)
+{
+ return mkdir_chown_full(path, mode, uid, gid, NULL);
+}
+
+int mkdir_chgrp(const char *path, mode_t mode,
+ gid_t gid, const char *gid_origin)
+{
+ return mkdir_chown_full(path, mode, (uid_t)-1, gid, gid_origin);
+}
+
+static int ATTR_NULL(5)
+mkdir_parents_chown_full(const char *path, mode_t mode, uid_t uid, gid_t gid,
+ const char *gid_origin)
+{
+ const char *p;
+ int ret;
+
+ if (mkdir_chown_full(path, mode, uid, gid, gid_origin) < 0) {
+ if (errno != ENOENT)
+ return -1;
+
+ /* doesn't exist, try recursively creating our parent dir */
+ p = strrchr(path, '/');
+ if (p == NULL || p == path)
+ return -1; /* shouldn't happen */
+
+ T_BEGIN {
+ ret = mkdir_parents_chown_full(t_strdup_until(path, p),
+ mode, uid,
+ gid, gid_origin);
+ } T_END;
+ if (ret < 0 && errno != EEXIST)
+ return -1;
+
+ /* should work now */
+ if (mkdir_chown_full(path, mode, uid, gid, gid_origin) < 0)
+ return -1;
+ }
+ return 0;
+}
+
+int mkdir_parents_chown(const char *path, mode_t mode, uid_t uid, gid_t gid)
+{
+ return mkdir_parents_chown_full(path, mode, uid, gid, NULL);
+}
+
+int mkdir_parents_chgrp(const char *path, mode_t mode,
+ gid_t gid, const char *gid_origin)
+{
+ return mkdir_parents_chown_full(path, mode, (uid_t)-1, gid, gid_origin);
+}
+
+int mkdir_parents(const char *path, mode_t mode)
+{
+ return mkdir_parents_chown(path, mode, (uid_t)-1, (gid_t)-1);
+}
+
+int stat_first_parent(const char *path, const char **root_dir_r,
+ struct stat *st_r)
+{
+ const char *p;
+
+ while (stat(path, st_r) < 0) {
+ if (errno != ENOENT || strcmp(path, "/") == 0) {
+ *root_dir_r = path;
+ return -1;
+ }
+ p = strrchr(path, '/');
+ if (p == NULL)
+ path = "/";
+ else
+ path = t_strdup_until(path, p);
+ }
+ *root_dir_r = path;
+ return 0;
+}
diff --git a/src/lib/mkdir-parents.h b/src/lib/mkdir-parents.h
new file mode 100644
index 0000000..9b5ddf0
--- /dev/null
+++ b/src/lib/mkdir-parents.h
@@ -0,0 +1,33 @@
+#ifndef MKDIR_PARENTS_H
+#define MKDIR_PARENTS_H
+
+#include <sys/stat.h>
+
+/* Create path and all the directories under it if needed. Permissions for
+ existing directories isn't changed. Returns 0 if ok. If directory already
+ exists, returns -1 with errno=EEXIST. */
+int mkdir_parents(const char *path, mode_t mode);
+
+/* Like mkdir_parents(), but use the given uid/gid for newly created
+ directories. (uid_t)-1 or (gid_t)-1 can be used to indicate that it
+ doesn't need to be changed. If gid isn't (gid_t)-1 and the parent directory
+ had setgid-bit enabled, it's removed unless explicitly included in the
+ mode. */
+int mkdir_parents_chown(const char *path, mode_t mode, uid_t uid, gid_t gid);
+/* Like mkdir_parents_chown(), but change only group. If chown() fails with
+ EACCES, use gid_origin in the error message. */
+int mkdir_parents_chgrp(const char *path, mode_t mode,
+ gid_t gid, const char *gid_origin);
+
+/* Like mkdir_parents_chown(), but don't actually create any parents. */
+int mkdir_chown(const char *path, mode_t mode, uid_t uid, gid_t gid);
+int mkdir_chgrp(const char *path, mode_t mode,
+ gid_t gid, const char *gid_origin);
+
+/* stat() the path or its first parent that exists. Returns 0 if ok, -1 if
+ failed. root_dir is set to the last stat()ed directory (on success and
+ on failure). */
+int stat_first_parent(const char *path, const char **root_dir_r,
+ struct stat *st_r);
+
+#endif
diff --git a/src/lib/mmap-anon.c b/src/lib/mmap-anon.c
new file mode 100644
index 0000000..fbb2c47
--- /dev/null
+++ b/src/lib/mmap-anon.c
@@ -0,0 +1,183 @@
+/* Copyright (c) 2002-2018 Dovecot authors, see the included COPYING file */
+
+/* @UNSAFE: whole file */
+
+#include "lib.h"
+#include "mmap-util.h"
+
+#include <fcntl.h>
+
+#ifndef MAP_ANONYMOUS
+# ifdef MAP_ANON
+# define MAP_ANONYMOUS MAP_ANON
+# else
+# define MAP_ANONYMOUS 0
+# endif
+#endif
+
+#ifndef HAVE_LINUX_MREMAP
+
+#include <sys/mman.h>
+
+#define MMAP_SIGNATURE 0xdeadbeef
+
+#define PAGE_ALIGN(size) \
+ (((size) + (size_t)page_size-1) & ~(size_t)(page_size-1))
+
+struct anon_header {
+ unsigned int signature;
+ size_t size;
+};
+
+static int page_size = 0;
+static int header_size = 0;
+static int zero_fd = -1;
+
+static void movable_mmap_init(void)
+{
+#if MAP_ANONYMOUS == 0
+ /* mmap()ing /dev/zero should be the same with some platforms */
+ zero_fd = open("/dev/zero", O_RDWR);
+ if (zero_fd == -1)
+ i_fatal("Can't open /dev/zero for creating anonymous mmap: %m");
+ fd_close_on_exec(zero_fd, TRUE);
+#endif
+
+ page_size = getpagesize();
+ header_size = page_size;
+}
+
+void *mmap_anon(size_t length)
+{
+ struct anon_header *hdr;
+ void *base;
+
+ if (header_size == 0)
+ movable_mmap_init();
+
+ /* we need extra page to store the pieces which construct
+ the full mmap. also allocate only page-aligned mmap sizes. */
+ length = PAGE_ALIGN(length + header_size);
+
+ base = mmap(NULL, length, PROT_READ | PROT_WRITE,
+ MAP_ANONYMOUS | MAP_PRIVATE, zero_fd, 0);
+ if (base == MAP_FAILED)
+ return MAP_FAILED;
+
+ /* initialize the header */
+ hdr = base;
+ hdr->signature = MMAP_SIGNATURE;
+ hdr->size = length - header_size;
+
+ return (char *) hdr + header_size;
+}
+
+static void *mremap_move(struct anon_header *hdr, size_t new_size)
+{
+ void *new_base;
+ char *p;
+ size_t block_size, old_size;
+
+ new_base = mmap_anon(new_size);
+ if (new_base == MAP_FAILED)
+ return MAP_FAILED;
+
+ /* If we're moving large memory areas, it takes less memory to
+ copy the memory pages in smaller blocks. */
+ old_size = hdr->size;
+ block_size = 1024*1024;
+
+ p = (char *) hdr + header_size + hdr->size;
+ do {
+ if (block_size > old_size)
+ block_size = old_size;
+ p -= block_size;
+ old_size -= block_size;
+
+ memcpy((char *) new_base + old_size, p, block_size);
+ if (munmap((void *) p, block_size) < 0)
+ i_panic("munmap() failed: %m");
+ } while (old_size != 0);
+
+ if (munmap((void *) hdr, header_size) < 0)
+ i_panic("munmap() failed: %m");
+
+ return new_base;
+}
+
+void *mremap_anon(void *old_address, size_t old_size ATTR_UNUSED,
+ size_t new_size, unsigned long flags)
+{
+ struct anon_header *hdr;
+
+ if (old_address == NULL || old_address == MAP_FAILED) {
+ errno = EINVAL;
+ return MAP_FAILED;
+ }
+
+ hdr = (struct anon_header *) ((char *) old_address - header_size);
+ if (hdr->signature != MMAP_SIGNATURE)
+ i_panic("movable_mremap(): Invalid old_address");
+
+ new_size = PAGE_ALIGN(new_size);
+
+ if (new_size > hdr->size) {
+ /* grow */
+ if ((flags & MREMAP_MAYMOVE) == 0) {
+ errno = ENOMEM;
+ return MAP_FAILED;
+ }
+
+ return mremap_move(hdr, new_size);
+ }
+
+ if (new_size < hdr->size) {
+ /* shrink */
+ if (munmap((void *) ((char *) hdr + header_size + new_size),
+ hdr->size - new_size) < 0)
+ i_panic("munmap() failed: %m");
+ hdr->size = new_size;
+ }
+
+ return old_address;
+}
+
+int munmap_anon(void *start, size_t length ATTR_UNUSED)
+{
+ struct anon_header *hdr;
+
+ if (start == NULL || start == MAP_FAILED) {
+ errno = EINVAL;
+ return -1;
+ }
+
+ hdr = (struct anon_header *) ((char *) start - header_size);
+ if (hdr->signature != MMAP_SIGNATURE)
+ i_panic("movable_munmap(): Invalid address");
+
+ if (munmap((void *) hdr, hdr->size + header_size) < 0)
+ i_panic("munmap() failed: %m");
+
+ return 0;
+}
+
+#else
+
+void *mmap_anon(size_t length)
+{
+ return mmap(NULL, length, PROT_READ | PROT_WRITE,
+ MAP_ANONYMOUS | MAP_PRIVATE, -1, 0);
+}
+
+void *mremap_anon(void *old_address, size_t old_size, size_t new_size,
+ unsigned long flags)
+{
+ return mremap(old_address, old_size, new_size, flags);
+}
+
+int munmap_anon(void *start, size_t length)
+{
+ return munmap(start, length);
+}
+
+#endif
diff --git a/src/lib/mmap-util.c b/src/lib/mmap-util.c
new file mode 100644
index 0000000..8754226
--- /dev/null
+++ b/src/lib/mmap-util.c
@@ -0,0 +1,63 @@
+/* Copyright (c) 2002-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "mmap-util.h"
+
+#include <sys/stat.h>
+
+void *mmap_file(int fd, size_t *length, int prot)
+{
+ struct stat st;
+
+ if (fstat(fd, &st) < 0)
+ return MAP_FAILED;
+
+#if OFF_T_MAX > SSIZE_T_MAX
+ if (st.st_size > SSIZE_T_MAX) {
+ /* too large file to map into memory */
+ errno = EFBIG;
+ return MAP_FAILED;
+ }
+#endif
+
+ *length = (size_t)st.st_size;
+ if (*length == 0)
+ return NULL;
+
+ i_assert(*length > 0 && *length < SSIZE_T_MAX);
+
+ return mmap(NULL, *length, prot, MAP_SHARED, fd, 0);
+}
+
+void *mmap_ro_file(int fd, size_t *length)
+{
+ return mmap_file(fd, length, PROT_READ);
+}
+
+void *mmap_rw_file(int fd, size_t *length)
+{
+ return mmap_file(fd, length, PROT_READ | PROT_WRITE);
+}
+
+#undef madvise
+int my_madvise(void *start ATTR_UNUSED, size_t length ATTR_UNUSED,
+ int advice ATTR_UNUSED)
+{
+#ifdef HAVE_MADVISE
+ /* Ignore ENOSYS errors, which happen if the kernel hasn't implemented
+ the syscall even if libc has. */
+ if (madvise(start, length, advice) < 0 && errno != ENOSYS)
+ return -1;
+#endif
+ return 0;
+}
+
+size_t mmap_get_page_size(void)
+{
+ static size_t size = 0;
+
+ if (size != 0)
+ return size;
+ size = getpagesize();
+ return size;
+}
diff --git a/src/lib/mmap-util.h b/src/lib/mmap-util.h
new file mode 100644
index 0000000..0f4184e
--- /dev/null
+++ b/src/lib/mmap-util.h
@@ -0,0 +1,42 @@
+#ifndef MMAP_UTIL_H
+#define MMAP_UTIL_H
+
+#include <unistd.h>
+
+#ifdef HAVE_LINUX_MREMAP
+# define __USE_GNU /* for MREMAP_MAYMOVE */
+#endif
+
+#include <sys/mman.h>
+#undef __USE_GNU
+
+#if !defined (MREMAP_MAYMOVE) && !defined (HAVE_LINUX_MREMAP)
+# define MREMAP_MAYMOVE 1
+#endif
+
+#define madvise my_madvise
+int my_madvise(void *start, size_t length, int advice);
+#ifndef HAVE_MADVISE
+# ifndef MADV_NORMAL
+# define MADV_NORMAL 0
+# define MADV_RANDOM 0
+# define MADV_SEQUENTIAL 0
+# define MADV_WILLNEED 0
+# define MADV_DONTNEED 0
+# endif
+#endif
+
+void *mmap_file(int fd, size_t *length, int prot);
+void *mmap_ro_file(int fd, size_t *length);
+void *mmap_rw_file(int fd, size_t *length);
+
+/* for allocating anonymous mmap()s, with portable mremap(). these must not
+ be mixed with any standard mmap calls. */
+void *mmap_anon(size_t length);
+void *mremap_anon(void *old_address, size_t old_size, size_t new_size,
+ unsigned long flags);
+int munmap_anon(void *start, size_t length);
+
+size_t mmap_get_page_size(void) ATTR_CONST;
+
+#endif
diff --git a/src/lib/module-context.h b/src/lib/module-context.h
new file mode 100644
index 0000000..3d2b1d6
--- /dev/null
+++ b/src/lib/module-context.h
@@ -0,0 +1,116 @@
+#ifndef MODULE_CONTEXT_H
+#define MODULE_CONTEXT_H
+
+#include "array.h"
+
+/*
+ This is a bit complex to use, but it prevents using wrong module IDs
+ in module_contexts arrays.
+
+ ---------
+ The main structure is implemented like this:
+
+ struct STRUCT_NAME_module_register {
+ unsigned int id;
+ };
+ union STRUCT_NAME_module_context {
+ struct STRUCT_NAME_module_register *reg;
+ // it's allowed to have some structure here so it won't waste space.
+ // for example: struct STRUCT_NAME_vfuncs super;
+ };
+ struct STRUCT_NAME {
+ ARRAY(union STRUCT_NAME_module_context *) module_contexts;
+ };
+ extern struct STRUCT_NAME_module_register STRUCT_NAME_module_register;
+
+ ---------
+ The usage in modules goes like:
+
+ static MODULE_CONTEXT_DEFINE(mymodule_STRUCT_NAME_module,
+ &STRUCT_NAME_module_register);
+ struct mymodule_STRUCT_NAME {
+ union STRUCT_NAME_module_context module_ctx;
+ // module-specific data
+ };
+
+ struct mymodule_STRUCT_NAME *ctx = i_new(...);
+ MODULE_CONTEXT_SET(obj, mymodule_STRUCT_NAME_module, ctx);
+
+ struct mymodule_STRUCT_NAME *ctx =
+ MODULE_CONTEXT(obj, mymodule_STRUCT_NAME_module);
+*/
+
+#define OBJ_REGISTER(obj) \
+ ((**(obj)->module_contexts.v)->reg)
+#define OBJ_REGISTER_COMPATIBLE(obj, id_ctx) \
+ COMPILE_ERROR_IF_TYPES_NOT_COMPATIBLE(OBJ_REGISTER(obj), (id_ctx).reg)
+
+#define MODULE_CONTEXT(obj, id_ctx) \
+ (module_get_context_id(&(id_ctx).id) < array_count(&(obj)->module_contexts) ? \
+ (*((void **)array_idx_modifiable(&(obj)->module_contexts, \
+ module_get_context_id(&(id_ctx).id)) + \
+ OBJ_REGISTER_COMPATIBLE(obj, id_ctx))) : NULL)
+
+/* Will crash if context is missing. This is mainly used to simplify code and
+ keep static analyzers happy. This syntax discards result of i_panic and
+ returns NULL instead to keep compilers happy. */
+#define MODULE_CONTEXT_REQUIRE(obj, id_ctx) \
+ (module_get_context_id(&(id_ctx).id) < array_count(&(obj)->module_contexts) ? \
+ (*((void **)array_idx_modifiable(&(obj)->module_contexts, \
+ module_get_context_id(&(id_ctx).id)) + \
+ OBJ_REGISTER_COMPATIBLE(obj, id_ctx))) : (i_panic("Module context " #id_ctx " missing"), NULL))
+
+#ifdef HAVE_TYPEOF
+# define MODULE_CONTEXT_DEFINE(_name, _reg) \
+ struct _name { \
+ struct module_context_id id; \
+ typeof(_reg) reg; \
+ } _name
+# define MODULE_CONTEXT_INIT(_reg) \
+ { { &(_reg)->id, 0, FALSE }, NULL }
+#else
+# define MODULE_CONTEXT_DEFINE(_name, _reg) \
+ struct _name { \
+ struct module_context_id id; \
+ } _name
+# define MODULE_CONTEXT_INIT(_reg) \
+ { { &(_reg)->id, 0, FALSE } }
+#endif
+
+#define MODULE_CONTEXT_DEFINE_INIT(_name, _reg) \
+ MODULE_CONTEXT_DEFINE(_name, _reg) = MODULE_CONTEXT_INIT(_reg)
+
+struct module_context_id {
+ unsigned int *module_id_register;
+ unsigned int module_id;
+ bool module_id_set;
+};
+
+static inline unsigned int module_get_context_id(struct module_context_id *id)
+{
+ if (!id->module_id_set) {
+ id->module_id = *id->module_id_register;
+ id->module_id_set = TRUE;
+ *id->module_id_register += 1;
+ }
+ return id->module_id;
+}
+
+#define MODULE_CONTEXT_SET_FULL(obj, id_ctx, ctx, module_ctx) STMT_START { \
+ (void)COMPILE_ERROR_IF_TYPES_NOT_COMPATIBLE(module_ctx, \
+ (**(obj)->module_contexts.v)); \
+ (void)OBJ_REGISTER_COMPATIBLE(obj, id_ctx); \
+ void *_module_tmp = ctx; \
+ array_idx_set_i(&(obj)->module_contexts.arr, \
+ module_get_context_id(&(id_ctx).id), &_module_tmp); \
+ } STMT_END
+
+#define MODULE_CONTEXT_SET(obj, id_ctx, context) \
+ MODULE_CONTEXT_SET_FULL(obj, id_ctx, context, &(context)->module_ctx)
+#define MODULE_CONTEXT_SET_SELF(obj, id_ctx, context) \
+ MODULE_CONTEXT_SET_FULL(obj, id_ctx, context, context)
+
+#define MODULE_CONTEXT_UNSET(obj, id_ctx) \
+ array_idx_clear(&(obj)->module_contexts, (id_ctx).id.module_id)
+
+#endif
diff --git a/src/lib/module-dir.c b/src/lib/module-dir.c
new file mode 100644
index 0000000..26fdeac
--- /dev/null
+++ b/src/lib/module-dir.c
@@ -0,0 +1,697 @@
+/* Copyright (c) 2003-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "str.h"
+#include "sort.h"
+#include "module-dir.h"
+
+#ifdef HAVE_MODULES
+
+#include <unistd.h>
+#include <fcntl.h>
+#include <dirent.h>
+#include <dlfcn.h>
+
+#ifndef RTLD_GLOBAL
+# define RTLD_GLOBAL 0
+#endif
+
+#ifndef RTLD_NOW
+# define RTLD_NOW 0
+#endif
+
+static const char *module_name_drop_suffix(const char *name);
+
+void *module_get_symbol_quiet(struct module *module, const char *symbol)
+{
+ /* clear out old errors */
+ (void)dlerror();
+
+ return dlsym(module->handle, symbol);
+}
+
+void *module_get_symbol(struct module *module, const char *symbol)
+{
+ const char *error;
+ void *ret;
+
+ ret = module_get_symbol_quiet(module, symbol);
+ if (ret == NULL) {
+ error = dlerror();
+ if (error != NULL) {
+ i_error("module %s: dlsym(%s) failed: %s",
+ module->path, symbol, error);
+ ret = NULL;
+ }
+ }
+ return ret;
+}
+
+static void *get_symbol(struct module *module, const char *symbol, bool quiet)
+{
+ if (quiet)
+ return module_get_symbol_quiet(module, symbol);
+
+ return module_get_symbol(module, symbol);
+}
+
+static void module_free(struct module *module)
+{
+ if (module->deinit != NULL && module->initialized)
+ module->deinit();
+ /* dlclose()ing removes all symbols from valgrind's visibility.
+ if GDB environment is set, don't actually unload the module
+ (the GDB environment is used elsewhere too) */
+ if (getenv("GDB") == NULL) {
+ if (dlclose(module->handle) != 0)
+ i_error("dlclose(%s) failed: %m", module->path);
+ }
+ i_free(module->path);
+ i_free(module->name);
+ i_free(module);
+}
+
+static bool
+module_check_wrong_binary_dependency(const struct module_dir_load_settings *set,
+ struct module *module, const char **error_r)
+{
+ const char *symbol_name, *binary_dep, *const *names;
+ string_t *errstr;
+
+ if (set->binary_name == NULL)
+ return TRUE;
+
+ symbol_name = t_strconcat(module->name, "_binary_dependency", NULL);
+ binary_dep = dlsym(module->handle, symbol_name);
+ if (binary_dep == NULL)
+ return TRUE;
+
+ names = t_strsplit(binary_dep, " ");
+ if (str_array_find(names, set->binary_name))
+ return TRUE;
+
+ errstr = t_str_new(128);
+ str_printfa(errstr, "Can't load plugin %s: "
+ "Plugin is intended to be used only by ", module->name);
+ if (names[1] == NULL)
+ str_printfa(errstr, "%s binary", binary_dep);
+ else
+ str_printfa(errstr, "binaries: %s", binary_dep);
+ str_printfa(errstr, " (we're %s)", set->binary_name);
+ *error_r = str_c(errstr);
+ return FALSE;
+}
+
+static bool
+module_check_missing_plugin_dependencies(const struct module_dir_load_settings *set,
+ struct module *module,
+ struct module *all_modules,
+ const char **error_r)
+{
+ const char **deps;
+ struct module *m;
+ string_t *errmsg;
+ size_t len;
+
+ deps = dlsym(module->handle,
+ t_strconcat(module->name, "_dependencies", NULL));
+ if (deps == NULL)
+ return TRUE;
+
+ for (; *deps != NULL; deps++) {
+ len = strlen(*deps);
+ for (m = all_modules; m != NULL; m = m->next) {
+ if (strncmp(m->name, *deps, len) == 0 &&
+ (m->name[len] == '\0' ||
+ strcmp(m->name+len, "_plugin") == 0))
+ break;
+ }
+ if (m == NULL) {
+ errmsg = t_str_new(128);
+ str_printfa(errmsg, "Plugin %s must be loaded also",
+ *deps);
+ if (set->setting_name != NULL) {
+ str_printfa(errmsg,
+ " (you must set: %s=$%s %s)",
+ set->setting_name,
+ set->setting_name, *deps);
+ }
+ *error_r = str_c(errmsg);
+ return FALSE;
+ }
+ }
+ return TRUE;
+}
+
+static void *quiet_dlopen(const char *path, int flags)
+{
+#ifndef __OpenBSD__
+ return dlopen(path, flags);
+#else
+ void *handle;
+ int fd;
+
+ /* OpenBSD likes to print all "undefined symbol" errors to stderr.
+ Hide them by sending them to /dev/null. */
+ fd = dup(STDERR_FILENO);
+ if (fd == -1)
+ i_fatal("dup() failed: %m");
+ if (dup2(dev_null_fd, STDERR_FILENO) < 0)
+ i_fatal("dup2() failed: %m");
+ handle = dlopen(path, flags);
+ if (dup2(fd, STDERR_FILENO) < 0)
+ i_fatal("dup2() failed: %m");
+ if (close(fd) < 0)
+ i_error("close() failed: %m");
+ return handle;
+#endif
+}
+
+static bool versions_equal(const char *str1, const char *str2)
+{
+ while (*str1 == *str2) {
+ if (*str1 == '\0' || *str1 == '(')
+ return TRUE;
+ str1++;
+ str2++;
+ }
+ return FALSE;
+}
+
+static int
+module_load(const char *path, const char *name,
+ const struct module_dir_load_settings *set,
+ struct module *all_modules,
+ struct module **module_r, const char **error_r)
+{
+ void *handle;
+ struct module *module;
+ const char *const *module_version;
+ void (*preinit)(void);
+
+ *module_r = NULL;
+ *error_r = NULL;
+
+ if (set->ignore_dlopen_errors) {
+ handle = quiet_dlopen(path, RTLD_GLOBAL | RTLD_NOW);
+ if (handle == NULL) {
+ if (set->debug) {
+ i_debug("Skipping module %s, "
+ "because dlopen() failed: %s "
+ "(this is usually intentional, "
+ "so just ignore this message)",
+ name, dlerror());
+ }
+ return 0;
+ }
+ } else {
+ handle = dlopen(path, RTLD_GLOBAL | RTLD_NOW);
+ if (handle == NULL) {
+ *error_r = t_strdup_printf("dlopen() failed: %s",
+ dlerror());
+#ifdef RTLD_LAZY
+ /* try to give a better error message by lazily loading
+ the plugin and checking its dependencies */
+ handle = dlopen(path, RTLD_LAZY);
+ if (handle == NULL)
+ return -1;
+#else
+ return -1;
+#endif
+ }
+ }
+
+ module = i_new(struct module, 1);
+ module->path = i_strdup(path);
+ module->name = i_strdup(name);
+ module->handle = handle;
+
+ module_version = set->abi_version == NULL ? NULL :
+ get_symbol(module, t_strconcat(name, "_version", NULL), TRUE);
+ if (module_version != NULL &&
+ !versions_equal(*module_version, set->abi_version)) {
+ *error_r = t_strdup_printf(
+ "Module is for different ABI version %s (we have %s)",
+ *module_version, set->abi_version);
+ module_free(module);
+ return -1;
+ }
+
+ /* get our init func */
+ module->init = (void (*)(struct module *))
+ get_symbol(module, t_strconcat(name, "_init", NULL),
+ !set->require_init_funcs);
+ module->deinit = (void (*)(void))
+ get_symbol(module, t_strconcat(name, "_deinit", NULL),
+ !set->require_init_funcs);
+ preinit = (void (*)(void))
+ get_symbol(module, t_strconcat(name, "_preinit", NULL),
+ TRUE);
+ if (preinit != NULL)
+ preinit();
+
+ if ((module->init == NULL || module->deinit == NULL) &&
+ set->require_init_funcs) {
+ *error_r = t_strdup_printf(
+ "Module doesn't have %s function",
+ module->init == NULL ? "init" : "deinit");
+ } else if (!module_check_wrong_binary_dependency(set, module, error_r)) {
+ /* failed */
+ } else if (!module_check_missing_plugin_dependencies(set, module,
+ all_modules, error_r)) {
+ /* failed */
+ }
+
+ if (*error_r != NULL) {
+ module->deinit = NULL;
+ module_free(module);
+ return -1;
+ }
+
+ if (set->debug)
+ i_debug("Module loaded: %s", path);
+ *module_r = module;
+ return 1;
+}
+
+static int module_name_cmp(const char *const *n1, const char *const *n2)
+{
+ const char *s1 = *n1, *s2 = *n2;
+
+ if (str_begins(s1, "lib"))
+ s1 += 3;
+ if (str_begins(s2, "lib"))
+ s2 += 3;
+
+ return strcmp(s1, s2);
+}
+
+static bool module_want_load(const struct module_dir_load_settings *set,
+ const char **names, const char *name)
+{
+ if (set->filter_callback != NULL) {
+ if (!set->filter_callback(name, set->filter_context))
+ return FALSE;
+ }
+ if (names == NULL)
+ return TRUE;
+
+ for (; *names != NULL; names++) {
+ if (strcmp(*names, name) == 0) {
+ *names = "";
+ return TRUE;
+ }
+ }
+ return FALSE;
+}
+
+static void check_duplicates(ARRAY_TYPE(const_string) *names,
+ const char *name, const char *dir)
+{
+ const char *const *names_p, *base_name, *tmp;
+ unsigned int i, count;
+
+ base_name = module_file_get_name(name);
+ names_p = array_get(names, &count);
+ for (i = 0; i < count; i++) T_BEGIN {
+ tmp = module_file_get_name(names_p[i]);
+
+ if (strcmp(tmp, base_name) == 0)
+ i_fatal("Multiple files for module %s: %s/%s, %s/%s",
+ base_name, dir, name, dir, names_p[i]);
+ } T_END;
+}
+
+struct module *module_dir_find(struct module *modules, const char *name)
+{
+ struct module *module;
+ size_t len = strlen(name);
+
+ for (module = modules; module != NULL; module = module->next) {
+ if (strncmp(module->name, name, len) == 0) {
+ if (module->name[len] == '\0' ||
+ strcmp(module->name + len, "_plugin") == 0)
+ return module;
+ }
+ }
+ return NULL;
+}
+
+static bool module_is_loaded(struct module *modules, const char *name)
+{
+ return module_dir_find(modules, name) != NULL;
+}
+
+static void module_names_fix(const char **module_names)
+{
+ unsigned int i, j;
+
+ if (module_names[0] == NULL)
+ return;
+
+ /* allow giving the module names also in non-base form.
+ convert them in here. */
+ for (i = 0; module_names[i] != NULL; i++)
+ module_names[i] = module_file_get_name(module_names[i]);
+
+ /* @UNSAFE: drop duplicates */
+ i_qsort(module_names, i, sizeof(*module_names), i_strcmp_p);
+ for (i = j = 1; module_names[i] != NULL; i++) {
+ if (strcmp(module_names[i-1], module_names[i]) != 0)
+ module_names[j++] = module_names[i];
+ }
+ module_names[j] = NULL;
+}
+
+static bool
+module_dir_is_all_loaded(struct module *old_modules, const char **module_names)
+{
+ unsigned int i;
+
+ for (i = 0; module_names[i] != NULL; i++) {
+ if (!module_is_loaded(old_modules, module_names[i]))
+ return FALSE;
+ }
+ return TRUE;
+}
+
+static int
+module_dir_load_real(struct module **_modules,
+ const char *dir, const char **module_names,
+ const struct module_dir_load_settings *set,
+ char **error_r)
+{
+ DIR *dirp;
+ struct dirent *d;
+ const char *name, *p, *error, *const *names_p;
+ struct module *modules, *module, **module_pos, *old_modules = *_modules;
+ unsigned int i, count;
+ ARRAY_TYPE(const_string) names;
+ pool_t pool;
+ int ret;
+
+ *error_r = NULL;
+
+ if (module_names != NULL) {
+ if (module_dir_is_all_loaded(old_modules, module_names))
+ return 0;
+ }
+
+ if (set->debug)
+ i_debug("Loading modules from directory: %s", dir);
+
+ dirp = opendir(dir);
+ if (dirp == NULL) {
+ *error_r = i_strdup_printf("opendir(%s) failed: %m", dir);
+ if (module_names != NULL) {
+ /* we were given a list of modules to load.
+ we can't fail. */
+ return -1;
+ }
+ return errno == ENOENT ? 0 : -1;
+ }
+
+ pool = pool_alloconly_create("module loader", 4096);
+ p_array_init(&names, pool, 32);
+
+ modules = NULL;
+ for (errno = 0; (d = readdir(dirp)) != NULL; errno = 0) {
+ name = d->d_name;
+
+ if (name[0] == '.')
+ continue;
+
+ p = strstr(name, MODULE_SUFFIX);
+ if (p == NULL || strlen(p) != 3)
+ continue;
+
+ T_BEGIN {
+ check_duplicates(&names, name, dir);
+ } T_END;
+
+ name = p_strdup(pool, d->d_name);
+ array_push_back(&names, &name);
+ }
+ if (errno != 0)
+ *error_r = i_strdup_printf("readdir(%s) failed: %m", dir);
+ if (closedir(dirp) < 0 && *error_r == NULL)
+ *error_r = i_strdup_printf("closedir(%s) failed: %m", dir);
+ if (*error_r != NULL) {
+ pool_unref(&pool);
+ return -1;
+ }
+
+ array_sort(&names, module_name_cmp);
+ names_p = array_get(&names, &count);
+
+ modules = old_modules;
+ module_pos = &modules;
+ while (*module_pos != NULL)
+ module_pos = &(*module_pos)->next;
+ for (i = 0; i < count; i++) T_BEGIN {
+ const char *path, *stripped_name, *suffixless_name;
+
+ name = names_p[i];
+ stripped_name = module_file_get_name(name);
+ suffixless_name = module_name_drop_suffix(stripped_name);
+ if (!module_want_load(set, module_names, suffixless_name) ||
+ module_is_loaded(old_modules, suffixless_name))
+ module = NULL;
+ else {
+ path = t_strconcat(dir, "/", name, NULL);
+ ret = module_load(path, stripped_name, set, modules, &module, &error);
+ if (ret >= 0)
+ ;
+ else if (module_names != NULL) {
+ *error_r = i_strdup_printf("Couldn't load required plugin %s: %s",
+ path, error);
+ i = count;
+ } else {
+ i_error("Couldn't load plugin %s: %s", path, error);
+ }
+ }
+
+ if (module != NULL) {
+ *module_pos = module;
+ module_pos = &module->next;
+ }
+ } T_END;
+ pool_unref(&pool);
+
+ if (module_names != NULL && *error_r == NULL && !set->ignore_missing) {
+ /* make sure all modules were found */
+ for (; *module_names != NULL; module_names++) {
+ if (**module_names != '\0') {
+ *error_r = i_strdup_printf("Plugin '%s' not found from directory %s",
+ *module_names, dir);
+ break;
+ }
+ }
+ }
+ *_modules = modules;
+ return *error_r != NULL ? -1 : 0;
+}
+
+int module_dir_try_load_missing(struct module **modules,
+ const char *dir, const char *module_names,
+ const struct module_dir_load_settings *set,
+ const char **error_r)
+{
+ char *error = NULL;
+ int ret;
+
+ T_BEGIN {
+ const char **arr = NULL;
+
+ if (module_names != NULL) {
+ arr = t_strsplit_spaces(module_names, ", ");
+ module_names_fix(arr);
+ }
+
+ ret = module_dir_load_real(modules, dir, arr, set, &error);
+ } T_END;
+ *error_r = t_strdup(error);
+ i_free(error);
+ return ret;
+}
+
+struct module *
+module_dir_load_missing(struct module *old_modules,
+ const char *dir, const char *module_names,
+ const struct module_dir_load_settings *set)
+{
+ struct module *new_modules = old_modules;
+ const char *error;
+
+ if (module_dir_try_load_missing(&new_modules, dir, module_names,
+ set, &error) < 0) {
+ if (module_names != NULL)
+ i_fatal("%s", error);
+ else
+ i_error("%s", error);
+ }
+ return new_modules;
+}
+
+void module_dir_init(struct module *modules)
+{
+ struct module *module;
+
+ for (module = modules; module != NULL; module = module->next) {
+ if (!module->initialized) {
+ module->initialized = TRUE;
+ if (module->init != NULL) T_BEGIN {
+ module->init(module);
+ } T_END;
+ }
+ }
+}
+
+void module_dir_deinit(struct module *modules)
+{
+ struct module *module, **rev;
+ unsigned int i, count = 0;
+
+ for (module = modules; module != NULL; module = module->next) {
+ if (module->deinit != NULL && module->initialized)
+ count++;
+ }
+
+ if (count == 0)
+ return;
+
+ /* @UNSAFE: deinitialize in reverse order */
+ T_BEGIN {
+ rev = t_new(struct module *, count);
+ for (i = 0, module = modules; i < count; ) {
+ if (module->deinit != NULL && module->initialized) {
+ rev[count-i-1] = module;
+ i++;
+ }
+ module = module->next;
+ }
+
+ for (i = 0; i < count; i++) {
+ module = rev[i];
+
+ T_BEGIN {
+ module->deinit();
+ } T_END;
+ module->initialized = FALSE;
+ }
+ } T_END;
+}
+
+void module_dir_unload(struct module **modules)
+{
+ struct module *module, *next;
+
+ /* Call all modules' deinit() first, so that they may still call each
+ others' functions. */
+ module_dir_deinit(*modules);
+
+ for (module = *modules; module != NULL; module = next) {
+ next = module->next;
+ module_free(module);
+ }
+
+ *modules = NULL;
+}
+
+#else
+
+#ifndef MODULE_SUFFIX
+# define MODULE_SUFFIX ".so" /* just to avoid build failure */
+#endif
+
+struct module *
+module_dir_load_missing(struct module *old_modules ATTR_UNUSED,
+ const char *dir ATTR_UNUSED,
+ const char *module_names,
+ const struct module_dir_load_settings *set ATTR_UNUSED)
+{
+#define NO_SUPPORT_ERRSTR "Dynamically loadable module support not built in"
+ if (module_names == NULL)
+ i_error(NO_SUPPORT_ERRSTR);
+ else {
+ i_fatal(NO_SUPPORT_ERRSTR", can't load plugins: %s",
+ module_names);
+ }
+ return NULL;
+}
+
+void module_dir_init(struct module *modules ATTR_UNUSED)
+{
+}
+
+void module_dir_deinit(struct module *modules ATTR_UNUSED)
+{
+}
+
+void module_dir_unload(struct module **modules ATTR_UNUSED)
+{
+}
+
+struct module *module_dir_find(struct module *modules ATTR_UNUSED,
+ const char *name ATTR_UNUSED)
+{
+ return NULL;
+}
+
+void *module_get_symbol(struct module *module ATTR_UNUSED,
+ const char *symbol ATTR_UNUSED)
+{
+ return NULL;
+}
+
+void *module_get_symbol_quiet(struct module *module ATTR_UNUSED,
+ const char *symbol ATTR_UNUSED)
+{
+ return NULL;
+}
+
+#endif
+
+struct module *module_dir_load(const char *dir, const char *module_names,
+ const struct module_dir_load_settings *set)
+{
+ return module_dir_load_missing(NULL, dir, module_names, set);
+}
+
+const char *module_file_get_name(const char *fname)
+{
+ const char *p;
+
+ /* [lib][nn_]name(.so) */
+ if (str_begins(fname, "lib"))
+ fname += 3;
+
+ for (p = fname; *p != '\0'; p++) {
+ if (*p < '0' || *p > '9')
+ break;
+ }
+ if (*p == '_')
+ fname = p + 1;
+
+ p = strstr(fname, MODULE_SUFFIX);
+ if (p == NULL)
+ return fname;
+
+ return t_strdup_until(fname, p);
+}
+
+static const char *module_name_drop_suffix(const char *name)
+{
+ size_t len;
+
+ len = strlen(name);
+ if (len > 7 && strcmp(name + len - 7, "_plugin") == 0)
+ name = t_strndup(name, len - 7);
+ return name;
+}
+
+const char *module_get_plugin_name(struct module *module)
+{
+ return module_name_drop_suffix(module->name);
+}
diff --git a/src/lib/module-dir.h b/src/lib/module-dir.h
new file mode 100644
index 0000000..f62e906
--- /dev/null
+++ b/src/lib/module-dir.h
@@ -0,0 +1,77 @@
+#ifndef MODULE_DIR_H
+#define MODULE_DIR_H
+
+struct module_dir_load_settings {
+ /* If abi_version is non-NULL and the module contains a version symbol,
+ fail the load if they're different. In both strings ignore anything
+ after the first '(' character, so the version can be e.g.:
+ 2.2.ABIv1(2.2.15) */
+ const char *abi_version;
+ /* Binary name used for checking if plugin is tried to be loaded for
+ wrong binary. */
+ const char *binary_name;
+ /* Setting name used in plugin dependency error message */
+ const char *setting_name;
+
+ /* If non-NULL, load only modules where filter_callback returns TRUE */
+ bool (*filter_callback)(const char *name, void *context);
+ void *filter_context;
+
+ /* Require all plugins to have <plugin_name>_init() function */
+ bool require_init_funcs:1;
+ /* Enable debug logging */
+ bool debug:1;
+ /* If dlopen() fails for some modules, silently skip it. */
+ bool ignore_dlopen_errors:1;
+ /* Don't fail if some specified modules weren't found */
+ bool ignore_missing:1;
+};
+
+struct module {
+ char *path, *name;
+
+ void *handle;
+ void (*init)(struct module *module);
+ void (*deinit)(void);
+
+ bool initialized:1;
+
+ struct module *next;
+};
+
+/* Load modules in given directory. module_names is a space separated list of
+ module names to load. */
+struct module *module_dir_load(const char *dir, const char *module_names,
+ const struct module_dir_load_settings *set)
+ ATTR_NULL(2);
+/* Load modules that aren't already loaded. */
+struct module *
+module_dir_load_missing(struct module *old_modules,
+ const char *dir, const char *module_names,
+ const struct module_dir_load_settings *set)
+ ATTR_NULL(1, 3);
+/* Load modules that aren't already loaded. */
+int module_dir_try_load_missing(struct module **modules,
+ const char *dir, const char *module_names,
+ const struct module_dir_load_settings *set,
+ const char **error_r)
+ ATTR_NULL(1, 3);
+/* Call init() in all modules */
+void module_dir_init(struct module *modules);
+/* Call deinit() in all modules and mark them NULL so module_dir_unload()
+ won't do it again. */
+void module_dir_deinit(struct module *modules);
+/* Unload all modules */
+void module_dir_unload(struct module **modules);
+/* Find a module by name. */
+struct module *module_dir_find(struct module *modules, const char *name);
+
+void *module_get_symbol(struct module *module, const char *symbol);
+void *module_get_symbol_quiet(struct module *module, const char *symbol);
+
+/* Returns module's base name from the filename. */
+const char *module_file_get_name(const char *fname);
+/* Returns module's name without "_plugin" suffix. */
+const char *module_get_plugin_name(struct module *module);
+
+#endif
diff --git a/src/lib/mountpoint.c b/src/lib/mountpoint.c
new file mode 100644
index 0000000..3d2150e
--- /dev/null
+++ b/src/lib/mountpoint.c
@@ -0,0 +1,336 @@
+/* Copyright (c) 2006-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "mountpoint.h"
+
+#include <sys/stat.h>
+
+#ifdef HAVE_SYS_VMOUNT_H
+# include <stdio.h>
+# include <sys/vmount.h> /* AIX */
+# define MOUNTPOINT_AIX_MNTCTL
+#elif defined(HAVE_STATVFS_MNTFROMNAME)
+# include <sys/statvfs.h> /* NetBSD 3.0+, FreeBSD 5.0+ */
+# define STATVFS_STR "statvfs"
+# define MOUNTPOINT_STATVFS
+#elif defined(HAVE_STATFS_MNTFROMNAME)
+# include <sys/param.h> /* Older BSDs */
+# include <sys/mount.h>
+# define statvfs statfs
+# define STATVFS_STR "statfs"
+# define MOUNTPOINT_STATVFS
+#elif defined(HAVE_MNTENT_H)
+# include <stdio.h>
+# include <mntent.h> /* Linux */
+# define MOUNTPOINT_LINUX
+#elif defined(HAVE_SYS_MNTTAB_H)
+# include <stdio.h>
+# include <sys/mnttab.h> /* Solaris */
+# include <sys/mntent.h>
+# define MOUNTPOINT_SOLARIS
+#else
+# define MOUNTPOINT_UNKNOWN
+#endif
+
+#ifdef MOUNTPOINT_SOLARIS
+# define MTAB_PATH MNTTAB /* Solaris */
+#else
+# define MTAB_PATH "/etc/mtab" /* Linux */
+#endif
+
+/* AIX doesn't have these defined */
+#ifndef MNTTYPE_SWAP
+# define MNTTYPE_SWAP "swap"
+#endif
+#ifndef MNTTYPE_IGNORE
+# define MNTTYPE_IGNORE "ignore"
+#endif
+#ifndef MNTTYPE_JFS
+# define MNTTYPE_JFS "jfs"
+#endif
+#ifndef MNTTYPE_NFS
+# define MNTTYPE_NFS "nfs"
+#endif
+
+/* Linux sometimes has mtab entry for "rootfs" as well as the real root
+ entry. Skip the rootfs. */
+#ifndef MNTTYPE_ROOTFS
+# define MNTTYPE_ROOTFS "rootfs"
+#endif
+
+#ifdef MOUNTPOINT_STATVFS
+static int
+mountpoint_get_statvfs(const char *path, pool_t pool,
+ struct mountpoint *point_r)
+{
+ struct statvfs buf;
+
+ i_zero(point_r);
+ if (statvfs(path, &buf) < 0) {
+ if (errno == ENOENT)
+ return 0;
+
+ i_error(STATVFS_STR"(%s) failed: %m", path);
+ return -1;
+ }
+
+ point_r->device_path = p_strdup(pool, buf.f_mntfromname);
+ point_r->mount_path = p_strdup(pool, buf.f_mntonname);
+#ifdef __osf__ /* Tru64 */
+ point_r->type = p_strdup(pool, getvfsbynumber(buf.f_type));
+#else
+ point_r->type = p_strdup(pool, buf.f_fstypename);
+#endif
+ point_r->block_size = buf.f_bsize;
+ return 1;
+}
+#endif
+
+int mountpoint_get(const char *path, pool_t pool, struct mountpoint *point_r)
+{
+#ifdef MOUNTPOINT_UNKNOWN
+ i_zero(point_r);
+ errno = ENOSYS;
+ return -1;
+#elif defined (MOUNTPOINT_STATVFS)
+ /* BSDs, Tru64 */
+ return mountpoint_get_statvfs(path, pool, point_r);
+#else
+ /* find via mount iteration */
+ struct mountpoint_iter *iter;
+ const struct mountpoint *mnt;
+ struct stat st;
+
+ i_zero(point_r);
+ if (stat(path, &st) < 0) {
+ if (errno == ENOENT)
+ return 0;
+
+ i_error("stat(%s) failed: %m", path);
+ return -1;
+ }
+
+ iter = mountpoint_iter_init();
+ while ((mnt = mountpoint_iter_next(iter)) != NULL) {
+ if (minor(st.st_dev) == minor(mnt->dev) &&
+ major(st.st_dev) == major(mnt->dev))
+ break;
+ }
+ if (mnt != NULL) {
+ point_r->device_path = p_strdup(pool, mnt->device_path);
+ point_r->mount_path = p_strdup(pool, mnt->mount_path);
+ point_r->type = p_strdup(pool, mnt->type);
+ point_r->dev = mnt->dev;
+ point_r->block_size = st.st_blksize;
+ }
+ if (mountpoint_iter_deinit(&iter) < 0 && mnt == NULL)
+ return -1;
+ return mnt != NULL ? 1 : 0;
+#endif
+}
+
+struct mountpoint_iter {
+#ifdef MOUNTPOINT_AIX_MNTCTL
+ char *mtab;
+ struct vmount *vmt;
+ int count;
+#elif defined(MOUNTPOINT_SOLARIS) || defined(MOUNTPOINT_LINUX)
+ FILE *f;
+#elif defined(HAVE_GETMNTINFO) /* BSDs */
+#ifndef __NetBSD__
+ struct statfs *fs;
+#else
+ struct statvfs *fs;
+#endif
+ int count;
+#endif
+ struct mountpoint mnt;
+ bool failed;
+};
+
+struct mountpoint_iter *mountpoint_iter_init(void)
+{
+ struct mountpoint_iter *iter = i_new(struct mountpoint_iter, 1);
+#ifdef MOUNTPOINT_AIX_MNTCTL
+ unsigned int size = STATIC_MTAB_SIZE;
+ char *mtab;
+ int count;
+
+ mtab = t_buffer_get(size);
+ while ((count = mntctl(MCTL_QUERY, size, mtab)) == 0) {
+ size = *(unsigned int *)mtab;
+ mtab = t_buffer_get(size);
+ }
+ if (count < 0) {
+ i_error("mntctl(MCTL_QUERY) failed: %m");
+ iter->failed = TRUE;
+ return iter;
+ }
+ iter->count = count;
+ iter->mtab = i_malloc(size);
+ memcpy(iter->mtab, mtab, size);
+ iter->vmt = (void *)iter->mtab;
+#elif defined(MOUNTPOINT_SOLARIS)
+ iter->f = fopen(MTAB_PATH, "r");
+ if (iter->f == NULL) {
+ i_error("fopen(%s) failed: %m", MTAB_PATH);
+ iter->failed = TRUE;
+ return iter;
+ }
+ resetmnttab(iter->f);
+#elif defined(MOUNTPOINT_LINUX)
+ iter->f = setmntent(MTAB_PATH, "r");
+ if (iter->f == NULL) {
+ i_error("setmntent(%s) failed: %m", MTAB_PATH);
+ iter->failed = TRUE;
+ }
+#elif defined(HAVE_GETMNTINFO) /* BSDs */
+ iter->count = getmntinfo(&iter->fs, MNT_NOWAIT);
+ if (iter->count < 0) {
+ i_error("getmntinfo() failed: %m");
+ iter->failed = TRUE;
+ }
+#else
+ iter->failed = TRUE;
+#endif
+ return iter;
+}
+
+const struct mountpoint *mountpoint_iter_next(struct mountpoint_iter *iter)
+{
+#ifdef MOUNTPOINT_AIX_MNTCTL
+ struct vmount *vmt = iter->vmt;
+ char *vmt_base = (char *)vmt;
+ char *vmt_object, *vmt_stub, *vmt_hostname;
+ struct stat vst;
+
+ if (iter->count == 0)
+ return NULL;
+ iter->count--;
+
+ iter->vmt = PTR_OFFSET(vmt, vmt->vmt_length);
+ vmt_hostname = vmt_base + vmt->vmt_data[VMT_HOSTNAME].vmt_off;
+ vmt_object = vmt_base + vmt->vmt_data[VMT_OBJECT].vmt_off;
+ vmt_stub = vmt_base + vmt->vmt_data[VMT_STUB].vmt_off;
+
+ i_zero(&iter->mnt);
+ switch (vmt->vmt_gfstype) {
+ case MNT_NFS:
+ case MNT_NFS3:
+ case MNT_NFS4:
+ case MNT_RFS4:
+ iter->mnt.device_path =
+ t_strconcat(vmt_hostname, ":", vmt_object, NULL);
+ iter->mnt.mount_path = vmt_stub;
+ iter->mnt.type = MNTTYPE_NFS;
+ break;
+
+ case MNT_J2:
+ case MNT_JFS:
+ iter->mnt.device_path = vmt_object;
+ iter->mnt.mount_path = vmt_stub;
+ iter->mnt.type = MNTTYPE_JFS;
+ break;
+ default:
+ /* unwanted filesystem */
+ return mountpoint_iter_next(iter);
+ }
+ if (stat(iter->mnt.mount_path, &vst) == 0) {
+ iter->mnt.dev = vst.st_dev;
+ iter->mnt.block_size = vst.st_blksize;
+ }
+ return &iter->mnt;
+#elif defined (MOUNTPOINT_SOLARIS)
+ union {
+ struct mnttab ent;
+ struct extmnttab ext;
+ } ent;
+
+ if (iter->f == NULL)
+ return NULL;
+
+ i_zero(&iter->mnt);
+ while ((getextmntent(iter->f, &ent.ext, sizeof(ent.ext))) == 0) {
+ if (hasmntopt(&ent.ent, MNTOPT_IGNORE) != NULL)
+ continue;
+
+ /* mnt_type contains tmpfs with swap */
+ if (strcmp(ent.ent.mnt_special, MNTTYPE_SWAP) == 0)
+ continue;
+
+ iter->mnt.device_path = ent.ent.mnt_special;
+ iter->mnt.mount_path = ent.ent.mnt_mountp;
+ iter->mnt.type = ent.ent.mnt_fstype;
+ iter->mnt.dev = makedev(ent.ext.mnt_major, ent.ext.mnt_minor);
+ return &iter->mnt;
+ }
+ return NULL;
+#elif defined (MOUNTPOINT_LINUX)
+ const struct mntent *ent;
+ struct stat st;
+
+ if (iter->f == NULL)
+ return NULL;
+
+ i_zero(&iter->mnt);
+ while ((ent = getmntent(iter->f)) != NULL) {
+ if (strcmp(ent->mnt_type, MNTTYPE_SWAP) == 0 ||
+ strcmp(ent->mnt_type, MNTTYPE_IGNORE) == 0 ||
+ strcmp(ent->mnt_type, MNTTYPE_ROOTFS) == 0)
+ continue;
+
+ iter->mnt.device_path = ent->mnt_fsname;
+ iter->mnt.mount_path = ent->mnt_dir;
+ iter->mnt.type = ent->mnt_type;
+ if (stat(ent->mnt_dir, &st) == 0) {
+ iter->mnt.dev = st.st_dev;
+ iter->mnt.block_size = st.st_blksize;
+ }
+ return &iter->mnt;
+ }
+ return NULL;
+#elif defined(HAVE_GETMNTINFO) /* BSDs */
+ while (iter->count > 0) {
+#ifndef __NetBSD__
+ struct statfs *fs = iter->fs;
+#else
+ struct statvfs *fs = iter->fs;
+#endif
+
+ iter->fs++;
+ iter->count--;
+
+ iter->mnt.device_path = fs->f_mntfromname;
+ iter->mnt.mount_path = fs->f_mntonname;
+#ifdef __osf__ /* Tru64 */
+ iter->mnt.type = getvfsbynumber(fs->f_type);
+#else
+ iter->mnt.type = fs->f_fstypename;
+#endif
+ iter->mnt.block_size = fs->f_bsize;
+ return &iter->mnt;
+ }
+ return NULL;
+#else
+ return NULL;
+#endif
+}
+
+int mountpoint_iter_deinit(struct mountpoint_iter **_iter)
+{
+ struct mountpoint_iter *iter = *_iter;
+ int ret = iter->failed ? -1 : 0;
+
+ *_iter = NULL;
+#ifdef MOUNTPOINT_AIX_MNTCTL
+ i_free(iter->mtab);
+#elif defined (MOUNTPOINT_SOLARIS)
+ if (iter->f != NULL)
+ fclose(iter->f);
+#elif defined (MOUNTPOINT_LINUX)
+ if (iter->f != NULL)
+ endmntent(iter->f);
+#endif
+ i_free(iter);
+ return ret;
+}
diff --git a/src/lib/mountpoint.h b/src/lib/mountpoint.h
new file mode 100644
index 0000000..728085d
--- /dev/null
+++ b/src/lib/mountpoint.h
@@ -0,0 +1,23 @@
+#ifndef MOUNTPOINT_H
+#define MOUNTPOINT_H
+
+struct mountpoint {
+ char *device_path;
+ char *mount_path;
+ char *type;
+ dev_t dev;
+ unsigned int block_size; /* may not be set for iteration */
+};
+
+/* Returns 1 = found, 0 = not found (from mount tabs, or the path itself),
+ -1 = error */
+int mountpoint_get(const char *path, pool_t pool, struct mountpoint *point_r);
+
+/* Iterate through mountpoints */
+struct mountpoint_iter *mountpoint_iter_init(void);
+/* Returns the next mountpoint or NULL if there are no more. */
+const struct mountpoint *mountpoint_iter_next(struct mountpoint_iter *iter);
+/* Returns 0 if mountpoints were iterated successfully, -1 if it failed. */
+int mountpoint_iter_deinit(struct mountpoint_iter **iter);
+
+#endif
diff --git a/src/lib/net.c b/src/lib/net.c
new file mode 100644
index 0000000..fe69faf
--- /dev/null
+++ b/src/lib/net.c
@@ -0,0 +1,1237 @@
+/* Copyright (c) 1999-2018 Dovecot authors, see the included COPYING file */
+
+#define _GNU_SOURCE /* For Linux's struct ucred */
+#include "lib.h"
+#include "time-util.h"
+#include "net.h"
+
+#include <unistd.h>
+#include <fcntl.h>
+#include <ctype.h>
+#include <sys/un.h>
+#include <netinet/tcp.h>
+#if defined(HAVE_UCRED_H)
+# include <ucred.h> /* for getpeerucred() */
+#elif defined(HAVE_SYS_UCRED_H)
+# include <sys/ucred.h> /* for FreeBSD struct xucred */
+#endif
+
+union sockaddr_union {
+ struct sockaddr sa;
+ struct sockaddr_in sin;
+ struct sockaddr_in6 sin6;
+};
+
+union sockaddr_union_unix {
+ struct sockaddr sa;
+ struct sockaddr_un un;
+};
+
+#define SIZEOF_SOCKADDR(so) ((so).sa.sa_family == AF_INET6 ? \
+ sizeof(so.sin6) : sizeof(so.sin))
+
+#if !defined(HAVE_GETPEEREID) && !defined(SO_PEERCRED) && !defined(HAVE_GETPEERUCRED) && defined(MSG_WAITALL) && defined(LOCAL_CREDS)
+# define NEEDS_LOCAL_CREDS 1
+#else
+# undef NEEDS_LOCAL_CREDS
+#endif
+
+/* If connect() fails with EADDRNOTAVAIL (or some others on FreeBSD), retry it
+ this many times.
+
+ This is needed on busy systems kernel may assign the same source port to two
+ sockets at bind() stage, which is what we generally want to allow more than
+ 64k outgoing connections to different destinations. However, at bind() stage
+ the kernel doesn't know the destination yet. So it's possible that it
+ assigns the same source port to two (or more) sockets that have the same
+ destination IP+port as well. In this case connect() will fail with
+ EADDRNOTAVAIL. We'll need to retry this and hope that the next attempt won't
+ conflict. */
+#define MAX_CONNECT_RETRIES 20
+
+bool net_ip_compare(const struct ip_addr *ip1, const struct ip_addr *ip2)
+{
+ return net_ip_cmp(ip1, ip2) == 0;
+}
+
+int net_ip_cmp(const struct ip_addr *ip1, const struct ip_addr *ip2)
+{
+ if (ip1->family != ip2->family)
+ return ip1->family - ip2->family;
+
+ switch (ip1->family) {
+ case AF_INET6:
+ return memcmp(&ip1->u.ip6, &ip2->u.ip6, sizeof(ip1->u.ip6));
+ case AF_INET:
+ return memcmp(&ip1->u.ip4, &ip2->u.ip4, sizeof(ip1->u.ip4));
+ default:
+ break;
+ }
+ return 0;
+}
+
+unsigned int net_ip_hash(const struct ip_addr *ip)
+{
+ const unsigned char *p;
+ unsigned int len, g, h = 0;
+
+ if (ip->family == AF_INET6) {
+ p = ip->u.ip6.s6_addr;
+ len = sizeof(ip->u.ip6);
+ } else
+ {
+ return ip->u.ip4.s_addr;
+ }
+
+ for (; len > 0; len--, p++) {
+ h = (h << 4) + *p;
+ if ((g = h & 0xf0000000UL) != 0) {
+ h = h ^ (g >> 24);
+ h = h ^ g;
+ }
+ }
+
+ return h;
+}
+
+/* copy IP to sockaddr */
+static inline void
+sin_set_ip(union sockaddr_union *so, const struct ip_addr *ip)
+{
+ if (ip == NULL) {
+ so->sin6.sin6_family = AF_INET6;
+ so->sin6.sin6_addr = in6addr_any;
+ return;
+ }
+
+ so->sin.sin_family = ip->family;
+ if (ip->family == AF_INET6)
+ memcpy(&so->sin6.sin6_addr, &ip->u.ip6, sizeof(ip->u.ip6));
+ else
+ memcpy(&so->sin.sin_addr, &ip->u.ip4, sizeof(ip->u.ip4));
+}
+
+static inline void
+sin_get_ip(const union sockaddr_union *so, struct ip_addr *ip)
+{
+ /* IP structs may be sent across processes. Clear the whole struct
+ first to make sure it won't leak any data across processes. */
+ i_zero(ip);
+
+ ip->family = so->sin.sin_family;
+
+ if (ip->family == AF_INET6)
+ memcpy(&ip->u.ip6, &so->sin6.sin6_addr, sizeof(ip->u.ip6));
+ else
+ if (ip->family == AF_INET)
+ memcpy(&ip->u.ip4, &so->sin.sin_addr, sizeof(ip->u.ip4));
+ else
+ i_zero(&ip->u);
+}
+
+static inline void sin_set_port(union sockaddr_union *so, in_port_t port)
+{
+ if (so->sin.sin_family == AF_INET6)
+ so->sin6.sin6_port = htons(port);
+ else
+ so->sin.sin_port = htons(port);
+}
+
+static inline in_port_t sin_get_port(union sockaddr_union *so)
+{
+ if (so->sin.sin_family == AF_INET6)
+ return ntohs(so->sin6.sin6_port);
+ if (so->sin.sin_family == AF_INET)
+ return ntohs(so->sin.sin_port);
+
+ return 0;
+}
+
+static int net_connect_ip_once(const struct ip_addr *ip, in_port_t port,
+ const struct ip_addr *my_ip, int sock_type, bool blocking)
+{
+ union sockaddr_union so;
+ int fd, ret, opt = 1;
+
+ if (my_ip != NULL && ip->family != my_ip->family) {
+ i_warning("net_connect_ip(): ip->family != my_ip->family");
+ my_ip = NULL;
+ }
+
+ /* create the socket */
+ i_zero(&so);
+ so.sin.sin_family = ip->family;
+ fd = socket(ip->family, sock_type, 0);
+
+ if (fd == -1) {
+ i_error("socket() failed: %m");
+ return -1;
+ }
+
+ /* set socket options */
+ (void)setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
+ if (sock_type == SOCK_STREAM)
+ (void)setsockopt(fd, SOL_SOCKET, SO_KEEPALIVE, &opt, sizeof(opt));
+ if (!blocking)
+ net_set_nonblock(fd, TRUE);
+
+ /* set our own address */
+ if (my_ip != NULL) {
+ sin_set_ip(&so, my_ip);
+ if (bind(fd, &so.sa, SIZEOF_SOCKADDR(so)) == -1) {
+ i_error("bind(%s) failed: %m", net_ip2addr(my_ip));
+ i_close_fd(&fd);
+ return -1;
+ }
+ }
+
+ /* connect */
+ sin_set_ip(&so, ip);
+ sin_set_port(&so, port);
+ ret = connect(fd, &so.sa, SIZEOF_SOCKADDR(so));
+
+#ifndef WIN32
+ if (ret < 0 && errno != EINPROGRESS)
+#else
+ if (ret < 0 && WSAGetLastError() != WSAEWOULDBLOCK)
+#endif
+ {
+ i_close_fd(&fd);
+ return -1;
+ }
+
+ return fd;
+}
+
+static int net_connect_ip_full(const struct ip_addr *ip, in_port_t port,
+ const struct ip_addr *my_ip, int sock_type,
+ bool blocking)
+{
+ int fd, try;
+
+ for (try = 0;;) {
+ fd = net_connect_ip_once(ip, port, my_ip, sock_type, blocking);
+ if (fd != -1 || try++ >= MAX_CONNECT_RETRIES ||
+ (errno != EADDRNOTAVAIL
+#ifdef __FreeBSD__
+ /* busy */
+ && errno != EADDRINUSE
+ /* pf may cause this if another connection used
+ the same port recently */
+ && errno != EACCES
+#endif
+ ))
+ break;
+ }
+ return fd;
+}
+
+int net_connect_ip(const struct ip_addr *ip, in_port_t port,
+ const struct ip_addr *my_ip)
+{
+ return net_connect_ip_full(ip, port, my_ip, SOCK_STREAM, FALSE);
+}
+
+int net_connect_ip_blocking(const struct ip_addr *ip, in_port_t port,
+ const struct ip_addr *my_ip)
+{
+ return net_connect_ip_full(ip, port, my_ip, SOCK_STREAM, TRUE);
+}
+
+int net_connect_udp(const struct ip_addr *ip, in_port_t port,
+ const struct ip_addr *my_ip)
+{
+ return net_connect_ip_full(ip, port, my_ip, SOCK_DGRAM, FALSE);
+}
+
+int net_try_bind(const struct ip_addr *ip)
+{
+ union sockaddr_union so;
+ int fd;
+
+ /* create the socket */
+ i_zero(&so);
+ so.sin.sin_family = ip->family;
+ fd = socket(ip->family, SOCK_STREAM, 0);
+ if (fd == -1) {
+ i_error("socket() failed: %m");
+ return -1;
+ }
+
+ sin_set_ip(&so, ip);
+ if (bind(fd, &so.sa, SIZEOF_SOCKADDR(so)) == -1) {
+ i_close_fd(&fd);
+ return -1;
+ }
+ i_close_fd(&fd);
+ return 0;
+}
+
+int net_connect_unix(const char *path)
+{
+ union sockaddr_union_unix sa;
+ int fd, ret;
+
+ i_zero(&sa);
+ sa.un.sun_family = AF_UNIX;
+ if (i_strocpy(sa.un.sun_path, path, sizeof(sa.un.sun_path)) < 0) {
+ /* too long path */
+#ifdef ENAMETOOLONG
+ errno = ENAMETOOLONG;
+#else
+ errno = EOVERFLOW;
+#endif
+ return -1;
+ }
+
+ /* create the socket */
+ fd = socket(PF_UNIX, SOCK_STREAM, 0);
+ if (fd == -1) {
+ i_error("socket(%s) failed: %m", path);
+ return -1;
+ }
+
+ net_set_nonblock(fd, TRUE);
+
+ /* connect */
+ ret = connect(fd, &sa.sa, sizeof(sa));
+ if (ret < 0 && errno != EINPROGRESS) {
+ i_close_fd(&fd);
+ return -1;
+ }
+
+#ifdef NEEDS_LOCAL_CREDS
+ {
+ int on = 1;
+ if (setsockopt(fd, 0, LOCAL_CREDS, &on, sizeof on)) {
+ i_error("setsockopt(LOCAL_CREDS) failed: %m");
+ return -1;
+ }
+ }
+#endif
+
+ return fd;
+}
+
+int net_connect_unix_with_retries(const char *path, unsigned int msecs)
+{
+ struct timeval start, now;
+ int fd;
+
+ i_gettimeofday(&start);
+
+ do {
+ fd = net_connect_unix(path);
+ if (fd != -1 || (errno != EAGAIN && errno != ECONNREFUSED))
+ break;
+
+ /* busy. wait for a while. */
+ usleep(i_rand_minmax(1, 10) * 10000);
+ i_gettimeofday(&now);
+ } while (timeval_diff_msecs(&now, &start) < (int)msecs);
+ return fd;
+}
+
+void net_disconnect(int fd)
+{
+ /* FreeBSD's close() fails with ECONNRESET if socket still has unsent
+ data in transmit buffer. We don't care. */
+ if (close(fd) < 0 && errno != ECONNRESET)
+ i_error("net_disconnect() failed: %m");
+}
+
+void net_set_nonblock(int fd, bool nonblock)
+{
+ fd_set_nonblock(fd, nonblock);
+}
+
+int net_set_cork(int fd ATTR_UNUSED, bool cork ATTR_UNUSED)
+{
+#ifdef TCP_CORK
+ int val = cork;
+
+ return setsockopt(fd, IPPROTO_TCP, TCP_CORK, &val, sizeof(val));
+#else
+ errno = ENOPROTOOPT;
+ return -1;
+#endif
+}
+
+int net_set_tcp_nodelay(int fd, bool nodelay)
+{
+ int val = nodelay;
+
+ return setsockopt(fd, IPPROTO_TCP, TCP_NODELAY, &val, sizeof(val));
+}
+
+int net_set_tcp_quickack(int fd ATTR_UNUSED, bool quickack ATTR_UNUSED)
+{
+#ifdef TCP_QUICKACK
+ int val = quickack;
+
+ return setsockopt(fd, IPPROTO_TCP, TCP_QUICKACK, &val, sizeof(val));
+#else
+ errno = ENOPROTOOPT;
+ return -1;
+#endif
+}
+
+int net_set_send_buffer_size(int fd, size_t size)
+{
+ int opt;
+
+ if (size > INT_MAX) {
+ errno = EINVAL;
+ return -1;
+ }
+ opt = (int)size;
+ return setsockopt(fd, SOL_SOCKET, SO_SNDBUF, &opt, sizeof(opt));
+}
+
+int net_set_recv_buffer_size(int fd, size_t size)
+{
+ int opt;
+
+ if (size > INT_MAX) {
+ errno = EINVAL;
+ return -1;
+ }
+ opt = (int)size;
+ return setsockopt(fd, SOL_SOCKET, SO_RCVBUF, &opt, sizeof(opt));
+}
+
+const struct ip_addr net_ip4_any = {
+ .family = AF_INET,
+ .u.ip4.s_addr = INADDR_ANY
+};
+
+const struct ip_addr net_ip6_any = {
+ .family = AF_INET6,
+ .u.ip6 = IN6ADDR_ANY_INIT
+};
+
+const struct ip_addr net_ip4_loopback = {
+ .family = AF_INET,
+ .u.ip4.s_addr = INADDR_LOOPBACK
+};
+
+const struct ip_addr net_ip6_loopback = {
+ .family = AF_INET6,
+ .u.ip6 = IN6ADDR_LOOPBACK_INIT
+};
+
+int net_listen(const struct ip_addr *my_ip, in_port_t *port, int backlog)
+{
+ enum net_listen_flags flags = 0;
+
+ return net_listen_full(my_ip, port, &flags, backlog);
+}
+
+int net_listen_full(const struct ip_addr *my_ip, in_port_t *port,
+ enum net_listen_flags *flags, int backlog)
+{
+ union sockaddr_union so;
+ int ret, fd, opt = 1;
+ socklen_t len;
+
+ i_zero(&so);
+ sin_set_port(&so, *port);
+ sin_set_ip(&so, my_ip);
+
+ /* create the socket */
+ fd = socket(so.sin.sin_family, SOCK_STREAM, 0);
+ if (fd == -1 && my_ip == NULL &&
+ (errno == EINVAL || errno == EAFNOSUPPORT)) {
+ /* IPv6 is not supported by OS */
+ so.sin.sin_family = AF_INET;
+ so.sin.sin_addr.s_addr = INADDR_ANY;
+
+ fd = socket(AF_INET, SOCK_STREAM, 0);
+ }
+ if (fd == -1) {
+ i_error("socket() failed: %m");
+ return -1;
+ }
+
+ /* set socket options */
+ (void)setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
+ (void)setsockopt(fd, SOL_SOCKET, SO_KEEPALIVE, &opt, sizeof(opt));
+
+ if ((*flags & NET_LISTEN_FLAG_REUSEPORT) != 0) {
+#ifdef SO_REUSEPORT
+ if (setsockopt(fd, SOL_SOCKET, SO_REUSEPORT,
+ &opt, sizeof(opt)) < 0)
+#endif
+ *flags &= ENUM_NEGATE(NET_LISTEN_FLAG_REUSEPORT);
+ }
+
+ /* If using IPv6, bind only to IPv6 if possible. This avoids
+ ambiguities with IPv4-mapped IPv6 addresses. */
+#ifdef IPV6_V6ONLY
+ if (so.sin.sin_family == AF_INET6) {
+ opt = 1;
+ (void)setsockopt(fd, IPPROTO_IPV6, IPV6_V6ONLY, &opt, sizeof(opt));
+ }
+#endif
+ /* specify the address/port we want to listen in */
+ ret = bind(fd, &so.sa, SIZEOF_SOCKADDR(so));
+ if (ret < 0) {
+ if (errno != EADDRINUSE) {
+ i_error("bind(%s, %u) failed: %m",
+ my_ip == NULL ? "" : net_ip2addr(my_ip), *port);
+ }
+ } else {
+ /* get the actual port we started listen */
+ len = SIZEOF_SOCKADDR(so);
+ ret = getsockname(fd, &so.sa, &len);
+ if (ret >= 0) {
+ *port = sin_get_port(&so);
+
+ /* start listening */
+ if (listen(fd, backlog) >= 0)
+ return fd;
+
+ if (errno != EADDRINUSE)
+ i_error("listen() failed: %m");
+ }
+ }
+
+ /* error */
+ i_close_fd(&fd);
+ return -1;
+}
+
+int net_listen_unix(const char *path, int backlog)
+{
+ union {
+ struct sockaddr sa;
+ struct sockaddr_un un;
+ } sa;
+ int fd;
+
+ i_zero(&sa);
+ sa.un.sun_family = AF_UNIX;
+ if (i_strocpy(sa.un.sun_path, path, sizeof(sa.un.sun_path)) < 0) {
+ /* too long path */
+#ifdef ENAMETOOLONG
+ errno = ENAMETOOLONG;
+#else
+ errno = EOVERFLOW;
+#endif
+ return -1;
+ }
+
+ /* create the socket */
+ fd = socket(PF_UNIX, SOCK_STREAM, 0);
+ if (fd == -1) {
+ i_error("socket() failed: %m");
+ return -1;
+ }
+
+#ifdef NEEDS_LOCAL_CREDS
+ {
+ int on = 1;
+ if (setsockopt(fd, 0, LOCAL_CREDS, &on, sizeof on)) {
+ i_error("setsockopt(LOCAL_CREDS) failed: %m");
+ return -1;
+ }
+ }
+#endif
+
+ /* bind */
+ if (bind(fd, &sa.sa, sizeof(sa)) < 0) {
+ if (errno != EADDRINUSE)
+ i_error("bind(%s) failed: %m", path);
+ } else {
+ /* start listening */
+ if (listen(fd, backlog) == 0)
+ return fd;
+
+ if (errno != EADDRINUSE)
+ i_error("listen() failed: %m");
+ }
+
+ i_close_fd(&fd);
+ return -1;
+}
+
+int net_listen_unix_unlink_stale(const char *path, int backlog)
+{
+ unsigned int i = 0;
+ int fd;
+
+ while ((fd = net_listen_unix(path, backlog)) == -1) {
+ if (errno != EADDRINUSE || ++i == 2)
+ return -1;
+
+ /* see if it really exists */
+ fd = net_connect_unix(path);
+ if (fd != -1 || errno != ECONNREFUSED) {
+ i_close_fd(&fd);
+ errno = EADDRINUSE;
+ return -1;
+ }
+
+ /* delete and try again */
+ if (i_unlink_if_exists(path) < 0) {
+ errno = EADDRINUSE;
+ return -1;
+ }
+ }
+ return fd;
+}
+
+int net_accept(int fd, struct ip_addr *addr_r, in_port_t *port_r)
+{
+ union sockaddr_union so;
+ int ret;
+ socklen_t addrlen;
+
+ i_assert(fd >= 0);
+
+ i_zero(&so);
+ addrlen = sizeof(so);
+ ret = accept(fd, &so.sa, &addrlen);
+
+ if (ret < 0) {
+ if (errno == EAGAIN || errno == ECONNABORTED)
+ return -1;
+ else
+ return -2;
+ }
+ if (so.sin.sin_family == AF_UNIX) {
+ if (addr_r != NULL)
+ i_zero(addr_r);
+ if (port_r != NULL) *port_r = 0;
+ } else {
+ if (addr_r != NULL) sin_get_ip(&so, addr_r);
+ if (port_r != NULL) *port_r = sin_get_port(&so);
+ }
+ return ret;
+}
+
+ssize_t net_receive(int fd, void *buf, size_t len)
+{
+ ssize_t ret;
+
+ i_assert(fd >= 0);
+ i_assert(len <= SSIZE_T_MAX);
+
+ ret = read(fd, buf, len);
+ if (ret == 0) {
+ /* disconnected */
+ errno = 0;
+ return -2;
+ }
+
+ if (unlikely(ret < 0)) {
+ if (errno == EINTR || errno == EAGAIN)
+ return 0;
+
+ if (errno == ECONNRESET || errno == ETIMEDOUT) {
+ /* treat as disconnection */
+ return -2;
+ }
+ }
+
+ return ret;
+}
+
+int net_gethostbyname(const char *addr, struct ip_addr **ips,
+ unsigned int *ips_count)
+{
+ /* @UNSAFE */
+ union sockaddr_union *so;
+ struct addrinfo hints, *ai, *origai;
+ struct ip_addr ip;
+ int host_error;
+ int count;
+
+ *ips = NULL;
+ *ips_count = 0;
+
+ /* support [ipv6] style addresses here so they work globally */
+ if (addr[0] == '[' && net_addr2ip(addr, &ip) == 0) {
+ *ips_count = 1;
+ *ips = t_new(struct ip_addr, 1);
+ **ips = ip;
+ return 0;
+ }
+
+ i_zero(&hints);
+ hints.ai_socktype = SOCK_STREAM;
+
+ /* save error to host_error for later use */
+ host_error = getaddrinfo(addr, NULL, &hints, &ai);
+ if (host_error != 0)
+ return host_error;
+
+ /* get number of IPs */
+ origai = ai;
+ for (count = 0; ai != NULL; ai = ai->ai_next)
+ count++;
+
+ *ips_count = count;
+ *ips = t_new(struct ip_addr, count);
+
+ count = 0;
+ for (ai = origai; ai != NULL; ai = ai->ai_next, count++) {
+ so = (union sockaddr_union *) ai->ai_addr;
+
+ sin_get_ip(so, &(*ips)[count]);
+ }
+ freeaddrinfo(origai);
+
+ return 0;
+}
+
+int net_gethostbyaddr(const struct ip_addr *ip, const char **name_r)
+{
+ union sockaddr_union so;
+ socklen_t addrlen = sizeof(so);
+ char hbuf[NI_MAXHOST];
+ int ret;
+
+ i_zero(&so);
+ sin_set_ip(&so, ip);
+ ret = getnameinfo(&so.sa, addrlen, hbuf, sizeof(hbuf), NULL, 0,
+ NI_NAMEREQD);
+ if (ret != 0)
+ return ret;
+
+ *name_r = t_strdup(hbuf);
+ return 0;
+}
+
+int net_getsockname(int fd, struct ip_addr *addr, in_port_t *port)
+{
+ union sockaddr_union so;
+ socklen_t addrlen;
+
+ i_assert(fd >= 0);
+
+ i_zero(&so);
+ addrlen = sizeof(so);
+ if (getsockname(fd, &so.sa, &addrlen) == -1)
+ return -1;
+ if (so.sin.sin_family == AF_UNIX) {
+ if (addr != NULL)
+ i_zero(addr);
+ if (port != NULL) *port = 0;
+ } else {
+ if (addr != NULL) sin_get_ip(&so, addr);
+ if (port != NULL) *port = sin_get_port(&so);
+ }
+ return 0;
+}
+
+int net_getpeername(int fd, struct ip_addr *addr, in_port_t *port)
+{
+ union sockaddr_union so;
+ socklen_t addrlen;
+
+ i_assert(fd >= 0);
+
+ i_zero(&so);
+ addrlen = sizeof(so);
+ if (getpeername(fd, &so.sa, &addrlen) == -1)
+ return -1;
+ if (so.sin.sin_family == AF_UNIX) {
+ if (addr != NULL)
+ i_zero(addr);
+ if (port != NULL) *port = 0;
+ } else {
+ if (addr != NULL) sin_get_ip(&so, addr);
+ if (port != NULL) *port = sin_get_port(&so);
+ }
+ return 0;
+}
+
+int net_getunixname(int fd, const char **name_r)
+{
+ union sockaddr_union_unix so;
+ socklen_t addrlen = sizeof(so);
+
+ i_zero(&so);
+ if (getsockname(fd, &so.sa, &addrlen) < 0)
+ return -1;
+ if (so.un.sun_family != AF_UNIX) {
+ errno = ENOTSOCK;
+ return -1;
+ }
+ *name_r = t_strdup(so.un.sun_path);
+ return 0;
+}
+
+int net_getunixcred(int fd, struct net_unix_cred *cred_r)
+{
+#if defined(SO_PEERCRED)
+# if defined(HAVE_STRUCT_SOCKPEERCRED)
+ /* OpenBSD (may also provide getpeereid, but we also want pid) */
+ struct sockpeercred ucred;
+# else
+ /* Linux */
+ struct ucred ucred;
+# endif
+ socklen_t len = sizeof(ucred);
+
+ if (getsockopt(fd, SOL_SOCKET, SO_PEERCRED, &ucred, &len) < 0) {
+ i_error("getsockopt(SO_PEERCRED) failed: %m");
+ return -1;
+ }
+ cred_r->uid = ucred.uid;
+ cred_r->gid = ucred.gid;
+ cred_r->pid = ucred.pid;
+ return 0;
+#elif defined(LOCAL_PEEREID)
+ /* NetBSD (may also provide getpeereid, but we also want pid) */
+ struct unpcbid ucred;
+ socklen_t len = sizeof(ucred);
+
+ if (getsockopt(fd, 0, LOCAL_PEEREID, &ucred, &len) < 0) {
+ i_error("getsockopt(LOCAL_PEEREID) failed: %m");
+ return -1;
+ }
+
+ cred_r->uid = ucred.unp_euid;
+ cred_r->gid = ucred.unp_egid;
+ cred_r->pid = ucred.unp_pid;
+ return 0;
+#elif defined(HAVE_GETPEEREID)
+ /* OSX 10.4+, FreeBSD 4.6+, OpenBSD 3.0+, NetBSD 5.0+ */
+ if (getpeereid(fd, &cred_r->uid, &cred_r->gid) < 0) {
+ i_error("getpeereid() failed: %m");
+ return -1;
+ }
+ cred_r->pid = (pid_t)-1;
+ return 0;
+#elif defined(LOCAL_PEERCRED)
+ /* Older FreeBSD */
+ struct xucred ucred;
+ socklen_t len = sizeof(ucred);
+
+ if (getsockopt(fd, 0, LOCAL_PEERCRED, &ucred, &len) < 0) {
+ i_error("getsockopt(LOCAL_PEERCRED) failed: %m");
+ return -1;
+ }
+
+ if (ucred.cr_version != XUCRED_VERSION) {
+ errno = EINVAL;
+ return -1;
+ }
+
+ cred_r->uid = ucred.cr_uid;
+ cred_r->gid = ucred.cr_gid;
+ cred_r->pid = (pid_t)-1;
+ return 0;
+#elif defined(HAVE_GETPEERUCRED)
+ /* Solaris */
+ ucred_t *ucred = NULL;
+
+ if (getpeerucred(fd, &ucred) < 0) {
+ i_error("getpeerucred() failed: %m");
+ return -1;
+ }
+ cred_r->uid = ucred_geteuid(ucred);
+ cred_r->gid = ucred_getrgid(ucred);
+ cred_r->pid = ucred_getpid(ucred);
+ ucred_free(ucred);
+
+ if (cred_r->uid == (uid_t)-1 ||
+ cred_r->gid == (gid_t)-1) {
+ errno = EINVAL;
+ return -1;
+ }
+ return 0;
+#elif defined(NEEDS_LOCAL_CREDS)
+ /* NetBSD < 5 */
+ int i, n, on;
+ struct iovec iov;
+ struct msghdr msg;
+ struct {
+ struct cmsghdr ch;
+ char buf[110];
+ } cdata;
+ struct sockcred *sc;
+
+ iov.iov_base = (char *)&on;
+ iov.iov_len = 1;
+
+ sc = (struct sockcred *)cdata.buf;
+ sc->sc_uid = sc->sc_euid = sc->sc_gid = sc->sc_egid = -1;
+ i_zero(&cdata.ch);
+
+ i_zero(&msg);
+
+ msg.msg_iov = &iov;
+ msg.msg_iovlen = 1;
+ msg.msg_control = &cdata;
+ msg.msg_controllen = sizeof(cdata.ch) + sizeof(cdata.buf);
+
+ for (i = 0; i < 10; i++) {
+ n = recvmsg(fd, &msg, MSG_WAITALL | MSG_PEEK);
+ if (n >= 0 || errno != EAGAIN)
+ break;
+ usleep(100);
+ }
+ if (n < 0) {
+ i_error("recvmsg() failed: %m");
+ return -1;
+ }
+ cred_r->uid = sc->sc_euid;
+ cred_r->gid = sc->sc_egid;
+ cred_r->pid = (pid_t)-1;
+ return 0;
+#else
+ errno = EINVAL;
+ return -1;
+#endif
+}
+
+const char *net_ip2addr(const struct ip_addr *ip)
+{
+ char *addr = t_malloc_no0(MAX_IP_LEN+1);
+
+ if (inet_ntop(ip->family, &ip->u.ip6, addr, MAX_IP_LEN) == NULL)
+ return "";
+
+ return addr;
+}
+
+static bool net_addr2ip_inet4_fast(const char *addr, struct ip_addr *ip)
+{
+ uint8_t *saddr = (void *)&ip->u.ip4.s_addr;
+ unsigned int i, num;
+
+ if (str_parse_uint(addr, &num, &addr) < 0)
+ return FALSE;
+ if (*addr == '\0' && num <= 0xffffffff) {
+ /* single-number IPv4 address */
+ ip->u.ip4.s_addr = htonl(num);
+ ip->family = AF_INET;
+ return TRUE;
+ }
+
+ /* try to parse as a.b.c.d */
+ i = 0;
+ for (;;) {
+ if (num >= 256)
+ return FALSE;
+ saddr[i] = num;
+ if (i == 3)
+ break;
+ i++;
+ if (*addr != '.')
+ return FALSE;
+ addr++;
+ if (str_parse_uint(addr, &num, &addr) < 0)
+ return FALSE;
+ }
+ if (*addr != '\0')
+ return FALSE;
+ ip->family = AF_INET;
+ return TRUE;
+}
+
+int net_addr2ip(const char *addr, struct ip_addr *ip)
+{
+ int ret;
+
+ if (net_addr2ip_inet4_fast(addr, ip))
+ return 0;
+
+ if (strchr(addr, ':') != NULL) {
+ /* IPv6 */
+ T_BEGIN {
+ if (addr[0] == '[') {
+ /* allow [ipv6 addr] */
+ size_t len = strlen(addr);
+ if (addr[len-1] == ']')
+ addr = t_strndup(addr+1, len-2);
+ }
+ ret = inet_pton(AF_INET6, addr, &ip->u.ip6);
+ } T_END;
+ if (ret == 0)
+ return -1;
+ ip->family = AF_INET6;
+ } else {
+ /* IPv4 */
+ if (inet_aton(addr, &ip->u.ip4) == 0)
+ return -1;
+ ip->family = AF_INET;
+ }
+ return 0;
+}
+
+int net_str2port(const char *str, in_port_t *port_r)
+{
+ uintmax_t l;
+
+ if (str_to_uintmax(str, &l) < 0)
+ return -1;
+
+ if (l == 0 || l > (in_port_t)-1)
+ return -1;
+ *port_r = (in_port_t)l;
+ return 0;
+}
+
+int net_str2port_zero(const char *str, in_port_t *port_r)
+{
+ uintmax_t l;
+
+ if (str_to_uintmax(str, &l) < 0)
+ return -1;
+
+ if (l > (in_port_t)-1)
+ return -1;
+ *port_r = (in_port_t)l;
+ return 0;
+}
+
+int net_str2hostport(const char *str, in_port_t default_port,
+ const char **host_r, in_port_t *port_r)
+{
+ const char *p, *host;
+ in_port_t port;
+
+ if (str[0] == '[') {
+ /* [IPv6] address, possibly followed by :port */
+ p = strchr(str, ']');
+ if (p == NULL)
+ return -1;
+ host = t_strdup_until(str+1, p++);
+ } else {
+ p = strchr(str, ':');
+ if (p == NULL || strchr(p+1, ':') != NULL) {
+ /* host or IPv6 address */
+ *host_r = str;
+ *port_r = default_port;
+ return 0;
+ }
+ host = t_strdup_until(str, p);
+ }
+ if (p[0] == '\0') {
+ *host_r = host;
+ *port_r = default_port;
+ return 0;
+ }
+ if (p[0] != ':')
+ return -1;
+ if (net_str2port(p+1, &port) < 0)
+ return -1;
+ *host_r = host;
+ *port_r = port;
+ return 0;
+}
+
+int net_ipport2str(const struct ip_addr *ip, in_port_t port, const char **str_r)
+{
+ if (!IPADDR_IS_V4(ip) && !IPADDR_IS_V6(ip)) return -1;
+
+ *str_r = t_strdup_printf("%s%s%s:%u",
+ IPADDR_IS_V6(ip) ? "[" : "",
+ net_ip2addr(ip),
+ IPADDR_IS_V6(ip) ? "]" : "",
+ port);
+ return 0;
+}
+
+int net_ipv6_mapped_ipv4_convert(const struct ip_addr *src,
+ struct ip_addr *dest)
+{
+ static uint8_t v4_prefix[] =
+ { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0xff, 0xff };
+
+ if (!IPADDR_IS_V6(src))
+ return -1;
+ if (memcmp(src->u.ip6.s6_addr, v4_prefix, sizeof(v4_prefix)) != 0)
+ return -1;
+
+ i_zero(dest);
+ dest->family = AF_INET;
+ memcpy(&dest->u.ip6, &src->u.ip6.s6_addr[3*4], 4);
+ return 0;
+}
+
+int net_geterror(int fd)
+{
+ int data;
+ socklen_t len = sizeof(data);
+
+ if (getsockopt(fd, SOL_SOCKET, SO_ERROR, &data, &len) == -1) {
+ /* we're now really returning the getsockopt()'s error code
+ instead of the socket's, but normally we should never get
+ here anyway. */
+ return errno;
+ }
+
+ return data;
+}
+
+const char *net_gethosterror(int error)
+{
+ i_assert(error != 0);
+
+ return gai_strerror(error);
+}
+
+enum net_hosterror_type net_get_hosterror_type(int error)
+{
+ const struct {
+ int error;
+ enum net_hosterror_type type;
+ } error_map[] = {
+#ifdef EAI_ADDRFAMILY /* Obsoleted by RFC 2553bis-02 */
+ { EAI_ADDRFAMILY, NET_HOSTERROR_TYPE_NOT_FOUND },
+#endif
+ { EAI_AGAIN, NET_HOSTERROR_TYPE_NAMESERVER },
+ { EAI_BADFLAGS, NET_HOSTERROR_TYPE_INTERNAL_ERROR },
+ { EAI_FAIL, NET_HOSTERROR_TYPE_NAMESERVER },
+ { EAI_FAMILY, NET_HOSTERROR_TYPE_INTERNAL_ERROR },
+ { EAI_MEMORY, NET_HOSTERROR_TYPE_INTERNAL_ERROR },
+#ifdef EAI_NODATA /* Obsoleted by RFC 2553bis-02 */
+ { EAI_NODATA, NET_HOSTERROR_TYPE_NOT_FOUND },
+#endif
+ { EAI_NONAME, NET_HOSTERROR_TYPE_NOT_FOUND },
+ { EAI_SERVICE, NET_HOSTERROR_TYPE_INTERNAL_ERROR },
+ { EAI_SOCKTYPE, NET_HOSTERROR_TYPE_INTERNAL_ERROR },
+ { EAI_SYSTEM, NET_HOSTERROR_TYPE_INTERNAL_ERROR },
+ };
+ for (unsigned int i = 0; i < N_ELEMENTS(error_map); i++) {
+ if (error_map[i].error == error)
+ return error_map[i].type;
+ }
+
+ /* shouldn't happen? assume internal error */
+ return NET_HOSTERROR_TYPE_INTERNAL_ERROR;
+}
+
+int net_hosterror_notfound(int error)
+{
+#ifdef EAI_NODATA /* NODATA is depricated */
+ return (error != 1 && (error == EAI_NONAME || error == EAI_NODATA)) ? 1 : 0;
+#else
+ return (error != 1 && (error == EAI_NONAME)) ? 1 : 0;
+#endif
+}
+
+const char *net_getservbyport(in_port_t port)
+{
+ struct servent *entry;
+
+ entry = getservbyport(htons(port), "tcp");
+ return entry == NULL ? NULL : entry->s_name;
+}
+
+bool is_ipv4_address(const char *addr)
+{
+ while (*addr != '\0') {
+ if (*addr != '.' && !i_isdigit(*addr))
+ return FALSE;
+ addr++;
+ }
+
+ return TRUE;
+}
+
+bool is_ipv6_address(const char *addr)
+{
+ bool have_prefix = FALSE;
+
+ if (*addr == '[') {
+ have_prefix = TRUE;
+ addr++;
+ }
+ while (*addr != '\0') {
+ if (*addr != ':' && !i_isxdigit(*addr)) {
+ if (have_prefix && *addr == ']' && addr[1] == '\0')
+ break;
+ return FALSE;
+ }
+ addr++;
+ }
+
+ return TRUE;
+}
+
+int net_parse_range(const char *network, struct ip_addr *ip_r,
+ unsigned int *bits_r)
+{
+ const char *p;
+ unsigned int bits, max_bits;
+
+ p = strchr(network, '/');
+ if (p != NULL)
+ network = t_strdup_until(network, p++);
+
+ if (net_addr2ip(network, ip_r) < 0)
+ return -1;
+
+ max_bits = IPADDR_BITS(ip_r);
+ if (p == NULL) {
+ /* full IP address must match */
+ bits = max_bits;
+ } else {
+ /* get the network mask */
+ if (str_to_uint(p, &bits) < 0 || bits > max_bits)
+ return -1;
+ }
+ *bits_r = bits;
+ return 0;
+}
+
+bool net_is_in_network(const struct ip_addr *ip,
+ const struct ip_addr *net_ip, unsigned int bits)
+{
+ struct ip_addr tmp_ip;
+ const uint32_t *ip1, *ip2;
+ uint32_t mask, i1, i2;
+ unsigned int pos, i;
+
+ if (net_ipv6_mapped_ipv4_convert(ip, &tmp_ip) == 0) {
+ /* IPv4 address mapped disguised as IPv6 address */
+ ip = &tmp_ip;
+ }
+
+ if (ip->family == 0 || net_ip->family == 0) {
+ /* non-IPv4/IPv6 address (e.g. UNIX socket) never matches
+ anything */
+ return FALSE;
+ }
+ if (IPADDR_IS_V4(ip) != IPADDR_IS_V4(net_ip)) {
+ /* one is IPv6 and one is IPv4 */
+ return FALSE;
+ }
+ i_assert(IPADDR_IS_V6(ip) == IPADDR_IS_V6(net_ip));
+
+ if (IPADDR_IS_V4(ip)) {
+ ip1 = &ip->u.ip4.s_addr;
+ ip2 = &net_ip->u.ip4.s_addr;
+ } else {
+ ip1 = (const void *)&ip->u.ip6;
+ ip2 = (const void *)&net_ip->u.ip6;
+ }
+
+ /* check first the full 32bit ints */
+ for (pos = 0, i = 0; pos + 32 <= bits; pos += 32, i++) {
+ if (ip1[i] != ip2[i])
+ return FALSE;
+ }
+ i1 = htonl(ip1[i]);
+ i2 = htonl(ip2[i]);
+
+ /* check the last full bytes */
+ for (mask = 0xff000000; pos + 8 <= bits; pos += 8, mask >>= 8) {
+ if ((i1 & mask) != (i2 & mask))
+ return FALSE;
+ }
+
+ /* check the last bits, they're reversed in bytes */
+ bits -= pos;
+ for (mask = 0x80000000 >> (pos % 32); bits > 0; bits--, mask >>= 1) {
+ if ((i1 & mask) != (i2 & mask))
+ return FALSE;
+ }
+ return TRUE;
+}
diff --git a/src/lib/net.h b/src/lib/net.h
new file mode 100644
index 0000000..7f8abb7
--- /dev/null
+++ b/src/lib/net.h
@@ -0,0 +1,199 @@
+#ifndef NET_H
+#define NET_H
+
+#ifndef WIN32
+# include <sys/socket.h>
+# include <netinet/in.h>
+# include <netdb.h>
+# include <arpa/inet.h>
+#endif
+
+#ifdef HAVE_SOCKS_H
+#include <socks.h>
+#endif
+
+#ifndef AF_INET6
+# ifdef PF_INET6
+# define AF_INET6 PF_INET6
+# else
+# define AF_INET6 10
+# endif
+#endif
+
+struct ip_addr {
+ unsigned short family;
+ union {
+ struct in6_addr ip6;
+ struct in_addr ip4;
+ } u;
+};
+ARRAY_DEFINE_TYPE(ip_addr, struct ip_addr);
+
+struct net_unix_cred {
+ uid_t uid;
+ gid_t gid;
+ pid_t pid;
+};
+
+/* maximum string length of IP address */
+#define MAX_IP_LEN INET6_ADDRSTRLEN
+
+#define IPADDR_IS_V4(ip) ((ip)->family == AF_INET)
+#define IPADDR_IS_V6(ip) ((ip)->family == AF_INET6)
+#define IPADDR_BITS(ip) (IPADDR_IS_V4(ip) ? 32 : 128)
+
+enum net_listen_flags {
+ /* Try to use SO_REUSEPORT if available. If it's not, this flag is
+ cleared on return. */
+ NET_LISTEN_FLAG_REUSEPORT = 0x01
+};
+
+enum net_hosterror_type {
+ /* Internal error - should be logged as an error */
+ NET_HOSTERROR_TYPE_INTERNAL_ERROR,
+ /* Host not found or no valid IP addresses found */
+ NET_HOSTERROR_TYPE_NOT_FOUND,
+ /* Nameserver returned an error */
+ NET_HOSTERROR_TYPE_NAMESERVER,
+};
+
+/* INADDR_ANY for IPv4 or IPv6. The IPv6 any address may
+ include IPv4 depending on the system (Linux yes, BSD no). */
+extern const struct ip_addr net_ip4_any;
+extern const struct ip_addr net_ip6_any;
+
+extern const struct ip_addr net_ip4_loopback;
+extern const struct ip_addr net_ip6_loopback;
+
+/* Returns TRUE if IPs are the same */
+bool net_ip_compare(const struct ip_addr *ip1, const struct ip_addr *ip2);
+/* Returns 0 if IPs are the same, -1 or 1 otherwise. */
+int net_ip_cmp(const struct ip_addr *ip1, const struct ip_addr *ip2);
+unsigned int net_ip_hash(const struct ip_addr *ip);
+
+/* Connect to TCP socket with ip address. The socket and connect() is
+ non-blocking. */
+int net_connect_ip(const struct ip_addr *ip, in_port_t port,
+ const struct ip_addr *my_ip) ATTR_NULL(3);
+/* Like net_connect_ip(), but do a blocking connect(). */
+int net_connect_ip_blocking(const struct ip_addr *ip, in_port_t port,
+ const struct ip_addr *my_ip) ATTR_NULL(3);
+/* Like net_connect_ip(), but open a UDP socket. */
+int net_connect_udp(const struct ip_addr *ip, in_port_t port,
+ const struct ip_addr *my_ip);
+/* Returns 0 if we can bind() as given IP, -1 if not. */
+int net_try_bind(const struct ip_addr *ip);
+/* Connect to named UNIX socket */
+int net_connect_unix(const char *path);
+/* Try to connect to UNIX socket for give number of seconds when connect()
+ returns EAGAIN or ECONNREFUSED. */
+int net_connect_unix_with_retries(const char *path, unsigned int msecs);
+/* Disconnect socket */
+void net_disconnect(int fd);
+
+/* Set socket blocking/nonblocking */
+void net_set_nonblock(int fd, bool nonblock);
+/* Set TCP_CORK if supported, ie. don't send out partial frames.
+ Returns 0 if ok, -1 if failed. */
+int net_set_cork(int fd, bool cork) ATTR_NOWARN_UNUSED_RESULT;
+/* Set TCP_NODELAY, which disables the Nagle algorithm. */
+int net_set_tcp_nodelay(int fd, bool nodelay);
+/* Set TCP_QUICKACK, which tells the kernel to not delay ACKs. Note that the
+ kernel can (and will) re-enable delayed ACKs while processing the TCP stack.
+ This means that this function needs to be called repeatedly. */
+int net_set_tcp_quickack(int fd, bool quickack);
+
+/* Set socket kernel buffer sizes */
+int net_set_send_buffer_size(int fd, size_t size);
+int net_set_recv_buffer_size(int fd, size_t size);
+
+/* Listen for connections on a socket */
+int net_listen(const struct ip_addr *my_ip, in_port_t *port, int backlog);
+int net_listen_full(const struct ip_addr *my_ip, in_port_t *port,
+ enum net_listen_flags *flags, int backlog);
+/* Listen for connections on an UNIX socket */
+int net_listen_unix(const char *path, int backlog);
+/* Like net_listen_unix(), but if socket already exists, try to connect to it.
+ If it fails with ECONNREFUSED, unlink the socket and try creating it
+ again. */
+int net_listen_unix_unlink_stale(const char *path, int backlog);
+/* Accept a connection on a socket. Returns -1 if the connection got closed,
+ -2 for other failures. For UNIX sockets addr_r->family=port=0. */
+int net_accept(int fd, struct ip_addr *addr_r, in_port_t *port_r)
+ ATTR_NULL(2, 3);
+
+/* Read data from socket, return number of bytes read,
+ -1 = error, -2 = disconnected */
+ssize_t net_receive(int fd, void *buf, size_t len);
+
+/* Get IP addresses for host. ips contains ips_count of IPs, they don't need
+ to be free'd. Returns 0 = ok, others = error code for net_gethosterror() */
+int net_gethostbyname(const char *addr, struct ip_addr **ips,
+ unsigned int *ips_count);
+/* Return host for the IP address. Returns 0 = ok, others = error code for
+ net_gethosterror(). */
+int net_gethostbyaddr(const struct ip_addr *ip, const char **name_r);
+/* get error of net_gethostname() */
+const char *net_gethosterror(int error) ATTR_CONST;
+/* Return type of the error returned by net_gethostname() */
+enum net_hosterror_type net_get_hosterror_type(int error);
+/* return TRUE if host lookup failed because it didn't exist (ie. not
+ some error with name server) */
+int net_hosterror_notfound(int error) ATTR_CONST;
+
+/* Get socket local address/port. For UNIX sockets addr->family=port=0. */
+int net_getsockname(int fd, struct ip_addr *addr, in_port_t *port)
+ ATTR_NULL(2, 3);
+/* Get socket remote address/port. For UNIX sockets addr->family=port=0. */
+int net_getpeername(int fd, struct ip_addr *addr, in_port_t *port)
+ ATTR_NULL(2, 3);
+/* Get UNIX socket name. */
+int net_getunixname(int fd, const char **name_r);
+/* Get UNIX socket peer process's credentials. The pid may be (pid_t)-1 if
+ unavailable. */
+int net_getunixcred(int fd, struct net_unix_cred *cred_r);
+
+/* Returns ip_addr as string, or "" if ip isn't valid IPv4 or IPv6 address. */
+const char *net_ip2addr(const struct ip_addr *ip);
+/* char* -> struct ip_addr translation. */
+int net_addr2ip(const char *addr, struct ip_addr *ip);
+/* char* -> in_port_t translation */
+int net_str2port(const char *str, in_port_t *port_r);
+/* char* -> in_port_t translation (allows port zero) */
+int net_str2port_zero(const char *str, in_port_t *port_r);
+/* Parse "host", "host:port", "IPv4", "IPv4:port", "IPv6", "[IPv6]" or
+ "[IPv6]:port" to its host and port components. [IPv6] address is returned
+ without []. If no port is given, return default_port. The :port in the
+ parsed string isn't allowed to be zero, but default_port=0 is passed
+ through. */
+int net_str2hostport(const char *str, in_port_t default_port,
+ const char **host_r, in_port_t *port_r);
+/* Converts ip and port to ipv4:port or [ipv6]:port. Returns -1 if
+ ip is not valid IPv4 or IPv6 address. */
+int net_ipport2str(const struct ip_addr *ip, in_port_t port, const char **str_r);
+
+/* Convert IPv6 mapped IPv4 address to an actual IPv4 address. Returns 0 if
+ successful, -1 if the source address isn't IPv6 mapped IPv4 address. */
+int net_ipv6_mapped_ipv4_convert(const struct ip_addr *src,
+ struct ip_addr *dest);
+
+/* Get socket error */
+int net_geterror(int fd);
+
+/* Get name of TCP service */
+const char *net_getservbyport(in_port_t port) ATTR_CONST;
+
+bool is_ipv4_address(const char *addr) ATTR_PURE;
+bool is_ipv6_address(const char *addr) ATTR_PURE;
+
+/* Parse network as ip/bits. Returns 0 if successful, -1 if invalid input. */
+int net_parse_range(const char *network, struct ip_addr *ip_r,
+ unsigned int *bits_r);
+/* Returns TRUE if ip is in net_ip/bits network. IPv4-mapped IPv6 addresses
+ in "ip" parameter are converted to plain IPv4 addresses before matching.
+ No conversion is done to net_ip though, so using IPv4-mapped IPv6 addresses
+ there will always fail. Invalid IPs (family=0) never match anything. */
+bool net_is_in_network(const struct ip_addr *ip, const struct ip_addr *net_ip,
+ unsigned int bits) ATTR_PURE;
+
+#endif
diff --git a/src/lib/nfs-workarounds.c b/src/lib/nfs-workarounds.c
new file mode 100644
index 0000000..4bbea8a
--- /dev/null
+++ b/src/lib/nfs-workarounds.c
@@ -0,0 +1,406 @@
+/* Copyright (c) 2006-2018 Dovecot authors, see the included COPYING file */
+
+/*
+ These tests were done with various Linux 2.6 kernels, FreeBSD 6.2 and
+ Solaris 8 and 10.
+
+ Attribute cache is usually flushed with chown()ing or fchown()ing the file.
+ The safest way would be to use uid=-1 gid=-1, but this doesn't work with
+ Linux (it does with FreeBSD 6.2 and Solaris). So we'll first get the
+ file's owner and use it. As long as we're not root the file's owner can't
+ change accidentally. If would be possible to also use chmod()/fchmod(), but
+ that's riskier since it could actually cause an unwanted change.
+
+ Write cache can be flushed with fdatasync(). It's all we need, but other
+ tested alternatives are: fcntl locking (Linux 2.6, Solaris),
+ fchown() (Solaris) and dup()+close() (Linux 2.6, Solaris).
+
+ Read cache flushing is more problematic. There's no universal way to do it.
+ The working methods are:
+
+ Linux 2.6: fcntl(), O_DIRECT
+ Solaris: fchown(), fcntl(), dup()+close()
+ FreeBSD 6.2: fchown()
+
+ fchown() can be easily used for Solaris and FreeBSD, but Linux requires
+ playing with locks. O_DIRECT requires CONFIG_NFS_DIRECTIO to be enabled, so
+ we can't always use it.
+*/
+
+#include "lib.h"
+#include "path-util.h"
+#include "nfs-workarounds.h"
+
+#include <fcntl.h>
+#include <unistd.h>
+#include <sys/stat.h>
+
+#if defined (__linux__) || defined(__sun)
+# define READ_CACHE_FLUSH_FCNTL
+#endif
+#if defined(__FreeBSD__) || defined(__sun)
+# define ATTRCACHE_FLUSH_CHOWN_UID_1
+#endif
+
+static void nfs_flush_file_handle_cache_parent_dir(const char *path);
+
+static int
+nfs_safe_do(const char *path, int (*callback)(const char *path, void *context),
+ void *context)
+{
+ unsigned int i;
+ int ret;
+
+ for (i = 1;; i++) {
+ ret = callback(path, context);
+ if (ret == 0 || errno != ESTALE || i == NFS_ESTALE_RETRY_COUNT)
+ break;
+
+ /* ESTALE: Some operating systems may fail with this if they
+ can't internally revalidate the NFS file handle. Flush the
+ file handle and try again */
+ nfs_flush_file_handle_cache(path);
+ }
+ return ret;
+}
+
+struct nfs_safe_open_context {
+ int flags;
+ int fd;
+};
+
+static int nfs_safe_open_callback(const char *path, void *context)
+{
+ struct nfs_safe_open_context *ctx = context;
+
+ ctx->fd = open(path, ctx->flags);
+ return ctx->fd == -1 ? -1 : 0;
+}
+
+int nfs_safe_open(const char *path, int flags)
+{
+ struct nfs_safe_open_context ctx;
+
+ i_assert((flags & O_CREAT) == 0);
+
+ ctx.flags = flags;
+ if (nfs_safe_do(path, nfs_safe_open_callback, &ctx) < 0)
+ return -1;
+
+ return ctx.fd;
+}
+
+static int nfs_safe_stat_callback(const char *path, void *context)
+{
+ struct stat *buf = context;
+
+ return stat(path, buf);
+}
+
+int nfs_safe_stat(const char *path, struct stat *buf)
+{
+ return nfs_safe_do(path, nfs_safe_stat_callback, buf);
+}
+
+static int nfs_safe_lstat_callback(const char *path, void *context)
+{
+ struct stat *buf = context;
+
+ return lstat(path, buf);
+}
+
+int nfs_safe_lstat(const char *path, struct stat *buf)
+{
+ return nfs_safe_do(path, nfs_safe_lstat_callback, buf);
+}
+
+int nfs_safe_link(const char *oldpath, const char *newpath, bool links1)
+{
+ struct stat st;
+ nlink_t orig_link_count = 1;
+
+ if (!links1) {
+ if (stat(oldpath, &st) < 0)
+ return -1;
+ orig_link_count = st.st_nlink;
+ }
+
+ if (link(oldpath, newpath) == 0) {
+#ifndef __FreeBSD__
+ return 0;
+#endif
+ /* FreeBSD at least up to v6.2 converts EEXIST errors to
+ success. */
+ } else if (errno != EEXIST)
+ return -1;
+
+ /* We don't know if it succeeded or failed. stat() to make sure. */
+ if (stat(oldpath, &st) < 0)
+ return -1;
+ if (st.st_nlink == orig_link_count) {
+ errno = EEXIST;
+ return -1;
+ }
+ return 0;
+}
+
+static void nfs_flush_chown_uid(const char *path)
+{
+
+#ifdef ATTRCACHE_FLUSH_CHOWN_UID_1
+ uid_t uid = (uid_t)-1;
+ if (chown(path, uid, (gid_t)-1) < 0) {
+ if (errno == ESTALE || errno == EPERM || errno == ENOENT) {
+ /* attr cache is flushed */
+ return;
+ }
+ if (likely(errno == ENOENT)) {
+ nfs_flush_file_handle_cache_parent_dir(path);
+ return;
+ }
+ i_error("nfs_flush_chown_uid: chown(%s) failed: %m", path);
+ }
+#else
+ struct stat st;
+
+ if (stat(path, &st) == 0) {
+ /* do nothing */
+ } else {
+ if (errno == ESTALE) {
+ /* ESTALE causes the OS to flush the attr cache */
+ return;
+ }
+ if (likely(errno == ENOENT)) {
+ nfs_flush_file_handle_cache_parent_dir(path);
+ return;
+ }
+ i_error("nfs_flush_chown_uid: stat(%s) failed: %m", path);
+ return;
+ }
+ /* we use chmod for this operation since chown has been seen to drop S_UID
+ and S_GID bits from directory inodes in certain conditions */
+ if (chmod(path, st.st_mode & 07777) < 0) {
+ if (errno == EPERM) {
+ /* attr cache is flushed */
+ return;
+ }
+ if (likely(errno == ENOENT)) {
+ nfs_flush_file_handle_cache_parent_dir(path);
+ return;
+ }
+ i_error("nfs_flush_chown_uid: chmod(%s, %04o) failed: %m",
+ path, st.st_mode & 07777);
+ }
+#endif
+}
+
+#ifdef __FreeBSD__
+static bool nfs_flush_fchown_uid(const char *path, int fd)
+{
+ uid_t uid;
+#ifndef ATTRCACHE_FLUSH_CHOWN_UID_1
+ struct stat st;
+
+ if (fstat(fd, &st) < 0) {
+ if (likely(errno == ESTALE))
+ return FALSE;
+ i_error("nfs_flush_attr_cache_fchown: fstat(%s) failed: %m",
+ path);
+ return TRUE;
+ }
+ uid = st.st_uid;
+#else
+ uid = (uid_t)-1;
+#endif
+ if (fchown(fd, uid, (gid_t)-1) < 0) {
+ if (errno == ESTALE)
+ return FALSE;
+ if (likely(errno == EACCES || errno == EPERM)) {
+ /* attr cache is flushed */
+ return TRUE;
+ }
+
+ i_error("nfs_flush_attr_cache_fd_locked: fchown(%s) failed: %m",
+ path);
+ }
+ return TRUE;
+}
+#endif
+
+#ifdef READ_CACHE_FLUSH_FCNTL
+static bool nfs_flush_fcntl(const char *path, int fd)
+{
+ static bool locks_disabled = FALSE;
+ struct flock fl;
+ int ret;
+
+ if (locks_disabled)
+ return FALSE;
+
+ /* If the file was already locked, we'll just get the same lock
+ again. It should succeed just fine. If was was unlocked, we'll
+ have to get a lock and then unlock it. Linux 2.6 flushes read cache
+ only when read/write locking succeeded. */
+ fl.l_type = F_RDLCK;
+ fl.l_whence = SEEK_SET;
+ fl.l_start = 0;
+ fl.l_len = 0;
+
+ alarm(60);
+ ret = fcntl(fd, F_SETLKW, &fl);
+ alarm(0);
+
+ if (unlikely(ret < 0)) {
+ if (errno == ENOLCK) {
+ locks_disabled = TRUE;
+ return FALSE;
+ }
+ i_error("nfs_flush_fcntl: fcntl(%s, F_RDLCK) failed: %m", path);
+ return FALSE;
+ }
+
+ fl.l_type = F_UNLCK;
+ (void)fcntl(fd, F_SETLKW, &fl);
+ return TRUE;
+}
+#endif
+
+void nfs_flush_attr_cache_unlocked(const char *path)
+{
+ int fd;
+
+ /* Try to flush the attribute cache the nice way first. */
+ fd = open(path, O_RDONLY);
+ if (fd != -1)
+ i_close_fd(&fd);
+ else if (errno == ESTALE) {
+ /* this already flushed the cache */
+ } else {
+ /* most likely ENOENT, which means a negative cache hit.
+ flush the file handles for its parent directory. */
+ nfs_flush_file_handle_cache_parent_dir(path);
+ }
+}
+
+void nfs_flush_attr_cache_maybe_locked(const char *path)
+{
+ nfs_flush_chown_uid(path);
+}
+
+void nfs_flush_attr_cache_fd_locked(const char *path ATTR_UNUSED,
+ int fd ATTR_UNUSED)
+{
+#ifdef __FreeBSD__
+ /* FreeBSD doesn't flush attribute cache with fcntl(), so we have
+ to do it ourself. */
+ (void)nfs_flush_fchown_uid(path, fd);
+#else
+ /* Linux and Solaris are fine. */
+#endif
+}
+
+static bool
+nfs_flush_file_handle_cache_dir(const char *path, bool try_parent ATTR_UNUSED)
+{
+#ifdef __linux__
+ /* chown()ing parent is the safest way to handle this */
+ nfs_flush_chown_uid(path);
+#else
+ /* rmdir() is the only choice with FreeBSD and Solaris */
+ if (unlikely(rmdir(path) == 0)) {
+ if (mkdir(path, 0700) == 0) {
+ i_warning("nfs_flush_file_handle_cache_dir: "
+ "rmdir(%s) unexpectedly "
+ "removed the dir. recreated.", path);
+ } else {
+ i_warning("nfs_flush_file_handle_cache_dir: "
+ "rmdir(%s) unexpectedly "
+ "removed the dir. mkdir() failed: %m", path);
+ }
+ } else if (errno == ESTALE || errno == ENOTDIR ||
+ errno == ENOTEMPTY || errno == EEXIST || errno == EACCES) {
+ /* expected failures */
+ } else if (errno == ENOENT) {
+ return FALSE;
+ } else if (errno == EINVAL && try_parent) {
+ /* Solaris gives this if we're trying to rmdir() the current
+ directory. Work around this by temporarily changing the
+ current directory to the parent directory. */
+ const char *cur_path, *p;
+ int cur_dir_fd;
+ bool ret;
+
+ cur_dir_fd = open(".", O_RDONLY);
+ if (cur_dir_fd == -1) {
+ i_error("open(.) failed for: %m");
+ return TRUE;
+ }
+
+ const char *error;
+ if (t_get_working_dir(&cur_path, &error) < 0) {
+ i_error("nfs_flush_file_handle_cache_dir: %s", error);
+ i_close_fd(&cur_dir_fd);
+ return TRUE;
+ }
+ p = strrchr(cur_path, '/');
+ if (p == NULL)
+ cur_path = "/";
+ else
+ cur_path = t_strdup_until(cur_path, p);
+ if (chdir(cur_path) < 0) {
+ i_error("nfs_flush_file_handle_cache_dir: "
+ "chdir() failed");
+ }
+ ret = nfs_flush_file_handle_cache_dir(path, FALSE);
+ if (fchdir(cur_dir_fd) < 0)
+ i_error("fchdir() failed: %m");
+ i_close_fd(&cur_dir_fd);
+ return ret;
+ } else {
+ i_error("nfs_flush_file_handle_cache_dir: "
+ "rmdir(%s) failed: %m", path);
+ }
+#endif
+ return TRUE;
+}
+
+static void nfs_flush_file_handle_cache_parent_dir(const char *path)
+{
+ const char *p;
+
+ p = strrchr(path, '/');
+ T_BEGIN {
+ if (p == NULL)
+ (void)nfs_flush_file_handle_cache_dir(".", TRUE);
+ else
+ (void)nfs_flush_file_handle_cache_dir(t_strdup_until(path, p),
+ TRUE);
+ } T_END;
+}
+
+void nfs_flush_file_handle_cache(const char *path)
+{
+ nfs_flush_file_handle_cache_parent_dir(path);
+}
+
+void nfs_flush_read_cache_locked(const char *path ATTR_UNUSED,
+ int fd ATTR_UNUSED)
+{
+#ifdef READ_CACHE_FLUSH_FCNTL
+ /* already flushed when fcntl() was called */
+#else
+ /* we can only hope that underlying filesystem uses micro/nanosecond
+ resolution so that attribute cache flushing notices mtime changes */
+ nfs_flush_attr_cache_fd_locked(path, fd);
+#endif
+}
+
+void nfs_flush_read_cache_unlocked(const char *path, int fd)
+{
+#ifdef READ_CACHE_FLUSH_FCNTL
+ if (!nfs_flush_fcntl(path, fd))
+ nfs_flush_attr_cache_fd_locked(path, fd);
+#else
+ nfs_flush_read_cache_locked(path, fd);
+#endif
+}
diff --git a/src/lib/nfs-workarounds.h b/src/lib/nfs-workarounds.h
new file mode 100644
index 0000000..261f523
--- /dev/null
+++ b/src/lib/nfs-workarounds.h
@@ -0,0 +1,40 @@
+#ifndef NFS_WORKAROUNDS_H
+#define NFS_WORKAROUNDS_H
+
+/* Note that some systems (Solaris) may use a macro to redefine struct stat */
+#include <sys/stat.h>
+
+/* When syscall fails with ESTALE error, how many times to try reopening the
+ file and retrying the operation. */
+#define NFS_ESTALE_RETRY_COUNT 10
+
+/* Same as open(), but try to handle ESTALE errors. */
+int nfs_safe_open(const char *path, int flags);
+/* Same as stat(), but try to handle ESTALE errors.
+ Doesn't flush attribute cache. */
+int nfs_safe_stat(const char *path, struct stat *buf);
+int nfs_safe_lstat(const char *path, struct stat *buf);
+/* Same as link(), but handle problems with link() by verifying the file's
+ link count changes. If links1=TRUE, assume the original file's link count
+ is 1, otherwise stat() first to find it out. */
+int nfs_safe_link(const char *oldpath, const char *newpath, bool links1);
+
+/* Flush attribute cache for given path. The file must not be fcntl locked or
+ the locks may get dropped. */
+void nfs_flush_attr_cache_unlocked(const char *path);
+/* Flush attribute cache for given path. The file may be fcntl locked. */
+void nfs_flush_attr_cache_maybe_locked(const char *path);
+/* Flush attribute cache for a fcntl locked file descriptor. If locking flushes
+ the attribute cache with the running OS, this function does nothing.
+ The given path is used only for logging. */
+void nfs_flush_attr_cache_fd_locked(const char *path, int fd);
+/* Flush file handle cache for given file. */
+void nfs_flush_file_handle_cache(const char *path);
+
+/* Flush read cache for fd that was just fcntl locked. If the OS flushes
+ read cache when fcntl locking file, this function does nothing. */
+void nfs_flush_read_cache_locked(const char *path, int fd);
+/* Flush read cache for fd that doesn't have fcntl locks. */
+void nfs_flush_read_cache_unlocked(const char *path, int fd);
+
+#endif
diff --git a/src/lib/numpack.c b/src/lib/numpack.c
new file mode 100644
index 0000000..8a59f3a
--- /dev/null
+++ b/src/lib/numpack.c
@@ -0,0 +1,57 @@
+/* Copyright (c) 2013-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "buffer.h"
+#include "numpack.h"
+
+void numpack_encode(buffer_t *buf, uint64_t num)
+{
+ /* number continues as long as the highest bit is set */
+ while (num >= 0x80) {
+ buffer_append_c(buf, (num & 0x7f) | 0x80);
+ num >>= 7;
+ }
+
+ buffer_append_c(buf, num);
+}
+
+int numpack_decode(const uint8_t **p, const uint8_t *end, uint64_t *num_r)
+ ATTR_UNSIGNED_WRAPS
+{
+ const uint8_t *c = *p;
+ uint64_t value = 0;
+ unsigned int bits = 0;
+
+ while (bits < 64) {
+ if (c == end)
+ return -1;
+
+ value |= (uint64_t)(*c & 0x7f) << bits;
+ if (*c < 0x80)
+ break;
+
+ bits += 7;
+ c++;
+ }
+
+ bits += bits_required8(*c);
+ if (bits > 64) /* overflow */
+ return -1;
+
+ *p = c + 1;
+ *num_r = value;
+ return 0;
+}
+
+int numpack_decode32(const uint8_t **p, const uint8_t *end, uint32_t *num_r)
+{
+ uint64_t num;
+
+ if (numpack_decode(p, end, &num) < 0)
+ return -1;
+ if (num > 4294967295U)
+ return -1;
+
+ *num_r = (uint32_t)num;
+ return 0;
+}
diff --git a/src/lib/numpack.h b/src/lib/numpack.h
new file mode 100644
index 0000000..1ee0737
--- /dev/null
+++ b/src/lib/numpack.h
@@ -0,0 +1,11 @@
+#ifndef NUMPACK_H
+#define NUMPACK_H
+
+/* Numbers are stored by 7 bits at a time. The highest bit specifies if the
+ number continues to next byte. */
+
+void numpack_encode(buffer_t *buf, uint64_t num);
+int numpack_decode(const uint8_t **p, const uint8_t *end, uint64_t *num_r);
+int numpack_decode32(const uint8_t **p, const uint8_t *end, uint32_t *num_r);
+
+#endif
diff --git a/src/lib/ostream-buffer.c b/src/lib/ostream-buffer.c
new file mode 100644
index 0000000..af3dbef
--- /dev/null
+++ b/src/lib/ostream-buffer.c
@@ -0,0 +1,88 @@
+/* Copyright (c) 2002-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "buffer.h"
+#include "ostream-private.h"
+
+struct buffer_ostream {
+ struct ostream_private ostream;
+ buffer_t *buf;
+ bool seeked;
+};
+
+static int o_stream_buffer_seek(struct ostream_private *stream, uoff_t offset)
+{
+ struct buffer_ostream *bstream =
+ container_of(stream, struct buffer_ostream, ostream);
+
+ bstream->seeked = TRUE;
+ stream->ostream.offset = offset;
+ return 1;
+}
+
+static int
+o_stream_buffer_write_at(struct ostream_private *stream,
+ const void *data, size_t size, uoff_t offset)
+{
+ struct buffer_ostream *bstream =
+ container_of(stream, struct buffer_ostream, ostream);
+
+ buffer_write(bstream->buf, offset, data, size);
+ return 0;
+}
+
+static ssize_t
+o_stream_buffer_sendv(struct ostream_private *stream,
+ const struct const_iovec *iov, unsigned int iov_count)
+{
+ struct buffer_ostream *bstream =
+ container_of(stream, struct buffer_ostream, ostream);
+ size_t left, n, offset;
+ ssize_t ret = 0;
+ unsigned int i;
+
+ offset = bstream->seeked ? stream->ostream.offset : bstream->buf->used;
+
+ for (i = 0; i < iov_count; i++) {
+ left = bstream->ostream.max_buffer_size -
+ stream->ostream.offset;
+ n = I_MIN(left, iov[i].iov_len);
+ buffer_write(bstream->buf, offset, iov[i].iov_base, n);
+ stream->ostream.offset += n; offset += n;
+ ret += n;
+ if (n != iov[i].iov_len)
+ break;
+ }
+ return ret;
+}
+
+static size_t
+o_stream_buffer_get_buffer_used_size(const struct ostream_private *stream)
+{
+ const struct buffer_ostream *bstream =
+ container_of(stream, const struct buffer_ostream, ostream);
+
+ return bstream->buf->used;
+}
+
+struct ostream *o_stream_create_buffer(buffer_t *buf)
+{
+ struct buffer_ostream *bstream;
+ struct ostream *output;
+
+ bstream = i_new(struct buffer_ostream, 1);
+ /* we don't set buffer as blocking, because if max_buffer_size is
+ changed it can get truncated. this is used in various places in
+ unit tests. */
+ bstream->ostream.max_buffer_size = SIZE_MAX;
+ bstream->ostream.seek = o_stream_buffer_seek;
+ bstream->ostream.sendv = o_stream_buffer_sendv;
+ bstream->ostream.write_at = o_stream_buffer_write_at;
+ bstream->ostream.get_buffer_used_size =
+ o_stream_buffer_get_buffer_used_size;
+
+ bstream->buf = buf;
+ output = o_stream_create(&bstream->ostream, NULL, -1);
+ o_stream_set_name(output, "(buffer)");
+ return output;
+}
diff --git a/src/lib/ostream-failure-at.c b/src/lib/ostream-failure-at.c
new file mode 100644
index 0000000..87bd7b8
--- /dev/null
+++ b/src/lib/ostream-failure-at.c
@@ -0,0 +1,123 @@
+/* Copyright (c) 2015-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "buffer.h"
+#include "ostream-private.h"
+#include "ostream-failure-at.h"
+
+struct failure_at_ostream {
+ struct ostream_private ostream;
+ char *error_string;
+ uoff_t failure_offset;
+ bool failed;
+};
+
+static void o_stream_failure_at_destroy(struct iostream_private *stream)
+{
+ struct failure_at_ostream *fstream =
+ container_of(stream, struct failure_at_ostream,
+ ostream.iostream);
+
+ i_free(fstream->error_string);
+ o_stream_unref(&fstream->ostream.parent);
+}
+
+static ssize_t
+o_stream_failure_at_sendv(struct ostream_private *stream,
+ const struct const_iovec *iov, unsigned int iov_count)
+{
+ struct failure_at_ostream *fstream =
+ container_of(stream, struct failure_at_ostream, ostream);
+ unsigned int i;
+ struct const_iovec *iov_dup;
+ unsigned int iov_dup_count;
+ uoff_t bytes_until_failure, blocking_bytes_count = 0;
+ ssize_t ret;
+
+ if (stream->ostream.blocking) {
+ /* blocking ostream must return either a full success or a
+ failure. if the current write would go past failure_offset,
+ return a failure now before writing anything. */
+ for (i = 0; i < iov_count; i++)
+ blocking_bytes_count += iov[i].iov_len;
+ if (blocking_bytes_count > 0) {
+ /* if we're exactly at the failure offset after this
+ write, fail it only on the next write. */
+ blocking_bytes_count--;
+ }
+ }
+
+ if (fstream->failure_offset <= stream->ostream.offset + blocking_bytes_count) {
+ io_stream_set_error(&stream->iostream, "%s",
+ fstream->error_string);
+ stream->ostream.stream_errno = errno = EIO;
+ fstream->failed = TRUE;
+ return -1;
+ }
+ bytes_until_failure = fstream->failure_offset - stream->ostream.offset;
+
+ iov_dup = i_new(struct const_iovec, iov_count);
+ iov_dup_count = iov_count;
+ for (i = 0; i < iov_count; i++) {
+ iov_dup[i] = iov[i];
+ if (iov_dup[i].iov_len >= bytes_until_failure) {
+ iov_dup[i].iov_len = bytes_until_failure;
+ iov_dup_count = i+1;
+ break;
+ }
+ }
+ ret = o_stream_sendv(stream->parent, iov_dup, iov_dup_count);
+ i_free(iov_dup);
+
+ if (ret < 0) {
+ o_stream_copy_error_from_parent(stream);
+ return -1;
+ }
+ stream->ostream.offset += ret;
+ return ret;
+}
+
+static int
+o_stream_failure_at_flush(struct ostream_private *stream)
+{
+ struct failure_at_ostream *fstream =
+ container_of(stream, struct failure_at_ostream, ostream);
+
+ if (fstream->failed) {
+ io_stream_set_error(&stream->iostream, "%s",
+ fstream->error_string);
+ stream->ostream.stream_errno = errno = EIO;
+ return -1;
+ }
+ return o_stream_flush_parent(stream);
+}
+
+struct ostream *
+o_stream_create_failure_at(struct ostream *output, uoff_t failure_offset,
+ const char *error_string)
+{
+ struct failure_at_ostream *fstream;
+
+ fstream = i_new(struct failure_at_ostream, 1);
+ fstream->ostream.sendv = o_stream_failure_at_sendv;
+ fstream->ostream.flush = o_stream_failure_at_flush;
+ fstream->ostream.iostream.destroy = o_stream_failure_at_destroy;
+ fstream->failure_offset = failure_offset;
+ fstream->error_string = i_strdup(error_string);
+ return o_stream_create(&fstream->ostream, output,
+ o_stream_get_fd(output));
+}
+
+struct ostream *
+o_stream_create_failure_at_flush(struct ostream *output, const char *error_string)
+{
+ struct failure_at_ostream *fstream;
+
+ fstream = i_new(struct failure_at_ostream, 1);
+ fstream->ostream.flush = o_stream_failure_at_flush;
+ fstream->ostream.iostream.destroy = o_stream_failure_at_destroy;
+ fstream->error_string = i_strdup(error_string);
+ fstream->failed = TRUE;
+ return o_stream_create(&fstream->ostream, output,
+ o_stream_get_fd(output));
+}
diff --git a/src/lib/ostream-failure-at.h b/src/lib/ostream-failure-at.h
new file mode 100644
index 0000000..61e1d72
--- /dev/null
+++ b/src/lib/ostream-failure-at.h
@@ -0,0 +1,10 @@
+#ifndef OSTREAM_FAILURE_AT_H
+#define OSTREAM_FAILURE_AT_H
+
+struct ostream *
+o_stream_create_failure_at(struct ostream *output, uoff_t failure_offset,
+ const char *error_string);
+struct ostream *
+o_stream_create_failure_at_flush(struct ostream *output, const char *error_string);
+
+#endif
diff --git a/src/lib/ostream-file-private.h b/src/lib/ostream-file-private.h
new file mode 100644
index 0000000..2d58933
--- /dev/null
+++ b/src/lib/ostream-file-private.h
@@ -0,0 +1,45 @@
+#ifndef OSTREAM_FILE_PRIVATE_H
+#define OSTREAM_FILE_PRIVATE_H
+
+#include "ostream-private.h"
+
+struct file_ostream {
+ struct ostream_private ostream;
+
+ ssize_t (*writev)(struct file_ostream *fstream,
+ const struct const_iovec *iov,
+ unsigned int iov_count);
+
+ int fd;
+ struct io *io;
+ uoff_t buffer_offset;
+ uoff_t real_offset;
+
+ unsigned char *buffer; /* ring-buffer */
+ size_t buffer_size, optimal_block_size;
+ size_t head, tail; /* first unsent/unused byte */
+
+ bool full:1; /* if head == tail, is buffer empty or full? */
+ bool file:1;
+ bool flush_pending:1;
+ bool socket_cork_set:1;
+ bool no_socket_cork:1;
+ bool no_socket_nodelay:1;
+ bool no_socket_quickack:1;
+ bool no_sendfile:1;
+ bool autoclose_fd:1;
+};
+
+struct ostream *
+o_stream_create_file_common(struct file_ostream *fstream,
+ int fd, size_t max_buffer_size, bool autoclose_fd);
+ssize_t o_stream_file_writev(struct file_ostream *fstream,
+ const struct const_iovec *iov,
+ unsigned int iov_size);
+ssize_t o_stream_file_sendv(struct ostream_private *stream,
+ const struct const_iovec *iov,
+ unsigned int iov_count);
+void o_stream_file_close(struct iostream_private *stream,
+ bool close_parent);
+
+#endif
diff --git a/src/lib/ostream-file.c b/src/lib/ostream-file.c
new file mode 100644
index 0000000..2be00d2
--- /dev/null
+++ b/src/lib/ostream-file.c
@@ -0,0 +1,1154 @@
+/* Copyright (c) 2002-2018 Dovecot authors, see the included COPYING file */
+
+/* @UNSAFE: whole file */
+
+#include "lib.h"
+#include "ioloop.h"
+#include "write-full.h"
+#include "net.h"
+#include "sendfile-util.h"
+#include "istream.h"
+#include "istream-private.h"
+#include "ostream-file-private.h"
+
+#include <unistd.h>
+#include <sys/stat.h>
+#ifdef HAVE_SYS_UIO_H
+# include <sys/uio.h>
+#endif
+#include <fcntl.h>
+
+/* try to keep the buffer size within 4k..128k. ReiserFS may actually return
+ 128k as optimal size. */
+#define DEFAULT_OPTIMAL_BLOCK_SIZE IO_BLOCK_SIZE
+#define MAX_OPTIMAL_BLOCK_SIZE (128*1024)
+
+#define IS_STREAM_EMPTY(fstream) \
+ ((fstream)->head == (fstream)->tail && !(fstream)->full)
+
+#define MAX_SSIZE_T(size) \
+ ((size) < SSIZE_T_MAX ? (size_t)(size) : SSIZE_T_MAX)
+
+static void stream_send_io(struct file_ostream *fstream);
+static struct ostream * o_stream_create_fd_common(int fd,
+ size_t max_buffer_size, bool autoclose_fd);
+
+static void stream_closed(struct file_ostream *fstream)
+{
+ io_remove(&fstream->io);
+
+ if (fstream->autoclose_fd && fstream->fd != -1) {
+ /* Ignore ECONNRESET because we don't really care about it here,
+ as we are closing the socket down in any case. There might be
+ unsent data but nothing we can do about that. */
+ if (unlikely(close(fstream->fd) < 0 && errno != ECONNRESET)) {
+ i_error("file_ostream.close(%s) failed: %m",
+ o_stream_get_name(&fstream->ostream.ostream));
+ }
+ }
+ fstream->fd = -1;
+
+ fstream->ostream.ostream.closed = TRUE;
+}
+
+void o_stream_file_close(struct iostream_private *stream,
+ bool close_parent ATTR_UNUSED)
+{
+ struct file_ostream *fstream =
+ container_of(stream, struct file_ostream, ostream.iostream);
+
+ stream_closed(fstream);
+}
+
+static void o_stream_file_destroy(struct iostream_private *stream)
+{
+ struct file_ostream *fstream =
+ container_of(stream, struct file_ostream, ostream.iostream);
+
+ i_free(fstream->buffer);
+}
+
+static size_t file_buffer_get_used_size(struct file_ostream *fstream)
+{
+ if (fstream->head == fstream->tail)
+ return fstream->full ? fstream->buffer_size : 0;
+ else if (fstream->head < fstream->tail) {
+ /* ...HXXXT... */
+ return fstream->tail - fstream->head;
+ } else {
+ /* XXXT...HXXX */
+ return fstream->tail +
+ (fstream->buffer_size - fstream->head);
+ }
+}
+
+static void update_buffer(struct file_ostream *fstream, size_t size)
+{
+ size_t used;
+
+ if (IS_STREAM_EMPTY(fstream) || size == 0)
+ return;
+
+ if (fstream->head < fstream->tail) {
+ /* ...HXXXT... */
+ used = fstream->tail - fstream->head;
+ i_assert(size <= used);
+ fstream->head += size;
+ } else {
+ /* XXXT...HXXX */
+ used = fstream->buffer_size - fstream->head;
+ if (size > used) {
+ size -= used;
+ i_assert(size <= fstream->tail);
+ fstream->head = size;
+ } else {
+ fstream->head += size;
+ }
+
+ fstream->full = FALSE;
+ }
+
+ if (fstream->head == fstream->tail)
+ fstream->head = fstream->tail = 0;
+
+ if (fstream->head == fstream->buffer_size)
+ fstream->head = 0;
+}
+
+static void o_stream_socket_cork(struct file_ostream *fstream)
+{
+ if (fstream->ostream.corked && !fstream->socket_cork_set) {
+ if (!fstream->no_socket_cork) {
+ if (net_set_cork(fstream->fd, TRUE) < 0)
+ fstream->no_socket_cork = TRUE;
+ else
+ fstream->socket_cork_set = TRUE;
+ }
+ }
+}
+
+static int o_stream_lseek(struct file_ostream *fstream)
+{
+ off_t ret;
+
+ if (fstream->real_offset == fstream->buffer_offset)
+ return 0;
+
+ ret = lseek(fstream->fd, (off_t)fstream->buffer_offset, SEEK_SET);
+ if (ret < 0) {
+ io_stream_set_error(&fstream->ostream.iostream,
+ "lseek() failed: %m");
+ fstream->ostream.ostream.stream_errno = errno;
+ return -1;
+ }
+
+ if (ret != (off_t)fstream->buffer_offset) {
+ io_stream_set_error(&fstream->ostream.iostream,
+ "lseek() returned wrong value");
+ fstream->ostream.ostream.stream_errno = EINVAL;
+ return -1;
+ }
+ fstream->real_offset = fstream->buffer_offset;
+ return 0;
+}
+
+ssize_t o_stream_file_writev(struct file_ostream *fstream,
+ const struct const_iovec *iov,
+ unsigned int iov_count)
+{
+ ssize_t ret;
+ size_t size, sent;
+ unsigned int i;
+
+ if (iov_count == 1) {
+ i_assert(iov->iov_len > 0);
+
+ if (!fstream->file ||
+ fstream->real_offset == fstream->buffer_offset) {
+ ret = write(fstream->fd, iov->iov_base, iov->iov_len);
+ if (ret > 0)
+ fstream->real_offset += ret;
+ } else {
+ ret = pwrite(fstream->fd, iov->iov_base, iov->iov_len,
+ fstream->buffer_offset);
+ }
+ } else {
+ if (o_stream_lseek(fstream) < 0)
+ return -1;
+
+ sent = 0;
+ while (iov_count > IOV_MAX) {
+ size = 0;
+ for (i = 0; i < IOV_MAX; i++)
+ size += iov[i].iov_len;
+
+ ret = writev(fstream->fd, (const struct iovec *)iov,
+ IOV_MAX);
+ if (ret != (ssize_t)size) {
+ break;
+ }
+
+ fstream->real_offset += ret;
+ fstream->buffer_offset += ret;
+ sent += ret;
+ iov += IOV_MAX;
+ iov_count -= IOV_MAX;
+ }
+
+ if (iov_count <= IOV_MAX) {
+ size = 0;
+ for (i = 0; i < iov_count; i++)
+ size += iov[i].iov_len;
+
+ ret = writev(fstream->fd, (const struct iovec *)iov,
+ iov_count);
+ }
+ if (ret > 0) {
+ fstream->real_offset += ret;
+ ret += sent;
+ } else if (!fstream->file && sent > 0) {
+ /* return what we managed to get sent */
+ ret = sent;
+ }
+ }
+ return ret;
+}
+
+static ssize_t
+o_stream_file_writev_full(struct file_ostream *fstream,
+ const struct const_iovec *iov,
+ unsigned int iov_count)
+{
+ ssize_t ret, ret2;
+ size_t size, total_size;
+ bool partial;
+ unsigned int i;
+
+ for (i = 0, total_size = 0; i < iov_count; i++)
+ total_size += iov[i].iov_len;
+
+ o_stream_socket_cork(fstream);
+ ret = fstream->writev(fstream, iov, iov_count);
+ partial = ret != (ssize_t)total_size;
+
+ if (ret < 0) {
+ if (fstream->file) {
+ if (errno == EINTR) {
+ /* automatically retry */
+ return o_stream_file_writev_full(fstream, iov, iov_count);
+ }
+ } else if (errno == EAGAIN || errno == EINTR) {
+ /* try again later */
+ return 0;
+ }
+ fstream->ostream.ostream.stream_errno = errno;
+ stream_closed(fstream);
+ return -1;
+ }
+ if (unlikely(ret == 0 && fstream->file)) {
+ /* assume out of disk space */
+ fstream->ostream.ostream.stream_errno = ENOSPC;
+ stream_closed(fstream);
+ return -1;
+ }
+ fstream->buffer_offset += ret;
+ if (partial && fstream->file) {
+ /* we failed to write everything to a file. either we ran out
+ of disk space or we're writing to NFS. try to write the
+ rest to resolve this. */
+ size = ret;
+ while (iov_count > 0 && size >= iov->iov_len) {
+ size -= iov->iov_len;
+ iov++;
+ iov_count--;
+ }
+ i_assert(iov_count > 0);
+ if (size == 0)
+ ret2 = o_stream_file_writev_full(fstream, iov, iov_count);
+ else {
+ /* write the first iov separately */
+ struct const_iovec new_iov;
+
+ new_iov.iov_base =
+ CONST_PTR_OFFSET(iov->iov_base, size);
+ new_iov.iov_len = iov->iov_len - size;
+ ret2 = o_stream_file_writev_full(fstream, &new_iov, 1);
+ if (ret2 > 0) {
+ i_assert((size_t)ret2 == new_iov.iov_len);
+ /* write the rest */
+ if (iov_count > 1) {
+ ret += ret2;
+ ret2 = o_stream_file_writev_full(fstream, iov + 1,
+ iov_count - 1);
+ }
+ }
+ }
+ i_assert(ret2 != 0);
+ if (ret2 < 0)
+ ret = ret2;
+ else
+ ret += ret2;
+ }
+ i_assert(ret < 0 || !fstream->file ||
+ (size_t)ret == total_size);
+ return ret;
+}
+
+/* returns how much of vector was used */
+static int o_stream_fill_iovec(struct file_ostream *fstream,
+ struct const_iovec iov[2])
+{
+ if (IS_STREAM_EMPTY(fstream))
+ return 0;
+
+ if (fstream->head < fstream->tail) {
+ iov[0].iov_base = fstream->buffer + fstream->head;
+ iov[0].iov_len = fstream->tail - fstream->head;
+ return 1;
+ } else {
+ iov[0].iov_base = fstream->buffer + fstream->head;
+ iov[0].iov_len = fstream->buffer_size - fstream->head;
+ if (fstream->tail == 0)
+ return 1;
+ else {
+ iov[1].iov_base = fstream->buffer;
+ iov[1].iov_len = fstream->tail;
+ return 2;
+ }
+ }
+}
+
+static int buffer_flush(struct file_ostream *fstream)
+{
+ struct const_iovec iov[2];
+ int iov_len;
+ ssize_t ret;
+
+ iov_len = o_stream_fill_iovec(fstream, iov);
+ if (iov_len > 0) {
+ ret = o_stream_file_writev_full(fstream, iov, iov_len);
+ if (ret < 0)
+ return -1;
+
+ update_buffer(fstream, ret);
+ }
+
+ return IS_STREAM_EMPTY(fstream) ? 1 : 0;
+}
+
+static void o_stream_tcp_flush_via_nodelay(struct file_ostream *fstream)
+{
+ if (net_set_tcp_nodelay(fstream->fd, TRUE) < 0) {
+ /* Don't bother logging errors. There are quite a lot of
+ different errors that need to be ignored, and it differs
+ between OSes. At least:
+ Linux: ENOTSUP, ENOTSOCK, ENOPROTOOPT
+ FreeBSD: EINVAL, ECONNRESET */
+ fstream->no_socket_nodelay = TRUE;
+ } else if (net_set_tcp_nodelay(fstream->fd, FALSE) < 0) {
+ /* We already successfully enabled TCP_NODELAY, so there
+ shouldn't really be errors. Except ECONNRESET can possibly
+ still happen between these two calls, so again don't log
+ errors. */
+ fstream->no_socket_nodelay = TRUE;
+ }
+}
+
+static void o_stream_file_cork(struct ostream_private *stream, bool set)
+{
+ struct file_ostream *fstream =
+ container_of(stream, struct file_ostream, ostream);
+ struct iostream_private *iostream = &fstream->ostream.iostream;
+ int ret;
+
+ if (stream->corked != set && !stream->ostream.closed) {
+ if (set && fstream->io != NULL)
+ io_remove(&fstream->io);
+ else if (!set) {
+ /* buffer flushing might close the stream */
+ ret = buffer_flush(fstream);
+ stream->last_errors_not_checked = TRUE;
+ if (fstream->io == NULL &&
+ (ret == 0 || fstream->flush_pending) &&
+ !stream->ostream.closed) {
+ fstream->io = io_add_to(
+ io_stream_get_ioloop(iostream),
+ fstream->fd, IO_WRITE,
+ stream_send_io, fstream);
+ }
+ }
+ if (stream->ostream.closed) {
+ /* flushing may have closed the stream already */
+ return;
+ }
+
+ if (fstream->socket_cork_set) {
+ i_assert(!set);
+ if (net_set_cork(fstream->fd, FALSE) < 0)
+ fstream->no_socket_cork = TRUE;
+ fstream->socket_cork_set = FALSE;
+ }
+ if (!set && !fstream->no_socket_nodelay) {
+ /* Uncorking - send all the pending data immediately.
+ Remove nodelay immediately afterwards, so if any
+ output is sent outside corking it may get delayed. */
+ o_stream_tcp_flush_via_nodelay(fstream);
+ }
+ if (!set && !fstream->no_socket_quickack) {
+ /* Uncorking - disable delayed ACKs to reduce latency.
+ Note that this needs to be set repeatedly. */
+ if (net_set_tcp_quickack(fstream->fd, TRUE) < 0)
+ fstream->no_socket_quickack = TRUE;
+ }
+ stream->corked = set;
+ }
+}
+
+static int o_stream_file_flush(struct ostream_private *stream)
+{
+ struct file_ostream *fstream =
+ container_of(stream, struct file_ostream, ostream);
+
+ return buffer_flush(fstream);
+}
+
+static void
+o_stream_file_flush_pending(struct ostream_private *stream, bool set)
+{
+ struct file_ostream *fstream =
+ container_of(stream, struct file_ostream, ostream);
+ struct iostream_private *iostream = &fstream->ostream.iostream;
+
+ fstream->flush_pending = set;
+ if (set && !stream->corked && fstream->io == NULL) {
+ fstream->io = io_add_to(io_stream_get_ioloop(iostream),
+ fstream->fd, IO_WRITE,
+ stream_send_io, fstream);
+ }
+}
+
+static size_t get_unused_space(const struct file_ostream *fstream)
+{
+ if (fstream->head > fstream->tail) {
+ /* XXXT...HXXX */
+ return fstream->head - fstream->tail;
+ } else if (fstream->head < fstream->tail) {
+ /* ...HXXXT... */
+ return (fstream->buffer_size - fstream->tail) + fstream->head;
+ } else {
+ /* either fully unused or fully used */
+ return fstream->full ? 0 : fstream->buffer_size;
+ }
+}
+
+static size_t
+o_stream_file_get_buffer_used_size(const struct ostream_private *stream)
+{
+ const struct file_ostream *fstream =
+ container_of(stream, const struct file_ostream, ostream);
+
+ return fstream->buffer_size - get_unused_space(fstream);
+}
+
+static int o_stream_file_seek(struct ostream_private *stream, uoff_t offset)
+{
+ struct file_ostream *fstream =
+ container_of(stream, struct file_ostream, ostream);
+
+ if (offset > OFF_T_MAX) {
+ stream->ostream.stream_errno = EINVAL;
+ return -1;
+ }
+ if (!fstream->file) {
+ stream->ostream.stream_errno = ESPIPE;
+ return -1;
+ }
+
+ if (buffer_flush(fstream) < 0)
+ return -1;
+
+ stream->ostream.offset = offset;
+ fstream->buffer_offset = offset;
+ return 1;
+}
+
+static void o_stream_grow_buffer(struct file_ostream *fstream, size_t bytes)
+{
+ size_t size, new_size, end_size;
+
+ size = nearest_power(fstream->buffer_size + bytes);
+ if (size > fstream->ostream.max_buffer_size) {
+ /* limit the size */
+ size = fstream->ostream.max_buffer_size;
+ } else if (fstream->ostream.corked) {
+ /* try to use optimal buffer size with corking */
+ new_size = I_MIN(fstream->optimal_block_size,
+ fstream->ostream.max_buffer_size);
+ if (new_size > size)
+ size = new_size;
+ }
+
+ if (size <= fstream->buffer_size)
+ return;
+
+ fstream->buffer = i_realloc(fstream->buffer,
+ fstream->buffer_size, size);
+
+ if (fstream->tail <= fstream->head && !IS_STREAM_EMPTY(fstream)) {
+ /* move head forward to end of buffer */
+ end_size = fstream->buffer_size - fstream->head;
+ memmove(fstream->buffer + size - end_size,
+ fstream->buffer + fstream->head, end_size);
+ fstream->head = size - end_size;
+ }
+
+ fstream->full = FALSE;
+ fstream->buffer_size = size;
+}
+
+static void stream_send_io(struct file_ostream *fstream)
+{
+ struct ostream *ostream = &fstream->ostream.ostream;
+ struct iostream_private *iostream = &fstream->ostream.iostream;
+ bool use_cork = !fstream->ostream.corked;
+ int ret;
+
+ /* Set flush_pending = FALSE first before calling the flush callback,
+ and change it to TRUE only if callback returns 0. That way the
+ callback can call o_stream_set_flush_pending() again and we don't
+ forget it even if flush callback returns 1. */
+ fstream->flush_pending = FALSE;
+
+ o_stream_ref(ostream);
+ if (use_cork)
+ o_stream_cork(ostream);
+ if (fstream->ostream.callback != NULL)
+ ret = fstream->ostream.callback(fstream->ostream.context);
+ else
+ ret = o_stream_file_flush(&fstream->ostream);
+ if (use_cork)
+ o_stream_uncork(ostream);
+
+ if (ret == 0)
+ fstream->flush_pending = TRUE;
+
+ if (!fstream->flush_pending && IS_STREAM_EMPTY(fstream)) {
+ io_remove(&fstream->io);
+ } else if (!fstream->ostream.ostream.closed) {
+ /* Add the IO handler if it's not there already. Callback
+ might have just returned 0 without there being any data
+ to be sent. */
+ if (fstream->io == NULL) {
+ fstream->io = io_add_to(io_stream_get_ioloop(iostream),
+ fstream->fd, IO_WRITE,
+ stream_send_io, fstream);
+ }
+ }
+
+ o_stream_unref(&ostream);
+}
+
+static size_t o_stream_add(struct file_ostream *fstream,
+ const void *data, size_t size)
+{
+ struct iostream_private *iostream = &fstream->ostream.iostream;
+ size_t unused, sent;
+ int i;
+
+ unused = get_unused_space(fstream);
+ if (unused < size)
+ o_stream_grow_buffer(fstream, size-unused);
+
+ sent = 0;
+ for (i = 0; i < 2 && sent < size && !fstream->full; i++) {
+ unused = fstream->tail >= fstream->head ?
+ fstream->buffer_size - fstream->tail :
+ fstream->head - fstream->tail;
+
+ if (unused > size-sent)
+ unused = size-sent;
+ memcpy(fstream->buffer + fstream->tail,
+ CONST_PTR_OFFSET(data, sent), unused);
+ sent += unused;
+
+ fstream->tail += unused;
+ if (fstream->tail == fstream->buffer_size)
+ fstream->tail = 0;
+
+ if (fstream->head == fstream->tail &&
+ fstream->buffer_size > 0)
+ fstream->full = TRUE;
+ }
+
+ if (sent != 0 && fstream->io == NULL &&
+ !fstream->ostream.corked && !fstream->file) {
+ fstream->io = io_add_to(io_stream_get_ioloop(iostream),
+ fstream->fd, IO_WRITE, stream_send_io,
+ fstream);
+ }
+
+ return sent;
+}
+
+ssize_t o_stream_file_sendv(struct ostream_private *stream,
+ const struct const_iovec *iov,
+ unsigned int iov_count)
+{
+ struct file_ostream *fstream =
+ container_of(stream, struct file_ostream, ostream);
+ size_t size, total_size, added, optimal_size;
+ unsigned int i;
+ ssize_t ret = 0;
+
+ for (i = 0, size = 0; i < iov_count; i++)
+ size += iov[i].iov_len;
+ total_size = size;
+
+ if (size > get_unused_space(fstream) && !IS_STREAM_EMPTY(fstream)) {
+ if (o_stream_file_flush(stream) < 0)
+ return -1;
+ }
+
+ optimal_size = I_MIN(fstream->optimal_block_size,
+ fstream->ostream.max_buffer_size);
+ if (IS_STREAM_EMPTY(fstream) &&
+ (!stream->corked || size >= optimal_size)) {
+ /* send immediately */
+ ret = o_stream_file_writev_full(fstream, iov, iov_count);
+ if (ret < 0)
+ return -1;
+
+ size = ret;
+ while (size > 0 && iov_count > 0 && size >= iov[0].iov_len) {
+ size -= iov[0].iov_len;
+ iov++;
+ iov_count--;
+ }
+
+ if (iov_count == 0)
+ i_assert(size == 0);
+ else {
+ added = o_stream_add(fstream,
+ CONST_PTR_OFFSET(iov[0].iov_base, size),
+ iov[0].iov_len - size);
+ ret += added;
+
+ if (added != iov[0].iov_len - size) {
+ /* buffer full */
+ stream->ostream.offset += ret;
+ return ret;
+ }
+
+ iov++;
+ iov_count--;
+ }
+ }
+
+ /* buffer it, at least partly */
+ for (i = 0; i < iov_count; i++) {
+ added = o_stream_add(fstream, iov[i].iov_base, iov[i].iov_len);
+ ret += added;
+ if (added != iov[i].iov_len)
+ break;
+ }
+ stream->ostream.offset += ret;
+ i_assert((size_t)ret <= total_size);
+ i_assert((size_t)ret == total_size || !fstream->file);
+ return ret;
+}
+
+static size_t
+o_stream_file_update_buffer(struct file_ostream *fstream,
+ const void *data, size_t size, size_t pos)
+{
+ size_t avail, copy_size;
+
+ if (fstream->head < fstream->tail) {
+ /* ...HXXXT... */
+ i_assert(pos < fstream->tail);
+ avail = fstream->tail - pos;
+ } else {
+ /* XXXT...HXXX */
+ avail = fstream->buffer_size - pos;
+ }
+ copy_size = I_MIN(size, avail);
+ memcpy(fstream->buffer + pos, data, copy_size);
+ data = CONST_PTR_OFFSET(data, copy_size);
+ size -= copy_size;
+
+ if (size > 0 && fstream->head >= fstream->tail) {
+ /* wraps to beginning of the buffer */
+ copy_size = I_MIN(size, fstream->tail);
+ memcpy(fstream->buffer, data, copy_size);
+ size -= copy_size;
+ }
+ return size;
+}
+
+static int
+o_stream_file_write_at(struct ostream_private *stream,
+ const void *data, size_t size, uoff_t offset)
+{
+ struct file_ostream *fstream =
+ container_of(stream, struct file_ostream, ostream);
+ size_t used, pos, skip, left;
+
+ /* update buffer if the write overlaps it */
+ used = file_buffer_get_used_size(fstream);
+ if (used > 0 &&
+ fstream->buffer_offset < offset + size &&
+ fstream->buffer_offset + used > offset) {
+ if (fstream->buffer_offset <= offset) {
+ /* updating from the beginning */
+ skip = 0;
+ } else {
+ skip = fstream->buffer_offset - offset;
+ }
+ pos = (fstream->head + offset + skip - fstream->buffer_offset) %
+ fstream->buffer_size;
+ left = o_stream_file_update_buffer(fstream,
+ CONST_PTR_OFFSET(data, skip), size - skip, pos);
+ if (left > 0) {
+ /* didn't write all of it */
+ if (skip > 0) {
+ /* we also have to write a prefix. don't
+ bother with two syscalls, just write all
+ of it in one pwrite(). */
+ } else {
+ /* write only the suffix */
+ size_t update_count = size - left;
+
+ data = CONST_PTR_OFFSET(data, update_count);
+ size -= update_count;
+ offset += update_count;
+ }
+ } else if (skip == 0) {
+ /* everything done */
+ return 0;
+ } else {
+ /* still have to write prefix */
+ size = skip;
+ }
+ }
+
+ /* we couldn't write everything to the buffer. flush the buffer
+ and pwrite() the rest. */
+ if (o_stream_file_flush(stream) < 0)
+ return -1;
+
+ if (pwrite_full(fstream->fd, data, size, offset) < 0) {
+ stream->ostream.stream_errno = errno;
+ stream_closed(fstream);
+ return -1;
+ }
+ return 0;
+}
+
+static bool
+io_stream_sendfile(struct ostream_private *outstream,
+ struct istream *instream, int in_fd,
+ enum ostream_send_istream_result *res_r)
+{
+ struct file_ostream *foutstream =
+ container_of(outstream, struct file_ostream, ostream);
+ uoff_t in_size, offset, send_size, v_offset, abs_start_offset;
+ ssize_t ret;
+ bool sendfile_not_supported = FALSE;
+
+ if ((ret = i_stream_get_size(instream, TRUE, &in_size)) < 0) {
+ *res_r = OSTREAM_SEND_ISTREAM_RESULT_ERROR_INPUT;
+ return TRUE;
+ }
+ if (ret == 0) {
+ /* size unknown. we can't use sendfile(). */
+ return FALSE;
+ }
+
+ o_stream_socket_cork(foutstream);
+
+ /* flush out any data in buffer */
+ if ((ret = buffer_flush(foutstream)) < 0) {
+ *res_r = OSTREAM_SEND_ISTREAM_RESULT_ERROR_OUTPUT;
+ return TRUE;
+ } else if (ret == 0) {
+ *res_r = OSTREAM_SEND_ISTREAM_RESULT_WAIT_OUTPUT;
+ return TRUE;
+ }
+
+ if (o_stream_lseek(foutstream) < 0) {
+ *res_r = OSTREAM_SEND_ISTREAM_RESULT_ERROR_OUTPUT;
+ return TRUE;
+ }
+
+ v_offset = instream->v_offset;
+ abs_start_offset = i_stream_get_absolute_offset(instream) - v_offset;
+ while (v_offset < in_size) {
+ offset = abs_start_offset + v_offset;
+ send_size = in_size - v_offset;
+
+ ret = safe_sendfile(foutstream->fd, in_fd, &offset,
+ MAX_SSIZE_T(send_size));
+ if (ret <= 0) {
+ if (ret == 0) {
+ /* Unexpectedly early EOF at input */
+ i_stream_seek(instream, v_offset);
+ instream->eof = TRUE;
+ *res_r = OSTREAM_SEND_ISTREAM_RESULT_FINISHED;
+ return TRUE;
+ }
+ if (foutstream->file) {
+ if (errno == EINTR) {
+ /* automatically retry */
+ continue;
+ }
+ } else {
+ if (errno == EINTR || errno == EAGAIN) {
+ ret = 0;
+ break;
+ }
+ }
+ if (errno == EINVAL)
+ sendfile_not_supported = TRUE;
+ else {
+ io_stream_set_error(&outstream->iostream,
+ "sendfile() failed: %m");
+ outstream->ostream.stream_errno = errno;
+ /* close only if error wasn't because
+ sendfile() isn't supported */
+ stream_closed(foutstream);
+ }
+ break;
+ }
+
+ v_offset += ret;
+ foutstream->real_offset += ret;
+ foutstream->buffer_offset += ret;
+ outstream->ostream.offset += ret;
+ }
+
+ i_stream_seek(instream, v_offset);
+ if (v_offset == in_size) {
+ instream->eof = TRUE;
+ *res_r = OSTREAM_SEND_ISTREAM_RESULT_FINISHED;
+ return TRUE;
+ }
+ i_assert(ret <= 0);
+ if (sendfile_not_supported)
+ return FALSE;
+ if (ret < 0)
+ *res_r = OSTREAM_SEND_ISTREAM_RESULT_ERROR_OUTPUT;
+ else
+ *res_r = OSTREAM_SEND_ISTREAM_RESULT_WAIT_OUTPUT;
+ return TRUE;
+}
+
+static enum ostream_send_istream_result
+io_stream_copy_backwards(struct ostream_private *outstream,
+ struct istream *instream, uoff_t in_size)
+{
+ struct file_ostream *foutstream =
+ container_of(outstream, struct file_ostream, ostream);
+ uoff_t in_start_offset, in_offset, in_limit, out_offset;
+ const unsigned char *data;
+ size_t buffer_size, size, read_size;
+ ssize_t ret;
+
+ i_assert(IS_STREAM_EMPTY(foutstream));
+
+ /* figure out optimal buffer size */
+ buffer_size = instream->real_stream->buffer_size;
+ if (buffer_size == 0 || buffer_size > foutstream->buffer_size) {
+ if (foutstream->optimal_block_size > foutstream->buffer_size) {
+ o_stream_grow_buffer(foutstream,
+ foutstream->optimal_block_size -
+ foutstream->buffer_size);
+ }
+
+ buffer_size = foutstream->buffer_size;
+ }
+
+ in_start_offset = instream->v_offset;
+ in_offset = in_limit = in_size;
+ out_offset = outstream->ostream.offset + (in_offset - in_start_offset);
+
+ while (in_offset > in_start_offset) {
+ if (in_offset - in_start_offset <= buffer_size)
+ read_size = in_offset - in_start_offset;
+ else
+ read_size = buffer_size;
+ in_offset -= read_size;
+ out_offset -= read_size;
+
+ for (;;) {
+ i_assert(in_offset <= in_limit);
+
+ i_stream_seek(instream, in_offset);
+ read_size = in_limit - in_offset;
+
+ /* FIXME: something's wrong here */
+ if (i_stream_read_bytes(instream, &data, &size,
+ read_size) == 0)
+ i_unreached();
+ if (size >= read_size) {
+ size = read_size;
+ if (instream->mmaped) {
+ /* we'll have to write it through
+ buffer or the file gets corrupted */
+ i_assert(size <=
+ foutstream->buffer_size);
+ memcpy(foutstream->buffer, data, size);
+ data = foutstream->buffer;
+ }
+ break;
+ }
+
+ /* buffer too large probably, try with smaller */
+ read_size -= size;
+ in_offset += read_size;
+ out_offset += read_size;
+ buffer_size -= read_size;
+ }
+ in_limit -= size;
+
+ ret = pwrite_full(foutstream->fd, data, size, out_offset);
+ if (ret < 0) {
+ /* error */
+ outstream->ostream.stream_errno = errno;
+ return OSTREAM_SEND_ISTREAM_RESULT_WAIT_OUTPUT;
+ }
+ i_stream_skip(instream, size);
+ }
+ /* make it visible that we're at instream's EOF */
+ i_stream_seek(instream, in_size);
+ instream->eof = TRUE;
+
+ outstream->ostream.offset += in_size - in_start_offset;
+ return OSTREAM_SEND_ISTREAM_RESULT_FINISHED;
+}
+
+static enum ostream_send_istream_result
+io_stream_copy_same_stream(struct ostream_private *outstream,
+ struct istream *instream)
+{
+ uoff_t in_size;
+ off_t in_abs_offset, ret = 0;
+
+ /* copying data within same fd. we'll have to be careful with
+ seeks and overlapping writes. */
+ if ((ret = i_stream_get_size(instream, TRUE, &in_size)) < 0)
+ return OSTREAM_SEND_ISTREAM_RESULT_ERROR_INPUT;
+ if (ret == 0) {
+ /* if we couldn't find out the size, it means that instream
+ isn't a regular file_istream. we can be reasonably sure that
+ we can copy it safely the regular way. (there's really no
+ other possibility, other than failing completely.) */
+ return io_stream_copy(&outstream->ostream, instream);
+ }
+ i_assert(instream->v_offset <= in_size);
+
+ in_abs_offset = i_stream_get_absolute_offset(instream);
+ ret = (off_t)outstream->ostream.offset - in_abs_offset;
+ if (ret == 0) {
+ /* copying data over itself. we don't really
+ need to do that, just fake it. */
+ return OSTREAM_SEND_ISTREAM_RESULT_FINISHED;
+ }
+ if (ret > 0 && in_size > (uoff_t)ret) {
+ /* overlapping */
+ i_assert(instream->seekable);
+ return io_stream_copy_backwards(outstream, instream, in_size);
+ } else {
+ /* non-overlapping */
+ return io_stream_copy(&outstream->ostream, instream);
+ }
+}
+
+static enum ostream_send_istream_result
+o_stream_file_send_istream(struct ostream_private *outstream,
+ struct istream *instream)
+{
+ struct file_ostream *foutstream =
+ container_of(outstream, struct file_ostream, ostream);
+ bool same_stream;
+ int in_fd;
+ enum ostream_send_istream_result res;
+
+ in_fd = !instream->readable_fd ? -1 : i_stream_get_fd(instream);
+ if (!foutstream->no_sendfile && in_fd != -1 &&
+ in_fd != foutstream->fd && instream->seekable) {
+ if (io_stream_sendfile(outstream, instream, in_fd, &res))
+ return res;
+
+ /* sendfile() not supported (with this fd), fallback to
+ regular sending. */
+ foutstream->no_sendfile = TRUE;
+ }
+
+ same_stream = i_stream_get_fd(instream) == foutstream->fd &&
+ foutstream->fd != -1;
+ if (!same_stream)
+ return io_stream_copy(&outstream->ostream, instream);
+ return io_stream_copy_same_stream(outstream, instream);
+}
+
+static void o_stream_file_switch_ioloop_to(struct ostream_private *stream,
+ struct ioloop *ioloop)
+{
+ struct file_ostream *fstream =
+ container_of(stream, struct file_ostream, ostream);
+
+ if (fstream->io != NULL)
+ fstream->io = io_loop_move_io_to(ioloop, &fstream->io);
+}
+
+struct ostream *
+o_stream_create_file_common(struct file_ostream *fstream,
+ int fd, size_t max_buffer_size, bool autoclose_fd)
+{
+ struct ostream *ostream;
+
+ fstream->fd = fd;
+ fstream->autoclose_fd = autoclose_fd;
+ fstream->optimal_block_size = DEFAULT_OPTIMAL_BLOCK_SIZE;
+
+ fstream->ostream.iostream.close = o_stream_file_close;
+ fstream->ostream.iostream.destroy = o_stream_file_destroy;
+
+ fstream->ostream.cork = o_stream_file_cork;
+ fstream->ostream.flush = o_stream_file_flush;
+ fstream->ostream.flush_pending = o_stream_file_flush_pending;
+ fstream->ostream.get_buffer_used_size =
+ o_stream_file_get_buffer_used_size;
+ fstream->ostream.seek = o_stream_file_seek;
+ fstream->ostream.sendv = o_stream_file_sendv;
+ fstream->ostream.write_at = o_stream_file_write_at;
+ fstream->ostream.send_istream = o_stream_file_send_istream;
+ fstream->ostream.switch_ioloop_to = o_stream_file_switch_ioloop_to;
+
+ fstream->writev = o_stream_file_writev;
+
+ fstream->ostream.max_buffer_size = max_buffer_size;
+ ostream = o_stream_create(&fstream->ostream, NULL, fd);
+
+ if (max_buffer_size == 0)
+ fstream->ostream.max_buffer_size = fstream->optimal_block_size;
+
+ return ostream;
+}
+
+static void fstream_init_file(struct file_ostream *fstream)
+{
+ struct stat st;
+
+ fstream->no_sendfile = TRUE;
+ if (fstat(fstream->fd, &st) < 0)
+ return;
+
+ if ((uoff_t)st.st_blksize > fstream->optimal_block_size) {
+ /* use the optimal block size, but with a reasonable limit */
+ fstream->optimal_block_size =
+ I_MIN(st.st_blksize, MAX_OPTIMAL_BLOCK_SIZE);
+ }
+
+ if (S_ISREG(st.st_mode)) {
+ fstream->no_socket_cork = TRUE;
+ fstream->no_socket_nodelay = TRUE;
+ fstream->no_socket_quickack = TRUE;
+ fstream->file = TRUE;
+ }
+}
+
+static
+struct ostream * o_stream_create_fd_common(int fd, size_t max_buffer_size,
+ bool autoclose_fd)
+{
+ struct file_ostream *fstream;
+ struct ostream *ostream;
+ off_t offset;
+
+ fstream = i_new(struct file_ostream, 1);
+ ostream = o_stream_create_file_common
+ (fstream, fd, max_buffer_size, autoclose_fd);
+
+ offset = lseek(fd, 0, SEEK_CUR);
+ if (offset >= 0) {
+ ostream->offset = offset;
+ fstream->real_offset = offset;
+ fstream->buffer_offset = offset;
+ fstream_init_file(fstream);
+ } else {
+ struct ip_addr local_ip;
+
+ if (net_getsockname(fd, &local_ip, NULL) < 0) {
+ /* not a socket */
+ fstream->no_sendfile = TRUE;
+ fstream->no_socket_cork = TRUE;
+ fstream->no_socket_nodelay = TRUE;
+ fstream->no_socket_quickack = TRUE;
+ } else if (local_ip.family == 0) {
+ /* UNIX domain socket */
+ fstream->no_socket_cork = TRUE;
+ fstream->no_socket_nodelay = TRUE;
+ fstream->no_socket_quickack = TRUE;
+ }
+ }
+
+ return ostream;
+}
+
+struct ostream *
+o_stream_create_fd(int fd, size_t max_buffer_size)
+{
+ return o_stream_create_fd_common(fd, max_buffer_size, FALSE);
+}
+
+struct ostream *
+o_stream_create_fd_autoclose(int *fd, size_t max_buffer_size)
+{
+ struct ostream *ostream = o_stream_create_fd_common(*fd,
+ max_buffer_size, TRUE);
+ *fd = -1;
+ return ostream;
+}
+
+struct ostream *
+o_stream_create_fd_file(int fd, uoff_t offset, bool autoclose_fd)
+{
+ struct file_ostream *fstream;
+ struct ostream *ostream;
+
+ if (offset == UOFF_T_MAX)
+ offset = lseek(fd, 0, SEEK_CUR);
+
+ fstream = i_new(struct file_ostream, 1);
+ ostream = o_stream_create_file_common(fstream, fd, 0, autoclose_fd);
+ fstream_init_file(fstream);
+ fstream->real_offset = offset;
+ fstream->buffer_offset = offset;
+ ostream->blocking = fstream->file;
+ ostream->offset = offset;
+ return ostream;
+}
+
+struct ostream *o_stream_create_fd_file_autoclose(int *fd, uoff_t offset)
+{
+ struct ostream *output;
+
+ output = o_stream_create_fd_file(*fd, offset, TRUE);
+ *fd = -1;
+ return output;
+}
+
+struct ostream *o_stream_create_file(const char *path, uoff_t offset, mode_t mode,
+ enum ostream_create_file_flags flags)
+{
+ int fd;
+ int open_flags = O_WRONLY|O_CREAT;
+ if (HAS_ANY_BITS(flags, OSTREAM_CREATE_FILE_FLAG_APPEND))
+ open_flags |= O_APPEND;
+ else
+ open_flags |= O_TRUNC;
+ if ((fd = open(path, open_flags, mode)) < 0)
+ return o_stream_create_error(errno);
+ return o_stream_create_fd_file_autoclose(&fd, offset);
+}
diff --git a/src/lib/ostream-hash.c b/src/lib/ostream-hash.c
new file mode 100644
index 0000000..c83b43e
--- /dev/null
+++ b/src/lib/ostream-hash.c
@@ -0,0 +1,56 @@
+/* Copyright (c) 2013-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "hash-method.h"
+#include "ostream-private.h"
+#include "ostream-hash.h"
+
+struct hash_ostream {
+ struct ostream_private ostream;
+ const struct hash_method *method;
+ void *hash_context;
+};
+
+static ssize_t
+o_stream_hash_sendv(struct ostream_private *stream,
+ const struct const_iovec *iov, unsigned int iov_count)
+{
+ struct hash_ostream *hstream =
+ container_of(stream, struct hash_ostream, ostream);
+ unsigned int i;
+ size_t bytes_left, block_len;
+ ssize_t ret;
+
+ if ((ret = o_stream_sendv(stream->parent, iov, iov_count)) < 0) {
+ o_stream_copy_error_from_parent(stream);
+ return -1;
+ }
+ if (ret > 0) {
+ bytes_left = ret;
+ for (i = 0; i < iov_count && bytes_left > 0; i++) {
+ block_len = iov[i].iov_len <= bytes_left ?
+ iov[i].iov_len : bytes_left;
+ hstream->method->loop(hstream->hash_context,
+ iov[i].iov_base, block_len);
+ bytes_left -= block_len;
+ }
+ }
+
+ stream->ostream.offset += ret;
+ return ret;
+}
+
+struct ostream *
+o_stream_create_hash(struct ostream *output, const struct hash_method *method,
+ void *hash_context)
+{
+ struct hash_ostream *hstream;
+
+ hstream = i_new(struct hash_ostream, 1);
+ hstream->ostream.sendv = o_stream_hash_sendv;
+ hstream->method = method;
+ hstream->hash_context = hash_context;
+
+ return o_stream_create(&hstream->ostream, output,
+ o_stream_get_fd(output));
+}
diff --git a/src/lib/ostream-hash.h b/src/lib/ostream-hash.h
new file mode 100644
index 0000000..7fb4b6d
--- /dev/null
+++ b/src/lib/ostream-hash.h
@@ -0,0 +1,12 @@
+#ifndef OSTREAM_HASH_H
+#define OSTREAM_HASH_H
+
+struct hash_method;
+
+/* hash_context must be allocated and initialized by caller. This ostream will
+ simply call method->loop() for all the data going through the ostream. */
+struct ostream *
+o_stream_create_hash(struct ostream *output, const struct hash_method *method,
+ void *hash_context);
+
+#endif
diff --git a/src/lib/ostream-multiplex.c b/src/lib/ostream-multiplex.c
new file mode 100644
index 0000000..6c3bf85
--- /dev/null
+++ b/src/lib/ostream-multiplex.c
@@ -0,0 +1,367 @@
+/* Copyright (c) 2017-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "ioloop.h"
+#include "array.h"
+#include "ostream-private.h"
+#include "ostream-multiplex.h"
+
+/* all multiplex packets are [1 byte cid][4 byte length][data] */
+
+struct multiplex_ostream;
+
+struct multiplex_ochannel {
+ struct ostream_private ostream;
+ struct multiplex_ostream *mstream;
+ uint8_t cid;
+ buffer_t *buf;
+ uint64_t last_sent_counter;
+ bool closed:1;
+ bool corked:1;
+};
+
+struct multiplex_ostream {
+ struct ostream *parent;
+
+ stream_flush_callback_t *old_flush_callback;
+ void *old_flush_context;
+
+ /* channel 0 is main channel */
+ uint8_t cur_channel;
+ unsigned int remain;
+ size_t bufsize;
+ uint64_t send_counter;
+ ARRAY(struct multiplex_ochannel *) channels;
+
+ bool destroyed:1;
+};
+
+static struct multiplex_ochannel *
+get_channel(struct multiplex_ostream *mstream, uint8_t cid)
+{
+ struct multiplex_ochannel *channel;
+ i_assert(mstream != NULL);
+ array_foreach_elem(&mstream->channels, channel) {
+ if (channel != NULL && channel->cid == cid)
+ return channel;
+ }
+ return NULL;
+}
+
+static void propagate_error(struct multiplex_ostream *mstream, int stream_errno)
+{
+ struct multiplex_ochannel *channel;
+ array_foreach_elem(&mstream->channels, channel)
+ if (channel != NULL)
+ channel->ostream.ostream.stream_errno = stream_errno;
+}
+
+static struct multiplex_ochannel *get_next_channel(struct multiplex_ostream *mstream)
+{
+ struct multiplex_ochannel *oldest_channel = NULL;
+ struct multiplex_ochannel *channel;
+ uint64_t last_counter = mstream->send_counter;
+
+ array_foreach_elem(&mstream->channels, channel) {
+ if (channel != NULL &&
+ channel->last_sent_counter <= last_counter &&
+ channel->buf->used > 0) {
+ last_counter = channel->last_sent_counter;
+ oldest_channel = channel;
+ }
+ }
+ return oldest_channel;
+}
+
+static bool
+o_stream_multiplex_sendv(struct multiplex_ostream *mstream)
+{
+ struct multiplex_ochannel *channel;
+ ssize_t ret = 0;
+ bool all_sent = TRUE;
+
+ while((channel = get_next_channel(mstream)) != NULL) {
+ if (channel->buf->used == 0)
+ continue;
+ if (o_stream_get_buffer_avail_size(mstream->parent) < 6) {
+ all_sent = FALSE;
+ break;
+ }
+ /* check parent stream capacity */
+ size_t tmp = o_stream_get_buffer_avail_size(mstream->parent) - 5;
+ /* ensure it fits into 32 bit int */
+ size_t amt = I_MIN(UINT_MAX, I_MIN(tmp, channel->buf->used));
+ /* ensure amt fits */
+ if (tmp == 0)
+ break;
+ /* delay corking here now that we are going to send something */
+ if (!o_stream_is_corked(mstream->parent))
+ o_stream_cork(mstream->parent);
+ uint32_t len = cpu32_to_be(amt);
+ const struct const_iovec vec[] = {
+ { &channel->cid, 1 },
+ { &len, 4 },
+ { channel->buf->data, amt }
+ };
+ if ((ret = o_stream_sendv(mstream->parent, vec, N_ELEMENTS(vec))) < 0) {
+ propagate_error(mstream, mstream->parent->stream_errno);
+ break;
+ }
+ i_assert((size_t)ret == 1 + 4 + amt);
+ buffer_delete(channel->buf, 0, amt);
+ channel->last_sent_counter = ++mstream->send_counter;
+ }
+ if (o_stream_is_corked(mstream->parent))
+ o_stream_uncork(mstream->parent);
+ return all_sent;
+}
+
+static int o_stream_multiplex_flush(struct multiplex_ostream *mstream)
+{
+ int ret = o_stream_flush(mstream->parent);
+ if (ret >= 0) {
+ if (!o_stream_multiplex_sendv(mstream))
+ return 0;
+ }
+
+ /* a) Everything is flushed. See if one of the callbacks' flush
+ callbacks wants to write more data.
+ b) ostream failed. Notify the callbacks in case they need to know. */
+ struct multiplex_ochannel *channel;
+ bool unfinished = FALSE;
+ bool failed = FALSE;
+ array_foreach_elem(&mstream->channels, channel) {
+ if (channel != NULL && channel->ostream.callback != NULL) {
+ ret = channel->ostream.callback(channel->ostream.context);
+ if (ret < 0)
+ failed = TRUE;
+ else if (ret == 0)
+ unfinished = TRUE;
+ }
+ }
+ return failed ? -1 :
+ (unfinished ? 0 : 1);
+}
+
+static int o_stream_multiplex_ochannel_flush(struct ostream_private *stream)
+{
+ ssize_t ret;
+ struct multiplex_ochannel *channel =
+ container_of(stream, struct multiplex_ochannel, ostream);
+ struct multiplex_ostream *mstream = channel->mstream;
+
+ /* flush parent stream always, so there is room for more. */
+ if ((ret = o_stream_flush(mstream->parent)) <= 0) {
+ if (ret == -1)
+ propagate_error(mstream, mstream->parent->stream_errno);
+ return ret;
+ }
+
+ /* send all channels */
+ o_stream_multiplex_sendv(mstream);
+
+ if (channel->buf->used > 0)
+ return 0;
+ return 1;
+}
+
+static void o_stream_multiplex_ochannel_cork(struct ostream_private *stream, bool set)
+{
+ struct multiplex_ochannel *channel =
+ container_of(stream, struct multiplex_ochannel, ostream);
+ if (channel->corked != set && !set) {
+ /* flush */
+ (void)o_stream_multiplex_ochannel_flush(stream);
+ }
+ channel->corked = set;
+}
+
+static ssize_t
+o_stream_multiplex_ochannel_sendv(struct ostream_private *stream,
+ const struct const_iovec *iov, unsigned int iov_count)
+{
+ struct multiplex_ochannel *channel =
+ container_of(stream, struct multiplex_ochannel, ostream);
+ size_t total = 0, avail = o_stream_get_buffer_avail_size(&stream->ostream);
+ size_t optimal_size = I_MIN(IO_BLOCK_SIZE, avail);
+
+ for (unsigned int i = 0; i < iov_count; i++)
+ total += iov[i].iov_len;
+
+ if (avail < total) {
+ o_stream_multiplex_sendv(channel->mstream);
+ avail = o_stream_get_buffer_avail_size(&stream->ostream);
+ if (avail == 0)
+ return 0;
+ }
+
+ total = 0;
+
+ for (unsigned int i = 0; i < iov_count; i++) {
+ /* copy data to buffer */
+ size_t tmp = avail - total;
+ if (tmp == 0)
+ break;
+ buffer_append(channel->buf, iov[i].iov_base,
+ I_MIN(tmp, iov[i].iov_len));
+ total += I_MIN(tmp, iov[i].iov_len);
+ }
+
+ stream->ostream.offset += total;
+
+ /* will send later */
+ if (channel->corked && channel->buf->used < optimal_size)
+ return total;
+
+ o_stream_multiplex_sendv(channel->mstream);
+ return total;
+}
+
+static void
+o_stream_multiplex_ochannel_set_flush_callback(struct ostream_private *stream,
+ stream_flush_callback_t *callback,
+ void *context)
+{
+ /* We have overwritten our parent's flush-callback. Don't change it. */
+ stream->callback = callback;
+ stream->context = context;
+}
+
+static size_t
+o_stream_multiplex_ochannel_get_buffer_used_size(const struct ostream_private *stream)
+{
+ const struct multiplex_ochannel *channel =
+ container_of(stream, const struct multiplex_ochannel, ostream);
+
+ return channel->buf->used +
+ o_stream_get_buffer_used_size(channel->mstream->parent);
+}
+
+static size_t
+o_stream_multiplex_ochannel_get_buffer_avail_size(const struct ostream_private *stream)
+{
+ const struct multiplex_ochannel *channel =
+ container_of(stream, const struct multiplex_ochannel, ostream);
+ size_t max_avail = I_MIN(channel->mstream->bufsize,
+ o_stream_get_buffer_avail_size(stream->parent));
+
+ /* There is 5-byte overhead per message, so take that into account */
+ return max_avail <= (channel->buf->used + 5) ? 0 :
+ max_avail - (channel->buf->used + 5);
+}
+
+static void
+o_stream_multiplex_ochannel_close(struct iostream_private *stream, bool close_parent)
+{
+ struct multiplex_ochannel *arr_channel;
+ struct multiplex_ochannel *channel =
+ container_of(stream, struct multiplex_ochannel, ostream.iostream);
+
+ channel->closed = TRUE;
+ if (close_parent) {
+ array_foreach_elem(&channel->mstream->channels, arr_channel)
+ if (arr_channel != NULL && !arr_channel->closed)
+ return;
+ o_stream_close(channel->mstream->parent);
+ }
+}
+
+static void o_stream_multiplex_try_destroy(struct multiplex_ostream *mstream)
+{
+ struct multiplex_ochannel *channel;
+ /* can't do anything until they are all closed */
+ array_foreach_elem(&mstream->channels, channel)
+ if (channel != NULL)
+ return;
+
+ i_assert(mstream->parent->real_stream->callback ==
+ (stream_flush_callback_t *)o_stream_multiplex_flush);
+ o_stream_set_flush_callback(mstream->parent,
+ *mstream->old_flush_callback,
+ mstream->old_flush_context);
+ o_stream_unref(&mstream->parent);
+ array_free(&mstream->channels);
+ i_free(mstream);
+}
+
+static void o_stream_multiplex_ochannel_destroy(struct iostream_private *stream)
+{
+ struct multiplex_ochannel **channelp;
+ struct multiplex_ochannel *channel =
+ container_of(stream, struct multiplex_ochannel, ostream.iostream);
+ o_stream_unref(&channel->ostream.parent);
+ if (channel->buf != NULL)
+ buffer_free(&channel->buf);
+ /* delete the channel */
+ array_foreach_modifiable(&channel->mstream->channels, channelp) {
+ if (*channelp != NULL && (*channelp)->cid == channel->cid) {
+ *channelp = NULL;
+ break;
+ }
+ }
+ o_stream_multiplex_try_destroy(channel->mstream);
+}
+
+static struct ostream *
+o_stream_add_channel_real(struct multiplex_ostream *mstream, uint8_t cid)
+{
+ struct multiplex_ochannel *channel = i_new(struct multiplex_ochannel, 1);
+ channel->cid = cid;
+ channel->buf = buffer_create_dynamic(default_pool, 256);
+ channel->mstream = mstream;
+ channel->ostream.cork = o_stream_multiplex_ochannel_cork;
+ channel->ostream.flush = o_stream_multiplex_ochannel_flush;
+ channel->ostream.sendv = o_stream_multiplex_ochannel_sendv;
+ channel->ostream.set_flush_callback =
+ o_stream_multiplex_ochannel_set_flush_callback;
+ channel->ostream.get_buffer_used_size =
+ o_stream_multiplex_ochannel_get_buffer_used_size;
+ channel->ostream.get_buffer_avail_size =
+ o_stream_multiplex_ochannel_get_buffer_avail_size;
+ channel->ostream.iostream.close = o_stream_multiplex_ochannel_close;
+ channel->ostream.iostream.destroy = o_stream_multiplex_ochannel_destroy;
+ channel->ostream.fd = o_stream_get_fd(mstream->parent);
+ array_push_back(&channel->mstream->channels, &channel);
+
+ (void)o_stream_create(&channel->ostream, mstream->parent, -1);
+ /* o_stream_create() defaults the flush_callback to parent's callback.
+ Here it points to o_stream_multiplex_flush(), which just causes
+ infinite looping. */
+ channel->ostream.callback = NULL;
+ channel->ostream.context = NULL;
+ return &channel->ostream.ostream;
+}
+
+struct ostream *o_stream_multiplex_add_channel(struct ostream *stream, uint8_t cid)
+{
+ struct multiplex_ochannel *chan =
+ container_of(stream->real_stream, struct multiplex_ochannel,
+ ostream);
+ i_assert(get_channel(chan->mstream, cid) == NULL);
+
+ return o_stream_add_channel_real(chan->mstream, cid);
+}
+
+struct ostream *o_stream_create_multiplex(struct ostream *parent, size_t bufsize)
+{
+ struct multiplex_ostream *mstream;
+
+ mstream = i_new(struct multiplex_ostream, 1);
+ mstream->parent = parent;
+ mstream->bufsize = bufsize;
+ mstream->old_flush_callback = parent->real_stream->callback;
+ mstream->old_flush_context = parent->real_stream->context;
+ o_stream_set_flush_callback(parent, o_stream_multiplex_flush, mstream);
+ i_array_init(&mstream->channels, 8);
+ o_stream_ref(parent);
+
+ return o_stream_add_channel_real(mstream, 0);
+}
+
+uint8_t o_stream_multiplex_get_channel_id(struct ostream *stream)
+{
+ struct multiplex_ochannel *channel =
+ container_of(stream->real_stream, struct multiplex_ochannel,
+ ostream);
+ return channel->cid;
+}
diff --git a/src/lib/ostream-multiplex.h b/src/lib/ostream-multiplex.h
new file mode 100644
index 0000000..2c7cdcd
--- /dev/null
+++ b/src/lib/ostream-multiplex.h
@@ -0,0 +1,8 @@
+#ifndef OSTREAM_MULTIPLEX
+#define OSTREAM_MULTIPLEX 1
+
+struct ostream *o_stream_create_multiplex(struct ostream *parent, size_t bufsize);
+struct ostream *o_stream_multiplex_add_channel(struct ostream *stream, uint8_t cid);
+uint8_t o_stream_multiplex_get_channel_id(struct ostream *stream);
+
+#endif
diff --git a/src/lib/ostream-null.c b/src/lib/ostream-null.c
new file mode 100644
index 0000000..0ce858b
--- /dev/null
+++ b/src/lib/ostream-null.c
@@ -0,0 +1,33 @@
+/* Copyright (c) 2016-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "ostream-private.h"
+#include "ostream-null.h"
+
+static ssize_t
+o_stream_null_sendv(struct ostream_private *stream,
+ const struct const_iovec *iov, unsigned int iov_count)
+{
+ unsigned int i;
+ size_t ret = 0;
+
+ for (i = 0; i < iov_count; i++)
+ ret += iov[i].iov_len;
+ stream->ostream.offset += ret;
+ return ret;
+}
+
+struct ostream *o_stream_create_null(void)
+{
+ struct ostream_private *stream;
+ struct ostream *output;
+
+ stream = i_new(struct ostream_private, 1);
+ stream->ostream.blocking = TRUE;
+ stream->sendv = o_stream_null_sendv;
+
+ output = o_stream_create(stream, NULL, -1);
+ o_stream_set_no_error_handling(output, TRUE);
+ o_stream_set_name(output, "(/dev/null)");
+ return output;
+}
diff --git a/src/lib/ostream-null.h b/src/lib/ostream-null.h
new file mode 100644
index 0000000..7c83c80
--- /dev/null
+++ b/src/lib/ostream-null.h
@@ -0,0 +1,7 @@
+#ifndef OSTREAM_NULL_H
+#define OSTREAM_NULL_H
+
+/* Create an output stream that ignores all the writes. */
+struct ostream *o_stream_create_null(void);
+
+#endif
diff --git a/src/lib/ostream-private.h b/src/lib/ostream-private.h
new file mode 100644
index 0000000..6222aa0
--- /dev/null
+++ b/src/lib/ostream-private.h
@@ -0,0 +1,73 @@
+#ifndef OSTREAM_PRIVATE_H
+#define OSTREAM_PRIVATE_H
+
+#include "ostream.h"
+#include "iostream-private.h"
+
+struct ostream_private {
+/* inheritance: */
+ struct iostream_private iostream;
+
+/* methods: */
+ void (*cork)(struct ostream_private *stream, bool set);
+ int (*flush)(struct ostream_private *stream);
+ void (*set_flush_callback)(struct ostream_private *stream,
+ stream_flush_callback_t *callback,
+ void *context);
+ void (*flush_pending)(struct ostream_private *stream, bool set);
+ size_t (*get_buffer_used_size)(const struct ostream_private *stream);
+ size_t (*get_buffer_avail_size)(const struct ostream_private *stream);
+ int (*seek)(struct ostream_private *stream, uoff_t offset);
+ ssize_t (*sendv)(struct ostream_private *stream,
+ const struct const_iovec *iov,
+ unsigned int iov_count);
+ int (*write_at)(struct ostream_private *stream,
+ const void *data, size_t size, uoff_t offset);
+ enum ostream_send_istream_result
+ (*send_istream)(struct ostream_private *outstream,
+ struct istream *instream);
+ void (*switch_ioloop_to)(struct ostream_private *stream,
+ struct ioloop *ioloop);
+
+/* data: */
+ struct ostream ostream;
+ size_t max_buffer_size;
+
+ struct ostream *parent; /* for filter streams */
+
+ int fd;
+ struct timeval last_write_timeval;
+
+ stream_flush_callback_t *callback;
+ void *context;
+
+ bool corked:1;
+ bool finished:1;
+ bool closing:1;
+ bool last_errors_not_checked:1;
+ bool error_handling_disabled:1;
+ bool noverflow:1;
+ bool finish_also_parent:1;
+ bool finish_via_child:1;
+};
+
+struct ostream *
+o_stream_create(struct ostream_private *_stream, struct ostream *parent, int fd)
+ ATTR_NULL(2);
+
+enum ostream_send_istream_result
+io_stream_copy(struct ostream *outstream, struct istream *instream);
+
+void o_stream_copy_error_from_parent(struct ostream_private *_stream);
+/* This should be called before sending data to parent stream. It makes sure
+ that the parent stream's output buffer doesn't become too large.
+ Returns 1 if more data can be safely added, 0 if not, -1 if error. */
+int o_stream_flush_parent_if_needed(struct ostream_private *_stream);
+
+/* Call this in flush() handler to flush the parent stream. It will call
+ either o_stream_flush() or o_stream_finish() depending on whether this
+ stream is already finished. If the parent fails, its error will be also
+ copied to this stream. */
+int o_stream_flush_parent(struct ostream_private *_stream);
+
+#endif
diff --git a/src/lib/ostream-rawlog.c b/src/lib/ostream-rawlog.c
new file mode 100644
index 0000000..8066392
--- /dev/null
+++ b/src/lib/ostream-rawlog.c
@@ -0,0 +1,88 @@
+/* Copyright (c) 2011-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "iostream-rawlog-private.h"
+#include "ostream-private.h"
+#include "ostream-rawlog.h"
+
+struct rawlog_ostream {
+ struct ostream_private ostream;
+ struct rawlog_iostream riostream;
+};
+
+static void o_stream_rawlog_close(struct iostream_private *stream,
+ bool close_parent)
+{
+ struct rawlog_ostream *rstream =
+ container_of(stream, struct rawlog_ostream, ostream.iostream);
+
+ iostream_rawlog_close(&rstream->riostream);
+ if (close_parent)
+ o_stream_close(rstream->ostream.parent);
+}
+
+static ssize_t
+o_stream_rawlog_sendv(struct ostream_private *stream,
+ const struct const_iovec *iov, unsigned int iov_count)
+{
+ struct rawlog_ostream *rstream =
+ container_of(stream, struct rawlog_ostream, ostream);
+ unsigned int i;
+ ssize_t ret, bytes;
+
+ if ((ret = o_stream_sendv(stream->parent, iov, iov_count)) < 0) {
+ o_stream_copy_error_from_parent(stream);
+ return -1;
+ }
+ bytes = ret;
+ for (i = 0; i < iov_count && bytes > 0; i++) {
+ if (iov[i].iov_len < (size_t)bytes) {
+ iostream_rawlog_write(&rstream->riostream,
+ iov[i].iov_base, iov[i].iov_len);
+ bytes -= iov[i].iov_len;
+ } else {
+ iostream_rawlog_write(&rstream->riostream,
+ iov[i].iov_base, bytes);
+ break;
+ }
+ }
+
+ stream->ostream.offset += ret;
+ return ret;
+}
+
+struct ostream *
+o_stream_create_rawlog(struct ostream *output, const char *rawlog_path,
+ int rawlog_fd, enum iostream_rawlog_flags flags)
+{
+ struct ostream *rawlog_output;
+ bool autoclose_fd = (flags & IOSTREAM_RAWLOG_FLAG_AUTOCLOSE) != 0;
+
+ i_assert(rawlog_path != NULL);
+ i_assert(rawlog_fd != -1);
+
+ rawlog_output = autoclose_fd ?
+ o_stream_create_fd_autoclose(&rawlog_fd, 0):
+ o_stream_create_fd(rawlog_fd, 0);
+
+ o_stream_set_name(rawlog_output,
+ t_strdup_printf("rawlog(%s)", rawlog_path));
+ return o_stream_create_rawlog_from_stream(output, rawlog_output, flags);
+}
+
+struct ostream *
+o_stream_create_rawlog_from_stream(struct ostream *output,
+ struct ostream *rawlog_output,
+ enum iostream_rawlog_flags flags)
+{
+ struct rawlog_ostream *rstream;
+
+ rstream = i_new(struct rawlog_ostream, 1);
+ rstream->ostream.sendv = o_stream_rawlog_sendv;
+ rstream->ostream.iostream.close = o_stream_rawlog_close;
+
+ rstream->riostream.rawlog_output = rawlog_output;
+ iostream_rawlog_init(&rstream->riostream, flags, FALSE);
+ return o_stream_create(&rstream->ostream, output,
+ o_stream_get_fd(output));
+}
diff --git a/src/lib/ostream-rawlog.h b/src/lib/ostream-rawlog.h
new file mode 100644
index 0000000..8f3e2b7
--- /dev/null
+++ b/src/lib/ostream-rawlog.h
@@ -0,0 +1,14 @@
+#ifndef OSTREAM_RAWLOG_H
+#define OSTREAM_RAWLOG_H
+
+#include "iostream-rawlog.h"
+
+struct ostream *
+o_stream_create_rawlog(struct ostream *output, const char *rawlog_path,
+ int rawlog_fd, enum iostream_rawlog_flags flags);
+struct ostream *
+o_stream_create_rawlog_from_stream(struct ostream *output,
+ struct ostream *rawlog_output,
+ enum iostream_rawlog_flags flags);
+
+#endif
diff --git a/src/lib/ostream-unix.c b/src/lib/ostream-unix.c
new file mode 100644
index 0000000..06e918f
--- /dev/null
+++ b/src/lib/ostream-unix.c
@@ -0,0 +1,95 @@
+/* Copyright (c) 2015-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "fdpass.h"
+#include "ostream-file-private.h"
+#include "ostream-unix.h"
+
+struct unix_ostream {
+ struct file_ostream fstream;
+ int write_fd;
+};
+
+static void
+o_stream_unix_close(struct iostream_private *stream, bool close_parent)
+{
+ struct unix_ostream *ustream =
+ container_of(stream, struct unix_ostream,
+ fstream.ostream.iostream);
+
+ i_close_fd(&ustream->write_fd);
+ o_stream_file_close(stream, close_parent);
+}
+
+static ssize_t o_stream_unix_writev(struct file_ostream *fstream,
+ const struct const_iovec *iov,
+ unsigned int iov_count)
+{
+ struct unix_ostream *ustream =
+ container_of(fstream, struct unix_ostream, fstream);
+ size_t sent;
+ ssize_t ret;
+
+ if (ustream->write_fd == -1) {
+ /* no fd */
+ return o_stream_file_writev(fstream, iov, iov_count);
+ }
+
+ /* send first iovec along with fd */
+ if (iov_count == 0)
+ return 0;
+ i_assert(iov[0].iov_len > 0);
+ ret = fd_send(fstream->fd, ustream->write_fd,
+ iov[0].iov_base, iov[0].iov_len);
+ if (ret < 0)
+ return ret;
+
+ /* update stream */
+ sent = ret;
+ fstream->real_offset += sent;
+
+ ustream->write_fd = -1;
+
+ if (sent < iov[0].iov_len || iov_count == 1) {
+ /* caller will call us again to write the rest */
+ return sent;
+ }
+
+ /* send remaining iovecs */
+ ret = o_stream_file_writev(fstream, &iov[1], iov_count-1);
+ if (ret < 0)
+ return (errno == EAGAIN || errno == EINTR ? (ssize_t)sent : ret);
+ sent += ret;
+ return sent;
+}
+
+struct ostream *o_stream_create_unix(int fd, size_t max_buffer_size)
+{
+ struct unix_ostream *ustream;
+ struct ostream *output;
+
+ i_assert(fd != -1);
+
+ ustream = i_new(struct unix_ostream, 1);
+ ustream->write_fd = -1;
+ output = o_stream_create_file_common(&ustream->fstream, fd,
+ max_buffer_size, FALSE);
+ output->real_stream->iostream.close = o_stream_unix_close;
+ ustream->fstream.writev = o_stream_unix_writev;
+
+ return output;
+}
+
+bool o_stream_unix_write_fd(struct ostream *output, int fd)
+{
+ struct unix_ostream *ustream =
+ container_of(output->real_stream, struct unix_ostream,
+ fstream.ostream);
+
+ i_assert(fd >= 0);
+
+ if (ustream->write_fd >= 0)
+ return FALSE;
+ ustream->write_fd = fd;
+ return TRUE;
+}
diff --git a/src/lib/ostream-unix.h b/src/lib/ostream-unix.h
new file mode 100644
index 0000000..849669b
--- /dev/null
+++ b/src/lib/ostream-unix.h
@@ -0,0 +1,10 @@
+#ifndef OSTREAM_UNIX_H
+#define OSTREAM_UNIX_H
+
+struct ostream *o_stream_create_unix(int fd, size_t max_buffer_size);
+/* Write fd to UNIX socket along with the next outgoing data block.
+ Returns TRUE if fd is accepted, and FALSE if a previous fd still
+ needs to be sent. */
+bool o_stream_unix_write_fd(struct ostream *output, int fd);
+
+#endif
diff --git a/src/lib/ostream-wrapper.c b/src/lib/ostream-wrapper.c
new file mode 100644
index 0000000..dfd6699
--- /dev/null
+++ b/src/lib/ostream-wrapper.c
@@ -0,0 +1,1259 @@
+/* Copyright (c) 2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "buffer.h"
+#include "ostream-private.h"
+
+#include "ostream-wrapper.h"
+
+static int wrapper_ostream_flush(struct ostream_private *stream);
+static void
+wrapper_ostream_switch_ioloop_to(struct ostream_private *stream,
+ struct ioloop *ioloop);
+
+/*
+ * Buffer
+ */
+
+/* Determine the optimum buffer size for the wrapper stream itself. */
+static inline size_t
+wrapper_ostream_optimal_size(struct wrapper_ostream *wostream)
+{
+ size_t optimal_size = wostream->ostream.max_buffer_size;
+
+ if (wostream->output != NULL) {
+ optimal_size = I_MIN(
+ o_stream_get_max_buffer_size(wostream->output),
+ optimal_size);
+ }
+ if (optimal_size == SIZE_MAX)
+ optimal_size = IO_BLOCK_SIZE;
+
+ return optimal_size;
+}
+
+/* Return the current size of the wrapper output stream buffer. */
+static inline size_t wrapper_ostream_size(struct wrapper_ostream *wostream)
+{
+ buffer_t *buffer = wostream->buffer;
+
+ if (buffer == NULL)
+ return 0;
+ return buffer->used;
+}
+
+/* Return TRUE when the wrapper stream's internal buffer is empty. */
+static inline bool wrapper_ostream_is_empty(struct wrapper_ostream *wostream)
+{
+ return (wrapper_ostream_size(wostream) == 0);
+}
+/* Return TRUE when the wrapper stream's internal buffer is filled to the
+ maximum. */
+static inline bool wrapper_ostream_is_full(struct wrapper_ostream *wostream)
+{
+ return (wrapper_ostream_size(wostream) >=
+ wostream->ostream.max_buffer_size);
+}
+/* Return TRUE when the wrapper stream's internal buffer is filled at or beyond
+ the optimum. */
+static inline bool wrapper_ostream_is_filled(struct wrapper_ostream *wostream)
+{
+ return (wrapper_ostream_size(wostream) >=
+ wrapper_ostream_optimal_size(wostream));
+}
+
+/*
+ * Underlying output
+ */
+
+/* Handle error in the underlying output stream (the parent). */
+static void
+wrapper_ostream_copy_parent_error(struct wrapper_ostream *wostream)
+{
+ i_assert(wostream->output != NULL);
+ i_assert(wostream->output->stream_errno != 0);
+
+ wostream->ostream.ostream.stream_errno =
+ wostream->output->stream_errno;
+ wostream->ostream.ostream.overflow =
+ wostream->output->overflow;
+}
+
+static void
+wrapper_ostream_handle_parent_error(struct wrapper_ostream *wostream)
+{
+ wrapper_ostream_copy_parent_error(wostream);
+
+ if (wostream->output->closed)
+ o_stream_close(&wostream->ostream.ostream);
+
+ if (wostream->output_error != NULL)
+ wostream->output_error(wostream);
+}
+
+static void wrapper_ostream_closed(struct wrapper_ostream *wostream)
+{
+ wostream->ostream.ostream.closed = TRUE;
+}
+
+/* Drop the underlying output. */
+static void wrapper_ostream_output_close(struct wrapper_ostream *wostream)
+{
+ o_stream_unref(&wostream->output);
+ wostream->output_finished = TRUE;
+ wostream->output_closed = TRUE;
+ wostream->output_closed_api = TRUE;
+}
+
+/* Method calls */
+
+/* Called when the implementation should start making the parent output stream
+ available, e.g. connect to the server (see output_start() method).
+ */
+static void wrapper_ostream_output_start(struct wrapper_ostream *wostream)
+{
+ if (wostream->output_started)
+ return;
+ wostream->output_started = TRUE;
+ if (wostream->output_start != NULL)
+ wostream->output_start(wostream);
+}
+
+/* Returns TRUE when the output is ready for data (see output_ready() method).
+ */
+static bool wrapper_ostream_output_ready(struct wrapper_ostream *wostream)
+{
+ i_assert(wostream->output_ready != NULL);
+ return wostream->output_ready(wostream);
+}
+
+/* Finish the underlying output (see output_finish() method).*/
+static int wrapper_ostream_output_finish(struct wrapper_ostream *wostream)
+{
+ i_assert(wostream->output_finish != NULL);
+ return wostream->output_finish(wostream);
+}
+
+/* Called when the wrapper ostream does not need write to parent output stream.
+ (see output_halt() method).
+ */
+static void wrapper_ostream_output_halt(struct wrapper_ostream *wostream)
+{
+ if (wostream->output_closed)
+ return;
+ if (wostream->output_halt != NULL)
+ wostream->output_halt(wostream);
+}
+
+/* Called when the wrapper ostream has data available for the parent output and
+ wants wrapper_ostream_continue() to be called when the parent stream is
+ writeable (see output_resume() method). */
+static void wrapper_ostream_output_resume(struct wrapper_ostream *wostream)
+{
+ if (wostream->output_closed)
+ return;
+ if (wostream->output_resume != NULL)
+ wostream->output_resume(wostream);
+}
+
+/* Update any timeouts for the underlying (parent) output (see
+ output_update_timeouts() method). */
+static void
+wrapper_ostream_output_update_timeouts(struct wrapper_ostream *wostream)
+{
+ struct ostream_private *stream = &wostream->ostream;
+ bool sender_blocking;
+
+ if (wostream->output_closed)
+ return;
+ if (wostream->output_update_timeouts == NULL)
+ return;
+
+ sender_blocking = (!stream->finished &&
+ (wrapper_ostream_is_empty(wostream) ||
+ (stream->corked &&
+ !wrapper_ostream_is_filled(wostream))));
+ wostream->output_update_timeouts(wostream, sender_blocking);
+}
+
+/*
+ * Wrapper
+ */
+
+/* Halt/resume the underlying output based on the state of the wrapper stream.
+ */
+static void
+wrapper_ostream_output_manage(struct wrapper_ostream *wostream, bool sending)
+{
+ struct ostream_private *stream = &wostream->ostream;
+ bool must_flush, no_data;
+
+ if (wostream->output_closed)
+ return;
+
+ must_flush = (sending || stream->finished || wostream->flush_pending);
+ no_data = (wrapper_ostream_is_empty(wostream) ||
+ (stream->corked && !wrapper_ostream_is_filled(wostream)));
+
+ if (!must_flush && (no_data || stream->ostream.closed))
+ wrapper_ostream_output_halt(wostream);
+ else {
+ wrapper_ostream_output_resume(wostream);
+ if (wostream->output != NULL && must_flush)
+ o_stream_set_flush_pending(wostream->output, TRUE);
+ }
+}
+
+/* Handle any pending error by making it available to the application through
+ the output stream API. */
+static int
+wrapper_ostream_handle_pending_error(struct wrapper_ostream *wostream)
+{
+ struct ostream_private *stream = &wostream->ostream;
+
+ if (wostream->pending_errno != 0) {
+ if (wostream->pending_error != NULL) {
+ io_stream_set_error(&stream->iostream,
+ "%s", wostream->pending_error);
+ }
+ stream->ostream.stream_errno = wostream->pending_errno;
+ wostream->pending_errno = 0;
+ wostream->returned_error = TRUE;
+ wrapper_ostream_closed(wostream);
+ i_free_and_null(wostream->pending_error);
+ return -1;
+ }
+ return 0;
+}
+
+/* Called when the wrapper stream is first finished using o_stream_finish(). */
+static int wrapper_ostream_finish(struct wrapper_ostream *wostream)
+{
+ int ret;
+
+ if (wostream->output_closed) {
+ if (wrapper_ostream_handle_pending_error(wostream) < 0)
+ return -1;
+ return 1;
+ }
+
+ if (!wrapper_ostream_output_ready(wostream)) {
+ return 0;
+ }
+
+ wostream->output_finished = TRUE;
+ if (wostream->output != NULL) {
+ if (o_stream_uncork_flush(wostream->output) < 0) {
+ wrapper_ostream_handle_parent_error(wostream);
+ o_stream_unref(&wostream->output);
+ return -1;
+ }
+ }
+
+ /* Finished sending payload; now also finish the underlying output. */
+ ret = wrapper_ostream_output_finish(wostream);
+ if (ret == 0)
+ return ret;
+ if (ret < 0 && wostream->ostream.ostream.stream_errno != 0) {
+ wrapper_ostream_copy_parent_error(wostream);
+ return -1;
+ }
+ if (wrapper_ostream_handle_pending_error(wostream) < 0 || ret < 0) {
+ i_assert(wostream->ostream.ostream.stream_errno != 0);
+ return -1;
+ }
+ wrapper_ostream_output_close(wostream);
+ return 1;
+}
+
+/* Wait in ioloop until underlying (parent) output can be flushed. This is
+ called only when the wrapper stream is blocking. */
+static int
+wrapper_ostream_flush_wait(struct wrapper_ostream *wostream)
+{
+ struct ostream_private *stream = &wostream->ostream;
+ struct ioloop *ioloop, *prev_ioloop;
+ bool was_corked = FALSE;
+
+ wrapper_ostream_output_manage(wostream, !wostream->flushing);
+
+ /* Cannot be already waiting */
+ i_assert(!wostream->flush_waiting);
+ i_assert(wostream->flush_ioloop == NULL);
+
+ i_assert(wostream->wait_begin != NULL);
+ i_assert(wostream->wait_end != NULL);
+
+ if (wostream->output != NULL && o_stream_is_corked(wostream->output)) {
+ /* Make sure parent is uncorked here to make sure output IO is
+ active. */
+ if (o_stream_uncork_flush(wostream->output) < 0) {
+ wrapper_ostream_handle_parent_error(wostream);
+ return -1;
+ }
+ was_corked = TRUE;
+ }
+
+ wostream->flush_ioloop = ioloop = io_loop_create();
+ prev_ioloop = wostream->wait_begin(wostream, ioloop);
+ o_stream_switch_ioloop_to(&wostream->ostream.ostream, ioloop);
+
+ /* Either we're waiting for network I/O or we're getting out of a
+ callback using timeout_add_short(0) */
+ i_assert(io_loop_have_ios(ioloop) ||
+ io_loop_have_immediate_timeouts(ioloop));
+
+ wostream->flush_waiting = TRUE;
+ do {
+ e_debug(wostream->event, "Waiting for output flush");
+ io_loop_run(ioloop);
+ } while (wostream->flush_waiting);
+
+ e_debug(wostream->event, "Can now flush output");
+
+ o_stream_switch_ioloop_to(&wostream->ostream.ostream, prev_ioloop);
+ wostream->wait_end(wostream, prev_ioloop);
+ io_loop_destroy(&ioloop);
+ wostream->flush_ioloop = NULL;
+
+ if (stream->ostream.blocking)
+ wrapper_ostream_output_halt(wostream);
+
+ if (was_corked && wostream->output != NULL)
+ o_stream_cork(wostream->output);
+
+ if (wrapper_ostream_handle_pending_error(wostream) < 0) {
+ /* Stream already hit an error */
+ return -1;
+ }
+ return 0;
+}
+
+/* Try to flush the underlying (parent) output. */
+static int wrapper_ostream_flush_parent(struct wrapper_ostream *wostream)
+{
+ struct ostream *parent;
+
+ if (wostream->output_closed) {
+ /* Output already dropped; nothing to flush */
+ return 1;
+ }
+ if (!wrapper_ostream_output_ready(wostream)) {
+ /* There is no parent ostream yet */
+ return 1;
+ }
+
+ parent = wostream->output;
+ if (parent == NULL) {
+ /* There is no parent ostream anymore */
+ i_assert(wostream->buffer == NULL ||
+ wostream->buffer->used == 0);
+ return 1;
+ }
+ if (o_stream_get_buffer_used_size(parent) >= IO_BLOCK_SIZE) {
+ /* We already have quite a lot of data in parent stream.
+ unless we can flush it, don't add any more to it or we
+ could keep wasting memory by just increasing the buffer
+ size all the time. */
+ if (o_stream_flush(parent) < 0) {
+ wrapper_ostream_handle_parent_error(wostream);
+ return -1;
+ }
+ if (o_stream_get_buffer_used_size(parent) >= IO_BLOCK_SIZE)
+ return 0;
+ }
+
+ return 1;
+}
+
+/* Try to write data to underlying (parent) output. */
+static ssize_t
+wrapper_ostream_writev(struct wrapper_ostream *wostream,
+ const struct const_iovec *iov, unsigned int iov_count)
+{
+ struct ostream *parent = wostream->output;
+ ssize_t sent;
+
+ i_assert(!wostream->output_closed);
+ i_assert(!wostream->output_finished);
+
+ if (!wrapper_ostream_output_ready(wostream))
+ return 0;
+
+ /* Send more data to parent ostream */
+ i_assert(parent != NULL);
+ o_stream_set_max_buffer_size(parent, IO_BLOCK_SIZE);
+ sent = o_stream_sendv(parent, iov, iov_count);
+ o_stream_set_max_buffer_size(parent, SIZE_MAX);
+ if (sent < 0) {
+ wrapper_ostream_handle_parent_error(wostream);
+ return -1;
+ }
+
+ return sent;
+}
+
+/* Try to write data to underlying (parent) output and implement blocking
+ behavior by running an ioloop. */
+static ssize_t
+wrapper_ostream_writev_full(struct wrapper_ostream *wostream,
+ const struct const_iovec *iov,
+ unsigned int iov_count)
+{
+ struct ostream_private *stream = &wostream->ostream;
+ unsigned int i;
+ ssize_t sent, sent_total;
+
+ if (!stream->ostream.blocking) {
+ /* Not blocking; send what we can */
+ return wrapper_ostream_writev(wostream, iov, iov_count);
+ }
+
+ /* Blocking; loop and wait until all is sent */
+
+ sent_total = 0;
+ for (;;) {
+ struct const_iovec niov;
+ size_t iov_pos;
+
+ i_assert(iov_count > 0);
+
+ /* Send iovec with complete entries */
+ sent = wrapper_ostream_writev(wostream, iov, iov_count);
+ if (sent < 0)
+ return -1;
+ if (sent == 0) {
+ if (wrapper_ostream_flush_wait(wostream) < 0)
+ return -1;
+ i_assert(!wostream->output_closed);
+ continue;
+ }
+
+ /* Determine what was sent */
+ sent_total += sent;
+ iov_pos = (size_t)sent;
+ for (i = 0; i < iov_count && iov_pos >= iov[i].iov_len; i++)
+ iov_pos -= iov[i].iov_len;
+ if (i >= iov_count) {
+ /* All sent */
+ i_assert(iov_pos == 0);
+ return sent_total;
+ }
+
+ iov = &iov[i];
+ iov_count -= i;
+ if (iov_pos == 0) {
+ /* Nicely sent until an iovec boundary */
+ continue;
+ }
+
+ /* Send partial iovec entry */
+ i_zero(&niov);
+ niov = iov[0];
+ i_assert(iov_pos < niov.iov_len);
+ niov.iov_base = CONST_PTR_OFFSET(niov.iov_base, iov_pos);
+ niov.iov_len -= iov_pos;
+
+ while (niov.iov_len > 0) {
+ sent = wrapper_ostream_writev(wostream, &niov, 1);
+ if (sent < 0)
+ return sent;
+ if (sent == 0) {
+ if (wrapper_ostream_flush_wait(wostream) < 0)
+ return -1;
+ i_assert(!wostream->output_closed);
+ continue;
+ }
+ i_assert((size_t)sent <= niov.iov_len);
+ niov.iov_base = CONST_PTR_OFFSET(niov.iov_base, sent);
+ niov.iov_len -= sent;
+ sent_total += sent;
+ }
+
+ if (iov_count == 1) {
+ i_assert(sent_total != 0);
+ return sent_total;
+ }
+
+ /* Now sent until an iovec boundary */
+ iov = &iov[1];
+ iov_count--;
+ }
+
+ i_unreached();
+}
+
+/* Try to flush wrapper stream's buffer content. */
+static int wrapper_ostream_flush_buffer(struct wrapper_ostream *wostream)
+{
+ struct ostream_private *stream = &wostream->ostream;
+ buffer_t *buffer = wostream->buffer;
+ struct const_iovec iov;
+ ssize_t sent;
+
+ if (wostream->output_closed) {
+ /* Ostream already finished */
+ i_assert(wostream->ostream.finished);
+ return 1;
+ }
+
+ if (buffer == NULL || buffer->used == 0) {
+ /* Buffer already empty */
+ return 1;
+ }
+
+ do {
+ /* Try to flush whole buffer */
+ iov.iov_base = buffer->data;
+ iov.iov_len = buffer->used;
+ sent = wrapper_ostream_writev_full(wostream, &iov, 1);
+ if (sent < 0)
+ return -1;
+
+ /* Remove sent data from buffer */
+ buffer_delete(buffer, 0, sent);
+
+ /* More aggressively flush the buffer when this stream is
+ finished
+ */
+ } while (wostream->ostream.finished && sent > 0 && buffer->used > 0);
+
+ if (buffer->used == 0 ||
+ (stream->corked && !wrapper_ostream_is_filled(wostream)))
+ wrapper_ostream_output_halt(wostream);
+
+ return (buffer->used == 0 ? 1 : 0);
+}
+
+static int wrapper_ostream_flush_real(struct wrapper_ostream *wostream)
+{
+ struct ostream_private *stream = &wostream->ostream;
+ int ret;
+
+ if (wrapper_ostream_handle_pending_error(wostream) < 0) {
+ /* Stream already hit an error */
+ return -1;
+ }
+ wrapper_ostream_output_start(wostream);
+
+ if ((ret = wrapper_ostream_flush_parent(wostream)) <= 0) {
+ /* Try to flush parent stream first to make room for more
+ data */
+ return ret;
+ }
+ if ((ret = wrapper_ostream_flush_buffer(wostream)) <= 0) {
+ /* Try sending data we already buffered */
+ return ret;
+ }
+
+ if (wostream->output_closed || wostream->output_finished) {
+ /* Already finished the ostream */
+ i_assert(stream->finished);
+ return 1;
+ }
+
+ if (!wrapper_ostream_output_ready(wostream)) {
+ return ((wostream->buffer == NULL ||
+ wostream->buffer->used == 0) ? 1 : 0);
+ }
+
+ if (wostream->output == NULL) {
+ i_assert(wrapper_ostream_is_empty(wostream));
+ ret = 1;
+ } else {
+ ret = o_stream_flush(wostream->output);
+ if (ret < 0)
+ wrapper_ostream_handle_parent_error(wostream);
+ }
+
+ return ret;
+}
+
+static bool
+wrapper_ostream_send_prepare(struct wrapper_ostream *wostream, size_t size)
+{
+ struct ostream_private *stream = &wostream->ostream;
+
+ if (wostream->output_closed || wostream->output_started)
+ return TRUE;
+
+ if (stream->corked && !stream->finished) {
+ if (wostream->buffer == NULL)
+ return FALSE;
+ if ((wostream->buffer->used + size) < stream->max_buffer_size)
+ return FALSE;
+ }
+ wrapper_ostream_output_start(wostream);
+ return TRUE;
+}
+
+/* Add data to the wrapper stream's internal buffer. */
+static size_t
+wrapper_ostream_add(struct wrapper_ostream *wostream,
+ const struct const_iovec *iov,
+ unsigned int iov_count, unsigned int *iov_idx,
+ size_t *iov_idx_pos)
+{
+ buffer_t *buffer = wostream->buffer;
+ unsigned int i;
+ size_t added = 0;
+
+ /* Create buffer */
+ if (buffer == NULL) {
+ wostream->buffer = buffer =
+ buffer_create_dynamic(default_pool, IO_BLOCK_SIZE);
+ }
+
+ for (i = *iov_idx; i < iov_count; i++) {
+ size_t iov_len, iov_add, space;
+ const unsigned char *iov_data;
+
+ iov_len = iov[i].iov_len;
+ iov_data = iov[i].iov_base;
+ space = wostream->ostream.max_buffer_size - buffer->used;
+
+ i_assert(*iov_idx_pos < iov_len);
+ if (*iov_idx_pos > 0) {
+ iov_len -= *iov_idx_pos;
+ iov_data += *iov_idx_pos;
+ }
+ iov_add = I_MIN(space, iov_len);
+ buffer_append(buffer, iov_data, iov_add);
+ added += iov_add;
+ if (iov_add < iov_len) {
+ /* Buffer is full */
+ *iov_idx_pos += iov_add;
+ break;
+ }
+ *iov_idx_pos = 0;
+ }
+
+ *iov_idx = i;
+ return added;
+}
+
+static ssize_t
+wrapper_ostream_sendv_real(struct wrapper_ostream *wostream,
+ const struct const_iovec *iov,
+ unsigned int iov_count)
+{
+ struct ostream_private *stream = &wostream->ostream;
+ ssize_t written;
+ size_t size, iov_pos, sent;
+ unsigned int i;
+ int ret;
+
+ if (wrapper_ostream_handle_pending_error(wostream) < 0) {
+ /* Stream already hit an error */
+ return -1;
+ }
+
+ i_assert(!wostream->output_closed);
+ i_assert(!wostream->output_finished);
+
+ /* Determine total size of data to send */
+ size = 0;
+ for (i = 0; i < iov_count; i++)
+ size += iov[i].iov_len;
+
+ /* Flush buffer if required */
+ if (!wrapper_ostream_is_empty(wostream) &&
+ (!stream->corked || wrapper_ostream_is_filled(wostream)) &&
+ wrapper_ostream_send_prepare(wostream, size) &&
+ wrapper_ostream_flush_buffer(wostream) < 0)
+ return -1;
+
+ if (!stream->corked && wrapper_ostream_is_full(wostream)) {
+ /* No space in buffer for more data */
+ i_assert(!stream->ostream.blocking);
+ return 0;
+ }
+
+ /* Send data to connection directly if possible */
+ i = 0;
+ sent = iov_pos = 0;
+ if (wrapper_ostream_is_empty(wostream) &&
+ (!stream->corked ||
+ size >= wrapper_ostream_optimal_size(wostream)) &&
+ wrapper_ostream_send_prepare(wostream, size)) {
+ written = wrapper_ostream_writev_full(wostream, iov, iov_count);
+ if (written < 0)
+ return -1;
+ sent += written;
+ if (sent == size) {
+ /* All sent */
+ return (ssize_t)sent;
+ }
+
+ i_assert(!stream->ostream.blocking);
+
+ /* Determine send position */
+ iov_pos = sent;
+ for (; i < iov_count && iov_pos >= iov[i].iov_len; i++)
+ iov_pos -= iov[i].iov_len;
+ i_assert(i < iov_count);
+ }
+
+ /* Fill buffer with remainder that was not sent directly */
+ for (;;) {
+ sent += wrapper_ostream_add(wostream, iov, iov_count,
+ &i, &iov_pos);
+ i_assert(sent <= size);
+
+ if (!stream->corked || !wrapper_ostream_is_filled(wostream))
+ break;
+
+ /* Flush corked full buffer */
+ wrapper_ostream_output_start(wostream);
+ if ((ret = wrapper_ostream_flush_buffer(wostream)) < 0)
+ return -1;
+ if (ret == 0)
+ break;
+ }
+
+ i_assert(!stream->ostream.blocking || sent == size);
+ return sent;
+}
+
+/* Run the flush callback for the wrapper stream. */
+static int wrapper_ostream_callback(struct wrapper_ostream *wostream)
+{
+ int ret;
+
+ if (wostream->ostream.callback != NULL) {
+ if (wostream->callback_pre != NULL)
+ wostream->callback_pre(wostream);
+ ret = wostream->ostream.callback(wostream->ostream.context);
+ if (wostream->callback_post != NULL)
+ wostream->callback_post(wostream);
+ } else {
+ ret = wrapper_ostream_flush(&wostream->ostream);
+ }
+ return ret;
+}
+
+/* Handle an event by running wrapper_ostream_continue(). This called from
+ ioloop on a zero timeout. */
+static void wrapper_ostream_handle_event(struct wrapper_ostream *wostream)
+{
+ timeout_remove(&wostream->to_event);
+ (void)wrapper_ostream_continue(wostream);
+}
+
+/*
+ * iostream methods
+ */
+
+static void
+wrapper_ostream_close(struct iostream_private *stream,
+ bool close_parent ATTR_UNUSED)
+{
+ struct wrapper_ostream *wostream =
+ container_of(stream, struct wrapper_ostream, ostream.iostream);
+
+ timeout_remove(&wostream->to_event);
+ wrapper_ostream_output_close(wostream);
+ if (wostream->close != NULL)
+ wostream->close(wostream);
+}
+
+static void wrapper_ostream_destroy(struct iostream_private *stream)
+{
+ struct wrapper_ostream *wostream =
+ container_of(stream, struct wrapper_ostream, ostream.iostream);
+
+ timeout_remove(&wostream->to_event);
+ i_free(wostream->pending_error);
+
+ if (wostream->destroy != NULL)
+ wostream->destroy(wostream);
+ buffer_free(&wostream->buffer);
+ o_stream_unref(&wostream->output);
+ event_unref(&wostream->event);
+}
+
+/*
+ * ostream methods
+ */
+
+static void wrapper_ostream_cork(struct ostream_private *stream, bool set)
+{
+ struct wrapper_ostream *wostream =
+ container_of(stream, struct wrapper_ostream, ostream);
+ int ret;
+
+ if (stream->ostream.closed || wostream->pending_errno != 0)
+ return;
+
+ if (wostream->output_closed) {
+ i_assert(wostream->ostream.finished);
+ return;
+ }
+
+ if (set) {
+ if (wostream->output != NULL)
+ o_stream_cork(wostream->output);
+ } else {
+ /* Buffer flushing might close the stream */
+ ret = wrapper_ostream_flush_buffer(wostream);
+ stream->last_errors_not_checked = TRUE;
+
+ if (wostream->output != NULL) {
+ if (o_stream_uncork_flush(wostream->output) < 0) {
+ wrapper_ostream_handle_parent_error(wostream);
+ ret = -1;
+ }
+ }
+ if ((ret == 0 || wostream->flush_pending) &&
+ !stream->ostream.closed)
+ wrapper_ostream_output_resume(wostream);
+ }
+ stream->corked = set;
+
+ wrapper_ostream_output_manage(wostream, FALSE);
+}
+
+static ssize_t
+wrapper_ostream_sendv(struct ostream_private *stream,
+ const struct const_iovec *iov, unsigned int iov_count)
+{
+ struct wrapper_ostream *wostream =
+ container_of(stream, struct wrapper_ostream, ostream);
+ bool must_uncork = FALSE;
+ ssize_t sret;
+
+ if (wrapper_ostream_handle_pending_error(wostream) < 0) {
+ /* Stream already hit an error */
+ return -1;
+ }
+
+ /* Cork parent ostream if necessary */
+ if (!wostream->output_closed && wostream->output != NULL &&
+ !o_stream_is_corked(wostream->output)) {
+ o_stream_cork(wostream->output);
+ must_uncork = TRUE;
+ }
+
+ sret = wrapper_ostream_sendv_real(wostream, iov, iov_count);
+ if (sret > 0)
+ stream->ostream.offset += (ssize_t)sret;
+
+ /* Uncork the parent ostream */
+ if (must_uncork && !wostream->output_closed &&
+ wostream->output != NULL) {
+ if (o_stream_uncork_flush(wostream->output) < 0 &&
+ sret >= 0) {
+ wrapper_ostream_handle_parent_error(wostream);
+ sret = -1;
+ }
+ }
+
+ if (sret >= 0) {
+ wrapper_ostream_output_update_timeouts(wostream);
+ if (!stream->ostream.blocking)
+ wrapper_ostream_output_manage(wostream, FALSE);
+ }
+
+ return sret;
+}
+
+static int wrapper_ostream_flush(struct ostream_private *stream)
+{
+ struct wrapper_ostream *wostream =
+ container_of(stream, struct wrapper_ostream, ostream);
+ struct ostream *ostream = &stream->ostream;
+ bool must_uncork = FALSE;
+ int ret;
+
+ if (wrapper_ostream_handle_pending_error(wostream) < 0) {
+ /* Stream already hit an error */
+ return -1;
+ }
+
+ if (wostream->output_closed) {
+ if (!stream->finished || !wrapper_ostream_is_empty(wostream)) {
+ stream->ostream.stream_errno = EPIPE;
+ return -1;
+ }
+ /* Already finished the ostream */
+ return 1;
+ }
+
+ if (wostream->flushing) {
+ /* Prevent recursion while finishing output */
+ return 1;
+ }
+ wostream->flushing = TRUE;
+ o_stream_ref(ostream);
+
+ /* Cork parent ostream if necessary */
+ if (wostream->output != NULL && !o_stream_is_corked(wostream->output)) {
+ o_stream_cork(wostream->output);
+ must_uncork = TRUE;
+ }
+
+ /* If blocking: loop until all is flushed; otherwise try once */
+ do {
+ /* Try to flush */
+ if ((ret = wrapper_ostream_flush_real(wostream)) < 0) {
+ ret = -1;
+ break;
+ }
+
+ if (ret == 0 && stream->ostream.blocking) {
+ /* Block until we can write more */
+ if (wrapper_ostream_flush_wait(wostream) < 0) {
+ ret = -1;
+ break;
+ }
+ }
+
+ if (stream->ostream.closed) {
+ /* Ostream was closed in the mean time */
+ ret = -1;
+ break;
+ }
+
+ if (wostream->output_closed) {
+ /* Already finished the ostream */
+ i_assert(stream->finished);
+ ret = 1;
+ break;
+ }
+ } while (ret == 0 && stream->ostream.blocking);
+
+ if (ret > 0 && stream->finished) {
+ /* This was an o_stream_finish() call or subsequent flush */
+ i_assert(wrapper_ostream_is_empty(wostream));
+ while ((ret = wrapper_ostream_finish(wostream)) == 0) {
+ if (!stream->ostream.blocking) {
+ /* Not yet finished completely */
+ break;
+ }
+ /* Block until we can write more */
+ if (wrapper_ostream_flush_wait(wostream) < 0) {
+ ret = -1;
+ break;
+ }
+ }
+ }
+ wrapper_ostream_output_update_timeouts(wostream);
+ wostream->flushing = FALSE;
+
+ if (ret >= 0 && !ostream->blocking)
+ wrapper_ostream_output_manage(wostream, FALSE);
+
+ if (wostream->output_closed) {
+ i_assert(ret < 0 || ostream->stream_errno == 0 ||
+ ostream->closed);
+ i_assert(ret >= 0 || ostream->stream_errno != 0);
+ o_stream_unref(&ostream);
+ return (ret >= 0 ? 1 : -1);
+ }
+
+ if (!must_uncork || wostream->output == NULL) {
+ /* Nothing */
+ } else if (ret >= 0) {
+ /* Uncork the parent ostream */
+ if (o_stream_uncork_flush(wostream->output) < 0) {
+ wrapper_ostream_handle_parent_error(wostream);
+ ret = -1;
+ }
+ } else {
+ o_stream_uncork(wostream->output);
+ }
+
+ i_assert(ret >= 0 || ostream->stream_errno != 0);
+ o_stream_unref(&ostream);
+ return ret;
+}
+
+static void
+wrapper_ostream_set_flush_callback(struct ostream_private *stream,
+ stream_flush_callback_t *callback,
+ void *context)
+{
+ struct wrapper_ostream *wostream =
+ container_of(stream, struct wrapper_ostream, ostream);
+
+ stream->callback = callback;
+ stream->context = context;
+
+ if (!stream->ostream.blocking && stream->callback == NULL) {
+ /* Application is currently not interested in flush events and
+ that includes request events like errors. */
+ timeout_remove(&wostream->to_event);
+ } else if (wostream->pending_error != NULL &&
+ wostream->to_event == NULL) {
+ /* Schedule flush callback to notify application of events */
+ wostream->to_event = timeout_add_short(
+ 0, wrapper_ostream_handle_event, wostream);
+ }
+}
+
+static void
+wrapper_ostream_flush_pending(struct ostream_private *stream, bool set)
+{
+ struct wrapper_ostream *wostream =
+ container_of(stream, struct wrapper_ostream, ostream);
+
+ wostream->flush_pending = set;
+ if (!set)
+ return;
+ if (wostream->output_closed) {
+ i_assert(wostream->ostream.ostream.closed);
+ return;
+ }
+ if (wostream->to_event == NULL) {
+ wostream->to_event = timeout_add_short(
+ 0, wrapper_ostream_handle_event, wostream);
+ }
+}
+
+static size_t
+wrapper_ostream_get_buffer_used_size(const struct ostream_private *stream)
+{
+ const struct wrapper_ostream *wostream =
+ container_of(stream, const struct wrapper_ostream, ostream);
+ size_t size = 0;
+
+ if (wostream->buffer != NULL)
+ size += wostream->buffer->used;
+ if (wostream->output != NULL)
+ size += o_stream_get_buffer_used_size(wostream->output);
+ return size;
+}
+
+static size_t
+wrapper_ostream_get_buffer_avail_size(const struct ostream_private *stream)
+{
+ const struct wrapper_ostream *wostream =
+ container_of(stream, const struct wrapper_ostream, ostream);
+ size_t size = 0;
+
+ if (wostream->ostream.max_buffer_size == SIZE_MAX)
+ return SIZE_MAX;
+
+ if (wostream->buffer == NULL)
+ size = wostream->ostream.max_buffer_size;
+ else if (wostream->buffer->used < wostream->ostream.max_buffer_size) {
+ size = (wostream->ostream.max_buffer_size -
+ wostream->buffer->used);
+ }
+
+ if (wostream->output != NULL)
+ size += o_stream_get_buffer_avail_size(wostream->output);
+
+ return size;
+}
+
+static void
+wrapper_ostream_switch_ioloop_to(struct ostream_private *stream,
+ struct ioloop *ioloop)
+{
+ struct wrapper_ostream *wostream =
+ container_of(stream, struct wrapper_ostream, ostream);
+
+ if (wostream->flush_ioloop != ioloop &&
+ wostream->switch_ioloop_to != NULL)
+ wostream->switch_ioloop_to(wostream, ioloop);
+
+ if (wostream->to_event != NULL) {
+ wostream->to_event =
+ io_loop_move_timeout_to(ioloop, &wostream->to_event);
+ }
+}
+
+/*
+ * API
+ */
+
+struct ostream *
+wrapper_ostream_create(struct wrapper_ostream *wostream,
+ size_t max_buffer_size, bool blocking,
+ struct event *event)
+{
+ wostream->ostream.iostream.close = wrapper_ostream_close;
+ wostream->ostream.iostream.destroy = wrapper_ostream_destroy;
+
+ wostream->ostream.ostream.blocking = blocking;
+ wostream->ostream.max_buffer_size = max_buffer_size;
+ wostream->ostream.cork = wrapper_ostream_cork;
+ wostream->ostream.sendv = wrapper_ostream_sendv;
+ wostream->ostream.flush = wrapper_ostream_flush;
+ wostream->ostream.set_flush_callback =
+ wrapper_ostream_set_flush_callback;
+ wostream->ostream.flush_pending = wrapper_ostream_flush_pending;
+ wostream->ostream.get_buffer_used_size =
+ wrapper_ostream_get_buffer_used_size;
+ wostream->ostream.get_buffer_avail_size =
+ wrapper_ostream_get_buffer_avail_size;
+ wostream->ostream.switch_ioloop_to =
+ wrapper_ostream_switch_ioloop_to;
+
+ wostream->event = event_create(event);
+
+ return o_stream_create(&wostream->ostream, NULL, -1);
+}
+
+int wrapper_ostream_continue(struct wrapper_ostream *wostream)
+{
+ struct ostream_private *stream = &wostream->ostream;
+ struct ostream *ostream = &stream->ostream;
+ struct ioloop *ioloop = NULL;
+ bool use_cork = !stream->corked;
+ int ret = 1;
+
+ if (wostream->flush_waiting) {
+ /* Inside wrapper_ostream_flush_wait() */
+ ioloop = wostream->flush_ioloop;
+ }
+ if (stream->ostream.closed ||
+ (stream->finished && wrapper_ostream_is_empty(wostream) &&
+ wostream->output != NULL &&
+ o_stream_get_buffer_used_size(wostream->output) == 0)) {
+ /* Already finished */
+ ret = wrapper_ostream_finish(wostream);
+ if (ret == 0)
+ return 0;
+ }
+ if (wostream->flush_waiting) {
+ i_assert(ioloop != NULL);
+ io_loop_stop(ioloop);
+ wostream->flush_waiting = FALSE;
+ return ret;
+ }
+
+ /* Set flush_pending = FALSE first before calling the flush callback,
+ and change it to TRUE only if callback returns 0. That way the
+ callback can call o_stream_set_flush_pending() again and we don't
+ forget it even if flush callback returns 1. */
+ wostream->flush_pending = FALSE;
+
+ o_stream_ref(ostream);
+ wostream->continuing = TRUE;
+ for (;;) {
+ if (use_cork)
+ o_stream_cork(ostream);
+ ret = wrapper_ostream_callback(wostream);
+ if (use_cork && !wostream->output_closed) {
+ int fret = o_stream_uncork_flush(ostream);
+ if (ret == 0 && fret > 0)
+ continue;
+ if (fret < 0 && ret >= 0) {
+ i_assert(ostream->stream_errno != 0);
+ (void)wrapper_ostream_callback(wostream);
+ ret = -1;
+ }
+ }
+ break;
+ }
+ wostream->continuing = FALSE;
+ if (wostream->output_closed)
+ o_stream_close(ostream);
+
+ if (ret == 0)
+ wostream->flush_pending = TRUE;
+
+ if (!stream->ostream.blocking)
+ wrapper_ostream_output_manage(wostream, FALSE);
+
+ if (ret < 0 || ostream->stream_errno != 0 ||
+ wostream->pending_errno != 0)
+ ret = -1;
+ else if (wostream->output_closed)
+ ret = 1;
+ else if (!wrapper_ostream_is_empty(wostream) &&
+ (!stream->corked || wrapper_ostream_is_filled(wostream)))
+ ret = 0;
+ else if (wostream->flush_pending)
+ ret = 0;
+
+ o_stream_unref(&ostream);
+
+ return ret;
+}
+
+void wrapper_ostream_trigger_flush(struct wrapper_ostream *wostream)
+{
+ struct ostream *ostream = &wostream->ostream.ostream;
+
+ if (ostream->closed)
+ return;
+ if (wostream->to_event != NULL)
+ return;
+ if (!wostream->flush_waiting && wostream->ostream.callback == NULL)
+ return;
+
+ wostream->to_event = timeout_add_short(
+ 0, wrapper_ostream_handle_event, wostream);
+}
+
+bool wrapper_ostream_get_buffered_size(struct wrapper_ostream *wostream,
+ uoff_t *size_r)
+{
+ buffer_t *buffer = wostream->buffer;
+
+ if (!wostream->ostream.finished)
+ return FALSE;
+
+ *size_r = (buffer == NULL ? 0 : (uoff_t)buffer->used);
+ i_assert(*size_r == wostream->ostream.ostream.offset);
+ return TRUE;
+}
+
+void wrapper_ostream_output_available(struct wrapper_ostream *wostream,
+ struct ostream *output)
+{
+ i_assert(!wostream->output_closed);
+ i_assert(!wostream->output_finished);
+ i_assert(wostream->output == NULL);
+ wostream->output = output;
+ if (output != NULL) {
+ if (wostream->ostream.corked)
+ o_stream_cork(wostream->output);
+ o_stream_ref(output);
+ }
+}
+
+void wrapper_ostream_output_destroyed(struct wrapper_ostream *wostream)
+{
+ struct ostream *ostream = &wostream->ostream.ostream;
+
+ wrapper_ostream_trigger_flush(wostream);
+ o_stream_set_no_error_handling(ostream, TRUE);
+
+ o_stream_unref(&wostream->output);
+ wostream->output_closed = TRUE;
+ wostream->output_finished = TRUE;
+}
+
+void wrapper_ostream_set_error(struct wrapper_ostream *wostream,
+ int stream_errno, const char *stream_error)
+{
+ struct ostream *ostream = &wostream->ostream.ostream;
+
+ if (ostream->closed || wostream->pending_errno != 0 ||
+ wostream->returned_error)
+ return;
+
+ i_assert(wostream->pending_error == NULL);
+ wostream->pending_errno = stream_errno;
+ wostream->pending_error = i_strdup(stream_error);
+
+ wrapper_ostream_trigger_flush(wostream);
+}
+
+void wrapper_ostream_notify_error(struct wrapper_ostream *wostream)
+{
+ struct ostream *ostream = &wostream->ostream.ostream;
+
+ if (ostream->closed || ostream->blocking ||
+ wostream->output_closed_api || wostream->returned_error ||
+ wostream->continuing)
+ return;
+ if (wostream->pending_errno == 0)
+ return;
+ wostream->returned_error = TRUE;
+ (void)wrapper_ostream_callback(wostream);
+}
diff --git a/src/lib/ostream-wrapper.h b/src/lib/ostream-wrapper.h
new file mode 100644
index 0000000..3fb6ccc
--- /dev/null
+++ b/src/lib/ostream-wrapper.h
@@ -0,0 +1,165 @@
+#ifndef OSTREAM_WRAPPER_H
+#define OSTREAM_WRAPPER_H
+
+#include "ostream-private.h"
+
+/* The wrapper output stream allows turning any form* of activity involving data
+ output into a standard Dovecot output stream. The wrapper output stream can
+ operate both in blocking and non-blocking mode. When the wrapped activity is
+ non-blocking, a blocking wrapper output stream will implicitly run its own
+ ioloop.
+
+ It is possible to have the wrapper output stream object available even before
+ the data can be written anywhere, even before any form of output object (a
+ connection) exists. In that case, any data written to the wrapper stream is
+ buffered until the buffer is full. Once that happens, the stream will block
+ or refuse writes until the underlying output becomes available.
+
+ The wrapper output stream is not meant to be used directly. Instead, it is
+ to be used as part of the implementation of an application-specific output
+ stream. The wrapper output stream serves as the means to prevent code
+ duplication between similar output stream implementations. It defines several
+ methods that need to be implemented by the application-specific output
+ stream.
+
+ * Currently, the wrapper stream still expects an output stream object when
+ data is to be written somewhere, but that should be easily circumvented
+ once such behavior is needed (FIXME).
+ */
+
+struct wrapper_ostream {
+ struct ostream_private ostream;
+ struct event *event;
+
+ /* Called when the implementation should start making the parent output
+ stream available, e.g. connect to the server. This happens when data
+ was written to the wrapper ostream (when it is corked this only
+ happens when the wrapper ostream buffer is full or the wrapper
+ ostream is finished). */
+ void (*output_start)(struct wrapper_ostream *wostream);
+ /* Returns TRUE when the output is ready for data. */
+ bool (*output_ready)(struct wrapper_ostream *wostream);
+ /* Called when an error occurred while writing to the output stream. */
+ void (*output_error)(struct wrapper_ostream *wostream);
+ /* Called when the wrapper ostream was finished using o_stream_finish()
+ and the wrapper ostream buffer is empty. Also, the parent output
+ was flushed successfully. */
+ int (*output_finish)(struct wrapper_ostream *wostream);
+ /* Called when the wrapper ostream does not need write to parent output
+ stream. This is will e.g. drop the parent output's flush callback or
+ equivalent notification mechanism. */
+ void (*output_halt)(struct wrapper_ostream *wostream);
+ /* Called when the wrapper ostream has data available for the parent
+ output and wants wrapper_ostream_continue() to be called when the
+ parent stream is writeable. */
+ void (*output_resume)(struct wrapper_ostream *wostream);
+ /* Update the timeouts. The sender_blocking parameter indicates which
+ side of the data transfer is blocking, so whether a timeout needs to
+ be set for limiting the time other side is not doing anything. */
+ void (*output_update_timeouts)(struct wrapper_ostream *wostream,
+ bool sender_blocking);
+
+ /* Called before and after running ioloop for performing blocking I/O
+ wait. Use these vfuncs to switch to and from the temporary ioloop. */
+ struct ioloop *(*wait_begin)(struct wrapper_ostream *wostream,
+ struct ioloop *ioloop);
+ void (*wait_end)(struct wrapper_ostream *wostream,
+ struct ioloop *prev_ioloop);
+
+ /* Called before and after running the flush callback for the ostream.
+ */
+ void (*callback_pre)(struct wrapper_ostream *wostream);
+ void (*callback_post)(struct wrapper_ostream *wostream);
+
+ /* Called when the ostream is switched to a different ioloop. */
+ void (*switch_ioloop_to)(struct wrapper_ostream *wostream,
+ struct ioloop *ioloop);
+
+ /* Called when the wrapper ostream is forcibly closed using
+ o_stream_close() (or indirectly through e.g. o_stream_destroy()). */
+ void (*close)(struct wrapper_ostream *wostream);
+ /* Called when the ostream is destroyed. */
+ void (*destroy)(struct wrapper_ostream *wostream);
+
+ buffer_t *buffer; // FIXME: use a ringbuffer instead (file_ostream)
+
+ /* The (parent) output stream. */
+ struct ostream *output;
+
+ /* The ioloop used while flushing/sending output for when the wrapper
+ ostream is blocking. */
+ struct ioloop *flush_ioloop;
+
+ /* Error set using wrapper_ostream_return_error(). This is returned to
+ the application once it continues using the wrapper ostream. */
+ char *pending_error;
+ int pending_errno;
+
+ /* Timeout for delayed execution of wrapper_ostream_continue(). */
+ struct timeout *to_event;
+
+ /* Output was started (output_start() vfunc was called). */
+ bool output_started:1;
+ /* Output was finished (output_finish() vfunc was called). */
+ bool output_finished:1;
+ /* Output was was closed somehow. This means that the output is no
+ longer available. This is not the same as the ostream close flag. */
+ bool output_closed:1;
+ /* Output was closed directly or indirectly by the application action.
+ */
+ bool output_closed_api:1;
+
+ bool flush_pending:1;
+ bool flush_waiting:1;
+ bool flushing:1;
+ bool continuing:1;
+ bool returned_error:1;
+};
+
+/* Create the wrapper output stream. This function calls o_stream_create()
+ internally. The initial maximum buffer size is set to max_buffer_size. When
+ blocking is TRUE, a blocking output stream will be created. The provided
+ event is used internally for debug logging. */
+struct ostream *
+wrapper_ostream_create(struct wrapper_ostream *wostream,
+ size_t max_buffer_size, bool blocking,
+ struct event *event) ATTR_NULL(4);
+
+/* Continue sending output. Returns 1 if all buffered data is sent so far,
+ 0 if not, and -1 if an error occurred. */
+int wrapper_ostream_continue(struct wrapper_ostream *wostream);
+/* Trigger an (asynchronous) flush on the output stream. */
+void wrapper_ostream_trigger_flush(struct wrapper_ostream *wostream);
+
+/* This function returns the size of the data buffered in the wrapper stream,
+ but only when the output stream is finished using o_stream_finish(). When the
+ output stream is finished, the data is complete and this function returns
+ TRUE and size_r is set to the size. If it is not complete, this function
+ returns FALSE and size_r is not assigned. This function is meant to be called
+ just before sending the first block of data internally for deciding between
+ sending the data using a chunked transfer encoding or, when it is already
+ complete, as a single blob with known size. E.g., for HTTP this is the choice
+ between sending the message using the Transfer-Encoding: chunked header or
+ the Content-Length header. */
+bool wrapper_ostream_get_buffered_size(struct wrapper_ostream *wostream,
+ uoff_t *size_r);
+
+/* Call this when the underlying output stream first becomes available. */
+void wrapper_ostream_output_available(struct wrapper_ostream *wostream,
+ struct ostream *output);
+/* Call this to notify the wrapper that the underlying output is destroyed and
+ no more data can be written ever. */
+void wrapper_ostream_output_destroyed(struct wrapper_ostream *wostream);
+
+/* Call this to notify the wrapper that an error has occurred. It will be
+ returned as such for the next stream write/flush and subsequent
+ o_stream_get_error(). */
+void wrapper_ostream_set_error(struct wrapper_ostream *wostream,
+ int stream_errno, const char *stream_error);
+/* Notify the application immediately about any error condition set earlier
+ using wrapper_ostream_set_error() by calling the ostream flush callback
+ right now.
+ */
+void wrapper_ostream_notify_error(struct wrapper_ostream *wostream);
+
+#endif
diff --git a/src/lib/ostream.c b/src/lib/ostream.c
new file mode 100644
index 0000000..2f9c49d
--- /dev/null
+++ b/src/lib/ostream.c
@@ -0,0 +1,804 @@
+/* Copyright (c) 2002-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "istream.h"
+#include "ostream-private.h"
+
+void o_stream_set_name(struct ostream *stream, const char *name)
+{
+ i_free(stream->real_stream->iostream.name);
+ stream->real_stream->iostream.name = i_strdup(name);
+}
+
+const char *o_stream_get_name(struct ostream *stream)
+{
+ while (stream->real_stream->iostream.name == NULL) {
+ stream = stream->real_stream->parent;
+ if (stream == NULL)
+ return "";
+ }
+ return stream->real_stream->iostream.name;
+}
+
+int o_stream_get_fd(struct ostream *stream)
+{
+ return stream->real_stream->fd;
+}
+
+const char *o_stream_get_error(struct ostream *stream)
+{
+ struct ostream *s;
+
+ /* we'll only return errors for streams that have stream_errno set.
+ we might be returning unintended error otherwise. */
+ if (stream->stream_errno == 0)
+ return "<no error>";
+
+ for (s = stream; s != NULL; s = s->real_stream->parent) {
+ if (s->stream_errno == 0)
+ break;
+ if (s->real_stream->iostream.error != NULL)
+ return s->real_stream->iostream.error;
+ }
+ return strerror(stream->stream_errno);
+}
+
+const char *o_stream_get_disconnect_reason(struct ostream *stream)
+{
+ return io_stream_get_disconnect_reason(NULL, stream);
+}
+
+static void o_stream_close_full(struct ostream *stream, bool close_parents)
+{
+ /* Ideally o_stream_finish() would be called for all non-failed
+ ostreams, but strictly requiring it would cause unnecessary
+ complexity for many callers. Just require that at this point
+ after flushing there isn't anything in the output buffer or that
+ we're ignoring all errors. */
+ if (o_stream_flush(stream) == 0)
+ i_assert(stream->real_stream->error_handling_disabled);
+
+ if (!stream->closed && !stream->real_stream->closing) {
+ /* first mark the stream as being closed so the
+ o_stream_copy_error_from_parent() won't recurse us back
+ here. but don't immediately mark the stream closed, because
+ we may still want to write something to it. */
+ stream->real_stream->closing = TRUE;
+ io_stream_close(&stream->real_stream->iostream, close_parents);
+ stream->closed = TRUE;
+ }
+
+ if (stream->stream_errno == 0)
+ stream->stream_errno = EPIPE;
+}
+
+void o_stream_destroy(struct ostream **_stream)
+{
+ struct ostream *stream = *_stream;
+
+ if (stream == NULL)
+ return;
+
+ *_stream = NULL;
+ o_stream_close_full(stream, FALSE);
+ o_stream_unref(&stream);
+}
+
+void o_stream_ref(struct ostream *stream)
+{
+ io_stream_ref(&stream->real_stream->iostream);
+}
+
+void o_stream_unref(struct ostream **_stream)
+{
+ struct ostream *stream;
+
+ if (*_stream == NULL)
+ return;
+
+ stream = *_stream;
+
+ if (stream->real_stream->last_errors_not_checked &&
+ !stream->real_stream->error_handling_disabled &&
+ stream->real_stream->iostream.refcount == 1) {
+ i_panic("output stream %s is missing error handling",
+ o_stream_get_name(stream));
+ }
+
+ if (!io_stream_unref(&stream->real_stream->iostream))
+ io_stream_free(&stream->real_stream->iostream);
+ *_stream = NULL;
+}
+
+#undef o_stream_add_destroy_callback
+void o_stream_add_destroy_callback(struct ostream *stream,
+ ostream_callback_t *callback, void *context)
+{
+ io_stream_add_destroy_callback(&stream->real_stream->iostream,
+ callback, context);
+}
+
+void o_stream_remove_destroy_callback(struct ostream *stream,
+ void (*callback)())
+{
+ io_stream_remove_destroy_callback(&stream->real_stream->iostream,
+ callback);
+}
+
+void o_stream_close(struct ostream *stream)
+{
+ if (stream != NULL)
+ o_stream_close_full(stream, TRUE);
+}
+
+#undef o_stream_set_flush_callback
+void o_stream_set_flush_callback(struct ostream *stream,
+ stream_flush_callback_t *callback,
+ void *context)
+{
+ struct ostream_private *_stream = stream->real_stream;
+
+ _stream->set_flush_callback(_stream, callback, context);
+}
+
+void o_stream_unset_flush_callback(struct ostream *stream)
+{
+ struct ostream_private *_stream = stream->real_stream;
+
+ _stream->set_flush_callback(_stream, NULL, NULL);
+}
+
+void o_stream_set_max_buffer_size(struct ostream *stream, size_t max_size)
+{
+ io_stream_set_max_buffer_size(&stream->real_stream->iostream, max_size);
+}
+
+size_t o_stream_get_max_buffer_size(struct ostream *stream)
+{
+ return stream->real_stream->max_buffer_size;
+}
+
+void o_stream_cork(struct ostream *stream)
+{
+ struct ostream_private *_stream = stream->real_stream;
+
+ if (unlikely(stream->closed || stream->stream_errno != 0))
+ return;
+
+ _stream->cork(_stream, TRUE);
+}
+
+void o_stream_uncork(struct ostream *stream)
+{
+ struct ostream_private *_stream = stream->real_stream;
+
+ if (unlikely(stream->closed || stream->stream_errno != 0))
+ return;
+
+ _stream->cork(_stream, FALSE);
+}
+
+bool o_stream_is_corked(struct ostream *stream)
+{
+ struct ostream_private *_stream = stream->real_stream;
+
+ return _stream->corked;
+}
+
+int o_stream_flush(struct ostream *stream)
+{
+ struct ostream_private *_stream = stream->real_stream;
+ int ret = 1;
+
+ o_stream_ignore_last_errors(stream);
+
+ if (unlikely(stream->closed || stream->stream_errno != 0)) {
+ errno = stream->stream_errno;
+ return -1;
+ }
+
+ if (unlikely(_stream->noverflow)) {
+ io_stream_set_error(&_stream->iostream,
+ "Output stream buffer was full (%zu bytes)",
+ o_stream_get_max_buffer_size(stream));
+ errno = stream->stream_errno = ENOBUFS;
+ return -1;
+ }
+
+ if (unlikely((ret = _stream->flush(_stream)) < 0)) {
+ i_assert(stream->stream_errno != 0);
+ errno = stream->stream_errno;
+ }
+ return ret;
+}
+
+void o_stream_set_flush_pending(struct ostream *stream, bool set)
+{
+ struct ostream_private *_stream = stream->real_stream;
+
+ if (unlikely(stream->closed || stream->stream_errno != 0))
+ return;
+
+ _stream->flush_pending(_stream, set);
+}
+
+size_t o_stream_get_buffer_used_size(const struct ostream *stream)
+{
+ const struct ostream_private *_stream = stream->real_stream;
+
+ return _stream->get_buffer_used_size(_stream);
+}
+
+size_t o_stream_get_buffer_avail_size(const struct ostream *stream)
+{
+ const struct ostream_private *_stream = stream->real_stream;
+
+ return _stream->get_buffer_avail_size(_stream);
+}
+
+int o_stream_seek(struct ostream *stream, uoff_t offset)
+{
+ struct ostream_private *_stream = stream->real_stream;
+
+ if (unlikely(stream->closed || stream->stream_errno != 0)) {
+ errno = stream->stream_errno;
+ return -1;
+ }
+
+ if (unlikely(_stream->seek(_stream, offset) < 0)) {
+ i_assert(stream->stream_errno != 0);
+ errno = stream->stream_errno;
+ return -1;
+ }
+ return 1;
+}
+
+ssize_t o_stream_send(struct ostream *stream, const void *data, size_t size)
+{
+ struct const_iovec iov;
+
+ i_zero(&iov);
+ iov.iov_base = data;
+ iov.iov_len = size;
+
+ return o_stream_sendv(stream, &iov, 1);
+}
+
+static ssize_t
+o_stream_sendv_int(struct ostream *stream, const struct const_iovec *iov,
+ unsigned int iov_count, bool *overflow_r)
+{
+ struct ostream_private *_stream = stream->real_stream;
+ unsigned int i;
+ size_t total_size;
+ ssize_t ret;
+
+ *overflow_r = FALSE;
+
+ for (i = 0, total_size = 0; i < iov_count; i++)
+ total_size += iov[i].iov_len;
+ if (total_size == 0)
+ return 0;
+
+ i_assert(!_stream->finished);
+ ret = _stream->sendv(_stream, iov, iov_count);
+ if (ret > 0)
+ stream->real_stream->last_write_timeval = ioloop_timeval;
+ if (unlikely(ret != (ssize_t)total_size)) {
+ if (ret < 0) {
+ i_assert(stream->stream_errno != 0);
+ errno = stream->stream_errno;
+ } else {
+ i_assert(!stream->blocking);
+ stream->overflow = TRUE;
+ *overflow_r = TRUE;
+ }
+ }
+ return ret;
+}
+
+ssize_t o_stream_sendv(struct ostream *stream, const struct const_iovec *iov,
+ unsigned int iov_count)
+{
+ bool overflow;
+
+ if (unlikely(stream->closed || stream->stream_errno != 0)) {
+ errno = stream->stream_errno;
+ return -1;
+ }
+ return o_stream_sendv_int(stream, iov, iov_count, &overflow);
+}
+
+ssize_t o_stream_send_str(struct ostream *stream, const char *str)
+{
+ return o_stream_send(stream, str, strlen(str));
+}
+
+void o_stream_nsend(struct ostream *stream, const void *data, size_t size)
+{
+ struct const_iovec iov;
+
+ i_zero(&iov);
+ iov.iov_base = data;
+ iov.iov_len = size;
+
+ o_stream_nsendv(stream, &iov, 1);
+}
+
+void o_stream_nsendv(struct ostream *stream, const struct const_iovec *iov,
+ unsigned int iov_count)
+{
+ bool overflow;
+
+ if (unlikely(stream->closed || stream->stream_errno != 0 ||
+ stream->real_stream->noverflow))
+ return;
+ (void)o_stream_sendv_int(stream, iov, iov_count, &overflow);
+ if (overflow)
+ stream->real_stream->noverflow = TRUE;
+ stream->real_stream->last_errors_not_checked = TRUE;
+}
+
+void o_stream_nsend_str(struct ostream *stream, const char *str)
+{
+ o_stream_nsend(stream, str, strlen(str));
+}
+
+int o_stream_finish(struct ostream *stream)
+{
+ stream->real_stream->finished = TRUE;
+ return o_stream_flush(stream);
+}
+
+void o_stream_set_finish_also_parent(struct ostream *stream, bool set)
+{
+ stream->real_stream->finish_also_parent = set;
+}
+
+void o_stream_set_finish_via_child(struct ostream *stream, bool set)
+{
+ stream->real_stream->finish_via_child = set;
+}
+
+void o_stream_ignore_last_errors(struct ostream *stream)
+{
+ while (stream != NULL) {
+ stream->real_stream->last_errors_not_checked = FALSE;
+ stream = stream->real_stream->parent;
+ }
+}
+
+void o_stream_abort(struct ostream *stream)
+{
+ o_stream_ignore_last_errors(stream);
+ if (stream->stream_errno != 0)
+ return;
+ io_stream_set_error(&stream->real_stream->iostream, "aborted writing");
+ stream->stream_errno = EPIPE;
+}
+
+void o_stream_set_no_error_handling(struct ostream *stream, bool set)
+{
+ stream->real_stream->error_handling_disabled = set;
+}
+
+enum ostream_send_istream_result
+o_stream_send_istream(struct ostream *outstream, struct istream *instream)
+{
+ struct ostream_private *_outstream = outstream->real_stream;
+ uoff_t old_outstream_offset = outstream->offset;
+ uoff_t old_instream_offset = instream->v_offset;
+ enum ostream_send_istream_result res;
+
+ if (unlikely(instream->closed || instream->stream_errno != 0)) {
+ errno = instream->stream_errno;
+ return OSTREAM_SEND_ISTREAM_RESULT_ERROR_INPUT;
+ }
+ if (unlikely(outstream->closed || outstream->stream_errno != 0)) {
+ errno = outstream->stream_errno;
+ return OSTREAM_SEND_ISTREAM_RESULT_ERROR_OUTPUT;
+ }
+
+ i_assert(!_outstream->finished);
+ res = _outstream->send_istream(_outstream, instream);
+ switch (res) {
+ case OSTREAM_SEND_ISTREAM_RESULT_FINISHED:
+ i_assert(instream->stream_errno == 0);
+ i_assert(outstream->stream_errno == 0);
+ i_assert(!i_stream_have_bytes_left(instream));
+ break;
+ case OSTREAM_SEND_ISTREAM_RESULT_WAIT_INPUT:
+ i_assert(!instream->blocking);
+ break;
+ case OSTREAM_SEND_ISTREAM_RESULT_WAIT_OUTPUT:
+ i_assert(!outstream->blocking);
+ o_stream_set_flush_pending(outstream, TRUE);
+ break;
+ case OSTREAM_SEND_ISTREAM_RESULT_ERROR_INPUT:
+ i_assert(instream->stream_errno != 0);
+ return res;
+ case OSTREAM_SEND_ISTREAM_RESULT_ERROR_OUTPUT:
+ i_assert(outstream->stream_errno != 0);
+ return res;
+ }
+ /* non-failure - make sure stream offsets match */
+ i_assert((outstream->offset - old_outstream_offset) ==
+ (instream->v_offset - old_instream_offset));
+
+ if (outstream->offset != old_outstream_offset)
+ outstream->real_stream->last_write_timeval = ioloop_timeval;
+ return res;
+}
+
+void o_stream_nsend_istream(struct ostream *outstream, struct istream *instream)
+{
+ i_assert(instream->blocking);
+
+ switch (o_stream_send_istream(outstream, instream)) {
+ case OSTREAM_SEND_ISTREAM_RESULT_FINISHED:
+ break;
+ case OSTREAM_SEND_ISTREAM_RESULT_WAIT_INPUT:
+ i_unreached();
+ case OSTREAM_SEND_ISTREAM_RESULT_WAIT_OUTPUT:
+ outstream->real_stream->noverflow = TRUE;
+ break;
+ case OSTREAM_SEND_ISTREAM_RESULT_ERROR_INPUT:
+ outstream->stream_errno = instream->stream_errno;
+ io_stream_set_error(&outstream->real_stream->iostream,
+ "nsend-istream: read(%s) failed: %s",
+ i_stream_get_name(instream),
+ i_stream_get_error(instream));
+ break;
+ case OSTREAM_SEND_ISTREAM_RESULT_ERROR_OUTPUT:
+ break;
+ }
+ outstream->real_stream->last_errors_not_checked = TRUE;
+}
+
+int o_stream_pwrite(struct ostream *stream, const void *data, size_t size,
+ uoff_t offset)
+{
+ int ret;
+
+ if (unlikely(stream->closed || stream->stream_errno != 0)) {
+ errno = stream->stream_errno;
+ return -1;
+ }
+
+ i_assert(!stream->real_stream->finished);
+ ret = stream->real_stream->write_at(stream->real_stream,
+ data, size, offset);
+ if (ret > 0)
+ stream->real_stream->last_write_timeval = ioloop_timeval;
+ else if (unlikely(ret < 0)) {
+ i_assert(stream->stream_errno != 0);
+ errno = stream->stream_errno;
+ }
+
+ return ret;
+}
+
+void o_stream_get_last_write_time(struct ostream *stream, struct timeval *tv_r)
+{
+ *tv_r = stream->real_stream->last_write_timeval;
+}
+
+enum ostream_send_istream_result
+io_stream_copy(struct ostream *outstream, struct istream *instream)
+{
+ struct const_iovec iov;
+ const unsigned char *data;
+ ssize_t ret;
+
+ while (i_stream_read_more(instream, &data, &iov.iov_len) > 0) {
+ iov.iov_base = data;
+ if ((ret = o_stream_sendv(outstream, &iov, 1)) < 0)
+ return OSTREAM_SEND_ISTREAM_RESULT_ERROR_OUTPUT;
+ else if (ret == 0)
+ return OSTREAM_SEND_ISTREAM_RESULT_WAIT_OUTPUT;
+ i_stream_skip(instream, ret);
+ }
+
+ if (instream->stream_errno != 0)
+ return OSTREAM_SEND_ISTREAM_RESULT_ERROR_INPUT;
+ if (i_stream_have_bytes_left(instream))
+ return OSTREAM_SEND_ISTREAM_RESULT_WAIT_INPUT;
+ return OSTREAM_SEND_ISTREAM_RESULT_FINISHED;
+}
+
+void o_stream_switch_ioloop_to(struct ostream *stream, struct ioloop *ioloop)
+{
+ struct ostream_private *_stream = stream->real_stream;
+
+ io_stream_switch_ioloop_to(&_stream->iostream, ioloop);
+
+ _stream->switch_ioloop_to(_stream, ioloop);
+}
+
+void o_stream_switch_ioloop(struct ostream *stream)
+{
+ o_stream_switch_ioloop_to(stream, current_ioloop);
+}
+
+static void o_stream_default_close(struct iostream_private *stream,
+ bool close_parent)
+{
+ struct ostream_private *_stream =
+ container_of(stream, struct ostream_private, iostream);
+
+ (void)o_stream_flush(&_stream->ostream);
+ if (close_parent)
+ o_stream_close(_stream->parent);
+}
+
+static void o_stream_default_destroy(struct iostream_private *stream)
+{
+ struct ostream_private *_stream =
+ container_of(stream, struct ostream_private, iostream);
+
+ o_stream_unref(&_stream->parent);
+}
+
+static void
+o_stream_default_set_max_buffer_size(struct iostream_private *stream,
+ size_t max_size)
+{
+ struct ostream_private *_stream =
+ container_of(stream, struct ostream_private, iostream);
+
+ if (_stream->parent != NULL)
+ o_stream_set_max_buffer_size(_stream->parent, max_size);
+ _stream->max_buffer_size = max_size;
+}
+
+static void o_stream_default_cork(struct ostream_private *_stream, bool set)
+{
+ _stream->corked = set;
+ if (set) {
+ if (_stream->parent != NULL)
+ o_stream_cork(_stream->parent);
+ } else {
+ (void)o_stream_flush(&_stream->ostream);
+ _stream->last_errors_not_checked = TRUE;
+
+ if (_stream->parent != NULL)
+ o_stream_uncork(_stream->parent);
+ }
+}
+
+void o_stream_copy_error_from_parent(struct ostream_private *_stream)
+{
+ struct ostream *src = _stream->parent;
+ struct ostream *dest = &_stream->ostream;
+
+ i_assert(src->stream_errno != 0);
+
+ dest->stream_errno = src->stream_errno;
+ dest->overflow = src->overflow;
+ if (src->closed)
+ o_stream_close(dest);
+}
+
+int o_stream_flush_parent_if_needed(struct ostream_private *_stream)
+{
+ if (o_stream_get_buffer_used_size(_stream->parent) >= IO_BLOCK_SIZE) {
+ /* we already have quite a lot of data in parent stream.
+ unless we can flush it, don't add any more to it or we
+ could keep wasting memory by just increasing the buffer
+ size all the time. */
+ if (o_stream_flush(_stream->parent) < 0) {
+ o_stream_copy_error_from_parent(_stream);
+ return -1;
+ }
+ if (o_stream_get_buffer_used_size(_stream->parent) >= IO_BLOCK_SIZE)
+ return 0;
+ }
+ return 1;
+}
+
+int o_stream_flush_parent(struct ostream_private *_stream)
+{
+ int ret;
+
+ i_assert(_stream->parent != NULL);
+
+ if (!_stream->finished || !_stream->finish_also_parent ||
+ !_stream->parent->real_stream->finish_via_child)
+ ret = o_stream_flush(_stream->parent);
+ else
+ ret = o_stream_finish(_stream->parent);
+ if (ret < 0)
+ o_stream_copy_error_from_parent(_stream);
+ return ret;
+}
+
+static int o_stream_default_flush(struct ostream_private *_stream)
+{
+ if (_stream->parent == NULL)
+ return 1;
+
+ return o_stream_flush_parent(_stream);
+}
+
+static void
+o_stream_default_set_flush_callback(struct ostream_private *_stream,
+ stream_flush_callback_t *callback,
+ void *context)
+{
+ if (_stream->parent != NULL)
+ o_stream_set_flush_callback(_stream->parent, callback, context);
+
+ _stream->callback = callback;
+ _stream->context = context;
+}
+
+static void
+o_stream_default_set_flush_pending(struct ostream_private *_stream, bool set)
+{
+ if (_stream->parent != NULL)
+ o_stream_set_flush_pending(_stream->parent, set);
+}
+
+static size_t
+o_stream_default_get_buffer_used_size(const struct ostream_private *_stream)
+{
+ if (_stream->parent == NULL)
+ return 0;
+ else
+ return o_stream_get_buffer_used_size(_stream->parent);
+}
+
+static size_t
+o_stream_default_get_buffer_avail_size(const struct ostream_private *_stream)
+{
+ /* This default implementation assumes that the returned buffer size is
+ between 0..max_buffer_size. There's no assert though, in case the
+ max_buffer_size changes. */
+ size_t used = o_stream_get_buffer_used_size(&_stream->ostream);
+
+ return _stream->max_buffer_size <= used ? 0 :
+ _stream->max_buffer_size - used;
+}
+
+static int
+o_stream_default_seek(struct ostream_private *_stream,
+ uoff_t offset ATTR_UNUSED)
+{
+ _stream->ostream.stream_errno = ESPIPE;
+ return -1;
+}
+
+static ssize_t
+o_stream_default_sendv(struct ostream_private *stream,
+ const struct const_iovec *iov, unsigned int iov_count)
+{
+ ssize_t ret;
+
+ if ((ret = o_stream_sendv(stream->parent, iov, iov_count)) < 0) {
+ o_stream_copy_error_from_parent(stream);
+ return -1;
+ }
+ stream->ostream.offset += ret;
+ return ret;
+}
+
+static int
+o_stream_default_write_at(struct ostream_private *_stream,
+ const void *data ATTR_UNUSED,
+ size_t size ATTR_UNUSED, uoff_t offset ATTR_UNUSED)
+{
+ _stream->ostream.stream_errno = ESPIPE;
+ return -1;
+}
+
+static enum ostream_send_istream_result
+o_stream_default_send_istream(struct ostream_private *outstream,
+ struct istream *instream)
+{
+ return io_stream_copy(&outstream->ostream, instream);
+}
+
+static void
+o_stream_default_switch_ioloop_to(struct ostream_private *_stream,
+ struct ioloop *ioloop)
+{
+ if (_stream->parent != NULL)
+ o_stream_switch_ioloop_to(_stream->parent, ioloop);
+}
+
+struct ostream *
+o_stream_create(struct ostream_private *_stream, struct ostream *parent, int fd)
+{
+ _stream->finish_also_parent = TRUE;
+ _stream->finish_via_child = TRUE;
+ _stream->fd = fd;
+ _stream->ostream.real_stream = _stream;
+ if (parent != NULL) {
+ _stream->ostream.blocking = parent->blocking;
+ _stream->parent = parent;
+ o_stream_ref(parent);
+
+ _stream->callback = parent->real_stream->callback;
+ _stream->context = parent->real_stream->context;
+ _stream->max_buffer_size = parent->real_stream->max_buffer_size;
+ _stream->error_handling_disabled =
+ parent->real_stream->error_handling_disabled;
+ }
+
+ if (_stream->iostream.close == NULL)
+ _stream->iostream.close = o_stream_default_close;
+ if (_stream->iostream.destroy == NULL)
+ _stream->iostream.destroy = o_stream_default_destroy;
+ if (_stream->iostream.set_max_buffer_size == NULL) {
+ _stream->iostream.set_max_buffer_size =
+ o_stream_default_set_max_buffer_size;
+ }
+
+ if (_stream->cork == NULL)
+ _stream->cork = o_stream_default_cork;
+ if (_stream->flush == NULL)
+ _stream->flush = o_stream_default_flush;
+ if (_stream->set_flush_callback == NULL) {
+ _stream->set_flush_callback =
+ o_stream_default_set_flush_callback;
+ }
+ if (_stream->flush_pending == NULL)
+ _stream->flush_pending = o_stream_default_set_flush_pending;
+ if (_stream->get_buffer_used_size == NULL)
+ _stream->get_buffer_used_size =
+ o_stream_default_get_buffer_used_size;
+ if (_stream->get_buffer_avail_size == NULL) {
+ _stream->get_buffer_avail_size =
+ o_stream_default_get_buffer_avail_size;
+ }
+ if (_stream->seek == NULL)
+ _stream->seek = o_stream_default_seek;
+ if (_stream->sendv == NULL)
+ _stream->sendv = o_stream_default_sendv;
+ if (_stream->write_at == NULL)
+ _stream->write_at = o_stream_default_write_at;
+ if (_stream->send_istream == NULL)
+ _stream->send_istream = o_stream_default_send_istream;
+ if (_stream->switch_ioloop_to == NULL)
+ _stream->switch_ioloop_to = o_stream_default_switch_ioloop_to;
+
+ io_stream_init(&_stream->iostream);
+ return &_stream->ostream;
+}
+
+struct ostream *o_stream_create_error(int stream_errno)
+{
+ struct ostream_private *stream;
+ struct ostream *output;
+
+ stream = i_new(struct ostream_private, 1);
+ stream->ostream.blocking = TRUE;
+ stream->ostream.closed = TRUE;
+ stream->ostream.stream_errno = stream_errno;
+
+ output = o_stream_create(stream, NULL, -1);
+ o_stream_set_no_error_handling(output, TRUE);
+ o_stream_set_name(output, "(error)");
+ return output;
+}
+
+struct ostream *
+o_stream_create_error_str(int stream_errno, const char *fmt, ...)
+{
+ struct ostream *output;
+ va_list args;
+
+ va_start(args, fmt);
+ output = o_stream_create_error(stream_errno);
+ io_stream_set_verror(&output->real_stream->iostream, fmt, args);
+ va_end(args);
+ return output;
+}
+
+struct ostream *o_stream_create_passthrough(struct ostream *output)
+{
+ struct ostream_private *stream;
+
+ stream = i_new(struct ostream_private, 1);
+ return o_stream_create(stream, output, o_stream_get_fd(output));
+}
diff --git a/src/lib/ostream.h b/src/lib/ostream.h
new file mode 100644
index 0000000..2063847
--- /dev/null
+++ b/src/lib/ostream.h
@@ -0,0 +1,260 @@
+#ifndef OSTREAM_H
+#define OSTREAM_H
+
+#include "ioloop.h"
+
+enum ostream_send_istream_result {
+ /* All of the istream was successfully sent to ostream. */
+ OSTREAM_SEND_ISTREAM_RESULT_FINISHED,
+ /* Caller needs to wait for more input from non-blocking istream. */
+ OSTREAM_SEND_ISTREAM_RESULT_WAIT_INPUT,
+ /* Caller needs to wait for output to non-blocking ostream.
+ o_stream_set_flush_pending() is automatically called. */
+ OSTREAM_SEND_ISTREAM_RESULT_WAIT_OUTPUT,
+ /* Read from istream failed. See istream->stream_errno. */
+ OSTREAM_SEND_ISTREAM_RESULT_ERROR_INPUT,
+ /* Write to ostream failed. See ostream->stream_errno. */
+ OSTREAM_SEND_ISTREAM_RESULT_ERROR_OUTPUT
+};
+
+enum ostream_create_file_flags {
+ /* without append, file is truncated */
+ OSTREAM_CREATE_FILE_FLAG_APPEND = BIT(0),
+};
+
+struct ostream {
+ /* Number of bytes sent via o_stream_send*() and similar functions.
+ This is counting the input data. For example with a compressed
+ ostream this is counting the uncompressed bytes. The compressed
+ bytes could be counted from the parent ostream's offset.
+
+ Seeking to a specified offset only makes sense if there is no
+ difference between input and output data sizes (e.g. there are no
+ wrapper ostreams changing the data). */
+ uoff_t offset;
+
+ /* errno for the last operation send/seek operation. cleared before
+ each call. */
+ int stream_errno;
+
+ /* overflow is set when some of the data given to send()
+ functions was neither sent nor buffered. It's never unset inside
+ ostream code. */
+ bool overflow:1;
+ /* o_stream_send() writes all the data or returns failure */
+ bool blocking:1;
+ bool closed:1;
+
+ struct ostream_private *real_stream;
+};
+
+/* Returns 1 if all data is sent (not necessarily flushed), 0 if not.
+ Pretty much the only real reason to return 0 is if you wish to send more
+ data to client which isn't buffered, eg. o_stream_send_istream(). */
+typedef int stream_flush_callback_t(void *context);
+typedef void ostream_callback_t(void *context);
+
+/* Create new output stream from given file descriptor.
+ If max_buffer_size is 0, an "optimal" buffer size is used (max 128kB). */
+struct ostream *o_stream_create_fd(int fd, size_t max_buffer_size);
+/* The fd is set to -1 immediately to avoid accidentally closing it twice. */
+struct ostream *o_stream_create_fd_autoclose(int *fd, size_t max_buffer_size);
+/* Create an output stream from a regular file which begins at given offset.
+ If offset==UOFF_T_MAX, the current offset isn't known. */
+struct ostream *
+o_stream_create_fd_file(int fd, uoff_t offset, bool autoclose_fd);
+struct ostream *o_stream_create_fd_file_autoclose(int *fd, uoff_t offset);
+/* Create ostream for file. If append flag is not set, file will be truncated. */
+struct ostream *o_stream_create_file(const char *path, uoff_t offset, mode_t mode,
+ enum ostream_create_file_flags flags);
+/* Create an output stream to a buffer. Note that the buffer is treated as the
+ ostream's internal buffer. This means that o_stream_get_buffer_used_size()
+ returns buf->used, and _get_buffer_avail_size() returns how many bytes can
+ be written until the buffer's max size is reached. This behavior may make
+ ostream-buffer unsuitable for code that assumes that having bytes in the
+ internal buffer means that ostream isn't finished flushing its internal
+ buffer. Especially o_stream_flush_parent_if_needed() (used by
+ lib-compression ostreams) don't work with this. */
+struct ostream *o_stream_create_buffer(buffer_t *buf);
+/* Create an output streams that always fails the writes. */
+struct ostream *o_stream_create_error(int stream_errno);
+struct ostream *
+o_stream_create_error_str(int stream_errno, const char *fmt, ...)
+ ATTR_FORMAT(2, 3);
+/* Create an output stream that simply passes through data. This is mainly
+ useful as a wrapper when combined with destroy callbacks. */
+struct ostream *o_stream_create_passthrough(struct ostream *output);
+
+/* Set name (e.g. path) for output stream. */
+void o_stream_set_name(struct ostream *stream, const char *name);
+/* Get output stream's name. Returns "" if stream has no name. */
+const char *o_stream_get_name(struct ostream *stream);
+
+/* Return file descriptor for stream, or -1 if none is available. */
+int o_stream_get_fd(struct ostream *stream);
+/* Returns error string for the previous error. */
+const char *o_stream_get_error(struct ostream *stream);
+/* Returns human-readable reason for why ostream was disconnected.
+ The output is either "Connection closed" for clean disconnections or
+ "Connection closed: <error>" for unclean disconnections. This is an
+ alternative to o_stream_get_error(), which is preferred to be used when
+ logging errors about client connections. */
+const char *o_stream_get_disconnect_reason(struct ostream *stream);
+
+/* Close this stream (but not its parents) and unreference it. */
+void o_stream_destroy(struct ostream **stream);
+/* Reference counting. References start from 1, so calling o_stream_unref()
+ destroys the stream if o_stream_ref() is never used. */
+void o_stream_ref(struct ostream *stream);
+/* Unreferences the stream and sets stream pointer to NULL. */
+void o_stream_unref(struct ostream **stream);
+/* Call the given callback function when stream is destroyed. */
+void o_stream_add_destroy_callback(struct ostream *stream,
+ ostream_callback_t *callback, void *context)
+ ATTR_NULL(3);
+#define o_stream_add_destroy_callback(stream, callback, context) \
+ o_stream_add_destroy_callback(stream - \
+ CALLBACK_TYPECHECK(callback, void (*)(typeof(context))), \
+ (ostream_callback_t *)callback, context)
+/* Remove the destroy callback. */
+void o_stream_remove_destroy_callback(struct ostream *stream,
+ void (*callback)());
+
+/* Mark the stream and all of its parent streams closed. Nothing will be
+ sent after this call. When using ostreams that require writing a trailer,
+ o_stream_finish() must be used before the stream is closed. When ostream
+ is destroyed, it's also closed but its parents aren't.
+
+ Closing the ostream (also via destroy) will first flush the ostream, and
+ afterwards requires one of: a) stream has failed, b) there is no more
+ buffered data, c) o_stream_set_no_error_handling() has been called. */
+void o_stream_close(struct ostream *stream);
+
+/* Set IO_WRITE callback. Default will just try to flush the output and
+ finishes when the buffer is empty. */
+void o_stream_set_flush_callback(struct ostream *stream,
+ stream_flush_callback_t *callback,
+ void *context) ATTR_NULL(3);
+#define o_stream_set_flush_callback(stream, callback, context) \
+ o_stream_set_flush_callback(stream - \
+ CALLBACK_TYPECHECK(callback, int (*)(typeof(context))), \
+ (stream_flush_callback_t *)callback, context)
+void o_stream_unset_flush_callback(struct ostream *stream);
+/* Change the maximum size for stream's output buffer to grow. */
+void o_stream_set_max_buffer_size(struct ostream *stream, size_t max_size);
+/* Returns the current max. buffer size. */
+size_t o_stream_get_max_buffer_size(struct ostream *stream);
+
+/* Delays sending as far as possible, writing only full buffers. Also sets
+ TCP_CORK on if supported. */
+void o_stream_cork(struct ostream *stream);
+/* Try to flush the buffer by calling o_stream_flush() and remove TCP_CORK.
+ Note that after this o_stream_flush() must be called, unless the stream
+ ignores errors. */
+void o_stream_uncork(struct ostream *stream);
+bool o_stream_is_corked(struct ostream *stream);
+/* Try to flush the output stream. If o_stream_nsend*() had been used and
+ the stream had overflown, return error. Returns 1 if all data is sent,
+ 0 there's still buffered data, -1 if error. */
+int o_stream_flush(struct ostream *stream);
+/* Wrapper to easily both uncork and flush. */
+static inline int o_stream_uncork_flush(struct ostream *stream)
+{
+ o_stream_uncork(stream);
+ return o_stream_flush(stream);
+}
+
+/* Set "flush pending" state of stream. If set, the flush callback is called
+ when more data is allowed to be sent, even if the buffer itself is empty.
+ Note that if the stream is corked, the flush callback won't be called until
+ the stream is first uncorked. */
+void o_stream_set_flush_pending(struct ostream *stream, bool set);
+/* Returns the number of bytes currently in all the pending write buffers of
+ this ostream, including its parent streams. This function is commonly used
+ by callers to determine when they've filled up the ostream so they can stop
+ writing to it. Because of this, the return value shouldn't include buffers
+ that are expected to be filled up before they send anything to their parent
+ stream. Otherwise the callers may stop writing to the stream too early and
+ hang. Such an example could be a compression ostream that won't send
+ anything to its parent stream before an internal compression buffer is
+ full. */
+size_t o_stream_get_buffer_used_size(const struct ostream *stream) ATTR_PURE;
+/* Returns the (minimum) number of bytes we can still write without failing.
+ This is commonly used by callers to find out how many bytes they're
+ guaranteed to be able to send, and then generate that much data and send
+ it. */
+size_t o_stream_get_buffer_avail_size(const struct ostream *stream) ATTR_PURE;
+
+/* Seek to specified position from beginning of file. This works only for
+ files. Returns 1 if successful, -1 if error. */
+int o_stream_seek(struct ostream *stream, uoff_t offset);
+/* Returns number of bytes sent, -1 = error */
+ssize_t o_stream_send(struct ostream *stream, const void *data, size_t size)
+ ATTR_WARN_UNUSED_RESULT;
+ssize_t o_stream_sendv(struct ostream *stream, const struct const_iovec *iov,
+ unsigned int iov_count) ATTR_WARN_UNUSED_RESULT;
+ssize_t o_stream_send_str(struct ostream *stream, const char *str)
+ ATTR_WARN_UNUSED_RESULT;
+/* Send with delayed error handling. o_stream_flush() or
+ o_stream_ignore_last_errors() must be called after these functions before
+ the stream is destroyed. If any of the data can't be sent due to stream's
+ buffer getting full, all further nsends are ignores and o_stream_flush()
+ will fail. */
+void o_stream_nsend(struct ostream *stream, const void *data, size_t size);
+void o_stream_nsendv(struct ostream *stream, const struct const_iovec *iov,
+ unsigned int iov_count);
+void o_stream_nsend_str(struct ostream *stream, const char *str);
+/* Mark the ostream as finished and flush it. If the ostream has a footer,
+ it's written here. Any further write attempts to the ostream will
+ assert-crash. Returns the same as o_stream_flush(). Afterwards any calls to
+ this function are identical to o_stream_flush(). */
+int o_stream_finish(struct ostream *stream);
+/* Specify whether calling o_stream_finish() will cause the parent stream to
+ be finished as well. The default is yes. */
+void o_stream_set_finish_also_parent(struct ostream *stream, bool set);
+/* Specify whether calling o_stream_finish() on a child stream will cause
+ this stream to be finished as well. The default is yes. */
+void o_stream_set_finish_via_child(struct ostream *stream, bool set);
+/* Marks the stream's error handling as completed to avoid i_panic() on
+ destroy. */
+void o_stream_ignore_last_errors(struct ostream *stream);
+/* Abort writing to the ostream, also marking any previous error handling as
+ completed. If the stream hasn't already failed, sets the stream_errno=EPIPE.
+ This is necessary when aborting write to streams that require finishing. */
+void o_stream_abort(struct ostream *stream);
+/* If error handling is disabled, the i_panic() on destroy is never called.
+ This function can be called immediately after the stream is created.
+ When creating wrapper streams, they copy this behavior from the parent
+ stream. */
+void o_stream_set_no_error_handling(struct ostream *stream, bool set);
+/* Send all of the instream to outstream.
+
+ On non-failure instream is skips over all data written to outstream.
+ This means that the number of bytes written to outstream is always equal to
+ the number of bytes skipped in instream.
+
+ It's also possible to use this function to copy data within same file
+ descriptor, even if the source and destination overlaps. If the file must
+ be grown, you have to do it manually before calling this function. */
+enum ostream_send_istream_result ATTR_WARN_UNUSED_RESULT
+o_stream_send_istream(struct ostream *outstream, struct istream *instream);
+/* Same as o_stream_send_istream(), but assume that reads and writes will
+ succeed. If not, o_stream_flush() will fail with the correct error
+ message (even istream's). */
+void o_stream_nsend_istream(struct ostream *outstream, struct istream *instream);
+
+/* Write data to specified offset. Returns 0 if successful, -1 if error. */
+int o_stream_pwrite(struct ostream *stream, const void *data, size_t size,
+ uoff_t offset);
+
+/* Return the last timestamp when something was successfully sent to the
+ ostream's internal buffers (no guarantees that anything was sent further).
+ The timestamp is 0 if nothing has ever been written. */
+void o_stream_get_last_write_time(struct ostream *stream, struct timeval *tv_r);
+
+/* If there are any I/O loop items associated with the stream, move all of
+ them to provided/current ioloop. */
+void o_stream_switch_ioloop_to(struct ostream *stream, struct ioloop *ioloop);
+void o_stream_switch_ioloop(struct ostream *stream);
+
+#endif
diff --git a/src/lib/path-util.c b/src/lib/path-util.c
new file mode 100644
index 0000000..90e1e46
--- /dev/null
+++ b/src/lib/path-util.c
@@ -0,0 +1,398 @@
+/* Copyright (c) 2009-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "str.h"
+#include "path-util.h"
+
+#include <unistd.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+
+#define PATH_UTIL_MAX_PATH 8*1024
+#define PATH_UTIL_MAX_SYMLINKS 80
+
+static int t_getcwd_noalloc(char **dir_r, size_t *asize_r,
+ const char **error_r) ATTR_NULL(2)
+{
+ /* @UNSAFE */
+ char *dir;
+ size_t asize = 128;
+
+ dir = t_buffer_get(asize);
+ while (getcwd(dir, asize) == NULL) {
+ if (errno != ERANGE) {
+ *error_r = t_strdup_printf("getcwd() failed: %m");
+ return -1;
+ }
+ asize = nearest_power(asize+1);
+ dir = t_buffer_get(asize);
+ }
+ if (asize_r != NULL)
+ *asize_r = asize;
+ *dir_r = dir;
+ return 0;
+}
+
+static int path_normalize(const char *path, bool resolve_links,
+ const char **npath_r, const char **error_r)
+{
+ /* @UNSAFE */
+ unsigned int link_count = 0;
+ char *npath, *npath_pos;
+ const char *p;
+ size_t asize;
+
+ i_assert(path != NULL);
+ i_assert(npath_r != NULL);
+ i_assert(error_r != NULL);
+
+ if (path[0] != '/') {
+ /* relative; initialize npath with current directory */
+ if (t_getcwd_noalloc(&npath, &asize, error_r) < 0)
+ return -1;
+ npath_pos = npath + strlen(npath);
+ i_assert(npath[0] == '/');
+ } else {
+ /* absolute; initialize npath with root */
+ asize = 128;
+ npath = t_buffer_get(asize);
+ npath[0] = '/';
+ npath_pos = npath + 1;
+ }
+
+ p = path;
+ while (*p != '\0') {
+ struct stat st;
+ ptrdiff_t seglen;
+ const char *segend;
+
+ /* skip duplicate slashes */
+ while (*p == '/')
+ p++;
+
+ /* find end of path segment */
+ for (segend = p; *segend != '\0' && *segend != '/'; segend++);
+
+ if (segend == p)
+ break; /* '\0' */
+ seglen = segend - p;
+ if (seglen == 1 && p[0] == '.') {
+ /* a reference to this segment; nothing to do */
+ } else if (seglen == 2 && p[0] == '.' && p[1] == '.') {
+ /* a reference to parent segment; back up to previous
+ * slash */
+ i_assert(npath_pos >= npath);
+ if ((npath_pos - npath) > 1) {
+ if (*(npath_pos-1) == '/')
+ npath_pos--;
+ for (; *(npath_pos-1) != '/'; npath_pos--);
+ }
+ } else {
+ /* allocate space if necessary */
+ i_assert(npath_pos >= npath);
+ if ((size_t)((npath_pos - npath) + seglen + 1) >= asize) {
+ ptrdiff_t npath_offset = npath_pos - npath;
+ asize = nearest_power(npath_offset + seglen + 2);
+ npath = t_buffer_reget(npath, asize);
+ npath_pos = npath + npath_offset;
+ }
+
+ /* make sure npath now ends in slash */
+ i_assert(npath_pos > npath);
+ if (*(npath_pos-1) != '/') {
+ i_assert((size_t)((npath_pos - npath) + 1) < asize);
+ *(npath_pos++) = '/';
+ }
+
+ /* copy segment to normalized path */
+ i_assert(npath_pos >= npath);
+ i_assert((size_t)((npath_pos - npath) + seglen) < asize);
+ memmove(npath_pos, p, seglen);
+ npath_pos += seglen;
+ }
+
+ if (resolve_links) {
+ /* stat path up to here (segend points to tail) */
+ *npath_pos = '\0';
+ if (lstat(npath, &st) < 0) {
+ *error_r = t_strdup_printf("lstat() failed: %m");
+ return -1;
+ }
+
+ if (S_ISLNK (st.st_mode)) {
+ /* symlink */
+ char *npath_link;
+ size_t lsize = 128, tlen = strlen(segend), espace;
+ size_t ltlen = (link_count == 0 ? 0 : tlen);
+ ssize_t ret;
+
+ /* limit link dereferences */
+ if (++link_count > PATH_UTIL_MAX_SYMLINKS) {
+ errno = ELOOP;
+ *error_r = "Too many symlink dereferences";
+ return -1;
+ }
+
+ /* allocate space for preserving tail of previous symlink and
+ first attempt at reading symlink with room for the tail
+
+ buffer will look like this:
+ [npath][0][preserved tail][link buffer][room for tail][0]
+ */
+ espace = ltlen + tlen + 2;
+ i_assert(npath_pos >= npath);
+ if ((size_t)((npath_pos - npath) + espace + lsize) >= asize) {
+ ptrdiff_t npath_offset = npath_pos - npath;
+ asize = nearest_power((npath_offset + espace + lsize) + 1);
+ lsize = asize - (npath_offset + espace);
+ npath = t_buffer_reget(npath, asize);
+ npath_pos = npath + npath_offset;
+ }
+
+ if (ltlen > 0) {
+ /* preserve tail just after end of npath */
+ i_assert(npath_pos >= npath);
+ i_assert((size_t)((npath_pos + 1 - npath) + ltlen) < asize);
+ memmove(npath_pos + 1, segend, ltlen);
+ }
+
+ /* read the symlink after the preserved tail */
+ for (;;) {
+ npath_link = (npath_pos + 1) + ltlen;
+
+ i_assert(npath_link >= npath_pos);
+ i_assert((size_t)((npath_link - npath) + lsize) < asize);
+
+ /* attempt to read the link */
+ if ((ret=readlink(npath, npath_link, lsize)) < 0) {
+ *error_r = t_strdup_printf("readlink() failed: %m");
+ return -1;
+ }
+ if ((size_t)ret < lsize) {
+ /* POSIX doesn't guarantee the presence of a NIL */
+ npath_link[ret] = '\0';
+ break;
+ }
+
+ /* sum of new symlink content length
+ * and path tail length may not
+ exceed maximum */
+ if ((size_t)(ret + tlen) >= PATH_UTIL_MAX_PATH) {
+ errno = ENAMETOOLONG;
+ *error_r = "Resulting path is too long";
+ return -1;
+ }
+
+ /* try again with bigger buffer,
+ we need to allocate more space as well if lsize == ret,
+ because the returned link may have gotten truncated */
+ espace = ltlen + tlen + 2;
+ i_assert(npath_pos >= npath);
+ if ((size_t)((npath_pos - npath) + espace + lsize) >= asize ||
+ lsize == (size_t)ret) {
+ ptrdiff_t npath_offset = npath_pos - npath;
+ asize = nearest_power((npath_offset + espace + lsize) + 1);
+ lsize = asize - (npath_offset + espace);
+ npath = t_buffer_reget(npath, asize);
+ npath_pos = npath + npath_offset;
+ }
+ }
+
+ /* add tail of previous path at end of symlink */
+ i_assert(npath_link >= npath);
+ if (ltlen > 0) {
+ i_assert(npath_pos >= npath);
+ i_assert((size_t)((npath_pos - npath) + 1 + tlen) < asize);
+ i_assert((size_t)((npath_link - npath) + ret + tlen) < asize);
+ memcpy(npath_link + ret, npath_pos + 1, tlen);
+ } else {
+ i_assert((size_t)((npath_link - npath) + ret + tlen) < asize);
+ memcpy(npath_link + ret, segend, tlen);
+ }
+ *(npath_link+ret+tlen) = '\0';
+
+ /* use as new source path */
+ path = segend = npath_link;
+
+ if (path[0] == '/') {
+ /* absolute symlink; start over at root */
+ npath_pos = npath + 1;
+ } else {
+ /* relative symlink; back up to previous segment */
+ i_assert(npath_pos >= npath);
+ if ((npath_pos - npath) > 1) {
+ if (*(npath_pos-1) == '/')
+ npath_pos--;
+ for (; *(npath_pos-1) != '/'; npath_pos--);
+ }
+ }
+
+ } else if (*segend != '\0' && !S_ISDIR (st.st_mode)) {
+ /* not last segment, but not a directory either */
+ errno = ENOTDIR;
+ *error_r = t_strdup_printf("Not a directory: %s", npath);
+ return -1;
+ }
+ }
+
+ p = segend;
+ }
+
+ i_assert(npath_pos >= npath);
+ i_assert((size_t)(npath_pos - npath) < asize);
+
+ /* remove any trailing slash */
+ if ((npath_pos - npath) > 1 && *(npath_pos-1) == '/')
+ npath_pos--;
+ *npath_pos = '\0';
+
+ t_buffer_alloc(npath_pos - npath + 1);
+ *npath_r = npath;
+ return 0;
+}
+
+int t_normpath(const char *path, const char **npath_r, const char **error_r)
+{
+ return path_normalize(path, FALSE, npath_r, error_r);
+}
+
+int t_normpath_to(const char *path, const char *root, const char **npath_r,
+ const char **error_r)
+{
+ i_assert(path != NULL);
+ i_assert(root != NULL);
+ i_assert(npath_r != NULL);
+
+ if (*path == '/')
+ return t_normpath(path, npath_r, error_r);
+
+ return t_normpath(t_strconcat(root, "/", path, NULL), npath_r, error_r);
+}
+
+int t_realpath(const char *path, const char **npath_r, const char **error_r)
+{
+ return path_normalize(path, TRUE, npath_r, error_r);
+}
+
+int t_realpath_to(const char *path, const char *root, const char **npath_r,
+ const char **error_r)
+{
+ i_assert(path != NULL);
+ i_assert(root != NULL);
+ i_assert(npath_r != NULL);
+
+ if (*path == '/')
+ return t_realpath(path, npath_r, error_r);
+
+ return t_realpath(t_strconcat(root, "/", path, NULL), npath_r, error_r);
+}
+
+int t_abspath(const char *path, const char **abspath_r, const char **error_r)
+{
+ i_assert(path != NULL);
+ i_assert(abspath_r != NULL);
+ i_assert(error_r != NULL);
+
+ if (*path == '/') {
+ *abspath_r = path;
+ return 0;
+ }
+
+ const char *dir, *error;
+ if (t_get_working_dir(&dir, &error) < 0) {
+ *error_r = t_strconcat("Failed to get working directory: ",
+ error, NULL);
+ return -1;
+ }
+ *abspath_r = t_strconcat(dir, "/", path, NULL);
+ return 0;
+}
+
+const char *t_abspath_to(const char *path, const char *root)
+{
+ i_assert(path != NULL);
+ i_assert(root != NULL);
+
+ if (*path == '/')
+ return path;
+
+ return t_strconcat(root, "/", path, NULL);
+}
+
+int t_get_working_dir(const char **dir_r, const char **error_r)
+{
+ char *dir;
+
+ i_assert(dir_r != NULL);
+ i_assert(error_r != NULL);
+ if (t_getcwd_noalloc(&dir, NULL, error_r) < 0)
+ return -1;
+
+ t_buffer_alloc(strlen(dir) + 1);
+ *dir_r = dir;
+ return 0;
+}
+
+int t_readlink(const char *path, const char **dest_r, const char **error_r)
+{
+ i_assert(error_r != NULL);
+
+ /* @UNSAFE */
+ ssize_t ret;
+ char *dest;
+ size_t size = 128;
+
+ dest = t_buffer_get(size);
+ while ((ret = readlink(path, dest, size)) >= (ssize_t)size) {
+ size = nearest_power(size+1);
+ dest = t_buffer_get(size);
+ }
+ if (ret < 0) {
+ *error_r = t_strdup_printf("readlink() failed: %m");
+ return -1;
+ }
+
+ dest[ret] = '\0';
+ t_buffer_alloc(ret + 1);
+ *dest_r = dest;
+ return 0;
+}
+
+bool t_binary_abspath(const char **binpath, const char **error_r)
+{
+ const char *path_env, *const *paths;
+ string_t *path;
+
+ if (**binpath == '/') {
+ /* already have absolute path */
+ return TRUE;
+ } else if (strchr(*binpath, '/') != NULL) {
+ /* relative to current directory */
+ const char *error;
+ if (t_abspath(*binpath, binpath, &error) < 0) {
+ *error_r = t_strdup_printf("t_abspath(%s) failed: %s",
+ *binpath, error);
+ return FALSE;
+ }
+ return TRUE;
+ } else if ((path_env = getenv("PATH")) != NULL) {
+ /* we have to find our executable from path */
+ path = t_str_new(256);
+ paths = t_strsplit(path_env, ":");
+ for (; *paths != NULL; paths++) {
+ str_append(path, *paths);
+ str_append_c(path, '/');
+ str_append(path, *binpath);
+ if (access(str_c(path), X_OK) == 0) {
+ *binpath = str_c(path);
+ return TRUE;
+ }
+ str_truncate(path, 0);
+ }
+ *error_r = "Could not find the wanted executable from PATH";
+ return FALSE;
+ } else {
+ *error_r = "PATH environment variable undefined";
+ return FALSE;
+ }
+}
diff --git a/src/lib/path-util.h b/src/lib/path-util.h
new file mode 100644
index 0000000..8492cf3
--- /dev/null
+++ b/src/lib/path-util.h
@@ -0,0 +1,69 @@
+#ifndef PATH_UTIL_H
+#define PATH_UTIL_H
+
+/* Returns path as the normalized absolute path, which means that './'
+ * and '../' components are resolved, and that duplicate and trailing
+ * slashes are removed. If it's not already the absolute path, it's
+ * assumed to be relative to the current working directory.
+ *
+ * NOTE: Be careful with this function. The resolution of '../' components
+ * with the parent component as if it were a normal directory is not valid
+ * if the path contains symbolic links.
+ *
+ * Returns 0 on success, and -1 on failure. errno and error_r are set on
+ * failure, and error_r cannot be NULL.
+ */
+int t_normpath(const char *path, const char **npath_r, const char **error_r);
+/* Like t_normpath(), but path is relative to given root. */
+int t_normpath_to(const char *path, const char *root, const char **npath_r,
+ const char **error_r);
+
+/* Returns path as the real normalized absolute path, which means that all
+ * symbolic links in the path are resolved, that './' and '../' components
+ * are resolved, and that duplicate and trailing slashes are removed. If it's
+ * not already the absolute path, it's assumed to be relative to the current
+ * working directory.
+ *
+ * NOTE: This function calls stat() for each path component and more when
+ * there are symbolic links (just like POSIX realpath()).
+ *
+ * Returns 0 on success, and -1 on failure. errno and error_r are set on
+ * failure, and error_r cannot be NULL.
+ */
+int t_realpath(const char *path, const char **npath_r, const char **error_r);
+/* Like t_realpath(), but path is relative to given root. */
+int t_realpath_to(const char *path, const char *root, const char **npath_r,
+ const char **error_r);
+
+/* Returns path as absolute path. If it's not already absolute path,
+ * it's assumed to be relative to current working directory.
+ *
+ * In the t_abspath functions, the returned paths are not normalized. This
+ * means that './' and '../' are not resolved, but they left in the returned
+ * path as given in the parameters. Symbolic links are not resolved either.
+ *
+ * Returns 0 on success, and -1 on failure. error_r is set on failure, and
+ * cannot be NULL.
+ */
+int t_abspath(const char *path, const char **abspath_r, const char **error_r);
+/* Like t_abspath(), but path is relative to given root. */
+const char *t_abspath_to(const char *path, const char *root);
+
+/* Get current working directory allocated from data stack. Returns 0 on
+ * success and 1 on failure. error_r is set on failure and cannot be NULL. */
+int t_get_working_dir(const char **dir_r, const char **error_r);
+
+/* Get symlink destination allocated from data stack. Returns 0 on success and
+ * -1 on failure. error_r is set on failure and cannot be NULL. */
+int t_readlink(const char *path, const char **dest_r, const char **error_r);
+
+/* Update binpath to be absolute:
+ * a) begins with '/' -> no change
+ * b) contains '/' -> assume relative to working directory
+ * c) set to first executable that's found from $PATH
+ *
+ * error_r is set on failure, and cannot be NULL.
+ */
+bool t_binary_abspath(const char **binpath, const char **error_r);
+
+#endif
diff --git a/src/lib/pkcs5.c b/src/lib/pkcs5.c
new file mode 100644
index 0000000..e035d47
--- /dev/null
+++ b/src/lib/pkcs5.c
@@ -0,0 +1,97 @@
+/* Copyright (c) 2016-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "buffer.h"
+#include "hash-method.h"
+#include "hmac.h"
+#include "pkcs5.h"
+
+#include <stdint.h>
+#include <arpa/inet.h>
+
+static
+int pkcs5_pbkdf1(const struct hash_method *hash,
+ const unsigned char *password, size_t password_len,
+ const unsigned char *salt, size_t salt_len,
+ unsigned int iter, uint32_t length,
+ buffer_t *result)
+{
+ if (length < 1 ||
+ length > hash->digest_size) return -1;
+ if (iter < 1) return -1;
+
+ unsigned char dk[hash->digest_size];
+ unsigned char ctx[hash->context_size];
+
+ hash->init(ctx);
+ hash->loop(ctx, password, password_len);
+ hash->loop(ctx, salt, salt_len);
+ hash->result(ctx, dk);
+ length--;
+
+ for(;length>0;length--) {
+ hash->init(ctx);
+ hash->loop(ctx, dk, hash->digest_size);
+ hash->result(ctx, dk);
+ }
+
+ buffer_append(result, dk, hash->digest_size);
+
+ return 0;
+}
+
+static
+int pkcs5_pbkdf2(const struct hash_method *hash,
+ const unsigned char *password, size_t password_len,
+ const unsigned char *salt, size_t salt_len,
+ unsigned int iter, uint32_t length,
+ buffer_t *result)
+{
+ if (length < 1 || iter < 1) return -1;
+
+ size_t l = (length + hash->digest_size - 1)/hash->digest_size; /* same as ceil(length/hash->digest_size) */
+ unsigned char dk[l * hash->digest_size];
+ unsigned char *block;
+ struct hmac_context hctx;
+ unsigned int c,i,t;
+ unsigned char U_c[hash->digest_size];
+
+ for(t = 0; t < l; t++) {
+ block = &(dk[t*hash->digest_size]);
+ /* U_1 = PRF(Password, Salt|| INT_BE32(Block_Number)) */
+ c = htonl(t+1);
+ hmac_init(&hctx, password, password_len, hash);
+ hmac_update(&hctx, salt, salt_len);
+ hmac_update(&hctx, &c, sizeof(c));
+ hmac_final(&hctx, U_c);
+ /* block = U_1 ^ .. ^ U_iter */
+ memcpy(block, U_c, hash->digest_size);
+ /* U_c = PRF(Password, U_c-1) */
+ for(c = 1; c < iter; c++) {
+ hmac_init(&hctx, password, password_len, hash);
+ hmac_update(&hctx, U_c, hash->digest_size);
+ hmac_final(&hctx, U_c);
+ for(i = 0; i < hash->digest_size; i++)
+ block[i] ^= U_c[i];
+ }
+ }
+
+ buffer_append(result, dk, length);
+
+ return 0;
+}
+
+int pkcs5_pbkdf(enum pkcs5_pbkdf_mode mode, const struct hash_method *hash,
+ const unsigned char *password, size_t password_len,
+ const unsigned char *salt, size_t salt_len,
+ unsigned int iterations, uint32_t dk_len,
+ buffer_t *result)
+{
+ if (mode == PKCS5_PBKDF1)
+ return pkcs5_pbkdf1(hash,password,password_len,
+ salt,salt_len,iterations,dk_len,result);
+ else if (mode == PKCS5_PBKDF2)
+ return pkcs5_pbkdf2(hash,password,password_len,
+ salt,salt_len,iterations,dk_len,result);
+ i_unreached();
+}
diff --git a/src/lib/pkcs5.h b/src/lib/pkcs5.h
new file mode 100644
index 0000000..5cc0650
--- /dev/null
+++ b/src/lib/pkcs5.h
@@ -0,0 +1,35 @@
+#ifndef PKCS5_H
+#define PKCS5_H 1
+
+enum pkcs5_pbkdf_mode {
+ PKCS5_PBKDF1,
+ PKCS5_PBKDF2
+};
+
+/*
+
+ mode - v1.0 or v2.0
+ hash - hash_method_lookup return value
+ password - private password for generation
+ password_len - length of password in octets
+ salt - salt for generation
+ salt_len - length of salt in octets
+ iterations - number of iterations to hash (use at least 1000, a very large number => very very slow)
+ dk_len - number of bytes to return from derived key
+ result - buffer_t to hold the result, either use dynamic or make sure it fits dk_len
+
+ non-zero return value indicates that either iterations was less than 1 or dk_len was too large
+
+ Sample code:
+
+ buffer_t *result = t_buffer_create(256);
+ if (pkcs5_pbkdf(PKCS5_PBKDF2, hash_method_lookup("sha256"), "password", 8, "salt", 4, 4096, 256, result) != 0) { // error }
+
+*/
+
+int pkcs5_pbkdf(enum pkcs5_pbkdf_mode mode, const struct hash_method *hash,
+ const unsigned char *password, size_t password_len,
+ const unsigned char *salt, size_t salt_len,
+ unsigned int iterations, uint32_t dk_len,
+ buffer_t *result);
+#endif
diff --git a/src/lib/primes.c b/src/lib/primes.c
new file mode 100644
index 0000000..cb4a5e2
--- /dev/null
+++ b/src/lib/primes.c
@@ -0,0 +1,48 @@
+/* Copyright (c) 2013-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "primes.h"
+
+static const unsigned int primes[] = {
+#define PRIME_SKIP_COUNT 3
+ 17,
+ 37,
+ 67,
+ 131,
+ 257, /* next from 2^8 */
+ 521,
+ 1031,
+ 2053,
+ 4099,
+ 8209,
+ 16411,
+ 32771,
+ 65537, /* next from 2^16 */
+ 131101,
+ 262147,
+ 524309,
+ 1048583,
+ 2097169,
+ 4194319,
+ 8388617,
+ 16777259, /* next from 2^24 */
+ 33554467,
+ 67108879,
+ 134217757,
+ 268435459,
+ 536870923,
+ 1073741827,
+ 2147483659U,
+ 4294967291U /* previous from 2^32 */
+};
+
+unsigned int primes_closest(unsigned int num)
+{
+ unsigned int i;
+
+ for (i = 31; i > PRIME_SKIP_COUNT; i--) {
+ if ((num & (1U << i)) != 0)
+ return primes[i - PRIME_SKIP_COUNT];
+ }
+ return primes[0];
+}
diff --git a/src/lib/primes.h b/src/lib/primes.h
new file mode 100644
index 0000000..8153d6d
--- /dev/null
+++ b/src/lib/primes.h
@@ -0,0 +1,8 @@
+#ifndef PRIMES_H
+#define PRIMES_H
+
+/* Returns a prime close to specified number, or the number itself if it's
+ a prime. Note that the returned value may be smaller than requested! */
+unsigned int primes_closest(unsigned int num) ATTR_CONST;
+
+#endif
diff --git a/src/lib/printf-format-fix.c b/src/lib/printf-format-fix.c
new file mode 100644
index 0000000..001e7b0
--- /dev/null
+++ b/src/lib/printf-format-fix.c
@@ -0,0 +1,192 @@
+/* Copyright (c) 2002-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "printf-format-fix.h"
+
+static const char *
+fix_format_real(const char *fmt, const char *p, size_t *len_r)
+{
+ const char *errstr;
+ char *buf;
+ size_t len1, len2, len3;
+
+ i_assert((size_t)(p - fmt) < INT_MAX);
+ i_assert(p[0] == '%' && p[1] == 'm');
+
+ errstr = strerror(errno);
+
+ /* we'll assume that there's only one %m in the format string.
+ this simplifies the code and there's really no good reason to have
+ it multiple times. Callers can trap this case themselves. */
+ len1 = p - fmt;
+ len2 = strlen(errstr);
+ len3 = strlen(p + 2);
+
+ /* @UNSAFE */
+ buf = t_buffer_get(len1 + len2 + len3 + 1);
+ memcpy(buf, fmt, len1);
+ memcpy(buf + len1, errstr, len2);
+ memcpy(buf + len1 + len2, p + 2, len3 + 1);
+
+ *len_r = len1 + len2 + len3;
+ return buf;
+}
+
+static bool verify_length(const char **p)
+{
+ if (**p == '*') {
+ /* We don't bother supporting "*m$" - it's not used
+ anywhere and seems a bit dangerous. */
+ *p += 1;
+ } else if (**p >= '0' && **p <= '9') {
+ /* Limit to 4 digits - we'll never want more than that.
+ Some implementations might not handle long digits
+ correctly, or maybe even could be used for DoS due
+ to using too much CPU. If you want to express '99'
+ as '00099', then you lose in this function. */
+ unsigned int i = 0;
+ do {
+ *p += 1;
+ if (++i > 4)
+ return FALSE;
+ } while (**p >= '0' && **p <= '9');
+ }
+ return TRUE;
+}
+
+static const char *
+printf_format_fix_noalloc(const char *format, size_t *len_r)
+{
+ /* NOTE: This function is overly strict in what it accepts. Some
+ format strings that are valid (and safe) in C99 will cause a panic
+ here. This is because we don't really need to support the weirdest
+ special cases, and we're also being extra careful not to pass
+ anything to the underlying libc printf, which might treat the string
+ differently than us and unexpectedly handling it as %n. For example
+ "%**%n" with glibc. */
+
+ /* Allow only the standard C99 flags. There are also <'> and <I> flags,
+ but we don't really need them. And at worst if they're not supported
+ by the underlying printf, they could potentially be used to work
+ around our restrictions. */
+ const char printf_flags[] = "#0- +";
+ /* As a tiny optimization keep the most commonly used conversion
+ specifiers first, so strchr() stops early. */
+ static const char *printf_specifiers = "sudcixXpoeEfFgGaA";
+ const char *ret, *p, *p2;
+ char *flag;
+
+ p = ret = format;
+ while ((p2 = strchr(p, '%')) != NULL) {
+ const unsigned int start_pos = p2 - format;
+
+ p = p2+1;
+ if (*p == '%') {
+ /* we'll be strict and allow %% only when there are no
+ optinal flags or modifiers. */
+ p++;
+ continue;
+ }
+ /* 1) zero or more flags. We'll add a further restriction that
+ each flag can be used only once, since there's no need to
+ use them more than once, and some implementations might
+ add their own limits. */
+ bool printf_flags_seen[N_ELEMENTS(printf_flags)] = { FALSE, };
+ while (*p != '\0' &&
+ (flag = strchr(printf_flags, *p)) != NULL) {
+ unsigned int flag_idx = flag - printf_flags;
+
+ if (printf_flags_seen[flag_idx]) {
+ i_panic("Duplicate %% flag '%c' starting at #%u in '%s'",
+ *p, start_pos, format);
+ }
+ printf_flags_seen[flag_idx] = TRUE;
+ p++;
+ }
+
+ /* 2) Optional minimum field width */
+ if (!verify_length(&p)) {
+ i_panic("Too large minimum field width starting at #%u in '%s'",
+ start_pos, format);
+ }
+
+ /* 3) Optional precision */
+ if (*p == '.') {
+ p++;
+ if (!verify_length(&p)) {
+ i_panic("Too large precision starting at #%u in '%s'",
+ start_pos, format);
+ }
+ }
+
+ /* 4) Optional length modifier */
+ switch (*p) {
+ case 'h':
+ if (*++p == 'h')
+ p++;
+ break;
+ case 'l':
+ if (*++p == 'l')
+ p++;
+ break;
+ case 'L':
+ case 'j':
+ case 'z':
+ case 't':
+ p++;
+ break;
+ }
+
+ /* 5) conversion specifier */
+ if (*p == '\0' || strchr(printf_specifiers, *p) == NULL) {
+ switch (*p) {
+ case 'n':
+ i_panic("%%n modifier used");
+ case 'm':
+ if (ret != format)
+ i_panic("%%m used twice");
+ ret = fix_format_real(format, p-1, len_r);
+ break;
+ case '\0':
+ i_panic("Missing %% specifier starting at #%u in '%s'",
+ start_pos, format);
+ default:
+ i_panic("Unsupported 0x%02x specifier starting at #%u in '%s'",
+ *p, start_pos, format);
+ }
+ }
+ p++;
+ }
+
+ if (ret == format)
+ *len_r = p - format + strlen(p);
+ return ret;
+}
+
+const char *printf_format_fix_get_len(const char *format, size_t *len_r)
+{
+ const char *ret;
+
+ ret = printf_format_fix_noalloc(format, len_r);
+ if (ret != format)
+ t_buffer_alloc(*len_r + 1);
+ return ret;
+}
+
+const char *printf_format_fix(const char *format)
+{
+ const char *ret;
+ size_t len;
+
+ ret = printf_format_fix_noalloc(format, &len);
+ if (ret != format)
+ t_buffer_alloc(len + 1);
+ return ret;
+}
+
+const char *printf_format_fix_unsafe(const char *format)
+{
+ size_t len;
+
+ return printf_format_fix_noalloc(format, &len);
+}
diff --git a/src/lib/printf-format-fix.h b/src/lib/printf-format-fix.h
new file mode 100644
index 0000000..5691a07
--- /dev/null
+++ b/src/lib/printf-format-fix.h
@@ -0,0 +1,15 @@
+#ifndef PRINTF_FORMAT_FIX_H
+#define PRINTF_FORMAT_FIX_H
+
+/* Replaces %m in format with strerror(errno) and panics if %n modifier is
+ used. If the format string was modified, it's returned from data stack. */
+const char *printf_format_fix(const char *format) ATTR_FORMAT_ARG(1);
+/* Like printf_format_fix(), except return also the format string's length. */
+const char *printf_format_fix_get_len(const char *format, size_t *len_r)
+ ATTR_FORMAT_ARG(1);
+/* Like printf_format_fix(), except the format string is written to data
+ stack without actually allocating it. Data stack must not be used until
+ format string is no longer needed. */
+const char *printf_format_fix_unsafe(const char *format) ATTR_FORMAT_ARG(1);
+
+#endif
diff --git a/src/lib/priorityq.c b/src/lib/priorityq.c
new file mode 100644
index 0000000..e532de8
--- /dev/null
+++ b/src/lib/priorityq.c
@@ -0,0 +1,171 @@
+/* Copyright (c) 2008-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "priorityq.h"
+
+/* Macros for moving inside an array implementation of binary tree where
+ [0] is the root. */
+#define PARENT_IDX(idx) \
+ (((idx) - 1) / 2)
+#define LEFT_CHILD_IDX(idx) \
+ ((idx) * 2 + 1)
+#define RIGHT_CHILD_IDX(idx) \
+ ((idx) * 2 + 2)
+
+struct priorityq {
+ priorityq_cmp_callback_t *cmp_callback;
+
+ ARRAY(struct priorityq_item *) items;
+};
+
+struct priorityq *
+priorityq_init(priorityq_cmp_callback_t *cmp_callback, unsigned int init_size)
+{
+ struct priorityq *pq;
+
+ pq = i_new(struct priorityq, 1);
+ pq->cmp_callback = cmp_callback;
+ i_array_init(&pq->items, init_size);
+ return pq;
+}
+
+void priorityq_deinit(struct priorityq **_pq)
+{
+ struct priorityq *pq = *_pq;
+
+ *_pq = NULL;
+ array_free(&pq->items);
+ i_free(pq);
+}
+
+unsigned int priorityq_count(const struct priorityq *pq)
+{
+ return array_count(&pq->items);
+}
+
+static void heap_items_swap(struct priorityq_item **items,
+ unsigned int idx1, unsigned int idx2)
+{
+ struct priorityq_item *tmp;
+
+ /* swap the item indexes */
+ i_assert(items[idx1]->idx == idx1);
+ i_assert(items[idx2]->idx == idx2);
+
+ items[idx1]->idx = idx2;
+ items[idx2]->idx = idx1;
+
+ /* swap the item pointers */
+ tmp = items[idx1];
+ items[idx1] = items[idx2];
+ items[idx2] = tmp;
+}
+
+static unsigned int
+heap_item_bubble_up(struct priorityq *pq, unsigned int idx)
+{
+ struct priorityq_item **items;
+ unsigned int parent_idx, count;
+
+ items = array_get_modifiable(&pq->items, &count);
+ while (idx > 0) {
+ parent_idx = PARENT_IDX(idx);
+
+ i_assert(idx < count);
+ if (pq->cmp_callback(items[idx], items[parent_idx]) >= 0)
+ break;
+
+ /* wrong order - swap */
+ heap_items_swap(items, idx, parent_idx);
+ idx = parent_idx;
+ }
+ return idx;
+}
+
+static void heap_item_bubble_down(struct priorityq *pq, unsigned int idx)
+{
+ struct priorityq_item **items;
+ unsigned int left_idx, right_idx, min_child_idx, count;
+
+ items = array_get_modifiable(&pq->items, &count);
+ while ((left_idx = LEFT_CHILD_IDX(idx)) < count) {
+ right_idx = RIGHT_CHILD_IDX(idx);
+ if (right_idx >= count ||
+ pq->cmp_callback(items[left_idx], items[right_idx]) < 0)
+ min_child_idx = left_idx;
+ else
+ min_child_idx = right_idx;
+
+ if (pq->cmp_callback(items[min_child_idx], items[idx]) >= 0)
+ break;
+
+ /* wrong order - swap */
+ heap_items_swap(items, idx, min_child_idx);
+ idx = min_child_idx;
+ }
+}
+
+void priorityq_add(struct priorityq *pq, struct priorityq_item *item)
+{
+ item->idx = array_count(&pq->items);
+ array_push_back(&pq->items, &item);
+ (void)heap_item_bubble_up(pq, item->idx);
+}
+
+static void priorityq_remove_idx(struct priorityq *pq, unsigned int idx)
+{
+ struct priorityq_item **items;
+ unsigned int count;
+
+ items = array_get_modifiable(&pq->items, &count);
+ i_assert(idx < count);
+
+ /* move last item over the removed one and fix the heap */
+ count--;
+ heap_items_swap(items, idx, count);
+ array_delete(&pq->items, count, 1);
+
+ if (count > 0 && idx != count) {
+ if (idx > 0)
+ idx = heap_item_bubble_up(pq, idx);
+ heap_item_bubble_down(pq, idx);
+ }
+}
+
+void priorityq_remove(struct priorityq *pq, struct priorityq_item *item)
+{
+ priorityq_remove_idx(pq, item->idx);
+ item->idx = UINT_MAX;
+}
+
+struct priorityq_item *priorityq_peek(struct priorityq *pq)
+{
+ struct priorityq_item *const *items;
+
+ if (array_count(&pq->items) == 0)
+ return NULL;
+
+ items = array_front(&pq->items);
+ return items[0];
+}
+
+struct priorityq_item *priorityq_pop(struct priorityq *pq)
+{
+ struct priorityq_item *item;
+
+ item = priorityq_peek(pq);
+ if (item != NULL) {
+ priorityq_remove_idx(pq, 0);
+ item->idx = UINT_MAX;
+ }
+ return item;
+}
+
+struct priorityq_item *const *priorityq_items(struct priorityq *pq)
+{
+ if (array_count(&pq->items) == 0)
+ return NULL;
+
+ return array_front(&pq->items);
+}
diff --git a/src/lib/priorityq.h b/src/lib/priorityq.h
new file mode 100644
index 0000000..aa38914
--- /dev/null
+++ b/src/lib/priorityq.h
@@ -0,0 +1,39 @@
+#ifndef PRIORITYQ_H
+#define PRIORITYQ_H
+
+/* Priority queue implementation using heap. The items you add to the queue
+ must begin with a struct priorityq_item. This is necessary for
+ priorityq_remove() to work fast. */
+
+struct priorityq_item {
+ /* Current index in the queue array, updated automatically. */
+ unsigned int idx;
+ /* [your own data] */
+};
+
+/* Returns <0, 0 or >0 */
+typedef int priorityq_cmp_callback_t(const void *p1, const void *p2);
+
+/* Create a new priority queue. Callback is used to compare added items. */
+struct priorityq *
+priorityq_init(priorityq_cmp_callback_t *cmp_callback, unsigned int init_size);
+void priorityq_deinit(struct priorityq **pq);
+
+/* Return number of items in the queue. */
+unsigned int priorityq_count(const struct priorityq *pq) ATTR_PURE;
+
+/* Add a new item to the queue. */
+void priorityq_add(struct priorityq *pq, struct priorityq_item *item);
+/* Remove the specified item from the queue. */
+void priorityq_remove(struct priorityq *pq, struct priorityq_item *item);
+
+/* Return the item with the highest priority. Returns NULL if queue is empty. */
+struct priorityq_item *priorityq_peek(struct priorityq *pq);
+/* Like priorityq_peek(), but also remove the returned item from the queue. */
+struct priorityq_item *priorityq_pop(struct priorityq *pq);
+/* Returns array containing all the priorityq_items. Only the first item is
+ guaranteed to be the highest priority item, the rest can't be assumed to
+ be in any order. */
+struct priorityq_item *const *priorityq_items(struct priorityq *pq);
+
+#endif
diff --git a/src/lib/process-stat.c b/src/lib/process-stat.c
new file mode 100644
index 0000000..fae57c7
--- /dev/null
+++ b/src/lib/process-stat.c
@@ -0,0 +1,267 @@
+/* Copyright (c) 2008-2021 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "process-stat.h"
+#include "time-util.h"
+#include <limits.h>
+#include <fcntl.h>
+#include <unistd.h>
+#include <sys/time.h>
+#include <time.h>
+#include <sys/resource.h>
+#include <stdio.h>
+
+#define PROC_STAT_PATH "/proc/self/stat"
+#define PROC_STATUS_PATH "/proc/self/status"
+#define PROC_IO_PATH "/proc/self/io"
+
+static const uint64_t stat_undefined = 0xFFFFFFFFFFFFFFFF;
+
+struct key_val {
+ const char *key;
+ uint64_t *value;
+ unsigned int idx;
+};
+
+static int parse_field(const char *line, struct key_val *field)
+{
+ if (str_begins(line, field->key))
+ return str_to_uint64(line + strlen(field->key), field->value);
+ return -1;
+}
+
+static void buffer_parse(const char *buf, struct key_val *fields)
+{
+ const char *const *tmp;
+ tmp = t_strsplit(buf, "\n");
+ unsigned int tmp_count = str_array_length(tmp);
+ for (; fields->key != NULL; fields++) {
+ if (fields->idx >= tmp_count ||
+ parse_field(tmp[fields->idx], fields) < 0)
+ *fields->value = stat_undefined;
+ }
+}
+
+static int open_fd(const char *path, struct event *event)
+{
+ int fd;
+ uid_t uid;
+
+ fd = open(path, O_RDONLY);
+
+ if (fd == -1 && errno == EACCES) {
+ uid = geteuid();
+ /* kludge: if we're running with permissions temporarily
+ dropped, get them temporarily back so we can open
+ /proc/self/io. */
+ if (seteuid(0) == 0) {
+ fd = open(path, O_RDONLY);
+ if (seteuid(uid) < 0)
+ i_fatal("seteuid(%s) failed", dec2str(uid));
+ }
+ errno = EACCES;
+ }
+ if (fd == -1) {
+ if (errno == ENOENT || errno == EACCES)
+ e_debug(event, "open(%s) failed: %m", path);
+ else
+ e_error(event, "open(%s) failed: %m", path);
+ }
+ return fd;
+}
+
+static int
+read_file(int fd, const char *path, char *buf_r, size_t buf_size, struct event *event)
+{
+ ssize_t ret;
+ ret = read(fd, buf_r, buf_size);
+ if (ret <= 0) {
+ if (ret == -1)
+ e_error(event, "read(%s) failed: %m", path);
+ else
+ e_error(event, "read(%s) returned EOF", path);
+ } else if (ret == (ssize_t)buf_size) {
+ e_error(event, "%s is larger than expected", path);
+ buf_r[buf_size - 1] = '\0';
+ } else {
+ buf_r[ret] = '\0';
+ }
+ i_close_fd(&fd);
+ return ret <= 0 ? -1 : 0;
+}
+
+static int parse_key_val_file(const char *path,
+ struct key_val *fields,
+ struct event *event)
+{
+ char buf[2048];
+ int fd;
+
+ fd = open_fd(path, event);
+ if (fd == -1 || read_file(fd, path, buf, sizeof(buf), event) < 0) {
+ for (; fields->key != NULL; fields++)
+ *fields->value = stat_undefined;
+ return -1;
+ }
+ buffer_parse(buf, fields);
+ return 0;
+}
+
+static int parse_proc_io(struct process_stat *stat_r, struct event *event)
+{
+ struct key_val fields[] = {
+ { "rchar: ", &stat_r->rchar, 0 },
+ { "wchar: ", &stat_r->wchar, 1 },
+ { "syscr: ", &stat_r->syscr, 2 },
+ { "syscw: ", &stat_r->syscw, 3 },
+ { NULL, NULL, 0 },
+ };
+ if (stat_r->proc_io_failed ||
+ parse_key_val_file(PROC_IO_PATH, fields, event) < 0) {
+ stat_r->proc_io_failed = TRUE;
+ return -1;
+ }
+ return 0;
+}
+
+static int parse_proc_status(struct process_stat *stat_r, struct event *event)
+{
+ struct key_val fields [] = {
+ { "voluntary_ctxt_switches:\t", &stat_r->vol_cs, 53 },
+ { "nonvoluntary_ctxt_switches:\t", &stat_r->invol_cs, 54 },
+ { NULL, NULL, 0 },
+ };
+ if (stat_r->proc_status_failed ||
+ parse_key_val_file(PROC_STATUS_PATH, fields, event) < 0) {
+ stat_r->proc_status_failed = TRUE;
+ return -1;
+ }
+ return 0;
+}
+
+static int stat_get_rusage(struct process_stat *stat_r)
+{
+ struct rusage usage;
+
+ if (getrusage(RUSAGE_SELF, &usage) < 0)
+ return -1;
+ stat_r->utime = timeval_to_usecs(&usage.ru_utime);
+ stat_r->stime = timeval_to_usecs(&usage.ru_stime);
+ stat_r->minor_faults = usage.ru_minflt;
+ stat_r->major_faults = usage.ru_majflt;
+ stat_r->vol_cs = usage.ru_nvcsw;
+ stat_r->invol_cs = usage.ru_nivcsw;
+ return 0;
+}
+
+static int parse_stat_file(struct process_stat *stat_r, struct event *event)
+{
+ int fd = -1;
+ char buf[1024];
+ unsigned int i;
+ const char *const *tmp;
+ struct {
+ uint64_t *value;
+ unsigned int idx;
+ } fields[] = {
+ { &stat_r->minor_faults, 9 },
+ { &stat_r->major_faults, 11 },
+ { &stat_r->utime, 13 },
+ { &stat_r->stime, 14 },
+ { &stat_r->vsz, 22 },
+ { &stat_r->rss, 23 },
+ };
+ if (!stat_r->proc_stat_failed)
+ fd = open_fd(PROC_STAT_PATH, event);
+ if (fd == -1) {
+ stat_r->proc_stat_failed = TRUE;
+ /* vsz and rss are not provided by getrusage(), setting to undefined */
+ stat_r->vsz = stat_undefined;
+ stat_r->rss = stat_undefined;
+ if (stat_r->rusage_failed)
+ return -1;
+ if (stat_get_rusage(stat_r) < 0) {
+ e_error(event, "getrusage() failed: %m");
+ stat_r->rusage_failed = TRUE;
+ return -1;
+ }
+ return 0;
+ }
+ if (read_file(fd, PROC_STAT_PATH, buf, sizeof(buf), event) < 0) {
+ stat_r->proc_stat_failed = TRUE;
+ return -1;
+ }
+ tmp = t_strsplit(buf, " ");
+ unsigned int tmp_count = str_array_length(tmp);
+
+ for (i = 0; i < N_ELEMENTS(fields); i++) {
+ if (fields[i].idx >= tmp_count ||
+ str_to_uint64(tmp[fields[i].idx], fields[i].value) < 0)
+ *fields[i].value = stat_undefined;
+ }
+ /* rss is provided in pages, convert to bytes */
+ stat_r->rss *= sysconf(_SC_PAGESIZE);
+ return 0;
+}
+
+static int parse_all_stats(struct process_stat *stat_r, struct event *event)
+{
+ bool has_fields = FALSE;
+
+ if (parse_stat_file(stat_r, event) == 0)
+ has_fields = TRUE;
+ if (parse_proc_io(stat_r, event) == 0)
+ has_fields = TRUE;
+ if ((!stat_r->proc_stat_failed || stat_r->rusage_failed) &&
+ parse_proc_status(stat_r, event) == 0)
+ has_fields = TRUE;
+
+ if (has_fields)
+ return 0;
+ return -1;
+}
+
+void process_stat_read_start(struct process_stat *stat_r, struct event *event)
+{
+ i_zero(stat_r);
+ (void)parse_all_stats(stat_r, event);
+}
+
+void process_stat_read_finish(struct process_stat *stat, struct event *event)
+{
+ unsigned int i;
+ struct process_stat new_stat;
+ i_zero(&new_stat);
+ new_stat.proc_io_failed = stat->proc_io_failed;
+ new_stat.proc_status_failed = stat->proc_status_failed;
+ new_stat.proc_stat_failed = stat->proc_stat_failed;
+ new_stat.rusage_failed = stat->rusage_failed;
+ if (parse_all_stats(&new_stat, event) < 0) {
+ i_zero(stat);
+ return;
+ }
+ stat->vsz = new_stat.vsz == stat_undefined ? 0 : new_stat.vsz;
+ stat->rss = new_stat.rss == stat_undefined ? 0 : new_stat.rss;
+
+ unsigned int cumulative_field_offsets[] = {
+ offsetof(struct process_stat, utime),
+ offsetof(struct process_stat, stime),
+ offsetof(struct process_stat, minor_faults),
+ offsetof(struct process_stat, major_faults),
+ offsetof(struct process_stat, vol_cs),
+ offsetof(struct process_stat, invol_cs),
+ offsetof(struct process_stat, rchar),
+ offsetof(struct process_stat, wchar),
+ offsetof(struct process_stat, syscr),
+ offsetof(struct process_stat, syscw),
+ };
+ for (i = 0; i < N_ELEMENTS(cumulative_field_offsets); i++) {
+ uint64_t *old_value = PTR_OFFSET(stat, cumulative_field_offsets[i]);
+ uint64_t *new_value = PTR_OFFSET(&new_stat, cumulative_field_offsets[i]);
+ if (*old_value == stat_undefined || *new_value == stat_undefined)
+ *old_value = 0;
+ else
+ *old_value = *new_value > *old_value ?
+ (*new_value - *old_value) : 0;
+ }
+}
diff --git a/src/lib/process-stat.h b/src/lib/process-stat.h
new file mode 100644
index 0000000..66fea7d
--- /dev/null
+++ b/src/lib/process-stat.h
@@ -0,0 +1,26 @@
+#ifndef PROCESS_STAT_H
+#define PROCESS_STAT_H
+
+struct process_stat {
+ uint64_t utime;
+ uint64_t stime;
+ uint64_t minor_faults;
+ uint64_t major_faults;
+ uint64_t vol_cs;
+ uint64_t invol_cs;
+ uint64_t rss;
+ uint64_t vsz;
+ uint64_t rchar;
+ uint64_t wchar;
+ uint64_t syscr;
+ uint64_t syscw;
+ bool proc_io_failed:1;
+ bool rusage_failed:1;
+ bool proc_stat_failed:1;
+ bool proc_status_failed:1;
+};
+
+void process_stat_read_start(struct process_stat *stat_r, struct event *event);
+void process_stat_read_finish(struct process_stat *stat, struct event *event);
+
+#endif
diff --git a/src/lib/process-title.c b/src/lib/process-title.c
new file mode 100644
index 0000000..6c680c3
--- /dev/null
+++ b/src/lib/process-title.c
@@ -0,0 +1,187 @@
+/* Copyright (c) 2002-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "env-util.h"
+#include "process-title.h"
+
+#ifdef HAVE_LIBBSD
+#include <bsd/unistd.h>
+#else
+#include <unistd.h> /* FreeBSD */
+#endif
+
+static char *process_name = NULL;
+static char *current_process_title;
+
+#ifdef HAVE_SETPROCTITLE
+# undef PROCTITLE_HACK
+#endif
+
+#ifdef PROCTITLE_HACK
+
+#ifdef DEBUG
+/* if there are problems with this approach, try to make sure we notice it */
+# define PROCTITLE_CLEAR_CHAR 0xab
+#else
+/* There are always race conditions when updating the process title. ps might
+ read a partially written title. Try to at least minimize this by using NUL
+ as the fill character, so ps won't show a large number of 0xab chars. */
+# define PROCTITLE_CLEAR_CHAR 0
+#endif
+
+static char *process_title;
+static size_t process_title_len, process_title_clean_pos;
+static void *argv_memblock, *environ_memblock;
+
+static void proctitle_hack_init(char *argv[], char *env[])
+{
+ char *last;
+ unsigned int i;
+ bool clear_env;
+
+ i_assert(argv[0] != NULL);
+
+ /* find the last argv or environment string. it should always be the
+ last string in environ, but don't rely on it. this is what openssh
+ does, so hopefully it's safe enough. */
+ last = argv[0] + strlen(argv[0]) + 1;
+ for (i = 1; argv[i] != NULL; i++) {
+ if (argv[i] == last)
+ last = argv[i] + strlen(argv[i]) + 1;
+ }
+ if (env[0] == NULL)
+ clear_env = FALSE;
+ else {
+ clear_env = last == env[0];
+ for (i = 0; env[i] != NULL; i++) {
+ if (env[i] == last)
+ last = env[i] + strlen(env[i]) + 1;
+ }
+ }
+
+ process_title = argv[0];
+ process_title_len = last - argv[0];
+
+ if (clear_env) {
+ memset(env[0], PROCTITLE_CLEAR_CHAR, last - env[0]);
+ process_title_clean_pos = env[0] - process_title;
+ } else {
+ process_title_clean_pos = 0;
+ }
+}
+
+static char **argv_dup(char *old_argv[], void **memblock_r)
+{
+ /* @UNSAFE */
+ void *memblock, *memblock_end;
+ char **new_argv;
+ unsigned int i, count;
+ size_t len, memblock_len = 0;
+
+ for (count = 0; old_argv[count] != NULL; count++)
+ memblock_len += strlen(old_argv[count]) + 1;
+ memblock_len += sizeof(char *) * (count + 1);
+
+ memblock = malloc(memblock_len);
+ if (memblock == NULL)
+ i_fatal_status(FATAL_OUTOFMEM, "malloc() failed: %m");
+ *memblock_r = memblock;
+ memblock_end = PTR_OFFSET(memblock, memblock_len);
+
+ new_argv = memblock;
+ memblock = PTR_OFFSET(memblock, sizeof(char *) * (count + 1));
+
+ for (i = 0; i < count; i++) {
+ new_argv[i] = memblock;
+ len = strlen(old_argv[i]) + 1;
+ memcpy(memblock, old_argv[i], len);
+ memblock = PTR_OFFSET(memblock, len);
+ }
+ i_assert(memblock == memblock_end);
+ new_argv[i] = NULL;
+ return new_argv;
+}
+
+static void proctitle_hack_set(const char *title)
+{
+ size_t len = strlen(title);
+
+ /* OS X wants two NULs */
+ if (len >= process_title_len-1)
+ len = process_title_len - 2;
+
+ memcpy(process_title, title, len);
+ process_title[len++] = '\0';
+ process_title[len++] = '\0';
+
+ if (len < process_title_clean_pos) {
+ memset(process_title + len, PROCTITLE_CLEAR_CHAR,
+ process_title_clean_pos - len);
+ process_title_clean_pos = len;
+ } else if (process_title_clean_pos != 0) {
+ process_title_clean_pos = len;
+ }
+}
+
+#endif
+
+void process_title_init(int argc ATTR_UNUSED, char **argv[])
+{
+#ifdef PROCTITLE_HACK
+ char ***environ_p = env_get_environ_p();
+ char **orig_argv = *argv;
+ char **orig_environ = *environ_p;
+
+ *argv = argv_dup(orig_argv, &argv_memblock);
+ *environ_p = argv_dup(orig_environ, &environ_memblock);
+ proctitle_hack_init(orig_argv, orig_environ);
+#endif
+#ifdef HAVE_LIBBSD
+ setproctitle_init(argc, *argv, *env_get_environ_p());
+#endif
+ process_name = (*argv)[0];
+}
+
+void process_title_set(const char *title)
+{
+ i_assert(process_name != NULL);
+
+ i_free(current_process_title);
+ current_process_title = i_strdup(title);
+#ifdef HAVE_SETPROCTITLE
+ if (title == NULL)
+ setproctitle(NULL);
+ else
+ setproctitle("%s", title);
+#elif defined(PROCTITLE_HACK)
+ T_BEGIN {
+ proctitle_hack_set(t_strconcat(process_name, " ", title, NULL));
+ } T_END;
+#endif
+}
+
+const char *process_title_get(void)
+{
+ return current_process_title;
+}
+
+void process_title_deinit(void)
+{
+#ifdef PROCTITLE_HACK
+ char ***environ_p = env_get_environ_p();
+
+ free(argv_memblock);
+ free(environ_memblock);
+
+ /* Environment is no longer usable. Make sure we won't crash in case
+ some library's deinit function still calls getenv(). This code was
+ mainly added because of GNUTLS where we don't really care about the
+ getenv() call.
+
+ Alternatively we could remove the free() calls above, but that would
+ annoy memory leak checking tools. Also we could attempt to restore
+ the environ_p to its original state, but that's a bit complicated. */
+ *environ_p = NULL;
+#endif
+ i_free(current_process_title);
+}
diff --git a/src/lib/process-title.h b/src/lib/process-title.h
new file mode 100644
index 0000000..1e63b49
--- /dev/null
+++ b/src/lib/process-title.h
@@ -0,0 +1,17 @@
+#ifndef PROCESS_TITLE_H
+#define PROCESS_TITLE_H
+
+/* Initialize title changing. */
+void process_title_init(int argc, char **argv[]);
+
+/* Change the process title if possible. */
+void process_title_set(const char *title);
+/* Return the previously set process title. NULL means that it's either not
+ set, or the title was explicitly set to NULL previously. */
+const char *process_title_get(void);
+
+/* Free all memory used by process title hacks. This should be the last
+ function called by the process, since it frees argv and environment. */
+void process_title_deinit(void);
+
+#endif
diff --git a/src/lib/rand.c b/src/lib/rand.c
new file mode 100644
index 0000000..12c9686
--- /dev/null
+++ b/src/lib/rand.c
@@ -0,0 +1,101 @@
+/* Copyright (c) 2014-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "randgen.h"
+
+#ifdef HAVE_ARC4RANDOM
+#ifdef HAVE_LIBBSD
+#include <bsd/stdlib.h>
+#endif
+
+uint32_t i_rand(void)
+{
+ return arc4random();
+}
+
+uint32_t i_rand_limit(uint32_t upper_bound)
+{
+ i_assert(upper_bound > 0);
+
+ return arc4random_uniform(upper_bound);
+}
+#else
+uint32_t i_rand(void)
+{
+ uint32_t value;
+ random_fill(&value, sizeof(value));
+ return value;
+}
+
+/*
+ * The following generates a random number in the range [0, upper_bound)
+ * with each possible value having equal probability of occurring.
+ *
+ * This algorithm is not original, but it is dense enough that a detailed
+ * explanation is in order.
+ *
+ * The big problem is that we want a uniformly random values. If one were
+ * to do `i_rand() % upper_bound`, the result probability distribution would
+ * depend on the value of the upper bound. When the upper bound is a power
+ * of 2, the distribution is uniform. If it is not a power of 2, the
+ * distribution is skewed.
+ *
+ * The naive modulo approach breaks down because the division effectively
+ * splits the whole range of input values into a number of fixed sized
+ * "buckets", but with non-power-of-2 bound the last bucket is not the full
+ * size.
+ *
+ * To fix this bias, we reduce the input range such that the remaining
+ * values can be split exactly into equal sized buckets.
+ *
+ * For example, let's assume that i_rand() produces a uint8_t to simplify
+ * the math, and that we want a random number [0, 9] - in other words,
+ * upper_bound == 10.
+ *
+ * `i_rand() % 10` makes buckets 10 numbers wide, but the last bucket is only
+ * 6 numbers wide (250..255). Therefore, 0..5 will occur more frequently
+ * than 6..9.
+ *
+ * If we reduce the input range to [0, 250), the result of the mod 10 will
+ * be uniform. Interestingly, the same can be accomplished if we reduce the
+ * input range to [6, 255].
+ *
+ * This minimum value can be calculated as: 256 % 10 = 6.
+ *
+ * Or more generically: (UINT32_MAX + 1) % upper_bound.
+ *
+ * Then, we can pick random numbers until we get one that is >= this
+ * minimum. Once we have it, we can simply mod it by the limit to get our
+ * answer.
+ *
+ * For our example of modding by 10, we pick random numbers until they are
+ * greater than or equal to 6. Once we have one, we have a value in the
+ * range [6, 255] which when modded by 10 yields uniformly distributed
+ * values [0, 9].
+ *
+ * There are two things to consider while implementing this algorithm:
+ *
+ * 1. Division by 0: Getting called with a 0 upper bound doesn't make sense,
+ * therefore we simply assert that the passed in bound is non-zero.
+ *
+ * 2. 32-bit performance: The above expression to calculate the minimum
+ * value requires 64-bit division. This generally isn't a problem on
+ * 64-bit systems, but 32-bit systems often end up calling a software
+ * implementation (e.g., `__umoddi3`). This is undesirable.
+ *
+ * Therefore, we rewrite the expression as:
+ *
+ * ~(upper_bound - 1) % upper_bound
+ *
+ * This is harder to understand, but it is 100% equivalent.
+ */
+uint32_t i_rand_limit(uint32_t upper_bound)
+{
+ i_assert(upper_bound > 0);
+
+ uint32_t val;
+ uint32_t min = UNSIGNED_MINUS(upper_bound) % upper_bound;
+ while((val = i_rand()) < min);
+ return val % upper_bound;
+}
+#endif
diff --git a/src/lib/randgen.c b/src/lib/randgen.c
new file mode 100644
index 0000000..f6b2da9
--- /dev/null
+++ b/src/lib/randgen.c
@@ -0,0 +1,222 @@
+/* Copyright (c) 2002-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "randgen.h"
+#include <unistd.h>
+#include <fcntl.h>
+
+#ifdef DEBUG
+/* For reproducing tests, fall back onto using a simple deterministic PRNG */
+/* Marsaglia's 1999 KISS, de-macro-ified, and with the fixed KISS11 SHR3,
+ which is clearly what was intended given the "cycle length 2^123" claim. */
+static bool kiss_in_use;
+static unsigned int kiss_seed;
+static uint32_t kiss_z, kiss_w, kiss_jsr, kiss_jcong;
+static void
+kiss_init(unsigned int seed)
+{
+ i_info("Random numbers are PRNG using kiss, as per DOVECOT_SRAND=%u", seed);
+ kiss_seed = seed;
+ kiss_jsr = 0x5eed5eed; /* simply musn't be 0 */
+ kiss_z = 1 ^ (kiss_w = kiss_jcong = seed); /* w=z=0 is bad, see Rose */
+ kiss_in_use = TRUE;
+}
+static unsigned int
+kiss_rand(void)
+{
+ kiss_z = 36969 * (kiss_z&65535) + (kiss_z>>16);
+ kiss_w = 18000 * (kiss_w&65535) + (kiss_w>>16);
+ kiss_jcong = 69069 * kiss_jcong + 1234567;
+ kiss_jsr^=(kiss_jsr<<13); /* <<17, >>13 gives cycle length 2^28.2 max */
+ kiss_jsr^=(kiss_jsr>>17); /* <<13, >>17 gives maximal cycle length */
+ kiss_jsr^=(kiss_jsr<<5);
+ return (((kiss_z<<16) + kiss_w) ^ kiss_jcong) + kiss_jsr;
+}
+int rand_get_last_seed(unsigned int *seed_r)
+{
+ if (!kiss_in_use)
+ return -1; /* not using a deterministic PRNG, seed is irrelevant */
+ *seed_r = kiss_seed;
+ return 0;
+}
+#endif
+
+/* get randomness from either getrandom, arc4random or /dev/urandom */
+
+#if defined(HAVE_GETRANDOM) && HAVE_DECL_GETRANDOM != 0
+# include <sys/random.h>
+# define USE_GETRANDOM
+static bool getrandom_present = TRUE;
+#elif defined(HAVE_ARC4RANDOM)
+# if defined(HAVE_LIBBSD)
+# include <bsd/stdlib.h>
+# endif
+# define USE_ARC4RANDOM
+#else
+static bool getrandom_present = FALSE;
+# define USE_RANDOM_DEV
+#endif
+
+static int init_refcount = 0;
+static int urandom_fd = -1;
+
+#if defined(USE_GETRANDOM) || defined(USE_RANDOM_DEV)
+/* Use a small buffer when reading randomness. This is mainly to make small
+ random reads more efficient, such as i_rand*(). When reading larger amount
+ of randomness this buffer is bypassed.
+
+ There doesn't seem to be a big difference in Linux system CPU usage when
+ buffer size is above 16 bytes. Double it just to be safe. Avoid it being
+ too large anyway so we don't unnecessarily waste CPU and memory. */
+#define RANDOM_READ_BUFFER_SIZE 32
+static unsigned char random_next[RANDOM_READ_BUFFER_SIZE];
+static size_t random_next_pos = 0;
+static size_t random_next_size = 0;
+
+static void random_open_urandom(void)
+{
+ urandom_fd = open(DEV_URANDOM_PATH, O_RDONLY);
+ if (urandom_fd == -1) {
+ if (errno == ENOENT) {
+ i_fatal("open("DEV_URANDOM_PATH") failed: doesn't exist,"
+ "currently we require it");
+ } else {
+ i_fatal("open("DEV_URANDOM_PATH") failed: %m");
+ }
+ }
+ fd_close_on_exec(urandom_fd, TRUE);
+}
+
+static inline int random_read(unsigned char *buf, size_t size)
+{
+ ssize_t ret = 0;
+# if defined(USE_GETRANDOM)
+ if (getrandom_present) {
+ ret = getrandom(buf, size, 0);
+ if (ret < 0 && errno == ENOSYS) {
+ getrandom_present = FALSE;
+ /* It gets complicated here... While the libc (and its
+ headers) indicated that getrandom() was available when
+ we were compiled, the kernel disagreed just now at
+ runtime. Fall back to reading /dev/urandom. */
+ random_open_urandom();
+ }
+ }
+ /* this is here to avoid clang complain,
+ because getrandom_present will be always FALSE
+ if USE_GETRANDOM is not defined */
+ if (!getrandom_present)
+# endif
+ ret = read(urandom_fd, buf, size);
+ if (unlikely(ret <= 0)) {
+ if (ret == 0) {
+ i_fatal("read("DEV_URANDOM_PATH") failed: EOF");
+ } else if (errno != EINTR) {
+ if (getrandom_present) {
+ i_fatal("getrandom() failed: %m");
+ } else {
+ i_fatal("read("DEV_URANDOM_PATH") failed: %m");
+ }
+ }
+ }
+ i_assert(ret > 0 || errno == EINTR);
+ return ret;
+}
+#endif
+
+void random_fill(void *buf, size_t size)
+{
+ i_assert(init_refcount > 0);
+ i_assert(size < SSIZE_T_MAX);
+
+#ifdef DEBUG
+ if (kiss_in_use) {
+ for (size_t pos = 0; pos < size; pos++)
+ ((unsigned char*)buf)[pos] = kiss_rand();
+ return;
+ }
+#endif
+
+#if defined(USE_ARC4RANDOM)
+ arc4random_buf(buf, size);
+#else
+ size_t pos;
+ ssize_t ret;
+
+ for (pos = 0; pos < size; ) {
+ if (size >= sizeof(random_next) && random_next_size == 0) {
+ /* Asking for lots of randomness. Read directly to the
+ destination buffer. */
+ ret = random_read(PTR_OFFSET(buf, pos), size - pos);
+ if (ret > -1)
+ pos += ret;
+ } else {
+ /* Asking for a little randomness. Read via a larger
+ buffer to reduce the number of syscalls. */
+ if (random_next_size > random_next_pos)
+ ret = random_next_size - random_next_pos;
+ else {
+ random_next_pos = 0;
+ ret = random_read(random_next,
+ sizeof(random_next));
+ random_next_size = ret < 0 ? 0 : ret;
+ }
+ if (ret > 0) {
+ size_t used = I_MIN(size - pos, (size_t)ret);
+ memcpy(PTR_OFFSET(buf, pos),
+ random_next + random_next_pos, used);
+ random_next_pos += used;
+ pos += used;
+ }
+ }
+ }
+#endif /* defined(USE_ARC4RANDOM) */
+}
+
+void random_init(void)
+{
+ /* static analyzer seems to require this */
+ unsigned int seed = 0;
+ const char *env_seed;
+
+ if (init_refcount++ > 0)
+ return;
+
+ env_seed = getenv("DOVECOT_SRAND");
+#ifdef DEBUG
+ if (env_seed != NULL && str_to_uint(env_seed, &seed) >= 0) {
+ kiss_init(seed);
+ /* getrandom_present = FALSE; not needed, only used in random_read() */
+ goto normal_exit;
+ }
+#else
+ if (env_seed != NULL && *env_seed != '\0')
+ i_warning("DOVECOT_SRAND is not available in non-debug builds");
+#endif /* DEBUG */
+
+#if defined(USE_RANDOM_DEV)
+ random_open_urandom();
+#endif
+ /* DO NOT REMOVE THIS - It is also
+ needed to make sure getrandom really works.
+ */
+ random_fill(&seed, sizeof(seed));
+#ifdef DEBUG
+ if (env_seed != NULL) {
+ if (strcmp(env_seed, "kiss") != 0)
+ i_fatal("DOVECOT_SRAND not a number or 'kiss'");
+ kiss_init(seed);
+ i_close_fd(&urandom_fd);
+ }
+
+normal_exit:
+#endif
+ srand(seed);
+}
+
+void random_deinit(void)
+{
+ if (--init_refcount > 0)
+ return;
+ i_close_fd(&urandom_fd);
+}
diff --git a/src/lib/randgen.h b/src/lib/randgen.h
new file mode 100644
index 0000000..d532880
--- /dev/null
+++ b/src/lib/randgen.h
@@ -0,0 +1,17 @@
+#ifndef RANDGEN_H
+#define RANDGEN_H
+
+/* Fill given buffer with semi-strong randomness */
+void random_fill(void *buf, size_t size);
+
+/* may be called multiple times,
+ and are called by default in lib_init */
+void random_init(void);
+void random_deinit(void);
+
+#ifdef DEBUG
+/* Debug helper to make random tests reproduceable. 0=got seed, -1=failure. */
+int rand_get_last_seed(unsigned int *seed_r);
+#endif
+
+#endif
diff --git a/src/lib/read-full.c b/src/lib/read-full.c
new file mode 100644
index 0000000..733c3df
--- /dev/null
+++ b/src/lib/read-full.c
@@ -0,0 +1,40 @@
+/* Copyright (c) 2003-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "read-full.h"
+
+#include <unistd.h>
+
+int read_full(int fd, void *data, size_t size)
+{
+ ssize_t ret;
+
+ while (size > 0) {
+ ret = read(fd, data, size < SSIZE_T_MAX ? size : SSIZE_T_MAX);
+ if (ret <= 0)
+ return ret;
+
+ data = PTR_OFFSET(data, ret);
+ size -= ret;
+ }
+
+ return 1;
+}
+
+int pread_full(int fd, void *data, size_t size, off_t offset)
+{
+ ssize_t ret;
+
+ while (size > 0) {
+ ret = pread(fd, data, size < SSIZE_T_MAX ?
+ size : SSIZE_T_MAX, offset);
+ if (ret <= 0)
+ return ret;
+
+ data = PTR_OFFSET(data, ret);
+ size -= ret;
+ offset += ret;
+ }
+
+ return 1;
+}
diff --git a/src/lib/read-full.h b/src/lib/read-full.h
new file mode 100644
index 0000000..8a6dc7d
--- /dev/null
+++ b/src/lib/read-full.h
@@ -0,0 +1,9 @@
+#ifndef READ_FULL_H
+#define READ_FULL_H
+
+/* Read data from file. Returns -1 if error occurred, or 0 if EOF came before
+ everything was read, or 1 if all was ok. */
+int read_full(int fd, void *data, size_t size);
+int pread_full(int fd, void *data, size_t size, off_t offset);
+
+#endif
diff --git a/src/lib/restrict-access.c b/src/lib/restrict-access.c
new file mode 100644
index 0000000..a8fc47d
--- /dev/null
+++ b/src/lib/restrict-access.c
@@ -0,0 +1,531 @@
+/* Copyright (c) 2002-2018 Dovecot authors, see the included COPYING file */
+
+#define _GNU_SOURCE /* setresgid() */
+#include <stdio.h> /* for AIX */
+#include <sys/types.h>
+#include <unistd.h>
+
+#include "lib.h"
+#include "str.h"
+#include "restrict-access.h"
+#include "env-util.h"
+#include "ipwd.h"
+
+#include <time.h>
+#ifdef HAVE_PR_SET_DUMPABLE
+# include <sys/prctl.h>
+#endif
+
+static gid_t process_primary_gid = (gid_t)-1;
+static gid_t process_privileged_gid = (gid_t)-1;
+static bool process_using_priv_gid = FALSE;
+static char *chroot_dir = NULL;
+
+void restrict_access_init(struct restrict_access_settings *set)
+{
+ i_zero(set);
+
+ set->uid = (uid_t)-1;
+ set->gid = (gid_t)-1;
+ set->privileged_gid = (gid_t)-1;
+}
+
+static const char *get_uid_str(uid_t uid)
+{
+ struct passwd pw;
+ const char *ret;
+ int old_errno = errno;
+
+ if (i_getpwuid(uid, &pw) <= 0)
+ ret = dec2str(uid);
+ else
+ ret = t_strdup_printf("%s(%s)", dec2str(uid), pw.pw_name);
+ errno = old_errno;
+ return ret;
+}
+
+static const char *get_gid_str(gid_t gid)
+{
+ struct group group;
+ const char *ret;
+ int old_errno = errno;
+
+ if (i_getgrgid(gid, &group) <= 0)
+ ret = dec2str(gid);
+ else
+ ret = t_strdup_printf("%s(%s)", dec2str(gid), group.gr_name);
+ errno = old_errno;
+ return ret;
+}
+
+static void restrict_init_groups(gid_t primary_gid, gid_t privileged_gid,
+ const char *gid_source)
+{
+ string_t *str;
+
+ if (privileged_gid == (gid_t)-1) {
+ if (primary_gid == getgid() && primary_gid == getegid()) {
+ /* everything is already set */
+ return;
+ }
+
+ if (setgid(primary_gid) == 0)
+ return;
+
+ str = t_str_new(128);
+ str_printfa(str, "setgid(%s", get_gid_str(primary_gid));
+ if (gid_source != NULL)
+ str_printfa(str, " from %s", gid_source);
+ str_printfa(str, ") failed with euid=%s, gid=%s, egid=%s: %m "
+ "(This binary should probably be called with "
+ "process group set to %s instead of %s)",
+ get_uid_str(geteuid()),
+ get_gid_str(getgid()), get_gid_str(getegid()),
+ get_gid_str(primary_gid), get_gid_str(getegid()));
+ i_fatal("%s", str_c(str));
+ }
+
+ if (getegid() != 0 && primary_gid == getgid() &&
+ primary_gid == getegid()) {
+ /* privileged_gid is hopefully in saved ID. if not,
+ there's nothing we can do about it. */
+ return;
+ }
+
+#ifdef HAVE_SETRESGID
+ if (setresgid(primary_gid, primary_gid, privileged_gid) != 0) {
+ i_fatal("setresgid(%s,%s,%s) failed with euid=%s: %m",
+ get_gid_str(primary_gid), get_gid_str(primary_gid),
+ get_gid_str(privileged_gid), get_uid_str(geteuid()));
+ }
+#else
+ if (geteuid() == 0) {
+ /* real, effective, saved -> privileged_gid */
+ if (setgid(privileged_gid) < 0) {
+ i_fatal("setgid(%s) failed: %m",
+ get_gid_str(privileged_gid));
+ }
+ }
+ /* real, effective -> primary_gid
+ saved -> keep */
+ if (setregid(primary_gid, primary_gid) != 0) {
+ i_fatal("setregid(%s,%s) failed with euid=%s: %m",
+ get_gid_str(primary_gid), get_gid_str(privileged_gid),
+ get_uid_str(geteuid()));
+ }
+#endif
+}
+
+gid_t *restrict_get_groups_list(unsigned int *gid_count_r)
+{
+ gid_t *gid_list;
+ int ret, gid_count;
+
+ if ((gid_count = getgroups(0, NULL)) < 0)
+ i_fatal("getgroups() failed: %m");
+
+ /* @UNSAFE */
+ gid_list = t_new(gid_t, gid_count+1); /* +1 in case gid_count=0 */
+ if ((ret = getgroups(gid_count, gid_list)) < 0)
+ i_fatal("getgroups() failed: %m");
+
+ *gid_count_r = ret;
+ return gid_list;
+}
+
+static void drop_restricted_groups(const struct restrict_access_settings *set,
+ gid_t *gid_list, unsigned int *gid_count,
+ bool *have_root_group)
+{
+ /* @UNSAFE */
+ unsigned int i, used;
+
+ for (i = 0, used = 0; i < *gid_count; i++) {
+ if (gid_list[i] >= set->first_valid_gid &&
+ (set->last_valid_gid == 0 ||
+ gid_list[i] <= set->last_valid_gid)) {
+ if (gid_list[i] == 0)
+ *have_root_group = TRUE;
+ gid_list[used++] = gid_list[i];
+ }
+ }
+ *gid_count = used;
+}
+
+static gid_t get_group_id(const char *name)
+{
+ struct group group;
+ gid_t gid;
+
+ if (str_to_gid(name, &gid) == 0)
+ return gid;
+
+ switch (i_getgrnam(name, &group)) {
+ case -1:
+ i_fatal("getgrnam(%s) failed: %m", name);
+ case 0:
+ i_fatal("unknown group name in extra_groups: %s", name);
+ default:
+ return group.gr_gid;
+ }
+}
+
+static void fix_groups_list(const struct restrict_access_settings *set,
+ bool preserve_existing, bool *have_root_group)
+{
+ gid_t gid, *gid_list, *gid_list2;
+ const char *const *tmp, *empty = NULL;
+ unsigned int i, gid_count;
+ bool add_primary_gid;
+
+ /* if we're using a privileged GID, we can temporarily drop our
+ effective GID. we still want to be able to use its privileges,
+ so add it to supplementary groups. */
+ add_primary_gid = process_privileged_gid != (gid_t)-1;
+
+ tmp = set->extra_groups == NULL ? &empty :
+ t_strsplit_spaces(set->extra_groups, ", ");
+
+ if (preserve_existing) {
+ gid_list = restrict_get_groups_list(&gid_count);
+ drop_restricted_groups(set, gid_list, &gid_count,
+ have_root_group);
+ /* see if the list already contains the primary GID */
+ for (i = 0; i < gid_count; i++) {
+ if (gid_list[i] == process_primary_gid) {
+ add_primary_gid = FALSE;
+ break;
+ }
+ }
+ } else {
+ gid_list = NULL;
+ gid_count = 0;
+ }
+ if (gid_count == 0) {
+ /* Some OSes don't like an empty groups list,
+ so use the primary GID as the only one. */
+ gid_list = t_new(gid_t, 2);
+ gid_list[0] = process_primary_gid;
+ gid_count = 1;
+ add_primary_gid = FALSE;
+ }
+
+ if (*tmp != NULL || add_primary_gid) {
+ /* @UNSAFE: add extra groups and/or primary GID to gids list */
+ gid_list2 = t_new(gid_t, gid_count + str_array_length(tmp) + 1);
+ memcpy(gid_list2, gid_list, gid_count * sizeof(gid_t));
+ for (; *tmp != NULL; tmp++) {
+ gid = get_group_id(*tmp);
+ if (gid != process_primary_gid)
+ gid_list2[gid_count++] = gid;
+ }
+ if (add_primary_gid)
+ gid_list2[gid_count++] = process_primary_gid;
+ gid_list = gid_list2;
+ }
+
+ if (setgroups(gid_count, gid_list) < 0) {
+ if (errno == EINVAL) {
+ i_fatal("setgroups(%s) failed: Too many extra groups",
+ set->extra_groups == NULL ? "" :
+ set->extra_groups);
+ } else {
+ i_fatal("setgroups() failed: %m");
+ }
+ }
+}
+
+static const char *
+get_setuid_error_str(const struct restrict_access_settings *set, uid_t target_uid)
+{
+ string_t *str = t_str_new(128);
+
+ str_printfa(str, "setuid(%s", get_uid_str(target_uid));
+ if (set->uid_source != NULL)
+ str_printfa(str, " from %s", set->uid_source);
+ str_printfa(str, ") failed with euid=%s: %m ",
+ get_uid_str(geteuid()));
+ if (errno == EAGAIN) {
+ str_append(str, "(ulimit -u reached)");
+ } else {
+ str_printfa(str, "(This binary should probably be called with "
+ "process user set to %s instead of %s)",
+ get_uid_str(target_uid), get_uid_str(geteuid()));
+ }
+ return str_c(str);
+}
+
+void restrict_access(const struct restrict_access_settings *set,
+ enum restrict_access_flags flags, const char *home)
+{
+ bool is_root, have_root_group, preserve_groups = FALSE;
+ bool allow_root_gid;
+ bool allow_root = (flags & RESTRICT_ACCESS_FLAG_ALLOW_ROOT) != 0;
+ uid_t target_uid = set->uid;
+
+ is_root = geteuid() == 0;
+
+ if (!is_root &&
+ !set->allow_setuid_root &&
+ getuid() == 0) {
+ /* recover current effective UID */
+ if (target_uid == (uid_t)-1)
+ target_uid = geteuid();
+ else
+ i_assert(target_uid > 0);
+ /* try to elevate to root */
+ if (seteuid(0) < 0)
+ i_fatal("seteuid(0) failed: %m");
+ is_root = TRUE;
+ }
+
+ /* set the primary/privileged group */
+ process_primary_gid = set->gid;
+ process_privileged_gid = set->privileged_gid;
+ if (process_privileged_gid == process_primary_gid) {
+ /* a pointless configuration, ignore it */
+ process_privileged_gid = (gid_t)-1;
+ }
+
+ have_root_group = process_primary_gid == 0;
+ if (process_primary_gid != (gid_t)-1 ||
+ process_privileged_gid != (gid_t)-1) {
+ if (process_primary_gid == (gid_t)-1)
+ process_primary_gid = getegid();
+ restrict_init_groups(process_primary_gid,
+ process_privileged_gid, set->gid_source);
+ } else {
+ if (process_primary_gid == (gid_t)-1)
+ process_primary_gid = getegid();
+ }
+
+ /* set system user's groups */
+ if (set->system_groups_user != NULL && is_root) {
+ if (initgroups(set->system_groups_user,
+ process_primary_gid) < 0) {
+ i_fatal("initgroups(%s, %s) failed: %m",
+ set->system_groups_user,
+ get_gid_str(process_primary_gid));
+ }
+ preserve_groups = TRUE;
+ }
+
+ /* add extra groups. if we set system user's groups, drop the
+ restricted groups at the same time. */
+ if (is_root) T_BEGIN {
+ fix_groups_list(set, preserve_groups,
+ &have_root_group);
+ } T_END;
+
+ /* chrooting */
+ if (set->chroot_dir != NULL) {
+ /* kludge: localtime() must be called before chroot(),
+ or the timezone isn't known */
+ time_t t = 0;
+ (void)localtime(&t);
+
+ if (chroot(set->chroot_dir) != 0)
+ i_fatal("chroot(%s) failed: %m", set->chroot_dir);
+ /* makes static analyzers happy, and is more secure */
+ if (chdir("/") != 0)
+ i_fatal("chdir(/) failed: %m");
+
+ chroot_dir = i_strdup(set->chroot_dir);
+
+ if (home != NULL) {
+ if (chdir(home) < 0) {
+ i_error("chdir(%s) failed: %m", home);
+ }
+ }
+ }
+
+ /* uid last */
+ if (target_uid != (uid_t)-1) {
+ if (setuid(target_uid) != 0)
+ i_fatal("%s", get_setuid_error_str(set, target_uid));
+ }
+
+ /* verify that we actually dropped the privileges */
+ if ((target_uid != (uid_t)-1 && target_uid != 0) || !allow_root) {
+ if (setuid(0) == 0) {
+ if (!allow_root &&
+ (target_uid == 0 || target_uid == (uid_t)-1))
+ i_fatal("This process must not be run as root");
+
+ i_fatal("We couldn't drop root privileges");
+ }
+ }
+
+ if (set->first_valid_gid != 0)
+ allow_root_gid = FALSE;
+ else if (process_primary_gid == 0 || process_privileged_gid == 0)
+ allow_root_gid = TRUE;
+ else
+ allow_root_gid = FALSE;
+
+ if (!allow_root_gid && target_uid != 0 &&
+ (target_uid != (uid_t)-1 || !is_root)) {
+ if (getgid() == 0 || getegid() == 0 || setgid(0) == 0) {
+ if (process_primary_gid == 0)
+ i_fatal("GID 0 isn't permitted");
+ i_fatal("We couldn't drop root group privileges "
+ "(wanted=%s, gid=%s, egid=%s)",
+ get_gid_str(process_primary_gid),
+ get_gid_str(getgid()), get_gid_str(getegid()));
+ }
+ }
+}
+
+void restrict_access_set_env(const struct restrict_access_settings *set)
+{
+ if (set->system_groups_user != NULL &&
+ *set->system_groups_user != '\0')
+ env_put("RESTRICT_USER", set->system_groups_user);
+ if (set->chroot_dir != NULL && *set->chroot_dir != '\0')
+ env_put("RESTRICT_CHROOT", set->chroot_dir);
+
+ if (set->uid != (uid_t)-1)
+ env_put("RESTRICT_SETUID", dec2str(set->uid));
+ if (set->gid != (gid_t)-1)
+ env_put("RESTRICT_SETGID", dec2str(set->gid));
+ if (set->privileged_gid != (gid_t)-1)
+ env_put("RESTRICT_SETGID_PRIV", dec2str(set->privileged_gid));
+ if (set->extra_groups != NULL && *set->extra_groups != '\0')
+ env_put("RESTRICT_SETEXTRAGROUPS", set->extra_groups);
+
+ if (set->first_valid_gid != 0)
+ env_put("RESTRICT_GID_FIRST", dec2str(set->first_valid_gid));
+ if (set->last_valid_gid != 0)
+ env_put("RESTRICT_GID_LAST", dec2str(set->last_valid_gid));
+}
+
+static const char *null_if_empty(const char *str)
+{
+ return str == NULL || *str == '\0' ? NULL : str;
+}
+
+void restrict_access_get_env(struct restrict_access_settings *set_r)
+{
+ const char *value;
+
+ restrict_access_init(set_r);
+ if ((value = getenv("RESTRICT_SETUID")) != NULL) {
+ if (str_to_uid(value, &set_r->uid) < 0)
+ i_fatal("Invalid uid: %s", value);
+ }
+ if ((value = getenv("RESTRICT_SETGID")) != NULL) {
+ if (str_to_gid(value, &set_r->gid) < 0)
+ i_fatal("Invalid gid: %s", value);
+ }
+ if ((value = getenv("RESTRICT_SETGID_PRIV")) != NULL) {
+ if (str_to_gid(value, &set_r->privileged_gid) < 0)
+ i_fatal("Invalid privileged_gid: %s", value);
+ }
+ if ((value = getenv("RESTRICT_GID_FIRST")) != NULL) {
+ if (str_to_gid(value, &set_r->first_valid_gid) < 0)
+ i_fatal("Invalid first_valid_gid: %s", value);
+ }
+ if ((value = getenv("RESTRICT_GID_LAST")) != NULL) {
+ if (str_to_gid(value, &set_r->last_valid_gid) < 0)
+ i_fatal("Invalid last_value_gid: %s", value);
+ }
+
+ set_r->extra_groups = null_if_empty(getenv("RESTRICT_SETEXTRAGROUPS"));
+ set_r->system_groups_user = null_if_empty(getenv("RESTRICT_USER"));
+ set_r->chroot_dir = null_if_empty(getenv("RESTRICT_CHROOT"));
+}
+
+void restrict_access_by_env(enum restrict_access_flags flags, const char *home)
+{
+ struct restrict_access_settings set;
+
+ restrict_access_get_env(&set);
+ restrict_access(&set, flags, home);
+
+ /* clear the environment, so we don't fail if we get back here */
+ env_remove("RESTRICT_SETUID");
+ if (process_privileged_gid == (gid_t)-1) {
+ /* if we're dropping privileges before executing and
+ a privileged group is set, the groups must be fixed
+ after exec */
+ env_remove("RESTRICT_SETGID");
+ env_remove("RESTRICT_SETGID_PRIV");
+ }
+ env_remove("RESTRICT_GID_FIRST");
+ env_remove("RESTRICT_GID_LAST");
+ if (getuid() != 0)
+ env_remove("RESTRICT_SETEXTRAGROUPS");
+ else {
+ /* Preserve RESTRICT_SETEXTRAGROUPS, so if we're again dropping
+ more privileges we'll still preserve the extra groups. This
+ mainly means preserving service { extra_groups } for lmtp
+ and doveadm accesses. */
+ }
+ env_remove("RESTRICT_USER");
+ env_remove("RESTRICT_CHROOT");
+}
+
+const char *restrict_access_get_current_chroot(void)
+{
+ return chroot_dir;
+}
+
+void restrict_access_set_dumpable(bool allow ATTR_UNUSED)
+{
+#ifdef HAVE_PR_SET_DUMPABLE
+ if (prctl(PR_SET_DUMPABLE, allow ? 1 : 0, 0, 0, 0) < 0)
+ i_error("prctl(PR_SET_DUMPABLE) failed: %m");
+#endif
+}
+
+bool restrict_access_get_dumpable(void)
+{
+#ifdef HAVE_PR_SET_DUMPABLE
+ bool allow = FALSE;
+ if (prctl(PR_GET_DUMPABLE, &allow, 0, 0, 0) < 0)
+ i_error("prctl(PR_GET_DUMPABLE) failed: %m");
+ return allow;
+#endif
+ return TRUE;
+}
+
+void restrict_access_allow_coredumps(bool allow)
+{
+ if (getenv("PR_SET_DUMPABLE") != NULL) {
+ restrict_access_set_dumpable(allow);
+ }
+}
+
+int restrict_access_use_priv_gid(void)
+{
+ i_assert(!process_using_priv_gid);
+
+ if (process_privileged_gid == (gid_t)-1)
+ return 0;
+ if (setegid(process_privileged_gid) < 0) {
+ i_error("setegid(privileged) failed: %m");
+ return -1;
+ }
+ process_using_priv_gid = TRUE;
+ return 0;
+}
+
+void restrict_access_drop_priv_gid(void)
+{
+ if (!process_using_priv_gid)
+ return;
+
+ if (setegid(process_primary_gid) < 0)
+ i_fatal("setegid(primary) failed: %m");
+ process_using_priv_gid = FALSE;
+}
+
+bool restrict_access_have_priv_gid(void)
+{
+ return process_privileged_gid != (gid_t)-1;
+}
+
+void restrict_access_deinit(void)
+{
+ i_free(chroot_dir);
+}
diff --git a/src/lib/restrict-access.h b/src/lib/restrict-access.h
new file mode 100644
index 0000000..ba4d893
--- /dev/null
+++ b/src/lib/restrict-access.h
@@ -0,0 +1,90 @@
+#ifndef RESTRICT_ACCESS_H
+#define RESTRICT_ACCESS_H
+
+enum restrict_access_flags {
+ /* If flags given to restrict_access() include
+ * RESTRICT_ACCESS_FLAG_ALLOW_ROOT, we won't kill
+ * ourself when we have root privileges. */
+ RESTRICT_ACCESS_FLAG_ALLOW_ROOT = 1,
+};
+
+struct restrict_access_settings {
+ /* UID to use, or (uid_t)-1 if you don't want to change it */
+ uid_t uid;
+ /* Effective GID to use, or (gid_t)-1 if you don't want to change it */
+ gid_t gid;
+ /* If not (gid_t)-1, the privileged GID can be temporarily
+ enabled/disabled. */
+ gid_t privileged_gid;
+
+ /* Add access to these space or comma -separated extra groups */
+ const char *extra_groups;
+ /* Add access to groups this system user belongs to */
+ const char *system_groups_user;
+
+ /* All specified GIDs must be in this range. If extra_groups or system
+ group user contains other GIDs, they're silently dropped. */
+ gid_t first_valid_gid, last_valid_gid;
+
+ /* Human readable "source" of UID and GID values. If non-NULL,
+ displayed on error messages about failing to change uid/gid. */
+ const char *uid_source, *gid_source;
+
+ /* Chroot directory */
+ const char *chroot_dir;
+
+ /* Allow running in setuid-root mode, where real UID is root and
+ * effective UID is non-root. By default the real UID is changed
+ * to be the same as the effective UID. */
+ bool allow_setuid_root;
+};
+
+/* Initialize settings with values that don't change anything. */
+void restrict_access_init(struct restrict_access_settings *set);
+/* Restrict access as specified by the settings. If home is not NULL,
+ it's chdir()ed after chrooting, otherwise it chdirs to / (the chroot). */
+void restrict_access(const struct restrict_access_settings *set,
+ enum restrict_access_flags flags, const char *home)
+ ATTR_NULL(3);
+/* Set environment variables so they can be read with
+ restrict_access_by_env(). */
+void restrict_access_set_env(const struct restrict_access_settings *set);
+/* Read restrict_access_set_env() environments back into struct. */
+void restrict_access_get_env(struct restrict_access_settings *set_r);
+/* Read restrictions from environment and call restrict_access().
+ If flags do not include RESTRICT_ACCESS_FLAG_ALLOW_ROOT, we'll kill ourself
+ unless the RESTRICT_* environments caused root privileges to be dropped */
+void restrict_access_by_env(enum restrict_access_flags flags,
+ const char *home) ATTR_NULL(2);
+
+/* Return the chrooted directory if restrict_access*() chrooted,
+ otherwise NULL. */
+const char *restrict_access_get_current_chroot(void);
+
+/*
+ Checks if PR_SET_DUMPABLE environment variable is set, and if it is,
+ calls restrict_access_set_dumpable(allow).
+*/
+void restrict_access_allow_coredumps(bool allow);
+
+/* Sets process dumpable true or false. Setting this true allows core dumping,
+ reading /proc/self/io, attaching with PTRACE_ATTACH, and also changes
+ ownership of /proc/[pid] directory. */
+void restrict_access_set_dumpable(bool allow);
+
+/* Gets process dumpability, returns TRUE if not supported, because
+ we then assume that constraint is not present. */
+bool restrict_access_get_dumpable(void);
+
+/* If privileged_gid was set, these functions can be used to temporarily
+ gain access to the group. */
+int restrict_access_use_priv_gid(void);
+void restrict_access_drop_priv_gid(void);
+/* Returns TRUE if privileged GID exists for this process. */
+bool restrict_access_have_priv_gid(void);
+
+gid_t *restrict_get_groups_list(unsigned int *gid_count_r);
+
+void restrict_access_deinit(void);
+
+#endif
diff --git a/src/lib/restrict-process-size.c b/src/lib/restrict-process-size.c
new file mode 100644
index 0000000..0181473
--- /dev/null
+++ b/src/lib/restrict-process-size.c
@@ -0,0 +1,113 @@
+/* Copyright (c) 2002-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "restrict-process-size.h"
+
+#include <unistd.h>
+
+void restrict_process_size(rlim_t bytes)
+{
+ struct rlimit rlim;
+
+ rlim.rlim_max = rlim.rlim_cur = bytes;
+ if (setrlimit(RLIMIT_DATA, &rlim) < 0) {
+ i_fatal("setrlimit(RLIMIT_DATA, %llu): %m",
+ (unsigned long long)bytes);
+ }
+
+#ifdef HAVE_RLIMIT_AS
+ if (setrlimit(RLIMIT_AS, &rlim) < 0) {
+ i_fatal("setrlimit(RLIMIT_AS, %llu): %m",
+ (unsigned long long)bytes);
+ }
+#endif
+}
+
+void restrict_process_count(rlim_t count ATTR_UNUSED)
+{
+#ifdef HAVE_RLIMIT_NPROC
+ struct rlimit rlim;
+
+ rlim.rlim_max = rlim.rlim_cur = count;
+ if (setrlimit(RLIMIT_NPROC, &rlim) < 0) {
+ i_error("setrlimit(RLIMIT_NPROC, %llu): %m",
+ (unsigned long long)count);
+ }
+#endif
+}
+
+void restrict_fd_limit(rlim_t count)
+{
+#ifdef HAVE_SETRLIMIT
+ struct rlimit rlim;
+
+ rlim.rlim_cur = rlim.rlim_max = count;
+ if (setrlimit(RLIMIT_NOFILE, &rlim) < 0) {
+ i_error("setrlimit(RLIMIT_NOFILE, %llu): %m",
+ (unsigned long long)count);
+ }
+#endif
+}
+
+int restrict_get_process_size(rlim_t *limit_r)
+{
+ struct rlimit rlim;
+
+#ifdef HAVE_RLIMIT_AS
+ if (getrlimit(RLIMIT_AS, &rlim) < 0) {
+ i_error("getrlimit(RLIMIT_AS): %m");
+ return -1;
+ }
+#else
+ if (getrlimit(RLIMIT_DATA, &rlim) < 0) {
+ i_error("getrlimit(RLIMIT_DATA): %m");
+ return -1;
+ }
+#endif
+ *limit_r = rlim.rlim_cur;
+ return 0;
+}
+
+int restrict_get_core_limit(rlim_t *limit_r)
+{
+#ifdef HAVE_RLIMIT_CORE
+ struct rlimit rlim;
+
+ if (getrlimit(RLIMIT_CORE, &rlim) < 0) {
+ i_error("getrlimit(RLIMIT_CORE) failed: %m");
+ return -1;
+ }
+ *limit_r = rlim.rlim_cur;
+ return 0;
+#else
+ return -1;
+#endif
+}
+
+int restrict_get_process_limit(rlim_t *limit_r)
+{
+#ifdef HAVE_RLIMIT_NPROC
+ struct rlimit rlim;
+
+ if (getrlimit(RLIMIT_NPROC, &rlim) < 0) {
+ i_error("getrlimit(RLIMIT_NPROC) failed: %m");
+ return -1;
+ }
+ *limit_r = rlim.rlim_cur;
+ return 0;
+#else
+ return -1;
+#endif
+}
+
+int restrict_get_fd_limit(rlim_t *limit_r)
+{
+ struct rlimit rlim;
+
+ if (getrlimit(RLIMIT_NOFILE, &rlim) < 0) {
+ i_error("getrlimit(RLIMIT_NOFILE) failed: %m");
+ return -1;
+ }
+ *limit_r = rlim.rlim_cur;
+ return 0;
+}
diff --git a/src/lib/restrict-process-size.h b/src/lib/restrict-process-size.h
new file mode 100644
index 0000000..35c323d
--- /dev/null
+++ b/src/lib/restrict-process-size.h
@@ -0,0 +1,25 @@
+#ifndef RESTRICT_PROCESS_SIZE_H
+#define RESTRICT_PROCESS_SIZE_H
+
+#include <sys/time.h>
+#ifdef HAVE_SYS_RESOURCE_H
+# include <sys/resource.h>
+#endif
+
+/* Restrict max. process size. */
+void restrict_process_size(rlim_t bytes);
+/* Restrict max. number of processes. */
+void restrict_process_count(rlim_t count);
+/* Set fd limit to count. */
+void restrict_fd_limit(rlim_t count);
+
+/* Get the core dump size limit. Returns 0 if ok, -1 if lookup failed. */
+int restrict_get_core_limit(rlim_t *limit_r);
+/* Get the process VSZ size limit. Returns 0 if ok, -1 if lookup failed. */
+int restrict_get_process_size(rlim_t *limit_r);
+/* Get the process count limit. Returns 0 if ok, -1 if lookup failed. */
+int restrict_get_process_limit(rlim_t *limit_r);
+/* Get the fd limit. Returns 0 if ok, -1 if lookup failed. */
+int restrict_get_fd_limit(rlim_t *limit_r);
+
+#endif
diff --git a/src/lib/safe-memset.c b/src/lib/safe-memset.c
new file mode 100644
index 0000000..3062a41
--- /dev/null
+++ b/src/lib/safe-memset.c
@@ -0,0 +1,17 @@
+/* Copyright (c) 2002-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "safe-memset.h"
+
+void safe_memset(void *data, int c, size_t size)
+{
+ volatile unsigned int volatile_zero_idx = 0;
+ volatile unsigned char *p = data;
+
+ if (size == 0)
+ return;
+
+ do {
+ memset(data, c, size);
+ } while (p[volatile_zero_idx] != c);
+}
diff --git a/src/lib/safe-memset.h b/src/lib/safe-memset.h
new file mode 100644
index 0000000..affd562
--- /dev/null
+++ b/src/lib/safe-memset.h
@@ -0,0 +1,8 @@
+#ifndef SAFE_MEMSET_H
+#define SAFE_MEMSET_H
+
+/* memset() guaranteed not to get optimized away by compiler.
+ Should be used instead of memset() when clearing any sensitive data. */
+void safe_memset(void *data, int c, size_t size);
+
+#endif
diff --git a/src/lib/safe-mkdir.c b/src/lib/safe-mkdir.c
new file mode 100644
index 0000000..dc695c7
--- /dev/null
+++ b/src/lib/safe-mkdir.c
@@ -0,0 +1,78 @@
+/* Copyright (c) 2002-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "safe-mkdir.h"
+
+#include <sys/stat.h>
+#include <unistd.h>
+#include <fcntl.h>
+
+int safe_mkdir(const char *dir, mode_t mode, uid_t uid, gid_t gid)
+{
+ struct stat st;
+ int fd, ret = 2, changed_ret = 0;
+
+ if (lstat(dir, &st) < 0) {
+ if (errno != ENOENT)
+ i_fatal("lstat() failed for %s: %m", dir);
+
+ if (mkdir(dir, mode) < 0) {
+ if (errno != EEXIST)
+ i_fatal("Can't create directory %s: %m", dir);
+ } else {
+ /* created it */
+ ret = changed_ret = 1;
+ }
+ }
+
+ /* use fchown() and fchmod() just to make sure we aren't following
+ symbolic links. */
+ fd = open(dir, O_RDONLY);
+ if (fd == -1)
+ i_fatal("open() failed for %s: %m", dir);
+
+ if (fstat(fd, &st) < 0)
+ i_fatal("fstat() failed for %s: %m", dir);
+
+ if (!S_ISDIR(st.st_mode) || S_ISLNK(st.st_mode))
+ i_fatal("Not a directory %s", dir);
+
+ /* change the file owner first, since it's the only user one who
+ can mess up with the file mode. */
+ if ((st.st_uid != uid && uid != (uid_t)-1) ||
+ (st.st_gid != gid && gid != (gid_t)-1)) {
+ if (fchown(fd, uid, gid) < 0)
+ i_fatal("fchown() failed for %s: %m", dir);
+ ret = changed_ret;
+ }
+
+ if ((st.st_mode & 07777) != mode) {
+ if (fchmod(fd, mode) < 0)
+ i_fatal("chmod() failed for %s: %m", dir);
+ ret = changed_ret;
+ }
+
+ if (close(fd) < 0)
+ i_fatal("close() failed for %s: %m", dir);
+
+ /* paranoia: make sure we succeeded in everything. */
+ if (lstat(dir, &st) < 0)
+ i_fatal("lstat() check failed for %s: %m", dir);
+
+ if (!S_ISDIR(st.st_mode) || S_ISLNK(st.st_mode))
+ i_fatal("Not a directory %s", dir);
+
+ if ((st.st_mode & 07777) != mode) {
+ i_fatal("safe_mkdir() failed: %s (%o) is still not mode %o",
+ dir, (int)st.st_mode, (int)mode);
+ }
+ if ((st.st_uid != uid && uid != (uid_t)-1) ||
+ (st.st_gid != gid && gid != (gid_t)-1)) {
+ i_fatal("safe_mkdir() failed: %s (%s, %s) "
+ "is still not owned by %s.%s",
+ dir, dec2str(st.st_uid), dec2str(st.st_gid),
+ dec2str(uid), dec2str(gid));
+ }
+
+ return ret;
+}
diff --git a/src/lib/safe-mkdir.h b/src/lib/safe-mkdir.h
new file mode 100644
index 0000000..eb75fd1
--- /dev/null
+++ b/src/lib/safe-mkdir.h
@@ -0,0 +1,10 @@
+#ifndef SAFE_MKDIR_H
+#define SAFE_MKDIR_H
+
+/* Either create a directory or make sure that it already exists with given
+ permissions. If anything fails, the i_fatal() is called. Returns 1 if
+ directory was created, 2 if it already existed with correct permissions,
+ 0 if we changed permissions. */
+int safe_mkdir(const char *dir, mode_t mode, uid_t uid, gid_t gid);
+
+#endif
diff --git a/src/lib/safe-mkstemp.c b/src/lib/safe-mkstemp.c
new file mode 100644
index 0000000..27b401e
--- /dev/null
+++ b/src/lib/safe-mkstemp.c
@@ -0,0 +1,106 @@
+/* Copyright (c) 2007-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "str.h"
+#include "hex-binary.h"
+#include "randgen.h"
+#include "hostpid.h"
+#include "eacces-error.h"
+#include "safe-mkstemp.h"
+
+#include <unistd.h>
+#include <fcntl.h>
+#include <sys/stat.h>
+
+static int ATTR_NULL(5)
+safe_mkstemp_full(string_t *prefix, mode_t mode, uid_t uid, gid_t gid,
+ const char *gid_origin)
+{
+ size_t prefix_len;
+ struct stat st;
+ unsigned char randbuf[8];
+ mode_t old_umask;
+ int fd;
+
+ prefix_len = str_len(prefix);
+ for (;;) {
+ do {
+ random_fill(randbuf, sizeof(randbuf));
+ str_truncate(prefix, prefix_len);
+ str_append(prefix,
+ binary_to_hex(randbuf, sizeof(randbuf)));
+ } while (lstat(str_c(prefix), &st) == 0);
+
+ if (errno != ENOENT) {
+ i_error("stat(%s) failed: %m", str_c(prefix));
+ str_truncate(prefix, prefix_len);
+ return -1;
+ }
+
+ old_umask = umask(0666 ^ mode);
+ fd = open(str_c(prefix), O_RDWR | O_EXCL | O_CREAT, 0666);
+ umask(old_umask);
+ if (fd != -1)
+ break;
+
+ if (errno != EEXIST) {
+ if (errno != ENOENT && errno != EACCES)
+ i_error("open(%s) failed: %m", str_c(prefix));
+ str_truncate(prefix, prefix_len);
+ return -1;
+ }
+ }
+ if (uid == (uid_t)-1 && gid == (gid_t)-1)
+ return fd;
+
+ if (fchown(fd, uid, gid) < 0) {
+ if (errno == EPERM) {
+ i_error("%s", eperm_error_get_chgrp("fchown",
+ str_c(prefix), gid, gid_origin));
+ } else {
+ i_error("fchown(%s, %ld, %ld) failed: %m",
+ str_c(prefix),
+ uid == (uid_t)-1 ? -1L : (long)uid,
+ gid == (gid_t)-1 ? -1L : (long)gid);
+ }
+ i_close_fd(&fd);
+ i_unlink(str_c(prefix));
+ str_truncate(prefix, prefix_len);
+ return -1;
+ }
+ return fd;
+}
+
+int safe_mkstemp(string_t *prefix, mode_t mode, uid_t uid, gid_t gid)
+{
+ return safe_mkstemp_full(prefix, mode, uid, gid, NULL);
+}
+
+int safe_mkstemp_group(string_t *prefix, mode_t mode,
+ gid_t gid, const char *gid_origin)
+{
+ return safe_mkstemp_full(prefix, mode, (uid_t)-1, gid, gid_origin);
+}
+
+int safe_mkstemp_hostpid(string_t *prefix, mode_t mode, uid_t uid, gid_t gid)
+{
+ size_t orig_prefix_len = str_len(prefix);
+ int fd;
+
+ str_printfa(prefix, "%s.%s.", my_hostname, my_pid);
+ if ((fd = safe_mkstemp(prefix, mode, uid, gid)) == -1)
+ str_truncate(prefix, orig_prefix_len);
+ return fd;
+}
+
+int safe_mkstemp_hostpid_group(string_t *prefix, mode_t mode,
+ gid_t gid, const char *gid_origin)
+{
+ size_t orig_prefix_len = str_len(prefix);
+ int fd;
+
+ str_printfa(prefix, "%s.%s.", my_hostname, my_pid);
+ if ((fd = safe_mkstemp_group(prefix, mode, gid, gid_origin)) == -1)
+ str_truncate(prefix, orig_prefix_len);
+ return fd;
+}
diff --git a/src/lib/safe-mkstemp.h b/src/lib/safe-mkstemp.h
new file mode 100644
index 0000000..f04d5cf
--- /dev/null
+++ b/src/lib/safe-mkstemp.h
@@ -0,0 +1,15 @@
+#ifndef SAFE_MKSTEMP_H
+#define SAFE_MKSTEMP_H
+
+/* Create a new file with a given prefix. The string is updated to contain the
+ created filename. uid and gid can be (uid_t)-1 and (gid_t)-1 to use the
+ defaults. */
+int safe_mkstemp(string_t *prefix, mode_t mode, uid_t uid, gid_t gid);
+int safe_mkstemp_group(string_t *prefix, mode_t mode,
+ gid_t gid, const char *gid_origin);
+/* Append host and PID to the prefix. */
+int safe_mkstemp_hostpid(string_t *prefix, mode_t mode, uid_t uid, gid_t gid);
+int safe_mkstemp_hostpid_group(string_t *prefix, mode_t mode,
+ gid_t gid, const char *gid_origin);
+
+#endif
diff --git a/src/lib/sendfile-util.c b/src/lib/sendfile-util.c
new file mode 100644
index 0000000..662285b
--- /dev/null
+++ b/src/lib/sendfile-util.c
@@ -0,0 +1,137 @@
+/* Copyright (c) 2002-2018 Dovecot authors, see the included COPYING file */
+
+/* kludge a bit to remove _FILE_OFFSET_BITS definition from config.h.
+ It's required to be able to include sys/sendfile.h with Linux. */
+#include "config.h"
+#undef HAVE_CONFIG_H
+
+#ifdef HAVE_LINUX_SENDFILE
+# undef _FILE_OFFSET_BITS
+#endif
+
+#include "lib.h"
+#include "sendfile-util.h"
+
+#ifdef HAVE_LINUX_SENDFILE
+
+#include <sys/sendfile.h>
+
+ssize_t safe_sendfile(int out_fd, int in_fd, uoff_t *offset, size_t count)
+{
+ /* REMEMBER: uoff_t and off_t may not be of same size. */
+ off_t safe_offset;
+ ssize_t ret;
+
+ i_assert(count > 0);
+
+ /* make sure given offset fits into off_t */
+ if (sizeof(off_t) * CHAR_BIT == 32) {
+ /* 32bit off_t */
+ if (*offset >= 2147483647L) {
+ errno = EINVAL;
+ return -1;
+ }
+ if (count > 2147483647L - *offset)
+ count = 2147483647L - *offset;
+ } else {
+ /* they're most likely the same size. if not, fix this
+ code later */
+ i_assert(sizeof(off_t) == sizeof(uoff_t));
+
+ if (*offset >= OFF_T_MAX) {
+ errno = EINVAL;
+ return -1;
+ }
+ if (count > OFF_T_MAX - *offset)
+ count = OFF_T_MAX - *offset;
+ }
+
+ safe_offset = (off_t)*offset;
+ ret = sendfile(out_fd, in_fd, &safe_offset, count);
+ /* ret=0 : trying to read past EOF */
+ *offset = (uoff_t)safe_offset;
+ return ret;
+}
+
+#elif defined(HAVE_FREEBSD_SENDFILE)
+
+#include <sys/socket.h>
+#include <sys/uio.h>
+
+ssize_t safe_sendfile(int out_fd, int in_fd, uoff_t *offset, size_t count)
+{
+ struct sf_hdtr hdtr;
+ off_t sbytes;
+ int ret;
+
+ /* if count=0 is passed to sendfile(), it sends everything
+ from in_fd until EOF. We don't want that. */
+ i_assert(count > 0);
+ i_assert(count <= SSIZE_T_MAX);
+
+ i_zero(&hdtr);
+ ret = sendfile(in_fd, out_fd, *offset, count, &hdtr, &sbytes, 0);
+
+ *offset += sbytes;
+
+ if (ret == 0 || (ret < 0 && errno == EAGAIN && sbytes > 0))
+ return (ssize_t)sbytes;
+ else {
+ if (errno == ENOTSOCK) {
+ /* out_fd wasn't a socket. behave as if sendfile()
+ wasn't supported at all. */
+ errno = EINVAL;
+ }
+ return -1;
+ }
+}
+
+#elif defined (HAVE_SOLARIS_SENDFILE)
+
+#include <sys/sendfile.h>
+#include "net.h"
+
+ssize_t safe_sendfile(int out_fd, int in_fd, uoff_t *offset, size_t count)
+{
+ ssize_t ret;
+ off_t s_offset;
+
+ i_assert(count > 0);
+ i_assert(count <= SSIZE_T_MAX);
+
+ /* NOTE: if outfd is not a socket, some Solaris versions will
+ kernel panic */
+
+ s_offset = (off_t)*offset;
+ ret = sendfile(out_fd, in_fd, &s_offset, count);
+
+ if (ret < 0) {
+ /* if remote is gone, EPIPE is returned */
+ if (errno == EINVAL) {
+ /* most likely trying to read past EOF */
+ ret = 0;
+ } else if (errno == EAFNOSUPPORT || errno == EOPNOTSUPP) {
+ /* not supported, return Linux-like EINVAL so caller
+ sees only consistent errnos. */
+ errno = EINVAL;
+ } else if (s_offset != (off_t)*offset) {
+ /* some data was sent, return it */
+ i_assert(s_offset > (off_t)*offset);
+ ret = s_offset - (off_t)*offset;
+ }
+ }
+ *offset = (uoff_t)s_offset;
+ i_assert(ret < 0 || (size_t)ret <= count);
+ return ret;
+}
+
+#else
+ssize_t safe_sendfile(int out_fd ATTR_UNUSED, int in_fd ATTR_UNUSED,
+ uoff_t *offset ATTR_UNUSED,
+ size_t count ATTR_UNUSED)
+{
+ errno = EINVAL;
+ return -1;
+}
+
+#endif
diff --git a/src/lib/sendfile-util.h b/src/lib/sendfile-util.h
new file mode 100644
index 0000000..3d7f14a
--- /dev/null
+++ b/src/lib/sendfile-util.h
@@ -0,0 +1,16 @@
+#ifndef SENDFILE_UTIL_H
+#define SENDFILE_UTIL_H
+
+/* Wrapper for various sendfile()-like calls. Read a maximum of count bytes
+ from the given offset in in_fd and write them to out_fd. The offset is
+ updated after the call. Note the call assert-crashes if count is 0.
+
+ Returns:
+ >0 number of bytes successfully written (maybe less than count)
+ 0 if offset points to the input's EOF or past it
+ -1, errno=EINVAL if it isn't supported for some reason (out_fd isn't a
+ socket or there simply is no sendfile()).
+ -1, errno=EAGAIN if non-blocking write couldn't send anything */
+ssize_t safe_sendfile(int out_fd, int in_fd, uoff_t *offset, size_t count);
+
+#endif
diff --git a/src/lib/seq-range-array.c b/src/lib/seq-range-array.c
new file mode 100644
index 0000000..2d8dbfc
--- /dev/null
+++ b/src/lib/seq-range-array.c
@@ -0,0 +1,569 @@
+/* Copyright (c) 2005-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "seq-range-array.h"
+
+static bool seq_range_is_overflowed(const ARRAY_TYPE(seq_range) *array)
+{
+ const struct seq_range *range;
+ unsigned int count;
+
+ range = array_get(array, &count);
+ return count == 1 && range[0].seq1 == 0 &&
+ range[0].seq2 == (uint32_t)-1;
+}
+
+static bool ATTR_NOWARN_UNUSED_RESULT
+seq_range_lookup(const ARRAY_TYPE(seq_range) *array,
+ uint32_t seq, unsigned int *idx_r)
+{
+ const struct seq_range *data;
+ unsigned int idx, left_idx, right_idx, count;
+
+ data = array_get(array, &count);
+ i_assert(count < INT_MAX);
+
+ idx = 0; left_idx = 0; right_idx = count;
+ while (left_idx < right_idx) {
+ idx = (left_idx + right_idx) / 2;
+
+ if (data[idx].seq1 <= seq) {
+ if (data[idx].seq2 >= seq) {
+ /* it's already in the range */
+ *idx_r = idx;
+ return TRUE;
+ }
+ left_idx = idx+1;
+ } else {
+ right_idx = idx;
+ }
+ }
+ if (left_idx > idx)
+ idx++;
+ *idx_r = idx;
+ return FALSE;
+}
+
+static bool
+seq_range_array_add_slow_path(ARRAY_TYPE(seq_range) *array, uint32_t seq)
+{
+ struct seq_range *data, value;
+ unsigned int idx, count;
+
+ value.seq1 = value.seq2 = seq;
+ data = array_get_modifiable(array, &count);
+
+ /* somewhere in the middle, array is sorted so find it with
+ binary search */
+ if (seq_range_lookup(array, seq, &idx))
+ return TRUE;
+
+ /* idx == count couldn't happen because we already handle it above */
+ i_assert(idx < count && data[idx].seq1 >= seq);
+ i_assert(data[idx].seq1 > seq || data[idx].seq2 < seq);
+
+ if (data[idx].seq1 == seq+1) {
+ data[idx].seq1 = seq;
+ if (idx > 0 && data[idx-1].seq2 == seq-1) {
+ /* merge */
+ data[idx-1].seq2 = data[idx].seq2;
+ array_delete(array, idx, 1);
+ }
+ } else {
+ if (idx > 0 && data[idx-1].seq2 == seq-1)
+ idx--;
+ if (data[idx].seq2 == seq-1) {
+ i_assert(idx+1 < count); /* already handled above */
+ data[idx].seq2 = seq;
+ if (data[idx+1].seq1 == seq+1) {
+ /* merge */
+ data[idx+1].seq1 = data[idx].seq1;
+ array_delete(array, idx, 1);
+ }
+ } else {
+ array_insert(array, idx, &value, 1);
+ }
+ }
+ return FALSE;
+}
+
+bool seq_range_array_add(ARRAY_TYPE(seq_range) *array, uint32_t seq)
+{
+ struct seq_range *data, value;
+ unsigned int count;
+ bool exists = FALSE;
+
+ value.seq1 = value.seq2 = seq;
+
+ data = array_get_modifiable(array, &count);
+ /* quick checks */
+ if (count == 0)
+ array_push_back(array, &value);
+ else if (data[count-1].seq2 < seq) {
+ if (data[count-1].seq2 == seq-1) {
+ /* grow last range */
+ data[count-1].seq2 = seq;
+ } else {
+ array_push_back(array, &value);
+ }
+ } else if (data[0].seq1 > seq) {
+ if (data[0].seq1-1 == seq) {
+ /* grow down first range */
+ data[0].seq1 = seq;
+ } else {
+ array_push_front(array, &value);
+ }
+ } else {
+ exists = seq_range_array_add_slow_path(array, seq);
+ }
+ i_assert(!seq_range_is_overflowed(array));
+ return exists;
+}
+
+void seq_range_array_add_with_init(ARRAY_TYPE(seq_range) *array,
+ unsigned int init_count, uint32_t seq)
+{
+ if (!array_is_created(array))
+ i_array_init(array, init_count);
+ seq_range_array_add(array, seq);
+}
+
+static void
+seq_range_array_add_range_internal(ARRAY_TYPE(seq_range) *array,
+ uint32_t seq1, uint32_t seq2,
+ unsigned int *r_count)
+{
+ struct seq_range *data, value;
+ unsigned int idx1, idx2, count;
+
+ seq_range_lookup(array, seq1, &idx1);
+ seq_range_lookup(array, seq2, &idx2);
+
+ data = array_get_modifiable(array, &count);
+ if (r_count != NULL) {
+ /* Find number we're adding by counting the number we're
+ not adding, and subtracting that from the nominal range. */
+ unsigned int added = seq2+1 - seq1;
+ unsigned int countidx2 = idx2;
+ unsigned int overcounted = 0u, notadded = 0u;
+ unsigned int i;
+
+ if (idx1 == count) {
+ /* not in a range as too far right */
+ } else if (seq1 < data[idx1].seq1) {
+ /* not in a range, to the left of a real range */
+ } else {
+ /* count the whole of this range, which is an overcount */
+ overcounted += seq1 - data[idx1].seq1;
+ /* fencepost check: equality means the whole range is valid,
+ therefore there's no overcounting. Result = 0 overcount */
+ }
+ if (idx2 == count) {
+ /* not in a range as too far right */
+ } else if (seq2 < data[idx2].seq1) {
+ /* not in a range, to the left of a real range */
+ } else {
+ /* count the whole of this range, which is an overcount */
+ overcounted += data[idx2].seq2 - seq2;
+ countidx2++; /* may become == count i.e. past the end */
+ /* fencepost check: equality means the whole range is valid,
+ therefore there's no overcounting. Result = 0 overcount. */
+ }
+ /* Now count how many we're not adding */
+ for (i = idx1; i < countidx2; i++)
+ notadded += data[i].seq2+1 - data[i].seq1;
+ /* Maybe the not added tally included some over-counting too */
+ added -= (notadded - overcounted);
+ *r_count = added;
+ }
+
+ if (idx1 > 0 && data[idx1-1].seq2+1 == seq1)
+ idx1--;
+
+ if (idx1 == idx2 &&
+ (idx2 == count || (seq2 < (uint32_t)-1 && data[idx2].seq1 > seq2+1)) &&
+ (idx1 == 0 || data[idx1-1].seq2 < seq1-1)) {
+ /* no overlapping */
+ value.seq1 = seq1;
+ value.seq2 = seq2;
+ array_insert(array, idx1, &value, 1);
+ } else {
+ i_assert(idx1 < count);
+ if (seq1 < data[idx1].seq1)
+ data[idx1].seq1 = seq1;
+ if (seq2 > data[idx1].seq2) {
+ /* merge */
+ if (idx2 == count ||
+ data[idx2].seq1 > seq2+1)
+ idx2--;
+ if (seq2 >= data[idx2].seq2) {
+ data[idx1].seq2 = seq2;
+ } else {
+ data[idx1].seq2 = data[idx2].seq2;
+ }
+ array_delete(array, idx1 + 1, idx2 - idx1);
+ }
+ }
+ i_assert(!seq_range_is_overflowed(array));
+}
+
+void seq_range_array_add_range(ARRAY_TYPE(seq_range) *array,
+ uint32_t seq1, uint32_t seq2)
+{
+ seq_range_array_add_range_internal(array, seq1, seq2, NULL);
+}
+unsigned int seq_range_array_add_range_count(ARRAY_TYPE(seq_range) *array,
+ uint32_t seq1, uint32_t seq2)
+{
+ unsigned int count;
+ seq_range_array_add_range_internal(array, seq1, seq2, &count);
+ return count;
+}
+
+void seq_range_array_merge(ARRAY_TYPE(seq_range) *dest,
+ const ARRAY_TYPE(seq_range) *src)
+{
+ const struct seq_range *range;
+
+ if (array_count(dest) == 0) {
+ array_append_array(dest, src);
+ return;
+ }
+
+ array_foreach(src, range)
+ seq_range_array_add_range(dest, range->seq1, range->seq2);
+}
+
+void seq_range_array_merge_n(ARRAY_TYPE(seq_range) *dest,
+ const ARRAY_TYPE(seq_range) *src,
+ unsigned int count)
+{
+ const struct seq_range *src_range;
+ unsigned int src_idx, src_count;
+ unsigned int merge_count = count;
+
+ src_range = array_get(src, &src_count);
+ for (src_idx = 0; src_idx < src_count && merge_count > 0; src_idx++) {
+ uint32_t first_seq = src_range[src_idx].seq1;
+ uint32_t last_seq = src_range[src_idx].seq2;
+ unsigned int idx_count = last_seq - first_seq + 1;
+
+ if (idx_count > merge_count) {
+ last_seq = first_seq + merge_count - 1;
+ merge_count = 0;
+ } else {
+ merge_count -= idx_count;
+ }
+ seq_range_array_add_range(dest, first_seq, last_seq);
+ }
+}
+
+bool seq_range_array_remove(ARRAY_TYPE(seq_range) *array, uint32_t seq)
+{
+ struct seq_range *data, value;
+ unsigned int idx, left_idx, right_idx, count;
+
+ if (!array_is_created(array))
+ return FALSE;
+
+ data = array_get_modifiable(array, &count);
+ if (count == 0)
+ return FALSE;
+
+ /* quick checks */
+ if (seq > data[count-1].seq2 || seq < data[0].seq1) {
+ /* outside the range */
+ return FALSE;
+ }
+ if (data[count-1].seq2 == seq) {
+ /* shrink last range */
+ if (data[count-1].seq1 != data[count-1].seq2)
+ data[count-1].seq2--;
+ else
+ array_delete(array, count-1, 1);
+ return TRUE;
+ }
+ if (data[0].seq1 == seq) {
+ /* shrink up first range */
+ if (data[0].seq1 != data[0].seq2)
+ data[0].seq1++;
+ else
+ array_pop_front(array);
+ return TRUE;
+ }
+
+ /* somewhere in the middle, array is sorted so find it with
+ binary search */
+ i_assert(count < INT_MAX);
+ left_idx = 0; right_idx = count;
+ while (left_idx < right_idx) {
+ idx = (left_idx + right_idx) / 2;
+
+ if (data[idx].seq1 > seq)
+ right_idx = idx;
+ else if (data[idx].seq2 < seq)
+ left_idx = idx+1;
+ else {
+ /* found it */
+ if (data[idx].seq1 == seq) {
+ if (data[idx].seq1 == data[idx].seq2) {
+ /* a single sequence range.
+ remove it entirely */
+ array_delete(array, idx, 1);
+ } else {
+ /* shrink the range */
+ data[idx].seq1++;
+ }
+ } else if (data[idx].seq2 == seq) {
+ /* shrink the range */
+ data[idx].seq2--;
+ } else {
+ /* split the sequence range */
+ value.seq1 = seq + 1;
+ value.seq2 = data[idx].seq2;
+ data[idx].seq2 = seq - 1;
+
+ array_insert(array, idx + 1, &value, 1);
+ }
+ return TRUE;
+ }
+ }
+ return FALSE;
+}
+
+unsigned int seq_range_array_remove_range(ARRAY_TYPE(seq_range) *array,
+ uint32_t seq1, uint32_t seq2)
+{
+ const struct seq_range *data;
+ unsigned int idx, idx2, count, remove_count = 0;
+
+ /* remove first and last. this makes sure that everything between
+ can simply be deleted with array_delete().
+
+ FIXME: it would be faster if we did only one binary lookup here
+ and handled the splitting ourself.. */
+ if (seq_range_array_remove(array, seq1))
+ remove_count++;
+ if (seq1 == seq2)
+ return remove_count;
+ seq1++;
+
+ if (seq_range_array_remove(array, seq2--))
+ remove_count++;
+ if (seq1 > seq2)
+ return remove_count;
+
+ /* find the beginning */
+ data = array_get(array, &count);
+ seq_range_lookup(array, seq1, &idx);
+
+ if (idx == count)
+ return remove_count;
+
+ i_assert(data[idx].seq1 >= seq1);
+ for (idx2 = idx; idx2 < count; idx2++) {
+ if (data[idx2].seq1 > seq2)
+ break;
+ i_assert(UINT_MAX - remove_count >= seq_range_length(&data[idx2]));
+ remove_count += seq_range_length(&data[idx2]);
+ }
+ array_delete(array, idx, idx2-idx);
+ return remove_count;
+}
+
+unsigned int seq_range_array_remove_seq_range(ARRAY_TYPE(seq_range) *dest,
+ const ARRAY_TYPE(seq_range) *src)
+{
+ unsigned int count, full_count = 0;
+ const struct seq_range *src_range;
+
+ array_foreach(src, src_range) {
+ count = seq_range_array_remove_range(dest, src_range->seq1,
+ src_range->seq2);
+ i_assert(UINT_MAX - full_count >= count);
+ full_count += count;
+ }
+ return full_count;
+}
+
+void seq_range_array_remove_nth(ARRAY_TYPE(seq_range) *array,
+ uint32_t n, uint32_t count)
+{
+ struct seq_range_iter iter;
+ uint32_t seq1, seq2;
+
+ if (count == 0)
+ return;
+
+ seq_range_array_iter_init(&iter, array);
+ if (!seq_range_array_iter_nth(&iter, n, &seq1)) {
+ /* n points beyond array */
+ return;
+ }
+ if (count-1 >= (uint32_t)-1 - n ||
+ !seq_range_array_iter_nth(&iter, n + (count-1), &seq2)) {
+ /* count points beyond array */
+ seq2 = (uint32_t)-1;
+ }
+ seq_range_array_remove_range(array, seq1, seq2);
+}
+
+unsigned int seq_range_array_intersect(ARRAY_TYPE(seq_range) *dest,
+ const ARRAY_TYPE(seq_range) *src)
+{
+ const struct seq_range *src_range;
+ unsigned int i, count, remove_count, full_count = 0;
+ uint32_t last_seq = 0;
+
+ src_range = array_get(src, &count);
+ for (i = 0; i < count; i++) {
+ if (last_seq + 1 < src_range[i].seq1) {
+ remove_count = seq_range_array_remove_range(dest,
+ last_seq + 1, src_range[i].seq1 - 1);
+ i_assert(UINT_MAX - full_count >= remove_count);
+ full_count += remove_count;
+ }
+ last_seq = src_range[i].seq2;
+ }
+ if (last_seq != (uint32_t)-1) {
+ remove_count = seq_range_array_remove_range(dest, last_seq + 1,
+ (uint32_t)-1);
+ i_assert(UINT_MAX - full_count >= remove_count);
+ full_count += remove_count;
+ }
+ return full_count;
+}
+
+bool seq_range_exists(const ARRAY_TYPE(seq_range) *array, uint32_t seq)
+{
+ unsigned int idx;
+
+ return seq_range_lookup(array, seq, &idx);
+}
+
+bool seq_range_array_have_common(const ARRAY_TYPE(seq_range) *array1,
+ const ARRAY_TYPE(seq_range) *array2)
+{
+ const struct seq_range *range1, *range2;
+ unsigned int i1, i2, count1, count2;
+
+ range1 = array_get(array1, &count1);
+ range2 = array_get(array2, &count2);
+ for (i1 = i2 = 0; i1 < count1 && i2 < count2; ) {
+ if (range1[i1].seq1 <= range2[i2].seq2 &&
+ range1[i1].seq2 >= range2[i2].seq1)
+ return TRUE;
+
+ if (range1[i1].seq1 < range2[i2].seq1)
+ i1++;
+ else
+ i2++;
+ }
+ return FALSE;
+}
+
+unsigned int seq_range_count(const ARRAY_TYPE(seq_range) *array)
+{
+ const struct seq_range *range;
+ unsigned int seq_count = 0;
+
+ array_foreach(array, range) {
+ i_assert(UINT_MAX - seq_count >= seq_range_length(range));
+ seq_count += seq_range_length(range);
+ }
+ return seq_count;
+}
+
+void seq_range_array_invert(ARRAY_TYPE(seq_range) *array,
+ uint32_t min_seq, uint32_t max_seq)
+{
+ struct seq_range *range, value;
+ unsigned int i, count;
+ uint32_t prev_min_seq;
+
+ if (array_is_created(array))
+ range = array_get_modifiable(array, &count);
+ else {
+ range = NULL;
+ count = 0;
+ }
+ if (count == 0) {
+ /* empty -> full */
+ if (!array_is_created(array))
+ i_array_init(array, 4);
+ value.seq1 = min_seq;
+ value.seq2 = max_seq;
+ array_push_back(array, &value);
+ return;
+ }
+ i_assert(range[0].seq1 >= min_seq);
+ i_assert(range[count-1].seq2 <= max_seq);
+
+ if (range[0].seq1 == min_seq && range[0].seq2 == max_seq) {
+ /* full -> empty */
+ array_clear(array);
+ return;
+ }
+
+ for (i = 0; i < count; ) {
+ prev_min_seq = min_seq;
+ min_seq = range[i].seq2;
+ if (range[i].seq1 == prev_min_seq) {
+ array_delete(array, i, 1);
+ range = array_get_modifiable(array, &count);
+ } else {
+ range[i].seq2 = range[i].seq1 - 1;
+ range[i].seq1 = prev_min_seq;
+ i++;
+ }
+ if (min_seq >= max_seq) {
+ /* max_seq is reached. the rest of the array should be
+ empty. we'll return here, because min_seq++ may
+ wrap to 0. */
+ i_assert(min_seq == max_seq);
+ i_assert(i == count);
+ return;
+ }
+ min_seq++;
+ }
+ if (min_seq <= max_seq) {
+ value.seq1 = min_seq;
+ value.seq2 = max_seq;
+ array_push_back(array, &value);
+ }
+}
+
+void seq_range_array_iter_init(struct seq_range_iter *iter_r,
+ const ARRAY_TYPE(seq_range) *array)
+{
+ i_zero(iter_r);
+ iter_r->array = array;
+}
+
+bool seq_range_array_iter_nth(struct seq_range_iter *iter, unsigned int n,
+ uint32_t *seq_r)
+{
+ const struct seq_range *range;
+ unsigned int i, count, diff;
+
+ if (n < iter->prev_n) {
+ /* iterating backwards, don't bother optimizing */
+ iter->prev_n = 0;
+ iter->prev_idx = 0;
+ }
+
+ range = array_get(iter->array, &count);
+ for (i = iter->prev_idx; i < count; i++) {
+ diff = range[i].seq2 - range[i].seq1;
+ if (n <= iter->prev_n + diff) {
+ i_assert(n >= iter->prev_n);
+ *seq_r = range[i].seq1 + (n - iter->prev_n);
+ iter->prev_idx = i;
+ return TRUE;
+ }
+ iter->prev_n += diff + 1;
+ }
+ iter->prev_idx = i;
+ return FALSE;
+}
diff --git a/src/lib/seq-range-array.h b/src/lib/seq-range-array.h
new file mode 100644
index 0000000..e323ead
--- /dev/null
+++ b/src/lib/seq-range-array.h
@@ -0,0 +1,82 @@
+#ifndef SEQ_RANGE_ARRAY_H
+#define SEQ_RANGE_ARRAY_H
+
+/* NOTE: A full 0..UINT_MAX sequence range isn't valid to use here, because its
+ size would become UINT_MAX+1, which can't be returned by e.g.
+ seq_range_count() and similar functions. Attempting to use such sequence
+ ranges will result in assert-crash. */
+
+struct seq_range {
+ uint32_t seq1, seq2;
+};
+ARRAY_DEFINE_TYPE(seq_range, struct seq_range);
+
+struct seq_range_iter {
+ const ARRAY_TYPE(seq_range) *array;
+ unsigned int prev_n, prev_idx;
+};
+
+static inline uint32_t ATTR_PURE seq_range_length(const struct seq_range *range)
+{
+ i_assert(range->seq2 >= range->seq1);
+ i_assert(range->seq1 > 0 || range->seq2 < (uint32_t)-1);
+ return range->seq2 - range->seq1 + 1;
+}
+
+/* Add sequence to range. If the array isn't created yet, create it with
+ initial size of init_count. */
+bool ATTR_NOWARN_UNUSED_RESULT
+seq_range_array_add(ARRAY_TYPE(seq_range) *array, uint32_t seq);
+/* Like seq_range_array_add(), but if the array isn't already initialized do
+ it with i_array_init(). */
+void seq_range_array_add_with_init(ARRAY_TYPE(seq_range) *array,
+ unsigned int init_count, uint32_t seq);
+void seq_range_array_add_range(ARRAY_TYPE(seq_range) *array,
+ uint32_t seq1, uint32_t seq2);
+unsigned int seq_range_array_add_range_count(ARRAY_TYPE(seq_range) *array,
+ uint32_t seq1, uint32_t seq2);
+void seq_range_array_merge(ARRAY_TYPE(seq_range) *dest,
+ const ARRAY_TYPE(seq_range) *src);
+/* Merge the first n sequences from src into dest. */
+void seq_range_array_merge_n(ARRAY_TYPE(seq_range) *dest,
+ const ARRAY_TYPE(seq_range) *src,
+ unsigned int count);
+/* Remove the given sequence from range. Returns TRUE if it was found. */
+bool ATTR_NOWARN_UNUSED_RESULT
+seq_range_array_remove(ARRAY_TYPE(seq_range) *array, uint32_t seq);
+/* Remove a sequence range. Returns number of sequences actually removed. */
+unsigned int ATTR_NOWARN_UNUSED_RESULT
+seq_range_array_remove_range(ARRAY_TYPE(seq_range) *array,
+ uint32_t seq1, uint32_t seq2);
+unsigned int ATTR_NOWARN_UNUSED_RESULT
+seq_range_array_remove_seq_range(ARRAY_TYPE(seq_range) *dest,
+ const ARRAY_TYPE(seq_range) *src);
+/* Remove count number of sequences from the nth sequence (0 = first). */
+void seq_range_array_remove_nth(ARRAY_TYPE(seq_range) *array,
+ uint32_t n, uint32_t count);
+/* Remove sequences from dest that don't exist in src. */
+unsigned int ATTR_NOWARN_UNUSED_RESULT
+seq_range_array_intersect(ARRAY_TYPE(seq_range) *dest,
+ const ARRAY_TYPE(seq_range) *src);
+/* Returns TRUE if sequence exists in the range. */
+bool seq_range_exists(const ARRAY_TYPE(seq_range) *array,
+ uint32_t seq) ATTR_PURE;
+/* Returns TRUE if arrays have common sequences. */
+bool seq_range_array_have_common(const ARRAY_TYPE(seq_range) *array1,
+ const ARRAY_TYPE(seq_range) *array2) ATTR_PURE;
+/* Return number of sequences in the range. */
+unsigned int seq_range_count(const ARRAY_TYPE(seq_range) *array) ATTR_PURE;
+
+/* Invert the sequence range. For example 5:6 -> min_seq:4,7:max_seq.
+ The array must not have any sequences outside min_seq..max_seq or this
+ function will assert-crash. */
+void seq_range_array_invert(ARRAY_TYPE(seq_range) *array,
+ uint32_t min_seq, uint32_t max_seq);
+
+void seq_range_array_iter_init(struct seq_range_iter *iter_r,
+ const ARRAY_TYPE(seq_range) *array);
+/* Get the nth sequence (0 = first). Returns FALSE if idx is too large. */
+bool seq_range_array_iter_nth(struct seq_range_iter *iter, unsigned int n,
+ uint32_t *seq_r);
+
+#endif
diff --git a/src/lib/seq-set-builder.c b/src/lib/seq-set-builder.c
new file mode 100644
index 0000000..b5649b7
--- /dev/null
+++ b/src/lib/seq-set-builder.c
@@ -0,0 +1,113 @@
+/* Copyright (c) 2021 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "str.h"
+#include "seq-set-builder.h"
+
+struct seqset_builder {
+ string_t *str;
+ uint32_t last_seq;
+ size_t last_seq_pos;
+ size_t prefix_length;
+};
+
+struct seqset_builder *seqset_builder_init(string_t *str)
+{
+ struct seqset_builder *builder;
+ builder = i_new(struct seqset_builder, 1);
+ builder->str = str;
+ builder->last_seq = 0;
+ builder->prefix_length = str_len(str);
+ builder->last_seq_pos = 0;
+ return builder;
+}
+
+static void
+seqset_builder_append_one(struct seqset_builder *builder, uint32_t seq)
+{
+ builder->last_seq_pos = str_len(builder->str)+1;
+ str_printfa(builder->str, "%u,", seq);
+}
+
+static void
+seqset_builder_create_or_merge_range(struct seqset_builder *builder,
+ uint32_t seq)
+{
+ char delimiter = '\0';
+
+ i_assert(builder->last_seq_pos > builder->prefix_length);
+
+ str_truncate(builder->str, builder->last_seq_pos-1);
+
+ /* Get the delimiter from the builder string */
+ if (str_len(builder->str) > 0 &&
+ str_len(builder->str) - 1 > builder->prefix_length)
+ delimiter = str_data(builder->str)[str_len(builder->str) - 1];
+
+ if (delimiter == ':') {
+ seqset_builder_append_one(builder, seq);
+ } else if (delimiter == ',' || delimiter == '\0') {
+ str_printfa(builder->str, "%u:", builder->last_seq);
+ builder->last_seq_pos = str_len(builder->str) + 1;
+ str_printfa(builder->str, "%u,", seq);
+ } else
+ i_unreached();
+ return;
+}
+
+void seqset_builder_add(struct seqset_builder *builder, uint32_t seq)
+{
+ if (builder->last_seq == 0) {
+ /* No seq was yet appened so just append this one */
+ seqset_builder_append_one(builder, seq);
+ } else if (builder->last_seq + 1 == seq) {
+ /* This seq is following directly on the previous one
+ try to create a range of seqs */
+ seqset_builder_create_or_merge_range(builder, seq);
+ } else {
+ /* Append this seq without creating a range */
+ seqset_builder_append_one(builder, seq);
+ }
+ builder->last_seq = seq;
+}
+
+bool seqset_builder_try_add(struct seqset_builder *builder, size_t max_len,
+ uint32_t seq)
+{
+ /* Length of this sequence to be appended */
+ unsigned int seq_len = 0;
+ /* Buffer to use when calculating seq length as string */
+ char seq_str[MAX_INT_STRLEN];
+ /* Current length of the seq string */
+ unsigned int builder_str_len = str_len(builder->str);
+
+ if (builder->last_seq + 1 == seq && builder_str_len + 1 <= max_len) {
+ /* Following sequence: This seq can't grow the overall length
+ by more than one. */
+ seqset_builder_add(builder, seq);
+ return TRUE;
+ }
+
+ if (builder_str_len + MAX_INT_STRLEN + 1 <= max_len) {
+ /* Appending the maximum length of a sequence number and ','
+ still fits into max_len. There is no need to check the
+ actual length. */
+ seqset_builder_add(builder, seq);
+ return TRUE;
+ }
+
+ seq_len = strlen(dec2str_buf(seq_str, seq)) + 1;
+ if (seq_len + builder_str_len > max_len)
+ return FALSE;
+
+ seqset_builder_add(builder, seq);
+ return TRUE;
+}
+
+void seqset_builder_deinit(struct seqset_builder **builder)
+{
+ /* If anything was appened to the string remove the trailing ',' */
+ if ((*builder)->last_seq != 0)
+ str_truncate((*builder)->str, str_len((*builder)->str) - 1);
+ i_free(*builder);
+}
diff --git a/src/lib/seq-set-builder.h b/src/lib/seq-set-builder.h
new file mode 100644
index 0000000..37d9cbc
--- /dev/null
+++ b/src/lib/seq-set-builder.h
@@ -0,0 +1,15 @@
+#ifndef SEQ_SET_BUILDER_H
+#define SEQ_SET_BUILDER_H
+
+/* Append a seqset to the given string. */
+struct seqset_builder *seqset_builder_init(string_t *str);
+/* Add seq to the string. The string must not have been modified before the previous
+ seqset_builder_add() call, since the last sequence in it may be rewritten. */
+void seqset_builder_add(struct seqset_builder *builder, uint32_t seq);
+/* Add the seq to the string, but only if the string length stays below max_len.
+ Returns TRUE if added, FALSE if not. */
+bool seqset_builder_try_add(struct seqset_builder *builder, size_t max_len, uint32_t seq);
+/* Deinitialize the builder */
+void seqset_builder_deinit(struct seqset_builder **builder);
+
+#endif
diff --git a/src/lib/sha-common.h b/src/lib/sha-common.h
new file mode 100644
index 0000000..abc63ae
--- /dev/null
+++ b/src/lib/sha-common.h
@@ -0,0 +1,12 @@
+#ifndef SHA_COMMON
+
+#define SHA256_RESULTLEN (256 / 8)
+#define SHA256_BLOCK_SIZE (512 / 8)
+
+#define SHA384_RESULTLEN (384 / 8)
+#define SHA384_BLOCK_SIZE (1024 / 8)
+
+#define SHA512_RESULTLEN (512 / 8)
+#define SHA512_BLOCK_SIZE (1024 / 8)
+
+#endif
diff --git a/src/lib/sha1.c b/src/lib/sha1.c
new file mode 100644
index 0000000..b647a91
--- /dev/null
+++ b/src/lib/sha1.c
@@ -0,0 +1,288 @@
+/* $KAME: sha1.c,v 1.5 2000/11/08 06:13:08 itojun Exp $ */
+/*
+ * Copyright (C) 1995, 1996, 1997, and 1998 WIDE Project.
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ * 3. Neither the name of the project nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE PROJECT AND CONTRIBUTORS ``AS IS'' AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE PROJECT OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+ * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+ * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+ * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+ * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+ * SUCH DAMAGE.
+ */
+
+/*
+ * FIPS pub 180-1: Secure Hash Algorithm (SHA-1)
+ * based on: http://csrc.nist.gov/fips/fip180-1.txt
+ * implemented by Jun-ichiro itojun Itoh <itojun@itojun.org>
+ */
+
+#include "lib.h"
+#include "sha1.h"
+#include "safe-memset.h"
+
+/* constant table */
+static uint32_t SHA1_K[] = { 0x5a827999, 0x6ed9eba1, 0x8f1bbcdc, 0xca62c1d6 };
+#define K(t) SHA1_K[(t) / 20]
+
+#define F0(b, c, d) (((b) & (c)) | ((~(b)) & (d)))
+#define F1(b, c, d) (((b) ^ (c)) ^ (d))
+#define F2(b, c, d) (((b) & (c)) | ((b) & (d)) | ((c) & (d)))
+#define F3(b, c, d) (((b) ^ (c)) ^ (d))
+
+#define S(n, x) (((x) << (n)) | ((x) >> (32 - n)))
+
+#define H(n) (ctxt->h.b32[(n)])
+#define COUNT (ctxt->count)
+#define BCOUNT (ctxt->c.b64[0] / 8)
+#define W(n) (ctxt->m.b32[(n)])
+
+#define PUTBYTE(x) { \
+ ctxt->m.b8[(COUNT % 64)] = (x); \
+ COUNT++; \
+ COUNT %= 64; \
+ ctxt->c.b64[0] += 8; \
+ if (COUNT % 64 == 0) \
+ sha1_step(ctxt); \
+ }
+
+#define PUTPAD(x) { \
+ ctxt->m.b8[(COUNT % 64)] = (x); \
+ COUNT++; \
+ COUNT %= 64; \
+ if (COUNT % 64 == 0) \
+ sha1_step(ctxt); \
+ }
+
+static void sha1_step(struct sha1_ctxt *);
+
+static void ATTR_UNSIGNED_WRAPS
+sha1_step(struct sha1_ctxt *ctxt)
+{
+ uint32_t a, b, c, d, e;
+ size_t t, s;
+ uint32_t tmp;
+
+#ifndef WORDS_BIGENDIAN
+ struct sha1_ctxt tctxt;
+ memmove(&tctxt.m.b8[0], &ctxt->m.b8[0], 64);
+ ctxt->m.b8[0] = tctxt.m.b8[3]; ctxt->m.b8[1] = tctxt.m.b8[2];
+ ctxt->m.b8[2] = tctxt.m.b8[1]; ctxt->m.b8[3] = tctxt.m.b8[0];
+ ctxt->m.b8[4] = tctxt.m.b8[7]; ctxt->m.b8[5] = tctxt.m.b8[6];
+ ctxt->m.b8[6] = tctxt.m.b8[5]; ctxt->m.b8[7] = tctxt.m.b8[4];
+ ctxt->m.b8[8] = tctxt.m.b8[11]; ctxt->m.b8[9] = tctxt.m.b8[10];
+ ctxt->m.b8[10] = tctxt.m.b8[9]; ctxt->m.b8[11] = tctxt.m.b8[8];
+ ctxt->m.b8[12] = tctxt.m.b8[15]; ctxt->m.b8[13] = tctxt.m.b8[14];
+ ctxt->m.b8[14] = tctxt.m.b8[13]; ctxt->m.b8[15] = tctxt.m.b8[12];
+ ctxt->m.b8[16] = tctxt.m.b8[19]; ctxt->m.b8[17] = tctxt.m.b8[18];
+ ctxt->m.b8[18] = tctxt.m.b8[17]; ctxt->m.b8[19] = tctxt.m.b8[16];
+ ctxt->m.b8[20] = tctxt.m.b8[23]; ctxt->m.b8[21] = tctxt.m.b8[22];
+ ctxt->m.b8[22] = tctxt.m.b8[21]; ctxt->m.b8[23] = tctxt.m.b8[20];
+ ctxt->m.b8[24] = tctxt.m.b8[27]; ctxt->m.b8[25] = tctxt.m.b8[26];
+ ctxt->m.b8[26] = tctxt.m.b8[25]; ctxt->m.b8[27] = tctxt.m.b8[24];
+ ctxt->m.b8[28] = tctxt.m.b8[31]; ctxt->m.b8[29] = tctxt.m.b8[30];
+ ctxt->m.b8[30] = tctxt.m.b8[29]; ctxt->m.b8[31] = tctxt.m.b8[28];
+ ctxt->m.b8[32] = tctxt.m.b8[35]; ctxt->m.b8[33] = tctxt.m.b8[34];
+ ctxt->m.b8[34] = tctxt.m.b8[33]; ctxt->m.b8[35] = tctxt.m.b8[32];
+ ctxt->m.b8[36] = tctxt.m.b8[39]; ctxt->m.b8[37] = tctxt.m.b8[38];
+ ctxt->m.b8[38] = tctxt.m.b8[37]; ctxt->m.b8[39] = tctxt.m.b8[36];
+ ctxt->m.b8[40] = tctxt.m.b8[43]; ctxt->m.b8[41] = tctxt.m.b8[42];
+ ctxt->m.b8[42] = tctxt.m.b8[41]; ctxt->m.b8[43] = tctxt.m.b8[40];
+ ctxt->m.b8[44] = tctxt.m.b8[47]; ctxt->m.b8[45] = tctxt.m.b8[46];
+ ctxt->m.b8[46] = tctxt.m.b8[45]; ctxt->m.b8[47] = tctxt.m.b8[44];
+ ctxt->m.b8[48] = tctxt.m.b8[51]; ctxt->m.b8[49] = tctxt.m.b8[50];
+ ctxt->m.b8[50] = tctxt.m.b8[49]; ctxt->m.b8[51] = tctxt.m.b8[48];
+ ctxt->m.b8[52] = tctxt.m.b8[55]; ctxt->m.b8[53] = tctxt.m.b8[54];
+ ctxt->m.b8[54] = tctxt.m.b8[53]; ctxt->m.b8[55] = tctxt.m.b8[52];
+ ctxt->m.b8[56] = tctxt.m.b8[59]; ctxt->m.b8[57] = tctxt.m.b8[58];
+ ctxt->m.b8[58] = tctxt.m.b8[57]; ctxt->m.b8[59] = tctxt.m.b8[56];
+ ctxt->m.b8[60] = tctxt.m.b8[63]; ctxt->m.b8[61] = tctxt.m.b8[62];
+ ctxt->m.b8[62] = tctxt.m.b8[61]; ctxt->m.b8[63] = tctxt.m.b8[60];
+#endif
+
+ a = H(0); b = H(1); c = H(2); d = H(3); e = H(4);
+
+ for (t = 0; t < 20; t++) {
+ s = t & 0x0f;
+ if (t >= 16) {
+ W(s) = S(1, W((s+13) & 0x0f) ^ W((s+8) & 0x0f) ^ W((s+2) & 0x0f) ^ W(s));
+ }
+ tmp = S(5, a) + F0(b, c, d) + e + W(s) + K(t);
+ e = d; d = c; c = S(30, b); b = a; a = tmp;
+ }
+ for (t = 20; t < 40; t++) {
+ s = t & 0x0f;
+ W(s) = S(1, W((s+13) & 0x0f) ^ W((s+8) & 0x0f) ^ W((s+2) & 0x0f) ^ W(s));
+ tmp = S(5, a) + F1(b, c, d) + e + W(s) + K(t);
+ e = d; d = c; c = S(30, b); b = a; a = tmp;
+ }
+ for (t = 40; t < 60; t++) {
+ s = t & 0x0f;
+ W(s) = S(1, W((s+13) & 0x0f) ^ W((s+8) & 0x0f) ^ W((s+2) & 0x0f) ^ W(s));
+ tmp = S(5, a) + F2(b, c, d) + e + W(s) + K(t);
+ e = d; d = c; c = S(30, b); b = a; a = tmp;
+ }
+ for (t = 60; t < 80; t++) {
+ s = t & 0x0f;
+ W(s) = S(1, W((s+13) & 0x0f) ^ W((s+8) & 0x0f) ^ W((s+2) & 0x0f) ^ W(s));
+ tmp = S(5, a) + F3(b, c, d) + e + W(s) + K(t);
+ e = d; d = c; c = S(30, b); b = a; a = tmp;
+ }
+
+ H(0) = H(0) + a;
+ H(1) = H(1) + b;
+ H(2) = H(2) + c;
+ H(3) = H(3) + d;
+ H(4) = H(4) + e;
+
+ memset(&ctxt->m.b8[0], 0, 64);
+}
+
+/*------------------------------------------------------------*/
+
+void
+sha1_init(struct sha1_ctxt *ctxt)
+{
+ memset(ctxt, 0, sizeof(struct sha1_ctxt));
+ H(0) = 0x67452301;
+ H(1) = 0xefcdab89;
+ H(2) = 0x98badcfe;
+ H(3) = 0x10325476;
+ H(4) = 0xc3d2e1f0;
+}
+
+void
+sha1_pad(struct sha1_ctxt *ctxt)
+{
+ size_t padlen; /*pad length in bytes*/
+ size_t padstart;
+
+ PUTPAD(0x80);
+
+ padstart = COUNT % 64;
+ padlen = 64 - padstart;
+ if (padlen < 8) {
+ memset(&ctxt->m.b8[padstart], 0, padlen);
+ COUNT += padlen;
+ COUNT %= 64;
+ sha1_step(ctxt);
+ padstart = COUNT % 64; /* should be 0 */
+ padlen = 64 - padstart; /* should be 64 */
+ }
+ memset(&ctxt->m.b8[padstart], 0, padlen - 8);
+ COUNT += (padlen - 8);
+ COUNT %= 64;
+#ifdef WORDS_BIGENDIAN
+ PUTPAD(ctxt->c.b8[0]); PUTPAD(ctxt->c.b8[1]);
+ PUTPAD(ctxt->c.b8[2]); PUTPAD(ctxt->c.b8[3]);
+ PUTPAD(ctxt->c.b8[4]); PUTPAD(ctxt->c.b8[5]);
+ PUTPAD(ctxt->c.b8[6]); PUTPAD(ctxt->c.b8[7]);
+#else
+ PUTPAD(ctxt->c.b8[7]); PUTPAD(ctxt->c.b8[6]);
+ PUTPAD(ctxt->c.b8[5]); PUTPAD(ctxt->c.b8[4]);
+ PUTPAD(ctxt->c.b8[3]); PUTPAD(ctxt->c.b8[2]);
+ PUTPAD(ctxt->c.b8[1]); PUTPAD(ctxt->c.b8[0]);
+#endif
+}
+
+void
+sha1_loop(struct sha1_ctxt *ctxt, const void *input, size_t len)
+{
+ const unsigned char *input_c = input;
+ size_t gaplen;
+ size_t gapstart;
+ size_t off;
+ size_t copysiz;
+
+ off = 0;
+
+ while (off < len) {
+ gapstart = COUNT % 64;
+ gaplen = 64 - gapstart;
+
+ copysiz = (gaplen < len - off) ? gaplen : len - off;
+ memmove(&ctxt->m.b8[gapstart], &input_c[off], copysiz);
+ COUNT += copysiz;
+ COUNT %= 64;
+ ctxt->c.b64[0] += copysiz * 8;
+ if (COUNT % 64 == 0)
+ sha1_step(ctxt);
+ off += copysiz;
+ }
+}
+
+void
+sha1_result(struct sha1_ctxt *ctxt, void *digest0)
+{
+ uint8_t *digest;
+
+ digest = (uint8_t *)digest0;
+ sha1_pad(ctxt);
+#ifdef WORDS_BIGENDIAN
+ memmove(digest, &ctxt->h.b8[0], 20);
+#else
+ digest[0] = ctxt->h.b8[3]; digest[1] = ctxt->h.b8[2];
+ digest[2] = ctxt->h.b8[1]; digest[3] = ctxt->h.b8[0];
+ digest[4] = ctxt->h.b8[7]; digest[5] = ctxt->h.b8[6];
+ digest[6] = ctxt->h.b8[5]; digest[7] = ctxt->h.b8[4];
+ digest[8] = ctxt->h.b8[11]; digest[9] = ctxt->h.b8[10];
+ digest[10] = ctxt->h.b8[9]; digest[11] = ctxt->h.b8[8];
+ digest[12] = ctxt->h.b8[15]; digest[13] = ctxt->h.b8[14];
+ digest[14] = ctxt->h.b8[13]; digest[15] = ctxt->h.b8[12];
+ digest[16] = ctxt->h.b8[19]; digest[17] = ctxt->h.b8[18];
+ digest[18] = ctxt->h.b8[17]; digest[19] = ctxt->h.b8[16];
+#endif
+ safe_memset(ctxt, 0, sizeof(struct sha1_ctxt));
+}
+
+void sha1_get_digest(const void *data, size_t size,
+ unsigned char result[STATIC_ARRAY SHA1_RESULTLEN])
+{
+ struct sha1_ctxt ctx;
+
+ sha1_init(&ctx);
+ sha1_loop(&ctx, data, size);
+ sha1_result(&ctx, result);
+}
+
+static void hash_method_init_sha1(void *context)
+{
+ sha1_init(context);
+}
+static void hash_method_loop_sha1(void *context, const void *data, size_t size)
+{
+ sha1_loop(context, data, size);
+}
+
+static void hash_method_result_sha1(void *context, unsigned char *result_r)
+{
+ sha1_result(context, result_r);
+}
+
+const struct hash_method hash_method_sha1 = {
+ .name = "sha1",
+ .block_size = 64, /* block size is 512 bits */
+ .context_size = sizeof(struct sha1_ctxt),
+ .digest_size = SHA1_RESULTLEN,
+
+ .init = hash_method_init_sha1,
+ .loop = hash_method_loop_sha1,
+ .result = hash_method_result_sha1,
+};
diff --git a/src/lib/sha1.h b/src/lib/sha1.h
new file mode 100644
index 0000000..c8e1e67
--- /dev/null
+++ b/src/lib/sha1.h
@@ -0,0 +1,84 @@
+/* $FreeBSD: src/sys/crypto/sha1.h,v 1.8 2002/03/20 05:13:50 alfred Exp $ */
+/* $KAME: sha1.h,v 1.5 2000/03/27 04:36:23 sumikawa Exp $ */
+
+/*
+ * Copyright (C) 1995, 1996, 1997, and 1998 WIDE Project.
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ * 3. Neither the name of the project nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE PROJECT AND CONTRIBUTORS ``AS IS'' AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE PROJECT OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+ * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+ * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+ * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+ * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+ * SUCH DAMAGE.
+ */
+/*
+ * FIPS pub 180-1: Secure Hash Algorithm (SHA-1)
+ * based on: http://csrc.nist.gov/fips/fip180-1.txt
+ * implemented by Jun-ichiro itojun Itoh <itojun@itojun.org>
+ */
+
+#ifndef SHA1_H
+#define SHA1_H
+
+#include "hash-method.h"
+
+/* libmysqlclient really should try to keep its internal stuff internal so
+ they won't conflict with the actual programs that are trying to use it.
+ This particular instance has been fixed in 4.1.18 and 5.0.19, but there
+ are others. */
+#define sha1_result sha1_result_libmysqlclient_craps_all_over
+
+struct sha1_ctxt {
+ union {
+ uint8_t b8[20];
+ uint32_t b32[5];
+ } h;
+ union {
+ uint8_t b8[8];
+ uint64_t b64[1];
+ } c;
+ union {
+ uint8_t b8[64];
+ uint32_t b32[16];
+ } m;
+ uint8_t count;
+};
+
+extern void sha1_init(struct sha1_ctxt *);
+extern void sha1_pad(struct sha1_ctxt *);
+extern void sha1_loop(struct sha1_ctxt *, const void *, size_t);
+extern void sha1_result(struct sha1_ctxt *, void *);
+
+
+/* compatibility with other SHA1 source codes */
+typedef struct sha1_ctxt SHA1_CTX;
+#define SHA1Init(x) sha1_init((x))
+#define SHA1Update(x, y, z) sha1_loop((x), (y), (z))
+#define SHA1Final(x, y) sha1_result((y), (x))
+
+#define SHA1_RESULTLEN (160/8)
+
+extern void sha1_get_digest(const void *data, size_t size,
+ unsigned char result[STATIC_ARRAY SHA1_RESULTLEN]);
+
+extern const struct hash_method hash_method_sha1;
+
+#endif
diff --git a/src/lib/sha2.c b/src/lib/sha2.c
new file mode 100644
index 0000000..93dddfb
--- /dev/null
+++ b/src/lib/sha2.c
@@ -0,0 +1,647 @@
+/*
+ * FIPS 180-2 SHA-224/256/384/512 implementation
+ * Last update: 02/02/2007
+ * Issue date: 04/30/2005
+ *
+ * Copyright (C) 2005, 2007 Olivier Gay <olivier.gay@a3.epfl.ch>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ * 3. Neither the name of the project nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE PROJECT AND CONTRIBUTORS ``AS IS'' AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE PROJECT OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+ * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+ * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+ * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+ * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+ * SUCH DAMAGE.
+ */
+
+#include "lib.h"
+#include "sha2.h"
+
+#define SHFR(x, n) (x >> n)
+#define ROTR(x, n) ((x >> n) | (x << ((sizeof(x) << 3) - n)))
+#define ROTL(x, n) ((x << n) | (x >> ((sizeof(x) << 3) - n)))
+#define CH(x, y, z) ((x & y) ^ (~x & z))
+#define MAJ(x, y, z) ((x & y) ^ (x & z) ^ (y & z))
+
+#define SHA256_F1(x) (ROTR(x, 2) ^ ROTR(x, 13) ^ ROTR(x, 22))
+#define SHA256_F2(x) (ROTR(x, 6) ^ ROTR(x, 11) ^ ROTR(x, 25))
+#define SHA256_F3(x) (ROTR(x, 7) ^ ROTR(x, 18) ^ SHFR(x, 3))
+#define SHA256_F4(x) (ROTR(x, 17) ^ ROTR(x, 19) ^ SHFR(x, 10))
+
+#define SHA384_F1(x) (ROTR(x, 28) ^ ROTR(x, 34) ^ ROTR(x, 39))
+#define SHA384_F2(x) (ROTR(x, 14) ^ ROTR(x, 18) ^ ROTR(x, 41))
+#define SHA384_F3(x) (ROTR(x, 1) ^ ROTR(x, 8) ^ SHFR(x, 7))
+#define SHA384_F4(x) (ROTR(x, 19) ^ ROTR(x, 61) ^ SHFR(x, 6))
+
+#define SHA512_F1(x) (ROTR(x, 28) ^ ROTR(x, 34) ^ ROTR(x, 39))
+#define SHA512_F2(x) (ROTR(x, 14) ^ ROTR(x, 18) ^ ROTR(x, 41))
+#define SHA512_F3(x) (ROTR(x, 1) ^ ROTR(x, 8) ^ SHFR(x, 7))
+#define SHA512_F4(x) (ROTR(x, 19) ^ ROTR(x, 61) ^ SHFR(x, 6))
+
+#define UNPACK32(x, str) \
+{ \
+ *((str) + 3) = (uint8_t) ((x) ); \
+ *((str) + 2) = (uint8_t) ((x) >> 8); \
+ *((str) + 1) = (uint8_t) ((x) >> 16); \
+ *((str) + 0) = (uint8_t) ((x) >> 24); \
+}
+
+#define PACK32(str, x) \
+{ \
+ *(x) = ((uint32_t) *((str) + 3) ) \
+ | ((uint32_t) *((str) + 2) << 8) \
+ | ((uint32_t) *((str) + 1) << 16) \
+ | ((uint32_t) *((str) + 0) << 24); \
+}
+
+#define UNPACK64(x, str) \
+{ \
+ *((str) + 7) = (uint8_t) ((x) ); \
+ *((str) + 6) = (uint8_t) ((x) >> 8); \
+ *((str) + 5) = (uint8_t) ((x) >> 16); \
+ *((str) + 4) = (uint8_t) ((x) >> 24); \
+ *((str) + 3) = (uint8_t) ((x) >> 32); \
+ *((str) + 2) = (uint8_t) ((x) >> 40); \
+ *((str) + 1) = (uint8_t) ((x) >> 48); \
+ *((str) + 0) = (uint8_t) ((x) >> 56); \
+}
+
+#define PACK64(str, x) \
+{ \
+ *(x) = ((uint64_t) *((str) + 7) ) \
+ | ((uint64_t) *((str) + 6) << 8) \
+ | ((uint64_t) *((str) + 5) << 16) \
+ | ((uint64_t) *((str) + 4) << 24) \
+ | ((uint64_t) *((str) + 3) << 32) \
+ | ((uint64_t) *((str) + 2) << 40) \
+ | ((uint64_t) *((str) + 1) << 48) \
+ | ((uint64_t) *((str) + 0) << 56); \
+}
+
+#define SHA256_SCR(i) \
+{ \
+ w[i] = SHA256_F4(w[i - 2]) + w[i - 7] \
+ + SHA256_F3(w[i - 15]) + w[i - 16]; \
+}
+
+#define SHA384_SCR(i) \
+{ \
+ w[i] = SHA512_F4(w[i - 2]) + w[i - 7] \
+ + SHA512_F3(w[i - 15]) + w[i - 16]; \
+}
+
+#define SHA512_SCR(i) \
+{ \
+ w[i] = SHA512_F4(w[i - 2]) + w[i - 7] \
+ + SHA512_F3(w[i - 15]) + w[i - 16]; \
+}
+
+static const uint32_t sha256_h0[8] =
+ {0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a,
+ 0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19};
+
+static const uint64_t sha384_h0[8] =
+ {0xcbbb9d5dc1059ed8ULL, 0x629a292a367cd507ULL,
+ 0x9159015a3070dd17ULL, 0x152fecd8f70e5939ULL,
+ 0x67332667ffc00b31ULL, 0x8eb44a8768581511ULL,
+ 0xdb0c2e0d64f98fa7ULL, 0x47b5481dbefa4fa4ULL};
+
+static const uint64_t sha512_h0[8] =
+ {0x6a09e667f3bcc908ULL, 0xbb67ae8584caa73bULL,
+ 0x3c6ef372fe94f82bULL, 0xa54ff53a5f1d36f1ULL,
+ 0x510e527fade682d1ULL, 0x9b05688c2b3e6c1fULL,
+ 0x1f83d9abfb41bd6bULL, 0x5be0cd19137e2179ULL};
+
+static const uint32_t sha256_k[64] =
+ {0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5,
+ 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5,
+ 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3,
+ 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174,
+ 0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc,
+ 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da,
+ 0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7,
+ 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967,
+ 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13,
+ 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85,
+ 0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3,
+ 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070,
+ 0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5,
+ 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3,
+ 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208,
+ 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2};
+
+static const uint64_t sha512_k[80] =
+ {0x428a2f98d728ae22ULL, 0x7137449123ef65cdULL,
+ 0xb5c0fbcfec4d3b2fULL, 0xe9b5dba58189dbbcULL,
+ 0x3956c25bf348b538ULL, 0x59f111f1b605d019ULL,
+ 0x923f82a4af194f9bULL, 0xab1c5ed5da6d8118ULL,
+ 0xd807aa98a3030242ULL, 0x12835b0145706fbeULL,
+ 0x243185be4ee4b28cULL, 0x550c7dc3d5ffb4e2ULL,
+ 0x72be5d74f27b896fULL, 0x80deb1fe3b1696b1ULL,
+ 0x9bdc06a725c71235ULL, 0xc19bf174cf692694ULL,
+ 0xe49b69c19ef14ad2ULL, 0xefbe4786384f25e3ULL,
+ 0x0fc19dc68b8cd5b5ULL, 0x240ca1cc77ac9c65ULL,
+ 0x2de92c6f592b0275ULL, 0x4a7484aa6ea6e483ULL,
+ 0x5cb0a9dcbd41fbd4ULL, 0x76f988da831153b5ULL,
+ 0x983e5152ee66dfabULL, 0xa831c66d2db43210ULL,
+ 0xb00327c898fb213fULL, 0xbf597fc7beef0ee4ULL,
+ 0xc6e00bf33da88fc2ULL, 0xd5a79147930aa725ULL,
+ 0x06ca6351e003826fULL, 0x142929670a0e6e70ULL,
+ 0x27b70a8546d22ffcULL, 0x2e1b21385c26c926ULL,
+ 0x4d2c6dfc5ac42aedULL, 0x53380d139d95b3dfULL,
+ 0x650a73548baf63deULL, 0x766a0abb3c77b2a8ULL,
+ 0x81c2c92e47edaee6ULL, 0x92722c851482353bULL,
+ 0xa2bfe8a14cf10364ULL, 0xa81a664bbc423001ULL,
+ 0xc24b8b70d0f89791ULL, 0xc76c51a30654be30ULL,
+ 0xd192e819d6ef5218ULL, 0xd69906245565a910ULL,
+ 0xf40e35855771202aULL, 0x106aa07032bbd1b8ULL,
+ 0x19a4c116b8d2d0c8ULL, 0x1e376c085141ab53ULL,
+ 0x2748774cdf8eeb99ULL, 0x34b0bcb5e19b48a8ULL,
+ 0x391c0cb3c5c95a63ULL, 0x4ed8aa4ae3418acbULL,
+ 0x5b9cca4f7763e373ULL, 0x682e6ff3d6b2b8a3ULL,
+ 0x748f82ee5defb2fcULL, 0x78a5636f43172f60ULL,
+ 0x84c87814a1f0ab72ULL, 0x8cc702081a6439ecULL,
+ 0x90befffa23631e28ULL, 0xa4506cebde82bde9ULL,
+ 0xbef9a3f7b2c67915ULL, 0xc67178f2e372532bULL,
+ 0xca273eceea26619cULL, 0xd186b8c721c0c207ULL,
+ 0xeada7dd6cde0eb1eULL, 0xf57d4f7fee6ed178ULL,
+ 0x06f067aa72176fbaULL, 0x0a637dc5a2c898a6ULL,
+ 0x113f9804bef90daeULL, 0x1b710b35131c471bULL,
+ 0x28db77f523047d84ULL, 0x32caab7b40c72493ULL,
+ 0x3c9ebe0a15c9bebcULL, 0x431d67c49c100d4cULL,
+ 0x4cc5d4becb3e42b6ULL, 0x597f299cfc657e2aULL,
+ 0x5fcb6fab3ad6faecULL, 0x6c44198c4a475817ULL};
+
+
+/* SHA-256 functions */
+
+static void ATTR_UNSIGNED_WRAPS
+sha256_transf(struct sha256_ctx *ctx, const unsigned char *data,
+ size_t block_nb)
+{
+ uint32_t w[64];
+ uint32_t wv[8];
+ uint32_t t1, t2;
+ const unsigned char *sub_block;
+ int i,j;
+
+
+ for (i = 0; i < (int) block_nb; i++) {
+ sub_block = data + (i << 6);
+
+ for (j = 0; j < 16; j++) {
+ PACK32(&sub_block[j << 2], &w[j]);
+ }
+
+ for (j = 16; j < 64; j++) {
+ SHA256_SCR(j);
+ }
+
+ for (j = 0; j < 8; j++) {
+ wv[j] = ctx->h[j];
+ }
+
+ for (j = 0; j < 64; j++) {
+ t1 = wv[7] + SHA256_F2(wv[4]) + CH(wv[4], wv[5], wv[6])
+ + sha256_k[j] + w[j];
+ t2 = SHA256_F1(wv[0]) + MAJ(wv[0], wv[1], wv[2]);
+ wv[7] = wv[6];
+ wv[6] = wv[5];
+ wv[5] = wv[4];
+ wv[4] = wv[3] + t1;
+ wv[3] = wv[2];
+ wv[2] = wv[1];
+ wv[1] = wv[0];
+ wv[0] = t1 + t2;
+ }
+
+ for (j = 0; j < 8; j++) {
+ ctx->h[j] += wv[j];
+ }
+ }
+}
+
+void sha256_init(struct sha256_ctx *ctx)
+{
+ int i;
+
+ for (i = 0; i < 8; i++) {
+ ctx->h[i] = sha256_h0[i];
+ }
+
+ ctx->len = 0;
+ ctx->tot_len = 0;
+}
+
+void sha256_loop(struct sha256_ctx *ctx, const void *data,
+ size_t len)
+{
+ const unsigned char *shifted_message;
+ size_t block_nb;
+ size_t new_len, rem_len, tmp_len;
+
+ tmp_len = SHA256_BLOCK_SIZE - ctx->len;
+ rem_len = len < tmp_len ? len : tmp_len;
+
+ memcpy(&ctx->block[ctx->len], data, rem_len);
+
+ if (ctx->len + len < SHA256_BLOCK_SIZE) {
+ ctx->len += len;
+ return;
+ }
+
+ new_len = len - rem_len;
+ block_nb = new_len / SHA256_BLOCK_SIZE;
+
+ shifted_message = CONST_PTR_OFFSET(data, rem_len);
+
+ sha256_transf(ctx, ctx->block, 1);
+ sha256_transf(ctx, shifted_message, block_nb);
+
+ rem_len = new_len % SHA256_BLOCK_SIZE;
+ memcpy(ctx->block, &shifted_message[block_nb << 6], rem_len);
+
+ ctx->len = rem_len;
+ ctx->tot_len += (block_nb + 1) << 6;
+}
+
+void sha256_result(struct sha256_ctx *ctx,
+ unsigned char digest[STATIC_ARRAY SHA256_RESULTLEN])
+{
+ size_t block_nb;
+ size_t pm_len;
+ size_t len_b;
+ int i;
+
+ block_nb = (1 + ((SHA256_BLOCK_SIZE - 9)
+ < (ctx->len % SHA256_BLOCK_SIZE)));
+
+ len_b = (ctx->tot_len + ctx->len) << 3;
+ pm_len = block_nb << 6;
+
+ memset(ctx->block + ctx->len, 0, pm_len - ctx->len);
+ ctx->block[ctx->len] = 0x80;
+ UNPACK32(len_b, ctx->block + pm_len - 4);
+
+ sha256_transf(ctx, ctx->block, block_nb);
+
+ for (i = 0 ; i < 8; i++) {
+ UNPACK32(ctx->h[i], &digest[i << 2]);
+ }
+}
+
+void sha256_get_digest(const void *data, size_t size,
+ unsigned char digest[STATIC_ARRAY SHA256_RESULTLEN])
+{
+ struct sha256_ctx ctx;
+
+ sha256_init(&ctx);
+ sha256_loop(&ctx, data, size);
+ sha256_result(&ctx, digest);
+}
+
+/* SHA-384 functions */
+
+static void ATTR_UNSIGNED_WRAPS
+sha384_transf(struct sha384_ctx *ctx, const unsigned char *data,
+ size_t block_nb)
+{
+ uint64_t w[80];
+ uint64_t wv[8];
+ uint64_t t1, t2;
+ const unsigned char *sub_block;
+ int i, j;
+
+ for (i = 0; i < (int) block_nb; i++) {
+ sub_block = data + (i << 7);
+
+ for (j = 0; j < 16; j++) {
+ PACK64(&sub_block[j << 3], &w[j]);
+ }
+
+ for (j = 16; j < 80; j++) {
+ SHA384_SCR(j);
+ }
+
+ for (j = 0; j < 8; j++) {
+ wv[j] = ctx->h[j];
+ }
+
+ for (j = 0; j < 80; j++) {
+ /* sha384_k is same as sha512_k */
+ t1 = wv[7] + SHA384_F2(wv[4]) + CH(wv[4], wv[5], wv[6])
+ + sha512_k[j] + w[j];
+ t2 = SHA384_F1(wv[0]) + MAJ(wv[0], wv[1], wv[2]);
+ wv[7] = wv[6];
+ wv[6] = wv[5];
+ wv[5] = wv[4];
+ wv[4] = wv[3] + t1;
+ wv[3] = wv[2];
+ wv[2] = wv[1];
+ wv[1] = wv[0];
+ wv[0] = t1 + t2;
+ }
+
+ for (j = 0; j < 8; j++) {
+ ctx->h[j] += wv[j];
+ }
+ }
+}
+
+void sha384_init(struct sha384_ctx *ctx)
+{
+ int i;
+
+ for (i = 0; i < 8; i++) {
+ ctx->h[i] = sha384_h0[i];
+ }
+
+ ctx->len = 0;
+ ctx->tot_len = 0;
+}
+
+void sha384_loop(struct sha384_ctx *ctx, const void *data,
+ size_t len)
+{
+ const unsigned char *shifted_message;
+ size_t block_nb;
+ size_t new_len, rem_len, tmp_len;
+
+ tmp_len = SHA384_BLOCK_SIZE - ctx->len;
+ rem_len = len < tmp_len ? len : tmp_len;
+
+ memcpy(&ctx->block[ctx->len], data, rem_len);
+
+ if (ctx->len + len < SHA384_BLOCK_SIZE) {
+ ctx->len += len;
+ return;
+ }
+
+ new_len = len - rem_len;
+ block_nb = new_len / SHA384_BLOCK_SIZE;
+
+ shifted_message = CONST_PTR_OFFSET(data, rem_len);
+
+ sha384_transf(ctx, ctx->block, 1);
+ sha384_transf(ctx, shifted_message, block_nb);
+
+ rem_len = new_len % SHA384_BLOCK_SIZE;
+ memcpy(ctx->block, &shifted_message[block_nb << 7], rem_len);
+
+ ctx->len = rem_len;
+ ctx->tot_len += (block_nb + 1) << 7;
+}
+
+void sha384_result(struct sha384_ctx *ctx,
+ unsigned char digest[STATIC_ARRAY SHA384_RESULTLEN])
+{
+ unsigned int block_nb;
+ unsigned int pm_len;
+ size_t len_b;
+ int i;
+
+ block_nb = 1 + ((SHA384_BLOCK_SIZE - 17)
+ < (ctx->len % SHA384_BLOCK_SIZE));
+
+ len_b = (ctx->tot_len + ctx->len) << 3;
+ pm_len = block_nb << 7;
+
+ memset(ctx->block + ctx->len, 0, pm_len - ctx->len);
+ ctx->block[ctx->len] = 0x80;
+ UNPACK32(len_b, ctx->block + pm_len - 4);
+
+ sha384_transf(ctx, ctx->block, block_nb);
+
+ for (i = 0 ; i < 6; i++) {
+ UNPACK64(ctx->h[i], &digest[i << 3]);
+ }
+}
+
+void sha384_get_digest(const void *data, size_t size,
+ unsigned char digest[STATIC_ARRAY SHA384_RESULTLEN])
+{
+ struct sha384_ctx ctx;
+
+ sha384_init(&ctx);
+ sha384_loop(&ctx, data, size);
+ sha384_result(&ctx, digest);
+}
+
+
+/* SHA-512 functions */
+
+static void ATTR_UNSIGNED_WRAPS
+sha512_transf(struct sha512_ctx *ctx, const unsigned char *data,
+ size_t block_nb)
+{
+ uint64_t w[80];
+ uint64_t wv[8];
+ uint64_t t1, t2;
+ const unsigned char *sub_block;
+ int i, j;
+
+ for (i = 0; i < (int) block_nb; i++) {
+ sub_block = data + (i << 7);
+
+ for (j = 0; j < 16; j++) {
+ PACK64(&sub_block[j << 3], &w[j]);
+ }
+
+ for (j = 16; j < 80; j++) {
+ SHA512_SCR(j);
+ }
+
+ for (j = 0; j < 8; j++) {
+ wv[j] = ctx->h[j];
+ }
+
+ for (j = 0; j < 80; j++) {
+ t1 = wv[7] + SHA512_F2(wv[4]) + CH(wv[4], wv[5], wv[6])
+ + sha512_k[j] + w[j];
+ t2 = SHA512_F1(wv[0]) + MAJ(wv[0], wv[1], wv[2]);
+ wv[7] = wv[6];
+ wv[6] = wv[5];
+ wv[5] = wv[4];
+ wv[4] = wv[3] + t1;
+ wv[3] = wv[2];
+ wv[2] = wv[1];
+ wv[1] = wv[0];
+ wv[0] = t1 + t2;
+ }
+
+ for (j = 0; j < 8; j++) {
+ ctx->h[j] += wv[j];
+ }
+ }
+}
+
+void sha512_init(struct sha512_ctx *ctx)
+{
+ int i;
+
+ for (i = 0; i < 8; i++) {
+ ctx->h[i] = sha512_h0[i];
+ }
+
+ ctx->len = 0;
+ ctx->tot_len = 0;
+}
+
+void sha512_loop(struct sha512_ctx *ctx, const void *data,
+ size_t len)
+{
+ const unsigned char *shifted_message;
+ size_t block_nb;
+ size_t new_len, rem_len, tmp_len;
+
+ tmp_len = SHA512_BLOCK_SIZE - ctx->len;
+ rem_len = len < tmp_len ? len : tmp_len;
+
+ memcpy(&ctx->block[ctx->len], data, rem_len);
+
+ if (ctx->len + len < SHA512_BLOCK_SIZE) {
+ ctx->len += len;
+ return;
+ }
+
+ new_len = len - rem_len;
+ block_nb = new_len / SHA512_BLOCK_SIZE;
+
+ shifted_message = CONST_PTR_OFFSET(data, rem_len);
+
+ sha512_transf(ctx, ctx->block, 1);
+ sha512_transf(ctx, shifted_message, block_nb);
+
+ rem_len = new_len % SHA512_BLOCK_SIZE;
+ memcpy(ctx->block, &shifted_message[block_nb << 7], rem_len);
+
+ ctx->len = rem_len;
+ ctx->tot_len += (block_nb + 1) << 7;
+}
+
+void sha512_result(struct sha512_ctx *ctx,
+ unsigned char digest[STATIC_ARRAY SHA512_RESULTLEN])
+{
+ unsigned int block_nb;
+ unsigned int pm_len;
+ size_t len_b;
+ int i;
+
+ block_nb = 1 + ((SHA512_BLOCK_SIZE - 17)
+ < (ctx->len % SHA512_BLOCK_SIZE));
+
+ len_b = (ctx->tot_len + ctx->len) << 3;
+ pm_len = block_nb << 7;
+
+ memset(ctx->block + ctx->len, 0, pm_len - ctx->len);
+ ctx->block[ctx->len] = 0x80;
+ UNPACK32(len_b, ctx->block + pm_len - 4);
+
+ sha512_transf(ctx, ctx->block, block_nb);
+
+ for (i = 0 ; i < 8; i++) {
+ UNPACK64(ctx->h[i], &digest[i << 3]);
+ }
+}
+
+void sha512_get_digest(const void *data, size_t size,
+ unsigned char digest[STATIC_ARRAY SHA512_RESULTLEN])
+{
+ struct sha512_ctx ctx;
+
+ sha512_init(&ctx);
+ sha512_loop(&ctx, data, size);
+ sha512_result(&ctx, digest);
+}
+
+static void hash_method_init_sha256(void *context)
+{
+ sha256_init(context);
+}
+static void hash_method_loop_sha256(void *context, const void *data, size_t size)
+{
+ sha256_loop(context, data, size);
+}
+
+static void hash_method_result_sha256(void *context, unsigned char *result_r)
+{
+ sha256_result(context, result_r);
+}
+
+const struct hash_method hash_method_sha256 = {
+ .name = "sha256",
+ .block_size = SHA256_BLOCK_SIZE,
+ .context_size = sizeof(struct sha256_ctx),
+ .digest_size = SHA256_RESULTLEN,
+
+ .init = hash_method_init_sha256,
+ .loop = hash_method_loop_sha256,
+ .result = hash_method_result_sha256,
+};
+
+static void hash_method_init_sha384(void *context)
+{
+ sha384_init(context);
+}
+static void hash_method_loop_sha384(void *context, const void *data, size_t size)
+{
+ sha384_loop(context, data, size);
+}
+
+static void hash_method_result_sha384(void *context, unsigned char *result_r)
+{
+ sha384_result(context, result_r);
+}
+
+const struct hash_method hash_method_sha384 = {
+ .name = "sha384",
+ .block_size = SHA384_BLOCK_SIZE,
+ .context_size = sizeof(struct sha384_ctx),
+ .digest_size = SHA384_RESULTLEN,
+
+ .init = hash_method_init_sha384,
+ .loop = hash_method_loop_sha384,
+ .result = hash_method_result_sha384,
+};
+
+static void hash_method_init_sha512(void *context)
+{
+ sha512_init(context);
+}
+static void hash_method_loop_sha512(void *context, const void *data, size_t size)
+{
+ sha512_loop(context, data, size);
+}
+
+static void hash_method_result_sha512(void *context, unsigned char *result_r)
+{
+ sha512_result(context, result_r);
+}
+
+const struct hash_method hash_method_sha512 = {
+ .name = "sha512",
+ .block_size = SHA512_BLOCK_SIZE,
+ .context_size = sizeof(struct sha512_ctx),
+ .digest_size = SHA512_RESULTLEN,
+
+ .init = hash_method_init_sha512,
+ .loop = hash_method_loop_sha512,
+ .result = hash_method_result_sha512,
+};
diff --git a/src/lib/sha2.h b/src/lib/sha2.h
new file mode 100644
index 0000000..8c893eb
--- /dev/null
+++ b/src/lib/sha2.h
@@ -0,0 +1,89 @@
+/*
+ * FIPS 180-2 SHA-224/256/384/512 implementation
+ * Last update: 02/02/2007
+ * Issue date: 04/30/2005
+ *
+ * Copyright (C) 2005, 2007 Olivier Gay <olivier.gay@a3.epfl.ch>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ * 3. Neither the name of the project nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE PROJECT AND CONTRIBUTORS ``AS IS'' AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE PROJECT OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+ * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+ * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+ * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+ * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+ * SUCH DAMAGE.
+ */
+
+#ifndef SHA2_H
+#define SHA2_H
+
+#include "hash-method.h"
+#include "sha-common.h"
+
+struct sha256_ctx {
+ size_t tot_len;
+ size_t len;
+ unsigned char block[2 * SHA256_BLOCK_SIZE];
+ uint32_t h[8];
+};
+
+struct sha384_ctx {
+ size_t tot_len;
+ size_t len;
+ unsigned char block[2 * SHA384_BLOCK_SIZE];
+ uint64_t h[8];
+};
+
+struct sha512_ctx {
+ size_t tot_len;
+ size_t len;
+ unsigned char block[2 * SHA512_BLOCK_SIZE];
+ uint64_t h[8];
+};
+
+void sha256_init(struct sha256_ctx *ctx);
+void sha256_loop(struct sha256_ctx *ctx, const void *data, size_t len);
+void sha256_result(struct sha256_ctx *ctx,
+ unsigned char digest[STATIC_ARRAY SHA256_RESULTLEN]);
+
+void sha256_get_digest(const void *data, size_t size,
+ unsigned char digest[STATIC_ARRAY SHA256_RESULTLEN]);
+
+void sha384_init(struct sha384_ctx *ctx);
+void sha384_loop(struct sha384_ctx *ctx, const void *data, size_t len);
+void sha384_result(struct sha384_ctx *ctx,
+ unsigned char digest[STATIC_ARRAY SHA384_RESULTLEN]);
+
+void sha384_get_digest(const void *data, size_t size,
+ unsigned char digest[STATIC_ARRAY SHA384_RESULTLEN]);
+
+void sha512_init(struct sha512_ctx *ctx);
+void sha512_loop(struct sha512_ctx *ctx, const void *data, size_t len);
+void sha512_result(struct sha512_ctx *ctx,
+ unsigned char digest[STATIC_ARRAY SHA512_RESULTLEN]);
+
+void sha512_get_digest(const void *data, size_t size,
+ unsigned char digest[STATIC_ARRAY SHA512_RESULTLEN]);
+
+extern const struct hash_method hash_method_sha256;
+extern const struct hash_method hash_method_sha384;
+extern const struct hash_method hash_method_sha512;
+
+#endif /* !SHA2_H */
diff --git a/src/lib/sha3.c b/src/lib/sha3.c
new file mode 100644
index 0000000..3b40ddd
--- /dev/null
+++ b/src/lib/sha3.c
@@ -0,0 +1,335 @@
+/* -------------------------------------------------------------------------
+ * Works when compiled for either 32-bit or 64-bit targets, optimized for
+ * 64 bit.
+ *
+ * Canonical implementation of Init/Update/Finalize for SHA-3 byte input.
+ *
+ * SHA3-256, SHA3-384, SHA-512 are implemented. SHA-224 can easily be added.
+ *
+ * Based on code from http://keccak.noekeon.org/ .
+ *
+ * I place the code that I wrote into public domain, free to use.
+ *
+ * I would appreciate if you give credits to this work if you used it to
+ * write or test * your code.
+ *
+ * Aug 2015. Andrey Jivsov. crypto@brainhub.org
+ *
+ * Modified for Dovecot oy use
+ * Oct 2016. Aki Tuomi <aki.tuomi@dovecot.fi>
+
+ * ---------------------------------------------------------------------- */
+#include "lib.h"
+#include "sha3.h"
+
+#include <stdio.h>
+#include <stdint.h>
+#include <string.h>
+
+#if defined(_MSC_VER)
+#define SHA3_CONST(x) x
+#else
+#define SHA3_CONST(x) x##L
+#endif
+
+/* The following state definition should normally be in a separate
+ * header file
+ */
+
+#ifndef SHA3_ROTL64
+#define SHA3_ROTL64(x, y) \
+ (((x) << (y)) | ((x) >> ((sizeof(uint64_t)*8) - (y))))
+#endif
+
+static const uint64_t keccakf_rndc[24] = {
+ SHA3_CONST(0x0000000000000001UL), SHA3_CONST(0x0000000000008082UL),
+ SHA3_CONST(0x800000000000808aUL), SHA3_CONST(0x8000000080008000UL),
+ SHA3_CONST(0x000000000000808bUL), SHA3_CONST(0x0000000080000001UL),
+ SHA3_CONST(0x8000000080008081UL), SHA3_CONST(0x8000000000008009UL),
+ SHA3_CONST(0x000000000000008aUL), SHA3_CONST(0x0000000000000088UL),
+ SHA3_CONST(0x0000000080008009UL), SHA3_CONST(0x000000008000000aUL),
+ SHA3_CONST(0x000000008000808bUL), SHA3_CONST(0x800000000000008bUL),
+ SHA3_CONST(0x8000000000008089UL), SHA3_CONST(0x8000000000008003UL),
+ SHA3_CONST(0x8000000000008002UL), SHA3_CONST(0x8000000000000080UL),
+ SHA3_CONST(0x000000000000800aUL), SHA3_CONST(0x800000008000000aUL),
+ SHA3_CONST(0x8000000080008081UL), SHA3_CONST(0x8000000000008080UL),
+ SHA3_CONST(0x0000000080000001UL), SHA3_CONST(0x8000000080008008UL)
+};
+
+static const unsigned keccakf_rotc[24] = {
+ 1, 3, 6, 10, 15, 21, 28, 36, 45, 55, 2, 14, 27, 41, 56, 8, 25, 43, 62,
+ 18, 39, 61, 20, 44
+};
+
+static const unsigned keccakf_piln[24] = {
+ 10, 7, 11, 17, 18, 3, 5, 16, 8, 21, 24, 4, 15, 23, 19, 13, 12, 2, 20,
+ 14, 22, 9, 6, 1
+};
+
+/* generally called after SHA3_KECCAK_SPONGE_WORDS-ctx->capacityWords words
+ * are XORed into the state s
+ */
+static void ATTR_UNSIGNED_WRAPS
+keccakf(uint64_t s[25])
+{
+ int i, j, round;
+ uint64_t t, bc[5];
+#define KECCAK_ROUNDS 24
+
+ for(round = 0; round < KECCAK_ROUNDS; round++) {
+
+ /* Theta */
+ for(i = 0; i < 5; i++)
+ bc[i] = s[i] ^ s[i + 5] ^ s[i + 10] ^ s[i + 15] ^ s[i + 20];
+
+ for(i = 0; i < 5; i++) {
+ t = bc[(i + 4) % 5] ^ SHA3_ROTL64(bc[(i + 1) % 5], 1);
+ for(j = 0; j < 25; j += 5)
+ s[j + i] ^= t;
+ }
+
+ /* Rho Pi */
+ t = s[1];
+ for(i = 0; i < 24; i++) {
+ j = keccakf_piln[i];
+ bc[0] = s[j];
+ s[j] = SHA3_ROTL64(t, keccakf_rotc[i]);
+ t = bc[0];
+ }
+
+ /* Chi */
+ for(j = 0; j < 25; j += 5) {
+ for(i = 0; i < 5; i++)
+ bc[i] = s[j + i];
+ for(i = 0; i < 5; i++)
+ s[j + i] ^= (~bc[(i + 1) % 5]) & bc[(i + 2) % 5];
+ }
+
+ /* Iota */
+ s[0] ^= keccakf_rndc[round];
+ }
+}
+
+/* *************************** Public Interface ************************ */
+
+void sha3_256_init(void *context)
+{
+ struct sha3_ctx *ctx = context;
+ i_zero(ctx);
+ ctx->capacityWords = 2 * 256 / (8 * sizeof(uint64_t));
+}
+
+void sha3_512_init(void *context)
+{
+ struct sha3_ctx *ctx = context;
+ i_zero(ctx);
+ ctx->capacityWords = 2 * 512 / (8 * sizeof(uint64_t));
+}
+
+void sha3_loop(void *context, const void *data, size_t len)
+{
+ struct sha3_ctx *ctx = context;
+ /* 0...7 -- how much is needed to have a word */
+ unsigned old_tail = (8 - ctx->byteIndex) & 7;
+
+ size_t words;
+ unsigned tail;
+ size_t i;
+
+ const uint8_t *buf = data;
+
+ i_assert(ctx->byteIndex < 8);
+ i_assert(ctx->wordIndex < sizeof(ctx->s) / sizeof(ctx->s[0]));
+
+ if(len < old_tail) { /* have no complete word or haven't started
+ * the word yet */
+ /* endian-independent code follows: */
+ while (len > 0) {
+ len--;
+ ctx->saved |= (uint64_t) (*(buf++)) <<
+ ((ctx->byteIndex++) * 8);
+ }
+ i_assert(ctx->byteIndex < 8);
+ return;
+ }
+
+ if(old_tail != 0) { /* will have one word to process */
+ /* endian-independent code follows: */
+ len -= old_tail;
+ while (old_tail > 0) {
+ old_tail--;
+ ctx->saved |= (uint64_t) (*(buf++)) <<
+ ((ctx->byteIndex++) * 8);
+ }
+
+ /* now ready to add saved to the sponge */
+ ctx->s[ctx->wordIndex] ^= ctx->saved;
+ i_assert(ctx->byteIndex == 8);
+ ctx->byteIndex = 0;
+ ctx->saved = 0;
+ if(++ctx->wordIndex ==
+ (SHA3_KECCAK_SPONGE_WORDS -
+ ctx->capacityWords)) {
+ keccakf(ctx->s);
+ ctx->wordIndex = 0;
+ }
+ }
+
+ /* now work in full words directly from input */
+
+ i_assert(ctx->byteIndex == 0);
+
+ words = len / sizeof(uint64_t);
+ tail = len - words * sizeof(uint64_t);
+
+ for(i = 0; i < words; i++, buf += sizeof(uint64_t)) {
+ const uint64_t t = (uint64_t) (buf[0]) |
+ ((uint64_t) (buf[1]) << 8 * 1) |
+ ((uint64_t) (buf[2]) << 8 * 2) |
+ ((uint64_t) (buf[3]) << 8 * 3) |
+ ((uint64_t) (buf[4]) << 8 * 4) |
+ ((uint64_t) (buf[5]) << 8 * 5) |
+ ((uint64_t) (buf[6]) << 8 * 6) |
+ ((uint64_t) (buf[7]) << 8 * 7);
+#if defined(__x86_64__ ) || defined(__i386__)
+ i_assert(memcmp(&t, buf, 8) == 0);
+#endif
+ ctx->s[ctx->wordIndex] ^= t;
+ if(++ctx->wordIndex ==
+ (SHA3_KECCAK_SPONGE_WORDS - ctx->capacityWords)) {
+ keccakf(ctx->s);
+ ctx->wordIndex = 0;
+ }
+ }
+
+ /* finally, save the partial word */
+ i_assert(ctx->byteIndex == 0 && tail < 8);
+ while (tail > 0) {
+ tail--;
+ ctx->saved |= (uint64_t) (*(buf++)) << ((ctx->byteIndex++) * 8);
+ }
+ i_assert(ctx->byteIndex < 8);
+}
+
+/* This is simply the 'update' with the padding block.
+ * The padding block is 0x01 || 0x00* || 0x80. First 0x01 and last 0x80
+ * bytes are always present, but they can be the same byte.
+ */
+static void
+sha3_finalize(struct sha3_ctx *ctx)
+{
+ /* Append 2-bit suffix 01, per SHA-3 spec. Instead of 1 for padding we
+ * use 1<<2 below. The 0x02 below corresponds to the suffix 01.
+ * Overall, we feed 0, then 1, and finally 1 to start padding. Without
+ * M || 01, we would simply use 1 to start padding. */
+
+ /* SHA3 version */
+ ctx->s[ctx->wordIndex] ^=
+ (ctx->saved ^ ((uint64_t) ((uint64_t) (0x02 | (1 << 2)) <<
+ ((ctx->byteIndex) * 8))));
+
+ ctx->s[SHA3_KECCAK_SPONGE_WORDS - ctx->capacityWords - 1] ^=
+ SHA3_CONST(0x8000000000000000UL);
+ keccakf(ctx->s);
+
+#ifdef WORDS_BIGENDIAN
+ {
+ unsigned i;
+ for(i = 0; i < SHA3_KECCAK_SPONGE_WORDS; i++) {
+ const unsigned t1 = (uint32_t) ctx->s[i];
+ const unsigned t2 = (uint32_t) ((ctx->s[i] >> 16) >> 16);
+ ctx->sb[i * 8 + 0] = (uint8_t) (t1);
+ ctx->sb[i * 8 + 1] = (uint8_t) (t1 >> 8);
+ ctx->sb[i * 8 + 2] = (uint8_t) (t1 >> 16);
+ ctx->sb[i * 8 + 3] = (uint8_t) (t1 >> 24);
+ ctx->sb[i * 8 + 4] = (uint8_t) (t2);
+ ctx->sb[i * 8 + 5] = (uint8_t) (t2 >> 8);
+ ctx->sb[i * 8 + 6] = (uint8_t) (t2 >> 16);
+ ctx->sb[i * 8 + 7] = (uint8_t) (t2 >> 24);
+ }
+ }
+#endif
+}
+
+void sha3_256_result(void *context,
+ unsigned char digest[STATIC_ARRAY SHA256_RESULTLEN])
+{
+ struct sha3_ctx *ctx = context;
+ sha3_finalize(ctx);
+ memcpy(digest, ctx->sb, SHA256_RESULTLEN);
+}
+
+
+void sha3_512_result(void *context,
+ unsigned char digest[STATIC_ARRAY SHA512_RESULTLEN])
+{
+ struct sha3_ctx *ctx = context;
+ sha3_finalize(ctx);
+ memcpy(digest, ctx->sb, SHA512_RESULTLEN);
+}
+
+
+void sha3_256_get_digest(const void *data, size_t size,
+ unsigned char digest[STATIC_ARRAY SHA256_RESULTLEN])
+{
+ struct sha3_ctx ctx;
+ sha3_256_init(&ctx);
+ sha3_loop(&ctx, data, size);
+ sha3_256_result(&ctx, digest);
+}
+
+void sha3_512_get_digest(const void *data, size_t size,
+ unsigned char digest[STATIC_ARRAY SHA512_RESULTLEN])
+{
+ struct sha3_ctx ctx;
+ sha3_512_init(&ctx);
+ sha3_loop(&ctx, data, size);
+ sha3_512_result(&ctx, digest);
+}
+
+static void hash_method_init_sha3_256(void *context)
+{
+ sha3_256_init(context);
+}
+
+static void hash_method_loop_sha3(void *context, const void *data, size_t size)
+{
+ sha3_loop(context, data, size);
+}
+
+static void hash_method_result_sha3_256(void *context, unsigned char *result_r)
+{
+ sha3_256_result(context, result_r);
+}
+
+const struct hash_method hash_method_sha3_256 = {
+ .name = "sha3-256",
+ .block_size = SHA256_BLOCK_SIZE,
+ .context_size = sizeof(struct sha3_ctx),
+ .digest_size = SHA256_RESULTLEN,
+
+ .init = hash_method_init_sha3_256,
+ .loop = hash_method_loop_sha3,
+ .result = hash_method_result_sha3_256,
+};
+
+static void hash_method_init_sha3_512(void *context)
+{
+ sha3_512_init(context);
+}
+
+static void hash_method_result_sha3_512(void *context, unsigned char *result_r)
+{
+ sha3_512_result(context, result_r);
+}
+
+const struct hash_method hash_method_sha3_512 = {
+ .name = "sha3-512",
+ .block_size = SHA512_BLOCK_SIZE,
+ .context_size = sizeof(struct sha3_ctx),
+ .digest_size = SHA512_RESULTLEN,
+
+ .init = hash_method_init_sha3_512,
+ .loop = hash_method_loop_sha3,
+ .result = hash_method_result_sha3_512,
+};
diff --git a/src/lib/sha3.h b/src/lib/sha3.h
new file mode 100644
index 0000000..69babc8
--- /dev/null
+++ b/src/lib/sha3.h
@@ -0,0 +1,75 @@
+/*
+ * FIPS 180-2 SHA-224/256/384/512 implementation
+ * Last update: 02/02/2007
+ * Issue date: 04/30/2005
+ *
+ * Copyright (C) 2005, 2007 Olivier Gay <olivier.gay@a3.epfl.ch>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ * 3. Neither the name of the project nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE PROJECT AND CONTRIBUTORS ``AS IS'' AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE PROJECT OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+ * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+ * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+ * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+ * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+ * SUCH DAMAGE.
+ */
+
+#ifndef SHA3_H
+#define SHA3_H
+
+#include "hash-method.h"
+#include "sha-common.h"
+
+#define SHA3_KECCAK_SPONGE_WORDS \
+ (((1600)/8/*bits to byte*/)/sizeof(uint64_t))
+
+struct sha3_ctx {
+ uint64_t saved; /* the portion of the input message that we
+ * didn't consume yet */
+ union { /* Keccak's state */
+ uint64_t s[SHA3_KECCAK_SPONGE_WORDS];
+ uint8_t sb[SHA3_KECCAK_SPONGE_WORDS * 8];
+ };
+ unsigned byteIndex; /* 0..7--the next byte after the set one
+ * (starts from 0; 0--none are buffered) */
+ unsigned wordIndex; /* 0..24--the next word to integrate input
+ * (starts from 0) */
+ unsigned capacityWords; /* the double size of the hash output in
+ * words (e.g. 16 for Keccak 512) */
+};
+
+void sha3_256_init(void *context);
+void sha3_256_result(void *context,
+ unsigned char digest[STATIC_ARRAY SHA256_RESULTLEN]);
+void sha3_256_get_digest(const void *data, size_t size,
+ unsigned char digest[STATIC_ARRAY SHA256_RESULTLEN]);
+
+void sha3_512_init(void *context);
+void sha3_512_result(void *context,
+ unsigned char digest[STATIC_ARRAY SHA512_RESULTLEN]);
+void sha3_512_get_digest(const void *data, size_t size,
+ unsigned char digest[STATIC_ARRAY SHA512_RESULTLEN]);
+
+void sha3_loop(void *context, const void *data, size_t len);
+
+extern const struct hash_method hash_method_sha3_256;
+extern const struct hash_method hash_method_sha3_512;
+
+#endif
diff --git a/src/lib/sleep.c b/src/lib/sleep.c
new file mode 100644
index 0000000..7b3b6fa
--- /dev/null
+++ b/src/lib/sleep.c
@@ -0,0 +1,74 @@
+/* Copyright (c) 2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "sleep.h"
+
+#include <time.h>
+
+static bool ATTR_NOWARN_UNUSED_RESULT
+sleep_timespec(const struct timespec *ts_sleep, bool interruptible)
+{
+ struct timespec ts_remain = *ts_sleep;
+
+ while (nanosleep(&ts_remain, &ts_remain) < 0) {
+ if (errno != EINTR)
+ i_fatal("nanosleep(): %m");
+ if (interruptible)
+ return FALSE;
+ }
+ return TRUE;
+}
+
+void i_sleep_usecs(unsigned long long usecs)
+{
+ struct timespec ts_sleep;
+
+ ts_sleep.tv_sec = (time_t)(usecs / 1000000);
+ ts_sleep.tv_nsec = (long)(usecs % 1000000) * 1000;
+ sleep_timespec(&ts_sleep, FALSE);
+}
+
+void i_sleep_msecs(unsigned int msecs)
+{
+ struct timespec ts_sleep;
+
+ ts_sleep.tv_sec = (time_t)(msecs / 1000);
+ ts_sleep.tv_nsec = (long)(msecs % 1000) * 1000000;
+ sleep_timespec(&ts_sleep, FALSE);
+}
+
+void i_sleep_secs(time_t secs)
+{
+ struct timespec ts_sleep;
+
+ ts_sleep.tv_sec = secs;
+ ts_sleep.tv_nsec = 0;
+ sleep_timespec(&ts_sleep, FALSE);
+}
+
+bool i_sleep_intr_usecs(unsigned long long usecs)
+{
+ struct timespec ts_sleep;
+
+ ts_sleep.tv_sec = (time_t)(usecs / 1000000);
+ ts_sleep.tv_nsec = (long)(usecs % 1000000) * 1000;
+ return sleep_timespec(&ts_sleep, TRUE);
+}
+
+bool i_sleep_intr_msecs(unsigned int msecs)
+{
+ struct timespec ts_sleep;
+
+ ts_sleep.tv_sec = (time_t)(msecs / 1000);
+ ts_sleep.tv_nsec = (long)(msecs % 1000) * 1000000;
+ return sleep_timespec(&ts_sleep, TRUE);
+}
+
+bool i_sleep_intr_secs(time_t secs)
+{
+ struct timespec ts_sleep;
+
+ ts_sleep.tv_sec = secs;
+ ts_sleep.tv_nsec = 0;
+ return sleep_timespec(&ts_sleep, TRUE);
+}
diff --git a/src/lib/sleep.h b/src/lib/sleep.h
new file mode 100644
index 0000000..0f8d7c3
--- /dev/null
+++ b/src/lib/sleep.h
@@ -0,0 +1,30 @@
+#ifndef SLEEP_H
+#define SLEEP_H
+
+/* Sleep for the indicated number of microseconds. Signal interruptions are
+ handled and ignored internally. */
+void i_sleep_usecs(unsigned long long usecs);
+/* Sleep for the indicated number of milliseconds. Signal interruptions are
+ handled and ignored internally. */
+void i_sleep_msecs(unsigned int msecs);
+/* Sleep for the indicated number of seconds. Signal interruptions are
+ handled and ignored internally. */
+void i_sleep_secs(time_t secs);
+
+/* Sleep for the indicated number of microseconds while allowing signal
+ interruptions. This function returns FALSE when it is interrupted by a
+ signal. Otherwise, this function always returns TRUE. */
+bool ATTR_NOWARN_UNUSED_RESULT
+i_sleep_intr_usecs(unsigned long long usecs);
+/* Sleep for the indicated number of milliseconds while allowing signal
+ interruptions. This function returns FALSE when it is interrupted by a
+ signal. Otherwise, this function always returns TRUE. */
+bool ATTR_NOWARN_UNUSED_RESULT
+i_sleep_intr_msecs(unsigned int msecs);
+/* Sleep for the indicated number of seconds while allowing signal
+ interruptions. This function returns FALSE when it is interrupted by a
+ signal. Otherwise, this function always returns TRUE. */
+bool ATTR_NOWARN_UNUSED_RESULT
+i_sleep_intr_secs(time_t secs);
+
+#endif
diff --git a/src/lib/sort.c b/src/lib/sort.c
new file mode 100644
index 0000000..897bffb
--- /dev/null
+++ b/src/lib/sort.c
@@ -0,0 +1,17 @@
+/* Copyright (c) 2016-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "sort.h"
+
+#include <string.h>
+#include <strings.h>
+
+int bsearch_strcmp(const char *key, const char *const *member)
+{
+ return strcmp(key, *member);
+}
+
+int bsearch_strcasecmp(const char *key, const char *const *member)
+{
+ return strcasecmp(key, *member);
+}
diff --git a/src/lib/sort.h b/src/lib/sort.h
new file mode 100644
index 0000000..94cab81
--- /dev/null
+++ b/src/lib/sort.h
@@ -0,0 +1,33 @@
+#ifndef SORT_H
+#define SORT_H
+
+#define INTEGER_CMP(name, type) \
+ static inline int name(const type *i1, const type *i2) \
+ { \
+ if (*i1 < *i2) \
+ return -1; \
+ else if (*i1 > *i2) \
+ return 1; \
+ else \
+ return 0; \
+ }
+
+INTEGER_CMP(uint64_cmp, uint64_t)
+INTEGER_CMP(uint32_cmp, uint32_t)
+
+#define i_qsort(base, nmemb, size, cmp) \
+ qsort(base, nmemb, size - \
+ CALLBACK_TYPECHECK(cmp, int (*)(typeof(const typeof(*base) *), \
+ typeof(const typeof(*base) *))), \
+ (int (*)(const void *, const void *))cmp)
+
+#define i_bsearch(key, base, nmemb, size, cmp) \
+ bsearch(key, base, nmemb, size - \
+ CALLBACK_TYPECHECK(cmp, int (*)(typeof(const typeof(*key) *), \
+ typeof(const typeof(*base) *))), \
+ (int (*)(const void *, const void *))cmp)
+
+int bsearch_strcmp(const char *key, const char *const *member) ATTR_PURE;
+int bsearch_strcasecmp(const char *key, const char *const *member) ATTR_PURE;
+
+#endif
diff --git a/src/lib/stats-dist.c b/src/lib/stats-dist.c
new file mode 100644
index 0000000..882bde1
--- /dev/null
+++ b/src/lib/stats-dist.c
@@ -0,0 +1,183 @@
+/* Copyright (c) 2015-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "stats-dist.h"
+#include "sort.h"
+
+/* In order to have a vaguely accurate 95th percentile, you need way
+ more than 20 in your subsample. */
+#define TIMING_DEFAULT_SUBSAMPLING_BUFFER (20*24) /* 20*24 fits in a page */
+
+struct stats_dist {
+ unsigned int sample_count;
+ unsigned int count;
+ bool sorted;
+ uint64_t min;
+ uint64_t max;
+ uint64_t sum;
+ uint64_t samples[];
+};
+
+struct stats_dist *stats_dist_init(void)
+{
+ return stats_dist_init_with_size(TIMING_DEFAULT_SUBSAMPLING_BUFFER);
+}
+
+struct stats_dist *stats_dist_init_with_size(unsigned int sample_count)
+{
+ i_assert(sample_count > 0);
+
+ struct stats_dist *stats =
+ i_malloc(sizeof(struct stats_dist) +
+ sizeof(uint64_t) * sample_count);
+ stats->sample_count = sample_count;
+ return stats;
+}
+
+void stats_dist_deinit(struct stats_dist **_stats)
+{
+ i_free_and_null(*_stats);
+}
+
+void stats_dist_reset(struct stats_dist *stats)
+{
+ unsigned int sample_count = stats->sample_count;
+ i_zero(stats);
+ stats->sample_count = sample_count;
+}
+
+void stats_dist_add(struct stats_dist *stats, uint64_t value)
+{
+ if (stats->count < stats->sample_count) {
+ stats->samples[stats->count] = value;
+ if (stats->count == 0)
+ stats->min = stats->max = value;
+ } else {
+ unsigned int idx = i_rand_limit(stats->count);
+ if (idx < stats->sample_count)
+ stats->samples[idx] = value;
+ }
+
+ stats->count++;
+ stats->sum += value;
+ if (stats->max < value)
+ stats->max = value;
+ if (stats->min > value)
+ stats->min = value;
+ stats->sorted = FALSE;
+}
+
+unsigned int stats_dist_get_count(const struct stats_dist *stats)
+{
+ return stats->count;
+}
+
+uint64_t stats_dist_get_sum(const struct stats_dist *stats)
+{
+ return stats->sum;
+}
+
+uint64_t stats_dist_get_min(const struct stats_dist *stats)
+{
+ return stats->min;
+}
+
+uint64_t stats_dist_get_max(const struct stats_dist *stats)
+{
+ return stats->max;
+}
+
+double stats_dist_get_avg(const struct stats_dist *stats)
+{
+ if (stats->count == 0)
+ return 0;
+
+ return (double)stats->sum / stats->count;
+}
+
+static void stats_dist_ensure_sorted(struct stats_dist *stats)
+{
+ if (stats->sorted)
+ return;
+
+ unsigned int count = (stats->count < stats->sample_count)
+ ? stats->count
+ : stats->sample_count;
+ i_qsort(stats->samples, count, sizeof(*stats->samples),
+ uint64_cmp);
+ stats->sorted = TRUE;
+}
+
+uint64_t stats_dist_get_median(struct stats_dist *stats)
+{
+ if (stats->count == 0)
+ return 0;
+ /* cast-away const - reading requires sorting */
+ stats_dist_ensure_sorted(stats);
+ unsigned int count = (stats->count < stats->sample_count)
+ ? stats->count
+ : stats->sample_count;
+ unsigned int idx1 = (count-1)/2, idx2 = count/2;
+ return (stats->samples[idx1] + stats->samples[idx2]) / 2;
+}
+
+double stats_dist_get_variance(const struct stats_dist *stats)
+{
+ double sum = 0;
+ if (stats->count == 0)
+ return 0;
+
+ double avg = stats_dist_get_avg(stats);
+ double count = (stats->count < stats->sample_count)
+ ? stats->count
+ : stats->sample_count;
+
+ for(unsigned int i = 0; i < count; i++) {
+ sum += (stats->samples[i] - avg)*(stats->samples[i] - avg);
+ }
+
+ return sum / count;
+}
+
+/* This is independent of the stats framework, useful for any selection task */
+static unsigned int stats_dist_get_index(unsigned int range, double fraction)
+{
+ /* With out of range fractions, we can give the caller what
+ they probably want rather than just crashing. */
+ if (fraction >= 1.)
+ return range - 1;
+ if (fraction <= 0.)
+ return 0;
+
+ double idx_float = range * fraction;
+ unsigned int idx = idx_float; /* C defaults to rounding down */
+ idx_float -= idx;
+ /* Exact boundaries belong to the open range below them.
+ As FP isn't exact, and ratios may be specified inexactly,
+ include a small amount of fuzz around the exact boundary. */
+ if (idx_float < 1e-8*range)
+ idx--;
+
+ return idx;
+}
+
+uint64_t stats_dist_get_percentile(struct stats_dist *stats, double fraction)
+{
+ if (stats->count == 0)
+ return 0;
+ stats_dist_ensure_sorted(stats);
+ unsigned int count = (stats->count < stats->sample_count)
+ ? stats->count
+ : stats->sample_count;
+ unsigned int idx = stats_dist_get_index(count, fraction);
+ return stats->samples[idx];
+}
+
+const uint64_t *stats_dist_get_samples(const struct stats_dist *stats,
+ unsigned int *count_r)
+{
+ *count_r = (stats->count < stats->sample_count)
+ ? stats->count
+ : stats->sample_count;
+ return stats->samples;
+}
diff --git a/src/lib/stats-dist.h b/src/lib/stats-dist.h
new file mode 100644
index 0000000..2d49f72
--- /dev/null
+++ b/src/lib/stats-dist.h
@@ -0,0 +1,40 @@
+#ifndef STATS_DIST_H
+#define STATS_DIST_H
+
+struct stats_dist *stats_dist_init(void);
+struct stats_dist *stats_dist_init_with_size(unsigned int sample_count);
+void stats_dist_deinit(struct stats_dist **stats);
+
+/* Reset all events. */
+void stats_dist_reset(struct stats_dist *stats);
+
+/* Add a new event. */
+void stats_dist_add(struct stats_dist *stats, uint64_t value);
+
+/* Returns number of events added. */
+unsigned int stats_dist_get_count(const struct stats_dist *stats);
+/* Returns the sum of all events. */
+uint64_t stats_dist_get_sum(const struct stats_dist *stats);
+
+/* Returns events' minimum. */
+uint64_t stats_dist_get_min(const struct stats_dist *stats);
+/* Returns events' maximum. */
+uint64_t stats_dist_get_max(const struct stats_dist *stats);
+/* Returns events' average. */
+double stats_dist_get_avg(const struct stats_dist *stats);
+/* Returns events' approximate (through random subsampling) median. */
+uint64_t stats_dist_get_median(struct stats_dist *stats);
+/* Returns events' variance */
+double stats_dist_get_variance(const struct stats_dist *stats);
+/* Returns events' approximate (through random subsampling) percentile.
+ fraction parameter is in the range (0., 1.], so 95th %-ile is 0.95. */
+uint64_t stats_dist_get_percentile(struct stats_dist *stats, double fraction);
+/* Returns events' approximate (through random subsampling) 95th percentile. */
+static inline uint64_t stats_dist_get_95th(struct stats_dist *stats)
+{
+ return stats_dist_get_percentile(stats, 0.95);
+}
+/* Returns the sample array */
+const uint64_t *stats_dist_get_samples(const struct stats_dist *stats,
+ unsigned int *count_r);
+#endif
diff --git a/src/lib/str-find.c b/src/lib/str-find.c
new file mode 100644
index 0000000..56d8069
--- /dev/null
+++ b/src/lib/str-find.c
@@ -0,0 +1,183 @@
+/* Copyright (c) 2007-2018 Dovecot authors, see the included COPYING file */
+
+/* @UNSAFE: whole file */
+
+#include "lib.h"
+#include "str-find.h"
+
+struct str_find_context {
+ pool_t pool;
+ unsigned char *key;
+ unsigned int key_len;
+
+ unsigned int *matches;
+ unsigned int match_count;
+
+ size_t match_end_pos;
+
+ int badtab[UCHAR_MAX+1];
+ int goodtab[FLEXIBLE_ARRAY_MEMBER];
+};
+
+static void init_badtab(struct str_find_context *ctx)
+{
+ unsigned int i, len_1 = ctx->key_len - 1;
+
+ for (i = 0; i <= UCHAR_MAX; i++)
+ ctx->badtab[i] = ctx->key_len;
+
+ for (i = 0; i < len_1; i++)
+ ctx->badtab[ctx->key[i]] = len_1 - i;
+}
+
+static void init_suffixes(struct str_find_context *ctx, unsigned int *suffixes)
+{
+ unsigned int len_1 = ctx->key_len - 1;
+ int f = 0, g, i;
+
+ suffixes[len_1] = ctx->key_len;
+ g = len_1;
+ for (i = (int)ctx->key_len - 2; i >= 0; i--) {
+ if (i > g && (int)suffixes[i + len_1 - f] < i - g)
+ suffixes[i] = suffixes[i + len_1 - f];
+ else {
+ if (i < g)
+ g = i;
+ f = i;
+ while (g >= 0 && ctx->key[g] == ctx->key[g + len_1 - f])
+ g--;
+ suffixes[i] = f - g;
+ }
+ }
+}
+
+static void init_goodtab(struct str_find_context *ctx)
+{
+ unsigned int *suffixes;
+ int j, i, len_1 = ctx->key_len - 1;
+
+ suffixes = t_buffer_get(sizeof(*suffixes) * ctx->key_len);
+ init_suffixes(ctx, suffixes);
+
+ for (i = 0; i < (int)ctx->key_len; i++)
+ ctx->goodtab[i] = ctx->key_len;
+
+ j = 0;
+ for (i = len_1; i >= -1; i--) {
+ if (i < 0 || suffixes[i] == (unsigned int)i + 1) {
+ for (; j < len_1 - i; j++) {
+ if (ctx->goodtab[j] == (int)ctx->key_len)
+ ctx->goodtab[j] = len_1 - i;
+ }
+ }
+ }
+ for (i = 0; i <= (int)ctx->key_len - 2; i++)
+ ctx->goodtab[len_1 - suffixes[i]] = len_1 - i;
+}
+
+struct str_find_context *str_find_init(pool_t pool, const char *key)
+{
+ struct str_find_context *ctx;
+ size_t key_len = strlen(key);
+
+ i_assert(key_len > 0);
+ i_assert(key_len < INT_MAX);
+
+ ctx = p_malloc(pool, MALLOC_ADD(sizeof(struct str_find_context),
+ MALLOC_MULTIPLY(sizeof(ctx->goodtab[0]), key_len)));
+ ctx->pool = pool;
+ ctx->matches = p_new(pool, unsigned int, key_len);
+ ctx->key_len = key_len;
+ ctx->key = p_malloc(pool, key_len);
+ memcpy(ctx->key, key, key_len);
+
+ init_goodtab(ctx);
+ init_badtab(ctx);
+ return ctx;
+}
+
+void str_find_deinit(struct str_find_context **_ctx)
+{
+ struct str_find_context *ctx = *_ctx;
+
+ *_ctx = NULL;
+ p_free(ctx->pool, ctx->matches);
+ p_free(ctx->pool, ctx->key);
+ p_free(ctx->pool, ctx);
+}
+
+bool str_find_more(struct str_find_context *ctx,
+ const unsigned char *data, size_t size)
+{
+ unsigned int key_len = ctx->key_len;
+ unsigned int i, j, a, b;
+ int bad_value;
+
+ for (i = j = 0; i < ctx->match_count; i++) {
+ a = ctx->matches[i];
+ if (ctx->matches[i] + size >= key_len) {
+ /* we can fully determine this match now */
+ for (; a < key_len; a++) {
+ if (ctx->key[a] != data[a - ctx->matches[i]])
+ break;
+ }
+
+ if (a == key_len) {
+ ctx->match_end_pos = key_len - ctx->matches[i];
+ return TRUE;
+ }
+ } else {
+ for (b = 0; b < size; b++) {
+ if (ctx->key[a+b] != data[b])
+ break;
+ }
+
+ if (b == size)
+ ctx->matches[j++] = a + size;
+ }
+ }
+ if (j > 0) {
+ i_assert(j + size < key_len);
+ ctx->match_count = j;
+ j = 0;
+ } else {
+ /* Boyer-Moore searching */
+ j = 0;
+ while (j + key_len <= size) {
+ i = key_len - 1;
+ while (ctx->key[i] == data[i + j]) {
+ if (i == 0) {
+ ctx->match_end_pos = j + key_len;
+ return TRUE;
+ }
+ i--;
+ }
+
+ bad_value = (int)(ctx->badtab[data[i + j]] + i + 1) -
+ (int)key_len;
+ j += I_MAX(ctx->goodtab[i], bad_value);
+ }
+ i_assert(j <= size);
+ ctx->match_count = 0;
+ }
+
+ for (; j < size; j++) {
+ for (i = j; i < size; i++) {
+ if (ctx->key[i-j] != data[i])
+ break;
+ }
+ if (i == size)
+ ctx->matches[ctx->match_count++] = size - j;
+ }
+ return FALSE;
+}
+
+size_t str_find_get_match_end_pos(struct str_find_context *ctx)
+{
+ return ctx->match_end_pos;
+}
+
+void str_find_reset(struct str_find_context *ctx)
+{
+ ctx->match_count = 0;
+}
diff --git a/src/lib/str-find.h b/src/lib/str-find.h
new file mode 100644
index 0000000..9ab8f37
--- /dev/null
+++ b/src/lib/str-find.h
@@ -0,0 +1,20 @@
+#ifndef STR_FIND_H
+#define STR_FIND_H
+
+struct str_find_context;
+
+struct str_find_context *str_find_init(pool_t pool, const char *key);
+void str_find_deinit(struct str_find_context **ctx);
+
+/* Returns TRUE if key is found. It's possible to send the data in arbitrary
+ blocks and have the key still match. */
+bool str_find_more(struct str_find_context *ctx,
+ const unsigned char *data, size_t size);
+/* After str_find_more() has returned TRUE, this function returns the end
+ position in the previous data block where the key had matched. */
+size_t str_find_get_match_end_pos(struct str_find_context *ctx);
+/* Reset input data. The next str_find_more() call won't try to match the key
+ to earlier data. */
+void str_find_reset(struct str_find_context *ctx);
+
+#endif
diff --git a/src/lib/str-sanitize.c b/src/lib/str-sanitize.c
new file mode 100644
index 0000000..859640a
--- /dev/null
+++ b/src/lib/str-sanitize.c
@@ -0,0 +1,165 @@
+/* Copyright (c) 2004-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "unichar.h"
+#include "str.h"
+#include "str-sanitize.h"
+
+static size_t str_sanitize_skip_start(const char *src, size_t max_bytes)
+{
+ unichar_t chr;
+ size_t i;
+
+ for (i = 0; i < max_bytes && src[i] != '\0'; ) {
+ int len = uni_utf8_get_char_n(src+i, max_bytes-i, &chr);
+ if (len <= 0)
+ break;
+ if ((unsigned char)src[i] < 32)
+ break;
+ i += len;
+ }
+ i_assert(i <= max_bytes);
+ return i;
+}
+
+
+static size_t
+str_sanitize_skip_start_utf8(const char *src, uintmax_t max_chars)
+{
+ unichar_t chr;
+ uintmax_t c;
+ size_t i;
+
+ for (i = 0, c = 0; c < max_chars && src[i] != '\0'; ) {
+ int len = uni_utf8_get_char(src+i, &chr);
+ if (len <= 0)
+ break;
+ if ((unsigned char)src[i] < 32)
+ break;
+ c++;
+ i += len;
+ }
+ i_assert(c <= max_chars);
+ return i;
+}
+
+static void str_sanitize_truncate_char(string_t *dest, unsigned int initial_pos)
+{
+ const unsigned char *data = str_data(dest);
+ size_t len = str_len(dest);
+
+ i_assert(len >= initial_pos);
+ if (len == initial_pos)
+ return;
+
+ data += initial_pos;
+ len -= initial_pos;
+ str_truncate(dest, initial_pos +
+ uni_utf8_data_truncate(data, len, len-1));
+}
+
+void str_sanitize_append(string_t *dest, const char *src, size_t max_bytes)
+{
+ size_t initial_pos = str_len(dest);
+ unichar_t chr;
+ size_t i;
+
+ for (i = 0; i < max_bytes && src[i] != '\0'; ) {
+ int len = uni_utf8_get_char_n(src+i, max_bytes-i, &chr);
+ if (len == 0)
+ break; /* input ended too early */
+
+ if (len < 0) {
+ /* invalid UTF-8 */
+ str_append_c(dest, '?');
+ i++;
+ continue;
+ }
+ if ((unsigned char)src[i] < 32)
+ str_append_c(dest, '?');
+ else
+ str_append_data(dest, src+i, len);
+ i += len;
+ }
+
+ if (src[i] != '\0') {
+ if (max_bytes < 3)
+ str_truncate(dest, initial_pos);
+ else {
+ while (str_len(dest) - initial_pos > max_bytes-3)
+ str_sanitize_truncate_char(dest, initial_pos);
+ }
+ str_append(dest, "...");
+ }
+}
+
+void str_sanitize_append_utf8(string_t *dest, const char *src,
+ uintmax_t max_cps)
+{
+ size_t last_pos = 0;
+ unichar_t chr;
+ uintmax_t c;
+ size_t i;
+
+ i_assert(max_cps > 0);
+
+ for (i = 0, c = 0; c < max_cps && src[i] != '\0'; ) {
+ int len = uni_utf8_get_char(src+i, &chr);
+ if (len == 0)
+ break; /* input ended too early */
+
+ last_pos = str_len(dest);
+ if (len < 0) {
+ /* invalid UTF-8 */
+ str_append(dest, UNICODE_REPLACEMENT_CHAR_UTF8);
+ i++;
+ continue;
+ }
+ if ((unsigned char)src[i] < 32)
+ str_append(dest, UNICODE_REPLACEMENT_CHAR_UTF8);
+ else
+ str_append_data(dest, src+i, len);
+ i += len;
+ c++;
+ }
+
+ if (src[i] != '\0') {
+ str_truncate(dest, last_pos);
+ str_append(dest, UNICODE_HORIZONTAL_ELLIPSIS_CHAR_UTF8);
+ }
+}
+
+const char *str_sanitize(const char *src, size_t max_bytes)
+{
+ string_t *str;
+ size_t i;
+
+ if (src == NULL)
+ return NULL;
+
+ i = str_sanitize_skip_start(src, max_bytes);
+ if (src[i] == '\0')
+ return src;
+
+ str = t_str_new(I_MIN(max_bytes, 256));
+ str_sanitize_append(str, src, max_bytes);
+ return str_c(str);
+}
+
+const char *str_sanitize_utf8(const char *src, uintmax_t max_cps)
+{
+ string_t *str;
+ size_t i;
+
+ if (src == NULL)
+ return NULL;
+
+ i = str_sanitize_skip_start_utf8(src, max_cps);
+ if (src[i] == '\0')
+ return src;
+
+ str = t_str_new(I_MIN(max_cps, 256));
+ str_sanitize_append_utf8(str, src, max_cps);
+ return str_c(str);
+}
+
diff --git a/src/lib/str-sanitize.h b/src/lib/str-sanitize.h
new file mode 100644
index 0000000..c1b2fce
--- /dev/null
+++ b/src/lib/str-sanitize.h
@@ -0,0 +1,22 @@
+#ifndef STR_SANITIZE_H
+#define STR_SANITIZE_H
+
+/* All control characters in src will be appended as '?'. If src is longer
+ than max_bytes, it's truncated with "..." appended to the end. Note that
+ src is treated as UTF-8 input, but max_bytes is in bytes instead of
+ UTF-8 characters. */
+void str_sanitize_append(string_t *dest, const char *src, size_t max_bytes);
+/* All control characters in src will be appended as the unicode replacement
+ character (U+FFFD). If src has more than max_cps unicode code points, it's
+ truncated with a horizontal ellipsis character (U+2026) appended to the end.
+ */
+void str_sanitize_append_utf8(string_t *dest, const char *src,
+ uintmax_t max_cps);
+/* Return src sanitized. If there are no changes, src pointer is returned.
+ If src is NULL, returns NULL. */
+const char *str_sanitize(const char *src, size_t max_bytes);
+/* The unicode version of str_sanitize() using str_sanitize_append_utf8()
+ internally. */
+const char *str_sanitize_utf8(const char *src, uintmax_t max_cps);
+
+#endif
diff --git a/src/lib/str-table.c b/src/lib/str-table.c
new file mode 100644
index 0000000..10eddd6
--- /dev/null
+++ b/src/lib/str-table.c
@@ -0,0 +1,78 @@
+/* Copyright (c) 2014-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "hash.h"
+#include "str-table.h"
+
+struct str_table {
+ HASH_TABLE(char *, void *) hash;
+};
+
+struct str_table *str_table_init(void)
+{
+ struct str_table *table;
+
+ table = i_new(struct str_table, 1);
+ hash_table_create(&table->hash, default_pool, 0, str_hash, strcmp);
+ return table;
+}
+
+void str_table_deinit(struct str_table **_table)
+{
+ struct str_table *table = *_table;
+ struct hash_iterate_context *iter;
+ char *key;
+ void *value;
+
+ *_table = NULL;
+
+ iter = hash_table_iterate_init(table->hash);
+ while (hash_table_iterate(iter, table->hash, &key, &value))
+ i_free(key);
+ hash_table_iterate_deinit(&iter);
+ hash_table_destroy(&table->hash);
+ i_free(table);
+}
+
+bool str_table_is_empty(struct str_table *table)
+{
+ return hash_table_count(table->hash) == 0;
+}
+
+const char *str_table_ref(struct str_table *table, const char *str)
+{
+ char *key;
+ void *value;
+ unsigned int ref;
+
+ if (!hash_table_lookup_full(table->hash, str, &key, &value)) {
+ key = i_strdup(str);
+ ref = 1;
+ } else {
+ ref = POINTER_CAST_TO(value, unsigned int);
+ i_assert(ref > 0);
+ ref++;
+ }
+ hash_table_update(table->hash, key, POINTER_CAST(ref));
+ return key;
+}
+
+void str_table_unref(struct str_table *table, const char **str)
+{
+ char *key;
+ void *value;
+ unsigned int ref;
+
+ if (!hash_table_lookup_full(table->hash, *str, &key, &value))
+ i_unreached();
+
+ ref = POINTER_CAST_TO(value, unsigned int);
+ i_assert(ref > 0);
+ if (--ref > 0)
+ hash_table_update(table->hash, key, POINTER_CAST(ref));
+ else {
+ hash_table_remove(table->hash, key);
+ i_free(key);
+ }
+ *str = NULL;
+}
diff --git a/src/lib/str-table.h b/src/lib/str-table.h
new file mode 100644
index 0000000..55111a6
--- /dev/null
+++ b/src/lib/str-table.h
@@ -0,0 +1,19 @@
+#ifndef STR_TABLE_H
+#define STR_TABLE_H
+
+/* Hash table containing string -> refcount. */
+
+struct str_table *str_table_init(void);
+void str_table_deinit(struct str_table **table);
+
+/* Returns TRUE if there are no referenced strings in the table. */
+bool str_table_is_empty(struct str_table *table);
+
+/* Return string allocated from the strtable and increase its reference
+ count. */
+const char *str_table_ref(struct str_table *table, const char *str);
+/* Decrease string's reference count, freeing it if it reaches zero.
+ The str pointer must have been returned by the str_table_ref(). */
+void str_table_unref(struct str_table *table, const char **str);
+
+#endif
diff --git a/src/lib/str.c b/src/lib/str.c
new file mode 100644
index 0000000..2ec5970
--- /dev/null
+++ b/src/lib/str.c
@@ -0,0 +1,158 @@
+/* Copyright (c) 2002-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "buffer.h"
+#include "printf-format-fix.h"
+#include "unichar.h"
+#include "str.h"
+
+#include <stdio.h>
+
+string_t *str_new(pool_t pool, size_t initial_size)
+{
+ /* never allocate a 0 byte size buffer. this is especially important
+ when str_c() is called on an empty string from a different stack
+ frame (see the comment in buffer.c about this). */
+ return buffer_create_dynamic(pool, I_MAX(initial_size, 1));
+}
+
+string_t *str_new_const(pool_t pool, const char *str, size_t len)
+{
+ string_t *ret;
+
+ i_assert(str[len] == '\0');
+
+ ret = p_new(pool, buffer_t, 1);
+ buffer_create_from_const_data(ret, str, len + 1);
+ str_truncate(ret, len);
+ return ret;
+}
+
+string_t *t_str_new(size_t initial_size)
+{
+ return str_new(pool_datastack_create(), initial_size);
+}
+
+string_t *t_str_new_const(const char *str, size_t len)
+{
+ return str_new_const(pool_datastack_create(), str, len);
+}
+
+void str_free(string_t **str)
+{
+ if (str == NULL || *str == NULL)
+ return;
+
+ buffer_free(str);
+}
+
+static void str_add_nul(string_t *str)
+{
+ const unsigned char *data = str_data(str);
+ size_t len = str_len(str);
+ size_t alloc = buffer_get_size(str);
+
+ if (len == alloc || data[len] != '\0') {
+ buffer_write(str, len, "", 1);
+ /* remove the \0 - we don't want to keep it */
+ buffer_set_used_size(str, len);
+ }
+}
+
+char *str_free_without_data(string_t **str)
+{
+ str_add_nul(*str);
+ return buffer_free_without_data(str);
+}
+
+const char *str_c(string_t *str)
+{
+ str_add_nul(str);
+ return str->data;
+}
+
+char *str_c_modifiable(string_t *str)
+{
+ str_add_nul(str);
+ return buffer_get_modifiable_data(str, NULL);
+}
+
+bool str_equals(const string_t *str1, const string_t *str2)
+{
+ if (str1->used != str2->used)
+ return FALSE;
+
+ return memcmp(str1->data, str2->data, str1->used) == 0;
+}
+
+void str_append_max(string_t *str, const char *cstr, size_t max_len)
+{
+ const char *p;
+ size_t len;
+
+ p = memchr(cstr, '\0', max_len);
+ if (p == NULL)
+ len = max_len;
+ else
+ len = p - (const char *)cstr;
+ buffer_append(str, cstr, len);
+}
+
+void str_printfa(string_t *str, const char *fmt, ...)
+{
+ va_list args;
+
+ va_start(args, fmt);
+ str_vprintfa(str, fmt, args);
+ va_end(args);
+}
+
+void str_vprintfa(string_t *str, const char *fmt, va_list args)
+{
+#define SNPRINTF_INITIAL_EXTRA_SIZE 128
+ va_list args2;
+ char *tmp;
+ size_t init_size;
+ size_t pos = str->used;
+ int ret, ret2;
+
+ VA_COPY(args2, args);
+
+ /* the format string is modified only if %m exists in it. it happens
+ only in error conditions, so don't try to t_push() here since it'll
+ just slow down the normal code path. */
+ fmt = printf_format_fix_get_len(fmt, &init_size);
+ init_size += SNPRINTF_INITIAL_EXTRA_SIZE;
+
+ /* @UNSAFE */
+ if (pos+init_size > buffer_get_writable_size(str) &&
+ pos < buffer_get_writable_size(str)) {
+ /* avoid growing buffer larger if possible. this is also
+ required if buffer isn't dynamically growing. */
+ init_size = buffer_get_writable_size(str)-pos;
+ }
+ tmp = buffer_get_space_unsafe(str, pos, init_size);
+ ret = vsnprintf(tmp, init_size, fmt, args);
+ i_assert(ret >= 0);
+
+ if ((unsigned int)ret >= init_size) {
+ /* didn't fit with the first guess. now we know the size,
+ so try again. */
+ tmp = buffer_get_space_unsafe(str, pos, ret + 1);
+ ret2 = vsnprintf(tmp, ret + 1, fmt, args2);
+ i_assert(ret2 == ret);
+ }
+ va_end(args2);
+
+ /* drop the unused data, including terminating NUL */
+ buffer_set_used_size(str, pos + ret);
+}
+
+void str_truncate_utf8(string_t *str, size_t len)
+{
+ size_t size = str_len(str);
+
+ if (size <= len)
+ return;
+ str_truncate(str, uni_utf8_data_truncate(str_data(str), size, len));
+}
diff --git a/src/lib/str.h b/src/lib/str.h
new file mode 100644
index 0000000..f0ec8f1
--- /dev/null
+++ b/src/lib/str.h
@@ -0,0 +1,100 @@
+#ifndef STR_H
+#define STR_H
+
+#include "buffer.h"
+
+string_t *str_new(pool_t pool, size_t initial_size);
+string_t *t_str_new(size_t initial_size);
+/* Allocate a constant string using the given str as the input data.
+ str pointer is saved directly, so it must not be freed until the returned
+ string is no longer used. len must contain strlen(str). */
+string_t *str_new_const(pool_t pool, const char *str, size_t len);
+string_t *t_str_new_const(const char *str, size_t len);
+void str_free(string_t **str);
+char *str_free_without_data(string_t **str);
+
+const char *str_c(string_t *str);
+char *str_c_modifiable(string_t *str);
+bool str_equals(const string_t *str1, const string_t *str2) ATTR_PURE;
+
+static inline const unsigned char *str_data(const string_t *str)
+{
+ return (const unsigned char*)str->data;
+}
+static inline size_t str_len(const string_t *str)
+{
+ return str->used;
+}
+
+/* Append NUL-terminated string. If the trailing NUL isn't found earlier,
+ append a maximum of max_len characters. */
+void str_append_max(string_t *str, const char *cstr, size_t max_len);
+
+static inline void str_append(string_t *str, const char *cstr)
+{
+ buffer_append(str, cstr, strlen(cstr));
+}
+static inline void str_append_data(string_t *str, const void *data, size_t len)
+{
+ buffer_append(str, data, len);
+}
+
+static inline void str_append_c(string_t *str, unsigned char chr)
+{
+ buffer_append_c(str, chr);
+}
+/* This macro ensures we add unsigned char to str to avoid
+ implicit casts which cause errors with clang's implicit integer truncation
+ sanitizier. Issues caught by these sanitizers are not undefined behavior,
+ but are often unintentional.
+ We also need to check that the type we are adding is compatible with char,
+ so that we don't end up doing a narrowing cast. */
+#ifdef HAVE_TYPE_CHECKS
+# define str_append_c(str, chr) \
+ str_append_c((str), __builtin_choose_expr( \
+ __builtin_types_compatible_p(typeof((chr)), char), \
+ (unsigned char)(chr), (chr)))
+#endif
+
+static inline void str_append_str(string_t *dest, const string_t *src)
+{
+ buffer_append(dest, src->data, src->used);
+}
+
+/* Append printf()-like data */
+void str_printfa(string_t *str, const char *fmt, ...)
+ ATTR_FORMAT(2, 3);
+void str_vprintfa(string_t *str, const char *fmt, va_list args)
+ ATTR_FORMAT(2, 0);
+
+static inline void str_insert(string_t *str, size_t pos, const char *cstr)
+{
+ buffer_insert(str, pos, cstr, strlen(cstr));
+}
+
+static inline void str_delete(string_t *str, size_t pos, size_t len)
+{
+ buffer_delete(str, pos, len);
+}
+
+static inline void str_replace(string_t *str, size_t pos, size_t len,
+ const char *cstr)
+{
+ buffer_replace(str, pos, len, cstr, strlen(cstr));
+}
+
+/* Truncate the string to specified length. If it's already smaller,
+ do nothing. */
+static inline void str_truncate(string_t *str, size_t len)
+{
+ if (str_len(str) > len)
+ buffer_set_used_size(str, len);
+}
+
+/* Truncate the string to specified length, but also make sure the truncation
+ doesn't happen in the middle of an UTF-8 character sequence. In that case,
+ the string will end up being up to a few bytes smaller than len. If it's
+ already smaller to begin with, do nothing. */
+void str_truncate_utf8(string_t *str, size_t len);
+
+#endif
diff --git a/src/lib/strescape.c b/src/lib/strescape.c
new file mode 100644
index 0000000..bfa4473
--- /dev/null
+++ b/src/lib/strescape.c
@@ -0,0 +1,358 @@
+/* Copyright (c) 2003-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "str.h"
+#include "strescape.h"
+
+const char *str_nescape(const void *str, size_t len)
+{
+ string_t *dest = t_str_new(len*2);
+ str_append_escaped(dest, str, len);
+ return str_c(dest);
+}
+
+void str_append_escaped(string_t *dest, const void *src, size_t src_size)
+{
+ const unsigned char *pstart = src, *p = src, *pend = pstart + src_size;
+ /* see if we need to quote it */
+ for (; p < pend; p++) {
+ if (IS_ESCAPED_CHAR(*p))
+ break;
+ }
+
+ /* quote */
+ str_append_data(dest, pstart, (size_t)(p - pstart));
+
+ for (; p < pend; p++) {
+ if (IS_ESCAPED_CHAR(*p))
+ str_append_c(dest, '\\');
+ str_append_data(dest, p, 1);
+ }
+}
+
+void str_append_unescaped(string_t *dest, const void *src, size_t src_size)
+{
+ const unsigned char *src_c = src;
+ size_t start = 0, i = 0;
+
+ while (i < src_size) {
+ for (; i < src_size; i++) {
+ if (src_c[i] == '\\')
+ break;
+ }
+
+ str_append_data(dest, src_c + start, i-start);
+
+ if (i < src_size) {
+ if (++i == src_size)
+ break;
+ str_append_c(dest, src_c[i++]);
+ }
+ start = i;
+ }
+}
+
+char *str_unescape(char *str)
+{
+ /* @UNSAFE */
+ char *dest, *start = str;
+
+ while (*str != '\\') {
+ if (*str == '\0')
+ return start;
+ str++;
+ }
+
+ for (dest = str; *str != '\0'; str++) {
+ if (*str == '\\') {
+ str++;
+ if (*str == '\0')
+ break;
+ }
+
+ *dest++ = *str;
+ }
+
+ *dest = '\0';
+ return start;
+}
+
+int str_unescape_next(const char **str, const char **unescaped_r)
+{
+ const char *p;
+ char *escaped;
+ bool esc_found = FALSE;
+
+ for (p = *str; *p != '\0'; p++) {
+ if (*p == '"')
+ break;
+ else if (*p == '\\') {
+ if (p[1] == '\0')
+ return -1;
+ esc_found = TRUE;
+ p++;
+ }
+ }
+ if (*p != '"')
+ return -1;
+ escaped = p_strdup_until(unsafe_data_stack_pool, *str, p);
+ *str = p+1;
+ *unescaped_r = !esc_found ? escaped : str_unescape(escaped);
+ return 0;
+}
+
+void str_append_tabescaped_n(string_t *dest, const unsigned char *src, size_t src_size)
+{
+ size_t prev_pos = 0;
+ char esc[2] = { '\001', '\0' };
+
+ for (size_t i = 0; i < src_size; i++) {
+ switch (src[i]) {
+ case '\000':
+ esc[1] = '0';
+ break;
+ case '\001':
+ esc[1] = '1';
+ break;
+ case '\t':
+ esc[1] = 't';
+ break;
+ case '\r':
+ esc[1] = 'r';
+ break;
+ case '\n':
+ esc[1] = 'n';
+ break;
+ default:
+ continue;
+ }
+ str_append_data(dest, src + prev_pos, i - prev_pos);
+ str_append_data(dest, esc, 2);
+ prev_pos = i + 1;
+ }
+ str_append_data(dest, src + prev_pos, src_size - prev_pos);
+}
+
+void str_append_tabescaped(string_t *dest, const char *src)
+{
+ size_t pos, prev_pos = 0;
+ char esc[2] = { '\001', '\0' };
+
+ for (;;) {
+ pos = prev_pos + strcspn(src + prev_pos, "\001\t\r\n");
+ str_append_data(dest, src + prev_pos, pos - prev_pos);
+ prev_pos = pos + 1;
+
+ switch (src[pos]) {
+ case '\000':
+ /* end of src string reached */
+ return;
+ case '\001':
+ esc[1] = '1';
+ break;
+ case '\t':
+ esc[1] = 't';
+ break;
+ case '\r':
+ esc[1] = 'r';
+ break;
+ case '\n':
+ esc[1] = 'n';
+ break;
+ default:
+ i_unreached();
+ }
+ str_append_data(dest, esc, 2);
+ }
+}
+
+
+const char *str_tabescape(const char *str)
+{
+ string_t *tmp;
+ const char *p;
+
+ if ((p = strpbrk(str, "\001\t\r\n")) != NULL) {
+ tmp = t_str_new(128);
+ str_append_data(tmp, str, p-str);
+ str_append_tabescaped(tmp, p);
+ return str_c(tmp);
+ }
+ return str;
+}
+
+void str_append_tabunescaped(string_t *dest, const void *src, size_t src_size)
+{
+ const unsigned char *src_c = src;
+ size_t start = 0, i = 0;
+
+ while (i < src_size) {
+ for (; i < src_size; i++) {
+ if (src_c[i] == '\001')
+ break;
+ }
+
+ str_append_data(dest, src_c + start, i-start);
+
+ if (i < src_size) {
+ i++;
+ if (i < src_size) {
+ switch (src_c[i]) {
+ case '0':
+ str_append_c(dest, '\000');
+ break;
+ case '1':
+ str_append_c(dest, '\001');
+ break;
+ case 't':
+ str_append_c(dest, '\t');
+ break;
+ case 'r':
+ str_append_c(dest, '\r');
+ break;
+ case 'n':
+ str_append_c(dest, '\n');
+ break;
+ default:
+ str_append_c(dest, src_c[i]);
+ break;
+ }
+ i++;
+ }
+ }
+ start = i;
+ }
+}
+
+static char *str_tabunescape_from(char *str, char *src)
+{
+ /* @UNSAFE */
+ char *dest, *p;
+
+ dest = src;
+ for (;;) {
+ switch (src[1]) {
+ case '\0':
+ /* truncated input */
+ *dest = '\0';
+ return str;
+ case '0':
+ *dest++ = '\000';
+ break;
+ case '1':
+ *dest++ = '\001';
+ break;
+ case 't':
+ *dest++ = '\t';
+ break;
+ case 'r':
+ *dest++ = '\r';
+ break;
+ case 'n':
+ *dest++ = '\n';
+ break;
+ default:
+ *dest++ = src[1];
+ break;
+ }
+ src += 2;
+
+ p = strchr(src, '\001');
+ if (p == NULL) {
+ memmove(dest, src, strlen(src)+1);
+ break;
+ }
+
+ size_t copy_len = p - src;
+ memmove(dest, src, copy_len);
+ dest += copy_len;
+ src = p;
+ }
+ return str;
+}
+
+char *str_tabunescape(char *str)
+{
+ char *src = strchr(str, '\001');
+ if (src == NULL) {
+ /* no unescaping needed */
+ return str;
+ }
+ return str_tabunescape_from(str, src);
+}
+
+const char *t_str_tabunescape(const char *str)
+{
+ const char *p;
+
+ p = strchr(str, '\001');
+ if (p == NULL)
+ return str;
+
+ char *dest = t_strdup_noconst(str);
+ return str_tabunescape_from(dest, dest + (p - str));
+}
+
+static char **p_strsplit_tabescaped_inplace(pool_t pool, char *data)
+{
+ /* @UNSAFE */
+ char **array;
+ unsigned int count, new_alloc_count, alloc_count;
+
+ if (*data == '\0')
+ return p_new(pool, char *, 1);
+
+ alloc_count = 32;
+ array = pool == unsafe_data_stack_pool ?
+ t_malloc_no0(sizeof(char *) * alloc_count) :
+ p_malloc(pool, sizeof(char *) * alloc_count);
+
+ array[0] = data; count = 1;
+ char *need_unescape = NULL;
+ while ((data = strpbrk(data, "\t\001")) != NULL) {
+ /* separator or escape char found */
+ if (*data == '\001') {
+ if (need_unescape == NULL)
+ need_unescape = data;
+ data++;
+ continue;
+ }
+ if (count+1 >= alloc_count) {
+ new_alloc_count = nearest_power(alloc_count+1);
+ array = p_realloc(pool, array,
+ sizeof(char *) * alloc_count,
+ sizeof(char *) *
+ new_alloc_count);
+ alloc_count = new_alloc_count;
+ }
+ *data++ = '\0';
+ if (need_unescape != NULL) {
+ str_tabunescape_from(array[count-1], need_unescape);
+ need_unescape = NULL;
+ }
+ array[count++] = data;
+ }
+ if (need_unescape != NULL)
+ str_tabunescape_from(array[count-1], need_unescape);
+ i_assert(count < alloc_count);
+ array[count] = NULL;
+
+ return array;
+}
+
+const char *const *t_strsplit_tabescaped_inplace(char *data)
+{
+ char *const *escaped =
+ p_strsplit_tabescaped_inplace(unsafe_data_stack_pool, data);
+ return (const char *const *)escaped;
+}
+
+char **p_strsplit_tabescaped(pool_t pool, const char *str)
+{
+ return p_strsplit_tabescaped_inplace(pool, p_strdup(pool, str));
+}
+
+const char *const *t_strsplit_tabescaped(const char *str)
+{
+ return t_strsplit_tabescaped_inplace(t_strdup_noconst(str));
+}
diff --git a/src/lib/strescape.h b/src/lib/strescape.h
new file mode 100644
index 0000000..1381650
--- /dev/null
+++ b/src/lib/strescape.h
@@ -0,0 +1,43 @@
+#ifndef STRESCAPE_H
+#define STRESCAPE_H
+
+#define IS_ESCAPED_CHAR(c) ((c) == '"' || (c) == '\\' || (c) == '\'')
+
+/* escape all '\', '"' and "'" characters,
+ this is nul safe */
+const char *str_nescape(const void *str, size_t len);
+
+/* escape string */
+static inline const char *str_escape(const char *str)
+{
+ return str_nescape(str, strlen(str));
+}
+
+void str_append_escaped(string_t *dest, const void *src, size_t src_size);
+/* remove all '\' characters, append to given string */
+void str_append_unescaped(string_t *dest, const void *src, size_t src_size);
+
+/* remove all '\' characters */
+char *str_unescape(char *str);
+
+/* Remove all '\' chars from str until '"' is reached and return the unescaped
+ string. *str is updated to point to the character after the '"'. Returns 0
+ if ok, -1 if '"' wasn't found. */
+int str_unescape_next(const char **str, const char **unescaped_r);
+
+/* For Dovecot's internal protocols: Escape \001, \t, \r and \n characters
+ using \001. */
+const char *str_tabescape(const char *str);
+void str_append_tabescaped(string_t *dest, const char *src);
+void str_append_tabescaped_n(string_t *dest, const unsigned char *src, size_t src_size);
+void str_append_tabunescaped(string_t *dest, const void *src, size_t src_size);
+char *str_tabunescape(char *str);
+const char *t_str_tabunescape(const char *str);
+
+char **p_strsplit_tabescaped(pool_t pool, const char *str);
+const char *const *t_strsplit_tabescaped(const char *str);
+/* Same as t_strsplit_tabescaped(), but the input string is modified and the
+ returned pointers inside the array point to the original string. */
+const char *const *t_strsplit_tabescaped_inplace(char *str);
+
+#endif
diff --git a/src/lib/strfuncs.c b/src/lib/strfuncs.c
new file mode 100644
index 0000000..56e4576
--- /dev/null
+++ b/src/lib/strfuncs.c
@@ -0,0 +1,945 @@
+/* Copyright (c) 2002-2018 Dovecot authors, see the included COPYING file */
+
+/* @UNSAFE: whole file */
+
+#include "lib.h"
+#include "str.h"
+#include "printf-format-fix.h"
+#include "strfuncs.h"
+#include "array.h"
+
+#include <stdio.h>
+#include <limits.h>
+#include <ctype.h>
+
+#define STRCONCAT_BUFSIZE 512
+
+enum _str_trim_sides {
+ STR_TRIM_LEFT = BIT(0),
+ STR_TRIM_RIGHT = BIT(1),
+};
+
+const unsigned char uchar_nul = '\0';
+const unsigned char *uchar_empty_ptr = &uchar_nul;
+
+volatile int timing_safety_unoptimization;
+
+int i_snprintf(char *dest, size_t max_chars, const char *format, ...)
+{
+ va_list args;
+ int ret;
+
+ i_assert(max_chars < INT_MAX);
+
+ va_start(args, format);
+ ret = vsnprintf(dest, max_chars, printf_format_fix_unsafe(format),
+ args);
+ va_end(args);
+
+ i_assert(ret >= 0);
+ return (unsigned int)ret < max_chars ? 0 : -1;
+}
+
+char *p_strdup(pool_t pool, const char *str)
+{
+ void *mem;
+ size_t len;
+
+ if (str == NULL)
+ return NULL;
+
+ len = strlen(str) + 1;
+ mem = p_malloc(pool, len);
+ memcpy(mem, str, len);
+ return mem;
+}
+
+void *p_memdup(pool_t pool, const void *data, size_t size)
+{
+ void *mem;
+
+ mem = p_malloc(pool, size);
+ memcpy(mem, data, size);
+ return mem;
+}
+
+char *p_strdup_empty(pool_t pool, const char *str)
+{
+ if (str == NULL || *str == '\0')
+ return NULL;
+
+ return p_strdup(pool, str);
+}
+
+char *p_strdup_until(pool_t pool, const void *start, const void *end)
+{
+ size_t size;
+ char *mem;
+
+ i_assert((const char *) start <= (const char *) end);
+
+ size = (size_t) ((const char *) end - (const char *) start);
+
+ mem = p_malloc(pool, size + 1);
+ memcpy(mem, start, size);
+ return mem;
+}
+
+char *p_strndup(pool_t pool, const void *str, size_t max_chars)
+{
+ const char *p;
+ char *mem;
+ size_t len;
+
+ i_assert(str != NULL);
+ i_assert(max_chars != SIZE_MAX);
+
+ p = memchr(str, '\0', max_chars);
+ if (p == NULL)
+ len = max_chars;
+ else
+ len = p - (const char *)str;
+
+ mem = p_malloc(pool, len+1);
+ memcpy(mem, str, len);
+ return mem;
+}
+
+char *p_strdup_printf(pool_t pool, const char *format, ...)
+{
+ va_list args;
+ char *ret;
+
+ va_start(args, format);
+ ret = p_strdup_vprintf(pool, format, args);
+ va_end(args);
+
+ return ret;
+}
+
+char *t_noalloc_strdup_vprintf(const char *format, va_list args,
+ unsigned int *size_r)
+{
+#define SNPRINTF_INITIAL_EXTRA_SIZE 256
+ va_list args2;
+ char *tmp;
+ size_t init_size;
+ int ret;
+#ifdef DEBUG
+ int old_errno = errno;
+#endif
+
+ VA_COPY(args2, args);
+
+ /* the format string is modified only if %m exists in it. it happens
+ only in error conditions, so don't try to t_push() here since it'll
+ just slow down the normal code path. */
+ format = printf_format_fix_get_len(format, &init_size);
+ init_size += SNPRINTF_INITIAL_EXTRA_SIZE;
+
+ tmp = t_buffer_get(init_size);
+ ret = vsnprintf(tmp, init_size, format, args);
+ i_assert(ret >= 0);
+
+ *size_r = ret + 1;
+ if ((unsigned int)ret >= init_size) {
+ /* didn't fit with the first guess. now we know the size,
+ so try again. */
+ tmp = t_buffer_get(*size_r);
+ ret = vsnprintf(tmp, *size_r, format, args2);
+ i_assert((unsigned int)ret == *size_r-1);
+ }
+#ifdef DEBUG
+ /* we rely on errno not changing. it shouldn't. */
+ i_assert(errno == old_errno);
+#endif
+ va_end(args2);
+ return tmp;
+}
+
+char *p_strdup_vprintf(pool_t pool, const char *format, va_list args)
+{
+ char *tmp, *buf;
+ unsigned int size;
+
+ tmp = t_noalloc_strdup_vprintf(format, args, &size);
+ if (pool->datastack_pool) {
+ t_buffer_alloc(size);
+ return tmp;
+ } else {
+ buf = p_malloc(pool, size);
+ memcpy(buf, tmp, size - 1);
+ return buf;
+ }
+}
+
+char *vstrconcat(const char *str1, va_list args, size_t *ret_len)
+{
+ const char *str;
+ char *temp;
+ size_t bufsize, i, len;
+
+ i_assert(str1 != NULL);
+
+ str = str1;
+ bufsize = STRCONCAT_BUFSIZE;
+ temp = t_buffer_get(bufsize);
+
+ i = 0;
+ do {
+ len = strlen(str);
+
+ if (i + len >= bufsize) {
+ /* need more memory */
+ bufsize = nearest_power(i + len + 1);
+ temp = t_buffer_reget(temp, bufsize);
+ }
+
+ memcpy(temp + i, str, len); i += len;
+
+ /* next string */
+ str = va_arg(args, const char *);
+ } while (str != NULL);
+
+ i_assert(i < bufsize);
+
+ temp[i++] = '\0';
+ *ret_len = i;
+ return temp;
+}
+
+char *p_strconcat(pool_t pool, const char *str1, ...)
+{
+ va_list args;
+ char *temp, *ret;
+ size_t len;
+
+ i_assert(str1 != NULL);
+
+ va_start(args, str1);
+
+ if (pool->datastack_pool) {
+ ret = vstrconcat(str1, args, &len);
+ t_buffer_alloc(len);
+ } else {
+ temp = vstrconcat(str1, args, &len);
+ ret = p_malloc(pool, len);
+ memcpy(ret, temp, len);
+ }
+
+ va_end(args);
+ return ret;
+}
+
+static void *t_memdup(const void *data, size_t size)
+{
+ void *mem = t_malloc_no0(size);
+ memcpy(mem, data, size);
+ return mem;
+}
+
+const char *t_strdup(const char *str)
+{
+ return t_strdup_noconst(str);
+}
+
+char *t_strdup_noconst(const char *str)
+{
+ if (str == NULL)
+ return NULL;
+ return t_memdup(str, strlen(str) + 1);
+}
+
+const char *t_strdup_empty(const char *str)
+{
+ if (str == NULL || *str == '\0')
+ return NULL;
+
+ return t_strdup(str);
+}
+
+const char *t_strdup_until(const void *start, const void *end)
+{
+ char *mem;
+ size_t size;
+
+ i_assert((const char *) start <= (const char *) end);
+
+ size = (size_t)((const char *)end - (const char *)start);
+
+ mem = t_malloc_no0(size + 1);
+ memcpy(mem, start, size);
+ mem[size] = '\0';
+ return mem;
+}
+
+const char *t_strndup(const void *str, size_t max_chars)
+{
+ i_assert(str != NULL);
+ return p_strndup(unsafe_data_stack_pool, str, max_chars);
+}
+
+const char *t_strdup_printf(const char *format, ...)
+{
+ va_list args;
+ const char *ret;
+
+ va_start(args, format);
+ ret = p_strdup_vprintf(unsafe_data_stack_pool, format, args);
+ va_end(args);
+
+ return ret;
+}
+
+const char *t_strdup_vprintf(const char *format, va_list args)
+{
+ return p_strdup_vprintf(unsafe_data_stack_pool, format, args);
+}
+
+const char *t_strconcat(const char *str1, ...)
+{
+ va_list args;
+ const char *ret;
+ size_t len;
+
+ i_assert(str1 != NULL);
+
+ va_start(args, str1);
+
+ ret = vstrconcat(str1, args, &len);
+ t_buffer_alloc(len);
+
+ va_end(args);
+ return ret;
+}
+
+const char *t_strcut(const char *str, char cutchar)
+{
+ const char *p;
+
+ for (p = str; *p != '\0'; p++) {
+ if (*p == cutchar)
+ return t_strdup_until(str, p);
+ }
+
+ return str;
+}
+
+const char *t_str_replace(const char *str, char from, char to)
+{
+ char *out;
+ size_t i, len;
+
+ if (strchr(str, from) == NULL)
+ return str;
+
+ len = strlen(str);
+ out = t_malloc_no0(len + 1);
+ for (i = 0; i < len; i++) {
+ if (str[i] == from)
+ out[i] = to;
+ else
+ out[i] = str[i];
+ }
+ out[i] = '\0';
+ return out;
+}
+
+const char *t_str_oneline(const char *str)
+{
+ string_t *out;
+ size_t len;
+ const char *p, *pend, *poff;
+ bool new_line;
+
+ if (strpbrk(str, "\r\n") == NULL)
+ return str;
+
+ len = strlen(str);
+ out = t_str_new(len + 1);
+ new_line = TRUE;
+ p = poff = str;
+ pend = str + len;
+ while (p < pend) {
+ switch (*p) {
+ case '\r':
+ if (p > poff)
+ str_append_data(out, poff, p - poff);
+ /* just drop \r */
+ poff = p + 1;
+ break;
+ case '\n':
+ if (p > poff)
+ str_append_data(out, poff, p - poff);
+ if (new_line) {
+ /* coalesce multiple \n into a single space */
+ } else {
+ /* first \n after text */
+ str_append_c(out, ' ');
+ new_line = TRUE;
+ }
+ poff = p + 1;
+ break;
+ default:
+ new_line = FALSE;
+ break;
+ }
+ p++;
+ }
+
+ if (new_line && str_len(out) > 0)
+ str_truncate(out, str_len(out) - 1);
+ else if (p > poff)
+ str_append_data(out, poff, p - poff);
+ return str_c(out);
+}
+
+int i_strocpy(char *dest, const char *src, size_t dstsize)
+{
+ if (dstsize == 0)
+ return -1;
+
+ while (*src != '\0' && dstsize > 1) {
+ *dest++ = *src++;
+ dstsize--;
+ }
+
+ *dest = '\0';
+ return *src == '\0' ? 0 : -1;
+}
+
+char *str_ucase(char *str)
+{
+ char *p;
+
+ for (p = str; *p != '\0'; p++)
+ *p = i_toupper(*p);
+ return str;
+}
+
+char *str_lcase(char *str)
+{
+ char *p;
+
+ for (p = str; *p != '\0'; p++)
+ *p = i_tolower(*p);
+ return str;
+}
+
+const char *t_str_lcase(const char *str)
+{
+ i_assert(str != NULL);
+
+ return str_lcase(t_strdup_noconst(str));
+}
+
+const char *t_str_ucase(const char *str)
+{
+ i_assert(str != NULL);
+
+ return str_ucase(t_strdup_noconst(str));
+}
+
+const char *i_strstr_arr(const char *haystack, const char *const *needles)
+{
+ const char *ptr;
+ for(; *needles != NULL; needles++)
+ if ((ptr = strstr(haystack, *needles)) != NULL)
+ return ptr;
+ return NULL;
+}
+
+static void str_trim_parse(const char *str,
+ const char *chars, enum _str_trim_sides sides,
+ const char **begin_r, const char **end_r)
+{
+ const char *p, *pend, *begin, *end;
+
+ *begin_r = *end_r = NULL;
+
+ pend = str + strlen(str);
+ if (pend == str)
+ return;
+
+ p = str;
+ if ((sides & STR_TRIM_LEFT) != 0) {
+ while (p < pend && strchr(chars, *p) != NULL)
+ p++;
+ if (p == pend)
+ return;
+ }
+ begin = p;
+
+ p = pend;
+ if ((sides & STR_TRIM_RIGHT) != 0) {
+ while (p > begin && strchr(chars, *(p-1)) != NULL)
+ p--;
+ if (p == begin)
+ return;
+ }
+ end = p;
+
+ *begin_r = begin;
+ *end_r = end;
+}
+
+const char *t_str_trim(const char *str, const char *chars)
+{
+ const char *begin, *end;
+
+ str_trim_parse(str, chars,
+ STR_TRIM_LEFT | STR_TRIM_RIGHT, &begin, &end);
+ if (begin == NULL)
+ return "";
+ return t_strdup_until(begin, end);
+}
+
+const char *p_str_trim(pool_t pool, const char *str, const char *chars)
+{
+ const char *begin, *end;
+
+ str_trim_parse(str, chars,
+ STR_TRIM_LEFT | STR_TRIM_RIGHT, &begin, &end);
+ if (begin == NULL)
+ return "";
+ return p_strdup_until(pool, begin, end);
+}
+
+const char *str_ltrim(const char *str, const char *chars)
+{
+ const char *begin, *end;
+
+ str_trim_parse(str, chars, STR_TRIM_LEFT, &begin, &end);
+ if (begin == NULL)
+ return "";
+ return begin;
+}
+
+const char *t_str_ltrim(const char *str, const char *chars)
+{
+ return t_strdup(str_ltrim(str, chars));
+}
+
+const char *p_str_ltrim(pool_t pool, const char *str, const char *chars)
+{
+ return p_strdup(pool, str_ltrim(str, chars));
+}
+
+const char *t_str_rtrim(const char *str, const char *chars)
+{
+ const char *begin, *end;
+
+ str_trim_parse(str, chars, STR_TRIM_RIGHT, &begin, &end);
+ if (begin == NULL)
+ return "";
+ return t_strdup_until(begin, end);
+}
+
+const char *p_str_rtrim(pool_t pool, const char *str, const char *chars)
+{
+ const char *begin, *end;
+
+ str_trim_parse(str, chars, STR_TRIM_RIGHT, &begin, &end);
+ if (begin == NULL)
+ return "";
+ return p_strdup_until(pool, begin, end);
+}
+
+int null_strcmp(const char *s1, const char *s2)
+{
+ if (s1 == NULL)
+ return s2 == NULL ? 0 : -1;
+ if (s2 == NULL)
+ return 1;
+
+ return strcmp(s1, s2);
+}
+
+int null_strcasecmp(const char *s1, const char *s2)
+{
+ if (s1 == NULL)
+ return s2 == NULL ? 0 : -1;
+ if (s2 == NULL)
+ return 1;
+
+ return strcasecmp(s1, s2);
+}
+
+int i_memcasecmp(const void *p1, const void *p2, size_t size)
+{
+ const unsigned char *s1 = p1;
+ const unsigned char *s2 = p2;
+ int ret;
+
+ while (size > 0) {
+ ret = i_toupper(*s1) - i_toupper(*s2);
+ if (ret != 0)
+ return ret;
+
+ s1++; s2++; size--;
+ }
+
+ return 0;
+}
+
+int i_strcmp_p(const char *const *p1, const char *const *p2)
+{
+ return strcmp(*p1, *p2);
+}
+
+int i_strcasecmp_p(const char *const *p1, const char *const *p2)
+{
+ return strcasecmp(*p1, *p2);
+}
+
+bool mem_equals_timing_safe(const void *p1, const void *p2, size_t size)
+{
+ const unsigned char *s1 = p1, *s2 = p2;
+ size_t i;
+ int ret = 0;
+
+ for (i = 0; i < size; i++)
+ ret |= s1[i] ^ s2[i];
+
+ /* make sure the compiler optimizer doesn't try to break out of the
+ above loop early. */
+ timing_safety_unoptimization = ret;
+ return ret == 0;
+}
+
+bool str_equals_timing_almost_safe(const char *s1, const char *s2)
+{
+ size_t i;
+ int ret = 0;
+
+ for (i = 0; s1[i] != '\0' && s2[i] != '\0'; i++)
+ ret |= s1[i] ^ s2[i];
+ ret |= s1[i] ^ s2[i];
+
+ /* make sure the compiler optimizer doesn't try to break out of the
+ above loop early. */
+ timing_safety_unoptimization = ret;
+ return ret == 0;
+}
+
+size_t
+str_match(const char *p1, const char *p2)
+{
+ size_t i = 0;
+
+ while(p1[i] != '\0' && p1[i] == p2[i])
+ i++;
+
+ return i;
+}
+
+size_t i_memspn(const void *data, size_t data_len,
+ const void *accept, size_t accept_len)
+{
+ const unsigned char *start = data;
+ i_assert(data != NULL || data_len == 0);
+ i_assert(accept != NULL || accept_len == 0);
+ size_t pos = 0;
+ /* nothing to accept */
+ if (accept_len == 0)
+ return 0;
+ for (; pos < data_len; pos++) {
+ if (memchr(accept, start[pos], accept_len) == NULL)
+ break;
+ }
+ return pos;
+}
+
+size_t i_memcspn(const void *data, size_t data_len,
+ const void *reject, size_t reject_len)
+{
+ const unsigned char *start = data;
+ const unsigned char *r = reject;
+ const unsigned char *ptr = CONST_PTR_OFFSET(data, data_len);
+ i_assert(data != NULL || data_len == 0);
+ i_assert(reject != NULL || reject_len == 0);
+ /* nothing to reject */
+ if (reject_len == 0 || data_len == 0)
+ return data_len;
+ /* Doing repeated memchr's over the data is faster than
+ going over it once byte by byte, as long as reject
+ is reasonably short. */
+ for (size_t i = 0; i < reject_len; i++) {
+ const unsigned char *kand =
+ memchr(start, r[i], data_len);
+ if (kand != NULL && kand < ptr)
+ ptr = kand;
+ }
+ return ptr - start;
+}
+
+static char **
+split_str_slow(pool_t pool, const char *data, const char *separators, bool spaces)
+{
+ char **array;
+ char *str;
+ unsigned int count, alloc_count, new_alloc_count;
+
+ if (spaces) {
+ /* skip leading separators */
+ while (*data != '\0' && strchr(separators, *data) != NULL)
+ data++;
+ }
+ if (*data == '\0')
+ return p_new(pool, char *, 1);
+
+ str = p_strdup(pool, data);
+
+ alloc_count = 32;
+ array = p_new(pool, char *, alloc_count);
+
+ array[0] = str; count = 1;
+ while (*str != '\0') {
+ if (strchr(separators, *str) != NULL) {
+ /* separator found */
+ if (count+1 >= alloc_count) {
+ new_alloc_count = nearest_power(alloc_count+1);
+ array = p_realloc(pool, array,
+ sizeof(char *) * alloc_count,
+ sizeof(char *) *
+ new_alloc_count);
+ alloc_count = new_alloc_count;
+ }
+
+ *str = '\0';
+ if (spaces) {
+ while (str[1] != '\0' &&
+ strchr(separators, str[1]) != NULL)
+ str++;
+
+ /* ignore trailing separators */
+ if (str[1] == '\0')
+ break;
+ }
+
+ array[count++] = str+1;
+ }
+
+ str++;
+ }
+
+ i_assert(count < alloc_count);
+ array[count] = NULL;
+
+ return array;
+}
+
+static char **
+split_str_fast(pool_t pool, const char *data, char sep)
+{
+ char **array, *str;
+ unsigned int count, alloc_count, new_alloc_count;
+
+ if (*data == '\0')
+ return p_new(pool, char *, 1);
+
+ str = p_strdup(pool, data);
+
+ alloc_count = 32;
+ array = p_new(pool, char *, alloc_count);
+
+ array[0] = str; count = 1;
+ while ((str = strchr(str, sep)) != NULL) {
+ /* separator found */
+ if (count+1 >= alloc_count) {
+ new_alloc_count = nearest_power(alloc_count+1);
+ array = p_realloc(pool, array,
+ sizeof(char *) * alloc_count,
+ sizeof(char *) *
+ new_alloc_count);
+ alloc_count = new_alloc_count;
+ }
+ *str++ = '\0';
+ array[count++] = str;
+ }
+ i_assert(count < alloc_count);
+ i_assert(array[count] == NULL);
+
+ return array;
+}
+
+static char **
+split_str(pool_t pool, const char *data, const char *separators, bool spaces)
+{
+ i_assert(*separators != '\0');
+
+ if (separators[1] == '\0' && !spaces)
+ return split_str_fast(pool, data, separators[0]);
+ else
+ return split_str_slow(pool, data, separators, spaces);
+}
+
+const char **t_strsplit(const char *data, const char *separators)
+{
+ return (const char **)split_str(unsafe_data_stack_pool, data,
+ separators, FALSE);
+}
+
+const char **t_strsplit_spaces(const char *data, const char *separators)
+{
+ return (const char **)split_str(unsafe_data_stack_pool, data,
+ separators, TRUE);
+}
+
+char **p_strsplit(pool_t pool, const char *data, const char *separators)
+{
+ return split_str(pool, data, separators, FALSE);
+}
+
+char **p_strsplit_spaces(pool_t pool, const char *data,
+ const char *separators)
+{
+ return split_str(pool, data, separators, TRUE);
+}
+
+void p_strsplit_free(pool_t pool, char **arr)
+{
+ p_free(pool, arr[0]);
+ p_free(pool, arr);
+}
+
+unsigned int str_array_length(const char *const *arr)
+{
+ unsigned int count;
+
+ if (arr == NULL)
+ return 0;
+
+ for (count = 0; *arr != NULL; arr++)
+ count++;
+
+ return count;
+}
+
+static char *
+p_strarray_join_n(pool_t pool, const char *const *arr, unsigned int arr_len,
+ const char *separator)
+{
+ size_t alloc_len, sep_len, len, pos, needed_space;
+ unsigned int i;
+ char *str;
+
+ sep_len = strlen(separator);
+ alloc_len = 64;
+ str = t_buffer_get(alloc_len);
+ pos = 0;
+
+ for (i = 0; i < arr_len; i++) {
+ len = strlen(arr[i]);
+ needed_space = pos + len + sep_len + 1;
+ if (needed_space > alloc_len) {
+ alloc_len = nearest_power(needed_space);
+ str = t_buffer_reget(str, alloc_len);
+ }
+
+ if (i != 0) {
+ memcpy(str + pos, separator, sep_len);
+ pos += sep_len;
+ }
+
+ memcpy(str + pos, arr[i], len);
+ pos += len;
+ }
+ str[pos] = '\0';
+ if (!pool->datastack_pool)
+ return p_memdup(pool, str, pos + 1);
+ t_buffer_alloc(pos + 1);
+ return str;
+}
+
+const char *t_strarray_join(const char *const *arr, const char *separator)
+{
+ return p_strarray_join_n(unsafe_data_stack_pool, arr,
+ str_array_length(arr), separator);
+}
+
+bool str_array_remove(const char **arr, const char *value)
+{
+ const char **dest;
+
+ for (; *arr != NULL; arr++) {
+ if (strcmp(*arr, value) == 0) {
+ /* found it. now move the rest. */
+ for (dest = arr, arr++; *arr != NULL; arr++, dest++)
+ *dest = *arr;
+ *dest = NULL;
+ return TRUE;
+ }
+ }
+ return FALSE;
+}
+
+bool str_array_find(const char *const *arr, const char *value)
+{
+ for (; *arr != NULL; arr++) {
+ if (strcmp(*arr, value) == 0)
+ return TRUE;
+ }
+ return FALSE;
+}
+
+bool str_array_icase_find(const char *const *arr, const char *value)
+{
+ for (; *arr != NULL; arr++) {
+ if (strcasecmp(*arr, value) == 0)
+ return TRUE;
+ }
+ return FALSE;
+}
+
+const char **p_strarray_dup(pool_t pool, const char *const *arr)
+{
+ unsigned int i;
+ const char **ret;
+ char *p;
+ size_t len, size = sizeof(const char *);
+
+ /* @UNSAFE: integer overflow checks are missing */
+ for (i = 0; arr[i] != NULL; i++)
+ size += sizeof(const char *) + strlen(arr[i]) + 1;
+
+ ret = p_malloc(pool, size);
+ p = PTR_OFFSET(ret, sizeof(const char *) * (i + 1));
+ for (i = 0; arr[i] != NULL; i++) {
+ len = strlen(arr[i]) + 1;
+ memcpy(p, arr[i], len);
+ ret[i] = p;
+ p += len;
+ }
+ i_assert(PTR_OFFSET(ret, size) == (void *)p);
+ return ret;
+}
+
+const char *dec2str(uintmax_t number)
+{
+ return dec2str_buf(t_malloc_no0(MAX_INT_STRLEN), number);
+}
+
+char *dec2str_buf(char buffer[STATIC_ARRAY MAX_INT_STRLEN], uintmax_t number)
+{
+ int pos;
+
+ pos = MAX_INT_STRLEN;
+ buffer[--pos] = '\0';
+ do {
+ buffer[--pos] = (number % 10) + '0';
+ number /= 10;
+ } while (number != 0 && pos >= 0);
+
+ i_assert(pos >= 0);
+ return buffer + pos;
+}
+
+char *p_array_const_string_join(pool_t pool, const ARRAY_TYPE(const_string) *arr,
+ const char *separator)
+{
+ if (array_count(arr) == 0)
+ return "";
+ return p_strarray_join_n(pool, array_front(arr), array_count(arr),
+ separator);
+}
diff --git a/src/lib/strfuncs.h b/src/lib/strfuncs.h
new file mode 100644
index 0000000..1ddb82f
--- /dev/null
+++ b/src/lib/strfuncs.h
@@ -0,0 +1,170 @@
+#ifndef STRFUNC_H
+#define STRFUNC_H
+
+/* Maximum number of bytes needed for the largest uintmax_t or the lowest
+ intmax_t number in base 10. This value includes the trailing \0. */
+#define MAX_INT_STRLEN ((sizeof(uintmax_t) * CHAR_BIT + 2) / 3 + 1)
+
+extern const unsigned char uchar_nul; /* (const unsigned char *)"" */
+extern const unsigned char *uchar_empty_ptr; /* non-NULL pointer that shouldn't be dereferenced. */
+
+/* Returns -1 if dest wasn't large enough, 0 if not. */
+int i_snprintf(char *dest, size_t max_chars, const char *format, ...)
+ ATTR_FORMAT(3, 4);
+
+char *p_strdup(pool_t pool, const char *str) ATTR_MALLOC;
+void *p_memdup(pool_t pool, const void *data, size_t size) ATTR_MALLOC;
+/* return NULL if str = "" */
+char *p_strdup_empty(pool_t pool, const char *str) ATTR_MALLOC;
+/* *end isn't included */
+char *p_strdup_until(pool_t pool, const void *start, const void *end)
+ ATTR_MALLOC ATTR_RETURNS_NONNULL;
+char *p_strndup(pool_t pool, const void *str, size_t max_chars) ATTR_MALLOC;
+char *p_strdup_printf(pool_t pool, const char *format, ...)
+ ATTR_FORMAT(2, 3) ATTR_MALLOC ATTR_RETURNS_NONNULL;
+char *p_strdup_vprintf(pool_t pool, const char *format, va_list args)
+ ATTR_FORMAT(2, 0) ATTR_MALLOC ATTR_RETURNS_NONNULL;
+char *p_strconcat(pool_t pool, const char *str1, ...)
+ ATTR_SENTINEL ATTR_MALLOC;
+
+/* same with temporary memory allocations: */
+const char *t_strdup(const char *str) ATTR_MALLOC;
+char *t_strdup_noconst(const char *str) ATTR_MALLOC;
+/* return NULL if str = "" */
+const char *t_strdup_empty(const char *str) ATTR_MALLOC;
+/* *end isn't included */
+const char *t_strdup_until(const void *start, const void *end)
+ ATTR_MALLOC ATTR_RETURNS_NONNULL;
+const char *t_strndup(const void *str, size_t max_chars) ATTR_MALLOC;
+const char *t_strdup_printf(const char *format, ...)
+ ATTR_FORMAT(1, 2) ATTR_MALLOC ATTR_RETURNS_NONNULL;
+const char *t_strdup_vprintf(const char *format, va_list args)
+ ATTR_FORMAT(1, 0) ATTR_MALLOC ATTR_RETURNS_NONNULL;
+const char *t_strconcat(const char *str1, ...)
+ ATTR_SENTINEL ATTR_MALLOC;
+
+/* Like t_strdup(), but stop at cutchar. */
+const char *t_strcut(const char *str, char cutchar);
+/* Replace all from->to chars in the string. */
+const char *t_str_replace(const char *str, char from, char to);
+/* Put the string on a single line by replacing all newlines with spaces and
+ dropping any carriage returns. Sequences of several newlines are merged into
+ one space and newlines at the beginning and end of the string are dropped. */
+const char *t_str_oneline(const char *str);
+
+/* Like strlcpy(), but return -1 if buffer was overflown, 0 if not. */
+int i_strocpy(char *dest, const char *src, size_t dstsize);
+
+char *str_ucase(char *str);
+char *str_lcase(char *str);
+const char *t_str_lcase(const char *str);
+const char *t_str_ucase(const char *str);
+
+/* Return pointer to first matching needle */
+const char *i_strstr_arr(const char *haystack, const char *const *needles);
+
+/* Trim matching chars from either side of the string */
+const char *t_str_trim(const char *str, const char *chars);
+const char *p_str_trim(pool_t pool, const char *str, const char *chars);
+const char *str_ltrim(const char *str, const char *chars);
+const char *t_str_ltrim(const char *str, const char *chars);
+const char *p_str_ltrim(pool_t pool, const char *str, const char *chars);
+const char *t_str_rtrim(const char *str, const char *chars);
+const char *p_str_rtrim(pool_t pool, const char *str, const char *chars);
+
+int null_strcmp(const char *s1, const char *s2) ATTR_PURE;
+int null_strcasecmp(const char *s1, const char *s2) ATTR_PURE;
+int i_memcasecmp(const void *p1, const void *p2, size_t size) ATTR_PURE;
+int i_strcmp_p(const char *const *p1, const char *const *p2) ATTR_PURE;
+int i_strcasecmp_p(const char *const *p1, const char *const *p2) ATTR_PURE;
+/* Returns TRUE if the two memory areas are equal. This function is safe
+ against timing attacks, so it compares all the bytes every time. */
+bool mem_equals_timing_safe(const void *p1, const void *p2, size_t size);
+/* Returns TRUE if the two strings are equal. Similar to
+ mem_equals_timing_safe() this function is safe against timing attacks when
+ the string lengths are the same. If not, the length of the secret string may
+ be leaked, but otherwise the contents won't be. */
+bool str_equals_timing_almost_safe(const char *s1, const char *s2);
+
+size_t str_match(const char *p1, const char *p2) ATTR_PURE;
+static inline ATTR_PURE bool str_begins(const char *haystack, const char *needle)
+{
+ return needle[str_match(haystack, needle)] == '\0';
+}
+#if defined(__GNUC__) && (__GNUC__ >= 2)
+/* GCC (and Clang) are known to have a compile-time strlen("literal") shortcut, and
+ an optimised strncmp(), so use that by default. Macro is multi-evaluation safe. */
+# define str_begins(h, n) (__builtin_constant_p(n) ? strncmp((h), (n), strlen(n))==0 : (str_begins)((h), (n)))
+#endif
+
+/* Get length of a prefix segment.
+
+ Calculates the length (in bytes) of the initial segment of s which consists
+ entirely of bytes in accept.
+*/
+size_t i_memspn(const void *data, size_t data_len,
+ const void *accept, size_t accept_len);
+/* Get length of a prefix segment.
+
+ Calculates the length of the initial segment of s which consists entirely of
+ bytes not in reject.
+*/
+size_t i_memcspn(const void *data, size_t data_len,
+ const void *reject, size_t reject_len);
+
+static inline char *i_strchr_to_next(const char *str, char chr)
+{
+ char *tmp = (char *)strchr(str, chr);
+ return tmp == NULL ? NULL : tmp+1;
+}
+
+/* separators is an array of separator characters, not a separator string.
+ an empty data string results in an array containing only NULL. */
+char **p_strsplit(pool_t pool, const char *data, const char *separators)
+ ATTR_MALLOC ATTR_RETURNS_NONNULL;
+const char **t_strsplit(const char *data, const char *separators)
+ ATTR_MALLOC ATTR_RETURNS_NONNULL;
+/* like p_strsplit(), but treats multiple adjacent separators as a single
+ separator. separators at the beginning or at the end of the string are also
+ ignored, so it's not possible for the result to have any empty strings. */
+char **p_strsplit_spaces(pool_t pool, const char *data, const char *separators)
+ ATTR_MALLOC ATTR_RETURNS_NONNULL;
+const char **t_strsplit_spaces(const char *data, const char *separators)
+ ATTR_MALLOC ATTR_RETURNS_NONNULL;
+void p_strsplit_free(pool_t pool, char **arr);
+
+const char *dec2str(uintmax_t number);
+/* Use the given buffer to write out the number. Returns pointer to the
+ written number in the buffer. Note that this isn't the same as the beginning
+ of the buffer. */
+char *dec2str_buf(char buffer[STATIC_ARRAY MAX_INT_STRLEN], uintmax_t number);
+
+/* Return length of NULL-terminated list string array */
+unsigned int str_array_length(const char *const *arr) ATTR_PURE;
+/* Return all strings from array joined into one string. */
+const char *t_strarray_join(const char *const *arr, const char *separator)
+ ATTR_MALLOC ATTR_RETURNS_NONNULL;
+/* Removes a value from NULL-terminated string array. Returns TRUE if found. */
+bool str_array_remove(const char **arr, const char *value);
+/* Returns TRUE if value exists in NULL-terminated string array. */
+bool str_array_find(const char *const *arr, const char *value);
+/* Like str_array_find(), but use strcasecmp(). */
+bool str_array_icase_find(const char *const *arr, const char *value);
+/* Duplicate array of strings. The memory can be freed by freeing the
+ return value. */
+const char **p_strarray_dup(pool_t pool, const char *const *arr)
+ ATTR_MALLOC ATTR_RETURNS_NONNULL;
+
+/* Join ARRAY_TYPE(const_string) to a string, similar to t_strarray_join() */
+char *p_array_const_string_join(pool_t pool, const ARRAY_TYPE(const_string) *arr,
+ const char *separator);
+#define t_array_const_string_join(arr, separator) \
+ ((const char *)p_array_const_string_join(unsafe_data_stack_pool, arr, separator))
+
+/* INTERNAL */
+char *t_noalloc_strdup_vprintf(const char *format, va_list args,
+ unsigned int *size_r)
+ ATTR_FORMAT(1, 0) ATTR_RETURNS_NONNULL;
+char *vstrconcat(const char *str1, va_list args, size_t *ret_len) ATTR_MALLOC;
+
+#endif
diff --git a/src/lib/strnum.c b/src/lib/strnum.c
new file mode 100644
index 0000000..4b3da4a
--- /dev/null
+++ b/src/lib/strnum.c
@@ -0,0 +1,464 @@
+/* Copyright (c) 2010-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "strnum.h"
+
+bool str_is_numeric(const char *str, char end_char)
+{
+ if (*str == '\0' || *str == end_char)
+ return FALSE;
+
+ while (*str != '\0' && *str != end_char) {
+ if (*str < '0' || *str > '9')
+ return FALSE;
+ str++;
+ }
+
+ return TRUE;
+}
+
+bool str_is_float(const char *str, char end_char)
+{
+ bool dot_seen = FALSE;
+ bool num_seen = FALSE;
+
+ if (*str == '\0' || *str == end_char)
+ return FALSE;
+
+ while (*str != '\0' && *str != end_char) {
+ if (*str == '.') {
+ if (dot_seen || !num_seen) return FALSE;
+ dot_seen = TRUE;
+ num_seen = FALSE;
+ str++;
+ /* enforce that number follows dot */
+ continue;
+ }
+ if (*str < '0' || *str > '9')
+ return FALSE;
+ num_seen = TRUE;
+ str++;
+ }
+
+ return num_seen;
+}
+
+/*
+ * Unsigned decimal
+ */
+
+#define STR_PARSE_U__TEMPLATE(name, type) \
+int name(const char *str, type *num_r, const char **endp_r) \
+{ \
+ uintmax_t l; \
+ if (str_parse_uintmax(str, &l, endp_r) < 0 || l > (type)-1) \
+ return -1; \
+ *num_r = (type)l; \
+ return 0; \
+}
+
+STR_PARSE_U__TEMPLATE(str_parse_uint, unsigned int)
+STR_PARSE_U__TEMPLATE(str_parse_ulong, unsigned long)
+STR_PARSE_U__TEMPLATE(str_parse_ullong, unsigned long long)
+STR_PARSE_U__TEMPLATE(str_parse_uint32, uint32_t)
+STR_PARSE_U__TEMPLATE(str_parse_uint64, uint64_t)
+
+#define STR_TO_U__TEMPLATE(name, type) \
+int name(const char *str, type *num_r) \
+{ \
+ uintmax_t l; \
+ if (str_to_uintmax(str, &l) < 0 || l > (type)-1) \
+ return -1; \
+ *num_r = (type)l; \
+ return 0; \
+}
+
+STR_TO_U__TEMPLATE(str_to_uint, unsigned int)
+STR_TO_U__TEMPLATE(str_to_ulong, unsigned long)
+STR_TO_U__TEMPLATE(str_to_ullong, unsigned long long)
+STR_TO_U__TEMPLATE(str_to_uint32, uint32_t)
+STR_TO_U__TEMPLATE(str_to_uint64, uint64_t)
+
+int str_parse_uintmax(const char *str, uintmax_t *num_r,
+ const char **endp_r)
+{
+ uintmax_t n = 0;
+
+ if (*str < '0' || *str > '9')
+ return -1;
+
+ do {
+ if (n >= ((uintmax_t)-1 / 10)) {
+ if (n > (uintmax_t)-1 / 10)
+ return -1;
+ if ((uintmax_t)(*str - '0') > ((uintmax_t)-1 % 10))
+ return -1;
+ }
+ n = n * 10 + (*str - '0');
+ str++;
+ } while (*str >= '0' && *str <= '9');
+
+ if (endp_r != NULL)
+ *endp_r = str;
+ *num_r = n;
+ return 0;
+}
+int str_to_uintmax(const char *str, uintmax_t *num_r)
+{
+ const char *endp;
+ uintmax_t n;
+ int ret = str_parse_uintmax(str, &n, &endp);
+ if ((ret != 0) || (*endp != '\0'))
+ return -1;
+ *num_r = n;
+ return 0;
+}
+
+bool str_uint_equals(const char *str, uintmax_t num)
+{
+ uintmax_t l;
+
+ if (str_to_uintmax(str, &l) < 0)
+ return FALSE;
+ return l == num;
+}
+
+/*
+ * Unsigned hexadecimal
+ */
+
+#define STR_PARSE_UHEX__TEMPLATE(name, type) \
+int name(const char *str, type *num_r, const char **endp_r) \
+{ \
+ uintmax_t l; \
+ if (str_parse_uintmax_hex(str, &l, endp_r) < 0 || l > (type)-1) \
+ return -1; \
+ *num_r = (type)l; \
+ return 0; \
+}
+
+STR_PARSE_UHEX__TEMPLATE(str_parse_uint_hex, unsigned int)
+STR_PARSE_UHEX__TEMPLATE(str_parse_ulong_hex, unsigned long)
+STR_PARSE_UHEX__TEMPLATE(str_parse_ullong_hex, unsigned long long)
+STR_PARSE_UHEX__TEMPLATE(str_parse_uint32_hex, uint32_t)
+STR_PARSE_UHEX__TEMPLATE(str_parse_uint64_hex, uint64_t)
+
+#define STR_TO_UHEX__TEMPLATE(name, type) \
+int name(const char *str, type *num_r) \
+{ \
+ uintmax_t l; \
+ if (str_to_uintmax_hex(str, &l) < 0 || l > (type)-1) \
+ return -1; \
+ *num_r = (type)l; \
+ return 0; \
+}
+
+STR_TO_UHEX__TEMPLATE(str_to_uint_hex, unsigned int)
+STR_TO_UHEX__TEMPLATE(str_to_ulong_hex, unsigned long)
+STR_TO_UHEX__TEMPLATE(str_to_ullong_hex, unsigned long long)
+STR_TO_UHEX__TEMPLATE(str_to_uint32_hex, uint32_t)
+STR_TO_UHEX__TEMPLATE(str_to_uint64_hex, uint64_t)
+
+static inline int _str_parse_hex(const char ch,
+ unsigned int *hex_r)
+{
+ switch (ch) {
+ case 'a': case 'b': case 'c': case 'd': case 'e': case 'f':
+ *hex_r = (unsigned int)(ch - 'a' + 10);
+ return 0;
+ case 'A': case 'B': case 'C': case 'D': case 'E': case 'F':
+ *hex_r = (unsigned int)(ch - 'A' + 10);
+ return 0;
+ case '0': case '1': case '2': case '3': case '4':
+ case '5': case '6': case '7': case '8': case '9':
+ *hex_r = (unsigned int)(ch - '0');
+ return 0;
+ default:
+ break;
+ }
+ return -1;
+}
+int str_parse_uintmax_hex(const char *str, uintmax_t *num_r,
+ const char **endp_r)
+{
+ unsigned int hex;
+ uintmax_t n = 0;
+
+ if (_str_parse_hex(*str, &hex) < 0)
+ return -1;
+
+ do {
+ if (n > (uintmax_t)-1 >> 4)
+ return -1;
+ n = (n << 4) + hex;
+ str++;
+ } while (_str_parse_hex(*str, &hex) >= 0);
+ if (endp_r != NULL)
+ *endp_r = str;
+ *num_r = n;
+ return 0;
+}
+int str_to_uintmax_hex(const char *str, uintmax_t *num_r)
+{
+ const char *endp;
+ uintmax_t n;
+ int ret = str_parse_uintmax_hex(str, &n, &endp);
+ if ((ret != 0) || (*endp != '\0'))
+ return -1;
+ *num_r = n;
+ return 0;
+}
+
+/*
+ * Unsigned octal
+ */
+
+#define STR_PARSE_UOCT__TEMPLATE(name, type) \
+int name(const char *str, type *num_r, const char **endp_r) \
+{ \
+ uintmax_t l; \
+ if (str_parse_uintmax_oct(str, &l, endp_r) < 0 || l > (type)-1) \
+ return -1; \
+ *num_r = (type)l; \
+ return 0; \
+}
+
+STR_PARSE_UOCT__TEMPLATE(str_parse_uint_oct, unsigned int)
+STR_PARSE_UOCT__TEMPLATE(str_parse_ulong_oct, unsigned long)
+STR_PARSE_UOCT__TEMPLATE(str_parse_ullong_oct, unsigned long long)
+STR_PARSE_UOCT__TEMPLATE(str_parse_uint32_oct, uint32_t)
+STR_PARSE_UOCT__TEMPLATE(str_parse_uint64_oct, uint64_t)
+
+#define STR_TO_UOCT__TEMPLATE(name, type) \
+int name(const char *str, type *num_r) \
+{ \
+ uintmax_t l; \
+ if (str_to_uintmax_oct(str, &l) < 0 || l > (type)-1) \
+ return -1; \
+ *num_r = (type)l; \
+ return 0; \
+}
+
+STR_TO_UOCT__TEMPLATE(str_to_uint_oct, unsigned int)
+STR_TO_UOCT__TEMPLATE(str_to_ulong_oct, unsigned long)
+STR_TO_UOCT__TEMPLATE(str_to_ullong_oct, unsigned long long)
+STR_TO_UOCT__TEMPLATE(str_to_uint32_oct, uint32_t)
+STR_TO_UOCT__TEMPLATE(str_to_uint64_oct, uint64_t)
+
+int str_parse_uintmax_oct(const char *str, uintmax_t *num_r,
+ const char **endp_r)
+{
+ uintmax_t n = 0;
+
+ if (*str < '0' || *str > '7')
+ return -1;
+
+ for (; *str >= '0' && *str <= '7'; str++) {
+ if (n > (uintmax_t)-1 >> 3)
+ return -1;
+ n = (n << 3) + (*str - '0');
+ }
+ if (endp_r != NULL)
+ *endp_r = str;
+ *num_r = n;
+ return 0;
+}
+int str_to_uintmax_oct(const char *str, uintmax_t *num_r)
+{
+ const char *endp;
+ uintmax_t n;
+ int ret = str_parse_uintmax_oct(str, &n, &endp);
+ if ((ret != 0) || (*endp != '\0'))
+ return -1;
+ *num_r = n;
+ return 0;
+}
+
+/*
+ * Signed
+ */
+
+#define STR_PARSE_S__TEMPLATE(name, type, int_min, int_max) \
+int name(const char *str, type *num_r, const char **endp_r) \
+{ \
+ intmax_t l; \
+ if (str_parse_intmax(str, &l, endp_r) < 0) \
+ return -1; \
+ if (l < int_min || l > int_max) \
+ return -1; \
+ *num_r = (type)l; \
+ return 0; \
+}
+
+STR_PARSE_S__TEMPLATE(str_parse_int, int, INT_MIN, INT_MAX)
+STR_PARSE_S__TEMPLATE(str_parse_long, long, LONG_MIN, LONG_MAX)
+STR_PARSE_S__TEMPLATE(str_parse_llong, long long, LLONG_MIN, LLONG_MAX)
+STR_PARSE_S__TEMPLATE(str_parse_int32, int32_t, INT32_MIN, INT32_MAX)
+STR_PARSE_S__TEMPLATE(str_parse_int64, int64_t, INT64_MIN, INT64_MAX)
+
+#define STR_TO_S__TEMPLATE(name, type, int_min, int_max) \
+int name(const char *str, type *num_r) \
+{ \
+ intmax_t l; \
+ if (str_to_intmax(str, &l) < 0) \
+ return -1; \
+ if (l < int_min || l > int_max) \
+ return -1; \
+ *num_r = (type)l; \
+ return 0; \
+}
+
+STR_TO_S__TEMPLATE(str_to_int, int, INT_MIN, INT_MAX)
+STR_TO_S__TEMPLATE(str_to_long, long, LONG_MIN, LONG_MAX)
+STR_TO_S__TEMPLATE(str_to_llong, long long, LLONG_MIN, LLONG_MAX)
+STR_TO_S__TEMPLATE(str_to_int32, int32_t, INT32_MIN, INT32_MAX)
+STR_TO_S__TEMPLATE(str_to_int64, int64_t, INT64_MIN, INT64_MAX)
+
+int ATTR_NO_SANITIZE_IMPLICIT_CONVERSION ATTR_NO_SANITIZE_INTEGER
+str_parse_intmax(const char *str, intmax_t *num_r, const char **endp_r)
+{
+ bool neg = FALSE;
+ uintmax_t l;
+
+ if (*str == '-') {
+ neg = TRUE;
+ str++;
+ }
+ if (str_parse_uintmax(str, &l, endp_r) < 0)
+ return -1;
+
+ if (!neg) {
+ if (l > INTMAX_MAX)
+ return -1;
+ *num_r = (intmax_t)l;
+ } else {
+ if (l > UINTMAX_MAX - (UINTMAX_MAX + INTMAX_MIN))
+ return -1;
+ *num_r = (intmax_t) UNSIGNED_MINUS(l);
+ }
+ return 0;
+}
+int str_to_intmax(const char *str, intmax_t *num_r)
+{
+ const char *endp;
+ intmax_t n;
+ int ret = str_parse_intmax(str, &n, &endp);
+ if ((ret != 0) || (*endp != '\0'))
+ return -1;
+ *num_r = n;
+ return 0;
+}
+
+/*
+ * Special numeric types
+ */
+
+static int verify_xid(uintmax_t l, unsigned int result_size)
+{
+ unsigned int result_bits;
+
+ /* we assume that result is a signed type,
+ but that it can never be negative */
+ result_bits = result_size*CHAR_BIT - 1;
+ if ((l >> result_bits) != 0)
+ return -1;
+ return 0;
+}
+
+int str_to_uid(const char *str, uid_t *num_r)
+{
+ uintmax_t l;
+
+ if (str_to_uintmax(str, &l) < 0)
+ return -1;
+
+ if (verify_xid(l, sizeof(*num_r)) < 0)
+ return -1;
+ *num_r = (uid_t)l;
+ return 0;
+}
+
+int str_to_gid(const char *str, gid_t *num_r)
+{
+ uintmax_t l;
+
+ if (str_to_uintmax(str, &l) < 0)
+ return -1;
+
+ /* OS X uses negative GIDs */
+#ifndef __APPLE__
+ if (verify_xid(l, sizeof(*num_r)) < 0)
+ return -1;
+#endif
+ *num_r = (gid_t)l;
+ return 0;
+}
+
+int str_to_pid(const char *str, pid_t *num_r)
+{
+ uintmax_t l;
+
+ if (str_to_uintmax(str, &l) < 0)
+ return -1;
+
+ if (verify_xid(l, sizeof(*num_r)) < 0)
+ return -1;
+ *num_r = (pid_t)l;
+ return 0;
+}
+
+int str_to_ino(const char *str, ino_t *num_r)
+{
+ uintmax_t l;
+
+ if (str_to_uintmax(str, &l) < 0)
+ return -1;
+
+ if (verify_xid(l, sizeof(*num_r)) < 0)
+ return -1;
+ *num_r = (ino_t)l;
+ return 0;
+}
+
+int str_to_uoff(const char *str, uoff_t *num_r)
+{
+ uintmax_t l;
+
+ if (str_to_uintmax(str, &l) < 0)
+ return -1;
+
+ if (l > UOFF_T_MAX)
+ return -1;
+ *num_r = (uoff_t)l;
+ return 0;
+}
+
+int str_to_time(const char *str, time_t *num_r)
+{
+ intmax_t l;
+
+ if (str_to_intmax(str, &l) < 0)
+ return -1;
+
+ *num_r = (time_t)l;
+ return 0;
+}
+
+STR_PARSE_U__TEMPLATE(str_parse_uoff, uoff_t)
+
+/*
+ * Error handling
+ */
+
+const char *str_num_error(const char *str)
+{
+ if (*str == '-') {
+ if (!str_is_numeric(str + 1, '\0'))
+ return "Not a valid number";
+ return "Number too small";
+ } else {
+ if (!str_is_numeric(str, '\0'))
+ return "Not a valid number";
+ return "Number too large";
+ }
+}
diff --git a/src/lib/strnum.h b/src/lib/strnum.h
new file mode 100644
index 0000000..471d4aa
--- /dev/null
+++ b/src/lib/strnum.h
@@ -0,0 +1,192 @@
+#ifndef STRNUM_H
+#define STRNUM_H
+
+/* str_to_*() functions return 0 if string is nothing more than a valid number
+ in valid range. Otherwise -1 is returned and num_r is left untouched
+
+ str_parse_*() helpers do not require the number to be the entire string
+ and pass back the pointer just past a valid parsed integer in endp_r if
+ it is non-NULL. What is written to endp_r in error cases is undefined.
+*/
+
+/*
+ * Unsigned decimal
+ */
+
+int str_to_uint(const char *str, unsigned int *num_r)
+ ATTR_WARN_UNUSED_RESULT;
+int str_parse_uint(const char *str, unsigned int *num_r,
+ const char **endp_r) ATTR_WARN_UNUSED_RESULT ATTR_NULL(3);
+
+int str_to_ulong(const char *str, unsigned long *num_r)
+ ATTR_WARN_UNUSED_RESULT;
+int str_parse_ulong(const char *str, unsigned long *num_r,
+ const char **endp_r) ATTR_WARN_UNUSED_RESULT ATTR_NULL(3);
+
+int str_to_ullong(const char *str, unsigned long long *num_r)
+ ATTR_WARN_UNUSED_RESULT;
+int str_parse_ullong(const char *str, unsigned long long *num_r,
+ const char **endp_r) ATTR_WARN_UNUSED_RESULT ATTR_NULL(3);
+
+int str_to_uint32(const char *str, uint32_t *num_r)
+ ATTR_WARN_UNUSED_RESULT;
+int str_parse_uint32(const char *str, uint32_t *num_r,
+ const char **endp_r) ATTR_WARN_UNUSED_RESULT ATTR_NULL(3);
+
+int str_to_uint64(const char *str, uint64_t *num_r)
+ ATTR_WARN_UNUSED_RESULT;
+int str_parse_uint64(const char *str, uint64_t *num_r,
+ const char **endp_r) ATTR_WARN_UNUSED_RESULT ATTR_NULL(3);
+
+int str_to_uintmax(const char *str, uintmax_t *num_r)
+ ATTR_WARN_UNUSED_RESULT;
+int str_parse_uintmax(const char *str, uintmax_t *num_r,
+ const char **endp_r) ATTR_WARN_UNUSED_RESULT ATTR_NULL(3);
+
+/* Returns TRUE if str is a valid unsigned number that equals to num. */
+bool str_uint_equals(const char *str, uintmax_t num);
+
+/*
+ * Unsigned hexadecimal
+ */
+
+int str_to_uint_hex(const char *str, unsigned int *num_r)
+ ATTR_WARN_UNUSED_RESULT;
+int str_parse_uint_hex(const char *str, unsigned int *num_r,
+ const char **endp_r) ATTR_WARN_UNUSED_RESULT ATTR_NULL(3);
+
+int str_to_ulong_hex(const char *str, unsigned long *num_r)
+ ATTR_WARN_UNUSED_RESULT;
+int str_parse_ulong_hex(const char *str, unsigned long *num_r,
+ const char **endp_r) ATTR_WARN_UNUSED_RESULT ATTR_NULL(3);
+
+int str_to_ullong_hex(const char *str, unsigned long long *num_r)
+ ATTR_WARN_UNUSED_RESULT;
+int str_parse_ullong_hex(const char *str, unsigned long long *num_r,
+ const char **endp_r) ATTR_WARN_UNUSED_RESULT ATTR_NULL(3);
+
+int str_to_uint32_hex(const char *str, uint32_t *num_r)
+ ATTR_WARN_UNUSED_RESULT;
+int str_parse_uint32_hex(const char *str, uint32_t *num_r,
+ const char **endp_r) ATTR_WARN_UNUSED_RESULT ATTR_NULL(3);
+
+int str_to_uint64_hex(const char *str, uint64_t *num_r)
+ ATTR_WARN_UNUSED_RESULT;
+int str_parse_uint64_hex(const char *str, uint64_t *num_r,
+ const char **endp_r) ATTR_WARN_UNUSED_RESULT ATTR_NULL(3);
+
+int str_to_uintmax_hex(const char *str, uintmax_t *num_r)
+ ATTR_WARN_UNUSED_RESULT;
+int str_parse_uintmax_hex(const char *str, uintmax_t *num_r,
+ const char **endp_r) ATTR_WARN_UNUSED_RESULT ATTR_NULL(3);
+
+/*
+ * Unsigned octal
+ */
+
+int str_to_uint_oct(const char *str, unsigned int *num_r)
+ ATTR_WARN_UNUSED_RESULT;
+int str_parse_uint_oct(const char *str, unsigned int *num_r,
+ const char **endp_r) ATTR_WARN_UNUSED_RESULT ATTR_NULL(3);
+
+int str_to_ulong_oct(const char *str, unsigned long *num_r)
+ ATTR_WARN_UNUSED_RESULT;
+int str_parse_ulong_oct(const char *str, unsigned long *num_r,
+ const char **endp_r) ATTR_WARN_UNUSED_RESULT ATTR_NULL(3);
+
+int str_to_ullong_oct(const char *str, unsigned long long *num_r)
+ ATTR_WARN_UNUSED_RESULT;
+int str_parse_ullong_oct(const char *str, unsigned long long *num_r,
+ const char **endp_r) ATTR_WARN_UNUSED_RESULT ATTR_NULL(3);
+
+int str_to_uint32_oct(const char *str, uint32_t *num_r)
+ ATTR_WARN_UNUSED_RESULT;
+int str_parse_uint32_oct(const char *str, uint32_t *num_r,
+ const char **endp_r) ATTR_WARN_UNUSED_RESULT ATTR_NULL(3);
+
+int str_to_uint64_oct(const char *str, uint64_t *num_r)
+ ATTR_WARN_UNUSED_RESULT;
+int str_parse_uint64_oct(const char *str, uint64_t *num_r,
+ const char **endp_r) ATTR_WARN_UNUSED_RESULT ATTR_NULL(3);
+
+int str_to_uintmax_oct(const char *str, uintmax_t *num_r)
+ ATTR_WARN_UNUSED_RESULT;
+int str_parse_uintmax_oct(const char *str, uintmax_t *num_r,
+ const char **endp_r) ATTR_WARN_UNUSED_RESULT ATTR_NULL(3);
+
+/*
+ * Signed
+ */
+
+int str_to_int(const char *str, int *num_r)
+ ATTR_WARN_UNUSED_RESULT;
+int str_parse_int(const char *str, int *num_r,
+ const char **endp_r) ATTR_WARN_UNUSED_RESULT ATTR_NULL(3);
+
+int str_to_long(const char *str, long *num_r)
+ ATTR_WARN_UNUSED_RESULT;
+int str_parse_long(const char *str, long *num_r,
+ const char **endp_r) ATTR_WARN_UNUSED_RESULT ATTR_NULL(3);
+
+int str_to_llong(const char *str, long long *num_r)
+ ATTR_WARN_UNUSED_RESULT;
+int str_parse_llong(const char *str, long long *num_r,
+ const char **endp_r) ATTR_WARN_UNUSED_RESULT ATTR_NULL(3);
+
+int str_to_int32(const char *str, int32_t *num_r)
+ ATTR_WARN_UNUSED_RESULT;
+int str_parse_int32(const char *str, int32_t *num_r,
+ const char **endp_r) ATTR_WARN_UNUSED_RESULT ATTR_NULL(3);
+
+int str_to_int64(const char *str, int64_t *num_r)
+ ATTR_WARN_UNUSED_RESULT;
+int str_parse_int64(const char *str, int64_t *num_r,
+ const char **endp_r) ATTR_WARN_UNUSED_RESULT ATTR_NULL(3);
+
+int str_to_intmax(const char *str, intmax_t *num_r)
+ ATTR_WARN_UNUSED_RESULT;
+int str_parse_intmax(const char *str, intmax_t *num_r,
+ const char **endp_r) ATTR_WARN_UNUSED_RESULT ATTR_NULL(3);
+
+/*
+ * Special numeric types
+ */
+
+int str_to_uid(const char *str, uid_t *num_r)
+ ATTR_WARN_UNUSED_RESULT;
+
+int str_to_gid(const char *str, gid_t *num_r)
+ ATTR_WARN_UNUSED_RESULT;
+
+int str_to_pid(const char *str, pid_t *num_r)
+ ATTR_WARN_UNUSED_RESULT;
+
+int str_to_ino(const char *str, ino_t *num_r)
+ ATTR_WARN_UNUSED_RESULT;
+
+int str_to_uoff(const char *str, uoff_t *num_r)
+ ATTR_WARN_UNUSED_RESULT;
+int str_parse_uoff(const char *str, uoff_t *num_r,
+ const char **endp_r) ATTR_WARN_UNUSED_RESULT ATTR_NULL(3);
+
+int str_to_time(const char *str, time_t *num_r)
+ ATTR_WARN_UNUSED_RESULT;
+
+/*
+ * Utility
+ */
+
+/* Return TRUE if all characters in string are numbers.
+ Stop when `end_char' is found from string. */
+bool str_is_numeric(const char *str, char end_char) ATTR_PURE;
+
+/* Return TRUE when string has one or more numbers, followed
+ with zero or one dot, followed with at least one number. */
+bool str_is_float(const char *str, char end_char) ATTR_PURE;
+
+/* Returns human readable string about what is wrong with the string.
+ This function assumes that str_to_*() had already returned -1 for the
+ string. */
+const char *str_num_error(const char *str);
+
+#endif
diff --git a/src/lib/test-aqueue.c b/src/lib/test-aqueue.c
new file mode 100644
index 0000000..0ff03ac
--- /dev/null
+++ b/src/lib/test-aqueue.c
@@ -0,0 +1,73 @@
+/* Copyright (c) 2007-2018 Dovecot authors, see the included COPYING file */
+
+#include "test-lib.h"
+#include "array.h"
+#include "aqueue.h"
+
+static bool aqueue_is_ok(struct aqueue *aqueue, unsigned int deleted_n)
+{
+ const unsigned int *p;
+ unsigned int n, i, count;
+
+ count = aqueue_count(aqueue);
+ for (i = 0, n = 1; i < count; i++, n++) {
+ p = array_idx_i(aqueue->arr, aqueue_idx(aqueue, i));
+ if (i == deleted_n)
+ n++;
+ if (*p != n)
+ return FALSE;
+ }
+ return TRUE;
+}
+
+static const unsigned int aqueue_input[] = { 1, 2, 3, 4, 5, 6 };
+static const char *test_aqueue2(unsigned int initial_size)
+{
+ ARRAY(unsigned int) aqueue_array;
+ unsigned int i, j, k;
+
+ for (i = 0; i < N_ELEMENTS(aqueue_input); i++) {
+ for (k = 0; k < N_ELEMENTS(aqueue_input); k++) {
+ struct aqueue *aqueue;
+
+ t_array_init(&aqueue_array, initial_size);
+ aqueue = aqueue_init(&aqueue_array.arr);
+ aqueue->head = aqueue->tail = initial_size - 1;
+ for (j = 0; j < k; j++) {
+ aqueue_append(aqueue, &aqueue_input[j]);
+ if (aqueue_count(aqueue) != j + 1) {
+ return t_strdup_printf("Wrong count after append %u vs %u)",
+ aqueue_count(aqueue), j + 1);
+ }
+ if (!aqueue_is_ok(aqueue, UINT_MAX))
+ return "Invalid data after append";
+ }
+
+ if (k != 0 && i < k) {
+ aqueue_delete(aqueue, i);
+ if (aqueue_count(aqueue) != k - 1)
+ return "Wrong count after delete";
+ if (!aqueue_is_ok(aqueue, i))
+ return "Invalid data after delete";
+ }
+ aqueue_clear(aqueue);
+ if (aqueue_count(aqueue) != 0)
+ return "aqueue_clear() broken";
+ aqueue_deinit(&aqueue);
+ }
+ }
+ return NULL;
+}
+
+void test_aqueue(void)
+{
+ unsigned int i;
+ const char *reason = NULL;
+
+ for (i = 1; i <= N_ELEMENTS(aqueue_input) + 1 && reason == NULL; i++) {
+ T_BEGIN {
+ reason = test_aqueue2(i);
+ } T_END;
+ }
+ test_out_reason("aqueue", reason == NULL, reason);
+}
diff --git a/src/lib/test-array.c b/src/lib/test-array.c
new file mode 100644
index 0000000..e6aade1
--- /dev/null
+++ b/src/lib/test-array.c
@@ -0,0 +1,441 @@
+/* Copyright (c) 2007-2018 Dovecot authors, see the included COPYING file */
+
+#include "test-lib.h"
+#include "array.h"
+
+
+struct foo {
+ unsigned int a, b, c;
+};
+
+static void test_array_elem(void)
+{
+ ARRAY(struct foo *) foos;
+ struct foo *nfoo;
+ struct foo *foo;
+ struct foo local_foo;
+ unsigned int i;
+
+ test_begin("array elem");
+ t_array_init(&foos, 32);
+
+ foo = &local_foo;
+ array_foreach_elem(&foos, foo)
+ test_assert(FALSE);
+ test_assert(foo == &local_foo);
+
+ for (i = 1; i <= 3; i++) {
+ nfoo = t_new(struct foo, 1);
+ nfoo->a = i;
+ array_push_back(&foos, &nfoo);
+ }
+
+ struct foo *const *foo_p = array_idx(&foos, 1);
+ unsigned int idx = 1;
+ foo = array_idx_elem(&foos, idx++);
+ /* make sure idx isn't expanded multiple times in the macro */
+ test_assert(idx == 2);
+ test_assert(*foo_p == foo);
+
+ i = 1;
+ array_foreach_elem(&foos, foo) {
+ test_assert(foo->a == i);
+ i++;
+ }
+ test_assert(foo->a == i-1);
+ test_end();
+}
+
+static void test_array_count(void)
+{
+ ARRAY(struct foo) foos;
+ struct foo nfoo;
+
+ test_begin("array count/empty");
+ t_array_init(&foos, 32);
+
+ test_assert(array_count(&foos) == 0);
+ test_assert(array_is_empty(&foos));
+ test_assert(!array_not_empty(&foos));
+ nfoo.a = nfoo.b = nfoo.c = 9;
+ array_push_back(&foos, &nfoo);
+ test_assert(array_count(&foos) == 1);
+ test_assert(!array_is_empty(&foos));
+ test_assert(array_not_empty(&foos));
+
+ test_end();
+}
+static void test_array_foreach(void)
+{
+ ARRAY(struct foo) foos;
+ const struct foo *foo;
+ struct foo nfoo;
+ unsigned int i;
+
+ test_begin("array foreach");
+ t_array_init(&foos, 32);
+ for (i = 0; i < 10; i++) {
+ nfoo.a = nfoo.b = nfoo.c = i;
+ array_push_back(&foos, &nfoo);
+ }
+
+ array_foreach(&foos, foo) {
+ i = array_foreach_idx(&foos, foo);
+ test_assert(foo->a == i);
+ test_assert(foo->b == i);
+ test_assert(foo->c == i);
+ }
+ /* points past the last element */
+ test_assert(foo == array_idx(&foos, i)+1);
+ test_end();
+}
+
+static void test_array_foreach_reverse(void)
+{
+ ARRAY(unsigned int) arr;
+ const unsigned int *i_p;
+ unsigned int i, i2, *imod_p;
+
+ test_begin("array foreach reverse");
+ t_array_init(&arr, 32);
+
+ /* first test that array_foreach() + array_delete() doesn't really
+ work as we might hope.. */
+ for (i = 1; i <= 5; i++)
+ array_push_back(&arr, &i);
+ array_foreach(&arr, i_p) {
+ i = array_foreach_idx(&arr, i_p);
+ array_delete(&arr, i, 1);
+ }
+ test_assert(array_count(&arr) == 2);
+
+ /* but using array_foreach_reverse() + array_delete() does work: */
+ array_clear(&arr);
+ i2 = 5;
+ for (i = 1; i <= i2; i++)
+ array_push_back(&arr, &i);
+ array_foreach_reverse(&arr, i_p) {
+ i = array_foreach_idx(&arr, i_p);
+ test_assert(*i_p == i2);
+ test_assert(*i_p == i + 1);
+ array_delete(&arr, i, 1);
+ i2--;
+ }
+ test_assert(array_count(&arr) == 0);
+
+ /* also array_foreach_reverse_modifiable() + array_delete() works: */
+ i2 = 5;
+ for (i = 1; i <= i2; i++)
+ array_push_back(&arr, &i);
+ array_foreach_reverse_modifiable(&arr, imod_p) {
+ i = array_foreach_idx(&arr, imod_p);
+ test_assert(*imod_p == i2);
+ test_assert(*imod_p == i + 1);
+ array_delete(&arr, i, 1);
+ i2--;
+ }
+ test_assert(array_count(&arr) == 0);
+
+ test_end();
+}
+
+static void test_array_foreach_elem_string(void)
+{
+ ARRAY(char *) blurbs;
+ ARRAY(const char *) cblurbs;
+ char *string;
+ const char *cstring;
+ int i;
+
+ test_begin("array foreach_elem ro/rw strings");
+ t_array_init(&blurbs, 32);
+ t_array_init(&cblurbs, 32);
+ for (i = 0; i < 10; i++) {
+ cstring = t_strdup_printf("x%iy", i);
+ string = t_strdup_noconst(cstring);
+ array_push_back(&blurbs, &string);
+ array_push_back(&cblurbs, &cstring);
+ }
+
+ i = 0;
+ array_foreach_elem(&blurbs, string) {
+ test_assert_idx(string[0] == 'x' && string[1]-'0' == i && string[2] == 'y', i);
+ i++;
+ }
+ i = 0;
+ array_foreach_elem(&cblurbs, cstring) {
+ test_assert_idx(cstring[0] == 'x' && cstring[1]-'0' == i && cstring[2] == 'y', i);
+ i++;
+ }
+ test_end();
+}
+
+static int test_int_compare(const int *key, const int *elem)
+{
+ return (*key < *elem) ? -1 :
+ (*key > *elem) ? 1 :
+ 0;
+}
+static void test_array_reverse(void)
+{
+ ARRAY(int) intarr;
+ int input[] = { -1234567890, -272585721, 272485922, 824725652 };
+ const int tmpi = 999, *output;
+ unsigned int i, j;
+
+ test_begin("array reverse");
+ t_array_init(&intarr, 5);
+ for (i = 0; i <= N_ELEMENTS(input); i++) {
+ array_clear(&intarr);
+ array_append(&intarr, input, i);
+ array_reverse(&intarr);
+
+ output = i == 0 ? NULL : array_front(&intarr);
+ for (j = 0; j < i; j++)
+ test_assert(input[i-j-1] == output[j]);
+ }
+ test_end();
+
+ test_begin("array_lsearch");
+ for (i = 0; i < N_ELEMENTS(input); i++) {
+ output = array_lsearch(&intarr, &input[i], test_int_compare);
+ test_assert(output != NULL);
+ j = array_ptr_to_idx(&intarr, output);
+ test_assert_idx(j == N_ELEMENTS(input) - 1 - i, i);
+ }
+ output = array_lsearch(&intarr, &tmpi, test_int_compare);
+ test_assert(output == NULL);
+ test_end();
+}
+static int test_compare_ushort(const unsigned short *c1, const unsigned short *c2)
+{
+ return *c1 > *c2 ? 1
+ : *c1 < *c2 ? -1
+ : 0;
+}
+static int test_compare_ushort_fuzz(const unsigned short *c1, const unsigned short *c2, const int *pfuzz)
+{
+ int d = (int)*c1 - (int)*c2;
+ if (d <= *pfuzz && -d <= *pfuzz)
+ return 0;
+ return d;
+}
+static void test_array_cmp(void)
+{
+ static const unsigned short deltas[] = {
+ 0x8000, 0xc000, 0xfe00, 0xff00, 0xff80, 0xffc0, 0xfffe, 0xffff,
+ 0, 1, 2, 64, 128, 256, 512, 16384, 32768
+ };
+
+#define NELEMS 5u
+ ARRAY(unsigned short) arr1, arr2;
+ unsigned short elems[NELEMS+1];
+ unsigned int i;
+ int fuzz;
+
+ test_begin("array compare (ushort)");
+ t_array_init(&arr1, NELEMS);
+ t_array_init(&arr2, NELEMS);
+ for (i = 0; i < NELEMS; i++) {
+ elems[i] = i_rand_ushort();
+ array_push_back(&arr2, &elems[i]);
+ }
+ array_append(&arr1, elems, NELEMS);
+ test_assert(array_cmp(&arr1, &arr2) == TRUE);
+ test_assert(array_equal_fn(&arr1, &arr2, test_compare_ushort) == TRUE);
+ fuzz = 0;
+ test_assert(array_equal_fn_ctx(&arr1, &arr2, test_compare_ushort_fuzz, &fuzz) == TRUE);
+
+ for (i = 0; i < 256; i++) {
+ unsigned int j = i_rand_limit(NELEMS);
+ const unsigned short *ptmp = array_idx(&arr2, j);
+ unsigned short tmp = *ptmp;
+ unsigned short repl = ((unsigned int)tmp +
+ deltas[i_rand_limit(N_ELEMENTS(deltas))]) & 0xffff;
+
+ array_idx_set(&arr2, j, &repl);
+ test_assert_idx(array_cmp(&arr1, &arr2) == (tmp == repl), i);
+ test_assert_idx(array_equal_fn(&arr1, &arr2, test_compare_ushort) == (tmp == repl), i);
+ fuzz = (int)tmp - (int)repl;
+ if (fuzz < 0)
+ fuzz = -fuzz;
+ test_assert_idx(array_equal_fn_ctx(&arr1, &arr2, test_compare_ushort_fuzz, &fuzz) == TRUE, i);
+ if (fuzz > 0) {
+ fuzz--;
+ test_assert_idx(array_equal_fn_ctx(&arr1, &arr2, test_compare_ushort_fuzz, &fuzz) == FALSE, i);
+ }
+ array_idx_set(&arr2, j, &tmp);
+ test_assert_idx(array_cmp(&arr1, &arr2) == TRUE, i);
+ test_assert_idx(array_equal_fn(&arr1, &arr2, test_compare_ushort) == TRUE, i);
+ fuzz = 0;
+ test_assert_idx(array_equal_fn_ctx(&arr1, &arr2, test_compare_ushort_fuzz, &fuzz) == TRUE, i);
+ }
+ elems[NELEMS] = 0;
+ array_push_back(&arr2, &elems[NELEMS]);
+ test_assert(array_cmp(&arr1, &arr2) == FALSE);
+ test_assert(array_equal_fn(&arr1, &arr2, test_compare_ushort) == FALSE);
+ test_assert_idx(array_equal_fn_ctx(&arr1, &arr2, test_compare_ushort_fuzz, &fuzz) == FALSE, i);
+
+ test_end();
+}
+
+static void test_array_cmp_str(void)
+{
+#define NELEMS 5u
+ ARRAY(const char *) arr1, arr2;
+ const char *elemstrs[NELEMS+1];
+ unsigned int i;
+
+ test_begin("array compare (char*)");
+ t_array_init(&arr1, NELEMS);
+ t_array_init(&arr2, NELEMS);
+ for (i = 0; i < NELEMS; i++) {
+ elemstrs[i] = t_strdup_printf("%x", i_rand()); /* never 0-length */
+ array_push_back(&arr2, &elemstrs[i]);
+ }
+ array_append(&arr1, elemstrs, NELEMS);
+ test_assert(array_cmp(&arr1, &arr2) == TRUE); /* pointers shared, so identical */
+ test_assert(array_equal_fn(&arr1, &arr2, i_strcmp_p) == TRUE); /* therefore value same */
+ for (i = 0; i < 2560; i++) {
+ unsigned int j = i_rand_limit(NELEMS);
+ const char *const *ostr_p = array_idx(&arr2, j);
+ const char *ostr = *ostr_p;
+ unsigned int olen = strlen(ostr);
+ unsigned int rc = i_rand_limit(olen + 1);
+ char ochar = ostr[rc];
+ char buf[12];
+ const char *bufp = buf;
+ memcpy(buf, ostr, olen+1);
+ buf[rc] = (int32_t)i_rand_limit(CHAR_MAX + 1 - CHAR_MIN) + CHAR_MIN;
+ if(rc == olen)
+ buf[rc+1] = '\0';
+ array_idx_set(&arr2, j, &bufp);
+ test_assert(array_cmp(&arr1, &arr2) == FALSE); /* pointers now differ */
+ test_assert_idx(array_equal_fn(&arr1, &arr2, i_strcmp_p)
+ == (strcmp(ostr, buf) == 0), i); /* sometimes still the same */
+ test_assert_idx(array_equal_fn(&arr1, &arr2, i_strcmp_p)
+ == (ochar == buf[rc]), i); /* ditto */
+ array_idx_set(&arr2, j, &ostr);
+ test_assert(array_cmp(&arr1, &arr2) == TRUE); /* pointers now same again */
+ test_assert_idx(array_equal_fn(&arr1, &arr2, i_strcmp_p) == TRUE, i); /* duh! */
+ }
+ /* length differences being detected are tested in other tests */
+ test_end();
+}
+
+static void
+test_array_free_case(bool keep)
+{
+ pool_t pool = pool_allocfree_create("array test");
+ ARRAY(int) r;
+ int *p;
+
+ test_begin(keep ? "array_free" : "array_free_without_data");
+
+ p_array_init(&r, pool, 100);
+ array_append_zero(&r);
+ if (keep) {
+ p = array_free_without_data(&r);
+ test_assert(pool_allocfree_get_total_used_size(pool)>=400);
+ p_free(pool, p);
+ } else {
+ array_free(&r);
+ test_assert(pool_allocfree_get_total_used_size(pool)==0);
+ }
+ pool_unref(&pool);
+ test_end();
+}
+static void
+test_array_free(void)
+{
+ test_array_free_case(FALSE);
+ test_array_free_case(TRUE);
+}
+
+void test_array(void)
+{
+ test_array_elem();
+ test_array_count();
+ test_array_foreach();
+ test_array_foreach_reverse();
+ test_array_foreach_elem_string();
+ test_array_reverse();
+ test_array_cmp();
+ test_array_cmp_str();
+ test_array_free();
+}
+
+enum fatal_test_state fatal_array(unsigned int stage)
+{
+ double tmpd[2] = { 42., -42. };
+ short tmps[8] = {1,2,3,4,5,6,7,8};
+ static const void *useless_ptr; /* persuade gcc to not optimise the tests */
+
+ switch(stage) {
+ case 0: {
+ ARRAY(double) ad;
+ test_begin("fatal_array");
+ t_array_init(&ad, 3);
+ /* allocation big enough, but memory not initialised */
+ test_expect_fatal_string("(array_idx_i): assertion failed: (idx < array->buffer->used / array->element_size)");
+ useless_ptr = array_front(&ad);
+ return FATAL_TEST_FAILURE;
+ }
+
+ case 1: {
+ ARRAY(double) ad;
+ t_array_init(&ad, 2);
+ array_append(&ad, tmpd, 2);
+ /* actual out of range address requested */
+ test_expect_fatal_string("(array_idx_i): assertion failed: (idx < array->buffer->used / array->element_size)");
+ useless_ptr = array_idx(&ad, 2);
+ return FATAL_TEST_FAILURE;
+ }
+
+ case 2: {
+ ARRAY(double) ad;
+ ARRAY(short) as;
+ t_array_init(&ad, 2);
+ t_array_init(&as, 8);
+ array_append(&as, tmps, 2);
+ /* can't copy different array sizes */
+ test_expect_fatal_string("(array_copy): assertion failed: (dest->element_size == src->element_size)");
+ array_copy(&ad.arr, 1, &as.arr, 0, 4);
+ return FATAL_TEST_FAILURE;
+ }
+ case 3: {
+ ARRAY(uint8_t) arr;
+ /* Allocate value dynamically, so compiler won't know the
+ allocated memory size and output a warning that it's too
+ small for array_append(). */
+ uint8_t *value = t_malloc0(1);
+
+ t_array_init(&arr, 2);
+ array_push_back(&arr, value);
+ test_expect_fatal_string("Buffer write out of range");
+ /* this is supposed to assert-crash before it even attempts to
+ access value */
+ array_append(&arr, value, UINT_MAX);
+ return FATAL_TEST_FAILURE;
+ }
+ case 4: {
+ ARRAY(uint32_t) arr;
+ /* Allocate value dynamically (see above for reasoning). */
+ uint32_t *value = t_malloc0(1);
+
+ t_array_init(&arr, 2);
+ array_push_back(&arr, value);
+ test_expect_fatal_string("Buffer write out of range");
+ /* this is supposed to assert-crash before it even attempts to
+ access value */
+ array_append(&arr, value, UINT_MAX);
+ return FATAL_TEST_FAILURE;
+ }
+ }
+ test_end();
+ /* Forces the compiler to check the value of useless_ptr, so that it
+ must call array_idx (which is marked as pure, and gcc was desperate
+ to optimise out. Of course, gcc is unaware stage is never UINT_MAX.*/
+ return (useless_ptr != NULL && stage == UINT_MAX)
+ ? FATAL_TEST_FAILURE : FATAL_TEST_FINISHED;
+}
diff --git a/src/lib/test-backtrace.c b/src/lib/test-backtrace.c
new file mode 100644
index 0000000..fdebe0f
--- /dev/null
+++ b/src/lib/test-backtrace.c
@@ -0,0 +1,57 @@
+#include "test-lib.h"
+#include "str.h"
+#include "backtrace-string.h"
+
+static void test_backtrace_append(void)
+{
+ test_begin("backtrace_append");
+ string_t *bt = t_str_new(128);
+#if (defined(HAVE_LIBUNWIND))
+ test_assert(backtrace_append(bt) == 0);
+ /* Check that there's a usable function in the backtrace.
+ Note that this function may be inlined, so don't check for
+ test_backtrace_get() */
+ test_assert(strstr(str_c(bt), "test_backtrace") != NULL);
+ /* make sure the backtrace_append is not */
+ test_assert(strstr(str_c(bt), " backtrace_append") == NULL);
+#elif (defined(HAVE_BACKTRACE_SYMBOLS) && defined(HAVE_EXECINFO_H)) || \
+ (defined(HAVE_WALKCONTEXT) && defined(HAVE_UCONTEXT_H))
+ test_assert(backtrace_append(bt) == 0);
+ /* it should have some kind of main in it */
+ test_assert(strstr(str_c(bt), "main") != NULL);
+#else
+ /* should not work in this context */
+ test_assert(backtrace_append(bt) == -1);
+#endif
+ test_end();
+}
+
+static void test_backtrace_get(void)
+{
+ test_begin("backtrace_get");
+ const char *bt = NULL;
+#if (defined(HAVE_LIBUNWIND))
+ test_assert(backtrace_get(&bt) == 0);
+ /* Check that there's a usable function in the backtrace.
+ Note that this function may be inlined, so don't check for
+ test_backtrace_get() */
+ test_assert(strstr(bt, "test_backtrace") != NULL);
+ /* make sure the backtrace_get is not */
+ test_assert(strstr(bt, " backtrace_get") == NULL);
+#elif (defined(HAVE_BACKTRACE_SYMBOLS) && defined(HAVE_EXECINFO_H)) || \
+ (defined(HAVE_WALKCONTEXT) && defined(HAVE_UCONTEXT_H))
+ test_assert(backtrace_get(&bt) == 0);
+ /* it should have some kind of main in it */
+ test_assert(strstr(bt, "main") != NULL);
+#else
+ /* should not work in this context */
+ test_assert(backtrace_get(&bt) == -1);
+#endif
+ test_end();
+}
+
+void test_backtrace(void)
+{
+ test_backtrace_append();
+ test_backtrace_get();
+}
diff --git a/src/lib/test-base32.c b/src/lib/test-base32.c
new file mode 100644
index 0000000..072dae8
--- /dev/null
+++ b/src/lib/test-base32.c
@@ -0,0 +1,189 @@
+/* Copyright (c) 2007-2018 Dovecot authors, see the included COPYING file */
+
+#include "test-lib.h"
+#include "str.h"
+#include "base32.h"
+
+
+static void test_base32_encode(void)
+{
+ static const char *input[] = {
+ "toedeledokie!!",
+ "bye bye world",
+ "hoeveel onzin kun je testen?????",
+ "c'est pas vrai! ",
+ "dit is het einde van deze test"
+ };
+ static const char *output[] = {
+ "ORXWKZDFNRSWI33LNFSSCII=",
+ "MJ4WKIDCPFSSA53POJWGI===",
+ "NBXWK5TFMVWCA33OPJUW4IDLOVXCA2TFEB2GK43UMVXD6PZ7H47Q====",
+ "MMTWK43UEBYGC4ZAOZZGC2JBEA======",
+ "MRUXIIDJOMQGQZLUEBSWS3TEMUQHMYLOEBSGK6TFEB2GK43U"
+ };
+ string_t *str;
+ unsigned int i;
+
+ test_begin("base32_encode() with padding");
+ str = t_str_new(256);
+ for (i = 0; i < N_ELEMENTS(input); i++) {
+ str_truncate(str, 0);
+ base32_encode(TRUE, input[i], strlen(input[i]), str);
+ test_assert(strcmp(output[i], str_c(str)) == 0);
+ }
+ test_end();
+
+ test_begin("base32_encode() no padding");
+ str = t_str_new(256);
+ for (i = 0; i < N_ELEMENTS(input); i++) {
+ const char *p = strchr(output[i], '=');
+ size_t len;
+
+ if (p == NULL)
+ len = strlen(output[i]);
+ else
+ len = (size_t)(p - output[i]);
+ str_truncate(str, 0);
+ base32_encode(FALSE, input[i], strlen(input[i]), str);
+ test_assert(strncmp(output[i], str_c(str), len) == 0);
+ }
+ test_end();
+}
+
+static void test_base32hex_encode(void)
+{
+ static const char *input[] = {
+ "toedeledokie!!",
+ "bye bye world",
+ "hoeveel onzin kun je testen?????",
+ "c'est pas vrai! ",
+ "dit is het einde van deze test"
+ };
+ static const char *output[] = {
+ "EHNMAP35DHIM8RRBD5II288=",
+ "C9SMA832F5II0TRFE9M68===",
+ "D1NMATJ5CLM20RREF9KMS83BELN20QJ541Q6ASRKCLN3UFPV7SVG====",
+ "CCJMASRK41O62SP0EPP62Q9140======",
+ "CHKN8839ECG6GPBK41IMIRJ4CKG7COBE41I6AUJ541Q6ASRK"
+ };
+ string_t *str;
+ unsigned int i;
+
+ test_begin("base32hex_encode() with padding");
+ str = t_str_new(256);
+ for (i = 0; i < N_ELEMENTS(input); i++) {
+ str_truncate(str, 0);
+ base32hex_encode(TRUE, input[i], strlen(input[i]), str);
+ test_assert(strcmp(output[i], str_c(str)) == 0);
+ }
+ test_end();
+
+ test_begin("base32hex_encode() no padding");
+ str = t_str_new(256);
+ for (i = 0; i < N_ELEMENTS(input); i++) {
+ const char *p = strchr(output[i], '=');
+ size_t len;
+
+ if (p == NULL)
+ len = strlen(output[i]);
+ else
+ len = (size_t)(p - output[i]);
+ str_truncate(str, 0);
+ base32hex_encode(FALSE, input[i], strlen(input[i]), str);
+ test_assert(strncmp(output[i], str_c(str), len) == 0);
+ }
+ test_end();
+
+}
+
+struct test_base32_decode_output {
+ const char *text;
+ int ret;
+ unsigned int src_pos;
+};
+
+static void test_base32_decode(void)
+{
+ static const char *input[] = {
+ "ORXWKZDFNRSWI33LNFSSCII=",
+ "MJ4WKIDCPFSSA53POJWGI===",
+ "NBXWK5TFMVWCA33OPJUW4IDLOVXCA2TFEB2GK43UMVXD6PZ7H47Q====",
+ "MMTWK43UEBYGC4ZAOZZGC2JBEA======",
+ "MRUXIIDJOMQGQZLUEBSWS3TEMUQHMYLOEBSGK6TFEB2GK43U"
+ };
+ static const struct test_base32_decode_output output[] = {
+ { "toedeledokie!!", 0, 24 },
+ { "bye bye world", 0, 24 },
+ { "hoeveel onzin kun je testen?????", 0, 56 },
+ { "c'est pas vrai! ", 0, 32 },
+ { "dit is het einde van deze test", 1, 48 },
+ };
+ string_t *str;
+ unsigned int i;
+ size_t src_pos;
+ int ret;
+
+ test_begin("base32_decode()");
+ str = t_str_new(256);
+ for (i = 0; i < N_ELEMENTS(input); i++) {
+ str_truncate(str, 0);
+
+ src_pos = 0;
+ ret = base32_decode(input[i], strlen(input[i]), &src_pos, str);
+
+ test_assert(output[i].ret == ret &&
+ strcmp(output[i].text, str_c(str)) == 0 &&
+ (src_pos == output[i].src_pos ||
+ (output[i].src_pos == UINT_MAX &&
+ src_pos == strlen(input[i]))));
+ }
+ test_end();
+}
+
+static void test_base32_random(void)
+{
+ string_t *str, *dest;
+ unsigned char buf[10];
+ unsigned int i, j, max;
+
+ str = t_str_new(256);
+ dest = t_str_new(256);
+
+ test_begin("padded base32 encode/decode with random input");
+ for (i = 0; i < 1000; i++) {
+ max = i_rand_limit(sizeof(buf));
+ for (j = 0; j < max; j++)
+ buf[j] = i_rand_uchar();
+
+ str_truncate(str, 0);
+ str_truncate(dest, 0);
+ base32_encode(TRUE, buf, max, str);
+ test_assert(base32_decode(str_data(str), str_len(str), NULL, dest) >= 0);
+ test_assert(str_len(dest) == max &&
+ memcmp(buf, str_data(dest), max) == 0);
+ }
+ test_end();
+
+ test_begin("padded base32hex encode/decode with random input");
+ for (i = 0; i < 1000; i++) {
+ max = i_rand_limit(sizeof(buf));
+ for (j = 0; j < max; j++)
+ buf[j] = i_rand_uchar();
+
+ str_truncate(str, 0);
+ str_truncate(dest, 0);
+ base32hex_encode(TRUE, buf, max, str);
+ test_assert(base32hex_decode(str_data(str), str_len(str), NULL, dest) >= 0);
+ test_assert(str_len(dest) == max &&
+ memcmp(buf, str_data(dest), max) == 0);
+ }
+ test_end();
+}
+
+void test_base32(void)
+{
+ test_base32_encode();
+ test_base32hex_encode();
+ test_base32_decode();
+ test_base32_random();
+}
diff --git a/src/lib/test-base64.c b/src/lib/test-base64.c
new file mode 100644
index 0000000..d024890
--- /dev/null
+++ b/src/lib/test-base64.c
@@ -0,0 +1,1317 @@
+/* Copyright (c) 2007-2018 Dovecot authors, see the included COPYING file */
+
+#include "test-lib.h"
+#include "str.h"
+#include "base64.h"
+
+static void test_base64_encode(void)
+{
+ const struct {
+ const char *input;
+ const char *output;
+ } tests[] = {
+ { "hello world", "aGVsbG8gd29ybGQ=" },
+ { "foo barits", "Zm9vIGJhcml0cw==" },
+ { "just niin", "anVzdCBuaWlu" },
+ { "\xe7\x8c\xbf\xe3\x82\x82\xe6\x9c\xa8\xe3\x81\x8b"
+ "\xe3\x82\x89\xe8\x90\xbd\xe3\x81\xa1\xe3\x82\x8b",
+ "54y/44KC5pyo44GL44KJ6JC944Gh44KL" },
+ { "\xe8\xa7\x92\xe3\x82\x92\xe7\x9f\xaf\xe3\x82\x81\xe3\x81"
+ "\xa6\xe7\x89\x9b\xe3\x82\x92\xe6\xae\xba\xe3\x81\x99",
+ "6KeS44KS55+v44KB44Gm54mb44KS5q6644GZ" },
+ };
+ string_t *str;
+ unsigned int i;
+
+ test_begin("base64_encode()");
+ str = t_str_new(256);
+ for (i = 0; i < N_ELEMENTS(tests); i++) {
+ str_truncate(str, 0);
+ base64_encode(tests[i].input, strlen(tests[i].input), str);
+ test_assert_idx(strcmp(tests[i].output, str_c(str)) == 0, i);
+ test_assert_idx(
+ str_len(str) == MAX_BASE64_ENCODED_SIZE(
+ strlen(tests[i].input)), i);
+ }
+ test_end();
+}
+
+struct test_base64_decode {
+ const char *input;
+ const char *output;
+ int ret;
+};
+
+static void test_base64_decode(void)
+{
+ static const struct test_base64_decode tests[] = {
+ { "", "", 0 },
+ { "\taGVsbG8gd29ybGQ=",
+ "hello world", 0 },
+ { "\nZm9v\n \tIGJh \t\ncml0cw==",
+ "foo barits", 0 },
+ { " anVzdCBuaWlu \n",
+ "just niin", 0 },
+ { "aGVsb",
+ "hel", -1 },
+ { "aGVsb!!!!!",
+ "hel", -1 },
+ { "aGVs!!!!!",
+ "hel", -1 },
+ { "0JPQvtCy0L7RgNGPzIHRgiwg0YfRgt"
+ "C+INC60YPRgCDQtNC+0Y/MgdGCLg==",
+ "\xd0\x93\xd0\xbe\xd0\xb2\xd0\xbe\xd1\x80\xd1\x8f\xcc"
+ "\x81\xd1\x82\x2c\x20\xd1\x87\xd1\x82\xd0\xbe\x20\xd0"
+ "\xba\xd1\x83\xd1\x80\x20\xd0\xb4\xd0\xbe\xd1\x8f\xcc"
+ "\x81\xd1\x82\x2e", 0 },
+ };
+ string_t *str;
+ buffer_t buf;
+ unsigned int i;
+ int ret;
+
+ test_begin("base64_decode()");
+ for (i = 0; i < N_ELEMENTS(tests); i++) {
+ /* Some of the base64_decode() callers use fixed size buffers.
+ Use a fixed size buffer here as well to test that
+ base64_decode() can't allocate any extra space even
+ temporarily. */
+ size_t max_decoded_size =
+ MAX_BASE64_DECODED_SIZE(strlen(tests[i].input));
+
+ buffer_create_from_data(&buf,
+ (max_decoded_size == 0 ? "" :
+ t_malloc0(max_decoded_size)),
+ max_decoded_size);
+ str = &buf;
+ ret = base64_decode(tests[i].input, strlen(tests[i].input),
+ NULL, str);
+
+ test_assert_idx(tests[i].ret == ret, i);
+ test_assert_idx(strlen(tests[i].output) == str_len(str) &&
+ memcmp(tests[i].output, str_data(str),
+ str_len(str)) == 0, i);
+ if (ret >= 0) {
+ test_assert_idx(
+ str_len(str) <= MAX_BASE64_DECODED_SIZE(
+ strlen(tests[i].input)), i);
+ }
+ }
+ test_end();
+}
+
+static void test_base64_random(void)
+{
+ string_t *str, *dest;
+ unsigned char buf[10];
+ unsigned int i, j, max;
+
+ str = t_str_new(256);
+ dest = t_str_new(256);
+
+ test_begin("base64 encode/decode with random input");
+ for (i = 0; i < 1000; i++) {
+ max = i_rand_limit(sizeof(buf));
+ for (j = 0; j < max; j++)
+ buf[j] = i_rand_uchar();
+
+ str_truncate(str, 0);
+ str_truncate(dest, 0);
+ base64_encode(buf, max, str);
+ test_assert_idx(base64_decode(str_data(str), str_len(str),
+ NULL, dest) >= 0, i);
+ test_assert_idx(str_len(dest) == max &&
+ memcmp(buf, str_data(dest), max) == 0, i);
+ }
+ test_end();
+}
+
+static void test_base64url_encode(void)
+{
+ const struct {
+ const char *input;
+ const char *output;
+ } tests[] = {
+ { "", "" },
+ { "hello world", "aGVsbG8gd29ybGQ=" },
+ { "foo barits", "Zm9vIGJhcml0cw==" },
+ { "just niin", "anVzdCBuaWlu" },
+ { "\xe7\x8c\xbf\xe3\x82\x82\xe6\x9c\xa8\xe3\x81\x8b"
+ "\xe3\x82\x89\xe8\x90\xbd\xe3\x81\xa1\xe3\x82\x8b",
+ "54y_44KC5pyo44GL44KJ6JC944Gh44KL" },
+ { "\xe8\xa7\x92\xe3\x82\x92\xe7\x9f\xaf\xe3\x82\x81\xe3\x81"
+ "\xa6\xe7\x89\x9b\xe3\x82\x92\xe6\xae\xba\xe3\x81\x99",
+ "6KeS44KS55-v44KB44Gm54mb44KS5q6644GZ" },
+ };
+ string_t *str;
+ unsigned int i;
+
+ test_begin("base64url_encode()");
+ str = t_str_new(256);
+ for (i = 0; i < N_ELEMENTS(tests); i++) {
+ str_truncate(str, 0);
+ base64url_encode(0, 0, tests[i].input, strlen(tests[i].input),
+ str);
+ test_assert_idx(strcmp(tests[i].output, str_c(str)) == 0, i);
+ test_assert_idx(
+ str_len(str) == MAX_BASE64_ENCODED_SIZE(
+ strlen(tests[i].input)), i);
+ }
+ test_end();
+}
+
+struct test_base64url_decode {
+ const char *input;
+ const char *output;
+ int ret;
+};
+
+static void test_base64url_decode(void)
+{
+ static const struct test_base64url_decode tests[] = {
+ { "", "", 0 },
+ { "\taGVsbG8gd29ybGQ=",
+ "hello world", 0 },
+ { "\nZm9v\n \tIGJh \t\ncml0cw==",
+ "foo barits", 0 },
+ { " anVzdCBuaWlu \n",
+ "just niin", 0 },
+ { "aGVsb",
+ "hel", -1 },
+ { "aGVsb!!!!!",
+ "hel", -1 },
+ { "aGVs!!!!!",
+ "hel", -1 },
+ { "0JPQvtCy0L7RgNGPzIHRgiwg0YfRgt"
+ "C-INC60YPRgCDQtNC-0Y_MgdGCLg==",
+ "\xd0\x93\xd0\xbe\xd0\xb2\xd0\xbe\xd1\x80\xd1\x8f\xcc"
+ "\x81\xd1\x82\x2c\x20\xd1\x87\xd1\x82\xd0\xbe\x20\xd0"
+ "\xba\xd1\x83\xd1\x80\x20\xd0\xb4\xd0\xbe\xd1\x8f\xcc"
+ "\x81\xd1\x82\x2e", 0 },
+ };
+ string_t *str;
+ buffer_t buf;
+ unsigned int i;
+ int ret;
+
+ test_begin("base64url_decode()");
+ for (i = 0; i < N_ELEMENTS(tests); i++) {
+ /* Some of the base64_decode() callers use fixed size buffers.
+ Use a fixed size buffer here as well to test that
+ base64_decode() can't allocate any extra space even
+ temporarily. */
+ size_t max_decoded_size =
+ MAX_BASE64_DECODED_SIZE(strlen(tests[i].input));
+
+ buffer_create_from_data(&buf,
+ (max_decoded_size == 0 ? "" :
+ t_malloc0(max_decoded_size)),
+ max_decoded_size);
+ str = &buf;
+ ret = base64url_decode(0, tests[i].input,
+ strlen(tests[i].input), str);
+
+ test_assert_idx(tests[i].ret == ret, i);
+ test_assert_idx(strlen(tests[i].output) == str_len(str) &&
+ memcmp(tests[i].output, str_data(str),
+ str_len(str)) == 0, i);
+ if (ret >= 0) {
+ test_assert_idx(
+ str_len(str) <= MAX_BASE64_DECODED_SIZE(
+ strlen(tests[i].input)), i);
+ }
+ }
+ test_end();
+}
+
+static void test_base64url_random(void)
+{
+ string_t *str, *dest;
+ unsigned char buf[10];
+ unsigned int i, j, max;
+
+ str = t_str_new(256);
+ dest = t_str_new(256);
+
+ test_begin("base64url encode/decode with random input");
+ for (i = 0; i < 1000; i++) {
+ max = i_rand_limit(sizeof(buf));
+ for (j = 0; j < max; j++)
+ buf[j] = i_rand_uchar();
+
+ str_truncate(str, 0);
+ str_truncate(dest, 0);
+ base64url_encode(0, 0, buf, max, str);
+ test_assert_idx(base64url_decode(0, str_data(str), str_len(str),
+ dest) >= 0, i);
+ test_assert_idx(str_len(dest) == max &&
+ memcmp(buf, str_data(dest), max) == 0, i);
+ }
+ test_end();
+}
+
+struct test_base64_encode_lowlevel {
+ const struct base64_scheme *scheme;
+ enum base64_encode_flags flags;
+ size_t max_line_len;
+
+ const char *input;
+ const char *output;
+};
+
+static const struct test_base64_encode_lowlevel
+tests_base64_encode_lowlevel[] = {
+ {
+ .scheme = &base64_scheme,
+ .input = "",
+ .output = "",
+ },
+ {
+ .scheme = &base64_scheme,
+ .max_line_len = 2,
+ .input = "",
+ .output = "",
+ },
+ {
+ .scheme = &base64_scheme,
+ .flags = BASE64_ENCODE_FLAG_CRLF,
+ .max_line_len = 2,
+ .input = "",
+ .output = "",
+ },
+ {
+ .scheme = &base64_scheme,
+ .flags = BASE64_ENCODE_FLAG_NO_PADDING,
+ .input = "",
+ .output = "",
+ },
+ {
+ .scheme = &base64_scheme,
+ .input = "hello world",
+ .output = "aGVsbG8gd29ybGQ=",
+ },
+ {
+ .scheme = &base64url_scheme,
+ .input = "hello world",
+ .output = "aGVsbG8gd29ybGQ=",
+ },
+ {
+ .scheme = &base64_scheme,
+ .flags = BASE64_ENCODE_FLAG_NO_PADDING,
+ .input = "hello world",
+ .output = "aGVsbG8gd29ybGQ",
+ },
+ {
+ .scheme = &base64_scheme,
+ .input = "foo barits",
+ .output = "Zm9vIGJhcml0cw==",
+ },
+ {
+ .scheme = &base64url_scheme,
+ .input = "foo barits",
+ .output = "Zm9vIGJhcml0cw==",
+ },
+ {
+ .scheme = &base64_scheme,
+ .flags = BASE64_ENCODE_FLAG_NO_PADDING,
+ .input = "foo barits",
+ .output = "Zm9vIGJhcml0cw",
+ },
+ {
+ .scheme = &base64_scheme,
+ .input = "just niin",
+ .output = "anVzdCBuaWlu",
+ },
+ {
+ .scheme = &base64url_scheme,
+ .input = "just niin",
+ .output = "anVzdCBuaWlu",
+ },
+ {
+ .scheme = &base64_scheme,
+ .flags = BASE64_ENCODE_FLAG_NO_PADDING,
+ .input = "just niin",
+ .output = "anVzdCBuaWlu",
+ },
+ {
+ .scheme = &base64_scheme,
+ .input =
+ "\xe7\x8c\xbf\xe3\x82\x82\xe6\x9c\xa8\xe3\x81\x8b"
+ "\xe3\x82\x89\xe8\x90\xbd\xe3\x81\xa1\xe3\x82\x8b",
+ .output = "54y/44KC5pyo44GL44KJ6JC944Gh44KL",
+ },
+ {
+ .scheme = &base64url_scheme,
+ .input =
+ "\xe7\x8c\xbf\xe3\x82\x82\xe6\x9c\xa8\xe3\x81\x8b"
+ "\xe3\x82\x89\xe8\x90\xbd\xe3\x81\xa1\xe3\x82\x8b",
+ .output = "54y_44KC5pyo44GL44KJ6JC944Gh44KL",
+ },
+ {
+ .scheme = &base64_scheme,
+ .input =
+ "\xe8\xa7\x92\xe3\x82\x92\xe7\x9f\xaf\xe3\x82\x81\xe3"
+ "\x81\xa6\xe7\x89\x9b\xe3\x82\x92\xe6\xae\xba\xe3\x81"
+ "\x99",
+ .output = "6KeS44KS55+v44KB44Gm54mb44KS5q6644GZ",
+ },
+ {
+ .scheme = &base64url_scheme,
+ .input =
+ "\xe8\xa7\x92\xe3\x82\x92\xe7\x9f\xaf\xe3\x82\x81\xe3"
+ "\x81\xa6\xe7\x89\x9b\xe3\x82\x92\xe6\xae\xba\xe3\x81"
+ "\x99",
+ .output = "6KeS44KS55-v44KB44Gm54mb44KS5q6644GZ",
+ },
+ {
+ .scheme = &base64_scheme,
+ .flags = BASE64_ENCODE_FLAG_CRLF,
+ .input = "just niin",
+ .output = "anVzdCBuaWlu",
+ },
+ {
+ .scheme = &base64_scheme,
+ .flags = BASE64_ENCODE_FLAG_CRLF,
+ .max_line_len = 80,
+ .input = "just niin",
+ .output = "anVzdCBuaWlu",
+ },
+ {
+ .scheme = &base64_scheme,
+ .flags = BASE64_ENCODE_FLAG_CRLF,
+ .max_line_len = 48,
+ .input =
+ "Passer, deliciae meae puellae,\n"
+ "quicum ludere, quem in sinu tenere,\n"
+ "cui primum digitum dare appetenti\n"
+ "et acris solet incitare morsus,\n"
+ "cum desiderio meo nitenti\n"
+ "carum nescio quid lubet iocari,\n"
+ "credo ut, cum gravis acquiescet ardor,\n"
+ "sit solaciolum sui doloris,\n"
+ "tecum ludere sicut ipsa possem\n"
+ "et tristis animi levare curas!\n",
+ .output =
+ "UGFzc2VyLCBkZWxpY2lhZSBtZWFlIHB1ZWxsYWUsCnF1aWN1\r\n"
+ "bSBsdWRlcmUsIHF1ZW0gaW4gc2ludSB0ZW5lcmUsCmN1aSBw\r\n"
+ "cmltdW0gZGlnaXR1bSBkYXJlIGFwcGV0ZW50aQpldCBhY3Jp\r\n"
+ "cyBzb2xldCBpbmNpdGFyZSBtb3JzdXMsCmN1bSBkZXNpZGVy\r\n"
+ "aW8gbWVvIG5pdGVudGkKY2FydW0gbmVzY2lvIHF1aWQgbHVi\r\n"
+ "ZXQgaW9jYXJpLApjcmVkbyB1dCwgY3VtIGdyYXZpcyBhY3F1\r\n"
+ "aWVzY2V0IGFyZG9yLApzaXQgc29sYWNpb2x1bSBzdWkgZG9s\r\n"
+ "b3JpcywKdGVjdW0gbHVkZXJlIHNpY3V0IGlwc2EgcG9zc2Vt\r\n"
+ "CmV0IHRyaXN0aXMgYW5pbWkgbGV2YXJlIGN1cmFzIQo=",
+ },
+ {
+ .scheme = &base64_scheme,
+ .max_line_len = 48,
+ .input =
+ "Lugete, o Veneres Cupidinesque,\n"
+ "et quantum est hominum venustiorum:\n"
+ "passer mortuus est meae puellae, \n"
+ "passer, deliciae meae puellae\n"
+ "quem plus amat illa oculis suis amabat.\n"
+ "Nam mellitus erat suamque norat\n"
+ "ipsam tam bene quam puella matrem,\n"
+ "nec sese a gremio illius movebat,\n"
+ "sed circumsiliens modo huc modo illuc\n"
+ "ad solam dominam usque pipiabat;\n"
+ "qui nunc it per iter tenebricosum\n"
+ "illuc, unde negant redire quemquam.\n"
+ "At vobis male sint, malae tenebrae\n"
+ "Orci, quae omnia bella devoratis:\n"
+ "tam bellum mihi passerem abstulistis.\n"
+ "O factum male! O miselle passer!\n"
+ "Tua nunc opera meae puellae\n"
+ "flendo turgiduli rubent ocelli.\n",
+ .output =
+ "THVnZXRlLCBvIFZlbmVyZXMgQ3VwaWRpbmVzcXVlLApldCBx\n"
+ "dWFudHVtIGVzdCBob21pbnVtIHZlbnVzdGlvcnVtOgpwYXNz\n"
+ "ZXIgbW9ydHV1cyBlc3QgbWVhZSBwdWVsbGFlLCAKcGFzc2Vy\n"
+ "LCBkZWxpY2lhZSBtZWFlIHB1ZWxsYWUKcXVlbSBwbHVzIGFt\n"
+ "YXQgaWxsYSBvY3VsaXMgc3VpcyBhbWFiYXQuCk5hbSBtZWxs\n"
+ "aXR1cyBlcmF0IHN1YW1xdWUgbm9yYXQKaXBzYW0gdGFtIGJl\n"
+ "bmUgcXVhbSBwdWVsbGEgbWF0cmVtLApuZWMgc2VzZSBhIGdy\n"
+ "ZW1pbyBpbGxpdXMgbW92ZWJhdCwKc2VkIGNpcmN1bXNpbGll\n"
+ "bnMgbW9kbyBodWMgbW9kbyBpbGx1YwphZCBzb2xhbSBkb21p\n"
+ "bmFtIHVzcXVlIHBpcGlhYmF0OwpxdWkgbnVuYyBpdCBwZXIg\n"
+ "aXRlciB0ZW5lYnJpY29zdW0KaWxsdWMsIHVuZGUgbmVnYW50\n"
+ "IHJlZGlyZSBxdWVtcXVhbS4KQXQgdm9iaXMgbWFsZSBzaW50\n"
+ "LCBtYWxhZSB0ZW5lYnJhZQpPcmNpLCBxdWFlIG9tbmlhIGJl\n"
+ "bGxhIGRldm9yYXRpczoKdGFtIGJlbGx1bSBtaWhpIHBhc3Nl\n"
+ "cmVtIGFic3R1bGlzdGlzLgpPIGZhY3R1bSBtYWxlISBPIG1p\n"
+ "c2VsbGUgcGFzc2VyIQpUdWEgbnVuYyBvcGVyYSBtZWFlIHB1\n"
+ "ZWxsYWUKZmxlbmRvIHR1cmdpZHVsaSBydWJlbnQgb2NlbGxp\n"
+ "Lgo=",
+ },
+};
+
+static void test_base64_encode_lowlevel(void)
+{
+ string_t *str;
+ unsigned int i;
+
+ test_begin("base64 encode low-level");
+ str = t_str_new(256);
+ for (i = 0; i < N_ELEMENTS(tests_base64_encode_lowlevel); i++) {
+ const struct test_base64_encode_lowlevel *test =
+ &tests_base64_encode_lowlevel[i];
+ struct base64_encoder enc;
+ uoff_t out_size;
+
+ str_truncate(str, 0);
+
+ base64_encode_init(&enc, test->scheme, test->flags,
+ test->max_line_len);
+ out_size = base64_get_full_encoded_size(
+ &enc, strlen(test->input));
+ test_assert_idx(base64_encode_more(&enc, test->input,
+ strlen(test->input),
+ NULL, str), i);
+ test_assert_idx(base64_encode_finish(&enc, str), i);
+
+ test_assert_idx(strcmp(test->output, str_c(str)) == 0, i);
+ test_assert_idx(test->flags != 0 || test->max_line_len != 0 ||
+ str_len(str) == MAX_BASE64_ENCODED_SIZE(
+ strlen(test->input)), i);
+ test_assert_idx(str_len(str) == out_size, i);
+ }
+ test_end();
+}
+
+struct test_base64_decode_lowlevel {
+ const struct base64_scheme *scheme;
+ enum base64_decode_flags flags;
+
+ const char *input;
+ const char *output;
+ int ret;
+ unsigned int src_pos;
+};
+
+static const struct test_base64_decode_lowlevel
+tests_base64_decode_lowlevel[] = {
+ {
+ .scheme = &base64_scheme,
+ .input = "",
+ .output = "",
+ .ret = 0,
+ .src_pos = UINT_MAX,
+ },
+ {
+ .scheme = &base64_scheme,
+ .input = " ",
+ .output = "",
+ .ret = 0,
+ .src_pos = UINT_MAX,
+ },
+ {
+ .scheme = &base64_scheme,
+ .input = "",
+ .output = "",
+ .ret = 0,
+ .src_pos = UINT_MAX,
+ .flags = BASE64_DECODE_FLAG_EXPECT_BOUNDARY,
+ },
+ {
+ .scheme = &base64_scheme,
+ .input = "",
+ .output = "",
+ .ret = 0,
+ .src_pos = UINT_MAX,
+ .flags = BASE64_DECODE_FLAG_NO_WHITESPACE,
+ },
+ {
+ .scheme = &base64_scheme,
+ .input = " ",
+ .output = "",
+ .ret = -1,
+ .src_pos = 0,
+ .flags = BASE64_DECODE_FLAG_NO_WHITESPACE,
+ },
+ {
+ .scheme = &base64_scheme,
+ .input = "",
+ .output = "",
+ .ret = 0,
+ .src_pos = UINT_MAX,
+ .flags = BASE64_DECODE_FLAG_NO_PADDING,
+ },
+ {
+ .scheme = &base64_scheme,
+ .input = "",
+ .output = "",
+ .ret = 0,
+ .src_pos = UINT_MAX,
+ .flags = BASE64_DECODE_FLAG_IGNORE_PADDING,
+ },
+ {
+ .scheme = &base64_scheme,
+ .input = "\taGVsbG8gd29ybGQ=",
+ .output = "hello world",
+ .ret = 0,
+ .src_pos = UINT_MAX,
+ },
+ {
+ .scheme = &base64url_scheme,
+ .input = "\taGVsbG8gd29ybGQ=",
+ .output = "hello world",
+ .ret = 0,
+ .src_pos = UINT_MAX,
+ },
+ {
+ .scheme = &base64_scheme,
+ .input = "aGVsbG8gd29ybGQ=\t",
+ .output = "hello world",
+ .ret = 0,
+ .src_pos = UINT_MAX,
+ },
+ {
+ .scheme = &base64_scheme,
+ .input = "\taGVsbG8gd29ybGQ=\t",
+ .output = "hello world",
+ .ret = 0,
+ .src_pos = UINT_MAX,
+ },
+ {
+ .scheme = &base64_scheme,
+ .input = "aGVsbG8gd29ybGQ=:frop",
+ .output = "hello world",
+ .ret = 0,
+ .src_pos = 16,
+ .flags = BASE64_DECODE_FLAG_EXPECT_BOUNDARY,
+ },
+ {
+ .scheme = &base64_scheme,
+ .input = "\taGVsbG8gd29ybGQ=\t:frop",
+ .output = "hello world",
+ .ret = 0,
+ .src_pos = 18,
+ .flags = BASE64_DECODE_FLAG_EXPECT_BOUNDARY,
+ },
+ {
+ .scheme = &base64_scheme,
+ .input = "aGVsbG8gd29ybGQ=\t",
+ .output = "hello world",
+ .ret = -1,
+ .src_pos = 16,
+ .flags = BASE64_DECODE_FLAG_NO_WHITESPACE,
+ },
+ {
+ .scheme = &base64_scheme,
+ .input = "\taGVsbG8gd29ybGQ=\t",
+ .output = "",
+ .ret = -1,
+ .src_pos = 0,
+ .flags = BASE64_DECODE_FLAG_NO_WHITESPACE,
+ },
+ {
+ .scheme = &base64_scheme,
+ .flags = BASE64_DECODE_FLAG_NO_PADDING,
+ .input = "\taGVsbG8gd29ybGQ=",
+ .output = "hello world",
+ .ret = -1,
+ .src_pos = 16,
+ },
+ {
+ .scheme = &base64_scheme,
+ .flags = BASE64_DECODE_FLAG_NO_PADDING,
+ .input = "\taGVsbG8gd29ybGQ",
+ .output = "hello world",
+ .ret = 0,
+ .src_pos = 16,
+ },
+ {
+ .scheme = &base64_scheme,
+ .flags = BASE64_DECODE_FLAG_IGNORE_PADDING,
+ .input = "\taGVsbG8gd29ybGQ=",
+ .output = "hello world",
+ .ret = 0,
+ .src_pos = 17,
+ },
+ {
+ .scheme = &base64_scheme,
+ .flags = BASE64_DECODE_FLAG_IGNORE_PADDING,
+ .input = "\taGVsbG8gd29ybGQ",
+ .output = "hello world",
+ .ret = 0,
+ .src_pos = 16,
+ },
+ {
+ .scheme = &base64_scheme,
+ .input = "\nZm9v\n \tIGJh \t\ncml0cw==",
+ .output = "foo barits",
+ .ret = 0,
+ .src_pos = UINT_MAX,
+ },
+ {
+ .scheme = &base64url_scheme,
+ .input = "\nZm9v\n \tIGJh \t\ncml0cw==",
+ .output = "foo barits",
+ .ret = 0,
+ .src_pos = UINT_MAX,
+ },
+ {
+ .scheme = &base64_scheme,
+ .input = "\nZm9v\n \tIGJh \t\ncml0cw==\n ",
+ .output = "foo barits",
+ .ret = 0,
+ .src_pos = UINT_MAX,
+ },
+ {
+ .scheme = &base64_scheme,
+ .input = "\nZm9v\n \tIGJh \t\ncml0cw= =\n ",
+ .output = "foo barits",
+ .ret = 0,
+ .src_pos = UINT_MAX,
+ },
+ {
+ .scheme = &base64_scheme,
+ .input = "\nZm9v\n \tIGJh \t\ncml0cw\n= =\n ",
+ .output = "foo barits",
+ .ret = 0,
+ .src_pos = UINT_MAX,
+ },
+ {
+ .scheme = &base64_scheme,
+ .flags = BASE64_DECODE_FLAG_NO_PADDING,
+ .input = "\nZm9v\n \tIGJh \t\ncml0cw==",
+ .output = "foo barits",
+ .ret = -1,
+ .src_pos = 22,
+ },
+ {
+ .scheme = &base64_scheme,
+ .flags = BASE64_DECODE_FLAG_NO_PADDING,
+ .input = "\nZm9v\n \tIGJh \t\ncml0cw",
+ .output = "foo barits",
+ .ret = 0,
+ .src_pos = 22,
+ },
+ {
+ .scheme = &base64_scheme,
+ .flags = BASE64_DECODE_FLAG_IGNORE_PADDING,
+ .input = "\nZm9v\n \tIGJh \t\ncml0cw==",
+ .output = "foo barits",
+ .ret = 0,
+ .src_pos = 24,
+ },
+ {
+ .scheme = &base64_scheme,
+ .flags = BASE64_DECODE_FLAG_IGNORE_PADDING,
+ .input = "\nZm9v\n \tIGJh \t\ncml0cw",
+ .output = "foo barits",
+ .ret = 0,
+ .src_pos = 22,
+ },
+ {
+ .scheme = &base64_scheme,
+ .input = " anVzdCBuaWlu \n",
+ .output = "just niin",
+ .ret = 0,
+ .src_pos = UINT_MAX,
+ },
+ {
+ .scheme = &base64url_scheme,
+ .input = " anVzdCBuaWlu \n",
+ .output = "just niin",
+ .ret = 0,
+ .src_pos = UINT_MAX,
+ },
+ {
+ .scheme = &base64_scheme,
+ .flags = BASE64_DECODE_FLAG_NO_PADDING,
+ .input = " anVzdCBuaWlu \n",
+ .output = "just niin",
+ .ret = 0,
+ .src_pos = UINT_MAX,
+ },
+ {
+ .scheme = &base64_scheme,
+ .flags = BASE64_DECODE_FLAG_IGNORE_PADDING,
+ .input = " anVzdCBuaWlu \n",
+ .output = "just niin",
+ .ret = 0,
+ .src_pos = UINT_MAX,
+ },
+ {
+ .scheme = &base64_scheme,
+ .input = "aGVsb",
+ .output = "hel",
+ .ret = -1,
+ .src_pos = 5,
+ },
+ {
+ .scheme = &base64url_scheme,
+ .input = "aGVsb",
+ .output = "hel",
+ .ret = -1,
+ .src_pos = 5,
+ },
+ {
+ .scheme = &base64_scheme,
+ .flags = BASE64_DECODE_FLAG_NO_PADDING,
+ .input = "aGVsb",
+ .output = "hel",
+ .ret = 0,
+ .src_pos = 5,
+ },
+ {
+ .scheme = &base64_scheme,
+ .flags = BASE64_DECODE_FLAG_IGNORE_PADDING,
+ .input = "aGVsb",
+ .output = "hel",
+ .ret = 0,
+ .src_pos = 5,
+ },
+ {
+ .scheme = &base64_scheme,
+ .input = "aGVsb!!!!!",
+ .output = "hel",
+ .ret = -1,
+ .src_pos = 5,
+ },
+ {
+ .scheme = &base64url_scheme,
+ .input = "aGVsb!!!!!",
+ .output = "hel",
+ .ret = -1,
+ .src_pos = 5,
+ },
+ {
+ .scheme = &base64_scheme,
+ .input = "aGVs!!!!!",
+ .output = "hel",
+ .ret = -1,
+ .src_pos = 4,
+ },
+ {
+ .scheme = &base64url_scheme,
+ .input = "aGVs!!!!!",
+ .output = "hel",
+ .ret = -1,
+ .src_pos = 4,
+ },
+ {
+ .scheme = &base64_scheme,
+ .input =
+ "0JPQvtCy0L7RgNGPzIHRgiwg0YfRgt"
+ "C+INC60YPRgCDQtNC+0Y/MgdGCLg==",
+ .output =
+ "\xd0\x93\xd0\xbe\xd0\xb2\xd0\xbe\xd1\x80\xd1\x8f\xcc"
+ "\x81\xd1\x82\x2c\x20\xd1\x87\xd1\x82\xd0\xbe\x20\xd0"
+ "\xba\xd1\x83\xd1\x80\x20\xd0\xb4\xd0\xbe\xd1\x8f\xcc"
+ "\x81\xd1\x82\x2e",
+ .ret = 0,
+ .src_pos = UINT_MAX,
+ },
+ {
+ .scheme = &base64url_scheme,
+ .input =
+ "0JPQvtCy0L7RgNGPzIHRgiwg0YfRgt"
+ "C-INC60YPRgCDQtNC-0Y_MgdGCLg==",
+ .output =
+ "\xd0\x93\xd0\xbe\xd0\xb2\xd0\xbe\xd1\x80\xd1\x8f\xcc"
+ "\x81\xd1\x82\x2c\x20\xd1\x87\xd1\x82\xd0\xbe\x20\xd0"
+ "\xba\xd1\x83\xd1\x80\x20\xd0\xb4\xd0\xbe\xd1\x8f\xcc"
+ "\x81\xd1\x82\x2e",
+ .ret = 0,
+ .src_pos = UINT_MAX,
+ },
+ {
+ .scheme = &base64_scheme,
+ .flags = BASE64_DECODE_FLAG_NO_PADDING,
+ .input =
+ "0JPQvtCy0L7RgNGPzIHRgiwg0YfRgt"
+ "C+INC60YPRgCDQtNC+0Y/MgdGCLg",
+ .output =
+ "\xd0\x93\xd0\xbe\xd0\xb2\xd0\xbe\xd1\x80\xd1\x8f\xcc"
+ "\x81\xd1\x82\x2c\x20\xd1\x87\xd1\x82\xd0\xbe\x20\xd0"
+ "\xba\xd1\x83\xd1\x80\x20\xd0\xb4\xd0\xbe\xd1\x8f\xcc"
+ "\x81\xd1\x82\x2e",
+ .ret = 0,
+ .src_pos = UINT_MAX,
+ },
+ {
+ .scheme = &base64_scheme,
+ .flags = BASE64_DECODE_FLAG_IGNORE_PADDING,
+ .input =
+ "0JPQvtCy0L7RgNGPzIHRgiwg0YfRgt"
+ "C+INC60YPRgCDQtNC+0Y/MgdGCLg",
+ .output =
+ "\xd0\x93\xd0\xbe\xd0\xb2\xd0\xbe\xd1\x80\xd1\x8f\xcc"
+ "\x81\xd1\x82\x2c\x20\xd1\x87\xd1\x82\xd0\xbe\x20\xd0"
+ "\xba\xd1\x83\xd1\x80\x20\xd0\xb4\xd0\xbe\xd1\x8f\xcc"
+ "\x81\xd1\x82\x2e",
+ .ret = 0,
+ .src_pos = UINT_MAX,
+ },
+ {
+ .scheme = &base64_scheme,
+ .flags = BASE64_DECODE_FLAG_IGNORE_PADDING,
+ .input =
+ "0JPQvtCy0L7RgNGPzIHRgiwg0YfRgt"
+ "C+INC60YPRgCDQtNC+0Y/MgdGCLg==",
+ .output =
+ "\xd0\x93\xd0\xbe\xd0\xb2\xd0\xbe\xd1\x80\xd1\x8f\xcc"
+ "\x81\xd1\x82\x2c\x20\xd1\x87\xd1\x82\xd0\xbe\x20\xd0"
+ "\xba\xd1\x83\xd1\x80\x20\xd0\xb4\xd0\xbe\xd1\x8f\xcc"
+ "\x81\xd1\x82\x2e",
+ .ret = 0,
+ .src_pos = UINT_MAX,
+ },
+};
+
+static void test_base64_decode_lowlevel(void)
+{
+ string_t *str;
+ buffer_t buf;
+ unsigned int i;
+
+ test_begin("base64 decode low-level");
+ for (i = 0; i < N_ELEMENTS(tests_base64_decode_lowlevel); i++) {
+ const struct test_base64_decode_lowlevel *test =
+ &tests_base64_decode_lowlevel[i];
+ struct base64_decoder dec;
+ size_t src_pos;
+ int ret;
+
+ /* Some of the base64_decode() callers use fixed size buffers.
+ Use a fixed size buffer here as well to test that
+ base64_decode() can't allocate any extra space even
+ temporarily. */
+ size_t max_decoded_size =
+ MAX_BASE64_DECODED_SIZE(strlen(test->input));
+
+ buffer_create_from_data(&buf,
+ (max_decoded_size == 0 ? "" :
+ t_malloc0(max_decoded_size)),
+ max_decoded_size);
+ str = &buf;
+ base64_decode_init(&dec, test->scheme, test->flags);
+ ret = base64_decode_more(&dec, test->input, strlen(test->input),
+ &src_pos, str);
+ if (ret >= 0)
+ ret = base64_decode_finish(&dec);
+
+ test_assert_idx(ret == test->ret, i);
+ test_assert_idx(strlen(test->output) == str_len(str) &&
+ memcmp(test->output, str_data(str),
+ str_len(str)) == 0, i);
+ test_assert_idx(src_pos == test->src_pos ||
+ (test->src_pos == UINT_MAX &&
+ src_pos == strlen(test->input)), i);
+ if (ret >= 0) {
+ test_assert_idx(
+ str_len(str) <= MAX_BASE64_DECODED_SIZE(
+ strlen(test->input)), i);
+ }
+ }
+ test_end();
+}
+
+static void
+test_base64_random_lowlevel_one_block(const struct base64_scheme *b64,
+ enum base64_encode_flags enc_flags,
+ enum base64_decode_flags dec_flags,
+ size_t max_line_len,
+ unsigned int test_idx,
+ const unsigned char *in_buf,
+ size_t in_buf_size,
+ buffer_t *buf1, buffer_t *buf2)
+{
+ struct base64_encoder enc;
+ struct base64_decoder dec;
+ void *space;
+ size_t enc_size;
+ buffer_t buf;
+ int ret;
+
+ buffer_set_used_size(buf1, 0);
+ buffer_set_used_size(buf2, 0);
+
+ base64_encode_init(&enc, b64, enc_flags, max_line_len);
+ enc_size = base64_get_full_encoded_size(&enc, in_buf_size);
+ space = buffer_append_space_unsafe(buf1, enc_size);
+ buffer_create_from_data(&buf, space, enc_size);
+
+ if (!base64_encode_more(&enc, in_buf, in_buf_size, NULL, &buf))
+ test_assert_idx(FALSE, test_idx);
+ if (!base64_encode_finish(&enc, &buf))
+ test_assert_idx(FALSE, test_idx);
+
+ test_assert(base64_get_full_encoded_size(&enc, in_buf_size) ==
+ buf1->used);
+
+ base64_decode_init(&dec, b64, dec_flags);
+ space = buffer_append_space_unsafe(buf2, in_buf_size);
+ buffer_create_from_data(&buf, space, in_buf_size);
+
+ ret = base64_decode_more(&dec, buf1->data, buf1->used, NULL, &buf);
+ if (ret >= 0)
+ ret = base64_decode_finish(&dec);
+
+ test_assert_idx(ret >= 0, test_idx);
+ test_assert_idx(buf2->used == in_buf_size &&
+ memcmp(in_buf, buf2->data, in_buf_size) == 0, test_idx);
+}
+
+static void
+test_base64_random_lowlevel_stream(const struct base64_scheme *b64,
+ enum base64_encode_flags enc_flags,
+ enum base64_decode_flags dec_flags,
+ size_t max_line_len, unsigned int test_idx,
+ const unsigned char *in_buf,
+ size_t in_buf_size,
+ buffer_t *buf1, buffer_t *buf2,
+ size_t chunk_size)
+{
+ struct base64_encoder enc;
+ struct base64_decoder dec;
+ const unsigned char *buf_p, *buf_begin, *buf_end;
+ int ret;
+ size_t out_space, out_full_size;
+ void *out_data;
+ buffer_t out;
+
+ /* Encode */
+
+ buffer_set_used_size(buf1, 0);
+
+ buf_begin = in_buf;
+ buf_end = buf_begin + in_buf_size;
+
+ base64_encode_init(&enc, b64, enc_flags, max_line_len);
+ out_full_size = base64_get_full_encoded_size(&enc, in_buf_size);
+ out_space = 0;
+ for (buf_p = buf_begin; buf_p < buf_end; ) {
+ size_t buf_ch, out_ch;
+ size_t left = (buf_end - buf_p);
+ size_t used = buf1->used;
+ size_t src_pos, out_size, src_full_space;
+ bool eres;
+
+ if (chunk_size == 0) {
+ buf_ch = i_rand_limit(32);
+ out_ch = i_rand_limit(32);
+ } else {
+ buf_ch = chunk_size;
+ out_ch = chunk_size;
+ }
+
+ out_space += out_ch;
+ out_data = buffer_append_space_unsafe(buf1, out_space);
+ buffer_create_from_data(&out, out_data, out_space);
+
+ if (buf_ch > left)
+ buf_ch = left;
+
+ src_full_space = base64_encode_get_full_space(
+ &enc, out_full_size - used);
+ test_assert_idx(src_full_space >= (size_t)(buf_end - buf_p),
+ test_idx);
+
+ out_size = base64_encode_get_size(&enc, buf_ch);
+
+ eres = base64_encode_more(&enc, buf_p, buf_ch, &src_pos, &out);
+ test_assert_idx((eres && src_pos == buf_ch) ||
+ (!eres && src_pos < buf_ch), test_idx);
+ test_assert_idx(out.used <= out_size, test_idx);
+ buf_p += src_pos;
+ i_assert(out_space >= out.used);
+ out_space -= out.used;
+ buffer_set_used_size(buf1, used + out.used);
+ }
+ test_assert_idx(base64_encode_finish(&enc, buf1), test_idx);
+
+ /* Verify encode */
+
+ test_assert(out_full_size == buf1->used);
+
+ buffer_set_used_size(buf2, 0);
+ base64_encode_init(&enc, b64, enc_flags, max_line_len);
+ test_assert_idx(base64_encode_more(&enc, in_buf, in_buf_size,
+ NULL, buf2), test_idx);
+ test_assert_idx(base64_encode_finish(&enc, buf2), test_idx);
+ test_assert_idx(buffer_cmp(buf1, buf2), test_idx);
+
+ /* Decode */
+
+ buffer_set_used_size(buf2, 0);
+
+ buf_begin = buf1->data;
+ buf_end = buf_begin + buf1->used;
+
+ base64_decode_init(&dec, b64, dec_flags);
+ ret = 1;
+ out_space = 0;
+ for (buf_p = buf_begin; buf_p < buf_end; ) {
+ size_t buf_ch, out_ch;
+ size_t left = (buf_end - buf_p);
+ size_t used = buf2->used;
+ size_t src_pos;
+
+ if (chunk_size == 0) {
+ buf_ch = i_rand_limit(32);
+ out_ch = i_rand_limit(32);
+ } else {
+ buf_ch = chunk_size;
+ out_ch = chunk_size;
+ }
+
+ out_space += out_ch;
+ out_data = buffer_append_space_unsafe(buf2, out_space);
+ buffer_create_from_data(&out, out_data, out_space);
+
+ if (buf_ch > left)
+ buf_ch = left;
+ ret = base64_decode_more(&dec, buf_p, buf_ch,
+ &src_pos, &out);
+ test_assert_idx(ret >= 0, test_idx);
+ if (ret < 0) {
+ break;
+ }
+ buf_p += src_pos;
+ i_assert(out_space >= out.used);
+ out_space -= out.used;
+ buffer_set_used_size(buf2, used + out.used);
+ }
+ test_assert_idx(ret >= 0, test_idx);
+
+ /* Verify decode */
+ if (ret > 0) {
+ ret = base64_decode_finish(&dec);
+ test_assert_idx(ret == 0, test_idx);
+ test_assert_idx(buf2->used == in_buf_size &&
+ memcmp(in_buf, buf2->data, in_buf_size) == 0,
+ test_idx);
+ }
+}
+
+static void
+test_base64_random_lowlevel_case(const struct base64_scheme *b64,
+ enum base64_encode_flags enc_flags,
+ enum base64_decode_flags dec_flags,
+ size_t max_line_len)
+{
+ unsigned char in_buf[512];
+ size_t in_buf_size;
+ buffer_t *buf1, *buf2;
+ unsigned int i, j;
+
+ if (test_has_failed())
+ return;
+
+ buf1 = t_buffer_create(MAX_BASE64_ENCODED_SIZE(sizeof(in_buf)));
+ buf2 = t_buffer_create(MAX_BASE64_ENCODED_SIZE(sizeof(in_buf)));
+
+ /* one block */
+ for (i = 0; i < 1000; i++) {
+ in_buf_size = i_rand_limit(sizeof(in_buf));
+ for (j = 0; j < in_buf_size; j++)
+ in_buf[j] = i_rand_uchar();
+
+ test_base64_random_lowlevel_one_block(b64, enc_flags, dec_flags,
+ max_line_len, i,
+ in_buf, in_buf_size,
+ buf1, buf2);
+
+ if (test_has_failed()) {
+ i_info("One block test failed ("
+ "enc_flags=%02x dec_flags=%02x "
+ "max_line_len=%zu size=%zu)",
+ enc_flags, dec_flags, max_line_len,
+ in_buf_size);
+ return;
+ }
+ }
+
+ /* streaming; single-byte trickle */
+ for (i = 0; i < 1000; i++) {
+ in_buf_size = i_rand_limit(sizeof(in_buf));
+ for (j = 0; j < in_buf_size; j++)
+ in_buf[j] = i_rand_uchar();
+
+ test_base64_random_lowlevel_stream(b64, enc_flags, dec_flags,
+ max_line_len, i,
+ in_buf, in_buf_size,
+ buf1, buf2, 1);
+
+ if (test_has_failed()) {
+ i_info("Streaming single-byte trickle test failed ("
+ "enc_flags=%02x dec_flags=%02x "
+ "max_line_len=%zu size=%zu)",
+ enc_flags, dec_flags, max_line_len,
+ in_buf_size);
+ return;
+ }
+ }
+
+ /* streaming; random chunks */
+ for (i = 0; i < 1000; i++) {
+ in_buf_size = i_rand_limit(sizeof(in_buf));
+ for (j = 0; j < in_buf_size; j++)
+ in_buf[j] = i_rand_uchar();
+
+ test_base64_random_lowlevel_stream(b64, enc_flags, dec_flags,
+ max_line_len, i,
+ in_buf, in_buf_size,
+ buf1, buf2, 0);
+ if (test_has_failed()) {
+ i_info("Streaming random chunks test failed ("
+ "enc_flags=%02x dec_flags=%02x "
+ "max_line_len=%zu size=%zu)",
+ enc_flags, dec_flags, max_line_len,
+ in_buf_size);
+ return;
+ }
+ }
+}
+
+static void
+test_base64_random_lowlevel(void)
+{
+ test_begin("base64 encode/decode low-level with random input");
+ test_base64_random_lowlevel_case(&base64_scheme, 0, 0, 0);
+ test_base64_random_lowlevel_case(&base64url_scheme, 0, 0, 0);
+ test_base64_random_lowlevel_case(&base64_scheme, 0,
+ BASE64_DECODE_FLAG_EXPECT_BOUNDARY, 0);
+ test_base64_random_lowlevel_case(&base64url_scheme, 0,
+ BASE64_DECODE_FLAG_EXPECT_BOUNDARY, 0);
+ test_base64_random_lowlevel_case(&base64_scheme, 0,
+ BASE64_DECODE_FLAG_NO_WHITESPACE, 0);
+ test_base64_random_lowlevel_case(&base64url_scheme, 0,
+ BASE64_DECODE_FLAG_NO_WHITESPACE, 0);
+ test_base64_random_lowlevel_case(&base64_scheme, 0, 0, 10);
+ test_base64_random_lowlevel_case(&base64url_scheme, 0, 0, 10);
+ test_base64_random_lowlevel_case(&base64_scheme,
+ BASE64_ENCODE_FLAG_CRLF, 0, 10);
+ test_base64_random_lowlevel_case(&base64url_scheme,
+ BASE64_ENCODE_FLAG_CRLF, 0, 10);
+ test_base64_random_lowlevel_case(&base64_scheme,
+ BASE64_ENCODE_FLAG_NO_PADDING,
+ BASE64_DECODE_FLAG_NO_PADDING, 0);
+ test_base64_random_lowlevel_case(&base64url_scheme,
+ BASE64_ENCODE_FLAG_NO_PADDING,
+ BASE64_DECODE_FLAG_NO_PADDING, 0);
+ test_base64_random_lowlevel_case(&base64_scheme,
+ BASE64_ENCODE_FLAG_NO_PADDING |
+ BASE64_ENCODE_FLAG_CRLF,
+ BASE64_DECODE_FLAG_NO_PADDING, 15);
+ test_base64_random_lowlevel_case(&base64url_scheme,
+ BASE64_ENCODE_FLAG_NO_PADDING |
+ BASE64_ENCODE_FLAG_CRLF,
+ BASE64_DECODE_FLAG_NO_PADDING, 15);
+ test_base64_random_lowlevel_case(&base64_scheme,
+ BASE64_ENCODE_FLAG_NO_PADDING |
+ BASE64_ENCODE_FLAG_CRLF,
+ BASE64_DECODE_FLAG_NO_PADDING, 1);
+ test_end();
+}
+
+static void
+_add_lines(const char *in, size_t max_line_len, bool crlf, string_t *out)
+{
+ size_t in_len = strlen(in);
+
+ while (max_line_len > 0 && in_len > max_line_len) {
+ str_append_data(out, in, max_line_len);
+ if (crlf)
+ str_append(out, "\r\n");
+ else
+ str_append_c(out, '\n');
+ in += max_line_len;
+ in_len -= max_line_len;
+ }
+
+ str_append_data(out, in, in_len);
+}
+
+static void test_base64_encode_lines(void)
+{
+ static const char *input[] = {
+ "Passer, deliciae meae puellae,\n"
+ "quicum ludere, quem in sinu tenere,\n"
+ "cui primum digitum dare appetenti\n"
+ "et acris solet incitare morsus,\n"
+ "cum desiderio meo nitenti\n"
+ "carum nescio quid lubet iocari,\n"
+ "credo ut, cum gravis acquiescet ardor,\n"
+ "sit solaciolum sui doloris,\n"
+ "tecum ludere sicut ipsa possem\n"
+ "et tristis animi levare curas!\n"
+ };
+ static const char *output[] = {
+ "UGFzc2VyLCBkZWxpY2lhZSBtZWFlIHB1ZWxsYWUsCnF1aWN1"
+ "bSBsdWRlcmUsIHF1ZW0gaW4gc2ludSB0ZW5lcmUsCmN1aSBw"
+ "cmltdW0gZGlnaXR1bSBkYXJlIGFwcGV0ZW50aQpldCBhY3Jp"
+ "cyBzb2xldCBpbmNpdGFyZSBtb3JzdXMsCmN1bSBkZXNpZGVy"
+ "aW8gbWVvIG5pdGVudGkKY2FydW0gbmVzY2lvIHF1aWQgbHVi"
+ "ZXQgaW9jYXJpLApjcmVkbyB1dCwgY3VtIGdyYXZpcyBhY3F1"
+ "aWVzY2V0IGFyZG9yLApzaXQgc29sYWNpb2x1bSBzdWkgZG9s"
+ "b3JpcywKdGVjdW0gbHVkZXJlIHNpY3V0IGlwc2EgcG9zc2Vt"
+ "CmV0IHRyaXN0aXMgYW5pbWkgbGV2YXJlIGN1cmFzIQo=",
+ };
+ string_t *out_test, *out_ref;
+ unsigned int i, n;
+
+ out_test = t_str_new(256);
+ out_ref = t_str_new(256);
+
+ test_begin("base64 encode lines (LF)");
+ for (i = 0; i < N_ELEMENTS(input); i++) {
+ struct base64_encoder b64enc;
+
+ for (n = 0; n <= 80; n++) {
+ str_truncate(out_test, 0);
+ base64_encode_init(&b64enc, &base64_scheme, 0, n);
+ base64_encode_more(&b64enc, input[i], strlen(input[i]),
+ NULL, out_test);
+ base64_encode_finish(&b64enc, out_test);
+
+ str_truncate(out_ref, 0);
+ _add_lines(output[i], n, FALSE, out_ref);
+
+ test_assert(strcmp(str_c(out_ref),
+ str_c(out_test)) == 0);
+
+ }
+ }
+ test_end();
+
+ test_begin("base64 encode lines (CRLF)");
+ for (i = 0; i < N_ELEMENTS(input); i++) {
+ struct base64_encoder b64enc;
+
+ for (n = 0; n <= 80; n++) {
+ str_truncate(out_test, 0);
+ base64_encode_init(&b64enc, &base64_scheme,
+ BASE64_ENCODE_FLAG_CRLF, n);
+ base64_encode_more(&b64enc, input[i], strlen(input[i]),
+ NULL, out_test);
+ base64_encode_finish(&b64enc, out_test);
+
+ str_truncate(out_ref, 0);
+ _add_lines(output[i], n, TRUE, out_ref);
+
+ test_assert(strcmp(str_c(out_ref),
+ str_c(out_test)) == 0);
+
+ }
+ }
+ test_end();
+}
+
+void test_base64(void)
+{
+ test_base64_encode();
+ test_base64_decode();
+ test_base64_random();
+ test_base64url_encode();
+ test_base64url_decode();
+ test_base64url_random();
+ test_base64_encode_lowlevel();
+ test_base64_decode_lowlevel();
+ test_base64_random_lowlevel();
+ test_base64_encode_lines();
+}
diff --git a/src/lib/test-bits.c b/src/lib/test-bits.c
new file mode 100644
index 0000000..d091dc5
--- /dev/null
+++ b/src/lib/test-bits.c
@@ -0,0 +1,281 @@
+/* Copyright (c) 2001-2018 Dovecot authors, see the included COPYING file */
+
+/* Unit tests for bit twiddles library */
+
+#include "test-lib.h"
+
+#include <stdio.h>
+
+static void test_bits_unsigned_minus(void)
+{
+ test_begin("bits_unsigned_minus()");
+
+ // 32 bit
+ test_assert(UNSIGNED_MINUS(0x00000000U) == 0x00000000U);
+
+ test_assert(UNSIGNED_MINUS(0x00000001U) == 0xffffffffU);
+ test_assert(UNSIGNED_MINUS(0x00000002U) == 0xfffffffeU);
+ test_assert(UNSIGNED_MINUS(0x00000003U) == 0xfffffffdU);
+ //..
+ test_assert(UNSIGNED_MINUS(0x7fffffffU) == 0x80000001U);
+ test_assert(UNSIGNED_MINUS(0x80000000U) == 0x80000000U);
+ test_assert(UNSIGNED_MINUS(0x80000001U) == 0x7fffffffU);
+ //..
+ test_assert(UNSIGNED_MINUS(0xffffffffU) == 0x00000001U);
+ test_assert(UNSIGNED_MINUS(0xfffffffeU) == 0x00000002U);
+ test_assert(UNSIGNED_MINUS(0xfffffffdU) == 0x00000003U);
+
+ // 64 bit
+ test_assert(UNSIGNED_MINUS(0x0000000000000000ULL) == 0x0000000000000000ULL);
+
+ test_assert(UNSIGNED_MINUS(0x0000000000000001ULL) == 0xffffffffffffffffULL);
+ test_assert(UNSIGNED_MINUS(0x0000000000000002ULL) == 0xfffffffffffffffeULL);
+ test_assert(UNSIGNED_MINUS(0x0000000000000003ULL) == 0xfffffffffffffffdULL);
+ //..
+ test_assert(UNSIGNED_MINUS(0x7fffffffffffffffULL) == 0x8000000000000001ULL);
+ test_assert(UNSIGNED_MINUS(0x8000000000000000ULL) == 0x8000000000000000ULL);
+ test_assert(UNSIGNED_MINUS(0x8000000000000001ULL) == 0x7fffffffffffffffULL);
+ //..
+ test_assert(UNSIGNED_MINUS(0xffffffffffffffffULL) == 0x0000000000000001ULL);
+ test_assert(UNSIGNED_MINUS(0xfffffffffffffffeULL) == 0x0000000000000002ULL);
+ test_assert(UNSIGNED_MINUS(0xfffffffffffffffdULL) == 0x0000000000000003ULL);
+
+ test_end();
+}
+
+
+/* nearest_power(0) = error bits_requiredXX(0) = 0
+ nearest_power(1) = 1 = 1<<0 bits_requiredXX(1) = 1
+ nearest_power(2) = 2 = 1<<1 bits_requiredXX(2) = 2
+ nearest_power(3) = 4 = 1<<2 bits_requiredXX(3) = 2
+ nearest_power(4) = 4 = 1<<2 bits_requiredXX(4) = 3
+ nearest_power(5) = 8 = 1<<3 bits_requiredXX(5) = 3
+ nearest_power(7) = 8 = 1<<3 bits_requiredXX(7) = 3
+ nearest_power(8) = 8 = 1<<3 bits_requiredXX(8) = 4
+*/
+
+/* nearest_power(num) == 1ULL << bits_required64(num-1) */
+static void test_nearest_power(void)
+{
+ unsigned int b;
+ size_t num;
+ test_begin("nearest_power()");
+ test_assert(nearest_power(1)==1);
+ test_assert(nearest_power(2)==2);
+ for (b = 2; b < CHAR_BIT*sizeof(size_t) - 1; ++b) {
+ /* b=2 tests 3,4,5; b=3 tests 7,8,9; ... b=30 tests ~1G */
+ num = (size_t)1 << b;
+ test_assert_idx(nearest_power(num-1) == num, b);
+ test_assert_idx(nearest_power(num ) == num, b);
+ test_assert_idx(nearest_power(num+1) == num<<1, b);
+ }
+ /* With 32-bit size_t, now: b=31 tests 2G-1, 2G, not 2G+1. */
+ num = (size_t)1 << b;
+ test_assert_idx(nearest_power(num-1) == num, b);
+ test_assert_idx(nearest_power(num ) == num, b);
+ /* i_assert()s: test_assert_idx(nearest_power(num+1) == num<<1, b); */
+ test_end();
+}
+
+static void test_bits_is_power_of_two(void)
+{
+ test_begin("bits_is_power_of_two()");
+ for (unsigned int i = 0; i < 64; i++)
+ test_assert_idx(bits_is_power_of_two(1ULL << i), i);
+ for (unsigned int i = 2; i < 64; i++) {
+ test_assert_idx(!bits_is_power_of_two((1ULL << i) - 1), i);
+ test_assert_idx(!bits_is_power_of_two((1ULL << i) + 1), i);
+ }
+ test_assert(!bits_is_power_of_two(0));
+ test_assert(!bits_is_power_of_two(0xffffffffffffffffULL));
+ test_assert( bits_is_power_of_two(0x8000000000000000ULL));
+ test_end();
+}
+
+static void test_bits_requiredXX(void)
+{
+ /* As ..64 depends on ..32 and tests it twice,
+ * and ..32 depends on ..16 and tests it twice,
+ * etc., we only test ..64
+ */
+ unsigned int b;
+ test_begin("bits_requiredXX()");
+ test_assert(bits_required64(0) == 0);
+ test_assert(bits_required64(1) == 1);
+ test_assert(bits_required64(2) == 2);
+ for (b = 2; b < 64; ++b) {
+ /* b=2 tests 3,4,5; b=3 tests 7,8,9; ... */
+ uint64_t num = 1ULL << b;
+ test_assert_idx(bits_required64(num-1) == b, b);
+ test_assert_idx(bits_required64(num ) == b+1, b);
+ test_assert_idx(bits_required64(num+1) == b+1, b);
+ }
+ test_end();
+}
+
+static void test_sum_overflows(void)
+{
+#define MAX64 (uint64_t)-1
+ static const struct {
+ uint64_t a, b;
+ bool overflows;
+ } tests[] = {
+ { MAX64-1, 1, FALSE },
+ { MAX64, 1, TRUE },
+ { MAX64-1, 1, FALSE },
+ { MAX64-1, 2, TRUE },
+ { MAX64-1, MAX64-1, TRUE },
+ { MAX64-1, MAX64, TRUE },
+ { MAX64, MAX64, TRUE }
+ };
+ unsigned int i;
+
+ test_begin("UINT64_SUM_OVERFLOWS");
+ for (i = 0; i < N_ELEMENTS(tests); i++)
+ test_assert(UINT64_SUM_OVERFLOWS(tests[i].a, tests[i].b) == tests[i].overflows);
+ test_end();
+}
+
+static void ATTR_NO_SANITIZE_INTEGER ATTR_NO_SANITIZE_IMPLICIT_CONVERSION
+test_bits_fraclog(void)
+{
+ unsigned int fracbits;
+ for (fracbits = 0; fracbits < 6; fracbits++) {
+ static char name[] = "fraclog x-bit";
+ name[8] = '0'+ fracbits;
+ test_begin(name);
+
+ unsigned int i;
+ unsigned int last_end = ~0u;
+ for (i = 0; i < BITS_FRACLOG_BUCKETS(fracbits); i++) {
+ unsigned int start = bits_fraclog_bucket_start(i, fracbits);
+ unsigned int end = bits_fraclog_bucket_end(i, fracbits);
+ test_assert_idx(start == last_end + 1, i);
+ last_end = end;
+ test_assert_idx(bits_fraclog(start, fracbits) == i, i);
+ test_assert_idx(bits_fraclog(end, fracbits) == i, i);
+ }
+ test_assert_idx(last_end == ~0u, fracbits);
+
+ test_end();
+ }
+}
+
+/* The compiler *should* generate different code when the fracbits parameter
+ is a compile-time constant, so we also need to check that's the case.
+*/
+static void ATTR_NO_SANITIZE_INTEGER ATTR_NO_SANITIZE_IMPLICIT_CONVERSION
+test_bits_fraclog_const(void)
+{
+#define FRACBITS 2
+#define STR2(s) #s
+#define STR(s) STR2(s)
+ test_begin("fraclog constant " STR(FRACBITS) " bit");
+
+ unsigned int i;
+ unsigned int last_end = ~0u;
+ for (i = 0; i < BITS_FRACLOG_BUCKETS(FRACBITS); i++) {
+ unsigned int start = bits_fraclog_bucket_start(i, FRACBITS);
+ unsigned int end = bits_fraclog_bucket_end(i, FRACBITS);
+ test_assert_idx(start == last_end + 1, i);
+ last_end = end;
+ test_assert_idx(bits_fraclog(start, FRACBITS) == i, i);
+ test_assert_idx(bits_fraclog(end, FRACBITS) == i, i);
+ }
+ test_assert(last_end == ~0u);
+
+ test_end();
+}
+
+static void test_bits_rotl32(void)
+{
+ test_begin("bits_rotl32");
+
+ test_assert(bits_rotl32(0x1c00000eU, 3) == 0xe0000070U);
+ test_assert(bits_rotl32(0xe0000070U, 5) == 0x00000e1cU);
+ test_assert(bits_rotl32(0x00000e1cU, 0) == 0x00000e1cU);
+ test_assert(bits_rotl32(0x1c00000eU, 3 + 32) == 0xe0000070U);
+
+ test_end();
+}
+
+static void test_bits_rotl64(void)
+{
+ test_begin("bits_rotl64");
+
+ test_assert(bits_rotl64(0x1c0000000000000eUL, 3) == 0xe000000000000070UL);
+ test_assert(bits_rotl64(0xe000000000000070UL, 5) == 0x0000000000000e1cUL);
+ test_assert(bits_rotl64(0x0000000000000e1cUL, 0) == 0x0000000000000e1cUL);
+ test_assert(bits_rotl64(0x1c0000000000000eUL, 3 + 64) == 0xe000000000000070UL);
+
+ test_end();
+}
+
+static void test_bits_rotr32(void)
+{
+ test_begin("bits_rotr32");
+
+ test_assert(bits_rotr32(0x1c00000eU, 3) == 0xc3800001U);
+ test_assert(bits_rotr32(0xc3800001U, 5) == 0x0e1c0000U);
+ test_assert(bits_rotr32(0x00000e1cU, 0) == 0x00000e1cU);
+ test_assert(bits_rotr32(0x1c00000eU, 3 + 32) == 0xc3800001U);
+
+ test_end();
+}
+
+static void test_bits_rotr64(void)
+{
+ test_begin("bits_rotr64");
+
+ test_assert(bits_rotr64(0x1c0000000000000eUL, 3) == 0xc380000000000001UL);
+ test_assert(bits_rotr64(0xc380000000000001UL, 5) == 0x0e1c000000000000UL);
+ test_assert(bits_rotr64(0x0000000000000e1cUL, 0) == 0x0000000000000e1cUL);
+ test_assert(bits_rotr64(0x1c0000000000000eUL, 3 + 64) == 0xc380000000000001UL);
+
+ test_end();
+}
+
+static void test_bit_tests(void)
+{
+ test_begin("HAS_..._BITS() macro tests");
+
+ test_assert(HAS_NO_BITS(1,0));
+ test_assert(HAS_NO_BITS(2,~2U));
+ test_assert(!HAS_NO_BITS(2,2));
+
+ /* OUCH - this vacuously true expression fails. However, if you are
+ dumb enough to use 0 as bits, then it will also fail in the verbose
+ case that this macro replaces, it's not a regression. */
+ /* test_assert(HAS_ANY_BITS(6,0)); */
+ test_assert(HAS_ANY_BITS(3,1));
+ test_assert(HAS_ANY_BITS(2,3));
+ test_assert(!HAS_ANY_BITS(7,~(7U|128U)));
+
+ test_assert(HAS_ALL_BITS(0,0));
+ test_assert(HAS_ALL_BITS(30,14));
+ test_assert(!HAS_ALL_BITS(~1U,~0U));
+
+ /* Trap double-evaluation */
+ unsigned int v=10,b=2;
+ test_assert(!HAS_NO_BITS(v++, b++) && v==11 && b==3);
+ test_assert(HAS_ANY_BITS(v++, b++) && v==12 && b==4);
+ test_assert(HAS_ALL_BITS(v++, b++) && v==13 && b==5);
+
+ test_end();
+}
+
+void test_bits(void)
+{
+ test_bits_unsigned_minus();
+ test_nearest_power();
+ test_bits_is_power_of_two();
+ test_bits_requiredXX();
+ test_bits_fraclog();
+ test_bits_fraclog_const();
+ test_bits_rotl32();
+ test_bits_rotr32();
+ test_bits_rotl64();
+ test_bits_rotr64();
+ test_sum_overflows();
+ test_bit_tests();
+}
diff --git a/src/lib/test-bsearch-insert-pos.c b/src/lib/test-bsearch-insert-pos.c
new file mode 100644
index 0000000..5b2454e
--- /dev/null
+++ b/src/lib/test-bsearch-insert-pos.c
@@ -0,0 +1,46 @@
+/* Copyright (c) 2007-2018 Dovecot authors, see the included COPYING file */
+
+#include "test-lib.h"
+#include "bsearch-insert-pos.h"
+
+static int cmp_uint(const unsigned int *i1, const unsigned int *i2)
+{
+ return (int)*i1 - (int)*i2;
+}
+
+void test_bsearch_insert_pos(void)
+{
+ static const unsigned int input[] = {
+ 1, 5, 9, 15, 16, UINT_MAX,
+ 1, 5, 9, 15, 16, 17, UINT_MAX,
+ UINT_MAX
+ };
+ static const unsigned int max_key = 18;
+ const unsigned int *cur;
+ unsigned int key, len, i, idx;
+ bool success;
+
+ cur = input;
+ for (i = 0; cur[0] != UINT_MAX; i++) {
+ for (len = 0; cur[len] != UINT_MAX; len++) ;
+ for (key = 0; key < max_key; key++) {
+ if (bsearch_insert_pos(&key, cur, len, sizeof(*cur),
+ cmp_uint, &idx))
+ success = cur[idx] == key;
+ else if (idx == 0)
+ success = cur[0] > key;
+ else if (idx == len)
+ success = cur[len-1] < key;
+ else {
+ success = cur[idx-1] < key &&
+ cur[idx+1] > key;
+ }
+ if (!success)
+ break;
+ }
+ cur += len + 1;
+
+ test_out(t_strdup_printf("bsearch_insert_pos(%d,%d)", i, key),
+ success);
+ }
+}
diff --git a/src/lib/test-buffer-istream.c b/src/lib/test-buffer-istream.c
new file mode 100644
index 0000000..056c859
--- /dev/null
+++ b/src/lib/test-buffer-istream.c
@@ -0,0 +1,101 @@
+/* Copyright (c) 2020 Dovecot authors, see the included COPYING file */
+
+#include "test-lib.h"
+#include "istream.h"
+#include "str.h"
+#include "write-full.h"
+
+#include <unistd.h>
+#include <fcntl.h>
+
+#define TEST_FILENAME ".test_buffer_append_full_file"
+static void test_buffer_append_full_file(void)
+{
+ const char *test_string = "this is a test string\n";
+ test_begin("buffer_append_full_file");
+ buffer_t *result = t_buffer_create(32);
+ const char *error;
+ int fd = open(TEST_FILENAME, O_WRONLY|O_CREAT, 0600);
+ i_assert(fd > -1);
+ test_assert(write_full(fd, test_string, strlen(test_string)) == 0);
+ i_close_fd(&fd);
+
+ test_assert(buffer_append_full_file(result, TEST_FILENAME, SIZE_MAX,
+ &error) == BUFFER_APPEND_OK);
+ test_assert_strcmp(str_c(result), test_string);
+
+ /* test max_read_size */
+ for (size_t max = 0; max < strlen(test_string)-1; max++) {
+ buffer_set_used_size(result, 0);
+ test_assert(buffer_append_full_file(result, TEST_FILENAME,
+ max, &error) == BUFFER_APPEND_READ_MAX_SIZE);
+ test_assert(result->used == max &&
+ memcmp(result->data, test_string, max) == 0);
+ }
+
+ fd = open(TEST_FILENAME, O_WRONLY|O_TRUNC);
+ i_assert(fd > -1);
+ /* write it enough many times */
+ for (size_t i = 0; i < IO_BLOCK_SIZE; i += strlen(test_string)) {
+ test_assert(write_full(fd, test_string, strlen(test_string)) == 0);
+ }
+ i_close_fd(&fd);
+ buffer_set_used_size(result, 0);
+ test_assert(buffer_append_full_file(result, TEST_FILENAME,
+ SIZE_MAX, &error) == BUFFER_APPEND_OK);
+ for (size_t i = 0; i < result->used; i += strlen(test_string)) {
+ const char *data = result->data;
+ data += i;
+ test_assert(memcmp(data, test_string, strlen(test_string)) == 0);
+ }
+ buffer_set_used_size(result, 0);
+ test_assert(chmod(TEST_FILENAME, 0) == 0);
+ error = NULL;
+ test_assert(buffer_append_full_file(result, TEST_FILENAME, SIZE_MAX,
+ &error) == BUFFER_APPEND_READ_ERROR);
+ test_assert(error != NULL && *error != '\0');
+ buffer_set_used_size(result, 0);
+ test_assert(chmod(TEST_FILENAME, 0700) == 0);
+ /* test permission problems */
+ i_unlink(TEST_FILENAME);
+ test_assert(buffer_append_full_file(result, TEST_FILENAME, SIZE_MAX,
+ &error) == BUFFER_APPEND_READ_ERROR);
+ test_assert_strcmp(str_c(result), "");
+ test_end();
+}
+
+static void test_buffer_append_full_istream(void)
+{
+ int fds[2];
+ const char *error;
+ test_begin("buffer_append_full_istream");
+ buffer_t *result = t_buffer_create(32);
+ test_assert(pipe(fds) == 0);
+ fd_set_nonblock(fds[0], TRUE);
+ fd_set_nonblock(fds[1], TRUE);
+
+ struct istream *is = i_stream_create_fd(fds[0], SIZE_MAX);
+ /* test just the READ_MORE stuff */
+
+ test_assert(write_full(fds[1], "some data ", 10) == 0);
+
+ test_assert(buffer_append_full_istream(result, is, SIZE_MAX, &error) ==
+ BUFFER_APPEND_READ_MORE);
+ test_assert(write_full(fds[1], "final read", 10) == 0);
+ i_close_fd(&fds[1]);
+
+ test_assert(buffer_append_full_istream(result, is, SIZE_MAX, &error) ==
+ BUFFER_APPEND_OK);
+ test_assert_strcmp(str_c(result), "some data final read");
+ i_stream_unref(&is);
+ i_close_fd(&fds[0]);
+
+ test_end();
+}
+
+void test_buffer_append_full(void)
+{
+ test_buffer_append_full_file();
+ test_buffer_append_full_istream();
+}
+
diff --git a/src/lib/test-buffer.c b/src/lib/test-buffer.c
new file mode 100644
index 0000000..50a2e92
--- /dev/null
+++ b/src/lib/test-buffer.c
@@ -0,0 +1,367 @@
+/* Copyright (c) 2007-2018 Dovecot authors, see the included COPYING file */
+
+#include "test-lib.h"
+#include "buffer.h"
+
+static void test_buffer_random(void)
+{
+#define BUF_TEST_SIZE (1024*2)
+#define BUF_TEST_COUNT 1000
+ buffer_t *buf;
+ unsigned char *p, testdata[BUF_TEST_SIZE], shadowbuf[BUF_TEST_SIZE];
+ unsigned int i, shadowbuf_size;
+ size_t pos, pos2, size, size2;
+ int test = -1;
+ bool zero;
+
+ buf = buffer_create_dynamic(default_pool, 1);
+ for (i = 0; i < BUF_TEST_SIZE; i++)
+ testdata[i] = i_rand_uchar();
+ memset(shadowbuf, 0, sizeof(shadowbuf));
+
+ shadowbuf_size = 0;
+ for (i = 0; i < BUF_TEST_COUNT; i++) {
+ if (buf->used == BUF_TEST_SIZE) {
+ size = shadowbuf_size = i_rand_limit(buf->used - 1);
+ buffer_set_used_size(buf, size);
+ memset(shadowbuf + shadowbuf_size, 0,
+ BUF_TEST_SIZE - shadowbuf_size);
+ i_assert(buf->used < BUF_TEST_SIZE);
+ }
+
+ test = i_rand_limit(7);
+ zero = i_rand_limit(10) == 0;
+ switch (test) {
+ case 0:
+ pos = i_rand_limit(BUF_TEST_SIZE - 1);
+ size = i_rand_limit(BUF_TEST_SIZE - pos);
+ if (!zero) {
+ buffer_write(buf, pos, testdata, size);
+ memcpy(shadowbuf + pos, testdata, size);
+ } else {
+ buffer_write_zero(buf, pos, size);
+ memset(shadowbuf + pos, 0, size);
+ }
+ if (pos + size > shadowbuf_size)
+ shadowbuf_size = pos + size;
+ break;
+ case 1:
+ size = i_rand_limit(BUF_TEST_SIZE - buf->used);
+ if (!zero) {
+ buffer_append(buf, testdata, size);
+ memcpy(shadowbuf + shadowbuf_size,
+ testdata, size);
+ } else {
+ buffer_append_zero(buf, size);
+ memset(shadowbuf + shadowbuf_size, 0, size);
+ }
+ shadowbuf_size += size;
+ break;
+ case 2:
+ pos = i_rand_limit(BUF_TEST_SIZE - 1);
+ size = i_rand_limit(BUF_TEST_SIZE - I_MAX(buf->used, pos));
+ if (!zero) {
+ buffer_insert(buf, pos, testdata, size);
+ memmove(shadowbuf + pos + size,
+ shadowbuf + pos,
+ BUF_TEST_SIZE - (pos + size));
+ memcpy(shadowbuf + pos, testdata, size);
+ } else {
+ buffer_insert_zero(buf, pos, size);
+ memmove(shadowbuf + pos + size,
+ shadowbuf + pos,
+ BUF_TEST_SIZE - (pos + size));
+ memset(shadowbuf + pos, 0, size);
+ }
+ if (pos < shadowbuf_size)
+ shadowbuf_size += size;
+ else
+ shadowbuf_size = pos + size;
+ break;
+ case 3:
+ pos = i_rand_limit(BUF_TEST_SIZE - 1);
+ size = i_rand_limit(BUF_TEST_SIZE - pos);
+ buffer_delete(buf, pos, size);
+ if (pos < shadowbuf_size) {
+ if (pos + size > shadowbuf_size)
+ size = shadowbuf_size - pos;
+ memmove(shadowbuf + pos,
+ shadowbuf + pos + size,
+ BUF_TEST_SIZE - (pos + size));
+
+ shadowbuf_size -= size;
+ memset(shadowbuf + shadowbuf_size, 0,
+ BUF_TEST_SIZE - shadowbuf_size);
+ }
+ break;
+ case 4:
+ pos = i_rand_limit(BUF_TEST_SIZE - 1);
+ size = i_rand_limit(BUF_TEST_SIZE - pos);
+ size2 = i_rand_limit(BUF_TEST_SIZE -
+ I_MAX(buf->used, pos));
+ buffer_replace(buf, pos, size, testdata, size2);
+ if (pos < shadowbuf_size) {
+ if (pos + size > shadowbuf_size)
+ size = shadowbuf_size - pos;
+ memmove(shadowbuf + pos,
+ shadowbuf + pos + size,
+ BUF_TEST_SIZE - (pos + size));
+
+ shadowbuf_size -= size;
+ memset(shadowbuf + shadowbuf_size, 0,
+ BUF_TEST_SIZE - shadowbuf_size);
+ }
+ memmove(shadowbuf + pos + size2,
+ shadowbuf + pos,
+ BUF_TEST_SIZE - (pos + size2));
+ memcpy(shadowbuf + pos, testdata, size2);
+ if (pos < shadowbuf_size)
+ shadowbuf_size += size2;
+ else
+ shadowbuf_size = pos + size2;
+ break;
+ case 5:
+ if (shadowbuf_size <= 1)
+ break;
+ pos = i_rand_limit(shadowbuf_size - 1); /* dest */
+ pos2 = i_rand_limit(shadowbuf_size - 1); /* source */
+ size = i_rand_limit(shadowbuf_size - I_MAX(pos, pos2));
+ buffer_copy(buf, pos, buf, pos2, size);
+ memmove(shadowbuf + pos,
+ shadowbuf + pos2, size);
+ if (pos > pos2 && pos + size > shadowbuf_size)
+ shadowbuf_size = pos + size;
+ break;
+ case 6:
+ pos = i_rand_limit(BUF_TEST_SIZE - 1);
+ size = i_rand_limit(BUF_TEST_SIZE - pos);
+ p = buffer_get_space_unsafe(buf, pos, size);
+ memcpy(p, testdata, size);
+ memcpy(shadowbuf + pos, testdata, size);
+ if (pos + size > shadowbuf_size)
+ shadowbuf_size = pos + size;
+ break;
+ }
+ i_assert(shadowbuf_size <= BUF_TEST_SIZE);
+
+ if (buf->used != shadowbuf_size ||
+ memcmp(buf->data, shadowbuf, buf->used) != 0)
+ break;
+ }
+ if (i == BUF_TEST_COUNT)
+ test_out("buffer", TRUE);
+ else {
+ test_out_reason("buffer", FALSE,
+ t_strdup_printf("round %u test %d failed", i, test));
+ }
+ buffer_free(&buf);
+}
+
+static void test_buffer_write(void)
+{
+ buffer_t *buf;
+
+ test_begin("buffer_write");
+ buf = t_buffer_create(8);
+ buffer_write(buf, 5, buf, 0);
+ test_assert(buf->used == 5);
+ test_end();
+}
+
+static void test_buffer_set_used_size(void)
+{
+ buffer_t *buf;
+
+ test_begin("buffer_set_used_size");
+ buf = t_buffer_create(8);
+ memset(buffer_append_space_unsafe(buf, 7), 'a', 7);
+ buffer_set_used_size(buf, 4);
+ test_assert(memcmp(buffer_get_space_unsafe(buf, 0, 7), "aaaa\0\0\0", 7) == 0);
+ memset(buffer_get_space_unsafe(buf, 4, 7), 'b', 7);
+ buffer_set_used_size(buf, 10);
+ test_assert(memcmp(buffer_append_space_unsafe(buf, 1), "\0", 1) == 0);
+ buffer_set_used_size(buf, 11);
+ test_assert(memcmp(buffer_get_space_unsafe(buf, 0, 11), "aaaabbbbbb\0", 11) == 0);
+ test_end();
+}
+
+
+#if 0
+
+/* this code is left here to produce the output found in
+ * buffer.h should it be needed for debugging purposes */
+#include "str.h"
+#include "hex-binary.h"
+static const char *binary_to_10(const unsigned char *data, size_t size)
+{
+ string_t *str = t_str_new(size*8);
+
+ for (size_t i = 0; i < size; i++) {
+ for (int j = 0; j < 8; j++) {
+ if ((data[i] & (1 << (7-j))) != 0)
+ str_append_c(str, '1');
+ else
+ str_append_c(str, '0');
+ }
+ }
+ return str_c(str);
+}
+
+static void test_foo(void)
+{
+ buffer_t *buf = buffer_create_dynamic(default_pool, 100);
+
+ for (int i = 1; i <= 24; i++) {
+ buffer_set_used_size(buf, 0);
+ buffer_append_c(buf, 0xff);
+ buffer_append_c(buf, 0xff);
+ buffer_append_c(buf, 0xff);
+ buffer_truncate_rshift_bits(buf, i);
+ printf("%2d bits: %24s %s\n", i,
+ binary_to_hex(buf->data, buf->used),
+ binary_to_10(buf->data, buf->used));
+ }
+}
+
+#endif
+
+static void test_buffer_truncate_bits(void)
+{
+ buffer_t *buf;
+ test_begin("buffer_test_truncate_bits");
+
+ struct {
+ buffer_t input;
+ size_t bits;
+ buffer_t output;
+ } test_cases[] = {
+ { { { { "\xff\xff\xff", 3 } } }, 0, { { { "", 0 } } } },
+ { { { { "\xff\xff\xff", 3 } } }, 1, { { { "\x01", 1 } } } },
+ { { { { "\xff\xff\xff", 3 } } }, 2, { { { "\x03", 1 } } } },
+ { { { { "\xff\xff\xff", 3 } } }, 3, { { { "\x07", 1 } } } },
+ { { { { "\xff\xff\xff", 3 } } }, 4, { { { "\x0f", 1 } } } },
+ { { { { "\xff\xff\xff", 3 } } }, 5, { { { "\x1f", 1 } } } },
+ { { { { "\xff\xff\xff", 3 } } }, 6, { { { "\x3f", 1 } } } },
+ { { { { "\xff\xff\xff", 3 } } }, 7, { { { "\x7f", 1 } } } },
+ { { { { "\xff\xff\xff", 3 } } }, 8, { { { "\xff", 1 } } } },
+ { { { { "\xff\xff\xff", 3 } } }, 9, { { { "\x01\xff", 2 } } } },
+ { { { { "\xff\xff\xff", 3 } } }, 10, { { { "\x03\xff", 2 } } } },
+ { { { { "\xff\xff\xff", 3 } } }, 11, { { { "\x07\xff", 2 } } } },
+ { { { { "\xff\xff\xff", 3 } } }, 12, { { { "\x0f\xff", 2 } } } },
+ { { { { "\xff\xff\xff", 3 } } }, 13, { { { "\x1f\xff", 2 } } } },
+ { { { { "\xff\xff\xff", 3 } } }, 14, { { { "\x3f\xff", 2 } } } },
+ { { { { "\xff\xff\xff", 3 } } }, 15, { { { "\x7f\xff", 2 } } } },
+ { { { { "0123456789", 10 } } }, 16, { { { "01", 2 } } } },
+ { { { { "0123456789", 10 } } }, 24, { { { "012", 3 } } } },
+ { { { { "0123456789", 10 } } }, 32, { { { "0123", 4 } } } },
+ { { { { "0123456789", 10 } } }, 40, { { { "01234", 5 } } } },
+ { { { { "0123456789", 10 } } }, 48, { { { "012345", 6 } } } },
+ { { { { "0123456789", 10 } } }, 56, { { { "0123456", 7 } } } },
+ { { { { "0123456789", 10 } } }, 64, { { { "01234567", 8 } } } },
+ { { { { "0123456789", 10 } } }, 72, { { { "012345678", 9 } } } },
+ { { { { "0123456789", 10 } } }, 80, { { { "0123456789", 10 } } } },
+
+ { { { { "\x58\x11\xed\x02\x4d\x87\x4a\xe2\x5c\xb2\xfa\x69\xf0\xa9\x46\x2e\x04\xca\x5d\x82", 20 } } },
+ 13,
+ { { { "\x0b\x02", 2 } } }
+ },
+
+ /* special test cases for auth policy */
+
+ { { { { "\x34\x40\xc8\xc9\x3a\xb6\xe7\xc4\x3f\xc1\xc3\x4d\xd5\x56\xa3\xea\xfb\x5a\x33\x57\xac\x11\x39\x2c\x71\xcb\xee\xbb\xc8\x66\x2f\x64", 32 } } },
+ 12,
+ { { { "\x03\x44", 2 } } }
+ },
+
+ { { { { "\x49\xe5\x8a\x88\x76\xd3\x25\x68\xc9\x89\x4a\xe0\x64\xe4\x04\xf4\xf9\x13\xec\x88\x97\x47\x30\x7f\x3f\xcd\x8f\x74\x4f\x40\xd1\x25", 32 } } },
+ 12,
+ { { { "\x04\x9e", 2 } } }
+ },
+
+ { { { { "\x08\x3c\xdc\x14\x61\x80\x1c\xe8\x43\x81\x98\xfa\xc0\x64\x04\x7a\xa2\x73\x25\x6e\xe6\x4b\x85\x42\xd0\xe2\x78\xd7\x91\xb4\x89\x3f", 32 } } },
+ 12,
+ { { { "\x00\x83", 2 } } }
+ },
+
+ };
+
+ buf = t_buffer_create(10);
+
+ for(size_t i = 0; i < N_ELEMENTS(test_cases); i++) {
+ buffer_set_used_size(buf, 0);
+ buffer_copy(buf, 0, &test_cases[i].input, 0, SIZE_MAX);
+ buffer_truncate_rshift_bits(buf, test_cases[i].bits);
+ test_assert_idx(buffer_cmp(buf, &test_cases[i].output) == TRUE, i);
+ }
+
+ test_end();
+}
+
+static void test_buffer_replace(void)
+{
+ const char orig_input[] = "123456789";
+ const char data[] = "abcdefghij";
+ buffer_t *buf, *buf2;
+ unsigned int init_size, pos, size, data_size;
+
+ test_begin("buffer_replace()");
+ for (init_size = 0; init_size <= sizeof(orig_input)-1; init_size++) {
+ for (pos = 0; pos < sizeof(orig_input)+1; pos++) {
+ for (size = 0; size < sizeof(orig_input)+1; size++) {
+ for (data_size = 0; data_size <= sizeof(data)-1; data_size++) T_BEGIN {
+ buf = buffer_create_dynamic(pool_datastack_create(), 4);
+ buf2 = buffer_create_dynamic(pool_datastack_create(), 4);
+ buffer_append(buf, orig_input, init_size);
+ buffer_append(buf2, orig_input, init_size);
+
+ buffer_replace(buf, pos, size, data, data_size);
+ buffer_delete(buf2, pos, size);
+ buffer_insert(buf2, pos, data, data_size);
+ test_assert(buf->used == buf2->used &&
+ memcmp(buf->data, buf2->data, buf->used) == 0);
+ } T_END;
+ }
+ }
+ }
+
+ test_end();
+}
+
+void test_buffer(void)
+{
+ test_buffer_random();
+ test_buffer_write();
+ test_buffer_set_used_size();
+ test_buffer_truncate_bits();
+ test_buffer_replace();
+}
+
+static void fatal_buffer_free(buffer_t *buf)
+{
+ buffer_free(&buf);
+}
+
+enum fatal_test_state fatal_buffer(unsigned int stage)
+{
+ buffer_t *buf;
+
+ switch (stage) {
+ case 0:
+ test_begin("fatal buffer_create_dynamic_max()");
+ buf = buffer_create_dynamic_max(default_pool, 1, 5);
+ buffer_append(buf, "12345", 5);
+ test_expect_fatal_string("Buffer write out of range");
+ test_fatal_set_callback(fatal_buffer_free, buf);
+ buffer_append_c(buf, 'x');
+ return FATAL_TEST_FAILURE;
+ case 1:
+ buf = buffer_create_dynamic_max(default_pool, 1, 5);
+ test_expect_fatal_string("Buffer write out of range");
+ test_fatal_set_callback(fatal_buffer_free, buf);
+ buffer_append(buf, "123456", 6);
+ return FATAL_TEST_FAILURE;
+ default:
+ test_end();
+ return FATAL_TEST_FINISHED;
+ }
+}
diff --git a/src/lib/test-byteorder.c b/src/lib/test-byteorder.c
new file mode 100644
index 0000000..9e350ce
--- /dev/null
+++ b/src/lib/test-byteorder.c
@@ -0,0 +1,251 @@
+/*
+ * Copyright (c) 2016-2017 Josef 'Jeff' Sipek <jeffpc@josefsipek.net>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+#include "test-lib.h"
+#include "byteorder.h"
+
+struct bswap_run {
+ uint64_t in;
+ uint8_t out8;
+ uint16_t out16;
+ uint32_t out32;
+ uint64_t out64;
+};
+
+static const struct bswap_run runs[] = {
+ {
+ .in = 0,
+ .out8 = 0,
+ .out16 = 0,
+ .out32 = 0,
+ .out64 = 0,
+ },
+ {
+ .in = 0xffffffffffffffff,
+ .out8 = 0xff,
+ .out16 = 0xffff,
+ .out32 = 0xffffffff,
+ .out64 = 0xffffffffffffffff,
+ },
+ {
+ .in = 0x123456789abcdef0,
+ .out8 = 0xf0,
+ .out16 = 0xf0de,
+ .out32 = 0xf0debc9a,
+ .out64 = 0xf0debc9a78563412,
+ },
+ {
+ .in = 0x8080808080808080,
+ .out8 = 0x80,
+ .out16 = 0x8080,
+ .out32 = 0x80808080,
+ .out64 = 0x8080808080808080,
+ },
+};
+
+#define CHECK(iter, size, in, exp) \
+ do { \
+ uint##size##_t got = i_bswap_##size(in); \
+ \
+ test_begin(t_strdup_printf("byteorder - bswap " \
+ "(size:%-2u iter:%u)", \
+ size, iter)); \
+ test_assert(got == exp); \
+ test_end(); \
+ } while (0)
+
+static void __test(int iter, const struct bswap_run *run)
+{
+ CHECK(iter, 8, run->in & 0xff, run->out8);
+ CHECK(iter, 16, run->in & 0xffff, run->out16);
+ CHECK(iter, 32, run->in & 0xffffffff, run->out32);
+ CHECK(iter, 64, run->in, run->out64);
+}
+
+static void test_bswap(void)
+{
+ unsigned int i;
+
+ for (i = 0; i < N_ELEMENTS(runs) ; i++)
+ __test(i, &runs[i]);
+}
+
+struct unaligned_run {
+ uint8_t in[8];
+
+ /* outputs */
+ uint8_t be8;
+ uint16_t be16;
+ uint32_t be32;
+ uint64_t be64;
+
+ uint8_t le8;
+ uint16_t le16;
+ uint32_t le32;
+ uint64_t le64;
+
+#ifdef WORDS_BIGENDIAN
+#define cpu8 be8
+#define cpu16 be16
+#define cpu32 be32
+#define cpu64 be64
+#else
+#define cpu8 le8
+#define cpu16 le16
+#define cpu32 le32
+#define cpu64 le64
+#endif
+};
+
+static const struct unaligned_run uruns[] = {
+ {
+ .in = {
+ 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00,
+ },
+ .be8 = 0,
+ .be16 = 0,
+ .be32 = 0,
+ .be64 = 0,
+ .le8 = 0,
+ .le16 = 0,
+ .le32 = 0,
+ .le64 = 0,
+ },
+ {
+ .in = {
+ 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff,
+ },
+ .be8 = 0xff,
+ .be16 = 0xffff,
+ .be32 = 0xffffffff,
+ .be64 = 0xffffffffffffffff,
+ .le8 = 0xff,
+ .le16 = 0xffff,
+ .le32 = 0xffffffff,
+ .le64 = 0xffffffffffffffff,
+ },
+ {
+ .in = {
+ 0x12, 0x34, 0x56, 0x78,
+ 0x9a, 0xbc, 0xde, 0xf0,
+ },
+ .be8 = 0x12,
+ .be16 = 0x1234,
+ .be32 = 0x12345678,
+ .be64 = 0x123456789abcdef0,
+ .le8 = 0x12,
+ .le16 = 0x3412,
+ .le32 = 0x78563412,
+ .le64 = 0xf0debc9a78563412,
+ },
+ {
+ .in = {
+ 0x80, 0x80, 0x80, 0x80,
+ 0x80, 0x80, 0x80, 0x80,
+ },
+ .be8 = 0x80,
+ .be16 = 0x8080,
+ .be32 = 0x80808080,
+ .be64 = 0x8080808080808080,
+ .le8 = 0x80,
+ .le16 = 0x8080,
+ .le32 = 0x80808080,
+ .le64 = 0x8080808080808080,
+ },
+};
+
+#define __CHECK_READ(iter, size, pfx, in, fxn, exp) \
+ do { \
+ uint##size##_t got = fxn(in); \
+ \
+ test_begin(t_strdup_printf("byteorder - unaligned read "\
+ "(%-3s size:%-2u iter:%u)", \
+ pfx, size, iter)); \
+ test_assert(got == exp); \
+ test_end(); \
+ } while (0)
+
+#define CHECK_READ(iter, size, in, be_exp, le_exp, cpu_exp) \
+ do { \
+ __CHECK_READ(iter, size, "BE", in, \
+ be##size##_to_cpu_unaligned, be_exp); \
+ __CHECK_READ(iter, size, "LE", in, \
+ le##size##_to_cpu_unaligned, le_exp); \
+ __CHECK_READ(iter, size, "CPU", in, \
+ cpu##size##_to_cpu_unaligned, cpu_exp); \
+ } while (0)
+
+static void __test_read(int iter, const struct unaligned_run *run)
+{
+ CHECK_READ(iter, 8, run->in, run->be8, run->le8, run->cpu8);
+ CHECK_READ(iter, 16, run->in, run->be16, run->le16, run->cpu16);
+ CHECK_READ(iter, 32, run->in, run->be32, run->le32, run->cpu32);
+ CHECK_READ(iter, 64, run->in, run->be64, run->le64, run->cpu64);
+}
+
+#define __CHECK_WRITE(iter, size, pfx, in, fxn, exp) \
+ do { \
+ uint8_t got[size / 8]; \
+ \
+ fxn(in, got); \
+ \
+ test_begin(t_strdup_printf("byteorder - unaligned write "\
+ "(%-3s size:%-2u iter:%u)", \
+ pfx, size, iter)); \
+ test_assert(memcmp(got, exp, sizeof(got)) == 0); \
+ test_end(); \
+ } while (0)
+
+#define CHECK_WRITE(iter, size, out, be_in, le_in) \
+ do { \
+ __CHECK_WRITE(iter, size, "BE", be_in, \
+ cpu##size##_to_be_unaligned, out); \
+ __CHECK_WRITE(iter, size, "LE", le_in, \
+ cpu##size##_to_le_unaligned, out); \
+ } while (0)
+
+static void __test_write(int iter, const struct unaligned_run *run)
+{
+ CHECK_WRITE(iter, 8, run->in, run->be8, run->le8);
+ CHECK_WRITE(iter, 16, run->in, run->be16, run->le16);
+ CHECK_WRITE(iter, 32, run->in, run->be32, run->le32);
+ CHECK_WRITE(iter, 64, run->in, run->be64, run->le64);
+}
+
+static void test_unaligned(void)
+{
+ unsigned int i;
+
+ for (i = 0; i < N_ELEMENTS(uruns) ; i++)
+ __test_read(i, &uruns[i]);
+
+ for (i = 0; i < N_ELEMENTS(uruns) ; i++)
+ __test_write(i, &uruns[i]);
+}
+
+void test_byteorder(void)
+{
+ test_bswap();
+ test_unaligned();
+}
diff --git a/src/lib/test-connection.c b/src/lib/test-connection.c
new file mode 100644
index 0000000..22677c3
--- /dev/null
+++ b/src/lib/test-connection.c
@@ -0,0 +1,744 @@
+/* Copyright (c) 2018 Dovecot authors, see the included COPYING file */
+
+#include "test-lib.h"
+#include "ioloop.h"
+#include "connection.h"
+#include "istream.h"
+#include "ostream.h"
+#include "strnum.h"
+#include "strescape.h"
+
+#include <unistd.h>
+
+static const struct connection_settings client_set =
+{
+ .service_name_in = "TEST-S",
+ .service_name_out = "TEST-C",
+ .major_version = 1,
+ .minor_version = 0,
+ .client = TRUE,
+ .input_max_size = SIZE_MAX,
+ .output_max_size = SIZE_MAX,
+};
+
+static const struct connection_settings server_set =
+{
+ .service_name_in = "TEST-C",
+ .service_name_out = "TEST-S",
+ .major_version = 1,
+ .minor_version = 0,
+ .client = FALSE,
+ .input_max_size = SIZE_MAX,
+ .output_max_size = SIZE_MAX,
+};
+
+static bool received_quit = FALSE;
+static bool was_resumed = FALSE;
+static bool was_idle_killed = FALSE;
+static int received_count = 0;
+
+static void test_connection_run(const struct connection_settings *set_s,
+ const struct connection_settings *set_c,
+ const struct connection_vfuncs *v_s,
+ const struct connection_vfuncs *v_c,
+ unsigned int iter_count)
+{
+ int fds[2];
+
+ struct ioloop *loop = io_loop_create();
+ struct connection_list *clients = connection_list_init(set_c, v_c);
+ struct connection_list *servers = connection_list_init(set_s, v_s);
+ struct connection *conn_c = i_new(struct connection, 1);
+ struct connection *conn_s = i_new(struct connection, 1);
+
+ conn_s->ioloop = loop;
+ conn_c->ioloop = loop;
+
+ for(unsigned int iters = 0; iters < iter_count; iters++) {
+ test_assert(socketpair(AF_UNIX, SOCK_STREAM, 0, fds) == 0);
+
+ fd_set_nonblock(fds[0], TRUE);
+ fd_set_nonblock(fds[1], TRUE);
+ connection_init_server(servers, conn_s, "client", fds[1], fds[1]);
+ connection_init_client_fd(clients, conn_c, "server", fds[0], fds[0]);
+
+ io_loop_run(loop);
+
+ connection_deinit(conn_c);
+ connection_deinit(conn_s);
+ }
+
+ i_free(conn_c);
+ i_free(conn_s);
+
+ connection_list_deinit(&clients);
+ connection_list_deinit(&servers);
+
+ io_loop_destroy(&loop);
+}
+
+/* BEGIN SIMPLE TEST */
+
+static void test_connection_simple_client_connected(struct connection *conn, bool success)
+{
+ if (conn->list->set.client)
+ o_stream_nsend_str(conn->output, "QUIT\n");
+ test_assert(success);
+};
+
+static int
+test_connection_simple_input_args(struct connection *conn, const char *const *args)
+{
+ if (strcmp(args[0], "QUIT") == 0) {
+ received_quit = TRUE;
+ connection_disconnect(conn);
+ return 0;
+ }
+ i_error("invalid input");
+ return -1;
+}
+
+static void test_connection_simple_destroy(struct connection *conn)
+{
+ io_loop_stop(conn->ioloop);
+ connection_disconnect(conn);
+}
+
+static const struct connection_vfuncs simple_v =
+{
+ .client_connected = test_connection_simple_client_connected,
+ .input_args = test_connection_simple_input_args,
+ .destroy = test_connection_simple_destroy,
+};
+
+static void test_connection_simple(void)
+{
+ test_begin("connection simple");
+
+ test_connection_run(&server_set, &client_set, &simple_v, &simple_v, 10);
+
+ test_assert(received_quit);
+ received_quit = FALSE;
+
+ test_end();
+}
+
+/* BEGIN NO INPUT TEST */
+
+static const struct connection_settings no_input_client_set =
+{
+ .service_name_in = "TEST-S",
+ .service_name_out = "TEST-C",
+ .major_version = 1,
+ .minor_version = 0,
+ .client = TRUE,
+ .input_max_size = 0,
+ .output_max_size = SIZE_MAX,
+};
+
+static const struct connection_settings no_input_server_set =
+{
+ .service_name_in = "TEST-C",
+ .service_name_out = "TEST-S",
+ .major_version = 1,
+ .minor_version = 0,
+ .client = FALSE,
+ .input_max_size = 0,
+ .output_max_size = SIZE_MAX,
+};
+
+static void
+test_connection_no_input_input(struct connection *conn)
+{
+ const char *input;
+ struct istream *is = i_stream_create_fd(conn->fd_in, SIZE_MAX);
+ i_stream_set_blocking(is, FALSE);
+ while ((input = i_stream_read_next_line(is)) != NULL) {
+ const char *const *args = t_strsplit_tabescaped(input);
+ if (!conn->handshake_received) {
+ if (connection_handshake_args_default(conn, args) > -1)
+ conn->handshake_received = TRUE;
+ continue;
+ }
+ if (strcmp(args[0], "QUIT") == 0) {
+ received_quit = TRUE;
+ io_loop_stop(conn->ioloop);
+ break;
+ }
+ }
+ i_stream_unref(&is);
+}
+
+static const struct connection_vfuncs no_input_v =
+{
+ .client_connected = test_connection_simple_client_connected,
+ .input = test_connection_no_input_input,
+ .destroy = test_connection_simple_destroy,
+};
+
+static void test_connection_no_input(void)
+{
+ test_begin("connection no input stream");
+
+ test_connection_run(&no_input_server_set, &no_input_client_set,
+ &no_input_v, &no_input_v, 1);
+
+ test_assert(received_quit);
+ received_quit = FALSE;
+
+ test_end();
+}
+
+/* BEGIN HANDSHAKE TEST */
+static void test_connection_custom_handshake_client_connected(struct connection *conn, bool success)
+{
+ if (conn->list->set.client)
+ o_stream_nsend_str(conn->output, "HANDSHAKE\tFRIEND\n");
+ test_assert(success);
+};
+
+static int test_connection_custom_handshake_args(struct connection *conn,
+ const char *const *args)
+{
+ if (!conn->version_received) {
+ if (connection_handshake_args_default(conn, args) < 0)
+ return -1;
+ return 0;
+ }
+ if (!conn->handshake_received) {
+ if (strcmp(args[0], "HANDSHAKE") == 0 &&
+ strcmp(args[1], "FRIEND") == 0) {
+ if (!conn->list->set.client)
+ o_stream_nsend_str(conn->output, "HANDSHAKE\tFRIEND\n");
+ else
+ o_stream_nsend_str(conn->output, "QUIT\n");
+ return 1;
+ }
+ return -1;
+ }
+ return 1;
+}
+
+static const struct connection_vfuncs custom_handshake_v =
+{
+ .client_connected = test_connection_custom_handshake_client_connected,
+ .input_args = test_connection_simple_input_args,
+ .handshake_args = test_connection_custom_handshake_args,
+ .destroy = test_connection_simple_destroy,
+};
+
+static void test_connection_custom_handshake(void)
+{
+ test_begin("connection custom handshake");
+
+ test_connection_run(&server_set, &client_set, &custom_handshake_v,
+ &custom_handshake_v, 10);
+
+ test_assert(received_quit);
+ received_quit = FALSE;
+
+ test_end();
+}
+
+/* BEGIN PING PONG TEST */
+
+static int test_connection_ping_pong_input_args(struct connection *conn, const char *const *args)
+{
+ unsigned int n;
+ test_assert(args[0] != NULL && args[1] != NULL);
+ if (args[0] == NULL || args[1] == NULL)
+ return -1;
+ if (str_to_uint(args[1], &n) < 0)
+ return -1;
+ if (n > 10)
+ o_stream_nsend_str(conn->output, "QUIT\t0\n");
+ else if (strcmp(args[0], "QUIT") == 0)
+ connection_disconnect(conn);
+ else if (strcmp(args[0], "PING") == 0) {
+ received_count++;
+ o_stream_nsend_str(conn->output, t_strdup_printf("PONG\t%u\n", n+1));
+ } else if (strcmp(args[0], "PONG") == 0)
+ o_stream_nsend_str(conn->output, t_strdup_printf("PING\t%u\n", n));
+ else
+ return -1;
+ return 1;
+}
+
+static void test_connection_ping_pong_client_connected(struct connection *conn, bool success)
+{
+ o_stream_nsend_str(conn->output, "PING\t1\n");
+ test_assert(success);
+};
+
+static const struct connection_vfuncs ping_pong_v =
+{
+ .client_connected = test_connection_ping_pong_client_connected,
+ .input_args = test_connection_ping_pong_input_args,
+ .destroy = test_connection_simple_destroy,
+};
+
+static void test_connection_ping_pong(void)
+{
+ test_begin("connection ping pong");
+
+ test_connection_run(&server_set, &client_set, &ping_pong_v,
+ &ping_pong_v, 10);
+
+ test_assert(received_count == 100);
+
+ test_end();
+}
+
+/* BEGIN INPUT FULL TEST */
+
+static const struct connection_settings input_full_client_set =
+{
+ .service_name_in = "TEST-S",
+ .service_name_out = "TEST-C",
+ .major_version = 1,
+ .minor_version = 0,
+ .client = TRUE,
+ .input_max_size = 100,
+ .output_max_size = SIZE_MAX,
+};
+
+static int test_connection_input_full_input_args(struct connection *conn,
+ const char *const *args ATTR_UNUSED)
+{
+ /* send a long line */
+ for (unsigned int i = 0; i < 200; i++)
+ o_stream_nsend(conn->output, "c", 1);
+ return 1;
+}
+
+static void test_connection_input_full_destroy(struct connection *conn)
+{
+ test_assert(conn->disconnect_reason == CONNECTION_DISCONNECT_BUFFER_FULL ||
+ conn->list->set.client == FALSE);
+ test_connection_simple_destroy(conn);
+}
+
+static const struct connection_vfuncs input_full_v =
+{
+ .client_connected = test_connection_simple_client_connected,
+ .input_args = test_connection_input_full_input_args,
+ .destroy = test_connection_input_full_destroy,
+};
+
+static void test_connection_input_full(void)
+{
+ test_begin("connection input full");
+
+ test_connection_run(&server_set, &input_full_client_set, &input_full_v,
+ &simple_v, 10);
+ test_end();
+}
+
+/* BEGIN RESUME TEST */
+static struct timeout *to_send_quit = NULL;
+static struct timeout *to_resume = NULL;
+
+static void test_connection_resume_client_connected(struct connection *conn, bool success)
+{
+ test_assert(success);
+ o_stream_nsend_str(conn->output, "BEGIN\n");
+}
+
+static void test_connection_resume_continue(struct connection *conn)
+{
+ timeout_remove(&to_resume);
+ /* ensure QUIT wasn't received early */
+ was_resumed = !received_quit;
+ connection_input_resume(conn);
+}
+
+static void test_connection_resume_send_quit(struct connection *conn)
+{
+ timeout_remove(&to_send_quit);
+ o_stream_nsend_str(conn->output, "QUIT\n");
+}
+
+static int test_connection_resume_input_args(struct connection *conn,
+ const char *const *args)
+{
+ test_assert(args[0] != NULL);
+ if (args[0] == NULL)
+ return -1;
+
+ if (strcmp(args[0], "BEGIN") == 0) {
+ o_stream_nsend_str(conn->output, "HALT\n");
+ to_send_quit = timeout_add_short(10, test_connection_resume_send_quit, conn);
+ } else if (strcmp(args[0], "HALT") == 0) {
+ connection_input_halt(conn);
+ to_resume = timeout_add_short(100, test_connection_resume_continue, conn);
+ } else if (strcmp(args[0], "QUIT") == 0) {
+ received_quit = TRUE;
+ connection_disconnect(conn);
+ }
+
+ return 1;
+}
+
+static const struct connection_vfuncs resume_v =
+{
+ .client_connected = test_connection_resume_client_connected,
+ .input_args = test_connection_resume_input_args,
+ .destroy = test_connection_simple_destroy,
+};
+
+static void test_connection_resume(void)
+{
+ test_begin("connection resume");
+
+ was_resumed = received_quit = FALSE;
+ test_connection_run(&server_set, &client_set, &resume_v, &resume_v, 1);
+
+ test_assert(was_resumed);
+ test_assert(received_quit);
+ was_resumed = received_quit = FALSE;
+
+ test_end();
+}
+
+/* BEGIN RESUME PIPELINED TEST */
+static int test_connection_resume_pipelined_input_args(struct connection *conn,
+ const char *const *args)
+{
+ test_assert(args[0] != NULL);
+ if (args[0] == NULL)
+ return -1;
+
+ if (strcmp(args[0], "BEGIN") == 0) {
+ o_stream_nsend_str(conn->output, "HALT\nQUIT\n");
+ } else if (strcmp(args[0], "HALT") == 0) {
+ connection_input_halt(conn);
+ to_resume = timeout_add_short(100, test_connection_resume_continue, conn);
+ return 0;
+ } else if (strcmp(args[0], "QUIT") == 0) {
+ received_quit = TRUE;
+ connection_disconnect(conn);
+ }
+
+ return 1;
+}
+
+static const struct connection_vfuncs resume_pipelined_v =
+{
+ .client_connected = test_connection_resume_client_connected,
+ .input_args = test_connection_resume_pipelined_input_args,
+ .destroy = test_connection_simple_destroy,
+};
+
+static void test_connection_resume_pipelined(void)
+{
+ test_begin("connection resume pipelined");
+
+ was_resumed = received_quit = FALSE;
+ test_connection_run(&server_set, &client_set,
+ &resume_pipelined_v, &resume_pipelined_v, 1);
+
+ test_assert(was_resumed);
+ test_assert(received_quit);
+ was_resumed = received_quit = FALSE;
+
+ test_end();
+}
+
+/* BEGIN IDLE KILL TEST */
+
+static void
+test_connection_idle_kill_client_connected(struct connection *conn ATTR_UNUSED,
+ bool success)
+{
+ test_assert(success);
+};
+
+static const struct connection_settings idle_kill_server_set =
+{
+ .service_name_in = "TEST-C",
+ .service_name_out = "TEST-S",
+ .major_version = 1,
+ .minor_version = 0,
+ .client = FALSE,
+ .input_max_size = SIZE_MAX,
+ .output_max_size = SIZE_MAX,
+ .input_idle_timeout_secs = 1,
+};
+
+static void test_connection_idle_kill_timeout(struct connection *conn)
+{
+ was_idle_killed = TRUE;
+ o_stream_nsend_str(conn->output, "QUIT\n");
+}
+
+static const struct connection_vfuncs idle_kill_v =
+{
+ .client_connected = test_connection_idle_kill_client_connected,
+ .input_args = test_connection_simple_input_args,
+ .destroy = test_connection_simple_destroy,
+ .idle_timeout = test_connection_idle_kill_timeout,
+};
+
+static void test_connection_idle_kill(void)
+{
+ test_begin("connection idle kill");
+
+ was_idle_killed = received_quit = FALSE;
+ test_connection_run(&idle_kill_server_set, &client_set, &idle_kill_v,
+ &idle_kill_v, 1);
+
+ test_assert(received_quit);
+ test_assert(was_idle_killed);
+ was_idle_killed = received_quit = FALSE;
+
+ test_end();
+}
+
+/* BEGIN HANDSHAKE FAILED TEST (version) */
+
+static void test_connection_handshake_failed_destroy(struct connection *conn)
+{
+ test_assert(conn->disconnect_reason == CONNECTION_DISCONNECT_HANDSHAKE_FAILED);
+ test_connection_simple_destroy(conn);
+}
+
+static const struct connection_vfuncs handshake_failed_version_v =
+{
+ .client_connected = test_connection_simple_client_connected,
+ .input_args = test_connection_simple_input_args,
+ .destroy = test_connection_handshake_failed_destroy,
+};
+
+static void test_connection_handshake_failed_version(void)
+{
+ static const struct connection_settings client_sets[] = {
+ {
+ .service_name_in = "TEST-S",
+ .service_name_out = "TEST-S",
+ .major_version = 1,
+ .minor_version = 0,
+ .client = TRUE,
+ .input_max_size = SIZE_MAX,
+ .output_max_size = SIZE_MAX,
+ },
+ {
+ .service_name_in = "TEST-C",
+ .service_name_out = "TEST-C",
+ .major_version = 1,
+ .minor_version = 0,
+ .client = TRUE,
+ .input_max_size = SIZE_MAX,
+ .output_max_size = SIZE_MAX,
+ },
+ {
+ .service_name_in = "TEST-S",
+ .service_name_out = "TEST-C",
+ .major_version = 2,
+ .minor_version = 0,
+ .client = TRUE,
+ .input_max_size = SIZE_MAX,
+ .output_max_size = SIZE_MAX,
+ }
+ };
+
+ static const struct connection_settings client_set_minor = {
+ .service_name_in = "TEST-S",
+ .service_name_out = "TEST-C",
+ .major_version = 1,
+ .minor_version = 2,
+ .client = TRUE,
+ .input_max_size = SIZE_MAX,
+ .output_max_size = SIZE_MAX,
+ };
+
+ test_begin("connection handshake failed (version)");
+
+ test_expect_errors(N_ELEMENTS(client_sets));
+
+ /* this should stay FALSE during the version mismatch sets */
+ received_quit = FALSE;
+ for (size_t i = 0; i < N_ELEMENTS(client_sets); i++) {
+ test_connection_run(&server_set, &client_sets[i], &simple_v,
+ &handshake_failed_version_v, 1);
+ test_assert(!received_quit);
+ }
+
+ received_quit = FALSE;
+ test_connection_run(&server_set, &client_set_minor, &simple_v,
+ &simple_v, 1);
+ test_assert(received_quit);
+ received_quit = FALSE;
+
+ test_end();
+}
+
+/* BEGIN HANDSHAKE FAILED TEST (args) */
+
+static int test_connection_handshake_failed_1_args(struct connection *conn ATTR_UNUSED,
+ const char *const *args ATTR_UNUSED)
+{
+ /* just fail */
+ return -1;
+}
+
+static const struct connection_vfuncs handshake_failed_1_v =
+{
+ .client_connected = test_connection_simple_client_connected,
+ .input_args = test_connection_simple_input_args,
+ .handshake_args = test_connection_handshake_failed_1_args,
+ .destroy = test_connection_handshake_failed_destroy,
+};
+
+static void test_connection_handshake_failed_args(void)
+{
+ test_begin("connection handshake failed (handshake_args)");
+
+ test_connection_run(&server_set, &client_set, &simple_v,
+ &handshake_failed_1_v, 10);
+
+ test_end();
+}
+
+/* BEGIN HANDSHAKE FAILED TEST (handshake_line) */
+
+static int test_connection_handshake_failed_2_line(struct connection *conn ATTR_UNUSED,
+ const char *line ATTR_UNUSED)
+{
+ return -1;
+}
+
+static const struct connection_vfuncs handshake_failed_2_v =
+{
+ .client_connected = test_connection_simple_client_connected,
+ .input_args = test_connection_simple_input_args,
+ .handshake_line = test_connection_handshake_failed_2_line,
+ .destroy = test_connection_handshake_failed_destroy,
+};
+
+static void test_connection_handshake_failed_line(void)
+{
+ test_begin("connection handshake failed (handshake_line)");
+
+ test_connection_run(&server_set, &client_set, &simple_v,
+ &handshake_failed_2_v, 10);
+
+ test_end();
+}
+
+/* BEGIN HANDSHAKE FAILED TEST (handshake) */
+
+static int test_connection_handshake_failed_3(struct connection *conn ATTR_UNUSED)
+{
+ return -1;
+}
+
+static const struct connection_vfuncs handshake_failed_3_v =
+{
+ .client_connected = test_connection_simple_client_connected,
+ .input_args = test_connection_simple_input_args,
+ .handshake = test_connection_handshake_failed_3,
+ .destroy = test_connection_handshake_failed_destroy,
+};
+
+static void test_connection_handshake_failed_input(void)
+{
+ test_begin("connection handshake failed (handshake)");
+
+ test_connection_run(&server_set, &client_set, &simple_v,
+ &handshake_failed_3_v, 10);
+
+ test_end();
+}
+
+/* BEGIN CONNECTION ERRORED TEST (ensure correct error) */
+
+static void test_connection_errored_client_connected(struct connection *conn,
+ bool success)
+{
+ test_assert(success);
+ o_stream_nsend_str(conn->output, "HELLO\n");
+}
+
+static void test_connection_errored_destroy(struct connection *conn)
+{
+ test_assert(conn->disconnect_reason == CONNECTION_DISCONNECT_DEINIT);
+ test_connection_simple_destroy(conn);
+}
+
+static int test_connection_errored_input_line(struct connection *conn ATTR_UNUSED,
+ const char *line)
+{
+ if (str_begins(line, "VERSION"))
+ return 1;
+ return -1;
+}
+
+static const struct connection_vfuncs test_connection_errored_1_v =
+{
+ .client_connected = test_connection_errored_client_connected,
+ .input_line = test_connection_errored_input_line,
+ .destroy = test_connection_errored_destroy,
+};
+
+static void test_connection_input_error_reason(void)
+{
+ test_begin("connection input error (correct disconnect reason)");
+
+ test_connection_run(&server_set, &client_set, &test_connection_errored_1_v,
+ &test_connection_errored_1_v, 10);
+
+ test_end();
+}
+
+/* END CONNECTION ERRORED TEST */
+
+/* BEGIN NO VERSION TEST */
+
+static const struct connection_settings no_version_client_set =
+{
+ .major_version = 0,
+ .minor_version = 0,
+ .client = TRUE,
+ .input_max_size = SIZE_MAX,
+ .output_max_size = SIZE_MAX,
+ .dont_send_version = TRUE,
+};
+
+static const struct connection_settings no_version_server_set =
+{
+ .major_version = 0,
+ .minor_version = 0,
+ .input_max_size = SIZE_MAX,
+ .output_max_size = SIZE_MAX,
+ .dont_send_version = TRUE,
+};
+
+static void test_connection_no_version(void)
+{
+ test_begin("connection no version sent");
+
+ test_connection_run(&no_version_server_set, &no_version_client_set,
+ &simple_v, &simple_v, 10);
+
+ test_end();
+}
+
+/* END NO VERSION TEST */
+
+void test_connection(void)
+{
+ test_connection_simple();
+ test_connection_no_input();
+ test_connection_custom_handshake();
+ test_connection_ping_pong();
+ test_connection_input_full();
+ test_connection_resume();
+ test_connection_resume_pipelined();
+ test_connection_idle_kill();
+ test_connection_handshake_failed_version();
+ test_connection_handshake_failed_args();
+ test_connection_handshake_failed_line();
+ test_connection_handshake_failed_input();
+ test_connection_input_error_reason();
+ test_connection_no_version();
+}
diff --git a/src/lib/test-cpu-limit.c b/src/lib/test-cpu-limit.c
new file mode 100644
index 0000000..c4c1090
--- /dev/null
+++ b/src/lib/test-cpu-limit.c
@@ -0,0 +1,145 @@
+#include "test-lib.h"
+#include "lib-signals.h"
+#include "guid.h"
+#include "time-util.h"
+#include "cpu-limit.h"
+
+#include <unistd.h>
+#include <fcntl.h>
+#include <sys/time.h>
+#include <sys/resource.h>
+
+/* The CPU limits aren't exact. Allow this much leniency in the time
+ comparisons. Note that system CPU usage can grow very large on loaded
+ systems, so we're not checking its upper limit at all. */
+#define ALLOW_MSECS_BELOW 500
+#define ALLOW_MSECS_ABOVE 3000
+
+static const char *const test_path = ".test.cpulimit";
+
+static struct timeval get_cpu_time(enum cpu_limit_type type)
+{
+ struct rusage rusage;
+ struct timeval cpu_usage = { 0, 0 };
+
+ /* Query cpu usage so far */
+ if (getrusage(RUSAGE_SELF, &rusage) < 0)
+ i_fatal("getrusage() failed: %m");
+ if ((type & CPU_LIMIT_TYPE_USER) != 0)
+ timeval_add(&cpu_usage, &rusage.ru_utime);
+ if ((type & CPU_LIMIT_TYPE_SYSTEM) != 0)
+ timeval_add(&cpu_usage, &rusage.ru_stime);
+ return cpu_usage;
+}
+
+static void test_cpu_loop_once(void)
+{
+ guid_128_t guid;
+
+ /* consume some user CPU */
+ for (unsigned int i = 0; i < 10000; i++)
+ guid_128_generate(guid);
+ /* consume some system CPU */
+ int fd = creat(test_path, 0600);
+ if (fd == -1)
+ i_fatal("creat(%s) failed: %m", test_path);
+ if (write(fd, guid, sizeof(guid)) < 0)
+ i_fatal("write(%s) failed: %m", test_path);
+ i_close_fd(&fd);
+}
+
+static void
+test_cpu_limit_simple(enum cpu_limit_type type, const char *type_str)
+{
+ struct cpu_limit *climit;
+ struct timeval usage, cpu;
+ int diff_msecs;
+
+ test_begin(t_strdup_printf("cpu limit - simple (%s)", type_str));
+
+ lib_signals_init();
+ climit = cpu_limit_init(2, type);
+ usage = get_cpu_time(type);
+
+ while (!cpu_limit_exceeded(climit))
+ test_cpu_loop_once();
+
+ cpu_limit_deinit(&climit);
+ cpu = get_cpu_time(type);
+ diff_msecs = timeval_diff_msecs(&cpu, &usage);
+ test_assert_cmp(diff_msecs, >=, 2000 - ALLOW_MSECS_BELOW);
+
+ lib_signals_deinit();
+ test_end();
+}
+
+static void test_cpu_limit_nested(enum cpu_limit_type type, const char *type_str)
+{
+ struct cpu_limit *climit1, *climit2;
+ struct timeval usage1, cpu;
+ unsigned int n;
+ int diff_msecs;
+
+ test_begin(t_strdup_printf("cpu limit - nested (%s)", type_str));
+
+ lib_signals_init();
+ climit1 = cpu_limit_init(3, type);
+ usage1 = get_cpu_time(type);
+
+ while (!cpu_limit_exceeded(climit1) && !test_has_failed()) {
+ climit2 = cpu_limit_init(1, type);
+
+ while (!cpu_limit_exceeded(climit2) && !test_has_failed())
+ test_cpu_loop_once();
+
+ cpu_limit_deinit(&climit2);
+ }
+
+ cpu_limit_deinit(&climit1);
+ cpu = get_cpu_time(type);
+ diff_msecs = timeval_diff_msecs(&cpu, &usage1);
+ test_assert_cmp(diff_msecs, >=, 3000 - ALLOW_MSECS_BELOW);
+
+ lib_signals_deinit();
+ test_end();
+
+ test_begin(t_strdup_printf("cpu limit - nested2 (%s)", type_str));
+
+ lib_signals_init();
+ climit1 = cpu_limit_init(3, type);
+ usage1 = get_cpu_time(type);
+
+ n = 0;
+ while (!cpu_limit_exceeded(climit1) && !test_has_failed()) {
+ if (++n >= 3) {
+ /* Consume last second in top cpu limit */
+ test_cpu_loop_once();
+ continue;
+ }
+ climit2 = cpu_limit_init(1, type);
+
+ while (!cpu_limit_exceeded(climit2) && !test_has_failed())
+ test_cpu_loop_once();
+
+ cpu_limit_deinit(&climit2);
+ }
+
+ cpu_limit_deinit(&climit1);
+ cpu = get_cpu_time(type);
+ diff_msecs = timeval_diff_msecs(&cpu, &usage1);
+ test_assert_cmp(diff_msecs, >=, 3000 - ALLOW_MSECS_BELOW);
+
+ i_unlink_if_exists(test_path);
+ lib_signals_deinit();
+ test_end();
+}
+
+void test_cpu_limit(void)
+{
+ test_cpu_limit_simple(CPU_LIMIT_TYPE_USER, "user");
+ test_cpu_limit_simple(CPU_LIMIT_TYPE_SYSTEM, "system");
+ test_cpu_limit_simple(CPU_LIMIT_TYPE_ALL, "all");
+ test_cpu_limit_nested(CPU_LIMIT_TYPE_USER, "user");
+ test_cpu_limit_nested(CPU_LIMIT_TYPE_SYSTEM, "system");
+ test_cpu_limit_nested(CPU_LIMIT_TYPE_ALL, "all");
+}
diff --git a/src/lib/test-crc32.c b/src/lib/test-crc32.c
new file mode 100644
index 0000000..3292d1c
--- /dev/null
+++ b/src/lib/test-crc32.c
@@ -0,0 +1,14 @@
+/* Copyright (c) 2010-2018 Dovecot authors, see the included COPYING file */
+
+#include "test-lib.h"
+#include "crc32.h"
+
+void test_crc32(void)
+{
+ const char str[] = "foo\0bar";
+
+ test_begin("crc32");
+ test_assert(crc32_str(str) == 0x8c736521);
+ test_assert(crc32_data(str, sizeof(str)) == 0x32c9723d);
+ test_end();
+}
diff --git a/src/lib/test-data-stack.c b/src/lib/test-data-stack.c
new file mode 100644
index 0000000..c38da60
--- /dev/null
+++ b/src/lib/test-data-stack.c
@@ -0,0 +1,455 @@
+/* Copyright (c) 2014-2018 Dovecot authors, see the included COPYING file */
+
+#include "test-lib.h"
+#include "lib-event-private.h"
+#include "event-filter.h"
+#include "data-stack.h"
+
+static int ds_grow_event_count = 0;
+
+static bool
+test_ds_grow_event_callback(struct event *event,
+ enum event_callback_type type,
+ struct failure_context *ctx,
+ const char *fmt ATTR_UNUSED,
+ va_list args ATTR_UNUSED)
+{
+ const struct event_field *field;
+
+ if (type != EVENT_CALLBACK_TYPE_SEND)
+ return TRUE;
+
+ ds_grow_event_count++;
+ test_assert(ctx->type == LOG_TYPE_DEBUG);
+
+ field = event_find_field_nonrecursive(event, "alloc_size");
+ test_assert(field != NULL &&
+ field->value_type == EVENT_FIELD_VALUE_TYPE_INTMAX &&
+ field->value.intmax >= 1024 * (5 + 100));
+ field = event_find_field_nonrecursive(event, "used_size");
+ test_assert(field != NULL &&
+ field->value_type == EVENT_FIELD_VALUE_TYPE_INTMAX &&
+ field->value.intmax >= 1024 * (5 + 100));
+ field = event_find_field_nonrecursive(event, "last_alloc_size");
+ test_assert(field != NULL &&
+ field->value_type == EVENT_FIELD_VALUE_TYPE_INTMAX &&
+ field->value.intmax >= 1024 * 100);
+ field = event_find_field_nonrecursive(event, "frame_marker");
+ test_assert(field != NULL &&
+ field->value_type == EVENT_FIELD_VALUE_TYPE_STR &&
+ strstr(field->value.str, "data-stack.c") != NULL);
+ return TRUE;
+}
+
+static void test_ds_grow_event(void)
+{
+ const char *error;
+
+ test_begin("data-stack grow event");
+ event_register_callback(test_ds_grow_event_callback);
+
+ i_assert(event_get_global_debug_log_filter() == NULL);
+ struct event_filter *filter = event_filter_create();
+ test_assert(event_filter_parse("event=data_stack_grow", filter, &error) == 0);
+ event_set_global_debug_log_filter(filter);
+ event_filter_unref(&filter);
+
+ /* make sure the test won't fail due to earlier data stack
+ allocations. */
+ data_stack_free_unused();
+ T_BEGIN {
+ (void)t_malloc0(1024*5);
+ test_assert(ds_grow_event_count == 0);
+ (void)t_malloc0(1024*100);
+ test_assert(ds_grow_event_count == 1);
+ } T_END;
+ event_unset_global_debug_log_filter();
+ event_unregister_callback(test_ds_grow_event_callback);
+ test_end();
+}
+
+static void test_ds_get_used_size(void)
+{
+ test_begin("data-stack data_stack_get_used_size()");
+ size_t size1 = data_stack_get_used_size();
+ (void)t_malloc0(500);
+ size_t size2 = data_stack_get_used_size();
+ test_assert(size1 + 500 <= size2);
+
+ T_BEGIN {
+ (void)t_malloc0(300);
+ size_t sub_size1 = data_stack_get_used_size();
+ T_BEGIN {
+ (void)t_malloc0(300);
+ } T_END;
+ test_assert_cmp(sub_size1, ==, data_stack_get_used_size());
+ } T_END;
+ test_assert_cmp(size2, ==, data_stack_get_used_size());
+ test_end();
+}
+
+static void test_ds_get_bytes_available(void)
+{
+ test_begin("data-stack t_get_bytes_available()");
+ for (unsigned int i = 0; i < 32; i++) {
+ size_t orig_avail = t_get_bytes_available();
+ size_t avail1;
+ T_BEGIN {
+ if (i > 0)
+ t_malloc_no0(i);
+ avail1 = t_get_bytes_available();
+ t_malloc_no0(avail1);
+ test_assert_idx(t_get_bytes_available() == 0, i);
+ t_malloc_no0(1);
+ test_assert_idx(t_get_bytes_available() > 0, i);
+ } T_END;
+ T_BEGIN {
+ if (i > 0)
+ t_malloc_no0(i);
+ size_t avail2 = t_get_bytes_available();
+ test_assert_idx(avail1 == avail2, i);
+ t_malloc_no0(avail2 + 1);
+ test_assert_idx(t_get_bytes_available() > 0, i);
+ } T_END;
+ test_assert_idx(t_get_bytes_available() == orig_avail, i);
+ }
+ test_end();
+}
+
+static void ATTR_FORMAT(2, 0)
+test_ds_growing_debug(const struct failure_context *ctx ATTR_UNUSED,
+ const char *format, va_list args)
+{
+ ds_grow_event_count++;
+ (void)t_strdup_vprintf(format, args);
+}
+
+static void test_ds_grow_in_event(void)
+{
+ size_t i, alloc1 = 8096;
+ unsigned char *buf;
+ const char *error;
+
+ test_begin("data-stack grow in event");
+
+ struct event_filter *filter = event_filter_create();
+ event_set_global_debug_log_filter(filter);
+ test_assert(event_filter_parse("event=data_stack_grow", filter, &error) == 0);
+ event_filter_unref(&filter);
+
+ i_set_debug_handler(test_ds_growing_debug);
+ buf = t_buffer_get(alloc1);
+ for (i = 0; i < alloc1; i++)
+ buf[i] = i & 0xff;
+
+ test_assert(ds_grow_event_count == 0);
+ buf = t_buffer_reget(buf, 65536);
+ test_assert(ds_grow_event_count == 1);
+ for (i = 0; i < alloc1; i++) {
+ if (buf[i] != (unsigned char)i)
+ break;
+ }
+ test_assert(i == alloc1);
+
+ i_set_debug_handler(default_error_handler);
+ event_unset_global_debug_log_filter();
+ test_end();
+}
+
+static void test_ds_buffers(void)
+{
+ test_begin("data-stack buffer growth");
+ T_BEGIN {
+ size_t i;
+ unsigned char *p;
+ size_t left = t_get_bytes_available();
+ while (left < 10000) {
+ t_malloc_no0(left+1); /* force a new block */
+ left = t_get_bytes_available();
+ }
+ left -= 64; /* make room for the sentry if DEBUG */
+ p = t_buffer_get(1);
+ p[0] = 1;
+ for (i = 2; i <= left; i++) {
+ /* grow it */
+ unsigned char *p2 = t_buffer_get(i);
+ test_assert_idx(p == p2, i);
+ p[i-1] = i & 0xff;
+ test_assert_idx(p[i-2] == (unsigned char)(i-1), i);
+ }
+ /* now fix it permanently */
+ t_buffer_alloc_last_full();
+ test_assert(t_get_bytes_available() < 64 + MEM_ALIGN(1));
+ } T_END;
+ test_end();
+
+ test_begin("data-stack buffer interruption");
+ T_BEGIN {
+ void *b = t_buffer_get(1000);
+ void *a = t_malloc_no0(1);
+ void *b2 = t_buffer_get(1001);
+ test_assert(a == b); /* expected, not guaranteed */
+ test_assert(b2 != b);
+ } T_END;
+ test_end();
+
+ test_begin("data-stack buffer with reallocs");
+ T_BEGIN {
+ size_t bigleft = t_get_bytes_available();
+ size_t i;
+ /* with DEBUG: the stack frame allocation takes 96 bytes
+ and malloc takes extra 40 bytes + alignment, so don't let
+ "i" be too high. */
+ for (i = 1; i < bigleft-96-40-16; i += i_rand_limit(32)) T_BEGIN {
+ unsigned char *p, *p2;
+ size_t left;
+ t_malloc_no0(i);
+ left = t_get_bytes_available();
+ /* The most useful idx for the assert is 'left' */
+ test_assert_idx(left <= bigleft-i, left);
+ p = t_buffer_get(left/2);
+ p[0] = 'Z'; p[left/2 - 1] = 'Z';
+ p2 = t_buffer_get(left + left/2);
+ test_assert_idx(p != p2, left);
+ test_assert_idx(p[0] == 'Z', left);
+ test_assert_idx(p[left/2 -1] == 'Z', left);
+ } T_END;
+ } T_END;
+ test_end();
+}
+
+static void test_ds_realloc()
+{
+ test_begin("data-stack realloc");
+ T_BEGIN {
+ size_t i;
+ unsigned char *p;
+ size_t left = t_get_bytes_available();
+ while (left < 10000) {
+ t_malloc_no0(left+1); /* force a new block */
+ left = t_get_bytes_available();
+ }
+ left -= 64; /* make room for the sentry if DEBUG */
+ p = t_malloc_no0(1);
+ p[0] = 1;
+ for (i = 2; i <= left; i++) {
+ /* grow it */
+ test_assert_idx(t_try_realloc(p, i), i);
+ p[i-1] = i & 0xff;
+ test_assert_idx(p[i-2] == (unsigned char)(i-1), i);
+ }
+ test_assert(t_get_bytes_available() < 64 + MEM_ALIGN(1));
+ } T_END;
+ test_end();
+}
+
+static void test_ds_recurse(int depth, int number, size_t size)
+{
+ int i;
+ char **ps;
+ char tag[2] = { depth+1, '\0' };
+ int try_fails = 0;
+ data_stack_frame_t t_id = t_push_named("test_ds_recurse[%i]", depth);
+ ps = t_buffer_get(sizeof(char *) * number);
+ i_assert(ps != NULL);
+ t_buffer_alloc(sizeof(char *) * number);
+
+ for (i = 0; i < number; i++) {
+ ps[i] = t_malloc_no0(size/2);
+ bool re = t_try_realloc(ps[i], size);
+ i_assert(ps[i] != NULL);
+ if (!re) {
+ try_fails++;
+ ps[i] = t_malloc_no0(size);
+ }
+ /* drop our own canaries */
+ memset(ps[i], tag[0], size);
+ ps[i][size-2] = 0;
+ }
+ /* Do not expect a high failure rate from t_try_realloc */
+ test_assert_idx(try_fails <= number / 20, depth);
+
+ /* Now recurse... */
+ if(depth>0)
+ test_ds_recurse(depth-1, number, size);
+
+ /* Test our canaries are still intact */
+ for (i = 0; i < number; i++) {
+ test_assert_idx(strspn(ps[i], tag) == size - 2, i);
+ test_assert_idx(ps[i][size-1] == tag[0], i);
+ }
+ test_assert_idx(t_pop(&t_id), depth);
+}
+
+static void test_ds_recursive(void)
+{
+ int count = 20, depth = 80;
+ int i;
+
+ test_begin("data-stack recursive");
+ size_t init_size = data_stack_get_used_size();
+ for(i = 0; i < count; i++) T_BEGIN {
+ int number=i_rand_limit(100)+50;
+ int size=i_rand_limit(100)+50;
+ test_ds_recurse(depth, number, size);
+ } T_END;
+ test_assert_cmp(init_size, ==, data_stack_get_used_size());
+ test_end();
+}
+
+static void test_ds_pass_str(void)
+{
+ data_stack_frame_t frames[32*2 + 1]; /* BLOCK_FRAME_COUNT*2 + 1 */
+ const char *strings[N_ELEMENTS(frames)];
+
+ test_begin("data-stack pass string");
+ for (unsigned int frame = 0; frame < N_ELEMENTS(frames); frame++) {
+ frames[frame] = t_push("test");
+ if (frame % 10 == 5) {
+ /* increase block counts */
+ (void)t_malloc_no0(1024*30);
+ (void)t_malloc_no0(1024*30);
+ }
+ strings[frame] = t_strdup_printf("frame %d", frame);
+ for (unsigned int i = 0; i <= frame; i++) {
+ test_assert_idx(data_stack_frame_contains(&frames[frame], strings[i]) == (i == frame),
+ frame * 100 + i);
+ }
+ }
+
+ const char *last_str = strings[N_ELEMENTS(frames)-1];
+ for (unsigned int frame = N_ELEMENTS(frames); frame > 0; ) {
+ frame--;
+ test_assert(t_pop_pass_str(&frames[frame], &last_str));
+ }
+ test_assert_strcmp(last_str, "frame 64");
+
+ /* make sure the pass_condition works properly */
+ const char *error, *orig_error, *orig2_error;
+ T_BEGIN {
+ (void)t_strdup("qwertyuiop");
+ error = orig_error = t_strdup("123456");
+ } T_END_PASS_STR_IF(TRUE, &error);
+
+ orig2_error = orig_error;
+ T_BEGIN {
+ (void)t_strdup("abcdefghijklmnopqrstuvwxyz");
+ } T_END_PASS_STR_IF(FALSE, &orig2_error);
+ /* orig_error and orig2_error both point to freed data stack frame */
+ test_assert(orig_error == orig2_error);
+ /* the passed error is still valid though */
+ test_assert_strcmp(error, "123456");
+
+ test_end();
+}
+
+void test_data_stack(void)
+{
+ void (*tests[])(void) = {
+ test_ds_grow_event,
+ test_ds_get_used_size,
+ test_ds_get_bytes_available,
+ test_ds_grow_in_event,
+ test_ds_buffers,
+ test_ds_realloc,
+ test_ds_recursive,
+ test_ds_pass_str,
+ };
+ for (unsigned int i = 0; i < N_ELEMENTS(tests); i++) {
+ ds_grow_event_count = 0;
+ data_stack_free_unused();
+ T_BEGIN {
+ tests[i]();
+ } T_END;
+ }
+}
+
+enum fatal_test_state fatal_data_stack(unsigned int stage)
+{
+#ifdef DEBUG
+#define NONEXISTENT_STACK_FRAME_ID (data_stack_frame_t)999999999
+ /* If we abort, then we'll be left with a dangling t_push()
+ keep a record of our temporary stack id, so we can clean up. */
+ static data_stack_frame_t t_id = NONEXISTENT_STACK_FRAME_ID;
+ static unsigned char *undo_ptr = NULL;
+ static unsigned char undo_data;
+ static bool things_are_messed_up = FALSE;
+ if (stage != 0) {
+ /* Presume that we need to clean up from the prior test:
+ undo the evil write, then we will be able to t_pop cleanly,
+ and finally we can end the test stanza. */
+ if (things_are_messed_up || undo_ptr == NULL)
+ return FATAL_TEST_ABORT; /* abort, things are messed up with t_pop */
+ *undo_ptr = undo_data;
+ undo_ptr = NULL;
+ /* t_pop mustn't abort, that would cause recursion */
+ things_are_messed_up = TRUE;
+ if (t_id != NONEXISTENT_STACK_FRAME_ID && !t_pop(&t_id))
+ return FATAL_TEST_ABORT; /* abort, things are messed up with us */
+ things_are_messed_up = FALSE;
+ t_id = NONEXISTENT_STACK_FRAME_ID;
+ test_end();
+ }
+
+ switch(stage) {
+ case 0: {
+ unsigned char *p;
+ test_begin("fatal data-stack underrun");
+ t_id = t_push_named("fatal_data_stack underrun");
+ size_t left = t_get_bytes_available();
+ p = t_malloc_no0(left-80); /* will fit */
+ p = t_malloc_no0(100); /* won't fit, will get new block */
+ int seek = 0;
+ /* Seek back for the canary, don't assume endianness */
+ while(seek > -60 &&
+ ((p[seek+1] != 0xDB) ||
+ ((p[seek] != 0xBA || p[seek+2] != 0xAD) &&
+ (p[seek+2] != 0xBA || p[seek] != 0xAD))))
+ seek--;
+ if (seek <= -60)
+ return FATAL_TEST_ABORT; /* abort, couldn't find header */
+ undo_ptr = p + seek;
+ undo_data = *undo_ptr;
+ *undo_ptr = '*';
+ /* t_malloc_no0 will panic block header corruption */
+ test_expect_fatal_string("Corrupted data stack canary");
+ (void)t_malloc_no0(10);
+ return FATAL_TEST_FAILURE;
+ }
+
+ case 1: case 2: {
+ test_begin(stage == 1 ? "fatal t_malloc_no0 overrun near" : "fatal t_malloc_no0 overrun far");
+ t_id = t_push_named(stage == 1 ? "fatal t_malloc_no0 overrun first" : "fatal t_malloc_no0 overrun far");
+ unsigned char *p = t_malloc_no0(10);
+ undo_ptr = p + 10 + (stage == 1 ? 0 : 8*4-1); /* presumes sentry size */
+ undo_data = *undo_ptr;
+ *undo_ptr = '*';
+ /* t_pop will now fail */
+ test_expect_fatal_string("buffer overflow");
+ (void)t_pop(&t_id);
+ t_id = NONEXISTENT_STACK_FRAME_ID; /* We're FUBAR, mustn't pop next entry */
+ return FATAL_TEST_FAILURE;
+ }
+
+ case 3: case 4: {
+ test_begin(stage == 3 ? "fatal t_buffer_get overrun near" : "fatal t_buffer_get overrun far");
+ t_id = t_push_named(stage == 3 ? "fatal t_buffer overrun near" : "fatal t_buffer_get overrun far");
+ unsigned char *p = t_buffer_get(10);
+ undo_ptr = p + 10 + (stage == 3 ? 0 : 8*4-1);
+ undo_data = *undo_ptr;
+ *undo_ptr = '*';
+ /* t_pop will now fail */
+ test_expect_fatal_string("buffer overflow");
+ (void)t_pop(&t_id);
+ t_id = NONEXISTENT_STACK_FRAME_ID; /* We're FUBAR, mustn't pop next entry */
+ return FATAL_TEST_FAILURE;
+ }
+
+ default:
+ things_are_messed_up = TRUE;
+ return FATAL_TEST_FINISHED;
+ }
+#else
+ return stage == 0 ? FATAL_TEST_FINISHED : FATAL_TEST_ABORT;
+#endif
+}
diff --git a/src/lib/test-env-util.c b/src/lib/test-env-util.c
new file mode 100644
index 0000000..7a24615
--- /dev/null
+++ b/src/lib/test-env-util.c
@@ -0,0 +1,92 @@
+/* Copyright (c) 2021 Dovecot authors, see the included COPYING file */
+
+#include "test-lib.h"
+#include "env-util.h"
+
+void test_env_util(void)
+{
+ test_begin("env util");
+
+ env_put("ENVUTIL_BACKUP", "saved");
+ struct env_backup *backup = env_backup_save();
+
+ /* test env_clean() */
+ env_clean();
+ char ***env = env_get_environ_p();
+ test_assert(*env == NULL || **env == NULL);
+ test_assert(getenv("ENVUTIL_BACKUP") == NULL);
+
+ /* test env_put_array() */
+ const char *add_env[] = { "a=1", "b=1", "c=1", "d=1", NULL };
+ env_put_array(add_env);
+ test_assert_strcmp(getenv("a"), "1");
+ test_assert_strcmp(getenv("b"), "1");
+ test_assert_strcmp(getenv("c"), "1");
+ test_assert_strcmp(getenv("d"), "1");
+ test_assert(getenv("e") == NULL);
+ const char *add_env2[] = { "b=", "e=2", NULL };
+ env_put_array(add_env2);
+ test_assert_strcmp(getenv("a"), "1");
+ test_assert_strcmp(getenv("b"), "");
+ test_assert_strcmp(getenv("c"), "1");
+ test_assert_strcmp(getenv("d"), "1");
+ test_assert_strcmp(getenv("e"), "2");
+
+ /* test env_clean_except() */
+ const char *preserve_env[] = { "a", "c", NULL };
+ env_clean_except(preserve_env);
+ test_assert_strcmp(getenv("a"), "1");
+ test_assert(getenv("b") == NULL);
+ test_assert_strcmp(getenv("c"), "1");
+ test_assert(getenv("d") == NULL);
+ test_assert(*env != NULL &&
+ (null_strcmp((*env)[0], "a=1") == 0 ||
+ null_strcmp((*env)[0], "c=1") == 0));
+ test_assert(*env != NULL &&
+ (null_strcmp((*env)[1], "a=1") == 0 ||
+ null_strcmp((*env)[1], "c=1") == 0));
+
+ /* test env_remove() */
+ env_remove("a");
+ test_assert(getenv("a") == NULL);
+ test_assert(getenv("c") != NULL);
+ env_remove("a");
+ test_assert(getenv("a") == NULL);
+ test_assert(getenv("c") != NULL);
+ env_remove("c");
+ test_assert(getenv("c") == NULL);
+ test_assert(*env == NULL || **env == NULL);
+
+ /* test restoring */
+ env_backup_restore(backup);
+ test_assert_strcmp(getenv("ENVUTIL_BACKUP"), "saved");
+ env_put("ENVUTIL_BACKUP", "overwrite");
+ test_assert_strcmp(getenv("ENVUTIL_BACKUP"), "overwrite");
+
+ /* test restoring again */
+ env_backup_restore(backup);
+ test_assert_strcmp(getenv("ENVUTIL_BACKUP"), "saved");
+ env_backup_free(&backup);
+
+ test_end();
+}
+
+enum fatal_test_state fatal_env_util(unsigned int stage)
+{
+ switch (stage) {
+ case 0:
+ test_begin("env util fatals");
+
+ test_expect_fatal_string("strchr(name, '=') == NULL");
+ env_put("key=bad", "value");
+ return FATAL_TEST_FAILURE;
+ case 1:
+ test_expect_fatal_string("value != NULL");
+ const char *const envs[] = { "key", NULL };
+ env_put_array(envs);
+ return FATAL_TEST_FAILURE;
+ default:
+ test_end();
+ return FATAL_TEST_FINISHED;
+ }
+}
diff --git a/src/lib/test-event-category-register.c b/src/lib/test-event-category-register.c
new file mode 100644
index 0000000..50cc038
--- /dev/null
+++ b/src/lib/test-event-category-register.c
@@ -0,0 +1,320 @@
+/* Copyright (c) 2019 Dovecot authors, see the included COPYING file */
+
+#include "test-lib.h"
+#include "ioloop.h"
+#include "time-util.h"
+#include "lib-event-private.h"
+#include "failures-private.h"
+
+/* we call a generic "unregister category function; to tell it what exact
+ * behavior it should expect from lib-lib, we pass in one of the following
+ * values
+ */
+enum unreg_expectation {
+ UNREG_NOT_LAST,
+ UNREG_LAST,
+ UNREG_NOP,
+};
+
+#define CAT_NAME_PREFIX "test-category"
+
+/* pointer to a category we expect to be registered/unregistered */
+static struct event_category *expected_callback_cat;
+static bool callback_called;
+
+static struct event *dummy_event;
+
+static void check_category(struct event_category *cat)
+{
+ callback_called = TRUE;
+
+ /* lib-lib called a callback with a NULL? (useless and a bug) */
+ test_assert(cat != NULL);
+
+ /* callback called, but didn't expect to be called? */
+ test_assert(expected_callback_cat != NULL);
+
+ /* test_assert() doesn't terminate, so avoid NULL ptr derefs later on */
+ if ((cat == NULL) || (expected_callback_cat == NULL))
+ return;
+
+ /* check that the categories have the same values */
+ test_assert(strcmp(cat->name, expected_callback_cat->name) == 0);
+ test_assert(cat->internal == expected_callback_cat->internal);
+}
+
+static void check_cat_registered(const char *name, bool should_exist)
+{
+ struct event_category *cat;
+
+ callback_called = FALSE;
+ cat = event_category_find_registered(name);
+ test_assert(callback_called == FALSE);
+
+ test_assert((cat != NULL) == should_exist);
+}
+
+static void register_cat(struct event_category *newcat,
+ struct event_category *expcat)
+{
+ /* start with a known state - no regs expected */
+ expected_callback_cat = NULL;
+ callback_called = FALSE;
+
+ dummy_event = event_create(NULL);
+ test_assert(callback_called == FALSE);
+
+ /* we expect a registration only when adding a cat */
+ expected_callback_cat = (expcat);
+ event_add_category(dummy_event, (newcat));
+ expected_callback_cat = NULL;
+
+ /* check that all went well */
+ test_assert(callback_called == (expcat != NULL));
+ test_assert((newcat)->internal != NULL);
+ test_assert(event_category_find_registered((newcat)->name) != NULL);
+
+ /* clean up */
+ event_unref(&dummy_event);
+}
+
+static void unregister_cat(struct event_category *cat,
+ enum unreg_expectation expectation)
+{
+ /* sanity check that cat is set up as expected */
+ switch (expectation) {
+ case UNREG_NOT_LAST:
+ /* must be registered to unregister */
+ test_assert(event_category_find_registered((cat)->name) != NULL);
+ expected_callback_cat = NULL;
+ break;
+
+ case UNREG_LAST:
+ /* must be registered to unregister */
+ test_assert(event_category_find_registered((cat)->name) != NULL);
+ expected_callback_cat = cat;
+ break;
+
+ case UNREG_NOP:
+ /* must not be registered for no-op */
+ /* event_category_find_registered(cat->name) should return
+ NULL, but since we don't actually unregister this lookup
+ would fail. Therefore, we skip it. */
+ expected_callback_cat = NULL;
+ break;
+ }
+
+ /* Note: We don't actually have a way to unregister categories. We
+ keep the above checks and the calls to this function as a form of
+ documentation of how unregistering should work. */
+}
+
+static void test_event_category_1ptr_null(void)
+{
+#define CAT_NAME_0 CAT_NAME_PREFIX "-1ptr-null"
+ static struct event_category cat = { .name = CAT_NAME_0 };
+
+ test_begin("event category rereg: same ptr, NULL parent");
+
+ check_cat_registered(CAT_NAME_0, FALSE);
+ register_cat(&cat, &cat);
+ register_cat(&cat, NULL);
+ check_cat_registered(CAT_NAME_0, TRUE);
+
+ unregister_cat(&cat, UNREG_LAST);
+ unregister_cat(&cat, UNREG_NOP);
+
+ test_end();
+#undef CAT_NAME_0
+}
+
+static void test_event_category_1ptr_nonnull(void)
+{
+#define CAT_NAME_0 CAT_NAME_PREFIX "-1ptr-nonnull-0"
+#define CAT_NAME_1 CAT_NAME_PREFIX "-1ptr-nonnull-1"
+ static struct event_category cat = { .name = CAT_NAME_0 };
+ static struct event_category cat_with_parent = { .name = CAT_NAME_1, .parent = &cat };
+
+ test_begin("event category rereg: same ptr, non-NULL parent");
+
+ check_cat_registered(CAT_NAME_0, FALSE);
+ check_cat_registered(CAT_NAME_1, FALSE);
+ register_cat(&cat, &cat);
+ register_cat(&cat_with_parent, &cat_with_parent);
+ register_cat(&cat_with_parent, NULL);
+ check_cat_registered(CAT_NAME_0, TRUE);
+ check_cat_registered(CAT_NAME_1, TRUE);
+
+ unregister_cat(&cat_with_parent, UNREG_LAST);
+ unregister_cat(&cat_with_parent, UNREG_NOP);
+ /* NOTE: we must unreg children before parent cats */
+ unregister_cat(&cat, UNREG_LAST);
+ unregister_cat(&cat, UNREG_NOP);
+
+ test_end();
+#undef CAT_NAME_0
+#undef CAT_NAME_1
+}
+
+static void test_event_category_2ptr_null(void)
+{
+#define CAT_NAME_0 CAT_NAME_PREFIX "-2ptr-null"
+ static struct event_category cat0 = { .name = CAT_NAME_0 };
+ static struct event_category cat1 = { .name = CAT_NAME_0 };
+
+ test_begin("event category rereg: different ptr, NULL parent");
+
+ check_cat_registered(CAT_NAME_0, FALSE);
+ register_cat(&cat0, &cat0);
+ register_cat(&cat1, NULL);
+ check_cat_registered(CAT_NAME_0, TRUE);
+
+ unregister_cat(&cat0, UNREG_NOT_LAST);
+ unregister_cat(&cat1, UNREG_LAST);
+ unregister_cat(&cat0, UNREG_NOP);
+ unregister_cat(&cat1, UNREG_NOP);
+
+ test_end();
+#undef CAT_NAME_0
+}
+
+static void test_event_category_2ptr_nonnull_same(void)
+{
+#define CAT_NAME_0 CAT_NAME_PREFIX "-2ptr-nonnull-same-0"
+#define CAT_NAME_1 CAT_NAME_PREFIX "-2ptr-nonnull-same-1"
+ static struct event_category cat = { .name = CAT_NAME_0 };
+ static struct event_category cat_with_parent0 = { .name = CAT_NAME_1, .parent = &cat };
+ static struct event_category cat_with_parent1 = { .name = CAT_NAME_1, .parent = &cat };
+
+ test_begin("event category rereg: different ptr, same non-NULL parent");
+
+ check_cat_registered(CAT_NAME_0, FALSE);
+ check_cat_registered(CAT_NAME_1, FALSE);
+ register_cat(&cat, &cat);
+ register_cat(&cat_with_parent0, &cat_with_parent0);
+ register_cat(&cat_with_parent1, NULL);
+ check_cat_registered(CAT_NAME_0, TRUE);
+ check_cat_registered(CAT_NAME_1, TRUE);
+
+ unregister_cat(&cat_with_parent0, UNREG_NOT_LAST);
+ unregister_cat(&cat_with_parent1, UNREG_LAST);
+ unregister_cat(&cat_with_parent0, UNREG_NOP);
+ unregister_cat(&cat_with_parent1, UNREG_NOP);
+ /* NOTE: we must unreg children before parent cats */
+ unregister_cat(&cat, UNREG_LAST);
+ unregister_cat(&cat, UNREG_NOP);
+
+ test_end();
+#undef CAT_NAME_0
+#undef CAT_NAME_1
+}
+
+static void test_event_category_2ptr_nonnull_similar(void)
+{
+#define CAT_NAME_0 CAT_NAME_PREFIX "-2ptr-nonnull-similar-0"
+#define CAT_NAME_1 CAT_NAME_PREFIX "-2ptr-nonnull-similar-1"
+ static struct event_category cat0 = { .name = CAT_NAME_0 };
+ static struct event_category cat1 = { .name = CAT_NAME_0 };
+ static struct event_category cat_with_parent0 = { .name = CAT_NAME_1, .parent = &cat0 };
+ static struct event_category cat_with_parent1 = { .name = CAT_NAME_1, .parent = &cat1 };
+
+ test_begin("event category rereg: different ptr, similar non-NULL parent");
+
+ check_cat_registered(CAT_NAME_0, FALSE);
+ check_cat_registered(CAT_NAME_1, FALSE);
+ register_cat(&cat0, &cat0);
+ register_cat(&cat1, NULL);
+ register_cat(&cat_with_parent0, &cat_with_parent0);
+ register_cat(&cat_with_parent1, NULL);
+ check_cat_registered(CAT_NAME_0, TRUE);
+ check_cat_registered(CAT_NAME_1, TRUE);
+
+ unregister_cat(&cat_with_parent0, UNREG_NOT_LAST);
+ unregister_cat(&cat_with_parent1, UNREG_LAST);
+ unregister_cat(&cat_with_parent0, UNREG_NOP);
+ unregister_cat(&cat_with_parent1, UNREG_NOP);
+ /* NOTE: we must unreg children before parent cats */
+ unregister_cat(&cat0, UNREG_NOT_LAST);
+ unregister_cat(&cat1, UNREG_LAST);
+ unregister_cat(&cat0, UNREG_NOP);
+ unregister_cat(&cat1, UNREG_NOP);
+
+ test_end();
+#undef CAT_NAME_0
+#undef CAT_NAME_1
+}
+
+void test_event_category_register(void)
+{
+ event_category_register_callback(check_category);
+
+ /*
+ * registering/unregistering the same exact category struct (i.e.,
+ * the pointer is the same) is a no-op after the first call
+ */
+ test_event_category_1ptr_null();
+ test_event_category_1ptr_nonnull();
+
+ /*
+ * registering/unregistering two different category structs (i.e.,
+ * the pointers are different) is a almost a no-op
+ */
+ test_event_category_2ptr_null();
+ test_event_category_2ptr_nonnull_same();
+ test_event_category_2ptr_nonnull_similar();
+
+ event_category_unregister_callback(check_category);
+}
+
+enum fatal_test_state fatal_event_category_register(unsigned int stage)
+{
+#define CAT_NAME_0 CAT_NAME_PREFIX "-2ptr-nonnull-different-0"
+#define CAT_NAME_1 CAT_NAME_PREFIX "-2ptr-nonnull-different-1"
+#define CAT_NAME_2 CAT_NAME_PREFIX "-2ptr-nonnull-different-2"
+ static struct event_category cat_no_parent0 = { .name = CAT_NAME_0 };
+ static struct event_category cat_parent0 = { .name = CAT_NAME_1, .parent = &cat_no_parent0 };
+ static struct event_category cat_other = { .name = CAT_NAME_2 };
+ static struct event_category cat_other_parent = { .name = CAT_NAME_1, .parent = &cat_other };
+
+ /* we have only one fatal stage at this point */
+ switch (stage) {
+ case 0:
+ event_category_register_callback(check_category);
+
+ test_begin("event category rereg: different ptr, different non-NULL parent");
+
+ check_cat_registered(CAT_NAME_0, FALSE);
+ check_cat_registered(CAT_NAME_1, FALSE);
+ check_cat_registered(CAT_NAME_2, FALSE);
+ register_cat(&cat_no_parent0, &cat_no_parent0);
+ register_cat(&cat_other, &cat_other);
+ register_cat(&cat_parent0, &cat_parent0);
+
+ test_expect_fatal_string("event category parent mismatch detected");
+ register_cat(&cat_other_parent, NULL); /* expected panic */
+
+ return FATAL_TEST_FAILURE;
+ case 1:
+ event_unref(&dummy_event);
+
+ unregister_cat(&cat_parent0, UNREG_LAST);
+ unregister_cat(&cat_parent0, UNREG_NOP);
+ unregister_cat(&cat_other, UNREG_LAST);
+ unregister_cat(&cat_other, UNREG_NOP);
+ /* NOTE: we must unreg children before parent cats */
+ unregister_cat(&cat_no_parent0, UNREG_LAST);
+ unregister_cat(&cat_no_parent0, UNREG_NOP);
+
+ test_end();
+
+ event_category_unregister_callback(check_category);
+
+ return FATAL_TEST_FINISHED;
+
+ default:
+ return FATAL_TEST_ABORT;
+ }
+#undef CAT_NAME_0
+#undef CAT_NAME_1
+#undef CAT_NAME_2
+}
diff --git a/src/lib/test-event-filter-expr.c b/src/lib/test-event-filter-expr.c
new file mode 100644
index 0000000..444f16f
--- /dev/null
+++ b/src/lib/test-event-filter-expr.c
@@ -0,0 +1,250 @@
+/* Copyright (c) 2021 Dovecot authors, see the included COPYING file */
+
+#include "test-lib.h"
+#include "str.h"
+#include "strescape.h"
+#include "event-filter.h"
+#include "event-filter-private.h"
+
+#define STRING1 "X"
+#define STRING2 "Y"
+
+/* dummy values, at least for now */
+#define SOURCE_FILENAME "blah.c"
+#define SOURCE_LINE 123
+
+static void check_expr(const char *test_name,
+ struct event *event,
+ struct event_filter *filter,
+ enum event_filter_log_type log_type,
+ bool expected)
+{
+ struct event_filter_node *expr;
+ unsigned int num_queries;
+ bool got;
+
+ /* get at the expr inside the filter */
+ expr = event_filter_get_expr_for_testing(filter, &num_queries);
+ test_out_quiet(t_strdup_printf("%s:num_queries==1", test_name),
+ num_queries == 1); /* should have only one query */
+
+ got = event_filter_query_match_eval(expr, event,
+ SOURCE_FILENAME, SOURCE_LINE,
+ log_type);
+ test_out_quiet(t_strdup_printf("%s:got=expected", test_name),
+ got == expected);
+}
+
+static void do_test_expr(const char *filter_string, struct event *event,
+ enum event_filter_log_type log_type,
+ bool expected)
+{
+ const char *test_name, *error;
+
+ test_name = t_strdup_printf(
+ "%.*s log type + event {a=%s, b=%s} + filter '%s' (exp %s)",
+ 3, /* truncate the type name to avoid CI seeing 'warning' messages */
+ event_filter_category_from_log_type(log_type),
+ event_find_field_recursive_str(event, "a"),
+ event_find_field_recursive_str(event, "b"),
+ filter_string,
+ expected ? "true" : "false");
+
+ /* set up the filter expression */
+ struct event_filter *filter = event_filter_create();
+ test_out_quiet(t_strdup_printf("%s:event_filter_parse()", test_name),
+ event_filter_parse(filter_string, filter, &error) == 0);
+
+ check_expr(test_name, event, filter, log_type, expected);
+
+ event_filter_unref(&filter);
+}
+
+static void test_unary_expr(struct event *event,
+ const char *expr, bool truth,
+ enum event_filter_log_type log_type)
+{
+ /*
+ * The UNARY() macro checks:
+ *
+ * 1. expr
+ * 2. NOT expr
+ * 3. NOT (expr)
+ *
+ * Note that numbers 2 and 3 are equivalent.
+ *
+ * The truth argument specifies the expected truth-iness of the
+ * passed in expression.
+ */
+#define UNARY() \
+ T_BEGIN { \
+ do_test_expr(expr, \
+ event, log_type, truth); \
+ do_test_expr(t_strdup_printf("NOT %s", expr), \
+ event, log_type, !truth); \
+ do_test_expr(t_strdup_printf("NOT (%s)", expr), \
+ event, log_type, !truth); \
+ } T_END
+
+ UNARY();
+}
+
+static void test_binary_expr(struct event *event,
+ const char *expr1, const char *expr2,
+ bool truth1, bool truth2,
+ enum event_filter_log_type log_type)
+{
+ /*
+ * The BINARY() macro checks:
+ *
+ * 1. expr1 op expr2
+ * 2. NOT expr1 op expr2
+ * 3. NOT (expr1) op expr2
+ * 4. (NOT expr1) op expr2
+ * 5. expr1 op NOT expr2
+ * 6. expr1 op NOT (expr2)
+ * 7. expr1 op (NOT expr2)
+ * 8. NOT (expr1 op expr2)
+ * 9. NOT expr1 op NOT expr2
+ * 10. NOT (expr1) op NOT (expr2)
+ * 11. (NOT expr1) op (NOT expr2)
+ *
+ * Where op is OR or AND.
+ *
+ * Note that:
+ * - numbers 2, 3, and 4 are equivalent
+ * - numbers 5, 6, and 7 are equivalent
+ * - numbers 9, 10, and 11 are equivalent
+ *
+ * The truth arugments specify the expected truth-iness of the
+ * passed in expressions.
+ */
+#define BINARY(opstr, op) \
+ T_BEGIN { \
+ do_test_expr(t_strdup_printf("%s %s %s", expr1, opstr, expr2),\
+ event, log_type, \
+ (truth1) op (truth2)); \
+ do_test_expr(t_strdup_printf("NOT %s %s %s", expr1, opstr, expr2),\
+ event, log_type, \
+ !(truth1) op (truth2)); \
+ do_test_expr(t_strdup_printf("NOT (%s) %s %s", expr1, opstr, expr2),\
+ event, log_type, \
+ !(truth1) op (truth2)); \
+ do_test_expr(t_strdup_printf("(NOT %s) %s %s", expr1, opstr, expr2),\
+ event, log_type, \
+ !(truth1) op (truth2)); \
+ do_test_expr(t_strdup_printf("%s %s NOT %s", expr1, opstr, expr2),\
+ event, log_type, \
+ (truth1) op !(truth2)); \
+ do_test_expr(t_strdup_printf("%s %s NOT (%s)", expr1, opstr, expr2),\
+ event, log_type, \
+ (truth1) op !(truth2)); \
+ do_test_expr(t_strdup_printf("%s %s (NOT %s)", expr1, opstr, expr2),\
+ event, log_type, \
+ (truth1) op !(truth2)); \
+ do_test_expr(t_strdup_printf("NOT (%s %s %s)", expr1, opstr, expr2),\
+ event, log_type, \
+ !((truth1) op (truth2))); \
+ do_test_expr(t_strdup_printf("NOT %s %s NOT %s", expr1, opstr, expr2),\
+ event, log_type, \
+ !(truth1) op !(truth2)); \
+ do_test_expr(t_strdup_printf("NOT (%s) %s NOT (%s)", expr1, opstr, expr2),\
+ event, log_type, \
+ !(truth1) op !(truth2)); \
+ do_test_expr(t_strdup_printf("(NOT %s) %s (NOT %s)", expr1, opstr, expr2),\
+ event, log_type, \
+ !(truth1) op !(truth2)); \
+ } T_END
+
+ BINARY("OR", ||);
+ BINARY("AND", &&);
+}
+
+static void test_event_filter_expr_fields(enum event_filter_log_type log_type)
+{
+ static const char *values[] = {
+ NULL,
+ "",
+ STRING1,
+ STRING2,
+ };
+ unsigned int a, b;
+
+#define STR_IS_EMPTY(v) \
+ (((v) == NULL) || (strcmp("", (v)) == 0))
+#define STR_MATCHES(v, c) \
+ (((v) != NULL) && (strcmp((c), (v)) == 0))
+
+ /* unary */
+ for (a = 0; a < N_ELEMENTS(values); a++) {
+ /* set up the event to match against */
+ struct event *event = event_create(NULL);
+ event_add_str(event, "a", values[a]);
+
+ test_unary_expr(event,
+ "a=\"\"",
+ STR_IS_EMPTY(values[a]),
+ log_type);
+ test_unary_expr(event,
+ "a=" STRING1,
+ STR_MATCHES(values[a], STRING1),
+ log_type);
+
+ event_unref(&event);
+ }
+
+ /* binary */
+ for (a = 0; a < N_ELEMENTS(values); a++) {
+ for (b = 0; b < N_ELEMENTS(values); b++) {
+ /* set up the event to match against */
+ struct event *event = event_create(NULL);
+ event_add_str(event, "a", values[a]);
+ event_add_str(event, "b", values[b]);
+
+ test_binary_expr(event,
+ "a=\"\"",
+ "b=\"\"",
+ STR_IS_EMPTY(values[a]),
+ STR_IS_EMPTY(values[b]),
+ log_type);
+ test_binary_expr(event,
+ "a=" STRING1,
+ "b=\"\"",
+ STR_MATCHES(values[a], STRING1),
+ STR_IS_EMPTY(values[b]),
+ log_type);
+ test_binary_expr(event,
+ "a=\"\"",
+ "b=" STRING2,
+ STR_IS_EMPTY(values[a]),
+ STR_MATCHES(values[b], STRING2),
+ log_type);
+ test_binary_expr(event,
+ "a=" STRING1,
+ "b=" STRING2,
+ STR_MATCHES(values[a], STRING1),
+ STR_MATCHES(values[b], STRING2),
+ log_type);
+
+ event_unref(&event);
+ }
+ }
+}
+
+void test_event_filter_expr(void)
+{
+ static const enum event_filter_log_type log_types[] = {
+ EVENT_FILTER_LOG_TYPE_DEBUG,
+ EVENT_FILTER_LOG_TYPE_INFO,
+ EVENT_FILTER_LOG_TYPE_WARNING,
+ EVENT_FILTER_LOG_TYPE_ERROR,
+ EVENT_FILTER_LOG_TYPE_FATAL,
+ EVENT_FILTER_LOG_TYPE_PANIC,
+ };
+ unsigned int i;
+
+ test_begin("event filter expressions");
+ for (i = 0; i < N_ELEMENTS(log_types); i++)
+ test_event_filter_expr_fields(log_types[i]);
+ test_end();
+}
diff --git a/src/lib/test-event-filter-merge.c b/src/lib/test-event-filter-merge.c
new file mode 100644
index 0000000..b520cf9
--- /dev/null
+++ b/src/lib/test-event-filter-merge.c
@@ -0,0 +1,67 @@
+/* Copyright (c) 2021 Dovecot authors, see the included COPYING file */
+
+#include "test-lib.h"
+#include "str.h"
+#include "event-filter.h"
+
+static void filter_merge(const char *parent_str, const char *child_str)
+{
+ struct event_filter *parent, *child;
+ const char *test_name, *error;
+ string_t *out = t_str_new(128);
+
+ test_name = t_strdup_printf("parent %s, child %s",
+ (parent_str == NULL) ? "NULL" : parent_str,
+ (child_str == NULL) ? "NULL" : child_str);
+
+ parent = event_filter_create();
+ child = event_filter_create();
+
+ /* prime the filters with an expression */
+ if (parent_str != NULL) {
+ test_out_quiet(t_strdup_printf("%s:parent", test_name),
+ event_filter_parse(parent_str, parent, &error) == 0);
+ }
+ if (child_str != NULL) {
+ test_out_quiet(t_strdup_printf("%s:child", test_name),
+ event_filter_parse(child_str, child, &error) == 0);
+ }
+ /* merge */
+ event_filter_merge(parent, child);
+
+ /* export - to visit/deref everything in the filter */
+ event_filter_export(parent, out);
+ event_filter_export(child, out);
+
+ event_filter_unref(&parent);
+ event_filter_unref(&child);
+}
+
+void test_event_filter_merge(void)
+{
+ static const char *inputs[] = {
+ NULL,
+ /* event name */
+ "event=\"bar\"",
+ "event=\"\"",
+ /* category */
+ "category=\"bar\"",
+ "category=\"\"",
+ /* source location */
+ "source_location=\"bar:123\"",
+ "source_location=\"bar\"",
+ "source_location=\"\"",
+ /* field */
+ "foo=\"bar\"",
+ "foo=\"\"",
+ };
+ unsigned int i, j;
+
+ test_begin("event filter merge");
+ for (i = 0; i < N_ELEMENTS(inputs); i++) {
+ for (j = 0; j < N_ELEMENTS(inputs); j++) T_BEGIN {
+ filter_merge(inputs[i], inputs[j]);
+ } T_END;
+ }
+ test_end();
+}
diff --git a/src/lib/test-event-filter-parser.c b/src/lib/test-event-filter-parser.c
new file mode 100644
index 0000000..3dd2658
--- /dev/null
+++ b/src/lib/test-event-filter-parser.c
@@ -0,0 +1,543 @@
+/* Copyright (c) 2020 Dovecot authors, see the included COPYING file */
+
+#include "test-lib.h"
+#include "str.h"
+#include "strescape.h"
+#include "event-filter.h"
+
+#define GOOD(i, o) \
+ { \
+ .input = (i), \
+ .output = (o), \
+ .fails = FALSE, \
+ }
+
+#define BAD(i, o) \
+ { \
+ .input = (i), \
+ .output = (o), \
+ .fails = TRUE, \
+ }
+
+enum quoting {
+ QUOTE_MUST,
+ QUOTE_MAY,
+ QUOTE_MUST_NOT,
+};
+
+static const char *what_special[] = {
+ "event",
+ "category",
+ "source_location",
+};
+
+/* some sample field names */
+static const char *what_fields_single[] = {
+ "foo",
+ "foo_bar",
+ "foo-bar",
+};
+
+static const char *comparators[] = {
+ "=",
+ "<",
+ "<=",
+ ">",
+ ">=",
+};
+
+/* values that may be quoted or not quoted */
+static const char *values_single[] = {
+ "foo",
+ "foo.c",
+ "foo.c:123",
+
+ /* wildcards */
+ "*foo",
+ "f*o",
+ "foo*",
+ "*",
+ "?foo",
+ "f?o",
+ "foo?",
+ "?",
+};
+
+/* values that need to be quoted */
+static const char *values_multi[] = {
+ "foo bar",
+ "foo\tbar",
+ "foo\nbar",
+ "foo\rbar",
+ "foo\"bar",
+ "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec ac "
+ "vestibulum magna. Maecenas erat mi, finibus et tellus id, suscipit "
+ "varius arcu. Morbi faucibus diam in ligula suscipit, non bibendum "
+ "orci venenatis. Vestibulum mattis luctus dictum. Vivamus ultrices "
+ "tincidunt vehicula. Aliquam nec ante vitae libero dignissim finibus "
+ "non ac massa. Proin sit amet semper ligula. Curabitur eleifend massa "
+ "et arcu euismod lacinia. Phasellus sapien mauris, dignissim vitae "
+ "commodo at, consequat eget augue. Integer posuere non enim eu "
+ "laoreet. Nulla eget lectus at enim sodales rutrum. Donec tincidunt "
+ "nibh ac convallis pulvinar. Nunc facilisis tempus ligula. Nullam at "
+ "ultrices enim, eu faucibus ipsum."
+ /* utf-8: >= U+128 only */
+ "\xc3\xa4\xc3\xa1\xc4\x8d\xc4\x8f\xc4\x9b\xc5\x88\xc3\xb6\xc5\x99\xc3\xbc\xc3\xba\xc5\xaf",
+ /* utf-8: ascii + combining char */
+ "r\xcc\x8c",
+
+ /* wildcards */
+ "foo * bar",
+ "foo ? bar",
+};
+
+/* boolean operators used as values get lowercased unless they are quoted */
+static const struct values_oper {
+ const char *in;
+ const char *out_unquoted;
+ const char *out_quoted;
+} values_oper[] = {
+ { "AND", "and", "AND" },
+ { "ANd", "and", "ANd" },
+ { "AnD", "and", "AnD" },
+ { "And", "and", "And" },
+ { "aND", "and", "aND" },
+ { "aNd", "and", "aNd" },
+ { "anD", "and", "anD" },
+ { "and", "and", "and" },
+
+ { "OR", "or", "OR" },
+ { "Or", "or", "Or" },
+ { "oR", "or", "oR" },
+ { "or", "or", "or" },
+
+ { "NOT", "not", "NOT" },
+ { "NOt", "not", "NOt" },
+ { "NoT", "not", "NoT" },
+ { "Not", "not", "Not" },
+ { "nOT", "not", "nOT" },
+ { "nOt", "not", "nOt" },
+ { "noT", "not", "noT" },
+ { "not", "not", "not" },
+};
+
+static struct test {
+ const char *input;
+ const char *output;
+ bool fails;
+} tests[] = {
+ GOOD("", ""),
+
+ /* unquoted tokens can be only [a-zA-Z0-9:.*?_-]+ */
+ BAD("abc=r\xcc\x8c", "event filter: syntax error"),
+
+ /* check that spaces and extra parens don't break anything */
+#define CHECK_REAL(sp1, key, sp2, sp3, value, sp4) \
+ GOOD(sp1 key sp2 "=" sp3 value sp4, \
+ "(" key "=\"" value "\")")
+#define CHECK_SPACES(key, value, sp, op, cp) \
+ CHECK_REAL(sp op, key, "", "", value, "" cp), \
+ CHECK_REAL(op sp, key, "", "", value, "" cp), \
+ CHECK_REAL(op "", key, sp, "", value, "" cp), \
+ CHECK_REAL(op "", key, "", sp, value, "" cp), \
+ CHECK_REAL(op "", key, "", "", value, sp cp), \
+ CHECK_REAL(op "", key, "", "", value, cp sp)
+#define CHECK_PARENS(key, value, sp) \
+ CHECK_SPACES(key, value, sp, "", ""), \
+ CHECK_SPACES(key, value, sp, "(", ")"), \
+ CHECK_SPACES(key, value, sp, "((", "))"), \
+ CHECK_SPACES(key, value, sp, "(((", ")))")
+
+ CHECK_PARENS("event", "abc", " "),
+ CHECK_PARENS("event", "abc", "\t"),
+ CHECK_PARENS("event", "abc", "\n"),
+ CHECK_PARENS("event", "abc", "\r"),
+ CHECK_PARENS("event", "abc", " "),
+#undef CHECK_PARENS
+#undef CHECK_SPACES
+#undef CHECK_REAL
+
+ /* check empty parens */
+ BAD("()", "event filter: syntax error"),
+
+ /* check name only / name+comparator (!negated & negated) */
+#define CHECK_CMP_REAL(not, name, cmp, err) \
+ BAD(not name cmp, err), \
+ BAD(not "\"" name "\"" cmp, err)
+#define CHECK_CMP(name, cmp, err) \
+ CHECK_CMP_REAL("", name, cmp, err), \
+ CHECK_CMP_REAL("NOT ", name, cmp, err)
+#define CHECK(name) \
+ CHECK_CMP(name, "", \
+ "event filter: syntax error"), \
+ CHECK_CMP(name, "=", \
+ "event filter: syntax error"), \
+ CHECK_CMP(name, "<", \
+ "event filter: syntax error"), \
+ CHECK_CMP(name, "<=", \
+ "event filter: syntax error"), \
+ CHECK_CMP(name, ">", \
+ "event filter: syntax error"), \
+ CHECK_CMP(name, ">=", \
+ "event filter: syntax error")
+
+ CHECK("event"),
+ CHECK("source_location"),
+ CHECK("category"),
+ CHECK("foo-field-name"),
+#undef CHECK
+#undef CHECK_CMP
+#undef CHECK_CMP_REAL
+
+ /* check simple nesting */
+#define CHECK(binop1, binop2) \
+ GOOD("(event=abc " binop1 " event=def) " binop2 " event=ghi", \
+ "(((event=\"abc\" " binop1 " event=\"def\") " binop2 " event=\"ghi\"))"), \
+ GOOD("event=abc " binop1 " (event=def " binop2 " event=ghi)", \
+ "((event=\"abc\" " binop1 " (event=\"def\" " binop2 " event=\"ghi\")))")
+
+ CHECK("AND", "AND"),
+ CHECK("AND", "OR"),
+ CHECK("OR", "AND"),
+ CHECK("OR", "OR"),
+#undef CHECK
+
+ /* check operator precedence */
+#define CMP(x) "event=\"" #x "\""
+#define CHECK(binop1, binop2) \
+ GOOD(CMP(1) " " binop1 " " CMP(2) " " binop2 " " CMP(3), \
+ "(((" CMP(1) " " binop1 " " CMP(2) ") " binop2 " " CMP(3) "))")
+
+ CHECK("AND", "AND"),
+ CHECK("AND", "OR"),
+ CHECK("OR", "AND"),
+ CHECK("OR", "OR"),
+#undef CHECK
+#undef CMP
+};
+
+static void testcase(const char *name, const char *input, const char *exp,
+ bool fails)
+{
+ struct event_filter *filter;
+ const char *error;
+ int ret;
+
+ filter = event_filter_create();
+ ret = event_filter_parse(input, filter, &error);
+
+ test_out_quiet(name != NULL ? name : "filter parser",
+ (ret != 0) == fails);
+
+ if (ret == 0) {
+ string_t *tmp = t_str_new(128);
+
+ event_filter_export(filter, tmp);
+
+ test_out_quiet(t_strdup_printf("input: %s", input),
+ strcmp(exp, str_c(tmp)) == 0);
+ } else {
+ test_out_quiet(t_strdup_printf("input: %s", input),
+ str_begins(error, exp));
+ }
+
+ event_filter_unref(&filter);
+}
+
+static void test_event_filter_parser_table(void)
+{
+ unsigned int i;
+
+ test_begin("event filter parser: table");
+ for (i = 0; i < N_ELEMENTS(tests); i++) T_BEGIN {
+ testcase(NULL, tests[i].input,
+ tests[i].output,
+ tests[i].fails);
+ } T_END;
+ test_end();
+}
+
+static void test_event_filter_parser_categories(void)
+{
+ static const char *cat_names[] = {
+ "debug", "info", "warning", "error", "fatal", "panic",
+ };
+ unsigned int i;
+
+ test_begin("event filter parser: log type category");
+ for (i = 0; i < N_ELEMENTS(cat_names); i++) T_BEGIN {
+ string_t *str = t_str_new(128);
+
+ str_append(str, "(category=");
+ str_append(str, cat_names[i]);
+ str_append(str, ")");
+
+ testcase(NULL, str_c(str), str_c(str), FALSE);
+ } T_END;
+ test_end();
+}
+
+static void
+test_event_filter_parser_simple_nesting_helper(bool not1, bool not2,
+ bool and, const char *sp,
+ bool sp1, bool sp2,
+ bool sp3, bool sp4)
+{
+ const char *op = and ? "AND" : "OR";
+ const char *expr1 = "event=\"abc\"";
+ const char *expr2 = "event=\"def\"";
+ const char *in;
+ const char *exp;
+
+ in = t_strdup_printf("%s(%s%s)%s%s%s(%s%s)%s",
+ sp1 ? sp : "",
+ not1 ? "NOT " : "",
+ expr1,
+ sp2 ? sp : "",
+ op,
+ sp3 ? sp : "",
+ not2 ? "NOT " : "",
+ expr2,
+ sp4 ? sp : "");
+
+ exp = t_strdup_printf("((%s%s%s %s %s%s%s))",
+ not1 ? "(NOT " : "",
+ expr1,
+ not1 ? ")" : "",
+ op,
+ not2 ? "(NOT " : "",
+ expr2,
+ not2 ? ")" : "");
+
+ testcase(NULL, in, exp, FALSE);
+}
+
+static void test_event_filter_parser_simple_nesting(void)
+{
+ const char *whitespace[] = {
+ "",
+ "\t",
+ "\n",
+ "\r",
+ " ",
+ };
+ unsigned int i;
+ unsigned int loc;
+ unsigned int not;
+
+ test_begin("event filter parser: simple nesting");
+ for (i = 0; i < N_ELEMENTS(whitespace); i++) {
+ for (not = 0; not < 4; not++) {
+ const bool not1 = (not & 0x2) != 0;
+ const bool not2 = (not & 0x1) != 0;
+
+ for (loc = 0; loc < 16; loc++) T_BEGIN {
+ const bool sp1 = (loc & 0x8) != 0;
+ const bool sp2 = (loc & 0x4) != 0;
+ const bool sp3 = (loc & 0x2) != 0;
+ const bool sp4 = (loc & 0x1) != 0;
+
+ test_event_filter_parser_simple_nesting_helper(not1, not2,
+ TRUE,
+ whitespace[i],
+ sp1, sp2,
+ sp3, sp4);
+ test_event_filter_parser_simple_nesting_helper(not1, not2,
+ FALSE,
+ whitespace[i],
+ sp1, sp2,
+ sp3, sp4);
+ } T_END;
+ }
+ }
+ test_end();
+}
+
+/*
+ * Test '<key><op><value>' with each possible operator and each possible
+ * quoting of <key> and <value>. Some quotings are not allowed. The keyq
+ * and valueq arguments specify whether the <key> and <value> strings
+ * should be quoted. key_special indicates that the key is *not* a field
+ * and therefore only the = operator should parse successfully.
+ */
+static void generated_single_comparison(const char *name,
+ bool parens,
+ const char *key,
+ enum quoting keyq,
+ bool key_special,
+ const char *value_in,
+ const char *value_exp,
+ enum quoting valueq)
+{
+ unsigned int c, q;
+ bool should_fail;
+
+ for (c = 0; c < N_ELEMENTS(comparators); c++) {
+ string_t *output = t_str_new(128);
+
+ if (key_special && (strcmp(comparators[c], "=") != 0)) {
+ /* the key is a not a field, only = is allowed */
+ str_append(output, "event filter: Only fields support inequality comparisons");
+ should_fail = TRUE;
+ } else {
+ /* the key is a field, all comparators are allowed */
+ str_append_c(output, '(');
+ if (keyq != QUOTE_MUST_NOT)
+ str_append_c(output, '"');
+ str_append(output, key);
+ if (keyq != QUOTE_MUST_NOT)
+ str_append_c(output, '"');
+ str_append(output, comparators[c]);
+ str_append_c(output, '"');
+ str_append_escaped(output, value_exp, strlen(value_exp));
+ str_append_c(output, '"');
+ str_append_c(output, ')');
+ should_fail = FALSE;
+ }
+
+ for (q = 0; q < 4; q++) {
+ const bool qkey = (q & 1) == 1;
+ const bool qval = (q & 2) == 2;
+ string_t *input = t_str_new(128);
+
+ if ((!qkey && (keyq == QUOTE_MUST)) ||
+ (qkey && (keyq == QUOTE_MUST_NOT)))
+ continue;
+ if ((!qval && (valueq == QUOTE_MUST)) ||
+ (qval && (valueq == QUOTE_MUST_NOT)))
+ continue;
+
+ if (parens)
+ str_append_c(input, '(');
+ if (qkey)
+ str_append_c(input, '"');
+ str_append(input, key);
+ if (qkey)
+ str_append_c(input, '"');
+ str_append(input, comparators[c]);
+ if (qval) {
+ str_append_c(input, '"');
+ str_append_escaped(input, value_in, strlen(value_in));
+ str_append_c(input, '"');
+ } else {
+ str_append(input, value_in);
+ }
+ if (parens)
+ str_append_c(input, ')');
+
+ testcase(name,
+ str_c(input),
+ str_c(output),
+ should_fail);
+ }
+ }
+}
+
+static void test_event_filter_parser_generated(bool parens)
+{
+ unsigned int w, v;
+
+ test_begin(t_strdup_printf("event filter parser: parser generated parens=%s",
+ parens ? "yes" : "no"));
+ /* check that non-field keys work */
+ for (w = 0; w < N_ELEMENTS(what_special); w++) {
+ for (v = 0; v < N_ELEMENTS(values_single); v++)
+ generated_single_comparison("non-field/single",
+ parens,
+ what_special[w],
+ QUOTE_MUST_NOT,
+ TRUE,
+ values_single[v],
+ values_single[v],
+ QUOTE_MAY);
+
+ for (v = 0; v < N_ELEMENTS(values_multi); v++)
+ generated_single_comparison("non-field/multi",
+ parens,
+ what_special[w],
+ QUOTE_MUST_NOT,
+ TRUE,
+ values_multi[v],
+ values_multi[v],
+ QUOTE_MUST);
+
+ for (v = 0; v < N_ELEMENTS(values_oper); v++) {
+ generated_single_comparison("non-field/bool-op",
+ parens,
+ what_special[w],
+ QUOTE_MUST_NOT,
+ TRUE,
+ values_oper[v].in,
+ values_oper[v].out_unquoted,
+ QUOTE_MUST_NOT);
+ generated_single_comparison("non-field/bool-op",
+ parens,
+ what_special[w],
+ QUOTE_MUST_NOT,
+ TRUE,
+ values_oper[v].in,
+ values_oper[v].out_quoted,
+ QUOTE_MUST);
+ }
+ }
+
+ /* check that field keys work */
+ for (w = 0; w < N_ELEMENTS(what_fields_single); w++) {
+ for (v = 0; v < N_ELEMENTS(values_single); v++)
+ generated_single_comparison("field/single",
+ parens,
+ what_fields_single[w],
+ QUOTE_MAY,
+ FALSE,
+ values_single[v],
+ values_single[v],
+ QUOTE_MAY);
+
+ for (v = 0; v < N_ELEMENTS(values_multi); v++)
+ generated_single_comparison("field/multi",
+ parens,
+ what_fields_single[w],
+ QUOTE_MAY,
+ FALSE,
+ values_multi[v],
+ values_multi[v],
+ QUOTE_MUST);
+
+ for (v = 0; v < N_ELEMENTS(values_oper); v++) {
+ generated_single_comparison("field/bool-op",
+ parens,
+ what_fields_single[w],
+ QUOTE_MAY,
+ FALSE,
+ values_oper[v].in,
+ values_oper[v].out_unquoted,
+ QUOTE_MUST_NOT);
+ generated_single_comparison("field/bool-op",
+ parens,
+ what_fields_single[w],
+ QUOTE_MAY,
+ FALSE,
+ values_oper[v].in,
+ values_oper[v].out_quoted,
+ QUOTE_MUST);
+ }
+ }
+ test_end();
+}
+
+static void test_event_filter_parser_simple_invalid(void)
+{
+ test_begin("event filter parser: simple invalid");
+ testcase(NULL, "a=b=c", "", TRUE);
+ test_end();
+}
+
+void test_event_filter_parser(void)
+{
+ test_event_filter_parser_table();
+ test_event_filter_parser_categories();
+ test_event_filter_parser_simple_nesting();
+ test_event_filter_parser_generated(FALSE);
+ test_event_filter_parser_generated(TRUE);
+ test_event_filter_parser_simple_invalid();
+}
diff --git a/src/lib/test-event-filter.c b/src/lib/test-event-filter.c
new file mode 100644
index 0000000..8e1e130
--- /dev/null
+++ b/src/lib/test-event-filter.c
@@ -0,0 +1,598 @@
+/* Copyright (c) 2018 Dovecot authors, see the included COPYING file */
+
+#include "test-lib.h"
+#include "ioloop.h"
+#include "event-filter-private.h"
+
+static void test_event_filter_override_parent_fields(void)
+{
+ struct event_filter *filter;
+ const char *error;
+ const struct failure_context failure_ctx = {
+ .type = LOG_TYPE_DEBUG
+ };
+
+ test_begin("event filter: override parent fields");
+
+ struct event *parent = event_create(NULL);
+ event_add_str(parent, "str", "parent_str");
+ event_add_str(parent, "parent_str", "parent_str");
+ event_add_int(parent, "int1", 0);
+ event_add_int(parent, "int2", 5);
+ event_add_int(parent, "parent_int", 6);
+
+ struct event *child = event_create(parent);
+ event_add_str(child, "str", "child_str");
+ event_add_str(child, "child_str", "child_str");
+ event_add_int(child, "int1", 6);
+ event_add_int(child, "int2", 0);
+ event_add_int(child, "child_int", 8);
+
+ /* parent matches: test a mix of parent/child fields */
+ filter = event_filter_create();
+ test_assert(event_filter_parse("str=parent_str AND int1=0 AND int2=5", filter, &error) == 0);
+ test_assert(event_filter_match(filter, parent, &failure_ctx));
+ test_assert(!event_filter_match(filter, child, &failure_ctx));
+ event_filter_unref(&filter);
+
+ /* parent matches: test fields that exist only in parent */
+ filter = event_filter_create();
+ test_assert(event_filter_parse("parent_str=parent_str AND parent_int=6", filter, &error) == 0);
+ test_assert(event_filter_match(filter, parent, &failure_ctx));
+ test_assert(event_filter_match(filter, child, &failure_ctx));
+ event_filter_unref(&filter);
+
+ /* child matches: test a mix of parent/child fields */
+ filter = event_filter_create();
+ test_assert(event_filter_parse("str=child_str AND int1=6 AND int2=0", filter, &error) == 0);
+ test_assert(event_filter_match(filter, child, &failure_ctx));
+ test_assert(!event_filter_match(filter, parent, &failure_ctx));
+ event_filter_unref(&filter);
+
+ /* child matches: test fields that exist only in child */
+ filter = event_filter_create();
+ test_assert(event_filter_parse("child_str=child_str AND child_int=8", filter, &error) == 0);
+ test_assert(event_filter_match(filter, child, &failure_ctx));
+ test_assert(!event_filter_match(filter, parent, &failure_ctx));
+ event_filter_unref(&filter);
+
+ event_unref(&parent);
+ event_unref(&child);
+ test_end();
+}
+
+static void test_event_filter_override_global_fields(void)
+{
+ struct event_filter *filter;
+ const char *error;
+ const struct failure_context failure_ctx = {
+ .type = LOG_TYPE_DEBUG
+ };
+
+ test_begin("event filter: override global fields");
+
+ struct event *global = event_create(NULL);
+ event_add_str(global, "str", "global_str");
+ event_add_str(global, "global_str", "global_str");
+ event_add_int(global, "int1", 0);
+ event_add_int(global, "int2", 5);
+ event_add_int(global, "global_int", 6);
+ event_push_global(global);
+
+ struct event *local = event_create(NULL);
+ event_add_str(local, "str", "local_str");
+ event_add_str(local, "local_str", "local_str");
+ event_add_int(local, "int1", 6);
+ event_add_int(local, "int2", 0);
+ event_add_int(local, "local_int", 8);
+
+ /* global matches: test a mix of global/local fields */
+ filter = event_filter_create();
+ test_assert(event_filter_parse("str=global_str AND int1=0 AND int2=5", filter, &error) == 0);
+ test_assert(event_filter_match(filter, global, &failure_ctx));
+ test_assert(!event_filter_match(filter, local, &failure_ctx));
+ event_filter_unref(&filter);
+
+ /* global matches: test fields that exist only in global */
+ filter = event_filter_create();
+ test_assert(event_filter_parse("global_str=global_str AND global_int=6", filter, &error) == 0);
+ test_assert(event_filter_match(filter, global, &failure_ctx));
+ test_assert(event_filter_match(filter, local, &failure_ctx));
+ event_filter_unref(&filter);
+
+ /* local matches: test a mix of global/local fields */
+ filter = event_filter_create();
+ test_assert(event_filter_parse("str=local_str AND int1=6 AND int2=0", filter, &error) == 0);
+ test_assert(event_filter_match(filter, local, &failure_ctx));
+ test_assert(!event_filter_match(filter, global, &failure_ctx));
+ event_filter_unref(&filter);
+
+ /* local matches: test fields that exist only in local */
+ filter = event_filter_create();
+ test_assert(event_filter_parse("local_str=local_str AND local_int=8", filter, &error) == 0);
+ test_assert(event_filter_match(filter, local, &failure_ctx));
+ test_assert(!event_filter_match(filter, global, &failure_ctx));
+ event_filter_unref(&filter);
+
+ event_pop_global(global);
+ event_unref(&global);
+ event_unref(&local);
+ test_end();
+}
+
+static void test_event_filter_clear_parent_fields(void)
+{
+ struct event_filter *filter;
+ const char *error;
+ const struct failure_context failure_ctx = {
+ .type = LOG_TYPE_DEBUG
+ };
+ const char *keys[] = { "str", "int" };
+
+ test_begin("event filter: clear parent fields");
+
+ struct event *parent = event_create(NULL);
+ event_add_str(parent, "str", "parent_str");
+ event_add_int(parent, "int", 0);
+
+ struct event *child = event_create(parent);
+ event_field_clear(child, "str");
+ event_field_clear(child, "int");
+
+ for (unsigned int i = 0; i < N_ELEMENTS(keys); i++) {
+ /* match any value */
+ const char *query = t_strdup_printf("%s=*", keys[i]);
+ filter = event_filter_create();
+ test_assert(event_filter_parse(query, filter, &error) == 0);
+
+ test_assert_idx(event_filter_match(filter, parent, &failure_ctx), i);
+ test_assert_idx(!event_filter_match(filter, child, &failure_ctx), i);
+ event_filter_unref(&filter);
+ }
+
+ /* match empty field */
+ filter = event_filter_create();
+ test_assert(event_filter_parse("str=\"\"", filter, &error) == 0);
+ test_assert(!event_filter_match(filter, parent, &failure_ctx));
+ test_assert(event_filter_match(filter, child, &failure_ctx));
+ event_filter_unref(&filter);
+
+ /* match nonexistent field */
+ filter = event_filter_create();
+ test_assert(event_filter_parse("nonexistent=\"\"", filter, &error) == 0);
+ test_assert(event_filter_match(filter, parent, &failure_ctx));
+ test_assert(event_filter_match(filter, child, &failure_ctx));
+ event_filter_unref(&filter);
+
+ event_unref(&parent);
+ event_unref(&child);
+ test_end();
+}
+
+static void test_event_filter_clear_global_fields(void)
+{
+ struct event_filter *filter;
+ const char *error;
+ const struct failure_context failure_ctx = {
+ .type = LOG_TYPE_DEBUG
+ };
+ const char *keys[] = { "str", "int" };
+
+ test_begin("event filter: clear global fields");
+
+ struct event *global = event_create(NULL);
+ event_add_str(global, "str", "global_str");
+ event_add_int(global, "int", 0);
+ event_push_global(global);
+
+ struct event *local = event_create(NULL);
+ event_field_clear(local, "str");
+ event_field_clear(local, "int");
+
+ for (unsigned int i = 0; i < N_ELEMENTS(keys); i++) {
+ /* match any value */
+ const char *query = t_strdup_printf("%s=*", keys[i]);
+ filter = event_filter_create();
+ test_assert(event_filter_parse(query, filter, &error) == 0);
+
+ test_assert_idx(event_filter_match(filter, global, &failure_ctx), i);
+ test_assert_idx(!event_filter_match(filter, local, &failure_ctx), i);
+ event_filter_unref(&filter);
+ }
+
+ /* match empty field */
+ filter = event_filter_create();
+ test_assert(event_filter_parse("str=\"\"", filter, &error) == 0);
+ test_assert(!event_filter_match(filter, global, &failure_ctx));
+ test_assert(event_filter_match(filter, local, &failure_ctx));
+ event_filter_unref(&filter);
+
+ /* match nonexistent field */
+ filter = event_filter_create();
+ test_assert(event_filter_parse("nonexistent=\"\"", filter, &error) == 0);
+ test_assert(event_filter_match(filter, global, &failure_ctx));
+ test_assert(event_filter_match(filter, local, &failure_ctx));
+ event_filter_unref(&filter);
+
+ event_pop_global(global);
+ event_unref(&global);
+ event_unref(&local);
+ test_end();
+}
+
+static void test_event_filter_inc_int(void)
+{
+ struct event_filter *filter;
+ const char *error;
+ const struct failure_context failure_ctx = {
+ .type = LOG_TYPE_DEBUG
+ };
+
+ test_begin("event filter: create and update keys with event_inc_int");
+
+ struct event *root = event_create(NULL);
+
+ filter = event_filter_create();
+ test_assert(event_filter_parse("int=14", filter, &error) == 0);
+
+ const struct event_field *f = event_find_field_recursive(root, "int");
+ i_assert(f == NULL);
+ test_assert(!event_filter_match(filter, root, &failure_ctx));
+
+ event_inc_int(root, "int", 7);
+ test_assert(!event_filter_match(filter, root, &failure_ctx));
+ f = event_find_field_recursive(root, "int");
+ i_assert(f != NULL);
+ test_assert_strcmp(f->key, "int");
+ test_assert(f->value_type == EVENT_FIELD_VALUE_TYPE_INTMAX);
+ test_assert(f->value.intmax == 7);
+
+ event_inc_int(root, "int", 7);
+ test_assert(event_filter_match(filter, root, &failure_ctx));
+ f = event_find_field_recursive(root, "int");
+ i_assert(f != NULL);
+ test_assert_strcmp(f->key, "int");
+ test_assert(f->value_type == EVENT_FIELD_VALUE_TYPE_INTMAX);
+ test_assert(f->value.intmax == 14);
+
+ event_filter_unref(&filter);
+ event_unref(&root);
+ test_end();
+}
+
+static void test_event_filter_parent_category_match(void)
+{
+ static struct event_category parent_category = {
+ .name = "parent",
+ };
+ static struct event_category child_category = {
+ .parent = &parent_category,
+ .name = "child",
+ };
+ struct event_filter *filter;
+ const char *error;
+ const struct failure_context failure_ctx = {
+ .type = LOG_TYPE_DEBUG
+ };
+
+ test_begin("event filter: parent category match");
+
+ struct event *e = event_create(NULL);
+ event_add_category(e, &child_category);
+
+ filter = event_filter_create();
+ test_assert(event_filter_parse("category=parent", filter, &error) == 0);
+
+ test_assert(event_filter_match(filter, e, &failure_ctx));
+
+ event_filter_unref(&filter);
+ event_unref(&e);
+ test_end();
+}
+
+static void test_event_filter_strlist(void)
+{
+ struct event_filter *filter;
+ const struct failure_context failure_ctx = {
+ .type = LOG_TYPE_DEBUG
+ };
+
+ test_begin("event filter: match string list");
+
+ struct event *e = event_create(NULL);
+
+ filter = event_filter_create();
+ /* should match empty list */
+ event_filter_parse("abc=\"\"", filter, NULL);
+ test_assert(event_filter_match(filter, e, &failure_ctx));
+ /* should still be empty */
+ event_strlist_append(e, "abc", NULL);
+ test_assert(event_filter_match(filter, e, &failure_ctx));
+
+ /* should not match non-empty list */
+ event_strlist_append(e, "abc", "one");
+ test_assert(!event_filter_match(filter, e, &failure_ctx));
+ event_filter_unref(&filter);
+
+ /* should match non-empty list that has value 'one' */
+ filter = event_filter_create();
+ event_strlist_append(e, "abc", "two");
+ event_filter_parse("abc=one", filter, NULL);
+ test_assert(event_filter_match(filter, e, &failure_ctx));
+ event_filter_unref(&filter);
+
+ /* should match non-empty list that has no value 'three' */
+ filter = event_filter_create();
+ event_filter_parse("abc=one AND NOT abc=three", filter, NULL);
+ test_assert(event_filter_match(filter, e, &failure_ctx));
+ event_filter_unref(&filter);
+
+ event_unref(&e);
+ test_end();
+}
+
+static void test_event_filter_strlist_recursive(void)
+{
+ struct event_filter *filter;
+ const struct failure_context failure_ctx = {
+ .type = LOG_TYPE_DEBUG
+ };
+
+ test_begin("event filter: match string list - recursive");
+
+ struct event *parent = event_create(NULL);
+ struct event *e = event_create(parent);
+
+ /* empty filter: parent is non-empty */
+ filter = event_filter_create();
+ event_filter_parse("list1=\"\"", filter, NULL);
+ test_assert(event_filter_match(filter, e, &failure_ctx));
+ event_strlist_append(parent, "list1", "foo");
+ test_assert(!event_filter_match(filter, e, &failure_ctx));
+ event_filter_unref(&filter);
+
+ /* matching filter: matches parent */
+ filter = event_filter_create();
+ event_filter_parse("list2=parent", filter, NULL);
+ /* empty: */
+ test_assert(!event_filter_match(filter, e, &failure_ctx));
+ /* set parent but no child: */
+ event_strlist_append(parent, "list2", "parent");
+ test_assert(event_filter_match(filter, e, &failure_ctx));
+ /* set child to non-matching: */
+ event_strlist_append(e, "list2", "child");
+ test_assert(event_filter_match(filter, e, &failure_ctx));
+ event_filter_unref(&filter);
+
+ /* matching filter: matches child */
+ filter = event_filter_create();
+ event_filter_parse("list3=child", filter, NULL);
+ /* empty: */
+ test_assert(!event_filter_match(filter, e, &failure_ctx));
+ /* set child but no parent: */
+ event_strlist_append(e, "list3", "child");
+ test_assert(event_filter_match(filter, e, &failure_ctx));
+ /* set parent to non-matching: */
+ event_strlist_append(e, "list3", "parent");
+ test_assert(event_filter_match(filter, e, &failure_ctx));
+ event_filter_unref(&filter);
+
+ event_unref(&e);
+ event_unref(&parent);
+ test_end();
+}
+
+static void test_event_filter_strlist_global_events(void)
+{
+ struct event_filter *filter;
+ const struct failure_context failure_ctx = {
+ .type = LOG_TYPE_DEBUG
+ };
+
+ test_begin("event filter: match string list - global events");
+
+ struct event *global = event_create(NULL);
+ event_push_global(global);
+
+ struct event *e = event_create(NULL);
+
+ /* empty filter: global is non-empty */
+ filter = event_filter_create();
+ event_filter_parse("list1=\"\"", filter, NULL);
+ test_assert(event_filter_match(filter, e, &failure_ctx));
+ event_strlist_append(global, "list1", "foo");
+ test_assert(!event_filter_match(filter, e, &failure_ctx));
+ event_filter_unref(&filter);
+
+ /* matching filter: matches global */
+ filter = event_filter_create();
+ event_filter_parse("list2=global", filter, NULL);
+ /* empty: */
+ test_assert(!event_filter_match(filter, e, &failure_ctx));
+ /* set global but no local: */
+ event_strlist_append(global, "list2", "global");
+ test_assert(event_filter_match(filter, e, &failure_ctx));
+ /* set local to non-matching: */
+ event_strlist_append(e, "list2", "local");
+ test_assert(event_filter_match(filter, e, &failure_ctx));
+ event_filter_unref(&filter);
+
+ /* matching filter: matches local */
+ filter = event_filter_create();
+ event_filter_parse("list3=local", filter, NULL);
+ /* empty: */
+ test_assert(!event_filter_match(filter, e, &failure_ctx));
+ /* set local but no global: */
+ event_strlist_append(e, "list3", "local");
+ test_assert(event_filter_match(filter, e, &failure_ctx));
+ /* set global to non-matching: */
+ event_strlist_append(e, "list3", "global");
+ test_assert(event_filter_match(filter, e, &failure_ctx));
+ event_filter_unref(&filter);
+
+ event_unref(&e);
+ event_pop_global(global);
+ event_unref(&global);
+ test_end();
+}
+
+static void test_event_filter_named_and_str(void)
+{
+ struct event_filter *filter;
+ const char *error;
+ const struct failure_context failure_ctx = {
+ .type = LOG_TYPE_DEBUG
+ };
+
+ test_begin("event filter: event name and str");
+
+ filter = event_filter_create();
+ struct event *e_noname_nostr = event_create(NULL);
+ struct event *e_noname_str = event_create(NULL);
+ event_add_str(e_noname_str, "str", "str");
+ struct event *e_noname_wrongstr = event_create(NULL);
+ event_add_str(e_noname_wrongstr, "str", "wrong");
+ struct event *e_named_nostr = event_create(NULL);
+ event_set_name(e_named_nostr, "named");
+ struct event *e_named_str = event_create(NULL);
+ event_set_name(e_named_str, "named");
+ event_add_str(e_named_str, "str", "str");
+ struct event *e_named_wrongstr = event_create(NULL);
+ event_set_name(e_named_wrongstr, "named");
+ event_add_str(e_named_wrongstr, "str", "wrong");
+ struct event *e_wrongname_nostr = event_create(NULL);
+ event_set_name(e_wrongname_nostr, "wrong");
+ struct event *e_wrongname_str = event_create(NULL);
+ event_set_name(e_wrongname_str, "wrong");
+ event_add_str(e_wrongname_str, "str", "str");
+ struct event *e_wrongname_wrongstr = event_create(NULL);
+ event_set_name(e_wrongname_wrongstr, "wrong");
+ event_add_str(e_wrongname_wrongstr, "str", "wrong");
+
+ test_assert(event_filter_parse("event=named AND str=str", filter, &error) == 0);
+ test_assert(filter->named_queries_only);
+ test_assert(!event_filter_match(filter, e_noname_nostr, &failure_ctx));
+ test_assert(!event_filter_match(filter, e_noname_str, &failure_ctx));
+ test_assert(!event_filter_match(filter, e_noname_wrongstr, &failure_ctx));
+ test_assert(!event_filter_match(filter, e_named_nostr, &failure_ctx));
+ test_assert(event_filter_match(filter, e_named_str, &failure_ctx));
+ test_assert(!event_filter_match(filter, e_named_wrongstr, &failure_ctx));
+ test_assert(!event_filter_match(filter, e_wrongname_nostr, &failure_ctx));
+ test_assert(!event_filter_match(filter, e_wrongname_str, &failure_ctx));
+ test_assert(!event_filter_match(filter, e_wrongname_wrongstr, &failure_ctx));
+
+ event_filter_unref(&filter);
+ event_unref(&e_noname_nostr);
+ event_unref(&e_noname_str);
+ event_unref(&e_noname_wrongstr);
+ event_unref(&e_named_nostr);
+ event_unref(&e_named_str);
+ event_unref(&e_named_wrongstr);
+ event_unref(&e_wrongname_nostr);
+ event_unref(&e_wrongname_str);
+ event_unref(&e_wrongname_wrongstr);
+ test_end();
+}
+
+static void test_event_filter_named_or_str(void)
+{
+ struct event_filter *filter;
+ const char *error;
+ const struct failure_context failure_ctx = {
+ .type = LOG_TYPE_DEBUG
+ };
+
+ test_begin("event filter: event name or str");
+
+ filter = event_filter_create();
+ struct event *e_noname_nostr = event_create(NULL);
+ struct event *e_noname_str = event_create(NULL);
+ event_add_str(e_noname_str, "str", "str");
+ struct event *e_noname_wrongstr = event_create(NULL);
+ event_add_str(e_noname_wrongstr, "str", "wrong");
+ struct event *e_named_nostr = event_create(NULL);
+ event_set_name(e_named_nostr, "named");
+ struct event *e_named_str = event_create(NULL);
+ event_set_name(e_named_str, "named");
+ event_add_str(e_named_str, "str", "str");
+ struct event *e_named_wrongstr = event_create(NULL);
+ event_set_name(e_named_wrongstr, "named");
+ event_add_str(e_named_wrongstr, "str", "wrong");
+ struct event *e_wrongname_nostr = event_create(NULL);
+ event_set_name(e_wrongname_nostr, "wrong");
+ struct event *e_wrongname_str = event_create(NULL);
+ event_set_name(e_wrongname_str, "wrong");
+ event_add_str(e_wrongname_str, "str", "str");
+ struct event *e_wrongname_wrongstr = event_create(NULL);
+ event_set_name(e_wrongname_wrongstr, "wrong");
+ event_add_str(e_wrongname_wrongstr, "str", "wrong");
+
+ test_assert(event_filter_parse("event=named OR str=str", filter, &error) == 0);
+ test_assert(!filter->named_queries_only);
+
+ test_assert(!event_filter_match(filter, e_noname_nostr, &failure_ctx));
+ test_assert(event_filter_match(filter, e_noname_str, &failure_ctx));
+ test_assert(!event_filter_match(filter, e_noname_wrongstr, &failure_ctx));
+ test_assert(event_filter_match(filter, e_named_nostr, &failure_ctx));
+ test_assert(event_filter_match(filter, e_named_str, &failure_ctx));
+ test_assert(event_filter_match(filter, e_named_wrongstr, &failure_ctx));
+ test_assert(!event_filter_match(filter, e_wrongname_nostr, &failure_ctx));
+ test_assert(event_filter_match(filter, e_wrongname_str, &failure_ctx));
+ test_assert(!event_filter_match(filter, e_wrongname_wrongstr, &failure_ctx));
+
+ event_filter_unref(&filter);
+ event_unref(&e_noname_nostr);
+ event_unref(&e_noname_str);
+ event_unref(&e_noname_wrongstr);
+ event_unref(&e_named_nostr);
+ event_unref(&e_named_str);
+ event_unref(&e_named_wrongstr);
+ event_unref(&e_wrongname_nostr);
+ event_unref(&e_wrongname_str);
+ event_unref(&e_wrongname_wrongstr);
+ test_end();
+}
+
+static void test_event_filter_named_separate_from_str(void)
+{
+ struct event_filter *filter;
+ const char *error;
+ const struct failure_context failure_ctx = {
+ .type = LOG_TYPE_DEBUG
+ };
+
+ test_begin("event filter: event name separate from str");
+
+ filter = event_filter_create();
+ struct event *e_named = event_create(NULL);
+ event_set_name(e_named, "named");
+ struct event *e_noname = event_create(NULL);
+ event_add_str(e_noname, "str", "str");
+
+ test_assert(event_filter_parse("event=named", filter, &error) == 0);
+ test_assert(event_filter_parse("str=str", filter, &error) == 0);
+ test_assert(!filter->named_queries_only);
+ test_assert(event_filter_match(filter, e_named, &failure_ctx));
+ test_assert(event_filter_match(filter, e_noname, &failure_ctx));
+
+ event_filter_unref(&filter);
+ event_unref(&e_named);
+ event_unref(&e_noname);
+ test_end();
+}
+
+void test_event_filter(void)
+{
+ test_event_filter_override_parent_fields();
+ test_event_filter_override_global_fields();
+ test_event_filter_clear_parent_fields();
+ test_event_filter_clear_global_fields();
+ test_event_filter_inc_int();
+ test_event_filter_parent_category_match();
+ test_event_filter_strlist();
+ test_event_filter_strlist_recursive();
+ test_event_filter_strlist_global_events();
+ test_event_filter_named_and_str();
+ test_event_filter_named_or_str();
+ test_event_filter_named_separate_from_str();
+}
diff --git a/src/lib/test-event-flatten.c b/src/lib/test-event-flatten.c
new file mode 100644
index 0000000..526366c
--- /dev/null
+++ b/src/lib/test-event-flatten.c
@@ -0,0 +1,391 @@
+/* Copyright (c) 2019 Dovecot authors, see the included COPYING file */
+
+#include "test-lib.h"
+#include "ioloop.h"
+#include "time-util.h"
+#include "lib-event-private.h"
+#include "failures-private.h"
+#include "array.h"
+#include "str.h"
+
+#define CHECK_FLATTEN_SAME(e) \
+ check_event_same(event_flatten(e), (e))
+
+#define CHECK_FLATTEN_DIFF(e, c, nc, f, nf) \
+ check_event_diff(event_flatten(e), (e), \
+ (c), (nc), \
+ (f), (nf))
+
+static struct event_category cats[] = {
+ { .name = "cat0", },
+ { .name = "cat1", },
+};
+
+static void check_event_diff_cats(struct event_category *const *got,
+ unsigned int ngot, const char **exp,
+ unsigned int nexp)
+{
+ unsigned int i;
+
+ test_assert(ngot == nexp);
+
+ for (i = 0; i < nexp; i++)
+ test_assert(strcmp(got[i]->name, exp[i]) == 0);
+}
+
+static void check_event_diff_fields(const struct event_field *got, unsigned int ngot,
+ const struct event_field *exp, unsigned int nexp)
+{
+ unsigned int i;
+ const char *got_str;
+
+ test_assert(ngot == nexp);
+
+ for (i = 0; i < nexp; i++) {
+ if (got[i].value_type != exp[i].value_type) {
+ test_assert(FALSE);
+ continue;
+ }
+
+ switch (exp[i].value_type) {
+ case EVENT_FIELD_VALUE_TYPE_STR:
+ test_assert(strcmp(exp[i].value.str,
+ got[i].value.str) == 0);
+ break;
+ case EVENT_FIELD_VALUE_TYPE_INTMAX:
+ test_assert(exp[i].value.intmax == got[i].value.intmax);
+ break;
+ case EVENT_FIELD_VALUE_TYPE_TIMEVAL:
+ test_assert(timeval_cmp(&exp[i].value.timeval,
+ &got[i].value.timeval) == 0);
+ break;
+ case EVENT_FIELD_VALUE_TYPE_STRLIST:
+ got_str = t_array_const_string_join(&got[i].value.strlist, ",");
+ test_assert_strcmp(exp[i].value.str, got_str);
+ break;
+ }
+ }
+}
+
+static void check_event_diff(struct event *e, struct event *orig,
+ const char **expected_cats,
+ unsigned int num_expected_cats,
+ const struct event_field *expected_fields,
+ unsigned int num_expected_fields)
+{
+ struct event_category *const *cats;
+ const struct event_field *fields;
+ unsigned int num_cats;
+ unsigned int num_fields;
+
+ test_assert(e != orig);
+ test_assert(e->parent == NULL);
+
+ /* different pointers implies different ids */
+ test_assert(e->id != orig->id); /* TODO: does this make sense? */
+
+ test_assert(timeval_cmp(&e->tv_created_ioloop, &orig->tv_created_ioloop) == 0);
+ test_assert(timeval_cmp(&e->tv_created, &orig->tv_created) == 0);
+ test_assert(timeval_cmp(&e->tv_last_sent, &orig->tv_last_sent) == 0);
+
+ test_assert(strcmp(e->source_filename, orig->source_filename) == 0);
+ test_assert(e->source_linenum == orig->source_linenum);
+
+ /* FIXME: check sending name? */
+
+ cats = event_get_categories(e, &num_cats);
+ check_event_diff_cats(cats, num_cats,
+ expected_cats, num_expected_cats);
+
+ fields = event_get_fields(e, &num_fields);
+ check_event_diff_fields(fields, num_fields,
+ expected_fields, num_expected_fields);
+
+ event_unref(&e);
+}
+
+static void check_event_same(struct event *e, struct event *orig)
+{
+ test_assert(e == orig);
+
+ /* the pointers are the same; nothing can possibly differ */
+
+ event_unref(&e);
+}
+
+static void test_event_flatten_no_parent(void)
+{
+ struct event *e;
+
+ test_begin("event flatten: no parent");
+
+ e = event_create(NULL);
+
+ CHECK_FLATTEN_SAME(e);
+
+ event_add_int(e, "abc", 4);
+ CHECK_FLATTEN_SAME(e);
+
+ event_add_int(e, "def", 2);
+ CHECK_FLATTEN_SAME(e);
+
+ event_add_str(e, "abc", "foo");
+ CHECK_FLATTEN_SAME(e);
+
+ event_add_category(e, &cats[0]);
+ CHECK_FLATTEN_SAME(e);
+
+ event_unref(&e);
+
+ test_end();
+}
+
+static void test_event_flatten_one_parent(void)
+{
+ static const char *exp_1cat[] = {
+ "cat0",
+ };
+ static const char *exp_2cat[] = {
+ "cat1",
+ "cat0",
+ };
+ static struct event_field exp_int = {
+ .key = "abc",
+ .value_type = EVENT_FIELD_VALUE_TYPE_INTMAX,
+ .value = {
+ .str = NULL,
+ .intmax = 42,
+ .timeval = {0,0},
+ }
+ };
+ static struct event_field exp_2int[2] = {
+ {
+ .key = "abc",
+ .value_type = EVENT_FIELD_VALUE_TYPE_INTMAX,
+ .value = {
+ .intmax = 42,
+ .str = NULL,
+ .timeval = {0,0},
+ }
+ },
+ {
+ .key = "def",
+ .value_type = EVENT_FIELD_VALUE_TYPE_INTMAX,
+ .value = {
+ .intmax = 49,
+ .str = NULL,
+ .timeval = {0,0},
+ }
+ },
+ };
+ static struct event_field exp_1str1int[2] = {
+ {
+ .key = "abc",
+ .value_type = EVENT_FIELD_VALUE_TYPE_STR,
+ .value = {
+ .str = "foo",
+ .intmax = 0,
+ .timeval = {0,0},
+ }
+ },
+ {
+ .key = "def",
+ .value_type = EVENT_FIELD_VALUE_TYPE_INTMAX,
+ .value = {
+ .intmax = 49,
+ .str = NULL,
+ .timeval = {0,0},
+ }
+ },
+ };
+ static struct event_field exp_1str1int1strlist[3] = {
+ {
+ .key = "abc",
+ .value_type = EVENT_FIELD_VALUE_TYPE_STR,
+ .value = {
+ .str = "foo",
+ .intmax = 0,
+ .timeval = {0,0},
+ }
+ },
+ {
+ .key = "def",
+ .value_type = EVENT_FIELD_VALUE_TYPE_INTMAX,
+ .value = {
+ .intmax = 49,
+ .str = NULL,
+ .timeval = {0,0},
+ }
+ },
+ {
+ .key = "cba",
+ .value_type = EVENT_FIELD_VALUE_TYPE_STRLIST,
+ .value = {
+ .str = "one,two,three",
+ },
+ },
+ };
+
+ struct event *parent;
+ struct event *e;
+
+ test_begin("event flatten: one parent");
+
+ t_array_init(&exp_1str1int1strlist[0].value.strlist, 3);
+ const char *str = "one";
+ array_push_back(&exp_1str1int1strlist[0].value.strlist, &str);
+ str = "two";
+ array_push_back(&exp_1str1int1strlist[0].value.strlist, &str);
+ str = "three";
+ array_push_back(&exp_1str1int1strlist[0].value.strlist, &str);
+
+ parent = event_create(NULL);
+
+ e = event_create(parent);
+
+ CHECK_FLATTEN_DIFF(e, NULL, 0, NULL, 0);
+
+ event_add_int(e, "abc", 42);
+ CHECK_FLATTEN_DIFF(e, NULL, 0, &exp_int, 1);
+
+ event_add_int(e, "def", 49);
+ CHECK_FLATTEN_DIFF(e, NULL, 0, exp_2int, 2);
+
+ event_add_str(e, "abc", "foo");
+ CHECK_FLATTEN_DIFF(e, NULL, 0, exp_1str1int, 2);
+
+ event_add_category(e, &cats[0]);
+ CHECK_FLATTEN_DIFF(e, exp_1cat, 1, exp_1str1int, 2);
+
+ event_add_category(e, &cats[1]);
+ CHECK_FLATTEN_DIFF(e, exp_2cat, 2, exp_1str1int, 2);
+
+ event_strlist_append(e, "cba", "one");
+ event_strlist_append(e, "cba", "two");
+ event_strlist_append(e, "cba", "three");
+ CHECK_FLATTEN_DIFF(e, exp_2cat, 2, exp_1str1int1strlist, 3);
+
+ event_unref(&e);
+ event_unref(&parent);
+
+ test_end();
+}
+
+static void test_event_flatten_override_parent_field(void)
+{
+ static struct event_field exp_int = {
+ .key = "abc",
+ .value_type = EVENT_FIELD_VALUE_TYPE_INTMAX,
+ .value = {
+ .intmax = 42,
+ .str = NULL,
+ .timeval = {0,0},
+ }
+ };
+ static struct event_field exp_str = {
+ .key = "abc",
+ .value_type = EVENT_FIELD_VALUE_TYPE_STR,
+ .value = {
+ .str = "def",
+ .intmax = 0,
+ .timeval = {0,0},
+ }
+ };
+ static struct event_field exp_2str[2] = {
+ {
+ .key = "abc",
+ .value_type = EVENT_FIELD_VALUE_TYPE_STR,
+ .value = {
+ .str = "def",
+ .intmax = 0,
+ .timeval = {0,0},
+ }
+ },
+ {
+ .key = "foo",
+ .value_type = EVENT_FIELD_VALUE_TYPE_STR,
+ .value = {
+ .str = "bar",
+ .intmax = 0,
+ .timeval = {0,0},
+ }
+ },
+ };
+ struct event *parent;
+ struct event *e;
+
+ test_begin("event flatten: override parent field");
+
+ parent = event_create(NULL);
+
+ event_add_int(parent, "abc", 5);
+
+ e = event_create(parent);
+
+ event_add_int(e, "abc", 42);
+
+ CHECK_FLATTEN_DIFF(e, NULL, 0, &exp_int, 1);
+
+ event_add_str(e, "abc", "def");
+ CHECK_FLATTEN_DIFF(e, NULL, 0, &exp_str, 1);
+
+ event_add_str(parent, "foo", "bar");
+ CHECK_FLATTEN_DIFF(e, NULL, 0, exp_2str, 2);
+
+ event_unref(&e);
+ event_unref(&parent);
+
+ test_end();
+}
+
+static void test_event_strlist_flatten(void)
+{
+ test_begin("event flatten: strlist");
+ struct event *l1 = event_create(NULL);
+ event_strlist_append(l1, "test", "l3");
+ struct event *l2 = event_create(l1);
+ event_strlist_append(l2, "test", "l1");
+ struct event *l3 = event_create(l2);
+ unsigned int line = __LINE__ - 1;
+ event_strlist_append(l3, "test", "l2");
+
+ string_t *dest = t_str_new(32);
+ struct event *event = event_flatten(l3);
+
+ event_export(event, dest);
+ /* see if it matches .. */
+ const char *reference = t_strdup_printf("%"PRIdTIME_T"\t%u"
+ "\ts"__FILE__
+ "\t%u\tLtest\t3\tl3\tl1\tl2",
+ event->tv_created.tv_sec,
+ (unsigned int)event->tv_created.tv_usec,
+ line);
+ test_assert_strcmp(str_c(dest), reference);
+
+ /* these should not end up duplicated */
+ event_strlist_append(event, "test", "l1");
+ event_strlist_append(event, "test", "l2");
+ event_strlist_append(event, "test", "l3");
+
+ /* and export should look the same */
+ str_truncate(dest, 0);
+ event_export(event, dest);
+ test_assert_strcmp(str_c(dest), reference);
+
+ event_unref(&event);
+
+ /* export event */
+ event_unref(&l3);
+ event_unref(&l2);
+ event_unref(&l1);
+
+ test_end();
+}
+
+void test_event_flatten(void)
+{
+ test_event_flatten_no_parent();
+ test_event_flatten_one_parent();
+ test_event_flatten_override_parent_field();
+ test_event_strlist_flatten();
+}
diff --git a/src/lib/test-event-log.c b/src/lib/test-event-log.c
new file mode 100644
index 0000000..286a981
--- /dev/null
+++ b/src/lib/test-event-log.c
@@ -0,0 +1,2528 @@
+/* Copyright (c) 2018 Dovecot authors, see the included COPYING file */
+
+#include "test-lib.h"
+#include "ioloop.h"
+#include "str.h"
+#include "failures-private.h"
+
+#include <unistd.h>
+
+enum test_log_event_type {
+ TYPE_END,
+ TYPE_PREFIX_APPEND,
+ TYPE_PREFIX_REPLACE,
+ TYPE_PREFIX_APPEND_CB,
+ TYPE_PREFIX_REPLACE_CB,
+ TYPE_MESSAGE_AMEND,
+ TYPE_SKIP,
+};
+
+enum test_log_event_flag {
+ FLAG_BASE_EVENT = BIT(0),
+ FLAG_DROP_PREFIXES_1 = BIT(1),
+ FLAG_DROP_PREFIXES_2 = BIT(2),
+ FLAG_DROP_PREFIXES_4 = BIT(3),
+};
+
+enum test_log_flag {
+ FLAG_NO_SEND = BIT(0),
+};
+
+struct test_log_event {
+ enum test_log_event_type type;
+ const char *str;
+ enum test_log_event_flag flags;
+};
+
+struct test_log {
+ const struct test_log_event *prefixes;
+ const char *global_log_prefix;
+ const char *base_send_prefix;
+ const char *base_str_prefix;
+ const char *result;
+ const char *result_str_out;
+ enum test_log_flag flags;
+};
+
+static char *test_output = NULL;
+
+static void ATTR_FORMAT(2, 0)
+info_handler(const struct failure_context *ctx,
+ const char *format, va_list args)
+{
+ size_t prefix_len;
+
+ i_assert(ctx->type == LOG_TYPE_INFO);
+
+ i_free(test_output);
+ T_BEGIN {
+ string_t *str = failure_handler.v->format(ctx, &prefix_len,
+ format, args);
+ test_output = i_strdup(str_c(str));
+ } T_END;
+}
+
+static void ATTR_FORMAT(2, 0)
+error_handler(const struct failure_context *ctx,
+ const char *format, va_list args)
+{
+ size_t prefix_len;
+
+ i_assert(ctx->type == LOG_TYPE_WARNING ||
+ ctx->type == LOG_TYPE_ERROR);
+
+ i_free(test_output);
+ T_BEGIN {
+ string_t *str = failure_handler.v->format(ctx, &prefix_len,
+ format, args);
+ test_output = i_strdup(str_c(str));
+ } T_END;
+}
+
+static const char *
+test_event_log_prefix_cb(char *prefix)
+{
+ return t_strdup_printf("callback(%s)", prefix);
+}
+
+static const char *
+test_event_log_message_cb(char *prefix,
+ enum log_type log_type ATTR_UNUSED,
+ const char *message)
+{
+ return t_strdup_printf("[%s%s]", prefix, message);
+}
+
+static void test_event_log_message(void)
+{
+ struct test_log tests[] = {
+ {
+ .prefixes = (const struct test_log_event []) {
+ { .type = TYPE_END }
+ },
+ .global_log_prefix = "global1.",
+ .result = "global1.Info: TEXT",
+ },
+ {
+ .prefixes = (const struct test_log_event []) {
+ { TYPE_PREFIX_REPLACE, "replaced1,", 0 },
+ { .type = TYPE_END }
+ },
+ .result = "replaced1,Info: TEXT",
+ },
+ {
+ .prefixes = (const struct test_log_event []) {
+ { TYPE_PREFIX_REPLACE, "replaced1,", 0 },
+ { TYPE_PREFIX_REPLACE, "replaced2.", 0 },
+ { .type = TYPE_END }
+ },
+ .result = "replaced2.Info: TEXT",
+ },
+ {
+ .prefixes = (const struct test_log_event []) {
+ { TYPE_PREFIX_REPLACE, "replaced1,", 0 },
+ { TYPE_PREFIX_APPEND, "appended2.", 0 },
+ { .type = TYPE_END }
+ },
+ .result = "replaced1,Info: appended2.TEXT",
+ },
+ {
+ .prefixes = (const struct test_log_event []) {
+ { TYPE_PREFIX_APPEND, "appended1,", 0 },
+ { TYPE_PREFIX_REPLACE, "replaced1,", 0 },
+ { .type = TYPE_END }
+ },
+ .result = "replaced1,Info: TEXT",
+ },
+ {
+ .prefixes = (const struct test_log_event []) {
+ { TYPE_PREFIX_APPEND, "appended1,", 0 },
+ { .type = TYPE_END }
+ },
+ .global_log_prefix = "global2.",
+ .result = "global2.Info: appended1,TEXT",
+ },
+ {
+ .prefixes = (const struct test_log_event []) {
+ { TYPE_PREFIX_APPEND, "appended1,", 0 },
+ { TYPE_PREFIX_APPEND, "appended2.", 0 },
+ { .type = TYPE_END }
+ },
+ .global_log_prefix = "global3.",
+ .result = "global3.Info: appended1,appended2.TEXT",
+ },
+ {
+ .prefixes = (const struct test_log_event []) {
+ { TYPE_PREFIX_APPEND, "appended1,", 0 },
+ { TYPE_PREFIX_APPEND, "appended2.", 0 },
+ { TYPE_PREFIX_APPEND, "appended3.", 0 },
+ { .type = TYPE_END }
+ },
+ .global_log_prefix = "global3.",
+ .result = "global3.Info: "
+ "appended1,appended2.appended3.TEXT",
+ },
+ {
+ .prefixes = (const struct test_log_event []) {
+ { TYPE_PREFIX_APPEND, "appended1,", 0 },
+ { TYPE_PREFIX_REPLACE, "replaced2.", 0 },
+ { TYPE_PREFIX_APPEND, "appended3#", 0 },
+ { .type = TYPE_END }
+ },
+ .result = "replaced2.Info: appended3#TEXT",
+ },
+ {
+ .prefixes = (const struct test_log_event []) {
+ { TYPE_PREFIX_APPEND, "appended1,", 0 },
+ { TYPE_PREFIX_REPLACE, "replaced2.", 0 },
+ { TYPE_PREFIX_APPEND, "appended3#", 0 },
+ { TYPE_PREFIX_REPLACE, "replaced4;", 0 },
+ { .type = TYPE_END }
+ },
+ .result = "replaced4;Info: TEXT",
+ },
+ {
+ .prefixes = (const struct test_log_event []) {
+ { TYPE_PREFIX_APPEND, "appended1,", 0 },
+ { TYPE_PREFIX_REPLACE, "replaced2.", 0 },
+ { TYPE_PREFIX_APPEND, "appended3#", 0 },
+ { TYPE_PREFIX_REPLACE, "replaced4;", 0 },
+ { TYPE_PREFIX_APPEND, "appended5-", 0 },
+ { .type = TYPE_END }
+ },
+ .result = "replaced4;Info: appended5-TEXT",
+ },
+ {
+ .prefixes = (const struct test_log_event []) {
+ { TYPE_PREFIX_APPEND_CB, "appended1-", 0 },
+ { .type = TYPE_END }
+ },
+ .global_log_prefix = "global3.",
+ .result = "global3.Info: callback(appended1-)TEXT",
+ },
+ {
+ .prefixes = (const struct test_log_event []) {
+ { TYPE_PREFIX_APPEND, "appended1,", 0 },
+ { TYPE_PREFIX_REPLACE, "replaced1.", 0 },
+ { TYPE_PREFIX_REPLACE_CB, "replaced2-", 0 },
+ { .type = TYPE_END }
+ },
+ .result = "callback(replaced2-)Info: TEXT",
+ },
+ {
+ .prefixes = (const struct test_log_event []) {
+ { TYPE_PREFIX_REPLACE_CB, "replaced1.", 0 },
+ { TYPE_PREFIX_APPEND, "appended1,", 0 },
+ { .type = TYPE_END }
+ },
+ .result = "callback(replaced1.)Info: appended1,TEXT",
+ },
+ {
+ .prefixes = (const struct test_log_event []) {
+ { TYPE_PREFIX_REPLACE_CB, "replaced1.", 0 },
+ { TYPE_PREFIX_REPLACE, "replaced2-", 0 },
+ { .type = TYPE_END }
+ },
+ .result = "replaced2-Info: TEXT",
+ },
+ /* Tests involving event_set_log_message_callback() */
+ {
+ .prefixes = (const struct test_log_event []) {
+ { TYPE_MESSAGE_AMEND, "amended1-" , 0},
+ { .type = TYPE_END }
+ },
+ .global_log_prefix = "global4.",
+ .result = "global4.Info: [amended1-TEXT]",
+ },
+ {
+ .prefixes = (const struct test_log_event []) {
+ { TYPE_MESSAGE_AMEND, "amended1-", 0 },
+ { TYPE_MESSAGE_AMEND, "amended2-", 0 },
+ { .type = TYPE_END }
+ },
+ .global_log_prefix = "global4.",
+ .result = "global4.Info: [amended1-[amended2-TEXT]]",
+ },
+ {
+ .prefixes = (const struct test_log_event []) {
+ { TYPE_MESSAGE_AMEND, "amended1-", 0 },
+ { TYPE_PREFIX_APPEND, "appended1-", 0 },
+ { .type = TYPE_END }
+ },
+ .global_log_prefix = "global4.",
+ .result = "global4.Info: [amended1-appended1-TEXT]",
+ },
+ {
+ .prefixes = (const struct test_log_event []) {
+ { TYPE_PREFIX_APPEND, "appended1-", 0 },
+ { TYPE_MESSAGE_AMEND, "amended1-", 0 },
+ { .type = TYPE_END }
+ },
+ .global_log_prefix = "global4.",
+ .result = "global4.Info: appended1-[amended1-TEXT]",
+ },
+ {
+ .prefixes = (const struct test_log_event []) {
+ { TYPE_PREFIX_APPEND, "appended1-", 0 },
+ { TYPE_MESSAGE_AMEND, "amended1-", 0 },
+ { TYPE_PREFIX_APPEND, "appended2-", 0 },
+ { .type = TYPE_END }
+ },
+ .global_log_prefix = "global4.",
+ .result = "global4.Info: "
+ "appended1-[amended1-appended2-TEXT]",
+ },
+ {
+ .prefixes = (const struct test_log_event []) {
+ { TYPE_MESSAGE_AMEND, "amended1-", 0 },
+ { TYPE_PREFIX_APPEND, "appended1-", 0 },
+ { TYPE_MESSAGE_AMEND, "amended2-", 0 },
+ { TYPE_PREFIX_APPEND, "appended2-", 0 },
+ { .type = TYPE_END }
+ },
+ .global_log_prefix = "global4.",
+ .result = "global4.Info: [amended1-appended1-"
+ "[amended2-appended2-TEXT]]",
+ },
+ {
+ .prefixes = (const struct test_log_event []) {
+ { TYPE_PREFIX_REPLACE, "replaced1,", 0 },
+ { TYPE_MESSAGE_AMEND, "amended1-", 0 },
+ { .type = TYPE_END }
+ },
+ .result = "replaced1,Info: [amended1-TEXT]",
+ },
+ {
+ .prefixes = (const struct test_log_event []) {
+ { TYPE_PREFIX_REPLACE, "replaced1,", 0 },
+ { TYPE_MESSAGE_AMEND, "amended1-", 0 },
+ { TYPE_PREFIX_REPLACE, "replaced2,", 0 },
+ { TYPE_MESSAGE_AMEND, "amended2-", 0 },
+ { .type = TYPE_END }
+ },
+ .result = "replaced2,Info: [amended2-TEXT]",
+ },
+ /* Tests with params->base_str_out != NULL */
+ {
+ .prefixes = (const struct test_log_event []) {
+ { .type = TYPE_END }
+ },
+ .global_log_prefix = "global1.",
+ .result = "global1.Info: TEXT",
+ .result_str_out = "TEXT",
+ },
+ {
+ .prefixes = (const struct test_log_event []) {
+ { TYPE_PREFIX_REPLACE, "replaced1,", 0 },
+ { .type = TYPE_END }
+ },
+ .result = "replaced1,Info: TEXT",
+ .result_str_out = "TEXT",
+ },
+ {
+ .prefixes = (const struct test_log_event []) {
+ { TYPE_PREFIX_REPLACE, "replaced1,",
+ FLAG_BASE_EVENT },
+ { .type = TYPE_END }
+ },
+ .result = "replaced1,Info: TEXT",
+ .result_str_out = "TEXT",
+ },
+ {
+ .prefixes = (const struct test_log_event []) {
+ { TYPE_PREFIX_REPLACE, "replaced1,", 0 },
+ { TYPE_PREFIX_REPLACE, "replaced2.", 0 },
+ { .type = TYPE_END }
+ },
+ .result = "replaced2.Info: TEXT",
+ .result_str_out = "TEXT",
+ },
+ {
+ .prefixes = (const struct test_log_event []) {
+ { TYPE_PREFIX_REPLACE, "replaced1,",
+ FLAG_BASE_EVENT },
+ { TYPE_PREFIX_REPLACE, "replaced2.", 0 },
+ { .type = TYPE_END }
+ },
+ .result = "replaced2.Info: TEXT",
+ .result_str_out = "TEXT",
+ },
+ {
+ .prefixes = (const struct test_log_event []) {
+ { TYPE_PREFIX_REPLACE, "replaced1,", 0 },
+ { TYPE_PREFIX_REPLACE, "replaced2.",
+ FLAG_BASE_EVENT },
+ { .type = TYPE_END }
+ },
+ .result = "replaced2.Info: TEXT",
+ .result_str_out = "TEXT",
+ },
+ {
+ .prefixes = (const struct test_log_event []) {
+ { TYPE_PREFIX_REPLACE, "replaced1,", 0 },
+ { TYPE_PREFIX_APPEND, "appended2.", 0 },
+ { .type = TYPE_END }
+ },
+ .result = "replaced1,Info: appended2.TEXT",
+ .result_str_out = "appended2.TEXT",
+ },
+ {
+ .prefixes = (const struct test_log_event []) {
+ { TYPE_PREFIX_REPLACE, "replaced1,",
+ FLAG_BASE_EVENT },
+ { TYPE_PREFIX_APPEND, "appended2.", 0 },
+ { .type = TYPE_END }
+ },
+ .result = "replaced1,Info: appended2.TEXT",
+ .result_str_out = "appended2.TEXT",
+ },
+ {
+ .prefixes = (const struct test_log_event []) {
+ { TYPE_PREFIX_REPLACE, "replaced1,", 0 },
+ { TYPE_PREFIX_APPEND, "appended2.",
+ FLAG_BASE_EVENT },
+ { .type = TYPE_END }
+ },
+ .result = "replaced1,Info: appended2.TEXT",
+ .result_str_out = "TEXT",
+ },
+ {
+ .prefixes = (const struct test_log_event []) {
+ { TYPE_PREFIX_APPEND, "appended1,", 0 },
+ { TYPE_PREFIX_REPLACE, "replaced1,", 0 },
+ { .type = TYPE_END }
+ },
+ .result = "replaced1,Info: TEXT",
+ .result_str_out = "TEXT",
+ },
+ {
+ .prefixes = (const struct test_log_event []) {
+ { TYPE_PREFIX_APPEND, "appended1,",
+ FLAG_BASE_EVENT },
+ { TYPE_PREFIX_REPLACE, "replaced1,", 0 },
+ { .type = TYPE_END }
+ },
+ .result = "replaced1,Info: TEXT",
+ .result_str_out = "TEXT",
+ },
+ {
+ .prefixes = (const struct test_log_event []) {
+ { TYPE_PREFIX_APPEND, "appended1,", 0 },
+ { TYPE_PREFIX_REPLACE, "replaced1,",
+ FLAG_BASE_EVENT },
+ { .type = TYPE_END }
+ },
+ .result = "replaced1,Info: TEXT",
+ .result_str_out = "TEXT",
+ },
+ {
+ .prefixes = (const struct test_log_event []) {
+ { TYPE_PREFIX_APPEND, "appended1,", 0 },
+ { .type = TYPE_END }
+ },
+ .global_log_prefix = "global2.",
+ .result = "global2.Info: appended1,TEXT",
+ .result_str_out = "appended1,TEXT",
+ },
+ {
+ .prefixes = (const struct test_log_event []) {
+ { TYPE_PREFIX_APPEND, "appended1,",
+ FLAG_BASE_EVENT },
+ { .type = TYPE_END }
+ },
+ .global_log_prefix = "global2.",
+ .result = "global2.Info: appended1,TEXT",
+ .result_str_out = "TEXT",
+ },
+ {
+ .prefixes = (const struct test_log_event []) {
+ { TYPE_PREFIX_APPEND, "appended1,", 0 },
+ { TYPE_PREFIX_APPEND, "appended2.", 0 },
+ { .type = TYPE_END }
+ },
+ .global_log_prefix = "global3.",
+ .result = "global3.Info: appended1,appended2.TEXT",
+ .result_str_out = "appended1,appended2.TEXT",
+ },
+ {
+ .prefixes = (const struct test_log_event []) {
+ { TYPE_PREFIX_APPEND, "appended1,",
+ FLAG_BASE_EVENT },
+ { TYPE_PREFIX_APPEND, "appended2.", 0 },
+ { .type = TYPE_END }
+ },
+ .global_log_prefix = "global3.",
+ .result = "global3.Info: appended1,appended2.TEXT",
+ .result_str_out = "appended2.TEXT",
+ },
+ {
+ .prefixes = (const struct test_log_event []) {
+ { TYPE_PREFIX_APPEND, "appended1,", 0 },
+ { TYPE_PREFIX_APPEND, "appended2.",
+ FLAG_BASE_EVENT },
+ { .type = TYPE_END }
+ },
+ .global_log_prefix = "global3.",
+ .result = "global3.Info: appended1,appended2.TEXT",
+ .result_str_out = "TEXT",
+ },
+ {
+ .prefixes = (const struct test_log_event []) {
+ { TYPE_PREFIX_APPEND, "appended1,", 0 },
+ { TYPE_PREFIX_APPEND, "appended2.", 0 },
+ { TYPE_PREFIX_APPEND, "appended3.", 0 },
+ { .type = TYPE_END }
+ },
+ .global_log_prefix = "global3.",
+ .result = "global3.Info: "
+ "appended1,appended2.appended3.TEXT",
+ .result_str_out = "appended1,appended2.appended3.TEXT",
+ },
+ {
+ .prefixes = (const struct test_log_event []) {
+ { TYPE_PREFIX_APPEND, "appended1,",
+ FLAG_BASE_EVENT },
+ { TYPE_PREFIX_APPEND, "appended2.", 0 },
+ { TYPE_PREFIX_APPEND, "appended3.", 0 },
+ { .type = TYPE_END }
+ },
+ .global_log_prefix = "global3.",
+ .result = "global3.Info: "
+ "appended1,appended2.appended3.TEXT",
+ .result_str_out = "appended2.appended3.TEXT",
+ },
+ {
+ .prefixes = (const struct test_log_event []) {
+ { TYPE_PREFIX_APPEND, "appended1,", 0 },
+ { TYPE_PREFIX_APPEND, "appended2.",
+ FLAG_BASE_EVENT },
+ { TYPE_PREFIX_APPEND, "appended3.", 0 },
+ { .type = TYPE_END }
+ },
+ .global_log_prefix = "global3.",
+ .result = "global3.Info: "
+ "appended1,appended2.appended3.TEXT",
+ .result_str_out = "appended3.TEXT",
+ },
+ {
+ .prefixes = (const struct test_log_event []) {
+ { TYPE_PREFIX_APPEND, "appended1,", 0 },
+ { TYPE_PREFIX_APPEND, "appended2.", 0 },
+ { TYPE_PREFIX_APPEND, "appended3.",
+ FLAG_BASE_EVENT },
+ { .type = TYPE_END }
+ },
+ .global_log_prefix = "global3.",
+ .result = "global3.Info: "
+ "appended1,appended2.appended3.TEXT",
+ .result_str_out = "TEXT",
+ },
+ {
+ .prefixes = (const struct test_log_event []) {
+ { TYPE_PREFIX_APPEND, "appended1,", 0 },
+ { TYPE_PREFIX_REPLACE, "replaced2.", 0 },
+ { TYPE_PREFIX_APPEND, "appended3#", 0 },
+ { .type = TYPE_END }
+ },
+ .result = "replaced2.Info: appended3#TEXT",
+ .result_str_out = "appended3#TEXT",
+ },
+ {
+ .prefixes = (const struct test_log_event []) {
+ { TYPE_PREFIX_APPEND, "appended1,",
+ FLAG_BASE_EVENT },
+ { TYPE_PREFIX_REPLACE, "replaced2.", 0 },
+ { TYPE_PREFIX_APPEND, "appended3#", 0 },
+ { .type = TYPE_END }
+ },
+ .result = "replaced2.Info: appended3#TEXT",
+ .result_str_out = "appended3#TEXT",
+ },
+ {
+ .prefixes = (const struct test_log_event []) {
+ { TYPE_PREFIX_APPEND, "appended1,", 0 },
+ { TYPE_PREFIX_REPLACE, "replaced2.",
+ FLAG_BASE_EVENT },
+ { TYPE_PREFIX_APPEND, "appended3#", 0 },
+ { .type = TYPE_END }
+ },
+ .result = "replaced2.Info: appended3#TEXT",
+ .result_str_out = "appended3#TEXT",
+ },
+ {
+ .prefixes = (const struct test_log_event []) {
+ { TYPE_PREFIX_APPEND, "appended1,", 0 },
+ { TYPE_PREFIX_REPLACE, "replaced2.", 0 },
+ { TYPE_PREFIX_APPEND, "appended3#",
+ FLAG_BASE_EVENT },
+ { .type = TYPE_END }
+ },
+ .result = "replaced2.Info: appended3#TEXT",
+ .result_str_out = "TEXT",
+ },
+ {
+ .prefixes = (const struct test_log_event []) {
+ { TYPE_PREFIX_APPEND, "appended1,", 0 },
+ { TYPE_PREFIX_REPLACE, "replaced2.", 0 },
+ { TYPE_PREFIX_APPEND, "appended3#", 0 },
+ { TYPE_PREFIX_REPLACE, "replaced4;", 0 },
+ { .type = TYPE_END }
+ },
+ .result = "replaced4;Info: TEXT",
+ .result_str_out = "TEXT",
+ },
+ {
+ .prefixes = (const struct test_log_event []) {
+ { TYPE_PREFIX_APPEND, "appended1,",
+ FLAG_BASE_EVENT },
+ { TYPE_PREFIX_REPLACE, "replaced2.", 0 },
+ { TYPE_PREFIX_APPEND, "appended3#", 0 },
+ { TYPE_PREFIX_REPLACE, "replaced4;", 0 },
+ { .type = TYPE_END }
+ },
+ .result = "replaced4;Info: TEXT",
+ .result_str_out = "TEXT",
+ },
+ {
+ .prefixes = (const struct test_log_event []) {
+ { TYPE_PREFIX_APPEND, "appended1,", 0 },
+ { TYPE_PREFIX_REPLACE, "replaced2.",
+ FLAG_BASE_EVENT },
+ { TYPE_PREFIX_APPEND, "appended3#", 0 },
+ { TYPE_PREFIX_REPLACE, "replaced4;", 0 },
+ { .type = TYPE_END }
+ },
+ .result = "replaced4;Info: TEXT",
+ .result_str_out = "TEXT",
+ },
+ {
+ .prefixes = (const struct test_log_event []) {
+ { TYPE_PREFIX_APPEND, "appended1,", 0 },
+ { TYPE_PREFIX_REPLACE, "replaced2.", 0 },
+ { TYPE_PREFIX_APPEND, "appended3#",
+ FLAG_BASE_EVENT },
+ { TYPE_PREFIX_REPLACE, "replaced4;", 0 },
+ { .type = TYPE_END }
+ },
+ .result = "replaced4;Info: TEXT",
+ .result_str_out = "TEXT",
+ },
+ {
+ .prefixes = (const struct test_log_event []) {
+ { TYPE_PREFIX_APPEND, "appended1,", 0 },
+ { TYPE_PREFIX_REPLACE, "replaced2.", 0 },
+ { TYPE_PREFIX_APPEND, "appended3#", 0 },
+ { TYPE_PREFIX_REPLACE, "replaced4;",
+ FLAG_BASE_EVENT },
+ { .type = TYPE_END }
+ },
+ .result = "replaced4;Info: TEXT",
+ .result_str_out = "TEXT",
+ },
+ {
+ .prefixes = (const struct test_log_event []) {
+ { TYPE_PREFIX_APPEND, "appended1,", 0 },
+ { TYPE_PREFIX_REPLACE, "replaced2.", 0 },
+ { TYPE_PREFIX_APPEND, "appended3#", 0 },
+ { TYPE_PREFIX_REPLACE, "replaced4;", 0 },
+ { TYPE_PREFIX_APPEND, "appended5-", 0 },
+ { .type = TYPE_END }
+ },
+ .result = "replaced4;Info: appended5-TEXT",
+ .result_str_out = "appended5-TEXT",
+ },
+ {
+ .prefixes = (const struct test_log_event []) {
+ { TYPE_PREFIX_APPEND, "appended1,",
+ FLAG_BASE_EVENT },
+ { TYPE_PREFIX_REPLACE, "replaced2.", 0 },
+ { TYPE_PREFIX_APPEND, "appended3#", 0 },
+ { TYPE_PREFIX_REPLACE, "replaced4;", 0 },
+ { TYPE_PREFIX_APPEND, "appended5-", 0 },
+ { .type = TYPE_END }
+ },
+ .result = "replaced4;Info: appended5-TEXT",
+ .result_str_out = "appended5-TEXT",
+ },
+ {
+ .prefixes = (const struct test_log_event []) {
+ { TYPE_PREFIX_APPEND, "appended1,", 0 },
+ { TYPE_PREFIX_REPLACE, "replaced2.",
+ FLAG_BASE_EVENT },
+ { TYPE_PREFIX_APPEND, "appended3#", 0 },
+ { TYPE_PREFIX_REPLACE, "replaced4;", 0 },
+ { TYPE_PREFIX_APPEND, "appended5-", 0 },
+ { .type = TYPE_END }
+ },
+ .result = "replaced4;Info: appended5-TEXT",
+ .result_str_out = "appended5-TEXT",
+ },
+ {
+ .prefixes = (const struct test_log_event []) {
+ { TYPE_PREFIX_APPEND, "appended1,", 0 },
+ { TYPE_PREFIX_REPLACE, "replaced2.", 0 },
+ { TYPE_PREFIX_APPEND, "appended3#",
+ FLAG_BASE_EVENT },
+ { TYPE_PREFIX_REPLACE, "replaced4;", 0 },
+ { TYPE_PREFIX_APPEND, "appended5-", 0 },
+ { .type = TYPE_END }
+ },
+ .result = "replaced4;Info: appended5-TEXT",
+ .result_str_out = "appended5-TEXT",
+ },
+ {
+ .prefixes = (const struct test_log_event []) {
+ { TYPE_PREFIX_APPEND, "appended1,", 0 },
+ { TYPE_PREFIX_REPLACE, "replaced2.", 0 },
+ { TYPE_PREFIX_APPEND, "appended3#", 0 },
+ { TYPE_PREFIX_REPLACE, "replaced4;",
+ FLAG_BASE_EVENT },
+ { TYPE_PREFIX_APPEND, "appended5-", 0 },
+ { .type = TYPE_END }
+ },
+ .result = "replaced4;Info: appended5-TEXT",
+ .result_str_out = "appended5-TEXT",
+ },
+ {
+ .prefixes = (const struct test_log_event []) {
+ { TYPE_PREFIX_APPEND, "appended1,", 0 },
+ { TYPE_PREFIX_REPLACE, "replaced2.", 0 },
+ { TYPE_PREFIX_APPEND, "appended3#", 0 },
+ { TYPE_PREFIX_REPLACE, "replaced4;", 0 },
+ { TYPE_PREFIX_APPEND, "appended5-",
+ FLAG_BASE_EVENT },
+ { .type = TYPE_END }
+ },
+ .result = "replaced4;Info: appended5-TEXT",
+ .result_str_out = "TEXT",
+ },
+ {
+ .prefixes = (const struct test_log_event []) {
+ { TYPE_PREFIX_APPEND_CB, "appended1-", 0 },
+ { .type = TYPE_END }
+ },
+ .global_log_prefix = "global3.",
+ .result = "global3.Info: callback(appended1-)TEXT",
+ .result_str_out = "callback(appended1-)TEXT",
+ },
+ {
+ .prefixes = (const struct test_log_event []) {
+ { TYPE_PREFIX_APPEND_CB, "appended1-",
+ FLAG_BASE_EVENT },
+ { .type = TYPE_END }
+ },
+ .global_log_prefix = "global3.",
+ .result = "global3.Info: callback(appended1-)TEXT",
+ .result_str_out = "TEXT",
+ },
+ {
+ .prefixes = (const struct test_log_event []) {
+ { TYPE_PREFIX_APPEND, "appended1,", 0 },
+ { TYPE_PREFIX_REPLACE, "replaced1.", 0 },
+ { TYPE_PREFIX_REPLACE_CB, "replaced2-", 0 },
+ { .type = TYPE_END }
+ },
+ .result = "callback(replaced2-)Info: TEXT",
+ .result_str_out = "TEXT",
+ },
+ {
+ .prefixes = (const struct test_log_event []) {
+ { TYPE_PREFIX_APPEND, "appended1,",
+ FLAG_BASE_EVENT },
+ { TYPE_PREFIX_REPLACE, "replaced1.", 0 },
+ { TYPE_PREFIX_REPLACE_CB, "replaced2-", 0 },
+ { .type = TYPE_END }
+ },
+ .result = "callback(replaced2-)Info: TEXT",
+ .result_str_out = "TEXT",
+ },
+ {
+ .prefixes = (const struct test_log_event []) {
+ { TYPE_PREFIX_APPEND, "appended1,", 0 },
+ { TYPE_PREFIX_REPLACE, "replaced1.",
+ FLAG_BASE_EVENT },
+ { TYPE_PREFIX_REPLACE_CB, "replaced2-", 0 },
+ { .type = TYPE_END }
+ },
+ .result = "callback(replaced2-)Info: TEXT",
+ .result_str_out = "TEXT",
+ },
+ {
+ .prefixes = (const struct test_log_event []) {
+ { TYPE_PREFIX_APPEND, "appended1,", 0 },
+ { TYPE_PREFIX_REPLACE, "replaced1.", 0 },
+ { TYPE_PREFIX_REPLACE_CB, "replaced2-",
+ FLAG_BASE_EVENT },
+ { .type = TYPE_END }
+ },
+ .result = "callback(replaced2-)Info: TEXT",
+ .result_str_out = "TEXT",
+ },
+ {
+ .prefixes = (const struct test_log_event []) {
+ { TYPE_PREFIX_REPLACE_CB, "replaced1.", 0 },
+ { TYPE_PREFIX_APPEND, "appended1,", 0 },
+ { .type = TYPE_END }
+ },
+ .result = "callback(replaced1.)Info: appended1,TEXT",
+ .result_str_out = "appended1,TEXT",
+ },
+ {
+ .prefixes = (const struct test_log_event []) {
+ { TYPE_PREFIX_REPLACE_CB, "replaced1.",
+ FLAG_BASE_EVENT },
+ { TYPE_PREFIX_APPEND, "appended1,", 0 },
+ { .type = TYPE_END }
+ },
+ .result = "callback(replaced1.)Info: appended1,TEXT",
+ .result_str_out = "appended1,TEXT",
+ },
+ {
+ .prefixes = (const struct test_log_event []) {
+ { TYPE_PREFIX_REPLACE_CB, "replaced1.", 0 },
+ { TYPE_PREFIX_APPEND, "appended1,",
+ FLAG_BASE_EVENT },
+ { .type = TYPE_END }
+ },
+ .result = "callback(replaced1.)Info: appended1,TEXT",
+ .result_str_out = "TEXT",
+ },
+ {
+ .prefixes = (const struct test_log_event []) {
+ { TYPE_PREFIX_REPLACE_CB, "replaced1.", 0 },
+ { TYPE_PREFIX_REPLACE, "replaced2-", 0 },
+ { .type = TYPE_END }
+ },
+ .result = "replaced2-Info: TEXT",
+ .result_str_out = "TEXT",
+ },
+ {
+ .prefixes = (const struct test_log_event []) {
+ { TYPE_PREFIX_REPLACE_CB, "replaced1.",
+ FLAG_BASE_EVENT },
+ { TYPE_PREFIX_REPLACE, "replaced2-", 0 },
+ { .type = TYPE_END }
+ },
+ .result = "replaced2-Info: TEXT",
+ .result_str_out = "TEXT",
+ },
+ {
+ .prefixes = (const struct test_log_event []) {
+ { TYPE_PREFIX_REPLACE_CB, "replaced1.", 0 },
+ { TYPE_PREFIX_REPLACE, "replaced2-",
+ FLAG_BASE_EVENT },
+ { .type = TYPE_END }
+ },
+ .result = "replaced2-Info: TEXT",
+ .result_str_out = "TEXT",
+ },
+ {
+ .prefixes = (const struct test_log_event []) {
+ { TYPE_MESSAGE_AMEND, "amended1-" , 0},
+ { .type = TYPE_END }
+ },
+ .global_log_prefix = "global4.",
+ .result = "global4.Info: [amended1-TEXT]",
+ .result_str_out = "[amended1-TEXT]",
+ },
+ {
+ .prefixes = (const struct test_log_event []) {
+ { TYPE_MESSAGE_AMEND, "amended1-" ,
+ FLAG_BASE_EVENT},
+ { .type = TYPE_END }
+ },
+ .global_log_prefix = "global4.",
+ .result = "global4.Info: [amended1-TEXT]",
+ .result_str_out = "TEXT",
+ },
+ {
+ .prefixes = (const struct test_log_event []) {
+ { TYPE_MESSAGE_AMEND, "amended1-", 0 },
+ { TYPE_MESSAGE_AMEND, "amended2-", 0 },
+ { .type = TYPE_END }
+ },
+ .global_log_prefix = "global4.",
+ .result = "global4.Info: [amended1-[amended2-TEXT]]",
+ .result_str_out = "[amended1-[amended2-TEXT]]",
+ },
+ {
+ .prefixes = (const struct test_log_event []) {
+ { TYPE_MESSAGE_AMEND, "amended1-",
+ FLAG_BASE_EVENT },
+ { TYPE_MESSAGE_AMEND, "amended2-", 0 },
+ { .type = TYPE_END }
+ },
+ .global_log_prefix = "global4.",
+ .result = "global4.Info: [amended1-[amended2-TEXT]]",
+ .result_str_out = "[amended2-TEXT]",
+ },
+ {
+ .prefixes = (const struct test_log_event []) {
+ { TYPE_MESSAGE_AMEND, "amended1-", 0 },
+ { TYPE_MESSAGE_AMEND, "amended2-",
+ FLAG_BASE_EVENT },
+ { .type = TYPE_END }
+ },
+ .global_log_prefix = "global4.",
+ .result = "global4.Info: [amended1-[amended2-TEXT]]",
+ .result_str_out = "TEXT",
+ },
+ {
+ .prefixes = (const struct test_log_event []) {
+ { TYPE_MESSAGE_AMEND, "amended1-", 0 },
+ { TYPE_PREFIX_APPEND, "appended1-", 0 },
+ { .type = TYPE_END }
+ },
+ .global_log_prefix = "global4.",
+ .result = "global4.Info: [amended1-appended1-TEXT]",
+ .result_str_out = "[amended1-appended1-TEXT]",
+ },
+ {
+ .prefixes = (const struct test_log_event []) {
+ { TYPE_MESSAGE_AMEND, "amended1-",
+ FLAG_BASE_EVENT },
+ { TYPE_PREFIX_APPEND, "appended1-", 0 },
+ { .type = TYPE_END }
+ },
+ .global_log_prefix = "global4.",
+ .result = "global4.Info: [amended1-appended1-TEXT]",
+ .result_str_out = "appended1-TEXT",
+ },
+ {
+ .prefixes = (const struct test_log_event []) {
+ { TYPE_MESSAGE_AMEND, "amended1-", 0 },
+ { TYPE_PREFIX_APPEND, "appended1-",
+ FLAG_BASE_EVENT },
+ { .type = TYPE_END }
+ },
+ .global_log_prefix = "global4.",
+ .result = "global4.Info: [amended1-appended1-TEXT]",
+ .result_str_out = "TEXT",
+ },
+ {
+ .prefixes = (const struct test_log_event []) {
+ { TYPE_PREFIX_APPEND, "appended1-", 0 },
+ { TYPE_MESSAGE_AMEND, "amended1-", 0 },
+ { .type = TYPE_END }
+ },
+ .global_log_prefix = "global4.",
+ .result = "global4.Info: appended1-[amended1-TEXT]",
+ .result_str_out = "appended1-[amended1-TEXT]",
+ },
+ {
+ .prefixes = (const struct test_log_event []) {
+ { TYPE_PREFIX_APPEND, "appended1-",
+ FLAG_BASE_EVENT },
+ { TYPE_MESSAGE_AMEND, "amended1-", 0 },
+ { .type = TYPE_END }
+ },
+ .global_log_prefix = "global4.",
+ .result = "global4.Info: appended1-[amended1-TEXT]",
+ .result_str_out = "[amended1-TEXT]",
+ },
+ {
+ .prefixes = (const struct test_log_event []) {
+ { TYPE_PREFIX_APPEND, "appended1-", 0 },
+ { TYPE_MESSAGE_AMEND, "amended1-",
+ FLAG_BASE_EVENT },
+ { .type = TYPE_END }
+ },
+ .global_log_prefix = "global4.",
+ .result = "global4.Info: appended1-[amended1-TEXT]",
+ .result_str_out = "TEXT",
+ },
+ {
+ .prefixes = (const struct test_log_event []) {
+ { TYPE_PREFIX_APPEND, "appended1-", 0 },
+ { TYPE_MESSAGE_AMEND, "amended1-", 0 },
+ { TYPE_PREFIX_APPEND, "appended2-", 0 },
+ { .type = TYPE_END }
+ },
+ .global_log_prefix = "global4.",
+ .result = "global4.Info: "
+ "appended1-[amended1-appended2-TEXT]",
+ .result_str_out = "appended1-[amended1-appended2-TEXT]",
+ },
+ {
+ .prefixes = (const struct test_log_event []) {
+ { TYPE_PREFIX_APPEND, "appended1-",
+ FLAG_BASE_EVENT },
+ { TYPE_MESSAGE_AMEND, "amended1-", 0 },
+ { TYPE_PREFIX_APPEND, "appended2-", 0 },
+ { .type = TYPE_END }
+ },
+ .global_log_prefix = "global4.",
+ .result = "global4.Info: "
+ "appended1-[amended1-appended2-TEXT]",
+ .result_str_out = "[amended1-appended2-TEXT]",
+ },
+ {
+ .prefixes = (const struct test_log_event []) {
+ { TYPE_PREFIX_APPEND, "appended1-", 0 },
+ { TYPE_MESSAGE_AMEND, "amended1-",
+ FLAG_BASE_EVENT },
+ { TYPE_PREFIX_APPEND, "appended2-", 0 },
+ { .type = TYPE_END }
+ },
+ .global_log_prefix = "global4.",
+ .result = "global4.Info: "
+ "appended1-[amended1-appended2-TEXT]",
+ .result_str_out = "appended2-TEXT",
+ },
+ {
+ .prefixes = (const struct test_log_event []) {
+ { TYPE_PREFIX_APPEND, "appended1-", 0 },
+ { TYPE_MESSAGE_AMEND, "amended1-", 0 },
+ { TYPE_PREFIX_APPEND, "appended2-",
+ FLAG_BASE_EVENT },
+ { .type = TYPE_END }
+ },
+ .global_log_prefix = "global4.",
+ .result = "global4.Info: "
+ "appended1-[amended1-appended2-TEXT]",
+ .result_str_out = "TEXT",
+ },
+ {
+ .prefixes = (const struct test_log_event []) {
+ { TYPE_MESSAGE_AMEND, "amended1-", 0 },
+ { TYPE_PREFIX_APPEND, "appended1-", 0 },
+ { TYPE_MESSAGE_AMEND, "amended2-", 0 },
+ { TYPE_PREFIX_APPEND, "appended2-", 0 },
+ { .type = TYPE_END }
+ },
+ .global_log_prefix = "global4.",
+ .result = "global4.Info: [amended1-appended1-"
+ "[amended2-appended2-TEXT]]",
+ .result_str_out = "[amended1-appended1-"
+ "[amended2-appended2-TEXT]]",
+ },
+ {
+ .prefixes = (const struct test_log_event []) {
+ { TYPE_MESSAGE_AMEND, "amended1-",
+ FLAG_BASE_EVENT },
+ { TYPE_PREFIX_APPEND, "appended1-", 0 },
+ { TYPE_MESSAGE_AMEND, "amended2-", 0 },
+ { TYPE_PREFIX_APPEND, "appended2-", 0 },
+ { .type = TYPE_END }
+ },
+ .global_log_prefix = "global4.",
+ .result = "global4.Info: [amended1-appended1-"
+ "[amended2-appended2-TEXT]]",
+ .result_str_out = "appended1-[amended2-appended2-TEXT]",
+ },
+ {
+ .prefixes = (const struct test_log_event []) {
+ { TYPE_MESSAGE_AMEND, "amended1-", 0 },
+ { TYPE_PREFIX_APPEND, "appended1-",
+ FLAG_BASE_EVENT },
+ { TYPE_MESSAGE_AMEND, "amended2-", 0 },
+ { TYPE_PREFIX_APPEND, "appended2-", 0 },
+ { .type = TYPE_END }
+ },
+ .global_log_prefix = "global4.",
+ .result = "global4.Info: [amended1-appended1-"
+ "[amended2-appended2-TEXT]]",
+ .result_str_out = "[amended2-appended2-TEXT]",
+ },
+ {
+ .prefixes = (const struct test_log_event []) {
+ { TYPE_MESSAGE_AMEND, "amended1-", 0 },
+ { TYPE_PREFIX_APPEND, "appended1-", 0 },
+ { TYPE_MESSAGE_AMEND, "amended2-",
+ FLAG_BASE_EVENT },
+ { TYPE_PREFIX_APPEND, "appended2-", 0 },
+ { .type = TYPE_END }
+ },
+ .global_log_prefix = "global4.",
+ .result = "global4.Info: [amended1-appended1-"
+ "[amended2-appended2-TEXT]]",
+ .result_str_out = "appended2-TEXT",
+ },
+ {
+ .prefixes = (const struct test_log_event []) {
+ { TYPE_MESSAGE_AMEND, "amended1-", 0 },
+ { TYPE_PREFIX_APPEND, "appended1-", 0 },
+ { TYPE_MESSAGE_AMEND, "amended2-", 0 },
+ { TYPE_PREFIX_APPEND, "appended2-",
+ FLAG_BASE_EVENT },
+ { .type = TYPE_END }
+ },
+ .global_log_prefix = "global4.",
+ .result = "global4.Info: [amended1-appended1-"
+ "[amended2-appended2-TEXT]]",
+ .result_str_out = "TEXT",
+ },
+ {
+ .prefixes = (const struct test_log_event []) {
+ { TYPE_PREFIX_REPLACE, "replaced1,", 0 },
+ { TYPE_MESSAGE_AMEND, "amended1-", 0 },
+ { .type = TYPE_END }
+ },
+ .result = "replaced1,Info: [amended1-TEXT]",
+ .result_str_out = "[amended1-TEXT]",
+ },
+ {
+ .prefixes = (const struct test_log_event []) {
+ { TYPE_PREFIX_REPLACE, "replaced1,",
+ FLAG_BASE_EVENT },
+ { TYPE_MESSAGE_AMEND, "amended1-", 0 },
+ { .type = TYPE_END }
+ },
+ .result = "replaced1,Info: [amended1-TEXT]",
+ .result_str_out = "[amended1-TEXT]",
+ },
+ {
+ .prefixes = (const struct test_log_event []) {
+ { TYPE_PREFIX_REPLACE, "replaced1,", 0 },
+ { TYPE_MESSAGE_AMEND, "amended1-",
+ FLAG_BASE_EVENT },
+ { .type = TYPE_END }
+ },
+ .result = "replaced1,Info: [amended1-TEXT]",
+ .result_str_out = "TEXT",
+ },
+ {
+ .prefixes = (const struct test_log_event []) {
+ { TYPE_PREFIX_REPLACE, "replaced1,", 0 },
+ { TYPE_MESSAGE_AMEND, "amended1-", 0 },
+ { TYPE_PREFIX_REPLACE, "replaced2,", 0 },
+ { TYPE_MESSAGE_AMEND, "amended2-", 0 },
+ { .type = TYPE_END }
+ },
+ .result = "replaced2,Info: [amended2-TEXT]",
+ .result_str_out = "[amended2-TEXT]",
+ },
+ {
+ .prefixes = (const struct test_log_event []) {
+ { TYPE_PREFIX_REPLACE, "replaced1,",
+ FLAG_BASE_EVENT },
+ { TYPE_MESSAGE_AMEND, "amended1-", 0 },
+ { TYPE_PREFIX_REPLACE, "replaced2,", 0 },
+ { TYPE_MESSAGE_AMEND, "amended2-", 0 },
+ { .type = TYPE_END }
+ },
+ .result = "replaced2,Info: [amended2-TEXT]",
+ .result_str_out = "[amended2-TEXT]",
+ },
+ {
+ .prefixes = (const struct test_log_event []) {
+ { TYPE_PREFIX_REPLACE, "replaced1,", 0 },
+ { TYPE_MESSAGE_AMEND, "amended1-",
+ FLAG_BASE_EVENT },
+ { TYPE_PREFIX_REPLACE, "replaced2,", 0 },
+ { TYPE_MESSAGE_AMEND, "amended2-", 0 },
+ { .type = TYPE_END }
+ },
+ .result = "replaced2,Info: [amended2-TEXT]",
+ .result_str_out = "[amended2-TEXT]",
+ },
+ {
+ .prefixes = (const struct test_log_event []) {
+ { TYPE_PREFIX_REPLACE, "replaced1,", 0 },
+ { TYPE_MESSAGE_AMEND, "amended1-", 0 },
+ { TYPE_PREFIX_REPLACE, "replaced2,",
+ FLAG_BASE_EVENT },
+ { TYPE_MESSAGE_AMEND, "amended2-", 0 },
+ { .type = TYPE_END }
+ },
+ .result = "replaced2,Info: [amended2-TEXT]",
+ .result_str_out = "[amended2-TEXT]",
+ },
+ {
+ .prefixes = (const struct test_log_event []) {
+ { TYPE_PREFIX_REPLACE, "replaced1,", 0 },
+ { TYPE_MESSAGE_AMEND, "amended1-", 0 },
+ { TYPE_PREFIX_REPLACE, "replaced2,", 0 },
+ { TYPE_MESSAGE_AMEND, "amended2-",
+ FLAG_BASE_EVENT },
+ { .type = TYPE_END }
+ },
+ .result = "replaced2,Info: [amended2-TEXT]",
+ .result_str_out = "TEXT",
+ },
+ /* Tests involving params->no_send */
+ {
+ .prefixes = (const struct test_log_event []) {
+ { TYPE_PREFIX_APPEND, "appended1,", 0 },
+ { TYPE_PREFIX_APPEND, "appended2.",
+ FLAG_BASE_EVENT },
+ { TYPE_PREFIX_APPEND, "appended3.", 0 },
+ { .type = TYPE_END }
+ },
+ .global_log_prefix = "global3.",
+ .result = NULL,
+ .result_str_out = "appended3.TEXT",
+ .flags = FLAG_NO_SEND,
+ },
+ {
+ .prefixes = (const struct test_log_event []) {
+ { TYPE_MESSAGE_AMEND, "amended1-", 0 },
+ { TYPE_PREFIX_APPEND, "appended1-",
+ FLAG_BASE_EVENT },
+ { TYPE_MESSAGE_AMEND, "amended2-", 0 },
+ { TYPE_PREFIX_APPEND, "appended2-", 0 },
+ { .type = TYPE_END }
+ },
+ .global_log_prefix = "global4.",
+ .result = NULL,
+ .result_str_out = "[amended2-appended2-TEXT]",
+ .flags = FLAG_NO_SEND,
+ },
+ /* Tests with params->base_*_prefix assigned */
+ {
+ .prefixes = (const struct test_log_event []) {
+ { .type = TYPE_END }
+ },
+ .global_log_prefix = "global1.",
+ .base_send_prefix = "PREFIX: ",
+ .base_str_prefix = "STR_PREFIX: ",
+ .result = "global1.Info: PREFIX: TEXT",
+ .result_str_out = "STR_PREFIX: TEXT",
+ },
+ {
+ .prefixes = (const struct test_log_event []) {
+ { TYPE_PREFIX_REPLACE, "replaced1,", 0 },
+ { .type = TYPE_END }
+ },
+ .base_send_prefix = "PREFIX: ",
+ .base_str_prefix = "STR_PREFIX: ",
+
+ .result = "replaced1,Info: TEXT",
+ .result_str_out = "TEXT",
+ },
+ {
+ .prefixes = (const struct test_log_event []) {
+ { TYPE_PREFIX_REPLACE, "replaced1,",
+ FLAG_BASE_EVENT },
+ { .type = TYPE_END }
+ },
+ .base_send_prefix = "PREFIX: ",
+ .base_str_prefix = "STR_PREFIX: ",
+ .result = "replaced1,Info: PREFIX: TEXT",
+ .result_str_out = "STR_PREFIX: TEXT",
+ },
+ {
+ .prefixes = (const struct test_log_event []) {
+ { TYPE_PREFIX_REPLACE, "replaced1,", 0 },
+ { TYPE_PREFIX_REPLACE, "replaced2.", 0 },
+ { .type = TYPE_END }
+ },
+ .base_send_prefix = "PREFIX: ",
+ .base_str_prefix = "STR_PREFIX: ",
+ .result = "replaced2.Info: TEXT",
+ .result_str_out = "TEXT",
+ },
+ {
+ .prefixes = (const struct test_log_event []) {
+ { TYPE_PREFIX_REPLACE, "replaced1,",
+ FLAG_BASE_EVENT },
+ { TYPE_PREFIX_REPLACE, "replaced2.", 0 },
+ { .type = TYPE_END }
+ },
+ .base_send_prefix = "PREFIX: ",
+ .base_str_prefix = "STR_PREFIX: ",
+ .result = "replaced2.Info: TEXT",
+ .result_str_out = "TEXT",
+ },
+ {
+ .prefixes = (const struct test_log_event []) {
+ { TYPE_PREFIX_REPLACE, "replaced1,", 0 },
+ { TYPE_PREFIX_REPLACE, "replaced2.",
+ FLAG_BASE_EVENT },
+ { .type = TYPE_END }
+ },
+ .base_send_prefix = "PREFIX: ",
+ .base_str_prefix = "STR_PREFIX: ",
+ .result = "replaced2.Info: PREFIX: TEXT",
+ .result_str_out = "STR_PREFIX: TEXT",
+ },
+ {
+ .prefixes = (const struct test_log_event []) {
+ { TYPE_PREFIX_REPLACE, "replaced1,", 0 },
+ { TYPE_PREFIX_APPEND, "appended2.", 0 },
+ { .type = TYPE_END }
+ },
+ .base_send_prefix = "PREFIX: ",
+ .base_str_prefix = "STR_PREFIX: ",
+ .result = "replaced1,Info: appended2.TEXT",
+ .result_str_out = "appended2.TEXT",
+ },
+ {
+ .prefixes = (const struct test_log_event []) {
+ { TYPE_PREFIX_REPLACE, "replaced1,",
+ FLAG_BASE_EVENT },
+ { TYPE_PREFIX_APPEND, "appended2.", 0 },
+ { .type = TYPE_END }
+ },
+ .base_send_prefix = "PREFIX: ",
+ .base_str_prefix = "STR_PREFIX: ",
+ .result = "replaced1,Info: PREFIX: appended2.TEXT",
+ .result_str_out = "STR_PREFIX: appended2.TEXT",
+ },
+ {
+ .prefixes = (const struct test_log_event []) {
+ { TYPE_PREFIX_REPLACE, "replaced1,", 0 },
+ { TYPE_PREFIX_APPEND, "appended2.",
+ FLAG_BASE_EVENT },
+ { .type = TYPE_END }
+ },
+ .base_send_prefix = "PREFIX: ",
+ .base_str_prefix = "STR_PREFIX: ",
+ .result = "replaced1,Info: appended2.PREFIX: TEXT",
+ .result_str_out = "STR_PREFIX: TEXT",
+ },
+ {
+ .prefixes = (const struct test_log_event []) {
+ { TYPE_PREFIX_APPEND, "appended1,", 0 },
+ { TYPE_PREFIX_REPLACE, "replaced1,", 0 },
+ { .type = TYPE_END }
+ },
+ .base_send_prefix = "PREFIX: ",
+ .base_str_prefix = "STR_PREFIX: ",
+ .result = "replaced1,Info: TEXT",
+ .result_str_out = "TEXT",
+ },
+ {
+ .prefixes = (const struct test_log_event []) {
+ { TYPE_PREFIX_APPEND, "appended1,",
+ FLAG_BASE_EVENT },
+ { TYPE_PREFIX_REPLACE, "replaced1,", 0 },
+ { .type = TYPE_END }
+ },
+ .base_send_prefix = "PREFIX: ",
+ .base_str_prefix = "STR_PREFIX: ",
+ .result = "replaced1,Info: TEXT",
+ .result_str_out = "TEXT",
+ },
+ {
+ .prefixes = (const struct test_log_event []) {
+ { TYPE_PREFIX_APPEND, "appended1,", 0 },
+ { TYPE_PREFIX_REPLACE, "replaced1,",
+ FLAG_BASE_EVENT },
+ { .type = TYPE_END }
+ },
+ .base_send_prefix = "PREFIX: ",
+ .base_str_prefix = "STR_PREFIX: ",
+ .result = "replaced1,Info: PREFIX: TEXT",
+ .result_str_out = "STR_PREFIX: TEXT",
+ },
+ {
+ .prefixes = (const struct test_log_event []) {
+ { TYPE_PREFIX_APPEND, "appended1,", 0 },
+ { .type = TYPE_END }
+ },
+ .global_log_prefix = "global2.",
+ .base_send_prefix = "PREFIX: ",
+ .base_str_prefix = "STR_PREFIX: ",
+ .result = "global2.Info: PREFIX: appended1,TEXT",
+ .result_str_out = "STR_PREFIX: appended1,TEXT",
+ },
+ {
+ .prefixes = (const struct test_log_event []) {
+ { TYPE_PREFIX_APPEND, "appended1,",
+ FLAG_BASE_EVENT },
+ { .type = TYPE_END }
+ },
+ .global_log_prefix = "global2.",
+ .base_send_prefix = "PREFIX: ",
+ .base_str_prefix = "STR_PREFIX: ",
+ .result = "global2.Info: appended1,PREFIX: TEXT",
+ .result_str_out = "STR_PREFIX: TEXT",
+ },
+ {
+ .prefixes = (const struct test_log_event []) {
+ { TYPE_PREFIX_APPEND, "appended1,", 0 },
+ { TYPE_PREFIX_APPEND, "appended2.", 0 },
+ { .type = TYPE_END }
+ },
+ .global_log_prefix = "global3.",
+ .base_send_prefix = "PREFIX: ",
+ .base_str_prefix = "STR_PREFIX: ",
+ .result = "global3.Info: PREFIX: "
+ "appended1,appended2.TEXT",
+ .result_str_out = "STR_PREFIX: "
+ "appended1,appended2.TEXT",
+ },
+ {
+ .prefixes = (const struct test_log_event []) {
+ { TYPE_PREFIX_APPEND, "appended1,",
+ FLAG_BASE_EVENT },
+ { TYPE_PREFIX_APPEND, "appended2.", 0 },
+ { .type = TYPE_END }
+ },
+ .global_log_prefix = "global3.",
+ .base_send_prefix = "PREFIX: ",
+ .base_str_prefix = "STR_PREFIX: ",
+ .result = "global3.Info: appended1,PREFIX: "
+ "appended2.TEXT",
+ .result_str_out = "STR_PREFIX: appended2.TEXT",
+ },
+ {
+ .prefixes = (const struct test_log_event []) {
+ { TYPE_PREFIX_APPEND, "appended1,", 0 },
+ { TYPE_PREFIX_APPEND, "appended2.",
+ FLAG_BASE_EVENT },
+ { .type = TYPE_END }
+ },
+ .global_log_prefix = "global3.",
+ .base_send_prefix = "PREFIX: ",
+ .base_str_prefix = "STR_PREFIX: ",
+ .result = "global3.Info: appended1,appended2."
+ "PREFIX: TEXT",
+ .result_str_out = "STR_PREFIX: TEXT",
+ },
+ {
+ .prefixes = (const struct test_log_event []) {
+ { TYPE_PREFIX_APPEND, "appended1,", 0 },
+ { TYPE_PREFIX_APPEND, "appended2.", 0 },
+ { TYPE_PREFIX_APPEND, "appended3.", 0 },
+ { .type = TYPE_END }
+ },
+ .global_log_prefix = "global3.",
+ .base_send_prefix = "PREFIX: ",
+ .base_str_prefix = "STR_PREFIX: ",
+ .result = "global3.Info: PREFIX: "
+ "appended1,appended2.appended3.TEXT",
+ .result_str_out = "STR_PREFIX: "
+ "appended1,appended2.appended3.TEXT",
+ },
+ {
+ .prefixes = (const struct test_log_event []) {
+ { TYPE_PREFIX_APPEND, "appended1,",
+ FLAG_BASE_EVENT },
+ { TYPE_PREFIX_APPEND, "appended2.", 0 },
+ { TYPE_PREFIX_APPEND, "appended3.", 0 },
+ { .type = TYPE_END }
+ },
+ .global_log_prefix = "global3.",
+ .base_send_prefix = "PREFIX: ",
+ .base_str_prefix = "STR_PREFIX: ",
+ .result = "global3.Info: appended1,PREFIX: "
+ "appended2.appended3.TEXT",
+ .result_str_out = "STR_PREFIX: "
+ "appended2.appended3.TEXT",
+ },
+ {
+ .prefixes = (const struct test_log_event []) {
+ { TYPE_PREFIX_APPEND, "appended1,", 0 },
+ { TYPE_PREFIX_APPEND, "appended2.",
+ FLAG_BASE_EVENT },
+ { TYPE_PREFIX_APPEND, "appended3.", 0 },
+ { .type = TYPE_END }
+ },
+ .global_log_prefix = "global3.",
+ .base_send_prefix = "PREFIX: ",
+ .base_str_prefix = "STR_PREFIX: ",
+ .result = "global3.Info: appended1,appended2.PREFIX: "
+ "appended3.TEXT",
+ .result_str_out = "STR_PREFIX: appended3.TEXT",
+ },
+ {
+ .prefixes = (const struct test_log_event []) {
+ { TYPE_PREFIX_APPEND, "appended1,", 0 },
+ { TYPE_PREFIX_APPEND, "appended2.", 0 },
+ { TYPE_PREFIX_APPEND, "appended3.",
+ FLAG_BASE_EVENT },
+ { .type = TYPE_END }
+ },
+ .global_log_prefix = "global3.",
+ .base_send_prefix = "PREFIX: ",
+ .base_str_prefix = "STR_PREFIX: ",
+ .result = "global3.Info: "
+ "appended1,appended2.appended3.PREFIX: TEXT",
+ .result_str_out = "STR_PREFIX: TEXT",
+ },
+ {
+ .prefixes = (const struct test_log_event []) {
+ { TYPE_PREFIX_APPEND, "appended1,", 0 },
+ { TYPE_PREFIX_REPLACE, "replaced2.", 0 },
+ { TYPE_PREFIX_APPEND, "appended3#", 0 },
+ { .type = TYPE_END }
+ },
+ .base_send_prefix = "PREFIX: ",
+ .base_str_prefix = "STR_PREFIX: ",
+ .result = "replaced2.Info: appended3#TEXT",
+ .result_str_out = "appended3#TEXT",
+ },
+ {
+ .prefixes = (const struct test_log_event []) {
+ { TYPE_PREFIX_APPEND, "appended1,",
+ FLAG_BASE_EVENT },
+ { TYPE_PREFIX_REPLACE, "replaced2.", 0 },
+ { TYPE_PREFIX_APPEND, "appended3#", 0 },
+ { .type = TYPE_END }
+ },
+ .base_send_prefix = "PREFIX: ",
+ .base_str_prefix = "STR_PREFIX: ",
+ .result = "replaced2.Info: appended3#TEXT",
+ .result_str_out = "appended3#TEXT",
+ },
+ {
+ .prefixes = (const struct test_log_event []) {
+ { TYPE_PREFIX_APPEND, "appended1,", 0 },
+ { TYPE_PREFIX_REPLACE, "replaced2.",
+ FLAG_BASE_EVENT },
+ { TYPE_PREFIX_APPEND, "appended3#", 0 },
+ { .type = TYPE_END }
+ },
+ .base_send_prefix = "PREFIX: ",
+ .base_str_prefix = "STR_PREFIX: ",
+ .result = "replaced2.Info: PREFIX: appended3#TEXT",
+ .result_str_out = "STR_PREFIX: appended3#TEXT",
+ },
+ {
+ .prefixes = (const struct test_log_event []) {
+ { TYPE_PREFIX_APPEND, "appended1,", 0 },
+ { TYPE_PREFIX_REPLACE, "replaced2.", 0 },
+ { TYPE_PREFIX_APPEND, "appended3#",
+ FLAG_BASE_EVENT },
+ { .type = TYPE_END }
+ },
+ .base_send_prefix = "PREFIX: ",
+ .base_str_prefix = "STR_PREFIX: ",
+ .result = "replaced2.Info: appended3#PREFIX: TEXT",
+ .result_str_out = "STR_PREFIX: TEXT",
+ },
+ {
+ .prefixes = (const struct test_log_event []) {
+ { TYPE_PREFIX_APPEND, "appended1,", 0 },
+ { TYPE_PREFIX_REPLACE, "replaced2.", 0 },
+ { TYPE_PREFIX_APPEND, "appended3#", 0 },
+ { TYPE_PREFIX_REPLACE, "replaced4;", 0 },
+ { .type = TYPE_END }
+ },
+ .base_send_prefix = "PREFIX: ",
+ .base_str_prefix = "STR_PREFIX: ",
+ .result = "replaced4;Info: TEXT",
+ .result_str_out = "TEXT",
+ },
+ {
+ .prefixes = (const struct test_log_event []) {
+ { TYPE_PREFIX_APPEND, "appended1,",
+ FLAG_BASE_EVENT },
+ { TYPE_PREFIX_REPLACE, "replaced2.", 0 },
+ { TYPE_PREFIX_APPEND, "appended3#", 0 },
+ { TYPE_PREFIX_REPLACE, "replaced4;", 0 },
+ { .type = TYPE_END }
+ },
+ .base_send_prefix = "PREFIX: ",
+ .base_str_prefix = "STR_PREFIX: ",
+ .result = "replaced4;Info: TEXT",
+ .result_str_out = "TEXT",
+ },
+ {
+ .prefixes = (const struct test_log_event []) {
+ { TYPE_PREFIX_APPEND, "appended1,", 0 },
+ { TYPE_PREFIX_REPLACE, "replaced2.",
+ FLAG_BASE_EVENT },
+ { TYPE_PREFIX_APPEND, "appended3#", 0 },
+ { TYPE_PREFIX_REPLACE, "replaced4;", 0 },
+ { .type = TYPE_END }
+ },
+ .base_send_prefix = "PREFIX: ",
+ .base_str_prefix = "STR_PREFIX: ",
+ .result = "replaced4;Info: TEXT",
+ .result_str_out = "TEXT",
+ },
+ {
+ .prefixes = (const struct test_log_event []) {
+ { TYPE_PREFIX_APPEND, "appended1,", 0 },
+ { TYPE_PREFIX_REPLACE, "replaced2.", 0 },
+ { TYPE_PREFIX_APPEND, "appended3#",
+ FLAG_BASE_EVENT },
+ { TYPE_PREFIX_REPLACE, "replaced4;", 0 },
+ { .type = TYPE_END }
+ },
+ .base_send_prefix = "PREFIX: ",
+ .base_str_prefix = "STR_PREFIX: ",
+ .result = "replaced4;Info: TEXT",
+ .result_str_out = "TEXT",
+ },
+ {
+ .prefixes = (const struct test_log_event []) {
+ { TYPE_PREFIX_APPEND, "appended1,", 0 },
+ { TYPE_PREFIX_REPLACE, "replaced2.", 0 },
+ { TYPE_PREFIX_APPEND, "appended3#", 0 },
+ { TYPE_PREFIX_REPLACE, "replaced4;",
+ FLAG_BASE_EVENT },
+ { .type = TYPE_END }
+ },
+ .base_send_prefix = "PREFIX: ",
+ .base_str_prefix = "STR_PREFIX: ",
+ .result = "replaced4;Info: PREFIX: TEXT",
+ .result_str_out = "STR_PREFIX: TEXT",
+ },
+ {
+ .prefixes = (const struct test_log_event []) {
+ { TYPE_PREFIX_APPEND, "appended1,", 0 },
+ { TYPE_PREFIX_REPLACE, "replaced2.", 0 },
+ { TYPE_PREFIX_APPEND, "appended3#", 0 },
+ { TYPE_PREFIX_REPLACE, "replaced4;", 0 },
+ { TYPE_PREFIX_APPEND, "appended5-", 0 },
+ { .type = TYPE_END }
+ },
+ .base_send_prefix = "PREFIX: ",
+ .base_str_prefix = "STR_PREFIX: ",
+ .result = "replaced4;Info: appended5-TEXT",
+ .result_str_out = "appended5-TEXT",
+ },
+ {
+ .prefixes = (const struct test_log_event []) {
+ { TYPE_PREFIX_APPEND, "appended1,",
+ FLAG_BASE_EVENT },
+ { TYPE_PREFIX_REPLACE, "replaced2.", 0 },
+ { TYPE_PREFIX_APPEND, "appended3#", 0 },
+ { TYPE_PREFIX_REPLACE, "replaced4;", 0 },
+ { TYPE_PREFIX_APPEND, "appended5-", 0 },
+ { .type = TYPE_END }
+ },
+ .base_send_prefix = "PREFIX: ",
+ .base_str_prefix = "STR_PREFIX: ",
+ .result = "replaced4;Info: appended5-TEXT",
+ .result_str_out = "appended5-TEXT",
+ },
+ {
+ .prefixes = (const struct test_log_event []) {
+ { TYPE_PREFIX_APPEND, "appended1,", 0 },
+ { TYPE_PREFIX_REPLACE, "replaced2.",
+ FLAG_BASE_EVENT },
+ { TYPE_PREFIX_APPEND, "appended3#", 0 },
+ { TYPE_PREFIX_REPLACE, "replaced4;", 0 },
+ { TYPE_PREFIX_APPEND, "appended5-", 0 },
+ { .type = TYPE_END }
+ },
+ .base_send_prefix = "PREFIX: ",
+ .base_str_prefix = "STR_PREFIX: ",
+ .result = "replaced4;Info: appended5-TEXT",
+ .result_str_out = "appended5-TEXT",
+ },
+ {
+ .prefixes = (const struct test_log_event []) {
+ { TYPE_PREFIX_APPEND, "appended1,", 0 },
+ { TYPE_PREFIX_REPLACE, "replaced2.", 0 },
+ { TYPE_PREFIX_APPEND, "appended3#",
+ FLAG_BASE_EVENT },
+ { TYPE_PREFIX_REPLACE, "replaced4;", 0 },
+ { TYPE_PREFIX_APPEND, "appended5-", 0 },
+ { .type = TYPE_END }
+ },
+ .base_send_prefix = "PREFIX: ",
+ .base_str_prefix = "STR_PREFIX: ",
+ .result = "replaced4;Info: appended5-TEXT",
+ .result_str_out = "appended5-TEXT",
+ },
+ {
+ .prefixes = (const struct test_log_event []) {
+ { TYPE_PREFIX_APPEND, "appended1,", 0 },
+ { TYPE_PREFIX_REPLACE, "replaced2.", 0 },
+ { TYPE_PREFIX_APPEND, "appended3#", 0 },
+ { TYPE_PREFIX_REPLACE, "replaced4;",
+ FLAG_BASE_EVENT },
+ { TYPE_PREFIX_APPEND, "appended5-", 0 },
+ { .type = TYPE_END }
+ },
+ .base_send_prefix = "PREFIX: ",
+ .base_str_prefix = "STR_PREFIX: ",
+ .result = "replaced4;Info: PREFIX: appended5-TEXT",
+ .result_str_out = "STR_PREFIX: appended5-TEXT",
+ },
+ {
+ .prefixes = (const struct test_log_event []) {
+ { TYPE_PREFIX_APPEND, "appended1,", 0 },
+ { TYPE_PREFIX_REPLACE, "replaced2.", 0 },
+ { TYPE_PREFIX_APPEND, "appended3#", 0 },
+ { TYPE_PREFIX_REPLACE, "replaced4;", 0 },
+ { TYPE_PREFIX_APPEND, "appended5-",
+ FLAG_BASE_EVENT },
+ { .type = TYPE_END }
+ },
+ .base_send_prefix = "PREFIX: ",
+ .base_str_prefix = "STR_PREFIX: ",
+ .result = "replaced4;Info: appended5-PREFIX: TEXT",
+ .result_str_out = "STR_PREFIX: TEXT",
+ },
+ {
+ .prefixes = (const struct test_log_event []) {
+ { TYPE_PREFIX_APPEND_CB, "appended1-", 0 },
+ { .type = TYPE_END }
+ },
+ .global_log_prefix = "global3.",
+ .base_send_prefix = "PREFIX: ",
+ .base_str_prefix = "STR_PREFIX: ",
+ .result = "global3.Info: PREFIX: "
+ "callback(appended1-)TEXT",
+ .result_str_out = "STR_PREFIX: "
+ "callback(appended1-)TEXT",
+ },
+ {
+ .prefixes = (const struct test_log_event []) {
+ { TYPE_PREFIX_APPEND_CB, "appended1-",
+ FLAG_BASE_EVENT },
+ { .type = TYPE_END }
+ },
+ .global_log_prefix = "global3.",
+ .base_send_prefix = "PREFIX: ",
+ .base_str_prefix = "STR_PREFIX: ",
+ .result = "global3.Info: callback(appended1-)PREFIX: "
+ "TEXT",
+ .result_str_out = "STR_PREFIX: TEXT",
+ },
+ {
+ .prefixes = (const struct test_log_event []) {
+ { TYPE_PREFIX_APPEND, "appended1,", 0 },
+ { TYPE_PREFIX_REPLACE, "replaced1.", 0 },
+ { TYPE_PREFIX_REPLACE_CB, "replaced2-", 0 },
+ { .type = TYPE_END }
+ },
+ .base_send_prefix = "PREFIX: ",
+ .base_str_prefix = "STR_PREFIX: ",
+ .result = "callback(replaced2-)Info: TEXT",
+ .result_str_out = "TEXT",
+ },
+ {
+ .prefixes = (const struct test_log_event []) {
+ { TYPE_PREFIX_APPEND, "appended1,",
+ FLAG_BASE_EVENT },
+ { TYPE_PREFIX_REPLACE, "replaced1.", 0 },
+ { TYPE_PREFIX_REPLACE_CB, "replaced2-", 0 },
+ { .type = TYPE_END }
+ },
+ .base_send_prefix = "PREFIX: ",
+ .base_str_prefix = "STR_PREFIX: ",
+ .result = "callback(replaced2-)Info: TEXT",
+ .result_str_out = "TEXT",
+ },
+ {
+ .prefixes = (const struct test_log_event []) {
+ { TYPE_PREFIX_APPEND, "appended1,", 0 },
+ { TYPE_PREFIX_REPLACE, "replaced1.",
+ FLAG_BASE_EVENT },
+ { TYPE_PREFIX_REPLACE_CB, "replaced2-", 0 },
+ { .type = TYPE_END }
+ },
+ .base_send_prefix = "PREFIX: ",
+ .base_str_prefix = "STR_PREFIX: ",
+ .result = "callback(replaced2-)Info: TEXT",
+ .result_str_out = "TEXT",
+ },
+ {
+ .prefixes = (const struct test_log_event []) {
+ { TYPE_PREFIX_APPEND, "appended1,", 0 },
+ { TYPE_PREFIX_REPLACE, "replaced1.", 0 },
+ { TYPE_PREFIX_REPLACE_CB, "replaced2-",
+ FLAG_BASE_EVENT },
+ { .type = TYPE_END }
+ },
+ .base_send_prefix = "PREFIX: ",
+ .base_str_prefix = "STR_PREFIX: ",
+ .result = "callback(replaced2-)Info: PREFIX: TEXT",
+ .result_str_out = "STR_PREFIX: TEXT",
+ },
+ {
+ .prefixes = (const struct test_log_event []) {
+ { TYPE_PREFIX_REPLACE_CB, "replaced1.", 0 },
+ { TYPE_PREFIX_APPEND, "appended1,", 0 },
+ { .type = TYPE_END }
+ },
+ .base_send_prefix = "PREFIX: ",
+ .base_str_prefix = "STR_PREFIX: ",
+ .result = "callback(replaced1.)Info: appended1,TEXT",
+ .result_str_out = "appended1,TEXT",
+ },
+ {
+ .prefixes = (const struct test_log_event []) {
+ { TYPE_PREFIX_REPLACE_CB, "replaced1.",
+ FLAG_BASE_EVENT },
+ { TYPE_PREFIX_APPEND, "appended1,", 0 },
+ { .type = TYPE_END }
+ },
+ .base_send_prefix = "PREFIX: ",
+ .base_str_prefix = "STR_PREFIX: ",
+ .result = "callback(replaced1.)Info: PREFIX: "
+ "appended1,TEXT",
+ .result_str_out = "STR_PREFIX: appended1,TEXT",
+ },
+ {
+ .prefixes = (const struct test_log_event []) {
+ { TYPE_PREFIX_REPLACE_CB, "replaced1.", 0 },
+ { TYPE_PREFIX_APPEND, "appended1,",
+ FLAG_BASE_EVENT },
+ { .type = TYPE_END }
+ },
+ .base_send_prefix = "PREFIX: ",
+ .base_str_prefix = "STR_PREFIX: ",
+ .result = "callback(replaced1.)Info: appended1,PREFIX: "
+ "TEXT",
+ .result_str_out = "STR_PREFIX: TEXT",
+ },
+ {
+ .prefixes = (const struct test_log_event []) {
+ { TYPE_PREFIX_REPLACE_CB, "replaced1.", 0 },
+ { TYPE_PREFIX_REPLACE, "replaced2-", 0 },
+ { .type = TYPE_END }
+ },
+ .base_send_prefix = "PREFIX: ",
+ .base_str_prefix = "STR_PREFIX: ",
+ .result = "replaced2-Info: TEXT",
+ .result_str_out = "TEXT",
+ },
+ {
+ .prefixes = (const struct test_log_event []) {
+ { TYPE_PREFIX_REPLACE_CB, "replaced1.",
+ FLAG_BASE_EVENT },
+ { TYPE_PREFIX_REPLACE, "replaced2-", 0 },
+ { .type = TYPE_END }
+ },
+ .base_send_prefix = "PREFIX: ",
+ .base_str_prefix = "STR_PREFIX: ",
+ .result = "replaced2-Info: TEXT",
+ .result_str_out = "TEXT",
+ },
+ {
+ .prefixes = (const struct test_log_event []) {
+ { TYPE_PREFIX_REPLACE_CB, "replaced1.", 0 },
+ { TYPE_PREFIX_REPLACE, "replaced2-",
+ FLAG_BASE_EVENT },
+ { .type = TYPE_END }
+ },
+ .base_send_prefix = "PREFIX: ",
+ .base_str_prefix = "STR_PREFIX: ",
+ .result = "replaced2-Info: PREFIX: TEXT",
+ .result_str_out = "STR_PREFIX: TEXT",
+ },
+ {
+ .prefixes = (const struct test_log_event []) {
+ { TYPE_MESSAGE_AMEND, "amended1-" , 0},
+ { .type = TYPE_END }
+ },
+ .global_log_prefix = "global4.",
+ .base_send_prefix = "PREFIX: ",
+ .base_str_prefix = "STR_PREFIX: ",
+ .result = "global4.Info: PREFIX: [amended1-TEXT]",
+ .result_str_out = "STR_PREFIX: [amended1-TEXT]",
+ },
+ {
+ .prefixes = (const struct test_log_event []) {
+ { TYPE_MESSAGE_AMEND, "amended1-" ,
+ FLAG_BASE_EVENT},
+ { .type = TYPE_END }
+ },
+ .global_log_prefix = "global4.",
+ .base_send_prefix = "PREFIX: ",
+ .base_str_prefix = "STR_PREFIX: ",
+ .result = "global4.Info: [amended1-PREFIX: TEXT]",
+ .result_str_out = "STR_PREFIX: TEXT",
+ },
+ {
+ .prefixes = (const struct test_log_event []) {
+ { TYPE_MESSAGE_AMEND, "amended1-", 0 },
+ { TYPE_MESSAGE_AMEND, "amended2-", 0 },
+ { .type = TYPE_END }
+ },
+ .global_log_prefix = "global4.",
+ .base_send_prefix = "PREFIX: ",
+ .base_str_prefix = "STR_PREFIX: ",
+ .result = "global4.Info: PREFIX: "
+ "[amended1-[amended2-TEXT]]",
+ .result_str_out = "STR_PREFIX: "
+ "[amended1-[amended2-TEXT]]",
+ },
+ {
+ .prefixes = (const struct test_log_event []) {
+ { TYPE_MESSAGE_AMEND, "amended1-",
+ FLAG_BASE_EVENT },
+ { TYPE_MESSAGE_AMEND, "amended2-", 0 },
+ { .type = TYPE_END }
+ },
+ .global_log_prefix = "global4.",
+ .base_send_prefix = "PREFIX: ",
+ .base_str_prefix = "STR_PREFIX: ",
+ .result = "global4.Info: [amended1-PREFIX: "
+ "[amended2-TEXT]]",
+ .result_str_out = "STR_PREFIX: [amended2-TEXT]",
+ },
+ {
+ .prefixes = (const struct test_log_event []) {
+ { TYPE_MESSAGE_AMEND, "amended1-", 0 },
+ { TYPE_MESSAGE_AMEND, "amended2-",
+ FLAG_BASE_EVENT },
+ { .type = TYPE_END }
+ },
+ .global_log_prefix = "global4.",
+ .base_send_prefix = "PREFIX: ",
+ .base_str_prefix = "STR_PREFIX: ",
+ .result = "global4.Info: [amended1-[amended2-PREFIX: "
+ "TEXT]]",
+ .result_str_out = "STR_PREFIX: TEXT",
+ },
+ {
+ .prefixes = (const struct test_log_event []) {
+ { TYPE_MESSAGE_AMEND, "amended1-", 0 },
+ { TYPE_PREFIX_APPEND, "appended1-", 0 },
+ { .type = TYPE_END }
+ },
+ .global_log_prefix = "global4.",
+ .base_send_prefix = "PREFIX: ",
+ .base_str_prefix = "STR_PREFIX: ",
+ .result = "global4.Info: PREFIX: "
+ "[amended1-appended1-TEXT]",
+ .result_str_out = "STR_PREFIX: "
+ "[amended1-appended1-TEXT]",
+ },
+ {
+ .prefixes = (const struct test_log_event []) {
+ { TYPE_MESSAGE_AMEND, "amended1-",
+ FLAG_BASE_EVENT },
+ { TYPE_PREFIX_APPEND, "appended1-", 0 },
+ { .type = TYPE_END }
+ },
+ .global_log_prefix = "global4.",
+ .base_send_prefix = "PREFIX: ",
+ .base_str_prefix = "STR_PREFIX: ",
+ .result = "global4.Info: [amended1-PREFIX: "
+ "appended1-TEXT]",
+ .result_str_out = "STR_PREFIX: appended1-TEXT",
+ },
+ {
+ .prefixes = (const struct test_log_event []) {
+ { TYPE_MESSAGE_AMEND, "amended1-", 0 },
+ { TYPE_PREFIX_APPEND, "appended1-",
+ FLAG_BASE_EVENT },
+ { .type = TYPE_END }
+ },
+ .global_log_prefix = "global4.",
+ .base_send_prefix = "PREFIX: ",
+ .base_str_prefix = "STR_PREFIX: ",
+ .result = "global4.Info: [amended1-appended1-PREFIX: "
+ "TEXT]",
+ .result_str_out = "STR_PREFIX: TEXT",
+ },
+ {
+ .prefixes = (const struct test_log_event []) {
+ { TYPE_PREFIX_APPEND, "appended1-", 0 },
+ { TYPE_MESSAGE_AMEND, "amended1-", 0 },
+ { .type = TYPE_END }
+ },
+ .global_log_prefix = "global4.",
+ .base_send_prefix = "PREFIX: ",
+ .base_str_prefix = "STR_PREFIX: ",
+ .result = "global4.Info: PREFIX: "
+ "appended1-[amended1-TEXT]",
+ .result_str_out = "STR_PREFIX: "
+ "appended1-[amended1-TEXT]",
+ },
+ {
+ .prefixes = (const struct test_log_event []) {
+ { TYPE_PREFIX_APPEND, "appended1-",
+ FLAG_BASE_EVENT },
+ { TYPE_MESSAGE_AMEND, "amended1-", 0 },
+ { .type = TYPE_END }
+ },
+ .global_log_prefix = "global4.",
+ .base_send_prefix = "PREFIX: ",
+ .base_str_prefix = "STR_PREFIX: ",
+ .result = "global4.Info: appended1-PREFIX: "
+ "[amended1-TEXT]",
+ .result_str_out = "STR_PREFIX: [amended1-TEXT]",
+ },
+ {
+ .prefixes = (const struct test_log_event []) {
+ { TYPE_PREFIX_APPEND, "appended1-", 0 },
+ { TYPE_MESSAGE_AMEND, "amended1-",
+ FLAG_BASE_EVENT },
+ { .type = TYPE_END }
+ },
+ .global_log_prefix = "global4.",
+ .base_send_prefix = "PREFIX: ",
+ .base_str_prefix = "STR_PREFIX: ",
+ .result = "global4.Info: appended1-[amended1-PREFIX: "
+ "TEXT]",
+ .result_str_out = "STR_PREFIX: TEXT",
+ },
+ {
+ .prefixes = (const struct test_log_event []) {
+ { TYPE_PREFIX_APPEND, "appended1-", 0 },
+ { TYPE_MESSAGE_AMEND, "amended1-", 0 },
+ { TYPE_PREFIX_APPEND, "appended2-", 0 },
+ { .type = TYPE_END }
+ },
+ .global_log_prefix = "global4.",
+ .base_send_prefix = "PREFIX: ",
+ .base_str_prefix = "STR_PREFIX: ",
+ .result = "global4.Info: PREFIX: "
+ "appended1-[amended1-appended2-TEXT]",
+ .result_str_out = "STR_PREFIX: "
+ "appended1-[amended1-appended2-TEXT]",
+ },
+ {
+ .prefixes = (const struct test_log_event []) {
+ { TYPE_PREFIX_APPEND, "appended1-",
+ FLAG_BASE_EVENT },
+ { TYPE_MESSAGE_AMEND, "amended1-", 0 },
+ { TYPE_PREFIX_APPEND, "appended2-", 0 },
+ { .type = TYPE_END }
+ },
+ .global_log_prefix = "global4.",
+ .base_send_prefix = "PREFIX: ",
+ .base_str_prefix = "STR_PREFIX: ",
+ .result = "global4.Info: appended1-PREFIX: "
+ "[amended1-appended2-TEXT]",
+ .result_str_out = "STR_PREFIX: "
+ "[amended1-appended2-TEXT]",
+ },
+ {
+ .prefixes = (const struct test_log_event []) {
+ { TYPE_PREFIX_APPEND, "appended1-", 0 },
+ { TYPE_MESSAGE_AMEND, "amended1-",
+ FLAG_BASE_EVENT },
+ { TYPE_PREFIX_APPEND, "appended2-", 0 },
+ { .type = TYPE_END }
+ },
+ .global_log_prefix = "global4.",
+ .base_send_prefix = "PREFIX: ",
+ .base_str_prefix = "STR_PREFIX: ",
+ .result = "global4.Info: appended1-[amended1-PREFIX: "
+ "appended2-TEXT]",
+ .result_str_out = "STR_PREFIX: appended2-TEXT",
+ },
+ {
+ .prefixes = (const struct test_log_event []) {
+ { TYPE_PREFIX_APPEND, "appended1-", 0 },
+ { TYPE_MESSAGE_AMEND, "amended1-", 0 },
+ { TYPE_PREFIX_APPEND, "appended2-",
+ FLAG_BASE_EVENT },
+ { .type = TYPE_END }
+ },
+ .global_log_prefix = "global4.",
+ .base_send_prefix = "PREFIX: ",
+ .base_str_prefix = "STR_PREFIX: ",
+ .result = "global4.Info: "
+ "appended1-[amended1-appended2-PREFIX: TEXT]",
+ .result_str_out = "STR_PREFIX: TEXT",
+ },
+ {
+ .prefixes = (const struct test_log_event []) {
+ { TYPE_MESSAGE_AMEND, "amended1-", 0 },
+ { TYPE_PREFIX_APPEND, "appended1-", 0 },
+ { TYPE_MESSAGE_AMEND, "amended2-", 0 },
+ { TYPE_PREFIX_APPEND, "appended2-", 0 },
+ { .type = TYPE_END }
+ },
+ .global_log_prefix = "global4.",
+ .base_send_prefix = "PREFIX: ",
+ .base_str_prefix = "STR_PREFIX: ",
+ .result = "global4.Info: PREFIX: [amended1-appended1-"
+ "[amended2-appended2-TEXT]]",
+ .result_str_out = "STR_PREFIX: [amended1-appended1-"
+ "[amended2-appended2-TEXT]]",
+ },
+ {
+ .prefixes = (const struct test_log_event []) {
+ { TYPE_MESSAGE_AMEND, "amended1-",
+ FLAG_BASE_EVENT },
+ { TYPE_PREFIX_APPEND, "appended1-", 0 },
+ { TYPE_MESSAGE_AMEND, "amended2-", 0 },
+ { TYPE_PREFIX_APPEND, "appended2-", 0 },
+ { .type = TYPE_END }
+ },
+ .global_log_prefix = "global4.",
+ .base_send_prefix = "PREFIX: ",
+ .base_str_prefix = "STR_PREFIX: ",
+ .result = "global4.Info: [amended1-PREFIX: appended1-"
+ "[amended2-appended2-TEXT]]",
+ .result_str_out = "STR_PREFIX: "
+ "appended1-[amended2-appended2-TEXT]",
+ },
+ {
+ .prefixes = (const struct test_log_event []) {
+ { TYPE_MESSAGE_AMEND, "amended1-", 0 },
+ { TYPE_PREFIX_APPEND, "appended1-",
+ FLAG_BASE_EVENT },
+ { TYPE_MESSAGE_AMEND, "amended2-", 0 },
+ { TYPE_PREFIX_APPEND, "appended2-", 0 },
+ { .type = TYPE_END }
+ },
+ .global_log_prefix = "global4.",
+ .base_send_prefix = "PREFIX: ",
+ .base_str_prefix = "STR_PREFIX: ",
+ .result = "global4.Info: [amended1-appended1-PREFIX: "
+ "[amended2-appended2-TEXT]]",
+ .result_str_out = "STR_PREFIX: "
+ "[amended2-appended2-TEXT]",
+ },
+ {
+ .prefixes = (const struct test_log_event []) {
+ { TYPE_MESSAGE_AMEND, "amended1-", 0 },
+ { TYPE_PREFIX_APPEND, "appended1-", 0 },
+ { TYPE_MESSAGE_AMEND, "amended2-",
+ FLAG_BASE_EVENT },
+ { TYPE_PREFIX_APPEND, "appended2-", 0 },
+ { .type = TYPE_END }
+ },
+ .global_log_prefix = "global4.",
+ .base_send_prefix = "PREFIX: ",
+ .base_str_prefix = "STR_PREFIX: ",
+ .result = "global4.Info: [amended1-appended1-"
+ "[amended2-PREFIX: appended2-TEXT]]",
+ .result_str_out = "STR_PREFIX: appended2-TEXT",
+ },
+ {
+ .prefixes = (const struct test_log_event []) {
+ { TYPE_MESSAGE_AMEND, "amended1-", 0 },
+ { TYPE_PREFIX_APPEND, "appended1-", 0 },
+ { TYPE_MESSAGE_AMEND, "amended2-", 0 },
+ { TYPE_PREFIX_APPEND, "appended2-",
+ FLAG_BASE_EVENT },
+ { .type = TYPE_END }
+ },
+ .global_log_prefix = "global4.",
+ .base_send_prefix = "PREFIX: ",
+ .base_str_prefix = "STR_PREFIX: ",
+ .result = "global4.Info: [amended1-appended1-"
+ "[amended2-appended2-PREFIX: TEXT]]",
+ .result_str_out = "STR_PREFIX: TEXT",
+ },
+ {
+ .prefixes = (const struct test_log_event []) {
+ { TYPE_PREFIX_REPLACE, "replaced1,", 0 },
+ { TYPE_MESSAGE_AMEND, "amended1-", 0 },
+ { .type = TYPE_END }
+ },
+ .base_send_prefix = "PREFIX: ",
+ .base_str_prefix = "STR_PREFIX: ",
+ .result = "replaced1,Info: [amended1-TEXT]",
+ .result_str_out = "[amended1-TEXT]",
+ },
+ {
+ .prefixes = (const struct test_log_event []) {
+ { TYPE_PREFIX_REPLACE, "replaced1,",
+ FLAG_BASE_EVENT },
+ { TYPE_MESSAGE_AMEND, "amended1-", 0 },
+ { .type = TYPE_END }
+ },
+ .base_send_prefix = "PREFIX: ",
+ .base_str_prefix = "STR_PREFIX: ",
+ .result = "replaced1,Info: PREFIX: [amended1-TEXT]",
+ .result_str_out = "STR_PREFIX: [amended1-TEXT]",
+ },
+ {
+ .prefixes = (const struct test_log_event []) {
+ { TYPE_PREFIX_REPLACE, "replaced1,", 0 },
+ { TYPE_MESSAGE_AMEND, "amended1-",
+ FLAG_BASE_EVENT },
+ { .type = TYPE_END }
+ },
+ .base_send_prefix = "PREFIX: ",
+ .base_str_prefix = "STR_PREFIX: ",
+ .result = "replaced1,Info: [amended1-PREFIX: TEXT]",
+ .result_str_out = "STR_PREFIX: TEXT",
+ },
+ {
+ .prefixes = (const struct test_log_event []) {
+ { TYPE_PREFIX_REPLACE, "replaced1,", 0 },
+ { TYPE_MESSAGE_AMEND, "amended1-", 0 },
+ { TYPE_PREFIX_REPLACE, "replaced2,", 0 },
+ { TYPE_MESSAGE_AMEND, "amended2-", 0 },
+ { .type = TYPE_END }
+ },
+ .base_send_prefix = "PREFIX: ",
+ .base_str_prefix = "STR_PREFIX: ",
+ .result = "replaced2,Info: [amended2-TEXT]",
+ .result_str_out = "[amended2-TEXT]",
+ },
+ {
+ .prefixes = (const struct test_log_event []) {
+ { TYPE_PREFIX_REPLACE, "replaced1,",
+ FLAG_BASE_EVENT },
+ { TYPE_MESSAGE_AMEND, "amended1-", 0 },
+ { TYPE_PREFIX_REPLACE, "replaced2,", 0 },
+ { TYPE_MESSAGE_AMEND, "amended2-", 0 },
+ { .type = TYPE_END }
+ },
+ .base_send_prefix = "PREFIX: ",
+ .base_str_prefix = "STR_PREFIX: ",
+ .result = "replaced2,Info: [amended2-TEXT]",
+ .result_str_out = "[amended2-TEXT]",
+ },
+ {
+ .prefixes = (const struct test_log_event []) {
+ { TYPE_PREFIX_REPLACE, "replaced1,", 0 },
+ { TYPE_MESSAGE_AMEND, "amended1-",
+ FLAG_BASE_EVENT },
+ { TYPE_PREFIX_REPLACE, "replaced2,", 0 },
+ { TYPE_MESSAGE_AMEND, "amended2-", 0 },
+ { .type = TYPE_END }
+ },
+ .base_send_prefix = "PREFIX: ",
+ .base_str_prefix = "STR_PREFIX: ",
+ .result = "replaced2,Info: [amended2-TEXT]",
+ .result_str_out = "[amended2-TEXT]",
+ },
+ {
+ .prefixes = (const struct test_log_event []) {
+ { TYPE_PREFIX_REPLACE, "replaced1,", 0 },
+ { TYPE_MESSAGE_AMEND, "amended1-", 0 },
+ { TYPE_PREFIX_REPLACE, "replaced2,",
+ FLAG_BASE_EVENT },
+ { TYPE_MESSAGE_AMEND, "amended2-", 0 },
+ { .type = TYPE_END }
+ },
+ .base_send_prefix = "PREFIX: ",
+ .base_str_prefix = "STR_PREFIX: ",
+ .result = "replaced2,Info: PREFIX: [amended2-TEXT]",
+ .result_str_out = "STR_PREFIX: [amended2-TEXT]",
+ },
+ {
+ .prefixes = (const struct test_log_event []) {
+ { TYPE_PREFIX_REPLACE, "replaced1,", 0 },
+ { TYPE_MESSAGE_AMEND, "amended1-", 0 },
+ { TYPE_PREFIX_REPLACE, "replaced2,", 0 },
+ { TYPE_MESSAGE_AMEND, "amended2-",
+ FLAG_BASE_EVENT },
+ { .type = TYPE_END }
+ },
+ .base_send_prefix = "PREFIX: ",
+ .base_str_prefix = "STR_PREFIX: ",
+ .result = "replaced2,Info: [amended2-PREFIX: TEXT]",
+ .result_str_out = "STR_PREFIX: TEXT",
+ },
+ /* Tests in which parent log prefixes are dropped by an event
+ lower in the hierarchy. */
+ {
+ .prefixes = (const struct test_log_event []) {
+ { TYPE_PREFIX_APPEND, "appended1,", 0 },
+ { TYPE_PREFIX_APPEND, "appended2.", 0 },
+ { TYPE_PREFIX_APPEND, "appended3.", 0 },
+ { TYPE_PREFIX_APPEND, "appended4.", 0 },
+ { TYPE_PREFIX_APPEND, "appended5.", 0 },
+ { .type = TYPE_END }
+ },
+ .global_log_prefix = "global3.",
+ .result = "global3.Info: "
+ "appended1,appended2.appended3."
+ "appended4.appended5.TEXT",
+ .result_str_out = "appended1,appended2.appended3."
+ "appended4.appended5.TEXT",
+ },
+ {
+ .prefixes = (const struct test_log_event []) {
+ { TYPE_PREFIX_APPEND, "appended1,", 0 },
+ { TYPE_PREFIX_APPEND, "appended2.", 0 },
+ { TYPE_PREFIX_APPEND, "appended3.", 0 },
+ { TYPE_PREFIX_APPEND, "appended4.", 0 },
+ { TYPE_PREFIX_APPEND, "appended5.",
+ FLAG_DROP_PREFIXES_1 },
+ { .type = TYPE_END }
+ },
+ .global_log_prefix = "global3.",
+ .result = "global3.Info: "
+ "appended1,appended2.appended3."
+ "appended5.TEXT",
+ .result_str_out = "appended1,appended2.appended3."
+ "appended5.TEXT",
+ },
+ {
+ .prefixes = (const struct test_log_event []) {
+ { TYPE_PREFIX_APPEND, "appended1,", 0 },
+ { TYPE_PREFIX_APPEND, "appended2.", 0 },
+ { TYPE_PREFIX_APPEND, "appended3.", 0 },
+ { TYPE_PREFIX_APPEND, "appended4.", 0 },
+ { TYPE_PREFIX_APPEND, "appended5.", 0 },
+ { TYPE_SKIP, NULL, FLAG_DROP_PREFIXES_1 },
+ { .type = TYPE_END }
+ },
+ .global_log_prefix = "global3.",
+ .result = "global3.Info: "
+ "appended1,appended2.appended3."
+ "appended4.TEXT",
+ .result_str_out = "appended1,appended2.appended3."
+ "appended4.TEXT",
+ },
+ {
+ .prefixes = (const struct test_log_event []) {
+ { TYPE_PREFIX_APPEND, "appended1,", 0 },
+ { TYPE_PREFIX_APPEND, "appended2.", 0 },
+ { TYPE_PREFIX_APPEND, "appended3.", 0 },
+ { TYPE_PREFIX_APPEND, "appended4.", 0 },
+ { TYPE_PREFIX_APPEND, "appended5.",
+ FLAG_DROP_PREFIXES_2 },
+ { .type = TYPE_END }
+ },
+ .global_log_prefix = "global3.",
+ .result = "global3.Info: "
+ "appended1,appended2.appended5.TEXT",
+ .result_str_out = "appended1,appended2.appended5.TEXT",
+ },
+ {
+ .prefixes = (const struct test_log_event []) {
+ { TYPE_PREFIX_APPEND, "appended1,", 0 },
+ { TYPE_PREFIX_APPEND, "appended2.", 0 },
+ { TYPE_PREFIX_APPEND, "appended3.", 0 },
+ { TYPE_PREFIX_APPEND, "appended4.", 0 },
+ { TYPE_PREFIX_APPEND, "appended5.",
+ (FLAG_DROP_PREFIXES_1 |
+ FLAG_DROP_PREFIXES_2) },
+ { .type = TYPE_END }
+ },
+ .global_log_prefix = "global3.",
+ .result = "global3.Info: appended1,appended5.TEXT",
+ .result_str_out = "appended1,appended5.TEXT",
+ },
+ {
+ .prefixes = (const struct test_log_event []) {
+ { TYPE_PREFIX_APPEND, "appended1,", 0 },
+ { TYPE_PREFIX_APPEND, "appended2.", 0 },
+ { TYPE_PREFIX_APPEND, "appended3.", 0 },
+ { TYPE_PREFIX_APPEND, "appended4.", 0 },
+ { TYPE_PREFIX_APPEND, "appended5.",
+ FLAG_DROP_PREFIXES_4 },
+ { .type = TYPE_END }
+ },
+ .global_log_prefix = "global3.",
+ .result = "global3.Info: appended5.TEXT",
+ .result_str_out = "appended5.TEXT",
+ },
+ {
+ .prefixes = (const struct test_log_event []) {
+ { TYPE_PREFIX_APPEND, "appended1,", 0 },
+ { TYPE_PREFIX_APPEND, "appended2.", 0 },
+ { TYPE_PREFIX_APPEND, "appended3.", 0 },
+ { TYPE_PREFIX_APPEND, "appended4.", 0 },
+ { TYPE_PREFIX_APPEND, "appended5.",
+ (FLAG_DROP_PREFIXES_1 |
+ FLAG_DROP_PREFIXES_2 |
+ FLAG_DROP_PREFIXES_4) },
+ { .type = TYPE_END }
+ },
+ .global_log_prefix = "global3.",
+ .result = "global3.Info: appended5.TEXT",
+ .result_str_out = "appended5.TEXT",
+ },
+ {
+ .prefixes = (const struct test_log_event []) {
+ { TYPE_PREFIX_APPEND, "appended1,", 0 },
+ { TYPE_PREFIX_APPEND, "appended2.", 0 },
+ { TYPE_PREFIX_APPEND, "appended3.", 0 },
+ { TYPE_PREFIX_APPEND, "appended4.", 0 },
+ { TYPE_PREFIX_APPEND, "appended5.", 0 },
+ { TYPE_SKIP, NULL, (FLAG_DROP_PREFIXES_1 |
+ FLAG_DROP_PREFIXES_2 |
+ FLAG_DROP_PREFIXES_4) },
+ { .type = TYPE_END }
+ },
+ .global_log_prefix = "global3.",
+ .result = "global3.Info: TEXT",
+ .result_str_out = "TEXT",
+ },
+ {
+ .prefixes = (const struct test_log_event []) {
+ { TYPE_PREFIX_APPEND, "appended1,", 0 },
+ { TYPE_PREFIX_APPEND, "appended2.",
+ FLAG_DROP_PREFIXES_1 },
+ { TYPE_PREFIX_APPEND, "appended3.", 0 },
+ { TYPE_PREFIX_APPEND, "appended4.", 0 },
+ { TYPE_PREFIX_APPEND, "appended5.",
+ FLAG_DROP_PREFIXES_1 },
+ { .type = TYPE_END }
+ },
+ .global_log_prefix = "global3.",
+ .result = "global3.Info: "
+ "appended2.appended3.appended5.TEXT",
+ .result_str_out = "appended2.appended3.appended5.TEXT",
+ },
+ {
+ .prefixes = (const struct test_log_event []) {
+ { TYPE_PREFIX_APPEND, "appended1,",
+ FLAG_DROP_PREFIXES_1 },
+ { TYPE_PREFIX_APPEND, "appended2.",
+ FLAG_DROP_PREFIXES_1 },
+ { TYPE_PREFIX_APPEND, "appended3.",
+ FLAG_DROP_PREFIXES_1 },
+ { TYPE_PREFIX_APPEND, "appended4.",
+ FLAG_DROP_PREFIXES_1 },
+ { TYPE_PREFIX_APPEND, "appended5.",
+ FLAG_DROP_PREFIXES_1 },
+ { .type = TYPE_END }
+ },
+ .global_log_prefix = "global3.",
+ .result = "global3.Info: appended5.TEXT",
+ .result_str_out = "appended5.TEXT",
+ },
+ {
+ .prefixes = (const struct test_log_event []) {
+ { TYPE_PREFIX_APPEND, "appended1,", 0 },
+ { .type = TYPE_SKIP },
+ { TYPE_PREFIX_APPEND, "appended2.",
+ FLAG_DROP_PREFIXES_1 },
+ { .type = TYPE_SKIP },
+ { TYPE_PREFIX_APPEND, "appended3.", 0 },
+ { .type = TYPE_SKIP },
+ { TYPE_PREFIX_APPEND, "appended4.", 0 },
+ { .type = TYPE_SKIP },
+ { TYPE_PREFIX_APPEND, "appended5.",
+ FLAG_DROP_PREFIXES_1 },
+ { .type = TYPE_SKIP },
+ { .type = TYPE_END }
+ },
+ .global_log_prefix = "global3.",
+ .result = "global3.Info: "
+ "appended2.appended3.appended5.TEXT",
+ .result_str_out = "appended2.appended3.appended5.TEXT",
+ },
+ {
+ .prefixes = (const struct test_log_event []) {
+ { TYPE_PREFIX_APPEND, "appended1,", 0 },
+ { TYPE_PREFIX_APPEND, "appended2.", 0 },
+ { TYPE_PREFIX_APPEND, "appended3.",
+ FLAG_DROP_PREFIXES_1 },
+ { TYPE_PREFIX_APPEND, "appended4.", 0 },
+ { TYPE_PREFIX_APPEND, "appended5.",
+ (FLAG_DROP_PREFIXES_1 |
+ FLAG_DROP_PREFIXES_2) },
+ { .type = TYPE_END }
+ },
+ .global_log_prefix = "global3.",
+ .result = "global3.Info: appended5.TEXT",
+ .result_str_out = "appended5.TEXT",
+ },
+ };
+
+ test_begin("event log message");
+
+ failure_callback_t *orig_fatal, *orig_error, *orig_info, *orig_debug;
+ i_get_failure_handlers(&orig_fatal, &orig_error, &orig_info, &orig_debug);
+ i_set_info_handler(info_handler);
+ for (unsigned int i = 0; i < N_ELEMENTS(tests); i++) T_BEGIN {
+ const struct test_log *test = &tests[i];
+ struct event_log_params params = {
+ .log_type = LOG_TYPE_INFO,
+ .base_send_prefix = test->base_send_prefix,
+ .base_str_prefix = test->base_str_prefix,
+ .no_send = ((test->flags & FLAG_NO_SEND) != 0),
+ };
+
+ i_free(test_output);
+ if (test->global_log_prefix != NULL)
+ i_set_failure_prefix("%s", test->global_log_prefix);
+ else
+ i_set_failure_prefix("UNEXPECTED GLOBAL PREFIX");
+
+ struct event *event, *parent;
+ event = parent = event_create(NULL);
+ for (unsigned int j = 0; test->prefixes[j].type != TYPE_END; j++) {
+ unsigned int drop_prefixes = 0;
+
+ if (event == NULL) {
+ struct event *child = event_create(parent);
+ event_unref(&parent);
+ event = parent = child;
+ }
+ if ((test->prefixes[j].flags & FLAG_BASE_EVENT) != 0) {
+ i_assert(params.base_event == NULL);
+ params.base_event = event;
+ }
+ if ((test->prefixes[j].flags &
+ FLAG_DROP_PREFIXES_1) != 0)
+ drop_prefixes += 1;
+ if ((test->prefixes[j].flags &
+ FLAG_DROP_PREFIXES_2) != 0)
+ drop_prefixes += 2;
+ if ((test->prefixes[j].flags &
+ FLAG_DROP_PREFIXES_4) != 0)
+ drop_prefixes += 4;
+ event_drop_parent_log_prefixes(event, drop_prefixes);
+
+ switch (test->prefixes[j].type) {
+ case TYPE_END:
+ i_unreached();
+ case TYPE_PREFIX_APPEND:
+ event_set_append_log_prefix(event, test->prefixes[j].str);
+ break;
+ case TYPE_PREFIX_REPLACE:
+ event_replace_log_prefix(event, test->prefixes[j].str);
+ break;
+ case TYPE_PREFIX_APPEND_CB:
+ event_set_log_prefix_callback(event, FALSE,
+ test_event_log_prefix_cb,
+ (char*)test->prefixes[j].str);
+ break;
+ case TYPE_PREFIX_REPLACE_CB:
+ event_set_log_prefix_callback(event, TRUE,
+ test_event_log_prefix_cb,
+ (char*)test->prefixes[j].str);
+ break;
+ case TYPE_MESSAGE_AMEND:
+ event_set_log_message_callback(event,
+ test_event_log_message_cb,
+ (char*)test->prefixes[j].str);
+ break;
+ case TYPE_SKIP:
+ break;
+ }
+ event = NULL;
+ }
+ event = parent;
+
+ if (test->result_str_out != NULL) {
+ /* Use small value so buffer size grows. This way the
+ unit test fails if anyone attempts to add data-stack
+ frame to event_log(). */
+ params.base_str_out = t_str_new(1);
+ }
+ event_log(event, &params, "TEXT");
+
+ test_assert_strcmp(test->result, test_output);
+ if (test->result_str_out != NULL) {
+ test_assert_strcmp(test->result_str_out,
+ str_c(params.base_str_out));
+ }
+ event_unref(&event);
+ } T_END;
+ i_set_info_handler(orig_info);
+ i_unset_failure_prefix();
+ i_free(test_output);
+ test_end();
+}
+
+static void test_event_duration()
+{
+ uintmax_t duration;
+ test_begin("event duration");
+ struct event *e = event_create(NULL);
+ usleep(10);
+ e_info(e, "Submit event");
+ event_get_last_duration(e, &duration);
+ test_assert(duration > 0);
+ event_unref(&e);
+ test_end();
+}
+
+static void test_event_log_level(void)
+{
+ test_begin("event log level");
+ failure_callback_t *orig_fatal, *orig_error, *orig_info, *orig_debug;
+ i_get_failure_handlers(&orig_fatal, &orig_error, &orig_info, &orig_debug);
+ i_set_info_handler(info_handler);
+ i_set_error_handler(error_handler);
+
+ struct event *event = event_create(NULL);
+ event_set_min_log_level(event, LOG_TYPE_WARNING);
+ errno = EACCES;
+ e_info(event, "Info event");
+ test_assert(test_output == NULL);
+ e_warning(event, "Warning event");
+ test_assert_strcmp(test_output, "Warning: Warning event");
+ event_unref(&event);
+ i_set_info_handler(orig_info);
+ i_set_error_handler(orig_error);
+ i_free(test_output);
+ test_assert(errno == EACCES);
+ test_end();
+}
+
+void test_event_log(void)
+{
+ test_event_log_message();
+ test_event_duration();
+ test_event_log_level();
+}
diff --git a/src/lib/test-failures.c b/src/lib/test-failures.c
new file mode 100644
index 0000000..c76d5b8
--- /dev/null
+++ b/src/lib/test-failures.c
@@ -0,0 +1,176 @@
+/* Copyright (c) 2001-2018 Dovecot authors, see the included COPYING file */
+
+/* Unit tests for failure helpers */
+
+#include "test-lib.h"
+#include "hostpid.h"
+#include "istream.h"
+#include "failures.h"
+
+#include <unistd.h>
+
+static int handlers_set_me;
+
+static void test_failures_handler(const struct failure_context *ctx,
+ const char *format ATTR_UNUSED,
+ va_list args ATTR_UNUSED)
+{
+ handlers_set_me = ctx->type;
+}
+static void test_get_set_handlers(void)
+{
+ failure_callback_t *handlers[4];
+ test_begin("get_handlers");
+ i_get_failure_handlers(handlers, handlers+1, handlers+2, handlers+3);
+ test_end();
+
+ test_begin("set_handlers");
+
+ i_set_debug_handler(&test_failures_handler);
+ i_debug("If you see this debug, something's gone wrong");
+ test_assert(handlers_set_me == LOG_TYPE_DEBUG);
+ i_set_debug_handler(handlers[3]);
+
+ i_set_info_handler(&test_failures_handler);
+ i_info("If you see this info, something's gone wrong");
+ test_assert(handlers_set_me == LOG_TYPE_INFO);
+ i_set_info_handler(handlers[2]);
+
+ i_set_error_handler(&test_failures_handler);
+ i_warning("If you see this warning, something's gone wrong");
+ test_assert(handlers_set_me == LOG_TYPE_WARNING);
+ i_error("If you see this error, something's gone wrong");
+ test_assert(handlers_set_me == LOG_TYPE_ERROR);
+ i_set_error_handler(handlers[1]);
+
+ //i_set_fatal_handler(&test_failures_handler);
+ //i_fatal("If you see this fatal, something's gone wrong");
+ //test_assert(handlers_set_me == LOG_TYPE_FATAL);
+ //i_set_fatal_handler(handlers[0]);
+
+ test_end();
+}
+static void test_expected(void)
+{
+ test_begin("expected messages");
+ test_expect_errors(1);
+ i_warning("deliberate warning - not suppressed");
+ test_expect_no_more_errors();
+ test_end();
+}
+static void test_expected_str(void)
+{
+ test_begin("expected strings in messages");
+ test_expect_error_string("be unhappy");
+ i_error("deliberate error - suppressed - be unhappy if you see this");
+ test_expect_no_more_errors();
+ test_end();
+}
+
+static bool
+internal_line_match(const char *line, const char *prefix, const char *text)
+{
+ if (line == NULL)
+ return FALSE;
+
+ if (line[0] != '\001')
+ return FALSE;
+ uint8_t type = (uint8_t)line[1];
+ if (type != ((LOG_TYPE_DEBUG+1) | 0x80))
+ return FALSE;
+ line += 2;
+
+ if (!str_begins(line, "123 "))
+ return FALSE;
+ line += 4;
+
+ if (!str_begins(line, prefix))
+ return FALSE;
+ line += strlen(prefix);
+
+ return strcmp(line, text) == 0;
+}
+
+static void test_internal_split(void)
+{
+ int fd[2];
+
+ test_begin("splitting long internal log lines");
+
+ char long_log_prefix[PIPE_BUF+1];
+ memset(long_log_prefix, 'X', sizeof(long_log_prefix)-1);
+ long_log_prefix[sizeof(long_log_prefix)-1] = '\0';
+
+#define TEXT10 "tttttttttt"
+#define TEXT128 TEXT10 TEXT10 TEXT10 TEXT10 TEXT10 TEXT10 TEXT10 TEXT10 \
+ TEXT10 TEXT10 TEXT10 TEXT10 "tttttttt"
+
+ char long_lext[PIPE_BUF*2+1];
+ memset(long_lext, 'T', sizeof(long_lext)-1);
+ long_lext[sizeof(long_lext)-1] = '\0';
+
+ if (pipe(fd) < 0)
+ i_fatal("pipe() failed: %m");
+ switch (fork()) {
+ case (pid_t)-1:
+ i_fatal("fork() failed: %m");
+ case 0:
+ /* child - log writer */
+ if (dup2(fd[1], STDERR_FILENO) < 0)
+ i_fatal("dup2() failed: %m");
+ i_close_fd(&fd[0]);
+ i_close_fd(&fd[1]);
+
+ struct failure_context ctx = {
+ .type = LOG_TYPE_DEBUG,
+ .log_prefix = long_log_prefix,
+ };
+
+ i_set_failure_internal();
+ my_pid = "123";
+ i_log_type(&ctx, "little text");
+ i_log_type(&ctx, TEXT128 TEXT128 TEXT128);
+ ctx.log_prefix = "";
+ i_log_type(&ctx, "%s", long_lext);
+ test_exit(0);
+ case 1:
+ /* parent - log reader */
+ i_close_fd(&fd[1]);
+ break;
+ }
+
+ alarm(10);
+ struct istream *input = i_stream_create_fd(fd[0], SIZE_MAX);
+
+ /* long prefix, little text */
+ const char *line = i_stream_read_next_line(input);
+ test_assert(internal_line_match(line, long_log_prefix, "little text"));
+
+ /* long prefix, text split to multiple lines */
+ for (unsigned int i = 0; i < 3; i++) {
+ line = i_stream_read_next_line(input);
+ test_assert(internal_line_match(line, long_log_prefix, TEXT128));
+ }
+
+ /* no prefix, just lots of text */
+ line = i_stream_read_next_line(input);
+ long_lext[PIPE_BUF-7] = '\0';
+ test_assert(internal_line_match(line, "", long_lext));
+ line = i_stream_read_next_line(input);
+ test_assert(internal_line_match(line, "", long_lext));
+ line = i_stream_read_next_line(input);
+ test_assert(internal_line_match(line, "", "TTTTTTTTTTTTTT"));
+
+ i_stream_unref(&input);
+ alarm(0);
+
+ test_end();
+}
+
+void test_failures(void)
+{
+ test_get_set_handlers();
+ test_expected();
+ test_expected_str();
+ test_internal_split();
+}
diff --git a/src/lib/test-fd-util.c b/src/lib/test-fd-util.c
new file mode 100644
index 0000000..1058eca
--- /dev/null
+++ b/src/lib/test-fd-util.c
@@ -0,0 +1,24 @@
+/* Copyright (c) 2019 Dovecot authors, see the included COPYING file */
+
+#include "test-lib.h"
+#include "fd-util.h"
+
+enum fatal_test_state fatal_i_close(unsigned int stage)
+{
+ if (stage == 0) {
+ test_begin("fatal i_close");
+ } else {
+ test_end();
+ return FATAL_TEST_FINISHED;
+ }
+
+ int fd = 0;
+ const char *fatal_string = t_strdup_printf(
+ "%s: close((&fd)) @ %s:%d attempted with fd=%d",
+ __func__, __FILE__, __LINE__ + 2, fd);
+ test_expect_fatal_string(fatal_string);
+ i_close_fd(&fd);
+
+ /* This cannot be reached. */
+ return FATAL_TEST_ABORT;
+}
diff --git a/src/lib/test-file-cache.c b/src/lib/test-file-cache.c
new file mode 100644
index 0000000..6bac9ab
--- /dev/null
+++ b/src/lib/test-file-cache.c
@@ -0,0 +1,293 @@
+/* Copyright (c) 2020 Dovecot authors, see the included COPYING file */
+
+#include "test-lib.h"
+#include "istream.h"
+#include "ostream.h"
+#include "file-cache.h"
+
+#include <sys/resource.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <fcntl.h>
+#include <unistd.h>
+#include <sys/mman.h>
+
+#define TEST_FILENAME ".test_file_cache"
+
+static void test_file_cache_read(void)
+{
+ test_begin("file_cache_read");
+
+ /* create a file */
+ struct ostream *os = o_stream_create_file(TEST_FILENAME, 0, 0600, 0);
+ o_stream_nsend_str(os, "initial data\n");
+ test_assert(o_stream_finish(os) == 1);
+ o_stream_destroy(&os);
+
+ int fd = open(TEST_FILENAME, O_RDONLY);
+ i_assert(fd > -1);
+ struct file_cache *cache = file_cache_new_path(fd, TEST_FILENAME);
+
+ /* this should be 0 before read */
+ size_t size;
+ const unsigned char *map = file_cache_get_map(cache, &size);
+ test_assert(map == NULL);
+ test_assert(size == 0);
+ test_assert(map == NULL);
+
+ test_assert(file_cache_read(cache, 0, 13) == 13);
+ map = file_cache_get_map(cache, &size);
+ test_assert(map != NULL && size == 13 && memcmp(map, "initial data\n", 13) == 0);
+
+ file_cache_free(&cache);
+ i_close_fd(&fd);
+ i_unlink(TEST_FILENAME);
+
+ test_end();
+}
+
+static void test_file_cache_write_read(void)
+{
+ test_begin("file_cache_write_read");
+
+ /* create a file */
+ struct ostream *os = o_stream_create_file(TEST_FILENAME, 0, 0600, 0);
+ o_stream_nsend_str(os, "initial data\n");
+ test_assert(o_stream_finish(os) == 1);
+ o_stream_destroy(&os);
+
+ int fd = open(TEST_FILENAME, O_RDONLY);
+ i_assert(fd > -1);
+ struct file_cache *cache = file_cache_new_path(fd, TEST_FILENAME);
+
+ /* this should be 0 before read */
+ size_t size;
+ const unsigned char *map = file_cache_get_map(cache, &size);
+ test_assert(map == NULL);
+ test_assert(size == 0);
+ test_assert(map == NULL);
+ test_assert(file_cache_read(cache, 0, 13) == 13);
+ file_cache_write(cache, "updated data\n", 13, 0);
+ map = file_cache_get_map(cache, &size);
+ test_assert(map != NULL && size == 13 && memcmp(map, "updated data\n", 13) == 0);
+ file_cache_free(&cache);
+ i_close_fd(&fd);
+
+ struct istream *is = i_stream_create_file(TEST_FILENAME, SIZE_MAX);
+ const unsigned char *data;
+ test_assert(i_stream_read_more(is, &data, &size) > 0 && size == 13);
+ test_assert(map != NULL && size == 13 && memcmp(data, "initial data\n", 13) == 0);
+ i_stream_destroy(&is);
+ i_unlink(TEST_FILENAME);
+
+ test_end();
+}
+
+static void test_file_cache_read_invalidate(void)
+{
+ test_begin("file_cache_read_invalidate");
+
+ /* create a file */
+ struct ostream *os = o_stream_create_file(TEST_FILENAME, 0, 0600, 0);
+ o_stream_nsend_str(os, "initial data\n");
+ test_assert(o_stream_finish(os) == 1);
+ o_stream_destroy(&os);
+
+ int fd = open(TEST_FILENAME, O_RDONLY);
+ i_assert(fd > -1);
+ struct file_cache *cache = file_cache_new_path(fd, TEST_FILENAME);
+
+ /* this should be 0 before read */
+ size_t size;
+ test_assert(file_cache_read(cache, 0, 13) == 13);
+ const unsigned char *map = file_cache_get_map(cache, &size);
+ test_assert(map != NULL && size == 13 && memcmp(map, "initial data\n", 13) == 0);
+
+ /* update file */
+ os = o_stream_create_file(TEST_FILENAME, 0, 0600, 0);
+ o_stream_nsend_str(os, "updated data\n");
+ test_assert(o_stream_finish(os) == 1);
+ o_stream_destroy(&os);
+
+ map = file_cache_get_map(cache, &size);
+ test_assert(map != NULL && size == 13 && memcmp(map, "initial data\n", 13) == 0);
+
+ /* invalidate cache */
+ file_cache_invalidate(cache, 0, size);
+ test_assert(file_cache_read(cache, 0, 13) == 13);
+ map = file_cache_get_map(cache, &size);
+ test_assert(size == 13);
+ test_assert(map != NULL && size == 13 && memcmp(map, "updated data\n", 13) == 0);
+ file_cache_free(&cache);
+ i_close_fd(&fd);
+ i_unlink(TEST_FILENAME);
+
+ test_end();
+}
+
+static void test_file_cache_multipage(void)
+{
+ test_begin("file_cache_multipage");
+
+ size_t page_size = getpagesize();
+ struct ostream *os = o_stream_create_file(TEST_FILENAME, 0, 0600, 0);
+ size_t total_size = 0;
+ for (size_t i = 0; i < page_size * 3 + 100; i += 12) {
+ o_stream_nsend_str(os, "initial data");
+ total_size += 12;
+ }
+ test_assert(o_stream_finish(os) == 1);
+ o_stream_destroy(&os);
+
+ int fd = open(TEST_FILENAME, O_RDONLY);
+ i_assert(fd > -1);
+ struct file_cache *cache = file_cache_new_path(fd, TEST_FILENAME);
+
+ /* read everything to memory page at a time */
+ test_assert(file_cache_read(cache, 0, page_size) == (ssize_t)page_size);
+ test_assert(file_cache_read(cache, page_size, page_size) ==
+ (ssize_t)page_size);
+ test_assert(file_cache_read(cache, page_size*2, page_size) ==
+ (ssize_t)page_size);
+ test_assert(file_cache_read(cache, page_size*3, page_size) ==
+ (ssize_t)total_size-(ssize_t)page_size*3);
+
+ size_t size;
+ const unsigned char *map = file_cache_get_map(cache, &size);
+ test_assert(size == total_size);
+ test_assert(map != NULL);
+
+ /* write-read-invalidate-read */
+ for(size_t i = 0; i < page_size * 3; i+= page_size / 3) {
+ char orig[13];
+ const char *ptr = CONST_PTR_OFFSET(map, i);
+ memcpy(orig, ptr, 12);
+ orig[12] = '\0';
+ file_cache_write(cache, "updated data", 12, i);
+ map = file_cache_get_map(cache, &size);
+ ptr = CONST_PTR_OFFSET(map, i);
+ test_assert(strncmp(ptr, "updated data", 12) == 0);
+ /* invalidate cache */
+ file_cache_invalidate(cache, i, 12);
+ /* check that it's back what it was */
+ test_assert(file_cache_read(cache, i, 12) == 12);
+ map = file_cache_get_map(cache, &size);
+ ptr = CONST_PTR_OFFSET(map, i);
+ test_assert(strncmp(ptr, orig, 12) == 0);
+ }
+
+ file_cache_free(&cache);
+ i_close_fd(&fd);
+ i_unlink(TEST_FILENAME);
+ test_end();
+}
+
+static void test_file_cache_anon(void)
+{
+ /* file-cache should work as anonymous cache for small files */
+ test_begin("file_cache_anon");
+ test_assert(access(TEST_FILENAME, F_OK) == -1 && errno == ENOENT);
+ struct file_cache *cache = file_cache_new_path(-1, TEST_FILENAME);
+
+ test_assert(file_cache_set_size(cache, 1024) == 0);
+ file_cache_write(cache, "initial data", 12, 0);
+
+ size_t size;
+ const unsigned char *map = file_cache_get_map(cache, &size);
+ test_assert(map != NULL && size == 12 && memcmp(map, "initial data", 12) == 0);
+
+ file_cache_free(&cache);
+ i_unlink_if_exists(TEST_FILENAME);
+ test_end();
+}
+
+static void test_file_cache_switch_fd(void)
+{
+ test_begin("file_cache_switch_fd");
+ test_assert(access(TEST_FILENAME, F_OK) == -1 && errno == ENOENT);
+ struct file_cache *cache = file_cache_new_path(-1, TEST_FILENAME);
+
+ test_assert(file_cache_set_size(cache, 13) == 0);
+ file_cache_write(cache, "initial data\n", 13, 0);
+
+ /* create a file */
+ struct ostream *os = o_stream_create_file(TEST_FILENAME, 0, 0600, 0);
+ o_stream_nsend_str(os, "updated data\n");
+ test_assert(o_stream_finish(os) == 1);
+ o_stream_destroy(&os);
+
+ int fd = open(TEST_FILENAME, O_RDONLY);
+ i_assert(fd > -1);
+ /* map should be invalidated and updated data read
+ from given file */
+ file_cache_set_fd(cache, fd);
+ test_assert(file_cache_read(cache, 0, 13) == 13);
+ size_t size;
+ const unsigned char *map = file_cache_get_map(cache, &size);
+ test_assert(map != NULL && size == 13 && memcmp(map, "updated data\n", 13) == 0);
+
+ file_cache_free(&cache);
+ i_close_fd(&fd);
+ i_unlink(TEST_FILENAME);
+ test_end();
+}
+
+static void test_file_cache_errors(void)
+{
+ test_begin("file_cache_errors");
+
+ size_t page_size = getpagesize();
+
+ test_assert(access(TEST_FILENAME, F_OK) == -1 && errno == ENOENT);
+ int fd = open(TEST_FILENAME, O_RDONLY);
+ struct file_cache *cache = file_cache_new_path(fd, TEST_FILENAME);
+ size_t size;
+
+ /* file does not exist and we try large enough mapping */
+ test_expect_error_string("fstat(.test_file_cache) failed: "
+ "Bad file descriptor");
+ test_assert(file_cache_read(cache, 0, 2*1024*1024) == -1);
+ const unsigned char *map = file_cache_get_map(cache, &size);
+ test_assert(size == 0);
+ test_assert(map == NULL);
+
+ /* temporarily set a small memory limit to make mmap attempt fail */
+ struct rlimit rl_cur;
+ test_assert(getrlimit(RLIMIT_AS, &rl_cur) == 0);
+ struct rlimit rl_new = {
+ .rlim_cur = 1,
+ .rlim_max = rl_cur.rlim_max
+ };
+ const char *errstr =
+ t_strdup_printf("mmap_anon(.test_file_cache, %zu) failed: "
+ "Cannot allocate memory", page_size);
+ test_assert(setrlimit(RLIMIT_AS, &rl_new) == 0);
+ test_expect_error_string(errstr);
+ test_assert(file_cache_set_size(cache, 1024) == -1);
+ test_assert(setrlimit(RLIMIT_AS, &rl_cur) == 0);
+
+ /* same for mremap */
+ errstr = t_strdup_printf("mremap_anon(.test_file_cache, %zu) failed: "
+ "Cannot allocate memory", page_size*2);
+ test_assert(file_cache_set_size(cache, 1) == 0);
+ test_assert(setrlimit(RLIMIT_AS, &rl_new) == 0);
+ test_expect_error_string(errstr);
+ test_assert(file_cache_set_size(cache, page_size*2) == -1);
+ test_assert(setrlimit(RLIMIT_AS, &rl_cur) == 0);
+
+ file_cache_free(&cache);
+ i_close_fd(&fd);
+ i_unlink_if_exists(TEST_FILENAME);
+ test_end();
+}
+
+void test_file_cache(void)
+{
+ test_file_cache_read();
+ test_file_cache_write_read();
+ test_file_cache_read_invalidate();
+ test_file_cache_multipage();
+ test_file_cache_anon();
+ test_file_cache_switch_fd();
+ test_file_cache_errors();
+}
diff --git a/src/lib/test-file-create-locked.c b/src/lib/test-file-create-locked.c
new file mode 100644
index 0000000..f5b4f6a
--- /dev/null
+++ b/src/lib/test-file-create-locked.c
@@ -0,0 +1,142 @@
+/* Copyright (c) 2017-2018 Dovecot authors, see the included COPYING file */
+
+#include "test-lib.h"
+#include "unlink-directory.h"
+#include "file-create-locked.h"
+#include "sleep.h"
+
+#include <fcntl.h>
+#include <signal.h>
+#include <sys/stat.h>
+
+static void create_file(const char *path)
+{
+ int fd;
+
+ fd = creat(path, 0600);
+ if (fd == -1)
+ i_fatal("creat(%s) failed: %m", path);
+ i_close_fd(&fd);
+}
+
+static bool wait_for_file(pid_t pid, const char *path)
+{
+ struct stat st;
+
+ for (unsigned int i = 0; i < 1000; i++) {
+ if (stat(path, &st) == 0)
+ return TRUE;
+ if (errno != ENOENT)
+ i_fatal("stat(%s) failed: %m", path);
+ if (kill(pid, 0) < 0) {
+ if (errno == ESRCH)
+ return FALSE;
+ i_fatal("kill(SIGSRCH) failed: %m");
+ }
+ i_sleep_msecs(10);
+ }
+ i_error("%s isn't being created", path);
+ return FALSE;
+}
+
+static void test_file_create_locked_basic(void)
+{
+ struct file_create_settings set = {
+ .lock_timeout_secs = 0,
+ .lock_settings = {
+ .lock_method = FILE_LOCK_METHOD_FCNTL,
+ },
+ };
+ const char *path = ".test-file-create-locked";
+ struct file_lock *lock;
+ const char *error;
+ bool created;
+ pid_t pid;
+ int fd;
+
+ test_begin("file_create_locked()");
+
+ i_unlink_if_exists(path);
+ i_unlink_if_exists(".test-temp-file-create-locked-child");
+ pid = fork();
+ switch (pid) {
+ case (pid_t)-1:
+ i_error("fork() failed: %m");
+ break;
+ case 0:
+ /* child */
+ fd = file_create_locked(path, &set, &lock, &created, &error);
+ test_assert(fd > 0);
+ test_assert(created);
+ if (test_has_failed())
+ lib_exit(1);
+ create_file(".test-temp-file-create-locked-child");
+ sleep(60);
+ i_close_fd(&fd);
+ lib_exit(0);
+ default:
+ /* parent */
+ test_assert(wait_for_file(pid, ".test-temp-file-create-locked-child"));
+ if (test_has_failed())
+ break;
+ test_assert(file_create_locked(path, &set, &lock, &created, &error) == -1);
+ test_assert(errno == EAGAIN);
+ if (kill(pid, SIGKILL) < 0)
+ i_error("kill(SIGKILL) failed: %m");
+ break;
+ }
+ i_unlink_if_exists(".test-temp-file-create-locked-child");
+ i_unlink_if_exists(path);
+ test_end();
+}
+
+static void test_file_create_locked_mkdir(void)
+{
+ struct file_create_settings set = {
+ .lock_timeout_secs = 0,
+ .lock_settings = {
+ .lock_method = FILE_LOCK_METHOD_FCNTL,
+ },
+ };
+ const char *path;
+ struct file_lock *lock;
+ const char *error, *dir;
+ bool created;
+ int fd;
+
+ test_begin("file_create_locked() with mkdir");
+
+ dir = ".test-temp-file-create-locked-dir";
+ if (unlink_directory(dir, UNLINK_DIRECTORY_FLAG_RMDIR, &error) < 0)
+ i_fatal("unlink_directory(%s) failed: %s", dir, error);
+ path = t_strconcat(dir, "/lockfile", NULL);
+
+ /* try without mkdir enabled */
+ test_assert(file_create_locked(path, &set, &lock, &created, &error) == -1);
+ test_assert(errno == ENOENT);
+
+ /* try with mkdir enabled */
+ set.mkdir_mode = 0700;
+ fd = file_create_locked(path, &set, &lock, &created, &error);
+ test_assert(fd > 0);
+ test_assert(created);
+ i_close_fd(&fd);
+
+ struct stat st;
+ if (stat(dir, &st) < 0)
+ i_error("stat(%s) failed: %m", dir);
+ test_assert((st.st_mode & 0777) == 0700);
+ i_unlink(path);
+ file_lock_free(&lock);
+
+ if (unlink_directory(dir, UNLINK_DIRECTORY_FLAG_RMDIR, &error) < 0)
+ i_fatal("unlink_directory(%s) failed: %s", dir, error);
+
+ test_end();
+}
+
+void test_file_create_locked(void)
+{
+ test_file_create_locked_basic();
+ test_file_create_locked_mkdir();
+}
diff --git a/src/lib/test-guid.c b/src/lib/test-guid.c
new file mode 100644
index 0000000..0a13cf1
--- /dev/null
+++ b/src/lib/test-guid.c
@@ -0,0 +1,259 @@
+/* Copyright (c) 2014-2018 Dovecot authors, see the included COPYING file */
+
+#include "test-lib.h"
+#include "guid.h"
+#include "ioloop.h"
+
+/*
+ * We want earlier timestamps to compare as < with later timestamps, but
+ * guid_128_cmp() doesn't do that because the timestamps in the guid are
+ * stored in little-endian byte order.
+ */
+static int reverse_guid_128_cmp(const guid_128_t a, const guid_128_t b)
+{
+ int i;
+
+ for (i = GUID_128_SIZE - 1; i >= 0; i--)
+ if (a[i] != b[i])
+ return (int)a[i] - (int)b[i];
+
+ return 0;
+}
+
+static bool guid_128_has_sane_nsecs(const guid_128_t g)
+{
+ unsigned long nsecs;
+
+ nsecs = le32_to_cpu_unaligned(g);
+
+ return nsecs < 1000000000UL;
+}
+
+static inline void set_fake_time(time_t sec, long usec)
+{
+ ioloop_timeval.tv_sec = sec;
+ ioloop_timeval.tv_usec = usec;
+}
+
+/*
+ * We muck with the ioloop_timeval in various ways and make sure that the
+ * guids that get generated make sense. To make sure that the guid
+ * generation code takes up our faked timestamp, we use a far-away time (Jan
+ * 1 2038) as the base time. We don't want to go beyond 32-bit signed
+ * time_t for the base time to avoid issues on systems with 32-bit signed
+ * time_t.
+ *
+ * While guids really only need to be unique, here we actually enforce that
+ * they are increasing (as defined by reverse_guid_128_cmp()). If guids are
+ * always increasing, they will always be unique.
+ */
+static void test_ioloop_guid_128_generate(void)
+{
+ const time_t basetime = 2145909600; /* Jan 1 2038 */
+ struct timeval saved_ioloop_timeval;
+ guid_128_t guids[2];
+ int i;
+
+ /* save the ioloop_timeval before we start messing with it */
+ saved_ioloop_timeval = ioloop_timeval;
+
+ /*
+ * Generating multiple guids within a microsecond should keep
+ * incrementing them.
+ */
+ test_begin("guid_128_generate() increasing guid within a usec");
+ set_fake_time(basetime, 0);
+ guid_128_generate(guids[1]);
+ for (i = 0; i < 10; i++) {
+ const int this = i % 2;
+ const int prev = 1 - this;
+
+ guid_128_generate(guids[this]);
+
+ test_assert(reverse_guid_128_cmp(guids[prev], guids[this]) < 0);
+ test_assert(guid_128_has_sane_nsecs(guids[this]));
+ }
+ test_end();
+
+ /*
+ * If the current time changes by +1 usec, so should the guids.
+ */
+ test_begin("guid_128_generate() increasing guid with usec fast-forward");
+ for (i = 0; i < 10; i++) {
+ const int this = i % 2;
+ const int prev = 1 - this;
+
+ set_fake_time(basetime, 1 + i);
+ guid_128_generate(guids[this]);
+
+ test_assert(reverse_guid_128_cmp(guids[prev], guids[this]) < 0);
+ test_assert(guid_128_has_sane_nsecs(guids[this]));
+ }
+ test_end();
+
+ /*
+ * If the current time changes by +1 sec, so should the guids.
+ */
+ test_begin("guid_128_generate() increasing guid with sec fast-forward");
+ for (i = 0; i < 10; i++) {
+ const int this = i % 2;
+ const int prev = 1 - this;
+
+ set_fake_time(basetime + 1 + i, 0);
+ guid_128_generate(guids[this]);
+
+ test_assert(reverse_guid_128_cmp(guids[prev], guids[this]) < 0);
+ test_assert(guid_128_has_sane_nsecs(guids[this]));
+ }
+ test_end();
+
+ /*
+ * Requesting enough guids should increment the seconds but always
+ * produce valid nsecs.
+ *
+ * (Set a time that leaves us 1000 guids before seconds overflow and
+ * then ask for 2500 guids.)
+ */
+ test_begin("guid_128_generate() proper guid nsec overflow");
+ set_fake_time(basetime + 11, 999999L);
+ for (i = 0; i < 2500; i++) {
+ const int this = i % 2;
+ const int prev = 1 - this;
+
+ guid_128_generate(guids[this]);
+ test_assert(reverse_guid_128_cmp(guids[prev], guids[this]) < 0);
+ test_assert(guid_128_has_sane_nsecs(guids[this]));
+ }
+ test_end();
+
+ /*
+ * When ahead by 1500 guids (see previous test), +1 usec shouldn't
+ * have any effect.
+ */
+ test_begin("guid_128_generate() no effect with increasing time when ahead");
+ set_fake_time(basetime + 12, 0);
+ guid_128_generate(guids[0]);
+ test_assert(reverse_guid_128_cmp(guids[1], guids[0]) < 0);
+ test_assert(guid_128_has_sane_nsecs(guids[0]));
+ test_end();
+
+ /* not a test - just set a more convenient time */
+ set_fake_time(basetime + 15, 500);
+ guid_128_generate(guids[1]);
+ test_assert(reverse_guid_128_cmp(guids[0], guids[1]) < 0);
+ test_assert(guid_128_has_sane_nsecs(guids[1]));
+
+ /*
+ * Time going backwards by 1 usec should have no effect on guids.
+ */
+ test_begin("guid_128_generate() usec time-travel still increasing");
+ set_fake_time(basetime + 15, 499);
+ guid_128_generate(guids[0]);
+ test_assert(reverse_guid_128_cmp(guids[1], guids[0]) < 0);
+ test_assert(guid_128_has_sane_nsecs(guids[0]));
+ test_end();
+
+ /*
+ * Time going backwards by 1 sec should have no effect on guids.
+ */
+ test_begin("guid_128_generate() sec time-travel still increasing");
+ set_fake_time(basetime + 14, 499);
+ guid_128_generate(guids[1]);
+ test_assert(reverse_guid_128_cmp(guids[0], guids[1]) < 0);
+ test_assert(guid_128_has_sane_nsecs(guids[1]));
+ test_end();
+
+ /* restore the previously saved value just in case */
+ ioloop_timeval = saved_ioloop_timeval;
+}
+
+void test_guid(void)
+{
+ static const guid_128_t test_guid =
+ { 0x01, 0x23, 0x45, 0x67, 0x89,
+ 0xab, 0xcd, 0xef,
+ 0xAB, 0xCD, 0xEF,
+ 0x00, 0x00, 0x00, 0x00, 0x00 };
+ guid_128_t guid1, guid2, guid3;
+ const char *str;
+ char guidbuf[GUID_128_SIZE*2 + 2];
+ unsigned int i;
+
+ test_begin("guid_128_generate()");
+ guid_128_generate(guid1);
+ guid_128_generate(guid2);
+ test_assert(!guid_128_equals(guid1, guid2));
+ test_assert(guid_128_cmp(guid1, guid2) != 0);
+ test_end();
+
+ test_begin("guid_128_is_empty()");
+ test_assert(!guid_128_is_empty(guid1));
+ test_assert(!guid_128_is_empty(guid2));
+ guid_128_generate(guid3);
+ guid_128_empty(guid3);
+ test_assert(guid_128_is_empty(guid3));
+ test_end();
+
+ test_begin("guid_128_copy()");
+ guid_128_copy(guid3, guid1);
+ test_assert(guid_128_equals(guid3, guid1));
+ test_assert(!guid_128_equals(guid3, guid2));
+ guid_128_copy(guid3, guid2);
+ test_assert(!guid_128_equals(guid3, guid1));
+ test_assert(guid_128_equals(guid3, guid2));
+ test_end();
+
+ test_begin("guid_128_to_string()");
+ str = guid_128_to_string(guid1);
+ test_assert(guid_128_from_string(str, guid3) == 0);
+ test_assert(guid_128_equals(guid3, guid1));
+ test_end();
+
+ test_begin("guid_128_from_string()");
+ /* empty */
+ memset(guidbuf, '0', GUID_128_SIZE*2);
+ guidbuf[GUID_128_SIZE*2] = '\0';
+ guidbuf[GUID_128_SIZE*2+1] = '\0';
+ test_assert(guid_128_from_string(guidbuf, guid3) == 0);
+ test_assert(guid_128_is_empty(guid3));
+ /* too large */
+ guidbuf[GUID_128_SIZE*2] = '0';
+ test_assert(guid_128_from_string(guidbuf, guid3) < 0);
+ /* too small */
+ guidbuf[GUID_128_SIZE*2-1] = '\0';
+ test_assert(guid_128_from_string(guidbuf, guid3) < 0);
+ /* reset to normal */
+ guidbuf[GUID_128_SIZE*2-1] = '0';
+ guidbuf[GUID_128_SIZE*2] = '\0';
+ test_assert(guid_128_from_string(guidbuf, guid3) == 0);
+ /* upper + lowercase hex chars */
+ i_assert(GUID_128_SIZE*2 > 16 + 6);
+ for (i = 0; i < 10; i++)
+ guidbuf[i] = '0' + i;
+ for (i = 0; i < 6; i++)
+ guidbuf[10 + i] = 'a' + i;
+ for (i = 0; i < 6; i++)
+ guidbuf[16 + i] = 'A' + i;
+ test_assert(guid_128_from_string(guidbuf, guid3) == 0);
+ test_assert(guid_128_equals(guid3, test_guid));
+ /* non-hex chars */
+ guidbuf[0] = 'g';
+ test_assert(guid_128_from_string(guidbuf, guid3) < 0);
+ guidbuf[0] = ' ';
+ test_assert(guid_128_from_string(guidbuf, guid3) < 0);
+
+ test_assert(guid_128_from_uuid_string("fee0ceac-0327-11e7-ad39-52540078f374", guid3) == 0);
+ test_assert(guid_128_from_uuid_string("fee0ceac032711e7ad3952540078f374", guid2) == 0);
+ test_assert(guid_128_cmp(guid3, guid2) == 0);
+ test_assert(guid_128_from_uuid_string("{fee0ceac-0327-11e7-ad39-52540078f374}", guid2) == 0);
+ test_assert(guid_128_cmp(guid3, guid2) == 0);
+ test_assert(strcmp(guid_128_to_uuid_string(guid3, FORMAT_RECORD), "fee0ceac-0327-11e7-ad39-52540078f374")==0);
+ test_assert(strcmp(guid_128_to_uuid_string(guid3, FORMAT_COMPACT), "fee0ceac032711e7ad3952540078f374")==0);
+ test_assert(strcmp(guid_128_to_uuid_string(guid3, FORMAT_MICROSOFT), "{fee0ceac-0327-11e7-ad39-52540078f374}")==0);
+ /* failure test */
+ test_assert(guid_128_from_uuid_string("fe-e0ceac-0327-11e7-ad39-52540078f374", guid3) < 0);
+
+ test_end();
+
+ test_ioloop_guid_128_generate();
+}
diff --git a/src/lib/test-hash-format.c b/src/lib/test-hash-format.c
new file mode 100644
index 0000000..447e797
--- /dev/null
+++ b/src/lib/test-hash-format.c
@@ -0,0 +1,54 @@
+/* Copyright (c) 2010-2018 Dovecot authors, see the included COPYING file */
+
+#include "test-lib.h"
+#include "str.h"
+#include "hash-format.h"
+
+struct hash_format_test {
+ const char *input;
+ const char *output;
+};
+
+void test_hash_format(void)
+{
+ static const char *fail_input[] = {
+ "%",
+ "%A{sha1}",
+ "%{sha1",
+ "%{sha1:8",
+ "%{sha1:8a}",
+ "%{sha1:0}",
+ "%{sha1:168}",
+ NULL
+ };
+ static const struct hash_format_test tests[] = {
+ { "%{sha1}", "8843d7f92416211de9ebb963ff4ce28125932878" },
+ { "*%{sha1}*", "*8843d7f92416211de9ebb963ff4ce28125932878*" },
+ { "*%{sha1:8}*", "*88*" },
+ { "%{sha1:152}", "8843d7f92416211de9ebb963ff4ce281259328" },
+ { "%X{size}", "6" },
+ { "%{sha256:80}", "c3ab8ff13720e8ad9047" },
+ { "%{sha512:80}", "0a50261ebd1a390fed2b" },
+ { "%{md4}", "547aefd231dcbaac398625718336f143" },
+ { "%{md5}", "3858f62230ac3c915f300c664312c63f" },
+ { "%{sha256:80}-%X{size}", "c3ab8ff13720e8ad9047-6" }
+ };
+ struct hash_format *format;
+ string_t *str = t_str_new(128);
+ const char *error;
+ unsigned int i;
+
+ test_begin("hash_format");
+ for (i = 0; fail_input[i] != NULL; i++)
+ test_assert(hash_format_init(fail_input[i], &format, &error) < 0);
+
+ for (i = 0; i < N_ELEMENTS(tests); i++) {
+ test_assert(hash_format_init(tests[i].input, &format, &error) == 0);
+ hash_format_loop(format, "foo", 3);
+ hash_format_loop(format, "bar", 3);
+ str_truncate(str, 0);
+ hash_format_deinit(&format, str);
+ test_assert(strcmp(str_c(str), tests[i].output) == 0);
+ }
+ test_end();
+}
diff --git a/src/lib/test-hash-method.c b/src/lib/test-hash-method.c
new file mode 100644
index 0000000..0fd41e0
--- /dev/null
+++ b/src/lib/test-hash-method.c
@@ -0,0 +1,460 @@
+/* Copyright (c) 2014-2018 Dovecot authors, see the included COPYING file */
+
+#include "test-lib.h"
+#include "mmap-util.h"
+#include "hash-method.h"
+
+#if !defined(MAP_ANONYMOUS) && defined(MAP_ANON)
+# define MAP_ANONYMOUS MAP_ANON
+#endif
+
+static unsigned char *buf;
+static unsigned int buf_size;
+
+static void test_hash_method_one(const struct hash_method *method)
+{
+ unsigned char *ctx, *digest;
+ unsigned int i;
+
+ test_begin(t_strdup_printf("hash method %s", method->name));
+
+ ctx = i_malloc(method->context_size);
+ digest = i_malloc(method->digest_size);
+ method->init(ctx);
+
+ /* make sure the code doesn't try to access data past boundaries */
+ for (i = 0; i < buf_size; i++)
+ method->loop(ctx, buf + buf_size - i, i);
+ method->result(ctx, digest);
+
+ i_free(ctx);
+ i_free(digest);
+ test_end();
+}
+
+static void test_hash_method_boundary(void)
+{
+ unsigned int i;
+
+ buf_size = mmap_get_page_size();
+#ifdef MAP_ANONYMOUS
+ buf = mmap(NULL, buf_size*2, PROT_READ | PROT_WRITE,
+ MAP_ANONYMOUS | MAP_PRIVATE, -1, 0);
+ mprotect(buf + buf_size, buf_size, PROT_NONE);
+#else
+ buf = i_malloc(buf_size);
+#endif
+ memset(buf, 0, buf_size);
+
+ for (i = 0; hash_methods[i] != NULL; i++)
+ test_hash_method_one(hash_methods[i]);
+}
+
+static void test_hash_methods_fips() {
+ const char *last_method = NULL;
+
+ struct {
+ const char *method;
+ const void *input;
+ size_t ilen;
+ size_t rounds;
+ const void *output;
+ size_t olen;
+ } test_vectors[] =
+ {
+ { "md4",
+ "",
+ 0,
+ 1,
+ "\x31\xd6\xcf\xe0\xd1\x6a\xe9\x31"
+ "\xb7\x3c\x59\xd7\xe0\xc0\x89\xc0",
+ 128 / 8
+ },
+ { "md4",
+ "abc",
+ 3,
+ 1,
+ "\xa4\x48\x01\x7a\xaf\x21\xd8\x52"
+ "\x5f\xc1\x0a\xe8\x7a\xa6\x72\x9d",
+ 128 / 8
+ },
+ { "md4",
+ "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdef"
+ "ghijklmnopqrstuvwxyz0123456789",
+ 62,
+ 1,
+ "\x04\x3f\x85\x82\xf2\x41\xdb\x35"
+ "\x1c\xe6\x27\xe1\x53\xe7\xf0\xe4",
+ 128 / 8
+ },
+ { "md5",
+ "",
+ 0,
+ 1,
+ "\xd4\x1d\x8c\xd9\x8f\x00\xb2\x04"
+ "\xe9\x80\x09\x98\xec\xf8\x42\x7e",
+ 128 / 8
+ },
+ { "md5",
+ "abc",
+ 3,
+ 1,
+ "\x90\x01\x50\x98\x3c\xd2\x4f\xb0"
+ "\xd6\x96\x3f\x7d\x28\xe1\x7f\x72",
+ 128 / 8
+ },
+ { "md5",
+ "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdef"
+ "ghijklmnopqrstuvwxyz0123456789",
+ 62,
+ 1,
+ "\xd1\x74\xab\x98\xd2\x77\xd9\xf5"
+ "\xa5\x61\x1c\x2c\x9f\x41\x9d\x9f",
+ 128 / 8
+ },
+ { "sha1",
+ "",
+ 0,
+ 1,
+ "\xda\x39\xa3\xee\x5e\x6b\x4b\x0d"
+ "\x32\x55\xbf\xef\x95\x60\x18\x90"
+ "\xaf\xd8\x07\x09",
+ 160 / 8
+ },
+ { "sha1",
+ "abc",
+ 3,
+ 1,
+ "\xa9\x99\x3e\x36\x47\x06\x81\x6a"
+ "\xba\x3e\x25\x71\x78\x50\xc2\x6c"
+ "\x9c\xd0\xd8\x9d",
+ 160 / 8
+ },
+ { "sha1",
+ "abcdbcdecdefdefgefghfghighijhijk"
+ "ijkljklmklmnlmnomnopnopq",
+ 56,
+ 1,
+ "\x84\x98\x3e\x44\x1c\x3b\xd2\x6e"
+ "\xba\xae\x4a\xa1\xf9\x51\x29\xe5"
+ "\xe5\x46\x70\xf1",
+ 160 / 8
+ },
+ { "sha256",
+ "",
+ 0,
+ 1,
+ "\xe3\xb0\xc4\x42\x98\xfc\x1c\x14"
+ "\x9a\xfb\xf4\xc8\x99\x6f\xb9\x24"
+ "\x27\xae\x41\xe4\x64\x9b\x93\x4c"
+ "\xa4\x95\x99\x1b\x78\x52\xb8\x55",
+ 256 / 8
+ },
+ { "sha256",
+ "abc",
+ 3,
+ 1,
+ "\xba\x78\x16\xbf\x8f\x01\xcf\xea"
+ "\x41\x41\x40\xde\x5d\xae\x22\x23"
+ "\xb0\x03\x61\xa3\x96\x17\x7a\x9c"
+ "\xb4\x10\xff\x61\xf2\x00\x15\xad",
+ 256 / 8
+ },
+ { "sha256",
+ "abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq",
+ 56,
+ 1,
+ "\x24\x8d\x6a\x61\xd2\x06\x38\xb8"
+ "\xe5\xc0\x26\x93\x0c\x3e\x60\x39"
+ "\xa3\x3c\xe4\x59\x64\xff\x21\x67"
+ "\xf6\xec\xed\xd4\x19\xdb\x06\xc1",
+ 256 / 8
+ },
+ { "sha384",
+ "",
+ 0,
+ 1,
+ "\x38\xb0\x60\xa7\x51\xac\x96\x38"
+ "\x4c\xd9\x32\x7e\xb1\xb1\xe3\x6a"
+ "\x21\xfd\xb7\x11\x14\xbe\x07\x43"
+ "\x4c\x0c\xc7\xbf\x63\xf6\xe1\xda"
+ "\x27\x4e\xde\xbf\xe7\x6f\x65\xfb"
+ "\xd5\x1a\xd2\xf1\x48\x98\xb9\x5b",
+ 384 / 8
+ },
+ { "sha384",
+ "abc",
+ 3,
+ 1,
+ "\xcb\x00\x75\x3f\x45\xa3\x5e\x8b"
+ "\xb5\xa0\x3d\x69\x9a\xc6\x50\x07"
+ "\x27\x2c\x32\xab\x0e\xde\xd1\x63"
+ "\x1a\x8b\x60\x5a\x43\xff\x5b\xed"
+ "\x80\x86\x07\x2b\xa1\xe7\xcc\x23"
+ "\x58\xba\xec\xa1\x34\xc8\x25\xa7",
+ 384 / 8
+ },
+ { "sha384",
+ "abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq",
+ 56,
+ 1,
+ "\x33\x91\xfd\xdd\xfc\x8d\xc7\x39"
+ "\x37\x07\xa6\x5b\x1b\x47\x09\x39"
+ "\x7c\xf8\xb1\xd1\x62\xaf\x05\xab"
+ "\xfe\x8f\x45\x0d\xe5\xf3\x6b\xc6"
+ "\xb0\x45\x5a\x85\x20\xbc\x4e\x6f"
+ "\x5f\xe9\x5b\x1f\xe3\xc8\x45\x2b",
+ 384 / 8
+ },
+ { "sha512",
+ "",
+ 0,
+ 1,
+ "\xcf\x83\xe1\x35\x7e\xef\xb8\xbd"
+ "\xf1\x54\x28\x50\xd6\x6d\x80\x07"
+ "\xd6\x20\xe4\x05\x0b\x57\x15\xdc"
+ "\x83\xf4\xa9\x21\xd3\x6c\xe9\xce"
+ "\x47\xd0\xd1\x3c\x5d\x85\xf2\xb0"
+ "\xff\x83\x18\xd2\x87\x7e\xec\x2f"
+ "\x63\xb9\x31\xbd\x47\x41\x7a\x81"
+ "\xa5\x38\x32\x7a\xf9\x27\xda\x3e",
+ 512 / 8
+ },
+ { "sha512",
+ "abc",
+ 3,
+ 1,
+ "\xdd\xaf\x35\xa1\x93\x61\x7a\xba"
+ "\xcc\x41\x73\x49\xae\x20\x41\x31"
+ "\x12\xe6\xfa\x4e\x89\xa9\x7e\xa2"
+ "\x0a\x9e\xee\xe6\x4b\x55\xd3\x9a"
+ "\x21\x92\x99\x2a\x27\x4f\xc1\xa8"
+ "\x36\xba\x3c\x23\xa3\xfe\xeb\xbd"
+ "\x45\x4d\x44\x23\x64\x3c\xe8\x0e"
+ "\x2a\x9a\xc9\x4f\xa5\x4c\xa4\x9f",
+ 512 / 8
+ },
+ { "sha512",
+ "abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq",
+ 56,
+ 1,
+ "\x20\x4a\x8f\xc6\xdd\xa8\x2f\x0a"
+ "\x0c\xed\x7b\xeb\x8e\x08\xa4\x16"
+ "\x57\xc1\x6e\xf4\x68\xb2\x28\xa8"
+ "\x27\x9b\xe3\x31\xa7\x03\xc3\x35"
+ "\x96\xfd\x15\xc1\x3b\x1b\x07\xf9"
+ "\xaa\x1d\x3b\xea\x57\x78\x9c\xa0"
+ "\x31\xad\x85\xc7\xa7\x1d\xd7\x03"
+ "\x54\xec\x63\x12\x38\xca\x34\x45",
+ 512 / 8
+ },
+ { "sha3-256",
+ "",
+ 0,
+ 1,
+ "\xa7\xff\xc6\xf8\xbf\x1e\xd7\x66"
+ "\x51\xc1\x47\x56\xa0\x61\xd6\x62"
+ "\xf5\x80\xff\x4d\xe4\x3b\x49\xfa"
+ "\x82\xd8\x0a\x4b\x80\xf8\x43\x4a",
+ 256 / 8
+ },
+ { "sha3-256",
+ "\xb7\x71\xd5\xce\xf5\xd1\xa4\x1a"
+ "\x93\xd1\x56\x43\xd7\x18\x1d\x2a"
+ "\x2e\xf0\xa8\xe8\x4d\x91\x81\x2f"
+ "\x20\xed\x21\xf1\x47\xbe\xf7\x32"
+ "\xbf\x3a\x60\xef\x40\x67\xc3\x73"
+ "\x4b\x85\xbc\x8c\xd4\x71\x78\x0f"
+ "\x10\xdc\x9e\x82\x91\xb5\x83\x39"
+ "\xa6\x77\xb9\x60\x21\x8f\x71\xe7"
+ "\x93\xf2\x79\x7a\xea\x34\x94\x06"
+ "\x51\x28\x29\x06\x5d\x37\xbb\x55"
+ "\xea\x79\x6f\xa4\xf5\x6f\xd8\x89"
+ "\x6b\x49\xb2\xcd\x19\xb4\x32\x15"
+ "\xad\x96\x7c\x71\x2b\x24\xe5\x03"
+ "\x2d\x06\x52\x32\xe0\x2c\x12\x74"
+ "\x09\xd2\xed\x41\x46\xb9\xd7\x5d"
+ "\x76\x3d\x52\xdb\x98\xd9\x49\xd3"
+ "\xb0\xfe\xd6\xa8\x05\x2f\xbb",
+ 1080 / 8,
+ 1,
+ "\xa1\x9e\xee\x92\xbb\x20\x97\xb6"
+ "\x4e\x82\x3d\x59\x77\x98\xaa\x18"
+ "\xbe\x9b\x7c\x73\x6b\x80\x59\xab"
+ "\xfd\x67\x79\xac\x35\xac\x81\xb5",
+ 256 / 8
+ },
+ { "sha3-256",
+ "\xa3\xa3\xa3\xa3\xa3\xa3\xa3\xa3"
+ "\xa3\xa3\xa3\xa3\xa3\xa3\xa3\xa3"
+ "\xa3\xa3\xa3\xa3\xa3\xa3\xa3\xa3"
+ "\xa3\xa3\xa3\xa3\xa3\xa3\xa3\xa3"
+ "\xa3\xa3\xa3\xa3\xa3\xa3\xa3\xa3"
+ "\xa3\xa3\xa3\xa3\xa3\xa3\xa3\xa3"
+ "\xa3\xa3\xa3\xa3\xa3\xa3\xa3\xa3"
+ "\xa3\xa3\xa3\xa3\xa3\xa3\xa3\xa3"
+ "\xa3\xa3\xa3\xa3\xa3\xa3\xa3\xa3"
+ "\xa3\xa3\xa3\xa3\xa3\xa3\xa3\xa3"
+ "\xa3\xa3\xa3\xa3\xa3\xa3\xa3\xa3"
+ "\xa3\xa3\xa3\xa3\xa3\xa3\xa3\xa3"
+ "\xa3\xa3\xa3\xa3\xa3\xa3\xa3\xa3"
+ "\xa3\xa3\xa3\xa3\xa3\xa3\xa3\xa3"
+ "\xa3\xa3\xa3\xa3\xa3\xa3\xa3\xa3"
+ "\xa3\xa3\xa3\xa3\xa3\xa3\xa3\xa3"
+ "\xa3\xa3\xa3\xa3\xa3\xa3\xa3\xa3"
+ "\xa3\xa3\xa3\xa3\xa3\xa3\xa3\xa3"
+ "\xa3\xa3\xa3\xa3\xa3\xa3\xa3\xa3"
+ "\xa3\xa3\xa3\xa3\xa3\xa3\xa3\xa3"
+ "\xa3\xa3\xa3\xa3\xa3\xa3\xa3\xa3"
+ "\xa3\xa3\xa3\xa3\xa3\xa3\xa3\xa3"
+ "\xa3\xa3\xa3\xa3\xa3\xa3\xa3\xa3"
+ "\xa3\xa3\xa3\xa3\xa3\xa3\xa3\xa3"
+ "\xa3\xa3\xa3\xa3\xa3\xa3\xa3\xa3",
+ 200,
+ 1,
+ "\x79\xf3\x8a\xde\xc5\xc2\x03\x07"
+ "\xa9\x8e\xf7\x6e\x83\x24\xaf\xbf"
+ "\xd4\x6c\xfd\x81\xb2\x2e\x39\x73"
+ "\xc6\x5f\xa1\xbd\x9d\xe3\x17\x87",
+ 256 / 8,
+ },
+ { "sha3-256",
+ "\xa3",
+ 1,
+ 200,
+ "\x79\xf3\x8a\xde\xc5\xc2\x03\x07"
+ "\xa9\x8e\xf7\x6e\x83\x24\xaf\xbf"
+ "\xd4\x6c\xfd\x81\xb2\x2e\x39\x73"
+ "\xc6\x5f\xa1\xbd\x9d\xe3\x17\x87",
+ 256 / 8,
+ },
+ { "sha3-512",
+ "",
+ 0,
+ 1,
+ "\xa6\x9f\x73\xcc\xa2\x3a\x9a\xc5"
+ "\xc8\xb5\x67\xdc\x18\x5a\x75\x6e"
+ "\x97\xc9\x82\x16\x4f\xe2\x58\x59"
+ "\xe0\xd1\xdc\xc1\x47\x5c\x80\xa6"
+ "\x15\xb2\x12\x3a\xf1\xf5\xf9\x4c"
+ "\x11\xe3\xe9\x40\x2c\x3a\xc5\x58"
+ "\xf5\x00\x19\x9d\x95\xb6\xd3\xe3"
+ "\x01\x75\x85\x86\x28\x1d\xcd\x26",
+ 512 / 8
+ },
+ { "sha3-512",
+ "\xb7\x71\xd5\xce\xf5\xd1\xa4\x1a"
+ "\x93\xd1\x56\x43\xd7\x18\x1d\x2a"
+ "\x2e\xf0\xa8\xe8\x4d\x91\x81\x2f"
+ "\x20\xed\x21\xf1\x47\xbe\xf7\x32"
+ "\xbf\x3a\x60\xef\x40\x67\xc3\x73"
+ "\x4b\x85\xbc\x8c\xd4\x71\x78\x0f"
+ "\x10\xdc\x9e\x82\x91\xb5\x83\x39"
+ "\xa6\x77\xb9\x60\x21\x8f\x71\xe7"
+ "\x93\xf2\x79\x7a\xea\x34\x94\x06"
+ "\x51\x28\x29\x06\x5d\x37\xbb\x55"
+ "\xea\x79\x6f\xa4\xf5\x6f\xd8\x89"
+ "\x6b\x49\xb2\xcd\x19\xb4\x32\x15"
+ "\xad\x96\x7c\x71\x2b\x24\xe5\x03"
+ "\x2d\x06\x52\x32\xe0\x2c\x12\x74"
+ "\x09\xd2\xed\x41\x46\xb9\xd7\x5d"
+ "\x76\x3d\x52\xdb\x98\xd9\x49\xd3"
+ "\xb0\xfe\xd6\xa8\x05\x2f\xbb",
+ 1080 / 8,
+ 1,
+ "\x75\x75\xa1\xfb\x4f\xc9\xa8\xf9"
+ "\xc0\x46\x6b\xd5\xfc\xa4\x96\xd1"
+ "\xcb\x78\x69\x67\x73\xa2\x12\xa5"
+ "\xf6\x2d\x02\xd1\x4e\x32\x59\xd1"
+ "\x92\xa8\x7e\xba\x44\x07\xdd\x83"
+ "\x89\x35\x27\x33\x14\x07\xb6\xda"
+ "\xda\xad\x92\x0d\xbc\x46\x48\x9b"
+ "\x67\x74\x93\xce\x5f\x20\xb5\x95",
+ 512 / 8
+ },
+ { "sha3-512",
+ "\xa3\xa3\xa3\xa3\xa3\xa3\xa3\xa3"
+ "\xa3\xa3\xa3\xa3\xa3\xa3\xa3\xa3"
+ "\xa3\xa3\xa3\xa3\xa3\xa3\xa3\xa3"
+ "\xa3\xa3\xa3\xa3\xa3\xa3\xa3\xa3"
+ "\xa3\xa3\xa3\xa3\xa3\xa3\xa3\xa3"
+ "\xa3\xa3\xa3\xa3\xa3\xa3\xa3\xa3"
+ "\xa3\xa3\xa3\xa3\xa3\xa3\xa3\xa3"
+ "\xa3\xa3\xa3\xa3\xa3\xa3\xa3\xa3"
+ "\xa3\xa3\xa3\xa3\xa3\xa3\xa3\xa3"
+ "\xa3\xa3\xa3\xa3\xa3\xa3\xa3\xa3"
+ "\xa3\xa3\xa3\xa3\xa3\xa3\xa3\xa3"
+ "\xa3\xa3\xa3\xa3\xa3\xa3\xa3\xa3"
+ "\xa3\xa3\xa3\xa3\xa3\xa3\xa3\xa3"
+ "\xa3\xa3\xa3\xa3\xa3\xa3\xa3\xa3"
+ "\xa3\xa3\xa3\xa3\xa3\xa3\xa3\xa3"
+ "\xa3\xa3\xa3\xa3\xa3\xa3\xa3\xa3"
+ "\xa3\xa3\xa3\xa3\xa3\xa3\xa3\xa3"
+ "\xa3\xa3\xa3\xa3\xa3\xa3\xa3\xa3"
+ "\xa3\xa3\xa3\xa3\xa3\xa3\xa3\xa3"
+ "\xa3\xa3\xa3\xa3\xa3\xa3\xa3\xa3"
+ "\xa3\xa3\xa3\xa3\xa3\xa3\xa3\xa3"
+ "\xa3\xa3\xa3\xa3\xa3\xa3\xa3\xa3"
+ "\xa3\xa3\xa3\xa3\xa3\xa3\xa3\xa3"
+ "\xa3\xa3\xa3\xa3\xa3\xa3\xa3\xa3"
+ "\xa3\xa3\xa3\xa3\xa3\xa3\xa3\xa3",
+ 200,
+ 1,
+ "\xe7\x6d\xfa\xd2\x20\x84\xa8\xb1"
+ "\x46\x7f\xcf\x2f\xfa\x58\x36\x1b"
+ "\xec\x76\x28\xed\xf5\xf3\xfd\xc0"
+ "\xe4\x80\x5d\xc4\x8c\xae\xec\xa8"
+ "\x1b\x7c\x13\xc3\x0a\xdf\x52\xa3"
+ "\x65\x95\x84\x73\x9a\x2d\xf4\x6b"
+ "\xe5\x89\xc5\x1c\xa1\xa4\xa8\x41"
+ "\x6d\xf6\x54\x5a\x1c\xe8\xba\x00",
+ 512 / 8,
+ },
+ { "sha3-512",
+ "\xa3",
+ 1,
+ 200,
+ "\xe7\x6d\xfa\xd2\x20\x84\xa8\xb1"
+ "\x46\x7f\xcf\x2f\xfa\x58\x36\x1b"
+ "\xec\x76\x28\xed\xf5\xf3\xfd\xc0"
+ "\xe4\x80\x5d\xc4\x8c\xae\xec\xa8"
+ "\x1b\x7c\x13\xc3\x0a\xdf\x52\xa3"
+ "\x65\x95\x84\x73\x9a\x2d\xf4\x6b"
+ "\xe5\x89\xc5\x1c\xa1\xa4\xa8\x41"
+ "\x6d\xf6\x54\x5a\x1c\xe8\xba\x00",
+ 512 / 8,
+ },
+ };
+
+ for(size_t i = 0; i < N_ELEMENTS(test_vectors); i++) {
+
+ if (last_method == NULL ||
+ strcmp(last_method, test_vectors[i].method) != 0) {
+ if (last_method != NULL)
+ test_end();
+ last_method = test_vectors[i].method;
+ test_begin(t_strdup_printf("hash method %s (test vectors)",
+ last_method));
+ }
+ const struct hash_method *method =
+ hash_method_lookup(test_vectors[i].method);
+ unsigned char context[method->context_size];
+ unsigned char result[method->digest_size];
+ test_assert_idx(method->digest_size == test_vectors[i].olen, i);
+ method->init(context);
+ for(size_t n = 0; n < test_vectors[i].rounds; n++)
+ method->loop(context, test_vectors[i].input,
+ test_vectors[i].ilen);
+ method->result(context, result);
+ test_assert_idx(memcmp(result, test_vectors[i].output,
+ test_vectors[i].olen) == 0, i);
+ }
+
+ test_end();
+}
+
+void test_hash_method(void)
+{
+ test_hash_method_boundary();
+ test_hash_methods_fips();
+}
diff --git a/src/lib/test-hash.c b/src/lib/test-hash.c
new file mode 100644
index 0000000..8fe0e53
--- /dev/null
+++ b/src/lib/test-hash.c
@@ -0,0 +1,47 @@
+/* Copyright (c) 2014-2018 Dovecot authors, see the included COPYING file */
+
+#include "test-lib.h"
+#include "hash.h"
+
+
+static void test_hash_random_pool(pool_t pool)
+{
+#define KEYMAX 100000
+ HASH_TABLE(void *, void *) hash;
+ unsigned int *keys;
+ unsigned int i, key, keyidx, delidx;
+
+ keys = i_new(unsigned int, KEYMAX); keyidx = 0;
+ hash_table_create_direct(&hash, pool, 0);
+ for (i = 0; i < KEYMAX; i++) {
+ key = (i_rand_limit(KEYMAX)) + 1;
+ if (i_rand_limit(5) > 0) {
+ if (hash_table_lookup(hash, POINTER_CAST(key)) == NULL) {
+ hash_table_insert(hash, POINTER_CAST(key),
+ POINTER_CAST(1));
+ keys[keyidx++] = key;
+ }
+ } else if (keyidx > 0) {
+ delidx = i_rand_limit(keyidx);
+ hash_table_remove(hash, POINTER_CAST(keys[delidx]));
+ memmove(&keys[delidx], &keys[delidx+1],
+ (keyidx-delidx-1) * sizeof(*keys));
+ keyidx--;
+ }
+ }
+ for (i = 0; i < keyidx; i++)
+ hash_table_remove(hash, POINTER_CAST(keys[i]));
+ hash_table_destroy(&hash);
+ i_free(keys);
+}
+
+void test_hash(void)
+{
+ pool_t pool;
+
+ test_hash_random_pool(default_pool);
+
+ pool = pool_alloconly_create("test hash", 1024);
+ test_hash_random_pool(pool);
+ pool_unref(&pool);
+}
diff --git a/src/lib/test-hex-binary.c b/src/lib/test-hex-binary.c
new file mode 100644
index 0000000..cc248d4
--- /dev/null
+++ b/src/lib/test-hex-binary.c
@@ -0,0 +1,61 @@
+/* Copyright (c) 2009-2018 Dovecot authors, see the included COPYING file */
+
+#include "test-lib.h"
+#include "buffer.h"
+#include "str.h"
+#include "hex-binary.h"
+
+static void test_binary_to_hex(void)
+{
+ static unsigned char input[] = { 0xff, 0x00, 0x01, 0xb3 };
+ static char *output_lcase = "ff0001b3";
+ static char *output_ucase = "FF0001B3";
+ string_t *str;
+
+ test_begin("binary to hex");
+ test_assert(strcmp(binary_to_hex(input, sizeof(input)), output_lcase) == 0);
+ test_end();
+
+ test_begin("binary to hex ucase");
+ test_assert(strcmp(binary_to_hex_ucase(input, sizeof(input)), output_ucase) == 0);
+ test_end();
+
+ test_begin("binary to hex ucase");
+ str = t_str_new(32);
+ str_append_c(str, '<');
+ binary_to_hex_append(str, input, sizeof(input));
+ str_append_c(str, '>');
+ test_assert(strcmp(str_c(str), t_strconcat("<", output_lcase, ">", NULL)) == 0);
+ test_end();
+}
+
+static void test_hex_to_binary(void)
+{
+ static const char *ok_input = "0001fEFf";
+ static unsigned char ok_output[] = { 0x00, 0x01, 0xfe, 0xff };
+ static const char *error_input[] = {
+ "00 01",
+ "0x01",
+ "0g"
+ };
+ buffer_t *buf = t_buffer_create(10);
+ unsigned int i;
+
+ test_begin("hex to binary");
+ test_assert(hex_to_binary("", buf) == 0);
+ test_assert(buf->used == 0);
+
+ test_assert(hex_to_binary(ok_input, buf) == 0);
+ test_assert(buf->used == N_ELEMENTS(ok_output));
+ test_assert(memcmp(buf->data, ok_output, buf->used) == 0);
+
+ for (i = 0; i < N_ELEMENTS(error_input); i++)
+ test_assert(hex_to_binary(error_input[i], buf) == -1);
+ test_end();
+}
+
+void test_hex_binary(void)
+{
+ test_binary_to_hex();
+ test_hex_to_binary();
+}
diff --git a/src/lib/test-hmac.c b/src/lib/test-hmac.c
new file mode 100644
index 0000000..cf3d6d6
--- /dev/null
+++ b/src/lib/test-hmac.c
@@ -0,0 +1,302 @@
+/* Copyright (c) 2016-2018 Dovecot authors, see the included COPYING file */
+
+#include "test-lib.h"
+#include "hash-method.h"
+#include "hmac.h"
+#include "sha-common.h"
+#include "buffer.h"
+
+#include "hex-binary.h"
+
+struct test_vector {
+ const char *prf;
+ const unsigned char *key;
+ size_t key_len;
+ const unsigned char *data;
+ size_t data_len;
+ const unsigned char *res;
+ size_t res_len;
+};
+
+#define TEST_BUF(x) (const unsigned char*)x, sizeof(x)-1
+
+/* RFC 4231 test vectors */
+static const struct test_vector test_vectors[] = {
+ /* Test Case 1 */
+ { "sha256",
+ TEST_BUF("\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b"),
+ TEST_BUF("Hi There"),
+ TEST_BUF("\xb0\x34\x4c\x61\xd8\xdb\x38\x53\x5c\xa8\xaf\xce\xaf\x0b\xf1\x2b\x88\x1d\xc2\x00\xc9\x83\x3d\xa7\x26\xe9\x37\x6c\x2e\x32\xcf\xf7")
+ },
+ /* Test Case 2 */
+ { "sha256",
+ TEST_BUF("\x4a\x65\x66\x65"), /* "Jefe" */
+ TEST_BUF("what do ya want for nothing?"),
+ TEST_BUF("\x5b\xdc\xc1\x46\xbf\x60\x75\x4e\x6a\x04\x24\x26\x08\x95\x75\xc7\x5a\x00\x3f\x08\x9d\x27\x39\x83\x9d\xec\x58\xb9\x64\xec\x38\x43")
+ },
+ /* Test Case 3 */
+ { "sha256",
+ TEST_BUF("\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa"),
+ TEST_BUF("\xdd\xdd\xdd\xdd\xdd\xdd\xdd\xdd\xdd\xdd\xdd\xdd\xdd\xdd\xdd\xdd\xdd\xdd\xdd\xdd\xdd\xdd\xdd\xdd\xdd\xdd\xdd\xdd\xdd\xdd\xdd\xdd\xdd\xdd\xdd\xdd\xdd\xdd\xdd\xdd\xdd\xdd\xdd\xdd\xdd\xdd\xdd\xdd\xdd\xdd"),
+ TEST_BUF("\x77\x3e\xa9\x1e\x36\x80\x0e\x46\x85\x4d\xb8\xeb\xd0\x91\x81\xa7\x29\x59\x09\x8b\x3e\xf8\xc1\x22\xd9\x63\x55\x14\xce\xd5\x65\xfe")
+ },
+ /* Test Case 4 */
+ { "sha256",
+ TEST_BUF("\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19"),
+ TEST_BUF("\xcd\xcd\xcd\xcd\xcd\xcd\xcd\xcd\xcd\xcd\xcd\xcd\xcd\xcd\xcd\xcd\xcd\xcd\xcd\xcd\xcd\xcd\xcd\xcd\xcd\xcd\xcd\xcd\xcd\xcd\xcd\xcd\xcd\xcd\xcd\xcd\xcd\xcd\xcd\xcd\xcd\xcd\xcd\xcd\xcd\xcd\xcd\xcd\xcd\xcd"),
+ TEST_BUF("\x82\x55\x8a\x38\x9a\x44\x3c\x0e\xa4\xcc\x81\x98\x99\xf2\x08\x3a\x85\xf0\xfa\xa3\xe5\x78\xf8\x07\x7a\x2e\x3f\xf4\x67\x29\x66\x5b")
+ },
+ /* Test Case 5 */
+ { "sha256",
+ TEST_BUF("\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c"),
+ TEST_BUF("\x54\x65\x73\x74\x20\x57\x69\x74\x68\x20\x54\x72\x75\x6e\x63\x61\x74\x69\x6f\x6e"), /* "Test With Truncation" */
+ TEST_BUF("\xa3\xb6\x16\x74\x73\x10\x0e\xe0\x6e\x0c\x79\x6c\x29\x55\x55\x2b")
+ },
+ /* Test Case 6 */
+ { "sha256",
+ TEST_BUF("\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa"),
+ TEST_BUF("\x54\x65\x73\x74\x20\x55\x73\x69\x6e\x67\x20\x4c\x61\x72\x67\x65\x72\x20\x54\x68\x61\x6e\x20\x42\x6c\x6f\x63\x6b\x2d\x53\x69\x7a\x65\x20\x4b\x65\x79\x20\x2d\x20\x48\x61\x73\x68\x20\x4b\x65\x79\x20\x46\x69\x72\x73\x74"), /* "Test Using Larger Than Block-Size Key - Hash Key First" */
+ TEST_BUF("\x60\xe4\x31\x59\x1e\xe0\xb6\x7f\x0d\x8a\x26\xaa\xcb\xf5\xb7\x7f\x8e\x0b\xc6\x21\x37\x28\xc5\x14\x05\x46\x04\x0f\x0e\xe3\x7f\x54")
+ },
+ /* Test Case 7 */
+ { "sha256",
+ TEST_BUF("\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa"),
+ TEST_BUF("\x54\x68\x69\x73\x20\x69\x73\x20\x61\x20\x74\x65\x73\x74\x20\x75\x73\x69\x6e\x67\x20\x61\x20\x6c\x61\x72\x67\x65\x72\x20\x74\x68\x61\x6e\x20\x62\x6c\x6f\x63\x6b\x2d\x73\x69\x7a\x65\x20\x6b\x65\x79\x20\x61\x6e\x64\x20\x61\x20\x6c\x61\x72\x67\x65\x72\x20\x74\x68\x61\x6e\x20\x62\x6c\x6f\x63\x6b\x2d\x73\x69\x7a\x65\x20\x64\x61\x74\x61\x2e\x20\x54\x68\x65\x20\x6b\x65\x79\x20\x6e\x65\x65\x64\x73\x20\x74\x6f\x20\x62\x65\x20\x68\x61\x73\x68\x65\x64\x20\x62\x65\x66\x6f\x72\x65\x20\x62\x65\x69\x6e\x67\x20\x75\x73\x65\x64\x20\x62\x79\x20\x74\x68\x65\x20\x48\x4d\x41\x43\x20\x61\x6c\x67\x6f\x72\x69\x74\x68\x6d\x2e"),
+ /* "This is a test using a larger than block-size key and a larger than block-size data. The key needs to be hashed before being used by the HMAC algorithm." */
+ TEST_BUF("\x9b\x09\xff\xa7\x1b\x94\x2f\xcb\x27\x63\x5f\xbc\xd5\xb0\xe9\x44\xbf\xdc\x63\x64\x4f\x07\x13\x93\x8a\x7f\x51\x53\x5c\x3a\x35\xe2")
+ }
+
+};
+
+static const struct test_vector test_vectors_hmac384[] = {
+ /* Test Case 1 */
+ { "sha384",
+ TEST_BUF("\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b"),
+ TEST_BUF("Hi There"),
+ TEST_BUF("\xaf\xd0\x39\x44\xd8\x48\x95\x62\x6b\x08\x25\xf4\xab\x46\x90\x7f\x15\xf9\xda\xdb\xe4\x10\x1e\xc6\x82\xaa\x03\x4c\x7c\xeb\xc5\x9c\xfa\xea\x9e\xa9\x07\x6e\xde\x7f\x4a\xf1\x52\xe8\xb2\xfa\x9c\xb6"),
+ },
+ /* Test Case 2 */
+ { "sha384",
+ TEST_BUF("\x4a\x65\x66\x65"), /* "Jefe" */
+ TEST_BUF("what do ya want for nothing?"),
+ TEST_BUF("\xaf\x45\xd2\xe3\x76\x48\x40\x31\x61\x7f\x78\xd2\xb5\x8a\x6b\x1b\x9c\x7e\xf4\x64\xf5\xa0\x1b\x47\xe4\x2e\xc3\x73\x63\x22\x44\x5e\x8e\x22\x40\xca\x5e\x69\xe2\xc7\x8b\x32\x39\xec\xfa\xb2\x16\x49"),
+ },
+ /* Test Case 3 */
+ { "sha384",
+ TEST_BUF("\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa"),
+ TEST_BUF("\xdd\xdd\xdd\xdd\xdd\xdd\xdd\xdd\xdd\xdd\xdd\xdd\xdd\xdd\xdd\xdd\xdd\xdd\xdd\xdd\xdd\xdd\xdd\xdd\xdd\xdd\xdd\xdd\xdd\xdd\xdd\xdd\xdd\xdd\xdd\xdd\xdd\xdd\xdd\xdd\xdd\xdd\xdd\xdd\xdd\xdd\xdd\xdd\xdd\xdd"),
+ TEST_BUF("\x88\x06\x26\x08\xd3\xe6\xad\x8a\x0a\xa2\xac\xe0\x14\xc8\xa8\x6f\x0a\xa6\x35\xd9\x47\xac\x9f\xeb\xe8\x3e\xf4\xe5\x59\x66\x14\x4b\x2a\x5a\xb3\x9d\xc1\x38\x14\xb9\x4e\x3a\xb6\xe1\x01\xa3\x4f\x27"),
+ },
+ /* Test Case 4 */
+ { "sha384",
+ TEST_BUF("\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19"),
+ TEST_BUF("\xcd\xcd\xcd\xcd\xcd\xcd\xcd\xcd\xcd\xcd\xcd\xcd\xcd\xcd\xcd\xcd\xcd\xcd\xcd\xcd\xcd\xcd\xcd\xcd\xcd\xcd\xcd\xcd\xcd\xcd\xcd\xcd\xcd\xcd\xcd\xcd\xcd\xcd\xcd\xcd\xcd\xcd\xcd\xcd\xcd\xcd\xcd\xcd\xcd\xcd"),
+ TEST_BUF("\x3e\x8a\x69\xb7\x78\x3c\x25\x85\x19\x33\xab\x62\x90\xaf\x6c\xa7\x7a\x99\x81\x48\x08\x50\x00\x9c\xc5\x57\x7c\x6e\x1f\x57\x3b\x4e\x68\x01\xdd\x23\xc4\xa7\xd6\x79\xcc\xf8\xa3\x86\xc6\x74\xcf\xfb"),
+ },
+ /* Test Case 5 */
+ { "sha384",
+ TEST_BUF("\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c"),
+ TEST_BUF("\x54\x65\x73\x74\x20\x57\x69\x74\x68\x20\x54\x72\x75\x6e\x63\x61\x74\x69\x6f\x6e"), /* "Test With Truncation" */
+ TEST_BUF("\x3a\xbf\x34\xc3\x50\x3b\x2a\x23\xa4\x6e\xfc\x61\x9b\xae\xf8\x97"),
+ },
+ /* Test Case 6 */
+ { "sha384",
+ TEST_BUF("\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa"),
+ TEST_BUF("\x54\x65\x73\x74\x20\x55\x73\x69\x6e\x67\x20\x4c\x61\x72\x67\x65\x72\x20\x54\x68\x61\x6e\x20\x42\x6c\x6f\x63\x6b\x2d\x53\x69\x7a\x65\x20\x4b\x65\x79\x20\x2d\x20\x48\x61\x73\x68\x20\x4b\x65\x79\x20\x46\x69\x72\x73\x74"), /* "Test Using Larger Than Block-Size Key - Hash Key First" */
+ TEST_BUF("\x4e\xce\x08\x44\x85\x81\x3e\x90\x88\xd2\xc6\x3a\x04\x1b\xc5\xb4\x4f\x9e\xf1\x01\x2a\x2b\x58\x8f\x3c\xd1\x1f\x05\x03\x3a\xc4\xc6\x0c\x2e\xf6\xab\x40\x30\xfe\x82\x96\x24\x8d\xf1\x63\xf4\x49\x52"),
+ },
+ /* Test Case 7 */
+ { "sha384",
+ TEST_BUF("\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa"),
+ TEST_BUF("\x54\x68\x69\x73\x20\x69\x73\x20\x61\x20\x74\x65\x73\x74\x20\x75\x73\x69\x6e\x67\x20\x61\x20\x6c\x61\x72\x67\x65\x72\x20\x74\x68\x61\x6e\x20\x62\x6c\x6f\x63\x6b\x2d\x73\x69\x7a\x65\x20\x6b\x65\x79\x20\x61\x6e\x64\x20\x61\x20\x6c\x61\x72\x67\x65\x72\x20\x74\x68\x61\x6e\x20\x62\x6c\x6f\x63\x6b\x2d\x73\x69\x7a\x65\x20\x64\x61\x74\x61\x2e\x20\x54\x68\x65\x20\x6b\x65\x79\x20\x6e\x65\x65\x64\x73\x20\x74\x6f\x20\x62\x65\x20\x68\x61\x73\x68\x65\x64\x20\x62\x65\x66\x6f\x72\x65\x20\x62\x65\x69\x6e\x67\x20\x75\x73\x65\x64\x20\x62\x79\x20\x74\x68\x65\x20\x48\x4d\x41\x43\x20\x61\x6c\x67\x6f\x72\x69\x74\x68\x6d\x2e"),
+ /* "This is a test using a larger than block-size key and a larger than block-size data. The key needs to be hashed before being used by the HMAC algorithm." */
+ TEST_BUF("\x66\x17\x17\x8e\x94\x1f\x02\x0d\x35\x1e\x2f\x25\x4e\x8f\xd3\x2c\x60\x24\x20\xfe\xb0\xb8\xfb\x9a\xdc\xce\xbb\x82\x46\x1e\x99\xc5\xa6\x78\xcc\x31\xe7\x99\x17\x6d\x38\x60\xe6\x11\x0c\x46\x52\x3e"),
+ }
+};
+
+static const struct test_vector test_vectors_hmac512[] = {
+ /* Test Case 1 */
+ { "sha512",
+ TEST_BUF("\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b"),
+ TEST_BUF("Hi There"),
+ TEST_BUF("\x87\xaa\x7c\xde\xa5\xef\x61\x9d\x4f\xf0\xb4\x24\x1a\x1d\x6c\xb0\x23\x79\xf4\xe2\xce\x4e\xc2\x78\x7a\xd0\xb3\x05\x45\xe1\x7c\xde\xda\xa8\x33\xb7\xd6\xb8\xa7\x02\x03\x8b\x27\x4e\xae\xa3\xf4\xe4\xbe\x9d\x91\x4e\xeb\x61\xf1\x70\x2e\x69\x6c\x20\x3a\x12\x68\x54")
+ },
+ /* Test Case 2 */
+ { "sha512",
+ TEST_BUF("\x4a\x65\x66\x65"), /* "Jefe" */
+ TEST_BUF("what do ya want for nothing?"),
+ TEST_BUF("\x16\x4b\x7a\x7b\xfc\xf8\x19\xe2\xe3\x95\xfb\xe7\x3b\x56\xe0\xa3\x87\xbd\x64\x22\x2e\x83\x1f\xd6\x10\x27\x0c\xd7\xea\x25\x05\x54\x97\x58\xbf\x75\xc0\x5a\x99\x4a\x6d\x03\x4f\x65\xf8\xf0\xe6\xfd\xca\xea\xb1\xa3\x4d\x4a\x6b\x4b\x63\x6e\x07\x0a\x38\xbc\xe7\x37")
+ },
+ /* Test Case 3 */
+ { "sha512",
+ TEST_BUF("\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa"),
+ TEST_BUF("\xdd\xdd\xdd\xdd\xdd\xdd\xdd\xdd\xdd\xdd\xdd\xdd\xdd\xdd\xdd\xdd\xdd\xdd\xdd\xdd\xdd\xdd\xdd\xdd\xdd\xdd\xdd\xdd\xdd\xdd\xdd\xdd\xdd\xdd\xdd\xdd\xdd\xdd\xdd\xdd\xdd\xdd\xdd\xdd\xdd\xdd\xdd\xdd\xdd\xdd"),
+ TEST_BUF("\xfa\x73\xb0\x08\x9d\x56\xa2\x84\xef\xb0\xf0\x75\x6c\x89\x0b\xe9\xb1\xb5\xdb\xdd\x8e\xe8\x1a\x36\x55\xf8\x3e\x33\xb2\x27\x9d\x39\xbf\x3e\x84\x82\x79\xa7\x22\xc8\x06\xb4\x85\xa4\x7e\x67\xc8\x07\xb9\x46\xa3\x37\xbe\xe8\x94\x26\x74\x27\x88\x59\xe1\x32\x92\xfb")
+ },
+ /* Test Case 4 */
+ { "sha512",
+ TEST_BUF("\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19"),
+ TEST_BUF("\xcd\xcd\xcd\xcd\xcd\xcd\xcd\xcd\xcd\xcd\xcd\xcd\xcd\xcd\xcd\xcd\xcd\xcd\xcd\xcd\xcd\xcd\xcd\xcd\xcd\xcd\xcd\xcd\xcd\xcd\xcd\xcd\xcd\xcd\xcd\xcd\xcd\xcd\xcd\xcd\xcd\xcd\xcd\xcd\xcd\xcd\xcd\xcd\xcd\xcd"),
+ TEST_BUF("\xb0\xba\x46\x56\x37\x45\x8c\x69\x90\xe5\xa8\xc5\xf6\x1d\x4a\xf7\xe5\x76\xd9\x7f\xf9\x4b\x87\x2d\xe7\x6f\x80\x50\x36\x1e\xe3\xdb\xa9\x1c\xa5\xc1\x1a\xa2\x5e\xb4\xd6\x79\x27\x5c\xc5\x78\x80\x63\xa5\xf1\x97\x41\x12\x0c\x4f\x2d\xe2\xad\xeb\xeb\x10\xa2\x98\xdd")
+ },
+ /* Test Case 5 */
+ { "sha512",
+ TEST_BUF("\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c"),
+ TEST_BUF("\x54\x65\x73\x74\x20\x57\x69\x74\x68\x20\x54\x72\x75\x6e\x63\x61\x74\x69\x6f\x6e"), /* "Test With Truncation" */
+ TEST_BUF("\x41\x5f\xad\x62\x71\x58\x0a\x53\x1d\x41\x79\xbc\x89\x1d\x87\xa6")
+ },
+ /* Test Case 6 */
+ { "sha512",
+ TEST_BUF("\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa"),
+ TEST_BUF("\x54\x65\x73\x74\x20\x55\x73\x69\x6e\x67\x20\x4c\x61\x72\x67\x65\x72\x20\x54\x68\x61\x6e\x20\x42\x6c\x6f\x63\x6b\x2d\x53\x69\x7a\x65\x20\x4b\x65\x79\x20\x2d\x20\x48\x61\x73\x68\x20\x4b\x65\x79\x20\x46\x69\x72\x73\x74"), /* "Test Using Larger Than Block-Size Key - Hash Key First" */
+ TEST_BUF("\x80\xb2\x42\x63\xc7\xc1\xa3\xeb\xb7\x14\x93\xc1\xdd\x7b\xe8\xb4\x9b\x46\xd1\xf4\x1b\x4a\xee\xc1\x12\x1b\x01\x37\x83\xf8\xf3\x52\x6b\x56\xd0\x37\xe0\x5f\x25\x98\xbd\x0f\xd2\x21\x5d\x6a\x1e\x52\x95\xe6\x4f\x73\xf6\x3f\x0a\xec\x8b\x91\x5a\x98\x5d\x78\x65\x98")
+ },
+ /* Test Case 7 */
+ { "sha512",
+ TEST_BUF("\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa"),
+ TEST_BUF("\x54\x68\x69\x73\x20\x69\x73\x20\x61\x20\x74\x65\x73\x74\x20\x75\x73\x69\x6e\x67\x20\x61\x20\x6c\x61\x72\x67\x65\x72\x20\x74\x68\x61\x6e\x20\x62\x6c\x6f\x63\x6b\x2d\x73\x69\x7a\x65\x20\x6b\x65\x79\x20\x61\x6e\x64\x20\x61\x20\x6c\x61\x72\x67\x65\x72\x20\x74\x68\x61\x6e\x20\x62\x6c\x6f\x63\x6b\x2d\x73\x69\x7a\x65\x20\x64\x61\x74\x61\x2e\x20\x54\x68\x65\x20\x6b\x65\x79\x20\x6e\x65\x65\x64\x73\x20\x74\x6f\x20\x62\x65\x20\x68\x61\x73\x68\x65\x64\x20\x62\x65\x66\x6f\x72\x65\x20\x62\x65\x69\x6e\x67\x20\x75\x73\x65\x64\x20\x62\x79\x20\x74\x68\x65\x20\x48\x4d\x41\x43\x20\x61\x6c\x67\x6f\x72\x69\x74\x68\x6d\x2e"),
+ /* "This is a test using a larger than block-size key and a larger than block-size data. The key needs to be hashed before being used by the HMAC algorithm." */
+ TEST_BUF("\xe3\x7b\x6a\x77\x5d\xc8\x7d\xba\xa4\xdf\xa9\xf9\x6e\x5e\x3f\xfd\xde\xbd\x71\xf8\x86\x72\x89\x86\x5d\xf5\xa3\x2d\x20\xcd\xc9\x44\xb6\x02\x2c\xac\x3c\x49\x82\xb1\x0d\x5e\xeb\x55\xc3\xe4\xde\x15\x13\x46\x76\xfb\x6d\xe0\x44\x60\x65\xc9\x74\x40\xfa\x8c\x6a\x58")
+ }
+
+};
+
+/* RFC 5869 test vectors */
+
+static const struct test_vector_5869 {
+ const char *prf;
+ const unsigned char *ikm;
+ size_t ikm_len;
+ const unsigned char *salt;
+ size_t salt_len;
+ const unsigned char *info;
+ size_t info_len;
+ const unsigned char *okm;
+ size_t okm_len;
+} test_vectors_5869[] = {
+ { "sha256",
+ TEST_BUF("\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b"),
+ TEST_BUF("\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c"),
+ TEST_BUF("\xf0\xf1\xf2\xf3\xf4\xf5\xf6\xf7\xf8\xf9"),
+ TEST_BUF("\x3c\xb2\x5f\x25\xfa\xac\xd5\x7a\x90\x43\x4f\x64\xd0\x36\x2f\x2a\x2d\x2d\x0a\x90\xcf\x1a\x5a\x4c\x5d\xb0\x2d\x56\xec\xc4\xc5\xbf\x34\x00\x72\x08\xd5\xb8\x87\x18\x58\x65")
+ },
+ { "sha256",
+ TEST_BUF("\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f\x20\x21\x22\x23\x24\x25\x26\x27\x28\x29\x2a\x2b\x2c\x2d\x2e\x2f\x30\x31\x32\x33\x34\x35\x36\x37\x38\x39\x3a\x3b\x3c\x3d\x3e\x3f\x40\x41\x42\x43\x44\x45\x46\x47\x48\x49\x4a\x4b\x4c\x4d\x4e\x4f"),
+ TEST_BUF("\x60\x61\x62\x63\x64\x65\x66\x67\x68\x69\x6a\x6b\x6c\x6d\x6e\x6f\x70\x71\x72\x73\x74\x75\x76\x77\x78\x79\x7a\x7b\x7c\x7d\x7e\x7f\x80\x81\x82\x83\x84\x85\x86\x87\x88\x89\x8a\x8b\x8c\x8d\x8e\x8f\x90\x91\x92\x93\x94\x95\x96\x97\x98\x99\x9a\x9b\x9c\x9d\x9e\x9f\xa0\xa1\xa2\xa3\xa4\xa5\xa6\xa7\xa8\xa9\xaa\xab\xac\xad\xae\xaf"),
+ TEST_BUF("\xb0\xb1\xb2\xb3\xb4\xb5\xb6\xb7\xb8\xb9\xba\xbb\xbc\xbd\xbe\xbf\xc0\xc1\xc2\xc3\xc4\xc5\xc6\xc7\xc8\xc9\xca\xcb\xcc\xcd\xce\xcf\xd0\xd1\xd2\xd3\xd4\xd5\xd6\xd7\xd8\xd9\xda\xdb\xdc\xdd\xde\xdf\xe0\xe1\xe2\xe3\xe4\xe5\xe6\xe7\xe8\xe9\xea\xeb\xec\xed\xee\xef\xf0\xf1\xf2\xf3\xf4\xf5\xf6\xf7\xf8\xf9\xfa\xfb\xfc\xfd\xfe\xff"),
+ TEST_BUF("\xb1\x1e\x39\x8d\xc8\x03\x27\xa1\xc8\xe7\xf7\x8c\x59\x6a\x49\x34\x4f\x01\x2e\xda\x2d\x4e\xfa\xd8\xa0\x50\xcc\x4c\x19\xaf\xa9\x7c\x59\x04\x5a\x99\xca\xc7\x82\x72\x71\xcb\x41\xc6\x5e\x59\x0e\x09\xda\x32\x75\x60\x0c\x2f\x09\xb8\x36\x77\x93\xa9\xac\xa3\xdb\x71\xcc\x30\xc5\x81\x79\xec\x3e\x87\xc1\x4c\x01\xd5\xc1\xf3\x43\x4f\x1d\x87")
+ },
+ { "sha256",
+ TEST_BUF("\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b"),
+ TEST_BUF(""),
+ TEST_BUF(""),
+ TEST_BUF("\x8d\xa4\xe7\x75\xa5\x63\xc1\x8f\x71\x5f\x80\x2a\x06\x3c\x5a\x31\xb8\xa1\x1f\x5c\x5e\xe1\x87\x9e\xc3\x45\x4e\x5f\x3c\x73\x8d\x2d\x9d\x20\x13\x95\xfa\xa4\xb6\x1a\x96\xc8")
+ },
+ /* should be equal to above */
+ { "sha256",
+ TEST_BUF("\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b"),
+ NULL, 0,
+ NULL, 0,
+ TEST_BUF("\x8d\xa4\xe7\x75\xa5\x63\xc1\x8f\x71\x5f\x80\x2a\x06\x3c\x5a\x31\xb8\xa1\x1f\x5c\x5e\xe1\x87\x9e\xc3\x45\x4e\x5f\x3c\x73\x8d\x2d\x9d\x20\x13\x95\xfa\xa4\xb6\x1a\x96\xc8")
+ },
+};
+
+static void test_hmac_rfc(void)
+{
+ test_begin("hmac sha256 rfc4231 vectors");
+ for(size_t i = 0; i < N_ELEMENTS(test_vectors); i++) {
+ const struct test_vector *vec = &(test_vectors[i]);
+ struct hmac_context ctx;
+ hmac_init(&ctx, vec->key, vec->key_len, hash_method_lookup(vec->prf));
+ hmac_update(&ctx, vec->data, vec->data_len);
+ unsigned char res[SHA256_RESULTLEN];
+ hmac_final(&ctx, res);
+ test_assert_idx(memcmp(res, vec->res, vec->res_len) == 0, i);
+ }
+ test_end();
+}
+
+static void test_hmac384_rfc(void)
+{
+ test_begin("hmac sha384 rfc4231 vectors");
+ for (size_t i = 0; i < N_ELEMENTS(test_vectors_hmac384); i++) {
+ const struct test_vector *vec = &(test_vectors_hmac384[i]);
+ struct hmac_context ctx;
+ hmac_init(&ctx, vec->key, vec->key_len, hash_method_lookup(vec->prf));
+ hmac_update(&ctx, vec->data, vec->data_len);
+ unsigned char res[SHA384_RESULTLEN];
+ hmac_final(&ctx, res);
+ test_assert_idx(memcmp(res, vec->res, vec->res_len) == 0, i);
+ }
+ test_end();
+}
+
+static void test_hmac512_rfc(void)
+{
+ test_begin("hmac sha512 rfc4231 vectors");
+ for (size_t i = 0; i < N_ELEMENTS(test_vectors_hmac512); i++) {
+ const struct test_vector *vec = &(test_vectors_hmac512[i]);
+ struct hmac_context ctx;
+ hmac_init(&ctx, vec->key, vec->key_len, hash_method_lookup(vec->prf));
+ hmac_update(&ctx, vec->data, vec->data_len);
+ unsigned char res[SHA512_RESULTLEN];
+ hmac_final(&ctx, res);
+ test_assert_idx(memcmp(res, vec->res, vec->res_len) == 0, i);
+ }
+ test_end();
+}
+
+static void test_hmac_buffer(void)
+{
+ const struct test_vector *vec = &(test_vectors[0]);
+ test_begin("hmac temporary buffer");
+
+ buffer_t *tmp;
+
+ tmp = t_hmac_data(hash_method_lookup(vec->prf), vec->key, vec->key_len,
+ vec->data, vec->data_len);
+
+ test_assert(tmp->used == vec->res_len &&
+ memcmp(tmp->data, vec->res, vec->res_len) == 0);
+
+ test_end();
+}
+
+static void test_hkdf_rfc(void)
+{
+ test_begin("hkdf sha256 rfc5869 vectors");
+ buffer_t *res = t_buffer_create(82);
+ for(size_t i = 0; i < N_ELEMENTS(test_vectors_5869); i++) {
+ buffer_set_used_size(res, 0);
+ const struct test_vector_5869 *vec = &(test_vectors_5869[i]);
+ const struct hash_method *m = hash_method_lookup(vec->prf);
+ hmac_hkdf(m, vec->salt, vec->salt_len, vec->ikm, vec->ikm_len,
+ vec->info, vec->info_len, res, vec->okm_len);
+ test_assert_idx(memcmp(res->data, vec->okm, vec->okm_len) == 0, i);
+ }
+
+ test_end();
+}
+
+static void test_hkdf_buffer(void)
+{
+ test_begin("hkdf temporary buffer");
+ const struct test_vector_5869 *vec = &(test_vectors_5869[0]);
+ const struct hash_method *m = hash_method_lookup(vec->prf);
+ buffer_t *tmp = t_hmac_hkdf(m, vec->salt, vec->salt_len, vec->ikm,
+ vec->ikm_len, vec->info, vec->info_len,
+ vec->okm_len);
+ test_assert(tmp->used == vec->okm_len &&
+ memcmp(tmp->data, vec->okm, vec->okm_len) == 0);
+ test_end();
+}
+
+void test_hmac(void)
+{
+ test_hmac_rfc();
+ test_hmac384_rfc();
+ test_hmac512_rfc();
+ test_hmac_buffer();
+ test_hkdf_rfc();
+ test_hkdf_buffer();
+}
diff --git a/src/lib/test-imem.c b/src/lib/test-imem.c
new file mode 100644
index 0000000..c18c4aa
--- /dev/null
+++ b/src/lib/test-imem.c
@@ -0,0 +1,61 @@
+/* Copyright (c) 2017-2018 Dovecot authors, see the included COPYING file */
+
+#include "test-lib.h"
+
+struct test_struct {
+ uint32_t num[10];
+};
+
+static void test_imem_alloc(void)
+{
+ struct test_struct ab, bc, cd, de;
+
+ test_begin("imem allocs");
+
+ memset(ab.num, 0xab, sizeof(ab.num));
+ memset(bc.num, 0xbc, sizeof(bc.num));
+ memset(cd.num, 0xcd, sizeof(cd.num));
+ memset(de.num, 0xde, sizeof(de.num));
+
+ /* regular alloc */
+ struct test_struct *s1 = i_new(struct test_struct, 2);
+ struct test_struct *s2 = i_malloc(sizeof(struct test_struct) * 2);
+ s1[0] = ab; s2[0] = ab;
+ s1[1] = bc; s2[1] = bc;
+ test_assert(memcmp(s1, s2, sizeof(struct test_struct) * 2) == 0);
+
+ /* realloc */
+ s1 = i_realloc_type(s1, struct test_struct, 2, 4);
+ s2 = i_realloc(s2, sizeof(struct test_struct) * 2,
+ sizeof(struct test_struct) * 4);
+ s1[2] = cd; s2[2] = cd;
+ s1[3] = de; s2[3] = de;
+ test_assert(memcmp(&s1[0], &ab, sizeof(ab)) == 0);
+ test_assert(memcmp(&s1[1], &bc, sizeof(bc)) == 0);
+ test_assert(memcmp(&s1[2], &cd, sizeof(cd)) == 0);
+ test_assert(memcmp(&s1[3], &de, sizeof(de)) == 0);
+ test_assert(memcmp(s1, s2, sizeof(struct test_struct) * 4) == 0);
+
+ /* freeing realloced memory */
+ i_free(s1);
+ i_free(s2);
+ test_assert(s1 == NULL);
+ test_assert(s2 == NULL);
+
+ /* allcating new memory with realloc */
+ s1 = i_realloc_type(NULL, struct test_struct, 0, 2);
+ s2 = i_realloc(NULL, 0, sizeof(struct test_struct) * 2);
+ s1[0] = ab; s2[0] = ab;
+ s1[1] = bc; s2[1] = bc;
+ test_assert(memcmp(s1, s2, sizeof(struct test_struct) * 2) == 0);
+
+ i_free(s1);
+ i_free(s2);
+
+ test_end();
+}
+
+void test_imem(void)
+{
+ test_imem_alloc();
+}
diff --git a/src/lib/test-ioloop.c b/src/lib/test-ioloop.c
new file mode 100644
index 0000000..89f6888
--- /dev/null
+++ b/src/lib/test-ioloop.c
@@ -0,0 +1,404 @@
+/* Copyright (c) 2015-2018 Dovecot authors, see the included COPYING file */
+
+#include "test-lib.h"
+#include "net.h"
+#include "time-util.h"
+#include "ioloop.h"
+#include "istream.h"
+
+#include <unistd.h>
+
+struct test_ctx {
+ bool got_left;
+ bool got_right;
+ bool got_to;
+};
+
+static void timeout_callback(struct timeval *tv)
+{
+ i_gettimeofday(tv);
+ io_loop_stop(current_ioloop);
+}
+
+static void test_ioloop_fd_cb_left(struct test_ctx *ctx)
+{
+ ctx->got_left = TRUE;
+ if (ctx->got_left && ctx->got_right)
+ io_loop_stop(current_ioloop);
+}
+
+static void test_ioloop_fd_cb_right(struct test_ctx *ctx)
+{
+ ctx->got_right = TRUE;
+ if (ctx->got_left && ctx->got_right)
+ io_loop_stop(current_ioloop);
+}
+
+static void test_ioloop_fd_to(struct test_ctx *ctx)
+{
+ ctx->got_to = TRUE;
+ io_loop_stop(current_ioloop);
+}
+
+static void test_ioloop_fd(void)
+{
+ test_begin("ioloop fd");
+
+ struct test_ctx test_ctx;
+ int fds[2];
+ int ret = socketpair(AF_UNIX, SOCK_STREAM, 0, fds);
+
+ test_assert(ret == 0);
+ if (ret < 0) {
+ i_error("socketpair() failed: %m");
+ test_end();
+ return;
+ }
+
+ i_zero(&test_ctx);
+
+ struct ioloop *ioloop = io_loop_create();
+
+ struct io *io_left =
+ io_add(fds[0], IO_READ,
+ test_ioloop_fd_cb_left, &test_ctx);
+ struct io *io_right =
+ io_add(fds[1], IO_READ,
+ test_ioloop_fd_cb_right, &test_ctx);
+
+ struct timeout *to = timeout_add(2000, test_ioloop_fd_to, &test_ctx);
+
+ if (write(fds[0], "ltr", 3) != 3 ||
+ write(fds[1], "rtl", 3) != 3)
+ i_fatal("write() failed: %m");
+
+ io_loop_run(ioloop);
+
+ timeout_remove(&to);
+ io_remove(&io_left);
+ io_remove(&io_right);
+
+ test_assert(test_ctx.got_to == FALSE);
+ test_assert(test_ctx.got_left == TRUE);
+ test_assert(test_ctx.got_right == TRUE);
+
+ io_loop_destroy(&ioloop);
+ i_close_fd(&fds[0]);
+ i_close_fd(&fds[1]);
+
+ test_end();
+}
+
+static void test_ioloop_timeout(void)
+{
+ struct ioloop *ioloop, *ioloop2;
+ struct timeout *to, *to2;
+ struct timeval tv_start, tv_callback;
+
+ test_begin("ioloop timeout");
+
+ ioloop = io_loop_create();
+
+ /* add a timeout by moving it from another ioloop */
+ ioloop2 = io_loop_create();
+ test_assert(io_loop_is_empty(ioloop));
+ test_assert(io_loop_is_empty(ioloop2));
+ to2 = timeout_add(1000, timeout_callback, &tv_callback);
+ test_assert(io_loop_is_empty(ioloop));
+ test_assert(!io_loop_is_empty(ioloop2));
+ io_loop_set_current(ioloop);
+ to2 = io_loop_move_timeout(&to2);
+ test_assert(!io_loop_is_empty(ioloop));
+ test_assert(io_loop_is_empty(ioloop2));
+ io_loop_set_current(ioloop2);
+ io_loop_destroy(&ioloop2);
+
+ sleep(1);
+
+ /* add & remove immediately */
+ to = timeout_add(1000, timeout_callback, &tv_callback);
+ timeout_remove(&to);
+
+ /* add the timeout we're actually testing below */
+ to = timeout_add(1000, timeout_callback, &tv_callback);
+ i_gettimeofday(&tv_start);
+ io_loop_run(ioloop);
+ test_assert(timeval_diff_msecs(&tv_callback, &tv_start) >= 500);
+ timeout_remove(&to);
+ timeout_remove(&to2);
+ test_assert(io_loop_is_empty(ioloop));
+ io_loop_destroy(&ioloop);
+
+ test_end();
+}
+
+static void zero_timeout_callback(unsigned int *counter)
+{
+ *counter += 1;
+}
+
+static void test_ioloop_zero_timeout(void)
+{
+ struct ioloop *ioloop;
+ struct timeout *to;
+ struct io *io;
+ unsigned int counter = 0;
+ int fd[2];
+
+ test_begin("ioloop zero timeout");
+
+ if (pipe(fd) < 0)
+ i_fatal("pipe() failed: %m");
+ switch (fork()) {
+ case (pid_t)-1:
+ i_fatal("fork() failed: %m");
+ case 0:
+ sleep(1);
+ char c = 0;
+ if (write(fd[1], &c, 1) < 0)
+ i_fatal("write(pipe) failed: %m");
+ test_exit(0);
+ default:
+ break;
+ }
+
+ ioloop = io_loop_create();
+ to = timeout_add_short(0, zero_timeout_callback, &counter);
+ io = io_add(fd[0], IO_READ, io_loop_stop, ioloop);
+
+ io_loop_run(ioloop);
+ test_assert_ucmp(counter, >, 1000);
+
+ timeout_remove(&to);
+ io_remove(&io);
+ io_loop_destroy(&ioloop);
+ test_end();
+}
+
+struct zero_timeout_recreate_ctx {
+ struct timeout *to;
+ unsigned int counter;
+};
+
+static void
+zero_timeout_recreate_callback(struct zero_timeout_recreate_ctx *ctx)
+{
+ timeout_remove(&ctx->to);
+ ctx->to = timeout_add_short(0, zero_timeout_recreate_callback, ctx);
+ ctx->counter++;
+}
+
+static void test_ioloop_zero_timeout_recreate(void)
+{
+ struct ioloop *ioloop;
+ struct io *io;
+ struct zero_timeout_recreate_ctx ctx = { .counter = 0 };
+ int fd[2];
+
+ test_begin("ioloop zero timeout recreate");
+
+ if (pipe(fd) < 0)
+ i_fatal("pipe() failed: %m");
+ switch (fork()) {
+ case (pid_t)-1:
+ i_fatal("fork() failed: %m");
+ case 0:
+ sleep(1);
+ char c = 0;
+ if (write(fd[1], &c, 1) < 0)
+ i_fatal("write(pipe) failed: %m");
+ test_exit(0);
+ default:
+ break;
+ }
+
+ ioloop = io_loop_create();
+ ctx.to = timeout_add_short(0, zero_timeout_recreate_callback, &ctx);
+ io = io_add(fd[0], IO_READ, io_loop_stop, ioloop);
+
+ io_loop_run(ioloop);
+ test_assert_ucmp(ctx.counter, >, 1000);
+
+ timeout_remove(&ctx.to);
+ io_remove(&io);
+ io_loop_destroy(&ioloop);
+ test_end();
+}
+
+static void io_callback(void *context ATTR_UNUSED)
+{
+}
+
+static void test_ioloop_find_fd_conditions(void)
+{
+ static struct {
+ enum io_condition condition;
+ int fd[2];
+ struct io *io;
+ } tests[] = {
+ { IO_ERROR, { -1, -1 }, NULL },
+ { IO_READ, { -1, -1 }, NULL },
+ { IO_WRITE, { -1, -1 }, NULL },
+ { IO_READ | IO_WRITE, { -1, -1 }, NULL },
+ { IO_READ, { -1, -1 }, NULL } /* read+write as separate ios */
+ };
+ struct ioloop *ioloop;
+ struct io *io;
+ unsigned int i;
+
+ test_begin("ioloop find fd conditions");
+
+ ioloop = io_loop_create();
+
+ for (i = 0; i < N_ELEMENTS(tests); i++) {
+ if (socketpair(AF_UNIX, SOCK_STREAM, 0, tests[i].fd) < 0)
+ i_fatal("socketpair() failed: %m");
+ tests[i].io = io_add(tests[i].fd[0], tests[i].condition, io_callback, NULL);
+ }
+ io = io_add(tests[i-1].fd[0], IO_WRITE, io_callback, NULL);
+ tests[i-1].condition |= IO_WRITE;
+
+ for (i = 0; i < N_ELEMENTS(tests); i++)
+ test_assert_idx(io_loop_find_fd_conditions(ioloop, tests[i].fd[0]) == tests[i].condition, i);
+
+ io_remove(&io);
+ for (i = 0; i < N_ELEMENTS(tests); i++) {
+ io_remove(&tests[i].io);
+ i_close_fd(&tests[i].fd[0]);
+ i_close_fd(&tests[i].fd[1]);
+ }
+ io_loop_destroy(&ioloop);
+
+ test_end();
+}
+
+static void io_callback_pending_io(void *context ATTR_UNUSED)
+{
+ io_loop_stop(current_ioloop);
+}
+
+static void test_ioloop_pending_io(void)
+{
+ test_begin("ioloop pending io");
+
+ struct istream *is = i_stream_create_from_data("data", 4);
+ struct ioloop *ioloop = io_loop_create();
+ test_assert(io_loop_is_empty(ioloop));
+ struct io *io = io_add_istream(is, io_callback_pending_io, NULL);
+ test_assert(!io_loop_is_empty(ioloop));
+ io_loop_set_current(ioloop);
+ io_set_pending(io);
+ io_loop_run(ioloop);
+ io_remove(&io);
+ i_stream_unref(&is);
+ io_loop_destroy(&ioloop);
+
+ test_end();
+}
+
+static void test_ioloop_context_callback(struct ioloop_context *ctx)
+{
+ test_assert(io_loop_get_current_context(current_ioloop) == ctx);
+ io_loop_stop(current_ioloop);
+}
+
+static void test_ioloop_context(void)
+{
+ test_begin("ioloop context");
+ struct ioloop *ioloop = io_loop_create();
+ struct ioloop_context *ctx = io_loop_context_new(ioloop);
+
+ test_assert(io_loop_get_current_context(current_ioloop) == NULL);
+ io_loop_context_activate(ctx);
+ test_assert(io_loop_get_current_context(current_ioloop) == ctx);
+ struct timeout *to = timeout_add(0, test_ioloop_context_callback, ctx);
+
+ io_loop_run(ioloop);
+ test_assert(io_loop_get_current_context(current_ioloop) == NULL);
+ /* test that we don't crash at deinit if we leave the context active */
+ io_loop_context_activate(ctx);
+ test_assert(io_loop_get_current_context(current_ioloop) == ctx);
+
+ timeout_remove(&to);
+ io_loop_context_unref(&ctx);
+ io_loop_destroy(&ioloop);
+ test_end();
+}
+
+static void test_ioloop_context_events_run(struct event *root_event)
+{
+ struct ioloop *ioloop = io_loop_create();
+ struct ioloop_context *ctx1, *ctx2;
+
+ /* create context 1 */
+ ctx1 = io_loop_context_new(ioloop);
+ io_loop_context_switch(ctx1);
+ struct event *ctx1_event1 = event_create(NULL);
+ event_push_global(ctx1_event1);
+ struct event *ctx1_event2 = event_create(NULL);
+ event_push_global(ctx1_event2);
+ io_loop_context_deactivate(ctx1);
+
+ test_assert(event_get_global() == root_event);
+
+ /* create context 2 */
+ ctx2 = io_loop_context_new(ioloop);
+ io_loop_context_switch(ctx2);
+ struct event *ctx2_event1 = event_create(NULL);
+ event_push_global(ctx2_event1);
+ io_loop_context_deactivate(ctx2);
+
+ test_assert(event_get_global() == root_event);
+
+ /* test switching contexts */
+ io_loop_context_switch(ctx1);
+ test_assert(event_get_global() == ctx1_event2);
+ io_loop_context_switch(ctx2);
+ test_assert(event_get_global() == ctx2_event1);
+
+ /* test popping away events */
+ io_loop_context_switch(ctx1);
+ event_pop_global(ctx1_event2);
+ io_loop_context_switch(ctx2);
+ event_pop_global(ctx2_event1);
+ io_loop_context_switch(ctx1);
+ test_assert(event_get_global() == ctx1_event1);
+ io_loop_context_switch(ctx2);
+ test_assert(event_get_global() == root_event);
+
+ io_loop_context_deactivate(ctx2);
+ io_loop_context_unref(&ctx1);
+ io_loop_context_unref(&ctx2);
+ io_loop_destroy(&ioloop);
+
+ event_unref(&ctx1_event1);
+ event_unref(&ctx1_event2);
+ event_unref(&ctx2_event1);
+}
+
+static void test_ioloop_context_events(void)
+{
+ test_begin("ioloop context - no root event");
+ test_ioloop_context_events_run(NULL);
+ test_end();
+
+ test_begin("ioloop context - with root event");
+ struct event *root_event = event_create(NULL);
+ event_push_global(root_event);
+ test_ioloop_context_events_run(root_event);
+ event_pop_global(root_event);
+ event_unref(&root_event);
+ test_end();
+}
+
+void test_ioloop(void)
+{
+ test_ioloop_timeout();
+ test_ioloop_zero_timeout();
+ test_ioloop_zero_timeout_recreate();
+ test_ioloop_find_fd_conditions();
+ test_ioloop_pending_io();
+ test_ioloop_fd();
+ test_ioloop_context();
+ test_ioloop_context_events();
+}
diff --git a/src/lib/test-iostream-proxy.c b/src/lib/test-iostream-proxy.c
new file mode 100644
index 0000000..9086c5d
--- /dev/null
+++ b/src/lib/test-iostream-proxy.c
@@ -0,0 +1,113 @@
+/* Copyright (c) 2016-2018 Dovecot authors, see the included COPYING file */
+
+#include "test-lib.h"
+#include "istream.h"
+#include "ostream.h"
+#include "buffer.h"
+#include "ioloop.h"
+#include "iostream-proxy.h"
+
+#include <unistd.h>
+#include <sys/types.h>
+#include <sys/socket.h>
+
+static
+void completed(enum iostream_proxy_side side ATTR_UNUSED,
+ enum iostream_proxy_status status ATTR_UNUSED, int *u0)
+{
+ i_assert(*u0 > 0);
+ if (--*u0 == 0)
+ io_loop_stop(current_ioloop);
+}
+
+static
+void test_iostream_proxy_simple(void)
+{
+ size_t bytes;
+
+ test_begin("iostream_proxy");
+ int sfdl[2];
+ int sfdr[2];
+
+ int counter;
+
+ test_assert(socketpair(AF_UNIX, SOCK_STREAM, 0, sfdl) == 0);
+ test_assert(socketpair(AF_UNIX, SOCK_STREAM, 0, sfdr) == 0);
+
+ fd_set_nonblock(sfdl[0], TRUE);
+ fd_set_nonblock(sfdl[1], TRUE);
+ fd_set_nonblock(sfdr[0], TRUE);
+ fd_set_nonblock(sfdr[1], TRUE);
+
+ struct ioloop *ioloop = io_loop_create();
+
+ struct istream *left_in = i_stream_create_fd(sfdl[1], IO_BLOCK_SIZE);
+ struct ostream *left_out = o_stream_create_fd(sfdl[1], IO_BLOCK_SIZE);
+
+ struct istream *right_in = i_stream_create_fd(sfdr[1], IO_BLOCK_SIZE);
+ struct ostream *right_out = o_stream_create_fd(sfdr[1], IO_BLOCK_SIZE);
+
+ struct iostream_proxy *proxy;
+
+ proxy = iostream_proxy_create(left_in, left_out, right_in, right_out);
+ i_stream_unref(&left_in);
+ o_stream_unref(&left_out);
+ i_stream_unref(&right_in);
+ o_stream_unref(&right_out);
+
+ iostream_proxy_set_completion_callback(proxy, completed, &counter);
+ iostream_proxy_start(proxy);
+
+ left_in = i_stream_create_fd(sfdl[0], IO_BLOCK_SIZE);
+ left_out = o_stream_create_fd(sfdl[0], IO_BLOCK_SIZE);
+
+ right_in = i_stream_create_fd(sfdr[0], IO_BLOCK_SIZE);
+ right_out = o_stream_create_fd(sfdr[0], IO_BLOCK_SIZE);
+
+ test_assert(proxy != NULL);
+ test_assert(o_stream_send_str(left_out, "hello, world") > 0);
+ test_assert(o_stream_flush(left_out) > 0);
+ o_stream_unref(&left_out);
+ test_assert(shutdown(sfdl[0], SHUT_WR) == 0);
+
+ counter = 1;
+ io_loop_run(ioloop);
+
+ test_assert(i_stream_read(right_in) > 0);
+ test_assert(strcmp((const char*)i_stream_get_data(right_in, &bytes), "hello, world") == 0);
+ i_stream_skip(right_in, bytes);
+
+ test_assert(o_stream_send_str(right_out, "hello, world") > 0);
+ test_assert(o_stream_flush(right_out) > 0);
+ o_stream_unref(&right_out);
+ test_assert(shutdown(sfdr[0], SHUT_WR) == 0);
+
+ counter = 1;
+ io_loop_run(ioloop);
+
+ test_assert(i_stream_read(left_in) > 0);
+ test_assert(strcmp((const char*)i_stream_get_data(left_in, &bytes), "hello, world") == 0);
+ i_stream_skip(left_in, bytes);
+
+ iostream_proxy_unref(&proxy);
+
+ io_loop_destroy(&ioloop);
+
+ i_stream_unref(&left_in);
+ i_stream_unref(&right_in);
+
+ /* close fd */
+ i_close_fd(&sfdl[0]);
+ i_close_fd(&sfdl[1]);
+ i_close_fd(&sfdr[0]);
+ i_close_fd(&sfdr[1]);
+
+ test_end();
+}
+
+void test_iostream_proxy(void)
+{
+ T_BEGIN {
+ test_iostream_proxy_simple();
+ } T_END;
+}
diff --git a/src/lib/test-iostream-pump.c b/src/lib/test-iostream-pump.c
new file mode 100644
index 0000000..f970fba
--- /dev/null
+++ b/src/lib/test-iostream-pump.c
@@ -0,0 +1,325 @@
+/* Copyright (c) 2016-2018 Dovecot authors, see the included COPYING file */
+
+#include "test-lib.h"
+#include "istream.h"
+#include "ostream.h"
+#include "buffer.h"
+#include "str.h"
+#include "ioloop.h"
+#include "iostream-pump.h"
+#include "istream-failure-at.h"
+#include "ostream-failure-at.h"
+
+#include <unistd.h>
+#include <sys/types.h>
+#include <sys/socket.h>
+
+struct nonblock_ctx {
+ struct istream *in;
+ struct ostream *out;
+ uoff_t pos, max_size;
+};
+
+static unsigned char data[] = "hello, world";
+
+static void completed(enum iostream_pump_status status, int *u0)
+{
+ /* to somehow discern between error and success .. */
+ (*u0) -= (status == IOSTREAM_PUMP_STATUS_INPUT_EOF ? 1 : 2);
+ io_loop_stop(current_ioloop);
+}
+
+static void failed(int *u0)
+{
+ *u0 = -1; /* ensure failure */
+ io_loop_stop(current_ioloop);
+}
+
+static void pump_nonblocking_timeout(struct nonblock_ctx *ctx)
+{
+ switch (ctx->pos % 4) {
+ case 0:
+ break;
+ case 1:
+ /* allow more input */
+ if (ctx->in->blocking)
+ break;
+ if (ctx->pos/4 == ctx->max_size+1)
+ test_istream_set_allow_eof(ctx->in, TRUE);
+ else
+ test_istream_set_size(ctx->in, ctx->pos/4);
+ i_stream_set_input_pending(ctx->in, TRUE);
+ break;
+ case 2:
+ break;
+ case 3: {
+ /* allow more output. give always one byte less than the
+ input size so there's something in internal buffer. */
+ if (ctx->out->blocking)
+ break;
+ size_t size = ctx->pos/4;
+ if (size > 0)
+ test_ostream_set_max_output_size(ctx->out, size-1);
+ break;
+ }
+ }
+ ctx->pos++;
+}
+
+static const char *
+run_pump(struct istream *in, struct ostream *out, int *counter,
+ buffer_t *out_buffer)
+{
+ struct iostream_pump *pump;
+ struct ioloop *ioloop = io_loop_create();
+ io_loop_set_current(ioloop);
+ struct nonblock_ctx ctx = { in, out, 0, 0 };
+ struct timeout *to2 = NULL;
+
+ if (!in->blocking) {
+ test_assert(i_stream_get_size(in, TRUE, &ctx.max_size) > 0);
+ test_istream_set_size(in, 0);
+ test_istream_set_allow_eof(in, FALSE);
+ }
+ if (!out->blocking) {
+ test_ostream_set_max_output_size(out, 0);
+ }
+ if (!in->blocking || !out->blocking) {
+ to2 = timeout_add_short(0, pump_nonblocking_timeout, &ctx);
+ }
+
+ pump = iostream_pump_create(in, out);
+ i_stream_unref(&in);
+ o_stream_unref(&out);
+
+ iostream_pump_set_completion_callback(pump, completed, counter);
+ iostream_pump_start(pump);
+
+ alarm(5);
+ struct timeout *to = timeout_add(3000, failed, counter);
+
+ io_loop_run(current_ioloop);
+
+ timeout_remove(&to);
+ timeout_remove(&to2);
+ alarm(0);
+
+ test_assert(*counter == 0);
+
+ if (!ctx.out->blocking && ctx.in->stream_errno != 0 &&
+ ctx.out->stream_errno == 0) {
+ /* input failed, finish flushing output */
+ test_ostream_set_max_output_size(ctx.out, SIZE_MAX);
+ test_assert(o_stream_flush(ctx.out) > 0);
+ } else {
+ test_assert(o_stream_flush(ctx.out) != 0);
+ }
+
+ const char *ret = t_strdup(str_c(out_buffer));
+
+ iostream_pump_unref(&pump);
+ io_loop_destroy(&ioloop);
+ return ret;
+}
+
+static void
+test_iostream_setup(bool in_block, bool out_block,
+ struct istream **in_r, struct ostream **out_r,
+ buffer_t **out_buffer_r)
+{
+ *out_buffer_r = t_buffer_create(128);
+
+ *in_r = test_istream_create_data(data, sizeof(data));
+ (*in_r)->blocking = in_block;
+
+ if (out_block)
+ *out_r = test_ostream_create(*out_buffer_r);
+ else
+ *out_r = test_ostream_create_nonblocking(*out_buffer_r, 1);
+}
+
+static void
+test_iostream_pump_simple(bool in_block, bool out_block)
+{
+ int counter;
+ struct istream *in;
+ struct ostream *out;
+ buffer_t *buffer;
+
+ test_begin(t_strdup_printf("iostream_pump "
+ "(in=%sblocking, out=%sblocking)",
+ (in_block ? "" : "non-"),
+ (out_block ? "" : "non-")));
+
+ test_iostream_setup(in_block, out_block, &in, &out, &buffer);
+ counter = 1;
+
+ test_assert(strcmp(run_pump(in, out, &counter, buffer),
+ "hello, world") == 0);
+
+ test_end();
+}
+
+static void
+test_iostream_pump_failure_start_read(bool in_block, bool out_block)
+{
+ int counter;
+ struct istream *in, *in_2;
+ struct ostream *out;
+ buffer_t *buffer;
+
+ test_begin(t_strdup_printf("iostream_pump failure start-read "
+ "(in=%sblocking, out=%sblocking)",
+ (in_block ? "" : "non-"),
+ (out_block ? "" : "non-")));
+
+ test_iostream_setup(in_block, out_block, &in_2, &out, &buffer);
+ in = i_stream_create_failure_at(in_2, 0, EIO, "test pump fail");
+ i_stream_unref(&in_2);
+ counter = 2;
+ test_assert(strcmp(run_pump(in, out, &counter, buffer), "") == 0);
+
+ test_end();
+}
+
+static void
+test_iostream_pump_failure_mid_read(bool in_block, bool out_block)
+{
+ int counter;
+ struct istream *in, *in_2;
+ struct ostream *out;
+ buffer_t *buffer;
+
+ test_begin(t_strdup_printf("iostream_pump failure mid-read "
+ "(in=%sblocking, out=%sblocking)",
+ (in_block ? "" : "non-"),
+ (out_block ? "" : "non-")));
+
+ test_iostream_setup(in_block, out_block, &in_2, &out, &buffer);
+ in = i_stream_create_failure_at(in_2, 4, EIO, "test pump fail");
+ i_stream_unref(&in_2);
+ counter = 2;
+ test_assert(strcmp(run_pump(in, out, &counter, buffer), "hell") == 0);
+
+ test_end();
+}
+
+static void
+test_iostream_pump_failure_end_read(bool in_block, bool out_block)
+{
+ int counter;
+ struct istream *in, *in_2;
+ struct ostream *out;
+ buffer_t *buffer;
+
+ test_begin(t_strdup_printf("iostream_pump failure mid-read "
+ "(in=%sblocking, out=%sblocking)",
+ (in_block ? "" : "non-"),
+ (out_block ? "" : "non-")));
+
+ test_iostream_setup(in_block, out_block, &in_2, &out, &buffer);
+ in = i_stream_create_failure_at_eof(in_2, EIO, "test pump fail");
+ i_stream_unref(&in_2);
+ counter = 2;
+ test_assert(strcmp(run_pump(in, out, &counter, buffer),
+ "hello, world") == 0);
+
+ test_end();
+}
+
+static void
+test_iostream_pump_failure_start_write(bool in_block, bool out_block)
+{
+ int counter;
+ struct istream *in;
+ struct ostream *out, *out_2;
+ buffer_t *buffer;
+
+ test_begin(t_strdup_printf("iostream_pump failure start-write "
+ "(in=%sblocking, out=%sblocking)",
+ (in_block ? "" : "non-"),
+ (out_block ? "" : "non-")));
+
+ test_iostream_setup(in_block, out_block, &in, &out_2, &buffer);
+ out = o_stream_create_failure_at(out_2, 0, "test pump fail");
+ o_stream_unref(&out_2);
+ counter = 2;
+ test_assert(strcmp(run_pump(in, out, &counter, buffer), "") == 0);
+
+ test_end();
+}
+
+static void
+test_iostream_pump_failure_mid_write(bool in_block, bool out_block)
+{
+ int counter;
+ struct istream *in;
+ struct ostream *out, *out_2;
+ buffer_t *buffer;
+
+ test_begin(t_strdup_printf("iostream_pump failure mid-write "
+ "(in=%sblocking, out=%sblocking)",
+ (in_block ? "" : "non-"),
+ (out_block ? "" : "non-")));
+
+ test_iostream_setup(in_block, out_block, &in, &out_2, &buffer);
+ out = o_stream_create_failure_at(out_2, 4, "test pump fail");
+ o_stream_unref(&out_2);
+ counter = 2;
+
+ /* "hel" because the last byte is only in internal buffer */
+ test_assert(strcmp(run_pump(in, out, &counter, buffer),
+ (out_block ? (in_block ? "" : "hell") :
+ "hel")) == 0);
+
+ test_end();
+}
+
+static void
+test_iostream_pump_failure_end_write(bool in_block, bool out_block)
+{
+ int counter;
+ struct istream *in;
+ struct ostream *out, *out_2;
+ buffer_t *buffer;
+
+ if (!out_block || !in_block) {
+ /* we'll get flushes constantly */
+ return;
+ }
+
+ test_begin("iostream_pump failure end-write (blocking)");
+
+ test_iostream_setup(in_block, out_block, &in, &out_2, &buffer);
+ out = o_stream_create_failure_at_flush(out_2, "test pump fail");
+ o_stream_unref(&out_2);
+ counter = 2;
+ test_assert(strcmp(run_pump(in, out, &counter, buffer),
+ "hello, world") == 0);
+
+ test_end();
+}
+
+static void
+test_iostream_pump_real(void)
+{
+ for(int i = 0; i < 3; i++) {
+ bool in_block = ((i & BIT(0)) != 0);
+ bool out_block = ((i & BIT(1)) != 0);
+
+ test_iostream_pump_simple(in_block, out_block);
+ test_iostream_pump_failure_start_read(in_block, out_block);
+ test_iostream_pump_failure_mid_read(in_block, out_block);
+ test_iostream_pump_failure_end_read(in_block, out_block);
+ test_iostream_pump_failure_start_write(in_block, out_block);
+ test_iostream_pump_failure_mid_write(in_block, out_block);
+ test_iostream_pump_failure_end_write(in_block, out_block);
+ }
+}
+
+void test_iostream_pump(void)
+{
+ T_BEGIN {
+ test_iostream_pump_real();
+ } T_END;
+}
diff --git a/src/lib/test-iostream-temp.c b/src/lib/test-iostream-temp.c
new file mode 100644
index 0000000..cfb8658
--- /dev/null
+++ b/src/lib/test-iostream-temp.c
@@ -0,0 +1,150 @@
+/* Copyright (c) 2016-2018 Dovecot authors, see the included COPYING file */
+
+#include "test-lib.h"
+#include "istream.h"
+#include "ostream.h"
+#include "iostream-temp.h"
+
+#include <unistd.h>
+#include <fcntl.h>
+
+static void test_iostream_temp_create_sized_memory(void)
+{
+ struct ostream *output;
+
+ test_begin("iostream_temp_create_sized() memory");
+ output = iostream_temp_create_sized(".intentional-nonexistent-error/", 0, "test", 4);
+ test_assert(o_stream_send(output, "123", 3) == 3);
+ test_assert(output->offset == 3);
+ test_assert(o_stream_send(output, "4", 1) == 1);
+ test_assert(output->offset == 4);
+ test_assert(o_stream_get_fd(output) == -1);
+
+ /* now we'll try to switch to writing to a file, but it'll fail */
+ test_expect_error_string("safe_mkstemp");
+ test_assert(o_stream_send(output, "5", 1) == 1);
+ test_expect_no_more_errors();
+
+ test_assert(o_stream_get_fd(output) == -1);
+ o_stream_destroy(&output);
+ test_end();
+}
+
+static void test_iostream_temp_create_sized_disk(void)
+{
+ struct ostream *output;
+
+ test_begin("iostream_temp_create_sized() disk");
+ output = iostream_temp_create_sized(".", 0, "test", 4);
+ test_assert(o_stream_send(output, "123", 3) == 3);
+ test_assert(output->offset == 3);
+ test_assert(o_stream_send(output, "4", 1) == 1);
+ test_assert(output->offset == 4);
+ test_assert(o_stream_get_fd(output) == -1);
+ test_assert(o_stream_send(output, "5", 1) == 1);
+ test_assert(output->offset == 5);
+ test_assert(o_stream_get_fd(output) != -1);
+ o_stream_destroy(&output);
+ test_end();
+}
+
+static void test_iostream_temp_create_write_error(void)
+{
+ struct ostream *output;
+
+ test_begin("iostream_temp_create_sized() write error");
+ output = iostream_temp_create_sized(".", 0, "test", 1);
+
+ test_assert(o_stream_send(output, "123", 3) == 3);
+ test_assert(o_stream_get_fd(output) != -1);
+ test_assert(output->offset == 3);
+ test_assert(o_stream_temp_move_to_memory(output) == 0);
+ test_assert(o_stream_get_fd(output) == -1);
+ test_assert(o_stream_send(output, "45", 2) == 2);
+ test_assert(output->offset == 5);
+
+ const unsigned char *data;
+ size_t size;
+ struct istream *input = iostream_temp_finish(&output, 128);
+ test_assert(i_stream_read_bytes(input, &data, &size, 5) == 1 &&
+ memcmp(data, "12345", 5) == 0);
+ i_stream_destroy(&input);
+
+ test_end();
+}
+
+static void test_iostream_temp_istream(void)
+{
+ struct istream *input, *input2, *temp_input;
+ struct ostream *output;
+ int fd;
+
+ test_begin("iostream_temp istream");
+
+ fd = open(".temp.istream", O_RDWR | O_CREAT | O_TRUNC, 0600);
+ if (fd == -1)
+ i_fatal("create(.temp.istream) failed: %m");
+ test_assert(write(fd, "foobar", 6) == 6);
+ test_assert(lseek(fd, 0, SEEK_SET) == 0);
+
+ input = i_stream_create_fd_autoclose(&fd, 1024);
+ /* a working fd-dup */
+ output = iostream_temp_create_sized(".nonexistent/",
+ IOSTREAM_TEMP_FLAG_TRY_FD_DUP, "test", 1);
+ test_assert(o_stream_send_istream(output, input) == OSTREAM_SEND_ISTREAM_RESULT_FINISHED);
+ test_assert(output->offset == 6);
+ temp_input = iostream_temp_finish(&output, 128);
+ test_assert(i_stream_read(temp_input) == 6);
+ i_stream_destroy(&temp_input);
+
+ /* non-working fd-dup: write data before sending istream */
+ i_stream_seek(input, 0);
+ output = iostream_temp_create_sized(".intentional-nonexistent-error/",
+ IOSTREAM_TEMP_FLAG_TRY_FD_DUP, "test", 4);
+ test_assert(o_stream_send(output, "1234", 4) == 4);
+ test_assert(output->offset == 4);
+ test_expect_error_string("safe_mkstemp");
+ test_assert(o_stream_send_istream(output, input) == OSTREAM_SEND_ISTREAM_RESULT_FINISHED);
+ test_assert(output->offset == 10);
+ test_expect_no_more_errors();
+ o_stream_destroy(&output);
+
+ /* non-working fd-dup: write data after sending istream */
+ i_stream_seek(input, 0);
+ output = iostream_temp_create_sized(".intentional-nonexistent-error/",
+ IOSTREAM_TEMP_FLAG_TRY_FD_DUP, "test", 4);
+ test_assert(o_stream_send_istream(output, input) == OSTREAM_SEND_ISTREAM_RESULT_FINISHED);
+ test_assert(output->offset == 6);
+ test_expect_error_string("safe_mkstemp");
+ test_assert(o_stream_send(output, "1", 1) == 1);
+ test_assert(output->offset == 7);
+ test_expect_no_more_errors();
+ o_stream_destroy(&output);
+
+ /* non-working fd-dup: send two istreams */
+ i_stream_seek(input, 0);
+ input2 = i_stream_create_limit(input, UOFF_T_MAX);
+ output = iostream_temp_create_sized(".intentional-nonexistent-error/",
+ IOSTREAM_TEMP_FLAG_TRY_FD_DUP, "test", 4);
+ test_assert(o_stream_send_istream(output, input) == OSTREAM_SEND_ISTREAM_RESULT_FINISHED);
+ test_assert(output->offset == 6);
+ test_expect_error_string("safe_mkstemp");
+ test_assert(o_stream_send_istream(output, input2) == OSTREAM_SEND_ISTREAM_RESULT_FINISHED);
+ test_assert(output->offset == 12);
+ test_expect_no_more_errors();
+ o_stream_destroy(&output);
+ i_stream_unref(&input2);
+
+ i_stream_destroy(&input);
+
+ i_unlink(".temp.istream");
+ test_end();
+}
+
+void test_iostream_temp(void)
+{
+ test_iostream_temp_create_sized_memory();
+ test_iostream_temp_create_sized_disk();
+ test_iostream_temp_create_write_error();
+ test_iostream_temp_istream();
+}
diff --git a/src/lib/test-iso8601-date.c b/src/lib/test-iso8601-date.c
new file mode 100644
index 0000000..0b584dd
--- /dev/null
+++ b/src/lib/test-iso8601-date.c
@@ -0,0 +1,147 @@
+/* Copyright (c) 2009-2018 Dovecot authors, see the included COPYING file */
+
+#include "test-lib.h"
+#include "test-common.h"
+#include "iso8601-date.h"
+
+#include <time.h>
+
+struct iso8601_date_test {
+ const char *date_in;
+ const char *date_out;
+
+ struct tm tm;
+ int zone_offset;
+};
+
+/* Valid date tests */
+struct iso8601_date_test valid_date_tests[] = {
+ {
+ .date_in = "2007-11-07T23:05:34+00:00",
+ .tm = {
+ .tm_year = 107, .tm_mon = 10, .tm_mday = 7,
+ .tm_hour = 23, .tm_min = 5, .tm_sec = 34 },
+ },{
+ .date_in = "2011-01-07T21:03:31+00:30",
+ .tm = {
+ .tm_year = 111, .tm_mon = 0, .tm_mday = 7,
+ .tm_hour = 21, .tm_min = 3, .tm_sec = 31 },
+ .zone_offset = 30
+ },{
+ .date_in = "2006-05-09T18:04:12+05:30",
+ .tm = {
+ .tm_year = 106, .tm_mon = 4, .tm_mday = 9,
+ .tm_hour = 18, .tm_min = 4, .tm_sec = 12 },
+ .zone_offset = 5*60+30
+ },{
+ .date_in = "1975-10-30T06:33:29Z",
+ .date_out = "1975-10-30T06:33:29+00:00",
+ .tm = {
+ .tm_year = 75, .tm_mon = 9, .tm_mday = 30,
+ .tm_hour = 6, .tm_min = 33, .tm_sec = 29 },
+ },{
+ .date_in = "1988-04-24t15:02:12z",
+ .date_out = "1988-04-24T15:02:12+00:00",
+ .tm = {
+ .tm_year = 88, .tm_mon = 3, .tm_mday = 24,
+ .tm_hour = 15, .tm_min = 2, .tm_sec = 12 },
+ },{
+ .date_in = "2012-02-29T08:12:34.23198Z",
+ .date_out = "2012-02-29T08:12:34+00:00",
+ .tm = {
+ .tm_year = 112, .tm_mon = 1, .tm_mday = 29,
+ .tm_hour = 8, .tm_min = 12, .tm_sec = 34 },
+ }
+};
+
+unsigned int valid_date_test_count = N_ELEMENTS(valid_date_tests);
+
+static void test_iso8601_date_valid(void)
+{
+ unsigned int i;
+
+ for (i = 0; i < valid_date_test_count; i++) T_BEGIN {
+ const char *date_in, *date_out, *pdate_out;
+ struct tm *tm = &valid_date_tests[i].tm, ptm;
+ int zone_offset = valid_date_tests[i].zone_offset, pzone_offset;
+ bool result;
+
+ date_in = valid_date_tests[i].date_in;
+ date_out = valid_date_tests[i].date_out == NULL ?
+ date_in : valid_date_tests[i].date_out;
+
+ test_begin(t_strdup_printf("iso8601 date valid [%d]", i));
+
+ result = iso8601_date_parse_tm
+ ((const unsigned char *)date_in, strlen(date_in), &ptm, &pzone_offset);
+ test_out(t_strdup_printf("parse %s", date_in), result);
+ if (result) {
+ bool equal = tm->tm_year == ptm.tm_year && tm->tm_mon == ptm.tm_mon &&
+ tm->tm_mday == ptm.tm_mday && tm->tm_hour == ptm.tm_hour &&
+ tm->tm_min == ptm.tm_min && tm->tm_sec == ptm.tm_sec;
+
+ test_out("valid timestamp", equal);
+ test_out_reason("valid timezone", zone_offset == pzone_offset,
+ t_strdup_printf("%d", pzone_offset));
+
+ pdate_out = iso8601_date_create_tm(tm, zone_offset);
+ test_out_reason("valid create", strcmp(date_out, pdate_out) == 0,
+ pdate_out);
+ }
+
+ test_end();
+ } T_END;
+}
+
+/* Invalid date tests */
+const char *invalid_date_tests[] = {
+ "200-11-17T23:05:34+00:00",
+ "2007:11-17T23:05:34+00:00",
+ "2007-11?17T23:05:34+00:00",
+ "2007-49-17T23:05:34+00:00",
+ "2007-11-77T23:05:34+00:00",
+ "2007-11-17K23:05:34+00:00",
+ "2007-11-13T59:05:34+00:00",
+ "2007-112-13T12:15:34+00:00",
+ "2007-11-133T12:15:34+00:00",
+ "2007-11-13T12J15:34+00:00",
+ "2007-11-13T12:15*34+00:00",
+ "2007-11-13T12:15:34/00:00",
+ "2007-11-13T12:15:34+00-00",
+ "2007-11-13T123:15:34+00:00",
+ "2007-11-13T12:157:34+00:00",
+ "2007-11-13T12:15:342+00:00",
+ "2007-11-13T12:15:34+001:00",
+ "2007-11-13T12:15:32+00:006",
+ "2007-02-29T15:13:21Z"
+};
+
+unsigned int invalid_date_test_count = N_ELEMENTS(invalid_date_tests);
+
+static void test_iso8601_date_invalid(void)
+{
+ unsigned int i;
+
+ for (i = 0; i < invalid_date_test_count; i++) T_BEGIN {
+ const char *date_in;
+ struct tm tm;
+ int tz;
+ bool result;
+
+ date_in = invalid_date_tests[i];
+
+ test_begin(t_strdup_printf("iso8601 date invalid [%d]", i));
+
+ result = iso8601_date_parse_tm
+ ((const unsigned char *)date_in, strlen(date_in), &tm, &tz);
+ test_out(t_strdup_printf("parse %s", date_in), !result);
+
+ test_end();
+ } T_END;
+}
+
+void test_iso8601_date(void)
+{
+ test_iso8601_date_valid();
+ test_iso8601_date_invalid();
+}
diff --git a/src/lib/test-istream-base64-decoder.c b/src/lib/test-istream-base64-decoder.c
new file mode 100644
index 0000000..ea45f6d
--- /dev/null
+++ b/src/lib/test-istream-base64-decoder.c
@@ -0,0 +1,337 @@
+/* Copyright (c) 2010-2018 Dovecot authors, see the included COPYING file */
+
+#include "test-lib.h"
+#include "str.h"
+#include "istream-private.h"
+#include "istream-base64.h"
+#include "istream-sized.h"
+#include "base64.h"
+
+struct base64_istream_test {
+ const char *input;
+ const char *output;
+ int stream_errno;
+};
+
+static const struct base64_istream_test base64_tests[] = {
+ { "", "", 0 },
+ { "aGVsbG8gd29ybGQ=", "hello world", 0 },
+ { "\naGVs\nbG8g\nd29y\nbGQ=\n", "hello world", 0 },
+ { " aGVs \r\n bG8g \r\n d29y \t \r\n bGQ= \r\n\r\n",
+ "hello world", 0 },
+ { "0JPQvtCy0L7RgNGPzIHRgiwg0YfRgtC+INC60YPRgCDQtNC+0Y/MgdGCLg==",
+ "\xd0\x93\xd0\xbe\xd0\xb2\xd0\xbe\xd1\x80\xd1\x8f\xcc"
+ "\x81\xd1\x82\x2c\x20\xd1\x87\xd1\x82\xd0\xbe\x20\xd0"
+ "\xba\xd1\x83\xd1\x80\x20\xd0\xb4\xd0\xbe\xd1\x8f\xcc"
+ "\x81\xd1\x82\x2e", 0 },
+ { "\r", "", 0 },
+ { "\n", "", 0 },
+ { "\r\n", "", 0 },
+ { " ", "", 0 },
+ { "foo", "\x7e\x8a", EPIPE },
+ { "foo ","\x7e\x8a", EPIPE },
+ { "Zm9vC", "foo", EPIPE },
+ { "Zm9v!", "foo", EINVAL },
+ { "Zm9!v", "fo", EINVAL },
+ { "Zm9 v", "foo", 0 },
+ { "Zm 9v", "foo", 0 },
+ { "Z m9v", "foo", 0 },
+};
+
+static const struct base64_istream_test base64url_tests[] = {
+ { "", "", 0 },
+ { "aGVsbG8gd29ybGQ=", "hello world", 0 },
+ { "\naGVs\nbG8g\nd29y\nbGQ=\n", "hello world", 0 },
+ { " aGVs \r\n bG8g \r\n d29y \t \r\n bGQ= \r\n\r\n",
+ "hello world", 0 },
+ { "0JPQvtCy0L7RgNGPzIHRgiwg0YfRgtC-INC60YPRgCDQtNC-0Y_MgdGCLg==",
+ "\xd0\x93\xd0\xbe\xd0\xb2\xd0\xbe\xd1\x80\xd1\x8f\xcc"
+ "\x81\xd1\x82\x2c\x20\xd1\x87\xd1\x82\xd0\xbe\x20\xd0"
+ "\xba\xd1\x83\xd1\x80\x20\xd0\xb4\xd0\xbe\xd1\x8f\xcc"
+ "\x81\xd1\x82\x2e", 0 },
+ { "\r", "", 0 },
+ { "\n", "", 0 },
+ { "\r\n", "", 0 },
+ { " ", "", 0 },
+ { "foo", "\x7e\x8a", EPIPE },
+ { "foo ","\x7e\x8a", EPIPE },
+ { "Zm9vC", "foo", EPIPE },
+ { "Zm9v!", "foo", EINVAL },
+ { "Zm9!v", "fo", EINVAL },
+ { "Zm9 v", "foo", 0 },
+ { "Zm 9v", "foo", 0 },
+ { "Z m9v", "foo", 0 },
+};
+
+static void
+decode_test(unsigned int base64_input_len,
+ struct istream *input_data, struct istream *input,
+ const char *output, int stream_errno)
+{
+ const unsigned char *data;
+ size_t i, size;
+ int ret = 0;
+
+ for (i = 1; i <= base64_input_len; i++) {
+ test_istream_set_size(input_data, i);
+ while ((ret = i_stream_read(input)) > 0) ;
+ if (ret == -1 && stream_errno != 0)
+ break;
+ test_assert(ret == 0);
+ }
+ if (ret == 0) {
+ test_istream_set_allow_eof(input_data, TRUE);
+ while ((ret = i_stream_read(input)) > 0) ;
+ }
+ test_assert(ret == -1);
+ test_assert(input->stream_errno == stream_errno);
+
+ data = i_stream_get_data(input, &size);
+ test_assert(size == strlen(output));
+ if (size > 0)
+ test_assert(memcmp(data, output, size) == 0);
+}
+
+static void
+decode_base64_test(const char *base64_input, const char *output,
+ int stream_errno)
+{
+ unsigned int base64_input_len = strlen(base64_input);
+ struct istream *input_data, *input;
+
+ input_data = test_istream_create_data(base64_input, base64_input_len);
+ test_istream_set_allow_eof(input_data, FALSE);
+ input = i_stream_create_base64_decoder(input_data);
+
+ decode_test(base64_input_len, input_data, input, output, stream_errno);
+
+ i_stream_unref(&input);
+ i_stream_unref(&input_data);
+}
+
+static void
+decode_base64url_test(const char *base64_input, const char *output,
+ int stream_errno)
+{
+ unsigned int base64_input_len = strlen(base64_input);
+ struct istream *input_data, *input;
+
+ input_data = test_istream_create_data(base64_input, base64_input_len);
+ test_istream_set_allow_eof(input_data, FALSE);
+ input = i_stream_create_base64url_decoder(input_data);
+
+ decode_test(base64_input_len, input_data, input, output, stream_errno);
+
+ i_stream_unref(&input);
+ i_stream_unref(&input_data);
+}
+
+static void
+test_istream_base64_io_random(void)
+{
+ unsigned char in_buf[2048];
+ size_t in_buf_size;
+ buffer_t *out_buf;
+ unsigned int i, j;
+ int ret;
+
+ out_buf = t_buffer_create(sizeof(in_buf));
+
+ test_begin("istream base64 random I/O");
+
+ for (i = 0; !test_has_failed() && i < 4000; i++) {
+ struct istream *input1, *input2, *input3, *input4, *input5;
+ struct istream *sinput1, *sinput2, *sinput3, *sinput4;
+ struct istream *top_input;
+ const unsigned char *data;
+ unsigned int chpl1, chpl2;
+ unsigned int sized_streams;
+ unsigned int crlf_encode;
+ size_t size;
+ struct base64_encoder b64enc;
+
+ /* Initialize test data */
+ in_buf_size = i_rand_limit(sizeof(in_buf));
+ for (j = 0; j < in_buf_size; j++)
+ in_buf[j] = i_rand_uchar();
+
+ /* Reset final output buffer */
+ buffer_set_used_size(out_buf, 0);
+
+ /* Determine line lengths */
+ chpl1 = i_rand_limit(30)*4;
+ chpl2 = i_rand_limit(30)*4;
+
+ /* Create stream for test data */
+ input1 = i_stream_create_from_data(in_buf, in_buf_size);
+ i_stream_set_name(input1, "[data]");
+
+ /* Determine which stages have sized streams */
+ sized_streams = i_rand_minmax(0x00, 0x0f);
+ /* Determine which stages use CRLF */
+ crlf_encode = i_rand_minmax(0x00, 0x03);
+
+ /* Create first encoder stream */
+ input2 = i_stream_create_base64_encoder(
+ input1, chpl1, HAS_ALL_BITS(crlf_encode, BIT(0)));
+ i_stream_set_name(input2, "[base64_encoder #1]");
+
+ if (HAS_ALL_BITS(sized_streams, BIT(0))) {
+ /* Wrap the first encoder stream in a sized stream to
+ check size and trigger any buffer overflow problems
+ */
+ base64_encode_init(&b64enc, &base64_scheme,
+ (HAS_ALL_BITS(crlf_encode, BIT(0)) ?
+ BASE64_ENCODE_FLAG_CRLF : 0),
+ chpl1);
+ sinput1 = i_stream_create_sized(input2,
+ base64_get_full_encoded_size(&b64enc,
+ in_buf_size));
+ i_stream_set_name(sinput1, "[sized #1]");
+ } else {
+ sinput1 = input2;
+ i_stream_ref(sinput1);
+ }
+
+ /* Create first decoder stream */
+ input3 = i_stream_create_base64_decoder(sinput1);
+ i_stream_set_name(input3, "[base64_decoder #1]");
+
+ if (HAS_ALL_BITS(sized_streams, BIT(1))) {
+ /* Wrap the first decoder stream in a sized stream to
+ check size and trigger any buffer overflow problems
+ */
+ sinput2 = i_stream_create_sized(input3, in_buf_size);
+ i_stream_set_name(sinput2, "[sized #2]");
+ } else {
+ sinput2 = input3;
+ i_stream_ref(sinput2);
+ }
+
+ /* Create second encoder stream */
+ input4 = i_stream_create_base64_encoder(
+ sinput2, chpl2, HAS_ALL_BITS(crlf_encode, BIT(1)));
+ i_stream_set_name(input4, "[base64_encoder #2]");
+
+ if (HAS_ALL_BITS(sized_streams, BIT(2))) {
+ /* Wrap the second encoder stream in a sized stream to
+ check size and trigger any buffer overflow problems
+ */
+ base64_encode_init(&b64enc, &base64_scheme,
+ (HAS_ALL_BITS(crlf_encode, BIT(1)) ?
+ BASE64_ENCODE_FLAG_CRLF : 0),
+ chpl2);
+ sinput3 = i_stream_create_sized(input4,
+ base64_get_full_encoded_size(&b64enc,
+ in_buf_size));
+ i_stream_set_name(sinput3, "[sized #3]");
+ } else {
+ sinput3 = input4;
+ i_stream_ref(sinput3);
+ }
+
+ /* Create second deoder stream */
+ input5 = i_stream_create_base64_decoder(sinput3);
+ i_stream_set_name(input5, "[base64_decoder #2]");
+
+ if (HAS_ALL_BITS(sized_streams, BIT(3))) {
+ /* Wrap the second decoder stream in a sized stream to
+ check size and trigger any buffer overflow problems
+ */
+ sinput4 = i_stream_create_sized(input5, in_buf_size);
+ i_stream_set_name(sinput4, "[sized #4]");
+ } else {
+ sinput4 = input5;
+ i_stream_ref(sinput4);
+ }
+
+
+ /* Assign random buffer sizes */
+ i_stream_set_max_buffer_size(input5, i_rand_minmax(1, 512));
+ i_stream_set_max_buffer_size(input4, i_rand_minmax(1, 512));
+ i_stream_set_max_buffer_size(input3, i_rand_minmax(1, 512));
+ i_stream_set_max_buffer_size(input2, i_rand_minmax(1, 512));
+
+ /* Read the outer stream in full with random increments. */
+ top_input = sinput4;
+ while ((ret = i_stream_read_more(
+ top_input, &data, &size)) > 0) {
+ size_t ch = i_rand_limit(512);
+
+ size = I_MIN(size, ch);
+ buffer_append(out_buf, data, size);
+ i_stream_skip(top_input, size);
+ }
+ if (ret < 0 && top_input->stream_errno == 0) {
+ data = i_stream_get_data(top_input, &size);
+ if (size > 0) {
+ buffer_append(out_buf, data, size);
+ i_stream_skip(top_input, size);
+ }
+ }
+
+ /* Assert stream status */
+ test_assert_idx(ret < 0 && top_input->stream_errno == 0, i);
+ /* Assert input/output equality */
+ test_assert_idx(out_buf->used == in_buf_size &&
+ memcmp(in_buf, out_buf->data, in_buf_size) == 0,
+ i);
+
+ if (top_input->stream_errno != 0) {
+ i_error("%s: %s", i_stream_get_name(input1),
+ i_stream_get_error(input1));
+ i_error("%s: %s", i_stream_get_name(input2),
+ i_stream_get_error(input2));
+ i_error("%s: %s", i_stream_get_name(input3),
+ i_stream_get_error(input3));
+ i_error("%s: %s", i_stream_get_name(input4),
+ i_stream_get_error(input4));
+ i_error("%s: %s", i_stream_get_name(input5),
+ i_stream_get_error(input5));
+ }
+
+ if (test_has_failed()) {
+ i_info("Test parameters: size=%zu "
+ "line_length_1=%u line_length_2=%u",
+ in_buf_size, chpl1, chpl2);
+ }
+
+ /* Clean up */
+ i_stream_unref(&input1);
+ i_stream_unref(&input2);
+ i_stream_unref(&input3);
+ i_stream_unref(&input4);
+ i_stream_unref(&input5);
+ i_stream_unref(&sinput1);
+ i_stream_unref(&sinput2);
+ i_stream_unref(&sinput3);
+ i_stream_unref(&sinput4);
+ }
+ test_end();
+}
+
+void test_istream_base64_decoder(void)
+{
+ unsigned int i;
+
+ for (i = 0; i < N_ELEMENTS(base64_tests); i++) {
+ const struct base64_istream_test *test = &base64_tests[i];
+
+ test_begin(t_strdup_printf("istream base64 decoder %u", i+1));
+ decode_base64_test(test->input, test->output,
+ test->stream_errno);
+ test_end();
+ }
+
+ for (i = 0; i < N_ELEMENTS(base64url_tests); i++) {
+ const struct base64_istream_test *test = &base64url_tests[i];
+
+ test_begin(t_strdup_printf("istream base64url decoder %u",
+ i+1));
+ decode_base64url_test(test->input, test->output,
+ test->stream_errno);
+ test_end();
+ }
+
+ test_istream_base64_io_random();
+}
diff --git a/src/lib/test-istream-base64-encoder.c b/src/lib/test-istream-base64-encoder.c
new file mode 100644
index 0000000..9fc0608
--- /dev/null
+++ b/src/lib/test-istream-base64-encoder.c
@@ -0,0 +1,222 @@
+/* Copyright (c) 2010-2018 Dovecot authors, see the included COPYING file */
+
+#include "test-lib.h"
+#include "str.h"
+#include "istream-private.h"
+#include "istream-base64.h"
+
+struct base64_istream_test {
+ const char *input;
+ unsigned int chars_per_line;
+ bool crlf;
+ const char *output;
+};
+
+static const struct base64_istream_test base64_tests[] = {
+ { "", 80, FALSE, "" },
+ { "1", 80, FALSE, "MQ==" },
+ { "12", 80, FALSE, "MTI=" },
+ { "123", 80, FALSE, "MTIz" },
+ { "1234", 80, FALSE, "MTIzNA==" },
+ { "12345", 80, FALSE, "MTIzNDU=" },
+ { "hello world", 80, FALSE, "aGVsbG8gd29ybGQ=" },
+ { "hello world", 4, FALSE, "aGVs\nbG8g\nd29y\nbGQ=" },
+ { "hello world", 4, TRUE, "aGVs\r\nbG8g\r\nd29y\r\nbGQ=" },
+ { "hello worlds", 80, FALSE, "aGVsbG8gd29ybGRz" },
+ { "hello worlds", 4, FALSE, "aGVs\nbG8g\nd29y\nbGRz" },
+ { "hello worlds", 4, TRUE, "aGVs\r\nbG8g\r\nd29y\r\nbGRz" },
+ { "hell world", 80, FALSE, "aGVsbCB3b3JsZA==" },
+ { "hell world", 4, FALSE, "aGVs\nbCB3\nb3Js\nZA==" },
+ { "hell world", 4, TRUE, "aGVs\r\nbCB3\r\nb3Js\r\nZA==" },
+ { "hello to the world!!", 80, FALSE,
+ "aGVsbG8gdG8gdGhlIHdvcmxkISE=" },
+ { "hello to the world!!", 8, FALSE,
+ "aGVsbG8g\ndG8gdGhl\nIHdvcmxk\nISE=" },
+ { "hello to the world!!", 8, TRUE,
+ "aGVsbG8g\r\ndG8gdGhl\r\nIHdvcmxk\r\nISE=" },
+ { "\xd0\x93\xd0\xbe\xd0\xb2\xd0\xbe\xd1\x80\xd1\x8f\xcc"
+ "\x81\xd1\x82\x2c\x20\xd1\x87\xd1\x82\xd0\xbe\x20\xd0"
+ "\xba\xd1\x83\xd1\x80\x20\xd0\xb4\xd0\xbe\xd1\x8f\xcc"
+ "\x81\xd1\x82\x2e", 80, FALSE,
+ "0JPQvtCy0L7RgNGPzIHRgiwg0YfRgtC+INC60YPRgCDQtNC+0Y/MgdGCLg==" },
+};
+
+static const struct base64_istream_test base64url_tests[] = {
+ { "", 80, FALSE, "" },
+ { "1", 80, FALSE, "MQ==" },
+ { "12", 80, FALSE, "MTI=" },
+ { "123", 80, FALSE, "MTIz" },
+ { "1234", 80, FALSE, "MTIzNA==" },
+ { "12345", 80, FALSE, "MTIzNDU=" },
+ { "hello world", 80, FALSE, "aGVsbG8gd29ybGQ=" },
+ { "hello world", 4, FALSE, "aGVs\nbG8g\nd29y\nbGQ=" },
+ { "hello world", 4, TRUE, "aGVs\r\nbG8g\r\nd29y\r\nbGQ=" },
+ { "hello worlds", 80, FALSE, "aGVsbG8gd29ybGRz" },
+ { "hello worlds", 4, FALSE, "aGVs\nbG8g\nd29y\nbGRz" },
+ { "hello worlds", 4, TRUE, "aGVs\r\nbG8g\r\nd29y\r\nbGRz" },
+ { "hell world", 80, FALSE, "aGVsbCB3b3JsZA==" },
+ { "hell world", 4, FALSE, "aGVs\nbCB3\nb3Js\nZA==" },
+ { "hell world", 4, TRUE, "aGVs\r\nbCB3\r\nb3Js\r\nZA==" },
+ { "hello to the world!!", 80, FALSE,
+ "aGVsbG8gdG8gdGhlIHdvcmxkISE=" },
+ { "hello to the world!!", 8, FALSE,
+ "aGVsbG8g\ndG8gdGhl\nIHdvcmxk\nISE=" },
+ { "hello to the world!!", 8, TRUE,
+ "aGVsbG8g\r\ndG8gdGhl\r\nIHdvcmxk\r\nISE=" },
+ { "\xd0\x93\xd0\xbe\xd0\xb2\xd0\xbe\xd1\x80\xd1\x8f\xcc"
+ "\x81\xd1\x82\x2c\x20\xd1\x87\xd1\x82\xd0\xbe\x20\xd0"
+ "\xba\xd1\x83\xd1\x80\x20\xd0\xb4\xd0\xbe\xd1\x8f\xcc"
+ "\x81\xd1\x82\x2e", 80, FALSE,
+ "0JPQvtCy0L7RgNGPzIHRgiwg0YfRgtC-INC60YPRgCDQtNC-0Y_MgdGCLg==" },
+};
+
+static const char *hello = "hello world";
+
+static void encode_test(unsigned int text_len,
+ struct istream *input, struct istream *input_data,
+ const char *output)
+{
+ unsigned int i;
+ const unsigned char *data;
+ uoff_t stream_size;
+ size_t size;
+ ssize_t ret;
+
+ for (i = 1; i <= text_len; i++) {
+ test_istream_set_size(input_data, i);
+ while ((ret = i_stream_read(input)) > 0) ;
+ test_assert(ret == 0);
+ }
+ test_istream_set_allow_eof(input_data, TRUE);
+ while ((ret = i_stream_read(input)) > 0) ;
+ test_assert(ret == -1);
+
+ data = i_stream_get_data(input, &size);
+ test_assert(size == strlen(output) && memcmp(data, output, size) == 0);
+
+ ret = i_stream_get_size(input, TRUE, &stream_size);
+ test_assert(ret > 0);
+ test_assert(size == stream_size);
+}
+
+static void
+encode_base64_test(const char *text, unsigned int chars_per_line,
+ bool crlf, const char *output)
+{
+ unsigned int text_len = strlen(text);
+ struct istream *input, *input_data;
+
+ input_data = test_istream_create_data(text, text_len);
+ test_istream_set_allow_eof(input_data, FALSE);
+ input = i_stream_create_base64_encoder(input_data, chars_per_line,
+ crlf);
+
+ encode_test(text_len, input, input_data, output);
+
+ i_stream_unref(&input);
+ i_stream_unref(&input_data);
+}
+
+static void
+encode_base64url_test(const char *text, unsigned int chars_per_line,
+ bool crlf, const char *output)
+{
+ unsigned int text_len = strlen(text);
+ struct istream *input, *input_data;
+
+ input_data = test_istream_create_data(text, text_len);
+ test_istream_set_allow_eof(input_data, FALSE);
+ input = i_stream_create_base64url_encoder(input_data, chars_per_line,
+ crlf);
+
+ encode_test(text_len, input, input_data, output);
+
+ i_stream_unref(&input);
+ i_stream_unref(&input_data);
+}
+
+static void
+test_encoder_seek(struct istream *input, const char *textout)
+{
+ unsigned int offset, len = strlen(textout);
+ const unsigned char *data;
+ size_t size;
+ ssize_t ret;
+
+ while (i_stream_read(input) > 0) ;
+ i_stream_skip(input, i_stream_get_data_size(input));
+
+ for (offset = 0; offset < len; offset++) {
+ i_stream_seek(input, offset);
+ while ((ret = i_stream_read(input)) > 0) ;
+ test_assert(ret == -1);
+
+ data = i_stream_get_data(input, &size);
+ test_assert(size == len-offset);
+ test_assert(memcmp(data, textout+offset, size) == 0);
+ i_stream_skip(input, size);
+ }
+}
+
+static void
+test_istream_base64_encoder_seek(const char *textin, const char *textout)
+{
+ struct istream *input, *input_data;
+
+ input_data = i_stream_create_from_data(textin, strlen(textin));
+ input = i_stream_create_base64_encoder(input_data, 4, TRUE);
+
+ test_encoder_seek(input, textout);
+
+ i_stream_unref(&input);
+ i_stream_unref(&input_data);
+}
+
+static void
+test_istream_base64url_encoder_seek(const char *textin, const char *textout)
+{
+ struct istream *input, *input_data;
+
+ input_data = i_stream_create_from_data(textin, strlen(textin));
+ input = i_stream_create_base64url_encoder(input_data, 4, TRUE);
+
+ test_encoder_seek(input, textout);
+
+ i_stream_unref(&input);
+ i_stream_unref(&input_data);
+}
+
+void test_istream_base64_encoder(void)
+{
+ unsigned int i;
+
+ for (i = 0; i < N_ELEMENTS(base64_tests); i++) {
+ const struct base64_istream_test *test = &base64_tests[i];
+
+ test_begin(t_strdup_printf(
+ "istream base64 encoder %u", i+1));
+ encode_base64_test(test->input, test->chars_per_line,
+ test->crlf, test->output);
+ test_end();
+ }
+
+ for (i = 0; i < N_ELEMENTS(base64url_tests); i++) {
+ const struct base64_istream_test *test = &base64url_tests[i];
+
+ test_begin(t_strdup_printf(
+ "istream base64url encoder %u", i+1));
+ encode_base64url_test(test->input, test->chars_per_line,
+ test->crlf, test->output);
+ test_end();
+ }
+
+ test_begin("istream base64 encoder seek");
+ test_istream_base64_encoder_seek(
+ hello, "aGVs\r\nbG8g\r\nd29y\r\nbGQ=");
+ test_end();
+
+ test_begin("istream base64url encoder seek");
+ test_istream_base64url_encoder_seek(
+ hello, "aGVs\r\nbG8g\r\nd29y\r\nbGQ=");
+ test_end();
+}
diff --git a/src/lib/test-istream-chain.c b/src/lib/test-istream-chain.c
new file mode 100644
index 0000000..cb72c64
--- /dev/null
+++ b/src/lib/test-istream-chain.c
@@ -0,0 +1,199 @@
+/* Copyright (c) 2016-2018 Dovecot authors, see the included COPYING file */
+
+#include "test-lib.h"
+#include "istream-private.h"
+#include "istream-chain.h"
+
+static void test_istream_chain_basic(void)
+{
+ struct istream *input, *test_input, *test_input2;
+ struct istream_chain *chain;
+ const unsigned char *data;
+ size_t size;
+
+ test_begin("istream chain");
+
+ test_input = test_istream_create("stream1");
+ test_input2 = test_istream_create("STREAM2");
+
+ input = i_stream_create_chain(&chain, IO_BLOCK_SIZE);
+ /* no input */
+ test_assert(i_stream_read(input) == 0);
+ /* stream1 input */
+ i_stream_chain_append(chain, test_input);
+ test_assert(i_stream_read(input) == 7);
+ data = i_stream_get_data(input, &size);
+ test_assert(size == 7 && memcmp(data, "stream1", 7) == 0);
+ test_assert(i_stream_read(input) == 0);
+ data = i_stream_get_data(input, &size);
+ test_assert(size == 7 && memcmp(data, "stream1", 7) == 0);
+ /* STREAM2 input */
+ i_stream_chain_append(chain, test_input2);
+ test_assert(i_stream_read(input) == 7);
+ data = i_stream_get_data(input, &size);
+ test_assert(size == 14 && memcmp(data, "stream1STREAM2", 14) == 0);
+ test_assert(i_stream_read(input) == 0);
+ data = i_stream_get_data(input, &size);
+ test_assert(size == 14 && memcmp(data, "stream1STREAM2", 14) == 0);
+ /* EOF */
+ i_stream_chain_append_eof(chain);
+ test_assert(i_stream_read(input) == -1 &&
+ input->eof && input->stream_errno == 0);
+ data = i_stream_get_data(input, &size);
+ test_assert(size == 14 && memcmp(data, "stream1STREAM2", 14) == 0);
+
+ i_stream_unref(&input);
+
+ test_assert(test_input->eof && test_input->stream_errno == 0);
+ test_assert(test_input2->eof && test_input2->stream_errno == 0);
+
+ i_stream_unref(&test_input);
+ i_stream_unref(&test_input2);
+ test_end();
+}
+
+static void test_istream_chain_early_end(void)
+{
+ struct istream *input, *test_input;
+ struct istream_chain *chain;
+
+ test_begin("istream chain early end");
+
+ test_input = test_istream_create("string");
+ test_istream_set_size(test_input, 3);
+ test_istream_set_allow_eof(test_input, FALSE);
+
+ input = i_stream_create_chain(&chain, IO_BLOCK_SIZE);
+ i_stream_chain_append(chain, test_input);
+ test_assert(i_stream_read(input) == 3);
+ test_istream_set_size(test_input, 5);
+ test_assert(i_stream_read(input) == 2);
+ /* with current implementation we could skip less than 5 and have
+ v_offset<5, but I don't think that can work in all situations.
+ the normal case is anyway that we'll read everything up until some
+ point and skip over all the data up to there. */
+ i_stream_skip(input, 5);
+ i_stream_unref(&input);
+
+ test_assert(test_input->v_offset == 5);
+ i_stream_unref(&test_input);
+ test_end();
+}
+
+static void test_istream_chain_accumulate(void)
+{
+ struct istream *input, *tmp_istream;
+ struct istream *test_istreams[5];
+ struct istream_chain *chain;
+ const unsigned char *data;
+ size_t size;
+
+ test_begin("istream chain accumulate");
+
+ test_istreams[0] = test_istream_create("abcdefghijklmnopqrst");
+ test_istreams[1] = test_istream_create("ABCDEFGHIJKLMNOPQRSTUVWXY");
+ test_istreams[2] = test_istream_create("!\"#$%&'()*+,-./01234567890:;<=");
+ test_istreams[3] = test_istream_create("z1y2x3w4v5u6t7s8r9q0p.o,n");
+ test_istreams[4] = test_istream_create("aAbBcCdDeEfFgGhHiIjJ");
+
+ input = i_stream_create_chain(&chain, IO_BLOCK_SIZE);
+ /* no input */
+ test_assert(i_stream_read(input) == 0);
+
+ /* first stream */
+ i_stream_chain_append(chain, test_istreams[0]);
+ tmp_istream = test_istreams[0]; i_stream_unref(&tmp_istream);
+ test_assert(i_stream_read_data(input, &data, &size, 0) == 1);
+ test_assert(size == 20);
+ test_assert(memcmp(data, "abcdefghijklmnopqrst", 20) == 0);
+
+ /* partially skip */
+ i_stream_skip(input, 12);
+
+ /* second stream */
+ i_stream_chain_append(chain, test_istreams[1]);
+ tmp_istream = test_istreams[1]; i_stream_unref(&tmp_istream);
+ test_istream_set_size(test_istreams[1], 0);
+ test_assert(i_stream_read_data(input, &data, &size, 10) == 0);
+ test_assert(size == 8);
+ test_istream_set_size(test_istreams[1], 10);
+ test_assert(i_stream_read_data(input, &data, &size, 10) == 1);
+ test_assert(size == 18);
+ test_istream_set_allow_eof(test_istreams[1], FALSE);
+ test_assert(i_stream_read(input) == 0);
+ test_istream_set_size(test_istreams[1], 25);
+ test_istream_set_allow_eof(test_istreams[1], TRUE);
+ test_assert(i_stream_read_data(input, &data, &size, 30) == 1);
+ test_assert(size == 33);
+ test_assert(memcmp(data, "mnopqrst"
+ "ABCDEFGHIJKLMNOPQRSTUVWXY", 33) == 0);
+
+ /* partially skip */
+ i_stream_skip(input, 12);
+
+ /* third stream */
+ i_stream_chain_append(chain, test_istreams[2]);
+ tmp_istream = test_istreams[2]; i_stream_unref(&tmp_istream);
+ test_istream_set_size(test_istreams[2], 0);
+ test_assert(i_stream_read(input) == 0);
+ test_istream_set_size(test_istreams[2], 30);
+ test_assert(i_stream_read_data(input, &data, &size, 25) == 1);
+ test_assert(size == 51);
+ test_assert(memcmp(data, "EFGHIJKLMNOPQRSTUVWXY"
+ "!\"#$%&'()*+,-./01234567890:;<=", 51) == 0);
+ test_assert(i_stream_read(input) == 0);
+
+ /* partially skip */
+ i_stream_skip(input, 12);
+
+ /* forth stream */
+ i_stream_chain_append(chain, test_istreams[3]);
+ tmp_istream = test_istreams[3]; i_stream_unref(&tmp_istream);
+ test_assert(i_stream_read_data(input, &data, &size, 40) == 1);
+ test_assert(size == 64);
+ test_assert(memcmp(data, "QRSTUVWXY"
+ "!\"#$%&'()*+,-./01234567890:;<="
+ "z1y2x3w4v5u6t7s8r9q0p.o,n", 64) == 0);
+
+ /* partially skip */
+ i_stream_skip(input, 6);
+
+ /* fifth stream */
+ i_stream_chain_append(chain, test_istreams[4]);
+ tmp_istream = test_istreams[4]; i_stream_unref(&tmp_istream);
+ test_assert(i_stream_read_data(input, &data, &size, 60) == 1);
+ test_assert(size == 78);
+ test_assert(memcmp(data, "WXY"
+ "!\"#$%&'()*+,-./01234567890:;<="
+ "z1y2x3w4v5u6t7s8r9q0p.o,n"
+ "aAbBcCdDeEfFgGhHiIjJ", 78) == 0);
+
+ /* EOF */
+ i_stream_chain_append_eof(chain);
+ test_assert(i_stream_read(input) == -1);
+ test_assert(input->eof && input->stream_errno == 0);
+ test_assert(i_stream_read_data(input, &data, &size, 78) == -1);
+ test_assert(size == 78);
+ test_assert(memcmp(data, "WXY"
+ "!\"#$%&'()*+,-./01234567890:;<="
+ "z1y2x3w4v5u6t7s8r9q0p.o,n"
+ "aAbBcCdDeEfFgGhHiIjJ", 78) == 0);
+
+ /* skip rest */
+ i_stream_skip(input, 78);
+
+ test_assert(i_stream_read(input) == -1);
+ test_assert(input->eof && input->stream_errno == 0);
+ data = i_stream_get_data(input, &size);
+ test_assert(size == 0);
+
+ i_stream_unref(&input);
+ test_end();
+}
+
+void test_istream_chain(void)
+{
+ test_istream_chain_basic();
+ test_istream_chain_early_end();
+ test_istream_chain_accumulate();
+}
diff --git a/src/lib/test-istream-concat.c b/src/lib/test-istream-concat.c
new file mode 100644
index 0000000..5e80cfa
--- /dev/null
+++ b/src/lib/test-istream-concat.c
@@ -0,0 +1,254 @@
+/* Copyright (c) 2009-2018 Dovecot authors, see the included COPYING file */
+
+#include "test-lib.h"
+#include "istream-private.h"
+#include "istream-concat.h"
+
+#include <fcntl.h>
+#include <unistd.h>
+
+#define TEST_MAX_ISTREAM_COUNT 10
+#define TEST_MAX_ISTREAM_SIZE 1024
+#define TEST_MAX_BUFFER_SIZE 128
+
+static void test_istream_concat_one(unsigned int buffer_size)
+{
+ static const char *input_string = "xyz";
+#define STREAM_COUNT 5
+#define STREAM_BYTES 3
+ struct istream *streams[STREAM_COUNT+1];
+ struct istream *input;
+ const unsigned char *data;
+ size_t size;
+ unsigned int i, j;
+
+ for (i = 0; i < STREAM_COUNT; i++) {
+ streams[i] = test_istream_create(input_string);
+ test_istream_set_allow_eof(streams[i], TRUE);
+ test_istream_set_size(streams[i], 0);
+ }
+ streams[i] = NULL;
+
+ input = i_stream_create_concat(streams);
+ for (i = 0; i/STREAM_BYTES < STREAM_COUNT; i++) {
+ test_istream_set_size(streams[i/STREAM_BYTES], (i%STREAM_BYTES) + 1);
+ test_assert(i_stream_read(input) == 1);
+ if (i < buffer_size) {
+ data = i_stream_get_data(input, &size);
+ test_assert(size == i+1);
+ } else {
+ i_stream_skip(input, 1);
+ data = i_stream_get_data(input, &size);
+ test_assert(size == buffer_size);
+ }
+ for (j = 0; j < size; j++) {
+ test_assert((char)data[j] == input_string[(input->v_offset + j) % STREAM_BYTES]);
+ }
+ test_assert(i_stream_read(input) <= 0);
+ }
+ test_assert(i_stream_read(input) == -1);
+ i_stream_skip(input, i_stream_get_data_size(input));
+ i_stream_unref(&input);
+
+ for (i = 0; i < STREAM_COUNT; i++) {
+ test_assert(streams[i]->eof && streams[i]->stream_errno == 0);
+ i_stream_unref(&streams[i]);
+ }
+}
+
+static bool test_istream_concat_random(void)
+{
+ struct istream **streams, *concat, **limits = NULL;
+ const unsigned char *data;
+ unsigned char *w_data;
+ size_t size = 0;
+ unsigned int i, j, offset, stream_count, data_len, simult;
+
+ stream_count = i_rand_minmax(2, TEST_MAX_ISTREAM_COUNT + 2 - 1);
+ streams = t_new(struct istream *, stream_count + 1);
+ for (i = 0, offset = 0; i < stream_count; i++) {
+ data_len = i_rand_minmax(1, TEST_MAX_ISTREAM_SIZE);
+ w_data = t_malloc_no0(data_len);
+ for (j = 0; j < data_len; j++)
+ w_data[j] = (offset++) & 0xff;
+ streams[i] = test_istream_create_data(w_data, data_len);
+ test_istream_set_allow_eof(streams[i], TRUE);
+ }
+ streams[i] = NULL;
+ i_assert(offset > 0);
+
+ concat = i_stream_create_concat(streams);
+ i_stream_set_max_buffer_size(concat, TEST_MAX_BUFFER_SIZE);
+
+ simult = i_rand_limit(TEST_MAX_ISTREAM_COUNT);
+ if (simult > 0) {
+ limits = t_new(struct istream *, simult);
+ for (i = 0; i < simult; i++)
+ limits[i] = i_stream_create_limit(concat, UOFF_T_MAX);
+ }
+
+ for (i = 0; i < 1000; i++) {
+ struct istream *input = (simult == 0) ? concat : limits[i_rand_limit(simult)];
+ if (i_rand_limit(3) == 0) {
+ i_stream_seek(input, i_rand_limit(offset));
+ } else {
+ ssize_t ret = i_stream_read(input);
+ size = i_stream_get_data_size(input);
+ if (ret == -2) {
+ test_assert(size >= TEST_MAX_BUFFER_SIZE);
+ } else if (input->v_offset + size != offset) {
+ test_assert(ret > 0);
+ test_assert(input->v_offset + ret <= offset);
+ i_stream_skip(input, i_rand_limit(ret));
+
+ data = i_stream_get_data(input, &size);
+ for (j = 0; j < size; j++) {
+ test_assert(data[j] == (input->v_offset + j) % 256);
+ }
+ }
+ }
+ if (test_has_failed())
+ break;
+ }
+ for (i = 0; i < stream_count; i++)
+ i_stream_unref(&streams[i]);
+ for (i = 0; i < simult; i++)
+ i_stream_unref(&limits[i]);
+ i_stream_unref(&concat);
+ return !test_has_failed();
+}
+
+static void test_istream_concat_seek_end(void)
+{
+ test_begin("istream concat seek end");
+
+ struct istream *streams[] = {
+ test_istream_create("s1"),
+ test_istream_create("s2"),
+ NULL
+ };
+ struct istream *input = i_stream_create_concat(streams);
+ i_stream_unref(&streams[0]);
+ i_stream_unref(&streams[1]);
+
+ i_stream_seek(input, 4);
+ test_assert(i_stream_read(input) == -1);
+ i_stream_unref(&input);
+
+ test_end();
+}
+
+static void test_istream_concat_early_end(void)
+{
+ struct istream *input, *streams[2];
+
+ test_begin("istream concat early end");
+
+ streams[0] = test_istream_create("stream");
+ test_istream_set_size(streams[0], 3);
+ test_istream_set_allow_eof(streams[0], FALSE);
+ streams[1] = NULL;
+
+ input = i_stream_create_concat(streams);
+ test_assert(i_stream_read(input) == 3);
+ test_istream_set_size(streams[0], 5);
+ test_assert(i_stream_read(input) == 2);
+ i_stream_skip(input, 5);
+ i_stream_unref(&input);
+
+ test_assert(streams[0]->v_offset == 5);
+ i_stream_unref(&streams[0]);
+
+ test_end();
+}
+
+static void test_istream_concat_snapshot(void)
+{
+ struct istream *input;
+ const unsigned char *data;
+ size_t size;
+
+ test_begin("istream concat snapshot");
+
+ struct istream *test_istreams[] = {
+ test_istream_create("abcdefghijklmnopqrst"),
+ test_istream_create("ABCDEFGHIJKLMNOPQRSTUVWXY"),
+ test_istream_create("!\"#$%&'()*+,-./01234567890:;<="),
+ NULL
+ };
+
+ input = i_stream_create_concat(test_istreams);
+ for (unsigned int i = 0; test_istreams[i] != NULL; i++) {
+ struct istream *tmp_istream = test_istreams[i];
+ i_stream_unref(&tmp_istream);
+ }
+
+ test_istream_set_size(test_istreams[0], 20);
+ test_istream_set_size(test_istreams[1], 0);
+ test_istream_set_size(test_istreams[2], 0);
+
+ /* first stream */
+ test_istream_set_allow_eof(test_istreams[0], FALSE);
+ test_assert(i_stream_read_data(input, &data, &size, 0) == 1);
+ test_assert(size == 20);
+ test_assert(memcmp(data, "abcdefghijklmnopqrst", 20) == 0);
+
+ /* partially skip */
+ i_stream_skip(input, 12);
+
+ /* second stream */
+ test_assert(i_stream_read_data(input, &data, &size, 10) == 0);
+ test_assert(size == 8);
+ test_istream_set_allow_eof(test_istreams[0], TRUE);
+ test_istream_set_size(test_istreams[0], 0);
+ test_assert(i_stream_read_data(input, &data, &size, 10) == 0);
+ test_assert(size == 8);
+ test_istream_set_size(test_istreams[1], 10);
+ test_assert(i_stream_read_data(input, &data, &size, 10) == 1);
+ test_assert(size == 18);
+ test_istream_set_allow_eof(test_istreams[1], FALSE);
+ test_assert(i_stream_read(input) == 0);
+ test_istream_set_size(test_istreams[1], 25);
+ test_istream_set_allow_eof(test_istreams[1], TRUE);
+ test_assert(i_stream_read_data(input, &data, &size, 30) == 1);
+ test_assert(size == 33);
+ test_assert(memcmp(data, "mnopqrst"
+ "ABCDEFGHIJKLMNOPQRSTUVWXY", 33) == 0);
+
+ /* partially skip */
+ i_stream_skip(input, 12);
+
+ /* third stream */
+ test_istream_set_size(test_istreams[2], 0);
+ test_assert(i_stream_read(input) == 0);
+ test_istream_set_size(test_istreams[2], 30);
+ test_assert(i_stream_read_data(input, &data, &size, 25) == 1);
+ test_assert(size == 51);
+ test_assert(memcmp(data, "EFGHIJKLMNOPQRSTUVWXY"
+ "!\"#$%&'()*+,-./01234567890:;<=", 51) == 0);
+
+ i_stream_unref(&input);
+ test_end();
+}
+
+void test_istream_concat(void)
+{
+ unsigned int i;
+
+ test_begin("istream concat");
+ for (i = 1; i < STREAM_BYTES*STREAM_COUNT; i++) {
+ test_istream_concat_one(i);
+ }
+ test_end();
+
+ test_begin("istream concat random");
+ for (i = 0; i < 100; i++) T_BEGIN {
+ if(!test_istream_concat_random())
+ i = 101; /* don't break a T_BEGIN */
+ } T_END;
+ test_end();
+
+ test_istream_concat_seek_end();
+ test_istream_concat_early_end();
+ test_istream_concat_snapshot();
+}
diff --git a/src/lib/test-istream-crlf.c b/src/lib/test-istream-crlf.c
new file mode 100644
index 0000000..5239e36
--- /dev/null
+++ b/src/lib/test-istream-crlf.c
@@ -0,0 +1,115 @@
+/* Copyright (c) 2007-2018 Dovecot authors, see the included COPYING file */
+
+#include "test-lib.h"
+#include "str.h"
+#include "istream-private.h"
+#include "istream-crlf.h"
+
+static void test_istream_crlf_input(const char *input)
+{
+ string_t *output;
+ const unsigned char *data;
+ size_t size = 0;
+ ssize_t ret1, ret2;
+ unsigned int i, j, pos, input_len = strlen(input);
+ struct istream *istream, *crlf_istream;
+
+ output = t_str_new(256);
+
+ for (j = 0; j < 4; j++) {
+ istream = i_stream_create_from_data(input, input_len);
+ str_truncate(output, 0);
+ if (j%2 == 0) {
+ /* drop CRs */
+ crlf_istream = i_stream_create_lf(istream);
+ for (i = 0; i < input_len; i++) {
+ if (input[i] == '\r' &&
+ (i == input_len-1 || input[i+1] == '\n'))
+ ;
+ else
+ str_append_c(output, input[i]);
+ }
+ } else {
+ /* add missing CRs */
+ crlf_istream = i_stream_create_crlf(istream);
+ for (i = 0; i < input_len; i++) {
+ if (input[i] == '\n' &&
+ (i == 0 || input[i-1] != '\r'))
+ str_append_c(output, '\r');
+ str_append_c(output, input[i]);
+ }
+ }
+
+ pos = 0;
+ for (i = 1; i <= input_len; i++) {
+ if (j >= 2) {
+ i_stream_unref(&istream);
+ i_stream_unref(&crlf_istream);
+ istream = i_stream_create_from_data(input,
+ input_len);
+ crlf_istream = j%2 == 0 ?
+ i_stream_create_lf(istream) :
+ i_stream_create_crlf(istream);
+ pos = 0;
+ }
+ istream->real_stream->pos = i;
+ ret1 = i_stream_read(crlf_istream);
+ if (crlf_istream->real_stream->buffer_size != 0) {
+ /* this is pretty evil */
+ crlf_istream->real_stream->buffer_size =
+ I_MAX(crlf_istream->real_stream->pos, i);
+ }
+ ret2 = i_stream_read(crlf_istream);
+ data = i_stream_get_data(crlf_istream, &size);
+ if (ret1 > 0 || ret2 > 0) {
+ ret1 = I_MAX(ret1, 0) + I_MAX(ret2, 0);
+ test_assert(pos + (unsigned int)ret1 == size);
+ pos += ret1;
+ }
+ if (size > 0)
+ test_assert_idx(memcmp(data, str_data(output),
+ size) == 0, j*10000+i);
+ }
+ test_assert_idx(size == str_len(output), j*10000+i);
+ i_stream_unref(&crlf_istream);
+ i_stream_unref(&istream);
+ }
+}
+
+void test_istream_crlf(void)
+{
+ const char *input[] = {
+ "\rfoo",
+ "foo\nbar\r\nbaz\r\r\n",
+ "\r\nfoo",
+ "\r\r\n",
+ "\nfoo"
+ };
+ unsigned int i;
+
+ test_begin("istream crlf");
+ for (i = 0; i < N_ELEMENTS(input); i++)
+ test_istream_crlf_input(input[i]);
+ test_end();
+
+#define ISTREAM_CRLF_TEST_REPS 1000
+ test_begin("istream crlf(random)");
+ for (i = 0; i < ISTREAM_CRLF_TEST_REPS; i++) T_BEGIN {
+ char buf[100];
+ size_t len = 0;
+ while (len < sizeof(buf) - 1) {
+ switch(i_rand_limit(16)) {
+ case 0: goto outahere;
+ case 1: buf[len] = '\r'; break;
+ case 2: buf[len] = '\n'; break;
+ default: buf[len]= '.'; break;
+ }
+ len++;
+ }
+ outahere:
+ buf[len] = '\0';
+ if (len > 0)
+ test_istream_crlf_input(buf);
+ } T_END;
+ test_end();
+}
diff --git a/src/lib/test-istream-failure-at.c b/src/lib/test-istream-failure-at.c
new file mode 100644
index 0000000..d0313ba
--- /dev/null
+++ b/src/lib/test-istream-failure-at.c
@@ -0,0 +1,49 @@
+/* Copyright (c) 2015-2018 Dovecot authors, see the included COPYING file */
+
+#include "test-lib.h"
+#include "istream.h"
+#include "istream-failure-at.h"
+
+#define TEST_DATA_LENGTH 128
+#define TEST_ERRMSG "test-istream-failure-at error triggered"
+
+void test_istream_failure_at(void)
+{
+ struct istream *input, *data_input;
+ unsigned char test_data[TEST_DATA_LENGTH];
+ unsigned int i;
+ ssize_t ret;
+
+ test_begin("istream failure at");
+ for (i = 0; i < sizeof(test_data); i++)
+ test_data[i] = i;
+ data_input = i_stream_create_from_data(test_data, sizeof(test_data));
+ for (i = 0; i < TEST_DATA_LENGTH; i++) {
+ i_stream_seek(data_input, 0);
+ input = i_stream_create_failure_at(data_input, i, EIO, TEST_ERRMSG);
+ while ((ret = i_stream_read(input)) > 0)
+ i_stream_skip(input, ret);
+ test_assert_idx(ret == -1 && input->v_offset == i &&
+ input->stream_errno == EIO &&
+ strcmp(i_stream_get_error(input), TEST_ERRMSG) == 0, i);
+ i_stream_destroy(&input);
+ }
+ /* shouldn't fail */
+ i_stream_seek(data_input, 0);
+ input = i_stream_create_failure_at(data_input, TEST_DATA_LENGTH, EIO, TEST_ERRMSG);
+ while ((ret = i_stream_read(input)) > 0)
+ i_stream_skip(input, ret);
+ test_assert(ret == -1 && input->stream_errno == 0);
+ i_stream_destroy(&input);
+ /* fail at EOF */
+ i_stream_seek(data_input, 0);
+ input = i_stream_create_failure_at_eof(data_input, EIO, TEST_ERRMSG);
+ while ((ret = i_stream_read(input)) > 0)
+ i_stream_skip(input, ret);
+ test_assert_idx(ret == -1 && input->v_offset == TEST_DATA_LENGTH &&
+ input->stream_errno == EIO &&
+ strcmp(i_stream_get_error(input), TEST_ERRMSG) == 0, i);
+ i_stream_destroy(&input);
+ i_stream_destroy(&data_input);
+ test_end();
+}
diff --git a/src/lib/test-istream-jsonstr.c b/src/lib/test-istream-jsonstr.c
new file mode 100644
index 0000000..626994d
--- /dev/null
+++ b/src/lib/test-istream-jsonstr.c
@@ -0,0 +1,139 @@
+/* Copyright (c) 2016-2018 Dovecot authors, see the included COPYING file */
+
+#include "test-lib.h"
+#include "str.h"
+#include "istream-private.h"
+#include "istream-jsonstr.h"
+
+static const struct {
+ const char *input;
+ const char *output;
+ int stream_errno;
+} tests[] = {
+ { "foo\\\\\\\"\\b\\f\\n\\r\\t\\u0001\\uffff\"",
+ "foo\\\"\b\f\n\r\t\001\xEF\xBF\xBF", 0 },
+ { "\\ud801\\udc37\"", "\xf0\x90\x90\xb7", 0 }, /* valid codepoint */
+ { "\"", "", 0 },
+ { "foo\\?\"", "foo", EINVAL },
+ { "foo\\?\"", "foo", EINVAL },
+ { "", "", EPIPE },
+ { "\\\"", "\"", EPIPE },
+ { "foo", "foo", EPIPE },
+ { "\\ud801", "", EPIPE }, /* high surrogate alone */
+ { "\\udced\\udc37\"", "", EINVAL }, /* low surrogate before high */
+ { "\\ud8011\\udc37\"", "", EINVAL }, /* has extra 1 in middle */
+ { "hello \\udc37\"", "hello ", EINVAL }, /* low surrogate before high with valid prefix*/
+ { "hello \\ud801", "hello ", EPIPE }, /* high surrogate alone with valid prefix */
+ { "\\uabcg", "", EINVAL }, /* invalid hex value */
+};
+
+static void
+run_test_buffer(const char *json_input, const char *output, int stream_errno,
+ unsigned int skip_count)
+{
+ size_t json_input_len = strlen(json_input);
+ struct istream *input_data, *input;
+ const unsigned char *data;
+ size_t i, size;
+ ssize_t ret = 0;
+
+ input_data = test_istream_create_data(json_input, json_input_len);
+ test_istream_set_allow_eof(input_data, FALSE);
+ input = i_stream_create_jsonstr(input_data);
+
+ for (i = 1; i < json_input_len;) {
+ test_istream_set_size(input_data, i);
+ while ((ret = i_stream_read(input)) > 0) ;
+ if (ret == -1 && stream_errno != 0)
+ break;
+ test_assert_idx(ret == 0, i);
+ if (i + skip_count < json_input_len)
+ i += skip_count;
+ else
+ i++;
+ }
+ test_istream_set_allow_eof(input_data, TRUE);
+ test_istream_set_size(input_data, json_input_len);
+ ret = i_stream_read(input);
+ while (ret > 0 && stream_errno != 0)
+ ret = i_stream_read(input);
+ test_assert(ret == -1);
+ test_assert(input->stream_errno == stream_errno);
+
+ if (stream_errno == 0) {
+ data = i_stream_get_data(input, &size);
+ test_assert(size == strlen(output));
+ if (size > 0)
+ test_assert(memcmp(data, output, size) == 0);
+ }
+ i_stream_unref(&input);
+ i_stream_unref(&input_data);
+}
+
+static void
+run_test(const char *json_input, const char *output, int stream_errno)
+{
+ for (unsigned int i = 1; i <= 5; i++)
+ run_test_buffer(json_input, output, stream_errno, i);
+}
+
+static void test_istream_jsonstr_autoretry(void)
+{
+ const char *json_input = "\\u0001\"";
+ const size_t json_input_len = strlen(json_input);
+ struct istream *input_data, *input;
+
+ test_begin("istream-jsonstr autoretry");
+ input_data = test_istream_create_data(json_input, json_input_len);
+ input = i_stream_create_jsonstr(input_data);
+
+ test_istream_set_size(input_data, 2);
+ test_assert(i_stream_read(input_data) == 2);
+ test_istream_set_size(input_data, json_input_len);
+ test_assert(i_stream_read(input) == 1);
+ test_assert(i_stream_read(input) == -1);
+
+ i_stream_unref(&input);
+ i_stream_unref(&input_data);
+ test_end();
+}
+
+static void test_istream_jsonstr_partial(void)
+{
+ size_t len = 0;
+ const char *json_input = "hello\\u0060x\"";
+ const char *output = "hello`x";
+ const size_t json_input_len = strlen(json_input);
+ struct istream *input_data, *input;
+
+ test_begin("istream-jsonstr partial");
+
+ input_data = test_istream_create_data(json_input, json_input_len);
+ input = i_stream_create_jsonstr(input_data);
+ test_istream_set_size(input_data, 9);
+ test_assert(i_stream_read(input) == 5);
+ test_istream_set_size(input_data, json_input_len);
+ test_assert(i_stream_read(input) == 2);
+ test_assert(i_stream_read(input) == -1);
+
+ test_assert(memcmp(i_stream_get_data(input, &len), output, I_MIN(len, strlen(output))) == 0 &&
+ len == strlen(output));
+
+ i_stream_unref(&input);
+ i_stream_unref(&input_data);
+
+ test_end();
+}
+
+void test_istream_jsonstr(void)
+{
+ unsigned int i;
+
+ for (i = 0; i < N_ELEMENTS(tests); i++) {
+ test_begin(t_strdup_printf("istream-jsonstr %u", i+1));
+ run_test(tests[i].input, tests[i].output, tests[i].stream_errno);
+ test_end();
+ }
+ test_istream_jsonstr_autoretry();
+ test_istream_jsonstr_partial();
+}
diff --git a/src/lib/test-istream-multiplex.c b/src/lib/test-istream-multiplex.c
new file mode 100644
index 0000000..185c271
--- /dev/null
+++ b/src/lib/test-istream-multiplex.c
@@ -0,0 +1,372 @@
+/* Copyright (c) 2016-2018 Dovecot authors, see the included COPYING file */
+
+#include "test-lib.h"
+#include "ioloop.h"
+#include "str.h"
+#include "crc32.h"
+#include "randgen.h"
+#include "istream-private.h"
+#include "istream-multiplex.h"
+#include "ostream.h"
+#include <unistd.h>
+
+static void test_istream_multiplex_simple(void)
+{
+ test_begin("istream multiplex (simple)");
+
+ static const char data[] = "\x00\x00\x00\x00\x06Hello\x00"
+ "\x01\x00\x00\x00\x03Wor"
+ "\x00\x00\x00\x00\x00"
+ "\x01\x00\x00\x00\x03ld\x00";
+ static const size_t data_len = sizeof(data)-1;
+ struct istream *input = test_istream_create_data(data, data_len);
+ size_t siz;
+
+ struct istream *chan0 = i_stream_create_multiplex(input, SIZE_MAX);
+ struct istream *chan1 = i_stream_multiplex_add_channel(chan0, 1);
+
+ /* nothing to read until the first byte */
+ for (size_t i = 0; i <= 1+4; i++) {
+ test_istream_set_size(input, i);
+ test_assert(i_stream_read(chan0) == 0);
+ test_assert(i_stream_read(chan1) == 0);
+ }
+
+ /* partial read of the first packet */
+ size_t input_max = 1+4+3;
+ test_istream_set_size(input, input_max);
+ test_assert(i_stream_read(chan0) == 3);
+ test_assert(memcmp(i_stream_get_data(chan0, &siz), "Hel", 3) == 0 &&
+ siz == 3);
+ test_assert(i_stream_read(chan1) == 0);
+
+ /* read the rest of the first packet and the second packet.
+ read chan1 before chan0 to see that it works. */
+ input_max += 3 + 1+4+3;
+ test_istream_set_size(input, input_max);
+ test_assert(i_stream_read(chan1) == 3);
+ test_assert(i_stream_read(chan0) == 3);
+ test_assert(memcmp(i_stream_get_data(chan0, &siz), "Hello\0", 6) == 0 &&
+ siz == 6);
+ test_assert(memcmp(i_stream_get_data(chan1, &siz), "Wor", 3) == 0 &&
+ siz == 3);
+
+ /* 0-sized packet is ignored */
+ input_max += 1+4;
+ test_istream_set_size(input, input_max);
+ test_assert(i_stream_read(chan0) == 0);
+ test_assert(i_stream_read(chan1) == 0);
+
+ /* read the final packet */
+ input_max += 1+4+3;
+ i_assert(input_max == data_len);
+ test_istream_set_size(input, input_max);
+ test_assert(i_stream_read(chan0) == 0);
+ test_assert(i_stream_read(chan1) == 3);
+
+ /* we should have the final data in all channels now */
+ test_assert(memcmp(i_stream_get_data(chan0, &siz), "Hello\0", 6) == 0 &&
+ siz == 6);
+ test_assert(memcmp(i_stream_get_data(chan1, &siz), "World\0", 6) == 0 &&
+ siz == 6);
+
+ /* all channels should return EOF */
+ test_assert(i_stream_read(chan0) == -1 && chan0->stream_errno == 0);
+ i_stream_unref(&chan0);
+
+ test_assert(i_stream_read(chan1) == -1 && chan1->stream_errno == 0);
+ i_stream_unref(&chan1);
+
+ i_stream_unref(&input);
+
+ test_end();
+}
+
+static void test_istream_multiplex_maxbuf(void)
+{
+ test_begin("istream multiplex (maxbuf)");
+
+ static const char data[] = "\x00\x00\x00\x00\x06Hello\x00"
+ "\x01\x00\x00\x00\x06World\x00";
+ static const size_t data_len = sizeof(data)-1;
+ struct istream *input = test_istream_create_data(data, data_len);
+ size_t siz;
+
+ struct istream *chan0 = i_stream_create_multiplex(input, 5);
+ struct istream *chan1 = i_stream_multiplex_add_channel(chan0, 1);
+
+ /* we get data for channel 0 and congest */
+ test_assert(i_stream_read(chan1) == 0);
+ /* we read data for channel 0 */
+ test_assert(i_stream_read(chan0) == 5);
+ /* and now it's congested */
+ test_assert(i_stream_read(chan0) == -2);
+ test_assert(memcmp(i_stream_get_data(chan0, &siz), "Hello", 5) == 0 &&
+ siz == 5);
+ /* consume data */
+ i_stream_skip(chan0, 5);
+ /* we read data for channel 1 */
+ test_assert(i_stream_read(chan1) == 5);
+ test_assert(memcmp(i_stream_get_data(chan1, &siz), "World", 5) == 0 &&
+ siz == 5);
+ /* consume data */
+ i_stream_skip(chan1, 5);
+ /* read last byte */
+ test_assert(i_stream_read(chan0) == 1);
+ /* now we get byte for channel 1 */
+ test_assert(i_stream_read(chan0) == 0);
+ /* now we read byte for channel 1 */
+ test_assert(i_stream_read(chan1) == 1);
+ /* and everything should return EOF now */
+ test_assert(i_stream_read(chan1) == -1);
+ test_assert(i_stream_read(chan0) == -1);
+
+ i_stream_unref(&chan0);
+ i_stream_unref(&chan1);
+
+ i_stream_unref(&input);
+
+ test_end();
+}
+
+static void test_istream_multiplex_random(void)
+{
+ const unsigned int max_channel = 6;
+ const unsigned int packets_count = 30;
+
+ test_begin("istream multiplex (random)");
+
+ unsigned int i;
+ uoff_t bytes_written = 0, bytes_read = 0;
+ buffer_t *buf = buffer_create_dynamic(default_pool, 10240);
+ uint32_t input_crc[max_channel];
+ uint32_t output_crc[max_channel];
+ memset(input_crc, 0, sizeof(input_crc));
+ memset(output_crc, 0, sizeof(output_crc));
+
+ for (i = 0; i < packets_count; i++) {
+ unsigned int len = i_rand_limit(1024+1);
+ unsigned char packet_data[len];
+ uint32_t len_be = cpu32_to_be(len);
+ unsigned int channel = i_rand_limit(max_channel);
+
+ random_fill(packet_data, len);
+ input_crc[channel] =
+ crc32_data_more(input_crc[channel], packet_data, len);
+
+ buffer_append_c(buf, channel);
+ buffer_append(buf, &len_be, sizeof(len_be));
+ buffer_append(buf, packet_data, len);
+ bytes_written += len;
+ }
+
+ struct istream *input = test_istream_create_data(buf->data, buf->used);
+ struct istream *chan[max_channel];
+ chan[0] = i_stream_create_multiplex(input, 1024/4);
+ for (i = 1; i < max_channel; i++)
+ chan[i] = i_stream_multiplex_add_channel(chan[0], i);
+
+ test_istream_set_size(input, 0);
+
+ /* read from each stream, 1 byte at a time */
+ size_t input_size = 0;
+ int max_ret = -3;
+ unsigned int read_max_channel = max_channel/2;
+ bool something_read = FALSE;
+ for (i = 0;;) {
+ ssize_t ret = i_stream_read(chan[i]);
+ if (max_ret < ret)
+ max_ret = ret;
+ if (ret > 0) {
+ size_t size;
+ const unsigned char *data =
+ i_stream_get_data(chan[i], &size);
+
+ output_crc[i] = crc32_data_more(output_crc[i], data, size);
+ bytes_read += size;
+
+ test_assert((size_t)ret == size);
+ i_stream_skip(chan[i], size);
+ something_read = TRUE;
+ }
+ if (++i < read_max_channel)
+ ;
+ else if (max_ret == 0 && !something_read &&
+ read_max_channel < max_channel) {
+ read_max_channel++;
+ } else {
+ if (max_ret <= -1) {
+ test_assert(max_ret == -1);
+ break;
+ }
+ if (max_ret == 0)
+ test_istream_set_size(input, ++input_size);
+ i = 0;
+ max_ret = -3;
+ something_read = FALSE;
+ read_max_channel = max_channel/2;
+ }
+ }
+ test_assert(bytes_read == bytes_written);
+ for (i = 0; i < max_channel; i++) {
+ test_assert_idx(input_crc[i] == output_crc[i], i);
+ test_assert_idx(i_stream_read(chan[i]) == -1 &&
+ chan[i]->stream_errno == 0, i);
+ i_stream_unref(&chan[i]);
+ }
+ i_stream_unref(&input);
+ buffer_free(&buf);
+ test_end();
+}
+
+static unsigned int channel_counter[2] = {0, 0};
+
+static const char *msgs[] = {
+ "",
+ "a",
+ "bb",
+ "ccc",
+ "dddd",
+ "eeeee",
+ "ffffff"
+};
+
+static void test_istream_multiplex_stream_read(struct istream *channel)
+{
+ uint8_t cid = i_stream_multiplex_get_channel_id(channel);
+ const char *line;
+ size_t siz;
+
+ if (i_stream_read(channel) < 0)
+ return;
+
+ while((line = i_stream_next_line(channel)) != NULL) {
+ siz = strlen(line);
+ test_assert_idx(siz > 0 && siz < N_ELEMENTS(msgs),
+ channel_counter[cid]);
+ if (siz > 0 && siz < N_ELEMENTS(msgs)) {
+ test_assert_idx(strcmp(line, msgs[siz]) == 0,
+ channel_counter[cid]);
+ }
+ channel_counter[cid]++;
+ }
+
+ if (channel_counter[0] > 100 && channel_counter[1] > 100)
+ io_loop_stop(current_ioloop);
+}
+
+static void test_send_msg(struct ostream *os, uint8_t cid, const char *msg)
+{
+ uint32_t len = cpu32_to_be(strlen(msg) + 1);
+ const struct const_iovec iov[] = {
+ { &cid, sizeof(cid) },
+ { &len, sizeof(len) },
+ { msg, strlen(msg) },
+ { "\n", 1 } /* newline added for i_stream_next_line */
+ };
+ o_stream_nsendv(os, iov, N_ELEMENTS(iov));
+}
+
+static void test_istream_multiplex_stream_write(struct ostream *channel)
+{
+ size_t rounds = i_rand_limit(10);
+ for(size_t i = 0; i < rounds; i++) {
+ uint8_t cid = i_rand_limit(2);
+ test_send_msg(channel, cid,
+ msgs[1 + i_rand_limit(N_ELEMENTS(msgs) - 1)]);
+ }
+}
+
+static void test_istream_multiplex_stream(void)
+{
+ test_begin("istream multiplex (stream)");
+ struct ioloop *ioloop = io_loop_create();
+ io_loop_set_current(ioloop);
+
+ int fds[2];
+ test_assert(pipe(fds) == 0);
+ fd_set_nonblock(fds[0], TRUE);
+ fd_set_nonblock(fds[1], TRUE);
+ struct ostream *os = o_stream_create_fd(fds[1], SIZE_MAX);
+ struct istream *is = i_stream_create_fd(fds[0], 10 + i_rand_limit(10));
+
+ struct istream *chan0 = i_stream_create_multiplex(is, SIZE_MAX);
+ struct istream *chan1 = i_stream_multiplex_add_channel(chan0, 1);
+
+ struct io *io0 =
+ io_add_istream(chan0, test_istream_multiplex_stream_read, chan0);
+ struct io *io1 =
+ io_add_istream(chan1, test_istream_multiplex_stream_read, chan1);
+ struct io *io2 =
+ io_add(fds[1], IO_WRITE, test_istream_multiplex_stream_write, os);
+
+ io_loop_run(current_ioloop);
+
+ io_remove(&io0);
+ io_remove(&io1);
+ io_remove(&io2);
+
+ i_stream_unref(&chan1);
+ i_stream_unref(&chan0);
+ i_stream_unref(&is);
+
+ test_assert(o_stream_finish(os) > 0);
+ o_stream_unref(&os);
+
+ io_loop_destroy(&ioloop);
+
+ i_close_fd(&fds[0]);
+ i_close_fd(&fds[1]);
+
+ test_end();
+}
+
+static void test_istream_multiplex_close_channel(void)
+{
+ test_begin("istream multiplex (close channel)");
+ static const char *data = "\x00\x00\x00\x00\x06Hello\x00"
+ "\x01\x00\x00\x00\x06World\x00";
+ static const size_t data_len = 22;
+ struct istream *input = test_istream_create_data(data, data_len);
+ size_t siz;
+
+ struct istream *chan0 = i_stream_create_multiplex(input, SIZE_MAX);
+ struct istream *chan1 = i_stream_multiplex_add_channel(chan0, 1);
+
+ i_stream_unref(&chan1);
+
+ test_assert(i_stream_read(chan0) == 6);
+
+ test_assert(memcmp(i_stream_get_data(chan0, &siz), "Hello\0", 6) == 0 &&
+ siz == 6);
+
+ i_stream_unref(&chan0);
+ i_stream_unref(&input);
+
+ input = test_istream_create_data(data, data_len);
+ chan0 = i_stream_create_multiplex(input, SIZE_MAX);
+ chan1 = i_stream_multiplex_add_channel(chan0, 1);
+
+ /* this is needed to populate chan1 data */
+ (void)i_stream_read(chan0);
+ i_stream_unref(&chan0);
+
+ test_assert(i_stream_read(chan1) == 6);
+
+ test_assert(memcmp(i_stream_get_data(chan1, &siz), "World\0", 6) == 0 &&
+ siz == 6);
+
+ i_stream_unref(&chan1);
+ i_stream_unref(&input);
+
+ test_end();
+}
+
+void test_istream_multiplex(void)
+{
+ test_istream_multiplex_simple();
+ test_istream_multiplex_maxbuf();
+ test_istream_multiplex_random();
+ test_istream_multiplex_stream();
+ test_istream_multiplex_close_channel();
+}
diff --git a/src/lib/test-istream-seekable.c b/src/lib/test-istream-seekable.c
new file mode 100644
index 0000000..3373f52
--- /dev/null
+++ b/src/lib/test-istream-seekable.c
@@ -0,0 +1,290 @@
+/* Copyright (c) 2009-2018 Dovecot authors, see the included COPYING file */
+
+#include "test-lib.h"
+#include "sha2.h"
+#include "istream-private.h"
+#include "istream-sized.h"
+#include "istream-hash.h"
+#include "istream-seekable.h"
+
+#include <fcntl.h>
+#include <unistd.h>
+
+static int fd_callback_fd = -1;
+
+static int fd_callback(const char **path_r, void *context ATTR_UNUSED)
+{
+ int fd;
+
+ *path_r = "test-lib.tmp";
+ fd = open(*path_r, O_RDWR | O_CREAT | O_TRUNC, 0600);
+ if (fd == -1)
+ i_error("creat(%s) failed: %m", *path_r);
+ else
+ i_unlink(*path_r);
+ fd_callback_fd = fd;
+ return fd;
+}
+
+static void test_istream_seekable_one(unsigned int buffer_size)
+{
+ static const char *input_string = "xyz";
+#define STREAM_COUNT 5
+#define STREAM_BYTES 3
+ struct istream *streams[STREAM_COUNT+1];
+ struct istream *input;
+ const unsigned char *data;
+ size_t size;
+ unsigned int i, j;
+
+ for (i = 0; i < STREAM_COUNT; i++) {
+ streams[i] = test_istream_create(input_string);
+ streams[i]->seekable = FALSE;
+ test_istream_set_allow_eof(streams[i], TRUE);
+ test_istream_set_size(streams[i], 0);
+ }
+ streams[i] = NULL;
+
+ input = i_stream_create_seekable(streams, buffer_size, fd_callback, NULL);
+ test_assert(!input->blocking);
+ for (i = 0; i/STREAM_BYTES < STREAM_COUNT; i++) {
+ test_istream_set_size(streams[i/STREAM_BYTES], (i%STREAM_BYTES) + 1);
+ if (i < buffer_size) {
+ test_assert(i_stream_read(input) == 1);
+ data = i_stream_get_data(input, &size);
+ test_assert(size == i+1);
+ } else {
+ test_assert(i_stream_read(input) == -2);
+ i_stream_skip(input, 1);
+ test_assert(i_stream_read(input) == 1);
+ data = i_stream_get_data(input, &size);
+ test_assert(size == buffer_size);
+ }
+ for (j = 0; j < size; j++) {
+ test_assert((char)data[j] == input_string[(input->v_offset + j) % STREAM_BYTES]);
+ }
+ }
+ test_assert(!input->blocking);
+ test_assert(i_stream_read(input) == -1);
+ test_assert(input->blocking);
+ for (i = 0; i < STREAM_COUNT; i++) {
+ test_assert(streams[i]->eof && streams[i]->stream_errno == 0);
+ i_stream_unref(&streams[i]);
+ }
+ i_stream_unref(&input);
+}
+
+static void test_istream_seekable_random(void)
+{
+ struct istream **streams, *input;
+ const unsigned char *data;
+ unsigned char *w_data;
+ size_t size;
+ unsigned int i, j, offset, stream_count, data_len, buffer_size;
+
+ stream_count = i_rand_minmax(2, 10 + 2 - 1);
+ streams = t_new(struct istream *, stream_count + 1);
+ for (i = 0, offset = 0; i < stream_count; i++) {
+ data_len = i_rand_minmax(1, 100);
+ w_data = t_malloc_no0(data_len);
+ for (j = 0; j < data_len; j++)
+ w_data[j] = (offset++) & 0xff;
+ streams[i] = test_istream_create_data(w_data, data_len);
+ streams[i]->seekable = FALSE;
+ test_istream_set_allow_eof(streams[i], TRUE);
+ }
+ streams[i] = NULL;
+ i_assert(offset > 0);
+
+ buffer_size = i_rand_minmax(1, 100); size = 0;
+ input = i_stream_create_seekable(streams, buffer_size, fd_callback, NULL);
+ test_assert(!input->blocking);
+
+ /* first read it through */
+ while (i_stream_read(input) > 0) {
+ size = i_stream_get_data_size(input);
+ i_stream_skip(input, size);
+ }
+ test_assert(input->blocking);
+
+ i_stream_seek(input, 0);
+ for (i = 0; i < 100; i++) {
+ if (i_rand_limit(3) == 0) {
+ i_stream_seek(input, i_rand_limit(offset));
+ } else {
+ ssize_t ret = i_stream_read(input);
+ if (input->v_offset + size == offset)
+ test_assert(ret < 0);
+ else if (ret == -2) {
+ test_assert(size == buffer_size);
+ } else {
+ test_assert(ret > 0);
+ test_assert(input->v_offset + ret <= offset);
+ i_stream_skip(input, i_rand_limit(ret + 1));
+
+ data = i_stream_get_data(input, &size);
+ for (j = 0; j < size; j++) {
+ test_assert(data[j] == (input->v_offset + j) % 256);
+ }
+ }
+ }
+ size = i_stream_get_data_size(input);
+ }
+ for (i = 0; i < stream_count; i++) {
+ test_assert(streams[i]->eof && streams[i]->stream_errno == 0);
+ i_stream_unref(&streams[i]);
+ }
+ i_stream_unref(&input);
+}
+
+static void test_istream_seekable_eof(void)
+{
+ static const char *in_str = "foo";
+ unsigned int in_str_len = strlen(in_str);
+ struct istream *streams[2], *input;
+ const unsigned char *data;
+ size_t size;
+
+ test_begin("istream seekable eof");
+
+ streams[0] = i_stream_create_from_data(in_str, in_str_len);
+ streams[0]->seekable = FALSE;
+ streams[1] = NULL;
+
+ input = i_stream_create_seekable(streams, in_str_len, fd_callback, NULL);
+
+ test_assert(i_stream_read(input) == (ssize_t)in_str_len);
+ data = i_stream_get_data(input, &size);
+ test_assert(size == in_str_len);
+ test_assert(memcmp(data, in_str, in_str_len) == 0);
+
+ test_assert(i_stream_read(input) == -1);
+ data = i_stream_get_data(input, &size);
+ test_assert(size == in_str_len);
+ test_assert(memcmp(data, in_str, in_str_len) == 0);
+ i_stream_seek(input, size);
+
+ i_stream_unref(&input);
+
+ test_assert(streams[0]->v_offset == in_str_len);
+ test_assert(streams[0]->eof);
+ i_stream_unref(&streams[0]);
+ test_end();
+}
+
+static void test_istream_seekable_early_end(void)
+{
+ struct istream *input, *streams[2];
+
+ test_begin("istream seekable early end");
+
+ streams[0] = test_istream_create("stream");
+ test_istream_set_size(streams[0], 3);
+ test_istream_set_allow_eof(streams[0], FALSE);
+ streams[0]->seekable = FALSE;
+ streams[1] = NULL;
+
+ input = i_stream_create_seekable(streams, 1000, fd_callback, NULL);
+ test_assert(i_stream_read(input) == 3);
+ test_istream_set_size(streams[0], 5);
+ test_assert(i_stream_read(input) == 2);
+ i_stream_skip(input, 5);
+ i_stream_unref(&input);
+
+ test_assert(streams[0]->v_offset == 5);
+ i_stream_unref(&streams[0]);
+
+ test_end();
+}
+
+static void test_istream_seekable_invalid_read(void)
+{
+ test_begin("istream seekable + other streams causing invalid read");
+ struct sha256_ctx hash_ctx;
+ sha256_init(&hash_ctx);
+ struct istream *str_input = test_istream_create("123456");
+ str_input->seekable = FALSE;
+ struct istream *seek_inputs[] = { str_input, NULL };
+ struct istream *seek_input = i_stream_create_seekable(seek_inputs, 3, fd_callback, NULL);
+ struct istream *sized_input = i_stream_create_sized(seek_input, 3);
+ struct istream *input = i_stream_create_hash(sized_input, &hash_method_sha256, &hash_ctx);
+ test_assert(i_stream_read(input) == 3);
+ test_assert(i_stream_read(input) == -2);
+ i_stream_skip(input, 3);
+ test_assert(i_stream_read(input) == -1);
+ i_stream_unref(&input);
+ i_stream_unref(&sized_input);
+ i_stream_unref(&seek_input);
+ i_stream_unref(&str_input);
+ test_end();
+}
+
+static void test_istream_seekable_get_size(void)
+{
+ test_begin("istream seekable get size");
+ struct istream *str_input = test_istream_create("123456");
+ str_input->seekable = FALSE;
+ struct istream *seek_inputs[] = { str_input, NULL };
+ struct istream *input = i_stream_create_seekable(seek_inputs, 32, fd_callback, NULL);
+ uoff_t size;
+ test_assert(i_stream_read(input) == 6);
+ test_assert(i_stream_read(input) == -1);
+ test_assert(i_stream_get_size(input, TRUE, &size) == 1 &&
+ size == 6);
+ i_stream_unref(&input);
+ i_stream_unref(&str_input);
+ test_end();
+}
+
+static void test_istream_seekable_failed_writes(void)
+{
+ struct istream *input, *streams[2];
+
+ test_begin("istream seekable failed write");
+ streams[0] = test_istream_create("stream");
+ test_istream_set_size(streams[0], 3);
+ test_istream_set_allow_eof(streams[0], FALSE);
+ streams[0]->seekable = FALSE;
+ streams[1] = NULL;
+
+ input = i_stream_create_seekable(streams, 2, fd_callback, NULL);
+ i_stream_set_name(input, "test seekable");
+ test_assert(i_stream_read(input) == 2);
+ i_stream_skip(input, 2);
+ test_assert(i_stream_read(input) == 1);
+ i_close_fd(&fd_callback_fd);
+ test_istream_set_size(streams[0], 5);
+
+ test_expect_error_string("istream-seekable: write_full(test-lib.tmp) failed: Bad file descriptor");
+ test_assert(i_stream_read(input) == -1);
+ test_expect_no_more_errors();
+
+ test_expect_error_string("file_istream.close((seekable temp-istream for: test seekable)) failed: Bad file descriptor");
+ i_stream_unref(&input);
+ test_expect_no_more_errors();
+
+ i_stream_unref(&streams[0]);
+ test_end();
+}
+
+void test_istream_seekable(void)
+{
+ unsigned int i;
+
+ test_begin("istream seekable");
+ for (i = 1; i <= STREAM_BYTES*STREAM_COUNT; i++)
+ test_istream_seekable_one(i);
+ test_end();
+
+ test_begin("istream seekable random");
+ for (i = 0; i < 100; i++) T_BEGIN {
+ test_istream_seekable_random();
+ } T_END;
+ test_end();
+
+ test_istream_seekable_eof();
+ test_istream_seekable_early_end();
+ test_istream_seekable_invalid_read();
+ test_istream_seekable_get_size();
+ test_istream_seekable_failed_writes();
+}
diff --git a/src/lib/test-istream-sized.c b/src/lib/test-istream-sized.c
new file mode 100644
index 0000000..82fa663
--- /dev/null
+++ b/src/lib/test-istream-sized.c
@@ -0,0 +1,111 @@
+/* Copyright (c) 2016-2018 Dovecot authors, see the included COPYING file */
+
+#include "test-lib.h"
+#include "istream.h"
+#include "istream-sized.h"
+
+static const struct {
+ const char *input;
+ uoff_t size;
+ int stream_errno;
+} tests[] = {
+ { "", 0, 0 },
+ { "", 1, EPIPE },
+ { "a", 1, 0 },
+ { "ab", 1, EINVAL },
+ { "ab", 0, EINVAL },
+ { "ab", UOFF_T_MAX, EPIPE },
+};
+
+static void
+run_test(const char *sized_input, uoff_t sized_size, int stream_errno)
+{
+ unsigned int sized_input_len = strlen(sized_input);
+ struct istream *input_data, *input;
+ const unsigned char *data;
+ size_t i, size;
+ int ret = 0;
+
+ input_data = test_istream_create_data(sized_input, sized_input_len);
+ test_istream_set_allow_eof(input_data, FALSE);
+ input = i_stream_create_sized(input_data, sized_size);
+
+ for (i = 1; i < sized_input_len; i++) {
+ test_istream_set_size(input_data, i);
+ while ((ret = i_stream_read(input)) > 0) ;
+ if (ret == -1 && stream_errno != 0)
+ break;
+ test_assert(ret == 0);
+ }
+ if (ret == 0) {
+ test_istream_set_allow_eof(input_data, TRUE);
+ test_istream_set_size(input_data, i);
+ while ((ret = i_stream_read(input)) > 0) ;
+ }
+ test_assert(ret == -1);
+ test_assert(input->stream_errno == stream_errno);
+
+ data = i_stream_get_data(input, &size);
+ test_assert(size == I_MIN(sized_input_len, sized_size));
+ if (size > 0)
+ test_assert(memcmp(data, sized_input, size) == 0);
+ i_stream_unref(&input);
+ i_stream_unref(&input_data);
+}
+
+static void test_istream_sized_full(bool exact)
+{
+ const unsigned char test_data[10] = "1234567890";
+ struct istream *test_input, *input;
+ unsigned int i, j;
+ int expected_errno;
+
+ for (i = 1; i < sizeof(test_data)*2; i++) {
+ test_input = test_istream_create_data(test_data, sizeof(test_data));
+ test_istream_set_allow_eof(test_input, FALSE);
+ test_istream_set_size(test_input, 0);
+
+ if (exact)
+ input = i_stream_create_sized(test_input, i);
+ else
+ input = i_stream_create_min_sized(test_input, i);
+ for (j = 1; j <= I_MIN(i, sizeof(test_data)); j++) {
+ test_assert_idx(i_stream_read(input) == 0, j);
+ test_istream_set_size(test_input, j);
+ test_assert_idx(i_stream_read(input) == 1, j);
+ }
+ test_assert_idx(i_stream_read(input) == 0, i);
+ if (j <= sizeof(test_data))
+ test_istream_set_size(test_input, j);
+ else
+ test_istream_set_allow_eof(test_input, TRUE);
+ test_assert_idx(i_stream_read(input) == -1 && input->eof, i);
+ if (i > sizeof(test_data))
+ expected_errno = EPIPE;
+ else if (i < sizeof(test_data) && exact)
+ expected_errno = EINVAL;
+ else
+ expected_errno = 0;
+ test_assert_idx(input->stream_errno == expected_errno, i);
+ i_stream_unref(&input);
+ i_stream_unref(&test_input);
+ }
+}
+
+void test_istream_sized(void)
+{
+ unsigned int i;
+
+ for (i = 0; i < N_ELEMENTS(tests); i++) {
+ test_begin(t_strdup_printf("istream sized %u", i+1));
+ run_test(tests[i].input, tests[i].size, tests[i].stream_errno);
+ test_end();
+ }
+ test_begin("istream sized");
+ test_istream_sized_full(TRUE);
+ test_end();
+
+ test_begin("istream sized min");
+ test_istream_sized_full(FALSE);
+ test_end();
+}
diff --git a/src/lib/test-istream-tee.c b/src/lib/test-istream-tee.c
new file mode 100644
index 0000000..53e9a0c
--- /dev/null
+++ b/src/lib/test-istream-tee.c
@@ -0,0 +1,139 @@
+/* Copyright (c) 2009-2018 Dovecot authors, see the included COPYING file */
+
+#include "test-lib.h"
+#include "str.h"
+#include "istream-private.h"
+#include "istream-tee.h"
+
+
+#define TEST_BUF_SIZE I_STREAM_MIN_SIZE
+#define TEST_STR_LEN (TEST_BUF_SIZE*3)
+#define CHILD_COUNT 5
+
+static void test_istream_tee_tailing(const char *str)
+{
+ struct istream *test_input, *child_input[CHILD_COUNT];
+ struct tee_istream *tee;
+ unsigned int i, len, delta;
+
+ test_input = test_istream_create(str);
+ test_istream_set_max_buffer_size(test_input, TEST_BUF_SIZE);
+
+ test_begin("istream tee tailing");
+ tee = tee_i_stream_create(test_input);
+ for (i = 0; i < CHILD_COUNT; i++)
+ child_input[i] = tee_i_stream_create_child(tee);
+
+ test_istream_set_allow_eof(test_input, FALSE);
+ delta = 1;
+ for (len = 1; len < TEST_BUF_SIZE; len += delta) {
+ test_istream_set_size(test_input, len);
+ for (i = 0; i < CHILD_COUNT; i++) {
+ test_assert_idx(i_stream_read(child_input[i]) == (int)delta, len);
+ test_assert_idx(!tee_i_stream_child_is_waiting(child_input[i]), len);
+ test_assert_idx(i_stream_read(child_input[i]) == 0, len);
+ test_assert_idx(!tee_i_stream_child_is_waiting(child_input[i]), len);
+ }
+ delta = i_rand_limit(32); /* may stand still */
+ if(delta > TEST_BUF_SIZE - len)
+ delta = 1;
+ }
+
+ test_istream_set_size(test_input, len);
+ for (i = 0; i < CHILD_COUNT; i++) {
+ test_assert(i_stream_read(child_input[i]) == (int)delta);
+ test_assert(i_stream_read(child_input[i]) == -2);
+ test_assert(!tee_i_stream_child_is_waiting(child_input[i]));
+ }
+
+ delta = 1;
+ while ((len += delta) <= TEST_STR_LEN) {
+ unsigned int lagger = i_rand_limit(CHILD_COUNT);
+ test_istream_set_size(test_input, len);
+ for (i = 0; i < CHILD_COUNT; i++) {
+ test_assert(i_stream_read(child_input[i]) == -2);
+ test_assert(!tee_i_stream_child_is_waiting(child_input[i]));
+ }
+ for (i = 0; i < CHILD_COUNT; i++) {
+ if (i == lagger)
+ continue;
+ i_stream_skip(child_input[i], delta);
+ test_assert(i_stream_read(child_input[i]) == 0);
+ test_assert(tee_i_stream_child_is_waiting(child_input[i]));
+ }
+ i_stream_skip(child_input[lagger], delta);
+ for (i = 0; i < CHILD_COUNT; i++) {
+ test_assert(i_stream_read(child_input[i]) == (int)delta);
+ test_assert(i_stream_read(child_input[i]) == -2);
+ test_assert(!tee_i_stream_child_is_waiting(child_input[i]));
+ }
+ delta = i_rand_minmax(1, 31); /* mustn't stand still */
+ if(delta > TEST_STR_LEN - len)
+ delta = 1;
+ }
+
+ for (i = 0; i < CHILD_COUNT-1; i++) {
+ i_stream_skip(child_input[i], 1);
+ test_assert(i_stream_read(child_input[i]) == 0);
+ test_assert(tee_i_stream_child_is_waiting(child_input[i]));
+ }
+ i_stream_skip(child_input[i], 1);
+ test_assert(i_stream_read(child_input[i]) == 0);
+ test_assert(!tee_i_stream_child_is_waiting(child_input[i]));
+
+ test_istream_set_allow_eof(test_input, TRUE);
+ for (i = 0; i < CHILD_COUNT; i++) {
+ test_assert(i_stream_read(child_input[i]) == -1);
+ i_stream_unref(&child_input[i]);
+ }
+ i_stream_unref(&test_input);
+
+ test_end();
+}
+
+static void test_istream_tee_blocks(const char *str)
+{
+ struct istream *test_input, *child_input[CHILD_COUNT];
+ struct tee_istream *tee;
+ unsigned int i, j;
+
+ test_input = test_istream_create(str);
+ test_istream_set_max_buffer_size(test_input, TEST_BUF_SIZE);
+
+ test_begin("istream tee blocks");
+ tee = tee_i_stream_create(test_input);
+ for (i = 0; i < CHILD_COUNT; i++)
+ child_input[i] = tee_i_stream_create_child(tee);
+
+ test_istream_set_allow_eof(test_input, FALSE);
+ for (j = 1; j <= 3; j++) {
+ test_istream_set_size(test_input, TEST_BUF_SIZE*j);
+ for (i = 0; i < CHILD_COUNT; i++) {
+ test_assert(i_stream_read(child_input[i]) == TEST_BUF_SIZE);
+ i_stream_skip(child_input[i], TEST_BUF_SIZE);
+ }
+ }
+ test_istream_set_allow_eof(test_input, TRUE);
+ for (i = 0; i < CHILD_COUNT; i++) {
+ test_assert(i_stream_read(child_input[i]) == -1);
+ i_stream_unref(&child_input[i]);
+ }
+ i_stream_unref(&test_input);
+
+ test_end();
+}
+
+void test_istream_tee(void)
+{
+ string_t *str;
+ unsigned int i;
+
+ str = str_new(default_pool, TEST_STR_LEN);
+ for (i = 0; i < TEST_STR_LEN; i++)
+ str_append_c(str, 'a' + i%26);
+
+ test_istream_tee_tailing(str_c(str));
+ test_istream_tee_blocks(str_c(str));
+
+ str_free(&str);
+}
diff --git a/src/lib/test-istream-try.c b/src/lib/test-istream-try.c
new file mode 100644
index 0000000..888e00f
--- /dev/null
+++ b/src/lib/test-istream-try.c
@@ -0,0 +1,195 @@
+/* Copyright (c) 2009-2018 Dovecot authors, see the included COPYING file */
+
+#include "test-lib.h"
+#include "istream-private.h"
+#include "istream-base64.h"
+#include "istream-try.h"
+
+#define MIN_FULL_SIZE 1
+
+static void test_istream_try_normal(void)
+{
+ bool finished = FALSE;
+
+ test_begin("istream try");
+ for (unsigned int test = 0; test <= 10; test++) {
+ struct istream *test_inputs[3], *try_input;
+
+ test_inputs[0] = test_istream_create("1");
+ test_inputs[1] = test_istream_create("2");
+ test_inputs[2] = NULL;
+ test_istream_set_size(test_inputs[0], 0);
+ test_istream_set_size(test_inputs[1], 0);
+ try_input = istream_try_create(test_inputs, MIN_FULL_SIZE);
+
+ /* nonblocking read */
+ test_assert_idx(i_stream_read(try_input) == 0, test);
+
+ switch (test) {
+ case 0:
+ /* stream 0 is available */
+ test_istream_set_size(test_inputs[0], 1);
+ test_assert_idx(i_stream_read(try_input) == 1, test);
+ test_assert_idx(i_stream_get_data_size(test_inputs[0]) == 1, test);
+ test_assert_idx(i_stream_get_data_size(test_inputs[1]) == 0, test);
+ break;
+ case 1:
+ /* stream 1 is available, but not used before 0 */
+ test_istream_set_size(test_inputs[1], 1);
+ test_assert_idx(i_stream_read(try_input) == 0, test);
+ test_assert_idx(i_stream_get_data_size(test_inputs[0]) == 0, test);
+ test_assert_idx(i_stream_get_data_size(test_inputs[1]) == 0, test);
+ /* continue failing stream 0 -> 1 is available */
+ test_inputs[0]->stream_errno = EINVAL;
+ test_assert_idx(i_stream_read(try_input) == 1, test);
+ test_assert_idx(i_stream_get_data_size(test_inputs[0]) == 0, test);
+ test_assert_idx(i_stream_get_data_size(test_inputs[1]) == 1, test);
+ break;
+ case 2:
+ /* both streams are available - stream 0 is read */
+ test_istream_set_size(test_inputs[0], 1);
+ test_istream_set_size(test_inputs[1], 1);
+ test_assert_idx(i_stream_read(try_input) == 1, test);
+ test_assert_idx(i_stream_get_data_size(test_inputs[0]) == 1, test);
+ test_assert_idx(i_stream_get_data_size(test_inputs[1]) == 0, test);
+ break;
+ case 3:
+ /* stream 0 fails */
+ test_inputs[0]->stream_errno = EINVAL;
+ test_assert_idx(i_stream_read(try_input) == 0, test);
+ test_assert_idx(i_stream_get_data_size(test_inputs[0]) == 0, test);
+ test_assert_idx(i_stream_get_data_size(test_inputs[1]) == 0, test);
+ /* continue making stream 1 available */
+ test_istream_set_size(test_inputs[1], 1);
+ test_assert_idx(i_stream_read(try_input) == 1, test);
+ test_assert_idx(i_stream_get_data_size(test_inputs[0]) == 0, test);
+ test_assert_idx(i_stream_get_data_size(test_inputs[1]) == 1, test);
+ break;
+ case 4:
+ /* stream 1 fails */
+ test_inputs[1]->stream_errno = EINVAL;
+ test_assert_idx(i_stream_read(try_input) == 0, test);
+ test_assert_idx(i_stream_get_data_size(test_inputs[0]) == 0, test);
+ test_assert_idx(i_stream_get_data_size(test_inputs[1]) == 0, test);
+ break;
+ case 5:
+ /* stream 0 fails, stream 1 is available */
+ test_inputs[0]->stream_errno = EINVAL;
+ test_istream_set_size(test_inputs[1], 1);
+ test_assert_idx(i_stream_read(try_input) == 1, test);
+ test_assert_idx(i_stream_get_data_size(test_inputs[0]) == 0, test);
+ test_assert_idx(i_stream_get_data_size(test_inputs[1]) == 1, test);
+ break;
+ case 6:
+ /* stream 0 is available, stream 1 fails */
+ test_inputs[1]->stream_errno = EINVAL;
+ test_istream_set_size(test_inputs[0], 1);
+ test_assert_idx(i_stream_read(try_input) == 1, test);
+ test_assert_idx(i_stream_get_data_size(test_inputs[0]) == 1, test);
+ test_assert_idx(i_stream_get_data_size(test_inputs[1]) == 0, test);
+ break;
+ case 7:
+ /* both streams fail */
+ test_inputs[0]->stream_errno = EINVAL;
+ test_inputs[1]->stream_errno = EINVAL;
+ test_assert_idx(i_stream_read(try_input) == -1, test);
+ test_assert_idx(try_input->stream_errno == EINVAL, test);
+ break;
+ case 8:
+ /* stream 0 fails with EINVAL, stream 1 with EIO */
+ test_inputs[0]->stream_errno = EINVAL;
+ test_inputs[1]->stream_errno = EIO;
+ test_assert_idx(i_stream_read(try_input) == -1, test);
+ test_assert_idx(try_input->stream_errno == EIO, test);
+ break;
+ case 9:
+ /* stream 0 fails with EIO, stream 1 with EINVAL */
+ test_inputs[0]->stream_errno = EIO;
+ test_inputs[1]->stream_errno = EINVAL;
+ test_assert_idx(i_stream_read(try_input) == -1, test);
+ test_assert_idx(try_input->stream_errno == EIO, test);
+ break;
+ case 10:
+ /* stream 0 fails with EIO, stream 1 would work.. */
+ test_inputs[0]->stream_errno = EIO;
+ test_istream_set_size(test_inputs[1], 1);
+ test_assert_idx(i_stream_read(try_input) == -1, test);
+ test_assert_idx(try_input->stream_errno == EIO, test);
+ test_assert_idx(i_stream_get_data_size(test_inputs[1]) == 0, test);
+
+ finished = TRUE;
+ break;
+ }
+
+ test_assert_idx(test_inputs[0]->v_offset == 0, test);
+ test_assert_idx(test_inputs[1]->v_offset == 0, test);
+
+ i_stream_unref(&test_inputs[0]);
+ i_stream_unref(&test_inputs[1]);
+ i_stream_unref(&try_input);
+ }
+ i_assert(finished);
+ test_end();
+}
+
+static void test_istream_try_empty(void)
+{
+ test_begin("istream try empty stream");
+ struct istream *test_inputs[] = {
+ test_istream_create(""),
+ test_istream_create(""),
+ NULL
+ };
+ struct istream *try_input =
+ istream_try_create(test_inputs, MIN_FULL_SIZE);
+ test_assert(i_stream_read(try_input) == -1);
+ test_assert(try_input->eof);
+ test_assert(try_input->stream_errno == 0);
+ i_stream_unref(&test_inputs[0]);
+ i_stream_unref(&test_inputs[1]);
+ i_stream_unref(&try_input);
+ test_end();
+}
+
+static void test_istream_try_buffer_full(void)
+{
+ const char *test_strings[] = { "Zm9v", "YmFy" };
+ struct istream *test_inputs[3], *try_input, *input, *input2;
+
+ test_begin("istream try buffer full");
+
+ for (unsigned int i = 0; i < 2; i++) {
+ input = test_istream_create(test_strings[i]);
+ test_istream_set_size(input, 1);
+ test_istream_set_max_buffer_size(input, 1);
+ input2 = i_stream_create_base64_decoder(input);
+ i_stream_unref(&input);
+ test_inputs[i] = input2;
+ };
+ test_inputs[2] = NULL;
+
+ try_input = istream_try_create(test_inputs, MIN_FULL_SIZE);
+
+ test_assert(i_stream_read(try_input) == 0);
+ test_assert(try_input->real_stream->parent != NULL);
+ test_assert(i_stream_get_data_size(test_inputs[0]) == 0);
+ test_assert(i_stream_get_data_size(test_inputs[1]) == 0);
+
+ test_istream_set_size(test_inputs[0], 2);
+ test_assert(i_stream_read(try_input) == 1);
+ test_assert(i_stream_get_data_size(test_inputs[0]) == 1);
+ test_assert(i_stream_get_data_size(test_inputs[1]) == 0);
+
+ i_stream_unref(&test_inputs[0]);
+ i_stream_unref(&test_inputs[1]);
+ i_stream_unref(&try_input);
+
+ test_end();
+}
+
+void test_istream_try(void)
+{
+ test_istream_try_normal();
+ test_istream_try_empty();
+ test_istream_try_buffer_full();
+}
diff --git a/src/lib/test-istream-unix.c b/src/lib/test-istream-unix.c
new file mode 100644
index 0000000..e1fb649
--- /dev/null
+++ b/src/lib/test-istream-unix.c
@@ -0,0 +1,187 @@
+/* Copyright (c) 2015-2018 Dovecot authors, see the included COPYING file */
+
+#include "test-lib.h"
+#include "net.h"
+#include "fdpass.h"
+#include "istream.h"
+#include "istream-unix.h"
+
+#include <unistd.h>
+#include <fcntl.h>
+#include <sys/stat.h>
+
+static int send_fd, send_fd2;
+
+static void write_one(int fd)
+{
+ if (write(fd, "1", 1) < 0)
+ i_fatal("write() failed: %m");
+}
+
+static void read_one(int fd)
+{
+ char buf;
+
+ if (read(fd, &buf, 1) < 0)
+ i_fatal("read() failed: m");
+}
+
+static void
+test_server_read_nofd(struct istream *input, unsigned int idx)
+{
+ const unsigned char *data;
+ size_t size;
+
+ test_assert_idx(i_stream_read_more(input, &data, &size) == 1, idx);
+ i_stream_skip(input, 1);
+ test_assert_idx(i_stream_unix_get_read_fd(input) == -1, idx);
+}
+
+static void
+test_server_read_fd(struct istream *input, int wanted_fd, unsigned int idx)
+{
+ struct stat st1, st2;
+ const unsigned char *data;
+ size_t size;
+ int recv_fd;
+
+ test_assert_idx(i_stream_read_more(input, &data, &size) == 1, idx);
+ i_stream_skip(input, 1);
+ test_assert_idx((recv_fd = i_stream_unix_get_read_fd(input)) != -1, idx);
+ if (recv_fd != -1) {
+ if (fstat(recv_fd, &st1) < 0 || fstat(wanted_fd, &st2) < 0)
+ i_fatal("fstat() failed: %m");
+ test_assert_idx(st1.st_ino == st2.st_ino, idx);
+ i_close_fd(&recv_fd);
+ }
+}
+
+static void test_istream_unix_server(int fd)
+{
+ struct istream *input;
+ const unsigned char *data;
+ size_t size;
+
+ input = i_stream_create_unix(fd, 1024);
+ /* 1) simple read */
+ test_server_read_nofd(input, 1);
+ write_one(fd);
+
+ /* 2) fd was sent but we won't get it */
+ test_server_read_nofd(input, 2);
+ /* we still shouldn't have the fd */
+ i_stream_set_blocking(input, FALSE);
+ i_stream_unix_set_read_fd(input);
+ test_assert(i_stream_read_more(input, &data, &size) == 0);
+ test_assert(i_stream_unix_get_read_fd(input) == -1);
+ i_stream_set_blocking(input, TRUE);
+ write_one(fd);
+
+ /* 3) the previous fd should be lost now */
+ test_server_read_nofd(input, 3);
+ write_one(fd);
+
+ /* 4) we should get the fd now */
+ test_server_read_fd(input, send_fd2, 4);
+ write_one(fd);
+
+ /* 5) the previous fd shouldn't be returned anymore */
+ i_stream_unix_set_read_fd(input);
+ test_server_read_nofd(input, 5);
+ write_one(fd);
+
+ /* 6) with i_stream_unix_unset_read_fd() we shouldn't get fd anymore */
+ i_stream_unix_unset_read_fd(input);
+ test_server_read_nofd(input, 6);
+ write_one(fd);
+
+ /* 7-8) two fds were sent, but we'll get only the first one */
+ i_stream_unix_set_read_fd(input);
+ test_server_read_fd(input, send_fd, 7);
+ test_server_read_nofd(input, 8);
+ write_one(fd);
+
+ /* 9-10) two fds were sent, and we'll get them both */
+ i_stream_unix_set_read_fd(input);
+ test_server_read_fd(input, send_fd, 9);
+ i_stream_unix_set_read_fd(input);
+ test_server_read_fd(input, send_fd2, 10);
+ write_one(fd);
+
+ i_stream_destroy(&input);
+ i_close_fd(&fd);
+}
+
+static void test_istream_unix_client(int fd)
+{
+ /* 1) */
+ write_one(fd);
+ read_one(fd);
+
+ /* 2) */
+ if (fd_send(fd, send_fd, "1", 1) < 0)
+ i_fatal("fd_send() failed: %m");
+ read_one(fd);
+
+ /* 3) */
+ write_one(fd);
+ read_one(fd);
+
+ /* 4) */
+ if (fd_send(fd, send_fd2, "1", 1) < 0)
+ i_fatal("fd_send() failed: %m");
+ read_one(fd);
+
+ /* 5) */
+ write_one(fd);
+ read_one(fd);
+
+ /* 6) */
+ if (fd_send(fd, send_fd, "1", 1) < 0)
+ i_fatal("fd_send() failed: %m");
+ read_one(fd);
+
+ /* 7-8) */
+ if (fd_send(fd, send_fd, "1", 1) < 0)
+ i_fatal("fd_send() failed: %m");
+ if (fd_send(fd, send_fd2, "1", 1) < 0)
+ i_fatal("fd_send() failed: %m");
+ read_one(fd);
+
+ /* 9-10) */
+ if (fd_send(fd, send_fd, "1", 1) < 0)
+ i_fatal("fd_send() failed: %m");
+ if (fd_send(fd, send_fd2, "1", 1) < 0)
+ i_fatal("fd_send() failed: %m");
+ read_one(fd);
+
+ i_close_fd(&fd);
+}
+
+void test_istream_unix(void)
+{
+ int fd[2];
+
+ test_begin("istream unix");
+ if ((send_fd = open("/dev/null", O_RDONLY)) == -1)
+ i_fatal("open(/dev/null) failed: %m");
+ if ((send_fd2 = open("/dev/zero", O_RDONLY)) == -1)
+ i_fatal("open(/dev/zero) failed: %m");
+ if (socketpair(AF_UNIX, SOCK_STREAM, 0, fd) < 0)
+ i_fatal("socketpair() failed: %m");
+ switch (fork()) {
+ case -1:
+ i_fatal("fork() failed: %m");
+ case 0:
+ i_close_fd(&fd[0]);
+ test_istream_unix_client(fd[1]);
+ test_exit(0);
+ default:
+ i_close_fd(&fd[1]);
+ test_istream_unix_server(fd[0]);
+ break;
+ }
+ i_close_fd(&send_fd);
+ i_close_fd(&send_fd2);
+ test_end();
+}
diff --git a/src/lib/test-istream.c b/src/lib/test-istream.c
new file mode 100644
index 0000000..cedc650
--- /dev/null
+++ b/src/lib/test-istream.c
@@ -0,0 +1,381 @@
+/* Copyright (c) 2014-2018 Dovecot authors, see the included COPYING file */
+
+#include "test-lib.h"
+#include "istream.h"
+#include "istream-crlf.h"
+
+static void test_istream_children(void)
+{
+ struct istream *parent, *child1, *child2;
+ const unsigned char *data;
+ size_t size;
+
+ test_begin("istream children");
+
+ parent = test_istream_create_data("123456789", 9);
+ test_istream_set_max_buffer_size(parent, 3);
+
+ child1 = i_stream_create_limit(parent, UOFF_T_MAX);
+ child2 = i_stream_create_limit(parent, UOFF_T_MAX);
+
+ /* child1 read beginning */
+ test_assert(i_stream_read(child1) == 3);
+ data = i_stream_get_data(child1, &size);
+ test_assert(size == 3 && memcmp(data, "123", 3) == 0);
+ i_stream_skip(child1, 3);
+ /* child1 read middle.. */
+ test_assert(i_stream_read(child1) == 3);
+ data = i_stream_get_data(child1, &size);
+ test_assert(size == 3 && memcmp(data, "456", 3) == 0);
+ /* child2 read beginning.. */
+ test_assert(i_stream_read(child2) == 3);
+ data = i_stream_get_data(child2, &size);
+ test_assert(size == 3 && memcmp(data, "123", 3) == 0);
+ /* child1 check middle again.. the parent has been modified,
+ so it can't return the original data (without some code changes). */
+ test_assert(i_stream_get_data_size(child1) == 0);
+ i_stream_skip(child1, 3);
+ /* child1 read end */
+ test_assert(i_stream_read(child1) == 3);
+ data = i_stream_get_data(child1, &size);
+ test_assert(size == 3 && memcmp(data, "789", 3) == 0);
+ i_stream_skip(child1, 3);
+ test_assert(i_stream_read(child1) == -1);
+ /* child2 check beginning again.. */
+ test_assert(i_stream_get_data_size(child1) == 0);
+ i_stream_skip(child2, 3);
+ /* child2 read middle */
+ test_assert(i_stream_read(child2) == 3);
+ data = i_stream_get_data(child2, &size);
+ test_assert(size == 3 && memcmp(data, "456", 3) == 0);
+ i_stream_skip(child2, 3);
+
+ i_stream_destroy(&child1);
+ i_stream_destroy(&child2);
+ i_stream_destroy(&parent);
+
+ test_end();
+}
+
+static void test_istream_next_line_expect(struct istream *is, const char *expect,
+ unsigned int i)
+{
+ const char *line = i_stream_next_line(is);
+ test_assert_strcmp_idx(line, expect, i);
+}
+
+static void test_istream_next_line(void)
+{
+ /* single line cases */
+#define TEST_CASE(a, s, b) { \
+ .input = (const unsigned char*)((a)), .input_len = sizeof((a)), \
+ .skip = s, \
+ .output = b }
+ const struct test_case_sl {
+ const unsigned char *input;
+ size_t input_len;
+ size_t skip;
+ const char *output;
+ } test_cases_sl[] = {
+ TEST_CASE("", 0, NULL),
+ TEST_CASE("a\n", 1, ""),
+ TEST_CASE("a\r\n", 0, "a"),
+ TEST_CASE("a\r\n", 1, ""),
+ TEST_CASE("a\r\n", 2, ""),
+ TEST_CASE("hello\nworld\n", 6, "world"),
+ TEST_CASE("hello\nworld", 6, NULL),
+ TEST_CASE("hello\n\n\n\n", 6, ""),
+ TEST_CASE("wrong\n\r\n\n", 0, "wrong"),
+ TEST_CASE("wrong\n\r\r\n", 6, "\r"),
+ TEST_CASE("wrong\n\r\r\n", 7, ""),
+ };
+
+ test_begin("i_stream_next_line");
+
+ for(unsigned int i = 0; i < N_ELEMENTS(test_cases_sl); i++) {
+ const struct test_case_sl *test_case = &test_cases_sl[i];
+ struct istream *input =
+ i_stream_create_copy_from_data(test_case->input, test_case->input_len);
+ test_assert_idx(i_stream_read(input) >= 0 ||
+ (input->stream_errno == 0 && input->eof), i);
+ i_stream_skip(input, test_case->skip);
+ test_assert_strcmp_idx(i_stream_next_line(input), test_case->output, i);
+ test_assert_idx(input->stream_errno == 0, i);
+ i_stream_unref(&input);
+
+ input = test_istream_create_data(test_case->input, test_case->input_len);
+ test_assert_idx(i_stream_read(input) >= 0 ||
+ (input->stream_errno == 0 && input->eof), i);
+ i_stream_skip(input, test_case->skip);
+ test_assert_strcmp_idx(i_stream_next_line(input), test_case->output, i);
+ test_assert_idx(input->stream_errno == 0, i);
+ i_stream_unref(&input);
+ }
+
+#undef TEST_CASE
+#define TEST_CASE(a) test_istream_create_data((a), sizeof(a))
+ /* multiline tests */
+ struct istream *is = TEST_CASE("\n\n\n\n\n\n");
+ size_t i;
+ test_assert(i_stream_read(is) >= 0 || (is->stream_errno == 0 && is->eof));
+ for(i = 0; i < 6; i++)
+ test_istream_next_line_expect(is, "", i);
+ test_assert(is->stream_errno == 0);
+ i_stream_unref(&is);
+
+ is = TEST_CASE(
+ "simple\r\n"
+ "multiline\n"
+ "test with\0"
+ "some exciting\n"
+ "things\r\n\0");
+ test_assert(i_stream_read(is) >= 0 || (is->stream_errno == 0 && is->eof));
+ test_istream_next_line_expect(is, "simple", 0);
+ test_istream_next_line_expect(is, "multiline", 1);
+ test_istream_next_line_expect(is, "test with", 2);
+ test_istream_next_line_expect(is, "things", 3);
+ test_istream_next_line_expect(is, NULL, 4);
+ test_assert(is->stream_errno == 0);
+ i_stream_unref(&is);
+
+ is = TEST_CASE(
+ "NUL\0"
+ "test\n");
+ test_assert(i_stream_read(is) >= 0 || (is->stream_errno == 0 && is->eof));
+ test_istream_next_line_expect(is, "NUL", 0);
+ test_istream_next_line_expect(is, NULL, 1);
+ test_assert(is->stream_errno == 0);
+ i_stream_unref(&is);
+
+ const char test_data_1[] =
+ "this is some data\n"
+ "written like this\n"
+ "to attempt and induce\n"
+ "errors or flaws\n";
+
+ is = TEST_CASE(test_data_1);
+ size_t n = 0;
+ const char *const *lines = t_strsplit(test_data_1, "\n");
+ for(i = 0; i < sizeof(test_data_1); i++) {
+ test_istream_set_size(is, i);
+ test_assert(i_stream_read(is) >= 0);
+ const char *line = i_stream_next_line(is);
+ if (line != NULL) {
+ test_assert_strcmp_idx(lines[n], line, n);
+ n++;
+ }
+ }
+ test_assert(n == 4);
+ test_assert(is->stream_errno == 0);
+ i_stream_unref(&is);
+
+ const char test_data_2[] =
+ "this is some data\n"
+ "written like this\n"
+ "to attempt and induce\n"
+ "errors or flaws";
+
+ is = TEST_CASE(test_data_2);
+ lines = t_strsplit(test_data_2, "\n");
+ i_stream_set_return_partial_line(is, TRUE);
+ n = 0;
+
+ /* requires one extra read to get the last line */
+ for(i = 0; i < sizeof(test_data_1) + 1; i++) {
+ test_istream_set_size(is, I_MIN(sizeof(test_data_1), i));
+ (void)i_stream_read(is);
+ const char *line = i_stream_next_line(is);
+ if (line != NULL) {
+ test_assert_strcmp_idx(lines[n], line, n);
+ n++;
+ }
+ (void)i_stream_read(is);
+ }
+ test_assert(n == 4);
+ test_assert(is->stream_errno == 0);
+ i_stream_unref(&is);
+
+ const char test_data_3[] =
+ "this is some data\r\n"
+ "written like this\r\n"
+ "to attempt and induce\r\n"
+ "errors or flaws\r\n";
+
+ struct istream *is_1 = TEST_CASE(test_data_3);
+ is = i_stream_create_crlf(is_1);
+ i_stream_unref(&is_1);
+
+ lines = t_strsplit_spaces(test_data_3, "\r\n");
+ n = 0;
+
+ for(i = 0; i < sizeof(test_data_3); i++) {
+ test_istream_set_size(is, i);
+ test_assert(i_stream_read(is) >= 0);
+ const char *line = i_stream_next_line(is);
+ if (line != NULL) {
+ test_assert_strcmp_idx(lines[n], line, n);
+ n++;
+ }
+ }
+ test_assert(n == 4);
+ test_assert(is->stream_errno == 0);
+ i_stream_unref(&is);
+
+ test_end();
+}
+
+static void test_istream_read_next_line(void)
+{
+ /* single line cases */
+#undef TEST_CASE
+#define TEST_CASE(a, s, b) { \
+ .input = (const unsigned char*)((a)), .input_len = sizeof((a)), \
+ .skip = s, \
+ .output = b }
+ const struct test_case_sl {
+ const unsigned char *input;
+ size_t input_len;
+ size_t skip;
+ const char *output;
+ } test_cases_sl[] = {
+ TEST_CASE("", 0, NULL),
+ TEST_CASE("a\n", 1, ""),
+ TEST_CASE("a\r\n", 0, "a"),
+ TEST_CASE("a\r\n", 1, ""),
+ TEST_CASE("a\r\n", 2, ""),
+ TEST_CASE("hello\nworld\n", 6, "world"),
+ TEST_CASE("hello\nworld", 6, NULL),
+ TEST_CASE("hello\n\n\n\n", 6, ""),
+ TEST_CASE("wrong\n\r\n\n", 0, "wrong"),
+ TEST_CASE("wrong\n\r\r\n", 6, "\r"),
+ TEST_CASE("wrong\n\r\r\n", 7, ""),
+ };
+
+ test_begin("i_stream_read_next_line");
+ for(unsigned int i = 0; i < N_ELEMENTS(test_cases_sl); i++) {
+ const struct test_case_sl *test_case = &test_cases_sl[i];
+ struct istream *input =
+ i_stream_create_copy_from_data(test_case->input, test_case->input_len);
+ i_stream_skip(input, test_case->skip);
+ test_assert_strcmp_idx(i_stream_read_next_line(input), test_case->output, i);
+ test_assert_idx(input->stream_errno == 0, i);
+ i_stream_unref(&input);
+
+ input = test_istream_create_data(test_case->input, test_case->input_len);
+ i_stream_skip(input, test_case->skip);
+ test_assert_strcmp_idx(i_stream_read_next_line(input), test_case->output, i);
+ test_assert_idx(input->stream_errno == 0, i);
+ i_stream_unref(&input);
+ }
+
+ const char test_data_1[] =
+ "this is some data\n"
+ "written like this\n"
+ "to attempt and induce\n"
+ "errors or flaws\n";
+
+#undef TEST_CASE
+#define TEST_CASE(a) test_istream_create_data((a), sizeof(a))
+ /* multiline tests */
+ struct istream *is = TEST_CASE("\n\n\n\n\n\n");
+ size_t i;
+ for(i = 0; i < 6; i++)
+ test_assert_strcmp_idx(i_stream_read_next_line(is), "", i);
+ test_assert(is->stream_errno == 0);
+ i_stream_unref(&is);
+
+ is = TEST_CASE(
+ "simple\r\n"
+ "multiline\n"
+ "test with\0"
+ "some exciting\n"
+ "things\r\n\0");
+ test_assert_strcmp_idx(i_stream_read_next_line(is), "simple", 0);
+ test_assert_strcmp_idx(i_stream_read_next_line(is), "multiline", 1);
+ test_assert_strcmp_idx(i_stream_read_next_line(is), "test with", 2);
+ test_assert_strcmp_idx(i_stream_read_next_line(is), "things", 3);
+ test_assert_strcmp_idx(i_stream_read_next_line(is), NULL, 4);
+ test_assert(is->stream_errno == 0);
+ i_stream_unref(&is);
+
+ is = TEST_CASE(
+ "NUL\0"
+ "test\n");
+ test_assert_strcmp_idx(i_stream_read_next_line(is), "NUL", 0);
+ test_assert_strcmp_idx(i_stream_read_next_line(is), NULL, 1);
+ test_assert(is->stream_errno == 0);
+ i_stream_unref(&is);
+
+ is = TEST_CASE(test_data_1);
+ size_t n = 0;
+ const char *const *lines = t_strsplit(test_data_1, "\n");
+ for(i = 0; i < sizeof(test_data_1); i++) {
+ test_istream_set_size(is, i);
+ const char *line = i_stream_read_next_line(is);
+ if (line != NULL) {
+ test_assert_strcmp_idx(lines[n], line, n);
+ n++;
+ }
+ }
+ test_assert(n == 4);
+ test_assert(is->stream_errno == 0);
+ i_stream_unref(&is);
+
+ const char test_data_2[] =
+ "this is some data\n"
+ "written like this\n"
+ "to attempt and induce\n"
+ "errors or flaws";
+
+ is = TEST_CASE(test_data_2);
+ lines = t_strsplit(test_data_2, "\n");
+ i_stream_set_return_partial_line(is, TRUE);
+ n = 0;
+
+ for(i = 0; i < sizeof(test_data_1); i++) {
+ test_istream_set_size(is, i);
+ const char *line = i_stream_read_next_line(is);
+ if (line != NULL) {
+ test_assert_strcmp_idx(lines[n], line, n);
+ n++;
+ }
+ }
+ test_assert(n == 4);
+ test_assert(is->stream_errno == 0);
+ i_stream_unref(&is);
+
+ const char test_data_3[] =
+ "this is some data\r\n"
+ "written like this\r\n"
+ "to attempt and induce\r\n"
+ "errors or flaws\r\n";
+
+
+ struct istream *is_1 = TEST_CASE(test_data_3);
+ is = i_stream_create_crlf(is_1);
+ i_stream_unref(&is_1);
+
+ lines = t_strsplit_spaces(test_data_3, "\r\n");
+ n = 0;
+
+ for(i = 0; i < sizeof(test_data_3); i++) {
+ test_istream_set_size(is, i);
+ const char *line = i_stream_read_next_line(is);
+ if (line != NULL) {
+ test_assert_strcmp_idx(lines[n], line, n);
+ n++;
+ }
+ }
+ test_assert(n == 4);
+ test_assert(is->stream_errno == 0);
+ i_stream_unref(&is);
+
+ test_end();
+}
+
+void test_istream(void)
+{
+ test_istream_children();
+ test_istream_next_line();
+ test_istream_read_next_line();
+}
diff --git a/src/lib/test-json-parser.c b/src/lib/test-json-parser.c
new file mode 100644
index 0000000..30ac5da
--- /dev/null
+++ b/src/lib/test-json-parser.c
@@ -0,0 +1,436 @@
+/* Copyright (c) 2013-2018 Dovecot authors, see the included COPYING file */
+
+#include "test-lib.h"
+#include "str.h"
+#include "istream-private.h"
+#include "json-parser.h"
+
+#define TYPE_SKIP 100
+#define TYPE_STREAM 101
+
+static const char json_input[] =
+ "{\n"
+ "\t\"key\"\t:\t\t\"string\","
+ " \"key2\" : 1234, \n"
+ "\"key3\":true,"
+ "\"key4\":false,"
+ "\"skip1\": \"jsifjaisfjiasji\","
+ "\"skip2\": { \"x\":{ \"y\":123}, \"z\":[5,[6],{\"k\":0},3]},"
+ "\"key5\":null,"
+ "\"key6\": {},"
+ "\"key7\": {"
+ " \"sub1\":\"value\""
+ "},"
+ "\"key8\": {"
+ " \"sub2\":-12.456,\n"
+ " \"sub3\":12.456e9,\n"
+ " \"sub4\":0.456e-789"
+ "},"
+ "\"key9\": \"foo\\\\\\\"\\b\\f\\n\\r\\t\\u0001\\u10ff\","
+ "\"key10\": \"foo\\\\\\\"\\b\\f\\n\\r\\t\\u0001\\u10ff\","
+ "\"key11\": [],"
+ "\"key12\": [ \"foo\" , 5.24,[true],{\"aobj\":[]}],"
+ "\"key13\": \"\\ud801\\udc37\","
+ "\"key14\": \"\xd8\xb3\xd9\x84\xd8\xa7\xd9\x85\","
+ "\"key15\": \"\\u10000\""
+ "}\n";
+
+static const struct {
+ enum json_type type;
+ const char *value;
+} json_output[] = {
+ { JSON_TYPE_OBJECT_KEY, "key" },
+ { JSON_TYPE_STRING, "string" },
+ { JSON_TYPE_OBJECT_KEY, "key2" },
+ { JSON_TYPE_NUMBER, "1234" },
+ { JSON_TYPE_OBJECT_KEY, "key3" },
+ { JSON_TYPE_TRUE, "true" },
+ { JSON_TYPE_OBJECT_KEY, "key4" },
+ { JSON_TYPE_FALSE, "false" },
+ { JSON_TYPE_OBJECT_KEY, "skip1" },
+ { TYPE_SKIP, NULL },
+ { JSON_TYPE_OBJECT_KEY, "skip2" },
+ { TYPE_SKIP, NULL },
+ { JSON_TYPE_OBJECT_KEY, "key5" },
+ { JSON_TYPE_NULL, NULL },
+ { JSON_TYPE_OBJECT_KEY, "key6" },
+ { JSON_TYPE_OBJECT, NULL },
+ { JSON_TYPE_OBJECT_END, NULL },
+ { JSON_TYPE_OBJECT_KEY, "key7" },
+ { JSON_TYPE_OBJECT, NULL },
+ { JSON_TYPE_OBJECT_KEY, "sub1" },
+ { JSON_TYPE_STRING, "value" },
+ { JSON_TYPE_OBJECT_END, NULL },
+ { JSON_TYPE_OBJECT_KEY, "key8" },
+ { JSON_TYPE_OBJECT, NULL },
+ { JSON_TYPE_OBJECT_KEY, "sub2" },
+ { JSON_TYPE_NUMBER, "-12.456" },
+ { JSON_TYPE_OBJECT_KEY, "sub3" },
+ { JSON_TYPE_NUMBER, "12.456e9" },
+ { JSON_TYPE_OBJECT_KEY, "sub4" },
+ { JSON_TYPE_NUMBER, "0.456e-789" },
+ { JSON_TYPE_OBJECT_END, NULL },
+ { JSON_TYPE_OBJECT_KEY, "key9" },
+ { JSON_TYPE_STRING, "foo\\\"\b\f\n\r\t\001\xe1\x83\xbf" },
+ { JSON_TYPE_OBJECT_KEY, "key10" },
+ { TYPE_STREAM, "foo\\\"\b\f\n\r\t\001\xe1\x83\xbf" },
+ { JSON_TYPE_OBJECT_KEY, "key11" },
+ { JSON_TYPE_ARRAY, NULL },
+ { JSON_TYPE_ARRAY_END, NULL },
+ { JSON_TYPE_OBJECT_KEY, "key12" },
+ { JSON_TYPE_ARRAY, NULL },
+ { JSON_TYPE_STRING, "foo" },
+ { JSON_TYPE_NUMBER, "5.24" },
+ { JSON_TYPE_ARRAY, NULL },
+ { JSON_TYPE_TRUE, "true" },
+ { JSON_TYPE_ARRAY_END, NULL },
+ { JSON_TYPE_OBJECT, NULL },
+ { JSON_TYPE_OBJECT_KEY, "aobj" },
+ { JSON_TYPE_ARRAY, NULL },
+ { JSON_TYPE_ARRAY_END, NULL },
+ { JSON_TYPE_OBJECT_END, NULL },
+ { JSON_TYPE_ARRAY_END, NULL },
+ { JSON_TYPE_OBJECT_KEY, "key13" },
+ { JSON_TYPE_STRING, "\xf0\x90\x90\xb7" },
+ { JSON_TYPE_OBJECT_KEY, "key14" },
+ { JSON_TYPE_STRING, "\xd8\xb3\xd9\x84\xd8\xa7\xd9\x85" },
+ { JSON_TYPE_OBJECT_KEY, "key15" },
+ { JSON_TYPE_STRING, "\xe1\x80\x80""0" },
+};
+
+static int
+stream_read_value(struct istream **input, const char **value_r)
+{
+ const unsigned char *data;
+ size_t size;
+ ssize_t ret;
+
+ while ((ret = i_stream_read(*input)) > 0) ;
+ if (ret == 0)
+ return 0;
+ i_assert(ret == -1);
+ if ((*input)->stream_errno != 0)
+ return -1;
+
+ data = i_stream_get_data(*input, &size);
+ *value_r = t_strndup(data, size);
+ i_stream_unref(input);
+ return 1;
+}
+
+static void test_json_parser_success(bool full_size)
+{
+ struct json_parser *parser;
+ struct istream *input, *jsoninput = NULL;
+ enum json_type type;
+ const char *value, *error;
+ unsigned int i, pos, json_input_len = strlen(json_input);
+ int ret = 0;
+
+ test_begin(full_size ? "json parser" : "json parser (nonblocking)");
+ input = test_istream_create_data(json_input, json_input_len);
+ test_istream_set_allow_eof(input, FALSE);
+ parser = json_parser_init(input);
+
+ i = full_size ? json_input_len : 0;
+ for (pos = 0; i <= json_input_len; i++) {
+ test_istream_set_size(input, i);
+
+ for (;;) {
+ value = NULL;
+ if (pos < N_ELEMENTS(json_output) &&
+ json_output[pos].type == (enum json_type)TYPE_SKIP) {
+ json_parse_skip_next(parser);
+ pos++;
+ continue;
+ } else if (pos == N_ELEMENTS(json_output) ||
+ json_output[pos].type != (enum json_type)TYPE_STREAM) {
+ ret = json_parse_next(parser, &type, &value);
+ } else {
+ ret = jsoninput != NULL ? 1 :
+ json_parse_next_stream(parser, &jsoninput);
+ if (ret > 0 && jsoninput != NULL)
+ ret = stream_read_value(&jsoninput, &value);
+ type = TYPE_STREAM;
+ }
+ if (ret <= 0)
+ break;
+
+ i_assert(pos < N_ELEMENTS(json_output));
+ test_assert_idx(json_output[pos].type == type, pos);
+ test_assert_idx(null_strcmp(json_output[pos].value, value) == 0, pos);
+
+ pos++;
+ }
+ test_assert_idx(ret == 0, pos);
+ }
+ test_assert(pos == N_ELEMENTS(json_output));
+ test_istream_set_allow_eof(input, TRUE);
+ test_assert(json_parse_next(parser, &type, &value) == -1);
+
+ i_stream_unref(&input);
+ test_assert(json_parser_deinit(&parser, &error) == 0);
+ test_end();
+}
+
+static void test_json_parser_skip_array(void)
+{
+ static const char *test_input =
+ "[ 1, {\"foo\": 1 }, 2, \"bar\", 3, 1.234, 4, [], 5, [[]], 6, true ]";
+ struct json_parser *parser;
+ struct istream *input;
+ enum json_type type;
+ const char *value, *error;
+ int i;
+
+ test_begin("json parser skip array");
+
+ input = test_istream_create_data(test_input, strlen(test_input));
+ parser = json_parser_init_flags(input, JSON_PARSER_NO_ROOT_OBJECT);
+ test_assert(json_parse_next(parser, &type, &value) > 0 &&
+ type == JSON_TYPE_ARRAY);
+ for (i = 1; i <= 6; i++) {
+ test_assert(json_parse_next(parser, &type, &value) > 0 &&
+ type == JSON_TYPE_NUMBER && atoi(value) == i);
+ json_parse_skip_next(parser);
+ }
+ test_assert(json_parse_next(parser, &type, &value) > 0 &&
+ type == JSON_TYPE_ARRAY_END);
+ test_assert(json_parser_deinit(&parser, &error) == 0);
+ i_stream_unref(&input);
+ test_end();
+}
+
+static void test_json_parser_skip_object_fields(void)
+{
+ static const char *test_input =
+ "{\"access_token\":\"9a2dea3c-f8be-4271-b9c8-5b37da4f2f7e\","
+ "\"grant_type\":\"authorization_code\","
+ "\"openid\":\"\","
+ "\"scope\":[\"openid\",\"profile\",\"email\"],"
+ "\"profile\":\"\","
+ "\"realm\":\"/employees\","
+ "\"token_type\":\"Bearer\","
+ "\"expires_in\":2377,"
+ "\"client_i\\u0064\":\"mosaic\\u0064\","
+ "\"email\":\"\","
+ "\"extensions\":"
+ "{\"algorithm\":\"cuttlefish\","
+ "\"tentacles\":8"
+ "}"
+ "}";
+ static const char *const keys[] = {
+ "access_token", "grant_type", "openid", "scope", "profile",
+ "realm", "token_type", "expires_in", "client_id", "email",
+ "extensions"
+ };
+ static const unsigned int keys_count = N_ELEMENTS(keys);
+ struct json_parser *parser;
+ struct istream *input;
+ enum json_type type;
+ const char *value, *error;
+ unsigned int i;
+ size_t pos;
+ int ret;
+
+ test_begin("json parser skip object fields (by key)");
+ input = test_istream_create_data(test_input, strlen(test_input));
+ parser = json_parser_init(input);
+ for (i = 0; i < keys_count; i++) {
+ ret = json_parse_next(parser, &type, &value);
+ if (ret < 0)
+ break;
+ test_assert(ret > 0 && type == JSON_TYPE_OBJECT_KEY);
+ test_assert(strcmp(value, keys[i]) == 0);
+ json_parse_skip_next(parser);
+ }
+ test_assert(i == keys_count);
+ test_assert(json_parser_deinit(&parser, &error) == 0);
+ i_stream_unref(&input);
+
+ i = 0;
+ input = test_istream_create_data(test_input, strlen(test_input));
+ parser = json_parser_init(input);
+ for (pos = 0; pos <= strlen(test_input)*2; pos++) {
+ test_istream_set_size(input, pos/2);
+ ret = json_parse_next(parser, &type, &value);
+ if (ret == 0)
+ continue;
+ if (ret < 0)
+ break;
+ i_assert(i < keys_count);
+ test_assert(ret > 0 && type == JSON_TYPE_OBJECT_KEY);
+ test_assert(strcmp(value, keys[i]) == 0);
+ json_parse_skip_next(parser);
+ i++;
+ }
+ test_assert(i == keys_count);
+ test_assert(json_parser_deinit(&parser, &error) == 0);
+ i_stream_unref(&input);
+ test_end();
+
+ test_begin("json parser skip object fields (by value type)");
+ input = test_istream_create_data(test_input, strlen(test_input));
+ parser = json_parser_init(input);
+ for (i = 0; i < keys_count; i++) {
+ ret = json_parse_next(parser, &type, &value);
+ if (ret < 0)
+ break;
+ test_assert(ret > 0 && type == JSON_TYPE_OBJECT_KEY);
+ test_assert(strcmp(value, keys[i]) == 0);
+ ret = json_parse_next(parser, &type, &value);
+ test_assert(ret > 0 && type != JSON_TYPE_OBJECT_KEY);
+ json_parse_skip(parser);
+ }
+ test_assert(i == keys_count);
+ test_assert(json_parser_deinit(&parser, &error) == 0);
+ i_stream_unref(&input);
+
+ i = 0;
+ input = test_istream_create_data(test_input, strlen(test_input));
+ parser = json_parser_init(input);
+ for (pos = 0; pos <= strlen(test_input)*2; pos++) {
+ test_istream_set_size(input, pos/2);
+ ret = json_parse_next(parser, &type, &value);
+ if (ret < 0)
+ break;
+ if (ret == 0)
+ continue;
+ test_assert(ret > 0);
+ if (type == JSON_TYPE_OBJECT_KEY) {
+ i_assert(i < keys_count);
+ test_assert(strcmp(value, keys[i]) == 0);
+ i++;
+ } else {
+ json_parse_skip(parser);
+ }
+ }
+ test_assert(i == keys_count);
+ test_assert(json_parser_deinit(&parser, &error) == 0);
+ i_stream_unref(&input);
+
+ test_end();
+}
+
+static int
+test_json_parse_input(const void *test_input, size_t test_input_size,
+ enum json_parser_flags flags)
+{
+ struct json_parser *parser;
+ struct istream *input;
+ enum json_type type;
+ const char *value, *error;
+ int ret = 0;
+
+ input = test_istream_create_data(test_input, test_input_size);
+ parser = json_parser_init_flags(input, flags);
+ while (json_parse_next(parser, &type, &value) > 0)
+ ret++;
+ if (json_parser_deinit(&parser, &error) < 0)
+ ret = -1;
+ i_stream_unref(&input);
+ return ret;
+}
+
+static void test_json_parser_primitive_values(void)
+{
+ static const struct {
+ const char *str;
+ int ret;
+ } test_inputs[] = {
+ { "\"hello\"", 1 },
+ { "null", 1 },
+ { "1234", 1 },
+ { "1234.1234", 1 },
+ { "{}", 2 },
+ { "[]", 2 },
+ { "true", 1 },
+ { "false", 1 }
+ };
+ unsigned int i;
+
+ test_begin("json_parser (primitives)");
+ for (i = 0; i < N_ELEMENTS(test_inputs); i++)
+ test_assert_idx(test_json_parse_input(test_inputs[i].str,
+ strlen(test_inputs[i].str),
+ JSON_PARSER_NO_ROOT_OBJECT) == test_inputs[i].ret, i);
+ test_end();
+}
+
+static void test_json_parser_errors(void)
+{
+ static const char *test_inputs[] = {
+ "{",
+ "{:}",
+ "{\"foo\":}",
+ "{\"foo\" []}",
+ "{\"foo\": [1}",
+ "{\"foo\": [1,]}",
+ "{\"foo\": [1,]}",
+ "{\"foo\": 1,}",
+ "{\"foo\": 1.}}",
+ "{\"foo\": 1},{}",
+ "{\"foo\": \"\\ud808\"}",
+ "{\"foo\": \"\\udfff\"}",
+ "{\"foo\": \"\\uyyyy\"}",
+ };
+ unsigned int i;
+
+ test_begin("json parser error handling");
+ for (i = 0; i < N_ELEMENTS(test_inputs); i++)
+ test_assert_idx(test_json_parse_input(test_inputs[i],
+ strlen(test_inputs[i]),
+ 0) < 0, i);
+ test_end();
+}
+
+static void test_json_parser_nuls_in_string(void)
+{
+ static const unsigned char test_input[] =
+ { '{', '"', 'k', '"', ':', '"', '\0', '"', '}' };
+ static const unsigned char test_input2[] =
+ { '{', '"', 'k', '"', ':', '"', '\\', '\0', '"', '}' };
+ static const unsigned char test_input3[] =
+ { '{', '"', 'k', '"', ':', '"', '\\', 'u', '0', '0', '0', '0', '"', '}' };
+
+ test_begin("json parser nuls in string");
+ test_assert(test_json_parse_input(test_input, sizeof(test_input), 0) < 0);
+ test_assert(test_json_parse_input(test_input2, sizeof(test_input2), 0) < 0);
+ test_assert(test_json_parse_input(test_input3, sizeof(test_input3), 0) < 0);
+ test_end();
+}
+
+static void test_json_append_escaped(void)
+{
+ string_t *str = t_str_new(32);
+
+ test_begin("json_append_escaped()");
+ json_append_escaped(str, "\b\f\r\n\t\"\\\001\002-\xC3\xA4\xf0\x90\x90\xb7\xe2\x80\xa8\xe2\x80\xa9\xff");
+ test_assert(strcmp(str_c(str), "\\b\\f\\r\\n\\t\\\"\\\\\\u0001\\u0002-\xC3\xA4\xf0\x90\x90\xb7\\u2028\\u2029" UNICODE_REPLACEMENT_CHAR_UTF8) == 0);
+ test_end();
+}
+
+static void test_json_append_escaped_data(void)
+{
+ static const unsigned char test_input[] =
+ "\b\f\r\n\t\"\\\000\001\002-\xC3\xA4\xf0\x90\x90\xb7\xe2\x80\xa8\xe2\x80\xa9\xff";
+ string_t *str = t_str_new(32);
+
+ test_begin("json_append_escaped_data()");
+ json_append_escaped_data(str, test_input, sizeof(test_input)-1);
+ test_assert(strcmp(str_c(str), "\\b\\f\\r\\n\\t\\\"\\\\\\u0000\\u0001\\u0002-\xC3\xA4\xf0\x90\x90\xb7\\u2028\\u2029" UNICODE_REPLACEMENT_CHAR_UTF8) == 0);
+ test_end();
+}
+
+void test_json_parser(void)
+{
+ test_json_parser_success(TRUE);
+ test_json_parser_success(FALSE);
+ test_json_parser_skip_array();
+ test_json_parser_skip_object_fields();
+ test_json_parser_primitive_values();
+ test_json_parser_errors();
+ test_json_parser_nuls_in_string();
+ test_json_append_escaped();
+ test_json_append_escaped_data();
+}
diff --git a/src/lib/test-json-tree.c b/src/lib/test-json-tree.c
new file mode 100644
index 0000000..40eff8c
--- /dev/null
+++ b/src/lib/test-json-tree.c
@@ -0,0 +1,113 @@
+/* Copyright (c) 2015-2018 Dovecot authors, see the included COPYING file */
+
+#include "test-lib.h"
+#include "json-tree.h"
+
+struct {
+ enum json_type type;
+ const char *value;
+} test_input[] = {
+ { JSON_TYPE_OBJECT_KEY, "key-str" },
+ { JSON_TYPE_STRING, "string" },
+ { JSON_TYPE_OBJECT_KEY, "key-num" },
+ { JSON_TYPE_NUMBER, "1234" },
+ { JSON_TYPE_OBJECT_KEY, "key-true" },
+ { JSON_TYPE_TRUE, "true" },
+ { JSON_TYPE_OBJECT_KEY, "key-false" },
+ { JSON_TYPE_FALSE, "false" },
+ { JSON_TYPE_OBJECT_KEY, "key-null" },
+ { JSON_TYPE_NULL, NULL },
+
+ { JSON_TYPE_OBJECT_KEY, "key-obj-empty" },
+ { JSON_TYPE_OBJECT, NULL },
+ { JSON_TYPE_OBJECT_END, NULL },
+
+ { JSON_TYPE_OBJECT_KEY, "key-obj" },
+ { JSON_TYPE_OBJECT, NULL },
+ { JSON_TYPE_OBJECT_KEY, "sub" },
+ { JSON_TYPE_STRING, "value" },
+ { JSON_TYPE_OBJECT_END, NULL },
+
+ { JSON_TYPE_OBJECT_KEY, "key-arr-empty" },
+ { JSON_TYPE_ARRAY, NULL },
+ { JSON_TYPE_ARRAY_END, NULL },
+
+ { JSON_TYPE_OBJECT_KEY, "key-arr" },
+ { JSON_TYPE_ARRAY, NULL },
+ { JSON_TYPE_STRING, "foo" },
+ { JSON_TYPE_ARRAY, NULL },
+ { JSON_TYPE_TRUE, "true" },
+ { JSON_TYPE_ARRAY_END, NULL },
+ { JSON_TYPE_OBJECT, NULL },
+ { JSON_TYPE_OBJECT_KEY, "aobj" },
+ { JSON_TYPE_ARRAY, NULL },
+ { JSON_TYPE_ARRAY_END, NULL },
+ { JSON_TYPE_OBJECT_END, NULL },
+ { JSON_TYPE_OBJECT, NULL },
+ { JSON_TYPE_OBJECT_KEY, "aobj-key" },
+ { JSON_TYPE_STRING, "value1" },
+ { JSON_TYPE_OBJECT_END, NULL },
+ { JSON_TYPE_OBJECT, NULL },
+ { JSON_TYPE_OBJECT_KEY, "aobj-key" },
+ { JSON_TYPE_STRING, "value2" },
+ { JSON_TYPE_OBJECT_END, NULL },
+ { JSON_TYPE_ARRAY_END, NULL }
+};
+
+void test_json_tree(void)
+{
+ struct json_tree *tree;
+ const struct json_tree_node *root, *node, *node1, *node2;
+ unsigned int i;
+
+ test_begin("json tree");
+ tree = json_tree_init();
+ for (i = 0; i < N_ELEMENTS(test_input); i++) {
+ test_assert_idx(json_tree_append(tree, test_input[i].type,
+ test_input[i].value) == 0, i);
+ }
+
+ root = json_tree_root(tree);
+ i_assert(root != NULL);
+ test_assert(root->value_type == JSON_TYPE_OBJECT);
+ i_assert(root != NULL);
+
+ for (i = 0; i < 10+2; i += 2) {
+ node = json_tree_find_key(root, test_input[i].value);
+ test_assert(node != NULL &&
+ node->value_type == test_input[i+1].type &&
+ null_strcmp(json_tree_get_value_str(node), test_input[i+1].value) == 0);
+ }
+
+ node = json_tree_find_key(root, "key-obj");
+ test_assert(node != NULL);
+
+ node = json_tree_find_key(root, "key-arr-empty");
+ test_assert(node != NULL && node->value_type == JSON_TYPE_ARRAY &&
+ json_tree_get_child(node) == NULL);
+
+ node = json_tree_find_key(root, "key-arr");
+ test_assert(node != NULL && node->value_type == JSON_TYPE_ARRAY);
+ node = json_tree_get_child(node);
+ test_assert(node != NULL && node->value_type == JSON_TYPE_STRING &&
+ strcmp(json_tree_get_value_str(node), "foo") == 0);
+ node = node->next;
+ test_assert(node != NULL && node->value_type == JSON_TYPE_ARRAY &&
+ json_tree_get_child(node) != NULL &&
+ json_tree_get_child(node)->next == NULL &&
+ json_tree_get_child(node)->value_type == JSON_TYPE_TRUE);
+ node = node->next;
+ test_assert(node != NULL && node->value_type == JSON_TYPE_OBJECT &&
+ json_tree_get_child(node) != NULL &&
+ json_tree_get_child(node)->next == NULL &&
+ json_tree_get_child(node)->value_type == JSON_TYPE_ARRAY &&
+ json_tree_get_child(json_tree_get_child(node)) == NULL);
+
+ node1 = json_tree_find_child_with(node->parent, "aobj-key", "value1");
+ node2 = json_tree_find_child_with(node->parent, "aobj-key", "value2");
+ test_assert(node1 != NULL && node2 != NULL && node1 != node2);
+ test_assert(json_tree_find_child_with(node->parent, "aobj-key", "value3") == NULL);
+
+ json_tree_deinit(&tree);
+ test_end();
+}
diff --git a/src/lib/test-lib-event.c b/src/lib/test-lib-event.c
new file mode 100644
index 0000000..cfc4106
--- /dev/null
+++ b/src/lib/test-lib-event.c
@@ -0,0 +1,96 @@
+/* Copyright (c) 2021 Dovecot authors, see the included COPYING file */
+
+#include "test-lib.h"
+
+static void test_event_strlist(void)
+{
+ test_begin("event strlist");
+ struct event *e1 = event_create(NULL);
+ event_strlist_append(e1, "key", "s1");
+ event_strlist_append(e1, "key", "s2");
+ struct event *e2 = event_create(e1);
+ event_strlist_append(e2, "key", "s3");
+ event_strlist_append(e2, "key", "s2");
+
+ test_assert_strcmp(event_find_field_recursive_str(e1, "key"), "s1,s2");
+ test_assert_strcmp(event_find_field_recursive_str(e2, "key"), "s3,s2,s1");
+
+ const char *new_strlist[] = { "new1", "new2", "new2", "s2" };
+ event_strlist_replace(e2, "key", new_strlist, N_ELEMENTS(new_strlist));
+ test_assert_strcmp(event_find_field_recursive_str(e2, "key"), "new1,new2,s2,s1");
+
+ struct event *e3 = event_create(NULL);
+ event_strlist_copy_recursive(e3, e2, "key");
+ test_assert_strcmp(event_find_field_recursive_str(e3, "key"), "new1,new2,s2,s1");
+ event_unref(&e3);
+
+ event_unref(&e1);
+ event_unref(&e2);
+ test_end();
+}
+
+static void test_lib_event_reason_code(void)
+{
+ test_begin("event reason codes");
+ test_assert_strcmp(event_reason_code("foo", "bar"), "foo:bar");
+ test_assert_strcmp(event_reason_code("foo", "B A-r"), "foo:b_a_r");
+ test_assert_strcmp(event_reason_code_prefix("foo", "x", "bar"), "foo:xbar");
+ test_assert_strcmp(event_reason_code_prefix("foo", "", "bar"), "foo:bar");
+ test_end();
+}
+
+void test_lib_event(void)
+{
+ test_event_strlist();
+ test_lib_event_reason_code();
+}
+
+enum fatal_test_state fatal_lib_event(unsigned int stage)
+{
+ switch (stage) {
+ case 0:
+ test_begin("event reason codes - asserts");
+ /* module: uppercase */
+ test_expect_fatal_string("Invalid module");
+ (void)event_reason_code("FOO", "bar");
+ return FATAL_TEST_FAILURE;
+ case 1:
+ /* module: space */
+ test_expect_fatal_string("Invalid module");
+ (void)event_reason_code("f oo", "bar");
+ return FATAL_TEST_FAILURE;
+ case 2:
+ /* module: - */
+ test_expect_fatal_string("Invalid module");
+ (void)event_reason_code("f-oo", "bar");
+ return FATAL_TEST_FAILURE;
+ case 3:
+ /* module: empty */
+ test_expect_fatal_string("module[0] != '\\0'");
+ (void)event_reason_code("", "bar");
+ return FATAL_TEST_FAILURE;
+ case 4:
+ /* name_prefix: uppercase */
+ test_expect_fatal_string("Invalid name_prefix");
+ (void)event_reason_code_prefix("module", "FOO", "bar");
+ return FATAL_TEST_FAILURE;
+ case 5:
+ /* name_prefix: space */
+ test_expect_fatal_string("Invalid name_prefix");
+ (void)event_reason_code_prefix("module", "f oo", "bar");
+ return FATAL_TEST_FAILURE;
+ case 6:
+ /* name_prefix: - */
+ test_expect_fatal_string("Invalid name_prefix");
+ (void)event_reason_code_prefix("module", "f-oo", "bar");
+ return FATAL_TEST_FAILURE;
+ case 7:
+ /* name: empty */
+ test_expect_fatal_string("(name[0] != '\\0')");
+ (void)event_reason_code("foo:", "");
+ return FATAL_TEST_FAILURE;
+ default:
+ test_end();
+ return FATAL_TEST_FINISHED;
+ }
+}
diff --git a/src/lib/test-lib-signals.c b/src/lib/test-lib-signals.c
new file mode 100644
index 0000000..75d324a
--- /dev/null
+++ b/src/lib/test-lib-signals.c
@@ -0,0 +1,245 @@
+/* Copyright (c) 2015-2018 Dovecot authors, see the included COPYING file */
+
+#include "test-lib.h"
+#include "time-util.h"
+#include "ioloop.h"
+#include "lib-signals.h"
+
+#include <unistd.h>
+#include <sys/types.h>
+
+struct test_context_delayed {
+ bool timed_out:1;
+ bool signal_handled:1;
+};
+
+static void
+kill_timeout(struct test_context_delayed *tctx ATTR_UNUSED)
+{
+ if (kill(getpid(), SIGALRM) < 0)
+ i_fatal("Failed to send signal: %m");
+}
+
+static void
+test_timeout(struct test_context_delayed *tctx)
+{
+ tctx->timed_out = TRUE;
+ io_loop_stop(current_ioloop);
+}
+
+static void
+signal_handler_delayed(const siginfo_t *si ATTR_UNUSED,
+ void *context ATTR_UNUSED)
+{
+ struct test_context_delayed *tctx =
+ (struct test_context_delayed *)context;
+ tctx->signal_handled = TRUE;
+ io_loop_stop(current_ioloop);
+}
+
+static void
+test_lib_signals_delayed(void)
+{
+ struct test_context_delayed tctx;
+ struct timeout *to_kill, *to_test;
+ struct ioloop *ioloop;
+
+ test_begin("lib-signals delayed - init lib-signals first");
+
+ i_zero(&tctx);
+
+ lib_signals_init();
+ lib_signals_set_handler(SIGALRM,
+ LIBSIG_FLAGS_SAFE | LIBSIG_FLAG_IOLOOP_AUTOMOVE,
+ signal_handler_delayed, &tctx);
+
+ ioloop = io_loop_create();
+ to_kill = timeout_add_short(200, kill_timeout, &tctx);
+ to_test = timeout_add_short(400, test_timeout, &tctx);
+ io_loop_run(ioloop);
+
+ timeout_remove(&to_kill);
+ timeout_remove(&to_test);
+ io_loop_destroy(&ioloop);
+
+ lib_signals_deinit();
+
+ test_assert(!tctx.timed_out);
+ test_assert(tctx.signal_handled);
+
+ test_end();
+
+ test_begin("lib-signals delayed - init ioloop first");
+
+ i_zero(&tctx);
+
+ ioloop = io_loop_create();
+
+ lib_signals_init();
+ lib_signals_set_handler(SIGALRM,
+ LIBSIG_FLAGS_SAFE | LIBSIG_FLAG_IOLOOP_AUTOMOVE,
+ signal_handler_delayed, &tctx);
+
+ to_kill = timeout_add_short(200, kill_timeout, &tctx);
+ to_test = timeout_add_short(400, test_timeout, &tctx);
+ io_loop_run(ioloop);
+
+ timeout_remove(&to_kill);
+ timeout_remove(&to_test);
+
+ lib_signals_deinit();
+
+ io_loop_destroy(&ioloop);
+
+ test_assert(!tctx.timed_out);
+ test_assert(tctx.signal_handled);
+
+ test_end();
+
+}
+
+static void
+test_lib_signals_delayed_nested_ioloop(void)
+{
+ struct test_context_delayed tctx;
+ struct timeout *to_kill, *to_test;
+ struct ioloop *ioloop1, *ioloop2;
+
+ test_begin("lib-signals delayed in nested ioloop");
+
+ i_zero(&tctx);
+
+ lib_signals_init();
+ lib_signals_set_handler(SIGALRM,
+ LIBSIG_FLAGS_SAFE | LIBSIG_FLAG_IOLOOP_AUTOMOVE,
+ signal_handler_delayed, &tctx);
+
+ /* briefly run outer ioloop */
+ ioloop1 = io_loop_create();
+ to_test = timeout_add_short(100, test_timeout, &tctx);
+ io_loop_run(ioloop1);
+ timeout_remove(&to_test);
+ test_assert(tctx.timed_out);
+ test_assert(!tctx.signal_handled);
+ tctx.timed_out = FALSE;
+
+ /* run inner ioloop, which triggers the signal */
+ ioloop2 = io_loop_create();
+ to_kill = timeout_add_short(200, kill_timeout, &tctx);
+ to_test = timeout_add_short(400, test_timeout, &tctx);
+ io_loop_run(ioloop2);
+
+ timeout_remove(&to_kill);
+ timeout_remove(&to_test);
+ io_loop_destroy(&ioloop2);
+
+ io_loop_destroy(&ioloop1);
+
+ lib_signals_deinit();
+
+ test_assert(!tctx.timed_out);
+ test_assert(tctx.signal_handled);
+
+ test_end();
+}
+
+static void
+test_lib_signals_delayed_no_ioloop_automove(void)
+{
+ struct test_context_delayed tctx;
+ struct timeout *to_kill, *to_test;
+ struct ioloop *ioloop1, *ioloop2;
+
+ test_begin("lib-signals delayed with NO_IOLOOP_AUTOMOVE - unmoved");
+
+ i_zero(&tctx);
+
+ ioloop1 = io_loop_create();
+
+ lib_signals_init();
+ lib_signals_set_handler(SIGALRM, LIBSIG_FLAGS_SAFE,
+ signal_handler_delayed, &tctx);
+
+ /* briefly run outer ioloop */
+ to_test = timeout_add_short(100, test_timeout, &tctx);
+ io_loop_run(ioloop1);
+ timeout_remove(&to_test);
+ test_assert(tctx.timed_out);
+ test_assert(!tctx.signal_handled);
+ tctx.timed_out = FALSE;
+
+ /* run inner ioloop, which triggers the signal but musn't handle it */
+ ioloop2 = io_loop_create();
+ to_kill = timeout_add_short(200, kill_timeout, &tctx);
+ to_test = timeout_add_short(400, test_timeout, &tctx);
+ io_loop_run(ioloop2);
+
+ test_assert(tctx.timed_out);
+ test_assert(!tctx.signal_handled);
+ tctx.timed_out = FALSE;
+
+ timeout_remove(&to_kill);
+ timeout_remove(&to_test);
+ io_loop_destroy(&ioloop2);
+
+ /* run outer ioloop once more */
+ to_test = timeout_add_short(100, test_timeout, &tctx);
+ io_loop_run(ioloop1);
+ timeout_remove(&to_test);
+
+ lib_signals_deinit();
+
+ io_loop_destroy(&ioloop1);
+
+ test_assert(!tctx.timed_out);
+ test_assert(tctx.signal_handled);
+
+ test_end();
+
+ test_begin("lib-signals delayed with NO_IOLOOP_AUTOMOVE - moved");
+
+ i_zero(&tctx);
+
+ ioloop1 = io_loop_create();
+
+ lib_signals_init();
+ lib_signals_set_handler(SIGALRM, LIBSIG_FLAGS_SAFE,
+ signal_handler_delayed, &tctx);
+
+ /* briefly run outer ioloop */
+ to_test = timeout_add_short(100, test_timeout, &tctx);
+ io_loop_run(ioloop1);
+ timeout_remove(&to_test);
+ test_assert(tctx.timed_out);
+ test_assert(!tctx.signal_handled);
+ tctx.timed_out = FALSE;
+
+ /* run inner ioloop, which triggers the signal */
+ ioloop2 = io_loop_create();
+ lib_signals_switch_ioloop(SIGALRM,
+ signal_handler_delayed, &tctx);
+
+ to_kill = timeout_add_short(200, kill_timeout, &tctx);
+ to_test = timeout_add_short(400, test_timeout, &tctx);
+ io_loop_run(ioloop2);
+
+ test_assert(!tctx.timed_out);
+ test_assert(tctx.signal_handled);
+
+ timeout_remove(&to_kill);
+ timeout_remove(&to_test);
+ io_loop_destroy(&ioloop2);
+
+ lib_signals_deinit();
+ io_loop_destroy(&ioloop1);
+
+ test_end();
+}
+
+
+void test_lib_signals(void)
+{
+ test_lib_signals_delayed();
+ test_lib_signals_delayed_nested_ioloop();
+ test_lib_signals_delayed_no_ioloop_automove();
+}
diff --git a/src/lib/test-lib.c b/src/lib/test-lib.c
new file mode 100644
index 0000000..62e6b0d
--- /dev/null
+++ b/src/lib/test-lib.c
@@ -0,0 +1,28 @@
+/* Copyright (c) 2007-2018 Dovecot authors, see the included COPYING file */
+
+#include "test-lib.h"
+
+int main(int argc, char **argv)
+{
+ const char *match = "";
+ if (argc > 2 && strcmp(argv[1], "--match") == 0)
+ match = argv[2];
+
+ static const struct named_test test_functions[] = {
+#define TEST(x) TEST_NAMED(x)
+#define FATAL(x)
+#include "test-lib.inc"
+#undef TEST
+#undef FATAL
+ { NULL, NULL }
+ };
+ static const struct named_fatal fatal_functions[] = {
+#define TEST(x)
+#define FATAL(x) FATAL_NAMED(x)
+#include "test-lib.inc"
+#undef TEST
+#undef FATAL
+ { NULL, NULL }
+ };
+ return test_run_named_with_fatals(match, test_functions, fatal_functions);
+}
diff --git a/src/lib/test-lib.h b/src/lib/test-lib.h
new file mode 100644
index 0000000..7fdd105
--- /dev/null
+++ b/src/lib/test-lib.h
@@ -0,0 +1,13 @@
+#ifndef TEST_LIB
+#define TEST_LIB
+
+#include "lib.h"
+#include "test-common.h"
+
+#define TEST(x) TEST_DECL(x)
+#define FATAL(x) FATAL_DECL(x)
+#include "test-lib.inc"
+#undef TEST
+#undef FATAL
+
+#endif
diff --git a/src/lib/test-lib.inc b/src/lib/test-lib.inc
new file mode 100644
index 0000000..e698708
--- /dev/null
+++ b/src/lib/test-lib.inc
@@ -0,0 +1,112 @@
+/* This file may be multiply-included, with different definitions of
+ 'TEST()' macro. This is sometimes called "the X trick" (as the
+ macro is often imaginatively called X(). */
+
+TEST(test_aqueue)
+TEST(test_array)
+FATAL(fatal_array)
+TEST(test_backtrace)
+TEST(test_base32)
+TEST(test_base64)
+TEST(test_bits)
+TEST(test_bsearch_insert_pos)
+TEST(test_buffer)
+TEST(test_buffer_append_full)
+FATAL(fatal_buffer)
+TEST(test_byteorder)
+TEST(test_connection)
+TEST(test_crc32)
+TEST(test_cpu_limit)
+TEST(test_data_stack)
+FATAL(fatal_data_stack)
+TEST(test_env_util)
+FATAL(fatal_env_util)
+TEST(test_event_category_register)
+FATAL(fatal_event_category_register)
+TEST(test_lib_event)
+FATAL(fatal_lib_event)
+TEST(test_event_filter)
+TEST(test_event_filter_expr)
+TEST(test_event_filter_merge)
+TEST(test_event_filter_parser)
+TEST(test_event_flatten)
+TEST(test_event_log)
+TEST(test_failures)
+TEST(test_file_cache)
+TEST(test_file_create_locked)
+TEST(test_guid)
+TEST(test_hash)
+TEST(test_hash_format)
+TEST(test_hash_method)
+TEST(test_hmac)
+TEST(test_hex_binary)
+FATAL(fatal_i_close)
+TEST(test_imem)
+TEST(test_ioloop)
+TEST(test_iso8601_date)
+TEST(test_iostream_pump)
+TEST(test_iostream_proxy)
+TEST(test_iostream_temp)
+TEST(test_istream)
+TEST(test_istream_base64_decoder)
+TEST(test_istream_base64_encoder)
+TEST(test_istream_chain)
+TEST(test_istream_concat)
+TEST(test_istream_crlf)
+TEST(test_istream_failure_at)
+TEST(test_istream_jsonstr)
+TEST(test_istream_multiplex)
+TEST(test_istream_seekable)
+TEST(test_istream_sized)
+TEST(test_istream_tee)
+TEST(test_istream_try)
+TEST(test_istream_unix)
+TEST(test_json_parser)
+TEST(test_json_tree)
+TEST(test_lib_event)
+TEST(test_lib_signals)
+TEST(test_llist)
+TEST(test_log_throttle)
+TEST(test_macros)
+TEST(test_malloc_overflow)
+FATAL(fatal_malloc_overflow)
+TEST(test_memarea)
+TEST(test_mempool)
+FATAL(fatal_mempool)
+TEST(test_mempool_alloconly)
+FATAL(fatal_mempool_alloconly)
+TEST(test_mempool_allocfree)
+FATAL(fatal_mempool_allocfree)
+TEST(test_net)
+TEST(test_numpack)
+TEST(test_ostream_buffer)
+TEST(test_ostream_failure_at)
+TEST(test_ostream_file)
+TEST(test_ostream_multiplex)
+TEST(test_multiplex)
+TEST(test_path_util)
+TEST(test_pkcs5_pbkdf2)
+TEST(test_primes)
+TEST(test_printf_format_fix)
+FATAL(fatal_printf_format_fix)
+TEST(test_priorityq)
+TEST(test_random)
+FATAL(fatal_random)
+TEST(test_seq_range_array)
+FATAL(fatal_seq_range_array)
+TEST(test_seq_set_builder)
+TEST(test_stats_dist)
+TEST(test_str)
+TEST(test_strescape)
+TEST(test_strfuncs)
+FATAL(fatal_strfuncs)
+TEST(test_strnum)
+TEST(test_str_find)
+TEST(test_str_sanitize)
+TEST(test_str_table)
+TEST(test_time_util)
+TEST(test_unichar)
+TEST(test_uri)
+TEST(test_utc_mktime)
+TEST(test_var_expand)
+TEST(test_wildcard_match)
diff --git a/src/lib/test-llist.c b/src/lib/test-llist.c
new file mode 100644
index 0000000..d57006c
--- /dev/null
+++ b/src/lib/test-llist.c
@@ -0,0 +1,138 @@
+/* Copyright (c) 2009-2018 Dovecot authors, see the included COPYING file */
+
+#include "test-lib.h"
+#include "llist.h"
+
+
+struct dllist {
+ struct dllist *prev, *next;
+};
+
+static void test_dllist(void)
+{
+ struct dllist *head = NULL, *l4, *l3, *l2, *l1;
+ struct dllist empty = { NULL, NULL };
+
+ l4 = t_new(struct dllist, 1);
+ l3 = t_new(struct dllist, 1);
+ l2 = t_new(struct dllist, 1);
+ l1 = t_new(struct dllist, 1);
+
+ test_begin("dllist");
+ DLLIST_PREPEND(&head, l4);
+ test_assert(head == l4);
+ test_assert(l4->prev == NULL && l4->next == NULL);
+ DLLIST_PREPEND(&head, l3);
+ test_assert(head == l3);
+ test_assert(l3->prev == NULL && l3->next == l4);
+ test_assert(l4->prev == l3 && l4->next == NULL);
+ DLLIST_PREPEND(&head, l2);
+ DLLIST_PREPEND(&head, l1);
+ /* remove from middle */
+ DLLIST_REMOVE(&head, l2);
+ test_assert(l2->prev == NULL && l2->next == NULL);
+ test_assert(head == l1);
+ test_assert(l1->prev == NULL && l1->next == l3);
+ test_assert(l3->prev == l1 && l3->next == l4);
+ test_assert(l4->prev == l3 && l4->next == NULL);
+ /* remove from head */
+ DLLIST_REMOVE(&head, l1);
+ test_assert(l1->prev == NULL && l1->next == NULL);
+ test_assert(head == l3);
+ test_assert(l3->prev == NULL && l3->next == l4);
+ test_assert(l4->prev == l3 && l4->next == NULL);
+ /* remove from tail */
+ DLLIST_PREPEND(&head, l1);
+ DLLIST_REMOVE(&head, l4);
+ test_assert(l4->prev == NULL && l4->next == NULL);
+ test_assert(head == l1);
+ test_assert(l1->prev == NULL && l1->next == l3);
+ test_assert(l3->prev == l1 && l3->next == NULL);
+ /* removal of an entry not in the list shouldn't cause the list to break */
+ DLLIST_REMOVE(&head, &empty);
+ test_assert(head == l1);
+ test_assert(l1->prev == NULL && l1->next == l3);
+ test_assert(l3->prev == l1 && l3->next == NULL);
+ /* remove last two */
+ DLLIST_REMOVE(&head, l1);
+ DLLIST_REMOVE(&head, l3);
+ test_assert(l3->prev == NULL && l3->next == NULL);
+ test_assert(head == NULL);
+ test_end();
+}
+
+static void test_dllist2(void)
+{
+ struct dllist *head = NULL, *tail = NULL, *l4, *l3, *l2, *l1;
+ struct dllist empty = { NULL, NULL };
+
+ l4 = t_new(struct dllist, 1);
+ l3 = t_new(struct dllist, 1);
+ l2 = t_new(struct dllist, 1);
+ l1 = t_new(struct dllist, 1);
+
+ test_begin("dllist");
+ /* prepend to empty */
+ DLLIST2_PREPEND(&head, &tail, l3);
+ test_assert(head == l3 && tail == l3);
+ test_assert(l3->next == NULL && l3->prev == NULL);
+ /* remove last */
+ DLLIST2_REMOVE(&head, &tail, l3);
+ test_assert(head == NULL && tail == NULL);
+ test_assert(l3->next == NULL && l3->prev == NULL);
+ /* append to empty */
+ DLLIST2_APPEND(&head, &tail, l3);
+ test_assert(head == l3 && tail == l3);
+ test_assert(l3->next == NULL && l3->prev == NULL);
+ /* prepend */
+ DLLIST2_PREPEND(&head, &tail, l2);
+ test_assert(head == l2 && tail == l3);
+ test_assert(l2->prev == NULL && l2->next == l3);
+ test_assert(l3->prev == l2 && l3->next == NULL);
+ /* append */
+ DLLIST2_APPEND(&head, &tail, l4);
+ test_assert(head == l2 && tail == l4);
+ test_assert(l2->prev == NULL && l2->next == l3);
+ test_assert(l3->prev == l2 && l3->next == l4);
+ test_assert(l4->prev == l3 && l4->next == NULL);
+ DLLIST2_PREPEND(&head, &tail, l1);
+
+ /* remove from middle */
+ DLLIST2_REMOVE(&head, &tail, l2);
+ test_assert(l2->prev == NULL && l2->next == NULL);
+ test_assert(head == l1 && tail == l4);
+ test_assert(l1->prev == NULL && l1->next == l3);
+ test_assert(l3->prev == l1 && l3->next == l4);
+ test_assert(l4->prev == l3 && l4->next == NULL);
+ /* remove from head */
+ DLLIST2_REMOVE(&head, &tail, l1);
+ test_assert(l1->prev == NULL && l1->next == NULL);
+ test_assert(head == l3 && tail == l4);
+ test_assert(l3->prev == NULL && l3->next == l4);
+ test_assert(l4->prev == l3 && l4->next == NULL);
+ /* remove from tail */
+ DLLIST2_PREPEND(&head, &tail, l1);
+ DLLIST2_REMOVE(&head, &tail, l4);
+ test_assert(l4->prev == NULL && l4->next == NULL);
+ test_assert(head == l1 && tail == l3);
+ test_assert(l1->prev == NULL && l1->next == l3);
+ test_assert(l3->prev == l1 && l3->next == NULL);
+ /* removal of an entry not in the list shouldn't cause the list to break */
+ DLLIST2_REMOVE(&head, &tail, &empty);
+ test_assert(head == l1);
+ test_assert(head == l1 && tail == l3);
+ test_assert(l1->prev == NULL && l1->next == l3);
+ test_assert(l3->prev == l1 && l3->next == NULL);
+ /* remove last two */
+ DLLIST2_REMOVE(&head, &tail, l1);
+ DLLIST2_REMOVE(&head, &tail, l3);
+ test_assert(l3->prev == NULL && l3->next == NULL);
+ test_assert(head == NULL && tail == NULL);
+ test_end();
+}
+
+void test_llist(void)
+{
+ test_dllist();
+ test_dllist2();
+}
diff --git a/src/lib/test-log-throttle.c b/src/lib/test-log-throttle.c
new file mode 100644
index 0000000..430aba0
--- /dev/null
+++ b/src/lib/test-log-throttle.c
@@ -0,0 +1,57 @@
+/* Copyright (c) 2016-2018 Dovecot authors, see the included COPYING file */
+
+#include "test-lib.h"
+#include "ioloop.h"
+#include "log-throttle.h"
+
+static unsigned int test_log_throttle_new_events_count;
+
+static void test_log_throttle_callback(unsigned int new_events_count,
+ struct ioloop *ioloop)
+{
+ test_log_throttle_new_events_count = new_events_count;
+ io_loop_stop(ioloop);
+}
+
+void test_log_throttle(void)
+{
+ const struct log_throttle_settings set = {
+ .throttle_at_max_per_interval = 10,
+ .unthrottle_at_max_per_interval = 5,
+ .interval_msecs = 10,
+ };
+ struct log_throttle *throttle;
+ struct ioloop *ioloop;
+ unsigned int i;
+
+ test_begin("log throttle");
+
+ ioloop = io_loop_create();
+ throttle = log_throttle_init(&set, test_log_throttle_callback, ioloop);
+
+ /* throttle once and drop out just below */
+ for (i = 0; i < 10; i++)
+ test_assert_idx(log_throttle_accept(throttle), i);
+ for (i = 0; i < 4; i++)
+ test_assert_idx(!log_throttle_accept(throttle), i);
+
+ io_loop_run(ioloop);
+ test_assert(test_log_throttle_new_events_count == 4);
+
+ /* throttle and continue just above */
+ for (i = 0; i < 10; i++)
+ test_assert_idx(log_throttle_accept(throttle), i);
+ for (i = 0; i < 5; i++)
+ test_assert_idx(!log_throttle_accept(throttle), i);
+
+ io_loop_run(ioloop);
+ test_assert(test_log_throttle_new_events_count == 5);
+
+ /* we should be still throttled */
+ test_assert(!log_throttle_accept(throttle));
+
+ log_throttle_deinit(&throttle);
+ io_loop_destroy(&ioloop);
+
+ test_end();
+}
diff --git a/src/lib/test-macros.c b/src/lib/test-macros.c
new file mode 100644
index 0000000..4b9e1b1
--- /dev/null
+++ b/src/lib/test-macros.c
@@ -0,0 +1,63 @@
+/* Copyright (c) 2021 Dovecot authors, see the included COPYING file */
+
+#include "test-lib.h"
+
+struct parent {
+ unsigned int a;
+};
+struct child {
+ unsigned int b;
+ struct parent p;
+};
+
+static void test_container_of(void)
+{
+ struct child child;
+ struct parent *parent = &child.p;
+
+ test_begin("container_of()");
+ struct child *ptr_child = container_of(parent, struct child, p);
+ test_assert(ptr_child == &child);
+ test_end();
+}
+
+static void test_pointer_cast(void)
+{
+#define TEST_POINTER_CAST(type, prefix, value) \
+ type prefix ## _num = value; \
+ void *prefix ## _ptr = POINTER_CAST(prefix ## _num); \
+ test_assert(POINTER_CAST_TO(prefix ## _ptr, type) == prefix ## _num);
+ test_begin("POINTER_CAST");
+
+ TEST_POINTER_CAST(unsigned int, uint, 0x87654321);
+ TEST_POINTER_CAST(uint32_t, uint32, 0xf00dabcd);
+ TEST_POINTER_CAST(uint16_t, uint16, 0x9876);
+ TEST_POINTER_CAST(uint8_t, uint8, 0xf8);
+#if SIZEOF_VOID_P == 8
+ TEST_POINTER_CAST(unsigned long, ulong, 0xfedcba9876543210);
+ TEST_POINTER_CAST(size_t, size, 0xfedcba9876543210);
+#else
+ TEST_POINTER_CAST(unsigned long, ulong, 0xfedcba98);
+ TEST_POINTER_CAST(size_t, size, 0xfedcba98);
+#endif
+
+ test_end();
+}
+
+static void test_ptr_offset(void)
+{
+ uint32_t foo[] = { 1, 2, 3 };
+ const uint32_t foo2[] = { 1, 2, 3 };
+
+ test_begin("PTR_OFFSET");
+ test_assert(PTR_OFFSET(foo, 4) == &foo[1]);
+ test_assert(CONST_PTR_OFFSET(foo2, 8) == &foo2[2]);
+ test_end();
+}
+
+void test_macros(void)
+{
+ test_container_of();
+ test_pointer_cast();
+ test_ptr_offset();
+}
diff --git a/src/lib/test-malloc-overflow.c b/src/lib/test-malloc-overflow.c
new file mode 100644
index 0000000..ceb48bb
--- /dev/null
+++ b/src/lib/test-malloc-overflow.c
@@ -0,0 +1,132 @@
+/* Copyright (c) 2016-2018 Dovecot authors, see the included COPYING file */
+
+#include "test-lib.h"
+
+static void test_malloc_overflow_multiply(void)
+{
+ static const struct {
+ size_t a, b;
+ } tests[] = {
+ { 0, SIZE_MAX },
+ { 1, SIZE_MAX },
+ { SIZE_MAX/2, 2 },
+ };
+ test_begin("MALLOC_MULTIPLY()");
+ for (unsigned int i = 0; i < N_ELEMENTS(tests); i++) {
+ test_assert_idx(MALLOC_MULTIPLY(tests[i].a, tests[i].b) == tests[i].a * tests[i].b, i);
+ test_assert_idx(MALLOC_MULTIPLY(tests[i].b, tests[i].a) == tests[i].b * tests[i].a, i);
+ }
+ test_end();
+}
+
+static void test_malloc_overflow_add(void)
+{
+ static const struct {
+ size_t a, b;
+ } tests[] = {
+ { 0, SIZE_MAX },
+ { 1, SIZE_MAX-1 },
+ { SIZE_MAX/2+1, SIZE_MAX/2 },
+ };
+ unsigned short n = 2;
+
+ test_begin("MALLOC_ADD()");
+ /* check that no compiler warning is given */
+ test_assert(MALLOC_ADD(2, n) == 2U+n);
+ for (unsigned int i = 0; i < N_ELEMENTS(tests); i++) {
+ test_assert_idx(MALLOC_ADD(tests[i].a, tests[i].b) == tests[i].a + tests[i].b, i);
+ test_assert_idx(MALLOC_ADD(tests[i].b, tests[i].a) == tests[i].b + tests[i].a, i);
+ }
+ test_end();
+}
+
+void test_malloc_overflow(void)
+{
+ test_malloc_overflow_multiply();
+ test_malloc_overflow_add();
+}
+
+static enum fatal_test_state fatal_malloc_overflow_multiply(unsigned int *stage)
+{
+ const struct {
+ size_t a, b;
+ } mul_tests[] = {
+ { SIZE_MAX/2+1, 2 },
+ };
+ unsigned int i;
+
+ test_expect_fatal_string("memory allocation overflow");
+ switch (*stage) {
+ case 0:
+ test_begin("MALLOC_MULTIPLY() overflows");
+ i_error("%zu", MALLOC_MULTIPLY((size_t)SIZE_MAX/2, (uint8_t)3));
+ break;
+ case 1:
+ i_error("%zu", MALLOC_MULTIPLY((uint8_t)3, (size_t)SIZE_MAX/2));
+ break;
+ }
+ *stage -= 2;
+
+ if (*stage >= N_ELEMENTS(mul_tests)*2) {
+ *stage -= N_ELEMENTS(mul_tests)*2;
+ if (*stage == 0)
+ test_end();
+ test_expect_fatal_string(NULL);
+ return FATAL_TEST_FINISHED;
+ }
+ i = *stage / 2;
+
+ if (*stage % 2 == 0)
+ i_error("%zu", MALLOC_MULTIPLY(mul_tests[i].a, mul_tests[i].b));
+ else
+ i_error("%zu", MALLOC_MULTIPLY(mul_tests[i].b, mul_tests[i].a));
+ return FATAL_TEST_FAILURE;
+}
+
+static enum fatal_test_state fatal_malloc_overflow_add(unsigned int *stage)
+{
+ const struct {
+ size_t a, b;
+ } add_tests[] = {
+ { SIZE_MAX, 1 },
+ { SIZE_MAX/2+1, SIZE_MAX/2+1 },
+ };
+ unsigned int i;
+
+ test_expect_fatal_string("memory allocation overflow");
+ switch (*stage) {
+ case 0:
+ test_begin("MALLOC_ADD() overflows");
+ i_error("%zu", MALLOC_ADD((size_t)SIZE_MAX, (uint8_t)1));
+ break;
+ case 1:
+ i_error("%zu", MALLOC_ADD((uint8_t)1, (size_t)SIZE_MAX));
+ break;
+ }
+ *stage -= 2;
+
+ if (*stage >= N_ELEMENTS(add_tests)*2) {
+ *stage -= N_ELEMENTS(add_tests)*2;
+ if (*stage == 0)
+ test_end();
+ test_expect_fatal_string(NULL);
+ return FATAL_TEST_FINISHED;
+ }
+ i = *stage / 2;
+
+ if (*stage % 2 == 0)
+ i_error("%zu", MALLOC_ADD(add_tests[i].a, add_tests[i].b));
+ else
+ i_error("%zu", MALLOC_ADD(add_tests[i].b, add_tests[i].a));
+ return FATAL_TEST_FAILURE;
+}
+
+enum fatal_test_state fatal_malloc_overflow(unsigned int stage)
+{
+ enum fatal_test_state state;
+
+ state = fatal_malloc_overflow_multiply(&stage);
+ if (state != FATAL_TEST_FINISHED)
+ return state;
+ return fatal_malloc_overflow_add(&stage);
+}
diff --git a/src/lib/test-memarea.c b/src/lib/test-memarea.c
new file mode 100644
index 0000000..9a851dc
--- /dev/null
+++ b/src/lib/test-memarea.c
@@ -0,0 +1,42 @@
+/* Copyright (c) 2017-2018 Dovecot authors, see the included COPYING file */
+
+#include "test-lib.h"
+#include "buffer.h"
+#include "memarea.h"
+
+static bool test_callback_called = FALSE;
+
+static void test_callback(buffer_t *buf)
+{
+ test_assert(!test_callback_called);
+ test_callback_called = TRUE;
+ buffer_free(&buf);
+}
+
+void test_memarea(void)
+{
+ struct memarea *area, *area2;
+ buffer_t *buf;
+ size_t size;
+
+ test_begin("memarea");
+ buf = buffer_create_dynamic(default_pool, 128);
+ buffer_append(buf, "123", 3);
+
+ area = memarea_init(buf->data, buf->used, test_callback, buf);
+ test_assert(memarea_get_refcount(area) == 1);
+ test_assert(memarea_get(area, &size) == buf->data && size == buf->used);
+
+ area2 = area;
+ memarea_ref(area2);
+ test_assert(memarea_get_refcount(area2) == 2);
+ test_assert(memarea_get(area2, &size) == buf->data && size == buf->used);
+ memarea_unref(&area2);
+ test_assert(area2 == NULL);
+ test_assert(!test_callback_called);
+
+ memarea_unref(&area);
+ test_assert(test_callback_called);
+
+ test_end();
+}
diff --git a/src/lib/test-mempool-allocfree.c b/src/lib/test-mempool-allocfree.c
new file mode 100644
index 0000000..6fd69dc
--- /dev/null
+++ b/src/lib/test-mempool-allocfree.c
@@ -0,0 +1,135 @@
+/* Copyright (c) 2007-2018 Dovecot authors, see the included COPYING file */
+
+#include "test-lib.h"
+
+#define SENSE 0xAB /* produces 10101011 */
+
+static bool mem_has_bytes(const void *mem, size_t size, uint8_t b)
+{
+ const uint8_t *bytes = mem;
+ unsigned int i;
+
+ for (i = 0; i < size; i++) {
+ if (bytes[i] != b) {
+ i_debug("bytes[%u] != %u", i, b);
+ return FALSE;
+ }
+ }
+ return TRUE;
+}
+
+void test_mempool_allocfree(void)
+{
+ pool_t pool;
+ unsigned int i;
+ size_t last_alloc = 0;
+ size_t used = 0;
+ size_t count = 0;
+ void *mem = NULL;
+
+ test_begin("mempool_allocfree");
+ pool = pool_allocfree_create("test");
+
+ for(i = 0; i <= 1000; i++) {
+ /* release previous allocation */
+ if ((i % 3) == 0) {
+ if (mem != NULL) {
+ test_assert_idx(mem_has_bytes(mem, last_alloc, SENSE), i);
+ used -= last_alloc;
+ count--;
+ }
+ last_alloc = 0;
+ p_free(pool, mem);
+ /* grow previous allocation */
+ } else if ((i % 5) == 0) {
+ if (mem != NULL)
+ used -= last_alloc;
+ else
+ count++;
+ mem = p_realloc(pool, mem, last_alloc, i*2);
+ if (last_alloc > 0)
+ test_assert_idx(mem_has_bytes(mem, last_alloc, SENSE), i);
+ memset(mem, SENSE, i*2);
+ last_alloc = i*2;
+ used += i*2;
+ /* shrink previous allocation */
+ } else if ((i % 7) == 0) {
+ if (mem != NULL)
+ used -= last_alloc;
+ else
+ count++;
+ mem = p_realloc(pool, mem, last_alloc, i-2);
+ if (last_alloc > 0)
+ test_assert_idx(mem_has_bytes(mem, i-2, SENSE), i);
+ memset(mem, SENSE, i-2);
+ last_alloc = i-2;
+ used += i-2;
+ /* allocate some memory */
+ } else {
+ mem = p_malloc(pool, i);
+ /* fill it with sense marker */
+ memset(mem, SENSE, i);
+ used += i;
+ count++;
+ last_alloc = i;
+ }
+ }
+
+ test_assert(pool_allocfree_get_total_used_size(pool) == used);
+
+ pool_unref(&pool);
+
+ /* make sure realloc works correctly */
+ pool = pool_allocfree_create("test");
+ mem = NULL;
+
+ for(i = 1; i < 1000; i++) {
+ mem = p_realloc(pool, mem, i-1, i);
+ test_assert_idx(mem_has_bytes(mem, i-1, 0xde), i);
+ memset(mem, 0xde, i);
+ }
+
+ pool_unref(&pool);
+
+ test_end();
+}
+
+enum fatal_test_state fatal_mempool_allocfree(unsigned int stage)
+{
+ static pool_t pool;
+
+ if (pool == NULL && stage != 0)
+ return FATAL_TEST_FAILURE;
+
+ switch(stage) {
+ case 0: /* forbidden size */
+ test_begin("fatal_mempool_allocfree");
+ pool = pool_allocfree_create("fatal");
+ test_expect_fatal_string("Trying to allocate 0 bytes");
+ (void)p_malloc(pool, 0);
+ return FATAL_TEST_FAILURE;
+
+ case 1: /* logically impossible size */
+ test_expect_fatal_string("Trying to allocate");
+ (void)p_malloc(pool, POOL_MAX_ALLOC_SIZE + 1ULL);
+ return FATAL_TEST_FAILURE;
+
+#ifdef _LP64 /* malloc(POOL_MAX_ALLOC_SIZE) may succeed with 32bit */
+ case 2: /* physically impossible size */
+ test_expect_fatal_string("Out of memory");
+ (void)p_malloc(pool, POOL_MAX_ALLOC_SIZE);
+ return FATAL_TEST_FAILURE;
+#endif
+
+ /* Continue with other tests as follows:
+ case 3:
+ something_fatal();
+ return FATAL_TEST_FAILURE;
+ */
+ }
+
+ /* Either our tests have finished, or the test suite has got confused. */
+ pool_unref(&pool);
+ test_end();
+ return FATAL_TEST_FINISHED;
+}
diff --git a/src/lib/test-mempool-alloconly.c b/src/lib/test-mempool-alloconly.c
new file mode 100644
index 0000000..c3b0001
--- /dev/null
+++ b/src/lib/test-mempool-alloconly.c
@@ -0,0 +1,94 @@
+/* Copyright (c) 2007-2018 Dovecot authors, see the included COPYING file */
+
+#include "test-lib.h"
+
+static bool mem_has_bytes(const void *mem, size_t size, uint8_t b)
+{
+ const uint8_t *bytes = mem;
+ unsigned int i;
+
+ for (i = 0; i < size; i++) {
+ if (bytes[i] != b)
+ return FALSE;
+ }
+ return TRUE;
+}
+
+void test_mempool_alloconly(void)
+{
+#define SENTRY_SIZE 32
+#define SENTRY_CHAR 0xDE
+#define PMALLOC_MAX_COUNT 128
+ pool_t pool;
+ unsigned int i, j, k;
+ void *mem[PMALLOC_MAX_COUNT + 1];
+ char *sentry;
+
+ test_begin("mempool_alloconly");
+ for (i = 0; i < 64; i++) {
+ for (j = 1; j <= 128; j++) {
+ pool = pool_alloconly_create(MEMPOOL_GROWING"test", i);
+ /* make sure p_malloc() doesn't overwrite unallocated
+ data in data stack. parts of the code relies on
+ this. */
+ sentry = t_buffer_get(SENTRY_SIZE);
+ memset(sentry, SENTRY_CHAR, SENTRY_SIZE);
+
+ mem[0] = p_malloc(pool, j);
+ memset(mem[0], j, j);
+
+ for (k = 1; k <= PMALLOC_MAX_COUNT; k++) {
+ mem[k] = p_malloc(pool, k);
+ memset(mem[k], k, k);
+ }
+ test_assert(mem_has_bytes(sentry, SENTRY_SIZE, SENTRY_CHAR));
+ test_assert(t_buffer_get(SENTRY_SIZE) == sentry);
+
+ test_assert(mem_has_bytes(mem[0], j, j));
+ for (k = 1; k <= PMALLOC_MAX_COUNT; k++)
+ test_assert(mem_has_bytes(mem[k], k, k));
+ pool_unref(&pool);
+ }
+ }
+ test_end();
+}
+
+enum fatal_test_state fatal_mempool_alloconly(unsigned int stage)
+{
+ static pool_t pool;
+
+ if (pool == NULL && stage != 0)
+ return FATAL_TEST_FAILURE;
+
+ switch(stage) {
+ case 0: /* forbidden size */
+ test_begin("fatal_mempool_alloconly");
+ pool = pool_alloconly_create(MEMPOOL_GROWING"fatal", 1);
+ test_expect_fatal_string("Trying to allocate 0 bytes");
+ (void)p_malloc(pool, 0);
+ return FATAL_TEST_FAILURE;
+
+ case 1: /* logically impossible size */
+ test_expect_fatal_string("Trying to allocate");
+ (void)p_malloc(pool, POOL_MAX_ALLOC_SIZE + 1ULL);
+ return FATAL_TEST_FAILURE;
+
+#ifdef _LP64 /* malloc(POOL_MAX_ALLOC_SIZE) may succeed with 32bit */
+ case 2: /* physically impossible size */
+ test_expect_fatal_string("Out of memory");
+ (void)p_malloc(pool, POOL_MAX_ALLOC_SIZE);
+ return FATAL_TEST_FAILURE;
+#endif
+
+ /* Continue with other tests as follows:
+ case 3:
+ something_fatal();
+ return FATAL_TEST_FAILURE;
+ */
+ }
+
+ /* Either our tests have finished, or the test suite has got confused. */
+ pool_unref(&pool);
+ test_end();
+ return FATAL_TEST_FINISHED;
+}
diff --git a/src/lib/test-mempool.c b/src/lib/test-mempool.c
new file mode 100644
index 0000000..7c707dd
--- /dev/null
+++ b/src/lib/test-mempool.c
@@ -0,0 +1,117 @@
+/* Copyright (c) 2017-2018 Dovecot authors, see the included COPYING file */
+
+#include "test-lib.h"
+
+#if SIZEOF_VOID_P == 8
+typedef char uint32max_array_t[4294967295];
+#else
+typedef char uint32max_array_t[65535];
+#endif
+
+#define BIG_MAX POOL_MAX_ALLOC_SIZE
+
+#if defined(_LP64)
+#define LITTLE_MAX ((unsigned long long) INT32_MAX)
+#else
+#define LITTLE_MAX ((unsigned long long) INT16_MAX)
+#endif
+
+extern struct pool test_pool;
+
+/* Checks allocations & reallocations for a given type. */
+#define CHECK_OVERFLOW(type, nelem, _maxsize) \
+ do { \
+ const size_t maxsize = (_maxsize); \
+ test_begin("mempool overflow - " #type); \
+ type *ptr = p_new(&test_pool, type, (nelem)); \
+ test_assert(ptr == POINTER_CAST(maxsize)); \
+ /* grow: */ \
+ test_assert(p_realloc_type(&test_pool, ptr, type, (nelem) - 1, (nelem)) == POINTER_CAST(maxsize)); \
+ /* shrink: */ \
+ test_assert(p_realloc_type(&test_pool, ptr, type, (nelem), (nelem) - 1) == POINTER_CAST(maxsize - sizeof(type))); \
+ test_end(); \
+ } while (0)
+
+static void test_mempool_overflow(void)
+{
+ CHECK_OVERFLOW(uint32max_array_t, LITTLE_MAX, sizeof(uint32max_array_t) * LITTLE_MAX);
+ CHECK_OVERFLOW(char, BIG_MAX, BIG_MAX);
+ CHECK_OVERFLOW(uint32_t, BIG_MAX / sizeof(uint32_t), BIG_MAX - 3);
+}
+
+enum fatal_test_state fatal_mempool(unsigned int stage)
+{
+ static uint32max_array_t *m1;
+ static uint32_t *m2;
+
+ switch(stage) {
+ case 0:
+ test_expect_fatal_string("Trying to allocate");
+ test_begin("fatal mempool overflow");
+ m1 = p_new(&test_pool, uint32max_array_t, LITTLE_MAX + 3);
+ return FATAL_TEST_FAILURE;
+ case 1:
+ test_expect_fatal_string("Trying to allocate");
+ m2 = p_new(&test_pool, uint32_t, BIG_MAX / sizeof(uint32_t) + 1);
+ return FATAL_TEST_FAILURE;
+ case 2: /* grow */
+ test_expect_fatal_string("Trying to reallocate");
+ m1 = p_realloc_type(&test_pool, m1, uint32max_array_t,
+ LITTLE_MAX + 2, LITTLE_MAX + 3);
+ return FATAL_TEST_FAILURE;
+ case 3:
+ test_expect_fatal_string("Trying to reallocate");
+ m2 = p_realloc_type(&test_pool, m2, uint32_t,
+ BIG_MAX / sizeof(uint32_t),
+ BIG_MAX / sizeof(uint32_t) + 1);
+ return FATAL_TEST_FAILURE;
+ case 4: /* shrink */
+ test_expect_fatal_string("Trying to reallocate");
+ m1 = p_realloc_type(&test_pool, m1, uint32max_array_t,
+ LITTLE_MAX + 3, LITTLE_MAX + 2);
+ return FATAL_TEST_FAILURE;
+ case 5:
+ test_expect_fatal_string("Trying to reallocate");
+ m2 = p_realloc_type(&test_pool, m2, uint32_t,
+ BIG_MAX / sizeof(uint32_t) + 2,
+ BIG_MAX / sizeof(uint32_t) + 1);
+ return FATAL_TEST_FAILURE;
+ }
+ test_expect_fatal_string(NULL);
+ test_end();
+ return FATAL_TEST_FINISHED;
+}
+
+static const char *pool_test_get_name(pool_t pool ATTR_UNUSED) { return "test"; }
+static void pool_test_ref(pool_t pool ATTR_UNUSED) { }
+static void pool_test_unref(pool_t *pool) { *pool = NULL; }
+static void *pool_test_malloc(pool_t pool ATTR_UNUSED, size_t size) { return POINTER_CAST(size); }
+static void pool_test_free(pool_t pool ATTR_UNUSED, void *mem ATTR_UNUSED) { }
+static void *pool_test_realloc(pool_t pool ATTR_UNUSED, void *mem ATTR_UNUSED,
+ size_t old_size ATTR_UNUSED, size_t new_size) {
+ return POINTER_CAST(new_size);
+}
+static void pool_test_clear(pool_t pool ATTR_UNUSED) { }
+static size_t pool_test_get_max_easy_alloc_size(pool_t pool ATTR_UNUSED) { return 12345; }
+static const struct pool_vfuncs test_pool_vfuncs = {
+ pool_test_get_name,
+ pool_test_ref,
+ pool_test_unref,
+ pool_test_malloc,
+ pool_test_free,
+ pool_test_realloc,
+ pool_test_clear,
+ pool_test_get_max_easy_alloc_size
+};
+
+struct pool test_pool = {
+ .v = &test_pool_vfuncs,
+
+ .alloconly_pool = TRUE,
+ .datastack_pool = FALSE,
+};
+
+void test_mempool(void)
+{
+ test_mempool_overflow();
+}
diff --git a/src/lib/test-multiplex.c b/src/lib/test-multiplex.c
new file mode 100644
index 0000000..7f36094
--- /dev/null
+++ b/src/lib/test-multiplex.c
@@ -0,0 +1,167 @@
+/* Copyright (c) 2017-2018 Dovecot authors, see the included COPYING file */
+
+#include "test-lib.h"
+#include "ioloop.h"
+#include "str.h"
+#include "istream.h"
+#include "istream-multiplex.h"
+#include "ostream.h"
+#include "ostream-multiplex.h"
+#include "ostream.h"
+#include "randgen.h"
+
+#include <unistd.h>
+
+struct test_channel {
+ int fds[2];
+ unsigned int cid;
+
+ struct istream *in;
+ struct ostream *out;
+ struct io *io;
+
+ struct istream *in_alt;
+ struct ostream *out_alt;
+ struct io *io_alt;
+
+ buffer_t *received;
+ buffer_t *received_alt;
+
+ unsigned int counter;
+};
+
+static struct test_channel test_channel[2];
+
+static void test_multiplex_channel_write(struct test_channel *channel)
+{
+ unsigned char buf[128];
+ size_t len = i_rand_limit(sizeof(buf));
+ random_fill(buf, len);
+ o_stream_nsend(channel->out, buf, len);
+ o_stream_nsend(channel->out_alt, buf, len);
+}
+
+static void test_multiplex_stream_write(struct ostream *channel ATTR_UNUSED)
+{
+ if (test_channel[0].received->used > 1000 &&
+ test_channel[1].received->used > 1000)
+ io_loop_stop(current_ioloop);
+ else
+ test_multiplex_channel_write(&test_channel[i_rand_limit(2)]);
+}
+
+static void test_istream_multiplex_stream_read(struct test_channel *channel)
+{
+ const unsigned char *data = NULL;
+ size_t siz = 0;
+
+ if (i_stream_read(channel->in) > 0) {
+ data = i_stream_get_data(channel->in, &siz);
+ buffer_append(channel->received, data, siz);
+ i_stream_skip(channel->in, siz);
+ }
+}
+
+static void test_istream_read_alt(struct test_channel *channel)
+{
+ const unsigned char *data = NULL;
+ size_t siz = 0;
+
+ if (i_stream_read(channel->in_alt) > 0) {
+ data = i_stream_get_data(channel->in_alt, &siz);
+ buffer_append(channel->received_alt, data, siz);
+ i_stream_skip(channel->in_alt, siz);
+ }
+}
+
+static void setup_channel(struct test_channel *channel,
+ struct istream *is, struct ostream *os)
+{
+ /* setup first channel */
+ channel->in = is;
+ channel->out = os;
+ channel->io = io_add_istream(is, test_istream_multiplex_stream_read,
+ channel);
+ test_assert(pipe(channel->fds) == 0);
+ fd_set_nonblock(channel->fds[0], TRUE);
+ fd_set_nonblock(channel->fds[1], TRUE);
+ channel->in_alt = i_stream_create_fd(channel->fds[0], SIZE_MAX);
+ channel->out_alt = o_stream_create_fd(channel->fds[1], IO_BLOCK_SIZE);
+ channel->io_alt = io_add_istream(channel->in_alt, test_istream_read_alt,
+ channel);
+ channel->received = buffer_create_dynamic(default_pool, 32768);
+ channel->received_alt = buffer_create_dynamic(default_pool, 32768);
+}
+
+static void teardown_channel(struct test_channel *channel)
+{
+ test_istream_read_alt(channel);
+ test_assert(memcmp(channel->received->data,
+ channel->received_alt->data,
+ channel->received->used) == 0);
+ test_assert(channel->received->used == channel->received_alt->used);
+
+ buffer_free(&channel->received);
+ buffer_free(&channel->received_alt);
+
+ io_remove(&channel->io);
+ io_remove(&channel->io_alt);
+ i_stream_unref(&channel->in);
+ test_assert(o_stream_finish(channel->out) > 0);
+ o_stream_unref(&channel->out);
+ i_stream_unref(&channel->in_alt);
+ test_assert(o_stream_finish(channel->out_alt) > 0);
+ o_stream_unref(&channel->out_alt);
+ i_close_fd(&channel->fds[0]);
+ i_close_fd(&channel->fds[1]);
+}
+
+static void test_multiplex_stream(void) {
+ test_begin("test multiplex (stream)");
+
+ struct ioloop *ioloop = io_loop_create();
+ io_loop_set_current(ioloop);
+
+ int fds[2];
+ test_assert(pipe(fds) == 0);
+ fd_set_nonblock(fds[0], TRUE);
+ fd_set_nonblock(fds[1], TRUE);
+ struct ostream *os = o_stream_create_fd(fds[1], SIZE_MAX);
+ struct istream *is = i_stream_create_fd(fds[0], SIZE_MAX);
+
+ struct istream *ichan0 = i_stream_create_multiplex(is, SIZE_MAX);
+ struct istream *ichan1 = i_stream_multiplex_add_channel(ichan0, 1);
+ i_stream_unref(&is);
+
+ struct ostream *ochan0 = o_stream_create_multiplex(os, 1024);
+ struct ostream *ochan1 = o_stream_multiplex_add_channel(ochan0, 1);
+ o_stream_unref(&os);
+
+ struct io *io = io_add(fds[1], IO_WRITE, test_multiplex_stream_write, os);
+
+ setup_channel(&test_channel[0], ichan0, ochan0);
+ setup_channel(&test_channel[1], ichan1, ochan1);
+
+ test_channel[0].cid = 0;
+ test_channel[1].cid = 1;
+
+ io_loop_run(current_ioloop);
+
+ io_remove(&io);
+
+ teardown_channel(&test_channel[0]);
+ teardown_channel(&test_channel[1]);
+
+ io_loop_destroy(&ioloop);
+
+ i_close_fd(&fds[0]);
+ i_close_fd(&fds[1]);
+
+ test_end();
+}
+
+void test_multiplex(void) {
+ random_init();
+ test_multiplex_stream();
+ random_deinit();
+}
diff --git a/src/lib/test-net.c b/src/lib/test-net.c
new file mode 100644
index 0000000..fb19d5b
--- /dev/null
+++ b/src/lib/test-net.c
@@ -0,0 +1,167 @@
+/* Copyright (c) 2007-2018 Dovecot authors, see the included COPYING file */
+
+#include "test-lib.h"
+#include "net.h"
+
+struct test_net_is_in_network_input {
+ const char *ip;
+ const char *net;
+ unsigned int bits;
+ bool ret;
+};
+
+static void test_net_is_in_network(void)
+{
+ static const struct test_net_is_in_network_input input[] = {
+ { "1.2.3.4", "1.2.3.4", 32, TRUE },
+ { "1.2.3.4", "1.2.3.3", 32, FALSE },
+ { "1.2.3.4", "1.2.3.5", 32, FALSE },
+ { "1.2.3.4", "1.2.2.4", 32, FALSE },
+ { "1.2.3.4", "1.1.3.4", 32, FALSE },
+ { "1.2.3.4", "0.2.3.4", 32, FALSE },
+ { "1.2.3.253", "1.2.3.254", 31, FALSE },
+ { "1.2.3.254", "1.2.3.254", 31, TRUE },
+ { "1.2.3.255", "1.2.3.254", 31, TRUE },
+ { "1.2.3.255", "1.2.3.0", 24, TRUE },
+ { "1.2.255.255", "1.2.254.0", 23, TRUE },
+ { "255.255.255.255", "128.0.0.0", 1, TRUE },
+ { "255.255.255.255", "127.0.0.0", 1, FALSE },
+ { "1234:5678::abcf", "1234:5678::abce", 127, TRUE },
+ { "1234:5678::abcd", "1234:5678::abce", 127, FALSE },
+ { "123e::ffff", "123e::0", 15, TRUE },
+ { "::ffff:1.2.3.4", "1.2.3.4", 32, TRUE },
+ { "::ffff:1.2.3.4", "1.2.3.3", 32, FALSE },
+ { "::ffff:1.2.3.4", "::ffff:1.2.3.4", 0, FALSE }
+ };
+ struct ip_addr ip, net_ip;
+ unsigned int i;
+
+ test_begin("net_is_in_network()");
+ for (i = 0; i < N_ELEMENTS(input); i++) {
+ test_assert(net_addr2ip(input[i].ip, &ip) == 0);
+ test_assert(net_addr2ip(input[i].net, &net_ip) == 0);
+ test_assert_idx(net_is_in_network(&ip, &net_ip, input[i].bits) ==
+ input[i].ret, i);
+ }
+ /* make sure non-IPv4 and non-IPv6 ip_addrs fail */
+ test_assert(net_addr2ip("127.0.0.1", &ip) == 0);
+ net_ip = ip;
+ net_ip.family = 0;
+ test_assert(!net_is_in_network(&ip, &net_ip, 0));
+ test_assert(!net_is_in_network(&net_ip, &ip, 0));
+ test_assert(net_addr2ip("::1", &ip) == 0);
+ net_ip = ip;
+ net_ip.family = 0;
+ test_assert(!net_is_in_network(&ip, &net_ip, 0));
+ test_assert(!net_is_in_network(&net_ip, &ip, 0));
+ test_end();
+}
+
+static void test_net_ip2addr(void)
+{
+ struct ip_addr ip;
+
+ test_begin("net_ip2addr()");
+ test_assert(net_addr2ip("127.0.0.1", &ip) == 0 &&
+ ip.family == AF_INET &&
+ ntohl(ip.u.ip4.s_addr) == (0x7f000001));
+ test_assert(net_addr2ip("2130706433", &ip) == 0 &&
+ ip.family == AF_INET &&
+ ntohl(ip.u.ip4.s_addr) == (0x7f000001));
+ test_assert(strcmp(net_ip2addr(&ip), "127.0.0.1") == 0);
+ test_assert(net_addr2ip("255.254.253.252", &ip) == 0 &&
+ ip.family == AF_INET &&
+ ntohl(ip.u.ip4.s_addr) == (0xfffefdfc));
+ test_assert(strcmp(net_ip2addr(&ip), "255.254.253.252") == 0);
+ test_assert(net_addr2ip("::5", &ip) == 0 &&
+ ip.family == AF_INET6 &&
+ ip.u.ip6.s6_addr[15] == 5);
+ test_assert(strcmp(net_ip2addr(&ip), "::5") == 0);
+ test_assert(net_addr2ip("[::5]", &ip) == 0 &&
+ ip.family == AF_INET6 &&
+ ip.u.ip6.s6_addr[15] == 5);
+ test_assert(strcmp(net_ip2addr(&ip), "::5") == 0);
+ ip.family = 123;
+ test_assert(net_addr2ip("abc", &ip) < 0 &&
+ ip.family == 123);
+ test_end();
+}
+
+static void test_net_str2hostport(void)
+{
+ const char *host;
+ in_port_t port;
+
+ test_begin("net_str2hostport()");
+ /* [IPv6] */
+ test_assert(net_str2hostport("[1::4]", 0, &host, &port) == 0 &&
+ strcmp(host, "1::4") == 0 && port == 0);
+ test_assert(net_str2hostport("[1::4]", 1234, &host, &port) == 0 &&
+ strcmp(host, "1::4") == 0 && port == 1234);
+ test_assert(net_str2hostport("[1::4]:78", 1234, &host, &port) == 0 &&
+ strcmp(host, "1::4") == 0 && port == 78);
+ host = NULL;
+ test_assert(net_str2hostport("[1::4]:", 1234, &host, &port) < 0 && host == NULL);
+ test_assert(net_str2hostport("[1::4]:0", 1234, &host, &port) < 0 && host == NULL);
+ test_assert(net_str2hostport("[1::4]:x", 1234, &host, &port) < 0 && host == NULL);
+ /* IPv6 */
+ test_assert(net_str2hostport("1::4", 0, &host, &port) == 0 &&
+ strcmp(host, "1::4") == 0 && port == 0);
+ test_assert(net_str2hostport("1::4", 1234, &host, &port) == 0 &&
+ strcmp(host, "1::4") == 0 && port == 1234);
+ /* host */
+ test_assert(net_str2hostport("foo", 0, &host, &port) == 0 &&
+ strcmp(host, "foo") == 0 && port == 0);
+ test_assert(net_str2hostport("foo", 1234, &host, &port) == 0 &&
+ strcmp(host, "foo") == 0 && port == 1234);
+ test_assert(net_str2hostport("foo:78", 1234, &host, &port) == 0 &&
+ strcmp(host, "foo") == 0 && port == 78);
+ host = NULL;
+ test_assert(net_str2hostport("foo:", 1234, &host, &port) < 0 && host == NULL);
+ test_assert(net_str2hostport("foo:0", 1234, &host, &port) < 0 && host == NULL);
+ test_assert(net_str2hostport("foo:x", 1234, &host, &port) < 0 && host == NULL);
+ /* edge cases with multiple ':' - currently these don't return errors,
+ but perhaps they should. */
+ test_assert(net_str2hostport("foo::78", 1234, &host, &port) == 0 &&
+ strcmp(host, "foo::78") == 0 && port == 1234);
+ test_assert(net_str2hostport("::foo:78", 1234, &host, &port) == 0 &&
+ strcmp(host, "::foo:78") == 0 && port == 1234);
+ test_assert(net_str2hostport("[::foo]:78", 1234, &host, &port) == 0 &&
+ strcmp(host, "::foo") == 0 && port == 78);
+ test_assert(net_str2hostport("[::::]", 1234, &host, &port) == 0 &&
+ strcmp(host, "::::") == 0 && port == 1234);
+ test_assert(net_str2hostport("[::::]:78", 1234, &host, &port) == 0 &&
+ strcmp(host, "::::") == 0 && port == 78);
+ test_end();
+}
+
+static void test_net_unix_long_paths(void)
+{
+#ifdef ENAMETOOLONG
+ int long_errno = ENAMETOOLONG;
+#else
+ int long_errno = EOVERFLOW;
+#endif
+
+ test_begin("net_*_unix() - long paths");
+
+ char path[PATH_MAX];
+ memset(path, 'x', sizeof(path)-1);
+ path[sizeof(path)-1] = '\0';
+
+ test_assert(net_listen_unix(path, 1) == -1);
+ test_assert(errno == long_errno);
+
+ test_assert(net_connect_unix(path) == -1);
+ test_assert(errno == long_errno);
+
+ test_end();
+}
+
+void test_net(void)
+{
+ test_net_is_in_network();
+ test_net_ip2addr();
+ test_net_str2hostport();
+ test_net_unix_long_paths();
+}
diff --git a/src/lib/test-numpack.c b/src/lib/test-numpack.c
new file mode 100644
index 0000000..9da8bb2
--- /dev/null
+++ b/src/lib/test-numpack.c
@@ -0,0 +1,64 @@
+/* Copyright (c) 2013-2018 Dovecot authors, see the included COPYING file */
+
+#include "test-lib.h"
+#include "buffer.h"
+#include "numpack.h"
+
+
+static const struct test {
+ uint64_t input;
+ uint8_t output[10];
+ unsigned int output_size;
+} enc_tests[] = {
+ { 0xffffffff, { 0xff, 0xff, 0xff, 0xff, 0xf }, 5 },
+ { 0, { 0 }, 1 },
+ { 0x7f, { 0x7f }, 1 },
+ { 0x80, { 0x80, 1 }, 2 },
+ { 0x81, { 0x81, 1 }, 2 },
+ { 0xdeadbeefcafe, { 0xfe, 0x95, 0xbf, 0xf7, 0xdb, 0xd5, 0x37 }, 7 },
+ { 0xffffffffffffffff, { 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 1 }, 10 },
+ { 0xfffffffe, { 0xfe, 0xff, 0xff, 0xff, 0xf }, 5 },
+};
+static const struct fail {
+ uint8_t input[11];
+ unsigned int input_size;
+} dec_fails[] = {
+ { { 0 }, 0 }, /* has no termination byte */
+ { { 0x80 }, 1 }, /* ditto */
+ { { 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80 }, 10 }, /* ditto*/
+ { { 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 2 }, 10 }, /* overflow */
+ { { 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x7f }, 11 }, /* ditto */
+};
+
+void test_numpack(void)
+{
+ buffer_t *buf = t_buffer_create(32);
+ unsigned int i;
+ const uint8_t *p, *end;
+ uint64_t num;
+ uint64_t magic=0x9669699669969669;
+
+ test_begin("numpack (good)");
+ for (i = 0; i < N_ELEMENTS(enc_tests); i++) {
+ buffer_set_used_size(buf, 0);
+ numpack_encode(buf, enc_tests[i].input);
+ test_assert_idx(buf->used == enc_tests[i].output_size, i);
+ test_assert_idx(memcmp(buf->data, enc_tests[i].output,
+ enc_tests[i].output_size) == 0,
+ i);
+
+ p = buf->data; end = p + buf->used;
+ test_assert_idx(numpack_decode(&p, end, &num) == 0, i);
+ test_assert_idx(num == enc_tests[i].input, i);
+ }
+ test_end();
+
+ test_begin("numpack (bad)");
+ for (i = 0; i < N_ELEMENTS(dec_fails); i++) {
+ p = dec_fails[i].input; end = p + dec_fails[i].input_size;
+ num = magic;
+ test_assert_idx(numpack_decode(&p, end, &num) == -1, i);
+ test_assert_idx(p == dec_fails[i].input && num == magic, i);
+ }
+ test_end();
+}
diff --git a/src/lib/test-ostream-buffer.c b/src/lib/test-ostream-buffer.c
new file mode 100644
index 0000000..4a449f6
--- /dev/null
+++ b/src/lib/test-ostream-buffer.c
@@ -0,0 +1,108 @@
+/* Copyright (c) 2017-2018 Dovecot authors, see the included COPYING file */
+
+#include "test-lib.h"
+#include "buffer.h"
+#include "str.h"
+#include "randgen.h"
+#include "istream.h"
+#include "ostream.h"
+
+#define MAX_BUFSIZE 256
+
+static void test_ostream_buffer_random_once(void)
+{
+ buffer_t *buffer;
+ struct ostream *output;
+ char buf[MAX_BUFSIZE*4], randbuf[MAX_BUFSIZE];
+ unsigned int i, offset, size;
+
+ buffer = buffer_create_dynamic(default_pool, 8);
+
+ memset(buf, 0, sizeof(buf));
+
+ output = o_stream_create_buffer(buffer);
+ o_stream_cork(output);
+
+ size = i_rand_minmax(1, MAX_BUFSIZE);
+ random_fill(randbuf, size);
+ memcpy(buf, randbuf, size);
+ test_assert(o_stream_send(output, buf, size) > 0);
+
+ for (i = 0; i < 10; i++) {
+ offset = i_rand_limit(MAX_BUFSIZE * 3);
+ size = i_rand_minmax(1, MAX_BUFSIZE);
+ random_fill(randbuf, size);
+ memcpy(buf + offset, randbuf, size);
+ test_assert(o_stream_pwrite(output, randbuf, size, offset) == 0);
+ if (i_rand_limit(10) == 0)
+ test_assert(o_stream_flush(output) > 0);
+ }
+
+ o_stream_uncork(output);
+ test_assert(o_stream_finish(output) > 0);
+
+ i_assert(buffer->used <= MAX_BUFSIZE*4);
+ test_assert(memcmp(buf, buffer->data, buffer->used) == 0);
+
+ o_stream_unref(&output);
+ buffer_free(&buffer);
+}
+
+static void test_ostream_buffer_random(void)
+{
+ unsigned int i;
+
+ test_begin("ostream buffer pwrite random");
+ for (i = 0; i < 100; i++) T_BEGIN {
+ test_ostream_buffer_random_once();
+ } T_END;
+ test_end();
+}
+
+static void test_ostream_buffer_size(void)
+{
+ struct ostream *output;
+ string_t *str = t_str_new(64);
+
+ test_begin("ostream buffer size/available");
+ output = o_stream_create_buffer(str);
+ test_assert(o_stream_get_buffer_used_size(output) == 0);
+ test_assert(o_stream_get_buffer_avail_size(output) == SIZE_MAX);
+
+ /* test shrinking sink's max buffer size */
+ o_stream_set_max_buffer_size(output, 10);
+ test_assert(o_stream_get_buffer_used_size(output) == 0);
+ test_assert(o_stream_get_buffer_avail_size(output) == 10);
+
+ /* partial send */
+ const char *partial_input = "01234567890123456789";
+ ssize_t ret = o_stream_send_str(output, partial_input);
+ test_assert(ret == 10);
+ test_assert(o_stream_get_buffer_used_size(output) == 10);
+ test_assert(o_stream_get_buffer_avail_size(output) == 0);
+
+ /* increase max buffer size so that it can hold the whole message */
+ o_stream_set_max_buffer_size(output, 100);
+ test_assert(o_stream_get_buffer_used_size(output) == 10);
+ test_assert(o_stream_get_buffer_avail_size(output) == 90);
+
+ /* send the rest */
+ ret += o_stream_send_str(output, partial_input + ret);
+ test_assert(ret == (ssize_t)strlen(partial_input));
+ test_assert(output->offset == str_len(str));
+ test_assert(o_stream_get_buffer_used_size(output) == 20);
+ test_assert(o_stream_get_buffer_avail_size(output) == 80);
+
+ /* check buffered data */
+ test_assert(strcmp(str_c(str), partial_input) == 0);
+
+ o_stream_unref(&output);
+
+ test_end();
+}
+
+void test_ostream_buffer(void)
+{
+ test_ostream_buffer_random();
+ test_ostream_buffer_size();
+}
diff --git a/src/lib/test-ostream-failure-at.c b/src/lib/test-ostream-failure-at.c
new file mode 100644
index 0000000..ba17966
--- /dev/null
+++ b/src/lib/test-ostream-failure-at.c
@@ -0,0 +1,52 @@
+/* Copyright (c) 2015-2018 Dovecot authors, see the included COPYING file */
+
+#include "test-lib.h"
+#include "buffer.h"
+#include "ostream.h"
+#include "ostream-failure-at.h"
+
+#define TEST_DATA_LENGTH 128
+#define TEST_ERRMSG "test-ostream-failure-at error triggered"
+
+void test_ostream_failure_at(void)
+{
+ unsigned char test_data[TEST_DATA_LENGTH];
+ struct ostream *output, *buf_output;
+ buffer_t *buf = t_buffer_create(256);
+ unsigned int i;
+
+ test_begin("ostream failure at");
+ for (i = 0; i < sizeof(test_data); i++)
+ test_data[i] = i;
+ for (i = 0; i < TEST_DATA_LENGTH; i++) {
+ buf_output = o_stream_create_buffer(buf);
+ output = o_stream_create_failure_at(buf_output, i, TEST_ERRMSG);
+ if (i > 0)
+ test_assert(o_stream_send(output, test_data, sizeof(test_data)) == (int)i);
+ test_assert_idx(o_stream_send(output, test_data, sizeof(test_data)) == -1 &&
+ output->offset == i &&
+ output->stream_errno == EIO &&
+ strcmp(o_stream_get_error(output), TEST_ERRMSG) == 0, i);
+ o_stream_destroy(&output);
+ o_stream_destroy(&buf_output);
+ }
+ /* shouldn't fail */
+ buf_output = o_stream_create_buffer(buf);
+ output = o_stream_create_failure_at(buf_output, TEST_DATA_LENGTH, TEST_ERRMSG);
+ test_assert(o_stream_send(output, test_data, sizeof(test_data)) == TEST_DATA_LENGTH);
+ test_assert(o_stream_flush(output) > 0 &&
+ output->offset == TEST_DATA_LENGTH &&
+ output->stream_errno == 0);
+ o_stream_destroy(&output);
+ o_stream_destroy(&buf_output);
+
+ /* fail at flush */
+ buf_output = o_stream_create_buffer(buf);
+ output = o_stream_create_failure_at_flush(buf_output, TEST_ERRMSG);
+ test_assert(o_stream_send(output, test_data, sizeof(test_data)) == TEST_DATA_LENGTH);
+ test_assert(o_stream_flush(output) < 0 && output->stream_errno == EIO &&
+ strcmp(o_stream_get_error(output), TEST_ERRMSG) == 0);
+ o_stream_destroy(&output);
+ o_stream_destroy(&buf_output);
+ test_end();
+}
diff --git a/src/lib/test-ostream-file.c b/src/lib/test-ostream-file.c
new file mode 100644
index 0000000..d4a9eff
--- /dev/null
+++ b/src/lib/test-ostream-file.c
@@ -0,0 +1,189 @@
+/* Copyright (c) 2009-2018 Dovecot authors, see the included COPYING file */
+
+#include "test-lib.h"
+#include "net.h"
+#include "str.h"
+#include "safe-mkstemp.h"
+#include "randgen.h"
+#include "istream.h"
+#include "ostream.h"
+
+#include <fcntl.h>
+#include <unistd.h>
+
+#define MAX_BUFSIZE 256
+
+static void test_ostream_file_random_once(void)
+{
+ struct ostream *output;
+ string_t *path = t_str_new(128);
+ char buf[MAX_BUFSIZE*4], buf2[MAX_BUFSIZE*4], randbuf[MAX_BUFSIZE];
+ unsigned int i, offset, size;
+ ssize_t ret;
+ int fd;
+
+ memset(buf, 0, sizeof(buf));
+ fd = safe_mkstemp(path, 0600, (uid_t)-1, (gid_t)-1);
+ if (fd == -1)
+ i_fatal("safe_mkstemp(%s) failed: %m", str_c(path));
+ i_unlink(str_c(path));
+ output = o_stream_create_fd(fd, MAX_BUFSIZE);
+ o_stream_cork(output);
+
+ size = i_rand_minmax(1, MAX_BUFSIZE);
+ random_fill(randbuf, size);
+ memcpy(buf, randbuf, size);
+ test_assert(o_stream_send(output, buf, size) > 0);
+
+ for (i = 0; i < 10; i++) {
+ offset = i_rand_limit(MAX_BUFSIZE * 3);
+ size = i_rand_minmax(1, MAX_BUFSIZE);
+ random_fill(randbuf, size);
+ memcpy(buf + offset, randbuf, size);
+ test_assert(o_stream_pwrite(output, randbuf, size, offset) == 0);
+ if (i_rand_limit(10) == 0)
+ test_assert(o_stream_flush(output) > 0);
+ }
+
+ o_stream_uncork(output);
+ test_assert(o_stream_finish(output) > 0);
+ ret = pread(fd, buf2, sizeof(buf2), 0);
+ if (ret < 0)
+ i_fatal("pread() failed: %m");
+ else {
+ i_assert(ret > 0);
+ test_assert(memcmp(buf, buf2, ret) == 0);
+ }
+ o_stream_unref(&output);
+ i_close_fd(&fd);
+}
+
+static void test_ostream_file_random(void)
+{
+ unsigned int i;
+
+ test_begin("ostream pwrite random");
+ for (i = 0; i < 100; i++) T_BEGIN {
+ test_ostream_file_random_once();
+ } T_END;
+ test_end();
+}
+
+static void test_ostream_file_send_istream_file(void)
+{
+ struct istream *input, *input2;
+ struct ostream *output;
+ char buf[10];
+ int fd;
+
+ test_begin("ostream file send istream file");
+
+ /* temp file istream */
+ fd = open(".temp.istream", O_RDWR | O_CREAT | O_TRUNC, 0600);
+ if (fd == -1)
+ i_fatal("creat(.temp.istream) failed: %m");
+ test_assert(write(fd, "1234567890", 10) == 10);
+ test_assert(lseek(fd, 0, SEEK_SET) == 0);
+ input = i_stream_create_fd_autoclose(&fd, 1024);
+
+ /* temp file ostream */
+ fd = open(".temp.ostream", O_RDWR | O_CREAT | O_TRUNC, 0600);
+ if (fd == -1)
+ i_fatal("creat(.temp.ostream) failed: %m");
+ output = o_stream_create_fd(fd, 0);
+
+ /* test that writing works between two files */
+ i_stream_seek(input, 3);
+ input2 = i_stream_create_limit(input, 4);
+ test_assert(o_stream_send_istream(output, input2) == OSTREAM_SEND_ISTREAM_RESULT_FINISHED);
+ test_assert(output->offset == 4);
+ test_assert(pread(fd, buf, sizeof(buf), 0) == 4 &&
+ memcmp(buf, "4567", 4) == 0);
+ i_stream_unref(&input2);
+
+ /* test that writing works within the same file */
+ i_stream_destroy(&input);
+
+ input = i_stream_create_fd(fd, 1024);
+ /* forwards: 4567 -> 4677 */
+ o_stream_seek(output, 1);
+ i_stream_seek(input, 2);
+ input2 = i_stream_create_limit(input, 2);
+ test_assert(o_stream_send_istream(output, input2) == OSTREAM_SEND_ISTREAM_RESULT_FINISHED);
+ test_assert(output->offset == 3);
+ test_assert(pread(fd, buf, sizeof(buf), 0) == 4 &&
+ memcmp(buf, "4677", 4) == 0);
+ i_stream_destroy(&input2);
+ i_stream_destroy(&input);
+
+ /* backwards: 1234 -> 11234 */
+ memcpy(buf, "1234", 4);
+ test_assert(pwrite(fd, buf, 4, 0) == 4);
+ input = i_stream_create_fd(fd, 1024);
+ o_stream_seek(output, 1);
+ test_assert(o_stream_send_istream(output, input) == OSTREAM_SEND_ISTREAM_RESULT_FINISHED);
+ test_assert(output->offset == 5);
+ test_assert(pread(fd, buf, sizeof(buf), 0) == 5 &&
+ memcmp(buf, "11234", 5) == 0);
+ i_stream_destroy(&input);
+
+ o_stream_destroy(&output);
+ i_close_fd(&fd);
+
+ i_unlink(".temp.istream");
+ i_unlink(".temp.ostream");
+ test_end();
+}
+
+static void test_ostream_file_send_istream_sendfile(void)
+{
+ struct istream *input, *input2;
+ struct ostream *output;
+ char buf[10];
+ int fd, sock_fd[2];
+
+ test_begin("ostream file send istream sendfile()");
+
+ /* temp file istream */
+ fd = open(".temp.istream", O_RDWR | O_CREAT | O_TRUNC, 0600);
+ if (fd == -1)
+ i_fatal("creat(.temp.istream) failed: %m");
+ test_assert(write(fd, "abcdefghij", 10) == 10);
+ test_assert(lseek(fd, 0, SEEK_SET) == 0);
+ input = i_stream_create_fd_autoclose(&fd, 1024);
+
+ /* temp socket ostream */
+ i_assert(socketpair(AF_UNIX, SOCK_STREAM, 0, sock_fd) == 0);
+ output = o_stream_create_fd_autoclose(sock_fd, 0);
+
+ /* test that sendfile() works */
+ i_stream_seek(input, 3);
+ input2 = i_stream_create_limit(input, 4);
+ test_assert(o_stream_send_istream(output, input2) == OSTREAM_SEND_ISTREAM_RESULT_FINISHED);
+ test_assert(output->offset == 4);
+ test_assert(read(sock_fd[1], buf, sizeof(buf)) == 4 &&
+ memcmp(buf, "defg", 4) == 0);
+ i_stream_unref(&input2);
+
+ /* test reading past EOF */
+ i_stream_seek(input, 0);
+ input2 = i_stream_create_limit(input, 20);
+ test_assert(o_stream_send_istream(output, input2) == OSTREAM_SEND_ISTREAM_RESULT_FINISHED);
+ test_assert(input2->v_offset == 10);
+ test_assert(output->offset == 14);
+ i_stream_unref(&input2);
+
+ i_stream_unref(&input);
+ o_stream_destroy(&output);
+ i_close_fd(&sock_fd[1]);
+
+ i_unlink(".temp.istream");
+ test_end();
+}
+
+void test_ostream_file(void)
+{
+ test_ostream_file_random();
+ test_ostream_file_send_istream_file();
+ test_ostream_file_send_istream_sendfile();
+}
diff --git a/src/lib/test-ostream-multiplex.c b/src/lib/test-ostream-multiplex.c
new file mode 100644
index 0000000..d5b7ac1
--- /dev/null
+++ b/src/lib/test-ostream-multiplex.c
@@ -0,0 +1,405 @@
+/* Copyright (c) 2017-2018 Dovecot authors, see the included COPYING file */
+
+#include "test-lib.h"
+#include "ioloop.h"
+#include "str.h"
+#include "istream.h"
+#include "ostream-private.h"
+#include "istream-multiplex.h"
+#include "ostream-multiplex.h"
+#include "ostream.h"
+#include <unistd.h>
+
+#include "hex-binary.h"
+
+static void test_ostream_multiplex_simple(void)
+{
+ test_begin("ostream multiplex (simple)");
+
+ const unsigned char expected[] = {
+ '\x00','\x00','\x00','\x00','\x05','\x68','\x65',
+ '\x6c','\x6c','\x6f','\x01','\x00','\x00','\x00',
+ '\x05','\x77','\x6f','\x72','\x6c','\x64'
+ };
+
+ buffer_t *result = t_str_new(64);
+ struct ostream *os = test_ostream_create(result);
+ struct ostream *os2 = o_stream_create_multiplex(os, SIZE_MAX);
+ struct ostream *os3 = o_stream_multiplex_add_channel(os2, 1);
+
+ test_assert(o_stream_send_str(os2, "hello") == 5);
+ test_assert(o_stream_send_str(os3, "world") == 5);
+
+ o_stream_unref(&os3);
+ o_stream_unref(&os2);
+
+ test_assert(o_stream_finish(os) == 1);
+ o_stream_unref(&os);
+
+ test_assert(sizeof(expected) == result->used);
+ test_assert(memcmp(result->data, expected, I_MIN(sizeof(expected),
+ result->used)) == 0);
+
+ test_end();
+}
+
+static unsigned int channel_counter[2] = {0, 0};
+static struct ostream *chan0, *chan1;
+
+static const char *msgs[] = {
+ "",
+ "a",
+ "bb",
+ "ccc",
+ "dddd",
+ "eeeee",
+ "ffffff"
+};
+
+static void test_ostream_multiplex_stream_read(struct istream *is)
+{
+ uint8_t cid;
+ const unsigned char *data;
+ size_t siz,dlen=0,pos=0;
+
+ if (i_stream_read_more(is, &data, &siz)>0) {
+ /* parse stream */
+ for(;pos<siz;) {
+ if (dlen > 0) {
+ if (dlen < N_ELEMENTS(msgs)) {
+ test_assert_idx(memcmp(&data[pos],
+ msgs[dlen], dlen)==0,
+ channel_counter[data[0] % 2]);
+ }
+ channel_counter[data[0] % 2]++;
+ pos += dlen;
+ dlen = 0;
+ } else if (dlen == 0) {
+ cid = data[pos] % 2;
+ test_assert_idx(data[pos] < 2, channel_counter[cid]);
+ pos++;
+ dlen = be32_to_cpu_unaligned(&data[pos]);
+ pos += 4;
+ test_assert(dlen > 0 && dlen < N_ELEMENTS(msgs));
+ }
+ }
+ i_stream_skip(is, siz);
+ }
+
+ if (channel_counter[0] > 100 && channel_counter[1] > 100)
+ io_loop_stop(current_ioloop);
+}
+
+static void test_ostream_multiplex_stream_write(struct ostream *channel ATTR_UNUSED)
+{
+ size_t rounds = 1 + i_rand_limit(10);
+ for(size_t i = 0; i < rounds; i++) {
+ if ((i_rand_limit(2)) != 0) {
+ o_stream_cork(chan1);
+ /* send one byte at a time */
+ for(const char *p = msgs[i_rand_limit(N_ELEMENTS(msgs))];
+ *p != '\0'; p++) {
+ o_stream_nsend(chan1, p, 1);
+ }
+ o_stream_uncork(chan1);
+ } else {
+ o_stream_nsend_str(chan0,
+ msgs[i_rand_limit(N_ELEMENTS(msgs))]);
+ }
+ }
+}
+
+static void test_ostream_multiplex_stream(void)
+{
+ test_begin("ostream multiplex (stream)");
+
+ struct ioloop *ioloop = io_loop_create();
+ io_loop_set_current(ioloop);
+
+ int fds[2];
+ test_assert(pipe(fds) == 0);
+ fd_set_nonblock(fds[0], TRUE);
+ fd_set_nonblock(fds[1], TRUE);
+ struct ostream *os = o_stream_create_fd(fds[1], SIZE_MAX);
+ struct istream *is = i_stream_create_fd(fds[0], SIZE_MAX);
+
+ chan0 = o_stream_create_multiplex(os, SIZE_MAX);
+ chan1 = o_stream_multiplex_add_channel(chan0, 1);
+
+ struct io *io0 =
+ io_add_istream(is, test_ostream_multiplex_stream_read, is);
+ struct io *io1 =
+ io_add(fds[1], IO_WRITE, test_ostream_multiplex_stream_write, os);
+
+ io_loop_run(current_ioloop);
+
+ io_remove(&io0);
+ io_remove(&io1);
+
+ test_assert(o_stream_finish(chan1) > 0);
+ o_stream_unref(&chan1);
+ test_assert(o_stream_finish(chan0) > 0);
+ o_stream_unref(&chan0);
+
+ i_stream_unref(&is);
+ o_stream_unref(&os);
+
+ io_loop_destroy(&ioloop);
+
+ i_close_fd(&fds[0]);
+ i_close_fd(&fds[1]);
+
+ test_end();
+}
+
+static void test_ostream_multiplex_cork(void)
+{
+ test_begin("ostream multiplex (corking)");
+ buffer_t *output = t_buffer_create(128);
+ struct ostream *os = test_ostream_create(output);
+ struct ostream *chan0 = o_stream_create_multiplex(os, SIZE_MAX);
+
+ const struct const_iovec iov[] = {
+ { "hello", 5 },
+ { " ", 1 },
+ { "world", 5 },
+ { "!", 1 }
+ };
+
+ /* send data in parts, expect to see single blob */
+ o_stream_cork(chan0);
+ o_stream_nsendv(chan0, iov, N_ELEMENTS(iov));
+ o_stream_uncork(chan0);
+ test_assert(o_stream_flush(os) == 1);
+
+ /* check output */
+ test_assert(memcmp(output->data, "\0\0\0\0\f", 5) == 0);
+ test_assert(strcmp(str_c(output)+5, "hello world!") == 0);
+
+ test_assert(o_stream_finish(chan0) > 0);
+ o_stream_unref(&chan0);
+ o_stream_unref(&os);
+
+ test_end();
+}
+
+struct test_hang_context {
+ struct istream *input1, *input2;
+ size_t sent_bytes, sent2_bytes;
+ size_t read_bytes, read2_bytes;
+};
+
+static void test_hang_input(struct test_hang_context *ctx)
+{
+ ssize_t ret, ret2;
+
+ do {
+ ret = i_stream_read(ctx->input1);
+ if (ret > 0) {
+ i_stream_skip(ctx->input1, ret);
+ ctx->read_bytes += ret;
+ }
+ ret2 = i_stream_read(ctx->input2);
+ if (ret2 > 0) {
+ i_stream_skip(ctx->input2, ret2);
+ ctx->read2_bytes += ret2;
+ }
+ } while (ret > 0 || ret2 > 0);
+
+ test_assert(ret == 0 && ret2 == 0);
+ if (ctx->read_bytes == ctx->sent_bytes &&
+ ctx->read2_bytes == ctx->sent2_bytes)
+ io_loop_stop(current_ioloop);
+}
+
+static void test_ostream_multiplex_hang(void)
+{
+ int fd[2];
+
+ test_begin("ostream multiplex hang");
+ if (pipe(fd) < 0)
+ i_fatal("pipe() failed: %m");
+ fd_set_nonblock(fd[0], TRUE);
+ fd_set_nonblock(fd[1], TRUE);
+
+ struct ioloop *ioloop = io_loop_create();
+ struct ostream *file_output = o_stream_create_fd(fd[1], 1024);
+ o_stream_set_no_error_handling(file_output, TRUE);
+ struct ostream *channel = o_stream_create_multiplex(file_output, 4096);
+ struct ostream *channel2 = o_stream_multiplex_add_channel(channel, 1);
+ char buf[256];
+
+ /* send multiplex output until the buffer is full */
+ ssize_t ret, ret2;
+ size_t sent_bytes = 0, sent2_bytes = 0;
+ i_zero(&buf);
+ o_stream_cork(channel);
+ o_stream_cork(channel2);
+ while ((ret = o_stream_send(channel, buf, sizeof(buf))) > 0) {
+ sent_bytes += ret;
+ ret2 = o_stream_send(channel2, buf, sizeof(buf));
+ if (ret2 <= 0)
+ break;
+ sent2_bytes += ret2;
+ }
+ test_assert(o_stream_finish(channel) == 0);
+ test_assert(o_stream_finish(channel2) == 0);
+ o_stream_uncork(channel);
+ o_stream_uncork(channel2);
+ /* We expect the first channel to have data buffered */
+ test_assert(o_stream_get_buffer_used_size(channel) >=
+ o_stream_get_buffer_used_size(file_output));
+ test_assert(o_stream_get_buffer_used_size(channel) -
+ o_stream_get_buffer_used_size(file_output) > 0);
+
+ /* read everything that was already sent */
+ struct istream *file_input = i_stream_create_fd(fd[0], 1024);
+ struct istream *input = i_stream_create_multiplex(file_input, 4096);
+ struct istream *input2 = i_stream_multiplex_add_channel(input, 1);
+
+ struct test_hang_context ctx = {
+ .input1 = input,
+ .input2 = input2,
+ .sent_bytes = sent_bytes,
+ .sent2_bytes = sent2_bytes,
+ };
+
+ struct timeout *to = timeout_add(5000, io_loop_stop, current_ioloop);
+ struct io *io = io_add_istream(file_input, test_hang_input, &ctx);
+ io_loop_run(ioloop);
+ io_remove(&io);
+ timeout_remove(&to);
+
+ /* everything that was sent should have been received now.
+ ostream-multiplex's internal buffer is also supposed to have
+ been sent. */
+ test_assert(input->v_offset == sent_bytes);
+ test_assert(input2->v_offset == sent2_bytes);
+ test_assert(o_stream_get_buffer_used_size(channel) == 0);
+ test_assert(o_stream_get_buffer_used_size(channel2) == 0);
+
+ i_stream_unref(&file_input);
+ i_stream_unref(&input);
+ i_stream_unref(&input2);
+ o_stream_unref(&channel);
+ o_stream_unref(&channel2);
+ o_stream_unref(&file_output);
+ io_loop_destroy(&ioloop);
+ test_end();
+}
+
+#define FLUSH_CALLBACK_TOTAL_BYTES 10240
+
+struct test_flush_context {
+ struct ostream *output1, *output2;
+ struct istream *input1, *input2;
+};
+
+static int flush_callback1(struct test_flush_context *ctx)
+{
+ char buf[32];
+
+ i_assert(ctx->output1->offset <= FLUSH_CALLBACK_TOTAL_BYTES);
+ size_t bytes_left = FLUSH_CALLBACK_TOTAL_BYTES - ctx->output1->offset;
+
+ memset(buf, '1', sizeof(buf));
+ if (o_stream_send(ctx->output1, buf, I_MIN(sizeof(buf), bytes_left)) < 0)
+ return -1;
+ return ctx->output1->offset < FLUSH_CALLBACK_TOTAL_BYTES ? 0 : 1;
+}
+
+static int flush_callback2(struct test_flush_context *ctx)
+{
+ char buf[64];
+
+ i_assert(ctx->output2->offset <= FLUSH_CALLBACK_TOTAL_BYTES);
+ size_t bytes_left = FLUSH_CALLBACK_TOTAL_BYTES - ctx->output2->offset;
+
+ memset(buf, '2', sizeof(buf));
+ if (o_stream_send(ctx->output2, buf, I_MIN(sizeof(buf), bytes_left)) < 0)
+ return -1;
+ return ctx->output2->offset < FLUSH_CALLBACK_TOTAL_BYTES ? 0 : 1;
+}
+
+static void test_flush_input(struct test_flush_context *ctx)
+{
+ ssize_t ret, ret2;
+
+ do {
+ ret = i_stream_read(ctx->input1);
+ if (ret > 0)
+ i_stream_skip(ctx->input1, ret);
+ ret2 = i_stream_read(ctx->input2);
+ if (ret2 > 0)
+ i_stream_skip(ctx->input2, ret2);
+ } while (ret > 0 || ret2 > 0);
+
+ test_assert(ret == 0 && ret2 == 0);
+ if (ctx->input1->v_offset == FLUSH_CALLBACK_TOTAL_BYTES &&
+ ctx->input2->v_offset == FLUSH_CALLBACK_TOTAL_BYTES)
+ io_loop_stop(current_ioloop);
+}
+
+static void test_ostream_multiplex_flush_callback(void)
+{
+ int fd[2];
+
+ test_begin("ostream multiplex flush callback");
+ if (pipe(fd) < 0)
+ i_fatal("pipe() failed: %m");
+ fd_set_nonblock(fd[0], TRUE);
+ fd_set_nonblock(fd[1], TRUE);
+
+ struct ioloop *ioloop = io_loop_create();
+ struct ostream *file_output = o_stream_create_fd(fd[1], 1024);
+ o_stream_set_no_error_handling(file_output, TRUE);
+ struct ostream *channel = o_stream_create_multiplex(file_output, 4096);
+ struct ostream *channel2 = o_stream_multiplex_add_channel(channel, 1);
+
+ struct istream *file_input = i_stream_create_fd(fd[0], 1024);
+ struct istream *input = i_stream_create_multiplex(file_input, 4096);
+ struct istream *input2 = i_stream_multiplex_add_channel(input, 1);
+
+ struct test_flush_context ctx = {
+ .output1 = channel,
+ .output2 = channel2,
+ .input1 = input,
+ .input2 = input2,
+ };
+ o_stream_set_flush_callback(channel, flush_callback1, &ctx);
+ o_stream_set_flush_callback(channel2, flush_callback2, &ctx);
+ o_stream_set_flush_pending(channel, TRUE);
+ o_stream_set_flush_pending(channel2, TRUE);
+
+ struct timeout *to = timeout_add(5000, io_loop_stop, current_ioloop);
+ struct io *io = io_add_istream(file_input, test_flush_input, &ctx);
+ io_loop_run(ioloop);
+ io_remove(&io);
+ timeout_remove(&to);
+
+ test_assert(channel->offset == FLUSH_CALLBACK_TOTAL_BYTES);
+ test_assert(channel2->offset == FLUSH_CALLBACK_TOTAL_BYTES);
+ test_assert(input->v_offset == FLUSH_CALLBACK_TOTAL_BYTES);
+ test_assert(input2->v_offset == FLUSH_CALLBACK_TOTAL_BYTES);
+
+ test_assert(o_stream_finish(channel) == 1);
+ test_assert(o_stream_finish(channel2) == 1);
+
+ i_stream_unref(&file_input);
+ i_stream_unref(&input);
+ i_stream_unref(&input2);
+ o_stream_unref(&channel);
+ o_stream_unref(&channel2);
+ o_stream_unref(&file_output);
+ io_loop_destroy(&ioloop);
+ test_end();
+}
+
+void test_ostream_multiplex(void)
+{
+ test_ostream_multiplex_simple();
+ test_ostream_multiplex_stream();
+ test_ostream_multiplex_cork();
+ test_ostream_multiplex_hang();
+ test_ostream_multiplex_flush_callback();
+}
diff --git a/src/lib/test-path-util.c b/src/lib/test-path-util.c
new file mode 100644
index 0000000..c5d4122
--- /dev/null
+++ b/src/lib/test-path-util.c
@@ -0,0 +1,278 @@
+/* Copyright (c) 2016-2018 Dovecot authors, see the included COPYING file */
+
+#include "test-lib.h"
+#include "path-util.h"
+#include "unlink-directory.h"
+#include "str.h"
+
+#include <unistd.h>
+#include <fcntl.h>
+#include <stdlib.h>
+#include <sys/stat.h>
+
+#define TEMP_DIRNAME ".test-path-util"
+
+static const char *tmpdir;
+static const char *cwd;
+static const char *link1;
+static const char *link2;
+static const char *link3;
+static const char *link4;
+
+static void test_local_path(void)
+{
+ const char *expected = t_strconcat(cwd, "/README.md", NULL);
+ const char *npath = NULL, *error = NULL;
+ test_assert(t_normpath_to("README.md", cwd, &npath, &error) == 0);
+ test_assert_strcmp(npath, expected);
+}
+
+static void test_absolute_path_no_change(void)
+{
+ const char *npath = NULL, *error = NULL;
+ test_assert(t_normpath_to("/", "/", &npath, &error) == 0);
+ test_assert_strcmp(npath, "/");
+
+ test_assert(t_normpath_to(cwd, cwd, &npath, &error) == 0);
+ test_assert_strcmp(npath, cwd);
+}
+
+static int path_height(const char *p)
+{
+ int n;
+ for (n = 0; *p != '\0'; ++p)
+ n += *p == '/';
+ return n;
+}
+
+static void test_travel_to_root(void)
+{
+ int l = path_height(cwd);
+ const char *npath = cwd;
+ for (npath = cwd; l != 0; l--) {
+ const char *error;
+ test_assert_idx(t_normpath_to("../", npath, &npath, &error) == 0, l);
+ }
+ test_assert_strcmp(npath, "/");
+}
+
+static void test_extra_slashes(void)
+{
+ const char *npath = NULL, *error = NULL;
+ test_assert(t_normpath_to(".", cwd, &npath, &error) == 0);
+ test_assert_strcmp(npath, cwd);
+
+ test_assert(t_normpath_to("./", cwd, &npath, &error) == 0);
+ test_assert_strcmp(npath, cwd);
+
+ test_assert(t_normpath_to(".////", cwd, &npath, &error) == 0);
+ test_assert_strcmp(npath, cwd);
+}
+
+static void test_nonexistent_path(void)
+{
+ const char *npath = NULL, *error = NULL;
+ const char *expected = t_strconcat(cwd, "/nonexistent", NULL);
+ test_assert(t_normpath_to("nonexistent", cwd, &npath, &error) == 0);
+ test_assert_strcmp(npath, expected);
+ test_assert(t_realpath_to("nonexistent", cwd, &npath, &error) == -1);
+ test_assert(error != NULL);
+}
+
+static void test_relative_dotdot(void)
+{
+ const char *rel_path = "../"TEMP_DIRNAME;
+ const char *npath = NULL, *error = NULL;
+ test_assert(t_normpath_to(rel_path, tmpdir, &npath, &error) == 0);
+ test_assert_strcmp(npath, tmpdir);
+
+ test_assert(t_normpath_to("..", tmpdir, &npath, &error) == 0);
+ test_assert_strcmp(npath, cwd);
+
+ test_assert(t_normpath_to("../", tmpdir, &npath, &error) == 0);
+ test_assert_strcmp(npath, cwd);
+
+ test_assert(t_normpath_to("../.", tmpdir, &npath, &error) == 0);
+ test_assert_strcmp(npath, cwd);
+}
+
+static void test_link1(void)
+{
+ const char *old_dir, *npath = NULL, *error = NULL;
+ test_assert(t_realpath_to(link1, "/", &npath, &error) == 0);
+ test_assert_strcmp(npath, tmpdir);
+
+ /* .../link1/link1/child */
+ test_assert(t_realpath_to(t_strconcat(link1, "/link1/child", NULL),
+ "/", &npath, &error) == 0);
+ test_assert_strcmp(npath, t_strconcat(tmpdir, "/child", NULL));
+
+ /* relative link1/link1/child */
+ if (t_get_working_dir(&old_dir, &error) < 0)
+ i_fatal("t_get_working_dir() failed: %s", error);
+ if (chdir(tmpdir) < 0)
+ i_fatal("chdir(%s) failed: %m", tmpdir);
+ test_assert(t_realpath(t_strconcat("link1", "/link1/child", NULL),
+ &npath, &error) == 0);
+ if (chdir(old_dir) < 0)
+ i_fatal("chdir(%s) failed: %m", old_dir);
+}
+
+static void test_link4(void)
+{
+ const char *npath = NULL, *error = NULL;
+
+ test_assert(t_realpath_to(t_strconcat(link1, "/link4/child", NULL),
+ "/", &npath, &error) == 0);
+ test_assert_strcmp(npath, t_strconcat(tmpdir, "/child", NULL));
+}
+
+static void test_link_loop(void)
+{
+ const char *npath = NULL, *error = NULL;
+ errno = 0;
+ test_assert(t_realpath_to(link2, "/", &npath, &error) == -1);
+ test_assert(errno == ELOOP);
+ test_assert(error != NULL);
+}
+
+static void test_abspath_vs_normpath(void)
+{
+ const char *abs = t_abspath_to("../../bin", "/usr/lib/");
+ test_assert_strcmp(abs, "/usr/lib//../../bin");
+
+ const char *norm = NULL, *error = NULL;
+ test_assert(t_normpath_to("../../bin", "/usr///lib/", &norm, &error) == 0);
+ test_assert_strcmp(norm, "/bin");
+}
+
+static void create_links(const char *tmpdir)
+{
+ link1 = t_strconcat(tmpdir, "/link1", NULL);
+ if (symlink(tmpdir, link1) < 0)
+ i_fatal("symlink(%s, %s) failed: %m", tmpdir, link1);
+
+ const char *link1_child = t_strconcat(link1, "/child", NULL);
+ int fd = creat(link1_child, 0600);
+ if (fd == -1)
+ i_fatal("creat(%s) failed: %m", link1_child);
+ i_close_fd(&fd);
+
+ /* link2 and link3 point to each other to create a loop */
+ link2 = t_strconcat(tmpdir, "/link2", NULL);
+ link3 = t_strconcat(tmpdir, "/link3", NULL);
+ if (symlink(link3, link2) < 0)
+ i_fatal("symlink(%s, %s) failed: %m", link3, link2);
+ if (symlink(link2, link3) < 0)
+ i_fatal("symlink(%s, %s) failed: %m", link2, link3);
+
+ /* link4 points to link1 */
+ link4 = t_strconcat(tmpdir, "/link4", NULL);
+ if (symlink("link1", link4) < 0)
+ i_fatal("symlink(link1, %s) failed: %m", link4);
+}
+
+static void test_link_alloc(void)
+{
+#define COMPONENT_COMPONENT "/component-component"
+ const char *o_tmpdir;
+
+ /* idea here is to make sure component-component
+ would optimally hit to the nearest_power value.
+
+ it has to be big enough to cause requirement for
+ allocation in t_realpath. */
+ string_t *basedir = t_str_new(256);
+ str_append(basedir, cwd);
+ str_append(basedir, "/"TEMP_DIRNAME);
+ size_t len = nearest_power(I_MAX(127, str_len(basedir) + strlen(COMPONENT_COMPONENT) + 1)) -
+ strlen(COMPONENT_COMPONENT);
+
+ while(str_len(basedir) < len) {
+ str_append(basedir, COMPONENT_COMPONENT);
+ (void)mkdir(str_c(basedir), 0700);
+ }
+ o_tmpdir = tmpdir;
+ tmpdir = str_c(basedir);
+
+ create_links(tmpdir);
+
+ test_link1();
+ test_link_loop();
+
+ tmpdir = o_tmpdir;
+}
+
+static void test_link_alloc2(void)
+{
+ const char *o_tmpdir;
+
+ /* try enough different sized base directory lengths so the code
+ hits the different reallocations and tests for off-by-one errors */
+ string_t *basedir = t_str_new(256);
+ str_append(basedir, cwd);
+ str_append(basedir, "/"TEMP_DIRNAME);
+ str_append_c(basedir, '/');
+ size_t base_len = str_len(basedir);
+
+ o_tmpdir = tmpdir;
+ /* path_normalize() initially allocates 128 bytes, so we'll test paths
+ up to that length+1. */
+ unsigned char buf[128+1];
+ memset(buf, 'x', sizeof(buf));
+ for (size_t i = 1; i <= sizeof(buf); i++) {
+ str_truncate(basedir, base_len);
+ str_append_data(basedir, buf, i);
+ tmpdir = str_c(basedir);
+ (void)mkdir(str_c(basedir), 0700);
+
+ create_links(tmpdir);
+ test_link1();
+ test_link_loop();
+ }
+ tmpdir = o_tmpdir;
+}
+
+static void test_cleanup(void)
+{
+ const char *error;
+
+ if (unlink_directory(tmpdir, UNLINK_DIRECTORY_FLAG_RMDIR, &error) < 0)
+ i_error("unlink_directory() failed: %s", error);
+}
+
+static void test_init(void)
+{
+ const char *error;
+ test_assert(t_get_working_dir(&cwd, &error) == 0);
+ tmpdir = t_strconcat(cwd, "/"TEMP_DIRNAME, NULL);
+
+ test_cleanup();
+ if (mkdir(tmpdir, 0700) < 0) {
+ i_fatal("mkdir: %m");
+ }
+
+ create_links(tmpdir);
+}
+
+void test_path_util(void)
+{
+ test_begin("test_path_util");
+ alarm(20);
+ test_init();
+ test_local_path();
+ test_absolute_path_no_change();
+ test_travel_to_root();
+ test_extra_slashes();
+ test_nonexistent_path();
+ test_relative_dotdot();
+ test_link1();
+ test_link4();
+ test_link_loop();
+ test_abspath_vs_normpath();
+ test_link_alloc();
+ test_link_alloc2();
+ test_cleanup();
+ alarm(0);
+ test_end();
+}
diff --git a/src/lib/test-pkcs5.c b/src/lib/test-pkcs5.c
new file mode 100644
index 0000000..c826eea
--- /dev/null
+++ b/src/lib/test-pkcs5.c
@@ -0,0 +1,49 @@
+/* Copyright (c) 2007-2018 Dovecot authors, see the included COPYING file */
+
+#include "test-lib.h"
+#include "str.h"
+#include "buffer.h"
+#include "hash-method.h"
+#include "pkcs5.h"
+
+struct test_vector {
+ const char *prf;
+ unsigned char *p;
+ size_t pLen;
+ unsigned char *s;
+ size_t sLen;
+ unsigned int i;
+ unsigned char *dk;
+ size_t dkLen;
+};
+
+#define TEST_BUF(x) (unsigned char*)x, sizeof(x)-1
+
+/* RFC 6070 test vectors */
+static const struct test_vector test_vectors_v2[] = {
+ { "sha1", TEST_BUF("password"), TEST_BUF("salt"), 1, TEST_BUF("\x0c\x60\xc8\x0f\x96\x1f\x0e\x71\xf3\xa9\xb5\x24\xaf\x60\x12\x06\x2f\xe0\x37\xa6") },
+ { "sha1", TEST_BUF("password"), TEST_BUF("salt"), 2, TEST_BUF("\xea\x6c\x01\x4d\xc7\x2d\x6f\x8c\xcd\x1e\xd9\x2a\xce\x1d\x41\xf0\xd8\xde\x89\x57") },
+ { "sha1", TEST_BUF("password"), TEST_BUF("salt"), 4096, TEST_BUF("\x4b\x00\x79\x01\xb7\x65\x48\x9a\xbe\xad\x49\xd9\x26\xf7\x21\xd0\x65\xa4\x29\xc1") },
+/* enable the next test only when you need it, it takes quite long time */
+/* { "sha1", TEST_BUF("password"), TEST_BUF("salt"), 16777216, TEST_BUF("\xee\xfe\x3d\x61\xcd\x4d\xa4\xe4\xe9\x94\x5b\x3d\x6b\xa2\x15\x8c\x26\x34\xe9\x84") }, */
+ { "sha1", TEST_BUF("passwordPASSWORDpassword"), TEST_BUF("saltSALTsaltSALTsaltSALTsaltSALTsalt"), 4096, TEST_BUF("\x3d\x2e\xec\x4f\xe4\x1c\x84\x9b\x80\xc8\xd8\x36\x62\xc0\xe4\x4a\x8b\x29\x1a\x96\x4c\xf2\xf0\x70\x38") },
+ { "sha1", TEST_BUF("pass\0word"), TEST_BUF("sa\0lt"), 4096, TEST_BUF("\x56\xfa\x6a\xa7\x55\x48\x09\x9d\xcc\x37\xd7\xf0\x34\x25\xe0\xc3") }
+};
+
+void test_pkcs5_pbkdf2(void)
+{
+ buffer_t *res = buffer_create_dynamic(default_pool, 25);
+
+ test_begin("pkcs5_pbkdf2");
+
+ for(size_t i = 0; i < N_ELEMENTS(test_vectors_v2); i++) {
+ buffer_set_used_size(res, 0);
+ const struct test_vector *vec = &(test_vectors_v2[i]);
+ pkcs5_pbkdf(PKCS5_PBKDF2, hash_method_lookup(vec->prf), vec->p, vec->pLen, vec->s, vec->sLen, vec->i, vec->dkLen, res);
+ test_assert_idx(memcmp(res->data, vec->dk, vec->dkLen) == 0, i);
+ }
+
+ buffer_free(&res);
+
+ test_end();
+}
diff --git a/src/lib/test-primes.c b/src/lib/test-primes.c
new file mode 100644
index 0000000..fc792b4
--- /dev/null
+++ b/src/lib/test-primes.c
@@ -0,0 +1,24 @@
+/* Copyright (c) 2007-2018 Dovecot authors, see the included COPYING file */
+
+#include "test-lib.h"
+#include "primes.h"
+
+void test_primes(void)
+{
+ unsigned int i, j, num;
+ bool success;
+
+ success = primes_closest(0) > 0;
+ for (num = 1; num < 1024; num++) {
+ if (primes_closest(num) < num)
+ success = FALSE;
+ }
+ for (i = 10; i < 32; i++) {
+ num = (1U << i) - 100;
+ for (j = 0; j < 200; j++, num++) {
+ if (primes_closest(num) < num)
+ success = FALSE;
+ }
+ }
+ test_out("primes_closest()", success);
+}
diff --git a/src/lib/test-printf-format-fix.c b/src/lib/test-printf-format-fix.c
new file mode 100644
index 0000000..c9d6203
--- /dev/null
+++ b/src/lib/test-printf-format-fix.c
@@ -0,0 +1,142 @@
+/* Copyright (c) 2001-2018 Dovecot authors, see the included COPYING file */
+
+/* Unit tests for printf-format-fix helper */
+
+#include "test-lib.h"
+#include "printf-format-fix.h"
+
+#include <string.h>
+
+struct format_fix_rewrites {
+ const char *input;
+ const char *output;
+ size_t length;
+};
+
+static void test_unchanged()
+{
+ static const char *tests[] = {
+ "Hello world",
+ "Embedded %%, %u, %f, %s, etc. are OK",
+ "Allow %#0- +s flags",
+ "duplicate flags in different args %0-123s %0-123s",
+ "Minimum length %9999s",
+ "Minimum length parameter %*s",
+ "Precision %.9999s",
+ "Precision %1.9999s",
+ "Precision parameter %1.*s %.*s",
+ "Floating precisions such as %.0f %0.4f %-4.0f",
+ "Length modifiers %hd %hhd %ld %lld %Lg %jd %zd %td",
+ "Specifiers %s %u %d %c %i %x %X %p %o %e %E %f %F %g %G %a %A",
+ "%%doesn't cause confusion in %%m and %%n",
+ };
+ unsigned int i;
+
+ test_begin("printf_format_fix(safe)");
+ for (i = 0; i < N_ELEMENTS(tests); ++i) {
+ size_t len;
+ T_BEGIN {
+ test_assert_idx(printf_format_fix(tests[i])
+ == tests[i], i);
+ test_assert_idx(printf_format_fix_get_len(tests[i], &len)
+ == tests[i], i);
+ test_assert_idx(len == strlen(tests[i]), i);
+ } T_END;
+ }
+ test_end();
+}
+
+static void test_ok_changes()
+{
+ static const char *tests[] = {
+ "OK to have a trailing %m",
+ "%m can appear at the start too",
+ "Even %m in the middle with a confusing %%m elsewhere is OK",
+ };
+ unsigned int i;
+ const char *needle;
+ unsigned int needlen;
+ int old_errno = errno;
+
+ test_begin("printf_format_fix(rewrites)");
+
+ errno = EINVAL;
+ needle = strerror(errno);
+ i_assert(needle != NULL);
+ needlen = strlen(needle);
+
+ for (i = 0; i < N_ELEMENTS(tests); ++i) {
+ size_t len;
+ char const *chgd;
+ char const *insert;
+ unsigned int offs;
+
+ T_BEGIN {
+ chgd = printf_format_fix(tests[i]);
+ test_assert_idx(chgd != tests[i], i);
+ insert = strstr(chgd, needle);
+ test_assert_idx(insert != NULL, i);
+ offs = insert - chgd;
+ test_assert_idx(memcmp(chgd, tests[i], offs) == 0, i);
+ test_assert_idx(memcmp(chgd+offs, needle, needlen) == 0, i);
+ test_assert_idx(strcmp(chgd+offs+needlen, tests[i]+offs+2) == 0, i);
+
+ chgd = printf_format_fix_get_len(tests[i], &len);
+ test_assert_idx(chgd != tests[i], i);
+ test_assert_idx(len == strlen(chgd), i);
+ insert = strstr(chgd, needle);
+ test_assert_idx(insert != NULL, i);
+ offs = insert - chgd;
+ test_assert_idx(memcmp(chgd, tests[i], offs) == 0, i);
+ test_assert_idx(memcmp(chgd+offs, needle, needlen) == 0, i);
+ test_assert_idx(memcmp(chgd+offs+needlen, tests[i]+offs+2, len-needlen-offs) == 0, i);
+ } T_END;
+ }
+
+ errno = old_errno;
+
+ test_end();
+}
+
+void test_printf_format_fix()
+{
+ test_unchanged();
+ test_ok_changes();
+}
+
+/* Want to test the panics too? go for it! */
+enum fatal_test_state fatal_printf_format_fix(unsigned int stage)
+{
+ static const struct {
+ const char *format;
+ const char *expected_fatal;
+ } fatals[] = {
+ { "no no no %n's", "%n modifier used" },
+ { "no no no %-1234567890123n's with extra stuff", "Too large minimum field width" },
+ { "%m allowed once, but not twice: %m", "%m used twice" },
+ { "%m must not obscure a later %n", "%n modifier used" },
+ { "definitely can't have a tailing %", "Missing % specifier" },
+ { "Evil %**%n", "Unsupported 0x2a specifier" },
+ { "Evil %*#%99999$s", "Unsupported 0x23 specifier" },
+ { "No weird %% with %0%", "Unsupported 0x25 specifier" },
+ { "No duplicate modifiers %00s", "Duplicate % flag '0'" },
+ { "Minimum length can't be too long %10000s", "Too large minimum field width" },
+ { "Minimum length doesn't support %*1$s", "Unsupported 0x31 specifier" },
+ { "Precision can't be too long %.10000s", "Too large precision" },
+ { "Precision can't be too long %1.10000s", "Too large precision" },
+ { "Precision doesn't support %1.-1s", "Unsupported 0x2d specifier" },
+ };
+
+ if(stage >= N_ELEMENTS(fatals)) {
+ test_end();
+ return FATAL_TEST_FINISHED;
+ }
+
+ if(stage == 0)
+ test_begin("fatal_printf_format_fix");
+
+ /* let's crash! */
+ test_expect_fatal_string(fatals[stage].expected_fatal);
+ (void)printf_format_fix(fatals[stage].format);
+ return FATAL_TEST_FAILURE;
+}
diff --git a/src/lib/test-priorityq.c b/src/lib/test-priorityq.c
new file mode 100644
index 0000000..a4eb90f
--- /dev/null
+++ b/src/lib/test-priorityq.c
@@ -0,0 +1,106 @@
+/* Copyright (c) 2007-2018 Dovecot authors, see the included COPYING file */
+
+#include "test-lib.h"
+#include "priorityq.h"
+
+
+struct pq_test_item {
+ struct priorityq_item item;
+ int num;
+};
+
+static int cmp_int(const void *p1, const void *p2)
+{
+ const struct pq_test_item *i1 = p1, *i2 = p2;
+
+ return i1->num - i2->num;
+}
+
+void test_priorityq(void)
+{
+#define PQ_MAX_ITEMS 100
+ static const int input[] = {
+ 1, 2, 3, 4, 5, 6, 7, 8, -1,
+ 8, 7, 6, 5, 4, 3, 2, 1, -1,
+ 8, 7, 5, 6, 1, 3, 4, 2, -1,
+ -1
+ };
+ static const int output[] = {
+ 1, 2, 3, 4, 5, 6, 7, 8
+ };
+ struct pq_test_item *item, items[PQ_MAX_ITEMS];
+ struct priorityq_item *const *all_items;
+ unsigned int i, j;
+ struct priorityq *pq;
+ pool_t pool;
+ int prev;
+
+ pool = pool_alloconly_create("priorityq items", 1024);
+
+ /* simple tests with popping only */
+ test_begin("priorityq");
+ for (i = 0; input[i] != -1; i++) {
+ p_clear(pool);
+ pq = priorityq_init(cmp_int, 1);
+ for (j = 0; input[i] != -1; i++, j++) {
+ test_assert(priorityq_count(pq) == j);
+ item = p_new(pool, struct pq_test_item, 1);
+ item->num = input[i];
+ priorityq_add(pq, &item->item);
+ }
+ all_items = priorityq_items(pq);
+ test_assert(priorityq_count(pq) == N_ELEMENTS(output));
+ item = (struct pq_test_item *)all_items[0];
+ test_assert(item->num == output[0]);
+ for (j = 1; j < N_ELEMENTS(output); j++) {
+ item = (struct pq_test_item *)all_items[j];
+ test_assert(item->num > output[0]);
+ test_assert(item->num <= output[N_ELEMENTS(output)-1]);
+ }
+ for (j = 0; j < N_ELEMENTS(output); j++) {
+ test_assert(priorityq_count(pq) == N_ELEMENTS(output) - j);
+
+ item = (struct pq_test_item *)priorityq_peek(pq);
+ i_assert(item != NULL);
+ test_assert(output[j] == item->num);
+ item = (struct pq_test_item *)priorityq_pop(pq);
+ i_assert(item != NULL);
+ test_assert(output[j] == item->num);
+ }
+ test_assert(priorityq_count(pq) == 0);
+ test_assert(priorityq_peek(pq) == NULL);
+ test_assert(priorityq_pop(pq) == NULL);
+ priorityq_deinit(&pq);
+ }
+ test_end();
+
+ /* randomized tests, remove elements */
+ test_begin("priorityq randomized");
+ for (i = 0; i < 100; i++) {
+ pq = priorityq_init(cmp_int, 1);
+ for (j = 0; j < PQ_MAX_ITEMS; j++) {
+ items[j].num = i_rand_limit(INT_MAX);
+ priorityq_add(pq, &items[j].item);
+ }
+ for (j = 0; j < PQ_MAX_ITEMS; j++) {
+ if (i_rand_limit(3) == 0) {
+ priorityq_remove(pq, &items[j].item);
+ items[j].num = -1;
+ }
+ }
+ prev = 0;
+ while (priorityq_count(pq) > 0) {
+ item = (struct pq_test_item *)priorityq_pop(pq);
+ i_assert(item != NULL);
+ test_assert(item->num >= 0 && prev <= item->num);
+ prev = item->num;
+ item->num = -1;
+ }
+ for (j = 0; j < PQ_MAX_ITEMS; j++) {
+ test_assert(items[j].num == -1);
+ }
+ priorityq_deinit(&pq);
+ }
+ test_end();
+ pool_unref(&pool);
+}
diff --git a/src/lib/test-random.c b/src/lib/test-random.c
new file mode 100644
index 0000000..6c83ff6
--- /dev/null
+++ b/src/lib/test-random.c
@@ -0,0 +1,67 @@
+/* Copyright (c) 2018 Dovecot authors, see the included COPYING file */
+
+#include "test-lib.h"
+#include "stats-dist.h"
+#include "randgen.h"
+#include <math.h>
+
+#define TEST_RAND_SIZE_MEDIAN 100000.0
+
+static void test_random_median(void)
+{
+ uint64_t tmp;
+ double median, average;
+
+ struct stats_dist *s = stats_dist_init_with_size(TEST_RAND_SIZE_MEDIAN);
+ test_begin("test_random (median & average)");
+ for(unsigned int i = 0; i < TEST_RAND_SIZE_MEDIAN; i++) {
+ uint64_t value;
+ value = i_rand_limit(TEST_RAND_SIZE_MEDIAN);
+ stats_dist_add(s, value);
+ }
+ tmp = stats_dist_get_median(s);
+
+ /* median should be 0.5 +-2% */
+ median = (double)tmp / TEST_RAND_SIZE_MEDIAN;
+ test_assert(fabs(median - 0.5) < 0.01);
+
+ /* average should be 0.5 +- %2 */
+ average = stats_dist_get_avg(s) / TEST_RAND_SIZE_MEDIAN;
+
+ test_assert(fabs(average - 0.5) < 0.01);
+
+ stats_dist_deinit(&s);
+ test_end();
+}
+
+static void test_random_limits(void)
+{
+ test_begin("random limits");
+ test_assert(i_rand_limit(1) == 0);
+ test_assert(i_rand_minmax(0, 0) == 0);
+ test_assert(i_rand_minmax(UINT32_MAX, UINT32_MAX) == UINT32_MAX);
+ test_end();
+}
+
+void test_random(void)
+{
+ test_random_median();
+ test_random_limits();
+}
+
+enum fatal_test_state fatal_random(unsigned int stage)
+{
+ switch (stage) {
+ case 0:
+ test_begin("random fatals");
+ test_expect_fatal_string("min_val <= max_val");
+ (void)i_rand_minmax(1, 0);
+ return FATAL_TEST_FAILURE;
+ case 1:
+ test_expect_fatal_string("upper_bound > 0");
+ (void)i_rand_limit(0);
+ return FATAL_TEST_FAILURE;
+ }
+ test_end();
+ return FATAL_TEST_FINISHED;
+}
diff --git a/src/lib/test-seq-range-array.c b/src/lib/test-seq-range-array.c
new file mode 100644
index 0000000..ca0178a
--- /dev/null
+++ b/src/lib/test-seq-range-array.c
@@ -0,0 +1,419 @@
+/* Copyright (c) 2007-2018 Dovecot authors, see the included COPYING file */
+
+#include "test-lib.h"
+#include "array.h"
+#include "seq-range-array.h"
+
+
+static void
+boundaries_permute(uint32_t *input, unsigned int i, unsigned int count)
+{
+ ARRAY_TYPE(seq_range) range;
+ const struct seq_range *seqs;
+ unsigned int seqs_count;
+ uint32_t tmp;
+ unsigned int j;
+
+ if (i+1 < count) {
+ for (j = i; j < count; j++) {
+ tmp = input[i]; input[i] = input[j]; input[j] = tmp;
+ boundaries_permute(input, i+1, count);
+ tmp = input[i]; input[i] = input[j]; input[j] = tmp;
+ }
+ return;
+ }
+ t_array_init(&range, 4);
+ for (i = 0; i < count; i++)
+ seq_range_array_add(&range, input[i]);
+ seqs = array_get(&range, &seqs_count);
+ test_assert(seqs_count == 2);
+ test_assert(seqs[0].seq1 == 0);
+ test_assert(seqs[0].seq2 == 1);
+ test_assert(seqs[1].seq1 == (uint32_t)-2);
+ test_assert(seqs[1].seq2 == (uint32_t)-1);
+}
+
+static void test_seq_range_array_add_boundaries(void)
+{
+ static uint32_t input[] = { 0, 1, (uint32_t)-2, (uint32_t)-1 };
+
+ boundaries_permute(input, 0, N_ELEMENTS(input));
+}
+
+static void test_seq_range_array_add_merge(void)
+{
+ ARRAY_TYPE(seq_range) range;
+
+ test_begin("seq_range_array_add() merging");
+ t_array_init(&range, 8);
+ seq_range_array_add(&range, 4);
+ seq_range_array_add(&range, 1);
+ seq_range_array_add(&range, 2);
+ test_assert(array_count(&range) == 2);
+
+ seq_range_array_add_range(&range, 1, (uint32_t)-1);
+ test_assert(array_count(&range) == 1);
+ seq_range_array_add_range(&range, 1, (uint32_t)-1);
+ test_assert(array_count(&range) == 1);
+ test_end();
+}
+
+static void test_seq_range_array_merge_n(void)
+{
+ ARRAY_TYPE(seq_range) src, dest, dest2;
+ struct seq_range_iter iter;
+ const uint32_t seqs[] = { 4, 5, 7, 8, 9, 11 };
+ uint32_t seq;
+
+ test_begin("seq_range_array_merge_n()");
+ t_array_init(&src, 16);
+ t_array_init(&dest, 16);
+ t_array_init(&dest2, 16);
+ for (unsigned int i = 0; i < N_ELEMENTS(seqs); i++)
+ seq_range_array_add(&src, seqs[i]);
+
+ for (unsigned int i = 0; i <= N_ELEMENTS(seqs); i++) {
+ array_clear(&dest);
+ array_clear(&dest2);
+ seq_range_array_merge_n(&dest, &src, i);
+ test_assert_idx(seq_range_count(&dest) == I_MIN(i, N_ELEMENTS(seqs)), i);
+
+ seq_range_array_iter_init(&iter, &src);
+ for (unsigned int j = 0; j < i; j++) {
+ test_assert_idx(seq_range_array_iter_nth(&iter, j, &seq), i);
+ seq_range_array_add(&dest2, seq);
+ }
+ seq_range_array_invert(&dest2, 1, UINT32_MAX);
+ seq_range_array_intersect(&dest2, &dest);
+ test_assert_idx(array_count(&dest2) == 0, i);
+ }
+ test_end();
+}
+
+static void test_seq_range_array_remove_nth(void)
+{
+ ARRAY_TYPE(seq_range) range;
+ const struct seq_range *r;
+
+ test_begin("seq_range_array_remove_nth()");
+ t_array_init(&range, 8);
+ seq_range_array_add_range(&range, 1, 5);
+ seq_range_array_add(&range, 7);
+ seq_range_array_add_range(&range, 10,20);
+ test_assert(array_count(&range) == 3);
+
+ seq_range_array_remove_nth(&range, 0, 2);
+ r = array_front(&range); test_assert(r->seq1 == 3 && r->seq2 == 5);
+
+ seq_range_array_remove_nth(&range, 1, 4);
+ r = array_front(&range); test_assert(r->seq1 == 3 && r->seq2 == 3);
+ r = array_idx(&range, 1); test_assert(r->seq1 == 11 && r->seq2 == 20);
+
+ seq_range_array_remove_nth(&range, 5, (uint32_t)-1);
+ r = array_idx(&range, 1); test_assert(r->seq1 == 11 && r->seq2 == 14);
+
+ test_assert(array_count(&range) == 2);
+ test_end();
+}
+
+static void test_seq_range_array_remove_range(void)
+{
+ ARRAY_TYPE(seq_range) range;
+ const struct seq_range *r;
+
+ test_begin("seq_range_array_remove_range()");
+ t_array_init(&range, 8);
+
+ seq_range_array_add_range(&range, 0, (uint32_t)-2);
+ test_assert(seq_range_array_remove_range(&range, 0, 2) == 3);
+ r = array_front(&range); test_assert(r->seq1 == 3 && r->seq2 == (uint32_t)-2);
+
+ seq_range_array_add_range(&range, 0, (uint32_t)-2);
+ test_assert(array_count(&range) == 1);
+ test_assert(seq_range_array_remove_range(&range, 0, (uint32_t)-2) == UINT_MAX);
+ test_assert(array_count(&range) == 0);
+
+ seq_range_array_add_range(&range, (uint32_t)-1, (uint32_t)-1);
+ test_assert(seq_range_array_remove_range(&range, (uint32_t)-1, (uint32_t)-1) == 1);
+ test_assert(array_count(&range) == 0);
+
+ seq_range_array_add_range(&range, (uint32_t)-1, (uint32_t)-1);
+ test_assert(seq_range_array_remove_range(&range, 1, (uint32_t)-1) == 1);
+ test_assert(array_count(&range) == 0);
+
+ seq_range_array_add_range(&range, 1, 10);
+ test_assert(seq_range_array_remove_range(&range, 5, 6) == 2);
+ test_assert(seq_range_array_remove_range(&range, 4, 7) == 2);
+ test_assert(seq_range_array_remove_range(&range, 1, 4) == 3);
+ test_assert(seq_range_array_remove_range(&range, 8, 10) == 3);
+ test_assert(array_count(&range) == 0);
+
+ test_end();
+}
+
+static void test_seq_range_array_random(void)
+{
+#define SEQ_RANGE_TEST_BUFSIZE 100
+#define SEQ_RANGE_TEST_COUNT 20000
+ unsigned char shadowbuf[SEQ_RANGE_TEST_BUFSIZE];
+ ARRAY_TYPE(seq_range) range;
+ const struct seq_range *seqs;
+ uint32_t seq1, seq2;
+ unsigned int i, j, ret, ret2, count;
+ int test = -1;
+
+ ret = ret2 = 0;
+ i_array_init(&range, 1);
+ memset(shadowbuf, 0, sizeof(shadowbuf));
+ for (i = 0; i < SEQ_RANGE_TEST_COUNT; i++) {
+ seq1 = i_rand_limit(SEQ_RANGE_TEST_BUFSIZE);
+ seq2 = seq1 + i_rand_limit(SEQ_RANGE_TEST_BUFSIZE - seq1);
+ test = i_rand_limit(4);
+ switch (test) {
+ case 0:
+ ret = seq_range_array_add(&range, seq1) ? 0 : 1; /* FALSE == added */
+ ret2 = shadowbuf[seq1] == 0 ? 1 : 0;
+ shadowbuf[seq1] = 1;
+ break;
+ case 1:
+ ret = seq_range_array_add_range_count(&range, seq1, seq2);
+ for (ret2 = 0; seq1 <= seq2; seq1++) {
+ if (shadowbuf[seq1] == 0) {
+ ret2++;
+ shadowbuf[seq1] = 1;
+ }
+ }
+ break;
+ case 2:
+ ret = seq_range_array_remove(&range, seq1) ? 1 : 0;
+ ret2 = shadowbuf[seq1] != 0 ? 1 : 0;
+ shadowbuf[seq1] = 0;
+ break;
+ case 3:
+ ret = seq_range_array_remove_range(&range, seq1, seq2);
+ for (ret2 = 0; seq1 <= seq2; seq1++) {
+ if (shadowbuf[seq1] != 0) {
+ ret2++;
+ shadowbuf[seq1] = 0;
+ }
+ }
+ break;
+ }
+ if (ret != ret2)
+ break;
+
+ seqs = array_get(&range, &count);
+ for (j = 0, seq1 = 0; j < count; j++) {
+ if (j > 0 && seqs[j-1].seq2+1 >= seqs[j].seq1)
+ goto fail;
+ for (; seq1 < seqs[j].seq1; seq1++) {
+ if (shadowbuf[seq1] != 0)
+ goto fail;
+ }
+ for (; seq1 <= seqs[j].seq2; seq1++) {
+ if (shadowbuf[seq1] == 0)
+ goto fail;
+ }
+ }
+ i_assert(seq1 <= SEQ_RANGE_TEST_BUFSIZE);
+ for (; seq1 < SEQ_RANGE_TEST_BUFSIZE; seq1++) {
+ if (shadowbuf[seq1] != 0)
+ goto fail;
+ }
+ }
+fail:
+ if (i == SEQ_RANGE_TEST_COUNT)
+ test_out("seq_range_array random", TRUE);
+ else {
+ test_out_reason("seq_range_array random", FALSE,
+ t_strdup_printf("round %u test %d failed", i, test));
+ }
+ array_free(&range);
+}
+
+static void test_seq_range_array_invert_minmax(uint32_t min, uint32_t max)
+{
+ ARRAY_TYPE(seq_range) range = ARRAY_INIT;
+ struct seq_range_iter iter;
+ unsigned int n, inverse_mask, mask_inside, mask_size = max-min+1;
+ uint32_t seq;
+
+ i_assert(mask_size <= sizeof(unsigned int)*8);
+ t_array_init(&range, 16);
+ for (unsigned int mask = 0; mask < mask_size; mask++) {
+ array_clear(&range);
+ for (unsigned int i = 0; i < mask_size; i++) {
+ if ((mask & (1 << i)) != 0)
+ seq_range_array_add(&range, min+i);
+ }
+ seq_range_array_invert(&range, min, max);
+
+ inverse_mask = 0;
+ seq_range_array_iter_init(&iter, &range); n = 0;
+ while (seq_range_array_iter_nth(&iter, n++, &seq)) {
+ test_assert(seq >= min && seq <= max);
+ inverse_mask |= 1 << (seq-min);
+ }
+ mask_inside = ((1 << mask_size)-1);
+ test_assert_idx((inverse_mask & ~mask_inside) == 0, mask);
+ test_assert_idx(inverse_mask == (mask ^ mask_inside), mask);
+ }
+}
+
+static void test_seq_range_array_invert(void)
+{
+ test_begin("seq_range_array_invert()");
+ /* first numbers */
+ for (unsigned int min = 0; min <= 7; min++) {
+ for (unsigned int max = min; max <= 7; max++) T_BEGIN {
+ test_seq_range_array_invert_minmax(min, max);
+ } T_END;
+ }
+ /* last numbers */
+ for (uint64_t min = 0xffffffff-7; min <= 0xffffffff; min++) {
+ for (uint64_t max = min; max <= 0xffffffff; max++) T_BEGIN {
+ test_seq_range_array_invert_minmax(min, max);
+ } T_END;
+ }
+ test_end();
+}
+
+static void test_seq_range_array_invert_edges(void)
+{
+ static const struct {
+ int64_t a_seq1, a_seq2, b_seq1, b_seq2;
+ int64_t resa_seq1, resa_seq2, resb_seq1, resb_seq2;
+ } tests[] = {
+ { -1, -1, -1, -1,
+ 0, 0xffffffff, -1, -1 },
+ /*{ 0, 0xffffffff, -1, -1, too large, will assert-crash
+ -1, -1, -1, -1 }, */
+ { 0, 0xfffffffe, -1, -1,
+ 0xffffffff, 0xffffffff, -1, -1 },
+ { 1, 0xfffffffe, -1, -1,
+ 0, 0, 0xffffffff, 0xffffffff },
+ { 1, 0xffffffff, -1, -1,
+ 0, 0, -1, -1 },
+ { 0, 0, 0xffffffff, 0xffffffff,
+ 1, 0xfffffffe, -1, -1 },
+ { 0xffffffff, 0xffffffff, -1, -1,
+ 0, 0xfffffffe, -1, -1 },
+ };
+ ARRAY_TYPE(seq_range) range = ARRAY_INIT;
+ const struct seq_range *result;
+ unsigned int count;
+
+ test_begin("seq_range_array_invert() edges");
+ for (unsigned int i = 0; i < N_ELEMENTS(tests); i++) T_BEGIN {
+ t_array_init(&range, 10);
+ if (tests[i].a_seq1 != -1)
+ seq_range_array_add_range(&range, tests[i].a_seq1, tests[i].a_seq2);
+ if (tests[i].b_seq1 != -1)
+ seq_range_array_add_range(&range, tests[i].b_seq1, tests[i].b_seq2);
+ seq_range_array_invert(&range, 0, 0xffffffff);
+
+ result = array_get(&range, &count);
+ if (tests[i].resa_seq1 == -1)
+ test_assert_idx(count == 0, i);
+ else {
+ test_assert(result[0].seq1 == tests[i].resa_seq1);
+ test_assert(result[0].seq2 == tests[i].resa_seq2);
+ if (tests[i].resb_seq1 == -1)
+ test_assert_idx(count == 1, i);
+ else {
+ test_assert(result[1].seq1 == tests[i].resb_seq1);
+ test_assert(result[1].seq2 == tests[i].resb_seq2);
+ }
+ }
+ } T_END;
+ test_end();
+}
+
+static void test_seq_range_create(ARRAY_TYPE(seq_range) *array, uint8_t byte)
+{
+ unsigned int i;
+
+ array_clear(array);
+ for (i = 0; i < 8; i++) {
+ if ((byte & (1 << i)) != 0)
+ seq_range_array_add(array, i + 1);
+ }
+}
+
+static void test_seq_range_array_have_common(void)
+{
+ ARRAY_TYPE(seq_range) arr1, arr2;
+ unsigned int i, j;
+ bool ret1, ret2, success = TRUE;
+
+ t_array_init(&arr1, 8);
+ t_array_init(&arr2, 8);
+ for (i = 0; i < 256; i++) {
+ test_seq_range_create(&arr1, i);
+ for (j = 0; j < 256; j++) {
+ test_seq_range_create(&arr2, j);
+ ret1 = seq_range_array_have_common(&arr1, &arr2);
+ ret2 = (i & j) != 0;
+ if (ret1 != ret2)
+ success = FALSE;
+ }
+ }
+ test_out("seq_range_array_have_common()", success);
+}
+
+void test_seq_range_array(void)
+{
+ test_seq_range_array_add_boundaries();
+ test_seq_range_array_add_merge();
+ test_seq_range_array_merge_n();
+ test_seq_range_array_remove_nth();
+ test_seq_range_array_remove_range();
+ test_seq_range_array_invert();
+ test_seq_range_array_invert_edges();
+ test_seq_range_array_have_common();
+ test_seq_range_array_random();
+}
+
+enum fatal_test_state fatal_seq_range_array(unsigned int stage)
+{
+ ARRAY_TYPE(seq_range) arr;
+ struct seq_range *range;
+
+ t_array_init(&arr, 2);
+ switch (stage) {
+ case 0:
+ test_begin("seq_range_array fatals");
+ test_expect_fatal_string("!seq_range_is_overflowed(array)");
+ seq_range_array_add_range(&arr, 0, (uint32_t)-1);
+ return FATAL_TEST_FAILURE;
+ case 1:
+ seq_range_array_add_range(&arr, 1, (uint32_t)-1);
+ test_expect_fatal_string("!seq_range_is_overflowed(array)");
+ seq_range_array_add(&arr, 0);
+ return FATAL_TEST_FAILURE;
+ case 2:
+ seq_range_array_add_range(&arr, 0, (uint32_t)-2);
+ test_expect_fatal_string("!seq_range_is_overflowed(array)");
+ seq_range_array_add(&arr, (uint32_t)-1);
+ return FATAL_TEST_FAILURE;
+ case 3:
+ range = array_append_space(&arr);
+ range->seq2 = (uint32_t)-1;
+ test_expect_fatal_string("range->seq1 > 0 || range->seq2 < (uint32_t)-1");
+ i_error("This shouldn't return: %u", seq_range_count(&arr));
+ return FATAL_TEST_FAILURE;
+ case 4:
+ range = array_append_space(&arr);
+ range->seq2 = (uint32_t)-2;
+ test_assert(seq_range_count(&arr) == (uint32_t)-1);
+
+ range = array_append_space(&arr);
+ range->seq1 = (uint32_t)-2;
+ range->seq2 = (uint32_t)-1;
+ test_expect_fatal_string("UINT_MAX - seq_count >= seq_range_length(range)");
+ i_error("This shouldn't return: %u", seq_range_count(&arr));
+ return FATAL_TEST_FAILURE;
+ }
+ test_end();
+ return FATAL_TEST_FINISHED;
+}
diff --git a/src/lib/test-seq-set-builder.c b/src/lib/test-seq-set-builder.c
new file mode 100644
index 0000000..c449caf
--- /dev/null
+++ b/src/lib/test-seq-set-builder.c
@@ -0,0 +1,132 @@
+/* Copyright (c) 2021 Dovecot authors, see the included COPYING file */
+
+#include "test-lib.h"
+#include "str.h"
+#include "seq-set-builder.h"
+
+static void test_seq_set_builder_add(void)
+{
+ struct seqset_builder *seq_set_builder;
+
+ test_begin("seq set builder add");
+ string_t *test_str = t_str_new(128);
+ str_append(test_str, "UID COPY ");
+ seq_set_builder = seqset_builder_init(test_str);
+ seqset_builder_add(seq_set_builder, 1);
+ seqset_builder_add(seq_set_builder, 3);
+ seqset_builder_add(seq_set_builder, 6);
+ seqset_builder_add(seq_set_builder, 7);
+ seqset_builder_add(seq_set_builder, 8);
+ seqset_builder_add(seq_set_builder, 9);
+ seqset_builder_add(seq_set_builder, 10);
+ seqset_builder_add(seq_set_builder, 12);
+ seqset_builder_deinit(&seq_set_builder);
+ test_assert_strcmp(str_c(test_str), "UID COPY 1,3,6:10,12");
+
+ str_truncate(test_str, 0);
+ seq_set_builder = seqset_builder_init(test_str);
+ seqset_builder_add(seq_set_builder, 99999);
+ seqset_builder_add(seq_set_builder, 100000);
+ seqset_builder_add(seq_set_builder, 5);
+ seqset_builder_add(seq_set_builder, 7);
+ seqset_builder_add(seq_set_builder, 9);
+ seqset_builder_add(seq_set_builder, 10);
+ seqset_builder_add(seq_set_builder, 120);
+ seqset_builder_add(seq_set_builder, 121);
+ seqset_builder_add(seq_set_builder, 122);
+ seqset_builder_add(seq_set_builder, 125);
+ seqset_builder_deinit(&seq_set_builder);
+ test_assert_strcmp(str_c(test_str), "99999:100000,5,7,9:10,120:122,125");
+
+ str_truncate(test_str, 0);
+ str_append(test_str, "UID COPY ");
+ seq_set_builder = seqset_builder_init(test_str);
+ seqset_builder_add(seq_set_builder, 287409);
+ seqset_builder_add(seq_set_builder, 287410);
+ seqset_builder_deinit(&seq_set_builder);
+ test_assert_strcmp(str_c(test_str), "UID COPY 287409:287410");
+
+ str_truncate(test_str, 0);
+ str_append(test_str, "UID COPY 287409,");
+ seq_set_builder = seqset_builder_init(test_str);
+ seqset_builder_add(seq_set_builder, 287410);
+ seqset_builder_add(seq_set_builder, 287411);
+ test_assert_strcmp(str_c(test_str), "UID COPY 287409,287410:287411,");
+ seqset_builder_deinit(&seq_set_builder);
+ test_assert_strcmp(str_c(test_str), "UID COPY 287409,287410:287411");
+
+ str_truncate(test_str, 0);
+ seq_set_builder = seqset_builder_init(test_str);
+ seqset_builder_add(seq_set_builder, 4294967289);
+ seqset_builder_add(seq_set_builder, 4294967291);
+ seqset_builder_add(seq_set_builder, 4294967293);
+ seqset_builder_add(seq_set_builder, 4294967294);
+ seqset_builder_add(seq_set_builder, 4294967295);
+ test_assert_strcmp(str_c(test_str), "4294967289,4294967291,4294967293:4294967295,");
+ seqset_builder_deinit(&seq_set_builder);
+ test_assert_strcmp(str_c(test_str), "4294967289,4294967291,4294967293:4294967295");
+
+ str_truncate(test_str, 0);
+ str_append(test_str, ";j;,");
+ seq_set_builder = seqset_builder_init(test_str);
+ test_assert_strcmp(str_c(test_str), ";j;,");
+ seqset_builder_deinit(&seq_set_builder);
+ test_assert_strcmp(str_c(test_str), ";j;,");
+
+ test_end();
+}
+
+static void test_seq_set_builder_try_add(void)
+{
+ struct seqset_builder *seq_set_builder;
+
+ test_begin("seq set builder try add");
+
+ string_t *test_str = t_str_new(128);
+ str_append(test_str, "UID MOVE ");
+
+ seq_set_builder = seqset_builder_init(test_str);
+ test_assert(seqset_builder_try_add(seq_set_builder, 20, 1));
+ test_assert(seqset_builder_try_add(seq_set_builder, 20, 3));
+ test_assert(seqset_builder_try_add(seq_set_builder, 20, 5));
+ test_assert(seqset_builder_try_add(seq_set_builder, 20, 7));
+ test_assert(seqset_builder_try_add(seq_set_builder, 20, 9));
+ test_assert(19 == str_len(test_str));
+
+ test_assert_strcmp(str_c(test_str), "UID MOVE 1,3,5,7,9,");
+
+ test_assert(!seqset_builder_try_add(seq_set_builder, 20, 11));
+ test_assert(str_len(test_str) <= 20);
+ test_assert_strcmp(str_c(test_str), "UID MOVE 1,3,5,7,9,");
+
+ test_assert(seqset_builder_try_add(seq_set_builder, 21, 2));
+ test_assert(str_len(test_str) <= 21);
+ test_assert_strcmp(str_c(test_str), "UID MOVE 1,3,5,7,9,2,");
+
+ test_assert(!seqset_builder_try_add(seq_set_builder, 20, 15));
+ test_assert(seqset_builder_try_add(seq_set_builder, 24, 13));
+ test_assert(!seqset_builder_try_add(seq_set_builder, 24, 17));
+ test_assert(str_len(test_str) <= 24);
+ test_assert_strcmp(str_c(test_str), "UID MOVE 1,3,5,7,9,2,13,");
+ seqset_builder_deinit(&seq_set_builder);
+
+ str_truncate(test_str, 0);
+ seq_set_builder = seqset_builder_init(test_str);
+ test_assert(seqset_builder_try_add(seq_set_builder, 32, 4294967289));
+ test_assert(seqset_builder_try_add(seq_set_builder, 32, 4294967291));
+ test_assert(seqset_builder_try_add(seq_set_builder, 32, 4294967292));
+ test_assert(!seqset_builder_try_add(seq_set_builder, 32, 4294967293));
+ test_assert(seqset_builder_try_add(seq_set_builder, 50, 4294967293));
+ test_assert(seqset_builder_try_add(seq_set_builder, 50, 4294967295));
+ test_assert_strcmp(str_c(test_str), "4294967289,4294967291:4294967293,4294967295,");
+ seqset_builder_deinit(&seq_set_builder);
+ test_assert_strcmp(str_c(test_str), "4294967289,4294967291:4294967293,4294967295");
+
+ test_end();
+}
+
+void test_seq_set_builder(void)
+{
+ test_seq_set_builder_add();
+ test_seq_set_builder_try_add();
+}
diff --git a/src/lib/test-stats-dist.c b/src/lib/test-stats-dist.c
new file mode 100644
index 0000000..795c22f
--- /dev/null
+++ b/src/lib/test-stats-dist.c
@@ -0,0 +1,132 @@
+/* Copyright (c) 2007-2018 Dovecot authors, see the included COPYING file */
+
+#include "test-lib.h"
+#include "stats-dist.h"
+#include "sort.h"
+#include "math.h"
+
+#define DBL_EQ(a, b) (fabs((a)-(b)) < 0.001)
+
+static void
+test_stats_dist_verify(struct stats_dist *t, const int64_t *input,
+ unsigned int input_size)
+{
+ uint64_t min = INT_MAX, max = 0, sum = 0;
+ uint64_t *copy;
+ unsigned int i;
+
+ i_assert(input_size > 0);
+
+ copy = i_new(uint64_t, input_size);
+ for (i = 0; i < input_size; i++) {
+ uint64_t value = input[i];
+
+ if (min > value)
+ min = value;
+ if (max < value)
+ max = value;
+ sum += value;
+ copy[i] = value;
+ }
+ i_qsort(copy, input_size, sizeof(*copy), uint64_cmp);
+
+ test_assert_idx(stats_dist_get_count(t) == input_size, input_size);
+ test_assert_idx(stats_dist_get_sum(t) == sum, input_size);
+ test_assert_idx(stats_dist_get_min(t) == min, input_size);
+ test_assert_idx(stats_dist_get_max(t) == max, input_size);
+ test_assert_idx(DBL_EQ(stats_dist_get_avg(t), (double)sum/input_size),
+ input_size);
+
+ /* these aren't always fully accurate: */
+ test_assert_idx(stats_dist_get_median(t) >= copy[(input_size-1)/2] &&
+ stats_dist_get_median(t) <= copy[input_size/2],
+ input_size);
+ /* when we have 20 elements, [19] is the max, not the 95th %ile, so subtract 1 */
+ test_assert_idx(stats_dist_get_95th(t) == copy[input_size*95/100 - ((input_size%20) == 0 ? 1 : 0)],
+ input_size);
+
+ i_free(copy);
+}
+
+static void test_stats_dist_get_variance(void)
+{
+ static const struct {
+ int64_t in[10];
+ double out;
+ } tests[] = {
+ { .in = { 2, 2, 2, -1 }, .out = 0.0 },
+ { .in = { -1 }, .out = 0.0 },
+ { .in = { 1, 2, 3, 4, 5, 6, 7, 8, -1 }, .out = 5.25 },
+ };
+
+ struct stats_dist *t;
+ unsigned int i, j;
+
+ test_begin("stats_dists_get_variance");
+
+ for (i = 0; i < N_ELEMENTS(tests); i++) {
+ t = stats_dist_init();
+ for (j = 0; tests[i].in[j] >= 0; j++) {
+ stats_dist_add(t, tests[i].in[j]);
+ test_stats_dist_verify(t, tests[i].in, j+1);
+ }
+ test_assert_idx(DBL_EQ(stats_dist_get_variance(t),
+ tests[i].out), i);
+
+ stats_dist_deinit(&t);
+ }
+
+ test_end();
+}
+
+void test_stats_dist(void)
+{
+ static int64_t test_input1[] = {
+ 20, 19, 18, 1, 2, 3, 4, 5, 6, 7, 8,
+ 9, 10, 11, 12, 13, 14, 15, 16, 17, -1
+ };
+ static int64_t test_input2[] = {
+ 20, 21, 19, 18, 1, 2, 3, 4, 5, 6, 7, 8,
+ 9, 10, 11, 12, 13, 14, 15, 16, 17, -1
+ };
+ static int64_t test_input3[] = {
+ 20, 21, 19, 18, 1, 2, 3, 4, 5, 6, 7, 8,
+ 9, 10, 11, 12, 13, 14, 15, 16, 17, 22, -1
+ };
+ static int64_t *test_inputs[] = {
+ test_input1, test_input2, test_input3
+ };
+ struct stats_dist *t;
+ unsigned int i, j;
+
+ for (i = 0; i < N_ELEMENTS(test_inputs); i++) {
+ test_begin(t_strdup_printf("stats_dists %u", i));
+ t = stats_dist_init();
+ for (j = 0; test_inputs[i][j] >= 0; j++) {
+ stats_dist_add(t, test_inputs[i][j]);
+ test_stats_dist_verify(t, test_inputs[i], j+1);
+ }
+ stats_dist_reset(t);
+ test_assert(stats_dist_get_count(t) == 0);
+ test_assert(stats_dist_get_max(t) == 0);
+ stats_dist_deinit(&t);
+ test_end();
+ }
+
+ test_begin("stats_dists large");
+ t = stats_dist_init();
+ for (i = 0; i < 10000; i++)
+ stats_dist_add(t, i);
+ test_assert(stats_dist_get_count(t) == i);
+ test_assert(stats_dist_get_sum(t) == (i-1)*i/2);
+ test_assert(stats_dist_get_min(t) == 0);
+ test_assert(stats_dist_get_max(t) == i-1);
+ test_assert(DBL_EQ(stats_dist_get_avg(t), 4999.500000));
+ /* just test that these work: */
+ test_assert(stats_dist_get_median(t) > 0 && stats_dist_get_median(t) < i-1);
+ test_assert(stats_dist_get_95th(t) > 0 && stats_dist_get_95th(t) < i-1);
+ stats_dist_deinit(&t);
+ test_end();
+
+ test_stats_dist_get_variance();
+}
diff --git a/src/lib/test-str-find.c b/src/lib/test-str-find.c
new file mode 100644
index 0000000..a6a6340
--- /dev/null
+++ b/src/lib/test-str-find.c
@@ -0,0 +1,86 @@
+/* Copyright (c) 2007-2018 Dovecot authors, see the included COPYING file */
+
+#include "test-lib.h"
+#include "str-find.h"
+
+static const char *str_find_text = "xababcd";
+
+static bool test_str_find_substring(const char *key, int expected_pos)
+{
+ const unsigned char *text = (const unsigned char *)str_find_text;
+ const unsigned int text_len = strlen(str_find_text);
+ struct str_find_context *ctx;
+ unsigned int i, j, pos, max, offset;
+ bool ret;
+
+ ctx = str_find_init(pool_datastack_create(), key);
+ /* divide text into every possible block combination and test that
+ it matches */
+ i_assert(text_len > 0);
+ max = 1U << (text_len-1);
+ for (i = 0; i < max; i++) {
+ str_find_reset(ctx);
+ pos = 0; offset = 0; ret = FALSE;
+ for (j = 0; j < text_len; j++) {
+ if ((i & (1 << j)) != 0) {
+ if (str_find_more(ctx, text+pos, j-pos+1)) {
+ ret = TRUE;
+ break;
+ }
+ offset += j-pos + 1;
+ pos = j + 1;
+ }
+ }
+ if (pos != text_len && !ret) {
+ if (str_find_more(ctx, text+pos, j-pos))
+ ret = TRUE;
+ }
+ if (expected_pos < 0) {
+ if (ret)
+ return FALSE;
+ } else {
+ if (!ret)
+ return FALSE;
+
+ pos = str_find_get_match_end_pos(ctx) +
+ offset - strlen(key);
+ if ((int)pos != expected_pos)
+ return FALSE;
+ }
+ }
+ return TRUE;
+}
+
+struct str_find_input {
+ const char *str;
+ int pos;
+};
+
+void test_str_find(void)
+{
+ static const char *fail_input[] = {
+ "xabc",
+ "xabd",
+ "abd"
+ };
+ unsigned int idx, len;
+ const char *key, *p;
+ unsigned int i;
+ bool success = TRUE;
+
+ for (idx = 0; idx < strlen(str_find_text); idx++) {
+ for (len = strlen(str_find_text)-idx; len > 0; len--) {
+ /* we'll get a search key for all substrings of text */
+ T_BEGIN {
+ key = t_strndup(str_find_text + idx, len);
+ p = strstr(str_find_text, key);
+ success = test_str_find_substring(key, p - str_find_text);
+ } T_END;
+ if (!success)
+ break;
+ }
+ }
+ for (i = 0; i < N_ELEMENTS(fail_input) && success; i++)
+ success = test_str_find_substring(fail_input[i], -1);
+ test_out("str_find()", success);
+}
diff --git a/src/lib/test-str-sanitize.c b/src/lib/test-str-sanitize.c
new file mode 100644
index 0000000..d3c9716
--- /dev/null
+++ b/src/lib/test-str-sanitize.c
@@ -0,0 +1,127 @@
+/* Copyright (c) 2007-2018 Dovecot authors, see the included COPYING file */
+
+#include "test-lib.h"
+#include "str.h"
+#include "str-sanitize.h"
+
+struct str_sanitize_test {
+ const char *str;
+ unsigned int max_len;
+ const char *sanitized; /* NULL for no change */
+};
+
+static void test_str_sanitize_max_bytes(void)
+{
+ static const struct str_sanitize_test tests[] = {
+ { NULL, 2, NULL },
+ { "", 2, NULL },
+ { "a", 2, NULL },
+ { "ab", 2, NULL },
+ { "abc", 2, "..." },
+ { "abcd", 3, "..." },
+ { "abcde", 4, "a..." },
+ { "\xD1\x81", 1, "..." },
+ { "\xD1\x81", 2, "\xD1\x81" },
+ { "\xD1\x81", 3, NULL },
+ { "\xC3\xA4\xC3\xA4zyxa", 1, "..." },
+ { "\xC3\xA4\xC3\xA4zyxa", 2, "..." },
+ { "\xC3\xA4\xC3\xA4zyxa", 3, "..." },
+ { "\xC3\xA4\xC3\xA4zyxa", 4, "..." },
+ { "\xC3\xA4\xC3\xA4zyxa", 5, "\xC3\xA4..." },
+ { "\xC3\xA4\xC3\xA4zyxa", 6, "\xC3\xA4..." },
+ { "\xC3\xA4\xC3\xA4zyxa", 7, "\xC3\xA4\xC3\xA4..." },
+ { "\xC3\xA4\xC3\xA4zyxa", 8, "\xC3\xA4\xC3\xA4zyxa" },
+ { "\001x\x1fy\x81", 10, "?x?y?" }
+ };
+ const char *str;
+ string_t *str2;
+ unsigned int i;
+
+ test_begin("str_sanitize");
+ for (i = 0; i < N_ELEMENTS(tests); i++) {
+ str = str_sanitize(tests[i].str, tests[i].max_len);
+ if (tests[i].sanitized != NULL)
+ test_assert_idx(null_strcmp(str, tests[i].sanitized) == 0, i);
+ else
+ test_assert_idx(str == tests[i].str, i);
+ }
+ test_end();
+
+ test_begin("str_sanitize_append");
+ str2 = t_str_new(128);
+ for (i = 0; i < N_ELEMENTS(tests); i++) {
+ if (tests[i].str == NULL)
+ continue;
+ str_truncate(str2, 0);
+ str_append(str2, "1234567890");
+ str_sanitize_append(str2, tests[i].str, tests[i].max_len);
+
+ test_assert_idx(str_begins(str_c(str2), "1234567890"), i);
+ if (tests[i].sanitized != NULL)
+ test_assert_idx(strcmp(str_c(str2)+10, tests[i].sanitized) == 0, i);
+ else
+ test_assert_idx(strcmp(str_c(str2)+10, tests[i].str) == 0, i);
+ }
+ test_end();
+}
+
+static void test_str_sanitize_max_codepoints(void)
+{
+ static const struct str_sanitize_test tests[] = {
+ { NULL, 2, NULL },
+ { "", 2, NULL },
+ { "a", 2, NULL },
+ { "ab", 2, NULL },
+ { "abc", 2, "a\xE2\x80\xA6" },
+ { "abcd", 3, "ab\xE2\x80\xA6" },
+ { "abcde", 4, "abc\xE2\x80\xA6" },
+ { "\xD1\x81", 1, "\xD1\x81" },
+ { "\xD1\x81", 2, "\xD1\x81" },
+ { "\xD1\x81", 3, NULL },
+ { "\xC3\xA4\xC3\xA4zyxa", 1, "\xE2\x80\xA6" },
+ { "\xC3\xA4\xC3\xA4zyxa", 2, "\xC3\xA4\xE2\x80\xA6" },
+ { "\xC3\xA4\xC3\xA4zyxa", 3, "\xC3\xA4\xC3\xA4\xE2\x80\xA6" },
+ { "\xC3\xA4\xC3\xA4zyxa", 4, "\xC3\xA4\xC3\xA4z\xE2\x80\xA6" },
+ { "\xC3\xA4\xC3\xA4zyxa", 5, "\xC3\xA4\xC3\xA4zy\xE2\x80\xA6" },
+ { "\xC3\xA4\xC3\xA4zyxa", 6, "\xC3\xA4\xC3\xA4zyxa" },
+ { "\xC3\xA4\xC3\xA4zyxa", 7, "\xC3\xA4\xC3\xA4zyxa" },
+ { "\xC3\xA4\xC3\xA4zyxa", 8, "\xC3\xA4\xC3\xA4zyxa" },
+ { "\001x\x1fy\x81", 10, "\xEF\xBF\xBDx\xEF\xBF\xBDy\xEF\xBF\xBD" }
+ };
+ const char *str;
+ string_t *str2;
+ unsigned int i;
+
+ test_begin("str_sanitize_utf8");
+ for (i = 0; i < N_ELEMENTS(tests); i++) {
+ str = str_sanitize_utf8(tests[i].str, tests[i].max_len);
+ if (tests[i].sanitized != NULL)
+ test_assert_idx(null_strcmp(str, tests[i].sanitized) == 0, i);
+ else
+ test_assert_idx(str == tests[i].str, i);
+ }
+ test_end();
+
+ test_begin("str_sanitize_append_utf8");
+ str2 = t_str_new(128);
+ for (i = 0; i < N_ELEMENTS(tests); i++) {
+ if (tests[i].str == NULL)
+ continue;
+ str_truncate(str2, 0);
+ str_append(str2, "1234567890");
+ str_sanitize_append_utf8(str2, tests[i].str, tests[i].max_len);
+
+ test_assert_idx(strncmp(str_c(str2), "1234567890", 10) == 0, i);
+ if (tests[i].sanitized != NULL)
+ test_assert_idx(strcmp(str_c(str2)+10, tests[i].sanitized) == 0, i);
+ else
+ test_assert_idx(strcmp(str_c(str2)+10, tests[i].str) == 0, i);
+ }
+ test_end();
+}
+
+void test_str_sanitize(void)
+{
+ test_str_sanitize_max_bytes();
+ test_str_sanitize_max_codepoints();
+}
diff --git a/src/lib/test-str-table.c b/src/lib/test-str-table.c
new file mode 100644
index 0000000..ea97d14
--- /dev/null
+++ b/src/lib/test-str-table.c
@@ -0,0 +1,33 @@
+/* Copyright (c) 2014-2018 Dovecot authors, see the included COPYING file */
+
+#include "test-lib.h"
+#include "str-table.h"
+
+void test_str_table(void)
+{
+ struct str_table *table;
+ const char *key1, *key2, *key1_copy, *key2_copy;
+
+ test_begin("str_table");
+ table = str_table_init();
+
+ key1 = str_table_ref(table, "str1");
+ key2 = str_table_ref(table, "str2");
+ test_assert(key1 != key2);
+ key1_copy = str_table_ref(table, "str1");
+ test_assert(key1_copy == key1);
+ key2_copy = str_table_ref(table, "str2");
+ test_assert(key2_copy == key2);
+
+ str_table_unref(table, &key1);
+ test_assert(key1 == NULL);
+ str_table_unref(table, &key1_copy);
+
+ str_table_unref(table, &key2);
+ str_table_unref(table, &key2_copy);
+ test_assert(str_table_is_empty(table));
+
+ str_table_deinit(&table);
+ test_assert(table == NULL);
+ test_end();
+}
diff --git a/src/lib/test-str.c b/src/lib/test-str.c
new file mode 100644
index 0000000..e20e7f9
--- /dev/null
+++ b/src/lib/test-str.c
@@ -0,0 +1,182 @@
+/* Copyright (c) 2012-2018 Dovecot authors, see the included COPYING file */
+
+#include "test-lib.h"
+#include "unichar.h"
+#include "str.h"
+
+static void test_str_append(void)
+{
+ string_t *str = t_str_new(32);
+ string_t *str2 = t_str_new(32);
+
+ test_begin("str_append_*()");
+ str_append(str, "foo");
+ str_append_c(str, '|');
+ str_append_c(str, '\0');
+ test_assert(str->used == 5 && memcmp(str_data(str), "foo|\0", 5) == 0);
+
+ str_append(str2, "sec");
+ str_append_c(str2, '\0');
+ str_append(str2, "ond");
+ str_append_str(str, str2);
+ test_assert(str->used == 5+7 && memcmp(str_data(str), "foo|\0sec\0ond", 5+7) == 0);
+
+ test_end();
+}
+
+static void test_str_c(void)
+{
+ string_t *str;
+ unsigned int i, j;
+
+ test_begin("str_c()");
+ str = t_str_new(0);
+ T_BEGIN {
+ (void)str_c(str);
+ } T_END;
+
+ for (i = 0; i < 32; i++) T_BEGIN {
+ str = t_str_new(15);
+ for (j = 0; j < i; j++)
+ str_append_c(str, 'x');
+ T_BEGIN {
+ (void)str_c(str);
+ } T_END;
+ } T_END;
+ test_end();
+}
+
+static void test_str_insert(void)
+{
+ string_t *str = t_str_new(32);
+
+ test_begin("str_insert()");
+ str_insert(str, 0, "foo");
+ str_insert(str, 3, ">");
+ str_insert(str, 3, "bar");
+ str_insert(str, 0, "<");
+ test_assert(str->used == 8 && memcmp(str_data(str), "<foobar>", 8) == 0);
+
+ str_insert(str, 10, "!");
+ test_assert(str->used == 11 && memcmp(str_data(str), "<foobar>\0\0!", 11) == 0);
+
+ test_end();
+}
+
+static void test_str_delete(void)
+{
+ string_t *str = t_str_new(32);
+
+ test_begin("str_delete()");
+ str_delete(str, 0, 100);
+ str_append(str, "123456");
+ str_delete(str, 0, 1);
+ str_delete(str, 4, 1);
+ str_delete(str, 1, 1);
+ test_assert(str->used == 3 && memcmp(str_data(str), "245", 3) == 0);
+
+ str_delete(str, 1, 2);
+ test_assert(str->used == 1 && memcmp(str_data(str), "2", 1) == 0);
+
+ str_append(str, "bar");
+ str_delete(str, 1, 100);
+ test_assert(str->used == 1 && memcmp(str_data(str), "2", 1) == 0);
+
+ test_end();
+}
+
+static void test_str_append_max(void)
+{
+ string_t *str = t_str_new(32);
+
+ test_begin("str_append_max()");
+ str_append_max(str, "foo", 0);
+ test_assert(str->used == 0);
+
+ str_append_max(str, "\0foo", 4);
+ test_assert(str->used == 0);
+
+ str_append_max(str, "foo", 3);
+ test_assert(str->used == 3 && memcmp(str_data(str), "foo", 3) == 0);
+ str_truncate(str, 0);
+
+ str_append_max(str, "foo", 2);
+ test_assert(str->used == 2 && memcmp(str_data(str), "fo", 2) == 0);
+ str_truncate(str, 0);
+
+ str_append_max(str, "foo\0bar", 7);
+ test_assert(str->used == 3 && memcmp(str_data(str), "foo", 3) == 0);
+ str_truncate(str, 0);
+ test_end();
+}
+
+static void test_str_truncate(void)
+{
+ string_t *str = t_str_new(8);
+ int i;
+
+ test_begin("str_truncate()");
+ str_append(str, "123456");
+ for (i = 100; i >= 6; i--) {
+ str_truncate(str, i);
+ test_assert_idx(str_len(str) == 6, i);
+ }
+ for (; i >= 0; i--) {
+ str_truncate(str, i);
+ test_assert_idx(str_len(str) == (unsigned int)i, i);
+ }
+ test_end();
+}
+
+static void test_str_truncate_utf8(void)
+{
+ string_t *str = t_str_new(8);
+ int i;
+
+ test_begin("str_truncate_utf8()");
+ str_append(str, "123456");
+ for (i = 100; i >= 6; i--) {
+ str_truncate_utf8(str, i);
+ test_assert_idx(str_len(str) == 6, i);
+ }
+ for (; i >= 0; i--) {
+ str_truncate_utf8(str, i);
+ test_assert_idx(str_len(str) == (unsigned int)i, i);
+ }
+
+ str_append(str, "\xE4\xB8\x80\xE4\xBa\x8C\xE4\xB8\x89"
+ "\xE5\x9b\x9b\xE4\xBa\x94\xE5\x85\xAD");
+ for (i = 100; i >= 18; i--) {
+ str_truncate_utf8(str, i);
+ test_assert_idx(str_len(str) == 18, i);
+ }
+ for (; i >= 0; i--) {
+ str_truncate_utf8(str, i);
+ test_assert_idx(str_len(str) % 3 == 0, i);
+ test_assert_idx((str_len(str) / 3) == ((unsigned int)i / 3), i);
+ }
+
+ str_append(str, "\xE4\xB8\x80""1""\xE4\xBa\x8C""2""\xE4\xB8\x89""3"
+ "\xE5\x9b\x9b""4""\xE4\xBa\x94""5""\xE5\x85\xAD""6");
+ for (i = 100; i >= 24; i--) {
+ str_truncate_utf8(str, i);
+ test_assert_idx(str_len(str) == 24, i);
+ }
+ for (; i >= 0; i--) {
+ str_truncate_utf8(str, i);
+ test_assert_idx(uni_utf8_data_is_valid(str_data(str),
+ str_len(str)), i);
+ }
+ test_end();
+}
+
+void test_str(void)
+{
+ test_str_append();
+ test_str_c();
+ test_str_insert();
+ test_str_delete();
+ test_str_append_max();
+ test_str_truncate();
+ test_str_truncate_utf8();
+}
diff --git a/src/lib/test-strescape.c b/src/lib/test-strescape.c
new file mode 100644
index 0000000..5b13232
--- /dev/null
+++ b/src/lib/test-strescape.c
@@ -0,0 +1,197 @@
+/* Copyright (c) 2009-2018 Dovecot authors, see the included COPYING file */
+
+#include "test-lib.h"
+#include "str.h"
+#include "strescape.h"
+
+struct strinput {
+ const char *input;
+ const char *output;
+};
+
+static const char tabescaped_input[] = "\0011\001t\001r\001nplip\001n";
+static const char tabescaped_input_with_nul[] = "\0011\001t\001r\001nplip\001n\0010";
+static const char tabunescaped_input[] = "\001\t\r\nplip\n";
+
+static const char *wrong_tabescaped_input = "a\001\001b\001\nc\0011\001t\001r\001nplip\001n";
+static const char *wrong_tabescaped_output = "a\001b\nc\001\t\r\nplip\n";
+
+static struct {
+ const char *input;
+ const char *const *output;
+} strsplit_tests[] = {
+ { /*tabescaped_input3*/NULL, (const char *const []) {
+ tabunescaped_input,
+ tabunescaped_input,
+ tabunescaped_input,
+ "",
+ NULL
+ } },
+ { "", (const char *const []) { NULL } },
+ { "\t", (const char *const []) { "", "", NULL } },
+ { tabescaped_input, (const char *const []) {
+ tabunescaped_input,
+ NULL
+ } },
+};
+
+static void test_str_escape(void)
+{
+ static const struct strinput unesc[] = {
+ { "foo", "foo" },
+ { "\\\\\\\\\\\"\\\"\\\'\\\'", "\\\\\"\"\'\'" },
+ { "\\a\\n\\r\\", "anr" }
+ };
+ static const struct strinput tabesc[] = {
+ { "foo", "foo" },
+ { "\001", "\0011" },
+ { "\t", "\001t" },
+ { "\r", "\001r" },
+ { "\n", "\001n" },
+ { "\001\001\t\t\r\r\n\n", "\0011\0011\001t\001t\001r\001r\001n\001n" }
+ };
+ unsigned char buf[1 << CHAR_BIT];
+ const char *escaped, *tabstr, *unesc_str;
+ string_t *str;
+ unsigned int i;
+
+ test_begin("str_escape");
+ for (i = 1; i < sizeof(buf); i++)
+ buf[i-1] = i;
+ buf[i-1] = '\0';
+
+ escaped = str_escape((char *)buf);
+ test_assert(strlen(escaped) == (1 << CHAR_BIT) - 1 + 3);
+ test_assert(escaped['\"'-1] == '\\'); /* 34 */
+ test_assert(escaped['\"'] == '\"');
+ test_assert(escaped['\''+1-1] == '\\'); /* 39 */
+ test_assert(escaped['\''+1] == '\'');
+ test_assert(escaped['\\'+2-1] == '\\'); /* 92 */
+ test_assert(escaped['\\'+2] == '\\');
+ test_assert(strcmp(str_escape("\\\\\"\"\'\'"),
+ "\\\\\\\\\\\"\\\"\\\'\\\'") == 0);
+ test_end();
+
+ test_begin("str_nescape");
+
+ escaped = str_nescape("\"escape only first but not 'this'", 10);
+ test_assert(strcmp(escaped, "\\\"escape on") == 0);
+
+ escaped = str_nescape("\"hello\"\0\"world\"", 15);
+ test_assert(memcmp(escaped, "\\\"hello\\\"\0\\\"world\\\"", 19) == 0);
+
+ test_end();
+
+ str = t_str_new(256);
+ test_begin("str_unescape");
+ for (i = 0; i < N_ELEMENTS(unesc); i++) {
+ test_assert(strcmp(str_unescape(t_strdup_noconst(unesc[i].input)),
+ unesc[i].output) == 0);
+ str_truncate(str, 0);
+ str_append_unescaped(str, unesc[i].input, strlen(unesc[i].input));
+ test_assert(strcmp(str_c(str), unesc[i].output) == 0);
+ }
+ test_end();
+
+ test_begin("str_unescape_next");
+ escaped = "foo\"bar\\\"b\\\\az\"plop";
+ test_assert(str_unescape_next(&escaped, &unesc_str) == 0);
+ test_assert(strcmp(unesc_str, "foo") == 0);
+ test_assert(str_unescape_next(&escaped, &unesc_str) == 0);
+ test_assert(strcmp(unesc_str, "bar\"b\\az") == 0);
+ test_assert(str_unescape_next(&escaped, &unesc_str) == -1);
+ escaped = "foo\\";
+ test_assert(str_unescape_next(&escaped, &unesc_str) == -1);
+ test_end();
+
+ test_begin("str_tabescape");
+ for (i = 0; i < N_ELEMENTS(tabesc); i++) {
+ test_assert(strcmp(t_str_tabunescape(tabesc[i].output),
+ tabesc[i].input) == 0);
+ test_assert(strcmp(str_tabunescape(t_strdup_noconst(tabesc[i].output)),
+ tabesc[i].input) == 0);
+ test_assert(strcmp(str_tabescape(tabesc[i].input),
+ tabesc[i].output) == 0);
+ str_truncate(str, 0);
+ str_append_tabunescaped(str, tabesc[i].output, strlen(tabesc[i].output));
+ test_assert(strcmp(str_c(str), tabesc[i].input) == 0);
+ }
+ str_truncate(str, 0);
+ tabstr = "\0012\001l\001";
+ str_append_tabunescaped(str, tabstr, strlen(tabstr));
+ test_assert(strcmp(str_c(str), "2l") == 0);
+ test_assert(strcmp(str_c(str), str_tabunescape(t_strdup_noconst(tabstr))) == 0);
+ test_end();
+}
+
+static void test_tabescape(void)
+{
+ string_t *str = t_str_new(128);
+
+ test_begin("string tabescaping");
+ test_assert(strcmp(str_tabescape(tabunescaped_input), tabescaped_input) == 0);
+
+ str_append_tabescaped(str, tabunescaped_input);
+ test_assert(strcmp(str_c(str), tabescaped_input) == 0);
+
+ /* test escaping the trailing NUL as well */
+ str_truncate(str, 0);
+ str_append_tabescaped_n(str, (const unsigned char *)tabunescaped_input,
+ strlen(tabunescaped_input)+1);
+ test_assert_strcmp(str_c(str), tabescaped_input_with_nul);
+
+ /* unescaping */
+ str_truncate(str, 0);
+ str_append_tabunescaped(str, tabescaped_input, strlen(tabescaped_input));
+ test_assert(strcmp(str_c(str), tabunescaped_input) == 0);
+
+ test_assert(strcmp(str_tabunescape(t_strdup_noconst(tabescaped_input)), tabunescaped_input) == 0);
+ test_assert(strcmp(t_str_tabunescape(tabescaped_input), tabunescaped_input) == 0);
+
+ /* unescaping with wrongly written tabescape-input */
+ str_truncate(str, 0);
+ str_append_tabunescaped(str, wrong_tabescaped_input, strlen(wrong_tabescaped_input));
+ test_assert(strcmp(str_c(str), wrong_tabescaped_output) == 0);
+
+ test_assert(strcmp(str_tabunescape(t_strdup_noconst(wrong_tabescaped_input)), wrong_tabescaped_output) == 0);
+ test_assert(strcmp(t_str_tabunescape(wrong_tabescaped_input), wrong_tabescaped_output) == 0);
+
+ test_end();
+}
+
+static void test_strsplit_tabescaped(void)
+{
+ const char *const *args;
+
+ test_begin("*_strsplit_tabescaped()");
+ for (unsigned int i = 0; i < N_ELEMENTS(strsplit_tests); i++) {
+ args = t_strsplit_tabescaped(strsplit_tests[i].input);
+ for (unsigned int j = 0; strsplit_tests[i].output[j] != NULL; j++)
+ test_assert_idx(null_strcmp(strsplit_tests[i].output[j], args[j]) == 0, i);
+ }
+ test_end();
+}
+
+static void test_strsplit_tabescaped_inplace(void)
+{
+ const char *const *args;
+
+ test_begin("*_strsplit_tabescaped_inplace()");
+ for (unsigned int i = 0; i < N_ELEMENTS(strsplit_tests); i++) {
+ char *input = t_strdup_noconst(strsplit_tests[i].input);
+ args = t_strsplit_tabescaped_inplace(input);
+ for (unsigned int j = 0; strsplit_tests[i].output[j] != NULL; j++)
+ test_assert_idx(null_strcmp(strsplit_tests[i].output[j], args[j]) == 0, i);
+ }
+ test_end();
+}
+
+void test_strescape(void)
+{
+ strsplit_tests[0].input = t_strdup_printf("%s\t%s\t%s\t",
+ tabescaped_input, tabescaped_input, tabescaped_input);
+ test_str_escape();
+ test_tabescape();
+ test_strsplit_tabescaped();
+ test_strsplit_tabescaped_inplace();
+}
diff --git a/src/lib/test-strfuncs.c b/src/lib/test-strfuncs.c
new file mode 100644
index 0000000..b546384
--- /dev/null
+++ b/src/lib/test-strfuncs.c
@@ -0,0 +1,640 @@
+/* Copyright (c) 2009-2018 Dovecot authors, see the included COPYING file */
+
+#include "test-lib.h"
+#include "array.h"
+
+static void test_p_strdup(void)
+{
+ test_begin("p_strdup()");
+ test_assert(p_strdup(default_pool, NULL) == NULL);
+
+ const char *src = "foo";
+ char *str = p_strdup(default_pool, src);
+ test_assert(str != src && str != NULL && strcmp(src, str) == 0);
+ p_free(default_pool, str);
+
+ test_end();
+}
+
+static void test_p_strndup(void)
+{
+ struct {
+ const char *input;
+ const char *output;
+ size_t len;
+ } tests[] = {
+ { "foo", "fo", 2 },
+ { "foo", "foo", 3 },
+ { "foo", "foo", 4 },
+ { "foo\0more", "foo", 8 },
+ };
+ test_begin("p_strndup()");
+
+ for (unsigned int i = 0; i < N_ELEMENTS(tests); i++) {
+ char *str = p_strndup(default_pool, tests[i].input,
+ tests[i].len);
+ test_assert_strcmp_idx(str, tests[i].output, i);
+ p_free(default_pool, str);
+ }
+ test_end();
+}
+
+static void test_p_strdup_empty(void)
+{
+ test_begin("p_strdup_empty()");
+ test_assert(p_strdup_empty(default_pool, NULL) == NULL);
+ test_assert(p_strdup_empty(default_pool, "") == NULL);
+
+ const char *src = "foo";
+ char *str = p_strdup_empty(default_pool, src);
+ test_assert(str != src && str != NULL && strcmp(src, str) == 0);
+ p_free(default_pool, str);
+
+ test_end();
+}
+
+static void test_p_strdup_until(void)
+{
+ const char src[] = "foo\0bar";
+ char *str;
+
+ test_begin("p_strdup_until()");
+ str = p_strdup_until(default_pool, src, src+2);
+ test_assert(strcmp(str, "fo") == 0);
+ p_free(default_pool, str);
+
+ str = p_strdup_until(default_pool, src, src+3);
+ test_assert(strcmp(str, "foo") == 0);
+ p_free(default_pool, str);
+
+ /* \0 is ignored */
+ str = p_strdup_until(default_pool, src, src+7);
+ test_assert(memcmp(str, src, sizeof(src)) == 0);
+ p_free(default_pool, str);
+
+ str = p_strdup_until(default_pool, src, src+8);
+ test_assert(memcmp(str, src, sizeof(src)) == 0);
+ p_free(default_pool, str);
+
+ test_end();
+}
+
+static void test_p_strarray_dup(void)
+{
+ const char *input[][3] = {
+ { NULL },
+ { "a", NULL },
+ { "foobar", NULL },
+ { "a", "foo", NULL }
+ };
+ const char **ret;
+ unsigned int i, j;
+
+ test_begin("p_strarray_dup");
+
+ for (i = 0; i < N_ELEMENTS(input); i++) {
+ ret = p_strarray_dup(default_pool, input[i]);
+ for (j = 0; input[i][j] != NULL; j++) {
+ test_assert(strcmp(input[i][j], ret[j]) == 0);
+ test_assert(input[i][j] != ret[j]);
+ }
+ test_assert(ret[j] == NULL);
+ i_free(ret);
+ }
+ test_end();
+}
+
+static void test_t_strsplit(void)
+{
+ struct {
+ const char *input;
+ const char *const *output;
+ } tests[] = {
+ /* empty string -> empty array. was this perhaps a mistake for
+ the API to do this originally?.. can't really change now
+ anyway. */
+ { "", (const char *const []) { NULL } },
+ { "\n", (const char *const []) { "", "", NULL } },
+ { "\n\n", (const char *const []) { "", "", "", NULL } },
+ { "foo", (const char *const []) { "foo", NULL } },
+ { "foo\n", (const char *const []) { "foo", "", NULL } },
+ { "foo\nbar", (const char *const []) { "foo", "bar", NULL } },
+ { "foo\nbar\n", (const char *const []) { "foo", "bar", "", NULL } },
+ { "\nfoo\n\nbar\n\n", (const char *const []) { "", "foo", "", "bar", "", "", NULL } },
+ };
+ const char *const *args, *const *args2, *const *args3;
+
+ test_begin("t_strsplit");
+
+ for (unsigned int i = 0; i < N_ELEMENTS(tests); i++) {
+ /* split_str_fast() with single separator */
+ args = t_strsplit(tests[i].input, "\n");
+ /* split_str_slow() with a secondary separator */
+ args2 = t_strsplit(tests[i].input, "\r\n");
+ /* also as suffix */
+ args3 = t_strsplit(tests[i].input, "\n\r");
+ for (unsigned int j = 0; tests[i].output[j] != NULL; j++) {
+ test_assert_idx(null_strcmp(tests[i].output[j], args[j]) == 0, i);
+ test_assert_idx(null_strcmp(args[j], args2[j]) == 0, i);
+ test_assert_idx(null_strcmp(args[j], args3[j]) == 0, i);
+ }
+ }
+ test_end();
+}
+
+static void test_t_strsplit_spaces(void)
+{
+ struct {
+ const char *input;
+ const char *const *output;
+ } tests[] = {
+ /* empty strings */
+ { "", (const char *const []) { NULL } },
+ { "\n", (const char *const []) { NULL } },
+ { "\n\n", (const char *const []) { NULL } },
+ /* normal */
+ { "foo", (const char *const []) { "foo", NULL } },
+ { "foo\n", (const char *const []) { "foo", NULL } },
+ { "foo\nbar", (const char *const []) { "foo", "bar", NULL } },
+ { "foo\nbar\n", (const char *const []) { "foo", "bar", NULL } },
+ { "\nfoo\n\nbar\n\n", (const char *const []) { "foo", "bar", NULL } },
+ };
+ const char *const *args, *const *args2, *const *args3;
+
+ test_begin("t_strsplit_spaces");
+
+ for (unsigned int i = 0; i < N_ELEMENTS(tests); i++) {
+ args = t_strsplit_spaces(tests[i].input, "\n");
+ /* test also with a secondary nonexistent separator */
+ args2 = t_strsplit_spaces(tests[i].input, "\r\n");
+ /* also as suffix */
+ args3 = t_strsplit_spaces(tests[i].input, "\n\r");
+ for (unsigned int j = 0; tests[i].output[j] != NULL; j++) {
+ test_assert_idx(null_strcmp(tests[i].output[j], args[j]) == 0, i);
+ test_assert_idx(null_strcmp(args[j], args2[j]) == 0, i);
+ test_assert_idx(null_strcmp(args[j], args3[j]) == 0, i);
+ }
+ }
+
+ /* multiple separators */
+ args = t_strsplit_spaces(" , , ,str1 , ,,, , str2 , ", " ,");
+ test_assert(strcmp(args[0], "str1") == 0);
+ test_assert(strcmp(args[1], "str2") == 0);
+ test_assert(args[2] == NULL);
+ test_end();
+}
+
+static void test_t_str_replace(void)
+{
+ test_begin("t_str_replace");
+ test_assert(strcmp(t_str_replace("foo", 'a', 'b'), "foo") == 0);
+ test_assert(strcmp(t_str_replace("fooa", 'a', 'b'), "foob") == 0);
+ test_assert(strcmp(t_str_replace("afooa", 'a', 'b'), "bfoob") == 0);
+ test_assert(strcmp(t_str_replace("", 'a', 'b'), "") == 0);
+ test_assert(strcmp(t_str_replace("a", 'a', 'b'), "b") == 0);
+ test_assert(strcmp(t_str_replace("aaa", 'a', 'b'), "bbb") == 0);
+ test_assert(strcmp(t_str_replace("bbb", 'a', 'b'), "bbb") == 0);
+ test_assert(strcmp(t_str_replace("aba", 'a', 'b'), "bbb") == 0);
+ test_end();
+}
+
+static void test_t_str_oneline(void)
+{
+ test_begin("t_str_oneline");
+ test_assert(strcmp(t_str_oneline("\n"), "") == 0);
+ test_assert(strcmp(t_str_oneline("\r"), "") == 0);
+ test_assert(strcmp(t_str_oneline("\n\n"), "") == 0);
+ test_assert(strcmp(t_str_oneline("\r\r"), "") == 0);
+ test_assert(strcmp(t_str_oneline("\r\n"), "") == 0);
+ test_assert(strcmp(t_str_oneline("\r\n\r\n"), "") == 0);
+ test_assert(strcmp(t_str_oneline("\n\r"), "") == 0);
+ test_assert(strcmp(t_str_oneline("\n\r\n\r"), "") == 0);
+ test_assert(strcmp(t_str_oneline("foo"), "foo") == 0);
+ test_assert(strcmp(t_str_oneline("\nfoo"), "foo") == 0);
+ test_assert(strcmp(t_str_oneline("foo\n"), "foo") == 0);
+ test_assert(strcmp(t_str_oneline("\nfoo\n"), "foo") == 0);
+ test_assert(strcmp(t_str_oneline("foo\nbar"), "foo bar") == 0);
+ test_assert(strcmp(t_str_oneline("foo\n\nbar"), "foo bar") == 0);
+ test_assert(strcmp(t_str_oneline("\nfoo\nbar"), "foo bar") == 0);
+ test_assert(strcmp(t_str_oneline("foo\nbar\n"), "foo bar") == 0);
+ test_assert(strcmp(t_str_oneline("foo\nbar\nbaz"), "foo bar baz") == 0);
+ test_assert(strcmp(t_str_oneline("\rfoo"), "foo") == 0);
+ test_assert(strcmp(t_str_oneline("foo\r"), "foo") == 0);
+ test_assert(strcmp(t_str_oneline("\rfoo\r"), "foo") == 0);
+ test_assert(strcmp(t_str_oneline("foo\rbar"), "foobar") == 0);
+ test_assert(strcmp(t_str_oneline("foo\r\rbar"), "foobar") == 0);
+ test_assert(strcmp(t_str_oneline("\rfoo\rbar"), "foobar") == 0);
+ test_assert(strcmp(t_str_oneline("foo\rbar\r"), "foobar") == 0);
+ test_assert(strcmp(t_str_oneline("foo\rbar\rbaz"), "foobarbaz") == 0);
+ test_assert(strcmp(t_str_oneline("\r\nfoo\r\n"), "foo") == 0);
+ test_assert(strcmp(t_str_oneline("foo\r\n"), "foo") == 0);
+ test_assert(strcmp(t_str_oneline("\r\nfoo"), "foo") == 0);
+ test_assert(strcmp(t_str_oneline("foo\r\nbar"), "foo bar") == 0);
+ test_assert(strcmp(t_str_oneline("foo\r\n\r\nbar"), "foo bar") == 0);
+ test_assert(strcmp(t_str_oneline("\r\nfoo\r\nbar"), "foo bar") == 0);
+ test_assert(strcmp(t_str_oneline("foo\r\nbar\r\n"), "foo bar") == 0);
+ test_assert(strcmp(t_str_oneline("foo\r\nbar\r\nbaz"), "foo bar baz") == 0);
+ test_end();
+}
+
+static void test_t_str_trim(void)
+{
+ test_begin("t_str_trim");
+ test_assert(strcmp(t_str_trim("", " "), "") == 0);
+ test_assert(strcmp(t_str_trim(" ", " "), "") == 0);
+ test_assert(strcmp(t_str_trim(" \t ", "\t "), "") == 0);
+ test_assert(strcmp(t_str_trim("f \t ", "\t "), "f") == 0);
+ test_assert(strcmp(t_str_trim("foo", ""), "foo") == 0);
+ test_assert(strcmp(t_str_trim("foo", " "), "foo") == 0);
+ test_assert(strcmp(t_str_trim("foo ", " "), "foo") == 0);
+ test_assert(strcmp(t_str_trim(" foo", " "), "foo") == 0);
+ test_assert(strcmp(t_str_trim(" foo ", " "), "foo") == 0);
+ test_assert(strcmp(t_str_trim("\tfoo ", "\t "), "foo") == 0);
+ test_assert(strcmp(t_str_trim(" \tfoo\t ", "\t "), "foo") == 0);
+ test_assert(strcmp(t_str_trim("\r \tfoo\t \r", "\t \r"), "foo") == 0);
+ test_assert(strcmp(t_str_trim("\r \tfoo foo\t \r", "\t \r"), "foo foo") == 0);
+ test_assert(strcmp(t_str_trim("\tfoo\tfoo\t", "\t \r"), "foo\tfoo") == 0);
+ test_end();
+}
+
+static void test_t_str_ltrim(void)
+{
+ test_begin("t_str_ltrim");
+ test_assert(strcmp(t_str_ltrim("", " "), "") == 0);
+ test_assert(strcmp(t_str_ltrim(" ", " "), "") == 0);
+ test_assert(strcmp(t_str_ltrim(" \t ", "\t "), "") == 0);
+ test_assert(strcmp(t_str_ltrim(" \t f", "\t "), "f") == 0);
+ test_assert(strcmp(t_str_ltrim("foo", ""), "foo") == 0);
+ test_assert(strcmp(t_str_ltrim("foo", " "), "foo") == 0);
+ test_assert(strcmp(t_str_ltrim("foo ", " "), "foo ") == 0);
+ test_assert(strcmp(t_str_ltrim(" foo", " "), "foo") == 0);
+ test_assert(strcmp(t_str_ltrim(" foo ", " "), "foo ") == 0);
+ test_assert(strcmp(t_str_ltrim("\tfoo ", "\t "), "foo ") == 0);
+ test_assert(strcmp(t_str_ltrim(" \tfoo\t ", "\t "), "foo\t ") == 0);
+ test_assert(strcmp(t_str_ltrim("\r \tfoo\t \r", "\t \r"), "foo\t \r") == 0);
+ test_assert(strcmp(t_str_ltrim("\r \tfoo foo\t \r", "\t \r"), "foo foo\t \r") == 0);
+ test_assert(strcmp(t_str_ltrim("\tfoo\tfoo\t", "\t \r"), "foo\tfoo\t") == 0);
+ test_end();
+}
+
+static void test_t_str_rtrim(void)
+{
+ test_begin("t_str_rtrim");
+ test_assert(strcmp(t_str_rtrim("", " "), "") == 0);
+ test_assert(strcmp(t_str_rtrim(" ", " "), "") == 0);
+ test_assert(strcmp(t_str_rtrim(" \t ", "\t "), "") == 0);
+ test_assert(strcmp(t_str_rtrim("f \t ", "\t "), "f") == 0);
+ test_assert(strcmp(t_str_rtrim("foo", ""), "foo") == 0);
+ test_assert(strcmp(t_str_rtrim("foo", " "), "foo") == 0);
+ test_assert(strcmp(t_str_rtrim("foo ", " "), "foo") == 0);
+ test_assert(strcmp(t_str_rtrim(" foo", " "), " foo") == 0);
+ test_assert(strcmp(t_str_rtrim(" foo ", " "), " foo") == 0);
+ test_assert(strcmp(t_str_rtrim("\tfoo ", "\t "), "\tfoo") == 0);
+ test_assert(strcmp(t_str_rtrim(" \tfoo\t ", "\t "), " \tfoo") == 0);
+ test_assert(strcmp(t_str_rtrim("\r \tfoo\t \r", "\t \r"), "\r \tfoo") == 0);
+ test_assert(strcmp(t_str_rtrim("\r \tfoo foo\t \r", "\t \r"), "\r \tfoo foo") == 0);
+ test_assert(strcmp(t_str_rtrim("\tfoo\tfoo\t", "\t \r"), "\tfoo\tfoo") == 0);
+ test_end();
+}
+
+static const char *const test_strarray_input[] = {
+ "", "hello", "world", "", "yay", "", NULL
+};
+static const struct {
+ const char *separator;
+ const char *output;
+} test_strarray_outputs[] = {
+ { "", "helloworldyay" },
+ { " ", " hello world yay " },
+ { "!-?", "!-?hello!-?world!-?!-?yay!-?" }
+};
+
+static const char *const test_strarray_input2[] = {
+ "", "", "hello", "world", "", "yay", "", NULL
+};
+static struct {
+ const char *separator;
+ const char *output;
+} test_strarray_outputs2[] = {
+ { "", "helloworldyay" },
+ { " ", " hello world yay " },
+ { "!-?", "!-?!-?hello!-?world!-?!-?yay!-?" }
+};
+
+static const char *const test_strarray_input3[] = {
+ "hello", "", "", "yay", NULL
+};
+static struct {
+ const char *separator;
+ const char *output;
+} test_strarray_outputs3[] = {
+ { "", "helloyay" },
+ { " ", "hello yay" },
+ { "!-?", "hello!-?!-?!-?yay" }
+};
+
+static void test_t_strarray_join(void)
+{
+ const char *null = NULL;
+ unsigned int i;
+
+ test_begin("t_strarray_join()");
+
+ /* empty array -> empty string */
+ test_assert(strcmp(t_strarray_join(&null, " "), "") == 0);
+
+ for (i = 0; i < N_ELEMENTS(test_strarray_outputs); i++) {
+ test_assert_idx(strcmp(t_strarray_join(test_strarray_input,
+ test_strarray_outputs[i].separator),
+ test_strarray_outputs[i].output) == 0, i);
+ }
+ for (i = 0; i < N_ELEMENTS(test_strarray_outputs2); i++) {
+ test_assert_idx(strcmp(t_strarray_join(test_strarray_input2,
+ test_strarray_outputs2[i].separator),
+ test_strarray_outputs2[i].output) == 0, i);
+ }
+ for (i = 0; i < N_ELEMENTS(test_strarray_outputs3); i++) {
+ test_assert_idx(strcmp(t_strarray_join(test_strarray_input3,
+ test_strarray_outputs3[i].separator),
+ test_strarray_outputs3[i].output) == 0, i);
+ }
+ test_end();
+}
+
+static void test_p_array_const_string_join(void)
+{
+ ARRAY_TYPE(const_string) arr;
+ unsigned int i;
+ char *res;
+
+ test_begin("p_array_const_string_join()");
+
+ i_array_init(&arr, 2);
+ /* empty array -> empty string */
+ test_assert(strcmp(t_array_const_string_join(&arr, " "), "") == 0);
+
+ array_append(&arr, test_strarray_input,
+ str_array_length(test_strarray_input));
+ for (i = 0; i < N_ELEMENTS(test_strarray_outputs); i++) {
+ res = p_array_const_string_join(default_pool, &arr,
+ test_strarray_outputs[i].separator);
+ test_assert_idx(strcmp(res, test_strarray_outputs[i].output) == 0, i);
+ i_free(res);
+ }
+
+ array_free(&arr);
+ test_end();
+}
+
+static void test_mem_equals_timing_safe(void)
+{
+ const struct {
+ const char *a, *b;
+ } tests[] = {
+ { "", "" },
+ { "a", "a" },
+ { "b", "a" },
+ { "ab", "ab" },
+ { "ab", "ba" },
+ { "ab", "bc" },
+ };
+ test_begin("mem_equals_timing_safe()");
+ for (unsigned int i = 0; i < N_ELEMENTS(tests); i++) {
+ size_t len = strlen(tests[i].a);
+ i_assert(len == strlen(tests[i].b));
+ test_assert((memcmp(tests[i].a, tests[i].b, len) == 0) ==
+ mem_equals_timing_safe(tests[i].a, tests[i].b, len));
+ test_assert((memcmp(tests[i].a, tests[i].b, len) == 0) ==
+ mem_equals_timing_safe(tests[i].b, tests[i].a, len));
+ }
+ test_end();
+}
+
+static void test_str_equals_timing_almost_safe(void)
+{
+ const struct {
+ const char *a, *b;
+ } tests[] = {
+ { "", "" },
+ { "a", "a" },
+ { "b", "a" },
+ { "ab", "ab" },
+ { "ab", "ba" },
+ { "ab", "bc" },
+ { "a", "" },
+ { "a", "ab" },
+ { "a", "abc" },
+ { "ab", "abc" },
+ };
+ test_begin("str_equals_timing_almost_safe()");
+ for (unsigned int i = 0; i < N_ELEMENTS(tests); i++) {
+ test_assert((strcmp(tests[i].a, tests[i].b) == 0) ==
+ str_equals_timing_almost_safe(tests[i].a, tests[i].b));
+ test_assert((strcmp(tests[i].a, tests[i].b) == 0) ==
+ str_equals_timing_almost_safe(tests[i].b, tests[i].a));
+ }
+ test_end();
+}
+
+static void test_dec2str_buf(void)
+{
+ const uintmax_t test_input[] = {
+ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10,
+ 99, 999, 9999, 65535, 65536, 99999, 999999, 9999999,
+ 99999999, 999999999, 4294967295, 4294967296ULL,
+ 9999999999999999999ULL,
+ 18446744073709551615ULL
+ };
+ char buf[MAX_INT_STRLEN], buf2[MAX_INT_STRLEN];
+
+ test_begin("dec2str_buf()");
+ for (unsigned int i = 0; i < N_ELEMENTS(test_input); i++) {
+ i_snprintf(buf2, sizeof(buf2), "%ju", test_input[i]);
+ test_assert_idx(strcmp(dec2str_buf(buf, test_input[i]),
+ buf2) == 0, i);
+ }
+ test_end();
+}
+
+static void
+test_str_match(void)
+{
+ static const struct {
+ const char*s1, *s2; size_t match;
+ } tests[] = {
+#define MATCH_TEST(common, left, right) { common left, common right, sizeof(common)-1 }
+ MATCH_TEST("", "", ""),
+ MATCH_TEST("", "x", ""),
+ MATCH_TEST("", "", "x"),
+ MATCH_TEST("", "foo", "bar"),
+ MATCH_TEST("x", "", ""),
+ MATCH_TEST("x", "y", "z"),
+ MATCH_TEST("blahblahblah", "", ""),
+ MATCH_TEST("blahblahblah", "", "bar"),
+ MATCH_TEST("blahblahblah", "foo", ""),
+ MATCH_TEST("blahblahblah", "foo", "bar"),
+#undef MATCH_TEST
+ };
+
+ unsigned int i;
+
+ test_begin("str_match");
+ for (i = 0; i < N_ELEMENTS(tests); i++)
+ test_assert_idx(str_match(tests[i].s1, tests[i].s2) == tests[i].match, i);
+ test_end();
+
+ test_begin("str_begins");
+ for (i = 0; i < N_ELEMENTS(tests); i++) {
+ /* This is just 2 ways of wording the same test, but that also
+ sanity tests the match values above. */
+ test_assert_idx(str_begins(tests[i].s1, tests[i].s2) ==
+ (str_begins(tests[i].s1, tests[i].s2)), i);
+ test_assert_idx(str_begins(tests[i].s1, tests[i].s2) ==
+ (strlen(tests[i].s2) == tests[i].match), i);
+ }
+ test_end();
+}
+
+static void test_memspn(void)
+{
+#undef TEST_CASE
+/* we substract 1 to ensure we don't include the final \0 byte */
+#define TEST_CASE(a, b, r) { \
+ .input = (const unsigned char*)((a)), .input_len = sizeof((a))-1, \
+ .accept = (const unsigned char*)((b)), .accept_len = sizeof((b))-1, \
+ .result = r, \
+}
+
+ static struct {
+ const unsigned char *input;
+ size_t input_len;
+ const unsigned char *accept;
+ size_t accept_len;
+ size_t result;
+ } tests[] = {
+ TEST_CASE("", "", 0),
+ TEST_CASE("", "123456789", 0),
+ TEST_CASE("123456789", "", 0),
+ TEST_CASE("hello, world", "helo", 5),
+ TEST_CASE("hello, uuuuu", "helo", 5),
+ TEST_CASE("\0\0\0\0\0hello", "\0", 5),
+ TEST_CASE("\r\r\r\r", "\r", 4),
+ TEST_CASE("aaa", "a", 3),
+ TEST_CASE("bbb", "a", 0),
+ /* null safety test */
+ {
+ .input = NULL, .accept = NULL,
+ .input_len = 0, .accept_len = 0,
+ .result = 0,
+ }
+ };
+
+ test_begin("i_memspn");
+
+ for (unsigned int i = 0; i < N_ELEMENTS(tests); i++) {
+ size_t a = i_memspn(tests[i].input, tests[i].input_len,
+ tests[i].accept, tests[i].accept_len);
+ test_assert_ucmp_idx(a, ==, tests[i].result, i);
+ if (tests[i].input == NULL)
+ continue;
+ a = i_memspn(tests[i].input, strlen((const char*)tests[i].input),
+ tests[i].accept, strlen((const char*)tests[i].accept));
+ size_t b = strspn((const char*)tests[i].input,
+ (const char*)tests[i].accept);
+ test_assert_ucmp_idx(a, ==, b, i);
+ }
+
+ test_end();
+}
+
+static void test_memcspn(void)
+{
+#undef TEST_CASE
+/* we substract 1 to ensure we don't include the final \0 byte */
+#define TEST_CASE(a, b, r) { \
+ .input = (const unsigned char*)((a)), .input_len = sizeof((a))-1, \
+ .reject = (const unsigned char*)((b)), .reject_len = sizeof((b))-1, \
+ .result = r, \
+}
+
+ static struct {
+ const unsigned char *input;
+ size_t input_len;
+ const unsigned char *reject;
+ size_t reject_len;
+ size_t result;
+ } tests[] = {
+ TEST_CASE("", "", 0),
+ TEST_CASE("hello", "", 5),
+ TEST_CASE("uuuuu, hello", "helo", 7),
+ TEST_CASE("\0\0\0\0\0\0hello", "u", 11),
+ TEST_CASE("this\0is\0test", "\0", 4),
+ TEST_CASE("hello, world\r", "\r", 12),
+ TEST_CASE("aaa", "a", 0),
+ TEST_CASE("bbb", "a", 3),
+ /* null safety test */
+ {
+ .input = NULL, .reject = NULL,
+ .input_len = 0, .reject_len = 0,
+ .result = 0,
+ }
+ };
+
+ test_begin("i_memcspn");
+
+ for (unsigned int i = 0; i < N_ELEMENTS(tests); i++) {
+ size_t a = i_memcspn(tests[i].input, tests[i].input_len,
+ tests[i].reject, tests[i].reject_len);
+ test_assert_ucmp_idx(a, ==, tests[i].result, i);
+ if (tests[i].input == NULL)
+ continue;
+ a = i_memcspn(tests[i].input, strlen((const char*)tests[i].input),
+ tests[i].reject, strlen((const char*)tests[i].reject));
+ size_t b = strcspn((const char*)tests[i].input,
+ (const char*)tests[i].reject);
+ test_assert_ucmp_idx(a, ==, b, i);
+ }
+
+ test_end();
+}
+
+void test_strfuncs(void)
+{
+ test_p_strdup();
+ test_p_strndup();
+ test_p_strdup_empty();
+ test_p_strdup_until();
+ test_p_strarray_dup();
+ test_t_strsplit();
+ test_t_strsplit_spaces();
+ test_t_str_replace();
+ test_t_str_oneline();
+ test_t_str_trim();
+ test_t_str_ltrim();
+ test_t_str_rtrim();
+ test_t_strarray_join();
+ test_p_array_const_string_join();
+ test_mem_equals_timing_safe();
+ test_str_equals_timing_almost_safe();
+ test_dec2str_buf();
+ test_str_match();
+ test_memspn();
+ test_memcspn();
+}
+
+enum fatal_test_state fatal_strfuncs(unsigned int stage)
+{
+ switch (stage) {
+ case 0:
+ test_begin("fatal p_strndup()");
+ test_expect_fatal_string("(str != NULL)");
+ (void)p_strndup(default_pool, NULL, 100);
+ return FATAL_TEST_FAILURE;
+ case 1:
+ test_expect_fatal_string("(max_chars != SIZE_MAX)");
+ (void)p_strndup(default_pool, "foo", SIZE_MAX);
+ return FATAL_TEST_FAILURE;
+ }
+ test_end();
+ return FATAL_TEST_FINISHED;
+}
diff --git a/src/lib/test-strnum.c b/src/lib/test-strnum.c
new file mode 100644
index 0000000..892c579
--- /dev/null
+++ b/src/lib/test-strnum.c
@@ -0,0 +1,398 @@
+/* Copyright (c) 2014-2018 Dovecot authors, see the included COPYING file */
+
+#include "test-lib.h"
+
+
+#define INVALID(n) { #n, -1, 0 }
+#define VALID(n) { #n, 0, n }
+
+/* always pads with leading zeros to a size of 9 digits */
+static int crappy_uintmax_to_str(char *into, uintmax_t val)
+{
+#define BIGBASE 1000000000ull
+#define STRINGIFY(s) #s
+#define STRINGIFY2(s) STRINGIFY(s)
+ int len = 0;
+ if(val >= BIGBASE) {
+ len = crappy_uintmax_to_str(into, val/BIGBASE);
+ }
+ i_snprintf(into + len, 10, "%09llu",
+ (unsigned long long)(val % BIGBASE));
+ return len + strlen(STRINGIFY2(BIGBASE))-4;
+#undef STRINGIFY2
+#undef STRINGIFY
+#undef BIGBASE
+}
+static void test_str_to_uintmax(void)
+{
+ unsigned int i=0;
+ int randrange = i_rand_minmax(1, 15); /* when 1, will max out on 1s */
+ uintmax_t value = 0, valbase = i_rand() * 1000ull;
+ int len, ret;
+ char buff[50]; /* totally assumes < 159 bits */
+
+ test_begin("str_to_uintmax in range");
+ while (i < sizeof(uintmax_t)*CHAR_BIT) {
+ uintmax_t value_back;
+ const char *endp;
+
+ value = (value << 1) + 1;
+ if (value >= 64)
+ value -= i_rand_limit(randrange); /* don't always test the same numbers */
+ len = crappy_uintmax_to_str(buff, value);
+ ret = str_to_uintmax(buff, &value_back);
+ test_assert_idx(ret == 0, i);
+ test_assert_idx(value == value_back, i);
+
+ /* test with trailing noise */
+ buff[len] = 'x'; /* don't even null-terminate, let's be evil */
+ value_back = 0x1234567890123456;
+ ret = str_to_uintmax(buff, &value_back);
+ test_assert_idx(ret < 0, i);
+ test_assert_idx(value_back == 0x1234567890123456, i);
+ ret = str_parse_uintmax(buff, &value_back, &endp);
+ test_assert_idx(ret == 0, i);
+ test_assert_idx(value_back == value, i);
+ test_assert_idx(endp == &buff[len], i);
+ i++;
+ }
+ test_end();
+
+ /* not knowing exactly how large a uintmax_t is, we have to construct
+ the troublesome near-10/9*MAX strings manually by appending digits
+ to a MAX/9 string which we can easily create. Do a wider range
+ of 30 rather than the obvious 10, just in case - all are too large.*/
+ test_begin("str_to_uintmax overflow corner case");
+ value = UINTMAX_MAX/9-1;
+ len = crappy_uintmax_to_str(buff, value);
+ buff[len] = '0';
+ buff[len+1] = '\0';
+ for(i = 0; i <= 30; ++i) {
+ int j = len + 1;
+ while (buff[--j] == '9')
+ buff[j] = '0';
+ buff[j]++;
+ value = valbase + i;
+ ret = str_to_uintmax(buff, &value);
+ test_assert_idx(ret < 0 && value == valbase + i, i);
+ }
+ test_end();
+}
+
+/* always pads with leading zeros to a size of 9 digits */
+static int crappy_uintmax_to_str_hex(char *into, uintmax_t val)
+{
+#define BIGBASE 0x1000000000ull
+#define STRINGIFY(s) #s
+#define STRINGIFY2(s) STRINGIFY(s)
+ int len = 0;
+ if(val >= BIGBASE) {
+ len = crappy_uintmax_to_str_hex(into, val/BIGBASE);
+ }
+ i_snprintf(into + len, 10, "%09llx",
+ (unsigned long long)(val % BIGBASE));
+ return len + strlen(STRINGIFY2(BIGBASE))-6;
+#undef STRINGIFY2
+#undef STRINGIFY
+#undef BIGBASE
+}
+static void test_str_to_uintmax_hex(void)
+{
+ unsigned int i=0;
+ int randrange = i_rand_minmax(1, 15); /* when 1, will max out on 1s */
+ uintmax_t value = 0, valbase = i_rand() * 1000ull;
+ int len, ret;
+ char buff[52]; /* totally assumes < 200 bits */
+
+ test_begin("str_to_uintmax_hex in range");
+ while (i < sizeof(uintmax_t)*CHAR_BIT) {
+ uintmax_t value_back;
+ const char *endp;
+
+ value = (value << 1) + 1;
+ if (value >= 64)
+ value -= i_rand_limit(randrange); /* don't always test the same numbers */
+ len = crappy_uintmax_to_str_hex(buff, value);
+ ret = str_to_uintmax_hex(buff, &value_back);
+ test_assert_idx(ret == 0, i);
+ test_assert_idx(value == value_back, i);
+
+ /* test with trailing noise */
+ buff[len] = 'x'; /* don't even null-terminate, let's be evil */
+ value_back = 0x1234567890123456;
+ ret = str_to_uintmax_hex(buff, &value_back);
+ test_assert_idx(ret < 0, i);
+ test_assert_idx(value_back == 0x1234567890123456, i);
+ ret = str_parse_uintmax_hex(buff, &value_back, &endp);
+ test_assert_idx(ret == 0, i);
+ test_assert_idx(value_back == value, i);
+ test_assert_idx(endp == &buff[len], i);
+ i++;
+ }
+ test_end();
+
+ /* not knowing exactly how large a uintmax_t is, we have to construct
+ the troublesome near-0x10/0x0F*MAX strings manually by appending digits
+ to a MAX/0x0f string which we can easily create. Do a wider range
+ of 0x30 rather than the obvious 0x10, just in case - all are too large.*/
+ test_begin("str_to_uintmax_hex overflow corner case");
+ value = (UINTMAX_MAX/0x0f)-1;
+ len = crappy_uintmax_to_str_hex(buff, value);
+ buff[len] = '0';
+ buff[len+1] = '\0';
+ for(i = 0; i <= 0x30; ++i) {
+ int j = len + 1;
+ while (buff[--j] == 'f')
+ buff[j] = '0';
+ if (buff[j] == '9')
+ buff[j] = 'a';
+ else
+ buff[j]++;
+ value = valbase + i;
+ ret = str_to_uintmax_hex(buff, &value);
+ test_assert_idx(ret < 0 && value == valbase + i, i);
+ }
+ test_end();
+}
+
+/* always pads with leading zeros to a size of 9 digits */
+static int crappy_uintmax_to_str_oct(char *into, uintmax_t val)
+{
+#define BIGBASE 01000000000ull
+#define STRINGIFY(s) #s
+#define STRINGIFY2(s) STRINGIFY(s)
+ int len = 0;
+ if(val >= BIGBASE) {
+ len = crappy_uintmax_to_str_oct(into, val/BIGBASE);
+ }
+ i_snprintf(into + len, 10, "%09llo",
+ (unsigned long long)(val % BIGBASE));
+ return len + strlen(STRINGIFY2(BIGBASE))-5;
+#undef STRINGIFY2
+#undef STRINGIFY
+#undef BIGBASE
+}
+static void test_str_to_uintmax_oct(void)
+{
+ unsigned int i=0;
+ int randrange = i_rand_minmax(1, 15); /* when 1, will max out on 1s */
+ uintmax_t value = 0, valbase = i_rand() * 1000ull;
+ int len, ret;
+ char buff[69]; /* totally assumes < 200 bits */
+
+ test_begin("str_to_uintmax_oct in range");
+ while (i < sizeof(uintmax_t)*CHAR_BIT) {
+ uintmax_t value_back;
+ const char *endp = NULL;
+
+ value = (value << 1) + 1;
+ if (value >= 64)
+ value -= i_rand_limit(randrange); /* don't always test the same numbers */
+ len = crappy_uintmax_to_str_oct(buff, value);
+ ret = str_to_uintmax_oct(buff, &value_back);
+ test_assert_idx(ret == 0, i);
+ test_assert_idx(value == value_back, i);
+
+ /* test with trailing noise */
+ buff[len] = 'x'; /* don't even null-terminate, let's be evil */
+ value_back = 0x1234567890123456;
+ ret = str_to_uintmax_oct(buff, &value_back);
+ test_assert_idx(ret < 0, i);
+ test_assert_idx(value_back == 0x1234567890123456, i);
+ ret = str_parse_uintmax_oct(buff, &value_back, &endp);
+ test_assert_idx(ret == 0, i);
+ test_assert_idx(value_back == value, i);
+ test_assert_idx(endp == &buff[len], i);
+ i++;
+ }
+ test_end();
+
+ /* not knowing exactly how large a uintmax_t is, we have to construct
+ the troublesome near-010/007*MAX strings manually by appending digits
+ to a MAX/007 string which we can easily create. Do a wider range
+ of 030 rather than the obvious 010, just in case - all are too large.*/
+ test_begin("str_to_uintmax_oct overflow corner case");
+ value = (UINTMAX_MAX/007)-1;
+ len = crappy_uintmax_to_str_oct(buff, value);
+ buff[len] = '0';
+ buff[len+1] = '\0';
+ for(i = 0; i <= 030; ++i) {
+ int j = len + 1;
+ while (buff[--j] == '7')
+ buff[j] = '0';
+ buff[j]++;
+ value = valbase + i;
+ ret = str_to_uintmax_oct(buff, &value);
+ test_assert_idx(ret < 0 && value == valbase + i, i);
+ }
+ test_end();
+}
+
+static void test_str_to_u64(void)
+{
+ unsigned int i;
+ const struct {
+ const char *input;
+ int ret;
+ uint64_t val;
+ } u64tests[] = {
+ INVALID(-1),
+ INVALID(foo),
+ VALID(0),
+ VALID(000000000000000000000000000000000000000000000000000000000000000),
+ { "000000000000000000000000000000000000000000000000000001000000001", 0, 1000000001 },
+ { "18446744073709551615", 0, 18446744073709551615ULL },
+ INVALID(18446744073709551616),
+ INVALID(20496382304121724010), /* 2^64*10/9 doesn't wrap */
+ INVALID(20496382304121724017), /* 2^64*10/9 wraps only after addition */
+ INVALID(20496382304121724020), /* 2^64*10/9 wraps on multiply*/
+ };
+ test_begin("str_to_uint64");
+ for (i = 0; i < N_ELEMENTS(u64tests); ++i) {
+ uint64_t val = 0xBADBEEF15BADF00D;
+ int ret = str_to_uint64(u64tests[i].input, &val);
+ test_assert_idx(ret == u64tests[i].ret, i);
+ if (ret == 0)
+ test_assert_idx(val == u64tests[i].val, i);
+ else
+ test_assert_idx(val == 0xBADBEEF15BADF00D, i);
+
+ if (ret == 0)
+ T_BEGIN {
+ const char *longer = t_strconcat(u64tests[i].input, "x", NULL);
+ ret = str_to_uint64(longer, &val);
+ test_assert_idx(ret < 0, i);
+ } T_END;
+ }
+ test_end();
+}
+
+static void test_str_to_u32(void)
+{
+ unsigned int i;
+ const struct {
+ const char *input;
+ int ret;
+ uint32_t val;
+ } u32tests[] = {
+ VALID(0),
+ INVALID(-0),
+ VALID(4294967295),
+ INVALID(4294967296),
+ INVALID(4772185880),
+ INVALID(4772185884),
+ INVALID(4772185890),
+ };
+ test_begin("str_to_uint32");
+ for (i = 0; i < N_ELEMENTS(u32tests); ++i) {
+ uint32_t val = 0xDEADF00D;
+ int ret = str_to_uint32(u32tests[i].input, &val);
+ test_assert_idx(ret == u32tests[i].ret, i);
+ if (ret == 0)
+ test_assert_idx(val == u32tests[i].val, i);
+ else
+ test_assert_idx(val == 0xDEADF00D, i);
+ }
+ test_end();
+}
+
+/* Assumes long long is 64 bit, 2's complement */
+static void test_str_to_llong(void)
+{
+ unsigned int i;
+ const struct {
+ const char *input;
+ int ret;
+ long long val;
+ } i64tests[] = {
+ VALID(0),
+ VALID(-0),
+ INVALID(--0),
+ VALID(2147483648),
+ VALID(-2147483649),
+ VALID(9223372036854775807),
+ { "-9223372036854775808", 0, -9223372036854775807-1 },
+ INVALID(9223372036854775808),
+ INVALID(-9223372036854775809),
+ };
+ test_begin("str_to_llong");
+ for (i = 0; i < N_ELEMENTS(i64tests); ++i) {
+ long long val = 123456789;
+ int ret = str_to_llong(i64tests[i].input, &val);
+ test_assert_idx(ret == i64tests[i].ret, i);
+ if (ret == 0)
+ test_assert_idx(val == i64tests[i].val, i);
+ else
+ test_assert_idx(val == 123456789, i);
+ }
+ test_end();
+}
+
+/* Assumes int is 32 bit, 2's complement */
+static void test_str_to_i32(void)
+{
+ unsigned int i;
+ const struct {
+ const char *input;
+ int ret;
+ int val;
+ } i32tests[] = {
+ VALID(0),
+ VALID(-0),
+ INVALID(--0),
+ VALID(2147483647),
+ VALID(-2147483648),
+ INVALID(2147483648),
+ INVALID(-2147483649),
+ };
+ test_begin("str_to_int");
+ for (i = 0; i < N_ELEMENTS(i32tests); ++i) {
+ int val = 123456789;
+ int ret = str_to_int(i32tests[i].input, &val);
+ test_assert_idx(ret == i32tests[i].ret, i);
+ if (ret == 0)
+ test_assert_idx(val == i32tests[i].val, i);
+ else
+ test_assert_idx(val == 123456789, i);
+ }
+ test_end();
+}
+
+static void test_str_is_float(void)
+{
+ test_begin("str_is_float accepts integer");
+ /* accepts integer */
+ test_assert(str_is_float("0",'\0'));
+ test_assert(str_is_float("1234",'\0'));
+ test_end();
+ test_begin("str_is_float accepts float");
+ test_assert(str_is_float("0.0",'\0'));
+ test_assert(str_is_float("1234.0",'\0'));
+ test_assert(str_is_float("0.1234",'\0'));
+ test_assert(str_is_float("1234.1234",'\0'));
+ test_assert(str_is_float("0.1234 ",' '));
+ test_assert(str_is_float("1234.1234",'.'));
+ test_end();
+ test_begin("str_is_float refuses invalid values");
+ test_assert(!str_is_float(".",'\0'));
+ test_assert(!str_is_float(".1234",'\0'));
+ test_assert(!str_is_float("1234.",'\0'));
+ test_assert(!str_is_float("i am not a float at all",'\0'));
+ test_assert(!str_is_float("0x1234.0x1234",'\0'));
+ test_assert(!str_is_float(".0",'\0'));
+ test_assert(!str_is_float("0.",'\0'));
+ test_end();
+}
+
+void test_strnum(void)
+{
+ /* If the above isn't true, then we do expect some failures possibly */
+ test_str_to_uintmax();
+ test_str_to_uintmax_hex();
+ test_str_to_uintmax_oct();
+ test_str_to_u64();
+ test_str_to_u32();
+ test_str_to_llong();
+ test_str_to_i32();
+ test_str_is_float();
+}
diff --git a/src/lib/test-time-util.c b/src/lib/test-time-util.c
new file mode 100644
index 0000000..675a5db
--- /dev/null
+++ b/src/lib/test-time-util.c
@@ -0,0 +1,406 @@
+/* Copyright (c) 2009-2018 Dovecot authors, see the included COPYING file */
+
+#include "test-lib.h"
+#include "time-util.h"
+
+#include <time.h>
+
+static void test_timeval_cmp(void)
+{
+ static const struct {
+ struct timeval tv1, tv2;
+ int output;
+ } tests[] = {
+ {
+ .tv1 = { 0, 0 },
+ .tv2 = { 0, 0 },
+ .output = 0,
+ }, {
+ .tv1 = { INT_MAX, 999999 },
+ .tv2 = { INT_MAX, 999999 },
+ .output = 0,
+ }, {
+ .tv1 = { 0, 0 },
+ .tv2 = { 0, 1 },
+ .output = -1,
+ }, {
+ .tv1 = { 0, 0 },
+ .tv2 = { 1, 0 },
+ .output = -1,
+ }, {
+ .tv1 = { 0, 999999 },
+ .tv2 = { 1, 0 },
+ .output = -1,
+ }, {
+ .tv1 = { 1, 0 },
+ .tv2 = { 1, 1 },
+ .output = -1,
+ }, {
+ .tv1 = { -INT_MAX, 0 },
+ .tv2 = { INT_MAX, 0 },
+ .output = -1,
+ }
+ };
+ unsigned int i;
+
+ test_begin("timeval_cmp()");
+ for (i = 0; i < N_ELEMENTS(tests); i++) {
+ const struct timeval *tv1 = &tests[i].tv1, *tv2 = &tests[i].tv2;
+ int output = tests[i].output;
+
+ test_assert(timeval_cmp(tv1, tv2) == output);
+ test_assert(timeval_cmp(tv2, tv1) == -output);
+ }
+ test_end();
+}
+
+static void test_timeval_cmp_margin(void)
+{
+ static const struct {
+ struct timeval tv1, tv2;
+ unsigned int margin;
+ int output;
+ } tests[] = {
+ {
+ .tv1 = { 0, 0 },
+ .tv2 = { 0, 0 },
+ .output = 0,
+ },{
+ .tv1 = { INT_MAX, 999999 },
+ .tv2 = { INT_MAX, 999999 },
+ .output = 0,
+
+ },{
+ .tv1 = { 0, 0 },
+ .tv2 = { 0, 1 },
+ .output = -1,
+ },{
+ .tv1 = { 0, 0 },
+ .tv2 = { 1, 0 },
+ .output = -1,
+ },{
+ .tv1 = { 0, 999999 },
+ .tv2 = { 1, 0 },
+ .output = -1,
+ },{
+ .tv1 = { 1, 0 },
+ .tv2 = { 1, 1 },
+ .output = -1,
+ },{
+ .tv1 = { -INT_MAX, 0 },
+ .tv2 = { INT_MAX, 0 },
+ .output = -1,
+ },{
+ .tv1 = { 0, 999999 },
+ .tv2 = { 1, 0 },
+ .margin = 1,
+ .output = 0,
+ },{
+ .tv1 = { 1, 0 },
+ .tv2 = { 1, 1 },
+ .margin = 1,
+ .output = 0,
+ },{
+ .tv1 = { 0, 999998 },
+ .tv2 = { 1, 0 },
+ .margin = 1,
+ .output = -1,
+ },{
+ .tv1 = { 1, 0 },
+ .tv2 = { 1, 2 },
+ .margin = 1,
+ .output = -1,
+ },{
+ .tv1 = { 0, 998000 },
+ .tv2 = { 1, 0 },
+ .margin = 2000,
+ .output = 0,
+ },{
+ .tv1 = { 1, 0 },
+ .tv2 = { 1, 2000 },
+ .margin = 2000,
+ .output = 0,
+ },{
+ .tv1 = { 0, 997999 },
+ .tv2 = { 1, 0 },
+ .margin = 2000,
+ .output = -1,
+ },{
+ .tv1 = { 1, 0 },
+ .tv2 = { 1, 2001 },
+ .margin = 2000,
+ .output = -1,
+ },{
+ .tv1 = { 0, 1 },
+ .tv2 = { 1, 0 },
+ .margin = 999999,
+ .output = 0,
+ },{
+ .tv1 = { 1, 0 },
+ .tv2 = { 1, 999999 },
+ .margin = 999999,
+ .output = 0,
+ },{
+ .tv1 = { 0, 0 },
+ .tv2 = { 1, 0 },
+ .margin = 999999,
+ .output = -1,
+ },{
+ .tv1 = { 1, 0 },
+ .tv2 = { 2, 0 },
+ .margin = 999999,
+ .output = -1,
+ },{
+ .tv1 = { 10, 0 },
+ .tv2 = { 11, 500000 },
+ .margin = 1500000,
+ .output = 0,
+ },{
+ .tv1 = { 8, 500000 },
+ .tv2 = { 10, 0 },
+ .margin = 1500000,
+ .output = 0,
+ },{
+ .tv1 = { 10, 0 },
+ .tv2 = { 11, 500001 },
+ .margin = 1500000,
+ .output = -1,
+ },{
+ .tv1 = { 8, 499999 },
+ .tv2 = { 10, 0 },
+ .margin = 1500000,
+ .output = -1,
+ },{
+ .tv1 = { 1517925358, 999989 },
+ .tv2 = { 1517925359, 753 },
+ .margin = 2000,
+ .output = 0,
+ }
+ };
+ unsigned int i;
+
+ test_begin("timeval_cmp_margin()");
+ for (i = 0; i < N_ELEMENTS(tests); i++) {
+ const struct timeval *tv1 = &tests[i].tv1, *tv2 = &tests[i].tv2;
+ unsigned int margin = tests[i].margin;
+ int output = tests[i].output;
+
+ test_assert(timeval_cmp_margin(tv1, tv2, margin) == output);
+ test_assert(timeval_cmp_margin(tv2, tv1, margin) == -output);
+ }
+ test_end();
+}
+
+static void test_timeval_diff(void)
+{
+ static const struct timeval input[] = {
+ { 1, 0 }, { 0, 999999 },
+ { 1, 0 }, { 0, 999001 },
+ { 1, 1 }, { 0, 999001 },
+ { 2, 1 }, { 1, 0 },
+ { INT_MAX, 0 }, { INT_MAX-1, 1 }
+ };
+ static int output[] = {
+ 1,
+ 999,
+ 1000,
+ 1000001,
+ 999999
+ };
+ unsigned int i;
+ long long udiff;
+ int mdiff;
+
+ test_begin("timeval_diff_*()");
+ for (i = 0; i < N_ELEMENTS(input); i += 2) {
+ udiff = timeval_diff_usecs(&input[i], &input[i+1]);
+ mdiff = timeval_diff_msecs(&input[i], &input[i+1]);
+ test_assert(udiff == output[i/2]);
+ test_assert(mdiff == udiff/1000);
+
+ udiff = timeval_diff_usecs(&input[i+1], &input[i]);
+ mdiff = timeval_diff_msecs(&input[i+1], &input[i]);
+ test_assert(udiff == -output[i/2]);
+ test_assert(mdiff == udiff/1000);
+ }
+ test_end();
+}
+
+static void test_time_to_local_day_start(void)
+{
+ /* Try this around days when DST changes in some of the more popular
+ timezones. If that works, everything else probably works too. */
+ const struct tm tests[] = {
+ /* Europe winter -> summer */
+ { .tm_year = 2017-1900, .tm_mon = 2, .tm_mday = 26 },
+ { .tm_year = 2017-1900, .tm_mon = 2, .tm_mday = 26,
+ .tm_hour = 23, .tm_min = 59, .tm_sec = 59 },
+ /* Europe summer -> winter */
+ { .tm_year = 2017-1900, .tm_mon = 9, .tm_mday = 29 },
+ { .tm_year = 2017-1900, .tm_mon = 9, .tm_mday = 29,
+ .tm_hour = 23, .tm_min = 59, .tm_sec = 59 },
+ /* USA winter -> summer */
+ { .tm_year = 2017-1900, .tm_mon = 2, .tm_mday = 12 },
+ { .tm_year = 2017-1900, .tm_mon = 2, .tm_mday = 12,
+ .tm_hour = 23, .tm_min = 59, .tm_sec = 59 },
+ /* USA summer -> winter */
+ { .tm_year = 2017-1900, .tm_mon = 10, .tm_mday = 5 },
+ { .tm_year = 2017-1900, .tm_mon = 10, .tm_mday = 5,
+ .tm_hour = 23, .tm_min = 59, .tm_sec = 59 },
+
+ /* (some of) Australia summer -> winter */
+ { .tm_year = 2017-1900, .tm_mon = 3, .tm_mday = 2 },
+ { .tm_year = 2017-1900, .tm_mon = 3, .tm_mday = 2,
+ .tm_hour = 23, .tm_min = 59, .tm_sec = 59 },
+ /* (some of) Australia winter -> summer */
+ { .tm_year = 2017-1900, .tm_mon = 9, .tm_mday = 1 },
+ { .tm_year = 2017-1900, .tm_mon = 9, .tm_mday = 1,
+ .tm_hour = 23, .tm_min = 59, .tm_sec = 59 },
+ };
+ const struct tm *tm;
+ struct tm tm_copy;
+ time_t t;
+
+ test_begin("time_to_local_day_start()");
+ for (unsigned i = 0; i < N_ELEMENTS(tests); i++) {
+ tm_copy = tests[i];
+ tm_copy.tm_isdst = -1;
+ t = mktime(&tm_copy);
+ test_assert_idx(t != (time_t)-1, i);
+
+ t = time_to_local_day_start(t);
+ tm = localtime(&t);
+ test_assert_idx(tm->tm_year == tests[i].tm_year &&
+ tm->tm_mon == tests[i].tm_mon &&
+ tm->tm_mday == tests[i].tm_mday, i);
+ test_assert_idx(tm->tm_hour == 0 && tm->tm_min == 0 &&
+ tm->tm_sec == 0, i);
+ }
+ test_end();
+}
+
+static void test_timestamp(const char *ts, int idx)
+{
+ /* %G:%H:%M:%S */
+ const char **t = t_strsplit(ts, ":");
+ unsigned len = str_array_length(t);
+ test_assert_idx(len == 4, idx);
+
+ /* %G - ISO 8601 year */
+ test_assert_idx(strlen(t[0]) == 4, idx);
+ unsigned v = 0;
+ test_assert_idx(str_to_uint(t[0], &v) == 0, idx);
+ test_assert_idx(1000 <= v, idx);
+ test_assert_idx(v <= 3000, idx);
+
+ /* %H - hour from 00 to 23 */
+ test_assert_idx(strlen(t[1]) == 2, idx);
+ test_assert_idx(str_to_uint(t[1], &v) == 0, idx);
+ test_assert_idx(v <= 23, idx);
+
+ /* %M - minute from 00 to 59 */
+ test_assert_idx(strlen(t[2]) == 2, idx);
+ test_assert_idx(str_to_uint(t[2], &v) == 0, idx);
+ test_assert_idx(v <= 59, idx);
+
+ /* %S - second from 00 to 60 */
+ test_assert_idx(strlen(t[3]) == 2, idx);
+ test_assert_idx(str_to_uint(t[3], &v) == 0, idx);
+ test_assert_idx(v <= 60, idx);
+}
+
+#define TS_FMT "%G:%H:%M:%S"
+static void test_strftime_now(void)
+{
+ test_begin("t_strftime and variants now");
+
+ time_t now = time(NULL);
+ test_timestamp(t_strftime(TS_FMT, gmtime(&now)), 0);
+ test_timestamp(t_strfgmtime(TS_FMT, now), 1);
+ test_timestamp(t_strflocaltime(TS_FMT, now), 2);
+
+ test_end();
+}
+
+#define RFC2822_FMT "%a, %d %b %Y %T"
+static void test_strftime_fixed(void)
+{
+ test_begin("t_strftime and variants fixed timestamp");
+
+ time_t ts = 1481222536;
+ const char *exp = "Thu, 08 Dec 2016 18:42:16";
+ test_assert(strcmp(t_strftime(RFC2822_FMT, gmtime(&ts)), exp) == 0);
+ test_assert(strcmp(t_strfgmtime(RFC2822_FMT, ts), exp) == 0);
+
+ test_end();
+}
+
+static void test_micro_nanoseconds(void)
+{
+ uint64_t secs, usecs, nsecs;
+
+ test_begin("i_microseconds() and i_nanoseconds()");
+
+ secs = time(NULL);
+ usecs = i_microseconds();
+ nsecs = i_nanoseconds();
+
+ /* Assume max 1 seconds time difference between the calls. That should
+ be more than enough, while still not failing if there are temporary
+ hangs when running in heavily loaded systems. */
+ test_assert(usecs/1000000 - secs <= 1);
+ test_assert(nsecs/1000 - usecs <= 1000000);
+
+ test_end();
+}
+
+static void test_str_to_timeval(void)
+{
+ struct {
+ const char *str;
+ time_t tv_sec;
+ suseconds_t tv_usec;
+ } tests[] = {
+ { "0", 0, 0 },
+ { "0.0", 0, 0 },
+ { "0.000000", 0, 0 },
+ { "0.1", 0, 100000 },
+ { "0.100000", 0, 100000 },
+ { "0.000001", 0, 1 },
+ { "0.100000", 0, 100000 },
+ { "2147483647", 2147483647, 0 },
+ { "2147483647.999999", 2147483647, 999999 },
+ };
+ const char *test_failures[] = {
+ "",
+ "0.",
+ "0.0000000",
+ "1234.-1",
+ "1234.x",
+ "x",
+ "x.100",
+ };
+ struct timeval tv;
+
+ test_begin("str_to_timeval");
+ for (unsigned int i = 0; i < N_ELEMENTS(tests); i++) {
+ test_assert_idx(str_to_timeval(tests[i].str, &tv) == 0, i);
+ test_assert_idx(tv.tv_sec == tests[i].tv_sec, i);
+ test_assert_idx(tv.tv_usec == tests[i].tv_usec, i);
+ }
+ for (unsigned int i = 0; i < N_ELEMENTS(test_failures); i++)
+ test_assert_idx(str_to_timeval(test_failures[i], &tv) == -1, i);
+ test_end();
+}
+
+void test_time_util(void)
+{
+ test_timeval_cmp();
+ test_timeval_cmp_margin();
+ test_timeval_diff();
+ test_time_to_local_day_start();
+ test_strftime_now();
+ test_strftime_fixed();
+ test_micro_nanoseconds();
+ test_str_to_timeval();
+}
diff --git a/src/lib/test-unichar.c b/src/lib/test-unichar.c
new file mode 100644
index 0000000..b5d3d06
--- /dev/null
+++ b/src/lib/test-unichar.c
@@ -0,0 +1,185 @@
+/* Copyright (c) 2007-2018 Dovecot authors, see the included COPYING file */
+
+#include "test-lib.h"
+#include "str.h"
+#include "buffer.h"
+#include "unichar.h"
+
+static void test_unichar_uni_utf8_strlen(void)
+{
+ static const char input[] = "\xC3\xA4\xC3\xA4\0a";
+
+ test_begin("uni_utf8_strlen()");
+ test_assert(uni_utf8_strlen(input) == 2);
+ test_end();
+
+ test_begin("uni_utf8_strlen_n()");
+ test_assert(uni_utf8_strlen_n(input, 1) == 0);
+ test_assert(uni_utf8_strlen_n(input, 2) == 1);
+ test_assert(uni_utf8_strlen_n(input, 3) == 1);
+ test_assert(uni_utf8_strlen_n(input, 4) == 2);
+ test_end();
+}
+
+static void test_unichar_uni_utf8_partial_strlen_n(void)
+{
+ static const char input[] = "\xC3\xA4\xC3\xA4\0a";
+ size_t pos;
+
+ test_begin("uni_utf8_partial_strlen_n()");
+ test_assert(uni_utf8_partial_strlen_n(input, 1, &pos) == 0 && pos == 0);
+ test_assert(uni_utf8_partial_strlen_n(input, 2, &pos) == 1 && pos == 2);
+ test_assert(uni_utf8_partial_strlen_n(input, 3, &pos) == 1 && pos == 2);
+ test_assert(uni_utf8_partial_strlen_n(input, 4, &pos) == 2 && pos == 4);
+ test_assert(uni_utf8_partial_strlen_n(input, 5, &pos) == 3 && pos == 5);
+ test_assert(uni_utf8_partial_strlen_n(input, 6, &pos) == 4 && pos == 6);
+ test_end();
+}
+
+static void test_unichar_valid_unicode(void)
+{
+ struct {
+ const char *input;
+ bool valid;
+ unichar_t expected;
+ } test_cases[] = {
+ { "a", TRUE, 'a' },
+ { "\xc3\xb1", TRUE, 0x00F1 }, /* U+00F1 */
+ { "\xc3\x28", FALSE, 0x0 }, /* has invalid 2nd octet */
+ { "\xa0\xa1", FALSE, 0x0 }, /* invalid sequence identifier */
+ { "\xed\xb2\x80", FALSE, 0x0 }, /* UTF-8B */
+ { "\xed\xa0\x80", FALSE, 0x0 }, /* surrogate halves, U+D800 .. */
+ { "\xed\xa0\x80", FALSE, 0x0 },
+ { "\xed\xa1\x80", FALSE, 0x0 },
+ { "\xed\xa2\x80", FALSE, 0x0 },
+ { "\xed\xa3\x80", FALSE, 0x0 },
+ { "\xed\xa4\x80", FALSE, 0x0 },
+ { "\xed\xa5\x80", FALSE, 0x0 },
+ { "\xed\xa6\x80", FALSE, 0x0 },
+ { "\xed\xa7\x80", FALSE, 0x0 },
+ { "\xed\xa8\x80", FALSE, 0x0 },
+ { "\xed\xa9\x80", FALSE, 0x0 },
+ { "\xed\xaa\x80", FALSE, 0x0 },
+ { "\xed\xab\x80", FALSE, 0x0 },
+ { "\xed\xac\x80", FALSE, 0x0 },
+ { "\xed\xad\x80", FALSE, 0x0 },
+ { "\xed\xaf\x80", FALSE, 0x0 },
+ { "\xed\xb0\x80", FALSE, 0x0 },
+ { "\xed\xb1\x80", FALSE, 0x0 },
+ { "\xed\xb2\x80", FALSE, 0x0 },
+ { "\xed\xb3\x80", FALSE, 0x0 },
+ { "\xed\xb4\x80", FALSE, 0x0 },
+ { "\xed\xb5\x80", FALSE, 0x0 },
+ { "\xed\xb6\x80", FALSE, 0x0 },
+ { "\xed\xb7\x80", FALSE, 0x0 },
+ { "\xed\xb8\x80", FALSE, 0x0 },
+ { "\xed\xb9\x80", FALSE, 0x0 },
+ { "\xed\xba\x80", FALSE, 0x0 },
+ { "\xed\xbb\x80", FALSE, 0x0 },
+ { "\xed\xbc\x80", FALSE, 0x0 },
+ { "\xed\xbd\x80", FALSE, 0x0 },
+ { "\xed\xbf\x80", FALSE, 0x0 }, /* .. U+DFFF */
+ { "\xe2\x82\xa1", TRUE, 0x20A1 }, /* U+20A1 */
+ { "\xe2\x28\xa1", FALSE, 0x0 }, /* invalid 2nd octet */
+ { "\xe2\x82\x28", FALSE, 0x0 }, /* invalid 3rd octet */
+ { "\xf0\x90\x8c\xbc", TRUE, 0x1033C }, /* U+1033C */
+ { "\xf0\x28\x8c\xbc", FALSE, 0x0 }, /*invalid 2nd octet*/
+ { "\xf0\x90\x28\xbc", FALSE, 0x0 }, /* invalid 3rd octet */
+ { "\xf0\x28\x8c\x28", FALSE, 0x0 }, /* invalid 4th octet */
+ { "\xf4\x80\x80\x80", TRUE, 0x100000 }, /* U+100000, supplementary plane start */
+ { "\xf4\x8f\xbf\xbf", TRUE, 0x10FFFF }, /* U+10FFFF, maximum value */
+ { "\xf8\xa1\xa1\xa1\xa1", FALSE, 0x0 }, /* invalid unicode */
+ { "\xfc\xa1\xa1\xa1\xa1\xa1", FALSE, 0x0 }, /* invalid unicode */
+ };
+
+ test_begin("unichar valid unicode");
+
+ for(size_t i = 0; i < N_ELEMENTS(test_cases); i++) {
+ unichar_t chr;
+ if (test_cases[i].valid) {
+ test_assert_idx(uni_utf8_get_char(test_cases[i].input, &chr) > 0, i);
+ test_assert_idx(test_cases[i].expected == chr, i);
+ } else {
+ test_assert_idx(uni_utf8_get_char(test_cases[i].input, &chr) < 1, i);
+ }
+ }
+
+ test_end();
+}
+
+static void test_unichar_surrogates(void)
+{
+ unichar_t orig, high, low;
+ test_begin("unichar surrogates");
+
+ orig = 0x10437;
+ uni_split_surrogate(orig, &high, &low);
+ test_assert(high == 0xD801);
+ test_assert(low == 0xDC37);
+ test_assert(uni_join_surrogate(high, low) == orig);
+
+ test_end();
+}
+
+void test_unichar(void)
+{
+ static const char overlong_utf8[] = "\xf8\x80\x95\x81\xa1";
+ static const char collate_in[] = "\xc3\xbc \xc2\xb3";
+ static const char collate_exp[] = "U\xcc\x88 3";
+ buffer_t *collate_out;
+ unichar_t chr, chr2;
+ string_t *str = t_str_new(16);
+
+ test_begin("unichars encode/decode");
+ for (chr = 0; chr <= 0x10ffff; chr++) {
+ /* skip surrogates */
+ if ((chr & 0xfff800) == 0xd800)
+ continue;
+ /* The bottom 6 bits should be irrelevant to code coverage,
+ only test 000000, 111111, and something in between. */
+ if ((chr & 63) == 1)
+ chr += i_rand_limit(62); /* After 0, somewhere between 1 and 62 */
+ else if ((chr & 63) > 0 && (chr & 63) < 63)
+ chr |= 63; /* After random, straight to 63 */
+
+ str_truncate(str, 0);
+ uni_ucs4_to_utf8_c(chr, str);
+ test_assert(uni_utf8_str_is_valid(str_c(str)));
+ test_assert(uni_utf8_get_char(str_c(str), &chr2) == (int)uni_utf8_char_bytes(*str_data(str)));
+ test_assert(chr2 == chr);
+
+ if ((chr & 0x63) == 0) {
+ unsigned int utf8len = uni_utf8_char_bytes((unsigned char)*str_c(str));
+
+ /* virtually truncate the byte string */
+ while (--utf8len > 0)
+ test_assert(uni_utf8_get_char_n(str_c(str), utf8len, &chr2) == 0);
+
+ utf8len = uni_utf8_char_bytes((unsigned char)*str_c(str));
+
+ /* actually truncate the byte stream */
+ while (--utf8len > 0) {
+ str_truncate(str, utf8len);
+ test_assert(!uni_utf8_str_is_valid(str_c(str)));
+ test_assert(uni_utf8_get_char(str_c(str), &chr2) == 0);
+ }
+ }
+ }
+ test_end();
+
+ test_begin("unichar collation");
+ collate_out = buffer_create_dynamic(default_pool, 32);
+ uni_utf8_to_decomposed_titlecase(collate_in, sizeof(collate_in),
+ collate_out);
+ test_assert(strcmp(collate_out->data, collate_exp) == 0);
+ buffer_free(&collate_out);
+
+ test_assert(!uni_utf8_str_is_valid(overlong_utf8));
+ test_assert(uni_utf8_get_char(overlong_utf8, &chr2) < 0);
+ test_end();
+
+ test_unichar_uni_utf8_strlen();
+ test_unichar_uni_utf8_partial_strlen_n();
+ test_unichar_valid_unicode();
+ test_unichar_surrogates();
+}
diff --git a/src/lib/test-uri.c b/src/lib/test-uri.c
new file mode 100644
index 0000000..299214d
--- /dev/null
+++ b/src/lib/test-uri.c
@@ -0,0 +1,807 @@
+/* Copyright (c) 2009-2018 Dovecot authors, see the included COPYING file */
+
+#include "test-lib.h"
+#include "test-common.h"
+#include "str.h"
+#include "str-sanitize.h"
+#include "uri-util.h"
+
+/* Valid uri tests */
+const char *valid_uri_tests[] = {
+ "http://www.dovecot.org",
+ "http://127.0.0.1",
+ "http://www.dovecot.org/frop",
+ "http://www.dovecot.org/frop%20frop",
+ "http://www.dovecot.org/frop/frop",
+ "http://www.dovecot.org/frop/frop?query",
+ "http://www.dovecot.org?query",
+ "http://www.dovecot.org?query&query",
+ "mailto:frop@example.com",
+};
+
+unsigned int valid_uri_test_count = N_ELEMENTS(valid_uri_tests);
+
+static void test_uri_valid(void)
+{
+ unsigned int i;
+
+ test_begin("uri valid");
+ for (i = 0; i < valid_uri_test_count; i++) T_BEGIN {
+ const char *uri_in, *error = NULL;
+ int ret;
+
+ uri_in = valid_uri_tests[i];
+
+ ret = uri_check(uri_in, 0, &error);
+ test_out_quiet(
+ t_strdup_printf("parse [%u] <%s>", i, str_sanitize(uri_in, 64)),
+ ret >= 0);
+ } T_END;
+ test_end();
+}
+
+/* Invalid uri tests */
+const char *invalid_uri_tests[] = {
+ "http",
+ "http$44",
+ "/index.html",
+ "imap:[",
+ "imap://[",
+ "frop://friep\"",
+ "http://example.com/settings/%00/",
+ "http://[]/index.html",
+ "http://example.com:65536/index.html"
+};
+
+unsigned int invalid_uri_test_count = N_ELEMENTS(invalid_uri_tests);
+
+static void test_uri_invalid(void)
+{
+ unsigned int i;
+
+ test_begin("uri invalid");
+ for (i = 0; i < invalid_uri_test_count; i++) T_BEGIN {
+ const char *uri_in, *error = NULL;
+ int ret;
+
+ uri_in = invalid_uri_tests[i];
+
+ ret = uri_check(uri_in, 0, &error);
+ test_out_quiet(
+ t_strdup_printf("parse [%u] <%s>", i, str_sanitize(uri_in, 64)),
+ ret < 0);
+ } T_END;
+ test_end();
+}
+
+/* RFC uri tests */
+const char *rfc_uri_tests[] = {
+ /* from RFC 1738 */
+ "http://www.acl.lanl.gov/URI/archive/uri-archive.index.html",
+ "file://vms.host.edu/disk$user/my/notes/note12345.txt",
+ "ftp://@host.com/",
+ "ftp://host.com/",
+ "ftp://foo:@host.com/",
+ "ftp://myname@host.dom/%2Fetc/motd",
+ "ftp://myname@host.dom/etc/motd",
+ "ftp://myname@host.dom//etc/motd",
+ "ftp://info.cern.ch/pub/www/doc;type=d",
+ "http://ds.internic.net/instructions/overview.html#WARNING",
+ /* from RFC 2056 */
+ "z39.50s://melvyl.ucop.edu/cat",
+ "z39.50r://melvyl.ucop.edu/mags?elecworld.v30.n19",
+ "z39.50r://cnidr.org:2100/tmf?bkirch_rules__a1;esn=f;rs=marc",
+ /* from RFC 2122 */
+ "vemmi://zeus.mctel.fr/demo",
+ "vemmi://zeus.mctel.fr",
+ "vemmi://zeus.mctel.fr",
+ "vemmi://mctel.fr/demo;$USERDATA=smith;account=1234",
+ "vemmi://ares.mctel.fr/TEST",
+ /* from RFC 2141 */
+ "URN:foo:a123,456",
+ "urn:foo:a123,456",
+ "urn:FOO:a123,456",
+ "urn:foo:A123,456",
+ "urn:foo:a123%2C456",
+ "URN:FOO:a123%2c456",
+ /* from RFC 2224 */
+ "nfs://server/d/e/f",
+ "nfs://server//a/b/c/d/e/f",
+ "nfs://server/a/b",
+ /* from RFC 2229 */
+ "dict://dict.org/d:shortcake:",
+ "dict://dict.org/d:shortcake:*",
+ "dict://dict.org/d:shortcake:wordnet:",
+ "dict://dict.org/d:shortcake:wordnet:1",
+ "dict://dict.org/d:abcdefgh",
+ "dict://dict.org/d:sun",
+ "dict://dict.org/d:sun::1",
+ "dict://dict.org/m:sun",
+ "dict://dict.org/m:sun::soundex",
+ "dict://dict.org/m:sun:wordnet::1",
+ "dict://dict.org/m:sun::soundex:1",
+ "dict://dict.org/m:sun:::",
+ /* from RFC 2326 */
+ "rtsp://media.example.com:554/twister/audiotrack",
+ "rtsp://media.example.com:554/twister",
+ "rtsp://server.example.com/fizzle/foo",
+ "rtsp://example.com/foo/bar/baz.rm",
+ "rtsp://audio.example.com/audio",
+ "rtsp://audio.example.com/twister.en",
+ "rtsp://audio.example.com/meeting.en",
+ "rtsp://example.com/fizzle/foo",
+ "rtsp://bigserver.com:8001",
+ "rtsp://example.com/meeting/audio.en",
+ "rtsp://foo.com/bar.file",
+ "rtsp://foo.com/bar.avi/streamid=0;seq=45102",
+ "rtsp://foo.com/bar.avi/streamid=1;seq=30211",
+ "rtsp://audio.example.com/twister/audio.en",
+ "rtsp://video.example.com/twister/video",
+ "rtsp://video.example.com/twister/video;seq=12312232;rtptime=78712811",
+ "rtsp://audio.example.com/twister/audio.en;seq=876655;rtptime=1032181",
+ "rtsp://foo/twister/video;seq=9810092;rtptime=3450012",
+ "rtsp://foo.com/test.wav/streamid=0;seq=981888;rtptime=3781123",
+ "rtsp://server.example.com/demo/548/sound",
+ "rtsp://server.example.com/demo/548/sound",
+ "rtsp://server.example.com/meeting",
+ "rtsp://server.example.com/meeting/audiotrack",
+ "rtsp://server.example.com/meeting/videotrack",
+ "rtsp://server.example.com/meeting",
+ "rtsp://example.com/movie/trackID=1",
+ "rtsp://media.example.com:554/twister",
+ /* from RFC 2371 */
+ "tip://123.123.123.123/?urn:xopen:xid",
+ "tip://123.123.123.123/?transid1",
+ /* from RFC 2384 */
+ "pop://rg@mailsrv.qualcomm.com",
+ "pop://rg;AUTH=+APOP@mail.eudora.com:8110",
+ "pop://baz;AUTH=SCRAM-MD5@foo.bar",
+ /* from RFC 2392 */
+ "mid:960830.1639@XIson.com/partA.960830.1639@XIson.com",
+ "cid:foo4%25foo1@bar.net",
+ "cid:foo4*foo1@bar.net",
+ /* from RFC 2397 */
+ "data:,A%20brief%20note",
+ "data:image/gif;base64,R0lGODdhMAAwAPAAAAAAAP///ywAAAAAMAAw"
+ "AAAC8IyPqcvt3wCcDkiLc7C0qwyGHhSWpjQu5yqmCYsapyuvUUlvONmOZtfzgFz"
+ "ByTB10QgxOR0TqBQejhRNzOfkVJ+5YiUqrXF5Y5lKh/DeuNcP5yLWGsEbtLiOSp"
+ "a/TPg7JpJHxyendzWTBfX0cxOnKPjgBzi4diinWGdkF8kjdfnycQZXZeYGejmJl"
+ "ZeGl9i2icVqaNVailT6F5iJ90m6mvuTS4OK05M0vDk0Q4XUtwvKOzrcd3iq9uis"
+ "F81M1OIcR7lEewwcLp7tuNNkM3uNna3F2JQFo97Vriy/Xl4/f1cf5VWzXyym7PH"
+ "hhx4dbgYKAAA7",
+#if 0 // this one doesn't comply with RFC 3986
+ "data:text/plain;charset=iso-8859-7,%be%fg%be",
+#endif
+ "data:application/vnd-xxx-query,select_vcount,fcol_from_fieldtable/local",
+ /* from RFC 2838 */
+ "tv:wqed.org",
+ "tv:nbc.com",
+ "tv:",
+ "tv:abc.com",
+ "tv:abc.co.au",
+ "tv:east.hbo.com",
+ "tv:west.hbo.com",
+ /* from RFC 3261 */
+#if 0 // these don't comply with RFC 3986
+ "sip:+1-212-555-1212:1234@gateway.com;user=phone",
+ "sip:+12125551212@server.phone2net.com",
+ "sip:+12125551212@server.phone2net.com;tag=887s",
+ "sip:+358-555-1234567@foo.com;postd=pp22;user=phone",
+ "sip:+358-555-1234567;isub=1411;postd=pp22@foo.com;user=phone",
+ "sip:+358-555-1234567;phone-context=5;tsp=a.b@foo.com;user=phone",
+ "sip:+358-555-1234567;postd=pp22@foo.com;user=phone",
+ "sip:+358-555-1234567;POSTD=PP22@foo.com;user=phone",
+ "sip:+358-555-1234567;postd=pp22;isub=1411@foo.com;user=phone",
+ "sip:%61lice@atlanta.com;transport=TCPv",
+ "sip:agb@bell-telephone.com",
+ "sip:alice@192.0.2.4v",
+ "sip:alice@atlanta.covm",
+ "sip:alice@atlanta.com;maddr=239.255.255.1;ttl=15",
+ "sip:alice@atlanta.com?priority=urgent&subject=project%20x",
+ "sip:alice@atlanta.com?subject=project%20x&priority=urgent",
+ "sip:alice@AtLanTa.CoM;Transport=tcp",
+ "sip:alice@AtLanTa.CoM;Transport=UDP",
+ "SIP:ALICE@AtLanTa.CoM;Transport=udp",
+ "sip:alice;day=tuesday@atlanta.com",
+ "sip:alice@pc33.atlanta.com",
+ "sip:alice:secretword@atlanta.com;transport=tcp",
+ "sip:anonymous@anonymizer.invalid",
+ "sip:atlanta.com;method=REGISTER?to=alice%40atlanta.com",
+ "sip:bigbox3.site3.atlanta.com;lr",
+ "sip:biloxi.com;method=REGISTER;transport=tcp?to=sip:bob%40biloxi.com",
+ "sip:biloxi.com;transport=tcp;method=REGISTER?to=sip:bob%40biloxi.com",
+ "sip:bob@192.0.2.4",
+ "sip:bob@biloxi.com",
+ "sip:bob@biloxi.com:5060",
+ "sip:bob@biloxi.com:6000;transport=tcp",
+ "sip:bob@biloxi.com;transport=udp",
+ "sip:bob@engineering.biloxi.com",
+ "sip:bob@phone21.boxesbybob.com",
+ "sip:c8oqz84zk7z@privacy.org>;tag=hyh8",
+ "sip:callee@domain.com",
+ "sip:callee@gateway.leftprivatespace.com",
+ "sip:callee@u2.domain.com",
+ "sip:callee@u2.rightprivatespace.com",
+ "sip:caller@u1.example.com",
+ "sip:carol@chicago.com",
+ "sip:carol@chicago.com;security=off",
+ "sip:carol@chicago.com;security=on",
+ "sip:carol@chicago.com;newparam=5",
+ "sip:carol@chicago.com;security=off",
+ "sip:carol@chicago.com;security=on",
+ "sip:carol@chicago.com?Subject=next%20meeting",
+ "sip:carol@cube2214a.chicago.com",
+ "sip:chicago.com",
+ "sip:not-in-service-recording@atlanta.com",
+ "sip:operator@cs.columbia.edu",
+ "sip:p1.domain.com;lr",
+ "sip:p1.example.com;lr",
+ "sip:p2.domain.com;lr",
+ "sips:1212@gateway.com",
+ "sips:+358-555-1234567@foo.com;postd=pp22;user=phone",
+ "sips:+358-555-1234567;postd=pp22@foo.com;user=phone",
+ "sips:alice@atlanta.com?subject=project%20x&priority=urgent",
+ "sip:server10.biloxi.com;lr",
+ "sip:ss1.carrier.com",
+ "sip:user@host?Subject=foo&Call-Info=<http://www.foo.com>",
+ "sip:watson@bell-telephone.com",
+ "sip:watson@worcester.bell-telephone.com",
+#endif
+ /* from RFC 3368 */
+ "go:Mercedes%20Benz",
+ "go://?Mercedes%20Benz",
+ "go://cnrp.foo.com?Mercedes%20Benz;geography=US-ga",
+ "go://cnrp.foo.org?Martin%20J.%20D%C3%BCrst",
+ "go://cnrp.foo.com?id=5432345",
+ /* from RFC 3507 */
+ "icap://icap.example.net:2000/services/icap-service-1",
+ "icap://icap.net/service?mode=translate&lang=french",
+ "icap://icap.example.net/translate?mode=french",
+ "icap://icap-server.net/server?arg=87",
+ "icap://icap.example.org/satisf",
+ "icap://icap.server.net/sample-service",
+ /* from RFC 3510 */
+ "ipp://example.com",
+ "ipp://example.com/printer",
+ "ipp://example.com/printer/tiger",
+ "ipp://example.com/printer/fox",
+ "ipp://example.com/printer/tiger/bob",
+ "ipp://example.com/printer/tiger/ira",
+ "ipp://example.com",
+ "ipp://example.com/~smith/printer",
+ "ipp://example.com:631/~smith/printer",
+ "ipp://example.com/printer/123",
+ "ipp://example.com/printer/tiger/job123",
+ /* from RFC 3529 */
+ "xmlrpc.beep://stateserver.example.com/NumberToName",
+ "xmlrpc.beep://stateserver.example.com:1026",
+ "xmlrpc.beep://stateserver.example.com",
+ "xmlrpc.beep://10.0.0.2:1026",
+ "xmlrpc.beeps://stateserver.example.com/NumberToName",
+ /* from RFC 3617 */
+ "tftp://example.com/myconfigurationfile;mode=netascii",
+ "tftp://example.com/mystartupfile",
+ /* from RFC 3859 */
+ "pres:fred@example.com",
+ /* from RFC 3860 */
+ "im:fred@example.com",
+ "im:pepp=example.com/fred@relay-domain",
+ /* from RFC 3966 */
+ "tel:+1-201-555-0123",
+ "tel:7042;phone-context=example.com",
+ "tel:863-1234;phone-context=+1-914-555",
+ /* from RFC 3981 */
+ "iris:dreg1//example.com/local/myhosts",
+ "iris:dreg1//com",
+ "iris:dreg1//com/iris/id",
+ "iris:dreg1//example.com/domain/example.com",
+ "iris:dreg1//example.com",
+ "iris:dreg1//com/domain/example.com",
+ "iris:dreg1//192.0.2.1:44/domain/example.com",
+ "iris.lwz:dreg1//192.0.2.1:44/domain/example.com",
+ "iris.beep:dreg1//com/domain/example.com",
+ "iris:dreg1/bottom/example.com/domain/example.com",
+ "iris.beep:dreg1/bottom/example.com/domain/example.com",
+ /* from RFC 3986 */
+ "ftp://ftp.is.co.za/rfc/rfc1808.txt",
+ "http://www.ietf.org/rfc/rfc2396.txt",
+ "ldap://[2001:db8::7]/c=GB?objectClass?one",
+ "mailto:John.Doe@example.com",
+ "news:comp.infosystems.www.servers.unix",
+ "tel:+1-816-555-1212",
+ "telnet://192.0.2.16:80/",
+ "urn:oasis:names:specification:docbook:dtd:xml:4.1.2",
+ /* from RFC 4078 */
+ "crid://example.com/foobar",
+ "crid://example.co.jp/%E3%82%A8%E3%82%A4%E3%82%AC",
+ /* from RFC 4088 */
+ "snmp://example.com",
+ "snmp://tester5@example.com:8161",
+ "snmp://example.com/bridge1",
+ "snmp://example.com/bridge1;800002b804616263",
+ "snmp://example.com//1.3.6.1.2.1.1.3.0",
+ "snmp://example.com//1.3.6.1.2.1.1.3+",
+ "snmp://example.com//1.3.6.1.2.1.1.3.*",
+ "snmp://example.com/bridge1/1.3.6.1.2.1.2.2.1.8.*",
+ "snmp://example.com//(1.3.6.1.2.1.2.2.1.7,1.3.6.1.2.1.2.2.1.8).*",
+ /* from RFC 4151 */
+ "tag:timothy@hpl.hp.com,2001:web/externalHome",
+ "tag:sandro@w3.org,2004-05:Sandro",
+ "tag:my-ids.com,2001-09-15:TimKindberg:presentations:UBath2004-05-19",
+ "tag:blogger.com,1999:blog-555",
+ "tag:yaml.org,2002:int",
+ /* from RFC 4227 */
+ "soap.beep://stockquoteserver.example.com/StockQuote",
+ "soap.beep://stockquoteserver.example.com:1026",
+ "soap.beep://stockquoteserver.example.com",
+ "soap.beep://192.0.2.0:1026",
+ /* from RFC 4324 */
+ "cap://cal.example.com",
+ "cap://cal.example.com/Company/Holidays",
+ "cap://cal.example.com/abcd1234Usr",
+ "cap://cal.example.com/abcd1234USR",
+ "cap://host.com/joe",
+ "cap:example.com/Doug",
+ "cap://cal.example.com/sdfifgty4321",
+ "cap://calendar.example.com",
+ "cap://mycal.example.com",
+ /* from RFC 4452 */
+ "info:ddc/22/eng//004.678",
+ "info:lccn/2002022641",
+ "info:sici/0363-0277(19950315)120:5%3C%3E1.0.TX;2-V",
+ "info:bibcode/2003Icar..163..263Z",
+ "info:pmid/12376099",
+ /* from RFC 4501 */
+ "dns:www.example.org.?clAsS=IN;tYpE=A",
+ "dns:www.example.org",
+ "dns:simon.example.org?type=CERT",
+ "dns://192.168.1.1/ftp.example.org?type=A",
+ "dns:world%20wide%20web.example%5c.domain.org?TYPE=TXT",
+#if 0 // contains %00 encoding, which is currently always rejected
+ "dns://fw.example.org/*.%20%00.example?type=TXT",
+#endif
+ /* from RFC 4516 */
+ "ldap:///o=University%20of%20Michigan,c=US",
+ "ldap://ldap1.example.net/o=University%20of%20Michigan,c=US",
+ "ldap://ldap1.example.net/o=University%20of%20Michigan,"
+ "c=US?postalAddress",
+ "ldap://ldap1.example.net:6666/o=University%20of%20Michigan,"
+ "c=US?\?sub?(cn=Babs%20Jensen)",
+ "LDAP://ldap1.example.com/c=GB?objectClass?ONE",
+ "ldap://ldap2.example.com/o=Question%3f,c=US?mail",
+ "ldap://ldap3.example.com/o=Babsco,c=US"
+ "??\?(four-octet=%5c00%5c00%5c00%5c04)",
+ "ldap://ldap.example.com/o=An%20Example%5C2C%20Inc.,c=US",
+ "ldap://ldap.example.net",
+ "ldap://ldap.example.net/",
+ "ldap://ldap.example.net/?",
+ "ldap:///?\?sub?\?e-bindname=cn=Manager%2cdc=example%2cdc=com",
+ "ldap:///?\?sub?\?!e-bindname=cn=Manager%2cdc=example%2cdc=com"
+ /* from RFC 4975 */
+ "msrp://atlanta.example.com:7654/jshA7weztas;tcp",
+ "msrp://biloxi.example.com:12763/kjhd37s2s20w2a;tcp",
+ "msrp://host.example.com:8493/asfd34;tcp",
+ "msrp://alice.example.com:7394/2s93i9ek2a;tcp",
+ "msrp://bob.example.com:8493/si438dsaodes;tcp",
+ "msrp://alicepc.example.com:7777/iau39soe2843z;tcp",
+ "msrp://bob.example.com:8888/9di4eae923wzd;tcp",
+ "msrp://alice.example.com:7777/iau39soe2843z;tcp",
+ "msrp://bobpc.example.com:8888/9di4eae923wzd;tcp",
+ "msrp://alicepc.example.com:7654/iau39soe2843z;tcp",
+ "msrp://alicepc.example.com:8888/9di4eae923wzd;tcp",
+ "msrp://example.com:7777/iau39soe2843z;tcp",
+ "msrp://bob.example.com:8888/9di4eae923wzd;tcp",
+ /* from RFC 5092 */
+ "imap://michael@example.org/INBOX",
+ "imap://bester@example.org/INBOX",
+ "imap://joe@example.com/INBOX/;uid=20/;section=1.2;urlauth="
+ "submit+fred:internal:91354a473744909de610943775f92038",
+ "imap://minbari.example.org/gray-council;UIDVALIDITY=385759045/;"
+ "UID=20/;PARTIAL=0.1024",
+ "imap://psicorp.example.org/~peter/%E6%97%A5%E6%9C%AC%E8%AA%9E/"
+ "%E5%8F%B0%E5%8C%97",
+ "imap://;AUTH=GSSAPI@minbari.example.org/gray-council/;uid=20/"
+ ";section=1.2",
+ "imap://;AUTH=*@minbari.example.org/gray%20council?"
+ "SUBJECT%20shadows",
+ "imap://john;AUTH=*@minbari.example.org/babylon5/personel?"
+ "charset%20UTF-8%20SUBJECT%20%7B14+%7D%0D%0A%D0%98%D0%B2%"
+ "D0%B0%D0%BD%D0%BE%D0%B2%D0%B0",
+ /* from RFC 5122 */
+ "xmpp:node@example.com",
+ "xmpp://guest@example.com",
+ "xmpp:guest@example.com",
+ "xmpp://guest@example.com/support@example.com?message",
+ "xmpp:support@example.com?message",
+ "xmpp:example-node@example.com",
+ "xmpp:example-node@example.com/some-resource",
+ "xmpp:example.com",
+ "xmpp:example-node@example.com?message",
+ "xmpp:example-node@example.com?message;subject=Hello%20World",
+ "xmpp:example-node@example.com",
+ "xmpp:example-node@example.com?query",
+ "xmpp:nasty!%23$%25()*+,-.;=%3F%5B%5C%5D%5E_%60%7B%7C%7D~node@example.com",
+ "xmpp:node@example.com/repulsive%20!%23%22$%25&'()*+,-.%2F:;%3C="
+ "%3E%3F%40%5B%5C%5D%5E_%60%7B%7C%7D~resource",
+ "xmpp:ji%C5%99i@%C4%8Dechy.example/v%20Praze",
+ /* from RFC 5456 */
+#if 0 // these don't comply with RFC 3986
+ "iax:example.com/alice",
+ "iax:example.com:4569/alice",
+ "iax:example.com:4570/alice?friends",
+ "iax:192.0.2.4:4569/alice?friends",
+ "iax:[2001:db8::1]:4569/alice?friends",
+ "iax:example.com/12022561414",
+ "iax:johnQ@example.com/12022561414",
+ "iax:atlanta.com/alice",
+ "iax:AtLaNtA.com/ALicE",
+ "iax:atlanta.com:4569/alice",
+ "iax:alice@atlanta.com/alice",
+ "iax:alice@AtLaNtA.com:4569/ALicE",
+ "iax:ALICE@atlanta.com/alice",
+ "iax:alice@atlanta.com/alice",
+#endif
+ /* from RFC 5724 */
+ "sms:+15105550101",
+ "sms:+15105550101,+15105550102",
+ "sms:+15105550101?body=hello%20there",
+ /* from RFC 5804 */
+ "sieve://example.com//script",
+ "sieve://example.com/script",
+ /* from RFC 5538 */
+ "news://news.server.example/example.group.this",
+ "news://news.server.example/*",
+ "news://news.server.example/",
+ "news://wild.server.example/example.group.th%3Fse",
+ "news:example.group.*",
+ "news:example.group.this",
+ "news://news.gmane.org/gmane.ietf.tools",
+ "news://news.gmane.org/p0624081dc30b8699bf9b@%5B10.20.30.108%5D",
+ "nntp://wild.server.example/example.group.n%2Fa/12345",
+ "nntp://news.server.example/example.group.this",
+ "nntp://news.gmane.org/gmane.ietf.tools/742",
+ "nntp://news.server.example/example.group.this/12345",
+ /* from RFC 5870 */
+ "geo:13.4125,103.8667",
+ "geo:48.2010,16.3695,183",
+ "geo:48.198634,16.371648;crs=wgs84;u=40",
+ "geo:90,-22.43;crs=WGS84",
+ "geo:90,46",
+ "geo:22.300;-118.44",
+ "geo:22.3;-118.4400",
+ "geo:66,30;u=6.500;FOo=this%2dthat",
+ "geo:66.0,30;u=6.5;foo=this-that",
+ "geo:70,20;foo=1.00;bar=white",
+ "geo:70,20;foo=1;bar=white",
+ "geo:47,11;foo=blue;bar=white",
+ "geo:47,11;bar=white;foo=blue",
+ "geo:22,0;bar=Blue",
+ "geo:22,0;BAR=blue",
+ /* from RFC 6068 */
+ "mailto:addr1@an.example,addr2@an.example",
+ "mailto:?to=addr1@an.example,addr2@an.example",
+ "mailto:addr1@an.example?to=addr2@an.example",
+ "mailto:chris@example.com",
+ "mailto:infobot@example.com?subject=current-issue",
+ "mailto:infobot@example.com?body=send%20current-issue",
+ "mailto:infobot@example.com?body=send%20current-issue%0D%0Asend%20index",
+ "mailto:list@example.org?In-Reply-To=%3C3469A91.D10AF4C@example.com%3E",
+ "mailto:majordomo@example.com?body=subscribe%20bamboo-l",
+ "mailto:joe@example.com?cc=bob@example.com&body=hello",
+ "mailto:gorby%25kremvax@example.com",
+ "mailto:unlikely%3Faddress@example.com?blat=foop",
+ "mailto:joe@an.example?cc=bob@an.example&amp;body=hello",
+ "mailto:Mike%26family@example.org",
+ "mailto:%22not%40me%22@example.org",
+ "mailto:%22oh%5C%5Cno%22@example.org",
+ "mailto:%22%5C%5C%5C%22it's%5C%20ugly%5C%5C%5C%22%22@example.org",
+ "mailto:user@example.org?subject=caf%C3%A9",
+ "mailto:user@example.org?subject=%3D%3Futf-8%3FQ%3Fcaf%3DC3%3DA9%3F%3D",
+ "mailto:user@example.org?subject=%3D%3Fiso-8859-1%3FQ%3Fcaf%3DE9%3F%3D",
+ "mailto:user@example.org?subject=caf%C3%A9&body=caf%C3%A9",
+ "mailto:user@%E7%B4%8D%E8%B1%86.example.org?subject=Test&body=NATTO",
+ /* from RFC 6455 */
+ "ws://example.com/chat",
+ /* from RFC 6694 */
+ "about:blank",
+ /* from RFC 6733 */
+#if 0 // these don't comply with RFC 3986
+ "aaa://host.example.com;transport=tcp",
+ "aaa://host.example.com:6666;transport=tcp",
+ "aaa://host.example.com;protocol=diameter",
+ "aaa://host.example.com:6666;protocol=diameter",
+ "aaa://host.example.com:6666;transport=tcp;protocol=diameter",
+ "aaa://host.example.com:1813;transport=udp;protocol=radius",
+#endif
+ /* from RFC 6787 */
+ "session:request1@form-level.store",
+ "session:help@root-level.store",
+ "session:menu1@menu-level.store",
+ "session:request1@form-level.store",
+ "session:request2@field-level.store",
+ "session:helpgramar@root-level.store",
+ "session:request1@form-level.store",
+ "session:field3@form-level.store",
+ /* from RFC 6920 */
+ "ni:///sha-256;UyaQV-Ev4rdLoHyJJWCi11OHfrYv9E1aGQAlMO2X_-Q",
+ "ni:///sha-256-32;f4OxZQ?ct=text/plain",
+ "ni:///sha-256;f4OxZX_x_FO5LcGBSKHWXfwtSx-j1ncoSt3SABJtkGk",
+ "ni://example.com/sha-256;f4OxZX_x_FO5LcGBSKHWXfwtSx-j1ncoSt3SABJtkGk",
+ "nih:sha-256-120;5326-9057-e12f-e2b7-4ba0-7c89-2560-a2;f",
+ "nih:sha-256-32;53269057;b",
+ "nih:3;532690-57e12f-e2b74b-a07c89-2560a2;f",
+ /* from RFC 7064 */
+ "stun:example.org",
+ "stuns:example.org",
+ "stun:example.org:8000",
+ /* from RFC 7065 */
+ "turn:example.org",
+ "turns:example.org",
+ "turn:example.org:8000",
+ "turn:example.org?transport=udp",
+ "turn:example.org?transport=tcp",
+ "turns:example.org?transport=tcp",
+ /* from RFC 7230 */
+ "http://www.example.com/hello.txt",
+ "http://example.com:80/~smith/home.html",
+ "http://EXAMPLE.com/%7Esmith/home.html",
+ "http://EXAMPLE.com:/%7esmith/home.html",
+ "http://www.example.org/where?q=now",
+ "http://www.example.org/pub/WWW/TheProject.html",
+ "http://www.example.org:8001",
+ "http://www.example.org:8080/pub/WWW/TheProject.html",
+ /* from RFC 7252 */
+ "coap://example.com:5683/~sensors/temp.xml",
+ "coap://EXAMPLE.com/%7Esensors/temp.xml",
+ "coap://EXAMPLE.com:/%7esensors/temp.xml",
+ "coap://server/temperature",
+ "coap://[2001:db8::2:1]/",
+ "coap://example.net/",
+ "coap://example.net/.well-known/core",
+ "coap://xn--18j4d.example/%E3%81%93%E3%82%93%E3%81%AB%E3%81%A1%E3%81%AF",
+ "coap://198.51.100.1:61616//%2F//?%2F%2F&?%26"
+ /* from draft-ietf-appsawg-acct-uri-06 */
+ "acct:foobar@status.example.net",
+ "acct:user@example.com",
+ "acct:bob@example.com",
+ /* from draft-mcdonald-ipps-uri-scheme-18 */
+ "ipps://example.com/",
+ "ipps://example.com/ipp",
+ "ipps://example.com/ipp/faxout",
+ "ipps://example.com/ipp/print",
+ "ipps://example.com/ipp/scan",
+ "ipps://example.com/ipp/print/bob",
+ "ipps://example.com/ipp/print/ira",
+ "ipps://example.com/",
+ "ipps://example.com/ipp/print",
+ "ipps://example.com:631/ipp/print",
+ /* from draft-pechanec-pkcs11uri-21 */
+ "pkcs11:",
+ "pkcs11:object=my-pubkey;type=public",
+ "pkcs11:object=my-key;type=private?pin-source=file:/etc/token",
+ "pkcs11:token=The%20Software%20PKCS%2311%20Softtoken;"
+ "manufacturer=Snake%20Oil,%20Inc.;model=1.0;"
+ "object=my-certificate;type=cert;"
+ "id=%69%95%3E%5C%F4%BD%EC%91;serial="
+ "?pin-source=file:/etc/token_pin",
+ "pkcs11:object=my-sign-key;type=private?module-name=mypkcs11",
+ "pkcs11:object=my-sign-key;type=private"
+ "?module-path=/mnt/libmypkcs11.so.1",
+ "pkcs11:token=Software%20PKCS%2311%20softtoken;"
+ "manufacturer=Snake%20Oil,%20Inc.?pin-value=the-pin",
+ "pkcs11:slot-description=Sun%20Metaslot",
+ "pkcs11:library-manufacturer=Snake%20Oil,%20Inc.;"
+ "library-description=Soft%20Token%20Library;"
+ "library-version=1.23",
+ "pkcs11:token=My%20token%25%20created%20by%20Joe;"
+ "library-version=3;id=%01%02%03%Ba%dd%Ca%fe%04%05%06",
+ "pkcs11:token=A%20name%20with%20a%20substring%20%25%3B;"
+ "object=my-certificate;type=cert",
+ "pkcs11:token=Name%20with%20a%20small%20A%20with%20acute:%20%C3%A1;"
+ "object=my-certificate;type=cert",
+ "pkcs11:token=my-token;object=my-certificate;"
+ "type=cert;vendor-aaa=value-a"
+ "?pin-source=file:/etc/token_pin&vendor-bbb=value-b"
+};
+
+unsigned int rfc_uri_test_count = N_ELEMENTS(rfc_uri_tests);
+
+static void test_uri_rfc(void)
+{
+ unsigned int i;
+
+ test_begin("uri from rfcs");
+ for (i = 0; i < rfc_uri_test_count; i++) T_BEGIN {
+ const char *uri_in, *error = NULL;
+ int ret;
+
+ uri_in = rfc_uri_tests[i];
+
+ ret = uri_check(uri_in, URI_PARSE_ALLOW_FRAGMENT_PART, &error);
+ test_out_quiet(
+ t_strdup_printf("parse [%d] <%s>", i, str_sanitize(uri_in, 64)),
+ ret >= 0);
+ } T_END;
+ test_end();
+}
+
+static void test_uri_escape(void)
+{
+ string_t *str = t_str_new(256);
+
+ test_begin("uri escape - userinfo");
+ uri_append_user_data(str, NULL, "abcdefghijklmnopqrstuvwxyz");
+ test_assert(strcmp(str_c(str), "abcdefghijklmnopqrstuvwxyz") == 0);
+ str_truncate(str, 0);
+ uri_append_user_data(str, NULL, "ABCDEFGHIJKLMNOPQRSTUVWXYZ");
+ test_assert(strcmp(str_c(str), "ABCDEFGHIJKLMNOPQRSTUVWXYZ") == 0);
+ str_truncate(str, 0);
+ uri_append_user_data(str, NULL, "0123456789");
+ test_assert(strcmp(str_c(str), "0123456789") == 0);
+ str_truncate(str, 0);
+ uri_append_user_data(str, NULL, "-._~!$&'()*+,;=");
+ test_assert(strcmp(str_c(str), "-._~!$&'()*+,;=") == 0);
+ str_truncate(str, 0);
+ uri_append_user_data(str, NULL, "a@b/c/d:e");
+ test_assert(strcmp(str_c(str), "a%40b%2fc%2fd:e") == 0);
+ str_truncate(str, 0);
+ uri_append_user_data(str, NULL, "[yes]what?oh#13");
+ test_assert(strcmp(str_c(str), "%5byes%5dwhat%3foh%2313") == 0);
+ str_truncate(str, 0);
+ uri_append_user_data(str, ":", "a@b/c/d:e");
+ test_assert(strcmp(str_c(str), "a%40b%2fc%2fd%3ae") == 0);
+ str_truncate(str, 0);
+ test_end();
+
+ test_begin("uri escape - path segment");
+ uri_append_path_segment_data(str, NULL, "abcdefghijklmnopqrstuvwxyz");
+ test_assert(strcmp(str_c(str), "abcdefghijklmnopqrstuvwxyz") == 0);
+ str_truncate(str, 0);
+ uri_append_path_segment_data(str, NULL, "ABCDEFGHIJKLMNOPQRSTUVWXYZ");
+ test_assert(strcmp(str_c(str), "ABCDEFGHIJKLMNOPQRSTUVWXYZ") == 0);
+ str_truncate(str, 0);
+ uri_append_path_segment_data(str, NULL, "0123456789");
+ test_assert(strcmp(str_c(str), "0123456789") == 0);
+ str_truncate(str, 0);
+ uri_append_path_segment_data(str, NULL, "-._~!$&'()*+,;=");
+ test_assert(strcmp(str_c(str), "-._~!$&'()*+,;=") == 0);
+ str_truncate(str, 0);
+ uri_append_path_segment_data(str, NULL, "a@b/c/d:e");
+ test_assert(strcmp(str_c(str), "a@b%2fc%2fd:e") == 0);
+ str_truncate(str, 0);
+ uri_append_path_segment_data(str, NULL, "[yes]what?oh#13");
+ test_assert(strcmp(str_c(str), "%5byes%5dwhat%3foh%2313") == 0);
+ str_truncate(str, 0);
+ uri_append_path_segment_data(str, "@", "a@b/c/d:e");
+ test_assert(strcmp(str_c(str), "a%40b%2fc%2fd:e") == 0);
+ str_truncate(str, 0);
+ test_end();
+
+ test_begin("uri escape - path");
+ uri_append_path_data(str, NULL, "abcdefghijklmnopqrstuvwxyz");
+ test_assert(strcmp(str_c(str), "abcdefghijklmnopqrstuvwxyz") == 0);
+ str_truncate(str, 0);
+ uri_append_path_data(str, NULL, "ABCDEFGHIJKLMNOPQRSTUVWXYZ");
+ test_assert(strcmp(str_c(str), "ABCDEFGHIJKLMNOPQRSTUVWXYZ") == 0);
+ str_truncate(str, 0);
+ uri_append_path_data(str, NULL, "0123456789");
+ test_assert(strcmp(str_c(str), "0123456789") == 0);
+ str_truncate(str, 0);
+ uri_append_path_data(str, NULL, "-._~!$&'()*+,;=");
+ test_assert(strcmp(str_c(str), "-._~!$&'()*+,;=") == 0);
+ str_truncate(str, 0);
+ uri_append_path_data(str, NULL, "a@b/c/d:e");
+ test_assert(strcmp(str_c(str), "a@b/c/d:e") == 0);
+ str_truncate(str, 0);
+ uri_append_path_data(str, NULL, "[yes]what?oh#13");
+ test_assert(strcmp(str_c(str), "%5byes%5dwhat%3foh%2313") == 0);
+ str_truncate(str, 0);
+ uri_append_path_data(str, "@", "a@b/c/d:e");
+ test_assert(strcmp(str_c(str), "a%40b/c/d:e") == 0);
+ str_truncate(str, 0);
+ test_end();
+
+ test_begin("uri escape - query");
+ uri_append_query_data(str, NULL, "abcdefghijklmnopqrstuvwxyz");
+ test_assert(strcmp(str_c(str), "abcdefghijklmnopqrstuvwxyz") == 0);
+ str_truncate(str, 0);
+ uri_append_query_data(str, NULL, "ABCDEFGHIJKLMNOPQRSTUVWXYZ");
+ test_assert(strcmp(str_c(str), "ABCDEFGHIJKLMNOPQRSTUVWXYZ") == 0);
+ str_truncate(str, 0);
+ uri_append_query_data(str, NULL, "0123456789");
+ test_assert(strcmp(str_c(str), "0123456789") == 0);
+ str_truncate(str, 0);
+ uri_append_query_data(str, NULL, "-._~!$&'()*+,;=");
+ test_assert(strcmp(str_c(str), "-._~!$&'()*+,;=") == 0);
+ str_truncate(str, 0);
+ uri_append_query_data(str, NULL, "a@b/c/d:e");
+ test_assert(strcmp(str_c(str), "a@b/c/d:e") == 0);
+ str_truncate(str, 0);
+ uri_append_query_data(str, NULL, "[yes]what?oh#13");
+ test_assert(strcmp(str_c(str), "%5byes%5dwhat?oh%2313") == 0);
+ str_truncate(str, 0);
+ uri_append_query_data(str, "@", "a@b/c/d:e");
+ test_assert(strcmp(str_c(str), "a%40b/c/d:e") == 0);
+ str_truncate(str, 0);
+ test_end();
+
+ test_begin("uri escape - fragment");
+ uri_append_fragment_data(str, NULL, "abcdefghijklmnopqrstuvwxyz");
+ test_assert(strcmp(str_c(str), "abcdefghijklmnopqrstuvwxyz") == 0);
+ str_truncate(str, 0);
+ uri_append_fragment_data(str, NULL, "ABCDEFGHIJKLMNOPQRSTUVWXYZ");
+ test_assert(strcmp(str_c(str), "ABCDEFGHIJKLMNOPQRSTUVWXYZ") == 0);
+ str_truncate(str, 0);
+ uri_append_fragment_data(str, NULL, "0123456789");
+ test_assert(strcmp(str_c(str), "0123456789") == 0);
+ str_truncate(str, 0);
+ uri_append_fragment_data(str, NULL, "-._~!$&'()*+,;=");
+ test_assert(strcmp(str_c(str), "-._~!$&'()*+,;=") == 0);
+ str_truncate(str, 0);
+ uri_append_fragment_data(str, NULL, "a@b/c/d:e");
+ test_assert(strcmp(str_c(str), "a@b/c/d:e") == 0);
+ str_truncate(str, 0);
+ uri_append_fragment_data(str, NULL, "[yes]what?oh#13");
+ test_assert(strcmp(str_c(str), "%5byes%5dwhat?oh%2313") == 0);
+ str_truncate(str, 0);
+ uri_append_fragment_data(str, "@", "a@b/c/d:e");
+ test_assert(strcmp(str_c(str), "a%40b/c/d:e") == 0);
+ str_truncate(str, 0);
+ test_end();
+
+ test_begin("uri escape - unreserved");
+ uri_append_unreserved(str, "abcdefghijklmnopqrstuvwxyz");
+ test_assert(strcmp(str_c(str), "abcdefghijklmnopqrstuvwxyz") == 0);
+ str_truncate(str, 0);
+ uri_append_unreserved(str, "ABCDEFGHIJKLMNOPQRSTUVWXYZ");
+ test_assert(strcmp(str_c(str), "ABCDEFGHIJKLMNOPQRSTUVWXYZ") == 0);
+ str_truncate(str, 0);
+ uri_append_unreserved(str, "0123456789");
+ test_assert(strcmp(str_c(str), "0123456789") == 0);
+ str_truncate(str, 0);
+ uri_append_unreserved(str, "-._~");
+ test_assert(strcmp(str_c(str), "-._~") == 0);
+ str_truncate(str, 0);
+ uri_append_unreserved(str, "!$&'()*+,;=");
+ test_assert(strcmp(str_c(str), "%21%24%26%27%28%29%2a%2b%2c%3b%3d") == 0);
+ str_truncate(str, 0);
+ uri_append_unreserved(str, "a@b/c/d:e");
+ test_assert(strcmp(str_c(str), "a%40b%2fc%2fd%3ae") == 0);
+ str_truncate(str, 0);
+ uri_append_unreserved(str, "[yes]what?oh#13");
+ test_assert(strcmp(str_c(str), "%5byes%5dwhat%3foh%2313") == 0);
+ str_truncate(str, 0);
+ test_end();
+
+ test_begin("uri escape - unreserved");
+ uri_append_unreserved_path(str, "abcdefghijklmnopqrstuvwxyz");
+ test_assert(strcmp(str_c(str), "abcdefghijklmnopqrstuvwxyz") == 0);
+ str_truncate(str, 0);
+ uri_append_unreserved_path(str, "ABCDEFGHIJKLMNOPQRSTUVWXYZ");
+ test_assert(strcmp(str_c(str), "ABCDEFGHIJKLMNOPQRSTUVWXYZ") == 0);
+ str_truncate(str, 0);
+ uri_append_unreserved_path(str, "0123456789");
+ test_assert(strcmp(str_c(str), "0123456789") == 0);
+ str_truncate(str, 0);
+ uri_append_unreserved_path(str, "-._~");
+ test_assert(strcmp(str_c(str), "-._~") == 0);
+ str_truncate(str, 0);
+ uri_append_unreserved_path(str, "!$&'()*+,;=");
+ test_assert(strcmp(str_c(str), "%21%24%26%27%28%29%2a%2b%2c%3b%3d") == 0);
+ str_truncate(str, 0);
+ uri_append_unreserved_path(str, "a@b/c/d:e");
+ test_assert(strcmp(str_c(str), "a%40b/c/d%3ae") == 0);
+ str_truncate(str, 0);
+ uri_append_unreserved_path(str, "[yes]what?oh#13");
+ test_assert(strcmp(str_c(str), "%5byes%5dwhat%3foh%2313") == 0);
+ str_truncate(str, 0);
+ test_end();
+}
+
+void test_uri(void)
+{
+ test_uri_valid();
+ test_uri_invalid();
+ test_uri_rfc();
+ test_uri_escape();
+}
diff --git a/src/lib/test-utc-mktime.c b/src/lib/test-utc-mktime.c
new file mode 100644
index 0000000..157179b
--- /dev/null
+++ b/src/lib/test-utc-mktime.c
@@ -0,0 +1,61 @@
+/* Copyright (c) 2007-2018 Dovecot authors, see the included COPYING file */
+
+#include "test-lib.h"
+#include "utc-mktime.h"
+
+struct test_utc_mktime {
+ int year, month, day, hour, min, sec;
+ time_t out;
+};
+
+void test_utc_mktime(void)
+{
+ static const struct test_utc_mktime tests[] = {
+#ifdef TIME_T_SIGNED
+ { 1969, 12, 31, 23, 59, 59, -1 },
+ { 1901, 12, 13, 20, 45, 53, -2147483647 },
+#endif
+#if (TIME_T_MAX_BITS > 32 || !defined(TIME_T_SIGNED))
+ { 2106, 2, 7, 6, 28, 15, 4294967295 },
+ { 2038, 1, 19, 3, 14, 8, 2147483648 },
+#endif
+ { 2007, 11, 7, 1, 7, 20, 1194397640 },
+ { 1970, 1, 1, 0, 0, 0, 0 },
+ { 2038, 1, 19, 3, 14, 7, 2147483647 },
+ { INT_MAX, INT_MAX, INT_MAX, INT_MAX, INT_MAX, INT_MAX, -1 },
+#if TIME_T_MAX_BITS > 32
+ { 2106, 2, 7, 6, 28, 16, 4294967296 },
+#endif
+ /* June leap second */
+ { 2015, 6, 30, 23, 59, 59, 1435708799 },
+ { 2015, 6, 30, 23, 59, 60, 1435708799 },
+ { 2015, 7, 1, 0, 0, 0, 1435708800 },
+ /* Invalid leap second */
+ { 2017, 1, 24, 16, 40, 60, 1485276059 },
+ /* Dec leap second */
+ { 2016, 12, 31, 23, 59, 59, 1483228799 },
+ { 2016, 12, 31, 23, 59, 60, 1483228799 },
+ { 2017, 1, 1, 0, 0, 0, 1483228800 },
+ };
+ struct tm tm;
+ unsigned int i;
+ time_t t;
+ bool success;
+
+ for (i = 0; i < N_ELEMENTS(tests); i++) {
+ const struct test_utc_mktime *test = &tests[i];
+ i_zero(&tm);
+ tm.tm_year = test->year - 1900;
+ tm.tm_mon = test->month - 1;
+ tm.tm_mday = test->day;
+ tm.tm_hour = test->hour;
+ tm.tm_min = test->min;
+ tm.tm_sec = test->sec;
+
+ t = utc_mktime(&tm);
+ success = t == test->out;
+ test_out_reason(t_strdup_printf("utc_mktime(%d)", i), success,
+ success ? NULL : t_strdup_printf("%ld != %ld",
+ (long)t, (long)test->out));
+ }
+}
diff --git a/src/lib/test-var-expand.c b/src/lib/test-var-expand.c
new file mode 100644
index 0000000..558ef76
--- /dev/null
+++ b/src/lib/test-var-expand.c
@@ -0,0 +1,474 @@
+/* Copyright (c) 2009-2018 Dovecot authors, see the included COPYING file */
+
+#include "test-lib.h"
+#include "str.h"
+#include "env-util.h"
+#include "hostpid.h"
+#include "var-expand.h"
+#include "var-expand-private.h"
+
+struct var_expand_test {
+ const char *in;
+ const char *out;
+ int ret;
+};
+
+struct var_get_key_range_test {
+ const char *in;
+ unsigned int idx, size;
+};
+
+static void test_var_expand_ranges(void)
+{
+ static const struct var_expand_test tests[] = {
+ { "%v", "value1234", 1 },
+ { "%3v", "val", 1 },
+ { "%3.2v", "ue", 1 },
+ { "%3.-2v", "ue12", 1 },
+ { "%-3.2v", "23", 1 },
+ { "%0.-1v", "value123", 1 },
+ { "%-4.-1v", "123", 1 }
+ };
+ static const struct var_expand_table table[] = {
+ { 'v', "value1234", NULL },
+ { '\0', NULL, NULL }
+ };
+ string_t *str = t_str_new(128);
+ const char *error;
+ unsigned int i;
+
+ test_begin("var_expand - ranges");
+ for (i = 0; i < N_ELEMENTS(tests); i++) {
+ str_truncate(str, 0);
+ test_assert(var_expand(str, tests[i].in, table, &error) == tests[i].ret);
+ test_assert(strcmp(tests[i].out, str_c(str)) == 0);
+ }
+ test_end();
+}
+
+static void test_var_expand_builtin(void)
+{
+ static struct var_expand_test tests[] = {
+ { "%{hostname}", NULL, 1 },
+ { "%{pid}", NULL, 1 },
+ { "a%{env:FOO}b", "abaRb", 1 },
+ { "%50Hv", "1f", 1 },
+ { "%50Hw", "2e", 1 },
+ { "%50Nv", "25", 1 },
+ { "%50Nw", "e", 1 },
+
+ { "%{nonexistent}", "UNSUPPORTED_VARIABLE_nonexistent", 0 },
+ { "%1.2M{nonexistent:default}", "UNSUPPORTED_VARIABLE_nonexistent", 0 },
+ { "%x", "UNSUPPORTED_VARIABLE_x", 0 },
+ { "%5Mm", "UNSUPPORTED_VARIABLE_m", 0 },
+ };
+ static const struct var_expand_table table[] = {
+ { 'v', "value", NULL },
+ { 'w', "value2", NULL },
+ { '\0', NULL, NULL }
+ };
+ string_t *str = t_str_new(128);
+ const char *error;
+ unsigned int i;
+
+ tests[0].out = my_hostname;
+ tests[1].out = my_pid;
+ env_put("FOO", "baR");
+
+ test_begin("var_expand - builtin");
+ for (i = 0; i < N_ELEMENTS(tests); i++) {
+ str_truncate(str, 0);
+ test_assert_idx(var_expand(str, tests[i].in, table, &error) == tests[i].ret, i);
+ test_assert_idx(strcmp(tests[i].out, str_c(str)) == 0, i);
+ }
+ test_end();
+}
+
+static void test_var_get_key_range(void)
+{
+ static const struct var_get_key_range_test tests[] = {
+ { "", 0, 0 },
+ { "{", 1, 0 },
+ { "k", 0, 1 },
+ { "{key}", 1, 3 },
+ { "5.5Rk", 4, 1 },
+ { "5.5R{key}", 5, 3 },
+ { "{key", 1, 3 },
+ { "{if;%{if;%{value};eq;value;t;f};eq;t;t;f}", 1, 39 },
+ };
+ unsigned int i, idx, size;
+
+ test_begin("var_get_key_range");
+ for (i = 0; i < N_ELEMENTS(tests); i++) {
+ var_get_key_range(tests[i].in, &idx, &size);
+ test_assert_idx(tests[i].idx == idx, i);
+ test_assert_idx(tests[i].size == size, i);
+
+ if (tests[i].size == 1)
+ test_assert_idx(tests[i].in[idx] == var_get_key(tests[i].in), i);
+ }
+ test_end();
+}
+
+static int test_var_expand_func1(const char *data, void *context,
+ const char **value_r,
+ const char **error_r ATTR_UNUSED)
+{
+ test_assert(*(int *)context == 0xabcdef);
+ *value_r = t_strdup_printf("<%s>", data);
+ return 1;
+}
+
+static int test_var_expand_func2(const char *data ATTR_UNUSED,
+ void *context ATTR_UNUSED,
+ const char **value_r,
+ const char **error_r ATTR_UNUSED)
+{
+ *value_r = "";
+ return 1;
+}
+
+static int test_var_expand_func3(const char *data ATTR_UNUSED,
+ void *context ATTR_UNUSED,
+ const char **value_r,
+ const char **error_r ATTR_UNUSED)
+{
+ *value_r = NULL;
+ return 1;
+}
+
+static int test_var_expand_func4(const char *data,
+ void *context ATTR_UNUSED,
+ const char **value_r ATTR_UNUSED,
+ const char **error_r)
+{
+ *error_r = t_strdup_printf("Unknown data %s", data == NULL ? "" : data);
+ return 0;
+}
+
+static int test_var_expand_func5(const char *data ATTR_UNUSED,
+ void *context ATTR_UNUSED,
+ const char **value_r ATTR_UNUSED,
+ const char **error_r)
+{
+ *error_r = "Internal error";
+ return -1;
+}
+
+static void test_var_expand_with_funcs(void)
+{
+ static const struct var_expand_test tests[] = {
+ { "%{func1}", "<>", 1 },
+ { "%{func1:foo}", "<foo>", 1 },
+ { "%{func2}", "", 1 },
+ { "%{func3}", "", 1 },
+ { "%{func4}", "", 0 },
+ { "%{func5}", "", -1 },
+ { "%{func4}%{func5}", "", -1 },
+ { "%{func5}%{func4}%{func3}", "", -1 },
+ };
+ static const struct var_expand_table table[] = {
+ { '\0', NULL, NULL }
+ };
+ static const struct var_expand_func_table func_table[] = {
+ { "func1", test_var_expand_func1 },
+ { "func2", test_var_expand_func2 },
+ { "func3", test_var_expand_func3 },
+ { "func4", test_var_expand_func4 },
+ { "func5", test_var_expand_func5 },
+ { NULL, NULL }
+ };
+ string_t *str = t_str_new(128);
+ const char *error;
+ unsigned int i;
+ int ctx = 0xabcdef;
+
+ test_begin("var_expand_with_funcs");
+ for (i = 0; i < N_ELEMENTS(tests); i++) {
+ str_truncate(str, 0);
+ test_assert_idx(var_expand_with_funcs(str, tests[i].in, table, func_table, &ctx, &error) == tests[i].ret, i);
+ test_assert_idx(strcmp(tests[i].out, str_c(str)) == 0, i);
+ }
+ test_end();
+}
+
+static void test_var_get_key(void)
+{
+ static const struct {
+ const char *str;
+ char key;
+ } tests[] = {
+ { "x", 'x' },
+ { "2.5Mx", 'x' },
+ { "200MDx", 'x' },
+ { "200MD{foo}", '{' },
+ { "{foo}", '{' },
+ { "", '\0' },
+ };
+
+ test_begin("var_get_key");
+ for (unsigned int i = 0; i < N_ELEMENTS(tests); i++)
+ test_assert_idx(var_get_key(tests[i].str) == tests[i].key, i);
+ test_end();
+}
+
+static void test_var_has_key(void)
+{
+ static const struct {
+ const char *str;
+ char key;
+ const char *long_key;
+ bool result;
+ } tests[] = {
+ { "%x%y", 'x', NULL, TRUE },
+ { "%x%y", 'y', NULL, TRUE },
+ { "%x%y", 'z', NULL, FALSE },
+ { "%{foo}", 'f', NULL, FALSE },
+ { "%{foo}", 'o', NULL, FALSE },
+ { "%{foo}", '\0', "foo", TRUE },
+ { "%{foo}", 'o', "foo", TRUE },
+ { "%2.5Mx%y", 'x', NULL, TRUE },
+ { "%2.5M{foo}", '\0', "foo", TRUE },
+ };
+
+ test_begin("var_has_key");
+ for (unsigned int i = 0; i < N_ELEMENTS(tests); i++)
+ test_assert_idx(var_has_key(tests[i].str, tests[i].key, tests[i].long_key) == tests[i].result, i);
+ test_end();
+}
+
+static int test_var_expand_hashing_func1(const char *data,
+ void *context ATTR_UNUSED,
+ const char **value_r,
+ const char **error_r ATTR_UNUSED)
+{
+ *value_r = data;
+ return 1;
+}
+
+static int test_var_expand_bad_func(struct var_expand_context *ctx ATTR_UNUSED,
+ const char *key,
+ const char *field ATTR_UNUSED,
+ const char **result_r ATTR_UNUSED,
+ const char **error_r)
+{
+ if (strcmp(key, "notfound") == 0) return 0;
+ *error_r = "Bad parameters";
+ return -1;
+}
+
+static const struct var_expand_extension_func_table test_extension_funcs[] = {
+ { "notfound", test_var_expand_bad_func },
+ { "badparam", test_var_expand_bad_func },
+ { NULL, NULL }
+};
+
+static void test_var_expand_extensions(void)
+{
+ const char *error;
+ test_begin("var_expand_extensions");
+
+ var_expand_register_func_array(test_extension_funcs);
+
+ static const struct var_expand_table table[] = {
+ {'\0', "example", "value" },
+ {'\0', "other-example", "other-value" },
+ {'\0', NULL, NULL}
+ };
+
+ static const struct {
+ const char *in;
+ const char *out;
+ } tests[] = {
+ { "md5: %M{value} %{md5:value}", "md5: 1a79a4d60de6718e8e5b326e338ae533 1a79a4d60de6718e8e5b326e338ae533" },
+ { "sha1: %{sha1:value}", "sha1: c3499c2729730a7f807efb8676a92dcb6f8a3f8f" },
+ { "sha1: %{sha1:func1:example}", "sha1: c3499c2729730a7f807efb8676a92dcb6f8a3f8f" },
+ { "truncate: %{sha1;truncate=12:value}", "truncate: 0c34" },
+ { "truncate: %{sha1;truncate=16:value}", "truncate: c349" },
+ { "rounds,salt: %{sha1;rounds=1000,salt=seawater:value}", "rounds,salt: b515c85884f6b82dc7588279f3643a73e55d2289" },
+ { "rounds,salt,expand: %{sha1;rounds=1000,salt=%{other-value}:value} %{other-value}", "rounds,salt,expand: 49a598ee110af615e175f2e4511cc5d7ccff96ab other-example" },
+ { "format: %4.8{sha1:value}", "format: 9c272973" },
+ { "base64: %{sha1;format=base64:value}", "base64: w0mcJylzCn+AfvuGdqkty2+KP48=" },
+ };
+
+ static const struct var_expand_func_table func_table[] = {
+ { "func1", test_var_expand_hashing_func1 },
+ { NULL, NULL }
+ };
+
+ string_t *str = t_str_new(128);
+
+ for (unsigned int i = 0; i < N_ELEMENTS(tests); i++) {
+ str_truncate(str, 0);
+ error = NULL;
+ test_assert(var_expand_with_funcs(str, tests[i].in, table,
+ func_table, NULL, &error) == 1);
+ test_assert_idx(strcmp(str_c(str), tests[i].out) == 0, i);
+ if (error != NULL) {
+ i_debug("Error: %s", error);
+ }
+ }
+
+ test_assert(var_expand_with_funcs(str, "notfound: %{notfound:field}",
+ table, func_table, NULL, &error) == 0);
+ error = NULL;
+ test_assert(var_expand_with_funcs(str, "notfound: %{badparam:field}",
+ table, func_table, NULL, &error) == -1);
+ test_assert(error != NULL);
+
+ var_expand_unregister_func_array(test_extension_funcs);
+
+ test_end();
+}
+
+static void test_var_expand_if(void)
+{
+ static const struct var_expand_table table[] = {
+ { 'a', "alpha", "alpha" },
+ { 'b', "beta", "beta" },
+ { 'o', "1", "one" },
+ { 't', "2", "two" },
+ { '\0', ";:", "evil1" },
+ { '\0', ";test;", "evil2" },
+ { '\0', NULL, NULL }
+ };
+ const char *error;
+ string_t *dest = t_str_new(64);
+ test_begin("var_expand_if");
+
+ static const struct var_expand_test tests[] = {
+ /* basic numeric operand test */
+ { "%{if;1;==;1;yes;no}", "yes", 1 },
+ { "%{if;1;==;2;yes;no}", "no", 1 },
+ { "%{if;1;<;1;yes;no}", "no", 1 },
+ { "%{if;1;<;2;yes;no}", "yes", 1 },
+ { "%{if;1;<=;1;yes;no}", "yes", 1 },
+ { "%{if;1;<=;2;yes;no}", "yes", 1 },
+ { "%{if;1;>;1;yes;no}", "no", 1 },
+ { "%{if;1;>;2;yes;no}", "no", 1 },
+ { "%{if;1;>=;1;yes;no}", "yes", 1 },
+ { "%{if;1;>=;2;yes;no}", "no", 1 },
+ { "%{if;1;!=;1;yes;no}", "no", 1 },
+ { "%{if;1;!=;2;yes;no}", "yes", 1 },
+ /* basic string operand test */
+ { "%{if;a;eq;a;yes;no}", "yes", 1 },
+ { "%{if;a;eq;b;yes;no}", "no", 1 },
+ { "%{if;a;lt;a;yes;no}", "no", 1 },
+ { "%{if;a;lt;b;yes;no}", "yes", 1 },
+ { "%{if;a;le;a;yes;no}", "yes", 1 },
+ { "%{if;a;le;b;yes;no}", "yes", 1 },
+ { "%{if;a;gt;a;yes;no}", "no", 1 },
+ { "%{if;a;gt;b;yes;no}", "no", 1 },
+ { "%{if;a;ge;a;yes;no}", "yes", 1 },
+ { "%{if;a;ge;b;yes;no}", "no", 1 },
+ { "%{if;a;ne;a;yes;no}", "no", 1 },
+ { "%{if;a;ne;b;yes;no}", "yes", 1 },
+ { "%{if;a;*;a;yes;no}", "yes", 1 },
+ { "%{if;a;*;b;yes;no}", "no", 1 },
+ { "%{if;a;*;*a*;yes;no}", "yes", 1 },
+ { "%{if;a;*;*b*;yes;no}", "no", 1 },
+ { "%{if;a;*;*;yes;no}", "yes", 1 },
+ { "%{if;a;!*;a;yes;no}", "no", 1 },
+ { "%{if;a;!*;b;yes;no}", "yes", 1 },
+ { "%{if;a;!*;*a*;yes;no}", "no", 1 },
+ { "%{if;a;!*;*b*;yes;no}", "yes", 1 },
+ { "%{if;a;!*;*;yes;no}", "no", 1 },
+ { "%{if;a;~;a;yes;no}", "yes", 1 },
+ { "%{if;a;~;b;yes;no}", "no", 1 },
+ { "%{if;a;~;.*a.*;yes;no}", "yes", 1 },
+ { "%{if;a;~;.*b.*;yes;no}", "no", 1 },
+ { "%{if;a;~;.*;yes;no}", "yes", 1 },
+ { "%{if;a;!~;a;yes;no}", "no", 1 },
+ { "%{if;a;!~;b;yes;no}", "yes", 1 },
+ { "%{if;a;!~;.*a.*;yes;no}", "no", 1 },
+ { "%{if;a;!~;.*b.*;yes;no}", "yes", 1 },
+ { "%{if;a;!~;.*;yes;no}", "no", 1 },
+ { "%{if;this is test;~;^test;yes;no}", "no", 1 },
+ { "%{if;this is test;~;.*test;yes;no}", "yes", 1 },
+ /* variable expansion */
+ { "%{if;%a;eq;%a;yes;no}", "yes", 1 },
+ { "%{if;%a;eq;%b;yes;no}", "no", 1 },
+ { "%{if;%{alpha};eq;%{alpha};yes;no}", "yes", 1 },
+ { "%{if;%{alpha};eq;%{beta};yes;no}", "no", 1 },
+ { "%{if;%o;eq;%o;yes;no}", "yes", 1 },
+ { "%{if;%o;eq;%t;yes;no}", "no", 1 },
+ { "%{if;%{one};eq;%{one};yes;no}", "yes", 1 },
+ { "%{if;%{one};eq;%{two};yes;no}", "no", 1 },
+ { "%{if;%{one};eq;%{one};%{one};%{two}}", "1", 1 },
+ { "%{if;%{one};gt;%{two};%{one};%{two}}", "2", 1 },
+ { "%{if;%{evil1};eq;\\;\\:;%{evil2};no}", ";test;", 1 },
+ /* inner if */
+ { "%{if;%{if;%{one};eq;1;1;0};eq;%{if;%{two};eq;2;2;3};yes;no}", "no", 1 },
+ /* no false */
+ { "%{if;1;==;1;yes}", "yes", 1 },
+ { "%{if;1;==;2;yes}", "", 1 },
+ /* invalid input */
+ { "%{if;}", "", -1 },
+ { "%{if;1;}", "", -1 },
+ { "%{if;1;==;}", "", -1 },
+ { "%{if;1;==;2;}", "", -1 },
+ { "%{if;1;fu;2;yes;no}", "", -1 },
+ /* missing variables */
+ { "%{if;%{missing1};==;%{missing2};yes;no}", "", 0 },
+ };
+
+ for(size_t i = 0; i < N_ELEMENTS(tests); i++) {
+ int ret;
+ error = NULL;
+ str_truncate(dest, 0);
+ ret = var_expand(dest, tests[i].in, table, &error);
+ test_assert_idx(tests[i].ret == ret, i);
+ test_assert_idx(strcmp(tests[i].out, str_c(dest)) == 0, i);
+ }
+
+ test_end();
+}
+
+static void test_var_expand_merge_tables(void)
+{
+ const struct var_expand_table one[] = {
+ { 'a', "1", "alpha" },
+ { '\0', "2", "beta" },
+ { '\0', NULL, NULL }
+ },
+ two[] = {
+ { 't', "3", "theta" },
+ { '\0', "4", "phi" },
+ { '\0', NULL, NULL }
+ },
+ *merged = NULL;
+
+
+ test_begin("var_expand_merge_tables");
+
+ merged = t_var_expand_merge_tables(one, two);
+
+ test_assert(var_expand_table_size(merged) == 4);
+ for(unsigned int i = 0; i < var_expand_table_size(merged); i++) {
+ if (i < 2) {
+ test_assert_idx(merged[i].key == one[i].key, i);
+ test_assert_idx(merged[i].value == one[i].value || strcmp(merged[i].value, one[i].value) == 0, i);
+ test_assert_idx(merged[i].long_key == one[i].long_key || strcmp(merged[i].long_key, one[i].long_key) == 0, i);
+ } else if (i < 4) {
+ test_assert_idx(merged[i].key == two[i-2].key, i);
+ test_assert_idx(merged[i].value == two[i-2].value || strcmp(merged[i].value, two[i-2].value) == 0, i);
+ test_assert_idx(merged[i].long_key == two[i-2].long_key || strcmp(merged[i].long_key, two[i-2].long_key) == 0, i);
+ } else {
+ break;
+ }
+ }
+ test_end();
+}
+
+void test_var_expand(void)
+{
+ test_var_expand_ranges();
+ test_var_expand_builtin();
+ test_var_get_key_range();
+ test_var_expand_with_funcs();
+ test_var_get_key();
+ test_var_has_key();
+ test_var_expand_extensions();
+ test_var_expand_if();
+ test_var_expand_merge_tables();
+}
diff --git a/src/lib/test-wildcard-match.c b/src/lib/test-wildcard-match.c
new file mode 100644
index 0000000..7a459da
--- /dev/null
+++ b/src/lib/test-wildcard-match.c
@@ -0,0 +1,48 @@
+/* Copyright (c) 2014-2018 Dovecot authors, see the included COPYING file */
+
+#include "test-lib.h"
+#include "wildcard-match.h"
+
+static const struct {
+ const char *data;
+ const char *mask;
+ bool result;
+} tests[] = {
+ { "foo", "*", TRUE },
+ { "foo", "*foo*", TRUE },
+ { "foo", "foo", TRUE },
+ { "foo", "f*o*o", TRUE },
+ { "foo", "f??", TRUE },
+ { "foo", "f?o", TRUE },
+ { "foo", "*??", TRUE },
+ { "foo", "???", TRUE },
+ { "foo", "f??*", TRUE },
+ { "foo", "???*", TRUE },
+
+ { "foo", "", FALSE },
+ { "foo", "f", FALSE },
+ { "foo", "fo", FALSE },
+ { "foo", "fooo", FALSE },
+ { "foo", "????", FALSE },
+ { "foo", "f*o*o*o", FALSE },
+ { "foo", "f???*", FALSE },
+
+ { "*foo", "foo", FALSE },
+ { "foo*", "foo", FALSE },
+ { "*foo*", "foo", FALSE },
+
+ { "", "*", TRUE },
+ { "", "", TRUE },
+ { "", "?", FALSE }
+};
+
+void test_wildcard_match(void)
+{
+ unsigned int i;
+
+ test_begin("wildcard_match()");
+ for (i = 0; i < N_ELEMENTS(tests); i++) {
+ test_assert_idx(wildcard_match(tests[i].data, tests[i].mask) == tests[i].result, i);
+ }
+ test_end();
+}
diff --git a/src/lib/time-util.c b/src/lib/time-util.c
new file mode 100644
index 0000000..3f4cd01
--- /dev/null
+++ b/src/lib/time-util.c
@@ -0,0 +1,169 @@
+/* Copyright (c) 2008-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "time-util.h"
+
+#include <time.h>
+
+#define STRFTIME_MAX_BUFSIZE (1024*64)
+
+void i_gettimeofday(struct timeval *tv_r)
+{
+ if (gettimeofday(tv_r, NULL) < 0)
+ i_fatal("gettimeofday() failed: %m");
+}
+
+uint64_t i_nanoseconds(void)
+{
+ struct timespec ts;
+
+ if (clock_gettime(CLOCK_REALTIME, &ts) < 0)
+ i_fatal("clock_gettime() failed: %m");
+ return ts.tv_sec * 1000000000ULL + ts.tv_nsec;
+}
+
+int timeval_cmp(const struct timeval *tv1, const struct timeval *tv2)
+{
+ if (tv1->tv_sec < tv2->tv_sec)
+ return -1;
+ if (tv1->tv_sec > tv2->tv_sec)
+ return 1;
+ if (tv1->tv_usec < tv2->tv_usec)
+ return -1;
+ if (tv1->tv_usec > tv2->tv_usec)
+ return 1;
+ return 0;
+}
+
+int timeval_cmp_margin(const struct timeval *tv1, const struct timeval *tv2,
+ unsigned int usec_margin)
+{
+ long long secs_diff, usecs_diff;
+ int sec_margin, ret;
+
+ if (tv1->tv_sec < tv2->tv_sec) {
+ sec_margin = ((int)usec_margin / 1000000) + 1;
+ secs_diff = (long long)tv2->tv_sec - (long long)tv1->tv_sec;
+ if (secs_diff > sec_margin)
+ return -1;
+ usecs_diff = secs_diff * 1000000LL +
+ (tv2->tv_usec - tv1->tv_usec);
+ ret = -1;
+ } else if (tv1->tv_sec > tv2->tv_sec) {
+ sec_margin = ((int)usec_margin / 1000000) + 1;
+ secs_diff = (long long)tv1->tv_sec - (long long)tv2->tv_sec;
+ if (secs_diff > sec_margin)
+ return 1;
+ usecs_diff = secs_diff * 1000000LL +
+ (tv1->tv_usec - tv2->tv_usec);
+ ret = 1;
+ } else if (tv1->tv_usec < tv2->tv_usec) {
+ usecs_diff = tv2->tv_usec - tv1->tv_usec;
+ ret = -1;
+ } else {
+ usecs_diff = tv1->tv_usec - tv2->tv_usec;
+ ret = 1;
+ }
+ i_assert(usecs_diff >= 0);
+ return (unsigned long long)usecs_diff > usec_margin ? ret : 0;
+}
+
+int timeval_diff_msecs(const struct timeval *tv1, const struct timeval *tv2)
+{
+ long long diff = timeval_diff_usecs(tv1, tv2) / 1000LL;
+#ifdef DEBUG
+ /* FIXME v2.4: Remove the ifdef */
+ i_assert(diff <= INT_MAX);
+#endif
+ return (int)diff;
+}
+
+long long timeval_diff_usecs(const struct timeval *tv1,
+ const struct timeval *tv2)
+{
+ time_t secs;
+ int usecs;
+
+ secs = tv1->tv_sec - tv2->tv_sec;
+ usecs = tv1->tv_usec - tv2->tv_usec;
+ if (usecs < 0) {
+ secs--;
+ usecs += 1000000;
+ }
+ return ((long long)secs * 1000000LL) + usecs;
+}
+
+time_t time_to_local_day_start(time_t t)
+{
+ const struct tm *day_tm;
+ struct tm tm;
+ time_t new_start_time;
+
+ day_tm = localtime(&t);
+ i_zero(&tm);
+ tm.tm_year = day_tm->tm_year;
+ tm.tm_mon = day_tm->tm_mon;
+ tm.tm_mday = day_tm->tm_mday;
+ tm.tm_isdst = -1;
+ new_start_time = mktime(&tm);
+ i_assert(new_start_time != (time_t)-1);
+ return new_start_time;
+}
+
+static const char *strftime_real(const char *fmt, const struct tm *tm)
+{
+ size_t bufsize = strlen(fmt) + 32;
+ char *buf = t_buffer_get(bufsize);
+ size_t ret;
+
+ while ((ret = strftime(buf, bufsize, fmt, tm)) == 0) {
+ bufsize *= 2;
+ i_assert(bufsize <= STRFTIME_MAX_BUFSIZE);
+ buf = t_buffer_get(bufsize);
+ }
+ t_buffer_alloc(ret + 1);
+ return buf;
+}
+
+const char *t_strftime(const char *fmt, const struct tm *tm)
+{
+ return strftime_real(fmt, tm);
+}
+
+const char *t_strflocaltime(const char *fmt, time_t t)
+{
+ return strftime_real(fmt, localtime(&t));
+}
+
+const char *t_strfgmtime(const char *fmt, time_t t)
+{
+ return strftime_real(fmt, gmtime(&t));
+}
+
+int str_to_timeval(const char *str, struct timeval *tv_r)
+{
+ tv_r->tv_usec = 0;
+
+ const char *p = strchr(str, '.');
+ if (p == NULL)
+ return str_to_time(str, &tv_r->tv_sec);
+
+ int ret;
+ T_BEGIN {
+ ret = str_to_time(t_strdup_until(str, p++), &tv_r->tv_sec);
+ } T_END;
+ if (ret < 0 || p[0] == '\0')
+ return -1;
+
+ unsigned int len = strlen(p);
+ if (len > 6)
+ return -1; /* we don't support sub-microseconds */
+ char usecs_str[7] = "000000";
+ memcpy(usecs_str, p, len);
+
+ unsigned int usec;
+ if (str_to_uint(usecs_str, &usec) < 0)
+ return -1;
+ tv_r->tv_usec = usec;
+ return 0;
+}
diff --git a/src/lib/time-util.h b/src/lib/time-util.h
new file mode 100644
index 0000000..0cda92c
--- /dev/null
+++ b/src/lib/time-util.h
@@ -0,0 +1,108 @@
+#ifndef TIME_UTIL_H
+#define TIME_UTIL_H
+
+#include <sys/time.h> /* for struct timeval */
+
+/* Same as gettimeofday(), but call i_fatal() if the call fails. */
+void i_gettimeofday(struct timeval *tv_r);
+/* Return nanoseconds since UNIX epoch (1970-01-01). */
+uint64_t i_nanoseconds(void);
+/* Return microseconds since UNIX epoch (1970-01-01). */
+static inline uint64_t i_microseconds(void) {
+ return i_nanoseconds() / 1000;
+}
+
+/* Returns -1 if tv1<tv2, 1 if tv1>tv2, 0 if they're equal. */
+int timeval_cmp(const struct timeval *tv1, const struct timeval *tv2);
+/* Same as timeval_cmp, but tv->usecs must differ by at least usec_margin */
+int timeval_cmp_margin(const struct timeval *tv1, const struct timeval *tv2,
+ unsigned int usec_margin);
+/* Returns tv1-tv2 in milliseconds. */
+int timeval_diff_msecs(const struct timeval *tv1, const struct timeval *tv2);
+/* Returns tv1-tv2 in microseconds. */
+long long timeval_diff_usecs(const struct timeval *tv1,
+ const struct timeval *tv2);
+
+static inline void
+timeval_add_usecs(struct timeval *tv, long long usecs)
+{
+ i_assert(usecs >= 0);
+ tv->tv_sec += usecs / 1000000;
+ tv->tv_usec += (usecs % 1000000);
+ if (tv->tv_usec >= 1000000) {
+ tv->tv_sec++;
+ tv->tv_usec -= 1000000;
+ }
+}
+
+static inline void
+timeval_sub_usecs(struct timeval *tv, long long usecs)
+{
+ i_assert(usecs >= 0);
+ tv->tv_sec -= usecs / 1000000;
+ tv->tv_usec -= (usecs % 1000000);
+ if (tv->tv_usec < 0) {
+ tv->tv_sec--;
+ tv->tv_usec += 1000000;
+ }
+}
+
+static inline void
+timeval_add_msecs(struct timeval *tv, unsigned int msecs)
+{
+ tv->tv_sec += msecs / 1000;
+ tv->tv_usec += (msecs % 1000) * 1000;
+ if (tv->tv_usec >= 1000000) {
+ tv->tv_sec++;
+ tv->tv_usec -= 1000000;
+ }
+}
+
+static inline void
+timeval_sub_msecs(struct timeval *tv, unsigned int msecs)
+{
+ tv->tv_sec -= msecs / 1000;
+ tv->tv_usec -= (msecs % 1000) * 1000;
+ if (tv->tv_usec < 0) {
+ tv->tv_sec--;
+ tv->tv_usec += 1000000;
+ }
+}
+
+static inline unsigned long long timeval_to_usecs(const struct timeval *tv)
+{
+ return (tv->tv_sec * 1000000ULL + tv->tv_usec);
+}
+
+static inline void timeval_add(struct timeval *tv, const struct timeval *val)
+{
+ i_assert(val->tv_usec < 1000000);
+ tv->tv_sec += val->tv_sec;
+ tv->tv_usec += val->tv_usec;
+ if (tv->tv_usec >= 1000000) {
+ tv->tv_sec++;
+ tv->tv_usec -= 1000000;
+ }
+}
+
+static inline time_t timeval_round(struct timeval *tv)
+{
+ return (tv->tv_usec < 500000 ? tv->tv_sec : tv->tv_sec + 1);
+}
+
+/* Convert t to local time and return timestamp on that day at 00:00:00. */
+time_t time_to_local_day_start(time_t t);
+
+/* Wrappers to strftime() */
+const char *t_strftime(const char *fmt, const struct tm *tm) ATTR_STRFTIME(1);
+const char *t_strflocaltime(const char *fmt, time_t t) ATTR_STRFTIME(1);
+const char *t_strfgmtime(const char *fmt, time_t t) ATTR_STRFTIME(1);
+
+/* Parse string as <unix timestamp>[.<usecs>] into timeval. <usecs> must not
+ have higher precision time, i.e. a maximum of 6 digits is allowed. Note that
+ ".1" is handled as ".1000000" so the string should have been written using
+ "%06u" printf format. */
+int str_to_timeval(const char *str, struct timeval *tv_r)
+ ATTR_WARN_UNUSED_RESULT;
+
+#endif
diff --git a/src/lib/unichar.c b/src/lib/unichar.c
new file mode 100644
index 0000000..7036e73
--- /dev/null
+++ b/src/lib/unichar.c
@@ -0,0 +1,447 @@
+/* Copyright (c) 2005-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "bsearch-insert-pos.h"
+#include "unichar.h"
+
+#include "unicodemap.c"
+
+#define HANGUL_FIRST 0xac00
+#define HANGUL_LAST 0xd7a3
+
+const unsigned char utf8_replacement_char[UTF8_REPLACEMENT_CHAR_LEN] =
+ { 0xef, 0xbf, 0xbd }; /* 0xfffd */
+
+static const uint8_t utf8_non1_bytes[256 - 192 - 2] = {
+ 2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,
+ 3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,4,4,4,4,4,4,4,4,5,5,5,5,6,6,1,1
+};
+
+const uint8_t *const uni_utf8_non1_bytes = utf8_non1_bytes;
+
+unsigned int uni_strlen(const unichar_t *str)
+{
+ unsigned int len = 0;
+
+ for (len = 0; str[len] != 0; len++) ;
+
+ return len;
+}
+
+int uni_utf8_get_char(const char *input, unichar_t *chr_r)
+{
+ return uni_utf8_get_char_n((const unsigned char *)input, SIZE_MAX,
+ chr_r);
+}
+
+int uni_utf8_get_char_n(const void *_input, size_t max_len, unichar_t *chr_r)
+{
+ static unichar_t lowest_valid_chr_table[] =
+ { 0, 0, 0x80, 0x800, 0x10000, 0x200000, 0x4000000 };
+ const unsigned char *input = _input;
+ unichar_t chr, lowest_valid_chr;
+ unsigned int i, len;
+ int ret;
+
+ i_assert(max_len > 0);
+
+ if (*input < 0x80) {
+ *chr_r = *input;
+ return 1;
+ }
+
+ /* first byte has len highest bits set, followed by zero bit.
+ the rest of the bits are used as the highest bits of the value. */
+ chr = *input;
+ len = uni_utf8_char_bytes(*input);
+ switch (len) {
+ case 2:
+ chr &= 0x1f;
+ break;
+ case 3:
+ chr &= 0x0f;
+ break;
+ case 4:
+ chr &= 0x07;
+ break;
+ case 5:
+ chr &= 0x03;
+ break;
+ case 6:
+ chr &= 0x01;
+ break;
+ default:
+ /* only 7bit chars should have len==1 */
+ i_assert(len == 1);
+ return -1;
+ }
+
+ if (len <= max_len) {
+ lowest_valid_chr = lowest_valid_chr_table[len];
+ ret = len;
+ } else {
+ /* check first if the input is invalid before returning 0 */
+ lowest_valid_chr = 0;
+ ret = 0;
+ len = max_len;
+ }
+
+ /* the following bytes must all be 10xxxxxx */
+ for (i = 1; i < len; i++) {
+ if ((input[i] & 0xc0) != 0x80)
+ return input[i] == '\0' ? 0 : -1;
+
+ chr <<= 6;
+ chr |= input[i] & 0x3f;
+ }
+ /* these are specified as invalid encodings by standards
+ see RFC3629 */
+ if (!uni_is_valid_ucs4(chr))
+ return -1;
+ if (chr < lowest_valid_chr) {
+ /* overlong encoding */
+ return -1;
+ }
+
+ *chr_r = chr;
+ return ret;
+}
+
+int uni_utf8_to_ucs4(const char *input, ARRAY_TYPE(unichars) *output)
+{
+ unichar_t chr;
+
+ while (*input != '\0') {
+ int len = uni_utf8_get_char(input, &chr);
+ if (len <= 0) {
+ /* invalid input */
+ return -1;
+ }
+ input += len;
+
+ array_push_back(output, &chr);
+ }
+ return 0;
+}
+
+int uni_utf8_to_ucs4_n(const unsigned char *input, size_t size,
+ ARRAY_TYPE(unichars) *output)
+{
+ unichar_t chr;
+
+ while (size > 0) {
+ int len = uni_utf8_get_char_n(input, size, &chr);
+ if (len <= 0)
+ return -1; /* invalid input */
+ input += len; size -= len;
+
+ array_push_back(output, &chr);
+ }
+ return 0;
+}
+
+void uni_ucs4_to_utf8(const unichar_t *input, size_t len, buffer_t *output)
+{
+ for (; len > 0 && *input != '\0'; input++, len--)
+ uni_ucs4_to_utf8_c(*input, output);
+}
+
+void uni_ucs4_to_utf8_c(unichar_t chr, buffer_t *output)
+{
+ unsigned char first;
+ int bitpos;
+
+ if (chr < 0x80) {
+ buffer_append_c(output, chr);
+ return;
+ }
+
+ i_assert(uni_is_valid_ucs4(chr));
+
+ if (chr < (1 << (6 + 5))) {
+ /* 110xxxxx */
+ bitpos = 6;
+ first = 0x80 | 0x40;
+ } else if (chr < (1 << ((2*6) + 4))) {
+ /* 1110xxxx */
+ bitpos = 2*6;
+ first = 0x80 | 0x40 | 0x20;
+ } else if (chr < (1 << ((3*6) + 3))) {
+ /* 11110xxx */
+ bitpos = 3*6;
+ first = 0x80 | 0x40 | 0x20 | 0x10;
+ } else if (chr < (1 << ((4*6) + 2))) {
+ /* 111110xx */
+ bitpos = 4*6;
+ first = 0x80 | 0x40 | 0x20 | 0x10 | 0x08;
+ } else {
+ /* 1111110x */
+ bitpos = 5*6;
+ first = 0x80 | 0x40 | 0x20 | 0x10 | 0x08 | 0x04;
+ }
+ buffer_append_c(output, first | (chr >> bitpos));
+
+ do {
+ bitpos -= 6;
+ buffer_append_c(output, 0x80 | ((chr >> bitpos) & 0x3f));
+ } while (bitpos > 0);
+}
+
+unsigned int uni_utf8_strlen(const char *input)
+{
+ return uni_utf8_strlen_n(input, strlen(input));
+}
+
+unsigned int uni_utf8_strlen_n(const void *input, size_t size)
+{
+ size_t partial_pos;
+
+ return uni_utf8_partial_strlen_n(input, size, &partial_pos);
+}
+
+unsigned int uni_utf8_partial_strlen_n(const void *_input, size_t size,
+ size_t *partial_pos_r)
+{
+ const unsigned char *input = _input;
+ unsigned int count, len = 0;
+ size_t i;
+
+ for (i = 0; i < size; ) {
+ count = uni_utf8_char_bytes(input[i]);
+ if (i + count > size)
+ break;
+ i += count;
+ len++;
+ }
+ *partial_pos_r = i;
+ return len;
+}
+
+static bool uint16_find(const uint16_t *data, unsigned int count,
+ uint16_t value, unsigned int *idx_r)
+{
+ BINARY_NUMBER_SEARCH(data, count, value, idx_r);
+}
+
+static bool uint32_find(const uint32_t *data, unsigned int count,
+ uint32_t value, unsigned int *idx_r)
+{
+ BINARY_NUMBER_SEARCH(data, count, value, idx_r);
+}
+
+unichar_t uni_ucs4_to_titlecase(unichar_t chr)
+{
+ unsigned int idx;
+
+ if (chr <= 0xff)
+ return titlecase8_map[chr];
+ else if (chr <= 0xffff) {
+ if (!uint16_find(titlecase16_keys, N_ELEMENTS(titlecase16_keys),
+ chr, &idx))
+ return chr;
+ else
+ return titlecase16_values[idx];
+ } else {
+ if (!uint32_find(titlecase32_keys, N_ELEMENTS(titlecase32_keys),
+ chr, &idx))
+ return chr;
+ else
+ return titlecase32_values[idx];
+ }
+}
+
+static bool uni_ucs4_decompose_uni(unichar_t *chr)
+{
+ unsigned int idx;
+
+ if (*chr <= 0xff) {
+ if (uni8_decomp_map[*chr] == *chr)
+ return FALSE;
+ *chr = uni8_decomp_map[*chr];
+ } else if (*chr <= 0xffff) {
+ if (*chr < uni16_decomp_keys[0])
+ return FALSE;
+
+ if (!uint16_find(uni16_decomp_keys,
+ N_ELEMENTS(uni16_decomp_keys), *chr, &idx))
+ return FALSE;
+ *chr = uni16_decomp_values[idx];
+ } else {
+ if (!uint32_find(uni32_decomp_keys,
+ N_ELEMENTS(uni32_decomp_keys), *chr, &idx))
+ return FALSE;
+ *chr = uni32_decomp_values[idx];
+ }
+ return TRUE;
+}
+
+static void uni_ucs4_decompose_hangul_utf8(unichar_t chr, buffer_t *output)
+{
+#define SBase HANGUL_FIRST
+#define LBase 0x1100
+#define VBase 0x1161
+#define TBase 0x11A7
+#define LCount 19
+#define VCount 21
+#define TCount 28
+#define NCount (VCount * TCount)
+ unsigned int SIndex = chr - SBase;
+ unichar_t L = LBase + SIndex / NCount;
+ unichar_t V = VBase + (SIndex % NCount) / TCount;
+ unichar_t T = TBase + SIndex % TCount;
+
+ uni_ucs4_to_utf8_c(L, output);
+ uni_ucs4_to_utf8_c(V, output);
+ if (T != TBase) uni_ucs4_to_utf8_c(T, output);
+}
+
+static bool uni_ucs4_decompose_multi_utf8(unichar_t chr, buffer_t *output)
+{
+ const uint32_t *value;
+ unsigned int idx;
+
+ if (chr < multidecomp_keys[0] || chr > 0xffff)
+ return FALSE;
+
+ if (!uint32_find(multidecomp_keys, N_ELEMENTS(multidecomp_keys),
+ chr, &idx))
+ return FALSE;
+
+ value = &multidecomp_values[multidecomp_offsets[idx]];
+ for (; *value != 0; value++)
+ uni_ucs4_to_utf8_c(*value, output);
+ return TRUE;
+}
+
+static void output_add_replacement_char(buffer_t *output)
+{
+ if (output->used >= UTF8_REPLACEMENT_CHAR_LEN &&
+ memcmp(CONST_PTR_OFFSET(output->data,
+ output->used - UTF8_REPLACEMENT_CHAR_LEN),
+ utf8_replacement_char, UTF8_REPLACEMENT_CHAR_LEN) == 0) {
+ /* don't add the replacement char multiple times */
+ return;
+ }
+ buffer_append(output, utf8_replacement_char, UTF8_REPLACEMENT_CHAR_LEN);
+}
+
+int uni_utf8_to_decomposed_titlecase(const void *_input, size_t size,
+ buffer_t *output)
+{
+ const unsigned char *input = _input;
+ unichar_t chr;
+ int ret = 0;
+
+ while (size > 0) {
+ int bytes = uni_utf8_get_char_n(input, size, &chr);
+ if (bytes <= 0) {
+ /* invalid input. try the next byte. */
+ ret = -1;
+ input++; size--;
+ output_add_replacement_char(output);
+ continue;
+ }
+ input += bytes;
+ size -= bytes;
+
+ chr = uni_ucs4_to_titlecase(chr);
+ if (chr >= HANGUL_FIRST && chr <= HANGUL_LAST)
+ uni_ucs4_decompose_hangul_utf8(chr, output);
+ else if (uni_ucs4_decompose_uni(&chr) ||
+ !uni_ucs4_decompose_multi_utf8(chr, output))
+ uni_ucs4_to_utf8_c(chr, output);
+ }
+ return ret;
+}
+
+static inline unsigned int
+is_valid_utf8_seq(const unsigned char *input, unsigned int size)
+{
+ unichar_t chr;
+ int len = uni_utf8_get_char_n(input, size, &chr);
+ return len <= 0 ? 0 : len;
+}
+
+static int uni_utf8_find_invalid_pos(const unsigned char *input, size_t size,
+ size_t *pos_r)
+{
+ size_t i, len;
+
+ /* find the first invalid utf8 sequence */
+ for (i = 0; i < size;) {
+ if (input[i] < 0x80)
+ i++;
+ else {
+ len = is_valid_utf8_seq(input + i, size-i);
+ if (unlikely(len == 0)) {
+ *pos_r = i;
+ return -1;
+ }
+ i += len;
+ }
+ }
+ return 0;
+}
+
+bool uni_utf8_get_valid_data(const unsigned char *input, size_t size,
+ buffer_t *buf)
+{
+ size_t i, len;
+
+ if (uni_utf8_find_invalid_pos(input, size, &i) == 0)
+ return TRUE;
+
+ /* broken utf-8 input - skip the broken characters */
+ buffer_append(buf, input, i++);
+
+ output_add_replacement_char(buf);
+ while (i < size) {
+ if (input[i] < 0x80) {
+ buffer_append_c(buf, input[i++]);
+ continue;
+ }
+
+ len = is_valid_utf8_seq(input + i, size-i);
+ if (len == 0) {
+ i++;
+ output_add_replacement_char(buf);
+ continue;
+ }
+ buffer_append(buf, input + i, len);
+ i += len;
+ }
+ return FALSE;
+}
+
+bool uni_utf8_str_is_valid(const char *str)
+{
+ size_t i;
+
+ return uni_utf8_find_invalid_pos((const unsigned char *)str,
+ strlen(str), &i) == 0;
+}
+
+bool uni_utf8_data_is_valid(const unsigned char *data, size_t size)
+{
+ size_t i;
+
+ return uni_utf8_find_invalid_pos(data, size, &i) == 0;
+}
+
+size_t uni_utf8_data_truncate(const unsigned char *data, size_t old_size,
+ size_t max_new_size)
+{
+ if (max_new_size >= old_size)
+ return old_size;
+ if (max_new_size == 0)
+ return 0;
+
+ if ((data[max_new_size] & 0x80) == 0)
+ return max_new_size;
+ while (max_new_size > 0 && (data[max_new_size-1] & 0xc0) == 0x80)
+ max_new_size--;
+ if (max_new_size > 0 && (data[max_new_size-1] & 0xc0) == 0xc0)
+ max_new_size--;
+ return max_new_size;
+}
diff --git a/src/lib/unichar.h b/src/lib/unichar.h
new file mode 100644
index 0000000..88defcd
--- /dev/null
+++ b/src/lib/unichar.h
@@ -0,0 +1,145 @@
+#ifndef UNICHAR_H
+#define UNICHAR_H
+
+/* Character used to replace invalid input. */
+#define UNICODE_REPLACEMENT_CHAR 0xfffd
+#define UNICODE_REPLACEMENT_CHAR_UTF8 "\xEF\xBF\xBD"
+#define UNICODE_REPLACEMENT_CHAR_UTF8_LEN \
+ (sizeof(UNICODE_REPLACEMENT_CHAR_UTF8) - 1);
+/* Horizontal ellipsis character ('...') */
+#define UNICODE_HORIZONTAL_ELLIPSIS_CHAR 0x2026
+#define UNICODE_HORIZONTAL_ELLIPSIS_CHAR_UTF8 "\xE2\x80\xA6"
+#define UNICODE_HORIZONTAL_ELLIPSIS_CHAR_UTF8_LEN \
+ (sizeof(UNICODE_HORIZONTAL_ELLIPSIS_CHAR_UTF8) - 1);
+
+/* Characters >= base require surrogates */
+#define UTF16_SURROGATE_BASE 0x10000
+
+#define UTF16_SURROGATE_SHIFT 10
+#define UTF16_SURROGATE_MASK 0x03ff
+#define UTF16_SURROGATE_HIGH_FIRST 0xd800
+#define UTF16_SURROGATE_HIGH_LAST 0xdbff
+#define UTF16_SURROGATE_HIGH_MAX 0xdfff
+#define UTF16_SURROGATE_LOW_FIRST 0xdc00
+#define UTF16_SURROGATE_LOW_LAST 0xdfff
+
+#define UTF16_SURROGATE_HIGH(chr) \
+ (UTF16_SURROGATE_HIGH_FIRST + \
+ (((chr) - UTF16_SURROGATE_BASE) >> UTF16_SURROGATE_SHIFT))
+#define UTF16_SURROGATE_LOW(chr) \
+ (UTF16_SURROGATE_LOW_FIRST + \
+ (((chr) - UTF16_SURROGATE_BASE) & UTF16_SURROGATE_MASK))
+
+/* Returns TRUE if given byte is ASCII character or the beginning of a
+ multibyte UTF-8 sequence */
+#define UTF8_IS_START_SEQ(b) \
+ (((b) & 0x80) == 0 || ((b) & 0xC0) == 0xC0)
+
+#define UTF8_REPLACEMENT_CHAR_LEN 3
+
+#define UNICHAR_T_MAX 0x10ffff
+
+#define UTF16_VALID_HIGH_SURROGATE(chr) (((chr) & 0xfffc00) == UTF16_SURROGATE_HIGH_FIRST)
+#define UTF16_VALID_LOW_SURROGATE(chr) (((chr) & 0xfffc00) == UTF16_SURROGATE_LOW_FIRST)
+
+typedef uint32_t unichar_t;
+ARRAY_DEFINE_TYPE(unichars, unichar_t);
+
+/* Normalize UTF8 input and append it to output buffer.
+ Returns 0 if ok, -1 if input was invalid. Even if input was invalid,
+ as much as possible should be added to output. */
+typedef int normalizer_func_t(const void *input, size_t size,
+ buffer_t *output);
+
+extern const unsigned char utf8_replacement_char[UTF8_REPLACEMENT_CHAR_LEN];
+extern const uint8_t *const uni_utf8_non1_bytes;
+
+static inline bool ATTR_PURE uni_is_valid_ucs4(unichar_t chr)
+{
+ return (!UTF16_VALID_HIGH_SURROGATE(chr) &&
+ !UTF16_VALID_LOW_SURROGATE(chr) &&
+ chr <= UNICHAR_T_MAX);
+};
+
+/* Returns number of characters in a NUL-terminated unicode string */
+unsigned int uni_strlen(const unichar_t *str) ATTR_PURE;
+/* Translates UTF-8 input to UCS-4 output. Returns 0 if ok, -1 if input was
+ invalid */
+int uni_utf8_to_ucs4(const char *input, ARRAY_TYPE(unichars) *output);
+int uni_utf8_to_ucs4_n(const unsigned char *input, size_t size,
+ ARRAY_TYPE(unichars) *output);
+/* Translates UCS-4 input to UTF-8 output. */
+void uni_ucs4_to_utf8(const unichar_t *input, size_t len, buffer_t *output);
+void uni_ucs4_to_utf8_c(unichar_t chr, buffer_t *output);
+
+/* Returns char_bytes (>0) if *chr_r is set, 0 for incomplete trailing character,
+ -1 for invalid input. */
+int uni_utf8_get_char(const char *input, unichar_t *chr_r);
+int uni_utf8_get_char_n(const void *input, size_t max_len, unichar_t *chr_r);
+/* Returns number of characters in UTF-8 string. */
+unsigned int uni_utf8_strlen(const char *input) ATTR_PURE;
+/* Returns number of characters in UTF-8 input of specified size. */
+unsigned int uni_utf8_strlen_n(const void *input, size_t size) ATTR_PURE;
+/* Same as uni_utf8_strlen_n(), but if input ends with a partial UTF-8
+ character, don't include it in the return value and set partial_pos_r to
+ where the character begins. Otherwise partial_pos_r is set to the end
+ of the input. */
+unsigned int uni_utf8_partial_strlen_n(const void *input, size_t size,
+ size_t *partial_pos_r);
+
+/* Returns the number of bytes belonging to this UTF-8 character. The given
+ parameter is the first byte of the UTF-8 sequence. Invalid input is
+ returned with length 1. */
+static inline unsigned int ATTR_CONST
+uni_utf8_char_bytes(unsigned char chr)
+{
+ /* 0x00 .. 0x7f are ASCII. 0x80 .. 0xC1 are invalid. */
+ if (chr < (192 + 2))
+ return 1;
+ return uni_utf8_non1_bytes[chr - (192 + 2)];
+}
+
+/* Return given character in titlecase. */
+unichar_t uni_ucs4_to_titlecase(unichar_t chr) ATTR_CONST;
+
+/* Convert UTF-8 input to titlecase and decompose the titlecase characters to
+ output buffer. Returns 0 if ok, -1 if input was invalid. This generates
+ output that's compatible with i;unicode-casemap comparator. Invalid input
+ is replaced with unicode replacement character (0xfffd). */
+int uni_utf8_to_decomposed_titlecase(const void *input, size_t size,
+ buffer_t *output);
+
+/* If input contains only valid UTF-8 characters, return TRUE without updating
+ buf. If input contains invalid UTF-8 characters, replace them with unicode
+ replacement character (0xfffd), write the output to buf and return FALSE. */
+bool uni_utf8_get_valid_data(const unsigned char *input, size_t size,
+ buffer_t *buf) ATTR_WARN_UNUSED_RESULT;
+/* Returns TRUE if string is valid UTF-8 input. */
+bool uni_utf8_str_is_valid(const char *str);
+/* Returns TRUE if data contains only valid UTF-8 input. */
+bool uni_utf8_data_is_valid(const unsigned char *data, size_t size);
+/* Returns the size of the data when truncated to be less than or equal to
+ max_new_size, making sure UTF-8 character boundaries are respected. This only
+ looks at the last character at the new boundary. */
+size_t uni_utf8_data_truncate(const unsigned char *data, size_t old_size,
+ size_t max_new_size);
+
+/* surrogate handling */
+static inline unichar_t uni_join_surrogate(unichar_t high, unichar_t low)
+{
+ i_assert(UTF16_VALID_HIGH_SURROGATE(high) &&
+ UTF16_VALID_LOW_SURROGATE(low));
+
+ return ((high - UTF16_SURROGATE_HIGH_FIRST)<<10) +
+ (low - UTF16_SURROGATE_LOW_FIRST) +
+ UTF16_SURROGATE_BASE;
+}
+
+static inline void uni_split_surrogate(unichar_t chr, unichar_t *high_r, unichar_t *low_r)
+{
+ i_assert(chr >= UTF16_SURROGATE_BASE && chr <= UNICHAR_T_MAX);
+ i_assert(high_r != NULL && low_r != NULL);
+ *high_r = UTF16_SURROGATE_HIGH(chr);
+ *low_r = UTF16_SURROGATE_LOW(chr);
+}
+#endif
diff --git a/src/lib/unicodemap.c b/src/lib/unicodemap.c
new file mode 100644
index 0000000..ba39cae
--- /dev/null
+++ b/src/lib/unicodemap.c
@@ -0,0 +1,2474 @@
+/* This file is automatically generated by unicodemap.pl from UnicodeData.txt
+
+ NOTE: decompositions for characters having titlecase characters
+ are not included, because we first translate everything to titlecase */
+static const uint16_t titlecase8_map[256] = {
+ 0x00000, 0x00001, 0x00002, 0x00003, 0x00004, 0x00005, 0x00006, 0x00007,
+ 0x00008, 0x00009, 0x0000a, 0x0000b, 0x0000c, 0x0000d, 0x0000e, 0x0000f,
+ 0x00010, 0x00011, 0x00012, 0x00013, 0x00014, 0x00015, 0x00016, 0x00017,
+ 0x00018, 0x00019, 0x0001a, 0x0001b, 0x0001c, 0x0001d, 0x0001e, 0x0001f,
+ 0x00020, 0x00021, 0x00022, 0x00023, 0x00024, 0x00025, 0x00026, 0x00027,
+ 0x00028, 0x00029, 0x0002a, 0x0002b, 0x0002c, 0x0002d, 0x0002e, 0x0002f,
+ 0x00030, 0x00031, 0x00032, 0x00033, 0x00034, 0x00035, 0x00036, 0x00037,
+ 0x00038, 0x00039, 0x0003a, 0x0003b, 0x0003c, 0x0003d, 0x0003e, 0x0003f,
+ 0x00040, 0x00041, 0x00042, 0x00043, 0x00044, 0x00045, 0x00046, 0x00047,
+ 0x00048, 0x00049, 0x0004a, 0x0004b, 0x0004c, 0x0004d, 0x0004e, 0x0004f,
+ 0x00050, 0x00051, 0x00052, 0x00053, 0x00054, 0x00055, 0x00056, 0x00057,
+ 0x00058, 0x00059, 0x0005a, 0x0005b, 0x0005c, 0x0005d, 0x0005e, 0x0005f,
+ 0x00060, 0x00041, 0x00042, 0x00043, 0x00044, 0x00045, 0x00046, 0x00047,
+ 0x00048, 0x00049, 0x0004a, 0x0004b, 0x0004c, 0x0004d, 0x0004e, 0x0004f,
+ 0x00050, 0x00051, 0x00052, 0x00053, 0x00054, 0x00055, 0x00056, 0x00057,
+ 0x00058, 0x00059, 0x0005a, 0x0007b, 0x0007c, 0x0007d, 0x0007e, 0x0007f,
+ 0x00080, 0x00081, 0x00082, 0x00083, 0x00084, 0x00085, 0x00086, 0x00087,
+ 0x00088, 0x00089, 0x0008a, 0x0008b, 0x0008c, 0x0008d, 0x0008e, 0x0008f,
+ 0x00090, 0x00091, 0x00092, 0x00093, 0x00094, 0x00095, 0x00096, 0x00097,
+ 0x00098, 0x00099, 0x0009a, 0x0009b, 0x0009c, 0x0009d, 0x0009e, 0x0009f,
+ 0x000a0, 0x000a1, 0x000a2, 0x000a3, 0x000a4, 0x000a5, 0x000a6, 0x000a7,
+ 0x000a8, 0x000a9, 0x000aa, 0x000ab, 0x000ac, 0x000ad, 0x000ae, 0x000af,
+ 0x000b0, 0x000b1, 0x000b2, 0x000b3, 0x000b4, 0x0039c, 0x000b6, 0x000b7,
+ 0x000b8, 0x000b9, 0x000ba, 0x000bb, 0x000bc, 0x000bd, 0x000be, 0x000bf,
+ 0x000c0, 0x000c1, 0x000c2, 0x000c3, 0x000c4, 0x000c5, 0x000c6, 0x000c7,
+ 0x000c8, 0x000c9, 0x000ca, 0x000cb, 0x000cc, 0x000cd, 0x000ce, 0x000cf,
+ 0x000d0, 0x000d1, 0x000d2, 0x000d3, 0x000d4, 0x000d5, 0x000d6, 0x000d7,
+ 0x000d8, 0x000d9, 0x000da, 0x000db, 0x000dc, 0x000dd, 0x000de, 0x000df,
+ 0x000c0, 0x000c1, 0x000c2, 0x000c3, 0x000c4, 0x000c5, 0x000c6, 0x000c7,
+ 0x000c8, 0x000c9, 0x000ca, 0x000cb, 0x000cc, 0x000cd, 0x000ce, 0x000cf,
+ 0x000d0, 0x000d1, 0x000d2, 0x000d3, 0x000d4, 0x000d5, 0x000d6, 0x000f7,
+ 0x000d8, 0x000d9, 0x000da, 0x000db, 0x000dc, 0x000dd, 0x000de, 0x00178
+};
+static const uint16_t titlecase16_keys[] = {
+ 0x00101, 0x00103, 0x00105, 0x00107, 0x00109, 0x0010b, 0x0010d, 0x0010f,
+ 0x00111, 0x00113, 0x00115, 0x00117, 0x00119, 0x0011b, 0x0011d, 0x0011f,
+ 0x00121, 0x00123, 0x00125, 0x00127, 0x00129, 0x0012b, 0x0012d, 0x0012f,
+ 0x00131, 0x00133, 0x00135, 0x00137, 0x0013a, 0x0013c, 0x0013e, 0x00140,
+ 0x00142, 0x00144, 0x00146, 0x00148, 0x0014b, 0x0014d, 0x0014f, 0x00151,
+ 0x00153, 0x00155, 0x00157, 0x00159, 0x0015b, 0x0015d, 0x0015f, 0x00161,
+ 0x00163, 0x00165, 0x00167, 0x00169, 0x0016b, 0x0016d, 0x0016f, 0x00171,
+ 0x00173, 0x00175, 0x00177, 0x0017a, 0x0017c, 0x0017e, 0x0017f, 0x00180,
+ 0x00183, 0x00185, 0x00188, 0x0018c, 0x00192, 0x00195, 0x00199, 0x0019a,
+ 0x0019e, 0x001a1, 0x001a3, 0x001a5, 0x001a8, 0x001ad, 0x001b0, 0x001b4,
+ 0x001b6, 0x001b9, 0x001bd, 0x001bf, 0x001c4, 0x001c6, 0x001c7, 0x001c9,
+ 0x001ca, 0x001cc, 0x001ce, 0x001d0, 0x001d2, 0x001d4, 0x001d6, 0x001d8,
+ 0x001da, 0x001dc, 0x001dd, 0x001df, 0x001e1, 0x001e3, 0x001e5, 0x001e7,
+ 0x001e9, 0x001eb, 0x001ed, 0x001ef, 0x001f1, 0x001f3, 0x001f5, 0x001f9,
+ 0x001fb, 0x001fd, 0x001ff, 0x00201, 0x00203, 0x00205, 0x00207, 0x00209,
+ 0x0020b, 0x0020d, 0x0020f, 0x00211, 0x00213, 0x00215, 0x00217, 0x00219,
+ 0x0021b, 0x0021d, 0x0021f, 0x00223, 0x00225, 0x00227, 0x00229, 0x0022b,
+ 0x0022d, 0x0022f, 0x00231, 0x00233, 0x0023c, 0x0023f, 0x00240, 0x00242,
+ 0x00247, 0x00249, 0x0024b, 0x0024d, 0x0024f, 0x00250, 0x00251, 0x00252,
+ 0x00253, 0x00254, 0x00256, 0x00257, 0x00259, 0x0025b, 0x0025c, 0x00260,
+ 0x00261, 0x00263, 0x00265, 0x00266, 0x00268, 0x00269, 0x0026a, 0x0026b,
+ 0x0026c, 0x0026f, 0x00271, 0x00272, 0x00275, 0x0027d, 0x00280, 0x00283,
+ 0x00287, 0x00288, 0x00289, 0x0028a, 0x0028b, 0x0028c, 0x00292, 0x0029d,
+ 0x0029e, 0x00345, 0x00371, 0x00373, 0x00377, 0x0037b, 0x0037c, 0x0037d,
+ 0x003ac, 0x003ad, 0x003ae, 0x003af, 0x003b1, 0x003b2, 0x003b3, 0x003b4,
+ 0x003b5, 0x003b6, 0x003b7, 0x003b8, 0x003b9, 0x003ba, 0x003bb, 0x003bc,
+ 0x003bd, 0x003be, 0x003bf, 0x003c0, 0x003c1, 0x003c2, 0x003c3, 0x003c4,
+ 0x003c5, 0x003c6, 0x003c7, 0x003c8, 0x003c9, 0x003ca, 0x003cb, 0x003cc,
+ 0x003cd, 0x003ce, 0x003d0, 0x003d1, 0x003d5, 0x003d6, 0x003d7, 0x003d9,
+ 0x003db, 0x003dd, 0x003df, 0x003e1, 0x003e3, 0x003e5, 0x003e7, 0x003e9,
+ 0x003eb, 0x003ed, 0x003ef, 0x003f0, 0x003f1, 0x003f2, 0x003f3, 0x003f5,
+ 0x003f8, 0x003fb, 0x00430, 0x00431, 0x00432, 0x00433, 0x00434, 0x00435,
+ 0x00436, 0x00437, 0x00438, 0x00439, 0x0043a, 0x0043b, 0x0043c, 0x0043d,
+ 0x0043e, 0x0043f, 0x00440, 0x00441, 0x00442, 0x00443, 0x00444, 0x00445,
+ 0x00446, 0x00447, 0x00448, 0x00449, 0x0044a, 0x0044b, 0x0044c, 0x0044d,
+ 0x0044e, 0x0044f, 0x00450, 0x00451, 0x00452, 0x00453, 0x00454, 0x00455,
+ 0x00456, 0x00457, 0x00458, 0x00459, 0x0045a, 0x0045b, 0x0045c, 0x0045d,
+ 0x0045e, 0x0045f, 0x00461, 0x00463, 0x00465, 0x00467, 0x00469, 0x0046b,
+ 0x0046d, 0x0046f, 0x00471, 0x00473, 0x00475, 0x00477, 0x00479, 0x0047b,
+ 0x0047d, 0x0047f, 0x00481, 0x0048b, 0x0048d, 0x0048f, 0x00491, 0x00493,
+ 0x00495, 0x00497, 0x00499, 0x0049b, 0x0049d, 0x0049f, 0x004a1, 0x004a3,
+ 0x004a5, 0x004a7, 0x004a9, 0x004ab, 0x004ad, 0x004af, 0x004b1, 0x004b3,
+ 0x004b5, 0x004b7, 0x004b9, 0x004bb, 0x004bd, 0x004bf, 0x004c2, 0x004c4,
+ 0x004c6, 0x004c8, 0x004ca, 0x004cc, 0x004ce, 0x004cf, 0x004d1, 0x004d3,
+ 0x004d5, 0x004d7, 0x004d9, 0x004db, 0x004dd, 0x004df, 0x004e1, 0x004e3,
+ 0x004e5, 0x004e7, 0x004e9, 0x004eb, 0x004ed, 0x004ef, 0x004f1, 0x004f3,
+ 0x004f5, 0x004f7, 0x004f9, 0x004fb, 0x004fd, 0x004ff, 0x00501, 0x00503,
+ 0x00505, 0x00507, 0x00509, 0x0050b, 0x0050d, 0x0050f, 0x00511, 0x00513,
+ 0x00515, 0x00517, 0x00519, 0x0051b, 0x0051d, 0x0051f, 0x00521, 0x00523,
+ 0x00525, 0x00527, 0x00529, 0x0052b, 0x0052d, 0x0052f, 0x00561, 0x00562,
+ 0x00563, 0x00564, 0x00565, 0x00566, 0x00567, 0x00568, 0x00569, 0x0056a,
+ 0x0056b, 0x0056c, 0x0056d, 0x0056e, 0x0056f, 0x00570, 0x00571, 0x00572,
+ 0x00573, 0x00574, 0x00575, 0x00576, 0x00577, 0x00578, 0x00579, 0x0057a,
+ 0x0057b, 0x0057c, 0x0057d, 0x0057e, 0x0057f, 0x00580, 0x00581, 0x00582,
+ 0x00583, 0x00584, 0x00585, 0x00586, 0x013f8, 0x013f9, 0x013fa, 0x013fb,
+ 0x013fc, 0x013fd, 0x01c80, 0x01c81, 0x01c82, 0x01c83, 0x01c84, 0x01c85,
+ 0x01c86, 0x01c87, 0x01c88, 0x01d79, 0x01d7d, 0x01e01, 0x01e03, 0x01e05,
+ 0x01e07, 0x01e09, 0x01e0b, 0x01e0d, 0x01e0f, 0x01e11, 0x01e13, 0x01e15,
+ 0x01e17, 0x01e19, 0x01e1b, 0x01e1d, 0x01e1f, 0x01e21, 0x01e23, 0x01e25,
+ 0x01e27, 0x01e29, 0x01e2b, 0x01e2d, 0x01e2f, 0x01e31, 0x01e33, 0x01e35,
+ 0x01e37, 0x01e39, 0x01e3b, 0x01e3d, 0x01e3f, 0x01e41, 0x01e43, 0x01e45,
+ 0x01e47, 0x01e49, 0x01e4b, 0x01e4d, 0x01e4f, 0x01e51, 0x01e53, 0x01e55,
+ 0x01e57, 0x01e59, 0x01e5b, 0x01e5d, 0x01e5f, 0x01e61, 0x01e63, 0x01e65,
+ 0x01e67, 0x01e69, 0x01e6b, 0x01e6d, 0x01e6f, 0x01e71, 0x01e73, 0x01e75,
+ 0x01e77, 0x01e79, 0x01e7b, 0x01e7d, 0x01e7f, 0x01e81, 0x01e83, 0x01e85,
+ 0x01e87, 0x01e89, 0x01e8b, 0x01e8d, 0x01e8f, 0x01e91, 0x01e93, 0x01e95,
+ 0x01e9b, 0x01ea1, 0x01ea3, 0x01ea5, 0x01ea7, 0x01ea9, 0x01eab, 0x01ead,
+ 0x01eaf, 0x01eb1, 0x01eb3, 0x01eb5, 0x01eb7, 0x01eb9, 0x01ebb, 0x01ebd,
+ 0x01ebf, 0x01ec1, 0x01ec3, 0x01ec5, 0x01ec7, 0x01ec9, 0x01ecb, 0x01ecd,
+ 0x01ecf, 0x01ed1, 0x01ed3, 0x01ed5, 0x01ed7, 0x01ed9, 0x01edb, 0x01edd,
+ 0x01edf, 0x01ee1, 0x01ee3, 0x01ee5, 0x01ee7, 0x01ee9, 0x01eeb, 0x01eed,
+ 0x01eef, 0x01ef1, 0x01ef3, 0x01ef5, 0x01ef7, 0x01ef9, 0x01efb, 0x01efd,
+ 0x01eff, 0x01f00, 0x01f01, 0x01f02, 0x01f03, 0x01f04, 0x01f05, 0x01f06,
+ 0x01f07, 0x01f10, 0x01f11, 0x01f12, 0x01f13, 0x01f14, 0x01f15, 0x01f20,
+ 0x01f21, 0x01f22, 0x01f23, 0x01f24, 0x01f25, 0x01f26, 0x01f27, 0x01f30,
+ 0x01f31, 0x01f32, 0x01f33, 0x01f34, 0x01f35, 0x01f36, 0x01f37, 0x01f40,
+ 0x01f41, 0x01f42, 0x01f43, 0x01f44, 0x01f45, 0x01f51, 0x01f53, 0x01f55,
+ 0x01f57, 0x01f60, 0x01f61, 0x01f62, 0x01f63, 0x01f64, 0x01f65, 0x01f66,
+ 0x01f67, 0x01f70, 0x01f71, 0x01f72, 0x01f73, 0x01f74, 0x01f75, 0x01f76,
+ 0x01f77, 0x01f78, 0x01f79, 0x01f7a, 0x01f7b, 0x01f7c, 0x01f7d, 0x01f80,
+ 0x01f81, 0x01f82, 0x01f83, 0x01f84, 0x01f85, 0x01f86, 0x01f87, 0x01f90,
+ 0x01f91, 0x01f92, 0x01f93, 0x01f94, 0x01f95, 0x01f96, 0x01f97, 0x01fa0,
+ 0x01fa1, 0x01fa2, 0x01fa3, 0x01fa4, 0x01fa5, 0x01fa6, 0x01fa7, 0x01fb0,
+ 0x01fb1, 0x01fb3, 0x01fbe, 0x01fc3, 0x01fd0, 0x01fd1, 0x01fe0, 0x01fe1,
+ 0x01fe5, 0x01ff3, 0x0214e, 0x02170, 0x02171, 0x02172, 0x02173, 0x02174,
+ 0x02175, 0x02176, 0x02177, 0x02178, 0x02179, 0x0217a, 0x0217b, 0x0217c,
+ 0x0217d, 0x0217e, 0x0217f, 0x02184, 0x024d0, 0x024d1, 0x024d2, 0x024d3,
+ 0x024d4, 0x024d5, 0x024d6, 0x024d7, 0x024d8, 0x024d9, 0x024da, 0x024db,
+ 0x024dc, 0x024dd, 0x024de, 0x024df, 0x024e0, 0x024e1, 0x024e2, 0x024e3,
+ 0x024e4, 0x024e5, 0x024e6, 0x024e7, 0x024e8, 0x024e9, 0x02c30, 0x02c31,
+ 0x02c32, 0x02c33, 0x02c34, 0x02c35, 0x02c36, 0x02c37, 0x02c38, 0x02c39,
+ 0x02c3a, 0x02c3b, 0x02c3c, 0x02c3d, 0x02c3e, 0x02c3f, 0x02c40, 0x02c41,
+ 0x02c42, 0x02c43, 0x02c44, 0x02c45, 0x02c46, 0x02c47, 0x02c48, 0x02c49,
+ 0x02c4a, 0x02c4b, 0x02c4c, 0x02c4d, 0x02c4e, 0x02c4f, 0x02c50, 0x02c51,
+ 0x02c52, 0x02c53, 0x02c54, 0x02c55, 0x02c56, 0x02c57, 0x02c58, 0x02c59,
+ 0x02c5a, 0x02c5b, 0x02c5c, 0x02c5d, 0x02c5e, 0x02c61, 0x02c65, 0x02c66,
+ 0x02c68, 0x02c6a, 0x02c6c, 0x02c73, 0x02c76, 0x02c81, 0x02c83, 0x02c85,
+ 0x02c87, 0x02c89, 0x02c8b, 0x02c8d, 0x02c8f, 0x02c91, 0x02c93, 0x02c95,
+ 0x02c97, 0x02c99, 0x02c9b, 0x02c9d, 0x02c9f, 0x02ca1, 0x02ca3, 0x02ca5,
+ 0x02ca7, 0x02ca9, 0x02cab, 0x02cad, 0x02caf, 0x02cb1, 0x02cb3, 0x02cb5,
+ 0x02cb7, 0x02cb9, 0x02cbb, 0x02cbd, 0x02cbf, 0x02cc1, 0x02cc3, 0x02cc5,
+ 0x02cc7, 0x02cc9, 0x02ccb, 0x02ccd, 0x02ccf, 0x02cd1, 0x02cd3, 0x02cd5,
+ 0x02cd7, 0x02cd9, 0x02cdb, 0x02cdd, 0x02cdf, 0x02ce1, 0x02ce3, 0x02cec,
+ 0x02cee, 0x02cf3, 0x02d00, 0x02d01, 0x02d02, 0x02d03, 0x02d04, 0x02d05,
+ 0x02d06, 0x02d07, 0x02d08, 0x02d09, 0x02d0a, 0x02d0b, 0x02d0c, 0x02d0d,
+ 0x02d0e, 0x02d0f, 0x02d10, 0x02d11, 0x02d12, 0x02d13, 0x02d14, 0x02d15,
+ 0x02d16, 0x02d17, 0x02d18, 0x02d19, 0x02d1a, 0x02d1b, 0x02d1c, 0x02d1d,
+ 0x02d1e, 0x02d1f, 0x02d20, 0x02d21, 0x02d22, 0x02d23, 0x02d24, 0x02d25,
+ 0x02d27, 0x02d2d, 0x0a641, 0x0a643, 0x0a645, 0x0a647, 0x0a649, 0x0a64b,
+ 0x0a64d, 0x0a64f, 0x0a651, 0x0a653, 0x0a655, 0x0a657, 0x0a659, 0x0a65b,
+ 0x0a65d, 0x0a65f, 0x0a661, 0x0a663, 0x0a665, 0x0a667, 0x0a669, 0x0a66b,
+ 0x0a66d, 0x0a681, 0x0a683, 0x0a685, 0x0a687, 0x0a689, 0x0a68b, 0x0a68d,
+ 0x0a68f, 0x0a691, 0x0a693, 0x0a695, 0x0a697, 0x0a699, 0x0a69b, 0x0a723,
+ 0x0a725, 0x0a727, 0x0a729, 0x0a72b, 0x0a72d, 0x0a72f, 0x0a733, 0x0a735,
+ 0x0a737, 0x0a739, 0x0a73b, 0x0a73d, 0x0a73f, 0x0a741, 0x0a743, 0x0a745,
+ 0x0a747, 0x0a749, 0x0a74b, 0x0a74d, 0x0a74f, 0x0a751, 0x0a753, 0x0a755,
+ 0x0a757, 0x0a759, 0x0a75b, 0x0a75d, 0x0a75f, 0x0a761, 0x0a763, 0x0a765,
+ 0x0a767, 0x0a769, 0x0a76b, 0x0a76d, 0x0a76f, 0x0a77a, 0x0a77c, 0x0a77f,
+ 0x0a781, 0x0a783, 0x0a785, 0x0a787, 0x0a78c, 0x0a791, 0x0a793, 0x0a797,
+ 0x0a799, 0x0a79b, 0x0a79d, 0x0a79f, 0x0a7a1, 0x0a7a3, 0x0a7a5, 0x0a7a7,
+ 0x0a7a9, 0x0a7b5, 0x0a7b7, 0x0ab53, 0x0ab70, 0x0ab71, 0x0ab72, 0x0ab73,
+ 0x0ab74, 0x0ab75, 0x0ab76, 0x0ab77, 0x0ab78, 0x0ab79, 0x0ab7a, 0x0ab7b,
+ 0x0ab7c, 0x0ab7d, 0x0ab7e, 0x0ab7f, 0x0ab80, 0x0ab81, 0x0ab82, 0x0ab83,
+ 0x0ab84, 0x0ab85, 0x0ab86, 0x0ab87, 0x0ab88, 0x0ab89, 0x0ab8a, 0x0ab8b,
+ 0x0ab8c, 0x0ab8d, 0x0ab8e, 0x0ab8f, 0x0ab90, 0x0ab91, 0x0ab92, 0x0ab93,
+ 0x0ab94, 0x0ab95, 0x0ab96, 0x0ab97, 0x0ab98, 0x0ab99, 0x0ab9a, 0x0ab9b,
+ 0x0ab9c, 0x0ab9d, 0x0ab9e, 0x0ab9f, 0x0aba0, 0x0aba1, 0x0aba2, 0x0aba3,
+ 0x0aba4, 0x0aba5, 0x0aba6, 0x0aba7, 0x0aba8, 0x0aba9, 0x0abaa, 0x0abab,
+ 0x0abac, 0x0abad, 0x0abae, 0x0abaf, 0x0abb0, 0x0abb1, 0x0abb2, 0x0abb3,
+ 0x0abb4, 0x0abb5, 0x0abb6, 0x0abb7, 0x0abb8, 0x0abb9, 0x0abba, 0x0abbb,
+ 0x0abbc, 0x0abbd, 0x0abbe, 0x0abbf, 0x0ff41, 0x0ff42, 0x0ff43, 0x0ff44,
+ 0x0ff45, 0x0ff46, 0x0ff47, 0x0ff48, 0x0ff49, 0x0ff4a, 0x0ff4b, 0x0ff4c,
+ 0x0ff4d, 0x0ff4e, 0x0ff4f, 0x0ff50, 0x0ff51, 0x0ff52, 0x0ff53, 0x0ff54,
+ 0x0ff55, 0x0ff56, 0x0ff57, 0x0ff58, 0x0ff59, 0x0ff5a
+};
+static const uint16_t titlecase16_values[] = {
+ 0x00100, 0x00102, 0x00104, 0x00106, 0x00108, 0x0010a, 0x0010c, 0x0010e,
+ 0x00110, 0x00112, 0x00114, 0x00116, 0x00118, 0x0011a, 0x0011c, 0x0011e,
+ 0x00120, 0x00122, 0x00124, 0x00126, 0x00128, 0x0012a, 0x0012c, 0x0012e,
+ 0x00049, 0x00132, 0x00134, 0x00136, 0x00139, 0x0013b, 0x0013d, 0x0013f,
+ 0x00141, 0x00143, 0x00145, 0x00147, 0x0014a, 0x0014c, 0x0014e, 0x00150,
+ 0x00152, 0x00154, 0x00156, 0x00158, 0x0015a, 0x0015c, 0x0015e, 0x00160,
+ 0x00162, 0x00164, 0x00166, 0x00168, 0x0016a, 0x0016c, 0x0016e, 0x00170,
+ 0x00172, 0x00174, 0x00176, 0x00179, 0x0017b, 0x0017d, 0x00053, 0x00243,
+ 0x00182, 0x00184, 0x00187, 0x0018b, 0x00191, 0x001f6, 0x00198, 0x0023d,
+ 0x00220, 0x001a0, 0x001a2, 0x001a4, 0x001a7, 0x001ac, 0x001af, 0x001b3,
+ 0x001b5, 0x001b8, 0x001bc, 0x001f7, 0x001c5, 0x001c5, 0x001c8, 0x001c8,
+ 0x001cb, 0x001cb, 0x001cd, 0x001cf, 0x001d1, 0x001d3, 0x001d5, 0x001d7,
+ 0x001d9, 0x001db, 0x0018e, 0x001de, 0x001e0, 0x001e2, 0x001e4, 0x001e6,
+ 0x001e8, 0x001ea, 0x001ec, 0x001ee, 0x001f2, 0x001f2, 0x001f4, 0x001f8,
+ 0x001fa, 0x001fc, 0x001fe, 0x00200, 0x00202, 0x00204, 0x00206, 0x00208,
+ 0x0020a, 0x0020c, 0x0020e, 0x00210, 0x00212, 0x00214, 0x00216, 0x00218,
+ 0x0021a, 0x0021c, 0x0021e, 0x00222, 0x00224, 0x00226, 0x00228, 0x0022a,
+ 0x0022c, 0x0022e, 0x00230, 0x00232, 0x0023b, 0x02c7e, 0x02c7f, 0x00241,
+ 0x00246, 0x00248, 0x0024a, 0x0024c, 0x0024e, 0x02c6f, 0x02c6d, 0x02c70,
+ 0x00181, 0x00186, 0x00189, 0x0018a, 0x0018f, 0x00190, 0x0a7ab, 0x00193,
+ 0x0a7ac, 0x00194, 0x0a78d, 0x0a7aa, 0x00197, 0x00196, 0x0a7ae, 0x02c62,
+ 0x0a7ad, 0x0019c, 0x02c6e, 0x0019d, 0x0019f, 0x02c64, 0x001a6, 0x001a9,
+ 0x0a7b1, 0x001ae, 0x00244, 0x001b1, 0x001b2, 0x00245, 0x001b7, 0x0a7b2,
+ 0x0a7b0, 0x00399, 0x00370, 0x00372, 0x00376, 0x003fd, 0x003fe, 0x003ff,
+ 0x00386, 0x00388, 0x00389, 0x0038a, 0x00391, 0x00392, 0x00393, 0x00394,
+ 0x00395, 0x00396, 0x00397, 0x00398, 0x00399, 0x0039a, 0x0039b, 0x0039c,
+ 0x0039d, 0x0039e, 0x0039f, 0x003a0, 0x003a1, 0x003a3, 0x003a3, 0x003a4,
+ 0x003a5, 0x003a6, 0x003a7, 0x003a8, 0x003a9, 0x003aa, 0x003ab, 0x0038c,
+ 0x0038e, 0x0038f, 0x00392, 0x00398, 0x003a6, 0x003a0, 0x003cf, 0x003d8,
+ 0x003da, 0x003dc, 0x003de, 0x003e0, 0x003e2, 0x003e4, 0x003e6, 0x003e8,
+ 0x003ea, 0x003ec, 0x003ee, 0x0039a, 0x003a1, 0x003f9, 0x0037f, 0x00395,
+ 0x003f7, 0x003fa, 0x00410, 0x00411, 0x00412, 0x00413, 0x00414, 0x00415,
+ 0x00416, 0x00417, 0x00418, 0x00419, 0x0041a, 0x0041b, 0x0041c, 0x0041d,
+ 0x0041e, 0x0041f, 0x00420, 0x00421, 0x00422, 0x00423, 0x00424, 0x00425,
+ 0x00426, 0x00427, 0x00428, 0x00429, 0x0042a, 0x0042b, 0x0042c, 0x0042d,
+ 0x0042e, 0x0042f, 0x00400, 0x00401, 0x00402, 0x00403, 0x00404, 0x00405,
+ 0x00406, 0x00407, 0x00408, 0x00409, 0x0040a, 0x0040b, 0x0040c, 0x0040d,
+ 0x0040e, 0x0040f, 0x00460, 0x00462, 0x00464, 0x00466, 0x00468, 0x0046a,
+ 0x0046c, 0x0046e, 0x00470, 0x00472, 0x00474, 0x00476, 0x00478, 0x0047a,
+ 0x0047c, 0x0047e, 0x00480, 0x0048a, 0x0048c, 0x0048e, 0x00490, 0x00492,
+ 0x00494, 0x00496, 0x00498, 0x0049a, 0x0049c, 0x0049e, 0x004a0, 0x004a2,
+ 0x004a4, 0x004a6, 0x004a8, 0x004aa, 0x004ac, 0x004ae, 0x004b0, 0x004b2,
+ 0x004b4, 0x004b6, 0x004b8, 0x004ba, 0x004bc, 0x004be, 0x004c1, 0x004c3,
+ 0x004c5, 0x004c7, 0x004c9, 0x004cb, 0x004cd, 0x004c0, 0x004d0, 0x004d2,
+ 0x004d4, 0x004d6, 0x004d8, 0x004da, 0x004dc, 0x004de, 0x004e0, 0x004e2,
+ 0x004e4, 0x004e6, 0x004e8, 0x004ea, 0x004ec, 0x004ee, 0x004f0, 0x004f2,
+ 0x004f4, 0x004f6, 0x004f8, 0x004fa, 0x004fc, 0x004fe, 0x00500, 0x00502,
+ 0x00504, 0x00506, 0x00508, 0x0050a, 0x0050c, 0x0050e, 0x00510, 0x00512,
+ 0x00514, 0x00516, 0x00518, 0x0051a, 0x0051c, 0x0051e, 0x00520, 0x00522,
+ 0x00524, 0x00526, 0x00528, 0x0052a, 0x0052c, 0x0052e, 0x00531, 0x00532,
+ 0x00533, 0x00534, 0x00535, 0x00536, 0x00537, 0x00538, 0x00539, 0x0053a,
+ 0x0053b, 0x0053c, 0x0053d, 0x0053e, 0x0053f, 0x00540, 0x00541, 0x00542,
+ 0x00543, 0x00544, 0x00545, 0x00546, 0x00547, 0x00548, 0x00549, 0x0054a,
+ 0x0054b, 0x0054c, 0x0054d, 0x0054e, 0x0054f, 0x00550, 0x00551, 0x00552,
+ 0x00553, 0x00554, 0x00555, 0x00556, 0x013f0, 0x013f1, 0x013f2, 0x013f3,
+ 0x013f4, 0x013f5, 0x00412, 0x00414, 0x0041e, 0x00421, 0x00422, 0x00422,
+ 0x0042a, 0x00462, 0x0a64a, 0x0a77d, 0x02c63, 0x01e00, 0x01e02, 0x01e04,
+ 0x01e06, 0x01e08, 0x01e0a, 0x01e0c, 0x01e0e, 0x01e10, 0x01e12, 0x01e14,
+ 0x01e16, 0x01e18, 0x01e1a, 0x01e1c, 0x01e1e, 0x01e20, 0x01e22, 0x01e24,
+ 0x01e26, 0x01e28, 0x01e2a, 0x01e2c, 0x01e2e, 0x01e30, 0x01e32, 0x01e34,
+ 0x01e36, 0x01e38, 0x01e3a, 0x01e3c, 0x01e3e, 0x01e40, 0x01e42, 0x01e44,
+ 0x01e46, 0x01e48, 0x01e4a, 0x01e4c, 0x01e4e, 0x01e50, 0x01e52, 0x01e54,
+ 0x01e56, 0x01e58, 0x01e5a, 0x01e5c, 0x01e5e, 0x01e60, 0x01e62, 0x01e64,
+ 0x01e66, 0x01e68, 0x01e6a, 0x01e6c, 0x01e6e, 0x01e70, 0x01e72, 0x01e74,
+ 0x01e76, 0x01e78, 0x01e7a, 0x01e7c, 0x01e7e, 0x01e80, 0x01e82, 0x01e84,
+ 0x01e86, 0x01e88, 0x01e8a, 0x01e8c, 0x01e8e, 0x01e90, 0x01e92, 0x01e94,
+ 0x01e60, 0x01ea0, 0x01ea2, 0x01ea4, 0x01ea6, 0x01ea8, 0x01eaa, 0x01eac,
+ 0x01eae, 0x01eb0, 0x01eb2, 0x01eb4, 0x01eb6, 0x01eb8, 0x01eba, 0x01ebc,
+ 0x01ebe, 0x01ec0, 0x01ec2, 0x01ec4, 0x01ec6, 0x01ec8, 0x01eca, 0x01ecc,
+ 0x01ece, 0x01ed0, 0x01ed2, 0x01ed4, 0x01ed6, 0x01ed8, 0x01eda, 0x01edc,
+ 0x01ede, 0x01ee0, 0x01ee2, 0x01ee4, 0x01ee6, 0x01ee8, 0x01eea, 0x01eec,
+ 0x01eee, 0x01ef0, 0x01ef2, 0x01ef4, 0x01ef6, 0x01ef8, 0x01efa, 0x01efc,
+ 0x01efe, 0x01f08, 0x01f09, 0x01f0a, 0x01f0b, 0x01f0c, 0x01f0d, 0x01f0e,
+ 0x01f0f, 0x01f18, 0x01f19, 0x01f1a, 0x01f1b, 0x01f1c, 0x01f1d, 0x01f28,
+ 0x01f29, 0x01f2a, 0x01f2b, 0x01f2c, 0x01f2d, 0x01f2e, 0x01f2f, 0x01f38,
+ 0x01f39, 0x01f3a, 0x01f3b, 0x01f3c, 0x01f3d, 0x01f3e, 0x01f3f, 0x01f48,
+ 0x01f49, 0x01f4a, 0x01f4b, 0x01f4c, 0x01f4d, 0x01f59, 0x01f5b, 0x01f5d,
+ 0x01f5f, 0x01f68, 0x01f69, 0x01f6a, 0x01f6b, 0x01f6c, 0x01f6d, 0x01f6e,
+ 0x01f6f, 0x01fba, 0x01fbb, 0x01fc8, 0x01fc9, 0x01fca, 0x01fcb, 0x01fda,
+ 0x01fdb, 0x01ff8, 0x01ff9, 0x01fea, 0x01feb, 0x01ffa, 0x01ffb, 0x01f88,
+ 0x01f89, 0x01f8a, 0x01f8b, 0x01f8c, 0x01f8d, 0x01f8e, 0x01f8f, 0x01f98,
+ 0x01f99, 0x01f9a, 0x01f9b, 0x01f9c, 0x01f9d, 0x01f9e, 0x01f9f, 0x01fa8,
+ 0x01fa9, 0x01faa, 0x01fab, 0x01fac, 0x01fad, 0x01fae, 0x01faf, 0x01fb8,
+ 0x01fb9, 0x01fbc, 0x00399, 0x01fcc, 0x01fd8, 0x01fd9, 0x01fe8, 0x01fe9,
+ 0x01fec, 0x01ffc, 0x02132, 0x02160, 0x02161, 0x02162, 0x02163, 0x02164,
+ 0x02165, 0x02166, 0x02167, 0x02168, 0x02169, 0x0216a, 0x0216b, 0x0216c,
+ 0x0216d, 0x0216e, 0x0216f, 0x02183, 0x024b6, 0x024b7, 0x024b8, 0x024b9,
+ 0x024ba, 0x024bb, 0x024bc, 0x024bd, 0x024be, 0x024bf, 0x024c0, 0x024c1,
+ 0x024c2, 0x024c3, 0x024c4, 0x024c5, 0x024c6, 0x024c7, 0x024c8, 0x024c9,
+ 0x024ca, 0x024cb, 0x024cc, 0x024cd, 0x024ce, 0x024cf, 0x02c00, 0x02c01,
+ 0x02c02, 0x02c03, 0x02c04, 0x02c05, 0x02c06, 0x02c07, 0x02c08, 0x02c09,
+ 0x02c0a, 0x02c0b, 0x02c0c, 0x02c0d, 0x02c0e, 0x02c0f, 0x02c10, 0x02c11,
+ 0x02c12, 0x02c13, 0x02c14, 0x02c15, 0x02c16, 0x02c17, 0x02c18, 0x02c19,
+ 0x02c1a, 0x02c1b, 0x02c1c, 0x02c1d, 0x02c1e, 0x02c1f, 0x02c20, 0x02c21,
+ 0x02c22, 0x02c23, 0x02c24, 0x02c25, 0x02c26, 0x02c27, 0x02c28, 0x02c29,
+ 0x02c2a, 0x02c2b, 0x02c2c, 0x02c2d, 0x02c2e, 0x02c60, 0x0023a, 0x0023e,
+ 0x02c67, 0x02c69, 0x02c6b, 0x02c72, 0x02c75, 0x02c80, 0x02c82, 0x02c84,
+ 0x02c86, 0x02c88, 0x02c8a, 0x02c8c, 0x02c8e, 0x02c90, 0x02c92, 0x02c94,
+ 0x02c96, 0x02c98, 0x02c9a, 0x02c9c, 0x02c9e, 0x02ca0, 0x02ca2, 0x02ca4,
+ 0x02ca6, 0x02ca8, 0x02caa, 0x02cac, 0x02cae, 0x02cb0, 0x02cb2, 0x02cb4,
+ 0x02cb6, 0x02cb8, 0x02cba, 0x02cbc, 0x02cbe, 0x02cc0, 0x02cc2, 0x02cc4,
+ 0x02cc6, 0x02cc8, 0x02cca, 0x02ccc, 0x02cce, 0x02cd0, 0x02cd2, 0x02cd4,
+ 0x02cd6, 0x02cd8, 0x02cda, 0x02cdc, 0x02cde, 0x02ce0, 0x02ce2, 0x02ceb,
+ 0x02ced, 0x02cf2, 0x010a0, 0x010a1, 0x010a2, 0x010a3, 0x010a4, 0x010a5,
+ 0x010a6, 0x010a7, 0x010a8, 0x010a9, 0x010aa, 0x010ab, 0x010ac, 0x010ad,
+ 0x010ae, 0x010af, 0x010b0, 0x010b1, 0x010b2, 0x010b3, 0x010b4, 0x010b5,
+ 0x010b6, 0x010b7, 0x010b8, 0x010b9, 0x010ba, 0x010bb, 0x010bc, 0x010bd,
+ 0x010be, 0x010bf, 0x010c0, 0x010c1, 0x010c2, 0x010c3, 0x010c4, 0x010c5,
+ 0x010c7, 0x010cd, 0x0a640, 0x0a642, 0x0a644, 0x0a646, 0x0a648, 0x0a64a,
+ 0x0a64c, 0x0a64e, 0x0a650, 0x0a652, 0x0a654, 0x0a656, 0x0a658, 0x0a65a,
+ 0x0a65c, 0x0a65e, 0x0a660, 0x0a662, 0x0a664, 0x0a666, 0x0a668, 0x0a66a,
+ 0x0a66c, 0x0a680, 0x0a682, 0x0a684, 0x0a686, 0x0a688, 0x0a68a, 0x0a68c,
+ 0x0a68e, 0x0a690, 0x0a692, 0x0a694, 0x0a696, 0x0a698, 0x0a69a, 0x0a722,
+ 0x0a724, 0x0a726, 0x0a728, 0x0a72a, 0x0a72c, 0x0a72e, 0x0a732, 0x0a734,
+ 0x0a736, 0x0a738, 0x0a73a, 0x0a73c, 0x0a73e, 0x0a740, 0x0a742, 0x0a744,
+ 0x0a746, 0x0a748, 0x0a74a, 0x0a74c, 0x0a74e, 0x0a750, 0x0a752, 0x0a754,
+ 0x0a756, 0x0a758, 0x0a75a, 0x0a75c, 0x0a75e, 0x0a760, 0x0a762, 0x0a764,
+ 0x0a766, 0x0a768, 0x0a76a, 0x0a76c, 0x0a76e, 0x0a779, 0x0a77b, 0x0a77e,
+ 0x0a780, 0x0a782, 0x0a784, 0x0a786, 0x0a78b, 0x0a790, 0x0a792, 0x0a796,
+ 0x0a798, 0x0a79a, 0x0a79c, 0x0a79e, 0x0a7a0, 0x0a7a2, 0x0a7a4, 0x0a7a6,
+ 0x0a7a8, 0x0a7b4, 0x0a7b6, 0x0a7b3, 0x013a0, 0x013a1, 0x013a2, 0x013a3,
+ 0x013a4, 0x013a5, 0x013a6, 0x013a7, 0x013a8, 0x013a9, 0x013aa, 0x013ab,
+ 0x013ac, 0x013ad, 0x013ae, 0x013af, 0x013b0, 0x013b1, 0x013b2, 0x013b3,
+ 0x013b4, 0x013b5, 0x013b6, 0x013b7, 0x013b8, 0x013b9, 0x013ba, 0x013bb,
+ 0x013bc, 0x013bd, 0x013be, 0x013bf, 0x013c0, 0x013c1, 0x013c2, 0x013c3,
+ 0x013c4, 0x013c5, 0x013c6, 0x013c7, 0x013c8, 0x013c9, 0x013ca, 0x013cb,
+ 0x013cc, 0x013cd, 0x013ce, 0x013cf, 0x013d0, 0x013d1, 0x013d2, 0x013d3,
+ 0x013d4, 0x013d5, 0x013d6, 0x013d7, 0x013d8, 0x013d9, 0x013da, 0x013db,
+ 0x013dc, 0x013dd, 0x013de, 0x013df, 0x013e0, 0x013e1, 0x013e2, 0x013e3,
+ 0x013e4, 0x013e5, 0x013e6, 0x013e7, 0x013e8, 0x013e9, 0x013ea, 0x013eb,
+ 0x013ec, 0x013ed, 0x013ee, 0x013ef, 0x0ff21, 0x0ff22, 0x0ff23, 0x0ff24,
+ 0x0ff25, 0x0ff26, 0x0ff27, 0x0ff28, 0x0ff29, 0x0ff2a, 0x0ff2b, 0x0ff2c,
+ 0x0ff2d, 0x0ff2e, 0x0ff2f, 0x0ff30, 0x0ff31, 0x0ff32, 0x0ff33, 0x0ff34,
+ 0x0ff35, 0x0ff36, 0x0ff37, 0x0ff38, 0x0ff39, 0x0ff3a
+};
+static const uint32_t titlecase32_keys[] = {
+ 0x10428, 0x10429, 0x1042a, 0x1042b, 0x1042c, 0x1042d, 0x1042e, 0x1042f,
+ 0x10430, 0x10431, 0x10432, 0x10433, 0x10434, 0x10435, 0x10436, 0x10437,
+ 0x10438, 0x10439, 0x1043a, 0x1043b, 0x1043c, 0x1043d, 0x1043e, 0x1043f,
+ 0x10440, 0x10441, 0x10442, 0x10443, 0x10444, 0x10445, 0x10446, 0x10447,
+ 0x10448, 0x10449, 0x1044a, 0x1044b, 0x1044c, 0x1044d, 0x1044e, 0x1044f,
+ 0x104d8, 0x104d9, 0x104da, 0x104db, 0x104dc, 0x104dd, 0x104de, 0x104df,
+ 0x104e0, 0x104e1, 0x104e2, 0x104e3, 0x104e4, 0x104e5, 0x104e6, 0x104e7,
+ 0x104e8, 0x104e9, 0x104ea, 0x104eb, 0x104ec, 0x104ed, 0x104ee, 0x104ef,
+ 0x104f0, 0x104f1, 0x104f2, 0x104f3, 0x104f4, 0x104f5, 0x104f6, 0x104f7,
+ 0x104f8, 0x104f9, 0x104fa, 0x104fb, 0x10cc0, 0x10cc1, 0x10cc2, 0x10cc3,
+ 0x10cc4, 0x10cc5, 0x10cc6, 0x10cc7, 0x10cc8, 0x10cc9, 0x10cca, 0x10ccb,
+ 0x10ccc, 0x10ccd, 0x10cce, 0x10ccf, 0x10cd0, 0x10cd1, 0x10cd2, 0x10cd3,
+ 0x10cd4, 0x10cd5, 0x10cd6, 0x10cd7, 0x10cd8, 0x10cd9, 0x10cda, 0x10cdb,
+ 0x10cdc, 0x10cdd, 0x10cde, 0x10cdf, 0x10ce0, 0x10ce1, 0x10ce2, 0x10ce3,
+ 0x10ce4, 0x10ce5, 0x10ce6, 0x10ce7, 0x10ce8, 0x10ce9, 0x10cea, 0x10ceb,
+ 0x10cec, 0x10ced, 0x10cee, 0x10cef, 0x10cf0, 0x10cf1, 0x10cf2, 0x118c0,
+ 0x118c1, 0x118c2, 0x118c3, 0x118c4, 0x118c5, 0x118c6, 0x118c7, 0x118c8,
+ 0x118c9, 0x118ca, 0x118cb, 0x118cc, 0x118cd, 0x118ce, 0x118cf, 0x118d0,
+ 0x118d1, 0x118d2, 0x118d3, 0x118d4, 0x118d5, 0x118d6, 0x118d7, 0x118d8,
+ 0x118d9, 0x118da, 0x118db, 0x118dc, 0x118dd, 0x118de, 0x118df, 0x1e922,
+ 0x1e923, 0x1e924, 0x1e925, 0x1e926, 0x1e927, 0x1e928, 0x1e929, 0x1e92a,
+ 0x1e92b, 0x1e92c, 0x1e92d, 0x1e92e, 0x1e92f, 0x1e930, 0x1e931, 0x1e932,
+ 0x1e933, 0x1e934, 0x1e935, 0x1e936, 0x1e937, 0x1e938, 0x1e939, 0x1e93a,
+ 0x1e93b, 0x1e93c, 0x1e93d, 0x1e93e, 0x1e93f, 0x1e940, 0x1e941, 0x1e942,
+ 0x1e943
+};
+static const uint32_t titlecase32_values[] = {
+ 0x10400, 0x10401, 0x10402, 0x10403, 0x10404, 0x10405, 0x10406, 0x10407,
+ 0x10408, 0x10409, 0x1040a, 0x1040b, 0x1040c, 0x1040d, 0x1040e, 0x1040f,
+ 0x10410, 0x10411, 0x10412, 0x10413, 0x10414, 0x10415, 0x10416, 0x10417,
+ 0x10418, 0x10419, 0x1041a, 0x1041b, 0x1041c, 0x1041d, 0x1041e, 0x1041f,
+ 0x10420, 0x10421, 0x10422, 0x10423, 0x10424, 0x10425, 0x10426, 0x10427,
+ 0x104b0, 0x104b1, 0x104b2, 0x104b3, 0x104b4, 0x104b5, 0x104b6, 0x104b7,
+ 0x104b8, 0x104b9, 0x104ba, 0x104bb, 0x104bc, 0x104bd, 0x104be, 0x104bf,
+ 0x104c0, 0x104c1, 0x104c2, 0x104c3, 0x104c4, 0x104c5, 0x104c6, 0x104c7,
+ 0x104c8, 0x104c9, 0x104ca, 0x104cb, 0x104cc, 0x104cd, 0x104ce, 0x104cf,
+ 0x104d0, 0x104d1, 0x104d2, 0x104d3, 0x10c80, 0x10c81, 0x10c82, 0x10c83,
+ 0x10c84, 0x10c85, 0x10c86, 0x10c87, 0x10c88, 0x10c89, 0x10c8a, 0x10c8b,
+ 0x10c8c, 0x10c8d, 0x10c8e, 0x10c8f, 0x10c90, 0x10c91, 0x10c92, 0x10c93,
+ 0x10c94, 0x10c95, 0x10c96, 0x10c97, 0x10c98, 0x10c99, 0x10c9a, 0x10c9b,
+ 0x10c9c, 0x10c9d, 0x10c9e, 0x10c9f, 0x10ca0, 0x10ca1, 0x10ca2, 0x10ca3,
+ 0x10ca4, 0x10ca5, 0x10ca6, 0x10ca7, 0x10ca8, 0x10ca9, 0x10caa, 0x10cab,
+ 0x10cac, 0x10cad, 0x10cae, 0x10caf, 0x10cb0, 0x10cb1, 0x10cb2, 0x118a0,
+ 0x118a1, 0x118a2, 0x118a3, 0x118a4, 0x118a5, 0x118a6, 0x118a7, 0x118a8,
+ 0x118a9, 0x118aa, 0x118ab, 0x118ac, 0x118ad, 0x118ae, 0x118af, 0x118b0,
+ 0x118b1, 0x118b2, 0x118b3, 0x118b4, 0x118b5, 0x118b6, 0x118b7, 0x118b8,
+ 0x118b9, 0x118ba, 0x118bb, 0x118bc, 0x118bd, 0x118be, 0x118bf, 0x1e900,
+ 0x1e901, 0x1e902, 0x1e903, 0x1e904, 0x1e905, 0x1e906, 0x1e907, 0x1e908,
+ 0x1e909, 0x1e90a, 0x1e90b, 0x1e90c, 0x1e90d, 0x1e90e, 0x1e90f, 0x1e910,
+ 0x1e911, 0x1e912, 0x1e913, 0x1e914, 0x1e915, 0x1e916, 0x1e917, 0x1e918,
+ 0x1e919, 0x1e91a, 0x1e91b, 0x1e91c, 0x1e91d, 0x1e91e, 0x1e91f, 0x1e920,
+ 0x1e921
+};
+static const uint16_t uni8_decomp_map[256] = {
+ 0x00000, 0x00001, 0x00002, 0x00003, 0x00004, 0x00005, 0x00006, 0x00007,
+ 0x00008, 0x00009, 0x0000a, 0x0000b, 0x0000c, 0x0000d, 0x0000e, 0x0000f,
+ 0x00010, 0x00011, 0x00012, 0x00013, 0x00014, 0x00015, 0x00016, 0x00017,
+ 0x00018, 0x00019, 0x0001a, 0x0001b, 0x0001c, 0x0001d, 0x0001e, 0x0001f,
+ 0x00020, 0x00021, 0x00022, 0x00023, 0x00024, 0x00025, 0x00026, 0x00027,
+ 0x00028, 0x00029, 0x0002a, 0x0002b, 0x0002c, 0x0002d, 0x0002e, 0x0002f,
+ 0x00030, 0x00031, 0x00032, 0x00033, 0x00034, 0x00035, 0x00036, 0x00037,
+ 0x00038, 0x00039, 0x0003a, 0x0003b, 0x0003c, 0x0003d, 0x0003e, 0x0003f,
+ 0x00040, 0x00041, 0x00042, 0x00043, 0x00044, 0x00045, 0x00046, 0x00047,
+ 0x00048, 0x00049, 0x0004a, 0x0004b, 0x0004c, 0x0004d, 0x0004e, 0x0004f,
+ 0x00050, 0x00051, 0x00052, 0x00053, 0x00054, 0x00055, 0x00056, 0x00057,
+ 0x00058, 0x00059, 0x0005a, 0x0005b, 0x0005c, 0x0005d, 0x0005e, 0x0005f,
+ 0x00060, 0x00061, 0x00062, 0x00063, 0x00064, 0x00065, 0x00066, 0x00067,
+ 0x00068, 0x00069, 0x0006a, 0x0006b, 0x0006c, 0x0006d, 0x0006e, 0x0006f,
+ 0x00070, 0x00071, 0x00072, 0x00073, 0x00074, 0x00075, 0x00076, 0x00077,
+ 0x00078, 0x00079, 0x0007a, 0x0007b, 0x0007c, 0x0007d, 0x0007e, 0x0007f,
+ 0x00080, 0x00081, 0x00082, 0x00083, 0x00084, 0x00085, 0x00086, 0x00087,
+ 0x00088, 0x00089, 0x0008a, 0x0008b, 0x0008c, 0x0008d, 0x0008e, 0x0008f,
+ 0x00090, 0x00091, 0x00092, 0x00093, 0x00094, 0x00095, 0x00096, 0x00097,
+ 0x00098, 0x00099, 0x0009a, 0x0009b, 0x0009c, 0x0009d, 0x0009e, 0x0009f,
+ 0x00020, 0x000a1, 0x000a2, 0x000a3, 0x000a4, 0x000a5, 0x000a6, 0x000a7,
+ 0x000a8, 0x000a9, 0x00061, 0x000ab, 0x000ac, 0x000ad, 0x000ae, 0x000af,
+ 0x000b0, 0x000b1, 0x00032, 0x00033, 0x000b4, 0x000b5, 0x000b6, 0x000b7,
+ 0x000b8, 0x00031, 0x0006f, 0x000bb, 0x000bc, 0x000bd, 0x000be, 0x000bf,
+ 0x000c0, 0x000c1, 0x000c2, 0x000c3, 0x000c4, 0x000c5, 0x000c6, 0x000c7,
+ 0x000c8, 0x000c9, 0x000ca, 0x000cb, 0x000cc, 0x000cd, 0x000ce, 0x000cf,
+ 0x000d0, 0x000d1, 0x000d2, 0x000d3, 0x000d4, 0x000d5, 0x000d6, 0x000d7,
+ 0x000d8, 0x000d9, 0x000da, 0x000db, 0x000dc, 0x000dd, 0x000de, 0x000df,
+ 0x000e0, 0x000e1, 0x000e2, 0x000e3, 0x000e4, 0x000e5, 0x000e6, 0x000e7,
+ 0x000e8, 0x000e9, 0x000ea, 0x000eb, 0x000ec, 0x000ed, 0x000ee, 0x000ef,
+ 0x000f0, 0x000f1, 0x000f2, 0x000f3, 0x000f4, 0x000f5, 0x000f6, 0x000f7,
+ 0x000f8, 0x000f9, 0x000fa, 0x000fb, 0x000fc, 0x000fd, 0x000fe, 0x000ff
+};
+static const uint16_t uni16_decomp_keys[] = {
+ 0x002b0, 0x002b1, 0x002b2, 0x002b3, 0x002b4, 0x002b5, 0x002b6, 0x002b7,
+ 0x002b8, 0x002e0, 0x002e1, 0x002e2, 0x002e3, 0x002e4, 0x00340, 0x00341,
+ 0x00343, 0x00374, 0x0037e, 0x00387, 0x003d2, 0x003f4, 0x003f9, 0x00f0c,
+ 0x010fc, 0x01d2c, 0x01d2d, 0x01d2e, 0x01d30, 0x01d31, 0x01d32, 0x01d33,
+ 0x01d34, 0x01d35, 0x01d36, 0x01d37, 0x01d38, 0x01d39, 0x01d3a, 0x01d3c,
+ 0x01d3d, 0x01d3e, 0x01d3f, 0x01d40, 0x01d41, 0x01d42, 0x01d43, 0x01d44,
+ 0x01d45, 0x01d46, 0x01d47, 0x01d48, 0x01d49, 0x01d4a, 0x01d4b, 0x01d4c,
+ 0x01d4d, 0x01d4f, 0x01d50, 0x01d51, 0x01d52, 0x01d53, 0x01d54, 0x01d55,
+ 0x01d56, 0x01d57, 0x01d58, 0x01d59, 0x01d5a, 0x01d5b, 0x01d5c, 0x01d5d,
+ 0x01d5e, 0x01d5f, 0x01d60, 0x01d61, 0x01d62, 0x01d63, 0x01d64, 0x01d65,
+ 0x01d66, 0x01d67, 0x01d68, 0x01d69, 0x01d6a, 0x01d78, 0x01d9b, 0x01d9c,
+ 0x01d9d, 0x01d9e, 0x01d9f, 0x01da0, 0x01da1, 0x01da2, 0x01da3, 0x01da4,
+ 0x01da5, 0x01da6, 0x01da7, 0x01da8, 0x01da9, 0x01daa, 0x01dab, 0x01dac,
+ 0x01dad, 0x01dae, 0x01daf, 0x01db0, 0x01db1, 0x01db2, 0x01db3, 0x01db4,
+ 0x01db5, 0x01db6, 0x01db7, 0x01db8, 0x01db9, 0x01dba, 0x01dbb, 0x01dbc,
+ 0x01dbd, 0x01dbe, 0x01dbf, 0x01fbb, 0x01fc9, 0x01fcb, 0x01fd3, 0x01fdb,
+ 0x01fe3, 0x01feb, 0x01fee, 0x01fef, 0x01ff9, 0x01ffb, 0x01ffd, 0x02000,
+ 0x02001, 0x02002, 0x02003, 0x02004, 0x02005, 0x02006, 0x02007, 0x02008,
+ 0x02009, 0x0200a, 0x02011, 0x02024, 0x0202f, 0x0205f, 0x02070, 0x02071,
+ 0x02074, 0x02075, 0x02076, 0x02077, 0x02078, 0x02079, 0x0207a, 0x0207b,
+ 0x0207c, 0x0207d, 0x0207e, 0x0207f, 0x02080, 0x02081, 0x02082, 0x02083,
+ 0x02084, 0x02085, 0x02086, 0x02087, 0x02088, 0x02089, 0x0208a, 0x0208b,
+ 0x0208c, 0x0208d, 0x0208e, 0x02090, 0x02091, 0x02092, 0x02093, 0x02094,
+ 0x02095, 0x02096, 0x02097, 0x02098, 0x02099, 0x0209a, 0x0209b, 0x0209c,
+ 0x02102, 0x02107, 0x0210a, 0x0210b, 0x0210c, 0x0210d, 0x0210e, 0x0210f,
+ 0x02110, 0x02111, 0x02112, 0x02113, 0x02115, 0x02119, 0x0211a, 0x0211b,
+ 0x0211c, 0x0211d, 0x02124, 0x02126, 0x02128, 0x0212a, 0x0212b, 0x0212c,
+ 0x0212d, 0x0212f, 0x02130, 0x02131, 0x02133, 0x02134, 0x02135, 0x02136,
+ 0x02137, 0x02138, 0x02139, 0x0213c, 0x0213d, 0x0213e, 0x0213f, 0x02140,
+ 0x02145, 0x02146, 0x02147, 0x02148, 0x02149, 0x02160, 0x02164, 0x02169,
+ 0x0216c, 0x0216d, 0x0216e, 0x0216f, 0x02329, 0x0232a, 0x02460, 0x02461,
+ 0x02462, 0x02463, 0x02464, 0x02465, 0x02466, 0x02467, 0x02468, 0x024b6,
+ 0x024b7, 0x024b8, 0x024b9, 0x024ba, 0x024bb, 0x024bc, 0x024bd, 0x024be,
+ 0x024bf, 0x024c0, 0x024c1, 0x024c2, 0x024c3, 0x024c4, 0x024c5, 0x024c6,
+ 0x024c7, 0x024c8, 0x024c9, 0x024ca, 0x024cb, 0x024cc, 0x024cd, 0x024ce,
+ 0x024cf, 0x024ea, 0x02c7c, 0x02c7d, 0x02d6f, 0x02e9f, 0x02ef3, 0x02f00,
+ 0x02f01, 0x02f02, 0x02f03, 0x02f04, 0x02f05, 0x02f06, 0x02f07, 0x02f08,
+ 0x02f09, 0x02f0a, 0x02f0b, 0x02f0c, 0x02f0d, 0x02f0e, 0x02f0f, 0x02f10,
+ 0x02f11, 0x02f12, 0x02f13, 0x02f14, 0x02f15, 0x02f16, 0x02f17, 0x02f18,
+ 0x02f19, 0x02f1a, 0x02f1b, 0x02f1c, 0x02f1d, 0x02f1e, 0x02f1f, 0x02f20,
+ 0x02f21, 0x02f22, 0x02f23, 0x02f24, 0x02f25, 0x02f26, 0x02f27, 0x02f28,
+ 0x02f29, 0x02f2a, 0x02f2b, 0x02f2c, 0x02f2d, 0x02f2e, 0x02f2f, 0x02f30,
+ 0x02f31, 0x02f32, 0x02f33, 0x02f34, 0x02f35, 0x02f36, 0x02f37, 0x02f38,
+ 0x02f39, 0x02f3a, 0x02f3b, 0x02f3c, 0x02f3d, 0x02f3e, 0x02f3f, 0x02f40,
+ 0x02f41, 0x02f42, 0x02f43, 0x02f44, 0x02f45, 0x02f46, 0x02f47, 0x02f48,
+ 0x02f49, 0x02f4a, 0x02f4b, 0x02f4c, 0x02f4d, 0x02f4e, 0x02f4f, 0x02f50,
+ 0x02f51, 0x02f52, 0x02f53, 0x02f54, 0x02f55, 0x02f56, 0x02f57, 0x02f58,
+ 0x02f59, 0x02f5a, 0x02f5b, 0x02f5c, 0x02f5d, 0x02f5e, 0x02f5f, 0x02f60,
+ 0x02f61, 0x02f62, 0x02f63, 0x02f64, 0x02f65, 0x02f66, 0x02f67, 0x02f68,
+ 0x02f69, 0x02f6a, 0x02f6b, 0x02f6c, 0x02f6d, 0x02f6e, 0x02f6f, 0x02f70,
+ 0x02f71, 0x02f72, 0x02f73, 0x02f74, 0x02f75, 0x02f76, 0x02f77, 0x02f78,
+ 0x02f79, 0x02f7a, 0x02f7b, 0x02f7c, 0x02f7d, 0x02f7e, 0x02f7f, 0x02f80,
+ 0x02f81, 0x02f82, 0x02f83, 0x02f84, 0x02f85, 0x02f86, 0x02f87, 0x02f88,
+ 0x02f89, 0x02f8a, 0x02f8b, 0x02f8c, 0x02f8d, 0x02f8e, 0x02f8f, 0x02f90,
+ 0x02f91, 0x02f92, 0x02f93, 0x02f94, 0x02f95, 0x02f96, 0x02f97, 0x02f98,
+ 0x02f99, 0x02f9a, 0x02f9b, 0x02f9c, 0x02f9d, 0x02f9e, 0x02f9f, 0x02fa0,
+ 0x02fa1, 0x02fa2, 0x02fa3, 0x02fa4, 0x02fa5, 0x02fa6, 0x02fa7, 0x02fa8,
+ 0x02fa9, 0x02faa, 0x02fab, 0x02fac, 0x02fad, 0x02fae, 0x02faf, 0x02fb0,
+ 0x02fb1, 0x02fb2, 0x02fb3, 0x02fb4, 0x02fb5, 0x02fb6, 0x02fb7, 0x02fb8,
+ 0x02fb9, 0x02fba, 0x02fbb, 0x02fbc, 0x02fbd, 0x02fbe, 0x02fbf, 0x02fc0,
+ 0x02fc1, 0x02fc2, 0x02fc3, 0x02fc4, 0x02fc5, 0x02fc6, 0x02fc7, 0x02fc8,
+ 0x02fc9, 0x02fca, 0x02fcb, 0x02fcc, 0x02fcd, 0x02fce, 0x02fcf, 0x02fd0,
+ 0x02fd1, 0x02fd2, 0x02fd3, 0x02fd4, 0x02fd5, 0x03000, 0x03036, 0x03038,
+ 0x03039, 0x0303a, 0x03131, 0x03132, 0x03133, 0x03134, 0x03135, 0x03136,
+ 0x03137, 0x03138, 0x03139, 0x0313a, 0x0313b, 0x0313c, 0x0313d, 0x0313e,
+ 0x0313f, 0x03140, 0x03141, 0x03142, 0x03143, 0x03144, 0x03145, 0x03146,
+ 0x03147, 0x03148, 0x03149, 0x0314a, 0x0314b, 0x0314c, 0x0314d, 0x0314e,
+ 0x0314f, 0x03150, 0x03151, 0x03152, 0x03153, 0x03154, 0x03155, 0x03156,
+ 0x03157, 0x03158, 0x03159, 0x0315a, 0x0315b, 0x0315c, 0x0315d, 0x0315e,
+ 0x0315f, 0x03160, 0x03161, 0x03162, 0x03163, 0x03164, 0x03165, 0x03166,
+ 0x03167, 0x03168, 0x03169, 0x0316a, 0x0316b, 0x0316c, 0x0316d, 0x0316e,
+ 0x0316f, 0x03170, 0x03171, 0x03172, 0x03173, 0x03174, 0x03175, 0x03176,
+ 0x03177, 0x03178, 0x03179, 0x0317a, 0x0317b, 0x0317c, 0x0317d, 0x0317e,
+ 0x0317f, 0x03180, 0x03181, 0x03182, 0x03183, 0x03184, 0x03185, 0x03186,
+ 0x03187, 0x03188, 0x03189, 0x0318a, 0x0318b, 0x0318c, 0x0318d, 0x0318e,
+ 0x03192, 0x03193, 0x03194, 0x03195, 0x03196, 0x03197, 0x03198, 0x03199,
+ 0x0319a, 0x0319b, 0x0319c, 0x0319d, 0x0319e, 0x0319f, 0x03244, 0x03245,
+ 0x03246, 0x03247, 0x03260, 0x03261, 0x03262, 0x03263, 0x03264, 0x03265,
+ 0x03266, 0x03267, 0x03268, 0x03269, 0x0326a, 0x0326b, 0x0326c, 0x0326d,
+ 0x03280, 0x03281, 0x03282, 0x03283, 0x03284, 0x03285, 0x03286, 0x03287,
+ 0x03288, 0x03289, 0x0328a, 0x0328b, 0x0328c, 0x0328d, 0x0328e, 0x0328f,
+ 0x03290, 0x03291, 0x03292, 0x03293, 0x03294, 0x03295, 0x03296, 0x03297,
+ 0x03298, 0x03299, 0x0329a, 0x0329b, 0x0329c, 0x0329d, 0x0329e, 0x0329f,
+ 0x032a0, 0x032a1, 0x032a2, 0x032a3, 0x032a4, 0x032a5, 0x032a6, 0x032a7,
+ 0x032a8, 0x032a9, 0x032aa, 0x032ab, 0x032ac, 0x032ad, 0x032ae, 0x032af,
+ 0x032b0, 0x032d0, 0x032d1, 0x032d2, 0x032d3, 0x032d4, 0x032d5, 0x032d6,
+ 0x032d7, 0x032d8, 0x032d9, 0x032da, 0x032db, 0x032dc, 0x032dd, 0x032de,
+ 0x032df, 0x032e0, 0x032e1, 0x032e2, 0x032e3, 0x032e4, 0x032e5, 0x032e6,
+ 0x032e7, 0x032e8, 0x032e9, 0x032ea, 0x032eb, 0x032ec, 0x032ed, 0x032ee,
+ 0x032ef, 0x032f0, 0x032f1, 0x032f2, 0x032f3, 0x032f4, 0x032f5, 0x032f6,
+ 0x032f7, 0x032f8, 0x032f9, 0x032fa, 0x032fb, 0x032fc, 0x032fd, 0x032fe,
+ 0x0a69c, 0x0a69d, 0x0a770, 0x0a7f8, 0x0a7f9, 0x0ab5c, 0x0ab5d, 0x0ab5e,
+ 0x0ab5f, 0x0f900, 0x0f901, 0x0f902, 0x0f903, 0x0f904, 0x0f905, 0x0f906,
+ 0x0f907, 0x0f908, 0x0f909, 0x0f90a, 0x0f90b, 0x0f90c, 0x0f90d, 0x0f90e,
+ 0x0f90f, 0x0f910, 0x0f911, 0x0f912, 0x0f913, 0x0f914, 0x0f915, 0x0f916,
+ 0x0f917, 0x0f918, 0x0f919, 0x0f91a, 0x0f91b, 0x0f91c, 0x0f91d, 0x0f91e,
+ 0x0f91f, 0x0f920, 0x0f921, 0x0f922, 0x0f923, 0x0f924, 0x0f925, 0x0f926,
+ 0x0f927, 0x0f928, 0x0f929, 0x0f92a, 0x0f92b, 0x0f92c, 0x0f92d, 0x0f92e,
+ 0x0f92f, 0x0f930, 0x0f931, 0x0f932, 0x0f933, 0x0f934, 0x0f935, 0x0f936,
+ 0x0f937, 0x0f938, 0x0f939, 0x0f93a, 0x0f93b, 0x0f93c, 0x0f93d, 0x0f93e,
+ 0x0f93f, 0x0f940, 0x0f941, 0x0f942, 0x0f943, 0x0f944, 0x0f945, 0x0f946,
+ 0x0f947, 0x0f948, 0x0f949, 0x0f94a, 0x0f94b, 0x0f94c, 0x0f94d, 0x0f94e,
+ 0x0f94f, 0x0f950, 0x0f951, 0x0f952, 0x0f953, 0x0f954, 0x0f955, 0x0f956,
+ 0x0f957, 0x0f958, 0x0f959, 0x0f95a, 0x0f95b, 0x0f95c, 0x0f95d, 0x0f95e,
+ 0x0f95f, 0x0f960, 0x0f961, 0x0f962, 0x0f963, 0x0f964, 0x0f965, 0x0f966,
+ 0x0f967, 0x0f968, 0x0f969, 0x0f96a, 0x0f96b, 0x0f96c, 0x0f96d, 0x0f96e,
+ 0x0f96f, 0x0f970, 0x0f971, 0x0f972, 0x0f973, 0x0f974, 0x0f975, 0x0f976,
+ 0x0f977, 0x0f978, 0x0f979, 0x0f97a, 0x0f97b, 0x0f97c, 0x0f97d, 0x0f97e,
+ 0x0f97f, 0x0f980, 0x0f981, 0x0f982, 0x0f983, 0x0f984, 0x0f985, 0x0f986,
+ 0x0f987, 0x0f988, 0x0f989, 0x0f98a, 0x0f98b, 0x0f98c, 0x0f98d, 0x0f98e,
+ 0x0f98f, 0x0f990, 0x0f991, 0x0f992, 0x0f993, 0x0f994, 0x0f995, 0x0f996,
+ 0x0f997, 0x0f998, 0x0f999, 0x0f99a, 0x0f99b, 0x0f99c, 0x0f99d, 0x0f99e,
+ 0x0f99f, 0x0f9a0, 0x0f9a1, 0x0f9a2, 0x0f9a3, 0x0f9a4, 0x0f9a5, 0x0f9a6,
+ 0x0f9a7, 0x0f9a8, 0x0f9a9, 0x0f9aa, 0x0f9ab, 0x0f9ac, 0x0f9ad, 0x0f9ae,
+ 0x0f9af, 0x0f9b0, 0x0f9b1, 0x0f9b2, 0x0f9b3, 0x0f9b4, 0x0f9b5, 0x0f9b6,
+ 0x0f9b7, 0x0f9b8, 0x0f9b9, 0x0f9ba, 0x0f9bb, 0x0f9bc, 0x0f9bd, 0x0f9be,
+ 0x0f9bf, 0x0f9c0, 0x0f9c1, 0x0f9c2, 0x0f9c3, 0x0f9c4, 0x0f9c5, 0x0f9c6,
+ 0x0f9c7, 0x0f9c8, 0x0f9c9, 0x0f9ca, 0x0f9cb, 0x0f9cc, 0x0f9cd, 0x0f9ce,
+ 0x0f9cf, 0x0f9d0, 0x0f9d1, 0x0f9d2, 0x0f9d3, 0x0f9d4, 0x0f9d5, 0x0f9d6,
+ 0x0f9d7, 0x0f9d8, 0x0f9d9, 0x0f9da, 0x0f9db, 0x0f9dc, 0x0f9dd, 0x0f9de,
+ 0x0f9df, 0x0f9e0, 0x0f9e1, 0x0f9e2, 0x0f9e3, 0x0f9e4, 0x0f9e5, 0x0f9e6,
+ 0x0f9e7, 0x0f9e8, 0x0f9e9, 0x0f9ea, 0x0f9eb, 0x0f9ec, 0x0f9ed, 0x0f9ee,
+ 0x0f9ef, 0x0f9f0, 0x0f9f1, 0x0f9f2, 0x0f9f3, 0x0f9f4, 0x0f9f5, 0x0f9f6,
+ 0x0f9f7, 0x0f9f8, 0x0f9f9, 0x0f9fa, 0x0f9fb, 0x0f9fc, 0x0f9fd, 0x0f9fe,
+ 0x0f9ff, 0x0fa00, 0x0fa01, 0x0fa02, 0x0fa03, 0x0fa04, 0x0fa05, 0x0fa06,
+ 0x0fa07, 0x0fa08, 0x0fa09, 0x0fa0a, 0x0fa0b, 0x0fa0c, 0x0fa0d, 0x0fa10,
+ 0x0fa12, 0x0fa15, 0x0fa16, 0x0fa17, 0x0fa18, 0x0fa19, 0x0fa1a, 0x0fa1b,
+ 0x0fa1c, 0x0fa1d, 0x0fa1e, 0x0fa20, 0x0fa22, 0x0fa25, 0x0fa26, 0x0fa2a,
+ 0x0fa2b, 0x0fa2c, 0x0fa2d, 0x0fa2e, 0x0fa2f, 0x0fa30, 0x0fa31, 0x0fa32,
+ 0x0fa33, 0x0fa34, 0x0fa35, 0x0fa36, 0x0fa37, 0x0fa38, 0x0fa39, 0x0fa3a,
+ 0x0fa3b, 0x0fa3c, 0x0fa3d, 0x0fa3e, 0x0fa3f, 0x0fa40, 0x0fa41, 0x0fa42,
+ 0x0fa43, 0x0fa44, 0x0fa45, 0x0fa46, 0x0fa47, 0x0fa48, 0x0fa49, 0x0fa4a,
+ 0x0fa4b, 0x0fa4c, 0x0fa4d, 0x0fa4e, 0x0fa4f, 0x0fa50, 0x0fa51, 0x0fa52,
+ 0x0fa53, 0x0fa54, 0x0fa55, 0x0fa56, 0x0fa57, 0x0fa58, 0x0fa59, 0x0fa5a,
+ 0x0fa5b, 0x0fa5c, 0x0fa5d, 0x0fa5e, 0x0fa5f, 0x0fa60, 0x0fa61, 0x0fa62,
+ 0x0fa63, 0x0fa64, 0x0fa65, 0x0fa66, 0x0fa67, 0x0fa68, 0x0fa69, 0x0fa6a,
+ 0x0fa6b, 0x0fa6c, 0x0fa6d, 0x0fa70, 0x0fa71, 0x0fa72, 0x0fa73, 0x0fa74,
+ 0x0fa75, 0x0fa76, 0x0fa77, 0x0fa78, 0x0fa79, 0x0fa7a, 0x0fa7b, 0x0fa7c,
+ 0x0fa7d, 0x0fa7e, 0x0fa7f, 0x0fa80, 0x0fa81, 0x0fa82, 0x0fa83, 0x0fa84,
+ 0x0fa85, 0x0fa86, 0x0fa87, 0x0fa88, 0x0fa89, 0x0fa8a, 0x0fa8b, 0x0fa8c,
+ 0x0fa8d, 0x0fa8e, 0x0fa8f, 0x0fa90, 0x0fa91, 0x0fa92, 0x0fa93, 0x0fa94,
+ 0x0fa95, 0x0fa96, 0x0fa97, 0x0fa98, 0x0fa99, 0x0fa9a, 0x0fa9b, 0x0fa9c,
+ 0x0fa9d, 0x0fa9e, 0x0fa9f, 0x0faa0, 0x0faa1, 0x0faa2, 0x0faa3, 0x0faa4,
+ 0x0faa5, 0x0faa6, 0x0faa7, 0x0faa8, 0x0faa9, 0x0faaa, 0x0faab, 0x0faac,
+ 0x0faad, 0x0faae, 0x0faaf, 0x0fab0, 0x0fab1, 0x0fab2, 0x0fab3, 0x0fab4,
+ 0x0fab5, 0x0fab6, 0x0fab7, 0x0fab8, 0x0fab9, 0x0faba, 0x0fabb, 0x0fabc,
+ 0x0fabd, 0x0fabe, 0x0fabf, 0x0fac0, 0x0fac1, 0x0fac2, 0x0fac3, 0x0fac4,
+ 0x0fac5, 0x0fac6, 0x0fac7, 0x0fac8, 0x0fac9, 0x0faca, 0x0facb, 0x0facc,
+ 0x0facd, 0x0face, 0x0facf, 0x0fad0, 0x0fad1, 0x0fad2, 0x0fad3, 0x0fad4,
+ 0x0fad5, 0x0fad6, 0x0fad7, 0x0fad8, 0x0fad9, 0x0fb20, 0x0fb21, 0x0fb22,
+ 0x0fb23, 0x0fb24, 0x0fb25, 0x0fb26, 0x0fb27, 0x0fb28, 0x0fb29, 0x0fb50,
+ 0x0fb51, 0x0fb52, 0x0fb53, 0x0fb54, 0x0fb55, 0x0fb56, 0x0fb57, 0x0fb58,
+ 0x0fb59, 0x0fb5a, 0x0fb5b, 0x0fb5c, 0x0fb5d, 0x0fb5e, 0x0fb5f, 0x0fb60,
+ 0x0fb61, 0x0fb62, 0x0fb63, 0x0fb64, 0x0fb65, 0x0fb66, 0x0fb67, 0x0fb68,
+ 0x0fb69, 0x0fb6a, 0x0fb6b, 0x0fb6c, 0x0fb6d, 0x0fb6e, 0x0fb6f, 0x0fb70,
+ 0x0fb71, 0x0fb72, 0x0fb73, 0x0fb74, 0x0fb75, 0x0fb76, 0x0fb77, 0x0fb78,
+ 0x0fb79, 0x0fb7a, 0x0fb7b, 0x0fb7c, 0x0fb7d, 0x0fb7e, 0x0fb7f, 0x0fb80,
+ 0x0fb81, 0x0fb82, 0x0fb83, 0x0fb84, 0x0fb85, 0x0fb86, 0x0fb87, 0x0fb88,
+ 0x0fb89, 0x0fb8a, 0x0fb8b, 0x0fb8c, 0x0fb8d, 0x0fb8e, 0x0fb8f, 0x0fb90,
+ 0x0fb91, 0x0fb92, 0x0fb93, 0x0fb94, 0x0fb95, 0x0fb96, 0x0fb97, 0x0fb98,
+ 0x0fb99, 0x0fb9a, 0x0fb9b, 0x0fb9c, 0x0fb9d, 0x0fb9e, 0x0fb9f, 0x0fba0,
+ 0x0fba1, 0x0fba2, 0x0fba3, 0x0fba4, 0x0fba5, 0x0fba6, 0x0fba7, 0x0fba8,
+ 0x0fba9, 0x0fbaa, 0x0fbab, 0x0fbac, 0x0fbad, 0x0fbae, 0x0fbaf, 0x0fbb0,
+ 0x0fbb1, 0x0fbd3, 0x0fbd4, 0x0fbd5, 0x0fbd6, 0x0fbd7, 0x0fbd8, 0x0fbd9,
+ 0x0fbda, 0x0fbdb, 0x0fbdc, 0x0fbdd, 0x0fbde, 0x0fbdf, 0x0fbe0, 0x0fbe1,
+ 0x0fbe2, 0x0fbe3, 0x0fbe4, 0x0fbe5, 0x0fbe6, 0x0fbe7, 0x0fbe8, 0x0fbe9,
+ 0x0fbfc, 0x0fbfd, 0x0fbfe, 0x0fbff, 0x0fe10, 0x0fe11, 0x0fe12, 0x0fe13,
+ 0x0fe14, 0x0fe15, 0x0fe16, 0x0fe17, 0x0fe18, 0x0fe19, 0x0fe30, 0x0fe31,
+ 0x0fe32, 0x0fe33, 0x0fe34, 0x0fe35, 0x0fe36, 0x0fe37, 0x0fe38, 0x0fe39,
+ 0x0fe3a, 0x0fe3b, 0x0fe3c, 0x0fe3d, 0x0fe3e, 0x0fe3f, 0x0fe40, 0x0fe41,
+ 0x0fe42, 0x0fe43, 0x0fe44, 0x0fe47, 0x0fe48, 0x0fe49, 0x0fe4a, 0x0fe4b,
+ 0x0fe4c, 0x0fe4d, 0x0fe4e, 0x0fe4f, 0x0fe50, 0x0fe51, 0x0fe52, 0x0fe54,
+ 0x0fe55, 0x0fe56, 0x0fe57, 0x0fe58, 0x0fe59, 0x0fe5a, 0x0fe5b, 0x0fe5c,
+ 0x0fe5d, 0x0fe5e, 0x0fe5f, 0x0fe60, 0x0fe61, 0x0fe62, 0x0fe63, 0x0fe64,
+ 0x0fe65, 0x0fe66, 0x0fe68, 0x0fe69, 0x0fe6a, 0x0fe6b, 0x0fe80, 0x0fe81,
+ 0x0fe82, 0x0fe83, 0x0fe84, 0x0fe85, 0x0fe86, 0x0fe87, 0x0fe88, 0x0fe89,
+ 0x0fe8a, 0x0fe8b, 0x0fe8c, 0x0fe8d, 0x0fe8e, 0x0fe8f, 0x0fe90, 0x0fe91,
+ 0x0fe92, 0x0fe93, 0x0fe94, 0x0fe95, 0x0fe96, 0x0fe97, 0x0fe98, 0x0fe99,
+ 0x0fe9a, 0x0fe9b, 0x0fe9c, 0x0fe9d, 0x0fe9e, 0x0fe9f, 0x0fea0, 0x0fea1,
+ 0x0fea2, 0x0fea3, 0x0fea4, 0x0fea5, 0x0fea6, 0x0fea7, 0x0fea8, 0x0fea9,
+ 0x0feaa, 0x0feab, 0x0feac, 0x0fead, 0x0feae, 0x0feaf, 0x0feb0, 0x0feb1,
+ 0x0feb2, 0x0feb3, 0x0feb4, 0x0feb5, 0x0feb6, 0x0feb7, 0x0feb8, 0x0feb9,
+ 0x0feba, 0x0febb, 0x0febc, 0x0febd, 0x0febe, 0x0febf, 0x0fec0, 0x0fec1,
+ 0x0fec2, 0x0fec3, 0x0fec4, 0x0fec5, 0x0fec6, 0x0fec7, 0x0fec8, 0x0fec9,
+ 0x0feca, 0x0fecb, 0x0fecc, 0x0fecd, 0x0fece, 0x0fecf, 0x0fed0, 0x0fed1,
+ 0x0fed2, 0x0fed3, 0x0fed4, 0x0fed5, 0x0fed6, 0x0fed7, 0x0fed8, 0x0fed9,
+ 0x0feda, 0x0fedb, 0x0fedc, 0x0fedd, 0x0fede, 0x0fedf, 0x0fee0, 0x0fee1,
+ 0x0fee2, 0x0fee3, 0x0fee4, 0x0fee5, 0x0fee6, 0x0fee7, 0x0fee8, 0x0fee9,
+ 0x0feea, 0x0feeb, 0x0feec, 0x0feed, 0x0feee, 0x0feef, 0x0fef0, 0x0fef1,
+ 0x0fef2, 0x0fef3, 0x0fef4, 0x0ff01, 0x0ff02, 0x0ff03, 0x0ff04, 0x0ff05,
+ 0x0ff06, 0x0ff07, 0x0ff08, 0x0ff09, 0x0ff0a, 0x0ff0b, 0x0ff0c, 0x0ff0d,
+ 0x0ff0e, 0x0ff0f, 0x0ff10, 0x0ff11, 0x0ff12, 0x0ff13, 0x0ff14, 0x0ff15,
+ 0x0ff16, 0x0ff17, 0x0ff18, 0x0ff19, 0x0ff1a, 0x0ff1b, 0x0ff1c, 0x0ff1d,
+ 0x0ff1e, 0x0ff1f, 0x0ff20, 0x0ff21, 0x0ff22, 0x0ff23, 0x0ff24, 0x0ff25,
+ 0x0ff26, 0x0ff27, 0x0ff28, 0x0ff29, 0x0ff2a, 0x0ff2b, 0x0ff2c, 0x0ff2d,
+ 0x0ff2e, 0x0ff2f, 0x0ff30, 0x0ff31, 0x0ff32, 0x0ff33, 0x0ff34, 0x0ff35,
+ 0x0ff36, 0x0ff37, 0x0ff38, 0x0ff39, 0x0ff3a, 0x0ff3b, 0x0ff3c, 0x0ff3d,
+ 0x0ff3e, 0x0ff3f, 0x0ff40, 0x0ff5b, 0x0ff5c, 0x0ff5d, 0x0ff5e, 0x0ff5f,
+ 0x0ff60, 0x0ff61, 0x0ff62, 0x0ff63, 0x0ff64, 0x0ff65, 0x0ff66, 0x0ff67,
+ 0x0ff68, 0x0ff69, 0x0ff6a, 0x0ff6b, 0x0ff6c, 0x0ff6d, 0x0ff6e, 0x0ff6f,
+ 0x0ff70, 0x0ff71, 0x0ff72, 0x0ff73, 0x0ff74, 0x0ff75, 0x0ff76, 0x0ff77,
+ 0x0ff78, 0x0ff79, 0x0ff7a, 0x0ff7b, 0x0ff7c, 0x0ff7d, 0x0ff7e, 0x0ff7f,
+ 0x0ff80, 0x0ff81, 0x0ff82, 0x0ff83, 0x0ff84, 0x0ff85, 0x0ff86, 0x0ff87,
+ 0x0ff88, 0x0ff89, 0x0ff8a, 0x0ff8b, 0x0ff8c, 0x0ff8d, 0x0ff8e, 0x0ff8f,
+ 0x0ff90, 0x0ff91, 0x0ff92, 0x0ff93, 0x0ff94, 0x0ff95, 0x0ff96, 0x0ff97,
+ 0x0ff98, 0x0ff99, 0x0ff9a, 0x0ff9b, 0x0ff9c, 0x0ff9d, 0x0ff9e, 0x0ff9f,
+ 0x0ffa0, 0x0ffa1, 0x0ffa2, 0x0ffa3, 0x0ffa4, 0x0ffa5, 0x0ffa6, 0x0ffa7,
+ 0x0ffa8, 0x0ffa9, 0x0ffaa, 0x0ffab, 0x0ffac, 0x0ffad, 0x0ffae, 0x0ffaf,
+ 0x0ffb0, 0x0ffb1, 0x0ffb2, 0x0ffb3, 0x0ffb4, 0x0ffb5, 0x0ffb6, 0x0ffb7,
+ 0x0ffb8, 0x0ffb9, 0x0ffba, 0x0ffbb, 0x0ffbc, 0x0ffbd, 0x0ffbe, 0x0ffc2,
+ 0x0ffc3, 0x0ffc4, 0x0ffc5, 0x0ffc6, 0x0ffc7, 0x0ffca, 0x0ffcb, 0x0ffcc,
+ 0x0ffcd, 0x0ffce, 0x0ffcf, 0x0ffd2, 0x0ffd3, 0x0ffd4, 0x0ffd5, 0x0ffd6,
+ 0x0ffd7, 0x0ffda, 0x0ffdb, 0x0ffdc, 0x0ffe0, 0x0ffe1, 0x0ffe2, 0x0ffe3,
+ 0x0ffe4, 0x0ffe5, 0x0ffe6, 0x0ffe8, 0x0ffe9, 0x0ffea, 0x0ffeb, 0x0ffec,
+ 0x0ffed, 0x0ffee
+};
+static const uint32_t uni16_decomp_values[] = {
+ 0x00068, 0x00266, 0x0006a, 0x00072, 0x00279, 0x0027b, 0x00281, 0x00077,
+ 0x00079, 0x00263, 0x0006c, 0x00073, 0x00078, 0x00295, 0x00300, 0x00301,
+ 0x00313, 0x002b9, 0x0003b, 0x000b7, 0x003a5, 0x00398, 0x003a3, 0x00f0b,
+ 0x010dc, 0x00041, 0x000c6, 0x00042, 0x00044, 0x00045, 0x0018e, 0x00047,
+ 0x00048, 0x00049, 0x0004a, 0x0004b, 0x0004c, 0x0004d, 0x0004e, 0x0004f,
+ 0x00222, 0x00050, 0x00052, 0x00054, 0x00055, 0x00057, 0x00061, 0x00250,
+ 0x00251, 0x01d02, 0x00062, 0x00064, 0x00065, 0x00259, 0x0025b, 0x0025c,
+ 0x00067, 0x0006b, 0x0006d, 0x0014b, 0x0006f, 0x00254, 0x01d16, 0x01d17,
+ 0x00070, 0x00074, 0x00075, 0x01d1d, 0x0026f, 0x00076, 0x01d25, 0x003b2,
+ 0x003b3, 0x003b4, 0x003c6, 0x003c7, 0x00069, 0x00072, 0x00075, 0x00076,
+ 0x003b2, 0x003b3, 0x003c1, 0x003c6, 0x003c7, 0x0043d, 0x00252, 0x00063,
+ 0x00255, 0x000f0, 0x0025c, 0x00066, 0x0025f, 0x00261, 0x00265, 0x00268,
+ 0x00269, 0x0026a, 0x01d7b, 0x0029d, 0x0026d, 0x01d85, 0x0029f, 0x00271,
+ 0x00270, 0x00272, 0x00273, 0x00274, 0x00275, 0x00278, 0x00282, 0x00283,
+ 0x001ab, 0x00289, 0x0028a, 0x01d1c, 0x0028b, 0x0028c, 0x0007a, 0x00290,
+ 0x00291, 0x00292, 0x003b8, 0x00386, 0x00388, 0x00389, 0x00390, 0x0038a,
+ 0x003b0, 0x0038e, 0x00385, 0x00060, 0x0038c, 0x0038f, 0x000b4, 0x02002,
+ 0x02003, 0x00020, 0x00020, 0x00020, 0x00020, 0x00020, 0x00020, 0x00020,
+ 0x00020, 0x00020, 0x02010, 0x0002e, 0x00020, 0x00020, 0x00030, 0x00069,
+ 0x00034, 0x00035, 0x00036, 0x00037, 0x00038, 0x00039, 0x0002b, 0x02212,
+ 0x0003d, 0x00028, 0x00029, 0x0006e, 0x00030, 0x00031, 0x00032, 0x00033,
+ 0x00034, 0x00035, 0x00036, 0x00037, 0x00038, 0x00039, 0x0002b, 0x02212,
+ 0x0003d, 0x00028, 0x00029, 0x00061, 0x00065, 0x0006f, 0x00078, 0x00259,
+ 0x00068, 0x0006b, 0x0006c, 0x0006d, 0x0006e, 0x00070, 0x00073, 0x00074,
+ 0x00043, 0x00190, 0x00067, 0x00048, 0x00048, 0x00048, 0x00068, 0x00127,
+ 0x00049, 0x00049, 0x0004c, 0x0006c, 0x0004e, 0x00050, 0x00051, 0x00052,
+ 0x00052, 0x00052, 0x0005a, 0x003a9, 0x0005a, 0x0004b, 0x000c5, 0x00042,
+ 0x00043, 0x00065, 0x00045, 0x00046, 0x0004d, 0x0006f, 0x005d0, 0x005d1,
+ 0x005d2, 0x005d3, 0x00069, 0x003c0, 0x003b3, 0x00393, 0x003a0, 0x02211,
+ 0x00044, 0x00064, 0x00065, 0x00069, 0x0006a, 0x00049, 0x00056, 0x00058,
+ 0x0004c, 0x00043, 0x00044, 0x0004d, 0x03008, 0x03009, 0x00031, 0x00032,
+ 0x00033, 0x00034, 0x00035, 0x00036, 0x00037, 0x00038, 0x00039, 0x00041,
+ 0x00042, 0x00043, 0x00044, 0x00045, 0x00046, 0x00047, 0x00048, 0x00049,
+ 0x0004a, 0x0004b, 0x0004c, 0x0004d, 0x0004e, 0x0004f, 0x00050, 0x00051,
+ 0x00052, 0x00053, 0x00054, 0x00055, 0x00056, 0x00057, 0x00058, 0x00059,
+ 0x0005a, 0x00030, 0x0006a, 0x00056, 0x02d61, 0x06bcd, 0x09f9f, 0x04e00,
+ 0x04e28, 0x04e36, 0x04e3f, 0x04e59, 0x04e85, 0x04e8c, 0x04ea0, 0x04eba,
+ 0x0513f, 0x05165, 0x0516b, 0x05182, 0x05196, 0x051ab, 0x051e0, 0x051f5,
+ 0x05200, 0x0529b, 0x052f9, 0x05315, 0x0531a, 0x05338, 0x05341, 0x0535c,
+ 0x05369, 0x05382, 0x053b6, 0x053c8, 0x053e3, 0x056d7, 0x0571f, 0x058eb,
+ 0x05902, 0x0590a, 0x05915, 0x05927, 0x05973, 0x05b50, 0x05b80, 0x05bf8,
+ 0x05c0f, 0x05c22, 0x05c38, 0x05c6e, 0x05c71, 0x05ddb, 0x05de5, 0x05df1,
+ 0x05dfe, 0x05e72, 0x05e7a, 0x05e7f, 0x05ef4, 0x05efe, 0x05f0b, 0x05f13,
+ 0x05f50, 0x05f61, 0x05f73, 0x05fc3, 0x06208, 0x06236, 0x0624b, 0x0652f,
+ 0x06534, 0x06587, 0x06597, 0x065a4, 0x065b9, 0x065e0, 0x065e5, 0x066f0,
+ 0x06708, 0x06728, 0x06b20, 0x06b62, 0x06b79, 0x06bb3, 0x06bcb, 0x06bd4,
+ 0x06bdb, 0x06c0f, 0x06c14, 0x06c34, 0x0706b, 0x0722a, 0x07236, 0x0723b,
+ 0x0723f, 0x07247, 0x07259, 0x0725b, 0x072ac, 0x07384, 0x07389, 0x074dc,
+ 0x074e6, 0x07518, 0x0751f, 0x07528, 0x07530, 0x0758b, 0x07592, 0x07676,
+ 0x0767d, 0x076ae, 0x076bf, 0x076ee, 0x077db, 0x077e2, 0x077f3, 0x0793a,
+ 0x079b8, 0x079be, 0x07a74, 0x07acb, 0x07af9, 0x07c73, 0x07cf8, 0x07f36,
+ 0x07f51, 0x07f8a, 0x07fbd, 0x08001, 0x0800c, 0x08012, 0x08033, 0x0807f,
+ 0x08089, 0x081e3, 0x081ea, 0x081f3, 0x081fc, 0x0820c, 0x0821b, 0x0821f,
+ 0x0826e, 0x08272, 0x08278, 0x0864d, 0x0866b, 0x08840, 0x0884c, 0x08863,
+ 0x0897e, 0x0898b, 0x089d2, 0x08a00, 0x08c37, 0x08c46, 0x08c55, 0x08c78,
+ 0x08c9d, 0x08d64, 0x08d70, 0x08db3, 0x08eab, 0x08eca, 0x08f9b, 0x08fb0,
+ 0x08fb5, 0x09091, 0x09149, 0x091c6, 0x091cc, 0x091d1, 0x09577, 0x09580,
+ 0x0961c, 0x096b6, 0x096b9, 0x096e8, 0x09751, 0x0975e, 0x09762, 0x09769,
+ 0x097cb, 0x097ed, 0x097f3, 0x09801, 0x098a8, 0x098db, 0x098df, 0x09996,
+ 0x09999, 0x099ac, 0x09aa8, 0x09ad8, 0x09adf, 0x09b25, 0x09b2f, 0x09b32,
+ 0x09b3c, 0x09b5a, 0x09ce5, 0x09e75, 0x09e7f, 0x09ea5, 0x09ebb, 0x09ec3,
+ 0x09ecd, 0x09ed1, 0x09ef9, 0x09efd, 0x09f0e, 0x09f13, 0x09f20, 0x09f3b,
+ 0x09f4a, 0x09f52, 0x09f8d, 0x09f9c, 0x09fa0, 0x00020, 0x03012, 0x05341,
+ 0x05344, 0x05345, 0x01100, 0x01101, 0x011aa, 0x01102, 0x011ac, 0x011ad,
+ 0x01103, 0x01104, 0x01105, 0x011b0, 0x011b1, 0x011b2, 0x011b3, 0x011b4,
+ 0x011b5, 0x0111a, 0x01106, 0x01107, 0x01108, 0x01121, 0x01109, 0x0110a,
+ 0x0110b, 0x0110c, 0x0110d, 0x0110e, 0x0110f, 0x01110, 0x01111, 0x01112,
+ 0x01161, 0x01162, 0x01163, 0x01164, 0x01165, 0x01166, 0x01167, 0x01168,
+ 0x01169, 0x0116a, 0x0116b, 0x0116c, 0x0116d, 0x0116e, 0x0116f, 0x01170,
+ 0x01171, 0x01172, 0x01173, 0x01174, 0x01175, 0x01160, 0x01114, 0x01115,
+ 0x011c7, 0x011c8, 0x011cc, 0x011ce, 0x011d3, 0x011d7, 0x011d9, 0x0111c,
+ 0x011dd, 0x011df, 0x0111d, 0x0111e, 0x01120, 0x01122, 0x01123, 0x01127,
+ 0x01129, 0x0112b, 0x0112c, 0x0112d, 0x0112e, 0x0112f, 0x01132, 0x01136,
+ 0x01140, 0x01147, 0x0114c, 0x011f1, 0x011f2, 0x01157, 0x01158, 0x01159,
+ 0x01184, 0x01185, 0x01188, 0x01191, 0x01192, 0x01194, 0x0119e, 0x011a1,
+ 0x04e00, 0x04e8c, 0x04e09, 0x056db, 0x04e0a, 0x04e2d, 0x04e0b, 0x07532,
+ 0x04e59, 0x04e19, 0x04e01, 0x05929, 0x05730, 0x04eba, 0x0554f, 0x05e7c,
+ 0x06587, 0x07b8f, 0x01100, 0x01102, 0x01103, 0x01105, 0x01106, 0x01107,
+ 0x01109, 0x0110b, 0x0110c, 0x0110e, 0x0110f, 0x01110, 0x01111, 0x01112,
+ 0x04e00, 0x04e8c, 0x04e09, 0x056db, 0x04e94, 0x0516d, 0x04e03, 0x0516b,
+ 0x04e5d, 0x05341, 0x06708, 0x0706b, 0x06c34, 0x06728, 0x091d1, 0x0571f,
+ 0x065e5, 0x0682a, 0x06709, 0x0793e, 0x0540d, 0x07279, 0x08ca1, 0x0795d,
+ 0x052b4, 0x079d8, 0x07537, 0x05973, 0x09069, 0x0512a, 0x05370, 0x06ce8,
+ 0x09805, 0x04f11, 0x05199, 0x06b63, 0x04e0a, 0x04e2d, 0x04e0b, 0x05de6,
+ 0x053f3, 0x0533b, 0x05b97, 0x05b66, 0x076e3, 0x04f01, 0x08cc7, 0x05354,
+ 0x0591c, 0x030a2, 0x030a4, 0x030a6, 0x030a8, 0x030aa, 0x030ab, 0x030ad,
+ 0x030af, 0x030b1, 0x030b3, 0x030b5, 0x030b7, 0x030b9, 0x030bb, 0x030bd,
+ 0x030bf, 0x030c1, 0x030c4, 0x030c6, 0x030c8, 0x030ca, 0x030cb, 0x030cc,
+ 0x030cd, 0x030ce, 0x030cf, 0x030d2, 0x030d5, 0x030d8, 0x030db, 0x030de,
+ 0x030df, 0x030e0, 0x030e1, 0x030e2, 0x030e4, 0x030e6, 0x030e8, 0x030e9,
+ 0x030ea, 0x030eb, 0x030ec, 0x030ed, 0x030ef, 0x030f0, 0x030f1, 0x030f2,
+ 0x0044a, 0x0044c, 0x0a76f, 0x00126, 0x00153, 0x0a727, 0x0ab37, 0x0026b,
+ 0x0ab52, 0x08c48, 0x066f4, 0x08eca, 0x08cc8, 0x06ed1, 0x04e32, 0x053e5,
+ 0x09f9c, 0x09f9c, 0x05951, 0x091d1, 0x05587, 0x05948, 0x061f6, 0x07669,
+ 0x07f85, 0x0863f, 0x087ba, 0x088f8, 0x0908f, 0x06a02, 0x06d1b, 0x070d9,
+ 0x073de, 0x0843d, 0x0916a, 0x099f1, 0x04e82, 0x05375, 0x06b04, 0x0721b,
+ 0x0862d, 0x09e1e, 0x05d50, 0x06feb, 0x085cd, 0x08964, 0x062c9, 0x081d8,
+ 0x0881f, 0x05eca, 0x06717, 0x06d6a, 0x072fc, 0x090ce, 0x04f86, 0x051b7,
+ 0x052de, 0x064c4, 0x06ad3, 0x07210, 0x076e7, 0x08001, 0x08606, 0x0865c,
+ 0x08def, 0x09732, 0x09b6f, 0x09dfa, 0x0788c, 0x0797f, 0x07da0, 0x083c9,
+ 0x09304, 0x09e7f, 0x08ad6, 0x058df, 0x05f04, 0x07c60, 0x0807e, 0x07262,
+ 0x078ca, 0x08cc2, 0x096f7, 0x058d8, 0x05c62, 0x06a13, 0x06dda, 0x06f0f,
+ 0x07d2f, 0x07e37, 0x0964b, 0x052d2, 0x0808b, 0x051dc, 0x051cc, 0x07a1c,
+ 0x07dbe, 0x083f1, 0x09675, 0x08b80, 0x062cf, 0x06a02, 0x08afe, 0x04e39,
+ 0x05be7, 0x06012, 0x07387, 0x07570, 0x05317, 0x078fb, 0x04fbf, 0x05fa9,
+ 0x04e0d, 0x06ccc, 0x06578, 0x07d22, 0x053c3, 0x0585e, 0x07701, 0x08449,
+ 0x08aaa, 0x06bba, 0x08fb0, 0x06c88, 0x062fe, 0x082e5, 0x063a0, 0x07565,
+ 0x04eae, 0x05169, 0x051c9, 0x06881, 0x07ce7, 0x0826f, 0x08ad2, 0x091cf,
+ 0x052f5, 0x05442, 0x05973, 0x05eec, 0x065c5, 0x06ffe, 0x0792a, 0x095ad,
+ 0x09a6a, 0x09e97, 0x09ece, 0x0529b, 0x066c6, 0x06b77, 0x08f62, 0x05e74,
+ 0x06190, 0x06200, 0x0649a, 0x06f23, 0x07149, 0x07489, 0x079ca, 0x07df4,
+ 0x0806f, 0x08f26, 0x084ee, 0x09023, 0x0934a, 0x05217, 0x052a3, 0x054bd,
+ 0x070c8, 0x088c2, 0x08aaa, 0x05ec9, 0x05ff5, 0x0637b, 0x06bae, 0x07c3e,
+ 0x07375, 0x04ee4, 0x056f9, 0x05be7, 0x05dba, 0x0601c, 0x073b2, 0x07469,
+ 0x07f9a, 0x08046, 0x09234, 0x096f6, 0x09748, 0x09818, 0x04f8b, 0x079ae,
+ 0x091b4, 0x096b8, 0x060e1, 0x04e86, 0x050da, 0x05bee, 0x05c3f, 0x06599,
+ 0x06a02, 0x071ce, 0x07642, 0x084fc, 0x0907c, 0x09f8d, 0x06688, 0x0962e,
+ 0x05289, 0x0677b, 0x067f3, 0x06d41, 0x06e9c, 0x07409, 0x07559, 0x0786b,
+ 0x07d10, 0x0985e, 0x0516d, 0x0622e, 0x09678, 0x0502b, 0x05d19, 0x06dea,
+ 0x08f2a, 0x05f8b, 0x06144, 0x06817, 0x07387, 0x09686, 0x05229, 0x0540f,
+ 0x05c65, 0x06613, 0x0674e, 0x068a8, 0x06ce5, 0x07406, 0x075e2, 0x07f79,
+ 0x088cf, 0x088e1, 0x091cc, 0x096e2, 0x0533f, 0x06eba, 0x0541d, 0x071d0,
+ 0x07498, 0x085fa, 0x096a3, 0x09c57, 0x09e9f, 0x06797, 0x06dcb, 0x081e8,
+ 0x07acb, 0x07b20, 0x07c92, 0x072c0, 0x07099, 0x08b58, 0x04ec0, 0x08336,
+ 0x0523a, 0x05207, 0x05ea6, 0x062d3, 0x07cd6, 0x05b85, 0x06d1e, 0x066b4,
+ 0x08f3b, 0x0884c, 0x0964d, 0x0898b, 0x05ed3, 0x05140, 0x055c0, 0x0585a,
+ 0x06674, 0x051de, 0x0732a, 0x076ca, 0x0793c, 0x0795e, 0x07965, 0x0798f,
+ 0x09756, 0x07cbe, 0x07fbd, 0x08612, 0x08af8, 0x09038, 0x090fd, 0x098ef,
+ 0x098fc, 0x09928, 0x09db4, 0x090de, 0x096b7, 0x04fae, 0x050e7, 0x0514d,
+ 0x052c9, 0x052e4, 0x05351, 0x0559d, 0x05606, 0x05668, 0x05840, 0x058a8,
+ 0x05c64, 0x05c6e, 0x06094, 0x06168, 0x0618e, 0x061f2, 0x0654f, 0x065e2,
+ 0x06691, 0x06885, 0x06d77, 0x06e1a, 0x06f22, 0x0716e, 0x0722b, 0x07422,
+ 0x07891, 0x0793e, 0x07949, 0x07948, 0x07950, 0x07956, 0x0795d, 0x0798d,
+ 0x0798e, 0x07a40, 0x07a81, 0x07bc0, 0x07df4, 0x07e09, 0x07e41, 0x07f72,
+ 0x08005, 0x081ed, 0x08279, 0x08279, 0x08457, 0x08910, 0x08996, 0x08b01,
+ 0x08b39, 0x08cd3, 0x08d08, 0x08fb6, 0x09038, 0x096e3, 0x097ff, 0x0983b,
+ 0x06075, 0x242ee, 0x08218, 0x04e26, 0x051b5, 0x05168, 0x04f80, 0x05145,
+ 0x05180, 0x052c7, 0x052fa, 0x0559d, 0x05555, 0x05599, 0x055e2, 0x0585a,
+ 0x058b3, 0x05944, 0x05954, 0x05a62, 0x05b28, 0x05ed2, 0x05ed9, 0x05f69,
+ 0x05fad, 0x060d8, 0x0614e, 0x06108, 0x0618e, 0x06160, 0x061f2, 0x06234,
+ 0x063c4, 0x0641c, 0x06452, 0x06556, 0x06674, 0x06717, 0x0671b, 0x06756,
+ 0x06b79, 0x06bba, 0x06d41, 0x06edb, 0x06ecb, 0x06f22, 0x0701e, 0x0716e,
+ 0x077a7, 0x07235, 0x072af, 0x0732a, 0x07471, 0x07506, 0x0753b, 0x0761d,
+ 0x0761f, 0x076ca, 0x076db, 0x076f4, 0x0774a, 0x07740, 0x078cc, 0x07ab1,
+ 0x07bc0, 0x07c7b, 0x07d5b, 0x07df4, 0x07f3e, 0x08005, 0x08352, 0x083ef,
+ 0x08779, 0x08941, 0x08986, 0x08996, 0x08abf, 0x08af8, 0x08acb, 0x08b01,
+ 0x08afe, 0x08aed, 0x08b39, 0x08b8a, 0x08d08, 0x08f38, 0x09072, 0x09199,
+ 0x09276, 0x0967c, 0x096e3, 0x09756, 0x097db, 0x097ff, 0x0980b, 0x0983b,
+ 0x09b12, 0x09f9c, 0x2284a, 0x22844, 0x233d5, 0x03b9d, 0x04018, 0x04039,
+ 0x25249, 0x25cd0, 0x27ed3, 0x09f43, 0x09f8e, 0x005e2, 0x005d0, 0x005d3,
+ 0x005d4, 0x005db, 0x005dc, 0x005dd, 0x005e8, 0x005ea, 0x0002b, 0x00671,
+ 0x00671, 0x0067b, 0x0067b, 0x0067b, 0x0067b, 0x0067e, 0x0067e, 0x0067e,
+ 0x0067e, 0x00680, 0x00680, 0x00680, 0x00680, 0x0067a, 0x0067a, 0x0067a,
+ 0x0067a, 0x0067f, 0x0067f, 0x0067f, 0x0067f, 0x00679, 0x00679, 0x00679,
+ 0x00679, 0x006a4, 0x006a4, 0x006a4, 0x006a4, 0x006a6, 0x006a6, 0x006a6,
+ 0x006a6, 0x00684, 0x00684, 0x00684, 0x00684, 0x00683, 0x00683, 0x00683,
+ 0x00683, 0x00686, 0x00686, 0x00686, 0x00686, 0x00687, 0x00687, 0x00687,
+ 0x00687, 0x0068d, 0x0068d, 0x0068c, 0x0068c, 0x0068e, 0x0068e, 0x00688,
+ 0x00688, 0x00698, 0x00698, 0x00691, 0x00691, 0x006a9, 0x006a9, 0x006a9,
+ 0x006a9, 0x006af, 0x006af, 0x006af, 0x006af, 0x006b3, 0x006b3, 0x006b3,
+ 0x006b3, 0x006b1, 0x006b1, 0x006b1, 0x006b1, 0x006ba, 0x006ba, 0x006bb,
+ 0x006bb, 0x006bb, 0x006bb, 0x006c0, 0x006c0, 0x006c1, 0x006c1, 0x006c1,
+ 0x006c1, 0x006be, 0x006be, 0x006be, 0x006be, 0x006d2, 0x006d2, 0x006d3,
+ 0x006d3, 0x006ad, 0x006ad, 0x006ad, 0x006ad, 0x006c7, 0x006c7, 0x006c6,
+ 0x006c6, 0x006c8, 0x006c8, 0x00677, 0x006cb, 0x006cb, 0x006c5, 0x006c5,
+ 0x006c9, 0x006c9, 0x006d0, 0x006d0, 0x006d0, 0x006d0, 0x00649, 0x00649,
+ 0x006cc, 0x006cc, 0x006cc, 0x006cc, 0x0002c, 0x03001, 0x03002, 0x0003a,
+ 0x0003b, 0x00021, 0x0003f, 0x03016, 0x03017, 0x02026, 0x02025, 0x02014,
+ 0x02013, 0x0005f, 0x0005f, 0x00028, 0x00029, 0x0007b, 0x0007d, 0x03014,
+ 0x03015, 0x03010, 0x03011, 0x0300a, 0x0300b, 0x03008, 0x03009, 0x0300c,
+ 0x0300d, 0x0300e, 0x0300f, 0x0005b, 0x0005d, 0x0203e, 0x0203e, 0x0203e,
+ 0x0203e, 0x0005f, 0x0005f, 0x0005f, 0x0002c, 0x03001, 0x0002e, 0x0003b,
+ 0x0003a, 0x0003f, 0x00021, 0x02014, 0x00028, 0x00029, 0x0007b, 0x0007d,
+ 0x03014, 0x03015, 0x00023, 0x00026, 0x0002a, 0x0002b, 0x0002d, 0x0003c,
+ 0x0003e, 0x0003d, 0x0005c, 0x00024, 0x00025, 0x00040, 0x00621, 0x00622,
+ 0x00622, 0x00623, 0x00623, 0x00624, 0x00624, 0x00625, 0x00625, 0x00626,
+ 0x00626, 0x00626, 0x00626, 0x00627, 0x00627, 0x00628, 0x00628, 0x00628,
+ 0x00628, 0x00629, 0x00629, 0x0062a, 0x0062a, 0x0062a, 0x0062a, 0x0062b,
+ 0x0062b, 0x0062b, 0x0062b, 0x0062c, 0x0062c, 0x0062c, 0x0062c, 0x0062d,
+ 0x0062d, 0x0062d, 0x0062d, 0x0062e, 0x0062e, 0x0062e, 0x0062e, 0x0062f,
+ 0x0062f, 0x00630, 0x00630, 0x00631, 0x00631, 0x00632, 0x00632, 0x00633,
+ 0x00633, 0x00633, 0x00633, 0x00634, 0x00634, 0x00634, 0x00634, 0x00635,
+ 0x00635, 0x00635, 0x00635, 0x00636, 0x00636, 0x00636, 0x00636, 0x00637,
+ 0x00637, 0x00637, 0x00637, 0x00638, 0x00638, 0x00638, 0x00638, 0x00639,
+ 0x00639, 0x00639, 0x00639, 0x0063a, 0x0063a, 0x0063a, 0x0063a, 0x00641,
+ 0x00641, 0x00641, 0x00641, 0x00642, 0x00642, 0x00642, 0x00642, 0x00643,
+ 0x00643, 0x00643, 0x00643, 0x00644, 0x00644, 0x00644, 0x00644, 0x00645,
+ 0x00645, 0x00645, 0x00645, 0x00646, 0x00646, 0x00646, 0x00646, 0x00647,
+ 0x00647, 0x00647, 0x00647, 0x00648, 0x00648, 0x00649, 0x00649, 0x0064a,
+ 0x0064a, 0x0064a, 0x0064a, 0x00021, 0x00022, 0x00023, 0x00024, 0x00025,
+ 0x00026, 0x00027, 0x00028, 0x00029, 0x0002a, 0x0002b, 0x0002c, 0x0002d,
+ 0x0002e, 0x0002f, 0x00030, 0x00031, 0x00032, 0x00033, 0x00034, 0x00035,
+ 0x00036, 0x00037, 0x00038, 0x00039, 0x0003a, 0x0003b, 0x0003c, 0x0003d,
+ 0x0003e, 0x0003f, 0x00040, 0x00041, 0x00042, 0x00043, 0x00044, 0x00045,
+ 0x00046, 0x00047, 0x00048, 0x00049, 0x0004a, 0x0004b, 0x0004c, 0x0004d,
+ 0x0004e, 0x0004f, 0x00050, 0x00051, 0x00052, 0x00053, 0x00054, 0x00055,
+ 0x00056, 0x00057, 0x00058, 0x00059, 0x0005a, 0x0005b, 0x0005c, 0x0005d,
+ 0x0005e, 0x0005f, 0x00060, 0x0007b, 0x0007c, 0x0007d, 0x0007e, 0x02985,
+ 0x02986, 0x03002, 0x0300c, 0x0300d, 0x03001, 0x030fb, 0x030f2, 0x030a1,
+ 0x030a3, 0x030a5, 0x030a7, 0x030a9, 0x030e3, 0x030e5, 0x030e7, 0x030c3,
+ 0x030fc, 0x030a2, 0x030a4, 0x030a6, 0x030a8, 0x030aa, 0x030ab, 0x030ad,
+ 0x030af, 0x030b1, 0x030b3, 0x030b5, 0x030b7, 0x030b9, 0x030bb, 0x030bd,
+ 0x030bf, 0x030c1, 0x030c4, 0x030c6, 0x030c8, 0x030ca, 0x030cb, 0x030cc,
+ 0x030cd, 0x030ce, 0x030cf, 0x030d2, 0x030d5, 0x030d8, 0x030db, 0x030de,
+ 0x030df, 0x030e0, 0x030e1, 0x030e2, 0x030e4, 0x030e6, 0x030e8, 0x030e9,
+ 0x030ea, 0x030eb, 0x030ec, 0x030ed, 0x030ef, 0x030f3, 0x03099, 0x0309a,
+ 0x03164, 0x03131, 0x03132, 0x03133, 0x03134, 0x03135, 0x03136, 0x03137,
+ 0x03138, 0x03139, 0x0313a, 0x0313b, 0x0313c, 0x0313d, 0x0313e, 0x0313f,
+ 0x03140, 0x03141, 0x03142, 0x03143, 0x03144, 0x03145, 0x03146, 0x03147,
+ 0x03148, 0x03149, 0x0314a, 0x0314b, 0x0314c, 0x0314d, 0x0314e, 0x0314f,
+ 0x03150, 0x03151, 0x03152, 0x03153, 0x03154, 0x03155, 0x03156, 0x03157,
+ 0x03158, 0x03159, 0x0315a, 0x0315b, 0x0315c, 0x0315d, 0x0315e, 0x0315f,
+ 0x03160, 0x03161, 0x03162, 0x03163, 0x000a2, 0x000a3, 0x000ac, 0x000af,
+ 0x000a6, 0x000a5, 0x020a9, 0x02502, 0x02190, 0x02191, 0x02192, 0x02193,
+ 0x025a0, 0x025cb
+};
+static const uint32_t uni32_decomp_keys[] = {
+ 0x1d400, 0x1d401, 0x1d402, 0x1d403, 0x1d404, 0x1d405, 0x1d406, 0x1d407,
+ 0x1d408, 0x1d409, 0x1d40a, 0x1d40b, 0x1d40c, 0x1d40d, 0x1d40e, 0x1d40f,
+ 0x1d410, 0x1d411, 0x1d412, 0x1d413, 0x1d414, 0x1d415, 0x1d416, 0x1d417,
+ 0x1d418, 0x1d419, 0x1d41a, 0x1d41b, 0x1d41c, 0x1d41d, 0x1d41e, 0x1d41f,
+ 0x1d420, 0x1d421, 0x1d422, 0x1d423, 0x1d424, 0x1d425, 0x1d426, 0x1d427,
+ 0x1d428, 0x1d429, 0x1d42a, 0x1d42b, 0x1d42c, 0x1d42d, 0x1d42e, 0x1d42f,
+ 0x1d430, 0x1d431, 0x1d432, 0x1d433, 0x1d434, 0x1d435, 0x1d436, 0x1d437,
+ 0x1d438, 0x1d439, 0x1d43a, 0x1d43b, 0x1d43c, 0x1d43d, 0x1d43e, 0x1d43f,
+ 0x1d440, 0x1d441, 0x1d442, 0x1d443, 0x1d444, 0x1d445, 0x1d446, 0x1d447,
+ 0x1d448, 0x1d449, 0x1d44a, 0x1d44b, 0x1d44c, 0x1d44d, 0x1d44e, 0x1d44f,
+ 0x1d450, 0x1d451, 0x1d452, 0x1d453, 0x1d454, 0x1d456, 0x1d457, 0x1d458,
+ 0x1d459, 0x1d45a, 0x1d45b, 0x1d45c, 0x1d45d, 0x1d45e, 0x1d45f, 0x1d460,
+ 0x1d461, 0x1d462, 0x1d463, 0x1d464, 0x1d465, 0x1d466, 0x1d467, 0x1d468,
+ 0x1d469, 0x1d46a, 0x1d46b, 0x1d46c, 0x1d46d, 0x1d46e, 0x1d46f, 0x1d470,
+ 0x1d471, 0x1d472, 0x1d473, 0x1d474, 0x1d475, 0x1d476, 0x1d477, 0x1d478,
+ 0x1d479, 0x1d47a, 0x1d47b, 0x1d47c, 0x1d47d, 0x1d47e, 0x1d47f, 0x1d480,
+ 0x1d481, 0x1d482, 0x1d483, 0x1d484, 0x1d485, 0x1d486, 0x1d487, 0x1d488,
+ 0x1d489, 0x1d48a, 0x1d48b, 0x1d48c, 0x1d48d, 0x1d48e, 0x1d48f, 0x1d490,
+ 0x1d491, 0x1d492, 0x1d493, 0x1d494, 0x1d495, 0x1d496, 0x1d497, 0x1d498,
+ 0x1d499, 0x1d49a, 0x1d49b, 0x1d49c, 0x1d49e, 0x1d49f, 0x1d4a2, 0x1d4a5,
+ 0x1d4a6, 0x1d4a9, 0x1d4aa, 0x1d4ab, 0x1d4ac, 0x1d4ae, 0x1d4af, 0x1d4b0,
+ 0x1d4b1, 0x1d4b2, 0x1d4b3, 0x1d4b4, 0x1d4b5, 0x1d4b6, 0x1d4b7, 0x1d4b8,
+ 0x1d4b9, 0x1d4bb, 0x1d4bd, 0x1d4be, 0x1d4bf, 0x1d4c0, 0x1d4c1, 0x1d4c2,
+ 0x1d4c3, 0x1d4c5, 0x1d4c6, 0x1d4c7, 0x1d4c8, 0x1d4c9, 0x1d4ca, 0x1d4cb,
+ 0x1d4cc, 0x1d4cd, 0x1d4ce, 0x1d4cf, 0x1d4d0, 0x1d4d1, 0x1d4d2, 0x1d4d3,
+ 0x1d4d4, 0x1d4d5, 0x1d4d6, 0x1d4d7, 0x1d4d8, 0x1d4d9, 0x1d4da, 0x1d4db,
+ 0x1d4dc, 0x1d4dd, 0x1d4de, 0x1d4df, 0x1d4e0, 0x1d4e1, 0x1d4e2, 0x1d4e3,
+ 0x1d4e4, 0x1d4e5, 0x1d4e6, 0x1d4e7, 0x1d4e8, 0x1d4e9, 0x1d4ea, 0x1d4eb,
+ 0x1d4ec, 0x1d4ed, 0x1d4ee, 0x1d4ef, 0x1d4f0, 0x1d4f1, 0x1d4f2, 0x1d4f3,
+ 0x1d4f4, 0x1d4f5, 0x1d4f6, 0x1d4f7, 0x1d4f8, 0x1d4f9, 0x1d4fa, 0x1d4fb,
+ 0x1d4fc, 0x1d4fd, 0x1d4fe, 0x1d4ff, 0x1d500, 0x1d501, 0x1d502, 0x1d503,
+ 0x1d504, 0x1d505, 0x1d507, 0x1d508, 0x1d509, 0x1d50a, 0x1d50d, 0x1d50e,
+ 0x1d50f, 0x1d510, 0x1d511, 0x1d512, 0x1d513, 0x1d514, 0x1d516, 0x1d517,
+ 0x1d518, 0x1d519, 0x1d51a, 0x1d51b, 0x1d51c, 0x1d51e, 0x1d51f, 0x1d520,
+ 0x1d521, 0x1d522, 0x1d523, 0x1d524, 0x1d525, 0x1d526, 0x1d527, 0x1d528,
+ 0x1d529, 0x1d52a, 0x1d52b, 0x1d52c, 0x1d52d, 0x1d52e, 0x1d52f, 0x1d530,
+ 0x1d531, 0x1d532, 0x1d533, 0x1d534, 0x1d535, 0x1d536, 0x1d537, 0x1d538,
+ 0x1d539, 0x1d53b, 0x1d53c, 0x1d53d, 0x1d53e, 0x1d540, 0x1d541, 0x1d542,
+ 0x1d543, 0x1d544, 0x1d546, 0x1d54a, 0x1d54b, 0x1d54c, 0x1d54d, 0x1d54e,
+ 0x1d54f, 0x1d550, 0x1d552, 0x1d553, 0x1d554, 0x1d555, 0x1d556, 0x1d557,
+ 0x1d558, 0x1d559, 0x1d55a, 0x1d55b, 0x1d55c, 0x1d55d, 0x1d55e, 0x1d55f,
+ 0x1d560, 0x1d561, 0x1d562, 0x1d563, 0x1d564, 0x1d565, 0x1d566, 0x1d567,
+ 0x1d568, 0x1d569, 0x1d56a, 0x1d56b, 0x1d56c, 0x1d56d, 0x1d56e, 0x1d56f,
+ 0x1d570, 0x1d571, 0x1d572, 0x1d573, 0x1d574, 0x1d575, 0x1d576, 0x1d577,
+ 0x1d578, 0x1d579, 0x1d57a, 0x1d57b, 0x1d57c, 0x1d57d, 0x1d57e, 0x1d57f,
+ 0x1d580, 0x1d581, 0x1d582, 0x1d583, 0x1d584, 0x1d585, 0x1d586, 0x1d587,
+ 0x1d588, 0x1d589, 0x1d58a, 0x1d58b, 0x1d58c, 0x1d58d, 0x1d58e, 0x1d58f,
+ 0x1d590, 0x1d591, 0x1d592, 0x1d593, 0x1d594, 0x1d595, 0x1d596, 0x1d597,
+ 0x1d598, 0x1d599, 0x1d59a, 0x1d59b, 0x1d59c, 0x1d59d, 0x1d59e, 0x1d59f,
+ 0x1d5a0, 0x1d5a1, 0x1d5a2, 0x1d5a3, 0x1d5a4, 0x1d5a5, 0x1d5a6, 0x1d5a7,
+ 0x1d5a8, 0x1d5a9, 0x1d5aa, 0x1d5ab, 0x1d5ac, 0x1d5ad, 0x1d5ae, 0x1d5af,
+ 0x1d5b0, 0x1d5b1, 0x1d5b2, 0x1d5b3, 0x1d5b4, 0x1d5b5, 0x1d5b6, 0x1d5b7,
+ 0x1d5b8, 0x1d5b9, 0x1d5ba, 0x1d5bb, 0x1d5bc, 0x1d5bd, 0x1d5be, 0x1d5bf,
+ 0x1d5c0, 0x1d5c1, 0x1d5c2, 0x1d5c3, 0x1d5c4, 0x1d5c5, 0x1d5c6, 0x1d5c7,
+ 0x1d5c8, 0x1d5c9, 0x1d5ca, 0x1d5cb, 0x1d5cc, 0x1d5cd, 0x1d5ce, 0x1d5cf,
+ 0x1d5d0, 0x1d5d1, 0x1d5d2, 0x1d5d3, 0x1d5d4, 0x1d5d5, 0x1d5d6, 0x1d5d7,
+ 0x1d5d8, 0x1d5d9, 0x1d5da, 0x1d5db, 0x1d5dc, 0x1d5dd, 0x1d5de, 0x1d5df,
+ 0x1d5e0, 0x1d5e1, 0x1d5e2, 0x1d5e3, 0x1d5e4, 0x1d5e5, 0x1d5e6, 0x1d5e7,
+ 0x1d5e8, 0x1d5e9, 0x1d5ea, 0x1d5eb, 0x1d5ec, 0x1d5ed, 0x1d5ee, 0x1d5ef,
+ 0x1d5f0, 0x1d5f1, 0x1d5f2, 0x1d5f3, 0x1d5f4, 0x1d5f5, 0x1d5f6, 0x1d5f7,
+ 0x1d5f8, 0x1d5f9, 0x1d5fa, 0x1d5fb, 0x1d5fc, 0x1d5fd, 0x1d5fe, 0x1d5ff,
+ 0x1d600, 0x1d601, 0x1d602, 0x1d603, 0x1d604, 0x1d605, 0x1d606, 0x1d607,
+ 0x1d608, 0x1d609, 0x1d60a, 0x1d60b, 0x1d60c, 0x1d60d, 0x1d60e, 0x1d60f,
+ 0x1d610, 0x1d611, 0x1d612, 0x1d613, 0x1d614, 0x1d615, 0x1d616, 0x1d617,
+ 0x1d618, 0x1d619, 0x1d61a, 0x1d61b, 0x1d61c, 0x1d61d, 0x1d61e, 0x1d61f,
+ 0x1d620, 0x1d621, 0x1d622, 0x1d623, 0x1d624, 0x1d625, 0x1d626, 0x1d627,
+ 0x1d628, 0x1d629, 0x1d62a, 0x1d62b, 0x1d62c, 0x1d62d, 0x1d62e, 0x1d62f,
+ 0x1d630, 0x1d631, 0x1d632, 0x1d633, 0x1d634, 0x1d635, 0x1d636, 0x1d637,
+ 0x1d638, 0x1d639, 0x1d63a, 0x1d63b, 0x1d63c, 0x1d63d, 0x1d63e, 0x1d63f,
+ 0x1d640, 0x1d641, 0x1d642, 0x1d643, 0x1d644, 0x1d645, 0x1d646, 0x1d647,
+ 0x1d648, 0x1d649, 0x1d64a, 0x1d64b, 0x1d64c, 0x1d64d, 0x1d64e, 0x1d64f,
+ 0x1d650, 0x1d651, 0x1d652, 0x1d653, 0x1d654, 0x1d655, 0x1d656, 0x1d657,
+ 0x1d658, 0x1d659, 0x1d65a, 0x1d65b, 0x1d65c, 0x1d65d, 0x1d65e, 0x1d65f,
+ 0x1d660, 0x1d661, 0x1d662, 0x1d663, 0x1d664, 0x1d665, 0x1d666, 0x1d667,
+ 0x1d668, 0x1d669, 0x1d66a, 0x1d66b, 0x1d66c, 0x1d66d, 0x1d66e, 0x1d66f,
+ 0x1d670, 0x1d671, 0x1d672, 0x1d673, 0x1d674, 0x1d675, 0x1d676, 0x1d677,
+ 0x1d678, 0x1d679, 0x1d67a, 0x1d67b, 0x1d67c, 0x1d67d, 0x1d67e, 0x1d67f,
+ 0x1d680, 0x1d681, 0x1d682, 0x1d683, 0x1d684, 0x1d685, 0x1d686, 0x1d687,
+ 0x1d688, 0x1d689, 0x1d68a, 0x1d68b, 0x1d68c, 0x1d68d, 0x1d68e, 0x1d68f,
+ 0x1d690, 0x1d691, 0x1d692, 0x1d693, 0x1d694, 0x1d695, 0x1d696, 0x1d697,
+ 0x1d698, 0x1d699, 0x1d69a, 0x1d69b, 0x1d69c, 0x1d69d, 0x1d69e, 0x1d69f,
+ 0x1d6a0, 0x1d6a1, 0x1d6a2, 0x1d6a3, 0x1d6a4, 0x1d6a5, 0x1d6a8, 0x1d6a9,
+ 0x1d6aa, 0x1d6ab, 0x1d6ac, 0x1d6ad, 0x1d6ae, 0x1d6af, 0x1d6b0, 0x1d6b1,
+ 0x1d6b2, 0x1d6b3, 0x1d6b4, 0x1d6b5, 0x1d6b6, 0x1d6b7, 0x1d6b8, 0x1d6b9,
+ 0x1d6ba, 0x1d6bb, 0x1d6bc, 0x1d6bd, 0x1d6be, 0x1d6bf, 0x1d6c0, 0x1d6c1,
+ 0x1d6c2, 0x1d6c3, 0x1d6c4, 0x1d6c5, 0x1d6c6, 0x1d6c7, 0x1d6c8, 0x1d6c9,
+ 0x1d6ca, 0x1d6cb, 0x1d6cc, 0x1d6cd, 0x1d6ce, 0x1d6cf, 0x1d6d0, 0x1d6d1,
+ 0x1d6d2, 0x1d6d3, 0x1d6d4, 0x1d6d5, 0x1d6d6, 0x1d6d7, 0x1d6d8, 0x1d6d9,
+ 0x1d6da, 0x1d6db, 0x1d6dc, 0x1d6dd, 0x1d6de, 0x1d6df, 0x1d6e0, 0x1d6e1,
+ 0x1d6e2, 0x1d6e3, 0x1d6e4, 0x1d6e5, 0x1d6e6, 0x1d6e7, 0x1d6e8, 0x1d6e9,
+ 0x1d6ea, 0x1d6eb, 0x1d6ec, 0x1d6ed, 0x1d6ee, 0x1d6ef, 0x1d6f0, 0x1d6f1,
+ 0x1d6f2, 0x1d6f3, 0x1d6f4, 0x1d6f5, 0x1d6f6, 0x1d6f7, 0x1d6f8, 0x1d6f9,
+ 0x1d6fa, 0x1d6fb, 0x1d6fc, 0x1d6fd, 0x1d6fe, 0x1d6ff, 0x1d700, 0x1d701,
+ 0x1d702, 0x1d703, 0x1d704, 0x1d705, 0x1d706, 0x1d707, 0x1d708, 0x1d709,
+ 0x1d70a, 0x1d70b, 0x1d70c, 0x1d70d, 0x1d70e, 0x1d70f, 0x1d710, 0x1d711,
+ 0x1d712, 0x1d713, 0x1d714, 0x1d715, 0x1d716, 0x1d717, 0x1d718, 0x1d719,
+ 0x1d71a, 0x1d71b, 0x1d71c, 0x1d71d, 0x1d71e, 0x1d71f, 0x1d720, 0x1d721,
+ 0x1d722, 0x1d723, 0x1d724, 0x1d725, 0x1d726, 0x1d727, 0x1d728, 0x1d729,
+ 0x1d72a, 0x1d72b, 0x1d72c, 0x1d72d, 0x1d72e, 0x1d72f, 0x1d730, 0x1d731,
+ 0x1d732, 0x1d733, 0x1d734, 0x1d735, 0x1d736, 0x1d737, 0x1d738, 0x1d739,
+ 0x1d73a, 0x1d73b, 0x1d73c, 0x1d73d, 0x1d73e, 0x1d73f, 0x1d740, 0x1d741,
+ 0x1d742, 0x1d743, 0x1d744, 0x1d745, 0x1d746, 0x1d747, 0x1d748, 0x1d749,
+ 0x1d74a, 0x1d74b, 0x1d74c, 0x1d74d, 0x1d74e, 0x1d74f, 0x1d750, 0x1d751,
+ 0x1d752, 0x1d753, 0x1d754, 0x1d755, 0x1d756, 0x1d757, 0x1d758, 0x1d759,
+ 0x1d75a, 0x1d75b, 0x1d75c, 0x1d75d, 0x1d75e, 0x1d75f, 0x1d760, 0x1d761,
+ 0x1d762, 0x1d763, 0x1d764, 0x1d765, 0x1d766, 0x1d767, 0x1d768, 0x1d769,
+ 0x1d76a, 0x1d76b, 0x1d76c, 0x1d76d, 0x1d76e, 0x1d76f, 0x1d770, 0x1d771,
+ 0x1d772, 0x1d773, 0x1d774, 0x1d775, 0x1d776, 0x1d777, 0x1d778, 0x1d779,
+ 0x1d77a, 0x1d77b, 0x1d77c, 0x1d77d, 0x1d77e, 0x1d77f, 0x1d780, 0x1d781,
+ 0x1d782, 0x1d783, 0x1d784, 0x1d785, 0x1d786, 0x1d787, 0x1d788, 0x1d789,
+ 0x1d78a, 0x1d78b, 0x1d78c, 0x1d78d, 0x1d78e, 0x1d78f, 0x1d790, 0x1d791,
+ 0x1d792, 0x1d793, 0x1d794, 0x1d795, 0x1d796, 0x1d797, 0x1d798, 0x1d799,
+ 0x1d79a, 0x1d79b, 0x1d79c, 0x1d79d, 0x1d79e, 0x1d79f, 0x1d7a0, 0x1d7a1,
+ 0x1d7a2, 0x1d7a3, 0x1d7a4, 0x1d7a5, 0x1d7a6, 0x1d7a7, 0x1d7a8, 0x1d7a9,
+ 0x1d7aa, 0x1d7ab, 0x1d7ac, 0x1d7ad, 0x1d7ae, 0x1d7af, 0x1d7b0, 0x1d7b1,
+ 0x1d7b2, 0x1d7b3, 0x1d7b4, 0x1d7b5, 0x1d7b6, 0x1d7b7, 0x1d7b8, 0x1d7b9,
+ 0x1d7ba, 0x1d7bb, 0x1d7bc, 0x1d7bd, 0x1d7be, 0x1d7bf, 0x1d7c0, 0x1d7c1,
+ 0x1d7c2, 0x1d7c3, 0x1d7c4, 0x1d7c5, 0x1d7c6, 0x1d7c7, 0x1d7c8, 0x1d7c9,
+ 0x1d7ca, 0x1d7cb, 0x1d7ce, 0x1d7cf, 0x1d7d0, 0x1d7d1, 0x1d7d2, 0x1d7d3,
+ 0x1d7d4, 0x1d7d5, 0x1d7d6, 0x1d7d7, 0x1d7d8, 0x1d7d9, 0x1d7da, 0x1d7db,
+ 0x1d7dc, 0x1d7dd, 0x1d7de, 0x1d7df, 0x1d7e0, 0x1d7e1, 0x1d7e2, 0x1d7e3,
+ 0x1d7e4, 0x1d7e5, 0x1d7e6, 0x1d7e7, 0x1d7e8, 0x1d7e9, 0x1d7ea, 0x1d7eb,
+ 0x1d7ec, 0x1d7ed, 0x1d7ee, 0x1d7ef, 0x1d7f0, 0x1d7f1, 0x1d7f2, 0x1d7f3,
+ 0x1d7f4, 0x1d7f5, 0x1d7f6, 0x1d7f7, 0x1d7f8, 0x1d7f9, 0x1d7fa, 0x1d7fb,
+ 0x1d7fc, 0x1d7fd, 0x1d7fe, 0x1d7ff, 0x1ee00, 0x1ee01, 0x1ee02, 0x1ee03,
+ 0x1ee05, 0x1ee06, 0x1ee07, 0x1ee08, 0x1ee09, 0x1ee0a, 0x1ee0b, 0x1ee0c,
+ 0x1ee0d, 0x1ee0e, 0x1ee0f, 0x1ee10, 0x1ee11, 0x1ee12, 0x1ee13, 0x1ee14,
+ 0x1ee15, 0x1ee16, 0x1ee17, 0x1ee18, 0x1ee19, 0x1ee1a, 0x1ee1b, 0x1ee1c,
+ 0x1ee1d, 0x1ee1e, 0x1ee1f, 0x1ee21, 0x1ee22, 0x1ee24, 0x1ee27, 0x1ee29,
+ 0x1ee2a, 0x1ee2b, 0x1ee2c, 0x1ee2d, 0x1ee2e, 0x1ee2f, 0x1ee30, 0x1ee31,
+ 0x1ee32, 0x1ee34, 0x1ee35, 0x1ee36, 0x1ee37, 0x1ee39, 0x1ee3b, 0x1ee42,
+ 0x1ee47, 0x1ee49, 0x1ee4b, 0x1ee4d, 0x1ee4e, 0x1ee4f, 0x1ee51, 0x1ee52,
+ 0x1ee54, 0x1ee57, 0x1ee59, 0x1ee5b, 0x1ee5d, 0x1ee5f, 0x1ee61, 0x1ee62,
+ 0x1ee64, 0x1ee67, 0x1ee68, 0x1ee69, 0x1ee6a, 0x1ee6c, 0x1ee6d, 0x1ee6e,
+ 0x1ee6f, 0x1ee70, 0x1ee71, 0x1ee72, 0x1ee74, 0x1ee75, 0x1ee76, 0x1ee77,
+ 0x1ee79, 0x1ee7a, 0x1ee7b, 0x1ee7c, 0x1ee7e, 0x1ee80, 0x1ee81, 0x1ee82,
+ 0x1ee83, 0x1ee84, 0x1ee85, 0x1ee86, 0x1ee87, 0x1ee88, 0x1ee89, 0x1ee8b,
+ 0x1ee8c, 0x1ee8d, 0x1ee8e, 0x1ee8f, 0x1ee90, 0x1ee91, 0x1ee92, 0x1ee93,
+ 0x1ee94, 0x1ee95, 0x1ee96, 0x1ee97, 0x1ee98, 0x1ee99, 0x1ee9a, 0x1ee9b,
+ 0x1eea1, 0x1eea2, 0x1eea3, 0x1eea5, 0x1eea6, 0x1eea7, 0x1eea8, 0x1eea9,
+ 0x1eeab, 0x1eeac, 0x1eead, 0x1eeae, 0x1eeaf, 0x1eeb0, 0x1eeb1, 0x1eeb2,
+ 0x1eeb3, 0x1eeb4, 0x1eeb5, 0x1eeb6, 0x1eeb7, 0x1eeb8, 0x1eeb9, 0x1eeba,
+ 0x1eebb, 0x1f12b, 0x1f12c, 0x1f130, 0x1f131, 0x1f132, 0x1f133, 0x1f134,
+ 0x1f135, 0x1f136, 0x1f137, 0x1f138, 0x1f139, 0x1f13a, 0x1f13b, 0x1f13c,
+ 0x1f13d, 0x1f13e, 0x1f13f, 0x1f140, 0x1f141, 0x1f142, 0x1f143, 0x1f144,
+ 0x1f145, 0x1f146, 0x1f147, 0x1f148, 0x1f149, 0x1f202, 0x1f210, 0x1f211,
+ 0x1f212, 0x1f213, 0x1f214, 0x1f215, 0x1f216, 0x1f217, 0x1f218, 0x1f219,
+ 0x1f21a, 0x1f21b, 0x1f21c, 0x1f21d, 0x1f21e, 0x1f21f, 0x1f220, 0x1f221,
+ 0x1f222, 0x1f223, 0x1f224, 0x1f225, 0x1f226, 0x1f227, 0x1f228, 0x1f229,
+ 0x1f22a, 0x1f22b, 0x1f22c, 0x1f22d, 0x1f22e, 0x1f22f, 0x1f230, 0x1f231,
+ 0x1f232, 0x1f233, 0x1f234, 0x1f235, 0x1f236, 0x1f237, 0x1f238, 0x1f239,
+ 0x1f23a, 0x1f23b, 0x1f250, 0x1f251, 0x2f800, 0x2f801, 0x2f802, 0x2f803,
+ 0x2f804, 0x2f805, 0x2f806, 0x2f807, 0x2f808, 0x2f809, 0x2f80a, 0x2f80b,
+ 0x2f80c, 0x2f80d, 0x2f80e, 0x2f80f, 0x2f810, 0x2f811, 0x2f812, 0x2f813,
+ 0x2f814, 0x2f815, 0x2f816, 0x2f817, 0x2f818, 0x2f819, 0x2f81a, 0x2f81b,
+ 0x2f81c, 0x2f81d, 0x2f81e, 0x2f81f, 0x2f820, 0x2f821, 0x2f822, 0x2f823,
+ 0x2f824, 0x2f825, 0x2f826, 0x2f827, 0x2f828, 0x2f829, 0x2f82a, 0x2f82b,
+ 0x2f82c, 0x2f82d, 0x2f82e, 0x2f82f, 0x2f830, 0x2f831, 0x2f832, 0x2f833,
+ 0x2f834, 0x2f835, 0x2f836, 0x2f837, 0x2f838, 0x2f839, 0x2f83a, 0x2f83b,
+ 0x2f83c, 0x2f83d, 0x2f83e, 0x2f83f, 0x2f840, 0x2f841, 0x2f842, 0x2f843,
+ 0x2f844, 0x2f845, 0x2f846, 0x2f847, 0x2f848, 0x2f849, 0x2f84a, 0x2f84b,
+ 0x2f84c, 0x2f84d, 0x2f84e, 0x2f84f, 0x2f850, 0x2f851, 0x2f852, 0x2f853,
+ 0x2f854, 0x2f855, 0x2f856, 0x2f857, 0x2f858, 0x2f859, 0x2f85a, 0x2f85b,
+ 0x2f85c, 0x2f85d, 0x2f85e, 0x2f85f, 0x2f860, 0x2f861, 0x2f862, 0x2f863,
+ 0x2f864, 0x2f865, 0x2f866, 0x2f867, 0x2f868, 0x2f869, 0x2f86a, 0x2f86b,
+ 0x2f86c, 0x2f86d, 0x2f86e, 0x2f86f, 0x2f870, 0x2f871, 0x2f872, 0x2f873,
+ 0x2f874, 0x2f875, 0x2f876, 0x2f877, 0x2f878, 0x2f879, 0x2f87a, 0x2f87b,
+ 0x2f87c, 0x2f87d, 0x2f87e, 0x2f87f, 0x2f880, 0x2f881, 0x2f882, 0x2f883,
+ 0x2f884, 0x2f885, 0x2f886, 0x2f887, 0x2f888, 0x2f889, 0x2f88a, 0x2f88b,
+ 0x2f88c, 0x2f88d, 0x2f88e, 0x2f88f, 0x2f890, 0x2f891, 0x2f892, 0x2f893,
+ 0x2f894, 0x2f895, 0x2f896, 0x2f897, 0x2f898, 0x2f899, 0x2f89a, 0x2f89b,
+ 0x2f89c, 0x2f89d, 0x2f89e, 0x2f89f, 0x2f8a0, 0x2f8a1, 0x2f8a2, 0x2f8a3,
+ 0x2f8a4, 0x2f8a5, 0x2f8a6, 0x2f8a7, 0x2f8a8, 0x2f8a9, 0x2f8aa, 0x2f8ab,
+ 0x2f8ac, 0x2f8ad, 0x2f8ae, 0x2f8af, 0x2f8b0, 0x2f8b1, 0x2f8b2, 0x2f8b3,
+ 0x2f8b4, 0x2f8b5, 0x2f8b6, 0x2f8b7, 0x2f8b8, 0x2f8b9, 0x2f8ba, 0x2f8bb,
+ 0x2f8bc, 0x2f8bd, 0x2f8be, 0x2f8bf, 0x2f8c0, 0x2f8c1, 0x2f8c2, 0x2f8c3,
+ 0x2f8c4, 0x2f8c5, 0x2f8c6, 0x2f8c7, 0x2f8c8, 0x2f8c9, 0x2f8ca, 0x2f8cb,
+ 0x2f8cc, 0x2f8cd, 0x2f8ce, 0x2f8cf, 0x2f8d0, 0x2f8d1, 0x2f8d2, 0x2f8d3,
+ 0x2f8d4, 0x2f8d5, 0x2f8d6, 0x2f8d7, 0x2f8d8, 0x2f8d9, 0x2f8da, 0x2f8db,
+ 0x2f8dc, 0x2f8dd, 0x2f8de, 0x2f8df, 0x2f8e0, 0x2f8e1, 0x2f8e2, 0x2f8e3,
+ 0x2f8e4, 0x2f8e5, 0x2f8e6, 0x2f8e7, 0x2f8e8, 0x2f8e9, 0x2f8ea, 0x2f8eb,
+ 0x2f8ec, 0x2f8ed, 0x2f8ee, 0x2f8ef, 0x2f8f0, 0x2f8f1, 0x2f8f2, 0x2f8f3,
+ 0x2f8f4, 0x2f8f5, 0x2f8f6, 0x2f8f7, 0x2f8f8, 0x2f8f9, 0x2f8fa, 0x2f8fb,
+ 0x2f8fc, 0x2f8fd, 0x2f8fe, 0x2f8ff, 0x2f900, 0x2f901, 0x2f902, 0x2f903,
+ 0x2f904, 0x2f905, 0x2f906, 0x2f907, 0x2f908, 0x2f909, 0x2f90a, 0x2f90b,
+ 0x2f90c, 0x2f90d, 0x2f90e, 0x2f90f, 0x2f910, 0x2f911, 0x2f912, 0x2f913,
+ 0x2f914, 0x2f915, 0x2f916, 0x2f917, 0x2f918, 0x2f919, 0x2f91a, 0x2f91b,
+ 0x2f91c, 0x2f91d, 0x2f91e, 0x2f91f, 0x2f920, 0x2f921, 0x2f922, 0x2f923,
+ 0x2f924, 0x2f925, 0x2f926, 0x2f927, 0x2f928, 0x2f929, 0x2f92a, 0x2f92b,
+ 0x2f92c, 0x2f92d, 0x2f92e, 0x2f92f, 0x2f930, 0x2f931, 0x2f932, 0x2f933,
+ 0x2f934, 0x2f935, 0x2f936, 0x2f937, 0x2f938, 0x2f939, 0x2f93a, 0x2f93b,
+ 0x2f93c, 0x2f93d, 0x2f93e, 0x2f93f, 0x2f940, 0x2f941, 0x2f942, 0x2f943,
+ 0x2f944, 0x2f945, 0x2f946, 0x2f947, 0x2f948, 0x2f949, 0x2f94a, 0x2f94b,
+ 0x2f94c, 0x2f94d, 0x2f94e, 0x2f94f, 0x2f950, 0x2f951, 0x2f952, 0x2f953,
+ 0x2f954, 0x2f955, 0x2f956, 0x2f957, 0x2f958, 0x2f959, 0x2f95a, 0x2f95b,
+ 0x2f95c, 0x2f95d, 0x2f95e, 0x2f95f, 0x2f960, 0x2f961, 0x2f962, 0x2f963,
+ 0x2f964, 0x2f965, 0x2f966, 0x2f967, 0x2f968, 0x2f969, 0x2f96a, 0x2f96b,
+ 0x2f96c, 0x2f96d, 0x2f96e, 0x2f96f, 0x2f970, 0x2f971, 0x2f972, 0x2f973,
+ 0x2f974, 0x2f975, 0x2f976, 0x2f977, 0x2f978, 0x2f979, 0x2f97a, 0x2f97b,
+ 0x2f97c, 0x2f97d, 0x2f97e, 0x2f97f, 0x2f980, 0x2f981, 0x2f982, 0x2f983,
+ 0x2f984, 0x2f985, 0x2f986, 0x2f987, 0x2f988, 0x2f989, 0x2f98a, 0x2f98b,
+ 0x2f98c, 0x2f98d, 0x2f98e, 0x2f98f, 0x2f990, 0x2f991, 0x2f992, 0x2f993,
+ 0x2f994, 0x2f995, 0x2f996, 0x2f997, 0x2f998, 0x2f999, 0x2f99a, 0x2f99b,
+ 0x2f99c, 0x2f99d, 0x2f99e, 0x2f99f, 0x2f9a0, 0x2f9a1, 0x2f9a2, 0x2f9a3,
+ 0x2f9a4, 0x2f9a5, 0x2f9a6, 0x2f9a7, 0x2f9a8, 0x2f9a9, 0x2f9aa, 0x2f9ab,
+ 0x2f9ac, 0x2f9ad, 0x2f9ae, 0x2f9af, 0x2f9b0, 0x2f9b1, 0x2f9b2, 0x2f9b3,
+ 0x2f9b4, 0x2f9b5, 0x2f9b6, 0x2f9b7, 0x2f9b8, 0x2f9b9, 0x2f9ba, 0x2f9bb,
+ 0x2f9bc, 0x2f9bd, 0x2f9be, 0x2f9bf, 0x2f9c0, 0x2f9c1, 0x2f9c2, 0x2f9c3,
+ 0x2f9c4, 0x2f9c5, 0x2f9c6, 0x2f9c7, 0x2f9c8, 0x2f9c9, 0x2f9ca, 0x2f9cb,
+ 0x2f9cc, 0x2f9cd, 0x2f9ce, 0x2f9cf, 0x2f9d0, 0x2f9d1, 0x2f9d2, 0x2f9d3,
+ 0x2f9d4, 0x2f9d5, 0x2f9d6, 0x2f9d7, 0x2f9d8, 0x2f9d9, 0x2f9da, 0x2f9db,
+ 0x2f9dc, 0x2f9dd, 0x2f9de, 0x2f9df, 0x2f9e0, 0x2f9e1, 0x2f9e2, 0x2f9e3,
+ 0x2f9e4, 0x2f9e5, 0x2f9e6, 0x2f9e7, 0x2f9e8, 0x2f9e9, 0x2f9ea, 0x2f9eb,
+ 0x2f9ec, 0x2f9ed, 0x2f9ee, 0x2f9ef, 0x2f9f0, 0x2f9f1, 0x2f9f2, 0x2f9f3,
+ 0x2f9f4, 0x2f9f5, 0x2f9f6, 0x2f9f7, 0x2f9f8, 0x2f9f9, 0x2f9fa, 0x2f9fb,
+ 0x2f9fc, 0x2f9fd, 0x2f9fe, 0x2f9ff, 0x2fa00, 0x2fa01, 0x2fa02, 0x2fa03,
+ 0x2fa04, 0x2fa05, 0x2fa06, 0x2fa07, 0x2fa08, 0x2fa09, 0x2fa0a, 0x2fa0b,
+ 0x2fa0c, 0x2fa0d, 0x2fa0e, 0x2fa0f, 0x2fa10, 0x2fa11, 0x2fa12, 0x2fa13,
+ 0x2fa14, 0x2fa15, 0x2fa16, 0x2fa17, 0x2fa18, 0x2fa19, 0x2fa1a, 0x2fa1b,
+ 0x2fa1c, 0x2fa1d
+};
+static const uint32_t uni32_decomp_values[] = {
+ 0x00041, 0x00042, 0x00043, 0x00044, 0x00045, 0x00046, 0x00047, 0x00048,
+ 0x00049, 0x0004a, 0x0004b, 0x0004c, 0x0004d, 0x0004e, 0x0004f, 0x00050,
+ 0x00051, 0x00052, 0x00053, 0x00054, 0x00055, 0x00056, 0x00057, 0x00058,
+ 0x00059, 0x0005a, 0x00061, 0x00062, 0x00063, 0x00064, 0x00065, 0x00066,
+ 0x00067, 0x00068, 0x00069, 0x0006a, 0x0006b, 0x0006c, 0x0006d, 0x0006e,
+ 0x0006f, 0x00070, 0x00071, 0x00072, 0x00073, 0x00074, 0x00075, 0x00076,
+ 0x00077, 0x00078, 0x00079, 0x0007a, 0x00041, 0x00042, 0x00043, 0x00044,
+ 0x00045, 0x00046, 0x00047, 0x00048, 0x00049, 0x0004a, 0x0004b, 0x0004c,
+ 0x0004d, 0x0004e, 0x0004f, 0x00050, 0x00051, 0x00052, 0x00053, 0x00054,
+ 0x00055, 0x00056, 0x00057, 0x00058, 0x00059, 0x0005a, 0x00061, 0x00062,
+ 0x00063, 0x00064, 0x00065, 0x00066, 0x00067, 0x00069, 0x0006a, 0x0006b,
+ 0x0006c, 0x0006d, 0x0006e, 0x0006f, 0x00070, 0x00071, 0x00072, 0x00073,
+ 0x00074, 0x00075, 0x00076, 0x00077, 0x00078, 0x00079, 0x0007a, 0x00041,
+ 0x00042, 0x00043, 0x00044, 0x00045, 0x00046, 0x00047, 0x00048, 0x00049,
+ 0x0004a, 0x0004b, 0x0004c, 0x0004d, 0x0004e, 0x0004f, 0x00050, 0x00051,
+ 0x00052, 0x00053, 0x00054, 0x00055, 0x00056, 0x00057, 0x00058, 0x00059,
+ 0x0005a, 0x00061, 0x00062, 0x00063, 0x00064, 0x00065, 0x00066, 0x00067,
+ 0x00068, 0x00069, 0x0006a, 0x0006b, 0x0006c, 0x0006d, 0x0006e, 0x0006f,
+ 0x00070, 0x00071, 0x00072, 0x00073, 0x00074, 0x00075, 0x00076, 0x00077,
+ 0x00078, 0x00079, 0x0007a, 0x00041, 0x00043, 0x00044, 0x00047, 0x0004a,
+ 0x0004b, 0x0004e, 0x0004f, 0x00050, 0x00051, 0x00053, 0x00054, 0x00055,
+ 0x00056, 0x00057, 0x00058, 0x00059, 0x0005a, 0x00061, 0x00062, 0x00063,
+ 0x00064, 0x00066, 0x00068, 0x00069, 0x0006a, 0x0006b, 0x0006c, 0x0006d,
+ 0x0006e, 0x00070, 0x00071, 0x00072, 0x00073, 0x00074, 0x00075, 0x00076,
+ 0x00077, 0x00078, 0x00079, 0x0007a, 0x00041, 0x00042, 0x00043, 0x00044,
+ 0x00045, 0x00046, 0x00047, 0x00048, 0x00049, 0x0004a, 0x0004b, 0x0004c,
+ 0x0004d, 0x0004e, 0x0004f, 0x00050, 0x00051, 0x00052, 0x00053, 0x00054,
+ 0x00055, 0x00056, 0x00057, 0x00058, 0x00059, 0x0005a, 0x00061, 0x00062,
+ 0x00063, 0x00064, 0x00065, 0x00066, 0x00067, 0x00068, 0x00069, 0x0006a,
+ 0x0006b, 0x0006c, 0x0006d, 0x0006e, 0x0006f, 0x00070, 0x00071, 0x00072,
+ 0x00073, 0x00074, 0x00075, 0x00076, 0x00077, 0x00078, 0x00079, 0x0007a,
+ 0x00041, 0x00042, 0x00044, 0x00045, 0x00046, 0x00047, 0x0004a, 0x0004b,
+ 0x0004c, 0x0004d, 0x0004e, 0x0004f, 0x00050, 0x00051, 0x00053, 0x00054,
+ 0x00055, 0x00056, 0x00057, 0x00058, 0x00059, 0x00061, 0x00062, 0x00063,
+ 0x00064, 0x00065, 0x00066, 0x00067, 0x00068, 0x00069, 0x0006a, 0x0006b,
+ 0x0006c, 0x0006d, 0x0006e, 0x0006f, 0x00070, 0x00071, 0x00072, 0x00073,
+ 0x00074, 0x00075, 0x00076, 0x00077, 0x00078, 0x00079, 0x0007a, 0x00041,
+ 0x00042, 0x00044, 0x00045, 0x00046, 0x00047, 0x00049, 0x0004a, 0x0004b,
+ 0x0004c, 0x0004d, 0x0004f, 0x00053, 0x00054, 0x00055, 0x00056, 0x00057,
+ 0x00058, 0x00059, 0x00061, 0x00062, 0x00063, 0x00064, 0x00065, 0x00066,
+ 0x00067, 0x00068, 0x00069, 0x0006a, 0x0006b, 0x0006c, 0x0006d, 0x0006e,
+ 0x0006f, 0x00070, 0x00071, 0x00072, 0x00073, 0x00074, 0x00075, 0x00076,
+ 0x00077, 0x00078, 0x00079, 0x0007a, 0x00041, 0x00042, 0x00043, 0x00044,
+ 0x00045, 0x00046, 0x00047, 0x00048, 0x00049, 0x0004a, 0x0004b, 0x0004c,
+ 0x0004d, 0x0004e, 0x0004f, 0x00050, 0x00051, 0x00052, 0x00053, 0x00054,
+ 0x00055, 0x00056, 0x00057, 0x00058, 0x00059, 0x0005a, 0x00061, 0x00062,
+ 0x00063, 0x00064, 0x00065, 0x00066, 0x00067, 0x00068, 0x00069, 0x0006a,
+ 0x0006b, 0x0006c, 0x0006d, 0x0006e, 0x0006f, 0x00070, 0x00071, 0x00072,
+ 0x00073, 0x00074, 0x00075, 0x00076, 0x00077, 0x00078, 0x00079, 0x0007a,
+ 0x00041, 0x00042, 0x00043, 0x00044, 0x00045, 0x00046, 0x00047, 0x00048,
+ 0x00049, 0x0004a, 0x0004b, 0x0004c, 0x0004d, 0x0004e, 0x0004f, 0x00050,
+ 0x00051, 0x00052, 0x00053, 0x00054, 0x00055, 0x00056, 0x00057, 0x00058,
+ 0x00059, 0x0005a, 0x00061, 0x00062, 0x00063, 0x00064, 0x00065, 0x00066,
+ 0x00067, 0x00068, 0x00069, 0x0006a, 0x0006b, 0x0006c, 0x0006d, 0x0006e,
+ 0x0006f, 0x00070, 0x00071, 0x00072, 0x00073, 0x00074, 0x00075, 0x00076,
+ 0x00077, 0x00078, 0x00079, 0x0007a, 0x00041, 0x00042, 0x00043, 0x00044,
+ 0x00045, 0x00046, 0x00047, 0x00048, 0x00049, 0x0004a, 0x0004b, 0x0004c,
+ 0x0004d, 0x0004e, 0x0004f, 0x00050, 0x00051, 0x00052, 0x00053, 0x00054,
+ 0x00055, 0x00056, 0x00057, 0x00058, 0x00059, 0x0005a, 0x00061, 0x00062,
+ 0x00063, 0x00064, 0x00065, 0x00066, 0x00067, 0x00068, 0x00069, 0x0006a,
+ 0x0006b, 0x0006c, 0x0006d, 0x0006e, 0x0006f, 0x00070, 0x00071, 0x00072,
+ 0x00073, 0x00074, 0x00075, 0x00076, 0x00077, 0x00078, 0x00079, 0x0007a,
+ 0x00041, 0x00042, 0x00043, 0x00044, 0x00045, 0x00046, 0x00047, 0x00048,
+ 0x00049, 0x0004a, 0x0004b, 0x0004c, 0x0004d, 0x0004e, 0x0004f, 0x00050,
+ 0x00051, 0x00052, 0x00053, 0x00054, 0x00055, 0x00056, 0x00057, 0x00058,
+ 0x00059, 0x0005a, 0x00061, 0x00062, 0x00063, 0x00064, 0x00065, 0x00066,
+ 0x00067, 0x00068, 0x00069, 0x0006a, 0x0006b, 0x0006c, 0x0006d, 0x0006e,
+ 0x0006f, 0x00070, 0x00071, 0x00072, 0x00073, 0x00074, 0x00075, 0x00076,
+ 0x00077, 0x00078, 0x00079, 0x0007a, 0x00041, 0x00042, 0x00043, 0x00044,
+ 0x00045, 0x00046, 0x00047, 0x00048, 0x00049, 0x0004a, 0x0004b, 0x0004c,
+ 0x0004d, 0x0004e, 0x0004f, 0x00050, 0x00051, 0x00052, 0x00053, 0x00054,
+ 0x00055, 0x00056, 0x00057, 0x00058, 0x00059, 0x0005a, 0x00061, 0x00062,
+ 0x00063, 0x00064, 0x00065, 0x00066, 0x00067, 0x00068, 0x00069, 0x0006a,
+ 0x0006b, 0x0006c, 0x0006d, 0x0006e, 0x0006f, 0x00070, 0x00071, 0x00072,
+ 0x00073, 0x00074, 0x00075, 0x00076, 0x00077, 0x00078, 0x00079, 0x0007a,
+ 0x00041, 0x00042, 0x00043, 0x00044, 0x00045, 0x00046, 0x00047, 0x00048,
+ 0x00049, 0x0004a, 0x0004b, 0x0004c, 0x0004d, 0x0004e, 0x0004f, 0x00050,
+ 0x00051, 0x00052, 0x00053, 0x00054, 0x00055, 0x00056, 0x00057, 0x00058,
+ 0x00059, 0x0005a, 0x00061, 0x00062, 0x00063, 0x00064, 0x00065, 0x00066,
+ 0x00067, 0x00068, 0x00069, 0x0006a, 0x0006b, 0x0006c, 0x0006d, 0x0006e,
+ 0x0006f, 0x00070, 0x00071, 0x00072, 0x00073, 0x00074, 0x00075, 0x00076,
+ 0x00077, 0x00078, 0x00079, 0x0007a, 0x00131, 0x00237, 0x00391, 0x00392,
+ 0x00393, 0x00394, 0x00395, 0x00396, 0x00397, 0x00398, 0x00399, 0x0039a,
+ 0x0039b, 0x0039c, 0x0039d, 0x0039e, 0x0039f, 0x003a0, 0x003a1, 0x003f4,
+ 0x003a3, 0x003a4, 0x003a5, 0x003a6, 0x003a7, 0x003a8, 0x003a9, 0x02207,
+ 0x003b1, 0x003b2, 0x003b3, 0x003b4, 0x003b5, 0x003b6, 0x003b7, 0x003b8,
+ 0x003b9, 0x003ba, 0x003bb, 0x003bc, 0x003bd, 0x003be, 0x003bf, 0x003c0,
+ 0x003c1, 0x003c2, 0x003c3, 0x003c4, 0x003c5, 0x003c6, 0x003c7, 0x003c8,
+ 0x003c9, 0x02202, 0x003f5, 0x003d1, 0x003f0, 0x003d5, 0x003f1, 0x003d6,
+ 0x00391, 0x00392, 0x00393, 0x00394, 0x00395, 0x00396, 0x00397, 0x00398,
+ 0x00399, 0x0039a, 0x0039b, 0x0039c, 0x0039d, 0x0039e, 0x0039f, 0x003a0,
+ 0x003a1, 0x003f4, 0x003a3, 0x003a4, 0x003a5, 0x003a6, 0x003a7, 0x003a8,
+ 0x003a9, 0x02207, 0x003b1, 0x003b2, 0x003b3, 0x003b4, 0x003b5, 0x003b6,
+ 0x003b7, 0x003b8, 0x003b9, 0x003ba, 0x003bb, 0x003bc, 0x003bd, 0x003be,
+ 0x003bf, 0x003c0, 0x003c1, 0x003c2, 0x003c3, 0x003c4, 0x003c5, 0x003c6,
+ 0x003c7, 0x003c8, 0x003c9, 0x02202, 0x003f5, 0x003d1, 0x003f0, 0x003d5,
+ 0x003f1, 0x003d6, 0x00391, 0x00392, 0x00393, 0x00394, 0x00395, 0x00396,
+ 0x00397, 0x00398, 0x00399, 0x0039a, 0x0039b, 0x0039c, 0x0039d, 0x0039e,
+ 0x0039f, 0x003a0, 0x003a1, 0x003f4, 0x003a3, 0x003a4, 0x003a5, 0x003a6,
+ 0x003a7, 0x003a8, 0x003a9, 0x02207, 0x003b1, 0x003b2, 0x003b3, 0x003b4,
+ 0x003b5, 0x003b6, 0x003b7, 0x003b8, 0x003b9, 0x003ba, 0x003bb, 0x003bc,
+ 0x003bd, 0x003be, 0x003bf, 0x003c0, 0x003c1, 0x003c2, 0x003c3, 0x003c4,
+ 0x003c5, 0x003c6, 0x003c7, 0x003c8, 0x003c9, 0x02202, 0x003f5, 0x003d1,
+ 0x003f0, 0x003d5, 0x003f1, 0x003d6, 0x00391, 0x00392, 0x00393, 0x00394,
+ 0x00395, 0x00396, 0x00397, 0x00398, 0x00399, 0x0039a, 0x0039b, 0x0039c,
+ 0x0039d, 0x0039e, 0x0039f, 0x003a0, 0x003a1, 0x003f4, 0x003a3, 0x003a4,
+ 0x003a5, 0x003a6, 0x003a7, 0x003a8, 0x003a9, 0x02207, 0x003b1, 0x003b2,
+ 0x003b3, 0x003b4, 0x003b5, 0x003b6, 0x003b7, 0x003b8, 0x003b9, 0x003ba,
+ 0x003bb, 0x003bc, 0x003bd, 0x003be, 0x003bf, 0x003c0, 0x003c1, 0x003c2,
+ 0x003c3, 0x003c4, 0x003c5, 0x003c6, 0x003c7, 0x003c8, 0x003c9, 0x02202,
+ 0x003f5, 0x003d1, 0x003f0, 0x003d5, 0x003f1, 0x003d6, 0x00391, 0x00392,
+ 0x00393, 0x00394, 0x00395, 0x00396, 0x00397, 0x00398, 0x00399, 0x0039a,
+ 0x0039b, 0x0039c, 0x0039d, 0x0039e, 0x0039f, 0x003a0, 0x003a1, 0x003f4,
+ 0x003a3, 0x003a4, 0x003a5, 0x003a6, 0x003a7, 0x003a8, 0x003a9, 0x02207,
+ 0x003b1, 0x003b2, 0x003b3, 0x003b4, 0x003b5, 0x003b6, 0x003b7, 0x003b8,
+ 0x003b9, 0x003ba, 0x003bb, 0x003bc, 0x003bd, 0x003be, 0x003bf, 0x003c0,
+ 0x003c1, 0x003c2, 0x003c3, 0x003c4, 0x003c5, 0x003c6, 0x003c7, 0x003c8,
+ 0x003c9, 0x02202, 0x003f5, 0x003d1, 0x003f0, 0x003d5, 0x003f1, 0x003d6,
+ 0x003dc, 0x003dd, 0x00030, 0x00031, 0x00032, 0x00033, 0x00034, 0x00035,
+ 0x00036, 0x00037, 0x00038, 0x00039, 0x00030, 0x00031, 0x00032, 0x00033,
+ 0x00034, 0x00035, 0x00036, 0x00037, 0x00038, 0x00039, 0x00030, 0x00031,
+ 0x00032, 0x00033, 0x00034, 0x00035, 0x00036, 0x00037, 0x00038, 0x00039,
+ 0x00030, 0x00031, 0x00032, 0x00033, 0x00034, 0x00035, 0x00036, 0x00037,
+ 0x00038, 0x00039, 0x00030, 0x00031, 0x00032, 0x00033, 0x00034, 0x00035,
+ 0x00036, 0x00037, 0x00038, 0x00039, 0x00627, 0x00628, 0x0062c, 0x0062f,
+ 0x00648, 0x00632, 0x0062d, 0x00637, 0x0064a, 0x00643, 0x00644, 0x00645,
+ 0x00646, 0x00633, 0x00639, 0x00641, 0x00635, 0x00642, 0x00631, 0x00634,
+ 0x0062a, 0x0062b, 0x0062e, 0x00630, 0x00636, 0x00638, 0x0063a, 0x0066e,
+ 0x006ba, 0x006a1, 0x0066f, 0x00628, 0x0062c, 0x00647, 0x0062d, 0x0064a,
+ 0x00643, 0x00644, 0x00645, 0x00646, 0x00633, 0x00639, 0x00641, 0x00635,
+ 0x00642, 0x00634, 0x0062a, 0x0062b, 0x0062e, 0x00636, 0x0063a, 0x0062c,
+ 0x0062d, 0x0064a, 0x00644, 0x00646, 0x00633, 0x00639, 0x00635, 0x00642,
+ 0x00634, 0x0062e, 0x00636, 0x0063a, 0x006ba, 0x0066f, 0x00628, 0x0062c,
+ 0x00647, 0x0062d, 0x00637, 0x0064a, 0x00643, 0x00645, 0x00646, 0x00633,
+ 0x00639, 0x00641, 0x00635, 0x00642, 0x00634, 0x0062a, 0x0062b, 0x0062e,
+ 0x00636, 0x00638, 0x0063a, 0x0066e, 0x006a1, 0x00627, 0x00628, 0x0062c,
+ 0x0062f, 0x00647, 0x00648, 0x00632, 0x0062d, 0x00637, 0x0064a, 0x00644,
+ 0x00645, 0x00646, 0x00633, 0x00639, 0x00641, 0x00635, 0x00642, 0x00631,
+ 0x00634, 0x0062a, 0x0062b, 0x0062e, 0x00630, 0x00636, 0x00638, 0x0063a,
+ 0x00628, 0x0062c, 0x0062f, 0x00648, 0x00632, 0x0062d, 0x00637, 0x0064a,
+ 0x00644, 0x00645, 0x00646, 0x00633, 0x00639, 0x00641, 0x00635, 0x00642,
+ 0x00631, 0x00634, 0x0062a, 0x0062b, 0x0062e, 0x00630, 0x00636, 0x00638,
+ 0x0063a, 0x00043, 0x00052, 0x00041, 0x00042, 0x00043, 0x00044, 0x00045,
+ 0x00046, 0x00047, 0x00048, 0x00049, 0x0004a, 0x0004b, 0x0004c, 0x0004d,
+ 0x0004e, 0x0004f, 0x00050, 0x00051, 0x00052, 0x00053, 0x00054, 0x00055,
+ 0x00056, 0x00057, 0x00058, 0x00059, 0x0005a, 0x030b5, 0x0624b, 0x05b57,
+ 0x053cc, 0x030c7, 0x04e8c, 0x0591a, 0x089e3, 0x05929, 0x04ea4, 0x06620,
+ 0x07121, 0x06599, 0x0524d, 0x05f8c, 0x0518d, 0x065b0, 0x0521d, 0x07d42,
+ 0x0751f, 0x08ca9, 0x058f0, 0x05439, 0x06f14, 0x06295, 0x06355, 0x04e00,
+ 0x04e09, 0x0904a, 0x05de6, 0x04e2d, 0x053f3, 0x06307, 0x08d70, 0x06253,
+ 0x07981, 0x07a7a, 0x05408, 0x06e80, 0x06709, 0x06708, 0x07533, 0x05272,
+ 0x055b6, 0x0914d, 0x05f97, 0x053ef, 0x04e3d, 0x04e38, 0x04e41, 0x20122,
+ 0x04f60, 0x04fae, 0x04fbb, 0x05002, 0x0507a, 0x05099, 0x050e7, 0x050cf,
+ 0x0349e, 0x2063a, 0x0514d, 0x05154, 0x05164, 0x05177, 0x2051c, 0x034b9,
+ 0x05167, 0x0518d, 0x2054b, 0x05197, 0x051a4, 0x04ecc, 0x051ac, 0x051b5,
+ 0x291df, 0x051f5, 0x05203, 0x034df, 0x0523b, 0x05246, 0x05272, 0x05277,
+ 0x03515, 0x052c7, 0x052c9, 0x052e4, 0x052fa, 0x05305, 0x05306, 0x05317,
+ 0x05349, 0x05351, 0x0535a, 0x05373, 0x0537d, 0x0537f, 0x0537f, 0x0537f,
+ 0x20a2c, 0x07070, 0x053ca, 0x053df, 0x20b63, 0x053eb, 0x053f1, 0x05406,
+ 0x0549e, 0x05438, 0x05448, 0x05468, 0x054a2, 0x054f6, 0x05510, 0x05553,
+ 0x05563, 0x05584, 0x05584, 0x05599, 0x055ab, 0x055b3, 0x055c2, 0x05716,
+ 0x05606, 0x05717, 0x05651, 0x05674, 0x05207, 0x058ee, 0x057ce, 0x057f4,
+ 0x0580d, 0x0578b, 0x05832, 0x05831, 0x058ac, 0x214e4, 0x058f2, 0x058f7,
+ 0x05906, 0x0591a, 0x05922, 0x05962, 0x216a8, 0x216ea, 0x059ec, 0x05a1b,
+ 0x05a27, 0x059d8, 0x05a66, 0x036ee, 0x036fc, 0x05b08, 0x05b3e, 0x05b3e,
+ 0x219c8, 0x05bc3, 0x05bd8, 0x05be7, 0x05bf3, 0x21b18, 0x05bff, 0x05c06,
+ 0x05f53, 0x05c22, 0x03781, 0x05c60, 0x05c6e, 0x05cc0, 0x05c8d, 0x21de4,
+ 0x05d43, 0x21de6, 0x05d6e, 0x05d6b, 0x05d7c, 0x05de1, 0x05de2, 0x0382f,
+ 0x05dfd, 0x05e28, 0x05e3d, 0x05e69, 0x03862, 0x22183, 0x0387c, 0x05eb0,
+ 0x05eb3, 0x05eb6, 0x05eca, 0x2a392, 0x05efe, 0x22331, 0x22331, 0x08201,
+ 0x05f22, 0x05f22, 0x038c7, 0x232b8, 0x261da, 0x05f62, 0x05f6b, 0x038e3,
+ 0x05f9a, 0x05fcd, 0x05fd7, 0x05ff9, 0x06081, 0x0393a, 0x0391c, 0x06094,
+ 0x226d4, 0x060c7, 0x06148, 0x0614c, 0x0614e, 0x0614c, 0x0617a, 0x0618e,
+ 0x061b2, 0x061a4, 0x061af, 0x061de, 0x061f2, 0x061f6, 0x06210, 0x0621b,
+ 0x0625d, 0x062b1, 0x062d4, 0x06350, 0x22b0c, 0x0633d, 0x062fc, 0x06368,
+ 0x06383, 0x063e4, 0x22bf1, 0x06422, 0x063c5, 0x063a9, 0x03a2e, 0x06469,
+ 0x0647e, 0x0649d, 0x06477, 0x03a6c, 0x0654f, 0x0656c, 0x2300a, 0x065e3,
+ 0x066f8, 0x06649, 0x03b19, 0x06691, 0x03b08, 0x03ae4, 0x05192, 0x05195,
+ 0x06700, 0x0669c, 0x080ad, 0x043d9, 0x06717, 0x0671b, 0x06721, 0x0675e,
+ 0x06753, 0x233c3, 0x03b49, 0x067fa, 0x06785, 0x06852, 0x06885, 0x2346d,
+ 0x0688e, 0x0681f, 0x06914, 0x03b9d, 0x06942, 0x069a3, 0x069ea, 0x06aa8,
+ 0x236a3, 0x06adb, 0x03c18, 0x06b21, 0x238a7, 0x06b54, 0x03c4e, 0x06b72,
+ 0x06b9f, 0x06bba, 0x06bbb, 0x23a8d, 0x21d0b, 0x23afa, 0x06c4e, 0x23cbc,
+ 0x06cbf, 0x06ccd, 0x06c67, 0x06d16, 0x06d3e, 0x06d77, 0x06d41, 0x06d69,
+ 0x06d78, 0x06d85, 0x23d1e, 0x06d34, 0x06e2f, 0x06e6e, 0x03d33, 0x06ecb,
+ 0x06ec7, 0x23ed1, 0x06df9, 0x06f6e, 0x23f5e, 0x23f8e, 0x06fc6, 0x07039,
+ 0x0701e, 0x0701b, 0x03d96, 0x0704a, 0x0707d, 0x07077, 0x070ad, 0x20525,
+ 0x07145, 0x24263, 0x0719c, 0x243ab, 0x07228, 0x07235, 0x07250, 0x24608,
+ 0x07280, 0x07295, 0x24735, 0x24814, 0x0737a, 0x0738b, 0x03eac, 0x073a5,
+ 0x03eb8, 0x03eb8, 0x07447, 0x0745c, 0x07471, 0x07485, 0x074ca, 0x03f1b,
+ 0x07524, 0x24c36, 0x0753e, 0x24c92, 0x07570, 0x2219f, 0x07610, 0x24fa1,
+ 0x24fb8, 0x25044, 0x03ffc, 0x04008, 0x076f4, 0x250f3, 0x250f2, 0x25119,
+ 0x25133, 0x0771e, 0x0771f, 0x0771f, 0x0774a, 0x04039, 0x0778b, 0x04046,
+ 0x04096, 0x2541d, 0x0784e, 0x0788c, 0x078cc, 0x040e3, 0x25626, 0x07956,
+ 0x2569a, 0x256c5, 0x0798f, 0x079eb, 0x0412f, 0x07a40, 0x07a4a, 0x07a4f,
+ 0x2597c, 0x25aa7, 0x25aa7, 0x07aee, 0x04202, 0x25bab, 0x07bc6, 0x07bc9,
+ 0x04227, 0x25c80, 0x07cd2, 0x042a0, 0x07ce8, 0x07ce3, 0x07d00, 0x25f86,
+ 0x07d63, 0x04301, 0x07dc7, 0x07e02, 0x07e45, 0x04334, 0x26228, 0x26247,
+ 0x04359, 0x262d9, 0x07f7a, 0x2633e, 0x07f95, 0x07ffa, 0x08005, 0x264da,
+ 0x26523, 0x08060, 0x265a8, 0x08070, 0x2335f, 0x043d5, 0x080b2, 0x08103,
+ 0x0440b, 0x0813e, 0x05ab5, 0x267a7, 0x267b5, 0x23393, 0x2339c, 0x08201,
+ 0x08204, 0x08f9e, 0x0446b, 0x08291, 0x0828b, 0x0829d, 0x052b3, 0x082b1,
+ 0x082b3, 0x082bd, 0x082e6, 0x26b3c, 0x082e5, 0x0831d, 0x08363, 0x083ad,
+ 0x08323, 0x083bd, 0x083e7, 0x08457, 0x08353, 0x083ca, 0x083cc, 0x083dc,
+ 0x26c36, 0x26d6b, 0x26cd5, 0x0452b, 0x084f1, 0x084f3, 0x08516, 0x273ca,
+ 0x08564, 0x26f2c, 0x0455d, 0x04561, 0x26fb1, 0x270d2, 0x0456b, 0x08650,
+ 0x0865c, 0x08667, 0x08669, 0x086a9, 0x08688, 0x0870e, 0x086e2, 0x08779,
+ 0x08728, 0x0876b, 0x08786, 0x045d7, 0x087e1, 0x08801, 0x045f9, 0x08860,
+ 0x08863, 0x27667, 0x088d7, 0x088de, 0x04635, 0x088fa, 0x034bb, 0x278ae,
+ 0x27966, 0x046be, 0x046c7, 0x08aa0, 0x08aed, 0x08b8a, 0x08c55, 0x27ca8,
+ 0x08cab, 0x08cc1, 0x08d1b, 0x08d77, 0x27f2f, 0x20804, 0x08dcb, 0x08dbc,
+ 0x08df0, 0x208de, 0x08ed4, 0x08f38, 0x285d2, 0x285ed, 0x09094, 0x090f1,
+ 0x09111, 0x2872e, 0x0911b, 0x09238, 0x092d7, 0x092d8, 0x0927c, 0x093f9,
+ 0x09415, 0x28bfa, 0x0958b, 0x04995, 0x095b7, 0x28d77, 0x049e6, 0x096c3,
+ 0x05db2, 0x09723, 0x29145, 0x2921a, 0x04a6e, 0x04a76, 0x097e0, 0x2940a,
+ 0x04ab2, 0x29496, 0x0980b, 0x0980b, 0x09829, 0x295b6, 0x098e2, 0x04b33,
+ 0x09929, 0x099a7, 0x099c2, 0x099fe, 0x04bce, 0x29b30, 0x09b12, 0x09c40,
+ 0x09cfd, 0x04cce, 0x04ced, 0x09d67, 0x2a0ce, 0x04cf8, 0x2a105, 0x2a20e,
+ 0x2a291, 0x09ebb, 0x04d56, 0x09ef9, 0x09efe, 0x09f05, 0x09f0f, 0x09f16,
+ 0x09f3b, 0x2a600
+};
+static const uint32_t multidecomp_keys[] = {
+ 0x000a8, 0x000af, 0x000b4, 0x000b8, 0x000bc, 0x000bd, 0x000be, 0x000c0,
+ 0x000c1, 0x000c2, 0x000c3, 0x000c4, 0x000c5, 0x000c7, 0x000c8, 0x000c9,
+ 0x000ca, 0x000cb, 0x000cc, 0x000cd, 0x000ce, 0x000cf, 0x000d1, 0x000d2,
+ 0x000d3, 0x000d4, 0x000d5, 0x000d6, 0x000d9, 0x000da, 0x000db, 0x000dc,
+ 0x000dd, 0x00100, 0x00102, 0x00104, 0x00106, 0x00108, 0x0010a, 0x0010c,
+ 0x0010e, 0x00112, 0x00114, 0x00116, 0x00118, 0x0011a, 0x0011c, 0x0011e,
+ 0x00120, 0x00122, 0x00124, 0x00128, 0x0012a, 0x0012c, 0x0012e, 0x00130,
+ 0x00132, 0x00134, 0x00136, 0x00139, 0x0013b, 0x0013d, 0x0013f, 0x00143,
+ 0x00145, 0x00147, 0x00149, 0x0014c, 0x0014e, 0x00150, 0x00154, 0x00156,
+ 0x00158, 0x0015a, 0x0015c, 0x0015e, 0x00160, 0x00162, 0x00164, 0x00168,
+ 0x0016a, 0x0016c, 0x0016e, 0x00170, 0x00172, 0x00174, 0x00176, 0x00178,
+ 0x00179, 0x0017b, 0x0017d, 0x001a0, 0x001af, 0x001cd, 0x001cf, 0x001d1,
+ 0x001d3, 0x001d5, 0x001d7, 0x001d9, 0x001db, 0x001de, 0x001e0, 0x001e2,
+ 0x001e6, 0x001e8, 0x001ea, 0x001ec, 0x001ee, 0x001f0, 0x001f4, 0x001f8,
+ 0x001fa, 0x001fc, 0x001fe, 0x00200, 0x00202, 0x00204, 0x00206, 0x00208,
+ 0x0020a, 0x0020c, 0x0020e, 0x00210, 0x00212, 0x00214, 0x00216, 0x00218,
+ 0x0021a, 0x0021e, 0x00226, 0x00228, 0x0022a, 0x0022c, 0x0022e, 0x00230,
+ 0x00232, 0x002d8, 0x002d9, 0x002da, 0x002db, 0x002dc, 0x002dd, 0x00344,
+ 0x0037a, 0x00384, 0x00385, 0x00386, 0x00388, 0x00389, 0x0038a, 0x0038c,
+ 0x0038e, 0x0038f, 0x00390, 0x003aa, 0x003ab, 0x003b0, 0x003d3, 0x003d4,
+ 0x00400, 0x00401, 0x00403, 0x00407, 0x0040c, 0x0040d, 0x0040e, 0x00419,
+ 0x00476, 0x004c1, 0x004d0, 0x004d2, 0x004d6, 0x004da, 0x004dc, 0x004de,
+ 0x004e2, 0x004e4, 0x004e6, 0x004ea, 0x004ec, 0x004ee, 0x004f0, 0x004f2,
+ 0x004f4, 0x004f8, 0x00587, 0x00622, 0x00623, 0x00624, 0x00625, 0x00626,
+ 0x00675, 0x00676, 0x00677, 0x00678, 0x006c0, 0x006c2, 0x006d3, 0x00929,
+ 0x00931, 0x00934, 0x00958, 0x00959, 0x0095a, 0x0095b, 0x0095c, 0x0095d,
+ 0x0095e, 0x0095f, 0x009cb, 0x009cc, 0x009dc, 0x009dd, 0x009df, 0x00a33,
+ 0x00a36, 0x00a59, 0x00a5a, 0x00a5b, 0x00a5e, 0x00b48, 0x00b4b, 0x00b4c,
+ 0x00b5c, 0x00b5d, 0x00b94, 0x00bca, 0x00bcb, 0x00bcc, 0x00c48, 0x00cc0,
+ 0x00cc7, 0x00cc8, 0x00cca, 0x00ccb, 0x00d4a, 0x00d4b, 0x00d4c, 0x00dda,
+ 0x00ddc, 0x00ddd, 0x00dde, 0x00e33, 0x00eb3, 0x00edc, 0x00edd, 0x00f43,
+ 0x00f4d, 0x00f52, 0x00f57, 0x00f5c, 0x00f69, 0x00f73, 0x00f75, 0x00f76,
+ 0x00f77, 0x00f78, 0x00f79, 0x00f81, 0x00f93, 0x00f9d, 0x00fa2, 0x00fa7,
+ 0x00fac, 0x00fb9, 0x01026, 0x01b06, 0x01b08, 0x01b0a, 0x01b0c, 0x01b0e,
+ 0x01b12, 0x01b3b, 0x01b3d, 0x01b40, 0x01b41, 0x01b43, 0x01e00, 0x01e02,
+ 0x01e04, 0x01e06, 0x01e08, 0x01e0a, 0x01e0c, 0x01e0e, 0x01e10, 0x01e12,
+ 0x01e14, 0x01e16, 0x01e18, 0x01e1a, 0x01e1c, 0x01e1e, 0x01e20, 0x01e22,
+ 0x01e24, 0x01e26, 0x01e28, 0x01e2a, 0x01e2c, 0x01e2e, 0x01e30, 0x01e32,
+ 0x01e34, 0x01e36, 0x01e38, 0x01e3a, 0x01e3c, 0x01e3e, 0x01e40, 0x01e42,
+ 0x01e44, 0x01e46, 0x01e48, 0x01e4a, 0x01e4c, 0x01e4e, 0x01e50, 0x01e52,
+ 0x01e54, 0x01e56, 0x01e58, 0x01e5a, 0x01e5c, 0x01e5e, 0x01e60, 0x01e62,
+ 0x01e64, 0x01e66, 0x01e68, 0x01e6a, 0x01e6c, 0x01e6e, 0x01e70, 0x01e72,
+ 0x01e74, 0x01e76, 0x01e78, 0x01e7a, 0x01e7c, 0x01e7e, 0x01e80, 0x01e82,
+ 0x01e84, 0x01e86, 0x01e88, 0x01e8a, 0x01e8c, 0x01e8e, 0x01e90, 0x01e92,
+ 0x01e94, 0x01e96, 0x01e97, 0x01e98, 0x01e99, 0x01e9a, 0x01ea0, 0x01ea2,
+ 0x01ea4, 0x01ea6, 0x01ea8, 0x01eaa, 0x01eac, 0x01eae, 0x01eb0, 0x01eb2,
+ 0x01eb4, 0x01eb6, 0x01eb8, 0x01eba, 0x01ebc, 0x01ebe, 0x01ec0, 0x01ec2,
+ 0x01ec4, 0x01ec6, 0x01ec8, 0x01eca, 0x01ecc, 0x01ece, 0x01ed0, 0x01ed2,
+ 0x01ed4, 0x01ed6, 0x01ed8, 0x01eda, 0x01edc, 0x01ede, 0x01ee0, 0x01ee2,
+ 0x01ee4, 0x01ee6, 0x01ee8, 0x01eea, 0x01eec, 0x01eee, 0x01ef0, 0x01ef2,
+ 0x01ef4, 0x01ef6, 0x01ef8, 0x01f08, 0x01f09, 0x01f0a, 0x01f0b, 0x01f0c,
+ 0x01f0d, 0x01f0e, 0x01f0f, 0x01f18, 0x01f19, 0x01f1a, 0x01f1b, 0x01f1c,
+ 0x01f1d, 0x01f28, 0x01f29, 0x01f2a, 0x01f2b, 0x01f2c, 0x01f2d, 0x01f2e,
+ 0x01f2f, 0x01f38, 0x01f39, 0x01f3a, 0x01f3b, 0x01f3c, 0x01f3d, 0x01f3e,
+ 0x01f3f, 0x01f48, 0x01f49, 0x01f4a, 0x01f4b, 0x01f4c, 0x01f4d, 0x01f50,
+ 0x01f52, 0x01f54, 0x01f56, 0x01f59, 0x01f5b, 0x01f5d, 0x01f5f, 0x01f68,
+ 0x01f69, 0x01f6a, 0x01f6b, 0x01f6c, 0x01f6d, 0x01f6e, 0x01f6f, 0x01f88,
+ 0x01f89, 0x01f8a, 0x01f8b, 0x01f8c, 0x01f8d, 0x01f8e, 0x01f8f, 0x01f98,
+ 0x01f99, 0x01f9a, 0x01f9b, 0x01f9c, 0x01f9d, 0x01f9e, 0x01f9f, 0x01fa8,
+ 0x01fa9, 0x01faa, 0x01fab, 0x01fac, 0x01fad, 0x01fae, 0x01faf, 0x01fb2,
+ 0x01fb4, 0x01fb6, 0x01fb7, 0x01fb8, 0x01fb9, 0x01fba, 0x01fbc, 0x01fbd,
+ 0x01fbf, 0x01fc0, 0x01fc1, 0x01fc2, 0x01fc4, 0x01fc6, 0x01fc7, 0x01fc8,
+ 0x01fca, 0x01fcc, 0x01fcd, 0x01fce, 0x01fcf, 0x01fd2, 0x01fd6, 0x01fd7,
+ 0x01fd8, 0x01fd9, 0x01fda, 0x01fdd, 0x01fde, 0x01fdf, 0x01fe2, 0x01fe4,
+ 0x01fe6, 0x01fe7, 0x01fe8, 0x01fe9, 0x01fea, 0x01fec, 0x01fed, 0x01ff2,
+ 0x01ff4, 0x01ff6, 0x01ff7, 0x01ff8, 0x01ffa, 0x01ffc, 0x01ffe, 0x02017,
+ 0x02025, 0x02026, 0x02033, 0x02034, 0x02036, 0x02037, 0x0203c, 0x0203e,
+ 0x02047, 0x02048, 0x02049, 0x02057, 0x020a8, 0x02100, 0x02101, 0x02103,
+ 0x02105, 0x02106, 0x02109, 0x02116, 0x02120, 0x02121, 0x02122, 0x0213b,
+ 0x02150, 0x02151, 0x02152, 0x02153, 0x02154, 0x02155, 0x02156, 0x02157,
+ 0x02158, 0x02159, 0x0215a, 0x0215b, 0x0215c, 0x0215d, 0x0215e, 0x0215f,
+ 0x02161, 0x02162, 0x02163, 0x02165, 0x02166, 0x02167, 0x02168, 0x0216a,
+ 0x0216b, 0x02189, 0x0219a, 0x0219b, 0x021ae, 0x021cd, 0x021ce, 0x021cf,
+ 0x02204, 0x02209, 0x0220c, 0x02224, 0x02226, 0x0222c, 0x0222d, 0x0222f,
+ 0x02230, 0x02241, 0x02244, 0x02247, 0x02249, 0x02260, 0x02262, 0x0226d,
+ 0x0226e, 0x0226f, 0x02270, 0x02271, 0x02274, 0x02275, 0x02278, 0x02279,
+ 0x02280, 0x02281, 0x02284, 0x02285, 0x02288, 0x02289, 0x022ac, 0x022ad,
+ 0x022ae, 0x022af, 0x022e0, 0x022e1, 0x022e2, 0x022e3, 0x022ea, 0x022eb,
+ 0x022ec, 0x022ed, 0x02469, 0x0246a, 0x0246b, 0x0246c, 0x0246d, 0x0246e,
+ 0x0246f, 0x02470, 0x02471, 0x02472, 0x02473, 0x02474, 0x02475, 0x02476,
+ 0x02477, 0x02478, 0x02479, 0x0247a, 0x0247b, 0x0247c, 0x0247d, 0x0247e,
+ 0x0247f, 0x02480, 0x02481, 0x02482, 0x02483, 0x02484, 0x02485, 0x02486,
+ 0x02487, 0x02488, 0x02489, 0x0248a, 0x0248b, 0x0248c, 0x0248d, 0x0248e,
+ 0x0248f, 0x02490, 0x02491, 0x02492, 0x02493, 0x02494, 0x02495, 0x02496,
+ 0x02497, 0x02498, 0x02499, 0x0249a, 0x0249b, 0x0249c, 0x0249d, 0x0249e,
+ 0x0249f, 0x024a0, 0x024a1, 0x024a2, 0x024a3, 0x024a4, 0x024a5, 0x024a6,
+ 0x024a7, 0x024a8, 0x024a9, 0x024aa, 0x024ab, 0x024ac, 0x024ad, 0x024ae,
+ 0x024af, 0x024b0, 0x024b1, 0x024b2, 0x024b3, 0x024b4, 0x024b5, 0x02a0c,
+ 0x02a74, 0x02a75, 0x02a76, 0x02adc, 0x0304c, 0x0304e, 0x03050, 0x03052,
+ 0x03054, 0x03056, 0x03058, 0x0305a, 0x0305c, 0x0305e, 0x03060, 0x03062,
+ 0x03065, 0x03067, 0x03069, 0x03070, 0x03071, 0x03073, 0x03074, 0x03076,
+ 0x03077, 0x03079, 0x0307a, 0x0307c, 0x0307d, 0x03094, 0x0309b, 0x0309c,
+ 0x0309e, 0x0309f, 0x030ac, 0x030ae, 0x030b0, 0x030b2, 0x030b4, 0x030b6,
+ 0x030b8, 0x030ba, 0x030bc, 0x030be, 0x030c0, 0x030c2, 0x030c5, 0x030c7,
+ 0x030c9, 0x030d0, 0x030d1, 0x030d3, 0x030d4, 0x030d6, 0x030d7, 0x030d9,
+ 0x030da, 0x030dc, 0x030dd, 0x030f4, 0x030f7, 0x030f8, 0x030f9, 0x030fa,
+ 0x030fe, 0x030ff, 0x03200, 0x03201, 0x03202, 0x03203, 0x03204, 0x03205,
+ 0x03206, 0x03207, 0x03208, 0x03209, 0x0320a, 0x0320b, 0x0320c, 0x0320d,
+ 0x0320e, 0x0320f, 0x03210, 0x03211, 0x03212, 0x03213, 0x03214, 0x03215,
+ 0x03216, 0x03217, 0x03218, 0x03219, 0x0321a, 0x0321b, 0x0321c, 0x0321d,
+ 0x0321e, 0x03220, 0x03221, 0x03222, 0x03223, 0x03224, 0x03225, 0x03226,
+ 0x03227, 0x03228, 0x03229, 0x0322a, 0x0322b, 0x0322c, 0x0322d, 0x0322e,
+ 0x0322f, 0x03230, 0x03231, 0x03232, 0x03233, 0x03234, 0x03235, 0x03236,
+ 0x03237, 0x03238, 0x03239, 0x0323a, 0x0323b, 0x0323c, 0x0323d, 0x0323e,
+ 0x0323f, 0x03240, 0x03241, 0x03242, 0x03243, 0x03250, 0x03251, 0x03252,
+ 0x03253, 0x03254, 0x03255, 0x03256, 0x03257, 0x03258, 0x03259, 0x0325a,
+ 0x0325b, 0x0325c, 0x0325d, 0x0325e, 0x0325f, 0x0326e, 0x0326f, 0x03270,
+ 0x03271, 0x03272, 0x03273, 0x03274, 0x03275, 0x03276, 0x03277, 0x03278,
+ 0x03279, 0x0327a, 0x0327b, 0x0327c, 0x0327d, 0x0327e, 0x032b1, 0x032b2,
+ 0x032b3, 0x032b4, 0x032b5, 0x032b6, 0x032b7, 0x032b8, 0x032b9, 0x032ba,
+ 0x032bb, 0x032bc, 0x032bd, 0x032be, 0x032bf, 0x032c0, 0x032c1, 0x032c2,
+ 0x032c3, 0x032c4, 0x032c5, 0x032c6, 0x032c7, 0x032c8, 0x032c9, 0x032ca,
+ 0x032cb, 0x032cc, 0x032cd, 0x032ce, 0x032cf, 0x03300, 0x03301, 0x03302,
+ 0x03303, 0x03304, 0x03305, 0x03306, 0x03307, 0x03308, 0x03309, 0x0330a,
+ 0x0330b, 0x0330c, 0x0330d, 0x0330e, 0x0330f, 0x03310, 0x03311, 0x03312,
+ 0x03313, 0x03314, 0x03315, 0x03316, 0x03317, 0x03318, 0x03319, 0x0331a,
+ 0x0331b, 0x0331c, 0x0331d, 0x0331e, 0x0331f, 0x03320, 0x03321, 0x03322,
+ 0x03323, 0x03324, 0x03325, 0x03326, 0x03327, 0x03328, 0x03329, 0x0332a,
+ 0x0332b, 0x0332c, 0x0332d, 0x0332e, 0x0332f, 0x03330, 0x03331, 0x03332,
+ 0x03333, 0x03334, 0x03335, 0x03336, 0x03337, 0x03338, 0x03339, 0x0333a,
+ 0x0333b, 0x0333c, 0x0333d, 0x0333e, 0x0333f, 0x03340, 0x03341, 0x03342,
+ 0x03343, 0x03344, 0x03345, 0x03346, 0x03347, 0x03348, 0x03349, 0x0334a,
+ 0x0334b, 0x0334c, 0x0334d, 0x0334e, 0x0334f, 0x03350, 0x03351, 0x03352,
+ 0x03353, 0x03354, 0x03355, 0x03356, 0x03357, 0x03358, 0x03359, 0x0335a,
+ 0x0335b, 0x0335c, 0x0335d, 0x0335e, 0x0335f, 0x03360, 0x03361, 0x03362,
+ 0x03363, 0x03364, 0x03365, 0x03366, 0x03367, 0x03368, 0x03369, 0x0336a,
+ 0x0336b, 0x0336c, 0x0336d, 0x0336e, 0x0336f, 0x03370, 0x03371, 0x03372,
+ 0x03373, 0x03374, 0x03375, 0x03376, 0x03377, 0x03378, 0x03379, 0x0337a,
+ 0x0337b, 0x0337c, 0x0337d, 0x0337e, 0x0337f, 0x03380, 0x03381, 0x03382,
+ 0x03383, 0x03384, 0x03385, 0x03386, 0x03387, 0x03388, 0x03389, 0x0338a,
+ 0x0338b, 0x0338c, 0x0338d, 0x0338e, 0x0338f, 0x03390, 0x03391, 0x03392,
+ 0x03393, 0x03394, 0x03395, 0x03396, 0x03397, 0x03398, 0x03399, 0x0339a,
+ 0x0339b, 0x0339c, 0x0339d, 0x0339e, 0x0339f, 0x033a0, 0x033a1, 0x033a2,
+ 0x033a3, 0x033a4, 0x033a5, 0x033a6, 0x033a7, 0x033a8, 0x033a9, 0x033aa,
+ 0x033ab, 0x033ac, 0x033ad, 0x033ae, 0x033af, 0x033b0, 0x033b1, 0x033b2,
+ 0x033b3, 0x033b4, 0x033b5, 0x033b6, 0x033b7, 0x033b8, 0x033b9, 0x033ba,
+ 0x033bb, 0x033bc, 0x033bd, 0x033be, 0x033bf, 0x033c0, 0x033c1, 0x033c2,
+ 0x033c3, 0x033c4, 0x033c5, 0x033c6, 0x033c7, 0x033c8, 0x033c9, 0x033ca,
+ 0x033cb, 0x033cc, 0x033cd, 0x033ce, 0x033cf, 0x033d0, 0x033d1, 0x033d2,
+ 0x033d3, 0x033d4, 0x033d5, 0x033d6, 0x033d7, 0x033d8, 0x033d9, 0x033da,
+ 0x033db, 0x033dc, 0x033dd, 0x033de, 0x033df, 0x033e0, 0x033e1, 0x033e2,
+ 0x033e3, 0x033e4, 0x033e5, 0x033e6, 0x033e7, 0x033e8, 0x033e9, 0x033ea,
+ 0x033eb, 0x033ec, 0x033ed, 0x033ee, 0x033ef, 0x033f0, 0x033f1, 0x033f2,
+ 0x033f3, 0x033f4, 0x033f5, 0x033f6, 0x033f7, 0x033f8, 0x033f9, 0x033fa,
+ 0x033fb, 0x033fc, 0x033fd, 0x033fe, 0x033ff, 0x0fb00, 0x0fb01, 0x0fb02,
+ 0x0fb03, 0x0fb04, 0x0fb05, 0x0fb06, 0x0fb13, 0x0fb14, 0x0fb15, 0x0fb16,
+ 0x0fb17, 0x0fb1d, 0x0fb1f, 0x0fb2a, 0x0fb2b, 0x0fb2c, 0x0fb2d, 0x0fb2e,
+ 0x0fb2f, 0x0fb30, 0x0fb31, 0x0fb32, 0x0fb33, 0x0fb34, 0x0fb35, 0x0fb36,
+ 0x0fb38, 0x0fb39, 0x0fb3a, 0x0fb3b, 0x0fb3c, 0x0fb3e, 0x0fb40, 0x0fb41,
+ 0x0fb43, 0x0fb44, 0x0fb46, 0x0fb47, 0x0fb48, 0x0fb49, 0x0fb4a, 0x0fb4b,
+ 0x0fb4c, 0x0fb4d, 0x0fb4e, 0x0fb4f, 0x0fbea, 0x0fbeb, 0x0fbec, 0x0fbed,
+ 0x0fbee, 0x0fbef, 0x0fbf0, 0x0fbf1, 0x0fbf2, 0x0fbf3, 0x0fbf4, 0x0fbf5,
+ 0x0fbf6, 0x0fbf7, 0x0fbf8, 0x0fbf9, 0x0fbfa, 0x0fbfb, 0x0fc00, 0x0fc01,
+ 0x0fc02, 0x0fc03, 0x0fc04, 0x0fc05, 0x0fc06, 0x0fc07, 0x0fc08, 0x0fc09,
+ 0x0fc0a, 0x0fc0b, 0x0fc0c, 0x0fc0d, 0x0fc0e, 0x0fc0f, 0x0fc10, 0x0fc11,
+ 0x0fc12, 0x0fc13, 0x0fc14, 0x0fc15, 0x0fc16, 0x0fc17, 0x0fc18, 0x0fc19,
+ 0x0fc1a, 0x0fc1b, 0x0fc1c, 0x0fc1d, 0x0fc1e, 0x0fc1f, 0x0fc20, 0x0fc21,
+ 0x0fc22, 0x0fc23, 0x0fc24, 0x0fc25, 0x0fc26, 0x0fc27, 0x0fc28, 0x0fc29,
+ 0x0fc2a, 0x0fc2b, 0x0fc2c, 0x0fc2d, 0x0fc2e, 0x0fc2f, 0x0fc30, 0x0fc31,
+ 0x0fc32, 0x0fc33, 0x0fc34, 0x0fc35, 0x0fc36, 0x0fc37, 0x0fc38, 0x0fc39,
+ 0x0fc3a, 0x0fc3b, 0x0fc3c, 0x0fc3d, 0x0fc3e, 0x0fc3f, 0x0fc40, 0x0fc41,
+ 0x0fc42, 0x0fc43, 0x0fc44, 0x0fc45, 0x0fc46, 0x0fc47, 0x0fc48, 0x0fc49,
+ 0x0fc4a, 0x0fc4b, 0x0fc4c, 0x0fc4d, 0x0fc4e, 0x0fc4f, 0x0fc50, 0x0fc51,
+ 0x0fc52, 0x0fc53, 0x0fc54, 0x0fc55, 0x0fc56, 0x0fc57, 0x0fc58, 0x0fc59,
+ 0x0fc5a, 0x0fc5b, 0x0fc5c, 0x0fc5d, 0x0fc5e, 0x0fc5f, 0x0fc60, 0x0fc61,
+ 0x0fc62, 0x0fc63, 0x0fc64, 0x0fc65, 0x0fc66, 0x0fc67, 0x0fc68, 0x0fc69,
+ 0x0fc6a, 0x0fc6b, 0x0fc6c, 0x0fc6d, 0x0fc6e, 0x0fc6f, 0x0fc70, 0x0fc71,
+ 0x0fc72, 0x0fc73, 0x0fc74, 0x0fc75, 0x0fc76, 0x0fc77, 0x0fc78, 0x0fc79,
+ 0x0fc7a, 0x0fc7b, 0x0fc7c, 0x0fc7d, 0x0fc7e, 0x0fc7f, 0x0fc80, 0x0fc81,
+ 0x0fc82, 0x0fc83, 0x0fc84, 0x0fc85, 0x0fc86, 0x0fc87, 0x0fc88, 0x0fc89,
+ 0x0fc8a, 0x0fc8b, 0x0fc8c, 0x0fc8d, 0x0fc8e, 0x0fc8f, 0x0fc90, 0x0fc91,
+ 0x0fc92, 0x0fc93, 0x0fc94, 0x0fc95, 0x0fc96, 0x0fc97, 0x0fc98, 0x0fc99,
+ 0x0fc9a, 0x0fc9b, 0x0fc9c, 0x0fc9d, 0x0fc9e, 0x0fc9f, 0x0fca0, 0x0fca1,
+ 0x0fca2, 0x0fca3, 0x0fca4, 0x0fca5, 0x0fca6, 0x0fca7, 0x0fca8, 0x0fca9,
+ 0x0fcaa, 0x0fcab, 0x0fcac, 0x0fcad, 0x0fcae, 0x0fcaf, 0x0fcb0, 0x0fcb1,
+ 0x0fcb2, 0x0fcb3, 0x0fcb4, 0x0fcb5, 0x0fcb6, 0x0fcb7, 0x0fcb8, 0x0fcb9,
+ 0x0fcba, 0x0fcbb, 0x0fcbc, 0x0fcbd, 0x0fcbe, 0x0fcbf, 0x0fcc0, 0x0fcc1,
+ 0x0fcc2, 0x0fcc3, 0x0fcc4, 0x0fcc5, 0x0fcc6, 0x0fcc7, 0x0fcc8, 0x0fcc9,
+ 0x0fcca, 0x0fccb, 0x0fccc, 0x0fccd, 0x0fcce, 0x0fccf, 0x0fcd0, 0x0fcd1,
+ 0x0fcd2, 0x0fcd3, 0x0fcd4, 0x0fcd5, 0x0fcd6, 0x0fcd7, 0x0fcd8, 0x0fcd9,
+ 0x0fcda, 0x0fcdb, 0x0fcdc, 0x0fcdd, 0x0fcde, 0x0fcdf, 0x0fce0, 0x0fce1,
+ 0x0fce2, 0x0fce3, 0x0fce4, 0x0fce5, 0x0fce6, 0x0fce7, 0x0fce8, 0x0fce9,
+ 0x0fcea, 0x0fceb, 0x0fcec, 0x0fced, 0x0fcee, 0x0fcef, 0x0fcf0, 0x0fcf1,
+ 0x0fcf2, 0x0fcf3, 0x0fcf4, 0x0fcf5, 0x0fcf6, 0x0fcf7, 0x0fcf8, 0x0fcf9,
+ 0x0fcfa, 0x0fcfb, 0x0fcfc, 0x0fcfd, 0x0fcfe, 0x0fcff, 0x0fd00, 0x0fd01,
+ 0x0fd02, 0x0fd03, 0x0fd04, 0x0fd05, 0x0fd06, 0x0fd07, 0x0fd08, 0x0fd09,
+ 0x0fd0a, 0x0fd0b, 0x0fd0c, 0x0fd0d, 0x0fd0e, 0x0fd0f, 0x0fd10, 0x0fd11,
+ 0x0fd12, 0x0fd13, 0x0fd14, 0x0fd15, 0x0fd16, 0x0fd17, 0x0fd18, 0x0fd19,
+ 0x0fd1a, 0x0fd1b, 0x0fd1c, 0x0fd1d, 0x0fd1e, 0x0fd1f, 0x0fd20, 0x0fd21,
+ 0x0fd22, 0x0fd23, 0x0fd24, 0x0fd25, 0x0fd26, 0x0fd27, 0x0fd28, 0x0fd29,
+ 0x0fd2a, 0x0fd2b, 0x0fd2c, 0x0fd2d, 0x0fd2e, 0x0fd2f, 0x0fd30, 0x0fd31,
+ 0x0fd32, 0x0fd33, 0x0fd34, 0x0fd35, 0x0fd36, 0x0fd37, 0x0fd38, 0x0fd39,
+ 0x0fd3a, 0x0fd3b, 0x0fd3c, 0x0fd3d, 0x0fd50, 0x0fd51, 0x0fd52, 0x0fd53,
+ 0x0fd54, 0x0fd55, 0x0fd56, 0x0fd57, 0x0fd58, 0x0fd59, 0x0fd5a, 0x0fd5b,
+ 0x0fd5c, 0x0fd5d, 0x0fd5e, 0x0fd5f, 0x0fd60, 0x0fd61, 0x0fd62, 0x0fd63,
+ 0x0fd64, 0x0fd65, 0x0fd66, 0x0fd67, 0x0fd68, 0x0fd69, 0x0fd6a, 0x0fd6b,
+ 0x0fd6c, 0x0fd6d, 0x0fd6e, 0x0fd6f, 0x0fd70, 0x0fd71, 0x0fd72, 0x0fd73,
+ 0x0fd74, 0x0fd75, 0x0fd76, 0x0fd77, 0x0fd78, 0x0fd79, 0x0fd7a, 0x0fd7b,
+ 0x0fd7c, 0x0fd7d, 0x0fd7e, 0x0fd7f, 0x0fd80, 0x0fd81, 0x0fd82, 0x0fd83,
+ 0x0fd84, 0x0fd85, 0x0fd86, 0x0fd87, 0x0fd88, 0x0fd89, 0x0fd8a, 0x0fd8b,
+ 0x0fd8c, 0x0fd8d, 0x0fd8e, 0x0fd8f, 0x0fd92, 0x0fd93, 0x0fd94, 0x0fd95,
+ 0x0fd96, 0x0fd97, 0x0fd98, 0x0fd99, 0x0fd9a, 0x0fd9b, 0x0fd9c, 0x0fd9d,
+ 0x0fd9e, 0x0fd9f, 0x0fda0, 0x0fda1, 0x0fda2, 0x0fda3, 0x0fda4, 0x0fda5,
+ 0x0fda6, 0x0fda7, 0x0fda8, 0x0fda9, 0x0fdaa, 0x0fdab, 0x0fdac, 0x0fdad,
+ 0x0fdae, 0x0fdaf, 0x0fdb0, 0x0fdb1, 0x0fdb2, 0x0fdb3, 0x0fdb4, 0x0fdb5,
+ 0x0fdb6, 0x0fdb7, 0x0fdb8, 0x0fdb9, 0x0fdba, 0x0fdbb, 0x0fdbc, 0x0fdbd,
+ 0x0fdbe, 0x0fdbf, 0x0fdc0, 0x0fdc1, 0x0fdc2, 0x0fdc3, 0x0fdc4, 0x0fdc5,
+ 0x0fdc6, 0x0fdc7, 0x0fdf0, 0x0fdf1, 0x0fdf2, 0x0fdf3, 0x0fdf4, 0x0fdf5,
+ 0x0fdf6, 0x0fdf7, 0x0fdf8, 0x0fdf9, 0x0fdfa, 0x0fdfb, 0x0fdfc, 0x0fe70,
+ 0x0fe71, 0x0fe72, 0x0fe74, 0x0fe76, 0x0fe77, 0x0fe78, 0x0fe79, 0x0fe7a,
+ 0x0fe7b, 0x0fe7c, 0x0fe7d, 0x0fe7e, 0x0fe7f, 0x0fef5, 0x0fef6, 0x0fef7,
+ 0x0fef8, 0x0fef9, 0x0fefa, 0x0fefb, 0x0fefc, 0x1109a, 0x1109c, 0x110ab,
+ 0x1112e, 0x1112f, 0x1134b, 0x1134c, 0x114bb, 0x114bc, 0x114be, 0x115ba,
+ 0x115bb, 0x1d15e, 0x1d15f, 0x1d160, 0x1d161, 0x1d162, 0x1d163, 0x1d164,
+ 0x1d1bb, 0x1d1bc, 0x1d1bd, 0x1d1be, 0x1d1bf, 0x1d1c0, 0x1f100, 0x1f101,
+ 0x1f102, 0x1f103, 0x1f104, 0x1f105, 0x1f106, 0x1f107, 0x1f108, 0x1f109,
+ 0x1f10a, 0x1f110, 0x1f111, 0x1f112, 0x1f113, 0x1f114, 0x1f115, 0x1f116,
+ 0x1f117, 0x1f118, 0x1f119, 0x1f11a, 0x1f11b, 0x1f11c, 0x1f11d, 0x1f11e,
+ 0x1f11f, 0x1f120, 0x1f121, 0x1f122, 0x1f123, 0x1f124, 0x1f125, 0x1f126,
+ 0x1f127, 0x1f128, 0x1f129, 0x1f12a, 0x1f12d, 0x1f12e, 0x1f14a, 0x1f14b,
+ 0x1f14c, 0x1f14d, 0x1f14e, 0x1f14f, 0x1f16a, 0x1f16b, 0x1f190, 0x1f200,
+ 0x1f201, 0x1f240, 0x1f241, 0x1f242, 0x1f243, 0x1f244, 0x1f245, 0x1f246,
+ 0x1f247, 0x1f248
+};
+static const uint16_t multidecomp_offsets[] = {
+ 0x00000, 0x00003, 0x00006, 0x00009, 0x0000c, 0x00010, 0x00014, 0x00018,
+ 0x0001b, 0x0001e, 0x00021, 0x00024, 0x00027, 0x0002a, 0x0002d, 0x00030,
+ 0x00033, 0x00036, 0x00039, 0x0003c, 0x0003f, 0x00042, 0x00045, 0x00048,
+ 0x0004b, 0x0004e, 0x00051, 0x00054, 0x00057, 0x0005a, 0x0005d, 0x00060,
+ 0x00063, 0x00066, 0x00069, 0x0006c, 0x0006f, 0x00072, 0x00075, 0x00078,
+ 0x0007b, 0x0007e, 0x00081, 0x00084, 0x00087, 0x0008a, 0x0008d, 0x00090,
+ 0x00093, 0x00096, 0x00099, 0x0009c, 0x0009f, 0x000a2, 0x000a5, 0x000a8,
+ 0x000ab, 0x000ae, 0x000b1, 0x000b4, 0x000b7, 0x000ba, 0x000bd, 0x000c0,
+ 0x000c3, 0x000c6, 0x000c9, 0x000cc, 0x000cf, 0x000d2, 0x000d5, 0x000d8,
+ 0x000db, 0x000de, 0x000e1, 0x000e4, 0x000e7, 0x000ea, 0x000ed, 0x000f0,
+ 0x000f3, 0x000f6, 0x000f9, 0x000fc, 0x000ff, 0x00102, 0x00105, 0x00108,
+ 0x0010b, 0x0010e, 0x00111, 0x00114, 0x00117, 0x0011a, 0x0011d, 0x00120,
+ 0x00123, 0x00126, 0x00129, 0x0012c, 0x0012f, 0x00132, 0x00135, 0x00138,
+ 0x0013b, 0x0013e, 0x00141, 0x00144, 0x00147, 0x0014a, 0x0014d, 0x00150,
+ 0x00153, 0x00156, 0x00159, 0x0015c, 0x0015f, 0x00162, 0x00165, 0x00168,
+ 0x0016b, 0x0016e, 0x00171, 0x00174, 0x00177, 0x0017a, 0x0017d, 0x00180,
+ 0x00183, 0x00186, 0x00189, 0x0018c, 0x0018f, 0x00192, 0x00195, 0x00198,
+ 0x0019b, 0x0019e, 0x001a1, 0x001a4, 0x001a7, 0x001aa, 0x001ad, 0x001b0,
+ 0x001b3, 0x001b6, 0x001b9, 0x001bc, 0x001bf, 0x001c2, 0x001c5, 0x001c8,
+ 0x001cb, 0x001ce, 0x001d1, 0x001d4, 0x001d7, 0x001da, 0x001dd, 0x001e0,
+ 0x001e3, 0x001e6, 0x001e9, 0x001ec, 0x001ef, 0x001f2, 0x001f5, 0x001f8,
+ 0x001fb, 0x001fe, 0x00201, 0x00204, 0x00207, 0x0020a, 0x0020d, 0x00210,
+ 0x00213, 0x00216, 0x00219, 0x0021c, 0x0021f, 0x00222, 0x00225, 0x00228,
+ 0x0022b, 0x0022e, 0x00231, 0x00234, 0x00237, 0x0023a, 0x0023d, 0x00240,
+ 0x00243, 0x00246, 0x00249, 0x0024c, 0x0024f, 0x00252, 0x00255, 0x00258,
+ 0x0025b, 0x0025e, 0x00261, 0x00264, 0x00267, 0x0026a, 0x0026d, 0x00270,
+ 0x00273, 0x00276, 0x00279, 0x0027c, 0x0027f, 0x00282, 0x00285, 0x00288,
+ 0x0028b, 0x0028e, 0x00291, 0x00294, 0x00297, 0x0029a, 0x0029d, 0x002a0,
+ 0x002a3, 0x002a6, 0x002a9, 0x002ac, 0x002af, 0x002b2, 0x002b5, 0x002b8,
+ 0x002bb, 0x002be, 0x002c1, 0x002c4, 0x002c7, 0x002ca, 0x002cd, 0x002d0,
+ 0x002d3, 0x002d6, 0x002d9, 0x002dc, 0x002df, 0x002e2, 0x002e5, 0x002e8,
+ 0x002eb, 0x002ee, 0x002f1, 0x002f4, 0x002f7, 0x002fa, 0x002fd, 0x00300,
+ 0x00303, 0x00306, 0x00309, 0x0030c, 0x0030f, 0x00312, 0x00315, 0x00318,
+ 0x0031b, 0x0031e, 0x00321, 0x00324, 0x00327, 0x0032a, 0x0032d, 0x00330,
+ 0x00333, 0x00336, 0x00339, 0x0033c, 0x0033f, 0x00342, 0x00345, 0x00348,
+ 0x0034b, 0x0034e, 0x00351, 0x00354, 0x00357, 0x0035a, 0x0035d, 0x00360,
+ 0x00363, 0x00366, 0x00369, 0x0036c, 0x0036f, 0x00372, 0x00375, 0x00378,
+ 0x0037b, 0x0037e, 0x00381, 0x00384, 0x00387, 0x0038a, 0x0038d, 0x00390,
+ 0x00393, 0x00396, 0x00399, 0x0039c, 0x0039f, 0x003a2, 0x003a5, 0x003a8,
+ 0x003ab, 0x003ae, 0x003b1, 0x003b4, 0x003b7, 0x003ba, 0x003bd, 0x003c0,
+ 0x003c3, 0x003c6, 0x003c9, 0x003cc, 0x003cf, 0x003d2, 0x003d5, 0x003d8,
+ 0x003db, 0x003de, 0x003e1, 0x003e4, 0x003e7, 0x003ea, 0x003ed, 0x003f0,
+ 0x003f3, 0x003f6, 0x003f9, 0x003fc, 0x003ff, 0x00402, 0x00405, 0x00408,
+ 0x0040b, 0x0040e, 0x00411, 0x00414, 0x00417, 0x0041a, 0x0041d, 0x00420,
+ 0x00423, 0x00426, 0x00429, 0x0042c, 0x0042f, 0x00432, 0x00435, 0x00438,
+ 0x0043b, 0x0043e, 0x00441, 0x00444, 0x00447, 0x0044a, 0x0044d, 0x00450,
+ 0x00453, 0x00456, 0x00459, 0x0045c, 0x0045f, 0x00462, 0x00465, 0x00468,
+ 0x0046b, 0x0046e, 0x00471, 0x00474, 0x00477, 0x0047a, 0x0047d, 0x00480,
+ 0x00483, 0x00486, 0x00489, 0x0048c, 0x0048f, 0x00492, 0x00495, 0x00498,
+ 0x0049b, 0x0049e, 0x004a1, 0x004a4, 0x004a7, 0x004aa, 0x004ad, 0x004b0,
+ 0x004b3, 0x004b6, 0x004b9, 0x004bc, 0x004bf, 0x004c2, 0x004c5, 0x004c8,
+ 0x004cb, 0x004ce, 0x004d1, 0x004d4, 0x004d7, 0x004da, 0x004dd, 0x004e0,
+ 0x004e3, 0x004e6, 0x004e9, 0x004ec, 0x004ef, 0x004f2, 0x004f5, 0x004f8,
+ 0x004fb, 0x004fe, 0x00501, 0x00504, 0x00507, 0x0050a, 0x0050d, 0x00510,
+ 0x00513, 0x00516, 0x00519, 0x0051c, 0x0051f, 0x00522, 0x00525, 0x00528,
+ 0x0052b, 0x0052e, 0x00531, 0x00534, 0x00537, 0x0053a, 0x0053d, 0x00540,
+ 0x00543, 0x00546, 0x00549, 0x0054c, 0x0054f, 0x00552, 0x00555, 0x00558,
+ 0x0055b, 0x0055e, 0x00561, 0x00564, 0x00567, 0x0056a, 0x0056d, 0x00570,
+ 0x00573, 0x00576, 0x00579, 0x0057c, 0x0057f, 0x00582, 0x00585, 0x00588,
+ 0x0058b, 0x0058e, 0x00591, 0x00594, 0x00597, 0x0059a, 0x0059d, 0x005a0,
+ 0x005a3, 0x005a6, 0x005a9, 0x005ac, 0x005af, 0x005b2, 0x005b5, 0x005b8,
+ 0x005bb, 0x005be, 0x005c1, 0x005c4, 0x005c7, 0x005ca, 0x005cd, 0x005d0,
+ 0x005d3, 0x005d6, 0x005d9, 0x005dc, 0x005df, 0x005e2, 0x005e5, 0x005e8,
+ 0x005eb, 0x005ee, 0x005f1, 0x005f4, 0x005f7, 0x005fa, 0x005fd, 0x00600,
+ 0x00603, 0x00606, 0x00609, 0x0060c, 0x0060f, 0x00612, 0x00615, 0x00618,
+ 0x0061b, 0x0061e, 0x00621, 0x00624, 0x00627, 0x0062a, 0x0062d, 0x00630,
+ 0x00633, 0x00636, 0x0063a, 0x0063d, 0x00641, 0x00644, 0x00648, 0x0064b,
+ 0x0064e, 0x00651, 0x00654, 0x00657, 0x0065c, 0x0065f, 0x00663, 0x00667,
+ 0x0066a, 0x0066e, 0x00672, 0x00675, 0x00678, 0x0067b, 0x0067f, 0x00682,
+ 0x00686, 0x0068a, 0x0068e, 0x00693, 0x00697, 0x0069b, 0x0069f, 0x006a3,
+ 0x006a7, 0x006ab, 0x006af, 0x006b3, 0x006b7, 0x006bb, 0x006bf, 0x006c3,
+ 0x006c6, 0x006c9, 0x006cd, 0x006d0, 0x006d3, 0x006d7, 0x006dc, 0x006df,
+ 0x006e2, 0x006e6, 0x006ea, 0x006ed, 0x006f0, 0x006f3, 0x006f6, 0x006f9,
+ 0x006fc, 0x006ff, 0x00702, 0x00705, 0x00708, 0x0070b, 0x0070e, 0x00712,
+ 0x00715, 0x00719, 0x0071c, 0x0071f, 0x00722, 0x00725, 0x00728, 0x0072b,
+ 0x0072e, 0x00731, 0x00734, 0x00737, 0x0073a, 0x0073d, 0x00740, 0x00743,
+ 0x00746, 0x00749, 0x0074c, 0x0074f, 0x00752, 0x00755, 0x00758, 0x0075b,
+ 0x0075e, 0x00761, 0x00764, 0x00767, 0x0076a, 0x0076d, 0x00770, 0x00773,
+ 0x00776, 0x00779, 0x0077c, 0x0077f, 0x00782, 0x00785, 0x00788, 0x0078b,
+ 0x0078e, 0x00791, 0x00794, 0x00797, 0x0079a, 0x0079d, 0x007a1, 0x007a5,
+ 0x007a9, 0x007ad, 0x007b1, 0x007b5, 0x007b9, 0x007bd, 0x007c1, 0x007c6,
+ 0x007cb, 0x007d0, 0x007d5, 0x007da, 0x007df, 0x007e4, 0x007e9, 0x007ee,
+ 0x007f3, 0x007f8, 0x007fb, 0x007fe, 0x00801, 0x00804, 0x00807, 0x0080a,
+ 0x0080d, 0x00810, 0x00813, 0x00817, 0x0081b, 0x0081f, 0x00823, 0x00827,
+ 0x0082b, 0x0082f, 0x00833, 0x00837, 0x0083b, 0x0083f, 0x00843, 0x00847,
+ 0x0084b, 0x0084f, 0x00853, 0x00857, 0x0085b, 0x0085f, 0x00863, 0x00867,
+ 0x0086b, 0x0086f, 0x00873, 0x00877, 0x0087b, 0x0087f, 0x00883, 0x00887,
+ 0x0088b, 0x0088f, 0x00893, 0x00897, 0x0089b, 0x0089f, 0x008a3, 0x008a7,
+ 0x008ac, 0x008b0, 0x008b3, 0x008b7, 0x008ba, 0x008bd, 0x008c0, 0x008c3,
+ 0x008c6, 0x008c9, 0x008cc, 0x008cf, 0x008d2, 0x008d5, 0x008d8, 0x008db,
+ 0x008de, 0x008e1, 0x008e4, 0x008e7, 0x008ea, 0x008ed, 0x008f0, 0x008f3,
+ 0x008f6, 0x008f9, 0x008fc, 0x008ff, 0x00902, 0x00905, 0x00908, 0x0090b,
+ 0x0090e, 0x00911, 0x00914, 0x00917, 0x0091a, 0x0091d, 0x00920, 0x00923,
+ 0x00926, 0x00929, 0x0092c, 0x0092f, 0x00932, 0x00935, 0x00938, 0x0093b,
+ 0x0093e, 0x00941, 0x00944, 0x00947, 0x0094a, 0x0094d, 0x00950, 0x00953,
+ 0x00956, 0x00959, 0x0095c, 0x0095f, 0x00962, 0x00965, 0x00968, 0x0096b,
+ 0x0096e, 0x00971, 0x00974, 0x00978, 0x0097c, 0x00980, 0x00984, 0x00988,
+ 0x0098c, 0x00990, 0x00994, 0x00998, 0x0099c, 0x009a0, 0x009a4, 0x009a8,
+ 0x009ac, 0x009b1, 0x009b6, 0x009bb, 0x009c0, 0x009c5, 0x009ca, 0x009cf,
+ 0x009d4, 0x009d9, 0x009de, 0x009e3, 0x009e8, 0x009ed, 0x009f2, 0x009f7,
+ 0x009ff, 0x00a06, 0x00a0a, 0x00a0e, 0x00a12, 0x00a16, 0x00a1a, 0x00a1e,
+ 0x00a22, 0x00a26, 0x00a2a, 0x00a2e, 0x00a32, 0x00a36, 0x00a3a, 0x00a3e,
+ 0x00a42, 0x00a46, 0x00a4a, 0x00a4e, 0x00a52, 0x00a56, 0x00a5a, 0x00a5e,
+ 0x00a62, 0x00a66, 0x00a6a, 0x00a6e, 0x00a72, 0x00a76, 0x00a7a, 0x00a7e,
+ 0x00a82, 0x00a86, 0x00a8a, 0x00a8e, 0x00a92, 0x00a96, 0x00a9a, 0x00a9d,
+ 0x00aa0, 0x00aa3, 0x00aa6, 0x00aa9, 0x00aac, 0x00aaf, 0x00ab2, 0x00ab5,
+ 0x00ab8, 0x00abb, 0x00abe, 0x00ac1, 0x00ac4, 0x00ac7, 0x00aca, 0x00acd,
+ 0x00ad0, 0x00ad3, 0x00ad6, 0x00ad9, 0x00adc, 0x00adf, 0x00ae2, 0x00ae5,
+ 0x00ae8, 0x00aeb, 0x00aee, 0x00af1, 0x00af7, 0x00afc, 0x00aff, 0x00b02,
+ 0x00b05, 0x00b08, 0x00b0b, 0x00b0e, 0x00b11, 0x00b14, 0x00b17, 0x00b1a,
+ 0x00b1d, 0x00b20, 0x00b23, 0x00b26, 0x00b29, 0x00b2c, 0x00b2f, 0x00b32,
+ 0x00b35, 0x00b38, 0x00b3b, 0x00b3e, 0x00b41, 0x00b44, 0x00b47, 0x00b4b,
+ 0x00b4f, 0x00b53, 0x00b56, 0x00b5a, 0x00b5d, 0x00b61, 0x00b66, 0x00b6b,
+ 0x00b70, 0x00b74, 0x00b79, 0x00b7d, 0x00b81, 0x00b87, 0x00b8c, 0x00b90,
+ 0x00b94, 0x00b98, 0x00b9d, 0x00ba2, 0x00ba6, 0x00baa, 0x00bad, 0x00bb1,
+ 0x00bb6, 0x00bbb, 0x00bbe, 0x00bc4, 0x00bcb, 0x00bd1, 0x00bd5, 0x00bdb,
+ 0x00be1, 0x00be6, 0x00bea, 0x00bee, 0x00bf2, 0x00bf7, 0x00bfd, 0x00c02,
+ 0x00c06, 0x00c0a, 0x00c0e, 0x00c11, 0x00c14, 0x00c17, 0x00c1a, 0x00c1e,
+ 0x00c22, 0x00c28, 0x00c2c, 0x00c31, 0x00c37, 0x00c3b, 0x00c3e, 0x00c41,
+ 0x00c47, 0x00c4c, 0x00c52, 0x00c56, 0x00c5c, 0x00c5f, 0x00c63, 0x00c67,
+ 0x00c6b, 0x00c6f, 0x00c73, 0x00c78, 0x00c7c, 0x00c7f, 0x00c83, 0x00c87,
+ 0x00c8b, 0x00c90, 0x00c94, 0x00c98, 0x00c9c, 0x00ca2, 0x00ca7, 0x00caa,
+ 0x00cb0, 0x00cb3, 0x00cb8, 0x00cbd, 0x00cc1, 0x00cc5, 0x00cc9, 0x00cce,
+ 0x00cd1, 0x00cd5, 0x00cda, 0x00cdd, 0x00ce3, 0x00ce7, 0x00cea, 0x00ced,
+ 0x00cf0, 0x00cf3, 0x00cf6, 0x00cf9, 0x00cfc, 0x00cff, 0x00d02, 0x00d05,
+ 0x00d09, 0x00d0d, 0x00d11, 0x00d15, 0x00d19, 0x00d1d, 0x00d21, 0x00d25,
+ 0x00d29, 0x00d2d, 0x00d31, 0x00d35, 0x00d39, 0x00d3d, 0x00d41, 0x00d45,
+ 0x00d48, 0x00d4b, 0x00d4f, 0x00d52, 0x00d55, 0x00d58, 0x00d5c, 0x00d60,
+ 0x00d63, 0x00d66, 0x00d69, 0x00d6c, 0x00d6f, 0x00d74, 0x00d77, 0x00d7a,
+ 0x00d7d, 0x00d80, 0x00d83, 0x00d86, 0x00d89, 0x00d8c, 0x00d90, 0x00d95,
+ 0x00d98, 0x00d9b, 0x00d9e, 0x00da1, 0x00da4, 0x00da7, 0x00daa, 0x00dae,
+ 0x00db2, 0x00db6, 0x00dba, 0x00dbd, 0x00dc0, 0x00dc3, 0x00dc6, 0x00dc9,
+ 0x00dcc, 0x00dcf, 0x00dd2, 0x00dd5, 0x00dd8, 0x00ddc, 0x00de0, 0x00de3,
+ 0x00de7, 0x00deb, 0x00def, 0x00df2, 0x00df6, 0x00dfa, 0x00dff, 0x00e02,
+ 0x00e06, 0x00e0a, 0x00e0e, 0x00e12, 0x00e18, 0x00e1f, 0x00e22, 0x00e25,
+ 0x00e28, 0x00e2b, 0x00e2e, 0x00e31, 0x00e34, 0x00e37, 0x00e3a, 0x00e3d,
+ 0x00e40, 0x00e43, 0x00e46, 0x00e49, 0x00e4c, 0x00e4f, 0x00e52, 0x00e55,
+ 0x00e5a, 0x00e5d, 0x00e60, 0x00e63, 0x00e68, 0x00e6c, 0x00e6f, 0x00e72,
+ 0x00e75, 0x00e78, 0x00e7b, 0x00e7e, 0x00e81, 0x00e84, 0x00e87, 0x00e8a,
+ 0x00e8e, 0x00e91, 0x00e94, 0x00e98, 0x00e9c, 0x00e9f, 0x00ea4, 0x00ea8,
+ 0x00eab, 0x00eae, 0x00eb1, 0x00eb4, 0x00eb8, 0x00ebc, 0x00ebf, 0x00ec2,
+ 0x00ec5, 0x00ec8, 0x00ecb, 0x00ece, 0x00ed1, 0x00ed4, 0x00ed7, 0x00edb,
+ 0x00edf, 0x00ee3, 0x00ee7, 0x00eeb, 0x00eef, 0x00ef3, 0x00ef7, 0x00efb,
+ 0x00eff, 0x00f03, 0x00f07, 0x00f0b, 0x00f0f, 0x00f13, 0x00f17, 0x00f1b,
+ 0x00f1f, 0x00f23, 0x00f27, 0x00f2b, 0x00f2f, 0x00f33, 0x00f36, 0x00f39,
+ 0x00f3c, 0x00f40, 0x00f44, 0x00f47, 0x00f4a, 0x00f4d, 0x00f50, 0x00f53,
+ 0x00f56, 0x00f59, 0x00f5c, 0x00f5f, 0x00f62, 0x00f65, 0x00f68, 0x00f6b,
+ 0x00f6e, 0x00f71, 0x00f74, 0x00f77, 0x00f7a, 0x00f7d, 0x00f80, 0x00f83,
+ 0x00f86, 0x00f89, 0x00f8c, 0x00f8f, 0x00f92, 0x00f95, 0x00f98, 0x00f9b,
+ 0x00f9e, 0x00fa1, 0x00fa4, 0x00fa7, 0x00faa, 0x00fad, 0x00fb0, 0x00fb3,
+ 0x00fb6, 0x00fb9, 0x00fbc, 0x00fbf, 0x00fc2, 0x00fc5, 0x00fc8, 0x00fcb,
+ 0x00fce, 0x00fd1, 0x00fd4, 0x00fd7, 0x00fda, 0x00fdd, 0x00fe0, 0x00fe3,
+ 0x00fe6, 0x00fe9, 0x00fec, 0x00fef, 0x00ff2, 0x00ff5, 0x00ff8, 0x00ffb,
+ 0x00ffe, 0x01001, 0x01004, 0x01007, 0x0100a, 0x0100d, 0x01010, 0x01013,
+ 0x01016, 0x01019, 0x0101c, 0x0101f, 0x01022, 0x01025, 0x01028, 0x0102b,
+ 0x0102e, 0x01031, 0x01034, 0x01037, 0x0103a, 0x0103d, 0x01040, 0x01043,
+ 0x01046, 0x01049, 0x0104c, 0x0104f, 0x01052, 0x01055, 0x01058, 0x0105b,
+ 0x0105e, 0x01061, 0x01064, 0x01067, 0x0106a, 0x0106d, 0x01070, 0x01073,
+ 0x01076, 0x01079, 0x0107c, 0x0107f, 0x01082, 0x01085, 0x01088, 0x0108b,
+ 0x0108e, 0x01091, 0x01094, 0x01097, 0x0109a, 0x0109d, 0x010a0, 0x010a3,
+ 0x010a6, 0x010a9, 0x010ac, 0x010af, 0x010b2, 0x010b5, 0x010b8, 0x010bb,
+ 0x010be, 0x010c1, 0x010c4, 0x010c7, 0x010ca, 0x010cd, 0x010d0, 0x010d3,
+ 0x010d6, 0x010d9, 0x010dc, 0x010df, 0x010e2, 0x010e5, 0x010e8, 0x010eb,
+ 0x010ee, 0x010f1, 0x010f4, 0x010f7, 0x010fa, 0x010fd, 0x01100, 0x01103,
+ 0x01106, 0x01109, 0x0110c, 0x0110f, 0x01112, 0x01116, 0x0111a, 0x0111e,
+ 0x01122, 0x01126, 0x0112a, 0x0112d, 0x01130, 0x01133, 0x01136, 0x01139,
+ 0x0113c, 0x0113f, 0x01142, 0x01145, 0x01148, 0x0114b, 0x0114e, 0x01151,
+ 0x01154, 0x01157, 0x0115a, 0x0115d, 0x01160, 0x01163, 0x01166, 0x01169,
+ 0x0116c, 0x0116f, 0x01172, 0x01175, 0x01178, 0x0117b, 0x0117e, 0x01181,
+ 0x01184, 0x01187, 0x0118a, 0x0118d, 0x01190, 0x01193, 0x01196, 0x01199,
+ 0x0119c, 0x0119f, 0x011a2, 0x011a5, 0x011a8, 0x011ab, 0x011ae, 0x011b1,
+ 0x011b4, 0x011b7, 0x011ba, 0x011bd, 0x011c0, 0x011c3, 0x011c6, 0x011c9,
+ 0x011cc, 0x011cf, 0x011d2, 0x011d5, 0x011d8, 0x011db, 0x011de, 0x011e1,
+ 0x011e4, 0x011e7, 0x011ea, 0x011ed, 0x011f0, 0x011f3, 0x011f6, 0x011f9,
+ 0x011fc, 0x011ff, 0x01202, 0x01205, 0x01208, 0x0120b, 0x0120e, 0x01211,
+ 0x01214, 0x01217, 0x0121a, 0x0121d, 0x01220, 0x01223, 0x01226, 0x01229,
+ 0x0122c, 0x0122f, 0x01232, 0x01235, 0x01238, 0x0123b, 0x0123e, 0x01241,
+ 0x01244, 0x01247, 0x0124a, 0x0124d, 0x01250, 0x01253, 0x01256, 0x01259,
+ 0x0125c, 0x0125f, 0x01262, 0x01265, 0x01268, 0x0126b, 0x0126e, 0x01271,
+ 0x01274, 0x01277, 0x0127a, 0x0127d, 0x01280, 0x01283, 0x01286, 0x01289,
+ 0x0128c, 0x0128f, 0x01292, 0x01295, 0x01298, 0x0129b, 0x0129e, 0x012a1,
+ 0x012a4, 0x012a7, 0x012aa, 0x012ad, 0x012b0, 0x012b3, 0x012b6, 0x012b9,
+ 0x012bc, 0x012bf, 0x012c2, 0x012c5, 0x012c8, 0x012cb, 0x012ce, 0x012d1,
+ 0x012d4, 0x012d8, 0x012dc, 0x012e0, 0x012e3, 0x012e6, 0x012e9, 0x012ec,
+ 0x012ef, 0x012f2, 0x012f5, 0x012f8, 0x012fb, 0x012fe, 0x01301, 0x01304,
+ 0x01307, 0x0130a, 0x0130d, 0x01310, 0x01313, 0x01316, 0x01319, 0x0131c,
+ 0x0131f, 0x01322, 0x01325, 0x01328, 0x0132b, 0x0132e, 0x01331, 0x01334,
+ 0x01337, 0x0133a, 0x0133d, 0x01340, 0x01343, 0x01346, 0x01349, 0x0134c,
+ 0x0134f, 0x01352, 0x01355, 0x01358, 0x0135b, 0x0135e, 0x01361, 0x01364,
+ 0x01367, 0x0136a, 0x0136d, 0x01370, 0x01373, 0x01376, 0x01379, 0x0137c,
+ 0x0137f, 0x01382, 0x01385, 0x01388, 0x0138b, 0x0138e, 0x01391, 0x01394,
+ 0x01397, 0x0139a, 0x0139d, 0x013a0, 0x013a3, 0x013a6, 0x013a9, 0x013ac,
+ 0x013af, 0x013b2, 0x013b5, 0x013b8, 0x013bb, 0x013bf, 0x013c3, 0x013c7,
+ 0x013cb, 0x013cf, 0x013d3, 0x013d7, 0x013db, 0x013df, 0x013e3, 0x013e7,
+ 0x013eb, 0x013ef, 0x013f3, 0x013f7, 0x013fb, 0x013ff, 0x01403, 0x01407,
+ 0x0140b, 0x0140f, 0x01413, 0x01417, 0x0141b, 0x0141f, 0x01423, 0x01427,
+ 0x0142b, 0x0142f, 0x01433, 0x01437, 0x0143b, 0x0143f, 0x01443, 0x01447,
+ 0x0144b, 0x0144f, 0x01453, 0x01457, 0x0145b, 0x0145f, 0x01463, 0x01467,
+ 0x0146b, 0x0146f, 0x01473, 0x01477, 0x0147b, 0x0147f, 0x01483, 0x01487,
+ 0x0148b, 0x0148f, 0x01493, 0x01497, 0x0149b, 0x0149f, 0x014a3, 0x014a7,
+ 0x014ab, 0x014af, 0x014b3, 0x014b7, 0x014bb, 0x014bf, 0x014c3, 0x014c7,
+ 0x014cb, 0x014cf, 0x014d3, 0x014d7, 0x014db, 0x014df, 0x014e3, 0x014e7,
+ 0x014eb, 0x014ef, 0x014f3, 0x014f7, 0x014fb, 0x014ff, 0x01503, 0x01507,
+ 0x0150b, 0x0150f, 0x01513, 0x01517, 0x0151b, 0x0151f, 0x01523, 0x01527,
+ 0x0152b, 0x0152f, 0x01533, 0x01537, 0x0153b, 0x0153f, 0x01543, 0x01547,
+ 0x0154b, 0x0154f, 0x01553, 0x01557, 0x0155b, 0x0155f, 0x01563, 0x01567,
+ 0x0156b, 0x0156f, 0x01573, 0x01577, 0x0157b, 0x0157f, 0x01583, 0x01587,
+ 0x0158b, 0x0158f, 0x01593, 0x01597, 0x0159b, 0x015a0, 0x015a5, 0x015aa,
+ 0x015af, 0x015b4, 0x015b9, 0x015be, 0x015c2, 0x015d5, 0x015de, 0x015e3,
+ 0x015e6, 0x015e9, 0x015ec, 0x015ef, 0x015f2, 0x015f5, 0x015f8, 0x015fb,
+ 0x015fe, 0x01601, 0x01604, 0x01607, 0x0160a, 0x0160d, 0x01610, 0x01613,
+ 0x01616, 0x01619, 0x0161c, 0x0161f, 0x01622, 0x01625, 0x01628, 0x0162b,
+ 0x0162e, 0x01631, 0x01634, 0x01637, 0x0163a, 0x0163d, 0x01640, 0x01643,
+ 0x01646, 0x01649, 0x0164c, 0x0164f, 0x01652, 0x01655, 0x01658, 0x0165b,
+ 0x0165e, 0x01661, 0x01664, 0x01667, 0x0166a, 0x0166d, 0x01670, 0x01673,
+ 0x01676, 0x01679, 0x0167c, 0x0167f, 0x01682, 0x01685, 0x01688, 0x0168b,
+ 0x0168e, 0x01691, 0x01695, 0x01699, 0x0169d, 0x016a1, 0x016a5, 0x016a9,
+ 0x016ad, 0x016b1, 0x016b5, 0x016b9, 0x016bd, 0x016c1, 0x016c5, 0x016c9,
+ 0x016cd, 0x016d1, 0x016d5, 0x016d9, 0x016dd, 0x016e1, 0x016e5, 0x016e9,
+ 0x016ed, 0x016f1, 0x016f5, 0x016f9, 0x016fd, 0x01700, 0x01703, 0x01706,
+ 0x01709, 0x0170c, 0x0170f, 0x01713, 0x01716, 0x01719, 0x0171c, 0x0171f,
+ 0x01722, 0x01725, 0x01729, 0x0172d, 0x01731, 0x01735, 0x01739, 0x0173d,
+ 0x01741, 0x01745
+};
+static const uint32_t multidecomp_values[] = {
+ 0x00020, 0x00308, 0x00000, 0x00020, 0x00304, 0x00000, 0x00020, 0x00301,
+ 0x00000, 0x00020, 0x00327, 0x00000, 0x00031, 0x02044, 0x00034, 0x00000,
+ 0x00031, 0x02044, 0x00032, 0x00000, 0x00033, 0x02044, 0x00034, 0x00000,
+ 0x00041, 0x00300, 0x00000, 0x00041, 0x00301, 0x00000, 0x00041, 0x00302,
+ 0x00000, 0x00041, 0x00303, 0x00000, 0x00041, 0x00308, 0x00000, 0x00041,
+ 0x0030a, 0x00000, 0x00043, 0x00327, 0x00000, 0x00045, 0x00300, 0x00000,
+ 0x00045, 0x00301, 0x00000, 0x00045, 0x00302, 0x00000, 0x00045, 0x00308,
+ 0x00000, 0x00049, 0x00300, 0x00000, 0x00049, 0x00301, 0x00000, 0x00049,
+ 0x00302, 0x00000, 0x00049, 0x00308, 0x00000, 0x0004e, 0x00303, 0x00000,
+ 0x0004f, 0x00300, 0x00000, 0x0004f, 0x00301, 0x00000, 0x0004f, 0x00302,
+ 0x00000, 0x0004f, 0x00303, 0x00000, 0x0004f, 0x00308, 0x00000, 0x00055,
+ 0x00300, 0x00000, 0x00055, 0x00301, 0x00000, 0x00055, 0x00302, 0x00000,
+ 0x00055, 0x00308, 0x00000, 0x00059, 0x00301, 0x00000, 0x00041, 0x00304,
+ 0x00000, 0x00041, 0x00306, 0x00000, 0x00041, 0x00328, 0x00000, 0x00043,
+ 0x00301, 0x00000, 0x00043, 0x00302, 0x00000, 0x00043, 0x00307, 0x00000,
+ 0x00043, 0x0030c, 0x00000, 0x00044, 0x0030c, 0x00000, 0x00045, 0x00304,
+ 0x00000, 0x00045, 0x00306, 0x00000, 0x00045, 0x00307, 0x00000, 0x00045,
+ 0x00328, 0x00000, 0x00045, 0x0030c, 0x00000, 0x00047, 0x00302, 0x00000,
+ 0x00047, 0x00306, 0x00000, 0x00047, 0x00307, 0x00000, 0x00047, 0x00327,
+ 0x00000, 0x00048, 0x00302, 0x00000, 0x00049, 0x00303, 0x00000, 0x00049,
+ 0x00304, 0x00000, 0x00049, 0x00306, 0x00000, 0x00049, 0x00328, 0x00000,
+ 0x00049, 0x00307, 0x00000, 0x00049, 0x0004a, 0x00000, 0x0004a, 0x00302,
+ 0x00000, 0x0004b, 0x00327, 0x00000, 0x0004c, 0x00301, 0x00000, 0x0004c,
+ 0x00327, 0x00000, 0x0004c, 0x0030c, 0x00000, 0x0004c, 0x000b7, 0x00000,
+ 0x0004e, 0x00301, 0x00000, 0x0004e, 0x00327, 0x00000, 0x0004e, 0x0030c,
+ 0x00000, 0x002bc, 0x0006e, 0x00000, 0x0004f, 0x00304, 0x00000, 0x0004f,
+ 0x00306, 0x00000, 0x0004f, 0x0030b, 0x00000, 0x00052, 0x00301, 0x00000,
+ 0x00052, 0x00327, 0x00000, 0x00052, 0x0030c, 0x00000, 0x00053, 0x00301,
+ 0x00000, 0x00053, 0x00302, 0x00000, 0x00053, 0x00327, 0x00000, 0x00053,
+ 0x0030c, 0x00000, 0x00054, 0x00327, 0x00000, 0x00054, 0x0030c, 0x00000,
+ 0x00055, 0x00303, 0x00000, 0x00055, 0x00304, 0x00000, 0x00055, 0x00306,
+ 0x00000, 0x00055, 0x0030a, 0x00000, 0x00055, 0x0030b, 0x00000, 0x00055,
+ 0x00328, 0x00000, 0x00057, 0x00302, 0x00000, 0x00059, 0x00302, 0x00000,
+ 0x00059, 0x00308, 0x00000, 0x0005a, 0x00301, 0x00000, 0x0005a, 0x00307,
+ 0x00000, 0x0005a, 0x0030c, 0x00000, 0x0004f, 0x0031b, 0x00000, 0x00055,
+ 0x0031b, 0x00000, 0x00041, 0x0030c, 0x00000, 0x00049, 0x0030c, 0x00000,
+ 0x0004f, 0x0030c, 0x00000, 0x00055, 0x0030c, 0x00000, 0x000dc, 0x00304,
+ 0x00000, 0x000dc, 0x00301, 0x00000, 0x000dc, 0x0030c, 0x00000, 0x000dc,
+ 0x00300, 0x00000, 0x000c4, 0x00304, 0x00000, 0x00226, 0x00304, 0x00000,
+ 0x000c6, 0x00304, 0x00000, 0x00047, 0x0030c, 0x00000, 0x0004b, 0x0030c,
+ 0x00000, 0x0004f, 0x00328, 0x00000, 0x001ea, 0x00304, 0x00000, 0x001b7,
+ 0x0030c, 0x00000, 0x0006a, 0x0030c, 0x00000, 0x00047, 0x00301, 0x00000,
+ 0x0004e, 0x00300, 0x00000, 0x000c5, 0x00301, 0x00000, 0x000c6, 0x00301,
+ 0x00000, 0x000d8, 0x00301, 0x00000, 0x00041, 0x0030f, 0x00000, 0x00041,
+ 0x00311, 0x00000, 0x00045, 0x0030f, 0x00000, 0x00045, 0x00311, 0x00000,
+ 0x00049, 0x0030f, 0x00000, 0x00049, 0x00311, 0x00000, 0x0004f, 0x0030f,
+ 0x00000, 0x0004f, 0x00311, 0x00000, 0x00052, 0x0030f, 0x00000, 0x00052,
+ 0x00311, 0x00000, 0x00055, 0x0030f, 0x00000, 0x00055, 0x00311, 0x00000,
+ 0x00053, 0x00326, 0x00000, 0x00054, 0x00326, 0x00000, 0x00048, 0x0030c,
+ 0x00000, 0x00041, 0x00307, 0x00000, 0x00045, 0x00327, 0x00000, 0x000d6,
+ 0x00304, 0x00000, 0x000d5, 0x00304, 0x00000, 0x0004f, 0x00307, 0x00000,
+ 0x0022e, 0x00304, 0x00000, 0x00059, 0x00304, 0x00000, 0x00020, 0x00306,
+ 0x00000, 0x00020, 0x00307, 0x00000, 0x00020, 0x0030a, 0x00000, 0x00020,
+ 0x00328, 0x00000, 0x00020, 0x00303, 0x00000, 0x00020, 0x0030b, 0x00000,
+ 0x00308, 0x00301, 0x00000, 0x00020, 0x00345, 0x00000, 0x00020, 0x00301,
+ 0x00000, 0x000a8, 0x00301, 0x00000, 0x00391, 0x00301, 0x00000, 0x00395,
+ 0x00301, 0x00000, 0x00397, 0x00301, 0x00000, 0x00399, 0x00301, 0x00000,
+ 0x0039f, 0x00301, 0x00000, 0x003a5, 0x00301, 0x00000, 0x003a9, 0x00301,
+ 0x00000, 0x003ca, 0x00301, 0x00000, 0x00399, 0x00308, 0x00000, 0x003a5,
+ 0x00308, 0x00000, 0x003cb, 0x00301, 0x00000, 0x003d2, 0x00301, 0x00000,
+ 0x003d2, 0x00308, 0x00000, 0x00415, 0x00300, 0x00000, 0x00415, 0x00308,
+ 0x00000, 0x00413, 0x00301, 0x00000, 0x00406, 0x00308, 0x00000, 0x0041a,
+ 0x00301, 0x00000, 0x00418, 0x00300, 0x00000, 0x00423, 0x00306, 0x00000,
+ 0x00418, 0x00306, 0x00000, 0x00474, 0x0030f, 0x00000, 0x00416, 0x00306,
+ 0x00000, 0x00410, 0x00306, 0x00000, 0x00410, 0x00308, 0x00000, 0x00415,
+ 0x00306, 0x00000, 0x004d8, 0x00308, 0x00000, 0x00416, 0x00308, 0x00000,
+ 0x00417, 0x00308, 0x00000, 0x00418, 0x00304, 0x00000, 0x00418, 0x00308,
+ 0x00000, 0x0041e, 0x00308, 0x00000, 0x004e8, 0x00308, 0x00000, 0x0042d,
+ 0x00308, 0x00000, 0x00423, 0x00304, 0x00000, 0x00423, 0x00308, 0x00000,
+ 0x00423, 0x0030b, 0x00000, 0x00427, 0x00308, 0x00000, 0x0042b, 0x00308,
+ 0x00000, 0x00565, 0x00582, 0x00000, 0x00627, 0x00653, 0x00000, 0x00627,
+ 0x00654, 0x00000, 0x00648, 0x00654, 0x00000, 0x00627, 0x00655, 0x00000,
+ 0x0064a, 0x00654, 0x00000, 0x00627, 0x00674, 0x00000, 0x00648, 0x00674,
+ 0x00000, 0x006c7, 0x00674, 0x00000, 0x0064a, 0x00674, 0x00000, 0x006d5,
+ 0x00654, 0x00000, 0x006c1, 0x00654, 0x00000, 0x006d2, 0x00654, 0x00000,
+ 0x00928, 0x0093c, 0x00000, 0x00930, 0x0093c, 0x00000, 0x00933, 0x0093c,
+ 0x00000, 0x00915, 0x0093c, 0x00000, 0x00916, 0x0093c, 0x00000, 0x00917,
+ 0x0093c, 0x00000, 0x0091c, 0x0093c, 0x00000, 0x00921, 0x0093c, 0x00000,
+ 0x00922, 0x0093c, 0x00000, 0x0092b, 0x0093c, 0x00000, 0x0092f, 0x0093c,
+ 0x00000, 0x009c7, 0x009be, 0x00000, 0x009c7, 0x009d7, 0x00000, 0x009a1,
+ 0x009bc, 0x00000, 0x009a2, 0x009bc, 0x00000, 0x009af, 0x009bc, 0x00000,
+ 0x00a32, 0x00a3c, 0x00000, 0x00a38, 0x00a3c, 0x00000, 0x00a16, 0x00a3c,
+ 0x00000, 0x00a17, 0x00a3c, 0x00000, 0x00a1c, 0x00a3c, 0x00000, 0x00a2b,
+ 0x00a3c, 0x00000, 0x00b47, 0x00b56, 0x00000, 0x00b47, 0x00b3e, 0x00000,
+ 0x00b47, 0x00b57, 0x00000, 0x00b21, 0x00b3c, 0x00000, 0x00b22, 0x00b3c,
+ 0x00000, 0x00b92, 0x00bd7, 0x00000, 0x00bc6, 0x00bbe, 0x00000, 0x00bc7,
+ 0x00bbe, 0x00000, 0x00bc6, 0x00bd7, 0x00000, 0x00c46, 0x00c56, 0x00000,
+ 0x00cbf, 0x00cd5, 0x00000, 0x00cc6, 0x00cd5, 0x00000, 0x00cc6, 0x00cd6,
+ 0x00000, 0x00cc6, 0x00cc2, 0x00000, 0x00cca, 0x00cd5, 0x00000, 0x00d46,
+ 0x00d3e, 0x00000, 0x00d47, 0x00d3e, 0x00000, 0x00d46, 0x00d57, 0x00000,
+ 0x00dd9, 0x00dca, 0x00000, 0x00dd9, 0x00dcf, 0x00000, 0x00ddc, 0x00dca,
+ 0x00000, 0x00dd9, 0x00ddf, 0x00000, 0x00e4d, 0x00e32, 0x00000, 0x00ecd,
+ 0x00eb2, 0x00000, 0x00eab, 0x00e99, 0x00000, 0x00eab, 0x00ea1, 0x00000,
+ 0x00f42, 0x00fb7, 0x00000, 0x00f4c, 0x00fb7, 0x00000, 0x00f51, 0x00fb7,
+ 0x00000, 0x00f56, 0x00fb7, 0x00000, 0x00f5b, 0x00fb7, 0x00000, 0x00f40,
+ 0x00fb5, 0x00000, 0x00f71, 0x00f72, 0x00000, 0x00f71, 0x00f74, 0x00000,
+ 0x00fb2, 0x00f80, 0x00000, 0x00fb2, 0x00f81, 0x00000, 0x00fb3, 0x00f80,
+ 0x00000, 0x00fb3, 0x00f81, 0x00000, 0x00f71, 0x00f80, 0x00000, 0x00f92,
+ 0x00fb7, 0x00000, 0x00f9c, 0x00fb7, 0x00000, 0x00fa1, 0x00fb7, 0x00000,
+ 0x00fa6, 0x00fb7, 0x00000, 0x00fab, 0x00fb7, 0x00000, 0x00f90, 0x00fb5,
+ 0x00000, 0x01025, 0x0102e, 0x00000, 0x01b05, 0x01b35, 0x00000, 0x01b07,
+ 0x01b35, 0x00000, 0x01b09, 0x01b35, 0x00000, 0x01b0b, 0x01b35, 0x00000,
+ 0x01b0d, 0x01b35, 0x00000, 0x01b11, 0x01b35, 0x00000, 0x01b3a, 0x01b35,
+ 0x00000, 0x01b3c, 0x01b35, 0x00000, 0x01b3e, 0x01b35, 0x00000, 0x01b3f,
+ 0x01b35, 0x00000, 0x01b42, 0x01b35, 0x00000, 0x00041, 0x00325, 0x00000,
+ 0x00042, 0x00307, 0x00000, 0x00042, 0x00323, 0x00000, 0x00042, 0x00331,
+ 0x00000, 0x000c7, 0x00301, 0x00000, 0x00044, 0x00307, 0x00000, 0x00044,
+ 0x00323, 0x00000, 0x00044, 0x00331, 0x00000, 0x00044, 0x00327, 0x00000,
+ 0x00044, 0x0032d, 0x00000, 0x00112, 0x00300, 0x00000, 0x00112, 0x00301,
+ 0x00000, 0x00045, 0x0032d, 0x00000, 0x00045, 0x00330, 0x00000, 0x00228,
+ 0x00306, 0x00000, 0x00046, 0x00307, 0x00000, 0x00047, 0x00304, 0x00000,
+ 0x00048, 0x00307, 0x00000, 0x00048, 0x00323, 0x00000, 0x00048, 0x00308,
+ 0x00000, 0x00048, 0x00327, 0x00000, 0x00048, 0x0032e, 0x00000, 0x00049,
+ 0x00330, 0x00000, 0x000cf, 0x00301, 0x00000, 0x0004b, 0x00301, 0x00000,
+ 0x0004b, 0x00323, 0x00000, 0x0004b, 0x00331, 0x00000, 0x0004c, 0x00323,
+ 0x00000, 0x01e36, 0x00304, 0x00000, 0x0004c, 0x00331, 0x00000, 0x0004c,
+ 0x0032d, 0x00000, 0x0004d, 0x00301, 0x00000, 0x0004d, 0x00307, 0x00000,
+ 0x0004d, 0x00323, 0x00000, 0x0004e, 0x00307, 0x00000, 0x0004e, 0x00323,
+ 0x00000, 0x0004e, 0x00331, 0x00000, 0x0004e, 0x0032d, 0x00000, 0x000d5,
+ 0x00301, 0x00000, 0x000d5, 0x00308, 0x00000, 0x0014c, 0x00300, 0x00000,
+ 0x0014c, 0x00301, 0x00000, 0x00050, 0x00301, 0x00000, 0x00050, 0x00307,
+ 0x00000, 0x00052, 0x00307, 0x00000, 0x00052, 0x00323, 0x00000, 0x01e5a,
+ 0x00304, 0x00000, 0x00052, 0x00331, 0x00000, 0x00053, 0x00307, 0x00000,
+ 0x00053, 0x00323, 0x00000, 0x0015a, 0x00307, 0x00000, 0x00160, 0x00307,
+ 0x00000, 0x01e62, 0x00307, 0x00000, 0x00054, 0x00307, 0x00000, 0x00054,
+ 0x00323, 0x00000, 0x00054, 0x00331, 0x00000, 0x00054, 0x0032d, 0x00000,
+ 0x00055, 0x00324, 0x00000, 0x00055, 0x00330, 0x00000, 0x00055, 0x0032d,
+ 0x00000, 0x00168, 0x00301, 0x00000, 0x0016a, 0x00308, 0x00000, 0x00056,
+ 0x00303, 0x00000, 0x00056, 0x00323, 0x00000, 0x00057, 0x00300, 0x00000,
+ 0x00057, 0x00301, 0x00000, 0x00057, 0x00308, 0x00000, 0x00057, 0x00307,
+ 0x00000, 0x00057, 0x00323, 0x00000, 0x00058, 0x00307, 0x00000, 0x00058,
+ 0x00308, 0x00000, 0x00059, 0x00307, 0x00000, 0x0005a, 0x00302, 0x00000,
+ 0x0005a, 0x00323, 0x00000, 0x0005a, 0x00331, 0x00000, 0x00068, 0x00331,
+ 0x00000, 0x00074, 0x00308, 0x00000, 0x00077, 0x0030a, 0x00000, 0x00079,
+ 0x0030a, 0x00000, 0x00061, 0x002be, 0x00000, 0x00041, 0x00323, 0x00000,
+ 0x00041, 0x00309, 0x00000, 0x000c2, 0x00301, 0x00000, 0x000c2, 0x00300,
+ 0x00000, 0x000c2, 0x00309, 0x00000, 0x000c2, 0x00303, 0x00000, 0x01ea0,
+ 0x00302, 0x00000, 0x00102, 0x00301, 0x00000, 0x00102, 0x00300, 0x00000,
+ 0x00102, 0x00309, 0x00000, 0x00102, 0x00303, 0x00000, 0x01ea0, 0x00306,
+ 0x00000, 0x00045, 0x00323, 0x00000, 0x00045, 0x00309, 0x00000, 0x00045,
+ 0x00303, 0x00000, 0x000ca, 0x00301, 0x00000, 0x000ca, 0x00300, 0x00000,
+ 0x000ca, 0x00309, 0x00000, 0x000ca, 0x00303, 0x00000, 0x01eb8, 0x00302,
+ 0x00000, 0x00049, 0x00309, 0x00000, 0x00049, 0x00323, 0x00000, 0x0004f,
+ 0x00323, 0x00000, 0x0004f, 0x00309, 0x00000, 0x000d4, 0x00301, 0x00000,
+ 0x000d4, 0x00300, 0x00000, 0x000d4, 0x00309, 0x00000, 0x000d4, 0x00303,
+ 0x00000, 0x01ecc, 0x00302, 0x00000, 0x001a0, 0x00301, 0x00000, 0x001a0,
+ 0x00300, 0x00000, 0x001a0, 0x00309, 0x00000, 0x001a0, 0x00303, 0x00000,
+ 0x001a0, 0x00323, 0x00000, 0x00055, 0x00323, 0x00000, 0x00055, 0x00309,
+ 0x00000, 0x001af, 0x00301, 0x00000, 0x001af, 0x00300, 0x00000, 0x001af,
+ 0x00309, 0x00000, 0x001af, 0x00303, 0x00000, 0x001af, 0x00323, 0x00000,
+ 0x00059, 0x00300, 0x00000, 0x00059, 0x00323, 0x00000, 0x00059, 0x00309,
+ 0x00000, 0x00059, 0x00303, 0x00000, 0x00391, 0x00313, 0x00000, 0x00391,
+ 0x00314, 0x00000, 0x01f08, 0x00300, 0x00000, 0x01f09, 0x00300, 0x00000,
+ 0x01f08, 0x00301, 0x00000, 0x01f09, 0x00301, 0x00000, 0x01f08, 0x00342,
+ 0x00000, 0x01f09, 0x00342, 0x00000, 0x00395, 0x00313, 0x00000, 0x00395,
+ 0x00314, 0x00000, 0x01f18, 0x00300, 0x00000, 0x01f19, 0x00300, 0x00000,
+ 0x01f18, 0x00301, 0x00000, 0x01f19, 0x00301, 0x00000, 0x00397, 0x00313,
+ 0x00000, 0x00397, 0x00314, 0x00000, 0x01f28, 0x00300, 0x00000, 0x01f29,
+ 0x00300, 0x00000, 0x01f28, 0x00301, 0x00000, 0x01f29, 0x00301, 0x00000,
+ 0x01f28, 0x00342, 0x00000, 0x01f29, 0x00342, 0x00000, 0x00399, 0x00313,
+ 0x00000, 0x00399, 0x00314, 0x00000, 0x01f38, 0x00300, 0x00000, 0x01f39,
+ 0x00300, 0x00000, 0x01f38, 0x00301, 0x00000, 0x01f39, 0x00301, 0x00000,
+ 0x01f38, 0x00342, 0x00000, 0x01f39, 0x00342, 0x00000, 0x0039f, 0x00313,
+ 0x00000, 0x0039f, 0x00314, 0x00000, 0x01f48, 0x00300, 0x00000, 0x01f49,
+ 0x00300, 0x00000, 0x01f48, 0x00301, 0x00000, 0x01f49, 0x00301, 0x00000,
+ 0x003c5, 0x00313, 0x00000, 0x01f50, 0x00300, 0x00000, 0x01f50, 0x00301,
+ 0x00000, 0x01f50, 0x00342, 0x00000, 0x003a5, 0x00314, 0x00000, 0x01f59,
+ 0x00300, 0x00000, 0x01f59, 0x00301, 0x00000, 0x01f59, 0x00342, 0x00000,
+ 0x003a9, 0x00313, 0x00000, 0x003a9, 0x00314, 0x00000, 0x01f68, 0x00300,
+ 0x00000, 0x01f69, 0x00300, 0x00000, 0x01f68, 0x00301, 0x00000, 0x01f69,
+ 0x00301, 0x00000, 0x01f68, 0x00342, 0x00000, 0x01f69, 0x00342, 0x00000,
+ 0x01f08, 0x00345, 0x00000, 0x01f09, 0x00345, 0x00000, 0x01f0a, 0x00345,
+ 0x00000, 0x01f0b, 0x00345, 0x00000, 0x01f0c, 0x00345, 0x00000, 0x01f0d,
+ 0x00345, 0x00000, 0x01f0e, 0x00345, 0x00000, 0x01f0f, 0x00345, 0x00000,
+ 0x01f28, 0x00345, 0x00000, 0x01f29, 0x00345, 0x00000, 0x01f2a, 0x00345,
+ 0x00000, 0x01f2b, 0x00345, 0x00000, 0x01f2c, 0x00345, 0x00000, 0x01f2d,
+ 0x00345, 0x00000, 0x01f2e, 0x00345, 0x00000, 0x01f2f, 0x00345, 0x00000,
+ 0x01f68, 0x00345, 0x00000, 0x01f69, 0x00345, 0x00000, 0x01f6a, 0x00345,
+ 0x00000, 0x01f6b, 0x00345, 0x00000, 0x01f6c, 0x00345, 0x00000, 0x01f6d,
+ 0x00345, 0x00000, 0x01f6e, 0x00345, 0x00000, 0x01f6f, 0x00345, 0x00000,
+ 0x01f70, 0x00345, 0x00000, 0x003ac, 0x00345, 0x00000, 0x003b1, 0x00342,
+ 0x00000, 0x01fb6, 0x00345, 0x00000, 0x00391, 0x00306, 0x00000, 0x00391,
+ 0x00304, 0x00000, 0x00391, 0x00300, 0x00000, 0x00391, 0x00345, 0x00000,
+ 0x00020, 0x00313, 0x00000, 0x00020, 0x00313, 0x00000, 0x00020, 0x00342,
+ 0x00000, 0x000a8, 0x00342, 0x00000, 0x01f74, 0x00345, 0x00000, 0x003ae,
+ 0x00345, 0x00000, 0x003b7, 0x00342, 0x00000, 0x01fc6, 0x00345, 0x00000,
+ 0x00395, 0x00300, 0x00000, 0x00397, 0x00300, 0x00000, 0x00397, 0x00345,
+ 0x00000, 0x01fbf, 0x00300, 0x00000, 0x01fbf, 0x00301, 0x00000, 0x01fbf,
+ 0x00342, 0x00000, 0x003ca, 0x00300, 0x00000, 0x003b9, 0x00342, 0x00000,
+ 0x003ca, 0x00342, 0x00000, 0x00399, 0x00306, 0x00000, 0x00399, 0x00304,
+ 0x00000, 0x00399, 0x00300, 0x00000, 0x01ffe, 0x00300, 0x00000, 0x01ffe,
+ 0x00301, 0x00000, 0x01ffe, 0x00342, 0x00000, 0x003cb, 0x00300, 0x00000,
+ 0x003c1, 0x00313, 0x00000, 0x003c5, 0x00342, 0x00000, 0x003cb, 0x00342,
+ 0x00000, 0x003a5, 0x00306, 0x00000, 0x003a5, 0x00304, 0x00000, 0x003a5,
+ 0x00300, 0x00000, 0x003a1, 0x00314, 0x00000, 0x000a8, 0x00300, 0x00000,
+ 0x01f7c, 0x00345, 0x00000, 0x003ce, 0x00345, 0x00000, 0x003c9, 0x00342,
+ 0x00000, 0x01ff6, 0x00345, 0x00000, 0x0039f, 0x00300, 0x00000, 0x003a9,
+ 0x00300, 0x00000, 0x003a9, 0x00345, 0x00000, 0x00020, 0x00314, 0x00000,
+ 0x00020, 0x00333, 0x00000, 0x0002e, 0x0002e, 0x00000, 0x0002e, 0x0002e,
+ 0x0002e, 0x00000, 0x02032, 0x02032, 0x00000, 0x02032, 0x02032, 0x02032,
+ 0x00000, 0x02035, 0x02035, 0x00000, 0x02035, 0x02035, 0x02035, 0x00000,
+ 0x00021, 0x00021, 0x00000, 0x00020, 0x00305, 0x00000, 0x0003f, 0x0003f,
+ 0x00000, 0x0003f, 0x00021, 0x00000, 0x00021, 0x0003f, 0x00000, 0x02032,
+ 0x02032, 0x02032, 0x02032, 0x00000, 0x00052, 0x00073, 0x00000, 0x00061,
+ 0x0002f, 0x00063, 0x00000, 0x00061, 0x0002f, 0x00073, 0x00000, 0x000b0,
+ 0x00043, 0x00000, 0x00063, 0x0002f, 0x0006f, 0x00000, 0x00063, 0x0002f,
+ 0x00075, 0x00000, 0x000b0, 0x00046, 0x00000, 0x0004e, 0x0006f, 0x00000,
+ 0x00053, 0x0004d, 0x00000, 0x00054, 0x00045, 0x0004c, 0x00000, 0x00054,
+ 0x0004d, 0x00000, 0x00046, 0x00041, 0x00058, 0x00000, 0x00031, 0x02044,
+ 0x00037, 0x00000, 0x00031, 0x02044, 0x00039, 0x00000, 0x00031, 0x02044,
+ 0x00031, 0x00030, 0x00000, 0x00031, 0x02044, 0x00033, 0x00000, 0x00032,
+ 0x02044, 0x00033, 0x00000, 0x00031, 0x02044, 0x00035, 0x00000, 0x00032,
+ 0x02044, 0x00035, 0x00000, 0x00033, 0x02044, 0x00035, 0x00000, 0x00034,
+ 0x02044, 0x00035, 0x00000, 0x00031, 0x02044, 0x00036, 0x00000, 0x00035,
+ 0x02044, 0x00036, 0x00000, 0x00031, 0x02044, 0x00038, 0x00000, 0x00033,
+ 0x02044, 0x00038, 0x00000, 0x00035, 0x02044, 0x00038, 0x00000, 0x00037,
+ 0x02044, 0x00038, 0x00000, 0x00031, 0x02044, 0x00000, 0x00049, 0x00049,
+ 0x00000, 0x00049, 0x00049, 0x00049, 0x00000, 0x00049, 0x00056, 0x00000,
+ 0x00056, 0x00049, 0x00000, 0x00056, 0x00049, 0x00049, 0x00000, 0x00056,
+ 0x00049, 0x00049, 0x00049, 0x00000, 0x00049, 0x00058, 0x00000, 0x00058,
+ 0x00049, 0x00000, 0x00058, 0x00049, 0x00049, 0x00000, 0x00030, 0x02044,
+ 0x00033, 0x00000, 0x02190, 0x00338, 0x00000, 0x02192, 0x00338, 0x00000,
+ 0x02194, 0x00338, 0x00000, 0x021d0, 0x00338, 0x00000, 0x021d4, 0x00338,
+ 0x00000, 0x021d2, 0x00338, 0x00000, 0x02203, 0x00338, 0x00000, 0x02208,
+ 0x00338, 0x00000, 0x0220b, 0x00338, 0x00000, 0x02223, 0x00338, 0x00000,
+ 0x02225, 0x00338, 0x00000, 0x0222b, 0x0222b, 0x00000, 0x0222b, 0x0222b,
+ 0x0222b, 0x00000, 0x0222e, 0x0222e, 0x00000, 0x0222e, 0x0222e, 0x0222e,
+ 0x00000, 0x0223c, 0x00338, 0x00000, 0x02243, 0x00338, 0x00000, 0x02245,
+ 0x00338, 0x00000, 0x02248, 0x00338, 0x00000, 0x0003d, 0x00338, 0x00000,
+ 0x02261, 0x00338, 0x00000, 0x0224d, 0x00338, 0x00000, 0x0003c, 0x00338,
+ 0x00000, 0x0003e, 0x00338, 0x00000, 0x02264, 0x00338, 0x00000, 0x02265,
+ 0x00338, 0x00000, 0x02272, 0x00338, 0x00000, 0x02273, 0x00338, 0x00000,
+ 0x02276, 0x00338, 0x00000, 0x02277, 0x00338, 0x00000, 0x0227a, 0x00338,
+ 0x00000, 0x0227b, 0x00338, 0x00000, 0x02282, 0x00338, 0x00000, 0x02283,
+ 0x00338, 0x00000, 0x02286, 0x00338, 0x00000, 0x02287, 0x00338, 0x00000,
+ 0x022a2, 0x00338, 0x00000, 0x022a8, 0x00338, 0x00000, 0x022a9, 0x00338,
+ 0x00000, 0x022ab, 0x00338, 0x00000, 0x0227c, 0x00338, 0x00000, 0x0227d,
+ 0x00338, 0x00000, 0x02291, 0x00338, 0x00000, 0x02292, 0x00338, 0x00000,
+ 0x022b2, 0x00338, 0x00000, 0x022b3, 0x00338, 0x00000, 0x022b4, 0x00338,
+ 0x00000, 0x022b5, 0x00338, 0x00000, 0x00031, 0x00030, 0x00000, 0x00031,
+ 0x00031, 0x00000, 0x00031, 0x00032, 0x00000, 0x00031, 0x00033, 0x00000,
+ 0x00031, 0x00034, 0x00000, 0x00031, 0x00035, 0x00000, 0x00031, 0x00036,
+ 0x00000, 0x00031, 0x00037, 0x00000, 0x00031, 0x00038, 0x00000, 0x00031,
+ 0x00039, 0x00000, 0x00032, 0x00030, 0x00000, 0x00028, 0x00031, 0x00029,
+ 0x00000, 0x00028, 0x00032, 0x00029, 0x00000, 0x00028, 0x00033, 0x00029,
+ 0x00000, 0x00028, 0x00034, 0x00029, 0x00000, 0x00028, 0x00035, 0x00029,
+ 0x00000, 0x00028, 0x00036, 0x00029, 0x00000, 0x00028, 0x00037, 0x00029,
+ 0x00000, 0x00028, 0x00038, 0x00029, 0x00000, 0x00028, 0x00039, 0x00029,
+ 0x00000, 0x00028, 0x00031, 0x00030, 0x00029, 0x00000, 0x00028, 0x00031,
+ 0x00031, 0x00029, 0x00000, 0x00028, 0x00031, 0x00032, 0x00029, 0x00000,
+ 0x00028, 0x00031, 0x00033, 0x00029, 0x00000, 0x00028, 0x00031, 0x00034,
+ 0x00029, 0x00000, 0x00028, 0x00031, 0x00035, 0x00029, 0x00000, 0x00028,
+ 0x00031, 0x00036, 0x00029, 0x00000, 0x00028, 0x00031, 0x00037, 0x00029,
+ 0x00000, 0x00028, 0x00031, 0x00038, 0x00029, 0x00000, 0x00028, 0x00031,
+ 0x00039, 0x00029, 0x00000, 0x00028, 0x00032, 0x00030, 0x00029, 0x00000,
+ 0x00031, 0x0002e, 0x00000, 0x00032, 0x0002e, 0x00000, 0x00033, 0x0002e,
+ 0x00000, 0x00034, 0x0002e, 0x00000, 0x00035, 0x0002e, 0x00000, 0x00036,
+ 0x0002e, 0x00000, 0x00037, 0x0002e, 0x00000, 0x00038, 0x0002e, 0x00000,
+ 0x00039, 0x0002e, 0x00000, 0x00031, 0x00030, 0x0002e, 0x00000, 0x00031,
+ 0x00031, 0x0002e, 0x00000, 0x00031, 0x00032, 0x0002e, 0x00000, 0x00031,
+ 0x00033, 0x0002e, 0x00000, 0x00031, 0x00034, 0x0002e, 0x00000, 0x00031,
+ 0x00035, 0x0002e, 0x00000, 0x00031, 0x00036, 0x0002e, 0x00000, 0x00031,
+ 0x00037, 0x0002e, 0x00000, 0x00031, 0x00038, 0x0002e, 0x00000, 0x00031,
+ 0x00039, 0x0002e, 0x00000, 0x00032, 0x00030, 0x0002e, 0x00000, 0x00028,
+ 0x00061, 0x00029, 0x00000, 0x00028, 0x00062, 0x00029, 0x00000, 0x00028,
+ 0x00063, 0x00029, 0x00000, 0x00028, 0x00064, 0x00029, 0x00000, 0x00028,
+ 0x00065, 0x00029, 0x00000, 0x00028, 0x00066, 0x00029, 0x00000, 0x00028,
+ 0x00067, 0x00029, 0x00000, 0x00028, 0x00068, 0x00029, 0x00000, 0x00028,
+ 0x00069, 0x00029, 0x00000, 0x00028, 0x0006a, 0x00029, 0x00000, 0x00028,
+ 0x0006b, 0x00029, 0x00000, 0x00028, 0x0006c, 0x00029, 0x00000, 0x00028,
+ 0x0006d, 0x00029, 0x00000, 0x00028, 0x0006e, 0x00029, 0x00000, 0x00028,
+ 0x0006f, 0x00029, 0x00000, 0x00028, 0x00070, 0x00029, 0x00000, 0x00028,
+ 0x00071, 0x00029, 0x00000, 0x00028, 0x00072, 0x00029, 0x00000, 0x00028,
+ 0x00073, 0x00029, 0x00000, 0x00028, 0x00074, 0x00029, 0x00000, 0x00028,
+ 0x00075, 0x00029, 0x00000, 0x00028, 0x00076, 0x00029, 0x00000, 0x00028,
+ 0x00077, 0x00029, 0x00000, 0x00028, 0x00078, 0x00029, 0x00000, 0x00028,
+ 0x00079, 0x00029, 0x00000, 0x00028, 0x0007a, 0x00029, 0x00000, 0x0222b,
+ 0x0222b, 0x0222b, 0x0222b, 0x00000, 0x0003a, 0x0003a, 0x0003d, 0x00000,
+ 0x0003d, 0x0003d, 0x00000, 0x0003d, 0x0003d, 0x0003d, 0x00000, 0x02add,
+ 0x00338, 0x00000, 0x0304b, 0x03099, 0x00000, 0x0304d, 0x03099, 0x00000,
+ 0x0304f, 0x03099, 0x00000, 0x03051, 0x03099, 0x00000, 0x03053, 0x03099,
+ 0x00000, 0x03055, 0x03099, 0x00000, 0x03057, 0x03099, 0x00000, 0x03059,
+ 0x03099, 0x00000, 0x0305b, 0x03099, 0x00000, 0x0305d, 0x03099, 0x00000,
+ 0x0305f, 0x03099, 0x00000, 0x03061, 0x03099, 0x00000, 0x03064, 0x03099,
+ 0x00000, 0x03066, 0x03099, 0x00000, 0x03068, 0x03099, 0x00000, 0x0306f,
+ 0x03099, 0x00000, 0x0306f, 0x0309a, 0x00000, 0x03072, 0x03099, 0x00000,
+ 0x03072, 0x0309a, 0x00000, 0x03075, 0x03099, 0x00000, 0x03075, 0x0309a,
+ 0x00000, 0x03078, 0x03099, 0x00000, 0x03078, 0x0309a, 0x00000, 0x0307b,
+ 0x03099, 0x00000, 0x0307b, 0x0309a, 0x00000, 0x03046, 0x03099, 0x00000,
+ 0x00020, 0x03099, 0x00000, 0x00020, 0x0309a, 0x00000, 0x0309d, 0x03099,
+ 0x00000, 0x03088, 0x0308a, 0x00000, 0x030ab, 0x03099, 0x00000, 0x030ad,
+ 0x03099, 0x00000, 0x030af, 0x03099, 0x00000, 0x030b1, 0x03099, 0x00000,
+ 0x030b3, 0x03099, 0x00000, 0x030b5, 0x03099, 0x00000, 0x030b7, 0x03099,
+ 0x00000, 0x030b9, 0x03099, 0x00000, 0x030bb, 0x03099, 0x00000, 0x030bd,
+ 0x03099, 0x00000, 0x030bf, 0x03099, 0x00000, 0x030c1, 0x03099, 0x00000,
+ 0x030c4, 0x03099, 0x00000, 0x030c6, 0x03099, 0x00000, 0x030c8, 0x03099,
+ 0x00000, 0x030cf, 0x03099, 0x00000, 0x030cf, 0x0309a, 0x00000, 0x030d2,
+ 0x03099, 0x00000, 0x030d2, 0x0309a, 0x00000, 0x030d5, 0x03099, 0x00000,
+ 0x030d5, 0x0309a, 0x00000, 0x030d8, 0x03099, 0x00000, 0x030d8, 0x0309a,
+ 0x00000, 0x030db, 0x03099, 0x00000, 0x030db, 0x0309a, 0x00000, 0x030a6,
+ 0x03099, 0x00000, 0x030ef, 0x03099, 0x00000, 0x030f0, 0x03099, 0x00000,
+ 0x030f1, 0x03099, 0x00000, 0x030f2, 0x03099, 0x00000, 0x030fd, 0x03099,
+ 0x00000, 0x030b3, 0x030c8, 0x00000, 0x00028, 0x01100, 0x00029, 0x00000,
+ 0x00028, 0x01102, 0x00029, 0x00000, 0x00028, 0x01103, 0x00029, 0x00000,
+ 0x00028, 0x01105, 0x00029, 0x00000, 0x00028, 0x01106, 0x00029, 0x00000,
+ 0x00028, 0x01107, 0x00029, 0x00000, 0x00028, 0x01109, 0x00029, 0x00000,
+ 0x00028, 0x0110b, 0x00029, 0x00000, 0x00028, 0x0110c, 0x00029, 0x00000,
+ 0x00028, 0x0110e, 0x00029, 0x00000, 0x00028, 0x0110f, 0x00029, 0x00000,
+ 0x00028, 0x01110, 0x00029, 0x00000, 0x00028, 0x01111, 0x00029, 0x00000,
+ 0x00028, 0x01112, 0x00029, 0x00000, 0x00028, 0x01100, 0x01161, 0x00029,
+ 0x00000, 0x00028, 0x01102, 0x01161, 0x00029, 0x00000, 0x00028, 0x01103,
+ 0x01161, 0x00029, 0x00000, 0x00028, 0x01105, 0x01161, 0x00029, 0x00000,
+ 0x00028, 0x01106, 0x01161, 0x00029, 0x00000, 0x00028, 0x01107, 0x01161,
+ 0x00029, 0x00000, 0x00028, 0x01109, 0x01161, 0x00029, 0x00000, 0x00028,
+ 0x0110b, 0x01161, 0x00029, 0x00000, 0x00028, 0x0110c, 0x01161, 0x00029,
+ 0x00000, 0x00028, 0x0110e, 0x01161, 0x00029, 0x00000, 0x00028, 0x0110f,
+ 0x01161, 0x00029, 0x00000, 0x00028, 0x01110, 0x01161, 0x00029, 0x00000,
+ 0x00028, 0x01111, 0x01161, 0x00029, 0x00000, 0x00028, 0x01112, 0x01161,
+ 0x00029, 0x00000, 0x00028, 0x0110c, 0x0116e, 0x00029, 0x00000, 0x00028,
+ 0x0110b, 0x01169, 0x0110c, 0x01165, 0x011ab, 0x00029, 0x00000, 0x00028,
+ 0x0110b, 0x01169, 0x01112, 0x0116e, 0x00029, 0x00000, 0x00028, 0x04e00,
+ 0x00029, 0x00000, 0x00028, 0x04e8c, 0x00029, 0x00000, 0x00028, 0x04e09,
+ 0x00029, 0x00000, 0x00028, 0x056db, 0x00029, 0x00000, 0x00028, 0x04e94,
+ 0x00029, 0x00000, 0x00028, 0x0516d, 0x00029, 0x00000, 0x00028, 0x04e03,
+ 0x00029, 0x00000, 0x00028, 0x0516b, 0x00029, 0x00000, 0x00028, 0x04e5d,
+ 0x00029, 0x00000, 0x00028, 0x05341, 0x00029, 0x00000, 0x00028, 0x06708,
+ 0x00029, 0x00000, 0x00028, 0x0706b, 0x00029, 0x00000, 0x00028, 0x06c34,
+ 0x00029, 0x00000, 0x00028, 0x06728, 0x00029, 0x00000, 0x00028, 0x091d1,
+ 0x00029, 0x00000, 0x00028, 0x0571f, 0x00029, 0x00000, 0x00028, 0x065e5,
+ 0x00029, 0x00000, 0x00028, 0x0682a, 0x00029, 0x00000, 0x00028, 0x06709,
+ 0x00029, 0x00000, 0x00028, 0x0793e, 0x00029, 0x00000, 0x00028, 0x0540d,
+ 0x00029, 0x00000, 0x00028, 0x07279, 0x00029, 0x00000, 0x00028, 0x08ca1,
+ 0x00029, 0x00000, 0x00028, 0x0795d, 0x00029, 0x00000, 0x00028, 0x052b4,
+ 0x00029, 0x00000, 0x00028, 0x04ee3, 0x00029, 0x00000, 0x00028, 0x0547c,
+ 0x00029, 0x00000, 0x00028, 0x05b66, 0x00029, 0x00000, 0x00028, 0x076e3,
+ 0x00029, 0x00000, 0x00028, 0x04f01, 0x00029, 0x00000, 0x00028, 0x08cc7,
+ 0x00029, 0x00000, 0x00028, 0x05354, 0x00029, 0x00000, 0x00028, 0x0796d,
+ 0x00029, 0x00000, 0x00028, 0x04f11, 0x00029, 0x00000, 0x00028, 0x081ea,
+ 0x00029, 0x00000, 0x00028, 0x081f3, 0x00029, 0x00000, 0x00050, 0x00054,
+ 0x00045, 0x00000, 0x00032, 0x00031, 0x00000, 0x00032, 0x00032, 0x00000,
+ 0x00032, 0x00033, 0x00000, 0x00032, 0x00034, 0x00000, 0x00032, 0x00035,
+ 0x00000, 0x00032, 0x00036, 0x00000, 0x00032, 0x00037, 0x00000, 0x00032,
+ 0x00038, 0x00000, 0x00032, 0x00039, 0x00000, 0x00033, 0x00030, 0x00000,
+ 0x00033, 0x00031, 0x00000, 0x00033, 0x00032, 0x00000, 0x00033, 0x00033,
+ 0x00000, 0x00033, 0x00034, 0x00000, 0x00033, 0x00035, 0x00000, 0x01100,
+ 0x01161, 0x00000, 0x01102, 0x01161, 0x00000, 0x01103, 0x01161, 0x00000,
+ 0x01105, 0x01161, 0x00000, 0x01106, 0x01161, 0x00000, 0x01107, 0x01161,
+ 0x00000, 0x01109, 0x01161, 0x00000, 0x0110b, 0x01161, 0x00000, 0x0110c,
+ 0x01161, 0x00000, 0x0110e, 0x01161, 0x00000, 0x0110f, 0x01161, 0x00000,
+ 0x01110, 0x01161, 0x00000, 0x01111, 0x01161, 0x00000, 0x01112, 0x01161,
+ 0x00000, 0x0110e, 0x01161, 0x011b7, 0x01100, 0x01169, 0x00000, 0x0110c,
+ 0x0116e, 0x0110b, 0x01174, 0x00000, 0x0110b, 0x0116e, 0x00000, 0x00033,
+ 0x00036, 0x00000, 0x00033, 0x00037, 0x00000, 0x00033, 0x00038, 0x00000,
+ 0x00033, 0x00039, 0x00000, 0x00034, 0x00030, 0x00000, 0x00034, 0x00031,
+ 0x00000, 0x00034, 0x00032, 0x00000, 0x00034, 0x00033, 0x00000, 0x00034,
+ 0x00034, 0x00000, 0x00034, 0x00035, 0x00000, 0x00034, 0x00036, 0x00000,
+ 0x00034, 0x00037, 0x00000, 0x00034, 0x00038, 0x00000, 0x00034, 0x00039,
+ 0x00000, 0x00035, 0x00030, 0x00000, 0x00031, 0x06708, 0x00000, 0x00032,
+ 0x06708, 0x00000, 0x00033, 0x06708, 0x00000, 0x00034, 0x06708, 0x00000,
+ 0x00035, 0x06708, 0x00000, 0x00036, 0x06708, 0x00000, 0x00037, 0x06708,
+ 0x00000, 0x00038, 0x06708, 0x00000, 0x00039, 0x06708, 0x00000, 0x00031,
+ 0x00030, 0x06708, 0x00000, 0x00031, 0x00031, 0x06708, 0x00000, 0x00031,
+ 0x00032, 0x06708, 0x00000, 0x00048, 0x00067, 0x00000, 0x00065, 0x00072,
+ 0x00067, 0x00000, 0x00065, 0x00056, 0x00000, 0x0004c, 0x00054, 0x00044,
+ 0x00000, 0x030a2, 0x030d1, 0x030fc, 0x030c8, 0x00000, 0x030a2, 0x030eb,
+ 0x030d5, 0x030a1, 0x00000, 0x030a2, 0x030f3, 0x030da, 0x030a2, 0x00000,
+ 0x030a2, 0x030fc, 0x030eb, 0x00000, 0x030a4, 0x030cb, 0x030f3, 0x030b0,
+ 0x00000, 0x030a4, 0x030f3, 0x030c1, 0x00000, 0x030a6, 0x030a9, 0x030f3,
+ 0x00000, 0x030a8, 0x030b9, 0x030af, 0x030fc, 0x030c9, 0x00000, 0x030a8,
+ 0x030fc, 0x030ab, 0x030fc, 0x00000, 0x030aa, 0x030f3, 0x030b9, 0x00000,
+ 0x030aa, 0x030fc, 0x030e0, 0x00000, 0x030ab, 0x030a4, 0x030ea, 0x00000,
+ 0x030ab, 0x030e9, 0x030c3, 0x030c8, 0x00000, 0x030ab, 0x030ed, 0x030ea,
+ 0x030fc, 0x00000, 0x030ac, 0x030ed, 0x030f3, 0x00000, 0x030ac, 0x030f3,
+ 0x030de, 0x00000, 0x030ae, 0x030ac, 0x00000, 0x030ae, 0x030cb, 0x030fc,
+ 0x00000, 0x030ad, 0x030e5, 0x030ea, 0x030fc, 0x00000, 0x030ae, 0x030eb,
+ 0x030c0, 0x030fc, 0x00000, 0x030ad, 0x030ed, 0x00000, 0x030ad, 0x030ed,
+ 0x030b0, 0x030e9, 0x030e0, 0x00000, 0x030ad, 0x030ed, 0x030e1, 0x030fc,
+ 0x030c8, 0x030eb, 0x00000, 0x030ad, 0x030ed, 0x030ef, 0x030c3, 0x030c8,
+ 0x00000, 0x030b0, 0x030e9, 0x030e0, 0x00000, 0x030b0, 0x030e9, 0x030e0,
+ 0x030c8, 0x030f3, 0x00000, 0x030af, 0x030eb, 0x030bc, 0x030a4, 0x030ed,
+ 0x00000, 0x030af, 0x030ed, 0x030fc, 0x030cd, 0x00000, 0x030b1, 0x030fc,
+ 0x030b9, 0x00000, 0x030b3, 0x030eb, 0x030ca, 0x00000, 0x030b3, 0x030fc,
+ 0x030dd, 0x00000, 0x030b5, 0x030a4, 0x030af, 0x030eb, 0x00000, 0x030b5,
+ 0x030f3, 0x030c1, 0x030fc, 0x030e0, 0x00000, 0x030b7, 0x030ea, 0x030f3,
+ 0x030b0, 0x00000, 0x030bb, 0x030f3, 0x030c1, 0x00000, 0x030bb, 0x030f3,
+ 0x030c8, 0x00000, 0x030c0, 0x030fc, 0x030b9, 0x00000, 0x030c7, 0x030b7,
+ 0x00000, 0x030c9, 0x030eb, 0x00000, 0x030c8, 0x030f3, 0x00000, 0x030ca,
+ 0x030ce, 0x00000, 0x030ce, 0x030c3, 0x030c8, 0x00000, 0x030cf, 0x030a4,
+ 0x030c4, 0x00000, 0x030d1, 0x030fc, 0x030bb, 0x030f3, 0x030c8, 0x00000,
+ 0x030d1, 0x030fc, 0x030c4, 0x00000, 0x030d0, 0x030fc, 0x030ec, 0x030eb,
+ 0x00000, 0x030d4, 0x030a2, 0x030b9, 0x030c8, 0x030eb, 0x00000, 0x030d4,
+ 0x030af, 0x030eb, 0x00000, 0x030d4, 0x030b3, 0x00000, 0x030d3, 0x030eb,
+ 0x00000, 0x030d5, 0x030a1, 0x030e9, 0x030c3, 0x030c9, 0x00000, 0x030d5,
+ 0x030a3, 0x030fc, 0x030c8, 0x00000, 0x030d6, 0x030c3, 0x030b7, 0x030a7,
+ 0x030eb, 0x00000, 0x030d5, 0x030e9, 0x030f3, 0x00000, 0x030d8, 0x030af,
+ 0x030bf, 0x030fc, 0x030eb, 0x00000, 0x030da, 0x030bd, 0x00000, 0x030da,
+ 0x030cb, 0x030d2, 0x00000, 0x030d8, 0x030eb, 0x030c4, 0x00000, 0x030da,
+ 0x030f3, 0x030b9, 0x00000, 0x030da, 0x030fc, 0x030b8, 0x00000, 0x030d9,
+ 0x030fc, 0x030bf, 0x00000, 0x030dd, 0x030a4, 0x030f3, 0x030c8, 0x00000,
+ 0x030dc, 0x030eb, 0x030c8, 0x00000, 0x030db, 0x030f3, 0x00000, 0x030dd,
+ 0x030f3, 0x030c9, 0x00000, 0x030db, 0x030fc, 0x030eb, 0x00000, 0x030db,
+ 0x030fc, 0x030f3, 0x00000, 0x030de, 0x030a4, 0x030af, 0x030ed, 0x00000,
+ 0x030de, 0x030a4, 0x030eb, 0x00000, 0x030de, 0x030c3, 0x030cf, 0x00000,
+ 0x030de, 0x030eb, 0x030af, 0x00000, 0x030de, 0x030f3, 0x030b7, 0x030e7,
+ 0x030f3, 0x00000, 0x030df, 0x030af, 0x030ed, 0x030f3, 0x00000, 0x030df,
+ 0x030ea, 0x00000, 0x030df, 0x030ea, 0x030d0, 0x030fc, 0x030eb, 0x00000,
+ 0x030e1, 0x030ac, 0x00000, 0x030e1, 0x030ac, 0x030c8, 0x030f3, 0x00000,
+ 0x030e1, 0x030fc, 0x030c8, 0x030eb, 0x00000, 0x030e4, 0x030fc, 0x030c9,
+ 0x00000, 0x030e4, 0x030fc, 0x030eb, 0x00000, 0x030e6, 0x030a2, 0x030f3,
+ 0x00000, 0x030ea, 0x030c3, 0x030c8, 0x030eb, 0x00000, 0x030ea, 0x030e9,
+ 0x00000, 0x030eb, 0x030d4, 0x030fc, 0x00000, 0x030eb, 0x030fc, 0x030d6,
+ 0x030eb, 0x00000, 0x030ec, 0x030e0, 0x00000, 0x030ec, 0x030f3, 0x030c8,
+ 0x030b2, 0x030f3, 0x00000, 0x030ef, 0x030c3, 0x030c8, 0x00000, 0x00030,
+ 0x070b9, 0x00000, 0x00031, 0x070b9, 0x00000, 0x00032, 0x070b9, 0x00000,
+ 0x00033, 0x070b9, 0x00000, 0x00034, 0x070b9, 0x00000, 0x00035, 0x070b9,
+ 0x00000, 0x00036, 0x070b9, 0x00000, 0x00037, 0x070b9, 0x00000, 0x00038,
+ 0x070b9, 0x00000, 0x00039, 0x070b9, 0x00000, 0x00031, 0x00030, 0x070b9,
+ 0x00000, 0x00031, 0x00031, 0x070b9, 0x00000, 0x00031, 0x00032, 0x070b9,
+ 0x00000, 0x00031, 0x00033, 0x070b9, 0x00000, 0x00031, 0x00034, 0x070b9,
+ 0x00000, 0x00031, 0x00035, 0x070b9, 0x00000, 0x00031, 0x00036, 0x070b9,
+ 0x00000, 0x00031, 0x00037, 0x070b9, 0x00000, 0x00031, 0x00038, 0x070b9,
+ 0x00000, 0x00031, 0x00039, 0x070b9, 0x00000, 0x00032, 0x00030, 0x070b9,
+ 0x00000, 0x00032, 0x00031, 0x070b9, 0x00000, 0x00032, 0x00032, 0x070b9,
+ 0x00000, 0x00032, 0x00033, 0x070b9, 0x00000, 0x00032, 0x00034, 0x070b9,
+ 0x00000, 0x00068, 0x00050, 0x00061, 0x00000, 0x00064, 0x00061, 0x00000,
+ 0x00041, 0x00055, 0x00000, 0x00062, 0x00061, 0x00072, 0x00000, 0x0006f,
+ 0x00056, 0x00000, 0x00070, 0x00063, 0x00000, 0x00064, 0x0006d, 0x00000,
+ 0x00064, 0x0006d, 0x000b2, 0x00000, 0x00064, 0x0006d, 0x000b3, 0x00000,
+ 0x00049, 0x00055, 0x00000, 0x05e73, 0x06210, 0x00000, 0x0662d, 0x0548c,
+ 0x00000, 0x05927, 0x06b63, 0x00000, 0x0660e, 0x06cbb, 0x00000, 0x0682a,
+ 0x05f0f, 0x04f1a, 0x0793e, 0x00000, 0x00070, 0x00041, 0x00000, 0x0006e,
+ 0x00041, 0x00000, 0x003bc, 0x00041, 0x00000, 0x0006d, 0x00041, 0x00000,
+ 0x0006b, 0x00041, 0x00000, 0x0004b, 0x00042, 0x00000, 0x0004d, 0x00042,
+ 0x00000, 0x00047, 0x00042, 0x00000, 0x00063, 0x00061, 0x0006c, 0x00000,
+ 0x0006b, 0x00063, 0x00061, 0x0006c, 0x00000, 0x00070, 0x00046, 0x00000,
+ 0x0006e, 0x00046, 0x00000, 0x003bc, 0x00046, 0x00000, 0x003bc, 0x00067,
+ 0x00000, 0x0006d, 0x00067, 0x00000, 0x0006b, 0x00067, 0x00000, 0x00048,
+ 0x0007a, 0x00000, 0x0006b, 0x00048, 0x0007a, 0x00000, 0x0004d, 0x00048,
+ 0x0007a, 0x00000, 0x00047, 0x00048, 0x0007a, 0x00000, 0x00054, 0x00048,
+ 0x0007a, 0x00000, 0x003bc, 0x02113, 0x00000, 0x0006d, 0x02113, 0x00000,
+ 0x00064, 0x02113, 0x00000, 0x0006b, 0x02113, 0x00000, 0x00066, 0x0006d,
+ 0x00000, 0x0006e, 0x0006d, 0x00000, 0x003bc, 0x0006d, 0x00000, 0x0006d,
+ 0x0006d, 0x00000, 0x00063, 0x0006d, 0x00000, 0x0006b, 0x0006d, 0x00000,
+ 0x0006d, 0x0006d, 0x000b2, 0x00000, 0x00063, 0x0006d, 0x000b2, 0x00000,
+ 0x0006d, 0x000b2, 0x00000, 0x0006b, 0x0006d, 0x000b2, 0x00000, 0x0006d,
+ 0x0006d, 0x000b3, 0x00000, 0x00063, 0x0006d, 0x000b3, 0x00000, 0x0006d,
+ 0x000b3, 0x00000, 0x0006b, 0x0006d, 0x000b3, 0x00000, 0x0006d, 0x02215,
+ 0x00073, 0x00000, 0x0006d, 0x02215, 0x00073, 0x000b2, 0x00000, 0x00050,
+ 0x00061, 0x00000, 0x0006b, 0x00050, 0x00061, 0x00000, 0x0004d, 0x00050,
+ 0x00061, 0x00000, 0x00047, 0x00050, 0x00061, 0x00000, 0x00072, 0x00061,
+ 0x00064, 0x00000, 0x00072, 0x00061, 0x00064, 0x02215, 0x00073, 0x00000,
+ 0x00072, 0x00061, 0x00064, 0x02215, 0x00073, 0x000b2, 0x00000, 0x00070,
+ 0x00073, 0x00000, 0x0006e, 0x00073, 0x00000, 0x003bc, 0x00073, 0x00000,
+ 0x0006d, 0x00073, 0x00000, 0x00070, 0x00056, 0x00000, 0x0006e, 0x00056,
+ 0x00000, 0x003bc, 0x00056, 0x00000, 0x0006d, 0x00056, 0x00000, 0x0006b,
+ 0x00056, 0x00000, 0x0004d, 0x00056, 0x00000, 0x00070, 0x00057, 0x00000,
+ 0x0006e, 0x00057, 0x00000, 0x003bc, 0x00057, 0x00000, 0x0006d, 0x00057,
+ 0x00000, 0x0006b, 0x00057, 0x00000, 0x0004d, 0x00057, 0x00000, 0x0006b,
+ 0x003a9, 0x00000, 0x0004d, 0x003a9, 0x00000, 0x00061, 0x0002e, 0x0006d,
+ 0x0002e, 0x00000, 0x00042, 0x00071, 0x00000, 0x00063, 0x00063, 0x00000,
+ 0x00063, 0x00064, 0x00000, 0x00043, 0x02215, 0x0006b, 0x00067, 0x00000,
+ 0x00043, 0x0006f, 0x0002e, 0x00000, 0x00064, 0x00042, 0x00000, 0x00047,
+ 0x00079, 0x00000, 0x00068, 0x00061, 0x00000, 0x00048, 0x00050, 0x00000,
+ 0x00069, 0x0006e, 0x00000, 0x0004b, 0x0004b, 0x00000, 0x0004b, 0x0004d,
+ 0x00000, 0x0006b, 0x00074, 0x00000, 0x0006c, 0x0006d, 0x00000, 0x0006c,
+ 0x0006e, 0x00000, 0x0006c, 0x0006f, 0x00067, 0x00000, 0x0006c, 0x00078,
+ 0x00000, 0x0006d, 0x00062, 0x00000, 0x0006d, 0x00069, 0x0006c, 0x00000,
+ 0x0006d, 0x0006f, 0x0006c, 0x00000, 0x00050, 0x00048, 0x00000, 0x00070,
+ 0x0002e, 0x0006d, 0x0002e, 0x00000, 0x00050, 0x00050, 0x0004d, 0x00000,
+ 0x00050, 0x00052, 0x00000, 0x00073, 0x00072, 0x00000, 0x00053, 0x00076,
+ 0x00000, 0x00057, 0x00062, 0x00000, 0x00056, 0x02215, 0x0006d, 0x00000,
+ 0x00041, 0x02215, 0x0006d, 0x00000, 0x00031, 0x065e5, 0x00000, 0x00032,
+ 0x065e5, 0x00000, 0x00033, 0x065e5, 0x00000, 0x00034, 0x065e5, 0x00000,
+ 0x00035, 0x065e5, 0x00000, 0x00036, 0x065e5, 0x00000, 0x00037, 0x065e5,
+ 0x00000, 0x00038, 0x065e5, 0x00000, 0x00039, 0x065e5, 0x00000, 0x00031,
+ 0x00030, 0x065e5, 0x00000, 0x00031, 0x00031, 0x065e5, 0x00000, 0x00031,
+ 0x00032, 0x065e5, 0x00000, 0x00031, 0x00033, 0x065e5, 0x00000, 0x00031,
+ 0x00034, 0x065e5, 0x00000, 0x00031, 0x00035, 0x065e5, 0x00000, 0x00031,
+ 0x00036, 0x065e5, 0x00000, 0x00031, 0x00037, 0x065e5, 0x00000, 0x00031,
+ 0x00038, 0x065e5, 0x00000, 0x00031, 0x00039, 0x065e5, 0x00000, 0x00032,
+ 0x00030, 0x065e5, 0x00000, 0x00032, 0x00031, 0x065e5, 0x00000, 0x00032,
+ 0x00032, 0x065e5, 0x00000, 0x00032, 0x00033, 0x065e5, 0x00000, 0x00032,
+ 0x00034, 0x065e5, 0x00000, 0x00032, 0x00035, 0x065e5, 0x00000, 0x00032,
+ 0x00036, 0x065e5, 0x00000, 0x00032, 0x00037, 0x065e5, 0x00000, 0x00032,
+ 0x00038, 0x065e5, 0x00000, 0x00032, 0x00039, 0x065e5, 0x00000, 0x00033,
+ 0x00030, 0x065e5, 0x00000, 0x00033, 0x00031, 0x065e5, 0x00000, 0x00067,
+ 0x00061, 0x0006c, 0x00000, 0x00066, 0x00066, 0x00000, 0x00066, 0x00069,
+ 0x00000, 0x00066, 0x0006c, 0x00000, 0x00066, 0x00066, 0x00069, 0x00000,
+ 0x00066, 0x00066, 0x0006c, 0x00000, 0x0017f, 0x00074, 0x00000, 0x00073,
+ 0x00074, 0x00000, 0x00574, 0x00576, 0x00000, 0x00574, 0x00565, 0x00000,
+ 0x00574, 0x0056b, 0x00000, 0x0057e, 0x00576, 0x00000, 0x00574, 0x0056d,
+ 0x00000, 0x005d9, 0x005b4, 0x00000, 0x005f2, 0x005b7, 0x00000, 0x005e9,
+ 0x005c1, 0x00000, 0x005e9, 0x005c2, 0x00000, 0x0fb49, 0x005c1, 0x00000,
+ 0x0fb49, 0x005c2, 0x00000, 0x005d0, 0x005b7, 0x00000, 0x005d0, 0x005b8,
+ 0x00000, 0x005d0, 0x005bc, 0x00000, 0x005d1, 0x005bc, 0x00000, 0x005d2,
+ 0x005bc, 0x00000, 0x005d3, 0x005bc, 0x00000, 0x005d4, 0x005bc, 0x00000,
+ 0x005d5, 0x005bc, 0x00000, 0x005d6, 0x005bc, 0x00000, 0x005d8, 0x005bc,
+ 0x00000, 0x005d9, 0x005bc, 0x00000, 0x005da, 0x005bc, 0x00000, 0x005db,
+ 0x005bc, 0x00000, 0x005dc, 0x005bc, 0x00000, 0x005de, 0x005bc, 0x00000,
+ 0x005e0, 0x005bc, 0x00000, 0x005e1, 0x005bc, 0x00000, 0x005e3, 0x005bc,
+ 0x00000, 0x005e4, 0x005bc, 0x00000, 0x005e6, 0x005bc, 0x00000, 0x005e7,
+ 0x005bc, 0x00000, 0x005e8, 0x005bc, 0x00000, 0x005e9, 0x005bc, 0x00000,
+ 0x005ea, 0x005bc, 0x00000, 0x005d5, 0x005b9, 0x00000, 0x005d1, 0x005bf,
+ 0x00000, 0x005db, 0x005bf, 0x00000, 0x005e4, 0x005bf, 0x00000, 0x005d0,
+ 0x005dc, 0x00000, 0x00626, 0x00627, 0x00000, 0x00626, 0x00627, 0x00000,
+ 0x00626, 0x006d5, 0x00000, 0x00626, 0x006d5, 0x00000, 0x00626, 0x00648,
+ 0x00000, 0x00626, 0x00648, 0x00000, 0x00626, 0x006c7, 0x00000, 0x00626,
+ 0x006c7, 0x00000, 0x00626, 0x006c6, 0x00000, 0x00626, 0x006c6, 0x00000,
+ 0x00626, 0x006c8, 0x00000, 0x00626, 0x006c8, 0x00000, 0x00626, 0x006d0,
+ 0x00000, 0x00626, 0x006d0, 0x00000, 0x00626, 0x006d0, 0x00000, 0x00626,
+ 0x00649, 0x00000, 0x00626, 0x00649, 0x00000, 0x00626, 0x00649, 0x00000,
+ 0x00626, 0x0062c, 0x00000, 0x00626, 0x0062d, 0x00000, 0x00626, 0x00645,
+ 0x00000, 0x00626, 0x00649, 0x00000, 0x00626, 0x0064a, 0x00000, 0x00628,
+ 0x0062c, 0x00000, 0x00628, 0x0062d, 0x00000, 0x00628, 0x0062e, 0x00000,
+ 0x00628, 0x00645, 0x00000, 0x00628, 0x00649, 0x00000, 0x00628, 0x0064a,
+ 0x00000, 0x0062a, 0x0062c, 0x00000, 0x0062a, 0x0062d, 0x00000, 0x0062a,
+ 0x0062e, 0x00000, 0x0062a, 0x00645, 0x00000, 0x0062a, 0x00649, 0x00000,
+ 0x0062a, 0x0064a, 0x00000, 0x0062b, 0x0062c, 0x00000, 0x0062b, 0x00645,
+ 0x00000, 0x0062b, 0x00649, 0x00000, 0x0062b, 0x0064a, 0x00000, 0x0062c,
+ 0x0062d, 0x00000, 0x0062c, 0x00645, 0x00000, 0x0062d, 0x0062c, 0x00000,
+ 0x0062d, 0x00645, 0x00000, 0x0062e, 0x0062c, 0x00000, 0x0062e, 0x0062d,
+ 0x00000, 0x0062e, 0x00645, 0x00000, 0x00633, 0x0062c, 0x00000, 0x00633,
+ 0x0062d, 0x00000, 0x00633, 0x0062e, 0x00000, 0x00633, 0x00645, 0x00000,
+ 0x00635, 0x0062d, 0x00000, 0x00635, 0x00645, 0x00000, 0x00636, 0x0062c,
+ 0x00000, 0x00636, 0x0062d, 0x00000, 0x00636, 0x0062e, 0x00000, 0x00636,
+ 0x00645, 0x00000, 0x00637, 0x0062d, 0x00000, 0x00637, 0x00645, 0x00000,
+ 0x00638, 0x00645, 0x00000, 0x00639, 0x0062c, 0x00000, 0x00639, 0x00645,
+ 0x00000, 0x0063a, 0x0062c, 0x00000, 0x0063a, 0x00645, 0x00000, 0x00641,
+ 0x0062c, 0x00000, 0x00641, 0x0062d, 0x00000, 0x00641, 0x0062e, 0x00000,
+ 0x00641, 0x00645, 0x00000, 0x00641, 0x00649, 0x00000, 0x00641, 0x0064a,
+ 0x00000, 0x00642, 0x0062d, 0x00000, 0x00642, 0x00645, 0x00000, 0x00642,
+ 0x00649, 0x00000, 0x00642, 0x0064a, 0x00000, 0x00643, 0x00627, 0x00000,
+ 0x00643, 0x0062c, 0x00000, 0x00643, 0x0062d, 0x00000, 0x00643, 0x0062e,
+ 0x00000, 0x00643, 0x00644, 0x00000, 0x00643, 0x00645, 0x00000, 0x00643,
+ 0x00649, 0x00000, 0x00643, 0x0064a, 0x00000, 0x00644, 0x0062c, 0x00000,
+ 0x00644, 0x0062d, 0x00000, 0x00644, 0x0062e, 0x00000, 0x00644, 0x00645,
+ 0x00000, 0x00644, 0x00649, 0x00000, 0x00644, 0x0064a, 0x00000, 0x00645,
+ 0x0062c, 0x00000, 0x00645, 0x0062d, 0x00000, 0x00645, 0x0062e, 0x00000,
+ 0x00645, 0x00645, 0x00000, 0x00645, 0x00649, 0x00000, 0x00645, 0x0064a,
+ 0x00000, 0x00646, 0x0062c, 0x00000, 0x00646, 0x0062d, 0x00000, 0x00646,
+ 0x0062e, 0x00000, 0x00646, 0x00645, 0x00000, 0x00646, 0x00649, 0x00000,
+ 0x00646, 0x0064a, 0x00000, 0x00647, 0x0062c, 0x00000, 0x00647, 0x00645,
+ 0x00000, 0x00647, 0x00649, 0x00000, 0x00647, 0x0064a, 0x00000, 0x0064a,
+ 0x0062c, 0x00000, 0x0064a, 0x0062d, 0x00000, 0x0064a, 0x0062e, 0x00000,
+ 0x0064a, 0x00645, 0x00000, 0x0064a, 0x00649, 0x00000, 0x0064a, 0x0064a,
+ 0x00000, 0x00630, 0x00670, 0x00000, 0x00631, 0x00670, 0x00000, 0x00649,
+ 0x00670, 0x00000, 0x00020, 0x0064c, 0x00651, 0x00000, 0x00020, 0x0064d,
+ 0x00651, 0x00000, 0x00020, 0x0064e, 0x00651, 0x00000, 0x00020, 0x0064f,
+ 0x00651, 0x00000, 0x00020, 0x00650, 0x00651, 0x00000, 0x00020, 0x00651,
+ 0x00670, 0x00000, 0x00626, 0x00631, 0x00000, 0x00626, 0x00632, 0x00000,
+ 0x00626, 0x00645, 0x00000, 0x00626, 0x00646, 0x00000, 0x00626, 0x00649,
+ 0x00000, 0x00626, 0x0064a, 0x00000, 0x00628, 0x00631, 0x00000, 0x00628,
+ 0x00632, 0x00000, 0x00628, 0x00645, 0x00000, 0x00628, 0x00646, 0x00000,
+ 0x00628, 0x00649, 0x00000, 0x00628, 0x0064a, 0x00000, 0x0062a, 0x00631,
+ 0x00000, 0x0062a, 0x00632, 0x00000, 0x0062a, 0x00645, 0x00000, 0x0062a,
+ 0x00646, 0x00000, 0x0062a, 0x00649, 0x00000, 0x0062a, 0x0064a, 0x00000,
+ 0x0062b, 0x00631, 0x00000, 0x0062b, 0x00632, 0x00000, 0x0062b, 0x00645,
+ 0x00000, 0x0062b, 0x00646, 0x00000, 0x0062b, 0x00649, 0x00000, 0x0062b,
+ 0x0064a, 0x00000, 0x00641, 0x00649, 0x00000, 0x00641, 0x0064a, 0x00000,
+ 0x00642, 0x00649, 0x00000, 0x00642, 0x0064a, 0x00000, 0x00643, 0x00627,
+ 0x00000, 0x00643, 0x00644, 0x00000, 0x00643, 0x00645, 0x00000, 0x00643,
+ 0x00649, 0x00000, 0x00643, 0x0064a, 0x00000, 0x00644, 0x00645, 0x00000,
+ 0x00644, 0x00649, 0x00000, 0x00644, 0x0064a, 0x00000, 0x00645, 0x00627,
+ 0x00000, 0x00645, 0x00645, 0x00000, 0x00646, 0x00631, 0x00000, 0x00646,
+ 0x00632, 0x00000, 0x00646, 0x00645, 0x00000, 0x00646, 0x00646, 0x00000,
+ 0x00646, 0x00649, 0x00000, 0x00646, 0x0064a, 0x00000, 0x00649, 0x00670,
+ 0x00000, 0x0064a, 0x00631, 0x00000, 0x0064a, 0x00632, 0x00000, 0x0064a,
+ 0x00645, 0x00000, 0x0064a, 0x00646, 0x00000, 0x0064a, 0x00649, 0x00000,
+ 0x0064a, 0x0064a, 0x00000, 0x00626, 0x0062c, 0x00000, 0x00626, 0x0062d,
+ 0x00000, 0x00626, 0x0062e, 0x00000, 0x00626, 0x00645, 0x00000, 0x00626,
+ 0x00647, 0x00000, 0x00628, 0x0062c, 0x00000, 0x00628, 0x0062d, 0x00000,
+ 0x00628, 0x0062e, 0x00000, 0x00628, 0x00645, 0x00000, 0x00628, 0x00647,
+ 0x00000, 0x0062a, 0x0062c, 0x00000, 0x0062a, 0x0062d, 0x00000, 0x0062a,
+ 0x0062e, 0x00000, 0x0062a, 0x00645, 0x00000, 0x0062a, 0x00647, 0x00000,
+ 0x0062b, 0x00645, 0x00000, 0x0062c, 0x0062d, 0x00000, 0x0062c, 0x00645,
+ 0x00000, 0x0062d, 0x0062c, 0x00000, 0x0062d, 0x00645, 0x00000, 0x0062e,
+ 0x0062c, 0x00000, 0x0062e, 0x00645, 0x00000, 0x00633, 0x0062c, 0x00000,
+ 0x00633, 0x0062d, 0x00000, 0x00633, 0x0062e, 0x00000, 0x00633, 0x00645,
+ 0x00000, 0x00635, 0x0062d, 0x00000, 0x00635, 0x0062e, 0x00000, 0x00635,
+ 0x00645, 0x00000, 0x00636, 0x0062c, 0x00000, 0x00636, 0x0062d, 0x00000,
+ 0x00636, 0x0062e, 0x00000, 0x00636, 0x00645, 0x00000, 0x00637, 0x0062d,
+ 0x00000, 0x00638, 0x00645, 0x00000, 0x00639, 0x0062c, 0x00000, 0x00639,
+ 0x00645, 0x00000, 0x0063a, 0x0062c, 0x00000, 0x0063a, 0x00645, 0x00000,
+ 0x00641, 0x0062c, 0x00000, 0x00641, 0x0062d, 0x00000, 0x00641, 0x0062e,
+ 0x00000, 0x00641, 0x00645, 0x00000, 0x00642, 0x0062d, 0x00000, 0x00642,
+ 0x00645, 0x00000, 0x00643, 0x0062c, 0x00000, 0x00643, 0x0062d, 0x00000,
+ 0x00643, 0x0062e, 0x00000, 0x00643, 0x00644, 0x00000, 0x00643, 0x00645,
+ 0x00000, 0x00644, 0x0062c, 0x00000, 0x00644, 0x0062d, 0x00000, 0x00644,
+ 0x0062e, 0x00000, 0x00644, 0x00645, 0x00000, 0x00644, 0x00647, 0x00000,
+ 0x00645, 0x0062c, 0x00000, 0x00645, 0x0062d, 0x00000, 0x00645, 0x0062e,
+ 0x00000, 0x00645, 0x00645, 0x00000, 0x00646, 0x0062c, 0x00000, 0x00646,
+ 0x0062d, 0x00000, 0x00646, 0x0062e, 0x00000, 0x00646, 0x00645, 0x00000,
+ 0x00646, 0x00647, 0x00000, 0x00647, 0x0062c, 0x00000, 0x00647, 0x00645,
+ 0x00000, 0x00647, 0x00670, 0x00000, 0x0064a, 0x0062c, 0x00000, 0x0064a,
+ 0x0062d, 0x00000, 0x0064a, 0x0062e, 0x00000, 0x0064a, 0x00645, 0x00000,
+ 0x0064a, 0x00647, 0x00000, 0x00626, 0x00645, 0x00000, 0x00626, 0x00647,
+ 0x00000, 0x00628, 0x00645, 0x00000, 0x00628, 0x00647, 0x00000, 0x0062a,
+ 0x00645, 0x00000, 0x0062a, 0x00647, 0x00000, 0x0062b, 0x00645, 0x00000,
+ 0x0062b, 0x00647, 0x00000, 0x00633, 0x00645, 0x00000, 0x00633, 0x00647,
+ 0x00000, 0x00634, 0x00645, 0x00000, 0x00634, 0x00647, 0x00000, 0x00643,
+ 0x00644, 0x00000, 0x00643, 0x00645, 0x00000, 0x00644, 0x00645, 0x00000,
+ 0x00646, 0x00645, 0x00000, 0x00646, 0x00647, 0x00000, 0x0064a, 0x00645,
+ 0x00000, 0x0064a, 0x00647, 0x00000, 0x00640, 0x0064e, 0x00651, 0x00000,
+ 0x00640, 0x0064f, 0x00651, 0x00000, 0x00640, 0x00650, 0x00651, 0x00000,
+ 0x00637, 0x00649, 0x00000, 0x00637, 0x0064a, 0x00000, 0x00639, 0x00649,
+ 0x00000, 0x00639, 0x0064a, 0x00000, 0x0063a, 0x00649, 0x00000, 0x0063a,
+ 0x0064a, 0x00000, 0x00633, 0x00649, 0x00000, 0x00633, 0x0064a, 0x00000,
+ 0x00634, 0x00649, 0x00000, 0x00634, 0x0064a, 0x00000, 0x0062d, 0x00649,
+ 0x00000, 0x0062d, 0x0064a, 0x00000, 0x0062c, 0x00649, 0x00000, 0x0062c,
+ 0x0064a, 0x00000, 0x0062e, 0x00649, 0x00000, 0x0062e, 0x0064a, 0x00000,
+ 0x00635, 0x00649, 0x00000, 0x00635, 0x0064a, 0x00000, 0x00636, 0x00649,
+ 0x00000, 0x00636, 0x0064a, 0x00000, 0x00634, 0x0062c, 0x00000, 0x00634,
+ 0x0062d, 0x00000, 0x00634, 0x0062e, 0x00000, 0x00634, 0x00645, 0x00000,
+ 0x00634, 0x00631, 0x00000, 0x00633, 0x00631, 0x00000, 0x00635, 0x00631,
+ 0x00000, 0x00636, 0x00631, 0x00000, 0x00637, 0x00649, 0x00000, 0x00637,
+ 0x0064a, 0x00000, 0x00639, 0x00649, 0x00000, 0x00639, 0x0064a, 0x00000,
+ 0x0063a, 0x00649, 0x00000, 0x0063a, 0x0064a, 0x00000, 0x00633, 0x00649,
+ 0x00000, 0x00633, 0x0064a, 0x00000, 0x00634, 0x00649, 0x00000, 0x00634,
+ 0x0064a, 0x00000, 0x0062d, 0x00649, 0x00000, 0x0062d, 0x0064a, 0x00000,
+ 0x0062c, 0x00649, 0x00000, 0x0062c, 0x0064a, 0x00000, 0x0062e, 0x00649,
+ 0x00000, 0x0062e, 0x0064a, 0x00000, 0x00635, 0x00649, 0x00000, 0x00635,
+ 0x0064a, 0x00000, 0x00636, 0x00649, 0x00000, 0x00636, 0x0064a, 0x00000,
+ 0x00634, 0x0062c, 0x00000, 0x00634, 0x0062d, 0x00000, 0x00634, 0x0062e,
+ 0x00000, 0x00634, 0x00645, 0x00000, 0x00634, 0x00631, 0x00000, 0x00633,
+ 0x00631, 0x00000, 0x00635, 0x00631, 0x00000, 0x00636, 0x00631, 0x00000,
+ 0x00634, 0x0062c, 0x00000, 0x00634, 0x0062d, 0x00000, 0x00634, 0x0062e,
+ 0x00000, 0x00634, 0x00645, 0x00000, 0x00633, 0x00647, 0x00000, 0x00634,
+ 0x00647, 0x00000, 0x00637, 0x00645, 0x00000, 0x00633, 0x0062c, 0x00000,
+ 0x00633, 0x0062d, 0x00000, 0x00633, 0x0062e, 0x00000, 0x00634, 0x0062c,
+ 0x00000, 0x00634, 0x0062d, 0x00000, 0x00634, 0x0062e, 0x00000, 0x00637,
+ 0x00645, 0x00000, 0x00638, 0x00645, 0x00000, 0x00627, 0x0064b, 0x00000,
+ 0x00627, 0x0064b, 0x00000, 0x0062a, 0x0062c, 0x00645, 0x00000, 0x0062a,
+ 0x0062d, 0x0062c, 0x00000, 0x0062a, 0x0062d, 0x0062c, 0x00000, 0x0062a,
+ 0x0062d, 0x00645, 0x00000, 0x0062a, 0x0062e, 0x00645, 0x00000, 0x0062a,
+ 0x00645, 0x0062c, 0x00000, 0x0062a, 0x00645, 0x0062d, 0x00000, 0x0062a,
+ 0x00645, 0x0062e, 0x00000, 0x0062c, 0x00645, 0x0062d, 0x00000, 0x0062c,
+ 0x00645, 0x0062d, 0x00000, 0x0062d, 0x00645, 0x0064a, 0x00000, 0x0062d,
+ 0x00645, 0x00649, 0x00000, 0x00633, 0x0062d, 0x0062c, 0x00000, 0x00633,
+ 0x0062c, 0x0062d, 0x00000, 0x00633, 0x0062c, 0x00649, 0x00000, 0x00633,
+ 0x00645, 0x0062d, 0x00000, 0x00633, 0x00645, 0x0062d, 0x00000, 0x00633,
+ 0x00645, 0x0062c, 0x00000, 0x00633, 0x00645, 0x00645, 0x00000, 0x00633,
+ 0x00645, 0x00645, 0x00000, 0x00635, 0x0062d, 0x0062d, 0x00000, 0x00635,
+ 0x0062d, 0x0062d, 0x00000, 0x00635, 0x00645, 0x00645, 0x00000, 0x00634,
+ 0x0062d, 0x00645, 0x00000, 0x00634, 0x0062d, 0x00645, 0x00000, 0x00634,
+ 0x0062c, 0x0064a, 0x00000, 0x00634, 0x00645, 0x0062e, 0x00000, 0x00634,
+ 0x00645, 0x0062e, 0x00000, 0x00634, 0x00645, 0x00645, 0x00000, 0x00634,
+ 0x00645, 0x00645, 0x00000, 0x00636, 0x0062d, 0x00649, 0x00000, 0x00636,
+ 0x0062e, 0x00645, 0x00000, 0x00636, 0x0062e, 0x00645, 0x00000, 0x00637,
+ 0x00645, 0x0062d, 0x00000, 0x00637, 0x00645, 0x0062d, 0x00000, 0x00637,
+ 0x00645, 0x00645, 0x00000, 0x00637, 0x00645, 0x0064a, 0x00000, 0x00639,
+ 0x0062c, 0x00645, 0x00000, 0x00639, 0x00645, 0x00645, 0x00000, 0x00639,
+ 0x00645, 0x00645, 0x00000, 0x00639, 0x00645, 0x00649, 0x00000, 0x0063a,
+ 0x00645, 0x00645, 0x00000, 0x0063a, 0x00645, 0x0064a, 0x00000, 0x0063a,
+ 0x00645, 0x00649, 0x00000, 0x00641, 0x0062e, 0x00645, 0x00000, 0x00641,
+ 0x0062e, 0x00645, 0x00000, 0x00642, 0x00645, 0x0062d, 0x00000, 0x00642,
+ 0x00645, 0x00645, 0x00000, 0x00644, 0x0062d, 0x00645, 0x00000, 0x00644,
+ 0x0062d, 0x0064a, 0x00000, 0x00644, 0x0062d, 0x00649, 0x00000, 0x00644,
+ 0x0062c, 0x0062c, 0x00000, 0x00644, 0x0062c, 0x0062c, 0x00000, 0x00644,
+ 0x0062e, 0x00645, 0x00000, 0x00644, 0x0062e, 0x00645, 0x00000, 0x00644,
+ 0x00645, 0x0062d, 0x00000, 0x00644, 0x00645, 0x0062d, 0x00000, 0x00645,
+ 0x0062d, 0x0062c, 0x00000, 0x00645, 0x0062d, 0x00645, 0x00000, 0x00645,
+ 0x0062d, 0x0064a, 0x00000, 0x00645, 0x0062c, 0x0062d, 0x00000, 0x00645,
+ 0x0062c, 0x00645, 0x00000, 0x00645, 0x0062e, 0x0062c, 0x00000, 0x00645,
+ 0x0062e, 0x00645, 0x00000, 0x00645, 0x0062c, 0x0062e, 0x00000, 0x00647,
+ 0x00645, 0x0062c, 0x00000, 0x00647, 0x00645, 0x00645, 0x00000, 0x00646,
+ 0x0062d, 0x00645, 0x00000, 0x00646, 0x0062d, 0x00649, 0x00000, 0x00646,
+ 0x0062c, 0x00645, 0x00000, 0x00646, 0x0062c, 0x00645, 0x00000, 0x00646,
+ 0x0062c, 0x00649, 0x00000, 0x00646, 0x00645, 0x0064a, 0x00000, 0x00646,
+ 0x00645, 0x00649, 0x00000, 0x0064a, 0x00645, 0x00645, 0x00000, 0x0064a,
+ 0x00645, 0x00645, 0x00000, 0x00628, 0x0062e, 0x0064a, 0x00000, 0x0062a,
+ 0x0062c, 0x0064a, 0x00000, 0x0062a, 0x0062c, 0x00649, 0x00000, 0x0062a,
+ 0x0062e, 0x0064a, 0x00000, 0x0062a, 0x0062e, 0x00649, 0x00000, 0x0062a,
+ 0x00645, 0x0064a, 0x00000, 0x0062a, 0x00645, 0x00649, 0x00000, 0x0062c,
+ 0x00645, 0x0064a, 0x00000, 0x0062c, 0x0062d, 0x00649, 0x00000, 0x0062c,
+ 0x00645, 0x00649, 0x00000, 0x00633, 0x0062e, 0x00649, 0x00000, 0x00635,
+ 0x0062d, 0x0064a, 0x00000, 0x00634, 0x0062d, 0x0064a, 0x00000, 0x00636,
+ 0x0062d, 0x0064a, 0x00000, 0x00644, 0x0062c, 0x0064a, 0x00000, 0x00644,
+ 0x00645, 0x0064a, 0x00000, 0x0064a, 0x0062d, 0x0064a, 0x00000, 0x0064a,
+ 0x0062c, 0x0064a, 0x00000, 0x0064a, 0x00645, 0x0064a, 0x00000, 0x00645,
+ 0x00645, 0x0064a, 0x00000, 0x00642, 0x00645, 0x0064a, 0x00000, 0x00646,
+ 0x0062d, 0x0064a, 0x00000, 0x00642, 0x00645, 0x0062d, 0x00000, 0x00644,
+ 0x0062d, 0x00645, 0x00000, 0x00639, 0x00645, 0x0064a, 0x00000, 0x00643,
+ 0x00645, 0x0064a, 0x00000, 0x00646, 0x0062c, 0x0062d, 0x00000, 0x00645,
+ 0x0062e, 0x0064a, 0x00000, 0x00644, 0x0062c, 0x00645, 0x00000, 0x00643,
+ 0x00645, 0x00645, 0x00000, 0x00644, 0x0062c, 0x00645, 0x00000, 0x00646,
+ 0x0062c, 0x0062d, 0x00000, 0x0062c, 0x0062d, 0x0064a, 0x00000, 0x0062d,
+ 0x0062c, 0x0064a, 0x00000, 0x00645, 0x0062c, 0x0064a, 0x00000, 0x00641,
+ 0x00645, 0x0064a, 0x00000, 0x00628, 0x0062d, 0x0064a, 0x00000, 0x00643,
+ 0x00645, 0x00645, 0x00000, 0x00639, 0x0062c, 0x00645, 0x00000, 0x00635,
+ 0x00645, 0x00645, 0x00000, 0x00633, 0x0062e, 0x0064a, 0x00000, 0x00646,
+ 0x0062c, 0x0064a, 0x00000, 0x00635, 0x00644, 0x006d2, 0x00000, 0x00642,
+ 0x00644, 0x006d2, 0x00000, 0x00627, 0x00644, 0x00644, 0x00647, 0x00000,
+ 0x00627, 0x00643, 0x00628, 0x00631, 0x00000, 0x00645, 0x0062d, 0x00645,
+ 0x0062f, 0x00000, 0x00635, 0x00644, 0x00639, 0x00645, 0x00000, 0x00631,
+ 0x00633, 0x00648, 0x00644, 0x00000, 0x00639, 0x00644, 0x0064a, 0x00647,
+ 0x00000, 0x00648, 0x00633, 0x00644, 0x00645, 0x00000, 0x00635, 0x00644,
+ 0x00649, 0x00000, 0x00635, 0x00644, 0x00649, 0x00020, 0x00627, 0x00644,
+ 0x00644, 0x00647, 0x00020, 0x00639, 0x00644, 0x0064a, 0x00647, 0x00020,
+ 0x00648, 0x00633, 0x00644, 0x00645, 0x00000, 0x0062c, 0x00644, 0x00020,
+ 0x0062c, 0x00644, 0x00627, 0x00644, 0x00647, 0x00000, 0x00631, 0x006cc,
+ 0x00627, 0x00644, 0x00000, 0x00020, 0x0064b, 0x00000, 0x00640, 0x0064b,
+ 0x00000, 0x00020, 0x0064c, 0x00000, 0x00020, 0x0064d, 0x00000, 0x00020,
+ 0x0064e, 0x00000, 0x00640, 0x0064e, 0x00000, 0x00020, 0x0064f, 0x00000,
+ 0x00640, 0x0064f, 0x00000, 0x00020, 0x00650, 0x00000, 0x00640, 0x00650,
+ 0x00000, 0x00020, 0x00651, 0x00000, 0x00640, 0x00651, 0x00000, 0x00020,
+ 0x00652, 0x00000, 0x00640, 0x00652, 0x00000, 0x00644, 0x00622, 0x00000,
+ 0x00644, 0x00622, 0x00000, 0x00644, 0x00623, 0x00000, 0x00644, 0x00623,
+ 0x00000, 0x00644, 0x00625, 0x00000, 0x00644, 0x00625, 0x00000, 0x00644,
+ 0x00627, 0x00000, 0x00644, 0x00627, 0x00000, 0x11099, 0x110ba, 0x00000,
+ 0x1109b, 0x110ba, 0x00000, 0x110a5, 0x110ba, 0x00000, 0x11131, 0x11127,
+ 0x00000, 0x11132, 0x11127, 0x00000, 0x11347, 0x1133e, 0x00000, 0x11347,
+ 0x11357, 0x00000, 0x114b9, 0x114ba, 0x00000, 0x114b9, 0x114b0, 0x00000,
+ 0x114b9, 0x114bd, 0x00000, 0x115b8, 0x115af, 0x00000, 0x115b9, 0x115af,
+ 0x00000, 0x1d157, 0x1d165, 0x00000, 0x1d158, 0x1d165, 0x00000, 0x1d15f,
+ 0x1d16e, 0x00000, 0x1d15f, 0x1d16f, 0x00000, 0x1d15f, 0x1d170, 0x00000,
+ 0x1d15f, 0x1d171, 0x00000, 0x1d15f, 0x1d172, 0x00000, 0x1d1b9, 0x1d165,
+ 0x00000, 0x1d1ba, 0x1d165, 0x00000, 0x1d1bb, 0x1d16e, 0x00000, 0x1d1bc,
+ 0x1d16e, 0x00000, 0x1d1bb, 0x1d16f, 0x00000, 0x1d1bc, 0x1d16f, 0x00000,
+ 0x00030, 0x0002e, 0x00000, 0x00030, 0x0002c, 0x00000, 0x00031, 0x0002c,
+ 0x00000, 0x00032, 0x0002c, 0x00000, 0x00033, 0x0002c, 0x00000, 0x00034,
+ 0x0002c, 0x00000, 0x00035, 0x0002c, 0x00000, 0x00036, 0x0002c, 0x00000,
+ 0x00037, 0x0002c, 0x00000, 0x00038, 0x0002c, 0x00000, 0x00039, 0x0002c,
+ 0x00000, 0x00028, 0x00041, 0x00029, 0x00000, 0x00028, 0x00042, 0x00029,
+ 0x00000, 0x00028, 0x00043, 0x00029, 0x00000, 0x00028, 0x00044, 0x00029,
+ 0x00000, 0x00028, 0x00045, 0x00029, 0x00000, 0x00028, 0x00046, 0x00029,
+ 0x00000, 0x00028, 0x00047, 0x00029, 0x00000, 0x00028, 0x00048, 0x00029,
+ 0x00000, 0x00028, 0x00049, 0x00029, 0x00000, 0x00028, 0x0004a, 0x00029,
+ 0x00000, 0x00028, 0x0004b, 0x00029, 0x00000, 0x00028, 0x0004c, 0x00029,
+ 0x00000, 0x00028, 0x0004d, 0x00029, 0x00000, 0x00028, 0x0004e, 0x00029,
+ 0x00000, 0x00028, 0x0004f, 0x00029, 0x00000, 0x00028, 0x00050, 0x00029,
+ 0x00000, 0x00028, 0x00051, 0x00029, 0x00000, 0x00028, 0x00052, 0x00029,
+ 0x00000, 0x00028, 0x00053, 0x00029, 0x00000, 0x00028, 0x00054, 0x00029,
+ 0x00000, 0x00028, 0x00055, 0x00029, 0x00000, 0x00028, 0x00056, 0x00029,
+ 0x00000, 0x00028, 0x00057, 0x00029, 0x00000, 0x00028, 0x00058, 0x00029,
+ 0x00000, 0x00028, 0x00059, 0x00029, 0x00000, 0x00028, 0x0005a, 0x00029,
+ 0x00000, 0x03014, 0x00053, 0x03015, 0x00000, 0x00043, 0x00044, 0x00000,
+ 0x00057, 0x0005a, 0x00000, 0x00048, 0x00056, 0x00000, 0x0004d, 0x00056,
+ 0x00000, 0x00053, 0x00044, 0x00000, 0x00053, 0x00053, 0x00000, 0x00050,
+ 0x00050, 0x00056, 0x00000, 0x00057, 0x00043, 0x00000, 0x0004d, 0x00043,
+ 0x00000, 0x0004d, 0x00044, 0x00000, 0x00044, 0x0004a, 0x00000, 0x0307b,
+ 0x0304b, 0x00000, 0x030b3, 0x030b3, 0x00000, 0x03014, 0x0672c, 0x03015,
+ 0x00000, 0x03014, 0x04e09, 0x03015, 0x00000, 0x03014, 0x04e8c, 0x03015,
+ 0x00000, 0x03014, 0x05b89, 0x03015, 0x00000, 0x03014, 0x070b9, 0x03015,
+ 0x00000, 0x03014, 0x06253, 0x03015, 0x00000, 0x03014, 0x076d7, 0x03015,
+ 0x00000, 0x03014, 0x052dd, 0x03015, 0x00000, 0x03014, 0x06557, 0x03015,
+ 0x00000
+};
diff --git a/src/lib/unicodemap.pl b/src/lib/unicodemap.pl
new file mode 100755
index 0000000..2c1bf7a
--- /dev/null
+++ b/src/lib/unicodemap.pl
@@ -0,0 +1,162 @@
+#!/usr/bin/env perl
+use strict;
+
+my (%titlecase8, %uni8_decomp);
+my (@titlecase16_keys, @titlecase16_values);
+my (@titlecase32_keys, @titlecase32_values);
+my (@uni16_decomp_keys, @uni16_decomp_values);
+my (@uni32_decomp_keys, @uni32_decomp_values);
+my (@multidecomp_keys, @multidecomp_offsets, @multidecomp_values);
+while (<>) {
+ chomp $_;
+ my @arr = split(";");
+ my $code = eval("0x".$arr[0]);
+ my $decomp = $arr[5];
+ my $titlecode = $arr[14];
+
+ if ($titlecode ne "") {
+ # titlecase mapping
+ my $value = eval("0x$titlecode");
+ if ($value == $code) {
+ # the same character, ignore
+ } elsif ($code <= 0xff) {
+ die "Error: We've assumed 8bit keys have max. 16bit values" if ($value > 0xffff);
+ $titlecase8{$code} = $value;
+ } elsif ($code <= 0xffff) {
+ die "Error: We've assumed 16bit keys have max. 16bit values" if ($value > 0xffff);
+ push @titlecase16_keys, $code;
+ push @titlecase16_values, $value;
+ } else {
+ push @titlecase32_keys, $code;
+ push @titlecase32_values, $value;
+ }
+ } elsif ($decomp =~ /(?:\<[^>]*> )?(.+)/) {
+ # decompositions
+ my $decomp_codes = $1;
+ if ($decomp_codes =~ /^([0-9A-Z]*)$/i) {
+ # unicharacter decomposition. use separate lists for this
+ my $value = eval("0x$1");
+ if ($value > 0xffffffff) {
+ print STDERR "Error: We've assumed decomposition codes are max. 32bit\n";
+ exit 1;
+ }
+ if ($code <= 0xff) {
+ $uni8_decomp{$code} = $value;
+ } elsif ($code <= 0xffff) {
+ push @uni16_decomp_keys, $code;
+ push @uni16_decomp_values, $value;
+ } else {
+ push @uni32_decomp_keys, $code;
+ push @uni32_decomp_values, $value;
+ }
+ } else {
+ # multicharacter decomposition.
+ if ($code > 0xffffffff) {
+ print STDERR "Error: We've assumed multi-decomposition key codes are max. 32bit\n";
+ exit 1;
+ }
+
+ push @multidecomp_keys, $code;
+ push @multidecomp_offsets, scalar(@multidecomp_values);
+
+ foreach my $dcode (split(" ", $decomp_codes)) {
+ my $value = eval("0x$dcode");
+ if ($value > 0xffffffff) {
+ print STDERR "Error: We've assumed decomposition codes are max. 32bit\n";
+ exit 1;
+ }
+ push @multidecomp_values, $value;
+ }
+ push @multidecomp_values, 0;
+ }
+ }
+}
+
+sub print_list {
+ my @list = @{$_[0]};
+
+ my $last = $#list;
+ my $n = 0;
+ foreach my $key (@list) {
+ printf("0x%05x", $key);
+ last if ($n == $last);
+ print ",";
+
+ $n++;
+ if (($n % 8) == 0) {
+ print "\n\t";
+ } else {
+ print " ";
+ }
+ }
+}
+
+print "/* This file is automatically generated by unicodemap.pl from UnicodeData.txt
+
+ NOTE: decompositions for characters having titlecase characters
+ are not included, because we first translate everything to titlecase */\n";
+
+sub print_map8 {
+ my %map = %{$_[0]};
+ my @list;
+ for (my $i = 0; $i <= 0xff; $i++) {
+ if (defined($map{$i})) {
+ push @list, $map{$i};
+ } else {
+ push @list, $i;
+ }
+ }
+ print_list(\@list);
+}
+
+print "static const uint16_t titlecase8_map[256] = {\n\t";
+print_map8(\%titlecase8);
+print "\n};\n";
+
+print "static const uint16_t titlecase16_keys[] = {\n\t";
+print_list(\@titlecase16_keys);
+print "\n};\n";
+
+print "static const uint16_t titlecase16_values[] = {\n\t";
+print_list(\@titlecase16_values);
+print "\n};\n";
+
+print "static const uint32_t titlecase32_keys[] = {\n\t";
+print_list(\@titlecase32_keys);
+print "\n};\n";
+
+print "static const uint32_t titlecase32_values[] = {\n\t";
+print_list(\@titlecase32_values);
+print "\n};\n";
+
+print "static const uint16_t uni8_decomp_map[256] = {\n\t";
+print_map8(\%uni8_decomp);
+print "\n};\n";
+
+print "static const uint16_t uni16_decomp_keys[] = {\n\t";
+print_list(\@uni16_decomp_keys);
+print "\n};\n";
+
+print "static const uint32_t uni16_decomp_values[] = {\n\t";
+print_list(\@uni16_decomp_values);
+print "\n};\n";
+
+print "static const uint32_t uni32_decomp_keys[] = {\n\t";
+print_list(\@uni32_decomp_keys);
+print "\n};\n";
+
+print "static const uint32_t uni32_decomp_values[] = {\n\t";
+print_list(\@uni32_decomp_values);
+print "\n};\n";
+
+print "static const uint32_t multidecomp_keys[] = {\n\t";
+print_list(\@multidecomp_keys);
+print "\n};\n";
+
+print "static const uint16_t multidecomp_offsets[] = {\n\t";
+print_list(\@multidecomp_offsets);
+print "\n};\n";
+
+print "static const uint32_t multidecomp_values[] = {\n\t";
+print_list(\@multidecomp_values);
+print "\n};\n";
diff --git a/src/lib/unix-socket-create.c b/src/lib/unix-socket-create.c
new file mode 100644
index 0000000..3614df1
--- /dev/null
+++ b/src/lib/unix-socket-create.c
@@ -0,0 +1,36 @@
+/* Copyright (c) 2005-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "net.h"
+#include "unix-socket-create.h"
+
+#include <unistd.h>
+#include <sys/stat.h>
+
+int unix_socket_create(const char *path, int mode,
+ uid_t uid, gid_t gid, int backlog)
+{
+ mode_t old_umask;
+ int fd;
+
+ old_umask = umask(0777 ^ mode);
+ fd = net_listen_unix_unlink_stale(path, backlog);
+ umask(old_umask);
+
+ if (fd < 0) {
+ i_error("net_listen_unix(%s) failed: %m", path);
+ return -1;
+ }
+
+ if (uid != (uid_t)-1 || gid != (gid_t)-1) {
+ /* set correct permissions */
+ if (chown(path, uid, gid) < 0) {
+ i_error("chown(%s, %s, %s) failed: %m",
+ path, dec2str(uid), dec2str(gid));
+ i_close_fd(&fd);
+ return -1;
+ }
+ }
+ return fd;
+}
+
diff --git a/src/lib/unix-socket-create.h b/src/lib/unix-socket-create.h
new file mode 100644
index 0000000..88ffffb
--- /dev/null
+++ b/src/lib/unix-socket-create.h
@@ -0,0 +1,7 @@
+#ifndef UNIX_SOCKET_CREATE_H
+#define UNIX_SOCKET_CREATE_H
+
+int unix_socket_create(const char *path, int mode,
+ uid_t uid, gid_t gid, int backlog);
+
+#endif
diff --git a/src/lib/unlink-directory.c b/src/lib/unlink-directory.c
new file mode 100644
index 0000000..98fddd1
--- /dev/null
+++ b/src/lib/unlink-directory.c
@@ -0,0 +1,283 @@
+/* Copyright (c) 2002-2018 Dovecot authors, see the included COPYING file */
+
+/*
+ There's a bit tricky race condition with recursive deletion.
+ Suppose this happens:
+
+ lstat(dir, ..) -> OK, it's a directory
+ // attacker deletes dir, replaces it with symlink to /
+ opendir(dir) -> it actually opens /
+
+ Most portable solution is to lstat() the dir, chdir() there, then check
+ that "." points to same device/inode as we originally lstat()ed. This
+ assumes that the device has usable inodes, most should except for some NFS
+ implementations.
+
+ Filesystems may also reassign a deleted inode to another file
+ immediately after it's deleted. That in theory makes it possible to exploit
+ this race to delete the new directory. However, the new inode is quite
+ unlikely to be any important directory, and attacker is quite unlikely to
+ find out which directory even got the inode. Maybe with some setuid program
+ or daemon interaction something could come out of it though.
+
+ Another less portable solution is to fchdir(open(dir, O_NOFOLLOW)).
+ This should be completely safe.
+
+ The actual file deletion also has to be done relative to current
+ directory, to make sure that the whole directory structure isn't replaced
+ with another one while we're deleting it. Going back to parent directory
+ isn't too easy either - safest (and easiest) way again is to open() the
+ directory and fchdir() back there.
+*/
+
+#define _GNU_SOURCE /* for O_NOFOLLOW with Linux */
+
+#include "lib.h"
+#include "path-util.h"
+#include "unlink-directory.h"
+
+#include <fcntl.h>
+#include <unistd.h>
+#include <dirent.h>
+#include <sys/stat.h>
+
+#define ERROR_FORMAT "%s(%s) failed: %m"
+#define ERROR_FORMAT_DNAME "%s(%s/%s) failed: %m"
+
+static void ATTR_FORMAT(3,4)
+unlink_directory_error(const char **error,
+ int *first_errno,
+ const char *fmt, ...)
+{
+ va_list args;
+ va_start(args, fmt);
+ const char *err = t_strdup_vprintf(fmt, args);
+ if (*error == NULL) {
+ if (first_errno != NULL)
+ *first_errno = errno;
+ *error = err;
+ } else
+ i_error("%s", err);
+ va_end(args);
+}
+
+static int
+unlink_directory_r(const char *dir, enum unlink_directory_flags flags,
+ const char **error)
+{
+ DIR *dirp;
+ struct dirent *d;
+ struct stat st;
+ int dir_fd, old_errno;
+
+#ifdef O_NOFOLLOW
+ dir_fd = open(dir, O_RDONLY | O_NOFOLLOW);
+ if (dir_fd == -1) {
+ unlink_directory_error(error, NULL,
+ "open(%s, O_RDONLY | O_NOFOLLOW) failed: %m",
+ dir);
+ return -1;
+ }
+#else
+ struct stat st2;
+
+ if (lstat(dir, &st) < 0) {
+ unlink_directory_error(error_r, NULL, ERROR_FORMAT, "lstat", dir);
+ return -1;
+ }
+
+ if (!S_ISDIR(st.st_mode)) {
+ if ((st.st_mode & S_IFMT) != S_IFLNK) {
+ unlink_directory_error(error_r, NULL, "%s is not a directory: %s", dir);
+ errno = ENOTDIR;
+ } else {
+ /* be compatible with O_NOFOLLOW */
+ errno = ELOOP;
+ unlink_directory_error(error_r, NULL, "%s is a symlink, not a directory: %s", dir);
+ }
+ return -1;
+ }
+
+ dir_fd = open(dir, O_RDONLY);
+ if (dir_fd == -1) {
+ unlink_directory_error(error_r, NULL, "open(%s, O_RDONLY) failed: %m", dir);
+ return -1;
+ }
+
+ if (fstat(dir_fd, &st2) < 0) {
+ i_close_fd(&dir_fd);
+ unlink_directory_error(error_r, NULL, ERROR_FORMAT, "fstat", dir);
+ return -1;
+ }
+
+ if (st.st_ino != st2.st_ino ||
+ !CMP_DEV_T(st.st_dev, st2.st_dev)) {
+ /* directory was just replaced with something else. */
+ i_close_fd(&dir_fd);
+ errno = ENOTDIR;
+ unlink_directory_error(error_r, NULL, "%s race condition: directory was just replaced", dir);
+ return -1;
+ }
+#endif
+ if (fchdir(dir_fd) < 0) {
+ i_close_fd(&dir_fd);
+ unlink_directory_error(error, NULL, ERROR_FORMAT, "fchdir", dir);
+ return -1;
+ }
+
+ dirp = opendir(".");
+ if (dirp == NULL) {
+ i_close_fd(&dir_fd);
+ unlink_directory_error(error, NULL, "opendir(.) (in %s) failed: %m", dir);
+ return -1;
+ }
+
+ int first_errno = 0;
+ for (;;) {
+ errno = 0;
+ d = readdir(dirp);
+ if (d == NULL) {
+ if (errno != 0) {
+ unlink_directory_error(error,
+ &first_errno,
+ ERROR_FORMAT,
+ "readdir",
+ dir);
+ }
+ break;
+ }
+ if (d->d_name[0] == '.') {
+ if ((d->d_name[1] == '\0' ||
+ (d->d_name[1] == '.' && d->d_name[2] == '\0'))) {
+ /* skip . and .. */
+ continue;
+ }
+ if ((flags & UNLINK_DIRECTORY_FLAG_SKIP_DOTFILES) != 0)
+ continue;
+ }
+
+ if (unlink(d->d_name) < 0 && errno != ENOENT) {
+ old_errno = errno;
+
+ if (lstat(d->d_name, &st) < 0) {
+ if (errno != ENOENT) {
+ unlink_directory_error(error,
+ &first_errno,
+ ERROR_FORMAT,
+ "lstat",
+ dir);
+ break;
+ }
+ } else if (S_ISDIR(st.st_mode) &&
+ (flags & UNLINK_DIRECTORY_FLAG_FILES_ONLY) == 0) {
+ if (unlink_directory_r(d->d_name, flags, error) < 0) {
+ if (first_errno == 0)
+ first_errno = errno;
+ if (errno != ENOENT)
+ break;
+ }
+ if (fchdir(dir_fd) < 0) {
+ unlink_directory_error(error,
+ &first_errno,
+ ERROR_FORMAT,
+ "fchdir",
+ dir);
+ break;
+ }
+
+ if (rmdir(d->d_name) < 0 &&
+ errno != ENOENT) {
+ if (errno == EEXIST)
+ /* standardize errno */
+ errno = ENOTEMPTY;
+ unlink_directory_error(error,
+ &first_errno,
+ ERROR_FORMAT,
+ "rmdir",
+ dir);
+ break;
+ }
+ } else if (S_ISDIR(st.st_mode) &&
+ (flags & UNLINK_DIRECTORY_FLAG_FILES_ONLY) != 0) {
+ /* skip directory */
+ } else if (old_errno == EBUSY &&
+ str_begins(d->d_name, ".nfs")) {
+ /* can't delete NFS files that are still
+ in use. let the caller decide if this error
+ is worth logging about */
+ break;
+ } else
+ /* so it wasn't a directory */
+ unlink_directory_error(error,
+ &first_errno,
+ ERROR_FORMAT_DNAME,
+ "unlink",
+ dir,
+ d->d_name);
+ }
+ }
+
+ i_close_fd(&dir_fd);
+ if (closedir(dirp) < 0)
+ unlink_directory_error(error,
+ &first_errno,
+ ERROR_FORMAT,
+ "closedir",
+ dir);
+
+ if (*error != NULL) {
+ errno = first_errno;
+ return -1;
+ }
+ return 0;
+}
+
+int unlink_directory(const char *dir, enum unlink_directory_flags flags,
+ const char **error_r)
+{
+ const char *orig_dir, *error;
+ int fd, ret, old_errno;
+
+ if (t_get_working_dir(&orig_dir, &error) < 0) {
+ i_warning("Could not get working directory in unlink_directory(): %s",
+ error);
+ orig_dir = ".";
+ }
+
+ fd = open(".", O_RDONLY);
+ if (fd == -1) {
+ *error_r = t_strdup_printf(
+ "Can't preserve current directory %s: "
+ "open(.) failed: %m", orig_dir);
+ return -1;
+ }
+
+ /* Cannot set error_r to NULL inside of unlink_directory_r()
+ because of recursion */
+ *error_r = NULL;
+ ret = unlink_directory_r(dir, flags, error_r);
+ old_errno = errno;
+
+ if (fchdir(fd) < 0) {
+ i_fatal("unlink_directory(%s): "
+ "Can't fchdir() back to our original dir %s: %m", dir, orig_dir);
+ }
+ i_close_fd(&fd);
+
+ if (ret < 0) {
+ errno = old_errno;
+ return errno == ENOENT ? 0 : -1;
+ }
+
+ if ((flags & UNLINK_DIRECTORY_FLAG_RMDIR) != 0) {
+ if (rmdir(dir) < 0 && errno != ENOENT) {
+ *error_r = t_strdup_printf("rmdir(%s) failed: %m", dir);
+ if (errno == EEXIST) {
+ /* standardize errno */
+ errno = ENOTEMPTY;
+ }
+ return errno == ENOENT ? 0 : 1;
+ }
+ }
+ return 1;
+}
diff --git a/src/lib/unlink-directory.h b/src/lib/unlink-directory.h
new file mode 100644
index 0000000..fbc2a44
--- /dev/null
+++ b/src/lib/unlink-directory.h
@@ -0,0 +1,21 @@
+#ifndef UNLINK_DIRECTORY_H
+#define UNLINK_DIRECTORY_H
+
+enum unlink_directory_flags {
+ /* After unlinking all files, rmdir() the directory itself */
+ UNLINK_DIRECTORY_FLAG_RMDIR = 0x01,
+ /* Don't unlink any files beginning with "." */
+ UNLINK_DIRECTORY_FLAG_SKIP_DOTFILES = 0x02,
+ /* Don't recurse into subdirectories */
+ UNLINK_DIRECTORY_FLAG_FILES_ONLY = 0x04
+};
+
+/* Unlink directory and/or everything under it.
+ Returns 1 if successful, 0 if error is ENOENT, -1 if other error.
+ The returned error message contains the exact syscall that failed,
+ e.g. "open(path) failed: Permission denied"
+ In case of ENOENT error, error message is also set. */
+int unlink_directory(const char *dir, enum unlink_directory_flags flags,
+ const char **error_r);
+
+#endif
diff --git a/src/lib/unlink-old-files.c b/src/lib/unlink-old-files.c
new file mode 100644
index 0000000..4074327
--- /dev/null
+++ b/src/lib/unlink-old-files.c
@@ -0,0 +1,73 @@
+/* Copyright (c) 2002-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "ioloop.h"
+#include "str.h"
+#include "unlink-old-files.h"
+
+#include <signal.h>
+#include <unistd.h>
+#include <dirent.h>
+#include <sys/stat.h>
+#include <utime.h>
+
+static int
+unlink_old_files_real(const char *dir, const char *prefix, time_t min_time)
+{
+ DIR *dirp;
+ struct dirent *d;
+ struct stat st;
+ string_t *path;
+ size_t prefix_len, dir_len;
+
+ dirp = opendir(dir);
+ if (dirp == NULL) {
+ if (errno != ENOENT)
+ i_error("opendir(%s) failed: %m", dir);
+ return -1;
+ }
+
+ /* update atime immediately, so if this scanning is done based on
+ atime it won't be done by multiple processes if the scan is slow */
+ if (utime(dir, NULL) < 0 && errno != ENOENT)
+ i_error("utime(%s) failed: %m", dir);
+
+ path = t_str_new(256);
+ str_printfa(path, "%s/", dir);
+ dir_len = str_len(path);
+
+ prefix_len = strlen(prefix);
+ while ((d = readdir(dirp)) != NULL) {
+ if (d->d_name[0] == '.' &&
+ (d->d_name[1] == '\0' ||
+ (d->d_name[1] == '.' && d->d_name[2] == '\0'))) {
+ /* skip . and .. */
+ continue;
+ }
+ if (strncmp(d->d_name, prefix, prefix_len) != 0)
+ continue;
+
+ str_truncate(path, dir_len);
+ str_append(path, d->d_name);
+ if (stat(str_c(path), &st) < 0) {
+ if (errno != ENOENT)
+ i_error("stat(%s) failed: %m", str_c(path));
+ } else if (!S_ISDIR(st.st_mode) && st.st_ctime < min_time) {
+ i_unlink_if_exists(str_c(path));
+ }
+ }
+
+ if (closedir(dirp) < 0)
+ i_error("closedir(%s) failed: %m", dir);
+ return 0;
+}
+
+int unlink_old_files(const char *dir, const char *prefix, time_t min_time)
+{
+ int ret;
+
+ T_BEGIN {
+ ret = unlink_old_files_real(dir, prefix, min_time);
+ } T_END;
+ return ret;
+}
diff --git a/src/lib/unlink-old-files.h b/src/lib/unlink-old-files.h
new file mode 100644
index 0000000..927adb7
--- /dev/null
+++ b/src/lib/unlink-old-files.h
@@ -0,0 +1,9 @@
+#ifndef UNLINK_OLD_FILES_H
+#define UNLINK_OLD_FILES_H
+
+/* Unlink all files from directory beginning with given prefix and having
+ ctime older than min_time. Makes sure that the directory's atime is updated.
+ Returns -1 if there were some errors. */
+int unlink_old_files(const char *dir, const char *prefix, time_t min_time);
+
+#endif
diff --git a/src/lib/uri-util.c b/src/lib/uri-util.c
new file mode 100644
index 0000000..498bc88
--- /dev/null
+++ b/src/lib/uri-util.c
@@ -0,0 +1,1332 @@
+/* Copyright (c) 2010-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "str.h"
+#include "net.h"
+#include "uri-util.h"
+
+#include <ctype.h>
+
+/* [URI-GEN] RFC3986 Appendix A:
+
+ URI = scheme ":" hier-part [ "?" query ] [ "#" fragment ]
+ absolute-URI = scheme ":" hier-part [ "?" query ]
+ scheme = ALPHA *( ALPHA / DIGIT / "+" / "-" / "." )
+
+ URI-reference = URI / relative-ref
+ relative-ref = relative-part [ "?" query ] [ "#" fragment ]
+
+ relative-part = "//" authority path-abempty
+ / path-absolute
+ / path-noscheme
+ / path-empty
+ hier-part = "//" authority path-abempty
+ / path-absolute
+ / path-rootless
+ / path-empty
+
+ authority = [ userinfo "@" ] host [ ":" port ]
+ userinfo = *( unreserved / pct-encoded / sub-delims / ":" )
+ host = IP-literal / IPv4address / reg-name
+ port = *DIGIT
+
+ IP-literal = "[" ( IPv6address / IPvFuture ) "]"
+ IPvFuture = "v" 1*HEXDIG "." 1*( unreserved / sub-delims / ":" )
+ IPv6address = 6( h16 ":" ) ls32
+ / "::" 5( h16 ":" ) ls32
+ / [ h16 ] "::" 4( h16 ":" ) ls32
+ / [ *1( h16 ":" ) h16 ] "::" 3( h16 ":" ) ls32
+ / [ *2( h16 ":" ) h16 ] "::" 2( h16 ":" ) ls32
+ / [ *3( h16 ":" ) h16 ] "::" h16 ":" ls32
+ / [ *4( h16 ":" ) h16 ] "::" ls32
+ / [ *5( h16 ":" ) h16 ] "::" h16
+ / [ *6( h16 ":" ) h16 ] "::"
+ h16 = 1*4HEXDIG
+ ls32 = ( h16 ":" h16 ) / IPv4address
+ IPv4address = dec-octet "." dec-octet "." dec-octet "." dec-octet
+ dec-octet = DIGIT ; 0-9
+ / %x31-39 DIGIT ; 10-99
+ / "1" 2DIGIT ; 100-199
+ / "2" %x30-34 DIGIT ; 200-249
+ / "25" %x30-35 ; 250-255
+ reg-name = *( unreserved / pct-encoded / sub-delims )
+
+ path = path-abempty ; begins with "/" or is empty
+ / path-absolute ; begins with "/" but not "//"
+ / path-noscheme ; begins with a non-colon segment
+ / path-rootless ; begins with a segment
+ / path-empty ; zero characters
+ path-abempty = *( "/" segment )
+ path-absolute = "/" [ segment-nz *( "/" segment ) ]
+ path-noscheme = segment-nz-nc *( "/" segment )
+ path-rootless = segment-nz *( "/" segment )
+ path-empty = 0<pchar>
+
+ segment = *pchar
+ segment-nz = 1*pchar
+ segment-nz-nc = 1*( unreserved / pct-encoded / sub-delims / "@" )
+ ; non-zero-length segment without any colon ":"
+ pchar = unreserved / pct-encoded / sub-delims / ":" / "@"
+
+ query = *( pchar / "/" / "?" )
+ fragment = *( pchar / "/" / "?" )
+
+ pct-encoded = "%" HEXDIG HEXDIG
+ unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~"
+ reserved = gen-delims / sub-delims
+ gen-delims = ":" / "/" / "?" / "#" / "[" / "]" / "@"
+ sub-delims = "!" / "$" / "&" / "'" / "(" / ")"
+ / "*" / "+" / "," / ";" / "="
+ */
+
+#define URI_MAX_SCHEME_NAME_LEN 64
+
+/* Character lookup table
+ *
+ * unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~" [bit0]
+ * sub-delims = "!" / "$" / "&" / "'" / "(" / ")"
+ * / "*" / "+" / "," / ";" / "=" [bit1]
+ * gen-delims = ":" / "/" / "?" / "#" / "[" / "]" / "@" [bit2]
+ * pchar = unreserved / sub-delims / ":" / "@" [bit0|bit1|bit3]
+ * 'pfchar' = unreserved / sub-delims / ":" / "@" / "/"
+ * [bit0|bit1|bit3|bit5]
+ * 'uchar' = unreserved / sub-delims / ":" [bit0|bit1|bit4]
+ * 'qchar' = pchar / "/" / "?" [bit0|bit1|bit3|bit5|bit6]
+ *
+ */
+
+#define CHAR_MASK_UNRESERVED (1<<0)
+#define CHAR_MASK_SUB_DELIMS (1<<1)
+#define CHAR_MASK_PCHAR ((1<<0)|(1<<1)|(1<<3))
+#define CHAR_MASK_PFCHAR ((1<<0)|(1<<1)|(1<<3)|(1<<5))
+#define CHAR_MASK_UCHAR ((1<<0)|(1<<1)|(1<<4))
+#define CHAR_MASK_QCHAR ((1<<0)|(1<<1)|(1<<3)|(1<<5)|(1<<6))
+#define CHAR_MASK_UNRESERVED_PATH ((1<<0)|(1<<5))
+
+static unsigned const char _uri_char_lookup[256] = {
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 00
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 10
+ 0, 2, 0, 4, 2, 0, 2, 2, 2, 2, 2, 2, 2, 1, 1, 36, // 20
+ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 28, 2, 0, 2, 0, 68, // 30
+ 12, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 40
+ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 4, 0, 4, 0, 1, // 50
+ 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 60
+ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 1, 0, // 70
+};
+
+static inline int _decode_hex_digit(const unsigned char digit)
+{
+ switch (digit) {
+ case '0': case '1': case '2': case '3': case '4':
+ case '5': case '6': case '7': case '8': case '9':
+ return digit - '0';
+
+ case 'a': case 'b': case 'c': case 'd': case 'e': case 'f':
+ return digit - 'a' + 0x0a;
+
+ case 'A': case 'B': case 'C': case 'D': case 'E': case 'F':
+ return digit - 'A' + 0x0A;
+ }
+ return -1;
+}
+
+static int
+uri_parse_pct_encoded_data(struct uri_parser *parser,
+ const unsigned char **p, const unsigned char *pend,
+ unsigned char *ch_r) ATTR_NULL(3)
+{
+ int value;
+
+ if (**p != '%' || (pend != NULL && *p >= pend))
+ return 0;
+ *p += 1;
+
+ if (**p == 0 || *(*p+1) == 0 || (pend != NULL && *p+1 >= pend)) {
+ parser->error = "Unexpected URI boundary after '%'";
+ return -1;
+ }
+
+ if ((value = _decode_hex_digit(**p)) < 0) {
+ parser->error = p_strdup_printf(parser->pool,
+ "Expecting hex digit after '%%', but found '%c'", **p);
+ return -1;
+ }
+
+ *ch_r = (value & 0x0f) << 4;
+ *p += 1;
+
+ if ((value = _decode_hex_digit(**p)) < 0) {
+ parser->error = p_strdup_printf(parser->pool,
+ "Expecting hex digit after '%%%c', but found '%c'", *((*p)-1), **p);
+ return -1;
+ }
+
+ *ch_r |= (value & 0x0f);
+ *p += 1;
+
+ if (!parser->allow_pct_nul && *ch_r == '\0') {
+ parser->error =
+ "Percent encoding is not allowed to encode NUL character";
+ return -1;
+ }
+ return 1;
+}
+
+int uri_parse_pct_encoded(struct uri_parser *parser,
+ unsigned char *ch_r)
+{
+ return uri_parse_pct_encoded_data
+ (parser, &parser->cur, parser->end, ch_r);
+}
+
+static int
+uri_parse_unreserved_char(struct uri_parser *parser, unsigned char *ch_r)
+{
+ if ((*parser->cur & 0x80) != 0)
+ return 0;
+
+ if ((_uri_char_lookup[*parser->cur] & CHAR_MASK_UNRESERVED) != 0) {
+ *ch_r = *parser->cur;
+ parser->cur++;
+ return 1;
+ }
+ return 0;
+}
+
+int uri_parse_unreserved(struct uri_parser *parser, string_t *part)
+{
+ int len = 0;
+
+ while (parser->cur < parser->end) {
+ int ret;
+ unsigned char ch = 0;
+
+ if ((ret = uri_parse_unreserved_char(parser, &ch)) < 0)
+ return -1;
+ if (ret == 0)
+ break;
+
+ if (part != NULL)
+ str_append_c(part, ch);
+ len++;
+ }
+
+ return len > 0 ? 1 : 0;
+}
+
+int uri_parse_unreserved_pct(struct uri_parser *parser, string_t *part)
+{
+ int len = 0;
+
+ while (parser->cur < parser->end) {
+ int ret;
+ unsigned char ch = 0;
+
+ if ((ret=uri_parse_pct_encoded(parser, &ch)) < 0)
+ return -1;
+ else if (ret == 0 &&
+ (ret=uri_parse_unreserved_char(parser, &ch)) < 0)
+ return -1;
+ if (ret == 0)
+ break;
+
+ if (part != NULL)
+ str_append_c(part, ch);
+ len++;
+ }
+
+ return len > 0 ? 1 : 0;
+}
+
+bool uri_data_decode(struct uri_parser *parser, const char *data,
+ const char *until, const char **decoded_r)
+{
+ const unsigned char *p = (const unsigned char *)data;
+ const unsigned char *pend = (const unsigned char *)until;
+ string_t *decoded;
+ int ret;
+
+ if (pend == NULL) {
+ /* NULL means unlimited; solely rely on '\0' */
+ pend = (const unsigned char *)SIZE_MAX;
+ }
+
+ if (p >= pend || *p == '\0') {
+ if (decoded_r != NULL)
+ *decoded_r = "";
+ return TRUE;
+ }
+
+ decoded = uri_parser_get_tmpbuf(parser, 256);
+ while (p < pend && *p != '\0') {
+ unsigned char ch;
+
+ if ((ret=uri_parse_pct_encoded_data
+ (parser, &p, NULL, &ch)) != 0) {
+ if (ret < 0)
+ return FALSE;
+ str_append_c(decoded, ch);
+ } else {
+ str_append_c(decoded, *p);
+ p++;
+ }
+ }
+
+ if (decoded_r != NULL)
+ *decoded_r = p_strdup(parser->pool, str_c(decoded));
+ return TRUE;
+}
+
+int uri_parse_scheme(struct uri_parser *parser, const char **scheme_r)
+{
+ const unsigned char *first = parser->cur;
+ size_t len = 1;
+
+ /* RFC 3968:
+ * scheme = ALPHA *( ALPHA / DIGIT / "+" / "-" / "." )
+ */
+
+ if (parser->cur >= parser->end || !i_isalpha(*parser->cur))
+ return 0;
+ parser->cur++;
+
+ while (len < URI_MAX_SCHEME_NAME_LEN &&
+ parser->cur < parser->end) {
+ if (!i_isalnum(*parser->cur) &&
+ *parser->cur != '+' && *parser->cur != '-' &&
+ *parser->cur != '.')
+ break;
+ parser->cur++;
+ len++;
+ }
+
+ if (parser->cur >= parser->end || *parser->cur != ':') {
+ parser->error = "Invalid URI scheme";
+ return -1;
+ }
+ if (scheme_r != NULL)
+ *scheme_r = t_strndup(first, parser->cur - first);
+ parser->cur++;
+ return 1;
+}
+
+int uri_cut_scheme(const char **uri_p, const char **scheme_r)
+{
+ struct uri_parser parser;
+
+ uri_parser_init(&parser, NULL, *uri_p);
+ if (uri_parse_scheme(&parser, scheme_r) <= 0)
+ return -1;
+ *uri_p = (const char *)parser.cur;
+ return 0;
+}
+
+static int
+uri_parse_dec_octet(struct uri_parser *parser, string_t *literal,
+ uint8_t *octet_r) ATTR_NULL(2)
+{
+ unsigned int octet = 0;
+ int count = 0;
+
+ /* RFC 3986:
+ *
+ * dec-octet = DIGIT ; 0-9
+ * / %x31-39 DIGIT ; 10-99
+ * / "1" 2DIGIT ; 100-199
+ * / "2" %x30-34 DIGIT ; 200-249
+ * / "25" %x30-35 ; 250-255
+ */
+
+ while (parser->cur < parser->end && i_isdigit(*parser->cur)) {
+ octet = octet * 10 + (parser->cur[0] - '0');
+ if (octet > 255)
+ return -1;
+
+ if (literal != NULL)
+ str_append_c(literal, *parser->cur);
+
+ parser->cur++;
+ count++;
+ }
+
+ if (count > 0) {
+ *octet_r = octet;
+ return 1;
+ }
+ return 0;
+}
+
+static int
+uri_parse_ipv4address(struct uri_parser *parser, string_t *literal,
+ struct in_addr *ip4_r) ATTR_NULL(2,3)
+{
+ uint8_t octet;
+ uint32_t ip = 0;
+ int ret;
+ int i;
+
+ /* RFC 3986:
+ *
+ * IPv4address = dec-octet "." dec-octet "." dec-octet "." dec-octet
+ */
+
+ if ((ret = uri_parse_dec_octet(parser, literal, &octet)) <= 0)
+ return ret;
+ ip = octet;
+
+ for (i = 0; i < 3 && parser->cur < parser->end; i++) {
+ if (*parser->cur != '.')
+ return -1;
+
+ if (literal != NULL)
+ str_append_c(literal, '.');
+ parser->cur++;
+
+ if (uri_parse_dec_octet(parser, literal, &octet) <= 0)
+ return -1;
+ ip = (ip << 8) + octet;
+ }
+
+ if (ip4_r != NULL)
+ ip4_r->s_addr = htonl(ip);
+ return 1;
+}
+
+static int
+uri_do_parse_reg_name(struct uri_parser *parser,
+ string_t *reg_name) ATTR_NULL(2)
+{
+ /* RFC 3986:
+ *
+ * reg-name = *( unreserved / pct-encoded / sub-delims )
+ */
+
+ while (parser->cur < parser->end) {
+ int ret;
+ unsigned char c;
+
+ /* unreserved / pct-encoded */
+ if ((ret=uri_parse_pct_encoded(parser, &c)) < 0)
+ return -1;
+ else if (ret == 0 &&
+ (ret=uri_parse_unreserved_char(parser, &c)) < 0)
+ return -1;
+
+ if (ret > 0) {
+ if (reg_name != NULL)
+ str_append_c(reg_name, c);
+ continue;
+ }
+
+ /* sub-delims */
+ c = *parser->cur;
+ if ((c & 0x80) == 0 && (_uri_char_lookup[c] & CHAR_MASK_SUB_DELIMS) != 0) {
+ if (reg_name != NULL)
+ str_append_c(reg_name, *parser->cur);
+ parser->cur++;
+ continue;
+ }
+ break;
+ }
+ return 0;
+}
+
+int uri_parse_reg_name(struct uri_parser *parser,
+ const char **reg_name_r)
+{
+ string_t *reg_name = NULL;
+ int ret;
+
+ if (reg_name_r != NULL)
+ reg_name = uri_parser_get_tmpbuf(parser, 256);
+
+ if ((ret=uri_do_parse_reg_name(parser, reg_name)) <= 0)
+ return ret;
+
+ if (reg_name_r != NULL)
+ *reg_name_r = str_c(reg_name);
+ return 1;
+}
+
+static int uri_do_parse_host_name(struct uri_parser *parser,
+ string_t *host_name) ATTR_NULL(2)
+{
+ const unsigned char *first, *part;
+ int ret;
+
+ /* RFC 3986, Section 3.2.2:
+
+ A registered name intended for lookup in the DNS uses the syntax
+ defined in Section 3.5 of [RFC1034] and Section 2.1 of [RFC1123].
+ Such a name consists of a sequence of domain labels separated by ".",
+ each domain label starting and ending with an alphanumeric character
+ and possibly also containing "-" characters. The rightmost domain
+ label of a fully qualified domain name in DNS may be followed by a
+ single "." and should be if it is necessary to distinguish between
+ the complete domain name and some local domain.
+
+ RFC 2396, Section 3.2.2 (old URI specification):
+
+ hostname = *( domainlabel "." ) toplabel [ "." ]
+ domainlabel = alphanum | alphanum *( alphanum | "-" ) alphanum
+ toplabel = alpha | alpha *( alphanum | "-" ) alphanum
+
+ The description in RFC 3986 is more liberal, so:
+
+ hostname = *( domainlabel "." ) domainlabel [ "." ]
+ domainlabel = alphanum | alphanum *( alphanum | "-" ) alphanum
+
+ We also support percent encoding in spirit of the generic reg-name,
+ even though this should explicitly not be used according to the RFC.
+ It is, however, not strictly forbidden (unlike older RFC), so we
+ support it.
+ */
+
+ first = part = parser->cur;
+ for (;;) {
+ const unsigned char *offset;
+ unsigned char ch, pch;
+
+ /* alphanum */
+ offset = parser->cur;
+ ch = pch = *parser->cur;
+ if (parser->cur >= parser->end)
+ break;
+ if ((ret=uri_parse_pct_encoded(parser, &ch)) < 0) {
+ return -1;
+ } else if (ret > 0) {
+ if (!i_isalnum(ch))
+ return -1;
+ if (host_name != NULL)
+ str_append_c(host_name, ch);
+ part = parser->cur;
+ } else {
+ if (!i_isalnum(*parser->cur))
+ break;
+ parser->cur++;
+ }
+
+ if (parser->cur < parser->end) {
+ /* *( alphanum | "-" ) alphanum */
+ do {
+ offset = parser->cur;
+
+ if ((ret=uri_parse_pct_encoded(parser, &ch)) < 0) {
+ return -1;
+ } else if (ret > 0) {
+ if (!i_isalnum(ch) && ch != '-')
+ break;
+ if (host_name != NULL) {
+ if (offset > part)
+ str_append_data(host_name, part, offset - part);
+ str_append_c(host_name, ch);
+ }
+ part = parser->cur;
+ } else {
+ ch = *parser->cur;
+ if (!i_isalnum(ch) && ch != '-')
+ break;
+ parser->cur++;
+ }
+ pch = ch;
+ } while (parser->cur < parser->end);
+
+ if (!i_isalnum(pch)) {
+ parser->error = "Invalid domain label in hostname";
+ return -1;
+ }
+ }
+
+ if (host_name != NULL && parser->cur > part)
+ str_append_data(host_name, part, parser->cur - part);
+
+ /* "." */
+ if (parser->cur >= parser->end || ch != '.')
+ break;
+ if (host_name != NULL)
+ str_append_c(host_name, '.');
+ if (parser->cur == offset)
+ parser->cur++;
+ part = parser->cur;
+ }
+
+ if (parser->cur == first)
+ return 0;
+
+ /* remove trailing '.' */
+ if (host_name != NULL) {
+ const char *name = str_c(host_name);
+
+ i_assert(str_len(host_name) > 0);
+ if (name[str_len(host_name)-1] == '.')
+ str_truncate(host_name, str_len(host_name)-1);
+ }
+ return 1;
+}
+
+int uri_parse_host_name(struct uri_parser *parser,
+ const char **host_name_r)
+{
+ string_t *host_name = NULL;
+ int ret;
+
+ if (host_name_r != NULL)
+ host_name = uri_parser_get_tmpbuf(parser, 256);
+
+ if ((ret=uri_do_parse_host_name(parser, host_name)) <= 0)
+ return ret;
+
+ if (host_name_r != NULL)
+ *host_name_r = str_c(host_name);
+ return 1;
+}
+
+static int
+uri_parse_ip_literal(struct uri_parser *parser, string_t *literal,
+ struct in6_addr *ip6_r) ATTR_NULL(2,3)
+{
+ const unsigned char *p;
+ const char *address;
+ struct in6_addr ip6;
+
+ /* IP-literal = "[" ( IPv6address / IPvFuture ) "]"
+ * IPvFuture = "v" 1*HEXDIG "." 1*( unreserved / sub-delims / ":" )
+ * IPv6address = ; Syntax not relevant: parsed using inet_pton()
+ */
+
+ /* "[" already verified */
+
+ /* Scan for end of address */
+ for (p = parser->cur+1; p < parser->end; p++) {
+ if (*p == ']')
+ break;
+ }
+
+ if (p >= parser->end || *p != ']') {
+ parser->error = "Expecting ']' at end of IP-literal";
+ return -1;
+ }
+
+ if (literal != NULL)
+ str_append_data(literal, parser->cur, p-parser->cur+1);
+ address = t_strdup_until(parser->cur+1, p);
+ parser->cur = p + 1;
+
+ if (*address == '\0') {
+ parser->error = "Empty IPv6 host address";
+ return -1;
+ }
+ if (*address == 'v') {
+ parser->error = p_strdup_printf(parser->pool,
+ "Future IP host address '%s' not supported", address);
+ return -1;
+ }
+ if (inet_pton(AF_INET6, address, &ip6) <= 0) {
+ parser->error = p_strdup_printf(parser->pool,
+ "Invalid IPv6 host address '%s'", address);
+ return -1;
+ }
+ if (ip6_r != NULL)
+ *ip6_r = ip6;
+ return 1;
+}
+
+static int
+uri_do_parse_host(struct uri_parser *parser,
+ struct uri_host *host, bool host_name)
+ ATTR_NULL(2)
+{
+ const unsigned char *preserve;
+ struct in_addr ip4;
+ struct in6_addr ip6;
+ string_t *literal = NULL;
+ int ret;
+
+ /* RFC 3986:
+ *
+ * host = IP-literal / IPv4address / reg-name
+ */
+
+ if (host != NULL)
+ i_zero(host);
+
+ literal = uri_parser_get_tmpbuf(parser, 256);
+
+ /* IP-literal / */
+ if (parser->cur < parser->end && *parser->cur == '[') {
+ if (uri_parse_ip_literal(parser, literal, &ip6) <= 0)
+ return -1;
+
+ if (host != NULL) {
+ host->name = p_strdup(parser->pool, str_c(literal));;
+ host->ip.family = AF_INET6;
+ host->ip.u.ip6 = ip6;
+ }
+ return 1;
+ }
+
+ /* IPv4address /
+ *
+ * If it fails to parse, we try to parse it as a reg-name
+ */
+ preserve = parser->cur;
+ if ((ret = uri_parse_ipv4address(parser, literal, &ip4)) > 0) {
+ if (host != NULL) {
+ host->name = p_strdup(parser->pool, str_c(literal));
+ host->ip.family = AF_INET;
+ host->ip.u.ip4 = ip4;
+ }
+ return ret;
+ }
+ parser->cur = preserve;
+ str_truncate(literal, 0);
+
+ /* reg-name */
+ if (host_name) {
+ if (uri_do_parse_host_name(parser, literal) < 0)
+ return -1;
+ } else if (uri_do_parse_reg_name(parser, literal) < 0)
+ return -1;
+ if (host != NULL)
+ host->name = p_strdup(parser->pool, str_c(literal));
+ return 0;
+}
+
+int uri_parse_host(struct uri_parser *parser,
+ struct uri_host *host)
+{
+ return uri_do_parse_host(parser, host, TRUE);
+}
+
+static int
+uri_parse_port(struct uri_parser *parser,
+ struct uri_authority *auth) ATTR_NULL(2)
+{
+ const unsigned char *first;
+ in_port_t port;
+
+ /* RFC 3986:
+ *
+ * port = *DIGIT
+ */
+
+ first = parser->cur;
+ while (parser->cur < parser->end && i_isdigit(*parser->cur))
+ parser->cur++;
+
+ if (parser->cur == first)
+ return 0;
+ if (net_str2port(t_strdup_until(first, parser->cur), &port) < 0) {
+ parser->error = "Invalid port number";
+ return -1;
+ }
+
+ if (auth != NULL)
+ auth->port = port;
+ return 1;
+}
+
+static int
+uri_do_parse_authority(struct uri_parser *parser,
+ struct uri_authority *auth, bool host_name) ATTR_NULL(2)
+{
+ const unsigned char *p;
+ int ret;
+
+ /*
+ * authority = [ userinfo "@" ] host [ ":" port ]
+ */
+
+ if (auth != NULL)
+ i_zero(auth);
+
+ /* Scan ahead to check whether there is a [userinfo "@"] uri component */
+ for (p = parser->cur; p < parser->end; p++){
+ /* refuse 8bit characters */
+ if ((*p & 0x80) != 0)
+ break;
+
+ /* break at first delimiter */
+ if (*p != '%' && (_uri_char_lookup[*p] & CHAR_MASK_UCHAR) == 0)
+ break;
+ }
+
+ /* Extract userinfo */
+ if (p < parser->end && *p == '@') {
+ if (auth != NULL)
+ auth->enc_userinfo = p_strdup_until(parser->pool, parser->cur, p);
+ parser->cur = p+1;
+ }
+
+ /* host */
+ if (uri_do_parse_host(parser,
+ (auth == NULL ? NULL : &auth->host), host_name) < 0)
+ return -1;
+ if (parser->cur == parser->end)
+ return 1;
+ switch (*parser->cur) {
+ case ':': case '/': case '?': case '#':
+ break;
+ default:
+ parser->error = "Invalid host identifier";
+ return -1;
+ }
+
+ /* [":" port] */
+ if (*parser->cur == ':') {
+ parser->cur++;
+
+ if ((ret = uri_parse_port(parser, auth)) < 0)
+ return ret;
+ if (parser->cur == parser->end)
+ return 1;
+ switch (*parser->cur) {
+ case '/': case '?': case '#':
+ break;
+ default:
+ parser->error = "Invalid host port";
+ return -1;
+ }
+ }
+
+ return 1;
+}
+
+static int
+uri_do_parse_slashslash_authority(struct uri_parser *parser,
+ struct uri_authority *auth, bool host_name)
+ ATTR_NULL(2)
+{
+ /* "//" authority */
+
+ if ((parser->end - parser->cur) <= 2 || parser->cur[0] != '/' ||
+ parser->cur[1] != '/')
+ return 0;
+
+ parser->cur += 2;
+ return uri_do_parse_authority(parser, auth, host_name);
+}
+
+int uri_parse_authority(struct uri_parser *parser,
+ struct uri_authority *auth)
+{
+ return uri_do_parse_authority(parser, auth, FALSE);
+}
+
+int uri_parse_slashslash_authority(struct uri_parser *parser,
+ struct uri_authority *auth)
+{
+ return uri_do_parse_slashslash_authority(parser, auth, FALSE);
+}
+
+int uri_parse_host_authority(struct uri_parser *parser,
+ struct uri_authority *auth)
+{
+ return uri_do_parse_authority(parser, auth, TRUE);
+}
+
+int uri_parse_slashslash_host_authority(struct uri_parser *parser,
+ struct uri_authority *auth)
+{
+ return uri_do_parse_slashslash_authority(parser, auth, TRUE);
+}
+
+int uri_parse_path_segment(struct uri_parser *parser, const char **segment_r)
+{
+ const unsigned char *first = parser->cur;
+ int ret;
+
+ while (parser->cur < parser->end) {
+ if (*parser->cur == '%') {
+ unsigned char ch = 0;
+ if ((ret=uri_parse_pct_encoded(parser, &ch)) < 0)
+ return -1;
+ if (ret > 0)
+ continue;
+ }
+
+ if ((*parser->cur & 0x80) != 0 ||
+ (_uri_char_lookup[*parser->cur] & CHAR_MASK_PCHAR) == 0)
+ break;
+
+ parser->cur++;
+ }
+
+ if (parser->cur < parser->end &&
+ *parser->cur != '/' && *parser->cur != '?' && *parser->cur != '#' ) {
+ parser->error =
+ "Path component contains invalid character";
+ return -1;
+ }
+
+ if (first == parser->cur)
+ return 0;
+
+ if (segment_r != NULL)
+ *segment_r = p_strdup_until(parser->pool, first, parser->cur);
+ return 1;
+}
+
+int uri_parse_path(struct uri_parser *parser,
+ int *relative_r, const char *const **path_r)
+{
+ const unsigned char *pbegin = parser->cur;
+ ARRAY_TYPE(const_string) segments;
+ const char *segment = NULL;
+ unsigned int count;
+ int relative = 1;
+ int ret;
+
+ count = 0;
+ if (path_r != NULL)
+ p_array_init(&segments, parser->pool, 16);
+ else
+ i_zero(&segments);
+
+ /* check for a leading '/' and indicate absolute path
+ when it is present
+ */
+ if (parser->cur < parser->end && *parser->cur == '/') {
+ parser->cur++;
+ relative = 0;
+ }
+
+ /* parse first segment */
+ if ((ret = uri_parse_path_segment(parser, &segment)) < 0)
+ return -1;
+
+ for (;;) {
+ if (ret > 0) {
+ /* strip dot segments */
+ if (segment[0] == '.') {
+ if (segment[1] == '.') {
+ if (segment[2] == '\0') {
+ /* '..' -> skip and... */
+ segment = NULL;
+
+ /* ... pop last segment (if any) */
+ if (count > 0) {
+ if (path_r != NULL) {
+ i_assert(count == array_count(&segments));
+ array_delete(&segments, count-1, 1);
+ }
+ count--;
+ } else if ( relative > 0 ) {
+ relative++;
+ }
+ }
+ } else if (segment[1] == '\0') {
+ /* '.' -> skip */
+ segment = NULL;
+ }
+ }
+ } else {
+ segment = "";
+ }
+
+ if (segment != NULL) {
+ if (path_r != NULL)
+ array_push_back(&segments, &segment);
+ count++;
+ }
+
+ if (parser->cur >= parser->end || *parser->cur != '/')
+ break;
+ parser->cur++;
+
+ /* parse next path segment */
+ if ((ret = uri_parse_path_segment(parser, &segment)) < 0)
+ return -1;
+ }
+
+ if (relative_r != NULL)
+ *relative_r = relative;
+ if (path_r != NULL)
+ *path_r = NULL;
+
+ if (parser->cur == pbegin) {
+ /* path part of URI is empty */
+ return 0;
+ }
+
+ if (path_r != NULL) {
+ /* special treatment for a trailing '..' or '.' */
+ if (segment == NULL) {
+ segment = "";
+ array_push_back(&segments, &segment);
+ }
+ array_append_zero(&segments);
+ *path_r = array_get(&segments, &count);
+ }
+ if (parser->cur < parser->end &&
+ *parser->cur != '?' && *parser->cur != '#') {
+ parser->error = "Path component contains invalid character";
+ return -1;
+ }
+ return 1;
+}
+
+int uri_parse_query(struct uri_parser *parser, const char **query_r)
+{
+ const unsigned char *first = parser->cur;
+ int ret;
+
+ /* RFC 3986:
+ *
+ * URI = { ... } [ "?" query ] { ... }
+ * query = *( pchar / "/" / "?" )
+ * pchar = unreserved / pct-encoded / sub-delims / ":" / "@"
+ */
+ if (parser->cur >= parser->end || *parser->cur != '?')
+ return 0;
+ parser->cur++;
+
+ while (parser->cur < parser->end) {
+ if (*parser->cur == '%') {
+ unsigned char ch = 0;
+ if ((ret=uri_parse_pct_encoded(parser, &ch)) < 0)
+ return -1;
+ if (ret > 0)
+ continue;
+ }
+
+ if ((*parser->cur & 0x80) != 0 ||
+ (_uri_char_lookup[*parser->cur] & CHAR_MASK_QCHAR) == 0)
+ break;
+ parser->cur++;
+ }
+
+ if (parser->cur < parser->end && *parser->cur != '#') {
+ parser->error = "Query component contains invalid character";
+ return -1;
+ }
+
+ if (query_r != NULL)
+ *query_r = p_strdup_until(parser->pool, first+1, parser->cur);
+ return 1;
+}
+
+int uri_parse_fragment(struct uri_parser *parser, const char **fragment_r)
+{
+ const unsigned char *first = parser->cur;
+ int ret;
+
+ /* RFC 3986:
+ *
+ * URI = { ... } [ "#" fragment ]
+ * fragment = *( pchar / "/" / "?" )
+ * pchar = unreserved / pct-encoded / sub-delims / ":" / "@"
+ */
+
+ if (parser->cur >= parser->end || *parser->cur != '#')
+ return 0;
+ parser->cur++;
+
+ while (parser->cur < parser->end) {
+ if (*parser->cur == '%') {
+ unsigned char ch = 0;
+ if ((ret=uri_parse_pct_encoded(parser, &ch)) < 0)
+ return -1;
+ if (ret > 0)
+ continue;
+ }
+
+ if ((*parser->cur & 0x80) != 0 ||
+ (_uri_char_lookup[*parser->cur] & CHAR_MASK_QCHAR) == 0)
+ break;
+ parser->cur++;
+ }
+
+ if (parser->cur < parser->end) {
+ parser->error = "Fragment component contains invalid character";
+ return -1;
+ }
+
+ if (fragment_r != NULL)
+ *fragment_r = p_strdup_until(parser->pool, first+1, parser->cur);
+ return 1;
+}
+
+void uri_parser_init_data(struct uri_parser *parser,
+ pool_t pool, const unsigned char *data, size_t size)
+{
+ i_zero(parser);
+ parser->pool = pool;
+ parser->begin = parser->cur = data;
+ parser->end = data + size;
+}
+
+void uri_parser_init(struct uri_parser *parser,
+ pool_t pool, const char *uri)
+{
+ uri_parser_init_data
+ (parser, pool, (const unsigned char *)uri, strlen(uri));
+}
+
+string_t *uri_parser_get_tmpbuf(struct uri_parser *parser, size_t size)
+{
+ if (parser->tmpbuf == NULL)
+ parser->tmpbuf = str_new(parser->pool, size);
+ else
+ str_truncate(parser->tmpbuf, 0);
+ return parser->tmpbuf;
+}
+
+int uri_parse_absolute_generic(struct uri_parser *parser,
+ enum uri_parse_flags flags)
+{
+ int relative, aret, ret = 0;
+
+ /*
+ URI = scheme ":" hier-part [ "?" query ] [ "#" fragment ]
+
+ hier-part = "//" authority path-abempty
+ / path-absolute
+ / path-rootless
+ / path-empty
+ path-abempty = *( "/" segment )
+ path-absolute = "/" [ segment-nz *( "/" segment ) ]
+ path-rootless = segment-nz *( "/" segment )
+ path-empty = 0<pchar>
+
+ segment = *pchar
+ segment-nz = 1*pchar
+ */
+
+ /* scheme ":" */
+ if ((flags & URI_PARSE_SCHEME_EXTERNAL) == 0 &&
+ (ret=uri_parse_scheme(parser, NULL)) <= 0) {
+ if (ret == 0)
+ parser->error = "Missing scheme";
+ return -1;
+ }
+
+ /* "//" authority */
+ if ((aret=uri_parse_slashslash_authority
+ (parser, NULL)) < 0)
+ return -1;
+
+ /* path-absolute / path-rootless / path-empty */
+ if (aret == 0) {
+ ret = uri_parse_path(parser, &relative, NULL);
+ /* path-abempty */
+ } else if (parser->cur < parser->end && *parser->cur == '/') {
+ ret = uri_parse_path(parser, &relative, NULL);
+ i_assert(ret <= 0 || relative == 0);
+ }
+ if (ret < 0)
+ return -1;
+
+ /* [ "?" query ] */
+ if (uri_parse_query(parser, NULL) < 0)
+ return -1;
+
+ /* [ "#" fragment ] */
+ if ((ret=uri_parse_fragment(parser, NULL)) < 0)
+ return ret;
+ if (ret > 0 && (flags & URI_PARSE_ALLOW_FRAGMENT_PART) == 0) {
+ parser->error = "Fragment part not allowed";
+ return -1;
+ }
+
+ i_assert(parser->cur == parser->end);
+ return 0;
+}
+
+/*
+ * Generic URI manipulation
+ */
+
+void uri_host_copy(pool_t pool, struct uri_host *dest,
+ const struct uri_host *src)
+{
+ const char *host_name = src->name;
+
+ /* create host name literal if caller is lazy */
+ if (host_name == NULL && src->ip.family != 0) {
+ host_name = net_ip2addr(&src->ip);
+ i_assert(*host_name != '\0');
+ }
+
+ *dest = *src;
+ dest->name = p_strdup(pool, host_name);
+}
+
+/*
+ * Check generic URI
+ */
+
+int uri_check_data(const unsigned char *data, size_t size,
+ enum uri_parse_flags flags, const char **error_r)
+{
+ struct uri_parser parser;
+ int ret;
+
+ i_zero(&parser);
+ parser.pool = pool_datastack_create();
+ parser.begin = parser.cur = data;
+ parser.end = data + size;
+
+ ret = uri_parse_absolute_generic(&parser, flags);
+ *error_r = parser.error;
+ return ret;
+}
+
+int uri_check(const char *uri, enum uri_parse_flags flags,
+ const char **error_r)
+{
+ return uri_check_data
+ ((const unsigned char *)uri, strlen(uri), flags, error_r);
+}
+
+/*
+ * Generic URI construction
+ */
+
+void uri_data_encode(string_t *out,
+ const unsigned char esc_table[256],
+ unsigned char esc_mask, const char *esc_extra,
+ const char *data)
+{
+ const unsigned char *pbegin, *p;
+
+ pbegin = p = (const unsigned char *)data;
+ while (*p != '\0') {
+ if ((*p & 0x80) != 0 || (esc_table[*p] & esc_mask) == 0 ||
+ (esc_extra != NULL && strchr(esc_extra, (char)*p) != NULL)) {
+ if ((p - pbegin) > 0)
+ str_append_data(out, pbegin, p - pbegin);
+ str_printfa(out, "%%%02x", *p);
+ p++;
+ pbegin = p;
+ } else {
+ p++;
+ }
+ }
+ if ((p - pbegin) > 0)
+ str_append_data(out, pbegin, p - pbegin);
+}
+
+void uri_append_scheme(string_t *out, const char *scheme)
+{
+ str_append(out, scheme);
+ str_append_c(out, ':');
+}
+
+void uri_append_user_data(string_t *out, const char *esc,
+ const char *data)
+{
+ uri_data_encode(out, _uri_char_lookup, CHAR_MASK_UCHAR, esc, data);
+}
+
+void uri_append_userinfo(string_t *out, const char *userinfo)
+{
+ uri_append_user_data(out, NULL, userinfo);
+ str_append_c(out, '@');
+}
+
+void uri_append_host_name(string_t *out, const char *name)
+{
+ uri_data_encode(out, _uri_char_lookup,
+ CHAR_MASK_UNRESERVED | CHAR_MASK_SUB_DELIMS, NULL, name);
+}
+
+void uri_append_host_ip(string_t *out, const struct ip_addr *host_ip)
+{
+ const char *addr = net_ip2addr(host_ip);
+
+ i_assert(host_ip->family != 0);
+
+ if (host_ip->family == AF_INET) {
+ str_append(out, addr);
+ return;
+ }
+
+ i_assert(host_ip->family == AF_INET6);
+ str_append_c(out, '[');
+ str_append(out, addr);
+ str_append_c(out, ']');
+}
+
+void uri_append_host(string_t *out, const struct uri_host *host)
+{
+ if (host->name != NULL) {
+ /* assume IPv6 literal if starts with '['; avoid encoding */
+ if (*host->name == '[')
+ str_append(out, host->name);
+ else
+ uri_append_host_name(out, host->name);
+ } else
+ uri_append_host_ip(out, &host->ip);
+}
+
+void uri_append_port(string_t *out, in_port_t port)
+{
+ if (port != 0)
+ str_printfa(out, ":%u", port);
+}
+
+void uri_append_path_segment_data(string_t *out, const char *esc,
+ const char *data)
+{
+ uri_data_encode(out, _uri_char_lookup, CHAR_MASK_PCHAR, esc, data);
+}
+
+void uri_append_path_segment(string_t *out, const char *segment)
+{
+ str_append_c(out, '/');
+ if (*segment != '\0')
+ uri_append_path_data(out, NULL, segment);
+}
+
+void uri_append_path_data(string_t *out, const char *esc,
+ const char *data)
+{
+ uri_data_encode(out, _uri_char_lookup, CHAR_MASK_PFCHAR, esc, data);
+}
+
+void uri_append_path(string_t *out, const char *path)
+{
+ str_append_c(out, '/');
+ if (*path != '\0')
+ uri_append_path_data(out, NULL, path);
+}
+
+void uri_append_query_data(string_t *out, const char *esc,
+ const char *data)
+{
+ uri_data_encode(out, _uri_char_lookup, CHAR_MASK_QCHAR, esc, data);
+}
+
+void uri_append_query(string_t *out, const char *query)
+{
+ str_append_c(out, '?');
+ if (*query != '\0')
+ uri_append_query_data(out, NULL, query);
+}
+
+void uri_append_fragment_data(string_t *out, const char *esc,
+ const char *data)
+{
+ uri_data_encode(out, _uri_char_lookup, CHAR_MASK_QCHAR, esc, data);
+}
+
+void uri_append_fragment(string_t *out, const char *fragment)
+{
+ str_append_c(out, '#');
+ if (*fragment != '\0')
+ uri_append_fragment_data(out, NULL, fragment);
+}
+
+void uri_append_unreserved(string_t *out, const char *data)
+{
+ uri_data_encode(out, _uri_char_lookup, CHAR_MASK_UNRESERVED,
+ NULL, data);
+}
+
+void uri_append_unreserved_path(string_t *out, const char *data)
+{
+ uri_data_encode(out, _uri_char_lookup, CHAR_MASK_UNRESERVED_PATH,
+ NULL, data);
+}
diff --git a/src/lib/uri-util.h b/src/lib/uri-util.h
new file mode 100644
index 0000000..837e54c
--- /dev/null
+++ b/src/lib/uri-util.h
@@ -0,0 +1,298 @@
+#ifndef URI_UTIL_H
+#define URI_UTIL_H
+
+#include "net.h"
+
+/*
+ * Generic URI parsing.
+ */
+
+enum uri_parse_flags {
+ /* Scheme part 'scheme:' is already parsed externally. */
+ URI_PARSE_SCHEME_EXTERNAL = BIT(0),
+ /* Allow '#fragment' part in URI */
+ URI_PARSE_ALLOW_FRAGMENT_PART = BIT(1),
+};
+
+struct uri_host {
+ const char *name;
+ struct ip_addr ip;
+};
+
+struct uri_authority {
+ /* encoded userinfo part; e.g. "user:pass" */
+ const char *enc_userinfo;
+
+ struct uri_host host;
+ in_port_t port; /* 0 means no port specified */
+};
+
+struct uri_parser {
+ pool_t pool;
+ const char *error;
+
+ const unsigned char *begin, *cur, *end;
+
+ string_t *tmpbuf;
+
+ bool allow_pct_nul:1;
+};
+
+/* parse one instance of percent encoding. Returns 1 for success,
+ 0 if none is preset at the current parser position, and -1 in
+ case of error. The decoded character is returned in ch_r upon
+ success */
+int uri_parse_pct_encoded(struct uri_parser *parser,
+ unsigned char *ch_r);
+
+/* parse characters as long as these comply with the the 'unreserved'
+ syntax. Returns 1 if characters were found, 0 if none were found,
+ and -1 if there was an error */
+int uri_parse_unreserved(struct uri_parser *parser, string_t *part);
+/* the same as uri_parse_unreserved(), but the allowed characters are
+ extended to 'unreserved / pct-encoded', meaning that percent encoding
+ is allowed */
+int uri_parse_unreserved_pct(struct uri_parser *parser, string_t *part);
+
+/* decode percent-encoded data from the 'data' parameter, up until the
+ 'until' parameter. If the latter is NULL, data is decoded up until the
+ '\0' character. The decoded data is allocated on the parser pool and
+ returned in decoded_r. Any errors are written to the parser object. */
+bool uri_data_decode(struct uri_parser *parser, const char *data,
+ const char *until, const char **decoded_r) ATTR_NULL(3);
+
+/* cut the 'scheme ":"' part from the URI. The uri_p pointer is updated to
+ point just past the ":". Returns 0 on success and -1 on error. The
+ result is returned in the scheme_r parameter. This can be NULL to use
+ this function for merely checking the presence of a valid scheme. */
+int uri_cut_scheme(const char **uri_p, const char **scheme_r)
+ ATTR_NULL(2);
+
+/* parse the URI 'scheme ":"' part. Returns 1 if successful, 0 if the first
+ character is not valid for a scheme, and -1 in case of error. The
+ result parameter scheme_r can be NULL to use this function for merely
+ checking the presence of a valid scheme. */
+int uri_parse_scheme(struct uri_parser *parser, const char **scheme_r)
+ ATTR_NULL(2);
+
+/* parse the URI 'reg-name' syntax. Returns 1 if successful, 0 if the first
+ character is not valid for a host name, and -1 in case of error. The
+ result parameter reg_name_r can be NULL to use this function for merely
+ checking the presence of a valid host name. The result is allocated from
+ the data stack.
+ */
+int uri_parse_reg_name(struct uri_parser *parser,
+ const char **reg_name_r) ATTR_NULL(2);
+/* parse the URI 'reg-name' part as an Internet host name, which is a
+ sequence of domain name labels separated by '.', as defined in
+ Section 3.5 of RFC 1034 and Section 2.1 of RFC 1123. Returns 1 if
+ successful, 0 if the first character is not valid for a host name,
+ and -1 in case of error. The result parameter host_name_r can be NULL
+ to use this function for merely checking the presence of a valid host
+ name. The result is allocated from the data stack.
+ */
+int uri_parse_host_name(struct uri_parser *parser,
+ const char **host_name_r) ATTR_NULL(2);
+/* parse the URI 'host' syntax, which is either an IP address literal or
+ a an Internet host name, as defined in Section 3.5 of RFC 1034 and
+ Section 2.1 of RFC 1123. An IP address literal is always allowed.
+ Returns 1 if successful, 0 if the first character is not valid for a
+ host name, and -1 in case of error. The provided host struct is filled
+ in with the parsed data, all allocated from the parser pool. The host
+ parameter can be NULL to use this function for merely checking for
+ valid 'host' syntax.
+ */
+int uri_parse_host(struct uri_parser *parser,
+ struct uri_host *host) ATTR_NULL(2);
+
+/* parse the URI 'authority' syntax. Returns 1 if successful, 0 if the
+ first character is not valid for the 'authority' syntax and -1 in case
+ of error. The provided uri_authority struct is filled in with the parsed
+ data, all allocated from the parser pool. The auth parameter can be
+ NULL to use this function for merely checking for valid 'authority'
+ syntax.
+ */
+int uri_parse_authority(struct uri_parser *parser,
+ struct uri_authority *auth) ATTR_NULL(2);
+/* identical to uri_parse_authority(), except that this function parses
+ '"//" authority', rather than 'authority'.
+ */
+int uri_parse_slashslash_authority(struct uri_parser *parser,
+ struct uri_authority *auth) ATTR_NULL(2);
+/* identical to uri_parse_authority(), except that this function parses
+ the registered name ('reg-name' syntax) as an Internet host name, as
+ defined in Section 3.5 of RFC 1034 and Section 2.1 of RFC 1123.
+ */
+int uri_parse_host_authority(struct uri_parser *parser,
+ struct uri_authority *auth) ATTR_NULL(2);
+/* identical to uri_parse_slashslash_authority(), except that this
+ function parses the registered name ('reg-name' syntax) as an Internet
+ host name, as defined in Section 3.5 of RFC 1034 and Section 2.1 of
+ RFC 1123.
+ */
+int uri_parse_slashslash_host_authority(struct uri_parser *parser,
+ struct uri_authority *auth) ATTR_NULL(2);
+
+/* parse the URI 'segment' syntax. Returns 1 if successful, 0 if the first
+ character is not valid for the 'segment' syntax and -1 in case of
+ error. The result is allocated from the parser pool. Percent encoding is
+ not decoded in the result. The result parameter can be NULL to use this
+ function for merely checking for valid 'segment' syntax.
+ */
+int uri_parse_path_segment(struct uri_parser *parser,
+ const char **segment_r) ATTR_NULL(2);
+/* parse the URI 'path' syntax. This also resolves '..' and '.' segments in
+ the path. If the path is relative, the relative_r parameter indicates
+ how many segments the base path must be moved towards root (as caused by
+ leading '..' segments). Returns 1 if successful, 0 if the first character
+ is not valid for the 'segment' syntax and -1 in case of error. The result
+ is a NULL-terminated string list allocated from the parser pool. Percent
+ encoding is not decoded in the result. The result parameter can be NULL
+ to use this function for merely checking for valid 'path' syntax.
+ */
+int uri_parse_path(struct uri_parser *parser, int *relative_r,
+ const char *const **path_r) ATTR_NULL(2,3);
+
+/* parse the URI 'query' syntax. Returns 1 if successful, 0 if the first
+ character is not valid for the 'query' syntax and -1 in case of
+ error. The result is allocated from the parser pool. Percent encoding is
+ not decoded in the result. The result parameter can be NULL to use this
+ function for merely checking for valid 'query' syntax.
+ */
+int uri_parse_query(struct uri_parser *parser,
+ const char **query_r) ATTR_NULL(2);
+/* parse the URI 'fragment' syntax. Returns 1 if successful, 0 if the first
+ character is not valid for the 'fragment' syntax and -1 in case of
+ error. The result is allocated from the parser pool. Percent encoding is
+ not decoded in the result. The result parameter can be NULL to use this
+ function for merely checking for valid 'fragment' syntax.
+ */
+int uri_parse_fragment(struct uri_parser *parser,
+ const char **fragment_r) ATTR_NULL(2);
+
+/* initialize the URI parser with the provided data */
+void uri_parser_init_data(struct uri_parser *parser,
+ pool_t pool, const unsigned char *data, size_t size);
+/* initialize the URI parser with the provided '\0'-terminated string */
+void uri_parser_init(struct uri_parser *parser,
+ pool_t pool, const char *uri);
+
+/* returns the temporary buffer associated with this parser. Can be used
+ for higher-level parsing activities. */
+string_t *uri_parser_get_tmpbuf(struct uri_parser *parser,
+ size_t size);
+
+/* Parse a generic (RFC3986) absolute URI for validity.
+ Returns 0 if valid and -1 otherwise. Note that some URI formats like
+ "sip", "aix" and "aaa" violate RFC3986 and will currently fail with
+ this function.
+ */
+int uri_parse_absolute_generic(struct uri_parser *parser,
+ enum uri_parse_flags flags);
+
+/*
+ * Generic URI manipulation
+ */
+
+/* copy uri_host struct from src to dest and allocate it on pool */
+void uri_host_copy(pool_t pool, struct uri_host *dest,
+ const struct uri_host *src);
+
+/*
+ * Generic URI validation
+ */
+
+/* Check whether the provided data is a valid absolute RFC3986 URI.
+ Returns 0 if valid and -1 otherwise. */
+int uri_check_data(const unsigned char *data, size_t size,
+ enum uri_parse_flags flags, const char **error_r);
+/* Check whether the provided string is a valid absolute RFC3986 URI.
+ Returns 0 if valid and -1 otherwise. */
+int uri_check(const char *uri, enum uri_parse_flags,
+ const char **error_r);
+
+/*
+ * Generic URI construction
+ */
+
+/* encodes the '\0'-terminated data using the percent encoding. The
+ esc_table is a 256 byte lookup table. If none of the esc_mask bits are
+ set at the character's position in the esc_table, a character needs
+ to be encoded. Also, when esc_extra contains a character, it needs to
+ be encoded. All other characters are copied verbatim to the out buffer.
+ */
+void uri_data_encode(string_t *out,
+ const unsigned char esc_table[256],
+ unsigned char esc_mask, const char *esc_extra,
+ const char *data) ATTR_NULL(4);
+
+/* append the provided scheme to the out buffer */
+void uri_append_scheme(string_t *out, const char *scheme);
+
+/* append partial user data (i.e. some part of what comes before '@') to
+ the out buffer. No '@' is produced. Characters are percent-encoded when
+ necessary. Characters in esc are always percent-encoded, even when these
+ are valid 'userinfo' characters. */
+void uri_append_user_data(string_t *out,
+ const char *esc, const char *data) ATTR_NULL(2);
+/* append userinfo and '@' to the out buffer. Characters in userinfo are
+ percent-encoded when necessary.*/
+void uri_append_userinfo(string_t *out, const char *userinfo);
+
+/* append the host name to the out buffer. Characters are percent-encoded
+ when necessary.*/
+void uri_append_host_name(string_t *out, const char *name);
+/* append the host IP address to the out buffer. */
+void uri_append_host_ip(string_t *out, const struct ip_addr *host_ip);
+/* encode the URI host struct to the out buffer. */
+void uri_append_host(string_t *out, const struct uri_host *host);
+/* append the port to the out buffer. */
+void uri_append_port(string_t *out, in_port_t port);
+
+/* append partial path segment data to the out buffer. No '/' is produced.
+ Characters are percent-encoded when necessary. Characters in esc are
+ always percent-encoded, even when these are valid 'segment' characters.
+ */
+void uri_append_path_segment_data(string_t *out,
+ const char *esc, const char *data) ATTR_NULL(2);
+/* append a full path segment to the out buffer. A leading '/' is
+ produced. Characters are percent-encoded when necessary. */
+void uri_append_path_segment(string_t *out, const char *segment);
+/* append partial path data to the out buffer. The data may include '/',
+ which is not encoded. Characters are percent-encoded when necessary.
+ Characters in esc are always percent-encoded, even when these are
+ valid 'path' characters.*/
+void uri_append_path_data(string_t *out,
+ const char *esc, const char *data) ATTR_NULL(2);
+/* append a full path to the out buffer. A leading '/' is produced. The
+ data may include more '/', which is not encoded. Characters are
+ percent-encoded when necessary.
+ */
+void uri_append_path(string_t *out, const char *path);
+
+/* append partial query data to the out buffer. No leading '?' is
+ produced. Characters are percent-encoded when necessary. Characters
+ in esc are always percent-encoded, even when these are valid 'query'
+ characters.*/
+void uri_append_query_data(string_t *out,
+ const char *esc, const char *data) ATTR_NULL(2);
+/* append a full URI query part to the out buffer. A leading '?' is
+ produced. Characters are percent-encoded when necessary. */
+void uri_append_query(string_t *out, const char *query);
+
+/* append partial fragment data to the out buffer. No leading '#' is
+ produced. Characters are percent-encoded when necessary. Characters
+ in esc are always percent-encoded, even when these are valid
+ 'fragment' characters.*/
+void uri_append_fragment_data(string_t *out,
+ const char *esc, const char *data) ATTR_NULL(2);
+/* append a full URI fragment part to the out buffer. A leading '#' is
+ produced. Characters are percent-encoded when necessary. */
+void uri_append_fragment(string_t *out, const char *fragment);
+
+/* append data to the out buffer and escape any reserved character */
+void uri_append_unreserved(string_t *out, const char *data);
+/* append data to the out buffer and escape any reserved character except '/' */
+void uri_append_unreserved_path(string_t *out, const char *data);
+
+#endif
diff --git a/src/lib/utc-mktime.c b/src/lib/utc-mktime.c
new file mode 100644
index 0000000..e67c687
--- /dev/null
+++ b/src/lib/utc-mktime.c
@@ -0,0 +1,79 @@
+/* Copyright (c) 2007-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "utc-mktime.h"
+
+static int tm_cmp(const struct tm *tm1, const struct tm *tm2)
+{
+ int diff;
+
+ if ((diff = tm1->tm_year - tm2->tm_year) != 0)
+ return diff;
+ if ((diff = tm1->tm_mon - tm2->tm_mon) != 0)
+ return diff;
+ if ((diff = tm1->tm_mday - tm2->tm_mday) != 0)
+ return diff;
+ if ((diff = tm1->tm_hour - tm2->tm_hour) != 0)
+ return diff;
+ if ((diff = tm1->tm_min - tm2->tm_min) != 0)
+ return diff;
+ return tm1->tm_sec - tm2->tm_sec;
+}
+
+static inline void adjust_leap_second(struct tm *tm)
+{
+ if (tm->tm_sec == 60)
+ tm->tm_sec = 59;
+}
+
+#ifdef HAVE_TIMEGM
+/* Normalization done by timegm is considered a failure here, since it means
+ * the timestamp is not valid as-is. Leap second 60 is adjusted to 59 before
+ * this though. */
+time_t utc_mktime(const struct tm *tm)
+{
+ struct tm leap_adj_tm = *tm;
+ adjust_leap_second(&leap_adj_tm);
+ struct tm tmp = leap_adj_tm;
+ time_t t;
+
+ t = timegm(&tmp);
+ if (tm_cmp(&leap_adj_tm, &tmp) != 0)
+ return (time_t)-1;
+ return t;
+}
+#else
+time_t utc_mktime(const struct tm *tm)
+{
+ struct tm leap_adj_tm = *tm;
+ adjust_leap_second(&leap_adj_tm);
+ const struct tm *try_tm;
+ time_t t;
+ int bits, dir;
+
+ /* we'll do a binary search across the entire valid time_t range.
+ when gmtime()'s output matches the tm parameter, we've found the
+ correct time_t value. this also means that if tm contains invalid
+ values, -1 is returned. */
+#ifdef TIME_T_SIGNED
+ t = 0;
+#else
+ t = (time_t)1 << (TIME_T_MAX_BITS - 1);
+#endif
+ for (bits = TIME_T_MAX_BITS - 2;; bits--) {
+ try_tm = gmtime(&t);
+ dir = tm_cmp(&leap_adj_tm, try_tm);
+ if (dir == 0)
+ return t;
+ if (bits < 0)
+ break;
+
+ if (dir < 0)
+ t -= (time_t)1 << bits;
+ else
+ t += (time_t)1 << bits;
+ }
+
+ return (time_t)-1;
+}
+#endif
diff --git a/src/lib/utc-mktime.h b/src/lib/utc-mktime.h
new file mode 100644
index 0000000..9f32f6e
--- /dev/null
+++ b/src/lib/utc-mktime.h
@@ -0,0 +1,11 @@
+#ifndef UTC_MKTIME_H
+#define UTC_MKTIME_H
+
+#include <time.h>
+
+/* Like mktime(), but assume that tm is in UTC. Unlike mktime(), values in
+ tm fields must be in valid range. Leap second is accepted any time though
+ since utc_mktime is often used before applying the time zone offset. */
+time_t utc_mktime(const struct tm *tm);
+
+#endif
diff --git a/src/lib/utc-offset.c b/src/lib/utc-offset.c
new file mode 100644
index 0000000..d3b4bd2
--- /dev/null
+++ b/src/lib/utc-offset.c
@@ -0,0 +1,38 @@
+/* Copyright (c) 2002-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "utc-offset.h"
+
+#include <sys/time.h>
+
+int utc_offset(struct tm *tm, time_t t ATTR_UNUSED)
+{
+#ifdef HAVE_TM_GMTOFF
+ return (int) (tm->tm_gmtoff/60);
+#else
+ struct tm ltm, gtm;
+ int offset;
+
+ /* gmtime() overwrites tm, so we need to copy it elsewhere */
+ ltm = *tm;
+ tm = gmtime(&t);
+ gtm = *tm;
+
+ /* max offset of 24 hours */
+ if ((ltm.tm_yday < gtm.tm_yday && ltm.tm_year == gtm.tm_year) ||
+ ltm.tm_year < gtm.tm_year)
+ offset = -24 * 60;
+ else if ((ltm.tm_yday > gtm.tm_yday && ltm.tm_year == gtm.tm_year) ||
+ ltm.tm_year > gtm.tm_year)
+ offset = 24 * 60;
+ else
+ offset = 0;
+
+ offset += (ltm.tm_hour - gtm.tm_hour) * 60;
+ offset += (ltm.tm_min - gtm.tm_min);
+
+ /* restore overwritten tm */
+ *tm = ltm;
+ return offset;
+#endif
+}
diff --git a/src/lib/utc-offset.h b/src/lib/utc-offset.h
new file mode 100644
index 0000000..a2196f3
--- /dev/null
+++ b/src/lib/utc-offset.h
@@ -0,0 +1,9 @@
+#ifndef UTC_OFFSET_H
+#define UTC_OFFSET_H
+
+#include <time.h>
+
+/* Returns given time's offset to UTC in minutes. */
+int utc_offset(struct tm *tm, time_t t);
+
+#endif
diff --git a/src/lib/var-expand-if.c b/src/lib/var-expand-if.c
new file mode 100644
index 0000000..aa0a9b3
--- /dev/null
+++ b/src/lib/var-expand-if.c
@@ -0,0 +1,269 @@
+/* Copyright (c) 2003-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "str.h"
+#include "var-expand.h"
+#include "var-expand-private.h"
+#include "wildcard-match.h"
+
+#include <regex.h>
+
+enum var_expand_if_op {
+ OP_UNKNOWN,
+ OP_NUM_EQ,
+ OP_NUM_LT,
+ OP_NUM_LE,
+ OP_NUM_GT,
+ OP_NUM_GE,
+ OP_NUM_NE,
+/* put all numeric comparisons before this line */
+ OP_STR_EQ,
+ OP_STR_LT,
+ OP_STR_LE,
+ OP_STR_GT,
+ OP_STR_GE,
+ OP_STR_NE,
+ OP_STR_LIKE,
+ OP_STR_NOT_LIKE,
+ OP_STR_REGEXP,
+ OP_STR_NOT_REGEXP,
+/* keep this as last */
+ OP_COUNT
+};
+
+static enum var_expand_if_op var_expand_if_str_to_comp(const char *op)
+{
+ const char *ops[OP_COUNT] = {
+ NULL,
+ "==",
+ "<",
+ "<=",
+ ">",
+ ">=",
+ "!=",
+ "eq",
+ "lt",
+ "le",
+ "gt",
+ "ge",
+ "ne",
+ "*",
+ "!*",
+ "~",
+ "!~",
+ };
+ for(enum var_expand_if_op i = 1; i < OP_COUNT; i++) {
+ i_assert(ops[i] != NULL);
+ if (strcmp(op, ops[i]) == 0)
+ return i;
+ }
+ return OP_UNKNOWN;
+}
+
+static int var_expand_if_comp(const char *lhs, const char *_op, const char *rhs,
+ bool *result_r, const char **error_r)
+{
+ bool neg = FALSE;
+ enum var_expand_if_op op = var_expand_if_str_to_comp(_op);
+
+ *result_r = FALSE;
+ if (op == OP_UNKNOWN) {
+ *error_r = t_strdup_printf("if: Unsupported comparator '%s'", _op);
+ return -1;
+ }
+
+ if (op < OP_STR_EQ) {
+ intmax_t a;
+ intmax_t b;
+ if (str_to_intmax(lhs, &a) < 0) {
+ *error_r = t_strdup_printf("if: %s (lhs) is not a number", lhs);
+ return -1;
+ }
+ if (str_to_intmax(rhs, &b) < 0) {
+ *error_r = t_strdup_printf("if: %s (rhs) is not a number", rhs);
+ return -1;
+ }
+ switch(op) {
+ case OP_NUM_EQ:
+ *result_r = a==b;
+ return 0;
+ case OP_NUM_LT:
+ *result_r = a<b;
+ return 0;
+ case OP_NUM_LE:
+ *result_r = a<=b;
+ return 0;
+ case OP_NUM_GT:
+ *result_r = a>b;
+ return 0;
+ case OP_NUM_GE:
+ *result_r = a>=b;
+ return 0;
+ case OP_NUM_NE:
+ *result_r = a!=b;
+ return 0;
+ default:
+ i_panic("Missing numeric comparator %u", op);
+ }
+ }
+
+ switch(op) {
+ case OP_STR_EQ:
+ *result_r = strcmp(lhs,rhs)==0;
+ return 0;
+ case OP_STR_LT:
+ *result_r = strcmp(lhs,rhs)<0;
+ return 0;
+ case OP_STR_LE:
+ *result_r = strcmp(lhs,rhs)<=0;
+ return 0;
+ case OP_STR_GT:
+ *result_r = strcmp(lhs,rhs)>0;
+ return 0;
+ case OP_STR_GE:
+ *result_r = strcmp(lhs,rhs)>=0;
+ return 0;
+ case OP_STR_NE:
+ *result_r = strcmp(lhs,rhs)!=0;
+ return 0;
+ case OP_STR_LIKE:
+ *result_r = wildcard_match(lhs, rhs);
+ return 0;
+ case OP_STR_NOT_LIKE:
+ *result_r = !wildcard_match(lhs, rhs);
+ return 0;
+ case OP_STR_NOT_REGEXP:
+ neg = TRUE;
+ /* fall through */
+ case OP_STR_REGEXP: {
+ int ec;
+ bool res;
+ regex_t reg;
+ if ((ec = regcomp(&reg, rhs, REG_EXTENDED)) != 0) {
+ size_t siz;
+ char *errbuf;
+ siz = regerror(ec, &reg, NULL, 0);
+ errbuf = t_malloc_no0(siz);
+ (void)regerror(ec, &reg, errbuf, siz);
+ *error_r = t_strdup_printf("if: regex failed: %s",
+ errbuf);
+ return -1;
+ }
+ if ((ec = regexec(&reg, lhs, 0, 0, 0)) != 0) {
+ i_assert(ec == REG_NOMATCH);
+ res = FALSE;
+ } else {
+ res = TRUE;
+ }
+ regfree(&reg);
+ /* this should be same as neg.
+ if NOT_REGEXP, neg == TRUE and res should be FALSE
+ if REGEXP, ned == FALSE, and res should be TRUE
+ */
+ *result_r = res != neg;
+ return 0;
+ }
+ default:
+ i_panic("Missing generic comparator %u", op);
+ }
+}
+
+int var_expand_if(struct var_expand_context *ctx,
+ const char *key, const char *field,
+ const char **result_r, const char **error_r)
+{
+ /* in case the original input had :, we need to fix that
+ by concatenating the key and field together. */
+ const char *input = t_strconcat(key, ":", field, NULL);
+ const char *p = strchr(input, ';');
+ const char *par_end;
+ string_t *parbuf;
+ const char *const *parms;
+ unsigned int depth = 0;
+ int ret;
+ bool result, escape = FALSE, maybe_var = FALSE;
+
+ if (p == NULL) {
+ *error_r = "if: missing parameter(s)";
+ return -1;
+ }
+ ARRAY_TYPE(const_string) params;
+ t_array_init(&params, 6);
+
+ parbuf = t_str_new(64);
+ /* we need to skip any %{} parameters here, so we can split the string
+ correctly from , without breaking any inner expansions */
+ for(par_end = p+1; *par_end != '\0'; par_end++) {
+ if (*par_end == '\\') {
+ escape = TRUE;
+ continue;
+ } else if (escape) {
+ str_append_c(parbuf, *par_end);
+ escape = FALSE;
+ continue;
+ }
+ if (*par_end == '%') {
+ maybe_var = TRUE;
+ } else if (maybe_var && *par_end == '{') {
+ depth++;
+ maybe_var = FALSE;
+ } else if (depth > 0 && *par_end == '}') {
+ depth--;
+ } else if (depth == 0 && *par_end == ';') {
+ const char *par = str_c(parbuf);
+ array_push_back(&params, &par);
+ parbuf = t_str_new(64);
+ continue;
+ /* if there is a unescaped : at top level it means
+ that the key + arguments end here. it's probably
+ a by-product of the t_strconcat at top of function,
+ which is best handled here. */
+ } else if (depth == 0 && *par_end == ':') {
+ break;
+ }
+ str_append_c(parbuf, *par_end);
+ }
+
+ if (str_len(parbuf) > 0) {
+ const char *par = str_c(parbuf);
+ array_push_back(&params, &par);
+ }
+
+ if (array_count(&params) != 5) {
+ if (array_count(&params) == 4) {
+ const char *empty = "";
+ array_push_back(&params, &empty);
+ } else {
+ *error_r = t_strdup_printf("if: requires four or five parameters, got %u",
+ array_count(&params));
+ return -1;
+ }
+ }
+
+ array_append_zero(&params);
+ parms = array_front(&params);
+ t_array_init(&params, 6);
+
+ for(;*parms != NULL; parms++) {
+ /* expand the parameters */
+ string_t *param = t_str_new(64);
+ if ((ret = var_expand_with_funcs(param, *parms, ctx->table,
+ ctx->func_table, ctx->context,
+ error_r)) <= 0) {
+ return ret;
+ }
+ const char *p = str_c(param);
+ array_push_back(&params, &p);
+ }
+
+ i_assert(array_count(&params) == 5);
+
+ /* execute comparison */
+ const char *const *args = array_front(&params);
+ if (var_expand_if_comp(args[0], args[1], args[2], &result, error_r)<0)
+ return -1;
+ *result_r = result ? args[3] : args[4];
+ return 1;
+}
+
diff --git a/src/lib/var-expand-private.h b/src/lib/var-expand-private.h
new file mode 100644
index 0000000..f02356a
--- /dev/null
+++ b/src/lib/var-expand-private.h
@@ -0,0 +1,56 @@
+#ifndef VAR_EXPAND_PRIVATE_H
+#define VAR_EXPAND_PRIVATE_H 1
+
+struct var_expand_context {
+ /* current variables */
+ const struct var_expand_table *table;
+ /* caller provided function table */
+ const struct var_expand_func_table *func_table;
+ /* caller provided context */
+ void *context;
+ /* last offset, negative counts from end*/
+ int offset;
+ /* last width, negative counts from end */
+ int width;
+ /* last zero padding */
+ bool zero_padding:1;
+};
+
+/* this can be used to register a *global* function that is
+ prepended to function table. These can be used to register some
+ special handling for keys.
+
+ you can call var_expand_with_funcs if you need to
+ expand something inside here.
+
+ return -1 on error, 0 on unknown variable, 1 on success
+*/
+typedef int
+var_expand_extension_func_t(struct var_expand_context *ctx,
+ const char *key, const char *field,
+ const char **result_r, const char **error_r);
+
+struct var_expand_extension_func_table {
+ const char *key;
+ var_expand_extension_func_t *func;
+};
+
+int var_expand_long(struct var_expand_context *ctx,
+ const void *key_start, size_t key_len,
+ const char **var_r, const char **error_r);
+
+void var_expand_extensions_init(void);
+void var_expand_extensions_deinit(void);
+
+/* Functions registered here are placed before in-built functions,
+ so you can include your own implementation of something.
+ Be careful. Use NULL terminated list.
+*/
+void var_expand_register_func_array(const struct var_expand_extension_func_table *funcs);
+void var_expand_unregister_func_array(const struct var_expand_extension_func_table *funcs);
+
+int var_expand_if(struct var_expand_context *ctx,
+ const char *key, const char *field,
+ const char **result_r, const char **error_r);
+
+#endif
diff --git a/src/lib/var-expand.c b/src/lib/var-expand.c
new file mode 100644
index 0000000..7ddaec0
--- /dev/null
+++ b/src/lib/var-expand.c
@@ -0,0 +1,833 @@
+/* Copyright (c) 2003-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "md5.h"
+#include "hash.h"
+#include "hex-binary.h"
+#include "base64.h"
+#include "hostpid.h"
+#include "hmac.h"
+#include "pkcs5.h"
+#include "hash-method.h"
+#include "str.h"
+#include "strescape.h"
+#include "var-expand.h"
+#include "var-expand-private.h"
+
+#include <unistd.h>
+#include <ctype.h>
+
+#define TABLE_LAST(t) \
+ ((t)->key == '\0' && (t)->long_key == NULL)
+
+struct var_expand_modifier {
+ char key;
+ const char *(*func)(const char *, struct var_expand_context *);
+};
+
+static ARRAY(struct var_expand_extension_func_table) var_expand_extensions;
+
+static const char *
+m_str_lcase(const char *str, struct var_expand_context *ctx ATTR_UNUSED)
+{
+ return t_str_lcase(str);
+}
+
+static const char *
+m_str_ucase(const char *str, struct var_expand_context *ctx ATTR_UNUSED)
+{
+ return t_str_ucase(str);
+}
+
+static const char *
+m_str_escape(const char *str, struct var_expand_context *ctx ATTR_UNUSED)
+{
+ return str_escape(str);
+}
+
+static const char *
+m_str_hex(const char *str, struct var_expand_context *ctx ATTR_UNUSED)
+{
+ unsigned long long l;
+
+ if (str_to_ullong(str, &l) < 0)
+ l = 0;
+ return t_strdup_printf("%llx", l);
+}
+
+static const char *
+m_str_reverse(const char *str, struct var_expand_context *ctx ATTR_UNUSED)
+{
+ size_t len = strlen(str);
+ char *p, *rev;
+
+ rev = t_malloc_no0(len + 1);
+ rev[len] = '\0';
+
+ for (p = rev + len - 1; *str != '\0'; str++)
+ *p-- = *str;
+ return rev;
+}
+
+static const char *m_str_hash(const char *str, struct var_expand_context *ctx)
+{
+ unsigned int value = str_hash(str);
+ string_t *hash = t_str_new(20);
+
+ if (ctx->width != 0) {
+ value %= ctx->width;
+ ctx->width = 0;
+ }
+
+ str_printfa(hash, "%x", value);
+ while ((int)str_len(hash) < ctx->offset)
+ str_insert(hash, 0, "0");
+ ctx->offset = 0;
+
+ return str_c(hash);
+}
+
+static const char *
+m_str_newhash(const char *str, struct var_expand_context *ctx)
+{
+ string_t *hash = t_str_new(20);
+ unsigned char result[MD5_RESULTLEN];
+ unsigned int i;
+ uint64_t value = 0;
+
+ md5_get_digest(str, strlen(str), result);
+ for (i = 0; i < sizeof(value); i++) {
+ value <<= 8;
+ value |= result[i];
+ }
+
+ if (ctx->width != 0) {
+ value %= ctx->width;
+ ctx->width = 0;
+ }
+
+ str_printfa(hash, "%x", (unsigned int)value);
+ while ((int)str_len(hash) < ctx->offset)
+ str_insert(hash, 0, "0");
+ ctx->offset = 0;
+
+ return str_c(hash);
+}
+
+static const char *
+m_str_md5(const char *str, struct var_expand_context *ctx ATTR_UNUSED)
+{
+ unsigned char digest[16];
+
+ md5_get_digest(str, strlen(str), digest);
+
+ return binary_to_hex(digest, sizeof(digest));
+}
+
+static const char *
+m_str_ldap_dn(const char *str, struct var_expand_context *ctx ATTR_UNUSED)
+{
+ string_t *ret = t_str_new(256);
+
+ while (*str != '\0') {
+ if (*str == '.')
+ str_append(ret, ",dc=");
+ else
+ str_append_c(ret, *str);
+ str++;
+ }
+
+ return str_free_without_data(&ret);
+}
+
+static const char *
+m_str_trim(const char *str, struct var_expand_context *ctx ATTR_UNUSED)
+{
+ size_t len;
+
+ len = strlen(str);
+ while (len > 0 && i_isspace(str[len-1]))
+ len--;
+ return t_strndup(str, len);
+}
+
+#define MAX_MODIFIER_COUNT 10
+static const struct var_expand_modifier modifiers[] = {
+ { 'L', m_str_lcase },
+ { 'U', m_str_ucase },
+ { 'E', m_str_escape },
+ { 'X', m_str_hex },
+ { 'R', m_str_reverse },
+ { 'H', m_str_hash },
+ { 'N', m_str_newhash },
+ { 'M', m_str_md5 },
+ { 'D', m_str_ldap_dn },
+ { 'T', m_str_trim },
+ { '\0', NULL }
+};
+
+static int
+var_expand_short(const struct var_expand_table *table, char key,
+ const char **var_r, const char **error_r)
+{
+ const struct var_expand_table *t;
+
+ if (table != NULL) {
+ for (t = table; !TABLE_LAST(t); t++) {
+ if (t->key == key) {
+ *var_r = t->value != NULL ? t->value : "";
+ return 1;
+ }
+ }
+ }
+
+ /* not found */
+ if (key == '%') {
+ *var_r = "%";
+ return 1;
+ }
+ if (*error_r == NULL)
+ *error_r = t_strdup_printf("Unknown variable '%%%c'", key);
+ *var_r = t_strdup_printf("UNSUPPORTED_VARIABLE_%c", key);
+ return 0;
+}
+
+static int
+var_expand_hash(struct var_expand_context *ctx,
+ const char *key, const char *field,
+ const char **result_r, const char **error_r)
+{
+ enum {
+ FORMAT_HEX,
+ FORMAT_HEX_UC,
+ FORMAT_BASE64
+ } format = FORMAT_HEX;
+
+ const char *p = strchr(key, ';');
+ const char *const *args = NULL;
+ const char *algo = key;
+ const char *value;
+ int ret;
+
+ if (p != NULL) {
+ algo = t_strcut(key, ';');
+ args = t_strsplit(p+1, ",");
+ }
+
+ const struct hash_method *method;
+ if (strcmp(algo, "pkcs5") == 0) {
+ method = hash_method_lookup("sha256");
+ } else if ((method = hash_method_lookup(algo)) == NULL) {
+ return 0;
+ }
+
+ string_t *field_value = t_str_new(64);
+ string_t *salt = t_str_new(64);
+ string_t *tmp = t_str_new(method->digest_size);
+
+ if ((ret = var_expand_long(ctx, field, strlen(field),
+ &value, error_r)) < 1) {
+ return ret;
+ }
+
+ str_append(field_value, value);
+
+ /* default values */
+ unsigned int rounds = 1;
+ unsigned int truncbits = 0;
+
+ if (strcmp(algo, "pkcs5") == 0) {
+ rounds = 2048;
+ str_append(salt, field);
+ }
+
+ while(args != NULL && *args != NULL) {
+ const char *k = t_strcut(*args, '=');
+ const char *value = strchr(*args, '=');
+ if (value == NULL) {
+ args++;
+ continue;
+ } else {
+ value++;
+ }
+ if (strcmp(k, "rounds") == 0) {
+ if (str_to_uint(value, &rounds)<0) {
+ *error_r = t_strdup_printf(
+ "Cannot parse hash arguments:"
+ "'%s' is not number for rounds",
+ value);
+ return -1;
+ }
+ if (rounds < 1) {
+ *error_r = t_strdup_printf(
+ "Cannot parse hash arguments:"
+ "rounds must be at least 1");
+ return -1;
+ }
+ } else if (strcmp(k, "truncate") == 0) {
+ if (str_to_uint(value, &truncbits)<0) {
+ *error_r = t_strdup_printf(
+ "Cannot parse hash arguments:"
+ "'%s' is not number for truncbits",
+ value);
+ return -1;
+ }
+ truncbits = I_MIN(truncbits, method->digest_size*8);
+ } else if (strcmp(k, "salt") == 0) {
+ str_truncate(salt, 0);
+ if (var_expand_with_funcs(salt, value, ctx->table,
+ ctx->func_table, ctx->context,
+ error_r) < 0) {
+ return -1;
+ }
+ break;
+ } else if (strcmp(k, "format") == 0) {
+ if (strcmp(value, "hex") == 0) {
+ format = FORMAT_HEX;
+ } else if (strcmp(value, "hexuc") == 0){
+ format = FORMAT_HEX_UC;
+ } else if (strcmp(value, "base64") == 0) {
+ format = FORMAT_BASE64;
+ } else {
+ *error_r = t_strdup_printf(
+ "Cannot parse hash arguments:"
+ "'%s' is not supported format",
+ value);
+ return -1;
+ }
+ }
+ args++;
+ }
+
+ str_truncate(tmp, 0);
+
+ if (strcmp(algo, "pkcs5") == 0) {
+ if (pkcs5_pbkdf(PKCS5_PBKDF2, method,
+ field_value->data, field_value->used,
+ salt->data, salt->used,
+ rounds, HMAC_MAX_CONTEXT_SIZE, tmp) != 0) {
+ *error_r = "Cannot hash: PKCS5_PBKDF2 failed";
+ return -1;
+ }
+ } else {
+ void *context = t_malloc_no0(method->context_size);
+
+ str_append_str(tmp, field_value);
+
+ for(;rounds>0;rounds--) {
+ method->init(context);
+ if (salt->used > 0)
+ method->loop(context, salt->data, salt->used);
+ method->loop(context, tmp->data, tmp->used);
+ unsigned char *digest =
+ buffer_get_modifiable_data(tmp, NULL);
+ method->result(context, digest);
+ if (tmp->used != method->digest_size)
+ buffer_set_used_size(tmp, method->digest_size);
+ }
+ }
+
+ if (truncbits > 0)
+ buffer_truncate_rshift_bits(tmp, truncbits);
+
+ switch(format) {
+ case FORMAT_HEX:
+ *result_r = binary_to_hex(tmp->data, tmp->used);
+ return 1;
+ case FORMAT_HEX_UC:
+ *result_r = binary_to_hex(tmp->data, tmp->used);
+ return 1;
+ case FORMAT_BASE64: {
+ string_t *dest = t_str_new(64);
+ base64_encode(tmp->data, tmp->used, dest);
+ *result_r = str_c(dest);
+ return 1;
+ }
+ }
+
+ i_unreached();
+}
+
+static int
+var_expand_func(const struct var_expand_func_table *func_table,
+ const char *key, const char *data, void *context,
+ const char **var_r, const char **error_r)
+{
+ const char *value = NULL;
+ int ret;
+
+ if (strcmp(key, "env") == 0) {
+ value = getenv(data);
+ *var_r = value != NULL ? value : "";
+ return 1;
+ }
+ if (func_table != NULL) {
+ for (; func_table->key != NULL; func_table++) {
+ if (strcmp(func_table->key, key) == 0) {
+ ret = func_table->func(data, context, &value, error_r);
+ *var_r = value != NULL ? value : "";
+ return ret;
+ }
+ }
+ }
+ if (*error_r == NULL)
+ *error_r = t_strdup_printf("Unknown variable '%%%s'", key);
+ *var_r = t_strdup_printf("UNSUPPORTED_VARIABLE_%s", key);
+ return 0;
+}
+
+static int
+var_expand_try_extension(struct var_expand_context *ctx,
+ const char *key, const char *data,
+ const char **var_r, const char **error_r)
+{
+ int ret;
+ const char *sep = strchr(key, ';');
+
+ if (sep == NULL) sep = key + strlen(key);
+
+ /* try with extensions */
+ const struct var_expand_extension_func_table *f;
+ array_foreach(&var_expand_extensions, f) {
+ /* ensure we won't match abbreviations */
+ size_t len = sep-key;
+ if (strncasecmp(key, f->key, len) == 0 && f->key[len] == '\0')
+ return f->func(ctx, key, data, var_r, error_r);
+ }
+ if ((ret = var_expand_func(ctx->func_table, key, data,
+ ctx->context, var_r, error_r)) == 0) {
+ *error_r = t_strdup_printf("Unknown variable '%%%s'", key);
+ }
+ return ret;
+}
+
+
+int
+var_expand_long(struct var_expand_context *ctx,
+ const void *key_start, size_t key_len,
+ const char **var_r, const char **error_r)
+{
+ const struct var_expand_table *t;
+ const char *key, *value = NULL;
+ int ret = 1;
+
+ if (ctx->table != NULL) {
+ for (t = ctx->table; !TABLE_LAST(t); t++) {
+ if (t->long_key != NULL &&
+ strncmp(t->long_key, key_start, key_len) == 0 &&
+ t->long_key[key_len] == '\0') {
+ *var_r = t->value != NULL ? t->value : "";
+ return 1;
+ }
+ }
+ }
+ key = t_strndup(key_start, key_len);
+
+ /* built-in variables: */
+ switch (key_len) {
+ case 3:
+ if (strcmp(key, "pid") == 0)
+ value = my_pid;
+ else if (strcmp(key, "uid") == 0)
+ value = dec2str(geteuid());
+ else if (strcmp(key, "gid") == 0)
+ value = dec2str(getegid());
+ break;
+ case 8:
+ if (strcmp(key, "hostname") == 0)
+ value = my_hostname;
+ break;
+ }
+
+ if (value == NULL) {
+ const char *data = strchr(key, ':');
+
+ if (data != NULL)
+ key = t_strdup_until(key, data++);
+ else
+ data = "";
+
+ ret = var_expand_try_extension(ctx, key, data, &value, error_r);
+
+ if (ret <= 0 && value == NULL) {
+ value = "";
+ }
+ }
+ *var_r = value;
+ return ret;
+}
+
+int var_expand_with_funcs(string_t *dest, const char *str,
+ const struct var_expand_table *table,
+ const struct var_expand_func_table *func_table,
+ void *context, const char **error_r)
+{
+ const struct var_expand_modifier *m;
+ const char *var;
+ struct var_expand_context ctx;
+ const char *(*modifier[MAX_MODIFIER_COUNT])
+ (const char *, struct var_expand_context *);
+ const char *end;
+ unsigned int i, modifier_count;
+ size_t len;
+ int ret, final_ret = 1;
+
+ *error_r = NULL;
+
+ i_zero(&ctx);
+ ctx.table = table;
+ ctx.func_table = func_table;
+ ctx.context = context;
+
+ for (; *str != '\0'; str++) {
+ if (*str != '%')
+ str_append_c(dest, *str);
+ else {
+ int sign = 1;
+
+ str++;
+
+ /* reset per-field modifiers */
+ ctx.offset = 0;
+ ctx.width = 0;
+ ctx.zero_padding = FALSE;
+
+ /* [<offset>.]<width>[<modifiers>]<variable> */
+ if (*str == '-') {
+ sign = -1;
+ str++;
+ }
+ if (*str == '0') {
+ ctx.zero_padding = TRUE;
+ str++;
+ }
+ while (*str >= '0' && *str <= '9') {
+ ctx.width = ctx.width*10 + (*str - '0');
+ str++;
+ }
+
+ if (*str == '.') {
+ ctx.offset = sign * ctx.width;
+ sign = 1;
+ ctx.width = 0;
+ str++;
+
+ /* if offset was prefixed with zero (or it was
+ plain zero), just ignore that. zero padding
+ is done with the width. */
+ ctx.zero_padding = FALSE;
+ if (*str == '0') {
+ ctx.zero_padding = TRUE;
+ str++;
+ }
+ if (*str == '-') {
+ sign = -1;
+ str++;
+ }
+
+ while (*str >= '0' && *str <= '9') {
+ ctx.width = ctx.width*10 + (*str - '0');
+ str++;
+ }
+ ctx.width = sign * ctx.width;
+ }
+
+ modifier_count = 0;
+ while (modifier_count < MAX_MODIFIER_COUNT) {
+ modifier[modifier_count] = NULL;
+ for (m = modifiers; m->key != '\0'; m++) {
+ if (m->key == *str) {
+ /* @UNSAFE */
+ modifier[modifier_count] =
+ m->func;
+ str++;
+ break;
+ }
+ }
+ if (modifier[modifier_count] == NULL)
+ break;
+ modifier_count++;
+ }
+
+ if (*str == '\0')
+ break;
+
+ var = NULL;
+ if (*str == '{' && strchr(str, '}') != NULL) {
+ /* %{long_key} */
+ unsigned int ctr = 1;
+ bool escape = FALSE;
+ end = str;
+ while(*++end != '\0' && ctr > 0) {
+ if (!escape && *end == '\\') {
+ escape = TRUE;
+ continue;
+ }
+ if (escape) {
+ escape = FALSE;
+ continue;
+ }
+ if (*end == '{') ctr++;
+ if (*end == '}') ctr--;
+ }
+ if (ctr == 0)
+ /* it needs to come back a bit */
+ end--;
+ /* if there is no } it will consume rest of the
+ string */
+ len = end - (str + 1);
+ ret = var_expand_long(&ctx, str+1, len,
+ &var, error_r);
+ str = end;
+ } else {
+ ret = var_expand_short(ctx.table, *str,
+ &var, error_r);
+ }
+ i_assert(var != NULL);
+
+ if (final_ret > ret)
+ final_ret = ret;
+
+ if (ret <= 0)
+ str_append(dest, var);
+ else {
+ for (i = 0; i < modifier_count; i++)
+ var = modifier[i](var, &ctx);
+
+ if (ctx.offset < 0) {
+ /* if offset is < 0 then we want to
+ start at the end */
+ size_t len = strlen(var);
+ size_t offset_from_end = -ctx.offset;
+
+ if (len > offset_from_end)
+ var += len - offset_from_end;
+ } else {
+ while (*var != '\0' && ctx.offset > 0) {
+ ctx.offset--;
+ var++;
+ }
+ }
+ if (ctx.width == 0)
+ str_append(dest, var);
+ else if (!ctx.zero_padding) {
+ if (ctx.width < 0)
+ ctx.width = strlen(var) - (-ctx.width);
+ str_append_max(dest, var, ctx.width);
+ } else {
+ /* %05d -like padding. no truncation. */
+ ssize_t len = strlen(var);
+ while (len < ctx.width) {
+ str_append_c(dest, '0');
+ ctx.width--;
+ }
+ str_append(dest, var);
+ }
+ }
+ }
+ }
+ return final_ret;
+}
+
+int var_expand(string_t *dest, const char *str,
+ const struct var_expand_table *table, const char **error_r)
+{
+ return var_expand_with_funcs(dest, str, table, NULL, NULL, error_r);
+}
+
+static bool
+var_get_key_range_full(const char *str, unsigned int *idx_r,
+ unsigned int *size_r)
+{
+ const struct var_expand_modifier *m;
+ unsigned int i = 0;
+
+ /* [<offset>.]<width>[<modifiers>]<variable> */
+ while ((str[i] >= '0' && str[i] <= '9') || str[i] == '-')
+ i++;
+
+ if (str[i] == '.') {
+ i++;
+ while ((str[i] >= '0' && str[i] <= '9') || str[i] == '-')
+ i++;
+ }
+
+ do {
+ for (m = modifiers; m->key != '\0'; m++) {
+ if (m->key == str[i]) {
+ i++;
+ break;
+ }
+ }
+ } while (m->key != '\0');
+
+ if (str[i] != '{') {
+ /* short key */
+ *idx_r = i;
+ *size_r = str[i] == '\0' ? 0 : 1;
+ return FALSE;
+ } else {
+ unsigned int depth = 1;
+ bool escape = FALSE;
+ /* long key */
+ *idx_r = ++i;
+ for (; str[i] != '\0'; i++) {
+ if (!escape && str[i] == '\\') {
+ escape = TRUE;
+ continue;
+ }
+ if (escape) {
+ escape = FALSE;
+ continue;
+ }
+ if (str[i] == '{')
+ depth++;
+ if (str[i] == '}') {
+ if (--depth==0)
+ break;
+ }
+ }
+ *size_r = i - *idx_r;
+ return TRUE;
+ }
+}
+
+char var_get_key(const char *str)
+{
+ unsigned int idx, size;
+
+ if (var_get_key_range_full(str, &idx, &size))
+ return '{';
+ return str[idx];
+}
+
+void var_get_key_range(const char *str, unsigned int *idx_r,
+ unsigned int *size_r)
+{
+ (void)var_get_key_range_full(str, idx_r, size_r);
+}
+
+static bool var_has_long_key(const char **str, const char *long_key)
+{
+ const char *start, *end;
+
+ start = strchr(*str, '{');
+ i_assert(start != NULL);
+
+ end = strchr(++start, '}');
+ if (end == NULL)
+ return FALSE;
+
+ if (strncmp(start, long_key, end-start) == 0 &&
+ long_key[end-start] == '\0')
+ return TRUE;
+
+ *str = end;
+ return FALSE;
+}
+
+bool var_has_key(const char *str, char key, const char *long_key)
+{
+ char c;
+
+ for (; *str != '\0'; str++) {
+ if (*str == '%' && str[1] != '\0') {
+ str++;
+ c = var_get_key(str);
+ if (c == key && key != '\0')
+ return TRUE;
+
+ if (c == '{' && long_key != NULL) {
+ if (var_has_long_key(&str, long_key))
+ return TRUE;
+ }
+ }
+ }
+ return FALSE;
+}
+
+void var_expand_extensions_deinit(void)
+{
+ array_free(&var_expand_extensions);
+}
+
+void var_expand_extensions_init(void)
+{
+ i_array_init(&var_expand_extensions, 32);
+
+ /* put all hash methods there */
+ for(const struct hash_method **meth = hash_methods;
+ *meth != NULL;
+ meth++) {
+ struct var_expand_extension_func_table *func =
+ array_append_space(&var_expand_extensions);
+ func->key = (*meth)->name;
+ func->func = var_expand_hash;
+ }
+
+ /* pkcs5 */
+ struct var_expand_extension_func_table *func =
+ array_append_space(&var_expand_extensions);
+ func->key = "pkcs5";
+ func->func = var_expand_hash;
+
+ /* if */
+ func = array_append_space(&var_expand_extensions);
+ func->key = "if";
+ func->func = var_expand_if;
+}
+
+void
+var_expand_register_func_array(const struct var_expand_extension_func_table *funcs)
+{
+ for(const struct var_expand_extension_func_table *ptr = funcs;
+ ptr->key != NULL;
+ ptr++) {
+ i_assert(*ptr->key != '\0');
+ array_push_front(&var_expand_extensions, ptr);
+ }
+}
+
+void
+var_expand_unregister_func_array(const struct var_expand_extension_func_table *funcs)
+{
+ for(const struct var_expand_extension_func_table *ptr = funcs;
+ ptr->key != NULL;
+ ptr++) {
+ i_assert(ptr->func != NULL);
+ for(unsigned int i = 0; i < array_count(&var_expand_extensions); i++) {
+ const struct var_expand_extension_func_table *func =
+ array_idx(&var_expand_extensions, i);
+ if (strcasecmp(func->key, ptr->key) == 0) {
+ array_delete(&var_expand_extensions, i, 1);
+ }
+ }
+ }
+}
+
+struct var_expand_table *
+var_expand_merge_tables(pool_t pool, const struct var_expand_table *a,
+ const struct var_expand_table *b)
+{
+ ARRAY(struct var_expand_table) table;
+ size_t a_size = var_expand_table_size(a);
+ size_t b_size = var_expand_table_size(b);
+ p_array_init(&table, pool, a_size + b_size + 1);
+ for(size_t i=0; i<a_size; i++) {
+ struct var_expand_table *entry =
+ array_append_space(&table);
+ entry->key = a[i].key;
+ entry->value = p_strdup(pool, a[i].value);
+ entry->long_key = p_strdup(pool, a[i].long_key);
+ }
+ for(size_t i=0; i<b_size; i++) {
+ struct var_expand_table *entry =
+ array_append_space(&table);
+ entry->key = b[i].key;
+ entry->value = p_strdup(pool, b[i].value);
+ entry->long_key = p_strdup(pool, b[i].long_key);
+ }
+ array_append_zero(&table);
+ return array_front_modifiable(&table);
+}
diff --git a/src/lib/var-expand.h b/src/lib/var-expand.h
new file mode 100644
index 0000000..c8485a4
--- /dev/null
+++ b/src/lib/var-expand.h
@@ -0,0 +1,60 @@
+#ifndef VAR_EXPAND_H
+#define VAR_EXPAND_H
+
+struct var_expand_table {
+ char key;
+ const char *value;
+ const char *long_key;
+};
+
+struct var_expand_func_table {
+ const char *key;
+ /* %{key:data}, or data is "" with %{key}.
+ Returns 1 on success, 0 if data is invalid, -1 on temporary error. */
+ int (*func)(const char *data, void *context,
+ const char **value_r, const char **error_r);
+};
+
+/* Expand % variables in src and append the string in dest.
+ table must end with key = 0. Returns 1 on success, 0 if the format string
+ contained invalid/unknown %variables, -1 if one of the functions returned
+ temporary error. Even in case of errors the dest string is still written as
+ fully as possible. */
+int var_expand(string_t *dest, const char *str,
+ const struct var_expand_table *table,
+ const char **error_r);
+/* Like var_expand(), but support also callback functions for
+ variable expansion. */
+int var_expand_with_funcs(string_t *dest, const char *str,
+ const struct var_expand_table *table,
+ const struct var_expand_func_table *func_table,
+ void *func_context, const char **error_r) ATTR_NULL(3, 4, 5);
+
+/* Returns the actual key character for given string, ie. skip any modifiers
+ that are before it. The string should be the data after the '%' character.
+ For %{long_variable}, '{' is returned. */
+char var_get_key(const char *str) ATTR_PURE;
+/* Similar to var_get_key(), but works for long keys as well. For single char
+ keys size=1, while for e.g. %{key} size=3 and idx points to 'k'. */
+void var_get_key_range(const char *str, unsigned int *idx_r,
+ unsigned int *size_r);
+/* Returns TRUE if key variable is used in the string.
+ If key is '\0', it's ignored. If long_key is NULL, it's ignored. */
+bool var_has_key(const char *str, char key, const char *long_key) ATTR_PURE;
+
+static inline size_t ATTR_PURE
+var_expand_table_size(const struct var_expand_table *table)
+{
+ size_t n = 0;
+ while(table != NULL && (table[n].key != '\0' ||
+ table[n].long_key != NULL))
+ n++;
+ return n;
+}
+
+struct var_expand_table *
+var_expand_merge_tables(pool_t pool, const struct var_expand_table *a,
+ const struct var_expand_table *b);
+#define t_var_expand_merge_tables(a, b) \
+ (const struct var_expand_table *)var_expand_merge_tables(pool_datastack_create(), (a), (b))
+#endif
diff --git a/src/lib/wildcard-match.c b/src/lib/wildcard-match.c
new file mode 100644
index 0000000..c1e2b2e
--- /dev/null
+++ b/src/lib/wildcard-match.c
@@ -0,0 +1,105 @@
+/*
+ * This code would not have been possible without the prior work and
+ * suggestions of various sourced. Special thanks to Robey for
+ * all his time/help tracking down bugs and his ever-helpful advice.
+ *
+ * 04/09: Fixed the "*\*" against "*a" bug (caused an endless loop)
+ *
+ * Chris Fuller (aka Fred1@IRC & Fwitz@IRC)
+ * crf@cfox.bchs.uh.edu
+ *
+ * I hereby release this code into the public domain
+ *
+ */
+
+#include "lib.h"
+#include "wildcard-match.h"
+
+#include <ctype.h>
+
+#define WILDS '*' /* matches 0 or more characters (including spaces) */
+#define WILDQ '?' /* matches exactly one character */
+
+#define NOMATCH 0
+#define MATCH (match+sofar)
+
+static int wildcard_match_int(const char *data, const char *mask, bool icase)
+{
+ const char *ma = mask, *na = data, *lsm = NULL, *lsn = NULL;
+ int match = 1;
+ int sofar = 0;
+
+ if (na[0] == '\0') {
+ /* empty string can match only "*" wildcard(s) */
+ while (ma[0] == '*') ma++;
+ return ma[0] == '\0' ? MATCH : NOMATCH;
+ }
+ /* find the end of each string */
+ while (*(mask++) != '\0');
+ mask-=2;
+ while (*(data++) != '\0');
+ data-=2;
+
+ while (data >= na) {
+ /* If the mask runs out of chars before the string, fall back on
+ * a wildcard or fail. */
+ if (mask < ma) {
+ if (lsm != NULL) {
+ data = --lsn;
+ mask = lsm;
+ if (data < na)
+ lsm = NULL;
+ sofar = 0;
+ }
+ else
+ return NOMATCH;
+ }
+
+ switch (*mask) {
+ case WILDS: /* Matches anything */
+ do
+ mask--; /* Zap redundant wilds */
+ while ((mask >= ma) && (*mask == WILDS));
+ lsm = mask;
+ lsn = data;
+ match += sofar;
+ sofar = 0; /* Update fallback pos */
+ if (mask < ma)
+ return MATCH;
+ continue; /* Next char, please */
+ case WILDQ:
+ mask--;
+ data--;
+ continue; /* '?' always matches */
+ }
+ if (icase ? (i_toupper(*mask) == i_toupper(*data)) :
+ (*mask == *data)) { /* If matching char */
+ mask--;
+ data--;
+ sofar++; /* Tally the match */
+ continue; /* Next char, please */
+ }
+ if (lsm != NULL) { /* To to fallback on '*' */
+ data = --lsn;
+ mask = lsm;
+ if (data < na)
+ lsm = NULL; /* Rewind to saved pos */
+ sofar = 0;
+ continue; /* Next char, please */
+ }
+ return NOMATCH; /* No fallback=No match */
+ }
+ while ((mask >= ma) && (*mask == WILDS))
+ mask--; /* Zap leftover %s & *s */
+ return (mask >= ma) ? NOMATCH : MATCH; /* Start of both = match */
+}
+
+bool wildcard_match(const char *data, const char *mask)
+{
+ return wildcard_match_int(data, mask, FALSE) != 0;
+}
+
+bool wildcard_match_icase(const char *data, const char *mask)
+{
+ return wildcard_match_int(data, mask, TRUE) != 0;
+}
diff --git a/src/lib/wildcard-match.h b/src/lib/wildcard-match.h
new file mode 100644
index 0000000..bfe6076
--- /dev/null
+++ b/src/lib/wildcard-match.h
@@ -0,0 +1,15 @@
+#ifndef WILDCARD_MATCH_H
+#define WILDCARD_MATCH_H
+
+/* Returns TRUE if mask matches data. mask can contain '*' and '?' wildcards. */
+bool wildcard_match(const char *data, const char *mask);
+/* Like wildcard_match(), but match ASCII characters case-insensitively. */
+bool wildcard_match_icase(const char *data, const char *mask);
+
+/* Returns TRUE if mask does *not* contain any '*' or '?' wildcards. */
+static inline bool wildcard_is_literal(const char *mask)
+{
+ return strpbrk(mask, "*?") == NULL;
+}
+
+#endif
diff --git a/src/lib/write-full.c b/src/lib/write-full.c
new file mode 100644
index 0000000..ded8d40
--- /dev/null
+++ b/src/lib/write-full.c
@@ -0,0 +1,54 @@
+/* Copyright (c) 2002-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "write-full.h"
+
+#include <unistd.h>
+
+int write_full(int fd, const void *data, size_t size)
+{
+ ssize_t ret;
+
+ while (size > 0) {
+ ret = write(fd, data, size < SSIZE_T_MAX ? size : SSIZE_T_MAX);
+ if (unlikely(ret < 0))
+ return -1;
+
+ if (unlikely(ret == 0)) {
+ /* nothing was written, only reason for this should
+ be out of disk space */
+ errno = ENOSPC;
+ return -1;
+ }
+
+ data = CONST_PTR_OFFSET(data, ret);
+ size -= ret;
+ }
+
+ return 0;
+}
+
+int pwrite_full(int fd, const void *data, size_t size, off_t offset)
+{
+ ssize_t ret;
+
+ while (size > 0) {
+ ret = pwrite(fd, data, size < SSIZE_T_MAX ?
+ size : SSIZE_T_MAX, offset);
+ if (unlikely(ret < 0))
+ return -1;
+
+ if (unlikely(ret == 0)) {
+ /* nothing was written, only reason for this should
+ be out of disk space */
+ errno = ENOSPC;
+ return -1;
+ }
+
+ data = CONST_PTR_OFFSET(data, ret);
+ size -= ret;
+ offset += ret;
+ }
+
+ return 0;
+}
diff --git a/src/lib/write-full.h b/src/lib/write-full.h
new file mode 100644
index 0000000..ddef406
--- /dev/null
+++ b/src/lib/write-full.h
@@ -0,0 +1,10 @@
+#ifndef WRITE_FULL_H
+#define WRITE_FULL_H
+
+/* Write data into file. Returns -1 if error occurred, or 0 if all was ok.
+ If there's not enough space in device, -1 with ENOSPC is returned, and
+ it's unspecified how much data was actually written. */
+int write_full(int fd, const void *data, size_t size);
+int pwrite_full(int fd, const void *data, size_t size, off_t offset);
+
+#endif